Skip to content

Commit 72ae296

Browse files
committed
fix(r3): balance replay padding across expert ranks
Rows masked from router accounting still reach Megatron MoE dispatch, while constant dummy routes concentrate work on the first contiguous expert shard. Repair only masked rows in model token order and enumerate assignments across EP ranks before local experts. Match unpacked replay splitting to Megatron sequence-major order and widen aligned local routes before writing IDs that may exceed compact rollout storage. Signed-off-by: EazyReal <8047065+EazyReal@users.noreply.github.com>
1 parent a956457 commit 72ae296

2 files changed

Lines changed: 84 additions & 22 deletions

File tree

skyrl/backends/skyrl_train/utils/replay_utils.py

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -116,10 +116,40 @@ def patched_apply_expert_bias(self, routing_map: torch.Tensor, padding_mask: tor
116116

117117

118118
def _split_replay_indices(rollout_expert_indices: torch.Tensor) -> list[torch.Tensor]:
119-
per_layer = rollout_expert_indices.permute(2, 0, 1, 3).contiguous().to(torch.int32)
119+
per_layer = rollout_expert_indices.permute(2, 1, 0, 3).contiguous().to(torch.int32)
120120
return list(per_layer.flatten(1, 2).unbind(0))
121121

122122

123+
def _distribute_replay_padding_indices(
124+
rollout_expert_indices: torch.Tensor,
125+
router_padding_mask: torch.Tensor,
126+
num_experts: int,
127+
expert_parallel_size: int,
128+
) -> None:
129+
"""Distribute padding routes across experts in model token order."""
130+
topk = rollout_expert_indices.shape[-1]
131+
if num_experts < topk:
132+
raise ValueError(f"Replay topk ({topk}) cannot exceed the number of MoE experts ({num_experts})")
133+
if expert_parallel_size < 1 or num_experts % expert_parallel_size:
134+
raise ValueError(
135+
f"The number of MoE experts ({num_experts}) must be divisible by the expert parallel size "
136+
f"({expert_parallel_size})"
137+
)
138+
139+
model_order_indices = rollout_expert_indices.permute(1, 0, 2, 3)
140+
model_order_padding_mask = router_padding_mask.transpose(0, 1)
141+
flat_padding_mask = model_order_padding_mask.flatten()
142+
padding_ordinals = flat_padding_mask.to(torch.long).cumsum(0)[flat_padding_mask] - 1
143+
expert_offsets = torch.arange(topk, device=rollout_expert_indices.device)
144+
assignment_ordinals = padding_ordinals.unsqueeze(1) * topk + expert_offsets
145+
num_local_experts = num_experts // expert_parallel_size
146+
# Megatron assigns each EP rank a contiguous expert-ID range. Enumerating
147+
# rank before local expert balances every prefix across those ranges.
148+
padding_routes = (assignment_ordinals % expert_parallel_size) * num_local_experts
149+
padding_routes += (assignment_ordinals // expert_parallel_size) % num_local_experts
150+
model_order_indices[model_order_padding_mask] = padding_routes.to(rollout_expert_indices.dtype).unsqueeze(1)
151+
152+
123153
def scatter_router_padding_mask_for_model(
124154
router_padding_mask: torch.Tensor | None,
125155
model,
@@ -264,17 +294,24 @@ def setup_per_microbatch_replay_forward(
264294
local_rollout_expert_indices,
265295
metadata_layout,
266296
route_padding,
267-
)
297+
).to(torch.int32)
268298

269299
# TP splitting: sequence parallelism across the tensor model parallel region
270300
tp_size = mpu.get_tensor_model_parallel_world_size()
301+
local_router_padding_mask = aligned_router_padding_mask
271302
if tp_size > 1:
272303
tp_rank = mpu.get_tensor_model_parallel_rank()
273304
seq_len = aligned_rollout_expert_indices.shape[1]
274305
chunk_size = seq_len // tp_size
275-
aligned_rollout_expert_indices = aligned_rollout_expert_indices[
276-
:, tp_rank * chunk_size : (tp_rank + 1) * chunk_size, :, :
277-
]
306+
local_slice = slice(tp_rank * chunk_size, (tp_rank + 1) * chunk_size)
307+
aligned_rollout_expert_indices = aligned_rollout_expert_indices[:, local_slice, :, :]
308+
local_router_padding_mask = local_router_padding_mask[:, local_slice]
309+
_distribute_replay_padding_indices(
310+
aligned_rollout_expert_indices,
311+
local_router_padding_mask,
312+
model_config.num_moe_experts,
313+
mpu.get_expert_model_parallel_world_size(),
314+
)
278315
RouterReplay.set_replay_data(_split_replay_indices(aligned_rollout_expert_indices))
279316
RouterReplay.set_global_router_replay_action(RouterReplayAction.REPLAY_FORWARD)
280317

tests/backends/skyrl_train/utils/test_replay_utils.py

Lines changed: 42 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ def parallel_state(monkeypatch):
3434
monkeypatch.setattr(mpu, "get_tensor_model_parallel_world_size", lambda: 1, raising=False)
3535
monkeypatch.setattr(mpu, "get_context_parallel_world_size", lambda: 1, raising=False)
3636
monkeypatch.setattr(mpu, "get_context_parallel_rank", lambda: 0, raising=False)
37+
monkeypatch.setattr(mpu, "get_expert_model_parallel_world_size", lambda: 1, raising=False)
3738
return mpu
3839

3940

@@ -93,7 +94,7 @@ def test_replay_has_no_dispatcher_specific_patch():
9394

9495

9596
@pytest.mark.parametrize("route_dtype", [torch.uint8, torch.int16, torch.int32])
96-
def test_setup_replay_installs_indices_and_returns_model_mask(monkeypatch, parallel_state, route_dtype):
97+
def test_setup_replay_installs_indices_in_model_order_and_returns_mask(monkeypatch, parallel_state, route_dtype):
9798
router_replay_module = types.ModuleType("megatron.core.transformer.moe.router_replay")
9899

99100
class RouterReplay:
@@ -131,19 +132,14 @@ def record_routed_layer_count(metadata, layout, padding_value):
131132

132133
monkeypatch.setattr(replay_utils, "align_token_metadata", record_routed_layer_count)
133134

134-
routes = torch.tensor(
135-
[
136-
[
137-
[[0, 1], [0, 1], [0, 1]],
138-
[[10, 11], [1, 2], [20, 21]],
139-
[[12, 13], [3, 4], [22, 23]],
140-
[[14, 15], [5, 6], [24, 25]],
141-
]
142-
],
143-
dtype=route_dtype,
135+
monkeypatch.setattr(parallel_state, "get_expert_model_parallel_world_size", lambda: 8)
136+
137+
routes = torch.arange(240, dtype=torch.int32).reshape(2, 5, 3, 8).to(route_dtype)
138+
attention_mask = torch.ones((2, 5), dtype=torch.long)
139+
router_padding_mask = torch.tensor(
140+
[[False, True, False, True, False], [True, False, True, False, True]],
141+
dtype=torch.bool,
144142
)
145-
attention_mask = torch.tensor([[0, 1, 1, 1]])
146-
router_padding_mask = torch.tensor([[1, 0, 0, 1]], dtype=torch.bool)
147143
metadata_layout = build_token_metadata_layout(
148144
attention_mask,
149145
routes.device,
@@ -156,16 +152,45 @@ def record_routed_layer_count(metadata, layout, padding_value):
156152
router_padding_mask,
157153
attention_mask,
158154
model=object(),
159-
model_config=SimpleNamespace(fp8=None),
155+
model_config=SimpleNamespace(fp8=None, num_moe_experts=384),
160156
metadata_layout=metadata_layout,
161157
)
162158

163-
assert RouterReplay.replay_data[0].tolist() == [[1, 2], [3, 4], [5, 6]]
159+
installed = RouterReplay.replay_data[0]
160+
flat_padding_mask = router_padding_mask.transpose(0, 1).flatten()
161+
expected_captured = torch.tensor(
162+
[
163+
list(range(8, 16)),
164+
list(range(152, 160)),
165+
list(range(56, 64)),
166+
list(range(200, 208)),
167+
list(range(104, 112)),
168+
],
169+
dtype=torch.int32,
170+
)
171+
assert torch.equal(installed[~flat_padding_mask], expected_captured)
172+
padding_routes = installed[flat_padding_mask]
173+
assignment_ordinals = torch.arange(40).reshape(5, 8)
174+
expected_padding_routes = (assignment_ordinals % 8) * 48 + assignment_ordinals // 8
175+
assert torch.equal(padding_routes, expected_padding_routes)
176+
assert padding_routes[0].tolist() == [0, 48, 96, 144, 192, 240, 288, 336]
177+
assert torch.all((padding_routes >= 0) & (padding_routes < 384))
178+
assert torch.all(padding_routes.sort(dim=1).values.diff(dim=1) > 0)
179+
for num_padding_rows in range(1, 6):
180+
expert_loads = torch.bincount(padding_routes[:num_padding_rows].flatten(), minlength=384)
181+
assert expert_loads.max() - expert_loads.min() <= 1
182+
assert torch.equal(
183+
expert_loads.reshape(8, 48).sum(dim=1),
184+
torch.full((8,), num_padding_rows, dtype=torch.long),
185+
)
164186
assert RouterReplay.replay_data[0].dtype == torch.int32
165187
assert RouterReplay.action == RouterReplayAction.REPLAY_FORWARD
166-
assert model_kwargs["padding_mask"].tolist() == [[False, False, True]]
188+
assert torch.equal(model_kwargs["padding_mask"], router_padding_mask)
167189
assert routed_layer_counts == [1]
168190

191+
packed_routes = torch.arange(10, dtype=torch.uint8).reshape(1, 5, 1, 2)
192+
assert torch.equal(replay_utils._split_replay_indices(packed_routes)[0], packed_routes[0, :, 0].to(torch.int32))
193+
169194

170195
@pytest.mark.parametrize("packed", [False, True])
171196
@pytest.mark.parametrize("tp_size", [1, 2])
@@ -219,7 +244,7 @@ def run(routes):
219244
router_padding_mask,
220245
attention_mask,
221246
model=object(),
222-
model_config=SimpleNamespace(fp8=None, sequence_parallel=False),
247+
model_config=SimpleNamespace(fp8=None, sequence_parallel=False, num_moe_experts=4096),
223248
metadata_layout=layout,
224249
remove_microbatch_padding=packed,
225250
)

0 commit comments

Comments
 (0)