← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
rmsnorm_qkv.c
Go to the documentation of this file.
1/**
2 * @file rmsnorm_qkv.c
3 * @brief Fused RMSNorm + QKV Projection
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 * Part of C-Kernel-Engine v6.6 Fusion Kernels
15 *
16 * PROBLEM:
17 * Non-fused version does 4 DRAM round-trips for 'normed' buffer:
18 * rmsnorm(x, weight, normed); // Write normed to DRAM
19 * gemv(wq, normed, q); // Read normed from DRAM
20 * gemv(wk, normed, k); // Read normed from DRAM
21 * gemv(wv, normed, v); // Read normed from DRAM
22 *
23 * SOLUTION:
24 * Fused version keeps 'normed' in registers/L1, zero DRAM access:
25 * rmsnorm_qkv_fused(x, weight, wq, wk, wv, q, k, v);
26 *
27 * EXPECTED SPEEDUP: 1.5-2x for this operation
28 */
29
30#include <stdint.h>
31#include <stddef.h>
32#include <math.h>
33#include <string.h>
34
35#ifdef __AVX2__
36#include <immintrin.h>
37#endif
38
39#include "ckernel_quant.h"
40
41extern void rmsnorm_forward(const float *input,
42 const float *gamma,
43 float *output,
44 float *rstd_cache,
45 int tokens,
46 int d_model,
47 int aligned_embed_dim,
48 float eps);
49
50/* ============================================================================
51 * HELPER: RMSNorm computation (inline, result stays in registers)
52 * ============================================================================ */
53
54static inline float compute_rms_scale(const float *x, int n, float eps) {
55 float sum_sq = 0.0f;
56
57#ifdef __AVX2__
58 __m256 vsum = _mm256_setzero_ps();
59 int i = 0;
60 for (; i + 7 < n; i += 8) {
61 __m256 vx = _mm256_loadu_ps(x + i);
62 vsum = _mm256_fmadd_ps(vx, vx, vsum);
63 }
64 // Horizontal sum
65 __m128 vlow = _mm256_castps256_ps128(vsum);
66 __m128 vhigh = _mm256_extractf128_ps(vsum, 1);
67 vlow = _mm_add_ps(vlow, vhigh);
68 vlow = _mm_hadd_ps(vlow, vlow);
69 vlow = _mm_hadd_ps(vlow, vlow);
70 sum_sq = _mm_cvtss_f32(vlow);
71 // Remainder
72 for (; i < n; i++) {
73 sum_sq += x[i] * x[i];
74 }
75#else
76 for (int i = 0; i < n; i++) {
77 sum_sq += x[i] * x[i];
78 }
79#endif
80
81 float rms = sqrtf(sum_sq / (float)n + eps);
82 return 1.0f / rms;
83}
84
85/* ============================================================================
86 * FUSED KERNEL: RMSNorm + QKV Projection (FP32 weights)
87 * ============================================================================ */
88
90 const float *x, /* [embed_dim] input hidden state */
91 const float *rms_weight, /* [embed_dim] RMSNorm gamma */
92 const float *wq, /* [q_dim, embed_dim] Q projection */
93 const float *wk, /* [kv_dim, embed_dim] K projection */
94 const float *wv, /* [kv_dim, embed_dim] V projection */
95 float *q_out, /* [q_dim] output Q */
96 float *k_out, /* [kv_dim] output K */
97 float *v_out, /* [kv_dim] output V */
98 int embed_dim, /* Hidden dimension */
99 int q_dim, /* Q output dimension (num_heads * head_dim) */
100 int kv_dim, /* KV output dimension (num_kv_heads * head_dim) */
101 float eps /* RMSNorm epsilon (typically 1e-6) */
102) {
103 /* Step 1: Compute RMS scale factor (stays in register) */
104 float scale = compute_rms_scale(x, embed_dim, eps);
105
106 /* Step 2: Fused normalize + project
107 *
108 * Key insight: We compute normed[i] = x[i] * rms_weight[i] * scale
109 * on-the-fly during the GEMV, never storing the full normed vector.
110 *
111 * For each output element:
112 * q[j] = sum_i( wq[j,i] * x[i] * rms_weight[i] * scale )
113 * = scale * sum_i( wq[j,i] * x[i] * rms_weight[i] )
114 */
115
116 /* Q projection */
117 for (int j = 0; j < q_dim; j++) {
118 float sum = 0.0f;
119 const float *wq_row = wq + j * embed_dim;
120
121#ifdef __AVX2__
122 __m256 vsum = _mm256_setzero_ps();
123 int i = 0;
124 for (; i + 7 < embed_dim; i += 8) {
125 __m256 vx = _mm256_loadu_ps(x + i);
126 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
127 __m256 vw = _mm256_loadu_ps(wq_row + i);
128 __m256 vnormed = _mm256_mul_ps(vx, vrms);
129 vsum = _mm256_fmadd_ps(vw, vnormed, vsum);
130 }
131 /* Horizontal sum */
132 __m128 vlow = _mm256_castps256_ps128(vsum);
133 __m128 vhigh = _mm256_extractf128_ps(vsum, 1);
134 vlow = _mm_add_ps(vlow, vhigh);
135 vlow = _mm_hadd_ps(vlow, vlow);
136 vlow = _mm_hadd_ps(vlow, vlow);
137 sum = _mm_cvtss_f32(vlow);
138 /* Remainder */
139 for (; i < embed_dim; i++) {
140 sum += wq_row[i] * x[i] * rms_weight[i];
141 }
142#else
143 for (int i = 0; i < embed_dim; i++) {
144 sum += wq_row[i] * x[i] * rms_weight[i];
145 }
146#endif
147 q_out[j] = sum * scale;
148 }
149
150 /* K projection */
151 for (int j = 0; j < kv_dim; j++) {
152 float sum = 0.0f;
153 const float *wk_row = wk + j * embed_dim;
154
155#ifdef __AVX2__
156 __m256 vsum = _mm256_setzero_ps();
157 int i = 0;
158 for (; i + 7 < embed_dim; i += 8) {
159 __m256 vx = _mm256_loadu_ps(x + i);
160 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
161 __m256 vw = _mm256_loadu_ps(wk_row + i);
162 __m256 vnormed = _mm256_mul_ps(vx, vrms);
163 vsum = _mm256_fmadd_ps(vw, vnormed, vsum);
164 }
165 __m128 vlow = _mm256_castps256_ps128(vsum);
166 __m128 vhigh = _mm256_extractf128_ps(vsum, 1);
167 vlow = _mm_add_ps(vlow, vhigh);
168 vlow = _mm_hadd_ps(vlow, vlow);
169 vlow = _mm_hadd_ps(vlow, vlow);
170 sum = _mm_cvtss_f32(vlow);
171 for (; i < embed_dim; i++) {
172 sum += wk_row[i] * x[i] * rms_weight[i];
173 }
174#else
175 for (int i = 0; i < embed_dim; i++) {
176 sum += wk_row[i] * x[i] * rms_weight[i];
177 }
178#endif
179 k_out[j] = sum * scale;
180 }
181
182 /* V projection */
183 for (int j = 0; j < kv_dim; j++) {
184 float sum = 0.0f;
185 const float *wv_row = wv + j * embed_dim;
186
187#ifdef __AVX2__
188 __m256 vsum = _mm256_setzero_ps();
189 int i = 0;
190 for (; i + 7 < embed_dim; i += 8) {
191 __m256 vx = _mm256_loadu_ps(x + i);
192 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
193 __m256 vw = _mm256_loadu_ps(wv_row + i);
194 __m256 vnormed = _mm256_mul_ps(vx, vrms);
195 vsum = _mm256_fmadd_ps(vw, vnormed, vsum);
196 }
197 __m128 vlow = _mm256_castps256_ps128(vsum);
198 __m128 vhigh = _mm256_extractf128_ps(vsum, 1);
199 vlow = _mm_add_ps(vlow, vhigh);
200 vlow = _mm_hadd_ps(vlow, vlow);
201 vlow = _mm_hadd_ps(vlow, vlow);
202 sum = _mm_cvtss_f32(vlow);
203 for (; i < embed_dim; i++) {
204 sum += wv_row[i] * x[i] * rms_weight[i];
205 }
206#else
207 for (int i = 0; i < embed_dim; i++) {
208 sum += wv_row[i] * x[i] * rms_weight[i];
209 }
210#endif
211 v_out[j] = sum * scale;
212 }
213}
214
215/* ============================================================================
216 * FUSED KERNEL: RMSNorm + QKV Projection (Q4_K quantized weights)
217 *
218 * This is the production version - weights are Q4_K quantized.
219 * We dequantize on-the-fly during the fused operation.
220 * ============================================================================ */
221
223 const float *x, /* [embed_dim] input hidden state */
224 const float *rms_weight, /* [embed_dim] RMSNorm gamma */
225 const void *wq, /* Q4_K quantized Q projection */
226 const void *wk, /* Q4_K quantized K projection */
227 const void *wv, /* Q4_K quantized V projection */
228 float *q_out, /* [q_dim] output Q */
229 float *k_out, /* [kv_dim] output K */
230 float *v_out, /* [kv_dim] output V */
231 int embed_dim, /* Hidden dimension */
232 int q_dim, /* Q output dimension */
233 int kv_dim, /* KV output dimension */
234 float eps /* RMSNorm epsilon */
235) {
236 /* Compute normalized input once using the same tuned kernel as the
237 * separate baseline. This keeps the fused-vs-separate comparison focused
238 * on avoiding extra call boundaries and keeping normed[] hot for the 3 GEMVs.
239 *
240 * For Q4_K, we can't easily fuse the normalization into the dequant loop
241 * because the block structure is complex. So we compute normed[] first,
242 * but keep it small enough to fit in L1 cache.
243 *
244 * TODO: For maximum performance, implement a true fused Q4_K GEMV
245 * that dequantizes and multiplies by normed[i] in the same loop.
246 */
247
248 /* Allocate normed on stack (fits in L1 for typical embed_dim <= 4096). */
249#if defined(__GNUC__) || defined(__clang__)
250 __attribute__((aligned(64))) float normed[4096];
251#else
252 float normed[4096];
253#endif
254 if (embed_dim > 4096) {
255 /* Fallback for very large models */
256 return; /* TODO: heap allocation */
257 }
258
259 rmsnorm_forward(x, rms_weight, normed, NULL, 1, embed_dim, embed_dim, eps);
260
261 /* Step 3: Q4_K GEMV with normed input
262 *
263 * Call existing Q4_K GEMV kernel with normed[] as input.
264 * The normed[] buffer is in L1 cache, so this is still fast.
265 *
266 * Key insight: normed[] never leaves L1 cache because we use it
267 * immediately in the next 3 GEMVs. This eliminates the DRAM write
268 * that would happen in the non-fused version.
269 */
270
271 /* Declare external GEMV function */
272 extern void gemv_q4_k(float *y, const void *W, const float *x, int M, int K);
273
274 /* Q projection: q_out[q_dim] = wq[q_dim, embed_dim] @ normed[embed_dim] */
275 gemv_q4_k(q_out, wq, normed, q_dim, embed_dim);
276
277 /* K projection: k_out[kv_dim] = wk[kv_dim, embed_dim] @ normed[embed_dim] */
278 gemv_q4_k(k_out, wk, normed, kv_dim, embed_dim);
279
280 /* V projection: v_out[kv_dim] = wv[kv_dim, embed_dim] @ normed[embed_dim] */
281 gemv_q4_k(v_out, wv, normed, kv_dim, embed_dim);
282}
283
284/* ============================================================================
285 * TRUE SIMD FUSION: RMSNorm + QKV (FP32 weights) - VARIATION 2
286 *
287 * KEY INSIGHT: Process OUTPUT cache-line by cache-line.
288 * For each output cache line:
289 * - Keep accumulators in YMM/ZMM registers
290 * - For each INPUT cache line:
291 * - Compute normed chunk IN REGISTER (never stored to memory!)
292 * - Use immediately for all output accumulators via FMADD
293 * - Only store when output cache line is complete
294 *
295 * This is TRUE register-level fusion:
296 * - normed[] NEVER touches L1 cache
297 * - Each input cache line loaded ONCE, used for multiple outputs
298 * - Memory traffic: input + weights + output (no intermediate!)
299 *
300 * Expected speedup: 1.5-2x over separate kernels
301 * ============================================================================ */
302
303#ifdef __AVX2__
304static inline float hsum256_ps(__m256 v) {
305 __m128 vlow = _mm256_castps256_ps128(v);
306 __m128 vhigh = _mm256_extractf128_ps(v, 1);
307 vlow = _mm_add_ps(vlow, vhigh);
308 __m128 shuf = _mm_movehdup_ps(vlow);
309 vlow = _mm_add_ps(vlow, shuf);
310 shuf = _mm_movehl_ps(shuf, vlow);
311 vlow = _mm_add_ss(vlow, shuf);
312 return _mm_cvtss_f32(vlow);
313}
314#endif
315
317 const float *x, /* [embed_dim] input hidden state */
318 const float *rms_weight, /* [embed_dim] RMSNorm gamma */
319 const float *wq, /* [q_dim, embed_dim] Q projection (row-major) */
320 const float *wk, /* [kv_dim, embed_dim] K projection */
321 const float *wv, /* [kv_dim, embed_dim] V projection */
322 float *q_out, /* [q_dim] output Q */
323 float *k_out, /* [kv_dim] output K */
324 float *v_out, /* [kv_dim] output V */
325 int embed_dim, /* Hidden dimension */
326 int q_dim, /* Q output dimension (num_heads * head_dim) */
327 int kv_dim, /* KV output dimension (num_kv_heads * head_dim) */
328 float eps /* RMSNorm epsilon (typically 1e-6) */
329) {
330 /* Step 1: Compute RMS scale (requires full pass - unavoidable) */
331 float scale = compute_rms_scale(x, embed_dim, eps);
332
333#ifdef __AVX2__
334 __m256 vscale = _mm256_set1_ps(scale);
335
336 /* ═══════════════════════════════════════════════════════════════════════
337 * Q PROJECTION: Process 8 outputs at a time (one cache line)
338 *
339 * For each output cache line [j:j+8]:
340 * acc[0..7] = 0
341 * For each input cache line [i:i+8]:
342 * normed = x[i:i+8] * rms_weight[i:i+8] * scale ← IN REGISTER!
343 * acc[k] += W[j+k, i:i+8] · normed ← FMADD
344 * Store q_out[j:j+8]
345 * ═══════════════════════════════════════════════════════════════════════ */
346
347 for (int j = 0; j < q_dim; j += 8) {
348 /* 8 accumulators for 8 output elements - all in YMM registers */
349 __m256 acc0 = _mm256_setzero_ps();
350 __m256 acc1 = _mm256_setzero_ps();
351 __m256 acc2 = _mm256_setzero_ps();
352 __m256 acc3 = _mm256_setzero_ps();
353 __m256 acc4 = _mm256_setzero_ps();
354 __m256 acc5 = _mm256_setzero_ps();
355 __m256 acc6 = _mm256_setzero_ps();
356 __m256 acc7 = _mm256_setzero_ps();
357
358 /* Process input in cache-line chunks */
359 int i = 0;
360 for (; i + 7 < embed_dim; i += 8) {
361 /* Load input cache line and normalize IN REGISTER */
362 __m256 vx = _mm256_loadu_ps(x + i);
363 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
364 __m256 normed = _mm256_mul_ps(_mm256_mul_ps(vx, vrms), vscale);
365 /* normed is now in YMM register - NEVER touches memory! */
366
367 /* Load 8 weight rows and accumulate */
368 /* Each row is at wq[(j+k)*embed_dim + i] */
369 if (j + 0 < q_dim) {
370 __m256 w0 = _mm256_loadu_ps(wq + (j+0)*embed_dim + i);
371 acc0 = _mm256_fmadd_ps(w0, normed, acc0);
372 }
373 if (j + 1 < q_dim) {
374 __m256 w1 = _mm256_loadu_ps(wq + (j+1)*embed_dim + i);
375 acc1 = _mm256_fmadd_ps(w1, normed, acc1);
376 }
377 if (j + 2 < q_dim) {
378 __m256 w2 = _mm256_loadu_ps(wq + (j+2)*embed_dim + i);
379 acc2 = _mm256_fmadd_ps(w2, normed, acc2);
380 }
381 if (j + 3 < q_dim) {
382 __m256 w3 = _mm256_loadu_ps(wq + (j+3)*embed_dim + i);
383 acc3 = _mm256_fmadd_ps(w3, normed, acc3);
384 }
385 if (j + 4 < q_dim) {
386 __m256 w4 = _mm256_loadu_ps(wq + (j+4)*embed_dim + i);
387 acc4 = _mm256_fmadd_ps(w4, normed, acc4);
388 }
389 if (j + 5 < q_dim) {
390 __m256 w5 = _mm256_loadu_ps(wq + (j+5)*embed_dim + i);
391 acc5 = _mm256_fmadd_ps(w5, normed, acc5);
392 }
393 if (j + 6 < q_dim) {
394 __m256 w6 = _mm256_loadu_ps(wq + (j+6)*embed_dim + i);
395 acc6 = _mm256_fmadd_ps(w6, normed, acc6);
396 }
397 if (j + 7 < q_dim) {
398 __m256 w7 = _mm256_loadu_ps(wq + (j+7)*embed_dim + i);
399 acc7 = _mm256_fmadd_ps(w7, normed, acc7);
400 }
401 }
402
403 /* Handle remainder (scalar, rare for aligned dims) */
404 for (; i < embed_dim; i++) {
405 float normed_scalar = x[i] * rms_weight[i] * scale;
406 if (j + 0 < q_dim) acc0 = _mm256_add_ps(acc0, _mm256_set1_ps(wq[(j+0)*embed_dim + i] * normed_scalar));
407 if (j + 1 < q_dim) acc1 = _mm256_add_ps(acc1, _mm256_set1_ps(wq[(j+1)*embed_dim + i] * normed_scalar));
408 if (j + 2 < q_dim) acc2 = _mm256_add_ps(acc2, _mm256_set1_ps(wq[(j+2)*embed_dim + i] * normed_scalar));
409 if (j + 3 < q_dim) acc3 = _mm256_add_ps(acc3, _mm256_set1_ps(wq[(j+3)*embed_dim + i] * normed_scalar));
410 if (j + 4 < q_dim) acc4 = _mm256_add_ps(acc4, _mm256_set1_ps(wq[(j+4)*embed_dim + i] * normed_scalar));
411 if (j + 5 < q_dim) acc5 = _mm256_add_ps(acc5, _mm256_set1_ps(wq[(j+5)*embed_dim + i] * normed_scalar));
412 if (j + 6 < q_dim) acc6 = _mm256_add_ps(acc6, _mm256_set1_ps(wq[(j+6)*embed_dim + i] * normed_scalar));
413 if (j + 7 < q_dim) acc7 = _mm256_add_ps(acc7, _mm256_set1_ps(wq[(j+7)*embed_dim + i] * normed_scalar));
414 }
415
416 /* Horizontal sum and store output cache line */
417 if (j + 0 < q_dim) q_out[j+0] = hsum256_ps(acc0);
418 if (j + 1 < q_dim) q_out[j+1] = hsum256_ps(acc1);
419 if (j + 2 < q_dim) q_out[j+2] = hsum256_ps(acc2);
420 if (j + 3 < q_dim) q_out[j+3] = hsum256_ps(acc3);
421 if (j + 4 < q_dim) q_out[j+4] = hsum256_ps(acc4);
422 if (j + 5 < q_dim) q_out[j+5] = hsum256_ps(acc5);
423 if (j + 6 < q_dim) q_out[j+6] = hsum256_ps(acc6);
424 if (j + 7 < q_dim) q_out[j+7] = hsum256_ps(acc7);
425 }
426
427 /* ═══════════════════════════════════════════════════════════════════════
428 * K PROJECTION: Same pattern, smaller output
429 * ═══════════════════════════════════════════════════════════════════════ */
430
431 for (int j = 0; j < kv_dim; j += 8) {
432 __m256 acc0 = _mm256_setzero_ps();
433 __m256 acc1 = _mm256_setzero_ps();
434 __m256 acc2 = _mm256_setzero_ps();
435 __m256 acc3 = _mm256_setzero_ps();
436 __m256 acc4 = _mm256_setzero_ps();
437 __m256 acc5 = _mm256_setzero_ps();
438 __m256 acc6 = _mm256_setzero_ps();
439 __m256 acc7 = _mm256_setzero_ps();
440
441 int i = 0;
442 for (; i + 7 < embed_dim; i += 8) {
443 __m256 vx = _mm256_loadu_ps(x + i);
444 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
445 __m256 normed = _mm256_mul_ps(_mm256_mul_ps(vx, vrms), vscale);
446
447 if (j + 0 < kv_dim) acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+0)*embed_dim + i), normed, acc0);
448 if (j + 1 < kv_dim) acc1 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+1)*embed_dim + i), normed, acc1);
449 if (j + 2 < kv_dim) acc2 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+2)*embed_dim + i), normed, acc2);
450 if (j + 3 < kv_dim) acc3 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+3)*embed_dim + i), normed, acc3);
451 if (j + 4 < kv_dim) acc4 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+4)*embed_dim + i), normed, acc4);
452 if (j + 5 < kv_dim) acc5 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+5)*embed_dim + i), normed, acc5);
453 if (j + 6 < kv_dim) acc6 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+6)*embed_dim + i), normed, acc6);
454 if (j + 7 < kv_dim) acc7 = _mm256_fmadd_ps(_mm256_loadu_ps(wk + (j+7)*embed_dim + i), normed, acc7);
455 }
456
457 for (; i < embed_dim; i++) {
458 float normed_scalar = x[i] * rms_weight[i] * scale;
459 if (j + 0 < kv_dim) acc0 = _mm256_add_ps(acc0, _mm256_set1_ps(wk[(j+0)*embed_dim + i] * normed_scalar));
460 if (j + 1 < kv_dim) acc1 = _mm256_add_ps(acc1, _mm256_set1_ps(wk[(j+1)*embed_dim + i] * normed_scalar));
461 if (j + 2 < kv_dim) acc2 = _mm256_add_ps(acc2, _mm256_set1_ps(wk[(j+2)*embed_dim + i] * normed_scalar));
462 if (j + 3 < kv_dim) acc3 = _mm256_add_ps(acc3, _mm256_set1_ps(wk[(j+3)*embed_dim + i] * normed_scalar));
463 if (j + 4 < kv_dim) acc4 = _mm256_add_ps(acc4, _mm256_set1_ps(wk[(j+4)*embed_dim + i] * normed_scalar));
464 if (j + 5 < kv_dim) acc5 = _mm256_add_ps(acc5, _mm256_set1_ps(wk[(j+5)*embed_dim + i] * normed_scalar));
465 if (j + 6 < kv_dim) acc6 = _mm256_add_ps(acc6, _mm256_set1_ps(wk[(j+6)*embed_dim + i] * normed_scalar));
466 if (j + 7 < kv_dim) acc7 = _mm256_add_ps(acc7, _mm256_set1_ps(wk[(j+7)*embed_dim + i] * normed_scalar));
467 }
468
469 if (j + 0 < kv_dim) k_out[j+0] = hsum256_ps(acc0);
470 if (j + 1 < kv_dim) k_out[j+1] = hsum256_ps(acc1);
471 if (j + 2 < kv_dim) k_out[j+2] = hsum256_ps(acc2);
472 if (j + 3 < kv_dim) k_out[j+3] = hsum256_ps(acc3);
473 if (j + 4 < kv_dim) k_out[j+4] = hsum256_ps(acc4);
474 if (j + 5 < kv_dim) k_out[j+5] = hsum256_ps(acc5);
475 if (j + 6 < kv_dim) k_out[j+6] = hsum256_ps(acc6);
476 if (j + 7 < kv_dim) k_out[j+7] = hsum256_ps(acc7);
477 }
478
479 /* ═══════════════════════════════════════════════════════════════════════
480 * V PROJECTION: Same pattern
481 * ═══════════════════════════════════════════════════════════════════════ */
482
483 for (int j = 0; j < kv_dim; j += 8) {
484 __m256 acc0 = _mm256_setzero_ps();
485 __m256 acc1 = _mm256_setzero_ps();
486 __m256 acc2 = _mm256_setzero_ps();
487 __m256 acc3 = _mm256_setzero_ps();
488 __m256 acc4 = _mm256_setzero_ps();
489 __m256 acc5 = _mm256_setzero_ps();
490 __m256 acc6 = _mm256_setzero_ps();
491 __m256 acc7 = _mm256_setzero_ps();
492
493 int i = 0;
494 for (; i + 7 < embed_dim; i += 8) {
495 __m256 vx = _mm256_loadu_ps(x + i);
496 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
497 __m256 normed = _mm256_mul_ps(_mm256_mul_ps(vx, vrms), vscale);
498
499 if (j + 0 < kv_dim) acc0 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+0)*embed_dim + i), normed, acc0);
500 if (j + 1 < kv_dim) acc1 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+1)*embed_dim + i), normed, acc1);
501 if (j + 2 < kv_dim) acc2 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+2)*embed_dim + i), normed, acc2);
502 if (j + 3 < kv_dim) acc3 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+3)*embed_dim + i), normed, acc3);
503 if (j + 4 < kv_dim) acc4 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+4)*embed_dim + i), normed, acc4);
504 if (j + 5 < kv_dim) acc5 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+5)*embed_dim + i), normed, acc5);
505 if (j + 6 < kv_dim) acc6 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+6)*embed_dim + i), normed, acc6);
506 if (j + 7 < kv_dim) acc7 = _mm256_fmadd_ps(_mm256_loadu_ps(wv + (j+7)*embed_dim + i), normed, acc7);
507 }
508
509 for (; i < embed_dim; i++) {
510 float normed_scalar = x[i] * rms_weight[i] * scale;
511 if (j + 0 < kv_dim) acc0 = _mm256_add_ps(acc0, _mm256_set1_ps(wv[(j+0)*embed_dim + i] * normed_scalar));
512 if (j + 1 < kv_dim) acc1 = _mm256_add_ps(acc1, _mm256_set1_ps(wv[(j+1)*embed_dim + i] * normed_scalar));
513 if (j + 2 < kv_dim) acc2 = _mm256_add_ps(acc2, _mm256_set1_ps(wv[(j+2)*embed_dim + i] * normed_scalar));
514 if (j + 3 < kv_dim) acc3 = _mm256_add_ps(acc3, _mm256_set1_ps(wv[(j+3)*embed_dim + i] * normed_scalar));
515 if (j + 4 < kv_dim) acc4 = _mm256_add_ps(acc4, _mm256_set1_ps(wv[(j+4)*embed_dim + i] * normed_scalar));
516 if (j + 5 < kv_dim) acc5 = _mm256_add_ps(acc5, _mm256_set1_ps(wv[(j+5)*embed_dim + i] * normed_scalar));
517 if (j + 6 < kv_dim) acc6 = _mm256_add_ps(acc6, _mm256_set1_ps(wv[(j+6)*embed_dim + i] * normed_scalar));
518 if (j + 7 < kv_dim) acc7 = _mm256_add_ps(acc7, _mm256_set1_ps(wv[(j+7)*embed_dim + i] * normed_scalar));
519 }
520
521 if (j + 0 < kv_dim) v_out[j+0] = hsum256_ps(acc0);
522 if (j + 1 < kv_dim) v_out[j+1] = hsum256_ps(acc1);
523 if (j + 2 < kv_dim) v_out[j+2] = hsum256_ps(acc2);
524 if (j + 3 < kv_dim) v_out[j+3] = hsum256_ps(acc3);
525 if (j + 4 < kv_dim) v_out[j+4] = hsum256_ps(acc4);
526 if (j + 5 < kv_dim) v_out[j+5] = hsum256_ps(acc5);
527 if (j + 6 < kv_dim) v_out[j+6] = hsum256_ps(acc6);
528 if (j + 7 < kv_dim) v_out[j+7] = hsum256_ps(acc7);
529 }
530
531#else
532 /* Scalar fallback - same logic, no SIMD */
533 for (int j = 0; j < q_dim; j++) {
534 float sum = 0.0f;
535 for (int i = 0; i < embed_dim; i++) {
536 float normed = x[i] * rms_weight[i] * scale;
537 sum += wq[j * embed_dim + i] * normed;
538 }
539 q_out[j] = sum;
540 }
541 for (int j = 0; j < kv_dim; j++) {
542 float sum = 0.0f;
543 for (int i = 0; i < embed_dim; i++) {
544 float normed = x[i] * rms_weight[i] * scale;
545 sum += wk[j * embed_dim + i] * normed;
546 }
547 k_out[j] = sum;
548 }
549 for (int j = 0; j < kv_dim; j++) {
550 float sum = 0.0f;
551 for (int i = 0; i < embed_dim; i++) {
552 float normed = x[i] * rms_weight[i] * scale;
553 sum += wv[j * embed_dim + i] * normed;
554 }
555 v_out[j] = sum;
556 }
557#endif
558}
559
560/* ============================================================================
561 * TRUE SIMD FUSION V3: RMSNorm + QKV (FP32 weights)
562 *
563 * KEY FIX: Process Q, K, V SIMULTANEOUSLY in one pass through input!
564 *
565 * Previous versions had a flaw:
566 * v1: Recomputes normed for each Q row, then each K row, then each V row
567 * = 3 * q_dim * embed_dim FMA operations (tripled work!)
568 * v2: Same issue, just with 8-at-a-time grouping
569 *
570 * v3 approach:
571 * For each OUTPUT index j (0 to max(q_dim, kv_dim)):
572 * q_acc = k_acc = v_acc = 0
573 * For each INPUT chunk [i:i+8]:
574 * normed = x[i:i+8] * rms_weight[i:i+8] * scale (computed ONCE!)
575 * q_acc += wq[j,i:i+8] · normed
576 * k_acc += wk[j,i:i+8] · normed (if j < kv_dim)
577 * v_acc += wv[j,i:i+8] · normed (if j < kv_dim)
578 * Store q_out[j], k_out[j], v_out[j]
579 *
580 * Benefits:
581 * - normed computed ONCE per input chunk, used 3x
582 * - Sequential weight access (good prefetch)
583 * - Minimal register pressure (3 accumulators + 1 normed)
584 * ============================================================================ */
585
587 const float *x, /* [embed_dim] input hidden state */
588 const float *rms_weight, /* [embed_dim] RMSNorm gamma */
589 const float *wq, /* [q_dim, embed_dim] Q projection (row-major) */
590 const float *wk, /* [kv_dim, embed_dim] K projection */
591 const float *wv, /* [kv_dim, embed_dim] V projection */
592 float *q_out, /* [q_dim] output Q */
593 float *k_out, /* [kv_dim] output K */
594 float *v_out, /* [kv_dim] output V */
595 int embed_dim, /* Hidden dimension */
596 int q_dim, /* Q output dimension (num_heads * head_dim) */
597 int kv_dim, /* KV output dimension (num_kv_heads * head_dim) */
598 float eps /* RMSNorm epsilon (typically 1e-6) */
599) {
600 /* Step 1: Compute RMS scale (requires full pass - unavoidable) */
601 float scale = compute_rms_scale(x, embed_dim, eps);
602
603#ifdef __AVX2__
604 __m256 vscale = _mm256_set1_ps(scale);
605
606 /* ═══════════════════════════════════════════════════════════════════════
607 * Phase 1: Process Q outputs that have corresponding K,V outputs
608 * (j < kv_dim: compute Q, K, V together)
609 * ═══════════════════════════════════════════════════════════════════════ */
610 for (int j = 0; j < kv_dim; j++) {
611 __m256 q_acc = _mm256_setzero_ps();
612 __m256 k_acc = _mm256_setzero_ps();
613 __m256 v_acc = _mm256_setzero_ps();
614
615 int i = 0;
616 for (; i + 7 < embed_dim; i += 8) {
617 /* Load input and normalize - computed ONCE, used THREE times! */
618 __m256 vx = _mm256_loadu_ps(x + i);
619 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
620 __m256 normed = _mm256_mul_ps(_mm256_mul_ps(vx, vrms), vscale);
621
622 /* Load weight rows - sequential access per row */
623 __m256 wq_row = _mm256_loadu_ps(wq + j * embed_dim + i);
624 __m256 wk_row = _mm256_loadu_ps(wk + j * embed_dim + i);
625 __m256 wv_row = _mm256_loadu_ps(wv + j * embed_dim + i);
626
627 /* Accumulate - normed stays in register! */
628 q_acc = _mm256_fmadd_ps(wq_row, normed, q_acc);
629 k_acc = _mm256_fmadd_ps(wk_row, normed, k_acc);
630 v_acc = _mm256_fmadd_ps(wv_row, normed, v_acc);
631 }
632
633 /* Handle remainder (scalar) */
634 float q_sum = hsum256_ps(q_acc);
635 float k_sum = hsum256_ps(k_acc);
636 float v_sum = hsum256_ps(v_acc);
637
638 for (; i < embed_dim; i++) {
639 float normed = x[i] * rms_weight[i] * scale;
640 q_sum += wq[j * embed_dim + i] * normed;
641 k_sum += wk[j * embed_dim + i] * normed;
642 v_sum += wv[j * embed_dim + i] * normed;
643 }
644
645 q_out[j] = q_sum;
646 k_out[j] = k_sum;
647 v_out[j] = v_sum;
648 }
649
650 /* ═══════════════════════════════════════════════════════════════════════
651 * Phase 2: Process remaining Q outputs (j >= kv_dim: Q only)
652 * This handles GQA where q_dim > kv_dim
653 * ═══════════════════════════════════════════════════════════════════════ */
654 for (int j = kv_dim; j < q_dim; j++) {
655 __m256 q_acc = _mm256_setzero_ps();
656
657 int i = 0;
658 for (; i + 7 < embed_dim; i += 8) {
659 __m256 vx = _mm256_loadu_ps(x + i);
660 __m256 vrms = _mm256_loadu_ps(rms_weight + i);
661 __m256 normed = _mm256_mul_ps(_mm256_mul_ps(vx, vrms), vscale);
662
663 __m256 wq_row = _mm256_loadu_ps(wq + j * embed_dim + i);
664 q_acc = _mm256_fmadd_ps(wq_row, normed, q_acc);
665 }
666
667 float q_sum = hsum256_ps(q_acc);
668 for (; i < embed_dim; i++) {
669 float normed = x[i] * rms_weight[i] * scale;
670 q_sum += wq[j * embed_dim + i] * normed;
671 }
672
673 q_out[j] = q_sum;
674 }
675
676#else
677 /* Scalar fallback - same simultaneous Q,K,V approach */
678 for (int j = 0; j < kv_dim; j++) {
679 float q_sum = 0.0f, k_sum = 0.0f, v_sum = 0.0f;
680 for (int i = 0; i < embed_dim; i++) {
681 float normed = x[i] * rms_weight[i] * scale;
682 q_sum += wq[j * embed_dim + i] * normed;
683 k_sum += wk[j * embed_dim + i] * normed;
684 v_sum += wv[j * embed_dim + i] * normed;
685 }
686 q_out[j] = q_sum;
687 k_out[j] = k_sum;
688 v_out[j] = v_sum;
689 }
690 for (int j = kv_dim; j < q_dim; j++) {
691 float q_sum = 0.0f;
692 for (int i = 0; i < embed_dim; i++) {
693 float normed = x[i] * rms_weight[i] * scale;
694 q_sum += wq[j * embed_dim + i] * normed;
695 }
696 q_out[j] = q_sum;
697 }
698#endif
699}
700
701/* ============================================================================
702 * NON-FUSED REFERENCE: For benchmarking comparison
703 *
704 * This is what we're comparing against. Call rmsnorm + 3x GEMV separately.
705 * ============================================================================ */
706
708 const float *x,
709 const float *rms_weight,
710 const float *wq,
711 const float *wk,
712 const float *wv,
713 float *normed, /* [embed_dim] intermediate buffer - DRAM write! */
714 float *q_out,
715 float *k_out,
716 float *v_out,
717 int embed_dim,
718 int q_dim,
719 int kv_dim,
720 float eps
721) {
722 /* Step 1: RMSNorm - writes normed to DRAM */
723 float scale = compute_rms_scale(x, embed_dim, eps);
724 for (int i = 0; i < embed_dim; i++) {
725 normed[i] = x[i] * rms_weight[i] * scale;
726 }
727
728 /* Step 2: Q projection - reads normed from DRAM */
729 for (int j = 0; j < q_dim; j++) {
730 float sum = 0.0f;
731 for (int i = 0; i < embed_dim; i++) {
732 sum += wq[j * embed_dim + i] * normed[i];
733 }
734 q_out[j] = sum;
735 }
736
737 /* Step 3: K projection - reads normed from DRAM */
738 for (int j = 0; j < kv_dim; j++) {
739 float sum = 0.0f;
740 for (int i = 0; i < embed_dim; i++) {
741 sum += wk[j * embed_dim + i] * normed[i];
742 }
743 k_out[j] = sum;
744 }
745
746 /* Step 4: V projection - reads normed from DRAM */
747 for (int j = 0; j < kv_dim; j++) {
748 float sum = 0.0f;
749 for (int i = 0; i < embed_dim; i++) {
750 sum += wv[j * embed_dim + i] * normed[i];
751 }
752 v_out[j] = sum;
753 }
754}
void gemv_q4_k(float *y, const void *W, const float *x, int M, int K)
Auto-dispatch GEMV based on available SIMD.
Quantization block structures for weight-only quantization.
static float compute_rms_scale(const float *x, int n, float eps)
Definition rmsnorm_qkv.c:54
void rmsnorm_qkv_fp32_fused_v3(const float *x, const float *rms_weight, const float *wq, const float *wk, const float *wv, float *q_out, float *k_out, float *v_out, int embed_dim, int q_dim, int kv_dim, float eps)
void rmsnorm_qkv_separate_fp32(const float *x, const float *rms_weight, const float *wq, const float *wk, const float *wv, float *normed, float *q_out, float *k_out, float *v_out, int embed_dim, int q_dim, int kv_dim, float eps)
void rmsnorm_qkv_fp32_fused_v2(const float *x, const float *rms_weight, const float *wq, const float *wk, const float *wv, float *q_out, float *k_out, float *v_out, int embed_dim, int q_dim, int kv_dim, float eps)
void rmsnorm_qkv_fp32_fused(const float *x, const float *rms_weight, const float *wq, const float *wk, const float *wv, float *q_out, float *k_out, float *v_out, int embed_dim, int q_dim, int kv_dim, float eps)
Definition rmsnorm_qkv.c:89
void rmsnorm_forward(const float *input, const float *gamma, float *output, float *rstd_cache, int tokens, int d_model, int aligned_embed_dim, float eps)
void rmsnorm_qkv_q4k_fused(const float *x, const float *rms_weight, const void *wq, const void *wk, const void *wv, float *q_out, float *k_out, float *v_out, int embed_dim, int q_dim, int kv_dim, float eps)
__attribute__((visibility("default"))) CKTokenizer *ck_tokenizer_create(CKTokenizerType type)