← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
gemm_head_major_output.c
Go to the documentation of this file.
1/**
2 * @file gemm_head_major_output.c
3 * @brief Output projection from head-major attention (NO LAYOUT CONVERSION)
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. NO memcpy for layout - use strided access, not copies
10 * 4. API must define: inputs, outputs, workspace, and memory layouts
11 * 5. Pure computation - deterministic, no side effects
12 *
13 * After changes: make test && make llamacpp-parity-full
14 *
15 * PROBLEM THIS SOLVES:
16 * ====================
17 * The standard mega_fused_attention_prefill has a bottleneck:
18 * attn_out [num_heads, tokens, head_dim] (head-major)
19 * → flatten_head_major() - 448 memcpy calls for 32 tokens × 14 heads!
20 * → token-major buffer
21 * → GEMM output projection
22 *
23 * This kernel eliminates the flatten by reading head-major data directly with
24 * strided access. The output projection computes:
25 *
26 * output[t, n] = bias[n] + sum_h wo[n, h*head_dim:(h+1)*head_dim] @ attn_out[h, t, :]
27 *
28 * where wo is Q5_0 quantized [embed_dim, embed_dim] and attn_out is head-major.
29 *
30 * Expected speedup: 1.5-2x by eliminating 448 small memcpy calls.
31 */
32
33#include <stdint.h>
34#include <stddef.h>
35#include <string.h>
36#include "ckernel_quant.h"
37#include "ckernel_dtype.h"
38
39#if defined(__AVX__) || defined(__AVX2__) || defined(__AVX512F__)
40#include <immintrin.h>
41#endif
42
43/* Forward declaration from dequant_kernels.c */
44void dequant_q5_0_block(const block_q5_0 *block, float *output);
45void dequant_q5_0_row(const void *src, float *dst, size_t n_elements);
46
47/* ============================================================================
48 * Scalar reference: Output projection from head-major attention
49 * ============================================================================ */
50
51/**
52 * @brief Output projection reading head-major attention output (Q5_0 weights)
53 *
54 * @param output Output [tokens, embed_dim] (token-major, written contiguously)
55 * @param attn_out Attention output [num_heads, tokens, head_dim] (head-major, strided)
56 * @param wo Output weights in Q5_0 format [embed_dim, embed_dim]
57 * @param bias Optional bias [embed_dim]
58 * @param tokens Number of tokens
59 * @param embed_dim Output embedding dimension
60 * @param num_heads Number of attention heads
61 * @param head_dim Head dimension (must be multiple of 32 for Q5_0)
62 */
64 const float *attn_out,
65 const void *wo,
66 const float *bias,
67 int tokens,
68 int embed_dim,
69 int num_heads,
70 int head_dim)
71{
72 if (!output || !attn_out || !wo) return;
73 if (tokens <= 0 || embed_dim <= 0 || num_heads <= 0 || head_dim <= 0) return;
74
75 const int blocks_per_head = head_dim / QK5_0;
76 const int blocks_per_row = embed_dim / QK5_0;
77 const block_q5_0 *weights = (const block_q5_0 *)wo;
78
79 /* Strides for head-major layout */
80 const size_t token_stride = head_dim; /* attn_out[h][t] offset */
81 const size_t head_stride = (size_t)tokens * token_stride; /* attn_out[h] offset */
82
83 /* Initialize output with bias (if provided) */
84 if (bias) {
85 for (int t = 0; t < tokens; t++) {
86 float *out_row = output + (size_t)t * embed_dim;
87 for (int n = 0; n < embed_dim; n++) {
88 out_row[n] = bias[n];
89 }
90 }
91 } else {
92 memset(output, 0, (size_t)tokens * embed_dim * sizeof(float));
93 }
94
95 /* Accumulate contributions from each head */
96 for (int h = 0; h < num_heads; h++) {
97 const float *head_data = attn_out + (size_t)h * head_stride;
98
99 /* For each output row (n) corresponding to this head's slice */
100 const int head_offset = h * blocks_per_head;
101
102 for (int n_block = 0; n_block < blocks_per_head; n_block++) {
103 for (int n = 0; n < embed_dim; n++) {
104 const block_q5_0 *w_row = weights + (size_t)n * blocks_per_row + head_offset + n_block;
105 const float d = CK_FP16_TO_FP32(w_row->d);
106
107 /* Get high bits */
108 uint32_t qh;
109 memcpy(&qh, w_row->qh, sizeof(qh));
110
111 /* Accumulate for all tokens at once (better cache reuse) */
112 for (int t = 0; t < tokens; t++) {
113 const float *token_vec = head_data + (size_t)t * token_stride + (size_t)n_block * QK5_0;
114 float sum = 0.0f;
115
116 /* Q5_0 dot product for this block */
117 for (int j = 0; j < QK5_0 / 2; j++) {
118 const uint8_t packed = w_row->qs[j];
119 const int lo = (packed & 0x0F);
120 const int hi = (packed >> 4);
121 const int xh_0 = ((qh >> (j + 0)) << 4) & 0x10;
122 const int xh_1 = ((qh >> (j + 12))) & 0x10;
123 const int q0 = (lo | xh_0) - 16;
124 const int q1 = (hi | xh_1) - 16;
125
126 sum += d * (float)q0 * token_vec[j];
127 sum += d * (float)q1 * token_vec[j + 16];
128 }
129
130 output[(size_t)t * embed_dim + n] += sum;
131 }
132 }
133 }
134 }
135}
136
137/* ============================================================================
138 * Vectorized version with AVX (dot product over decoded 32-wide blocks)
139 * ============================================================================ */
140
141#if defined(__AVX__)
142
143static inline float hsum256_ps_head_major(__m256 v)
144{
145 __m128 lo = _mm256_castps256_ps128(v);
146 __m128 hi = _mm256_extractf128_ps(v, 1);
147 __m128 sum128 = _mm_add_ps(lo, hi);
148 __m128 shuf = _mm_movehdup_ps(sum128);
149 __m128 sums = _mm_add_ps(sum128, shuf);
150 shuf = _mm_movehl_ps(shuf, sums);
151 sums = _mm_add_ss(sums, shuf);
152 return _mm_cvtss_f32(sums);
153}
154
155static inline float dot_fp32_q5_0_block_decoded_avx(const float *w,
156 const float *x)
157{
158 __m256 acc = _mm256_setzero_ps();
159 for (int i = 0; i < QK5_0; i += 8) {
160 const __m256 wv = _mm256_loadu_ps(w + i);
161 const __m256 xv = _mm256_loadu_ps(x + i);
162 acc = _mm256_add_ps(acc, _mm256_mul_ps(wv, xv));
163 }
164 return hsum256_ps_head_major(acc);
165}
166
167static inline void decode_8rows_q5_0_block(const block_q5_0 *w0,
168 const block_q5_0 *w1,
169 const block_q5_0 *w2,
170 const block_q5_0 *w3,
171 const block_q5_0 *w4,
172 const block_q5_0 *w5,
173 const block_q5_0 *w6,
174 const block_q5_0 *w7,
175 float w_dec[8][QK5_0])
176{
177 dequant_q5_0_block(w0, w_dec[0]);
178 dequant_q5_0_block(w1, w_dec[1]);
179 dequant_q5_0_block(w2, w_dec[2]);
180 dequant_q5_0_block(w3, w_dec[3]);
181 dequant_q5_0_block(w4, w_dec[4]);
182 dequant_q5_0_block(w5, w_dec[5]);
183 dequant_q5_0_block(w6, w_dec[6]);
184 dequant_q5_0_block(w7, w_dec[7]);
185}
186
187static inline void accum_8rows_q5_0_block_decoded_avx(float *out,
188 const float w_dec[8][QK5_0],
189 const float *x)
190{
191 __m256 acc = _mm256_loadu_ps(out);
192 for (int i = 0; i < QK5_0; i++) {
193 const __m256 wv = _mm256_setr_ps(
194 w_dec[0][i], w_dec[1][i], w_dec[2][i], w_dec[3][i],
195 w_dec[4][i], w_dec[5][i], w_dec[6][i], w_dec[7][i]);
196 const __m256 xv = _mm256_set1_ps(x[i]);
197 acc = _mm256_add_ps(acc, _mm256_mul_ps(wv, xv));
198 }
199 _mm256_storeu_ps(out, acc);
200}
201
202void gemv_nt_q5_0_head_major_output_avx(float *output,
203 const float *attn_out,
204 const void *wo,
205 const float *bias,
206 int tokens,
207 int embed_dim,
208 int num_heads,
209 int head_dim)
210{
211 if (!output || !attn_out || !wo) return;
212 if (tokens <= 0 || embed_dim <= 0 || num_heads <= 0 || head_dim <= 0) return;
213
214 const int blocks_per_head = head_dim / QK5_0;
215 const int blocks_per_row = embed_dim / QK5_0;
216 const block_q5_0 *weights = (const block_q5_0 *)wo;
217
218 const size_t token_stride = head_dim;
219 const size_t head_stride = (size_t)tokens * token_stride;
220
221 if (bias) {
222 for (int t = 0; t < tokens; t++) {
223 float *out_row = output + (size_t)t * embed_dim;
224 for (int n = 0; n < embed_dim; n++) {
225 out_row[n] = bias[n];
226 }
227 }
228 } else {
229 memset(output, 0, (size_t)tokens * embed_dim * sizeof(float));
230 }
231
232 for (int h = 0; h < num_heads; h++) {
233 const float *head_data = attn_out + (size_t)h * head_stride;
234 const int head_offset = h * blocks_per_head;
235
236 int n = 0;
237 for (; n + 7 < embed_dim; n += 8) {
238 for (int n_block = 0; n_block < blocks_per_head; n_block++) {
239 const block_q5_0 *w0 = weights + (size_t)(n + 0) * blocks_per_row + head_offset + n_block;
240 const block_q5_0 *w1 = weights + (size_t)(n + 1) * blocks_per_row + head_offset + n_block;
241 const block_q5_0 *w2 = weights + (size_t)(n + 2) * blocks_per_row + head_offset + n_block;
242 const block_q5_0 *w3 = weights + (size_t)(n + 3) * blocks_per_row + head_offset + n_block;
243 const block_q5_0 *w4 = weights + (size_t)(n + 4) * blocks_per_row + head_offset + n_block;
244 const block_q5_0 *w5 = weights + (size_t)(n + 5) * blocks_per_row + head_offset + n_block;
245 const block_q5_0 *w6 = weights + (size_t)(n + 6) * blocks_per_row + head_offset + n_block;
246 const block_q5_0 *w7 = weights + (size_t)(n + 7) * blocks_per_row + head_offset + n_block;
247 float w_dec[8][QK5_0];
248
249 decode_8rows_q5_0_block(w0, w1, w2, w3, w4, w5, w6, w7, w_dec);
250
251 for (int t = 0; t < tokens; t++) {
252 const float *token_vec =
253 head_data + (size_t)t * token_stride + (size_t)n_block * QK5_0;
254 float *out_row = output + (size_t)t * embed_dim + n;
255 accum_8rows_q5_0_block_decoded_avx(out_row, w_dec, token_vec);
256 }
257 }
258 }
259
260 for (; n < embed_dim; n++) {
261 const block_q5_0 *w_row = weights + (size_t)n * blocks_per_row + head_offset;
262 for (int n_block = 0; n_block < blocks_per_head; n_block++) {
263 const block_q5_0 *w_block = w_row + n_block;
264 float w_dec[QK5_0];
265
266 dequant_q5_0_block(w_block, w_dec);
267 for (int t = 0; t < tokens; t++) {
268 const float *token_vec =
269 head_data + (size_t)t * token_stride + (size_t)n_block * QK5_0;
270 output[(size_t)t * embed_dim + n] +=
271 dot_fp32_q5_0_block_decoded_avx(w_dec, token_vec);
272 }
273 }
274 }
275 }
276}
277
278#endif /* __AVX__ */
279
280/* ============================================================================
281 * Generic dispatch
282 * ============================================================================ */
283
284/**
285 * @brief Output projection from head-major attention (auto-dispatch)
286 *
287 * This replaces flatten_head_major() + ck_gemm_nt_quant() with a single
288 * strided-access kernel that reads head-major attention output directly.
289 */
290void ck_gemm_nt_head_major_q5_0(const float *attn_out, /* [num_heads, tokens, head_dim] */
291 const void *wo,
292 const float *bias,
293 float *output, /* [tokens, embed_dim] */
294 int tokens,
295 int embed_dim,
296 int num_heads,
297 int head_dim)
298{
299#if defined(__AVX__)
300 gemv_nt_q5_0_head_major_output_avx(output, attn_out, wo, bias,
301 tokens, embed_dim, num_heads, head_dim);
302#else
303 gemv_nt_q5_0_head_major_output(output, attn_out, wo, bias,
304 tokens, embed_dim, num_heads, head_dim);
305#endif
306}
307
308/* ============================================================================
309 * Q8_0 variant (for V weights which are often Q8_0)
310 * ============================================================================ */
311
312/**
313 * @brief Output projection from head-major attention (Q8_0 weights)
314 */
315void ck_gemm_nt_head_major_q8_0(const float *attn_out,
316 const void *wo,
317 const float *bias,
318 float *output,
319 int tokens,
320 int embed_dim,
321 int num_heads,
322 int head_dim)
323{
324 if (!output || !attn_out || !wo) return;
325 if (tokens <= 0 || embed_dim <= 0 || num_heads <= 0 || head_dim <= 0) return;
326
327 const int blocks_per_head = head_dim / QK8_0;
328 const int blocks_per_row = embed_dim / QK8_0;
329 const block_q8_0 *weights = (const block_q8_0 *)wo;
330
331 const size_t token_stride = head_dim;
332 const size_t head_stride = (size_t)tokens * token_stride;
333
334 /* Initialize output */
335 if (bias) {
336 for (int t = 0; t < tokens; t++) {
337 float *out_row = output + (size_t)t * embed_dim;
338 for (int n = 0; n < embed_dim; n++) {
339 out_row[n] = bias[n];
340 }
341 }
342 } else {
343 memset(output, 0, (size_t)tokens * embed_dim * sizeof(float));
344 }
345
346 /* Accumulate from each head */
347 for (int h = 0; h < num_heads; h++) {
348 const float *head_data = attn_out + (size_t)h * head_stride;
349 const int head_offset = h * blocks_per_head;
350
351 for (int n_block = 0; n_block < blocks_per_head; n_block++) {
352 for (int n = 0; n < embed_dim; n++) {
353 const block_q8_0 *w_row = weights + (size_t)n * blocks_per_row + head_offset + n_block;
354 const float d = CK_FP16_TO_FP32(w_row->d);
355
356 for (int t = 0; t < tokens; t++) {
357 const float *token_vec = head_data + (size_t)t * token_stride + (size_t)n_block * QK8_0;
358 float sum = 0.0f;
359
360 for (int j = 0; j < QK8_0; j++) {
361 sum += d * (float)w_row->qs[j] * token_vec[j];
362 }
363
364 output[(size_t)t * embed_dim + n] += sum;
365 }
366 }
367 }
368 }
369}
Quantization block structures for weight-only quantization.
#define QK5_0
#define CK_FP16_TO_FP32(x)
#define QK8_0
void ck_gemm_nt_head_major_q8_0(const float *attn_out, const void *wo, const float *bias, float *output, int tokens, int embed_dim, int num_heads, int head_dim)
Output projection from head-major attention (Q8_0 weights)
void dequant_q5_0_block(const block_q5_0 *block, float *output)
Dequantize a single Q5_0 block to FP32.
void ck_gemm_nt_head_major_q5_0(const float *attn_out, const void *wo, const float *bias, float *output, int tokens, int embed_dim, int num_heads, int head_dim)
Output projection from head-major attention (auto-dispatch)
void dequant_q5_0_row(const void *src, float *dst, size_t n_elements)
Dequantize Q5_0 row (multiple blocks)
void gemv_nt_q5_0_head_major_output(float *output, const float *attn_out, const void *wo, const float *bias, int tokens, int embed_dim, int num_heads, int head_dim)
Output projection reading head-major attention output (Q5_0 weights)
uint8_t qh[4]
uint8_t qs[32/2]
int8_t qs[32]