Skip to content

Commit 1cd8289

Browse files
committed
test(fp8): align async owner-output assertion to live native bridge + add v3 Metal matrix driver
test_fp8_vecmat_direct_path_uses_owner_output_without_mlx_fast_fallback: the async owner-output request is dropped under the now-live native bridge (it returned a fresh wrapper aliasing out, breaking 'returned is out'; the non-native route ran synchronously regardless), so assert the flag is absent rather than True. Add scripts/run_metal_speed_matrix_20260531.py (v3 Metal 1B matrix driver, companion to reports/cppmega_1b_speed_matrix_metal_20260531).
1 parent 4b4f940 commit 1cd8289

2 files changed

Lines changed: 397 additions & 1 deletion

File tree

Lines changed: 391 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,391 @@
1+
#!/usr/bin/env python3
2+
"""Local Metal (Apple M4 Max) 1B speed matrix driver — 20260531 campaign.
3+
4+
Reuses the exact per-cell m04 invocation shape of
5+
``scripts/bench_1b_training_matrix.py`` but bounds each cell with an OS-level
6+
1800s timeout and emits BOTH Markdown and HTML matrices. Cells run as fresh
7+
subprocesses; failures surface the real reason (fail-loud, never dropped).
8+
"""
9+
10+
from __future__ import annotations
11+
12+
import json
13+
import os
14+
import subprocess
15+
import sys
16+
import time
17+
from pathlib import Path
18+
19+
ROOT = Path(__file__).resolve().parents[1]
20+
TARGET_PARQUET = (
21+
ROOT / "data" / "parquet_samples" / "gb10" / "clang_semantic_4k_v10" / "val_00000.parquet"
22+
)
23+
WORK_DIR = Path("/tmp/cppmega_1b_speed_matrix_metal_20260531_cells")
24+
CACHE_ROOT = Path("/tmp/cppmega_1b_speed_matrix_metal_20260531_tilelang_cache")
25+
MD_OUT = ROOT / "reports" / "cppmega_1b_speed_matrix_metal_20260531.md"
26+
HTML_OUT = ROOT / "reports" / "cppmega_1b_speed_matrix_metal_20260531.html"
27+
28+
PYTHON = sys.executable # cppmega .venv python (python3.13)
29+
TIMEOUT_BIN = "/opt/homebrew/bin/timeout"
30+
CELL_TIMEOUT_S = 1800
31+
32+
DTYPES = ("bf16", "fp8")
33+
# request: adamw/adam8bit, lion/lion8bit, muon/muon_int8
34+
OPTIMIZERS = ("adamw", "adam8bit", "lion", "lion8bit", "muon", "muon_int8")
35+
PATHS = ("path_b", "path_c") # path_c == warm direct-chain route
36+
37+
# optimizer -> m04 --optimizer arg (mirrors bench harness optimizer_map)
38+
OPT_CLI = {
39+
"adamw": "adamw",
40+
"adam8bit": "adam8bit",
41+
"lion": "lion",
42+
"lion8bit": "lion8bit",
43+
"muon": "muon",
44+
"muon_int8": "int8",
45+
}
46+
47+
48+
def dtype_arg(dtype: str, path: str) -> str:
49+
if dtype == "bf16":
50+
return "bfloat16"
51+
return "fp8_path_b" if path == "path_b" else "fp8_path_c"
52+
53+
54+
def cell_env(dtype: str, path: str) -> dict[str, str]:
55+
env = dict(os.environ)
56+
kernel_path = "path_b" if path == "path_b" else "path_c"
57+
env["CPPMEGA_KERNEL_PATH"] = kernel_path
58+
env["CPPMEGA_KERNEL_PATH__MAMBA3_MIMO"] = kernel_path
59+
env["CPPMEGA_KERNEL_PATH__M2RNN"] = kernel_path
60+
env["CPPMEGA_KERNEL_PATH__SPARSE_MLA"] = kernel_path
61+
if path == "path_c":
62+
# SPLIT/WARM route: Path C forward + Path B backward (mamba3 bwd=path_b),
63+
# exactly matching the prior local matrix's `path_c_warm` cells. This is
64+
# the performance-safe route; full Path-C backward (mamba3 bwd=path_c) is
65+
# a separate experiment and is NOT what path_c_warm measured.
66+
env["CPPMEGA_MAMBA3_PATH_C_BWD"] = "path_b"
67+
if dtype == "fp8":
68+
env["CPPMEGA_SPARSE_MLA_FP8_ROUTE"] = "path_c"
69+
env["CPPMEGA_SPARSE_MLA_FP8_BWD"] = "path_c"
70+
elif dtype == "fp8":
71+
env["CPPMEGA_SPARSE_MLA_FP8_ROUTE"] = "path_b"
72+
# warm cache: persistent per (dtype,optimizer) cache dir so path_c is warm
73+
return env
74+
75+
76+
def git_sha(repo: Path) -> str:
77+
try:
78+
out = subprocess.run(
79+
["git", "-C", str(repo), "rev-parse", "--short", "HEAD"],
80+
capture_output=True, text=True, timeout=15,
81+
)
82+
return out.stdout.strip() or "unknown"
83+
except Exception as exc: # noqa: BLE001 - identity stamp only
84+
return f"unknown ({exc})"
85+
86+
87+
def build_command(dtype: str, optimizer: str, path: str, out_json: Path) -> list[str]:
88+
cmd = [
89+
TIMEOUT_BIN, str(CELL_TIMEOUT_S),
90+
PYTHON, "scripts/m04_train_step.py",
91+
"--model-profile", "local_gb10_quarter",
92+
"--data-path", str(TARGET_PARQUET.relative_to(ROOT)),
93+
"--data-format", "parquet",
94+
"--token-key", "token_ids",
95+
"--steps", "10",
96+
"--batch-size", "1",
97+
"--seq-len", "512",
98+
"--dtype", dtype_arg(dtype, path),
99+
"--optimizer", OPT_CLI[optimizer],
100+
"--optimizer-quant-scheme", "dynamic_int8_v1",
101+
"--lr", "1e-4",
102+
"--grad-checkpoint",
103+
"--output", str(out_json),
104+
"--json",
105+
]
106+
# NOTE: path_c here == SPLIT/WARM route (Path C fwd + Path B bwd), matching
107+
# the prior local matrix's `path_c_warm`. We deliberately do NOT pass
108+
# --use-path-c-direct-chain-runtime (direct-chain training runtime is not
109+
# implemented on Metal and fails-closed to status=blocked).
110+
return cmd
111+
112+
113+
def _structured_reason(receipt: dict, status: str) -> str | None:
114+
"""Extract the real, structured Path C / training-route blocker reason.
115+
116+
Avoids dumping TileLang compile logs into the matrix cell; surfaces the
117+
m04 contract status fields that actually explain a blocked/failed cell.
118+
"""
119+
training = receipt.get("training")
120+
if not isinstance(training, dict):
121+
return None
122+
route = training.get("fp8_path_c_training_route")
123+
if not isinstance(route, dict):
124+
return None
125+
# Direct-chain runtime contract (the --use-path-c-direct-chain-runtime route)
126+
dc_contract = route.get("direct_fusion_chain_training_runtime_contract")
127+
if isinstance(dc_contract, dict) and dc_contract.get("status") not in (None, "ok", "ready"):
128+
fusion = route.get("path_c_fusion") or {}
129+
dc = fusion.get("direct_chained_fusion") if isinstance(fusion, dict) else {}
130+
rb = dc.get("runtime_binding") if isinstance(dc, dict) else {}
131+
mba = dc.get("model_binding_audit") if isinstance(dc, dict) else {}
132+
parts = [
133+
f"direct-chain training runtime not implemented "
134+
f"(contract={dc_contract.get('status')}",
135+
f"runtime_binding={rb.get('status') if isinstance(rb, dict) else None}",
136+
f"model_binding={mba.get('status') if isinstance(mba, dict) else None})",
137+
]
138+
avail = route.get("end_to_end_training_status")
139+
if avail:
140+
parts.append(f"split route available={avail}")
141+
return "; ".join(parts)
142+
# Generic route-level status if blocked
143+
if status == "blocked":
144+
sel = route.get("selected_action") or route.get("status")
145+
e2e = route.get("end_to_end_training_status")
146+
if sel or e2e:
147+
return f"blocked: selected_action={sel}, end_to_end={e2e}"
148+
return None
149+
150+
151+
def parse_receipt(receipt: dict, returncode: int, stderr: str, stdout: str) -> dict:
152+
status = str(receipt.get("status") or "")
153+
timing = receipt.get("timing") if isinstance(receipt.get("timing"), dict) else {}
154+
memory = receipt.get("memory") if isinstance(receipt.get("memory"), dict) else {}
155+
training = receipt.get("training") if isinstance(receipt.get("training"), dict) else {}
156+
step_times = list(timing.get("step_times_s") or [])
157+
first_step = float(step_times[0]) if step_times else None
158+
steady = [float(v) for v in step_times[1:]] if len(step_times) > 1 else []
159+
step_dur = (sum(steady) / len(steady)) if steady else (
160+
float(timing["mean_step_time_s"]) if timing.get("mean_step_time_s") is not None else None
161+
)
162+
# "step/s" column == steps per second (matches prior LOCAL matrix semantics)
163+
step_sec = (1.0 / step_dur) if step_dur and step_dur > 0 else None
164+
tok_sec = float(timing["tokens_per_second"]) if timing.get("tokens_per_second") is not None else None
165+
peak_bytes = int(memory["peak_memory_bytes"]) if memory.get("peak_memory_bytes") is not None else None
166+
peak_gb = (peak_bytes / (1024 ** 3)) if peak_bytes is not None else None
167+
168+
reason = "ok"
169+
if status != "ok":
170+
reason = _structured_reason(receipt, status)
171+
if reason is None:
172+
for key in ("status_reason", "failure_reason", "reason", "error", "message", "title"):
173+
v = receipt.get(key)
174+
if v:
175+
reason = str(v)
176+
break
177+
else:
178+
# last resort: short tail of stderr/stdout, NOT a full compile-log dump
179+
tail = (stderr.strip() or stdout.strip() or f"cell exited {returncode}")
180+
reason = tail[-300:]
181+
elif training.get("all_finite") is False:
182+
status = "failed"
183+
reason = "training reported non-finite values"
184+
return {
185+
"status": status or "failed",
186+
"tok_sec": tok_sec,
187+
"step_sec": step_sec,
188+
"compile_s": first_step,
189+
"peak_gb": peak_gb,
190+
"reason": reason,
191+
}
192+
193+
194+
def run_cell(dtype: str, optimizer: str, path: str) -> dict:
195+
case_id = f"{dtype}_{optimizer}_{path}"
196+
out_json = WORK_DIR / f"{case_id}.json"
197+
if out_json.exists():
198+
out_json.unlink()
199+
cmd = build_command(dtype, optimizer, path, out_json)
200+
env = cell_env(dtype, path)
201+
# warm per-(dtype,optimizer) tilelang cache so path_c measures warm route
202+
cache_dir = CACHE_ROOT / f"{dtype}_{optimizer}"
203+
cache_dir.mkdir(parents=True, exist_ok=True)
204+
env["TILELANG_CACHE_DIR"] = str(cache_dir)
205+
206+
t0 = time.perf_counter()
207+
proc = subprocess.run(cmd, cwd=str(ROOT), env=env, capture_output=True, text=True)
208+
wall = time.perf_counter() - t0
209+
210+
receipt: dict = {}
211+
if out_json.exists():
212+
try:
213+
receipt = json.loads(out_json.read_text())
214+
except Exception as exc: # noqa: BLE001
215+
receipt = {"status": "failed", "reason": f"unparseable receipt: {exc}"}
216+
217+
if proc.returncode == 124 and not receipt:
218+
result = {
219+
"status": "timeout",
220+
"tok_sec": None, "step_sec": None, "compile_s": None, "peak_gb": None,
221+
"reason": f"cell exceeded {CELL_TIMEOUT_S}s OS timeout (rc=124)",
222+
}
223+
elif not receipt:
224+
tail = (proc.stderr.strip() or proc.stdout.strip() or "")[-600:]
225+
result = {
226+
"status": "failed",
227+
"tok_sec": None, "step_sec": None, "compile_s": None, "peak_gb": None,
228+
"reason": f"no JSON receipt (rc={proc.returncode}): {tail}",
229+
}
230+
else:
231+
result = parse_receipt(receipt, proc.returncode, proc.stderr, proc.stdout)
232+
233+
cache_hit = receipt.get("path_c_warm_cache_hit_observed")
234+
result.update({
235+
"dtype": dtype, "optimizer": optimizer, "path": path,
236+
"case_id": case_id, "wall_s": wall, "returncode": proc.returncode,
237+
"cache_hit": cache_hit,
238+
"command": " ".join(cmd),
239+
})
240+
return result
241+
242+
243+
def fmt(v, nd=3):
244+
if v is None:
245+
return ""
246+
if isinstance(v, float):
247+
return f"{v:.{nd}g}"
248+
return str(v)
249+
250+
251+
def write_markdown(results, identity):
252+
lines = []
253+
lines.append("# cppmega 1B Speed Matrix — Local Metal (Apple M4 Max)")
254+
lines.append("")
255+
lines.append(f"- Date: 20260531")
256+
lines.append(f"- Host: local Mac M4 Max (Metal)")
257+
lines.append(f"- Model profile: local_gb10_quarter")
258+
lines.append(f"- Data: {TARGET_PARQUET.relative_to(ROOT)} (parquet, token_ids)")
259+
lines.append(f"- Settings: seq-len 512, batch 1, --steps 10 warm, --grad-checkpoint, --optimizer-quant-scheme dynamic_int8_v1")
260+
lines.append(f"- Path C route: SPLIT/WARM (Path C fwd + Path B bwd, mamba3 bwd=path_b); matches prior `path_c_warm`; NO --use-path-c-direct-chain-runtime")
261+
lines.append(f"- Per-cell bound: {CELL_TIMEOUT_S}s OS timeout (fail-loud)")
262+
lines.append(f"- cppmega SHA: `{identity['cppmega_sha']}`")
263+
lines.append(f"- TileLang SHA: `{identity['tilelang_sha']}` (bf16 AtomicAdd fix 3fcfc21c live)")
264+
lines.append("")
265+
header = "| dtype | optimizer | path | status | tok/s | step/s | compile s | peak GB | cache hit | reason |"
266+
sep = "| ----- | --------- | ---- | ------ | ----: | -----: | --------: | ------: | --------- | ------ |"
267+
lines.append(header)
268+
lines.append(sep)
269+
for r in results:
270+
lines.append(
271+
f"| {r['dtype']} | {r['optimizer']} | {r['path']} | {r['status']} | "
272+
f"{fmt(r['tok_sec'])} | {fmt(r['step_sec'])} | {fmt(r['compile_s'])} | "
273+
f"{fmt(r['peak_gb'])} | {fmt(r['cache_hit'])} | {r['reason']} |"
274+
)
275+
lines.append("")
276+
lines.append("## Per-Cell Commands")
277+
lines.append("")
278+
for r in results:
279+
lines.append(f"- `{r['case_id']}`: `{r['command']}`")
280+
lines.append("")
281+
MD_OUT.parent.mkdir(parents=True, exist_ok=True)
282+
MD_OUT.write_text("\n".join(lines))
283+
284+
285+
def _esc(s):
286+
return (str(s).replace("&", "&amp;").replace("<", "&lt;").replace(">", "&gt;"))
287+
288+
289+
def write_html(results, identity):
290+
rows = []
291+
for r in results:
292+
cls = "ok" if r["status"] == "ok" else "bad"
293+
rows.append(
294+
f'<tr class="{cls}"><td>{_esc(r["dtype"])}</td><td>{_esc(r["optimizer"])}</td>'
295+
f'<td>{_esc(r["path"])}</td><td>{_esc(r["status"])}</td>'
296+
f'<td class="num">{_esc(fmt(r["tok_sec"]))}</td>'
297+
f'<td class="num">{_esc(fmt(r["step_sec"]))}</td>'
298+
f'<td class="num">{_esc(fmt(r["compile_s"]))}</td>'
299+
f'<td class="num">{_esc(fmt(r["peak_gb"]))}</td>'
300+
f'<td>{_esc(fmt(r["cache_hit"]))}</td><td>{_esc(r["reason"])}</td></tr>'
301+
)
302+
html = f"""<!DOCTYPE html>
303+
<html lang="en"><head><meta charset="utf-8">
304+
<title>cppmega 1B Speed Matrix — Local Metal (M4 Max) 20260531</title>
305+
<style>
306+
body {{ font-family: -apple-system, system-ui, sans-serif; margin: 2rem; color: #1a1a1a; }}
307+
h1 {{ font-size: 1.3rem; }}
308+
ul.meta {{ font-size: 0.85rem; color: #444; line-height: 1.5; }}
309+
table {{ border-collapse: collapse; width: 100%; font-size: 0.85rem; }}
310+
th, td {{ border: 1px solid #ccc; padding: 4px 8px; text-align: left; }}
311+
th {{ background: #2d2d2d; color: #fff; }}
312+
td.num {{ text-align: right; font-variant-numeric: tabular-nums; }}
313+
tr.ok td:nth-child(4) {{ color: #0a7d28; font-weight: 600; }}
314+
tr.bad td:nth-child(4) {{ color: #b40000; font-weight: 700; }}
315+
tr:nth-child(even) {{ background: #f6f6f6; }}
316+
</style></head><body>
317+
<h1>cppmega 1B Speed Matrix — Local Metal (Apple M4 Max)</h1>
318+
<ul class="meta">
319+
<li>Date: 20260531 &middot; Host: local Mac M4 Max (Metal) &middot; Profile: local_gb10_quarter</li>
320+
<li>seq-len 512, batch 1, --steps 10 warm, --grad-checkpoint, dynamic_int8_v1</li>
321+
<li>Path C: SPLIT/WARM (Path C fwd + Path B bwd, mamba3 bwd=path_b) &middot; matches prior path_c_warm &middot; per-cell bound {CELL_TIMEOUT_S}s</li>
322+
<li>cppmega SHA: <code>{_esc(identity['cppmega_sha'])}</code> &middot; TileLang SHA: <code>{_esc(identity['tilelang_sha'])}</code> (bf16 AtomicAdd fix 3fcfc21c live)</li>
323+
</ul>
324+
<table>
325+
<thead><tr><th>dtype</th><th>optimizer</th><th>path</th><th>status</th><th>tok/s</th><th>step/s</th><th>compile s</th><th>peak GB</th><th>cache hit</th><th>reason</th></tr></thead>
326+
<tbody>
327+
{chr(10).join(rows)}
328+
</tbody></table>
329+
</body></html>
330+
"""
331+
HTML_OUT.parent.mkdir(parents=True, exist_ok=True)
332+
HTML_OUT.write_text(html)
333+
334+
335+
def regen():
336+
"""Rebuild md+html from existing per-cell receipts (no re-run)."""
337+
identity = {
338+
"cppmega_sha": git_sha(ROOT),
339+
"tilelang_sha": git_sha(Path("/Volumes/external/sources/tilelang")),
340+
}
341+
results = []
342+
cells = [(d, o, p) for d in DTYPES for o in OPTIMIZERS for p in PATHS]
343+
for d, o, p in cells:
344+
case_id = f"{d}_{o}_{p}"
345+
rj = WORK_DIR / f"{case_id}.json"
346+
receipt = json.loads(rj.read_text()) if rj.exists() else {}
347+
parsed = parse_receipt(receipt, 0, "", "") if receipt else {
348+
"status": "failed", "tok_sec": None, "step_sec": None,
349+
"compile_s": None, "peak_gb": None, "reason": "receipt missing on regen",
350+
}
351+
parsed.update({
352+
"dtype": d, "optimizer": o, "path": p, "case_id": case_id,
353+
"cache_hit": receipt.get("path_c_warm_cache_hit_observed"),
354+
"command": " ".join(build_command(d, o, p, rj)),
355+
})
356+
results.append(parsed)
357+
write_markdown(results, identity)
358+
write_html(results, identity)
359+
print(f"REGEN DONE. md={MD_OUT}\nhtml={HTML_OUT}", flush=True)
360+
361+
362+
def main():
363+
if "--regen" in sys.argv:
364+
regen()
365+
return
366+
WORK_DIR.mkdir(parents=True, exist_ok=True)
367+
CACHE_ROOT.mkdir(parents=True, exist_ok=True)
368+
identity = {
369+
"cppmega_sha": git_sha(ROOT),
370+
"tilelang_sha": git_sha(Path("/Volumes/external/sources/tilelang")),
371+
}
372+
results = []
373+
cells = [(d, o, p) for d in DTYPES for o in OPTIMIZERS for p in PATHS]
374+
for i, (d, o, p) in enumerate(cells, 1):
375+
print(f"[{i}/{len(cells)}] running {d}_{o}_{p} ...", flush=True)
376+
r = run_cell(d, o, p)
377+
print(f" -> status={r['status']} tok/s={fmt(r['tok_sec'])} "
378+
f"peak_gb={fmt(r['peak_gb'])} wall={r['wall_s']:.1f}s reason={r['reason'][:80]}",
379+
flush=True)
380+
results.append(r)
381+
# write partial outputs after every cell so progress is durable
382+
write_markdown(results, identity)
383+
write_html(results, identity)
384+
(WORK_DIR / "_results.json").write_text(json.dumps(results, indent=2))
385+
write_markdown(results, identity)
386+
write_html(results, identity)
387+
print(f"\nDONE. md={MD_OUT}\nhtml={HTML_OUT}", flush=True)
388+
389+
390+
if __name__ == "__main__":
391+
main()

0 commit comments

Comments
 (0)