model: add Kimi-K3 text model - #26185
Conversation
ngxson
left a comment
There was a problem hiding this comment.
may need to shorten comments too, IMO some/most comments are too verbose
| std::vector<ggml_tensor *> ckpts; | ||
| ggml_tensor * stack_cache = nullptr; | ||
| int stack_cache_n = -1; | ||
|
|
There was a problem hiding this comment.
I'd suggest renaming:
- ckpts --> resi (short for residual stream, same naming mentioned in the paper)
- drop the
_cachesince technically there is no cache here,resi_stackshould 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);| 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); | ||
| } |
There was a problem hiding this comment.
these might not be necessary, I suppose for compat?
There was a problem hiding this comment.
Yeah, those are artifacts from the mock model runs, for the real model I'll purge them.
|
https://huggingface.co/inference-optimization/Kimi-K3-0.18B potentially useful, depending on how faithful the model is reconstructed in 0.18B . |
|
@Green-Sky I wouldn't put it up without checking parity with a mock model 😄 |
Isnt that a mock model? |
|
Yeah, that's what I'm saying, already built one of my own for parity testing purposes when doing the PR. |
|
@GrEarl please put your comment inside a collapsible block, it takes up too much space & make the discussion hard to keep track |
|
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.
|
|
Tested this branch today with a converted K3 checkpoint. Everything loaded successfully and generation worked as expected. Conversion (
Runtime
Two observations that may help others:
Thank you. |
|
@GrEarl 's late-edition Q2 works 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. ---snip--- (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. |
|
FYI: Here's another chat template PR on HF: https://huggingface.co/moonshotai/Kimi-K3/discussions/66 |
This is the template I used. |
|
Tested Method: I generated a random-init shrunk K3 (90M params, f32) that keeps the full structure: hybrid KDA/MLA with the real 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:
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 + 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. |
|
@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:
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 I’ll first inspect the current PR branch to avoid duplicating existing tests. |
|
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. |
|
@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. |
|
@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 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? |
|
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 |
|
Quality topic 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 |
|
@kat-palm good catch, thanks, messed that up on the rebase to master |
I've got a similar machine (dual
for the full ~/llama.cpp/build/bin/llama-quantize \
--tensor-type "ssm_=bf16" \
--tensor-type "_exps=mxfp4" \
Kimi-K3-BF16.gguf Kimi-K3-MXFP4.gguf Q8_0 44For reference, compiled using: #!/bin/bash
function safe_sed() {
local file=$1
local pattern=$2
local replacement=$3
# Check if pattern exists
if ! sed -n "s/${pattern}/${replacement}/p" "$file" | grep -q .; then
echo "Error: Pattern not found in $file: $pattern"
return 1
fi
# Create backup
cp "$file" "$file.bak"
# Perform the replacement
sed -i "s/${pattern}/${replacement}/g" "$file"
# Show diff
echo
echo "Changes in '$file':"
echo "-------------------"
diff "$file.bak" "$file"
# Clean up
rm "$file.bak"
echo "-------------------"
}
function safe_sed_function() {
local file=$1
local function_signature=$2
local replacement=$3
# Create backup
cp "$file" "$file.bak"
# Perform the replacement using address range and c command
sed -i "/${function_signature}/,/^}/c\\${replacement}" "$file"
# Show diff
echo
echo "Changes in '$file':"
echo "-------------------"
diff "$file.bak" "$file"
# Clean up
rm "$file.bak"
echo "-------------------"
}
cd
rm -rf llama.cpp
git clone https://github.com/ggml-org/llama.cpp
cd llama.cpp
# Fetch the PR and checkout
git fetch origin pull/26185/head:pr-26185
git checkout pr-26185
# Fix warmup bug.
safe_sed "src/models/kimi-k3.cpp" "hparams.n_expert," "n_expert,"
safe_sed "src/models/kimi-k3.cpp" "hparams.n_expert_used," "n_expert_used,"
safe_sed "common/common.cpp" "llama_decode(lctx, llama_batch_get_one(tmp.data(), std::min(tmp.size(), (size_t) params.n_batch)));" \
"llama_set_warmup(lctx, true);\n llama_decode(lctx, llama_batch_get_one(tmp.data(), std::min(tmp.size(), (size_t) params.n_batch)));\n llama_set_warmup(lctx, false);"
# Pin each thread to its corresponding core.
safe_sed_function "ggml/src/ggml-cpu/ggml-cpu.c" \
"^static void set_numa_thread_affinity(int thread_n) {$" \
"static void set_numa_thread_affinity(int thread_n) {\n\
cpu_set_t cpus;\n\
CPU_ZERO(&cpus);\n\
CPU_SET(thread_n, &cpus);\n\
pthread_setaffinity_np(pthread_self(), sizeof(cpu_set_t), &cpus);\n\
}"
# Turn off to ensure we call ggml_backend_cuda_buffer_set_tensor and the cached-tensor patch to work.
safe_sed "ggml/src/ggml-backend.cpp" "if (split->graph.n_nodes > 0" "if (false"
# Use a pinned buffer to get full PCI-E transfer speed.
safe_sed_function "ggml/src/ggml-cuda/ggml-cuda.cu" \
"^static void ggml_backend_cuda_buffer_set_tensor(" \
"#include \"patch_ggml_backend_cuda_buffer_set_tensor_llama_cpp.cu\"\n"
cmake -B build -DGGML_NATIVE=ON -DGGML_CUDA=ON -DGGML_AVX512=ON -DGGML_AVX512_VNNI=ON
cmake --build build --config Release -- -j "$(nproc)"
and run using this: #!/bin/bash
PORT_NUMBER=8082
# Turn off NUMA balancing
echo 0 | sudo tee /proc/sys/kernel/numa_balancing > /dev/null
# Ask for permission to drop caches
read -p "Do you want to drop caches? (y/n) " -n 1 -r
echo # Move to a new line
if [[ $REPLY =~ ^[Yy]$ ]]
then
echo "Dropping caches..."
sudo swapoff -a
sudo swapon -a
echo 3 | sudo tee /proc/sys/vm/drop_caches > /dev/null
fi
MODEL_NAME="Kimi-K3"
MODEL_FILE="${HOME}/models/gguf/Kimi-K3-MXFP4.gguf"
JINJA_FILE="${HOME}/models/${MODEL_NAME}.jinja"
ALIAS="$(hostname):${MODEL_NAME}"
export GGML_OP_OFFLOAD_MIN_BATCH=2048
export GGML_CUDA_EXPS_READAHEAD_THREADS=10
#export GGML_CUDA_EXPS_READAHEAD_DEBUG=1
export CUDA_VISIBLE_DEVICES=0,1
~/llama.cpp/build/bin/llama-server \
--host $(hostname -I) \
--port "$PORT_NUMBER" \
--alias "$ALIAS" \
--model "$MODEL_FILE" \
--jinja \
--chat-template-file "$JINJA_FILE" \
--n-gpu-layers 99 \
--flash-attn on \
--numa distribute \
--load-mode mmap \
--threads "$(nproc)" \
--tensor-split 43,50 \
--override-tensor "exps=CPU" \
--ctx_size 131072 \
--batch-size 8192 \
--ubatch-size 8192 \
--parallel 1 \
--no-cont-batching \
--cache-ram 0 \
--temp 1.0 \
--top-p 0.95 \
--min-p 0.01 \
--verbosity 4(*) I'm not sure if this model works properly with I don't think these see that bad stats, and seem to match what I expect (eg: for |
Just noticed this has |
|
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:
but I still can't work out where that |
@jukofyork Since there is no explicit 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. |
… for WebGPU backend
|
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. |
|
Think we should be able to merge it now. |
|
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 + 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... |
@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" |
|
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 |
| 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; | ||
|
|
There was a problem hiding this comment.
Assisted-by: Claude Opus 5
|
Congrats on the merge! 🎉 |
* 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>
* 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>
* 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>




Hybrid KDA (linear) + MLA (full) attention as in Kimi-Linear-48B, plus five things that architecture does not have:
Reuses DeepSeek4's HC_PRE for the cross-layer residual weighted sum. Supports repack for the MXFP4 weights in conversion.
Now need someone to actually convert and test :)