Skip to content

Commit f2917ee

Browse files
Merge pull request #3736 from abhinavgoel95:abgoel/fused-moe-mlp
PiperOrigin-RevId: 939895106
2 parents 3790645 + 3784013 commit f2917ee

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
@@ -845,7 +845,7 @@ class MoEGeneral(BaseModel):
845845
prefuse_moe_weights: bool = Field(
846846
False,
847847
description="Whether to pre-fuse MoE weights (w0 and w1) during initialization. "
848-
"This is useful for inference performance in vllm_rpa mode.",
848+
"This enables a single FFN1 grouped GEMM in sparse MoE paths and passes fused weights directly in vllm_rpa mode.",
849849
)
850850
fuse_expert_scales: bool = Field(
851851
False,

src/maxtext/layers/moe.py

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

500500
kernel_in_axis = np.arange(1)
501501
kernel_out_axis = np.arange(1, 2)
502+
# Pad the MoE input weight kernels for GMM_v2 execution when configured.
503+
moe_intermediate_dim = (
504+
self.config.padded_base_moe_mlp_dim
505+
if self.config.padded_base_moe_mlp_dim is not None
506+
else self.intermediate_dim
507+
)
502508

503509
if quantizations.in_serve_mode(self.quant):
504510
# During aqt convert state we delete kernel weight from params to save
@@ -507,13 +513,7 @@ def __init__(
507513
self.wi_0 = jnp.zeros((num_experts, self.moe_expert_input_dim, intermediate_dim))
508514
self.wi_1 = jnp.zeros((num_experts, self.moe_expert_input_dim, intermediate_dim))
509515
self.wo = jnp.zeros((num_experts, intermediate_dim, self.moe_expert_input_dim))
510-
elif self.config.prefuse_moe_weights and self.config.attention == "vllm_rpa":
511-
# Pad model dimension in Fused MoE weight kernels for GMM_v2 execution.
512-
moe_intermediate_dim = (
513-
self.config.padded_base_moe_mlp_dim
514-
if self.config.padded_base_moe_mlp_dim is not None
515-
else self.intermediate_dim
516-
)
516+
elif self.config.prefuse_moe_weights:
517517
self.wi = nnx.Param(
518518
self.kernel_init(
519519
self.rngs.params(),
@@ -535,12 +535,6 @@ def __init__(
535535
out_sharding=self.wo_kernel_axes,
536536
)
537537
else:
538-
# Pad model dimension in Unfused MoE weight kernels for GMM_v2 execution.
539-
moe_intermediate_dim = (
540-
self.config.padded_base_moe_mlp_dim
541-
if self.config.padded_base_moe_mlp_dim is not None
542-
else self.intermediate_dim
543-
)
544538
self.wi_0 = nnx.Param(
545539
self.kernel_init(
546540
self.rngs.params(),
@@ -1669,19 +1663,44 @@ def get_wo_gmm_params():
16691663
def gmm_up(x, w0, w1, w0_bias, w1_bias, gmm_fn, weight_gather):
16701664
"""Run the two up-projections (gate + up) and apply the FFN activation."""
16711665
wi_gather_axes, wi_tile_size = get_wi_gmm_params()
1672-
layer_w0 = gmm_fn(x, w0, tiling=wi_tile_size, weight_gather_axes=wi_gather_axes)
1673-
if self.get_tensor_transpose_parallelism_size() > 1:
1674-
layer_w0 = jax.lax.psum(layer_w0, "tensor_transpose")
1675-
if self.config.mlp_bias:
1676-
layer_w0 = layer_w0 + w0_bias
1677-
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
1666+
if self.config.prefuse_moe_weights:
1667+
# Weights are stored as (G,K,2N); w0/w1 are adjacent slices so XLA elides this concat.
1668+
w_fused = jnp.concatenate([w0, w1], axis=-1)
1669+
out = gmm_fn(x, w_fused, tiling=wi_tile_size, weight_gather_axes=wi_gather_axes)
1670+
n = out.shape[-1] // 2
1671+
layer_w0, layer_w1 = out[:, :n], out[:, n:]
1672+
if self.get_tensor_transpose_parallelism_size() > 1:
1673+
layer_w0 = jax.lax.psum(layer_w0, "tensor_transpose")
1674+
layer_w1 = jax.lax.psum(layer_w1, "tensor_transpose")
1675+
if self.config.mlp_bias:
1676+
layer_w0 = layer_w0 + w0_bias
1677+
layer_w1 = layer_w1 + w1_bias
1678+
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
1679+
layer_w1 = adc.checkpoint_name(layer_w1, "moe_mlpwi_1")
1680+
else:
1681+
layer_w0 = gmm_fn(
1682+
x,
1683+
w0,
1684+
tiling=wi_tile_size,
1685+
weight_gather_axes=wi_gather_axes,
1686+
)
1687+
if self.get_tensor_transpose_parallelism_size() > 1:
1688+
layer_w0 = jax.lax.psum(layer_w0, "tensor_transpose")
1689+
if self.config.mlp_bias:
1690+
layer_w0 = layer_w0 + w0_bias
1691+
layer_w0 = adc.checkpoint_name(adc.checkpoint_name(layer_w0, "mlpwi_0"), "moe_mlpwi_0")
16781692

1679-
layer_w1 = gmm_fn(x, w1, tiling=wi_tile_size, weight_gather_axes=wi_gather_axes)
1680-
if self.get_tensor_transpose_parallelism_size() > 1:
1681-
layer_w1 = jax.lax.psum(layer_w1, "tensor_transpose")
1682-
if self.config.mlp_bias:
1683-
layer_w1 = layer_w1 + w1_bias
1684-
layer_w1 = adc.checkpoint_name(layer_w1, "moe_mlpwi_1")
1693+
layer_w1 = gmm_fn(
1694+
x,
1695+
w1,
1696+
tiling=wi_tile_size,
1697+
weight_gather_axes=wi_gather_axes,
1698+
)
1699+
if self.get_tensor_transpose_parallelism_size() > 1:
1700+
layer_w1 = jax.lax.psum(layer_w1, "tensor_transpose")
1701+
if self.config.mlp_bias:
1702+
layer_w1 = layer_w1 + w1_bias
1703+
layer_w1 = adc.checkpoint_name(layer_w1, "moe_mlpwi_1")
16851704
return self.apply_ffn_activation(layer_w0, layer_w1)
16861705

16871706
def get_gmm_for_local_experts(x, routing, route_metadata):
@@ -2592,6 +2611,11 @@ def __call__(
25922611
w1_kernel = None
25932612
if cfg.prefuse_moe_weights and cfg.attention == "vllm_rpa" and not self.is_hash_routing:
25942613
fused_kernel = jnp.asarray(self.wi[...], self.dtype)
2614+
elif cfg.prefuse_moe_weights:
2615+
wi = jnp.asarray(self.wi[...], self.dtype)
2616+
n = wi.shape[-1] // 2
2617+
w0_kernel = wi[..., :n]
2618+
w1_kernel = wi[..., n:]
25952619
else:
25962620
w0_kernel = jnp.asarray(self.wi_0[...], self.dtype)
25972621
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)