← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
gemm_kernels_q4k.c
Go to the documentation of this file.
1/**
2 * @file gemm_kernels_q4k.c
3 * @brief GEMM/GEMV kernels with Q4_K quantized weights
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 matrix multiplication where:
15 * - Activations (input): FP32
16 * - Weights: Q4_K (4.5 bits/weight, nested scales)
17 * - Output: FP32
18 *
19 * Key optimization: Fused dequantization - weights are dequantized in
20 * registers and immediately used in FMA, never written to memory.
21 *
22 * Operations:
23 * - gemv_q4_k: Matrix-vector multiply (batch=1, token generation)
24 * - gemm_q4_k: Matrix-matrix multiply (batch>1, prefill)
25 */
26
27#include <stdint.h>
28#include <stddef.h>
29#include <string.h>
30#include <stdlib.h>
31#include "ckernel_quant.h"
32
33/* Include SIMD headers based on available extensions */
34#if defined(__AVX512F__) || defined(__AVX2__) || defined(__AVX__) || defined(__SSE4_1__)
35#include <immintrin.h>
36#endif
37
38#define CK_Q4K_STACK_Q8_BLOCKS 128
39
40void quantize_row_q8_k(const float *x, void *vy, int k);
41void gemv_q4_k_q8_k(float *y, const void *W, const void *x_q8, int M, int K);
42
44{
45 static int cached = -1;
46 if (cached < 0) {
47 const char *env = getenv("CK_DEBUG_Q4K_Q8_CONTRACT");
48 cached = (env && env[0] && env[0] != '0') ? 1 : 0;
49 }
50 return cached;
51}
52
53/* ============================================================================
54 * GEMV: y = W @ x (W is Q4_K, x and y are FP32)
55 *
56 * For token generation (batch=1), this is the critical path.
57 * Memory-bound: we're loading ~4GB of weights for a 7B model per token.
58 * ============================================================================ */
59
60/**
61 * @brief Matrix-vector multiply with Q4_K weights (scalar reference)
62 *
63 * @param y Output vector [M]
64 * @param W Weight matrix in Q4_K format [M x K], stored row-major
65 * @param x Input vector [K]
66 * @param M Number of output rows
67 * @param K Number of columns (must be multiple of 256)
68 */
69void gemv_q4_k_ref(float *y,
70 const void *W,
71 const float *x,
72 int M, int K)
73{
74 const block_q4_K *blocks = (const block_q4_K *)W;
75 const int blocks_per_row = K / QK_K; /* QK_K = 256 */
76
77 for (int row = 0; row < M; row++) {
78 float sum = 0.0f;
79
80 for (int b = 0; b < blocks_per_row; b++) {
81 const block_q4_K *block = &blocks[row * blocks_per_row + b];
82 const float d = GGML_FP16_TO_FP32(block->d);
83 const float dmin = GGML_FP16_TO_FP32(block->dmin);
84
85 /* Unpack sub-block scales */
86 uint8_t sc[8], m[8];
87 unpack_q4_k_scales(block->scales, sc, m);
88
89 /* llama.cpp Q4_K layout: 4 iterations of 64 weights each
90 * Each iteration uses 32 bytes of qs and 2 scales:
91 * - First 32 weights (indices 0-31): low nibbles with scale[2*iter]
92 * - Next 32 weights (indices 32-63): high nibbles with scale[2*iter+1]
93 */
94 for (int iter = 0; iter < 4; iter++) {
95 const float d1 = d * (float)sc[2*iter];
96 const float m1 = dmin * (float)m[2*iter];
97 const float d2 = d * (float)sc[2*iter + 1];
98 const float m2 = dmin * (float)m[2*iter + 1];
99 const uint8_t *qs = &block->qs[iter * 32];
100 const float *xp = &x[b * QK_K + iter * 64];
101
102 /* First 32 weights: low nibbles of qs[0..31] */
103 for (int l = 0; l < 32; l++) {
104 const int8_t q = (qs[l] & 0x0F);
105 sum += (d1 * (float)q - m1) * xp[l];
106 }
107 /* Next 32 weights: high nibbles of qs[0..31] */
108 for (int l = 0; l < 32; l++) {
109 const int8_t q = (qs[l] >> 4);
110 sum += (d2 * (float)q - m2) * xp[l + 32];
111 }
112 }
113 }
114
115 y[row] = sum;
116 }
117}
118
119#ifdef __AVX512F__
120/**
121 * @brief Matrix-vector multiply with Q4_K weights (AVX-512 optimized)
122 *
123 * Fused dequant + FMA: weights dequantized in ZMM registers, never touch RAM.
124 */
125void gemv_q4_k_avx512(float *y,
126 const void *W,
127 const float *x,
128 int M, int K)
129{
130 const block_q4_K *blocks = (const block_q4_K *)W;
131 const int blocks_per_row = K / QK_K;
132
133 for (int row = 0; row < M; row++) {
134 __m512 acc = _mm512_setzero_ps();
135
136 for (int b = 0; b < blocks_per_row; b++) {
137 const block_q4_K *block = &blocks[row * blocks_per_row + b];
138 const float d = GGML_FP16_TO_FP32(block->d);
139 const float dmin = GGML_FP16_TO_FP32(block->dmin);
140
141 uint8_t sc[8], m_arr[8];
142 unpack_q4_k_scales(block->scales, sc, m_arr);
143
144 const __m512i mask_lo = _mm512_set1_epi32(0x0F);
145
146 /* llama.cpp Q4_K layout: 4 iterations of 64 weights each
147 * Formula: w = d * q - m (NOT d * (q-8) + m)
148 */
149 for (int iter = 0; iter < 4; iter++) {
150 const float d1 = d * (float)sc[2*iter];
151 const float m1 = dmin * (float)m_arr[2*iter];
152 const float d2 = d * (float)sc[2*iter + 1];
153 const float m2 = dmin * (float)m_arr[2*iter + 1];
154
155 const __m512 vscale1 = _mm512_set1_ps(d1);
156 const __m512 vmin1 = _mm512_set1_ps(m1);
157 const __m512 vscale2 = _mm512_set1_ps(d2);
158 const __m512 vmin2 = _mm512_set1_ps(m2);
159
160 const uint8_t *qs = &block->qs[iter * 32];
161 const float *xp = &x[b * QK_K + iter * 64];
162
163 /* Process first 32 weights (low nibbles) */
164 /* Load 16 bytes at a time */
165 for (int chunk = 0; chunk < 2; chunk++) {
166 __m128i packed = _mm_loadu_si128((const __m128i *)&qs[chunk * 16]);
167 __m512i bytes = _mm512_cvtepu8_epi32(packed);
168 __m512i lo = _mm512_and_epi32(bytes, mask_lo);
169 /* w = d * q - m: use fnmadd (negative m) */
170 __m512 w = _mm512_fnmadd_ps(_mm512_set1_ps(1.0f), vmin1,
171 _mm512_mul_ps(_mm512_cvtepi32_ps(lo), vscale1));
172 __m512 x_vec = _mm512_loadu_ps(&xp[chunk * 16]);
173 acc = _mm512_fmadd_ps(w, x_vec, acc);
174 }
175
176 /* Process next 32 weights (high nibbles) */
177 for (int chunk = 0; chunk < 2; chunk++) {
178 __m128i packed = _mm_loadu_si128((const __m128i *)&qs[chunk * 16]);
179 __m512i bytes = _mm512_cvtepu8_epi32(packed);
180 __m512i hi = _mm512_srli_epi32(bytes, 4);
181 /* w = d * q - m: use fnmadd (negative m) */
182 __m512 w = _mm512_fnmadd_ps(_mm512_set1_ps(1.0f), vmin2,
183 _mm512_mul_ps(_mm512_cvtepi32_ps(hi), vscale2));
184 __m512 x_vec = _mm512_loadu_ps(&xp[32 + chunk * 16]);
185 acc = _mm512_fmadd_ps(w, x_vec, acc);
186 }
187 }
188 }
189
190 /* Horizontal sum */
191 y[row] = _mm512_reduce_add_ps(acc);
192 }
193}
194#endif /* __AVX512F__ */
195
196/* ============================================================================
197 * AVX Implementation (256-bit, works on Sandy Bridge and later)
198 *
199 * This is critical for CPUs that have AVX but not AVX-512.
200 * Processes 8 floats per iteration using 256-bit registers.
201 * NOTE: Uses separate mul+add (no FMA) for Ivy Bridge compatibility.
202 * ============================================================================ */
203
204#if defined(__AVX__) && !defined(__AVX512F__)
205/**
206 * @brief Matrix-vector multiply with Q4_K weights (AVX optimized)
207 *
208 * Processes 8 floats at a time. No FMA required (works on Ivy Bridge).
209 * About 4-8x faster than scalar reference.
210 */
211void gemv_q4_k_avx(float *y,
212 const void *W,
213 const float *x,
214 int M, int K)
215{
216 const block_q4_K *blocks = (const block_q4_K *)W;
217 const int blocks_per_row = K / QK_K; /* QK_K = 256 */
218
219 for (int row = 0; row < M; row++) {
220 /* Use 4 accumulators for better instruction-level parallelism */
221 __m256 acc0 = _mm256_setzero_ps();
222 __m256 acc1 = _mm256_setzero_ps();
223 __m256 acc2 = _mm256_setzero_ps();
224 __m256 acc3 = _mm256_setzero_ps();
225
226 for (int b = 0; b < blocks_per_row; b++) {
227 const block_q4_K *block = &blocks[row * blocks_per_row + b];
228 const float d = GGML_FP16_TO_FP32(block->d);
229 const float dmin = GGML_FP16_TO_FP32(block->dmin);
230
231 /* Unpack sub-block scales */
232 uint8_t sc[8], m_arr[8];
233 unpack_q4_k_scales(block->scales, sc, m_arr);
234
235 /* Process 256 weights in 4 iterations of 64 weights each */
236 for (int iter = 0; iter < 4; iter++) {
237 const float d1 = d * (float)sc[2*iter];
238 const float m1 = dmin * (float)m_arr[2*iter];
239 const float d2 = d * (float)sc[2*iter + 1];
240 const float m2 = dmin * (float)m_arr[2*iter + 1];
241 const uint8_t *qs = &block->qs[iter * 32];
242 const float *xp = &x[b * QK_K + iter * 64];
243
244 /* Broadcast scale and min values */
245 __m256 vd1 = _mm256_set1_ps(d1);
246 __m256 vm1 = _mm256_set1_ps(m1);
247 __m256 vd2 = _mm256_set1_ps(d2);
248 __m256 vm2 = _mm256_set1_ps(m2);
249
250 /* Process first 32 weights (low nibbles) in 4 groups of 8 */
251 for (int g = 0; g < 4; g++) {
252 /* Dequantize 8 weights from low nibbles */
253 float dq[8];
254 for (int i = 0; i < 8; i++) {
255 dq[i] = d1 * (float)(qs[g*8 + i] & 0x0F) - m1;
256 }
257 __m256 vw = _mm256_loadu_ps(dq);
258 __m256 vx = _mm256_loadu_ps(&xp[g*8]);
259
260 /* acc0 += vw * vx (using mul+add, no FMA needed) */
261 __m256 prod = _mm256_mul_ps(vw, vx);
262 acc0 = _mm256_add_ps(acc0, prod);
263 }
264
265 /* Process next 32 weights (high nibbles) in 4 groups of 8 */
266 for (int g = 0; g < 4; g++) {
267 /* Dequantize 8 weights from high nibbles */
268 float dq[8];
269 for (int i = 0; i < 8; i++) {
270 dq[i] = d2 * (float)(qs[g*8 + i] >> 4) - m2;
271 }
272 __m256 vw = _mm256_loadu_ps(dq);
273 __m256 vx = _mm256_loadu_ps(&xp[32 + g*8]);
274
275 __m256 prod = _mm256_mul_ps(vw, vx);
276 acc1 = _mm256_add_ps(acc1, prod);
277 }
278 }
279 }
280
281 /* Combine accumulators */
282 __m256 sum01 = _mm256_add_ps(acc0, acc1);
283 __m256 sum23 = _mm256_add_ps(acc2, acc3);
284 __m256 sum = _mm256_add_ps(sum01, sum23);
285
286 /* Horizontal sum of 8 floats */
287 __m128 hi = _mm256_extractf128_ps(sum, 1);
288 __m128 lo = _mm256_castps256_ps128(sum);
289 __m128 sum128 = _mm_add_ps(hi, lo);
290 sum128 = _mm_hadd_ps(sum128, sum128);
291 sum128 = _mm_hadd_ps(sum128, sum128);
292
293 y[row] = _mm_cvtss_f32(sum128);
294 }
295}
296#endif /* __AVX__ && !__AVX512F__ */
297
298/**
299 * @brief Auto-dispatch GEMV based on available SIMD
300 */
301void gemv_q4_k(float *y,
302 const void *W,
303 const float *x,
304 int M, int K)
305{
306 if (ck_q4k_debug_q8_contract() && K > 0 && (K % QK_K) == 0) {
307 const int nb = K / QK_K;
308 if (nb <= CK_Q4K_STACK_Q8_BLOCKS) {
310 quantize_row_q8_k(x, x_q8, K);
311 gemv_q4_k_q8_k(y, W, x_q8, M, K);
312 return;
313 }
314 }
315#ifdef __AVX512F__
316 gemv_q4_k_avx512(y, W, x, M, K);
317#elif defined(__AVX__)
318 gemv_q4_k_avx(y, W, x, M, K);
319#else
320 gemv_q4_k_ref(y, W, x, M, K);
321#endif
322}
323
324/* ============================================================================
325 * GEMM: Y = W @ X (W is Q4_K, X and Y are FP32)
326 *
327 * For prefill (batch > 1), we can amortize weight loading across batch.
328 * More compute-bound than GEMV.
329 * ============================================================================ */
330
331/**
332 * @brief Matrix-matrix multiply with Q4_K weights (scalar reference)
333 *
334 * @param Y Output matrix [M x N]
335 * @param W Weight matrix in Q4_K format [M x K]
336 * @param X Input matrix [K x N] (column-major for cache efficiency)
337 * @param M Number of output rows
338 * @param N Batch size (number of columns)
339 * @param K Hidden dimension
340 */
341void gemm_q4_k_ref(float *Y,
342 const void *W,
343 const float *X,
344 int M, int N, int K)
345{
346 /* For each column in batch, use the dispatching gemv_q4_k
347 * which automatically selects AVX/AVX-512/scalar based on CPU */
348 for (int n = 0; n < N; n++) {
349 gemv_q4_k(&Y[n * M], W, &X[n * K], M, K);
350 }
351}
352
353#ifdef __AVX512F__
354/**
355 * @brief Matrix-matrix multiply with Q4_K weights (AVX-512)
356 *
357 * Processes multiple batch elements to improve weight reuse.
358 */
359void gemm_q4_k_avx512(float *Y,
360 const void *W,
361 const float *X,
362 int M, int N, int K)
363{
364 const block_q4_K *blocks = (const block_q4_K *)W;
365 const int blocks_per_row = K / QK_K;
366
367 /* Process 4 batch elements at a time for better register utilization */
368 const int N4 = N / 4 * 4;
369
370 for (int row = 0; row < M; row++) {
371 /* Batch of 4 */
372 for (int n = 0; n < N4; n += 4) {
373 __m512 acc0 = _mm512_setzero_ps();
374 __m512 acc1 = _mm512_setzero_ps();
375 __m512 acc2 = _mm512_setzero_ps();
376 __m512 acc3 = _mm512_setzero_ps();
377
378 for (int b = 0; b < blocks_per_row; b++) {
379 const block_q4_K *block = &blocks[row * blocks_per_row + b];
380 const float d = GGML_FP16_TO_FP32(block->d);
381 const float dmin = GGML_FP16_TO_FP32(block->dmin);
382
383 uint8_t sc[8], m_arr[8];
384 unpack_q4_k_scales(block->scales, sc, m_arr);
385
386 for (int sub = 0; sub < 8; sub++) {
387 const float scale = d * (float)sc[sub];
388 const float min_val = dmin * (float)m_arr[sub];
389 const __m512 vscale = _mm512_set1_ps(scale);
390 const __m512 vmin = _mm512_set1_ps(min_val);
391 const __m512i offset = _mm512_set1_epi32(8);
392 const __m512i mask_lo = _mm512_set1_epi32(0x0F);
393
394 const uint8_t *qs = &block->qs[sub * 16];
395 const int x_offset = b * QK_K + sub * 32;
396
397 /* Dequantize weights (same for all batch elements) */
398 __m128i packed = _mm_loadu_si128((const __m128i *)qs);
399 __m512i bytes = _mm512_cvtepu8_epi32(packed);
400
401 __m512i lo = _mm512_sub_epi32(_mm512_and_epi32(bytes, mask_lo), offset);
402 __m512i hi = _mm512_sub_epi32(_mm512_srli_epi32(bytes, 4), offset);
403
404 __m512 w_lo = _mm512_fmadd_ps(_mm512_cvtepi32_ps(lo), vscale, vmin);
405 __m512 w_hi = _mm512_fmadd_ps(_mm512_cvtepi32_ps(hi), vscale, vmin);
406
407 /* Load inputs for 4 batch elements and accumulate */
408 /* (simplified - full impl would handle interleaving) */
409 for (int bn = 0; bn < 4; bn++) {
410 const float *xp = &X[(n + bn) * K + x_offset];
411
412 __m512 x_even = _mm512_set_ps(
413 xp[30], xp[28], xp[26], xp[24], xp[22], xp[20], xp[18], xp[16],
414 xp[14], xp[12], xp[10], xp[8], xp[6], xp[4], xp[2], xp[0]);
415 __m512 x_odd = _mm512_set_ps(
416 xp[31], xp[29], xp[27], xp[25], xp[23], xp[21], xp[19], xp[17],
417 xp[15], xp[13], xp[11], xp[9], xp[7], xp[5], xp[3], xp[1]);
418
419 __m512 *acc = (bn == 0) ? &acc0 : (bn == 1) ? &acc1 :
420 (bn == 2) ? &acc2 : &acc3;
421 *acc = _mm512_fmadd_ps(w_lo, x_even, *acc);
422 *acc = _mm512_fmadd_ps(w_hi, x_odd, *acc);
423 }
424 }
425 }
426
427 Y[(n + 0) * M + row] = _mm512_reduce_add_ps(acc0);
428 Y[(n + 1) * M + row] = _mm512_reduce_add_ps(acc1);
429 Y[(n + 2) * M + row] = _mm512_reduce_add_ps(acc2);
430 Y[(n + 3) * M + row] = _mm512_reduce_add_ps(acc3);
431 }
432
433 /* Remainder */
434 for (int n = N4; n < N; n++) {
435 __m512 acc = _mm512_setzero_ps();
436
437 for (int b = 0; b < blocks_per_row; b++) {
438 const block_q4_K *block = &blocks[row * blocks_per_row + b];
439 const float d = GGML_FP16_TO_FP32(block->d);
440 const float dmin = GGML_FP16_TO_FP32(block->dmin);
441
442 uint8_t sc[8], m_arr[8];
443 unpack_q4_k_scales(block->scales, sc, m_arr);
444
445 for (int sub = 0; sub < 8; sub++) {
446 const float scale = d * (float)sc[sub];
447 const float min_val = dmin * (float)m_arr[sub];
448 const __m512 vscale = _mm512_set1_ps(scale);
449 const __m512 vmin = _mm512_set1_ps(min_val);
450 const __m512i offset = _mm512_set1_epi32(8);
451 const __m512i mask_lo = _mm512_set1_epi32(0x0F);
452
453 const uint8_t *qs = &block->qs[sub * 16];
454 const float *xp = &X[n * K + b * QK_K + sub * 32];
455
456 __m128i packed = _mm_loadu_si128((const __m128i *)qs);
457 __m512i bytes = _mm512_cvtepu8_epi32(packed);
458
459 __m512i lo = _mm512_sub_epi32(_mm512_and_epi32(bytes, mask_lo), offset);
460 __m512i hi = _mm512_sub_epi32(_mm512_srli_epi32(bytes, 4), offset);
461
462 __m512 w_lo = _mm512_fmadd_ps(_mm512_cvtepi32_ps(lo), vscale, vmin);
463 __m512 w_hi = _mm512_fmadd_ps(_mm512_cvtepi32_ps(hi), vscale, vmin);
464
465 __m512 x_even = _mm512_set_ps(
466 xp[30], xp[28], xp[26], xp[24], xp[22], xp[20], xp[18], xp[16],
467 xp[14], xp[12], xp[10], xp[8], xp[6], xp[4], xp[2], xp[0]);
468 __m512 x_odd = _mm512_set_ps(
469 xp[31], xp[29], xp[27], xp[25], xp[23], xp[21], xp[19], xp[17],
470 xp[15], xp[13], xp[11], xp[9], xp[7], xp[5], xp[3], xp[1]);
471
472 acc = _mm512_fmadd_ps(w_lo, x_even, acc);
473 acc = _mm512_fmadd_ps(w_hi, x_odd, acc);
474 }
475 }
476
477 Y[n * M + row] = _mm512_reduce_add_ps(acc);
478 }
479 }
480}
481#endif /* __AVX512F__ */
482
483/**
484 * @brief Auto-dispatch GEMM based on available SIMD
485 */
486void gemm_q4_k(float *Y,
487 const void *W,
488 const float *X,
489 int M, int N, int K)
490{
491 /* Use reference implementation for correctness
492 * TODO: Fix AVX-512 version to match llama.cpp layout */
493 gemm_q4_k_ref(Y, W, X, M, N, K);
494}
495
496/* ============================================================================
497 * Dot Product: Single row dot product with Q4_K weights
498 * Used internally and for testing.
499 * ============================================================================ */
500
501/**
502 * @brief Compute dot product of Q4_K row with FP32 vector
503 *
504 * @param w_q4k Q4_K blocks for one row
505 * @param x FP32 input vector
506 * @param K Vector length (must be multiple of 256)
507 * @return Dot product result
508 */
509float dot_q4_k(const void *w_q4k, const float *x, int K)
510{
511 float result;
512 gemv_q4_k(&result, w_q4k, x, 1, K);
513 return result;
514}
515
516/* ============================================================================
517 * Backward Pass: Gradient w.r.t. Input
518 *
519 * Given: dL/dY (gradient of loss w.r.t. output)
520 * Compute: dL/dX = W^T @ dL/dY
521 *
522 * For quantized weights, we dequantize on-the-fly during backprop.
523 * Weight gradients are NOT computed (weights are frozen).
524 * For fine-tuning, use LoRA adapters which maintain FP32 gradients separately.
525 * ============================================================================ */
526
527/**
528 * @brief Backward pass: compute input gradient (scalar reference)
529 *
530 * @param dX Output gradient w.r.t. input [K]
531 * @param W Weight matrix in Q4_K format [M x K]
532 * @param dY Gradient w.r.t. output [M]
533 * @param M Number of output rows
534 * @param K Number of columns (input dimension)
535 */
537 const void *W,
538 const float *dY,
539 int M, int K)
540{
541 const block_q4_K *blocks = (const block_q4_K *)W;
542 const int blocks_per_row = K / QK_K;
543
544 /* Zero output gradient */
545 memset(dX, 0, K * sizeof(float));
546
547 /* Accumulate: dX += W^T @ dY
548 * Uses llama.cpp layout: 4 iterations of 64 weights each */
549 for (int row = 0; row < M; row++) {
550 const float dy = dY[row];
551
552 for (int b = 0; b < blocks_per_row; b++) {
553 const block_q4_K *block = &blocks[row * blocks_per_row + b];
554 const float d = CK_FP16_TO_FP32(block->d);
555 const float dmin = CK_FP16_TO_FP32(block->dmin);
556
557 uint8_t sc[8], m[8];
558 unpack_q4_k_scales(block->scales, sc, m);
559
560 /* llama.cpp layout: 4 iterations of 64 weights each */
561 for (int iter = 0; iter < 4; iter++) {
562 const float d1 = d * (float)sc[2 * iter];
563 const float m1 = dmin * (float)m[2 * iter];
564 const float d2 = d * (float)sc[2 * iter + 1];
565 const float m2 = dmin * (float)m[2 * iter + 1];
566
567 const uint8_t *qs = &block->qs[iter * 32];
568 float *dxp = &dX[b * QK_K + iter * 64];
569
570 /* First 32 weights: low nibbles */
571 for (int l = 0; l < 32; l++) {
572 const int q = (qs[l] & 0x0F);
573 const float w = d1 * (float)q - m1;
574 dxp[l] += w * dy;
575 }
576
577 /* Next 32 weights: high nibbles */
578 for (int l = 0; l < 32; l++) {
579 const int q = (qs[l] >> 4);
580 const float w = d2 * (float)q - m2;
581 dxp[32 + l] += w * dy;
582 }
583 }
584 }
585 }
586}
587
588#ifdef __AVX512F__
589/**
590 * @brief Backward pass with AVX-512
591 *
592 * Uses llama.cpp layout: 4 iterations of 64 weights each
593 */
594void gemv_q4_k_backward_avx512(float *dX,
595 const void *W,
596 const float *dY,
597 int M, int K)
598{
599 const block_q4_K *blocks = (const block_q4_K *)W;
600 const int blocks_per_row = K / QK_K;
601 const __m512i mask_lo = _mm512_set1_epi32(0x0F);
602
603 /* Zero output */
604 memset(dX, 0, K * sizeof(float));
605
606 for (int row = 0; row < M; row++) {
607 const __m512 vdy = _mm512_set1_ps(dY[row]);
608
609 for (int b = 0; b < blocks_per_row; b++) {
610 const block_q4_K *block = &blocks[row * blocks_per_row + b];
611 const float d = CK_FP16_TO_FP32(block->d);
612 const float dmin = CK_FP16_TO_FP32(block->dmin);
613
614 uint8_t sc[8], m_arr[8];
615 unpack_q4_k_scales(block->scales, sc, m_arr);
616
617 /* llama.cpp layout: 4 iterations of 64 weights each */
618 for (int iter = 0; iter < 4; iter++) {
619 const float d1 = d * (float)sc[2 * iter];
620 const float m1 = dmin * (float)m_arr[2 * iter];
621 const float d2 = d * (float)sc[2 * iter + 1];
622 const float m2 = dmin * (float)m_arr[2 * iter + 1];
623
624 const __m512 vd1 = _mm512_set1_ps(d1);
625 const __m512 vm1 = _mm512_set1_ps(m1);
626 const __m512 vd2 = _mm512_set1_ps(d2);
627 const __m512 vm2 = _mm512_set1_ps(m2);
628
629 const uint8_t *qs = &block->qs[iter * 32];
630 float *dxp = &dX[b * QK_K + iter * 64];
631
632 /* Process first 32 weights (low nibbles) */
633 for (int chunk = 0; chunk < 2; chunk++) {
634 __m128i packed = _mm_loadu_si128((const __m128i *)&qs[chunk * 16]);
635 __m512i bytes = _mm512_cvtepu8_epi32(packed);
636 __m512i lo = _mm512_and_epi32(bytes, mask_lo);
637 /* w = d1 * q - m1 */
638 __m512 w = _mm512_fnmadd_ps(_mm512_set1_ps(1.0f), vm1,
639 _mm512_mul_ps(_mm512_cvtepi32_ps(lo), vd1));
640 __m512 grad = _mm512_mul_ps(w, vdy);
641 __m512 existing = _mm512_loadu_ps(&dxp[chunk * 16]);
642 _mm512_storeu_ps(&dxp[chunk * 16], _mm512_add_ps(existing, grad));
643 }
644
645 /* Process next 32 weights (high nibbles) */
646 for (int chunk = 0; chunk < 2; chunk++) {
647 __m128i packed = _mm_loadu_si128((const __m128i *)&qs[chunk * 16]);
648 __m512i bytes = _mm512_cvtepu8_epi32(packed);
649 __m512i hi = _mm512_srli_epi32(bytes, 4);
650 /* w = d2 * q - m2 */
651 __m512 w = _mm512_fnmadd_ps(_mm512_set1_ps(1.0f), vm2,
652 _mm512_mul_ps(_mm512_cvtepi32_ps(hi), vd2));
653 __m512 grad = _mm512_mul_ps(w, vdy);
654 __m512 existing = _mm512_loadu_ps(&dxp[32 + chunk * 16]);
655 _mm512_storeu_ps(&dxp[32 + chunk * 16], _mm512_add_ps(existing, grad));
656 }
657 }
658 }
659 }
660}
661#endif
662
663/**
664 * @brief Auto-dispatch backward
665 */
666void gemv_q4_k_backward(float *dX,
667 const void *W,
668 const float *dY,
669 int M, int K)
670{
671#ifdef __AVX512F__
672 gemv_q4_k_backward_avx512(dX, W, dY, M, K);
673#else
674 gemv_q4_k_backward_ref(dX, W, dY, M, K);
675#endif
676}
677
678/**
679 * @brief Batched backward pass
680 */
681void gemm_q4_k_backward(float *dX,
682 const void *W,
683 const float *dY,
684 int M, int N, int K)
685{
686 for (int n = 0; n < N; n++) {
687 gemv_q4_k_backward(&dX[n * K], W, &dY[n * M], M, K);
688 }
689}
690
691/* ============================================================================
692 * Engine-compatible wrapper: GEMM_NT with Q4_K weights
693 *
694 * The core q4_k kernels in this file use the convention:
695 * - W: [M_out x K] (quantized row-major)
696 * - X: [N_batch x K] (fp32)
697 * - Y: [N_batch x M_out] (fp32)
698 *
699 * The C-Kernel-Engine convention for NN weights uses:
700 * - A: [M_tokens x K] (fp32)
701 * - B: [N_out x K] (quantized row-major, transposed layout)
702 * - C: [M_tokens x N_out] (fp32)
703 *
704 * This wrapper swaps (M_out, N_batch) to match the engine layout and applies
705 * an optional bias.
706 * ============================================================================ */
707
708void gemm_nt_q4_k(const float *A,
709 const void *B,
710 const float *bias,
711 float *C,
712 int M, int N, int K)
713{
714 if (!A || !B || !C) {
715 return;
716 }
717 if (M <= 0 || N <= 0 || K <= 0) {
718 return;
719 }
720
721 /* gemm_q4_k produces Y as [batch x M_out]. Here:
722 * batch = M (tokens)
723 * M_out = N (output channels) */
724 gemm_q4_k(C, B, A, /*M_out=*/N, /*N_batch=*/M, K);
725
726 if (!bias) {
727 return;
728 }
729
730 for (int i = 0; i < M; ++i) {
731 float *row = C + (size_t)i * (size_t)N;
732 for (int j = 0; j < N; ++j) {
733 row[j] += bias[j];
734 }
735 }
736}
Quantization block structures for weight-only quantization.
#define GGML_FP16_TO_FP32
#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 gemm_q4_k_ref(float *Y, const void *W, const float *X, int M, int N, int K)
Matrix-matrix multiply with Q4_K weights (scalar reference)
float dot_q4_k(const void *w_q4k, const float *x, int K)
Compute dot product of Q4_K row with FP32 vector.
void gemm_q4_k_backward(float *dX, const void *W, const float *dY, int M, int N, int K)
Batched backward pass.
void gemm_nt_q4_k(const float *A, const void *B, const float *bias, float *C, int M, int N, int K)
void gemv_q4_k_backward(float *dX, const void *W, const float *dY, int M, int K)
Auto-dispatch backward.
void gemv_q4_k_ref(float *y, const void *W, const float *x, int M, int K)
Matrix-vector multiply with Q4_K weights (scalar reference)
#define CK_Q4K_STACK_Q8_BLOCKS
void gemv_q4_k_backward_ref(float *dX, const void *W, const float *dY, int M, int K)
Backward pass: compute input gradient (scalar reference)
void quantize_row_q8_k(const float *x, void *vy, int k)
void gemv_q4_k(float *y, const void *W, const float *x, int M, int K)
Auto-dispatch GEMV based on available SIMD.
void gemv_q4_k_q8_k(float *y, const void *W, const void *x_q8, int M, int K)
void gemm_q4_k(float *Y, const void *W, const float *X, int M, int N, int K)
Auto-dispatch GEMM based on available SIMD.
static int ck_q4k_debug_q8_contract(void)
#define C(color)
Definition show_config.c:39
uint8_t scales[12]
uint8_t qs[256/2]