Skip to content

fix(training): sanitize masked sentinel and non-finite inputs in the DSpark loss - #5244

Open
khazic wants to merge 18 commits into
NVIDIA-NeMo:mainfrom
khazic:khazic/fix/dspark-masked-input-robustness
Open

khazic wants to merge 18 commits into
NVIDIA-NeMo:mainfrom
khazic:khazic/fix/dspark-masked-input-robustness

Conversation

@khazic

@khazic khazic commented Aug 2, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #5233 and #5234: this fixes the DSpark core introduced there, so until those merge this PR also lists their commits. Only the last commit is new here; I will rebase once the base PRs land.

What

dspark_loss computed CE, softmax, and BCE over every position and only zeroed masked positions by multiplying with the zero weight afterwards. Two failure modes survive that scheme once real padded batches arrive:

  1. target_ids padded with anything other than -100 (e.g. -1 or vocab_size) at masked positions crashes F.cross_entropy with a device-side assert.
  2. Non-finite padded logits at masked positions poison the loss through 0 * NaN == NaN, and a non-finite forward row also produces NaN gradients through cross_entropy's backward even when the upstream gradient is zero, so output-side masking alone cannot fix it.

Fix

Sanitize the inputs at masked positions before any op: draft_logits rows are zeroed via torch.where, target_ids sentinels are replaced with 0 via masked_fill, the per-position TV distance is scrubbed on the reduced [B, N, L] tensor (cheaper than copying the full-vocab teacher, which is detached and only consumed through this reduction), and confidence_pred is zeroed at masked positions. The padded-input contract is documented on DSparkForwardOutput.eval_mask.

The draft-side torch.where costs one extra logits-sized allocation per step. This is a deliberate robustness trade: it is the only placement that guarantees finite gradients regardless of how the future producer pads, and the loss already materializes two fp32 softmax copies of the same size. If a future producer is guaranteed to pad the draft with finite values, it can be dropped.

heads.get_prev_embeddings intentionally stays a strict lookup. Clamping sentinels there would silently map genuinely corrupt negative ids at supervised positions to row 0 (and corrupt the RNN head's recurrent state for later positions in the block), so the docstring now states the producer contract instead: replace sentinel pads before calling, exactly as dspark_loss sanitizes target_ids.

Tests

New unit test poisons masked positions with -1 target ids, -inf draft rows, and NaN teacher/confidence values, then asserts the loss, all metrics, and the gradients are finite and exactly equal to the clean-input reference, with zero gradient at masked positions. It fails (crashes) on the parent commit and passes here. All 17 dspark unit tests pass; ruff is clean.

Merge note

The sanitize mask is eval_mask, matching this branch's loss weighting. the block-keep-mask fix PR (same stack) redefines the weight as eval_mask AND block_keep_mask; whichever branch merges into the DSpark core branch (#5234) second should key the sanitization on that combined supervised mask so dropped-anchor padding is scrubbed too. This branch also conflicts textually with the TV-half-factor fix PR (same stack) in the l1_dist block; resolve by scrubbing first, then computing TV via the helper from that branch.

@copy-pr-bot

copy-pr-bot Bot commented Aug 2, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

khazic added 3 commits August 6, 2026 17:16
Design proposal (RFC) for adding DSpark speculative-decoding draft training to
Megatron-Bridge, tracking NVIDIA-NeMo#5230. It describes the DSpark draft (a
semi-autoregressive parallel backbone plus Markov and confidence heads), its
three-term CE / TV / confidence training objective and acceptance metric, and
how it maps onto the existing ModelOpt distillation subsystem pattern (provider
hook, custom forward step, loss, thin entry, HF weight mapping). It includes a
phased implementation plan and references the NeMo AutoModel reference
implementation.

Signed-off-by: khazic <khazzz1c@gmail.com>
Requested in review: the Megatron-Bridge design doc does not need a breakdown
of the AutoModel implementation; the pointer in the overview is enough.

Signed-off-by: khazic <khazzz1c@gmail.com>
The design page carried a four-phase implementation plan and an open-questions
list. Neither describes what this PR (or the repo) actually contains: they
promise work that is not scheduled and has no owner, so they go stale the moment
priorities move. The sequencing and the open decisions belong in the tracking
issue (NVIDIA-NeMo#5230), which is where they can be updated.

The remaining page is the design itself: the algorithm, where it fits in
Megatron-Bridge, and the proposed file surface. The frozen-target supervision
section keeps both capture options, since the trade-off between them is design
content rather than a schedule.

Signed-off-by: khazic <khazzz1c@gmail.com>
khazic added 4 commits August 13, 2026 10:51
…racts

Select Megatron-Core native ownership and give the reasons. ModelOpt's DSpark
support is HF-path only (hf_dspark and modeling_dspark plugins plus
DSparkExporter, with megatron_eagle and megatron_medusa but no megatron_dspark),
so routing through ModelOpt would put a Model-Optimizer contribution and a pin
bump in front of every Bridge milestone.

Replace the ModelProviderMixin recommendation with ModelConfig + ModelBuilder.
Provider-based configuration is deprecated in current main, so it could not be
the stable API this page promises.

Correct the objective: the training term is the raw weighted L1 of paper Eq. 10,
and the factor 1/2 belongs only to the Eq. 8 acceptance label. Training half the
L1 at the published coefficient would halve the dominant gradient.

Define the contracts the previous revision left open: replica-not-alias storage
for the frozen embedding and LM head, shard-local exact L1 under tensor
parallelism, selected-hidden transport under pipeline parallelism, block-aligned
anchor sampling under context parallelism, the versioned offline feature-cache
schema, the Megatron-Core loss return contract with its calculate_per_token_loss
requirement, the measured memory contract for the probability L1, and Bridge as
the owner of the HF export schema.

Signed-off-by: khazic <khazzz1c@gmail.com>
First increment of DSpark speculative-decoding draft training (NVIDIA-NeMo#5230; design in
NVIDIA-NeMo#5233): the architecture-agnostic, pure-torch core.

Adds src/megatron/bridge/training/post_training/dspark/:
* heads.py: the sequential head (VanillaMarkov / GatedMarkovHead / RNNHead) that
  injects intra-block token dependency onto the parallel-backbone logits, plus the
  confidence head.
* loss.py: the three-term CE + total-variation + confidence-BCE objective with
  per-position decay, and the analytical acceptance metrics (accept_rate = 1 - TV,
  tau = sum cumprod(accept) + 1). The data-parallel denominator reduction is an
  injectable dp_group, so the same math wires to a Megatron-Core forward step later.

CPU unit tests cover the head math and the loss / acceptance math. The
parallel-aware draft backbone and the forward-step / provider integration (which
need Megatron-Core and multi-GPU validation) are follow-ups.

Signed-off-by: khazic <khazzz1c@gmail.com>
The DSpark loss denominator reduction now runs only when a dp_group is passed;
dp_group=None is local-only. Previously it fell back to the default process
group whenever torch.distributed was initialized, whose members are not the DP
replicas under Megatron-Core parallelism (TP/PP/CP), so the caller must pass the
data-parallel group explicitly.

Add a 2-GPU functional test
(tests/functional_tests/test_groups/training/test_dspark_loss_dp.py) that spawns
two NCCL ranks and asserts the DP-averaged gradient through a shared head matches
the single-process full-batch gradient. This validates the dp_group reduction,
which cannot be exercised on a single process, and needs a GPU node to run.

Signed-off-by: khazic <khazzz1c@gmail.com>
Return the unnormalized micro-batch numerator plus num_tokens and reducible
[numerator, denominator] reporting pairs, instead of dividing by a locally
reduced denominator. Megatron-Core calls the loss once per micro-batch, so a
locally normalized value accumulates a mean of micro-batch means and biases the
objective whenever micro-batches carry different supervised-token counts. The
unbiased global mean needs calculate_per_token_loss=True, which
assert_per_token_loss_config now enforces at the integration point.

Chunk the FP32 probability L1 so the softmax temporaries are bounded by
chunk_tokens * vocab rather than the full [batch, num_blocks, block_size, vocab]
tensor, recomputing each chunk in the backward. Three such tensors at reference
scale are several GiB of forward temporaries before autograd storage.

Correct the objective's documentation: the training term is the raw weighted L1
of paper Eq. 10, and the factor 1/2 belongs only to the Eq. 8 acceptance label.
The implementation already did this; the docstring and the design page did not. A
disjoint-distribution regression now asserts l1_loss == 2 with accept_rate == 0
so the two cannot drift apart again.

Rewrite the 2-GPU functional test around the new contract: two ranks, two
micro-batches each, uneven supervised-token counts, asserting both that the
global-token gradient matches the single-process reference and that the
per-micro-batch normalization does not. Add a dedicated L0 launch script so CI
actually selects the test, using a single pytest coordinator because the test
self-spawns its ranks.

Signed-off-by: khazic <khazzz1c@gmail.com>
@khazic

khazic commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Status note: this PR needs a rebase and is not reviewable as it stands.

It was stacked on #5241, which I have now closed. Paper Eq. 10 trains the weighted raw L1 and the factor 1/2 belongs only to the Eq. 8 acceptance label, matching DeepSpec and NeMo AutoModel, so #5241 would have halved the dominant term's gradient. Because the stack is linear (#5241 to #5242 to #5243 to #5244), this branch still carries that commit as its base and its diff still shows the _tv_distance_per_token rename.

The change this PR actually contributes is still valid and I am keeping it. #5234 has meanwhile been reworked fairly heavily (Megatron-Core 3-tuple return contract, no dp_group, and both full-vocabulary terms fused into one chunked, checkpointed pass), so the fix has to be reapplied on top of that rather than replayed. I will rebase once #5234 settles, to avoid redoing it if that review moves again.

khazic added 2 commits August 13, 2026 11:16
… metrics

block_keep_mask marked anchors as dropped but nothing acted on it: eval_mask
alone drove the loss weight, so a dropped anchor still contributed gradient, and
it still entered num_tokens and the acceptance diagnostics.

Combine the two into one supervision mask, eval_mask AND the kept-anchor flag,
and drive the loss weight, the token count, and the acceptance metrics from it.
Dropping an anchor is now exactly equivalent to zeroing its eval_mask, which a
test asserts directly, and dropped anchors leave the metric denominators rather
than sitting in them at weight zero.

Signed-off-by: khazic <khazzz1c@gmail.com>
…DSpark loss

Padded teacher-forcing layouts fill unsupervised positions with sentinel target
ids (-1 / -100) and non-finite logits. A sentinel id indexes out of bounds in the
gather, and zero-weighting a non-finite row does not save the step: 0 * NaN is
NaN in the forward, and the backward of a non-finite row poisons the gradient
regardless of its weight.

Replace the inputs at unsupervised positions instead of masking the outputs.
torch.where on draft_logits selects exact zeros into those slots in the backward
rather than multiplying by zero, so no NaN reaches draft_logits.grad; target_ids
are filled with a valid row; the reduced L1 and the confidence logits are
scrubbed for non-finite teacher rows without copying the full-vocab teacher.

Keying off the combined supervision mask means dropped anchors get the same
protection as eval-masked positions. A poisoned batch now produces bit-identical
losses, metrics, and gradients to the equivalent clean one, which the tests
assert term by term.

Note in the head's docstring that the embedding lookup stays strict: it is the
producer's job to replace sentinels, and clamping there would silently train on
row 0 rather than fail.

Signed-off-by: khazic <khazzz1c@gmail.com>
@khazic
khazic force-pushed the khazic/fix/dspark-masked-input-robustness branch from f334076 to ba3eede Compare August 13, 2026 03:20
@khazic

khazic commented Aug 13, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto #5242 (itself now on the reworked #5234) in ba3eede. The closed #5241 is out of this branch's history, so the diff no longer carries the 0.5 * L1 change.

Reapplied rather than replayed. The sanitizing is the same idea and the reasoning is now written into the code, because it is the non-obvious part: masking the outputs is not enough, since 0 * NaN is NaN in the forward and a non-finite row poisons the backward whatever its weight. Replacing the inputs with torch.where is what makes it safe, because where's backward selects exact zeros into the masked slots instead of multiplying by zero, so no NaN reaches draft_logits.grad.

Two things changed with the new base. The scrub keys off #5242's combined supervision mask, so a poisoned position inside a dropped anchor is covered too, which a new test asserts. And the reduced-L1 scrub now sits ahead of the fused chunk output rather than the old _tv_distance_per_token call.

Tests: the original poisoned-versus-clean case is kept and strengthened to compare every reporting pair's numerator and denominator rather than a fixed metric list. 28 unit tests pass, and the 2-GPU functional test passes under torchrun --nproc_per_node=2.

@yaoyu-33

Copy link
Copy Markdown
Contributor

/ok to test ba3eede

kamran-nvidia and others added 8 commits August 13, 2026 19:55
…string

The docs are built with MyST, where a line of dashes under text is a setext
H2. The generated apidocs page for this module therefore started at H2 and
tripped "Document headings start at H2, not H1 [myst.header]", which fails
the docs build because it treats warnings as errors. Use bold text instead;
this was the only RST underline under src/megatron/bridge.

Signed-off-by: khazic <khazzz1c@gmail.com>
@khazic

khazic commented Aug 19, 2026

Copy link
Copy Markdown
Contributor Author

Merged the updated #5242 in (which itself now carries the current #5234 head), so this branch is back on top of the stack it belongs to.

That clears the docs failure: dspark/loss.py's module docstring used an RST underline for the Return contract section, MyST read it as a setext H2, and sphinx-build --fail-on-warning failed with Document headings start at H2, not H1 [myst.header]. #5234 fixed it by switching to bold; this branch was pinned to the version before that fix.

The other red checks on the previous run were not code failures: Launch_Unit_Tests_Core hit Test timed out after 18 minutes, and release-summary / Nemo_CICD_Test were just reporting that one failed job upstream. Could someone re-trigger CI on this head?

No change to the sanitization fix itself.

# Conflicts:
#	src/megatron/bridge/training/post_training/dspark/heads.py
#	src/megatron/bridge/training/post_training/dspark/loss.py
#	tests/unit_tests/training/post_training/test_dspark_loss.py

This branch was previously deployed

2 inactive (outdated) deployments
public — ba3eede3 Deployed Aug 13, 2026 by copy-pr-bot[bot] via release / finalize / notify #4529
test — ba3eede3 Deployed Aug 13, 2026 by copy-pr-bot[bot] via cicd-wait-in-queue #20423
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:training Training loop, callbacks, and runtime integration bug Something isn't working community-request needs-more-tests Requires additional L0 and L1 test coverage before merge needs-review PR is ready for code review and waiting on a reviewer

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants