main
cpp 1,461 lines 52.1 KB
Raw
1 // Adapted from
2 // https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
4 // clang-format off
5
6 #include "moe.h"
7
8 #include "common.h"
9 #include "gemm.h"
10
11 namespace {
12
13 // [NOTE]: Fused MoE kernel with AMX
14 //
15 // This file contains implementations for
16 // * `moe_align_block_size`
17 // * `fused_moe`
18 //
19 // The functionality is identical to triton kernel, excepts:
20 // * fuse silu_and_mul with gemm1, therefore this kernel
21 // allocates 2 intermediate_caches instead of 3
22 // * add `offsets` in `moe_align_block_size` which keeps track
23 // of starting offset for each M block. this is for keeping
24 // output of silu_and_mul in sorted order, thus load_A for
25 // the 2nd gemm would be contiguous, therefore we can directly
26 // load A from intermediate_cache1.
27 //
28 // TODO:
29 // 1. tune BLOCK_M and BLOCK_N (BLOCK_N * K fit L2)
30 // 2. add prefetch for load A which is indexed access
31 // 3. abstract at::native::cpublas::brgemm with WoQ gemm (M = 1 & M != 1)
32 //
33
34 template <int BLOCK_M>
35 int moe_align_block_size(
36 int32_t* __restrict__ sorted_ids,
37 int32_t* __restrict__ expert_ids,
38 int32_t* __restrict__ topk_ids,
39 int32_t* __restrict__ total_cnts,
40 int32_t* __restrict__ cumsums,
41 int32_t* __restrict__ offsets,
42 int num_experts,
43 int numel,
44 int num_threads) {
45 #define T_INDEX(tt) total_cnts + (tt) * num_experts
46
47 // accumulate count of expert ids locally
48 at::parallel_for(0, numel, 0, [&](int begin, int end) {
49 int tid = at::get_thread_num();
50 int32_t* __restrict__ local_cnts = T_INDEX(tid + 1);
51
52 for (int i = begin; i < end; ++i) {
53 local_cnts[topk_ids[i]]++;
54 }
55 });
56
57 using iVec = at::vec::Vectorized<int32_t>;
58 for (int t = 0; t < num_threads; ++t) {
59 at::vec::map2<int32_t>(
60 [](iVec x, iVec y) { return x + y; }, T_INDEX(t + 1), T_INDEX(t + 1), T_INDEX(t), num_experts);
61 }
62
63 // the last row holds sums of each experts
64 int32_t* total_cnts_t_1 = T_INDEX(num_threads);
65
66 cumsums[0] = 0;
67 for (int e = 0; e < num_experts; ++e) {
68 // accumulate `num_tokens_post_pad`, also as the expert offset
69 cumsums[e + 1] = cumsums[e] + div_up(total_cnts_t_1[e], BLOCK_M) * BLOCK_M;
70
71 for (int k = cumsums[e]; k < cumsums[e + 1]; k += BLOCK_M) {
72 expert_ids[k / BLOCK_M] = e;
73 }
74 }
75 int num_tokens_post_pad = cumsums[num_experts];
76
77 at::parallel_for(0, numel, 0, [&](int begin, int end) {
78 int tid = at::get_thread_num();
79 // thread tid offsets in `total_cnts`
80 int32_t* __restrict__ offsets = T_INDEX(tid);
81
82 for (int i = begin; i < end; ++i) {
83 int32_t expert_id = topk_ids[i];
84 int32_t b_offset = cumsums[expert_id];
85 int32_t t_offset = offsets[expert_id];
86 sorted_ids[b_offset + t_offset] = i;
87 offsets[expert_id]++;
88 }
89 });
90
91 // debug: the offset for thread t_1 should be identical to t_2
92 int32_t* total_cnts_t_2 = T_INDEX(num_threads - 1);
93 for (int e = 0; e < num_experts; ++e) {
94 TORCH_CHECK(total_cnts_t_1[e] == total_cnts_t_2[e]);
95 }
96
97 // padding value for sorted_ids: numel
98 auto sorted_id_size = [=](const int32_t* sorted_ids_ptr) {
99 for (int d = 0; d < BLOCK_M; ++d) {
100 if (sorted_ids_ptr[d] == numel) {
101 return d;
102 }
103 }
104 return BLOCK_M;
105 };
106
107 // offsets holds starting offset for each valida M blocks
108 // shape : [num_token_blocks + 1]
109 offsets[0] = 0;
110 const int num_token_blocks = num_tokens_post_pad / BLOCK_M;
111 at::parallel_for(0, num_token_blocks, GRAIN_SIZE / BLOCK_M, [&](int begin, int end) {
112 for (int mb = begin; mb < end; ++mb) {
113 offsets[mb + 1] = sorted_id_size(sorted_ids + mb * BLOCK_M);
114 }
115 });
116 // TODO: do we need to vecterize this ?
117 for (int mb = 0; mb < num_token_blocks; ++mb) {
118 offsets[mb + 1] += offsets[mb];
119 }
120 // debug: the last value of offsets should be `numel`
121 TORCH_CHECK(offsets[num_token_blocks] == numel);
122
123 return num_tokens_post_pad;
124 }
125
126 template <typename scalar_t, int BLOCK_M, int BLOCK_N>
127 struct tinygemm_kernel_nn2 {
128 static inline void apply(
129 const scalar_t* __restrict__ A,
130 const scalar_t* __restrict__ B0,
131 const scalar_t* __restrict__ B1,
132 scalar_t* __restrict__ C,
133 int64_t K,
134 int64_t lda,
135 int64_t ldb,
136 int64_t ldc) {
137 TORCH_CHECK(false, "tinygemm_kernel_nn: scalar path not implemented!");
138 }
139 };
140
141 #if defined(CPU_CAPABILITY_AVX512)
142 template <int BLOCK_M, int BLOCK_N>
143 struct tinygemm_kernel_nn2<at::BFloat16, BLOCK_M, BLOCK_N> {
144 static inline void apply(
145 const at::BFloat16* __restrict__ A,
146 const at::BFloat16* __restrict__ B0,
147 const at::BFloat16* __restrict__ B1,
148 at::BFloat16* __restrict__ C,
149 int64_t K,
150 int64_t lda,
151 int64_t ldb,
152 int64_t ldc) {
153 constexpr int ROWS = BLOCK_M;
154 constexpr int COLS = BLOCK_N / 16;
155
156 static_assert(COLS % 2 == 0);
157
158 // prefetch distance
159 constexpr int PREFETCH_SIZE_K = 0;
160
161 __m512bh va;
162 __m512bh vb0[COLS];
163 __m512bh vb1[COLS];
164 __m512 vc0[ROWS * COLS];
165 __m512 vc1[ROWS * COLS];
166
167 auto loadc = [&](auto i) {
168 vc0[i] = _mm512_set1_ps(0.f);
169 vc1[i] = _mm512_set1_ps(0.f);
170 };
171 Unroll<ROWS * COLS>{}(loadc);
172
173 const int64_t K2 = K >> 1;
174 const int64_t lda2 = lda >> 1;
175 const int64_t ldb2 = ldb; // ldb * 2 >> 1;
176 const float* a_ptr = reinterpret_cast<const float*>(A);
177 const float* b0_ptr = reinterpret_cast<const float*>(B0);
178 const float* b1_ptr = reinterpret_cast<const float*>(B1);
179
180 auto compute = [&](auto i, int64_t k) {
181 constexpr int row = i / COLS;
182 constexpr int col = i % COLS;
183
184 if constexpr (col == 0) {
185 va = (__m512bh)(_mm512_set1_ps(a_ptr[row * lda2 + k]));
186 }
187 if constexpr (row == 0) {
188 vb0[col] = (__m512bh)(_mm512_loadu_si512(b0_ptr + k * ldb2 + col * 16));
189 vb1[col] = (__m512bh)(_mm512_loadu_si512(b1_ptr + k * ldb2 + col * 16));
190 if constexpr (PREFETCH_SIZE_K > 0) {
191 _mm_prefetch(b0_ptr + (k + PREFETCH_SIZE_K) * ldb2 + col * 16, _MM_HINT_T0);
192 _mm_prefetch(b1_ptr + (k + PREFETCH_SIZE_K) * ldb2 + col * 16, _MM_HINT_T0);
193 }
194 }
195 vc0[i] = _mm512_dpbf16_ps(vc0[i], va, vb0[col]);
196 vc1[i] = _mm512_dpbf16_ps(vc1[i], va, vb1[col]);
197 };
198 for (int64_t k = 0; k < K2; ++k) {
199 Unroll<ROWS * COLS>{}(compute, k);
200 }
201
202 auto storec = [&](auto i) {
203 constexpr int row = i / COLS;
204 constexpr int col = i % COLS;
205 // for COLS = 2, 4 use 512bit store
206 if constexpr (col % 2 == 0) {
207 __m512 x0 = vc0[row * COLS + col + 0];
208 __m512 x1 = vc0[row * COLS + col + 1];
209 __m512 y0 = vc1[row * COLS + col + 0];
210 __m512 y1 = vc1[row * COLS + col + 1];
211 x0 = _mm512_mul_ps(_mm512_rcp14_silu_ps(x0), y0);
212 x1 = _mm512_mul_ps(_mm512_rcp14_silu_ps(x1), y1);
213
214 _mm512_storeu_si512(
215 reinterpret_cast<__m512i*>((C + row * ldc + col * 16)),
216 (__m512i)(_mm512_cvtne2ps_pbh(__m512(x1), __m512(x0))));
217 }
218 };
219 Unroll<ROWS * COLS>{}(storec);
220 }
221 };
222 #endif
223
224 #define LAUNCH_TINYGEMM_KERNEL_NN(MB_SIZE, NB_SIZE) \
225 tinygemm_kernel_nn2<scalar_t, MB_SIZE, NB_SIZE>::apply( \
226 A + mb_start * lda, B0 + nb_start * 2, B1 + nb_start * 2, C + mb_start * ldc + nb_start, K, lda, ldb, ldc);
227
228 template <typename scalar_t>
229 void tinygemm_kernel(
230 const scalar_t* __restrict__ A,
231 const scalar_t* __restrict__ B0,
232 const scalar_t* __restrict__ B1,
233 scalar_t* __restrict__ C,
234 int64_t M,
235 int64_t N,
236 int64_t K,
237 int64_t lda,
238 int64_t ldb,
239 int64_t ldc) {
240 // pattern: 1-(2+2)-(8+8)
241 constexpr int64_t BLOCK_M = 4;
242 constexpr int64_t BLOCK_N = 32;
243 const int64_t MB = div_up(M, BLOCK_M);
244 const int64_t NB = div_up(N, BLOCK_N);
245 for (int mb = 0; mb < MB; ++mb) {
246 int64_t mb_start = mb * BLOCK_M;
247 int64_t mb_size = std::min(BLOCK_M, M - mb_start);
248 for (int64_t nb = 0; nb < NB; ++nb) {
249 int64_t nb_start = nb * BLOCK_N;
250 int64_t nb_size = std::min(BLOCK_N, N - nb_start);
251
252 switch (mb_size << 4 | nb_size >> 4) {
253 // mb_size = 1
254 case 0x12:
255 LAUNCH_TINYGEMM_KERNEL_NN(1, 32);
256 break;
257 // mb_size = 2
258 case 0x22:
259 LAUNCH_TINYGEMM_KERNEL_NN(2, 32);
260 break;
261 // mb_size = 3
262 case 0x32:
263 LAUNCH_TINYGEMM_KERNEL_NN(3, 32);
264 break;
265 // mb_size = 4
266 case 0x42:
267 LAUNCH_TINYGEMM_KERNEL_NN(4, 32);
268 break;
269 default:
270 TORCH_CHECK(false, "Unexpected block size, ", mb_size, "x", "nb_size");
271 }
272 }
273 }
274 }
275
276 template <typename scalar_t, int BLOCK_M, int BLOCK_N>
277 struct tinygemm_kernel_nn {
278 static inline void apply(
279 const scalar_t* __restrict__ A,
280 const scalar_t* __restrict__ B,
281 float* __restrict__ C,
282 int64_t K,
283 int64_t lda,
284 int64_t ldb,
285 int64_t ldc) {
286 TORCH_CHECK(false, "tinygemm_kernel_nn: scalar path not implemented!");
287 }
288 };
289
290 #if defined(CPU_CAPABILITY_AVX512)
291 template <int BLOCK_M, int BLOCK_N>
292 struct tinygemm_kernel_nn<at::BFloat16, BLOCK_M, BLOCK_N> {
293 static inline void apply(
294 const at::BFloat16* __restrict__ A,
295 const at::BFloat16* __restrict__ B,
296 float* __restrict__ C,
297 int64_t K,
298 int64_t lda,
299 int64_t ldb,
300 int64_t ldc) {
301 constexpr int ROWS = BLOCK_M;
302 constexpr int COLS = BLOCK_N / 16;
303
304 static_assert(COLS % 2 == 0);
305
306 // prefetch distance
307 constexpr int PREFETCH_SIZE_K = 0;
308
309 __m512bh va;
310 __m512bh vb[COLS];
311 __m512 vc[ROWS * COLS];
312
313 auto loadc = [&](auto i) { vc[i] = _mm512_set1_ps(0.f); };
314 Unroll<ROWS * COLS>{}(loadc);
315
316 const int64_t K2 = K >> 1;
317 const int64_t lda2 = lda >> 1;
318 const int64_t ldb2 = ldb; // ldb * 2 >> 1;
319 const float* a_ptr = reinterpret_cast<const float*>(A);
320 const float* b_ptr = reinterpret_cast<const float*>(B);
321
322 auto compute = [&](auto i, int64_t k) {
323 constexpr int row = i / COLS;
324 constexpr int col = i % COLS;
325
326 if constexpr (col == 0) {
327 va = (__m512bh)(_mm512_set1_ps(a_ptr[row * lda2 + k]));
328 }
329 if constexpr (row == 0) {
330 vb[col] = (__m512bh)(_mm512_loadu_si512(b_ptr + k * ldb2 + col * 16));
331 if constexpr (PREFETCH_SIZE_K > 0) {
332 _mm_prefetch(b_ptr + (k + PREFETCH_SIZE_K) * ldb2 + col * 16, _MM_HINT_T0);
333 }
334 }
335 vc[i] = _mm512_dpbf16_ps(vc[i], va, vb[col]);
336 };
337 for (int64_t k = 0; k < K2; ++k) {
338 Unroll<ROWS * COLS>{}(compute, k);
339 }
340
341 auto storec = [&](auto i) {
342 constexpr int row = i / COLS;
343 constexpr int col = i % COLS;
344 _mm512_storeu_ps(reinterpret_cast<__m512*>(C + row * ldc + col * 16), vc[i]);
345 };
346 Unroll<ROWS * COLS>{}(storec);
347 }
348 };
349 #endif
350
351 #define LAUNCH_TINYGEMM_KERNEL_NN2(MB_SIZE, NB_SIZE) \
352 tinygemm_kernel_nn<scalar_t, MB_SIZE, NB_SIZE>::apply( \
353 A + mb_start * lda, B + nb_start * 2, C + mb_start * ldc + nb_start, K, lda, ldb, ldc);
354
355 template <typename scalar_t>
356 void tinygemm_kernel(
357 const scalar_t* __restrict__ A,
358 const scalar_t* __restrict__ B,
359 float* __restrict__ C,
360 int64_t M,
361 int64_t N,
362 int64_t K,
363 int64_t lda,
364 int64_t ldb,
365 int64_t ldc) {
366 // pattern: 1-2-8
367 constexpr int64_t BLOCK_M = 4;
368 constexpr int64_t BLOCK_N = 32;
369 const int64_t MB = div_up(M, BLOCK_M);
370 const int64_t NB = div_up(N, BLOCK_N);
371 for (int mb = 0; mb < MB; ++mb) {
372 int64_t mb_start = mb * BLOCK_M;
373 int64_t mb_size = std::min(BLOCK_M, M - mb_start);
374 for (int64_t nb = 0; nb < NB; ++nb) {
375 int64_t nb_start = nb * BLOCK_N;
376 int64_t nb_size = std::min(BLOCK_N, N - nb_start);
377
378 switch (mb_size << 4 | nb_size >> 4) {
379 // mb_size = 1
380 case 0x12:
381 LAUNCH_TINYGEMM_KERNEL_NN2(1, 32);
382 break;
383 // mb_size = 2
384 case 0x22:
385 LAUNCH_TINYGEMM_KERNEL_NN2(2, 32);
386 break;
387 // mb_size = 3
388 case 0x32:
389 LAUNCH_TINYGEMM_KERNEL_NN2(3, 32);
390 break;
391 // mb_size = 4
392 case 0x42:
393 LAUNCH_TINYGEMM_KERNEL_NN2(4, 32);
394 break;
395 default:
396 TORCH_CHECK(false, "Unexpected block size, ", mb_size, "x", "nb_size");
397 }
398 }
399 }
400 }
401
402 template <typename scalar_t>
403 void fused_experts_kernel_impl(
404 scalar_t* __restrict__ output,
405 scalar_t* __restrict__ ic1,
406 scalar_t* __restrict__ ic2,
407 scalar_t* __restrict__ A_tmp,
408 float* __restrict__ C_tmp,
409 const scalar_t* __restrict__ input,
410 const scalar_t* __restrict__ packed_w1,
411 const scalar_t* __restrict__ packed_w2,
412 const float* __restrict__ w1_bias,
413 const float* __restrict__ w2_bias,
414 const float* __restrict__ topk_weights,
415 const int32_t* __restrict__ sorted_ids,
416 const int32_t* __restrict__ expert_ids,
417 const int32_t* __restrict__ offsets,
418 int64_t M,
419 int64_t N,
420 int64_t K,
421 int64_t E,
422 int64_t topk,
423 int64_t num_tokens_post_pad,
424 float alpha,
425 float limit,
426 CPUActMethod act_func,
427 bool with_bias) {
428 // handle 2 tiles per block
429 constexpr int64_t BLOCK_M = block_size_m();
430 constexpr int64_t BLOCK_N = block_size_n();
431
432 // stage 1: intermediate_cache1 = silu(hidden_states @ w1)
433 const int64_t MB = div_up(num_tokens_post_pad, BLOCK_M);
434 const int64_t NB = div_up(N, BLOCK_N);
435
436 // strides for w1: [E, 2N, K]
437 TORCH_CHECK(N % BLOCK_N == 0, "Fixme when N is not multiples of ", BLOCK_N);
438
439 const int64_t stride_e = 2 * N * K;
440 const int64_t stride_n = K;
441
442 int64_t avg_M = std::max(int64_t(1), M * topk / E);
443 const bool use_brgemm = can_use_brgemm<scalar_t>(avg_M);
444
445 // here we only parallel on half of 2N to fuse silu_and_mul with gemm
446 parallel_2d(MB, NB, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
447 // get local pointers
448 int tid = get_thread_num();
449 scalar_t* __restrict__ A = A_tmp + tid * BLOCK_M * K;
450 float* __restrict__ C0 = C_tmp + tid * 2 * BLOCK_M * BLOCK_N;
451 float* __restrict__ C1 = C0 + BLOCK_M * BLOCK_N;
452
453 loop_2d<scalar_t>(mb0, mb1, nb0, nb1, BLOCK_N * K * 2, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
454 // nb_upper from top half and nb_lower from bottom half
455 int64_t nb_upper = nb, nb_lower = nb + NB;
456 int64_t n_size = std::min(N - nb * BLOCK_N, BLOCK_N);
457
458 // B shape [K, n_size] in vnni format
459 int32_t expert_id = expert_ids[mb];
460 const scalar_t* __restrict__ B0 = packed_w1 + expert_id * stride_e + nb_upper * BLOCK_N * stride_n;
461 const scalar_t* __restrict__ B1 = packed_w1 + expert_id * stride_e + nb_lower * BLOCK_N * stride_n;
462 const float* __restrict__ B0_bias = w1_bias + expert_id * 2 * N + nb_upper * BLOCK_N;
463 const float* __restrict__ B1_bias = w1_bias + expert_id * 2 * N + nb_lower * BLOCK_N;
464
465 int64_t m_size = offsets[mb + 1] - offsets[mb];
466
467 if (nb_offset == 0) {
468 // 1.a load A
469 const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
470 for (int64_t m = 0; m < m_size; ++m) {
471 int32_t index = A_ids[m] / topk;
472 copy_stub(A + m * K, input + index * K, K);
473 }
474 }
475
476 if (use_brgemm) {
477 // 1.b gemm: C0 = A @ B0
478 at::native::cpublas::brgemm(
479 /* M */ m_size,
480 /* N */ n_size,
481 /* K */ K,
482 /* lda */ K,
483 /* ldb */ n_size,
484 /* ldc */ BLOCK_N,
485 /* add_C */ false,
486 /* A */ A,
487 /* B */ B0,
488 /* C */ C0);
489
490 // 1.c gemm: C1 = A @ B1
491 at::native::cpublas::brgemm(
492 /* M */ m_size,
493 /* N */ n_size,
494 /* K */ K,
495 /* lda */ K,
496 /* ldb */ n_size,
497 /* ldc */ BLOCK_N,
498 /* add_C */ false,
499 /* A */ A,
500 /* B */ B1,
501 /* C */ C1);
502
503 } else {
504 const int64_t offset = offsets[mb];
505 if (act_func == CPUActMethod::swiglu) {
506 tinygemm_kernel(
507 /* A */ A,
508 /* B */ B0,
509 /* C */ C0,
510 /* M */ m_size,
511 /* N */ n_size,
512 /* K */ K,
513 /* lda */ K,
514 /* ldb */ n_size,
515 /* ldc */ BLOCK_N);
516 tinygemm_kernel(
517 /* A */ A,
518 /* B */ B1,
519 /* C */ C1,
520 /* M */ m_size,
521 /* N */ n_size,
522 /* K */ K,
523 /* lda */ K,
524 /* ldb */ n_size,
525 /* ldc */ BLOCK_N);
526 } else {
527 // fused 1.bcd: silu_and_mul(A @ B0, A @ B1)
528 tinygemm_kernel(
529 /* A */ A,
530 /* B0 */ B0,
531 /* B1 */ B1,
532 /* C */ ic1 + offset * N + nb * BLOCK_N,
533 /* M */ m_size,
534 /* N */ n_size,
535 /* K */ K,
536 /* lda */ K,
537 /* ldb */ n_size,
538 /* ldc */ N);
539 }
540 }
541 if (with_bias) {
542 for (int64_t m = 0; m < m_size; ++m) {
543 add_bias_stub(C0 + m * BLOCK_N, B0_bias, n_size);
544 add_bias_stub(C1 + m * BLOCK_N, B1_bias, n_size);
545 }
546 }
547 // 1.d silu and mul
548 const int64_t offset = offsets[mb];
549 if (act_func == CPUActMethod::silu_and_mul && use_brgemm) {
550 for (int64_t m = 0; m < m_size; ++m) {
551 silu_and_mul_stub(ic1 + (offset + m) * N + nb * BLOCK_N, C0 + m * BLOCK_N, C1 + m * BLOCK_N, BLOCK_N);
552 }
553 } else if (act_func == CPUActMethod::swiglu) {
554 for (int64_t m = 0; m < m_size; ++m) {
555 scalar_t* __restrict__ ic1_row = ic1 + (offset + m) * N;
556 clamp_sigmoid_and_mul_stub(ic1_row + nb * BLOCK_N / 2, C0 + m * BLOCK_N, BLOCK_N / 2, alpha, limit);
557 clamp_sigmoid_and_mul_stub(ic1_row + N / 2 + nb * BLOCK_N / 2, C1 + m * BLOCK_N, BLOCK_N / 2, alpha, limit);
558 }
559 }
560 });
561
562 if (use_brgemm) {
563 at::native::cpublas::brgemm_release();
564 }
565 });
566
567 // stage 2: intermediate_cache2 = intermediate_cache1 @ w2
568 // w2 : [E, K, N] as [E, OC, IC]
569 const int64_t OC = K; // rename K as OC
570 const int64_t IC = N; // rename N as IC
571 const int64_t MB2 = MB;
572 const int64_t NB2 = div_up(OC, BLOCK_N);
573 const int64_t stride_e2 = OC * IC;
574 const int64_t stride_oc = IC;
575
576 // parallel on [MB2, NB2]
577 parallel_2d(MB2, NB2, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
578 // get local pointers
579 int tid = get_thread_num();
580 // we won't be using C1 for gemm2
581 float* __restrict__ C = C_tmp + tid * 2 * BLOCK_M * BLOCK_N;
582
583 loop_2d<scalar_t>(mb0, mb1, nb0, nb1, BLOCK_N * IC, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
584 int64_t m_size = offsets[mb + 1] - offsets[mb];
585 int64_t n_size = std::min(OC - nb * BLOCK_N, BLOCK_N);
586
587 // A ptr from ic1 of [M * topk, N] in sorted order
588 // so as to avoid copy A to tmp buffer again
589 const scalar_t* __restrict__ A = ic1 + offsets[mb] * N;
590 const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
591
592 // B shape [IC, n_size] in vnni format
593 int32_t expert_id = expert_ids[mb];
594 const scalar_t* __restrict__ B = packed_w2 + expert_id * stride_e2 + nb * BLOCK_N * stride_oc;
595 const float* __restrict__ B_bias = w2_bias + expert_id * OC + nb * BLOCK_N;
596
597 // 2.a gemm: C = A @ B
598 if (use_brgemm) {
599 at::native::cpublas::brgemm(
600 /* M */ m_size,
601 /* N */ n_size,
602 /* K */ IC,
603 /* lda */ IC,
604 /* ldb */ n_size,
605 /* ldc */ BLOCK_N,
606 /* add_C */ false,
607 /* A */ A,
608 /* B */ B,
609 /* C */ C);
610 } else {
611 tinygemm_kernel(
612 /* A */ A,
613 /* B */ B,
614 /* C */ C,
615 /* M */ m_size,
616 /* N */ n_size,
617 /* K */ IC,
618 /* lda */ IC,
619 /* ldb */ n_size,
620 /* ldc */ BLOCK_N);
621 }
622
623 if (with_bias) {
624 for (int64_t m = 0; m < m_size; ++m) {
625 add_bias_stub(C + m * BLOCK_N, B_bias, n_size);
626 }
627 }
628 // 2.b copy from C to ic2 in original order
629 // and also mul topk_weights in float32
630 for (int64_t m = 0; m < m_size; ++m) {
631 int32_t index = A_ids[m];
632 float weight = topk_weights[index];
633 copy_mul_stub(ic2 + index * K + nb * BLOCK_N, C + m * BLOCK_N, weight, n_size);
634 }
635 });
636
637 if (use_brgemm) {
638 at::native::cpublas::brgemm_release();
639 }
640 });
641
642 // stage 3: out = intermediate_cache2.sum(dim=1)
643 // from [M, topk, K] to [M, K]
644 at::parallel_for(0, M, 0, [&](int64_t begin, int64_t end) {
645 for (int64_t m = begin; m < end; ++m) {
646 sum_stub(output + m * K, ic2 + m * topk * K, topk, K);
647 }
648 });
649 }
650
651 template <typename scalar_t>
652 void shared_expert_kernel_impl(
653 scalar_t* __restrict__ output,
654 scalar_t* __restrict__ ic1,
655 float* __restrict__ C_tmp,
656 scalar_t* __restrict__ input,
657 const scalar_t* __restrict__ packed_w1,
658 const scalar_t* __restrict__ packed_w2,
659 const scalar_t* __restrict__ fused_experts_out,
660 float routed_scaling_factor,
661 int64_t M,
662 int64_t N,
663 int64_t K) {
664 // handle 2 tiles per block
665 constexpr int64_t BLOCK_M = block_size_m();
666 constexpr int64_t BLOCK_N = block_size_n();
667
668 // stage 1: intermediate_cache1 = silu(hidden_states @ w1)
669 const int64_t MB = div_up(M, BLOCK_M);
670 const int64_t NB = div_up(N, BLOCK_N);
671
672 TORCH_CHECK(N % BLOCK_N == 0, "Fixme when N is not multiples of ", BLOCK_N);
673 const int64_t stride_n = K;
674
675 const bool use_brgemm = can_use_brgemm<scalar_t>(M);
676
677 const bool apply_scaling_factor = fused_experts_out != nullptr;
678
679 // here we only parallel on half of 2N to fuse silu_and_mul with gemm
680 parallel_2d(MB, NB, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
681 // get local pointers
682 int tid = get_thread_num();
683 float* __restrict__ C0 = C_tmp + tid * 2 * BLOCK_M * BLOCK_N;
684 float* __restrict__ C1 = C0 + BLOCK_M * BLOCK_N;
685
686 loop_2d<scalar_t>(mb0, mb1, nb0, nb1, BLOCK_N * K * 2, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
687 // nb_upper from top half and nb_lower from bottom half
688 int64_t nb_upper = nb, nb_lower = nb + NB;
689 int64_t n_size = std::min(N - nb * BLOCK_N, BLOCK_N);
690 int64_t m_size = std::min(M - mb * BLOCK_M, BLOCK_M);
691
692 // A shape [m_size, K]
693 const scalar_t* A = input + mb * BLOCK_M * K;
694
695 // B shape [K, n_size] in vnni format
696 const scalar_t* __restrict__ B0 = packed_w1 + nb_upper * BLOCK_N * stride_n;
697 const scalar_t* __restrict__ B1 = packed_w1 + nb_lower * BLOCK_N * stride_n;
698
699 if (use_brgemm) {
700 // 1.b gemm: C0 = A @ B0
701 at::native::cpublas::brgemm(
702 /* M */ m_size,
703 /* N */ n_size,
704 /* K */ K,
705 /* lda */ K,
706 /* ldb */ n_size,
707 /* ldc */ BLOCK_N,
708 /* add_C */ false,
709 /* A */ A,
710 /* B */ B0,
711 /* C */ C0);
712
713 // 1.c gemm: C1 = A @ B1
714 at::native::cpublas::brgemm(
715 /* M */ m_size,
716 /* N */ n_size,
717 /* K */ K,
718 /* lda */ K,
719 /* ldb */ n_size,
720 /* ldc */ BLOCK_N,
721 /* add_C */ false,
722 /* A */ A,
723 /* B */ B1,
724 /* C */ C1);
725
726 // 1.d silu and mul
727 for (int64_t m = 0; m < m_size; ++m) {
728 silu_and_mul_stub(ic1 + (mb * BLOCK_M + m) * N + nb * BLOCK_N, C0 + m * BLOCK_N, C1 + m * BLOCK_N, BLOCK_N);
729 }
730 } else {
731 // fused 1.bcd: silu_and_mul(A @ B0, A @ B1)
732 tinygemm_kernel(
733 /* A */ A,
734 /* B0 */ B0,
735 /* B1 */ B1,
736 /* C */ ic1 + mb * BLOCK_M * N + nb * BLOCK_N,
737 /* M */ m_size,
738 /* N */ n_size,
739 /* K */ K,
740 /* lda */ K,
741 /* ldb */ n_size,
742 /* ldc */ N);
743 }
744 });
745
746 if (use_brgemm) {
747 at::native::cpublas::brgemm_release();
748 }
749 });
750
751 // stage 2: output = intermediate_cache1 @ w2
752 // w2 : [K, N] as [OC, IC]
753 const int64_t OC = K; // rename K as OC
754 const int64_t IC = N; // rename N as IC
755 const int64_t MB2 = MB;
756 const int64_t NB2 = div_up(OC, BLOCK_N);
757 const int64_t stride_oc = IC;
758
759 // parallel on [MB2, NB2]
760 parallel_2d(MB2, NB2, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
761 // get local pointers
762 int tid = get_thread_num();
763 // we won't be using C1 for gemm2
764 float* __restrict__ C = C_tmp + tid * 2 * BLOCK_M * BLOCK_N;
765
766 loop_2d<scalar_t>(mb0, mb1, nb0, nb1, BLOCK_N * IC, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
767 int64_t m_size = std::min(M - mb * BLOCK_M, BLOCK_M);
768 int64_t n_size = std::min(OC - nb * BLOCK_N, BLOCK_N);
769
770 // A shape [m_size, IC]
771 const scalar_t* __restrict__ A = ic1 + mb * BLOCK_M * N;
772
773 // B shape [IC, n_size] in vnni format
774 const scalar_t* __restrict__ B = packed_w2 + nb * BLOCK_N * stride_oc;
775
776 // 2.a gemm: C = A @ B
777 if (use_brgemm) {
778 at::native::cpublas::brgemm(
779 /* M */ m_size,
780 /* N */ n_size,
781 /* K */ IC,
782 /* lda */ IC,
783 /* ldb */ n_size,
784 /* ldc */ BLOCK_N,
785 /* add_C */ false,
786 /* A */ A,
787 /* B */ B,
788 /* C */ C);
789 } else {
790 tinygemm_kernel(
791 /* A */ A,
792 /* B */ B,
793 /* C */ C,
794 /* M */ m_size,
795 /* N */ n_size,
796 /* K */ IC,
797 /* lda */ IC,
798 /* ldb */ n_size,
799 /* ldc */ BLOCK_N);
800 }
801
802 // 2.b copy from C to output and add fused_experts_out
803 scalar_t* __restrict__ out = output + mb * BLOCK_M * K + nb * BLOCK_N;
804 const scalar_t* __restrict__ fused_out =
805 apply_scaling_factor ? fused_experts_out + mb * BLOCK_M * K + nb * BLOCK_N : nullptr;
806 for (int64_t m = 0; m < m_size; ++m) {
807 const scalar_t* __restrict__ fused_out_row = apply_scaling_factor ? (fused_out + m * K) : nullptr;
808 add_mul_stub(out + m * K, C + m * BLOCK_N, fused_out_row, routed_scaling_factor, n_size);
809 }
810 });
811
812 if (use_brgemm) {
813 at::native::cpublas::brgemm_release();
814 }
815 });
816 }
817
818 } // anonymous namespace
819
820 // common checks
821 template <CPUQuantMethod quant>
822 static inline void check_moe_scales(
823 const std::optional<at::Tensor>& w1_scale,
824 const std::optional<at::Tensor>& w2_scale,
825 const std::optional<std::vector<int64_t>> block_size) {
826 if constexpr (quant == CPUQuantMethod::INT8_W8A8) {
827 TORCH_CHECK(w1_scale.has_value(), "missing w1_scale for int8 w8a8.");
828 TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for int8 w8a8.");
829 } else if constexpr (quant == CPUQuantMethod::FP8_W8A16) {
830 TORCH_CHECK(w1_scale.has_value(), "missing w1_scale for fp8 w8a16.");
831 TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for fp8 w8a16.");
832 TORCH_CHECK(block_size.has_value(), "missing block_size for fp8 w8a16.");
833 TORCH_CHECK(block_size.value().size() == 2, "expect block_size.size() to be 2.");
834 } else if constexpr (quant == CPUQuantMethod::MXFP4) {
835 TORCH_CHECK(w1_scale.has_value(), "missing w1_scale for mxfp4.");
836 TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for mxfp4.");
837 TORCH_CHECK(w1_scale.value().scalar_type() == at::kByte, "expect w1_scale to be uint8.");
838 TORCH_CHECK(w2_scale.value().scalar_type() == at::kByte, "expect w2_scale to be uint8.");
839 } else if constexpr (quant == CPUQuantMethod::FP8_W8A8) {
840 TORCH_CHECK(w1_scale.has_value(), "missing w1_scale for fp8 w8a8.");
841 TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for fp8 w8a8.");
842 TORCH_CHECK(block_size.has_value(), "missing block_size for fp8 w8a8.");
843 TORCH_CHECK(block_size.value().size() == 2, "expect block_size.size() to be 2.");
844 }
845 }
846
847 static inline void check_moe_scales(
848 int64_t moe_comp_method,
849 const std::optional<at::Tensor>& w1_scale,
850 const std::optional<at::Tensor>& w2_scale,
851 const std::optional<std::vector<int64_t>> block_size) {
852 if (moe_comp_method == CPUQuantMethod::INT8_W8A8) {
853 check_moe_scales<CPUQuantMethod::INT8_W8A8>(w1_scale, w2_scale, block_size);
854 } else if (moe_comp_method == CPUQuantMethod::FP8_W8A16) {
855 check_moe_scales<CPUQuantMethod::FP8_W8A16>(w1_scale, w2_scale, block_size);
856 } else if (moe_comp_method == CPUQuantMethod::MXFP4) {
857 check_moe_scales<CPUQuantMethod::MXFP4>(w1_scale, w2_scale, block_size);
858 } else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
859 check_moe_scales<CPUQuantMethod::FP8_W8A8>(w1_scale, w2_scale, block_size);
860 }
861 }
862
863 // CHECK_MOE_SCALES_FP8: validate scale shapes for FP8_W8A16.
864 // w1s shape [E, 2N/block_N, K/block_K, block_N]: DIM0=1, DIM1=2
865 // w2s shape [E, K/block_N, N/block_K]: DIM0=1, DIM1=2
866 #define CHECK_MOE_SCALES_FP8(DIM0, DIM1) \
867 auto w1s = w1_scale.value(); \
868 auto w2s = w2_scale.value(); \
869 auto block_size_val = block_size.value(); \
870 int64_t block_size_N = block_size_val[0]; \
871 int64_t block_size_K = block_size_val[1]; \
872 TORCH_CHECK(w1s.size(DIM0) == div_up(2 * N, block_size_N)); \
873 TORCH_CHECK(w1s.size(DIM1) == div_up(K, block_size_K)); \
874 TORCH_CHECK(w2s.size(DIM0) == div_up(K, block_size_N)); \
875 TORCH_CHECK(w2s.size(DIM1) == div_up(N, block_size_K))
876
877 // CHECK_MOE_SCALES_FP8_W8A8: extract scale info for FP8_W8A8 without shape
878 // validation, because w1s is pre-packed by float8_linear_prepack_cpu with
879 // shape [E, 2N/BLOCK_N, G, BLOCK_N] where BLOCK_N=32 != block_size_N(128).
880 // This matches the sglang upstream behavior (checks are commented out there).
881 #define CHECK_MOE_SCALES_FP8_W8A8(DIM0, DIM1) \
882 auto w1s = w1_scale.value(); \
883 auto w2s = w2_scale.value(); \
884 auto block_size_val = block_size.value(); \
885 int64_t block_size_N = block_size_val[0]; \
886 int64_t block_size_K = block_size_val[1]
887
888 // hidden_states: [M, K]
889 // w1: [E, 2N, K] or [E, 2N, K / 2] for uint8
890 // w2: [E, K, N] or [E, K, N / 2] for uint8
891 // topk_weights: [M, topk]
892 // topk_ids: [M, topk] (int32_t)
893 //
894
895 void fused_experts_cpu(
896 at::Tensor& out,
897 at::Tensor& hidden_states,
898 at::Tensor& w1,
899 at::Tensor& w2,
900 at::Tensor& topk_weights,
901 at::Tensor& topk_ids,
902 int64_t moe_comp_method,
903 const std::optional<at::Tensor>& w1_scale,
904 const std::optional<at::Tensor>& w2_scale,
905 const std::optional<at::Tensor>& w1_zero,
906 const std::optional<at::Tensor>& w2_zero,
907 const std::optional<at::Tensor>& a1_scale,
908 const std::optional<std::vector<int64_t>> block_size,
909 const std::optional<at::Tensor>& w1_bias,
910 const std::optional<at::Tensor>& w2_bias,
911 const std::optional<double>& alpha,
912 const std::optional<double>& limit,
913 bool is_vnni) {
914 auto packed_w1 = is_vnni ? w1 : convert_weight_packed(w1);
915 auto packed_w2 = is_vnni ? w2 : convert_weight_packed(w2);
916
917 constexpr int64_t BLOCK_M = block_size_m();
918 constexpr int64_t BLOCK_N = block_size_n();
919
920 // FP8_W8A8 hidden_states input is FP8; scalar_t should be BF16 for output
921 const auto st = (moe_comp_method == CPUQuantMethod::FP8_W8A8)
922 ? at::kBFloat16
923 : hidden_states.scalar_type();
924 // TODO: fused_topk_torch_native (CPU fallback for models like MiniMax)
925 // returns int64 topk_ids; fused_experts_cpu requires int32. Remove the typecast after topk kernel is provided
926 auto topk_ids_ = topk_ids.scalar_type() == at::kInt ? topk_ids : topk_ids.to(at::kInt);
927
928 CHECK_INPUT(hidden_states);
929 CHECK_INPUT(w1);
930 CHECK_INPUT(w2);
931 CHECK_EQ(topk_weights.sizes(), topk_ids_.sizes());
932 CHECK_DIM(2, hidden_states);
933 if (moe_comp_method == CPUQuantMethod::INT4_W4A8 && is_vnni) {
934 CHECK_DIM(4, w1);
935 CHECK_DIM(4, w2);
936 } else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
937 // w1 is prepackaged by float8_linear_prepack_cpu (5D), skip w1 dim check
938 CHECK_DIM(3, w2);
939 } else {
940 CHECK_DIM(3, w1);
941 CHECK_DIM(3, w2);
942 }
943 CHECK_DIM(2, topk_weights);
944 CHECK_DIM(2, topk_ids_);
945 CHECK_EQ(topk_ids_.scalar_type(), at::kInt);
946
947 // TODO: support topk_weights to be bf16 or fp16 in the kernel.
948 // The topk_weights of llama4 is computed via Llama4MoE:custom_routing_function and is bf16/fp16
949 // while the kernel currently only supports it to be float32
950 auto topk_weights_ = topk_weights.to(at::kFloat);
951 CHECK_EQ(topk_weights_.scalar_type(), at::kFloat);
952
953 int64_t M = hidden_states.size(0);
954 int64_t K = hidden_states.size(1);
955 int64_t N = moe_comp_method == CPUQuantMethod::INT4_W4A8
956 ? w1_scale.value().size(1) * w1_scale.value().size(3) / 2
957 : (moe_comp_method == CPUQuantMethod::FP8_W8A8 ? w2.size(2) : w1.size(1) / 2);
958 int64_t E = w1.size(0);
959 int64_t topk = topk_weights_.size(1);
960
961 // we use int32_t compensation for int8 w8a8
962 int64_t packed_K = get_row_size(static_cast<CPUQuantMethod>(moe_comp_method), K);
963 int64_t packed_N = get_row_size(static_cast<CPUQuantMethod>(moe_comp_method), N);
964
965 // check weight shapes
966 CHECK_EQ(w2.size(0), E);
967 if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
968 CHECK_EQ(w2.size(1), K);
969 CHECK_EQ(packed_w2.size(2), packed_N);
970 } else if (!(moe_comp_method == CPUQuantMethod::INT4_W4A8)) {
971 CHECK_EQ(w2.size(1), K);
972 CHECK_EQ(packed_w1.size(2), packed_K / (moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 2 : 1));
973 CHECK_EQ(packed_w2.size(2), packed_N / (moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 2 : 1));
974 }
975 // check scales
976 check_moe_scales(moe_comp_method, w1_scale, w2_scale, block_size);
977
978 at::Tensor& out_hidden_states = out;
979
980 // NB: worst case is each expert holds a block with remainder of 1
981 // 1. sorted_ids : [M * topk + E * (BLOCK_M - 1)]
982 // 2. expert_ids : [max_num_blocks]
983 // 3. total_cnts : [T + 1, E]
984 // 4. cumsums : [E + 1]
985 // 5. offsets : [max_num_blocks + 1]
986 //
987 int num_threads = at::get_num_threads();
988 int64_t max_num_tokens_padded = M * topk + E * (BLOCK_M - 1);
989 int64_t max_num_blocks = div_up(max_num_tokens_padded, BLOCK_M);
990 auto buffer = at::empty(
991 {max_num_tokens_padded + max_num_blocks + (num_threads + 1) * E + (E + 1) + (max_num_blocks + 1)},
992 topk_ids_.options());
993
994 int32_t* __restrict__ sorted_ids = buffer.data_ptr<int32_t>();
995 int32_t* __restrict__ expert_ids = sorted_ids + max_num_tokens_padded;
996 int32_t* __restrict__ total_cnts = expert_ids + max_num_blocks;
997 int32_t* __restrict__ cumsums = total_cnts + (num_threads + 1) * E;
998 int32_t* __restrict__ offsets = cumsums + (E + 1);
999
1000 // init sorted_ids with `numel` as the padding number
1001 // init expert_ids with `num_experts`
1002 int64_t numel = M * topk;
1003 at::parallel_for(0, max_num_blocks, GRAIN_SIZE / BLOCK_M, [&](int64_t begin, int64_t end) {
1004 int64_t m_start = begin * BLOCK_M;
1005 int64_t m_size = std::min((end - begin) * BLOCK_M, max_num_tokens_padded - m_start);
1006 fill_stub(sorted_ids + m_start, (int32_t)numel, m_size);
1007 fill_stub(expert_ids + begin, (int32_t)E, end - begin);
1008 });
1009 // zero total_cnts and cumsums
1010 at::parallel_for(0, (num_threads + 1) * E + (E + 1), GRAIN_SIZE, [&](int64_t begin, int64_t end) {
1011 fill_stub(total_cnts + begin, 0, end - begin);
1012 });
1013
1014 // align experts index
1015 int64_t num_tokens_post_pad = moe_align_block_size<BLOCK_M>(
1016 sorted_ids, expert_ids, topk_ids_.data_ptr<int32_t>(), total_cnts, cumsums, offsets, E, numel, num_threads);
1017
1018 // unlike triton kernel, we fuse silu with gemm1 so only need 2 intermediate_caches:
1019 // 1. intermediate_cache1 : [M * topk, N]
1020 // 2. intermediate_cache2 : [M * topk, K]
1021 // 3. A_tmp : [T, BLOCK_M * K]
1022 // 4. C_tmp : [T, 2 * BLOCK_M * BLOCK_N]
1023 //
1024 // for int8 w8a8:
1025 // 5. Aq_tmp : [M, K] or [M * topk, N]
1026 // 6. As_tmp : [M * topk]
1027 //
1028 // for fp8 w8a16, mxfp4, and fp8 w8a8:
1029 // 7. intermediate_cache0 : [M * topk, 2N]
1030 // 8. B_tmp : [T, MAX_CACHE_BLOCK_SIZE, BLOCK_N, std::max(K, N)]
1031 //
1032 // for fp8 w8a8 additionally:
1033 // 9. Ukernel_tmp : [T, 2 * BLOCK_M * BLOCK_N] (float32 for FP8×FP8 brgemm)
1034 //
1035 int64_t buffer_size_nbytes =
1036 M * topk * N * 2 + M * topk * K * 2 +
1037 num_threads * BLOCK_M * K *
1038 (moe_comp_method == CPUQuantMethod::INT8_W8A8 | moe_comp_method == CPUQuantMethod::INT4_W4A8
1039 | moe_comp_method == CPUQuantMethod::FP8_W8A8 ? 1 : 2) +
1040 num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float);
1041
1042 if (moe_comp_method == CPUQuantMethod::INT8_W8A8) {
1043 buffer_size_nbytes += std::max(M * K, M * topk * N) + M * topk * sizeof(float);
1044 }
1045 if (moe_comp_method == CPUQuantMethod::FP8_W8A16 || moe_comp_method == CPUQuantMethod::MXFP4
1046 || moe_comp_method == CPUQuantMethod::FP8_W8A8) {
1047 buffer_size_nbytes += M * topk * 2 * N * 2 + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_N * std::max(K, N) * 2;
1048 }
1049 if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
1050 // Ukernel buffer: float32 workspace per thread for FP8×FP8 brgemm
1051 buffer_size_nbytes += num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float);
1052 #ifndef CPUBLAS_BRGEMM_F8F8F32
1053 // BF16 fallback: also need dqA [T, BLOCK_M, BLOCK_K] + dqB [T, BLOCK_K, BLOCK_N]
1054 buffer_size_nbytes += num_threads * (BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N) * 2;
1055 #endif
1056 }
1057 if (moe_comp_method == CPUQuantMethod::INT4_W4A8) {
1058 buffer_size_nbytes += M * topk * 2 * N * 2 + std::max(M * K, M * topk * N) + M * topk * sizeof(float) +
1059 num_threads * 2 * get_4bit_block_k_size(K / w1_scale.value().size(2)) * BLOCK_N;
1060 }
1061 auto buffer2 = at::empty({buffer_size_nbytes}, hidden_states.options().dtype(at::kChar));
1062
1063 AT_DISPATCH_REDUCED_FLOATING_TYPES(st, "fused_experts_kernel_impl", [&] {
1064 scalar_t* __restrict__ intermediate_cache1 = (scalar_t*)((void*)(buffer2.data_ptr<int8_t>()));
1065 scalar_t* __restrict__ intermediate_cache2 = intermediate_cache1 + M * topk * N;
1066
1067 if (moe_comp_method == CPUQuantMethod::INT8_W8A8) {
1068 uint8_t* __restrict__ A_tmp = (uint8_t*)((void*)(intermediate_cache2 + M * topk * K));
1069 float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
1070 uint8_t* __restrict__ Aq_tmp = (uint8_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1071 float* __restrict__ As_tmp = (float*)((void*)(Aq_tmp + std::max(M * K, M * topk * N)));
1072
1073 auto w1s = w1_scale.value();
1074 auto w2s = w2_scale.value();
1075 TORCH_CHECK(w1s.numel() == E * 2 * N);
1076 TORCH_CHECK(w2s.numel() == E * K);
1077
1078 fused_experts_int8_kernel_impl<scalar_t>(
1079 out_hidden_states.data_ptr<scalar_t>(),
1080 intermediate_cache1,
1081 intermediate_cache2,
1082 A_tmp,
1083 C_tmp,
1084 Aq_tmp,
1085 As_tmp,
1086 hidden_states.data_ptr<scalar_t>(),
1087 packed_w1.data_ptr<int8_t>(),
1088 packed_w2.data_ptr<int8_t>(),
1089 w1s.data_ptr<float>(),
1090 w2s.data_ptr<float>(),
1091 topk_weights_.data_ptr<float>(),
1092 sorted_ids,
1093 expert_ids,
1094 offsets,
1095 M,
1096 N,
1097 K,
1098 E,
1099 topk,
1100 num_tokens_post_pad);
1101 } else if (moe_comp_method == CPUQuantMethod::FP8_W8A16) {
1102 // here we just ignore C_tmp as it is not used
1103 scalar_t* __restrict__ A_tmp = (scalar_t*)((void*)(intermediate_cache2 + M * topk * K));
1104 float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
1105 scalar_t* __restrict__ intermediate_cache0 = (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1106 scalar_t* __restrict__ B_tmp = (scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
1107 bool with_bias = w1_bias.has_value();
1108 auto act_func = alpha.has_value() && limit.has_value() ? CPUActMethod::swiglu : CPUActMethod::silu_and_mul;
1109
1110 CHECK_MOE_SCALES_FP8(1, 2);
1111 fused_experts_fp_kernel_impl<scalar_t, at::Float8_e4m3fn, float, false>(
1112 out_hidden_states.data_ptr<scalar_t>(),
1113 intermediate_cache0,
1114 intermediate_cache1,
1115 intermediate_cache2,
1116 A_tmp,
1117 B_tmp,
1118 C_tmp,
1119 hidden_states.data_ptr<scalar_t>(),
1120 packed_w1.data_ptr<at::Float8_e4m3fn>(),
1121 packed_w2.data_ptr<at::Float8_e4m3fn>(),
1122 with_bias ? w1_bias.value().data_ptr<float>() : nullptr,
1123 with_bias ? w2_bias.value().data_ptr<float>() : nullptr,
1124 w1s.data_ptr<float>(),
1125 w2s.data_ptr<float>(),
1126 block_size_N,
1127 block_size_K,
1128 topk_weights_.data_ptr<float>(),
1129 sorted_ids,
1130 expert_ids,
1131 offsets,
1132 M,
1133 N,
1134 K,
1135 E,
1136 topk,
1137 num_tokens_post_pad,
1138 alpha.has_value() ? float(alpha.value()) : 0,
1139 limit.has_value() ? float(limit.value()) : 0,
1140 act_func,
1141 with_bias);
1142 } else if (moe_comp_method == CPUQuantMethod::MXFP4) {
1143 scalar_t* __restrict__ A_tmp = (scalar_t*)((void*)(intermediate_cache2 + M * topk * K));
1144 float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
1145 scalar_t* __restrict__ intermediate_cache0 = (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1146 scalar_t* __restrict__ B_tmp = (scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
1147 bool with_bias = w1_bias.has_value();
1148 auto act_func = alpha.has_value() && limit.has_value() ? CPUActMethod::swiglu : CPUActMethod::silu_and_mul;
1149
1150 // mxfp4 supports only group size of 32 (2^5)
1151 constexpr int64_t group_size = 32;
1152 auto w1s = w1_scale.value();
1153 auto w2s = w2_scale.value();
1154 TORCH_CHECK(w1s.numel() == E * 2 * N * K / group_size, "w1_scale size mismatch");
1155 TORCH_CHECK(w2s.numel() == E * K * N / group_size, "w2_scale size mismatch");
1156 fused_experts_fp_kernel_impl<scalar_t, uint8_t, uint8_t, true>(
1157 out_hidden_states.data_ptr<scalar_t>(),
1158 intermediate_cache0,
1159 intermediate_cache1,
1160 intermediate_cache2,
1161 A_tmp,
1162 B_tmp,
1163 C_tmp,
1164 hidden_states.data_ptr<scalar_t>(),
1165 packed_w1.data_ptr<uint8_t>(),
1166 packed_w2.data_ptr<uint8_t>(),
1167 with_bias ? w1_bias.value().data_ptr<float>() : nullptr,
1168 with_bias ? w2_bias.value().data_ptr<float>() : nullptr,
1169 w1s.data_ptr<uint8_t>(),
1170 w2s.data_ptr<uint8_t>(),
1171 /*block_size_N*/ 1,
1172 /*block_size_K*/ group_size,
1173 topk_weights_.data_ptr<float>(),
1174 sorted_ids,
1175 expert_ids,
1176 offsets,
1177 M,
1178 N,
1179 K,
1180 E,
1181 topk,
1182 num_tokens_post_pad,
1183 alpha.has_value() ? float(alpha.value()) : 0,
1184 limit.has_value() ? float(limit.value()) : 0,
1185 act_func,
1186 with_bias);
1187 } else if (moe_comp_method == CPUQuantMethod::INT4_W4A8) {
1188 uint8_t* __restrict__ A_tmp = (uint8_t*)((void*)(intermediate_cache2 + M * topk * K));
1189 float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
1190 scalar_t* __restrict__ intermediate_cache0 = (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1191 uint8_t* __restrict__ Aq_tmp = (uint8_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
1192 float* __restrict__ As_tmp = (float*)((void*)(Aq_tmp + std::max(M * K, M * topk * N)));
1193 int8_t* __restrict__ dqB_tmp = (int8_t*)((void*)(As_tmp + M * topk));
1194
1195 // weight + compensation shape = [Nc, Kc, block_n * block_k / 2 + block_n*sizeof(int32_t)]
1196 // scales/qzeros shape = [E, Nc, G, block_n]
1197 int64_t num_groups = w1_scale.value().size(2);
1198 const int group_size = K / num_groups;
1199 // TODO: check scales and zeros
1200 fused_experts_int4_w4a8_kernel_impl<scalar_t>(
1201 out_hidden_states.data_ptr<scalar_t>(),
1202 intermediate_cache0,
1203 intermediate_cache1,
1204 intermediate_cache2,
1205 A_tmp,
1206 Aq_tmp,
1207 As_tmp,
1208 nullptr,
1209 C_tmp,
1210 dqB_tmp,
1211 hidden_states.data_ptr<scalar_t>(),
1212 packed_w1.data_ptr<uint8_t>(),
1213 packed_w2.data_ptr<uint8_t>(),
1214 w1_zero.value().data_ptr<int8_t>(),
1215 w2_zero.value().data_ptr<int8_t>(),
1216 w1_scale.value().data_ptr<float>(),
1217 w2_scale.value().data_ptr<float>(),
1218 group_size,
1219 topk_weights.data_ptr<float>(),
1220 sorted_ids,
1221 expert_ids,
1222 offsets,
1223 M,
1224 N,
1225 K,
1226 E,
1227 topk,
1228 num_tokens_post_pad);
1229 } else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
1230 // Stage 1: FP8×FP8 GEMM (hidden_states is FP8 input)
1231 // Stage 2: BF16×FP8 GEMM (W8A16 path for down-projection)
1232 at::Float8_e4m3fn* __restrict__ A_tmp =
1233 (at::Float8_e4m3fn*)((void*)(intermediate_cache2 + M * topk * K));
1234 float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
1235 scalar_t* __restrict__ intermediate_cache0 =
1236 (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1237 scalar_t* __restrict__ B_tmp =
1238 (scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
1239 float* __restrict__ Ukernel_tmp =
1240 (float*)((void*)(B_tmp + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_N * std::max(K, N)));
1241
1242 CHECK_MOE_SCALES_FP8_W8A8(1, 2);
1243 fused_experts_fp8_a8_kernel_impl(
1244 out_hidden_states.data_ptr<scalar_t>(),
1245 intermediate_cache0,
1246 intermediate_cache1,
1247 intermediate_cache2,
1248 A_tmp,
1249 B_tmp,
1250 C_tmp,
1251 Ukernel_tmp,
1252 hidden_states.data_ptr<at::Float8_e4m3fn>(),
1253 packed_w1.data_ptr<at::Float8_e4m3fn>(),
1254 packed_w2.data_ptr<at::Float8_e4m3fn>(),
1255 a1_scale.has_value() ? a1_scale.value().data_ptr<float>() : nullptr,
1256 w1s.data_ptr<float>(),
1257 w2s.data_ptr<float>(),
1258 block_size_N,
1259 block_size_K,
1260 topk_weights_.data_ptr<float>(),
1261 sorted_ids,
1262 expert_ids,
1263 offsets,
1264 M,
1265 N,
1266 K,
1267 E,
1268 topk,
1269 num_tokens_post_pad);
1270 } else {
1271 scalar_t* __restrict__ A_tmp = intermediate_cache2 + M * topk * K;
1272 float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
1273 bool with_bias = w1_bias.has_value();
1274 auto act_func = alpha.has_value() && limit.has_value() ? CPUActMethod::swiglu : CPUActMethod::silu_and_mul;
1275
1276 fused_experts_kernel_impl<scalar_t>(
1277 out_hidden_states.data_ptr<scalar_t>(),
1278 intermediate_cache1,
1279 intermediate_cache2,
1280 A_tmp,
1281 C_tmp,
1282 hidden_states.data_ptr<scalar_t>(),
1283 packed_w1.data_ptr<scalar_t>(),
1284 packed_w2.data_ptr<scalar_t>(),
1285 with_bias ? w1_bias.value().data_ptr<float>() : nullptr,
1286 with_bias ? w2_bias.value().data_ptr<float>() : nullptr,
1287 topk_weights_.data_ptr<float>(),
1288 sorted_ids,
1289 expert_ids,
1290 offsets,
1291 M,
1292 N,
1293 K,
1294 E,
1295 topk,
1296 num_tokens_post_pad,
1297 alpha.has_value() ? float(alpha.value()) : 0,
1298 limit.has_value() ? float(limit.value()) : 0,
1299 act_func,
1300 with_bias);
1301 }
1302 });
1303 }
1304
1305 // shared expert kernel
1306 //
1307 // hidden_states: [M, K]
1308 // w1: [2N, K]
1309 // w2: [K, N]
1310 // fused_experts_out
1311 at::Tensor shared_expert_cpu(
1312 at::Tensor& hidden_states,
1313 at::Tensor& w1,
1314 at::Tensor& w2,
1315 const std::optional<at::Tensor>& fused_experts_out,
1316 const std::optional<double> routed_scaling_factor,
1317 bool inplace,
1318 bool use_int8_w8a8,
1319 bool use_fp8_w8a16,
1320 const std::optional<at::Tensor>& w1_scale,
1321 const std::optional<at::Tensor>& w2_scale,
1322 const std::optional<std::vector<int64_t>> block_size,
1323 bool is_vnni) {
1324 auto packed_w1 = is_vnni ? w1 : convert_weight_packed(w1);
1325 auto packed_w2 = is_vnni ? w2 : convert_weight_packed(w2);
1326
1327 constexpr int64_t BLOCK_M = block_size_m();
1328 constexpr int64_t BLOCK_N = block_size_n();
1329
1330 double routed_scaling_factor_value = 0;
1331 if (routed_scaling_factor.has_value()) {
1332 TORCH_CHECK(fused_experts_out.has_value(), "shared_expert_cpu: expect fused_experts_out.");
1333 const auto fused_experts_out_tensor = fused_experts_out.value();
1334 routed_scaling_factor_value = routed_scaling_factor.value();
1335 CHECK_INPUT(fused_experts_out_tensor);
1336 CHECK_EQ(hidden_states.sizes(), fused_experts_out_tensor.sizes());
1337 }
1338
1339 const auto st = hidden_states.scalar_type();
1340 CHECK_INPUT(hidden_states);
1341 CHECK_INPUT(w1);
1342 CHECK_INPUT(w2);
1343 CHECK_DIM(2, hidden_states);
1344 CHECK_DIM(2, w1);
1345 CHECK_DIM(2, w2);
1346 CHECK_EQ(hidden_states.scalar_type(), st);
1347
1348 int64_t M = hidden_states.size(0);
1349 int64_t K = hidden_states.size(1);
1350 int64_t N = w1.size(0) / 2;
1351
1352 // we use int32_t compensation for int8 w8a8
1353 int64_t packed_K = get_row_size(K, use_int8_w8a8);
1354 int64_t packed_N = get_row_size(N, use_int8_w8a8);
1355
1356 // check weight shapes
1357 CHECK_EQ(w2.size(0), K);
1358 CHECK_EQ(packed_w1.size(1), packed_K);
1359 CHECK_EQ(packed_w2.size(1), packed_N);
1360
1361 // check scales
1362 if (use_int8_w8a8) {
1363 check_moe_scales<CPUQuantMethod::INT8_W8A8>(w1_scale, w2_scale, block_size);
1364 } else if (use_fp8_w8a16) {
1365 check_moe_scales<CPUQuantMethod::FP8_W8A16>(w1_scale, w2_scale, block_size);
1366 }
1367
1368 at::Tensor out_hidden_states = inplace ? hidden_states : at::empty_like(hidden_states);
1369
1370 // unlike triton kernel, we fuse silu with gemm1 so only need 2 intermediate_caches:
1371 // 1. intermediate_cache1 : [M, N]
1372 // 2. C_tmp : [T, 2 * BLOCK_M * BLOCK_N]
1373 //
1374 // for int8 w8a8:
1375 // 3. Aq_tmp : [M, K] or [M, N]
1376 // 4. As_tmp : [M]
1377 //
1378 // for fp8 w8a16:
1379 // 5. intermediate_cache0 : [M, 2N]
1380 // 6. B_tmp: [T, MAX_CACHE_BLOCK_SIZE, BLOCK_M, max(K, N)]
1381 //
1382 int num_threads = at::get_num_threads();
1383 int64_t buffer_size_nbytes = M * N * 2 + num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float);
1384
1385 if (use_int8_w8a8) {
1386 buffer_size_nbytes += std::max(M * K, M * N) + M * sizeof(float);
1387 }
1388 if (use_fp8_w8a16) {
1389 buffer_size_nbytes += M * 2 * N * 2 + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_M * std::max(K, N) * 2;
1390 }
1391
1392 auto buffer = at::empty({buffer_size_nbytes}, hidden_states.options().dtype(at::kChar));
1393 AT_DISPATCH_REDUCED_FLOATING_TYPES(st, "share_experts_kernel_impl", [&] {
1394 scalar_t* __restrict__ intermediate_cache1 = (scalar_t*)((void*)(buffer.data_ptr<int8_t>()));
1395 float* __restrict__ C_tmp = (float*)((void*)(intermediate_cache1 + M * N));
1396
1397 if (use_int8_w8a8) {
1398 uint8_t* __restrict__ Aq_tmp = (uint8_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1399 float* __restrict__ As_tmp = (float*)((void*)(Aq_tmp + std::max(M * K, M * N)));
1400
1401 auto w1s = w1_scale.value();
1402 auto w2s = w2_scale.value();
1403 TORCH_CHECK(w1s.numel() == 2 * N);
1404 TORCH_CHECK(w2s.numel() == K);
1405
1406 shared_expert_int8_kernel_impl<scalar_t>(
1407 out_hidden_states.data_ptr<scalar_t>(),
1408 intermediate_cache1,
1409 C_tmp,
1410 Aq_tmp,
1411 As_tmp,
1412 hidden_states.data_ptr<scalar_t>(),
1413 packed_w1.data_ptr<int8_t>(),
1414 packed_w2.data_ptr<int8_t>(),
1415 w1s.data_ptr<float>(),
1416 w2s.data_ptr<float>(),
1417 conditional_data_ptr<scalar_t>(fused_experts_out),
1418 routed_scaling_factor_value,
1419 M,
1420 N,
1421 K);
1422 } else if (use_fp8_w8a16) {
1423 scalar_t* __restrict__ intermediate_cache0 = (scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
1424 scalar_t* __restrict__ B_tmp = (scalar_t*)((void*)(intermediate_cache0 + M * 2 * N));
1425
1426 CHECK_MOE_SCALES_FP8(0, 1);
1427 shared_expert_fp8_kernel_impl<scalar_t>(
1428 out_hidden_states.data_ptr<scalar_t>(),
1429 intermediate_cache0,
1430 intermediate_cache1,
1431 B_tmp,
1432 C_tmp,
1433 hidden_states.data_ptr<scalar_t>(),
1434 packed_w1.data_ptr<at::Float8_e4m3fn>(),
1435 packed_w2.data_ptr<at::Float8_e4m3fn>(),
1436 w1s.data_ptr<float>(),
1437 w2s.data_ptr<float>(),
1438 block_size_N,
1439 block_size_K,
1440 conditional_data_ptr<scalar_t>(fused_experts_out),
1441 routed_scaling_factor_value,
1442 M,
1443 N,
1444 K);
1445 } else {
1446 shared_expert_kernel_impl<scalar_t>(
1447 out_hidden_states.data_ptr<scalar_t>(),
1448 intermediate_cache1,
1449 C_tmp,
1450 hidden_states.data_ptr<scalar_t>(),
1451 packed_w1.data_ptr<scalar_t>(),
1452 packed_w2.data_ptr<scalar_t>(),
1453 conditional_data_ptr<scalar_t>(fused_experts_out),
1454 routed_scaling_factor_value,
1455 M,
1456 N,
1457 K);
1458 }
1459 });
1460 return out_hidden_states;
1461 }