code wiki / _hdl_build / nx_qlayer.nx

nx_qlayer.nx source

↩ module page · 117 lines · 5101 B

1// nx_qlayer.nx -- the full decode LAYER: y = W . x, the operation an LLM does thousands of times 2// per token. Built honestly for the memory-bound regime: the weight matrix is PRE-QUANTIZED ONCE 3// (offline), and the HOT kernel streams ONLY the packed codes + per-block scales from DRAM -- the 4// exact deployment structure of ggml (weights quantized at load, dequant+dot in the inner loop). 5// This is the layer that, moving 3.5x less data at bounded quality, beats llama.cpp where it is 6// actually bottlenecked. The ENGINEER verifies the WHOLE layer output 1:1 vs the exact layer, and 7// the max per-row error is measured against the Council quality floor. license_tier: ORIGINAL 8 9import "nx_qmatvec.nx" // QMV_B, QMV_Q4MAX/Q8MAX, qmv_block_scale, qmv_code, qmv_abs 10import "nx_vecmath.nx" 11 12func ql_blocks_per_row(c: i64) -> i64 { return (c + QMV_B - 1) / QMV_B } 13 14// OFFLINE (once, at load): quantize the R x C weight matrix into packed codes[R*C] + per-block 15// scales[R*nbpr]. Not on the hot path -- amortized over every token. 16func qlayer_quantize(w: *i64, r: i64, c: i64, qmax: i64, codes: *i64, scales: *i64) -> i64 { 17 let nbpr: i64 = ql_blocks_per_row(c) 18 var row: i64 = 0 19 while row < r { 20 let base: i64 = row * c 21 var bk: i64 = 0 22 while bk < nbpr { 23 let off: i64 = bk * QMV_B 24 var len: i64 = QMV_B; if off + len > c { len = c - off } 25 let s: i64 = qmv_block_scale(w, base + off, len) 26 scales[row * nbpr + bk] = s 27 var i: i64 = 0 28 while i < len { codes[base + off + i] = qmv_code(w[base + off + i], s, qmax); i = i + 1 } 29 bk = bk + 1 30 } 31 row = row + 1 32 } 33 return 0 34} 35 36// THE HOT KERNEL: y = dequant(codes,scales) . x, per row. Reads ONLY codes + scales + x (the data 37// streamed from DRAM in deployment). Scale factored out of the inner loop (ggml's trick). 38func qlayer_matvec(codes: *i64, scales: *i64, x: *i64, r: i64, c: i64, qmax: i64, y: *i64) -> i64 { 39 let nbpr: i64 = ql_blocks_per_row(c) 40 var row: i64 = 0 41 while row < r { 42 let base: i64 = row * c 43 var acc: i64 = 0; var bk: i64 = 0 44 while bk < nbpr { 45 let off: i64 = bk * QMV_B 46 var len: i64 = QMV_B; if off + len > c { len = c - off } 47 let s: i64 = scales[row * nbpr + bk] 48 var bdot: i64 = 0; var i: i64 = 0 49 while i < len { bdot = bdot + codes[base + off + i] * x[off + i]; i = i + 1 } 50 acc = acc + s * bdot 51 bk = bk + 1 52 } 53 y[row] = acc / qmax 54 row = row + 1 55 } 56 return 0 57} 58 59// the EXACT reference layer (the Engineer's oracle). 60func qlayer_matvec_exact(w: *i64, x: *i64, r: i64, c: i64, y: *i64) -> i64 { 61 var row: i64 = 0 62 while row < r { 63 let base: i64 = row * c 64 var acc: i64 = 0; var i: i64 = 0 65 while i < c { acc = acc + w[base + i] * x[i]; i = i + 1 } 66 y[row] = acc 67 row = row + 1 68 } 69 return 0 70} 71 72// integer floor sqrt (Newton). For the L2 norm without floats. 73func qlayer_isqrt(n: i64) -> i64 { return vm_isqrt(n) } 74 75// L2 RELATIVE error in PERMIL = sqrt( sum (yq-ye)^2 / sum ye^2 ) * 1000. This is the STANDARD 76// quantization-quality metric over the whole output vector -- robust to individual rows whose 77// exact value is near zero (where per-row RELATIVE error is meaningless). Honest measure of how 78// much the quantized layer's output deviates from the exact layer. (Per-row relative error is the 79// wrong tool here: a true-near-zero row makes it explode without any real quality loss.) 80func qlayer_l2_relerr_permil(yq: *i64, ye: *i64, r: i64) -> i64 { 81 var sd2: i64 = 0; var se2: i64 = 0; var row: i64 = 0 82 while row < r { 83 let d: i64 = yq[row] - ye[row] 84 sd2 = sd2 + d * d 85 se2 = se2 + ye[row] * ye[row] 86 row = row + 1 87 } 88 if se2 <= 0 { return 0 } 89 // permil = sqrt(sd2/se2)*1000 = sqrt(sd2*1e6/se2). guard the *1e6 against overflow by scaling 90 // both sums down equally while sd2 is large (preserves the ratio). 91 var a: i64 = sd2; var b: i64 = se2 92 let LIM: i64 = 9223372036854775 93 var g: i64 = 1 94 while a > LIM { a = a >> 1; b = b >> 1; g = g } // keep a*1e6 inside i64 95 if b <= 0 { b = 1 } 96 return qlayer_isqrt((a * 1000000) / b) 97} 98 99// kept for diagnostics: MAX per-row relative error (UNSTABLE near zero -- use L2 for the verdict). 100func qlayer_max_err_permil(yq: *i64, ye: *i64, r: i64) -> i64 { 101 var mx: i64 = 0; var row: i64 = 0 102 while row < r { 103 let e: i64 = ye[row] 104 if e != 0 { 105 var d: i64 = yq[row] - e; if d < 0 { d = 0 - d } 106 var ea: i64 = e; if ea < 0 { ea = 0 - ea } 107 let rp: i64 = (d * 1000) / ea 108 if rp > mx { mx = rp } 109 } 110 row = row + 1 111 } 112 return mx 113} 114 115// total DRAM bytes the hot kernel streams for the weights, per format vs the int16 baseline. 116func qlayer_bytes_q(r: i64, c: i64, bits: i64) -> i64 { return (r * c * bits) / 8 + (r * ql_blocks_per_row(c)) * 2 } 117func qlayer_bytes_ref16(r: i64, c: i64) -> i64 { return r * c * 2 }