Skip to content

Commit 3784013

Browse files
committed
Add fused MoE FFN1: fuse wi_0 and wi_1 into one grouped GEMM
When prefuse_moe_weights=True (requires sparse_matmul=True), the two FFN1 expert weight matrices [G,K,N] are concatenated into [G,K,2N] and dispatched as a single grouped GEMM, then split. This halves FFN1 kernel launches and reads input activations from HBM once instead of twice. Backend-agnostic: works with Megablox, Tokamax, and jax.lax.ragged_dot. When attention=vllm_rpa the fused tensor is passed directly to the vLLM-TPU serving kernel.
1 parent c7b9871 commit 3784013

4 files changed

Lines changed: 116 additions & 26 deletions

File tree

docs/reference/core_concepts/moe_configuration.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,8 @@ Dropping:
9999

100100
`mlp_bias`: If enabled, add learnable bias terms for MLP matmul. Originally implemented to support the GPT-OSS model architecture.
101101

102+
`prefuse_moe_weights`: If enabled alongside `sparse_matmul=True`, fuses the two FFN1 grouped GEMMs (wi\_0 and wi\_1) into a single grouped GEMM call. Expert weights are stored in a concatenated `(num_experts, embed_dim, 2 * mlp_dim)` shape, so input activations are loaded from HBM once per forward pass instead of twice. Backend-agnostic (works with Megablox, JAX Ragged Dot, and Tokamax). When used with `attention=vllm_rpa`, the fused weight tensor is passed directly to the vLLM-TPU serving kernel without splitting.
103+
102104
`use_batch_split_schedule` (experimental): If enabled, split batch into micro-batches to hide communications that yields performance benefits.
103105

104106
## 2. Sharding

src/maxtext/configs/types.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -813,7 +813,7 @@ class MoEGeneral(BaseModel):
813813
prefuse_moe_weights: bool = Field(
814814
False,
815815
description="Whether to pre-fuse MoE weights (w0 and w1) during initialization. "
816-
"This is useful for inference performance in vllm_rpa mode.",
816+
"This enables a single FFN1 grouped GEMM in sparse MoE paths and passes fused weights directly in vllm_rpa mode.",
817817
)
818818
fuse_expert_scales: bool = Field(
819819
False,

src/maxtext/layers/moe.py

Lines changed: 49 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -496,6 +496,12 @@ def __init__(
496496

497497
kernel_in_axis = np.arange(1)
498498
kernel_out_axis = np.arange(1, 2)
499+
# Pad the MoE input weight kernels for GMM_v2 execution when configured.
500+
moe_intermediate_dim = (
501+
self.config.padded_base_moe_mlp_dim
502+
if self.config.padded_base_moe_mlp_dim is not None
503+
else self.intermediate_dim
504+
)
499505

500506
if quantizations.in_serve_mode(self.quant):
501507
# During aqt convert state we delete kernel weight from params to save
@@ -504,13 +510,7 @@ def __init__(
504510
self.wi_0 = jnp.zeros((num_experts, self.moe_expert_input_dim, intermediate_dim))
505511
self.wi_1 = jnp.zeros((num_experts, self.moe_expert_input_dim, intermediate_dim))
506512
self.wo = jnp.zeros((num_experts, intermediate_dim, self.moe_expert_input_dim))
507-
elif self.config.prefuse_moe_weights and self.config.attention == "vllm_rpa":
508-
# Pad model dimension in Fused MoE weight kernels for GMM_v2 execution.
509-
moe_intermediate_dim = (
510-
self.config.padded_base_moe_mlp_dim
511-
if self.config.padded_base_moe_mlp_dim is not None
512-
else self.intermediate_dim
513-
)
513+
elif self.config.prefuse_moe_weights:
514514
self.wi = nnx.Param(
515515
self.kernel_init(
516516
self.rngs.params(),
@@ -532,12 +532,6 @@ def __init__(
532532
out_sharding=self.wo_kernel_axes,
533533
)
534534
else:
535-
# Pad model dimension in Unfused MoE weight kernels for GMM_v2 execution.
536-
moe_intermediate_dim = (
537-
self.config.padded_base_moe_mlp_dim
538-
if self.config.padded_base_moe_mlp_dim is not None
539-
else self.intermediate_dim
540-
)
541535
self.wi_0 = nnx.Param(
542536
self.kernel_init(
543537
self.rngs.params(),
@@ -1665,19 +1659,44 @@ def get_wo_gmm_params():
16651659
def gmm_up(x, w0, w1, w0_bias, w1_bias, gmm_fn, weight_gather):
16661660
"""Run the two up-projections (gate + up) and apply the FFN activation."""
16671661
wi_gather_axes, wi_tile_size = get_wi_gmm_params()
1668-
layer_w0 = gmm_fn(x, w0, tiling=wi_tile_size, weight_gather_axes=wi_gather_axes)
1669-
if self.get_tensor_transpose_parallelism_size() > 1:
1670-
layer_w0 = jax.lax.psum(layer_w0, "tensor_transpose")
1671-
if self.config.mlp_bias:
1672-
layer_w0 = layer_w0 + w0_bias
1673-
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
1662+
if self.config.prefuse_moe_weights:
1663+
# Weights are stored as (G,K,2N); w0/w1 are adjacent slices so XLA elides this concat.
1664+
w_fused = jnp.concatenate([w0, w1], axis=-1)
1665+
out = gmm_fn(x, w_fused, tiling=wi_tile_size, weight_gather_axes=wi_gather_axes)
1666+
n = out.shape[-1] // 2
1667+
layer_w0, layer_w1 = out[:, :n], out[:, n:]
1668+
if self.get_tensor_transpose_parallelism_size() > 1:
1669+
layer_w0 = jax.lax.psum(layer_w0, "tensor_transpose")
1670+
layer_w1 = jax.lax.psum(layer_w1, "tensor_transpose")
1671+
if self.config.mlp_bias:
1672+
layer_w0 = layer_w0 + w0_bias
1673+
layer_w1 = layer_w1 + w1_bias
1674+
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
1675+
layer_w1 = adc.checkpoint_name(layer_w1, "moe_mlpwi_1")
1676+
else:
1677+
layer_w0 = gmm_fn(
1678+
x,
1679+
w0,
1680+
tiling=wi_tile_size,
1681+
weight_gather_axes=wi_gather_axes,
1682+
)
1683+
if self.get_tensor_transpose_parallelism_size() > 1:
1684+
layer_w0 = jax.lax.psum(layer_w0, "tensor_transpose")
1685+
if self.config.mlp_bias:
1686+
layer_w0 = layer_w0 + w0_bias
1687+
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
16741688

1675-
layer_w1 = gmm_fn(x, w1, tiling=wi_tile_size, weight_gather_axes=wi_gather_axes)
1676-
if self.get_tensor_transpose_parallelism_size() > 1:
1677-
layer_w1 = jax.lax.psum(layer_w1, "tensor_transpose")
1678-
if self.config.mlp_bias:
1679-
layer_w1 = layer_w1 + w1_bias
1680-
layer_w1 = adc.checkpoint_name(layer_w1, "moe_mlpwi_1")
1689+
layer_w1 = gmm_fn(
1690+
x,
1691+
w1,
1692+
tiling=wi_tile_size,
1693+
weight_gather_axes=wi_gather_axes,
1694+
)
1695+
if self.get_tensor_transpose_parallelism_size() > 1:
1696+
layer_w1 = jax.lax.psum(layer_w1, "tensor_transpose")
1697+
if self.config.mlp_bias:
1698+
layer_w1 = layer_w1 + w1_bias
1699+
layer_w1 = adc.checkpoint_name(layer_w1, "moe_mlpwi_1")
16811700
return self.apply_ffn_activation(layer_w0, layer_w1)
16821701

16831702
def get_gmm_for_local_experts(x, routing, route_metadata):
@@ -2588,6 +2607,11 @@ def __call__(
25882607
w1_kernel = None
25892608
if cfg.prefuse_moe_weights and cfg.attention == "vllm_rpa" and not self.is_hash_routing:
25902609
fused_kernel = jnp.asarray(self.wi[...], self.dtype)
2610+
elif cfg.prefuse_moe_weights:
2611+
wi = jnp.asarray(self.wi[...], self.dtype)
2612+
n = wi.shape[-1] // 2
2613+
w0_kernel = wi[..., :n]
2614+
w1_kernel = wi[..., n:]
25912615
else:
25922616
w0_kernel = jnp.asarray(self.wi_0[...], self.dtype)
25932617
w1_kernel = jnp.asarray(self.wi_1[...], self.dtype)

tests/unit/moe_test.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1643,5 +1643,69 @@ def test_moe_dispatch_no_expert_sharding_dense_forward():
16431643
assert out.shape == inputs.shape
16441644

16451645

1646+
@pytest.mark.tpu_only
1647+
class FusedMlpMoETest(unittest.TestCase):
1648+
"""Tests that prefuse_moe_weights=True and prefuse_moe_weights=False produce identical outputs for MoE."""
1649+
1650+
def __init__(self, *args, **kwargs):
1651+
super().__init__(*args, **kwargs)
1652+
self._B = 1
1653+
self._S = 16
1654+
1655+
def setUp(self):
1656+
super().setUp()
1657+
self.rng = jax.random.PRNGKey(0)
1658+
self.ref_cfg = pyconfig.initialize(
1659+
[None, get_test_config_path()],
1660+
run_name="fused_mlp_moe_ref",
1661+
enable_checkpointing=False,
1662+
model_name="mixtral-8x7b",
1663+
dtype="bfloat16",
1664+
sparse_matmul=True,
1665+
megablox=True,
1666+
prefuse_moe_weights=False,
1667+
ici_expert_parallelism=jax.device_count(),
1668+
max_target_length=self._S,
1669+
per_device_batch_size=self._B,
1670+
)
1671+
ref_devices = maxtext_utils.create_device_mesh(self.ref_cfg)
1672+
self.ref_mesh = Mesh(ref_devices, self.ref_cfg.mesh_axes)
1673+
self.ref_model = make_moe(self.ref_cfg, self.ref_mesh)
1674+
1675+
def _inputs(self):
1676+
return jax.random.normal(self.rng, (self._B, self._S, self.ref_cfg.base_emb_dim), dtype=jnp.bfloat16)
1677+
1678+
def test_prefuse_moe_weights_matches_unfused(self):
1679+
"""prefuse_moe_weights=True output matches prefuse_moe_weights=False with sparse_matmul (Megablox)."""
1680+
fused_cfg = pyconfig.initialize(
1681+
[None, get_test_config_path()],
1682+
run_name="fused_mlp_moe_fused",
1683+
enable_checkpointing=False,
1684+
model_name="mixtral-8x7b",
1685+
dtype="bfloat16",
1686+
sparse_matmul=True,
1687+
megablox=True,
1688+
prefuse_moe_weights=True,
1689+
ici_expert_parallelism=jax.device_count(),
1690+
max_target_length=self._S,
1691+
per_device_batch_size=self._B,
1692+
)
1693+
fused_devices = maxtext_utils.create_device_mesh(fused_cfg)
1694+
fused_mesh = Mesh(fused_devices, fused_cfg.mesh_axes)
1695+
fused_model = make_moe(fused_cfg, fused_mesh)
1696+
copy_weights_prefused(self.ref_model, fused_model)
1697+
1698+
inputs = self._inputs()
1699+
ref_out, _, _ = self.ref_model(inputs)
1700+
fused_out, _, _ = fused_model(inputs)
1701+
1702+
np.testing.assert_allclose(
1703+
np.array(ref_out, dtype=np.float32),
1704+
np.array(fused_out, dtype=np.float32),
1705+
rtol=1e-2,
1706+
atol=1e-2,
1707+
)
1708+
1709+
16461710
if __name__ == "__main__":
16471711
unittest.main()

0 commit comments

Comments
 (0)