Skip to content

Commit dfd8d29

Browse files
Add chunked MoE support to MaxText.
It introduces the `num_moe_token_chunks` configuration to allow chunking of the MoE layer. Also includes `equiv_chunk_test.py` to verify that chunked MoE produces equivalent results to the unchunked implementation. - [x] I have tested these changes - [x] I have updated the documentation PiperOrigin-RevId: 950076344
1 parent 995eea2 commit dfd8d29

8 files changed

Lines changed: 506 additions & 58 deletions

File tree

src/maxtext/configs/base.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,8 @@ num_experts_per_tok: 1
213213
megablox: true
214214
sparse_matmul: true
215215
capacity_factor: -1.0 # a factor to decide expert capacity for token dropping, and no dropping by default
216+
num_moe_token_chunks: 1 # split routed-MoE tokens into N chunks (ring-of-experts path) so each chunk's EP all-gather / reduce-scatter overlaps the previous chunk's GMM compute. 1 = disabled (identical to baseline).
217+
moe_chunk_barrier: false # wraps its iteration with optimization_barrier to chain the chunked ring-of-experts MoE loop so each chunk's input is fenced on the previous chunk's output, forcing sequential (no-interleave) chunk execution. Math unchanged (barrier is identity), loss bit-exact. Needs num_moe_token_chunks>1 + use_ring_of_experts=True to matter.
216218
ragged_buffer_factor: -1.0 # a factor to determine the size of the ragged buffer for routed MoE activations.
217219
# By default (-1), the routed buffer is worst case size to ensure no dropping.
218220
# When set to 1.0 this buffer if set to the size assuming perfectly balanced. If the routing dictates

src/maxtext/configs/types.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -793,6 +793,30 @@ class MoEGeneral(BaseModel):
793793
-1.0,
794794
description="Ragged buffer factor. If < 0, ragged buffer is worst case size.",
795795
)
796+
num_moe_token_chunks: PositiveInt = Field(
797+
1,
798+
description=(
799+
"Number of token chunks for the ring-of-experts MoE pipeline. 1"
800+
" disables chunking (identical to baseline). >1 splits the per-shard"
801+
" tokens along the sequence dimension so each chunk's EP all-gather /"
802+
" reduce-scatter overlaps the previous chunk's GMM compute. Requires"
803+
" use_ring_of_experts=True."
804+
),
805+
)
806+
moe_chunk_barrier: bool = Field(
807+
False,
808+
description=(
809+
"Diagnostic (profiling, not production). When True, chain the chunked"
810+
" ring-of-experts MoE loop so each chunk's input is fenced with"
811+
" jax.lax.optimization_barrier on the previous chunk's output,"
812+
" forcing XLA to run the chunks sequentially (no interleave/fusion)."
813+
" Math is unchanged (barrier is identity), so loss stays bit-exact."
814+
" Used to test whether the token-AG/RS chunks overlap at all today."
815+
" Requires num_moe_token_chunks>1 and use_ring_of_experts=True to have any"
816+
" effect."
817+
),
818+
)
819+
796820
moe_expert_input_dim: int = Field(
797821
-1,
798822
description="Dimension of tokens entering the MoE layer. If < 0, defaults to emb_dim.",
@@ -894,6 +918,12 @@ class MoEGeneral(BaseModel):
894918
"This can improve inference performance.",
895919
)
896920

921+
@model_validator(mode="after")
922+
def validate_moe_chunks(self) -> "MoEGeneral":
923+
if self.num_moe_token_chunks > 1 and not self.use_ring_of_experts:
924+
raise ValueError("num_moe_token_chunks > 1 requires use_ring_of_experts=True.")
925+
return self
926+
897927

898928
class MoEKernels(BaseModel):
899929
"""Configuration for MoE-specific kernels like Megablox."""
@@ -3561,6 +3591,13 @@ def calculate_global_batch_sizes(per_device_batch_size, expansion_factor, num_de
35613591
if self.share_kv_projections:
35623592
raise ValueError("`num_kv_shared_layers > 0` is not compatible with `share_kv_projections`.")
35633593

3594+
if self.num_moe_token_chunks > 1:
3595+
if self.max_target_length % self.num_moe_token_chunks != 0:
3596+
raise ValueError(
3597+
f"num_moe_token_chunks={self.num_moe_token_chunks} must evenly divide "
3598+
f"max_target_length={self.max_target_length}."
3599+
)
3600+
35643601
# I. FINAL TYPE CONVERSIONS AND DERIVED LISTS
35653602
ici_map = {
35663603
"diloco": self.ici_diloco_parallelism,

src/maxtext/kernels/megablox/ops.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ def gmm(
6262
group_offset: jnp.ndarray | None = None,
6363
existing_out: jnp.ndarray | None = None,
6464
transpose_rhs: bool = False,
65-
interpret: bool = False,
65+
interpret: bool | None = None,
6666
lhs_quantize_dtype: Literal[jnp.int4, jnp.int8] | None = None, # pyrefly: ignore[invalid-literal]
6767
rhs_quantize_dtype: Literal[jnp.int4, jnp.int8] | None = None, # pyrefly: ignore[invalid-literal]
6868
use_qwix_quantization: bool = False,
@@ -77,6 +77,13 @@ def gmm(
7777
partial_sum: jnp.ndarray | None = None,
7878
):
7979
"""Grouped matrix multiplication operation."""
80+
if interpret is None:
81+
# Default to native (TPU) lowering. `jax.devices()[0]` is NOT the compile TARGET:
82+
# during train_compile the local backend is CPU (JAX_PLATFORMS=cpu) while the mesh
83+
# targets tpu7x, and interpret-mode there breaks check_vma (exposes the kernel's
84+
# internal dynamic_slice VMA) and balloons HBM temporaries. Callers that genuinely
85+
# run off-TPU (e.g. equiv_chunk_test) pass interpret based on their target mesh.
86+
interpret = False
8087
quantization_rule = None
8188
if use_qwix_quantization:
8289
# get_current_rule has to be called outside of the _gmm_fwd function.

src/maxtext/kernels/ragged/ragged_gather.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -396,9 +396,14 @@ def ragged_gather(
396396

397397
dtype = x.dtype
398398

399+
# Guard against eager initialization on non-TPU hardware (e.g. during CPU tests).
400+
# pltpu.get_tpu_info() expects TPU hardware and will crash if executed on CPU.
401+
if enforce_fallback or jax.devices()[0].platform != "tpu":
402+
return _fallback_implementation(x, indices, weights, has_weights)
403+
399404
sc_info = pltpu.get_tpu_info().sparse_core
400-
if sc_info is None or enforce_fallback:
401-
# Sparse core is not available or fallback is enforced. Use JAX reference.
405+
if sc_info is None:
406+
# Sparse core is not available. Use JAX reference.
402407
return _fallback_implementation(x, indices, weights, has_weights)
403408

404409
hidden_size = x.shape[-1]

src/maxtext/kernels/ragged/ragged_gather_reduce.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -457,9 +457,14 @@ def ragged_gather_reduce(
457457
assert topk_weights.ndim == 1, "ragged_gather_reduce only supports 1d topk_weights."
458458
assert valid_rows_mask.ndim == 1, "ragged_gather_reduce only supports 1d valid_rows_mask."
459459

460+
# Guard against eager initialization on non-TPU hardware (e.g. during CPU tests).
461+
# pltpu.get_tpu_info() expects TPU hardware and will crash if executed on CPU.
462+
if enforce_fallback or jax.devices()[0].platform != "tpu":
463+
return _fallback_implementation(x, indices, topk_weights, valid_rows_mask, reduce_group_size)
464+
460465
sc_info = pltpu.get_tpu_info().sparse_core
461-
if sc_info is None or enforce_fallback:
462-
# Sparse core is not available or fallback is enforced. Use JAX reference.
466+
if sc_info is None:
467+
# Sparse core is not available. Use JAX reference.
463468
return _fallback_implementation(x, indices, topk_weights, valid_rows_mask, reduce_group_size)
464469

465470
# Heuristic threshold on whether to fallback for small inputs.

src/maxtext/kernels/ragged/ragged_gather_reduce_v2.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,13 @@ def ragged_gather_reduce(
650650
Reduced output, ``(input_size // reduce_group_size, hidden_size)``.
651651
"""
652652
# Step 1: Choose the implementation (TensorCore fallback or SparseCore).
653+
# Guard against eager initialization on non-TPU hardware (e.g. during CPU tests).
654+
# pltpu.get_tpu_info() expects TPU hardware and will crash if executed on CPU.
655+
if enforce_fallback or jax.devices()[0].platform != "tpu":
656+
return _fallback_implementation(x, indices, topk_weights, valid_rows_mask, reduce_group_size)
657+
653658
sc_info = pltpu.get_tpu_info().sparse_core
654-
if sc_info is None or enforce_fallback:
659+
if sc_info is None:
655660
return _fallback_implementation(x, indices, topk_weights, valid_rows_mask, reduce_group_size)
656661

657662
# For a small {input + output} both likely fit in TensorCore VMEM, where a

0 commit comments

Comments
 (0)