Conversation
The CPU AOT mamba ops are decode-only: causal_conv1d_update_cpu and fused_sigmoid_gating_delta_rule_update_cpu have no target-verify variant, and scatter_mamba_states_after_mtp_verify is CUDA-only. That leaves DFlash unusable on CPU for hybrid GDN models such as Qwen3.5-9B. DFlash drafts a linear chain (retrieve_next_token[t] == t + 1, no siblings), so the Triton kernels' parent walk degenerates to a rolling window and the draft block is the decode kernel replayed token by token. Both verify paths replay it against a scratch state so the committed pool only sees the final value, and raise NotImplementedError on a branching tree rather than silently computing the wrong thing. The conv path has to replay the C++ kernel rather than call F.conv1d: the decode path passes is_vnni=true, so layer.conv_weights is already VNNI-prepacked and is not usable as a plain [dim, width] conv kernel. Each replay is tested against an independent implementation rather than a transcription of the code under test -- the conv path against the C++ decode kernel, the SSM path against chunk_gated_delta_rule_cpu, which solves the same recurrence in matrix form and takes the draft block as one variable-length sequence, so it also pins the token packing. Qwen3.5-9B on Xeon, TP4, 20 prompts, greedy, block size 16: output 29.24 -> 65.44 tok/s (2.24x), mean TPOT 33.83 -> 14.70 ms, accept length 6.07. Generated text is identical to the non-speculative baseline. Builds on sgl-project#36782, which enables DFLASH on CPU.
ekintel
requested review from
BBuf,
DarkSharpness,
FlamingoPg,
Fridge003,
HaiShaw,
HydraQYH,
Qiaolin-Yu,
celve,
hebiao064,
ispobock,
merrymercy,
yizhang2077 and
yuan-luo
as code owners
September 3, 2026 17:51
Target verify replays the draft block one token at a time so that each token sees the state left by its parent. On CPU that meant block_size sequential calls into fused_sigmoid_gating_delta_rule_update_cpu per layer, plus an index_put_ per token to snapshot the state: 768 dispatches per rank per step for a 24-layer model at block size 16. fused_sigmoid_gating_delta_rule_update_spec_cpu walks the block inside the kernel instead. Tokens already arrive packed as n * steps + t, so one work item per (sequence, v_head) can carry the state forward across t and write each snapshot straight into intermediate_states_buffer. The conv snapshots are batched the same way: the state after token t is the width-1 window of [initial_state, block] ending at t, so all of them are a single strided copy rather than an index_put_ per token. The conv outputs still come from the per-token decode kernel, which is what test_verify_matches_cpp_decode_kernel checks against. Qwen3.5-9B, tp4, block size 16, 20 HumanEval prompts at concurrency 1: 65.44 -> 84.15 output tok/s, mean TPOT 14.70 -> 11.28 ms. Acceptance length is unchanged at 6.1.
The verify path replayed the decode conv kernel once per draft token, so a 16-token block issued 16 dispatches per layer per step and rebuilt the rolling window from the cache each time. The decode kernel already keeps that window in registers across the whole block it is given: only the first token reaches back into the incoming conv state. So one call over the full block computes the same outputs. causal_conv1d_verify_cpu does that, and in the same pass writes the per-token state snapshots the verifier needs and commits the final state from the last snapshot. The snapshot pass partitions over the channel axis rather than the token axis, because the deduplicated conv window is an overlapping as_strided view in which neighbouring tokens share bytes. Qwen3.5-9B, TP4, block size 16, 20 prompts, greedy, concurrency 1: 84.15 -> 94.44 tok/s, mean TPOT 11.28 -> 9.97 ms. Accept length is unchanged at 6.11, as expected from replaying the same arithmetic.
5 tasks
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.
Motivation
This PR enables DFlash speculative decoding for hybrid GDN models such as Qwen3.5-9B on CPU. The CPU AOT mamba ops are decode-only:
causal_conv1d_update_cpuhas no target-verify variant (nointermediate_conv_window/ chain metadata).fused_sigmoid_gating_delta_rule_update_cpuhas no target-verify variant (nointermediate_states_buffer).scatter_mamba_states_after_mtp_verifyraisesValueError: ... only supports CUDA tensors.This PR adds those three CPU paths so the DFlash verify step runs end to end on CPU.
Builds on #36782, which enables the
DFLASHalgorithm on CPU.Modifications
Modifications
DFlash drafts a linear chain (
retrieve_next_token[t] == t + 1, no siblings), so the Triton kernels' ancestor walk degenerates to a rolling window and the draft block is a plain sequential walk over the draft tokens.csrc/cpu/mamba/fla.cpp—fused_sigmoid_gating_delta_rule_update_spec_cpu, a target-verify variant of the decode kernel. Verify tokens already arrive packed asn * steps + t, so one work item per(sequence, v_head)carries the recurrence forward acrosstand writes each per-step snapshot straight intointermediate_states_buffer. The committedssm_statespool is only written at the end, and only whendisable_state_updateis false.csrc/cpu/mamba/conv.cpp—causal_conv1d_verify_cpu, the same idea for the conv. The decode kernel'stinygemm_kernelalready keeps its rolling window in registers across the whole block it is handed, and only readsconv_statesfor the first token, so one call over the full block is arithmetically identical to replaying it per token. A second pass writes the per-token state snapshots and a third commits the final state from the last snapshot. The snapshot pass partitions over the channel axis, not the token axis, because the deduplicated conv window is an overlappingas_stridedview in which neighboring tokens share bytes.sgl_kernel/mamba.py— target-verify replay for the conv and SSM ops. Each dispatches the block once through the kernel above, after normalising the input to the token-major layout the kernels stride over. Both validate the chain metadata and raiseNotImplementedErroron a branching tree rather than silently computing the wrong thing.mamba_state_scatter_triton.py— torch fallback forscatter_mamba_states_after_mtp_verify, reproducing the fused kernels' contract including their silent out-of-range skips (which is how requests with nothing to commit are excluded).gdn_triton.py— bind the CPU implementations on theis_cpu()branch.Two details worth calling out:
F.conv1d. The CPU decode path passesis_vnni=true, solayer.conv_weightsis already VNNI-prepacked bycausal_conv1d_weight_packand is not usable as a plain[dim, width]convolution kernel. Feeding it toF.conv1dproduces garbage with a 0.0 acceptance rate.intermediate_state_indicesarrives pool-sized.build_verify_intermediate_state_indicesreturns anarange(pool_size)padded table that the kernels read positionally per request, so the SSM path slices the firstbatchrows. This is benign at batch 1 and crashes at batch 3.Accuracy Tests
test/registered/cpu/test_causal_conv1d.pyandtest/registered/cpu/test_mamba.py(registered CPU CI).Each replay is checked against an independent implementation rather than a transcription of the code under test:
test_verify_matches_cpp_decode_kernel).chunk_gated_delta_rule_cpu(test_chain_verify_matches_chunk_kernel), which solves the same recurrence in matrix form and consumes the draft block as one variable-length sequence — so it also pins then * steps + ttoken packing that the replay's slicing assumes.Plus window layout, state-roll, untouched-slot, output-stride and tree-rejection cases.
Both files in full, on this branch, on a Xeon host:
End to end, Qwen3.5-9B greedy on CPU produces text identical to the non-speculative baseline.
Speed Tests and Profiling
Qwen3.5-9B, Xeon 6 (128 physical cores), TP4,
--attention-backend intel_amx, 20 prompts, greedy, concurrency 1,--speculative-dflash-block-size 16. Custom dataset consists of the first 20 rows of the HumanEval test split.The two fused kernels are what make that number. Replaying the block token by token cost 768 SSM and 768 conv dispatches per rank per step on a 24-layer model at block size 16; folding the SSM replay into one dispatch took the same configuration from 65.44 to 81.53 tok/s, and folding the conv replay took it from 84.15 to 94.44. Accept length is unchanged across all of them, which is the expected signature of replaying the same arithmetic in fewer calls.
Checklist
cc @mingfeima @yizhang2077 @ch-wan
CI States
Latest PR Test (Base): ❌ Run #33905040429
Latest PR Test (Extra): ❌ Run #33905040261
Latest PR Test (AMD ROCm 7.2): ❌ Run #33905040567