Skip to content

model: add Kimi-K3 text model - #26185

Merged
ngxson merged 22 commits into
ggml-org:masterfrom
pwilkin:kimi-k3-text
Aug 15, 2026
Merged

model: add Kimi-K3 text model#26185
ngxson merged 22 commits into
ggml-org:masterfrom
pwilkin:kimi-k3-text

Conversation

@pwilkin

@pwilkin pwilkin commented Jul 27, 2026

Copy link
Copy Markdown
Member

Hybrid KDA (linear) + MLA (full) attention as in Kimi-Linear-48B, plus five things that architecture does not have:

  1. cross-layer residual attention (attn_res_block_size)
  2. latent MoE (routed experts run at n_expert_latent)
  3. situ activation (replaces SwiGLU everywhere)
  4. MLA output gate (sigmoid gate before o_proj)
  5. full-rank KDA gate (single ssm_g instead of ssm_g_a/ssm_g_b)

Reuses DeepSeek4's HC_PRE for the cross-layer residual weighted sum. Supports repack for the MXFP4 weights in conversion.

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: Yes, ran the conversion session with Opus.

Now need someone to actually convert and test :)

@pwilkin
pwilkin requested review from CISC and ggerganov as code owners July 27, 2026 17:52
@github-actions github-actions Bot added model Model specific conversion labels Jul 27, 2026
Comment thread conversion/kimi_k3.py Outdated

@ngxson ngxson left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

may need to shorten comments too, IMO some/most comments are too verbose

Comment thread src/models/models.h Outdated
Comment on lines +2153 to +2156
std::vector<ggml_tensor *> ckpts;
ggml_tensor * stack_cache = nullptr;
int stack_cache_n = -1;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest renaming:

  • ckpts --> resi (short for residual stream, same naming mentioned in the paper)
  • drop the _cache since technically there is no cache here, resi_stack should be enough

also, it might be cleaner if these are grouped into a new struct and explicitly pass it like this:

struct attn_resi; // private struct, defined inside cpp file
void res_push(attn_resi r, int64_t n_embd, int64_t n_tokens);
ggml_tensor * res_stack(attn_resi r, int64_t n_embd, int64_t n_tokens);

Comment thread src/models/kimi-k3.cpp Outdated
Comment on lines +98 to +104
layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A, i), {n_head}, TENSOR_NOT_REQUIRED);
if (!layer.ssm_a) {
layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A, i), {1, n_head, 1, 1}, TENSOR_NOT_REQUIRED);
}
if (!layer.ssm_a) {
layer.ssm_a = create_tensor(tn(LLM_TENSOR_SSM_A, i), {1, n_head}, 0);
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

these might not be necessary, I suppose for compat?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, those are artifacts from the mock model runs, for the real model I'll purge them.

@Green-Sky

Copy link
Copy Markdown
Collaborator

https://huggingface.co/inference-optimization/Kimi-K3-0.18B

potentially useful, depending on how faithful the model is reconstructed in 0.18B .

@pwilkin

pwilkin commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

@Green-Sky I wouldn't put it up without checking parity with a mock model 😄

@Green-Sky

Copy link
Copy Markdown
Collaborator

@Green-Sky I wouldn't put it up without checking parity with a mock model 😄

Isnt that a mock model?

@pwilkin

pwilkin commented Jul 27, 2026

Copy link
Copy Markdown
Member Author

Yeah, that's what I'm saying, already built one of my own for parity testing purposes when doing the PR.

@ngxson

ngxson commented Jul 27, 2026

Copy link
Copy Markdown
Collaborator

@GrEarl please put your comment inside a collapsible block, it takes up too much space & make the discussion hard to keep track

@GrEarl

GrEarl commented Jul 27, 2026

Copy link
Copy Markdown

Sorry for the long text. reposting it collapsed and trimmed.

Ran this branch against the actual Kimi-K3 checkpoint. One thing needs fixing; the rest is context.

LLAMA_MAX_EXPERTS is 512, Kimi-K3 has 896

src/llama-hparams.h:11    #define LLAMA_MAX_EXPERTS 512 // Qwen3 Next
src/llama-model.cpp:1117  GGML_ASSERT(hparams.n_expert <= LLAMA_MAX_EXPERTS);

Arch-generic path, right after LLM_KV_EXPERT_COUNT is read and before any arch hook. The converter itself prints gguf: expert count = 896, so a converted file aborts at load.

It cannot be worked around on the file side: understating expert_count contradicts ne[2] = 896 on the ffn_*_exps tensors.

LLAMA_MAX_EXPERTS sizes exactly one thing in the tree — cur_experts[LLAMA_MAX_EXPERTS] in build_moe_ffn (src/llama-graph.cpp:2128) — and only [0, n_expert_used) is ever touched, so 1024 costs 4 KB of stack.

--- a/src/llama-hparams.h
+++ b/src/llama-hparams.h
@@
 // bump if necessary
 #define LLAMA_MAX_LAYERS  512
-#define LLAMA_MAX_EXPERTS 512 // Qwen3 Next
+#define LLAMA_MAX_EXPERTS 1024 // Kimi-K3

Not verified: I have not confirmed a K3 GGUF actually loads with this — I do not have a machine that can hold 938 GiB. It is inferred from the assert and the single array it bounds. Whether to include it here or split it out is your call.

Dry run: prepare_tensors() completes, all 93 layers mapped, file type = 38

convert_hf_to_gguf.py --remote moonshotai/Kimi-K3 --dry-run --outtype auto, ~3 min on 8 vCPU / 32 GiB.

  • hparams read correctly: context length 1048576 / embedding length 7168 / feed forward length 33792 / head count 96 / expert count 896 / experts used count 16 / expert score gating function sigmoid
  • file type = 38 (MOSTLY_MXFP4_MOE), so the MXFP4 detection and the ftype override both take effect
  • tensor map produced for all 93 layers with no unmapped names, including ssm_g / ssm_conv1d_{q,k,v} / ssm_{a,beta,dt,f_a,f_b,norm} / attn_res_score / ffn_res_score / output_res_score / ffn_routed_{down,up,norm} / exp_probs_b, and the MLA layers' attn_{q_a,q_b,kv_a_mqa,k_b,v_b,gate}
  • the expert writing path returned without raising, so every (layer, w1|w2|w3) group had all 896 weight_packed/weight_scale pairs
  • every *_res_norm / *_res_proj pair was fused, nothing left over

I did not write a complete GGUF or load one, so none of this says anything about inference correctness.

repack_mxfp4_blocks is bit-exact — 0 mismatches over 11,010,048 values

In --dry-run the expert tensors stay lazy, so the dry run never executes repack_mxfp4_blocks. I checked it separately.

Before seeing this PR I had derived the same repack from the compressed-tensors and ggml sources. Your output is byte-identical to mine, on layers.48.block_sparse_moe.experts.0.w1 (logical [3072, 3584]), fetching 5.6 MiB via HTTP Range:

mismatched elements : 0 / 11010048
max abs error       : 0.0
bytes in/out        : 5849088 / 5849088
bpw                 : 4.2500

Reference side: compressed-tensors nvfp4/helpers.py::pack_fp4_to_uint8 (element 2i in the low nibble, sign in bit 3, kE2M1ToFloat = [0, .5, 1, 1.5, 2, 3, 4, 6]) and mx_utils.py::decompress_mx_scale (2 ** (e - 127)). ggml side: dequantize_row_mxfp4 in ggml-quants.c, kvalues_fp4 in ggml-common.h, ggml_e8m0_to_fp32_half in ggml-impl.h (bits = (x - 1) << 23 for x >= 2).

So the docstring claim — kvalues doubled, scale halved, represented value unchanged — holds exactly. E8M0 exponents on this tensor are 120..122, so the x < 2 branch is not exercised by this data. Since the diff moves DeepSeek-V4's _pack_mxfp4_blocks into base.py and aliases it back, this covers that path too.

Reproduction, from a checkout of this branch (fetches ~5.6 MiB, stores no weights):

#!/usr/bin/env python3
import json
import urllib.request

import numpy as np
import torch

from conversion.base import repack_mxfp4_blocks

REPO = "moonshotai/Kimi-K3"
SHARD = "model-00049-of-000096.safetensors"
URL = f"https://huggingface.co/{REPO}/resolve/main/{SHARD}"
E2M1 = np.array([0.0, .5, 1., 1.5, 2., 3., 4., 6.], dtype=np.float32)
KV = np.array([0, 1, 2, 3, 4, 6, 8, 12, 0, -1, -2, -3, -4, -6, -8, -12], dtype=np.int8)


def rng(a, b):
    req = urllib.request.Request(URL, headers={"Range": f"bytes={a}-{b}"})
    return urllib.request.urlopen(req).read()


n = int.from_bytes(rng(0, 7), "little")
hdr = json.loads(rng(8, 8 + n - 1))
name = next(k for k in sorted(hdr) if k.endswith("experts.0.w1.weight_packed"))


def get(k):
    s, e = hdr[k]["data_offsets"]
    return np.frombuffer(rng(8 + n + s, 8 + n + e - 1), np.uint8).reshape(hdr[k]["shape"])


packed = get(name)
scale = get(name.replace("weight_packed", "weight_scale"))
rows, cols = packed.shape[0], packed.shape[1] * 2

# reference: compressed-tensors pack_fp4_to_uint8 / decompress_mx_scale, inverted
idx = np.empty((rows, cols), np.uint8)
idx[:, 0::2], idx[:, 1::2] = packed & 0xF, packed >> 4
mag = E2M1[(idx & 7).astype(np.int32)]
ref = np.where((idx & 8) != 0, -mag, mag).reshape(rows, cols // 32, 32) \
    * np.ldexp(np.ones(scale.shape, np.float32), scale.astype(np.int32) - 127)[:, :, None]

# ggml: dequantize_row_mxfp4 on the repacked blocks
blk = repack_mxfp4_blocks(torch.from_numpy(packed.copy()),
                          torch.from_numpy(scale.copy())).reshape(rows, -1, 17)
out = np.empty((rows, blk.shape[1], 32), np.float32)
out[:, :, :16] = KV[(blk[:, :, 1:] & 0xF)]
out[:, :, 16:] = KV[(blk[:, :, 1:] >> 4)]
got = out * np.ldexp(np.ones(blk.shape[:2], np.float32),
                     blk[:, :, 0].astype(np.int32) - 128)[:, :, None]

ref, got = ref.reshape(rows, cols), got.reshape(rows, cols)
print(f"{name}  [{rows}, {cols}]")
print(f"mismatched elements : {int((ref != got).sum())} / {ref.size}")
print(f"max abs error       : {float(np.abs(ref - got).max())}")
print(f"bytes in/out        : {packed.nbytes + scale.nbytes} / {blk.nbytes}")
print(f"bpw                 : {blk.nbytes * 8 / (rows * cols):.4f}")
--remote does not fetch the tokenizer files — not a problem with this PR

This is where my dry run stopped. Relevant only if you test with --remote.

Download patterns are ["LICENSE", "*.json", "*.md", "*.txt", "tokenizer.model"]. K3 keeps its vocabulary in tiktoken.model and its tokenizer class in tokenization_kimi.py (which relative-imports encoding_k3.py), so neither arrives, and AutoTokenizer.from_pretrained(self.dir_model, trust_remote_code=True) in kimi_linear.py fails with does not appear to have a file named tokenization_kimi.py. Kimi-K2 / K2.5 reach the same call through deepseek.py.

Measured with snapshot_download: adding *.model and *.py goes from 15 files / 57.05 MiB to 33 files / 59.89 MiB (+2.84 MiB) and brings in all three. Most of the 57 MiB is model.safetensors.index.json, already pulled by *.json. self.remote_hf_model_id is available on the model instance if letting transformers resolve the code from the Hub is preferable to widening the patterns.

With a complete local snapshot, set_vocab in this PR works as-is. What I checked:

  • AutoTokenizer.from_pretrained(dir, trust_remote_code=True) returns TikTokenTokenizer
  • model._mergeable_ranks has 163,584 entries and special_tokens 256; 163,584 + 256 = 163,840 = vocab_size exactly, so no UNUSED holes
  • get_vocab_base_pre hashes to 81212dc7cdb7e0c1074ca62c5aeab0d43c9f52b8a737be7b12a777c953027890, which base.py maps to kimi-k2 — no new pre-tokenizer needed, as your comment says
  • merge reconstruction yields 163,328 entries; tokens[163584] == "[BOS]", tokens[163839] == "[PAD]"
  • tokenizer.eos_id is 163585 ([EOS]) while config.json and generation_config.json both say 163586 (<|end_of_msg|>), so the eos restore in set_vocab is doing real work

One note for anyone converting: tokenization_kimi.py imports tiktoken, and tiktoken.load.read_file requires blobfile even for a local path. Neither is in requirements/requirements-convert_hf_to_gguf.txt.

AI usage disclosure: I found this while building a quantized model with the help of AI. Because of that, some of my views here may be technically wrong, or wrong about how llama.cpp is implemented. I am also not fluent in English, so I used AI to help with the translation.

@thispwd

thispwd commented Jul 28, 2026

Copy link
Copy Markdown

Tested this branch today with a converted K3 checkpoint. Everything loaded successfully and generation worked as expected.

Conversion (conversion/kimi_k3.py)

  • Dry run correctly detected the model (896 experts, MXFP4_MOE ftype).
  • Full export from local safetensors completed cleanly, producing 2,573 tensors split across multiple output files.
  • No manual fixes were required.

Runtime

  • Running with hybrid GPU + CPU offload (-ngl 99 with -ot keeping part of the expert stacks in system RAM).
  • Output is coherent and accurate on factual Q&A.
  • Thinking segments are parsed correctly into reasoning_content via the OpenAI-compatible server.

Two observations that may help others:

  1. -ot layer placement
    When using -ot to keep only a subset of MoE experts on the GPU, avoid contiguous layer ranges. Layers are distributed evenly across devices, so configurations like "layers 0–34 on GPU" can overload the first GPU(s). Interleaving layers (e.g. every Nth layer on the GPU) results in a much more balanced memory distribution.

  2. XTML chat template
    The XTML chat template currently circulating uses namespace({...}), which Minja rejects with:

    namespace() arguments must be kwargs

    Changing it to namespace(key=value, ...) resolves the issue.

Thank you.

@usrlocalben

usrlocalben commented Jul 28, 2026

Copy link
Copy Markdown

@GrEarl 's late-edition Q2 works with the same template issue/workaround as @thispwd describes.
edit: I now see that the Q2 had its 1-of-N GGUF shard updated with an improved chat template.

There are stray template elements (or special tokens?) in the output, but the reasoning parser still detects begin/end and eos token ends the turn properly.

e.g.

<|open|>think<|sep|>The user is asking me to "tell the one about
the cat and the fire extinguisher again" — as if we've had a prior
conversation where I told a story or joke about a cat and a fire extinguisher.

---snip---

If you remember details from the original, tell me and I'll work
them in — or I can spin a completely different take.<|close|>response<|sep|><|close|>message<|sep|>

(newlines added to avoid side-scrolling)

It would be helpful if llama warmed up the experts. It doesn't anymore, or doesn't with this model. The warmup run does not force all experts on. I know this is also a problem for DSv4 warmup.

Many long stories about cats or doubly-linked list impls are needed to get 2^7*7 mmap experts warmed up.

The no-imatrix (I think I can safely assume) Q2 quant is coherent, and seems quite capable at a glance.

@csabakecskemeti

Copy link
Copy Markdown
Contributor

FYI: Here's another chat template PR on HF: https://huggingface.co/moonshotai/Kimi-K3/discussions/66

@usrlocalben

Copy link
Copy Markdown

FYI: Here's another chat template PR on HF: https://huggingface.co/moonshotai/Kimi-K3/discussions/66

This is the template I used.

@csabakecskemeti

Copy link
Copy Markdown
Contributor

I can also confirm the conversion and the chat template gives coherent answer
Full precision cmoe mmap (system: 128gb vram, 1tb ram, experts loaded from nvme)
k3-cmoe-mmap-full-precision

@SolshineCode

Copy link
Copy Markdown
Contributor

Tested kimi-k3-text @ cf11c4c end to end with a synthetic fixture. Greedy outputs from llama-server match the HF reference implementation exactly, token for token, on all 3 test prompts.

Method: I generated a random-init shrunk K3 (90M params, f32) that keeps the full structure: hybrid KDA/MLA with the real linear_attn_config schedule shape (full attn every 4th layer plus the last), latent MoE with 16 experts and routed_expert_hidden_size, attention-residual blocks, situ, the MLA output gate, the full-rank KDA gate, and the real tokenizer. One honest caveat about what "reference" means here: the released modeling code can't run on CPU as shipped (fla's KDA kernels are Triton-only), so I rerouted them to fla's own naive torch ops (naive_recurrent_kda and friends). The match is against fla's reference math, not the fused kernels. Comparison was done by POSTing raw input_ids to /completion with top_k=1, which validates the graph independent of tokenization, and the decoded text matched too.

Hardware is deliberately ancient: 2x Xeon E5-2609v2 (AVX only, no AVX2), 512GB DDR3, CPU-only build so far. Build is clean on this ISA, conversion works (tensor mapping, res_norm x res_proj fusion, expert merge, tokenizer; transformers git-main / 5.15.0.dev0), and llama-server generates at ~55 tok/s on the tiny model.

Two problems hit along the way:

  1. llama-cli hangs silently in --no-conversation mode (repro 3/3, with and without stdin closed; conversation mode is fine). Might not be K3-specific.
  2. convert_hf_to_gguf breaks on transformers >= 5.15: bytes_to_unicode moved out of models.gpt2.tokenization_gpt2 (it's in convert_slow_tokenizer now). A one-line try/except in conversion/qwen.py fixes it. Happy to PR that separately.

Fixture generator, CPU shim, and reference outputs: https://gist.github.com/SolshineCode/3115760b0c3b655563a3102ba897c426. I can upstream the fixture into the test suite if useful, or defer to @200lz if their fixtures already cover this. Once a real quant exists I can also run a full-scale streamed validation on this box (512GB RAM, mmap + -ot exps=CPU), and it has sm_52 Teslas if legacy-CUDA coverage is ever wanted.

AI usage disclosure: the test harness and this report were built with Claude running on my machine; the numbers are from real runs I can rerun on request.

@200lz

200lz commented Jul 28, 2026

Copy link
Copy Markdown

@SolshineCode Excellent validation—thank you for sharing the fixture and the CPU reference path.

My work does not duplicate your end-to-end fixture. I analyzed the released checkpoint schema using all 96 official safetensors headers, without downloading tensor payloads. The production checkpoint confirms:

  • layer 0 is dense, while layers 1–92 are routed MoE;
  • every MoE layer contains the exact expert set 0..895;
  • KDA layers are 0–2, 4–6, ... , 88–90;
  • MLA layers are 3, 7, ... , 87, 91, 92;
  • the routed-expert inventory contains exactly 92 × 896 × 3 = 247,296 MXFP4 block/scale pairs;
  • Attention Residual uses four per-layer tensors, two output tensors, and 12-layer block boundaries.

Please do not defer the execution fixture to me—your fixture covers an area I have not implemented and would be valuable upstream.

I can instead review the PR against the full released tensor vocabulary and contribute a compact schema-level regression test, if useful, covering the exceptional final MLA layer, the dense-to-MoE boundary, expert-set invariants, and self_attn.g_proj mapping.

I’ll first inspect the current PR branch to avoid duplicating existing tests.

@SolshineCode

Copy link
Copy Markdown
Contributor

Ran the fixture against the CUDA build as well: kimi-k3-text @ cf11c4c, CUDA 12.4 with -DCMAKE_CUDA_ARCHITECTURES=52, 2x Tesla M40 (compute 5.2), -ngl 99 --tensor-split 1,1. All 3 prompts match the reference token for token, ~109 t/s vs ~55 CPU-only. One flag: load prints resolve_fused_ops: layer 3 is assigned to device CUDA0 but Flash Attention is assigned to device CPU (usually due to missing support), so at least one attention op lacks an sm_52 kernel. Logs: https://gist.github.com/SolshineCode/3115760b0c3b655563a3102ba897c426 (sm52_results.md)

@200lz thanks, schema-level checks are exactly what my fixture doesn't do, so those complement each other well. I cross-checked your header-derived layer map against the released config's linear_attn_config and they match exactly (your 0-indexed MLA 3, 7, ..., 91, 92 is the config's 1-based full_attn_layers). The fixture already encodes both structural exceptions you mention: it ends with consecutive MLA layers like 91+92, and it has the dense layer 0 to MoE boundary. I'll upstream it as an execution test to sit alongside your schema regression test, in whatever form the maintainers prefer.

@200lz

200lz commented Jul 28, 2026

Copy link
Copy Markdown

@SolshineCode Thanks for cross-checking the released layer map and for confirming the two structural exceptions in the fixture.

That separation sounds ideal: your fixture can cover execution and token-level parity, while I’ll focus on compact schema regression coverage derived from the official checkpoint.

I’ll review the current PR tests and prepare the smallest non-duplicative schema test proposal, especially around the final MLA layer, the dense/MoE boundary, expert-set invariants, and tensor-name mapping.

@200lz

200lz commented Jul 28, 2026

Copy link
Copy Markdown

@pwilkin I completed a read-only test-gap review of this PR against the full released K3 checkpoint schema.

The external execution fixture provides strong CPU/CUDA parity coverage, but the PR currently has no dedicated K3 schema regression test in CI.

I would like to contribute a compact converter-level test covering:

  • the final consecutive MLA layers;
  • layer 0 dense vs. layers 1+ MoE;
  • official expert/shared-expert invariants;
  • Attention Residual block grouping;
  • and context-sensitive self_attn.g_proj mapping.

The test would use synthetic config/tensor metadata only—no model weights, runtime execution, or overlap with the existing fixture.

Would you prefer this added to #26185, or submitted as a small follow-up PR after merge?

@idumlupinar

Copy link
Copy Markdown

Can we run this text model on dual RTX 3090 and 128GB DDR4 memory and 5800x3d cpu on Windows 11?

@RodriMora

Copy link
Copy Markdown
Contributor

Can we run this text model on dual RTX 3090 and 128GB DDR4 memory and 5800x3d cpu on Windows 11?

No, the lowest size, if by some miracle Aes or Bart make a Q1-2 with imatrix quant that is still coherent, that would even be 700GB

@github-actions github-actions Bot added the testing Everything test related label Jul 28, 2026
@csabakecskemeti

csabakecskemeti commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

Quality topic
Just an opinion based on my own quant, curious what's other's experience:
The intelligence per GB feels off with this model. I'm testing my Q2 quant with zero shot Mario clone, and it struggles to create a working (I've not meant errors in the html the game is not working) game. My reference is GLM-5.2 Q5 which has done it first shot at first try. I know the Q5 is less lossy than Q2 but at GB level it perform better. (At least in HTML coding)
This may an unfair comparison, or some issue with my quant. Otherwise the output is coherent and makes sense.

I also acknowledge that this is work in progress, also this maybe my local issue. Shared just for fyi

Looking forward to hear other's experience

@jukofyork

Copy link
Copy Markdown
Collaborator
0.01.766.673 I print_info: arch                  = kimi-k3
0.01.766.674 I print_info: vocab_only            = 0
0.01.766.674 I print_info: no_alloc              = 0
0.01.766.674 I print_info: n_ctx_train           = 1048576
0.01.766.675 I print_info: n_embd_inp            = 7168
0.01.766.676 I print_info: n_embd                = 7168
0.01.766.676 I print_info: n_embd_out            = 7168
0.01.766.677 I print_info: n_layer               = 93
0.01.766.677 I print_info: n_layer_all           = 93
0.01.766.704 I print_info: n_head                = 96
0.01.766.714 I print_info: n_head_kv             = [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1]
0.01.766.714 I print_info: n_rot                 = 64
0.01.766.715 I print_info: n_swa                 = 0
0.01.766.716 I print_info: is_swa_any            = 0
0.01.766.716 I print_info: n_embd_head_k         = 576
0.01.766.716 I print_info: n_embd_head_v         = 74
0.01.766.724 I print_info: n_gqa                 = [0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 0, 0, 0, 96, 96]
0.01.766.735 I print_info: n_embd_k_gqa          = [0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 0, 0, 0, 576, 576]
0.01.766.742 I print_info: n_embd_v_gqa          = [0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 0, 0, 0, 74, 74]

Just noticed this has 74 for n_embd_head_v and the MLA layers' n_embd_v_gqa. Where does this come from? I was expecting it to be 512 like the old MLA code, but can't see how it could be 74 here?

@jukofyork

Copy link
Copy Markdown
Collaborator

The original weights look a bit odd too:

Screenshot_20260814-171439 Firefox
    "qk_nope_head_dim": 128,
    "qk_rope_head_dim": 64,

as from the paper:

Screenshot_20260814-171656 Firefox

So what is stored in the extra 64 values created by kv_a_proj_with_mqa and q_b_proj if there is no RoPE and the heads are all dimension 128?

@jukofyork

Copy link
Copy Markdown
Collaborator

OK, so this guy seems to have found those 64 values are still used:

https://github.com/FareedKhan-dev/kimi-k3-in-c

but not rotated:

Screenshot_20260814-172846 Firefox

but I still can't work out where that 72 value comes from in llama.cpp or if it's important/used anywhere.

@fairydreaming

Copy link
Copy Markdown
Contributor

Just noticed this has 74 for n_embd_head_v and the MLA layers' n_embd_v_gqa. Where does this come from? I was expecting it to be 512 like the old MLA code, but can't see how it could be 74 here?

@jukofyork Since there is no explicit attention.value_length in the GGUF llama.cpp calculates hparams.n_embd_head_v_full = hparams.n_embd / hparams.n_head() (7168 / 96) and that's how we get 74.

So the origin of this value is a certain very dark place. But I don't think this value is used anywhere in the model code.

@jukofyork

Copy link
Copy Markdown
Collaborator

It seems to be working really well for me.

Huge thanks to @pwilkin and @fairydreaming for getting this working so quickly!

@fairydreaming

Copy link
Copy Markdown
Contributor

It seems to be working really well for me.

Huge thanks to @pwilkin and @fairydreaming for getting this working so quickly!

@jukofyork It's all @pwilkin work, I just helped with testing.

@pwilkin pwilkin added the merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. label Aug 14, 2026
@pwilkin

pwilkin commented Aug 14, 2026

Copy link
Copy Markdown
Member Author

Think we should be able to merge it now.

@kat-palm

Copy link
Copy Markdown

Thanks so much for taking the time to write all this up. The warmup fix alone is gold, and the build/run scripts are exactly the kind of thing that saves a whole weekend.

Update from the recycled-hardware end: we built the same quant with your exact recipe (Q8_0 + --tensor-type "ssm_=bf16" --tensor-type "_exps=mxfp4", keep-split) on the 2× 6148 box — 7.25h to quantize, 1.44TB at 4.36 BPW. Your warmup fix is applied and the first load is running now; watching it actually touch all 896 experts on the way in is very satisfying. Noted on dropping AVX512_VNNI for the 6148, thanks.

Next on our list is your #16000 readahead port. With a single 4060 Ti 16GB we obviously won't see your pp numbers, but at this scale every multiple counts — we'll report back with before/after so the thread has a bottom-of-the-GPU-range data point.

For our use-case — an overnight second set of eyes on code review — it's a good exercise. A very large slow-cooker...

@CISC

CISC commented Aug 14, 2026

Copy link
Copy Markdown
Member

may need to shorten comments too, IMO some/most comments are too verbose

@pwilkin Address this please (that includes docstrings too).

@ngxson

ngxson commented Aug 14, 2026

Copy link
Copy Markdown
Collaborator

may need to shorten comments too, IMO some/most comments are too verbose

@pwilkin Address this please (that includes docstrings too).

btw, the most aligned way is to simply tell your agent "adapt/remove code comments to follow agents.md expectations"

@ngxson

ngxson commented Aug 15, 2026

Copy link
Copy Markdown
Collaborator

I'm taking over this PR now, ran a bot review pass offline and it pointed out some issues

will fix it and push commits directly here

Comment thread src/models/kimi-k3.cpp
Comment on lines +554 to +558
ggml_tensor * Q = ggml_concat(ctx0, q_nope_absorbed, q_pe, 0);
ggml_tensor * kv_cmpr_3d = ggml_reshape_3d(ctx0, kv_cmpr, kv_lora_rank, 1, n_tokens);
ggml_tensor * K = ggml_concat(ctx0, kv_cmpr_3d, k_pe, 0);
ggml_tensor * V = kv_cmpr_3d;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

annoyingly this is the same [nope | rope] layout that I identified in #27091 , but I'm not trying to fix it here because that will require re-converting GGUF

hopefully #27120 get merged (plus all other backends support it) so that we can clean up this rope logic

@ngxson
ngxson merged commit ad1de39 into ggml-org:master Aug 15, 2026
30 of 34 checks passed
@createthis

Copy link
Copy Markdown
Contributor

Congrats on the merge! 🎉

brittlewis12 pushed a commit to brittlewis12/llama.cpp that referenced this pull request Aug 17, 2026
* model: add Kimi-K3 text model

Hybrid KDA (linear) + MLA (full) attention as in Kimi-Linear-48B, plus five
things that architecture does not have:

  1. cross-layer residual attention  (attn_res_block_size)
  2. latent MoE                      (routed experts run at n_expert_latent)
  3. situ activation                 (replaces SwiGLU everywhere)
  4. MLA output gate                 (sigmoid gate before o_proj)
  5. full-rank KDA gate              (single ssm_g instead of ssm_g_a/ssm_g_b)

K3's text_config reports KimiLinearForCausalLM - the older 48B architecture -
so get_model_architecture routes on the top-level name instead.

The KDA decay gate has two forms, selected by linear_attn_config's
gate_lower_bound. It is not a clamp: when set it swaps the activation entirely
(fla/ops/kda/gate.py), from -exp(A_log)*softplus(x) to
lower_bound*sigmoid(exp(A_log)*x). K3 sets it to -5.0; kimi-linear leaves it
unset, so that path is unchanged.

Cross-layer residuals reuse ggml_dsv4_hc_pre for the weighted sum. That op is
CPU + CUDA only, so Metal/Vulkan will fall back per-node until those kernels
exist.

The routed experts ship as compressed-tensors "mxfp4-pack-quantized". That is
bit-compatible with ggml's MXFP4 - same E2M1 code assignment, same E8M0 scale
byte, only the nibble positions within a block differ - so they are repacked
rather than dequantized, losslessly and without a ~5.5 TB bf16 round-trip.
The repack is built lazily because gguf_writer holds every added tensor until
the final write. DeepSeek-V4 was already doing the identical bit-shuffling, so
it now shares the helper.

Verified against Moonshot's own code path (transformers + fla's Triton KDA
kernels) on a tiny model exercising every K3-specific feature. Final-position
logits vs the fp32 reference: 6.7e-05 rel / corr 1.00000000 for both the
chunked and the recurrent delta-net path. MXFP4 blocks dequantize to the source
weights with 0.0e+00 error.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* model: fix ty errors in the Kimi-K3 converter

- `_res_parts` buffers (kind, tensor) pairs, not bare tensors
- `get_tensors` must return an Iterator, matching ModelBase
- LazyBase's `func` takes one argument, so pass the expert loaders through
  `args` instead of the closure
- borrowing KimiLinearModel.set_vocab from an unrelated TextModel is
  deliberate and safe, but not expressible in the signature

No behaviour change: the MXFP4 repack still dequantizes to the source weights
with 0.0e+00 error and end-to-end logits are unchanged (8.386e-03 rel,
corr 0.99996630).

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Update conversion/kimi_k3.py

Co-authored-by: Boris Dvorkin  <b_dvorkin@niuitmo.ru>

* Increase LLAMA_MAX_EXPERTS from 512 to 1024

* tests : support for Kimi K3 in archs test

* chat : add Kimi K3 chat format (reasoning, content, typed tool calls)

K3's assistant output is an XTML-ish tagged format built by the template's
open_tag/close_tag macros. Two properties break generic parsing:

1. The generation prompt ends with open_tag('think'), so the completion
   starts inside the think section with no opening marker in the output
   (thinking_forced_open).
2. Only <|open|>/<|close|>/<|sep|>/<|end_of_msg|> are special tokens; tag
   names ("think", "response", "message") are ordinary text tokens.

Adds common_chat_params_init_kimi_k3 (PEG_NATIVE) with detection on the
marker trio, reasoning extraction, response unwrapping, and tool-call
parsing of the tools/call/argument tag structure with argument types
taken from the tool schema. Includes the K3 chat template fixture and 9
test-chat cases derived from real generations of the full 2.8T model.

Verified end-to-end against Kimi-K3-Q2_K (GrEarl/Kimi-K3-GGUF) on 8x B200:
content, reasoning_content, streaming deltas, and tool_calls all correct;
finish_reason stop/tool_calls as appropriate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chat : add message_delimiters for Kimi K3

Per-role message-start markers for token-level span splitting. User and
assistant messages carry only the role attribute, so their full opener
(through <|sep|>) is used; system and tool messages continue with more
attributes (type=/tool=/index=), so those delimiters stop after the
role's closing quote. Verified against the K3 tiktoken vocabulary that
the closing quote is always a standalone token across all attribute
variants, so the token-level prefix match stays exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: apply nits from @ngxson and text fixes from @danielhanchen

* tests : added missing hyperparameters and tensors for Kimi K3 in test-llama-archs

* chore : move overly verbose header file comments to Kimi K3 source file

* tests : re-enabled KIMI_K3 in test-llama-archs for WebGPU backend

* model-saver : emit kda_gate_lower_bound for Kimi K3

Quick fix. The Kimi K3 loader reads kda_gate_lower_bound and gates a graph branch on it (it scales the KDA gate when the bound is above -INFINITY), but the model
saver never wrote the key, so a save->load roundtrip silently dropped it back to the -INFINITY default and changed the model's output. The real K3 config sets gate_lower_bound = -5.0.

I propose to emit it from the saver, and set it to -5.0 in the test-llama-archs K3 case so the roundtrip check exercises it (the roundtrip fails without the saver line).

* Refactor conditional for model architecture check

* tests : re-enabled (again) KIMI_K3 and MINIMAX_M3 in test-llama-archs for WebGPU backend

* fix code comments

* add template on conversion

* move repack_mxfp4_blocks to model base

* nits

* add_value_length

* optimize res_stack construction

* nits

---------

Co-authored-by: Boris Dvorkin <b_dvorkin@niuitmo.ru>
Co-authored-by: Stanisław Szymczyk <sszymczy@gmail.com>
Co-authored-by: Deepankar Singh <singh.deepankar39@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Caleb DeLeeuw <caleb.deleeuw@gmail.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
crusaderky added a commit to crusaderky/llama.cpp that referenced this pull request Aug 19, 2026
* model: add Kimi-K3 text model

Hybrid KDA (linear) + MLA (full) attention as in Kimi-Linear-48B, plus five
things that architecture does not have:

  1. cross-layer residual attention  (attn_res_block_size)
  2. latent MoE                      (routed experts run at n_expert_latent)
  3. situ activation                 (replaces SwiGLU everywhere)
  4. MLA output gate                 (sigmoid gate before o_proj)
  5. full-rank KDA gate              (single ssm_g instead of ssm_g_a/ssm_g_b)

K3's text_config reports KimiLinearForCausalLM - the older 48B architecture -
so get_model_architecture routes on the top-level name instead.

The KDA decay gate has two forms, selected by linear_attn_config's
gate_lower_bound. It is not a clamp: when set it swaps the activation entirely
(fla/ops/kda/gate.py), from -exp(A_log)*softplus(x) to
lower_bound*sigmoid(exp(A_log)*x). K3 sets it to -5.0; kimi-linear leaves it
unset, so that path is unchanged.

Cross-layer residuals reuse ggml_dsv4_hc_pre for the weighted sum. That op is
CPU + CUDA only, so Metal/Vulkan will fall back per-node until those kernels
exist.

The routed experts ship as compressed-tensors "mxfp4-pack-quantized". That is
bit-compatible with ggml's MXFP4 - same E2M1 code assignment, same E8M0 scale
byte, only the nibble positions within a block differ - so they are repacked
rather than dequantized, losslessly and without a ~5.5 TB bf16 round-trip.
The repack is built lazily because gguf_writer holds every added tensor until
the final write. DeepSeek-V4 was already doing the identical bit-shuffling, so
it now shares the helper.

Verified against Moonshot's own code path (transformers + fla's Triton KDA
kernels) on a tiny model exercising every K3-specific feature. Final-position
logits vs the fp32 reference: 6.7e-05 rel / corr 1.00000000 for both the
chunked and the recurrent delta-net path. MXFP4 blocks dequantize to the source
weights with 0.0e+00 error.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* model: fix ty errors in the Kimi-K3 converter

- `_res_parts` buffers (kind, tensor) pairs, not bare tensors
- `get_tensors` must return an Iterator, matching ModelBase
- LazyBase's `func` takes one argument, so pass the expert loaders through
  `args` instead of the closure
- borrowing KimiLinearModel.set_vocab from an unrelated TextModel is
  deliberate and safe, but not expressible in the signature

No behaviour change: the MXFP4 repack still dequantizes to the source weights
with 0.0e+00 error and end-to-end logits are unchanged (8.386e-03 rel,
corr 0.99996630).

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Update conversion/kimi_k3.py

Co-authored-by: Boris Dvorkin  <b_dvorkin@niuitmo.ru>

* Increase LLAMA_MAX_EXPERTS from 512 to 1024

* tests : support for Kimi K3 in archs test

* chat : add Kimi K3 chat format (reasoning, content, typed tool calls)

K3's assistant output is an XTML-ish tagged format built by the template's
open_tag/close_tag macros. Two properties break generic parsing:

1. The generation prompt ends with open_tag('think'), so the completion
   starts inside the think section with no opening marker in the output
   (thinking_forced_open).
2. Only <|open|>/<|close|>/<|sep|>/<|end_of_msg|> are special tokens; tag
   names ("think", "response", "message") are ordinary text tokens.

Adds common_chat_params_init_kimi_k3 (PEG_NATIVE) with detection on the
marker trio, reasoning extraction, response unwrapping, and tool-call
parsing of the tools/call/argument tag structure with argument types
taken from the tool schema. Includes the K3 chat template fixture and 9
test-chat cases derived from real generations of the full 2.8T model.

Verified end-to-end against Kimi-K3-Q2_K (GrEarl/Kimi-K3-GGUF) on 8x B200:
content, reasoning_content, streaming deltas, and tool_calls all correct;
finish_reason stop/tool_calls as appropriate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chat : add message_delimiters for Kimi K3

Per-role message-start markers for token-level span splitting. User and
assistant messages carry only the role attribute, so their full opener
(through <|sep|>) is used; system and tool messages continue with more
attributes (type=/tool=/index=), so those delimiters stop after the
role's closing quote. Verified against the K3 tiktoken vocabulary that
the closing quote is always a standalone token across all attribute
variants, so the token-level prefix match stays exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: apply nits from @ngxson and text fixes from @danielhanchen

* tests : added missing hyperparameters and tensors for Kimi K3 in test-llama-archs

* chore : move overly verbose header file comments to Kimi K3 source file

* tests : re-enabled KIMI_K3 in test-llama-archs for WebGPU backend

* model-saver : emit kda_gate_lower_bound for Kimi K3

Quick fix. The Kimi K3 loader reads kda_gate_lower_bound and gates a graph branch on it (it scales the KDA gate when the bound is above -INFINITY), but the model
saver never wrote the key, so a save->load roundtrip silently dropped it back to the -INFINITY default and changed the model's output. The real K3 config sets gate_lower_bound = -5.0.

I propose to emit it from the saver, and set it to -5.0 in the test-llama-archs K3 case so the roundtrip check exercises it (the roundtrip fails without the saver line).

* Refactor conditional for model architecture check

* tests : re-enabled (again) KIMI_K3 and MINIMAX_M3 in test-llama-archs for WebGPU backend

* fix code comments

* add template on conversion

* move repack_mxfp4_blocks to model base

* nits

* add_value_length

* optimize res_stack construction

* nits

---------

Co-authored-by: Boris Dvorkin <b_dvorkin@niuitmo.ru>
Co-authored-by: Stanisław Szymczyk <sszymczy@gmail.com>
Co-authored-by: Deepankar Singh <singh.deepankar39@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Caleb DeLeeuw <caleb.deleeuw@gmail.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
fewtarius added a commit to fewtarius/CachyLLama that referenced this pull request Aug 24, 2026
Brings 218 upstream commits into CachyLLama without losing any of our
features. Key carried-over changes from upstream:
- llama.cpp v0.2.0 / ggml v0.21.0 version bumps
- Vulkan FA MMQ fp32 scaling (ggml-org#27413), PAD_REFLECT_1D (ggml-org#26586), tiled
  transpose (ggml-org#26585), null checks in queue command pools cleanup (ggml-org#27353)
- ggml: rope_set_offset on multiple backends, recurrent state rollback
- Vulkan coopmat1 SHMEM_STRIDE_PAD/APPLY_SLM_A_RESHAPE for Intel Xe
- server: LLAMA_SERVER_SLOTS_N_DIFF (ggml-org#27600), /metrics during llama_decode
  (ggml-org#27041), index.html no-cache (ggml-org#27006), make-release workflow
- model: MiniMax-M1/Text01 (ggml-org#27018), Kimi-K3 (ggml-org#26185), BailingMoE3 (ggml-org#26608),
  GraniteSWA (ggml-org#25505), GLM-4.5-Air MTP, DSV4 tensor split (-sm tensor)
- ui: Chat Conversation Tabbed navigation, settings refactor
- common: --models-dir loading MTP assistant models (ggml-org#24431),
  --load-mode replacing --mmap (ggml-org#26934), json.h abstraction (ggml-org#27511)
- vendor: cpp-httplib 0.53.1, BoringSSL 0.20260813.0, vendor/hash

CachyLLama features preserved through conflict resolution:
- Persistent SSD-backed KV cache (3-tier hot/warm/cold + system prompt cache)
- Per-user isolation (user_id, per-user concurrency cap, slot affinity)
- MoE expert residency + co-activation tracking
- CachyLLama Vulkan Lightning Indexer (108/108 on Strix Halo) + DSV4
  hyper-connection fused ops + DSV4 sparse FA + coopmat shaders
- FA quant-KV dequant-once + f16 contiguize (with host-RAM safety gate)
- DFlash framework + Laguna-S-2.1 model support
- DFlash d2t reduced-vocab draft support (upstream merge)
- Context checkpoint ring buffer + SWA skip + memory budget scaling
- Stable-prefix LCP gate + prompt_stable_prefix_tokens param
- conv_hash conversation-boundary detection
- All CachyLLama Vulkan shaders (concat_transpose, lightning_indexer,
  mmid_row_lists, flash_attn_top_k, dequant_f16_transpose)
- common::host_available_ram() utility
- llama-moe-residency + llama-moe-coact modules

Manual conflict resolution touches: src/models/dflash.cpp (DFlash d2t +
aux_norm), src/llama-kv-cache-dsv4.cpp (state snapshot fix), src/llama-
memory-recurrent.cpp (rs_idx bounds check), src/llama-model-saver.cpp
(DSV4 compress_ratios + swiglu_clamp sizing), ggml/src/ggml-vulkan/
{ggml-vulkan.cpp,vulkan-shaders-gen.cpp,vulkan-shaders/dequant_q8_0.
comp,vulkan-shaders/flash_attn.comp,vulkan-shaders/copy_transpose_02.
comp} (CachyLLama shader registration + FA scratch gate), ggml/src/
ggml-cuda/mmvq.cu (RDNA3_5 + GB10 enum), gguf-py/gguf/constants.py
(DFlash ENC_AUX_NORM + D2T tensors), tests/{CMakeLists.txt,test-backend-
ops.cpp,test-llama-archs.cpp,test-recurrent-state-rollback.cpp}
(test additions), tools/{CMakeLists.txt,server/*} (server_batch embd
support + spec_is_replay + user_id routing + MCP servers + CORS), and
docs/{AGENTS.md,README.md} (kept CachyLLama branding).

Verified: full build succeeds, test-backend-ops Vulkan LIGHTNING_INDEXER +
FLASH_ATTN pass on Strix Halo.

Based on a re-merge from the 20260824 (pristine pre-merge) branch after
a previous agent's merge attempt produced an unbuildable state from
-X ours that wiped shader float-typing and broke the dequant_q8_0 +
flash_attn shaders with redefinition errors.
ravel7524 pushed a commit to ravel7524/llama.cpp that referenced this pull request Aug 30, 2026
* model: add Kimi-K3 text model

Hybrid KDA (linear) + MLA (full) attention as in Kimi-Linear-48B, plus five
things that architecture does not have:

  1. cross-layer residual attention  (attn_res_block_size)
  2. latent MoE                      (routed experts run at n_expert_latent)
  3. situ activation                 (replaces SwiGLU everywhere)
  4. MLA output gate                 (sigmoid gate before o_proj)
  5. full-rank KDA gate              (single ssm_g instead of ssm_g_a/ssm_g_b)

K3's text_config reports KimiLinearForCausalLM - the older 48B architecture -
so get_model_architecture routes on the top-level name instead.

The KDA decay gate has two forms, selected by linear_attn_config's
gate_lower_bound. It is not a clamp: when set it swaps the activation entirely
(fla/ops/kda/gate.py), from -exp(A_log)*softplus(x) to
lower_bound*sigmoid(exp(A_log)*x). K3 sets it to -5.0; kimi-linear leaves it
unset, so that path is unchanged.

Cross-layer residuals reuse ggml_dsv4_hc_pre for the weighted sum. That op is
CPU + CUDA only, so Metal/Vulkan will fall back per-node until those kernels
exist.

The routed experts ship as compressed-tensors "mxfp4-pack-quantized". That is
bit-compatible with ggml's MXFP4 - same E2M1 code assignment, same E8M0 scale
byte, only the nibble positions within a block differ - so they are repacked
rather than dequantized, losslessly and without a ~5.5 TB bf16 round-trip.
The repack is built lazily because gguf_writer holds every added tensor until
the final write. DeepSeek-V4 was already doing the identical bit-shuffling, so
it now shares the helper.

Verified against Moonshot's own code path (transformers + fla's Triton KDA
kernels) on a tiny model exercising every K3-specific feature. Final-position
logits vs the fp32 reference: 6.7e-05 rel / corr 1.00000000 for both the
chunked and the recurrent delta-net path. MXFP4 blocks dequantize to the source
weights with 0.0e+00 error.

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* model: fix ty errors in the Kimi-K3 converter

- `_res_parts` buffers (kind, tensor) pairs, not bare tensors
- `get_tensors` must return an Iterator, matching ModelBase
- LazyBase's `func` takes one argument, so pass the expert loaders through
  `args` instead of the closure
- borrowing KimiLinearModel.set_vocab from an unrelated TextModel is
  deliberate and safe, but not expressible in the signature

No behaviour change: the MXFP4 repack still dequantizes to the source weights
with 0.0e+00 error and end-to-end logits are unchanged (8.386e-03 rel,
corr 0.99996630).

Assisted-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* Update conversion/kimi_k3.py

Co-authored-by: Boris Dvorkin  <b_dvorkin@niuitmo.ru>

* Increase LLAMA_MAX_EXPERTS from 512 to 1024

* tests : support for Kimi K3 in archs test

* chat : add Kimi K3 chat format (reasoning, content, typed tool calls)

K3's assistant output is an XTML-ish tagged format built by the template's
open_tag/close_tag macros. Two properties break generic parsing:

1. The generation prompt ends with open_tag('think'), so the completion
   starts inside the think section with no opening marker in the output
   (thinking_forced_open).
2. Only <|open|>/<|close|>/<|sep|>/<|end_of_msg|> are special tokens; tag
   names ("think", "response", "message") are ordinary text tokens.

Adds common_chat_params_init_kimi_k3 (PEG_NATIVE) with detection on the
marker trio, reasoning extraction, response unwrapping, and tool-call
parsing of the tools/call/argument tag structure with argument types
taken from the tool schema. Includes the K3 chat template fixture and 9
test-chat cases derived from real generations of the full 2.8T model.

Verified end-to-end against Kimi-K3-Q2_K (GrEarl/Kimi-K3-GGUF) on 8x B200:
content, reasoning_content, streaming deltas, and tool_calls all correct;
finish_reason stop/tool_calls as appropriate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chat : add message_delimiters for Kimi K3

Per-role message-start markers for token-level span splitting. User and
assistant messages carry only the role attribute, so their full opener
(through <|sep|>) is used; system and tool messages continue with more
attributes (type=/tool=/index=), so those delimiters stop after the
role's closing quote. Verified against the K3 tiktoken vocabulary that
the closing quote is always a standalone token across all attribute
variants, so the token-level prefix match stays exact.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: apply nits from @ngxson and text fixes from @danielhanchen

* tests : added missing hyperparameters and tensors for Kimi K3 in test-llama-archs

* chore : move overly verbose header file comments to Kimi K3 source file

* tests : re-enabled KIMI_K3 in test-llama-archs for WebGPU backend

* model-saver : emit kda_gate_lower_bound for Kimi K3

Quick fix. The Kimi K3 loader reads kda_gate_lower_bound and gates a graph branch on it (it scales the KDA gate when the bound is above -INFINITY), but the model
saver never wrote the key, so a save->load roundtrip silently dropped it back to the -INFINITY default and changed the model's output. The real K3 config sets gate_lower_bound = -5.0.

I propose to emit it from the saver, and set it to -5.0 in the test-llama-archs K3 case so the roundtrip check exercises it (the roundtrip fails without the saver line).

* Refactor conditional for model architecture check

* tests : re-enabled (again) KIMI_K3 and MINIMAX_M3 in test-llama-archs for WebGPU backend

* fix code comments

* add template on conversion

* move repack_mxfp4_blocks to model base

* nits

* add_value_length

* optimize res_stack construction

* nits

---------

Co-authored-by: Boris Dvorkin <b_dvorkin@niuitmo.ru>
Co-authored-by: Stanisław Szymczyk <sszymczy@gmail.com>
Co-authored-by: Deepankar Singh <singh.deepankar39@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: Caleb DeLeeuw <caleb.deleeuw@gmail.com>
Co-authored-by: Xuan Son Nguyen <son@huggingface.co>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

conversion merge ready A maintainer can use this label to indicate that they consider the changes final and ready to merge. model Model specific testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.