Fix DeepSeek decode running the prefill attention path under NNX - #4634
Open
ecnal-cienet wants to merge 1 commit into
Open
Fix DeepSeek decode running the prefill attention path under NNX#4634ecnal-cienet wants to merge 1 commit into
ecnal-cienet wants to merge 1 commit into
Conversation
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
DeepSeekGenericLayer.attention_op passed the model_mode captured at __init__ to attention instead of the one its __call__ receives; self_attention_with_norm_op did the same on the MHC branch. MaxEngine's NNX path builds the model once in PREFILL mode and passes the mode per call, so every autoregressive step still read model_mode as "prefill" and took the prefill KV-cache branch. Prefill logits stay correct, so the first token is right and everything after it is wrong: deepseek2-16b decode emitted one repeated token for all 1024 positions. The bug was latent under Linen, which re-binds layers on each apply. Thread the call-time model_mode through both helpers, and have _nnx_run_model merge the graphdef built for the requested mode so a layer repeating the mistake cannot silently corrupt decode. Add two CPU-only tests that need no checkpoint: one asserting a decoder layer attends in the mode it was called with, in both directions across eight layer types; one asserting greedy decode through prefill/insert/generate matches the argmax rollout of a plain forward pass over the same prefix.
ecnal-cienet
force-pushed
the
fix/nnx-deepseek-decode-model-mode
branch
from
July 28, 2026 00:19
8a74015 to
a263725
Compare
xibinliu
approved these changes
Jul 28, 2026
xibinliu
left a comment
Collaborator
There was a problem hiding this comment.
Good catch of the model_mode difference for deepseek attention_op
ecnal-cienet
marked this pull request as ready for review
July 28, 2026 15:36
ecnal-cienet
requested review from
Lumosis,
NuojCheng,
RissyRan,
SurbhiJainUSC,
abhinavclemson,
aireenmei,
bvandermoon,
gagika,
gobbleturk,
gpolovets1,
huytransformer,
igorts-git,
jiangjy1982,
jrplatin,
khatwanimohit,
mailvijayasingh,
mitalisi,
parambole,
patemotter,
richjames0,
shralex,
shuningjin,
suexu1025 and
vipannalla
as code owners
July 28, 2026 15:36
ecnal-cienet
requested review from
A9isha,
darisoy,
dipannita08 and
hengtaoguo
as code owners
July 28, 2026 15:36
|
Caution The consumer version of Gemini Code Assist on GitHub has been sunset. All code review activity has officially ceased. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Decoding
deepseek2-16bthrough MaxEngine on the NNX path emits a single repeated token. On the nightly checkpoint (v6e-8,maxtext.inference.decode, greedy,mla_naive_kvcache=false):Root cause
DeepSeekGenericLayer.attention_oppassedmodel_mode=self.model_modeto attention — the value captured at__init__— instead of themodel_modeits__call__receives. Same inself_attention_with_norm_opfor the MHC branch.MaxEngine's NNX path builds the model once in PREFILL mode and passes the mode per call:
_nnx_run_modelalways mergedself.graphdef, which is the prefill graphdef. So on every autoregressive step each DeepSeek layer still readself.model_mode == "prefill"and took the prefill KV-cache branch. Prefill logits stay correct, which is why the first token is right and everything after it is wrong.Only DeepSeek is affected — llama2 and the other families already forward the call-time
model_mode. A sweep of every non-__init__read ofself.model_modeinsrc/maxtextfound DeepSeek to be the only one feeding a stale mode into attention.The bug was latent under Linen, which re-binds layers on each
applysoself.model_modeis always current. It only surfaces once a model object is built once and reused across modes, which is exactly what pure NNX does.Fix
models/deepseek.pyattention_opandself_attention_with_norm_optakemodel_modeand forward the call-time value.models/deepseek4.pyDeepSeek4DecoderLayerinherits both helpers).inference/maxengine/maxengine.pygraphdef_arand merge with the graphdef built for the requested mode.Either change alone restores correct output; both are included deliberately, so a future layer repeating the mistake cannot silently corrupt decode and the two protections cannot regress together unnoticed.
Tests
Two new CPU-only files, neither needing a checkpoint. They close a real gap: golden-logit tests only exercise the forward pass, so nothing covered the prefill → autoregressive handoff.
tests/unit/decoder_layer_model_mode_test.py— 16 cases. Asserts at the attention boundary that a layer attends in the mode it was called with. Covers deepseek dense/moe, llama2, mistral, gemma, gemma2, qwen2 and qwen3, in both directions (built-prefill/called-AR and built-AR/called-prefill) so no layer can pass by hardcoding a mode.tests/unit/deepseek_decode_consistency_test.py— 4 cases. Greedy decode through prefill/insert/generate must equal the argmax rollout of a plain forward pass over the same prefix. Parameterized over deepseek-moe, deepseek-dense and a generic model, so adding a family is one line. Includes a separate guard thatgeneratemerges an autoregressive graphdef, covering the engine side on its own.Verification
All on
wanglance-v6e-8(TPU v6 lite, 8 chips). Rows marked unmodifiedmainwere run in a throwawaygit worktree, so the branch was never reverted.-> a weighted sum of the values, where the weights are computed by a function of the query and the keys.main-> a a a a a a a a a ...for all 1024 tokens — the bug reproducedChecklist
Before submitting this PR, please make sure (put X in square brackets):
gemini-reviewlabel.