Repository navigation
[Spec] LiLiCorr: named head MLPs and quantized head linears - #42057
Conversation
Name the head's MLPs up_proj/down_proj with a separate feature_norm, the layout ModelOpt now exports, and keep loading the previous nn.Sequential names. Route the head's projections through quant_config so a quantized head loads; factor_input_proj/out_head/in_head stay dense since their raw weights are fused into inference buffers, and feature_mlp since its five inputs fit no quantized kernel. Give every DFlash draft's layers their module prefix and a quant-aware fc, so exclude lists resolve and a quantized fc loads for any draft.
|
cc @elvischenv @rystewart-nvidia for vis |
…ide W4A16 Quantized linear methods take [tokens, features] input, and pass_hidden is a non-contiguous slice once the batch exceeds one request. Only NVFP4 W4A16 registers an input_scale it never reads, so only it may skip that tensor.
There was a problem hiding this comment.
Please remove this newly added test. Add any important tests back to test_dflash_lilicorr.py
There was a problem hiding this comment.
Removed. test_dflash_lilicorr.py is an end-to-end server test that stays disabled until a
checkpoint is published, so I did not add unit tests to it.
| for name, weight in weights: | ||
| stripped = name[len("model.") :] if name.startswith("model.") else name | ||
| if stripped.startswith("lilicorr."): | ||
| # Older exports index the head MLPs as nn.Sequential. |
There was a problem hiding this comment.
Do we really need to support this mapping
There was a problem hiding this comment.
No, no LiLiCorr checkpoint with the old names is published, so I removed the mapping.
There was a problem hiding this comment.
Seems like all the changes in this file just remove a special path for self.is_nemotron_35_draft. Please confirm if this is intended @mrusanovsky
There was a problem hiding this comment.
That was wider than needed. With a target --quantization fp8, which the draft
inherits, every DFlash draft's fc became FP8 too. Now only drafts that set
supports_quantization (LiLiCorr) take that path; other DFlash drafts are unchanged from main.
| return out.view(*x.shape[:-1], -1) | ||
|
|
||
|
|
||
| class LiLiCorrMLP(nn.Module): |
There was a problem hiding this comment.
Q: is LiliCorr module not intended to be sharded under TP?
There was a problem hiding this comment.
Correct, it is not sharded. The head is replicated on every rank, as on main, where it was
nn.Linear; this PR only swaps in ReplicatedLinear so it can be quantized. At tp_size > 1,
LiLiCorr drafting also falls back to eager mode, as on main, and this PR does not change that.
One addition: a W4A16 head now fails at load when tp_size > 1, because its Marlin kernel does not
give bit-identical results on every rank, which a replicated head needs.
There was a problem hiding this comment.
At tp_size > 1,
LiLiCorr drafting also falls back to eager mode, as on main, and this PR does not change that.
We should fix this
There was a problem hiding this comment.
Done: at tp_size > 1 the LiLiCorr head now runs inside the draft CUDA graph, as at TP=1, instead of falling back to eager. Its token table is replicated on every rank (built per vocab shard and all-gathered once at load), as DFlash2's selector codebooks are, so the per-step path is the same as at TP=1. Verified at TP=2 and TP=4: same acceptance, greedy and sampled.
nvpohanh
left a comment
There was a problem hiding this comment.
[by Claude Code] Re-reviewed at 5c6efdb. The two earlier inline findings are fixed at this head. New findings: two inline comments, plus the two PR-level comments below.
[by Claude Code] Severity: style | Confidence: High
Issue
The description no longer matches the code. It says old nn.Sequential names "are renamed at load", but e201f48 removed that mapping, so a checkpoint with feature_mlp.1 or layers.N.mlp.0 now fails check_head_weight_coverage. It also says every DFlash draft now gets layer prefixes and a quant-aware fc, but 9e96f49 limits that to LiLiCorr and Nemotron 3.5 drafts. It does not mention that a W4A16 head now requires tp_size 1.
Fix
Update Motivation item 1 and the models/lilicorr.py and models/dflash.py bullets, and add the W4A16 TP limit.
[by Claude Code] Severity: style | Confidence: High
Issue
The accuracy and speed table names the target, GPU, and dataset, but gives no commands. A reviewer cannot rerun a row or see which speculative flags "draft_length 15" means.
Fix
Add the exact server launch command for each row (the --speculative-* flags, plus --quantization fp8 for the fp8 rows) and the gsm8k eval command. The drafts are not published, so also say how the bf16 and W4A16 drafts were produced, for example the ModelOpt export command.
|
/rerun-test test/registered/e2e/models/test_nvidia_nemotron_3_nano.py test/registered/core/test_basic_sanity_dflash.py test/registered/e2e/models/test_glm53_flash_b200.py test/registered/spec/dflash/test_dflash.py test/registered/e2e/speculative/test_dflash_domino.py test/registered/spec/test_gemma4_dflash_31b_extra.py test/registered/spec/test_spec_mixed_chunk.py test/registered/spec/dflash/test_muse_glimmer_dflash_assistant_gsm8k.py test/registered/cuda_graph/piecewise/test_pcg_with_speculative_decoding_dflash.py test/registered/unit/spec/test_dflash_logits.py test/registered/unit/spec/test_dflash_domino.py test/registered/kernels/ops/speculative/test_lilicorr_cuda.py test/registered/unit/spec/test_dflash_overlap_hostsync.py test/registered/unit/spec/test_dflash_extra_buffer_lazy.py test/registered/kernels/ops/speculative/test_dflash_domino.py test/registered/unit/spec/test_oot_dflash_hooks.py test/registered/unit/model_executor/model_runner_components/test_spec_aux_hidden_state.py test/registered/unit/layer_boundary/test_glm5_next_dflash_capture.py |
|
Results for 🚀 🚀 🚀 🚀 🚀 |
|
/rerun-test test/registered/e2e/models/test_nvidia_nemotron_3_nano.py test/registered/core/test_basic_sanity_dflash.py test/registered/e2e/models/test_glm53_flash_b200.py test/registered/spec/dflash/test_dflash.py test/registered/e2e/speculative/test_dflash_domino.py test/registered/spec/test_gemma4_dflash_31b_extra.py test/registered/spec/test_spec_mixed_chunk.py test/registered/spec/dflash/test_muse_glimmer_dflash_assistant_gsm8k.py test/registered/cuda_graph/piecewise/test_pcg_with_speculative_decoding_dflash.py test/registered/unit/spec/test_dflash_logits.py test/registered/unit/spec/test_dflash_domino.py test/registered/kernels/ops/speculative/test_lilicorr_cuda.py test/registered/unit/spec/test_dflash_overlap_hostsync.py test/registered/unit/spec/test_dflash_extra_buffer_lazy.py test/registered/kernels/ops/speculative/test_dflash_domino.py test/registered/unit/spec/test_oot_dflash_hooks.py test/registered/unit/model_executor/model_runner_components/test_spec_aux_hidden_state.py test/registered/unit/layer_boundary/test_glm5_next_dflash_capture.py |
|
Results for 🚀 🚀 🚀 🚀 🚀 |
|
/rerun-test test/registered/e2e/models/test_nvidia_nemotron_3_nano.py test/registered/core/test_basic_sanity_dflash.py test/registered/e2e/models/test_glm53_flash_b200.py test/registered/spec/dflash/test_dflash.py test/registered/e2e/speculative/test_dflash_domino.py test/registered/spec/test_gemma4_dflash_31b_extra.py test/registered/spec/test_spec_mixed_chunk.py test/registered/spec/dflash/test_muse_glimmer_dflash_assistant_gsm8k.py test/registered/cuda_graph/piecewise/test_pcg_with_speculative_decoding_dflash.py test/registered/unit/spec/test_dflash_logits.py test/registered/unit/spec/test_dflash_domino.py test/registered/kernels/ops/speculative/test_lilicorr_cuda.py test/registered/unit/spec/test_dflash_overlap_hostsync.py test/registered/unit/spec/test_dflash_extra_buffer_lazy.py test/registered/kernels/ops/speculative/test_dflash_domino.py test/registered/unit/spec/test_oot_dflash_hooks.py test/registered/unit/model_executor/model_runner_components/test_spec_aux_hidden_state.py test/registered/unit/layer_boundary/test_glm5_next_dflash_capture.py |
|
Results for 🚀 🚀 🚀 🚀 🚀 |
|
test_pcg_with_speculative_decoding_dflash.py is not a breakage caused by this PR. It is a known failure. |
Motivation
#37462 added the LiLiCorr draft head. Its
checkpoints name the head's MLP weights by
nn.Sequentialindex (feature_mlp.1,layers.N.mlp.0). ModelOpt now exports descriptive names instead (feature_mlp.up_proj,layers.N.mlp.down_proj, see NVIDIA/Model-Optimizer#2601),the layout vLLM loads, so one checkpoint serves on both engines.
This PR:
linears and the DFlash
fcare plainnn.Linearthat ignorequant_config, so packed weightsand their scales have nowhere to go.
Modifications
models/lilicorr.pynn.Sequential; they are nowup_proj/down_projmodules, with theinput
LayerNormas a separatefeature_norm.nn.Linear; they are nowReplicatedLinearwithquant_configand a module prefix, so a checkpoint's quantized layers and its exclude list arehonored. Two kinds stay
nn.Linear:factor_input_proj/out_head/in_head, whose raw weightsare fused into inference buffers at load, and
feature_mlp, whose five inputs fit no quantizedkernel.
input_scaleis allowed only for NVFP4 W4A16, which registers one itnever reads.
--fp4-gemm-backendresolves tomarlin, the default below SM100) is rejected attp_size > 1: every rank runs the replicatedhead, and Marlin's atomic-add reduce does not give the same draft on each rank.
slot_embeddingandinput_size/output_size, which stay validwhen a weight is packed to uint8.
models/dflash.pyfcwithquant_config. Asupports_quantizationclass flag, set by LiLiCorr, now opts a draft into that path; other DFlashdrafts are unchanged.
speculative/lilicorr_utils.pyinput_size/output_size, for the same reason as above.tp_size > 1the head is folded into the draft CUDA graph instead of running eager. Itspre-projected token table is replicated on every rank, as DFlash2's selector codebooks are: each
rank projects its vocab shard and the shards are all-gathered once at load, so candidates, which
carry global ids, index it directly and the per-step path is the same as at
tp_size 1.Accuracy Tests
Qwen3-8B target, one H100, gsm8k (1319), greedy, fa3,
draft_length 15, one server at a time onthe node.
fc.weight shape mismatch--quantization fp8--quantization fp8change bf16 rounding in the head; acceptance moves by ±0.02 either way (concurrency 2 / 4 / 8:
main 6.992 / 7.000 / 6.994, this PR 7.005 / 6.976 / 6.981).
W4A16_NVFP4format. Main stops atthe first packed weight,
fc(uint8(4096, 10240)against the(4096, 20480)nn.Linear).--quantization fp8rows: sglang gives the draft the target's quantization by default. On mainthat reached only the draft's decoder layers; it now also reaches the head and
fc, with acceptance within 0.001.tp_size 2, bf16, eager head (main) vs head in the draft graph (this PR): concurrency 17.0196 / 1016 tok/s vs 7.0211 / 1040 tok/s; concurrency 4 6.9783 / 3144 vs 6.9860 / 3284;
concurrency 64 6.9439 / 14998 vs 6.9684 / 15745. Target
--quantization fp8, concurrency 1:main 7.0174, this PR 7.0203. W4A16 is rejected at load.
tp_size 4, bf16, this PR: concurrency 1 7.0154 / 1322 tok/s; concurrency 64 6.9784 / 19305.Each row is one run of ModelOpt's
examples/specdec_bench(acceptance length is itsBlock_Weighted_AL), with concurrencyC:It starts
sgl.Enginewith the equivalent of:The drafts are not published. All three are one trained Qwen3-8B LiLiCorr draft: as trained,
with the head's MLP keys renamed, and packed by a short script into ModelOpt's
W4A16_NVFP4format.
Checklist
CI States
Latest PR Test (Base): ❌ Run #37250210108
Latest PR Test (Extra): ❌ Run #37250210119
Latest PR Test (AMD ROCm 10): ❌ Run #37250209913