code wiki / _hdl_build / nx_qmatvec.nx

nx_qmatvec.nx source

↩ module page · 82 lines · 4228 B

1// nx_qmatvec.nx -- the FIRST real kernel toward beating ggml/llama.cpp: a sovereign block- 2// quantized DOT PRODUCT (the hot path of LLM decode), built bits-up. The roofline proved decode 3// is MEMORY-bound, so the lever is MOVING LESS DATA -- this kernel does exactly that, and the 4// ENGINEER verifies it 1:1 against the exact dot while the quality (error) is measured vs the 5// Council's floor. Integer-only by design (no fp dequant) -- a real efficiency edge for the 6// edge/energy north-star. 7// 8// Scheme (ggml Q4_0/Q8_0 structure, integer scales): weights in blocks of B=32; each block has 9// ONE integer scale S = max|w| in the block; each weight becomes a signed code q in [-QMAX,QMAX] 10// (4-bit: -7..7, or 8-bit: -127..127). Dequant w' = q*S/QMAX. The block scale FACTORS OUT of the 11// inner loop (ggml's key trick): block_dot = (S/QMAX) * sum_i q_i*x_i, so the hot loop is pure 12// integer multiply-accumulate of small codes, and S applies once per block. 13// bytes moved (what DRAM must deliver, the memory-bound cost): 14// Q4: 4 bits/weight + 2 bytes scale per 32 = 4.5 bits/weight 15// Q8: 8 bits/weight + 2 bytes scale per 32 = 8.5 bits/weight vs int16 ref = 16 bits/weight 16// license_tier: ORIGINAL Refs: ggml Q4_0/Q8_0 block quantization; the memory-bound roofline. 17 18import "nx_syscalls.nx" 19 20const QMV_B: i64 = 32 // block size (ggml Q4_0/Q8_0) 21const QMV_Q4MAX: i64 = 7 // 4-bit signed symmetric range -7..7 22const QMV_Q8MAX: i64 = 127 // 8-bit signed symmetric range -127..127 23 24func qmv_abs(v: i64) -> i64 { if v < 0 { return 0 - v } return v } 25 26// block scale S = max|w| over [off, off+len) (0 if the block is all zero). 27func qmv_block_scale(w: *i64, off: i64, len: i64) -> i64 { 28 var s: i64 = 0; var i: i64 = 0 29 while i < len { let a: i64 = qmv_abs(w[off + i]); if a > s { s = a } i = i + 1 } 30 return s 31} 32 33// quantize one weight to a signed code in [-qmax, qmax], round-to-nearest, scale S. S>0 assumed. 34func qmv_code(wv: i64, s: i64, qmax: i64) -> i64 { 35 if s <= 0 { return 0 } 36 let half: i64 = s / 2 37 var q: i64 = 0 38 if wv >= 0 { q = (wv * qmax + half) / s } else { q = 0 - (((0 - wv) * qmax + half) / s) } 39 if q > qmax { q = qmax } 40 if q < (0 - qmax) { q = 0 - qmax } 41 return q 42} 43 44// THE KERNEL: block-quantized dot product of weights w (quantized on the fly to `qmax`) with 45// activations x, length n. Returns the dequantized dot (scale factored out of the inner loop, 46// divided once at the end). This is the memory-bound hot path -- in deployment w is PRE-quantized 47// and only the small codes + scales are loaded from DRAM (the data-movement win). 48func qmv_dot_q(w: *i64, x: *i64, n: i64, qmax: i64) -> i64 { 49 var acc: i64 = 0; var off: i64 = 0 50 while off < n { 51 var len: i64 = QMV_B; if off + len > n { len = n - off } 52 let s: i64 = qmv_block_scale(w, off, len) 53 var bdot: i64 = 0; var i: i64 = 0 54 while i < len { bdot = bdot + qmv_code(w[off + i], s, qmax) * x[off + i]; i = i + 1 } 55 acc = acc + s * bdot // scale applied once per block (factored out) 56 off = off + len 57 } 58 return acc / qmax 59} 60 61// the EXACT reference dot (the Engineer's oracle -- verify the kernel, don't trust it). 62func qmv_dot_exact(w: *i64, x: *i64, n: i64) -> i64 { 63 var acc: i64 = 0; var i: i64 = 0 64 while i < n { acc = acc + w[i] * x[i]; i = i + 1 } 65 return acc 66} 67 68// bytes the kernel must move from DRAM (the memory-bound cost), per format. bits/8 packed. 69func qmv_bytes_q(n: i64, bits: i64) -> i64 { 70 let nb: i64 = (n + QMV_B - 1) / QMV_B // number of blocks 71 return (n * bits) / 8 + nb * 2 // packed codes + 2-byte int scale per block 72} 73func qmv_bytes_ref16(n: i64) -> i64 { return n * 2 } // int16 (fp16-equivalent) baseline 74 75// relative error of the quantized dot vs exact, in PERMIL (parts per thousand). The quality metric 76// the Council floor judges. exact==0 -> 0 (avoid div by zero; caller should use a correlated vector). 77func qmv_rel_error_permil(approx: i64, exact: i64) -> i64 { 78 if exact == 0 { return 0 } 79 var d: i64 = approx - exact; if d < 0 { d = 0 - d } 80 var e: i64 = exact; if e < 0 { e = 0 - e } 81 return (d * 1000) / e 82}