← 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 <math.h>
37#include <stddef.h> /* NULL */
38#include <stdlib.h> /* getenv */
39#include <string.h> /* strcmp */
40
41#if defined(__AVX__) || defined(__AVX2__) || defined(__AVXVNNI__)
42#include <immintrin.h>
43#endif
44
45/* rmsnorm_forward is declared in ckernel_engine.h */
46void rmsnorm_forward(const float *input,
47 const float *gamma,
48 float *output,
49 float *rstd_cache,
50 int tokens,
51 int d_model,
52 int aligned_embed_dim,
53 float eps);
54
55void rmsnorm_forward_fp64_sum(const float *input,
56 const float *gamma,
57 float *output,
58 float *rstd_cache,
59 int tokens,
60 int d_model,
61 int aligned_embed_dim,
62 float eps);
63
64void rmsnorm_forward_llama_production(const float *input,
65 const float *gamma,
66 float *output,
67 float *rstd_cache,
68 int tokens,
69 int d_model,
70 int aligned_embed_dim,
71 float eps);
72
73void rmsnorm_forward_pytorch_bf16_storage(const float *input,
74 const float *gamma,
75 float *output,
76 float *rstd_cache,
77 int tokens,
78 int d_model,
79 int aligned_embed_dim,
80 float eps);
82 const float *input,
83 const float *gamma,
84 float *output,
85 float *rstd_cache,
86 int tokens,
87 int d_model,
88 int aligned_embed_dim,
89 float eps);
90
91void rmsnorm_backward(const float *d_output,
92 const float *input,
93 const float *gamma,
94 const float *rstd_cache,
95 float *d_input,
96 float *d_gamma,
97 int tokens,
98 int d_model,
99 int aligned_embed_dim);
100
108
110
112{
113 return g_qk_norm_last_isa;
114}
115
117{
118 const char *forced = getenv("CK_QK_NORM_BACKWARD_ISA");
119 if (!forced || forced[0] == '\0' || strcmp(forced, "auto") == 0) {
120 return QK_NORM_ISA_AUTO;
121 }
122 if (strcmp(forced, "scalar") == 0) {
123 return QK_NORM_ISA_SCALAR;
124 }
125 if (strcmp(forced, "avx") == 0) {
126 return QK_NORM_ISA_AVX;
127 }
128 if (strcmp(forced, "avx2") == 0) {
129 return QK_NORM_ISA_AVX2;
130 }
131 if (strcmp(forced, "avx_vnni") == 0) {
133 }
134 /* Unknown value -> keep behavior deterministic by falling back. */
135 return QK_NORM_ISA_SCALAR;
136}
137
139{
140 switch (isa) {
142 return 1;
143#if defined(__AVX__)
144 case QK_NORM_ISA_AVX:
145 return 1;
146#endif
147#if defined(__AVX2__)
148 case QK_NORM_ISA_AVX2:
149 return 1;
150#endif
151#if defined(__AVXVNNI__)
153 return 1;
154#endif
155 default:
156 return 0;
157 }
158}
159
161{
163 if (forced != QK_NORM_ISA_AUTO) {
164 return qk_norm_isa_compiled(forced) ? forced : QK_NORM_ISA_SCALAR;
165 }
166
167#if defined(__AVXVNNI__)
169#elif defined(__AVX2__)
170 return QK_NORM_ISA_AVX2;
171#elif defined(__AVX__)
172 return QK_NORM_ISA_AVX;
173#else
174 return QK_NORM_ISA_SCALAR;
175#endif
176}
177
178static void qk_norm_compute_rstd_scalar(const float *input,
179 float *rstd_cache,
180 int rows,
181 int head_dim,
182 float eps)
183{
184 for (int r = 0; r < rows; ++r) {
185 const float *x = input + (size_t)r * (size_t)head_dim;
186 double sum_sq = 0.0;
187 for (int d = 0; d < head_dim; ++d) {
188 double v = (double)x[d];
189 sum_sq += v * v;
190 }
191 float mean_sq = (float)(sum_sq / (double)head_dim);
192 rstd_cache[r] = 1.0f / sqrtf(mean_sq + eps);
193 }
194}
195
196#if defined(__AVX__)
197static inline float qk_norm_hsum256(__m256 v)
198{
199 __m128 hi = _mm256_extractf128_ps(v, 1);
200 __m128 lo = _mm256_castps256_ps128(v);
201 __m128 sum = _mm_add_ps(lo, hi);
202 sum = _mm_hadd_ps(sum, sum);
203 sum = _mm_hadd_ps(sum, sum);
204 return _mm_cvtss_f32(sum);
205}
206
207static void qk_norm_compute_rstd_avx(const float *input,
208 float *rstd_cache,
209 int rows,
210 int head_dim,
211 float eps)
212{
213 for (int r = 0; r < rows; ++r) {
214 const float *x = input + (size_t)r * (size_t)head_dim;
215 __m256 sum_sq_v = _mm256_setzero_ps();
216 int d = 0;
217 for (; d + 8 <= head_dim; d += 8) {
218 __m256 xv = _mm256_loadu_ps(&x[d]);
219 __m256 xv2 = _mm256_mul_ps(xv, xv);
220 sum_sq_v = _mm256_add_ps(sum_sq_v, xv2);
221 }
222 float sum_sq = qk_norm_hsum256(sum_sq_v);
223 for (; d < head_dim; ++d) {
224 sum_sq += x[d] * x[d];
225 }
226 float mean_sq = sum_sq / (float)head_dim;
227 rstd_cache[r] = 1.0f / sqrtf(mean_sq + eps);
228 }
229}
230#endif
231
232#if defined(__AVX2__)
233static void qk_norm_compute_rstd_avx2(const float *input,
234 float *rstd_cache,
235 int rows,
236 int head_dim,
237 float eps)
238{
239 for (int r = 0; r < rows; ++r) {
240 const float *x = input + (size_t)r * (size_t)head_dim;
241 __m256 sum_sq_v = _mm256_setzero_ps();
242 int d = 0;
243 for (; d + 8 <= head_dim; d += 8) {
244 __m256 xv = _mm256_loadu_ps(&x[d]);
245#if defined(__FMA__)
246 sum_sq_v = _mm256_fmadd_ps(xv, xv, sum_sq_v);
247#else
248 __m256 xv2 = _mm256_mul_ps(xv, xv);
249 sum_sq_v = _mm256_add_ps(sum_sq_v, xv2);
250#endif
251 }
252 float sum_sq = qk_norm_hsum256(sum_sq_v);
253 for (; d < head_dim; ++d) {
254 sum_sq += x[d] * x[d];
255 }
256 float mean_sq = sum_sq / (float)head_dim;
257 rstd_cache[r] = 1.0f / sqrtf(mean_sq + eps);
258 }
259}
260#endif
261
262#if defined(__AVXVNNI__)
263static void qk_norm_compute_rstd_avx_vnni(const float *input,
264 float *rstd_cache,
265 int rows,
266 int head_dim,
267 float eps)
268{
269#if defined(__AVX2__)
270 qk_norm_compute_rstd_avx2(input, rstd_cache, rows, head_dim, eps);
271#elif defined(__AVX__)
272 qk_norm_compute_rstd_avx(input, rstd_cache, rows, head_dim, eps);
273#else
274 qk_norm_compute_rstd_scalar(input, rstd_cache, rows, head_dim, eps);
275#endif
276}
277#endif
278
279static void qk_norm_compute_rstd(const float *input,
280 float *rstd_cache,
281 int rows,
282 int head_dim,
283 float eps)
284{
285 QKNormISA selected = qk_norm_select_isa();
286 g_qk_norm_last_isa = (int)selected;
287 switch (selected) {
288#if defined(__AVXVNNI__)
290 qk_norm_compute_rstd_avx_vnni(input, rstd_cache, rows, head_dim, eps);
291 return;
292#endif
293#if defined(__AVX2__)
294 case QK_NORM_ISA_AVX2:
295 qk_norm_compute_rstd_avx2(input, rstd_cache, rows, head_dim, eps);
296 return;
297#endif
298#if defined(__AVX__)
299 case QK_NORM_ISA_AVX:
300 qk_norm_compute_rstd_avx(input, rstd_cache, rows, head_dim, eps);
301 return;
302#endif
304 case QK_NORM_ISA_AUTO:
305 default:
306 qk_norm_compute_rstd_scalar(input, rstd_cache, rows, head_dim, eps);
307 return;
308 }
309}
310
311/**
312 * Per-head RMSNorm on Q and K.
313 *
314 * @param q Q scratch buffer [num_heads * num_tokens * head_dim], in-place
315 * @param k K scratch buffer [num_kv_heads * num_tokens * head_dim], in-place
316 * @param q_gamma Q norm gamma weights [head_dim]
317 * @param k_gamma K norm gamma weights [head_dim]
318 * @param num_heads Number of query heads (e.g. 32 for Qwen3-8B)
319 * @param num_kv_heads Number of KV heads (e.g. 8 for Qwen3-8B with GQA)
320 * @param num_tokens Number of tokens (1 for decode, T for prefill)
321 * @param head_dim Dimension per head (e.g. 128)
322 * @param eps RMSNorm epsilon (e.g. 1e-6)
323 *
324 * @test unittest/test_qk_norm.py
325 */
326void qk_norm_forward(float *q, float *k,
327 const float *q_gamma, const float *k_gamma,
328 int num_heads, int num_kv_heads,
329 int num_tokens, int head_dim, float eps)
330{
331 /* Q norm: [num_heads * num_tokens] rows of [head_dim]
332 * Each row is one head's vector for one token. */
333 rmsnorm_forward(q, q_gamma, q, NULL,
334 num_heads * num_tokens, head_dim, head_dim, eps);
335
336 /* K norm: [num_kv_heads * num_tokens] rows of [head_dim]
337 * Same logic, fewer rows when using GQA. */
338 rmsnorm_forward(k, k_gamma, k, NULL,
339 num_kv_heads * num_tokens, head_dim, head_dim, eps);
340}
341
342void qk_norm_forward_fp64_sum(float *q, float *k,
343 const float *q_gamma, const float *k_gamma,
344 int num_heads, int num_kv_heads,
345 int num_tokens, int head_dim, float eps)
346{
347 rmsnorm_forward_fp64_sum(q, q_gamma, q, NULL,
348 num_heads * num_tokens, head_dim, head_dim, eps);
349 rmsnorm_forward_fp64_sum(k, k_gamma, k, NULL,
350 num_kv_heads * num_tokens, head_dim, head_dim, eps);
351}
352
353void qk_norm_forward_llama_production(float *q, float *k,
354 const float *q_gamma, const float *k_gamma,
355 int num_heads, int num_kv_heads,
356 int num_tokens, int head_dim, float eps)
357{
359 q, q_gamma, q, NULL,
360 num_heads * num_tokens, head_dim, head_dim, eps);
362 k, k_gamma, k, NULL,
363 num_kv_heads * num_tokens, head_dim, head_dim, eps);
364}
365
367 const float *q_gamma,
368 const float *k_gamma,
369 int num_heads, int num_kv_heads,
370 int num_tokens, int head_dim,
371 float eps)
372{
374 q, q_gamma, q, NULL,
375 num_heads * num_tokens, head_dim, head_dim, eps);
377 k, k_gamma, k, NULL,
378 num_kv_heads * num_tokens, head_dim, head_dim, eps);
379}
380
382 const float *q_gamma,
383 const float *k_gamma,
384 int num_heads,
385 int num_kv_heads,
386 int num_tokens,
387 int head_dim,
388 float eps)
389{
391 q, q_gamma, q, NULL,
392 num_heads * num_tokens, head_dim, head_dim, eps);
394 k, k_gamma, k, NULL,
395 num_kv_heads * num_tokens, head_dim, head_dim, eps);
396}
397
398/**
399 * Forward pass for Gemma4-assistant q-only per-head RMSNorm.
400 *
401 * Some Gemma4 assistant/drafter checkpoints project only Q and then reuse Q as
402 * the shared K/V stream. This wrapper keeps that public kernel contract explicit
403 * while reusing the same row-wise RMSNorm implementation as qk_norm_forward.
404 */
405void q_norm_forward(float *q,
406 const float *q_gamma,
407 int num_heads,
408 int num_tokens,
409 int head_dim,
410 float eps)
411{
412 rmsnorm_forward(q, q_gamma, q, NULL,
413 num_heads * num_tokens, head_dim, head_dim, eps);
414}
415
416/**
417 * Backward pass for per-head QK RMSNorm.
418 *
419 * This computes:
420 * - d_q / d_k for the Q and K activations
421 * - d_q_gamma / d_k_gamma for shared per-head gamma vectors
422 *
423 * Implementation is reference-first and deterministic:
424 * 1) recompute row rstd values from saved q/k inputs
425 * 2) call rmsnorm_backward on flattened [rows, head_dim] views
426 */
427void qk_norm_backward(const float *d_q_out, const float *d_k_out,
428 const float *q_in, const float *k_in,
429 const float *q_gamma, const float *k_gamma,
430 float *d_q_in, float *d_k_in,
431 float *d_q_gamma, float *d_k_gamma,
432 int num_heads, int num_kv_heads,
433 int num_tokens, int head_dim, float eps)
434{
435 int q_rows = num_heads * num_tokens;
436 int k_rows = num_kv_heads * num_tokens;
437
438 if (q_rows > 0) {
439 float q_rstd_cache[q_rows];
440 qk_norm_compute_rstd(q_in, q_rstd_cache, q_rows, head_dim, eps);
441 rmsnorm_backward(d_q_out, q_in, q_gamma, q_rstd_cache,
442 d_q_in, d_q_gamma, q_rows, head_dim, head_dim);
443 }
444
445 if (k_rows > 0) {
446 float k_rstd_cache[k_rows];
447 qk_norm_compute_rstd(k_in, k_rstd_cache, k_rows, head_dim, eps);
448 rmsnorm_backward(d_k_out, k_in, k_gamma, k_rstd_cache,
449 d_k_in, d_k_gamma, k_rows, head_dim, head_dim);
450 }
451}
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 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)