Skip to content

refactor(speculative): draft DSpark and Domino through one AR-validation loop - #2994

Draft
h-guo18 wants to merge 2 commits into
haoguo/dflash-shared-forwardfrom
haoguo/dflash-shared-ar-decode
Draft

h-guo18 wants to merge 2 commits into
haoguo/dflash-shared-forwardfrom
haoguo/dflash-shared-ar-decode

Conversation

@h-guo18

@h-guo18 h-guo18 commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: Refactor. No behavior change: the drafted tokens are bitwise identical.

This PR is stacked on #2990 (branch haoguo/dflash-shared-forward, head 28f4f45), whose branch is its base; merge it after that one. The stack below it, on main 0638a27, is #2757 ← #2762 ← #2989 [1/2] (branch haoguo/dflash-loss-context) ← #2990 [2/2]. Three other PRs also sit on #2990 and are independent of this one: #2991 (DSpark and Domino follow dflash_loss_objective), #2992 (logging draft_metrics) and #2993 (loss terms in config). This branch merges with each of them without conflicts, and none of them touches the generation path.

#2762 gave Domino an AR-validation decode modeled on DSpark's, so pseudo_speculative_generate in hf_dspark.py and hf_domino.py carried the same loop over the block:

Only the step at each block position differed. A fix to the loop had to be made twice, and a third variant with a sequential head would have copied it again.

  • One loop. HFDFlashModel._decode_left_to_right(input_ids, steps) holds the loop and the device handling. Block position k drafts the token after the anchor and the k tokens drafted before it (_label_shift = 1).
  • One step per variant. A variant supplies _correction_step(k, prev_token, hidden, state). It returns the correction to position k's backbone logits, or None to leave them as they are, together with the head's new state.
    • DSpark: markov_step. Only the rnn head carries state.
    • Domino: steps the GRU over the previous token's embedding, which it still moves onto the draft's device. From pure_draft_prefix_len on, it returns embed_proj's correction.
  • Unchanged. Both pseudo_speculative_generate overrides keep their docstrings and call the shared loop. DFlash's parallel decode, which DFlash2 and LiLiCorr also use, is unchanged.
  • New test. DSpark's decode had only shape tests. TestDSparkGenerate mirrors fix(speculative): DSpark and Domino AR validation and label alignment #2762's TestDominoGenerate for the vanilla, gated and rnn heads: with head weights large enough to overturn the backbone, the greedy drafts must equal the argmax of _draft_logits teacher-forced on them. It lands first and passes on [2/2] refactor(speculative): train the DFlash family through one forward with shared loss terms #2990's head.

Usage

No API or config change. A variant with a sequential head now plugs in like this:

class HFMyDraftModel(HFDFlashModel):
    _label_shift = 1

    @torch.no_grad()
    def pseudo_speculative_generate(self, input_ids, steps=1):
        return self._decode_left_to_right(input_ids, steps)

    def _correction_step(self, k, prev_token, hidden, state):
        # The correction to block position k's backbone logits (or None), and the new state.
        return self.dflash_module.my_head_step(prev_token, hidden, state)

Testing

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?: ✅ Only private methods change; configs, checkpoints and pseudo_speculative_generate's outputs are unchanged.
  • 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?: ✅ TestDSparkGenerate.
  • Did you update Changelog?: N/A Refactor with no user-visible change.
  • Did you get Claude approval on this PR?: ❌ Not run yet.

Additional Information

Source changes: +50 / −41 lines in hf_dflash.py, hf_dspark.py and hf_domino.py. Two commits: the DSpark test, which passes on the parent, then the refactor.

Each variant opts in with a one-line pseudo_speculative_generate override rather than by defining _correction_step alone. Switching on whether a variant defines the step would need a None sentinel on HFDFlashModel and a branch in DFlash's decode. Making the loop the family's only decode, with no correction for DFlash, would turn DFlash's parallel decode into a loop and move DFlash2 and LiLiCorr onto it. Both are larger than this refactor. The base _correction_step raises NotImplementedError, so a variant that opts in without a step fails on its first draft.

Merge order: #2757, #2762, #2989, #2990, then this PR.

🤖 Generated with Claude Code

h-guo18 and others added 2 commits October 24, 2026 12:50
…head

DSpark's pseudo_speculative_generate drafts the block left to right, one
markov_step per position, while training applies the Markov head to the
whole block at once through _draft_logits. Only shape tests covered the
decode, so nothing checked that the two agree, and the next commit moves
the decode loop into HFDFlashModel.

The test mirrors Domino's TestDominoGenerate: with head weights large
enough to overturn the backbone, the greedy drafts must equal the argmax
of _draft_logits teacher-forced on the anchor and those drafts. It runs
for the vanilla, gated and rnn heads.

Verified: passes on the parent commit for all three heads, in under
0.2 s per case on 4 CPUs. On the next commit it is the only unit test
that fails when DSpark's step drops the rnn head's state.

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

DSpark's and Domino's pseudo_speculative_generate each carried the same
left-to-right loop over the block. Each copy ran the base forward and the
draft block, and moved the lm_head's logits onto the draft's device (the
#2762 fix, present in both copies). Each also kept the previous token and
the head's state, capped the drafts at the block size, stacked them and
moved them back to the caller's device. Only the step at each position
differed.

The loop is now HFDFlashModel._decode_left_to_right. A variant supplies
_correction_step(k, prev_token, hidden, state), which returns the
correction to block position k's backbone logits (None for none) and the
head's new state:
- DSpark's step is markov_step.
- Domino's step runs the GRU over the previous token's embedding, which
  it still moves onto the draft's device, and adds embed_proj's
  correction from pure_draft_prefix_len on.
Both pseudo_speculative_generate overrides keep their docstrings and call
the shared loop. DFlash's parallel decode, which DFlash2 and LiLiCorr
also use, is unchanged.

Verified against the parent commit:
- Bitwise A/B: the drafted tokens of the online DSpark (vanilla, gated
  and rnn heads) and Domino (pure_draft_prefix_len 1 and 0) cases are
  identical in fp32 and bf16, at init scale and with head weights large
  enough to decide the drafts. Loss, gradients and metrics of all 30
  cases, offline and online, are identical too.
- Sharded on one GPU with the CPU as the first device: accelerate
  dispatch with embed_tokens and lm_head on the CPU, and a split that
  also puts base layers 0-1 there. The drafts are identical for DFlash,
  DSpark and Domino in fp32 and bf16.
- Unit tests in test_hf_{dflash,dflash2,dspark,domino,lilicorr}.py and
  test_hf_spec_rope_export.py: 348 passed on transformers 5.5.4, 5.12.1
  and 5.18.0. GPU plugin tests: 20 passed, 1 skipped (needs 2 GPUs).
- Mutation check: 11 of 12 edits to the loop or the steps fail a unit
  test, the lm_head GPU test or the A/B. Dropping Domino's embedding move
  passes everything, because accelerate's hook on the GRU moves its input.

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 24, 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 24, 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