fix(kimi_linear): chunked prefill discards the recurrent state
I maintainer di solito rispondono entro 1 giorno
Nessuno ha ancora preso questa issue.
Valutazione
- Difficoltà
- 3/5
- Tempo stimato
- 1-2 giorni
- Idoneità per principianti
- 74/100
- Tipo di issue
- Bug
- Chiarezza
- Specificata chiaramente
- Stato di attività
- Attiva
- Stack tecnologico
- rust
- Ambito
- machine-learning, performance
Direzione di ricerca
Inizia in src/models/kimi_linear.rs, nell’implementazione di LanguageModel, e confronta i relativi metodi per la capacità di prefill con src/models/kimi_k3.rs:2011-2033. Leggi kimi_linear_tests.rs e il test esistente kimi_k3_opts_out_of_chunked_prefill, quindi esegui i cargo tests elencati; il lavoro è completato quando il percorso LanguageModel non usa più il chunked prefill per Kimi Linear e il test di regressione e i comandi di verifica hanno esito positivo.
Scritto dal modello di indicizzazione a partire dal testo della issue.
Descrizione
Problem / Background
KimiLinearModel answers any prompt longer than MLXCEL_PREFILL_CHUNK (default 2048) from its last chunk alone. Everything before the final chunk never reaches the recurrent state, and nothing raises: no error, no warning, no shape mismatch, just a wrong answer that looks like a normal one.
Found while porting Kimi K3 (#1334, PR #1741), which inherited this code shape byte for byte. Chunked prefill itself came from #672 / #674.
Current Behavior
forward_for_sequence at src/models/kimi_linear.rs:1553 opens with a reset keyed on the token count:
let seq_len = mlxcel_core::array_shape(input_ids)[1];
if seq_id.is_none() && seq_len > 1 {
self.sequence_state.replace_internal(self.make_kimi_caches());
}
That reset is what makes the cache-less LanguageModel::forward entry point (kimi_linear.rs:1607) safe to call for two unrelated prompts in a row: a multi-token forward with no sequence id is read as the start of a new prompt.
LanguageModel::supports_chunked_prefill defaults to true (src/lib/mlxcel-core/src/generate.rs:404) and KimiLinearModel does not override it, so a prompt longer than one chunk is routed through chunked_prefill_last_logits (generate.rs:302, dispatched at generate.rs:1738 and generate.rs:2526). That helper calls model.forward_last_logits(...) once per chunk with no sequence id, and KimiLinearModel does not override forward_last_logits, so each chunk lands on forward and then on the reset above. Every chunk after the first discards the KimiLinearCache::MLA(KVCache) (kv_latent, k_pe) pair and the KimiLinearCache::Delta(KimiDeltaCache) (q/k/v_conv_state, ssm_state) recurrence its predecessors built (kimi_linear.rs:417-451).
The server path is unaffected: its own prefill_chunk_size (default 512, src/server/cli_input.rs:2252) drives forward_for_sequence with a real SequenceId, which never enters the reset branch. This is a CLI and bench defect.
Proposed Solution
Option 1, minimal and immediate. Override supports_chunked_prefill() -> false on KimiLinearModel's impl LanguageModel, next to the existing supports_padded_prefill / supports_batching overrides at kimi_linear.rs:1581-1587, mirroring KimiK3Model at src/models/kimi_k3.rs:2031. One method, no contract change, correct answers. It gives up the per-chunk prefill memory bound of #672 for this family: transients become one graph over the whole prompt instead of one per chunk.
Option 2, proper. Teach forward_for_sequence to tell "new prompt" from "continuation" by something other than the token count, so chunked prefill can carry state across chunks. This changes the shared ModelOwnedSequenceState contract (src/models/model_owned.rs:22), so it must be checked against every family that resets state inside a forward, not just this one. If option 2 lands, remove Kimi K3's override in the same change so the two families do not drift.
Decision criterion: ship option 1 unless option 2 is already in hand. Option 1 is strictly better than silent truncation and is a one-line revert once option 2 exists.
Scope
In scope: src/models/kimi_linear.rs and src/models/kimi_linear_tests.rs.
Out of scope: fixing the five sibling families in the audit below. They carry the same defect but each needs its own validation checkpoint; file follow-ups rather than widening this issue.
Audit: in-forward state resets on ModelOwnedSequenceState
The defect shape is a replace_internal reached from a forward path, not from make_caches / reset_runtime_state / set_kv_cache_layer_modes, which the generator calls once per prompt and are safe. Of the 42 replace_internal call sites under src, seven are in-forward, all with the identical if seq_id.is_none() && seq_len > 1 guard:
| Model | Reset site | supports_chunked_prefill |
Verdict |
|---|---|---|---|
kimi_linear.rs |
1559 | default true |
Affected. This issue. |
kimi_k3.rs |
1982 | false at 2031 |
Already mitigated (PR #1741). |
qwen3_next.rs |
1669 | default true |
Affected. Follow-up needed. |
rwkv7.rs |
1016 | default true |
Affected. Follow-up needed. |
bailing_moe_linear.rs |
2509 | default true |
Affected. Follow-up needed. |
recurrent_gemma.rs |
1039 | default true |
Affected. Follow-up needed. |
afmoe.rs |
1226 | default true |
Affected. Follow-up needed. |
Checked and not affected, because their replace_internal calls are all in make_caches, reset_runtime_state or set_kv_cache_layer_modes: qwen3_5.rs (3329, 3344), mamba.rs, mamba2.rs, jamba.rs, nemotron_h.rs, falcon_h1.rs, granitemoehybrid.rs, plamo2.rs, lfm2.rs, deepseek_v4.rs, llama4.rs, gemma3.rs, gemma4.rs, muse_glimmer.rs, inkling/runtime.rs, vision/unlimited_ocr.rs (which opts out separately at unlimited_ocr.rs:275).
Implementation Notes
- Reuse: for option 1, copy the shape and the doc-comment reasoning from
kimi_k3.rs:2011-2033. The two families share the failure mode; do not reword it into a second, divergent account. - Reuse:
src/models/kimi_k3_tests.rs:1640(kimi_k3_opts_out_of_chunked_prefill) is the existing per-model test shape.generate.rs:3107(supports_chunked_prefill_default_and_override) andgenerate.rs:3139(chunked_prefill_matches_single_pass) are the trait-level and equivalence shapes. - No call-site plumbing:
effective_prefill_chunk(generate.rs:285) already ANDs in the model's answer, andLoadedModelforwards the method (src/loaded_model.rs:410), so option 1 reaches the CLI and bench paths on its own. - Edge case: a prompt exactly
MLXCEL_PREFILL_CHUNKlong stays single-pass (the gate requiresprompt_len > configured), so the bug starts at chunk + 1 tokens.MLXCEL_PREFILL_CHUNK=0also forces single-pass and is the current user-side workaround. - Edge case: option 2 must preserve the two-unrelated-prompts-in-a-row property the reset exists to provide. Whatever signal replaces the token count has to be cleared by
make_caches/reset_runtime_state, which the generator already calls at the start of each prompt. - Error handling: neither option adds a panic on the prefill path. The
unwrap_or_else(|err| panic!("KimiLinear {err}"))atkimi_linear.rs:1567stays as-is; it guards the server path, where the scheduler must have calledprepare_sequence_statefirst.
Acceptance Criteria
- A prompt longer than one chunk is no longer answered from its last chunk alone, by option 1 or option 2.
- A test through the
LanguageModelinterface asserts it: for option 1, thatsupports_chunked_prefill()isfalseon a tiny loaded model; for option 2, that a prompt longer than one chunk yields the same last-position logits chunked and unchunked. - The audit table above is confirmed or corrected in a comment on this issue. Option 1 needs the table re-checked at merge time; option 2 needs a fresh audit of every
ModelOwnedSequenceStateuser. - If option 2 lands,
KimiK3Model::supports_chunked_prefill(kimi_k3.rs:2031) is removed in the same change andkimi_k3_opts_out_of_chunked_prefillis updated or deleted. - Follow-ups are filed for the five other affected families, referencing this issue.
- Real-checkpoint validation is recorded in the PR body: prompt length, both answers, flag settings.
Verification
cargo test --workspace --profile test-fast --features metal,accelerate kimi_linear
cargo test --profile test-fast -p mlxcel-core generate::tests::supports_chunked_prefill
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --check
No Kimi Linear conversion is in the benchmark store today (models/ holds kimi-k3-2l-mla-synth, kimi-k3-8l-mxfp4, kimi-k3-tokenizer, kimi-vl-a3b-thinking-4bit), so fetch one first:
./target/release/mlxcel download mlx-community/Kimi-Linear-48B-A3B-Instruct-4bit
./target/release/mlxcel generate -m models/Kimi-Linear-48B-A3B-Instruct-4bit -p "$(cat long_prompt.txt)" -n 64
MLXCEL_PREFILL_CHUNK=0 ./target/release/mlxcel generate -m models/Kimi-Linear-48B-A3B-Instruct-4bit -p "$(cat long_prompt.txt)" -n 64
A pass is the two runs agreeing. Build long_prompt.txt at over 2048 tokens with the answer-bearing fact in its first 2048 tokens and the question at the end, so a truncated prefill cannot guess it; before the fix the two runs disagree.
Technical Considerations
Related: #1334 and PR #1741 (where the defect was found, and where Kimi K3 took option 1), #672 (chunked prefill's memory motivation), #674 (the supports_chunked_prefill policy and its default).
- Lingua principale
- Rust
- Stelle
- 471
- Fork
- 55
- Merge medio
- 9h 10m
- PR unite (30g)
- 303
Preparare l'ambiente
- Nessun Dockerfile né file Docker Compose
- Ha un modello di pull request
- Leggi la guida per i contributori
Come iniziare
- Leggi tutta la issue e poi la guida ai contributi del progetto.
- Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
- Fai un fork del repository e lavora su un branch.
- Apri una pull request che faccia riferimento al numero della issue.
Altre issue di lablup/mlxcel
-
area:core priority:low status:ready type:chore
Difficoltà 2/5 1-3 ore Idoneità per principianti 90/100
I maintainer di solito rispondono entro 1 giorno
-
priority:low status:ready type:docs
Difficoltà 1/5 Meno di un'ora Idoneità per principianti 95/100
I maintainer di solito rispondono entro 1 giorno
-
docs(webpage): add a webpage/site README covering the pnpm/uv/zensical build and deploy contractApertapriority:low status:ready type:docs
Difficoltà 1/5 1-3 ore Idoneità per principianti 86/100
I maintainer di solito rispondono entro 1 giorno
-
priority:low status:ready type:docs
Difficoltà 2/5 1-3 ore Idoneità per principianti 92/100
I maintainer di solito rispondono entro 1 giorno
-
priority:medium status:ready type:docs
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
I maintainer di solito rispondono entro 1 giorno
Tutte le issue di lablup/mlxcel
Issue simili
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 88/100
aws-samples/sample-pacer#76 ·
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 68/100
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 78/100
I maintainer di solito rispondono entro 1 giorno
-
Difficoltà 2/5 1-3 ore Idoneità per principianti 86/100
axodotdev/cargo-dist#2523 ·
I maintainer di solito rispondono entro 2 giorni