Skip to content

Commit 60477f5

Browse files
committed
test(moe): self-calibrating backend probe measures gathered-FFN reorder delta
The Device repr is Device(gpu,0) on BOTH Metal and CUDA — backend is not introspectable. Replace the proxy K=3584 matmul probe with one that measures the EXACT operation the sparse path performs: run an FFN on permuted (gathered) tokens and scatter back, compare to the direct FFN. Metal reads 0.0 here (uses tight 1e-5/1e-4 parity thresholds); CUDA cuBLAS reads ~1e-3 (uses the measured reassociation envelope). Self-calibrating, no hard-coded backend fudge.
1 parent 6c49768 commit 60477f5

1 file changed

Lines changed: 44 additions & 13 deletions

File tree

tests/test_moe_efficient.py

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,28 +29,59 @@
2929
from cppmega_mlx.nn.moe import MoEConfig, ReferenceMoE
3030

3131

32-
def _is_cuda_backend() -> bool:
33-
try:
34-
return mx.default_device().type == mx.DeviceType.gpu and "cuda" in repr(
35-
mx.default_device()
36-
).lower()
37-
except Exception:
38-
return False
32+
def _backend_reassociation_epsilon() -> float:
33+
"""Measure this backend's fp32 *gather-reorder* reassociation delta.
34+
35+
The Device repr is ``Device(gpu, 0)`` on BOTH Metal and CUDA, so backend
36+
identity is not introspectable. Instead we measure the exact property that
37+
drives the sparse-vs-dense parity delta: running the same expert FFN on the
38+
same tokens but in a permuted (gathered) order and scattering back. Any
39+
nonzero result is pure backend fp32 reduction-order rounding (the gather and
40+
scatter themselves are bitwise exact — see the round-trip test). Metal is
41+
(near-)associative here (~1e-7); CUDA cuBLAS is not (~1e-3). The parity
42+
thresholds derive directly from this measured envelope, so the test
43+
self-calibrates to whatever backend it runs on — no silent fudge.
44+
"""
45+
46+
mx.random.seed(123)
47+
num_tokens, d_model, hidden = 256, 3584, 128
48+
x = mx.random.normal((num_tokens, d_model)).astype(mx.float32)
49+
w_in = mx.random.normal((hidden, d_model)).astype(mx.float32) * 0.02
50+
w_out = mx.random.normal((d_model, hidden)).astype(mx.float32) * 0.02
51+
52+
def ffn(xx: mx.array) -> mx.array:
53+
return nn.gelu_approx(xx @ w_in.T) @ w_out.T
54+
55+
direct = ffn(x)
56+
perm = np.random.permutation(num_tokens).astype(np.int32)
57+
idx = mx.array(perm)
58+
gathered = ffn(mx.take(x, idx, axis=0))
59+
reordered = mx.zeros((num_tokens, d_model), dtype=mx.float32).at[idx].add(gathered)
60+
mx.eval(direct, reordered)
61+
return float(mx.max(mx.abs(direct - reordered)))
62+
63+
64+
# A non-associative backend (CUDA cuBLAS) yields ~1e-3 for the gathered FFN
65+
# reorder; Metal yields ~1e-7. Branch the parity tolerances on the measured
66+
# envelope; the dense-vs-sparse delta is the same kind of reordering, summed
67+
# across experts, so allow a small multiple over the single-chain epsilon.
68+
_BACKEND_REASSOC_EPS = _backend_reassociation_epsilon()
69+
_BACKEND_IS_NONASSOC = _BACKEND_REASSOC_EPS > 1e-5
70+
_FWD_TOL = 5e-3 if _BACKEND_IS_NONASSOC else 1e-5
71+
_INPUT_GRAD_TOL = 5e-3 if _BACKEND_IS_NONASSOC else 1e-5
72+
_PARAM_GRAD_TOL = 1e-2 if _BACKEND_IS_NONASSOC else 1e-4
3973

4074

41-
# fp32 reassociation envelope: Metal is (near-)associative; CUDA cuBLAS is not.
42-
# Measured: one reassociated K=3584 fp32 matmul differs ~3e-4 on CUDA; the swiglu
43-
# expert chain (two matmuls + gelu) compounds that into the low-e-3 range.
4475
def _fwd_tol() -> float:
45-
return 5e-3 if _is_cuda_backend() else 1e-5
76+
return _FWD_TOL
4677

4778

4879
def _input_grad_tol() -> float:
49-
return 5e-3 if _is_cuda_backend() else 1e-5
80+
return _INPUT_GRAD_TOL
5081

5182

5283
def _param_grad_tol() -> float:
53-
return 1e-2 if _is_cuda_backend() else 1e-4
84+
return _PARAM_GRAD_TOL
5485

5586

5687
def _config(*, d_model: int = 3584, num_experts: int = 16, top_k: int = 4) -> MoEConfig:

0 commit comments

Comments
 (0)