Repository navigation
[2/2] refactor(speculative): train the DFlash family through one forward with shared loss terms - #2990
Draft
h-guo18 wants to merge 6 commits into
Draft
[2/2] refactor(speculative): train the DFlash family through one forward with shared loss terms#2990h-guo18 wants to merge 6 commits into
h-guo18 wants to merge 6 commits into
Conversation
…ivergence terms The block objective was a branch in HFDFlashModel._compute_loss (KD or CE), and DFlash2's dflash_lk_loss_type replaced it after the fact: _lk_loss recomputed the loss from the context's per-token cross-entropy and weights, outside the gate that turns a step with nothing to train into a zero loss. The divergences are now components in dflash_losses.py that name no variant, a closed registry of callables (ctx, source) -> 0-dim loss, each reducing under the step's position weights the way the old code did: ce, kd (soft cross-entropy against the base model), tvd (L1 to the base model's distribution, DSpark's term), tv (1 - q(gold)) and lambda (the acceptance-weighted CE/TV blend, still reduced term by term and blended with a detached scalar). Each term declares whether it reads the teacher and whether it backpropagates through the per-token cross-entropy. An objective is an ordered [(name, weight, term)] list that modify() builds through the _build_objective hook: DFlash and LiLiCorr keep kd or ce, DFlash2 picks tv or lambda. _compute_loss sums it left to right and reports each term as loss_<name>, plus the backbone's accuracy when a head sits on it. The declarations replace the per-variant flags: offline training reconstructs the teacher's logits when a term needs them, and the context computes the cross-entropy with gradient only when a term backpropagates through it. LossContext gains the per-token TVD, and the TVD chunk compiles under dflash_use_torch_compile for every variant, no longer only DSpark's modify(). One disclosed behavior change: a DFlash2 step under tv or lambda whose total position weight is <= 1 now takes the zero-loss gate like every other objective; before, the blend was computed over that tiny weight sum. The selector term is unchanged, still computed on every step. Verified against the previous commit with one train step and one generation step per case, in fp32 and bf16: DFlash (KD and CE), DFlash2 (ce/kd/tv/lambda with the selector) and LiLiCorr, offline and online, D-PACE and decay, including zero-loss steps, give bitwise-equal loss, gradients, train_acc, metrics and generated tokens (30/32). The other two runs are the change above on a step that keeps one label of weight < 1: the fp32 loss goes from 0.96958977 to 0 (bf16: 0.968748331 to 0), the 33 backbone gradients lose the TV term, and the 3 selector gradients and all metrics are unchanged. The new loss_<name> metrics equal the loss for DFlash and origin_loss for LiLiCorr, bitwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
HFDSparkModel.forward was a copy of the DFlash training forward (base model, loss mask,
anchors, draft inputs, draft) followed by _apply_markov_head and _compute_dspark_loss,
which rebuilt the labels, weights, gate and accuracy that the shared loss now provides.
Copies of the forward are where the family's fixes went missing: the loss-mask rule DFlash
gained for padding never reached it.
DSpark now changes only its hooks:
- _build_objective: [("ce", dflash_ce_loss_alpha), ("tvd", dflash_l1_loss_alpha)].
- _draft_logits: the Markov transition bias on the backbone's logits (the old
_apply_markov_head without the confidence head).
- _online_base_outputs: the inner model and its final hidden, which the loss projects
only at the rows it reads.
- _position_weighting: "decay", as before (dflash_loss_objective is not wired for DSpark).
- _compute_loss: the objective, plus the confidence head's BCE against 1 - 0.5 * TVD,
sharing the TVD term's per-token values. A step with nothing to train keeps returning
_draft_zero_loss, which also feeds the confidence head for DDP.
With the shared forward DSpark adopts DFlash's loss mask (labels, attention_mask and
loss_mask intersected), the fix the padding PR makes on main; it is identical whenever
attention_mask is all ones where labels are valid, as in every shipped recipe and in
streaming and offline batches. Offline batches now fail at the entry when the TVD term has
no teacher to read, rather than after the draft has run.
Metrics move to draft_metrics: ce_loss, l1_loss and base_accuracy become loss_ce, loss_tvd
and backbone_accuracy; confidence_loss keeps its name.
Verified against the previous commit with one train step and one generation step per case,
in fp32 and bf16: the vanilla and rnn heads with the confidence head on, offline and
online, and a step that takes the zero-loss branch give bitwise-equal loss, gradients,
train_acc, metrics (under the names above) and generated tokens (8/8).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
HFDominoModel.forward was the second copy of the DFlash training forward, followed by
_apply_domino_head and _compute_domino_loss, which again rebuilt the labels, weights, gate
and accuracies. Domino now changes only its hooks:
- _build_objective: [("ce", 1.0)].
- _draft_logits: the GRU correction on the backbone's logits (the old _apply_domino_head).
- _online_base_outputs: the inner model; Domino reads no teacher.
- _position_weighting: "decay", as before.
- _compute_loss: (1 - lambda_base) * objective("final") + lambda_base *
objective("backbone"), behind the shared zero-loss gate.
Domino no longer overrides forward(): its eval branch already delegated to the shared
forward, so with training on the hooks above nothing is left to override.
Like DSpark, Domino adopts DFlash's loss mask (labels, attention_mask and loss_mask
intersected): identical whenever attention_mask is all ones where labels are valid.
Metrics move to draft_metrics: final_loss, base_loss and base_accuracy become loss_final,
loss_backbone and backbone_accuracy; lambda_base keeps its name.
Verified against the previous commit with one train step and one generation step per case,
in fp32 and bf16: offline and online steps and a step that takes the zero-loss branch give
bitwise-equal loss, gradients, train_acc, metrics (under the names above) and generated
tokens (6/6).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
…nd loss Update the call sites of the internals the previous commits removed or renamed, keeping each test's intent: - DSpark's zero-weight DDP test runs the real _loss_context/_compute_loss instead of the removed _apply_markov_head/_compute_dspark_loss. - Domino's AR-validation test teacher-forces the drafts through _draft_logits instead of the removed _apply_domino_head. It still fails when either the drafting or the training side skips the correction head or feeds it the wrong prefix. - The DSpark and Domino metrics are read from draft_metrics, under the new names where they changed, and as tensors. One new test pins the disclosed behavior change: a DFlash2 step whose position weights sum to <= 1 takes the zero loss under ce, tv and lambda alike. Before, tv and lambda gave 0.968 and 3.352 on it. tests/unit/torch/speculative and test_hf_spec_rope_export.py: 516 passed on transformers 5.5.4, 5.12.1 and 5.18.0. The changed tests take at most 0.7 s each on 4 CPUs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
Before the terms were shared, DFlash2's _lk_loss carried its own copy of the cross-entropy reduction, so test_zero_decay_and_unit_scale_is_exactly_cross_entropy compared two copies. Now the lambda blend calls the ce term, and that test compares the term with itself. The kd, tvd and tv reductions were never compared with anything. A parametrized test now runs ce, kd, tvd and tv through the real _loss_context and _compute_loss under decay weights, and compares each loss bitwise with a verbatim copy of the old reduction: HFDFlashModel._compute_loss for ce and kd, DSpark's _compute_dspark_loss for tvd, and DFlash2's _lk_loss for tv. The terms' needs_ce_grad declarations need no new test. They now set the per-token cross-entropy's grad mode, which test_cross_entropy_gradient_follows_the_objective already pins for ce, tv, lambda and KD. If the tv or lambda declaration is flipped, the loss value does not change, but the backbone trains through the selector alone; the shipped dflash2.yaml uses lambda with the selector. Mutation check. Each source change below passes the rest of tests/unit/torch/speculative and test_hf_spec_rope_export.py (519 passed), and fails exactly the case named: - ce, kd, tvd or tv divides by the supervised count instead of the weight sum: the new test's [ce], [kd], [tvd] or [tv]. Reducing ce under the mask instead of the weights also fails [ce]. - tv or lambda declares needs_ce_grad = False, or kd declares it True: test_cross_entropy_gradient_follows_the_objective's [tv], [lambda] or [kd]. Flipping ce's own declaration fails four existing tests and that test's [ce]. Each new case takes under 0.1 s on 4 CPUs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
DSpark's dspark_metrics and Domino's domino_metrics have been in every release since 0.46.0. The DSpark and Domino commits remove both keys, rename three inner keys of each, and return tensors where both returned Python floats. Nothing in the repo reads these keys, but user code that reads them breaks. The backward-breaking entry for DFlash2's and LiLiCorr's keys now also names these two keys and the renamed inner keys. 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 24, 2026
Draft
This branch has not been deployed
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.
What does this PR do?
Type of change: Refactor. Training behavior is unchanged except for the two disclosed cases below. One backward-breaking API change: DSpark's and Domino's training metrics move under
draft_metrics(see Metrics).This PR is stacked on #2989, whose branch is its base; that PR is stacked on #2762. It is the second of two PRs: merge #2989 first, then this one. A new DFlash-family variant is now added one way: it reuses DFlash's training forward and changes only its loss, plus its head if it has one.
HFDFlashModel.forwardis now the only training forward, and no variant overridesforward(). DSpark and Domino change only the hooks [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989 introduced, plus_build_objective:_online_base_outputs: DSpark uses the inner model's final hidden state, which the loss projects only at the rows it reads. Domino takes only the target hidden states._draft_logits: DSpark's Markov bias or Domino's GRU correction on the backbone's logits. The AR-validation decodes from fix(speculative): DSpark and Domino AR validation and label alignment #2762 are unchanged. Domino's generation test now checks its drafts against_draft_logits._build_objective: the block objective,ceandtvdfor DSpark andcefor Domino._position_weighting: DSpark and Domino still always decay._compute_loss: the objective plus the terms the variant owns. DSpark adds its confidence head, whose target reuses the TVD term's per-token values. Domino blends (λ) the objective on the corrected logits with the objective on the backbone logits.dflash_losses.py, which names no variant.ce,kd,tvd,tvandlambda) form a closed registry. Each one reduces under the position weights the way the old code did.[(name, weight, term)]list, summed left to right.modify()builds it through_build_objective: DFlash and LiLiCorr keepkdorce, and DFlash2 pickstvorlambda. DFlash2'stvandlambdaare now objective terms, so they pass through the zero-loss gate instead of replacing the loss after it (change (b) below).LossContextalso computes the per-token TVD lazily, at most once. The TVD chunk compiles underdflash_use_torch_compilefor every variant, no longer only in DSpark'smodify().draft_metrics, as detached 0-dim tensors. Each objective term is reported asloss_<term>, andbackbone_accuracyis reported when a head sits on the backbone.dspark_metricsanddomino_metrics, which have shipped since 0.46.0. [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989 already replacedselector_metricsandlilicorr_metrics.ce_loss/l1_loss/base_accuracybecomeloss_ce/loss_tvd/backbone_accuracy. Domino'sfinal_loss/base_loss/base_accuracybecomeloss_final/loss_backbone/backbone_accuracy. All other names stay as they were..item()where it needs a float.draft_metrics. The CHANGELOG entry from [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989 now covers all four keys and the renamed inner keys._compute_dspark_loss,_compute_domino_loss,_apply_markov_headand_apply_domino_head. Also removed: DSpark's and Domino'sforward()overrides.The three zero losses stay as they were:
final.sum() * 0.0), so an FSDP2 unit around the draft still runs its backward. Domino's λ-blend sits behind the same gate._draft_zero_loss._draft_zero_loss, which also feeds its confidence head for DDP.Disclosed changes:
attention_maskandloss_mask. This is the same fix as the separate padding PR, through the same_build_loss_maskhelper. Merging the two conflicts only where the padding PR edits the DSpark and Domino forwards that this PR removes; keeping this PR's side resolves it (see Testing). The result is identical wheneverattention_maskis all ones where labels are valid. That holds in every shipped recipe and in streaming and offline batches.tvorlambdawhose total position weight is <= 1 now takes the zero-loss gate like every other objective. Before, the blend was computed over that tiny weight sum. The selector term is unchanged.base_model_hidden_statesnow raises at the start of the forward, not after the draft has run.Usage
Configs are unchanged. A variant is now written as hooks:
Testing
tests/unit/torch/speculativeplustests/unit/torch/export/test_hf_spec_rope_export.py: 520 passed on transformers 5.5.4, 5.12.1 and 5.18.0 (513 on [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989; the other 7 are this PR's new cases). 5.5.4 and 5.18.0 fall within CI'stf_minandtf_latestpins. On 4 CPUs, each changed test takes at most 0.7 s and each new case under 0.1 s.tests/gpu/torch/speculative/pluginson one GPU: 20 passed, 1 skipped (it needs 2 GPUs).train_acc, the metrics under the rename above, and the generated draft tokens.lambdagoes from 2.86922026 to 0 in fp32 (2.87524128 to 0 in bf16).loss_<term>metrics equal the loss for DFlash andorigin_lossfor LiLiCorr, bitwise.fully_shardunit, and one rank on the in-loss zero branch. Both ranks finish, under CE and under KD, with the same losses as on [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989 and on fix(speculative): DSpark and Domino AR validation and label alignment #2762's head. Domino under the same wrapping stops with the same mixedTensor/DTensorerror on fix(speculative): DSpark and Domino AR validation and label alignment #2762's head, on [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989 and on this one; this PR does not change that.hf_dspark.pyandhf_domino.py, where the padding PR edits the forwards that this PR removes. Keeping this PR's side of both files gives this PR's tree plus the padding PR's regression test, with one_build_loss_mask. On that tree, the padding PR's regression test and the DSpark and Domino test files pass.ce,tvandlambda. Before this PR,tvandlambdagave 0.968 and 3.352 on it.ce,kd,tvdandtv) runs through the real_loss_contextand_compute_lossunder non-uniform decay weights. Each result equals, bitwise, a verbatim copy of the reduction it replaced. Before, the lambda test compared DFlash2's own copy of the CE reduction with DFlash's. That check went away when the blend started calling the shared term.ce,kd,tvdortvby the supervised count instead of the weight sum, or reducingceunder the mask, passes the rest of the suite (519 tests) and fails exactly one case of this test.needs_ce_graddeclarations now decide whether the cross-entropy keeps its gradient. [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989 added the test for that: under D-PACE, the context that a real training forward passes to the loss keeps the gradient force,tvandlambdabut not for KD. Itstv,lambdaand KD cases now also pin the declarations.needs_ce_gradontv,lambdaorkdpasses the rest of the suite (519 tests) and fails exactly that case.dflash2.yaml, flipping thelambdadeclaration leaves the loss bitwise unchanged, but 33 of 36 draft gradients change, because the backbone then trains only through the selector._loss_context/_compute_loss. Domino's generation test from fix(speculative): DSpark and Domino AR validation and label alignment #2762 teacher-forces the drafts through_draft_logits; it still fails if drafting or training skips the correction head or feeds it the wrong prefix. DSpark's and Domino's metrics are read fromdraft_metrics.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.).draft_metricsreplaces the forward output'sdspark_metricsanddomino_metrics. DSpark's and Domino's inner keys are renamed, and values that were Python floats are now 0-dim tensors (see Metrics and the CHANGELOG entry).CONTRIBUTING.md: ✅ The shared terms live indflash_losses.py, which carries the SpecForge and DeepSpec notices since [1/2] refactor(speculative): hand the DFlash-family loss one context per training step #2989.Additional Information
Against #2989, the source changes are +297 / −507 lines. Follow-ups build on the hooks: DSpark and Domino honoring
dflash_loss_objective, and loggingdraft_metricsin the trainer.Merge order: #2989, then this PR.
🤖 Generated with Claude Code