← Back to C-Kernel-Engine Docs Doxygen Source Documentation
 
Loading...
Searching...
No Matches
attention_mlp_fused.c
Go to the documentation of this file.
1/**
2 * @file attention_mlp_fused.c
3 * @brief Mega-Fused Attention + MLP Block
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 * VIOLATION: Uses memcpy for layout conversion. TODO: Use strided access.
16 *
17 * Part of C-Kernel-Engine v6.6 Fusion Kernels
18 *
19 * FUSES THE ENTIRE BLOCK from Attention output to next layer input:
20 *
21 * Attention(Q, K_cache, V_cache)
22 * │
23 * ▼
24 * Output Projection (attn @ Wo)
25 * │
26 * ▼
27 * + residual_1
28 * │
29 * ▼
30 * RMSNorm
31 * │
32 * ▼
33 * MLP: gate ──► SwiGLU ◄── up
34 * │
35 * ▼
36 * down
37 * │
38 * ▼
39 * + residual_2
40 * │
41 * ▼
42 * hidden_out (ready for next layer)
43 *
44 * NON-FUSED version writes these buffers to DRAM:
45 * - attn_output [embed_dim]
46 * - projected [embed_dim]
47 * - hidden_after_attn [embed_dim]
48 * - normed [embed_dim]
49 * - gate [intermediate_dim]
50 * - up [intermediate_dim]
51 * - swiglu [intermediate_dim]
52 * - mlp_out [embed_dim]
53 * = 8 DRAM round-trips!
54 *
55 * FUSED version: ALL intermediates stay in L1/L2, ZERO DRAM writes
56 *
57 * EXPECTED SPEEDUP: 2-3x for this block
58 */
59
60#include <stdint.h>
61#include <stddef.h>
62#include <stdlib.h>
63#include <math.h>
64#include <string.h>
65
66#ifdef __AVX2__
67#include <immintrin.h>
68#endif
69
70#include "ckernel_quant.h"
71
72/* ============================================================================
73 * HELPER: RMSNorm computation (inline, result stays in registers)
74 * ============================================================================ */
75
76static inline float compute_rms_scale_internal(const float *x, int n, float eps) {
77 float sum_sq = 0.0f;
78
79#ifdef __AVX2__
80 __m256 vsum = _mm256_setzero_ps();
81 int i = 0;
82 for (; i + 7 < n; i += 8) {
83 __m256 vx = _mm256_loadu_ps(x + i);
84 vsum = _mm256_fmadd_ps(vx, vx, vsum);
85 }
86 __m128 vlow = _mm256_castps256_ps128(vsum);
87 __m128 vhigh = _mm256_extractf128_ps(vsum, 1);
88 vlow = _mm_add_ps(vlow, vhigh);
89 vlow = _mm_hadd_ps(vlow, vlow);
90 vlow = _mm_hadd_ps(vlow, vlow);
91 sum_sq = _mm_cvtss_f32(vlow);
92 for (; i < n; i++) {
93 sum_sq += x[i] * x[i];
94 }
95#else
96 for (int i = 0; i < n; i++) {
97 sum_sq += x[i] * x[i];
98 }
99#endif
100
101 float rms = sqrtf(sum_sq / (float)n + eps);
102 return 1.0f / rms;
103}
104
105/* ============================================================================
106 * HELPER: SiLU activation (x * sigmoid(x))
107 * ============================================================================ */
108
109static inline float silu_scalar(float x) {
110 return x / (1.0f + expf(-x));
111}
112
113#ifdef __AVX2__
114static inline __m256 silu_avx2(__m256 x) {
115 float lanes[8];
116 _mm256_storeu_ps(lanes, x);
117 for (int i = 0; i < 8; i++) {
118 lanes[i] = silu_scalar(lanes[i]);
119 }
120 return _mm256_loadu_ps(lanes);
121}
122#endif
123
124/* ============================================================================
125 * HELPER: Softmax with online computation (for attention)
126 * ============================================================================ */
127
128static void softmax_inplace(float *x, int n) {
129 float max_val = x[0];
130 for (int i = 1; i < n; i++) {
131 if (x[i] > max_val) max_val = x[i];
132 }
133
134 float sum = 0.0f;
135 for (int i = 0; i < n; i++) {
136 x[i] = expf(x[i] - max_val);
137 sum += x[i];
138 }
139
140 float inv_sum = 1.0f / sum;
141 for (int i = 0; i < n; i++) {
142 x[i] *= inv_sum;
143 }
144}
145
146/* ============================================================================
147 * MEGA-FUSED KERNEL: Attention + Output + RMSNorm + MLP
148 *
149 * This fuses the entire block from attention to MLP output.
150 * All intermediates stay in L1/L2 cache.
151 * ============================================================================ */
152
154 /* Attention inputs */
155 const float *q, /* [num_heads * head_dim] query vector */
156 const float *k_cache, /* [seq_len, num_kv_heads * head_dim] K cache */
157 const float *v_cache, /* [seq_len, num_kv_heads * head_dim] V cache */
158 int seq_len, /* Current sequence length */
159 int num_heads,
160 int num_kv_heads,
161 int head_dim,
162 float attn_scale, /* 1/sqrt(head_dim) */
163
164 /* Output projection */
165 const float *wo, /* [embed_dim, num_heads * head_dim] */
166
167 /* Residual input */
168 const float *residual_1, /* [embed_dim] input to attention block */
169
170 /* RMSNorm */
171 const float *rms_weight, /* [embed_dim] */
172 float eps,
173
174 /* MLP weights (FP32 for this version) */
175 const float *w_gate, /* [intermediate_dim, embed_dim] */
176 const float *w_up, /* [intermediate_dim, embed_dim] */
177 const float *w_down, /* [embed_dim, intermediate_dim] */
178
179 /* Residual 2 input (usually same as after attention residual) */
180 /* If NULL, uses the hidden_after_attn */
181
182 /* Dimensions */
183 int embed_dim,
184 int intermediate_dim,
185
186 /* Output */
187 float *hidden_out /* [embed_dim] output for next layer */
188) {
189 const int heads_per_kv = num_heads / num_kv_heads;
190 const int q_dim = num_heads * head_dim;
191 const int kv_dim = num_kv_heads * head_dim;
192
193 /* Stack buffers - all stay in L1/L2 */
194 float attn_out[4096]; /* Attention output per head, then combined */
195 float hidden_after_attn[4096];
196 float normed[4096];
197 float gate_out[16384]; /* Intermediate dim (e.g., 4864 for Qwen2) */
198 float up_out[16384];
199
200 if (embed_dim > 4096 || intermediate_dim > 16384) {
201 return; /* TODO: heap allocation for large models */
202 }
203
204 /* ═══════════════════════════════════════════════════════════════════════
205 * STEP 1: Multi-Head Attention (Q @ K^T -> softmax -> @ V)
206 * ═══════════════════════════════════════════════════════════════════════ */
207
208 memset(attn_out, 0, q_dim * sizeof(float));
209
210 for (int h = 0; h < num_heads; h++) {
211 int kv_h = h / heads_per_kv; /* GQA: map query head to KV head */
212
213 const float *q_head = q + h * head_dim;
214 float *out_head = attn_out + h * head_dim;
215
216 /* Compute attention scores: Q @ K^T */
217 float scores[8192]; /* Max seq_len */
218 if (seq_len > 8192) return;
219
220 for (int t = 0; t < seq_len; t++) {
221 const float *k_t = k_cache + t * kv_dim + kv_h * head_dim;
222 float score = 0.0f;
223 for (int d = 0; d < head_dim; d++) {
224 score += q_head[d] * k_t[d];
225 }
226 scores[t] = score * attn_scale;
227 }
228
229 /* Softmax */
230 softmax_inplace(scores, seq_len);
231
232 /* Weighted sum of V: scores @ V */
233 for (int t = 0; t < seq_len; t++) {
234 const float *v_t = v_cache + t * kv_dim + kv_h * head_dim;
235 float w = scores[t];
236 for (int d = 0; d < head_dim; d++) {
237 out_head[d] += w * v_t[d];
238 }
239 }
240 }
241
242 /* ═══════════════════════════════════════════════════════════════════════
243 * STEP 2: Output Projection (attn_out @ Wo) + Residual
244 * ═══════════════════════════════════════════════════════════════════════ */
245
246 for (int i = 0; i < embed_dim; i++) {
247 float sum = 0.0f;
248 const float *wo_row = wo + i * q_dim;
249 for (int j = 0; j < q_dim; j++) {
250 sum += wo_row[j] * attn_out[j];
251 }
252 hidden_after_attn[i] = sum + residual_1[i]; /* Residual add */
253 }
254
255 /* ═══════════════════════════════════════════════════════════════════════
256 * STEP 3: RMSNorm
257 * ═══════════════════════════════════════════════════════════════════════ */
258
259 float rms_scale = compute_rms_scale_internal(hidden_after_attn, embed_dim, eps);
260
261#ifdef __AVX2__
262 __m256 vscale = _mm256_set1_ps(rms_scale);
263 int i = 0;
264 for (; i + 7 < embed_dim; i += 8) {
265 __m256 vh = _mm256_loadu_ps(hidden_after_attn + i);
266 __m256 vw = _mm256_loadu_ps(rms_weight + i);
267 __m256 vn = _mm256_mul_ps(_mm256_mul_ps(vh, vw), vscale);
268 _mm256_storeu_ps(normed + i, vn);
269 }
270 for (; i < embed_dim; i++) {
271 normed[i] = hidden_after_attn[i] * rms_weight[i] * rms_scale;
272 }
273#else
274 for (int i = 0; i < embed_dim; i++) {
275 normed[i] = hidden_after_attn[i] * rms_weight[i] * rms_scale;
276 }
277#endif
278
279 /* ═══════════════════════════════════════════════════════════════════════
280 * STEP 4: MLP Gate + Up projections (can be parallelized)
281 * ═══════════════════════════════════════════════════════════════════════ */
282
283 /* Gate projection: gate_out = normed @ W_gate^T */
284 for (int i = 0; i < intermediate_dim; i++) {
285 float sum = 0.0f;
286 const float *wg_row = w_gate + i * embed_dim;
287 for (int j = 0; j < embed_dim; j++) {
288 sum += wg_row[j] * normed[j];
289 }
290 gate_out[i] = sum;
291 }
292
293 /* Up projection: up_out = normed @ W_up^T */
294 for (int i = 0; i < intermediate_dim; i++) {
295 float sum = 0.0f;
296 const float *wu_row = w_up + i * embed_dim;
297 for (int j = 0; j < embed_dim; j++) {
298 sum += wu_row[j] * normed[j];
299 }
300 up_out[i] = sum;
301 }
302
303 /* ═══════════════════════════════════════════════════════════════════════
304 * STEP 5: SwiGLU activation: silu(gate) * up
305 * ═══════════════════════════════════════════════════════════════════════ */
306
307#ifdef __AVX2__
308 i = 0;
309 for (; i + 7 < intermediate_dim; i += 8) {
310 __m256 vg = _mm256_loadu_ps(gate_out + i);
311 __m256 vu = _mm256_loadu_ps(up_out + i);
312 __m256 vsilu = silu_avx2(vg);
313 __m256 vswiglu = _mm256_mul_ps(vsilu, vu);
314 _mm256_storeu_ps(gate_out + i, vswiglu); /* Reuse gate_out buffer */
315 }
316 for (; i < intermediate_dim; i++) {
317 gate_out[i] = silu_scalar(gate_out[i]) * up_out[i];
318 }
319#else
320 for (int i = 0; i < intermediate_dim; i++) {
321 gate_out[i] = silu_scalar(gate_out[i]) * up_out[i];
322 }
323#endif
324
325 /* ═══════════════════════════════════════════════════════════════════════
326 * STEP 6: Down projection + Final Residual
327 * ═══════════════════════════════════════════════════════════════════════ */
328
329 for (int i = 0; i < embed_dim; i++) {
330 float sum = 0.0f;
331 const float *wd_row = w_down + i * intermediate_dim;
332 for (int j = 0; j < intermediate_dim; j++) {
333 sum += wd_row[j] * gate_out[j]; /* gate_out now holds SwiGLU output */
334 }
335 hidden_out[i] = sum + hidden_after_attn[i]; /* Final residual */
336 }
337}
338
339/* ============================================================================
340 * V2: MLP-ONLY FUSED KERNEL with SIMD GEMV
341 *
342 * Key optimizations over v1:
343 * 1. AVX2 SIMD for ALL GEMVs (not just RMSNorm/SwiGLU)
344 * 2. Gate + Up computed TOGETHER (one pass through normed)
345 * 3. Horizontal sums done efficiently
346 *
347 * This isolates the MLP portion for benchmarking.
348 * ============================================================================ */
349
350#ifdef __AVX2__
351/* Inline SIMD GEMV helper - processes one output row */
352static inline float gemv_fp32_row_avx2(
353 const float *row, /* [K] weight row */
354 const float *x, /* [K] input vector */
355 int K
356) {
357 __m256 acc = _mm256_setzero_ps();
358 int k = 0;
359
360 for (; k + 7 < K; k += 8) {
361 __m256 vw = _mm256_loadu_ps(row + k);
362 __m256 vx = _mm256_loadu_ps(x + k);
363 acc = _mm256_fmadd_ps(vw, vx, acc);
364 }
365
366 /* Horizontal sum */
367 __m128 vlow = _mm256_castps256_ps128(acc);
368 __m128 vhigh = _mm256_extractf128_ps(acc, 1);
369 vlow = _mm_add_ps(vlow, vhigh);
370 __m128 shuf = _mm_movehdup_ps(vlow);
371 vlow = _mm_add_ps(vlow, shuf);
372 shuf = _mm_movehl_ps(shuf, vlow);
373 vlow = _mm_add_ss(vlow, shuf);
374 float sum = _mm_cvtss_f32(vlow);
375
376 /* Remainder */
377 for (; k < K; k++) {
378 sum += row[k] * x[k];
379 }
380
381 return sum;
382}
383#endif
384
386 /* Input (after attention + residual) */
387 const float *hidden_in, /* [embed_dim] */
388
389 /* RMSNorm */
390 const float *rms_weight, /* [embed_dim] */
391 float eps,
392
393 /* MLP weights (FP32) */
394 const float *w_gate, /* [intermediate_dim, embed_dim] */
395 const float *w_up, /* [intermediate_dim, embed_dim] */
396 const float *w_down, /* [embed_dim, intermediate_dim] */
397
398 /* Dimensions */
399 int embed_dim,
400 int intermediate_dim,
401
402 /* Output */
403 float *hidden_out /* [embed_dim] */
404) {
405 /* Stack buffers - sized for typical models */
406 float normed[4096];
407 float swiglu[16384]; /* intermediate_dim */
408
409 if (embed_dim > 4096 || intermediate_dim > 16384) {
410 return; /* TODO: handle larger models */
411 }
412
413 /* ═══════════════════════════════════════════════════════════════════════
414 * STEP 1: RMSNorm (SIMD)
415 * ═══════════════════════════════════════════════════════════════════════ */
416
417 float rms_scale = compute_rms_scale_internal(hidden_in, embed_dim, eps);
418
419#ifdef __AVX2__
420 __m256 vscale = _mm256_set1_ps(rms_scale);
421 int i = 0;
422 for (; i + 7 < embed_dim; i += 8) {
423 __m256 vh = _mm256_loadu_ps(hidden_in + i);
424 __m256 vw = _mm256_loadu_ps(rms_weight + i);
425 __m256 vn = _mm256_mul_ps(_mm256_mul_ps(vh, vw), vscale);
426 _mm256_storeu_ps(normed + i, vn);
427 }
428 for (; i < embed_dim; i++) {
429 normed[i] = hidden_in[i] * rms_weight[i] * rms_scale;
430 }
431#else
432 for (int i = 0; i < embed_dim; i++) {
433 normed[i] = hidden_in[i] * rms_weight[i] * rms_scale;
434 }
435#endif
436
437 /* ═══════════════════════════════════════════════════════════════════════
438 * STEP 2: Gate + Up projections with TRUE FUSION + SwiGLU
439 *
440 * Key insight: Compute gate[i] and up[i] together, then immediately
441 * apply SwiGLU. This eliminates separate gate_out and up_out buffers.
442 * ═══════════════════════════════════════════════════════════════════════ */
443
444#ifdef __AVX2__
445 for (int j = 0; j < intermediate_dim; j++) {
446 /* Compute gate and up for output j using SIMD GEMV */
447 const float *wg_row = w_gate + j * embed_dim;
448 const float *wu_row = w_up + j * embed_dim;
449
450 __m256 gate_acc = _mm256_setzero_ps();
451 __m256 up_acc = _mm256_setzero_ps();
452
453 int k = 0;
454 for (; k + 7 < embed_dim; k += 8) {
455 __m256 vn = _mm256_loadu_ps(normed + k);
456 __m256 vwg = _mm256_loadu_ps(wg_row + k);
457 __m256 vwu = _mm256_loadu_ps(wu_row + k);
458
459 gate_acc = _mm256_fmadd_ps(vwg, vn, gate_acc);
460 up_acc = _mm256_fmadd_ps(vwu, vn, up_acc);
461 }
462
463 /* Horizontal sums */
464 __m128 glow = _mm256_castps256_ps128(gate_acc);
465 __m128 ghigh = _mm256_extractf128_ps(gate_acc, 1);
466 glow = _mm_add_ps(glow, ghigh);
467 __m128 gshuf = _mm_movehdup_ps(glow);
468 glow = _mm_add_ps(glow, gshuf);
469 gshuf = _mm_movehl_ps(gshuf, glow);
470 glow = _mm_add_ss(glow, gshuf);
471 float gate_val = _mm_cvtss_f32(glow);
472
473 __m128 ulow = _mm256_castps256_ps128(up_acc);
474 __m128 uhigh = _mm256_extractf128_ps(up_acc, 1);
475 ulow = _mm_add_ps(ulow, uhigh);
476 __m128 ushuf = _mm_movehdup_ps(ulow);
477 ulow = _mm_add_ps(ulow, ushuf);
478 ushuf = _mm_movehl_ps(ushuf, ulow);
479 ulow = _mm_add_ss(ulow, ushuf);
480 float up_val = _mm_cvtss_f32(ulow);
481
482 /* Remainder */
483 for (; k < embed_dim; k++) {
484 gate_val += wg_row[k] * normed[k];
485 up_val += wu_row[k] * normed[k];
486 }
487
488 /* Fused SwiGLU: silu(gate) * up */
489 swiglu[j] = silu_scalar(gate_val) * up_val;
490 }
491#else
492 for (int j = 0; j < intermediate_dim; j++) {
493 const float *wg_row = w_gate + j * embed_dim;
494 const float *wu_row = w_up + j * embed_dim;
495 float gate_val = 0.0f, up_val = 0.0f;
496
497 for (int k = 0; k < embed_dim; k++) {
498 gate_val += wg_row[k] * normed[k];
499 up_val += wu_row[k] * normed[k];
500 }
501
502 swiglu[j] = silu_scalar(gate_val) * up_val;
503 }
504#endif
505
506 /* ═══════════════════════════════════════════════════════════════════════
507 * STEP 3: Down projection + Residual (SIMD GEMV)
508 * ═══════════════════════════════════════════════════════════════════════ */
509
510#ifdef __AVX2__
511 for (int j = 0; j < embed_dim; j++) {
512 float sum = gemv_fp32_row_avx2(w_down + j * intermediate_dim, swiglu, intermediate_dim);
513 hidden_out[j] = sum + hidden_in[j]; /* Residual */
514 }
515#else
516 for (int j = 0; j < embed_dim; j++) {
517 float sum = 0.0f;
518 const float *wd_row = w_down + j * intermediate_dim;
519 for (int k = 0; k < intermediate_dim; k++) {
520 sum += wd_row[k] * swiglu[k];
521 }
522 hidden_out[j] = sum + hidden_in[j];
523 }
524#endif
525}
526
527
528/* ============================================================================
529 * V3: MLP with SIMD GEMV but SEQUENTIAL weight access
530 *
531 * Key insight from v2 benchmark: fusing gate+up HURTS performance because
532 * interleaved weight loading destroys cache prefetch patterns.
533 *
534 * v3 approach:
535 * 1. Use SIMD GEMV for all projections
536 * 2. Keep SEQUENTIAL weight access (gate first, then up)
537 * 3. Still fuse SwiGLU immediately after projections
538 *
539 * This should be faster than v2 AND faster than scalar separate.
540 * ============================================================================ */
541
543 const float *hidden_in,
544 const float *rms_weight,
545 float eps,
546 const float *w_gate,
547 const float *w_up,
548 const float *w_down,
549 int embed_dim,
550 int intermediate_dim,
551 float *hidden_out
552) {
553 /* Stack buffers */
554 float normed[4096];
555 float gate_out[16384];
556 float swiglu[16384];
557
558 if (embed_dim > 4096 || intermediate_dim > 16384) {
559 return;
560 }
561
562 /* ═══════════════════════════════════════════════════════════════════════
563 * STEP 1: RMSNorm (SIMD)
564 * ═══════════════════════════════════════════════════════════════════════ */
565
566 float rms_scale = compute_rms_scale_internal(hidden_in, embed_dim, eps);
567
568#ifdef __AVX2__
569 __m256 vscale = _mm256_set1_ps(rms_scale);
570 int i = 0;
571 for (; i + 7 < embed_dim; i += 8) {
572 __m256 vh = _mm256_loadu_ps(hidden_in + i);
573 __m256 vw = _mm256_loadu_ps(rms_weight + i);
574 __m256 vn = _mm256_mul_ps(_mm256_mul_ps(vh, vw), vscale);
575 _mm256_storeu_ps(normed + i, vn);
576 }
577 for (; i < embed_dim; i++) {
578 normed[i] = hidden_in[i] * rms_weight[i] * rms_scale;
579 }
580#else
581 for (int i = 0; i < embed_dim; i++) {
582 normed[i] = hidden_in[i] * rms_weight[i] * rms_scale;
583 }
584#endif
585
586 /* ═══════════════════════════════════════════════════════════════════════
587 * STEP 2: Gate projection (SIMD GEMV, sequential weight access)
588 * ═══════════════════════════════════════════════════════════════════════ */
589
590#ifdef __AVX2__
591 for (int j = 0; j < intermediate_dim; j++) {
592 gate_out[j] = gemv_fp32_row_avx2(w_gate + j * embed_dim, normed, embed_dim);
593 }
594#else
595 for (int j = 0; j < intermediate_dim; j++) {
596 float sum = 0.0f;
597 const float *wg_row = w_gate + j * embed_dim;
598 for (int k = 0; k < embed_dim; k++) {
599 sum += wg_row[k] * normed[k];
600 }
601 gate_out[j] = sum;
602 }
603#endif
604
605 /* ═══════════════════════════════════════════════════════════════════════
606 * STEP 3: Up projection + FUSED SwiGLU (SIMD GEMV, sequential access)
607 *
608 * Key: compute up[j], then immediately apply SwiGLU with gate[j].
609 * This avoids storing the full up_out buffer.
610 * ═══════════════════════════════════════════════════════════════════════ */
611
612#ifdef __AVX2__
613 for (int j = 0; j < intermediate_dim; j++) {
614 float up_val = gemv_fp32_row_avx2(w_up + j * embed_dim, normed, embed_dim);
615 /* Fused SwiGLU: silu(gate) * up */
616 swiglu[j] = silu_scalar(gate_out[j]) * up_val;
617 }
618#else
619 for (int j = 0; j < intermediate_dim; j++) {
620 float up_val = 0.0f;
621 const float *wu_row = w_up + j * embed_dim;
622 for (int k = 0; k < embed_dim; k++) {
623 up_val += wu_row[k] * normed[k];
624 }
625 swiglu[j] = silu_scalar(gate_out[j]) * up_val;
626 }
627#endif
628
629 /* ═══════════════════════════════════════════════════════════════════════
630 * STEP 4: Down projection + Residual (SIMD GEMV)
631 * ═══════════════════════════════════════════════════════════════════════ */
632
633#ifdef __AVX2__
634 for (int j = 0; j < embed_dim; j++) {
635 float sum = gemv_fp32_row_avx2(w_down + j * intermediate_dim, swiglu, intermediate_dim);
636 hidden_out[j] = sum + hidden_in[j];
637 }
638#else
639 for (int j = 0; j < embed_dim; j++) {
640 float sum = 0.0f;
641 const float *wd_row = w_down + j * intermediate_dim;
642 for (int k = 0; k < intermediate_dim; k++) {
643 sum += wd_row[k] * swiglu[k];
644 }
645 hidden_out[j] = sum + hidden_in[j];
646 }
647#endif
648}
649
650
651/* ============================================================================
652 * SEPARATE MLP (for benchmarking comparison)
653 *
654 * Same operations but as separate function calls.
655 * ============================================================================ */
656
658 const float *hidden_in,
659 const float *rms_weight,
660 float eps,
661 const float *w_gate,
662 const float *w_up,
663 const float *w_down,
664 float *normed_buf, /* [embed_dim] caller-provided */
665 float *gate_buf, /* [intermediate_dim] caller-provided */
666 float *up_buf, /* [intermediate_dim] caller-provided */
667 int embed_dim,
668 int intermediate_dim,
669 float *hidden_out
670) {
671 /* Step 1: RMSNorm */
672 float rms_scale = compute_rms_scale_internal(hidden_in, embed_dim, eps);
673 for (int i = 0; i < embed_dim; i++) {
674 normed_buf[i] = hidden_in[i] * rms_weight[i] * rms_scale;
675 }
676
677 /* Step 2: Gate projection */
678 for (int j = 0; j < intermediate_dim; j++) {
679 float sum = 0.0f;
680 const float *wg_row = w_gate + j * embed_dim;
681 for (int k = 0; k < embed_dim; k++) {
682 sum += wg_row[k] * normed_buf[k];
683 }
684 gate_buf[j] = sum;
685 }
686
687 /* Step 3: Up projection */
688 for (int j = 0; j < intermediate_dim; j++) {
689 float sum = 0.0f;
690 const float *wu_row = w_up + j * embed_dim;
691 for (int k = 0; k < embed_dim; k++) {
692 sum += wu_row[k] * normed_buf[k];
693 }
694 up_buf[j] = sum;
695 }
696
697 /* Step 4: SwiGLU */
698 for (int j = 0; j < intermediate_dim; j++) {
699 gate_buf[j] = silu_scalar(gate_buf[j]) * up_buf[j];
700 }
701
702 /* Step 5: Down projection + Residual */
703 for (int j = 0; j < embed_dim; j++) {
704 float sum = 0.0f;
705 const float *wd_row = w_down + j * intermediate_dim;
706 for (int k = 0; k < intermediate_dim; k++) {
707 sum += wd_row[k] * gate_buf[k];
708 }
709 hidden_out[j] = sum + hidden_in[j];
710 }
711}
712
713
714/* ============================================================================
715 * Q4_K VERSION: Attention + Output + RMSNorm + MLP with quantized weights
716 *
717 * All MLP weights are Q4_K quantized.
718 * ============================================================================ */
719
721 /* Attention inputs */
722 const float *q, /* [num_heads * head_dim] */
723 const float *k_cache, /* [seq_len, num_kv_heads * head_dim] */
724 const float *v_cache, /* [seq_len, num_kv_heads * head_dim] */
725 int seq_len,
726 int num_heads,
727 int num_kv_heads,
728 int head_dim,
729 float attn_scale,
730
731 /* Output projection (Q4_K) */
732 const void *wo,
733
734 /* Residual */
735 const float *residual_1,
736
737 /* RMSNorm */
738 const float *rms_weight,
739 float eps,
740
741 /* MLP weights (Q4_K) */
742 const void *w_gate,
743 const void *w_up,
744 const void *w_down,
745
746 /* Dimensions */
747 int embed_dim,
748 int intermediate_dim,
749
750 /* Output */
751 float *hidden_out
752) {
753 const int heads_per_kv = num_heads / num_kv_heads;
754 const int q_dim = num_heads * head_dim;
755 const int kv_dim = num_kv_heads * head_dim;
756
757 /* Stack buffers */
758 float attn_out[4096];
759 float hidden_after_attn[4096];
760 float normed[4096];
761 float mlp_out[4096];
762
763 if (embed_dim > 4096) return;
764
765 /* ═══════════════════════════════════════════════════════════════════════
766 * STEP 1: Multi-Head Attention (same as FP32 version)
767 * ═══════════════════════════════════════════════════════════════════════ */
768
769 memset(attn_out, 0, q_dim * sizeof(float));
770
771 for (int h = 0; h < num_heads; h++) {
772 int kv_h = h / heads_per_kv;
773 const float *q_head = q + h * head_dim;
774 float *out_head = attn_out + h * head_dim;
775
776 float scores[8192];
777 if (seq_len > 8192) return;
778
779 for (int t = 0; t < seq_len; t++) {
780 const float *k_t = k_cache + t * kv_dim + kv_h * head_dim;
781 float score = 0.0f;
782 for (int d = 0; d < head_dim; d++) {
783 score += q_head[d] * k_t[d];
784 }
785 scores[t] = score * attn_scale;
786 }
787
788 softmax_inplace(scores, seq_len);
789
790 for (int t = 0; t < seq_len; t++) {
791 const float *v_t = v_cache + t * kv_dim + kv_h * head_dim;
792 float w = scores[t];
793 for (int d = 0; d < head_dim; d++) {
794 out_head[d] += w * v_t[d];
795 }
796 }
797 }
798
799 /* ═══════════════════════════════════════════════════════════════════════
800 * STEP 2: Output Projection (Q4_K) + Residual
801 * ═══════════════════════════════════════════════════════════════════════ */
802
803 extern void gemv_q4_k(float *y, const void *W, const float *x, int M, int K);
804
805 gemv_q4_k(hidden_after_attn, wo, attn_out, embed_dim, q_dim);
806
807 /* Add residual */
808 for (int i = 0; i < embed_dim; i++) {
809 hidden_after_attn[i] += residual_1[i];
810 }
811
812 /* ═══════════════════════════════════════════════════════════════════════
813 * STEP 3: RMSNorm (same as before)
814 * ═══════════════════════════════════════════════════════════════════════ */
815
816 float rms_scale = compute_rms_scale_internal(hidden_after_attn, embed_dim, eps);
817
818 for (int i = 0; i < embed_dim; i++) {
819 normed[i] = hidden_after_attn[i] * rms_weight[i] * rms_scale;
820 }
821
822 /* ═══════════════════════════════════════════════════════════════════════
823 * STEP 4-6: MLP with Q4_K weights (inline implementation)
824 *
825 * gate_out = normed @ W_gate
826 * up_out = normed @ W_up
827 * swiglu = silu(gate_out) * up_out
828 * mlp_out = swiglu @ W_down
829 * ═══════════════════════════════════════════════════════════════════════ */
830
831 float gate_out[16384];
832 float up_out[16384];
833
834 if (intermediate_dim > 16384) return;
835
836 /* Gate projection */
837 gemv_q4_k(gate_out, w_gate, normed, intermediate_dim, embed_dim);
838
839 /* Up projection */
840 gemv_q4_k(up_out, w_up, normed, intermediate_dim, embed_dim);
841
842 /* SwiGLU: silu(gate) * up */
843#ifdef __AVX2__
844 int i = 0;
845 for (; i + 7 < intermediate_dim; i += 8) {
846 __m256 vg = _mm256_loadu_ps(gate_out + i);
847 __m256 vu = _mm256_loadu_ps(up_out + i);
848 __m256 vsilu = silu_avx2(vg);
849 __m256 vswiglu = _mm256_mul_ps(vsilu, vu);
850 _mm256_storeu_ps(gate_out + i, vswiglu);
851 }
852 for (; i < intermediate_dim; i++) {
853 gate_out[i] = silu_scalar(gate_out[i]) * up_out[i];
854 }
855#else
856 for (int i = 0; i < intermediate_dim; i++) {
857 gate_out[i] = silu_scalar(gate_out[i]) * up_out[i];
858 }
859#endif
860
861 /* Down projection */
862 gemv_q4_k(mlp_out, w_down, gate_out, embed_dim, intermediate_dim);
863
864 /* Final residual add */
865 for (int i = 0; i < embed_dim; i++) {
866 hidden_out[i] = mlp_out[i] + hidden_after_attn[i];
867 }
868}
869
870/* ============================================================================
871 * COMPLETE LAYER FUSION: Attention → MLP → Next Layer's QKV
872 *
873 * This is the TRUE mega-fusion: from one layer's attention output all the
874 * way to the next layer's Q (ready for attention) + K,V written to cache.
875 *
876 * The hidden state NEVER touches DRAM between layers!
877 * ============================================================================ */
878
880 /* === CURRENT LAYER ATTENTION INPUTS === */
881 const float *q, /* [num_heads * head_dim] query for this layer */
882 const float *k_cache, /* [seq_len, num_kv_heads * head_dim] */
883 const float *v_cache, /* [seq_len, num_kv_heads * head_dim] */
884 int seq_len,
885 float attn_scale,
886
887 /* === CURRENT LAYER WEIGHTS (Q4_K) === */
888 const void *wo, /* Output projection */
889 const float *rms_weight_mlp, /* RMSNorm before MLP */
890 const void *w_gate, /* MLP gate */
891 const void *w_up, /* MLP up */
892 const void *w_down, /* MLP down */
893
894 /* === NEXT LAYER WEIGHTS (Q4_K) === */
895 const float *rms_weight_attn, /* RMSNorm before next attention */
896 const void *wq_next, /* Next layer Q projection */
897 const void *wk_next, /* Next layer K projection */
898 const void *wv_next, /* Next layer V projection */
899
900 /* === RESIDUAL INPUT === */
901 const float *residual_in, /* [embed_dim] input to this layer */
902
903 /* === DIMENSIONS === */
904 int embed_dim,
905 int intermediate_dim,
906 int num_heads,
907 int num_kv_heads,
908 int head_dim,
909 float eps,
910
911 /* === OUTPUTS === */
912 float *q_next, /* [num_heads * head_dim] Q for next layer */
913 float *k_next, /* [num_kv_heads * head_dim] K to write to cache */
914 float *v_next, /* [num_kv_heads * head_dim] V to write to cache */
915 float *hidden_out /* [embed_dim] hidden state (for final layer) */
916) {
917 extern void gemv_q4_k(float *y, const void *W, const float *x, int M, int K);
918
919 const int heads_per_kv = num_heads / num_kv_heads;
920 const int q_dim = num_heads * head_dim;
921 const int kv_dim = num_kv_heads * head_dim;
922
923 /* All intermediate buffers on stack - stay in L1/L2
924 * hidden_out is the final output buffer - we write to it directly! */
925 float attn_out[4096];
926 float hidden_after_attn[4096];
927 float normed_mlp[4096];
928 float gate_out[16384];
929 float up_out[16384];
930 /* NOTE: No hidden_after_mlp buffer - we output directly to hidden_out */
931 float normed_attn[4096];
932
933 if (embed_dim > 4096 || intermediate_dim > 16384) return;
934
935 /* ═══════════════════════════════════════════════════════════════════════
936 * STEP 1: Multi-Head Attention
937 * ═══════════════════════════════════════════════════════════════════════ */
938
939 memset(attn_out, 0, q_dim * sizeof(float));
940
941 for (int h = 0; h < num_heads; h++) {
942 int kv_h = h / heads_per_kv;
943 const float *q_head = q + h * head_dim;
944 float *out_head = attn_out + h * head_dim;
945
946 float scores[8192];
947 if (seq_len > 8192) return;
948
949 for (int t = 0; t < seq_len; t++) {
950 const float *k_t = k_cache + t * kv_dim + kv_h * head_dim;
951 float score = 0.0f;
952 for (int d = 0; d < head_dim; d++) {
953 score += q_head[d] * k_t[d];
954 }
955 scores[t] = score * attn_scale;
956 }
957
958 /* Softmax */
959 float max_score = scores[0];
960 for (int t = 1; t < seq_len; t++) {
961 if (scores[t] > max_score) max_score = scores[t];
962 }
963 float sum_exp = 0.0f;
964 for (int t = 0; t < seq_len; t++) {
965 scores[t] = expf(scores[t] - max_score);
966 sum_exp += scores[t];
967 }
968 float inv_sum = 1.0f / sum_exp;
969 for (int t = 0; t < seq_len; t++) {
970 scores[t] *= inv_sum;
971 }
972
973 /* Weighted sum of V */
974 for (int t = 0; t < seq_len; t++) {
975 const float *v_t = v_cache + t * kv_dim + kv_h * head_dim;
976 float w = scores[t];
977 for (int d = 0; d < head_dim; d++) {
978 out_head[d] += w * v_t[d];
979 }
980 }
981 }
982
983 /* ═══════════════════════════════════════════════════════════════════════
984 * STEP 2: Output Projection (Q4_K) + Residual
985 * ═══════════════════════════════════════════════════════════════════════ */
986
987 gemv_q4_k(hidden_after_attn, wo, attn_out, embed_dim, q_dim);
988
989 for (int i = 0; i < embed_dim; i++) {
990 hidden_after_attn[i] += residual_in[i];
991 }
992
993 /* ═══════════════════════════════════════════════════════════════════════
994 * STEP 3: RMSNorm (for MLP)
995 * ═══════════════════════════════════════════════════════════════════════ */
996
997 float sum_sq = 0.0f;
998 for (int i = 0; i < embed_dim; i++) {
999 sum_sq += hidden_after_attn[i] * hidden_after_attn[i];
1000 }
1001 float rms_scale = 1.0f / sqrtf(sum_sq / embed_dim + eps);
1002
1003 for (int i = 0; i < embed_dim; i++) {
1004 normed_mlp[i] = hidden_after_attn[i] * rms_weight_mlp[i] * rms_scale;
1005 }
1006
1007 /* ═══════════════════════════════════════════════════════════════════════
1008 * STEP 4-6: MLP (gate + up + SwiGLU + down)
1009 * ═══════════════════════════════════════════════════════════════════════ */
1010
1011 gemv_q4_k(gate_out, w_gate, normed_mlp, intermediate_dim, embed_dim);
1012 gemv_q4_k(up_out, w_up, normed_mlp, intermediate_dim, embed_dim);
1013
1014 /* SwiGLU: silu(gate) * up */
1015 for (int i = 0; i < intermediate_dim; i++) {
1016 float g = gate_out[i];
1017 float silu_g = g / (1.0f + expf(-g));
1018 gate_out[i] = silu_g * up_out[i];
1019 }
1020
1021 /* Down projection - output DIRECTLY to hidden_out (no intermediate buffer!) */
1022 gemv_q4_k(hidden_out, w_down, gate_out, embed_dim, intermediate_dim);
1023
1024 /* MLP residual - hidden_out now contains the final hidden state */
1025 for (int i = 0; i < embed_dim; i++) {
1026 hidden_out[i] += hidden_after_attn[i];
1027 }
1028
1029 /* ═══════════════════════════════════════════════════════════════════════
1030 * STEP 7: RMSNorm (for NEXT layer's attention)
1031 * Read from hidden_out (already contains final hidden state)
1032 * ═══════════════════════════════════════════════════════════════════════ */
1033
1034 sum_sq = 0.0f;
1035 for (int i = 0; i < embed_dim; i++) {
1036 sum_sq += hidden_out[i] * hidden_out[i];
1037 }
1038 rms_scale = 1.0f / sqrtf(sum_sq / embed_dim + eps);
1039
1040 for (int i = 0; i < embed_dim; i++) {
1041 normed_attn[i] = hidden_out[i] * rms_weight_attn[i] * rms_scale;
1042 }
1043
1044 /* ═══════════════════════════════════════════════════════════════════════
1045 * STEP 8: NEXT LAYER's Q, K, V Projections
1046 *
1047 * Q goes to caller (for attention computation)
1048 * K, V go to KV cache (DRAM write - this is intentional!)
1049 * ═══════════════════════════════════════════════════════════════════════ */
1050
1051 gemv_q4_k(q_next, wq_next, normed_attn, q_dim, embed_dim);
1052 gemv_q4_k(k_next, wk_next, normed_attn, kv_dim, embed_dim);
1053 gemv_q4_k(v_next, wv_next, normed_attn, kv_dim, embed_dim);
1054
1055 /* hidden_out already contains the final hidden state - no memcpy needed! */
1056}
1057
1058/* ============================================================================
1059 * NON-FUSED REFERENCE: For benchmarking comparison
1060 * ============================================================================ */
1061
1063 const float *q, const float *k_cache, const float *v_cache,
1064 int seq_len, int num_heads, int num_kv_heads, int head_dim,
1065 float attn_scale,
1066 const float *wo, const float *residual_1,
1067 const float *rms_weight, float eps,
1068 const float *w_gate, const float *w_up, const float *w_down,
1069 int embed_dim, int intermediate_dim,
1070 /* Intermediate buffers - DRAM traffic! */
1071 float *attn_out_buf,
1072 float *hidden_after_attn_buf,
1073 float *normed_buf,
1074 float *gate_buf,
1075 float *up_buf,
1076 float *mlp_out_buf,
1077 /* Output */
1078 float *hidden_out
1079) {
1080 /* This version writes all intermediates to the provided buffers,
1081 * simulating non-fused execution with DRAM traffic */
1082
1083 const int heads_per_kv = num_heads / num_kv_heads;
1084 const int q_dim = num_heads * head_dim;
1085 const int kv_dim = num_kv_heads * head_dim;
1086
1087 /* Step 1: Attention */
1088 memset(attn_out_buf, 0, q_dim * sizeof(float));
1089
1090 /* Stack-allocated scores buffer (no malloc!) */
1091 float scores[8192]; /* Max seq_len supported */
1092 if (seq_len > 8192) return;
1093
1094 for (int h = 0; h < num_heads; h++) {
1095 int kv_h = h / heads_per_kv;
1096 const float *q_head = q + h * head_dim;
1097 float *out_head = attn_out_buf + h * head_dim;
1098
1099 for (int t = 0; t < seq_len; t++) {
1100 const float *k_t = k_cache + t * kv_dim + kv_h * head_dim;
1101 float score = 0.0f;
1102 for (int d = 0; d < head_dim; d++) {
1103 score += q_head[d] * k_t[d];
1104 }
1105 scores[t] = score * attn_scale;
1106 }
1107
1108 softmax_inplace(scores, seq_len);
1109
1110 for (int t = 0; t < seq_len; t++) {
1111 const float *v_t = v_cache + t * kv_dim + kv_h * head_dim;
1112 float w = scores[t];
1113 for (int d = 0; d < head_dim; d++) {
1114 out_head[d] += w * v_t[d];
1115 }
1116 }
1117 }
1118
1119 /* Step 2: Output projection + residual -> DRAM write */
1120 for (int i = 0; i < embed_dim; i++) {
1121 float sum = 0.0f;
1122 const float *wo_row = wo + i * q_dim;
1123 for (int j = 0; j < q_dim; j++) {
1124 sum += wo_row[j] * attn_out_buf[j];
1125 }
1126 hidden_after_attn_buf[i] = sum + residual_1[i];
1127 }
1128
1129 /* Step 3: RMSNorm -> DRAM write */
1130 float rms_scale = compute_rms_scale_internal(hidden_after_attn_buf, embed_dim, eps);
1131 for (int i = 0; i < embed_dim; i++) {
1132 normed_buf[i] = hidden_after_attn_buf[i] * rms_weight[i] * rms_scale;
1133 }
1134
1135 /* Step 4: Gate projection -> DRAM write */
1136 for (int i = 0; i < intermediate_dim; i++) {
1137 float sum = 0.0f;
1138 const float *wg_row = w_gate + i * embed_dim;
1139 for (int j = 0; j < embed_dim; j++) {
1140 sum += wg_row[j] * normed_buf[j];
1141 }
1142 gate_buf[i] = sum;
1143 }
1144
1145 /* Step 5: Up projection -> DRAM write */
1146 for (int i = 0; i < intermediate_dim; i++) {
1147 float sum = 0.0f;
1148 const float *wu_row = w_up + i * embed_dim;
1149 for (int j = 0; j < embed_dim; j++) {
1150 sum += wu_row[j] * normed_buf[j];
1151 }
1152 up_buf[i] = sum;
1153 }
1154
1155 /* Step 6: SwiGLU (in-place in gate_buf) */
1156 for (int i = 0; i < intermediate_dim; i++) {
1157 gate_buf[i] = silu_scalar(gate_buf[i]) * up_buf[i];
1158 }
1159
1160 /* Step 7: Down projection -> DRAM write */
1161 for (int i = 0; i < embed_dim; i++) {
1162 float sum = 0.0f;
1163 const float *wd_row = w_down + i * intermediate_dim;
1164 for (int j = 0; j < intermediate_dim; j++) {
1165 sum += wd_row[j] * gate_buf[j];
1166 }
1167 mlp_out_buf[i] = sum;
1168 }
1169
1170 /* Step 8: Final residual */
1171 for (int i = 0; i < embed_dim; i++) {
1172 hidden_out[i] = mlp_out_buf[i] + hidden_after_attn_buf[i];
1173 }
1174}
void attention_mlp_fused_q4k(const float *q, const float *k_cache, const float *v_cache, int seq_len, int num_heads, int num_kv_heads, int head_dim, float attn_scale, const void *wo, const float *residual_1, const float *rms_weight, float eps, const void *w_gate, const void *w_up, const void *w_down, int embed_dim, int intermediate_dim, float *hidden_out)
void mlp_fused_fp32_v2(const float *hidden_in, const float *rms_weight, float eps, const float *w_gate, const float *w_up, const float *w_down, int embed_dim, int intermediate_dim, float *hidden_out)
static float silu_scalar(float x)
void mlp_separate_fp32(const float *hidden_in, const float *rms_weight, float eps, const float *w_gate, const float *w_up, const float *w_down, float *normed_buf, float *gate_buf, float *up_buf, int embed_dim, int intermediate_dim, float *hidden_out)
static float compute_rms_scale_internal(const float *x, int n, float eps)
void mlp_fused_fp32_v3(const float *hidden_in, const float *rms_weight, float eps, const float *w_gate, const float *w_up, const float *w_down, int embed_dim, int intermediate_dim, float *hidden_out)
static void softmax_inplace(float *x, int n)
void layer_fused_attn_mlp_qkv_q4k(const float *q, const float *k_cache, const float *v_cache, int seq_len, float attn_scale, const void *wo, const float *rms_weight_mlp, const void *w_gate, const void *w_up, const void *w_down, const float *rms_weight_attn, const void *wq_next, const void *wk_next, const void *wv_next, const float *residual_in, int embed_dim, int intermediate_dim, int num_heads, int num_kv_heads, int head_dim, float eps, float *q_next, float *k_next, float *v_next, float *hidden_out)
void attention_mlp_separate_fp32(const float *q, const float *k_cache, const float *v_cache, int seq_len, int num_heads, int num_kv_heads, int head_dim, float attn_scale, const float *wo, const float *residual_1, const float *rms_weight, float eps, const float *w_gate, const float *w_up, const float *w_down, int embed_dim, int intermediate_dim, float *attn_out_buf, float *hidden_after_attn_buf, float *normed_buf, float *gate_buf, float *up_buf, float *mlp_out_buf, float *hidden_out)
void attention_mlp_fused_fp32(const float *q, const float *k_cache, const float *v_cache, int seq_len, int num_heads, int num_kv_heads, int head_dim, float attn_scale, const float *wo, const float *residual_1, const float *rms_weight, float eps, const float *w_gate, const float *w_up, const float *w_down, int embed_dim, int intermediate_dim, float *hidden_out)
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.
int32_t float * score
Definition tokenizer.h:328