|
| 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("&", "&").replace("<", "<").replace(">", ">")) |
| 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 · Host: local Mac M4 Max (Metal) · 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) · matches prior path_c_warm · per-cell bound {CELL_TIMEOUT_S}s</li> |
| 322 | + <li>cppmega SHA: <code>{_esc(identity['cppmega_sha'])}</code> · 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