Skip to content

several fixes for THD e2e - #5535

Open
xiaoyao0115 wants to merge 1 commit into
NVIDIA:devfrom
xiaoyao0115:fix/thd-padding-mtp-local-sum
Open

several fixes for THD e2e#5535
xiaoyao0115 wants to merge 1 commit into
NVIDIA:devfrom
xiaoyao0115:fix/thd-padding-mtp-local-sum

Conversation

@xiaoyao0115

@xiaoyao0115 xiaoyao0115 commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

What does this PR do ?

This PR fixes padding handling for variable-length THD and SBHD training:

  • Keeps real cu_seqlens separate from physical cu_seqlens_padded. Trailing alignment padding only extends the padded endpoint, while pad_between_seqs is left for TE to infer.
  • Propagates SBHD validation padding masks through pipeline stages.
  • Excludes padding tokens from MoE routing and dispatch so they do not affect auxiliary losses, expert capacity, permutation, or grouped GEMM shapes.
  • Restores MTP logging from a global token-weighted loss (sum(loss) / sum(tokens)) to accumulated normalized per-microbatch losses.

Issue tracking

For PRs from open-source community contributors:

  • New features: a linked issue is required. Please open a feature request and reference it here before submitting the PR.
  • Small updates (bug fixes, minor improvements): a linked issue is recommended and will accelerate the PR review process.

Linked issue:

Contribution process

Pre-checks

  • I have added relevant unit tests
  • I have added relevant functional tests
  • I have added proper typing to my code Typing guidelines
  • I have added relevant documentation
  • I have run the autoformatter.sh on my PR

Code review

Feel free to message or comment @NVIDIA/mcore-oncall to help accelerate your merge into main. The less complex your PR is, the faster it will be approved and merged!

All PRs start as draft. If you open a non-draft PR, it will be automatically converted to draft.

Step 1: Mark PR as "Ready for Review"

  1. When your PR is ready, click Ready for Review.
  2. An oncall reviewer is auto-assigned and expert reviewers are notified based on your changes.
    • Some PRs may jump straight to step 2. This is determined by .github/CODEOWNERS.

⚠️ Only mark as ready once merge-conflicts are resolved and the CI is passing.
Final Review might get declined if these requirements are not fulfilled.

Step 2: Final Review

For PRs that change megatron/core, once all expert reviewers have approved, the Final Review label is applied automatically and final reviewers are assigned.

For PRs outside megatron/core, this step is skipped.

Step 3: Approved

Once all required reviewers have approved, the Approved label is applied automatically.

Merge

Any member of mcore-engineers will be able to merge your PR.

@xiaoyao0115 xiaoyao0115 self-assigned this Jun 29, 2026
@xiaoyao0115
xiaoyao0115 requested review from a team as code owners June 29, 2026 07:49
@copy-pr-bot

copy-pr-bot Bot commented Jun 29, 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.

Signed-off-by: xiaoyao0115 <1804647152@qq.com>
@xiaoyao0115
xiaoyao0115 force-pushed the fix/thd-padding-mtp-local-sum branch from 0b2ea65 to 9b4db6a Compare June 29, 2026 07:59
@xiaoyao0115

Copy link
Copy Markdown
Contributor Author

/ok to test 9b4db6a

@yaox12

yaox12 commented Jun 29, 2026

Copy link
Copy Markdown
Member

/claude strict-review

"""

pad_packed_seq_by_appending_dummy_seq: bool = True
pad_packed_seq_by_appending_dummy_seq: bool = False

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Compatibility] Changing the default from True to False is a silent behavior change for existing training scripts that rely on the default. Users upgrading without modifying their configs will get different cu_seqlens metadata (the padding tail now extends the last padded endpoint instead of creating a dummy sequence).

This is the correct long-term default (it aligns with the separation of valid vs. padded boundaries), but consider adding a deprecation note in the release/changelog so users relying on the old default know to set pad_packed_seq_by_appending_dummy_seq=True explicitly if needed.

Comment on lines +508 to +515
values = tracker["loss_sums"]
if tracker.get('reduce_group') is not None:
torch.distributed.all_reduce(values, group=tracker['reduce_group'])
if tracker.get('avg_group') is not None:
torch.distributed.all_reduce(
values, group=tracker['avg_group'], op=torch.distributed.ReduceOp.AVG
)
tracker["values"] = values

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION Simplification] values = tracker["loss_sums"] is a reference, not a copy. The in-place all_reduce modifies tracker["loss_sums"], and then tracker["values"] = values makes both keys point to the same tensor. This means clean_loss_in_tracker() zeroing loss_sums also destroys values.

Currently safe because track_mtp_metrics calls _report (which reads values) before clean_loss_in_tracker. But the aliasing is non-obvious — a future caller reading tracker["values"] after cleanup would silently get zeros. Consider cloning to keep the tensors independent:

tracker["values"] = values.clone()

Comment on lines 425 to 426
tracker = MTPLossLoggingHelper.tracker
if "loss_sums" not in tracker:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Compatibility] This changes MTP logging semantics from global token-weighted average (sum(loss) / sum(tokens) across microbatches and ranks) to mean of per-microbatch normalized losses. The corresponding mtp_loss_scale in training.py changed from 1.0 to 1/num_microbatches.

For ongoing training runs, the logged MTP loss metric value will change at the upgrade boundary even without any real model change — the two formulas only agree when all microbatches have the same token count. This is documented as intentional ("preserves sequence-packing semantics"), but anyone comparing pre- and post-upgrade loss curves should be aware the metric definition changed.

Comment on lines 464 to +486
if has_dummy_padding_seq:
cu_seqlens_q = _append_dummy_seq(cu_seqlens_q, global_target_len)
cu_seqlens_kv = _append_dummy_seq(cu_seqlens_kv, global_target_len)
cu_seqlens_q_padded = _append_dummy_seq(cu_seqlens_q_padded, global_target_len)
cu_seqlens_kv_padded = _append_dummy_seq(cu_seqlens_kv_padded, global_target_len)
# None is TE's auto-detect mode. Keep real and physical boundaries
# separate unless the caller explicitly disables padding between sequences.
if (
packed_seq_params.pad_between_seqs is not False
and cu_seqlens_q is not None
and cu_seqlens_q_padded is not None
):
cu_seqlens_q = _append_dummy_seq(cu_seqlens_q, int(cu_seqlens_q[-1].item()))
cu_seqlens_q_padded = _append_dummy_seq(cu_seqlens_q_padded, global_target_len)
else:
cu_seqlens_q = _append_dummy_seq(cu_seqlens_q, global_target_len)
cu_seqlens_q_padded = _append_dummy_seq(cu_seqlens_q_padded, global_target_len)
if (
packed_seq_params.pad_between_seqs is not False
and cu_seqlens_kv is not None
and cu_seqlens_kv_padded is not None
):
cu_seqlens_kv = _append_dummy_seq(cu_seqlens_kv, int(cu_seqlens_kv[-1].item()))
cu_seqlens_kv_padded = _append_dummy_seq(cu_seqlens_kv_padded, global_target_len)
else:
cu_seqlens_kv = _append_dummy_seq(cu_seqlens_kv, global_target_len)
cu_seqlens_kv_padded = _append_dummy_seq(cu_seqlens_kv_padded, global_target_len)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[SUGGESTION Simplification] The Q and KV branches are exact mirror copies. Consider extracting a small helper to remove the duplication:

def _append_pair(cu_valid, cu_padded, pad_between_seqs, global_target_len):
    if (
        pad_between_seqs is not False
        and cu_valid is not None
        and cu_padded is not None
    ):
        return (
            _append_dummy_seq(cu_valid, int(cu_valid[-1].item())),
            _append_dummy_seq(cu_padded, global_target_len),
        )
    return (
        _append_dummy_seq(cu_valid, global_target_len),
        _append_dummy_seq(cu_padded, global_target_len),
    )

Then call it twice for Q and KV. Not a correctness issue, just reduces 23 lines to ~5.

Comment on lines +833 to +838
valid_tokens = (~padding_mask).unsqueeze(-1)
probs = probs * valid_tokens
routing_map = routing_map & valid_tokens

# Apply token dropping after removing padding so padding tokens cannot
# consume expert capacity.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[IMPORTANT Correctness — verified safe] Good fix — masking padding tokens from probs and routing_map before token dropping is critical: without this, padding tokens consume expert capacity slots and can displace valid tokens.

The placement after routing but before apply_router_token_dropping is correct. The softmax and top-k still include padding tokens, but this has negligible impact since padding hidden states are zero/random and don't systematically bias expert selection. The aux loss path independently handles padding via compute_routing_scores_for_aux_loss(... padding_mask=padding_mask) below.

@claude

claude Bot commented Jun 29, 2026

Copy link
Copy Markdown
Contributor

Code Review Summary

CRITICAL: 0 | IMPORTANT: 2 | SUGGESTION: 2 | Positive: 1

Key Findings

[IMPORTANT Compatibility] Default change for pad_packed_seq_by_appending_dummy_seq (model_parallel_config.py:121)
The default flips from TrueFalse. Existing training scripts relying on the default will silently get different cu_seqlens metadata (extended last padded endpoint vs. appended dummy sequence). The new default is the better design — but should be called out in release notes.

[IMPORTANT Compatibility] MTP logging semantics change (multi_token_prediction.py:425, training.py:2960)
MTP loss logging changes from global token-weighted average to mean of per-microbatch normalized losses. These only agree when all microbatches have equal token counts. Ongoing training runs will see a metric discontinuity at the upgrade boundary.

[SUGGESTION Simplification] The Q/KV dummy-sequence branches in packed_seq_params.py:464-486 are exact mirror copies and could be factored into a small helper.

[SUGGESTION] In reduce_loss_in_tracker, the in-place all_reduce aliases tracker["values"] with tracker["loss_sums"]. Safe with current call ordering, but a .clone() would prevent subtle issues if the call sequence ever changes.

Correctness Verification

Traced the four main changes end-to-end:

  1. cu_seqlens/cu_seqlens_padded separation: Verified through _resolve_thd_padding_lengthspad_sequence_for_thd_replace_last_cu_seqlen / _append_dummy_seq. The padded endpoint correctly describes physical storage while valid boundaries are preserved. _max_physical_seqlen correctly computes max from padded boundaries. CP test cases (test_cp_alignment_*) trace correctly with the new logic.

  2. SBHD padding mask propagation: The has_padding_mask flag coordination between data-producing and non-data-producing ranks is correct. _broadcast(None) is a safe no-op. is_dataset_built_on_rank returning True for all PP stages in SBHD mode ensures each stage has its own data iterator producing matching padding metadata.

  3. MoE padding exclusion: Placement after routing but before apply_router_token_dropping is correct — padding tokens are excluded before capacity enforcement. Aux loss independently handles padding via compute_routing_scores_for_aux_loss. The valid_tokens mask shape [num_tokens, 1] broadcasts correctly with probs and routing_map.

  4. MTP logging restoration: Per-microbatch normalization in save_loss_to_tracker, ReduceOp.AVG for avg_group, and 1/num_microbatches scaling in training_log are internally consistent. The zero-token guard (num_tokens > 0) prevents inf/nan.

Overall Assessment

Risk: Low-Medium. The implementation is well-structured with comprehensive test updates. The two compatibility changes (default flip, metric semantics) are intentional but should be documented in release notes. No correctness or performance issues found.

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.

2 participants