Skip to content

Commit 7a3c6ce

Browse files
RissyRanGoogle-ML-Automation
authored andcommitted
Fix ragged all-to-all with ragged buffer factor in DeepSeek-V3.
- Implement traffic matrix truncation in forward/backward all-to-all passes when `ragged_buffer_factor > 0` to prevent out-of-bounds writes in `ragged_all_to_all`. - Add unit tests for new functions. PiperOrigin-RevId: 948020403
1 parent 19fbc5e commit 7a3c6ce

1 file changed

Lines changed: 161 additions & 32 deletions

File tree

src/maxtext/layers/moe.py

Lines changed: 161 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,26 @@ class RouteOutput:
9292
local_group_sizes: Optional[jax.Array] = None
9393

9494

95+
def _truncate_matrix(all_shards_group_sizes: jax.Array, buffer_size: int) -> jax.Array:
96+
"""Truncates the traffic matrix to fit in buffer_size on receiver side.
97+
98+
When ragged_buffer_factor > 0, the receiver buffer has a fixed capacity
99+
(buffer_size). Due to routing imbalance, some shards might receive more tokens
100+
than this capacity. We use a prefix sum to deterministically truncate the
101+
received tokens on all shards, ensuring we don't write out of bounds.
102+
"""
103+
cumsum = jnp.cumsum(all_shards_group_sizes, axis=0)
104+
clamped_cumsum = jnp.minimum(cumsum, buffer_size)
105+
clamped_cumsum_extended = jnp.concatenate(
106+
[
107+
jnp.zeros((1, all_shards_group_sizes.shape[1]), dtype=clamped_cumsum.dtype),
108+
clamped_cumsum,
109+
],
110+
axis=0,
111+
)
112+
return jnp.diff(clamped_cumsum_extended, axis=0)
113+
114+
95115
def _sort_activations(
96116
inputs: jax.Array,
97117
sort_indices: jax.Array,
@@ -985,6 +1005,31 @@ def unpermute(
9851005
)
9861006
return output.reshape(batch_size, sequence_length, -1).astype(self.dtype)
9871007

1008+
@staticmethod
1009+
def _maybe_truncate_local_group_size(
1010+
all_shard_local_sizes: jax.Array,
1011+
buffer_size: int,
1012+
ragged_buffer_factor: float,
1013+
) -> jax.Array:
1014+
"""Optionally truncates the local group sizes if ragged_buffer_factor > 0.0.
1015+
1016+
When ragged_buffer_factor > 0, the receiver buffer has a fixed capacity
1017+
(buffer_size). Due to routing imbalance, some shards might receive more
1018+
tokens
1019+
than this capacity. We use a prefix sum to deterministically truncate the
1020+
received tokens on all shards, ensuring we don't write out of bounds.
1021+
"""
1022+
if ragged_buffer_factor > 0.0:
1023+
flat_sizes = all_shard_local_sizes.reshape(-1)
1024+
cumsum = jnp.cumsum(flat_sizes)
1025+
clamped_cumsum = jnp.minimum(cumsum, buffer_size)
1026+
clamped_cumsum_extended = jnp.concatenate([jnp.zeros((1,), dtype=clamped_cumsum.dtype), clamped_cumsum])
1027+
truncated_flat_sizes = jnp.diff(clamped_cumsum_extended)
1028+
truncated_all_shard_local_sizes = truncated_flat_sizes.reshape(all_shard_local_sizes.shape)
1029+
return jnp.sum(truncated_all_shard_local_sizes, axis=0)
1030+
else:
1031+
return jnp.sum(all_shard_local_sizes, axis=0)
1032+
9881033
@staticmethod
9891034
def local_permute(
9901035
inputs,
@@ -995,6 +1040,7 @@ def local_permute(
9951040
global_sorted_experts=None,
9961041
use_custom_sort_vjp=True,
9971042
use_ragged_sort=False,
1043+
ragged_buffer_factor=-1.0,
9981044
):
9991045
"""Permutes tokens locally within an expert shard.
10001046
@@ -1047,7 +1093,9 @@ def local_permute(
10471093
# Total count of the local expert IDs is the sum of the counts across all
10481094
# batch shards, since all batch shards will send their contributions to the
10491095
# current expert shard.
1050-
local_group_size = jnp.sum(all_shard_local_sizes, axis=0)
1096+
local_group_size = RoutedMoE._maybe_truncate_local_group_size(
1097+
all_shard_local_sizes, inputs.shape[0], ragged_buffer_factor
1098+
)
10511099

10521100
# In this case, the data that needs to be processed by the local shard
10531101
# does not start from row 0 but actually starts at
@@ -1093,6 +1141,9 @@ def get_all_to_all_params(
10931141
shard_id,
10941142
num_expert_parallelism,
10951143
is_batch_sharded=True,
1144+
ragged_buffer_factor=-1.0,
1145+
buffer_size=None,
1146+
is_dispatch=True,
10961147
):
10971148
"""Generates input offsets, send sizes, output offsets, and receive sizes used for ragged_all_to_all."""
10981149

@@ -1151,30 +1202,99 @@ def transform_array(input_array, shard_id, strategy, is_batch_sharded):
11511202
else:
11521203
raise ValueError(f"Unknown transform array strategy: {strategy}")
11531204

1154-
input_offsets = transform_array(
1155-
all_shards_group_sizes,
1156-
shard_id,
1157-
TransformStrategy.INPUT_OFFSET,
1158-
is_batch_sharded,
1159-
)
1160-
send_sizes = transform_array(
1161-
all_shards_group_sizes,
1162-
shard_id,
1163-
TransformStrategy.SEND_SIZE,
1164-
is_batch_sharded,
1165-
)
1166-
output_offsets = transform_array(
1167-
all_shards_group_sizes,
1168-
shard_id,
1169-
TransformStrategy.OUTPUT_OFFSET,
1170-
is_batch_sharded,
1171-
)
1172-
recv_sizes = transform_array(
1173-
all_shards_group_sizes,
1174-
shard_id,
1175-
TransformStrategy.RECV_SIZE,
1176-
is_batch_sharded,
1177-
)
1205+
if ragged_buffer_factor > 0.0:
1206+
assert buffer_size is not None
1207+
truncated_all_shards_group_sizes = _truncate_matrix(all_shards_group_sizes, buffer_size)
1208+
1209+
if is_dispatch:
1210+
# For input_offsets, we use the untruncated group sizes because the
1211+
# sender's buffer still contains all tokens (including dropped ones).
1212+
input_offsets = transform_array(
1213+
all_shards_group_sizes,
1214+
shard_id,
1215+
TransformStrategy.INPUT_OFFSET,
1216+
is_batch_sharded,
1217+
)
1218+
# For send/recv sizes and output_offsets, we use truncated group sizes
1219+
# to ensure we don't write out of bounds of the receiver's capacity.
1220+
send_sizes = transform_array(
1221+
truncated_all_shards_group_sizes,
1222+
shard_id,
1223+
TransformStrategy.SEND_SIZE,
1224+
is_batch_sharded,
1225+
)
1226+
output_offsets = transform_array(
1227+
truncated_all_shards_group_sizes,
1228+
shard_id,
1229+
TransformStrategy.OUTPUT_OFFSET,
1230+
is_batch_sharded,
1231+
)
1232+
recv_sizes = transform_array(
1233+
truncated_all_shards_group_sizes,
1234+
shard_id,
1235+
TransformStrategy.RECV_SIZE,
1236+
is_batch_sharded,
1237+
)
1238+
else:
1239+
transposed_all_shards = jnp.transpose(all_shards_group_sizes)
1240+
transposed_truncated = jnp.transpose(truncated_all_shards_group_sizes)
1241+
1242+
# In combine stage, the roles are reversed:
1243+
# input_offsets/sizes and recv_sizes use truncated parameters because
1244+
# the combine sender buffer (dispatch receiver buffer) is packed.
1245+
input_offsets = transform_array(
1246+
transposed_truncated,
1247+
shard_id,
1248+
TransformStrategy.INPUT_OFFSET,
1249+
is_batch_sharded,
1250+
)
1251+
send_sizes = transform_array(
1252+
transposed_truncated,
1253+
shard_id,
1254+
TransformStrategy.SEND_SIZE,
1255+
is_batch_sharded,
1256+
)
1257+
# output_offsets use untruncated parameters because we write back
1258+
# to their original untruncated positions.
1259+
output_offsets = transform_array(
1260+
transposed_all_shards,
1261+
shard_id,
1262+
TransformStrategy.OUTPUT_OFFSET,
1263+
is_batch_sharded,
1264+
)
1265+
recv_sizes = transform_array(
1266+
transposed_truncated,
1267+
shard_id,
1268+
TransformStrategy.RECV_SIZE,
1269+
is_batch_sharded,
1270+
)
1271+
else:
1272+
matrix = all_shards_group_sizes if is_dispatch else jnp.transpose(all_shards_group_sizes)
1273+
input_offsets = transform_array(
1274+
matrix,
1275+
shard_id,
1276+
TransformStrategy.INPUT_OFFSET,
1277+
is_batch_sharded,
1278+
)
1279+
send_sizes = transform_array(
1280+
matrix,
1281+
shard_id,
1282+
TransformStrategy.SEND_SIZE,
1283+
is_batch_sharded,
1284+
)
1285+
output_offsets = transform_array(
1286+
matrix,
1287+
shard_id,
1288+
TransformStrategy.OUTPUT_OFFSET,
1289+
is_batch_sharded,
1290+
)
1291+
recv_sizes = transform_array(
1292+
matrix,
1293+
shard_id,
1294+
TransformStrategy.RECV_SIZE,
1295+
is_batch_sharded,
1296+
)
1297+
11781298
return input_offsets, send_sizes, output_offsets, recv_sizes
11791299

11801300
def transform_bias(self, experts_index, *biases):
@@ -1549,19 +1669,21 @@ def ra2a_and_route(x, logits, pre_bias_logits, num_ep, expert_shard_id, rngs, in
15491669

15501670
if is_batch_sharded_by_expert:
15511671
all_shards_group_sizes = jax.lax.all_gather(reshaped_group_sizes, axis_name=batch_axis)
1552-
input_offsets, send_sizes, output_offsets, recv_sizes = RoutedMoE.get_all_to_all_params(
1553-
all_shards_group_sizes,
1554-
expert_shard_id,
1555-
num_ep,
1556-
)
1557-
15581672
buffer_size = self.get_ragged_buffer_size(
15591673
jnp.shape(x)[0],
15601674
num_ep,
15611675
self.config.num_experts,
15621676
self.config.num_experts_per_tok,
15631677
self.config.ragged_buffer_factor,
15641678
)
1679+
input_offsets, send_sizes, output_offsets, recv_sizes = RoutedMoE.get_all_to_all_params(
1680+
all_shards_group_sizes,
1681+
expert_shard_id,
1682+
num_ep,
1683+
ragged_buffer_factor=self.config.ragged_buffer_factor,
1684+
buffer_size=buffer_size,
1685+
)
1686+
15651687
output_shape = jax.lax.empty((buffer_size, self.moe_expert_input_dim), dtype=x.dtype)
15661688

15671689
x = jax.lax.ragged_all_to_all(
@@ -1581,6 +1703,7 @@ def ra2a_and_route(x, logits, pre_bias_logits, num_ep, expert_shard_id, rngs, in
15811703
shard_index=expert_shard_id,
15821704
use_custom_sort_vjp=self.config.use_custom_sort_vjp,
15831705
use_ragged_sort=self.config.use_ragged_sort,
1706+
ragged_buffer_factor=self.config.ragged_buffer_factor,
15841707
)
15851708
else:
15861709
x, local_sorted_indices, group_sizes, selected_experts = RoutedMoE.local_permute(
@@ -1592,6 +1715,7 @@ def ra2a_and_route(x, logits, pre_bias_logits, num_ep, expert_shard_id, rngs, in
15921715
global_sorted_experts=selected_experts,
15931716
use_custom_sort_vjp=self.config.use_custom_sort_vjp,
15941717
use_ragged_sort=self.config.use_ragged_sort,
1718+
ragged_buffer_factor=self.config.ragged_buffer_factor,
15951719
)
15961720

15971721
return (
@@ -1779,10 +1903,14 @@ def unsort_output_and_ra2a(intermediate_output, routing, route_metadata, output_
17791903
self.config.use_custom_sort_vjp,
17801904
)
17811905

1906+
buffer_size = intermediate_output.shape[0]
17821907
input_offsets, send_sizes, output_offsets, recv_sizes = RoutedMoE.get_all_to_all_params(
1783-
jnp.transpose(route_metadata.all_shards_group_sizes),
1908+
route_metadata.all_shards_group_sizes,
17841909
route_metadata.expert_shard_id,
17851910
self.get_expert_parallelism_size(),
1911+
ragged_buffer_factor=self.config.ragged_buffer_factor,
1912+
buffer_size=buffer_size,
1913+
is_dispatch=False,
17861914
)
17871915
return jax.lax.ragged_all_to_all(
17881916
local_output,
@@ -1802,6 +1930,7 @@ def unsort_output_and_ra2a(intermediate_output, routing, route_metadata, output_
18021930
route_metadata.expert_shard_id,
18031931
self.get_expert_parallelism_size(),
18041932
is_batch_sharded=False,
1933+
is_dispatch=False,
18051934
)
18061935
return jax.lax.ragged_all_to_all(
18071936
intermediate_output,

0 commit comments

Comments
 (0)