← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
fused_rmsnorm_linear.c
Go to the documentation of this file.
1/**
2 * @file fused_rmsnorm_linear.c
3 * @brief Fused RMSNorm + Linear (GEMV) kernel
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 * FUSION BENEFIT:
16 * ===============
17 * Unfused:
18 * RMSNorm(x) → [DRAM write: norm_out] → Quantize → [DRAM write: q8] → GEMV
19 * Total DRAM: 2 writes + 2 reads = 4 * hidden_size bytes
20 *
21 * Fused:
22 * RMSNorm(x) → [registers] → Quantize → [stack/L1: q8] → GEMV
23 * Total DRAM: 0 intermediate writes/reads
24 *
25 * Expected: 2-4x memory traffic reduction for this operation
26 */
27
28#include <assert.h>
29#include <math.h>
30#include <stddef.h>
31#include <stdint.h>
32#include <string.h>
33
34#include "ckernel_quant.h"
35
36#if defined(__AVX__) || defined(__AVX2__) || defined(__AVX512F__)
37#include <immintrin.h>
38#endif
39
40/* Forward declarations */
41void gemv_q4_k_q8_k(float *y, const void *W, const void *x_q8, int M, int K);
42
43/* Inline quantization helper - same as quantize_row_q8_k but operates on
44 * normalized values that may still be in registers/cache */
45static inline int ck_nearest_int_fused(float fval) {
46 float val = fval + 12582912.f;
47 int i;
48 memcpy(&i, &val, sizeof(int));
49 return (i & 0x007fffff) - 0x00400000;
50}
51
52#if defined(__AVX__) && !defined(__AVX512F__)
53static inline float hsum256_ps_fused(__m256 v) {
54 __m128 hi = _mm256_extractf128_ps(v, 1);
55 __m128 lo = _mm256_castps256_ps128(v);
56 __m128 sum128 = _mm_add_ps(lo, hi);
57 sum128 = _mm_hadd_ps(sum128, sum128);
58 sum128 = _mm_hadd_ps(sum128, sum128);
59 return _mm_cvtss_f32(sum128);
60}
61#endif
62
63/**
64 * @brief Fused RMSNorm + Q4_K Linear projection
65 *
66 * Computes: y = Linear(RMSNorm(x))
67 * where Linear uses Q4_K weights and Q8_K activations internally.
68 *
69 * The key optimization is that the normalized values never touch DRAM -
70 * they go directly from RMSNorm computation to Q8_K quantization to GEMV.
71 *
72 * @param y Output (FP32), shape [M]
73 * @param x Input hidden state (FP32), shape [K]
74 * @param gamma RMSNorm scale weights (FP32), shape [K]
75 * @param W_q4k Linear weights in Q4_K format, shape [M, K]
76 * @param M Output dimension (e.g., 3 * hidden for QKV)
77 * @param K Input dimension (hidden_size)
78 * @param eps RMSNorm epsilon (typically 1e-5 or 1e-6)
79 */
81 const float *x,
82 const float *gamma,
83 const void *W_q4k,
84 int M, int K,
85 float eps)
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}
256
257/**
258 * @brief Reference (unfused) implementation for correctness testing
259 *
260 * This is the SLOW version that does separate RMSNorm and GEMV calls,
261 * with intermediate results going to DRAM.
262 */
264 const float *x,
265 const float *gamma,
266 const void *W_q4k,
267 int M, int K,
268 float eps)
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)
Quantization block structures for weight-only quantization.
#define QK_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.
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.
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]