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