Skip to content

Commit 98ef7b2

Browse files
committed
feat(v4): ROI 3.7 — head-to-head benchmark matrix + auto-promotion receipt
cppmega_v4._tilelang.benchmark_matrix wraps measure_cell into a matrix-level runner that: 1. Iterates every path (GDN: A/B/C/D/E; KDA: A/B/C/D) at one shape. 2. Records median fwd time over (warmup, iters). 3. Verifies output is finite (NaN→ineligible). 4. Decides promotion via the rule: eligible ⇔ backend_available AND output_finite promote ⇔ best.median ≤ incumbent.median * (1 - delta) AND best.path != incumbent.path delta=0.05 default — prevents thrashing on noise. 5. Emits MatrixReceipt JSON ({shape, cells[], promotion, warmup, iters}) plus per-cell JSONs (matches existing reports/raw/ schema). Promotion outcome is encoded as an env-var hint (CPPMEGA_V4_KERNEL_PATH__LINEAR_ATTENTION / __KDA) so CI can pin the winner without code changes. Conservative on ties (incumbent stays) and on unavailability (no demotion to broken backends). Tests (11 new): every decision-rule branch (within-margin no-op, unavailable skip, non-finite skip, no-eligible keeps incumbent, incumbent stable when winner equals it, candidate beats by margin), plus matrix runner end-to-end (GDN 5 paths, KDA 4 paths, winner is available, JSON round-trip with both matrix & per-cell receipts). v4 suite: 233 passed / 2 skipped.
1 parent ab7612a commit 98ef7b2

2 files changed

Lines changed: 473 additions & 0 deletions

File tree

Lines changed: 293 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,293 @@
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

Comments
 (0)