|
26 | 26 | import pytest |
27 | 27 |
|
28 | 28 | import mlx.core as mx |
| 29 | +import mlx.nn as nn |
| 30 | +from mlx.utils import tree_map |
29 | 31 |
|
30 | 32 | import cppmega_mlx.nn._tilelang.mamba3_path_c as mamba3_path_c |
31 | 33 | from cppmega_mlx.nn._tilelang import ( |
@@ -90,6 +92,84 @@ def _make_inputs( |
90 | 92 | return x, B, C, z, A, dt, D, h0 |
91 | 93 |
|
92 | 94 |
|
| 95 | +def _make_mamba3_projection_inputs( |
| 96 | + *, |
| 97 | + d_model: int, |
| 98 | + seq: int, |
| 99 | + dtype: mx.Dtype, |
| 100 | +) -> tuple[mx.array, ...]: |
| 101 | + """Build post-projection tensors at the Mamba3 scan contract.""" |
| 102 | + |
| 103 | + from cppmega_mlx.nn.mamba3 import ( |
| 104 | + Mamba3Config, |
| 105 | + Mamba3ReferenceBlock, |
| 106 | + _apply_rope_on_state_dim, |
| 107 | + _broadcast_groups_to_heads, |
| 108 | + _compute_trapezoidal_scale, |
| 109 | + _heads_to_group_scale, |
| 110 | + _split_by_sizes, |
| 111 | + causal_depthwise_conv1d, |
| 112 | + ) |
| 113 | + |
| 114 | + cfg = Mamba3Config( |
| 115 | + d_model=d_model, |
| 116 | + expand=2, |
| 117 | + headdim=64, |
| 118 | + d_state=16, |
| 119 | + ngroups=1, |
| 120 | + chunk_size=128, |
| 121 | + ) |
| 122 | + block = Mamba3ReferenceBlock(cfg) |
| 123 | + block.update( |
| 124 | + tree_map( |
| 125 | + lambda value: value.astype(dtype) if isinstance(value, mx.array) else value, |
| 126 | + block.parameters(), |
| 127 | + ) |
| 128 | + ) |
| 129 | + |
| 130 | + mx.random.seed(0) |
| 131 | + hidden = (mx.random.normal((1, seq, cfg.d_model)) * 0.02).astype(dtype) |
| 132 | + z, x, B, C, dd_dt, dd_A, trap, angles = block.split_in_proj( |
| 133 | + block.in_proj(hidden) |
| 134 | + ) |
| 135 | + |
| 136 | + xBC = mx.concatenate([x, B, C], axis=-1) |
| 137 | + xBC = causal_depthwise_conv1d( |
| 138 | + xBC, |
| 139 | + block.conv_weight.astype(xBC.dtype), |
| 140 | + block.conv_bias.astype(xBC.dtype), |
| 141 | + ) |
| 142 | + x, B, C = _split_by_sizes( |
| 143 | + nn.silu(xBC), |
| 144 | + [cfg.d_inner, block.dims.d_bc, block.dims.d_bc], |
| 145 | + ) |
| 146 | + x = x.reshape(1, seq, cfg.nheads, cfg.headdim) |
| 147 | + z = z.reshape(1, seq, cfg.nheads, cfg.headdim) |
| 148 | + |
| 149 | + B_mimo = B.reshape(1, seq, cfg.effective_mimo_rank, cfg.ngroups, cfg.d_state) |
| 150 | + C_mimo = C.reshape(1, seq, cfg.effective_mimo_rank, cfg.ngroups, cfg.d_state) |
| 151 | + B_mimo, C_mimo = block.transform_bc(B_mimo, C_mimo) |
| 152 | + B = mx.mean(B_mimo, axis=2) |
| 153 | + C = mx.mean(C_mimo, axis=2) |
| 154 | + |
| 155 | + dt = nn.softplus(dd_dt + block.dt_bias.astype(dd_dt.dtype)) |
| 156 | + trap_scale = _compute_trapezoidal_scale(dt, trap) |
| 157 | + B = B * _heads_to_group_scale(trap_scale, cfg.ngroups)[:, :, :, None] |
| 158 | + |
| 159 | + angles = mx.broadcast_to( |
| 160 | + angles[:, :, None, :], |
| 161 | + (1, seq, cfg.nheads, block.dims.num_rope_angles), |
| 162 | + ) |
| 163 | + angles_cumsum = mx.cumsum(angles * dt[:, :, :, None], axis=1) |
| 164 | + B = _apply_rope_on_state_dim(B, angles_cumsum) |
| 165 | + C = _apply_rope_on_state_dim(C, angles_cumsum) |
| 166 | + B = _broadcast_groups_to_heads(B, cfg.nheads, "B") |
| 167 | + C = _broadcast_groups_to_heads(C, cfg.nheads, "C") |
| 168 | + A = mx.minimum(-nn.softplus(dd_A), -cfg.A_floor) |
| 169 | + h0 = mx.zeros((1, cfg.nheads, cfg.headdim, cfg.d_state), dtype=dtype) |
| 170 | + return x, B, C, z, A, dt, block.D.astype(dtype), h0 |
| 171 | + |
| 172 | + |
93 | 173 | # --------------------------------------------------------------------------- |
94 | 174 | # Status & lowering smoke tests |
95 | 175 | # --------------------------------------------------------------------------- |
@@ -815,6 +895,36 @@ def test_bwd_path_c_snapshot_route_survives_decay_underflow() -> None: |
815 | 895 | ) |
816 | 896 |
|
817 | 897 |
|
| 898 | +def test_bwd_path_c_long_model_bf16_uses_stable_snapshot_simd() -> None: |
| 899 | + """Long model-shaped bf16 bwd must not use inverse-state SIMD recurrence.""" |
| 900 | + |
| 901 | + _require_mamba3_path_c() |
| 902 | + inputs = _make_mamba3_projection_inputs( |
| 903 | + d_model=1024, |
| 904 | + seq=2048, |
| 905 | + dtype=mx.bfloat16, |
| 906 | + ) |
| 907 | + x, B, C, z, A, dt, D, h0 = inputs |
| 908 | + mx.random.seed(123) |
| 909 | + dy = (mx.random.normal(x.shape) * 0.01).astype(mx.bfloat16) |
| 910 | + mx.eval(dy, *inputs) |
| 911 | + |
| 912 | + g_pc = mamba3_mimo_bwd_path_c(dy, x, B, C, z, A, dt, D, h0) |
| 913 | + g_pb = mamba3_mimo_bwd_metal(dy, x, B, C, z, A, dt, D, h0) |
| 914 | + mx.eval(*g_pc, *g_pb) |
| 915 | + |
| 916 | + names = ["dx", "dB", "dC", "dz", "dA", "ddt", "dD", "dh0"] |
| 917 | + for name, gpc, gpb in zip(names, g_pc, g_pb, strict=True): |
| 918 | + assert bool(mx.all(mx.isfinite(gpc.astype(mx.float32))).item()) |
| 919 | + np.testing.assert_allclose( |
| 920 | + _np(gpc), |
| 921 | + _np(gpb), |
| 922 | + rtol=1e-1, |
| 923 | + atol=5e-3, |
| 924 | + err_msg=f"long model bf16 grad mismatch on {name}", |
| 925 | + ) |
| 926 | + |
| 927 | + |
818 | 928 | def test_apply_path_c_vjp_matches_reference_inside_mlx_graph_transform() -> None: |
819 | 929 | """TileLang Path C is callable from ``mx.grad`` and uses its custom VJP.""" |
820 | 930 |
|
|
0 commit comments