[CPU] Migrate unquantized MoE to the modular-kernel experts structure (#50133)
Signed-off-by: jiang1.li <jiang1.li@intel.com> Signed-off-by: Fadi Arafeh <fadi.arafeh@arm.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com> Co-authored-by: Fadi Arafeh <fadi.arafeh@arm.com>
Li, Jiang committed
Aug 3, 2026 at 14:55 UTC
0a6446005d51c9e6bfa09352f7f288ddeff17c77
20 files changed
+972
-839
cmake/cpu_extension.cmake
+2
-1
@@ -461,6 +461,7 @@ set(VLLM_EXT_SRC
461
"csrc/cpu/pos_encoding.cpp"
462
"csrc/cpu/mamba_cpu.cpp"
463
"csrc/moe/dynamic_4bit_int_moe_cpu.cpp"
464
+ "csrc/cpu/cpu_fused_moe.cpp"
465
"csrc/cpu/cpu_attn.cpp"
466
"csrc/cpu/torch_bindings.cpp")
467
@@ -478,7 +479,6 @@ if (ASIMD_FOUND AND NOT APPLE_SILICON_FOUND)
479
"csrc/cpu/cpu_tanhf_neon.hpp"
480
${VLLM_EXT_SRC})
481
if (ARM_BF16_FOUND)
481
- set(VLLM_EXT_SRC "csrc/cpu/cpu_fused_moe.cpp" ${VLLM_EXT_SRC})
482
if (ARM_I8MM_FOUND)
483
set(VLLM_EXT_SRC "csrc/cpu/cpu_fused_moe_int8.cpp" ${VLLM_EXT_SRC})
484
endif()
@@ -535,6 +535,7 @@ if (ENABLE_X86_ISA)
535
536
set(VLLM_EXT_SRC_AVX2
537
"csrc/cpu/sgl-kernels/fla.cpp"
538
+ "csrc/cpu/cpu_fused_moe.cpp"
539
"csrc/cpu/utils.cpp"
540
"csrc/cpu/spec_decode_utils.cpp"
541
"csrc/cpu/cpu_attn.cpp"
csrc/cpu/cpu_fused_moe_activations.hpp
+24
-13
@@ -11,6 +11,13 @@
11
#include "cpu/cpu_arch_macros.h"
12
#include "cpu/utils.hpp"
13
14
+#if defined(DEFINE_FAST_EXP)
15
+ #define DEFINE_CPU_FUSED_MOE_EXP DEFINE_FAST_EXP
16
+#else
17
+ #define DEFINE_CPU_FUSED_MOE_EXP \
18
+ auto fast_exp = [](const vec_op::FP32Vec16& vec) { return vec.exp(); };
19
+#endif
20
+
21
namespace cpu_fused_moe_utils {
22
enum class FusedMOEAct {
23
SiluAndMul,
@@ -39,7 +46,7 @@ void swigluoai_and_mul(float* __restrict__ input, scalar_t* __restrict__ output,
46
const int32_t input_stride,
47
const int32_t output_stride) {
48
using scalar_vec_t = typename cpu_utils::VecTypeTrait<scalar_t>::vec_t;
42
-#if !defined(__aarch64__)
49
+#if defined(__AVX512F__)
50
// For GPT-OSS interleaved gate-up weights
51
alignas(64) static int32_t index[16] = {0, 2, 4, 6, 8, 10, 12, 14,
52
16, 18, 20, 22, 24, 26, 28, 30};
@@ -50,7 +57,7 @@ void swigluoai_and_mul(float* __restrict__ input, scalar_t* __restrict__ output,
57
vec_op::FP32Vec16 alpha_vec(1.702);
58
vec_op::FP32Vec16 one_vec(1.0);
59
53
- DEFINE_FAST_EXP
60
+ DEFINE_CPU_FUSED_MOE_EXP
61
62
for (int32_t m = 0; m < m_size; ++m) {
63
for (int32_t n = 0; n < n_size; n += 32) {
@@ -59,9 +66,19 @@ void swigluoai_and_mul(float* __restrict__ input, scalar_t* __restrict__ output,
66
vec_op::FP32Vec16 gate_vec(vec_op::uninit);
67
vec_op::FP32Vec16 up_vec(vec_op::uninit);
68
vec_op::FP32Vec16::load_even_odd(input + n, gate_vec, up_vec);
62
-#else
69
+#elif defined(__AVX512F__)
70
vec_op::FP32Vec16 gate_vec(input + n, index_vec);
71
vec_op::FP32Vec16 up_vec(input + n + 1, index_vec);
72
+#else
73
+ alignas(64) float gate_values[16];
74
+ alignas(64) float up_values[16];
75
+ const float* interleaved = input + n;
76
+ for (int32_t i = 0; i < 16; ++i) {
77
+ gate_values[i] = interleaved[2 * i];
78
+ up_values[i] = interleaved[2 * i + 1];
79
+ }
80
+ vec_op::FP32Vec16 gate_vec(gate_values);
81
+ vec_op::FP32Vec16 up_vec(up_values);
82
#endif
83
gate_vec = gate_vec.min(gate_up_max_vec);
84
up_vec = up_vec.clamp(up_min_vec, gate_up_max_vec);
@@ -86,7 +103,7 @@ void silu_and_mul(float* __restrict__ input, scalar_t* __restrict__ output,
103
float* __restrict__ up = input + dim;
104
vec_op::FP32Vec16 one_vec(1.0);
105
89
- DEFINE_FAST_EXP
106
+ DEFINE_CPU_FUSED_MOE_EXP
107
108
for (int32_t m = 0; m < m_size; ++m) {
109
for (int32_t n = 0; n < dim; n += 16) {
@@ -115,21 +132,13 @@ void gelu_and_mul(float* __restrict__ input, scalar_t* __restrict__ output,
132
vec_op::FP32Vec16 one_vec(1.0);
133
vec_op::FP32Vec16 w1_vec(M_SQRT1_2);
134
vec_op::FP32Vec16 w2_vec(0.5);
118
- alignas(64) float temp[16];
119
-
120
- DEFINE_FAST_EXP
135
136
for (int32_t m = 0; m < m_size; ++m) {
137
for (int32_t n = 0; n < dim; n += 16) {
138
vec_op::FP32Vec16 gate_vec(gate + n);
139
vec_op::FP32Vec16 up_vec(up + n);
140
auto er_input_vec = gate_vec * w1_vec;
127
-
128
- er_input_vec.save(temp);
129
- for (int32_t i = 0; i < 16; ++i) {
130
- temp[i] = std::erf(temp[i]);
131
- }
132
- vec_op::FP32Vec16 er_vec(temp);
141
+ auto er_vec = er_input_vec.er();
142
auto gelu = gate_vec * w2_vec * (one_vec + er_vec);
143
auto gated_output_fp32 = up_vec * gelu;
144
scalar_vec_t gated_output = scalar_vec_t(gated_output_fp32);
@@ -201,4 +210,6 @@ FORCE_INLINE void apply_gated_act(const FusedMOEAct act,
210
}
211
} // namespace cpu_fused_moe_utils
212
213
+#undef DEFINE_CPU_FUSED_MOE_EXP
214
+
215
#endif
csrc/cpu/cpu_types_riscv_impl.hpp
+22
@@ -678,6 +678,9 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
678
return FP32Vec16(
679
RVVI(__riscv_vfsub_vv_f32, LMUL_512)(reg, b.reg, VEC_ELEM_NUM));
680
}
681
+ FP32Vec16 operator-() const {
682
+ return FP32Vec16(RVVI(__riscv_vfneg_v_f32, LMUL_512)(reg, VEC_ELEM_NUM));
683
+ }
684
FP32Vec16 operator*(const FP32Vec16& b) const {
685
return FP32Vec16(
686
RVVI(__riscv_vfmul_vv_f32, LMUL_512)(reg, b.reg, VEC_ELEM_NUM));
@@ -898,6 +901,25 @@ struct INT8Vec16 : public Vec<INT8Vec16> {
901
}
902
};
903
904
+// Reference implementation for vector operations missing from some backends.
905
+struct INT8Vec64 {
906
+ constexpr static int VEC_ELEM_NUM = 64;
907
+
908
+ explicit INT8Vec64(const int8_t* ptr) {
909
+ std::memcpy(data_, ptr, sizeof(data_));
910
+ }
911
+
912
+ void save(int8_t* ptr) const { std::memcpy(ptr, data_, sizeof(data_)); }
913
+
914
+ void save(int8_t* ptr, const int elem_num) const {
915
+ TORCH_CHECK(elem_num > 0 && elem_num <= VEC_ELEM_NUM);
916
+ std::memcpy(ptr, data_, elem_num);
917
+ }
918
+
919
+ private:
920
+ int8_t data_[VEC_ELEM_NUM];
921
+};
922
+
923
// ============================================================================
924
// Type Traits & Global Helpers
925
// ============================================================================
csrc/cpu/cpu_types_scalar.hpp
+44
@@ -333,6 +333,13 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
333
return FP32Vec16(ret);
334
}
335
336
+ FP32Vec16 operator-() const {
337
+ f32x16_t ret;
338
+ unroll_loop<int, VEC_ELEM_NUM>(
339
+ [&ret, this](int i) { ret.val[i] = -reg.val[i]; });
340
+ return FP32Vec16(ret);
341
+ }
342
+
343
FP32Vec16 operator/(const FP32Vec16& b) const {
344
f32x16_t ret;
345
unroll_loop<int, VEC_ELEM_NUM>(
@@ -356,6 +363,10 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
363
return FP32Vec16(ret);
364
}
365
366
+ FP32Vec16 clamp(const FP32Vec16& min_v, const FP32Vec16& max_v) const {
367
+ return this->max(min_v).min(max_v);
368
+ }
369
+
370
FP32Vec16 abs() const {
371
f32x16_t ret;
372
unroll_loop<int, VEC_ELEM_NUM>(
@@ -363,6 +374,20 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
374
return FP32Vec16(ret);
375
}
376
377
+ FP32Vec16 exp() const {
378
+ f32x16_t ret;
379
+ unroll_loop<int, VEC_ELEM_NUM>(
380
+ [&ret, this](int i) { ret.val[i] = std::exp(reg.val[i]); });
381
+ return FP32Vec16(ret);
382
+ }
383
+
384
+ FP32Vec16 er() const {
385
+ f32x16_t ret;
386
+ unroll_loop<int, VEC_ELEM_NUM>(
387
+ [&ret, this](int i) { ret.val[i] = std::erf(reg.val[i]); });
388
+ return FP32Vec16(ret);
389
+ }
390
+
391
FP32Vec16 tanh() const {
392
f32x16_t ret;
393
unroll_loop<int, VEC_ELEM_NUM>(
@@ -404,6 +429,25 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
429
void save(void* ptr) const { *reinterpret_cast<f32x16_t*>(ptr) = reg; }
430
};
431
432
+// Reference implementation for vector operations missing from some backends.
433
+struct INT8Vec64 {
434
+ constexpr static int VEC_ELEM_NUM = 64;
435
+
436
+ explicit INT8Vec64(const int8_t* ptr) {
437
+ std::memcpy(data_, ptr, sizeof(data_));
438
+ }
439
+
440
+ void save(int8_t* ptr) const { std::memcpy(ptr, data_, sizeof(data_)); }
441
+
442
+ void save(int8_t* ptr, const int elem_num) const {
443
+ TORCH_CHECK(elem_num > 0 && elem_num <= VEC_ELEM_NUM);
444
+ std::memcpy(ptr, data_, elem_num);
445
+ }
446
+
447
+ private:
448
+ int8_t data_[VEC_ELEM_NUM];
449
+};
450
+
451
template <typename T>
452
struct VecType {
453
using vec_type = void;
csrc/cpu/cpu_types_vsx.hpp
+23
@@ -655,6 +655,11 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
655
vec_sub(reg.val[3], b.reg.val[3])}));
656
}
657
658
+ FP32Vec16 operator-() const {
659
+ return FP32Vec16(
660
+ f32x4x4_t({-reg.val[0], -reg.val[1], -reg.val[2], -reg.val[3]}));
661
+ }
662
+
663
FP32Vec16 operator/(const FP32Vec16& b) const {
664
return FP32Vec16(f32x4x4_t({vec_div(reg.val[0], b.reg.val[0]),
665
vec_div(reg.val[1], b.reg.val[1]),
@@ -756,6 +761,24 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
761
hi_e.reg.val[0], hi_e.reg.val[1]});
762
}
763
764
+ FP32Vec16 tanh() const {
765
+ FP32Vec8 lo(f32x4x2_t{reg.val[0], reg.val[1]});
766
+ FP32Vec8 hi(f32x4x2_t{reg.val[2], reg.val[3]});
767
+ auto lo_tanh = lo.tanh();
768
+ auto hi_tanh = hi.tanh();
769
+ return FP32Vec16(f32x4x4_t{lo_tanh.reg.val[0], lo_tanh.reg.val[1],
770
+ hi_tanh.reg.val[0], hi_tanh.reg.val[1]});
771
+ }
772
+
773
+ FP32Vec16 er() const {
774
+ FP32Vec8 lo(f32x4x2_t{reg.val[0], reg.val[1]});
775
+ FP32Vec8 hi(f32x4x2_t{reg.val[2], reg.val[3]});
776
+ auto lo_er = lo.er();
777
+ auto hi_er = hi.er();
778
+ return FP32Vec16(f32x4x4_t{lo_er.reg.val[0], lo_er.reg.val[1],
779
+ hi_er.reg.val[0], hi_er.reg.val[1]});
780
+ }
781
+
782
float reduce_max() {
783
__vector float max01 = vec_max(reg.val[0], reg.val[1]);
784
__vector float max23 = vec_max(reg.val[2], reg.val[3]);
csrc/cpu/cpu_types_vxe.hpp
+65
-1
@@ -6,8 +6,10 @@
6
#include <bit>
7
#include <cmath>
8
#include <cstdint>
9
+#include <cstring>
10
#include <limits>
11
#include <torch/all.h>
12
+
13
namespace vec_op {
14
15
struct fp8_e4m3_tag {};
@@ -727,6 +729,11 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
729
vec_sub(reg.val[3], b.reg.val[3])}));
730
}
731
732
+ FP32Vec16 operator-() const {
733
+ return FP32Vec16(f32x4x4_t({vec_neg(reg.val[0]), vec_neg(reg.val[1]),
734
+ vec_neg(reg.val[2]), vec_neg(reg.val[3])}));
735
+ }
736
+
737
FP32Vec16 operator/(const FP32Vec16& b) const {
738
return FP32Vec16(f32x4x4_t({vec_div(reg.val[0], b.reg.val[0]),
739
vec_div(reg.val[1], b.reg.val[1]),
@@ -734,6 +741,33 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
741
vec_div(reg.val[3], b.reg.val[3])}));
742
}
743
744
+ FP32Vec16 exp() const {
745
+ FP32Vec8 lo(f32x4x2_t{reg.val[0], reg.val[1]});
746
+ FP32Vec8 hi(f32x4x2_t{reg.val[2], reg.val[3]});
747
+ auto lo_exp = lo.exp();
748
+ auto hi_exp = hi.exp();
749
+ return FP32Vec16(f32x4x4_t{lo_exp.reg.val[0], lo_exp.reg.val[1],
750
+ hi_exp.reg.val[0], hi_exp.reg.val[1]});
751
+ }
752
+
753
+ FP32Vec16 tanh() const {
754
+ FP32Vec8 lo(f32x4x2_t{reg.val[0], reg.val[1]});
755
+ FP32Vec8 hi(f32x4x2_t{reg.val[2], reg.val[3]});
756
+ auto lo_tanh = lo.tanh();
757
+ auto hi_tanh = hi.tanh();
758
+ return FP32Vec16(f32x4x4_t{lo_tanh.reg.val[0], lo_tanh.reg.val[1],
759
+ hi_tanh.reg.val[0], hi_tanh.reg.val[1]});
760
+ }
761
+
762
+ FP32Vec16 er() const {
763
+ FP32Vec8 lo(f32x4x2_t{reg.val[0], reg.val[1]});
764
+ FP32Vec8 hi(f32x4x2_t{reg.val[2], reg.val[3]});
765
+ auto lo_er = lo.er();
766
+ auto hi_er = hi.er();
767
+ return FP32Vec16(f32x4x4_t{lo_er.reg.val[0], lo_er.reg.val[1],
768
+ hi_er.reg.val[0], hi_er.reg.val[1]});
769
+ }
770
+
771
float reduce_sum() const {
772
__vector float sum = vec_add(vec_add(reg.val[0], reg.val[1]),
773
vec_add(reg.val[2], reg.val[3]));
@@ -765,6 +799,17 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
799
vec_max(reg.val[3], b.reg.val[3])}));
800
}
801
802
+ FP32Vec16 min(const FP32Vec16& b) const {
803
+ return FP32Vec16(f32x4x4_t({vec_min(reg.val[0], b.reg.val[0]),
804
+ vec_min(reg.val[1], b.reg.val[1]),
805
+ vec_min(reg.val[2], b.reg.val[2]),
806
+ vec_min(reg.val[3], b.reg.val[3])}));
807
+ }
808
+
809
+ FP32Vec16 clamp(const FP32Vec16& min_v, const FP32Vec16& max_v) const {
810
+ return this->max(min_v).min(max_v);
811
+ }
812
+
813
float reduce_max() const {
814
__vector float m = vec_max(vec_max(reg.val[0], reg.val[1]),
815
vec_max(reg.val[2], reg.val[3]));
@@ -899,6 +944,25 @@ struct INT8Vec16 : public Vec<INT8Vec16> {
944
}
945
};
946
947
+// Reference implementation for vector operations missing from some backends.
948
+struct INT8Vec64 {
949
+ constexpr static int VEC_ELEM_NUM = 64;
950
+
951
+ explicit INT8Vec64(const int8_t* ptr) {
952
+ std::memcpy(data_, ptr, sizeof(data_));
953
+ }
954
+
955
+ void save(int8_t* ptr) const { std::memcpy(ptr, data_, sizeof(data_)); }
956
+
957
+ void save(int8_t* ptr, const int elem_num) const {
958
+ TORCH_CHECK(elem_num > 0 && elem_num <= VEC_ELEM_NUM);
959
+ std::memcpy(ptr, data_, elem_num);
960
+ }
961
+
962
+ private:
963
+ int8_t data_[VEC_ELEM_NUM];
964
+};
965
+
966
template <typename T>
967
struct VecType {
968
using vec_type = void;
@@ -1325,4 +1389,4 @@ FORCE_INLINE void prefetch(const void* addr) {
1389
1390
}; // namespace vec_op
1391
1328
-#endif
\ No newline at end of file
1392
+#endif
csrc/cpu/cpu_types_x86.hpp
+35
@@ -2,6 +2,7 @@
2
#ifndef CPU_TYPES_X86_HPP
3
#define CPU_TYPES_X86_HPP
4
5
+#include <cstring>
6
#include <immintrin.h>
7
#include <sleef.h>
8
#include <torch/all.h>
@@ -595,6 +596,8 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
596
597
FP32Vec16 tanh() const { return FP32Vec16(Sleef_tanhf16_u10(reg)); }
598
599
+ FP32Vec16 er() const { return FP32Vec16(Sleef_erff16_u10(reg)); }
600
+
601
float reduce_sum() const { return _mm512_reduce_add_ps(reg); }
602
603
float reduce_max() const { return _mm512_reduce_max_ps(reg); }
@@ -798,6 +801,14 @@ struct FP32Vec16 : public Vec<FP32Vec16> {
801
return FP32Vec16(low.tanh().reg, high.tanh().reg);
802
}
803
804
+ FP32Vec16 exp() const {
805
+ return FP32Vec16(Sleef_expf8_u10(reg_low), Sleef_expf8_u10(reg_high));
806
+ }
807
+
808
+ FP32Vec16 er() const {
809
+ return FP32Vec16(Sleef_erff8_u10(reg_low), Sleef_erff8_u10(reg_high));
810
+ }
811
+
812
FP32Vec16 min(const FP32Vec16& b) const {
813
return FP32Vec16(_mm256_min_ps(reg_low, b.reg_low),
814
_mm256_min_ps(reg_high, b.reg_high));
@@ -927,6 +938,30 @@ struct INT8Vec16 : public Vec<INT8Vec16> {
938
for (int i = 0; i < elem_num; ++i) ptr[i] = ar.values[i];
939
}
940
};
941
+
942
+struct INT8Vec64 : public Vec<INT8Vec64> {
943
+ constexpr static int VEC_ELEM_NUM = 64;
944
+
945
+ __m256i reg_low;
946
+ __m256i reg_high;
947
+
948
+ explicit INT8Vec64(const void* ptr)
949
+ : reg_low(_mm256_loadu_si256(reinterpret_cast<const __m256i*>(ptr))),
950
+ reg_high(
951
+ _mm256_loadu_si256(reinterpret_cast<const __m256i*>(ptr) + 1)) {}
952
+
953
+ void save(void* ptr) const {
954
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(ptr), reg_low);
955
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(ptr) + 1, reg_high);
956
+ }
957
+
958
+ void save(int8_t* ptr, const int elem_num) const {
959
+ TORCH_CHECK(elem_num > 0 && elem_num <= VEC_ELEM_NUM);
960
+ int8_t values[VEC_ELEM_NUM];
961
+ save(values);
962
+ std::memcpy(ptr, values, elem_num);
963
+ }
964
+};
965
#endif
966
967
template <typename T>
csrc/cpu/micro_gemm/cpu_micro_gemm_vec.hpp
+1
-2
@@ -71,8 +71,7 @@ class TileGemm82 {
71
scalar_t* __restrict__ curr_m_a = curr_a;
72
vec_op::unroll_loop<int32_t, M>([&](int32_t i) {
73
scalar_t v = *curr_m_a;
74
- load_vec_t a_reg_original(v);
75
- vec_op::FP32Vec16 a_reg(a_reg_original);
74
+ vec_op::FP32Vec16 a_reg(static_cast<float>(v));
75
c_regs[i * 2] = c_regs[i * 2] + a_reg * fp32_b_0_reg;
76
c_regs[i * 2 + 1] = c_regs[i * 2 + 1] + a_reg * fp32_b_1_reg;
77
csrc/cpu/torch_bindings.cpp
-3
@@ -614,7 +614,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
614
#endif
615
616
// fused moe
617
-#if defined(__AVX512F__) || (defined(ARM_BF16_SUPPORT) && !defined(__APPLE__))
617
ops.def(
618
"prepack_moe_weight(Tensor weight, Tensor(a1!) packed_weight, str isa) "
619
"-> ()");
@@ -625,8 +624,6 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
624
"bool skip_weighted, "
625
"str act, str isa) -> ()");
626
ops.impl("cpu_fused_moe", torch::kCPU, &cpu_fused_moe);
628
-#endif // #if defined(__AVX512F__) || (defined(ARM_BF16_SUPPORT) &&
629
- // !defined(__APPLE__))
627
#if defined(ARM_I8MM_SUPPORT) && defined(ARM_BF16_SUPPORT) && \
628
!defined(__APPLE__)
629
ops.def(
csrc/cpu/utils.hpp
-2
@@ -40,12 +40,10 @@ struct VecTypeTrait<c10::BFloat16> {
40
using vec_t = vec_op::BF16Vec16;
41
};
42
43
-#if !defined(__powerpc__)
43
template <>
44
struct VecTypeTrait<c10::Half> {
45
using vec_t = vec_op::FP16Vec16;
46
};
48
-#endif
47
48
struct Counter {
49
std::atomic<int64_t> counter;
docs/design/moe_kernel_features.md
+1
-1
@@ -91,7 +91,7 @@ To be used with a particular `FusedMoEPrepareAndFinalizeModular` subclass, MoE k
91
| trtllm | standard | mxfp4,</br>nvfp4 | G(16),G(32) | <sup>5</sup> | N | Y | [`TrtLlmMxfp4ExpertsMonolithic`][vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe.TrtLlmMxfp4ExpertsMonolithic],</br>[`TrtLlmMxfp4ExpertsModular`][vllm.model_executor.layers.fused_moe.experts.trtllm_mxfp4_moe.TrtLlmMxfp4ExpertsModular],</br>[`TrtLlmNvFp4ExpertsMonolithic`][vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe.TrtLlmNvFp4ExpertsMonolithic],</br>[`TrtLlmNvfp4ExpertsModular`][vllm.model_executor.layers.fused_moe.experts.trtllm_nvfp4_moe.TrtLlmNvFp4ExpertsModular] |
92
| hpc | standard | fp8 | G(128),T | silu | Y | Y | [`HPCExperts`][vllm.model_executor.layers.fused_moe.hpc_moe.HPCExperts] |
93
| rocm aiter moe | standard | mxfp4,</br>fp8 | G(32),G(128),A,T | silu, gelu,</br>swigluoai | Y | N | `rocm_aiter_fused_experts`,</br>`AiterExperts` |
94
-| cpu_fused_moe | standard | N/A | N/A | silu | N | N | [`CPUFusedMOE`][vllm.model_executor.layers.fused_moe.cpu_fused_moe.CPUFusedMOE] |
94
+| cpu_moe | standard | N/A | N/A | silu, gelu,</br>gelu_tanh,</br>swigluoai | Y | N | [`X86CPUUnquantizedExperts`][vllm.model_executor.layers.fused_moe.experts.cpu_moe.X86CPUUnquantizedExperts],</br>[`ArmCPUUnquantizedExperts`][vllm.model_executor.layers.fused_moe.experts.cpu_moe.ArmCPUUnquantizedExperts],</br>[`CPUUnquantizedExperts`][vllm.model_executor.layers.fused_moe.experts.cpu_moe.CPUUnquantizedExperts] |
95
| naive batched<sup>4</sup> | batched | int8,</br>fp8 | G,A,T | silu, gelu | <sup>6</sup> | Y | [`NaiveBatchedExperts`][vllm.model_executor.layers.fused_moe.experts.fused_batched_moe.NaiveBatchedExperts] |
96
97
!!! info "Table key"
docs/getting_started/installation/cpu.md
+1
-1
@@ -152,7 +152,7 @@ VLLM_USE_PRECOMPILED=1 VLLM_PRECOMPILED_WHEEL_VARIANT=cpu VLLM_TARGET_DEVICE=cpu
152
- `VLLM_CPU_OMP_THREADS_BIND`: specify the CPU cores dedicated to the OpenMP threads, can be set as CPU id lists, `auto` (by default), or `nobind` (to disable binding to individual CPU cores and to inherit user-defined OpenMP variables). For example, `VLLM_CPU_OMP_THREADS_BIND=0-31` means there will be 32 OpenMP threads bound on 0-31 CPU cores. `VLLM_CPU_OMP_THREADS_BIND=0-31|32-63` means there will be 2 tensor parallel processes, 32 OpenMP threads of rank0 are bound on 0-31 CPU cores, and the OpenMP threads of rank1 are bound on 32-63 CPU cores. By setting to `auto`, the OpenMP threads of each rank are bound to the CPU cores in each NUMA node respectively. If set to `nobind`, the number of OpenMP threads is determined by the standard `OMP_NUM_THREADS` environment variable.
153
- `VLLM_CPU_NUM_OF_RESERVED_CPU`: specify the number of CPU cores which are not dedicated to the OpenMP threads for each rank. The variable only takes effect when VLLM_CPU_OMP_THREADS_BIND is set to `auto`. Default value is `None`. If the value is not set and use `auto` thread binding, no CPU will be reserved for `world_size == 1`, 1 CPU per rank will be reserved for `world_size > 1`.
154
- `CPU_VISIBLE_MEMORY_NODES`: specify visible NUMA memory nodes for vLLM CPU workers, similar to ```CUDA_VISIBLE_DEVICES```. The variable only takes effect when VLLM_CPU_OMP_THREADS_BIND is set to `auto`. The variable provides more control for the auto thread-binding feature, such as masking nodes and changing nodes binding sequence.
155
-- `VLLM_CPU_SGL_KERNEL` (x86 only, Experimental): whether to use small-batch optimized kernels for linear layer and MoE layer, especially for low-latency requirements like online serving. The kernels require AMX instruction set, BFloat16 weight type and weight shapes divisible by 32. Default is `0` (False).
155
+- `VLLM_CPU_SGL_KERNEL` (x86 only, Experimental): whether to use small-batch optimized kernels for the linear layer, especially for low-latency requirements like online serving. The kernels require AMX instruction set, BFloat16 weight type and weight shapes divisible by 32. Default is `0` (False). MoE layers always use the grouped-gemm kernels on x86 and are unaffected by this variable.
156
- `VLLM_ZENTORCH_WEIGHT_PREPACK` (AMD Zen only): when `ZenCpuPlatform` is active, eagerly prepack linear weights into ZenDNN's blocked layout at model load time, eliminating per-inference layout conversion overhead. Default is `1` (enabled). See [AMD Zen optimizations](#amd-zen-optimizations).
157
158
## FAQ
tests/kernels/moe/test_cpu_fused_moe.py
+161
-48
@@ -1,9 +1,12 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
+import sys
5
+
6
import pytest
7
import torch
8
9
+import vllm.model_executor.layers.fused_moe.modular_kernel as mk
10
from tests.kernels.allclose_default import get_default_atol, get_default_rtol
11
from vllm._custom_ops import (
12
cpu_fused_moe,
@@ -12,9 +15,18 @@ from vllm._custom_ops import (
15
cpu_prepack_moe_weight_int8,
16
)
17
from vllm.model_executor.layers.fused_moe.activation import MoEActivation
15
-from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
16
- _CPU_MOE_ACT_FN,
17
- CPUFusedMOE,
18
+from vllm.model_executor.layers.fused_moe.config import (
19
+ FusedMoEConfig,
20
+ FusedMoEParallelConfig,
21
+ FusedMoEQuantConfig,
22
+ RoutingMethodType,
23
+ biased_moe_quant_config,
24
+)
25
+from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
26
+ ArmCPUUnquantizedExperts,
27
+ CPUUnquantizedExperts,
28
+ X86CPUUnquantizedExperts,
29
+ select_experts,
30
)
31
from vllm.platforms import CpuArchEnum, current_platform
32
from vllm.utils.torch_utils import set_random_seed
@@ -36,7 +48,10 @@ ACT = [
48
]
49
USE_BIAS = [False, True]
50
ISA = ["vec"]
39
-if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
51
+if (
52
+ current_platform.get_cpu_architecture() == CpuArchEnum.ARM
53
+ and sys.platform != "darwin"
54
+):
55
ISA.append("neon")
56
if torch.cpu._is_amx_tile_supported():
57
ISA.append("amx")
@@ -44,6 +59,27 @@ if torch.cpu._is_amx_tile_supported():
59
DTYPE = [torch.bfloat16]
60
61
62
+def _ref_moe_activation(
63
+ input: torch.Tensor,
64
+ activation: MoEActivation,
65
+) -> torch.Tensor:
66
+ if activation == MoEActivation.SWIGLUOAI:
67
+ gate = input[..., ::2].clamp(max=7.0)
68
+ up = input[..., 1::2].clamp(min=-7.0, max=7.0)
69
+ glu = gate * torch.sigmoid(gate * 1.702)
70
+ return (up + 1) * glu
71
+
72
+ d = input.shape[-1] // 2
73
+ gate, up = input[..., :d], input[..., d:]
74
+ if activation == MoEActivation.SILU:
75
+ return torch.nn.functional.silu(gate) * up
76
+ if activation == MoEActivation.GELU:
77
+ return torch.nn.functional.gelu(gate, approximate="none") * up
78
+ if activation == MoEActivation.GELU_TANH:
79
+ return torch.nn.functional.gelu(gate, approximate="tanh") * up
80
+ raise ValueError(f"Unsupported activation: {activation}")
81
+
82
+
83
def ref_fused_moe(
84
input: torch.Tensor,
85
w13: torch.Tensor,
@@ -87,7 +123,8 @@ def ref_fused_moe(
123
tokens_for_this_expert, curr_w13, curr_w13_bias
124
)
125
# Note: to simulate the kernel implementation
90
- gate_up = _CPU_MOE_ACT_FN[activation](gate_up).to(dtype=input.dtype).float()
126
+ gate_up = _ref_moe_activation(gate_up, activation)
127
+ gate_up = gate_up.to(dtype=input.dtype).float()
128
expert_out = torch.nn.functional.linear(gate_up, curr_w2, curr_w2_bias)
129
130
outputs.append(expert_out)
@@ -166,7 +203,7 @@ def ref_fused_moe_int8(
203
if w13_bias is not None:
204
gate_up += w13_bias[expert_idx].float()
205
169
- intermediate = _CPU_MOE_ACT_FN[activation](gate_up).to(input.dtype)
206
+ intermediate = _ref_moe_activation(gate_up, activation).to(input.dtype)
207
intermediate_int8, intermediate_scale = quantize_per_token(intermediate)
208
output = torch.matmul(
209
intermediate_int8.float(),
@@ -356,8 +393,8 @@ UNALIGNED_INTERMEDIATE_DIM = 176
393
394
class _StubMoELayer(torch.nn.Module):
395
"""Minimal stand-in for the real MoE layer module, exposing just what
359
- CPUFusedMOE reads/replaces (w13_weight, w2_weight, activation, and
360
- optionally w13_bias/w2_bias)."""
396
+ the unquantized CPU experts read/replace (w13_weight, w2_weight, the
397
+ router configuration, and optionally w13_bias/w2_bias)."""
398
399
def __init__(
400
self,
@@ -375,12 +412,77 @@ class _StubMoELayer(torch.nn.Module):
412
self.w13_bias = torch.nn.Parameter(w13_bias, requires_grad=False)
413
if w2_bias is not None:
414
self.w2_bias = torch.nn.Parameter(w2_bias, requires_grad=False)
415
+ self.use_grouped_topk = False
416
+ self.renormalize = False
417
+ self.scoring_func = "softmax"
418
+ self.custom_routing_function = None
419
+
420
+
421
+def _make_moe_config(
422
+ expert_num: int,
423
+ hidden_size: int,
424
+ intermediate_size: int,
425
+ topk_num: int,
426
+ dtype: torch.dtype,
427
+ act: MoEActivation,
428
+ has_bias: bool = False,
429
+) -> FusedMoEConfig:
430
+ return FusedMoEConfig(
431
+ num_experts=expert_num,
432
+ experts_per_token=topk_num,
433
+ hidden_dim=hidden_size,
434
+ intermediate_size=intermediate_size,
435
+ num_local_experts=expert_num,
436
+ num_logical_experts=expert_num,
437
+ moe_parallel_config=FusedMoEParallelConfig.make_no_parallel(),
438
+ activation=act,
439
+ in_dtype=dtype,
440
+ device="cpu",
441
+ routing_method=RoutingMethodType.Default,
442
+ has_bias=has_bias,
443
+ )
444
+
445
+
446
+@pytest.mark.parametrize(
447
+ ("hidden_size", "intermediate_size", "act", "expected"),
448
+ [
449
+ (128, 128, MoEActivation.SILU, True),
450
+ # Hidden size is not 32-aligned.
451
+ (112, 128, MoEActivation.SILU, False),
452
+ (128, 176, MoEActivation.SILU, True),
453
+ # SwiGLUOAI's interleaved gate/up layout cannot be padded.
454
+ (128, 176, MoEActivation.SWIGLUOAI, False),
455
+ ],
456
+)
457
+def test_cpu_vec_fused_moe_shape_support(
458
+ hidden_size: int,
459
+ intermediate_size: int,
460
+ act: MoEActivation,
461
+ expected: bool,
462
+):
463
+ moe_config = _make_moe_config(
464
+ expert_num=8,
465
+ hidden_size=hidden_size,
466
+ intermediate_size=intermediate_size,
467
+ topk_num=4,
468
+ dtype=torch.bfloat16,
469
+ act=act,
470
+ )
471
+ supported, _ = CPUUnquantizedExperts.is_supported_config(
472
+ CPUUnquantizedExperts,
473
+ moe_config,
474
+ None,
475
+ None,
476
+ mk.FusedMoEActivationFormat.Standard,
477
+ )
478
+ assert supported is expected
479
480
481
@pytest.mark.parametrize("expert_num", EXPERT_NUM)
482
@pytest.mark.parametrize("hidden_size", HIDDEN_DIM)
483
@pytest.mark.parametrize("use_bias", USE_BIAS)
484
@pytest.mark.parametrize("dtype", DTYPE)
485
+@pytest.mark.parametrize("isa", ISA)
486
@pytest.mark.parametrize(
487
"act",
488
[MoEActivation.SILU, MoEActivation.GELU, MoEActivation.GELU_TANH],
@@ -391,14 +493,11 @@ def test_cpu_fused_moe_unaligned_intermediate_size(
493
hidden_size: int,
494
use_bias: bool,
495
dtype: torch.dtype,
496
+ isa: str,
497
act: MoEActivation,
498
):
396
- """An unaligned per-partition moe_intermediate_size must still hit the
397
- grouped-gemm fast path via automatic zero-padding, with numerically
398
- correct output, instead of silently falling back to the much slower
399
- per-expert torch loop."""
400
- if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
401
- pytest.skip("padding is only applied on the x86 AMX/vector kernels")
499
+ """CPU kernels handle unaligned intermediate sizes by zero-padding the
500
+ weights before prepacking."""
501
502
set_random_seed(0)
503
batch_size = 64
@@ -423,9 +522,15 @@ def test_cpu_fused_moe_unaligned_intermediate_size(
522
w2_bias = torch.randn((expert_num, hidden_size), dtype=dtype) / (
523
0.5 * hidden_size**0.5
524
)
426
- score = torch.softmax(router_logits, dim=-1, dtype=torch.float32)
427
- topk_weight, topk_ids = torch.topk(score, topk_num)
428
- topk_ids = topk_ids.to(torch.int32)
525
+ # Route with the same helper apply() uses internally, so the reference
526
+ # only differs from the kernel in how the experts are evaluated.
527
+ topk_weight, topk_ids = select_experts(
528
+ hidden_states=input,
529
+ router_logits=router_logits,
530
+ top_k=topk_num,
531
+ use_grouped_topk=False,
532
+ renormalize=False,
533
+ )
534
535
ref_output = ref_fused_moe(
536
input, w13, w2, w13_bias, w2_bias, topk_weight, topk_ids, act
@@ -439,40 +544,48 @@ def test_cpu_fused_moe_unaligned_intermediate_size(
544
w2_bias.clone() if w2_bias is not None else None,
545
)
546
442
- cpu_moe = CPUFusedMOE(layer)
443
- assert cpu_moe.forward_method == cpu_moe.forward_grouped_gemm, (
444
- "expected the padded intermediate size to hit the grouped-gemm fast path"
547
+ moe_config = _make_moe_config(
548
+ expert_num, hidden_size, intermediate_size, topk_num, dtype, act, use_bias
549
+ )
550
+ experts_cls = {
551
+ "vec": CPUUnquantizedExperts,
552
+ "amx": X86CPUUnquantizedExperts,
553
+ "neon": ArmCPUUnquantizedExperts,
554
+ }[isa]
555
+ supported, reason = experts_cls.is_supported_config(
556
+ experts_cls,
557
+ moe_config,
558
+ None,
559
+ None,
560
+ mk.FusedMoEActivationFormat.Standard,
561
+ )
562
+ assert supported, (
563
+ f"expected the padded intermediate size to be supported, got {reason}"
564
)
565
447
- output = cpu_moe.forward_method(
448
- layer, input, topk_weight, topk_ids, act, expert_num, False
566
+ # Mirror UnquantizedFusedMoEMethod._setup_kernel: the quant config is
567
+ # built from the layer first, then the experts shuffle the layer into the
568
+ # kernel's runtime format.
569
+ quant_config = (
570
+ biased_moe_quant_config(layer.w13_bias, layer.w2_bias)
571
+ if use_bias
572
+ else FusedMoEQuantConfig.make()
573
+ )
574
+ experts = experts_cls(moe_config, quant_config)
575
+ assert experts.isa == isa
576
+ experts.process_weights_after_loading(layer)
577
+
578
+ output = experts.apply(
579
+ hidden_states=input,
580
+ w1=layer.w13_weight,
581
+ w2=layer.w2_weight,
582
+ router_logits=router_logits,
583
+ activation=act,
584
+ global_num_experts=expert_num,
585
+ expert_map=None,
586
+ a1q_scale=None,
587
+ apply_router_weight_on_input=False,
588
)
589
590
atol, rtol = get_default_atol(output), get_default_rtol(output)
591
torch.testing.assert_close(output, ref_output, atol=atol, rtol=rtol)
453
-
454
-
455
-def test_cpu_fused_moe_unaligned_intermediate_size_swigluoai(default_vllm_config):
456
- """swigluoai's interleaved gate/up layout isn't padded automatically. On
457
- AMX-capable CPUs this must raise rather than silently falling back to
458
- the (correct but much slower) per-expert torch loop; elsewhere it should
459
- fall back exactly as before."""
460
- set_random_seed(0)
461
- expert_num = 8
462
- hidden_size = 128
463
- intermediate_size = UNALIGNED_INTERMEDIATE_DIM
464
- dtype = torch.bfloat16
465
- up_dim = 2 * intermediate_size
466
-
467
- layer = _StubMoELayer(
468
- torch.randn((expert_num, up_dim, hidden_size), dtype=dtype),
469
- torch.randn((expert_num, hidden_size, intermediate_size), dtype=dtype),
470
- MoEActivation.SWIGLUOAI,
471
- )
472
-
473
- if torch.cpu._is_amx_tile_supported():
474
- with pytest.raises(RuntimeError):
475
- CPUFusedMOE(layer)
476
- else:
477
- cpu_moe = CPUFusedMOE(layer)
478
- assert cpu_moe.forward_method == cpu_moe.forward_torch
tests/kernels/moe/test_moe.py
-79
@@ -1482,85 +1482,6 @@ def test_moe_sum_pad_aware(topk: int, dtype: torch.dtype, topk_ids_dtype: torch.
1482
opcheck(torch.ops._moe_C.moe_sum, (input, actual, topk_ids, expert_map))
1483
1484
1485
-@pytest.mark.usefixtures("default_vllm_config")
1486
-@pytest.mark.parametrize("m", [1, 33])
1487
-@pytest.mark.parametrize("n,k", [(128, 128)])
1488
-@pytest.mark.parametrize("e", [8])
1489
-@pytest.mark.parametrize("topk", [2])
1490
-@pytest.mark.parametrize("dtype", [torch.float32, torch.bfloat16])
1491
-@pytest.mark.parametrize("with_bias", [False, True])
1492
-@pytest.mark.parametrize("activation", [MoEActivation.SILU])
1493
-@pytest.mark.skipif(not current_platform.is_cpu(), reason="CPU only test")
1494
-def test_cpu_fused_moe_basic(
1495
- m: int,
1496
- n: int,
1497
- k: int,
1498
- e: int,
1499
- topk: int,
1500
- dtype: torch.dtype,
1501
- with_bias: bool,
1502
- activation: MoEActivation,
1503
-):
1504
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import CPUFusedMOE
1505
-
1506
- device = "cpu"
1507
- set_random_seed(7)
1508
-
1509
- a = torch.randn((m, k), device=device, dtype=dtype) / 10
1510
- w13 = torch.randn((e, 2 * n, k), device=device, dtype=dtype) / 10
1511
- w2 = torch.randn((e, k, n), device=device, dtype=dtype) / 10
1512
- router_logits = torch.randn((m, e), device=device, dtype=dtype)
1513
-
1514
- b1 = b2 = None
1515
- if with_bias:
1516
- b1 = torch.randn((e, 2 * n), device=device, dtype=dtype) / 10
1517
- b2 = torch.randn((e, k), device=device, dtype=dtype) / 10
1518
-
1519
- ref = (
1520
- torch_moe(a, w13, w2, router_logits, topk, b1, b2)
1521
- if with_bias
1522
- else torch_moe(a, w13, w2, router_logits, topk)
1523
- )
1524
-
1525
- class _Dummy(torch.nn.Module):
1526
- def __init__(self, w13, w2, b1=None, b2=None):
1527
- super().__init__()
1528
- self.w13_weight = torch.nn.Parameter(w13, requires_grad=False)
1529
- self.w2_weight = torch.nn.Parameter(w2, requires_grad=False)
1530
- if b1 is not None:
1531
- self.w13_bias = torch.nn.Parameter(b1, requires_grad=False)
1532
- if b2 is not None:
1533
- self.w2_bias = torch.nn.Parameter(b2, requires_grad=False)
1534
-
1535
- layer = _Dummy(w13, w2, b1, b2).to(dtype)
1536
- fused = CPUFusedMOE(layer)
1537
- out = fused(
1538
- layer=layer,
1539
- x=a,
1540
- use_grouped_topk=False,
1541
- top_k=topk,
1542
- router_logits=router_logits,
1543
- renormalize=False,
1544
- global_num_experts=e,
1545
- expert_map=None,
1546
- custom_routing_function=None,
1547
- scoring_func="softmax",
1548
- routed_scaling_factor=1.0,
1549
- e_score_correction_bias=None,
1550
- apply_router_weight_on_input=False,
1551
- activation=activation,
1552
- )
1553
-
1554
- # Tolerances: fp32 tight; bf16 looser (esp. with bias)
1555
- if dtype == torch.float32:
1556
- atol = 1e-3
1557
- elif with_bias:
1558
- atol = 8e-2
1559
- else:
1560
- atol = 5e-2
1561
- torch.testing.assert_close(out, ref, atol=atol, rtol=0)
1562
-
1563
-
1485
def _batched_fused_marlin_moe_cases() -> list[Any]:
1486
cases = [
1487
pytest.param(
tests/kernels/moe/test_unquantized_backend_selection.py
+114
-3
@@ -3,14 +3,16 @@
3
from unittest.mock import patch
4
5
import pytest
6
+import torch
7
8
from tests.kernels.moe.utils import make_dummy_moe_config
9
from vllm.model_executor.layers.fused_moe.config import RoutingMethodType
10
from vllm.model_executor.layers.fused_moe.oracle.unquantized import (
11
UnquantizedMoeBackend,
12
+ backend_to_kernel_cls,
13
select_unquantized_moe_backend,
14
)
13
-from vllm.platforms import current_platform
15
+from vllm.platforms import CpuArchEnum, current_platform
16
17
skipif_not_cuda_rocm = pytest.mark.skipif(
18
not (current_platform.is_cuda() or current_platform.is_rocm()),
@@ -18,6 +20,109 @@ skipif_not_cuda_rocm = pytest.mark.skipif(
20
)
21
22
23
+@pytest.mark.parametrize(
24
+ ("amx_supported", "in_dtype", "expect_amx_kernel"),
25
+ [
26
+ (True, torch.bfloat16, True),
27
+ (False, torch.bfloat16, False),
28
+ (True, torch.float16, False),
29
+ ],
30
+)
31
+def test_x86_cpu_unquantized_kernel_selection(
32
+ amx_supported: bool,
33
+ in_dtype: torch.dtype,
34
+ expect_amx_kernel: bool,
35
+):
36
+ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
37
+ CPUUnquantizedExperts,
38
+ X86CPUUnquantizedExperts,
39
+ )
40
+
41
+ with (
42
+ patch.object(current_platform, "is_cpu", return_value=True),
43
+ patch.object(
44
+ current_platform,
45
+ "get_cpu_architecture",
46
+ return_value=CpuArchEnum.X86,
47
+ ),
48
+ patch("torch.cpu._is_amx_tile_supported", return_value=amx_supported),
49
+ ):
50
+ moe_config = make_dummy_moe_config(
51
+ hidden_dim=128,
52
+ intermediate_size=128,
53
+ in_dtype=in_dtype,
54
+ )
55
+ kernel_cls = next(
56
+ cls
57
+ for cls in backend_to_kernel_cls(UnquantizedMoeBackend.CPU)
58
+ if cls.is_supported_config(
59
+ cls,
60
+ moe_config,
61
+ None,
62
+ None,
63
+ CPUUnquantizedExperts.activation_format(),
64
+ )[0]
65
+ )
66
+
67
+ expected_kernel_cls = (
68
+ X86CPUUnquantizedExperts if expect_amx_kernel else CPUUnquantizedExperts
69
+ )
70
+ assert kernel_cls is expected_kernel_cls
71
+
72
+
73
+@pytest.mark.parametrize(
74
+ ("platform", "in_dtype", "expect_arm_kernel"),
75
+ [
76
+ ("linux", torch.bfloat16, True),
77
+ ("linux", torch.float16, False),
78
+ ("darwin", torch.bfloat16, False),
79
+ ],
80
+)
81
+def test_arm_cpu_unquantized_kernel_selection(
82
+ platform: str,
83
+ in_dtype: torch.dtype,
84
+ expect_arm_kernel: bool,
85
+):
86
+ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
87
+ ArmCPUUnquantizedExperts,
88
+ CPUUnquantizedExperts,
89
+ )
90
+
91
+ with (
92
+ patch.object(current_platform, "is_cpu", return_value=True),
93
+ patch.object(
94
+ current_platform,
95
+ "get_cpu_architecture",
96
+ return_value=CpuArchEnum.ARM,
97
+ ),
98
+ patch(
99
+ "vllm.model_executor.layers.fused_moe.experts.cpu_moe.sys.platform",
100
+ platform,
101
+ ),
102
+ ):
103
+ moe_config = make_dummy_moe_config(
104
+ hidden_dim=128,
105
+ intermediate_size=128,
106
+ in_dtype=in_dtype,
107
+ )
108
+ kernel_cls = next(
109
+ cls
110
+ for cls in backend_to_kernel_cls(UnquantizedMoeBackend.CPU)
111
+ if cls.is_supported_config(
112
+ cls,
113
+ moe_config,
114
+ None,
115
+ None,
116
+ CPUUnquantizedExperts.activation_format(),
117
+ )[0]
118
+ )
119
+
120
+ expected_kernel_cls = (
121
+ ArmCPUUnquantizedExperts if expect_arm_kernel else CPUUnquantizedExperts
122
+ )
123
+ assert kernel_cls is expected_kernel_cls
124
+
125
+
126
@pytest.mark.parametrize(
127
"platform_method,expected_backend",
128
[
@@ -69,14 +174,20 @@ def test_select_default_backend_by_platform(
174
patch.object(current_platform, "is_out_of_tree", return_value=False),
175
patch.object(current_platform, platform_method, return_value=True),
176
):
72
- moe_config = make_dummy_moe_config()
177
+ # CPU's grouped-gemm kernels require hidden/intermediate sizes
178
+ # aligned to 32; the size-1 defaults only work for backends that
179
+ # don't check shapes at selection time.
180
+ moe_config = (
181
+ make_dummy_moe_config(hidden_dim=128, intermediate_size=128)
182
+ if expected_backend == UnquantizedMoeBackend.CPU
183
+ else make_dummy_moe_config()
184
+ )
185
selected_backend, expert_cls = select_unquantized_moe_backend(
186
moe_config=moe_config
187
)
188
189
assert selected_backend == expected_backend
190
if expected_backend in [
79
- UnquantizedMoeBackend.CPU,
191
UnquantizedMoeBackend.OOT,
192
UnquantizedMoeBackend.TPU,
193
]:
vllm/model_executor/layers/fused_moe/cpu_fused_moe.py
deleted
-577
@@ -1,577 +0,0 @@
1
-# SPDX-License-Identifier: Apache-2.0
2
-# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-import weakref
4
-from collections.abc import Callable
5
-
6
-import torch
7
-from torch.nn import functional as F
8
-
9
-from vllm import _custom_ops as ops
10
-from vllm._custom_ops import (
11
- CPUQuantMethod,
12
- cpu_fused_moe,
13
- cpu_prepack_moe_weight,
14
- fused_experts_cpu,
15
-)
16
-from vllm.model_executor.layers.activation import SiluAndMul
17
-from vllm.model_executor.layers.fused_moe.activation import MoEActivation
18
-from vllm.model_executor.layers.quantization.utils.layer_utils import replace_parameter
19
-from vllm.platforms import CpuArchEnum, current_platform
20
-from vllm.utils.torch_utils import direct_register_custom_op
21
-
22
-_CPU_MOE_LAYER_CACHE = {}
23
-# The CPU grouped-gemm MoE kernels (AMX and vector) tile the expert
24
-# intermediate ("N") dimension in blocks of this size and have no tail/
25
-# remainder handling, so a shard is only eligible for the fast path when
26
-# its per-partition intermediate size is a multiple of it.
27
-_MOE_GROUPED_GEMM_N_TILE = 32
28
-
29
-
30
-def _swigluoai_forward_native(
31
- x: torch.Tensor,
32
- alpha: float = 1.702,
33
- limit: float = 7.0,
34
-) -> torch.Tensor:
35
- """PyTorch-native implementation of SwigluOAIAndMul.forward_native.
36
-
37
- Standalone function to avoid instantiating SwigluOAIAndMul (a CustomOp)
38
- which would trigger get_current_vllm_config() before config is set.
39
- """
40
- gate, up = x[..., ::2], x[..., 1::2]
41
- gate = gate.clamp(min=None, max=limit)
42
- up = up.clamp(min=-limit, max=limit)
43
- glu = gate * torch.sigmoid(gate * alpha)
44
- gated_output = (up + 1) * glu
45
- return gated_output
46
-
47
-
48
-def _gelu_and_mul(
49
- x: torch.Tensor,
50
-) -> torch.Tensor:
51
- d = x.shape[-1] // 2
52
- return F.gelu(x[..., :d], approximate="none") * x[..., d:]
53
-
54
-
55
-# Map activation names to their native forward functions.
56
-# Uses static methods or standalone functions to avoid instantiating CustomOp
57
-# classes, which would call get_current_vllm_config() before config is set.
58
-_CPU_MOE_ACT_FN: dict[MoEActivation, Callable[[torch.Tensor], torch.Tensor]] = {
59
- MoEActivation.SILU: SiluAndMul.forward_native,
60
- MoEActivation.SWIGLUOAI: _swigluoai_forward_native,
61
- MoEActivation.GELU: _gelu_and_mul,
62
- MoEActivation.GELU_TANH: (
63
- lambda x: F.gelu(x[..., : x.shape[-1] // 2], approximate="tanh")
64
- * x[..., x.shape[-1] // 2 :]
65
- ),
66
-}
67
-
68
-
69
-def grouped_topk(
70
- hidden_states: torch.Tensor,
71
- gating_output: torch.Tensor,
72
- topk: int,
73
- renormalize: bool,
74
- num_expert_group: int = 0,
75
- topk_group: int = 0,
76
- scoring_func: str = "softmax",
77
- routed_scaling_factor: float = 1.0,
78
- e_score_correction_bias: torch.Tensor | None = None,
79
-) -> tuple[torch.Tensor, torch.Tensor]:
80
- assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch"
81
-
82
- gating_output = gating_output.float()
83
- if scoring_func == "softmax":
84
- scores = torch.softmax(gating_output, dim=-1)
85
- elif scoring_func == "sigmoid":
86
- scores = gating_output.sigmoid()
87
- else:
88
- raise ValueError(f"Unsupported scoring function: {scoring_func}")
89
-
90
- num_token = scores.shape[0]
91
- if e_score_correction_bias is not None:
92
- original_scores = scores
93
- scores = scores + e_score_correction_bias.unsqueeze(0)
94
- group_scores = (
95
- scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
96
- )
97
- else:
98
- group_scores = (
99
- scores.view(num_token, num_expert_group, -1).max(dim=-1).values
100
- ) # [n, n_group]
101
- group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
102
- 1
103
- ] # [n, top_k_group]
104
- group_mask = torch.zeros_like(group_scores) # [n, n_group]
105
- group_mask.scatter_(1, group_idx, 1) # [n, n_group]
106
- score_mask = (
107
- group_mask.unsqueeze(-1)
108
- .expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
109
- .reshape(num_token, -1)
110
- ) # [n, e]
111
- tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e]
112
-
113
- if e_score_correction_bias is not None:
114
- topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)[1]
115
- topk_weights = original_scores.gather(1, topk_ids)
116
- else:
117
- topk_weights, topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)
118
-
119
- if renormalize:
120
- topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
121
-
122
- if routed_scaling_factor != 1.0:
123
- topk_weights = topk_weights * routed_scaling_factor
124
- return topk_weights, topk_ids.to(torch.int32)
125
-
126
-
127
-def select_experts(
128
- hidden_states: torch.Tensor,
129
- router_logits: torch.Tensor,
130
- top_k: int,
131
- use_grouped_topk: bool,
132
- renormalize: bool,
133
- topk_group: int | None = None,
134
- num_expert_group: int | None = None,
135
- custom_routing_function: Callable | None = None,
136
- scoring_func: str = "softmax",
137
- routed_scaling_factor: float = 1.0,
138
- e_score_correction_bias: torch.Tensor | None = None,
139
-) -> tuple[torch.Tensor, torch.Tensor]:
140
- if use_grouped_topk:
141
- assert topk_group is not None
142
- assert num_expert_group is not None
143
- return grouped_topk(
144
- hidden_states=hidden_states,
145
- gating_output=router_logits,
146
- topk=top_k,
147
- renormalize=renormalize,
148
- num_expert_group=num_expert_group,
149
- topk_group=topk_group,
150
- scoring_func=scoring_func,
151
- routed_scaling_factor=routed_scaling_factor,
152
- e_score_correction_bias=e_score_correction_bias,
153
- )
154
- elif custom_routing_function is None:
155
- assert scoring_func == "softmax"
156
- topk_logit_vals, topk_idx = torch.topk(
157
- router_logits, k=top_k, dim=-1, sorted=False
158
- )
159
- if renormalize:
160
- topk_vals = torch.softmax(topk_logit_vals, dim=-1)
161
- else:
162
- logZ = torch.logsumexp(router_logits, dim=-1, keepdim=True)
163
- topk_vals = (topk_logit_vals - logZ).exp()
164
- return topk_vals.to(torch.float32), topk_idx.to(torch.int32)
165
- else:
166
- return custom_routing_function(
167
- hidden_states=hidden_states,
168
- gating_output=router_logits,
169
- topk=top_k,
170
- renormalize=renormalize,
171
- )
172
-
173
-
174
-class SGLFusedMOE:
175
- def __init__(self, layer: torch.nn.Module) -> None:
176
- pass
177
-
178
- def __call__(
179
- self,
180
- layer: torch.nn.Module,
181
- x: torch.Tensor,
182
- use_grouped_topk: bool,
183
- top_k: int,
184
- router_logits: torch.Tensor,
185
- renormalize: bool,
186
- topk_group: int | None = None,
187
- num_expert_group: int | None = None,
188
- global_num_experts: int = -1,
189
- expert_map: torch.Tensor | None = None,
190
- custom_routing_function: Callable | None = None,
191
- scoring_func: str = "softmax",
192
- routed_scaling_factor: float = 1.0,
193
- e_score_correction_bias: torch.Tensor | None = None,
194
- apply_router_weight_on_input: bool = False,
195
- activation: MoEActivation = MoEActivation.SILU,
196
- ) -> torch.Tensor:
197
- assert activation == MoEActivation.SILU, f"{activation} is not supported."
198
- assert not apply_router_weight_on_input
199
- topk_weights, topk_ids = select_experts(
200
- hidden_states=x,
201
- router_logits=router_logits,
202
- use_grouped_topk=use_grouped_topk,
203
- top_k=top_k,
204
- renormalize=renormalize,
205
- topk_group=topk_group,
206
- num_expert_group=num_expert_group,
207
- custom_routing_function=custom_routing_function,
208
- scoring_func=scoring_func,
209
- routed_scaling_factor=routed_scaling_factor,
210
- e_score_correction_bias=e_score_correction_bias,
211
- )
212
-
213
- return fused_experts_cpu(
214
- x,
215
- layer.w13_weight,
216
- layer.w2_weight,
217
- topk_weights,
218
- topk_ids,
219
- False, # inplace
220
- CPUQuantMethod.UNQUANT, # moe_comp_method
221
- None, # w1_scale
222
- None, # w2_scale
223
- None, # w1_zero
224
- None, # w2_zero
225
- None, # block_size
226
- None, # w1_bias
227
- None, # w2_bias
228
- None, # alpha
229
- None, # limit
230
- True, # is_vnni
231
- )
232
-
233
-
234
-class CPUFusedMOE:
235
- """CPU-based fused MoE implementation."""
236
-
237
- def __init__(self, layer: torch.nn.Module) -> None:
238
- self._pad_moe_intermediate_for_grouped_gemm(layer)
239
- use_grouped_gemm, isa = self.check_grouped_gemm(layer)
240
- self.isa = isa
241
- if use_grouped_gemm:
242
- self.forward_method = self.forward_grouped_gemm
243
- self.init_moe_grouped_gemm(layer=layer)
244
- else:
245
- self.forward_method = self.forward_torch
246
- self.init_moe_torch(layer=layer)
247
-
248
- def __call__(
249
- self,
250
- layer: torch.nn.Module,
251
- x: torch.Tensor,
252
- use_grouped_topk: bool,
253
- top_k: int,
254
- router_logits: torch.Tensor,
255
- renormalize: bool,
256
- topk_group: int | None = None,
257
- num_expert_group: int | None = None,
258
- global_num_experts: int = -1,
259
- expert_map: torch.Tensor | None = None,
260
- custom_routing_function: Callable | None = None,
261
- scoring_func: str = "softmax",
262
- routed_scaling_factor: float = 1.0,
263
- e_score_correction_bias: torch.Tensor | None = None,
264
- apply_router_weight_on_input: bool = False,
265
- activation: MoEActivation = MoEActivation.SILU,
266
- ) -> torch.Tensor:
267
- assert activation in _CPU_MOE_ACT_FN, f"{activation} is not supported."
268
-
269
- topk_weights, topk_ids = select_experts(
270
- hidden_states=x,
271
- router_logits=router_logits,
272
- use_grouped_topk=use_grouped_topk,
273
- top_k=top_k,
274
- renormalize=renormalize,
275
- topk_group=topk_group,
276
- num_expert_group=num_expert_group,
277
- custom_routing_function=custom_routing_function,
278
- scoring_func=scoring_func,
279
- routed_scaling_factor=routed_scaling_factor,
280
- e_score_correction_bias=e_score_correction_bias,
281
- )
282
-
283
- return self.forward_method(
284
- layer,
285
- x,
286
- topk_weights,
287
- topk_ids,
288
- activation,
289
- global_num_experts,
290
- apply_router_weight_on_input,
291
- )
292
-
293
- def _pad_moe_intermediate_for_grouped_gemm(self, layer: torch.nn.Module) -> None:
294
- """Zero-pad the per-partition MoE intermediate dim up to a multiple
295
- of _MOE_GROUPED_GEMM_N_TILE, so the AMX/vector grouped-gemm kernels
296
- can be used even when TP sharding (moe_intermediate_size // tp_size)
297
- lands on an unaligned value (e.g. moe_intermediate_size=704 at tp=4
298
- -> 176). Only applies to the x86 (AMX/vec) kernels and half-split
299
- gate/up activations; interleaved layouts (swigluoai) are left
300
- untouched.
301
- """
302
- if not hasattr(torch.ops._C, "prepack_moe_weight"):
303
- return
304
- if current_platform.get_cpu_architecture() == CpuArchEnum.ARM:
305
- return
306
-
307
- intermediate_size = layer.w2_weight.size(2)
308
- remainder = intermediate_size % _MOE_GROUPED_GEMM_N_TILE
309
- if remainder == 0:
310
- return
311
- if layer.activation == MoEActivation.SWIGLUOAI:
312
- return
313
-
314
- pad = _MOE_GROUPED_GEMM_N_TILE - remainder
315
- padded_size = intermediate_size + pad
316
- num_experts, _, hidden_size = layer.w13_weight.shape
317
-
318
- new_w13 = layer.w13_weight.new_zeros(num_experts, 2 * padded_size, hidden_size)
319
- new_w13[:, :intermediate_size] = layer.w13_weight[:, :intermediate_size]
320
- new_w13[:, padded_size : padded_size + intermediate_size] = layer.w13_weight[
321
- :, intermediate_size:
322
- ]
323
- replace_parameter(layer, "w13_weight", new_w13)
324
-
325
- new_w2 = layer.w2_weight.new_zeros(num_experts, hidden_size, padded_size)
326
- new_w2[:, :, :intermediate_size] = layer.w2_weight
327
- replace_parameter(layer, "w2_weight", new_w2)
328
-
329
- if hasattr(layer, "w13_bias"):
330
- new_bias = layer.w13_bias.new_zeros(num_experts, 2 * padded_size)
331
- new_bias[:, :intermediate_size] = layer.w13_bias[:, :intermediate_size]
332
- new_bias[:, padded_size : padded_size + intermediate_size] = layer.w13_bias[
333
- :, intermediate_size:
334
- ]
335
- replace_parameter(layer, "w13_bias", new_bias)
336
-
337
- def _grouped_gemm_alignment_error(self, layer: torch.nn.Module) -> str:
338
- # w2's input size is the per-partition MoE intermediate size (the
339
- # dimension TP-sharding splits), and it's what most commonly breaks
340
- # alignment, e.g. moe_intermediate_size=704 at tp=4 gives
341
- # 704 // 4 == 176, which isn't a multiple of 32.
342
- intermediate_size_per_partition = layer.w2_weight.size(2)
343
- return (
344
- "CPU fused-MoE AMX grouped-gemm kernel cannot be used for a "
345
- f"layer with w13 shape {tuple(layer.w13_weight.shape)} / w2 "
346
- f"shape {tuple(layer.w2_weight.shape)}: the per-partition MoE "
347
- f"intermediate size ({intermediate_size_per_partition}) is not "
348
- f"a multiple of {_MOE_GROUPED_GEMM_N_TILE}, and automatic "
349
- "zero-padding could not resolve this (typically because the "
350
- "activation uses an interleaved gate/up layout, e.g. "
351
- "swigluoai). vLLM refuses to silently fall back to the much "
352
- "slower per-expert torch loop on AMX-capable CPUs; consider a "
353
- "different --tensor-parallel-size."
354
- )
355
-
356
- def check_grouped_gemm(
357
- self,
358
- layer: torch.nn.Module,
359
- ) -> tuple[bool, str]:
360
- if not hasattr(torch.ops._C, "prepack_moe_weight"):
361
- return False, "none"
362
-
363
- dtype = layer.w13_weight.dtype
364
- w13_input_size = layer.w13_weight.size(2)
365
- w13_output_size = layer.w13_weight.size(1)
366
- w2_input_size = layer.w2_weight.size(2)
367
- w2_output_size = layer.w2_weight.size(1)
368
-
369
- supports_amx = torch.cpu._is_amx_tile_supported()
370
- if supports_amx:
371
- if (
372
- dtype == torch.bfloat16
373
- and w13_output_size % 32 == 0
374
- and w2_output_size % 32 == 0
375
- and w13_input_size % 32 == 0
376
- and w2_input_size % 32 == 0
377
- ):
378
- return True, "amx"
379
- raise RuntimeError(self._grouped_gemm_alignment_error(layer))
380
-
381
- if not (w13_output_size % 32 == 0 and w2_output_size % 32 == 0):
382
- return False, "none"
383
-
384
- if (
385
- layer.activation == MoEActivation.SWIGLUOAI
386
- and w2_input_size % _MOE_GROUPED_GEMM_N_TILE != 0
387
- ):
388
- return False, "none"
389
-
390
- supports_neon = current_platform.get_cpu_architecture() == CpuArchEnum.ARM
391
- if supports_neon:
392
- if (
393
- dtype == torch.bfloat16
394
- and w13_input_size % 4 == 0
395
- and w2_input_size % 4 == 0
396
- ):
397
- return True, "neon"
398
- return False, "none"
399
-
400
- return True, "vec"
401
-
402
- def init_moe_grouped_gemm(
403
- self,
404
- layer: torch.nn.Module,
405
- ) -> None:
406
- new_w13 = cpu_prepack_moe_weight(layer.w13_weight, self.isa)
407
- replace_parameter(layer, "w13_weight", new_w13)
408
- new_w2 = cpu_prepack_moe_weight(layer.w2_weight, self.isa)
409
- replace_parameter(layer, "w2_weight", new_w2)
410
-
411
- def init_moe_torch(
412
- self,
413
- layer: torch.nn.Module,
414
- ) -> None:
415
- use_onednn_mm = ops._supports_onednn and ops.is_onednn_acl_supported()
416
- num_experts = layer.w13_weight.size(0)
417
- has_w13_bias = hasattr(layer, "w13_bias")
418
- has_w2_bias = hasattr(layer, "w2_bias")
419
-
420
- layer.gate_up_linear = []
421
- layer.down_linear = []
422
-
423
- for i in range(num_experts):
424
- layer_w13_weight = layer.w13_weight[i]
425
- layer_w13_bias = layer.w13_bias[i] if has_w13_bias else None
426
- layer_w2_weight = layer.w2_weight[i]
427
- layer_w2_bias = layer.w2_bias[i] if has_w2_bias else None
428
- if use_onednn_mm:
429
- gate_up_handle = ops.create_onednn_mm(layer_w13_weight.t(), 32)
430
- layer.gate_up_linear.append(
431
- lambda x, handle=gate_up_handle, bias=layer_w13_bias: ops.onednn_mm(
432
- handle, x, bias
433
- )
434
- )
435
- down_handle = ops.create_onednn_mm(layer_w2_weight.t(), 32)
436
- layer.down_linear.append(
437
- lambda x, handle=down_handle, bias=layer_w2_bias: ops.onednn_mm(
438
- handle, x, bias
439
- )
440
- )
441
- else:
442
- layer.gate_up_linear.append(
443
- lambda x, w=layer_w13_weight, b=layer_w13_bias: F.linear(x, w, b)
444
- )
445
- layer.down_linear.append(
446
- lambda x, w=layer_w2_weight, b=layer_w2_bias: F.linear(x, w, b)
447
- )
448
-
449
- if use_onednn_mm: # remove weight
450
- layer.w13_weight = torch.nn.Parameter(torch.empty(0), requires_grad=False)
451
- layer.w2_weight = torch.nn.Parameter(torch.empty(0), requires_grad=False)
452
-
453
- _CPU_MOE_LAYER_CACHE[id(layer)] = weakref.ref(layer)
454
-
455
- def forward_grouped_gemm(
456
- self,
457
- layer: torch.nn.Module,
458
- input: torch.Tensor,
459
- topk_weights: torch.Tensor,
460
- topk_ids: torch.Tensor,
461
- activation: MoEActivation,
462
- global_num_experts: int = -1,
463
- skip_weighted: bool = False,
464
- ) -> torch.Tensor:
465
- if skip_weighted:
466
- assert topk_ids.size(1) == 1, (
467
- "apply_router_weight_on_input is only implemented for topk=1"
468
- )
469
- input.mul_(topk_weights.to(input.dtype))
470
-
471
- output = cpu_fused_moe(
472
- input,
473
- layer.w13_weight,
474
- layer.w2_weight,
475
- getattr(layer, "w13_bias", None),
476
- getattr(layer, "w2_bias", None),
477
- topk_weights,
478
- topk_ids,
479
- activation.value,
480
- self.isa,
481
- skip_weighted,
482
- )
483
- return output
484
-
485
- def forward_torch(
486
- self,
487
- layer: torch.nn.Module,
488
- input: torch.Tensor,
489
- topk_weights: torch.Tensor,
490
- topk_ids: torch.Tensor,
491
- activation: MoEActivation,
492
- global_num_experts: int = -1,
493
- skip_weighted: bool = False,
494
- ) -> torch.Tensor:
495
- if skip_weighted:
496
- assert topk_ids.size(1) == 1, (
497
- "apply_router_weight_on_input is only implemented for topk=1"
498
- )
499
- input.mul_(topk_weights.to(input.dtype))
500
-
501
- output = torch.empty_like(input)
502
- layer_id = id(layer)
503
- torch.ops.vllm.cpu_fused_moe_torch(
504
- layer_id,
505
- output,
506
- input,
507
- topk_weights,
508
- topk_ids,
509
- activation.value,
510
- global_num_experts,
511
- skip_weighted,
512
- )
513
-
514
- return output
515
-
516
-
517
-def cpu_fused_moe_torch(
518
- layer_id: int,
519
- output: torch.Tensor,
520
- input: torch.Tensor,
521
- topk_weights: torch.Tensor,
522
- topk_ids: torch.Tensor,
523
- activation: str,
524
- global_num_experts: int = -1,
525
- skip_weighted: bool = False,
526
-) -> None:
527
- act = MoEActivation.from_str(activation)
528
- layer = _CPU_MOE_LAYER_CACHE[layer_id]()
529
-
530
- # Ref code from https://github.com/sgl-project/sglang/blob/716e682721397df103f347d22da8bd46c6016dab/python/sglang/srt/layers/moe/fused_moe_native.py#L53
531
- len_experts = global_num_experts
532
-
533
- cnts = topk_ids.new_zeros((topk_ids.shape[0], len_experts))
534
- cnts.scatter_(1, topk_ids.to(torch.int64), 1)
535
- tokens_per_expert = cnts.sum(dim=0)
536
- idxs = topk_ids.view(-1).argsort()
537
-
538
- sorted_tokens = input[idxs // topk_ids.shape[1]]
539
- tokens_per_expert = tokens_per_expert.cpu().numpy()
540
-
541
- outputs = []
542
- start_idx = 0
543
-
544
- for i, num_tokens in enumerate(tokens_per_expert):
545
- end_idx = start_idx + num_tokens
546
- if num_tokens == 0:
547
- continue
548
- tokens_for_this_expert = sorted_tokens[start_idx:end_idx]
549
-
550
- gate_up = layer.gate_up_linear[i](tokens_for_this_expert) # type: ignore
551
- gate_up = _CPU_MOE_ACT_FN[act](gate_up)
552
- expert_out = layer.down_linear[i](gate_up) # type: ignore
553
- outputs.append(expert_out)
554
- start_idx = end_idx
555
-
556
- outs = torch.cat(outputs, dim=0) if len(outputs) else sorted_tokens.new_empty(0)
557
- new_x = torch.empty_like(outs)
558
-
559
- new_x[idxs] = outs
560
- if skip_weighted:
561
- final_out = new_x
562
- else:
563
- final_out = (
564
- new_x.view(*topk_ids.shape, -1)
565
- .type(topk_weights.dtype)
566
- .mul_(topk_weights.unsqueeze(dim=-1))
567
- .sum(dim=1)
568
- .type(new_x.dtype)
569
- )
570
- output.copy_(final_out)
571
-
572
-
573
-direct_register_custom_op(
574
- op_name="cpu_fused_moe_torch",
575
- op_func=cpu_fused_moe_torch,
576
- mutates_args=["output"],
577
-)
vllm/model_executor/layers/fused_moe/experts/cpu_int4_moe.py
+1
-1
@@ -218,7 +218,7 @@ class CPUExpertsInt4(mk.FusedMoEExpertsMonolithic):
218
Returns:
219
Output tensor after MoE computation
220
"""
221
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
221
+ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
222
select_experts,
223
)
224
vllm/model_executor/layers/fused_moe/experts/cpu_moe.py
+440
-24
@@ -1,6 +1,11 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""CPU quantized fused MoE experts."""
3
+"""CPU fused MoE experts."""
4
+
5
+import math
6
+import sys
7
+from collections.abc import Callable
8
+from typing import cast
9
10
import torch
11
@@ -9,7 +14,9 @@ from vllm._custom_ops import (
14
CPUQuantAlgo,
15
CPUQuantMethod,
16
convert_weight_packed_scale_zp,
17
+ cpu_fused_moe,
18
cpu_fused_moe_int8,
19
+ cpu_prepack_moe_weight,
20
cpu_prepack_moe_weight_int8,
21
fused_experts_cpu,
22
)
@@ -29,7 +36,439 @@ from vllm.model_executor.layers.quantization.utils.quant_utils import (
36
kInt8StaticChannelSym,
37
kMxfp4Static,
38
)
39
+from vllm.model_executor.utils import replace_parameter
40
from vllm.platforms import CpuArchEnum, current_platform
41
+from vllm.utils.math_utils import round_up
42
+
43
+# ===========================================================================
44
+# Routing
45
+# ===========================================================================
46
+
47
+
48
+def grouped_topk(
49
+ hidden_states: torch.Tensor,
50
+ gating_output: torch.Tensor,
51
+ topk: int,
52
+ renormalize: bool,
53
+ num_expert_group: int = 0,
54
+ topk_group: int = 0,
55
+ scoring_func: str = "softmax",
56
+ routed_scaling_factor: float = 1.0,
57
+ e_score_correction_bias: torch.Tensor | None = None,
58
+) -> tuple[torch.Tensor, torch.Tensor]:
59
+ assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch"
60
+
61
+ gating_output = gating_output.float()
62
+ if scoring_func == "softmax":
63
+ scores = torch.softmax(gating_output, dim=-1)
64
+ elif scoring_func == "sigmoid":
65
+ scores = gating_output.sigmoid()
66
+ else:
67
+ raise ValueError(f"Unsupported scoring function: {scoring_func}")
68
+
69
+ num_token = scores.shape[0]
70
+ if e_score_correction_bias is not None:
71
+ original_scores = scores
72
+ scores = scores + e_score_correction_bias.unsqueeze(0)
73
+ group_scores = (
74
+ scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
75
+ )
76
+ else:
77
+ group_scores = (
78
+ scores.view(num_token, num_expert_group, -1).max(dim=-1).values
79
+ ) # [n, n_group]
80
+ group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
81
+ 1
82
+ ] # [n, top_k_group]
83
+ group_mask = torch.zeros_like(group_scores) # [n, n_group]
84
+ group_mask.scatter_(1, group_idx, 1) # [n, n_group]
85
+ score_mask = (
86
+ group_mask.unsqueeze(-1)
87
+ .expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
88
+ .reshape(num_token, -1)
89
+ ) # [n, e]
90
+ tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e]
91
+
92
+ if e_score_correction_bias is not None:
93
+ topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)[1]
94
+ topk_weights = original_scores.gather(1, topk_ids)
95
+ else:
96
+ topk_weights, topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)
97
+
98
+ if renormalize:
99
+ topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
100
+
101
+ if routed_scaling_factor != 1.0:
102
+ topk_weights = topk_weights * routed_scaling_factor
103
+ return topk_weights, topk_ids.to(torch.int32)
104
+
105
+
106
+def select_experts(
107
+ hidden_states: torch.Tensor,
108
+ router_logits: torch.Tensor,
109
+ top_k: int,
110
+ use_grouped_topk: bool,
111
+ renormalize: bool,
112
+ topk_group: int | None = None,
113
+ num_expert_group: int | None = None,
114
+ custom_routing_function: Callable | None = None,
115
+ scoring_func: str = "softmax",
116
+ routed_scaling_factor: float = 1.0,
117
+ e_score_correction_bias: torch.Tensor | None = None,
118
+) -> tuple[torch.Tensor, torch.Tensor]:
119
+ if use_grouped_topk:
120
+ assert topk_group is not None
121
+ assert num_expert_group is not None
122
+ return grouped_topk(
123
+ hidden_states=hidden_states,
124
+ gating_output=router_logits,
125
+ topk=top_k,
126
+ renormalize=renormalize,
127
+ num_expert_group=num_expert_group,
128
+ topk_group=topk_group,
129
+ scoring_func=scoring_func,
130
+ routed_scaling_factor=routed_scaling_factor,
131
+ e_score_correction_bias=e_score_correction_bias,
132
+ )
133
+ elif custom_routing_function is None:
134
+ assert scoring_func == "softmax"
135
+ topk_logit_vals, topk_idx = torch.topk(
136
+ router_logits, k=top_k, dim=-1, sorted=False
137
+ )
138
+ if renormalize:
139
+ topk_vals = torch.softmax(topk_logit_vals, dim=-1)
140
+ else:
141
+ logZ = torch.logsumexp(router_logits, dim=-1, keepdim=True)
142
+ topk_vals = (topk_logit_vals - logZ).exp()
143
+ return topk_vals.to(torch.float32), topk_idx.to(torch.int32)
144
+ else:
145
+ topk_weights, topk_ids = custom_routing_function(
146
+ hidden_states=hidden_states,
147
+ gating_output=router_logits,
148
+ topk=top_k,
149
+ renormalize=renormalize,
150
+ )
151
+ # cpu_fused_moe reads routing tensors as contiguous float32/int32
152
+ # buffers and does not account for tensor strides.
153
+ topk_weights = topk_weights.to(torch.float32).contiguous()
154
+ topk_ids = topk_ids.to(torch.int32).contiguous()
155
+ return topk_weights, topk_ids
156
+
157
+
158
+# ===========================================================================
159
+# Unquantized (BF16/FP16/FP32) MoE
160
+# ===========================================================================
161
+
162
+
163
+class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
164
+ """Portable vector grouped-gemm unquantized MoE experts."""
165
+
166
+ isa = "vec"
167
+ output_alignment = 32
168
+ reduction_alignment = 1
169
+
170
+ @classmethod
171
+ def _intermediate_alignment(cls) -> int:
172
+ return math.lcm(cls.output_alignment, cls.reduction_alignment)
173
+
174
+ @classmethod
175
+ def _padded_intermediate_size(cls, moe_config: FusedMoEConfig) -> int:
176
+ intermediate_size = moe_config.intermediate_size_per_partition
177
+ if moe_config.activation == MoEActivation.SWIGLUOAI:
178
+ return intermediate_size
179
+ return round_up(intermediate_size, cls._intermediate_alignment())
180
+
181
+ @classmethod
182
+ def _supports_grouped_gemm(
183
+ cls,
184
+ moe_config: FusedMoEConfig,
185
+ ) -> tuple[bool, str | None]:
186
+ intermediate_size = cls._padded_intermediate_size(moe_config)
187
+ if (
188
+ moe_config.hidden_dim % cls.output_alignment != 0
189
+ or intermediate_size % cls.output_alignment != 0
190
+ ):
191
+ return False, (
192
+ "kernel requires hidden and intermediate dimensions divisible by "
193
+ f"{cls.output_alignment}"
194
+ )
195
+ if (
196
+ moe_config.hidden_dim % cls.reduction_alignment != 0
197
+ or intermediate_size % cls.reduction_alignment != 0
198
+ ):
199
+ return False, (
200
+ "kernel requires reduction dimensions divisible by "
201
+ f"{cls.reduction_alignment}"
202
+ )
203
+ return True, None
204
+
205
+ def __init__(
206
+ self,
207
+ moe_config: FusedMoEConfig,
208
+ quant_config: FusedMoEQuantConfig,
209
+ ):
210
+ super().__init__(moe_config, quant_config)
211
+ # Router configuration that the monolithic apply() signature cannot
212
+ # carry. Captured off the layer in process_weights_after_loading.
213
+ self.use_grouped_topk = False
214
+ self.renormalize = False
215
+ self.scoring_func = "softmax"
216
+ self.custom_routing_function: Callable | None = None
217
+
218
+ @property
219
+ def expects_unquantized_inputs(self) -> bool:
220
+ return True
221
+
222
+ @staticmethod
223
+ def activation_format() -> mk.FusedMoEActivationFormat:
224
+ return mk.FusedMoEActivationFormat.Standard
225
+
226
+ @staticmethod
227
+ def _supports_current_device() -> bool:
228
+ return current_platform.is_cpu()
229
+
230
+ @staticmethod
231
+ def is_supported_config(
232
+ cls: type[mk.FusedMoEExperts],
233
+ moe_config: FusedMoEConfig,
234
+ weight_key: QuantKey | None,
235
+ activation_key: QuantKey | None,
236
+ activation_format: mk.FusedMoEActivationFormat,
237
+ ) -> tuple[bool, str | None]:
238
+ supported, reason = mk.FusedMoEExperts.is_supported_config(
239
+ cls, moe_config, weight_key, activation_key, activation_format
240
+ )
241
+ if not supported:
242
+ return supported, reason
243
+ cpu_cls = cast(type[CPUUnquantizedExperts], cls)
244
+ return cpu_cls._supports_grouped_gemm(moe_config)
245
+
246
+ @staticmethod
247
+ def _supports_no_act_and_mul() -> bool:
248
+ return False
249
+
250
+ @staticmethod
251
+ def _supports_activation(activation: MoEActivation) -> bool:
252
+ return activation in (
253
+ MoEActivation.SILU,
254
+ MoEActivation.SWIGLUOAI,
255
+ MoEActivation.GELU,
256
+ MoEActivation.GELU_TANH,
257
+ )
258
+
259
+ @staticmethod
260
+ def _supports_quant_scheme(
261
+ weight_key: QuantKey | None,
262
+ activation_key: QuantKey | None,
263
+ ) -> bool:
264
+ return (weight_key, activation_key) == (None, None)
265
+
266
+ @staticmethod
267
+ def _supports_parallel_config(
268
+ moe_parallel_config: FusedMoEParallelConfig,
269
+ ) -> bool:
270
+ return True
271
+
272
+ @staticmethod
273
+ def _supports_routing_method(
274
+ routing_method: RoutingMethodType,
275
+ weight_key: QuantKey | None,
276
+ activation_key: QuantKey | None,
277
+ ) -> bool:
278
+ # Routing runs in select_experts(), which covers every routing method
279
+ # a layer can be configured with, including custom routing functions.
280
+ return True
281
+
282
+ @staticmethod
283
+ def _supports_router_logits_dtype(
284
+ router_logits_dtype: torch.dtype | None,
285
+ routing_method: RoutingMethodType,
286
+ ) -> bool:
287
+ return True
288
+
289
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
290
+ self._pad_moe_intermediate(layer)
291
+ self.use_grouped_topk = layer.use_grouped_topk
292
+ self.renormalize = layer.renormalize
293
+ self.scoring_func = layer.scoring_func
294
+ self.custom_routing_function = layer.custom_routing_function
295
+ replace_parameter(
296
+ layer, "w13_weight", cpu_prepack_moe_weight(layer.w13_weight, self.isa)
297
+ )
298
+ replace_parameter(
299
+ layer, "w2_weight", cpu_prepack_moe_weight(layer.w2_weight, self.isa)
300
+ )
301
+
302
+ def _pad_moe_intermediate(self, layer: torch.nn.Module) -> None:
303
+ """Zero-pad the per-partition MoE intermediate dim of both weights and
304
+ the expert bias, see `_padded_intermediate_size`."""
305
+ intermediate_size = self.moe_config.intermediate_size_per_partition
306
+ padded_size = self._padded_intermediate_size(self.moe_config)
307
+ if padded_size == intermediate_size:
308
+ return
309
+
310
+ num_experts, _, hidden_size = layer.w13_weight.shape
311
+
312
+ new_w13 = layer.w13_weight.new_zeros(num_experts, 2 * padded_size, hidden_size)
313
+ new_w13[:, :intermediate_size] = layer.w13_weight[:, :intermediate_size]
314
+ new_w13[:, padded_size : padded_size + intermediate_size] = layer.w13_weight[
315
+ :, intermediate_size:
316
+ ]
317
+ replace_parameter(layer, "w13_weight", new_w13)
318
+
319
+ new_w2 = layer.w2_weight.new_zeros(num_experts, hidden_size, padded_size)
320
+ new_w2[:, :, :intermediate_size] = layer.w2_weight
321
+ replace_parameter(layer, "w2_weight", new_w2)
322
+
323
+ if hasattr(layer, "w13_bias"):
324
+ new_bias = layer.w13_bias.new_zeros(num_experts, 2 * padded_size)
325
+ new_bias[:, :intermediate_size] = layer.w13_bias[:, :intermediate_size]
326
+ new_bias[:, padded_size : padded_size + intermediate_size] = layer.w13_bias[
327
+ :, intermediate_size:
328
+ ]
329
+ # Assign through .data rather than replacing the Parameter: the
330
+ # quant config is built before this runs and holds a reference to
331
+ # this very object, which is what feeds self.w1_bias in apply().
332
+ layer.w13_bias.data = new_bias
333
+
334
+ def _select_experts(
335
+ self,
336
+ hidden_states: torch.Tensor,
337
+ router_logits: torch.Tensor,
338
+ num_expert_group: int | None,
339
+ topk_group: int | None,
340
+ e_score_correction_bias: torch.Tensor | None,
341
+ routed_scaling_factor: float | None,
342
+ ) -> tuple[torch.Tensor, torch.Tensor]:
343
+ return select_experts(
344
+ hidden_states=hidden_states,
345
+ router_logits=router_logits,
346
+ top_k=self.moe_config.experts_per_token,
347
+ use_grouped_topk=self.use_grouped_topk,
348
+ renormalize=self.renormalize,
349
+ topk_group=topk_group,
350
+ num_expert_group=num_expert_group,
351
+ custom_routing_function=self.custom_routing_function,
352
+ scoring_func=self.scoring_func,
353
+ routed_scaling_factor=(
354
+ routed_scaling_factor if routed_scaling_factor is not None else 1.0
355
+ ),
356
+ e_score_correction_bias=e_score_correction_bias,
357
+ )
358
+
359
+ def apply(
360
+ self,
361
+ hidden_states: torch.Tensor,
362
+ w1: torch.Tensor,
363
+ w2: torch.Tensor,
364
+ router_logits: torch.Tensor,
365
+ activation: MoEActivation,
366
+ global_num_experts: int,
367
+ expert_map: torch.Tensor | None,
368
+ a1q_scale: torch.Tensor | None,
369
+ apply_router_weight_on_input: bool,
370
+ # grouped topk + fused topk bias parameters
371
+ num_expert_group: int | None = None,
372
+ e_score_correction_bias: torch.Tensor | None = None,
373
+ routed_scaling_factor: float | None = None,
374
+ topk_group: int | None = None,
375
+ ) -> torch.Tensor:
376
+ topk_weights, topk_ids = self._select_experts(
377
+ hidden_states,
378
+ router_logits,
379
+ num_expert_group,
380
+ topk_group,
381
+ e_score_correction_bias,
382
+ routed_scaling_factor,
383
+ )
384
+
385
+ if apply_router_weight_on_input:
386
+ assert topk_ids.size(1) == 1, (
387
+ "apply_router_weight_on_input is only implemented for topk=1"
388
+ )
389
+ hidden_states.mul_(topk_weights.to(hidden_states.dtype))
390
+
391
+ return cpu_fused_moe(
392
+ hidden_states,
393
+ w1,
394
+ w2,
395
+ self.w1_bias,
396
+ self.w2_bias,
397
+ topk_weights,
398
+ topk_ids,
399
+ activation.value,
400
+ self.isa,
401
+ apply_router_weight_on_input,
402
+ )
403
+
404
+
405
+class X86CPUUnquantizedExperts(CPUUnquantizedExperts):
406
+ """x86 AMX grouped-gemm unquantized MoE experts."""
407
+
408
+ isa = "amx"
409
+ output_alignment = 32
410
+ reduction_alignment = 32
411
+
412
+ @staticmethod
413
+ def _supports_current_device() -> bool:
414
+ return (
415
+ current_platform.is_cpu()
416
+ and current_platform.get_cpu_architecture() == CpuArchEnum.X86
417
+ and torch.cpu._is_amx_tile_supported()
418
+ )
419
+
420
+ @staticmethod
421
+ def is_supported_config(
422
+ cls: type[mk.FusedMoEExperts],
423
+ moe_config: FusedMoEConfig,
424
+ weight_key: QuantKey | None,
425
+ activation_key: QuantKey | None,
426
+ activation_format: mk.FusedMoEActivationFormat,
427
+ ) -> tuple[bool, str | None]:
428
+ supported, reason = mk.FusedMoEExperts.is_supported_config(
429
+ cls, moe_config, weight_key, activation_key, activation_format
430
+ )
431
+ if not supported:
432
+ return supported, reason
433
+ if moe_config.in_dtype != torch.bfloat16:
434
+ return False, "kernel requires bfloat16 activations"
435
+ cpu_cls = cast(type[CPUUnquantizedExperts], cls)
436
+ return cpu_cls._supports_grouped_gemm(moe_config)
437
+
438
+
439
+class ArmCPUUnquantizedExperts(CPUUnquantizedExperts):
440
+ """Arm NEON grouped-gemm unquantized MoE experts."""
441
+
442
+ isa = "neon"
443
+ output_alignment = 32
444
+ reduction_alignment = 4
445
+
446
+ @staticmethod
447
+ def _supports_current_device() -> bool:
448
+ return (
449
+ current_platform.is_cpu()
450
+ and current_platform.get_cpu_architecture() == CpuArchEnum.ARM
451
+ and sys.platform != "darwin"
452
+ )
453
+
454
+ @staticmethod
455
+ def is_supported_config(
456
+ cls: type[mk.FusedMoEExperts],
457
+ moe_config: FusedMoEConfig,
458
+ weight_key: QuantKey | None,
459
+ activation_key: QuantKey | None,
460
+ activation_format: mk.FusedMoEActivationFormat,
461
+ ) -> tuple[bool, str | None]:
462
+ supported, reason = mk.FusedMoEExperts.is_supported_config(
463
+ cls, moe_config, weight_key, activation_key, activation_format
464
+ )
465
+ if not supported:
466
+ return supported, reason
467
+ if moe_config.in_dtype != torch.bfloat16:
468
+ return False, "kernel requires bfloat16 activations"
469
+ cpu_cls = cast(type[CPUUnquantizedExperts], cls)
470
+ return cpu_cls._supports_grouped_gemm(moe_config)
471
+
472
473
# ===========================================================================
474
# FP8 W8A16 MoE
@@ -131,10 +570,6 @@ class CPUExpertsFp8(mk.FusedMoEExpertsMonolithic):
570
routed_scaling_factor: float | None = None,
571
topk_group: int | None = None,
572
) -> torch.Tensor:
134
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
135
- select_experts,
136
- )
137
-
573
topk_weights, topk_ids = select_experts(
574
hidden_states=hidden_states,
575
router_logits=router_logits,
@@ -289,10 +724,6 @@ class CPUExpertsMxfp4(mk.FusedMoEExpertsMonolithic):
724
routed_scaling_factor: float | None = None,
725
topk_group: int | None = None,
726
) -> torch.Tensor:
292
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
293
- select_experts,
294
- )
295
-
727
topk_weights, topk_ids = select_experts(
728
hidden_states=hidden_states,
729
router_logits=router_logits,
@@ -505,10 +936,6 @@ class CPUExpertsInt4(mk.FusedMoEExpertsMonolithic):
936
"apply_router_weight_on_input=True. "
937
)
938
508
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
509
- select_experts,
510
- )
511
-
939
topk_weights, topk_ids = select_experts(
940
hidden_states=hidden_states,
941
router_logits=router_logits,
@@ -631,7 +1058,6 @@ class CPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
1058
1059
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
1060
"""VNNI-prepack INT8 MoE weights for CPU kernel."""
634
- from vllm.model_executor.utils import replace_parameter
1061
1062
w13 = torch.ops._C.convert_weight_packed(layer.w13_weight)
1063
w2 = torch.ops._C.convert_weight_packed(layer.w2_weight)
@@ -655,10 +1081,6 @@ class CPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
1081
routed_scaling_factor: float | None = None,
1082
topk_group: int | None = None,
1083
) -> torch.Tensor:
658
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
659
- select_experts,
660
- )
661
-
1084
topk_weights, topk_ids = select_experts(
1085
hidden_states=hidden_states,
1086
router_logits=router_logits,
@@ -796,8 +1218,6 @@ class ArmCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
1218
return True
1219
1220
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
799
- from vllm.model_executor.utils import replace_parameter
800
-
1221
w13 = cpu_prepack_moe_weight_int8(layer.w13_weight, "neon")
1222
w2 = cpu_prepack_moe_weight_int8(layer.w2_weight, "neon")
1223
replace_parameter(layer, "w13_weight", w13)
@@ -819,10 +1239,6 @@ class ArmCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
1239
routed_scaling_factor: float | None = None,
1240
topk_group: int | None = None,
1241
) -> torch.Tensor:
822
- from vllm.model_executor.layers.fused_moe.cpu_fused_moe import (
823
- select_experts,
824
- )
825
-
1242
topk_weights, topk_ids = select_experts(
1243
hidden_states=hidden_states,
1244
router_logits=router_logits,
vllm/model_executor/layers/fused_moe/oracle/unquantized.py
+14
-4
@@ -143,6 +143,20 @@ def backend_to_kernel_cls(
143
144
return [XPUExperts]
145
146
+ elif backend == UnquantizedMoeBackend.CPU:
147
+ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
148
+ ArmCPUUnquantizedExperts,
149
+ CPUUnquantizedExperts,
150
+ X86CPUUnquantizedExperts,
151
+ )
152
+
153
+ # Prefer architecture-specific kernels before the portable vector path.
154
+ return [
155
+ X86CPUUnquantizedExperts,
156
+ ArmCPUUnquantizedExperts,
157
+ CPUUnquantizedExperts,
158
+ ]
159
+
160
else:
161
raise ValueError(f"Unknown unquantized MoE backend: {backend.value}")
162
@@ -199,10 +213,6 @@ def select_unquantized_moe_backend(
213
Note: Shape-specific fallbacks may still occur at runtime.
214
"""
215
202
- if current_platform.is_cpu():
203
- # TODO: migrate to MK structure.
204
- return UnquantizedMoeBackend.CPU, None
205
-
216
if current_platform.is_tpu():
217
return UnquantizedMoeBackend.TPU, None
218
vllm/model_executor/layers/fused_moe/unquantized_fused_moe_method.py
+24
-79
@@ -1,7 +1,6 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
-from collections.abc import Callable
4
from typing import TYPE_CHECKING
5
6
import torch
@@ -33,7 +32,6 @@ from vllm.model_executor.layers.fused_moe.runner.shared_experts import (
32
)
33
from vllm.model_executor.utils import replace_parameter, set_weight_attrs
34
from vllm.platforms import current_platform
36
-from vllm.platforms.interface import CpuArchEnum
35
36
if TYPE_CHECKING:
37
from vllm.model_executor.layers.fused_moe.routed_experts import RoutedExperts
@@ -54,13 +52,6 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
52
moe_config=self.moe,
53
)
54
57
- @property
58
- def is_monolithic(self) -> bool:
59
- # Escape hatch for CPU, which stays on the old monolithic path.
60
- if self.unquantized_backend == UnquantizedMoeBackend.CPU:
61
- return True
62
- return super().is_monolithic
63
-
55
@property
56
def supports_eplb(self) -> bool:
57
return True
@@ -71,8 +62,7 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
62
):
63
raise ValueError(
64
f"{self.__class__.__name__} uses the new modular kernel initialization "
74
- "logic for all but the CPU backend. CPU backend is monolithic. "
75
- "So this function should not be called."
65
+ "logic. So this function should not be called."
66
)
67
68
def select_gemm_impl(
@@ -201,6 +191,14 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
191
routing_tables=layer._expert_routing_tables(),
192
)
193
194
+ if self.unquantized_backend == UnquantizedMoeBackend.CPU:
195
+ # The CPU experts need the layer itself for the setup that
196
+ # convert_to_unquantized_kernel_format cannot express, since
197
+ # it only sees the two weight tensors: padding and prepacking
198
+ # into the grouped-gemm layout (bias included), and capturing
199
+ # the router config that monolithic apply() cannot carry.
200
+ self.moe_kernel.fused_experts.process_weights_after_loading(layer)
201
+
202
def process_weights_after_loading(self, layer: "RoutedExperts") -> None:
203
super().process_weights_after_loading(layer)
204
@@ -221,38 +219,6 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
219
# OOT handles internally.
220
return
221
224
- elif self.unquantized_backend == UnquantizedMoeBackend.CPU:
225
- # CPU stays on the old path — no oracle, no moe_kernel.
226
- from vllm.model_executor.layers.fused_moe import cpu_fused_moe
227
-
228
- if current_platform.get_cpu_architecture() == CpuArchEnum.X86:
229
- from vllm.model_executor.layers.utils import check_cpu_sgl_kernel
230
-
231
- dtype_w13 = layer.w13_weight.dtype
232
- _, n_w13, k_w13 = layer.w13_weight.size()
233
- dtype_w2 = layer.w2_weight.dtype
234
- _, n_w2, k_w2 = layer.w2_weight.size()
235
- if (
236
- envs.VLLM_CPU_SGL_KERNEL
237
- and check_cpu_sgl_kernel(n_w13, k_w13, dtype_w13)
238
- and check_cpu_sgl_kernel(n_w2, k_w2, dtype_w2)
239
- ):
240
- packed_w13_weight = torch.ops._C.convert_weight_packed(
241
- layer.w13_weight
242
- )
243
- assert packed_w13_weight.size() == layer.w13_weight.size()
244
- layer.w13_weight.copy_(packed_w13_weight)
245
- del packed_w13_weight
246
- packed_w2_weight = torch.ops._C.convert_weight_packed(
247
- layer.w2_weight
248
- )
249
- assert packed_w2_weight.size() == layer.w2_weight.size()
250
- layer.w2_weight.copy_(packed_w2_weight)
251
- self.cpu_fused_moe: Callable = cpu_fused_moe.SGLFusedMOE(layer)
252
- else:
253
- self.cpu_fused_moe = cpu_fused_moe.CPUFusedMOE(layer)
254
- else:
255
- self.cpu_fused_moe = cpu_fused_moe.CPUFusedMOE(layer)
222
elif self.unquantized_backend == UnquantizedMoeBackend.XPU:
223
w13 = layer.w13_weight
224
w2 = layer.w2_weight
@@ -363,39 +329,18 @@ class UnquantizedFusedMoEMethod(FusedMoEMethodBase, CustomOp):
329
input_ids: torch.Tensor | None = None,
330
) -> torch.Tensor:
331
assert self.is_monolithic
366
- if self.unquantized_backend == UnquantizedMoeBackend.CPU:
367
- assert self.moe_kernel is None
368
- return self.cpu_fused_moe(
369
- layer,
370
- x,
371
- layer.use_grouped_topk,
372
- layer.top_k,
373
- router_logits,
374
- layer.renormalize,
375
- layer.topk_group,
376
- layer.num_expert_group,
377
- layer.global_num_experts,
378
- layer.expert_map,
379
- layer.custom_routing_function,
380
- layer.scoring_func,
381
- layer.routed_scaling_factor,
382
- layer.e_score_correction_bias,
383
- layer.apply_router_weight_on_input,
384
- layer.activation,
385
- )
386
- else:
387
- assert self.moe_kernel is not None
388
- return self.moe_kernel.apply_monolithic(
389
- x,
390
- layer.w13_weight,
391
- layer.w2_weight,
392
- router_logits,
393
- activation=layer.activation,
394
- global_num_experts=layer.global_num_experts,
395
- expert_map=layer.expert_map,
396
- apply_router_weight_on_input=layer.apply_router_weight_on_input,
397
- num_expert_group=layer.num_expert_group,
398
- topk_group=layer.topk_group,
399
- e_score_correction_bias=layer.e_score_correction_bias,
400
- routed_scaling_factor=layer.routed_scaling_factor,
401
- )
332
+ assert self.moe_kernel is not None
333
+ return self.moe_kernel.apply_monolithic(
334
+ x,
335
+ layer.w13_weight,
336
+ layer.w2_weight,
337
+ router_logits,
338
+ activation=layer.activation,
339
+ global_num_experts=layer.global_num_experts,
340
+ expert_map=layer.expert_map,
341
+ apply_router_weight_on_input=layer.apply_router_weight_on_input,
342
+ num_expert_group=layer.num_expert_group,
343
+ topk_group=layer.topk_group,
344
+ e_score_correction_bias=layer.e_score_correction_bias,
345
+ routed_scaling_factor=layer.routed_scaling_factor,
346
+ )