|
| 1 | +"""ROI 3.7 — head-to-head benchmark matrix + auto-promotion receipt. |
| 2 | +
|
| 3 | +Builds on ``benchmark_receipt.measure_cell`` to run every (block, path) |
| 4 | +combination at a fixed shape and emit: |
| 5 | +
|
| 6 | + 1. A per-cell receipt JSON (same schema as today). |
| 7 | + 2. A matrix-level summary JSON ranking paths by median fwd time. |
| 8 | + 3. An auto-promotion decision: the fastest path becomes the new default |
| 9 | + for that (block, shape) — recorded as ``CPPMEGA_V4_KERNEL_PATH__<...>`` |
| 10 | + env-var hints in the summary so callers can pin them in CI. |
| 11 | +
|
| 12 | +The decision rule is intentionally conservative: |
| 13 | + - A path is eligible only if ``backend_available == True`` and produces |
| 14 | + *finite* output. |
| 15 | + - To win, the candidate's median fwd time over ``--warmup + --iters`` |
| 16 | + measurements must be ≤ ``(1 - delta) * incumbent_best``, where |
| 17 | + ``incumbent`` is the previously-recorded best for the same shape (or |
| 18 | + Path A on first run). ``delta`` defaults to 0.05 (5% margin). |
| 19 | + - On ties (delta not met), the incumbent stays — prevents thrashing. |
| 20 | +
|
| 21 | +This module ships pure-Python: it does not run a 1B-parameter training |
| 22 | +step (that lives in ``scripts/run_v4_benchmark_matrix.py``, layered on |
| 23 | +top). The matrix-runner here is what feeds CI gates and human-readable |
| 24 | +HTML tables. |
| 25 | +""" |
| 26 | + |
| 27 | +import json |
| 28 | +import os |
| 29 | +import statistics |
| 30 | +import time |
| 31 | +from dataclasses import asdict, dataclass, field |
| 32 | +from pathlib import Path |
| 33 | +from typing import Optional |
| 34 | + |
| 35 | +import mlx.core as mx |
| 36 | + |
| 37 | +from cppmega_v4._tilelang.benchmark_receipt import ( |
| 38 | + Block, |
| 39 | + CellReceipt, |
| 40 | + CellShape, |
| 41 | + Path_, |
| 42 | + measure_cell, |
| 43 | + write_receipt, |
| 44 | +) |
| 45 | +from cppmega_v4._tilelang.kda_paths import ENV_VAR as KDA_ENV |
| 46 | +from cppmega_v4._tilelang.linear_attention_paths import ENV_VAR as GDN_ENV |
| 47 | + |
| 48 | +ENV_VAR_FOR_BLOCK: dict[Block, str] = { |
| 49 | + "gdn": GDN_ENV, |
| 50 | + "kda": KDA_ENV, |
| 51 | +} |
| 52 | + |
| 53 | +GDN_PATHS: tuple[Path_, ...] = ("path_a", "path_b", "path_c", "path_d", "path_e") |
| 54 | +KDA_PATHS: tuple[Path_, ...] = ("path_a", "path_b", "path_c", "path_d") |
| 55 | + |
| 56 | + |
| 57 | +@dataclass |
| 58 | +class MatrixCell: |
| 59 | + """One (block, path) measurement across multiple iters.""" |
| 60 | + |
| 61 | + block: Block |
| 62 | + path: Path_ |
| 63 | + median_seconds: float |
| 64 | + iters: int |
| 65 | + backend_available: bool |
| 66 | + backend_reason: str |
| 67 | + output_finite: bool |
| 68 | + |
| 69 | + def to_json(self) -> dict[str, object]: |
| 70 | + return asdict(self) |
| 71 | + |
| 72 | + |
| 73 | +@dataclass |
| 74 | +class PromotionDecision: |
| 75 | + """Auto-promotion outcome for one (block, shape).""" |
| 76 | + |
| 77 | + block: Block |
| 78 | + shape_signature: str |
| 79 | + winning_path: Path_ |
| 80 | + median_seconds: float |
| 81 | + incumbent_path: Path_ |
| 82 | + incumbent_seconds: float |
| 83 | + promotion_applied: bool |
| 84 | + margin_delta: float |
| 85 | + env_var: str |
| 86 | + env_value: str |
| 87 | + |
| 88 | + def to_json(self) -> dict[str, object]: |
| 89 | + return asdict(self) |
| 90 | + |
| 91 | + |
| 92 | +@dataclass |
| 93 | +class MatrixReceipt: |
| 94 | + """Full head-to-head matrix output.""" |
| 95 | + |
| 96 | + shape: CellShape # block field is the block under test |
| 97 | + cells: list[MatrixCell] |
| 98 | + promotion: PromotionDecision |
| 99 | + warmup: int |
| 100 | + iters: int |
| 101 | + timestamp: float = field(default_factory=time.time) |
| 102 | + |
| 103 | + def to_json(self) -> dict[str, object]: |
| 104 | + return { |
| 105 | + "shape": asdict(self.shape), |
| 106 | + "cells": [c.to_json() for c in self.cells], |
| 107 | + "promotion": self.promotion.to_json(), |
| 108 | + "warmup": self.warmup, |
| 109 | + "iters": self.iters, |
| 110 | + "timestamp": self.timestamp, |
| 111 | + } |
| 112 | + |
| 113 | + |
| 114 | +def _shape_signature(shape: CellShape) -> str: |
| 115 | + parts = [ |
| 116 | + f"B{shape.batch}", f"T{shape.seq_len}", f"H{shape.num_heads}", |
| 117 | + f"Dk{shape.head_dim_k}", f"Dv{shape.head_dim_v}", |
| 118 | + ] |
| 119 | + if shape.num_v_heads is not None: |
| 120 | + parts.append(f"HV{shape.num_v_heads}") |
| 121 | + parts.append(shape.dtype) |
| 122 | + return "_".join(parts) |
| 123 | + |
| 124 | + |
| 125 | +def _output_is_finite(receipt: CellReceipt) -> bool: |
| 126 | + """Re-run a forward to check finiteness — cheap on small shapes.""" |
| 127 | + s = receipt.cell_shape |
| 128 | + if s.block == "gdn": |
| 129 | + q = mx.random.normal((s.batch, s.seq_len, s.num_heads, s.head_dim_k)) |
| 130 | + k = mx.random.normal((s.batch, s.seq_len, s.num_heads, s.head_dim_k)) |
| 131 | + v = mx.random.normal((s.batch, s.seq_len, s.num_heads, s.head_dim_v)) |
| 132 | + beta = mx.random.normal((s.batch, s.seq_len, s.num_heads)) |
| 133 | + g = mx.random.normal((s.batch, s.seq_len, s.num_heads)) * 0.1 |
| 134 | + from cppmega_v4._tilelang.linear_attention_paths import ( |
| 135 | + gated_delta_recurrent_dispatch, |
| 136 | + ) |
| 137 | + o, _ = gated_delta_recurrent_dispatch(q, k, v, beta, g) |
| 138 | + else: |
| 139 | + hv = s.num_v_heads or s.num_heads |
| 140 | + q = mx.random.normal((s.batch, s.seq_len, s.num_heads, s.head_dim_k)) |
| 141 | + k = mx.random.normal((s.batch, s.seq_len, s.num_heads, s.head_dim_k)) |
| 142 | + v = mx.random.normal((s.batch, s.seq_len, hv, s.head_dim_v)) |
| 143 | + g = mx.random.normal((s.batch, s.seq_len, hv, s.head_dim_k)) * 0.05 |
| 144 | + beta = mx.random.normal((s.batch, s.seq_len, hv)) |
| 145 | + from cppmega_v4._tilelang.kda_paths import kda_recurrent_dispatch |
| 146 | + o, _ = kda_recurrent_dispatch(q, k, v, g, beta) |
| 147 | + mx.eval(o) |
| 148 | + return not bool(mx.any(mx.isnan(o)).item()) |
| 149 | + |
| 150 | + |
| 151 | +def _measure_path( |
| 152 | + shape_template: CellShape, |
| 153 | + path: Path_, |
| 154 | + *, |
| 155 | + warmup: int, |
| 156 | + iters: int, |
| 157 | +) -> MatrixCell: |
| 158 | + """Warm up + measure ``iters`` forwards through a forced path.""" |
| 159 | + env_var = ENV_VAR_FOR_BLOCK[shape_template.block] |
| 160 | + cell_shape = CellShape(**{**asdict(shape_template), "path": path}) |
| 161 | + samples: list[float] = [] |
| 162 | + prev = os.environ.get(env_var) |
| 163 | + os.environ[env_var] = path |
| 164 | + try: |
| 165 | + for _ in range(warmup): |
| 166 | + r = measure_cell(cell_shape) |
| 167 | + for _ in range(iters): |
| 168 | + r = measure_cell(cell_shape) |
| 169 | + samples.append(r.fwd_seconds) |
| 170 | + finite = _output_is_finite(r) if samples else False |
| 171 | + finally: |
| 172 | + if prev is None: |
| 173 | + os.environ.pop(env_var, None) |
| 174 | + else: |
| 175 | + os.environ[env_var] = prev |
| 176 | + |
| 177 | + return MatrixCell( |
| 178 | + block=shape_template.block, |
| 179 | + path=path, |
| 180 | + median_seconds=statistics.median(samples) if samples else float("inf"), |
| 181 | + iters=iters, |
| 182 | + backend_available=r.backend_available, |
| 183 | + backend_reason=r.backend_reason, |
| 184 | + output_finite=finite, |
| 185 | + ) |
| 186 | + |
| 187 | + |
| 188 | +def _decide_promotion( |
| 189 | + *, |
| 190 | + block: Block, |
| 191 | + shape_signature: str, |
| 192 | + cells: list[MatrixCell], |
| 193 | + incumbent: tuple[Path_, float], |
| 194 | + margin_delta: float, |
| 195 | +) -> PromotionDecision: |
| 196 | + eligible = [ |
| 197 | + c for c in cells if c.backend_available and c.output_finite |
| 198 | + and c.median_seconds < float("inf") |
| 199 | + ] |
| 200 | + if not eligible: |
| 201 | + # Nothing available — keep incumbent. |
| 202 | + winner = incumbent[0] |
| 203 | + win_time = incumbent[1] |
| 204 | + promote = False |
| 205 | + else: |
| 206 | + best = min(eligible, key=lambda c: c.median_seconds) |
| 207 | + # Promote only if best beats incumbent by at least margin_delta. |
| 208 | + threshold = incumbent[1] * (1.0 - margin_delta) |
| 209 | + if best.median_seconds <= threshold or best.path == incumbent[0]: |
| 210 | + winner = best.path |
| 211 | + win_time = best.median_seconds |
| 212 | + promote = winner != incumbent[0] |
| 213 | + else: |
| 214 | + winner = incumbent[0] |
| 215 | + win_time = incumbent[1] |
| 216 | + promote = False |
| 217 | + return PromotionDecision( |
| 218 | + block=block, |
| 219 | + shape_signature=shape_signature, |
| 220 | + winning_path=winner, |
| 221 | + median_seconds=win_time, |
| 222 | + incumbent_path=incumbent[0], |
| 223 | + incumbent_seconds=incumbent[1], |
| 224 | + promotion_applied=promote, |
| 225 | + margin_delta=margin_delta, |
| 226 | + env_var=ENV_VAR_FOR_BLOCK[block], |
| 227 | + env_value=winner, |
| 228 | + ) |
| 229 | + |
| 230 | + |
| 231 | +def run_matrix( |
| 232 | + shape_template: CellShape, |
| 233 | + *, |
| 234 | + warmup: int = 2, |
| 235 | + iters: int = 5, |
| 236 | + incumbent: Optional[tuple[Path_, float]] = None, |
| 237 | + margin_delta: float = 0.05, |
| 238 | +) -> MatrixReceipt: |
| 239 | + """Run all paths for one (block, shape), decide promotion, return receipt. |
| 240 | +
|
| 241 | + ``incumbent`` defaults to ``(path_a, +inf)`` — first ever run will |
| 242 | + install the fastest *available* path with no margin requirement (any |
| 243 | + finite time beats +inf trivially). |
| 244 | + """ |
| 245 | + paths = GDN_PATHS if shape_template.block == "gdn" else KDA_PATHS |
| 246 | + cells = [ |
| 247 | + _measure_path(shape_template, p, warmup=warmup, iters=iters) |
| 248 | + for p in paths |
| 249 | + ] |
| 250 | + sig = _shape_signature(shape_template) |
| 251 | + incumbent = incumbent or ("path_a", float("inf")) |
| 252 | + promotion = _decide_promotion( |
| 253 | + block=shape_template.block, shape_signature=sig, |
| 254 | + cells=cells, incumbent=incumbent, margin_delta=margin_delta, |
| 255 | + ) |
| 256 | + return MatrixReceipt( |
| 257 | + shape=shape_template, cells=cells, promotion=promotion, |
| 258 | + warmup=warmup, iters=iters, |
| 259 | + ) |
| 260 | + |
| 261 | + |
| 262 | +def write_matrix_receipt(receipt: MatrixReceipt, out_dir: Path) -> Path: |
| 263 | + """Drop the matrix receipt under ``out_dir/<block>_matrix_<sig>.json``.""" |
| 264 | + out_dir.mkdir(parents=True, exist_ok=True) |
| 265 | + sig = _shape_signature(receipt.shape) |
| 266 | + fname = f"{receipt.shape.block}_matrix_{sig}.json" |
| 267 | + out_path = out_dir / fname |
| 268 | + out_path.write_text(json.dumps(receipt.to_json(), indent=2)) |
| 269 | + # Also drop the per-cell receipts so existing matrix HTML still renders. |
| 270 | + for cell in receipt.cells: |
| 271 | + cs = CellShape(**{**asdict(receipt.shape), "path": cell.path}) |
| 272 | + write_receipt( |
| 273 | + CellReceipt( |
| 274 | + cell_shape=cs, fwd_seconds=cell.median_seconds, |
| 275 | + backend_available=cell.backend_available, |
| 276 | + backend_reason=cell.backend_reason, |
| 277 | + output_shape=(), output_dtype="float32", |
| 278 | + ), |
| 279 | + out_dir, |
| 280 | + ) |
| 281 | + return out_path |
| 282 | + |
| 283 | + |
| 284 | +__all__ = [ |
| 285 | + "ENV_VAR_FOR_BLOCK", |
| 286 | + "GDN_PATHS", |
| 287 | + "KDA_PATHS", |
| 288 | + "MatrixCell", |
| 289 | + "MatrixReceipt", |
| 290 | + "PromotionDecision", |
| 291 | + "run_matrix", |
| 292 | + "write_matrix_receipt", |
| 293 | +] |
0 commit comments