Skip to content

Commit bd99515

Browse files
committed
test: add helper to generate Mamba3 projection inputs and verify bf16 bwd stability
1 parent ca5fec8 commit bd99515

1 file changed

Lines changed: 110 additions & 0 deletions

File tree

tests/test_tilelang_mamba3_path_c.py

Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
import pytest
2727

2828
import mlx.core as mx
29+
import mlx.nn as nn
30+
from mlx.utils import tree_map
2931

3032
import cppmega_mlx.nn._tilelang.mamba3_path_c as mamba3_path_c
3133
from cppmega_mlx.nn._tilelang import (
@@ -90,6 +92,84 @@ def _make_inputs(
9092
return x, B, C, z, A, dt, D, h0
9193

9294

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+
93173
# ---------------------------------------------------------------------------
94174
# Status & lowering smoke tests
95175
# ---------------------------------------------------------------------------
@@ -815,6 +895,36 @@ def test_bwd_path_c_snapshot_route_survives_decay_underflow() -> None:
815895
)
816896

817897

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+
818928
def test_apply_path_c_vjp_matches_reference_inside_mlx_graph_transform() -> None:
819929
"""TileLang Path C is callable from ``mx.grad`` and uses its custom VJP."""
820930

0 commit comments

Comments
 (0)