[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>
This commit is contained in:
co-authored by
Li, Jiang <jiang1.li@intel.com>
parent
6df2b1a8c6
commit
b346479065
@@ -488,6 +488,7 @@ set(VLLM_EXT_SRC
|
||||
"csrc/moe/dynamic_4bit_int_moe_cpu.cpp"
|
||||
"csrc/cpu/cpu_fused_moe.cpp"
|
||||
"csrc/cpu/cpu_attn.cpp"
|
||||
"csrc/cpu/cpu_isa.cpp"
|
||||
"csrc/cpu/torch_bindings.cpp")
|
||||
|
||||
if (CMAKE_SYSTEM_PROCESSOR MATCHES "riscv64" AND VLLM_RVV_VLEN AND
|
||||
@@ -546,11 +547,13 @@ if (ENABLE_X86_ISA)
|
||||
"csrc/cpu/sgl-kernels/gemm.cpp"
|
||||
"csrc/cpu/sgl-kernels/gemm_int8.cpp"
|
||||
"csrc/cpu/sgl-kernels/gemm_fp8.cpp"
|
||||
"csrc/cpu/sgl-kernels/gemm_fp8_w8a8.cpp"
|
||||
"csrc/cpu/sgl-kernels/gemm_int4.cpp"
|
||||
"csrc/cpu/sgl-kernels/moe.cpp"
|
||||
"csrc/cpu/sgl-kernels/moe_int8.cpp"
|
||||
"csrc/cpu/sgl-kernels/moe_int4.cpp"
|
||||
"csrc/cpu/sgl-kernels/moe_fp8.cpp"
|
||||
"csrc/cpu/sgl-kernels/moe_fp8_w8a8.cpp"
|
||||
"csrc/cpu/sgl-kernels/bmm.cpp"
|
||||
"csrc/cpu/sgl-kernels/decode.cpp"
|
||||
"csrc/cpu/sgl-kernels/extend.cpp"
|
||||
@@ -571,6 +574,7 @@ if (ENABLE_X86_ISA)
|
||||
"csrc/cpu/utils.cpp"
|
||||
"csrc/cpu/spec_decode_utils.cpp"
|
||||
"csrc/cpu/cpu_attn.cpp"
|
||||
"csrc/cpu/cpu_isa.cpp"
|
||||
"csrc/cpu/dnnl_kernels.cpp"
|
||||
"csrc/cpu/mamba_cpu.cpp"
|
||||
"csrc/cpu/torch_bindings.cpp"
|
||||
@@ -587,6 +591,7 @@ if (ENABLE_X86_ISA)
|
||||
"csrc/cpu/utils.cpp"
|
||||
"csrc/cpu/spec_decode_utils.cpp"
|
||||
"csrc/cpu/cpu_attn.cpp"
|
||||
"csrc/cpu/cpu_isa.cpp"
|
||||
"csrc/cpu/mamba_cpu.cpp"
|
||||
"csrc/cpu/dnnl_kernels.cpp"
|
||||
"csrc/cpu/torch_bindings.cpp"
|
||||
|
||||
+2
-11
@@ -1,16 +1,7 @@
|
||||
#include "cpu_attn_dispatch_generated.h"
|
||||
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
#include <cpuid.h>
|
||||
// Runtime check for AMX-FP8 (Diamond Rapids): CPUID leaf 7, subleaf 1, EAX[21].
|
||||
static bool runtime_has_amx_fp8() {
|
||||
unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
|
||||
if (__get_cpuid_count(7, 1, &eax, &ebx, &ecx, &edx)) {
|
||||
return (eax >> 21) & 1u;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
#endif // x86_64
|
||||
// Runtime check for AMX-FP8, implemented in cpu_isa.cpp.
|
||||
extern bool runtime_has_amx_fp8();
|
||||
|
||||
// Maps kv_cache_dtype string to Fp8KVCacheDataType enum.
|
||||
// "auto" -> kAuto(0); "fp8"/"fp8_e4m3" -> kFp8E4M3; "fp8_e5m2" -> kFp8E5M2.
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
// AMX-FP8 is reported at CPUID leaf 0x1E, subleaf 1, EAX[4].
|
||||
#if defined(__x86_64__) || defined(_M_X64)
|
||||
#include <cpuid.h>
|
||||
|
||||
bool runtime_has_amx_fp8() {
|
||||
unsigned int eax = 0, ebx = 0, ecx = 0, edx = 0;
|
||||
if (__get_cpuid_max(0, nullptr) < 0x1e) return false;
|
||||
if (__get_cpuid_count(0x1e, 1, &eax, &ebx, &ecx, &edx)) {
|
||||
return (eax >> 4) & 1u;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool cpu_has_amx_fp8() { return runtime_has_amx_fp8(); }
|
||||
#else
|
||||
bool cpu_has_amx_fp8() { return false; }
|
||||
#endif
|
||||
@@ -12,11 +12,45 @@
|
||||
#include <ATen/ATen.h>
|
||||
#include <ATen/Dispatch.h>
|
||||
#include <ATen/Parallel.h>
|
||||
#include <cstring>
|
||||
|
||||
#if defined(_OPENMP)
|
||||
#include <omp.h>
|
||||
#endif
|
||||
|
||||
// Adapted from sglang: dispatch utilities for FP8 W8A8 kernels
|
||||
#define AT_DISPATCH_OUT_TYPES(TYPE, NAME, ...) \
|
||||
AT_DISPATCH_SWITCH( \
|
||||
TYPE, \
|
||||
NAME, \
|
||||
AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Float, out_t, __VA_ARGS__) \
|
||||
AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::BFloat16, out_t, __VA_ARGS__) \
|
||||
AT_PRIVATE_CASE_TYPE_USING_HINT(at::ScalarType::Half, out_t, __VA_ARGS__))
|
||||
|
||||
#define AT_DISPATCH_CASE_ENUM_NO_RETURN(VALUE, TYPE, HINT, ...) \
|
||||
case VALUE: { \
|
||||
constexpr TYPE HINT = VALUE; \
|
||||
__VA_ARGS__; \
|
||||
break; \
|
||||
}
|
||||
|
||||
#define AT_DISPATCH_BOOL_NO_RETURN(VALUE, NAME, HINT, ...) \
|
||||
[&]() { \
|
||||
switch (VALUE) { \
|
||||
AT_DISPATCH_CASE_ENUM_NO_RETURN(true, bool, HINT, __VA_ARGS__) \
|
||||
AT_DISPATCH_CASE_ENUM_NO_RETURN(false, bool, HINT, __VA_ARGS__) \
|
||||
} \
|
||||
}()
|
||||
|
||||
#define AT_DISPATCH_QUANT_MODE_NO_RETURN(MODE, NAME, HINT, ...) \
|
||||
[&]() { \
|
||||
switch (MODE) { \
|
||||
AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_TENSOR, int, HINT, __VA_ARGS__) \
|
||||
AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_ROW, int, HINT, __VA_ARGS__) \
|
||||
AT_DISPATCH_CASE_ENUM_NO_RETURN(PER_GROUP, int, HINT, __VA_ARGS__) \
|
||||
} \
|
||||
}()
|
||||
|
||||
namespace {
|
||||
|
||||
// dispatch bool
|
||||
@@ -472,4 +506,53 @@ inline T* conditional_data_ptr(const std::optional<at::Tensor>& opt) {
|
||||
return opt.has_value() ? opt.value().data_ptr<T>() : nullptr;
|
||||
}
|
||||
|
||||
// Adapted from sglang: M blocking utilities for FP8 W8A8 kernels
|
||||
inline int64_t get_m_block(int64_t M) {
|
||||
if (M <= 48) {
|
||||
return M;
|
||||
} else if (M < 64) {
|
||||
return 32;
|
||||
} else if (M < 96) {
|
||||
return 64;
|
||||
} else {
|
||||
return 128;
|
||||
}
|
||||
}
|
||||
|
||||
inline std::tuple<bool, int64_t, int64_t, int64_t> get_m_blocking(int64_t M) {
|
||||
bool parallel_on_M = M > 128;
|
||||
int64_t block_m = get_m_block(M);
|
||||
int64_t Mc = (M + block_m - 1) / block_m;
|
||||
int64_t Mc_parallel = parallel_on_M ? Mc : 1;
|
||||
return std::make_tuple(parallel_on_M, block_m, Mc, Mc_parallel);
|
||||
}
|
||||
|
||||
#if defined(CPU_CAPABILITY_AVX512)
|
||||
template <typename T>
|
||||
void zero_buffer(T* data, int64_t size) {
|
||||
const int32_t vec_size = at::vec::Vectorized<T>::size();
|
||||
auto zero_vec = at::vec::Vectorized<T>(0);
|
||||
int64_t d = 0;
|
||||
for (; d < size - (size % vec_size); d += vec_size) {
|
||||
zero_vec.store(data + d);
|
||||
}
|
||||
if (d < size) {
|
||||
zero_vec.store(data + d, size - d);
|
||||
}
|
||||
}
|
||||
#else
|
||||
template <typename T>
|
||||
void zero_buffer(T* data, int64_t size) {
|
||||
memset(data, 0, sizeof(T) * size);
|
||||
}
|
||||
#endif
|
||||
|
||||
#if defined(__x86_64__)
|
||||
bool avx10_2_available() {
|
||||
// __builtin_cpu_supports returns the masked feature bit rather than 0/1.
|
||||
static const bool available = __builtin_cpu_supports("avx10.2");
|
||||
return available;
|
||||
}
|
||||
#endif
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
@@ -94,7 +94,7 @@ enum class CPUActMethod : int {
|
||||
gelu_and_mul = 2,
|
||||
};
|
||||
|
||||
enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4 };
|
||||
enum class CPUQuantMethod : int64_t { BF16 = 0, INT8_W8A8 = 1, FP8_W8A16 = 2, INT4_W4A8 = 3, MXFP4 = 4, FP8_W8A8 = 5 };
|
||||
|
||||
constexpr bool operator==(CPUQuantMethod a, int64_t b) {
|
||||
return static_cast<int64_t>(a) == b;
|
||||
@@ -395,3 +395,55 @@ void tinygemm_kernel(
|
||||
bool brg,
|
||||
int64_t block_size_K,
|
||||
bool do_unpack = true);
|
||||
|
||||
// FP8 W8A8 tinygemm kernel (defined in gemm_fp8_w8a8.cpp)
|
||||
void tinygemm_kernel(
|
||||
float* C,
|
||||
const at::Float8_e4m3fn* A,
|
||||
const float* scales_a,
|
||||
const at::Float8_e4m3fn* B,
|
||||
const float* scales_b,
|
||||
int64_t M,
|
||||
int64_t K,
|
||||
int64_t lda,
|
||||
int64_t ldc,
|
||||
int64_t ldsa,
|
||||
float* ukernel_buf,
|
||||
at::BFloat16* dqA_buf,
|
||||
at::BFloat16* dqB_buf);
|
||||
|
||||
// FP8 W8A8 quantize activation (defined in gemm_fp8_w8a8.cpp)
|
||||
std::tuple<at::Tensor, at::Tensor> quantize_fp8e4m3_vec(
|
||||
const at::Tensor& t,
|
||||
bool channelwise,
|
||||
c10::optional<at::Tensor> scale_opt);
|
||||
|
||||
// FP8 W8A8 fused MoE kernel (defined in moe_fp8_w8a8.cpp)
|
||||
template <typename scalar_t>
|
||||
void fused_experts_fp8_a8_kernel_impl(
|
||||
scalar_t* __restrict__ output,
|
||||
scalar_t* __restrict__ ic0,
|
||||
scalar_t* __restrict__ ic1,
|
||||
scalar_t* __restrict__ ic2,
|
||||
at::Float8_e4m3fn* __restrict__ A_tmp,
|
||||
scalar_t* __restrict__ B_tmp,
|
||||
float* __restrict__ C_tmp,
|
||||
float* __restrict__ Ukernel_tmp,
|
||||
const at::Float8_e4m3fn* __restrict__ input,
|
||||
const at::Float8_e4m3fn* __restrict__ packed_w1,
|
||||
const at::Float8_e4m3fn* __restrict__ packed_w2,
|
||||
const float* __restrict__ As,
|
||||
const float* __restrict__ w1s,
|
||||
const float* __restrict__ w2s,
|
||||
int64_t block_size_N,
|
||||
int64_t block_size_K,
|
||||
const float* __restrict__ topk_weights,
|
||||
const int32_t* __restrict__ sorted_ids,
|
||||
const int32_t* __restrict__ expert_ids,
|
||||
const int32_t* __restrict__ offsets,
|
||||
int64_t M,
|
||||
int64_t N,
|
||||
int64_t K,
|
||||
int64_t E,
|
||||
int64_t topk,
|
||||
int64_t num_tokens_post_pad);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -836,6 +836,11 @@ static inline void check_moe_scales(
|
||||
TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for mxfp4.");
|
||||
TORCH_CHECK(w1_scale.value().scalar_type() == at::kByte, "expect w1_scale to be uint8.");
|
||||
TORCH_CHECK(w2_scale.value().scalar_type() == at::kByte, "expect w2_scale to be uint8.");
|
||||
} else if constexpr (quant == CPUQuantMethod::FP8_W8A8) {
|
||||
TORCH_CHECK(w1_scale.has_value(), "missing w1_scale for fp8 w8a8.");
|
||||
TORCH_CHECK(w2_scale.has_value(), "missing w2_scale for fp8 w8a8.");
|
||||
TORCH_CHECK(block_size.has_value(), "missing block_size for fp8 w8a8.");
|
||||
TORCH_CHECK(block_size.value().size() == 2, "expect block_size.size() to be 2.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -850,9 +855,14 @@ static inline void check_moe_scales(
|
||||
check_moe_scales<CPUQuantMethod::FP8_W8A16>(w1_scale, w2_scale, block_size);
|
||||
} else if (moe_comp_method == CPUQuantMethod::MXFP4) {
|
||||
check_moe_scales<CPUQuantMethod::MXFP4>(w1_scale, w2_scale, block_size);
|
||||
} else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
|
||||
check_moe_scales<CPUQuantMethod::FP8_W8A8>(w1_scale, w2_scale, block_size);
|
||||
}
|
||||
}
|
||||
|
||||
// CHECK_MOE_SCALES_FP8: validate scale shapes for FP8_W8A16.
|
||||
// w1s shape [E, 2N/block_N, K/block_K, block_N]: DIM0=1, DIM1=2
|
||||
// w2s shape [E, K/block_N, N/block_K]: DIM0=1, DIM1=2
|
||||
#define CHECK_MOE_SCALES_FP8(DIM0, DIM1) \
|
||||
auto w1s = w1_scale.value(); \
|
||||
auto w2s = w2_scale.value(); \
|
||||
@@ -864,6 +874,17 @@ static inline void check_moe_scales(
|
||||
TORCH_CHECK(w2s.size(DIM0) == div_up(K, block_size_N)); \
|
||||
TORCH_CHECK(w2s.size(DIM1) == div_up(N, block_size_K))
|
||||
|
||||
// CHECK_MOE_SCALES_FP8_W8A8: extract scale info for FP8_W8A8 without shape
|
||||
// validation, because w1s is pre-packed by float8_linear_prepack_cpu with
|
||||
// shape [E, 2N/BLOCK_N, G, BLOCK_N] where BLOCK_N=32 != block_size_N(128).
|
||||
// This matches the sglang upstream behavior (checks are commented out there).
|
||||
#define CHECK_MOE_SCALES_FP8_W8A8(DIM0, DIM1) \
|
||||
auto w1s = w1_scale.value(); \
|
||||
auto w2s = w2_scale.value(); \
|
||||
auto block_size_val = block_size.value(); \
|
||||
int64_t block_size_N = block_size_val[0]; \
|
||||
int64_t block_size_K = block_size_val[1]
|
||||
|
||||
// hidden_states: [M, K]
|
||||
// w1: [E, 2N, K] or [E, 2N, K / 2] for uint8
|
||||
// w2: [E, K, N] or [E, K, N / 2] for uint8
|
||||
@@ -883,6 +904,7 @@ void fused_experts_cpu(
|
||||
const std::optional<at::Tensor>& w2_scale,
|
||||
const std::optional<at::Tensor>& w1_zero,
|
||||
const std::optional<at::Tensor>& w2_zero,
|
||||
const std::optional<at::Tensor>& a1_scale,
|
||||
const std::optional<std::vector<int64_t>> block_size,
|
||||
const std::optional<at::Tensor>& w1_bias,
|
||||
const std::optional<at::Tensor>& w2_bias,
|
||||
@@ -895,7 +917,10 @@ void fused_experts_cpu(
|
||||
constexpr int64_t BLOCK_M = block_size_m();
|
||||
constexpr int64_t BLOCK_N = block_size_n();
|
||||
|
||||
const auto st = hidden_states.scalar_type();
|
||||
// FP8_W8A8 hidden_states input is FP8; scalar_t should be BF16 for output
|
||||
const auto st = (moe_comp_method == CPUQuantMethod::FP8_W8A8)
|
||||
? at::kBFloat16
|
||||
: hidden_states.scalar_type();
|
||||
// TODO: fused_topk_torch_native (CPU fallback for models like MiniMax)
|
||||
// returns int64 topk_ids; fused_experts_cpu requires int32. Remove the typecast after topk kernel is provided
|
||||
auto topk_ids_ = topk_ids.scalar_type() == at::kInt ? topk_ids : topk_ids.to(at::kInt);
|
||||
@@ -908,6 +933,9 @@ void fused_experts_cpu(
|
||||
if (moe_comp_method == CPUQuantMethod::INT4_W4A8 && is_vnni) {
|
||||
CHECK_DIM(4, w1);
|
||||
CHECK_DIM(4, w2);
|
||||
} else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
|
||||
// w1 is prepackaged by float8_linear_prepack_cpu (5D), skip w1 dim check
|
||||
CHECK_DIM(3, w2);
|
||||
} else {
|
||||
CHECK_DIM(3, w1);
|
||||
CHECK_DIM(3, w2);
|
||||
@@ -924,8 +952,9 @@ void fused_experts_cpu(
|
||||
|
||||
int64_t M = hidden_states.size(0);
|
||||
int64_t K = hidden_states.size(1);
|
||||
int64_t N = moe_comp_method == CPUQuantMethod::INT4_W4A8 ? w1_scale.value().size(1) * w1_scale.value().size(3) / 2
|
||||
: w1.size(1) / 2;
|
||||
int64_t N = moe_comp_method == CPUQuantMethod::INT4_W4A8
|
||||
? w1_scale.value().size(1) * w1_scale.value().size(3) / 2
|
||||
: (moe_comp_method == CPUQuantMethod::FP8_W8A8 ? w2.size(2) : w1.size(1) / 2);
|
||||
int64_t E = w1.size(0);
|
||||
int64_t topk = topk_weights_.size(1);
|
||||
|
||||
@@ -935,7 +964,10 @@ void fused_experts_cpu(
|
||||
|
||||
// check weight shapes
|
||||
CHECK_EQ(w2.size(0), E);
|
||||
if (!(moe_comp_method == CPUQuantMethod::INT4_W4A8)) {
|
||||
if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
|
||||
CHECK_EQ(w2.size(1), K);
|
||||
CHECK_EQ(packed_w2.size(2), packed_N);
|
||||
} else if (!(moe_comp_method == CPUQuantMethod::INT4_W4A8)) {
|
||||
CHECK_EQ(w2.size(1), K);
|
||||
CHECK_EQ(packed_w1.size(2), packed_K / (moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 2 : 1));
|
||||
CHECK_EQ(packed_w2.size(2), packed_N / (moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 2 : 1));
|
||||
@@ -993,22 +1025,35 @@ void fused_experts_cpu(
|
||||
// 5. Aq_tmp : [M, K] or [M * topk, N]
|
||||
// 6. As_tmp : [M * topk]
|
||||
//
|
||||
// for fp8 w8a16 and mxfp4:
|
||||
// for fp8 w8a16, mxfp4, and fp8 w8a8:
|
||||
// 7. intermediate_cache0 : [M * topk, 2N]
|
||||
// 8. B_tmp : [T, MAX_CACHE_BLOCK_SIZE, BLOCK_N, std::max(K, N)]
|
||||
//
|
||||
// for fp8 w8a8 additionally:
|
||||
// 9. Ukernel_tmp : [T, 2 * BLOCK_M * BLOCK_N] (float32 for FP8×FP8 brgemm)
|
||||
//
|
||||
int64_t buffer_size_nbytes =
|
||||
M * topk * N * 2 + M * topk * K * 2 +
|
||||
num_threads * BLOCK_M * K *
|
||||
(moe_comp_method == CPUQuantMethod::INT8_W8A8 | moe_comp_method == CPUQuantMethod::INT4_W4A8 ? 1 : 2) +
|
||||
(moe_comp_method == CPUQuantMethod::INT8_W8A8 | moe_comp_method == CPUQuantMethod::INT4_W4A8
|
||||
| moe_comp_method == CPUQuantMethod::FP8_W8A8 ? 1 : 2) +
|
||||
num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float);
|
||||
|
||||
if (moe_comp_method == CPUQuantMethod::INT8_W8A8) {
|
||||
buffer_size_nbytes += std::max(M * K, M * topk * N) + M * topk * sizeof(float);
|
||||
}
|
||||
if (moe_comp_method == CPUQuantMethod::FP8_W8A16 || moe_comp_method == CPUQuantMethod::MXFP4) {
|
||||
if (moe_comp_method == CPUQuantMethod::FP8_W8A16 || moe_comp_method == CPUQuantMethod::MXFP4
|
||||
|| moe_comp_method == CPUQuantMethod::FP8_W8A8) {
|
||||
buffer_size_nbytes += M * topk * 2 * N * 2 + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_N * std::max(K, N) * 2;
|
||||
}
|
||||
if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
|
||||
// Ukernel buffer: float32 workspace per thread for FP8×FP8 brgemm
|
||||
buffer_size_nbytes += num_threads * 2 * BLOCK_M * BLOCK_N * sizeof(float);
|
||||
#ifndef CPUBLAS_BRGEMM_F8F8F32
|
||||
// BF16 fallback: also need dqA [T, BLOCK_M, BLOCK_K] + dqB [T, BLOCK_K, BLOCK_N]
|
||||
buffer_size_nbytes += num_threads * (BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N) * 2;
|
||||
#endif
|
||||
}
|
||||
if (moe_comp_method == CPUQuantMethod::INT4_W4A8) {
|
||||
buffer_size_nbytes += M * topk * 2 * N * 2 + std::max(M * K, M * topk * N) + M * topk * sizeof(float) +
|
||||
num_threads * 2 * get_4bit_block_k_size(K / w1_scale.value().size(2)) * BLOCK_N;
|
||||
@@ -1181,6 +1226,47 @@ void fused_experts_cpu(
|
||||
E,
|
||||
topk,
|
||||
num_tokens_post_pad);
|
||||
} else if (moe_comp_method == CPUQuantMethod::FP8_W8A8) {
|
||||
// Stage 1: FP8×FP8 GEMM (hidden_states is FP8 input)
|
||||
// Stage 2: BF16×FP8 GEMM (W8A16 path for down-projection)
|
||||
at::Float8_e4m3fn* __restrict__ A_tmp =
|
||||
(at::Float8_e4m3fn*)((void*)(intermediate_cache2 + M * topk * K));
|
||||
float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
|
||||
scalar_t* __restrict__ intermediate_cache0 =
|
||||
(scalar_t*)((void*)(C_tmp + num_threads * 2 * BLOCK_M * BLOCK_N));
|
||||
scalar_t* __restrict__ B_tmp =
|
||||
(scalar_t*)((void*)(intermediate_cache0 + M * topk * 2 * N));
|
||||
float* __restrict__ Ukernel_tmp =
|
||||
(float*)((void*)(B_tmp + num_threads * MAX_CACHE_BLOCK_SIZE * BLOCK_N * std::max(K, N)));
|
||||
|
||||
CHECK_MOE_SCALES_FP8_W8A8(1, 2);
|
||||
fused_experts_fp8_a8_kernel_impl(
|
||||
out_hidden_states.data_ptr<scalar_t>(),
|
||||
intermediate_cache0,
|
||||
intermediate_cache1,
|
||||
intermediate_cache2,
|
||||
A_tmp,
|
||||
B_tmp,
|
||||
C_tmp,
|
||||
Ukernel_tmp,
|
||||
hidden_states.data_ptr<at::Float8_e4m3fn>(),
|
||||
packed_w1.data_ptr<at::Float8_e4m3fn>(),
|
||||
packed_w2.data_ptr<at::Float8_e4m3fn>(),
|
||||
a1_scale.has_value() ? a1_scale.value().data_ptr<float>() : nullptr,
|
||||
w1s.data_ptr<float>(),
|
||||
w2s.data_ptr<float>(),
|
||||
block_size_N,
|
||||
block_size_K,
|
||||
topk_weights_.data_ptr<float>(),
|
||||
sorted_ids,
|
||||
expert_ids,
|
||||
offsets,
|
||||
M,
|
||||
N,
|
||||
K,
|
||||
E,
|
||||
topk,
|
||||
num_tokens_post_pad);
|
||||
} else {
|
||||
scalar_t* __restrict__ A_tmp = intermediate_cache2 + M * topk * K;
|
||||
float* __restrict__ C_tmp = (float*)((void*)(A_tmp + num_threads * BLOCK_M * K));
|
||||
|
||||
@@ -0,0 +1,432 @@
|
||||
// Adapted from
|
||||
// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu
|
||||
|
||||
// clang-format off
|
||||
|
||||
#include <c10/util/Unroll.h>
|
||||
|
||||
#include "common.h"
|
||||
#include "gemm.h"
|
||||
#include "moe.h"
|
||||
|
||||
namespace {
|
||||
|
||||
// Byte-wise copy for FP8 elements (no remainder assumption)
|
||||
inline void copy_stub2(
|
||||
at::Float8_e4m3fn* __restrict__ out,
|
||||
const at::Float8_e4m3fn* __restrict__ input,
|
||||
int64_t size) {
|
||||
for (int64_t d = 0; d < size; d++) {
|
||||
out[d] = input[d];
|
||||
}
|
||||
}
|
||||
|
||||
#define PER_TENSOR 1
|
||||
#define PER_ROW 2
|
||||
#define PER_GROUP 3
|
||||
|
||||
// Store float32 GEMM result to output buffer with dequantization and dtype
|
||||
// conversion. Activations are PER_ROW quantized; weights are PER_GROUP quantized.
|
||||
// Uses AVX512 on capable platforms.
|
||||
#if defined(CPU_CAPABILITY_AVX512)
|
||||
|
||||
template <typename out_dtype, int64_t N, int act_quant_mode, int wei_quant_mode>
|
||||
inline void store_out(
|
||||
const float* y_buf,
|
||||
out_dtype* c_ptr,
|
||||
int64_t M,
|
||||
int64_t lda,
|
||||
const float* scales_a,
|
||||
const float* scales_b,
|
||||
const float* bias) {
|
||||
float a_scale = 1.0, b_scale = 1.0;
|
||||
__m512 va_scale, vb_scale;
|
||||
if constexpr (act_quant_mode == PER_TENSOR) {
|
||||
a_scale = *scales_a;
|
||||
}
|
||||
if constexpr (wei_quant_mode == PER_TENSOR) {
|
||||
b_scale = *scales_b;
|
||||
vb_scale = _mm512_set1_ps(b_scale);
|
||||
}
|
||||
for (int i = 0; i < M; ++i) {
|
||||
if constexpr (act_quant_mode == PER_ROW) {
|
||||
a_scale = *(scales_a + i);
|
||||
}
|
||||
if constexpr (act_quant_mode != PER_GROUP) {
|
||||
va_scale = _mm512_set1_ps(a_scale);
|
||||
}
|
||||
constexpr int N_UNROLL = N / 16;
|
||||
c10::ForcedUnroll<N_UNROLL>{}([&](auto idx) {
|
||||
constexpr int j = idx * 16;
|
||||
__m512 y_vec = _mm512_loadu_ps(y_buf + i * N + j);
|
||||
__m512 bias_vec = bias ? _mm512_loadu_ps(bias + j) : _mm512_setzero_ps();
|
||||
if constexpr (act_quant_mode != PER_GROUP) {
|
||||
y_vec = _mm512_mul_ps(y_vec, va_scale);
|
||||
}
|
||||
if constexpr (wei_quant_mode == PER_ROW) {
|
||||
vb_scale = _mm512_loadu_ps(scales_b + j);
|
||||
}
|
||||
if constexpr (wei_quant_mode != PER_GROUP) {
|
||||
y_vec = _mm512_mul_ps(y_vec, vb_scale);
|
||||
}
|
||||
y_vec = _mm512_add_ps(y_vec, bias_vec);
|
||||
if constexpr (std::is_same<out_dtype, float>::value) {
|
||||
_mm512_storeu_ps(c_ptr + i * lda + j, y_vec);
|
||||
} else if constexpr (std::is_same<out_dtype, at::BFloat16>::value) {
|
||||
__m256i y_bf16_vec = at::vec::cvtfp32_bf16(y_vec);
|
||||
_mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_bf16_vec);
|
||||
} else if constexpr (std::is_same<out_dtype, at::Half>::value) {
|
||||
__m256i y_fp16_vec = at::vec::cvtfp32_fp16(y_vec);
|
||||
_mm256_storeu_si256(reinterpret_cast<__m256i*>(c_ptr + i * lda + j), y_fp16_vec);
|
||||
} else {
|
||||
TORCH_CHECK(false, "Unsupported output dtype");
|
||||
}
|
||||
});
|
||||
constexpr int tail_start = N / 16 * 16;
|
||||
for (int j = tail_start; j < N; ++j) {
|
||||
if constexpr (wei_quant_mode == PER_ROW) {
|
||||
b_scale = scales_b[j];
|
||||
}
|
||||
c_ptr[i * lda + j] = static_cast<out_dtype>(y_buf[i * N + j] * a_scale * b_scale);
|
||||
}
|
||||
} // for M
|
||||
}
|
||||
|
||||
#else // no AVX512
|
||||
|
||||
template <typename out_dtype, int64_t N, int act_quant_mode, int wei_quant_mode>
|
||||
inline void store_out(
|
||||
const float* y_buf,
|
||||
out_dtype* c_ptr,
|
||||
int64_t M,
|
||||
int64_t lda,
|
||||
const float* scales_a,
|
||||
const float* scales_b,
|
||||
const float* bias) {
|
||||
float a_scale = 1.0, b_scale = 1.0;
|
||||
if constexpr (act_quant_mode == PER_TENSOR) {
|
||||
a_scale = *scales_a;
|
||||
}
|
||||
if constexpr (wei_quant_mode == PER_TENSOR) {
|
||||
b_scale = *scales_b;
|
||||
}
|
||||
for (int i = 0; i < M; ++i) {
|
||||
if constexpr (act_quant_mode == PER_ROW) {
|
||||
a_scale = *(scales_a + i);
|
||||
}
|
||||
for (int j = 0; j < N; ++j) {
|
||||
if constexpr (wei_quant_mode == PER_ROW) {
|
||||
b_scale = scales_b[j];
|
||||
}
|
||||
float val = y_buf[i * N + j] * a_scale * b_scale;
|
||||
if (bias) val += bias[j];
|
||||
c_ptr[i * lda + j] = static_cast<out_dtype>(val);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endif // CPU_CAPABILITY_AVX512
|
||||
|
||||
} // anonymous namespace
|
||||
|
||||
// FP8 W8A8 fused MoE kernel.
|
||||
//
|
||||
// hidden_states: [M, K] FP8 E4M3, pre-quantized activation
|
||||
// packed_w1: [E, 2*Nc, Kc, BLOCK_K, BLOCK_N] FP8 W8A8 prepack (float8_linear_prepack_cpu)
|
||||
// packed_w2: [E, K, N_packed] FP8 W8A16 VNNI pack (convert_weight_packed)
|
||||
// As: [M] float32 per-token activation scales
|
||||
// w1s: [E, num_groups, 2N] float32 per-group weight scales for w1
|
||||
// w2s: [E, ...] float32 per-group weight scales for w2
|
||||
//
|
||||
// Stage 1: hidden_states(FP8) @ w1(FP8) → ic0(BF16) [M*topk, 2N]
|
||||
// Uses FP8×FP8 GEMM (tinygemm_kernel with FP8 inputs)
|
||||
// Stage 1.5: ic1 = silu(ic0[:,:N]) * ic0[:,N:] [M*topk, N]
|
||||
// Stage 2: ic1(BF16) @ w2(FP8) → ic2(BF16) [M*topk, K]
|
||||
// Uses BF16×FP8 GEMM (W8A16 path, tinygemm_kernel<scalar_t>)
|
||||
// Stage 3: out = weighted_sum(ic2, topk_weights) [M, K]
|
||||
|
||||
template <typename scalar_t>
|
||||
void fused_experts_fp8_a8_kernel_impl(
|
||||
scalar_t* __restrict__ output,
|
||||
scalar_t* __restrict__ ic0,
|
||||
scalar_t* __restrict__ ic1,
|
||||
scalar_t* __restrict__ ic2,
|
||||
at::Float8_e4m3fn* __restrict__ A_tmp,
|
||||
scalar_t* __restrict__ B_tmp,
|
||||
float* __restrict__ C_tmp,
|
||||
float* __restrict__ Ukernel_tmp,
|
||||
const at::Float8_e4m3fn* __restrict__ input,
|
||||
const at::Float8_e4m3fn* __restrict__ packed_w1,
|
||||
const at::Float8_e4m3fn* __restrict__ packed_w2,
|
||||
const float* __restrict__ As,
|
||||
const float* __restrict__ w1s,
|
||||
const float* __restrict__ w2s,
|
||||
int64_t block_size_N,
|
||||
int64_t block_size_K,
|
||||
const float* __restrict__ topk_weights,
|
||||
const int32_t* __restrict__ sorted_ids,
|
||||
const int32_t* __restrict__ expert_ids,
|
||||
const int32_t* __restrict__ offsets,
|
||||
int64_t M,
|
||||
int64_t N,
|
||||
int64_t K,
|
||||
int64_t E,
|
||||
int64_t topk,
|
||||
int64_t num_tokens_post_pad) {
|
||||
// 1. intermediate_cache1 : [M * topk, N]
|
||||
// 2. intermediate_cache2 : [M * topk, K]
|
||||
// 3. A_tmp : [T, BLOCK_M * K] FP8
|
||||
// 4. C_tmp : [T, 2 * BLOCK_M * BLOCK_N] float32
|
||||
// 5. intermediate_cache0 : [M * topk, 2N]
|
||||
// 6. B_tmp : [T, MAX_CACHE_BLOCK_SIZE, BLOCK_N, max(K, N)] scalar_t
|
||||
// 7. Ukernel_tmp: [T, 2 * BLOCK_M * BLOCK_N] float32
|
||||
// 8. (non-FP8-brgemm only) dqA [T, BLOCK_M * BLOCK_K] + dqB [T, BLOCK_K * BLOCK_N] in BF16
|
||||
// appended after Ukernel_tmp; see moe.cpp FP8_W8A8 buffer allocation
|
||||
|
||||
constexpr int64_t BLOCK_M = block_size_m();
|
||||
constexpr int64_t BLOCK_N_VAL = block_size_n(); // = 32
|
||||
const int64_t KB = K / BLOCK_K; // K blocks
|
||||
|
||||
int64_t B_tmp_size_per_thread = MAX_CACHE_BLOCK_SIZE * BLOCK_N_VAL * std::max(K, N);
|
||||
|
||||
// stage 1: intermediate_cache0 = hidden_states @ w1
|
||||
const int64_t MB = div_up(num_tokens_post_pad, BLOCK_M);
|
||||
const int64_t NB = div_up(N, BLOCK_N_VAL);
|
||||
|
||||
int64_t scale_size_N = div_up(2 * N, block_size_N);
|
||||
int64_t scale_size_K = div_up(K, block_size_K);
|
||||
int64_t blocks_n_per_group = block_size_N / BLOCK_N_VAL;
|
||||
int64_t num_groups = div_up(K, block_size_K); // G
|
||||
int64_t blocks_k_per_group = block_size_K / BLOCK_K;
|
||||
const int64_t stride_e = 2 * N * K; // per-expert stride for w1 (total elements)
|
||||
bool use_brgemm = true;
|
||||
int64_t num_thread = at::get_num_threads();
|
||||
|
||||
// For the BF16 fallback path (no native FP8 brgemm), tinygemm_kernel needs temporary
|
||||
// BF16 buffers to dequantize FP8 inputs before running AMX-BF16 brgemm.
|
||||
// These are placed after the Ukernel_tmp region (see moe.cpp FP8_W8A8 buffer allocation).
|
||||
#ifndef CPUBLAS_BRGEMM_F8F8F32
|
||||
// dqA: [T, BLOCK_M, BLOCK_K] BF16
|
||||
// dqB: [T, BLOCK_K, BLOCK_N] BF16
|
||||
// Layout: immediately after Ukernel_tmp[num_thread * 2 * BLOCK_M * BLOCK_N_VAL]
|
||||
at::BFloat16* __restrict__ dq_base = reinterpret_cast<at::BFloat16*>(
|
||||
Ukernel_tmp + num_thread * 2 * BLOCK_M * BLOCK_N_VAL);
|
||||
const int64_t dq_per_thread = BLOCK_M * BLOCK_K + BLOCK_K * BLOCK_N_VAL;
|
||||
#endif
|
||||
|
||||
at::parallel_for(0, MB * NB, 1, [&](int64_t begin, int64_t end) {
|
||||
int tid = get_thread_num();
|
||||
// A_tmp: FP8 activation buffer for this thread [BLOCK_M, K]
|
||||
at::Float8_e4m3fn* __restrict__ A = A_tmp + tid * BLOCK_M * K;
|
||||
float* __restrict__ C0 = C_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL;
|
||||
float* __restrict__ C1 = C0 + BLOCK_M * BLOCK_N_VAL;
|
||||
alignas(64) float As_[BLOCK_M];
|
||||
float* __restrict__ ukernel_buf_1 = Ukernel_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL;
|
||||
#ifndef CPUBLAS_BRGEMM_F8F8F32
|
||||
at::BFloat16* __restrict__ dqA_buf = dq_base + tid * dq_per_thread;
|
||||
at::BFloat16* __restrict__ dqB_buf = dqA_buf + BLOCK_M * BLOCK_K;
|
||||
#else
|
||||
at::BFloat16* __restrict__ dqA_buf = nullptr;
|
||||
at::BFloat16* __restrict__ dqB_buf = nullptr;
|
||||
#endif
|
||||
float* __restrict__ ukernel_buf_2 = ukernel_buf_1 + BLOCK_M * BLOCK_N_VAL;
|
||||
auto ldsa = 1; // activation per-row quantized
|
||||
|
||||
for (int64_t i = begin; i < end; ++i) {
|
||||
int64_t mb = i / NB;
|
||||
int64_t nb = i % NB;
|
||||
int64_t nb1 = nb + NB; // gate is first N columns, up is second N columns
|
||||
|
||||
int64_t n_size = std::min(N - nb * BLOCK_N_VAL, BLOCK_N_VAL);
|
||||
|
||||
// B (weight) for expert expert_id: gate+up fused [2N, K] prepackaged as FP8
|
||||
int32_t expert_id = expert_ids[mb];
|
||||
const at::Float8_e4m3fn* __restrict__ B = packed_w1 + expert_id * stride_e;
|
||||
const float* __restrict__ Bs = w1s + expert_id * num_groups * (2 * N);
|
||||
|
||||
// Load activation tile for this M-block
|
||||
const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
|
||||
int64_t m_size = offsets[mb + 1] - offsets[mb];
|
||||
|
||||
for (int64_t m = 0; m < m_size; ++m) {
|
||||
int32_t index = A_ids[m] / topk;
|
||||
copy_stub2(A + m * K, input + index * K, K);
|
||||
As_[m] = As[index];
|
||||
}
|
||||
|
||||
const int64_t offset = offsets[mb];
|
||||
|
||||
// Gate path (nb): w1[:N, :]
|
||||
zero_buffer(C0, BLOCK_M * BLOCK_N_VAL);
|
||||
for (int kci = 0; kci < KB; ++kci) {
|
||||
// FP8×FP8 tinygemm: act_quant=PER_ROW(2), wei_quant=PER_GROUP(3)
|
||||
tinygemm_kernel(
|
||||
/* C */ C0,
|
||||
/* A */ A + kci * BLOCK_K,
|
||||
/* scales_a*/ As_ + kci / blocks_k_per_group,
|
||||
/* B */ B + (nb * KB + kci) * BLOCK_K * BLOCK_N_VAL,
|
||||
/* scales_b*/ Bs + nb * BLOCK_N_VAL * num_groups + kci / blocks_k_per_group * BLOCK_N_VAL,
|
||||
/* M */ m_size,
|
||||
/* K */ BLOCK_K,
|
||||
/* lda */ K,
|
||||
/* ldc */ BLOCK_N_VAL,
|
||||
/* ldsa */ ldsa,
|
||||
/* ukernel */ ukernel_buf_1,
|
||||
/* dqA_buf */ dqA_buf,
|
||||
/* dqB_buf */ dqB_buf);
|
||||
}
|
||||
store_out<scalar_t, BLOCK_N_VAL, PER_ROW, PER_GROUP>(
|
||||
C0,
|
||||
ic0 + offset * 2 * N + nb * BLOCK_N_VAL,
|
||||
m_size,
|
||||
2 * N, // lda
|
||||
As_,
|
||||
nullptr,
|
||||
nullptr);
|
||||
|
||||
// Up path (nb1): w1[N:, :]
|
||||
zero_buffer(C1, BLOCK_M * BLOCK_N_VAL);
|
||||
for (int kci = 0; kci < KB; ++kci) {
|
||||
tinygemm_kernel(
|
||||
/* C */ C1,
|
||||
/* A */ A + kci * BLOCK_K,
|
||||
/* scales_a*/ As_ + kci / blocks_k_per_group,
|
||||
/* B */ B + (nb1 * KB + kci) * BLOCK_K * BLOCK_N_VAL,
|
||||
/* scales_b*/ Bs + nb1 * BLOCK_N_VAL * num_groups + kci / blocks_k_per_group * BLOCK_N_VAL,
|
||||
/* M */ m_size,
|
||||
/* K */ BLOCK_K,
|
||||
/* lda */ K,
|
||||
/* ldc */ BLOCK_N_VAL,
|
||||
/* ldsa */ ldsa,
|
||||
/* ukernel */ ukernel_buf_2,
|
||||
/* dqA_buf */ dqA_buf,
|
||||
/* dqB_buf */ dqB_buf);
|
||||
}
|
||||
store_out<scalar_t, BLOCK_N_VAL, PER_ROW, PER_GROUP>(
|
||||
C1,
|
||||
ic0 + offset * 2 * N + nb1 * BLOCK_N_VAL,
|
||||
m_size,
|
||||
2 * N, // lda
|
||||
As_,
|
||||
nullptr,
|
||||
nullptr);
|
||||
}
|
||||
if (use_brgemm) {
|
||||
at::native::cpublas::brgemm_release();
|
||||
}
|
||||
});
|
||||
|
||||
// stage 1.5: intermediate_cache1 = silu(ic0[:, :N]) * ic0[:, N:]
|
||||
at::parallel_for(0, M * topk, 0, [&](int64_t begin, int64_t end) {
|
||||
for (int64_t m = begin; m < end; ++m) {
|
||||
silu_and_mul_stub(ic1 + m * N, ic0 + m * 2 * N, ic0 + m * 2 * N + N, N);
|
||||
}
|
||||
});
|
||||
|
||||
// stage 2: intermediate_cache2 = intermediate_cache1 @ w2
|
||||
// w2: [E, K, N] FP8, VNNI packed; treated as W8A16 (BF16 act × FP8 weight)
|
||||
const int64_t OC = K; // output channels (hidden dim)
|
||||
const int64_t IC = N; // input channels (expert intermediate dim)
|
||||
const int64_t MB2 = MB;
|
||||
const int64_t NB2 = div_up(OC, BLOCK_N_VAL);
|
||||
scale_size_N = div_up(K, block_size_N);
|
||||
scale_size_K = div_up(N, block_size_K);
|
||||
const int64_t stride_e2 = OC * IC; // per-expert stride for w2
|
||||
const int64_t stride_oc = IC;
|
||||
|
||||
int64_t avg_M = std::max(int64_t(1), M * topk / E);
|
||||
bool use_brgemm2 = can_use_brgemm<at::Float8_e4m3fn>(avg_M);
|
||||
|
||||
parallel_2d(MB2, NB2, [&](int64_t mb0, int64_t mb1, int64_t nb0, int64_t nb1) {
|
||||
int tid = get_thread_num();
|
||||
alignas(64) scalar_t C[BLOCK_M * BLOCK_K];
|
||||
|
||||
loop_2d<at::Float8_e4m3fn>(mb0, mb1, nb0, nb1, BLOCK_N_VAL * IC, [&](int64_t mb, int64_t nb, int64_t nb_offset) {
|
||||
int64_t m_size = offsets[mb + 1] - offsets[mb];
|
||||
int64_t n_size = std::min(OC - nb * BLOCK_N_VAL, BLOCK_N_VAL);
|
||||
|
||||
// A from ic1: [M*topk, N] in sorted order (contiguous after stage 1.5)
|
||||
const scalar_t* __restrict__ A2 = ic1 + offsets[mb] * N;
|
||||
const int32_t* A_ids = sorted_ids + mb * BLOCK_M;
|
||||
|
||||
// B from w2: [E, K, N] VNNI packed, offset to expert + output block
|
||||
int32_t expert_id = expert_ids[mb];
|
||||
const at::Float8_e4m3fn* __restrict__ B2 =
|
||||
packed_w2 + expert_id * stride_e2 + nb * BLOCK_N_VAL * stride_oc;
|
||||
const float* __restrict__ Bs2 =
|
||||
w2s + expert_id * scale_size_N * scale_size_K + (nb / blocks_n_per_group) * scale_size_K;
|
||||
|
||||
// Only unpack B on first visit or new expert
|
||||
int32_t pre_expert_id = mb == 0 ? -1 : expert_ids[mb - 1];
|
||||
bool do_unpack = (mb == mb0) || (expert_id != pre_expert_id);
|
||||
|
||||
tinygemm_kernel<scalar_t>(
|
||||
/* A */ A2,
|
||||
/* B */ B2,
|
||||
/* C */ C,
|
||||
/* Btmp */ B_tmp + tid * B_tmp_size_per_thread + nb_offset * BLOCK_N_VAL * IC,
|
||||
/* Ctmp */ C_tmp + tid * 2 * BLOCK_M * BLOCK_N_VAL,
|
||||
/* Bbias */ nullptr,
|
||||
/* scale */ Bs2,
|
||||
/* M */ m_size,
|
||||
/* N */ n_size,
|
||||
/* K */ IC,
|
||||
/* lda */ IC,
|
||||
/* ldb */ n_size,
|
||||
/* ldc */ BLOCK_N_VAL,
|
||||
/* brg */ use_brgemm2,
|
||||
/* block_sz_K */ block_size_K,
|
||||
/* do_unpack */ do_unpack);
|
||||
|
||||
// Scatter ic2 in original token order, weighting by topk_weights
|
||||
for (int64_t m = 0; m < m_size; ++m) {
|
||||
int32_t index = A_ids[m];
|
||||
float weight = topk_weights[index];
|
||||
copy_mul_stub(ic2 + index * K + nb * BLOCK_N_VAL, C + m * BLOCK_N_VAL, weight, n_size);
|
||||
}
|
||||
});
|
||||
|
||||
if (use_brgemm2) {
|
||||
at::native::cpublas::brgemm_release();
|
||||
}
|
||||
});
|
||||
|
||||
// stage 3: output = sum(ic2, dim=topk) [M, K]
|
||||
at::parallel_for(0, M, 0, [&](int64_t begin, int64_t end) {
|
||||
for (int64_t m = begin; m < end; ++m) {
|
||||
sum_stub(output + m * K, ic2 + m * topk * K, topk, K);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#define INSTANTIATE_MOE_FP8_A8_TEMPLATE(TYPE) \
|
||||
template void fused_experts_fp8_a8_kernel_impl<TYPE>( \
|
||||
TYPE* __restrict__ output, \
|
||||
TYPE* __restrict__ ic0, \
|
||||
TYPE* __restrict__ ic1, \
|
||||
TYPE* __restrict__ ic2, \
|
||||
at::Float8_e4m3fn* __restrict__ A_tmp, \
|
||||
TYPE* __restrict__ B_tmp, \
|
||||
float* __restrict__ C_tmp, \
|
||||
float* __restrict__ Ukernel_tmp, \
|
||||
const at::Float8_e4m3fn* __restrict__ input, \
|
||||
const at::Float8_e4m3fn* __restrict__ packed_w1, \
|
||||
const at::Float8_e4m3fn* __restrict__ packed_w2, \
|
||||
const float* __restrict__ As, \
|
||||
const float* __restrict__ w1s, \
|
||||
const float* __restrict__ w2s, \
|
||||
int64_t block_size_N, \
|
||||
int64_t block_size_K, \
|
||||
const float* __restrict__ topk_weights, \
|
||||
const int32_t* __restrict__ sorted_ids, \
|
||||
const int32_t* __restrict__ expert_ids, \
|
||||
const int32_t* __restrict__ offsets, \
|
||||
int64_t M, \
|
||||
int64_t N, \
|
||||
int64_t K, \
|
||||
int64_t E, \
|
||||
int64_t topk, \
|
||||
int64_t num_tokens_post_pad)
|
||||
|
||||
INSTANTIATE_MOE_FP8_A8_TEMPLATE(at::BFloat16);
|
||||
INSTANTIATE_MOE_FP8_A8_TEMPLATE(at::Half);
|
||||
@@ -73,6 +73,7 @@ void fused_experts_cpu(at::Tensor& out, at::Tensor& hidden_states,
|
||||
const std::optional<at::Tensor>& w2_scale,
|
||||
const std::optional<at::Tensor>& w1_zero,
|
||||
const std::optional<at::Tensor>& w2_zero,
|
||||
const std::optional<at::Tensor>& a1_scale,
|
||||
const std::optional<std::vector<int64_t>> block_size,
|
||||
const std::optional<at::Tensor>& w1_bias,
|
||||
const std::optional<at::Tensor>& w2_bias,
|
||||
@@ -91,6 +92,18 @@ at::Tensor fp8_scaled_mm_cpu(at::Tensor& mat1, at::Tensor& mat2,
|
||||
const std::optional<at::Tensor>& bias,
|
||||
at::ScalarType out_dtype, bool is_vnni);
|
||||
|
||||
// Adapted from sglang: FP8 W8A8 kernels
|
||||
std::tuple<at::Tensor, at::Tensor> float8_linear_prepack_impl(
|
||||
const at::Tensor& weight, const at::Tensor& scales);
|
||||
at::Tensor fp8_scaled_mm_with_quant(const at::Tensor& act,
|
||||
const std::optional<at::Tensor>& act_scales,
|
||||
bool channelwise, const at::Tensor& weight,
|
||||
const at::Tensor& weight_scales,
|
||||
const std::optional<at::Tensor>& bias,
|
||||
at::ScalarType output_dtype);
|
||||
std::tuple<at::Tensor, at::Tensor> quantize_fp8e4m3_vec(
|
||||
const at::Tensor& t, bool channelwise, c10::optional<at::Tensor> scale_opt);
|
||||
|
||||
// Adapted from sglang: MLA CPU kernels (AMX-only)
|
||||
void decode_attention_cpu(at::Tensor& query, at::Tensor& k_buffer,
|
||||
at::Tensor& v_buffer, at::Tensor& output,
|
||||
@@ -281,6 +294,7 @@ void activation_lut_bf16(torch::Tensor& out, torch::Tensor& input,
|
||||
const std::string& activation);
|
||||
|
||||
bool cpu_attn_has_isa(const std::string& isa);
|
||||
bool cpu_has_amx_fp8();
|
||||
|
||||
torch::Tensor get_scheduler_metadata(
|
||||
const int64_t num_req, const int64_t num_heads_q,
|
||||
@@ -611,7 +625,7 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
"fused_experts_cpu(Tensor(a0!) out, Tensor hidden_states, Tensor w1, "
|
||||
"Tensor w2, Tensor topk_weights, Tensor topk_ids, "
|
||||
"int moe_comp_method, Tensor? w1_scale, Tensor? w2_scale, "
|
||||
"Tensor? w1_zero, Tensor? w2_zero, int[]? block_size, "
|
||||
"Tensor? w1_zero, Tensor? w2_zero, Tensor? a1_scale, int[]? block_size, "
|
||||
"Tensor? w1_bias, Tensor? w2_bias, float? alpha, float? limit, "
|
||||
"bool is_vnni) -> "
|
||||
"()");
|
||||
@@ -629,7 +643,23 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
"bool is_vnni) -> Tensor");
|
||||
ops.impl("fp8_scaled_mm_cpu", torch::kCPU, &fp8_scaled_mm_cpu);
|
||||
|
||||
// Adapted from sglang: casual_conv1d kernels
|
||||
// Adapted from sglang: FP8 W8A8 kernels
|
||||
ops.def(
|
||||
"float8_linear_prepack_cpu(Tensor weight, Tensor scales) -> (Tensor, "
|
||||
"Tensor)");
|
||||
ops.impl("float8_linear_prepack_cpu", torch::kCPU,
|
||||
&float8_linear_prepack_impl);
|
||||
ops.def(
|
||||
"fp8_scaled_mm_with_quant(Tensor act, Tensor? act_scales, bool "
|
||||
"channelwise, "
|
||||
"Tensor weight, Tensor weight_scales, Tensor? bias, ScalarType "
|
||||
"output_dtype) -> Tensor");
|
||||
ops.impl("fp8_scaled_mm_with_quant", torch::kCPU, &fp8_scaled_mm_with_quant);
|
||||
ops.def(
|
||||
"quantize_fp8e4m3_vec(Tensor input, bool channelwise, Tensor? scale_opt) "
|
||||
"-> (Tensor, Tensor)");
|
||||
ops.impl("quantize_fp8e4m3_vec", torch::kCPU, &quantize_fp8e4m3_vec);
|
||||
|
||||
ops.def("causal_conv1d_weight_pack(Tensor weight) -> Tensor");
|
||||
ops.impl("causal_conv1d_weight_pack", torch::kCPU,
|
||||
&causal_conv1d_weight_pack);
|
||||
@@ -832,6 +862,9 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
|
||||
"-> (Tensor, Tensor)");
|
||||
ops.impl("fused_gdn_gating_cpu", torch::kCPU, &fused_gdn_gating_cpu);
|
||||
|
||||
// Pure runtime CPU ISA capability check.
|
||||
ops.def("cpu_has_amx_fp8() -> bool", &cpu_has_amx_fp8);
|
||||
|
||||
// CPU attention kernels
|
||||
ops.def("cpu_attn_has_isa(str isa) -> bool", &cpu_attn_has_isa);
|
||||
ops.def(
|
||||
|
||||
@@ -1114,5 +1114,203 @@ def test_int8_w8a8_cpu_fused_moe(M, N, K, E, topk, seed, is_vnni, inplace):
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# FP8 W8A8 MoE
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
requires_cpu_fp8_w8a8 = pytest.mark.skipif(
|
||||
not ops.cpu_has_amx_fp8(),
|
||||
reason="requires native AMX-FP8 hardware",
|
||||
)
|
||||
|
||||
FP8_W8A8_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
FP8_W8A8_QUANT_GROUP = 128 # quantization group size for both K and N dimensions
|
||||
|
||||
|
||||
def _make_fp8_w8a8_weight_w13(
|
||||
E: int, two_n: int, K: int, group_K: int = FP8_W8A8_QUANT_GROUP
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Create w13 [E, 2N, K] FP8 with group scales [E, 2N, G] where G = K//group_K."""
|
||||
G = K // group_K
|
||||
w_list, s_list = [], []
|
||||
for _ in range(E):
|
||||
w_f32 = torch.randn(two_n, K)
|
||||
# Per-row, per-K-group quantization: scale [2N, G]
|
||||
w_re = w_f32.view(two_n, G, group_K)
|
||||
abs_max = w_re.abs().amax(dim=2, keepdim=True).clamp(min=1e-7) # [2N, G, 1]
|
||||
scale = (abs_max / FP8_W8A8_MAX).squeeze(2) # [2N, G]
|
||||
w_q = (
|
||||
(w_re / abs_max).clamp(-FP8_W8A8_MAX, FP8_W8A8_MAX).to(torch.float8_e4m3fn)
|
||||
)
|
||||
w_list.append(w_q.view(two_n, K).contiguous())
|
||||
s_list.append(scale.float())
|
||||
return torch.stack(w_list), torch.stack(s_list) # [E, 2N, K], [E, 2N, G]
|
||||
|
||||
|
||||
def _make_fp8_w8a8_weight_w2(
|
||||
E: int,
|
||||
K: int,
|
||||
N: int,
|
||||
group_K: int = FP8_W8A8_QUANT_GROUP,
|
||||
group_N: int = FP8_W8A8_QUANT_GROUP,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Create w2 [E, K, N] FP8 with block scales [E, K//gK, N//gN]."""
|
||||
nK = K // group_K
|
||||
nN = N // group_N
|
||||
w_list, s_list = [], []
|
||||
for _ in range(E):
|
||||
w_f32 = torch.randn(K, N)
|
||||
w_re = w_f32.view(nK, group_K, nN, group_N)
|
||||
abs_max = w_re.abs().amax(dim=(1, 3), keepdim=True).clamp(min=1e-7)
|
||||
scale = (abs_max / FP8_W8A8_MAX).squeeze(1).squeeze(2) # [nK, nN]
|
||||
w_q = (
|
||||
(w_re / abs_max).clamp(-FP8_W8A8_MAX, FP8_W8A8_MAX).to(torch.float8_e4m3fn)
|
||||
)
|
||||
w_list.append(w_q.view(K, N).contiguous())
|
||||
s_list.append(scale.float())
|
||||
return torch.stack(w_list), torch.stack(s_list) # [E, K, N], [E, K//gK, N//gN]
|
||||
|
||||
|
||||
def _pack_fp8_w8a8_w13(
|
||||
w13: torch.Tensor, # [E, 2N, K] FP8
|
||||
w13_scale: torch.Tensor, # [E, 2N, G] float32
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Pack w13 for CPU FP8 W8A8 MoE kernel using float8_linear_prepack_cpu."""
|
||||
E = w13.size(0)
|
||||
packed_list, scale_list = [], []
|
||||
for i in range(E):
|
||||
pw, ps = torch.ops._C.float8_linear_prepack_cpu(
|
||||
w13[i].contiguous(), w13_scale[i].contiguous()
|
||||
)
|
||||
packed_list.append(pw)
|
||||
scale_list.append(ps)
|
||||
return torch.stack(packed_list), torch.stack(scale_list)
|
||||
|
||||
|
||||
def _ref_fp8_w8a8_moe(
|
||||
hidden_states: torch.Tensor, # BF16 [M, K_hidden] (after FP8 round-trip dequant)
|
||||
w13: torch.Tensor, # FP8 [E, 2N, K_hidden]
|
||||
w13_scale: torch.Tensor, # float32 [E, 2N, G] (G = K_hidden//group_K)
|
||||
w2: torch.Tensor, # FP8 [E, K_hidden, N]
|
||||
w2_scale: torch.Tensor, # float32 [E, K_hidden//gK, N//gN]
|
||||
topk_weights: torch.Tensor, # float32 [M, top_k]
|
||||
topk_ids: torch.Tensor, # int64 [M, top_k]
|
||||
N: int,
|
||||
group_K: int = FP8_W8A8_QUANT_GROUP,
|
||||
group_N: int = FP8_W8A8_QUANT_GROUP,
|
||||
) -> torch.Tensor:
|
||||
"""Reference implementation: dequantize weights then compute MoE."""
|
||||
M, K_hidden = hidden_states.shape
|
||||
top_k = topk_ids.shape[1]
|
||||
output = torch.zeros(M, K_hidden, dtype=torch.bfloat16)
|
||||
|
||||
for tok in range(M):
|
||||
x = hidden_states[tok].float() # [K_hidden]
|
||||
for k_idx in range(top_k):
|
||||
expert_id = topk_ids[tok, k_idx].item()
|
||||
w = topk_weights[tok, k_idx].item()
|
||||
|
||||
# Stage 1: x @ w13.T — gate and up projections
|
||||
w13_e = w13[expert_id].float() # [2N, K_hidden]
|
||||
ws_e = w13_scale[expert_id].float() # [2N, G]
|
||||
G = ws_e.shape[1]
|
||||
w13_dq = torch.zeros_like(w13_e)
|
||||
for g in range(G):
|
||||
c0, c1 = g * group_K, (g + 1) * group_K
|
||||
w13_dq[:, c0:c1] = w13_e[:, c0:c1] * ws_e[:, g : g + 1]
|
||||
gate_up = (x.unsqueeze(0) @ w13_dq.T).squeeze(0) # [2N]
|
||||
|
||||
gate = gate_up[:N]
|
||||
up = gate_up[N:]
|
||||
act = F.silu(gate) * up # [N]
|
||||
|
||||
# Stage 2: act @ w2.T — down projection
|
||||
w2_e = w2[expert_id].float() # [K_hidden, N]
|
||||
ws2_e = w2_scale[expert_id].float() # [K_hidden//gK, N//gN]
|
||||
nK2 = K_hidden // group_K
|
||||
nN2 = N // group_N
|
||||
w2_dq = torch.zeros_like(w2_e)
|
||||
for gi in range(nK2):
|
||||
for gj in range(nN2):
|
||||
r0, r1 = gi * group_K, (gi + 1) * group_K
|
||||
c0, c1 = gj * group_N, (gj + 1) * group_N
|
||||
w2_dq[r0:r1, c0:c1] = w2_e[r0:r1, c0:c1] * ws2_e[gi, gj]
|
||||
out_e = (act.unsqueeze(0) @ w2_dq.T).squeeze(0) # [K_hidden]
|
||||
|
||||
output[tok] += (w * out_e).to(torch.bfloat16)
|
||||
|
||||
return output
|
||||
|
||||
|
||||
@requires_cpu_fp8_w8a8
|
||||
@pytest.mark.parametrize(
|
||||
"E,N,K,M,top_k",
|
||||
[
|
||||
# N and K must be multiples of FP8_W8A8_QUANT_GROUP=128
|
||||
(4, 128, 256, 1, 2), # single token (decode phase)
|
||||
(4, 128, 256, 8, 2),
|
||||
(8, 256, 512, 16, 2),
|
||||
(4, 256, 512, 4, 1),
|
||||
],
|
||||
)
|
||||
def test_fp8_w8a8_cpu_fused_moe(E: int, N: int, K: int, M: int, top_k: int):
|
||||
"""Test fused_experts_cpu FP8_W8A8 shape/dtype and accuracy vs a
|
||||
dequantized BF16 reference."""
|
||||
set_random_seed(42)
|
||||
|
||||
w13, w13_scale = _make_fp8_w8a8_weight_w13(E, 2 * N, K)
|
||||
w2, w2_scale = _make_fp8_w8a8_weight_w2(E, K, N)
|
||||
|
||||
packed_w13, packed_w13_scale = _pack_fp8_w8a8_w13(w13, w13_scale)
|
||||
packed_w2 = _prepack_experts(w2)
|
||||
|
||||
hidden_states = torch.randn(M, K, dtype=torch.bfloat16) * 0.1
|
||||
x_fp8, x_scales = torch.ops._C.quantize_fp8e4m3_vec(hidden_states, True, None)
|
||||
assert x_fp8.dtype == torch.float8_e4m3fn
|
||||
assert x_scales.shape == (M,)
|
||||
|
||||
gen = torch.Generator().manual_seed(0)
|
||||
topk_weights = torch.softmax(torch.randn(M, top_k, generator=gen), dim=-1).float()
|
||||
topk_ids = torch.zeros(M, top_k, dtype=torch.int32)
|
||||
for i in range(M):
|
||||
topk_ids[i] = torch.randperm(E, generator=torch.Generator().manual_seed(i))[
|
||||
:top_k
|
||||
].int()
|
||||
|
||||
output = torch.empty(M, K, dtype=torch.bfloat16)
|
||||
ops.fused_experts_cpu(
|
||||
output,
|
||||
x_fp8,
|
||||
packed_w13,
|
||||
packed_w2,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
ops.CPUQuantMethod.FP8_W8A8,
|
||||
packed_w13_scale, # w1_scale: [E, Nc, G, BLOCK_N]
|
||||
w2_scale, # w2_scale: [E, K//128, N//128]
|
||||
None, # w1_zero
|
||||
None, # w2_zero
|
||||
[FP8_W8A8_QUANT_GROUP, FP8_W8A8_QUANT_GROUP], # block_size
|
||||
None, # w1_bias
|
||||
None, # w2_bias
|
||||
None, # alpha
|
||||
None, # limit
|
||||
True, # is_vnni (weights already packed)
|
||||
x_scales, # a1_scale: per-token FP8 activation scales
|
||||
)
|
||||
|
||||
assert output.shape == (M, K)
|
||||
assert output.dtype == torch.bfloat16
|
||||
assert not torch.isnan(output).any()
|
||||
assert not torch.isinf(output).any()
|
||||
|
||||
x_dequant = (x_fp8.float() * x_scales.unsqueeze(1)).bfloat16()
|
||||
ref = _ref_fp8_w8a8_moe(
|
||||
x_dequant, w13, w13_scale, w2, w2_scale, topk_weights, topk_ids.long(), N
|
||||
)
|
||||
torch.testing.assert_close(output.float(), ref.float(), atol=1e-2, rtol=1e-2)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(pytest.main([__file__, "-v"]))
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
"""Tests for CPU FP8 W8A16 block-scaled GEMM kernel (fp8_scaled_mm_cpu).
|
||||
"""Tests for CPU FP8 scaled-mm GEMM kernels.
|
||||
|
||||
Covers:
|
||||
* fp8_scaled_mm_cpu (W8A16)
|
||||
* fp8_scaled_mm_with_quant (W8A8)
|
||||
|
||||
Run `pytest tests/kernels/quantization/test_cpu_fp8_scaled_mm.py -v`.
|
||||
"""
|
||||
@@ -23,19 +27,31 @@ from vllm.platforms import current_platform
|
||||
if not current_platform.is_cpu():
|
||||
pytest.skip("skipping CPU-only tests", allow_module_level=True)
|
||||
|
||||
if not ops._supports_cpu_fp8_w8a16:
|
||||
pytest.skip("fp8_scaled_mm_cpu op not available", allow_module_level=True)
|
||||
requires_cpu_fp8_w8a16 = pytest.mark.skipif(
|
||||
not ops._supports_cpu_fp8_w8a16, reason="fp8_scaled_mm_cpu op not available"
|
||||
)
|
||||
requires_cpu_fp8_w8a8 = pytest.mark.skipif(
|
||||
not ops.cpu_has_amx_fp8(),
|
||||
reason="requires native AMX-FP8 hardware",
|
||||
)
|
||||
|
||||
if not torch.cpu._is_amx_tile_supported():
|
||||
pytest.skip("requires AMX tile support", allow_module_level=True)
|
||||
|
||||
FP8_MAX = torch.finfo(torch.float8_e4m3fn).max
|
||||
BLOCK_SIZE = [128, 128]
|
||||
GROUP_SIZE = 128
|
||||
|
||||
|
||||
def cdiv(a: int, b: int) -> int:
|
||||
return -(a // -b)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# W8A16 helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def quantize_weight_block_fp8(
|
||||
weight: torch.Tensor,
|
||||
block_size: list[int],
|
||||
@@ -126,7 +142,7 @@ def ref_fp8_block_scaled_mm(
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test parameters
|
||||
# W8A16 test: fp8_scaled_mm_cpu
|
||||
# ---------------------------------------------------------------------------
|
||||
M_SIZES = [1, 4, 16, 64, 128]
|
||||
# (N, K) — weight shape is [N, K], output has N columns.
|
||||
@@ -141,6 +157,7 @@ NK_SIZES = [
|
||||
]
|
||||
|
||||
|
||||
@requires_cpu_fp8_w8a16
|
||||
@pytest.mark.parametrize("M", M_SIZES)
|
||||
@pytest.mark.parametrize("N,K", NK_SIZES)
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
@@ -200,6 +217,77 @@ def ref_fp8_per_tensor_scaled_mm(
|
||||
return out.to(out_dtype)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# W8A8 helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def quantize_weight_per_tensor(
|
||||
weight: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Quantize weight [N, K] to FP8 with a single per-tensor scale, broadcast
|
||||
to [N, 1] (the shape float8_linear_prepack_cpu expects)."""
|
||||
N = weight.shape[0]
|
||||
abs_max = weight.abs().max()
|
||||
scale = (abs_max / FP8_MAX).clamp(min=1e-7)
|
||||
q = (weight.float() / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
|
||||
return q, scale.expand(N).contiguous().view(N, 1)
|
||||
|
||||
|
||||
def quantize_weight_per_channel(
|
||||
weight: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Quantize weight [N, K] to FP8 with per-channel (per-row) scales [N, 1]."""
|
||||
abs_max = weight.abs().amax(dim=1, keepdim=True)
|
||||
scale = (abs_max / FP8_MAX).clamp(min=1e-7)
|
||||
q = (weight.float() / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
|
||||
return q, scale.float()
|
||||
|
||||
|
||||
def quantize_weight_per_group(
|
||||
weight: torch.Tensor,
|
||||
group_size: int = GROUP_SIZE,
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Quantize weight [N, K] to FP8 with per-group (128-block) scales [N, G]."""
|
||||
N, K = weight.shape
|
||||
G = K // group_size
|
||||
w_groups = weight.view(N, G, group_size)
|
||||
abs_max = w_groups.abs().amax(dim=2, keepdim=True)
|
||||
scale = (abs_max / FP8_MAX).clamp(min=1e-7)
|
||||
q = (w_groups.float() / scale).clamp(-FP8_MAX, FP8_MAX).to(torch.float8_e4m3fn)
|
||||
return q.view(N, K), scale.squeeze(2).float()
|
||||
|
||||
|
||||
def quantize_act_per_token(x: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Reference per-token (row) dynamic FP8 quantization of activations."""
|
||||
x_f = x.float()
|
||||
scale = (x_f.abs().amax(dim=1, keepdim=True) / FP8_MAX).clamp(min=1e-7)
|
||||
scaled = (x_f / scale).clamp(-FP8_MAX, FP8_MAX)
|
||||
# Round via fp16 to match the AVX10.2 kernel path.
|
||||
q = scaled.half().to(torch.float8_e4m3fn)
|
||||
return q, scale
|
||||
|
||||
|
||||
def reference_static_fp8_linear(
|
||||
x: torch.Tensor, # BF16 [M, K]
|
||||
weight_fp8: torch.Tensor, # FP8 [N, K]
|
||||
weight_scale: torch.Tensor, # float32 [N, 1]
|
||||
act_scale: torch.Tensor, # float32 scalar
|
||||
bias: torch.Tensor | None,
|
||||
out_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
"""Static per-tensor: quantize/dequant x with act_scale, dequant weight, matmul."""
|
||||
x_scaled = (x.float() / act_scale.item()).clamp(-FP8_MAX, FP8_MAX)
|
||||
# Round via fp16 to match the AVX10.2 kernel path.
|
||||
x_q = x_scaled.half().to(torch.float8_e4m3fn)
|
||||
x_dq = x_q.float() * act_scale.item()
|
||||
w_dq = weight_fp8.float() * weight_scale.view(-1, 1)
|
||||
out = torch.mm(x_dq, w_dq.t())
|
||||
if bias is not None:
|
||||
out = out + bias.float()
|
||||
return out.to(out_dtype)
|
||||
|
||||
|
||||
NK_SIZES_PER_TENSOR = [
|
||||
(32, 64),
|
||||
(5120, 5120),
|
||||
@@ -274,3 +362,136 @@ def test_cpu_fp8_per_tensor_kernel_rejects_non_multiple_of_32_n(n: int):
|
||||
)
|
||||
supported, _ = CPUFp8PerTensorScaledMMLinearKernel.can_implement(config)
|
||||
assert not supported
|
||||
|
||||
|
||||
def reference_dynamic_fp8_linear(
|
||||
x: torch.Tensor, # BF16 [M, K]
|
||||
weight_fp8: torch.Tensor, # FP8 [N, K]
|
||||
weight_scale: torch.Tensor, # float32 [N, G]
|
||||
bias: torch.Tensor | None,
|
||||
out_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
"""Dynamic per-token act x per-group weight: quantize x per-token,
|
||||
dequant weight per-group, matmul."""
|
||||
N, K = weight_fp8.shape
|
||||
G = weight_scale.shape[1]
|
||||
group_size = K // G
|
||||
x_q, x_scale = quantize_act_per_token(x)
|
||||
x_dq = x_q.float() * x_scale
|
||||
w_dq = (
|
||||
weight_fp8.float().view(N, G, group_size) * weight_scale.view(N, G, 1)
|
||||
).view(N, K)
|
||||
out = torch.mm(x_dq, w_dq.t())
|
||||
if bias is not None:
|
||||
out = out + bias.float()
|
||||
return out.to(out_dtype)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# W8A8 tests: fp8_scaled_mm_with_quant
|
||||
# ---------------------------------------------------------------------------
|
||||
W8A8_M_SIZES = [1, 4, 16, 64]
|
||||
W8A8_NK_SIZES = [(128, 256), (256, 512), (64, 128)]
|
||||
|
||||
|
||||
@requires_cpu_fp8_w8a8
|
||||
@pytest.mark.parametrize("static_activation", [True, False])
|
||||
@pytest.mark.parametrize("M", W8A8_M_SIZES)
|
||||
@pytest.mark.parametrize("N,K", W8A8_NK_SIZES)
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
def test_cpu_fp8_w8a8_per_tensor(
|
||||
M: int, N: int, K: int, use_bias: bool, static_activation: bool
|
||||
):
|
||||
"""CPUFP8W8A8ScaledMMLinearKernel with per-tensor weight scale."""
|
||||
torch.manual_seed(42)
|
||||
out_dtype = torch.bfloat16
|
||||
|
||||
x = torch.randn(M, K, dtype=torch.bfloat16)
|
||||
weight = torch.randn(N, K, dtype=torch.bfloat16)
|
||||
bias = torch.randn(N, dtype=torch.float32) if use_bias else None
|
||||
|
||||
w_fp8, w_scale = quantize_weight_per_tensor(weight)
|
||||
packed_weight, packed_scale = torch.ops._C.float8_linear_prepack_cpu(w_fp8, w_scale)
|
||||
|
||||
if static_activation:
|
||||
act_scale = (x.float().abs().max() / FP8_MAX).clamp(min=1e-7).view(1)
|
||||
kernel_out = ops.fp8_scaled_mm_with_quant(
|
||||
x, act_scale, False, packed_weight, packed_scale, bias, out_dtype
|
||||
)
|
||||
ref_out = reference_static_fp8_linear(
|
||||
x, w_fp8, w_scale, act_scale, bias, out_dtype
|
||||
)
|
||||
atol = 0.05
|
||||
else:
|
||||
kernel_out = ops.fp8_scaled_mm_with_quant(
|
||||
x, None, True, packed_weight, packed_scale, bias, out_dtype
|
||||
)
|
||||
ref_out = reference_dynamic_fp8_linear(x, w_fp8, w_scale, bias, out_dtype)
|
||||
atol = 0.05
|
||||
|
||||
assert kernel_out.dtype == out_dtype
|
||||
torch.testing.assert_close(kernel_out, ref_out, rtol=0.05, atol=atol)
|
||||
|
||||
|
||||
@requires_cpu_fp8_w8a8
|
||||
@pytest.mark.parametrize("static_activation", [True, False])
|
||||
@pytest.mark.parametrize("M", W8A8_M_SIZES)
|
||||
@pytest.mark.parametrize("N,K", W8A8_NK_SIZES)
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
def test_cpu_fp8_w8a8_per_channel(
|
||||
M: int, N: int, K: int, use_bias: bool, static_activation: bool
|
||||
):
|
||||
"""CPUFP8W8A8ScaledMMLinearKernel with per-channel weight scale."""
|
||||
torch.manual_seed(42)
|
||||
out_dtype = torch.bfloat16
|
||||
|
||||
x = torch.randn(M, K, dtype=torch.bfloat16)
|
||||
weight = torch.randn(N, K, dtype=torch.bfloat16)
|
||||
bias = torch.randn(N, dtype=torch.float32) if use_bias else None
|
||||
|
||||
w_fp8, w_scale = quantize_weight_per_channel(weight)
|
||||
packed_weight, packed_scale = torch.ops._C.float8_linear_prepack_cpu(w_fp8, w_scale)
|
||||
|
||||
if static_activation:
|
||||
act_scale = (x.float().abs().max() / FP8_MAX).clamp(min=1e-7).view(1)
|
||||
kernel_out = ops.fp8_scaled_mm_with_quant(
|
||||
x, act_scale, False, packed_weight, packed_scale, bias, out_dtype
|
||||
)
|
||||
ref_out = reference_static_fp8_linear(
|
||||
x, w_fp8, w_scale, act_scale, bias, out_dtype
|
||||
)
|
||||
atol = 0.05
|
||||
else:
|
||||
kernel_out = ops.fp8_scaled_mm_with_quant(
|
||||
x, None, True, packed_weight, packed_scale, bias, out_dtype
|
||||
)
|
||||
ref_out = reference_dynamic_fp8_linear(x, w_fp8, w_scale, bias, out_dtype)
|
||||
atol = 0.05
|
||||
|
||||
assert kernel_out.dtype == out_dtype
|
||||
torch.testing.assert_close(kernel_out, ref_out, rtol=0.05, atol=atol)
|
||||
|
||||
|
||||
@requires_cpu_fp8_w8a8
|
||||
@pytest.mark.parametrize("M", W8A8_M_SIZES)
|
||||
@pytest.mark.parametrize("N,K", W8A8_NK_SIZES)
|
||||
@pytest.mark.parametrize("use_bias", [False, True])
|
||||
def test_cpu_fp8_w8a8_per_group(M: int, N: int, K: int, use_bias: bool):
|
||||
"""CPUFp8W8A8BlockScaledMMKernel: per-group (128-block) weight scale +
|
||||
dynamic per-token activation scale."""
|
||||
torch.manual_seed(42)
|
||||
out_dtype = torch.bfloat16
|
||||
|
||||
x = torch.randn(M, K, dtype=torch.bfloat16)
|
||||
weight = torch.randn(N, K, dtype=torch.bfloat16)
|
||||
bias = torch.randn(N, dtype=torch.float32) if use_bias else None
|
||||
|
||||
w_fp8, w_scale = quantize_weight_per_group(weight)
|
||||
packed_weight, packed_scale = torch.ops._C.float8_linear_prepack_cpu(w_fp8, w_scale)
|
||||
kernel_out = ops.fp8_scaled_mm_with_quant(
|
||||
x, None, True, packed_weight, packed_scale, bias, out_dtype
|
||||
)
|
||||
ref_out = reference_dynamic_fp8_linear(x, w_fp8, w_scale, bias, out_dtype)
|
||||
|
||||
assert kernel_out.dtype == out_dtype
|
||||
torch.testing.assert_close(kernel_out, ref_out, rtol=0.05, atol=0.05)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
|
||||
|
||||
from enum import IntEnum
|
||||
from functools import cache
|
||||
from typing import TYPE_CHECKING, Literal
|
||||
|
||||
import torch
|
||||
@@ -3384,6 +3385,7 @@ class CPUQuantMethod(IntEnum):
|
||||
FP8_W8A16 = 2
|
||||
INT4_W4A8 = 3
|
||||
MXFP4 = 4
|
||||
FP8_W8A8 = 5
|
||||
|
||||
|
||||
if hasattr(torch.ops._C, "dynamic_4bit_int_moe"):
|
||||
@@ -3422,6 +3424,7 @@ def fused_experts_cpu(
|
||||
alpha: float | None = None,
|
||||
limit: float | None = None,
|
||||
is_vnni: bool = True,
|
||||
a1_scale: torch.Tensor | None = None,
|
||||
) -> None:
|
||||
torch.ops._C.fused_experts_cpu(
|
||||
out,
|
||||
@@ -3435,6 +3438,7 @@ def fused_experts_cpu(
|
||||
w2_scale,
|
||||
w1_zero,
|
||||
w2_zero,
|
||||
a1_scale,
|
||||
block_size,
|
||||
w1_bias,
|
||||
w2_bias,
|
||||
@@ -3564,6 +3568,46 @@ def fp8_scaled_mm_cpu(
|
||||
)
|
||||
|
||||
|
||||
# FP8 W8A8 CPU kernels
|
||||
@cache
|
||||
def cpu_has_amx_fp8() -> bool:
|
||||
"""Whether this CPU has native AMX-FP8 MMA support."""
|
||||
if not hasattr(torch.ops._C, "cpu_has_amx_fp8"):
|
||||
return False
|
||||
return bool(torch.ops._C.cpu_has_amx_fp8())
|
||||
|
||||
|
||||
if hasattr(torch.ops._C, "fp8_scaled_mm_with_quant"):
|
||||
|
||||
@register_fake("_C::fp8_scaled_mm_with_quant")
|
||||
def fp8_scaled_mm_with_quant_fake(
|
||||
act: torch.Tensor,
|
||||
act_scales: torch.Tensor | None,
|
||||
channelwise: bool,
|
||||
weight: torch.Tensor,
|
||||
weight_scales: torch.Tensor,
|
||||
bias: torch.Tensor | None,
|
||||
output_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
M = act.reshape(-1, act.size(-1)).size(0)
|
||||
N = weight.size(0) * weight.size(-1)
|
||||
return torch.empty((M, N), dtype=output_dtype, device=act.device)
|
||||
|
||||
|
||||
def fp8_scaled_mm_with_quant(
|
||||
act: torch.Tensor,
|
||||
act_scales: torch.Tensor | None,
|
||||
channelwise: bool,
|
||||
weight: torch.Tensor,
|
||||
weight_scales: torch.Tensor,
|
||||
bias: torch.Tensor | None,
|
||||
output_dtype: torch.dtype,
|
||||
) -> torch.Tensor:
|
||||
return torch.ops._C.fp8_scaled_mm_with_quant(
|
||||
act, act_scales, channelwise, weight, weight_scales, bias, output_dtype
|
||||
)
|
||||
|
||||
|
||||
def chunk_gated_delta_rule_cpu(
|
||||
query: torch.Tensor,
|
||||
key: torch.Tensor,
|
||||
|
||||
@@ -186,6 +186,8 @@ from vllm.model_executor.kernels.linear.scaled_mm.b12x import (
|
||||
from vllm.model_executor.kernels.linear.scaled_mm.cpu import (
|
||||
CPUFp8BlockScaledMMKernel,
|
||||
CPUFp8PerTensorScaledMMLinearKernel,
|
||||
CPUFp8W8A8BlockScaledMMKernel,
|
||||
CPUFP8W8A8ScaledMMLinearKernel,
|
||||
CPUInt8ScaledMMLinearKernel,
|
||||
)
|
||||
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import (
|
||||
@@ -436,6 +438,7 @@ _POSSIBLE_FP8_KERNELS: dict[PlatformEnum, list[type[FP8ScaledMMLinearKernel]]] =
|
||||
ChannelWiseTorchFP8ScaledMMLinearKernel,
|
||||
],
|
||||
PlatformEnum.CPU: [
|
||||
CPUFP8W8A8ScaledMMLinearKernel,
|
||||
CPUFp8PerTensorScaledMMLinearKernel,
|
||||
PerTensorTorchFP8ScaledMMLinearKernel,
|
||||
ChannelWiseTorchFP8ScaledMMLinearKernel,
|
||||
@@ -468,6 +471,7 @@ _POSSIBLE_FP8_BLOCK_KERNELS: dict[
|
||||
TritonFp8BlockScaledMMKernel,
|
||||
],
|
||||
PlatformEnum.CPU: [
|
||||
CPUFp8W8A8BlockScaledMMKernel, # W8A8 preferred; falls back to W8A16 below
|
||||
CPUFp8BlockScaledMMKernel,
|
||||
],
|
||||
PlatformEnum.XPU: [
|
||||
|
||||
@@ -10,6 +10,7 @@ from vllm.model_executor.kernels.linear.scaled_mm.BlockScaledMMLinearKernel impo
|
||||
from vllm.model_executor.kernels.linear.scaled_mm.cpu import (
|
||||
CPUFp8BlockScaledMMKernel,
|
||||
CPUFp8PerTensorScaledMMLinearKernel,
|
||||
CPUFP8W8A8ScaledMMLinearKernel,
|
||||
CPUInt8ScaledMMLinearKernel,
|
||||
)
|
||||
from vllm.model_executor.kernels.linear.scaled_mm.cutlass import (
|
||||
@@ -69,5 +70,6 @@ __all__ = [
|
||||
"Fp8BlockScaledMMLinearKernel",
|
||||
"CPUFp8BlockScaledMMKernel",
|
||||
"CPUFp8PerTensorScaledMMLinearKernel",
|
||||
"CPUFP8W8A8ScaledMMLinearKernel",
|
||||
"XPUFp8BlockScaledMMKernel",
|
||||
]
|
||||
|
||||
@@ -270,12 +270,17 @@ class CPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
||||
# Skip the base class process (FP8 padding / fnuz normalization)
|
||||
# which is GPU-oriented. Instead, VNNI-prepack weights for AMX.
|
||||
params = self._get_layer_params(layer)
|
||||
packed_weight = torch.ops._C.convert_weight_packed(params.weight)
|
||||
replace_parameter(
|
||||
layer,
|
||||
params.WEIGHT,
|
||||
torch.nn.Parameter(packed_weight, requires_grad=False),
|
||||
)
|
||||
|
||||
# MLA's kv_b_proj (see `_cpu_skip_gemm_dispatch`): MLA reads this
|
||||
# weight to derive W_UK/W_UV, so keep the checkpoint [N, K] layout and
|
||||
# let the kernel pack on the fly instead.
|
||||
if not getattr(layer, "_cpu_skip_gemm_dispatch", False):
|
||||
packed_weight = torch.ops._C.convert_weight_packed(params.weight)
|
||||
replace_parameter(
|
||||
layer,
|
||||
params.WEIGHT,
|
||||
torch.nn.Parameter(packed_weight, requires_grad=False),
|
||||
)
|
||||
|
||||
# Re-wrap scale as a plain Parameter so the kernel can read it
|
||||
# without weight-loader metadata interfering.
|
||||
@@ -320,7 +325,7 @@ class CPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
||||
list(self.weight_group_shape),
|
||||
bias,
|
||||
x.dtype,
|
||||
True, # is_vnni (weight already prepacked)
|
||||
not getattr(layer, "_cpu_skip_gemm_dispatch", False), # is_vnni
|
||||
)
|
||||
return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
|
||||
|
||||
@@ -430,3 +435,294 @@ class CPUFp8PerTensorScaledMMLinearKernel(FP8ScaledMMLinearKernel):
|
||||
raise NotImplementedError(
|
||||
"CPUFp8PerTensorScaledMMLinearKernel overrides apply_weights directly."
|
||||
)
|
||||
|
||||
|
||||
class CPUFp8W8A8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
|
||||
"""FP8 W8A8 block-quantized GEMM with dynamic per-token activation quant on CPU."""
|
||||
|
||||
# Activation quant is done inside fp8_scaled_mm_with_quant;
|
||||
# the base class must not apply a second quant step.
|
||||
apply_input_quant = False
|
||||
|
||||
@classmethod
|
||||
def is_supported(
|
||||
cls, compute_capability: int | None = None
|
||||
) -> tuple[bool, str | None]:
|
||||
if not current_platform.is_cpu():
|
||||
return False, "requires CPU platform."
|
||||
if not torch.cpu._is_amx_tile_supported():
|
||||
return False, "requires AMX tile support (Sapphire Rapids or newer)."
|
||||
if not ops.cpu_has_amx_fp8():
|
||||
return False, (
|
||||
"requires native AMX-FP8 hardware; falls back "
|
||||
"to CPUFp8BlockScaledMMKernel (W8A16) otherwise."
|
||||
)
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def can_implement(
|
||||
cls, config: FP8ScaledMMLinearLayerConfig
|
||||
) -> tuple[bool, str | None]:
|
||||
# Same block-shape requirements as the W8A16 sibling kernel.
|
||||
weight_gs = config.weight_quant_key.scale.group_shape
|
||||
if weight_gs.col <= 0 or weight_gs.col != 128:
|
||||
return False, (
|
||||
"CPU FP8 W8A8 block kernel requires K-dimension block size of 128, "
|
||||
f"got {weight_gs.col}."
|
||||
)
|
||||
if weight_gs.row <= 0 or weight_gs.row % 32 != 0:
|
||||
return False, (
|
||||
"CPU FP8 W8A8 block kernel requires N-dimension block size to be "
|
||||
f"a positive multiple of 32, got {weight_gs.row}."
|
||||
)
|
||||
if config.out_dtype not in (torch.bfloat16, torch.float32):
|
||||
return False, "Only bfloat16/float32 output dtype supported."
|
||||
n = config.weight_shape[0]
|
||||
if n % 32 != 0:
|
||||
# float8_linear_prepack_cpu tiles N in chunks of 32 and cannot
|
||||
# handle a remainder tile smaller than that.
|
||||
return False, f"requires weight output dim (N={n}) to be a multiple of 32."
|
||||
return True, None
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
||||
"""Prepack block-quantized FP8 weight for W8A8 AMX BRGEMM.
|
||||
|
||||
For block-quant FP8 models weight is stored as [N, K] (NOT transposed,
|
||||
unlike non-block FP8 where vLLM stores [K, N]).
|
||||
|
||||
float8_linear_prepack_cpu expects:
|
||||
weight: [N, K]
|
||||
scales: [N, G] where G = K / block_k
|
||||
|
||||
weight_scale_inv from the checkpoint has shape [ceil(N/block_n), G],
|
||||
so we expand rows from ceil(N/block_n) to N via repeat_interleave.
|
||||
"""
|
||||
params = self._get_layer_params(layer)
|
||||
weight = params.weight # [N, K]
|
||||
|
||||
# MLA's kv_b_proj (see `_cpu_skip_gemm_dispatch`): MLA reads this
|
||||
# weight to derive W_UK/W_UV, so keep the checkpoint [N, K] layout and
|
||||
# its block scales; apply_weights falls back to the W8A16 op.
|
||||
if getattr(layer, "_cpu_skip_gemm_dispatch", False):
|
||||
return
|
||||
|
||||
weight_scale = (
|
||||
params.weight_scale_inv
|
||||
if params.weight_scale_inv is not None
|
||||
else params.weight_scale
|
||||
) # [ceil(N/block_n), ceil(K/block_k)]
|
||||
assert weight_scale is not None, "FP8 W8A8 block kernel requires weight scale."
|
||||
|
||||
N = weight.shape[0]
|
||||
block_n = self.weight_group_shape.row # e.g. 128
|
||||
|
||||
# Expand scale rows: [ceil(N/block_n), G] → [N, G]
|
||||
if weight_scale.size(0) != N:
|
||||
weight_scale_expanded = weight_scale.repeat_interleave(block_n, dim=0)[
|
||||
:N, :
|
||||
].contiguous()
|
||||
else:
|
||||
weight_scale_expanded = weight_scale.contiguous()
|
||||
|
||||
packed_weight, packed_scales = torch.ops._C.float8_linear_prepack_cpu(
|
||||
weight, weight_scale_expanded.to(torch.float32)
|
||||
)
|
||||
|
||||
scale_attr = (
|
||||
params.WEIGHT_SCALE_INV
|
||||
if params.weight_scale_inv is not None
|
||||
else params.WEIGHT_SCALE
|
||||
)
|
||||
replace_parameter(
|
||||
layer,
|
||||
params.WEIGHT,
|
||||
torch.nn.Parameter(packed_weight, requires_grad=False),
|
||||
)
|
||||
replace_parameter(
|
||||
layer,
|
||||
scale_attr,
|
||||
torch.nn.Parameter(packed_scales, requires_grad=False),
|
||||
)
|
||||
|
||||
def apply_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
params = self._get_layer_params(layer)
|
||||
weight_scale = (
|
||||
params.weight_scale_inv
|
||||
if params.weight_scale_inv is not None
|
||||
else params.weight_scale
|
||||
)
|
||||
|
||||
x_2d = x.reshape(-1, x.shape[-1]) if x.dim() > 2 else x
|
||||
if getattr(layer, "_cpu_skip_gemm_dispatch", False):
|
||||
# Unpacked weight: fp8_scaled_mm_with_quant has no is_vnni option,
|
||||
# so fall back to the W8A16 op (BF16 activation, block-scaled FP8
|
||||
# weight) for this layer.
|
||||
out = torch.ops._C.fp8_scaled_mm_cpu(
|
||||
x_2d,
|
||||
params.weight,
|
||||
weight_scale,
|
||||
list(self.weight_group_shape),
|
||||
bias,
|
||||
x.dtype,
|
||||
False, # is_vnni
|
||||
)
|
||||
return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
|
||||
|
||||
# act_scales=None + channelwise=True → dynamic per-token activation quant
|
||||
out = ops.fp8_scaled_mm_with_quant(
|
||||
x_2d,
|
||||
None,
|
||||
True,
|
||||
params.weight,
|
||||
weight_scale,
|
||||
bias,
|
||||
x.dtype,
|
||||
)
|
||||
return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
|
||||
|
||||
def apply_block_scaled_mm(
|
||||
self,
|
||||
A: torch.Tensor,
|
||||
B: torch.Tensor,
|
||||
As: torch.Tensor,
|
||||
Bs: torch.Tensor,
|
||||
) -> torch.Tensor:
|
||||
raise NotImplementedError(
|
||||
"CPUFp8W8A8BlockScaledMMKernel overrides apply_weights directly."
|
||||
)
|
||||
|
||||
|
||||
class CPUFP8W8A8ScaledMMLinearKernel(FP8ScaledMMLinearKernel):
|
||||
"""FP8 W8A8 GEMM with dynamic per-token activation quantization on CPU."""
|
||||
|
||||
@classmethod
|
||||
def is_supported(
|
||||
cls, compute_capability: int | None = None
|
||||
) -> tuple[bool, str | None]:
|
||||
if not current_platform.is_cpu():
|
||||
return False, "requires CPU platform."
|
||||
if not torch.cpu._is_amx_tile_supported():
|
||||
return False, "requires AMX tile support (Sapphire Rapids or newer)."
|
||||
if not ops.cpu_has_amx_fp8():
|
||||
return False, (
|
||||
"requires native AMX-FP8 hardware (Diamond Rapids); falls back "
|
||||
"to the Torch FP8 kernels otherwise."
|
||||
)
|
||||
return True, None
|
||||
|
||||
@classmethod
|
||||
def can_implement(
|
||||
cls, config: FP8ScaledMMLinearLayerConfig
|
||||
) -> tuple[bool, str | None]:
|
||||
if config.out_dtype not in (torch.bfloat16, torch.float16, torch.float32):
|
||||
return False, "Only bfloat16/float16/float32 output dtype supported."
|
||||
# Reject block-quantized weights — those go to CPUFp8BlockScaledMMKernel
|
||||
weight_gs = config.weight_quant_key.scale.group_shape
|
||||
if weight_gs.col > 0 and weight_gs.row > 0:
|
||||
return False, "Block-quantized weights not handled by FP8 W8A8 kernel."
|
||||
n = config.weight_shape[0]
|
||||
if n % 32 != 0:
|
||||
# float8_linear_prepack_cpu tiles N in chunks of 32 and cannot
|
||||
# handle a remainder tile smaller than that.
|
||||
return False, f"requires weight output dim (N={n}) to be a multiple of 32."
|
||||
return True, None
|
||||
|
||||
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
|
||||
"""Prepack weights with float8_linear_prepack_cpu (VNNI + block layout)."""
|
||||
w_name, ws_name, xs_name, _ = self.layer_param_names
|
||||
weight = getattr(layer, w_name)
|
||||
weight_scale = getattr(layer, ws_name)
|
||||
|
||||
# vLLM stores linear weight as [K, N], while the CPU prepack op expects [N, K].
|
||||
N = weight.shape[-1]
|
||||
|
||||
# Ensure scale is 2D [N, G]
|
||||
ws = weight_scale
|
||||
if ws is None:
|
||||
raise RuntimeError("FP8 W8A8 kernel requires weight scale.")
|
||||
ws = ws.to(torch.float32)
|
||||
|
||||
# Fused modules may carry per-shard scales (e.g. QKV/MLP shards).
|
||||
# Expand those to per-output-channel scales before prepack.
|
||||
if (
|
||||
len(getattr(layer, "logical_widths", [])) > 1
|
||||
and ws.dim() <= 1
|
||||
and ws.numel() == len(layer.logical_widths)
|
||||
):
|
||||
ws = convert_to_channelwise(ws.reshape(-1), layer.logical_widths)
|
||||
|
||||
if ws.dim() == 0:
|
||||
ws = ws.view(1, 1).expand(N, 1)
|
||||
elif ws.dim() == 1:
|
||||
if ws.numel() == 1:
|
||||
ws = ws.view(1, 1).expand(N, 1)
|
||||
elif ws.numel() == N:
|
||||
ws = ws.unsqueeze(1) # [N] -> [N, 1]
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"FP8 W8A8 kernel got unsupported 1D weight scale shape: "
|
||||
f"{tuple(ws.shape)} for N={N}."
|
||||
)
|
||||
|
||||
# Align scale rows to output channels N.
|
||||
if ws.size(0) != N:
|
||||
if ws.size(0) == 1:
|
||||
ws = ws.expand(N, ws.size(1)).contiguous()
|
||||
else:
|
||||
raise RuntimeError(
|
||||
"FP8 W8A8 kernel got mismatched weight scale rows: "
|
||||
f"ws_rows={ws.size(0)}, expected N={N}."
|
||||
)
|
||||
|
||||
w_2d = weight.t().contiguous() # [N, K]
|
||||
packed_weight, packed_scales = torch.ops._C.float8_linear_prepack_cpu(w_2d, ws)
|
||||
|
||||
replace_parameter(
|
||||
layer,
|
||||
w_name,
|
||||
torch.nn.Parameter(packed_weight, requires_grad=False),
|
||||
)
|
||||
replace_parameter(
|
||||
layer,
|
||||
ws_name,
|
||||
torch.nn.Parameter(packed_scales, requires_grad=False),
|
||||
)
|
||||
|
||||
def apply_weights(
|
||||
self,
|
||||
layer: torch.nn.Module,
|
||||
x: torch.Tensor,
|
||||
bias: torch.Tensor | None = None,
|
||||
**kwargs,
|
||||
) -> torch.Tensor:
|
||||
w_name, ws_name, xs_name, _ = self.layer_param_names
|
||||
weight = getattr(layer, w_name)
|
||||
weight_scale = getattr(layer, ws_name)
|
||||
# For static quantization, use pre-computed activation scale
|
||||
act_scales = getattr(layer, xs_name, None) if xs_name else None
|
||||
|
||||
x_2d = x.reshape(-1, x.shape[-1]) if x.dim() > 2 else x
|
||||
# channelwise=True: per-token (per-row) quantization when act_scales is None
|
||||
# channelwise=False: per-tensor quantization when act_scales is provided
|
||||
channelwise = act_scales is None
|
||||
out = ops.fp8_scaled_mm_with_quant(
|
||||
x_2d,
|
||||
act_scales,
|
||||
channelwise,
|
||||
weight,
|
||||
weight_scale,
|
||||
bias,
|
||||
x.dtype,
|
||||
)
|
||||
return out.reshape(x.shape[:-1] + (out.size(-1),)) if x.dim() > 2 else out
|
||||
|
||||
def apply_scaled_mm(self, A, B, out_dtype, As, Bs, bias=None, **kwargs):
|
||||
raise NotImplementedError(
|
||||
"CPUFP8W8A8ScaledMMLinearKernel uses apply_weights directly."
|
||||
)
|
||||
|
||||
@@ -1055,7 +1055,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
mqa_q_nope,
|
||||
self.impl._w_uk_packed, # type: ignore[attr-defined]
|
||||
True,
|
||||
None,
|
||||
self.impl._w_scale, # type: ignore[attr-defined]
|
||||
)
|
||||
mqa_ql_nope = mqa_ql_nope.transpose(0, 1)
|
||||
else:
|
||||
@@ -1401,7 +1401,7 @@ class MLAAttention(nn.Module, AttentionLayerBase):
|
||||
x,
|
||||
self.impl._w_uv_packed, # type: ignore[attr-defined]
|
||||
True,
|
||||
None,
|
||||
self.impl._w_scale, # type: ignore[attr-defined]
|
||||
)
|
||||
else:
|
||||
# Multiply + Transpose (N, B, L) x (N, L, V)->(N, B, V)->(B, N, V)
|
||||
|
||||
@@ -15,6 +15,7 @@ from vllm._custom_ops import (
|
||||
convert_weight_packed_scale_zp,
|
||||
cpu_fused_moe,
|
||||
cpu_fused_moe_int8,
|
||||
cpu_has_amx_fp8,
|
||||
cpu_prepack_moe_weight,
|
||||
cpu_prepack_moe_weight_int8,
|
||||
fused_experts_cpu,
|
||||
@@ -467,6 +468,7 @@ class CPUExpertsFp8(mk.FusedMoEExpertsModular):
|
||||
RoutingMethodType.Default,
|
||||
RoutingMethodType.Renormalize,
|
||||
RoutingMethodType.RenormalizeNaive,
|
||||
RoutingMethodType.DeepSeekV3,
|
||||
RoutingMethodType.DeepseekV4,
|
||||
RoutingMethodType.DeepSeekV3,
|
||||
]
|
||||
@@ -1612,3 +1614,218 @@ class ZenCPUExpertsInt8(mk.FusedMoEExpertsModular):
|
||||
self._w1_scale_bf16,
|
||||
self._w2_scale_bf16,
|
||||
)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# FP8 W8A8 MoE
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def prepare_fp8_w8a8_moe_layer_for_cpu(
|
||||
w13: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
w13_scale: torch.Tensor,
|
||||
w2_scale: torch.Tensor,
|
||||
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
|
||||
"""Prepack FP8 W8A8 MoE weights for CPU kernel."""
|
||||
E = w13.size(0)
|
||||
|
||||
packed_w13_list = []
|
||||
packed_w13_scale_list = []
|
||||
|
||||
for i in range(E):
|
||||
w1_exp = w13[i] # [2N, K] FP8
|
||||
ws_exp = w13_scale[i] # [2N, K/128] or [2N_blocks, K/128]
|
||||
|
||||
# Align scale rows to weight rows if necessary (for block-128 checkpoints
|
||||
# where scale may have fewer rows: [2N/128, K/128] → [2N, K/128])
|
||||
if ws_exp.size(0) < w1_exp.size(0):
|
||||
repeat_factor = w1_exp.size(0) // ws_exp.size(0)
|
||||
ws_exp = torch.repeat_interleave(ws_exp, repeat_factor, dim=0)
|
||||
ws_exp = ws_exp[: w1_exp.size(0), :].contiguous()
|
||||
|
||||
pw, ps = torch.ops._C.float8_linear_prepack_cpu(w1_exp, ws_exp)
|
||||
packed_w13_list.append(pw)
|
||||
packed_w13_scale_list.append(ps)
|
||||
|
||||
packed_w13 = torch.stack(packed_w13_list)
|
||||
packed_w13_scale = torch.stack(packed_w13_scale_list)
|
||||
|
||||
# w2 uses the W8A16 path (BF16 activation × FP8 weight), VNNI packed
|
||||
packed_w2 = torch.ops._C.convert_weight_packed(w2)
|
||||
return packed_w13, packed_w13_scale, packed_w2, w2_scale
|
||||
|
||||
|
||||
class CPUExpertsFp8W8A8(mk.FusedMoEExpertsModular):
|
||||
"""CPU FP8 W8A8 block-quantized modular MoE experts."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
moe_config: FusedMoEConfig,
|
||||
quant_config: FusedMoEQuantConfig,
|
||||
):
|
||||
super().__init__(moe_config, quant_config)
|
||||
|
||||
@property
|
||||
def expects_unquantized_inputs(self) -> bool:
|
||||
# Receives BF16 hidden states; quantization to FP8 is done inside apply()
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def activation_format() -> mk.FusedMoEActivationFormat:
|
||||
return mk.FusedMoEActivationFormat.Standard
|
||||
|
||||
@staticmethod
|
||||
def _supports_current_device() -> bool:
|
||||
# Requires native AMX-FP8 hardware.
|
||||
return current_platform.is_cpu() and cpu_has_amx_fp8()
|
||||
|
||||
@staticmethod
|
||||
def _supports_no_act_and_mul() -> bool:
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _supports_activation(activation: MoEActivation) -> bool:
|
||||
return activation == MoEActivation.SILU
|
||||
|
||||
@staticmethod
|
||||
def _supports_parallel_config(
|
||||
moe_parallel_config: FusedMoEParallelConfig,
|
||||
) -> bool:
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def _supports_quant_scheme(
|
||||
weight_key: QuantKey | None,
|
||||
activation_key: QuantKey | None,
|
||||
) -> bool:
|
||||
# Block-quantized FP8.
|
||||
return (weight_key, activation_key) == (
|
||||
kFp8Static128BlockSym,
|
||||
kFp8Dynamic128Sym,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _supports_routing_method(
|
||||
routing_method: RoutingMethodType,
|
||||
weight_key: QuantKey | None,
|
||||
activation_key: QuantKey | None,
|
||||
) -> bool:
|
||||
return routing_method in [
|
||||
RoutingMethodType.Default,
|
||||
RoutingMethodType.Renormalize,
|
||||
RoutingMethodType.RenormalizeNaive,
|
||||
RoutingMethodType.DeepSeekV3,
|
||||
RoutingMethodType.DeepseekV4,
|
||||
]
|
||||
|
||||
@staticmethod
|
||||
def _supports_router_logits_dtype(
|
||||
router_logits_dtype: torch.dtype | None,
|
||||
routing_method: RoutingMethodType,
|
||||
) -> bool:
|
||||
return True
|
||||
|
||||
def moe_problem_size(
|
||||
self,
|
||||
a1: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
) -> tuple[int, int, int, int, int]:
|
||||
# float8_linear_prepack_cpu blocks w1 into [E, Nc, Kc, block_k, block_n],
|
||||
# not the (E, N, K) layout the base implementation assumes -- N isn't
|
||||
# recoverable from that shape, so read it from moe_config instead.
|
||||
E = w1.shape[0]
|
||||
K = a1.size(-1)
|
||||
N = (
|
||||
self.moe_config.intermediate_size_per_partition
|
||||
* self.moe_config.w13_num_shards
|
||||
)
|
||||
M = a1.size(0) if a1.dim() == 2 else a1.size(1)
|
||||
topk = topk_ids.size(1)
|
||||
return E, M, N, K, topk
|
||||
|
||||
def workspace_shapes(
|
||||
self,
|
||||
M: int,
|
||||
N: int,
|
||||
K: int,
|
||||
topk: int,
|
||||
global_num_experts: int,
|
||||
local_num_experts: int,
|
||||
expert_tokens_meta: mk.ExpertTokensMetadata | None,
|
||||
activation: MoEActivation,
|
||||
) -> tuple[tuple[int, ...], tuple[int, ...], tuple[int, ...]]:
|
||||
# fused_experts_cpu manages its own scratch space.
|
||||
return (0,), (0,), (M, K)
|
||||
|
||||
def finalize_weight_and_reduce_impl(self) -> mk.TopKWeightAndReduce:
|
||||
return TopKWeightAndReduceNoOP()
|
||||
|
||||
def apply(
|
||||
self,
|
||||
output: torch.Tensor,
|
||||
hidden_states: torch.Tensor,
|
||||
w1: torch.Tensor,
|
||||
w2: torch.Tensor,
|
||||
topk_weights: torch.Tensor,
|
||||
topk_ids: torch.Tensor,
|
||||
activation: MoEActivation,
|
||||
global_num_experts: int,
|
||||
expert_map: torch.Tensor | None,
|
||||
a1q_scale: torch.Tensor | None,
|
||||
a2_scale: torch.Tensor | None,
|
||||
workspace13: torch.Tensor,
|
||||
workspace2: torch.Tensor,
|
||||
expert_tokens_meta: mk.ExpertTokensMetadata | None,
|
||||
apply_router_weight_on_input: bool,
|
||||
) -> None:
|
||||
if apply_router_weight_on_input:
|
||||
# fused_experts_cpu always applies topk_weights internally on
|
||||
# combine; MoEPrepareAndFinalizeNoDPEPModular.prepare() would
|
||||
# also pre-apply it to hidden_states, double-weighting the
|
||||
# output. Not needed by any CPU FP8 model today.
|
||||
raise NotImplementedError(
|
||||
"CPUExpertsFp8W8A8 does not support apply_router_weight_on_input=True."
|
||||
)
|
||||
|
||||
block_shape = (
|
||||
list(self.quant_config.block_shape)
|
||||
if self.quant_config.block_shape
|
||||
else (
|
||||
[
|
||||
self.quant_config._w1.shape.row,
|
||||
self.quant_config._w1.shape.col,
|
||||
]
|
||||
if self.quant_config._w1.shape is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
|
||||
# Quantize hidden_states (BF16) → FP8 with per-token scales
|
||||
hidden_states_2d = hidden_states.reshape(-1, hidden_states.shape[-1])
|
||||
x_fp8, x_scales = torch.ops._C.quantize_fp8e4m3_vec(
|
||||
hidden_states_2d, True, None
|
||||
)
|
||||
|
||||
fused_experts_cpu(
|
||||
output,
|
||||
x_fp8,
|
||||
w1,
|
||||
w2,
|
||||
topk_weights,
|
||||
topk_ids,
|
||||
CPUQuantMethod.FP8_W8A8,
|
||||
self.w1_scale, # w1_scale
|
||||
self.w2_scale, # w2_scale
|
||||
None, # w1_zero
|
||||
None, # w2_zero
|
||||
block_shape, # block_size
|
||||
None, # w1_bias
|
||||
None, # w2_bias
|
||||
None, # alpha
|
||||
None, # limit
|
||||
True, # is_vnni
|
||||
x_scales, # a1_scale: per-token FP8 activation scales
|
||||
)
|
||||
|
||||
@@ -57,7 +57,9 @@ class Fp8MoeBackend(Enum):
|
||||
VLLM_CUTLASS = "VLLM_CUTLASS"
|
||||
BATCHED_VLLM_CUTLASS = "BATCHED_VLLM_CUTLASS"
|
||||
XPU = "XPU"
|
||||
# CPU FP8 W8A16 (BF16 activations); CPU_W8A8 needs native AMX-FP8.
|
||||
CPU = "CPU"
|
||||
CPU_W8A8 = "CPU_W8A8"
|
||||
HPC = "HPC"
|
||||
# Dequantize-to-BF16 emulation for MXFP8 on devices without a native
|
||||
# MXFP8 MoE kernel (e.g. ROCm). Weights pass through unchanged here.
|
||||
@@ -92,6 +94,7 @@ def _get_priority_backends(
|
||||
Fp8MoeBackend.BATCHED_VLLM_CUTLASS,
|
||||
Fp8MoeBackend.BATCHED_TRITON,
|
||||
Fp8MoeBackend.XPU,
|
||||
Fp8MoeBackend.CPU_W8A8,
|
||||
Fp8MoeBackend.CPU,
|
||||
Fp8MoeBackend.HPC,
|
||||
]
|
||||
@@ -129,8 +132,10 @@ def _get_priority_backends(
|
||||
_move_to_front(_AVAILABLE_BACKENDS, Fp8MoeBackend.XPU)
|
||||
|
||||
if current_platform.is_cpu():
|
||||
# CPU platform uses FP8 W8A16 fused MoE kernel.
|
||||
# W8A8 first: it falls through to the W8A16 backend whenever the
|
||||
# hardware (AMX-FP8) or the config isn't supported.
|
||||
_move_to_front(_AVAILABLE_BACKENDS, Fp8MoeBackend.CPU)
|
||||
_move_to_front(_AVAILABLE_BACKENDS, Fp8MoeBackend.CPU_W8A8)
|
||||
|
||||
return _AVAILABLE_BACKENDS
|
||||
|
||||
@@ -238,6 +243,13 @@ def backend_to_kernel_cls(
|
||||
|
||||
return [CPUExpertsFp8]
|
||||
|
||||
elif backend == Fp8MoeBackend.CPU_W8A8:
|
||||
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
|
||||
CPUExpertsFp8W8A8,
|
||||
)
|
||||
|
||||
return [CPUExpertsFp8W8A8]
|
||||
|
||||
elif backend == Fp8MoeBackend.HPC:
|
||||
from vllm.model_executor.layers.fused_moe.hpc_moe import (
|
||||
HPCExperts,
|
||||
@@ -652,6 +664,14 @@ def convert_to_fp8_moe_kernel_format(
|
||||
)
|
||||
|
||||
w13, w2 = prepare_fp8_moe_layer_for_cpu(w13, w2)
|
||||
elif fp8_backend == Fp8MoeBackend.CPU_W8A8:
|
||||
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
|
||||
prepare_fp8_w8a8_moe_layer_for_cpu,
|
||||
)
|
||||
|
||||
w13, w13_scale, w2, w2_scale = prepare_fp8_w8a8_moe_layer_for_cpu(
|
||||
w13, w2, w13_scale, w2_scale
|
||||
)
|
||||
else:
|
||||
if fp8_backend not in [
|
||||
Fp8MoeBackend.TRITON,
|
||||
@@ -697,7 +717,15 @@ def make_fp8_moe_quant_config(
|
||||
In a future PR, we will have this function should be
|
||||
a method of the modular kernel itself.
|
||||
"""
|
||||
# MARLIN and CPU are mixed precision W8A16 config.
|
||||
if fp8_backend == Fp8MoeBackend.CPU_W8A8:
|
||||
return fp8_w8a8_moe_quant_config(
|
||||
w1_scale=w1_scale,
|
||||
w2_scale=w2_scale,
|
||||
a1_scale=a1_scale,
|
||||
block_shape=block_shape,
|
||||
)
|
||||
|
||||
# MARLIN and CPU (W8A16) are mixed precision W8A16 configs.
|
||||
if fp8_backend == Fp8MoeBackend.MARLIN or fp8_backend == Fp8MoeBackend.CPU:
|
||||
return fp8_w8a16_moe_quant_config(
|
||||
w1_scale=w1_scale,
|
||||
|
||||
@@ -737,11 +737,13 @@ class Fp8PtpcOnlineMoEMethod(_Fp8OnlineMoEBase):
|
||||
)
|
||||
# Reject backends whose make_fp8_moe_quant_config branch silently
|
||||
# drops per_act_token_quant / per_out_ch_quant or collapses scales:
|
||||
# MARLIN / CPU route through fp8_w8a16_moe_quant_config; FLASHINFER_*
|
||||
# fold scales into a per-tensor alpha (oracle/fp8.py).
|
||||
# MARLIN / CPU route through fp8_w8a16_moe_quant_config, CPU_W8A8
|
||||
# ignores both flags, and FLASHINFER_* fold scales into a per-tensor
|
||||
# alpha (oracle/fp8.py).
|
||||
if self.fp8_backend in (
|
||||
Fp8MoeBackend.MARLIN,
|
||||
Fp8MoeBackend.CPU,
|
||||
Fp8MoeBackend.CPU_W8A8,
|
||||
Fp8MoeBackend.FLASHINFER_CUTLASS,
|
||||
Fp8MoeBackend.FLASHINFER_TRTLLM,
|
||||
):
|
||||
|
||||
@@ -61,6 +61,44 @@ _MIN_WORK_PER_SPLIT = 512
|
||||
_SPLIT_OCCUPANCY_MULTIPLIER = 2
|
||||
|
||||
|
||||
def _block_quant_to_tensor_quant(
|
||||
weight: torch.Tensor, # FP8 [N, K] block-quantized weight
|
||||
weight_scale: torch.Tensor, # [ceil(N/bn), ceil(K/bk)] block scales
|
||||
block_size: list[int], # [block_n, block_k]
|
||||
) -> tuple[torch.Tensor, torch.Tensor]:
|
||||
"""Re-quantize FP8 block-quant weight to FP8 per-tensor quant.
|
||||
|
||||
Returns (fp8_weight, dequant_scale) where fp8_weight * dequant_scale
|
||||
approximates the original float weight. dequant_scale is a 0-dim
|
||||
scalar tensor as expected by bmm_cpu's ``scale`` argument.
|
||||
|
||||
NOTE: block->tensor re-quantization may slightly affect accuracy
|
||||
(equivalent to SGLang's block_quant_to_tensor_quant for CPU).
|
||||
"""
|
||||
block_n, block_k = block_size[0], block_size[1]
|
||||
n, k = weight.shape
|
||||
|
||||
# Expand block scales [ceil(N/bn), ceil(K/bk)] -> [N, K]
|
||||
scale = weight_scale.to(torch.float32)
|
||||
if scale.shape[0] < n:
|
||||
scale = scale.repeat_interleave(block_n, dim=0)[:n, :]
|
||||
if scale.shape[1] < k:
|
||||
scale = scale.repeat_interleave(block_k, dim=1)[:, :k]
|
||||
|
||||
# Dequantize to float32
|
||||
x_f32 = weight.to(torch.float32) * scale
|
||||
|
||||
# Re-quantize per-tensor back to FP8
|
||||
fp8_max = torch.finfo(weight.dtype).max # 448.0 for e4m3fn
|
||||
amax = x_f32.abs().amax().clamp(min=1e-12)
|
||||
quant_scale = fp8_max / amax
|
||||
x_fp8 = (x_f32 * quant_scale).clamp(-fp8_max, fp8_max).to(weight.dtype)
|
||||
|
||||
# dequant_scale = 1 / quant_scale (0-dim tensor for bmm_cpu)
|
||||
dequant_scale = (amax / fp8_max).to(torch.float32)
|
||||
return x_fp8, dequant_scale
|
||||
|
||||
|
||||
def _compute_num_kv_splits(max_seq_len: int, num_threads: int) -> int:
|
||||
"""Mirrors TritonMLAImpl's _compute_num_kv_splits, using the CPU thread
|
||||
count in place of SM count."""
|
||||
@@ -208,6 +246,9 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
)
|
||||
self._w_uk_packed: torch.Tensor | None = None
|
||||
self._w_uv_packed: torch.Tensor | None = None
|
||||
self._w_scale: torch.Tensor | None = (
|
||||
None # per-tensor dequant scale (FP8 W8A16)
|
||||
)
|
||||
|
||||
def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
|
||||
# forward_mha (unlike forward_mqa) receives no `layer` argument, so it
|
||||
@@ -216,12 +257,40 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
# impl derives its own copies from self.kv_b_proj (already available
|
||||
# via the constructor) and VNNI-packs them once, ahead of time, for
|
||||
# use with bmm_cpu in forward_mha.
|
||||
kv_b_proj_weight = get_and_maybe_dequant_weights(
|
||||
self.kv_b_proj, out_dtype=act_dtype
|
||||
).T
|
||||
kv_b_proj_weight = kv_b_proj_weight.view(
|
||||
self.kv_lora_rank, self.num_heads, self.qk_nope_head_dim + self.v_head_dim
|
||||
)
|
||||
#
|
||||
# kv_b_proj carries `_cpu_skip_gemm_dispatch`, so the CPU FP8 kernels
|
||||
# leave its weight in the checkpoint [N, K] layout. For block-quant
|
||||
# FP8 checkpoints, re-quantize per-tensor and keep the BMM in FP8
|
||||
# (W8A16); everything else goes through the BF16 path.
|
||||
weight = getattr(self.kv_b_proj, "weight", None)
|
||||
block_size = getattr(self.kv_b_proj, "weight_block_size", None)
|
||||
if (
|
||||
weight is not None
|
||||
and weight.dtype == torch.float8_e4m3fn
|
||||
and block_size is not None
|
||||
):
|
||||
block_scale = getattr(self.kv_b_proj, "weight_scale_inv", None)
|
||||
if block_scale is None:
|
||||
block_scale = self.kv_b_proj.weight_scale
|
||||
fp8_weight, self._w_scale = _block_quant_to_tensor_quant(
|
||||
weight, block_scale, block_size
|
||||
)
|
||||
# [N, K] -> [K, N] = [kv_lora_rank, heads * (nope + v)]
|
||||
kv_b_proj_weight = fp8_weight.T.contiguous().view(
|
||||
self.kv_lora_rank,
|
||||
self.num_heads,
|
||||
self.qk_nope_head_dim + self.v_head_dim,
|
||||
)
|
||||
else:
|
||||
kv_b_proj_weight = get_and_maybe_dequant_weights(
|
||||
self.kv_b_proj, out_dtype=act_dtype
|
||||
).T
|
||||
kv_b_proj_weight = kv_b_proj_weight.view(
|
||||
self.kv_lora_rank,
|
||||
self.num_heads,
|
||||
self.qk_nope_head_dim + self.v_head_dim,
|
||||
)
|
||||
self._w_scale = None
|
||||
w_uk, w_uv = kv_b_proj_weight.split(
|
||||
[self.qk_nope_head_dim, self.v_head_dim], dim=-1
|
||||
)
|
||||
@@ -348,7 +417,9 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
ql_nope = torch.empty(
|
||||
num_tokens, num_heads, self.kv_lora_rank, dtype=q.dtype, device=q.device
|
||||
)
|
||||
ops.bmm_cpu(ql_nope.transpose(0, 1), q_nope_t, self._w_uk_packed, True, None)
|
||||
ops.bmm_cpu(
|
||||
ql_nope.transpose(0, 1), q_nope_t, self._w_uk_packed, True, self._w_scale
|
||||
)
|
||||
mqa_q = torch.cat([ql_nope, q_pe], dim=-1)
|
||||
|
||||
kv_cache_flat = kv_c_and_k_pe_cache.view(-1, 1, self.head_size)
|
||||
@@ -402,4 +473,4 @@ class AMXMLAImpl(MLACommonImpl[MLACommonMetadata]):
|
||||
output_view = output.view(num_tokens, num_heads, self.v_head_dim).transpose(
|
||||
0, 1
|
||||
)
|
||||
ops.bmm_cpu(output_view, attn_out_t, self._w_uv_packed, True, None)
|
||||
ops.bmm_cpu(output_view, attn_out_t, self._w_uv_packed, True, self._w_scale)
|
||||
|
||||
Reference in New Issue
Block a user