-
Notifications
You must be signed in to change notification settings - Fork 32
Expand file tree
/
Copy pathcpu_ops.cpp
More file actions
3272 lines (3137 loc) · 158 KB
/
Copy pathcpu_ops.cpp
File metadata and controls
3272 lines (3137 loc) · 158 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
// vllm.cpp original (vt runtime, inventory deviation §9.1); no upstream mirror.
// EXCEPT the parallel dispatch (QUANT-GGUF-CPU-THREADPOOL): the GEMM chunk
// policy and the row/batch chunking are ported 1:1 from llama.cpp (local fork)
// ggml/src/ggml-cpu/ggml-cpu.c:1155-1443 and ggml-cpu/ops.cpp:9070-9126 @
// 237ad9b96 — see cpu_threadpool.h and the per-kernel anchors below. Every
// kernel keeps its exact per-element math and per-output sequential reduction
// order; parallelism partitions OUTPUT elements only, so results are
// bit-identical to single-thread by construction (spec § Dispatch behavior).
#include "vt/ops.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <limits>
#include <string>
#include <vector>
#include "cpu_matmul_elem.h"
#include "cpu_threadpool.h"
#include "vt/unaligned.h"
namespace vt::cpu {
namespace {
// Row/batch-chunked dispatch through the process pool (or the test-swapped
// pool). body(r0, r1) produces output rows [r0, r1) exactly once each.
inline void ForRows(int64_t nr, const std::function<void(int64_t, int64_t)>& body) {
ParallelForRows(CurrentThreadpool(), nr, body);
}
float LoadF32(const Tensor& t, int64_t elem_offset) {
const auto* address = static_cast<const uint8_t*>(t.data) +
elem_offset * SizeOf(t.dtype);
switch (t.dtype) {
case DType::kF32: return LoadUnaligned<float>(address);
case DType::kF16: return F16ToF32(LoadUnaligned<uint16_t>(address));
case DType::kBF16: return BF16ToF32(LoadUnaligned<uint16_t>(address));
default: VT_CHECK(false, "LoadF32: unsupported dtype"); return 0.0f;
}
}
// Mirror of LoadF32 for outputs: reduced-width formats round to storage dtype.
void StoreF32(const Tensor& t, int64_t elem_offset, float v) {
switch (t.dtype) {
case DType::kF32: t.Ptr<float>()[elem_offset] = v; break;
case DType::kF16: t.Ptr<uint16_t>()[elem_offset] = F32ToF16(v); break;
case DType::kBF16: t.Ptr<uint16_t>()[elem_offset] = F32ToBF16(v); break;
default: VT_CHECK(false, "StoreF32: unsupported dtype");
}
}
// GEMM chunk worker — 16x16 block tiling inside a chunk, ported from
// ggml_compute_forward_mul_mat_one_chunk (ggml-cpu.c:1155-1243; empty-chunk
// yield :1181-1184, blck_0/blck_1 = 16 :1192-1194). ggml's vec_dot per output
// element is our per-element K loop: byte-identical accumulation (sequential
// over K, f32, -ffp-contract=off pinned) to the pre-threadpool kernels.
// ir0 indexes output COLUMNS j (ggml nr0 = src0/weight rows = N), ir1 indexes
// output ROWS i (ggml nr1 = src1 rows = M). kBT selects the [N,K] row-major
// weight orientation (MatmulBT) vs [K,N] (Matmul).
template <bool kBT>
void MatmulOneChunkRef(Tensor& out, const Tensor& a, const Tensor& b, int64_t k, int64_t n,
int64_t ir0_start, int64_t ir0_end, int64_t ir1_start, int64_t ir1_end) {
// MLA campaign W6: the activation may be ROW-STRIDED (a column slice of a
// wider buffer — see vt::MatmulBT). For a contiguous activation `a_rs == k`,
// so the offsets are integer-identical to the pre-W6 `i * k + p` form and
// every existing model is bit-identical by construction.
const int64_t a_rs = a.stride[0];
// threads with no work simply yield
if (ir0_start >= ir0_end || ir1_start >= ir1_end) {
return;
}
// block-tiling attempt
const int64_t blck_0 = 16;
const int64_t blck_1 = 16;
for (int64_t iir1 = ir1_start; iir1 < ir1_end; iir1 += blck_1) {
for (int64_t iir0 = ir0_start; iir0 < ir0_end; iir0 += blck_0) {
for (int64_t i = iir1; i < iir1 + blck_1 && i < ir1_end; ++i) {
for (int64_t j = iir0; j < iir0 + blck_0 && j < ir0_end; ++j) {
float acc = 0.0f;
for (int64_t p = 0; p < k; ++p) {
acc += LoadF32(a, i * a_rs + p) * LoadF32(b, kBT ? j * k + p : p * n + j);
}
StoreF32(out, i * n + j, acc);
}
}
}
}
}
// Byte offset of element `off` of an ELEMENTWISE tensor.
inline const void* ElemPtr(const Tensor& t, int64_t off) {
return static_cast<const uint8_t*>(t.data) + static_cast<size_t>(off) * SizeOf(t.dtype);
}
// Specialized/vectorized elementwise GEMM chunk (row `CPU-ELEM-GEMM`,
// .agents/specs/cpu-elementwise-gemm.md). Structurally identical to
// MatmulOneChunkRef — same 16x16 tile from ggml_compute_forward_mul_mat_one_chunk
// (ggml-cpu.c:1155-1243), same output set, same strictly sequential f32
// accumulation per output element — with two defects removed:
// 1. the per-ELEMENT `LoadF32` dtype switch is hoisted out of the K loop
// (the activation row is widened to f32 ONCE per 16-row tile; the weight
// dtype is resolved once per chunk into a typed micro-kernel), and
// 2. the single serial accumulator becomes 16 independent ones, vectorized
// ACROSS OUTPUT COLUMNS so no reduction is reassociated.
// Both are bit-exact by construction; tests/vt/test_ops_matmul_elem.cpp
// asserts equality against MatmulOneChunkRef byte-for-byte.
template <bool kBT>
void MatmulOneChunk(Tensor& out, const Tensor& a, const Tensor& b, int64_t k, int64_t n,
int64_t ir0_start, int64_t ir0_end, int64_t ir1_start, int64_t ir1_end) {
if (ir0_start >= ir0_end || ir1_start >= ir1_end) {
return;
}
ElemKind bk;
ElemKind ak;
if (!ElemKindOf(b.dtype, &bk) || !ElemKindOf(a.dtype, &ak) || k <= 0 || ElemGemmUseRef()) {
MatmulOneChunkRef<kBT>(out, a, b, k, n, ir0_start, ir0_end, ir1_start, ir1_end);
return;
}
const ElemGemmTierTable& tier = ElemGemmTier();
const int bi = static_cast<int>(bk);
const int64_t a_rs = a.stride[0];
const int64_t blck_0 = kElemLanes;
const int64_t blck_1 = 16;
// Widened activation rows for the current 16-row tile, reused across every
// column block of the chunk. Thread-local so the buffer is allocated once
// per worker for the process lifetime (no per-chunk allocation).
static thread_local std::vector<float> af;
for (int64_t iir1 = ir1_start; iir1 < ir1_end; iir1 += blck_1) {
const int64_t i_hi = std::min(iir1 + blck_1, ir1_end);
const int64_t nrows = i_hi - iir1;
af.resize(static_cast<size_t>(nrows * k));
for (int64_t i = iir1; i < i_hi; ++i) {
WidenRowToF32(a.dtype, ElemPtr(a, i * a_rs), k, af.data() + (i - iir1) * k);
}
// M blocking applies to BOTH orientations. It used to be gated on kBT, so
// the [K,N] path always ran mr=1 and re-read the whole weight tile once per
// activation row (a 131-row activation read it 131 times). Each family is
// guarded on its own function pointer because a tier may provide one and
// not the other (the portable tier has no btm, having no transpose to
// amortize, but its nkm still amortizes the weight load).
const ElemNkMFn nkm_fn = tier.nkm[bi];
const int mr = (kBT ? (tier.btm[bi] != nullptr) : (nkm_fn != nullptr)) ? tier.mr : 1;
for (int64_t iir0 = ir0_start; iir0 < ir0_end; iir0 += blck_0) {
const int64_t j_hi = std::min(iir0 + blck_0, ir0_end);
int64_t i = iir1;
// M-blocked fast path: `mr` activation rows share one weight load +
// transpose per column block (see ElemBtMFn).
if (j_hi - iir0 == blck_0 && mr > 1) {
float accm[kElemLanes * 8];
for (; i + mr <= i_hi; i += mr) {
if (kBT) {
tier.btm[bi](af.data() + (i - iir1) * k, k, ElemPtr(b, iir0 * k), k, accm);
} else {
nkm_fn(af.data() + (i - iir1) * k, k, ElemPtr(b, iir0), k, n, accm);
}
for (int r = 0; r < mr; ++r) {
for (int64_t j = iir0; j < j_hi; ++j) {
StoreF32(out, (i + r) * n + j, accm[r * kElemLanes + (j - iir0)]);
}
}
}
}
for (; i < i_hi; ++i) {
const float* arow = af.data() + (i - iir1) * k;
float acc[kElemLanes];
if (j_hi - iir0 == blck_0) {
if (kBT) {
tier.bt[bi](arow, ElemPtr(b, iir0 * k), k, acc);
} else {
tier.nk[bi](arow, ElemPtr(b, iir0), k, n, acc);
}
} else {
// Ragged column tail: the scalar form, identical accumulation order.
for (int64_t j = iir0; j < j_hi; ++j) {
float s = 0.0f;
for (int64_t p = 0; p < k; ++p) {
s += arow[p] * LoadF32(b, kBT ? j * k + p : p * n + j);
}
acc[j - iir0] = s;
}
}
for (int64_t j = iir0; j < j_hi; ++j) {
StoreF32(out, i * n + j, acc[j - iir0]);
}
}
}
}
}
// GEMM chunking policy + atomic work stealing, ported from
// ggml_compute_forward_mul_mat (ggml-cpu.c:1245-1443): thread 0 seeds the
// steal cursor at nth and a barrier publishes it (:1350-1355); chunk_size 16,
// 64 for vector shapes (:1388-1393); nchunk0 x nchunk1 grid (:1398-1399);
// re-chunk per-thread when the grid is < nth*4 or NUMA (:1404-1408, IsNuma()
// stubbed false); each thread starts at chunk ith then steals via the atomic
// cursor (:1415-1442). num_rows_per_vec_dot is 1 for our scalar dot (no mmla).
// VT_CPU_MATMUL_STEAL: same-binary A/B for the decode-shape chunk policy above.
// Read once; default OFF keeps the ggml-mirrored behaviour byte-for-byte.
bool MatmulStealEnabled() {
static const bool on = [] {
const char* e = std::getenv("VT_CPU_MATMUL_STEAL");
return e != nullptr && e[0] == '1';
}();
return on;
}
template <bool kBT>
void MatmulChunked(Tensor& out, const Tensor& a, const Tensor& b) {
const int64_t m = a.shape[0], k = a.shape[1];
const int64_t n = kBT ? b.shape[0] : b.shape[1];
// ggml nr0 = ne0 (dst dim0 = weight rows) -> our N; nr1 = ne1*ne2*ne3
// (src1 rows) -> our M.
const int64_t nr0 = n;
const int64_t nr1 = m;
Threadpool& tp = CurrentThreadpool();
tp.Run([&](int ith, int nth) {
if (ith == 0) {
// Every thread starts at ith, so the first unprocessed chunk is nth.
tp.ChunkSet(nth);
}
tp.Barrier();
// Now select a reasonable chunk size.
int chunk_size = 16;
// We need to step up the size if it's small
if (nr0 == 1 || nr1 == 1) {
chunk_size = 64;
}
// distribute the work across the inner or outer loop based on which one is larger
int64_t nchunk0 = (nr0 + chunk_size - 1) / chunk_size;
int64_t nchunk1 = (nr1 + chunk_size - 1) / chunk_size;
// If the chunking is poor for the number of threads on this setup, scrap
// the whole plan. Re-chunk it by thread.
//
// VT_CPU_MATMUL_STEAL=1 (default OFF) SKIPS this collapse. Rationale, and
// why it is an opt-in A/B rather than a new default: the collapse is a
// faithful port of ggml (ggml-cpu.c:1404-1408), so changing it by default
// would be an unmirrored deviation. But it has a cost this project has not
// measured. At decode shapes it rewrites the grid to exactly `nth` chunks
// and the loop below then breaks after one chunk each, so a 20-thread
// decode GEMV becomes 20 EQUAL STATIC chunks gated by the slowest core,
// with the self-balancing steal cursor switched off. On a heterogeneous
// core complex (GB10 mixes core classes) that is a straggler trap.
// Skipping the collapse keeps the fine-grained grid and lets stealing
// balance it. Byte-identity is unaffected either way: every output's
// reduction is local and sequential over K (see MatmulOneChunk), so which
// thread computes which output changes nothing.
if ((nchunk0 * nchunk1 < nth * 4 && !MatmulStealEnabled()) || IsNuma()) {
nchunk0 = nr0 > nr1 ? nth : 1; // parallelize by weight rows (N)
nchunk1 = nr0 > nr1 ? 1 : nth; // parallelize by src1 rows (M)
}
// The number of elements in each chunk
const int64_t dr0 = (nr0 + nchunk0 - 1) / nchunk0;
const int64_t dr1 = (nr1 + nchunk1 - 1) / nchunk1;
// The first chunk comes from our thread_id, the rest will get auto-assigned.
int64_t current_chunk = ith;
while (current_chunk < nchunk0 * nchunk1) {
const int64_t ith0 = current_chunk % nchunk0;
const int64_t ith1 = current_chunk / nchunk0;
const int64_t ir0_start = dr0 * ith0;
const int64_t ir0_end = std::min(ir0_start + dr0, nr0);
const int64_t ir1_start = dr1 * ith1;
const int64_t ir1_end = std::min(ir1_start + dr1, nr1);
MatmulOneChunk<kBT>(out, a, b, k, n, ir0_start, ir0_end, ir1_start, ir1_end);
// Same switch: the early break is what makes the collapsed grid a pure
// static partition. With stealing enabled we keep pulling chunks.
if (nth >= nchunk0 * nchunk1 && !MatmulStealEnabled()) {
break;
}
current_chunk = tp.ChunkAdd(1);
}
});
}
void MatmulKernel(Queue&, Tensor& out, const Tensor& a, const Tensor& b) {
MatmulChunked<false>(out, a, b);
}
// b is the torch Linear weight [N,K] row-major (see vt::MatmulBT); identical
// accumulation order to MatmulKernel (sequential over K), so on CPU the two
// orientations are bit-identical for the same logical weight.
void MatmulBTKernel(Queue&, Tensor& out, const Tensor& a, const Tensor& b) {
// KERNEL-GEMM-CPU-TILED lever 2: a loader-repacked weight keeps its [N,K]
// SHAPE (so this op's contract is unchanged for callers) but its BYTES are
// [K,N]. Present it as the [K,N] tensor it literally is and take the
// transpose-free path. Byte-identical by the same argument the comment above
// MatmulBTKernel already states: both orientations accumulate each output
// over K in strict increasing order.
if (b.elem_kn_repacked) {
Tensor bkn = b;
bkn.shape[0] = b.shape[1]; // K
bkn.shape[1] = b.shape[0]; // N
bkn.stride[0] = b.shape[0]; // one [K,N] row is N elements
bkn.stride[1] = 1;
MatmulChunked<false>(out, a, bkn);
return;
}
MatmulChunked<true>(out, a, b);
}
// vt::BatchedMatmul (`torch.bmm`) CPU reference — out[G,M,N] = a[G,M,K] @
// b[G,K,N]. Sequential f32 accumulation over K, exactly like MatmulOneChunk's
// per-element dot, so the reference and the cuBLASLt CUDA path share the same
// numeric contract (f32 accumulate, round on store). Every operand is addressed
// through its STRIDES: the MLA absorption call sites (mla_attention.py:789,
// :1034) pass transposed views whose batch axis is not the outermost storage
// axis. Parallelized over the flattened (batch, row) output space, which leaves
// each output element's K reduction on one thread — bit-identical to a serial
// run and run-to-run reproducible.
void BatchedMatmulKernel(Queue&, Tensor& out, const Tensor& a, const Tensor& b) {
const int64_t g = out.shape[0], m = out.shape[1], n = out.shape[2];
const int64_t k = a.shape[2];
const int64_t rows = g * m;
if (rows == 0 || n == 0) return;
ForRows(rows, [&](int64_t r0, int64_t r1) {
for (int64_t r = r0; r < r1; ++r) {
const int64_t bi = r / m, i = r % m;
const int64_t a_row = bi * a.stride[0] + i * a.stride[1];
const int64_t b_base = bi * b.stride[0];
const int64_t o_row = bi * out.stride[0] + i * out.stride[1];
for (int64_t j = 0; j < n; ++j) {
float acc = 0.0f;
for (int64_t p = 0; p < k; ++p) {
acc += LoadF32(a, a_row + p) * LoadF32(b, b_base + p * b.stride[1] + j);
}
StoreF32(out, o_row + j, acc);
}
}
});
}
// vt::ConcatMlaNopeRope CPU reference — the scalar, width-generic form of
// upstream's `ConcatMLAQKernel` (csrc/libtorch_stable/concat_mla_q.cuh) and of
// the `_concat_k_nope_k_pe` slice assignments (mla_attention.py:2085-2090).
// Pure copy: no arithmetic, so it is exact for every dtype. `rope.shape[1] == 1`
// with more output heads is the broadcast form the prefill K concat needs.
void ConcatMlaNopeRopeKernel(Queue&, Tensor& out, const Tensor& nope, const Tensor& rope) {
const int64_t tokens = out.shape[0], heads = out.shape[1];
const int64_t dn = nope.shape[2], dr = rope.shape[2];
const bool rope_broadcast = rope.shape[1] == 1 && heads > 1;
ForRows(tokens, [&](int64_t t0, int64_t t1) {
for (int64_t t = t0; t < t1; ++t) {
for (int64_t h = 0; h < heads; ++h) {
const int64_t o = t * out.stride[0] + h * out.stride[1];
const int64_t n = t * nope.stride[0] + h * nope.stride[1];
const int64_t r =
t * rope.stride[0] + (rope_broadcast ? 0 : h) * rope.stride[1];
for (int64_t d = 0; d < dn; ++d) StoreF32(out, o + d, LoadF32(nope, n + d));
for (int64_t d = 0; d < dr; ++d) StoreF32(out, o + dn + d, LoadF32(rope, r + d));
}
}
});
}
void RmsNormKernel(Queue&, Tensor& out, const Tensor& x, const Tensor& w,
const RmsNormArgs& args, Tensor* residual) {
const int64_t t = x.shape[0], h = x.shape[1];
// Row-chunked over tokens (ops.cpp:9070-9126 pattern); each row's f32
// variance reduction stays sequential on one thread — bit-identical.
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
const int64_t rbase = i * h;
float sumsq = 0.0f;
for (int64_t j = 0; j < h; ++j) {
float v = LoadF32(x, i * h + j);
if (residual) {
v += LoadF32(*residual, rbase + j); // add in f32
StoreF32(*residual, rbase + j, v); // new residual stream (rounds to its dtype)
v = LoadF32(*residual, rbase + j); // re-read rounded value (bf16-faithful)
}
sumsq += v * v;
}
float inv = 1.0f / std::sqrt(sumsq / static_cast<float>(h) + args.eps);
for (int64_t j = 0; j < h; ++j) {
float v = residual ? LoadF32(*residual, rbase + j) : LoadF32(x, i * h + j);
float wj = LoadF32(w, j);
if (args.gemma) wj += 1.0f;
StoreF32(out, i * h + j, v * inv * wj);
}
}
});
}
void SiluAndMulKernel(Queue&, Tensor& out, const Tensor& x) {
const int64_t t = x.shape[0], d = x.shape[1] / 2;
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
for (int64_t j = 0; j < d; ++j) {
float gate = LoadF32(x, i * 2 * d + j);
float up = LoadF32(x, i * 2 * d + d + j);
float silu = gate / (1.0f + std::exp(-gate));
StoreF32(out, i * d + j, silu * up);
}
}
});
}
// Gemma GeGLU: out = gelu_tanh(gate) * up. gelu_tanh(g) = 0.5*g*(1 + tanh(
// sqrt(2/pi)*(g + 0.044715*g^3))) — the exact gelu_pytorch_tanh, computed in f32.
void GeluAndMulKernel(Queue&, Tensor& out, const Tensor& x) {
const int64_t t = x.shape[0], d = x.shape[1] / 2;
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
for (int64_t j = 0; j < d; ++j) {
float g = LoadF32(x, i * 2 * d + j);
float up = LoadF32(x, i * 2 * d + d + j);
float inner = 0.7978845608028654f * (g + 0.044715f * g * g * g);
float gelu = 0.5f * g * (1.0f + std::tanh(inner));
StoreF32(out, i * d + j, gelu * up);
}
}
});
}
// out[i] = x[i] * scalar (f32 compute, out-dtype store).
void MulScalarKernel(Queue&, Tensor& out, const Tensor& x, double scalar) {
const int64_t n = x.Numel();
const float s = static_cast<float>(scalar);
ForRows(n, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) StoreF32(out, i, LoadF32(x, i) * s);
});
}
// out[i] = cap * tanh(x[i] / cap) (f32 compute, out-dtype store). The Gemma-2
// final logit soft-cap (gemma2.py:344-345).
// Mirrors torch: logits.div_(cap).tanh_().mul_(cap) — division, not reciprocal.
void SoftCapKernel(Queue&, Tensor& out, const Tensor& x, double cap) {
const int64_t n = x.Numel();
const float c = static_cast<float>(cap);
ForRows(n, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) StoreF32(out, i, c * std::tanh(LoadF32(x, i) / c));
});
}
void MoeSiluMulKernel(Queue&, Tensor& out, const Tensor& gate, const Tensor& up) {
const int64_t n = out.Numel();
// Elementwise: partition the flat output range.
ForRows(n, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
const float g = LoadF32(gate, i);
const float silu = g / (1.0f + std::exp(-g));
StoreF32(out, i, silu * LoadF32(up, i));
}
});
}
// The NON-GATED MoE activation: out[i] = relu(x[i])^2, the whole epilogue of a
// NemotronH expert (nemotron_h.py:227 -> MoEActivation.RELU2_NO_MUL). Mirrors
// vLLM's relu_squared_kernel (csrc/libtorch_stable/activation_kernels.cu:673-678)
// EXACTLY in dtype order: widen to f32, clamp at zero in f32, square in f32, and
// round ONCE on the store. LoadF32/StoreF32 are that widen/round pair, so a bf16
// input with an f32 output keeps the full f32 square (no intermediate narrowing).
void MoeRelu2Kernel(Queue&, Tensor& out, const Tensor& x) {
const int64_t n = out.Numel();
ForRows(n, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
const float f = LoadF32(x, i);
const float v = f > 0.0f ? f : 0.0f;
StoreF32(out, i, v * v);
}
});
}
// --- TRUE W4A4 (fp4xfp4) helpers + kernels (notes §7). Self-contained fp8/fp4
// codec (vt does not depend on vllm), bit-matching vllm::F8E4M3ToF32 /
// F32ToF8E4M3 / CastToFp4 / kE2M1Lut so the op equals vllm::RunNvfp4Emulation.
constexpr float kFp4Max = 6.0F; // E2M1 max magnitude
constexpr float kFp8Max = 448.0F; // fp8-e4m3fn max finite
constexpr float kE2M1[8] = {0.0F, 0.5F, 1.0F, 1.5F, 2.0F, 3.0F, 4.0F, 6.0F};
inline float ClampF(float x, float lo, float hi) { return x < lo ? lo : (x > hi ? hi : x); }
inline float RecipF(float x) { return x == 0.0F ? 0.0F : 1.0F / x; }
// IEEE fp8-e4m3fn byte -> f32 (bit-matches vllm::F8E4M3ToF32).
float Fp8ToF32(uint8_t byte) {
const uint32_t sign = static_cast<uint32_t>(byte >> 7) & 0x1U;
const uint32_t exp = static_cast<uint32_t>(byte >> 3) & 0xFU;
const uint32_t mant = static_cast<uint32_t>(byte) & 0x7U;
const float sm = sign ? -1.0F : 1.0F;
if (exp == 0xFU && mant == 0x7U) return std::numeric_limits<float>::quiet_NaN();
if (exp == 0U) return sm * (static_cast<float>(mant) * (1.0F / 512.0F));
const float mantissa = 1.0F + static_cast<float>(mant) * (1.0F / 8.0F);
return sm * std::ldexp(mantissa, static_cast<int>(exp) - 7);
}
// f32 -> fp8-e4m3fn byte, round-to-nearest-even saturating (bit-matches
// vllm::F32ToF8E4M3).
uint8_t F32ToFp8(float f) {
if (std::isnan(f)) return 0x7FU;
const uint8_t sign = std::signbit(f) ? 0x80U : 0x00U;
const float a = std::fabs(f);
if (!std::isfinite(a) || a >= kFp8Max) return static_cast<uint8_t>(sign | 0x7EU);
if (a == 0.0F) return sign;
int e2 = 0;
const float frac = std::frexp(a, &e2);
int exp_field = (e2 - 1) + 7;
if (exp_field <= 0) {
const double qd = static_cast<double>(a) * 512.0;
const int qi = static_cast<int>(std::nearbyint(qd));
if (qi <= 0) return sign;
if (qi < 8) return static_cast<uint8_t>(sign | static_cast<uint8_t>(qi));
return static_cast<uint8_t>(sign | (1U << 3));
}
const double sig = static_cast<double>(frac) * 2.0;
int mi = static_cast<int>(std::nearbyint(sig * 8.0));
if (mi == 16) {
mi = 8;
exp_field += 1;
}
const int mant = mi - 8;
if (exp_field > 15 || (exp_field == 15 && mant >= 7)) {
return static_cast<uint8_t>(sign | 0x7EU);
}
return static_cast<uint8_t>(sign | (static_cast<uint8_t>(exp_field) << 3) |
static_cast<uint8_t>(mant));
}
// Fused fp8 RMSNorm -> static per-tensor quant (mirror vLLM Inductor
// fused_add_rms_norm_static_fp8_quant, rms_quant_fusion.py:124). Same reduction
// order as RmsNormKernel; the fp8 is taken from the SAME bf16-rounded normed value
// the split RmsNorm(bf16)+QuantFp8Static path quantizes (bf16-intermediate form),
// so the two are bit-identical. out_bf16 (optional) is the normed activation in
// bf16 (for a coexisting bf16 consumer, e.g. GDN in_proj_a/b). CUDA has the hot
// path; this CPU kernel keeps the op available on the host backend.
void RmsNormQuantFp8Kernel(Queue&, Tensor& out_fp8, Tensor* out_bf16, const Tensor& x,
const Tensor& w, const RmsNormArgs& args, Tensor* residual,
float input_scale) {
const int64_t t = x.shape[0], h = x.shape[1];
const float inv_scale = 1.0F / input_scale;
uint8_t* op = out_fp8.Ptr<uint8_t>();
uint16_t* bp = out_bf16 == nullptr ? nullptr : out_bf16->Ptr<uint16_t>();
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
const int64_t rbase = i * h;
float sumsq = 0.0F;
for (int64_t j = 0; j < h; ++j) {
float v = LoadF32(x, rbase + j);
if (residual) {
v += LoadF32(*residual, rbase + j); // add in f32
StoreF32(*residual, rbase + j, v); // new residual stream (rounds to its dtype)
v = LoadF32(*residual, rbase + j); // re-read rounded value (bf16-faithful)
}
sumsq += v * v;
}
const float inv = 1.0F / std::sqrt(sumsq / static_cast<float>(h) + args.eps);
for (int64_t j = 0; j < h; ++j) {
float v = residual ? LoadF32(*residual, rbase + j) : LoadF32(x, rbase + j);
float wj = LoadF32(w, j);
if (args.gemma) wj += 1.0F;
// bf16-intermediate: round the normed value to bf16 (as RmsNorm's bf16 store),
// then quant from that bf16 (as QuantFp8Static's bf16 load).
const uint16_t nb = F32ToBF16(v * inv * wj);
if (bp) bp[rbase + j] = nb;
op[rbase + j] = F32ToFp8(BF16ToF32(nb) * inv_scale);
}
}
});
}
// f32 -> E2M1 nibble (bit-matches vllm::CastToFp4 + Fp4ToNibble). Input pre-scaled.
uint8_t F32ToFp4Nibble(float x) {
const float a = std::fabs(x);
uint8_t idx = 7; // 6.0
if (a <= 0.25F) idx = 0;
else if (a < 0.75F) idx = 1;
else if (a <= 1.25F) idx = 2;
else if (a < 1.75F) idx = 3;
else if (a <= 2.5F) idx = 4;
else if (a < 3.5F) idx = 5;
else if (a <= 5.0F) idx = 6;
if (idx == 0) return 0;
return static_cast<uint8_t>((x < 0.0F ? 0x8U : 0x0U) | idx);
}
inline float Nibble(uint8_t nib) {
return kE2M1[nib & 0x7U] * ((nib & 0x8U) ? -1.0F : 1.0F);
}
// ScaledFp4Quant CPU kernel: x [M,K] float -> out_packed [M,K/2] i8 + out_scale
// [M,K/16] i8. Per-token, per-16-group; equals vllm::RefScaledFp4Quant.
void ScaledFp4QuantKernel(Queue&, Tensor& out_packed, Tensor& out_scale, const Tensor& x,
float input_global_scale_inv,
Fp4ScaleLayout scale_layout) {
const int64_t m = x.shape[0], k = x.shape[1];
constexpr int kBS = 16;
const int64_t groups = k / kBS;
const float gs_recip = RecipF(input_global_scale_inv);
auto* packed = out_packed.Ptr<uint8_t>();
auto* scale = out_scale.Ptr<uint8_t>();
const int64_t scale_cols = out_scale.shape[1];
if (scale_layout == Fp4ScaleLayout::kCutlassSwizzled) {
std::fill_n(scale, out_scale.Numel(), uint8_t{0});
}
ForRows(m, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
for (int64_t g = 0; g < groups; ++g) {
const int64_t base = g * kBS;
float vec_max = 0.0F;
for (int j = 0; j < kBS; ++j) vec_max = std::fmax(vec_max, std::fabs(LoadF32(x, i * k + base + j)));
float sc = ClampF(input_global_scale_inv * (vec_max * (1.0F / kFp4Max)), -kFp8Max, kFp8Max);
const uint8_t sc_f8 = F32ToFp8(sc);
if (scale_layout == Fp4ScaleLayout::kLinear) {
scale[i * groups + g] = sc_f8;
} else {
const int64_t m_tile = i / 128;
const int64_t outer_m = i % 32;
const int64_t inner_m = (i % 128) / 32;
const int64_t k_tile = g / 4;
const int64_t inner_k = g % 4;
const int64_t scale_offset =
((((m_tile * (scale_cols / 4) + k_tile) * 32 + outer_m) * 4 +
inner_m) *
4 +
inner_k);
scale[scale_offset] = sc_f8;
}
const float block_scale = Fp8ToF32(sc_f8) * gs_recip;
const float out_scale_v = RecipF(block_scale);
for (int j = 0; j < kBS; j += 2) {
const float lo = ClampF(LoadF32(x, i * k + base + j) * out_scale_v, -kFp4Max, kFp4Max);
const float hi = ClampF(LoadF32(x, i * k + base + j + 1) * out_scale_v, -kFp4Max, kFp4Max);
packed[(i * k + base + j) / 2] =
static_cast<uint8_t>(F32ToFp4Nibble(lo) | (F32ToFp4Nibble(hi) << 4));
}
}
}
});
}
// SiluMulFp4Quant CPU fallback = the exact composite (bf16 intermediate then
// quant) — which IS the definition of correctness for the CUDA fused kernel. The
// bf16 scratch reproduces the round-through-bf16 the CUDA kernel folds in.
void SiluMulFp4QuantKernel(Queue& q, Tensor& out_packed, Tensor& out_scale, const Tensor& gate,
const Tensor& up, float input_global_scale_inv,
Fp4ScaleLayout scale_layout) {
const int64_t m = gate.shape[0], i = gate.shape[1];
std::vector<uint16_t> tmp(static_cast<size_t>(m) * static_cast<size_t>(i));
Tensor act = Tensor::Contiguous(tmp.data(), DType::kBF16, gate.device, {m, i});
MoeSiluMulKernel(q, act, gate, up);
ScaledFp4QuantKernel(q, out_packed, out_scale, act, input_global_scale_inv,
scale_layout);
}
// CPU definition of vLLM's one-input silu_and_mul_nvfp4_quant custom op. Keep
// this visibly composite: it is the correctness oracle for the CUDA single-pass
// producer and preserves the BF16 store/load boundary exactly.
void SiluAndMulFp4QuantKernel(Queue& q, Tensor& out_packed, Tensor& out_scale,
const Tensor& gate_up,
float input_global_scale_inv,
Fp4ScaleLayout scale_layout) {
const int64_t m = gate_up.shape[0], i = gate_up.shape[1] / 2;
std::vector<uint16_t> tmp(static_cast<size_t>(m) * static_cast<size_t>(i));
Tensor act = Tensor::Contiguous(tmp.data(), DType::kBF16, gate_up.device, {m, i});
SiluAndMulKernel(q, act, gate_up);
ScaledFp4QuantKernel(q, out_packed, out_scale, act,
input_global_scale_inv, scale_layout);
}
void SigmoidGateBf16Kernel(Queue&, Tensor& out, const Tensor& attn,
const Tensor& gate); // defined below
// SigmoidGateFp4Quant CPU fallback = the exact composite (bf16 intermediate then
// quant) — the definition of correctness for the CUDA fused kernel. The bf16
// scratch reproduces the round-through-bf16 the CUDA kernel folds in.
void SigmoidGateFp4QuantKernel(Queue& q, Tensor& out_packed, Tensor& out_scale,
const Tensor& attn, const Tensor& gate,
float input_global_scale_inv, Fp4ScaleLayout scale_layout) {
const int64_t m = attn.shape[0], i = attn.shape[1];
std::vector<uint16_t> tmp(static_cast<size_t>(m) * static_cast<size_t>(i));
Tensor act = Tensor::Contiguous(tmp.data(), DType::kBF16, attn.device, {m, i});
SigmoidGateBf16Kernel(q, act, attn, gate);
ScaledFp4QuantKernel(q, out_packed, out_scale, act, input_global_scale_inv,
scale_layout);
}
// MatmulNvfp4Fp4 CPU kernel: out[m,n] = alpha * Σ_k (a_fp4·f8(a_scale))·(b_fp4·
// f8(b_scale)). Equals vllm::RunNvfp4Emulation up to K-reduction order.
void MatmulNvfp4Fp4Kernel(Queue&, Tensor& out, const Tensor& a_packed, const Tensor& a_scale,
const Tensor& b_packed, const Tensor& b_scale, float alpha) {
const int64_t m = a_packed.shape[0], k = a_packed.shape[1] * 2, n = b_packed.shape[0];
constexpr int kBS = 16;
const int64_t groups = k / kBS;
const auto* ap = a_packed.Ptr<uint8_t>();
const auto* as = a_scale.Ptr<uint8_t>();
const auto* bp = b_packed.Ptr<uint8_t>();
const auto* bs = b_scale.Ptr<uint8_t>();
// Row-chunked over M (each output row + its arow decode owned by one
// thread); per-column K-group reduction order unchanged.
ForRows(m, [&](int64_t r0, int64_t r1) {
std::vector<float> arow(static_cast<size_t>(k));
for (int64_t i = r0; i < r1; ++i) {
// Decode a_fp4·a_scale_fp8 for this row once (reused across N columns).
for (int64_t g = 0; g < groups; ++g) {
const float asf = Fp8ToF32(as[i * groups + g]);
for (int j = 0; j < kBS / 2; ++j) {
const uint8_t byte = ap[(i * k + g * kBS) / 2 + j];
arow[static_cast<size_t>(g * kBS + 2 * j)] = Nibble(byte & 0x0FU) * asf;
arow[static_cast<size_t>(g * kBS + 2 * j + 1)] = Nibble(byte >> 4) * asf;
}
}
for (int64_t col = 0; col < n; ++col) {
float acc = 0.0F;
for (int64_t g = 0; g < groups; ++g) {
const float bsf = Fp8ToF32(bs[col * groups + g]);
for (int j = 0; j < kBS / 2; ++j) {
const uint8_t byte = bp[(col * k + g * kBS) / 2 + j];
acc += arow[static_cast<size_t>(g * kBS + 2 * j)] * (Nibble(byte & 0x0FU) * bsf);
acc += arow[static_cast<size_t>(g * kBS + 2 * j + 1)] * (Nibble(byte >> 4) * bsf);
}
}
StoreF32(out, i * n + col, alpha * acc);
}
}
});
}
void EmbeddingKernel(Queue&, Tensor& out, const Tensor& table, const Tensor& ids) {
const int64_t t = ids.shape[0], h = table.shape[1], v = table.shape[0];
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
int64_t id = ids.dtype == DType::kI32 ? ids.Ptr<int32_t>()[i] : ids.Ptr<int64_t>()[i];
VT_CHECK(id >= 0 && id < v, "embedding: id out of range");
for (int64_t j = 0; j < h; ++j) {
StoreF32(out, i * h + j, LoadF32(table, id * h + j));
}
}
});
}
// Llama-3 rope frequency rescale (vLLM Llama3RotaryEmbedding._compute_inv_freq,
// rotary_embedding/llama3_rope.py:33-54); no-op when scaling_factor <= 0. Mirrors
// the CUDA Llama3ScaleFreq element-for-element so the CPU reference and the CUDA
// kernel agree.
inline double Llama3ScaleFreq(double freq, const RopeArgs& a) {
const double sf = static_cast<double>(a.llama3_scaling_factor);
if (!(sf > 0.0)) return freq;
constexpr double kTwoPi = 6.283185307179586476925286766559;
const double lo = static_cast<double>(a.llama3_low_freq_factor);
const double hi = static_cast<double>(a.llama3_high_freq_factor);
const double omax = static_cast<double>(a.llama3_orig_max_position);
const double low_freq_wavelen = omax / lo;
const double high_freq_wavelen = omax / hi;
const double wave_len = kTwoPi / freq;
double smooth = 0.0;
if (lo != hi) smooth = (omax / wave_len - lo) / (hi - lo);
if (wave_len < high_freq_wavelen) return freq;
if (wave_len > low_freq_wavelen) return freq / sf;
return (1.0 - smooth) * freq / sf + smooth * freq;
}
// In-place rotation of one head starting at element head_off; f32 math,
// stores round back to the tensor's dtype (f32 or bf16).
void RopeRotateHead(const Tensor& t, int64_t head_off, int rot, double base, int64_t pos,
const RopeArgs& args) {
const int half = rot / 2;
for (int i = 0; i < half; ++i) {
double freq = std::pow(base, -2.0 * i / rot);
freq = Llama3ScaleFreq(freq, args);
double angle = static_cast<double>(pos) * freq;
float c = static_cast<float>(std::cos(angle));
float s = static_cast<float>(std::sin(angle));
float x = LoadF32(t, head_off + i);
float y = LoadF32(t, head_off + i + half);
StoreF32(t, head_off + i, x * c - y * s);
StoreF32(t, head_off + i + half, x * s + y * c);
}
}
void RopeNeoxKernel(Queue&, Tensor& qs, Tensor& ks, const Tensor& pos, const RopeArgs& args) {
const int64_t t = qs.shape[0], hq = qs.shape[1], hk = ks.shape[1], d = qs.shape[2];
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
int64_t p = pos.dtype == DType::kI32 ? pos.Ptr<int32_t>()[i] : pos.Ptr<int64_t>()[i];
for (int64_t hh = 0; hh < hq; ++hh) {
RopeRotateHead(qs, (i * hq + hh) * d, args.rotary_dim, static_cast<double>(args.base), p, args);
}
for (int64_t hh = 0; hh < hk; ++hh) {
RopeRotateHead(ks, (i * hk + hh) * d, args.rotary_dim, static_cast<double>(args.base), p, args);
}
}
});
}
// Ported from vLLM's supplied-cache rotary path:
// base.py:160-252; common.py:145-185; mrope.py:14-187,263-375
// @ e24d1b24fe96. Formula construction stays outside this hot apply loop.
int MropeAxisForPair(int64_t pair, const RopeArgs& args) {
if (args.mrope_interleaved) {
if (pair % 3 == 1 &&
pair <= 3LL * static_cast<int64_t>(args.mrope_section[1])) {
return 1;
}
if (pair % 3 == 2 &&
pair <= 3LL * static_cast<int64_t>(args.mrope_section[2])) {
return 2;
}
return 0;
}
if (pair < args.mrope_section[0]) return 0;
if (pair < static_cast<int64_t>(args.mrope_section[0]) +
args.mrope_section[1]) {
return 1;
}
return 2;
}
void RopeFromCacheKernel(Queue&, Tensor& qs, Tensor* ks,
const Tensor& positions, const Tensor& cache,
const RopeArgs& args) {
const int64_t tokens = qs.shape[0];
const int64_t hq = qs.shape[1];
const int64_t hk = ks == nullptr ? 0 : ks->shape[1];
const int64_t half = args.rotary_dim / 2;
const bool is_mrope = positions.rank == 2;
// MLA campaign W6: q/k are addressed through their STRIDES, not a contiguous
// (token * heads + head) * head_dim formula. DeepSeek's DECOUPLED RoPE rotates
// only the trailing qk_rope_head_dim slice of the query head
// (deepseek_v2.py:580-595 / mla.py:160-167 pass `q[..., qk_nope_head_dim:]`),
// and its k_pe is the trailing column block of the single fused
// kv_a_proj_with_mqa output — both are STRIDED VIEWS. For a contiguous tensor
// the strided offsets are integer-identical to the old formula, so every
// existing caller is bit-identical by construction.
ForRows(tokens, [&](int64_t row_start, int64_t row_end) {
for (int64_t token = row_start; token < row_end; ++token) {
for (int64_t pair = 0; pair < half; ++pair) {
const int axis = is_mrope ? MropeAxisForPair(pair, args) : 0;
const int64_t pos_offset =
is_mrope ? static_cast<int64_t>(axis) * tokens + token : token;
const int64_t position =
positions.dtype == DType::kI32
? static_cast<int64_t>(positions.Ptr<int32_t>()[pos_offset])
: positions.Ptr<int64_t>()[pos_offset];
VT_CHECK(position >= 0 && position < cache.shape[0],
"rope_from_cache: position outside cache");
const int64_t cache_off = position * args.rotary_dim;
const float c = LoadF32(cache, cache_off + pair);
const float s = LoadF32(cache, cache_off + half + pair);
const int64_t first = args.is_neox_style ? pair : pair * 2;
const int64_t second =
args.is_neox_style ? pair + half : pair * 2 + 1;
for (int64_t head = 0; head < hq; ++head) {
const int64_t off = token * qs.stride[0] + head * qs.stride[1];
const float x = LoadF32(qs, off + first);
const float y = LoadF32(qs, off + second);
StoreF32(qs, off + first, x * c - y * s);
StoreF32(qs, off + second, x * s + y * c);
}
if (ks != nullptr) {
for (int64_t head = 0; head < hk; ++head) {
const int64_t off = token * ks->stride[0] + head * ks->stride[1];
const float x = LoadF32(*ks, off + first);
const float y = LoadF32(*ks, off + second);
StoreF32(*ks, off + first, x * c - y * s);
StoreF32(*ks, off + second, x * s + y * c);
}
}
}
}
});
}
// Fused MLA norm-rope (kFusedNormRope) CPU reference — the byte-exact composite
// of {RmsNormKernel(x[:, :off]) ; RopeFromCacheKernel(x[:, off:off+rot])}. The
// two halves address DISJOINT dims, so running them fused (one row loop) is the
// SAME arithmetic in the SAME order as the two standalone kernels; this is the
// Tier-0 golden the CUDA kernel is gated against.
// x [T, off+rot] — merged kv_a output (off = norm_weight length, rot = rotary_dim)
// norm_weight [off] — kv_a_layernorm weight
// latent_out [T, off] — RmsNorm of the leading latent slice (NOT roped)
// pe_out [T, rot] — RopeFromCache rotation of the trailing pe slice (single vector)
void FusedNormRopeKernel(Queue&, Tensor& latent_out, Tensor& pe_out, const Tensor& x,
const Tensor& norm_weight, const Tensor& positions,
const Tensor& cache, const RmsNormArgs& norm_args,
const RopeArgs& rope_args) {
const int64_t t = x.shape[0];
const int64_t off = norm_weight.shape[0]; // latent width (kv_lora_rank)
const int64_t rot = rope_args.rotary_dim; // decoupled-rope width (qk_rope_head_dim)
const int64_t half = rot / 2;
const int64_t xrs = x.stride[0], lrs = latent_out.stride[0], prs = pe_out.stride[0];
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
// --- latent RMSNorm over [0, off) — mirrors RmsNormKernel exactly. ------
const int64_t xb = i * xrs;
float sumsq = 0.0f;
for (int64_t j = 0; j < off; ++j) {
const float v = LoadF32(x, xb + j);
sumsq += v * v;
}
const float inv = 1.0f / std::sqrt(sumsq / static_cast<float>(off) + norm_args.eps);
const int64_t lb = i * lrs;
for (int64_t j = 0; j < off; ++j) {
float wj = LoadF32(norm_weight, j);
if (norm_args.gemma) wj += 1.0f;
StoreF32(latent_out, lb + j, LoadF32(x, xb + j) * inv * wj);
}
// --- decoupled-pe RopeFromCache over [off, off+rot) — mirrors
// RopeFromCacheKernel (single head, base rope, positions rank-1). ----
const int64_t position =
positions.dtype == DType::kI32
? static_cast<int64_t>(positions.Ptr<int32_t>()[i])
: positions.Ptr<int64_t>()[i];
VT_CHECK(position >= 0 && position < cache.shape[0],
"fused_norm_rope: position outside cache");
const int64_t cache_off = position * rot;
const int64_t pb = i * prs;
for (int64_t pair = 0; pair < half; ++pair) {
const float c = LoadF32(cache, cache_off + pair);
const float s = LoadF32(cache, cache_off + half + pair);
const int64_t first = rope_args.is_neox_style ? pair : pair * 2;
const int64_t second = rope_args.is_neox_style ? pair + half : pair * 2 + 1;
const float xr = LoadF32(x, xb + off + first);
const float yr = LoadF32(x, xb + off + second);
StoreF32(pe_out, pb + first, xr * c - yr * s);
StoreF32(pe_out, pb + second, xr * s + yr * c);
}
}
});
}
float Silu(float x) { return x / (1.0f + std::exp(-x)); }
// Per-step RoPE cos|sin cache fill (fused-attn-preamble prep). cos_sin[T,rot] f32:
// cols [0,half)=cos, [half,rot)=sin. Angle math in DOUBLE + f32 cast, matching
// RopeRotateHead/RopeNeoxKernel element-for-element so the cache reproduces the
// inline rotation bit-for-bit.
void RopeCosSinCacheKernel(Queue&, Tensor& cos_sin, const Tensor& positions, const RopeArgs& args) {
const int64_t t = cos_sin.shape[0];
const int rot = args.rotary_dim;
const int64_t half = rot / 2;
const double base = static_cast<double>(args.base);
ForRows(t, [&](int64_t r0, int64_t r1) {
for (int64_t i = r0; i < r1; ++i) {
const int64_t p =
positions.dtype == DType::kI32 ? positions.Ptr<int32_t>()[i] : positions.Ptr<int64_t>()[i];
for (int64_t pair = 0; pair < half; ++pair) {
double freq = std::pow(base, -2.0 * static_cast<double>(pair) / static_cast<double>(rot));
freq = Llama3ScaleFreq(freq, args);
const double angle = static_cast<double>(p) * freq;
StoreF32(cos_sin, i * rot + pair, static_cast<float>(std::cos(angle)));
StoreF32(cos_sin, i * rot + half + pair, static_cast<float>(std::sin(angle)));
}
}
});
}
// gemma-RMSNorm one element: (v*inv)*(gemma ? w+1 : w) — matches RmsNormKernel's
// `v * inv * wj` (wj = w [+1 if gemma]) grouping and order exactly.
float GemmaNormElem(float v, float inv, float w, bool gemma) {
float wj = w;
if (gemma) wj += 1.0f;
return v * inv * wj;
}
// Fused full-attention preamble: split q|gate + gemma qk-RMSNorm(Dh) + partial
// NeoX RoPE (from the cos_sin cache) + gate passthrough, in one pass. Bit-for-bit
// equal (f32 out) to AttnGateSplit + RmsNorm(q) + RmsNorm(k) + RopeNeox composed:
// the variance is f32, the weight is applied as (1+w), and the rotation reuses the
// same f32 c/sn the cache holds (x*c - y*sn / x*sn + y*c). Tail dims [rot,Dh) are
// normed but unrotated.
void AttnQkNormRopeGateKernel(Queue&, Tensor& q_out, Tensor& k_out, Tensor& gate_out,
const Tensor& qgate, const Tensor& kf, const Tensor& q_norm,
const Tensor& k_norm, const Tensor& cos_sin,
const RmsNormArgs& na, const RopeArgs& ra) {
const int64_t t = q_out.shape[0], hq = q_out.shape[1], dh = q_out.shape[2];
const int64_t hkv = k_out.shape[1];
const int rot = ra.rotary_dim;
const int64_t half = rot / 2;
const bool gemma = na.gemma;
// Normalize one head row (src..src+Dh) into out.., applying partial NeoX RoPE
// from cs[0..rot). Recomputes normed[i]/normed[i+half] where paired.
auto do_head = [&](const Tensor& src, int64_t src_off, const Tensor& w, const Tensor& out,
int64_t out_off, const float* cs) {
float ss = 0.0f;
for (int64_t j = 0; j < dh; ++j) {
const float v = LoadF32(src, src_off + j);
ss += v * v;
}
const float inv = 1.0f / std::sqrt(ss / static_cast<float>(dh) + na.eps);
for (int64_t j = 0; j < dh; ++j) {
if (j < half) {
const float ni = GemmaNormElem(LoadF32(src, src_off + j), inv, LoadF32(w, j), gemma);
const float nih =
GemmaNormElem(LoadF32(src, src_off + j + half), inv, LoadF32(w, j + half), gemma);