Skip to content

Commit e3d383c

Browse files
committed
docs+probe(§METAL-RETILE): Apple-GPU VALIDATE sub_chunks=2 carry + HPC=2 dchunk FIX
The §METAL-RETILE sub_chunks=2 body + the HPC=2 §BARRIER-FIX were committed at 53a392c as compile-check-only (no Apple-GPU dispatch, watchdog mandate). This run NUMERICALLY VALIDATES both on the Apple GPU through the safe_metal_run.sh guard (bounded B=1 S=64, GPU mutex + RSS cap + timeout; guard log clean, no watchdog). MEASURED (local Metal, guarded): - HPC=2 dchunk parity FIXED: 8.59e-3 -> 6.03e-6 (all grads <1e-3). 2.48x GO. The §BARRIER-FIX (per-head sync_threads fencing the head-band staging reuse) resolves the cross-head frag->shared->global drain race. - HPC=1 reproduced: 4.93x GO, 6.03e-6 (no regression from the barrier-fix). - sub_chunks=2 retile RUNS on Apple GPU + parity-clean at in-budget L=32->L_sub=16 (P=N=32, HPC=1): 5.30x GO, worst 6.03e-6 (dchunk 6.03e-6, dinp 5.21e-6). The inter-sub-chunk carry (dchunk additive + dinp first-order cross via Psc1 + dC_diag/dseg diag00/diag11/cross10 blocks) is numerically clean. - MSL: retile body emits simdgroup_multiply_accumulate x5, simdgroup_float8x8 x3. - z3: ALL_POSITIVES_PROVED_AND_NON_VACUOUS (5 metal_subchunk UNSAT + 5 negatives). HONEST NO-GO — prod L=64 via retile does NOT fit 32 KB: 68096 B (HPC=1) / 99328 B (HPC=2) >> Apple 32768 B. The build gate RAISES NotImplementedError end-to-end (verified ~68096 B >= 32768 B) — NEVER launches over-budget (RULE #1). The carry MECHANISM is validated correct (z3 + in-budget Apple-GPU run); prod L=64 remains a genuine 32 KB NO-GO. Serial prod prim BYTE-IDENTICAL (untouched). scratch/probe_retile_metal_local.py = the guarded in-budget retile A/B probe. docs/METAL-PROD-RETILE.md = full MEASURED writeup.
1 parent 53a392c commit e3d383c

2 files changed

Lines changed: 187 additions & 0 deletions

File tree

docs/METAL-PROD-RETILE.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# §METAL-RETILE — sub_chunks=2 L-retile for the batched B2 Metal GEMM prim
2+
3+
Status: **carry IMPLEMENTED + z3-PROVEN + Apple-GPU numerically VALIDATED (in-budget).
4+
Prod L=64 via this retile is an honest 32 KB NO-GO (gate RAISES, no over-budget
5+
launch).** HPC=2 dchunk parity bug **FIXED** (8.59e-3 -> 6.03e-6).
6+
7+
All Apple-GPU dispatches in this campaign went through
8+
`/Users/dave/.local/bin/safe_metal_run.sh <timeout> <cmd>` (single-owner GPU mutex +
9+
RSS>40GB SIGKILL + wall-clock timeout before the 90s SoC watchdog window). Bounded
10+
shapes only (B=1, S<=64). Guard log clean: every run `END rc=0`, no TIMEOUT, no
11+
RSS_CAP_HIT, no watchdog panic. Lock released after each run.
12+
13+
## What was delivered
14+
15+
### (1) sub_chunks=2 retile BODY — `main_retile` in `chunk_scan_combine_bwd_metal_gemm_prim_batched`
16+
`cppmega_mlx/nn/_tilelang/mamba3_chunked_backward_core.py` L3530-3891. Splits the
17+
length-L chunk into `sc0 = rows[0, L_sub)` and `sc1 = rows[L_sub, L)` (L_sub = L/2)
18+
so the L-indexed simdgroup operands fit Apple's 32 KB pool. Inter-sub-chunk SSD
19+
state carried per the z3-proven identities:
20+
- **dchunk_states ADDITIVE**: one `dchunk_frag` accumulates both L_sub-row
21+
`transpose_A` GEMMs (sc0 then sc1); the sc1 partial is also drained to a resident
22+
`Psc1[P,N]` for the dinp cross carry.
23+
- **dinp first-order cross carry**: for `s in sc0`, local sum over (l in sc0, l>=s)
24+
plus `Psc1[p,n] / exp2(dacs[s]*p)`; for `s in sc1` purely local.
25+
- **dC_diag / dseg**: the single (L,L) DYX GEMM is restructured into three L_sub
26+
blocks — DYX00 (diag00), DYX11 (diag11), DYX10 (cross10) — 3 inlined L_sub GEMMs;
27+
dseg scatters over the same three blocks.
28+
29+
`sub_chunks==1` returns the legacy `main` body BYTE-IDENTICAL. `sub_chunks not in
30+
{1,2}` RAISES. `L_sub % 8 != 0` RAISES. RULE #1: ONE path past the gates, no silent
31+
fallback.
32+
33+
### (2) HPC=2 dchunk parity bug FIX — §BARRIER-FIX
34+
Root cause: head-band staging-tile REUSE race. At `HEADS_PER_CTA>=2` the next head's
35+
refill of the shared `dY16/opA/opB/store_f32/DYX` tiles could race the previous
36+
head's frag->shared->global drain. Fix: per-head `T.sync_threads()` fences at the
37+
start of each head iteration in the DYX phase (A) and the dchunk_states phase (D),
38+
plus frag->store_f32 fences before the parallel/global reads. No-op at HPC=1.
39+
40+
## MEASURED (guarded, local Apple Metal, bounded B=1 S=64 G=2 H=4 P=N=32)
41+
42+
| run | shape | serial ms | batched/retile ms | speedup | parity worst | verdict |
43+
|-----|-------|-----------|-------------------|---------|--------------|---------|
44+
| HPC=1 (sub=1) | L=P=N=32 | 10.83 | 2.20 | **4.93x** | 6.03e-06 | GO PASS |
45+
| HPC=2 (sub=1) AFTER FIX | L=P=N=32 | 10.84 | 4.37 | **2.48x** | **6.03e-06** (dchunk 6.03e-06, was 8.59e-3) | GO PASS |
46+
| retile sub=2 HPC=1 | L=32->L_sub=16,P=N=32 | 10.83 | 2.04 | **5.30x** | 6.03e-06 (dchunk 6.03e-06, dinp 5.21e-06) | GO PASS |
47+
48+
All grads `<1e-3` in every run. dx/dz exactly 0.
49+
50+
## MSL codegen (CPU lower, no dispatch)
51+
retile body (sub=2, L=32, L_sub=16, P=N=32, HPC=1): MSL 44284 B,
52+
`simdgroup_multiply_accumulate: 5`, `simdgroup_float8x8: 3`,
53+
`make_filled_simdgroup_matrix: 44`. Legacy sub=1 (HPC=2): MSL 37982 B.
54+
55+
## z3 (non-vacuous) — `scratch/proof_b2_batched_driver.py`
56+
VERDICT `ALL_POSITIVES_PROVED_AND_NON_VACUOUS`. 5 metal_subchunk positives UNSAT
57+
(associativity, dchunk_additive, dinp_firstorder_carry, dcdiag_block_split,
58+
dseg_block_partition — all L64 nsub2), each paired with a bugged-control negative
59+
(dropped_carry / dropped_sc1 / dropped_cross / dropped_cross10 / nocross_misses)
60+
that is sat/miss. dchunk head-band disjointness: the `single_writer` obligation on
61+
the dchunk/dcoff/dcdiag contractions UNSAT; the interleaved-bands negative
62+
sat/single_writer=False.
63+
64+
## HONEST NO-GO — prod L=64 via retile does NOT fit 32 KB
65+
`_metal_subchunk_smem_bytes(L_sub=32, P=N=64)`:
66+
- HPC=1: all_static **68096 B**, store_dynamic 51712 B
67+
- HPC=2: all_static **99328 B**, store_dynamic 82944 B
68+
69+
Both far exceed Apple's **32768 B** pool — dominated by the full-L `dY_band`, the
70+
fp32 `Psc1` resident, and the fp32 `store_f32`. Even moving `store_f32` dynamic
71+
leaves the static residual > 32 KB. So **prod L=64 via this retile is an honest
72+
32 KB NO-GO on Apple**. The build gate RAISES `NotImplementedError` end-to-end
73+
(verified: `~68096 B ... >= Apple's 32768 B limit`) — it NEVER launches
74+
over-budget (RULE #1).
75+
76+
The carry MECHANISM is nonetheless validated correct: z3 over the reals AND a clean
77+
Apple-GPU numeric run at the in-budget L=32->L_sub=16 shape (5.30x, 6.03e-6). To
78+
make prod L=64 actually fit would require sub_chunks=4 (L_sub=16) PLUS shrinking the
79+
resident set (Psc1 fp16, store dynamic, dropping the full-L dY_band to per-sub-chunk
80+
reload) — and the nsub=4 generalized lower-triangular block carry is NOT z3-proven
81+
here, so the prim RAISES for sub_chunks not in {1,2} rather than emit an unproven
82+
generalization.
83+
84+
## Bottom line
85+
The 4.95x B2 batched Metal speedup is REAL and parity-clean at in-budget native
86+
L=32 (P=N=32), now at BOTH HPC=1 (4.93x) and HPC=2 (2.48x, bug fixed). The
87+
sub_chunks=2 retile carry is implemented, z3-proven, and Apple-GPU-validated at an
88+
in-budget shape (5.30x). It does NOT unlock prod L=64 (P=N=64) — that overflows the
89+
32 KB pool and the gate honestly RAISES.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
"""LOCAL Apple-Metal numeric A/B for the §METAL-RETILE sub_chunks=2 body.
2+
3+
Compares the byte-identical SERIAL prod Metal prim vs the BATCHED Metal GEMM prim
4+
built with sub_chunks=2 (the L_sub=L/2 re-tile carry), at a SMALL in-budget bounded
5+
shape (L=32 -> L_sub=16, P=N=32, HPC=1) that fits Apple's 32 KB pool. This is the
6+
ONLY shape at which the retile carry can be NUMERICALLY validated on the Apple GPU:
7+
prod L=64 via this retile is 68096 B (HPC=1) / 99328 B (HPC=2) >> 32 KB, so the
8+
build gate RAISES (honest NO-GO) and never dispatches over-budget (RULE #1).
9+
10+
Validates: the sub_chunks=2 body RUNS on Apple GPU and ALL grads parity <1e-3 vs the
11+
serial prod prim (the inter-sub-chunk dchunk additive + dinp first-order cross carry +
12+
dC_diag/dseg diag00/diag11/cross10 block recombination are numerically clean).
13+
"""
14+
import os
15+
import time
16+
17+
import numpy as np
18+
import mlx.core as mx # noqa: F401 (forces Metal availability)
19+
import tilelang
20+
import tilelang.language as T # noqa: F401
21+
import torch
22+
23+
from cppmega_mlx.nn._tilelang.mamba3_chunked_backward_core import (
24+
chunk_scan_combine_bwd_metal_prim,
25+
chunk_scan_combine_bwd_metal_gemm_prim_batched,
26+
)
27+
28+
DEV = "mps"
29+
B, S, CH, G, H, P, N = 1, 64, 32, 2, 4, 32, 32 # L=32 -> sub_chunks=2 -> L_sub=16
30+
HPC = 1
31+
SUB = 2
32+
nchunks = S // CH
33+
34+
rng = np.random.RandomState(0)
35+
def f16(shape):
36+
return torch.tensor((rng.randn(*shape) * 0.1).astype(np.float32), dtype=torch.float16, device=DEV)
37+
def f32(shape):
38+
return torch.tensor((rng.randn(*shape) * 0.1).astype(np.float32), dtype=torch.float32, device=DEV)
39+
40+
dout = f16((B, S, H, P)); cb = f16((B, nchunks, G, CH, CH)); x = f16((B, S, H, P))
41+
z = f16((B, S, H, P)); dt = f16((B, H, nchunks, CH)); dA = f16((B, H, nchunks, CH))
42+
C = f16((B, S, G, N)); Bm = f16((B, S, G, N)); prev = f32((B, nchunks, H, P, N))
43+
D = f16((H,)); y = f16((B, S, H, P))
44+
45+
def zeros():
46+
return (torch.zeros(B, S, H, N, dtype=torch.float32, device=DEV),
47+
torch.zeros(B, S, H, P, dtype=torch.float32, device=DEV),
48+
torch.zeros(B, S, H, P, dtype=torch.float32, device=DEV),
49+
torch.zeros(B, nchunks, H, P, N, dtype=torch.float32, device=DEV),
50+
torch.zeros(B, S, H, P, N, dtype=torch.float32, device=DEV),
51+
torch.zeros(B, H, nchunks, CH, dtype=torch.float32, device=DEV),
52+
torch.zeros(H, dtype=torch.float32, device=DEV))
53+
54+
OUT = [11, 12, 13, 14, 15, 16, 17]
55+
def compile_prim(prim):
56+
return tilelang.compile(prim, out_idx=OUT, target=None)
57+
def run(kern):
58+
o = zeros()
59+
kern(dout, cb, x, z, dt, dA, C, Bm, prev, D, y, *o)
60+
torch.mps.synchronize()
61+
return o
62+
def timeit(kern, iters=30):
63+
run(kern); torch.mps.synchronize()
64+
t0 = time.perf_counter()
65+
for _ in range(iters):
66+
o = zeros()
67+
kern(dout, cb, x, z, dt, dA, C, Bm, prev, D, y, *o)
68+
torch.mps.synchronize()
69+
return (time.perf_counter() - t0) / iters * 1e3
70+
71+
print(f"[METAL-RETILE] dims B={B} S={S} chunk={CH} G={G} H={H} P={P} N={N} HPC={HPC} SUB={SUB}")
72+
serial_prim = chunk_scan_combine_bwd_metal_prim(B, S, CH, G, H, P, N)
73+
retile_prim = chunk_scan_combine_bwd_metal_gemm_prim_batched(
74+
B, S, CH, G, H, P, N, heads_per_cta=HPC, sub_chunks=SUB)
75+
76+
k_serial = compile_prim(serial_prim)
77+
k_retile = compile_prim(retile_prim)
78+
79+
o_s = run(k_serial)
80+
o_b = run(k_retile)
81+
names = ["dC", "dx", "dz", "dchunk", "dinp", "dA_y", "dD"]
82+
maxabs = {nm: float((a - b).abs().max().cpu()) for nm, a, b in zip(names, o_s, o_b)}
83+
84+
t_s = timeit(k_serial)
85+
t_b = timeit(k_retile)
86+
speedup = t_s / t_b if t_b > 0 else float("nan")
87+
verdict = "GO" if t_b < t_s else "NO-GO"
88+
worst = max(maxabs.values())
89+
parity = "PASS" if worst < 1e-3 else "FAIL"
90+
91+
print(f"[METAL-RETILE] serial={t_s:.3f}ms retile(sub2)={t_b:.3f}ms speedup={speedup:.3f}x {verdict}")
92+
print(f"[METAL-RETILE] parity {parity} worst={worst:.2e} "
93+
+ " ".join(f"{k}={v:.2e}" for k, v in maxabs.items()))
94+
print("RETILE_RESULT_JSON " + str({
95+
"serial_ms": round(t_s, 4), "retile_ms": round(t_b, 4),
96+
"speedup": round(speedup, 4), "verdict": verdict, "sub_chunks": SUB, "HPC": HPC,
97+
"parity": parity, "maxabs": {k: f"{v:.2e}" for k, v in maxabs.items()},
98+
}))

0 commit comments

Comments
 (0)