One file per measured session, newest first. Compute tables only (S28 rule — wall legs retired; flow = FLOW_PERF kernel timer vs baselines' self-timed iteration). Ratios are vs flow only, derivation shown. CPU legs move with each box's CPU — compare within one session file, never across. numpy is in every verdict table (local: Accelerate-AMX labeled; box: OpenBLAS).
| Session | Box | What changed | File |
|---|---|---|---|
| S42 (2026-07-30/31) | M4 Pro (SME) + i9-14900F | The SME gap is operand cache residency, worth ~1.79× — and one wrong constant cost the session. loadcost.c holds the compute constant (4 fmopa/iteration) and varies only the operand source: L1-resident 1956.7 → 1864.2 GFLOP/s at 4 loads (95%), past-L2 1915.5 → 760.8 (40%). Loads cost 5% when they hit L1 and halve throughput at the first miss, so the load COUNT is nearly free and 1-load-per-fmopa is not a ceiling. Emitted kernel 1043 vs 1864 if operands were resident vs Accelerate 1655 ⇒ the headroom would pass Accelerate. Matrix units measured at exactly 2 (units.c: 1→1997.8, 2→3849.6 (1.93×), flat at ~4100 from 3 threads on), replacing S41b's inference. KC blocking built, swept, default-OFF: sme_kc derived 512 which measured 0.785×; the swept optimum is 1024 (+6.1% 1t at 4096, −25.5% threaded) — four write-ups had concluded "KC loses" from the single wrong depth. Box (i9-14900F, AVX2, 2 MB L2/core) swept too: kc_nest is a step function, ~0.55× at 1t and ~0.77× at best threaded at every depth, closing S29's "unmeasured on the box". Shipped: f32_tiles deleted (ISA rule), SVL/L1D/L2 detected via sysctl with --target=native byte-identical to the hand-written profile, SME b-addressing from recorded facts, A-pack reordered (spills 51→5). Cross-ISA values identical. Gate 1031/0, 159/159 + 636/636 byte-identical |
s42-sme-roofline.md |
| S41 (2026-07-29) | M4 Pro (SME) | ARM SME rung on matmul, f32. 1t: 512 2.2128 → 0.7451, 1024 17.9611 → 5.4102, 2048 157.957 → 40.448, 4096 1243.987 → 332.536 ms (2.97–3.91×). Threaded: 1024 2.2372 → 0.9715, 2048 18.6854 → 7.6156, 4096 151.0823 → 79.1425 ms (1.91–2.45×). NumPy 1t 0.160 / 1.2977 / 10.529 / 84.617 — gap 13.5× → 4.17× at 1024, 14.8× → 3.93× at 4096. NumPy mt 0.6757 / 5.3045 / 44.143 — gap 3.3× → 1.44× at 1024, 3.4× → 1.79× at 4096. GFLOP/s 1t 360/397/425/413 vs NumPy 1678/1655/1632/1624 — both flat. GFLOP/s threaded 1061/2210/2256/1737 — falls at 4096. NEON control reproduces S33 (17.9611 vs 17.5449; 151.08 vs 151.24). Values identical both legs, distributions disjoint except 512 threaded | s41-sme.md |
| S33 (2026-07-26) | M4 Pro + i9-14900F — both machines, full suite | The conv2d "per-core gap" was a measurement boundary, and Flow's GEMM reaches OpenBLAS on AVX2. (1) flow_rt_alloc handed back a reserved address range, not memory; the first store to each page faulted inside whatever () -> time region wrote first, and conv2d's output is first written by the convolution itself. The C++ baseline pre-pays it (std::vector value-initialises above its timer). Exact differenced counters: Flow's kernel 905,100 cycles / 299,221 ref-cycles / IPC 2.25 vs C++'s 1,072,928 / 382,489 / IPC 1.78 — 18% fewer cycles, and ref-cycles predicted the post-fix window (0.150 vs measured 0.144) before the fix was written. Fixed by reside; same-session A/B: conv2d 1.72–1.89x (M4) / 1.00–1.35x (i9), fir 1.08–1.36x, matmul 1.12→1.003x as N grows — the effect scales as output ÷ kernel, so matmul is immune, and it is platform-dependent: conv2d 512 gains 1.89x on macOS's 16 KiB pages and exactly nothing on Linux, where a 1 MB output hides inside a 2 MiB huge page its neighbours already faulted. conv2d now 1.21x ahead of cpp-1t on both NEON and AVX2; on the i9 Flow wins every shape at every size against every baseline incl. numpy. (2) The AMX question settled by cross-machine measurement: at 1024² f32 threaded, M4 numpy (AMX) is 3.3x ahead of Flow but i9 numpy (OpenBLAS/AVX2) is dead even (1.506 vs 1.526) — the M4 matmul deficit is silicon, not code generation. Parity, not victory, and stated in full: 1t is a flat 1.20x behind at 1024/2048/4096 (146 vs 174 GFLOP/s, both size-invariant ⟹ a steady micro-kernel deficit, not a blocking failure); threaded is within ±10%, Flow ahead only at 2048 (1.08x) and behind at 512 (1.24x) and 4096 (1.06x). Confound ruled out: OpenBLAS default = OPENBLAS_NUM_THREADS=32 (1.5036 vs 1.5055), so it does use the whole machine. Flow scales 9.2–9.8x vs OpenBLAS's 7.1–8.1x, but the "better scheduler" reading was TESTED and REFUTED: same box, same binaries, 8 threads per cell, only CPU uniformity varying — 8 E-cores uniform → numpy 5% ahead; 8 P-cores uniform → numpy 41% ahead; 4P+4E mixed → flow 1.65x ahead. OpenBLAS wins on uniform cores; Flow wins only on mixed. numpy's own column shows the mechanism: 8E 5.591 → 4P+4E 5.573, so four 35%-faster cores bought it nothing (static partitioning waits on the slowest), while Flow went 5.894 → 3.384. Heterogeneity tolerance, not a better scheduler — real (consumer CPUs are hybrid) but not a claim about servers. The 1t gap ran on the untuned generic profile: the honest remaining target. (3) P0 — a help-first race in flow_par_wait lets the host execute work past the watermark it waits on, so a kernel can finish before its own clock starts: 3–4% of threaded runs self-time far too low (FLOW_PAR=1: 0/100). This inverts "quote min, never median" — frequency ramp makes slow outliers, this race makes fast ones. Pre-existing (PRE 3/100, POST 3/100); every par minimum in S28–S32 is suspect and the S32 scheduling verdict needs re-confirming |
matmul/s33.md |
| S32a (2026-07-25) | LOCAL ONLY (M4 Pro) | Slice size is the dominant scheduling knob, and i_reuse predicts its direction. Step 1 of plan-s32 split the pool's two collapsed knobs (width, grain); sweeping grain at a fixed 14 lanes: conv2d 0.460 → 0.273 (coarsest wins — its sliding read pays a halo per boundary), matmul512 0.750 → 0.422 and matmul1024 3.627 → 2.485 (finest wins — ci == 0, no halo). Opposite gradients, one recorded fact predicts the sign. Two of three beat every previous number at full width, including all width tuning; "conv2d wants 4 threads" was always "conv2d wants 4 slices". Defaults leave 1.46–1.78× on the floor |
matmul/s31.md |
| S31c (2026-07-25) | LOCAL ONLY (M4 Pro) | Deduced row blocking — i_reuse shipped, and it pays. ci == 0 (matmul) and ci == cq (conv) become one predicate at q=0/q=1, so conv is blocked because the record says its read slides, not because it is conv. conv2d 1t −18%/−16% on top of the accumulator: 0.4740 → 0.3992 @1024, session total 0.5343 → 0.3992 = −25%, closing cpp-1t from 2.09× to 1.56×. FMA:load 0.80 → 1.20 in the disassembly (+50% arithmetic intensity = the deduced 2.0× reuse). Row blocking beat the accumulator (~17% vs ~10%) — the reverse of the plan's IR op-count prediction, in both terms. Par unchanged: optimum is still 4 threads (0.235) vs 0.411 at 14, so conv2d's remaining deficit is majority scheduling |
matmul/s31.md |
| S31b (2026-07-25) | LOCAL ONLY (M4 Pro) | conv main tile on vector accumulators — measured, and it kills the prediction that ordered the work. ~10% at 1t (conv2d_1024 0.534 → 0.481, conv2d_512 −11%), not the ~2.9× the plan projected from an emitted-IR memory-op count: LLVM was already promoting that accumulator (S29 said so) and the disassembly barely moves. Against competitors 1t: fir wins everything (6.8×/7.3×/3.8× over cpp/rust/numpy), conv2d 1.90× behind cpp-1t. Par: fir 3.2× ahead of cpp-mt; conv2d 3.3× behind. Decomposed: at conv2d's optimal width (4 threads) it is 1.83× behind cpp-mt — same as its 1t gap — while the default width alone costs 1.46×, so the deduced thread count is worth about as much as row blocking for this shape | matmul/s31.md |
| S31 (2026-07-25) | LOCAL ONLY (M4 Pro) | TargetProfile shipped — and the KC nest measured in the regime its own derived constant reopens. Six emitter literals became one named table plus arithmetic; generic reproduces every one, so the shipping path is byte-identical (66 A/B emissions) and every S29 competitor number stands unchanged by construction. New shape M=N=1024 K=8192 f32 (A = 33.6 MB = 2× L2) to cross apple-m's derived tile_kc = 4096: KC still LOSES — par +6.5%, 1t +4.5% vs off — but the derived depth cuts the penalty 3× against the literal (1t +14.0% at kc=128 → +4.5% at kc=4096; 64 park/reload round-trips become 2). The profile made a wrong rung less wrong, not right; kc_nest stays default-OFF for a second measured reason. Assembly verified: the derived panel is what codegen reserves (sub x9, sp, #0x10, lsl #12 = 65,536 B = TI 4 × KC 4096 × 4, 64-byte aligned, vs 2,848 B for the literal), and zero str q …, [sp] in all three legs — S30's register-resident accumulators intact. Outputs byte-equal across legs |
matmul/s31.md |
| S29 (2026-07-25) | LOCAL ONLY (M4 Pro; box leg NOT run — the KC verdict below is local) | The KC k-panel nest measured and gated OFF: a 3× LOSS at 1024 f32 (fma 59.8 ms with / 19.8 without; the OFF column reproduces S28's 18.9, so the tree is intact). The (jc,kc,ic) order forces partial sums to park in out once per k-panel, and that counterweight beats the ~250 MB of A re-reads it removes at this size. Kept, tested, bit-exact, EmitOpts::kc_nest default-off — it was designed for box-scale traffic (4096 on zen3) and is unmeasured there. First honest kernel-only shape numbers, via the new time builtin (benches/shapes/*.flow self-time; --perf retired there): fir wins both tables at both sizes — 1M fma-par 0.402 vs cpp-mt 1.462 (3.6×), numpy 6.368 (15.8×), cpp-1t 11.395 (28×); conv2d beats cpp-mt at 512 (0.083 vs 0.112) and LOSES 3.4× at 1024 (0.445 vs 0.133) — the TI=1 row-blocking ceiling (#11) is now measured, not predicted. Corrects S28: the "conv kernel ≈0.04 = 3× over cpp-mt" figure was a subtraction, not a measurement; the real 512 kernel is 0.083/0.107, ahead by 1.3×. Heap lowering shipped (HEAP_MIN_BYTES 256 KB → flow_rt_alloc): matmul2048_cap_f32 runs locally for the first time (was SIGSEGV on the 64 MB macOS stack). Full workspace gate green |
matmul/s29.md |
| S28 (2026-07-24) | LOCAL shapes (M4 Pro) + S27 box matrix on EPYC 7B13 zen3 64-core, on-demand (45712913, destroyed ≈$0.45) — the "balance 0" block was an agent misread (credit+autobill funded; box #1 vanished mid-run, relaunched on-demand) | Shapes ladder generalized (Sapir S28 focus): FIR 1-D window rung (emit_tiled_map_blocked_1d — TI×TJ blocks over the lane axis, shared scalar w[k], constant-TJ main; the rung-2 dual) + conv2d k-split micro-kernel (tile_plan records the fold's (k÷3,k%3) derived axes — TileRead.ksplit; emission unrolls 9 taps, zero div/mod). fir: both tables WON local + box (local fma-par 0.213 vs cpp-mt 0.239; box 0.287@16T vs numpy-1t 1.39). conv2d: kernel 3× over cpp-mt; box par table won at the leg level. Box matrix: disasm gates pass (conf 0 vfmadd / fma 128 vfmadd 0 unfused — S26 finding closed); 2048/4096 flow rows clean; OpenBLAS frontier measured: threaded 9.7×/5.9× ahead @1024/@4096, 1t 2.7× ahead of flow's fma-1t wall (agenda-2 target); GRAIN quantization measured (fir 61T 0.53 → 16T 0.29 — 16 slices = 0.26 waves @61T). Full workspace gate green; matmul .ll artifacts byte-identical |
matmul/s28.md · raw results-s27.csv |
| S27 (2026-07-24) | LOCAL ONLY (M-series arm64; box leg ran in S28 — see above) | FMA contraction (product face) + BLAS rung 3 packing + micro-kernel finishing — per-instruction contract flags (the LLVM≥14 driver-flag-never-retrofits fact, golden-verified: 0 → 34 fused, 0 unfused left); packed j-tile-major b panels + per-width TJ (f64→8) + k-unroll ×2 + prefetch. Local 1t @1024: f32 tile 32.8 → fma 19.3 ms (1.81× vs S26's 35.0), f64 64.0 → 38.6 (1.73×); pack +16% f32@1024 (M-series SLC hides b — zen2 is packing's test). fn-strip wired: map-body helper Calls tile now. S27b: loop→map/fold lifting shipped — naive LOOP matmul lifts+tiles automatically (matmul4 -275/3748 exact; loop-form legs matmul16..128 regenerated tiled — the N⁴-wall legs retire); panel residence: jt-outer nest, per-thread b-traffic ÷4–16. Conformance face byte-exact; fma face rel-tol + disasm-gated. S27c local matrix (Sapir directive): full cross-language run on the M4 Pro — par-on-par flow-fma 11.3×/7.1× over cpp-mt f32/f64 @1024; 1t 34×/19.4×; numpy=Accelerate-AMX labeled; shapes: fir mid-pack, conv2d refusal priced 7.9×-behind-1t-cpp (walker demand gate FIRED) |
matmul/s27.md · raw results-s27-local.csv (box leg: s28.md) |
| S26 (2026-07-23) | EPYC 7B12 zen2 (61.44-core cgroup-v1 quota → 62-thread pool; clang-18) | BLAS rung 2: TI=4 register blocking + the fixed-TJ main/remainder split — flow 7.4× f32 / 5.1× f64 over chapel-multicore @1024 (15.9 vs 117.4 · 23.3 vs 118.3), N=256 flips flow 1.4× — chapel loses every cell ≥256; numpy f64 gap 13.8× → 7.4× @1024; 1t f32@1024 84.9 vs S25's 568.3 (6.7×); full-width AVX2 ymm (0 xmm), vfmadd absent (recorded finding); par f32@1024 flat vs S25 — rung 3's floor. S26b (Sapir framing directive): 1t-on-1t / par-on-par tables only — quota-aware threaded cpp/rust baselines; flow 10.9× over cpp-mt f32@1024, beats every threaded naive-class baseline; numpy-1t 3.3× ahead of flow-1t (kernel gap) | matmul/s26.md |
| S25 (2026-07-23) | EPYC 7702P (62-core quota; CPU-only box) | tile emission v1: bit-exact SIMD via cell interleaving — flow-llvm 3–8.6× ahead of chapel-multicore at 512/1024 both widths; numpy gap 130× → 13.8× f64@1024 (rung 1 of the BLAS ladder); tile-vs-untile 2.5–4.6× 1t local + NEON/xmm disasm-verified; compute timer ends the wall-vs-floor estimates; shapes corpus (fir/attn/conv2d) coverage mapped | matmul/s25.md |
| S24+S24b (2026-07-23) | 4090 ×2 (EPYC 9B14 CPU leg; fmad mini-box) | parallel orchestrator v1: flow-llvm goes multicore — N=1024 f32 at chapel-multicore parity (184 vs 193 ms, flow ahead), 19× over its own single thread; S24b: -fmad=true decided+shipped+measured — f64 kernel 232.4→114.0 ms, parity with naive-CUDA-f64 (0.99×@4096, new column) and chapel-gpu |
matmul/s24.md |
| S23 (2026-07-22) | 4090, znver3 host | minimal emission (S22) + WP-D hoisting, hardware-verified; GEMM kernel = naive-CUDA = chapel-gpu at f32 saturation; f64 2× behind chapel = the measured -fmad price; scale to 4096 | matmul/s23.md |
| S21 (2026-07-22) | 4090 | ADR-0029 procedural sources + WP3b; first llvm cap legs at N≥128; first N=512 flow legs | matmul/s21.md |
| S20 (2026-07-22) | 4090 | pre-trap-free baseline; raw CSV lost (backup rule born here) | matmul/s20.md |
| S16–S19 | various | first numbers, walls named (docs/notes/bench-matmul.md); raw: results-pre-s20.csv |
— |
Raw CSVs: benches/matmul/results.csv (S24) · results-s27.csv (box, ran in S28) · results-s26b.csv · results-s26.csv · results-s25.csv · results-s24b.csv · results-s23.csv · results-s21.csv · results-pre-s20.csv.