[CPU] Add CPU FP8 W8A8 linear/MoE support (#49942)
Signed-off-by: yuwenzho <yuwen.zhou@intel.com> Co-authored-by: Li, Jiang <jiang1.li@intel.com>
Yuwen Zhou committed
Sep 18, 2026 at 17:59 UTC
b34647906550f6e9dc65c55ecfb36b697d5e1f43
20 files changed
+2924
-46
cmake/cpu_extension.cmake
+5
@@ -488,6 +488,7 @@ set(VLLM_EXT_SRC
488
"csrc/moe/dynamic_4bit_int_moe_cpu.cpp"
489
"csrc/cpu/cpu_fused_moe.cpp"
490
"csrc/cpu/cpu_attn.cpp"
491
+ "csrc/cpu/cpu_isa.cpp"
492
"csrc/cpu/torch_bindings.cpp")
493
494
if (CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64" AND VLLM_RVV_VLEN AND
@@ -546,11 +547,13 @@ if (ENABLE_X86_ISA)
547
"csrc/cpu/sgl-kernels/gemm.cpp"
548
"csrc/cpu/sgl-kernels/gemm_int8.cpp"
549
"csrc/cpu/sgl-kernels/gemm_fp8.cpp"
550
+ "csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp"
551
"csrc/cpu/sgl-kernels/gemm_int4.cpp"
552
"csrc/cpu/sgl-kernels/moe.cpp"
553
"csrc/cpu/sgl-kernels/moe_int8.cpp"
554
"csrc/cpu/sgl-kernels/moe_int4.cpp"
555
"csrc/cpu/sgl-kernels/moe_fp8.cpp"
556
+ "csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp"
557
"csrc/cpu/sgl-kernels/bmm.cpp"
558
"csrc/cpu/sgl-kernels/decode.cpp"
559
"csrc/cpu/sgl-kernels/extend.cpp"
@@ -571,6 +574,7 @@ if (ENABLE_X86_ISA)
574
"csrc/cpu/utils.cpp"
575
"csrc/cpu/spec_decode_utils.cpp"
576
"csrc/cpu/cpu_attn.cpp"
577
+ "csrc/cpu/cpu_isa.cpp"
578
"csrc/cpu/dnnl_kernels.cpp"
579
"csrc/cpu/mamba_cpu.cpp"
580
"csrc/cpu/torch_bindings.cpp"
@@ -587,6 +591,7 @@ if (ENABLE_X86_ISA)
591
"csrc/cpu/utils.cpp"
592
"csrc/cpu/spec_decode_utils.cpp"
593
"csrc/cpu/cpu_attn.cpp"
594
+ "csrc/cpu/cpu_isa.cpp"
595
"csrc/cpu/mamba_cpu.cpp"
596
"csrc/cpu/dnnl_kernels.cpp"
597
"csrc/cpu/torch_bindings.cpp"
csrc/cpu/cpu_attn.cpp
+2
-11
@@ -1,16 +1,7 @@
1
#include "cpu_attn_dispatch_generated.h"
2
3
-#if defined(__x86_64__) || defined(_M_X64)
4
- #include <cpuid.h>
5
-// Runtime check for AMX-FP8 (Diamond Rapids): CPUID leaf 7, subleaf 1, EAX[21].
6
-static bool runtime_has_amx_fp8() {
7
- unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
8
- if (__get_cpuid_count(7, 1, &eax, &ebx, &ecx, &edx)) {
9
- return (eax >> 21) & 1u;
10
- }
11
- return false;
12
-}
13
-#endif // x86_64
3
+// Runtime check for AMX-FP8, implemented in cpu_isa.cpp.
4
+extern bool runtime_has_amx_fp8();
5
6
// Maps kv_cache_dtype string to Fp8KVCacheDataType enum.
7
// "auto" -> kAuto(0); "fp8"/"fp8_e4m3" -> kFp8E4M3; "fp8_e5m2" -> kFp8E5M2.
csrc/cpu/cpu_isa.cpp
new
+20
@@ -0,0 +1,20 @@
1
+// SPDX-License-Identifier: Apache-2.0
2
+// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
+
4
+// AMX-FP8 is reported at CPUID leaf 0x1E, subleaf 1, EAX[4].
5
+#if defined(__x86_64__) || defined(_M_X64)
6
+ #include <cpuid.h>
7
+
8
+bool runtime_has_amx_fp8() {
9
+ unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
10
+ if (__get_cpuid_max(0, nullptr) < 0x1e) return false;
11
+ if (__get_cpuid_count(0x1e, 1, &eax, &ebx, &ecx, &edx)) {
12
+ return (eax >> 4) & 1u;
13
+ }
14
+ return false;
15
+}
16
+
17
+bool cpu_has_amx_fp8() { return runtime_has_amx_fp8(); }
18
+#else
19
+bool cpu_has_amx_fp8() { return false; }
20
+#endif
csrc/cpu/sgl-kernels/common.h
+83
@@ -12,11 +12,45 @@
12
#include <ATen/ATen.h>
13
#include <ATen/Dispatch.h>
14
#include <ATen/Parallel.h>
15
+#include <cstring>
16
17
#if defined(_OPENMP)
18
#include <omp.h>
19
#endif
20
21
+// Adapted from sglang: dispatch utilities for FP8 W8A8 kernels
22
+#define AT_DISPATCH_OUT_TYPES(TYPE, NAME, ...) \
23
+ AT_DISPATCH_SWITCH( \
24
+ TYPE, \
25
+ NAME, \
26
+ AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Float, out_t, __VA_ARGS__) \
27
+ AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::BFloat16, out_t, __VA_ARGS__) \
28
+ AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Half, out_t, __VA_ARGS__))
29
+
30
+#define AT_DISPATCH_CASE_ENUM_NO_RETURN(VALUE, TYPE, HINT, ...) \
31
+ case VALUE: { \
32
+ constexpr TYPE HINT = VALUE; \
33
+ __VA_ARGS__; \
34
+ break; \
35
+ }
36
+
37
+#define AT_DISPATCH_BOOL_NO_RETURN(VALUE, NAME, HINT, ...) \
38
+ [&]() { \
39
+ switch (VALUE) { \
40
+ AT_DISPATCH_CASE_ENUM_NO_RETURN(true, bool, HINT, __VA_ARGS__) \
41
+ AT_DISPATCH_CASE_ENUM_NO_RETURN(false, bool, HINT, __VA_ARGS__) \
42
+ } \
43
+ }()
44
+
45
+#define AT_DISPATCH_QUANT_MODE_NO_RETURN(MODE, NAME, HINT, ...) \
46
+ [&]() { \
47
+ switch (MODE) { \
48
+ AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_TENSOR, int, HINT, __VA_ARGS__) \
49
+ AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_ROW, int, HINT, __VA_ARGS__) \
50
+ AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_GROUP, int, HINT, __VA_ARGS__) \
51
+ } \
52
+ }()
53
+
54
namespace {
55
56
// dispatch bool
@@ -472,4 +506,53 @@ inline T* conditional_data_ptr(const std::optional<at::Tensor>& opt) {
506
return opt.has_value() ? opt.value().data_ptr<T>() : nullptr;
507
}
508
509
+// Adapted from sglang: M blocking utilities for FP8 W8A8 kernels
510
+inline int64_t get_m_block(int64_t M) {
511
+ if (M <= 48) {
512
+ return M;
513
+ } else if (M < 64) {
514
+ return 32;
515
+ } else if (M < 96) {
516
+ return 64;
517
+ } else {
518
+ return 128;
519
+ }
520
+}
521
+
522
+inline std::tuple<bool, int64_t, int64_t, int64_t> get_m_blocking(int64_t M) {
523
+ bool parallel_on_M = M > 128;
524
+ int64_t block_m = get_m_block(M);
525
+ int64_t Mc = (M + block_m - 1) / block_m;
526
+ int64_t Mc_parallel = parallel_on_M ? Mc : 1;
527
+ return std::make_tuple(parallel_on_M, block_m, Mc, Mc_parallel);
528
+}
529
+
530
+#if defined(CPU_CAPABILITY_AVX512)
531
+template <typename T>
532
+void zero_buffer(T* data, int64_t size) {
533
+ const int32_t vec_size = at::vec::Vectorized<T>::size();
534
+ auto zero_vec = at::vec::Vectorized<T>(0);
535
+ int64_t d = 0;
536
+ for (; d < size - (size % vec_size); d += vec_size) {
537
+ zero_vec.store(data + d);
538
+ }
539
+ if (d < size) {
540
+ zero_vec.store(data + d, size - d);
541
+ }
542
+}
543
+#else
544
+template <typename T>
545
+void zero_buffer(T* data, int64_t size) {
546
+ memset(data, 0, sizeof(T) * size);
547
+}
548
+#endif
549
+
550
+#if defined(__x86_64__)
551
+bool avx10_2_available() {
552
+ // __builtin_cpu_supports returns the masked feature bit rather than 0/1.
553
+ static const bool available = __builtin_cpu_supports("avx10.2");
554
+ return available;
555
+}
556
+#endif
557
+
558
} // anonymous namespace
csrc/cpu/sgl-kernels/gemm.h
+53
-1
@@ -94,7 +94,7 @@ enum class CPUActMethod : int {
94
gelu_and_mul = 2,
95
};
96
97
-enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4 };
97
+enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4, FP8_W8A8 = 5 };
98
99
constexpr bool operator==(CPUQuantMethod a, int64_t b) {
100
return static_cast<int64_t>(a) == b;
@@ -395,3 +395,55 @@ void tinygemm_kernel(
395
bool brg,
396
int64_t block_size_K,
397
bool do_unpack = true);
398
+
399
+// FP8 W8A8 tinygemm kernel (defined in gemm_fp8_w8a8.cpp)
400
+void tinygemm_kernel(
401
+ float* C,
402
+ const at::Float8_e4m3fn* A,
403
+ const float* scales_a,
404
+ const at::Float8_e4m3fn* B,
405
+ const float* scales_b,
406
+ int64_t M,
407
+ int64_t K,
408
+ int64_t lda,
409
+ int64_t ldc,
410
+ int64_t ldsa,
411
+ float* ukernel_buf,
412
+ at::BFloat16* dqA_buf,
413
+ at::BFloat16* dqB_buf);
414
+
415
+// FP8 W8A8 quantize activation (defined in gemm_fp8_w8a8.cpp)
416
+std::tuple<at::Tensor, at::Tensor> quantize_fp8e4m3_vec(
417
+ const at::Tensor& t,
418
+ bool channelwise,
419
+ c10::optional<at::Tensor> scale_opt);
420
+
421
+// FP8 W8A8 fused MoE kernel (defined in moe_fp8_w8a8.cpp)
422
+template <typename scalar_t>
423
+void fused_experts_fp8_a8_kernel_impl(
424
+ scalar_t* __restrict__ output,
425
+ scalar_t* __restrict__ ic0,
426
+ scalar_t* __restrict__ ic1,
427
+ scalar_t* __restrict__ ic2,
428
+ at::Float8_e4m3fn* __restrict__ A_tmp,
429
+ scalar_t* __restrict__ B_tmp,
430
+ float* __restrict__ C_tmp,
431
+ float* __restrict__ Ukernel_tmp,
432
+ const at::Float8_e4m3fn* __restrict__ input,
433
+ const at::Float8_e4m3fn* __restrict__ packed_w1,
434
+ const at::Float8_e4m3fn* __restrict__ packed_w2,
435
+ const float* __restrict__ As,
436
+ const float* __restrict__ w1s,
437
+ const float* __restrict__ w2s,
438
+ int64_t block_size_N,
439
+ int64_t block_size_K,
440
+ const float* __restrict__ topk_weights,
441
+ const int32_t* __restrict__ sorted_ids,
442
+ const int32_t* __restrict__ expert_ids,
443
+ const int32_t* __restrict__ offsets,
444
+ int64_t M,
445
+ int64_t N,
446
+ int64_t K,
447
+ int64_t E,
448
+ int64_t topk,
449
+ int64_t num_tokens_post_pad);
csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp
new
+1093
@@ -0,0 +1,1093 @@
1
+// Adapted from
2
+// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
+
4
+// clang-format off
5
+
6
+#include <ATen/native/CPUBlas.h>
7
+#include <c10/util/Unroll.h>
8
+#include <torch/all.h>
9
+
10
+#include "common.h"
11
+#include "vec.h"
12
+
13
+// Runtime check for AMX-FP8, implemented in cpu_isa.cpp.
14
+extern bool cpu_has_amx_fp8();
15
+
16
+namespace {
17
+
18
+#define BLOCK_N 32
19
+
20
+#define PER_TENSOR 1
21
+#define PER_ROW 2
22
+#define PER_GROUP 3
23
+
24
+static bool cpublas_checked = false;
25
+static bool cpublas_can_pack = false;
26
+
27
+bool cpublas_could_pack() {
28
+ // the could_pack check requires AMX support implicitly
29
+ if (cpublas_checked) {
30
+ return cpublas_can_pack;
31
+ }
32
+ cpublas_can_pack = cpu_has_amx_fp8() && at::native::cpublas::could_pack(at::kFloat8_e4m3fn);
33
+ cpublas_checked = true;
34
+ return cpublas_can_pack;
35
+}
36
+
37
+#if defined(CPU_CAPABILITY_AVX512)
38
+static void cvt_f8e4m3_to_bf16(
39
+ const at::Float8_e4m3fn* __restrict__ in, at::BFloat16* out, int64_t rows, int64_t cols, int64_t stride) {
40
+ if (stride == cols) {
41
+ // A contiguous buffer
42
+ size_t len = rows * cols;
43
+ size_t i = 0;
44
+ for (; i < len; i += 32) {
45
+ __m256i fp8_vec = _mm256_loadu_si256((__m256i*)&in[i]);
46
+ __m512bh bf16_vec = cvt_e4m3_bf16_intrinsic_no_nan(fp8_vec);
47
+ _mm512_storeu_si512((__m512i*)(out + i), (__m512i)bf16_vec);
48
+ }
49
+ for (; i < len; ++i) {
50
+ out[i] = (at::BFloat16)in[i];
51
+ }
52
+ } else {
53
+ // Non-contiguous. Access each row with stride
54
+ TORCH_CHECK(stride > cols);
55
+ for (int r = 0; r < rows; ++r) {
56
+ size_t i = 0;
57
+ size_t vec_len = cols / 32 * 32;
58
+ for (; i < vec_len; i += 32) {
59
+ __m256i fp8_vec = _mm256_loadu_si256((__m256i*)&in[r * stride + i]);
60
+ __m512bh bf16_vec = cvt_e4m3_bf16_intrinsic_no_nan(fp8_vec);
61
+ _mm512_storeu_si512((__m512i*)(out + r * cols + i), (__m512i)bf16_vec);
62
+ }
63
+ for (; i < cols; ++i) {
64
+ out[r * cols + i] = (at::BFloat16)in[r * stride + i];
65
+ }
66
+ }
67
+ }
68
+}
69
+
70
+// accumulate and store result to buffer
71
+// if act/wei are per_group quantized, apply scales
72
+template <bool accum, int64_t N, int act_quant_mode, int wei_quant_mode>
73
+static void _accumulate_result(
74
+ float* __restrict__ output,
75
+ const float* __restrict__ input,
76
+ const float* __restrict__ scale_a,
77
+ const float* __restrict__ scale_b,
78
+ int M,
79
+ int ldi,
80
+ int ldo,
81
+ int ldsa = 1) {
82
+ float a_scale, b_scale;
83
+ __m512 va_scale;
84
+ __m512 vb_scale;
85
+ for (int m = 0; m < M; ++m) {
86
+ if constexpr (act_quant_mode == PER_GROUP) {
87
+ a_scale = *(scale_a + m * ldsa);
88
+ va_scale = _mm512_set1_ps(a_scale);
89
+ }
90
+ constexpr int N_UNROLL = N / 16;
91
+ c10::ForcedUnroll<N_UNROLL>{}([&](auto i) {
92
+ constexpr int n = i * 16;
93
+ __m512 vc_f = _mm512_loadu_ps(input + m * ldi + n);
94
+ if constexpr (act_quant_mode == PER_GROUP) {
95
+ vc_f = _mm512_mul_ps(vc_f, va_scale);
96
+ }
97
+ if constexpr (wei_quant_mode == PER_GROUP) {
98
+ vb_scale = _mm512_loadu_ps(scale_b + n);
99
+ vc_f = _mm512_mul_ps(vc_f, vb_scale);
100
+ }
101
+ if constexpr (accum) {
102
+ __m512 vo = _mm512_loadu_ps(output + m * ldo + n);
103
+ _mm512_storeu_ps(output + m * ldo + n, _mm512_add_ps(vo, vc_f));
104
+ } else {
105
+ _mm512_storeu_ps(output + m * ldo + n, vc_f);
106
+ }
107
+ });
108
+ constexpr int tail_start = N / 16 * 16;
109
+ for (int n = tail_start; n < N; ++n) {
110
+ float dq_val = input[m * ldi + n];
111
+ if constexpr (act_quant_mode == PER_GROUP) {
112
+ dq_val = dq_val * a_scale;
113
+ }
114
+ if constexpr (wei_quant_mode == PER_GROUP) {
115
+ b_scale = scale_b[n];
116
+ dq_val = dq_val * b_scale;
117
+ }
118
+ if constexpr (accum) {
119
+ output[m * ldo + n] += dq_val;
120
+ } else {
121
+ output[m * ldo + n] = dq_val;
122
+ }
123
+ }
124
+ }
125
+}
126
+
127
+// Store result to output buffer with dtype conversion
128
+// If act/wei are per_row or per_tensor quantized, apply scales
129
+// If bias is not null, add bias
130
+template <typename out_dtype, int64_t N, int act_quant_mode, int wei_quant_mode>
131
+inline void store_out(
132
+ const float* y_buf,
133
+ out_dtype* c_ptr,
134
+ int64_t M,
135
+ int64_t lda,
136
+ const float* scales_a,
137
+ const float* scales_b,
138
+ const float* bias) {
139
+ float a_scale = 1.0, b_scale = 1.0;
140
+ __m512 va_scale, vb_scale;
141
+ if constexpr (act_quant_mode == PER_TENSOR) {
142
+ a_scale = *scales_a;
143
+ }
144
+ if constexpr (wei_quant_mode == PER_TENSOR) {
145
+ b_scale = *scales_b;
146
+ vb_scale = _mm512_set1_ps(b_scale);
147
+ }
148
+ for (int i = 0; i < M; ++i) {
149
+ if constexpr (act_quant_mode == PER_ROW) {
150
+ a_scale = *(scales_a + i);
151
+ }
152
+ if constexpr (act_quant_mode != PER_GROUP) {
153
+ va_scale = _mm512_set1_ps(a_scale);
154
+ }
155
+ constexpr int N_UNROLL = N / 16;
156
+ c10::ForcedUnroll<N_UNROLL>{}([&](auto idx) {
157
+ constexpr int j = idx * 16;
158
+ __m512 y_vec = _mm512_loadu_ps(y_buf + i * N + j);
159
+ __m512 bias_vec = bias ? _mm512_loadu_ps(bias + j) : _mm512_setzero_ps();
160
+ if constexpr (act_quant_mode != PER_GROUP) {
161
+ y_vec = _mm512_mul_ps(y_vec, va_scale);
162
+ }
163
+ if constexpr (wei_quant_mode == PER_ROW) {
164
+ vb_scale = _mm512_loadu_ps(scales_b + j);
165
+ }
166
+ if constexpr (wei_quant_mode != PER_GROUP) {
167
+ y_vec = _mm512_mul_ps(y_vec, vb_scale);
168
+ }
169
+ y_vec = _mm512_add_ps(y_vec, bias_vec);
170
+ if constexpr (std::is_same<out_dtype, float>::value) {
171
+ _mm512_storeu_ps(c_ptr + i * lda + j, y_vec);
172
+ } else if constexpr (std::is_same<out_dtype, at::BFloat16>::value) {
173
+ __m256i y_bf16_vec = at::vec::cvtfp32_bf16(y_vec);
174
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_bf16_vec);
175
+ } else if constexpr (std::is_same<out_dtype, at::Half>::value) {
176
+ __m256i y_fp16_vec = at::vec::cvtfp32_fp16(y_vec);
177
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_fp16_vec);
178
+ } else {
179
+ TORCH_CHECK(false, "Unsupported output dtype");
180
+ }
181
+ });
182
+ constexpr int tail_start = N / 16 * 16;
183
+ for (int j = tail_start; j < N; ++j) {
184
+ if constexpr (wei_quant_mode == PER_ROW) {
185
+ b_scale = scales_b[j];
186
+ }
187
+ c_ptr[i * lda + j] = static_cast<out_dtype>(y_buf[i * N + j] * a_scale * b_scale);
188
+ }
189
+ } // for M
190
+}
191
+
192
+#else // no AVX512
193
+
194
+static void cvt_f8e4m3_to_bf16(
195
+ const at::Float8_e4m3fn* __restrict__ in, at::BFloat16* out, int64_t rows, int64_t cols, int64_t stride) {
196
+ for (int r = 0; r < rows; ++r) {
197
+ for (int c = 0; c < cols; ++c) {
198
+ out[r * cols + c] = (at::BFloat16)in[r * stride + c];
199
+ }
200
+ }
201
+}
202
+
203
+// Store result to output buffer with dtype conversion
204
+// If act/wei are per_row or per_tensor quantized, apply scales
205
+// If bias is not null, add bias
206
+template <typename out_dtype, int64_t N, int act_quant_mode, int wei_quant_mode>
207
+inline void store_out(
208
+ const float* y_buf,
209
+ out_dtype* c_ptr,
210
+ int64_t M,
211
+ int64_t lda,
212
+ const float* scales_a,
213
+ const float* scales_b,
214
+ const float* bias) {
215
+ float a_scale = 1.0, b_scale = 1.0;
216
+ if constexpr (act_quant_mode == PER_TENSOR) {
217
+ a_scale = *scales_a;
218
+ }
219
+ if constexpr (wei_quant_mode == PER_TENSOR) {
220
+ b_scale = *scales_b;
221
+ }
222
+ for (int i = 0; i < M; ++i) {
223
+ if constexpr (act_quant_mode == PER_ROW) {
224
+ a_scale = *(scales_a + i);
225
+ }
226
+ for (int j = 0; j < N; ++j) {
227
+ if constexpr (wei_quant_mode == PER_ROW) {
228
+ b_scale = scales_b[j];
229
+ }
230
+ c_ptr[i * lda + j] = static_cast<out_dtype>(y_buf[i * N + j] * a_scale * b_scale);
231
+ }
232
+ } // for M
233
+}
234
+
235
+#endif // CPU_CAPABILITY_AVX512
236
+
237
+template <bool cpublas_can_pack, int64_t N, int act_quant_mode, int wei_quant_mode>
238
+void _micro_gemm(
239
+ float* C,
240
+ const at::Float8_e4m3fn* A,
241
+ const float* scales_a,
242
+ const at::Float8_e4m3fn* B,
243
+ const float* scales_b,
244
+ int64_t M,
245
+ int64_t K,
246
+ int64_t lda,
247
+ int64_t ldc,
248
+ int64_t ldsa,
249
+ float* ukernel_buf,
250
+ at::BFloat16* dqA_buf,
251
+ at::BFloat16* dqB_buf) {
252
+ // If FP8 brgemm is not available, convert A/B to bf16 for computation
253
+ // Compute GEMM fp8 * fp8 -> fp32 (or bf16 * bf16 -> fp32)
254
+ // If per_group quant, apply scales. Otherwise, don't apply scales here
255
+ // Finally accumulate and store results
256
+#if defined(CPU_CAPABILITY_AVX512)
257
+ if constexpr (cpublas_can_pack) {
258
+#ifdef CPUBLAS_BRGEMM_F8F8F32
259
+ at::native::cpublas::brgemm(
260
+ M, N, K, lda /*lda*/, N /*ldb*/, N /*ldc*/, false /* add_C */, A, B, ukernel_buf, true /* is_vnni */);
261
+#else
262
+ cvt_f8e4m3_to_bf16(A, dqA_buf, M, K, lda);
263
+ cvt_f8e4m3_to_bf16(B, dqB_buf, K, N, N);
264
+ at::native::cpublas::brgemm(
265
+ M, N, K, K /*lda*/, N /*ldb*/, N /*ldc*/, false /* add_C */, dqA_buf, dqB_buf, ukernel_buf, true /* is_vnni */);
266
+#endif
267
+ _mm_prefetch(B + N * (K + 128), _MM_HINT_T0);
268
+ _mm_prefetch(A + K + 128, _MM_HINT_T0);
269
+ _accumulate_result<true, N, act_quant_mode, wei_quant_mode>(
270
+ C, ukernel_buf, scales_a, scales_b, M, N /*ldi*/, ldc, ldsa);
271
+ } else
272
+#endif
273
+ {
274
+ for (int64_t i = 0; i < M; ++i) {
275
+ for (int64_t j = 0; j < N; ++j) {
276
+ float sum = 0;
277
+ for (int64_t k = 0; k < K; ++k) {
278
+ sum += ((float)A[i * lda + k] * (float)B[k * N + j]);
279
+ }
280
+ if constexpr (act_quant_mode == PER_GROUP) {
281
+ sum *= scales_a[i * ldsa];
282
+ }
283
+ if constexpr (wei_quant_mode == PER_GROUP) {
284
+ sum *= scales_b[j];
285
+ }
286
+ C[i * ldc + j] += sum;
287
+ }
288
+ }
289
+ }
290
+}
291
+
292
+template <typename out_dtype, bool cpublas_can_pack, int act_quant_mode, int wei_quant_mode>
293
+void _float8_linear_impl(
294
+ const at::Tensor& input,
295
+ const at::Tensor& input_scales,
296
+ const at::Tensor& weight,
297
+ const at::Tensor& weight_scales,
298
+ const std::optional<at::Tensor>& bias,
299
+ at::Tensor& output) {
300
+ // input shape = [..., K]
301
+ // input is per token quantized
302
+ int64_t K = input.size(-1);
303
+ auto input_view = input.view({-1, K});
304
+ int64_t M = input_view.size(0);
305
+
306
+ // weight shape = [Nc, Kc, block_k, block_n]
307
+ // scales shape = [Nc, G, block_n]
308
+ int64_t Nc = weight.size(0);
309
+ int64_t Kc = wei_quant_mode != PER_GROUP ? 1 : weight.size(1);
310
+ int64_t block_k = wei_quant_mode != PER_GROUP ? weight.size(1) * weight.size(2) : weight.size(2);
311
+ constexpr int64_t block_n = BLOCK_N;
312
+ TORCH_CHECK(weight.size(3) == block_n, "Float8 linear: unexpected weight shape");
313
+ int64_t N = Nc * block_n;
314
+ TORCH_CHECK(K == Kc * block_k, "Float8 linear: weight and input shapes mismatch");
315
+ auto [parallel_on_M, block_m, Mc, Mc_parallel] = get_m_blocking(M);
316
+
317
+ // scales shape = [Nc, G, block_n]
318
+ int64_t num_groups = wei_quant_mode == PER_TENSOR ? 1 : weight_scales.size(1);
319
+ TORCH_CHECK(K % num_groups == 0, "K should be divisible by num_groups");
320
+ int64_t group_size = K / num_groups;
321
+ TORCH_CHECK(group_size % block_k == 0, "Float8 linear: group_size should be divisible by block_k");
322
+ int64_t block_per_group = group_size / block_k;
323
+ TORCH_CHECK(
324
+ input_scales.numel() == 1 || input_scales.numel() == M || input_scales.numel() == M * num_groups,
325
+ "Float8 linear: unexpected input scales shape");
326
+ auto ldsa = act_quant_mode == PER_TENSOR ? 0 : act_quant_mode == PER_ROW ? 1 : num_groups;
327
+
328
+ const at::Float8_e4m3fn* a_ptr = input_view.data_ptr<at::Float8_e4m3fn>();
329
+ const float* a_scales_ptr = input_scales.data_ptr<float>();
330
+ const at::Float8_e4m3fn* b_ptr = weight.data_ptr<at::Float8_e4m3fn>();
331
+ const float* b_scales_ptr = weight_scales.data_ptr<float>();
332
+ out_dtype* c_ptr = output.data_ptr<out_dtype>();
333
+ const float* bias_ptr = bias.has_value() ? bias.value().data_ptr<float>() : nullptr;
334
+
335
+ int64_t block_size = block_m * block_n;
336
+ int64_t num_thread = at::get_num_threads();
337
+ at::Tensor y_buffer = at::empty({num_thread, block_size}, output.options().dtype(at::kFloat));
338
+ // Create buffer for brgemm output and dqA/dqB (optional)
339
+#if defined(CPU_CAPABILITY_AVX512)
340
+ // buffer for brgemm output in float32
341
+ int64_t buffer_size = block_size * 2; // float32 = bfloat16 * 2
342
+#ifndef CPUBLAS_BRGEMM_F8F8F32
343
+ // buffers for dqA & dqB in bf16
344
+ buffer_size += (block_k * block_n + block_m * block_k);
345
+#endif
346
+ at::Tensor micro_gemm_buffer = at::empty({num_thread, buffer_size}, output.options().dtype(at::kBFloat16));
347
+#endif
348
+
349
+ parallel_2d(Mc, Nc, [&](int64_t mc0, int64_t mc1, int64_t nc0, int64_t nc1) {
350
+ int tid = get_thread_num();
351
+ float* y_buf = y_buffer.data_ptr<float>() + tid * block_size;
352
+ at::BFloat16 *dqA_buffer = nullptr, *dqB_buffer = nullptr;
353
+ float* ukernel_buf = nullptr;
354
+#if defined(CPU_CAPABILITY_AVX512)
355
+ at::BFloat16* micro_gemm_buf = micro_gemm_buffer.data_ptr<at::BFloat16>() + tid * buffer_size;
356
+ ukernel_buf = reinterpret_cast<float*>(micro_gemm_buf);
357
+#ifndef CPUBLAS_BRGEMM_F8F8F32
358
+ dqA_buffer = micro_gemm_buf;
359
+ dqB_buffer = micro_gemm_buf + block_m * block_k;
360
+ ukernel_buf = reinterpret_cast<float*>(micro_gemm_buf + block_m * block_k + block_k * block_n);
361
+#endif
362
+#endif
363
+
364
+ loop_2d<at::Float8_e4m3fn>(mc0, mc1, nc0, nc1, block_n * K, [&](int64_t mci, int64_t nc, int64_t) {
365
+ int64_t m_size = mci * block_m + block_m > M ? M - mci * block_m : block_m;
366
+ zero_buffer(y_buf, m_size * block_n);
367
+ for (int kci = 0; kci < Kc; ++kci) {
368
+ auto scales_a = a_scales_ptr + mci * block_m * num_groups + kci / block_per_group;
369
+ auto scales_b = b_scales_ptr + nc * block_n * num_groups + kci / block_per_group * block_n;
370
+ _micro_gemm<cpublas_can_pack, block_n, act_quant_mode, wei_quant_mode>(
371
+ /* C */ y_buf,
372
+ /* A */ a_ptr + mci * block_m * K + kci * block_k,
373
+ /* scales_a */ scales_a,
374
+ /* B */ b_ptr + (nc * Kc + kci) * block_n * block_k,
375
+ /* scales_b */ scales_b,
376
+ /* M */ m_size,
377
+ /* K */ block_k,
378
+ /* lda */ K,
379
+ /* ldc */ block_n,
380
+ /* ldsa */ ldsa,
381
+ /* ukernel_buf */ ukernel_buf,
382
+ /* dqA_buf */ dqA_buffer,
383
+ /* dqB_buf */ dqB_buffer);
384
+ }
385
+ auto scales_a = act_quant_mode == PER_TENSOR ? a_scales_ptr
386
+ : act_quant_mode == PER_ROW ? a_scales_ptr + mci * block_m
387
+ : nullptr;
388
+ auto scales_b = wei_quant_mode == PER_TENSOR ? b_scales_ptr
389
+ : wei_quant_mode == PER_ROW ? b_scales_ptr + nc * block_n
390
+ : nullptr;
391
+ auto bias_data = bias_ptr ? bias_ptr + nc * block_n : nullptr;
392
+ store_out<out_dtype, block_n, act_quant_mode, wei_quant_mode>(
393
+ y_buf, c_ptr + mci * block_m * N + nc * block_n, m_size, N /*lda*/, scales_a, scales_b, bias_data);
394
+ });
395
+
396
+ if constexpr (cpublas_can_pack) {
397
+ at::native::cpublas::brgemm_release();
398
+ }
399
+ });
400
+}
401
+
402
+} // anonymous namespace
403
+
404
+void tinygemm_kernel(
405
+ float* C,
406
+ const at::Float8_e4m3fn* A,
407
+ const float* scales_a,
408
+ const at::Float8_e4m3fn* B,
409
+ const float* scales_b,
410
+ int64_t M,
411
+ int64_t K,
412
+ int64_t lda,
413
+ int64_t ldc,
414
+ int64_t ldsa,
415
+ float* ukernel_buf,
416
+ at::BFloat16* dqA_buf,
417
+ at::BFloat16* dqB_buf) {
418
+ // act_quant_mode = per row (2), wei_quant_mode = per group (3)
419
+ if (cpublas_could_pack()) {
420
+ _micro_gemm<true, BLOCK_N, 2, 3>(C, A, scales_a, B, scales_b, M, K, lda, ldc, ldsa, ukernel_buf, dqA_buf, dqB_buf);
421
+ } else {
422
+ _micro_gemm<false, BLOCK_N, 2, 3>(C, A, scales_a, B, scales_b, M, K, lda, ldc, ldsa, ukernel_buf, dqA_buf, dqB_buf);
423
+ }
424
+}
425
+
426
+#define INSTANTIATE_TINYGEMM_TEMPLATE() \
427
+ void tinygemm_kernel( \
428
+ float* C, \
429
+ const at::Float8_e4m3fn* A, \
430
+ const float* scales_a, \
431
+ const at::Float8_e4m3fn* B, \
432
+ const float* scales_b, \
433
+ int64_t M, \
434
+ int64_t K, \
435
+ int64_t lda, \
436
+ int64_t ldc, \
437
+ int64_t ldsa, \
438
+ float* ukernel_buf, \
439
+ at::BFloat16* dqA_buf, \
440
+ at::BFloat16* dqB_buf)
441
+
442
+INSTANTIATE_TINYGEMM_TEMPLATE();
443
+/*
444
+return: packed_weight, packed_scales
445
+*/
446
+std::tuple<at::Tensor, at::Tensor> float8_linear_prepack_impl(const at::Tensor& weight, const at::Tensor& scales) {
447
+ // weight shape = [N, K]
448
+ // scales shape = [N, G]
449
+ TORCH_CHECK(weight.dim() == 2, "Float8 linear CPU: Weight should be a 2D tensor for packing");
450
+ TORCH_CHECK(weight.size(1) % 2 == 0, "Float8 linear CPU: Weight should have even number of columns for packing");
451
+
452
+ auto new_scales = scales;
453
+ if (new_scales.dim() == 1) {
454
+ new_scales.unsqueeze_(1);
455
+ }
456
+ new_scales = new_scales.to(at::kFloat);
457
+ int N = weight.size(0);
458
+ int K = weight.size(1);
459
+ int G = scales.size(1);
460
+ int group_size = K / G;
461
+ int block_k = group_size > 128 ? 128 : group_size;
462
+ while (K % block_k != 0) {
463
+ block_k /= 2;
464
+ }
465
+ TORCH_CHECK(
466
+ block_k > 0 && block_k <= group_size, "Float8 linear CPU: Invalid block_k size, should be in (0, group_size]");
467
+ constexpr int block_n = BLOCK_N;
468
+ int Nc = N / block_n;
469
+ int Kc = K / block_k;
470
+
471
+ // Reorder weight to [N/block_n, K/block_k, block_k, block_n]
472
+ // Reorder scales to [N/block_n, G, block_n]
473
+ auto weight_view = weight.view({Nc, block_n, Kc, block_k});
474
+ at::Tensor weight_reordered = weight_view.permute({0, 2, 3, 1}).contiguous();
475
+ at::Tensor blocked_weight;
476
+ at::Tensor blocked_scales = new_scales.view({Nc, block_n, G}).permute({0, 2, 1}).contiguous();
477
+
478
+#if defined(CPU_CAPABILITY_AVX512)
479
+ if (cpublas_could_pack()) {
480
+#ifdef CPUBLAS_BRGEMM_F8F8F32
481
+ constexpr int vnni_size = 4; // for fp8
482
+#else
483
+ constexpr int vnni_size = 2; // for float16
484
+#endif
485
+ blocked_weight = at::empty({Nc, Kc, block_k, block_n}, weight.options());
486
+ auto weight_ptr = reinterpret_cast<uint8_t*>(weight_reordered.data_ptr());
487
+ auto blocked_weight_ptr = reinterpret_cast<uint8_t*>(blocked_weight.data_ptr());
488
+ int64_t num_blocks = Nc * Kc;
489
+ at::parallel_for(0, num_blocks, 1, [&](int64_t begin, int64_t end) {
490
+ for (const auto i : c10::irange(begin, end)) {
491
+ auto in_ptr = weight_ptr + i * block_k * block_n;
492
+ auto out_ptr = blocked_weight_ptr + i * block_k * block_n;
493
+
494
+ // Reorder weight block to VNNI
495
+ // plain shape = [block_k, block_n]
496
+ // packed shape = [block_k / VNNI_SIZE, block_n, VNNI_SIZE] viewed as [block_k, block_n]
497
+ constexpr int n_group_size = 8;
498
+ constexpr int n_group = block_n / n_group_size; // 4
499
+ for (int nb = 0; nb < n_group; ++nb) {
500
+ for (int k = 0; k < block_k; k += vnni_size) {
501
+ for (int ni = 0; ni < n_group_size; ++ni) {
502
+ for (int ki = 0; ki < vnni_size; ++ki) {
503
+ int src_idx = nb * n_group_size + ni + (k + ki) * block_n;
504
+ int dst_idx = (nb * n_group_size + ni) * vnni_size + k * block_n + ki;
505
+ *(out_ptr + dst_idx) = *(in_ptr + src_idx);
506
+ }
507
+ }
508
+ }
509
+ }
510
+ }
511
+ });
512
+ } else
513
+#endif
514
+ {
515
+ blocked_weight = weight_reordered;
516
+ }
517
+
518
+ return std::make_tuple(std::move(blocked_weight), std::move(blocked_scales));
519
+}
520
+
521
+// AVX512 optimized channel-wise max computation
522
+inline void
523
+compute_channel_max_avx512(const float* data, float* max_vals, int64_t num_channels, int64_t elements_per_channel) {
524
+ at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) {
525
+ for (int64_t c = start; c < end; ++c) {
526
+ const float* channel_data = data + c * elements_per_channel;
527
+ float max_val = 0.0f;
528
+
529
+ int64_t i = 0;
530
+ // Process 16 elements at a time
531
+ __m512 max_vec = _mm512_setzero_ps();
532
+
533
+ for (; i <= elements_per_channel - 16; i += 16) {
534
+ __m512 data_vec = _mm512_loadu_ps(&channel_data[i]);
535
+ __m512 abs_vec = _mm512_abs_ps(data_vec);
536
+ max_vec = _mm512_max_ps(max_vec, abs_vec);
537
+ }
538
+
539
+ // Horizontal max of the vector
540
+ float hmax = _mm512_reduce_max_ps(max_vec);
541
+ max_val = std::max(max_val, hmax);
542
+
543
+ // Handle remaining elements
544
+ for (; i < elements_per_channel; ++i) {
545
+ max_val = std::max(max_val, std::abs(channel_data[i]));
546
+ }
547
+
548
+ max_vals[c] = max_val;
549
+ }
550
+ });
551
+}
552
+
553
+// AVX512 optimized scaling and clamping for channel-wise quantization
554
+inline void scale_clamp_channelwise_avx512(
555
+ const float* src,
556
+ float* dst,
557
+ const float* scales,
558
+ int64_t num_channels,
559
+ int64_t elements_per_channel,
560
+ float quant_max,
561
+ float neg_quant_max) {
562
+ at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) {
563
+ for (int64_t c = start; c < end; ++c) {
564
+ float scale_val = scales[c];
565
+ float scale_reciprocal = 1.0f / scale_val;
566
+
567
+ const float* channel_src = src + c * elements_per_channel;
568
+ float* channel_dst = dst + c * elements_per_channel;
569
+
570
+ int64_t i = 0;
571
+ const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal);
572
+ const __m512 quant_max_vec = _mm512_set1_ps(quant_max);
573
+ const __m512 neg_quant_max_vec = _mm512_set1_ps(neg_quant_max);
574
+
575
+ for (; i <= elements_per_channel - 16; i += 16) {
576
+ __m512 src_vec = _mm512_loadu_ps(&channel_src[i]);
577
+ __m512 scaled_vec = _mm512_mul_ps(src_vec, scale_recip_vec);
578
+ __m512 clamped_vec = _mm512_min_ps(_mm512_max_ps(scaled_vec, neg_quant_max_vec), quant_max_vec);
579
+ _mm512_storeu_ps(&channel_dst[i], clamped_vec);
580
+ }
581
+
582
+ // Handle remaining elements
583
+ for (; i < elements_per_channel; ++i) {
584
+ float scaled = channel_src[i] * scale_reciprocal;
585
+ channel_dst[i] = std::clamp(scaled, neg_quant_max, quant_max);
586
+ }
587
+ }
588
+ });
589
+}
590
+
591
+// AVX512 optimized scaling and clamping for global quantization
592
+inline void scale_clamp_global_avx512(
593
+ const float* src, float* dst, int64_t size, float scale_reciprocal, float quant_max, float neg_quant_max) {
594
+ if (size <= 0) return;
595
+
596
+ const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal);
597
+ const __m512 quant_max_vec = _mm512_set1_ps(quant_max);
598
+ const __m512 neg_quant_max_vec = _mm512_set1_ps(neg_quant_max);
599
+
600
+ at::parallel_for(0, size, 4096, [&](int64_t start, int64_t end) {
601
+ int64_t block_size = end - start;
602
+ const float* block_src = src + start;
603
+ float* block_dst = dst + start;
604
+
605
+ int64_t i = 0;
606
+ for (; i <= block_size - 16; i += 16) {
607
+ __m512 src_vec = _mm512_loadu_ps(&block_src[i]);
608
+ __m512 scaled_vec = _mm512_mul_ps(src_vec, scale_recip_vec);
609
+ __m512 clamped_vec = _mm512_min_ps(_mm512_max_ps(scaled_vec, neg_quant_max_vec), quant_max_vec);
610
+ _mm512_storeu_ps(&block_dst[i], clamped_vec);
611
+ }
612
+
613
+ // Handle remaining elements in block
614
+ for (; i < block_size; ++i) {
615
+ float scaled = block_src[i] * scale_reciprocal;
616
+ block_dst[i] = std::clamp(scaled, neg_quant_max, quant_max);
617
+ }
618
+ });
619
+}
620
+
621
+std::tuple<at::Tensor, at::Tensor>
622
+_quantize_fp8e4m3(const at::Tensor& t, bool channelwise, c10::optional<at::Tensor> scale_opt = c10::nullopt) {
623
+ constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max
624
+ constexpr float eps = std::numeric_limits<float>::epsilon();
625
+
626
+ // Ensure input is contiguous and in float32
627
+ auto t_float = t.to(at::ScalarType::Float).contiguous();
628
+ at::Tensor qt;
629
+ at::Tensor scale_tensor;
630
+
631
+ if (channelwise) {
632
+ // Channel-wise quantization with AVX512 optimization
633
+ int64_t num_channels = t_float.size(0);
634
+ int64_t elements_per_channel = t_float.numel() / num_channels;
635
+
636
+ if (elements_per_channel * num_channels != t_float.numel()) {
637
+ throw std::runtime_error("Tensor must be divisible by number of channels for channel-wise quantization");
638
+ }
639
+
640
+ // Allocate scale tensor
641
+ scale_tensor = at::empty({num_channels}, t_float.options());
642
+ float* scale_data = scale_tensor.data_ptr<float>();
643
+
644
+ // Compute channel-wise max using AVX512
645
+ compute_channel_max_avx512(t_float.data_ptr<float>(), scale_data, num_channels, elements_per_channel);
646
+
647
+ // Apply quant_max and EPS
648
+ at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) {
649
+ for (int64_t c = start; c < end; ++c) {
650
+ scale_data[c] = std::max(scale_data[c] / quant_max, eps);
651
+ }
652
+ });
653
+
654
+ // Create output tensor for quantized values
655
+ qt = at::empty_like(t_float);
656
+
657
+ // Scale and clamp using AVX512
658
+ scale_clamp_channelwise_avx512(
659
+ t_float.data_ptr<float>(),
660
+ qt.data_ptr<float>(),
661
+ scale_data,
662
+ num_channels,
663
+ elements_per_channel,
664
+ quant_max,
665
+ -quant_max);
666
+ } else {
667
+ // Global quantization with AVX512 optimization
668
+ if (!scale_opt.has_value()) {
669
+ throw std::runtime_error("Scale must be provided for non-channelwise quantization in AVX512 version");
670
+ }
671
+
672
+ scale_tensor = scale_opt.value().to(at::ScalarType::Float).contiguous();
673
+
674
+ // Handle scalar scale case
675
+ float scale_val;
676
+ if (scale_tensor.numel() == 1) {
677
+ scale_val = std::max(scale_tensor.item<float>(), eps);
678
+ } else {
679
+ // Compute max of scale tensor if it's not scalar
680
+ float* scale_data = scale_tensor.data_ptr<float>();
681
+ __m512 max_vec = _mm512_set1_ps(eps);
682
+
683
+ int64_t i = 0;
684
+ for (; i <= scale_tensor.numel() - 16; i += 16) {
685
+ __m512 scale_vec = _mm512_loadu_ps(&scale_data[i]);
686
+ max_vec = _mm512_max_ps(max_vec, scale_vec);
687
+ }
688
+
689
+ float hmax = _mm512_reduce_max_ps(max_vec);
690
+ for (; i < scale_tensor.numel(); ++i) {
691
+ hmax = std::max(hmax, scale_data[i]);
692
+ }
693
+ scale_val = std::max(hmax, eps);
694
+ }
695
+
696
+ scale_tensor = at::tensor({scale_val}, scale_tensor.options());
697
+ float scale_reciprocal = 1.0f / scale_val;
698
+
699
+ // Create output tensor
700
+ qt = at::empty_like(t_float);
701
+
702
+ // Scale and clamp using AVX512
703
+ scale_clamp_global_avx512(
704
+ t_float.data_ptr<float>(), qt.data_ptr<float>(), t_float.numel(), scale_reciprocal, quant_max, -quant_max);
705
+ }
706
+
707
+ // Final conversion to FP8 E4M3FN using ATen's native conversion
708
+ qt = at::_to_copy(qt, at::ScalarType::Float8_e4m3fn);
709
+
710
+ return std::make_tuple(qt, scale_tensor);
711
+}
712
+
713
+inline __m128i cvtfp32_fp8e4m3(__m512& src) {
714
+ // cvt 16x32 from fp32 to fp8 e4m3
715
+ const __m512i sign_mask = _mm512_set1_epi32(0x80000000);
716
+ const __m512i fp8_max = _mm512_set1_epi32(UINT32_C(1087) << 20);
717
+ const __m512i denorm_thresh = _mm512_set1_epi32(UINT32_C(121) << 23);
718
+ const __m512i denorm_mask = _mm512_set1_epi32(UINT32_C(141) << 23);
719
+ const __m512i bias_part1 = _mm512_set1_epi32((uint32_t)(7 - 127) << 23);
720
+ const __m512i rounding_bias = _mm512_set1_epi32(0x7FFFF);
721
+ __m512i f_bits = _mm512_castps_si512(src);
722
+ // Extract and save sign
723
+ __m512i sign = _mm512_and_epi32(f_bits, sign_mask);
724
+ f_bits = _mm512_xor_epi32(f_bits, sign);
725
+
726
+ // Prepare result containers
727
+ __m512i result = _mm512_setzero_si512();
728
+
729
+ // Step 1: Handle case of overflow
730
+ // (f_bits >= fp8_max): set result = 0x7f
731
+ __mmask16 overflow_mask = _mm512_cmpge_epu32_mask(f_bits, fp8_max);
732
+ if (overflow_mask) {
733
+ result = _mm512_mask_set1_epi32(result, overflow_mask, 0x7f);
734
+ }
735
+
736
+ // Step 2: Handle small numbers (denormals)
737
+ // Small numbers (f_bits < denorm_thresh)
738
+ __mmask16 denorm_thresh_mask = _mm512_cmplt_epu32_mask(f_bits, denorm_thresh);
739
+
740
+ if (denorm_thresh_mask) {
741
+ __m512 small_input = _mm512_castsi512_ps(f_bits);
742
+ __m512 small_denorm = _mm512_add_ps(small_input, _mm512_castsi512_ps(denorm_mask));
743
+ __m512i small_denorm_bits = _mm512_castps_si512(small_denorm);
744
+ __m512i small_result = _mm512_sub_epi32(small_denorm_bits, denorm_mask);
745
+ result = _mm512_mask_mov_epi32(result, denorm_thresh_mask, small_result);
746
+ }
747
+
748
+ // Step 3: Handle normal numbers
749
+ __mmask16 normal_mask = ~(overflow_mask | denorm_thresh_mask);
750
+
751
+ if (normal_mask) {
752
+ // mant_odd = (f_bits >> 20) & 1
753
+ __m512i mant_odd = _mm512_and_epi32(_mm512_srli_epi32(f_bits, 20), _mm512_set1_epi32(1));
754
+ // f_bits += bias_part1 + rounding_bias
755
+ __m512i rounded = _mm512_add_epi32(f_bits, bias_part1);
756
+ rounded = _mm512_add_epi32(rounded, rounding_bias);
757
+ // Add mant_odd
758
+ rounded = _mm512_add_epi32(rounded, mant_odd);
759
+ // Shift right by 20 bits
760
+ __m512i normal_result = _mm512_srli_epi32(rounded, 20);
761
+ result = _mm512_mask_mov_epi32(result, normal_mask, normal_result);
762
+ }
763
+
764
+ // Merge back the sign
765
+ __m512i sign_shifted = _mm512_srli_epi32(sign, 24);
766
+ result = _mm512_or_epi32(result, sign_shifted);
767
+
768
+ // Now result is 16 x 32-bit integers, but we only need 8-bit for each
769
+ __m512i packed = _mm512_and_si512(result, _mm512_set1_epi32(0xFF));
770
+
771
+ // Narrow 32-bit integers to 8-bit
772
+ return _mm512_cvtepi32_epi8(packed);
773
+}
774
+
775
+__attribute__((target("avx10.2"))) inline __m128i cvtfp32_fp8e4m3_avx10_2(__m512& src) {
776
+ __m256i f16_vec = _mm512_cvt_roundps_ph(src, _MM_FROUND_TO_NEAREST_INT | _MM_FROUND_NO_EXC);
777
+ return _mm256_cvtph_hf8(_mm256_castsi256_ph(f16_vec));
778
+}
779
+
780
+// Carries the whole loop rather than just the conversion, as GCC cannot inline an avx10.2
781
+// function into a caller lacking that target, so branching per vector is costly.
782
+__attribute__((target("avx10.2"))) void quantize_bf16_block_avx10_2(
783
+ const at::BFloat16* __restrict__ src, at::Float8_e4m3fn* __restrict__ dst, int64_t count, float scale_reciprocal) {
784
+ constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max
785
+ const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal);
786
+ const __m512 quant_max_vec = _mm512_set1_ps(quant_max);
787
+ const __m512 neg_quant_max_vec = _mm512_set1_ps(-quant_max);
788
+
789
+ for (int64_t i = 0; i <= count - 32; i += 32) {
790
+ __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&src[i]));
791
+ __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&src[i + 16]));
792
+
793
+ __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec1), 16));
794
+ __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec2), 16));
795
+
796
+ __m512 scaled_vec1 = _mm512_mul_ps(fp32_vec1, scale_recip_vec);
797
+ __m512 clamped_vec1 = _mm512_min_ps(_mm512_max_ps(scaled_vec1, neg_quant_max_vec), quant_max_vec);
798
+
799
+ __m512 scaled_vec2 = _mm512_mul_ps(fp32_vec2, scale_recip_vec);
800
+ __m512 clamped_vec2 = _mm512_min_ps(_mm512_max_ps(scaled_vec2, neg_quant_max_vec), quant_max_vec);
801
+
802
+ __m128i fp8_vec1 = cvtfp32_fp8e4m3_avx10_2(clamped_vec1);
803
+ __m128i fp8_vec2 = cvtfp32_fp8e4m3_avx10_2(clamped_vec2);
804
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i), fp8_vec1);
805
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(dst + i + 16), fp8_vec2);
806
+ }
807
+}
808
+
809
+std::tuple<at::Tensor, at::Tensor> _quantize_fp8e4m3_bf16_per_tensor_no_scale(const at::Tensor& t) {
810
+ constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max
811
+ constexpr float eps = std::numeric_limits<float>::epsilon();
812
+
813
+ // Input validation and preparation
814
+ assert(t.scalar_type() == at::ScalarType::BFloat16);
815
+ auto t_bf16 = t.contiguous();
816
+ int64_t num_channels = t_bf16.size(0);
817
+ int64_t elements_per_channel = t_bf16.numel() / num_channels;
818
+ assert(elements_per_channel % 32 == 0); // do not consider tile currently
819
+ at::Tensor quant_t = at::empty(t_bf16.sizes(), t_bf16.options().dtype(at::kFloat8_e4m3fn));
820
+
821
+ // Allocate output tensors
822
+ at::Tensor scale_tensor = at::empty({num_channels}, t_bf16.options().dtype(at::ScalarType::Float));
823
+ float* scale_data = scale_tensor.data_ptr<float>();
824
+
825
+ // Unified processing: compute max, apply scale, and quantize in single pass
826
+ at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) {
827
+ for (int64_t c = start; c < end; ++c) {
828
+ const at::BFloat16* channel_src = t_bf16.data_ptr<at::BFloat16>() + c * elements_per_channel;
829
+ at::Float8_e4m3fn* quant_dst = quant_t.data_ptr<at::Float8_e4m3fn>() + c * elements_per_channel;
830
+
831
+ // Step 1: Compute channel-wise max using AVX512
832
+ float channel_max = 0.0f;
833
+ int64_t i = 0;
834
+
835
+ // Process 32 elements at a time for max computation
836
+ for (; i <= elements_per_channel - 32; i += 32) {
837
+ // Load 32 BF16 values (2x 256-bit vectors)
838
+ __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&channel_src[i]));
839
+ __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&channel_src[i + 16]));
840
+
841
+ // Convert BF16 to FP32
842
+ __m512i fp32_int1 = _mm512_cvtepu16_epi32(src_vec1);
843
+ __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(fp32_int1, 16));
844
+
845
+ __m512i fp32_int2 = _mm512_cvtepu16_epi32(src_vec2);
846
+ __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(fp32_int2, 16));
847
+
848
+ // Compute absolute values
849
+ __m512 abs_vec1 = _mm512_abs_ps(fp32_vec1);
850
+ __m512 abs_vec2 = _mm512_abs_ps(fp32_vec2);
851
+
852
+ // Find max in each vector
853
+ float max1 = _mm512_reduce_max_ps(abs_vec1);
854
+ float max2 = _mm512_reduce_max_ps(abs_vec2);
855
+ channel_max = std::max(channel_max, std::max(max1, max2));
856
+ }
857
+
858
+ // Step 2: Apply quant_max and EPS to compute scale
859
+ float scale_val = std::max(channel_max / quant_max, eps);
860
+ float scale_reciprocal = 1.0f / scale_val;
861
+ scale_data[c] = scale_val;
862
+
863
+ // Step 3: Scale and clamp using AVX512 (reuse the same loop structure)
864
+ if (avx10_2_available()) {
865
+ quantize_bf16_block_avx10_2(channel_src, quant_dst, elements_per_channel, scale_reciprocal);
866
+ continue;
867
+ }
868
+ i = 0;
869
+ const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal);
870
+ const __m512 quant_max_vec = _mm512_set1_ps(quant_max);
871
+ const __m512 neg_quant_max_vec = _mm512_set1_ps(-quant_max);
872
+
873
+ for (; i <= elements_per_channel - 32; i += 32) {
874
+ // Load 32 BF16 values
875
+ __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&channel_src[i]));
876
+ __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&channel_src[i + 16]));
877
+
878
+ // Convert BF16 to FP32
879
+ __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec1), 16));
880
+ __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec2), 16));
881
+
882
+ // Scale and clamp
883
+ __m512 scaled_vec1 = _mm512_mul_ps(fp32_vec1, scale_recip_vec);
884
+ __m512 clamped_vec1 = _mm512_min_ps(_mm512_max_ps(scaled_vec1, neg_quant_max_vec), quant_max_vec);
885
+
886
+ __m512 scaled_vec2 = _mm512_mul_ps(fp32_vec2, scale_recip_vec);
887
+ __m512 clamped_vec2 = _mm512_min_ps(_mm512_max_ps(scaled_vec2, neg_quant_max_vec), quant_max_vec);
888
+
889
+ __m128i fp8_vec1 = cvtfp32_fp8e4m3(clamped_vec1);
890
+ __m128i fp8_vec2 = cvtfp32_fp8e4m3(clamped_vec2);
891
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_dst + i), fp8_vec1);
892
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_dst + i + 16), fp8_vec2);
893
+ }
894
+ }
895
+ });
896
+
897
+ return std::make_tuple(quant_t, scale_tensor);
898
+}
899
+
900
+std::tuple<at::Tensor, at::Tensor>
901
+_quantize_fp8e4m3_bf16_per_tensor_with_scale(const at::Tensor& t, at::Tensor& scale_tensor) {
902
+ constexpr float quant_max = 448.0f; // torch.finfo(torch.float8_e4m3fn).max
903
+ constexpr float eps = std::numeric_limits<float>::epsilon();
904
+
905
+ // Input validation
906
+ assert(t.scalar_type() == at::ScalarType::BFloat16);
907
+
908
+ auto t_bf16 = t.contiguous();
909
+ auto scale_tensor_contig = scale_tensor.contiguous().to(at::ScalarType::Float);
910
+
911
+ // Get scale value (handle scalar or tensor)
912
+ float scale_val;
913
+ if (scale_tensor_contig.numel() == 1) {
914
+ scale_val = scale_tensor_contig.item<float>();
915
+ } else {
916
+ // Take max if scale is a tensor (though should be scalar for global quantization)
917
+ scale_val = scale_tensor_contig.max().item<float>();
918
+ }
919
+
920
+ // Apply EPS to scale
921
+ scale_val = std::max(scale_val, eps);
922
+ float scale_reciprocal = 1.0f / scale_val;
923
+
924
+ at::Tensor scale_output = at::tensor({scale_val}, scale_tensor_contig.options());
925
+
926
+ // Apply scale and clamp using AVX512
927
+ const at::BFloat16* src_data = t_bf16.data_ptr<at::BFloat16>();
928
+ at::Tensor quant_t = at::empty(t_bf16.sizes(), t_bf16.options().dtype(at::kFloat8_e4m3fn));
929
+ int64_t total_elements = t_bf16.numel();
930
+ const __m512 scale_recip_vec = _mm512_set1_ps(scale_reciprocal);
931
+ const __m512 quant_max_vec = _mm512_set1_ps(quant_max);
932
+ const __m512 neg_quant_max_vec = _mm512_set1_ps(-quant_max);
933
+ int64_t num_channels = t_bf16.size(0);
934
+ int64_t elements_per_channel = total_elements / num_channels;
935
+ assert(elements_per_channel % 32 == 0); // do not consider tile currently
936
+ // Process in parallel blocks
937
+ at::parallel_for(0, num_channels, 1, [&](int64_t start, int64_t end) {
938
+ for (int64_t c = start; c < end; ++c) {
939
+ const at::BFloat16* src_data = t_bf16.data_ptr<at::BFloat16>() + c * elements_per_channel;
940
+ at::Float8_e4m3fn* quant_t_data = quant_t.data_ptr<at::Float8_e4m3fn>() + c * elements_per_channel;
941
+
942
+ if (avx10_2_available()) {
943
+ quantize_bf16_block_avx10_2(src_data, quant_t_data, elements_per_channel, scale_reciprocal);
944
+ continue;
945
+ }
946
+
947
+ int64_t i = 0;
948
+ // Process 32 elements at a time using AVX512
949
+ for (; i <= elements_per_channel - 32; i += 32) {
950
+ // Load 32 BF16 values (2x 256-bit vectors)
951
+ __m256i src_vec1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&src_data[i]));
952
+ __m256i src_vec2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(&src_data[i + 16]));
953
+
954
+ // Convert BF16 to FP32
955
+ __m512 fp32_vec1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec1), 16));
956
+ __m512 fp32_vec2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(src_vec2), 16));
957
+
958
+ // Scale and clamp
959
+ __m512 scaled_vec1 = _mm512_mul_ps(fp32_vec1, scale_recip_vec);
960
+ __m512 clamped_vec1 = _mm512_min_ps(_mm512_max_ps(scaled_vec1, neg_quant_max_vec), quant_max_vec);
961
+
962
+ __m512 scaled_vec2 = _mm512_mul_ps(fp32_vec2, scale_recip_vec);
963
+ __m512 clamped_vec2 = _mm512_min_ps(_mm512_max_ps(scaled_vec2, neg_quant_max_vec), quant_max_vec);
964
+
965
+ __m128i fp8_vec1 = cvtfp32_fp8e4m3(clamped_vec1);
966
+ __m128i fp8_vec2 = cvtfp32_fp8e4m3(clamped_vec2);
967
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_t_data + i), fp8_vec1);
968
+ _mm_storeu_si128(reinterpret_cast<__m128i*>(quant_t_data + i + 16), fp8_vec2);
969
+ }
970
+ }
971
+ });
972
+
973
+ return std::make_tuple(quant_t, scale_output);
974
+}
975
+
976
+std::tuple<at::Tensor, at::Tensor>
977
+_quantize_fp8e4m3_vec(const at::Tensor& t, bool channelwise, c10::optional<at::Tensor> scale_opt) {
978
+ if (channelwise) {
979
+ return _quantize_fp8e4m3_bf16_per_tensor_no_scale(t);
980
+ } else {
981
+ assert(scale_opt.has_value());
982
+ return _quantize_fp8e4m3_bf16_per_tensor_with_scale(t, scale_opt.value());
983
+ }
984
+}
985
+
986
+// Public wrapper for torch op registration
987
+std::tuple<at::Tensor, at::Tensor>
988
+quantize_fp8e4m3_vec(const at::Tensor& t, bool channelwise, c10::optional<at::Tensor> scale_opt) {
989
+ return _quantize_fp8e4m3_vec(t, channelwise, scale_opt);
990
+}
991
+
992
+at::Tensor fp8_scaled_mm_with_quant(
993
+ const at::Tensor& act,
994
+ const std::optional<at::Tensor>& act_scales,
995
+ bool channelwise,
996
+ const at::Tensor& weight,
997
+ const at::Tensor& weight_scales,
998
+ const std::optional<at::Tensor>& bias,
999
+ at::ScalarType output_dtype) {
1000
+ // TODO: refine here to use tensor ptr and in below dispatch api.
1001
+ std::tuple<at::Tensor, at::Tensor> quant_act = act.scalar_type() == at::ScalarType::BFloat16
1002
+ ? _quantize_fp8e4m3_vec(act, channelwise, act_scales)
1003
+ : _quantize_fp8e4m3(act, channelwise, act_scales);
1004
+ auto input = std::get<0>(quant_act);
1005
+ auto input_scales = std::get<1>(quant_act);
1006
+ int64_t N = weight.dim() == 4 ? weight.size(0) * weight.size(-1) : weight.size(0);
1007
+ int act_quant_mode = input_scales.numel() == 1 ? PER_TENSOR
1008
+ : input_scales.numel() == input.numel() / input.size(-1) ? PER_ROW
1009
+ : PER_GROUP;
1010
+ int wei_quant_mode = weight_scales.numel() == 1 ? PER_TENSOR : weight_scales.numel() == N ? PER_ROW : PER_GROUP;
1011
+ // Case to fall back
1012
+ if (weight.dim() == 2) {
1013
+ TORCH_CHECK(
1014
+ act_quant_mode != PER_GROUP && wei_quant_mode != PER_GROUP,
1015
+ "FP8 linear: Per-group quantization is not supported in the fallback path");
1016
+ auto y_fp32 = at::linear(input.to(at::kFloat).mul_(input_scales), weight.to(at::kFloat).mul_(weight_scales), bias);
1017
+ return y_fp32.to(output_dtype);
1018
+ }
1019
+
1020
+ static bool cpublas_can_pack = cpublas_could_pack();
1021
+ auto out_sizes = input.sizes().vec();
1022
+ out_sizes.back() = N;
1023
+ auto output = at::empty(out_sizes, input.options().dtype(output_dtype));
1024
+
1025
+#define AT_DISPATCH_FP8_LINEAR_KERNEL(OUT_DTYPE, CAN_PACK, A_QUANT_MODE, B_QUANT_MODE, ...) \
1026
+ AT_DISPATCH_BOOL_NO_RETURN( \
1027
+ CAN_PACK, \
1028
+ "cpublas_can_pack", \
1029
+ can_pack, \
1030
+ AT_DISPATCH_QUANT_MODE_NO_RETURN( \
1031
+ A_QUANT_MODE, \
1032
+ "act_quant_mode", \
1033
+ a_quant_mode, \
1034
+ AT_DISPATCH_QUANT_MODE_NO_RETURN( \
1035
+ B_QUANT_MODE, \
1036
+ "wei_quant_mode", \
1037
+ b_quant_mode, \
1038
+ AT_DISPATCH_OUT_TYPES(OUT_DTYPE, "out_dtype", __VA_ARGS__))))
1039
+
1040
+ AT_DISPATCH_FP8_LINEAR_KERNEL(output_dtype, cpublas_can_pack, act_quant_mode, wei_quant_mode, [&]() {
1041
+ _float8_linear_impl<out_t, can_pack, a_quant_mode, b_quant_mode>(
1042
+ input, input_scales, weight, weight_scales, bias, output);
1043
+ });
1044
+ return output;
1045
+}
1046
+
1047
+at::Tensor float8_linear_impl(
1048
+ const at::Tensor& input,
1049
+ const at::Tensor& input_scales,
1050
+ const at::Tensor& weight,
1051
+ const at::Tensor& weight_scales,
1052
+ const std::optional<at::Tensor>& bias,
1053
+ at::ScalarType output_dtype) {
1054
+ int64_t N = weight.dim() == 4 ? weight.size(0) * weight.size(-1) : weight.size(0);
1055
+ int act_quant_mode = input_scales.numel() == 1 ? PER_TENSOR
1056
+ : input_scales.numel() == input.numel() / input.size(-1) ? PER_ROW
1057
+ : PER_GROUP;
1058
+ int wei_quant_mode = weight_scales.numel() == 1 ? PER_TENSOR : weight_scales.numel() == N ? PER_ROW : PER_GROUP;
1059
+ // Case to fall back
1060
+ if (weight.dim() == 2) {
1061
+ TORCH_CHECK(
1062
+ act_quant_mode != PER_GROUP && wei_quant_mode != PER_GROUP,
1063
+ "FP8 linear: Per-group quantization is not supported in the fallback path");
1064
+ auto y_fp32 = at::linear(input.to(at::kFloat).mul_(input_scales), weight.to(at::kFloat).mul_(weight_scales), bias);
1065
+ return y_fp32.to(output_dtype);
1066
+ }
1067
+
1068
+ static bool cpublas_can_pack = cpublas_could_pack();
1069
+ auto out_sizes = input.sizes().vec();
1070
+ out_sizes.back() = N;
1071
+ auto output = at::empty(out_sizes, input.options().dtype(output_dtype));
1072
+
1073
+#define AT_DISPATCH_FP8_LINEAR_KERNEL(OUT_DTYPE, CAN_PACK, A_QUANT_MODE, B_QUANT_MODE, ...) \
1074
+ AT_DISPATCH_BOOL_NO_RETURN( \
1075
+ CAN_PACK, \
1076
+ "cpublas_can_pack", \
1077
+ can_pack, \
1078
+ AT_DISPATCH_QUANT_MODE_NO_RETURN( \
1079
+ A_QUANT_MODE, \
1080
+ "act_quant_mode", \
1081
+ a_quant_mode, \
1082
+ AT_DISPATCH_QUANT_MODE_NO_RETURN( \
1083
+ B_QUANT_MODE, \
1084
+ "wei_quant_mode", \
1085
+ b_quant_mode, \
1086
+ AT_DISPATCH_OUT_TYPES(OUT_DTYPE, "out_dtype", __VA_ARGS__))))
1087
+
1088
+ AT_DISPATCH_FP8_LINEAR_KERNEL(output_dtype, cpublas_can_pack, act_quant_mode, wei_quant_mode, [&]() {
1089
+ _float8_linear_impl<out_t, can_pack, a_quant_mode, b_quant_mode>(
1090
+ input, input_scales, weight, weight_scales, bias, output);
1091
+ });
1092
+ return output;
1093
+}
\ No newline at end of file
csrc/cpu/sgl-kernels/moe.cpp
+93
-7
@@ -836,6 +836,11 @@ static inline void check_moe_scales(
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
@@ -850,9 +855,14 @@ static inline void check_moe_scales(
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(); \
@@ -864,6 +874,17 @@ static inline void check_moe_scales(
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
@@ -883,6 +904,7 @@ void fused_experts_cpu(
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,
@@ -895,7 +917,10 @@ void fused_experts_cpu(
917
constexpr int64_t BLOCK_M = block_size_m();
918
constexpr int64_t BLOCK_N = block_size_n();
919
898
- const auto st = hidden_states.scalar_type();
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);
@@ -908,6 +933,9 @@ void fused_experts_cpu(
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);
@@ -924,8 +952,9 @@ void fused_experts_cpu(
952
953
int64_t M = hidden_states.size(0);
954
int64_t K = hidden_states.size(1);
927
- int64_t N = moe_comp_method == CPUQuantMethod::INT4_W4A8 ? w1_scale.value().size(1) * w1_scale.value().size(3) / 2
928
- : w1.size(1) / 2;
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
@@ -935,7 +964,10 @@ void fused_experts_cpu(
964
965
// check weight shapes
966
CHECK_EQ(w2.size(0), E);
938
- if (!(moe_comp_method == CPUQuantMethod::INT4_W4A8)) {
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));
@@ -993,22 +1025,35 @@ void fused_experts_cpu(
1025
// 5. Aq_tmp : [M, K] or [M * topk, N]
1026
// 6. As_tmp : [M * topk]
1027
//
996
- // for fp8 w8a16 and mxfp4:
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 *
1003
- (moe_comp_method == CPUQuantMethod::INT8_W8A8 | moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 1 : 2) +
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
}
1009
- if (moe_comp_method == CPUQuantMethod::FP8_W8A16 || moe_comp_method == CPUQuantMethod::MXFP4) {
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;
@@ -1181,6 +1226,47 @@ void fused_experts_cpu(
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));
csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp
new
+432
@@ -0,0 +1,432 @@
1
+// Adapted from
2
+// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
3
+
4
+// clang-format off
5
+
6
+#include <c10/util/Unroll.h>
7
+
8
+#include "common.h"
9
+#include "gemm.h"
10
+#include "moe.h"
11
+
12
+namespace {
13
+
14
+// Byte-wise copy for FP8 elements (no remainder assumption)
15
+inline void copy_stub2(
16
+ at::Float8_e4m3fn* __restrict__ out,
17
+ const at::Float8_e4m3fn* __restrict__ input,
18
+ int64_t size) {
19
+ for (int64_t d = 0; d < size; d++) {
20
+ out[d] = input[d];
21
+ }
22
+}
23
+
24
+#define PER_TENSOR 1
25
+#define PER_ROW 2
26
+#define PER_GROUP 3
27
+
28
+// Store float32 GEMM result to output buffer with dequantization and dtype
29
+// conversion. Activations are PER_ROW quantized; weights are PER_GROUP quantized.
30
+// Uses AVX512 on capable platforms.
31
+#if defined(CPU_CAPABILITY_AVX512)
32
+
33
+template <typename out_dtype, int64_t N, int act_quant_mode, int wei_quant_mode>
34
+inline void store_out(
35
+ const float* y_buf,
36
+ out_dtype* c_ptr,
37
+ int64_t M,
38
+ int64_t lda,
39
+ const float* scales_a,
40
+ const float* scales_b,
41
+ const float* bias) {
42
+ float a_scale = 1.0, b_scale = 1.0;
43
+ __m512 va_scale, vb_scale;
44
+ if constexpr (act_quant_mode == PER_TENSOR) {
45
+ a_scale = *scales_a;
46
+ }
47
+ if constexpr (wei_quant_mode == PER_TENSOR) {
48
+ b_scale = *scales_b;
49
+ vb_scale = _mm512_set1_ps(b_scale);
50
+ }
51
+ for (int i = 0; i < M; ++i) {
52
+ if constexpr (act_quant_mode == PER_ROW) {
53
+ a_scale = *(scales_a + i);
54
+ }
55
+ if constexpr (act_quant_mode != PER_GROUP) {
56
+ va_scale = _mm512_set1_ps(a_scale);
57
+ }
58
+ constexpr int N_UNROLL = N / 16;
59
+ c10::ForcedUnroll<N_UNROLL>{}([&](auto idx) {
60
+ constexpr int j = idx * 16;
61
+ __m512 y_vec = _mm512_loadu_ps(y_buf + i * N + j);
62
+ __m512 bias_vec = bias ? _mm512_loadu_ps(bias + j) : _mm512_setzero_ps();
63
+ if constexpr (act_quant_mode != PER_GROUP) {
64
+ y_vec = _mm512_mul_ps(y_vec, va_scale);
65
+ }
66
+ if constexpr (wei_quant_mode == PER_ROW) {
67
+ vb_scale = _mm512_loadu_ps(scales_b + j);
68
+ }
69
+ if constexpr (wei_quant_mode != PER_GROUP) {
70
+ y_vec = _mm512_mul_ps(y_vec, vb_scale);
71
+ }
72
+ y_vec = _mm512_add_ps(y_vec, bias_vec);
73
+ if constexpr (std::is_same<out_dtype, float>::value) {
74
+ _mm512_storeu_ps(c_ptr + i * lda + j, y_vec);
75
+ } else if constexpr (std::is_same<out_dtype, at::BFloat16>::value) {
76
+ __m256i y_bf16_vec = at::vec::cvtfp32_bf16(y_vec);
77
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_bf16_vec);
78
+ } else if constexpr (std::is_same<out_dtype, at::Half>::value) {
79
+ __m256i y_fp16_vec = at::vec::cvtfp32_fp16(y_vec);
80
+ _mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_fp16_vec);
81
+ } else {
82
+ TORCH_CHECK(false, "Unsupported output dtype");
83
+ }
84
+ });
85
+ constexpr int tail_start = N / 16 * 16;
86
+ for (int j = tail_start; j < N; ++j) {
87
+ if constexpr (wei_quant_mode == PER_ROW) {
88
+ b_scale = scales_b[j];
89
+ }
90
+ c_ptr[i * lda + j] = static_cast<out_dtype>(y_buf[i * N + j] * a_scale * b_scale);
91
+ }
92
+ } // for M
93
+}
94
+
95
+#else // no AVX512
96
+
97
+template <typename out_dtype, int64_t N, int act_quant_mode, int wei_quant_mode>
98
+inline void store_out(
99
+ const float* y_buf,
100
+ out_dtype* c_ptr,
101
+ int64_t M,
102
+ int64_t lda,
103
+ const float* scales_a,
104
+ const float* scales_b,
105
+ const float* bias) {
106
+ float a_scale = 1.0, b_scale = 1.0;
107
+ if constexpr (act_quant_mode == PER_TENSOR) {
108
+ a_scale = *scales_a;
109
+ }
110
+ if constexpr (wei_quant_mode == PER_TENSOR) {
111
+ b_scale = *scales_b;
112
+ }
113
+ for (int i = 0; i < M; ++i) {
114
+ if constexpr (act_quant_mode == PER_ROW) {
115
+ a_scale = *(scales_a + i);
116
+ }
117
+ for (int j = 0; j < N; ++j) {
118
+ if constexpr (wei_quant_mode == PER_ROW) {
119
+ b_scale = scales_b[j];
120
+ }
121
+ float val = y_buf[i * N + j] * a_scale * b_scale;
122
+ if (bias) val += bias[j];
123
+ c_ptr[i * lda + j] = static_cast<out_dtype>(val);
124
+ }
125
+ }
126
+}
127
+
128
+#endif // CPU_CAPABILITY_AVX512
129
+
130
+} // anonymous namespace
131
+
132
+// FP8 W8A8 fused MoE kernel.
133
+//
134
+// hidden_states: [M, K] FP8 E4M3, pre-quantized activation
135
+// packed_w1: [E, 2*Nc, Kc, BLOCK_K, BLOCK_N] FP8 W8A8 prepack (float8_linear_prepack_cpu)
136
+// packed_w2: [E, K, N_packed] FP8 W8A16 VNNI pack (convert_weight_packed)
137
+// As: [M] float32 per-token activation scales
138
+// w1s: [E, num_groups, 2N] float32 per-group weight scales for w1
139
+// w2s: [E, ...] float32 per-group weight scales for w2
140
+//
141
+// Stage 1: hidden_states(FP8) @ w1(FP8) → ic0(BF16) [M*topk, 2N]
142
+// Uses FP8×FP8 GEMM (tinygemm_kernel with FP8 inputs)
143
+// Stage 1.5: ic1 = silu(ic0[:,:N]) * ic0[:,N:] [M*topk, N]
144
+// Stage 2: ic1(BF16) @ w2(FP8) → ic2(BF16) [M*topk, K]
145
+// Uses BF16×FP8 GEMM (W8A16 path, tinygemm_kernel<scalar_t>)
146
+// Stage 3: out = weighted_sum(ic2, topk_weights) [M, K]
147
+
148
+template <typename scalar_t>
149
+void fused_experts_fp8_a8_kernel_impl(
150
+ scalar_t* __restrict__ output,
151
+ scalar_t* __restrict__ ic0,
152
+ scalar_t* __restrict__ ic1,
153
+ scalar_t* __restrict__ ic2,
154
+ at::Float8_e4m3fn* __restrict__ A_tmp,
155
+ scalar_t* __restrict__ B_tmp,
156
+ float* __restrict__ C_tmp,
157
+ float* __restrict__ Ukernel_tmp,
158
+ const at::Float8_e4m3fn* __restrict__ input,
159
+ const at::Float8_e4m3fn* __restrict__ packed_w1,
160
+ const at::Float8_e4m3fn* __restrict__ packed_w2,
161
+ const float* __restrict__ As,
162
+ const float* __restrict__ w1s,
163
+ const float* __restrict__ w2s,
164
+ int64_t block_size_N,
165
+ int64_t block_size_K,
166
+ const float* __restrict__ topk_weights,
167
+ const int32_t* __restrict__ sorted_ids,
168
+ const int32_t* __restrict__ expert_ids,
169
+ const int32_t* __restrict__ offsets,
170
+ int64_t M,
171
+ int64_t N,
172
+ int64_t K,
173
+ int64_t E,
174
+ int64_t topk,
175
+ int64_t num_tokens_post_pad) {
176
+ // 1. intermediate_cache1 : [M * topk, N]
177
+ // 2. intermediate_cache2 : [M * topk, K]
178
+ // 3. A_tmp : [T, BLOCK_M * K] FP8
179
+ // 4. C_tmp : [T, 2 * BLOCK_M * BLOCK_N] float32
180
+ // 5. intermediate_cache0 : [M * topk, 2N]
181
+ // 6. B_tmp : [T, MAX_CACHE_BLOCK_SIZE, BLOCK_N, max(K, N)] scalar_t
182
+ // 7. Ukernel_tmp: [T, 2 * BLOCK_M * BLOCK_N] float32
183
+ // 8. (non-FP8-brgemm only) dqA [T, BLOCK_M * BLOCK_K] + dqB [T, BLOCK_K * BLOCK_N] in BF16
184
+ // appended after Ukernel_tmp; see moe.cpp FP8_W8A8 buffer allocation
185
+
186
+ constexpr int64_t BLOCK_M = block_size_m();
187
+ constexpr int64_t BLOCK_N_VAL = block_size_n(); // = 32
188
+ const int64_t KB = K / BLOCK_K; // K blocks
189
+
190
+ int64_t B_tmp_size_per_thread = MAX_CACHE_BLOCK_SIZE * BLOCK_N_VAL * std::max(K, N);
191
+
192
+ // stage 1: intermediate_cache0 = hidden_states @ w1
193
+ const int64_t MB = div_up(num_tokens_post_pad, BLOCK_M);
194
+ const int64_t NB = div_up(N, BLOCK_N_VAL);
195
+
196
+ int64_t scale_size_N = div_up(2 * N, block_size_N);
197
+ int64_t scale_size_K = div_up(K, block_size_K);
198
+ int64_t blocks_n_per_group = block_size_N / BLOCK_N_VAL;
199
+ int64_t num_groups = div_up(K, block_size_K); // G
200
+ int64_t blocks_k_per_group = block_size_K / BLOCK_K;
201
+ const int64_t stride_e = 2 * N * K; // per-expert stride for w1 (total elements)
202
+ bool use_brgemm = true;
203
+ int64_t num_thread = at::get_num_threads();
204
+
205
+ // For the BF16 fallback path (no native FP8 brgemm), tinygemm_kernel needs temporary
206
+ // BF16 buffers to dequantize FP8 inputs before running AMX-BF16 brgemm.
207
+ // These are placed after the Ukernel_tmp region (see moe.cpp FP8_W8A8 buffer allocation).
208
+#ifndef CPUBLAS_BRGEMM_F8F8F32
209
+ // dqA: [T, BLOCK_M, BLOCK_K] BF16
210
+ // dqB: [T, BLOCK_K, BLOCK_N] BF16
211
+ // Layout: immediately after Ukernel_tmp[num_thread * 2 * BLOCK_M * BLOCK_N_VAL]
212
+ at::BFloat16* __restrict__ dq_base = reinterpret_cast<at::BFloat16*>(
213
+ Ukernel_tmp + num_thread * 2 * BLOCK_M * BLOCK_N_VAL);
214
+ const int64_t dq_per_thread = BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N_VAL;
215
+#endif
216
+
217
+ at::parallel_for(0, MB * NB, 1, [&](int64_t begin, int64_t end) {
218
+ int tid = get_thread_num();
219
+ // A_tmp: FP8 activation buffer for this thread [BLOCK_M, K]
220
+ at::Float8_e4m3fn* __restrict__ A = A_tmp + tid * BLOCK_M * K;
221
+ float* __restrict__ C0 = C_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL;
222
+ float* __restrict__ C1 = C0 + BLOCK_M * BLOCK_N_VAL;
223
+ alignas(64) float As_[BLOCK_M];
224
+ float* __restrict__ ukernel_buf_1 = Ukernel_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL;
225
+#ifndef CPUBLAS_BRGEMM_F8F8F32
226
+ at::BFloat16* __restrict__ dqA_buf = dq_base + tid * dq_per_thread;
227
+ at::BFloat16* __restrict__ dqB_buf = dqA_buf + BLOCK_M * BLOCK_K;
228
+#else
229
+ at::BFloat16* __restrict__ dqA_buf = nullptr;
230
+ at::BFloat16* __restrict__ dqB_buf = nullptr;
231
+#endif
232
+ float* __restrict__ ukernel_buf_2 = ukernel_buf_1 + BLOCK_M * BLOCK_N_VAL;
233
+ auto ldsa = 1; // activation per-row quantized
234
+
235
+ for (int64_t i = begin; i < end; ++i) {
236
+ int64_t mb = i / NB;
237
+ int64_t nb = i % NB;
238
+ int64_t nb1 = nb + NB; // gate is first N columns, up is second N columns
239
+
240
+ int64_t n_size = std::min(N - nb * BLOCK_N_VAL, BLOCK_N_VAL);
241
+
242
+ // B (weight) for expert expert_id: gate+up fused [2N, K] prepackaged as FP8
243
+ int32_t expert_id = expert_ids[mb];
244
+ const at::Float8_e4m3fn* __restrict__ B = packed_w1 + expert_id * stride_e;
245
+ const float* __restrict__ Bs = w1s + expert_id * num_groups * (2 * N);
246
+
247
+ // Load activation tile for this M-block
248
+ const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
249
+ int64_t m_size = offsets[mb + 1] - offsets[mb];
250
+
251
+ for (int64_t m = 0; m < m_size; ++m) {
252
+ int32_t index = A_ids[m] / topk;
253
+ copy_stub2(A + m * K, input + index * K, K);
254
+ As_[m] = As[index];
255
+ }
256
+
257
+ const int64_t offset = offsets[mb];
258
+
259
+ // Gate path (nb): w1[:N, :]
260
+ zero_buffer(C0, BLOCK_M * BLOCK_N_VAL);
261
+ for (int kci = 0; kci < KB; ++kci) {
262
+ // FP8×FP8 tinygemm: act_quant=PER_ROW(2), wei_quant=PER_GROUP(3)
263
+ tinygemm_kernel(
264
+ /* C */ C0,
265
+ /* A */ A + kci * BLOCK_K,
266
+ /* scales_a*/ As_ + kci / blocks_k_per_group,
267
+ /* B */ B + (nb * KB + kci) * BLOCK_K * BLOCK_N_VAL,
268
+ /* scales_b*/ Bs + nb * BLOCK_N_VAL * num_groups + kci / blocks_k_per_group * BLOCK_N_VAL,
269
+ /* M */ m_size,
270
+ /* K */ BLOCK_K,
271
+ /* lda */ K,
272
+ /* ldc */ BLOCK_N_VAL,
273
+ /* ldsa */ ldsa,
274
+ /* ukernel */ ukernel_buf_1,
275
+ /* dqA_buf */ dqA_buf,
276
+ /* dqB_buf */ dqB_buf);
277
+ }
278
+ store_out<scalar_t, BLOCK_N_VAL, PER_ROW, PER_GROUP>(
279
+ C0,
280
+ ic0 + offset * 2 * N + nb * BLOCK_N_VAL,
281
+ m_size,
282
+ 2 * N, // lda
283
+ As_,
284
+ nullptr,
285
+ nullptr);
286
+
287
+ // Up path (nb1): w1[N:, :]
288
+ zero_buffer(C1, BLOCK_M * BLOCK_N_VAL);
289
+ for (int kci = 0; kci < KB; ++kci) {
290
+ tinygemm_kernel(
291
+ /* C */ C1,
292
+ /* A */ A + kci * BLOCK_K,
293
+ /* scales_a*/ As_ + kci / blocks_k_per_group,
294
+ /* B */ B + (nb1 * KB + kci) * BLOCK_K * BLOCK_N_VAL,
295
+ /* scales_b*/ Bs + nb1 * BLOCK_N_VAL * num_groups + kci / blocks_k_per_group * BLOCK_N_VAL,
296
+ /* M */ m_size,
297
+ /* K */ BLOCK_K,
298
+ /* lda */ K,
299
+ /* ldc */ BLOCK_N_VAL,
300
+ /* ldsa */ ldsa,
301
+ /* ukernel */ ukernel_buf_2,
302
+ /* dqA_buf */ dqA_buf,
303
+ /* dqB_buf */ dqB_buf);
304
+ }
305
+ store_out<scalar_t, BLOCK_N_VAL, PER_ROW, PER_GROUP>(
306
+ C1,
307
+ ic0 + offset * 2 * N + nb1 * BLOCK_N_VAL,
308
+ m_size,
309
+ 2 * N, // lda
310
+ As_,
311
+ nullptr,
312
+ nullptr);
313
+ }
314
+ if (use_brgemm) {
315
+ at::native::cpublas::brgemm_release();
316
+ }
317
+ });
318
+
319
+ // stage 1.5: intermediate_cache1 = silu(ic0[:, :N]) * ic0[:, N:]
320
+ at::parallel_for(0, M * topk, 0, [&](int64_t begin, int64_t end) {
321
+ for (int64_t m = begin; m < end; ++m) {
322
+ silu_and_mul_stub(ic1 + m * N, ic0 + m * 2 * N, ic0 + m * 2 * N + N, N);
323
+ }
324
+ });
325
+
326
+ // stage 2: intermediate_cache2 = intermediate_cache1 @ w2
327
+ // w2: [E, K, N] FP8, VNNI packed; treated as W8A16 (BF16 act × FP8 weight)
328
+ const int64_t OC = K; // output channels (hidden dim)
329
+ const int64_t IC = N; // input channels (expert intermediate dim)
330
+ const int64_t MB2 = MB;
331
+ const int64_t NB2 = div_up(OC, BLOCK_N_VAL);
332
+ scale_size_N = div_up(K, block_size_N);
333
+ scale_size_K = div_up(N, block_size_K);
334
+ const int64_t stride_e2 = OC * IC; // per-expert stride for w2
335
+ const int64_t stride_oc = IC;
336
+
337
+ int64_t avg_M = std::max(int64_t(1), M * topk / E);
338
+ bool use_brgemm2 = can_use_brgemm<at::Float8_e4m3fn>(avg_M);
339
+
340
+ parallel_2d(MB2, NB2, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
341
+ int tid = get_thread_num();
342
+ alignas(64) scalar_t C[BLOCK_M * BLOCK_K];
343
+
344
+ loop_2d<at::Float8_e4m3fn>(mb0, mb1, nb0, nb1, BLOCK_N_VAL * IC, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
345
+ int64_t m_size = offsets[mb + 1] - offsets[mb];
346
+ int64_t n_size = std::min(OC - nb * BLOCK_N_VAL, BLOCK_N_VAL);
347
+
348
+ // A from ic1: [M*topk, N] in sorted order (contiguous after stage 1.5)
349
+ const scalar_t* __restrict__ A2 = ic1 + offsets[mb] * N;
350
+ const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
351
+
352
+ // B from w2: [E, K, N] VNNI packed, offset to expert + output block
353
+ int32_t expert_id = expert_ids[mb];
354
+ const at::Float8_e4m3fn* __restrict__ B2 =
355
+ packed_w2 + expert_id * stride_e2 + nb * BLOCK_N_VAL * stride_oc;
356
+ const float* __restrict__ Bs2 =
357
+ w2s + expert_id * scale_size_N * scale_size_K + (nb / blocks_n_per_group) * scale_size_K;
358
+
359
+ // Only unpack B on first visit or new expert
360
+ int32_t pre_expert_id = mb == 0 ? -1 : expert_ids[mb - 1];
361
+ bool do_unpack = (mb == mb0) || (expert_id != pre_expert_id);
362
+
363
+ tinygemm_kernel<scalar_t>(
364
+ /* A */ A2,
365
+ /* B */ B2,
366
+ /* C */ C,
367
+ /* Btmp */ B_tmp + tid * B_tmp_size_per_thread + nb_offset * BLOCK_N_VAL * IC,
368
+ /* Ctmp */ C_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL,
369
+ /* Bbias */ nullptr,
370
+ /* scale */ Bs2,
371
+ /* M */ m_size,
372
+ /* N */ n_size,
373
+ /* K */ IC,
374
+ /* lda */ IC,
375
+ /* ldb */ n_size,
376
+ /* ldc */ BLOCK_N_VAL,
377
+ /* brg */ use_brgemm2,
378
+ /* block_sz_K */ block_size_K,
379
+ /* do_unpack */ do_unpack);
380
+
381
+ // Scatter ic2 in original token order, weighting by topk_weights
382
+ for (int64_t m = 0; m < m_size; ++m) {
383
+ int32_t index = A_ids[m];
384
+ float weight = topk_weights[index];
385
+ copy_mul_stub(ic2 + index * K + nb * BLOCK_N_VAL, C + m * BLOCK_N_VAL, weight, n_size);
386
+ }
387
+ });
388
+
389
+ if (use_brgemm2) {
390
+ at::native::cpublas::brgemm_release();
391
+ }
392
+ });
393
+
394
+ // stage 3: output = sum(ic2, dim=topk) [M, K]
395
+ at::parallel_for(0, M, 0, [&](int64_t begin, int64_t end) {
396
+ for (int64_t m = begin; m < end; ++m) {
397
+ sum_stub(output + m * K, ic2 + m * topk * K, topk, K);
398
+ }
399
+ });
400
+}
401
+
402
+#define INSTANTIATE_MOE_FP8_A8_TEMPLATE(TYPE) \
403
+ template void fused_experts_fp8_a8_kernel_impl<TYPE>( \
404
+ TYPE* __restrict__ output, \
405
+ TYPE* __restrict__ ic0, \
406
+ TYPE* __restrict__ ic1, \
407
+ TYPE* __restrict__ ic2, \
408
+ at::Float8_e4m3fn* __restrict__ A_tmp, \
409
+ TYPE* __restrict__ B_tmp, \
410
+ float* __restrict__ C_tmp, \
411
+ float* __restrict__ Ukernel_tmp, \
412
+ const at::Float8_e4m3fn* __restrict__ input, \
413
+ const at::Float8_e4m3fn* __restrict__ packed_w1, \
414
+ const at::Float8_e4m3fn* __restrict__ packed_w2, \
415
+ const float* __restrict__ As, \
416
+ const float* __restrict__ w1s, \
417
+ const float* __restrict__ w2s, \
418
+ int64_t block_size_N, \
419
+ int64_t block_size_K, \
420
+ const float* __restrict__ topk_weights, \
421
+ const int32_t* __restrict__ sorted_ids, \
422
+ const int32_t* __restrict__ expert_ids, \
423
+ const int32_t* __restrict__ offsets, \
424
+ int64_t M, \
425
+ int64_t N, \
426
+ int64_t K, \
427
+ int64_t E, \
428
+ int64_t topk, \
429
+ int64_t num_tokens_post_pad)
430
+
431
+INSTANTIATE_MOE_FP8_A8_TEMPLATE(at::BFloat16);
432
+INSTANTIATE_MOE_FP8_A8_TEMPLATE(at::Half);
csrc/cpu/torch_bindings.cpp
+35
-2
@@ -73,6 +73,7 @@ void fused_experts_cpu(at::Tensor& out, at::Tensor& hidden_states,
73
const std::optional<at::Tensor>& w2_scale,
74
const std::optional<at::Tensor>& w1_zero,
75
const std::optional<at::Tensor>& w2_zero,
76
+ const std::optional<at::Tensor>& a1_scale,
77
const std::optional<std::vector<int64_t>> block_size,
78
const std::optional<at::Tensor>& w1_bias,
79
const std::optional<at::Tensor>& w2_bias,
@@ -91,6 +92,18 @@ at::Tensor fp8_scaled_mm_cpu(at::Tensor& mat1, at::Tensor& mat2,
92
const std::optional<at::Tensor>& bias,
93
at::ScalarType out_dtype, bool is_vnni);
94
95
+// Adapted from sglang: FP8 W8A8 kernels
96
+std::tuple<at::Tensor, at::Tensor> float8_linear_prepack_impl(
97
+ const at::Tensor& weight, const at::Tensor& scales);
98
+at::Tensor fp8_scaled_mm_with_quant(const at::Tensor& act,
99
+ const std::optional<at::Tensor>& act_scales,
100
+ bool channelwise, const at::Tensor& weight,
101
+ const at::Tensor& weight_scales,
102
+ const std::optional<at::Tensor>& bias,
103
+ at::ScalarType output_dtype);
104
+std::tuple<at::Tensor, at::Tensor> quantize_fp8e4m3_vec(
105
+ const at::Tensor& t, bool channelwise, c10::optional<at::Tensor> scale_opt);
106
+
107
// Adapted from sglang: MLA CPU kernels (AMX-only)
108
void decode_attention_cpu(at::Tensor& query, at::Tensor& k_buffer,
109
at::Tensor& v_buffer, at::Tensor& output,
@@ -281,6 +294,7 @@ void activation_lut_bf16(torch::Tensor& out, torch::Tensor& input,
294
const std::string& activation);
295
296
bool cpu_attn_has_isa(const std::string& isa);
297
+bool cpu_has_amx_fp8();
298
299
torch::Tensor get_scheduler_metadata(
300
const int64_t num_req, const int64_t num_heads_q,
@@ -611,7 +625,7 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
625
"fused_experts_cpu(Tensor(a0!) out, Tensor hidden_states, Tensor w1, "
626
"Tensor w2, Tensor topk_weights, Tensor topk_ids, "
627
"int moe_comp_method, Tensor? w1_scale, Tensor? w2_scale, "
614
- "Tensor? w1_zero, Tensor? w2_zero, int[]? block_size, "
628
+ "Tensor? w1_zero, Tensor? w2_zero, Tensor? a1_scale, int[]? block_size, "
629
"Tensor? w1_bias, Tensor? w2_bias, float? alpha, float? limit, "
630
"bool is_vnni) -> "
631
"()");
@@ -629,7 +643,23 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
643
"bool is_vnni) -> Tensor");
644
ops.impl("fp8_scaled_mm_cpu", torch::kCPU, &fp8_scaled_mm_cpu);
645
632
- // Adapted from sglang: casual_conv1d kernels
646
+ // Adapted from sglang: FP8 W8A8 kernels
647
+ ops.def(
648
+ "float8_linear_prepack_cpu(Tensor weight, Tensor scales) -> (Tensor, "
649
+ "Tensor)");
650
+ ops.impl("float8_linear_prepack_cpu", torch::kCPU,
651
+ &float8_linear_prepack_impl);
652
+ ops.def(
653
+ "fp8_scaled_mm_with_quant(Tensor act, Tensor? act_scales, bool "
654
+ "channelwise, "
655
+ "Tensor weight, Tensor weight_scales, Tensor? bias, ScalarType "
656
+ "output_dtype) -> Tensor");
657
+ ops.impl("fp8_scaled_mm_with_quant", torch::kCPU, &fp8_scaled_mm_with_quant);
658
+ ops.def(
659
+ "quantize_fp8e4m3_vec(Tensor input, bool channelwise, Tensor? scale_opt) "
660
+ "-> (Tensor, Tensor)");
661
+ ops.impl("quantize_fp8e4m3_vec", torch::kCPU, &quantize_fp8e4m3_vec);
662
+
663
ops.def("causal_conv1d_weight_pack(Tensor weight) -> Tensor");
664
ops.impl("causal_conv1d_weight_pack", torch::kCPU,
665
&causal_conv1d_weight_pack);
@@ -832,6 +862,9 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
862
"-> (Tensor, Tensor)");
863
ops.impl("fused_gdn_gating_cpu", torch::kCPU, &fused_gdn_gating_cpu);
864
865
+ // Pure runtime CPU ISA capability check.
866
+ ops.def("cpu_has_amx_fp8() -> bool", &cpu_has_amx_fp8);
867
+
868
// CPU attention kernels
869
ops.def("cpu_attn_has_isa(str isa) -> bool", &cpu_attn_has_isa);
870
ops.def(
tests/kernels/moe/test_cpu_quant_fused_moe.py
+198
@@ -1114,5 +1114,203 @@ def test_int8_w8a8_cpu_fused_moe(M, N, K, E, topk, seed, is_vnni, inplace):
1114
)
1115
1116
1117
+# ===========================================================================
1118
+# FP8 W8A8 MoE
1119
+# ===========================================================================
1120
+
1121
+
1122
+requires_cpu_fp8_w8a8 = pytest.mark.skipif(
1123
+ not ops.cpu_has_amx_fp8(),
1124
+ reason="requires native AMX-FP8 hardware",
1125
+)
1126
+
1127
+FP8_W8A8_MAX = torch.finfo(torch.float8_e4m3fn).max
1128
+FP8_W8A8_QUANT_GROUP = 128 # quantization group size for both K and N dimensions
1129
+
1130
+
1131
+def _make_fp8_w8a8_weight_w13(
1132
+ E: int, two_n: int, K: int, group_K: int = FP8_W8A8_QUANT_GROUP
1133
+) -> tuple[torch.Tensor, torch.Tensor]:
1134
+ """Create w13 [E, 2N, K] FP8 with group scales [E, 2N, G] where G = K//group_K."""
1135
+ G = K // group_K
1136
+ w_list, s_list = [], []
1137
+ for _ in range(E):
1138
+ w_f32 = torch.randn(two_n, K)
1139
+ # Per-row, per-K-group quantization: scale [2N, G]
1140
+ w_re = w_f32.view(two_n, G, group_K)
1141
+ abs_max = w_re.abs().amax(dim=2, keepdim=True).clamp(min=1e-7) # [2N, G, 1]
1142
+ scale = (abs_max / FP8_W8A8_MAX).squeeze(2) # [2N, G]
1143
+ w_q = (
1144
+ (w_re / abs_max).clamp(-FP8_W8A8_MAX, FP8_W8A8_MAX).to(torch.float8_e4m3fn)
1145
+ )
1146
+ w_list.append(w_q.view(two_n, K).contiguous())
1147
+ s_list.append(scale.float())
1148
+ return torch.stack(w_list), torch.stack(s_list) # [E, 2N, K], [E, 2N, G]
1149
+
1150
+
1151
+def _make_fp8_w8a8_weight_w2(
1152
+ E: int,
1153
+ K: int,
1154
+ N: int,
1155
+ group_K: int = FP8_W8A8_QUANT_GROUP,
1156
+ group_N: int = FP8_W8A8_QUANT_GROUP,
1157
+) -> tuple[torch.Tensor, torch.Tensor]:
1158
+ """Create w2 [E, K, N] FP8 with block scales [E, K//gK, N//gN]."""
1159
+ nK = K // group_K
1160
+ nN = N // group_N
1161
+ w_list, s_list = [], []
1162
+ for _ in range(E):
1163
+ w_f32 = torch.randn(K, N)
1164
+ w_re = w_f32.view(nK, group_K, nN, group_N)
1165
+ abs_max = w_re.abs().amax(dim=(1, 3), keepdim=True).clamp(min=1e-7)
1166
+ scale = (abs_max / FP8_W8A8_MAX).squeeze(1).squeeze(2) # [nK, nN]
1167
+ w_q = (
1168
+ (w_re / abs_max).clamp(-FP8_W8A8_MAX, FP8_W8A8_MAX).to(torch.float8_e4m3fn)
1169
+ )
1170
+ w_list.append(w_q.view(K, N).contiguous())
1171
+ s_list.append(scale.float())
1172
+ return torch.stack(w_list), torch.stack(s_list) # [E, K, N], [E, K//gK, N//gN]
1173
+
1174
+
1175
+def _pack_fp8_w8a8_w13(
1176
+ w13: torch.Tensor, # [E, 2N, K] FP8
1177
+ w13_scale: torch.Tensor, # [E, 2N, G] float32
1178
+) -> tuple[torch.Tensor, torch.Tensor]:
1179
+ """Pack w13 for CPU FP8 W8A8 MoE kernel using float8_linear_prepack_cpu."""
1180
+ E = w13.size(0)
1181
+ packed_list, scale_list = [], []
1182
+ for i in range(E):
1183
+ pw, ps = torch.ops._C.float8_linear_prepack_cpu(
1184
+ w13[i].contiguous(), w13_scale[i].contiguous()
1185
+ )
1186
+ packed_list.append(pw)
1187
+ scale_list.append(ps)
1188
+ return torch.stack(packed_list), torch.stack(scale_list)
1189
+
1190
+
1191
+def _ref_fp8_w8a8_moe(
1192
+ hidden_states: torch.Tensor, # BF16 [M, K_hidden] (after FP8 round-trip dequant)
1193
+ w13: torch.Tensor, # FP8 [E, 2N, K_hidden]
1194
+ w13_scale: torch.Tensor, # float32 [E, 2N, G] (G = K_hidden//group_K)
1195
+ w2: torch.Tensor, # FP8 [E, K_hidden, N]
1196
+ w2_scale: torch.Tensor, # float32 [E, K_hidden//gK, N//gN]
1197
+ topk_weights: torch.Tensor, # float32 [M, top_k]
1198
+ topk_ids: torch.Tensor, # int64 [M, top_k]
1199
+ N: int,
1200
+ group_K: int = FP8_W8A8_QUANT_GROUP,
1201
+ group_N: int = FP8_W8A8_QUANT_GROUP,
1202
+) -> torch.Tensor:
1203
+ """Reference implementation: dequantize weights then compute MoE."""
1204
+ M, K_hidden = hidden_states.shape
1205
+ top_k = topk_ids.shape[1]
1206
+ output = torch.zeros(M, K_hidden, dtype=torch.bfloat16)
1207
+
1208
+ for tok in range(M):
1209
+ x = hidden_states[tok].float() # [K_hidden]
1210
+ for k_idx in range(top_k):
1211
+ expert_id = topk_ids[tok, k_idx].item()
1212
+ w = topk_weights[tok, k_idx].item()
1213
+
1214
+ # Stage 1: x @ w13.T — gate and up projections
1215
+ w13_e = w13[expert_id].float() # [2N, K_hidden]
1216
+ ws_e = w13_scale[expert_id].float() # [2N, G]
1217
+ G = ws_e.shape[1]
1218
+ w13_dq = torch.zeros_like(w13_e)
1219
+ for g in range(G):
1220
+ c0, c1 = g * group_K, (g + 1) * group_K
1221
+ w13_dq[:, c0:c1] = w13_e[:, c0:c1] * ws_e[:, g : g + 1]
1222
+ gate_up = (x.unsqueeze(0) @ w13_dq.T).squeeze(0) # [2N]
1223
+
1224
+ gate = gate_up[:N]
1225
+ up = gate_up[N:]
1226
+ act = F.silu(gate) * up # [N]
1227
+
1228
+ # Stage 2: act @ w2.T — down projection
1229
+ w2_e = w2[expert_id].float() # [K_hidden, N]
1230
+ ws2_e = w2_scale[expert_id].float() # [K_hidden//gK, N//gN]
1231
+ nK2 = K_hidden // group_K
1232
+ nN2 = N // group_N
1233
+ w2_dq = torch.zeros_like(w2_e)
1234
+ for gi in range(nK2):
1235
+ for gj in range(nN2):
1236
+ r0, r1 = gi * group_K, (gi + 1) * group_K
1237
+ c0, c1 = gj * group_N, (gj + 1) * group_N
1238
+ w2_dq[r0:r1, c0:c1] = w2_e[r0:r1, c0:c1] * ws2_e[gi, gj]
1239
+ out_e = (act.unsqueeze(0) @ w2_dq.T).squeeze(0) # [K_hidden]
1240
+
1241
+ output[tok] += (w * out_e).to(torch.bfloat16)
1242
+
1243
+ return output
1244
+
1245
+
1246
+@requires_cpu_fp8_w8a8
1247
+@pytest.mark.parametrize(
1248
+ "E,N,K,M,top_k",
1249
+ [
1250
+ # N and K must be multiples of FP8_W8A8_QUANT_GROUP=128
1251
+ (4, 128, 256, 1, 2), # single token (decode phase)
1252
+ (4, 128, 256, 8, 2),
1253
+ (8, 256, 512, 16, 2),
1254
+ (4, 256, 512, 4, 1),
1255
+ ],
1256
+)
1257
+def test_fp8_w8a8_cpu_fused_moe(E: int, N: int, K: int, M: int, top_k: int):
1258
+ """Test fused_experts_cpu FP8_W8A8 shape/dtype and accuracy vs a
1259
+ dequantized BF16 reference."""
1260
+ set_random_seed(42)
1261
+
1262
+ w13, w13_scale = _make_fp8_w8a8_weight_w13(E, 2 * N, K)
1263
+ w2, w2_scale = _make_fp8_w8a8_weight_w2(E, K, N)
1264
+
1265
+ packed_w13, packed_w13_scale = _pack_fp8_w8a8_w13(w13, w13_scale)
1266
+ packed_w2 = _prepack_experts(w2)
1267
+
1268
+ hidden_states = torch.randn(M, K, dtype=torch.bfloat16) * 0.1
1269
+ x_fp8, x_scales = torch.ops._C.quantize_fp8e4m3_vec(hidden_states, True, None)
1270
+ assert x_fp8.dtype == torch.float8_e4m3fn
1271
+ assert x_scales.shape == (M,)
1272
+
1273
+ gen = torch.Generator().manual_seed(0)
1274
+ topk_weights = torch.softmax(torch.randn(M, top_k, generator=gen), dim=-1).float()
1275
+ topk_ids = torch.zeros(M, top_k, dtype=torch.int32)
1276
+ for i in range(M):
1277
+ topk_ids[i] = torch.randperm(E, generator=torch.Generator().manual_seed(i))[
1278
+ :top_k
1279
+ ].int()
1280
+
1281
+ output = torch.empty(M, K, dtype=torch.bfloat16)
1282
+ ops.fused_experts_cpu(
1283
+ output,
1284
+ x_fp8,
1285
+ packed_w13,
1286
+ packed_w2,
1287
+ topk_weights,
1288
+ topk_ids,
1289
+ ops.CPUQuantMethod.FP8_W8A8,
1290
+ packed_w13_scale, # w1_scale: [E, Nc, G, BLOCK_N]
1291
+ w2_scale, # w2_scale: [E, K//128, N//128]
1292
+ None, # w1_zero
1293
+ None, # w2_zero
1294
+ [FP8_W8A8_QUANT_GROUP, FP8_W8A8_QUANT_GROUP], # block_size
1295
+ None, # w1_bias
1296
+ None, # w2_bias
1297
+ None, # alpha
1298
+ None, # limit
1299
+ True, # is_vnni (weights already packed)
1300
+ x_scales, # a1_scale: per-token FP8 activation scales
1301
+ )
1302
+
1303
+ assert output.shape == (M, K)
1304
+ assert output.dtype == torch.bfloat16
1305
+ assert not torch.isnan(output).any()
1306
+ assert not torch.isinf(output).any()
1307
+
1308
+ x_dequant = (x_fp8.float() * x_scales.unsqueeze(1)).bfloat16()
1309
+ ref = _ref_fp8_w8a8_moe(
1310
+ x_dequant, w13, w13_scale, w2, w2_scale, topk_weights, topk_ids.long(), N
1311
+ )
1312
+ torch.testing.assert_close(output.float(), ref.float(), atol=1e-2, rtol=1e-2)
1313
+
1314
+
1315
if __name__ == "__main__":
1316
sys.exit(pytest.main([__file__, "-v"]))
tests/kernels/quantization/test_cpu_fp8_scaled_mm.py
+225
-4
@@ -1,6 +1,10 @@
1
# SPDX-License-Identifier: Apache-2.0
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
-"""Tests for CPU FP8 W8A16 block-scaled GEMM kernel (fp8_scaled_mm_cpu).
3
+"""Tests for CPU FP8 scaled-mm GEMM kernels.
4
+
5
+Covers:
6
+ * fp8_scaled_mm_cpu (W8A16)
7
+ * fp8_scaled_mm_with_quant (W8A8)
8
9
Run `pytest tests/kernels/quantization/test_cpu_fp8_scaled_mm.py -v`.
10
"""
@@ -23,19 +27,31 @@ from vllm.platforms import current_platform
27
if not current_platform.is_cpu():
28
pytest.skip("skipping CPU-only tests", allow_module_level=True)
29
26
-if not ops._supports_cpu_fp8_w8a16:
27
- pytest.skip("fp8_scaled_mm_cpu op not available", allow_module_level=True)
30
+requires_cpu_fp8_w8a16 = pytest.mark.skipif(
31
+ not ops._supports_cpu_fp8_w8a16, reason="fp8_scaled_mm_cpu op not available"
32
+)
33
+requires_cpu_fp8_w8a8 = pytest.mark.skipif(
34
+ not ops.cpu_has_amx_fp8(),
35
+ reason="requires native AMX-FP8 hardware",
36
+)
37
38
if not torch.cpu._is_amx_tile_supported():
39
pytest.skip("requires AMX tile support", allow_module_level=True)
40
41
+FP8_MAX = torch.finfo(torch.float8_e4m3fn).max
42
BLOCK_SIZE = [128, 128]
43
+GROUP_SIZE = 128
44
45
46
def cdiv(a: int, b: int) -> int:
47
return -(a // -b)
48
49
50
+# ---------------------------------------------------------------------------
51
+# W8A16 helpers
52
+# ---------------------------------------------------------------------------
53
+
54
+
55
def quantize_weight_block_fp8(
56
weight: torch.Tensor,
57
block_size: list[int],
@@ -126,7 +142,7 @@ def ref_fp8_block_scaled_mm(
142
143
144
# ---------------------------------------------------------------------------
129
-# Test parameters
145
+# W8A16 test: fp8_scaled_mm_cpu
146
# ---------------------------------------------------------------------------
147
M_SIZES = [1, 4, 16, 64, 128]
148
# (N, K) — weight shape is [N, K], output has N columns.
@@ -141,6 +157,7 @@ NK_SIZES = [
157
]
158
159
160
+@requires_cpu_fp8_w8a16
161
@pytest.mark.parametrize("M", M_SIZES)
162
@pytest.mark.parametrize("N,K", NK_SIZES)
163
@pytest.mark.parametrize("use_bias", [False, True])
@@ -200,6 +217,77 @@ def ref_fp8_per_tensor_scaled_mm(
217
return out.to(out_dtype)
218
219
220
+# ---------------------------------------------------------------------------
221
+# W8A8 helpers
222
+# ---------------------------------------------------------------------------
223
+
224
+
225
+def quantize_weight_per_tensor(
226
+ weight: torch.Tensor,
227
+) -> tuple[torch.Tensor, torch.Tensor]:
228
+ """Quantize weight [N, K] to FP8 with a single per-tensor scale, broadcast
229
+ to [N, 1] (the shape float8_linear_prepack_cpu expects)."""
230
+ N = weight.shape[0]
231
+ abs_max = weight.abs().max()
232
+ scale = (abs_max / FP8_MAX).clamp(min=1e-7)
233
+ q = (weight.float() / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
234
+ return q, scale.expand(N).contiguous().view(N, 1)
235
+
236
+
237
+def quantize_weight_per_channel(
238
+ weight: torch.Tensor,
239
+) -> tuple[torch.Tensor, torch.Tensor]:
240
+ """Quantize weight [N, K] to FP8 with per-channel (per-row) scales [N, 1]."""
241
+ abs_max = weight.abs().amax(dim=1, keepdim=True)
242
+ scale = (abs_max / FP8_MAX).clamp(min=1e-7)
243
+ q = (weight.float() / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
244
+ return q, scale.float()
245
+
246
+
247
+def quantize_weight_per_group(
248
+ weight: torch.Tensor,
249
+ group_size: int = GROUP_SIZE,
250
+) -> tuple[torch.Tensor, torch.Tensor]:
251
+ """Quantize weight [N, K] to FP8 with per-group (128-block) scales [N, G]."""
252
+ N, K = weight.shape
253
+ G = K // group_size
254
+ w_groups = weight.view(N, G, group_size)
255
+ abs_max = w_groups.abs().amax(dim=2, keepdim=True)
256
+ scale = (abs_max / FP8_MAX).clamp(min=1e-7)
257
+ q = (w_groups.float() / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
258
+ return q.view(N, K), scale.squeeze(2).float()
259
+
260
+
261
+def quantize_act_per_token(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
262
+ """Reference per-token (row) dynamic FP8 quantization of activations."""
263
+ x_f = x.float()
264
+ scale = (x_f.abs().amax(dim=1, keepdim=True) / FP8_MAX).clamp(min=1e-7)
265
+ scaled = (x_f / scale).clamp(-FP8_MAX, FP8_MAX)
266
+ # Round via fp16 to match the AVX10.2 kernel path.
267
+ q = scaled.half().to(torch.float8_e4m3fn)
268
+ return q, scale
269
+
270
+
271
+def reference_static_fp8_linear(
272
+ x: torch.Tensor, # BF16 [M, K]
273
+ weight_fp8: torch.Tensor, # FP8 [N, K]
274
+ weight_scale: torch.Tensor, # float32 [N, 1]
275
+ act_scale: torch.Tensor, # float32 scalar
276
+ bias: torch.Tensor | None,
277
+ out_dtype: torch.dtype,
278
+) -> torch.Tensor:
279
+ """Static per-tensor: quantize/dequant x with act_scale, dequant weight, matmul."""
280
+ x_scaled = (x.float() / act_scale.item()).clamp(-FP8_MAX, FP8_MAX)
281
+ # Round via fp16 to match the AVX10.2 kernel path.
282
+ x_q = x_scaled.half().to(torch.float8_e4m3fn)
283
+ x_dq = x_q.float() * act_scale.item()
284
+ w_dq = weight_fp8.float() * weight_scale.view(-1, 1)
285
+ out = torch.mm(x_dq, w_dq.t())
286
+ if bias is not None:
287
+ out = out + bias.float()
288
+ return out.to(out_dtype)
289
+
290
+
291
NK_SIZES_PER_TENSOR = [
292
(32, 64),
293
(5120, 5120),
@@ -274,3 +362,136 @@ def test_cpu_fp8_per_tensor_kernel_rejects_non_multiple_of_32_n(n: int):
362
)
363
supported, _ = CPUFp8PerTensorScaledMMLinearKernel.can_implement(config)
364
assert not supported
365
+
366
+
367
+def reference_dynamic_fp8_linear(
368
+ x: torch.Tensor, # BF16 [M, K]
369
+ weight_fp8: torch.Tensor, # FP8 [N, K]
370
+ weight_scale: torch.Tensor, # float32 [N, G]
371
+ bias: torch.Tensor | None,
372
+ out_dtype: torch.dtype,
373
+) -> torch.Tensor:
374
+ """Dynamic per-token act x per-group weight: quantize x per-token,
375
+ dequant weight per-group, matmul."""
376
+ N, K = weight_fp8.shape
377
+ G = weight_scale.shape[1]
378
+ group_size = K // G
379
+ x_q, x_scale = quantize_act_per_token(x)
380
+ x_dq = x_q.float() * x_scale
381
+ w_dq = (
382
+ weight_fp8.float().view(N, G, group_size) * weight_scale.view(N, G, 1)
383
+ ).view(N, K)
384
+ out = torch.mm(x_dq, w_dq.t())
385
+ if bias is not None:
386
+ out = out + bias.float()
387
+ return out.to(out_dtype)
388
+
389
+
390
+# ---------------------------------------------------------------------------
391
+# W8A8 tests: fp8_scaled_mm_with_quant
392
+# ---------------------------------------------------------------------------
393
+W8A8_M_SIZES = [1, 4, 16, 64]
394
+W8A8_NK_SIZES = [(128, 256), (256, 512), (64, 128)]
395
+
396
+
397
+@requires_cpu_fp8_w8a8
398
+@pytest.mark.parametrize("static_activation", [True, False])
399
+@pytest.mark.parametrize("M", W8A8_M_SIZES)
400
+@pytest.mark.parametrize("N,K", W8A8_NK_SIZES)
401
+@pytest.mark.parametrize("use_bias", [False, True])
402
+def test_cpu_fp8_w8a8_per_tensor(
403
+ M: int, N: int, K: int, use_bias: bool, static_activation: bool
404
+):
405
+ """CPUFP8W8A8ScaledMMLinearKernel with per-tensor weight scale."""
406
+ torch.manual_seed(42)
407
+ out_dtype = torch.bfloat16
408
+
409
+ x = torch.randn(M, K, dtype=torch.bfloat16)
410
+ weight = torch.randn(N, K, dtype=torch.bfloat16)
411
+ bias = torch.randn(N, dtype=torch.float32) if use_bias else None
412
+
413
+ w_fp8, w_scale = quantize_weight_per_tensor(weight)
414
+ packed_weight, packed_scale = torch.ops._C.float8_linear_prepack_cpu(w_fp8, w_scale)
415
+
416
+ if static_activation:
417
+ act_scale = (x.float().abs().max() / FP8_MAX).clamp(min=1e-7).view(1)
418
+ kernel_out = ops.fp8_scaled_mm_with_quant(
419
+ x, act_scale, False, packed_weight, packed_scale, bias, out_dtype
420
+ )
421
+ ref_out = reference_static_fp8_linear(
422
+ x, w_fp8, w_scale, act_scale, bias, out_dtype
423
+ )
424
+ atol = 0.05
425
+ else:
426
+ kernel_out = ops.fp8_scaled_mm_with_quant(
427
+ x, None, True, packed_weight, packed_scale, bias, out_dtype
428
+ )
429
+ ref_out = reference_dynamic_fp8_linear(x, w_fp8, w_scale, bias, out_dtype)
430
+ atol = 0.05
431
+
432
+ assert kernel_out.dtype == out_dtype
433
+ torch.testing.assert_close(kernel_out, ref_out, rtol=0.05, atol=atol)
434
+
435
+
436
+@requires_cpu_fp8_w8a8
437
+@pytest.mark.parametrize("static_activation", [True, False])
438
+@pytest.mark.parametrize("M", W8A8_M_SIZES)
439
+@pytest.mark.parametrize("N,K", W8A8_NK_SIZES)
440
+@pytest.mark.parametrize("use_bias", [False, True])
441
+def test_cpu_fp8_w8a8_per_channel(
442
+ M: int, N: int, K: int, use_bias: bool, static_activation: bool
443
+):
444
+ """CPUFP8W8A8ScaledMMLinearKernel with per-channel weight scale."""
445
+ torch.manual_seed(42)
446
+ out_dtype = torch.bfloat16
447
+
448
+ x = torch.randn(M, K, dtype=torch.bfloat16)
449
+ weight = torch.randn(N, K, dtype=torch.bfloat16)
450
+ bias = torch.randn(N, dtype=torch.float32) if use_bias else None
451
+
452
+ w_fp8, w_scale = quantize_weight_per_channel(weight)
453
+ packed_weight, packed_scale = torch.ops._C.float8_linear_prepack_cpu(w_fp8, w_scale)
454
+
455
+ if static_activation:
456
+ act_scale = (x.float().abs().max() / FP8_MAX).clamp(min=1e-7).view(1)
457
+ kernel_out = ops.fp8_scaled_mm_with_quant(
458
+ x, act_scale, False, packed_weight, packed_scale, bias, out_dtype
459
+ )
460
+ ref_out = reference_static_fp8_linear(
461
+ x, w_fp8, w_scale, act_scale, bias, out_dtype
462
+ )
463
+ atol = 0.05
464
+ else:
465
+ kernel_out = ops.fp8_scaled_mm_with_quant(
466
+ x, None, True, packed_weight, packed_scale, bias, out_dtype
467
+ )
468
+ ref_out = reference_dynamic_fp8_linear(x, w_fp8, w_scale, bias, out_dtype)
469
+ atol = 0.05
470
+
471
+ assert kernel_out.dtype == out_dtype
472
+ torch.testing.assert_close(kernel_out, ref_out, rtol=0.05, atol=atol)
473
+
474
+
475
+@requires_cpu_fp8_w8a8
476
+@pytest.mark.parametrize("M", W8A8_M_SIZES)
477
+@pytest.mark.parametrize("N,K", W8A8_NK_SIZES)
478
+@pytest.mark.parametrize("use_bias", [False, True])
479
+def test_cpu_fp8_w8a8_per_group(M: int, N: int, K: int, use_bias: bool):
480
+ """CPUFp8W8A8BlockScaledMMKernel: per-group (128-block) weight scale +
481
+ dynamic per-token activation scale."""
482
+ torch.manual_seed(42)
483
+ out_dtype = torch.bfloat16
484
+
485
+ x = torch.randn(M, K, dtype=torch.bfloat16)
486
+ weight = torch.randn(N, K, dtype=torch.bfloat16)
487
+ bias = torch.randn(N, dtype=torch.float32) if use_bias else None
488
+
489
+ w_fp8, w_scale = quantize_weight_per_group(weight)
490
+ packed_weight, packed_scale = torch.ops._C.float8_linear_prepack_cpu(w_fp8, w_scale)
491
+ kernel_out = ops.fp8_scaled_mm_with_quant(
492
+ x, None, True, packed_weight, packed_scale, bias, out_dtype
493
+ )
494
+ ref_out = reference_dynamic_fp8_linear(x, w_fp8, w_scale, bias, out_dtype)
495
+
496
+ assert kernel_out.dtype == out_dtype
497
+ torch.testing.assert_close(kernel_out, ref_out, rtol=0.05, atol=0.05)
vllm/_custom_ops.py
+44
@@ -2,6 +2,7 @@
2
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
3
4
from enum import IntEnum
5
+from functools import cache
6
from typing import TYPE_CHECKING, Literal
7
8
import torch
@@ -3384,6 +3385,7 @@ class CPUQuantMethod(IntEnum):
3385
FP8_W8A16 = 2
3386
INT4_W4A8 = 3
3387
MXFP4 = 4
3388
+ FP8_W8A8 = 5
3389
3390
3391
if hasattr(torch.ops._C, "dynamic_4bit_int_moe"):
@@ -3422,6 +3424,7 @@ def fused_experts_cpu(
3424
alpha: float | None = None,
3425
limit: float | None = None,
3426
is_vnni: bool = True,
3427
+ a1_scale: torch.Tensor | None = None,
3428
) -> None:
3429
torch.ops._C.fused_experts_cpu(
3430
out,
@@ -3435,6 +3438,7 @@ def fused_experts_cpu(
3438
w2_scale,
3439
w1_zero,
3440
w2_zero,
3441
+ a1_scale,
3442
block_size,
3443
w1_bias,
3444
w2_bias,
@@ -3564,6 +3568,46 @@ def fp8_scaled_mm_cpu(
3568
)
3569
3570
3571
+# FP8 W8A8 CPU kernels
3572
+@cache
3573
+def cpu_has_amx_fp8() -> bool:
3574
+ """Whether this CPU has native AMX-FP8 MMA support."""
3575
+ if not hasattr(torch.ops._C, "cpu_has_amx_fp8"):
3576
+ return False
3577
+ return bool(torch.ops._C.cpu_has_amx_fp8())
3578
+
3579
+
3580
+if hasattr(torch.ops._C, "fp8_scaled_mm_with_quant"):
3581
+
3582
+ @register_fake("_C::fp8_scaled_mm_with_quant")
3583
+ def fp8_scaled_mm_with_quant_fake(
3584
+ act: torch.Tensor,
3585
+ act_scales: torch.Tensor | None,
3586
+ channelwise: bool,
3587
+ weight: torch.Tensor,
3588
+ weight_scales: torch.Tensor,
3589
+ bias: torch.Tensor | None,
3590
+ output_dtype: torch.dtype,
3591
+ ) -> torch.Tensor:
3592
+ M = act.reshape(-1, act.size(-1)).size(0)
3593
+ N = weight.size(0) * weight.size(-1)
3594
+ return torch.empty((M, N), dtype=output_dtype, device=act.device)
3595
+
3596
+
3597
+def fp8_scaled_mm_with_quant(
3598
+ act: torch.Tensor,
3599
+ act_scales: torch.Tensor | None,
3600
+ channelwise: bool,
3601
+ weight: torch.Tensor,
3602
+ weight_scales: torch.Tensor,
3603
+ bias: torch.Tensor | None,
3604
+ output_dtype: torch.dtype,
3605
+) -> torch.Tensor:
3606
+ return torch.ops._C.fp8_scaled_mm_with_quant(
3607
+ act, act_scales, channelwise, weight, weight_scales, bias, output_dtype
3608
+ )
3609
+
3610
+
3611
def chunk_gated_delta_rule_cpu(
3612
query: torch.Tensor,
3613
key: torch.Tensor,
vllm/model_executor/kernels/linear/__init__.py
+4
@@ -186,6 +186,8 @@ from vllm.model_executor.kernels.linear.scaled_mm.b12x import (
186
from vllm.model_executor.kernels.linear.scaled_mm.cpu import (
187
CPUFp8BlockScaledMMKernel,
188
CPUFp8PerTensorScaledMMLinearKernel,
189
+ CPUFp8W8A8BlockScaledMMKernel,
190
+ CPUFP8W8A8ScaledMMLinearKernel,
191
CPUInt8ScaledMMLinearKernel,
192
)
193
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import (
@@ -436,6 +438,7 @@ _POSSIBLE_FP8_KERNELS: dict[PlatformEnum, list[type[FP8ScaledMMLinearKernel]]] =
438
ChannelWiseTorchFP8ScaledMMLinearKernel,
439
],
440
PlatformEnum.CPU: [
441
+ CPUFP8W8A8ScaledMMLinearKernel,
442
CPUFp8PerTensorScaledMMLinearKernel,
443
PerTensorTorchFP8ScaledMMLinearKernel,
444
ChannelWiseTorchFP8ScaledMMLinearKernel,
@@ -468,6 +471,7 @@ _POSSIBLE_FP8_BLOCK_KERNELS: dict[
471
TritonFp8BlockScaledMMKernel,
472
],
473
PlatformEnum.CPU: [
474
+ CPUFp8W8A8BlockScaledMMKernel, # W8A8 preferred; falls back to W8A16 below
475
CPUFp8BlockScaledMMKernel,
476
],
477
PlatformEnum.XPU: [
vllm/model_executor/kernels/linear/scaled_mm/__init__.py
+2
@@ -10,6 +10,7 @@ from vllm.model_executor.kernels.linear.scaled_mm.BlockScaledMMLinearKernel impo
10
from vllm.model_executor.kernels.linear.scaled_mm.cpu import (
11
CPUFp8BlockScaledMMKernel,
12
CPUFp8PerTensorScaledMMLinearKernel,
13
+ CPUFP8W8A8ScaledMMLinearKernel,
14
CPUInt8ScaledMMLinearKernel,
15
)
16
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import (
@@ -69,5 +70,6 @@ __all__ = [
70
"Fp8BlockScaledMMLinearKernel",
71
"CPUFp8BlockScaledMMKernel",
72
"CPUFp8PerTensorScaledMMLinearKernel",
73
+ "CPUFP8W8A8ScaledMMLinearKernel",
74
"XPUFp8BlockScaledMMKernel",
75
]
vllm/model_executor/kernels/linear/scaled_mm/cpu.py
+303
-7
@@ -270,12 +270,17 @@ class CPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
270
# Skip the base class process (FP8 padding / fnuz normalization)
271
# which is GPU-oriented. Instead, VNNI-prepack weights for AMX.
272
params = self._get_layer_params(layer)
273
- packed_weight = torch.ops._C.convert_weight_packed(params.weight)
274
- replace_parameter(
275
- layer,
276
- params.WEIGHT,
277
- torch.nn.Parameter(packed_weight, requires_grad=False),
278
- )
273
+
274
+ # MLA's kv_b_proj (see `_cpu_skip_gemm_dispatch`): MLA reads this
275
+ # weight to derive W_UK/W_UV, so keep the checkpoint [N, K] layout and
276
+ # let the kernel pack on the fly instead.
277
+ if not getattr(layer, "_cpu_skip_gemm_dispatch", False):
278
+ packed_weight = torch.ops._C.convert_weight_packed(params.weight)
279
+ replace_parameter(
280
+ layer,
281
+ params.WEIGHT,
282
+ torch.nn.Parameter(packed_weight, requires_grad=False),
283
+ )
284
285
# Re-wrap scale as a plain Parameter so the kernel can read it
286
# without weight-loader metadata interfering.
@@ -320,7 +325,7 @@ class CPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
325
list(self.weight_group_shape),
326
bias,
327
x.dtype,
323
- True, # is_vnni (weight already prepacked)
328
+ not getattr(layer, "_cpu_skip_gemm_dispatch", False), # is_vnni
329
)
330
return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
331
@@ -430,3 +435,294 @@ class CPUFp8PerTensorScaledMMLinearKernel(FP8ScaledMMLinearKernel):
435
raise NotImplementedError(
436
"CPUFp8PerTensorScaledMMLinearKernel overrides apply_weights directly."
437
)
438
+
439
+
440
+class CPUFp8W8A8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
441
+ """FP8 W8A8 block-quantized GEMM with dynamic per-token activation quant on CPU."""
442
+
443
+ # Activation quant is done inside fp8_scaled_mm_with_quant;
444
+ # the base class must not apply a second quant step.
445
+ apply_input_quant = False
446
+
447
+ @classmethod
448
+ def is_supported(
449
+ cls, compute_capability: int | None = None
450
+ ) -> tuple[bool, str | None]:
451
+ if not current_platform.is_cpu():
452
+ return False, "requires CPU platform."
453
+ if not torch.cpu._is_amx_tile_supported():
454
+ return False, "requires AMX tile support (Sapphire Rapids or newer)."
455
+ if not ops.cpu_has_amx_fp8():
456
+ return False, (
457
+ "requires native AMX-FP8 hardware; falls back "
458
+ "to CPUFp8BlockScaledMMKernel (W8A16) otherwise."
459
+ )
460
+ return True, None
461
+
462
+ @classmethod
463
+ def can_implement(
464
+ cls, config: FP8ScaledMMLinearLayerConfig
465
+ ) -> tuple[bool, str | None]:
466
+ # Same block-shape requirements as the W8A16 sibling kernel.
467
+ weight_gs = config.weight_quant_key.scale.group_shape
468
+ if weight_gs.col <= 0 or weight_gs.col != 128:
469
+ return False, (
470
+ "CPU FP8 W8A8 block kernel requires K-dimension block size of 128, "
471
+ f"got {weight_gs.col}."
472
+ )
473
+ if weight_gs.row <= 0 or weight_gs.row % 32 != 0:
474
+ return False, (
475
+ "CPU FP8 W8A8 block kernel requires N-dimension block size to be "
476
+ f"a positive multiple of 32, got {weight_gs.row}."
477
+ )
478
+ if config.out_dtype not in (torch.bfloat16, torch.float32):
479
+ return False, "Only bfloat16/float32 output dtype supported."
480
+ n = config.weight_shape[0]
481
+ if n % 32 != 0:
482
+ # float8_linear_prepack_cpu tiles N in chunks of 32 and cannot
483
+ # handle a remainder tile smaller than that.
484
+ return False, f"requires weight output dim (N={n}) to be a multiple of 32."
485
+ return True, None
486
+
487
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
488
+ """Prepack block-quantized FP8 weight for W8A8 AMX BRGEMM.
489
+
490
+ For block-quant FP8 models weight is stored as [N, K] (NOT transposed,
491
+ unlike non-block FP8 where vLLM stores [K, N]).
492
+
493
+ float8_linear_prepack_cpu expects:
494
+ weight: [N, K]
495
+ scales: [N, G] where G = K / block_k
496
+
497
+ weight_scale_inv from the checkpoint has shape [ceil(N/block_n), G],
498
+ so we expand rows from ceil(N/block_n) to N via repeat_interleave.
499
+ """
500
+ params = self._get_layer_params(layer)
501
+ weight = params.weight # [N, K]
502
+
503
+ # MLA's kv_b_proj (see `_cpu_skip_gemm_dispatch`): MLA reads this
504
+ # weight to derive W_UK/W_UV, so keep the checkpoint [N, K] layout and
505
+ # its block scales; apply_weights falls back to the W8A16 op.
506
+ if getattr(layer, "_cpu_skip_gemm_dispatch", False):
507
+ return
508
+
509
+ weight_scale = (
510
+ params.weight_scale_inv
511
+ if params.weight_scale_inv is not None
512
+ else params.weight_scale
513
+ ) # [ceil(N/block_n), ceil(K/block_k)]
514
+ assert weight_scale is not None, "FP8 W8A8 block kernel requires weight scale."
515
+
516
+ N = weight.shape[0]
517
+ block_n = self.weight_group_shape.row # e.g. 128
518
+
519
+ # Expand scale rows: [ceil(N/block_n), G] → [N, G]
520
+ if weight_scale.size(0) != N:
521
+ weight_scale_expanded = weight_scale.repeat_interleave(block_n, dim=0)[
522
+ :N, :
523
+ ].contiguous()
524
+ else:
525
+ weight_scale_expanded = weight_scale.contiguous()
526
+
527
+ packed_weight, packed_scales = torch.ops._C.float8_linear_prepack_cpu(
528
+ weight, weight_scale_expanded.to(torch.float32)
529
+ )
530
+
531
+ scale_attr = (
532
+ params.WEIGHT_SCALE_INV
533
+ if params.weight_scale_inv is not None
534
+ else params.WEIGHT_SCALE
535
+ )
536
+ replace_parameter(
537
+ layer,
538
+ params.WEIGHT,
539
+ torch.nn.Parameter(packed_weight, requires_grad=False),
540
+ )
541
+ replace_parameter(
542
+ layer,
543
+ scale_attr,
544
+ torch.nn.Parameter(packed_scales, requires_grad=False),
545
+ )
546
+
547
+ def apply_weights(
548
+ self,
549
+ layer: torch.nn.Module,
550
+ x: torch.Tensor,
551
+ bias: torch.Tensor | None = None,
552
+ **kwargs,
553
+ ) -> torch.Tensor:
554
+ params = self._get_layer_params(layer)
555
+ weight_scale = (
556
+ params.weight_scale_inv
557
+ if params.weight_scale_inv is not None
558
+ else params.weight_scale
559
+ )
560
+
561
+ x_2d = x.reshape(-1, x.shape[-1]) if x.dim() > 2 else x
562
+ if getattr(layer, "_cpu_skip_gemm_dispatch", False):
563
+ # Unpacked weight: fp8_scaled_mm_with_quant has no is_vnni option,
564
+ # so fall back to the W8A16 op (BF16 activation, block-scaled FP8
565
+ # weight) for this layer.
566
+ out = torch.ops._C.fp8_scaled_mm_cpu(
567
+ x_2d,
568
+ params.weight,
569
+ weight_scale,
570
+ list(self.weight_group_shape),
571
+ bias,
572
+ x.dtype,
573
+ False, # is_vnni
574
+ )
575
+ return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
576
+
577
+ # act_scales=None + channelwise=True → dynamic per-token activation quant
578
+ out = ops.fp8_scaled_mm_with_quant(
579
+ x_2d,
580
+ None,
581
+ True,
582
+ params.weight,
583
+ weight_scale,
584
+ bias,
585
+ x.dtype,
586
+ )
587
+ return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
588
+
589
+ def apply_block_scaled_mm(
590
+ self,
591
+ A: torch.Tensor,
592
+ B: torch.Tensor,
593
+ As: torch.Tensor,
594
+ Bs: torch.Tensor,
595
+ ) -> torch.Tensor:
596
+ raise NotImplementedError(
597
+ "CPUFp8W8A8BlockScaledMMKernel overrides apply_weights directly."
598
+ )
599
+
600
+
601
+class CPUFP8W8A8ScaledMMLinearKernel(FP8ScaledMMLinearKernel):
602
+ """FP8 W8A8 GEMM with dynamic per-token activation quantization on CPU."""
603
+
604
+ @classmethod
605
+ def is_supported(
606
+ cls, compute_capability: int | None = None
607
+ ) -> tuple[bool, str | None]:
608
+ if not current_platform.is_cpu():
609
+ return False, "requires CPU platform."
610
+ if not torch.cpu._is_amx_tile_supported():
611
+ return False, "requires AMX tile support (Sapphire Rapids or newer)."
612
+ if not ops.cpu_has_amx_fp8():
613
+ return False, (
614
+ "requires native AMX-FP8 hardware (Diamond Rapids); falls back "
615
+ "to the Torch FP8 kernels otherwise."
616
+ )
617
+ return True, None
618
+
619
+ @classmethod
620
+ def can_implement(
621
+ cls, config: FP8ScaledMMLinearLayerConfig
622
+ ) -> tuple[bool, str | None]:
623
+ if config.out_dtype not in (torch.bfloat16, torch.float16, torch.float32):
624
+ return False, "Only bfloat16/float16/float32 output dtype supported."
625
+ # Reject block-quantized weights — those go to CPUFp8BlockScaledMMKernel
626
+ weight_gs = config.weight_quant_key.scale.group_shape
627
+ if weight_gs.col > 0 and weight_gs.row > 0:
628
+ return False, "Block-quantized weights not handled by FP8 W8A8 kernel."
629
+ n = config.weight_shape[0]
630
+ if n % 32 != 0:
631
+ # float8_linear_prepack_cpu tiles N in chunks of 32 and cannot
632
+ # handle a remainder tile smaller than that.
633
+ return False, f"requires weight output dim (N={n}) to be a multiple of 32."
634
+ return True, None
635
+
636
+ def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
637
+ """Prepack weights with float8_linear_prepack_cpu (VNNI + block layout)."""
638
+ w_name, ws_name, xs_name, _ = self.layer_param_names
639
+ weight = getattr(layer, w_name)
640
+ weight_scale = getattr(layer, ws_name)
641
+
642
+ # vLLM stores linear weight as [K, N], while the CPU prepack op expects [N, K].
643
+ N = weight.shape[-1]
644
+
645
+ # Ensure scale is 2D [N, G]
646
+ ws = weight_scale
647
+ if ws is None:
648
+ raise RuntimeError("FP8 W8A8 kernel requires weight scale.")
649
+ ws = ws.to(torch.float32)
650
+
651
+ # Fused modules may carry per-shard scales (e.g. QKV/MLP shards).
652
+ # Expand those to per-output-channel scales before prepack.
653
+ if (
654
+ len(getattr(layer, "logical_widths", [])) > 1
655
+ and ws.dim() <= 1
656
+ and ws.numel() == len(layer.logical_widths)
657
+ ):
658
+ ws = convert_to_channelwise(ws.reshape(-1), layer.logical_widths)
659
+
660
+ if ws.dim() == 0:
661
+ ws = ws.view(1, 1).expand(N, 1)
662
+ elif ws.dim() == 1:
663
+ if ws.numel() == 1:
664
+ ws = ws.view(1, 1).expand(N, 1)
665
+ elif ws.numel() == N:
666
+ ws = ws.unsqueeze(1) # [N] -> [N, 1]
667
+ else:
668
+ raise RuntimeError(
669
+ "FP8 W8A8 kernel got unsupported 1D weight scale shape: "
670
+ f"{tuple(ws.shape)} for N={N}."
671
+ )
672
+
673
+ # Align scale rows to output channels N.
674
+ if ws.size(0) != N:
675
+ if ws.size(0) == 1:
676
+ ws = ws.expand(N, ws.size(1)).contiguous()
677
+ else:
678
+ raise RuntimeError(
679
+ "FP8 W8A8 kernel got mismatched weight scale rows: "
680
+ f"ws_rows={ws.size(0)}, expected N={N}."
681
+ )
682
+
683
+ w_2d = weight.t().contiguous() # [N, K]
684
+ packed_weight, packed_scales = torch.ops._C.float8_linear_prepack_cpu(w_2d, ws)
685
+
686
+ replace_parameter(
687
+ layer,
688
+ w_name,
689
+ torch.nn.Parameter(packed_weight, requires_grad=False),
690
+ )
691
+ replace_parameter(
692
+ layer,
693
+ ws_name,
694
+ torch.nn.Parameter(packed_scales, requires_grad=False),
695
+ )
696
+
697
+ def apply_weights(
698
+ self,
699
+ layer: torch.nn.Module,
700
+ x: torch.Tensor,
701
+ bias: torch.Tensor | None = None,
702
+ **kwargs,
703
+ ) -> torch.Tensor:
704
+ w_name, ws_name, xs_name, _ = self.layer_param_names
705
+ weight = getattr(layer, w_name)
706
+ weight_scale = getattr(layer, ws_name)
707
+ # For static quantization, use pre-computed activation scale
708
+ act_scales = getattr(layer, xs_name, None) if xs_name else None
709
+
710
+ x_2d = x.reshape(-1, x.shape[-1]) if x.dim() > 2 else x
711
+ # channelwise=True: per-token (per-row) quantization when act_scales is None
712
+ # channelwise=False: per-tensor quantization when act_scales is provided
713
+ channelwise = act_scales is None
714
+ out = ops.fp8_scaled_mm_with_quant(
715
+ x_2d,
716
+ act_scales,
717
+ channelwise,
718
+ weight,
719
+ weight_scale,
720
+ bias,
721
+ x.dtype,
722
+ )
723
+ return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
724
+
725
+ def apply_scaled_mm(self, A, B, out_dtype, As, Bs, bias=None, **kwargs):
726
+ raise NotImplementedError(
727
+ "CPUFP8W8A8ScaledMMLinearKernel uses apply_weights directly."
728
+ )
vllm/model_executor/layers/attention/mla_attention.py
+2
-2
@@ -1055,7 +1055,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1055
mqa_q_nope,
1056
self.impl._w_uk_packed, # type: ignore[attr-defined]
1057
True,
1058
- None,
1058
+ self.impl._w_scale, # type: ignore[attr-defined]
1059
)
1060
mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
1061
else:
@@ -1401,7 +1401,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
1401
x,
1402
self.impl._w_uv_packed, # type: ignore[attr-defined]
1403
True,
1404
- None,
1404
+ self.impl._w_scale, # type: ignore[attr-defined]
1405
)
1406
else:
1407
# Multiply + Transpose (N, B, L) x (N, L, V)->(N, B, V)->(B, N, V)
vllm/model_executor/layers/fused_moe/experts/cpu_moe.py
+217
@@ -15,6 +15,7 @@ from vllm._custom_ops import (
15
convert_weight_packed_scale_zp,
16
cpu_fused_moe,
17
cpu_fused_moe_int8,
18
+ cpu_has_amx_fp8,
19
cpu_prepack_moe_weight,
20
cpu_prepack_moe_weight_int8,
21
fused_experts_cpu,
@@ -467,6 +468,7 @@ class CPUExpertsFp8(mk.FusedMoEExpertsModular):
468
RoutingMethodType.Default,
469
RoutingMethodType.Renormalize,
470
RoutingMethodType.RenormalizeNaive,
471
+ RoutingMethodType.DeepSeekV3,
472
RoutingMethodType.DeepseekV4,
473
RoutingMethodType.DeepSeekV3,
474
]
@@ -1612,3 +1614,218 @@ class ZenCPUExpertsInt8(mk.FusedMoEExpertsModular):
1614
self._w1_scale_bf16,
1615
self._w2_scale_bf16,
1616
)
1617
+
1618
+
1619
+# ===========================================================================
1620
+# FP8 W8A8 MoE
1621
+# ===========================================================================
1622
+
1623
+
1624
+def prepare_fp8_w8a8_moe_layer_for_cpu(
1625
+ w13: torch.Tensor,
1626
+ w2: torch.Tensor,
1627
+ w13_scale: torch.Tensor,
1628
+ w2_scale: torch.Tensor,
1629
+) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
1630
+ """Prepack FP8 W8A8 MoE weights for CPU kernel."""
1631
+ E = w13.size(0)
1632
+
1633
+ packed_w13_list = []
1634
+ packed_w13_scale_list = []
1635
+
1636
+ for i in range(E):
1637
+ w1_exp = w13[i] # [2N, K] FP8
1638
+ ws_exp = w13_scale[i] # [2N, K/128] or [2N_blocks, K/128]
1639
+
1640
+ # Align scale rows to weight rows if necessary (for block-128 checkpoints
1641
+ # where scale may have fewer rows: [2N/128, K/128] → [2N, K/128])
1642
+ if ws_exp.size(0) < w1_exp.size(0):
1643
+ repeat_factor = w1_exp.size(0) // ws_exp.size(0)
1644
+ ws_exp = torch.repeat_interleave(ws_exp, repeat_factor, dim=0)
1645
+ ws_exp = ws_exp[: w1_exp.size(0), :].contiguous()
1646
+
1647
+ pw, ps = torch.ops._C.float8_linear_prepack_cpu(w1_exp, ws_exp)
1648
+ packed_w13_list.append(pw)
1649
+ packed_w13_scale_list.append(ps)
1650
+
1651
+ packed_w13 = torch.stack(packed_w13_list)
1652
+ packed_w13_scale = torch.stack(packed_w13_scale_list)
1653
+
1654
+ # w2 uses the W8A16 path (BF16 activation × FP8 weight), VNNI packed
1655
+ packed_w2 = torch.ops._C.convert_weight_packed(w2)
1656
+ return packed_w13, packed_w13_scale, packed_w2, w2_scale
1657
+
1658
+
1659
+class CPUExpertsFp8W8A8(mk.FusedMoEExpertsModular):
1660
+ """CPU FP8 W8A8 block-quantized modular MoE experts."""
1661
+
1662
+ def __init__(
1663
+ self,
1664
+ moe_config: FusedMoEConfig,
1665
+ quant_config: FusedMoEQuantConfig,
1666
+ ):
1667
+ super().__init__(moe_config, quant_config)
1668
+
1669
+ @property
1670
+ def expects_unquantized_inputs(self) -> bool:
1671
+ # Receives BF16 hidden states; quantization to FP8 is done inside apply()
1672
+ return True
1673
+
1674
+ @staticmethod
1675
+ def activation_format() -> mk.FusedMoEActivationFormat:
1676
+ return mk.FusedMoEActivationFormat.Standard
1677
+
1678
+ @staticmethod
1679
+ def _supports_current_device() -> bool:
1680
+ # Requires native AMX-FP8 hardware.
1681
+ return current_platform.is_cpu() and cpu_has_amx_fp8()
1682
+
1683
+ @staticmethod
1684
+ def _supports_no_act_and_mul() -> bool:
1685
+ return False
1686
+
1687
+ @staticmethod
1688
+ def _supports_activation(activation: MoEActivation) -> bool:
1689
+ return activation == MoEActivation.SILU
1690
+
1691
+ @staticmethod
1692
+ def _supports_parallel_config(
1693
+ moe_parallel_config: FusedMoEParallelConfig,
1694
+ ) -> bool:
1695
+ return True
1696
+
1697
+ @staticmethod
1698
+ def _supports_quant_scheme(
1699
+ weight_key: QuantKey | None,
1700
+ activation_key: QuantKey | None,
1701
+ ) -> bool:
1702
+ # Block-quantized FP8.
1703
+ return (weight_key, activation_key) == (
1704
+ kFp8Static128BlockSym,
1705
+ kFp8Dynamic128Sym,
1706
+ )
1707
+
1708
+ @staticmethod
1709
+ def _supports_routing_method(
1710
+ routing_method: RoutingMethodType,
1711
+ weight_key: QuantKey | None,
1712
+ activation_key: QuantKey | None,
1713
+ ) -> bool:
1714
+ return routing_method in [
1715
+ RoutingMethodType.Default,
1716
+ RoutingMethodType.Renormalize,
1717
+ RoutingMethodType.RenormalizeNaive,
1718
+ RoutingMethodType.DeepSeekV3,
1719
+ RoutingMethodType.DeepseekV4,
1720
+ ]
1721
+
1722
+ @staticmethod
1723
+ def _supports_router_logits_dtype(
1724
+ router_logits_dtype: torch.dtype | None,
1725
+ routing_method: RoutingMethodType,
1726
+ ) -> bool:
1727
+ return True
1728
+
1729
+ def moe_problem_size(
1730
+ self,
1731
+ a1: torch.Tensor,
1732
+ w1: torch.Tensor,
1733
+ w2: torch.Tensor,
1734
+ topk_ids: torch.Tensor,
1735
+ ) -> tuple[int, int, int, int, int]:
1736
+ # float8_linear_prepack_cpu blocks w1 into [E, Nc, Kc, block_k, block_n],
1737
+ # not the (E, N, K) layout the base implementation assumes -- N isn't
1738
+ # recoverable from that shape, so read it from moe_config instead.
1739
+ E = w1.shape[0]
1740
+ K = a1.size(-1)
1741
+ N = (
1742
+ self.moe_config.intermediate_size_per_partition
1743
+ * self.moe_config.w13_num_shards
1744
+ )
1745
+ M = a1.size(0) if a1.dim() == 2 else a1.size(1)
1746
+ topk = topk_ids.size(1)
1747
+ return E, M, N, K, topk
1748
+
1749
+ def workspace_shapes(
1750
+ self,
1751
+ M: int,
1752
+ N: int,
1753
+ K: int,
1754
+ topk: int,
1755
+ global_num_experts: int,
1756
+ local_num_experts: int,
1757
+ expert_tokens_meta: mk.ExpertTokensMetadata | None,
1758
+ activation: MoEActivation,
1759
+ ) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
1760
+ # fused_experts_cpu manages its own scratch space.
1761
+ return (0,), (0,), (M, K)
1762
+
1763
+ def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
1764
+ return TopKWeightAndReduceNoOP()
1765
+
1766
+ def apply(
1767
+ self,
1768
+ output: torch.Tensor,
1769
+ hidden_states: torch.Tensor,
1770
+ w1: torch.Tensor,
1771
+ w2: torch.Tensor,
1772
+ topk_weights: torch.Tensor,
1773
+ topk_ids: torch.Tensor,
1774
+ activation: MoEActivation,
1775
+ global_num_experts: int,
1776
+ expert_map: torch.Tensor | None,
1777
+ a1q_scale: torch.Tensor | None,
1778
+ a2_scale: torch.Tensor | None,
1779
+ workspace13: torch.Tensor,
1780
+ workspace2: torch.Tensor,
1781
+ expert_tokens_meta: mk.ExpertTokensMetadata | None,
1782
+ apply_router_weight_on_input: bool,
1783
+ ) -> None:
1784
+ if apply_router_weight_on_input:
1785
+ # fused_experts_cpu always applies topk_weights internally on
1786
+ # combine; MoEPrepareAndFinalizeNoDPEPModular.prepare() would
1787
+ # also pre-apply it to hidden_states, double-weighting the
1788
+ # output. Not needed by any CPU FP8 model today.
1789
+ raise NotImplementedError(
1790
+ "CPUExpertsFp8W8A8 does not support apply_router_weight_on_input=True."
1791
+ )
1792
+
1793
+ block_shape = (
1794
+ list(self.quant_config.block_shape)
1795
+ if self.quant_config.block_shape
1796
+ else (
1797
+ [
1798
+ self.quant_config._w1.shape.row,
1799
+ self.quant_config._w1.shape.col,
1800
+ ]
1801
+ if self.quant_config._w1.shape is not None
1802
+ else None
1803
+ )
1804
+ )
1805
+
1806
+ # Quantize hidden_states (BF16) → FP8 with per-token scales
1807
+ hidden_states_2d = hidden_states.reshape(-1, hidden_states.shape[-1])
1808
+ x_fp8, x_scales = torch.ops._C.quantize_fp8e4m3_vec(
1809
+ hidden_states_2d, True, None
1810
+ )
1811
+
1812
+ fused_experts_cpu(
1813
+ output,
1814
+ x_fp8,
1815
+ w1,
1816
+ w2,
1817
+ topk_weights,
1818
+ topk_ids,
1819
+ CPUQuantMethod.FP8_W8A8,
1820
+ self.w1_scale, # w1_scale
1821
+ self.w2_scale, # w2_scale
1822
+ None, # w1_zero
1823
+ None, # w2_zero
1824
+ block_shape, # block_size
1825
+ None, # w1_bias
1826
+ None, # w2_bias
1827
+ None, # alpha
1828
+ None, # limit
1829
+ True, # is_vnni
1830
+ x_scales, # a1_scale: per-token FP8 activation scales
1831
+ )
vllm/model_executor/layers/fused_moe/oracle/fp8.py
+30
-2
@@ -57,7 +57,9 @@ class Fp8MoeBackend(Enum):
57
VLLM_CUTLASS = "VLLM_CUTLASS"
58
BATCHED_VLLM_CUTLASS = "BATCHED_VLLM_CUTLASS"
59
XPU = "XPU"
60
+ # CPU FP8 W8A16 (BF16 activations); CPU_W8A8 needs native AMX-FP8.
61
CPU = "CPU"
62
+ CPU_W8A8 = "CPU_W8A8"
63
HPC = "HPC"
64
# Dequantize-to-BF16 emulation for MXFP8 on devices without a native
65
# MXFP8 MoE kernel (e.g. ROCm). Weights pass through unchanged here.
@@ -92,6 +94,7 @@ def _get_priority_backends(
94
Fp8MoeBackend.BATCHED_VLLM_CUTLASS,
95
Fp8MoeBackend.BATCHED_TRITON,
96
Fp8MoeBackend.XPU,
97
+ Fp8MoeBackend.CPU_W8A8,
98
Fp8MoeBackend.CPU,
99
Fp8MoeBackend.HPC,
100
]
@@ -129,8 +132,10 @@ def _get_priority_backends(
132
_move_to_front(_AVAILABLE_BACKENDS, Fp8MoeBackend.XPU)
133
134
if current_platform.is_cpu():
132
- # CPU platform uses FP8 W8A16 fused MoE kernel.
135
+ # W8A8 first: it falls through to the W8A16 backend whenever the
136
+ # hardware (AMX-FP8) or the config isn't supported.
137
_move_to_front(_AVAILABLE_BACKENDS, Fp8MoeBackend.CPU)
138
+ _move_to_front(_AVAILABLE_BACKENDS, Fp8MoeBackend.CPU_W8A8)
139
140
return _AVAILABLE_BACKENDS
141
@@ -238,6 +243,13 @@ def backend_to_kernel_cls(
243
244
return [CPUExpertsFp8]
245
246
+ elif backend == Fp8MoeBackend.CPU_W8A8:
247
+ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
248
+ CPUExpertsFp8W8A8,
249
+ )
250
+
251
+ return [CPUExpertsFp8W8A8]
252
+
253
elif backend == Fp8MoeBackend.HPC:
254
from vllm.model_executor.layers.fused_moe.hpc_moe import (
255
HPCExperts,
@@ -652,6 +664,14 @@ def convert_to_fp8_moe_kernel_format(
664
)
665
666
w13, w2 = prepare_fp8_moe_layer_for_cpu(w13, w2)
667
+ elif fp8_backend == Fp8MoeBackend.CPU_W8A8:
668
+ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
669
+ prepare_fp8_w8a8_moe_layer_for_cpu,
670
+ )
671
+
672
+ w13, w13_scale, w2, w2_scale = prepare_fp8_w8a8_moe_layer_for_cpu(
673
+ w13, w2, w13_scale, w2_scale
674
+ )
675
else:
676
if fp8_backend not in [
677
Fp8MoeBackend.TRITON,
@@ -697,7 +717,15 @@ def make_fp8_moe_quant_config(
717
In a future PR, we will have this function should be
718
a method of the modular kernel itself.
719
"""
700
- # MARLIN and CPU are mixed precision W8A16 config.
720
+ if fp8_backend == Fp8MoeBackend.CPU_W8A8:
721
+ return fp8_w8a8_moe_quant_config(
722
+ w1_scale=w1_scale,
723
+ w2_scale=w2_scale,
724
+ a1_scale=a1_scale,
725
+ block_shape=block_shape,
726
+ )
727
+
728
+ # MARLIN and CPU (W8A16) are mixed precision W8A16 configs.
729
if fp8_backend == Fp8MoeBackend.MARLIN or fp8_backend == Fp8MoeBackend.CPU:
730
return fp8_w8a16_moe_quant_config(
731
w1_scale=w1_scale,
vllm/model_executor/layers/quantization/online/fp8.py
+4
-2
@@ -737,11 +737,13 @@ class Fp8PtpcOnlineMoEMethod(_Fp8OnlineMoEBase):
737
)
738
# Reject backends whose make_fp8_moe_quant_config branch silently
739
# drops per_act_token_quant / per_out_ch_quant or collapses scales:
740
- # MARLIN / CPU route through fp8_w8a16_moe_quant_config; FLASHINFER_*
741
- # fold scales into a per-tensor alpha (oracle/fp8.py).
740
+ # MARLIN / CPU route through fp8_w8a16_moe_quant_config, CPU_W8A8
741
+ # ignores both flags, and FLASHINFER_* fold scales into a per-tensor
742
+ # alpha (oracle/fp8.py).
743
if self.fp8_backend in (
744
Fp8MoeBackend.MARLIN,
745
Fp8MoeBackend.CPU,
746
+ Fp8MoeBackend.CPU_W8A8,
747
Fp8MoeBackend.FLASHINFER_CUTLASS,
748
Fp8MoeBackend.FLASHINFER_TRTLLM,
749
):
vllm/v1/attention/backends/mla/amx_mla.py
+79
-8
@@ -61,6 +61,44 @@ _MIN_WORK_PER_SPLIT = 512
61
_SPLIT_OCCUPANCY_MULTIPLIER = 2
62
63
64
+def _block_quant_to_tensor_quant(
65
+ weight: torch.Tensor, # FP8 [N, K] block-quantized weight
66
+ weight_scale: torch.Tensor, # [ceil(N/bn), ceil(K/bk)] block scales
67
+ block_size: list[int], # [block_n, block_k]
68
+) -> tuple[torch.Tensor, torch.Tensor]:
69
+ """Re-quantize FP8 block-quant weight to FP8 per-tensor quant.
70
+
71
+ Returns (fp8_weight, dequant_scale) where fp8_weight * dequant_scale
72
+ approximates the original float weight. dequant_scale is a 0-dim
73
+ scalar tensor as expected by bmm_cpu's ``scale`` argument.
74
+
75
+ NOTE: block->tensor re-quantization may slightly affect accuracy
76
+ (equivalent to SGLang's block_quant_to_tensor_quant for CPU).
77
+ """
78
+ block_n, block_k = block_size[0], block_size[1]
79
+ n, k = weight.shape
80
+
81
+ # Expand block scales [ceil(N/bn), ceil(K/bk)] -> [N, K]
82
+ scale = weight_scale.to(torch.float32)
83
+ if scale.shape[0] < n:
84
+ scale = scale.repeat_interleave(block_n, dim=0)[:n, :]
85
+ if scale.shape[1] < k:
86
+ scale = scale.repeat_interleave(block_k, dim=1)[:, :k]
87
+
88
+ # Dequantize to float32
89
+ x_f32 = weight.to(torch.float32) * scale
90
+
91
+ # Re-quantize per-tensor back to FP8
92
+ fp8_max = torch.finfo(weight.dtype).max # 448.0 for e4m3fn
93
+ amax = x_f32.abs().amax().clamp(min=1e-12)
94
+ quant_scale = fp8_max / amax
95
+ x_fp8 = (x_f32 * quant_scale).clamp(-fp8_max, fp8_max).to(weight.dtype)
96
+
97
+ # dequant_scale = 1 / quant_scale (0-dim tensor for bmm_cpu)
98
+ dequant_scale = (amax / fp8_max).to(torch.float32)
99
+ return x_fp8, dequant_scale
100
+
101
+
102
def _compute_num_kv_splits(max_seq_len: int, num_threads: int) -> int:
103
"""Mirrors TritonMLAImpl's _compute_num_kv_splits, using the CPU thread
104
count in place of SM count."""
@@ -208,6 +246,9 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
246
)
247
self._w_uk_packed: torch.Tensor | None = None
248
self._w_uv_packed: torch.Tensor | None = None
249
+ self._w_scale: torch.Tensor | None = (
250
+ None # per-tensor dequant scale (FP8 W8A16)
251
+ )
252
253
def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
254
# forward_mha (unlike forward_mqa) receives no `layer` argument, so it
@@ -216,12 +257,40 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
257
# impl derives its own copies from self.kv_b_proj (already available
258
# via the constructor) and VNNI-packs them once, ahead of time, for
259
# use with bmm_cpu in forward_mha.
219
- kv_b_proj_weight = get_and_maybe_dequant_weights(
220
- self.kv_b_proj, out_dtype=act_dtype
221
- ).T
222
- kv_b_proj_weight = kv_b_proj_weight.view(
223
- self.kv_lora_rank, self.num_heads, self.qk_nope_head_dim + self.v_head_dim
224
- )
260
+ #
261
+ # kv_b_proj carries `_cpu_skip_gemm_dispatch`, so the CPU FP8 kernels
262
+ # leave its weight in the checkpoint [N, K] layout. For block-quant
263
+ # FP8 checkpoints, re-quantize per-tensor and keep the BMM in FP8
264
+ # (W8A16); everything else goes through the BF16 path.
265
+ weight = getattr(self.kv_b_proj, "weight", None)
266
+ block_size = getattr(self.kv_b_proj, "weight_block_size", None)
267
+ if (
268
+ weight is not None
269
+ and weight.dtype == torch.float8_e4m3fn
270
+ and block_size is not None
271
+ ):
272
+ block_scale = getattr(self.kv_b_proj, "weight_scale_inv", None)
273
+ if block_scale is None:
274
+ block_scale = self.kv_b_proj.weight_scale
275
+ fp8_weight, self._w_scale = _block_quant_to_tensor_quant(
276
+ weight, block_scale, block_size
277
+ )
278
+ # [N, K] -> [K, N] = [kv_lora_rank, heads * (nope + v)]
279
+ kv_b_proj_weight = fp8_weight.T.contiguous().view(
280
+ self.kv_lora_rank,
281
+ self.num_heads,
282
+ self.qk_nope_head_dim + self.v_head_dim,
283
+ )
284
+ else:
285
+ kv_b_proj_weight = get_and_maybe_dequant_weights(
286
+ self.kv_b_proj, out_dtype=act_dtype
287
+ ).T
288
+ kv_b_proj_weight = kv_b_proj_weight.view(
289
+ self.kv_lora_rank,
290
+ self.num_heads,
291
+ self.qk_nope_head_dim + self.v_head_dim,
292
+ )
293
+ self._w_scale = None
294
w_uk, w_uv = kv_b_proj_weight.split(
295
[self.qk_nope_head_dim, self.v_head_dim], dim=-1
296
)
@@ -348,7 +417,9 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
417
ql_nope = torch.empty(
418
num_tokens, num_heads, self.kv_lora_rank, dtype=q.dtype, device=q.device
419
)
351
- ops.bmm_cpu(ql_nope.transpose(0, 1), q_nope_t, self._w_uk_packed, True, None)
420
+ ops.bmm_cpu(
421
+ ql_nope.transpose(0, 1), q_nope_t, self._w_uk_packed, True, self._w_scale
422
+ )
423
mqa_q = torch.cat([ql_nope, q_pe], dim=-1)
424
425
kv_cache_flat = kv_c_and_k_pe_cache.view(-1, 1, self.head_size)
@@ -402,4 +473,4 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
473
output_view = output.view(num_tokens, num_heads, self.v_head_dim).transpose(
474
0, 1
475
)
405
- ops.bmm_cpu(output_view, attn_out_t, self._w_uv_packed, True, None)
476
+ ops.bmm_cpu(output_view, attn_out_t, self._w_uv_packed, True, self._w_scale)