Skip to content

feat(speculative): log the DFlash family's draft_metrics from the example trainer - #2992

Draft
h-guo18 wants to merge 3 commits into
haoguo/dflash-shared-forwardfrom
haoguo/dflash-log-draft-metrics
Draft

h-guo18 wants to merge 3 commits into
haoguo/dflash-shared-forwardfrom
haoguo/dflash-log-draft-metrics

Conversation

@h-guo18

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

Copy link
Copy Markdown
Contributor

What does this PR do?

Type of change: New feature

Stacked on #2990 [2/2] (branch haoguo/dflash-shared-forward, head 28f4f45), whose branch is the base; merge it after that one. There, every DFlash-family training forward returns draft_metrics, a dict of detached 0-dim tensors. Nothing logs them: EagleTrainerWithAccLog records only train_acc and EAGLE's component losses. So DFlash2's selector_coverage, LiLiCorr's lattice diagnostics, DSpark's confidence_loss, Domino's lambda_base and the objective terms never reach TensorBoard, wandb or trainer_state.json.

  • Trainer.
    • compute_loss adds each metric to a running sum per name. The sum stays a tensor, so there is no host sync per step.
    • Trainer.log adds each name's mean over the steps that reported it to logs, as draft/<name>. That costs one .tolist() sync per log step.
    • A step with no supervised token takes the forward's early return, which reports no metrics. That step is left out of the mean rather than counted as zero.
    • The trainer names no variant and no metric.
  • Why Trainer.log and not EagleTrainingPlot.on_log. HF registers its reporters (TensorBoard, wandb, ...) as callbacks ahead of user callbacks, and Trainer.log copies logs into log_history before any callback runs. A key added in on_log therefore never reaches TensorBoard, and reaches trainer_state.json only by patching the recorded entry.
    • That is how train_acc/parallel_<i>_step_<j> travels on main: since specdec: stop the training regression gate passing unevaluated #2776, EagleTrainingPlot.on_log copies it into log_history[-1] for the regression gate. It reaches the console, trainer_state.json and EagleTrainingPlot's own wandb.log, but not TensorBoard.
    • Trainer.log is the hook HF documents for this ("Subclass and override this method to inject custom behavior").
    • The on_log comment claimed train_acc is "picked up by TensorBoard". It now says where train_acc actually goes. Its handling is unchanged.
  • Ranks. Each rank averages its own steps, as for train_acc, and nothing is reduced across ranks. So there is no collective for a rank without metrics to hang. The reporters write rank 0's values.
  • Regression gate. tools/launcher/common/check_regression.py reads the last logged entry of the latest checkpoint's trainer_state.json and takes its first key containing acc as the run's accuracy; on main that key is train_acc/parallel_0_step_0. TrainerState.save_to_json writes the entry with sorted keys, so this PR's draft/* keys come first.
    • Unguarded, MIN_FINAL_ACC would start gating DFlash2, DSpark, Domino and LiLiCorr runs on selector_accuracy, backbone_accuracy or lilicorr_slot_acc, which its thresholds were not chosen for.
    • Since [1/8] launcher: make the regression gate machine-readable and opt-in strict #2839 that verdict is machine-readable and can fail the training task. regression_check.json would report the diagnostic as the run's accuracy. The Qwen3-8B DFlash2 online example runs the gate with REGRESSION_STRICT=1, so its training task would pass or fail on the selector's accuracy.
    • The gate now skips draft/* and keeps reading train_acc. The MIN_FINAL_ACC row in the speculative-decoding skill's references/algorithms/dflash.md, which the other DFlash-family references point to, notes the exception.
  • Docs. The dflash_lilicorr_w_ce description now says where its metrics are logged, and there is a CHANGELOG entry under 0.49.0. DFlash2's comment that "the trainer converts them at the logging boundary" is now true as written.

What each variant logs:

projector_type draft/<name>
DFlash loss_ce or loss_kd
DFlash2 its objective term, selector_accuracy, selector_coverage
DSpark loss_ce, loss_tvd, backbone_accuracy, confidence_loss
Domino loss_final, loss_backbone, backbone_accuracy, lambda_base
LiLiCorr 26 names: the lattice diagnostics, each term and its weight, loss_ce or loss_kd, origin_loss, origin_accuracy

Usage

Nothing to configure. TensorBoard shows train/draft/<name>, since HF prefixes training keys with train/. trainer_state.json keeps the metrics too:

import json

history = json.load(open(f"{output_dir}/trainer_state.json"))["log_history"]
coverage = [(h["step"], h["draft/selector_coverage"]) for h in history if "draft/selector_coverage" in h]

Testing

  • New test_trainer_logs_each_draft_metric_mean_per_window in test_hf_speculative_offline.py, the unit module that already loads eagle_utils.
    • It runs the real HF Trainer loop through EagleTrainerWithAccLog: 4 steps, 2 per log window. The model's second step reports no metrics.
    • It checks log_history, which records the dict the reporters receive. Each window must hold each name's mean, and a float64 metric must read back exactly.
    • It fails on the base with KeyError: 'draft/step', and takes 0.3 s on 4 CPUs (taskset -c 0-3).
  • New test_draft_metrics_are_not_the_accuracy in tools/launcher/tests/test_check_regression.py. Its last log entry holds draft/selector_accuracy 0.09 and train_acc/parallel_0_step_0 0.17, written with sorted keys as HF writes them. Against main's check_regression.py it fails, reading 0.09.
  • End to end on 1 GPU (scratch script), on transformers 5.5.4, 5.12.1 and 5.18.0: DFlash, DFlash2, LiLiCorr, DSpark (with its confidence head) and Domino (with DominoLambdaCallback).
    • Each trained 6 steps through EagleTrainerWithAccLog and EagleTrainingPlot with report_to=tensorboard. One batch had no supervised token.
    • Every draft_metrics name appears as train/draft/<name> in the TensorBoard event file and as draft/<name> in log_history. Each value equals the mean of what that window's forwards returned. train_acc is in log_history but not in TensorBoard.
    • Under torch.cuda.set_sync_debug_mode, the per-step accumulation does not sync, and log syncs once when it has metrics.
    • On each run's saved trainer_state.json, main's check_regression.py reads draft/selector_accuracy (DFlash2), draft/lilicorr_slot_acc (LiLiCorr) or draft/backbone_accuracy (DSpark, Domino) as the accuracy. With this PR it reads train_acc/parallel_0_step_0 for all five. DFlash logs no draft/* key containing acc.
  • 2-rank gloo DDP through the HF Trainer, on the same three versions: it finishes on both ranks, each rank logs its own means, and rank 0's TensorBoard holds rank 0's.
  • test_hf_speculative_offline.py, test_hf_{dflash,dflash2,dspark,domino,lilicorr}.py and tools/launcher/tests/test_check_regression.py: 388 passed on each of transformers 5.5.4 and 5.18.0 (CI's tf_min and tf_latest pins), against 386 on the base.
  • tests/gpu/torch/speculative/plugins on 1 GPU: 20 passed, 1 skipped. The skipped test needs 2 GPUs.
  • Bitwise A/B against the base, comparing loss, gradients, metrics and generated tokens: 23 DFlash-family cases plus 15 more, in fp32 and bf16. The extra cases cover DSpark's gated head and its rnn head online, the KD and D-PACE flags on DSpark and Domino, DFlash2 KD online, more zero-loss steps, and labels that are -100 on some tokens. All 76 runs are identical, since no model code changes.
  • FSDP2 probe (2 gloo ranks, the DFlash draft as its own unit, one rank on the zero-loss branch, CE and KD): both ranks OK, with the same losses as on the base.
  • pre-commit run --files on the 7 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?: ✅ It only adds log keys, and the regression gate reads the same accuracy key as before.
  • 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?: ✅ Under 0.49.0, Speculative Decoding.
  • Did you get Claude approval on this PR?: ❌ Not run yet.

Additional Information

Stack: #2757 ← #2762 ← #2989 [1/2] ← #2990 [2/2] ← this PR. Merge them in that order. Three other PRs sit on #2990 and are independent of this one: DSpark and Domino following dflash_loss_objective (#2991), loss terms in config (#2993), and the shared AR-decode helper (#2994). This branch merges with each of them. The only conflict is in CHANGELOG.rst with the loss-terms PR (#2993), where both add an entry to the same 0.49.0 list; keeping both entries resolves it.

Not changed here: train_acc still never reaches TensorBoard, for EAGLE as for the DFlash family, because EagleTrainingPlot.on_log adds it after the reporters have run. #2776 copied it into trainer_state.json for the gate. Adding it in Trainer.log, as this PR does for draft/*, would also chart it in TensorBoard. That changes how train_acc is handled, so it is left for a follow-up. The same holds for estimated_training_ar, which reaches only the console and wandb.

🤖 Generated with Claude Code

h-guo18 and others added 3 commits October 24, 2026 13:05
…rics

The next commit has the speculative-decoding example trainer log each DFlash-family
variant's training metrics as draft/<name>, which also puts them in trainer_state.json.
check_regression.py takes the first key containing "acc" in the last logged entry as the
run's accuracy, and TrainerState.save_to_json writes that entry with sorted keys, so the
draft/ keys come before train_acc/parallel_0_step_0, the key EagleTrainingPlot copies into
the entry for this gate. MIN_FINAL_ACC would then silently start gating DFlash2, DSpark,
Domino and LiLiCorr runs on a head diagnostic (selector_accuracy, backbone_accuracy,
lilicorr_slot_acc) instead of the training accuracy its thresholds were chosen for:
regression_check.json would report that diagnostic as the run's accuracy, and the DFlash2
online example, which runs the gate with REGRESSION_STRICT=1, would pass or fail its
training task on it. Skip draft/* so the gate keeps reading train_acc.

The new test_check_regression.py case writes such an entry, a draft/selector_accuracy of
0.09 beside a train_acc of 0.17, and fails without the skip, which reads 0.09. The file's
19 tests pass.

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

Every DFlash-family training forward returns draft_metrics, but the example trainer never
logged them, so DFlash2's selector_coverage, LiLiCorr's lattice diagnostics, DSpark's
confidence_loss and Domino's lambda_base never reached TensorBoard or wandb.

EagleTrainerWithAccLog now sums each metric as a tensor per micro-batch, so no host sync,
and Trainer.log adds each name's mean over the steps that reported it as draft/<name>,
with one sync per log step. It has to be Trainer.log, not EagleTrainingPlot.on_log: the
HF reporters are callbacks registered ahead of ours, so a key added in on_log never
reaches TensorBoard, and log_history is recorded before any callback runs, so such a key
reaches trainer_state.json only by patching the recorded entry, as EagleTrainingPlot does
for train_acc. train_acc therefore still never reaches TensorBoard; the comment saying it
does is corrected, its handling left as is. The trainer names no variant or metric. Each
rank averages its own steps, as for train_acc. The LiLiCorr weight description now says
where the metrics are logged.

Verified:
- the new unit test, which fails on the parent with KeyError 'draft/step';
- DFlash, DFlash2, LiLiCorr, DSpark and Domino each trained through the real HF Trainer on
  GPU with report_to=tensorboard, on transformers 5.5.4, 5.12.1 and 5.18.0: every
  draft_metrics name lands as train/draft/<name> and in log_history, equal to the window
  mean of what the forwards returned, including a window with a step that had no
  supervised token; train_acc lands in log_history only;
- on the trainer_state.json each of those runs saves, check_regression.py reads
  train_acc/parallel_0_step_0 as the accuracy; without the previous commit it reads
  draft/selector_accuracy (DFlash2), draft/lilicorr_slot_acc (LiLiCorr) or
  draft/backbone_accuracy (DSpark, Domino);
- torch.cuda.set_sync_debug_mode: no sync in the per-step accumulation, one per log call
  that has metrics;
- 2-rank gloo DDP through the HF Trainer: no hang, each rank logs its own means and rank 0
  writes them to TensorBoard;
- test_hf_speculative_offline.py, the DFlash-family plugin tests and the launcher's
  test_check_regression.py on transformers 5.5.4 and 5.18.0 (388 passed on each);
- tests/gpu/torch/speculative/plugins (20 passed, 1 skipped);
- bitwise A/B of all 23 DFlash-family cases and 15 more, fp32 and bf16, against the
  parent: 76 MATCH;
- FSDP2 probe, dflash draft branch ce/kd: both ranks OK, same losses as the parent.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
Users of examples/speculative_decoding get new draft/<name> scalars in TensorBoard, wandb
and trainer_state.json; say what they are and that they are interval means. Checked with
the rst pre-commit hooks.

The entry goes under the 0.49.0 heading: release/0.48.0 is branched, so this change first
reaches users in 0.49.0.

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