nx_q4k_dot_simd.nx source
↩ module page · 275 lines · 11119 B
1// nx_q4k_dot_simd.nx -- SIMD-accelerated Q4_K fused dot (vpmaddwd), bit-exact vs nx_q4k_dot_row_col.
2//
3// Perf-exceed applied to the Qwen GEMM. Reformulation (exact integer algebra): per sub-block,
4// dot += Σ (d1*q - m0)*col = d1*(Σ q*col) - m0*(Σ col) = d1*sq - m0*sc
5// sq = Σ q*col via one vpmaddwd accumulate (q i16 0..15, col i16 Q10). Optimizations vs v1: read qs bytes
6// ONCE (extract lo+hi together), and take sc = Σ col from a PRECOMPUTED sub-block table (activation-only,
7// constant across the dot -- free in a GEMM). col: both Q10 i64 (unused now except pack) + i16-packed.
8// license_tier: ORIGINAL
9import "nx_syscalls.nx"
10import "nx_tier.nx"
11import "nx_le.nx"
12import "nx_strconv.nx"
13import "nx_tensor.nx"
14import "nx_gguf.nx"
15import "nx_gguf_load.nx"
16import "nx_gguf_meta.nx"
17import "nx_placement.nx"
18import "nx_gguf_load_lazy.nx"
19import "nx_q4k_matmul.nx"
20import "nx_dequant_iter.nx"
21import "nx_clock.nx"
22
23func ds_pack4(a: i64, b: i64, c: i64, d: i64) -> i64 {
24 return (a & 0xFFFF) | ((b & 0xFFFF) << 16) | ((c & 0xFFFF) << 32) | ((d & 0xFFFF) << 48)
25}
26
27func ds_hsum(acc: *i64) -> i64 {
28 var sum: i64 = 0
29 var i: i64 = 0
30 while i < 4 {
31 let v: i64 = acc[i]
32 var lo: i64 = v & 0xFFFFFFFF
33 if lo >= 0x80000000 { lo = lo - 0x100000000 }
34 var hi: i64 = (v >> 32) & 0xFFFFFFFF
35 if hi >= 0x80000000 { hi = hi - 0x100000000 }
36 sum = sum + lo + hi
37 i = i + 1
38 }
39 return sum
40}
41
42// sc_pre[blk*8 + is] = Σ_{l<32} col[blk*256 + is*32 + l] (activation-only, precompute once)
43func nx_q4k_sc_precompute(col_q10: *i64, n_blocks: i64, sc_pre: *i64) -> i64 {
44 var blk: i64 = 0
45 while blk < n_blocks {
46 var is_: i64 = 0
47 while is_ < 8 {
48 var s: i64 = 0
49 var l: i64 = 0
50 let base: i64 = blk * 256 + is_ * 32
51 while l < 32 { s = s + col_q10[base + l]; l = l + 1 }
52 sc_pre[blk * 8 + is_] = s
53 is_ = is_ + 1
54 }
55 blk = blk + 1
56 }
57 return 0
58}
59
60func nx_q4k_dot_simd(buf: *u8, base_off: i64, n_blocks: i64, col_i16: *i64,
61 qpk: *i64, qhi: *i64, acc: *i64, sc_pre: *i64) -> i64 {
62 var dot: i64 = 0
63 var blk: i64 = 0
64 while blk < n_blocks {
65 let sb: i64 = base_off + blk * 144
66 let d_q24: i64 = _gguf_f16_to_q24(nx_le_read_u16(buf, sb))
67 let dmin_q24: i64 = _gguf_f16_to_q24(nx_le_read_u16(buf, sb + 2))
68 let scales_off: i64 = sb + 4
69 let qs_off: i64 = sb + 16
70 let col_base: i64 = blk * 256
71 var g: i64 = 0
72 while g < 4 {
73 let is0: i64 = g + g
74 let is1: i64 = is0 + 1
75 var sc0: i64 = 0
76 var m0: i64 = 0
77 var sc1: i64 = 0
78 var m1s: i64 = 0
79 if is0 < 4 {
80 sc0 = nx_le_read_u8(buf, scales_off + is0) & 0x3F
81 m0 = nx_le_read_u8(buf, scales_off + is0 + 4) & 0x3F
82 } else {
83 let k0: i64 = is0 - 4
84 sc0 = ((nx_le_read_u8(buf, scales_off + k0) >> 6) << 4) | (nx_le_read_u8(buf, scales_off + 8 + k0) & 0x0F)
85 m0 = ((nx_le_read_u8(buf, scales_off + 4 + k0) >> 6) << 4) | (nx_le_read_u8(buf, scales_off + 8 + k0) >> 4)
86 }
87 if is1 < 4 {
88 sc1 = nx_le_read_u8(buf, scales_off + is1) & 0x3F
89 m1s = nx_le_read_u8(buf, scales_off + is1 + 4) & 0x3F
90 } else {
91 let k1: i64 = is1 - 4
92 sc1 = ((nx_le_read_u8(buf, scales_off + k1) >> 6) << 4) | (nx_le_read_u8(buf, scales_off + 8 + k1) & 0x0F)
93 m1s = ((nx_le_read_u8(buf, scales_off + 4 + k1) >> 6) << 4) | (nx_le_read_u8(buf, scales_off + 8 + k1) >> 4)
94 }
95 let d1: i64 = d_q24 * sc0
96 let m0v: i64 = dmin_q24 * m0
97 let d2: i64 = d_q24 * sc1
98 let m1v: i64 = dmin_q24 * m1s
99 let grp: i64 = qs_off + g * 32
100
101 // read 32 qs bytes ONCE; fill qpk (low nibbles) + qhi (high nibbles)
102 var j: i64 = 0
103 while j < 8 {
104 let b0: i64 = nx_le_read_u8(buf, grp + j * 4 + 0)
105 let b1: i64 = nx_le_read_u8(buf, grp + j * 4 + 1)
106 let b2: i64 = nx_le_read_u8(buf, grp + j * 4 + 2)
107 let b3: i64 = nx_le_read_u8(buf, grp + j * 4 + 3)
108 qpk[j] = ds_pack4(b0 & 0x0F, b1 & 0x0F, b2 & 0x0F, b3 & 0x0F)
109 qhi[j] = ds_pack4(b0 >> 4, b1 >> 4, b2 >> 4, b3 >> 4)
110 j = j + 1
111 }
112
113 acc[0] = 0; acc[1] = 0; acc[2] = 0; acc[3] = 0
114 let cl0: i64 = (col_i16 as i64) + ((col_base + is0 * 32) / 4) * 8
115 __i16x16_madd(acc as *i64, qpk as *i64, cl0 as *i64)
116 __i16x16_madd(acc as *i64, ((qpk as i64) + 32) as *i64, (cl0 + 32) as *i64)
117 let sq_lo: i64 = ds_hsum(acc)
118
119 acc[0] = 0; acc[1] = 0; acc[2] = 0; acc[3] = 0
120 let cl1: i64 = (col_i16 as i64) + ((col_base + is1 * 32) / 4) * 8
121 __i16x16_madd(acc as *i64, qhi as *i64, cl1 as *i64)
122 __i16x16_madd(acc as *i64, ((qhi as i64) + 32) as *i64, (cl1 + 32) as *i64)
123 let sq_hi: i64 = ds_hsum(acc)
124
125 let sc_lo: i64 = sc_pre[blk * 8 + is0]
126 let sc_hi: i64 = sc_pre[blk * 8 + is1]
127 dot = dot + d1 * sq_lo - m0v * sc_lo + d2 * sq_hi - m1v * sc_hi
128 g = g + 1
129 }
130 blk = blk + 1
131 }
132 return dot
133}
134
135func ds_emit(fd: i64, key: *u8, kl: i64, v: i64) -> i64 {
136 let line: *u8 = sys_mmap(64)
137 var lo: i64 = 0
138 var i: i64 = 0
139 while i < kl { line[lo] = key[i]; lo = lo + 1; i = i + 1 }
140 line[lo] = 0x3D; lo = lo + 1
141 let dec: *u8 = sys_mmap(32)
142 let nd: i64 = nx_strconv_format_i64(v, dec)
143 var k: i64 = 0
144 while k < nd { line[lo] = dec[k]; lo = lo + 1; k = k + 1 }
145 line[lo] = 0x0A; lo = lo + 1
146 return sys_write(fd, line, lo)
147}
148
149// Synthesise `nb` Q4_K blocks in place. A Q4_K block is 144 bytes: 2B d (fp16) + 2B dmin (fp16) +
150// 12B packed 6-bit scales + 128B of packed 4-bit quants. The PERF question -- is the SIMD dot faster
151// than the scalar one -- does not need real weights at all: both paths read the SAME bytes, so any
152// deterministic fill answers it exactly. This removes a 1.1GB model dependency from a timing bench.
153func ds_synth(buf: *u8, nb: i64) -> i64 {
154 var b: i64 = 0
155 while b < nb {
156 let sb: i64 = b * 144
157 buf[sb + 0] = 0 as u8
158 buf[sb + 1] = 60 as u8
159 buf[sb + 2] = 0 as u8
160 buf[sb + 3] = 52 as u8
161 var i: i64 = 0
162 while i < 12 { buf[sb + 4 + i] = ((b * 7 + i * 13) % 64) as u8; i = i + 1 }
163 i = 0
164 while i < 128 { buf[sb + 16 + i] = ((b * 31 + i * 17) % 256) as u8; i = i + 1 }
165 b = b + 1
166 }
167 return 0
168}
169
170// argv[1] = OPTIONAL gguf path (rule 17: never bake one). Absent or unopenable -> SYNTHETIC blocks, and
171// the run reports synthetic=1 so no reader can mistake a fixture number for a real-weights number.
172// The old build baked a /mnt/c laptop path and returned 30 on the NAS, writing zero bytes: a silent
173// failure that read as a hang. Results now also go to STDOUT, because nx_job_run captures stdout and a
174// file-only emitter looks empty even when it succeeded.
175func main(argc: i64, argv: *i64) -> i64 {
176 var buf: *u8 = 0 as *u8
177 var total: i64 = 0
178 var IN: i64 = 0
179 var w_off: i64 = 0
180 var synth: i64 = 1
181 if argc >= 2 {
182 let path: *u8 = argv[1] as *u8
183 let fd: i64 = sys_openat_rd(path)
184 if fd >= 0 {
185 let CAP: i64 = 1153433600
186 buf = sys_mmap(CAP)
187 var go: i64 = 1
188 while go == 1 {
189 let r: i64 = sys_read(fd, ((buf as i64) + total) as *u8, CAP - total)
190 if r <= 0 { go = 0 } else { total = total + r; if total >= CAP { go = 0 } }
191 }
192 sys_close(fd)
193 if total >= 100000000 {
194 let hdr: *NxGgufHeader = sys_mmap(NX_GGUF_HDR_BYTES) as *NxGgufHeader
195 if nx_gguf_parse(buf, total, hdr) == NX_GGUF_OK {
196 let qi: nx_int = nx_gguf_find_tensor(hdr, "blk.0.attn_q.weight" as *u8, 19)
197 if qi >= 0 {
198 let ti: *NxGgufTensorInfo = nx_gguf_tensor_at(hdr, qi)
199 IN = ti.dim_0
200 w_off = hdr.data_off + ti.offset
201 synth = 0
202 }
203 }
204 }
205 }
206 }
207 var n_blocks: i64 = 0
208 if synth == 1 {
209 n_blocks = 14
210 IN = n_blocks * 256
211 total = n_blocks * 144
212 buf = sys_mmap(total)
213 ds_synth(buf, n_blocks)
214 w_off = 0
215 } else {
216 n_blocks = IN / 256
217 if w_off + n_blocks * 144 > total { return 63 }
218 }
219
220 let col: *i64 = sys_mmap(IN * 8) as *i64
221 let col_i16: *i64 = sys_mmap(IN * 2) as *i64
222 var i: i64 = 0
223 while i < IN { col[i] = 1024 + (i - (i / 5) * 5) * 256; i = i + 1 }
224 var jj: i64 = 0
225 while jj < IN / 4 {
226 col_i16[jj] = ds_pack4(col[jj * 4], col[jj * 4 + 1], col[jj * 4 + 2], col[jj * 4 + 3])
227 jj = jj + 1
228 }
229 let sc_pre: *i64 = sys_mmap(n_blocks * 8 * 8) as *i64
230 nx_q4k_sc_precompute(col, n_blocks, sc_pre)
231
232 let qpk: *i64 = sys_mmap(64) as *i64
233 let qhi: *i64 = sys_mmap(64) as *i64
234 let acc: *i64 = sys_mmap(32) as *i64
235 let it: *NxQ4KBlockIter = nx_q4k_iter_alloc()
236
237 let scalar_dot: i64 = nx_q4k_dot_row_col(buf, w_off, n_blocks, col, it)
238 let simd_dot: i64 = nx_q4k_dot_simd(buf, w_off, n_blocks, col_i16, qpk, qhi, acc, sc_pre)
239
240 let IT: i64 = 20000
241 let t0: i64 = nx_clock_monotonic_ns()
242 var s1: i64 = 0
243 var k: i64 = 0
244 while k < IT { s1 = s1 + nx_q4k_dot_row_col(buf, w_off, n_blocks, col, it); k = k + 1 }
245 let t1: i64 = nx_clock_monotonic_ns()
246 let scalar_ns: i64 = t1 - t0
247
248 let t2: i64 = nx_clock_monotonic_ns()
249 var s2: i64 = 0
250 k = 0
251 while k < IT { s2 = s2 + nx_q4k_dot_simd(buf, w_off, n_blocks, col_i16, qpk, qhi, acc, sc_pre); k = k + 1 }
252 let t3: i64 = nx_clock_monotonic_ns()
253 let simd_ns: i64 = t3 - t2
254
255 let ofd: i64 = sys_openat_wr("/tmp/q4k_simd.txt" as *u8, 0x1a4)
256 if ofd >= 0 {
257 ds_emit(ofd, "scalar_dot" as *u8, 10, scalar_dot)
258 ds_emit(ofd, "simd_dot" as *u8, 8, simd_dot)
259 ds_emit(ofd, "scalar_ns" as *u8, 9, scalar_ns)
260 ds_emit(ofd, "simd_ns" as *u8, 7, simd_ns)
261 if simd_ns > 0 { ds_emit(ofd, "speedup_x100" as *u8, 12, scalar_ns * 100 / simd_ns) }
262 sys_close(ofd)
263 }
264 ds_emit(1, "synthetic" as *u8, 9, synth)
265 ds_emit(1, "n_blocks" as *u8, 8, n_blocks)
266 ds_emit(1, "scalar_dot" as *u8, 10, scalar_dot)
267 ds_emit(1, "simd_dot" as *u8, 8, simd_dot)
268 ds_emit(1, "bit_exact" as *u8, 9, 1 - ((scalar_dot - simd_dot) * (scalar_dot - simd_dot)))
269 ds_emit(1, "scalar_ns" as *u8, 9, scalar_ns)
270 ds_emit(1, "simd_ns" as *u8, 7, simd_ns)
271 if simd_ns > 0 { ds_emit(1, "speedup_x100" as *u8, 12, scalar_ns * 100 / simd_ns) }
272 if simd_dot != scalar_dot { return 80 }
273 if simd_ns >= scalar_ns { return 81 }
274 return 0
275}