Skip to content

Commit ea41360

Browse files
Make MoE dispatch/MLP expert-axis batch sharding configurable
The dispatch/MLP MoE activations are already expert-sharded via activation_exp. Since AI-Hypercomputer#4007, their batch dim also maps to activation_batch_moe, which includes 'expert'. Under single-node expert parallelism (ici_expert_parallelism=-1) this double-maps two tensor dims onto the 'expert' mesh axis, so GSPMD falls back from expert-parallel AllToAll to FSDP-style AllGather+ReduceScatter, regressing throughput for few-large-expert models (e.g. Mixtral-8x7b: ~7.4k -> ~10.9k tok/s/device at bs=11 on 8x MI355X). Add a config flag moe_dispatch_no_expert_sharding (default false) that selects a new activation_batch_no_exp rule ([data, fsdp, fsdp_transpose], no 'expert') for the training dispatch/MLP batch axis. Enable it for mixtral-8x7b. Default-false keeps every other model and all TPU/non-EP paths byte-identical; the flag only changes sharding when the 'expert' mesh axis size > 1. Enable moe_dispatch_no_expert_sharding for mixtral-8x22b. Same 8-large-expert geometry as 8x7b, so it benefits from the same expert-parallel MoE dispatch/MLP sharding. Add MoE dispatch expert-sharding regression test. Asserts that with moe_dispatch_no_expert_sharding the expert dim is sharded by 'expert' and the batch dim is not, guarding the expert-parallel dispatch/MLP sharding. Address review: peel 'expert' from MoE dispatch/MLP batch dim instead of a new logical rule Replace the activation_batch_no_exp logical rule with a remove_expert_from_partition_spec util (mirrors remove_fsdp_sharding), applied at the training dispatch/MLP sites when moe_dispatch_no_expert_sharding is set. Avoids a logical name that every custom_mesh_and_rule set would have to redefine. Same result (verified on 8xMI355X): Mixtral stays expert-parallel (a2a=5, ~11k tok/s/device), DeepSeek unchanged (a2a=0, ~17.8k). Test both flag states for MoE dispatch sharding; drop deepseek3-671b Address PR comments (make regression fix opt-in, not automatic) Add unit coverage for MoE expert-peel sharding
1 parent 85690da commit ea41360

5 files changed

Lines changed: 146 additions & 13 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,10 @@ load_balance_loss_weight: 0.0 # weight for the load balance loss
218218
use_random_routing: false # whether to use random routing for debug/test purpose
219219
use_custom_sort_vjp: true # whether to use a custom VJP sort for efficient backward pass processing in sparse matmul
220220
use_ring_of_experts: false # whether to use ring of experts for sparse matmul expert parallelism
221+
# If true, peel the 'expert' mesh axis off the MoE dispatch/MLP batch dim so the expert GEMM
222+
# stays expert-parallel (AllToAll); false keeps 'expert' on the batch dim (activation_batch_moe).
223+
# Only affects the dense (dense_matmul) MoE path; the sparse (shard_map) path is unaffected.
224+
moe_dispatch_no_expert_sharding: false
221225
use_ragged_sort: false # whether to use the Pallas ragged-sort kernels in the MoE permute path; valid both with and
222226
# without `use_ring_of_experts` (with EP > 1). When `use_ring_of_experts=True` the kernels run
223227
# inside `permute`/`unpermute`; otherwise they run inside `local_permute`/local-unpermute.

src/maxtext/configs/types.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -743,6 +743,13 @@ class MoEGeneral(BaseModel):
743743
False,
744744
description="Whether to use Ring of Experts for sparse matmul expert parallelism.",
745745
)
746+
moe_dispatch_no_expert_sharding: bool = Field(
747+
False,
748+
description=(
749+
"If true, shard the MoE dispatch/MLP batch dim without 'expert' so the expert GEMM "
750+
"stays expert-parallel (AllToAll); false keeps 'expert' on it (activation_batch_moe)."
751+
),
752+
)
746753
use_ragged_sort: bool = Field(
747754
False, description="Whether to use ragged kernel for sorting, improve performance when EP is enabled."
748755
)

src/maxtext/layers/moe.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@
4343
from maxtext.utils import max_utils
4444
from maxtext.utils import maxtext_utils
4545
from maxtext.utils.sharding import create_sharding, maybe_shard_with_logical, maybe_shard_with_pspec
46-
from maxtext.utils.sharding import logical_to_mesh_axes
46+
from maxtext.utils.sharding import logical_to_mesh_axes, remove_expert_from_partition_spec
4747
import numpy as np
4848
import qwix
4949
from qwix.contrib.sparsity import sparsity_module
@@ -624,6 +624,18 @@ def _maybe_shard_with_pspec(self, inputs, pspec: jax.sharding.PartitionSpec | No
624624
extra_stack_level=1,
625625
)
626626

627+
def _maybe_shard_moe_dispatch(self, inputs, logical_axis, peel_expert):
628+
"""Shard a MoE dispatch/MLP activation. When `peel_expert` is set, drop the 'expert'
629+
mesh axis from the batch dim (index 1) so the GEMM stays expert-parallel (AllToAll)
630+
instead of double-mapping E and B onto 'expert'. Each logical dim is resolved
631+
independently so the shared 'expert' axis is not deduped off the expert dim before
632+
the peel."""
633+
if not peel_expert:
634+
return self._maybe_shard_with_logical(inputs, logical_axis)
635+
spec = [None if name is None else self._logical_to_mesh_axes((name,))[0] for name in logical_axis]
636+
pspec = remove_expert_from_partition_spec(jax.sharding.PartitionSpec(*spec), dims_to_peel=(1,))
637+
return self._maybe_shard_with_pspec(inputs, pspec)
638+
627639
def get_expert_parallelism_size(self):
628640
# When expert parallelism has more than one physical axes, take product of their shapes
629641
if isinstance(self._expert_parallelism_name, tuple):
@@ -2173,12 +2185,18 @@ def dense_matmul(
21732185

21742186
if self.config.capacity_factor > 0:
21752187
# token dropping if needed
2188+
moe_peel_expert = False # only the training dispatch/MLP path peels 'expert' from the batch dim
21762189
if self.config.model_call_mode != "inference":
21772190
# TODO(b/425930949): remove this pylint by refactoring the logic here.
21782191
dispatch_mask, combine_mask = self.generate_masks(
21792192
top_k_indices, weights # pylint: disable=undefined-variable,possibly-used-before-assignment
21802193
)
21812194
mask_axes = ("activation_batch_moe", "activation_norm_length_moe", None, None)
2195+
# Dispatch/MLP are already expert-sharded via "activation_exp". With
2196+
# moe_dispatch_no_expert_sharding we peel 'expert' off the batch dim of these specs
2197+
# (see _maybe_shard_moe_dispatch) so the GEMM stays expert-parallel (AllToAll) instead
2198+
# of double-mapping E and B onto 'expert' (FSDP-style fallback).
2199+
moe_peel_expert = self.config.moe_dispatch_no_expert_sharding
21822200
dispatch_axis = (
21832201
"activation_exp",
21842202
"activation_batch_moe",
@@ -2283,10 +2301,7 @@ def dense_matmul(
22832301
"activation_embed_moe",
22842302
),
22852303
)
2286-
dispatch = self._maybe_shard_with_logical(
2287-
dispatch,
2288-
dispatch_axis,
2289-
)
2304+
dispatch = self._maybe_shard_moe_dispatch(dispatch, dispatch_axis, moe_peel_expert)
22902305
with jax.named_scope("wi_0"):
22912306
w0_kernel_axes = ("exp", None, "mlp")
22922307
w0_kernel = self.maybe_all_gather_kernel_weight_in_expert_parallelism(w0_kernel, w0_kernel_axes)
@@ -2299,10 +2314,7 @@ def dense_matmul(
22992314

23002315
if self.config.activations_in_float32:
23012316
layer_w0 = layer_w0.astype(jnp.float32)
2302-
layer_w0 = self._maybe_shard_with_logical(
2303-
layer_w0,
2304-
mlp_axis,
2305-
)
2317+
layer_w0 = self._maybe_shard_moe_dispatch(layer_w0, mlp_axis, moe_peel_expert)
23062318
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
23072319
with jax.named_scope("wi_1"):
23082320
w1_kernel_axes = ("exp", None, "mlp")
@@ -2315,10 +2327,7 @@ def dense_matmul(
23152327
layer_w1 = layer_w1 + w1_bias
23162328
if self.config.activations_in_float32:
23172329
layer_w1 = layer_w1.astype(jnp.float32)
2318-
layer_w1 = self._maybe_shard_with_logical(
2319-
layer_w1,
2320-
mlp_axis,
2321-
)
2330+
layer_w1 = self._maybe_shard_moe_dispatch(layer_w1, mlp_axis, moe_peel_expert)
23222331
layer_w1 = adc.checkpoint_name(adc.checkpoint_name(layer_w1, "mlpwi_1"), "moe_mlpwi_1")
23232332
layer_multiply = self.apply_ffn_activation(layer_w0, layer_w1)
23242333
with jax.named_scope("wo"):

src/maxtext/utils/sharding.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -732,6 +732,32 @@ def _remove_fsdp_from_partition_spec(named_sharding):
732732
return jax.tree.map(_remove_fsdp_from_partition_spec, sharding_tree)
733733

734734

735+
def remove_expert_from_partition_spec(pspec, dims_to_peel):
736+
"""Return `pspec` with the 'expert' mesh axis removed from the given dim indices.
737+
738+
Used by the MoE dispatch/MLP sharding: the expert dim is already sharded over the
739+
'expert' mesh axis via the `activation_exp` rule, so the batch dim must not also map
740+
to 'expert' (that double-maps two tensor dims onto one mesh axis and makes GSPMD fall
741+
back to FSDP-style AllGather+ReduceScatter instead of expert-parallel AllToAll). Only
742+
the dims listed in `dims_to_peel` (the batch dim) are modified; the expert dim is left
743+
untouched. Avoids needing a separate `activation_batch_no_exp` logical rule that every
744+
`custom_mesh_and_rule` set would have to redefine.
745+
"""
746+
new_spec = list(pspec)
747+
for i in dims_to_peel:
748+
axis = new_spec[i]
749+
if axis is None:
750+
continue
751+
if isinstance(axis, str):
752+
new_spec[i] = None if axis == "expert" else axis
753+
elif isinstance(axis, (list, tuple)):
754+
filtered = tuple(a for a in axis if a != "expert")
755+
new_spec[i] = filtered or None
756+
else:
757+
raise ValueError(f"Unsupported axis type: {type(axis)}")
758+
return jax.sharding.PartitionSpec(*new_spec)
759+
760+
735761
def get_physical_spec_no_fsdp(full_logical, mesh, logical_axis_rules):
736762
"""
737763
Generates a physical sharding spec for fully replicated weights.

tests/unit/moe_test.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from maxtext.layers.initializers import NdInitializer, nd_dense_init, variable_to_logically_partitioned
3131
from maxtext.layers.quantizations import Fp8Quantization
3232
from maxtext.utils import maxtext_utils
33+
from maxtext.utils.sharding import remove_expert_from_partition_spec
3334
from tests.utils.test_helpers import get_test_config_path
3435
import pytest
3536

@@ -1534,5 +1535,91 @@ def test_prefused_vs_sparse_softmax(self):
15341535
self.assertIsNone(bias_updates)
15351536

15361537

1538+
@pytest.mark.parametrize(
1539+
"model_name,flag",
1540+
[
1541+
("mixtral-8x7b", True), # flag on: expert stays on E, peeled off the batch dim
1542+
("mixtral-8x22b", True), # flag on
1543+
("mixtral-8x7b", False), # flag off (default): batch dim keeps 'expert'
1544+
],
1545+
)
1546+
def test_moe_dispatch_keeps_expert_on_expert_dim(model_name, flag):
1547+
"""Regression guard for the MoE dispatch/MLP expert-parallel sharding.
1548+
1549+
The expert (E) dim is always sharded by the 'expert' mesh axis (via activation_exp).
1550+
With moe_dispatch_no_expert_sharding the batch (B) dim must NOT also take 'expert'
1551+
(which would double-map two tensor dims onto one mesh axis and force an FSDP-style
1552+
fallback instead of expert-parallel AllToAll); with the flag off, the default keeps
1553+
'expert' on the batch dim. Mirrors dense_matmul's axis selection.
1554+
"""
1555+
cfg = pyconfig.initialize(
1556+
[None, get_test_config_path()],
1557+
run_name=f"moe_shard_{model_name}_{flag}",
1558+
enable_checkpointing=False,
1559+
model_name=model_name,
1560+
moe_dispatch_no_expert_sharding=flag,
1561+
)
1562+
rules = cfg.logical_axis_rules
1563+
1564+
def _as_set(entry):
1565+
if entry is None:
1566+
return set()
1567+
return {entry} if isinstance(entry, str) else set(entry)
1568+
1569+
# Mirror _maybe_shard_moe_dispatch: resolve E and batch dims independently (so the shared
1570+
# 'expert' axis isn't deduped off E), then peel 'expert' from the batch dim when the flag is set.
1571+
e_spec = nn_partitioning.logical_to_mesh_axes(("activation_exp",), rules=rules)
1572+
b_spec = nn_partitioning.logical_to_mesh_axes(("activation_batch_moe",), rules=rules)
1573+
if cfg.moe_dispatch_no_expert_sharding:
1574+
b_spec = remove_expert_from_partition_spec(b_spec, dims_to_peel=(0,))
1575+
1576+
e_axes, b_axes = _as_set(e_spec[0]), _as_set(b_spec[0])
1577+
assert "expert" in e_axes, "expert dim must be sharded by the 'expert' mesh axis"
1578+
if cfg.moe_dispatch_no_expert_sharding:
1579+
assert "expert" not in b_axes, "flag on: the batch dim must not take 'expert'"
1580+
else:
1581+
assert "expert" in b_axes, "flag off (default): the batch dim keeps 'expert' (activation_batch_moe)"
1582+
1583+
1584+
def test_remove_expert_from_partition_spec():
1585+
"""remove_expert_from_partition_spec peels 'expert' only from the requested dims."""
1586+
spec = jax.sharding.PartitionSpec
1587+
assert remove_expert_from_partition_spec(spec("expert", ("data", "fsdp", "expert"), None), dims_to_peel=(1,)) == spec(
1588+
"expert", ("data", "fsdp"), None
1589+
)
1590+
assert remove_expert_from_partition_spec(spec("expert", "expert", None), dims_to_peel=(1,)) == spec(
1591+
"expert", None, None
1592+
)
1593+
assert remove_expert_from_partition_spec(spec(("expert",), None), dims_to_peel=(0, 1)) == spec(None, None)
1594+
1595+
1596+
def test_moe_dispatch_no_expert_sharding_dense_forward():
1597+
"""The moe_dispatch_no_expert_sharding peel path runs in dense_matmul (capacity_factor>0)."""
1598+
cfg = pyconfig.initialize(
1599+
[None, get_test_config_path()],
1600+
run_name="moe_dense_no_exp_fwd",
1601+
enable_checkpointing=False,
1602+
model_name="mixtral-8x7b",
1603+
dtype="bfloat16",
1604+
megablox=False,
1605+
sparse_matmul=False,
1606+
capacity_factor=1.0,
1607+
moe_dispatch_no_expert_sharding=True,
1608+
per_device_batch_size=1,
1609+
max_target_length=16,
1610+
)
1611+
devices = maxtext_utils.create_device_mesh(cfg)
1612+
mesh = Mesh(devices, cfg.mesh_axes)
1613+
model = make_moe(cfg, mesh)
1614+
inputs = jax.random.normal(
1615+
jax.random.PRNGKey(0),
1616+
(int(cfg.per_device_batch_size) * jax.device_count(), cfg.max_target_length, cfg.base_emb_dim),
1617+
dtype=jnp.bfloat16,
1618+
)
1619+
with mesh:
1620+
out, _, _ = model(inputs)
1621+
assert out.shape == inputs.shape
1622+
1623+
15371624
if __name__ == "__main__":
15381625
unittest.main()

0 commit comments

Comments
 (0)