[CPU] Add AMX-only high-performance MLA backend for DeepSeek V2/V3/R1 (#52616)
Signed-off-by: jiang1.li <jiang1.li@intel.com>
Li, Jiang committed
Aug 19, 2026 at 10:28 UTC
e4d61d0d222ad21437a1507bb34ca7e916aac880
20 files changed
+4156
-6
.buildkite/hardware_tests/cpu.yaml
+4
@@ -12,6 +12,9 @@ steps:
12
- vllm/_custom_ops.py
13
- tests/kernels/attention/test_cpu_attn.py
14
- tests/v1/attention/test_group_head_counts.py
15
+ - tests/kernels/attention/test_amx_mla.py
16
+ - vllm/v1/attention/backends/mla/amx_mla.py
17
+ - vllm/model_executor/layers/attention/mla_attention.py
18
- tests/kernels/moe/test_cpu_fused_moe.py
19
- tests/kernels/moe/test_cpu_quant_fused_moe.py
20
- tests/kernels/test_onednn.py
@@ -29,6 +32,7 @@ steps:
32
bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 30m "
33
pytest -x -v -s tests/kernels/attention/test_cpu_attn.py
34
pytest -x -v -s tests/v1/attention/test_group_head_counts.py
35
+ pytest -x -v -s tests/kernels/attention/test_amx_mla.py
36
pytest -x -v -s tests/kernels/moe/test_cpu_fused_moe.py
37
pytest -x -v -s tests/kernels/moe/test_cpu_quant_fused_moe.py
38
pytest -x -v -s tests/kernels/mamba/test_cpu_short_conv.py
cmake/cpu_extension.cmake
+5
-1
@@ -519,7 +519,11 @@ if (ENABLE_X86_ISA)
519
"csrc/cpu/sgl-kernels/moe.cpp"
520
"csrc/cpu/sgl-kernels/moe_int8.cpp"
521
"csrc/cpu/sgl-kernels/moe_int4.cpp"
522
- "csrc/cpu/sgl-kernels/moe_fp8.cpp")
522
+ "csrc/cpu/sgl-kernels/moe_fp8.cpp"
523
+ "csrc/cpu/sgl-kernels/bmm.cpp"
524
+ "csrc/cpu/sgl-kernels/decode.cpp"
525
+ "csrc/cpu/sgl-kernels/extend.cpp"
526
+ "csrc/cpu/sgl-kernels/mla_cache.cpp")
527
528
set(VLLM_EXT_SRC_AVX512
529
"csrc/cpu/sgl-kernels/fla.cpp"
csrc/cpu/sgl-kernels/bmm.cpp
new
+208
@@ -0,0 +1,208 @@
1
+// Adapted from
2
+// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
+
4
+// clang-format off
5
+
6
+#include "common.h"
7
+#include "gemm.h"
8
+#include "vec.h"
9
+
10
+namespace {
11
+
12
+template <typename scalar_t, typename packed_t>
13
+void bmm_kernel_impl(
14
+ scalar_t* __restrict__ out,
15
+ const scalar_t* __restrict__ mat1,
16
+ const packed_t* __restrict__ mat2,
17
+ int64_t B,
18
+ int64_t M,
19
+ int64_t N,
20
+ int64_t K,
21
+ int64_t mat1_strideB,
22
+ int64_t mat1_strideM,
23
+ int64_t out_strideB,
24
+ int64_t out_strideM,
25
+ float scale = 0.f) {
26
+ constexpr int64_t BLOCK_M = block_size_m();
27
+ constexpr int64_t BLOCK_N = block_size_n();
28
+ const int64_t MB = div_up(M, BLOCK_M);
29
+ const int64_t NB = div_up(N, BLOCK_N);
30
+
31
+ // mat2 contiguous in [B, N, K]
32
+ int64_t mat2_strideB = N * K;
33
+ int64_t mat2_strideN = K;
34
+
35
+ const bool use_brgemm = can_use_brgemm<scalar_t>(M);
36
+
37
+ // parallel on [B, MB, NB]
38
+ at::parallel_for(0, B * MB * NB, 0, [&](int64_t begin, int64_t end) {
39
+ int64_t bs{0}, mb{0}, nb{0};
40
+ data_index_init(begin, bs, B, mb, MB, nb, NB);
41
+
42
+ // for brgemm, use float32 for accumulate
43
+ alignas(64) float Ctmp[BLOCK_M * BLOCK_N];
44
+
45
+ for (int i = begin; i < end; ++i) {
46
+ UNUSED(i);
47
+ int mb_start = mb * BLOCK_M;
48
+ int mb_size = std::min(M - mb_start, BLOCK_M);
49
+ int nb_start = nb * BLOCK_N;
50
+ int nb_size = std::min(N - nb_start, BLOCK_N);
51
+
52
+ tinygemm_kernel<scalar_t>(
53
+ /* A */ mat1 + bs * mat1_strideB + mb_start * mat1_strideM,
54
+ /* B */ mat2 + bs * mat2_strideB + nb_start * mat2_strideN /* nb * BLOCK_N * K */,
55
+ /* C */ out + bs * out_strideB + mb_start * out_strideM + nb_start,
56
+ /* Ctmp*/ Ctmp,
57
+ /* M */ mb_size,
58
+ /* N */ nb_size,
59
+ /* K */ K,
60
+ /* lda */ mat1_strideM,
61
+ /* ldb */ nb_size,
62
+ /* ldc */ out_strideM,
63
+ /* brg */ use_brgemm);
64
+
65
+ // move to the next index
66
+ data_index_step(bs, B, mb, MB, nb, NB);
67
+ }
68
+
69
+ if (use_brgemm) {
70
+ at::native::cpublas::brgemm_release();
71
+ }
72
+ });
73
+}
74
+
75
+template <>
76
+void bmm_kernel_impl(
77
+ at::BFloat16* __restrict__ out,
78
+ const at::BFloat16* __restrict__ mat1,
79
+ const at::Float8_e4m3fn* __restrict__ mat2,
80
+ int64_t B,
81
+ int64_t M,
82
+ int64_t N,
83
+ int64_t K,
84
+ int64_t mat1_strideB,
85
+ int64_t mat1_strideM,
86
+ int64_t out_strideB,
87
+ int64_t out_strideM,
88
+ float scale) {
89
+ constexpr int64_t BLOCK_M = block_size_m();
90
+ constexpr int64_t BLOCK_N = block_size_n();
91
+ const int64_t MB = div_up(M, BLOCK_M);
92
+ const int64_t NB = div_up(N, BLOCK_N);
93
+
94
+ // mat2 contiguous in [B, N, K]
95
+ int64_t mat2_strideB = N * K;
96
+ int64_t mat2_strideN = K;
97
+
98
+ const bool use_brgemm = can_use_brgemm<at::BFloat16>(M);
99
+
100
+ // parallel on [B, MB, NB]
101
+ parallel_2d(B * MB, NB, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
102
+ // for brgemm, use float32 for accumulate
103
+ alignas(64) float Ctmp[BLOCK_M * BLOCK_N];
104
+ // for brgemm when mat2 is float8_e4m3
105
+ alignas(64) at::BFloat16 Btmp[BLOCK_N * BLOCK_K];
106
+
107
+ loop_2d<at::Float8_e4m3fn>(mb0, mb1, nb0, nb1, BLOCK_N * K, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
108
+ int64_t bs = mb / MB;
109
+ int64_t mb_start = (mb % MB) * BLOCK_M;
110
+ int64_t mb_size = std::min(M - mb_start, BLOCK_M);
111
+ int64_t nb_start = nb * BLOCK_N;
112
+ int64_t nb_size = std::min(N - nb_start, BLOCK_N);
113
+
114
+ tinygemm_kernel(
115
+ /* A */ mat1 + bs * mat1_strideB + mb_start * mat1_strideM,
116
+ /* B */ mat2 + bs * mat2_strideB + nb_start * mat2_strideN /* nb * BLOCK_N * K */,
117
+ /* C */ out + bs * out_strideB + mb_start * out_strideM + nb_start,
118
+ /* Btmp*/ Btmp,
119
+ /* Ctmp*/ Ctmp,
120
+ /*scale*/ scale,
121
+ /* M */ mb_size,
122
+ /* N */ nb_size,
123
+ /* K */ K,
124
+ /* lda */ mat1_strideM,
125
+ /* ldb */ nb_size,
126
+ /* ldc */ out_strideM,
127
+ /* brg */ use_brgemm);
128
+ });
129
+
130
+ if (use_brgemm) {
131
+ at::native::cpublas::brgemm_release();
132
+ }
133
+ });
134
+}
135
+
136
+} // anonymous namespace
137
+
138
+// mat1 : [B, M, K]
139
+// mat2 : [B, N, K] or [B, OC, IC]
140
+// out : [B, M, N]
141
+// scale: [] 0-dim tensor for per tensor quant
142
+//
143
+void bmm_cpu(
144
+ at::Tensor& out, at::Tensor& mat1, at::Tensor& mat2, bool is_vnni, const std::optional<at::Tensor>& scale) {
145
+ auto packed_w = is_vnni ? mat2 : convert_weight_packed(mat2);
146
+
147
+ // input and out could be non-contiguous
148
+ // weight needs to be contiguous in [OC, IC] order
149
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(mat1);
150
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(out);
151
+ CHECK_INPUT(mat2);
152
+ CHECK_DIM(3, out);
153
+ CHECK_DIM(3, mat1);
154
+ CHECK_DIM(3, mat2);
155
+
156
+ int64_t B = mat1.size(0);
157
+ int64_t M = mat1.size(1);
158
+ int64_t N = mat2.size(1);
159
+ int64_t K = mat1.size(2);
160
+
161
+ const bool use_fp8_w8a16 = scale.has_value();
162
+ TORCH_CHECK(N % 32 == 0, "tinygemm requires N to be 32x.");
163
+
164
+ int64_t mat1_strideB = mat1.stride(0);
165
+ int64_t mat1_strideM = mat1.stride(1);
166
+ int64_t out_strideB = out.stride(0);
167
+ int64_t out_strideM = out.stride(1);
168
+
169
+ // check shapes
170
+ TORCH_CHECK(mat2.size(0) == B && mat2.size(2) == K, "bmm: mat2 shape mismatch!");
171
+ TORCH_CHECK(out.size(0) == B && out.size(1) == M, "bmm: out shape mismatch!");
172
+ if (!use_fp8_w8a16) {
173
+ AT_DISPATCH_REDUCED_FLOATING_TYPES(mat1.scalar_type(), "bmm_kernel_impl", [&] {
174
+ bmm_kernel_impl<scalar_t, scalar_t>(
175
+ out.data_ptr<scalar_t>(),
176
+ mat1.data_ptr<scalar_t>(),
177
+ packed_w.data_ptr<scalar_t>(),
178
+ B,
179
+ M,
180
+ N,
181
+ K,
182
+ mat1_strideB,
183
+ mat1_strideM,
184
+ out_strideB,
185
+ out_strideM);
186
+ });
187
+ } else { // fp8 bmm
188
+ float scale_val = 0.f;
189
+
190
+ auto scale_tensor = scale.value();
191
+ TORCH_CHECK(scale_tensor.ndimension() == 0, "bmm: expect scale to be 0-dim tensor.");
192
+ scale_val = scale_tensor.item<float>();
193
+
194
+ bmm_kernel_impl<at::BFloat16, at::Float8_e4m3fn>(
195
+ out.data_ptr<at::BFloat16>(),
196
+ mat1.data_ptr<at::BFloat16>(),
197
+ packed_w.data_ptr<at::Float8_e4m3fn>(),
198
+ B,
199
+ M,
200
+ N,
201
+ K,
202
+ mat1_strideB,
203
+ mat1_strideM,
204
+ out_strideB,
205
+ out_strideM,
206
+ scale_val);
207
+ }
208
+}
csrc/cpu/sgl-kernels/decode.cpp
new
+1821
@@ -0,0 +1,1821 @@
1
+// Adapted from
2
+// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
+
4
+// clang-format off
5
+
6
+#include "common.h"
7
+#include "gemm.h"
8
+#include "vec.h"
9
+
10
+namespace {
11
+
12
+// [NOTE] TODO list for this kernel:
13
+// 1. tune the value for BLOCK_N
14
+// 2. planning for {batches, num_heads, num_kv_splits}
15
+// and use actual num_kv_splits for small seq length
16
+// 3. try fast impl of `.tanh()`
17
+// 4. provide amx kernel for index_gemm_kernel_nn when M = 16
18
+//
19
+
20
+#if defined(CPU_CAPABILITY_AVX512)
21
+// key: from [N, 32] to [32/2, N, 2]
22
+// val: from [N, 32] to [N/2, 32, 2]
23
+template <typename scalar_t, typename index_t>
24
+inline void pack_vnni_Nx32(
25
+ scalar_t* __restrict__ dst0,
26
+ scalar_t* __restrict__ dst1,
27
+ const scalar_t* __restrict__ src,
28
+ const index_t* __restrict__ ind,
29
+ int N,
30
+ int ld_src,
31
+ int ld_dst0,
32
+ int ld_dst1,
33
+ bool convert_v) {
34
+ __m512i vinputs[16];
35
+ int n = 0;
36
+ for (; n < N; ++n) {
37
+ vinputs[n] = _mm512_loadu_si512(src + ind[n] * ld_src);
38
+ }
39
+ // padding with zero to avoid uninitialized vectors
40
+ for (; n < 16; ++n) {
41
+ vinputs[n] = _mm512_set1_epi32(0);
42
+ }
43
+
44
+ // pack value, skip 64 elems for deepseek
45
+ // handle 2 vectors at a time from [2, 32] to [32, 2]
46
+ if (convert_v) {
47
+ for (int n = 0; n < 16; n += 2) {
48
+ __m512i d0, d1;
49
+ std::tie(d0, d1) = transpose_2x32_16bit(vinputs[n], vinputs[n + 1]);
50
+ _mm512_storeu_si512(dst1 + (n >> 1) * ld_dst1 * 2, d0);
51
+ _mm512_storeu_si512(dst1 + (n >> 1) * ld_dst1 * 2 + 32, d1);
52
+ }
53
+ }
54
+
55
+ // pack key
56
+ transpose_16x16_32bit(vinputs);
57
+
58
+ const __mmask16 vmask = (1 << N) - 1;
59
+ for (int k = 0; k < 16; ++k) {
60
+ _mm512_mask_storeu_epi32(dst0 + k * ld_dst0 * 2, vmask, vinputs[k]);
61
+ }
62
+}
63
+#endif
64
+
65
+// [NOTE]: MLA vnni format conversion
66
+//
67
+// here we apply same strategy as `FlashMLA`:
68
+// each kv_cache is loaded once and packed twice (L2 cache hit)
69
+//
70
+// * for key: from [N, K/2, 2] to [K/2, N, 2]
71
+// * for value: from [N/2, 2, Kv] to [N/2, Kv, 2]
72
+//
73
+template <typename scalar_t, typename index_t>
74
+void pack_vnni(
75
+ scalar_t* __restrict__ dst0,
76
+ scalar_t* __restrict__ dst1,
77
+ const scalar_t* __restrict__ src,
78
+ const index_t* __restrict__ ind,
79
+ int N,
80
+ int K,
81
+ int Kv,
82
+ int ld_src,
83
+ int ld_dst0,
84
+ int ld_dst1) {
85
+#if defined(CPU_CAPABILITY_AVX512)
86
+ const int NB = div_up(N, 16);
87
+ const int KB = K / 32; // no remainder
88
+ const int KBv = Kv / 32; // no remainder
89
+
90
+ for (int nb = 0; nb < NB; ++nb) {
91
+ for (int kb = 0; kb < KB; ++kb) {
92
+ // handle 16x512bits each block
93
+ int nb_size = std::min(N - nb * 16, 16);
94
+ pack_vnni_Nx32<scalar_t, index_t>(
95
+ /* dst0 */ dst0 + ((kb * 32) >> 1) * ld_dst0 * 2 + nb * 16 * 2,
96
+ /* dst1 */ dst1 + ((nb * 16) >> 1) * ld_dst1 * 2 + kb * 32 * 2,
97
+ /* src */ src + kb * 32,
98
+ /* ind */ ind + nb * 16,
99
+ /* N */ nb_size,
100
+ /* ld_src */ ld_src,
101
+ /* ld_dst0 */ ld_dst0,
102
+ /* ld_dst1 */ ld_dst1,
103
+ /* cvt_v */ kb < KBv);
104
+ }
105
+ }
106
+#else
107
+ for (int n = 0; n < N; ++n) {
108
+ index_t index = ind[n];
109
+ for (int k = 0; k < K / 2; ++k) {
110
+ for (int d = 0; d < 2; ++d) {
111
+ dst0[k * ld_dst0 * 2 + n * 2 + d] = src[index * ld_src + k * 2 + d];
112
+ }
113
+ }
114
+ }
115
+ // from [N/2, 2, K] to [N/2, K, 2]
116
+ for (int n = 0; n < (N >> 1) * 2; n += 2) {
117
+ index_t index0 = ind[n + 0];
118
+ index_t index1 = ind[n + 1];
119
+ for (int k = 0; k < Kv; ++k) {
120
+ dst1[(n >> 1) * ld_dst1 * 2 + k * 2 + 0] = src[index0 * ld_src + k];
121
+ dst1[(n >> 1) * ld_dst1 * 2 + k * 2 + 1] = src[index1 * ld_src + k];
122
+ }
123
+ }
124
+ if (N % 2 != 0) {
125
+ index_t index = ind[N - 1];
126
+ for (int k = 0; k < Kv; ++k) {
127
+ dst1[(N >> 1) * ld_dst1 * 2 + k * 2 + 0] = src[index * ld_src + k];
128
+ dst1[(N >> 1) * ld_dst1 * 2 + k * 2 + 1] = 0;
129
+ }
130
+ }
131
+#endif
132
+}
133
+
134
+template <typename scalar_t>
135
+inline void fill_stub(scalar_t* __restrict__ out, float val, int64_t size) {
136
+ using Vec = at::vec::Vectorized<scalar_t>;
137
+ constexpr int kVecSize = Vec::size();
138
+ const Vec data_vec = Vec(static_cast<scalar_t>(val));
139
+ int64_t d = 0;
140
+#pragma GCC unroll 4
141
+ for (; d <= size - kVecSize; d += kVecSize) {
142
+ data_vec.store(out + d);
143
+ }
144
+ if (size - d > 0) {
145
+ data_vec.store(out + d, size - d);
146
+ }
147
+}
148
+
149
+template <typename scalar_t>
150
+inline void copy_stub(scalar_t* __restrict__ out, const float* __restrict__ acc, float s, int64_t size) {
151
+ using bVec = at::vec::Vectorized<scalar_t>;
152
+ using fVec = at::vec::Vectorized<float>;
153
+ constexpr int kVecSize = bVec::size();
154
+ const fVec s_fvec = fVec(s);
155
+ int64_t d = 0;
156
+#pragma GCC unroll 4
157
+ for (; d <= size - kVecSize; d += kVecSize) {
158
+ auto [a_fvec0, a_fvec1] = load_float_vec2(acc + d);
159
+ a_fvec0 = a_fvec0 * s_fvec;
160
+ a_fvec1 = a_fvec1 * s_fvec;
161
+ bVec out_bvec = convert_from_float_ext<scalar_t>(a_fvec0, a_fvec1);
162
+ out_bvec.store(out + d);
163
+ }
164
+ for (; d < size; ++d) {
165
+ out[d] = static_cast<scalar_t>(acc[d] * s);
166
+ }
167
+}
168
+
169
+template <typename scalar_t>
170
+inline void copy_stub(scalar_t* __restrict__ out, const scalar_t* __restrict__ src, int64_t size) {
171
+ using bVec = at::vec::Vectorized<scalar_t>;
172
+ constexpr int kVecSize = bVec::size();
173
+ int64_t d = 0;
174
+#pragma GCC unroll 4
175
+ for (; d <= size - kVecSize; d += kVecSize) {
176
+ bVec out_bvec = bVec::loadu(src + d);
177
+ out_bvec.store(out + d);
178
+ }
179
+ for (; d < size; ++d) {
180
+ out[d] = src[d];
181
+ }
182
+}
183
+
184
+template <typename scalar_t, int BLOCK_N>
185
+inline void copy_stub(scalar_t* __restrict__ out, const float* __restrict__ input) {
186
+ static_assert(BLOCK_N % 32 == 0);
187
+ using bVec = at::vec::Vectorized<scalar_t>;
188
+ using fVec = at::vec::Vectorized<float>;
189
+
190
+ constexpr int COLS = BLOCK_N / 16;
191
+ auto store = [&](auto i) {
192
+ constexpr int col = i % COLS;
193
+ // for COLS = 2, 4 use 512bit store
194
+ if constexpr (col % 2 == 0) {
195
+ auto [a_fvec0, a_fvec1] = load_float_vec2(input + col * 16);
196
+ bVec out_bvec = convert_from_float_ext<scalar_t>(a_fvec0, a_fvec1);
197
+ out_bvec.store(out + col * 16);
198
+ }
199
+ };
200
+ Unroll<COLS>{}(store);
201
+}
202
+
203
+// GEMM handles query @ key (indexed) x scale
204
+// A : [M, K]
205
+// B : [N, K] indexed
206
+// C : [M, N]
207
+//
208
+template <typename scalar_t, typename index_t, int BLOCK_M, int BLOCK_N>
209
+struct tinygemm_kernel_nt {
210
+ static inline void apply(
211
+ const scalar_t* __restrict__ A,
212
+ const scalar_t* __restrict__ B,
213
+ float* __restrict__ C,
214
+ const index_t* __restrict__ indices,
215
+ float scale,
216
+ int64_t lda,
217
+ int64_t ldb,
218
+ int64_t ldc,
219
+ int64_t K,
220
+ int64_t max_tokens) {
221
+ for (int64_t m = 0; m < BLOCK_M; ++m) {
222
+ for (int64_t n = 0; n < BLOCK_N; ++n) {
223
+ float sum = 0.f;
224
+ int64_t b_idx = indices[n];
225
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
226
+ for (int64_t k = 0; k < K; ++k) {
227
+ sum += scale * static_cast<float>(A[m * lda + k]) * static_cast<float>(B[b_idx * ldb + k]);
228
+ }
229
+ C[m * ldc + n] = sum;
230
+ }
231
+ }
232
+ }
233
+};
234
+
235
+#if defined(CPU_CAPABILITY_AVX512)
236
+template <typename index_t, int BLOCK_M, int BLOCK_N>
237
+struct tinygemm_kernel_nt<at::BFloat16, index_t, BLOCK_M, BLOCK_N> {
238
+ static inline void apply(
239
+ const at::BFloat16* __restrict__ A,
240
+ const at::BFloat16* __restrict__ B,
241
+ float* __restrict__ C,
242
+ const index_t* __restrict__ indices,
243
+ float scale,
244
+ int64_t lda,
245
+ int64_t ldb,
246
+ int64_t ldc,
247
+ int64_t K,
248
+ int64_t max_tokens) {
249
+ constexpr int ROWS = BLOCK_M;
250
+ constexpr int COLS = BLOCK_N;
251
+
252
+ __m512bh va;
253
+ __m512bh vb[COLS];
254
+ __m512 vc[ROWS * COLS];
255
+ __m512 vscale = _mm512_set1_ps(scale);
256
+
257
+ auto loadc = [&](auto i) { vc[i] = _mm512_setzero_ps(); };
258
+ Unroll<ROWS * COLS>{}(loadc);
259
+
260
+ // for main loop
261
+ auto compute = [&](auto i, int64_t k) {
262
+ constexpr int row = i / COLS;
263
+ constexpr int col = i % COLS;
264
+
265
+ if constexpr (col == 0) {
266
+ va = (__m512bh)(_mm512_loadu_si512(A + row * lda + k));
267
+ }
268
+ if constexpr (row == 0) {
269
+ if constexpr (col + 1 < COLS) {
270
+ int64_t b_idx_prefetch = indices[col + 1];
271
+ _mm_prefetch(B + b_idx_prefetch * ldb + k, _MM_HINT_T0);
272
+ }
273
+ int64_t b_idx = indices[col];
274
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
275
+ vb[col] = (__m512bh)(_mm512_loadu_si512(B + b_idx * ldb + k));
276
+ }
277
+ vc[i] = _mm512_dpbf16_ps(vc[i], va, vb[col]);
278
+ };
279
+
280
+ // for remainder
281
+ auto compute2 = [&](auto i, int64_t k, __mmask32 mask) {
282
+ constexpr int row = i / COLS;
283
+ constexpr int col = i % COLS;
284
+
285
+ if constexpr (col == 0) {
286
+ va = (__m512bh)(_mm512_maskz_loadu_epi16(mask, A + row * lda + k));
287
+ }
288
+ if constexpr (row == 0) {
289
+ int64_t b_idx = indices[col];
290
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
291
+ vb[col] = (__m512bh)(_mm512_maskz_loadu_epi16(mask, B + b_idx * ldb + k));
292
+ }
293
+ vc[i] = _mm512_dpbf16_ps(vc[i], va, vb[col]);
294
+ };
295
+
296
+ int64_t k = 0;
297
+ for (; k <= K - 32; k += 32) {
298
+ Unroll<ROWS * COLS>{}(compute, k);
299
+ }
300
+ int64_t count = K - k;
301
+ if (count > 0) {
302
+ __mmask32 mask = (1ULL << count) - 1;
303
+ Unroll<ROWS * COLS>{}(compute2, k, mask);
304
+ }
305
+
306
+ auto storec = [&](auto i) {
307
+ constexpr int row = i / COLS;
308
+ constexpr int col = i % COLS;
309
+ C[row * ldc + col] = _mm512_reduce_add_ps(_mm512_mul_ps(vc[i], vscale));
310
+ };
311
+ Unroll<ROWS * COLS>{}(storec);
312
+ }
313
+};
314
+#endif
315
+
316
+#if defined(CPU_CAPABILITY_AVX512)
317
+template <typename index_t, int BLOCK_M, int BLOCK_N>
318
+struct tinygemm_kernel_nt<at::Half, index_t, BLOCK_M, BLOCK_N> {
319
+ static inline void apply(
320
+ const at::Half* __restrict__ A,
321
+ const at::Half* __restrict__ B,
322
+ float* __restrict__ C,
323
+ const index_t* __restrict__ indices,
324
+ float scale,
325
+ int64_t lda,
326
+ int64_t ldb,
327
+ int64_t ldc,
328
+ int64_t K,
329
+ int64_t max_tokens) {
330
+ constexpr int ROWS = BLOCK_M;
331
+ constexpr int COLS = BLOCK_N;
332
+
333
+ __m512 va0, va1;
334
+ __m512 vb0[COLS], vb1[COLS];
335
+ __m512 vc[ROWS * COLS];
336
+ __m512 vscale = _mm512_set1_ps(scale);
337
+
338
+ auto loadc = [&](auto i) { vc[i] = _mm512_setzero_ps(); };
339
+ Unroll<ROWS * COLS>{}(loadc);
340
+
341
+ auto compute = [&](auto i, int64_t k) {
342
+ constexpr int row = i / COLS;
343
+ constexpr int col = i % COLS;
344
+
345
+ if constexpr (col == 0) {
346
+ __m512i a16 = _mm512_loadu_si512((__m512i const*)(A + row * lda + k));
347
+ va0 = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(a16, 0));
348
+ va1 = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(a16, 1));
349
+ }
350
+
351
+ if constexpr (row == 0) {
352
+ int64_t b_idx = indices[col];
353
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
354
+ __m512i b16 = _mm512_loadu_si512((__m512i const*)(B + b_idx * ldb + k));
355
+ vb0[col] = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(b16, 0));
356
+ vb1[col] = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(b16, 1));
357
+ }
358
+
359
+ vc[i] = _mm512_fmadd_ps(va0, vb0[col], _mm512_fmadd_ps(va1, vb1[col], vc[i]));
360
+ };
361
+
362
+ auto compute2 = [&](auto i, int64_t k, __mmask32 mask) {
363
+ constexpr int row = i / COLS;
364
+ constexpr int col = i % COLS;
365
+
366
+ if constexpr (col == 0) {
367
+ __m512i a16 = _mm512_maskz_loadu_epi16(mask, (const void*)(A + row * lda + k));
368
+ va0 = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(a16, 0));
369
+ va1 = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(a16, 1));
370
+ }
371
+
372
+ if constexpr (row == 0) {
373
+ int64_t b_idx = indices[col];
374
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
375
+ __m512i b16 = _mm512_maskz_loadu_epi16(mask, (const void*)(B + b_idx * ldb + k));
376
+ vb0[col] = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(b16, 0));
377
+ vb1[col] = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(b16, 1));
378
+ }
379
+
380
+ vc[i] = _mm512_fmadd_ps(va0, vb0[col], _mm512_fmadd_ps(va1, vb1[col], vc[i]));
381
+ };
382
+
383
+ int64_t k = 0;
384
+ for (; k <= K - 32; k += 32) {
385
+ Unroll<ROWS * COLS>{}(compute, k);
386
+ }
387
+ int64_t count = K - k;
388
+ if (count > 0) {
389
+ __mmask32 mask = (1ULL << count) - 1;
390
+ Unroll<ROWS * COLS>{}(compute2, k, mask);
391
+ }
392
+
393
+ auto storec = [&](auto i) {
394
+ constexpr int row = i / COLS;
395
+ constexpr int col = i % COLS;
396
+ C[row * ldc + col] = _mm512_reduce_add_ps(_mm512_mul_ps(vc[i], vscale));
397
+ };
398
+ Unroll<ROWS * COLS>{}(storec);
399
+ }
400
+};
401
+#endif
402
+
403
+#define LAUNCH_TINYGEMM_KERNEL_NT(MB_SIZE, NB_SIZE) \
404
+ tinygemm_kernel_nt<scalar_t, index_t, MB_SIZE, NB_SIZE>::apply( \
405
+ A + mb_start * lda, B, C + mb_start * ldc + nb_start, indices + nb_start, scale, lda, ldb, ldc, K, max_tokens);
406
+
407
+// this is used when N isn't multiple of 16,
408
+// N corresponds to `head_size_v` which should be 16x
409
+template <typename scalar_t, typename index_t>
410
+inline void tinygemm_kernel_nn_scalar(
411
+ const float* __restrict__ A,
412
+ const scalar_t* __restrict__ B,
413
+ float* __restrict__ C,
414
+ const index_t* __restrict__ indices,
415
+ const float* __restrict__ scale,
416
+ int64_t M,
417
+ int64_t N,
418
+ int64_t K,
419
+ int64_t lda,
420
+ int64_t ldb,
421
+ int64_t ldc,
422
+ int64_t max_tokens) {
423
+ for (int64_t m = 0; m < M; ++m) {
424
+ for (int64_t n = 0; n < N; ++n) {
425
+ C[m * ldc + n] *= scale[m];
426
+ for (int64_t k = 0; k < K; ++k) {
427
+ int64_t b_idx = indices[k];
428
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
429
+ C[m * ldc + n] += A[m * lda + k] * static_cast<float>(B[b_idx * ldb + n]);
430
+ }
431
+ }
432
+ }
433
+}
434
+
435
+// GEMM handles v' * scale + attn @ value (indexed)
436
+// A : [M, K]
437
+// B : [K, N] indexed
438
+// C :[M, N]
439
+//
440
+template <typename scalar_t, typename index_t, int BLOCK_M, int BLOCK_N>
441
+struct tinygemm_kernel_nn {
442
+ static inline void apply(
443
+ const float* __restrict__ A,
444
+ const scalar_t* __restrict__ B,
445
+ float* __restrict__ C,
446
+ const index_t* __restrict__ indices,
447
+ const float* __restrict__ scale,
448
+ int64_t lda,
449
+ int64_t ldb,
450
+ int64_t ldc,
451
+ int64_t K,
452
+ int64_t max_tokens) {
453
+ tinygemm_kernel_nn_scalar(A, B, C, indices, scale, BLOCK_M, BLOCK_N, K, lda, ldb, ldc, max_tokens);
454
+ }
455
+};
456
+
457
+#if defined(CPU_CAPABILITY_AVX512)
458
+template <typename index_t, int BLOCK_M, int BLOCK_N>
459
+struct tinygemm_kernel_nn<at::BFloat16, index_t, BLOCK_M, BLOCK_N> {
460
+ static inline void apply(
461
+ const float* __restrict__ A,
462
+ const at::BFloat16* __restrict__ B,
463
+ float* __restrict__ C,
464
+ const index_t* __restrict__ indices,
465
+ const float* __restrict__ scale,
466
+ int64_t lda,
467
+ int64_t ldb,
468
+ int64_t ldc,
469
+ int64_t K,
470
+ int64_t max_tokens) {
471
+ constexpr int ROWS = BLOCK_M;
472
+ constexpr int COLS = BLOCK_N / 16;
473
+
474
+ __m512 va;
475
+ __m512 vb[COLS];
476
+ __m512 vc[ROWS * COLS];
477
+ __m512 vscale;
478
+
479
+ auto loadc = [&](auto i) {
480
+ constexpr int row = i / COLS;
481
+ constexpr int col = i % COLS;
482
+#pragma GCC diagnostic push
483
+#pragma GCC diagnostic ignored "-Warray-bounds"
484
+ if constexpr (col == 0) {
485
+ vscale = _mm512_set1_ps(scale[row]);
486
+ }
487
+#pragma GCC diagnostic pop
488
+ vc[i] = _mm512_loadu_ps(C + row * ldc + col * 16);
489
+ vc[i] = _mm512_mul_ps(vc[i], vscale);
490
+ };
491
+ Unroll<ROWS * COLS>{}(loadc);
492
+
493
+ auto compute = [&](auto i, int64_t k) {
494
+ constexpr int row = i / COLS;
495
+ constexpr int col = i % COLS;
496
+
497
+ if constexpr (col == 0) {
498
+ va = _mm512_set1_ps(A[row * lda + k]);
499
+ }
500
+ if constexpr (row == 0) {
501
+ if (k + 1 < K) {
502
+ int64_t b_idx_prefetch = indices[k + 1];
503
+ _mm_prefetch(B + b_idx_prefetch * ldb + col * 16, _MM_HINT_T0);
504
+ }
505
+ int64_t b_idx = indices[k];
506
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
507
+
508
+ // for COLS = 2, 4, 6, 8 use 512 bit load
509
+ // for COLS = 1, 3, 5, 7 use 256 bit load
510
+ if constexpr (COLS % 2 == 0) {
511
+ if constexpr (col % 2 == 0) {
512
+ __m512i b16 = _mm512_loadu_si512(reinterpret_cast<const __m512i*>(B + b_idx * ldb + col * 16));
513
+ vb[col + 0] = CVT_BF16_TO_FP32(_mm512_extracti32x8_epi32(b16, 0));
514
+ vb[col + 1] = CVT_BF16_TO_FP32(_mm512_extracti32x8_epi32(b16, 1));
515
+ }
516
+ } else {
517
+ __m256i b16 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(B + b_idx * ldb + col * 16));
518
+ vb[col] = CVT_BF16_TO_FP32(b16);
519
+ }
520
+ }
521
+ vc[i] = _mm512_fmadd_ps(va, vb[col], vc[i]);
522
+ };
523
+
524
+ for (int64_t k = 0; k < K; ++k) {
525
+ Unroll<ROWS * COLS>{}(compute, k);
526
+ }
527
+
528
+ auto storec = [&](auto i) {
529
+ constexpr int row = i / COLS;
530
+ constexpr int col = i % COLS;
531
+ _mm512_storeu_ps(C + row * ldc + col * 16, vc[i]);
532
+ };
533
+ Unroll<ROWS * COLS>{}(storec);
534
+ }
535
+};
536
+#endif
537
+
538
+#if defined(CPU_CAPABILITY_AVX512)
539
+template <typename index_t, int BLOCK_M, int BLOCK_N>
540
+struct tinygemm_kernel_nn<at::Half, index_t, BLOCK_M, BLOCK_N> {
541
+ static inline void apply(
542
+ const float* __restrict__ A,
543
+ const at::Half* __restrict__ B,
544
+ float* __restrict__ C,
545
+ const index_t* __restrict__ indices,
546
+ const float* __restrict__ scale,
547
+ int64_t lda,
548
+ int64_t ldb,
549
+ int64_t ldc,
550
+ int64_t K,
551
+ int64_t max_tokens) {
552
+ constexpr int ROWS = BLOCK_M;
553
+ constexpr int COLS = BLOCK_N / 16;
554
+
555
+ __m512 va;
556
+ __m512 vb[COLS];
557
+ __m512 vc[ROWS * COLS];
558
+ __m512 vscale;
559
+
560
+ auto loadc = [&](auto i) {
561
+ constexpr int row = i / COLS;
562
+ constexpr int col = i % COLS;
563
+#pragma GCC diagnostic push
564
+#pragma GCC diagnostic ignored "-Warray-bounds"
565
+ if constexpr (col == 0) {
566
+ vscale = _mm512_set1_ps(scale[row]);
567
+ }
568
+#pragma GCC diagnostic pop
569
+ vc[i] = _mm512_loadu_ps(C + row * ldc + col * 16);
570
+ vc[i] = _mm512_mul_ps(vc[i], vscale);
571
+ };
572
+ Unroll<ROWS * COLS>{}(loadc);
573
+
574
+ auto compute = [&](auto i, int64_t k) {
575
+ constexpr int row = i / COLS;
576
+ constexpr int col = i % COLS;
577
+
578
+ if constexpr (col == 0) {
579
+ va = _mm512_set1_ps(A[row * lda + k]);
580
+ }
581
+ if constexpr (row == 0) {
582
+ if (k + 1 < K) {
583
+ int64_t b_idx_prefetch = indices[k + 1];
584
+ _mm_prefetch(B + b_idx_prefetch * ldb + col * 16, _MM_HINT_T0);
585
+ }
586
+ int64_t b_idx = indices[k];
587
+ TORCH_CHECK(b_idx < max_tokens, "token index out of scope!");
588
+
589
+ // for COLS = 2, 4, 6, 8 use 512 bit load
590
+ // for COLS = 1, 3, 5, 7 use 256 bit load
591
+ if constexpr (COLS % 2 == 0) {
592
+ if constexpr (col % 2 == 0) {
593
+ __m512i b16 = _mm512_loadu_si512(reinterpret_cast<const __m512i*>(B + b_idx * ldb + col * 16));
594
+ vb[col + 0] = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(b16, 0));
595
+ vb[col + 1] = CVT_FP16_TO_FP32(_mm512_extracti32x8_epi32(b16, 1));
596
+ }
597
+ } else {
598
+ __m256i b16 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(B + b_idx * ldb + col * 16));
599
+ vb[col] = CVT_FP16_TO_FP32(b16);
600
+ }
601
+ }
602
+ vc[i] = _mm512_fmadd_ps(va, vb[col], vc[i]);
603
+ };
604
+
605
+ for (int64_t k = 0; k < K; ++k) {
606
+ Unroll<ROWS * COLS>{}(compute, k);
607
+ }
608
+
609
+ auto storec = [&](auto i) {
610
+ constexpr int row = i / COLS;
611
+ constexpr int col = i % COLS;
612
+ _mm512_storeu_ps(C + row * ldc + col * 16, vc[i]);
613
+ };
614
+ Unroll<ROWS * COLS>{}(storec);
615
+ }
616
+};
617
+#endif
618
+
619
+#define LAUNCH_TINYGEMM_KERNEL_NN(MB_SIZE, NB_SIZE) \
620
+ tinygemm_kernel_nn<scalar_t, index_t, MB_SIZE, NB_SIZE>::apply( \
621
+ A + mb_start * lda, \
622
+ B + nb_start, \
623
+ C + mb_start * ldc + nb_start, \
624
+ indices, \
625
+ scale + mb_start, \
626
+ lda, \
627
+ ldb, \
628
+ ldc, \
629
+ K, \
630
+ max_tokens);
631
+
632
+template <typename scalar_t, typename index_t>
633
+void index_gemm_kernel_nt(
634
+ const scalar_t* __restrict__ A,
635
+ const scalar_t* __restrict__ B,
636
+ float* __restrict__ C,
637
+ const index_t* __restrict__ indices,
638
+ float scale,
639
+ int64_t M,
640
+ int64_t N,
641
+ int64_t K,
642
+ int64_t lda,
643
+ int64_t ldb,
644
+ int64_t ldc,
645
+ int64_t max_tokens) {
646
+ // pattern: 1-8-8
647
+ if (M == 1) {
648
+ constexpr int64_t BLOCK_N = 8;
649
+ const int64_t NB = div_up(N, BLOCK_N);
650
+ int64_t mb_start = 0, lda = 1, ldc = 1;
651
+
652
+ for (int64_t nb = 0; nb < NB; ++nb) {
653
+ int64_t nb_start = nb * BLOCK_N;
654
+ int64_t nb_size = std::min(BLOCK_N, N - nb_start);
655
+
656
+ switch (nb_size) {
657
+ case 1:
658
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 1);
659
+ break;
660
+ case 2:
661
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 2);
662
+ break;
663
+ case 3:
664
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 3);
665
+ break;
666
+ case 4:
667
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 4);
668
+ break;
669
+ case 5:
670
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 5);
671
+ break;
672
+ case 6:
673
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 6);
674
+ break;
675
+ case 7:
676
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 7);
677
+ break;
678
+ case 8:
679
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 8);
680
+ break;
681
+ default:
682
+ TORCH_CHECK(false, "Unexpected block size, 1x", "nb_size");
683
+ }
684
+ }
685
+ return;
686
+ }
687
+
688
+ // default pattern: 1-6-24
689
+ // FP16 pattern: 2-8-16
690
+ constexpr int64_t BLOCK_M = 4;
691
+ constexpr int64_t BLOCK_N = std::is_same_v<scalar_t, at::Half> ? 4 : 6;
692
+ const int64_t MB = div_up(M, BLOCK_M);
693
+ const int64_t NB = div_up(N, BLOCK_N);
694
+
695
+ for (int64_t mb = 0; mb < MB; ++mb) {
696
+ int64_t mb_start = mb * BLOCK_M;
697
+ int64_t mb_size = std::min(BLOCK_M, M - mb_start);
698
+ for (int64_t nb = 0; nb < NB; ++nb) {
699
+ int64_t nb_start = nb * BLOCK_N;
700
+ int64_t nb_size = std::min(BLOCK_N, N - nb_start);
701
+
702
+ switch (mb_size << 4 | nb_size) {
703
+ // mb_size = 1
704
+ case 0x11:
705
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 1);
706
+ break;
707
+ case 0x12:
708
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 2);
709
+ break;
710
+ case 0x13:
711
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 3);
712
+ break;
713
+ case 0x14:
714
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 4);
715
+ break;
716
+ case 0x15:
717
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 5);
718
+ break;
719
+ case 0x16:
720
+ LAUNCH_TINYGEMM_KERNEL_NT(1, 6);
721
+ break;
722
+ // mb_size = 2
723
+ case 0x21:
724
+ LAUNCH_TINYGEMM_KERNEL_NT(2, 1);
725
+ break;
726
+ case 0x22:
727
+ LAUNCH_TINYGEMM_KERNEL_NT(2, 2);
728
+ break;
729
+ case 0x23:
730
+ LAUNCH_TINYGEMM_KERNEL_NT(2, 3);
731
+ break;
732
+ case 0x24:
733
+ LAUNCH_TINYGEMM_KERNEL_NT(2, 4);
734
+ break;
735
+ case 0x25:
736
+ LAUNCH_TINYGEMM_KERNEL_NT(2, 5);
737
+ break;
738
+ case 0x26:
739
+ LAUNCH_TINYGEMM_KERNEL_NT(2, 6);
740
+ break;
741
+ // mb_size = 3
742
+ case 0x31:
743
+ LAUNCH_TINYGEMM_KERNEL_NT(3, 1);
744
+ break;
745
+ case 0x32:
746
+ LAUNCH_TINYGEMM_KERNEL_NT(3, 2);
747
+ break;
748
+ case 0x33:
749
+ LAUNCH_TINYGEMM_KERNEL_NT(3, 3);
750
+ break;
751
+ case 0x34:
752
+ LAUNCH_TINYGEMM_KERNEL_NT(3, 4);
753
+ break;
754
+ case 0x35:
755
+ LAUNCH_TINYGEMM_KERNEL_NT(3, 5);
756
+ break;
757
+ case 0x36:
758
+ LAUNCH_TINYGEMM_KERNEL_NT(3, 6);
759
+ break;
760
+ // mb_size = 4
761
+ case 0x41:
762
+ LAUNCH_TINYGEMM_KERNEL_NT(4, 1);
763
+ break;
764
+ case 0x42:
765
+ LAUNCH_TINYGEMM_KERNEL_NT(4, 2);
766
+ break;
767
+ case 0x43:
768
+ LAUNCH_TINYGEMM_KERNEL_NT(4, 3);
769
+ break;
770
+ case 0x44:
771
+ LAUNCH_TINYGEMM_KERNEL_NT(4, 4);
772
+ break;
773
+ case 0x45:
774
+ LAUNCH_TINYGEMM_KERNEL_NT(4, 5);
775
+ break;
776
+ case 0x46:
777
+ LAUNCH_TINYGEMM_KERNEL_NT(4, 6);
778
+ break;
779
+ default:
780
+ TORCH_CHECK(false, "Unexpected block size, ", mb_size, "x", "nb_size");
781
+ }
782
+ }
783
+ }
784
+}
785
+
786
+template <typename scalar_t, typename index_t>
787
+void index_gemm_kernel_nn(
788
+ const float* __restrict__ A,
789
+ const scalar_t* __restrict__ B,
790
+ float* __restrict__ C,
791
+ const index_t* __restrict__ indices,
792
+ float* __restrict__ scale,
793
+ int64_t M,
794
+ int64_t N,
795
+ int64_t K,
796
+ int64_t lda,
797
+ int64_t ldb,
798
+ int64_t ldc,
799
+ int64_t max_tokens) {
800
+ constexpr int kVecSize = 16;
801
+ if ((N & (kVecSize - 1)) != 0) {
802
+ tinygemm_kernel_nn_scalar(A, B, C, indices, scale, M, N, K, lda, ldb, ldc, max_tokens);
803
+ return;
804
+ }
805
+
806
+ // pattern: 1-8-8
807
+ if (M == 1) {
808
+ constexpr int64_t BLOCK_N = 8 * kVecSize;
809
+ const int64_t NB = div_up(N, BLOCK_N);
810
+ int64_t mb_start = 0, lda = 1, ldc = 1;
811
+
812
+ for (int64_t nb = 0; nb < NB; ++nb) {
813
+ int64_t nb_start = nb * BLOCK_N;
814
+ int64_t nb_size = std::min(BLOCK_N, N - nb_start);
815
+
816
+ switch (nb_size >> 4) {
817
+ case 1:
818
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 16);
819
+ break;
820
+ case 2:
821
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 32);
822
+ break;
823
+ case 3:
824
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 48);
825
+ break;
826
+ case 4:
827
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 64);
828
+ break;
829
+ case 5:
830
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 80);
831
+ break;
832
+ case 6:
833
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 96);
834
+ break;
835
+ case 7:
836
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 112);
837
+ break;
838
+ case 8:
839
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 128);
840
+ break;
841
+ default:
842
+ TORCH_CHECK(false, "Unexpected block size, 1x", "nb_size");
843
+ }
844
+ }
845
+ return;
846
+ }
847
+
848
+ constexpr int64_t BLOCK_M = 4;
849
+ constexpr int64_t BLOCK_N = 6 * kVecSize;
850
+ const int64_t MB = div_up(M, BLOCK_M);
851
+ const int64_t NB = div_up(N, BLOCK_N);
852
+
853
+ for (int64_t mb = 0; mb < MB; ++mb) {
854
+ int64_t mb_start = mb * BLOCK_M;
855
+ int64_t mb_size = std::min(BLOCK_M, M - mb_start);
856
+ for (int64_t nb = 0; nb < NB; ++nb) {
857
+ int64_t nb_start = nb * BLOCK_N;
858
+ int64_t nb_size = std::min(BLOCK_N, N - nb_start);
859
+
860
+ switch (mb_size << 4 | nb_size >> 4) {
861
+ // mb_size = 1
862
+ case 0x11:
863
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 16);
864
+ break;
865
+ case 0x12:
866
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 32);
867
+ break;
868
+ case 0x13:
869
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 48);
870
+ break;
871
+ case 0x14:
872
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 64);
873
+ break;
874
+ case 0x15:
875
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 80);
876
+ break;
877
+ case 0x16:
878
+ LAUNCH_TINYGEMM_KERNEL_NN(1, 96);
879
+ break;
880
+ // mb_size = 2
881
+ case 0x21:
882
+ LAUNCH_TINYGEMM_KERNEL_NN(2, 16);
883
+ break;
884
+ case 0x22:
885
+ LAUNCH_TINYGEMM_KERNEL_NN(2, 32);
886
+ break;
887
+ case 0x23:
888
+ LAUNCH_TINYGEMM_KERNEL_NN(2, 48);
889
+ break;
890
+ case 0x24:
891
+ LAUNCH_TINYGEMM_KERNEL_NN(2, 64);
892
+ break;
893
+ case 0x25:
894
+ LAUNCH_TINYGEMM_KERNEL_NN(2, 80);
895
+ break;
896
+ case 0x26:
897
+ LAUNCH_TINYGEMM_KERNEL_NN(2, 96);
898
+ break;
899
+ // mb_size = 3
900
+ case 0x31:
901
+ LAUNCH_TINYGEMM_KERNEL_NN(3, 16);
902
+ break;
903
+ case 0x32:
904
+ LAUNCH_TINYGEMM_KERNEL_NN(3, 32);
905
+ break;
906
+ case 0x33:
907
+ LAUNCH_TINYGEMM_KERNEL_NN(3, 48);
908
+ break;
909
+ case 0x34:
910
+ LAUNCH_TINYGEMM_KERNEL_NN(3, 64);
911
+ break;
912
+ case 0x35:
913
+ LAUNCH_TINYGEMM_KERNEL_NN(3, 80);
914
+ break;
915
+ case 0x36:
916
+ LAUNCH_TINYGEMM_KERNEL_NN(3, 96);
917
+ break;
918
+ // mb_size = 4
919
+ case 0x41:
920
+ LAUNCH_TINYGEMM_KERNEL_NN(4, 16);
921
+ break;
922
+ case 0x42:
923
+ LAUNCH_TINYGEMM_KERNEL_NN(4, 32);
924
+ break;
925
+ case 0x43:
926
+ LAUNCH_TINYGEMM_KERNEL_NN(4, 48);
927
+ break;
928
+ case 0x44:
929
+ LAUNCH_TINYGEMM_KERNEL_NN(4, 64);
930
+ break;
931
+ case 0x45:
932
+ LAUNCH_TINYGEMM_KERNEL_NN(4, 80);
933
+ break;
934
+ case 0x46:
935
+ LAUNCH_TINYGEMM_KERNEL_NN(4, 96);
936
+ break;
937
+ default:
938
+ TORCH_CHECK(false, "Unexpected block size, ", mb_size, "x", "nb_size");
939
+ }
940
+ }
941
+ }
942
+}
943
+
944
+template <typename scalar_t>
945
+void decode_set_kv_buffer(
946
+ scalar_t* __restrict__ k_buffer,
947
+ scalar_t* __restrict__ v_buffer,
948
+ const scalar_t* __restrict__ key,
949
+ const scalar_t* __restrict__ value,
950
+ const int64_t* __restrict__ loc,
951
+ int64_t batches,
952
+ int64_t num_heads_kv,
953
+ int64_t head_size,
954
+ int64_t head_size_v,
955
+ int64_t k_strideN,
956
+ int64_t k_strideH,
957
+ int64_t v_strideN,
958
+ int64_t v_strideH,
959
+ int64_t nk_strideN,
960
+ int64_t nk_strideH,
961
+ int64_t nv_strideN,
962
+ int64_t nv_strideH,
963
+ bool is_mla) {
964
+ at::parallel_for(0, batches * num_heads_kv, 0, [&](int64_t begin, int64_t end) {
965
+ int64_t bs{0}, head_kv_id{0};
966
+ data_index_init(begin, bs, batches, head_kv_id, num_heads_kv);
967
+
968
+ for (int64_t i = begin; i < end; i++) {
969
+ int64_t loc_val = loc[bs];
970
+ scalar_t* k_buffer_ptr = k_buffer + loc_val * k_strideN + head_kv_id * k_strideH;
971
+ const scalar_t* new_key_ptr = key + bs * nk_strideN + head_kv_id * nk_strideH;
972
+ copy_stub<scalar_t>(k_buffer_ptr, new_key_ptr, head_size);
973
+ if (!is_mla) {
974
+ scalar_t* v_buffer_ptr = v_buffer + loc_val * v_strideN + head_kv_id * v_strideH;
975
+ const scalar_t* new_value_ptr = value + bs * nv_strideN + head_kv_id * nv_strideH;
976
+ copy_stub<scalar_t>(v_buffer_ptr, new_value_ptr, head_size_v);
977
+ }
978
+
979
+ // move to the next index
980
+ data_index_step(bs, batches, head_kv_id, num_heads_kv);
981
+ }
982
+ });
983
+}
984
+
985
+template <typename scalar_t>
986
+void decode_accumulate_kv_splits(
987
+ scalar_t* __restrict__ output,
988
+ float* __restrict__ attn_logits,
989
+ const scalar_t* __restrict__ sinks_ptr,
990
+ int64_t batches,
991
+ int64_t num_heads,
992
+ int64_t head_size_v,
993
+ int64_t num_kv_splits,
994
+ int64_t l_stride1,
995
+ int64_t l_stride2,
996
+ bool has_sink) {
997
+ using Vec = at::vec::Vectorized<float>;
998
+
999
+ // parallel on [batches, num_heads]
1000
+ at::parallel_for(0, batches * num_heads, 0, [&](int64_t begin, int64_t end) {
1001
+ int64_t bi{0}, ni{0};
1002
+ data_index_init(begin, bi, batches, ni, num_heads);
1003
+ // NB: here we use logits[b][h][0] as acc, since
1004
+ // for the first kv split (kv_id == 0):
1005
+ // m_delta = std::exp(-inf) = 0
1006
+ // e_logic = std::exp(0) = 1
1007
+ // acc = acc * m_delta + tv * e_logic = tv
1008
+ for (int64_t i = begin; i < end; ++i) {
1009
+ float* __restrict__ acc = attn_logits + i * l_stride1;
1010
+
1011
+ float s_prime = 0.f;
1012
+ float m_prime = -std::numeric_limits<scalar_t>::infinity();
1013
+
1014
+ // update acc with from each kv_split
1015
+ for (int64_t kv_id = 0; kv_id < num_kv_splits; ++kv_id) {
1016
+ float* __restrict__ tv = acc + kv_id * l_stride2;
1017
+ const float tlogic = (acc + kv_id * l_stride2)[head_size_v];
1018
+
1019
+ float m_i = std::max(tlogic, m_prime);
1020
+ float m_delta = std::exp(m_prime - m_i);
1021
+ float e_logic = std::exp(tlogic - m_i);
1022
+ if (kv_id != 0) {
1023
+ at::vec::map2<float>(
1024
+ [m_delta, e_logic](Vec x, Vec y) { return x * Vec(m_delta) + y * Vec(e_logic); },
1025
+ acc,
1026
+ acc,
1027
+ tv,
1028
+ head_size_v);
1029
+ }
1030
+
1031
+ s_prime = s_prime * m_delta + e_logic;
1032
+ m_prime = m_i;
1033
+ }
1034
+ if (has_sink) {
1035
+ s_prime += std::exp(sinks_ptr[ni] - m_prime);
1036
+ }
1037
+ copy_stub<scalar_t>(output + i * head_size_v, acc, 1 / s_prime, head_size_v);
1038
+ // move to the next index
1039
+ data_index_step(bi, batches, ni, num_heads);
1040
+ }
1041
+ });
1042
+}
1043
+
1044
+template <typename scalar_t, typename index_t, int64_t BLOCK_N>
1045
+void decode_attention_kernel_impl(
1046
+ scalar_t* __restrict__ output,
1047
+ float* __restrict__ attn_logits,
1048
+ const scalar_t* __restrict__ query,
1049
+ const scalar_t* __restrict__ k_buffer,
1050
+ const scalar_t* __restrict__ v_buffer,
1051
+ const index_t* __restrict__ req_to_token,
1052
+ const int64_t* __restrict__ req_pool_indices,
1053
+ const int64_t* __restrict__ seq_lens,
1054
+ const int64_t* __restrict__ encoder_lens,
1055
+ const scalar_t* __restrict__ sinks,
1056
+ int64_t batches,
1057
+ int64_t num_heads,
1058
+ int64_t head_size,
1059
+ int64_t head_size_v,
1060
+ int64_t num_kv_splits,
1061
+ int64_t q_strideM,
1062
+ int64_t q_strideH,
1063
+ int64_t k_strideN,
1064
+ int64_t k_strideH,
1065
+ int64_t v_strideN,
1066
+ int64_t v_strideH,
1067
+ float sm_scale,
1068
+ float logit_cap,
1069
+ int64_t max_num_reqs,
1070
+ int64_t max_context_len,
1071
+ int64_t max_total_num_tokens,
1072
+ int64_t sliding_window_size,
1073
+ bool is_cross_attn,
1074
+ bool has_encoder_lens,
1075
+ bool has_sink) {
1076
+ using Vec = at::vec::Vectorized<float>;
1077
+
1078
+ // strides
1079
+ const int64_t l_stride1 = num_kv_splits * (head_size_v + 1);
1080
+ const int64_t l_stride2 = head_size_v + 1;
1081
+
1082
+ const bool has_logit_cap = logit_cap > 0;
1083
+ float rlogit_cap = has_logit_cap ? 1 / logit_cap : 0.f;
1084
+
1085
+ // parallel on [batches, num_heads, num_kv_splits]
1086
+ at::parallel_for(0, batches * num_heads * num_kv_splits, 0, [&](int64_t begin, int64_t end) {
1087
+ int64_t bs{0}, head_id{0}, kv_id{0};
1088
+ data_index_init(begin, bs, batches, head_id, num_heads, kv_id, num_kv_splits);
1089
+
1090
+ // s_prime and s_delta
1091
+ alignas(64) float s_i[BLOCK_N];
1092
+ float* __restrict__ s_delta = s_i;
1093
+
1094
+ for (int64_t i = begin; i < end; ++i) {
1095
+ // get query
1096
+ const scalar_t* __restrict__ q_ptr = query + bs * q_strideM + head_id * q_strideH;
1097
+
1098
+ // get key/value
1099
+ int64_t seq_len_kv = is_cross_attn ? encoder_lens[bs] : seq_lens[bs];
1100
+ int64_t req_pool_id = req_pool_indices[bs];
1101
+ int64_t kv_offset = (has_encoder_lens && (!is_cross_attn)) ? encoder_lens[bs] : 0;
1102
+ if (sliding_window_size > 0 && seq_len_kv > sliding_window_size) {
1103
+ kv_offset = seq_len_kv - sliding_window_size;
1104
+ seq_len_kv = sliding_window_size;
1105
+ }
1106
+ TORCH_CHECK(seq_len_kv <= max_context_len, "seq_len_kv out of scope!");
1107
+ TORCH_CHECK(req_pool_id < max_num_reqs, "req_pool_id out of scope!");
1108
+
1109
+ const int64_t SPLIT_SIZE = div_up(seq_len_kv, num_kv_splits);
1110
+ const int64_t kv_start = kv_id * SPLIT_SIZE;
1111
+ const int64_t kv_end = std::min(kv_start + SPLIT_SIZE, seq_len_kv);
1112
+
1113
+ float m_prime = -std::numeric_limits<float>::infinity();
1114
+ float s_prime = 0.f;
1115
+
1116
+ // get v_prime, and init to zero
1117
+ float* __restrict__ v_prime = attn_logits + i * (head_size_v + 1);
1118
+ fill_stub(v_prime, 0.f, head_size_v);
1119
+
1120
+ // loop over K and V sequence with BLOCK_N
1121
+ for (int64_t n = kv_start; n < kv_end; n += BLOCK_N) {
1122
+ int64_t n_size = std::min(BLOCK_N, kv_end - n);
1123
+
1124
+ // calculate s_i <- scale * Q @ K
1125
+ index_gemm_kernel_nt<scalar_t, index_t>(
1126
+ /* A */ q_ptr,
1127
+ /* B */ k_buffer + head_id * k_strideH,
1128
+ /* C */ s_i,
1129
+ /* ind */ req_to_token + req_pool_id * max_context_len + n + kv_offset,
1130
+ /* scl */ sm_scale,
1131
+ /* M */ 1,
1132
+ /* N */ n_size,
1133
+ /* K */ head_size,
1134
+ /* lda */ 1,
1135
+ /* ldb */ k_strideN,
1136
+ /* ldc */ 1,
1137
+ /* mtt */ max_total_num_tokens);
1138
+
1139
+ // TODO: `tanh` from torch uses sleef u10, going to be slow
1140
+ if (has_logit_cap) {
1141
+ at::vec::map<float>(
1142
+ [logit_cap, rlogit_cap](Vec x) { return Vec(logit_cap) * (x * Vec(rlogit_cap)).tanh(); },
1143
+ s_i,
1144
+ s_i,
1145
+ n_size);
1146
+ }
1147
+
1148
+ // m_i: max value per row
1149
+ float m_i = at::vec::reduce_all<float>([](Vec& x, Vec& y) { return at::vec::maximum(x, y); }, s_i, n_size);
1150
+ m_i = std::max(m_i, m_prime);
1151
+
1152
+ // m_delta <- exp(m' - m_i)
1153
+ float m_delta = std::exp(m_prime - m_i);
1154
+
1155
+ // s_delta <- exp(s_i - m_i)
1156
+ at::vec::map<float>([m_i](Vec x) { return (x - Vec(m_i)).exp_u20(); }, s_delta, s_i, n_size);
1157
+
1158
+ // s' <- s' * m_delta + sum(s_delta)
1159
+ s_prime *= m_delta;
1160
+ s_prime += at::vec::reduce_all<float>([](Vec& x, Vec& y) { return x + y; }, s_delta, n_size);
1161
+
1162
+ m_prime = m_i;
1163
+
1164
+ // calculate V' <- s_delta @ V + V' * m_delta
1165
+ index_gemm_kernel_nn<scalar_t, index_t>(
1166
+ /* A */ s_delta,
1167
+ /* B */ v_buffer + head_id * v_strideH,
1168
+ /* C */ v_prime,
1169
+ /* ind */ req_to_token + req_pool_id * max_context_len + n + kv_offset,
1170
+ /* scl */ &m_delta,
1171
+ /* M */ 1,
1172
+ /* N */ head_size_v,
1173
+ /* K */ n_size,
1174
+ /* lda */ 1,
1175
+ /* ldb */ v_strideN,
1176
+ /* ldc */ 1,
1177
+ /* mtt */ max_total_num_tokens);
1178
+ } // loop with KV blocks
1179
+
1180
+ // only update v' when kv_split_size > 0
1181
+ if (kv_end > kv_start) {
1182
+ float s = 1 / s_prime;
1183
+ at::vec::map<float>([s](Vec out) { return out * Vec(s); }, v_prime, v_prime, head_size_v);
1184
+
1185
+ v_prime[head_size_v] = m_prime + std::log(s_prime);
1186
+ } else {
1187
+ v_prime[head_size_v] = -std::numeric_limits<float>::infinity();
1188
+ }
1189
+
1190
+ // move to the next index
1191
+ data_index_step(bs, batches, head_id, num_heads, kv_id, num_kv_splits);
1192
+ }
1193
+ });
1194
+
1195
+ decode_accumulate_kv_splits(
1196
+ output, attn_logits, sinks, batches, num_heads, head_size_v, num_kv_splits, l_stride1, l_stride2, has_sink);
1197
+} // MHA
1198
+
1199
+template <typename scalar_t, typename index_t, int64_t BLOCK_N>
1200
+void decode_attention_mla_kernel_impl(
1201
+ scalar_t* __restrict__ output,
1202
+ float* __restrict__ attn_logits,
1203
+ const scalar_t* __restrict__ query,
1204
+ const scalar_t* __restrict__ k_buffer,
1205
+ const scalar_t* __restrict__ v_buffer,
1206
+ const index_t* __restrict__ req_to_token,
1207
+ const int64_t* __restrict__ req_pool_indices,
1208
+ const int64_t* __restrict__ seq_lens,
1209
+ scalar_t* __restrict__ buffer,
1210
+ const scalar_t* __restrict__ sinks,
1211
+ int64_t batches,
1212
+ int64_t num_heads,
1213
+ int64_t head_size,
1214
+ int64_t head_size_v,
1215
+ int64_t num_kv_splits,
1216
+ int64_t q_strideM,
1217
+ int64_t q_strideH,
1218
+ int64_t k_strideN,
1219
+ int64_t k_strideH,
1220
+ int64_t v_strideN,
1221
+ int64_t v_strideH,
1222
+ float sm_scale,
1223
+ float logit_cap,
1224
+ int64_t max_num_reqs,
1225
+ int64_t max_context_len,
1226
+ int64_t max_total_num_tokens,
1227
+ int64_t buffer_size_per_thread,
1228
+ bool has_sink) {
1229
+ using Vec = at::vec::Vectorized<float>;
1230
+
1231
+ // block length for heads
1232
+ const int64_t BLOCK_H = batches == 1 ? 6 : (batches > 16 ? 22 : 11);
1233
+
1234
+ // strides
1235
+ const int64_t l_stride0 = num_heads * num_kv_splits * (head_size_v + 1);
1236
+ const int64_t l_stride1 = num_kv_splits * (head_size_v + 1);
1237
+ const int64_t l_stride2 = head_size_v + 1;
1238
+
1239
+ TORCH_CHECK(logit_cap == 0.f, "decode MLA: expect no logit_cap.");
1240
+
1241
+ // partition the heads into blocks for parallel
1242
+ const int64_t num_blocks = div_up(num_heads, BLOCK_H);
1243
+
1244
+ // parallel on [batches, num_blocks, num_kv_splits]
1245
+ at::parallel_for(0, batches * num_blocks * num_kv_splits, 0, [&](int64_t begin, int64_t end) {
1246
+ int64_t bs{0}, block_id{0}, kv_id{0};
1247
+ data_index_init(begin, bs, batches, block_id, num_blocks, kv_id, num_kv_splits);
1248
+
1249
+ int tid = at::get_thread_num();
1250
+ scalar_t* __restrict__ Btmp0 = buffer + tid * buffer_size_per_thread;
1251
+ scalar_t* __restrict__ Btmp1 = Btmp0 + BLOCK_N * head_size;
1252
+
1253
+ // init Btmp1 just once for each thread to prevent NaN
1254
+ // Btmp0 is not needed as it computes full K every single time
1255
+ fill_stub(Btmp1, 0.f, BLOCK_N * head_size_v);
1256
+
1257
+ alignas(64) float s_i[BLOCK_H * BLOCK_N];
1258
+ float* __restrict__ s_delta = s_i;
1259
+ alignas(64) scalar_t s_delta2[BLOCK_H * BLOCK_N];
1260
+
1261
+ alignas(64) float s_prime[BLOCK_H];
1262
+ alignas(64) float m_prime[BLOCK_H];
1263
+ alignas(64) float m_delta[BLOCK_H];
1264
+
1265
+ for (int64_t i = begin; i < end; ++i) {
1266
+ const int64_t h_start = block_id * BLOCK_H;
1267
+ const int64_t h_end = std::min(block_id * BLOCK_H + BLOCK_H, num_heads);
1268
+ const int64_t h_size = h_end - h_start;
1269
+
1270
+ // get query
1271
+ const scalar_t* __restrict__ q_ptr = query + bs * q_strideM + h_start * q_strideH;
1272
+
1273
+ int64_t seq_len_kv = seq_lens[bs];
1274
+ int64_t req_pool_id = req_pool_indices[bs];
1275
+ TORCH_CHECK(seq_len_kv <= max_context_len, "seq_len_kv out of scope!");
1276
+ TORCH_CHECK(req_pool_id < max_num_reqs, "req_pool_id out of scope!");
1277
+
1278
+ const int64_t SPLIT_SIZE = div_up(seq_len_kv, num_kv_splits);
1279
+ const int64_t kv_start = kv_id * SPLIT_SIZE;
1280
+ const int64_t kv_end = std::min(kv_start + SPLIT_SIZE, seq_len_kv);
1281
+
1282
+ fill_stub(s_prime, 0.f, BLOCK_H);
1283
+ fill_stub(m_prime, -std::numeric_limits<float>::infinity(), BLOCK_H);
1284
+
1285
+ // get v_prime, and init to zero
1286
+ float* __restrict__ v_prime = attn_logits + bs * l_stride0 + h_start * l_stride1 + kv_id * l_stride2;
1287
+ for (int64_t h = 0; h < h_size; ++h) {
1288
+ fill_stub(v_prime + h * l_stride1, 0.f, head_size_v);
1289
+ }
1290
+
1291
+ // loop over K and V sequence with BLOCK_N
1292
+ for (int64_t n = kv_start; n < kv_end; n += BLOCK_N) {
1293
+ int64_t n_size = std::min(BLOCK_N, kv_end - n);
1294
+ const int64_t padded_n_size = div_up(int(n_size), TILE_K) * TILE_K;
1295
+
1296
+ // get key and pack
1297
+ pack_vnni<scalar_t, index_t>(
1298
+ /* dst0 */ Btmp0,
1299
+ /* dst1 */ Btmp1,
1300
+ /* src */ k_buffer + /* head_kv_id */ 0 * k_strideH,
1301
+ /* ind */ req_to_token + req_pool_id * max_context_len + n,
1302
+ /* N */ n_size,
1303
+ /* K */ head_size,
1304
+ /* Kv */ head_size_v,
1305
+ /* ld_src */ k_strideN,
1306
+ /* ld_dst0 */ BLOCK_N,
1307
+ /* ld_dst1 */ head_size_v);
1308
+
1309
+ // calculate s_i <- Q @ K
1310
+ at::native::cpublas::brgemm(
1311
+ /* M */ h_size,
1312
+ /* N */ n_size,
1313
+ /* K */ head_size,
1314
+ /* lda */ q_strideH,
1315
+ /* ldb */ BLOCK_N,
1316
+ /* ldc */ BLOCK_N,
1317
+ /* add_C */ false,
1318
+ /* A */ q_ptr,
1319
+ /* B */ Btmp0,
1320
+ /* C */ s_i);
1321
+
1322
+ const Vec scale_vec = Vec(sm_scale);
1323
+ for (int64_t h = 0; h < h_size; ++h) {
1324
+ // s_i <- s_i * scale
1325
+ at::vec::map<float>(
1326
+ [scale_vec](Vec x) { return x * scale_vec; }, s_i + h * BLOCK_N, s_i + h * BLOCK_N, n_size);
1327
+
1328
+ // m_i: max value per row
1329
+ float m_i = at::vec::reduce_all<float>(
1330
+ [](Vec& x, Vec& y) { return at::vec::maximum(x, y); }, s_i + h * BLOCK_N, n_size);
1331
+ m_i = std::max(m_i, m_prime[h]);
1332
+
1333
+ // m_delta <- exp(m' - m_i)
1334
+ m_delta[h] = std::exp(m_prime[h] - m_i);
1335
+
1336
+ // s_delta <- exp(s_i - m_i)
1337
+ at::vec::map<float>(
1338
+ [m_i](Vec x) { return (x - Vec(m_i)).exp_u20(); }, s_delta + h * BLOCK_N, s_i + h * BLOCK_N, n_size);
1339
+
1340
+ // s' <- s' * m_delta + sum(s_delta)
1341
+ s_prime[h] *= m_delta[h];
1342
+ s_prime[h] += at::vec::reduce_all<float>([](Vec& x, Vec& y) { return x + y; }, s_delta + h * BLOCK_N, n_size);
1343
+
1344
+ m_prime[h] = m_i;
1345
+
1346
+ // v' <- v' * m_delta
1347
+ float scale_m = m_delta[h];
1348
+ at::vec::map<float>(
1349
+ [scale_m](Vec x) { return x * Vec(scale_m); },
1350
+ v_prime + h * l_stride1,
1351
+ v_prime + h * l_stride1,
1352
+ head_size_v);
1353
+
1354
+ // pad s_delta with 0 first and then convert to scalar_t
1355
+ fill_stub(s_delta + h * BLOCK_N + n_size, 0.f, padded_n_size - n_size);
1356
+ copy_stub<scalar_t, BLOCK_N>(s_delta2 + h * BLOCK_N, s_delta + h * BLOCK_N);
1357
+ }
1358
+
1359
+ // calculate V' <- s_delta @ V + V'
1360
+ at::native::cpublas::brgemm(
1361
+ /* M */ h_size,
1362
+ /* N */ head_size_v,
1363
+ /* K */ padded_n_size, // n_size
1364
+ /* lda */ BLOCK_N,
1365
+ /* ldb */ head_size_v,
1366
+ /* ldc */ l_stride1,
1367
+ /* add_C */ true,
1368
+ /* A */ s_delta2,
1369
+ /* B */ Btmp1,
1370
+ /* C */ v_prime);
1371
+ } // loop with KV blocks
1372
+
1373
+ // only update v' when kv_split_size > 0
1374
+ if (kv_end > kv_start) {
1375
+ for (int64_t h = 0; h < h_size; ++h) {
1376
+ float s = 1 / s_prime[h];
1377
+ at::vec::map<float>(
1378
+ [s](Vec out) { return out * Vec(s); }, v_prime + h * l_stride1, v_prime + h * l_stride1, head_size_v);
1379
+ (v_prime + h * l_stride1)[head_size_v] = m_prime[h] + std::log(s_prime[h]);
1380
+ }
1381
+ } else {
1382
+ for (int64_t h = 0; h < h_size; ++h) {
1383
+ (v_prime + h * l_stride1)[head_size_v] = -std::numeric_limits<float>::infinity();
1384
+ }
1385
+ }
1386
+
1387
+ // move to the next index
1388
+ data_index_step(bs, batches, block_id, num_blocks, kv_id, num_kv_splits);
1389
+ }
1390
+ at::native::cpublas::brgemm_release();
1391
+ });
1392
+
1393
+ decode_accumulate_kv_splits(
1394
+ output, attn_logits, sinks, batches, num_heads, head_size_v, num_kv_splits, l_stride1, l_stride2, has_sink);
1395
+} // MLA
1396
+
1397
+template <typename scalar_t, typename index_t, int64_t BLOCK_N>
1398
+void decode_attention_grouped_kernel_impl(
1399
+ scalar_t* __restrict__ output,
1400
+ float* __restrict__ attn_logits,
1401
+ const scalar_t* __restrict__ query,
1402
+ const scalar_t* __restrict__ k_buffer,
1403
+ const scalar_t* __restrict__ v_buffer,
1404
+ const index_t* __restrict__ req_to_token,
1405
+ const int64_t* __restrict__ req_pool_indices,
1406
+ const int64_t* __restrict__ seq_lens,
1407
+ const int64_t* __restrict__ encoder_lens,
1408
+ const scalar_t* __restrict__ sinks,
1409
+ int64_t batches,
1410
+ int64_t num_heads,
1411
+ int64_t num_heads_kv,
1412
+ int64_t head_size,
1413
+ int64_t head_size_v,
1414
+ int64_t num_kv_splits,
1415
+ int64_t q_strideM,
1416
+ int64_t q_strideH,
1417
+ int64_t k_strideN,
1418
+ int64_t k_strideH,
1419
+ int64_t v_strideN,
1420
+ int64_t v_strideH,
1421
+ float sm_scale,
1422
+ float logit_cap,
1423
+ int64_t max_num_reqs,
1424
+ int64_t max_context_len,
1425
+ int64_t max_total_num_tokens,
1426
+ int64_t sliding_window_size,
1427
+ bool is_cross_attn,
1428
+ bool has_encoder_lens,
1429
+ bool has_sink) {
1430
+ using Vec = at::vec::Vectorized<float>;
1431
+
1432
+ // block length for heads
1433
+ // we parallel on [batches, divup(num_heads, BLOCK_H), num_kv_splits]
1434
+ // use smaller BLOCK_H when batches is small to utilize all cores
1435
+ constexpr int64_t kBLOCK_H = 16;
1436
+ const int64_t BLOCK_H = std::min(4 * batches, kBLOCK_H);
1437
+
1438
+ // strides
1439
+ const int64_t l_stride0 = num_heads * num_kv_splits * (head_size_v + 1);
1440
+ const int64_t l_stride1 = num_kv_splits * (head_size_v + 1);
1441
+ const int64_t l_stride2 = head_size_v + 1;
1442
+
1443
+ const bool has_logit_cap = logit_cap > 0;
1444
+ float rlogit_cap = has_logit_cap ? 1 / logit_cap : 0.f;
1445
+
1446
+ // partition the heads into blocks for parallel
1447
+ const int64_t num_groups = num_heads / num_heads_kv;
1448
+ const int64_t num_blocks = div_up(num_groups, BLOCK_H);
1449
+
1450
+ // parallel on [batches, num_heads_kv, num_blocks, num_kv_splits]
1451
+ at::parallel_for(0, batches * num_heads_kv * num_blocks * num_kv_splits, 0, [&](int64_t begin, int64_t end) {
1452
+ int64_t bs{0}, head_kv_id{0}, block_id{0}, kv_id{0};
1453
+ data_index_init(begin, bs, batches, head_kv_id, num_heads_kv, block_id, num_blocks, kv_id, num_kv_splits);
1454
+
1455
+ alignas(64) float s_i[BLOCK_H * BLOCK_N];
1456
+ float* __restrict__ s_delta = s_i;
1457
+
1458
+ alignas(64) float s_prime[BLOCK_H];
1459
+ alignas(64) float m_prime[BLOCK_H];
1460
+ alignas(64) float m_delta[BLOCK_H];
1461
+
1462
+ for (int64_t i = begin; i < end; ++i) {
1463
+ const int64_t h_start = head_kv_id * num_groups + block_id * BLOCK_H;
1464
+ const int64_t h_end = head_kv_id * num_groups + std::min(block_id * BLOCK_H + BLOCK_H, num_groups);
1465
+ const int64_t h_size = h_end - h_start;
1466
+
1467
+ // get query
1468
+ const scalar_t* __restrict__ q_ptr = query + bs * q_strideM + h_start * q_strideH;
1469
+
1470
+ int64_t seq_len_kv = is_cross_attn ? encoder_lens[bs] : seq_lens[bs];
1471
+ int64_t req_pool_id = req_pool_indices[bs];
1472
+ int64_t kv_offset = (has_encoder_lens && (!is_cross_attn)) ? encoder_lens[bs] : 0;
1473
+ TORCH_CHECK(seq_len_kv <= max_context_len, "seq_len_kv out of scope!");
1474
+ TORCH_CHECK(req_pool_id < max_num_reqs, "req_pool_id out of scope!");
1475
+ if (sliding_window_size > 0 && seq_len_kv > sliding_window_size) {
1476
+ kv_offset = seq_len_kv - sliding_window_size;
1477
+ seq_len_kv = sliding_window_size;
1478
+ }
1479
+ const int64_t SPLIT_SIZE = div_up(seq_len_kv, num_kv_splits);
1480
+ const int64_t kv_start = kv_id * SPLIT_SIZE;
1481
+ const int64_t kv_end = std::min(kv_start + SPLIT_SIZE, seq_len_kv);
1482
+
1483
+ fill_stub(s_prime, 0.f, BLOCK_H);
1484
+ fill_stub(m_prime, -std::numeric_limits<float>::infinity(), BLOCK_H);
1485
+
1486
+ // get v_prime, and init to zero
1487
+ float* __restrict__ v_prime = attn_logits + bs * l_stride0 + h_start * l_stride1 + kv_id * l_stride2;
1488
+ for (int64_t h = 0; h < h_size; ++h) {
1489
+ fill_stub(v_prime + h * l_stride1, 0.f, head_size_v);
1490
+ }
1491
+
1492
+ // loop over K and V sequence with BLOCK_N
1493
+ for (int64_t n = kv_start; n < kv_end; n += BLOCK_N) {
1494
+ int64_t n_size = std::min(BLOCK_N, kv_end - n);
1495
+
1496
+ // calculate Q @ K
1497
+ index_gemm_kernel_nt<scalar_t, index_t>(
1498
+ /* A */ q_ptr,
1499
+ /* B */ k_buffer + head_kv_id * k_strideH,
1500
+ /* C */ s_i,
1501
+ /* ind */ req_to_token + req_pool_id * max_context_len + n + kv_offset,
1502
+ /* scl */ sm_scale,
1503
+ /* M */ h_size,
1504
+ /* N */ n_size,
1505
+ /* K */ head_size,
1506
+ /* lda */ q_strideH,
1507
+ /* ldb */ k_strideN,
1508
+ /* ldc */ BLOCK_N,
1509
+ /* mtt */ max_total_num_tokens);
1510
+
1511
+ if (has_logit_cap) {
1512
+ at::vec::map<float>(
1513
+ [logit_cap, rlogit_cap](Vec x) { return Vec(logit_cap) * (x * Vec(rlogit_cap)).tanh(); },
1514
+ s_i,
1515
+ s_i,
1516
+ BLOCK_H * BLOCK_N);
1517
+ }
1518
+
1519
+ // update the sm_scale coefficients
1520
+ for (int64_t h = 0; h < h_size; ++h) {
1521
+ // m_i: max value per row
1522
+ float m_i = at::vec::reduce_all<float>(
1523
+ [](Vec& x, Vec& y) { return at::vec::maximum(x, y); }, s_i + h * BLOCK_N, n_size);
1524
+ m_i = std::max(m_i, m_prime[h]);
1525
+
1526
+ // m_delta <- exp(m' - m_i)
1527
+ m_delta[h] = std::exp(m_prime[h] - m_i);
1528
+
1529
+ // s_delta <- exp(s_i - m_i)
1530
+ at::vec::map<float>(
1531
+ [m_i](Vec x) { return (x - Vec(m_i)).exp_u20(); }, s_delta + h * BLOCK_N, s_i + h * BLOCK_N, n_size);
1532
+
1533
+ // s' <- s' * m_delta + sum(s_delta)
1534
+ s_prime[h] *= m_delta[h];
1535
+ s_prime[h] += at::vec::reduce_all<float>([](Vec& x, Vec& y) { return x + y; }, s_delta + h * BLOCK_N, n_size);
1536
+
1537
+ m_prime[h] = m_i;
1538
+ }
1539
+
1540
+ // calculate V' <- s_delta @ V + V' * m_delta
1541
+ index_gemm_kernel_nn<scalar_t, index_t>(
1542
+ /* A */ s_delta,
1543
+ /* B */ v_buffer + head_kv_id * v_strideH,
1544
+ /* C */ v_prime,
1545
+ /* ind */ req_to_token + req_pool_id * max_context_len + n + kv_offset,
1546
+ /* scl */ m_delta,
1547
+ /* M */ h_size,
1548
+ /* N */ head_size_v,
1549
+ /* K */ n_size,
1550
+ /* lda */ BLOCK_N,
1551
+ /* ldb */ v_strideN,
1552
+ /* ldc */ l_stride1,
1553
+ /* mtt */ max_total_num_tokens);
1554
+ } // loop with KV blocks
1555
+
1556
+ // only update v' when kv_split_size > 0
1557
+ if (kv_end > kv_start) {
1558
+ for (int64_t h = 0; h < h_size; ++h) {
1559
+ float s = 1 / s_prime[h];
1560
+ at::vec::map<float>(
1561
+ [s](Vec out) { return out * Vec(s); }, v_prime + h * l_stride1, v_prime + h * l_stride1, head_size_v);
1562
+ (v_prime + h * l_stride1)[head_size_v] = m_prime[h] + std::log(s_prime[h]);
1563
+ }
1564
+ } else {
1565
+ for (int64_t h = 0; h < h_size; ++h) {
1566
+ (v_prime + h * l_stride1)[head_size_v] = -std::numeric_limits<float>::infinity();
1567
+ }
1568
+ }
1569
+
1570
+ // move to the next index
1571
+ data_index_step(bs, batches, head_kv_id, num_heads_kv, block_id, num_blocks, kv_id, num_kv_splits);
1572
+ }
1573
+ });
1574
+
1575
+ decode_accumulate_kv_splits(
1576
+ output, attn_logits, sinks, batches, num_heads, head_size_v, num_kv_splits, l_stride1, l_stride2, has_sink);
1577
+} // GQA/MQA
1578
+
1579
+} // anonymous namespace
1580
+
1581
+// query: [num_tokens, num_heads, head_size]
1582
+// output: [num_tokens, num_heads, head_size]
1583
+// k_buffer: [max_total_num_tokens, num_heads, head_size]
1584
+// v_buffer: [max_total_num_tokens, num_heads, head_size_v]
1585
+// attn_logits: [num_seqs, num_heads, num_kv_splits, head_size_v + 1]
1586
+// req_to_token: [max_num_reqs, max_context_len] int32 or int64
1587
+// req_pool_indices: [num_seqs] int64
1588
+// seq_lens: [num_seqs] int64
1589
+// encoder_lens: [num_seqs] int64 or None
1590
+// sinks: [num_heads] or None
1591
+void decode_attention_cpu(
1592
+ at::Tensor& query,
1593
+ at::Tensor& k_buffer,
1594
+ at::Tensor& v_buffer,
1595
+ at::Tensor& output,
1596
+ const std::optional<at::Tensor>& key,
1597
+ const std::optional<at::Tensor>& value,
1598
+ const std::optional<at::Tensor>& loc,
1599
+ at::Tensor& attn_logits,
1600
+ at::Tensor& req_to_token,
1601
+ at::Tensor& req_pool_indices,
1602
+ at::Tensor& seq_lens,
1603
+ double sm_scale,
1604
+ double logit_cap,
1605
+ bool is_cross_attn,
1606
+ int64_t sliding_window_size,
1607
+ std::optional<at::Tensor> encoder_lens,
1608
+ std::optional<at::Tensor> sinks) {
1609
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(query);
1610
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(k_buffer);
1611
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(v_buffer);
1612
+ CHECK_DIM(3, query);
1613
+ CHECK_DIM(3, k_buffer);
1614
+ CHECK_DIM(3, v_buffer);
1615
+
1616
+ int64_t num_seqs = seq_lens.size(0);
1617
+ int64_t max_num_reqs = req_to_token.size(0);
1618
+ int64_t max_context_len = req_to_token.size(1);
1619
+ int64_t max_total_num_tokens = k_buffer.size(0);
1620
+
1621
+ int64_t num_heads = query.size(1);
1622
+ int64_t num_heads_kv = k_buffer.size(1);
1623
+ int64_t head_size = query.size(2);
1624
+ int64_t head_size_v = v_buffer.size(2);
1625
+
1626
+ int64_t num_kv_splits = attn_logits.size(2);
1627
+
1628
+ CHECK_EQ(attn_logits.size(0), num_seqs);
1629
+ CHECK_EQ(attn_logits.size(1), num_heads);
1630
+ CHECK_EQ(attn_logits.size(3), head_size_v + 1);
1631
+ CHECK_EQ(attn_logits.scalar_type(), at::kFloat);
1632
+
1633
+ // strides for query
1634
+ int64_t q_strideM = query.stride(0);
1635
+ int64_t q_strideH = query.stride(1);
1636
+
1637
+ // strides for k_buffer and v_buffer
1638
+ int64_t k_strideN = k_buffer.stride(0);
1639
+ int64_t k_strideH = k_buffer.stride(1);
1640
+ int64_t v_strideN = v_buffer.stride(0);
1641
+ int64_t v_strideH = v_buffer.stride(1);
1642
+
1643
+ // check index data types
1644
+ const auto index_dtype = req_to_token.scalar_type();
1645
+ TORCH_CHECK(
1646
+ index_dtype == at::kInt || index_dtype == at::kLong,
1647
+ "decode: expect req_to_token to be int32 or int64, got ",
1648
+ index_dtype);
1649
+ TORCH_CHECK(seq_lens.scalar_type() == at::kLong, "decode: expect req_lens to be int64, got ", seq_lens.scalar_type());
1650
+ TORCH_CHECK(
1651
+ req_pool_indices.scalar_type() == at::kLong,
1652
+ "decode: expect req_pool_indices to be int64, got ",
1653
+ req_pool_indices.scalar_type());
1654
+
1655
+ // check if we have MLA here
1656
+ void* k_buffer_data = k_buffer.data_ptr();
1657
+ void* v_buffer_data = v_buffer.data_ptr();
1658
+ const bool is_mla = (k_buffer_data == v_buffer_data) && (num_heads_kv == 1) && (head_size == head_size_v + 64);
1659
+
1660
+ // block length for k_buffer and v_buffer
1661
+ constexpr int BLOCK_N = 256;
1662
+
1663
+ // buffer for packing k_cache and v_cache
1664
+ int num_threads = at::get_num_threads();
1665
+ int64_t size_per_thread = is_mla ? BLOCK_N * head_size + BLOCK_N * head_size_v : 0;
1666
+ auto buffer = at::empty({num_threads, size_per_thread}, k_buffer.options());
1667
+ bool has_encoder_lens = encoder_lens.has_value();
1668
+ // Since encoder_lens is not used when it is None, encoder_lens_t can be initialized as any tensor of int64_t dtype.
1669
+ at::Tensor encoder_lens_t = seq_lens;
1670
+ if (has_encoder_lens) {
1671
+ encoder_lens_t = encoder_lens.value();
1672
+ CHECK_EQ(encoder_lens_t.size(0), num_seqs);
1673
+ }
1674
+ bool has_sink = sinks.has_value();
1675
+ at::Tensor sinks_tensor = has_sink ? sinks.value() : at::empty({num_heads}, query.options());
1676
+ CHECK_DIM(1, sinks_tensor);
1677
+ CHECK_EQ(sinks_tensor.size(0), num_heads);
1678
+ AT_DISPATCH_REDUCED_FLOATING_TYPES(query.scalar_type(), "decode_attention_kernel", [&] {
1679
+ AT_DISPATCH_INDEX_TYPES(index_dtype, "decode_attention_indices", [&] {
1680
+ if (key.has_value()) {
1681
+ TORCH_CHECK(value.has_value(), "key and value should have values at the same time")
1682
+ TORCH_CHECK(loc.has_value(), "loc must be given when key/value are given")
1683
+ auto loc_tensor = loc.value();
1684
+ CHECK_DIM(1, loc_tensor);
1685
+ CHECK_EQ(loc_tensor.numel(), num_seqs);
1686
+ auto key_tensor = key.value();
1687
+ auto value_tensor = value.value();
1688
+ // for MLA, key and value shares the same storage and value could be non-contiguous
1689
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(key_tensor);
1690
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(value_tensor);
1691
+ CHECK_DIM(3, key_tensor);
1692
+ CHECK_DIM(3, value_tensor);
1693
+ // strides for new key and value
1694
+ int64_t nk_strideN = key_tensor.stride(0);
1695
+ int64_t nk_strideH = key_tensor.stride(1);
1696
+ int64_t nv_strideN = value_tensor.stride(0);
1697
+ int64_t nv_strideH = value_tensor.stride(1);
1698
+ // update the kv buffer
1699
+ decode_set_kv_buffer(
1700
+ (scalar_t*)k_buffer_data,
1701
+ (scalar_t*)v_buffer_data,
1702
+ key_tensor.data_ptr<scalar_t>(),
1703
+ value_tensor.data_ptr<scalar_t>(),
1704
+ loc_tensor.data_ptr<int64_t>(),
1705
+ num_seqs,
1706
+ num_heads_kv,
1707
+ head_size,
1708
+ head_size_v,
1709
+ k_strideN,
1710
+ k_strideH,
1711
+ v_strideN,
1712
+ v_strideH,
1713
+ nk_strideN,
1714
+ nk_strideH,
1715
+ nv_strideN,
1716
+ nv_strideH,
1717
+ is_mla);
1718
+ }
1719
+
1720
+ if (num_heads == num_heads_kv) {
1721
+ // MHA
1722
+ decode_attention_kernel_impl<scalar_t, index_t, BLOCK_N>(
1723
+ output.data_ptr<scalar_t>(),
1724
+ attn_logits.data_ptr<float>(),
1725
+ query.data_ptr<scalar_t>(),
1726
+ (const scalar_t*)k_buffer_data,
1727
+ (const scalar_t*)v_buffer_data,
1728
+ req_to_token.data_ptr<index_t>(),
1729
+ req_pool_indices.data_ptr<int64_t>(),
1730
+ seq_lens.data_ptr<int64_t>(),
1731
+ encoder_lens_t.data_ptr<int64_t>(),
1732
+ sinks_tensor.data_ptr<scalar_t>(),
1733
+ num_seqs,
1734
+ num_heads,
1735
+ head_size,
1736
+ head_size_v,
1737
+ num_kv_splits,
1738
+ q_strideM,
1739
+ q_strideH,
1740
+ k_strideN,
1741
+ k_strideH,
1742
+ v_strideN,
1743
+ v_strideH,
1744
+ sm_scale,
1745
+ logit_cap,
1746
+ max_num_reqs,
1747
+ max_context_len,
1748
+ max_total_num_tokens,
1749
+ sliding_window_size,
1750
+ is_cross_attn,
1751
+ has_encoder_lens,
1752
+ has_sink);
1753
+ } else if (is_mla) {
1754
+ // MLA
1755
+ decode_attention_mla_kernel_impl<scalar_t, index_t, BLOCK_N>(
1756
+ output.data_ptr<scalar_t>(),
1757
+ attn_logits.data_ptr<float>(),
1758
+ query.data_ptr<scalar_t>(),
1759
+ (const scalar_t*)k_buffer_data,
1760
+ (const scalar_t*)v_buffer_data,
1761
+ req_to_token.data_ptr<index_t>(),
1762
+ req_pool_indices.data_ptr<int64_t>(),
1763
+ seq_lens.data_ptr<int64_t>(),
1764
+ buffer.data_ptr<scalar_t>(),
1765
+ sinks_tensor.data_ptr<scalar_t>(),
1766
+ num_seqs,
1767
+ num_heads,
1768
+ head_size,
1769
+ head_size_v,
1770
+ num_kv_splits,
1771
+ q_strideM,
1772
+ q_strideH,
1773
+ k_strideN,
1774
+ k_strideH,
1775
+ v_strideN,
1776
+ v_strideH,
1777
+ sm_scale,
1778
+ logit_cap,
1779
+ max_num_reqs,
1780
+ max_context_len,
1781
+ max_total_num_tokens,
1782
+ size_per_thread,
1783
+ has_sink);
1784
+ } else {
1785
+ // GQA/MQA
1786
+ decode_attention_grouped_kernel_impl<scalar_t, index_t, BLOCK_N>(
1787
+ output.data_ptr<scalar_t>(),
1788
+ attn_logits.data_ptr<float>(),
1789
+ query.data_ptr<scalar_t>(),
1790
+ (const scalar_t*)k_buffer_data,
1791
+ (const scalar_t*)v_buffer_data,
1792
+ req_to_token.data_ptr<index_t>(),
1793
+ req_pool_indices.data_ptr<int64_t>(),
1794
+ seq_lens.data_ptr<int64_t>(),
1795
+ encoder_lens_t.data_ptr<int64_t>(),
1796
+ sinks_tensor.data_ptr<scalar_t>(),
1797
+ num_seqs,
1798
+ num_heads,
1799
+ num_heads_kv,
1800
+ head_size,
1801
+ head_size_v,
1802
+ num_kv_splits,
1803
+ q_strideM,
1804
+ q_strideH,
1805
+ k_strideN,
1806
+ k_strideH,
1807
+ v_strideN,
1808
+ v_strideH,
1809
+ sm_scale,
1810
+ logit_cap,
1811
+ max_num_reqs,
1812
+ max_context_len,
1813
+ max_total_num_tokens,
1814
+ sliding_window_size,
1815
+ is_cross_attn,
1816
+ has_encoder_lens,
1817
+ has_sink);
1818
+ }
1819
+ });
1820
+ });
1821
+}
csrc/cpu/sgl-kernels/extend.cpp
new
+568
@@ -0,0 +1,568 @@
1
+// Adapted from
2
+// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
+
4
+// clang-format off
5
+
6
+#include "common.h"
7
+#include "flash_attn.h"
8
+#include "gemm.h"
9
+
10
+namespace {
11
+
12
+// [NOTE]: extend attention for CPU
13
+// 1. BLOCK_M and BLOCK_N tuned for various seq lengths
14
+// 2. can handle non-contiguous k_extend and v_extend
15
+// 3. computes attention for prefix and extend separately
16
+// 4. TODO: apply head dimension blocking to optimize GQA
17
+// 5. optional tree mask for speculative decoding TARGET_VERIFY (EAGLE topk > 1):
18
+// `tree_mask` is a flat [batches * qlen * qlen] bool tensor in
19
+// TreeMaskMode::QLEN_ONLY layout, where qlen == extend_seq_lens[bs] ==
20
+// max_len_extend (uniform across the batch, equal to draft_token_num).
21
+// Row i = query draft token, column j = key draft token; true means query i
22
+// may attend key j (each row marks self + ancestors + root). The committed
23
+// prefix (stage 1) is implicitly fully visible to every draft token, which
24
+// is why the mask only covers the qlen x qlen new-token block; the GPU
25
+// FULL_MASK layout carries the prefix columns explicitly but they are
26
+// all-true for EAGLE. When tree_mask is absent, stage 2 falls back to the
27
+// plain causal mask (correct for non-spec extend and topk == 1 chains).
28
+//
29
+
30
+template <typename scalar_t, typename index_t, int BLOCK_M, int BLOCK_N>
31
+void extend_attention_kernel_impl(
32
+ scalar_t* __restrict__ o_extend,
33
+ const scalar_t* __restrict__ q_extend,
34
+ const scalar_t* __restrict__ k_extend,
35
+ const scalar_t* __restrict__ v_extend,
36
+ const scalar_t* __restrict__ k_buffer,
37
+ const scalar_t* __restrict__ v_buffer,
38
+ const index_t* __restrict__ req_to_token,
39
+ const int64_t* __restrict__ req_pool_indices,
40
+ const int64_t* __restrict__ seq_lens,
41
+ const int64_t* __restrict__ encoder_lens,
42
+ const index_t* __restrict__ extend_seq_lens,
43
+ const index_t* __restrict__ extend_start_loc,
44
+ const void* __restrict__ buffer,
45
+ const scalar_t* __restrict__ sinks,
46
+ const bool* __restrict__ tree_mask,
47
+ int batches,
48
+ int num_heads,
49
+ int num_heads_kv,
50
+ int head_size,
51
+ int head_size_v,
52
+ int q_strideM,
53
+ int q_strideH,
54
+ int ke_strideN,
55
+ int ke_strideH,
56
+ int ve_strideN,
57
+ int ve_strideH,
58
+ int k_strideN,
59
+ int k_strideH,
60
+ int v_strideN,
61
+ int v_strideH,
62
+ float sm_scale,
63
+ int max_num_reqs,
64
+ int max_context_len,
65
+ int max_total_num_tokens,
66
+ int max_len_extend,
67
+ int buffer_size_per_thread,
68
+ int64_t sliding_window_size,
69
+ bool is_prefix_skipped,
70
+ bool is_cross_attn,
71
+ bool has_encoder_lens,
72
+ bool has_sink) {
73
+ // strides
74
+ const int o_strideM = num_heads * head_size_v;
75
+ const int o_strideH = head_size_v;
76
+
77
+ // we use same buffer for packed key and value
78
+ const int ldb_tmp = std::max(head_size, head_size_v);
79
+
80
+ const int num_groups = num_heads / num_heads_kv;
81
+ TORCH_CHECK(num_groups * num_heads_kv == num_heads);
82
+
83
+ // number of blocks along M
84
+ int MB = div_up(max_len_extend, BLOCK_M);
85
+
86
+ // parallel on [batches, num_heads, BM]
87
+ at::parallel_for(0, batches * num_heads * MB, 0, [&](int begin, int end) {
88
+ int bs{0}, head_id{0}, mb{0};
89
+ data_index_init(begin, bs, batches, head_id, num_heads, mb, MB);
90
+
91
+ int tid = at::get_thread_num();
92
+ // s_i: [BLOCK_M, BLOCK_N]
93
+ float* __restrict__ s_i = reinterpret_cast<float*>((char*)(buffer) + tid * buffer_size_per_thread);
94
+
95
+ // v_prime: [BLOCK_M, head_size_v]
96
+ float* __restrict__ v_prime = s_i + BLOCK_M * BLOCK_N;
97
+
98
+ // s_delta: [BLOCK_M, BLOCK_N]
99
+ scalar_t* __restrict__ s_delta = reinterpret_cast<scalar_t*>(v_prime + BLOCK_M * head_size_v);
100
+
101
+ // Btmp: [BLOCK_N, max(head_size, head_size_v)]
102
+ scalar_t* __restrict__ Btmp = reinterpret_cast<scalar_t*>(s_delta + BLOCK_M * BLOCK_N);
103
+
104
+ // init Btmp just once for each thread to prevent NaN
105
+ fill_stub(Btmp, 0.f, BLOCK_N * ldb_tmp);
106
+ fill_stub(s_delta, 0.f, BLOCK_M * BLOCK_N);
107
+
108
+ alignas(64) float s_prime[BLOCK_M];
109
+ alignas(64) float m_prime[BLOCK_M];
110
+
111
+ for (int i = begin; i < end; ++i) {
112
+ // seq_len = prefix + extend
113
+ int head_kv_id = head_id / num_groups;
114
+ int seq_len = seq_lens[bs];
115
+ int seq_len_extend = extend_seq_lens[bs];
116
+ int seq_len_prefix = seq_len - seq_len_extend;
117
+ int seq_extend_start_loc = extend_start_loc[bs];
118
+
119
+ int req_pool_id = req_pool_indices[bs];
120
+ int kv_offset = (has_encoder_lens && (!is_cross_attn)) ? encoder_lens[bs] : 0;
121
+ TORCH_CHECK(seq_len_prefix >= 0, "prefix len < 0!");
122
+ TORCH_CHECK(seq_len <= max_context_len, "seq_len out of scope!");
123
+ TORCH_CHECK(req_pool_id < max_num_reqs, "req_pool_id out of scope!");
124
+
125
+ if (is_prefix_skipped) {
126
+ TORCH_CHECK(seq_len_prefix == 0, "extend attention: expect seq_len_prefix to be 0, got ", seq_len_prefix);
127
+ }
128
+
129
+ if (tree_mask != nullptr) {
130
+ // QLEN_ONLY layout assumes a uniform qlen across the batch (TARGET_VERIFY)
131
+ TORCH_CHECK(
132
+ seq_len_extend == max_len_extend,
133
+ "extend attention: tree_mask requires uniform extend_seq_lens, got ",
134
+ seq_len_extend,
135
+ " vs ",
136
+ max_len_extend);
137
+ }
138
+
139
+ // offset and size in MB
140
+ int m = mb * BLOCK_M;
141
+ int m_size = std::min(BLOCK_M, seq_len_extend - m);
142
+
143
+ if (m_size <= 0) {
144
+ data_index_step(bs, batches, head_id, num_heads, mb, MB);
145
+ continue;
146
+ }
147
+
148
+ // get query
149
+ const scalar_t* __restrict__ q_ptr = q_extend + (seq_extend_start_loc + m) * q_strideM + head_id * q_strideH;
150
+
151
+ // init v', s' and m'
152
+ fill_stub(v_prime, 0.f, m_size * head_size_v);
153
+ fill_stub(s_prime, 0.f, m_size);
154
+ fill_stub(m_prime, -std::numeric_limits<scalar_t>::infinity(), m_size);
155
+ // stage 1: compute scores with prefix
156
+ int kv_start = 0;
157
+ int kv_end = is_cross_attn ? encoder_lens[bs] : seq_len_prefix;
158
+ for (int n = kv_start; n < kv_end; n += BLOCK_N) {
159
+ int n_size = std::min(BLOCK_N, kv_end - n);
160
+
161
+ // `n_size` is K in 2nd gemm, pad to TILE_K;
162
+ const int padded_n_size = div_up(n_size, TILE_K) * TILE_K;
163
+
164
+ // get key and pack
165
+ pack_vnni<scalar_t, index_t>(
166
+ /* dst */ Btmp,
167
+ /* src */ k_buffer + head_kv_id * k_strideH,
168
+ /* ind */ req_to_token + req_pool_id * max_context_len + n + kv_offset,
169
+ /* N */ n_size,
170
+ /* K */ head_size,
171
+ /* ld_src */ k_strideN,
172
+ /* ld_dst */ BLOCK_N);
173
+
174
+ // calculate s_i <- Q @ K
175
+ at::native::cpublas::brgemm(
176
+ /* M */ m_size,
177
+ /* N */ n_size,
178
+ /* K */ head_size,
179
+ /* lda */ q_strideM,
180
+ /* ldb */ BLOCK_N,
181
+ /* ldc */ BLOCK_N,
182
+ /* add_C */ false,
183
+ /* A */ q_ptr,
184
+ /* B */ Btmp,
185
+ /* C */ s_i);
186
+
187
+ for (int row = 0; row < m_size; ++row) {
188
+ if (sliding_window_size > 0) {
189
+ int last_col = seq_len_prefix + row + m - sliding_window_size + 1;
190
+ if (last_col >= n + n_size) {
191
+ continue;
192
+ }
193
+ fill_stub(s_i + row * BLOCK_N, -std::numeric_limits<float>::infinity(), last_col - n);
194
+ }
195
+ flash_attn_softmax<scalar_t, BLOCK_M, BLOCK_N>::apply(
196
+ s_i, s_delta, v_prime, s_prime, m_prime, m_size, n_size, padded_n_size, head_size_v, sm_scale, row);
197
+ }
198
+
199
+ // get value and pack
200
+ pack_vnni2<scalar_t>(
201
+ /* dst */ Btmp,
202
+ /* src */ v_buffer + head_kv_id * v_strideH,
203
+ /* ind */ req_to_token + req_pool_id * max_context_len + n + kv_offset,
204
+ /* K */ n_size,
205
+ /* N */ head_size_v,
206
+ /* ld_src */ v_strideN,
207
+ /* ld_dst */ head_size_v);
208
+
209
+ // calculate V' <- s_delta @ V + V'
210
+ at::native::cpublas::brgemm(
211
+ /* M */ m_size,
212
+ /* N */ head_size_v,
213
+ /* K */ padded_n_size, // n_size
214
+ /* lda */ BLOCK_N,
215
+ /* ldb */ head_size_v,
216
+ /* ldc */ head_size_v,
217
+ /* add_C */ true,
218
+ /* A */ s_delta,
219
+ /* B */ Btmp,
220
+ /* C */ v_prime);
221
+ } // loop with seq_len_prefix
222
+ if (!is_cross_attn) {
223
+ // stage 2: compute the triangle part
224
+ int num_keys = std::min(seq_len_extend, m + BLOCK_M);
225
+ for (int n = 0; n < num_keys; n += BLOCK_N) {
226
+ int n_size = std::min(BLOCK_N, num_keys - n);
227
+
228
+ // `n_size` is K in 2nd gemm, pad to TILE_K;
229
+ const int padded_n_size = div_up(n_size, TILE_K) * TILE_K;
230
+
231
+ // get key and pack
232
+ pack_vnni<scalar_t>(
233
+ /* dst */ Btmp,
234
+ /* src */ k_extend + (seq_extend_start_loc + n) * ke_strideN + head_kv_id * ke_strideH,
235
+ /* N */ n_size,
236
+ /* K */ head_size,
237
+ /* ld_src */ ke_strideN,
238
+ /* ld_dst */ BLOCK_N);
239
+
240
+ // calculate s_i <- Q @ K
241
+ at::native::cpublas::brgemm(
242
+ /* M */ m_size,
243
+ /* N */ n_size,
244
+ /* K */ head_size,
245
+ /* lda */ q_strideM,
246
+ /* ldb */ BLOCK_N,
247
+ /* ldc */ BLOCK_N,
248
+ /* add_C */ false,
249
+ /* A */ q_ptr,
250
+ /* B */ Btmp,
251
+ /* C */ s_i);
252
+
253
+ // apply tree mask (speculative TARGET_VERIFY) or causal mask
254
+ if (tree_mask != nullptr) {
255
+ // [Note] tree mask for EAGLE topk > 1 (TreeMaskMode::QLEN_ONLY).
256
+ // mask[bs][m + row][n + col] == false -> query draft token (m + row)
257
+ // may not attend key draft token (n + col); set the score to -inf
258
+ // before softmax. The tree mask subsumes the causal constraint:
259
+ // ancestors always precede descendants in the draft token ordering,
260
+ // so permitted keys satisfy j <= i and the causal `num_keys` bound
261
+ // above remains valid.
262
+ const bool* __restrict__ mask_base =
263
+ tree_mask + (static_cast<int64_t>(bs) * seq_len_extend + m) * seq_len_extend + n;
264
+ for (int row = 0; row < m_size; ++row) {
265
+ float* __restrict__ row_ptr = s_i + row * BLOCK_N;
266
+ const bool* __restrict__ mask_ptr = mask_base + static_cast<int64_t>(row) * seq_len_extend;
267
+ for (int col = 0; col < n_size; ++col) {
268
+ if (!mask_ptr[col]) {
269
+ row_ptr[col] = -std::numeric_limits<float>::infinity();
270
+ }
271
+ }
272
+ }
273
+ } else if (n + n_size - 1 > m) {
274
+ // apply causal mask
275
+ // [Note] condition to apply causal mask.
276
+ // Mask any block whose last key (n + n_size - 1) is strictly after the first query position (m), i.e. n +
277
+ // n_size - 1 > m. The original condition was `num_keys - n <= BLOCK_N` (last n-block only). That was
278
+ // correct when BLOCK_M <= BLOCK_N/2 because earlier n-blocks were guaranteed to contain only past keys.
279
+ // With BLOCK_M=512, BLOCK_N=768:
280
+ // BLOCK_M > BLOCK_N/2, so the first n-block can contain future keys.
281
+ // Example: m=512 (mb=1), num_keys=1024, first n-block covers keys [0, 768).
282
+ // Query row=0 is at position 512, so keys 513..767 are future and must be
283
+ // masked — but `num_keys - 0 = 1024 > BLOCK_N` skips masking entirely,
284
+ // producing wrong (non-causal) attention for rows 0..254 of this m-block.
285
+ for (int row = 0; row < m_size; ++row) {
286
+ int last_col = m + row - n;
287
+ // [Note] mask the entire row if last_col < 0.
288
+ // Clamp to -1: when n > m + row every key in this block is a future
289
+ // key, so the entire row should be masked. Without this clamp,
290
+ // last_col+1 <= 0 and fill_stub would write before row_ptr.
291
+ last_col = std::max(last_col, -1);
292
+ // fill [last_col + 1, n_size) to -inf
293
+ float* row_ptr = s_i + row * BLOCK_N;
294
+ fill_stub(row_ptr + last_col + 1, -std::numeric_limits<float>::infinity(), n_size - last_col - 1);
295
+ }
296
+ }
297
+
298
+ for (int row = 0; row < m_size; ++row) {
299
+ if (sliding_window_size > 0 && row + m + 1 >= n + sliding_window_size - 1 &&
300
+ row + m + 1 < n + sliding_window_size + n_size) {
301
+ fill_stub(
302
+ s_i + row * BLOCK_N, -std::numeric_limits<float>::infinity(), row + m - n - sliding_window_size + 1);
303
+ } else if (sliding_window_size > 0 && row + m + 1 >= n + sliding_window_size) {
304
+ continue;
305
+ }
306
+ flash_attn_softmax<scalar_t, BLOCK_M, BLOCK_N>::apply(
307
+ s_i, s_delta, v_prime, s_prime, m_prime, m_size, n_size, padded_n_size, head_size_v, sm_scale, row);
308
+ }
309
+
310
+ // get value and pack
311
+ pack_vnni2<scalar_t>(
312
+ /* dst */ Btmp,
313
+ /* src */ v_extend + (seq_extend_start_loc + n) * ve_strideN + head_kv_id * ve_strideH,
314
+ /* K */ n_size,
315
+ /* N */ head_size_v,
316
+ /* ld_src */ ve_strideN,
317
+ /* ld_dst */ head_size_v);
318
+
319
+ // calculate V' <- s_delta @ V + V'
320
+ at::native::cpublas::brgemm(
321
+ /* M */ m_size,
322
+ /* N */ head_size_v,
323
+ /* K */ padded_n_size, // n_size
324
+ /* lda */ BLOCK_N,
325
+ /* ldb */ head_size_v,
326
+ /* ldc */ head_size_v,
327
+ /* add_C */ true,
328
+ /* A */ s_delta,
329
+ /* B */ Btmp,
330
+ /* C */ v_prime);
331
+ } // loop with seq_len_extend
332
+ }
333
+ scalar_t* __restrict__ out_ptr = o_extend + (seq_extend_start_loc + m) * o_strideM + head_id * o_strideH;
334
+ for (int row = 0; row < m_size; ++row) {
335
+ if (has_sink) {
336
+ s_prime[row] += std::exp(sinks[head_id] - m_prime[row]);
337
+ }
338
+ float s = 1 / s_prime[row];
339
+ copy_stub<scalar_t>(out_ptr + row * o_strideM, v_prime + row * head_size_v, s, head_size_v);
340
+ }
341
+
342
+ // move to the next index
343
+ data_index_step(bs, batches, head_id, num_heads, mb, MB);
344
+ }
345
+ at::native::cpublas::brgemm_release();
346
+ });
347
+}
348
+
349
+} // anonymous namespace
350
+
351
+template <int BLOCK_M, int BLOCK_N>
352
+inline int resize_buffer(at::Tensor& buffer, int num_threads, int head_size, int head_size_v) {
353
+ static_assert(BLOCK_M <= BLOCK_N, "Make sure BLOCK_M <= BLOCK_N to prevent buffer overflows during causal masking");
354
+ const int size_per_thread =
355
+ /* s_i */ BLOCK_M * BLOCK_N * sizeof(float) +
356
+ /* v_prime */ BLOCK_M * head_size_v * sizeof(float) +
357
+ /* s_delta */ BLOCK_M * BLOCK_N * sizeof(uint16_t) +
358
+ /* Btmp */ BLOCK_N * std::max(head_size, head_size_v) * sizeof(uint16_t);
359
+
360
+ buffer.resize_({num_threads, size_per_thread});
361
+ return size_per_thread;
362
+}
363
+
364
+#define LAUNCH_EXTEND_ATTENTION_KERNEL(BLOCK_M, BLOCK_N) \
365
+ do { \
366
+ int sz = resize_buffer<BLOCK_M, BLOCK_N>(buffer, num_threads, head_size, head_size_v); \
367
+ \
368
+ extend_attention_kernel_impl<scalar_t, index_t, BLOCK_M, BLOCK_N>( \
369
+ o_extend.data_ptr<scalar_t>(), \
370
+ q_extend.data_ptr<scalar_t>(), \
371
+ k_extend.data_ptr<scalar_t>(), \
372
+ v_extend.data_ptr<scalar_t>(), \
373
+ k_buffer.data_ptr<scalar_t>(), \
374
+ v_buffer.data_ptr<scalar_t>(), \
375
+ req_to_token.data_ptr<index_t>(), \
376
+ req_pool_indices.data_ptr<int64_t>(), \
377
+ seq_lens.data_ptr<int64_t>(), \
378
+ encoder_lens_t.data_ptr<int64_t>(), \
379
+ extend_seq_lens.data_ptr<index_t>(), \
380
+ extend_start_loc.data_ptr<index_t>(), \
381
+ buffer.data_ptr(), \
382
+ sinks_tensor.data_ptr<scalar_t>(), \
383
+ tree_mask_ptr, \
384
+ num_seqs, \
385
+ num_heads, \
386
+ num_heads_kv, \
387
+ head_size, \
388
+ head_size_v, \
389
+ q_strideM, \
390
+ q_strideH, \
391
+ ke_strideN, \
392
+ ke_strideH, \
393
+ ve_strideN, \
394
+ ve_strideH, \
395
+ k_strideN, \
396
+ k_strideH, \
397
+ v_strideN, \
398
+ v_strideH, \
399
+ sm_scale, \
400
+ max_num_reqs, \
401
+ max_context_len, \
402
+ max_total_num_tokens, \
403
+ max_len_extend, \
404
+ sz, \
405
+ sliding_window_size, \
406
+ is_prefix_skipped, \
407
+ is_cross_attn, \
408
+ has_encoder_lens, \
409
+ has_sink); \
410
+ } while (0)
411
+
412
+// q_extend, k_extend, v_extend, o_extend: contiguous tensors
413
+// k_buffer, v_buffer: (prefix + extend) tensors in mem_manager
414
+//
415
+// q_extend: [num_tokens, num_heads, head_size]
416
+// k_extend: [num_extend_tokens, num_heads, head_size]
417
+// v_extend: [num_extend_tokens, num_heads, head_size]
418
+// o_extend: [num_tokens, num_heads, head_size]
419
+// k_buffer: [max_total_num_tokens, num_heads, head_size]
420
+// v_buffer: [max_total_num_tokens, num_heads, head_size]
421
+// req_to_token: [max_num_reqs, max_context_len] int32 or int64
422
+// req_pool_indices: [num_seqs] int64
423
+// seq_lens: [num_seqs] int64
424
+// extend_seq_lens: [num_seqs]
425
+// extend_start_loc: [num_seqs]
426
+// encoder_lens: [num_seqs] int64 or None
427
+// sinks: [num_heads] or None
428
+// tree_mask: [num_seqs * max_len_extend * max_len_extend] bool or None
429
+// TreeMaskMode::QLEN_ONLY tree mask for speculative TARGET_VERIFY; see [NOTE] 5 above.
430
+void extend_attention_cpu(
431
+ at::Tensor& q_extend,
432
+ const std::optional<at::Tensor>& k_extend_opt,
433
+ const std::optional<at::Tensor>& v_extend_opt,
434
+ at::Tensor& o_extend,
435
+ at::Tensor& k_buffer,
436
+ at::Tensor& v_buffer,
437
+ at::Tensor& req_to_token,
438
+ at::Tensor& req_pool_indices,
439
+ at::Tensor& seq_lens,
440
+ at::Tensor& extend_seq_lens,
441
+ at::Tensor& extend_start_loc,
442
+ int64_t max_len_extend,
443
+ double sm_scale,
444
+ double logit_cap,
445
+ bool is_cross_attn,
446
+ int64_t sliding_window_size,
447
+ std::optional<at::Tensor> encoder_lens,
448
+ std::optional<at::Tensor> sinks,
449
+ std::optional<at::Tensor> tree_mask) {
450
+ if (!is_cross_attn) {
451
+ TORCH_CHECK(
452
+ k_extend_opt.has_value() && v_extend_opt.has_value(),
453
+ "k_extend and v_extend are required for non-cross attention");
454
+ }
455
+ // Since k_extend and v_extend are not used for cross attention, they can be initialized as k_buffer and v_buffer
456
+ // here.
457
+ auto k_extend = k_extend_opt.has_value() ? k_extend_opt.value() : k_buffer;
458
+ auto v_extend = v_extend_opt.has_value() ? v_extend_opt.value() : v_buffer;
459
+
460
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(q_extend);
461
+ CHECK_INPUT(o_extend);
462
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(k_extend);
463
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(v_extend);
464
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(k_buffer);
465
+ CHECK_LAST_DIM_CONTIGUOUS_INPUT(v_buffer);
466
+
467
+ int num_seqs = seq_lens.size(0);
468
+ int max_num_reqs = req_to_token.size(0);
469
+ int max_context_len = req_to_token.size(1);
470
+ int max_total_num_tokens = k_buffer.size(0);
471
+
472
+ int num_heads = q_extend.size(1);
473
+ int num_heads_kv = k_extend.size(1);
474
+ int head_size = q_extend.size(2);
475
+ int head_size_v = v_extend.size(2);
476
+
477
+ // strides for q_extend, k_extend and v_extend
478
+ int q_strideM = q_extend.stride(0);
479
+ int q_strideH = q_extend.stride(1);
480
+ int ke_strideN = k_extend.stride(0);
481
+ int ke_strideH = k_extend.stride(1);
482
+ int ve_strideN = v_extend.stride(0);
483
+ int ve_strideH = v_extend.stride(1);
484
+
485
+ // strides for k_buffer and v_buffer
486
+ int k_strideN = k_buffer.stride(0);
487
+ int k_strideH = k_buffer.stride(1);
488
+ int v_strideN = v_buffer.stride(0);
489
+ int v_strideH = v_buffer.stride(1);
490
+
491
+ // check sizes
492
+ CHECK_EQ(req_pool_indices.size(0), num_seqs);
493
+ CHECK_EQ(extend_seq_lens.size(0), num_seqs);
494
+ CHECK_EQ(extend_start_loc.size(0), num_seqs);
495
+ CHECK_EQ(v_extend.size(1), num_heads_kv);
496
+ CHECK_EQ(k_buffer.size(1), v_buffer.size(1));
497
+
498
+ // MLA will skip prefix part
499
+ const bool is_prefix_skipped = k_buffer.size(1) != num_heads_kv;
500
+
501
+ // check index data types
502
+ const auto index_dtype = req_to_token.scalar_type();
503
+ TORCH_CHECK(
504
+ index_dtype == at::kInt || index_dtype == at::kLong,
505
+ "extend: expect req_to_token to be int32 or int64, got ",
506
+ index_dtype);
507
+ TORCH_CHECK(seq_lens.scalar_type() == at::kLong, "extend: expect req_lens to be int64, got ", seq_lens.scalar_type());
508
+ TORCH_CHECK(
509
+ req_pool_indices.scalar_type() == at::kLong,
510
+ "extend: expect req_pool_indices to be int64, got ",
511
+ req_pool_indices.scalar_type());
512
+ TORCH_CHECK(
513
+ extend_seq_lens.scalar_type() == index_dtype && extend_start_loc.scalar_type() == index_dtype,
514
+ "extend: expect extend_seq_lens and extend_start_loc to have same dtype as req_to_token.");
515
+
516
+ // D and DV need to be 32x as we transpose by 512-bit
517
+ TORCH_CHECK(head_size % 32 == 0, "invalid head_size ", head_size);
518
+ TORCH_CHECK(head_size_v % 32 == 0, "invalid head_size_v ", head_size_v);
519
+
520
+ int num_threads = at::get_num_threads();
521
+ auto buffer = at::empty({}, q_extend.options().dtype(at::kChar));
522
+
523
+ bool has_encoder_lens = encoder_lens.has_value();
524
+ // Since encoder_lens is not used when it is None, encoder_lens_t can be initialized as any tensor of int64_t dtype.
525
+ at::Tensor encoder_lens_t = seq_lens;
526
+ if (has_encoder_lens) {
527
+ encoder_lens_t = encoder_lens.value();
528
+ CHECK_EQ(encoder_lens_t.size(0), num_seqs);
529
+ }
530
+ bool has_sink = sinks.has_value();
531
+ at::Tensor sinks_tensor = has_sink ? sinks.value() : at::empty({num_heads}, q_extend.options());
532
+ CHECK_DIM(1, sinks_tensor);
533
+ CHECK_EQ(sinks_tensor.size(0), num_heads);
534
+
535
+ const bool* tree_mask_ptr = nullptr;
536
+ if (tree_mask.has_value()) {
537
+ const at::Tensor& tree_mask_t = tree_mask.value();
538
+ CHECK_INPUT(tree_mask_t);
539
+ TORCH_CHECK(
540
+ tree_mask_t.scalar_type() == at::kBool, "extend: expect tree_mask to be bool, got ", tree_mask_t.scalar_type());
541
+ TORCH_CHECK(
542
+ tree_mask_t.numel() == static_cast<int64_t>(num_seqs) * max_len_extend * max_len_extend,
543
+ "extend: expect tree_mask numel to be num_seqs * max_len_extend^2 = ",
544
+ static_cast<int64_t>(num_seqs) * max_len_extend * max_len_extend,
545
+ ", got ",
546
+ tree_mask_t.numel());
547
+ TORCH_CHECK(!is_cross_attn, "extend: tree_mask is not supported for cross attention");
548
+ // The window mask derives query positions from the row index
549
+ // (seq_len_prefix + m + row), but tree-mask rows sit at their tree depth,
550
+ // which is <= the row index; combining the two would over-mask the prefix.
551
+ TORCH_CHECK(sliding_window_size <= 0, "extend: tree_mask is not supported with sliding window attention");
552
+ tree_mask_ptr = tree_mask_t.data_ptr<bool>();
553
+ }
554
+
555
+ AT_DISPATCH_REDUCED_FLOATING_TYPES(q_extend.scalar_type(), "extend_attention_kernel", [&] {
556
+ AT_DISPATCH_INDEX_TYPES(index_dtype, "extend_attention_indices", [&] {
557
+ if (max_len_extend <= 256) {
558
+ LAUNCH_EXTEND_ATTENTION_KERNEL(32, 64);
559
+ } else if (max_len_extend <= 1024) {
560
+ LAUNCH_EXTEND_ATTENTION_KERNEL(128, 256);
561
+ } else if (max_len_extend <= 4096) {
562
+ LAUNCH_EXTEND_ATTENTION_KERNEL(256, 768);
563
+ } else { // max_len_extend > 4096
564
+ LAUNCH_EXTEND_ATTENTION_KERNEL(512, 768);
565
+ }
566
+ });
567
+ });
568
+}
csrc/cpu/sgl-kernels/flash_attn.h
new
+250
@@ -0,0 +1,250 @@
1
+// Adapted from
2
+// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
+
4
+// clang-format off
5
+
6
+#pragma once
7
+#include "common.h"
8
+#include "vec.h"
9
+#include "vec_pack.h"
10
+
11
+template <typename scalar_t>
12
+inline void fill_stub(scalar_t* __restrict__ out, float val, int size) {
13
+ using Vec = at::vec::Vectorized<scalar_t>;
14
+ constexpr int kVecSize = Vec::size();
15
+ const Vec data_vec = Vec(static_cast<scalar_t>(val));
16
+ int d = 0;
17
+#pragma GCC unroll 4
18
+ for (; d <= size - kVecSize; d += kVecSize) {
19
+ data_vec.store(out + d);
20
+ }
21
+ if (size - d > 0) {
22
+ data_vec.store(out + d, size - d);
23
+ }
24
+}
25
+
26
+template <typename scalar_t, int BLOCK_N>
27
+inline void copy_stub(scalar_t* __restrict__ out, const float* __restrict__ input) {
28
+ static_assert(BLOCK_N % 32 == 0);
29
+ using bVec = at::vec::Vectorized<scalar_t>;
30
+ using fVec = at::vec::Vectorized<float>;
31
+
32
+ constexpr int COLS = BLOCK_N / 16;
33
+ auto store = [&](auto i) {
34
+ constexpr int col = i % COLS;
35
+ // for COLS = 2, 4 use 512bit store
36
+ if constexpr (col % 2 == 0) {
37
+ auto [a_fvec0, a_fvec1] = load_float_vec2(input + col * 16);
38
+ bVec out_bvec = convert_from_float_ext<scalar_t>(a_fvec0, a_fvec1);
39
+ out_bvec.store(out + col * 16);
40
+ }
41
+ };
42
+ Unroll<COLS>{}(store);
43
+}
44
+
45
+template <typename scalar_t>
46
+inline void copy_stub(scalar_t* __restrict__ out, const float* __restrict__ acc, float s, int size) {
47
+ using bVec = at::vec::Vectorized<scalar_t>;
48
+ using fVec = at::vec::Vectorized<float>;
49
+ constexpr int kVecSize = bVec::size();
50
+ const fVec s_fvec = fVec(s);
51
+ int d = 0;
52
+#pragma GCC unroll 4
53
+ for (; d <= size - kVecSize; d += kVecSize) {
54
+ auto [a_fvec0, a_fvec1] = load_float_vec2(acc + d);
55
+ a_fvec0 = a_fvec0 * s_fvec;
56
+ a_fvec1 = a_fvec1 * s_fvec;
57
+ bVec out_bvec = convert_from_float_ext<scalar_t>(a_fvec0, a_fvec1);
58
+ out_bvec.store(out + d);
59
+ }
60
+ for (; d < size; ++d) {
61
+ out[d] = static_cast<scalar_t>(acc[d] * s);
62
+ }
63
+}
64
+
65
+#if defined(CPU_CAPABILITY_AVX512)
66
+template <>
67
+inline void copy_stub<at::BFloat16>(at::BFloat16* __restrict__ out, const float* __restrict__ acc, float s, int size) {
68
+ const __m512 vscale = _mm512_set1_ps(s);
69
+ int d = 0;
70
+#pragma GCC unroll 4
71
+ for (; d <= size - 32; d += 32) {
72
+ __m512 va0 = _mm512_mul_ps(_mm512_loadu_ps(acc + d), vscale);
73
+ __m512 va1 = _mm512_mul_ps(_mm512_loadu_ps(acc + d + 16), vscale);
74
+ __m512i vb = (__m512i)(_mm512_cvtne2ps_pbh(va1, va0));
75
+ _mm512_storeu_si512(out + d, vb);
76
+ }
77
+ int remainder = size - d;
78
+ if (remainder > 0) {
79
+ if (remainder <= 16) {
80
+ const __mmask16 vmask = (1ULL << remainder) - 1;
81
+ __m512 va = _mm512_mul_ps(_mm512_maskz_loadu_ps(vmask, acc + d), vscale);
82
+ __m256i vb = (__m256i)(_mm512_cvtneps_pbh(va));
83
+ _mm256_mask_storeu_epi16(reinterpret_cast<__m256i*>(out + d), vmask, vb);
84
+ } else { // remainder > 16
85
+ const __mmask16 vmask = (1ULL << (remainder - 16)) - 1;
86
+ __m512 va0 = _mm512_mul_ps(_mm512_loadu_ps(acc + d), vscale);
87
+ __m512 va1 = _mm512_mul_ps(_mm512_maskz_loadu_ps(vmask, acc + d + 16), vscale);
88
+ __m512i vb = (__m512i)(_mm512_cvtne2ps_pbh(va1, va0));
89
+ const __mmask32 vmask2 = (1ULL << remainder) - 1;
90
+ _mm512_mask_storeu_epi16(reinterpret_cast<__m512i*>(out + d), vmask2, vb);
91
+ }
92
+ }
93
+}
94
+#endif
95
+
96
+template <typename scalar_t, int BLOCK_M, int BLOCK_N>
97
+struct flash_attn_softmax {
98
+ static inline void apply(
99
+ float* __restrict__ s_i,
100
+ scalar_t* __restrict__ s_delta2,
101
+ float* __restrict__ v_prime,
102
+ float* __restrict__ s_prime,
103
+ float* __restrict__ m_prime,
104
+ int m_size,
105
+ int n_size,
106
+ int padded_n_size,
107
+ int head_size_v,
108
+ const float sm_scale,
109
+ int row) {
110
+ using Vec = at::vec::Vectorized<float>;
111
+ const Vec scale_vec = Vec(sm_scale);
112
+ float* s_delta = s_i;
113
+ // s_i <- s_i * scale
114
+ at::vec::map<float>([scale_vec](Vec x) { return x * scale_vec; }, s_i + row * BLOCK_N, s_i + row * BLOCK_N, n_size);
115
+
116
+ // m_i: max value per row
117
+ float m_i =
118
+ at::vec::reduce_all<float>([](Vec& x, Vec& y) { return at::vec::maximum(x, y); }, s_i + row * BLOCK_N, n_size);
119
+ m_i = std::max(m_i, m_prime[row]);
120
+
121
+ // m_delta <- exp(m' - m_i)
122
+ float m_delta = std::exp(m_prime[row] - m_i);
123
+
124
+ // s_delta <- exp(s_i - m_i)
125
+ at::vec::map<float>(
126
+ [m_i](Vec x) { return (x - Vec(m_i)).fexp_u20(); }, s_delta + row * BLOCK_N, s_i + row * BLOCK_N, n_size);
127
+
128
+ // s' <- s' * m_delta + sum(s_delta)
129
+ s_prime[row] *= m_delta;
130
+ s_prime[row] += at::vec::reduce_all<float>([](Vec& x, Vec& y) { return x + y; }, s_delta + row * BLOCK_N, n_size);
131
+
132
+ m_prime[row] = m_i;
133
+
134
+ // v' <- v' * m_delta
135
+ at::vec::map<float>(
136
+ [m_delta](Vec x) { return x * Vec(m_delta); },
137
+ v_prime + row * head_size_v,
138
+ v_prime + row * head_size_v,
139
+ head_size_v);
140
+
141
+ // Keep s_delta row-major for the following brgemm(P @ V), and only
142
+ // convert the columns that brgemm will consume.
143
+ fill_stub(s_delta + row * BLOCK_N + n_size, 0.f, padded_n_size - n_size);
144
+ copy_stub<scalar_t>(s_delta2 + row * BLOCK_N, s_delta + row * BLOCK_N, 1.f, padded_n_size);
145
+ }
146
+};
147
+
148
+#if defined(CPU_CAPABILITY_AVX512)
149
+template <int BLOCK_M, int BLOCK_N>
150
+struct flash_attn_softmax<at::BFloat16, BLOCK_M, BLOCK_N> {
151
+ static inline void apply(
152
+ float* __restrict__ s_i,
153
+ at::BFloat16* __restrict__ s_delta2,
154
+ float* __restrict__ v_prime,
155
+ float* __restrict__ s_prime,
156
+ float* __restrict__ m_prime,
157
+ int m_size,
158
+ int n_size,
159
+ int padded_n_size,
160
+ int head_size_v,
161
+ const float sm_scale,
162
+ int row) {
163
+ float* s_delta = s_i;
164
+ const __m512 vscale = _mm512_set1_ps(sm_scale);
165
+
166
+ int n_remainder = n_size & 15; // 0xF
167
+ const __mmask16 vmask = (1ULL << n_remainder) - 1;
168
+
169
+ int v_remainder = head_size_v & 15; // 0xF
170
+ const __mmask16 vmask1 = (1ULL << v_remainder) - 1;
171
+
172
+ constexpr float NEG_INF = -std::numeric_limits<float>::infinity();
173
+
174
+ __m512 va;
175
+ __m256i vb;
176
+ __m512 vmax;
177
+ __m512 vsum;
178
+ __m512 vmdelta;
179
+
180
+ const __m512 vneg_inf = _mm512_set1_ps(NEG_INF);
181
+
182
+ int m = row;
183
+ vmax = vneg_inf;
184
+
185
+ // s_i <- s_i * scale
186
+ int n = 0;
187
+ for (; n <= n_size - 16; n += 16) {
188
+ va = _mm512_mul_ps(_mm512_loadu_ps(s_i + m * BLOCK_N + n), vscale);
189
+ vmax = _mm512_max_ps(va, vmax);
190
+ }
191
+ if (n_remainder > 0) {
192
+ va = _mm512_mul_ps(_mm512_mask_loadu_ps(vneg_inf, vmask, s_i + m * BLOCK_N + n), vscale);
193
+ vmax = _mm512_max_ps(va, vmax);
194
+ }
195
+
196
+ // m_i: max value per row
197
+ float m_i = _mm512_reduce_max_ps(vmax);
198
+ m_i = std::max(m_i, m_prime[m]);
199
+ vmax = _mm512_set1_ps(m_i);
200
+
201
+ // m_delta <- exp(m' - m_i)
202
+ float m_delta = std::exp(m_prime[m] - m_i);
203
+
204
+ // s_delta <- exp(s_i - m_i)
205
+ vsum = _mm512_setzero_ps();
206
+ for (n = 0; n <= n_size - 16; n += 16) {
207
+ va = _mm512_mul_ps(_mm512_loadu_ps(s_i + m * BLOCK_N + n), vscale);
208
+ va = _mm512_fexp_u20_ps(_mm512_sub_ps(va, vmax));
209
+ vsum = _mm512_add_ps(vsum, va);
210
+
211
+ vb = (__m256i)(_mm512_cvtneps_pbh(va));
212
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(s_delta2 + m * BLOCK_N + n), vb);
213
+ }
214
+ if (n_remainder > 0) {
215
+ va = _mm512_mul_ps(_mm512_mask_loadu_ps(vneg_inf, vmask, s_i + m * BLOCK_N + n), vscale);
216
+ va = _mm512_fexp_u20_ps(_mm512_sub_ps(va, vmax));
217
+ vsum = _mm512_add_ps(vsum, va);
218
+
219
+ vb = (__m256i)(_mm512_cvtneps_pbh(va));
220
+ _mm256_mask_storeu_epi16(reinterpret_cast<__m256i*>(s_delta2 + m * BLOCK_N + n), vmask, vb);
221
+ }
222
+
223
+ // s' <- s' * m_delta + sum(s_delta)
224
+ s_prime[m] *= m_delta;
225
+ s_prime[m] += _mm512_reduce_add_ps(vsum);
226
+
227
+ m_prime[m] = m_i;
228
+
229
+ // pad s_delta with 0, pad_size range from [0, 32)
230
+ int pad_size = padded_n_size - n_size;
231
+ if (pad_size > 0) {
232
+ const __m512i vzero = _mm512_setzero_si512();
233
+ __mmask32 vmask2 = (1ULL << pad_size) - 1;
234
+ _mm512_mask_storeu_epi16(reinterpret_cast<__m512i*>(s_delta2 + m * BLOCK_N + n_size), vmask2, vzero);
235
+ }
236
+
237
+ // v' <- v' * m_delta
238
+ vmdelta = _mm512_set1_ps(m_delta);
239
+ int k = 0;
240
+ for (; k <= head_size_v - 16; k += 16) {
241
+ va = _mm512_mul_ps(_mm512_loadu_ps(v_prime + m * head_size_v + k), vmdelta);
242
+ _mm512_storeu_ps(reinterpret_cast<__m512*>(v_prime + m * head_size_v + k), va);
243
+ }
244
+ if (v_remainder > 0) {
245
+ va = _mm512_mul_ps(_mm512_maskz_loadu_ps(vmask1, v_prime + m * head_size_v + k), vmdelta);
246
+ _mm512_mask_storeu_ps(reinterpret_cast<__m512*>(v_prime + m * head_size_v + k), vmask1, va);
247
+ }
248
+ }
249
+};
250
+#endif
csrc/cpu/sgl-kernels/mla_cache.cpp
new
+111
@@ -0,0 +1,111 @@
1
+// vLLM-native CPU cache-write op for MLA's single-latent-buffer KV cache.
2
+//
3
+// `concat_and_cache_mla` (the generic MLA cache-write op used by every GPU
4
+// backend) is registered CUDA-only. This is the CPU counterpart, adapted in
5
+// spirit from SGLang's `store_cache_cpu` (csrc/cpu/kvcache.cpp) but
6
+// generalized to write two source tensors (`kv_c_normed`, `k_pe`) into two
7
+// different column-offset ranges of the SAME destination row -- SGLang's
8
+// version assumes k/v land in two independent, equal-row-width cache
9
+// tensors, which doesn't hold here since MLA's cache is one 576-wide buffer
10
+// and the two column ranges (512-wide, 64-wide) don't match the buffer's
11
+// true per-token stride, so the write can't reuse `store_cache_cpu` as-is.
12
+
13
+#include "common.h"
14
+#include "vec.h"
15
+
16
+namespace {
17
+
18
+template <typename scalar_t>
19
+inline void copy_stub(scalar_t* __restrict__ dst,
20
+ const scalar_t* __restrict__ src, int64_t size) {
21
+ int64_t d = 0;
22
+#if defined(CPU_CAPABILITY_AVX512)
23
+ using Vec = at::vec::Vectorized<scalar_t>;
24
+ constexpr int64_t kVecSize = Vec::size();
25
+ for (; d <= size - kVecSize; d += kVecSize) {
26
+ Vec data = Vec::loadu(src + d);
27
+ data.store(dst + d);
28
+ }
29
+#endif
30
+ for (; d < size; ++d) {
31
+ dst[d] = src[d];
32
+ }
33
+}
34
+
35
+template <typename scalar_t, typename index_t>
36
+void concat_and_cache_mla_kernel_impl(
37
+ const scalar_t* __restrict__ kv_c_normed, // [num_tokens, kv_lora_rank]
38
+ const scalar_t* __restrict__ k_pe, // [num_tokens, qk_rope_head_dim]
39
+ scalar_t* __restrict__ kv_cache, // [.., kv_lora_rank + qk_rope_head_dim]
40
+ const index_t* __restrict__ slot_mapping, // [num_tokens]
41
+ int64_t num_tokens, int64_t kv_lora_rank, int64_t qk_rope_head_dim,
42
+ int64_t kv_c_stride, int64_t k_pe_stride, int64_t cache_stride) {
43
+ at::parallel_for(0, num_tokens, 0, [&](int64_t begin, int64_t end) {
44
+ for (int64_t i = begin; i < end; ++i) {
45
+ int64_t slot = static_cast<int64_t>(slot_mapping[i]);
46
+ if (slot < 0) {
47
+ // padded/invalid token, matches `concat_and_cache_mla`'s semantics.
48
+ continue;
49
+ }
50
+ scalar_t* __restrict__ cache_row = kv_cache + slot * cache_stride;
51
+ copy_stub(cache_row, kv_c_normed + i * kv_c_stride, kv_lora_rank);
52
+ copy_stub(cache_row + kv_lora_rank, k_pe + i * k_pe_stride,
53
+ qk_rope_head_dim);
54
+ }
55
+ });
56
+}
57
+
58
+} // namespace
59
+
60
+// kv_c_normed : [num_tokens, kv_lora_rank]
61
+// k_pe : [num_tokens, qk_rope_head_dim] or [num_tokens, 1,
62
+// qk_rope_head_dim] kv_cache : [num_blocks, block_size, kv_lora_rank +
63
+// qk_rope_head_dim] slot_mapping: [num_tokens] int32/int64, absolute physical
64
+// row index
65
+// (block_id * block_size + block_offset); negative entries are
66
+// skipped (padded tokens), matching `concat_and_cache_mla`.
67
+void concat_and_cache_mla_cpu(const at::Tensor& kv_c_normed,
68
+ const at::Tensor& k_pe, at::Tensor& kv_cache,
69
+ const at::Tensor& slot_mapping) {
70
+ TORCH_CHECK(kv_c_normed.dim() == 2,
71
+ "kv_c_normed must be 2D [num_tokens, kv_lora_rank]");
72
+ TORCH_CHECK(k_pe.dim() == 2 || k_pe.dim() == 3, "k_pe must be 2D or 3D");
73
+ TORCH_CHECK(kv_cache.dim() == 3,
74
+ "kv_cache must be 3D [num_blocks, block_size, head_size]");
75
+ TORCH_CHECK(kv_c_normed.stride(-1) == 1,
76
+ "kv_c_normed innermost dim must be contiguous");
77
+ TORCH_CHECK(k_pe.stride(-1) == 1, "k_pe innermost dim must be contiguous");
78
+ TORCH_CHECK(kv_cache.stride(-1) == 1,
79
+ "kv_cache innermost dim must be contiguous");
80
+
81
+ int64_t num_tokens = kv_c_normed.size(0);
82
+ int64_t kv_lora_rank = kv_c_normed.size(1);
83
+ int64_t qk_rope_head_dim = k_pe.size(-1);
84
+ int64_t head_size = kv_cache.size(-1);
85
+ TORCH_CHECK(head_size == kv_lora_rank + qk_rope_head_dim,
86
+ "kv_cache head_size must equal kv_lora_rank + qk_rope_head_dim");
87
+ TORCH_CHECK(slot_mapping.size(0) == num_tokens, "slot_mapping size mismatch");
88
+
89
+ // Real physical per-token stride of the cache, read from the tensor's own
90
+ // strides rather than assumed to equal head_size (paged/pooled buffers are
91
+ // not guaranteed to have zero inter-row padding in general, even though in
92
+ // practice a freshly-allocated MLA cache is fully contiguous).
93
+ int64_t cache_stride = kv_cache.stride(1);
94
+
95
+ const auto dtype = kv_cache.scalar_type();
96
+ TORCH_CHECK(dtype == kv_c_normed.scalar_type() && dtype == k_pe.scalar_type(),
97
+ "concat_and_cache_mla_cpu: dtype mismatch");
98
+ const auto index_dtype = slot_mapping.scalar_type();
99
+ TORCH_CHECK(index_dtype == at::kLong || index_dtype == at::kInt,
100
+ "slot_mapping must be int32 or int64");
101
+
102
+ AT_DISPATCH_REDUCED_FLOATING_TYPES(dtype, "concat_and_cache_mla_cpu", [&] {
103
+ AT_DISPATCH_INDEX_TYPES(index_dtype, "concat_and_cache_mla_cpu_index", [&] {
104
+ concat_and_cache_mla_kernel_impl<scalar_t, index_t>(
105
+ kv_c_normed.data_ptr<scalar_t>(), k_pe.data_ptr<scalar_t>(),
106
+ kv_cache.data_ptr<scalar_t>(), slot_mapping.data_ptr<index_t>(),
107
+ num_tokens, kv_lora_rank, qk_rope_head_dim, kv_c_normed.stride(0),
108
+ k_pe.stride(0), cache_stride);
109
+ });
110
+ });
111
+}
csrc/cpu/torch_bindings.cpp
+62
@@ -91,6 +91,39 @@ at::Tensor fp8_scaled_mm_cpu(at::Tensor& mat1, at::Tensor& mat2,
91
const std::optional<at::Tensor>& bias,
92
at::ScalarType out_dtype, bool is_vnni);
93
94
+// Adapted from sglang: MLA CPU kernels (AMX-only)
95
+void decode_attention_cpu(at::Tensor& query, at::Tensor& k_buffer,
96
+ at::Tensor& v_buffer, at::Tensor& output,
97
+ const std::optional<at::Tensor>& key,
98
+ const std::optional<at::Tensor>& value,
99
+ const std::optional<at::Tensor>& loc,
100
+ at::Tensor& attn_logits, at::Tensor& req_to_token,
101
+ at::Tensor& req_pool_indices, at::Tensor& seq_lens,
102
+ double sm_scale, double logit_cap, bool is_cross_attn,
103
+ int64_t sliding_window_size,
104
+ std::optional<at::Tensor> encoder_lens,
105
+ std::optional<at::Tensor> sinks);
106
+
107
+void extend_attention_cpu(
108
+ at::Tensor& q_extend, const std::optional<at::Tensor>& k_extend,
109
+ const std::optional<at::Tensor>& v_extend, at::Tensor& o_extend,
110
+ at::Tensor& k_buffer, at::Tensor& v_buffer, at::Tensor& req_to_token,
111
+ at::Tensor& req_pool_indices, at::Tensor& seq_lens,
112
+ at::Tensor& extend_seq_lens, at::Tensor& extend_start_loc,
113
+ int64_t max_len_extend, double sm_scale, double logit_cap,
114
+ bool is_cross_attn, int64_t sliding_window_size,
115
+ std::optional<at::Tensor> encoder_lens, std::optional<at::Tensor> sinks,
116
+ std::optional<at::Tensor> tree_mask);
117
+
118
+void bmm_cpu(at::Tensor& out, at::Tensor& mat1, at::Tensor& mat2, bool is_vnni,
119
+ const std::optional<at::Tensor>& scale);
120
+
121
+// vLLM-native: CPU cache-write op for MLA's single-latent-buffer KV cache
122
+// (the CUDA-only `concat_and_cache_mla` has no CPU dispatch).
123
+void concat_and_cache_mla_cpu(const at::Tensor& kv_c_normed,
124
+ const at::Tensor& k_pe, at::Tensor& kv_cache,
125
+ const at::Tensor& slot_mapping);
126
+
127
// Adapted from sglang: INT4 W4A8 kernels
128
std::tuple<at::Tensor, at::Tensor, at::Tensor> convert_weight_packed_scale_zp(
129
at::Tensor qweight, // awq: (*, K, N / 8) || gptq: (*, K / 8, N) , int32
@@ -521,6 +554,35 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
554
"pad_slot_id, "
555
"bool is_vnni) -> Tensor");
556
ops.impl("causal_conv1d_update_cpu", torch::kCPU, &causal_conv1d_update_cpu);
557
+
558
+ // Adapted from sglang: MLA CPU kernels (AMX-only, DeepSeek V2/V3/R1)
559
+ ops.def(
560
+ "decode_attention_cpu(Tensor query, Tensor k_buffer, Tensor v_buffer, "
561
+ "Tensor(a!) output, Tensor? key, Tensor? value, Tensor? loc, Tensor "
562
+ "attn_logits, Tensor req_to_token, Tensor req_pool_indices, Tensor "
563
+ "seq_lens, float sm_scale, float logit_cap, bool is_cross_attn, int "
564
+ "sliding_window_size, Tensor? encoder_lens, Tensor? sinks) -> ()");
565
+ ops.impl("decode_attention_cpu", torch::kCPU, &decode_attention_cpu);
566
+
567
+ ops.def(
568
+ "extend_attention_cpu(Tensor q_extend, Tensor? k_extend, Tensor? "
569
+ "v_extend, Tensor(a!) o_extend, Tensor k_buffer, Tensor v_buffer, "
570
+ "Tensor req_to_token, Tensor req_pool_indices, Tensor seq_lens, Tensor "
571
+ "extend_seq_lens, Tensor extend_start_loc, int max_len_extend, float "
572
+ "sm_scale, float logit_cap, bool is_cross_attn, int "
573
+ "sliding_window_size, Tensor? encoder_lens, Tensor? sinks, Tensor? "
574
+ "tree_mask=None) -> ()");
575
+ ops.impl("extend_attention_cpu", torch::kCPU, &extend_attention_cpu);
576
+
577
+ ops.def(
578
+ "bmm_cpu(Tensor(a!) out, Tensor mat1, Tensor mat2, bool is_vnni, "
579
+ "Tensor? scale) -> ()");
580
+ ops.impl("bmm_cpu", torch::kCPU, &bmm_cpu);
581
+
582
+ ops.def(
583
+ "concat_and_cache_mla_cpu(Tensor kv_c_normed, Tensor k_pe, "
584
+ "Tensor(a!) kv_cache, Tensor slot_mapping) -> ()");
585
+ ops.impl("concat_and_cache_mla_cpu", torch::kCPU, &concat_and_cache_mla_cpu);
586
#endif
587
588
#if (defined(__AVX512BF16__) && defined(__AVX512F__) && \
tests/kernels/attention/test_amx_mla.py
new
+471
@@ -0,0 +1,471 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Correctness tests for the AMX-only CPU MLA backend: the vendored
4
+decode/extend/bmm kernels, the KV cache write, and the ``AMXMLAImpl``
5
+backend built on top of them.
6
+"""
7
+
8
+import pytest
9
+import torch
10
+
11
+from vllm.platforms import current_platform
12
+
13
+if not current_platform.is_cpu():
14
+ pytest.skip("skipping CPU-only tests", allow_module_level=True)
15
+
16
+if not torch.cpu._is_amx_tile_supported():
17
+ pytest.skip("AMX MLA requires an AMX-capable host", allow_module_level=True)
18
+
19
+torch.cpu._init_amx()
20
+
21
+from vllm import _custom_ops as ops # noqa: E402
22
+from vllm.utils.torch_utils import set_random_seed # noqa: E402
23
+from vllm.v1.attention.backends.mla.amx_mla import ( # noqa: E402
24
+ AMXMLAImpl,
25
+ _compute_num_kv_splits,
26
+ _expand_block_table,
27
+)
28
+
29
+KV_LORA_RANK = 64
30
+QK_ROPE_HEAD_DIM = 32
31
+HEAD_SIZE = KV_LORA_RANK + QK_ROPE_HEAD_DIM # 96, kv-cache row width
32
+# The extend/decode kernels transpose by 512-bit lanes: both the cache row
33
+# width and the (kv_lora_rank-wide) value width must be 32-element aligned.
34
+DTYPE = torch.bfloat16
35
+ATOL = 2e-2
36
+RTOL = 2e-2
37
+# Backend-level tests chain absorb -> attend -> de-absorb through bf16,
38
+# accumulating more rounding error than a single kernel op.
39
+_IMPL_ATOL = 1.5e-1
40
+_IMPL_RTOL = 1.5e-1
41
+
42
+
43
+def _flatten_cache(kv_cache: torch.Tensor) -> torch.Tensor:
44
+ """(num_blocks, block_size, head_size) -> (num_slots, head_size)."""
45
+ return kv_cache.view(-1, kv_cache.size(-1))
46
+
47
+
48
+def _random_paged_cache(num_slots: int, head_size: int) -> torch.Tensor:
49
+ return torch.randn(num_slots, head_size, dtype=DTYPE)
50
+
51
+
52
+def _ref_latent_attn(
53
+ q: torch.Tensor,
54
+ keys: torch.Tensor,
55
+ scale: float,
56
+ kv_lora_rank: int,
57
+) -> torch.Tensor:
58
+ """Plain-PyTorch causal-free latent-space MQA attention for one request.
59
+
60
+ q: (num_heads, head_size); keys: (ctx_len, head_size). Values are the
61
+ first ``kv_lora_rank`` columns of the same latent rows (MLA's K/V
62
+ aliasing), matching what decode_attention_cpu/extend_attention_cpu
63
+ compute internally for the MLA-shaped case.
64
+ """
65
+ values = keys[:, :kv_lora_rank]
66
+ scores = (q.float() @ keys.float().T) * scale
67
+ probs = torch.softmax(scores, dim=-1)
68
+ return (probs @ values.float()).to(q.dtype)
69
+
70
+
71
+def test_bmm_cpu_matches_torch_bmm():
72
+ set_random_seed(0)
73
+ # bmm_cpu requires the output's last dim (mat2's out-features) to be a
74
+ # multiple of 32 (tinygemm's tile width); the contraction dim is free.
75
+ n, b, p, l = 8, 32, 32, 32 # noqa: E741
76
+ mat1 = torch.randn(n, b, p, dtype=DTYPE)
77
+ mat2 = torch.randn(n, l, p, dtype=DTYPE) # (N, OUT, IN) Linear convention
78
+
79
+ ref = torch.bmm(mat1.float(), mat2.float().transpose(1, 2)).to(DTYPE)
80
+
81
+ out = torch.empty(n, b, l, dtype=DTYPE)
82
+ ops.bmm_cpu(out, mat1, mat2, False, None)
83
+ torch.testing.assert_close(out, ref, atol=ATOL, rtol=RTOL)
84
+
85
+ packed = torch.ops._C.convert_weight_packed(mat2.contiguous())
86
+ out_vnni = torch.empty(n, b, l, dtype=DTYPE)
87
+ ops.bmm_cpu(out_vnni, mat1, packed, True, None)
88
+ torch.testing.assert_close(out_vnni, ref, atol=ATOL, rtol=RTOL)
89
+
90
+
91
+def test_concat_and_cache_mla_round_trip():
92
+ set_random_seed(0)
93
+ num_tokens = 37
94
+ num_blocks, block_size = 8, 16
95
+ kv_cache = torch.zeros(num_blocks, block_size, HEAD_SIZE, dtype=DTYPE)
96
+
97
+ kv_c_normed = torch.randn(num_tokens, KV_LORA_RANK, dtype=DTYPE)
98
+ k_pe = torch.randn(num_tokens, QK_ROPE_HEAD_DIM, dtype=DTYPE)
99
+ slot_mapping = torch.randperm(num_blocks * block_size)[:num_tokens].to(torch.int64)
100
+
101
+ ops.amx_mla_concat_and_cache(kv_c_normed, k_pe, kv_cache, slot_mapping)
102
+
103
+ flat = _flatten_cache(kv_cache)
104
+ read_back = flat[slot_mapping]
105
+ expected = torch.cat([kv_c_normed, k_pe], dim=-1)
106
+ torch.testing.assert_close(read_back, expected)
107
+
108
+ untouched = torch.ones(num_blocks * block_size, dtype=torch.bool)
109
+ untouched[slot_mapping] = False
110
+ assert (flat[untouched] == 0).all()
111
+
112
+
113
+@pytest.mark.parametrize("seq_lens", [[5], [1, 300, 33], [513, 1, 129, 7]])
114
+def test_decode_attention_cpu_matches_reference(seq_lens):
115
+ set_random_seed(0)
116
+ num_seqs = len(seq_lens)
117
+ num_heads = 4
118
+ block_size = 32
119
+ max_len = max(seq_lens)
120
+ max_blocks = (max_len + block_size - 1) // block_size
121
+ num_blocks = num_seqs * max_blocks
122
+
123
+ kv_cache = _random_paged_cache(num_blocks * block_size, HEAD_SIZE)
124
+ block_table = torch.randperm(num_blocks).view(num_seqs, max_blocks).to(torch.int64)
125
+ req_to_token = _expand_block_table(block_table, block_size)
126
+ req_pool_indices = torch.arange(num_seqs, dtype=torch.int64)
127
+ seq_lens_t = torch.tensor(seq_lens, dtype=torch.int64)
128
+ scale = HEAD_SIZE**-0.5
129
+
130
+ q = torch.randn(num_seqs, num_heads, HEAD_SIZE, dtype=DTYPE)
131
+ kv_cache_flat = kv_cache.view(-1, 1, HEAD_SIZE)
132
+ v_buffer = kv_cache_flat[..., :KV_LORA_RANK]
133
+
134
+ o = torch.zeros(num_seqs, num_heads, KV_LORA_RANK, dtype=DTYPE)
135
+ attn_logits = torch.zeros(
136
+ num_seqs, num_heads, 4, KV_LORA_RANK + 1, dtype=torch.float32
137
+ )
138
+ ops.cpu_mla_decode(
139
+ q,
140
+ kv_cache_flat,
141
+ v_buffer,
142
+ o,
143
+ None,
144
+ None,
145
+ None,
146
+ attn_logits,
147
+ req_to_token,
148
+ req_pool_indices,
149
+ seq_lens_t,
150
+ scale,
151
+ 0.0,
152
+ False,
153
+ 0,
154
+ None,
155
+ None,
156
+ )
157
+
158
+ flat_cache = _flatten_cache(kv_cache)
159
+ for i, seq_len in enumerate(seq_lens):
160
+ token_ids = req_to_token[i, :seq_len]
161
+ keys = flat_cache[token_ids]
162
+ ref = _ref_latent_attn(q[i], keys, scale, KV_LORA_RANK)
163
+ torch.testing.assert_close(o[i], ref, atol=ATOL, rtol=RTOL)
164
+
165
+
166
+@pytest.mark.parametrize(
167
+ "prefix_lens,extend_lens",
168
+ [
169
+ ([0], [9]), # fresh prefill, no cached prefix
170
+ ([64], [17]), # continuation of a cached prefix
171
+ ([0, 40, 128], [23, 5, 31]), # mixed batch
172
+ ],
173
+)
174
+def test_extend_attention_cpu_matches_reference(prefix_lens, extend_lens):
175
+ set_random_seed(0)
176
+ num_seqs = len(prefix_lens)
177
+ num_heads = 4
178
+ block_size = 32
179
+ seq_lens = [p + e for p, e in zip(prefix_lens, extend_lens)]
180
+ max_len = max(seq_lens)
181
+ max_blocks = (max_len + block_size - 1) // block_size
182
+ num_blocks = num_seqs * max_blocks
183
+
184
+ kv_cache = _random_paged_cache(num_blocks * block_size, HEAD_SIZE)
185
+ block_table = torch.randperm(num_blocks).view(num_seqs, max_blocks).to(torch.int64)
186
+ req_to_token = _expand_block_table(block_table, block_size)
187
+ req_pool_indices = torch.arange(num_seqs, dtype=torch.int64)
188
+ seq_lens_t = torch.tensor(seq_lens, dtype=torch.int64)
189
+ extend_seq_lens_t = torch.tensor(extend_lens, dtype=torch.int64)
190
+ extend_start_loc = torch.cumsum(
191
+ torch.tensor([0, *extend_lens[:-1]], dtype=torch.int64), dim=0
192
+ )
193
+ scale = HEAD_SIZE**-0.5
194
+ total_new_tokens = sum(extend_lens)
195
+
196
+ # The new tokens' latent K/V, already written into the cache (mirrors
197
+ # do_kv_cache_update running before forward_mha in real usage).
198
+ k_extend_flat = torch.randn(total_new_tokens, HEAD_SIZE, dtype=DTYPE)
199
+ flat_cache = _flatten_cache(kv_cache)
200
+ for i in range(num_seqs):
201
+ prefix_len = prefix_lens[i]
202
+ ext_len = extend_lens[i]
203
+ new_token_ids = req_to_token[i, prefix_len : prefix_len + ext_len]
204
+ start = extend_start_loc[i].item()
205
+ flat_cache[new_token_ids] = k_extend_flat[start : start + ext_len]
206
+
207
+ q_extend = torch.randn(total_new_tokens, num_heads, HEAD_SIZE, dtype=DTYPE)
208
+ k_extend = k_extend_flat.unsqueeze(1)
209
+ v_extend = k_extend[..., :KV_LORA_RANK]
210
+ kv_cache_flat = kv_cache.view(-1, 1, HEAD_SIZE)
211
+ v_buffer = kv_cache_flat[..., :KV_LORA_RANK]
212
+
213
+ o_extend = torch.empty(total_new_tokens, num_heads, KV_LORA_RANK, dtype=DTYPE)
214
+ ops.cpu_mla_extend(
215
+ q_extend,
216
+ k_extend,
217
+ v_extend,
218
+ o_extend,
219
+ kv_cache_flat,
220
+ v_buffer,
221
+ req_to_token,
222
+ req_pool_indices,
223
+ seq_lens_t,
224
+ extend_seq_lens_t,
225
+ extend_start_loc,
226
+ max(extend_lens),
227
+ scale,
228
+ 0.0,
229
+ False,
230
+ 0,
231
+ None,
232
+ None,
233
+ None,
234
+ )
235
+
236
+ for i in range(num_seqs):
237
+ prefix_len = prefix_lens[i]
238
+ ext_len = extend_lens[i]
239
+ start = extend_start_loc[i].item()
240
+ for t in range(ext_len):
241
+ visible = prefix_len + t + 1
242
+ token_ids = req_to_token[i, :visible]
243
+ keys = flat_cache[token_ids]
244
+ ref = _ref_latent_attn(q_extend[start + t], keys, scale, KV_LORA_RANK)
245
+ torch.testing.assert_close(o_extend[start + t], ref, atol=ATOL, rtol=RTOL)
246
+
247
+
248
+class _FakeLinear:
249
+ """Minimal duck-typed stand-in for kv_b_proj: only .weight and
250
+ .quant_method are read by get_and_maybe_dequant_weights()."""
251
+
252
+ def __init__(self, weight: torch.Tensor):
253
+ self.weight = weight
254
+ self.quant_method = None
255
+
256
+
257
+def _make_amx_mla_impl(num_heads, qk_nope_head_dim, v_head_dim, kv_lora_rank):
258
+ weight = torch.randn(
259
+ num_heads * (qk_nope_head_dim + v_head_dim), kv_lora_rank, dtype=DTYPE
260
+ )
261
+ kv_b_proj = _FakeLinear(weight)
262
+ impl = AMXMLAImpl(
263
+ num_heads=num_heads,
264
+ head_size=kv_lora_rank + QK_ROPE_HEAD_DIM,
265
+ scale=(qk_nope_head_dim + QK_ROPE_HEAD_DIM) ** -0.5,
266
+ num_kv_heads=1,
267
+ alibi_slopes=None,
268
+ sliding_window=None,
269
+ kv_cache_dtype="auto",
270
+ logits_soft_cap=None,
271
+ attn_type="decoder",
272
+ kv_sharing_target_layer_name=None,
273
+ q_lora_rank=None,
274
+ kv_lora_rank=kv_lora_rank,
275
+ qk_nope_head_dim=qk_nope_head_dim,
276
+ qk_rope_head_dim=QK_ROPE_HEAD_DIM,
277
+ qk_head_dim=qk_nope_head_dim + QK_ROPE_HEAD_DIM,
278
+ v_head_dim=v_head_dim,
279
+ kv_b_proj=kv_b_proj,
280
+ )
281
+ impl.process_weights_after_loading(DTYPE)
282
+ return impl, weight
283
+
284
+
285
+class _FakePrefillMetadata:
286
+ def __init__(
287
+ self, block_table, cpu_seq_lens, query_start_loc, req_to_token, req_pool_indices
288
+ ):
289
+ self.block_table = block_table
290
+ self.cpu_seq_lens = cpu_seq_lens
291
+ query_start_loc_i64 = query_start_loc.to(torch.int64)
292
+ extend_seq_lens = query_start_loc_i64[1:] - query_start_loc_i64[:-1]
293
+ self.extend_seq_lens = extend_seq_lens
294
+ self.extend_start_loc = query_start_loc_i64[:-1]
295
+ self.max_len_extend = int(extend_seq_lens.max().item())
296
+ self.req_to_token = req_to_token
297
+ self.req_pool_indices = req_pool_indices
298
+
299
+
300
+class _FakeDecodeMetadata:
301
+ def __init__(self, block_table, seq_lens, req_to_token, req_pool_indices):
302
+ self.block_table = block_table
303
+ self.seq_lens_i64 = seq_lens.to(torch.int64)
304
+ self.req_to_token = req_to_token
305
+ self.req_pool_indices = req_pool_indices
306
+ self.num_kv_splits = _compute_num_kv_splits(
307
+ int(seq_lens.max().item()), current_platform.num_compute_units()
308
+ )
309
+
310
+
311
+class _FakeAttnMetadata:
312
+ def __init__(self, decode=None, prefill=None, max_seq_len=0):
313
+ self.decode = decode
314
+ self.prefill = prefill
315
+ self.max_seq_len = max_seq_len
316
+
317
+
318
+def test_amx_mla_impl_forward_mqa_matches_reference(default_vllm_config):
319
+ """forward_mqa receives already-absorbed Q (as MLAAttention.forward_impl
320
+ would produce via layer.W_UK_T) and must reproduce plain per-head MLA
321
+ decode attention through W_UK/W_UV."""
322
+ set_random_seed(1)
323
+ num_heads, qk_nope_head_dim, v_head_dim, kv_lora_rank = 4, 32, 32, 64
324
+ impl, kv_b_weight = _make_amx_mla_impl(
325
+ num_heads, qk_nope_head_dim, v_head_dim, kv_lora_rank
326
+ )
327
+ w_uk, w_uv = kv_b_weight.T.view(
328
+ kv_lora_rank, num_heads, qk_nope_head_dim + v_head_dim
329
+ ).split([qk_nope_head_dim, v_head_dim], dim=-1)
330
+
331
+ seq_lens = [1, 40, 5]
332
+ num_seqs = len(seq_lens)
333
+ block_size = 32
334
+ max_blocks = (max(seq_lens) + block_size - 1) // block_size
335
+ num_blocks = num_seqs * max_blocks
336
+ head_size = kv_lora_rank + QK_ROPE_HEAD_DIM
337
+
338
+ kv_cache = torch.randn(num_blocks, block_size, head_size, dtype=DTYPE)
339
+ block_table = torch.randperm(num_blocks).view(num_seqs, max_blocks).to(torch.int64)
340
+ req_to_token = _expand_block_table(block_table, block_size)
341
+
342
+ q_nope = torch.randn(num_seqs, num_heads, qk_nope_head_dim, dtype=DTYPE)
343
+ q_pe = torch.randn(num_seqs, num_heads, QK_ROPE_HEAD_DIM, dtype=DTYPE)
344
+ # Pre-absorb, mirroring MLAAttention.forward_impl's own bmm against W_UK_T.
345
+ ql_nope = torch.einsum("bnp,lnp->bnl", q_nope.float(), w_uk.float()).to(DTYPE)
346
+
347
+ attn_metadata = _FakeAttnMetadata(
348
+ decode=_FakeDecodeMetadata(
349
+ block_table=block_table,
350
+ seq_lens=torch.tensor(seq_lens, dtype=torch.int64),
351
+ req_to_token=req_to_token,
352
+ req_pool_indices=torch.arange(num_seqs, dtype=torch.int64),
353
+ ),
354
+ max_seq_len=max(seq_lens),
355
+ )
356
+ o, lse = impl.forward_mqa((ql_nope, q_pe), kv_cache, attn_metadata, layer=None)
357
+ assert lse is None
358
+ # De-absorb, mirroring MLAAttention.forward_impl's own bmm against
359
+ # layer._v_up_proj, to get back into real per-head V space for
360
+ # comparison against a from-scratch reference.
361
+ out_real = torch.einsum("bnl,lnv->bnv", o.float(), w_uv.float())
362
+
363
+ flat_cache = _flatten_cache(kv_cache)
364
+ scale = impl.scale
365
+ for i, seq_len in enumerate(seq_lens):
366
+ token_ids = req_to_token[i, :seq_len]
367
+ keys_latent = flat_cache[token_ids] # (L, head_size)
368
+ k_nope_latent, k_pe = keys_latent.split(
369
+ [kv_lora_rank, QK_ROPE_HEAD_DIM], dim=-1
370
+ )
371
+ k_real = torch.einsum("lp,pnd->nld", k_nope_latent.float(), w_uk.float())
372
+ k_pe_b = k_pe.float().unsqueeze(0).expand(num_heads, -1, -1)
373
+ k_full = torch.cat([k_real, k_pe_b], dim=-1) # (num_heads, L, qk_head_dim)
374
+ q_full = torch.cat(
375
+ [q_nope[i], q_pe[i]], dim=-1
376
+ ).float() # (num_heads, qk_head_dim)
377
+ scores = torch.einsum("nd,nld->nl", q_full, k_full) * scale
378
+ probs = torch.softmax(scores, dim=-1)
379
+ v_real = torch.einsum("lp,pnv->nlv", k_nope_latent.float(), w_uv.float())
380
+ ref = torch.einsum("nl,nlv->nv", probs, v_real)
381
+ torch.testing.assert_close(out_real[i], ref, atol=_IMPL_ATOL, rtol=_IMPL_RTOL)
382
+
383
+
384
+def test_amx_mla_impl_forward_mha_matches_reference(default_vllm_config):
385
+ """forward_mha receives raw unabsorbed Q and must attend correctly for a
386
+ prefill batch with a mix of fresh and continued (cached-prefix)
387
+ sequences."""
388
+ set_random_seed(2)
389
+ num_heads, qk_nope_head_dim, v_head_dim, kv_lora_rank = 4, 32, 32, 64
390
+ impl, kv_b_weight = _make_amx_mla_impl(
391
+ num_heads, qk_nope_head_dim, v_head_dim, kv_lora_rank
392
+ )
393
+ w_uk, w_uv = kv_b_weight.T.view(
394
+ kv_lora_rank, num_heads, qk_nope_head_dim + v_head_dim
395
+ ).split([qk_nope_head_dim, v_head_dim], dim=-1)
396
+
397
+ prefix_lens = [0, 20]
398
+ extend_lens = [13, 7]
399
+ seq_lens = [p + e for p, e in zip(prefix_lens, extend_lens)]
400
+ num_seqs = len(prefix_lens)
401
+ block_size = 32
402
+ max_blocks = (max(seq_lens) + block_size - 1) // block_size
403
+ num_blocks = num_seqs * max_blocks
404
+ head_size = kv_lora_rank + QK_ROPE_HEAD_DIM
405
+
406
+ kv_cache = torch.randn(num_blocks, block_size, head_size, dtype=DTYPE)
407
+ block_table = torch.randperm(num_blocks).view(num_seqs, max_blocks).to(torch.int64)
408
+ req_to_token = _expand_block_table(block_table, block_size)
409
+ flat_cache = _flatten_cache(kv_cache)
410
+
411
+ total_new_tokens = sum(extend_lens)
412
+ query_start_loc = torch.tensor(
413
+ [0, *torch.cumsum(torch.tensor(extend_lens), dim=0).tolist()],
414
+ dtype=torch.int64,
415
+ )
416
+
417
+ q_nope = torch.randn(total_new_tokens, num_heads, qk_nope_head_dim, dtype=DTYPE)
418
+ q_pe = torch.randn(total_new_tokens, num_heads, QK_ROPE_HEAD_DIM, dtype=DTYPE)
419
+ q = torch.cat([q_nope, q_pe], dim=-1)
420
+
421
+ kv_c_normed = torch.randn(total_new_tokens, kv_lora_rank, dtype=DTYPE)
422
+ k_pe = torch.randn(total_new_tokens, 1, QK_ROPE_HEAD_DIM, dtype=DTYPE)
423
+
424
+ # Write the new tokens' latent K/V into the cache ahead of time, mirroring
425
+ # do_kv_cache_update running before forward_mha in real usage.
426
+ for i in range(num_seqs):
427
+ prefix_len = prefix_lens[i]
428
+ ext_len = extend_lens[i]
429
+ new_token_ids = req_to_token[i, prefix_len : prefix_len + ext_len]
430
+ start = query_start_loc[i].item()
431
+ flat_cache[new_token_ids, :kv_lora_rank] = kv_c_normed[start : start + ext_len]
432
+ flat_cache[new_token_ids, kv_lora_rank:] = k_pe[start : start + ext_len, 0]
433
+
434
+ attn_metadata = _FakeAttnMetadata(
435
+ prefill=_FakePrefillMetadata(
436
+ block_table=block_table,
437
+ cpu_seq_lens=torch.tensor(seq_lens, dtype=torch.int64),
438
+ query_start_loc=query_start_loc,
439
+ req_to_token=req_to_token,
440
+ req_pool_indices=torch.arange(num_seqs, dtype=torch.int64),
441
+ )
442
+ )
443
+ output = torch.empty(total_new_tokens, num_heads * v_head_dim, dtype=DTYPE)
444
+ impl.forward_mha(
445
+ q, kv_c_normed, k_pe, kv_cache, attn_metadata, k_scale=None, output=output
446
+ )
447
+ out_view = output.view(total_new_tokens, num_heads, v_head_dim).float()
448
+
449
+ scale = impl.scale
450
+ for i in range(num_seqs):
451
+ prefix_len = prefix_lens[i]
452
+ ext_len = extend_lens[i]
453
+ start = query_start_loc[i].item()
454
+ for t in range(ext_len):
455
+ visible = prefix_len + t + 1
456
+ token_ids = req_to_token[i, :visible]
457
+ keys_latent = flat_cache[token_ids]
458
+ k_nope_latent, k_pe_ref = keys_latent.split(
459
+ [kv_lora_rank, QK_ROPE_HEAD_DIM], dim=-1
460
+ )
461
+ k_real = torch.einsum("lp,pnd->nld", k_nope_latent.float(), w_uk.float())
462
+ k_pe_b = k_pe_ref.float().unsqueeze(0).expand(num_heads, -1, -1)
463
+ k_full = torch.cat([k_real, k_pe_b], dim=-1)
464
+ q_full = torch.cat([q_nope[start + t], q_pe[start + t]], dim=-1).float()
465
+ scores = torch.einsum("nd,nld->nl", q_full, k_full) * scale
466
+ probs = torch.softmax(scores, dim=-1)
467
+ v_real = torch.einsum("lp,pnv->nlv", k_nope_latent.float(), w_uv.float())
468
+ ref = torch.einsum("nl,nlv->nv", probs, v_real)
469
+ torch.testing.assert_close(
470
+ out_view[start + t], ref, atol=_IMPL_ATOL, rtol=_IMPL_RTOL
471
+ )
tests/v1/attention/test_mla_backends.py
+2
@@ -172,9 +172,11 @@ def test_mla_post_load_preserves_runtime_weight_addresses(monkeypatch):
172
layer.kv_b_proj.quant_method = None
173
layer.is_aiter_triton_fp4_bmm_enabled = False
174
layer.is_aiter_triton_fp8_bmm_enabled = False
175
+ layer.is_amx_bmm_enabled = False
176
layer.dcp_q_replicate = False
177
layer.quant_config = None
178
layer.layer_name = "test"
179
+ layer.impl = SimpleNamespace(process_weights_after_loading=lambda act_dtype: None)
180
181
monkeypatch.setattr(
182
mla_attention_module, "set_default_quant_scales", lambda *_, **__: None
tests/v1/attention/test_mla_prefill_selector.py
+1
@@ -63,6 +63,7 @@ class TestGetMLAPrefillBackend:
63
64
with patch("vllm.platforms.current_platform") as mock_platform:
65
mock_platform.get_device_capability.return_value = None
66
+ mock_platform.is_cpu.return_value = False
67
68
backend = get_mla_prefill_backend(vllm_config)
69
assert backend.get_name() == "FLASH_ATTN"
vllm/_custom_ops.py
+103
@@ -3937,6 +3937,109 @@ def cpu_attention_with_kv_cache(
3937
)
3938
3939
3940
+def cpu_mla_decode(
3941
+ query: torch.Tensor,
3942
+ k_buffer: torch.Tensor,
3943
+ v_buffer: torch.Tensor,
3944
+ output: torch.Tensor,
3945
+ key: torch.Tensor | None,
3946
+ value: torch.Tensor | None,
3947
+ loc: torch.Tensor | None,
3948
+ attn_logits: torch.Tensor,
3949
+ req_to_token: torch.Tensor,
3950
+ req_pool_indices: torch.Tensor,
3951
+ seq_lens: torch.Tensor,
3952
+ sm_scale: float,
3953
+ logit_cap: float,
3954
+ is_cross_attn: bool,
3955
+ sliding_window_size: int,
3956
+ encoder_lens: torch.Tensor | None,
3957
+ sinks: torch.Tensor | None,
3958
+) -> None:
3959
+ torch.ops._C.decode_attention_cpu(
3960
+ query,
3961
+ k_buffer,
3962
+ v_buffer,
3963
+ output,
3964
+ key,
3965
+ value,
3966
+ loc,
3967
+ attn_logits,
3968
+ req_to_token,
3969
+ req_pool_indices,
3970
+ seq_lens,
3971
+ sm_scale,
3972
+ logit_cap,
3973
+ is_cross_attn,
3974
+ sliding_window_size,
3975
+ encoder_lens,
3976
+ sinks,
3977
+ )
3978
+
3979
+
3980
+def cpu_mla_extend(
3981
+ q_extend: torch.Tensor,
3982
+ k_extend: torch.Tensor | None,
3983
+ v_extend: torch.Tensor | None,
3984
+ o_extend: torch.Tensor,
3985
+ k_buffer: torch.Tensor,
3986
+ v_buffer: torch.Tensor,
3987
+ req_to_token: torch.Tensor,
3988
+ req_pool_indices: torch.Tensor,
3989
+ seq_lens: torch.Tensor,
3990
+ extend_seq_lens: torch.Tensor,
3991
+ extend_start_loc: torch.Tensor,
3992
+ max_len_extend: int,
3993
+ sm_scale: float,
3994
+ logit_cap: float,
3995
+ is_cross_attn: bool,
3996
+ sliding_window_size: int,
3997
+ encoder_lens: torch.Tensor | None,
3998
+ sinks: torch.Tensor | None,
3999
+ tree_mask: torch.Tensor | None = None,
4000
+) -> None:
4001
+ torch.ops._C.extend_attention_cpu(
4002
+ q_extend,
4003
+ k_extend,
4004
+ v_extend,
4005
+ o_extend,
4006
+ k_buffer,
4007
+ v_buffer,
4008
+ req_to_token,
4009
+ req_pool_indices,
4010
+ seq_lens,
4011
+ extend_seq_lens,
4012
+ extend_start_loc,
4013
+ max_len_extend,
4014
+ sm_scale,
4015
+ logit_cap,
4016
+ is_cross_attn,
4017
+ sliding_window_size,
4018
+ encoder_lens,
4019
+ sinks,
4020
+ tree_mask,
4021
+ )
4022
+
4023
+
4024
+def bmm_cpu(
4025
+ out: torch.Tensor,
4026
+ mat1: torch.Tensor,
4027
+ mat2: torch.Tensor,
4028
+ is_vnni: bool,
4029
+ scale: torch.Tensor | None = None,
4030
+) -> None:
4031
+ torch.ops._C.bmm_cpu(out, mat1, mat2, is_vnni, scale)
4032
+
4033
+
4034
+def amx_mla_concat_and_cache(
4035
+ kv_c_normed: torch.Tensor,
4036
+ k_pe: torch.Tensor,
4037
+ kv_cache: torch.Tensor,
4038
+ slot_mapping: torch.Tensor,
4039
+) -> None:
4040
+ torch.ops._C.concat_and_cache_mla_cpu(kv_c_normed, k_pe, kv_cache, slot_mapping)
4041
+
4042
+
4043
def cpu_gemm_wna16(
4044
input: torch.Tensor,
4045
q_weight: torch.Tensor,
vllm/model_executor/layers/attention/mla_attention.py
+41
@@ -540,6 +540,11 @@ class MLAAttention(nn.Module, AttentionLayerBase):
540
**extra_impl_args,
541
)
542
self.q_pad_num_heads = getattr(self.impl, "q_pad_num_heads", None)
543
+ self.is_amx_bmm_enabled = getattr(self.impl, "uses_amx_bmm", False)
544
+ # AMX reads kv_b_proj's weight directly and never calls it live; the
545
+ # reference CPU MLA backend calls it but isn't perf-critical. Skip
546
+ # the packed-kernel dispatch either way.
547
+ kv_b_proj._cpu_skip_gemm_dispatch = True
548
self.use_direct_call = not current_platform.opaque_attention_op()
549
550
vllm_config = get_current_vllm_config()
@@ -908,6 +913,20 @@ class MLAAttention(nn.Module, AttentionLayerBase):
913
group_size=128,
914
transpose_bm=True,
915
)
916
+ elif self.is_amx_bmm_enabled:
917
+ # bmm_cpu computes out[n] = mat1[n] @ mat2[n]^T against
918
+ # AMXMLAImpl's own (N, L, P) packed W_UK -- same as prefill.
919
+ N, B, P = mqa_q_nope.shape
920
+ L = self.kv_lora_rank
921
+ mqa_ql_nope = mqa_q_nope.new_empty((N, B, L))
922
+ ops.bmm_cpu(
923
+ mqa_ql_nope,
924
+ mqa_q_nope,
925
+ self.impl._w_uk_packed, # type: ignore[attr-defined]
926
+ True,
927
+ None,
928
+ )
929
+ mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
930
else:
931
# Pads the head_dim if necessary (for the underlying kernel)
932
N, B, P = mqa_q_nope.shape
@@ -1026,6 +1045,18 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1045
return output_padded
1046
1047
def process_weights_after_loading(self, act_dtype: torch.dtype):
1048
+ # Let per-backend impls do their own weight packing first (no-op
1049
+ # unless overridden), mirroring Attention.process_weights_after_loading.
1050
+ self.impl.process_weights_after_loading(act_dtype)
1051
+
1052
+ if self.is_amx_bmm_enabled:
1053
+ # AMXMLAImpl already packed its own W_UK/W_UV above, for both
1054
+ # prefill and decode. Release the now-unused raw weight.
1055
+ self.kv_b_proj.weight = torch.nn.Parameter(
1056
+ torch.empty(0), requires_grad=False
1057
+ )
1058
+ return
1059
+
1060
# we currently do not have quantized bmm's which are needed for
1061
# `W_UV` and `W_UK_T`, we just store fp16/bf16 copies and perform
1062
# the bmm's in 16-bit, the extra memory overhead of this is fairly low
@@ -1192,6 +1223,16 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1223
x = rocm_aiter_ops.triton_fp8_bmm(
1224
x, self.W_V, self.W_V_scale, group_size=128, transpose_bm=True, YQ=out
1225
)
1226
+ elif self.is_amx_bmm_enabled:
1227
+ # bmm_cpu computes out[n] = mat1[n] @ mat2[n]^T against
1228
+ # AMXMLAImpl's own (N, V, L) packed W_UV -- same as prefill.
1229
+ ops.bmm_cpu(
1230
+ out.transpose(0, 1),
1231
+ x,
1232
+ self.impl._w_uv_packed, # type: ignore[attr-defined]
1233
+ True,
1234
+ None,
1235
+ )
1236
else:
1237
# Multiply + Transpose (N, B, L) x (N, L, V)->(N, B, V)->(B, N, V)
1238
torch.bmm(x, self.W_UV, out=out.transpose(0, 1))
vllm/model_executor/layers/linear.py
+6
@@ -197,6 +197,12 @@ class UnquantizedLinearMethod(LinearMethodBase):
197
198
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
199
if current_platform.is_cpu():
200
+ # MLA's kv_b_proj (see `_cpu_skip_gemm_dispatch`): not
201
+ # perf-critical, so skip packing and use a plain fallback.
202
+ if getattr(layer, "_cpu_skip_gemm_dispatch", False):
203
+ layer.cpu_linear = torch.nn.functional.linear
204
+ return
205
+
206
from vllm.model_executor.layers.utils import dispatch_cpu_unquantized_gemm
207
208
dispatch_cpu_unquantized_gemm(layer, remove_weight=True)
vllm/platforms/cpu.py
+31
-5
@@ -89,13 +89,30 @@ class CpuPlatform(Platform):
89
if attn_selector_config.use_sparse:
90
raise NotImplementedError("Sparse Attention is not supported on CPU.")
91
if attn_selector_config.use_mla:
92
+ amx_available = (
93
+ cls.get_cpu_architecture() == CpuArchEnum.X86
94
+ and torch.cpu._is_amx_tile_supported()
95
+ )
96
+ if amx_available and selected_backend != AttentionBackendEnum.CPU_MLA:
97
+ # Prefer AMX when available, unless CPU_MLA was explicitly requested.
98
+ if (
99
+ selected_backend
100
+ and selected_backend != AttentionBackendEnum.AMX_MLA
101
+ ):
102
+ logger.info("Cannot use %s backend on CPU.", selected_backend)
103
+ logger.info_once("Using %s backend.", AttentionBackendEnum.AMX_MLA.name)
104
+ return AttentionBackendEnum.AMX_MLA.get_path()
105
# Reference MLA implementation on CPU. Performance is not the
106
# goal here; the backend simply wires the CPU decode kernel
107
# (`mla_decode_kvcache`) and an SDPA-based prefill together with
108
# the shared MLA scaffolding so that DeepSeek-style models can
109
# execute on CPU.
97
- if selected_backend and selected_backend != AttentionBackendEnum.CPU_MLA:
110
+ if selected_backend and selected_backend not in (
111
+ AttentionBackendEnum.CPU_MLA,
112
+ AttentionBackendEnum.AMX_MLA,
113
+ ):
114
logger.info("Cannot use %s backend on CPU.", selected_backend)
115
+ logger.info_once("Using %s backend.", AttentionBackendEnum.CPU_MLA.name)
116
return AttentionBackendEnum.CPU_MLA.get_path()
117
if selected_backend and selected_backend != AttentionBackendEnum.CPU_ATTN:
118
logger.info("Cannot use %s backend on CPU.", selected_backend)
@@ -134,11 +151,20 @@ class CpuPlatform(Platform):
151
# The CPU MLA decode kernel only compiles with block_size=16 today
152
# (see csrc/cpu/mla_decode.cpp). If the model uses MLA we override
153
# the default block size regardless of user preference to avoid a
137
- # runtime kernel dispatch failure.
154
+ # runtime kernel dispatch failure. AMX MLA has no such constraint
155
+ # (same AMX-available condition as get_attn_backend_cls), so it's
156
+ # excluded from this override.
157
cpu_mla_enabled = model_config is not None and getattr(
158
model_config, "use_mla", False
159
)
141
- if cpu_mla_enabled:
160
+ amx_mla_enabled = (
161
+ cpu_mla_enabled
162
+ and cls.get_cpu_architecture() == CpuArchEnum.X86
163
+ and torch.cpu._is_amx_tile_supported()
164
+ and vllm_config.attention_config.backend != AttentionBackendEnum.CPU_MLA
165
+ )
166
+ reference_cpu_mla_enabled = cpu_mla_enabled and not amx_mla_enabled
167
+ if reference_cpu_mla_enabled:
168
if cache_config.user_specified_block_size and cache_config.block_size != 16:
169
logger.warning(
170
"CPU MLA backend requires block_size=16, overriding "
@@ -149,7 +175,7 @@ class CpuPlatform(Platform):
175
elif not cache_config.user_specified_block_size:
176
cache_config.block_size = 128
177
152
- if not cpu_mla_enabled and cache_config.block_size % 32 != 0:
178
+ if not reference_cpu_mla_enabled and cache_config.block_size % 32 != 0:
179
logger.warning(
180
"CPU backend prefers block_size is multiples of 32, "
181
"otherwise the performance is not optimized."
@@ -342,7 +368,7 @@ class CpuPlatform(Platform):
368
vllm_config.parallel_config.tensor_parallel_size
369
)
370
345
- if model_config is not None and model_config.use_mla:
371
+ if model_config is not None and model_config.use_mla and not amx_mla_enabled:
372
logger.info_once(
373
"MLA is enabled on a non-GPU platform; forcing chunked "
374
"prefill and prefix caching to be disabled."
vllm/v1/attention/backends/mla/amx_mla.py
new
+405
@@ -0,0 +1,405 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""AMX-only, high-performance MLA backend for DeepSeek V2/V3/R1 on CPU.
4
+
5
+Built on the AMX decode/extend/bmm kernels vendored under
6
+``csrc/cpu/sgl-kernels/``, plugged into vLLM's ``MLACommonBackend``/
7
+``MLACommonImpl`` abstraction the same way every other concrete MLA backend
8
+(``TritonMLAImpl``, etc.) does.
9
+
10
+This is a separate backend from the reference ``CPUMLABackend``
11
+(``vllm/v1/attention/backends/mla/cpu_mla.py``): that one targets every CPU
12
+(any dtype, block_size=16, ``mla_decode_kvcache`` decode kernel + inherited
13
+SDPA-style prefill) as a functional/CI reference, not performance. This
14
+backend instead requires AMX (bf16 only, block_size a multiple of 32) and is
15
+selected by the platform layer in preference to the reference backend
16
+whenever the host supports it -- see ``CpuPlatform.get_attn_backend_cls``.
17
+
18
+Two points where this backend differs structurally from the GPU backends,
19
+both explained in the CPU MLA design plan:
20
+
21
+- ``forward_mha`` is fully overridden (not inherited from
22
+ ``MLACommonBaseImpl``): the GPU "compute-friendly" prefill path depends on
23
+ a pluggable ``MLAPrefillBackend`` and CUDA-only chunked-context gather ops,
24
+ neither of which exist on CPU. Instead, this attends directly in
25
+ latent-MQA space via the ``extend_attention_cpu`` kernel, which handles
26
+ cached-prefix continuation and fresh prefill in one causal pass (the KV
27
+ cache already contains the new tokens by the time this runs, since
28
+ ``do_kv_cache_update`` executes first).
29
+- Weight absorption for the prefill path is done with this impl's own
30
+ VNNI-packed copies of ``W_UK``/``W_UV`` (computed in
31
+ ``process_weights_after_loading``, using ``bmm_cpu``), rather than reading
32
+ the generic ``layer.W_UK_T``/``layer.W_UV``/``layer._v_up_proj`` the way
33
+ GPU backends do, since ``forward_mha``'s abstract signature has no
34
+ ``layer`` parameter (only ``forward_mqa`` does) and that signature is left
35
+ untouched. The decode path needs no such packing: ``MLAAttention.forward_impl``
36
+ already absorbs/de-absorbs Q around the ``forward_mqa`` call using the
37
+ generic (unpacked) ``layer.W_UK_T``/``layer._v_up_proj``, so
38
+ ``forward_mqa`` here only has to invoke the decode kernel.
39
+"""
40
+
41
+from __future__ import annotations
42
+
43
+from typing import ClassVar
44
+
45
+import torch
46
+
47
+from vllm import _custom_ops as ops
48
+from vllm.model_executor.layers.attention.mla_attention import (
49
+ MLACommonBackend,
50
+ MLACommonImpl,
51
+ MLACommonMetadata,
52
+ MLACommonMetadataBuilder,
53
+)
54
+from vllm.model_executor.layers.quantization.utils.quant_utils import (
55
+ get_and_maybe_dequant_weights,
56
+)
57
+from vllm.platforms import current_platform
58
+from vllm.v1.attention.backend import AttentionLayer, AttentionType, MultipleOf
59
+
60
+_MIN_WORK_PER_SPLIT = 512
61
+_SPLIT_OCCUPANCY_MULTIPLIER = 2
62
+
63
+
64
+def _compute_num_kv_splits(max_seq_len: int, num_threads: int) -> int:
65
+ """Mirrors TritonMLAImpl's _compute_num_kv_splits, using the CPU thread
66
+ count in place of SM count."""
67
+ ideal_splits = 1
68
+ while ideal_splits < max(1, max_seq_len // _MIN_WORK_PER_SPLIT):
69
+ ideal_splits *= 2
70
+ max_splits = num_threads * _SPLIT_OCCUPANCY_MULTIPLIER
71
+ return min(ideal_splits, max_splits)
72
+
73
+
74
+def _expand_block_table(block_table: torch.Tensor, block_size: int) -> torch.Tensor:
75
+ """Adapter: vLLM's block-table paging -> the flat per-(request, position)
76
+ physical row index the decode/extend kernels expect. Called once per step
77
+ from ``AMXMLAMetadataBuilder.build``, not per layer.
78
+ """
79
+ offsets = torch.arange(
80
+ block_size, device=block_table.device, dtype=block_table.dtype
81
+ )
82
+ flat = block_table.unsqueeze(-1) * block_size + offsets
83
+ return flat.reshape(block_table.size(0), -1).contiguous()
84
+
85
+
86
+class AMXMLABackend(MLACommonBackend):
87
+ supported_dtypes: ClassVar[list[torch.dtype]] = [torch.bfloat16]
88
+
89
+ @classmethod
90
+ def get_supported_head_sizes(cls) -> list[int]:
91
+ # kv_lora_rank(512) + qk_rope_head_dim(64), DeepSeek V2/V3/R1/V3.2.
92
+ return [576]
93
+
94
+ @staticmethod
95
+ def get_supported_kernel_block_sizes() -> list[int | MultipleOf]:
96
+ return [MultipleOf(32)]
97
+
98
+ @classmethod
99
+ def supports_block_size(cls, block_size: int | None) -> bool:
100
+ if block_size is None:
101
+ return True
102
+ return block_size % 32 == 0
103
+
104
+ @staticmethod
105
+ def get_name() -> str:
106
+ return "AMX_MLA"
107
+
108
+ @staticmethod
109
+ def get_impl_cls() -> type[AMXMLAImpl]:
110
+ return AMXMLAImpl
111
+
112
+ @staticmethod
113
+ def get_builder_cls() -> type[AMXMLAMetadataBuilder]:
114
+ return AMXMLAMetadataBuilder
115
+
116
+
117
+class AMXMLAMetadataBuilder(MLACommonMetadataBuilder[MLACommonMetadata]):
118
+ def build(self, common_prefix_len, common_attn_metadata, fast_build: bool = False):
119
+ attn_metadata = super().build(
120
+ common_prefix_len, common_attn_metadata, fast_build
121
+ )
122
+ block_size = self.kv_cache_spec.block_size
123
+ if attn_metadata.decode is not None:
124
+ # Built once per step and reused by every layer, instead of
125
+ # recomputing the same tensor arithmetic in every forward_mqa call.
126
+ decode = attn_metadata.decode
127
+ decode.req_to_token = _expand_block_table( # type: ignore[attr-defined]
128
+ decode.block_table, block_size
129
+ )
130
+ decode.seq_lens_i64 = decode.seq_lens.to(torch.int64) # type: ignore[attr-defined]
131
+ decode.req_pool_indices = torch.arange( # type: ignore[attr-defined]
132
+ decode.block_table.size(0),
133
+ dtype=torch.int64,
134
+ device=decode.block_table.device,
135
+ )
136
+ decode.num_kv_splits = _compute_num_kv_splits( # type: ignore[attr-defined]
137
+ attn_metadata.max_seq_len, current_platform.num_compute_units()
138
+ )
139
+ if attn_metadata.prefill is not None:
140
+ # cpu_seq_lens: total per-request context length (prefix + new
141
+ # tokens), the one thing the extend kernel needs that isn't
142
+ # already on the shared MLACommonPrefillMetadata.
143
+ prefill = attn_metadata.prefill
144
+ num_decodes = attn_metadata.num_decodes
145
+ num_prefills = attn_metadata.num_prefills
146
+ prefill.cpu_seq_lens = common_attn_metadata.seq_lens[ # type: ignore[attr-defined]
147
+ num_decodes : num_decodes + num_prefills
148
+ ].to(torch.int64)
149
+ prefill.req_to_token = _expand_block_table( # type: ignore[attr-defined]
150
+ prefill.block_table, block_size
151
+ ).to(torch.int64)
152
+ prefill.req_pool_indices = torch.arange( # type: ignore[attr-defined]
153
+ prefill.block_table.size(0),
154
+ dtype=torch.int64,
155
+ device=prefill.block_table.device,
156
+ )
157
+ query_start_loc_i64 = prefill.query_start_loc.to(torch.int64)
158
+ extend_seq_lens = query_start_loc_i64[1:] - query_start_loc_i64[:-1]
159
+ prefill.extend_seq_lens = extend_seq_lens # type: ignore[attr-defined]
160
+ prefill.extend_start_loc = query_start_loc_i64[:-1] # type: ignore[attr-defined]
161
+ prefill.max_len_extend = int(extend_seq_lens.max().item()) # type: ignore[attr-defined]
162
+ return attn_metadata
163
+
164
+
165
+class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
166
+ # Tells MLAAttention (mla_attention.py) to use this impl's own packed
167
+ # W_UK/W_UV + bmm_cpu for the shared decode absorb/de-absorb, mirroring
168
+ # is_aiter_triton_fp8_bmm_enabled/is_aiter_triton_fp4_bmm_enabled.
169
+ uses_amx_bmm = True
170
+
171
+ def __init__(
172
+ self,
173
+ num_heads: int,
174
+ head_size: int,
175
+ scale: float,
176
+ num_kv_heads: int,
177
+ alibi_slopes: list[float] | None,
178
+ sliding_window: int | None,
179
+ kv_cache_dtype: str,
180
+ logits_soft_cap: float | None,
181
+ attn_type: str,
182
+ kv_sharing_target_layer_name: str | None,
183
+ **mla_args,
184
+ ) -> None:
185
+ super().__init__(
186
+ num_heads,
187
+ head_size,
188
+ scale,
189
+ num_kv_heads,
190
+ alibi_slopes,
191
+ sliding_window,
192
+ kv_cache_dtype,
193
+ logits_soft_cap,
194
+ attn_type,
195
+ kv_sharing_target_layer_name,
196
+ **mla_args,
197
+ )
198
+ unsupported_features = [alibi_slopes, sliding_window, logits_soft_cap]
199
+ if any(unsupported_features):
200
+ raise NotImplementedError(
201
+ "AMXMLAImpl does not support one of the following: "
202
+ "alibi_slopes, sliding_window, logits_soft_cap"
203
+ )
204
+ if attn_type != AttentionType.DECODER:
205
+ raise NotImplementedError(
206
+ "Encoder self-attention and encoder/decoder cross-attention "
207
+ "are not implemented for AMXMLAImpl"
208
+ )
209
+ self._w_uk_packed: torch.Tensor | None = None
210
+ self._w_uv_packed: torch.Tensor | None = None
211
+
212
+ def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
213
+ # forward_mha (unlike forward_mqa) receives no `layer` argument, so it
214
+ # cannot read the generic layer.W_UK_T/W_UV/._v_up_proj that
215
+ # MLAAttention.process_weights_after_loading already computes. This
216
+ # impl derives its own copies from self.kv_b_proj (already available
217
+ # via the constructor) and VNNI-packs them once, ahead of time, for
218
+ # use with bmm_cpu in forward_mha.
219
+ kv_b_proj_weight = get_and_maybe_dequant_weights(
220
+ self.kv_b_proj, out_dtype=act_dtype
221
+ ).T
222
+ kv_b_proj_weight = kv_b_proj_weight.view(
223
+ self.kv_lora_rank, self.num_heads, self.qk_nope_head_dim + self.v_head_dim
224
+ )
225
+ w_uk, w_uv = kv_b_proj_weight.split(
226
+ [self.qk_nope_head_dim, self.v_head_dim], dim=-1
227
+ )
228
+ # bmm_cpu computes out[b] = mat1[b] @ mat2[b]^T (Linear-weight
229
+ # convention: mat2[b] is (OUT, IN)).
230
+ # absorb: ql_nope(N,B,L) = q_nope(N,B,P) @ w_uk_for_bmm(N,L,P)^T
231
+ # de-absorb: out(N,B,V) = attn_out(N,B,L) @ w_uv_for_bmm(N,V,L)^T
232
+ w_uk_for_bmm = w_uk.permute(1, 0, 2).contiguous() # (N, L, P)
233
+ w_uv_for_bmm = w_uv.permute(1, 2, 0).contiguous() # (N, V, L)
234
+ self._w_uk_packed = torch.ops._C.convert_weight_packed(w_uk_for_bmm)
235
+ self._w_uv_packed = torch.ops._C.convert_weight_packed(w_uv_for_bmm)
236
+
237
+ def do_kv_cache_update(
238
+ self,
239
+ kv_c_normed: torch.Tensor,
240
+ k_pe: torch.Tensor,
241
+ kv_cache: torch.Tensor,
242
+ slot_mapping: torch.Tensor,
243
+ kv_cache_dtype: str,
244
+ k_scale: torch.Tensor,
245
+ ) -> None:
246
+ # Overrides the default (CUDA-only concat_and_cache_mla).
247
+ if kv_cache.numel() == 0:
248
+ return
249
+ assert kv_cache_dtype == "auto", (
250
+ "AMXMLAImpl only supports an unquantized (bf16) KV cache; fp8 "
251
+ "KV cache for CPU MLA is not yet implemented."
252
+ )
253
+ ops.amx_mla_concat_and_cache(
254
+ kv_c_normed, k_pe.squeeze(1), kv_cache, slot_mapping.flatten()
255
+ )
256
+
257
+ def forward_mqa(
258
+ self,
259
+ q: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
260
+ kv_c_and_k_pe_cache: torch.Tensor,
261
+ attn_metadata: MLACommonMetadata,
262
+ layer: AttentionLayer,
263
+ ) -> tuple[torch.Tensor, torch.Tensor | None]:
264
+ # q arrives already absorbed (tensor or (ql_nope, q_pe) tuple) --
265
+ # MLAAttention.forward_impl does the Q-absorption bmm generically
266
+ # before calling forward_mqa, using layer.W_UK_T. De-absorption is
267
+ # likewise done generically afterwards via layer._v_up_proj. Nothing
268
+ # left for this impl to do beyond invoking the decode kernel.
269
+ if isinstance(q, tuple):
270
+ q = torch.cat(q, dim=-1)
271
+ assert attn_metadata.decode is not None
272
+
273
+ num_tokens = q.shape[0]
274
+ kv_cache_flat = kv_c_and_k_pe_cache.view(-1, 1, self.head_size)
275
+ v_buffer = kv_cache_flat[..., : self.kv_lora_rank]
276
+
277
+ seq_lens = attn_metadata.decode.seq_lens_i64 # type: ignore[attr-defined]
278
+ req_to_token = attn_metadata.decode.req_to_token # type: ignore[attr-defined]
279
+ req_pool_indices = attn_metadata.decode.req_pool_indices # type: ignore[attr-defined]
280
+
281
+ num_kv_splits = attn_metadata.decode.num_kv_splits # type: ignore[attr-defined]
282
+ o = torch.zeros(
283
+ num_tokens,
284
+ self.num_heads,
285
+ self.kv_lora_rank,
286
+ dtype=q.dtype,
287
+ device=q.device,
288
+ )
289
+ attn_logits = torch.zeros(
290
+ num_tokens,
291
+ self.num_heads,
292
+ num_kv_splits,
293
+ self.kv_lora_rank + 1,
294
+ dtype=torch.float32,
295
+ device=q.device,
296
+ )
297
+
298
+ ops.cpu_mla_decode(
299
+ q,
300
+ kv_cache_flat,
301
+ v_buffer,
302
+ o,
303
+ None,
304
+ None,
305
+ None, # loc: only meaningful when key/value are given; we
306
+ # already wrote the new K/V via do_kv_cache_update before this.
307
+ attn_logits,
308
+ req_to_token,
309
+ req_pool_indices,
310
+ seq_lens,
311
+ self.scale,
312
+ 0.0,
313
+ False,
314
+ 0,
315
+ None,
316
+ None,
317
+ )
318
+ return o, None
319
+
320
+ def forward_mha(
321
+ self,
322
+ q: torch.Tensor,
323
+ kv_c_normed: torch.Tensor,
324
+ k_pe: torch.Tensor,
325
+ kv_c_and_k_pe_cache: torch.Tensor,
326
+ attn_metadata: MLACommonMetadata,
327
+ k_scale: torch.Tensor,
328
+ output: torch.Tensor,
329
+ output_scale: torch.Tensor | None = None,
330
+ ) -> None:
331
+ assert output_scale is None, (
332
+ "AMXMLAImpl.forward_mha does not support fused output quantization"
333
+ )
334
+ prefill = attn_metadata.prefill
335
+ assert prefill is not None
336
+ assert self._w_uk_packed is not None and self._w_uv_packed is not None
337
+
338
+ num_tokens = q.shape[0]
339
+ num_heads = self.num_heads
340
+
341
+ q_nope, q_pe = q.split([self.qk_nope_head_dim, self.qk_rope_head_dim], dim=-1)
342
+
343
+ # Absorb: (N, B, P) x (N, L, P)^T -> (N, B, L)
344
+ # bmm_cpu tolerates non-contiguous mat1/out as long as their last dim
345
+ # is contiguous, so q_nope's transpose and ql_nope's transposed view
346
+ # can be passed directly -- no copy needed for either.
347
+ q_nope_t = q_nope.transpose(0, 1)
348
+ ql_nope = torch.empty(
349
+ num_tokens, num_heads, self.kv_lora_rank, dtype=q.dtype, device=q.device
350
+ )
351
+ ops.bmm_cpu(ql_nope.transpose(0, 1), q_nope_t, self._w_uk_packed, True, None)
352
+ mqa_q = torch.cat([ql_nope, q_pe], dim=-1)
353
+
354
+ kv_cache_flat = kv_c_and_k_pe_cache.view(-1, 1, self.head_size)
355
+ v_buffer = kv_cache_flat[..., : self.kv_lora_rank]
356
+
357
+ req_to_token = prefill.req_to_token # type: ignore[attr-defined]
358
+ req_pool_indices = prefill.req_pool_indices # type: ignore[attr-defined]
359
+
360
+ seq_lens = prefill.cpu_seq_lens # type: ignore[attr-defined]
361
+ extend_seq_lens = prefill.extend_seq_lens # type: ignore[attr-defined]
362
+ extend_start_loc = prefill.extend_start_loc # type: ignore[attr-defined]
363
+ max_len_extend = prefill.max_len_extend # type: ignore[attr-defined]
364
+
365
+ # k_extend/v_extend: the new tokens' own latent K/V, aliased in one
366
+ # 576-wide buffer (v_extend is a view of k_extend's first
367
+ # kv_lora_rank columns) so the kernel can gather both from one read,
368
+ # mirroring the cache's own layout.
369
+ k_extend = torch.cat(
370
+ [kv_c_normed, k_pe.reshape(num_tokens, self.qk_rope_head_dim)], dim=-1
371
+ ).unsqueeze(1)
372
+ v_extend = k_extend[..., : self.kv_lora_rank]
373
+
374
+ attn_out = torch.empty(
375
+ num_tokens, num_heads, self.kv_lora_rank, dtype=q.dtype, device=q.device
376
+ )
377
+ ops.cpu_mla_extend(
378
+ mqa_q,
379
+ k_extend,
380
+ v_extend,
381
+ attn_out,
382
+ kv_cache_flat,
383
+ v_buffer,
384
+ req_to_token,
385
+ req_pool_indices,
386
+ seq_lens,
387
+ extend_seq_lens,
388
+ extend_start_loc,
389
+ max_len_extend,
390
+ self.scale,
391
+ 0.0,
392
+ False,
393
+ 0,
394
+ None,
395
+ None,
396
+ None,
397
+ )
398
+
399
+ # De-absorb directly into `output` (view, no extra copy on either side):
400
+ # (N, B, L) x (N, V, L)^T -> (N, B, V)
401
+ attn_out_t = attn_out.transpose(0, 1)
402
+ output_view = output.view(num_tokens, num_heads, self.v_head_dim).transpose(
403
+ 0, 1
404
+ )
405
+ ops.bmm_cpu(output_view, attn_out_t, self._w_uv_packed, True, None)
vllm/v1/attention/backends/mla/prefill/cpu_native.py
new
+61
@@ -0,0 +1,61 @@
1
+# SPDX-License-Identifier: Apache-2.0
2
+# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+"""Inert MLA prefill backend for the CPU platform.
4
+
5
+CPU's MLA attention impl (``CPUMLAImpl``) fully overrides ``forward_mha`` with a
6
+kernel that attends directly against the paged latent KV cache (covering both
7
+fresh prefill and cached-prefix continuation in one pass), so it never calls
8
+into a pluggable ``MLAPrefillBackend``'s ``run_prefill_new_tokens``/
9
+``run_prefill_context_chunk``. This class exists only to satisfy
10
+``MLACommonMetadataBuilder``'s structural requirement that
11
+``MLAAttention.prefill_backend`` be a real, cloneable object.
12
+"""
13
+
14
+from typing import TYPE_CHECKING
15
+
16
+import torch
17
+
18
+from vllm.v1.attention.backends.mla.prefill.base import MLAPrefillBackend
19
+
20
+if TYPE_CHECKING:
21
+ from vllm.model_executor.layers.attention.mla_attention import (
22
+ MLACommonPrefillMetadata,
23
+ )
24
+
25
+
26
+class CPUNativeMLAPrefillBackend(MLAPrefillBackend):
27
+ """Placeholder MLA prefill backend for CPU; never actually invoked."""
28
+
29
+ @staticmethod
30
+ def get_name() -> str:
31
+ return "CPU_NATIVE"
32
+
33
+ def run_prefill_new_tokens(
34
+ self,
35
+ q: torch.Tensor,
36
+ k: torch.Tensor,
37
+ v: torch.Tensor,
38
+ return_softmax_lse: bool,
39
+ out: torch.Tensor | None = None,
40
+ output_scale: torch.Tensor | None = None,
41
+ ) -> torch.Tensor | tuple[torch.Tensor, torch.Tensor]:
42
+ raise AssertionError(
43
+ "CPUNativeMLAPrefillBackend.run_prefill_new_tokens is unreachable: "
44
+ "CPUMLAImpl.forward_mha fully overrides the dense-MHA prefill path "
45
+ "and never calls the pluggable prefill backend."
46
+ )
47
+
48
+ def run_prefill_context_chunk(
49
+ self,
50
+ chunk: "MLACommonPrefillMetadata.ContextChunk",
51
+ q: torch.Tensor,
52
+ k: torch.Tensor,
53
+ v: torch.Tensor,
54
+ out: torch.Tensor | None = None,
55
+ ) -> tuple[torch.Tensor, torch.Tensor]:
56
+ raise AssertionError(
57
+ "CPUNativeMLAPrefillBackend.run_prefill_context_chunk is "
58
+ "unreachable: CPUMLAImpl.forward_mha fully overrides the "
59
+ "dense-MHA prefill path and never calls the pluggable prefill "
60
+ "backend."
61
+ )
vllm/v1/attention/backends/mla/prefill/registry.py
+3
@@ -52,6 +52,9 @@ class MLAPrefillBackendEnum(Enum, metaclass=_MLAPrefillBackendEnumMeta):
52
"vllm.v1.attention.backends.mla.prefill.aiter_flash_attn."
53
"AiterFlashAttnPrefillBackend"
54
)
55
+ CPU_NATIVE = (
56
+ "vllm.v1.attention.backends.mla.prefill.cpu_native.CPUNativeMLAPrefillBackend"
57
+ )
58
# Placeholder for third-party/custom backends - must be registered before use
59
# set to None to avoid alias with other backend, whose value is an empty string
60
CUSTOM = None
vllm/v1/attention/backends/mla/prefill/selector.py
+2
@@ -109,6 +109,8 @@ def get_mla_prefill_backend(
109
110
device_capability = current_platform.get_device_capability()
111
if device_capability is None:
112
+ if current_platform.is_cpu():
113
+ return MLAPrefillBackendEnum.CPU_NATIVE.get_class()
114
logger.info_once(
115
"Device capability not available, using FlashAttention MLA prefill backend."
116
)
vllm/v1/attention/backends/registry.py
+1
@@ -123,6 +123,7 @@ class AttentionBackendEnum(Enum, metaclass=_AttentionBackendEnumMeta):
123
)
124
CPU_ATTN = "vllm.v1.attention.backends.cpu_attn.CPUAttentionBackend"
125
CPU_MLA = "vllm.v1.attention.backends.mla.cpu_mla.CPUMLABackend"
126
+ AMX_MLA = "vllm.v1.attention.backends.mla.amx_mla.AMXMLABackend"
127
TURBOQUANT = "vllm.v1.attention.backends.turboquant_attn.TurboQuantAttentionBackend"
128
# Placeholder for third-party/custom backends - must be registered before use
129
# set to None to avoid alias with other backend, whose value is an empty string