Skip to content

fix(speculative): keep padding out of DSpark and Domino loss masks - #2761

Open
h-guo18 wants to merge 1 commit into
mainfrom
haoguo/dflash-variant-loss-mask
Open

h-guo18 wants to merge 1 commit into
mainfrom
haoguo/dflash-variant-loss-mask

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

DSpark and Domino online training could train on padding.

HFDFlashModel.forward builds its loss mask as (labels != -100) * attention_mask * loss_mask. The attention_mask term was added in #1975 because labels do not, in general, mark padding with -100. The online collators (LanguageDataCollator, VisionLanguageDataCollator) copy labels from the padded input_ids and write -100 only where answer_only_loss masks non-assistant tokens.

HFDSparkModel.forward and HFDominoModel.forward kept an older copy of the rule that consults attention_mask only when labels is absent. With labels present they ignored it, so on a padded batch they sampled anchors in the padded tail and put pad tokens into the loss.

The rule lived in three copies and #1975 updated only one. This PR moves DFlash's rule into a single helper, HFDFlashModel._build_loss_mask, and calls it from the DFlash, DSpark and Domino training forwards. The next change to which positions are supervised then reaches every variant. DFlash's mask is bit-identical to before (same ops in the same order). DFlash2 and LiLiCorr inherit the DFlash forward and need no change.

Affected: DSpark and Domino trained with data.mode=online and training.answer_only_loss=false on batches that contain padding, i.e. any sample shorter than training.training_seq_len.

Not affected:

  • The shipped DSpark and Domino recipes and launcher examples. They all set answer_only_loss: true, which gives padding -100 labels. This is why the bug went unnoticed.
  • Streaming and offline training. Their batches carry a loss_mask that is zero on padding.
  • DFlash, DFlash2 and LiLiCorr.

Usage

N/A. Bug fix with no API change.

Testing

  • New test_hf_dflash.py::test_padding_is_never_supervised[dflash|dspark|domino]. It runs each variant's real training forward on a batch whose only non -100 labels sit on padded positions, and expects zero loss.
    • Without the fix, the dspark and domino cases fail with loss 0.456 and 3.486.
    • With the fix, all three pass in under 0.4 s total on 4 CPUs (taskset -c 0-3).
  • tests/unit/torch/speculative on CPU with torch 2.10: 315 passed on transformers 5.5.4, 5.12.1 and 5.18.0. 5.5.4 and 5.18.0 fall within CI's tf_min (~=5.5.0) and tf_latest (~=5.18.0) pins. Main before this change: 312 passed on each.
  • Scratch checks, not checked in:
    • The helper reproduces DFlash's old inline mask bit for bit over 147 combinations of labels, attention_mask and loss_mask (absent, int, bool, float, bf16, fp64, fractional).
    • End to end in fp32 with a fixed seed, DFlash's loss and draft gradients are bit-identical to main on unpadded, padded and loss_mask batches. DSpark and Domino are bit-identical too, except on a padded batch whose padding keeps its labels, which is the fixed case.
  • 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?: ✅ Configs and checkpoints are unchanged. Training changes only in the affected case above, where it stops supervising padding.
  • 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?: N/A. No shipped recipe or example is affected.
  • Did you get Claude approval on this PR?: ❌ Not run yet.

Additional Information

Follow-up to #1975, which fixed the rule in the DFlash forward only. DSpark and Domino still duplicate the rest of the DFlash training-forward prologue (base forward, anchor sampling, draft inputs). This PR consolidates only the loss-mask rule.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes
    • Training loss now consistently respects valid labels, padding masks, and optional loss masks across DFlash, DSpark, and Domino models.
    • Positions excluded by the attention mask or label mask no longer contribute to the training loss.

@h-guo18
h-guo18 requested review from a team as code owners October 12, 2026 11:47
@h-guo18
h-guo18 requested a review from yeyu-nvidia October 12, 2026 11:47
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

No actionable comments were generated in the recent review.

Recent review info
Run configuration
Commits

Reviewing files that changed from the base of the PR and between 2d38b37 and ef1276c.

Files selected for processing: 3
  • modelopt/torch/speculative/plugins/hf_dflash.py
  • modelopt/torch/speculative/plugins/hf_dspark.py
  • tests/unit/torch/speculative/plugins/test_hf_dflash.py

Included review availability: This review used your included allowance. Your plan provides up to 12 included reviews per hour; 11 remain after this review.

Walkthrough

DFlash adds shared loss-mask construction, and DFlash, Domino, and DSpark use it during training. A parameterized test checks that attention-masked padding positions with labels produce zero loss.

Changes: Speculative loss masking

Layer / File(s) Summary
Build the shared loss mask
modelopt/torch/speculative/plugins/hf_dflash.py
_build_loss_mask combines valid-label positions, attention_mask, and an optional loss_mask.
Use and test shared masking
modelopt/torch/speculative/plugins/hf_dflash.py, modelopt/torch/speculative/plugins/hf_domino.py, modelopt/torch/speculative/plugins/hf_dspark.py, tests/unit/torch/speculative/plugins/test_hf_dflash.py
The three training paths call _build_loss_mask. A parameterized test checks that attention-masked padding positions with labels produce exactly zero loss.

Priority: Normal

Estimated code review effort: 2 (Simple) | ~10 minutes

Change: Bug fix

Merge Risk: Minimal · up to ef127

The intended padding-loss issue is addressed without an established remaining merge-blocking risk.

Pre-merge checks: Passed 6
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the primary fix: excluding padding from DSpark and Domino loss masks during speculative training.
Docstring Coverage Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 8 functions across 4 files.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Security Anti-Patterns Passed PASS. The PR changes only three modelopt Python files and one test. The added code centralizes loss-mask construction and does not add torch.load(..., weights_only=False), numpy.load(..., allow_pickle…
Full details: Security Anti-Patterns

Explanation

PASS. The PR changes only three modelopt Python files and one test. The added code centralizes loss-mask construction and does not add torch.load(..., weights_only=False), numpy.load(..., allow_pickle=True), hardcoded trust_remote_code=True, eval/exec on external input, or # nosec. No pyproject.toml or requirements.txt changes add dependencies.

Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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

@github-actions

github-actions Bot commented Oct 12, 2026 •

Copy link
Copy Markdown
Contributor
PR Preview Action v1.8.1

QR code for preview link

🚀 View preview at
https://NVIDIA.github.io/Model-Optimizer/pr-preview/pr-2761/

Built to branch gh-pages at 2026-10-24 07:03 UTC.
Preview will be ready when the GitHub Pages deployment is complete.

@codecov

codecov Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 92.30769% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 71.25%. Comparing base (0638a27) to head (ef1276c).

Files with missing lines Patch % Lines
modelopt/torch/speculative/plugins/hf_dflash.py 90.90% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #2761      +/-   ##
==========================================
- Coverage   79.82%   71.25%   -8.57%     
==========================================
  Files         661      661              
  Lines       74928    74918      -10     
==========================================
- Hits        59810    53385    -6425     
- Misses      15118    21533    +6415     
Flag Coverage Δ
examples-gpt-oss 13.45% <15.38%> (-0.03%) ⬇️
examples-llm_eval 17.17% <15.38%> (-0.04%) ⬇️
examples-torch_onnx 21.22% <15.38%> (-0.06%) ⬇️
gpu 28.50% <15.38%> (-31.58%) ⬇️
unit 61.13% <92.30%> (+0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

HFDFlashModel.forward builds the training loss mask as
(labels != -100) * attention_mask * loss_mask. The attention_mask term
was added in #1975 because labels do not, in general, mark padding with
-100: the online collators copy labels from the padded input_ids and
write -100 only where answer_only_loss masks non-assistant tokens.

HFDSparkModel.forward and HFDominoModel.forward kept an older copy of
the rule that consults attention_mask only when labels are absent. With
labels present they ignored it, so online training with
answer_only_loss=false on a padded batch sampled anchors in the padded
tail and trained on pad tokens. The shipped recipes set
answer_only_loss=true, which gives padding -100 labels, and streaming
and offline batches carry a loss_mask that is zero on padding, so no
shipped configuration hit this.

The rule lived in three copies and #1975 updated only one. Move it into
one helper, HFDFlashModel._build_loss_mask, and call it from the
DFlash, DSpark and Domino training forwards, so the next change to which
positions are supervised reaches every variant. DFlash's mask is
bit-identical to before (same ops in the same order). DFlash2 and
LiLiCorr inherit the DFlash forward and need no change.

Add a regression test that runs each variant's real training forward on
a batch whose only labels sit on padded positions and expects nothing to
be supervised. Without this fix it fails for DSpark and Domino.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: h-guo18 <67671475+h-guo18@users.noreply.github.com>
@h-guo18
h-guo18 force-pushed the haoguo/dflash-variant-loss-mask branch from 2d38b37 to ef1276c Compare October 24, 2026 06:57

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