|
29 | 29 | from cppmega_mlx.nn.moe import MoEConfig, ReferenceMoE |
30 | 30 |
|
31 | 31 |
|
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 |
39 | 73 |
|
40 | 74 |
|
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. |
44 | 75 | def _fwd_tol() -> float: |
45 | | - return 5e-3 if _is_cuda_backend() else 1e-5 |
| 76 | + return _FWD_TOL |
46 | 77 |
|
47 | 78 |
|
48 | 79 | def _input_grad_tol() -> float: |
49 | | - return 5e-3 if _is_cuda_backend() else 1e-5 |
| 80 | + return _INPUT_GRAD_TOL |
50 | 81 |
|
51 | 82 |
|
52 | 83 | def _param_grad_tol() -> float: |
53 | | - return 1e-2 if _is_cuda_backend() else 1e-4 |
| 84 | + return _PARAM_GRAD_TOL |
54 | 85 |
|
55 | 86 |
|
56 | 87 | def _config(*, d_model: int = 3584, num_experts: int = 16, top_k: int = 4) -> MoEConfig: |
|
0 commit comments