← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
qk_norm_kernels.c
Go to the documentation of this file.
1/**
2 * @file qk_norm_kernels.c
3 * @brief Per-head RMSNorm on Q and K (Qwen3-style QK norm)
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:
13 * make v7-qk-norm-backward-parity-isa
14 * python unittest/test_qk_norm.py
15 *
16 * QK Norm normalizes each head's query/key vectors independently before RoPE.
17 * This stabilizes Q*K^T dot products before softmax, preventing attention
18 * collapse from large magnitude vectors.
19 *
20 * Why only Q and K, not V?
21 * V does not participate in the attention score computation (Q*K^T).
22 * The softmax saturation problem comes from large Q*K^T values, so only
23 * Q and K magnitudes matter. V is linearly combined after softmax weights
24 * are computed -- normalizing it would change output scale but not fix
25 * attention stability.
26 *
27 * Data layout after QKV projection (head-major):
28 * Q: [num_heads, num_tokens, head_dim] contiguous
29 * K: [num_kv_heads, num_tokens, head_dim] contiguous
30 *
31 * We treat Q as [num_heads * num_tokens] rows of [head_dim] elements.
32 * rmsnorm_forward normalizes each row independently. The gamma weight [head_dim]
33 * is shared across all heads (Qwen3 design: one gamma per Q, one per K).
34 */
35
36#include "bf16_utils.h"
37#include <math.h>
38#include <stddef.h> /* NULL */
39#include <stdlib.h> /* getenv */
40#include <string.h> /* strcmp */
41
42#if defined(__AVX__) || defined(__AVX2__) || defined(__AVXVNNI__)
43#include <immintrin.h>
44#endif
45
46/* rmsnorm_forward is declared in ckernel_engine.h */
47void rmsnorm_forward(const float *input,
48 const float *gamma,
49 float *output,
50 float *rstd_cache,
51 int tokens,
52 int d_model,
53 int aligned_embed_dim,
54 float eps);
55
56void rmsnorm_forward_fp64_sum(const float *input,
57 const float *gamma,
58 float *output,
59 float *rstd_cache,
60 int tokens,
61 int d_model,
62 int aligned_embed_dim,
63 float eps);
64
65void rmsnorm_forward_llama_production(const float *input,
66 const float *gamma,
67 float *output,
68 float *rstd_cache,
69 int tokens,
70 int d_model,
71 int aligned_embed_dim,
72 float eps);
73
74void rmsnorm_forward_pytorch_bf16_storage(const float *input,
75 const float *gamma,
76 float *output,
77 float *rstd_cache,
78 int tokens,
79 int d_model,
80 int aligned_embed_dim,
81 float eps);
83 const float *input,
84 const float *gamma,
85 float *output,
86 float *rstd_cache,
87 int tokens,
88 int d_model,
89 int aligned_embed_dim,
90 float eps);
92 const float *input,
93 float *output,
94 float *rstd_cache,
95 int tokens,
96 int d_model,
97 int aligned_embed_dim,
98 float eps);
99
100void rmsnorm_backward(const float *d_output,
101 const float *input,
102 const float *gamma,
103 const float *rstd_cache,
104 float *d_input,
105 float *d_gamma,
106 int tokens,
107 int d_model,
108 int aligned_embed_dim);
109
117
119
121{
122 return g_qk_norm_last_isa;
123}
124
126{
127 const char *forced = getenv("CK_QK_NORM_BACKWARD_ISA");
128 if (!forced || forced[0] == '\0' || strcmp(forced, "auto") == 0) {
129 return QK_NORM_ISA_AUTO;
130 }
131 if (strcmp(forced, "scalar") == 0) {
132 return QK_NORM_ISA_SCALAR;
133 }
134 if (strcmp(forced, "avx") == 0) {
135 return QK_NORM_ISA_AVX;
136 }
137 if (strcmp(forced, "avx2") == 0) {
138 return QK_NORM_ISA_AVX2;
139 }
140 if (strcmp(forced, "avx_vnni") == 0) {
142 }
143 /* Unknown value -> keep behavior deterministic by falling back. */
144 return QK_NORM_ISA_SCALAR;
145}
146
148{
149 switch (isa) {
151 return 1;
152#if defined(__AVX__)
153 case QK_NORM_ISA_AVX:
154 return 1;
155#endif
156#if defined(__AVX2__)
157 case QK_NORM_ISA_AVX2:
158 return 1;
159#endif
160#if defined(__AVXVNNI__)
162 return 1;
163#endif
164 default:
165 return 0;
166 }
167}
168
170{
172 if (forced != QK_NORM_ISA_AUTO) {
173 return qk_norm_isa_compiled(forced) ? forced : QK_NORM_ISA_SCALAR;
174 }
175
176#if defined(__AVXVNNI__)
178#elif defined(__AVX2__)
179 return QK_NORM_ISA_AVX2;
180#elif defined(__AVX__)
181 return QK_NORM_ISA_AVX;
182#else
183 return QK_NORM_ISA_SCALAR;
184#endif
185}
186
187static void qk_norm_compute_rstd_scalar(const float *input,
188 float *rstd_cache,
189 int rows,
190 int head_dim,
191 float eps)
192{
193 for (int r = 0; r < rows; ++r) {
194 const float *x = input + (size_t)r * (size_t)head_dim;
195 double sum_sq = 0.0;
196 for (int d = 0; d < head_dim; ++d) {
197 double v = (double)x[d];
198 sum_sq += v * v;
199 }
200 float mean_sq = (float)(sum_sq / (double)head_dim);
201 rstd_cache[r] = 1.0f / sqrtf(mean_sq + eps);
202 }
203}
204
205#if defined(__AVX__)
206static inline float qk_norm_hsum256(__m256 v)
207{
208 __m128 hi = _mm256_extractf128_ps(v, 1);
209 __m128 lo = _mm256_castps256_ps128(v);
210 __m128 sum = _mm_add_ps(lo, hi);
211 sum = _mm_hadd_ps(sum, sum);
212 sum = _mm_hadd_ps(sum, sum);
213 return _mm_cvtss_f32(sum);
214}
215
216static void qk_norm_compute_rstd_avx(const float *input,
217 float *rstd_cache,
218 int rows,
219 int head_dim,
220 float eps)
221{
222 for (int r = 0; r < rows; ++r) {
223 const float *x = input + (size_t)r * (size_t)head_dim;
224 __m256 sum_sq_v = _mm256_setzero_ps();
225 int d = 0;
226 for (; d + 8 <= head_dim; d += 8) {
227 __m256 xv = _mm256_loadu_ps(&x[d]);
228 __m256 xv2 = _mm256_mul_ps(xv, xv);
229 sum_sq_v = _mm256_add_ps(sum_sq_v, xv2);
230 }
231 float sum_sq = qk_norm_hsum256(sum_sq_v);
232 for (; d < head_dim; ++d) {
233 sum_sq += x[d] * x[d];
234 }
235 float mean_sq = sum_sq / (float)head_dim;
236 rstd_cache[r] = 1.0f / sqrtf(mean_sq + eps);
237 }
238}
239#endif
240
241#if defined(__AVX2__)
242static void qk_norm_compute_rstd_avx2(const float *input,
243 float *rstd_cache,
244 int rows,
245 int head_dim,
246 float eps)
247{
248 for (int r = 0; r < rows; ++r) {
249 const float *x = input + (size_t)r * (size_t)head_dim;
250 __m256 sum_sq_v = _mm256_setzero_ps();
251 int d = 0;
252 for (; d + 8 <= head_dim; d += 8) {
253 __m256 xv = _mm256_loadu_ps(&x[d]);
254#if defined(__FMA__)
255 sum_sq_v = _mm256_fmadd_ps(xv, xv, sum_sq_v);
256#else
257 __m256 xv2 = _mm256_mul_ps(xv, xv);
258 sum_sq_v = _mm256_add_ps(sum_sq_v, xv2);
259#endif
260 }
261 float sum_sq = qk_norm_hsum256(sum_sq_v);
262 for (; d < head_dim; ++d) {
263 sum_sq += x[d] * x[d];
264 }
265 float mean_sq = sum_sq / (float)head_dim;
266 rstd_cache[r] = 1.0f / sqrtf(mean_sq + eps);
267 }
268}
269#endif
270
271#if defined(__AVXVNNI__)
272static void qk_norm_compute_rstd_avx_vnni(const float *input,
273 float *rstd_cache,
274 int rows,
275 int head_dim,
276 float eps)
277{
278#if defined(__AVX2__)
279 qk_norm_compute_rstd_avx2(input, rstd_cache, rows, head_dim, eps);
280#elif defined(__AVX__)
281 qk_norm_compute_rstd_avx(input, rstd_cache, rows, head_dim, eps);
282#else
283 qk_norm_compute_rstd_scalar(input, rstd_cache, rows, head_dim, eps);
284#endif
285}
286#endif
287
288static void qk_norm_compute_rstd(const float *input,
289 float *rstd_cache,
290 int rows,
291 int head_dim,
292 float eps)
293{
294 QKNormISA selected = qk_norm_select_isa();
295 g_qk_norm_last_isa = (int)selected;
296 switch (selected) {
297#if defined(__AVXVNNI__)
299 qk_norm_compute_rstd_avx_vnni(input, rstd_cache, rows, head_dim, eps);
300 return;
301#endif
302#if defined(__AVX2__)
303 case QK_NORM_ISA_AVX2:
304 qk_norm_compute_rstd_avx2(input, rstd_cache, rows, head_dim, eps);
305 return;
306#endif
307#if defined(__AVX__)
308 case QK_NORM_ISA_AVX:
309 qk_norm_compute_rstd_avx(input, rstd_cache, rows, head_dim, eps);
310 return;
311#endif
313 case QK_NORM_ISA_AUTO:
314 default:
315 qk_norm_compute_rstd_scalar(input, rstd_cache, rows, head_dim, eps);
316 return;
317 }
318}
319
320/**
321 * Per-head RMSNorm on Q and K.
322 *
323 * @param q Q scratch buffer [num_heads * num_tokens * head_dim], in-place
324 * @param k K scratch buffer [num_kv_heads * num_tokens * head_dim], in-place
325 * @param q_gamma Q norm gamma weights [head_dim]
326 * @param k_gamma K norm gamma weights [head_dim]
327 * @param num_heads Number of query heads (e.g. 32 for Qwen3-8B)
328 * @param num_kv_heads Number of KV heads (e.g. 8 for Qwen3-8B with GQA)
329 * @param num_tokens Number of tokens (1 for decode, T for prefill)
330 * @param head_dim Dimension per head (e.g. 128)
331 * @param eps RMSNorm epsilon (e.g. 1e-6)
332 *
333 * @test unittest/test_qk_norm.py
334 */
335void qk_norm_forward(float *q, float *k,
336 const float *q_gamma, const float *k_gamma,
337 int num_heads, int num_kv_heads,
338 int num_tokens, int head_dim, float eps)
339{
340 /* Q norm: [num_heads * num_tokens] rows of [head_dim]
341 * Each row is one head's vector for one token. */
342 rmsnorm_forward(q, q_gamma, q, NULL,
343 num_heads * num_tokens, head_dim, head_dim, eps);
344
345 /* K norm: [num_kv_heads * num_tokens] rows of [head_dim]
346 * Same logic, fewer rows when using GQA. */
347 rmsnorm_forward(k, k_gamma, k, NULL,
348 num_kv_heads * num_tokens, head_dim, head_dim, eps);
349}
350
351void qk_norm_forward_fp64_sum(float *q, float *k,
352 const float *q_gamma, const float *k_gamma,
353 int num_heads, int num_kv_heads,
354 int num_tokens, int head_dim, float eps)
355{
356 rmsnorm_forward_fp64_sum(q, q_gamma, q, NULL,
357 num_heads * num_tokens, head_dim, head_dim, eps);
358 rmsnorm_forward_fp64_sum(k, k_gamma, k, NULL,
359 num_kv_heads * num_tokens, head_dim, head_dim, eps);
360}
361
362void qk_norm_forward_llama_production(float *q, float *k,
363 const float *q_gamma, const float *k_gamma,
364 int num_heads, int num_kv_heads,
365 int num_tokens, int head_dim, float eps)
366{
368 q, q_gamma, q, NULL,
369 num_heads * num_tokens, head_dim, head_dim, eps);
371 k, k_gamma, k, NULL,
372 num_kv_heads * num_tokens, head_dim, head_dim, eps);
373}
374
376 const float *q_gamma,
377 const float *k_gamma,
378 int num_heads, int num_kv_heads,
379 int num_tokens, int head_dim,
380 float eps)
381{
383 q, q_gamma, q, NULL,
384 num_heads * num_tokens, head_dim, head_dim, eps);
386 k, k_gamma, k, NULL,
387 num_kv_heads * num_tokens, head_dim, head_dim, eps);
388}
389
391 const float *q_gamma,
392 const float *k_gamma,
393 int num_heads,
394 int num_kv_heads,
395 int num_tokens,
396 int head_dim,
397 float eps)
398{
400 q, q_gamma, q, NULL,
401 num_heads * num_tokens, head_dim, head_dim, eps);
403 k, k_gamma, k, NULL,
404 num_kv_heads * num_tokens, head_dim, head_dim, eps);
405}
406
408 float *q,
409 float *k,
410 int num_heads,
411 int num_kv_heads,
412 int num_tokens,
413 int head_dim,
414 float eps,
415 float q_scale)
416{
417 const int q_elements = num_heads * num_tokens * head_dim;
419 q, q, NULL, num_heads * num_tokens, head_dim, head_dim, eps);
421 k, k, NULL, num_kv_heads * num_tokens, head_dim, head_dim, eps);
422 for (int index = 0; index < q_elements; ++index) {
423 q[index] = bf16_to_float(float_to_bf16(q[index] * q_scale));
424 }
425}
426
427/**
428 * Forward pass for Gemma4-assistant q-only per-head RMSNorm.
429 *
430 * Some Gemma4 assistant/drafter checkpoints project only Q and then reuse Q as
431 * the shared K/V stream. This wrapper keeps that public kernel contract explicit
432 * while reusing the same row-wise RMSNorm implementation as qk_norm_forward.
433 */
434void q_norm_forward(float *q,
435 const float *q_gamma,
436 int num_heads,
437 int num_tokens,
438 int head_dim,
439 float eps)
440{
441 rmsnorm_forward(q, q_gamma, q, NULL,
442 num_heads * num_tokens, head_dim, head_dim, eps);
443}
444
445/**
446 * Backward pass for per-head QK RMSNorm.
447 *
448 * This computes:
449 * - d_q / d_k for the Q and K activations
450 * - d_q_gamma / d_k_gamma for shared per-head gamma vectors
451 *
452 * Implementation is reference-first and deterministic:
453 * 1) recompute row rstd values from saved q/k inputs
454 * 2) call rmsnorm_backward on flattened [rows, head_dim] views
455 */
456void qk_norm_backward(const float *d_q_out, const float *d_k_out,
457 const float *q_in, const float *k_in,
458 const float *q_gamma, const float *k_gamma,
459 float *d_q_in, float *d_k_in,
460 float *d_q_gamma, float *d_k_gamma,
461 int num_heads, int num_kv_heads,
462 int num_tokens, int head_dim, float eps)
463{
464 int q_rows = num_heads * num_tokens;
465 int k_rows = num_kv_heads * num_tokens;
466
467 if (q_rows > 0) {
468 float q_rstd_cache[q_rows];
469 qk_norm_compute_rstd(q_in, q_rstd_cache, q_rows, head_dim, eps);
470 rmsnorm_backward(d_q_out, q_in, q_gamma, q_rstd_cache,
471 d_q_in, d_q_gamma, q_rows, head_dim, head_dim);
472 }
473
474 if (k_rows > 0) {
475 float k_rstd_cache[k_rows];
476 qk_norm_compute_rstd(k_in, k_rstd_cache, k_rows, head_dim, eps);
477 rmsnorm_backward(d_k_out, k_in, k_gamma, k_rstd_cache,
478 d_k_in, d_k_gamma, k_rows, head_dim, head_dim);
479 }
480}
static uint16_t float_to_bf16(float f)
Definition bf16_utils.h:90
static float bf16_to_float(uint16_t v)
Definition bf16_utils.h:38
QKNormISA
@ QK_NORM_ISA_AVX_VNNI
@ QK_NORM_ISA_AVX2
@ QK_NORM_ISA_AUTO
@ QK_NORM_ISA_SCALAR
@ QK_NORM_ISA_AVX
static int qk_norm_isa_compiled(QKNormISA isa)
static void qk_norm_compute_rstd_scalar(const float *input, float *rstd_cache, int rows, int head_dim, float eps)
void rmsnorm_forward_fp64_sum(const float *input, const float *gamma, float *output, float *rstd_cache, int tokens, int d_model, int aligned_embed_dim, float eps)
void qk_norm_forward_qwen4_pytorch_bf16_storage(float *q, float *k, const float *q_gamma, const float *k_gamma, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps)
void rmsnorm_forward_qwen3next_pytorch_bf16_storage(const float *input, const float *gamma, float *output, float *rstd_cache, int tokens, int d_model, int aligned_embed_dim, float eps)
void rmsnorm_forward_llama_production(const float *input, const float *gamma, float *output, float *rstd_cache, int tokens, int d_model, int aligned_embed_dim, float eps)
static void qk_norm_compute_rstd(const float *input, float *rstd_cache, int rows, int head_dim, float eps)
void rmsnorm_forward_muse_unweighted_pytorch_bf16_storage(const float *input, float *output, float *rstd_cache, int tokens, int d_model, int aligned_embed_dim, float eps)
void qk_norm_forward_muse_unweighted_scaled_pytorch_bf16_storage(float *q, float *k, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps, float q_scale)
void qk_norm_backward(const float *d_q_out, const float *d_k_out, const float *q_in, const float *k_in, const float *q_gamma, const float *k_gamma, float *d_q_in, float *d_k_in, float *d_q_gamma, float *d_k_gamma, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps)
void qk_norm_forward_llama_production(float *q, float *k, const float *q_gamma, const float *k_gamma, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps)
void qk_norm_forward_pytorch_bf16_storage(float *q, float *k, const float *q_gamma, const float *k_gamma, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps)
void qk_norm_forward(float *q, float *k, const float *q_gamma, const float *k_gamma, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps)
void qk_norm_forward_fp64_sum(float *q, float *k, const float *q_gamma, const float *k_gamma, int num_heads, int num_kv_heads, int num_tokens, int head_dim, float eps)
void rmsnorm_forward_pytorch_bf16_storage(const float *input, const float *gamma, float *output, float *rstd_cache, int tokens, int d_model, int aligned_embed_dim, float eps)
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)
static int g_qk_norm_last_isa
void q_norm_forward(float *q, const float *q_gamma, int num_heads, int num_tokens, int head_dim, float eps)
int qk_norm_backward_last_isa(void)
void rmsnorm_backward(const float *d_output, const float *input, const float *gamma, const float *rstd_cache, float *d_input, float *d_gamma, int tokens, int d_model, int aligned_embed_dim)
static QKNormISA qk_norm_parse_forced_isa(void)
static QKNormISA qk_norm_select_isa(void)