code wiki / _hdl_build / nx_roofline.nx
nx_roofline.nx source
↩ module page · 67 lines · 3678 B
1// nx_roofline.nx -- the team's "where is the real bottleneck?" intelligence (the mathematician's
2// FIRST move: diagnose the binding constraint before spending effort). Polishing a memory-bound
3// kernel's arithmetic is wasted work -- llama.cpp's AVX2 prefetch on the Q4 dot product bought
4// +0.8% because token generation is MEMORY-BANDWIDTH bound, not compute bound. This module makes
5// that judgment explicit and hardware-specific (it "grows to meet the hardware": ridge point is
6// computed from the chip's own peak flops + bandwidth, the vertical co-design north star).
7//
8// ROOFLINE (Williams/Patterson 2009): arithmetic intensity AI = flops / bytes_moved. The machine
9// has a ridge point R = peak_flops / peak_bandwidth. AI < R => MEMORY-bound (the lever is bytes
10// moved: quantization, sparsity, fusion, reuse -- "mental math", not SIMD). AI > R => COMPUTE-
11// bound (the lever is the inner-loop instruction sequence: superopt / the search governor).
12// Attainable perf = min(peak_flops, AI * peak_bandwidth).
13//
14// All comparisons are EXACT integer cross-multiplications (no float, no division truncation), and
15// peak_flops/peak_bw are taken in GFLOP/s and GB/s so the products stay well inside i64.
16// license_tier: ORIGINAL Refs: Williams, Waterman, Patterson, "Roofline" CACM 2009.
17
18import "nx_syscalls.nx"
19
20const RF_UNKNOWN: i64 = 0 // robustness sentinel: invalid inputs -> no verdict (fail loud)
21const RF_MEMORY_BOUND: i64 = 1
22const RF_COMPUTE_BOUND: i64 = 2
23const RF_OVF_LIMIT: i64 = 4611686018427387904 // 2^62: keep every product safely inside i64
24
25// MEMORY-bound iff AI < ridge iff flops/bytes < peak_flops/peak_bw iff (cross-multiplied, exact)
26// flops*peak_bw < bytes*peak_flops. Equal => exactly at the ridge (treat as compute-bound: the
27// arithmetic is the binding side). peak_flops in GFLOP/s, peak_bw in GB/s.
28// ROBUST: rejects non-positive inputs (RF_UNKNOWN, fail loud at the boundary), and scales BOTH
29// sides down equally (preserving the ratio) until neither cross-product can overflow i64 -- so it
30// is correct for any kernel size, from a sensor loop to a 70B layer, never a silent wrong verdict.
31func rf_bound(flops: i64, bytes: i64, peak_flops: i64, peak_bw: i64) -> i64 {
32 if flops <= 0 { return RF_UNKNOWN }
33 if bytes <= 0 { return RF_UNKNOWN }
34 if peak_flops <= 0 { return RF_UNKNOWN }
35 if peak_bw <= 0 { return RF_UNKNOWN }
36 var f: i64 = flops; var b: i64 = bytes
37 var ovf: i64 = 1
38 while ovf == 1 {
39 ovf = 0
40 if f > RF_OVF_LIMIT / peak_bw { ovf = 1 }
41 if b > RF_OVF_LIMIT / peak_flops { ovf = 1 }
42 if ovf == 1 { f = f >> 1; b = b >> 1 } // halving both preserves flops/bytes exactly
43 }
44 if f * peak_bw < b * peak_flops { return RF_MEMORY_BOUND }
45 return RF_COMPUTE_BOUND
46}
47
48// arithmetic intensity scaled by 1000 (flop per byte * 1000), so the team can print/compare it
49// without floats. AI = flops/bytes.
50func rf_ai_milli(flops: i64, bytes: i64) -> i64 {
51 if bytes <= 0 { return 0 }
52 return (flops * 1000) / bytes
53}
54
55// ridge point scaled by 1000 (flop/byte * 1000): R = peak_flops/peak_bw.
56func rf_ridge_milli(peak_flops: i64, peak_bw: i64) -> i64 {
57 if peak_bw <= 0 { return 0 }
58 return (peak_flops * 1000) / peak_bw
59}
60
61// in the MEMORY-bound regime, runtime is proportional to bytes moved, so cutting bytes is the
62// direct speedup: speedup(x1000) = old_bytes/new_bytes. This is the quantitative "mental math"
63// lever -- e.g. Q4->Q2 halves weight bytes => ~2x, where SIMD gave +0.8%.
64func rf_byte_speedup_milli(old_bytes: i64, new_bytes: i64) -> i64 {
65 if new_bytes <= 0 { return 0 }
66 return (old_bytes * 1000) / new_bytes
67}