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}