Repository navigation
Conversation
The selector cross-entropy now uses the backbone loss's own per-position weights (decay or D-PACE) and divides by the backbone's weight sum, misses included. Before, every covered position counted equally and the term was divided by the covered positions alone. A block position's choice only matters if every earlier position was accepted, which is what the backbone's weighting models, so the selector now spends its capacity the same way. Dividing by the covered positions alone also scaled the term up whenever the backbone's top-k recall dropped. This matches the SGLang/SpecForge DFlash2 reference, and it is the objective the released Muse-Spark-Open DFlash2 drafter was trained with. The selector reads the backbone's mask and weights from _DFlashLossTerms instead of rebuilding the mask, so the two cannot drift apart. The module docstring still described the gold-token substitution that the strict top-k replaced; it now describes the current objective. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
|
Auto-sync is disabled for draft pull requests in this repository. Workflows must be run manually. Contributors can view more details about this message here. |
Contributor
|
Important Draft PR not reviewedDraft PRs are not automatically reviewed by default.
To automatically review draft PRs, update your CodeRabbit configuration: reviews:
auto_review:
drafts: true
Comment |
This was referenced Oct 30, 2026
[3/7] fix(speculative): keep the streaming corpus memory-mapped instead of copying it per rank
#3142
Closed
Contributor
Author
|
Folded into #2617 together with #2617's own fix and the other two small fixes, rebased onto |
h-guo18
added a commit
that referenced
this pull request
Nov 5, 2026
…n DFlash2 recipe (#2617) <!-- stack:start --> **Stack** (merge in this order; each PR's base is the branch of the one before it). [4/4] reproduces [nvidia/Muse-Spark-Open-816B-A42B-DFlash2](https://huggingface.co/nvidia/Muse-Spark-Open-816B-A42B-DFlash2) and needs all of them. 1. #2617: four training fixes: draft RoPE in fp32, streaming capture on vLLM >= 0.29, a memory-mapped streaming corpus, DFlash2's selector loss weighted like the backbone's **<- this PR** 2. #3001: out-of-order training batches; compile + FlexAttention in the DFlash2 recipe 3. #3010: fused DFlash-family loss 4. #3144: Muse-Spark-Open DFlash2 recipes --- <!-- stack:end --> ### What does this PR do? Type of change: Bug fix Four independent fixes to speculative-decoding training, all needed to reproduce the Muse-Spark-Open DFlash2 drafter. They used to be four PRs (#2617, #2522, #3142, #3143). The other three are closed and point here. Each fix is its own commit, and the streaming connector takes two. Review follow-ups come after them. All four touch only `modelopt/torch/speculative`. The whole PR is rebased onto `main` after #2757, the DFlash-family refactor. #### 1. The draft's RoPE frequencies stay in fp32 on a fresh convert On a freshly converted DFlash-family model, `restore_draft_precision()` repeated the `.to(device, dtype)` that `modify()` had already applied. `Module.to()` casts floating buffers too, so this rounded the rotary `inv_freq` that `modify()` had just built in fp32 down to bf16. `examples/speculative_decoding/main.py` calls `restore_draft_precision()` unconditionally, trusting its docstring's "no-op in that case". - **Why it matters.** The rounding hits the frequencies, not the cos/sin, so the phase error grows with position: about 0.4% per frequency, and several radians for the highest-frequency pairs by position 4096. vLLM computes the angles from fp32 frequencies, so the draft trained under different RoPE phases than it was served with. - **Who is affected.** Every DFlash, DSpark, Domino, DFlash2 or LiLiCorr draft trained from scratch through `main.py` since #2483. - **The fix.** `modify()` records whether it placed the draft, and `restore_draft_precision()` returns early when it did. On the `from_pretrained` path, where the base is on the meta device, it still places the draft. - **Measured impact.** An 80-step Gemma-4-E4B DSpark A/B, fp32 against bf16 frequencies, stayed within run-to-run noise. The effect on acceptance length was not measured. #### 2. Streaming hidden-state capture on vLLM >= 0.29 Streaming training, where vLLM serves produce hidden states and the trainer pulls them over NIXL RDMA, did not work on current vLLM. Two of the defects produce wrong data without an error. | Commit | Defect | Symptom | | --- | --- | --- | | 1 | vLLM #51718 changed the KV-cache layout to `[B, H, N, C]` | **Silent.** The trainer sees one plane and hands the draft an empty aux tensor. `planes_dim()` now finds the plane axis from the configured capture count. | | 1 | Captures were addressed by their position in the batch | **Silent.** A capture read another request's hidden states. With concurrency, the hidden/token match was 0.00-0.02 instead of 0.96-1.00. Each request's slots now come from its own block ids. | | 1 | Capture index tensors were built on the wrong stream | A non-deterministic out-of-bounds assert, minutes in. | | 2 | The capture layer is merged into the attention group on uniform-attention models | Loud: serve does not start on Qwen3, Llama and most dense models. The lookup now recognises the merged group in both the worker's and the scheduler's view. | Because two of these are silent, the new checks are loud: - The resolved group and cache view are logged at startup. - Requests whose block ids cannot cover their tokens raise, naming the request. - The first few requests verify every derived slot. The module's logger now sits under `vllm.*`. vLLM configures only its own logger tree, so until now every `logger.info` in this module was dropped, the startup diagnostics included. The connector needs vLLM >= 0.22, for `HiddenStateCacheSpec`, and is tested on vLLM >= 0.29. It reads the KV-cache layout from both before and after vLLM #51718. The `prefer_cross_layer_blocks` override is removed: #51718 deleted the base property, and before that its default was already `False`, the value the override returned. #### 3. The streaming corpus stays memory-mapped `StreamingDataset.__init__` called `list()` on an Arrow-backed `datasets.Dataset`. That decoded the whole corpus into Python objects in every rank, and each forked DataLoader worker then copy-on-wrote the pages it touched. On a 3.1M-row corpus this measured 80 GB per rank, and with 4 ranks × 8 workers it OOM-killed a 950 GB trainer node. An indexable source is now kept as is, so its rows stay memory-mapped and shared across workers. Only an iterator is still materialized. One changelog entry. #### 4. DFlash2's selector loss is weighted like the backbone loss The selector cross-entropy now matches the backbone loss in three ways: - **Weights.** It multiplies each covered position by the backbone's per-position weight (decay or D-PACE). - **Denominator.** It divides by the backbone's weight sum, which counts the positions whose gold token is outside the top-k. Before, every covered position counted equally, and the term was divided by the covered positions alone, so it grew whenever backbone recall dropped. - **Mask.** It reads the backbone's mask, weights and weight sum from `_DFlashLossTerms` instead of rebuilding them, so the two losses cannot drift apart. A block position's choice only matters if every earlier one was accepted, which is what the backbone's weighting models. The SGLang/SpecForge DFlash2 reference trains its selector this way, and the released drafter was trained with this objective. On top of #2757, the selector takes its gold tokens from `_block_tokens` at the variant's label shift. One CHANGELOG entry, because the 0.48.0 release branch already ships DFlash2 with the old objective. ### Usage No API or config change. The selector term's value changes, so a DFlash2 run's logged loss will not match one on the previous objective. ### Testing New tests, each failing on the code it fixes: - `TestDFlashLazyRotaryEmb` gains two tests: a fresh convert keeps `inv_freq` in fp32, and the meta-device `from_pretrained` path still places the draft. - `test_an_indexable_corpus_is_kept_and_an_iterator_materialized` covers the streaming dataset. - `TestCandidateSelectorAlignment::test_positions_weigh_like_the_backbone_and_misses_stay_in_the_denominator` covers the selector. - `tests/gpu_vllm/torch/speculative/plugins/test_rdma_hidden_states_connector.py` covers the connector. One test runs `build_connector_meta` and `save_kv_layer` on CUDA for two prompts prefilled behind a decoding request, whose capture blocks are neither contiguous nor in batch order, and checks that each pool slot holds its own request's hidden states, on both KV-cache layouts. The others check that an ambiguous cache view is refused and that the capture group is found in each of the three shapes vLLM gives it. The file sits in `gpu_vllm` because the connector imports vLLM at module level. Reverting `save_kv_layer` to reading `slot_mapping` by position, or `build_connector_meta` to the attention group's block ids, fails the first test on both layouts. On this branch, rebased onto `main` (e352cac) after #2757, with Python 3.12, torch 2.10 and transformers 5.18.0 on CPU: - `test_hf_{dflash,dflash2,lilicorr,streaming_dataset}.py` and `tests/unit/torch/export/test_hf_spec_rope_export.py`: 244 passed. - The bitwise A/B of the whole stack before and after the rebase is in [3/4]. After the review follow-ups, in a venv with vLLM 0.30.0 (the gpu_vllm CI image's version), torch 2.13 and transformers 5.18.0, on one RTX PRO 6000: the connector tests and `test_hf_streaming_dataset.py`, 27 passed. End to end: - Qwen3-8B DFlash2 streaming on GB200 (1 serve node, TP4, plus 1 trainer node, 4-rank DDP): 600 steps, loss 17.0 -> 3.4. Before the connector fixes, this run failed at three different points. - Muse-Spark-Open DFlash2 streaming on GB300, on the full 3.48M-row corpus, is in [4/4]. ### Before your PR is "*Ready for review*" Make sure you read and follow [Contributor guidelines](https://github.com/NVIDIA/Model-Optimizer/blob/main/CONTRIBUTING.md) and your commits are signed (`git commit -s -S`). Make sure you read and follow the [Security Best Practices](https://github.com/NVIDIA/Model-Optimizer/blob/main/SECURITY.md#security-coding-practices-for-contributors) (e.g. avoiding hardcoded `trust_remote_code=True`, `torch.load(..., weights_only=False)`, `pickle`, etc.). - Is this change backward compatible?: ❌ The streaming connector now needs vLLM >= 0.22 (tested on >= 0.29), listed under Backward Breaking Changes. No API or config change. - If you copied code from any other sources or added a new PIP dependency, did you follow guidance in `CONTRIBUTING.md`: N/A - Did you write any new necessary tests?: ✅ - Did you update [Changelog](https://github.com/NVIDIA/Model-Optimizer/blob/main/CHANGELOG.rst)?: ✅ A Bug Fixes entry for each of the four fixes, and a Backward Breaking Changes entry for the connector's vLLM floor. The 0.48.0 release branch already carries the RoPE regression (#2483) and DFlash2, so those two fixes are to released code as well. - Did you get Claude approval on this PR?: ❌ The Claude review commented rather than approved. Its findings are addressed in the follow-up commits. ### Additional Information The four fixes were found bringing up DFlash2 streaming and the Muse-Spark-Open release. 🤖 Generated with [Claude Code](https://claude.com/claude-code) <!-- This is an auto-generated comment: release notes by coderabbit.ai --> <!-- coderabbit-ai-generated:v1 --> ## Summary by CodeRabbit * **Bug Fixes** * Draft restoration preserves rotary embedding precision and avoids unnecessary device placement, including for models initially loaded on a meta device. * Selector loss now respects per-position weights and includes missed candidates in its denominator. * Hidden-state transfer handles supported cache layouts and request block mappings more reliably. * Fixed memory-mapped training-corpus access and concurrent vLLM hidden-state capture. * **Performance** * Indexable streaming datasets are retained without copying; other iterable inputs are supported. * **Compatibility** * The streaming speculative-decoding connector requires vLLM 0.22 or later. <!-- end of auto-generated comment: release notes by coderabbit.ai --> --------- Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.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.
Stack (merge in this order; each PR's base is the branch of the one before it). [7/7] reproduces nvidia/Muse-Spark-Open-816B-A42B-DFlash2 and needs all of them.
What does this PR do?
Type of change: Bug fix
DFlash2's selector cross-entropy now weighs block positions the way the backbone loss does, and shares the backbone's denominator:
_DFlashLossTerms.weights. Before, every covered position counted equally._DFlashLossTerms.weight_sum, which also counts the positions whose gold token is outside the top-k. Before, it was divided by the covered positions alone, so the term grew whenever the backbone's top-k recall dropped._DFlashLossTerms.supervised_maskinstead of rebuilding it, so the two cannot drift apart.Why. A block position's choice only matters if every earlier position was accepted, and the backbone's weighting models exactly that. With this change the selector spends its capacity where acceptance is decided. This is how the SGLang/SpecForge DFlash2 reference trains its selector, and it is the objective that the released nvidia/Muse-Spark-Open-816B-A42B-DFlash2 drafter was trained with.
What I have not measured. I have no A/B of this change on its own.
mainalready trains the selector on the strict top-k (no gold-token substitution) and zero-initializes its successor codebook. Together with this change, that is the objective that beat the old gold-substituting one by +7.8% (greedy) and +10.1% (temperature 1) MT-Bench acceptance length, in from-scratch Muse-Spark-Open runs of 550K samples each. How that gain splits between the strict top-k and the weighting here, I did not measure.The module docstring still described the gold-token substitution that the strict top-k replaced. It now describes the current objective.
No CHANGELOG entry: DFlash2 first ships in 0.48.0, which is still at a release candidate.
Usage
No API or config change. The selector term's value changes, so a run's logged loss will not match a run on the previous objective.
Testing
TestCandidateSelectorAlignment::test_positions_weigh_like_the_backbone_and_misses_stay_in_the_denominatorintests/unit/torch/speculative/plugins/test_hf_dflash2.py. It checks that the term is the backbone-weighted mean over every supervised position, a miss included. With the previous objective restored, it fails and the other 33 tests in the file pass.pytestof the DFlash-family unit tests (test_hf_dflash.py,test_hf_dflash2.py,test_fakebase.py,test_dflash_fused_head.py,test_hf_lilicorr.py,test_modeling_final_norm.py) on the top of the stack: passed with transformers 5.5.4 and 5.18.0 (CPU).Before your PR is "Ready for review"
CONTRIBUTING.md: N/AAdditional Information
🤖 Generated with Claude Code