Conversation
… draft head loads
DFlash never finishes its profile run when the draft checkpoint is
quantized. `_build_context_kv_buffers` fuses the per-layer K/V projections
into one matrix by slicing rows out of `qkv_proj.weight`:
kv_weights = [a.qkv_proj.weight[a.q_size :] for a in layers_attn]
That attribute holds the dense `[N, K]` matrix only for an unquantized
layer. A quantized checkpoint keeps packed codes there -- NVFP4 packs two
values per byte, so the tensor is `[N, K // 2]` -- and the fused matrix
comes out half as wide. `_project_context_kv` then fails:
RuntimeError: mat1 and mat2 shapes cannot be multiplied
(2048x5120 and 2560x5120)
Reproduced on Qwen3.8-27B with an NVFP4 DFlash2 draft head derived from
incoai/Qwen3.8-27B-DFlash2. The engine dies in `determine_available_memory`
-> `profile_run` -> `speculator.propose` before serving a single token, so
DFlash is currently unusable with any quantized draft head, on any device
class.
Reach the dense rows through the layer's own quantization method instead:
feeding an identity matrix through `quant_method.apply` returns the
transposed weight whatever the packing is, so this needs no knowledge of
NVFP4, FP8 or marlin layouts. The unquantized path is untouched and still
fuses eagerly.
The rebuild has to be deferred. `_build_fused_kv_buffers` runs at the end of
`load_weights`, before `process_weights_after_loading`, so `quant_method` is
not usable yet; the fusion is therefore built on the first context
projection, when loading has completed. It costs one dense fp16 copy of the
K/V rows (about 52 MiB per rank on Qwen3.8-27B) and one GEMM per layer,
once. The per-step decode path keeps using the quantized weights, which is
where the win is -- the context projection is prefill work.
Measured on Qwen3.8-27B (TP2, DFlash2 k=7, greedy, 5 runs, median), fp16
draft head against the NVFP4 one:
2x Quadro RTX 8000 72.72 -> 77.13 tok/s (+6.1 %)
2x Tesla V100-PCIE 74.02 -> 76.33 tok/s (+3.1 %)
The answer text is byte-identical in every run (sha256 prefix
0106659946c064b1); acceptance length moves 3.353 -> 3.325. That is expected
and is the whole safety argument for quantizing a draft head: only tokens
the target model would have produced anyway are accepted, so quantization
can cost acceptance rate but never correctness.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Signed-off-by: Peuqui <peuqui@github.com>
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.
Purpose
DFlash never finishes its profile run when the draft checkpoint is quantized.
DFlashQwen3Model._build_context_kv_buffersfuses the per-layer K/Vprojections into one matrix by slicing rows out of
qkv_proj.weight:That attribute holds the dense
[N, K]matrix only for an unquantized layer.A quantized checkpoint keeps packed codes there — NVFP4 packs two values per
byte, so the tensor is
[N, K // 2]— and the fused matrix comes out half aswide.
_project_context_kvthen fails:The abort happens in
determine_available_memory→profile_run→speculator.propose, before a single token is served, so DFlash is currentlyunusable with any quantized draft head on any device class. Reproduced on
Qwen3.8-27B with an NVFP4 draft head derived from
incoai/Qwen3.8-27B-DFlash2; the line is unchanged onmain.Fix
Reach the dense rows through the layer's own quantization method: feeding an
identity matrix through
quant_method.applyreturns the transposed weightwhatever the packing is, so this needs no knowledge of NVFP4, FP8 or marlin
layouts. The unquantized path is untouched and still fuses eagerly.
The rebuild has to be deferred.
_build_fused_kv_buffersruns at the end ofload_weights, beforeprocess_weights_after_loading, soquant_methodisnot usable yet; the fusion is built on the first context projection instead.
It costs one dense fp16 copy of the K/V rows (~52 MiB per rank on
Qwen3.8-27B) and one GEMM per layer, once. The per-step decode path keeps
using the quantized weights, which is where the win is — the context
projection is prefill work.
Result
Qwen3.8-27B, TP2, DFlash2 k=7, greedy, 5 runs, median, fp16 draft head
against the NVFP4 one:
The answer text is byte-identical in every run (sha256 prefix
0106659946c064b1); acceptance length moves 3.353 → 3.325. That is expectedand is the safety argument for quantizing a draft head at all: only tokens
the target model would have produced anyway are accepted, so quantization can
cost acceptance rate but never correctness.
Not a duplicate
Checked on 2026-09-10 against
1CatAI/1Cat-vLLM:No open PR or issue covers this crash. The nearest open PR, #561 (Reduce
DFlash2 weight and scale memory on SM70, draft), shares NVFP4 codes between
QPN2 and TurboMind in the target model and does not touch
qwen3_dflash.py. The nearest issue, #478 (DFlash2 acceptance=0 on V100TP4), is an acceptance problem on a running server, not a load failure.
Test Plan
New file
tests/kernels/attention/test_dflash2_context_kv_quantized.py,CPU-only, using a minimal fake quantization method so no checkpoint is
needed:
.weightis packed and the dense weight is reachable only throughquant_method.apply.Commands (full outputs under Test Result):
python -m pytest tests/kernels/attention/test_dflash2_context_kv_quantized.py \ tests/kernels/attention/test_dflash2_context_pipeline.py -v python -m pytest tests/v1/spec_decode/test_dflash2_alignment_rank.py \ tests/v1/spec_decode/test_dflash2_ngram_assist.py \ tests/v1/spec_decode/test_dflash2_structured_output.py -q pre-commit run --files vllm/model_executor/models/qwen3_dflash.py \ tests/kernels/attention/test_dflash2_context_kv_quantized.py pre-commit run mypy-3.10 --hook-stage manual --files <same files>Test Result
The complete existing
test_dflash2_context_pipeline.pywas run, not justthe new tests. With the fix reverted, the two quantized tests fail and the
unquantized one still passes, so they do catch this bug and do not constrain
the dense path.
AI assistance
This change was prepared with AI assistance (Claude). I reviewed every
changed line, ran the tests and linters shown above on my own hardware, and
can defend the change end-to-end.
Essential Elements of an Effective PR Description Checklist
supported_models.mdandexamplesfor a new model.