Skip to content

Commit e9b882e

Browse files
ds-hwangchanglan
authored andcommitted
Fix MoE GMM crash during extend_step when tiling is a function config
GitOrigin-RevId: 64b3733
1 parent 89e1f1c commit e9b882e

2 files changed

Lines changed: 167 additions & 27 deletions

File tree

axlearn/common/mixture_of_experts.py

Lines changed: 24 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -2412,11 +2412,14 @@ def _pallas_gmm(self, lhs, rhs, tokens_per_expert):
24122412
cfg = self.config
24132413
if isinstance(cfg.tiling, tuple):
24142414
tiling = cfg.tiling
2415-
pad_length = cfg.tiling[0]
24162415
else:
2417-
tiling = cfg.tiling.instantiate()
2418-
# no padding when tiling is a function
2419-
pad_length = 1
2416+
tiling_fn = cfg.tiling.instantiate()
2417+
m, k = lhs.shape
2418+
_, _, n = rhs.shape
2419+
tiling = tiling_fn(m, k, n)
2420+
2421+
num_tokens = lhs.shape[0]
2422+
token_tile_size = tiling[0]
24202423

24212424
# TODO: Revisit once Mosaic supports highest precision.
24222425
matmul_precision = (
@@ -2425,30 +2428,24 @@ def _pallas_gmm(self, lhs, rhs, tokens_per_expert):
24252428
else contextlib.nullcontext()
24262429
)
24272430
with matmul_precision:
2428-
if lhs.shape[0] % pad_length:
2429-
padded_lhs = lhs
2430-
pad_length -= lhs.shape[0] % pad_length
2431-
padded_lhs = jax.lax.pad(
2432-
lhs, jnp.array(0.0).astype(lhs.dtype), [(0, pad_length, 0), (0, 0, 0)]
2433-
)
2434-
results = mblx.gmm(
2435-
padded_lhs,
2436-
rhs,
2437-
tokens_per_expert,
2438-
tiling=tiling,
2439-
preferred_element_type=cfg.preferred_element_type or jnp.bfloat16,
2440-
interpret=cfg.interpret,
2441-
)
2442-
results = results[: lhs.shape[0]]
2443-
else:
2444-
results = mblx.gmm(
2445-
lhs,
2446-
rhs,
2447-
tokens_per_expert,
2448-
tiling=tiling,
2449-
preferred_element_type=cfg.preferred_element_type or jnp.bfloat16,
2450-
interpret=cfg.interpret,
2431+
# Padding is wasteful for decoding (e.g., num_tokens=1, token_tile_size=512).
2432+
# Dynamically adjusting tiling (e.g., tiling[0]=num_tokens) triggers JIT recompilation,
2433+
# which is even slower.
2434+
# TODO: Find a better solution for decoding.
2435+
if num_tokens % token_tile_size:
2436+
pad_amount = token_tile_size - (num_tokens % token_tile_size)
2437+
lhs = jax.lax.pad(
2438+
lhs, jnp.array(0.0).astype(lhs.dtype), [(0, pad_amount, 0), (0, 0, 0)]
24512439
)
2440+
results = mblx.gmm(
2441+
lhs,
2442+
rhs,
2443+
tokens_per_expert,
2444+
tiling=tiling,
2445+
preferred_element_type=cfg.preferred_element_type or jnp.bfloat16,
2446+
interpret=cfg.interpret,
2447+
)
2448+
results = results[:num_tokens]
24522449
return results
24532450

24542451
def _dispatch_hook(

axlearn/common/mixture_of_experts_test.py

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@
3131
)
3232
from axlearn.common.config import config_for_function
3333
from axlearn.common.layers import RMSNorm, set_bias_recursively
34+
from axlearn.common.megablock.ops import select_tiling_fn
3435
from axlearn.common.mixture_of_experts import (
3536
AdaptiveLoadBalanceLoss,
3637
ApproximateTokenDropFreeMoE,
@@ -1640,6 +1641,148 @@ def shard_map_fn(tokens_per_expert, sorted_inputs):
16401641
],
16411642
)
16421643

1644+
@parameterized.parameters(
1645+
(GMMBackend.RAGGED_DOT, 3e-5), (GMMBackend.TOKAMAX, 3e-5), (GMMBackend.PALLAS, 1e-6)
1646+
)
1647+
def test_padded_gmm_parity_with_einsum(self, backend, atol):
1648+
"""Test that padded_gmm produces identical results to einsum reference.
1649+
1650+
This unit test verifies the correctness of padded_gmm by comparing it against a simple
1651+
einsum-based reference implementation.
1652+
"""
1653+
1654+
def _gmm_einsum_reference(lhs, rhs, tokens_per_expert):
1655+
"""Reference implementation using einsum for grouped matrix multiplication.
1656+
1657+
Args:
1658+
lhs: Tensor of shape [total_tokens, model_dim], sorted by expert.
1659+
rhs: Tensor of shape [num_experts, model_dim, hidden_dim].
1660+
tokens_per_expert: Tensor of shape [num_experts] with token counts per expert.
1661+
1662+
Returns:
1663+
Result tensor of shape [total_tokens, hidden_dim].
1664+
"""
1665+
# Compute expert boundaries from tokens_per_expert.
1666+
expert_ends = jnp.cumsum(tokens_per_expert)
1667+
expert_starts = jnp.concatenate([jnp.array([0]), expert_ends[:-1]])
1668+
1669+
# Create a mask for each expert's tokens and compute matmul.
1670+
total_tokens = lhs.shape[0]
1671+
token_indices = jnp.arange(total_tokens)[:, None] # [total_tokens, 1]
1672+
expert_mask = (token_indices >= expert_starts[None, :]) & (
1673+
token_indices < expert_ends[None, :]
1674+
) # [total_tokens, num_experts]
1675+
1676+
# Expand lhs for all experts.
1677+
# [total_tokens, model_dim] -> [total_tokens, num_experts, model_dim]
1678+
lhs_expanded = lhs[:, None, :] * expert_mask[:, :, None].astype(lhs.dtype)
1679+
1680+
# Compute all expert matmuls at once: [total_tokens, num_experts, hidden_dim]
1681+
# lhs_expanded: [total_tokens, num_experts, model_dim]
1682+
# rhs: [num_experts, model_dim, hidden_dim]
1683+
all_results = jnp.einsum("teh,ehd->ted", lhs_expanded, rhs)
1684+
1685+
# Sum across experts (each token only has one non-zero expert due to masking).
1686+
results = jnp.sum(all_results, axis=1) # [total_tokens, hidden_dim]
1687+
return results
1688+
1689+
total_tokens = 128
1690+
model_dim = 128
1691+
hidden_dim = 256
1692+
num_experts = 8
1693+
cfg = TransformerFeedForwardDropFreeMoE.default_config().set(
1694+
name="moe",
1695+
gmm_backend=backend,
1696+
input_dim=model_dim,
1697+
hidden_dim=hidden_dim,
1698+
num_experts=num_experts,
1699+
num_groups=1,
1700+
gating=TopKDropFreeGating.default_config(),
1701+
tiling=(128, 128, 128),
1702+
preferred_element_type=jnp.float32,
1703+
interpret=True,
1704+
)
1705+
layer = cfg.instantiate(parent=None)
1706+
1707+
k1, k2 = jax.random.split(jax.random.PRNGKey(42), 2)
1708+
1709+
# Create random inputs in bfloat16 (default dtype for _ragged_dot_gmm).
1710+
lhs = jax.random.normal(k1, (total_tokens, model_dim), dtype=jnp.float32)
1711+
rhs = jax.random.normal(k2, (num_experts, model_dim, hidden_dim), dtype=jnp.float32)
1712+
1713+
# Distribute tokens evenly across experts.
1714+
tokens_per_expert = jnp.array([8, 8, 8, 8, 8, 8, 8, 8], dtype=jnp.int32)
1715+
ref_result = _gmm_einsum_reference(lhs, rhs, tokens_per_expert)
1716+
test_result = layer._padded_gmm(lhs, rhs, tokens_per_expert)
1717+
# Pallas returns NaN for padded positions; replace with 0 to match reference.
1718+
if backend == GMMBackend.PALLAS:
1719+
test_result = jnp.where(jnp.isnan(test_result), 0, test_result)
1720+
assert_allclose(test_result, ref_result, atol=atol)
1721+
1722+
# Also test with non-uniform token distribution.
1723+
tokens_per_expert_nonuniform = jnp.array([16, 4, 12, 8, 2, 10, 6, 6], dtype=jnp.int32)
1724+
ref_result2 = _gmm_einsum_reference(lhs, rhs, tokens_per_expert_nonuniform)
1725+
test_result2 = layer._padded_gmm(lhs, rhs, tokens_per_expert_nonuniform)
1726+
if backend == GMMBackend.PALLAS:
1727+
test_result2 = jnp.where(jnp.isnan(test_result2), 0, test_result2)
1728+
assert_allclose(test_result2, ref_result2, atol=atol)
1729+
1730+
@parameterized.product(
1731+
backend=(GMMBackend.RAGGED_DOT, GMMBackend.TOKAMAX, GMMBackend.PALLAS),
1732+
tiling=(
1733+
(512, 512, 512),
1734+
config_for_function(select_tiling_fn).set(
1735+
tiling_larger_k=(512, 512, 512), tiling_larger_n=(512, 512, 512)
1736+
),
1737+
),
1738+
)
1739+
def test_gmm_dynamic_tiling_for_small_batch_decoding(self, backend, tiling):
1740+
"""Test that small batch sizes are handled via padding (i.e., extend_step)."""
1741+
mesh = [1, 1, 1, 1, 1, 1]
1742+
batch_size = 1
1743+
seq_len = 32
1744+
model_dim = 128
1745+
num_experts = 8
1746+
1747+
k1, k2, k3 = jax.random.split(jax.random.PRNGKey(123456), 3)
1748+
1749+
input_batch = self._random_dense((batch_size, seq_len, model_dim), k1, jnp.float32, limit=1)
1750+
1751+
mesh_axis_names = ["pipeline", "data", "expert", "fsdp", "seq", "model"]
1752+
1753+
with Mesh(mesh_utils.create_device_mesh(mesh), mesh_axis_names):
1754+
input_batch = with_sharding_constraint(input_batch, PartitionSpec("data", "fsdp"))
1755+
1756+
# Use large tile size - padding logic handles small batches
1757+
cfg = TransformerFeedForwardDropFreeMoE.default_config().set(
1758+
name="moe",
1759+
gmm_backend=backend,
1760+
input_dim=model_dim,
1761+
hidden_dim=model_dim * 4,
1762+
activation=("nn.silu", "linear"),
1763+
structure="prenorm",
1764+
num_experts=num_experts,
1765+
num_groups=1,
1766+
gating=TopKDropFreeGating.default_config().set(gating_logit_cap=0),
1767+
tiling=tiling,
1768+
interpret=True, # for cpu testing.
1769+
preferred_element_type=jnp.float32,
1770+
)
1771+
layer = cfg.instantiate(parent=None)
1772+
params = layer.initialize_parameters_recursively(prng_key=k2)
1773+
1774+
# This should not crash - padding handles num_tokens < tile_size
1775+
output, _ = F(
1776+
layer,
1777+
inputs=dict(inputs=input_batch),
1778+
state=params,
1779+
is_training=True,
1780+
prng_key=k3,
1781+
)
1782+
1783+
# Verify output shape is correct.
1784+
self.assertEqual(output.shape, input_batch.shape)
1785+
16431786
@parameterized.product(
16441787
gmm_backend=[None, GMMBackend.PALLAS, GMMBackend.RAGGED_DOT],
16451788
)

0 commit comments

Comments
 (0)