← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
fused_rmsnorm_linear.c File Reference

Fused RMSNorm + Linear (GEMV) kernel. More...

#include <assert.h>
#include <math.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "ckernel_quant.h"

Go to the source code of this file.

Functions

static int ck_nearest_int_fused (float fval)
 
void fused_rmsnorm_linear_q4k (float *y, const float *x, const float *gamma, const void *W_q4k, int M, int K, float eps)
 Fused RMSNorm + Q4_K Linear projection.
 
void gemv_q4_k_q8_k (float *y, const void *W, const void *x_q8, int M, int K)
 
void unfused_rmsnorm_linear_q4k_ref (float *y, const float *x, const float *gamma, const void *W_q4k, int M, int K, float eps)
 Reference (unfused) implementation for correctness testing.
 

Detailed Description

Fused RMSNorm + Linear (GEMV) kernel.

CK-ENGINE KERNEL RULES:

  1. NO malloc/free - memory via bump allocator, pointers passed in
  2. NO OpenMP - parallelization at orchestrator/codegen layer
  3. NO memcpy for layout - use strided access, not copies
  4. API must define: inputs, outputs, workspace, and memory layouts
  5. Pure computation - deterministic, no side effects

After changes: make test && make llamacpp-parity-full

FUSION BENEFIT:

Unfused: RMSNorm(x) → [DRAM write: norm_out] → Quantize → [DRAM write: q8] → GEMV Total DRAM: 2 writes + 2 reads = 4 * hidden_size bytes

Fused: RMSNorm(x) → [registers] → Quantize → [stack/L1: q8] → GEMV Total DRAM: 0 intermediate writes/reads

Expected: 2-4x memory traffic reduction for this operation

Definition in file fused_rmsnorm_linear.c.

Function Documentation

◆ ck_nearest_int_fused()

static int ck_nearest_int_fused ( float  fval)
inlinestatic

Definition at line 45 of file fused_rmsnorm_linear.c.

45 {
46 float val = fval + 12582912.f;
47 int i;
48 memcpy(&i, &val, sizeof(int));
49 return (i & 0x007fffff) - 0x00400000;
50}

Referenced by fused_rmsnorm_linear_q4k().

◆ fused_rmsnorm_linear_q4k()

void fused_rmsnorm_linear_q4k ( float *  y,
const float *  x,
const float *  gamma,
const void *  W_q4k,
int  M,
int  K,
float  eps 
)

Fused RMSNorm + Q4_K Linear projection.

Computes: y = Linear(RMSNorm(x)) where Linear uses Q4_K weights and Q8_K activations internally.

The key optimization is that the normalized values never touch DRAM - they go directly from RMSNorm computation to Q8_K quantization to GEMV.

Parameters
yOutput (FP32), shape [M]
xInput hidden state (FP32), shape [K]
gammaRMSNorm scale weights (FP32), shape [K]
W_q4kLinear weights in Q4_K format, shape [M, K]
MOutput dimension (e.g., 3 * hidden for QKV)
KInput dimension (hidden_size)
epsRMSNorm epsilon (typically 1e-5 or 1e-6)

Definition at line 80 of file fused_rmsnorm_linear.c.

86{
87 if (!y || !x || !gamma || !W_q4k || M <= 0 || K <= 0) {
88 return;
89 }
90
91 assert(K % QK_K == 0);
92 const int nb = K / QK_K; /* Number of Q8_K blocks */
93
94 /* Stack-allocated Q8_K buffer - stays in L1/L2 cache */
95 /* Max supported K = 8192 (8 blocks of 256) */
96 block_q8_K q8_buffer[32]; /* 32 * ~260 bytes = ~8KB on stack */
97 assert(nb <= 32 && "K too large for stack buffer");
98
99 /* ================================================================
100 * PHASE 1: Compute RMSNorm and quantize to Q8_K
101 * Result stays in stack (L1/L2), never touches DRAM
102 * ================================================================ */
103
104#if defined(__AVX512F__)
105 /* AVX-512: Compute sum of squares */
106 __m512 sum_sq_vec = _mm512_setzero_ps();
107 int d = 0;
108 for (; d + 16 <= K; d += 16) {
109 __m512 xv = _mm512_loadu_ps(&x[d]);
110 sum_sq_vec = _mm512_fmadd_ps(xv, xv, sum_sq_vec);
111 }
112 float sum_sq = _mm512_reduce_add_ps(sum_sq_vec);
113 for (; d < K; ++d) {
114 sum_sq += x[d] * x[d];
115 }
116
117#elif defined(__AVX__)
118 /* AVX: Compute sum of squares */
119 __m256 sum_sq_vec = _mm256_setzero_ps();
120 int d = 0;
121 for (; d + 8 <= K; d += 8) {
122 __m256 xv = _mm256_loadu_ps(&x[d]);
123 __m256 xv_sq = _mm256_mul_ps(xv, xv);
124 sum_sq_vec = _mm256_add_ps(sum_sq_vec, xv_sq);
125 }
126 float sum_sq = hsum256_ps_fused(sum_sq_vec);
127 for (; d < K; ++d) {
128 sum_sq += x[d] * x[d];
129 }
130
131#else
132 /* Scalar fallback */
133 double sum_sq = 0.0;
134 for (int d = 0; d < K; ++d) {
135 double v = (double)x[d];
136 sum_sq += v * v;
137 }
138#endif
139
140 float mean_sq = (float)sum_sq / (float)K;
141 float rstd = 1.0f / sqrtf(mean_sq + eps);
142
143 /* ================================================================
144 * PHASE 2: Apply RMSNorm and quantize to Q8_K in one pass
145 * Normalized values go directly to Q8_K blocks
146 * ================================================================ */
147
148 for (int i = 0; i < nb; ++i) {
149 const float *x_block = x + i * QK_K;
150 const float *g_block = gamma + i * QK_K;
151
152 /* Find max absolute value for this block's normalized output */
153 float max_val = 0.0f;
154 float amax = 0.0f;
155
156#if defined(__AVX512F__)
157 __m512 rstd_vec = _mm512_set1_ps(rstd);
158 __m512 max_vec = _mm512_setzero_ps();
159 __m512 sign_mask = _mm512_set1_ps(-0.0f);
160
161 for (int j = 0; j < QK_K; j += 16) {
162 __m512 xv = _mm512_loadu_ps(&x_block[j]);
163 __m512 gv = _mm512_loadu_ps(&g_block[j]);
164 __m512 norm = _mm512_mul_ps(_mm512_mul_ps(xv, rstd_vec), gv);
165 __m512 abs_norm = _mm512_andnot_ps(sign_mask, norm);
166 max_vec = _mm512_max_ps(max_vec, abs_norm);
167
168 /* Track max with sign for scale computation */
169 __mmask16 gt_mask = _mm512_cmp_ps_mask(abs_norm, _mm512_set1_ps(amax), _CMP_GT_OQ);
170 if (gt_mask) {
171 float temp_amax = _mm512_reduce_max_ps(abs_norm);
172 if (temp_amax > amax) {
173 amax = temp_amax;
174 /* Find the actual max value with sign */
175 for (int k = 0; k < 16; ++k) {
176 float v = x_block[j + k] * rstd * g_block[j + k];
177 if (fabsf(v) >= amax - 1e-6f) {
178 max_val = v;
179 break;
180 }
181 }
182 }
183 }
184 }
185 amax = _mm512_reduce_max_ps(max_vec);
186
187#elif defined(__AVX__)
188 __m256 rstd_vec = _mm256_set1_ps(rstd);
189
190 for (int j = 0; j < QK_K; j += 8) {
191 __m256 xv = _mm256_loadu_ps(&x_block[j]);
192 __m256 gv = _mm256_loadu_ps(&g_block[j]);
193 __m256 norm = _mm256_mul_ps(_mm256_mul_ps(xv, rstd_vec), gv);
194
195 /* Check each element for max */
196 float norm_arr[8];
197 _mm256_storeu_ps(norm_arr, norm);
198 for (int k = 0; k < 8; ++k) {
199 float av = fabsf(norm_arr[k]);
200 if (av > amax) {
201 amax = av;
202 max_val = norm_arr[k];
203 }
204 }
205 }
206
207#else
208 for (int j = 0; j < QK_K; ++j) {
209 float norm = x_block[j] * rstd * g_block[j];
210 float av = fabsf(norm);
211 if (av > amax) {
212 amax = av;
213 max_val = norm;
214 }
215 }
216#endif
217
218 /* Handle zero block */
219 if (amax < 1e-10f) {
220 q8_buffer[i].d = 0.0f;
221 memset(q8_buffer[i].qs, 0, sizeof(q8_buffer[i].qs));
222 memset(q8_buffer[i].bsums, 0, sizeof(q8_buffer[i].bsums));
223 continue;
224 }
225
226 /* Compute scale and quantize */
227 const float iscale = -127.0f / max_val;
228 q8_buffer[i].d = 1.0f / iscale;
229
230 /* Quantize and compute bsums */
231 for (int j = 0; j < QK_K; ++j) {
232 float norm = x_block[j] * rstd * g_block[j];
233 int v = ck_nearest_int_fused(iscale * norm);
234 v = (v > 127) ? 127 : ((v < -128) ? -128 : v);
235 q8_buffer[i].qs[j] = (int8_t)v;
236 }
237
238 /* Compute block sums (16 elements each) */
239 for (int j = 0; j < QK_K / 16; ++j) {
240 int sum = 0;
241 const int8_t *qs = &q8_buffer[i].qs[j * 16];
242 for (int k = 0; k < 16; ++k) {
243 sum += qs[k];
244 }
245 q8_buffer[i].bsums[j] = (int16_t)sum;
246 }
247 }
248
249 /* ================================================================
250 * PHASE 3: GEMV with Q4_K weights and Q8_K activations
251 * Q8_K data is in stack (L1/L2), not DRAM
252 * ================================================================ */
253
254 gemv_q4_k_q8_k(y, W_q4k, q8_buffer, M, K);
255}
#define QK_K
static int ck_nearest_int_fused(float fval)
void gemv_q4_k_q8_k(float *y, const void *W, const void *x_q8, int M, int K)
int8_t qs[256]
int16_t bsums[256/16]

References block_q8_K::bsums, ck_nearest_int_fused(), block_q8_K::d, gemv_q4_k_q8_k(), QK_K, and block_q8_K::qs.

◆ gemv_q4_k_q8_k()

void gemv_q4_k_q8_k ( float *  y,
const void *  W,
const void *  x_q8,
int  M,
int  K 
)

Definition at line 273 of file gemm_kernels_q4k_q8k.c.

277{
278 if (ck_q4k_q8k_force_ref()) {
279 gemv_q4_k_q8_k_ref(y, W, x_q8, M, K);
280 return;
281 }
282#if defined(__AVX512VNNI__) && defined(__AVX512VL__) && !defined(CK_NO_AVX512_VNNI)
283 /* VNNI: Best for decode (single token) - INT8 dot product acceleration */
284 gemv_q4_k_q8_k_vnni(y, W, x_q8, M, K);
285#elif defined(__AVX2__)
286 gemv_q4_k_q8_k_avx2(y, W, x_q8, M, K);
287#elif defined(__AVX__)
288 /* AVX version uses maddubs_epi16 (more efficient than SSE) */
289 gemv_q4_k_q8_k_avx(y, W, x_q8, M, K);
290#elif defined(__SSE4_1__)
291 gemv_q4_k_q8_k_sse(y, W, x_q8, M, K);
292#else
293 gemv_q4_k_q8_k_ref(y, W, x_q8, M, K);
294#endif
295}
void gemv_q4_k_q8_k_avx2(float *y, const void *W, const void *x_q8, int M, int K)
void gemv_q4_k_q8_k_vnni(float *y, const void *W, const void *x_q8, int M, int K)
void gemv_q4_k_q8_k_ref(float *y, const void *W, const void *x_q8, int M, int K)
static int ck_q4k_q8k_force_ref(void)
void gemv_q4_k_q8_k_avx(float *y, const void *W, const void *x_q8, int M, int K)
void gemv_q4_k_q8_k_sse(float *y, const void *W, const void *x_q8, int M, int K)

Referenced by fused_rmsnorm_linear_q4k(), and unfused_rmsnorm_linear_q4k_ref().

◆ unfused_rmsnorm_linear_q4k_ref()

void unfused_rmsnorm_linear_q4k_ref ( float *  y,
const float *  x,
const float *  gamma,
const void *  W_q4k,
int  M,
int  K,
float  eps 
)

Reference (unfused) implementation for correctness testing.

This is the SLOW version that does separate RMSNorm and GEMV calls, with intermediate results going to DRAM.

Definition at line 263 of file fused_rmsnorm_linear.c.

269{
270 if (!y || !x || !gamma || !W_q4k || M <= 0 || K <= 0) {
271 return;
272 }
273
274 assert(K % QK_K == 0);
275
276 /* Stack-allocated buffers (no malloc!) - stays in L1/L2 cache */
277 /* Max supported: K=4096 (16KB), 16 blocks (~5KB) */
278 if (K > 4096) return;
279
280 float norm_out[4096];
281 block_q8_K q8_buffer[16]; /* 16 blocks for K=4096, K/QK_K */
282
283 /* Step 1: RMSNorm (stays in cache via stack buffer) */
284 double sum_sq = 0.0;
285 for (int d = 0; d < K; ++d) {
286 sum_sq += (double)x[d] * (double)x[d];
287 }
288 float rstd = 1.0f / sqrtf((float)(sum_sq / K) + eps);
289
290 for (int d = 0; d < K; ++d) {
291 norm_out[d] = x[d] * rstd * gamma[d]; /* DRAM WRITE */
292 }
293
294 /* Step 2: Quantize (reads DRAM, writes DRAM) */
295 extern void quantize_row_q8_k(const float *x, void *vy, int k);
296 quantize_row_q8_k(norm_out, q8_buffer, K); /* DRAM READ + WRITE */
297
298 /* Step 3: GEMV (reads Q8_K from cache) */
299 gemv_q4_k_q8_k(y, W_q4k, q8_buffer, M, K);
300
301 /* No free needed - stack buffers auto-deallocate */
302}
void quantize_row_q8_k(const float *x, void *y, int k)

References gemv_q4_k_q8_k(), QK_K, and quantize_row_q8_k().