← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
gemm_kernels_q4k_q8k.c
Go to the documentation of this file.
1/**
2 * @file gemm_kernels_q4k_q8k.c
3 * @brief Q4_K (weights) x Q8_K (activations) kernels for inference
4 *
5 * CK-ENGINE KERNEL RULES:
6 * =======================
7 * 1. NO malloc/free - memory via bump allocator, pointers passed in
8 * 2. NO OpenMP - parallelization at orchestrator/codegen layer
9 * 3. API must define: inputs, outputs, workspace, and memory layouts
10 * 4. Pure computation - deterministic, no side effects
11 *
12 * After changes: make test && make llamacpp-parity-full
13 *
14 * Implements decode-style matvec/matmul where weights are Q4_K and the
15 * activations are quantized on-the-fly to Q8_K. This is inference-only;
16 * no backward pass is provided here.
17 */
18
19#include <assert.h>
20#include <immintrin.h>
21#include <math.h>
22#include <stdlib.h>
23#include <string.h>
24
25#include "ck_threadpool.h"
26#include "ckernel_quant.h"
27
28void gemv_q4_k_q8_k_avx2(float *y,
29 const void *W,
30 const void *x_q8,
31 int M, int K);
32
33void gemv_q4_k_q8_k_vnni(float *y,
34 const void *W,
35 const void *x_q8,
36 int M, int K);
37
38void gemv_q4_k_q8_k_avx(float *y,
39 const void *W,
40 const void *x_q8,
41 int M, int K);
42
43void gemv_q4_k_q8_k_sse(float *y,
44 const void *W,
45 const void *x_q8,
46 int M, int K);
47
48static inline int ck_nearest_int(float fval) {
49 /* Bit-level round-to-nearest from llama.cpp (fast + deterministic). */
50 float val = fval + 12582912.f;
51 int i;
52 memcpy(&i, &val, sizeof(int));
53 return (i & 0x007fffff) - 0x00400000;
54}
55
56#if defined(__clang__)
57#pragma clang fp contract(off)
58#elif defined(__GNUC__)
59__attribute__((optimize("fp-contract=off")))
60#endif
61void quantize_row_q8_k_ref(const float *x, void *vy, int k) {
62 if (!x || !vy || k <= 0) {
63 return;
64 }
65 assert(k % QK_K == 0);
66 const int nb = k / QK_K;
67 block_q8_K *y = (block_q8_K *)vy;
68
69 for (int i = 0; i < nb; ++i) {
70 float max = 0.0f;
71 float amax = 0.0f;
72 for (int j = 0; j < QK_K; ++j) {
73 float ax = fabsf(x[j]);
74 if (ax > amax) {
75 amax = ax;
76 max = x[j];
77 }
78 }
79 if (!amax) {
80 y[i].d = 0.0f;
81 memset(y[i].qs, 0, sizeof(y[i].qs));
82 memset(y[i].bsums, 0, sizeof(y[i].bsums));
83 x += QK_K;
84 continue;
85 }
86
87 const float iscale = -127.0f / max;
88 for (int j = 0; j < QK_K; ++j) {
89 /* llama.cpp rounds the multiply before adding nearest_int's magic
90 * constant. Contracting both operations changes Q8_K tie cases. */
91 float scaled = iscale * x[j];
92 int v = ck_nearest_int(scaled);
93 if (v > 127) {
94 v = 127;
95 }
96 if (v < -128) {
97 v = -128;
98 }
99 y[i].qs[j] = (int8_t)v;
100 }
101
102 for (int j = 0; j < QK_K / 16; ++j) {
103 int sum = 0;
104 const int8_t *qs = &y[i].qs[j * 16];
105 for (int ii = 0; ii < 16; ++ii) {
106 sum += qs[ii];
107 }
108 y[i].bsums[j] = (int16_t)sum;
109 }
110
111 y[i].d = 1.0f / iscale;
112 x += QK_K;
113 }
114}
115
116void quantize_row_q8_k_sse(const float *x, void *vy, int k);
117void quantize_row_q8_k_avx(const float *x, void *vy, int k);
118void quantize_row_q8_k_avx2(const float *x, void *vy, int k);
119void quantize_row_q8_k_avx512(const float *x, void *vy, int k);
120
121void quantize_row_q8_k(const float *x, void *vy, int k) {
122 const char *ref_env = getenv("CK_DEBUG_Q8K_REF");
123 if (ref_env && atoi(ref_env) != 0) {
124 quantize_row_q8_k_ref(x, vy, k);
125 return;
126 }
127#if defined(__AVX512F__) && defined(__AVX512BW__)
128 quantize_row_q8_k_avx512(x, vy, k);
129#elif defined(__AVX2__)
130 quantize_row_q8_k_avx2(x, vy, k);
131#elif defined(__AVX__)
132 quantize_row_q8_k_avx(x, vy, k);
133#elif defined(__SSE4_1__)
134 quantize_row_q8_k_sse(x, vy, k);
135#else
136 quantize_row_q8_k_ref(x, vy, k);
137#endif
138}
139
140void quantize_batch_q8_k_4row_nearest_even(const float *x, void *vy,
141 int num_rows, int k) {
142 if (!x || !vy || num_rows <= 0 || k <= 0) {
143 return;
144 }
145 assert(k % QK_K == 0);
146
147 block_q8_K *y = (block_q8_K *)vy;
148 const int blocks_per_row = k / QK_K;
149
150 /* Q8_K bytes are a numerical ABI. The previous four-row AVX2 path used
151 * a different max/scale evaluation order and changed real Qwen3-VL
152 * visual-prefix scales by one FP32 ULP. Keep this public grouped ABI on
153 * the canonical row provider until an optimized implementation is
154 * byte-exact against llama.cpp on both synthetic and production inputs. */
155 for (int row = 0; row < num_rows; ++row) {
157 x + (size_t)row * (size_t)k,
158 y + (size_t)row * (size_t)blocks_per_row,
159 k);
160 }
161}
162
163static float dot_q4_k_q8_k_ref(const block_q4_K *w,
164 const block_q8_K *x,
165 int k)
166{
167 const int nb = k / QK_K;
168 float sumf = 0.0f;
169
170 for (int i = 0; i < nb; ++i) {
171 uint8_t sc[8], m_val[8];
172 unpack_q4_k_scales(w[i].scales, sc, m_val);
173
174 const float d = CK_FP16_TO_FP32(w[i].d) * x[i].d;
175 const float dmin = CK_FP16_TO_FP32(w[i].dmin) * x[i].d;
176
177 int sumi = 0;
178 for (int j = 0; j < QK_K / 16; ++j) {
179 sumi += (int)x[i].bsums[j] * (int)m_val[j / 2];
180 }
181
182 int32_t scaled_sum = 0;
183 for (int group = 0; group < 4; ++group) {
184 const uint8_t *qs = &w[i].qs[group * 32];
185 const int8_t *q8_lo = &x[i].qs[group * 64];
186 const int8_t *q8_hi = q8_lo + 32;
187 int32_t lo = 0;
188 int32_t hi = 0;
189 for (int l = 0; l < 32; ++l) {
190 lo += (int32_t)(qs[l] & 0x0F) * (int32_t)q8_lo[l];
191 hi += (int32_t)(qs[l] >> 4) * (int32_t)q8_hi[l];
192 }
193 scaled_sum += (int32_t)sc[2 * group] * lo;
194 scaled_sum += (int32_t)sc[2 * group + 1] * hi;
195 }
196 sumf += d * (float)scaled_sum - dmin * (float)sumi;
197 }
198 return sumf;
199}
200
201void gemv_q4_k_q8_k_ref(float *y,
202 const void *W,
203 const void *x_q8,
204 int M, int K)
205{
206 if (!y || !W || !x_q8 || M <= 0 || K <= 0) {
207 return;
208 }
209
210 const block_q4_K *blocks = (const block_q4_K *)W;
211 const block_q8_K *x = (const block_q8_K *)x_q8;
212 const int blocks_per_row = K / QK_K;
213
214 for (int row = 0; row < M; ++row) {
215 const block_q4_K *w_row = blocks + (size_t)row * (size_t)blocks_per_row;
216 y[row] = dot_q4_k_q8_k_ref(w_row, x, K);
217 }
218}
219
220static int ck_q4k_q8k_force_ref(void)
221{
222 static int cached = -1;
223 if (cached < 0) {
224 const char *env = getenv("CK_DEBUG_Q4K_Q8_REF");
225 cached = (env && env[0] && env[0] != '0') ? 1 : 0;
226 }
227 return cached;
228}
229
230/* ============================================================================
231 * PARALLEL VERSIONS (for parallel orchestration)
232 *
233 * These receive ith (thread index) and nth (total threads) from orchestration.
234 * OpenMP lives in orchestration layer, NOT here.
235 *
236 * Naming: *_parallel = receives ith/nth, processes only its portion
237 * *_ref/_avx = single-threaded, processes all rows
238 * ============================================================================ */
239
241 const void *W,
242 const void *x_q8,
243 int M, int K,
244 int ith, int nth)
245{
246 if (!y || !W || !x_q8 || M <= 0 || K <= 0) {
247 return;
248 }
249 if (ith < 0 || nth <= 0 || ith >= nth) {
250 return;
251 }
252
253 /* Compute row range for this thread */
254 const int dr = (M + nth - 1) / nth;
255 const int r0 = dr * ith;
256 const int r1 = (r0 + dr < M) ? (r0 + dr) : M;
257
258 if (r0 >= M) {
259 return; /* This thread has no work */
260 }
261
262 const block_q4_K *blocks = (const block_q4_K *)W;
263 const block_q8_K *x = (const block_q8_K *)x_q8;
264 const int blocks_per_row = K / QK_K;
265
266 /* Only process rows [r0, r1) */
267 for (int row = r0; row < r1; ++row) {
268 const block_q4_K *w_row = blocks + (size_t)row * (size_t)blocks_per_row;
269 y[row] = dot_q4_k_q8_k_ref(w_row, x, K);
270 }
271}
272
273void gemv_q4_k_q8_k(float *y,
274 const void *W,
275 const void *x_q8,
276 int M, int K)
277{
278 if (ck_q4k_q8k_force_ref()) {
279 gemv_q4_k_q8_k_ref(y, W, x_q8, M, K);
280 return;
281 }
282#if defined(__AVX512VNNI__) && defined(__AVX512VL__) && !defined(CK_NO_AVX512_VNNI)
283 /* VNNI: Best for decode (single token) - INT8 dot product acceleration */
284 gemv_q4_k_q8_k_vnni(y, W, x_q8, M, K);
285#elif defined(__AVX2__)
286 gemv_q4_k_q8_k_avx2(y, W, x_q8, M, K);
287#elif defined(__AVX__)
288 /* AVX version uses maddubs_epi16 (more efficient than SSE) */
289 gemv_q4_k_q8_k_avx(y, W, x_q8, M, K);
290#elif defined(__SSE4_1__)
291 gemv_q4_k_q8_k_sse(y, W, x_q8, M, K);
292#else
293 gemv_q4_k_q8_k_ref(y, W, x_q8, M, K);
294#endif
295}
296
297void gemm_q4_k_q8_k_ref(float *Y,
298 const void *W,
299 const void *X_q8,
300 int M, int N, int K)
301{
302 if (!Y || !W || !X_q8 || M <= 0 || N <= 0 || K <= 0) {
303 return;
304 }
305
306 const block_q8_K *X = (const block_q8_K *)X_q8;
307 const int blocks_per_vec = K / QK_K;
308
309 for (int n = 0; n < N; ++n) {
310 const block_q8_K *x_row = X + (size_t)n * (size_t)blocks_per_vec;
311 gemv_q4_k_q8_k_ref(&Y[n * M], W, x_row, M, K);
312 }
313}
314
315typedef struct {
316 float *Y;
317 const void *W;
318 const block_q8_K *X;
319 int M_out;
320 int N_batch;
321 int K;
322 int blocks_per_vec;
323 int blocks_per_row;
324} gemm_q4_k_q8_k_work_t;
325
326static void gemm_q4_k_q8_k_thread_fn(int ith, int nth, void *args)
327{
328 gemm_q4_k_q8_k_work_t *a = (gemm_q4_k_q8_k_work_t *)args;
329 if (!a || ith < 0 || nth <= 0 || ith >= nth) {
330 return;
331 }
332
333 const int dr = (a->M_out + nth - 1) / nth;
334 const int r0 = dr * ith;
335 const int r1 = (r0 + dr < a->M_out) ? (r0 + dr) : a->M_out;
336 if (r0 >= a->M_out) {
337 return;
338 }
339
340 const block_q4_K *blocks = (const block_q4_K *)a->W;
341 const block_q4_K *w_start = blocks + (size_t)r0 * (size_t)a->blocks_per_row;
342 const int rows = r1 - r0;
343
344 for (int n = 0; n < a->N_batch; ++n) {
345 const block_q8_K *x_row = a->X + (size_t)n * (size_t)a->blocks_per_vec;
346 gemv_q4_k_q8_k(a->Y + (size_t)n * (size_t)a->M_out + (size_t)r0,
347 w_start,
348 x_row,
349 rows,
350 a->K);
351 }
352}
353
354void gemm_q4_k_q8_k(float *Y,
355 const void *W,
356 const void *X_q8,
357 int M, int N, int K)
358{
359 if (!Y || !W || !X_q8 || M <= 0 || N <= 0 || K <= 0) {
360 return;
361 }
362
363 const block_q8_K *X = (const block_q8_K *)X_q8;
364 const int blocks_per_vec = K / QK_K;
365 const int blocks_per_row = K / QK_K;
366 const size_t work_items = (size_t)M * (size_t)N;
367
368 if (work_items >= 4096u && M >= 512 && N > 1) {
369 ck_threadpool_t *pool = ck_threadpool_global();
370 const int pool_threads = pool ? ck_threadpool_n_threads(pool) : 1;
371 int active_threads = pool_threads;
372 if (active_threads > M) {
373 active_threads = M;
374 }
375 if (active_threads > 1) {
376 gemm_q4_k_q8_k_work_t work = {
377 .Y = Y,
378 .W = W,
379 .X = X,
380 .M_out = M,
381 .N_batch = N,
382 .K = K,
383 .blocks_per_vec = blocks_per_vec,
384 .blocks_per_row = blocks_per_row,
385 };
386 ck_threadpool_dispatch_n(pool, active_threads, gemm_q4_k_q8_k_thread_fn, &work);
387 return;
388 }
389 }
390
391 for (int n = 0; n < N; ++n) {
392 const block_q8_K *x_row = X + (size_t)n * (size_t)blocks_per_vec;
393 gemv_q4_k_q8_k(&Y[n * M], W, x_row, M, K);
394 }
395}
396
397void gemm_nt_q4_k_q8_k(const void *A_q8,
398 const void *B,
399 const float *bias,
400 float *C,
401 int M, int N, int K)
402{
403 if (!A_q8 || !B || !C) {
404 return;
405 }
406 if (M <= 0 || N <= 0 || K <= 0) {
407 return;
408 }
409
410 gemm_q4_k_q8_k(C, B, A_q8, /*M_out=*/N, /*N_batch=*/M, K);
411
412 if (!bias) {
413 return;
414 }
415
416 for (int i = 0; i < M; ++i) {
417 float *row = C + (size_t)i * (size_t)N;
418 for (int j = 0; j < N; ++j) {
419 row[j] += bias[j];
420 }
421 }
422}
Persistent pthread thread pool for CK-Engine inference.
void ck_threadpool_dispatch_n(ck_threadpool_t *pool, int active_threads, ck_work_fn_t fn, void *args)
ck_threadpool_t * ck_threadpool_global(void)
int ck_threadpool_n_threads(const ck_threadpool_t *pool)
Quantization block structures for weight-only quantization.
#define CK_FP16_TO_FP32(x)
static void unpack_q4_k_scales(const uint8_t *scales, uint8_t *sc, uint8_t *m)
Unpack Q4_K sub-block scales and mins.
#define QK_K
void gemv_q4_k_q8_k_avx2(float *y, const void *W, const void *x_q8, int M, int K)
void quantize_row_q8_k_avx512(const float *x, void *vy, int k)
void quantize_row_q8_k_avx2(const float *x, void *vy, int k)
void gemv_q4_k_q8_k_vnni(float *y, const void *W, const void *x_q8, int M, int K)
void quantize_row_q8_k(const float *x, void *vy, int k)
void gemm_nt_q4_k_q8_k(const void *A_q8, const void *B, const float *bias, float *C, int M, int N, int K)
void gemv_q4_k_q8_k_parallel(float *y, const void *W, const void *x_q8, int M, int K, int ith, int nth)
void quantize_batch_q8_k_4row_nearest_even(const float *x, void *vy, int num_rows, int k)
void gemm_q4_k_q8_k_ref(float *Y, const void *W, const void *X_q8, int M, int N, int K)
static int ck_nearest_int(float fval)
void quantize_row_q8_k_avx(const float *x, void *vy, int k)
void gemv_q4_k_q8_k(float *y, const void *W, const void *x_q8, int M, int K)
void gemv_q4_k_q8_k_ref(float *y, const void *W, const void *x_q8, int M, int K)
void gemm_q4_k_q8_k(float *Y, const void *W, const void *X_q8, int M, int N, int K)
static int ck_q4k_q8k_force_ref(void)
void quantize_row_q8_k_sse(const float *x, void *vy, int k)
static float dot_q4_k_q8_k_ref(const block_q4_K *w, const block_q8_K *x, int k)
static void gemm_q4_k_q8_k_thread_fn(int ith, int nth, void *args)
void quantize_row_q8_k_ref(const float *x, void *vy, int k)
void gemv_q4_k_q8_k_avx(float *y, const void *W, const void *x_q8, int M, int K)
void gemv_q4_k_q8_k_sse(float *y, const void *W, const void *x_q8, int M, int K)
#define C(color)
Definition show_config.c:39
uint8_t qs[256/2]
int8_t qs[256]
int16_t bsums[256/16]
__attribute__((visibility("default"))) CKTokenizer *ck_tokenizer_create(CKTokenizerType type)