Skip to content

Commit e95ea35

Browse files
authored
fix(mtp): preserve padding masks and handle empty auxiliary loss (#7611)
Signed-off-by: Chen Cui <chcui@nvidia.com>
1 parent 6a6c8aa commit e95ea35

6 files changed

Lines changed: 383 additions & 37 deletions

File tree

‎megatron/core/models/hybrid/hybrid_model.py‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,7 @@ def forward(
742742
mtp_input_mask=mtp_input_mask,
743743
packed_seq_params=packed_seq_params,
744744
cp_batch=cp_batch,
745+
padding_mask=padding_mask,
745746
)
746747
if mtp_inputs.decoder_input is None:
747748
assert mtp_inputs.input_ids is not None and mtp_inputs.position_ids is not None, (
@@ -760,6 +761,7 @@ def forward(
760761
embedding=self.embedding,
761762
decoder_input=mtp_inputs.decoder_input,
762763
mtp_input_mask=mtp_inputs.mtp_input_mask,
764+
padding_mask=mtp_inputs.padding_mask,
763765
packed_seq_params_by_layout=packed_seq_params_by_layout,
764766
cp_layout_plan=cp_layout_plan,
765767
)

‎megatron/core/transformer/moe/moe_utils.py‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,14 @@ def switch_load_balancing_loss_func(
133133
mask_expanded = padding_mask.unsqueeze(-1)
134134
probs = probs * mask_expanded
135135

136+
# An entirely padded MTP depth can have no valid tokens across the reduction
137+
# group. Its masked probabilities contribute zero loss; keep normalization
138+
# finite so backward does not turn that zero contribution into NaNs.
139+
if isinstance(total_num_tokens, torch.Tensor):
140+
total_num_tokens = total_num_tokens.clamp(min=1)
141+
else:
142+
total_num_tokens = max(total_num_tokens, 1)
143+
136144
if fused:
137145
if not HAVE_TE or fused_moe_aux_loss is None:
138146
raise ValueError("fused_moe_aux_loss is not available. Please install TE >= 2.7.0.")

‎megatron/core/transformer/multi_token_prediction.py‎

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from megatron.core.process_groups_config import ProcessGroupCollection
2727
from megatron.core.tensor_observation import is_observing_tensor, observe_tensor
2828
from megatron.core.tensor_parallel import (
29+
gather_from_sequence_parallel_region,
2930
gather_from_tensor_model_parallel_region,
3031
scatter_to_sequence_parallel_region,
3132
)
@@ -1585,6 +1586,8 @@ def _get_embeddings(
15851586
hidden_states (torch.Tensor): hidden states tensor of shape [s, b, h] where s is the
15861587
sequence length, b is the batch size, and h is the hidden size.
15871588
packed_seq_params (PackedSeqParams): Parameters for packed sequence processing.
1589+
padding_mask (torch.Tensor, optional): Padding flags of shape [b, s/tp] with
1590+
sequence parallelism, otherwise [b, s]. True marks padding.
15881591
mtp_input_mask (torch.Tensor, optional): Mask of conditioning tokens backed by
15891592
regular token embeddings. Shape: [b, s].
15901593
"""
@@ -1593,6 +1596,14 @@ def _get_embeddings(
15931596
f"mtp_input_mask shape {mtp_input_mask.shape} must match "
15941597
f"input_ids shape {input_ids.shape}"
15951598
)
1599+
if padding_mask is not None and self.config.sequence_parallel:
1600+
# The mask is SP-sharded, while IDs and CP metadata cover the full
1601+
# CP-local sequence. Restore that sequence before either layout's roll.
1602+
padding_mask = gather_from_sequence_parallel_region(
1603+
padding_mask.transpose(0, 1).contiguous(),
1604+
tensor_parallel_output_grad=False,
1605+
group=self.tp_group,
1606+
).transpose(0, 1)
15961607
if self.config.attention_cp_layout == "contiguous":
15971608
input_ids, position_ids, padding_mask, mtp_input_mask = roll_contiguous_fields(
15981609
(input_ids, position_ids, padding_mask, mtp_input_mask),
@@ -1636,14 +1647,24 @@ def _get_embeddings(
16361647
return_sum=False,
16371648
)
16381649
if padding_mask is not None:
1639-
padding_mask, _ = roll_tensor(
1640-
padding_mask,
1650+
# Roll validity so zero-filled sequence ends remain padding.
1651+
valid_mask, _ = roll_tensor(
1652+
~padding_mask,
16411653
shifts=-1,
16421654
dims=-1,
16431655
cp_group=self.cp_group,
16441656
packed_seq_params=packed_seq_params,
16451657
return_sum=False,
16461658
)
1659+
padding_mask = ~valid_mask
1660+
if padding_mask is not None and self.config.sequence_parallel:
1661+
padding_mask = (
1662+
scatter_to_sequence_parallel_region(
1663+
padding_mask.transpose(0, 1).contiguous(), group=self.tp_group
1664+
)
1665+
.transpose(0, 1)
1666+
.contiguous()
1667+
)
16471668
# embedding
16481669
decoder_input = embedding(input_ids=input_ids, position_ids=position_ids)
16491670

@@ -2241,6 +2262,7 @@ class MultiTokenPredictionInputs:
22412262
loss_mask: Optional[Tensor]
22422263
mtp_input_mask: Optional[Tensor]
22432264
packed_seq_params: Optional[PackedSeqParams]
2265+
padding_mask: Optional[Tensor] = None
22442266

22452267

22462268
def _get_mtp_block_submodules(
@@ -2404,8 +2426,9 @@ def prepare_cp_layout(
24042426
mtp_input_mask: Optional[Tensor],
24052427
packed_seq_params: Optional[PackedSeqParams],
24062428
cp_batch: Optional[ContextParallelBatch],
2429+
padding_mask: Optional[Tensor] = None,
24072430
) -> MultiTokenPredictionInputs:
2408-
"""Prepare activations and token-aligned inputs for the MTP block's CP layout."""
2431+
"""Prepare MTP inputs, including the batch-major, optionally SP-sharded padding mask."""
24092432
source_layout = (
24102433
cp_batch.boundary_layout if cp_batch is not None else self.config.linear_cp_layout
24112434
)
@@ -2449,6 +2472,23 @@ def prepare_cp_layout(
24492472
self.tp_cp_group,
24502473
cp_batch.thd_plan,
24512474
)
2475+
if padding_mask is not None:
2476+
# Convert validity so any new THD padding slots (zero-filled by
2477+
# layout conversion) remain excluded from routing.
2478+
padding_mask = (
2479+
~convert_cp_layout(
2480+
(~padding_mask).transpose(0, 1).contiguous(),
2481+
source_layout,
2482+
target_layout,
2483+
self.cp_group,
2484+
self.sequence_parallel,
2485+
self.tp_group,
2486+
self.tp_cp_group,
2487+
cp_batch.thd_plan,
2488+
)
2489+
.transpose(0, 1)
2490+
.contiguous()
2491+
)
24522492
packed_seq_params = cp_batch.get_packed_seq_params(target_layout)
24532493
layout_batch = cp_batch.get_batch(target_layout)
24542494
input_ids = layout_batch["tokens"]
@@ -2466,6 +2506,7 @@ def prepare_cp_layout(
24662506
loss_mask=loss_mask,
24672507
mtp_input_mask=mtp_input_mask,
24682508
packed_seq_params=packed_seq_params,
2509+
padding_mask=padding_mask,
24692510
)
24702511

24712512
def _build_layers(self, pg_collection):
@@ -2638,6 +2679,19 @@ def forward(
26382679
packed_seq_params=packed_seq_params,
26392680
return_sum=False,
26402681
)
2682+
if padding_mask is not None:
2683+
# Precomputed embeddings bypass the layer's _get_embeddings,
2684+
# so shift validity here alongside those embeddings.
2685+
valid_mask, _ = roll_tensor_precomputed_embeddings(
2686+
(~padding_mask).transpose(0, 1).contiguous(),
2687+
shifts=-1,
2688+
dims=0,
2689+
sp_group=self.tp_group if self.sequence_parallel else None,
2690+
cp_group=self.cp_group,
2691+
packed_seq_params=packed_seq_params,
2692+
return_sum=False,
2693+
)
2694+
padding_mask = ~valid_mask.transpose(0, 1).contiguous()
26412695

26422696
# Older HSM entries predict earlier targets than the newest entry. Roll
26432697
# them once per depth so all candidates correspond to the same target.

‎tests/unit_tests/determinism/kernels/test_moe_kernels.py‎

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -238,18 +238,25 @@ def test_group_limited_topk_replays():
238238
),
239239
],
240240
)
241-
def test_switch_load_balancing_loss_replays(fused):
241+
@pytest.mark.parametrize("all_padding", [False, True])
242+
def test_switch_load_balancing_loss_replays(fused, all_padding):
242243
seeded()
243244
routing_map, probs = _routing(num_tokens=65536, num_experts=256, topk=8)
245+
if all_padding:
246+
routing_map.zero_()
247+
probs.zero_()
244248
probs = probs.detach().requires_grad_(True)
245249
tokens_per_expert = routing_map.sum(dim=0)
250+
total_num_tokens = tokens_per_expert.sum() // 8 if all_padding else 65536
246251

247252
def fn(probs):
248253
return moe_utils.switch_load_balancing_loss_func(
249-
probs, tokens_per_expert, 65536, 8, 256, 1e-2, fused=fused
254+
probs, tokens_per_expert, total_num_tokens, 8, 256, 1e-2, fused=fused
250255
)
251256

252-
assert_replays_bit_exact(fn, (probs,), replays=4, what=f"aux loss[fused={fused}]")
257+
assert_replays_bit_exact(
258+
fn, (probs,), replays=4, what=f"aux loss[fused={fused}, all_padding={all_padding}]"
259+
)
253260

254261

255262
@pytest.mark.parametrize("router_dtype", [torch.float32, torch.float64])

‎tests/unit_tests/transformer/moe/test_routers.py‎

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
from megatron.core.transformer.moe.moe_logging import MoEMetricsTracker
2626
from megatron.core.transformer.moe.moe_utils import (
2727
MoEAuxLossAutoScaler,
28+
fused_compute_score_for_moe_aux_loss,
29+
fused_moe_aux_loss,
2830
get_default_pg_collection,
2931
get_tokens_per_expert_and_token_count,
3032
get_updated_expert_bias,
@@ -790,6 +792,59 @@ def test_aux_loss(self):
790792
out.sum().mul_(0).backward()
791793
assert self.sequential_mlp.router.weight.grad.abs().sum() > 0
792794

795+
@pytest.mark.internal
796+
@pytest.mark.parametrize("loss_type", ["aux_loss", "seq_aux_loss", "global_aux_loss"])
797+
@pytest.mark.parametrize("fused", [False, True])
798+
@pytest.mark.parametrize("with_history", [False, True])
799+
def test_aux_loss_with_entirely_padded_input(self, monkeypatch, loss_type, fused, with_history):
800+
"""Zero valid tokens contribute finite zero auxiliary gradients, even with history."""
801+
if fused and (fused_moe_aux_loss is None or fused_compute_score_for_moe_aux_loss is None):
802+
pytest.skip("TE fused auxiliary-loss operators are unavailable")
803+
config = TransformerConfig(
804+
num_layers=1,
805+
hidden_size=16,
806+
num_attention_heads=2,
807+
num_moe_experts=4,
808+
moe_router_topk=2,
809+
moe_router_load_balancing_type=loss_type,
810+
moe_aux_loss_coeff=0.001,
811+
moe_router_aux_loss_fusion=fused,
812+
moe_router_dtype="fp32",
813+
params_dtype=torch.float32,
814+
add_bias_linear=False,
815+
)
816+
losses = []
817+
compute_aux_loss = router_mod.switch_load_balancing_loss_func
818+
819+
def record_aux_loss(*args, **kwargs):
820+
loss = compute_aux_loss(*args, **kwargs)
821+
losses.append(loss.detach())
822+
return loss
823+
824+
monkeypatch.setattr(router_mod, "switch_load_balancing_loss_func", record_aux_loss)
825+
router = TopKRouter(config, pg_collection=get_default_pg_collection()).cuda()
826+
hidden_states = torch.randn(8, 2, 16, device="cuda", requires_grad=True)
827+
if with_history:
828+
probs, _ = router(hidden_states)
829+
# Isolate the attached auxiliary gradient from the routing output loss.
830+
(probs.sum() * 0).backward()
831+
assert torch.isfinite(router.weight.grad).all()
832+
assert torch.count_nonzero(router.weight.grad) > 0
833+
router.zero_grad(set_to_none=True)
834+
hidden_states.grad = None
835+
previous_counts = (
836+
router.global_tokens_per_expert.clone() if loss_type == "global_aux_loss" else None
837+
)
838+
padding_mask = torch.ones(8, 2, dtype=torch.bool, device="cuda")
839+
probs, _ = router(hidden_states, padding_mask=padding_mask)
840+
(probs.sum() * 0).backward()
841+
assert len(losses) == (2 if with_history else 1)
842+
torch.testing.assert_close(losses[-1], torch.zeros_like(losses[-1]))
843+
torch.testing.assert_close(router.weight.grad, torch.zeros_like(router.weight.grad))
844+
torch.testing.assert_close(hidden_states.grad, torch.zeros_like(hidden_states.grad))
845+
if previous_counts is not None:
846+
torch.testing.assert_close(router.global_tokens_per_expert, previous_counts)
847+
793848
@pytest.mark.internal
794849
@pytest.mark.skipif(not torch.cuda.is_available(), reason="CUDA not available")
795850
@pytest.mark.parametrize("router_fusion", [False, True])

0 commit comments

Comments
 (0)