镜像站点 · 本页由第三方 GitHub 只读镜像提供,非 GitHub 官方站点,不接受任何登录或凭据输入。前往 github.com
Skip to content

fix(speculative): DSpark and Domino AR validation and label alignment - #2762

Draft
h-guo18 wants to merge 5 commits into
haoguo/dflash-family-cleanupfrom
haoguo/dflash-label-alignment
Draft

h-guo18 wants to merge 5 commits into
haoguo/dflash-family-cleanupfrom
haoguo/dflash-label-alignment

Conversation

@h-guo18

@h-guo18 h-guo18 commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Bug fix

Stacked on #2757 (its branch is the base). DSpark and Domino train block position k on the token at anchor + k + 1, and DFlash, DFlash2 and LiLiCorr train it on anchor + k. #2757 made the class constant _label_shift drive the training targets. The same fact was still written in four more places:

  • DSparkModule.shift_label = True
  • DominoModule.shift_label, read from the config
  • a NotImplementedError in HFDominoModel.modify
  • the DSpark and Domino exporters' getattr(draft_config, "shift_label", True), which re-read the user's raw architecture config

Those copies, and the AR-validation decode, caused four bugs:

  1. Domino's AR validation drafted one position off, without its head. HFDominoModel inherited DFlash's pseudo_speculative_generate, which decodes block positions 1..bs-1 with the backbone alone.
    • For Domino, those positions predict the token one step further on. Every draft was therefore checked against the wrong target, so drafts were almost never accepted.
    • The GRU correction head, which training optimizes and the exporter ships, was never applied.
    • domino.yaml keeps AR validation off, so shipped training was not affected.
  2. DSpark's AR validation crashed on a sharded base with tied embeddings. scripts/ar_validate.py loads with device_map="auto". accelerate keeps a tied lm_head with embed_tokens on the first GPU, while the draft can land on a later one. DSpark added its Markov bias, computed on the draft's GPU, to backbone logits left on the first GPU, so every sample raised a device error. This dates from DSpark's addition, and Domino's new decode needs the same handling.
  3. DSpark ignored shift_label: false. It trained with the shift anyway and exported shift_label: false, so a loader that honors the field would read every drafted position one token off. Domino refused the same config, but only after building the draft.
  4. The exporters re-derived head fields from the raw config. DSparkModule lower-cases markov_head_type, so Gated trained the gated head but exported "Gated". markov_rank, use_confidence_head and Domino's pure_draft_prefix_len were also re-read, each with its own default.

The fixes, one commit each:

  • DSpark's decode keeps its logits on the draft's device. The backbone logits move to the draft's device before the Markov loop. On a single device this is a no-op.
  • Domino generation.
    • DFlash's default decode starts at block position 1 - _label_shift and returns at most bs - 1 + _label_shift tokens. For shift 0 this is the same slice as before.
    • Domino overrides it the way DSpark does for its Markov head. It decodes the block left to right and steps the GRU over the anchor and the tokens drafted so far. From pure_draft_prefix_len on, it adds embed_proj([h_k; GRU state]) to the backbone logits, as _apply_domino_head does in training. Like DSpark's decode, it brings the backbone logits and the token embeddings to the draft's device.
    • The misplaced eval warning ("acceptance rates are backbone-only") is removed, along with every other copy of that claim: the comments in domino.yaml, dspark.yaml and the Nemotron-3.5 DSpark warm-start recipe, the Domino launcher example, and the speculative-decoding skill's Domino, DSpark and validation sheets. DSpark's generation has applied its Markov head since DSpark was added.
    • The Domino sheet and example now say how to add the HF AR eval: DFlash's task_2 with --steps equal to the block size, since Domino drafts the whole block. The flag values do not change.
  • One alignment rule.
    • HFDFlashModel.modify refuses shift_label: false for every variant whose _label_shift is 1, before anything is built. Unset or true converts as before.
    • The refusal applies at convert only. restore_dflash_model drops shift_label before it replays convert, as it already drops the warm-start path. DSpark checkpoints saved with shift_label: false therefore still load, and now re-export shift_label: true.
    • Both exporters write shift_label from _label_shift. The modules' shift_label attributes and Domino's own check are removed, since nothing reads them any more.
  • Exporters read the built head. DSparkExporter and DominoExporter read markov_rank, markov_head_type, use_confidence_head, pure_draft_prefix_len, gru_hidden_dim and emb_dim off the module, as LiLiCorrExporter already does for its reranker.
  • Changelog. One 0.48.0 Bug Fixes entry.

Usage

N/A. Bug fix with no API change.

Testing

  • New and changed tests:
    • TestDominoGenerate::test_drafts_are_the_training_head_argmax[0|1] checks that Domino's greedy drafts equal the argmax of _apply_domino_head teacher-forced on those drafts, for pure_draft_prefix_len 0 and 1.
    • test_next_token_variants_refuse_shift_label_false_at_convert[dspark|domino] converts both variants with shift_label unset, as the recipes leave it. It then restores a modelopt_state that carries shift_label: false, and expects false to be refused at convert. On refactor(speculative): one home each for the DFlash family's shared rules #2757 DSpark does not raise and Domino raises NotImplementedError; without the restore change, the restore raises.
    • test_export_config_has_dspark_fields now converts with markov_head_type: Gated and expects gated in config.json. It fails on refactor(speculative): one home each for the DFlash family's shared rules #2757.
    • GPU TestLmHeadOffTheDraftDevice::test_generate[dflash|dspark|domino] uses accelerate's dispatch_model to put embed_tokens and lm_head on the CPU, standing in for the first GPU, and the layers and draft on cuda:0. Generation must return on the caller's device. DSpark fails on refactor(speculative): one home each for the DFlash family's shared rules #2757, and Domino fails without its device move.
    • Each new unit test takes at most 0.12 s on 4 CPUs (taskset -c 0-3).
  • tests/unit/torch/speculative and tests/unit/torch/export/test_hf_spec_rope_export.py on CPU: 415 passed on each of transformers 5.5.4, 5.12.1 and 5.18.0 (411 on refactor(speculative): one home each for the DFlash family's shared rules #2757). 5.5.4 and 5.18.0 fall within CI's tf_min and tf_latest pins.
  • tests/gpu/torch/speculative/plugins on 1 GPU: 17 passed, 1 skipped. The skipped test needs 2 GPUs to shard the base.
  • Scratch checks on 1 GPU, with the CPU standing in for the first GPU:
    • A tiny tied-embedding Qwen3 + Domino checkpoint, loaded through ar_validate.py's load_vlm_or_llm with real from_pretrained and accelerate dispatch, in both layouts transformers picks for 2 GPUs. The drafts equal the single-device drafts, and validate_online completes with the inputs on the first device, where ar_validate.py puts them. Without the device move in Domino's decode, the same load crashes.
    • The base's early hidden states moved to the CPU while the model runs on the GPU: DFlash, DSpark and Domino generation all gather them and return on the caller's device.
    • A tiny DSpark checkpoint saved by 0.47.0 with shift_label: false loads through from_pretrained and exports shift_label: true.
  • Bitwise A/B against refactor(speculative): one home each for the DFlash family's shared rules #2757: one training step for each of 23 cases, plus generation at steps 0, 2 and 4 in the online ones, in fp32 and bf16.
    • 44 of 46 runs are identical: loss, every gradient, the variants' metrics and every generated token, including every DFlash, DFlash2, LiLiCorr and DSpark generation.
    • The other 2 are Domino's online case, in fp32 and bf16. Only its drafted tokens differ, as intended; its loss, gradients, metrics and base tokens are identical.
    • In fp32 at steps=4, the drafts go from [[22, 8, 8], [17, 17, 17]] (3 tokens, positions 1 to 3, backbone only) to [[8, 22, 8, 8], [21, 17, 17, 17]] (4 tokens, positions 0 to 3, head applied). The old tokens reappear one position later: this is the off-by-one.
  • FSDP2 probe (2 gloo ranks, the draft as its own fully_shard unit, one rank on the zero-loss branch): DFlash finishes on both ranks, with the same losses as on refactor(speculative): one home each for the DFlash family's shared rules #2757. This PR does not change training code.
  • pre-commit run --files <changed files>: clean.

Before your PR is "Ready for review"

Make sure you read and follow Contributor guidelines and your commits are signed (git commit -s -S).

Make sure you read and follow the Security Best Practices (e.g. avoiding hardcoded trust_remote_code=True, torch.load(..., weights_only=False), pickle, etc.).

  • Is this change backward compatible?: ✅ Shipped recipes and existing checkpoints are unaffected. The recipes leave shift_label unset (or true, in domino.yaml), and checkpoints saved with shift_label: false still restore. Only a new conversion behaves differently: DSpark with shift_label: false now raises instead of training next-token alignment anyway and exporting false, and Domino raises ValueError for that config instead of NotImplementedError.
  • 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?: ✅ One Bug Fixes entry, since DSpark and Domino have shipped since 0.46.0.
  • Did you get Claude approval on this PR?: ❌ Not run yet.

Additional Information

Follow-up to #2757; review after it.

Found while testing, not fixed here. Both fail the same way on #2757:

  • With DFlashModule as its own FSDP2 unit, DSpark and Domino training fails with got mixed torch.Tensor and DTensor.
    • The heads' layers (DSpark's Markov and confidence layers, Domino's prefix_gru and embed_proj) run outside DFlashModule.forward, so FSDP2 never unshards their parameters.
    • With the draft in the root unit, both train on both ranks.
    • The only launcher that makes DFlashModule its own unit is the MiniMax-M2.7 DFlash one, so no shipped DSpark or Domino setup hits this.
  • AcceptanceRateValidation.validate_online compares the base lm_head's predictions with drafts returned on the caller's device. When device_map="auto" puts an untied lm_head on another GPU than the inputs, it raises a device error for every DFlash-family variant. This was reproduced on 1 GPU with the CPU as the first device, not on a 2-GPU host.

🤖 Generated with Claude Code

h-guo18 and others added 5 commits October 12, 2026 14:06
…evice

DSpark's pseudo_speculative_generate adds its Markov bias, computed on
the draft's device, to the backbone logits that the base model's lm_head
returns. scripts/ar_validate.py loads with device_map="auto", and
accelerate keeps a tied lm_head with embed_tokens on the first GPU while
the draft can land on a later one. The logits then stay on the first
GPU, so the sum raises "Expected all tensors to be on the same device"
for every sample and AR validation reports nothing. This has been the
case since DSpark was added.

The backbone logits now move to the draft's device before the loop. On
a single device the move is a no-op.

Verified: a new GPU test dispatches the model with accelerate's
dispatch_model, embed_tokens and lm_head on the CPU and everything else
on cuda:0, and generates with DFlash and DSpark. DSpark fails without
the fix and passes with it. The bitwise A/B against #2757 shows DSpark's
drafted tokens unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
…nt and head

HFDominoModel did not override pseudo_speculative_generate, so AR
validation ran DFlash's, which decodes block positions 1..bs-1 with the
backbone alone. DFlash trains position k on the token at anchor + k;
Domino trains it on anchor + k + 1 (_label_shift = 1). Every drafted
token was therefore compared with the target one position too early,
so drafts were almost never accepted, and the correction head that
training optimizes and the exporter ships was never applied. domino.yaml
keeps AR validation off, so shipped training did not hit this.

DFlash's default decode now starts at block position 1 - _label_shift
and returns at most bs - 1 + _label_shift tokens. For DFlash, DFlash2
and LiLiCorr (shift 0) that is the same slice as before. Domino
overrides it the way DSpark does for its Markov head: it decodes the
block left to right, stepping the GRU over the anchor and the tokens
drafted so far, and from pure_draft_prefix_len on adds
embed_proj([hidden_k; GRU state]) to the backbone logits, as
_apply_domino_head does in training. As in DSpark's decode, the
backbone logits and the drafted tokens' embeddings are moved to the
draft's device: under device_map="auto" the embedding table, and a tied
lm_head with it, can sit on another GPU.

The eval forward warned that Domino's acceptance rates were
backbone-only, but that method computes no acceptance rate, and AR
validation now runs the head. The warning goes, and so do the other
copies of the claim: the comments in domino.yaml, dspark.yaml and the
Nemotron-3.5 DSpark warm-start recipe, the note in the Domino launcher
example, and the speculative-decoding skill's Domino, DSpark and
validation sheets. DSpark's generation has applied its Markov head
since DSpark was added. The Domino sheet and example now say how to add
the HF AR eval: DFlash's task_2 with --steps equal to the block size,
since Domino drafts the whole block. The flag values do not change.

Verified: a new test checks that Domino's greedy drafts equal the
argmax of _apply_domino_head teacher-forced on them, with head weights
large enough to overturn the backbone. It fails on the parent commit,
and also when the head is skipped, its GRU state lags a step, or the
prefix boundary is off by one. The GPU test that puts lm_head on
another device now covers Domino as well, and fails without the move.
Bitwise A/B against #2757: DFlash and DSpark loss, gradients and drafted
tokens are unchanged; only Domino's drafted tokens change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
DSpark and Domino train block position k on the token at anchor + k + 1
(HFDFlashModel._label_shift = 1). The same fact was also written as
DSparkModule.shift_label = True, as DominoModule.shift_label read from
the config, as a NotImplementedError in HFDominoModel.modify, and as the
exporters' getattr(draft_config, "shift_label", True), which read the
user's raw architecture config.

A DSpark config with shift_label: false therefore trained with the shift
but exported shift_label: false, so a loader that honors the field would
read every drafted position one token off. Domino refused the same
config, but only after building the draft.

HFDFlashModel.modify now refuses shift_label: false for every variant
whose _label_shift is 1, before anything is built; unset or true
converts as before. Both exporters write shift_label from _label_shift.
The modules' shift_label attributes and Domino's own check go, since
nothing reads them any more. Domino's error becomes a ValueError, like
the other convert-time config checks, and the skill's Domino and DSpark
sheets list it.

The refusal applies at convert only. Restore replays convert, so
restore_dflash_model drops shift_label from the saved architecture
config, as it already drops the warm-start path. A DSpark checkpoint
saved with shift_label: false trained next-token alignment all the same:
it still loads, and now re-exports shift_label: true.

Verified: a new test converts DSpark and Domino with shift_label unset,
as the recipes leave it, restores a modelopt_state that carries
shift_label: false, and expects shift_label: false to be refused at
convert. On the parent commit DSpark does not raise and Domino raises
NotImplementedError; without the restore change the restore raises.
A tiny DSpark checkpoint saved by 0.47.0 with shift_label: false loads
through from_pretrained and exports shift_label: true. The existing
export tests still read shift_label: true.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
… was built with

DSparkModule resolves its head fields from the architecture config as
it builds them: it lower-cases markov_head_type, casts markov_rank to
int and use_confidence_head to bool, and fills in defaults for the ones
left out. DSparkExporter read the raw config dict again with its own
defaults. markov_head_type: Gated therefore trained the gated head but
exported "Gated", which a serving loader that compares the name exactly
does not recognize. DominoExporter likewise re-derived
pure_draft_prefix_len with a second default.

Both exporters now read the head fields off the built module, as
LiLiCorrExporter already does for its reranker, so the exported config
describes the head that the saved weights belong to.

Verified: the DSpark export test now converts with markov_head_type
"Gated" and expects "gated" in config.json. It fails on the parent
commit. Domino's exported values are unchanged for every config, since
DominoModule stores them as given.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
DSpark and Domino have shipped since 0.46.0, so these are fixes to
released features, and two of them ask something of users: drop
shift_label: false from DSpark configs, which now fail to convert, and
re-export DSpark drafters whose export carried shift_label: false or a
markov_head_type spelled in another case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@copy-pr-bot

copy-pr-bot Bot commented Oct 12, 2026

Copy link
Copy Markdown

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.

@coderabbitai

coderabbitai Bot commented Oct 12, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant