|
31 | 31 | ) |
32 | 32 | from axlearn.common.config import config_for_function |
33 | 33 | from axlearn.common.layers import RMSNorm, set_bias_recursively |
| 34 | +from axlearn.common.megablock.ops import select_tiling_fn |
34 | 35 | from axlearn.common.mixture_of_experts import ( |
35 | 36 | AdaptiveLoadBalanceLoss, |
36 | 37 | ApproximateTokenDropFreeMoE, |
@@ -1640,6 +1641,148 @@ def shard_map_fn(tokens_per_expert, sorted_inputs): |
1640 | 1641 | ], |
1641 | 1642 | ) |
1642 | 1643 |
|
| 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 | + |
1643 | 1786 | @parameterized.product( |
1644 | 1787 | gmm_backend=[None, GMMBackend.PALLAS, GMMBackend.RAGGED_DOT], |
1645 | 1788 | ) |
|
0 commit comments