[Model] Add DeepSeek-V4 CPU backend (#55355)

Signed-off-by: jiang1.li <jiang1.li@intel.com>
This commit is contained in:
Li, Jiang
2026-09-10 14:14:48 +08:00
committed by GitHub
parent 49632fb3ac
commit c3ccc0e957
47 changed files with 9564 additions and 627 deletions
+19 -15
View File
@@ -1,7 +1,7 @@
group: CPU
depends_on: []
steps:
- label: CPU-Kernel Tests
- label: CPU-Kernel Tests Shard %N
key: cpu-kernel-tests
depends_on: []
device: intel_cpu
@@ -16,6 +16,7 @@ steps:
- tests/kernels/attention/test_amx_mla.py
- vllm/v1/attention/backends/mla/amx_mla.py
- vllm/model_executor/layers/attention/mla_attention.py
- tests/kernels/test_deepseek_v4_cpu_kernels.py
- tests/kernels/moe/test_cpu_fused_moe.py
- tests/kernels/moe/test_cpu_quant_fused_moe.py
- tests/kernels/moe/test_zen_cpu_int8_moe.py
@@ -33,20 +34,23 @@ steps:
commands:
- |
bash .buildkite/scripts/hardware_ci/run-cpu-test.sh 30m "
pytest -x -v -s tests/kernels/attention/test_cpu_attn.py
pytest -x -v -s tests/v1/attention/test_group_head_counts.py
pytest -x -v -s tests/kernels/attention/test_amx_mla.py
pytest -x -v -s tests/kernels/moe/test_cpu_fused_moe.py
pytest -x -v -s tests/kernels/moe/test_cpu_quant_fused_moe.py
pytest -x -v -s tests/kernels/moe/test_zen_cpu_int8_moe.py
pytest -x -v -s tests/kernels/mamba/test_cpu_short_conv.py
pytest -x -v -s tests/kernels/test_onednn.py
pytest -x -v -s tests/kernels/test_awq_int4_to_int8.py
pytest -x -v -s tests/kernels/quantization/test_cpu_fp8_scaled_mm.py
pytest -x -v -s tests/kernels/mamba/cpu/test_cpu_gdn_ops.py
pytest -x -v -s tests/kernels/mamba/test_causal_conv1d.py
pytest -x -v -s tests/kernels/mamba/test_mamba_ssm.py
pytest -x -v -s tests/v1/sample/test_topk_topp_sampler.py::TestTritonTopkTopp"
pytest -x -v -s --num-shards=$$BUILDKITE_PARALLEL_JOB_COUNT --shard-id=$$BUILDKITE_PARALLEL_JOB \
tests/kernels/attention/test_cpu_attn.py \
tests/v1/attention/test_group_head_counts.py \
tests/kernels/attention/test_amx_mla.py \
tests/kernels/test_deepseek_v4_cpu_kernels.py \
tests/kernels/moe/test_cpu_fused_moe.py \
tests/kernels/moe/test_cpu_quant_fused_moe.py \
tests/kernels/moe/test_zen_cpu_int8_moe.py \
tests/kernels/mamba/test_cpu_short_conv.py \
tests/kernels/test_onednn.py \
tests/kernels/test_awq_int4_to_int8.py \
tests/kernels/quantization/test_cpu_fp8_scaled_mm.py \
tests/kernels/mamba/cpu/test_cpu_gdn_ops.py \
tests/kernels/mamba/test_causal_conv1d.py \
tests/kernels/mamba/test_mamba_ssm.py \
tests/v1/sample/test_topk_topp_sampler.py::TestTritonTopkTopp"
parallelism: 2
# Note: SDE can't be downloaded from CI host because of AWS WAF
# - label: CPU-Compatibility Tests
@@ -70,12 +70,14 @@ def main(
packed_w13 = cpu_prepack_moe_weight(w13, isa)
packed_w2 = cpu_prepack_moe_weight(w2, isa)
output = torch.empty_like(input_tensor)
def run_benchmark(iters: int) -> list[float]:
times = []
for _ in range(iters):
start_time = time.perf_counter_ns()
_ = cpu_fused_moe(
cpu_fused_moe(
output,
input_tensor,
packed_w13,
packed_w2,
+8 -1
View File
@@ -548,7 +548,14 @@ if (ENABLE_X86_ISA)
"csrc/cpu/sgl-kernels/bmm.cpp"
"csrc/cpu/sgl-kernels/decode.cpp"
"csrc/cpu/sgl-kernels/extend.cpp"
"csrc/cpu/sgl-kernels/mla_cache.cpp")
"csrc/cpu/sgl-kernels/mla_cache.cpp"
"csrc/cpu/sgl-kernels/mhc.cpp"
"csrc/cpu/sgl-kernels/store_cache.cpp"
"csrc/cpu/sgl-kernels/flash_mla.cpp"
"csrc/cpu/sgl-kernels/compressor.cpp"
"csrc/cpu/sgl-kernels/paged_mqa_logits.cpp"
"csrc/cpu/sgl-kernels/topk.cpp"
"csrc/cpu/sgl-kernels/indexer.cpp")
set(VLLM_EXT_SRC_AVX512
"csrc/cpu/sgl-kernels/fla.cpp"
+30
View File
@@ -343,6 +343,36 @@ inline void parallel_2d(int m, int n, const func_t& f) {
#endif
}
// Like parallel_2d but with explicit nth_m x nth_n decomposition.
// Caller is responsible for choosing nth_m and nth_n.
//
// Cherry-picked from a newer sglang commit than this file's last full sync
// (needed by mhc.cpp's phase-1 tiling); not part of a full common.h resync.
template <typename func_t>
inline void parallel_2d_tiled(int m, int n, int nth_m, int nth_n, const func_t& f) {
const int nth = nth_m * nth_n;
#if defined(_OPENMP)
#pragma omp parallel num_threads(nth)
{
int ith = omp_get_thread_num();
int ith_m = ith / nth_n;
int ith_n = ith % nth_n;
int thread_block_m = div_up(m, nth_m);
int thread_block_n = div_up(n, nth_n);
int begin_m = std::min(ith_m * thread_block_m, m);
int end_m = std::min(begin_m + thread_block_m, m);
int begin_n = std::min(ith_n * thread_block_n, n);
int end_n = std::min(begin_n + thread_block_n, n);
f(begin_m, end_m, begin_n, end_n);
}
#else
f(0, m, 0, n);
#endif
}
// limit max cache blocks
// when we need to do pre-unpack for weights, e.g. fp8
#define MAX_CACHE_BLOCK_SIZE 4
+747
View File
@@ -0,0 +1,747 @@
// Adapted from
// https://github.com/sgl-project/sglang/blob/main/python/sglang/kernels/aot/csrc/cpu/compressor.cpp
//
// DeepSeek-V4 compressor: softmax-weighted pooling over a sliding window of
// raw KV/score state, RMSNorm, FP8/UE8M0 quant (NoPE) + GPT-J RoPE (RoPE),
// paged-insert into the fp8_ds_mla cache layout (see store_cache.cpp).
// Covers both the head_dim=512 attention-compressor path
// (`compress_norm_rope_store_cpu`) and the head_dim=128 indexer-compressor
// path (`compress_norm_rope_store_indexer_cpu`, distinct cache layout --
// see its own comment below).
//
// Differences from upstream:
// - Upstream splits decode (`compress_decode_cpu`) from prefill/extend
// (eager `compress_extend_separate`); this port stays unified across
// decode/prefill/extend, one program per token, matching vLLM's own
// GPU/Triton reference (`_fused_kv_compress_norm_rope_insert_sparse_attn`,
// common/ops/fused_compress_quant_cache.py).
// - Window-gather indices arrive pre-resolved to flat state-cache row ids
// from the Python caller (mirroring flash_mla.cpp), not looked up via
// block_table in-kernel.
// - The state cache is pooled with the main KV cache's physical pages
// (CompressorStateCache in compressor.py), so its per-block byte stride
// is page-aligned, not `block_size * row_width` -- row ids are addressed
// as (block_idx, pos_in_block) against the tensor's real block stride,
// never by flattening to a 2D view.
//
// clang-format off
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include "common.h"
#include "vec.h"
#if !defined(CPU_CAPABILITY_AVX512)
static_assert(false,
"compressor.cpp is only ever compiled into the AVX512+AMX-flagged "
"_C target (see cmake/cpu_extension.cmake's VLLM_EXT_SRC_SGL) and "
"relies on that unconditionally -- CPU_CAPABILITY_AVX512 must be "
"defined here.");
#endif
namespace {
using fVec = at::vec::Vectorized<float>;
constexpr int64_t kNopeDim = 448;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kHeadDim = kNopeDim + kRopeDim;
constexpr int64_t kQuantBlock = 64;
constexpr int64_t kNumQuantBlocks = kNopeDim / kQuantBlock;
constexpr int64_t kScaleBytesPerToken = kNumQuantBlocks + 1;
constexpr int64_t kTokenDataBytes = kNopeDim + kRopeDim * 2;
constexpr float kFp8Max = 448.0f;
constexpr float kFp8Min = -448.0f;
constexpr float kQuantEps = 1e-4f;
// In-place interleaved (GPT-J-style) RoPE: x[2i]/x[2i+1] are one (even, odd)
// pair, cos[i]/sin[i] its angle. Adapted from SGLang's `rope.cpp`
// (`apply_rotary_emb_row`), which avoids gather/scatter (slow on AVX512) by
// broadcasting cos/sin across each pair via in-register permutes instead:
// `_mm512_permute_ps(xv, 0xB1)` swaps the (even, odd) lanes within every
// 2-float pair in one shuffle, and cos/sin -- loaded as 8 contiguous values
// -- are duplicated pairwise into the same layout via `_mm512_permutexvar_ps`.
// SGLang's version reads cos/sin pre-interleaved with x (`[c0,s0,c1,s1,...]`)
// since its freqs tensor is laid out that way; ours are two separate flat
// arrays (`cos[i]`, `sin[i]`, matching store_cache.cpp's convention), hence
// the extra broadcast-duplicate permute here instead of a plain load.
// Both call sites in this file pass half_dim=32 (kRopeDim/2, kIndexerRopeDim/2
// are both 64/2), an exact multiple of 8, so this file's build (always AVX512,
// see file header) never needs a scalar remainder.
inline void apply_gptj_rope_inplace(float* __restrict__ x, const float* __restrict__ cos, const float* __restrict__ sin, int64_t half_dim) {
// Duplicates each of 8 source lanes into 2 adjacent output lanes:
// [c0,c1,...,c7] -> [c0,c0,c1,c1,...,c7,c7].
const __m512i dup_idx = _mm512_set_epi32(7, 7, 6, 6, 5, 5, 4, 4, 3, 3, 2, 2, 1, 1, 0, 0);
// Forward GPT-J rotation negates the sin broadcast on the even (real)
// lane of each pair; the odd (imag) lane keeps sin's sign.
const __m512 sign_mask = _mm512_castsi512_ps(_mm512_set_epi32(
0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000,
0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000));
for (int64_t i = 0; i + 8 <= half_dim; i += 8) {
__m512 xv = _mm512_loadu_ps(x + 2 * i);
__m512 xv_swapped = _mm512_permute_ps(xv, 0xB1);
__m512 cos_b = _mm512_permutexvar_ps(dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(cos + i)));
__m512 sin_b = _mm512_permutexvar_ps(dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(sin + i)));
__m512 sin_signed = _mm512_xor_ps(sin_b, sign_mask);
__m512 out = _mm512_fmadd_ps(xv, cos_b, _mm512_mul_ps(xv_swapped, sin_signed));
_mm512_storeu_ps(x + 2 * i, out);
}
}
// AVX512 tile-quantization, adapted verbatim from SGLang's `store_cache.cpp`
// (`quant_to_nope_fp8_rope_bf16_pack_cpu`'s
// `cvt_fp32x16_to_fp8x16`/`quantize_tile_avx512`) -- SGLang itself keeps
// `compress_decode_cpu` fp32-output-only and calls this quant step as a
// separate op; vLLM's compressor kernel stays fused (per this file's header
// comment), so the same AVX512 tile-quant math is inlined here instead.
#if defined(CPU_CAPABILITY_AVX512)
// Vectorized float32x16 -> fp8_e4m3fn x16 using AVX512 bit manipulation.
// Implements RNE (round-to-nearest-even). Flushes subnormal fp8 to zero.
// fp8_e4m3fn: sign(1), exp(4, bias=7), mant(3), no inf/nan special values.
// Normal: exp_field in [1..15], value = 2^(exp-7) * (1 + mant/8)
// Input assumed clamped to [-448, 448].
inline __m128i cvt_fp32x16_to_fp8x16(__m512 input) {
__m512i bits = _mm512_castps_si512(input);
__m512i signs = _mm512_srli_epi32(_mm512_and_si512(bits, _mm512_set1_epi32(0x80000000)), 24);
__m512i abs_bits = _mm512_and_si512(bits, _mm512_set1_epi32(0x7FFFFFFF));
__m512i f32_exp = _mm512_srli_epi32(abs_bits, 23);
__m512i f32_mant = _mm512_and_si512(abs_bits, _mm512_set1_epi32(0x7FFFFF));
__m512i fp8_exp = _mm512_sub_epi32(f32_exp, _mm512_set1_epi32(120));
__m512i mant3 = _mm512_srli_epi32(f32_mant, 20);
__m512i round_bit = _mm512_and_si512(_mm512_srli_epi32(f32_mant, 19), _mm512_set1_epi32(1));
__m512i sticky_bits = _mm512_and_si512(f32_mant, _mm512_set1_epi32(0x7FFFF));
__mmask16 has_sticky = _mm512_cmpneq_epi32_mask(sticky_bits, _mm512_setzero_si512());
__m512i lsb = _mm512_and_si512(mant3, _mm512_set1_epi32(1));
__m512i sticky_or_lsb = _mm512_or_si512(_mm512_maskz_mov_epi32(has_sticky, _mm512_set1_epi32(1)), lsb);
__m512i do_round = _mm512_and_si512(round_bit, sticky_or_lsb);
mant3 = _mm512_add_epi32(mant3, do_round);
__mmask16 mant_ovf = _mm512_cmpeq_epi32_mask(mant3, _mm512_set1_epi32(8));
mant3 = _mm512_mask_mov_epi32(mant3, mant_ovf, _mm512_setzero_si512());
fp8_exp = _mm512_mask_add_epi32(fp8_exp, mant_ovf, fp8_exp, _mm512_set1_epi32(1));
fp8_exp = _mm512_min_epi32(fp8_exp, _mm512_set1_epi32(15));
__m512i result = _mm512_or_si512(_mm512_slli_epi32(fp8_exp, 3), mant3);
__mmask16 is_subnormal_or_zero = _mm512_cmplt_epi32_mask(f32_exp, _mm512_set1_epi32(121));
result = _mm512_mask_mov_epi32(result, is_subnormal_or_zero, _mm512_setzero_si512());
result = _mm512_or_si512(result, signs);
return _mm512_cvtepi32_epi8(result);
}
// Process one 64-wide tile: find amax, compute a shared power-of-two scale,
// quantize to fp8. Returns the UE8M0 scale byte for the tile.
inline uint8_t quantize_tile_avx512(const at::BFloat16* __restrict__ src, uint8_t* __restrict__ dst) {
__m256i bf16_0 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src));
__m256i bf16_1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src + 16));
__m256i bf16_2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src + 32));
__m256i bf16_3 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src + 48));
__m512 f0 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_0), 16));
__m512 f1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_1), 16));
__m512 f2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_2), 16));
__m512 f3 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_3), 16));
const __m512i abs_mask_i = _mm512_set1_epi32(0x7FFFFFFF);
__m512 abs0 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f0), abs_mask_i));
__m512 abs1 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f1), abs_mask_i));
__m512 abs2 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f2), abs_mask_i));
__m512 abs3 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f3), abs_mask_i));
__m512 max01 = _mm512_max_ps(abs0, abs1);
__m512 max23 = _mm512_max_ps(abs2, abs3);
__m512 max0123 = _mm512_max_ps(max01, max23);
float amax = _mm512_reduce_max_ps(max0123);
float scale = std::max(amax / kFp8Max, kQuantEps);
float ceil_log2 = std::ceil(std::log2(scale));
float scale_pow2 = std::exp2(ceil_log2);
float scale_inv = 1.0f / scale_pow2;
int exponent = static_cast<int>(ceil_log2);
uint8_t scale_uint8 = static_cast<uint8_t>(exponent + 127);
__m512 vinv = _mm512_set1_ps(scale_inv);
__m512 s0 = _mm512_mul_ps(f0, vinv);
__m512 s1 = _mm512_mul_ps(f1, vinv);
__m512 s2 = _mm512_mul_ps(f2, vinv);
__m512 s3 = _mm512_mul_ps(f3, vinv);
__m512 vmax = _mm512_set1_ps(kFp8Max);
__m512 vmin = _mm512_set1_ps(kFp8Min);
s0 = _mm512_max_ps(_mm512_min_ps(s0, vmax), vmin);
s1 = _mm512_max_ps(_mm512_min_ps(s1, vmax), vmin);
s2 = _mm512_max_ps(_mm512_min_ps(s2, vmax), vmin);
s3 = _mm512_max_ps(_mm512_min_ps(s3, vmax), vmin);
__m128i fp8_0 = cvt_fp32x16_to_fp8x16(s0);
__m128i fp8_1 = cvt_fp32x16_to_fp8x16(s1);
__m128i fp8_2 = cvt_fp32x16_to_fp8x16(s2);
__m128i fp8_3 = cvt_fp32x16_to_fp8x16(s3);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst), fp8_0);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 16), fp8_1);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 32), fp8_2);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 48), fp8_3);
// Patch subnormal-magnitude lanes: `cvt_fp32x16_to_fp8x16` flushes
// anything below the smallest fp8_e4m3fn normal (2^-6) to zero, but
// vLLM's dequant path relies on `at::Float8_e4m3fn`'s real subnormal
// encoding -- redo just those rare lanes with the exact scalar
// conversion to stay consistent with the read side.
alignas(64) float scaled[kQuantBlock];
_mm512_store_ps(scaled, s0);
_mm512_store_ps(scaled + 16, s1);
_mm512_store_ps(scaled + 32, s2);
_mm512_store_ps(scaled + 48, s3);
for (int64_t j = 0; j < kQuantBlock; ++j) {
const float av = std::abs(scaled[j]);
if (av != 0.f && av < 0x1p-6f) {
dst[j] = at::Float8_e4m3fn(scaled[j]).x;
}
}
return scale_uint8;
}
#endif // CPU_CAPABILITY_AVX512
void save_partial_states_impl(
float* __restrict__ state_cache, // [num_blocks, block_stride]
int64_t block_stride, // element stride between blocks
int64_t block_size, // rows (slots) per block
const float* __restrict__ kv, // [num_tokens, state_width]
int64_t kv_row_stride, // element stride between kv rows
const float* __restrict__ score, // [num_tokens, state_width]
int64_t score_row_stride, // element stride between score rows
const float* __restrict__ ape, // [compress_ratio, state_width]
const int64_t* __restrict__ positions, // [num_tokens]
const int64_t* __restrict__ slot_mapping, // [num_tokens]
int64_t state_width,
int64_t compress_ratio,
int64_t num_tokens) {
const int64_t row_width = 2 * state_width;
at::parallel_for(0, num_tokens, 0, [&](int64_t begin, int64_t end) {
for (int64_t t = begin; t < end; ++t) {
const int64_t slot_id = slot_mapping[t];
if (slot_id < 0) continue;
const int64_t block_idx = slot_id / block_size;
const int64_t pos_in_block = slot_id % block_size;
float* const row = state_cache + block_idx * block_stride + pos_in_block * row_width;
const float* const kv_t = kv + t * kv_row_stride;
// state_width is always a multiple of 16 for this model (512/1024 for
// the main compressor, 128/256 for the indexer compressor).
for (int64_t i = 0; i + 16 <= state_width; i += 16) {
fVec::loadu(kv_t + i).store(row + i);
}
const int64_t position = positions[t];
const float* const ape_row = ape + (position % compress_ratio) * state_width;
const float* const score_t = score + t * score_row_stride;
float* const score_out = row + state_width;
for (int64_t i = 0; i + 16 <= state_width; i += 16) {
(fVec::loadu(score_t + i) + fVec::loadu(ape_row + i)).store(score_out + i);
}
}
});
}
void compress_norm_rope_store_impl(
const float* __restrict__ state_cache, // [num_blocks, state_block_stride]
int64_t state_block_stride, // element stride between state blocks
int64_t state_block_size, // rows (slots) per state block
const int64_t* __restrict__ gather_slots, // [num_tokens, window], -1 = invalid
const int64_t* __restrict__ positions, // [num_tokens]
const int64_t* __restrict__ kv_slot_mapping, // [num_tokens], -1 = skip
const float* __restrict__ rms_norm_weight, // [kHeadDim]
float rms_norm_eps,
const float* __restrict__ cos_sin_cache, // [max_pos, kRopeDim]
uint8_t* __restrict__ kv_cache, // [num_blocks, cache_row_stride]
int64_t cache_row_stride,
int64_t kv_cache_block_size,
int64_t compress_ratio,
int64_t window,
int64_t state_width,
int64_t num_tokens) {
const int64_t rope_half = kRopeDim / 2;
const int64_t state_row_width = 2 * state_width;
at::parallel_for(0, num_tokens, 0, [&](int64_t begin, int64_t end) {
std::array<float, kHeadDim> max_v;
std::array<float, kHeadDim> denom;
std::array<float, kHeadDim> acc;
std::array<float, kHeadDim> normed;
std::array<at::BFloat16, kNopeDim> nope_bf16;
for (int64_t t = begin; t < end; ++t) {
const int64_t position = positions[t];
if ((position + 1) % compress_ratio != 0) continue;
const int64_t kv_slot = kv_slot_mapping[t];
if (kv_slot < 0) continue;
std::fill(max_v.begin(), max_v.end(), -std::numeric_limits<float>::infinity());
std::fill(denom.begin(), denom.end(), 0.f);
std::fill(acc.begin(), acc.end(), 0.f);
// Per-channel (not per-position) softmax over the window: each of the
// kHeadDim columns is normalized independently across the window axis.
for (int64_t w = 0; w < window; ++w) {
const int64_t slot = gather_slots[t * window + w];
if (slot < 0) continue;
const int64_t block_idx = slot / state_block_size;
const int64_t pos_in_block = slot % state_block_size;
const int64_t head_offset = (w >= compress_ratio) ? kHeadDim : 0;
const float* const score_row = state_cache + block_idx * state_block_stride +
pos_in_block * state_row_width + head_offset + state_width;
for (int64_t d = 0; d < kHeadDim; d += 16) {
__m512 mx = _mm512_loadu_ps(max_v.data() + d);
__m512 sv = _mm512_loadu_ps(score_row + d);
_mm512_storeu_ps(max_v.data() + d, _mm512_max_ps(mx, sv));
}
}
for (int64_t w = 0; w < window; ++w) {
const int64_t slot = gather_slots[t * window + w];
if (slot < 0) continue;
const int64_t block_idx = slot / state_block_size;
const int64_t pos_in_block = slot % state_block_size;
const int64_t head_offset = (w >= compress_ratio) ? kHeadDim : 0;
const float* const row = state_cache + block_idx * state_block_stride +
pos_in_block * state_row_width + head_offset;
const float* const score_row = row + state_width;
for (int64_t d = 0; d < kHeadDim; d += 16) {
fVec e_v = (fVec::loadu(score_row + d) - fVec::loadu(max_v.data() + d)).exp();
(fVec::loadu(denom.data() + d) + e_v).store(denom.data() + d);
(fVec::loadu(acc.data() + d) + e_v * fVec::loadu(row + d)).store(acc.data() + d);
}
}
float sq_sum = 0.f;
{
fVec sq_sum_v(0.f);
for (int64_t d = 0; d < kHeadDim; d += 16) {
fVec v = fVec::loadu(acc.data() + d) / fVec::loadu(denom.data() + d);
v.store(normed.data() + d);
sq_sum_v = sq_sum_v + v * v;
}
sq_sum = vec_reduce_sum(sq_sum_v);
}
const float inv_rms = 1.0f / std::sqrt(sq_sum / static_cast<float>(kHeadDim) + rms_norm_eps);
{
fVec inv_rms_v(inv_rms);
for (int64_t d = 0; d < kHeadDim; d += 16) {
(fVec::loadu(normed.data() + d) * inv_rms_v * fVec::loadu(rms_norm_weight + d)).store(normed.data() + d);
}
}
// bf16 round-trip feeds only the NoPE FP8 quantization (RoPE rotates
// the un-rounded `normed` below, then casts once to bf16 at store
// time for the separate bf16 rope region).
for (int64_t d = 0; d < kNopeDim; d += 16)
store_from_float_ext<at::BFloat16>(nope_bf16.data() + d, fVec::loadu(normed.data() + d));
const int64_t compressed_pos = (position / compress_ratio) * compress_ratio;
const float* const cos = cos_sin_cache + compressed_pos * kRopeDim;
const float* const sin = cos + rope_half;
apply_gptj_rope_inplace(normed.data() + kNopeDim, cos, sin, rope_half);
const int64_t block_idx = kv_slot / kv_cache_block_size;
const int64_t pos_in_block = kv_slot % kv_cache_block_size;
uint8_t* const block_base = kv_cache + block_idx * cache_row_stride;
uint8_t* const token_fp8_ptr = block_base + pos_in_block * kTokenDataBytes;
uint8_t* const token_bf16_ptr = token_fp8_ptr + kNopeDim;
uint8_t* const token_scale_ptr =
block_base + kv_cache_block_size * kTokenDataBytes + pos_in_block * kScaleBytesPerToken;
// AVX512 tile-quantization (SGLang's `quantize_tile_avx512`), one
// 64-wide tile per iteration.
for (int64_t qb = 0; qb < kNumQuantBlocks; ++qb) {
const at::BFloat16* tile_src = nope_bf16.data() + qb * kQuantBlock;
uint8_t* tile_dst = token_fp8_ptr + qb * kQuantBlock;
token_scale_ptr[qb] = quantize_tile_avx512(tile_src, tile_dst);
}
token_scale_ptr[kNumQuantBlocks] = 0;
auto* const out_rope = reinterpret_cast<at::BFloat16*>(token_bf16_ptr);
for (int64_t ri = 0; ri < kRopeDim; ri += 16)
store_from_float_ext<at::BFloat16>(out_rope + ri, fVec::loadu(normed.data() + kNopeDim + ri));
}
});
}
// Indexer compressor (head_dim=128): same softmax-pool + RMSNorm + GPT-J
// RoPE prologue as the head=512 path above, but a different cache layout:
// all 128 dims are FP8 in one combined quant block (no separate bf16 rope
// region, single raw fp32 scale per token) instead of 7 UE8M0 blocks over
// nope alone -- matching the paged indexer K-cache layout
// fp8_paged_mqa_logits_cpu/topk_transform_512_cpu (paged_mqa_logits.cpp/
// topk.cpp) read, and the GPU/Triton reference
// (`_fused_kv_compress_norm_rope_insert_indexer_attn`,
// common/ops/fused_compress_quant_cache.py) exactly.
constexpr int64_t kIndexerHeadDim = 128;
constexpr int64_t kIndexerRopeDim = 64;
constexpr int64_t kIndexerNopeDim = kIndexerHeadDim - kIndexerRopeDim;
constexpr int64_t kIndexerTokenBytes = kIndexerHeadDim;
constexpr int64_t kIndexerScaleBytes = 4;
void compress_norm_rope_store_indexer_impl(
const float* __restrict__ state_cache, // [num_blocks, state_block_stride]
int64_t state_block_stride,
int64_t state_block_size,
const int64_t* __restrict__ gather_slots, // [num_tokens, window], -1 = invalid
const int64_t* __restrict__ positions, // [num_tokens]
const int64_t* __restrict__ kv_slot_mapping, // [num_tokens], -1 = skip
const float* __restrict__ rms_norm_weight, // [kIndexerHeadDim]
float rms_norm_eps,
const float* __restrict__ cos_sin_cache, // [max_pos, kIndexerRopeDim]
uint8_t* __restrict__ kv_cache, // [num_blocks, cache_row_stride]
int64_t cache_row_stride,
int64_t kv_cache_block_size,
int64_t compress_ratio,
int64_t window,
int64_t state_width,
int64_t num_tokens) {
const int64_t rope_half = kIndexerRopeDim / 2;
const int64_t state_row_width = 2 * state_width;
at::parallel_for(0, num_tokens, 0, [&](int64_t begin, int64_t end) {
std::array<float, kIndexerHeadDim> max_v;
std::array<float, kIndexerHeadDim> denom;
std::array<float, kIndexerHeadDim> acc;
std::array<float, kIndexerHeadDim> normed;
std::array<at::BFloat16, kIndexerHeadDim> quant_bf16;
for (int64_t t = begin; t < end; ++t) {
const int64_t position = positions[t];
if ((position + 1) % compress_ratio != 0) continue;
const int64_t kv_slot = kv_slot_mapping[t];
if (kv_slot < 0) continue;
std::fill(max_v.begin(), max_v.end(), -std::numeric_limits<float>::infinity());
std::fill(denom.begin(), denom.end(), 0.f);
std::fill(acc.begin(), acc.end(), 0.f);
for (int64_t w = 0; w < window; ++w) {
const int64_t slot = gather_slots[t * window + w];
if (slot < 0) continue;
const int64_t block_idx = slot / state_block_size;
const int64_t pos_in_block = slot % state_block_size;
const int64_t head_offset = (w >= compress_ratio) ? kIndexerHeadDim : 0;
const float* const score_row = state_cache + block_idx * state_block_stride +
pos_in_block * state_row_width + head_offset + state_width;
for (int64_t d = 0; d < kIndexerHeadDim; d += 16) {
__m512 mx = _mm512_loadu_ps(max_v.data() + d);
__m512 sv = _mm512_loadu_ps(score_row + d);
_mm512_storeu_ps(max_v.data() + d, _mm512_max_ps(mx, sv));
}
}
for (int64_t w = 0; w < window; ++w) {
const int64_t slot = gather_slots[t * window + w];
if (slot < 0) continue;
const int64_t block_idx = slot / state_block_size;
const int64_t pos_in_block = slot % state_block_size;
const int64_t head_offset = (w >= compress_ratio) ? kIndexerHeadDim : 0;
const float* const row = state_cache + block_idx * state_block_stride +
pos_in_block * state_row_width + head_offset;
const float* const score_row = row + state_width;
for (int64_t d = 0; d < kIndexerHeadDim; d += 16) {
fVec e_v = (fVec::loadu(score_row + d) - fVec::loadu(max_v.data() + d)).exp();
(fVec::loadu(denom.data() + d) + e_v).store(denom.data() + d);
(fVec::loadu(acc.data() + d) + e_v * fVec::loadu(row + d)).store(acc.data() + d);
}
}
float sq_sum = 0.f;
{
fVec sq_sum_v(0.f);
for (int64_t d = 0; d < kIndexerHeadDim; d += 16) {
fVec v = fVec::loadu(acc.data() + d) / fVec::loadu(denom.data() + d);
v.store(normed.data() + d);
sq_sum_v = sq_sum_v + v * v;
}
sq_sum = vec_reduce_sum(sq_sum_v);
}
const float inv_rms = 1.0f / std::sqrt(sq_sum / static_cast<float>(kIndexerHeadDim) + rms_norm_eps);
{
fVec inv_rms_v(inv_rms);
for (int64_t d = 0; d < kIndexerHeadDim; d += 16) {
(fVec::loadu(normed.data() + d) * inv_rms_v * fVec::loadu(rms_norm_weight + d)).store(normed.data() + d);
}
}
const int64_t compressed_pos = (position / compress_ratio) * compress_ratio;
const float* const cos = cos_sin_cache + compressed_pos * kIndexerRopeDim;
const float* const sin = cos + rope_half;
apply_gptj_rope_inplace(normed.data() + kIndexerNopeDim, cos, sin, rope_half);
// Indexer path rounds the FULL post-RoPE vector through bf16 before
// quantizing (single combined block); the head=512 path above instead
// rounds only the NoPE region before RoPE (separate bf16 rope store).
for (int64_t d = 0; d < kIndexerHeadDim; d += 16)
store_from_float_ext<at::BFloat16>(quant_bf16.data() + d, fVec::loadu(normed.data() + d));
const int64_t block_idx = kv_slot / kv_cache_block_size;
const int64_t pos_in_block = kv_slot % kv_cache_block_size;
uint8_t* const block_base = kv_cache + block_idx * cache_row_stride;
uint8_t* const token_fp8_ptr = block_base + pos_in_block * kIndexerTokenBytes;
uint8_t* const token_scale_ptr =
block_base + kv_cache_block_size * kIndexerTokenBytes + pos_in_block * kIndexerScaleBytes;
float absmax = 1e-4f;
{
__m512 max_v_reg = _mm512_set1_ps(absmax);
for (int64_t di = 0; di < kIndexerHeadDim; di += 16) {
__m256i bf16_v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(quant_bf16.data() + di));
__m512 f = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_v), 16));
__m512 av = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f), _mm512_set1_epi32(0x7FFFFFFF)));
max_v_reg = _mm512_max_ps(max_v_reg, av);
}
absmax = _mm512_reduce_max_ps(max_v_reg);
}
const float exponent = std::ceil(std::log2(absmax / kFp8Max));
const float scale = std::exp2(exponent);
const float inv_scale = 1.0f / scale;
// AVX512 quantize+store, reusing SGLang's `cvt_fp32x16_to_fp8x16` bit-
// manipulation conversion (this single-shared-scale 128-wide block has
// no direct SGLang equivalent -- see file header -- so only the fp8
// conversion primitive is ported, the amax/scale formula is unchanged
// from vLLM's own reference).
uint8_t* const out_fp8 = token_fp8_ptr;
{
const __m512 vinv = _mm512_set1_ps(inv_scale);
const __m512 vmax = _mm512_set1_ps(kFp8Max);
const __m512 vmin = _mm512_set1_ps(kFp8Min);
alignas(64) float scaled_buf[kIndexerHeadDim];
for (int64_t d = 0; d < kIndexerHeadDim; d += 16) {
__m256i bf16_v = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(quant_bf16.data() + d));
__m512 f = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_v), 16));
__m512 s = _mm512_mul_ps(f, vinv);
s = _mm512_max_ps(_mm512_min_ps(s, vmax), vmin);
_mm512_store_ps(scaled_buf + d, s);
__m128i fp8_v = cvt_fp32x16_to_fp8x16(s);
_mm_storeu_si128(reinterpret_cast<__m128i*>(out_fp8 + d), fp8_v);
}
// Patch subnormal-magnitude lanes (see `quantize_tile_avx512`'s
// comment above for why the vectorized conversion's flush-to-zero
// must not be trusted here).
for (int64_t d = 0; d < kIndexerHeadDim; ++d) {
const float av = std::abs(scaled_buf[d]);
if (av != 0.f && av < 0x1p-6f) {
out_fp8[d] = at::Float8_e4m3fn(scaled_buf[d]).x;
}
}
}
// Raw fp32 scale (not UE8M0-encoded) -- single quant block, matches
// fp8_paged_mqa_logits_cpu's (paged_mqa_logits.cpp) read-side expectations.
*reinterpret_cast<float*>(token_scale_ptr) = scale;
}
});
}
} // anonymous namespace
void save_partial_states_cpu(
at::Tensor& kv,
at::Tensor& score,
at::Tensor& ape,
at::Tensor& positions,
at::Tensor& state_cache,
at::Tensor& slot_mapping) {
TORCH_CHECK(
kv.dim() == 2 && kv.scalar_type() == at::kFloat, "save_partial_states_cpu: kv must be a 2D fp32 tensor");
TORCH_CHECK(score.sizes() == kv.sizes() && score.scalar_type() == at::kFloat,
"save_partial_states_cpu: score must match kv's shape/dtype");
TORCH_CHECK(ape.dim() == 2 && ape.size(1) == kv.size(1) && ape.scalar_type() == at::kFloat,
"save_partial_states_cpu: ape must be [compress_ratio, state_width] fp32");
TORCH_CHECK(positions.scalar_type() == at::kLong && positions.numel() == kv.size(0),
"save_partial_states_cpu: positions must be int64 with num_tokens entries");
TORCH_CHECK(slot_mapping.scalar_type() == at::kLong && slot_mapping.numel() == kv.size(0),
"save_partial_states_cpu: slot_mapping must be int64 with num_tokens entries");
TORCH_CHECK(state_cache.dim() == 3 && state_cache.size(2) == 2 * kv.size(1) &&
state_cache.scalar_type() == at::kFloat,
"save_partial_states_cpu: state_cache must be [num_blocks, block_size, "
"2*state_width] fp32");
TORCH_CHECK(state_cache.stride(2) == 1 && state_cache.stride(1) == state_cache.size(2),
"save_partial_states_cpu: state_cache rows must be contiguous within a block");
TORCH_CHECK(
kv.device().is_cpu() && score.device().is_cpu() && ape.device().is_cpu() && positions.device().is_cpu() &&
state_cache.device().is_cpu() && slot_mapping.device().is_cpu(),
"save_partial_states_cpu: all inputs must be CPU tensors");
const int64_t num_tokens = kv.size(0);
const int64_t state_width = kv.size(1);
const int64_t compress_ratio = ape.size(0);
const int64_t block_size = state_cache.size(1);
TORCH_CHECK(
kv.stride(1) == 1 && score.stride(1) == 1 && ape.is_contiguous() && positions.is_contiguous() &&
slot_mapping.is_contiguous(),
"save_partial_states_cpu: kv/score must be contiguous in their last dim "
"(row stride may exceed state_width -- e.g. a column-split view of a "
"fused kv_score projection); ape/positions/slot_mapping must be fully "
"contiguous");
save_partial_states_impl(
state_cache.data_ptr<float>(),
state_cache.stride(0),
block_size,
kv.data_ptr<float>(),
kv.stride(0),
score.data_ptr<float>(),
score.stride(0),
ape.data_ptr<float>(),
positions.data_ptr<int64_t>(),
slot_mapping.data_ptr<int64_t>(),
state_width,
compress_ratio,
num_tokens);
}
void compress_norm_rope_store_cpu(
at::Tensor& state_cache,
at::Tensor& gather_slots,
at::Tensor& positions,
at::Tensor& kv_slot_mapping,
at::Tensor& rms_norm_weight,
double rms_norm_eps,
at::Tensor& cos_sin_cache,
at::Tensor& kv_cache_2d,
int64_t kv_cache_block_size,
int64_t compress_ratio) {
TORCH_CHECK(state_cache.dim() == 3 && state_cache.scalar_type() == at::kFloat,
"compress_norm_rope_store_cpu: state_cache must be a 3D fp32 tensor");
TORCH_CHECK(state_cache.stride(2) == 1 && state_cache.stride(1) == state_cache.size(2),
"compress_norm_rope_store_cpu: state_cache rows must be contiguous within a block");
TORCH_CHECK(gather_slots.dim() == 2 && gather_slots.scalar_type() == at::kLong,
"compress_norm_rope_store_cpu: gather_slots must be a 2D int64 tensor");
const int64_t num_tokens = gather_slots.size(0);
TORCH_CHECK(positions.scalar_type() == at::kLong && positions.numel() == num_tokens,
"compress_norm_rope_store_cpu: positions must be int64 with num_tokens entries");
TORCH_CHECK(kv_slot_mapping.scalar_type() == at::kLong && kv_slot_mapping.numel() == num_tokens,
"compress_norm_rope_store_cpu: kv_slot_mapping must be int64 with num_tokens entries");
TORCH_CHECK(rms_norm_weight.numel() == kHeadDim,
"compress_norm_rope_store_cpu: rms_norm_weight must have head_dim (512) entries");
TORCH_CHECK(kv_cache_2d.dim() == 2 && kv_cache_2d.scalar_type() == at::kByte,
"compress_norm_rope_store_cpu: kv_cache_2d must be a 2D uint8 tensor");
TORCH_CHECK(state_cache.size(2) % 2 == 0, "compress_norm_rope_store_cpu: state_cache width must be even");
TORCH_CHECK(
rms_norm_weight.scalar_type() == at::kFloat && rms_norm_weight.is_contiguous() &&
cos_sin_cache.scalar_type() == at::kFloat && cos_sin_cache.is_contiguous(),
"compress_norm_rope_store_cpu: rms_norm_weight/cos_sin_cache must be "
"float32 and contiguous (convert once in Python)");
TORCH_CHECK(
gather_slots.is_contiguous() && positions.is_contiguous() && kv_slot_mapping.is_contiguous(),
"compress_norm_rope_store_cpu: gather_slots/positions/kv_slot_mapping must be contiguous");
TORCH_CHECK(
state_cache.device().is_cpu() && gather_slots.device().is_cpu() && positions.device().is_cpu() &&
kv_slot_mapping.device().is_cpu() && rms_norm_weight.device().is_cpu() &&
cos_sin_cache.device().is_cpu() && kv_cache_2d.device().is_cpu(),
"compress_norm_rope_store_cpu: all inputs must be CPU tensors");
const int64_t window = gather_slots.size(1);
const int64_t state_width = state_cache.size(2) / 2;
const int64_t state_block_size = state_cache.size(1);
// Row strides are threaded through explicitly rather than assumed, since
// both buffers may be pages of a larger pooled allocation (matches
// store_cache.cpp/flash_mla.cpp's precedent).
const int64_t state_block_stride = state_cache.stride(0);
const int64_t cache_row_stride = kv_cache_2d.stride(0);
compress_norm_rope_store_impl(
state_cache.data_ptr<float>(),
state_block_stride,
state_block_size,
gather_slots.data_ptr<int64_t>(),
positions.data_ptr<int64_t>(),
kv_slot_mapping.data_ptr<int64_t>(),
rms_norm_weight.data_ptr<float>(),
static_cast<float>(rms_norm_eps),
cos_sin_cache.data_ptr<float>(),
kv_cache_2d.data_ptr<uint8_t>(),
cache_row_stride,
kv_cache_block_size,
compress_ratio,
window,
state_width,
num_tokens);
}
void compress_norm_rope_store_indexer_cpu(
at::Tensor& state_cache,
at::Tensor& gather_slots,
at::Tensor& positions,
at::Tensor& kv_slot_mapping,
at::Tensor& rms_norm_weight,
double rms_norm_eps,
at::Tensor& cos_sin_cache,
at::Tensor& kv_cache_2d,
int64_t kv_cache_block_size,
int64_t compress_ratio) {
TORCH_CHECK(state_cache.dim() == 3 && state_cache.scalar_type() == at::kFloat,
"compress_norm_rope_store_indexer_cpu: state_cache must be a 3D fp32 tensor");
TORCH_CHECK(state_cache.stride(2) == 1 && state_cache.stride(1) == state_cache.size(2),
"compress_norm_rope_store_indexer_cpu: state_cache rows must be contiguous within a block");
TORCH_CHECK(gather_slots.dim() == 2 && gather_slots.scalar_type() == at::kLong,
"compress_norm_rope_store_indexer_cpu: gather_slots must be a 2D int64 tensor");
const int64_t num_tokens = gather_slots.size(0);
TORCH_CHECK(positions.scalar_type() == at::kLong && positions.numel() == num_tokens,
"compress_norm_rope_store_indexer_cpu: positions must be int64 with num_tokens entries");
TORCH_CHECK(kv_slot_mapping.scalar_type() == at::kLong && kv_slot_mapping.numel() == num_tokens,
"compress_norm_rope_store_indexer_cpu: kv_slot_mapping must be int64 with num_tokens entries");
TORCH_CHECK(rms_norm_weight.numel() == kIndexerHeadDim,
"compress_norm_rope_store_indexer_cpu: rms_norm_weight must have head_dim (128) entries");
TORCH_CHECK(kv_cache_2d.dim() == 2 && kv_cache_2d.scalar_type() == at::kByte,
"compress_norm_rope_store_indexer_cpu: kv_cache_2d must be a 2D uint8 tensor");
TORCH_CHECK(state_cache.size(2) % 2 == 0,
"compress_norm_rope_store_indexer_cpu: state_cache width must be even");
TORCH_CHECK(
rms_norm_weight.scalar_type() == at::kFloat && rms_norm_weight.is_contiguous() &&
cos_sin_cache.scalar_type() == at::kFloat && cos_sin_cache.is_contiguous(),
"compress_norm_rope_store_indexer_cpu: rms_norm_weight/cos_sin_cache must "
"be float32 and contiguous (convert once in Python)");
TORCH_CHECK(
gather_slots.is_contiguous() && positions.is_contiguous() && kv_slot_mapping.is_contiguous(),
"compress_norm_rope_store_indexer_cpu: gather_slots/positions/kv_slot_mapping must be contiguous");
TORCH_CHECK(
state_cache.device().is_cpu() && gather_slots.device().is_cpu() && positions.device().is_cpu() &&
kv_slot_mapping.device().is_cpu() && rms_norm_weight.device().is_cpu() &&
cos_sin_cache.device().is_cpu() && kv_cache_2d.device().is_cpu(),
"compress_norm_rope_store_indexer_cpu: all inputs must be CPU tensors");
const int64_t window = gather_slots.size(1);
const int64_t state_width = state_cache.size(2) / 2;
const int64_t state_block_size = state_cache.size(1);
const int64_t state_block_stride = state_cache.stride(0);
const int64_t cache_row_stride = kv_cache_2d.stride(0);
compress_norm_rope_store_indexer_impl(
state_cache.data_ptr<float>(),
state_block_stride,
state_block_size,
gather_slots.data_ptr<int64_t>(),
positions.data_ptr<int64_t>(),
kv_slot_mapping.data_ptr<int64_t>(),
rms_norm_weight.data_ptr<float>(),
static_cast<float>(rms_norm_eps),
cos_sin_cache.data_ptr<float>(),
kv_cache_2d.data_ptr<uint8_t>(),
cache_row_stride,
kv_cache_block_size,
compress_ratio,
window,
state_width,
num_tokens);
}
+625
View File
@@ -0,0 +1,625 @@
// Adapted from
// https://github.com/sgl-project/sglang/blob/main/python/sglang/kernels/aot/csrc/cpu/flash_mla.cpp
//
// DeepSeek-V4 sparse MQA attention kernel for the fp8_ds_mla cache layout.
// Near-verbatim port of `sparse_mla_decode_kernel_impl` (VNNI K/V packing +
// AMX brgemm QK^T/PV + online softmax); the `V4_FP8Sparse` byte layout
// matches vLLM's fp8_ds_mla layout exactly (see store_cache.cpp).
//
// Differences from upstream:
// - Outer shape: upstream takes a padded batch-major query `[B, S_q, H, D]`
// with per-batch topk sets; vLLM passes flat `[num_tokens, H, D]` with
// per-token resolved indices. The kernel's own `at::parallel_for` already
// flattens `(batch, s_q)` internally, so this is called with
// `batches = num_tokens, s_q = 1` -- no padding, no kernel-body changes.
// - Two physically distinct paged caches (SWA "window" + compressor
// "compressed") are addressed via block_table-resolved flat slot ids,
// matching upstream's `k_main`/`k_extra` two-index-set convention.
// - `attn_sink` uses upstream's own post-hoc LSE correction
// (`out *= 1 / (1 + exp(sink - lse))`).
// - bf16 query only (matching upstream's own restriction: its FP8->bf16
// dequant intrinsics are bf16-specific); fp16 is not supported here.
//
// clang-format off
#include <algorithm>
#include <cmath>
#include <limits>
#include <vector>
#include "common.h"
#include "gemm.h"
#include "vec.h"
#if !defined(CPU_CAPABILITY_AVX512)
static_assert(false,
"flash_mla.cpp is only ever compiled into the AVX512+AMX-flagged "
"_C target (see cmake/cpu_extension.cmake's VLLM_EXT_SRC_SGL) and "
"relies on that unconditionally -- CPU_CAPABILITY_AVX512 must be "
"defined here.");
#endif
namespace {
constexpr int64_t kNopeDim = 448;
constexpr int64_t kRopeDim = 64;
constexpr int64_t kHeadDim = kNopeDim + kRopeDim;
constexpr int64_t kQuantBlock = 64;
constexpr int64_t kNumQuantBlocks = kNopeDim / kQuantBlock;
constexpr int64_t kScaleBytesPerToken = kNumQuantBlocks + 1;
constexpr int64_t kTokenDataBytes = kNopeDim + kRopeDim * 2;
// ---------------------------------------------------------------------------
// V4_FP8Sparse layout (SGLang's `flash_mla.cpp`) -- byte-for-byte identical
// to vLLM's own fp8_ds_mla layout above (d=512, d_nope=448, d_rope=64,
// tile=64, num_tiles=7):
// per block:
// [block_size * (d_nope + 2*d_rope) bytes ; FP8 NoPE + bf16 RoPE per token]
// [block_size * 8 bytes ; 7 e8m0 scales + 1 pad byte/token]
// ---------------------------------------------------------------------------
struct FP8LayoutMeta {
int64_t d;
int64_t d_nope;
int64_t d_rope;
int64_t tile_size;
int64_t num_tiles;
};
constexpr FP8LayoutMeta kV4Meta = {kHeadDim, kNopeDim, kRopeDim, kQuantBlock, kNumQuantBlocks};
// Convert one fp8_e8m0 byte (= unsigned 8-bit exponent) to float.
// e8m0 only stores an exponent (bias 127); value = 2^(e - 127), with 0xFF
// reserved for NaN.
inline float fp8_e8m0_to_float(uint8_t v) {
if (v == 0xFF) return std::numeric_limits<float>::quiet_NaN();
if (v == 0) return 0.f;
union {
uint32_t u;
float f;
} u;
u.u = static_cast<uint32_t>(v) << 23;
return u.f;
}
template <typename index_t>
inline bool is_valid_sparse_index(index_t idx, int64_t pos, int64_t topk_limit, int64_t total_tokens) {
const int64_t v = static_cast<int64_t>(idx);
return pos < topk_limit && v >= 0 && v < total_tokens;
}
#if defined(CPU_CAPABILITY_AVX512)
inline __attribute__((always_inline)) __m512i cvt_fp8_32_to_scaled_bf16(__m256i fp8, float scale) {
const __m512bh bf16_ext = CVT_FP8_TO_BF16_EXT(fp8);
const __m512 scale_vec = _mm512_mul_ps(_mm512_set1_ps(scale), _mm512_castsi512_ps(_mm512_set1_epi32(kFP8_BIAS)));
const __m512 f0 = _mm512_mul_ps(CVT_BF16_TO_FP32(_mm512_extracti32x8_epi32((__m512i)bf16_ext, 0)), scale_vec);
const __m512 f1 = _mm512_mul_ps(CVT_BF16_TO_FP32(_mm512_extracti32x8_epi32((__m512i)bf16_ext, 1)), scale_vec);
return (__m512i)_mm512_cvtne2ps_pbh(f1, f0);
}
// Load 32 contiguous logical dims (NoPE fp8-dequant-to-bf16, or raw bf16
// RoPE) starting at `dim_offset` from one token row of the V4_FP8Sparse
// layout.
inline __attribute__((always_inline)) __m512i load_fp8_kvcache_32_from_row(
const uint8_t* __restrict__ row_base, const uint8_t* __restrict__ scale_base, int dim_offset) {
if (dim_offset < kV4Meta.d_nope) {
const float scale = fp8_e8m0_to_float(scale_base[dim_offset / kV4Meta.tile_size]);
return cvt_fp8_32_to_scaled_bf16(
_mm256_loadu_si256(reinterpret_cast<const __m256i*>(row_base + dim_offset)), scale);
}
const auto* rope_ptr = reinterpret_cast<const at::BFloat16*>(row_base + kV4Meta.d_nope);
return _mm512_loadu_si512(rope_ptr + dim_offset - kV4Meta.d_nope);
}
inline __attribute__((always_inline)) void init_fp8_kvcache_tile_rows(
const uint8_t* __restrict__ fp8_storage,
const int64_t block_size,
const int64_t storage_block_stride_bytes,
const int64_t token_idx,
const uint8_t*& row_base,
const uint8_t*& scale_base) {
constexpr int64_t nope_rope_per_token = kV4Meta.d_nope + 2 * kV4Meta.d_rope;
constexpr int64_t scale_stride = 8;
const int64_t block_idx = token_idx / block_size;
const int64_t block_off = token_idx - block_idx * block_size;
const uint8_t* block_base = fp8_storage + block_idx * storage_block_stride_bytes;
row_base = block_base + block_off * nope_rope_per_token;
scale_base = block_base + block_size * nope_rope_per_token + block_off * scale_stride;
}
template <bool convert_v, typename scalar_t, typename index_t, typename load_vec_t>
inline __attribute__((always_inline)) void sparse_pack_vnni_Nx32(
scalar_t* __restrict__ dst0,
scalar_t* __restrict__ dst1,
const index_t* __restrict__ ind,
const bool* __restrict__ valid_mask,
int N,
int dim_offset,
int ld_dst0,
int ld_dst1,
const load_vec_t& load_vec) {
__m512i vinputs[16];
int n = 0;
for (; n < N; ++n) {
if (!valid_mask[n]) {
vinputs[n] = _mm512_set1_epi32(0);
} else {
vinputs[n] = load_vec(n, static_cast<int64_t>(ind[n]), dim_offset);
}
}
for (; n < 16; ++n) {
vinputs[n] = _mm512_set1_epi32(0);
}
if constexpr (convert_v) {
for (int nn = 0; nn < 16; nn += 2) {
__m512i d0, d1;
std::tie(d0, d1) = transpose_2x32_16bit(vinputs[nn], vinputs[nn + 1]);
_mm512_storeu_si512(dst1 + (nn >> 1) * ld_dst1 * 2, d0);
_mm512_storeu_si512(dst1 + (nn >> 1) * ld_dst1 * 2 + 32, d1);
}
}
transpose_16x16_32bit(vinputs);
const __mmask16 vmask = (1 << N) - 1;
for (int k = 0; k < 16; ++k) {
_mm512_mask_storeu_epi32(dst0 + k * ld_dst0 * 2, vmask, vinputs[k]);
}
}
#endif // CPU_CAPABILITY_AVX512
// Gather+VNNI-pack `N` fp8-cache rows (dequantized to bf16 while packing)
// into `dst0` (K, vnni-packed for brgemm's B operand) and `dst1` (V,
// vnni-packed).
template <typename scalar_t, typename index_t>
void sparse_pack_vnni_fp8(
scalar_t* __restrict__ dst0,
scalar_t* __restrict__ dst1,
const uint8_t* __restrict__ fp8_storage,
const index_t* __restrict__ ind,
const bool* __restrict__ valid_mask,
int N,
int K,
int Kv,
int64_t block_size,
int64_t storage_block_stride_bytes,
int ld_dst0,
int ld_dst1) {
const int NB = div_up(N, 16);
const int KB = K / 32;
const int KBv = std::min(Kv / 32, KB);
for (int nb = 0; nb < NB; ++nb) {
const uint8_t* row_base[16];
const uint8_t* scale_base[16];
const int nb_size = std::min(N - nb * 16, 16);
for (int n = 0; n < nb_size; ++n) {
if (!valid_mask[nb * 16 + n]) {
row_base[n] = nullptr;
scale_base[n] = nullptr;
continue;
}
init_fp8_kvcache_tile_rows(
fp8_storage, block_size, storage_block_stride_bytes, static_cast<int64_t>(ind[nb * 16 + n]), row_base[n],
scale_base[n]);
}
auto load_vec = [&row_base, &scale_base](int n, int64_t idx, int dim_offset) {
UNUSED(idx);
return load_fp8_kvcache_32_from_row(row_base[n], scale_base[n], dim_offset);
};
for (int kb = 0; kb < KBv; ++kb) {
sparse_pack_vnni_Nx32<true, scalar_t, index_t>(
dst0 + ((kb * 32) >> 1) * ld_dst0 * 2 + nb * 16 * 2,
dst1 + ((nb * 16) >> 1) * ld_dst1 * 2 + kb * 32 * 2,
ind + nb * 16,
valid_mask + nb * 16,
nb_size,
kb * 32,
ld_dst0,
ld_dst1,
load_vec);
}
for (int kb = KBv; kb < KB; ++kb) {
sparse_pack_vnni_Nx32<false, scalar_t, index_t>(
dst0 + ((kb * 32) >> 1) * ld_dst0 * 2 + nb * 16 * 2,
dst1 + ((nb * 16) >> 1) * ld_dst1 * 2 + kb * 32 * 2,
ind + nb * 16,
valid_mask + nb * 16,
nb_size,
kb * 32,
ld_dst0,
ld_dst1,
load_vec);
}
}
}
template <typename scalar_t>
inline void fmla_fill_stub(scalar_t* __restrict__ out, float val, int64_t size) {
using Vec = at::vec::Vectorized<scalar_t>;
constexpr int kVecSize = Vec::size();
const Vec data_vec = Vec(static_cast<scalar_t>(val));
int64_t d = 0;
#pragma GCC unroll 4
for (; d <= size - kVecSize; d += kVecSize) {
data_vec.store(out + d);
}
if (size - d > 0) {
data_vec.store(out + d, size - d);
}
}
template <typename scalar_t, int BLOCK_N>
inline void fmla_copy_stub(scalar_t* __restrict__ out, const float* __restrict__ input) {
static_assert(BLOCK_N % 32 == 0);
using bVec = at::vec::Vectorized<scalar_t>;
using fVec = at::vec::Vectorized<float>;
constexpr int COLS = BLOCK_N / 16;
auto store = [&](auto i) {
constexpr int col = i % COLS;
if constexpr (col % 2 == 0) {
fVec a0 = fVec::loadu(input + col * 16);
fVec a1 = fVec::loadu(input + col * 16 + 16);
bVec out_bvec = convert_from_float_ext<scalar_t>(a0, a1);
out_bvec.store(out + col * 16);
}
};
Unroll<COLS>{}(store);
}
template <typename scalar_t>
inline void fmla_finalize_out(scalar_t* __restrict__ out, const float* __restrict__ acc, float inv_s, int64_t size) {
using bVec = at::vec::Vectorized<scalar_t>;
using fVec = at::vec::Vectorized<float>;
constexpr int kVecSize = bVec::size();
const fVec s_fvec = fVec(inv_s);
int64_t d = 0;
#pragma GCC unroll 4
for (; d <= size - kVecSize; d += kVecSize) {
fVec a0 = fVec::loadu(acc + d) * s_fvec;
fVec a1 = fVec::loadu(acc + d + fVec::size()) * s_fvec;
bVec out_bvec = convert_from_float_ext<scalar_t>(a0, a1);
out_bvec.store(out + d);
}
for (; d < size; ++d) {
out[d] = static_cast<scalar_t>(acc[d] * inv_s);
}
}
// ---------------------------------------------------------------------------
// query : [num_tokens, H_q, D_qk] bf16 (SGLang's `[B, S_q, H_q, D]`
// with S_q folded away -- vLLM's flat per-token convention)
// indices : [num_tokens, topk_main] int64 (flat, block-table-resolved
// slot ids, -1 = invalid; SGLang's "main"/window index set)
// extra_* : same, for the optional "extra"/compressed index set
// attn_sink: [H_q] float32
// output : [num_tokens, H_q, D_v] bf16
// ---------------------------------------------------------------------------
template <typename scalar_t, typename index_t, int64_t BLOCK_N>
void sparse_mla_decode_kernel_impl(
scalar_t* __restrict__ output,
float* __restrict__ lse_out,
const scalar_t* __restrict__ query,
const uint8_t* __restrict__ k_main_fp8,
const uint8_t* __restrict__ k_extra_fp8,
const index_t* __restrict__ indices,
const index_t* __restrict__ extra_indices,
const float* __restrict__ attn_sink,
scalar_t* __restrict__ buffer,
int64_t batches,
int64_t s_q,
int64_t num_heads,
int64_t head_size,
int64_t head_size_v,
int64_t topk_main,
int64_t topk_extra,
int64_t total_tokens_main,
int64_t total_tokens_extra,
int64_t q_strideB,
int64_t q_strideH,
int64_t k_main_block_size,
int64_t k_extra_block_size,
int64_t k_main_block_stride_bytes,
int64_t k_extra_block_stride_bytes,
int64_t idx_strideB,
int64_t extra_idx_strideB,
float scaling,
int64_t buffer_size_per_thread) {
using Vec = at::vec::Vectorized<float>;
// partition heads
constexpr int64_t kBLOCK_H_MAX = 16;
const int64_t BLOCK_H = (batches * s_q) >= 16 ? kBLOCK_H_MAX : 8;
const int64_t num_h_blocks = div_up(num_heads, BLOCK_H);
// parallel on [B, S_q, head_block] -- with s_q == 1 (vLLM's flat call
// convention) this collapses to a plain [num_tokens, head_block] loop,
// one flat iteration per token with no padding to skip.
at::parallel_for(0, batches * s_q * num_h_blocks, 0, [&](int64_t begin, int64_t end) {
int64_t bs{0}, sq{0}, hb{0};
data_index_init(begin, bs, batches, sq, s_q, hb, num_h_blocks);
int tid = at::get_thread_num();
scalar_t* __restrict__ Btmp0 = buffer + tid * buffer_size_per_thread; // K packed
scalar_t* __restrict__ Btmp1 = Btmp0 + BLOCK_N * head_size; // V packed
// f32 V accumulator follows the bf16 packing region (reinterpret cast).
float* __restrict__ v_acc_local = reinterpret_cast<float*>(Btmp1 + BLOCK_N * head_size_v);
fmla_fill_stub(Btmp1, 0.f, BLOCK_N * head_size_v); // initialize V padding
alignas(64) float s_i[kBLOCK_H_MAX * BLOCK_N];
float* __restrict__ s_delta = s_i;
alignas(64) scalar_t s_delta2[kBLOCK_H_MAX * BLOCK_N];
alignas(64) float s_prime[kBLOCK_H_MAX];
alignas(64) float m_prime[kBLOCK_H_MAX];
for (int64_t i = begin; i < end; ++i) {
const int64_t h_start = hb * BLOCK_H;
const int64_t h_end = std::min(h_start + BLOCK_H, num_heads);
const int64_t h_size = h_end - h_start;
const scalar_t* __restrict__ q_ptr = query + bs * q_strideB + h_start * q_strideH;
const index_t* __restrict__ idx_ptr = indices + bs * idx_strideB;
const index_t* __restrict__ extra_idx_ptr = extra_indices == nullptr ? nullptr : extra_indices + bs * extra_idx_strideB;
fmla_fill_stub(s_prime, 0.f, BLOCK_H);
fmla_fill_stub(m_prime, -std::numeric_limits<float>::infinity(), BLOCK_H);
for (int64_t h = 0; h < h_size; ++h) {
fmla_fill_stub(v_acc_local + h * head_size_v, 0.f, head_size_v);
}
auto process_cache = [&](const uint8_t* __restrict__ fp8_ptr,
const index_t* __restrict__ cur_idx_ptr,
int64_t topk_count,
int64_t total_tokens,
int64_t fp8_block_size,
int64_t fp8_block_stride_bytes) {
if (fp8_ptr == nullptr || cur_idx_ptr == nullptr || topk_count == 0) {
return;
}
const int64_t topk_limit = topk_count;
for (int64_t n = 0; n < topk_limit; n += BLOCK_N) {
int64_t n_size = std::min<int64_t>(BLOCK_N, topk_limit - n);
const int64_t padded_n_size = div_up(int(n_size), TILE_K) * TILE_K;
bool valid_mask[BLOCK_N];
bool has_valid = false;
for (int64_t k = 0; k < n_size; ++k) {
const bool valid = is_valid_sparse_index(cur_idx_ptr[n + k], n + k, topk_limit, total_tokens);
valid_mask[k] = valid;
has_valid |= valid;
}
if (!has_valid) {
continue;
}
// Pack K (BLOCK_N rows via gather) into Btmp0 (key, vnni) and Btmp1
// (value, vnni), dequantizing FP8 cache entries while packing.
sparse_pack_vnni_fp8<scalar_t, index_t>(
Btmp0, Btmp1, fp8_ptr, cur_idx_ptr + n, valid_mask, static_cast<int>(n_size), static_cast<int>(head_size),
static_cast<int>(head_size_v), fp8_block_size, fp8_block_stride_bytes, static_cast<int>(BLOCK_N),
static_cast<int>(head_size_v));
// Q @ K
at::native::cpublas::brgemm(
/* M */ h_size,
/* N */ n_size,
/* K */ head_size,
/* lda */ q_strideH,
/* ldb */ BLOCK_N,
/* ldc */ BLOCK_N,
/* add_C */ false,
/* A */ q_ptr,
/* B */ Btmp0,
/* C */ s_i);
const Vec scale_vec = Vec(scaling);
for (int64_t h = 0; h < h_size; ++h) {
float* row = s_i + h * BLOCK_N;
at::vec::map<float>([scale_vec](Vec x) { return x * scale_vec; }, row, row, n_size);
for (int64_t k = 0; k < n_size; ++k) {
if (!valid_mask[k]) {
row[k] = -std::numeric_limits<float>::infinity();
}
}
// online softmax update
float m_i = at::vec::reduce_all<float>([](Vec& x, Vec& y) { return at::vec::maximum(x, y); }, row, n_size);
m_i = std::max(m_i, m_prime[h]);
if (!std::isfinite(m_i)) {
fmla_fill_stub(s_delta + h * BLOCK_N, 0.f, padded_n_size);
fmla_copy_stub<scalar_t, BLOCK_N>(s_delta2 + h * BLOCK_N, s_delta + h * BLOCK_N);
continue;
}
const float m_delta = std::exp(m_prime[h] - m_i);
at::vec::map<float>([m_i](Vec x) { return (x - Vec(m_i)).exp_u20(); }, s_delta + h * BLOCK_N, row, n_size);
s_prime[h] *= m_delta;
s_prime[h] += at::vec::reduce_all<float>([](Vec& x, Vec& y) { return x + y; }, s_delta + h * BLOCK_N, n_size);
m_prime[h] = m_i;
float scale_m = m_delta;
at::vec::map<float>(
[scale_m](Vec x) { return x * Vec(scale_m); }, v_acc_local + h * head_size_v, v_acc_local + h * head_size_v,
head_size_v);
fmla_fill_stub(s_delta + h * BLOCK_N + n_size, 0.f, padded_n_size - n_size);
fmla_copy_stub<scalar_t, BLOCK_N>(s_delta2 + h * BLOCK_N, s_delta + h * BLOCK_N);
}
// V' <- s_delta @ V + V' (accumulate into v_acc_local at f32)
at::native::cpublas::brgemm(
/* M */ h_size,
/* N */ head_size_v,
/* K */ padded_n_size,
/* lda */ BLOCK_N,
/* ldb */ head_size_v,
/* ldc */ head_size_v,
/* add_C */ true,
/* A */ s_delta2,
/* B */ Btmp1,
/* C */ v_acc_local);
}
};
process_cache(k_main_fp8, idx_ptr, topk_main, total_tokens_main, k_main_block_size, k_main_block_stride_bytes);
process_cache(
k_extra_fp8, extra_idx_ptr, topk_extra, total_tokens_extra, k_extra_block_size, k_extra_block_stride_bytes);
// Apply attention sink correction directly on the output and lse.
// out *= exp(lse_no_sink) / (exp(lse_no_sink) + exp(attn_sink))
// = 1 / (1 + exp(attn_sink - lse_no_sink))
// (Algebraically equivalent to treating attn_sink as a virtual,
// zero-value extra key inside the softmax.)
for (int64_t h = 0; h < h_size; ++h) {
const int64_t hh = h_start + h;
const bool lonely = !std::isfinite(m_prime[h]) || s_prime[h] == 0.f;
float lse_val = lonely ? std::numeric_limits<float>::infinity() : (m_prime[h] + std::log(s_prime[h]));
float inv_s = lonely ? 0.f : (1.f / s_prime[h]);
if (!lonely && attn_sink != nullptr) {
const float sink = attn_sink[hh];
const float corr = 1.f / (1.f + std::exp(sink - lse_val));
inv_s *= corr;
}
scalar_t* out_row = output + bs * (s_q * num_heads * head_size_v) + sq * (num_heads * head_size_v) + hh * head_size_v;
if (lonely) {
fmla_fill_stub(out_row, 0.f, head_size_v);
} else {
fmla_finalize_out<scalar_t>(out_row, v_acc_local + h * head_size_v, inv_s, head_size_v);
}
lse_out[bs * num_heads * s_q + hh * s_q + sq] = lse_val;
}
data_index_step(bs, batches, sq, s_q, hb, num_h_blocks);
}
at::native::cpublas::brgemm_release();
});
}
} // anonymous namespace
void flash_mla_with_kvcache_cpu(
at::Tensor& out,
at::Tensor& q,
at::Tensor& window_cache_2d,
at::Tensor& window_slots,
int64_t window_block_size,
at::Tensor& compressed_cache_2d,
at::Tensor& compressed_slots,
int64_t compressed_block_size,
at::Tensor& attn_sink,
double scale) {
TORCH_CHECK(
q.dim() == 3 && q.size(2) == kHeadDim,
"flash_mla_with_kvcache_cpu: q must be [num_tokens, num_heads, 512]");
TORCH_CHECK(
out.dim() == 3 && out.size(0) == q.size(0) && out.size(1) == q.size(1) && out.size(2) == kHeadDim,
"flash_mla_with_kvcache_cpu: out must be [num_tokens, num_heads, 512]");
TORCH_CHECK(
out.scalar_type() == at::kBFloat16 && out.is_contiguous(),
"flash_mla_with_kvcache_cpu: out must be bf16 and contiguous "
"(pre-allocate once in Python -- see DeepseekV4CPUAttention.forward_mqa)");
TORCH_CHECK(out.device().is_cpu(), "flash_mla_with_kvcache_cpu: out must be a CPU tensor");
TORCH_CHECK(
q.scalar_type() == at::kBFloat16,
"flash_mla_with_kvcache_cpu: q must be bf16 (the AMX/VNNI kernel's "
"FP8->bf16 dequant intrinsics are bf16-specific)");
TORCH_CHECK(
window_slots.dim() == 2 && window_slots.size(0) == q.size(0),
"flash_mla_with_kvcache_cpu: window_slots must be [num_tokens, num_window]");
TORCH_CHECK(
compressed_slots.dim() == 2 && compressed_slots.size(0) == q.size(0),
"flash_mla_with_kvcache_cpu: compressed_slots must be [num_tokens, num_compressed]");
TORCH_CHECK(
window_slots.scalar_type() == at::kLong && compressed_slots.scalar_type() == at::kLong,
"flash_mla_with_kvcache_cpu: slot tensors must be int64");
TORCH_CHECK(
attn_sink.numel() == q.size(1), "flash_mla_with_kvcache_cpu: attn_sink must have num_heads entries");
TORCH_CHECK(
attn_sink.scalar_type() == at::kFloat && attn_sink.is_contiguous(),
"flash_mla_with_kvcache_cpu: attn_sink must be float32 and contiguous "
"(convert once in Python -- see DeepseekV4CPUAttention.forward_mqa)");
TORCH_CHECK(
window_slots.is_contiguous() && compressed_slots.is_contiguous(),
"flash_mla_with_kvcache_cpu: window_slots/compressed_slots must be contiguous");
TORCH_CHECK(
q.device().is_cpu() && window_cache_2d.device().is_cpu() && window_slots.device().is_cpu() &&
compressed_cache_2d.device().is_cpu() && compressed_slots.device().is_cpu() &&
attn_sink.device().is_cpu(),
"flash_mla_with_kvcache_cpu: all inputs must be CPU tensors");
const int64_t num_tokens = q.size(0);
const int64_t num_heads = q.size(1);
const int64_t num_window = window_slots.size(1);
const int64_t num_compressed = compressed_slots.size(1);
TORCH_CHECK(num_window > 0, "flash_mla_with_kvcache_cpu: window_slots must have at least one column");
// Row stride is threaded through explicitly rather than assumed to equal
// block_size * (token_data_bytes + scale_bytes_per_token): a paged/pooled
// cache buffer's physical row stride is not guaranteed to match the
// logical per-block byte count (matches store_cache.cpp's precedent).
const int64_t window_cache_row_stride = window_cache_2d.stride(0);
const int64_t compressed_cache_row_stride = num_compressed > 0 ? compressed_cache_2d.stride(0) : 0;
// AMX/VNNI path: flatten (batch, s_q) -> batches = num_tokens, s_q = 1.
CHECK_LAST_DIM_CONTIGUOUS_INPUT(q);
constexpr int64_t BLOCK_N = 128; // multiple of 32 for AMX brgemm
constexpr int64_t kBLOCK_H_MAX = 16;
static_assert(kHeadDim % 32 == 0, "head_dim must be a multiple of 32");
const int num_threads = at::get_num_threads();
// Layout per thread (in bf16 elements):
// [Btmp0 : BLOCK_N * D] [Btmp1 : BLOCK_N * D] [v_acc_local : kBLOCK_H_MAX * D floats]
// f32 takes 2 bf16 elements -> multiply by 2.
const int64_t buffer_size_per_thread = BLOCK_N * kHeadDim + BLOCK_N * kHeadDim + 2 * kBLOCK_H_MAX * kHeadDim;
auto buffer = at::empty({num_threads, buffer_size_per_thread}, q.options());
// lse is an internal scratch (attn_sink correction needs it per-token/head)
// -- not surfaced to the Python call site, which only consumes `out`.
auto lse = at::empty({num_tokens, num_heads}, q.options().dtype(at::kFloat));
const int64_t q_strideB = q.stride(0);
const int64_t q_strideH = q.stride(1);
const int64_t idx_strideB = window_slots.stride(0);
const int64_t extra_idx_strideB = num_compressed > 0 ? compressed_slots.stride(0) : 0;
const int64_t total_tokens_main = window_cache_2d.size(0) * window_block_size;
const int64_t total_tokens_extra = num_compressed > 0 ? compressed_cache_2d.size(0) * compressed_block_size : 0;
sparse_mla_decode_kernel_impl<at::BFloat16, int64_t, BLOCK_N>(
out.data_ptr<at::BFloat16>(),
lse.data_ptr<float>(),
q.data_ptr<at::BFloat16>(),
window_cache_2d.data_ptr<uint8_t>(),
num_compressed > 0 ? compressed_cache_2d.data_ptr<uint8_t>() : nullptr,
window_slots.data_ptr<int64_t>(),
num_compressed > 0 ? compressed_slots.data_ptr<int64_t>() : nullptr,
attn_sink.data_ptr<float>(),
buffer.data_ptr<at::BFloat16>(),
/* batches */ num_tokens,
/* s_q */ 1,
num_heads,
/* head_size */ kHeadDim,
/* head_size_v */ kHeadDim,
/* topk_main */ num_window,
/* topk_extra */ num_compressed,
total_tokens_main,
total_tokens_extra,
q_strideB,
q_strideH,
window_block_size,
num_compressed > 0 ? compressed_block_size : 1,
window_cache_row_stride,
compressed_cache_row_stride,
idx_strideB,
extra_idx_strideB,
static_cast<float>(scale),
buffer_size_per_thread);
}
+276
View File
@@ -0,0 +1,276 @@
// vLLM-native: DeepSeek-V4 sparse indexer Q-side RoPE + FP8 quant kernel.
// Ports the math of vLLM's own GPU/Triton reference
// (`_fused_indexer_q_rope_quant_kernel`, common/ops/fused_indexer_q.py) --
// not a port of SGLang's own indexer-Q kernel, since that applies RoPE plus
// a 128-point Hadamard rotation the real DeepSeek-V4-Flash checkpoint
// doesn't use (verified: it applies RoPE only, no Hadamard step).
//
// Per (token, head): GPT-J interleaved RoPE on the last `rot_dim` dims;
// leading NoPE dims pass through unrotated. RoPE-rotated values are bf16-
// round-tripped before the absmax reduction (matching the K-side compressor
// kernel), NoPE values are not. One UE8M0 scale per (token, head) covers the
// full head_dim; since the FP8 Q tensor has no companion scale tensor, the
// scale is folded into `index_weights_out` instead:
// weights_out = weights * q_scale * softmax_scale * head_scale
// MXFP4 (`use_fp4=True`) has per-block scales that can't be folded this way
// and stays on triton-cpu -- out of scope here.
//
// AVX512 path: NoPE copy+absmax and the FP8 scale/clamp use plain 16-wide
// vector ops (final FP8 cast stays scalar, matching SGLang's own
// `act_quant_cpu_impl`). The RoPE rotation vectorizes the interleaved
// (x_even, x_odd) pairs via in-register permutes (`_mm512_permute_ps` /
// `_mm512_permutexvar_ps`), the same technique compressor.cpp's
// `apply_gptj_rope_inplace` and store_cache.cpp's
// `inverse_gptj_rope_o_proj_cpu` use, since gather/scatter is comparatively
// expensive on AVX512.
#include <algorithm>
#include <cmath>
#include <vector>
#include "common.h"
#include "vec.h"
#if !defined(CPU_CAPABILITY_AVX512)
static_assert(
false,
"indexer.cpp is only ever compiled into the AVX512+AMX-flagged "
"_C target (see cmake/cpu_extension.cmake's VLLM_EXT_SRC_SGL) and "
"relies on that unconditionally -- CPU_CAPABILITY_AVX512 must be "
"defined here.");
#endif
namespace {
constexpr float kFp8Max = 448.0f;
#if defined(CPU_CAPABILITY_AVX512)
// fp32 -> bf16 -> fp32 round trip via the AVX512-BF16 conversion
// instruction, numerically matching `static_cast<float>(at::BFloat16(x))`
// (round-to-nearest-even).
inline __m512 bf16_round_trip_avx512(__m512 x) {
return CVT_BF16_TO_FP32((__m256i)(_mm512_cvtneps_pbh(x)));
}
inline __m512 abs_ps(__m512 x) {
return _mm512_andnot_ps(_mm512_set1_ps(-0.0f), x);
}
#endif // CPU_CAPABILITY_AVX512
void fused_indexer_q_rope_quant_impl(
const int64_t* __restrict__ positions, // [num_tokens]
const float* __restrict__ index_q, // [num_tokens, num_heads, head_dim]
int64_t index_q_stride0, int64_t index_q_stride1,
const float* __restrict__ cos_sin_cache, // [max_pos, rot_dim]
int64_t cos_sin_stride, int64_t half_rot_dim,
at::Float8_e4m3fn* __restrict__ index_q_fp8, // [num_tokens, num_heads,
// head_dim]
int64_t index_q_fp8_stride0, int64_t index_q_fp8_stride1, int64_t head_dim,
const float* __restrict__ index_weights, // [num_tokens, num_heads]
int64_t index_weights_stride, float softmax_scale, float head_scale,
float* __restrict__ index_weights_out, // [num_tokens, num_heads]
int64_t index_weights_out_stride, int64_t num_tokens, int64_t num_heads) {
const int64_t rot_dim = 2 * half_rot_dim;
const int64_t nope_dim = head_dim - rot_dim;
at::parallel_for(
0, num_tokens * num_heads, 0, [&](int64_t begin, int64_t end) {
std::vector<float> buf(head_dim);
for (int64_t idx = begin; idx < end; ++idx) {
const int64_t t = idx / num_heads;
const int64_t h = idx % num_heads;
const int64_t pos = positions[t];
const float* const cos = cos_sin_cache + pos * cos_sin_stride;
const float* const sin = cos + half_rot_dim;
const float* const q =
index_q + t * index_q_stride0 + h * index_q_stride1;
float amax = 1e-4f;
#if defined(CPU_CAPABILITY_AVX512)
{
__m512 vamax = _mm512_set1_ps(amax);
for (int64_t d = 0; d < nope_dim; d += 16) {
__m512 v = _mm512_loadu_ps(q + d);
_mm512_storeu_ps(&buf[d], v);
vamax = _mm512_max_ps(vamax, abs_ps(v));
}
amax = _mm512_reduce_max_ps(vamax);
}
#endif
const float* const q_rot = q + nope_dim;
#if defined(CPU_CAPABILITY_AVX512)
{
// Permute-based (no gather/scatter), same technique as
// compressor.cpp's apply_gptj_rope_inplace / store_cache.cpp's
// inverse_gptj_rope_o_proj_cpu: `_mm512_permute_ps` swaps each
// pair's (even, odd) lanes in one shuffle, and cos/sin -- loaded as
// 8 contiguous values -- are duplicated pairwise via
// `_mm512_permutexvar_ps` to align with that layout. Two batches of
// 8 pairs per iteration (16 total, matching the old gather step).
// Unlike the RoPE kernels above, the result here stays in its
// original interleaved layout, so no un-permute/scatter is needed
// before the bf16 round-trip and store.
const __m512i dup_idx = _mm512_set_epi32(7, 7, 6, 6, 5, 5, 4, 4, 3,
3, 2, 2, 1, 1, 0, 0);
// Forward GPT-J rotation negates the sin broadcast on the even
// (real) lane of each pair; the odd (imag) lane keeps sin's sign.
const __m512 sign_mask = _mm512_castsi512_ps(_mm512_set_epi32(
0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0,
(int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0,
(int)0x80000000, 0, (int)0x80000000));
__m512 vamax_rope = _mm512_setzero_ps();
for (int64_t i = 0; i < half_rot_dim; i += 16) {
const float* const base = q_rot + 2 * i;
float* const dst = buf.data() + nope_dim + 2 * i;
__m512 xv0 = _mm512_loadu_ps(base);
__m512 cos_b0 = _mm512_permutexvar_ps(
dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(cos + i)));
__m512 sin_b0 = _mm512_permutexvar_ps(
dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(sin + i)));
__m512 out0 = _mm512_fmadd_ps(
xv0, cos_b0,
_mm512_mul_ps(_mm512_permute_ps(xv0, 0xB1),
_mm512_xor_ps(sin_b0, sign_mask)));
// Match reference numerics: fp32 -> bf16 -> fp32 before the
// absmax.
out0 = bf16_round_trip_avx512(out0);
_mm512_storeu_ps(dst, out0);
vamax_rope = _mm512_max_ps(vamax_rope, abs_ps(out0));
__m512 xv1 = _mm512_loadu_ps(base + 16);
__m512 cos_b1 = _mm512_permutexvar_ps(
dup_idx,
_mm512_castps256_ps512(_mm256_loadu_ps(cos + i + 8)));
__m512 sin_b1 = _mm512_permutexvar_ps(
dup_idx,
_mm512_castps256_ps512(_mm256_loadu_ps(sin + i + 8)));
__m512 out1 = _mm512_fmadd_ps(
xv1, cos_b1,
_mm512_mul_ps(_mm512_permute_ps(xv1, 0xB1),
_mm512_xor_ps(sin_b1, sign_mask)));
out1 = bf16_round_trip_avx512(out1);
_mm512_storeu_ps(dst + 16, out1);
vamax_rope = _mm512_max_ps(vamax_rope, abs_ps(out1));
}
amax = std::max(amax, _mm512_reduce_max_ps(vamax_rope));
}
#endif
const float exponent = std::ceil(std::log2(amax / kFp8Max));
const float scale = std::exp2(exponent);
const float inv_scale = 1.0f / scale;
at::Float8_e4m3fn* const out =
index_q_fp8 + t * index_q_fp8_stride0 + h * index_q_fp8_stride1;
#if defined(CPU_CAPABILITY_AVX512)
{
const __m512 vinv = _mm512_set1_ps(inv_scale);
const __m512 vmax = _mm512_set1_ps(kFp8Max);
const __m512 vmin = _mm512_set1_ps(-kFp8Max);
alignas(64) float scaled_buf[16];
for (int64_t d2 = 0; d2 < head_dim; d2 += 16) {
__m512 v = _mm512_loadu_ps(&buf[d2]);
v = _mm512_min_ps(_mm512_max_ps(_mm512_mul_ps(v, vinv), vmin),
vmax);
_mm512_store_ps(scaled_buf, v);
for (int64_t j = 0; j < 16; ++j)
out[d2 + j] = at::Float8_e4m3fn(scaled_buf[j]);
}
}
#endif
float w = index_weights[t * index_weights_stride + h];
w *= scale;
w *= softmax_scale;
w *= head_scale;
index_weights_out[t * index_weights_out_stride + h] = w;
}
});
}
} // anonymous namespace
void fused_indexer_q_rope_quant_cpu(at::Tensor& positions, at::Tensor& index_q,
at::Tensor& index_q_cos_sin_cache,
at::Tensor& index_q_fp8,
at::Tensor& index_weights,
double index_weights_softmax_scale,
double index_weights_head_scale,
at::Tensor& index_weights_out) {
TORCH_CHECK(
positions.dim() == 1 && positions.scalar_type() == at::kLong,
"fused_indexer_q_rope_quant_cpu: positions must be a 1D int64 tensor");
TORCH_CHECK(positions.is_contiguous(),
"fused_indexer_q_rope_quant_cpu: positions must be contiguous");
TORCH_CHECK(index_q.dim() == 3,
"fused_indexer_q_rope_quant_cpu: index_q must be 3D");
TORCH_CHECK(index_q.scalar_type() == at::kFloat,
"fused_indexer_q_rope_quant_cpu: index_q must be float32 "
"(convert once in Python)");
TORCH_CHECK(index_q.stride(2) == 1,
"fused_indexer_q_rope_quant_cpu: index_q must be contiguous in "
"the head_dim axis");
TORCH_CHECK(
index_q_cos_sin_cache.dim() == 2 && index_q_cos_sin_cache.stride(1) == 1,
"fused_indexer_q_rope_quant_cpu: index_q_cos_sin_cache must be a "
"contiguous 2D tensor");
TORCH_CHECK(
index_q_cos_sin_cache.scalar_type() == at::kFloat,
"fused_indexer_q_rope_quant_cpu: index_q_cos_sin_cache must be float32 "
"(convert once in Python)");
TORCH_CHECK(index_q_fp8.sizes() == index_q.sizes() &&
index_q_fp8.scalar_type() == at::kFloat8_e4m3fn &&
index_q_fp8.stride(2) == 1,
"fused_indexer_q_rope_quant_cpu: index_q_fp8 must match "
"index_q's shape, be "
"float8_e4m3fn, and contiguous in the head_dim axis");
TORCH_CHECK(index_weights.dim() == 2 && index_weights.stride(1) == 1,
"fused_indexer_q_rope_quant_cpu: index_weights must be a "
"contiguous-in-head 2D tensor");
TORCH_CHECK(index_weights.scalar_type() == at::kFloat,
"fused_indexer_q_rope_quant_cpu: index_weights must be float32 "
"(convert once in Python)");
TORCH_CHECK(index_weights_out.sizes() == index_weights.sizes() &&
index_weights_out.scalar_type() == at::kFloat &&
index_weights_out.stride(1) == 1,
"fused_indexer_q_rope_quant_cpu: index_weights_out must match "
"index_weights' shape, "
"be float32, and contiguous in the head axis");
TORCH_CHECK(positions.device().is_cpu() && index_q.device().is_cpu() &&
index_q_cos_sin_cache.device().is_cpu() &&
index_q_fp8.device().is_cpu() &&
index_weights.device().is_cpu() &&
index_weights_out.device().is_cpu(),
"fused_indexer_q_rope_quant_cpu: all inputs must be CPU tensors");
const int64_t num_tokens = positions.size(0);
const int64_t num_heads = index_q.size(1);
const int64_t head_dim = index_q.size(2);
const int64_t half_rot_dim = index_q_cos_sin_cache.size(-1) / 2;
// Only ever called with head_dim=128, rot_dim=64 (DeepseekV4's sparse
// indexer Q side); the AVX512 loops below process 16 elements/pairs per
// iteration with no scalar remainder, so require even divisibility.
TORCH_CHECK(head_dim % 16 == 0 && (head_dim - 2 * half_rot_dim) % 16 == 0 &&
half_rot_dim % 16 == 0,
"fused_indexer_q_rope_quant_cpu: head_dim, head_dim-rot_dim, and "
"rot_dim/2 "
"must be multiples of 16");
fused_indexer_q_rope_quant_impl(
positions.data_ptr<int64_t>(), index_q.data_ptr<float>(),
index_q.stride(0), index_q.stride(1),
index_q_cos_sin_cache.data_ptr<float>(), index_q_cos_sin_cache.stride(0),
half_rot_dim, index_q_fp8.data_ptr<at::Float8_e4m3fn>(),
index_q_fp8.stride(0), index_q_fp8.stride(1), head_dim,
index_weights.data_ptr<float>(), index_weights.stride(0),
static_cast<float>(index_weights_softmax_scale),
static_cast<float>(index_weights_head_scale),
index_weights_out.data_ptr<float>(), index_weights_out.stride(0),
num_tokens, num_heads);
}
File diff suppressed because it is too large Load Diff
+9 -9
View File
@@ -1,14 +1,14 @@
// vLLM-native CPU cache-write op for MLA's single-latent-buffer KV cache.
// `concat_and_cache_mla` (the generic op used by every GPU backend) is
// CUDA-only; this is the CPU counterpart, adapted in spirit from SGLang's
// `store_cache_cpu` (csrc/cpu/kvcache.cpp).
//
// `concat_and_cache_mla` (the generic MLA cache-write op used by every GPU
// backend) is registered CUDA-only. This is the CPU counterpart, adapted in
// spirit from SGLang's `store_cache_cpu` (csrc/cpu/kvcache.cpp) but
// generalized to write two source tensors (`kv_c_normed`, `k_pe`) into two
// different column-offset ranges of the SAME destination row -- SGLang's
// version assumes k/v land in two independent, equal-row-width cache
// tensors, which doesn't hold here since MLA's cache is one 576-wide buffer
// and the two column ranges (512-wide, 64-wide) don't match the buffer's
// true per-token stride, so the write can't reuse `store_cache_cpu` as-is.
// Difference from upstream: writes two source tensors (`kv_c_normed`,
// `k_pe`) into two column-offset ranges of the SAME destination row.
// Upstream assumes k/v land in two independent, equal-row-width cache
// tensors -- MLA's cache is one 576-wide buffer whose two column ranges
// (512-wide, 64-wide) don't match that per-token stride, so
// `store_cache_cpu` can't be reused as-is.
#include "common.h"
#include "vec.h"
+3 -4
View File
@@ -871,13 +871,13 @@ static inline void check_moe_scales(
// topk_ids: [M, topk] (int32_t)
//
at::Tensor fused_experts_cpu(
void fused_experts_cpu(
at::Tensor& out,
at::Tensor& hidden_states,
at::Tensor& w1,
at::Tensor& w2,
at::Tensor& topk_weights,
at::Tensor& topk_ids,
bool inplace,
int64_t moe_comp_method,
const std::optional<at::Tensor>& w1_scale,
const std::optional<at::Tensor>& w2_scale,
@@ -943,7 +943,7 @@ at::Tensor fused_experts_cpu(
// check scales
check_moe_scales(moe_comp_method, w1_scale, w2_scale, block_size);
at::Tensor out_hidden_states = inplace ? hidden_states : at::empty_like(hidden_states);
at::Tensor& out_hidden_states = out;
// NB: worst case is each expert holds a block with remainder of 1
// 1. sorted_ids : [M * topk + E * (BLOCK_M - 1)]
@@ -1214,7 +1214,6 @@ at::Tensor fused_experts_cpu(
with_bias);
}
});
return out_hidden_states;
}
// shared expert kernel
+217
View File
@@ -0,0 +1,217 @@
// Adapted from
// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu:
// `fp8_paged_mqa_logits_cpu`/`fp8_paged_mqa_logits_cpu_impl` (DECODE path).
//
// Reads the paged indexer K-cache directly via `page_table` -- no separate
// eager gather step -- and processes the whole decode batch (one query token
// per request; the CPU indexer decode path doesn't support native MTP/spec
// decode yet, see `sparse_attn_indexer_cpu`) in a single `at::parallel_for`
// flattening `(batch, token)`, exactly like upstream. AVX512 `dot_fp8_128`
// dequant-dot-product and the split K-region-then-scale-region page byte
// layout are verbatim (that part of the upstream contract already matches
// vLLM's cache, written by `compress_norm_rope_store_indexer_cpu` in
// compressor.cpp).
//
// Differences from upstream:
// - `block_size` is a runtime argument instead of the hardcoded
// `kBlockSize = 64`. vLLM's indexer K-cache shares the model's single
// global `--block-size` KV-cache-group setting (confirmed empirically:
// the DeepSeek-V4-Flash TP1/TP4 baseline configs run with
// `block_size=256`, not 64 -- see `.agent-workspace/perf_baseline_tp*_
// server.log`), so the upstream `kvcache_fp8.size(1) == 64` shape
// contract does not hold in general.
// - `q_fp8`/`weight` drop the redundant `S_q=1` dimension (`[batch, heads,
// 128]`/`[batch, heads]` instead of `[batch, 1, heads, 128]`) since S_q is
// always 1 here; `weight` is fp32-only (already folds q_scale/softmax_
// scale/head_scale, matching this call site's actual dtype -- unlike
// upstream's bf16/fp16/fp32 dispatch, which vLLM's CPU indexer never
// produces).
// - `index_head_dim` (128) stays a compile-time constant, matching
// upstream: it's `config.index_head_dim`, a genuine DeepSeek-V4 model
// invariant, not a vLLM KV-cache config knob like `block_size`.
#include "common.h"
#include "vec.h"
#if !defined(CPU_CAPABILITY_AVX512)
static_assert(false,
"paged_mqa_logits.cpp is only ever compiled into the "
"AVX512+AMX-flagged _C target (see cmake/cpu_extension.cmake's "
"VLLM_EXT_SRC_SGL) and relies on that unconditionally -- "
"CPU_CAPABILITY_AVX512 must be defined here.");
#endif
namespace {
constexpr int64_t kIndexHeadDim = 128;
constexpr int64_t kIndexHeadDimWithScaleBytes = 132;
inline float dot_fp8_128(const uint8_t* k, const uint8_t* q) {
__m512 acc = _mm512_setzero_ps();
for (int64_t d = 0; d < kIndexHeadDim; d += 32) {
const __m256i k8 =
_mm256_loadu_si256(reinterpret_cast<const __m256i*>(k + d));
const __m256i q8 =
_mm256_loadu_si256(reinterpret_cast<const __m256i*>(q + d));
acc = _mm512_dpbf16_ps(acc, CVT_FP8_TO_BF16(k8), CVT_FP8_TO_BF16(q8));
}
return _mm512_reduce_add_ps(acc);
}
template <typename seq_t, typename page_t>
void fp8_paged_mqa_logits_cpu_impl(const at::Tensor& q_fp8,
const at::Tensor& kvcache_fp8,
const at::Tensor& weight,
const at::Tensor& seq_lens,
const at::Tensor& page_table,
at::Tensor& logits, int64_t block_size,
int64_t max_seq_len) {
const int64_t batch_size = q_fp8.size(0);
const int64_t num_heads = q_fp8.size(1);
const int64_t num_pages = kvcache_fp8.size(0);
// Row stride is threaded through explicitly rather than assumed equal to
// the page byte width: the K-cache may be a page of a larger pooled
// allocation shared across layers (matches store_cache.cpp/flash_mla.cpp's
// precedent).
const int64_t page_stride = kvcache_fp8.stride(0);
const int64_t pages_per_batch = page_table.size(1);
const int64_t scale_offset_bytes = block_size * kIndexHeadDim;
const auto* q_ptr = reinterpret_cast<const uint8_t*>(q_fp8.const_data_ptr());
const auto* cache_ptr =
reinterpret_cast<const uint8_t*>(kvcache_fp8.const_data_ptr());
const auto* weight_ptr = weight.const_data_ptr<float>();
const auto* seq_ptr = seq_lens.const_data_ptr<seq_t>();
const auto* page_ptr = page_table.const_data_ptr<page_t>();
auto* out_ptr = logits.data_ptr<float>();
at::parallel_for(
0, batch_size * max_seq_len, GRAIN_SIZE / kIndexHeadDim,
[&](int64_t begin, int64_t end) {
int64_t b{0}, token{0};
data_index_init(begin, b, batch_size, token, max_seq_len);
for (int64_t i = begin; i < end; ++i) {
const int64_t seq_len = static_cast<int64_t>(seq_ptr[b]);
if (token >= seq_len) {
data_index_step(b, batch_size, token, max_seq_len);
continue;
}
const int64_t q_batch_offset = b * num_heads * kIndexHeadDim;
const int64_t weight_batch_offset = b * num_heads;
const int64_t page_batch_offset = b * pages_per_batch;
float* out_row = out_ptr + b * max_seq_len;
const int64_t logical_page = token / block_size;
const int64_t token_in_page = token % block_size;
const int64_t physical_page =
static_cast<int64_t>(page_ptr[page_batch_offset + logical_page]);
const uint8_t* block = cache_ptr + physical_page * page_stride;
const uint8_t* k_token = block + token_in_page * kIndexHeadDim;
const float* scale_ptr =
reinterpret_cast<const float*>(block + scale_offset_bytes);
const float k_scale = scale_ptr[token_in_page];
float score_sum = 0.0f;
for (int64_t h = 0; h < num_heads; ++h) {
const uint8_t* q_head = q_ptr + q_batch_offset + h * kIndexHeadDim;
float dot = dot_fp8_128(k_token, q_head);
dot = std::max(dot, 0.0f);
score_sum += dot * weight_ptr[weight_batch_offset + h];
}
out_row[token] = score_sum * k_scale;
data_index_step(b, batch_size, token, max_seq_len);
}
});
}
template <typename seq_t>
void dispatch_page_type(const at::Tensor& q_fp8, const at::Tensor& kvcache_fp8,
const at::Tensor& weight, const at::Tensor& seq_lens,
const at::Tensor& page_table, at::Tensor& logits,
int64_t block_size, int64_t max_seq_len) {
if (page_table.scalar_type() == at::kInt) {
fp8_paged_mqa_logits_cpu_impl<seq_t, int32_t>(q_fp8, kvcache_fp8, weight,
seq_lens, page_table, logits,
block_size, max_seq_len);
} else if (page_table.scalar_type() == at::kLong) {
fp8_paged_mqa_logits_cpu_impl<seq_t, int64_t>(q_fp8, kvcache_fp8, weight,
seq_lens, page_table, logits,
block_size, max_seq_len);
} else {
TORCH_CHECK(false, "page_table must be int32 or int64");
}
}
} // namespace
at::Tensor fp8_paged_mqa_logits_cpu(at::Tensor& q_fp8, at::Tensor& kvcache_fp8,
at::Tensor& weight, at::Tensor& seq_lens,
at::Tensor& page_table, int64_t block_size,
int64_t max_seq_len) {
CHECK_INPUT(q_fp8);
CHECK_CPU(kvcache_fp8);
CHECK_INPUT(weight);
CHECK_INPUT(seq_lens);
CHECK_INPUT(page_table);
TORCH_CHECK(
q_fp8.dim() == 3 && q_fp8.scalar_type() == at::kFloat8_e4m3fn,
"fp8_paged_mqa_logits_cpu: q_fp8 must be a 3D float8_e4m3fn tensor "
"[batch, heads, 128]");
TORCH_CHECK(q_fp8.size(2) == kIndexHeadDim,
"fp8_paged_mqa_logits_cpu: q_fp8 head_dim must be 128");
TORCH_CHECK(kvcache_fp8.dim() == 2 && kvcache_fp8.scalar_type() == at::kByte,
"fp8_paged_mqa_logits_cpu: kvcache_fp8 must be a 2D uint8 tensor "
"[num_pages, buf_numel_per_page]");
TORCH_CHECK(kvcache_fp8.stride(1) == 1 &&
kvcache_fp8.stride(0) >= kvcache_fp8.size(1),
"fp8_paged_mqa_logits_cpu: kvcache_fp8 must be contiguous "
"within a page (may be a page of a larger pooled allocation "
"between pages)");
TORCH_CHECK(block_size > 0,
"fp8_paged_mqa_logits_cpu: block_size must be positive");
TORCH_CHECK(
kvcache_fp8.size(1) >= block_size * kIndexHeadDimWithScaleBytes,
"fp8_paged_mqa_logits_cpu: kvcache_fp8 page byte width too small for "
"block_size");
const int64_t batch_size = q_fp8.size(0);
const int64_t num_heads = q_fp8.size(1);
TORCH_CHECK(
weight.dim() == 2 && weight.scalar_type() == at::kFloat &&
weight.size(0) == batch_size && weight.size(1) == num_heads,
"fp8_paged_mqa_logits_cpu: weight must be a 2D fp32 tensor [batch, "
"heads]");
TORCH_CHECK(seq_lens.dim() == 1 && seq_lens.size(0) == batch_size,
"fp8_paged_mqa_logits_cpu: seq_lens must be a 1D tensor [batch]");
TORCH_CHECK(page_table.dim() == 2 && page_table.size(0) == batch_size,
"fp8_paged_mqa_logits_cpu: page_table must be a 2D tensor "
"[batch, pages]");
TORCH_CHECK(max_seq_len >= 0,
"fp8_paged_mqa_logits_cpu: max_seq_len must be non-negative");
// Positions with token >= seq_len are never written by the kernel below,
// and topk_transform_512_cpu (the only consumer) never reads them either
// -- left uninitialized, matching upstream.
auto logits =
at::empty({batch_size, max_seq_len}, q_fp8.options().dtype(at::kFloat));
if (batch_size == 0 || max_seq_len == 0) {
return logits;
}
if (seq_lens.scalar_type() == at::kInt) {
dispatch_page_type<int32_t>(q_fp8, kvcache_fp8, weight, seq_lens,
page_table, logits, block_size, max_seq_len);
} else if (seq_lens.scalar_type() == at::kLong) {
dispatch_page_type<int64_t>(q_fp8, kvcache_fp8, weight, seq_lens,
page_table, logits, block_size, max_seq_len);
} else {
TORCH_CHECK(false, "seq_lens must be int32 or int64");
}
return logits;
}
+481
View File
@@ -0,0 +1,481 @@
// Adapted from
// https://github.com/sgl-project/sglang/blob/main/python/sglang/kernels/aot/csrc/cpu/store_cache.cpp
//
// DeepSeek-V4 fused Q RMSNorm+RoPE / KV RoPE+UE8M0-quant+paged-cache-insert
// kernel for the fp8_ds_mla cache layout. Not a port of upstream's
// `set_k_and_s_cpu`/`quant_to_nope_fp8_rope_bf16_pack_cpu` -- their
// pool_kv/pool_score signature doesn't apply to vLLM's layout. This is the
// CPU counterpart to vLLM's own CUDA kernel
// (`fused_deepseek_v4_qnorm_rope_kv_insert_kernel.cu`); see
// `DeepseekV4Attention._fused_qnorm_rope_kv_insert_cpu` (attention.py) for
// the eager reference this replaces.
//
// clang-format off
#include <algorithm>
#include <cmath>
#include <vector>
#include "common.h"
#include "vec.h"
#if !defined(CPU_CAPABILITY_AVX512)
static_assert(false,
"store_cache.cpp is only ever compiled into the AVX512+AMX-flagged "
"_C target (see cmake/cpu_extension.cmake's VLLM_EXT_SRC_SGL) and "
"relies on that unconditionally -- CPU_CAPABILITY_AVX512 must be "
"defined here.");
#endif
namespace {
constexpr int64_t kRopeDim = 64;
constexpr int64_t kQuantBlock = 64;
constexpr float kFp8Max = 448.0f;
constexpr float kFp8Min = -448.0f;
constexpr float kQuantEps = 1e-4f;
inline void apply_gptj_rope_inplace(float* __restrict__ x, const float* __restrict__ cos, const float* __restrict__ sin, int64_t half_dim) {
for (int64_t i = 0; i < half_dim; ++i) {
const float e = x[2 * i];
const float o = x[2 * i + 1];
x[2 * i] = e * cos[i] - o * sin[i];
x[2 * i + 1] = e * sin[i] + o * cos[i];
}
}
// AVX512 tile-quantization, adapted verbatim from SGLang's
// `store_cache.cpp` (`quant_to_nope_fp8_rope_bf16_pack_cpu`'s
// `cvt_fp32x16_to_fp8x16`/`quantize_tile_avx512`): per-64-element tile,
// computes the UE8M0 power-of-two scale and quantizes to fp8_e4m3fn with
// RNE rounding via bit manipulation instead of a per-element scalar
// `at::Float8_e4m3fn` cast. Subnormal-magnitude fp8 lanes are flushed to
// zero (matches SGLang's own convention, `cvt_fp32x16_to_fp8x16`) --
// vLLM's own AVX512 dequant of this cache (`flash_mla.cpp`'s
// `CVT_FP8_TO_BF16_EXT`) doesn't reconstruct exact subnormal magnitudes
// either way, so there is nothing to preserve by rounding them exactly on
// the write side.
#if defined(CPU_CAPABILITY_AVX512)
// Vectorized float32x16 -> fp8_e4m3fn x16 using AVX512 bit manipulation.
// Implements RNE (round-to-nearest-even). Flushes subnormal fp8 to zero.
// fp8_e4m3fn: sign(1), exp(4, bias=7), mant(3), no inf/nan special values.
// Normal: exp_field in [1..15], value = 2^(exp-7) * (1 + mant/8)
// Input assumed clamped to [-448, 448].
inline __m128i cvt_fp32x16_to_fp8x16(__m512 input) {
__m512i bits = _mm512_castps_si512(input);
// Extract sign -> bit 7 of fp8 byte
__m512i signs = _mm512_srli_epi32(_mm512_and_si512(bits, _mm512_set1_epi32(0x80000000)), 24);
// Absolute value bits
__m512i abs_bits = _mm512_and_si512(bits, _mm512_set1_epi32(0x7FFFFFFF));
// Float32 biased exponent
__m512i f32_exp = _mm512_srli_epi32(abs_bits, 23);
// Float32 mantissa (23 bits)
__m512i f32_mant = _mm512_and_si512(abs_bits, _mm512_set1_epi32(0x7FFFFF));
// fp8_exp = f32_exp - 120 (rebias from 127 to 7)
__m512i fp8_exp = _mm512_sub_epi32(f32_exp, _mm512_set1_epi32(120));
// Top 3 mantissa bits
__m512i mant3 = _mm512_srli_epi32(f32_mant, 20);
// RNE rounding: round_bit = bit 19, sticky = bits[18:0]
__m512i round_bit = _mm512_and_si512(_mm512_srli_epi32(f32_mant, 19), _mm512_set1_epi32(1));
__m512i sticky_bits = _mm512_and_si512(f32_mant, _mm512_set1_epi32(0x7FFFF));
__mmask16 has_sticky = _mm512_cmpneq_epi32_mask(sticky_bits, _mm512_setzero_si512());
// Round up if round_bit AND (sticky OR lsb_of_mant3)
__m512i lsb = _mm512_and_si512(mant3, _mm512_set1_epi32(1));
__m512i sticky_or_lsb = _mm512_or_si512(_mm512_maskz_mov_epi32(has_sticky, _mm512_set1_epi32(1)), lsb);
__m512i do_round = _mm512_and_si512(round_bit, sticky_or_lsb);
mant3 = _mm512_add_epi32(mant3, do_round);
// Mantissa overflow (mant3 == 8) -> increment exponent, zero mantissa
__mmask16 mant_ovf = _mm512_cmpeq_epi32_mask(mant3, _mm512_set1_epi32(8));
mant3 = _mm512_mask_mov_epi32(mant3, mant_ovf, _mm512_setzero_si512());
fp8_exp = _mm512_mask_add_epi32(fp8_exp, mant_ovf, fp8_exp, _mm512_set1_epi32(1));
// Clamp exponent: max 15
fp8_exp = _mm512_min_epi32(fp8_exp, _mm512_set1_epi32(15));
// Result: (fp8_exp << 3) | mant3
__m512i result = _mm512_or_si512(_mm512_slli_epi32(fp8_exp, 3), mant3);
// Flush to zero: if f32_exp < 121 (would be subnormal in fp8), output 0
__mmask16 is_subnormal_or_zero = _mm512_cmplt_epi32_mask(f32_exp, _mm512_set1_epi32(121));
result = _mm512_mask_mov_epi32(result, is_subnormal_or_zero, _mm512_setzero_si512());
// Add sign
result = _mm512_or_si512(result, signs);
// Pack 16 x i32 -> 16 x i8
return _mm512_cvtepi32_epi8(result);
}
// Process one tile (64 bf16 values): find amax, compute scale, quantize to fp8.
inline uint8_t quantize_tile_avx512(const at::BFloat16* __restrict__ src, uint8_t* __restrict__ dst) {
// Load 64 bf16 -> 4 x 16 floats
__m256i bf16_0 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src));
__m256i bf16_1 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src + 16));
__m256i bf16_2 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src + 32));
__m256i bf16_3 = _mm256_loadu_si256(reinterpret_cast<const __m256i*>(src + 48));
__m512 f0 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_0), 16));
__m512 f1 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_1), 16));
__m512 f2 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_2), 16));
__m512 f3 = _mm512_castsi512_ps(_mm512_slli_epi32(_mm512_cvtepu16_epi32(bf16_3), 16));
// Absolute values
const __m512i abs_mask_i = _mm512_set1_epi32(0x7FFFFFFF);
__m512 abs0 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f0), abs_mask_i));
__m512 abs1 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f1), abs_mask_i));
__m512 abs2 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f2), abs_mask_i));
__m512 abs3 = _mm512_castsi512_ps(_mm512_and_si512(_mm512_castps_si512(f3), abs_mask_i));
// Horizontal max
__m512 max01 = _mm512_max_ps(abs0, abs1);
__m512 max23 = _mm512_max_ps(abs2, abs3);
__m512 max0123 = _mm512_max_ps(max01, max23);
float amax = _mm512_reduce_max_ps(max0123);
// Scale computation
float scale = std::max(amax / kFp8Max, kQuantEps);
float ceil_log2 = std::ceil(std::log2(scale));
float scale_pow2 = std::exp2(ceil_log2);
float scale_inv = 1.0f / scale_pow2;
int exponent = static_cast<int>(ceil_log2);
uint8_t scale_uint8 = static_cast<uint8_t>(exponent + 127);
// Scale all values
__m512 vinv = _mm512_set1_ps(scale_inv);
__m512 s0 = _mm512_mul_ps(f0, vinv);
__m512 s1 = _mm512_mul_ps(f1, vinv);
__m512 s2 = _mm512_mul_ps(f2, vinv);
__m512 s3 = _mm512_mul_ps(f3, vinv);
// Clamp
__m512 vmax = _mm512_set1_ps(kFp8Max);
__m512 vmin = _mm512_set1_ps(kFp8Min);
s0 = _mm512_max_ps(_mm512_min_ps(s0, vmax), vmin);
s1 = _mm512_max_ps(_mm512_min_ps(s1, vmax), vmin);
s2 = _mm512_max_ps(_mm512_min_ps(s2, vmax), vmin);
s3 = _mm512_max_ps(_mm512_min_ps(s3, vmax), vmin);
// Vectorized float -> fp8 conversion (16 values at a time)
__m128i fp8_0 = cvt_fp32x16_to_fp8x16(s0);
__m128i fp8_1 = cvt_fp32x16_to_fp8x16(s1);
__m128i fp8_2 = cvt_fp32x16_to_fp8x16(s2);
__m128i fp8_3 = cvt_fp32x16_to_fp8x16(s3);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst), fp8_0);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 16), fp8_1);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 32), fp8_2);
_mm_storeu_si128(reinterpret_cast<__m128i*>(dst + 48), fp8_3);
return scale_uint8;
}
#endif // CPU_CAPABILITY_AVX512
template <typename scalar_t>
static void qnorm_rope_kv_insert_impl(
scalar_t* __restrict__ q_out, // [num_tokens_full, q_head_padded, head_dim]
uint8_t* __restrict__ cache, // [num_blocks, cache_row_stride] bytes
const scalar_t* __restrict__ q, // [num_tokens_full, num_heads_q, head_dim]
const scalar_t* __restrict__ kv, // [>=num_tokens_insert, head_dim]
const int64_t* __restrict__ positions, // [num_tokens_full]
const int64_t* __restrict__ slot_mapping, // [num_tokens_insert]
const float* __restrict__ cos_sin_cache, // [max_pos, rope_dim] (first half cos, second half sin)
int64_t num_tokens_full,
int64_t num_heads_q,
int64_t q_head_padded,
int64_t head_dim,
int64_t nope_dim,
int64_t num_quant_blocks,
int64_t token_data_bytes,
int64_t scale_bytes_per_token,
int64_t num_tokens_insert,
int64_t cache_block_size,
int64_t cache_row_stride,
float eps) {
const int64_t rope_half = kRopeDim / 2;
// Q: per-head weight-free RMSNorm + GPT-J RoPE on the last rope_dim dims.
at::parallel_for(0, num_tokens_full * num_heads_q, 0, [&](int64_t begin, int64_t end) {
std::vector<float> buf(head_dim);
for (int64_t idx = begin; idx < end; ++idx) {
const int64_t t = idx / num_heads_q;
const int64_t h = idx % num_heads_q;
const scalar_t* q_th = q + (t * num_heads_q + h) * head_dim;
float sq_sum = 0.f;
for (int64_t i = 0; i < head_dim; ++i) {
const float v = static_cast<float>(q_th[i]);
buf[i] = v;
sq_sum += v * v;
}
const float inv_rms = 1.0f / std::sqrt(sq_sum / static_cast<float>(head_dim) + eps);
for (int64_t i = 0; i < head_dim; ++i)
buf[i] *= inv_rms;
const int64_t pos = positions[t];
const float* cos = cos_sin_cache + pos * kRopeDim;
const float* sin = cos + rope_half;
apply_gptj_rope_inplace(buf.data() + nope_dim, cos, sin, rope_half);
scalar_t* out_th = q_out + (t * q_head_padded + h) * head_dim;
for (int64_t i = 0; i < head_dim; ++i)
out_th[i] = static_cast<scalar_t>(buf[i]);
}
});
// KV: GPT-J RoPE (no RMSNorm) + bf16 round-trip + UE8M0 FP8 quant + paged insert.
at::parallel_for(0, num_tokens_insert, 0, [&](int64_t begin, int64_t end) {
std::vector<float> buf(head_dim);
std::vector<at::BFloat16> nope_bf16(nope_dim);
for (int64_t t = begin; t < end; ++t) {
const int64_t slot_id = slot_mapping[t];
if (slot_id < 0) continue;
const scalar_t* kv_t = kv + t * head_dim;
for (int64_t i = 0; i < head_dim; ++i)
buf[i] = static_cast<float>(kv_t[i]);
const int64_t pos = positions[t];
const float* cos = cos_sin_cache + pos * kRopeDim;
const float* sin = cos + rope_half;
apply_gptj_rope_inplace(buf.data() + nope_dim, cos, sin, rope_half);
// bf16 round-trip, matching the CUDA kernel's double convert.
for (int64_t i = 0; i < head_dim; ++i)
buf[i] = static_cast<float>(at::BFloat16(buf[i]));
for (int64_t i = 0; i < nope_dim; ++i)
nope_bf16[i] = at::BFloat16(buf[i]);
const int64_t block_idx = slot_id / cache_block_size;
const int64_t pos_in_block = slot_id % cache_block_size;
uint8_t* const block_base = cache + block_idx * cache_row_stride;
uint8_t* const token_fp8_ptr = block_base + pos_in_block * token_data_bytes;
uint8_t* const token_bf16_ptr = token_fp8_ptr + nope_dim;
uint8_t* const token_scale_ptr =
block_base + cache_block_size * token_data_bytes + pos_in_block * scale_bytes_per_token;
// AVX512 tile-quantization (SGLang's `quantize_tile_avx512`), one
// 64-wide tile per iteration.
for (int64_t qb = 0; qb < num_quant_blocks; ++qb) {
const at::BFloat16* tile_src = nope_bf16.data() + qb * kQuantBlock;
uint8_t* tile_dst = token_fp8_ptr + qb * kQuantBlock;
token_scale_ptr[qb] = quantize_tile_avx512(tile_src, tile_dst);
}
token_scale_ptr[num_quant_blocks] = 0;
auto* out_rope = reinterpret_cast<at::BFloat16*>(token_bf16_ptr);
for (int64_t i = 0; i < kRopeDim; ++i)
out_rope[i] = at::BFloat16(buf[nope_dim + i]);
}
});
}
} // anonymous namespace
at::Tensor fused_qnorm_rope_kv_insert_cpu(
at::Tensor& q,
at::Tensor& kv,
at::Tensor& positions,
at::Tensor& swa_kv_cache_2d,
at::Tensor& slot_mapping,
at::Tensor& cos_sin_cache,
int64_t q_head_padded,
double eps,
int64_t cache_block_size) {
TORCH_CHECK(
q.dim() == 3 && (q.scalar_type() == at::kBFloat16 || q.scalar_type() == at::kHalf),
"fused_qnorm_rope_kv_insert_cpu: q must be bf16/fp16 [num_tokens, num_heads_q, head_dim]");
TORCH_CHECK(kv.dim() == 2 && kv.scalar_type() == q.scalar_type(), "fused_qnorm_rope_kv_insert_cpu: kv dtype must match q");
TORCH_CHECK(positions.scalar_type() == at::kLong, "fused_qnorm_rope_kv_insert_cpu: positions must be int64");
TORCH_CHECK(slot_mapping.scalar_type() == at::kLong, "fused_qnorm_rope_kv_insert_cpu: slot_mapping must be int64");
TORCH_CHECK(
swa_kv_cache_2d.dim() == 2 && swa_kv_cache_2d.scalar_type() == at::kByte,
"fused_qnorm_rope_kv_insert_cpu: swa_kv_cache_2d must be a 2D uint8 tensor");
TORCH_CHECK(
q.device().is_cpu() && kv.device().is_cpu() && positions.device().is_cpu() && slot_mapping.device().is_cpu() &&
swa_kv_cache_2d.device().is_cpu() && cos_sin_cache.device().is_cpu(),
"fused_qnorm_rope_kv_insert_cpu: all inputs must be CPU tensors");
const int64_t num_tokens_full = q.size(0);
const int64_t num_heads_q = q.size(1);
const int64_t head_dim = q.size(2);
const int64_t nope_dim = head_dim - kRopeDim;
TORCH_CHECK(nope_dim > 0 && nope_dim % kQuantBlock == 0, "fused_qnorm_rope_kv_insert_cpu: unexpected head_dim");
const int64_t num_quant_blocks = nope_dim / kQuantBlock;
const int64_t scale_bytes_per_token = num_quant_blocks + 1;
const int64_t token_data_bytes = nope_dim + kRopeDim * 2;
TORCH_CHECK(q_head_padded >= num_heads_q, "fused_qnorm_rope_kv_insert_cpu: q_head_padded must be >= num_heads_q");
TORCH_CHECK(kv.size(1) == head_dim, "fused_qnorm_rope_kv_insert_cpu: kv head_dim must match q");
TORCH_CHECK(positions.numel() == num_tokens_full, "fused_qnorm_rope_kv_insert_cpu: positions must have num_tokens_full entries");
const int64_t num_tokens_insert = slot_mapping.size(0);
TORCH_CHECK(kv.size(0) >= num_tokens_insert, "fused_qnorm_rope_kv_insert_cpu: kv must have >= num_tokens_insert rows");
TORCH_CHECK(cache_block_size > 0, "fused_qnorm_rope_kv_insert_cpu: cache_block_size must be positive");
TORCH_CHECK(
cos_sin_cache.scalar_type() == at::kFloat && cos_sin_cache.is_contiguous(),
"fused_qnorm_rope_kv_insert_cpu: cos_sin_cache must be float32 and "
"contiguous (convert once in Python)");
TORCH_CHECK(
q.is_contiguous() && kv.is_contiguous() && positions.is_contiguous() && slot_mapping.is_contiguous(),
"fused_qnorm_rope_kv_insert_cpu: q/kv/positions/slot_mapping must be contiguous");
// swa_kv_cache_2d is mutated in place; index via its real row stride
// rather than assuming it equals cache_block_size * (token_data_bytes +
// scale_bytes_per_token) -- a paged/pooled buffer's row stride is not
// guaranteed to match the logical per-block byte count.
const int64_t cache_row_stride = swa_kv_cache_2d.stride(0);
auto q_out = at::zeros({num_tokens_full, q_head_padded, head_dim}, q.options());
AT_DISPATCH_REDUCED_FLOATING_TYPES(q.scalar_type(), "fused_qnorm_rope_kv_insert_cpu", [&] {
qnorm_rope_kv_insert_impl<scalar_t>(
q_out.data_ptr<scalar_t>(),
swa_kv_cache_2d.data_ptr<uint8_t>(),
q.data_ptr<scalar_t>(),
kv.data_ptr<scalar_t>(),
positions.data_ptr<int64_t>(),
slot_mapping.data_ptr<int64_t>(),
cos_sin_cache.data_ptr<float>(),
num_tokens_full,
num_heads_q,
q_head_padded,
head_dim,
nope_dim,
num_quant_blocks,
token_data_bytes,
scale_bytes_per_token,
num_tokens_insert,
cache_block_size,
cache_row_stride,
static_cast<float>(eps));
});
return q_out;
}
// vLLM-native: inverse GPT-J RoPE for `_o_proj`'s output de-rotation (undoes
// the query's own rotation on the attention output before the low-rank
// wo_a/wo_b projection -- see the math writeup in cpu_sparse.py's `_o_proj`).
// Upstream needs this same step (`fused_rope_inplace(..., inverse=True)` in
// `deepseek_v4.py`) but only has a CUDA-JIT implementation, no CPU kernel --
// this fills that gap rather than porting one. Replaces the eager reference
// in `DeepseekV4CPUAttention._o_proj` (cpu_sparse.py). Uses the same
// half-half `[cos...|sin...]` cache layout as `apply_gptj_rope_inplace`
// above (not upstream's interleaved `[c0,s0,c1,s1,...]` layout).
//
// AVX512 path: NoPE dtype-convert copy uses `vec.h`'s `load_float_vec`
// (16-wide bf16/fp16 -> fp32). The interleaved (even,odd) rotation widens
// 32 raw elements to fp32 via `load_float_vec2`, round-trips them through
// an aligned scratch buffer, then gathers/scatters the even/odd lanes --
// same technique as `indexer.cpp`'s Q-side RoPE, since `cos`/`sin` here
// are two contiguous halves rather than per-pair-interleaved.
at::Tensor inverse_gptj_rope_o_proj_cpu(
at::Tensor& o, // [num_tokens, num_heads, head_dim]
at::Tensor& positions, // [num_tokens]
at::Tensor& cos_sin_cache, // [max_pos, rope_dim] (first half cos, second half sin)
int64_t rope_dim) {
TORCH_CHECK(o.dim() == 3, "inverse_gptj_rope_o_proj_cpu: o must be 3D [num_tokens, num_heads, head_dim]");
TORCH_CHECK(positions.scalar_type() == at::kLong, "inverse_gptj_rope_o_proj_cpu: positions must be int64");
TORCH_CHECK(
o.device().is_cpu() && positions.device().is_cpu() && cos_sin_cache.device().is_cpu(),
"inverse_gptj_rope_o_proj_cpu: all inputs must be CPU tensors");
const int64_t num_tokens = o.size(0);
const int64_t num_heads = o.size(1);
const int64_t head_dim = o.size(2);
const int64_t nope_dim = head_dim - rope_dim;
const int64_t half_dim = rope_dim / 2;
TORCH_CHECK(nope_dim >= 0, "inverse_gptj_rope_o_proj_cpu: rope_dim must not exceed head_dim");
// Only ever called with head_dim=512, rope_dim=64 (DeepseekV4CPUAttention's
// _o_proj); the AVX512 loops below process 16 elements/pairs per iteration
// with no scalar remainder, so require both to divide evenly.
TORCH_CHECK(nope_dim % 16 == 0 && half_dim % 16 == 0,
"inverse_gptj_rope_o_proj_cpu: head_dim-rope_dim and rope_dim/2 must be multiples of 16");
TORCH_CHECK(
cos_sin_cache.scalar_type() == at::kFloat && cos_sin_cache.is_contiguous(),
"inverse_gptj_rope_o_proj_cpu: cos_sin_cache must be float32 and "
"contiguous (convert once in Python)");
TORCH_CHECK(o.is_contiguous() && positions.is_contiguous(),
"inverse_gptj_rope_o_proj_cpu: o/positions must be contiguous");
auto out = at::empty({num_tokens, num_heads, head_dim}, o.options().dtype(at::kFloat));
AT_DISPATCH_REDUCED_FLOATING_TYPES(o.scalar_type(), "inverse_gptj_rope_o_proj_cpu", [&] {
const scalar_t* o_ptr = o.data_ptr<scalar_t>();
float* out_ptr = out.data_ptr<float>();
const int64_t* pos_ptr = positions.data_ptr<int64_t>();
const float* cos_sin_ptr = cos_sin_cache.data_ptr<float>();
at::parallel_for(0, num_tokens * num_heads, 0, [&](int64_t begin, int64_t end) {
for (int64_t idx = begin; idx < end; ++idx) {
const int64_t t = idx / num_heads;
const int64_t h = idx % num_heads;
const scalar_t* src = o_ptr + (t * num_heads + h) * head_dim;
float* dst = out_ptr + (t * num_heads + h) * head_dim;
// head_dim=512, rope_dim=64 in practice (checked above), so
// nope_dim/half_dim are always multiples of 16 -- no scalar
// remainder needed.
for (int64_t i = 0; i < nope_dim; i += 16) {
load_float_vec<scalar_t>(src + i).store(dst + i);
}
const int64_t pos = pos_ptr[t];
const float* cos = cos_sin_ptr + pos * rope_dim;
const float* sin = cos + half_dim;
{
// Permute-based (no gather/scatter), adapted from SGLang's
// `rope.cpp` (`apply_rotary_emb_row`) the same way as
// compressor.cpp's `apply_gptj_rope_inplace`: `_mm512_permute_ps`
// swaps each pair's (even, odd) lanes in one shuffle, and cos/sin
// -- loaded as 8 contiguous values -- are duplicated pairwise via
// `_mm512_permutexvar_ps` to align with that layout, since our
// cos/sin are two separate flat arrays rather than SGLang's
// pre-interleaved freqs. Two batches of 8 pairs per iteration
// (matching `load_float_vec2`'s 16+16-lane split).
const __m512i dup_idx = _mm512_set_epi32(7, 7, 6, 6, 5, 5, 4, 4, 3, 3, 2, 2, 1, 1, 0, 0);
// Inverse GPT-J rotation negates the sin broadcast on the odd
// (imag) lane of each pair; the even (real) lane keeps sin's sign.
const __m512 sign_mask = _mm512_castsi512_ps(_mm512_set_epi32(
(int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0,
(int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0, (int)0x80000000, 0));
for (int64_t j = 0; j < half_dim; j += 16) {
const scalar_t* rsrc = src + nope_dim + 2 * j;
at::vec::Vectorized<float> x0, x1;
std::tie(x0, x1) = load_float_vec2(rsrc);
float* rdst = dst + nope_dim + 2 * j;
__m512 xv0 = x0;
__m512 cos_b0 = _mm512_permutexvar_ps(dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(cos + j)));
__m512 sin_b0 = _mm512_permutexvar_ps(dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(sin + j)));
__m512 out0 = _mm512_fmadd_ps(
xv0, cos_b0, _mm512_mul_ps(_mm512_permute_ps(xv0, 0xB1), _mm512_xor_ps(sin_b0, sign_mask)));
_mm512_storeu_ps(rdst, out0);
__m512 xv1 = x1;
__m512 cos_b1 = _mm512_permutexvar_ps(dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(cos + j + 8)));
__m512 sin_b1 = _mm512_permutexvar_ps(dup_idx, _mm512_castps256_ps512(_mm256_loadu_ps(sin + j + 8)));
__m512 out1 = _mm512_fmadd_ps(
xv1, cos_b1, _mm512_mul_ps(_mm512_permute_ps(xv1, 0xB1), _mm512_xor_ps(sin_b1, sign_mask)));
_mm512_storeu_ps(rdst + 16, out1);
}
}
}
});
});
return out;
}
+834
View File
@@ -0,0 +1,834 @@
// Adapted from
// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu:
// `biased_topk_kernel_impl`/`biased_topk_cpu` (flat, non-grouped DeepSeek-V4
// biased top-k) and `hash_topk_kernel_impl`/`hash_topk_cpu` (hash-routed
// layers, expert IDs from a precomputed `tid2eid` lookup table), plus their
// `sigmoid`/`softmax`/`apply_bias`/`sqrtsoftplus` scoring helpers. See
// `biased_topk_cpu`/`hash_topk_cpu` below for the one deliberate deviation
// from the upstream kernel (routed_scaling_factor handling when no shared
// expert is fused into routing, vLLM's CPU DeepSeek-V4 calling convention).
//
// clang-format off
#include <algorithm>
#include <array>
#include <cmath>
#include <limits>
#include <type_traits>
#include <utility>
#include "common.h"
#include "vec.h"
namespace {
template <typename scalar_t, int SIZE, std::enable_if_t<!std::is_same_v<scalar_t, float>, int> = 0>
inline void sigmoid(float* __restrict__ out, const scalar_t* __restrict__ input) {
using bVec = at::vec::Vectorized<scalar_t>;
using fVec = at::vec::Vectorized<float>;
const fVec one = fVec(1.f);
constexpr int kVecSize = bVec::size();
for (int d = 0; d < SIZE; d += kVecSize) {
bVec x_bvec = bVec::loadu(input + d);
fVec x_fvec0, x_fvec1;
std::tie(x_fvec0, x_fvec1) = at::vec::convert_to_float(x_bvec);
x_fvec0 = one / (one + x_fvec0.neg().exp_u20());
x_fvec1 = one / (one + x_fvec1.neg().exp_u20());
x_fvec0.store(out + d);
x_fvec1.store(out + d + fVec::size());
}
}
template <typename scalar_t, int SIZE, std::enable_if_t<std::is_same_v<scalar_t, float>, int> = 0>
inline void sigmoid(float* __restrict__ out, const float* __restrict__ input) {
using fVec = at::vec::Vectorized<float>;
const fVec one = fVec(1.f);
constexpr int kVecSize = fVec::size();
for (int d = 0; d < SIZE; d += kVecSize) {
fVec in_fvec = fVec::loadu(input + d);
in_fvec = one / (one + in_fvec.neg().exp_u20());
in_fvec.store(out + d);
}
}
template <typename scalar_t, int SIZE, std::enable_if_t<!std::is_same_v<scalar_t, float>, int> = 0>
inline void softmax(float* __restrict__ out, const scalar_t* __restrict__ input) {
using bVec = at::vec::Vectorized<scalar_t>;
using fVec = at::vec::Vectorized<float>;
constexpr int kVecSize = bVec::size();
// step 1: get max
fVec max_fvec = fVec(-std::numeric_limits<float>::infinity());
for (int d = 0; d < SIZE; d += kVecSize) {
bVec x_bvec = bVec::loadu(input + d);
fVec x_fvec0, x_fvec1;
std::tie(x_fvec0, x_fvec1) = at::vec::convert_to_float(x_bvec);
max_fvec = at::vec::maximum(max_fvec, x_fvec0);
max_fvec = at::vec::maximum(max_fvec, x_fvec1);
x_fvec0.store(out + d);
x_fvec1.store(out + d + fVec::size());
}
float max_val = vec_reduce_max(max_fvec);
max_fvec = fVec(max_val);
// step 2: sum of (x - max).exp()
fVec sum_fvec = fVec(float(0));
for (int d = 0; d < SIZE; d += fVec::size()) {
fVec x_fvec = (fVec::loadu(out + d) - max_fvec).exp_u20();
sum_fvec += x_fvec;
x_fvec.store(out + d);
}
float sum_val = vec_reduce_sum(sum_fvec);
// step 3: x * (1 / sum)
sum_fvec = fVec(1.f / sum_val);
for (int d = 0; d < SIZE; d += fVec::size()) {
fVec out_fvec = fVec::loadu(out + d) * sum_fvec;
out_fvec.store(out + d);
}
}
template <typename scalar_t, int SIZE, std::enable_if_t<std::is_same_v<scalar_t, float>, int> = 0>
inline void softmax(float* __restrict__ out, const float* __restrict__ input) {
using fVec = at::vec::Vectorized<float>;
constexpr int kVecSize = fVec::size();
// step 1: get max
fVec max_fvec = fVec(-std::numeric_limits<float>::infinity());
for (int d = 0; d < SIZE; d += kVecSize) {
fVec x_fvec = fVec::loadu(input + d);
max_fvec = at::vec::maximum(max_fvec, x_fvec);
x_fvec.store(out + d);
}
float max_val = vec_reduce_max(max_fvec);
max_fvec = fVec(max_val);
// step 2: sum of (x - max).exp()
fVec sum_fvec = fVec(float(0));
for (int d = 0; d < SIZE; d += kVecSize) {
fVec x_fvec = (fVec::loadu(out + d) - max_fvec).exp_u20();
sum_fvec += x_fvec;
x_fvec.store(out + d);
}
float sum_val = vec_reduce_sum(sum_fvec);
// step 3: x * (1 / sum)
sum_fvec = fVec(1.f / sum_val);
for (int d = 0; d < SIZE; d += kVecSize) {
fVec out_fvec = fVec::loadu(out + d) * sum_fvec;
out_fvec.store(out + d);
}
}
template <typename param_t, int SIZE>
inline void
apply_bias(float* __restrict__ scores2, const float* __restrict__ scores, const param_t* __restrict__ bias) {
using fVec = at::vec::Vectorized<float>;
auto vec_size = fVec::size() * 2;
int d = 0;
for (; d <= SIZE - vec_size; d += vec_size) {
fVec bias0, bias1, x0, x1;
std::tie(bias0, bias1) = load_float_vec2(bias + d);
std::tie(x0, x1) = load_float_vec2(scores + d);
x0 = x0 + bias0;
x1 = x1 + bias1;
x0.store(scores2 + d);
x1.store(scores2 + d + fVec::size());
}
for (; d < SIZE; d++) {
scores2[d] = scores[d] + (float)bias[d];
}
}
// sqrtsoftplus: sqrt(softplus(x)) = sqrt(log(1 + exp(x)))
// For numerical stability: when x > threshold, softplus(x) ≈ x
// When x < -threshold, softplus(x) ≈ exp(x), so sqrt(softplus(x)) ≈ sqrt(exp(x)) = exp(x/2)
template <typename scalar_t, int SIZE, std::enable_if_t<!std::is_same_v<scalar_t, float>, int> = 0>
inline void sqrtsoftplus(float* __restrict__ out, const scalar_t* __restrict__ input) {
using bVec = at::vec::Vectorized<scalar_t>;
using fVec = at::vec::Vectorized<float>;
const fVec one = fVec(1.f);
const fVec half = fVec(0.5f);
const fVec threshold = fVec(20.f);
const fVec neg_threshold = fVec(-15.f); // below this, 1+exp(x) loses precision in float32
constexpr int kVecSize = bVec::size();
for (int d = 0; d < SIZE; d += kVecSize) {
bVec x_bvec = bVec::loadu(input + d);
fVec x_fvec0, x_fvec1;
std::tie(x_fvec0, x_fvec1) = at::vec::convert_to_float(x_bvec);
// default: softplus(x) = log(1 + exp(x))
fVec sp0 = (one + x_fvec0.exp_u20()).log();
fVec sp1 = (one + x_fvec1.exp_u20()).log();
// x > 20: softplus(x) ≈ x
sp0 = fVec::blendv(sp0, x_fvec0, x_fvec0 > threshold);
sp1 = fVec::blendv(sp1, x_fvec1, x_fvec1 > threshold);
// x < -15: softplus(x) ≈ exp(x), sqrt(exp(x)) = exp(x/2)
fVec exp_half0 = (x_fvec0 * half).exp_u20();
fVec exp_half1 = (x_fvec1 * half).exp_u20();
sp0 = fVec::blendv(sp0.sqrt(), exp_half0, x_fvec0 < neg_threshold);
sp1 = fVec::blendv(sp1.sqrt(), exp_half1, x_fvec1 < neg_threshold);
sp0.store(out + d);
sp1.store(out + d + fVec::size());
}
}
template <typename scalar_t, int SIZE, std::enable_if_t<std::is_same_v<scalar_t, float>, int> = 0>
inline void sqrtsoftplus(float* __restrict__ out, const float* __restrict__ input) {
using fVec = at::vec::Vectorized<float>;
const fVec one = fVec(1.f);
const fVec half = fVec(0.5f);
const fVec threshold = fVec(20.f);
const fVec neg_threshold = fVec(-15.f);
constexpr int kVecSize = fVec::size();
for (int d = 0; d < SIZE; d += kVecSize) {
fVec x = fVec::loadu(input + d);
fVec sp = (one + x.exp_u20()).log();
sp = fVec::blendv(sp, x, x > threshold);
fVec exp_half = (x * half).exp_u20();
sp = fVec::blendv(sp.sqrt(), exp_half, x < neg_threshold);
sp.store(out + d);
}
}
// biased_topk: flat (non-grouped) biased topk for DeepSeek V4
// scoring_func: 0 = sigmoid, 1 = sqrtsoftplus
template <typename scalar_t, typename param_t, int NUM_EXPERTS, int TOPK>
void biased_topk_kernel_impl(
float* __restrict__ topk_weights,
int32_t* __restrict__ topk_ids,
const scalar_t* __restrict__ gating_output,
const param_t* __restrict__ bias,
int64_t num_tokens,
int64_t topk,
bool renormalize,
int scoring_func,
int64_t num_fused_shared_experts,
float routed_scaling_factor,
bool apply_routed_scaling_factor_on_output) {
at::parallel_for(0, num_tokens, 0, [&](int64_t begin, int64_t end) {
alignas(64) float scores[NUM_EXPERTS];
alignas(64) float scores_biased[NUM_EXPERTS];
using elem_t = std::pair<float, int32_t>;
std::array<elem_t, NUM_EXPERTS> queue;
// simple RNG for fused shared expert random ID
uint64_t rng_state = begin * 6364136223846793005ULL + 1442695040888963407ULL;
for (int64_t i = begin; i < end; ++i) {
// compute scores
if (scoring_func == 0) {
sigmoid<scalar_t, NUM_EXPERTS>(scores, gating_output + i * NUM_EXPERTS);
} else {
sqrtsoftplus<scalar_t, NUM_EXPERTS>(scores, gating_output + i * NUM_EXPERTS);
}
// add bias for selection
apply_bias<param_t, NUM_EXPERTS>(scores_biased, scores, bias);
// build queue and partial sort to find top-k
for (int64_t e = 0; e < NUM_EXPERTS; ++e) {
queue[e] = {scores_biased[e], static_cast<int32_t>(e)};
}
std::partial_sort(
queue.begin(), queue.begin() + topk, queue.end(), [](const elem_t& x, const elem_t& y) -> bool {
return x.first > y.first;
});
// gather original scores (without bias) as weights
for (int64_t j = 0; j < topk; ++j) {
int32_t idx = queue[j].second;
topk_ids[i * topk + j] = idx;
topk_weights[i * topk + j] = scores[idx];
}
// handle fused shared experts
if (num_fused_shared_experts > 0) {
// replace last slot with random shared expert ID
rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
int32_t shared_id =
NUM_EXPERTS + static_cast<int32_t>((rng_state >> 33) % static_cast<uint64_t>(num_fused_shared_experts));
topk_ids[i * topk + topk - 1] = shared_id;
// shared expert weight = sum of routed weights / scaling_factor
if (routed_scaling_factor != 0.0f) {
float routed_sum = 0.f;
for (int64_t j = 0; j < topk - 1; ++j) {
routed_sum += topk_weights[i * topk + j];
}
topk_weights[i * topk + topk - 1] = routed_sum / routed_scaling_factor;
}
}
// renormalize
if (renormalize) {
float sum = 0.f;
int64_t norm_end = (num_fused_shared_experts == 0) ? topk : topk - 1;
for (int64_t j = 0; j < norm_end; ++j) {
sum += topk_weights[i * topk + j];
}
float scale = 1.f / sum;
if (apply_routed_scaling_factor_on_output) {
scale *= routed_scaling_factor;
}
for (int64_t j = 0; j < norm_end; ++j) {
topk_weights[i * topk + j] *= scale;
}
// also scale the shared expert weight if present
if (num_fused_shared_experts > 0) {
topk_weights[i * topk + topk - 1] *= scale;
}
}
}
});
}
// hash_topk: expert IDs come from a precomputed lookup table tid2eid[input_ids]
// scoring_func: 0 = softmax, 1 = sigmoid, 2 = sqrtsoftplus
template <typename scalar_t, int NUM_EXPERTS, int TOPK>
void hash_topk_kernel_impl(
float* __restrict__ topk_weights,
int32_t* __restrict__ topk_ids,
const scalar_t* __restrict__ gating_output,
const int32_t* __restrict__ tid2eid, // [num_tokens, routed_topk]
int64_t num_tokens,
int scoring_func,
int64_t num_fused_shared_experts,
int64_t num_experts,
float routed_scaling_factor,
int64_t topk) {
const int64_t routed_topk = topk - num_fused_shared_experts;
const bool need_renormalize = (scoring_func != 0); // renormalize for non-softmax
at::parallel_for(0, num_tokens, 0, [&](int64_t begin, int64_t end) {
alignas(64) float scores[NUM_EXPERTS];
// simple RNG for fused shared expert random ID
uint64_t rng_state = begin * 6364136223846793005ULL + 1442695040888963407ULL;
for (int64_t i = begin; i < end; ++i) {
// compute scores over all experts
if (scoring_func == 0) {
softmax<scalar_t, NUM_EXPERTS>(scores, gating_output + i * NUM_EXPERTS);
} else if (scoring_func == 1) {
sigmoid<scalar_t, NUM_EXPERTS>(scores, gating_output + i * NUM_EXPERTS);
} else {
sqrtsoftplus<scalar_t, NUM_EXPERTS>(scores, gating_output + i * NUM_EXPERTS);
}
// gather expert IDs from lookup table
const int32_t* eid_row = tid2eid + i * routed_topk;
for (int64_t j = 0; j < routed_topk; ++j) {
int32_t eid = eid_row[j];
topk_ids[i * topk + j] = eid;
topk_weights[i * topk + j] = scores[eid];
}
// renormalize routed weights (for non-softmax scoring). Unlike the
// upstream kernel, always fold routed_scaling_factor in here rather
// than only applying it to the fused-shared-expert slot below: vLLM's
// DeepSeek-V4 CPU hash-routed layers never fuse a shared expert into
// this routing table (num_fused_shared_experts is always 0 on this
// call path), so this is the only place the scaling factor can be
// applied. Matches the eager reference (`sqrtsoftplus_bias_topk` in
// cpu_moe.py) and the CUDA kernel (`dsv4HashTopkSoftplusSqrt`).
if (need_renormalize) {
float sum = 0.f;
for (int64_t j = 0; j < routed_topk; ++j) {
sum += topk_weights[i * topk + j];
}
if (sum > 0.f) {
float scale = 1.f / sum;
if (num_fused_shared_experts == 0) {
scale *= routed_scaling_factor;
}
for (int64_t j = 0; j < routed_topk; ++j) {
topk_weights[i * topk + j] *= scale;
}
}
}
// handle fused shared expert
if (num_fused_shared_experts > 0) {
// random shared expert ID in [num_experts, num_experts + num_fused_shared_experts)
rng_state = rng_state * 6364136223846793005ULL + 1442695040888963407ULL;
int32_t shared_id =
num_experts + static_cast<int32_t>((rng_state >> 33) % static_cast<uint64_t>(num_fused_shared_experts));
topk_ids[i * topk + topk - 1] = shared_id;
// shared expert weight = sum of routed weights / scaling_factor
float routed_sum = 0.f;
for (int64_t j = 0; j < routed_topk; ++j) {
routed_sum += topk_weights[i * topk + j];
}
topk_weights[i * topk + topk - 1] = routed_sum / routed_scaling_factor;
}
}
});
}
#define LAUNCH_HASH_TOPK_KERNEL(NE, NTOPK) \
hash_topk_kernel_impl<scalar_t, NE, NTOPK>( \
topk_weights.data_ptr<float>(), \
topk_ids.data_ptr<int32_t>(), \
gating_output.data_ptr<scalar_t>(), \
tid2eid_flat.data_ptr<int32_t>(), \
num_tokens, \
scoring_func_id, \
num_fused_shared_experts, \
num_experts, \
routed_scaling_factor_value, \
topk);
#define LAUNCH_BIASED_TOPK_KERNEL(NE, NTOPK) \
biased_topk_kernel_impl<scalar_t, param_t, NE, NTOPK>( \
topk_weights.data_ptr<float>(), \
topk_ids.data_ptr<int32_t>(), \
gating_output.data_ptr<scalar_t>(), \
correction_bias.data_ptr<param_t>(), \
num_tokens, \
topk, \
renormalize, \
scoring_func_id, \
num_fused_shared_experts, \
routed_scaling_factor_value, \
apply_routed_scaling_factor_on_output);
} // namespace
// biased topk for DeepSeek V4 (flat, non-grouped)
std::tuple<at::Tensor, at::Tensor> biased_topk_cpu(
at::Tensor& hidden_states,
at::Tensor& gating_output,
at::Tensor& correction_bias,
int64_t topk,
bool renormalize,
std::string scoring_func,
int64_t num_fused_shared_experts,
std::optional<double> routed_scaling_factor,
bool apply_routed_scaling_factor_on_output) {
CHECK_INPUT(gating_output);
CHECK_INPUT(correction_bias);
const auto st = gating_output.scalar_type();
int64_t num_tokens = hidden_states.size(0);
int64_t num_experts = gating_output.size(1);
TORCH_CHECK(gating_output.size(0) == num_tokens, "Number of tokens mismatch");
TORCH_CHECK(correction_bias.numel() == num_experts, "Bias shape mismatch");
int scoring_func_id = 0;
if (scoring_func == "sigmoid") {
scoring_func_id = 0;
} else if (scoring_func == "sqrtsoftplus") {
scoring_func_id = 1;
} else {
TORCH_CHECK(false, "Unsupported scoring_func: ", scoring_func);
}
float routed_scaling_factor_value = routed_scaling_factor.has_value() ? routed_scaling_factor.value() : 0.0f;
at::Tensor topk_weights = at::empty({num_tokens, topk}, hidden_states.options().dtype(at::kFloat));
at::Tensor topk_ids = at::empty({num_tokens, topk}, hidden_states.options().dtype(at::kInt));
// The actual routed topk (excluding fused shared experts)
int64_t routed_topk = topk - num_fused_shared_experts;
CPU_DISPATCH_FLOATING_TYPES_EXT(st, correction_bias.scalar_type(), "biased_topk_kernel", [&] {
// dispatch on num_experts and routed_topk. For DeepSeek V4: 256 experts,
// routed_topk=6 (num_fused_shared_experts is always 0 on this CPU call
// path -- see cpu_moe.py). Anything outside this table isn't a real
// model shape, so fail loudly instead of silently degrading to a slow
// generic path.
switch (num_experts) {
case 64:
switch (routed_topk) {
case 6:
LAUNCH_BIASED_TOPK_KERNEL(64, 6);
break;
case 8:
LAUNCH_BIASED_TOPK_KERNEL(64, 8);
break;
default:
TORCH_CHECK(false, "biased_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=64");
}
break;
case 128:
switch (routed_topk) {
case 6:
LAUNCH_BIASED_TOPK_KERNEL(128, 6);
break;
case 8:
LAUNCH_BIASED_TOPK_KERNEL(128, 8);
break;
default:
TORCH_CHECK(false, "biased_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=128");
}
break;
case 256:
switch (routed_topk) {
case 6:
LAUNCH_BIASED_TOPK_KERNEL(256, 6);
break;
case 8:
LAUNCH_BIASED_TOPK_KERNEL(256, 8);
break;
case 9:
LAUNCH_BIASED_TOPK_KERNEL(256, 9);
break;
default:
TORCH_CHECK(false, "biased_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=256");
}
break;
case 384:
switch (routed_topk) {
case 6:
LAUNCH_BIASED_TOPK_KERNEL(384, 6);
break;
case 8:
LAUNCH_BIASED_TOPK_KERNEL(384, 8);
break;
default:
TORCH_CHECK(false, "biased_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=384");
}
break;
default:
TORCH_CHECK(false, "biased_topk_cpu: unsupported num_experts ", num_experts);
}
});
return std::make_tuple(topk_weights, topk_ids);
}
// hash topk for DeepSeek V4 (expert IDs from precomputed lookup table)
std::tuple<at::Tensor, at::Tensor> hash_topk_cpu(
at::Tensor& gating_output,
at::Tensor& tid2eid,
int64_t topk,
std::string scoring_func,
int64_t num_fused_shared_experts,
int64_t num_experts,
double routed_scaling_factor) {
CHECK_INPUT(gating_output);
CHECK_INPUT(tid2eid);
const auto st = gating_output.scalar_type();
int64_t num_tokens = gating_output.size(0);
int64_t num_experts_gating = gating_output.size(1);
int64_t routed_topk = topk - num_fused_shared_experts;
TORCH_CHECK(tid2eid.size(0) == num_tokens, "tid2eid row count must match num_tokens");
TORCH_CHECK(tid2eid.size(1) == routed_topk, "tid2eid column count must match routed_topk");
TORCH_CHECK(tid2eid.scalar_type() == at::kInt, "tid2eid must be int32");
TORCH_CHECK(num_experts_gating == num_experts, "num_experts mismatch");
int scoring_func_id = 0;
if (scoring_func == "softmax") {
scoring_func_id = 0;
} else if (scoring_func == "sigmoid") {
scoring_func_id = 1;
} else if (scoring_func == "sqrtsoftplus") {
scoring_func_id = 2;
} else {
TORCH_CHECK(false, "Unsupported scoring_func: ", scoring_func);
}
float routed_scaling_factor_value = static_cast<float>(routed_scaling_factor);
at::Tensor topk_weights = at::empty({num_tokens, topk}, gating_output.options().dtype(at::kFloat));
at::Tensor topk_ids = at::empty({num_tokens, topk}, gating_output.options().dtype(at::kInt));
// tid2eid is [num_tokens, routed_topk], already indexed by input_ids in
// Python; CHECK_INPUT above already guarantees it is contiguous.
const at::Tensor& tid2eid_flat = tid2eid;
// Dispatch for bf16, fp16, and float32
// Note: cannot use AT_DISPATCH_FLOATING_TYPES_AND2 since it includes double which lacks convert_to_float
auto dispatch_fn = [&]<typename scalar_t>() {
// For DeepSeek V4: 256 experts, routed_topk in {6,7,8} depending on
// layer (num_fused_shared_experts is always 0 on this CPU call path --
// see cpu_moe.py). Anything outside this table isn't a real model
// shape, so fail loudly instead of silently degrading to a slow
// generic path.
switch (num_experts) {
case 64:
switch (routed_topk) {
case 6:
LAUNCH_HASH_TOPK_KERNEL(64, 6);
break;
case 7:
LAUNCH_HASH_TOPK_KERNEL(64, 7);
break;
case 8:
LAUNCH_HASH_TOPK_KERNEL(64, 8);
break;
default:
TORCH_CHECK(false, "hash_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=64");
}
break;
case 128:
switch (routed_topk) {
case 6:
LAUNCH_HASH_TOPK_KERNEL(128, 6);
break;
case 7:
LAUNCH_HASH_TOPK_KERNEL(128, 7);
break;
case 8:
LAUNCH_HASH_TOPK_KERNEL(128, 8);
break;
default:
TORCH_CHECK(false, "hash_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=128");
}
break;
case 256:
switch (routed_topk) {
case 6:
LAUNCH_HASH_TOPK_KERNEL(256, 6);
break;
case 7:
LAUNCH_HASH_TOPK_KERNEL(256, 7);
break;
case 8:
LAUNCH_HASH_TOPK_KERNEL(256, 8);
break;
default:
TORCH_CHECK(false, "hash_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=256");
}
break;
case 384:
switch (routed_topk) {
case 6:
LAUNCH_HASH_TOPK_KERNEL(384, 6);
break;
case 7:
LAUNCH_HASH_TOPK_KERNEL(384, 7);
break;
case 8:
LAUNCH_HASH_TOPK_KERNEL(384, 8);
break;
default:
TORCH_CHECK(false, "hash_topk_cpu: unsupported routed_topk ", routed_topk,
" for num_experts=384");
}
break;
default:
TORCH_CHECK(false, "hash_topk_cpu: unsupported num_experts ", num_experts);
}
};
if (st == at::ScalarType::BFloat16) {
dispatch_fn.template operator()<at::BFloat16>();
} else if (st == at::ScalarType::Half) {
dispatch_fn.template operator()<at::Half>();
} else if (st == at::ScalarType::Float) {
dispatch_fn.template operator()<float>();
} else {
TORCH_CHECK(false, "Unsupported dtype for hash_topk_cpu: ", st);
}
return std::make_tuple(topk_weights, topk_ids);
}
// Adapted from
// https://github.com/sgl-project/sglang/tree/main/sgl-kernel/csrc/cpu:
// `topk_transform_512_cpu`/`topk_transform_512_cpu_kernel_impl`. Near-
// verbatim port for the sparse indexer's DECODE path: hardcodes top-512
// (the only `index_topk` DeepSeek-V4-Flash uses) and streams a min-heap over
// the tail of the row instead of sorting the whole row.
//
// Differences from upstream:
// - `out_page_indices` is still computed (required by the kernel contract)
// but vLLM's caller only consumes `out_raw_indices` (local/compressed-
// context positions -- `DeepseekV4CPUAttention.forward_mqa` resolves those
// to physical slots itself via `map_local_to_global_slots_cpu`).
// - `page_size` was already a runtime argument upstream, unlike
// `fp8_paged_mqa_logits_cpu` (paged_mqa_logits.cpp), which needed one.
namespace {
constexpr int64_t kC4Topk = 512;
template <typename T>
inline int64_t load_index_value(const T* __restrict__ ptr, int64_t idx) {
return static_cast<int64_t>(ptr[idx]);
}
struct TopKTransformElem {
float score;
int32_t index;
};
struct TopKTransformMinHeapCmp {
bool operator()(const TopKTransformElem& lhs, const TopKTransformElem& rhs) const {
if (lhs.score == rhs.score) {
return lhs.index > rhs.index;
}
return lhs.score > rhs.score;
}
};
template <typename seq_t, typename page_t>
void topk_transform_512_cpu_kernel_impl(
const float* __restrict__ scores,
const seq_t* __restrict__ seq_lens,
const page_t* __restrict__ page_tables,
int32_t* __restrict__ out_page_indices,
int32_t* __restrict__ out_raw_indices,
int64_t batch_size,
int64_t max_seq_len,
int64_t page_table_stride,
int64_t out_stride,
int64_t page_size) {
TORCH_CHECK(page_size > 0, "page_size must be positive");
TORCH_CHECK((page_size & (page_size - 1)) == 0, "page_size must be a power of 2");
const int page_bits = page_size > 1 ? static_cast<int>(std::log2(static_cast<double>(page_size))) : 0;
const int64_t page_mask = page_size - 1;
at::parallel_for(0, batch_size, 0, [&](int64_t begin, int64_t end) {
std::array<TopKTransformElem, kC4Topk> heap;
for (int64_t b = begin; b < end; ++b) {
const float* __restrict__ scores_row = scores + b * max_seq_len;
const page_t* __restrict__ page_table_row = page_tables + b * page_table_stride;
int32_t* __restrict__ out_page_row = out_page_indices + b * out_stride;
int32_t* __restrict__ out_raw_row = out_raw_indices == nullptr ? nullptr : out_raw_indices + b * out_stride;
int64_t seq_len = load_index_value(seq_lens, b);
seq_len = std::max<int64_t>(0, std::min<int64_t>(seq_len, max_seq_len));
const int64_t valid_topk = std::min<int64_t>(seq_len, kC4Topk);
auto store_slot = [&](int64_t slot, int32_t raw_index) {
if (raw_index < 0) {
out_page_row[slot] = -1;
if (out_raw_row != nullptr) {
out_raw_row[slot] = -1;
}
return;
}
const int64_t page_idx = static_cast<int64_t>(raw_index) >> page_bits;
const int64_t offset_in_page = static_cast<int64_t>(raw_index) & page_mask;
const int64_t physical_page = load_index_value(page_table_row, page_idx);
out_page_row[slot] = static_cast<int32_t>((physical_page << page_bits) | offset_in_page);
if (out_raw_row != nullptr) {
out_raw_row[slot] = raw_index;
}
};
if (seq_len <= kC4Topk) {
for (int64_t i = 0; i < valid_topk; ++i) {
store_slot(i, static_cast<int32_t>(i));
}
for (int64_t i = valid_topk; i < kC4Topk; ++i) {
store_slot(i, -1);
}
continue;
}
for (int64_t i = 0; i < kC4Topk; ++i) {
heap[i] = {scores_row[i], static_cast<int32_t>(i)};
}
std::make_heap(heap.begin(), heap.end(), TopKTransformMinHeapCmp());
for (int64_t i = kC4Topk; i < seq_len; ++i) {
const float score = scores_row[i];
const TopKTransformElem& current_min = heap.front();
if (score > current_min.score ||
(score == current_min.score && static_cast<int32_t>(i) < current_min.index)) {
std::pop_heap(heap.begin(), heap.end(), TopKTransformMinHeapCmp());
heap.back() = {score, static_cast<int32_t>(i)};
std::push_heap(heap.begin(), heap.end(), TopKTransformMinHeapCmp());
}
}
for (int64_t i = 0; i < kC4Topk; ++i) {
store_slot(i, heap[i].index);
}
}
});
}
} // namespace
void topk_transform_512_cpu(
at::Tensor& scores,
at::Tensor& seq_lens,
at::Tensor& page_tables,
at::Tensor& out_page_indices,
int64_t page_size,
const std::optional<at::Tensor>& out_raw_indices) {
CHECK_INPUT(scores);
CHECK_INPUT(seq_lens);
CHECK_INPUT(page_tables);
CHECK_INPUT(out_page_indices);
TORCH_CHECK(scores.scalar_type() == at::kFloat, "scores must be float32");
TORCH_CHECK(out_page_indices.scalar_type() == at::kInt, "out_page_indices must be int32");
TORCH_CHECK(scores.dim() == 2, "scores must be a 2D tensor");
TORCH_CHECK(seq_lens.dim() == 1, "seq_lens must be a 1D tensor");
TORCH_CHECK(page_tables.dim() == 2, "page_tables must be a 2D tensor");
TORCH_CHECK(out_page_indices.dim() == 2, "out_page_indices must be a 2D tensor");
const int64_t batch_size = scores.size(0);
const int64_t max_seq_len = scores.size(1);
TORCH_CHECK(seq_lens.size(0) == batch_size, "seq_lens row count must match scores");
TORCH_CHECK(page_tables.size(0) == batch_size, "page_tables row count must match scores");
TORCH_CHECK(out_page_indices.size(0) == batch_size, "out_page_indices row count must match scores");
TORCH_CHECK(out_page_indices.size(1) >= kC4Topk, "out_page_indices must have at least 512 columns");
int32_t* raw_ptr = nullptr;
if (out_raw_indices.has_value()) {
at::Tensor raw = out_raw_indices.value();
CHECK_INPUT(raw);
TORCH_CHECK(raw.scalar_type() == at::kInt, "out_raw_indices must be int32");
TORCH_CHECK(raw.dim() == 2, "out_raw_indices must be a 2D tensor");
TORCH_CHECK(raw.sizes() == out_page_indices.sizes(), "out_raw_indices shape must match out_page_indices");
raw_ptr = raw.data_ptr<int32_t>();
}
if (batch_size == 0) {
return;
}
auto launch_with_seq_type = [&](auto seq_zero) {
using seq_t = decltype(seq_zero);
if (page_tables.scalar_type() == at::kInt) {
topk_transform_512_cpu_kernel_impl<seq_t, int32_t>(
scores.data_ptr<float>(), seq_lens.data_ptr<seq_t>(), page_tables.data_ptr<int32_t>(),
out_page_indices.data_ptr<int32_t>(), raw_ptr, batch_size, max_seq_len, page_tables.stride(0),
out_page_indices.stride(0), page_size);
} else if (page_tables.scalar_type() == at::kLong) {
topk_transform_512_cpu_kernel_impl<seq_t, int64_t>(
scores.data_ptr<float>(), seq_lens.data_ptr<seq_t>(), page_tables.data_ptr<int64_t>(),
out_page_indices.data_ptr<int32_t>(), raw_ptr, batch_size, max_seq_len, page_tables.stride(0),
out_page_indices.stride(0), page_size);
} else {
TORCH_CHECK(false, "page_tables must be int32 or int64");
}
};
if (seq_lens.scalar_type() == at::kInt) {
launch_with_seq_type(int32_t{0});
} else if (seq_lens.scalar_type() == at::kLong) {
launch_with_seq_type(int64_t{0});
} else {
TORCH_CHECK(false, "seq_lens must be int32 or int64");
}
}
+213 -16
View File
@@ -66,18 +66,18 @@ at::Tensor convert_weight_packed(at::Tensor& weight);
at::Tensor convert_scale_packed(at::Tensor& scale);
at::Tensor fused_experts_cpu(
at::Tensor& hidden_states, at::Tensor& w1, at::Tensor& w2,
at::Tensor& topk_weights, at::Tensor& topk_ids, bool inplace,
int64_t moe_comp_method, const std::optional<at::Tensor>& w1_scale,
const std::optional<at::Tensor>& w2_scale,
const std::optional<at::Tensor>& w1_zero,
const std::optional<at::Tensor>& w2_zero,
const std::optional<std::vector<int64_t>> block_size,
const std::optional<at::Tensor>& w1_bias,
const std::optional<at::Tensor>& w2_bias,
const std::optional<double>& alpha, const std::optional<double>& limit,
bool is_vnni);
void fused_experts_cpu(at::Tensor& out, at::Tensor& hidden_states,
at::Tensor& w1, at::Tensor& w2, at::Tensor& topk_weights,
at::Tensor& topk_ids, int64_t moe_comp_method,
const std::optional<at::Tensor>& w1_scale,
const std::optional<at::Tensor>& w2_scale,
const std::optional<at::Tensor>& w1_zero,
const std::optional<at::Tensor>& w2_zero,
const std::optional<std::vector<int64_t>> block_size,
const std::optional<at::Tensor>& w1_bias,
const std::optional<at::Tensor>& w2_bias,
const std::optional<double>& alpha,
const std::optional<double>& limit, bool is_vnni);
at::Tensor int8_scaled_mm_with_quant(at::Tensor& mat1, at::Tensor& mat2,
at::Tensor& scales2,
@@ -124,6 +124,99 @@ void concat_and_cache_mla_cpu(const at::Tensor& kv_c_normed,
const at::Tensor& k_pe, at::Tensor& kv_cache,
const at::Tensor& slot_mapping);
// Adapted from sglang: DeepSeek-V4 mHC gating kernels
std::tuple<at::Tensor, at::Tensor, at::Tensor> hc_pre_fused_cpu(
at::Tensor& x, at::Tensor& hc_fn, at::Tensor& hc_scale, at::Tensor& hc_base,
int64_t hc_mult, int64_t sinkhorn_iters, double rms_eps, double hc_eps);
at::Tensor hc_post_fused_cpu(at::Tensor& x, at::Tensor& residual,
at::Tensor& post, at::Tensor& comb);
at::Tensor hc_head_fused_cpu(at::Tensor& x, at::Tensor& hc_fn,
at::Tensor& hc_scale, at::Tensor& hc_base,
double hc_eps, double norm_eps);
// Adapted from sglang: DeepSeek-V4 fp8_ds_mla cache-write kernel
at::Tensor fused_qnorm_rope_kv_insert_cpu(at::Tensor& q, at::Tensor& kv,
at::Tensor& positions,
at::Tensor& swa_kv_cache_2d,
at::Tensor& slot_mapping,
at::Tensor& cos_sin_cache,
int64_t q_head_padded, double eps,
int64_t cache_block_size);
// vLLM-native: DeepSeek-V4 sparse MQA attention kernel for the fp8_ds_mla
// cache layout (SWA window + optional compressed top-k index sets).
void flash_mla_with_kvcache_cpu(
at::Tensor& out, at::Tensor& q, at::Tensor& window_cache_2d,
at::Tensor& window_slots, int64_t window_block_size,
at::Tensor& compressed_cache_2d, at::Tensor& compressed_slots,
int64_t compressed_block_size, at::Tensor& attn_sink, double scale);
// Adapted from sglang: DeepSeek-V4 compressor state-cache write + fused
// compress+RMSNorm+quant+RoPE+store kernels.
void save_partial_states_cpu(at::Tensor& kv, at::Tensor& score, at::Tensor& ape,
at::Tensor& positions, at::Tensor& state_cache,
at::Tensor& slot_mapping);
void compress_norm_rope_store_cpu(
at::Tensor& state_cache, at::Tensor& gather_slots, at::Tensor& positions,
at::Tensor& kv_slot_mapping, at::Tensor& rms_norm_weight,
double rms_norm_eps, at::Tensor& cos_sin_cache, at::Tensor& kv_cache_2d,
int64_t kv_cache_block_size, int64_t compress_ratio);
// head_dim=128 indexer-compressor variant of the above (single fp8 quant
// block + raw fp32 scale -- matches the paged indexer K-cache layout
// fp8_paged_mqa_logits_cpu/topk_transform_512_cpu read).
void compress_norm_rope_store_indexer_cpu(
at::Tensor& state_cache, at::Tensor& gather_slots, at::Tensor& positions,
at::Tensor& kv_slot_mapping, at::Tensor& rms_norm_weight,
double rms_norm_eps, at::Tensor& cos_sin_cache, at::Tensor& kv_cache_2d,
int64_t kv_cache_block_size, int64_t compress_ratio);
// Adapted from sglang: DeepSeek-V4 sparse indexer paged MQA-logits +
// top-512 transform kernels (DECODE path -- reads the paged K-cache
// directly via `page_table`, whole batch in one call).
at::Tensor fp8_paged_mqa_logits_cpu(at::Tensor& q_fp8, at::Tensor& kvcache_fp8,
at::Tensor& weight, at::Tensor& seq_lens,
at::Tensor& page_table, int64_t block_size,
int64_t max_seq_len);
void topk_transform_512_cpu(at::Tensor& scores, at::Tensor& seq_lens,
at::Tensor& page_tables,
at::Tensor& out_page_indices, int64_t page_size,
const std::optional<at::Tensor>& out_raw_indices);
// Adapted from sglang: DeepSeek-V4 MoE routing kernels (flat biased top-k and
// hash-routed-layer top-k).
std::tuple<at::Tensor, at::Tensor> biased_topk_cpu(
at::Tensor& hidden_states, at::Tensor& gating_output,
at::Tensor& correction_bias, int64_t topk, bool renormalize,
std::string scoring_func, int64_t num_fused_shared_experts,
std::optional<double> routed_scaling_factor,
bool apply_routed_scaling_factor_on_output);
std::tuple<at::Tensor, at::Tensor> hash_topk_cpu(
at::Tensor& gating_output, at::Tensor& tid2eid, int64_t topk,
std::string scoring_func, int64_t num_fused_shared_experts,
int64_t num_experts, double routed_scaling_factor);
// vLLM-native: DeepSeek-V4 sparse indexer Q-side RoPE + FP8 quant kernel
// (fp8 path only; MXFP4 stays on triton-cpu).
void fused_indexer_q_rope_quant_cpu(at::Tensor& positions, at::Tensor& index_q,
at::Tensor& index_q_cos_sin_cache,
at::Tensor& index_q_fp8,
at::Tensor& index_weights,
double index_weights_softmax_scale,
double index_weights_head_scale,
at::Tensor& index_weights_out);
// vLLM-native: inverse GPT-J RoPE for DeepSeek-V4 CPU attention's `_o_proj`
// output de-rotation.
at::Tensor inverse_gptj_rope_o_proj_cpu(at::Tensor& o, at::Tensor& positions,
at::Tensor& cos_sin_cache,
int64_t rope_dim);
// Adapted from sglang: INT4 W4A8 kernels
std::tuple<at::Tensor, at::Tensor, at::Tensor> convert_weight_packed_scale_zp(
at::Tensor qweight, // awq: (*, K, N / 8) || gptq: (*, K / 8, N) , int32
@@ -515,13 +608,13 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
ops.def("convert_scale_packed(Tensor! scale) -> Tensor");
ops.impl("convert_scale_packed", torch::kCPU, &convert_scale_packed);
ops.def(
"fused_experts_cpu(Tensor hidden_states, Tensor w1, Tensor w2, Tensor "
"topk_weights, Tensor topk_ids, bool "
"inplace, int moe_comp_method, Tensor? w1_scale, Tensor? w2_scale, "
"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_bias, Tensor? w2_bias, float? alpha, float? limit, "
"bool is_vnni) -> "
"Tensor");
"()");
ops.impl("fused_experts_cpu", torch::kCPU, &fused_experts_cpu);
ops.def(
"int8_scaled_mm_with_quant(Tensor mat1, Tensor mat2, Tensor scales2, "
@@ -583,6 +676,110 @@ TORCH_LIBRARY_EXPAND(TORCH_EXTENSION_NAME, ops) {
"concat_and_cache_mla_cpu(Tensor kv_c_normed, Tensor k_pe, "
"Tensor(a!) kv_cache, Tensor slot_mapping) -> ()");
ops.impl("concat_and_cache_mla_cpu", torch::kCPU, &concat_and_cache_mla_cpu);
// Adapted from sglang: DeepSeek-V4 mHC gating kernels
ops.def(
"hc_pre_fused_cpu(Tensor x, Tensor hc_fn, Tensor hc_scale, "
"Tensor hc_base, int hc_mult, int sinkhorn_iters, float rms_eps, "
"float hc_eps) -> (Tensor, Tensor, Tensor)");
ops.impl("hc_pre_fused_cpu", torch::kCPU, &hc_pre_fused_cpu);
ops.def(
"hc_post_fused_cpu(Tensor x, Tensor residual, Tensor post, "
"Tensor comb) -> Tensor");
ops.impl("hc_post_fused_cpu", torch::kCPU, &hc_post_fused_cpu);
ops.def(
"hc_head_fused_cpu(Tensor x, Tensor hc_fn, Tensor "
"hc_scale, Tensor hc_base, float hc_eps, float norm_eps) -> "
"Tensor");
ops.impl("hc_head_fused_cpu", torch::kCPU, &hc_head_fused_cpu);
// Adapted from sglang: DeepSeek-V4 fp8_ds_mla cache-write kernel
ops.def(
"fused_qnorm_rope_kv_insert_cpu(Tensor q, Tensor kv, Tensor positions, "
"Tensor(a!) swa_kv_cache_2d, Tensor slot_mapping, Tensor "
"cos_sin_cache, int q_head_padded, float eps, int cache_block_size) "
"-> Tensor");
ops.impl("fused_qnorm_rope_kv_insert_cpu", torch::kCPU,
&fused_qnorm_rope_kv_insert_cpu);
// vLLM-native: DeepSeek-V4 sparse MQA attention kernel
ops.def(
"flash_mla_with_kvcache_cpu(Tensor(a!) out, Tensor q, Tensor "
"window_cache_2d, Tensor window_slots, int window_block_size, Tensor "
"compressed_cache_2d, Tensor compressed_slots, int "
"compressed_block_size, Tensor attn_sink, float scale) -> ()");
ops.impl("flash_mla_with_kvcache_cpu", torch::kCPU,
&flash_mla_with_kvcache_cpu);
// Adapted from sglang: DeepSeek-V4 compressor kernels
ops.def(
"save_partial_states_cpu(Tensor kv, Tensor score, Tensor ape, "
"Tensor positions, Tensor(a!) state_cache, Tensor slot_mapping) "
"-> ()");
ops.impl("save_partial_states_cpu", torch::kCPU, &save_partial_states_cpu);
ops.def(
"compress_norm_rope_store_cpu(Tensor state_cache, Tensor "
"gather_slots, Tensor positions, Tensor kv_slot_mapping, Tensor "
"rms_norm_weight, float rms_norm_eps, Tensor cos_sin_cache, "
"Tensor(a!) kv_cache_2d, int kv_cache_block_size, int compress_ratio) "
"-> ()");
ops.impl("compress_norm_rope_store_cpu", torch::kCPU,
&compress_norm_rope_store_cpu);
ops.def(
"compress_norm_rope_store_indexer_cpu(Tensor state_cache, Tensor "
"gather_slots, Tensor positions, Tensor kv_slot_mapping, Tensor "
"rms_norm_weight, float rms_norm_eps, Tensor cos_sin_cache, "
"Tensor(a!) kv_cache_2d, int kv_cache_block_size, int compress_ratio) "
"-> ()");
ops.impl("compress_norm_rope_store_indexer_cpu", torch::kCPU,
&compress_norm_rope_store_indexer_cpu);
// Adapted from sglang: DeepSeek-V4 sparse indexer paged MQA-logits kernel
// (DECODE path)
ops.def(
"fp8_paged_mqa_logits_cpu(Tensor q_fp8, Tensor kvcache_fp8, Tensor "
"weight, Tensor seq_lens, Tensor page_table, int block_size, int "
"max_seq_len) -> Tensor");
ops.impl("fp8_paged_mqa_logits_cpu", torch::kCPU, &fp8_paged_mqa_logits_cpu);
ops.def(
"topk_transform_512_cpu(Tensor scores, Tensor seq_lens, Tensor "
"page_tables, Tensor(a!) out_page_indices, int page_size, Tensor(b!)? "
"out_raw_indices) -> ()");
ops.impl("topk_transform_512_cpu", torch::kCPU, &topk_transform_512_cpu);
ops.def(
"fused_indexer_q_rope_quant_cpu(Tensor positions, Tensor index_q, "
"Tensor index_q_cos_sin_cache, Tensor(a!) index_q_fp8, Tensor "
"index_weights, float index_weights_softmax_scale, float "
"index_weights_head_scale, Tensor(b!) index_weights_out) -> ()");
ops.impl("fused_indexer_q_rope_quant_cpu", torch::kCPU,
&fused_indexer_q_rope_quant_cpu);
// Adapted from sglang: DeepSeek-V4 MoE routing kernels
ops.def(
"biased_topk_cpu(Tensor hidden_states, Tensor gating_output, Tensor "
"correction_bias, int topk, bool renormalize, str scoring_func, int "
"num_fused_shared_experts, float? routed_scaling_factor, bool "
"apply_routed_scaling_factor_on_output) -> (Tensor, Tensor)");
ops.impl("biased_topk_cpu", torch::kCPU, &biased_topk_cpu);
ops.def(
"hash_topk_cpu(Tensor gating_output, Tensor tid2eid, int topk, str "
"scoring_func, int num_fused_shared_experts, int num_experts, float "
"routed_scaling_factor) -> (Tensor, Tensor)");
ops.impl("hash_topk_cpu", torch::kCPU, &hash_topk_cpu);
// vLLM-native: inverse GPT-J RoPE for _o_proj's output de-rotation
ops.def(
"inverse_gptj_rope_o_proj_cpu(Tensor o, Tensor positions, Tensor "
"cos_sin_cache, int rope_dim) -> Tensor");
ops.impl("inverse_gptj_rope_o_proj_cpu", torch::kCPU,
&inverse_gptj_rope_o_proj_cpu);
#endif
#if (defined(__AVX512BF16__) && defined(__AVX512F__) && \
+20 -14
View File
@@ -26,8 +26,8 @@ from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
CPUUnquantizedExperts,
PowerCPUUnquantizedExperts,
X86CPUUnquantizedExperts,
select_experts,
)
from vllm.model_executor.layers.fused_moe.router.cpu_router import _softmax_topk
from vllm.platforms import CpuArchEnum, current_platform
from vllm.utils.torch_utils import set_random_seed
@@ -284,7 +284,9 @@ def test_cpu_fused_moe(
packed_w13 = cpu_prepack_moe_weight(w13, isa)
packed_w2 = cpu_prepack_moe_weight(w2, isa)
output = cpu_fused_moe(
output = torch.empty_like(input)
cpu_fused_moe(
output,
input,
packed_w13,
packed_w2,
@@ -368,7 +370,9 @@ def test_cpu_fused_moe_int8(
)
packed_w13 = cpu_prepack_moe_weight_int8(w13, isa)
packed_w2 = cpu_prepack_moe_weight_int8(w2, isa)
output = cpu_fused_moe_int8(
output = torch.empty_like(input)
cpu_fused_moe_int8(
output,
input,
packed_w13,
packed_w2,
@@ -521,15 +525,10 @@ def test_cpu_fused_moe_unaligned_intermediate_size(
w2_bias = torch.randn((expert_num, hidden_size), dtype=dtype) / (
0.5 * hidden_size**0.5
)
# Route with the same helper apply() uses internally, so the reference
# only differs from the kernel in how the experts are evaluated.
topk_weight, topk_ids = select_experts(
hidden_states=input,
router_logits=router_logits,
top_k=topk_num,
use_grouped_topk=False,
renormalize=False,
)
# Route with the same helper the router uses internally, so the
# reference only differs from the kernel in how the experts are
# evaluated.
topk_weight, topk_ids = _softmax_topk(router_logits, topk_num, False)
ref_output = ref_fused_moe(
input, w13, w2, w13_bias, w2_bias, topk_weight, topk_ids, act
@@ -575,15 +574,22 @@ def test_cpu_fused_moe_unaligned_intermediate_size(
assert experts.isa == isa
experts.process_weights_after_loading(layer)
output = experts.apply(
output = torch.empty_like(input)
experts.apply(
output=output,
hidden_states=input,
w1=layer.w13_weight,
w2=layer.w2_weight,
router_logits=router_logits,
topk_weights=topk_weight,
topk_ids=topk_ids,
activation=act,
global_num_experts=expert_num,
expert_map=None,
a1q_scale=None,
a2_scale=None,
workspace13=torch.empty(0),
workspace2=torch.empty(0),
expert_tokens_meta=None,
apply_router_weight_on_input=False,
)
+48 -29
View File
@@ -197,14 +197,15 @@ def test_w8a16_block_fp8_cpu_fused_moe(M, N, K, E, topk, seed):
pw1, pw2 = _prepack_experts(w1), _prepack_experts(w2)
# Test inplace=False against reference
out = ops.fused_experts_cpu(
# Test writing into a fresh output buffer against reference
out = torch.empty_like(a)
ops.fused_experts_cpu(
out,
a.clone(),
pw1,
pw2,
topk_weight,
topk_ids,
False,
ops.CPUQuantMethod.FP8_W8A16,
w1_s,
w2_s,
@@ -215,14 +216,16 @@ def test_w8a16_block_fp8_cpu_fused_moe(M, N, K, E, topk, seed):
)
torch.testing.assert_close(ref_out.bfloat16(), out, atol=1e-2, rtol=1e-2)
# Test inplace=True produces identical output
out_inplace = ops.fused_experts_cpu(
a.clone(),
# Test writing into the input tensor itself (aliased in-place) produces
# an identical result
a_inplace = a.clone()
ops.fused_experts_cpu(
a_inplace,
a_inplace,
pw1,
pw2,
topk_weight,
topk_ids,
True,
ops.CPUQuantMethod.FP8_W8A16,
w1_s,
w2_s,
@@ -231,7 +234,7 @@ def test_w8a16_block_fp8_cpu_fused_moe(M, N, K, E, topk, seed):
BLOCK_SIZE,
is_vnni=True,
)
torch.testing.assert_close(out_inplace, out, atol=0, rtol=0)
torch.testing.assert_close(a_inplace, out, atol=0, rtol=0)
def test_w8a16_block_fp8_cpu_fused_moe_small_expert_blocks():
@@ -249,13 +252,14 @@ def test_w8a16_block_fp8_cpu_fused_moe_small_expert_blocks():
a, w1, w2, w1_s, w2_s, topk_weight, topk_ids, BLOCK_SIZE
)
pw1, pw2 = _prepack_experts(w1), _prepack_experts(w2)
out = ops.fused_experts_cpu(
out = torch.empty_like(a)
ops.fused_experts_cpu(
out,
a,
pw1,
pw2,
topk_weight,
topk_ids,
False,
ops.CPUQuantMethod.FP8_W8A16,
w1_s,
w2_s,
@@ -469,13 +473,15 @@ def test_mxfp4_cpu_fused_moe(M, N, K, E, topk, seed):
pw2, pw2s = _prepack_mxfp4_experts(w2q, w2s)
# Kernel
out = ops.fused_experts_cpu(
a.clone(),
a_in = a.clone()
out = torch.empty_like(a_in)
ops.fused_experts_cpu(
out,
a_in,
pw1,
pw2,
topk_weight,
topk_ids,
False, # inplace
ops.CPUQuantMethod.MXFP4,
pw1s, # w1_scale
pw2s, # w2_scale
@@ -511,13 +517,14 @@ def test_mxfp4_cpu_fused_moe_small_expert_blocks():
pw1, pw1s = _prepack_mxfp4_experts(w1q, w1s)
pw2, pw2s = _prepack_mxfp4_experts(w2q, w2s)
out = ops.fused_experts_cpu(
out = torch.empty_like(a)
ops.fused_experts_cpu(
out,
a,
pw1,
pw2,
topk_weight,
topk_ids,
False,
ops.CPUQuantMethod.MXFP4,
pw1s,
pw2s,
@@ -569,13 +576,14 @@ def test_mxfp4_cpu_zero_codes_stay_zero(zero_byte, e8m0):
pw1, pw1s = _prepack_mxfp4_experts(w1q, w1s)
pw2, pw2s = _prepack_mxfp4_experts(w2q, w2s)
out = ops.fused_experts_cpu(
out = torch.empty_like(a)
ops.fused_experts_cpu(
out,
a,
pw1,
pw2,
topk_weight,
topk_ids,
False,
ops.CPUQuantMethod.MXFP4,
pw1s,
pw2s,
@@ -630,13 +638,14 @@ def test_mxfp4_cpu_zero_codes_mixed_with_nonzero(e8m0):
pw1, pw1s = _prepack_mxfp4_experts(w1q, w1s)
pw2, pw2s = _prepack_mxfp4_experts(w2q, w2s)
out = ops.fused_experts_cpu(
out = torch.empty_like(a)
ops.fused_experts_cpu(
out,
a,
pw1,
pw2,
topk_weight,
topk_ids,
False,
ops.CPUQuantMethod.MXFP4,
pw1s,
pw2s,
@@ -689,13 +698,15 @@ def test_mxfp4_cpu_fused_moe_bias_swiglu(M, N, K, E, topk, seed):
pw2, pw2s = _prepack_mxfp4_experts(w2q, w2s)
# Kernel
out = ops.fused_experts_cpu(
a.clone(),
a_in = a.clone()
out = torch.empty_like(a_in)
ops.fused_experts_cpu(
out,
a_in,
pw1,
pw2,
topk_weight,
topk_ids,
False, # inplace
ops.CPUQuantMethod.MXFP4,
pw1s, # w1_scale
pw2s, # w2_scale
@@ -944,13 +955,15 @@ def test_int4_w4a16_cpu_fused_moe(M, N, K, E, topk, group_size, quant_algo, seed
)
)
out = ops.fused_experts_cpu(
a.clone(),
a_in = a.clone()
out = torch.empty_like(a_in)
ops.fused_experts_cpu(
out,
a_in,
blocked_w1,
blocked_w2,
topk_weight,
topk_ids,
False, # inplace
ops.CPUQuantMethod.INT4_W4A8,
blocked_s1,
blocked_s2,
@@ -1051,7 +1064,11 @@ INT8_MOE_CONFIGS = [
@pytest.mark.parametrize("is_vnni", [False, True])
@pytest.mark.parametrize("inplace", [False, True])
def test_int8_w8a8_cpu_fused_moe(M, N, K, E, topk, seed, is_vnni, inplace):
"""Test fused_experts_cpu INT8 W8A8 against torch reference."""
"""Test fused_experts_cpu INT8 W8A8 against torch reference.
``inplace`` exercises writing into a fresh output buffer vs. aliasing
the input tensor as the output buffer.
"""
set_random_seed(seed)
a = torch.randn(M, K, dtype=torch.bfloat16) / (0.5 * K**0.5)
@@ -1067,13 +1084,15 @@ def test_int8_w8a8_cpu_fused_moe(M, N, K, E, topk, seed, is_vnni, inplace):
w1 = _prepack_experts(w1_q) if is_vnni else w1_q
w2 = _prepack_experts(w2_q) if is_vnni else w2_q
out = ops.fused_experts_cpu(
a.clone(),
a_in = a.clone()
out = a_in if inplace else torch.empty_like(a_in)
ops.fused_experts_cpu(
out,
a_in,
w1,
w2,
topk_weight,
topk_ids,
inplace,
ops.CPUQuantMethod.INT8_W8A8,
w1_s,
w2_s,
+30 -40
View File
@@ -2,8 +2,6 @@
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Tests for ZenTorch CPU fused MoE dispatch and forward."""
from types import SimpleNamespace
import pytest
import torch
@@ -27,8 +25,8 @@ from vllm.model_executor.layers.fused_moe.config import (
)
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
CPUUnquantizedExperts,
select_experts,
)
from vllm.model_executor.layers.fused_moe.router.cpu_router import _softmax_topk
from vllm.platforms import current_platform
from vllm.utils.torch_utils import set_random_seed
@@ -95,18 +93,19 @@ def _make_layer_and_config(
@pytest.mark.parametrize("act", ZENTORCH_ACT)
def test_zen_cpu_fused_moe_dispatches_to_zentorch(act: MoEActivation):
"""When zentorch MoE is supported, experts select the zentorch path."""
layer, moe_config = _make_layer_and_config(act)
w13_before = layer.w13_weight.detach().clone()
w2_before = layer.w2_weight.detach().clone()
def test_zen_cpu_fused_moe_config_supported(act: MoEActivation):
"""CPUUnquantizedExperts is selected when zentorch MoE is supported."""
_, moe_config = _make_layer_and_config(act)
assert is_zentorch_moe_config_supported(moe_config)
experts = CPUUnquantizedExperts(moe_config, FusedMoEQuantConfig.make())
experts.process_weights_after_loading(layer)
assert experts._use_zentorch is True
torch.testing.assert_close(layer.w13_weight, w13_before)
torch.testing.assert_close(layer.w2_weight, w2_before)
supported, reason = CPUUnquantizedExperts.is_supported_config(
CPUUnquantizedExperts,
moe_config,
None,
None,
mk.FusedMoEActivationFormat.Standard,
)
assert supported, reason
def test_zen_cpu_fused_moe_config_supported_unaligned_intermediate():
@@ -169,26 +168,34 @@ def test_zen_cpu_fused_moe_forward(
experts.process_weights_after_loading(layer)
assert experts._use_zentorch is True
topk_weight, topk_ids = select_experts(
hidden_states=input,
router_logits=router_logits,
top_k=topk_num,
use_grouped_topk=False,
renormalize=False,
)
w13_before = layer.w13_weight.detach().clone()
w2_before = layer.w2_weight.detach().clone()
output = experts.apply(
topk_weight, topk_ids = _softmax_topk(router_logits, topk_num, False)
output = torch.empty_like(input)
experts.apply(
output=output,
hidden_states=input,
w1=layer.w13_weight,
w2=layer.w2_weight,
router_logits=router_logits,
topk_weights=topk_weight,
topk_ids=topk_ids,
activation=act,
global_num_experts=expert_num,
expert_map=None,
a1q_scale=None,
a2_scale=None,
workspace13=torch.empty(0),
workspace2=torch.empty(0),
expert_tokens_meta=None,
apply_router_weight_on_input=False,
)
# zentorch needs no prepacking; weights are consumed as loaded.
torch.testing.assert_close(layer.w13_weight, w13_before)
torch.testing.assert_close(layer.w2_weight, w2_before)
ref_output = ref_fused_moe(
input,
layer.w13_weight,
@@ -202,20 +209,3 @@ def test_zen_cpu_fused_moe_forward(
atol, rtol = get_default_atol(output), get_default_rtol(output)
torch.testing.assert_close(output, ref_output, atol=atol, rtol=rtol)
def test_zen_cpu_fused_moe_skips_without_activation():
"""Layers without an activation attribute fall back from zentorch."""
layer = SimpleNamespace()
moe_config = _make_moe_config(
expert_num=8,
hidden_size=128,
intermediate_size=128,
topk_num=4,
dtype=torch.bfloat16,
act=MoEActivation.SILU,
)
experts = CPUUnquantizedExperts(moe_config, FusedMoEQuantConfig.make())
experts.process_weights_after_loading(layer)
assert experts._use_zentorch is False
+12 -14
View File
@@ -18,14 +18,12 @@ from vllm.model_executor.layers.fused_moe.config import (
FusedMoEParallelConfig,
int8_w8a8_moe_quant_config,
)
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
ZenCPUExpertsInt8,
select_experts,
)
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import ZenCPUExpertsInt8
from vllm.model_executor.layers.fused_moe.oracle.int8 import (
Int8MoeBackend,
backend_to_kernel_cls,
)
from vllm.model_executor.layers.fused_moe.router.cpu_router import _softmax_topk
from vllm.model_executor.layers.quantization.utils.quant_utils import (
kInt8DynamicTokenSym,
kInt8StaticChannelSym,
@@ -199,19 +197,27 @@ def test_zen_int8_dispatch_contract(
0.5 * HIDDEN_SIZE**0.5
)
router_logits = torch.randn((BATCH_SIZE, EXPERT_NUM), dtype=torch.bfloat16)
topk_weights, topk_ids = _softmax_topk(router_logits, topk_num, False)
experts = ZenCPUExpertsInt8(moe_config, quant_config)
# zentorch_fused_moe takes no expert_map.
assert not experts.supports_expert_map()
output = experts.apply(
output = torch.empty_like(input)
experts.apply(
output=output,
hidden_states=input,
w1=w13,
w2=w2,
router_logits=router_logits,
topk_weights=topk_weights,
topk_ids=topk_ids,
activation=act,
global_num_experts=EXPERT_NUM,
expert_map=None,
a1q_scale=None,
a2_scale=None,
workspace13=torch.empty(0),
workspace2=torch.empty(0),
expert_tokens_meta=None,
apply_router_weight_on_input=False,
)
@@ -230,14 +236,6 @@ def test_zen_int8_dispatch_contract(
else:
assert call["w13_bias"] is None and call["w2_bias"] is None
topk_weights, topk_ids = select_experts(
hidden_states=input,
router_logits=router_logits,
use_grouped_topk=False,
top_k=topk_num,
renormalize=False,
scoring_func="softmax",
)
ref = ref_fused_moe_int8(
input,
w13,
File diff suppressed because it is too large Load Diff
@@ -92,6 +92,11 @@ _XPU_EXCLUDED_MODEL_IDS = {
"thinkingmachines/Inkling-NVFP4",
}
_CPU_EXCLUDED_MODEL_IDS = {
# DeepSeek-V4 vision variant is only supported on NVIDIA GPUs for now.
"deepseek-ai/DeepSeek-V4-Flash-Vision-Exp",
}
def _iter_model_ids_to_test(model_arch_list: AbstractSet[str]):
for model_arch in model_arch_list:
@@ -113,6 +118,11 @@ def _get_model_ids_to_test(model_arch_list: AbstractSet[str]):
while excluded_model_id in model_ids:
model_ids.remove(excluded_model_id)
if current_platform.is_cpu():
for excluded_model_id in _CPU_EXCLUDED_MODEL_IDS:
while excluded_model_id in model_ids:
model_ids.remove(excluded_model_id)
return model_ids
@@ -56,7 +56,7 @@ from vllm.v1.attention.backends.mla.flashmla_sparse import (
FlashMLASparseMetadataBuilder,
triton_convert_req_index_to_global_index,
)
from vllm.v1.attention.backends.mla.indexer import split_indexer_prefill_chunks
from vllm.v1.attention.backends.mla.indexer import DeepseekV32IndexerMetadataBuilder
from vllm.v1.attention.backends.utils import (
split_decodes_and_prefills,
split_prefill_chunks,
@@ -1424,7 +1424,7 @@ def test_sparse_backend_prefill_correctness(
def test_split_indexer_prefill_chunks(
seq_lens, query_lens, workspace_size, max_logits_bytes, expected
):
out = split_indexer_prefill_chunks(
out = DeepseekV32IndexerMetadataBuilder._split_indexer_prefill_chunks(
seq_lens,
query_lens,
workspace_size,
@@ -1438,7 +1438,9 @@ def test_split_indexer_prefill_chunks_single_request_overflow():
seq_lens = torch.tensor([1000, 50])
query_lens = torch.tensor([100, 5])
out = split_indexer_prefill_chunks(seq_lens, query_lens, 2000, 1000)
out = DeepseekV32IndexerMetadataBuilder._split_indexer_prefill_chunks(
seq_lens, query_lens, 2000, 1000
)
# max_logits_elems = 250, N=1000 -> max_q = 1 -> 100 query sub-chunks
expected = [(slice(0, 1), slice(i, i + 1)) for i in range(100)]
# req1: M=5, N=50 -> 250 elems fits budget
+397 -37
View File
@@ -3357,31 +3357,6 @@ class CPUQuantMethod(IntEnum):
MXFP4 = 4
if hasattr(torch.ops._C, "fused_experts_cpu"):
@register_fake("_C::fused_experts_cpu")
def fused_experts_cpu_fake(
hidden_states: torch.Tensor,
w1: torch.Tensor,
w2: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
inplace: bool,
moe_comp_method: CPUQuantMethod,
w1_scale: torch.Tensor | None,
w2_scale: torch.Tensor | None,
w1_zero: torch.Tensor | None,
w2_zero: torch.Tensor | None,
block_size: list[int] | None,
w1_bias: torch.Tensor | None,
w2_bias: torch.Tensor | None,
alpha: float | None,
limit: float | None,
is_vnni: bool,
) -> torch.Tensor:
return torch.empty_like(hidden_states)
if hasattr(torch.ops._C, "dynamic_4bit_int_moe"):
@register_fake("_C::dynamic_4bit_int_moe")
@@ -3401,12 +3376,12 @@ if hasattr(torch.ops._C, "dynamic_4bit_int_moe"):
def fused_experts_cpu(
out: torch.Tensor,
hidden_states: torch.Tensor,
w1: torch.Tensor,
w2: torch.Tensor,
topk_weights: torch.Tensor,
topk_ids: torch.Tensor,
inplace: bool,
moe_comp_method: CPUQuantMethod,
w1_scale: torch.Tensor | None,
w2_scale: torch.Tensor | None,
@@ -3418,14 +3393,14 @@ def fused_experts_cpu(
alpha: float | None = None,
limit: float | None = None,
is_vnni: bool = True,
) -> torch.Tensor:
return torch.ops._C.fused_experts_cpu(
) -> None:
torch.ops._C.fused_experts_cpu(
out,
hidden_states,
w1,
w2,
topk_weights,
topk_ids,
inplace,
moe_comp_method,
w1_scale,
w2_scale,
@@ -4059,6 +4034,393 @@ def amx_mla_concat_and_cache(
torch.ops._C.concat_and_cache_mla_cpu(kv_c_normed, k_pe, kv_cache, slot_mapping)
if hasattr(torch.ops._C, "hc_pre_fused_cpu"):
@register_fake("_C::hc_pre_fused_cpu")
def hc_pre_fused_cpu_fake(
x: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_mult: int,
sinkhorn_iters: int,
rms_eps: float,
hc_eps: float,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
t, _, d = x.shape
y = x.new_empty((t, d))
post = x.new_empty((t, hc_mult), dtype=torch.float32)
comb = x.new_empty((t, hc_mult, hc_mult), dtype=torch.float32)
return y, post, comb
def hc_pre_fused_cpu(
x: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_mult: int,
sinkhorn_iters: int,
rms_eps: float,
hc_eps: float,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return torch.ops._C.hc_pre_fused_cpu(
x, hc_fn, hc_scale, hc_base, hc_mult, sinkhorn_iters, rms_eps, hc_eps
)
if hasattr(torch.ops._C, "hc_post_fused_cpu"):
@register_fake("_C::hc_post_fused_cpu")
def hc_post_fused_cpu_fake(
x: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
) -> torch.Tensor:
t, d = x.shape
hc = residual.size(1)
return x.new_empty((t, hc, d))
def hc_post_fused_cpu(
x: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
) -> torch.Tensor:
return torch.ops._C.hc_post_fused_cpu(x, residual, post, comb)
if hasattr(torch.ops._C, "hc_head_fused_cpu"):
@register_fake("_C::hc_head_fused_cpu")
def hc_head_fused_cpu_fake(
x: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_eps: float,
norm_eps: float,
) -> torch.Tensor:
t, _, d = x.shape
return x.new_empty((t, d))
def hc_head_fused_cpu(
x: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
hc_eps: float,
norm_eps: float,
) -> torch.Tensor:
return torch.ops._C.hc_head_fused_cpu(x, hc_fn, hc_scale, hc_base, hc_eps, norm_eps)
if hasattr(torch.ops._C, "fused_qnorm_rope_kv_insert_cpu"):
@register_fake("_C::fused_qnorm_rope_kv_insert_cpu")
def fused_qnorm_rope_kv_insert_cpu_fake(
q: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor,
swa_kv_cache_2d: torch.Tensor,
slot_mapping: torch.Tensor,
cos_sin_cache: torch.Tensor,
q_head_padded: int,
eps: float,
cache_block_size: int,
) -> torch.Tensor:
num_tokens_full, _, head_dim = q.shape
return q.new_empty((num_tokens_full, q_head_padded, head_dim))
def fused_qnorm_rope_kv_insert_cpu(
q: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor,
swa_kv_cache_2d: torch.Tensor,
slot_mapping: torch.Tensor,
cos_sin_cache: torch.Tensor,
q_head_padded: int,
eps: float,
cache_block_size: int,
) -> torch.Tensor:
return torch.ops._C.fused_qnorm_rope_kv_insert_cpu(
q,
kv,
positions,
swa_kv_cache_2d,
slot_mapping,
cos_sin_cache,
q_head_padded,
eps,
cache_block_size,
)
def flash_mla_with_kvcache_cpu(
out: torch.Tensor,
q: torch.Tensor,
window_cache_2d: torch.Tensor,
window_slots: torch.Tensor,
window_block_size: int,
compressed_cache_2d: torch.Tensor,
compressed_slots: torch.Tensor,
compressed_block_size: int,
attn_sink: torch.Tensor,
scale: float,
) -> None:
torch.ops._C.flash_mla_with_kvcache_cpu(
out,
q,
window_cache_2d,
window_slots,
window_block_size,
compressed_cache_2d,
compressed_slots,
compressed_block_size,
attn_sink,
scale,
)
def save_partial_states_cpu(
kv: torch.Tensor,
score: torch.Tensor,
ape: torch.Tensor,
positions: torch.Tensor,
state_cache: torch.Tensor,
slot_mapping: torch.Tensor,
) -> None:
torch.ops._C.save_partial_states_cpu(
kv, score, ape, positions, state_cache, slot_mapping
)
def compress_norm_rope_store_cpu(
state_cache: torch.Tensor,
gather_slots: torch.Tensor,
positions: torch.Tensor,
kv_slot_mapping: torch.Tensor,
rms_norm_weight: torch.Tensor,
rms_norm_eps: float,
cos_sin_cache: torch.Tensor,
kv_cache_2d: torch.Tensor,
kv_cache_block_size: int,
compress_ratio: int,
) -> None:
torch.ops._C.compress_norm_rope_store_cpu(
state_cache,
gather_slots,
positions,
kv_slot_mapping,
rms_norm_weight,
rms_norm_eps,
cos_sin_cache,
kv_cache_2d,
kv_cache_block_size,
compress_ratio,
)
def compress_norm_rope_store_indexer_cpu(
state_cache: torch.Tensor,
gather_slots: torch.Tensor,
positions: torch.Tensor,
kv_slot_mapping: torch.Tensor,
rms_norm_weight: torch.Tensor,
rms_norm_eps: float,
cos_sin_cache: torch.Tensor,
kv_cache_2d: torch.Tensor,
kv_cache_block_size: int,
compress_ratio: int,
) -> None:
torch.ops._C.compress_norm_rope_store_indexer_cpu(
state_cache,
gather_slots,
positions,
kv_slot_mapping,
rms_norm_weight,
rms_norm_eps,
cos_sin_cache,
kv_cache_2d,
kv_cache_block_size,
compress_ratio,
)
if hasattr(torch.ops._C, "fp8_paged_mqa_logits_cpu"):
@register_fake("_C::fp8_paged_mqa_logits_cpu")
def fp8_paged_mqa_logits_cpu_fake(
q_fp8: torch.Tensor,
kvcache_fp8: torch.Tensor,
weight: torch.Tensor,
seq_lens: torch.Tensor,
page_table: torch.Tensor,
block_size: int,
max_seq_len: int,
) -> torch.Tensor:
batch_size = q_fp8.size(0)
return q_fp8.new_empty((batch_size, max_seq_len), dtype=torch.float32)
def fp8_paged_mqa_logits_cpu(
q_fp8: torch.Tensor,
kvcache_fp8: torch.Tensor,
weight: torch.Tensor,
seq_lens: torch.Tensor,
page_table: torch.Tensor,
block_size: int,
max_seq_len: int,
) -> torch.Tensor:
return torch.ops._C.fp8_paged_mqa_logits_cpu(
q_fp8, kvcache_fp8, weight, seq_lens, page_table, block_size, max_seq_len
)
def topk_transform_512_cpu(
scores: torch.Tensor,
seq_lens: torch.Tensor,
page_tables: torch.Tensor,
out_page_indices: torch.Tensor,
page_size: int,
out_raw_indices: torch.Tensor | None = None,
) -> None:
torch.ops._C.topk_transform_512_cpu(
scores, seq_lens, page_tables, out_page_indices, page_size, out_raw_indices
)
def fused_indexer_q_rope_quant_cpu(
positions: torch.Tensor,
index_q: torch.Tensor,
index_q_cos_sin_cache: torch.Tensor,
index_q_fp8: torch.Tensor,
index_weights: torch.Tensor,
index_weights_softmax_scale: float,
index_weights_head_scale: float,
index_weights_out: torch.Tensor,
) -> None:
torch.ops._C.fused_indexer_q_rope_quant_cpu(
positions,
index_q,
index_q_cos_sin_cache,
index_q_fp8,
index_weights,
index_weights_softmax_scale,
index_weights_head_scale,
index_weights_out,
)
if hasattr(torch.ops._C, "biased_topk_cpu"):
@register_fake("_C::biased_topk_cpu")
def biased_topk_cpu_fake(
hidden_states: torch.Tensor,
gating_output: torch.Tensor,
correction_bias: torch.Tensor,
topk: int,
renormalize: bool,
scoring_func: str,
num_fused_shared_experts: int,
routed_scaling_factor: float | None,
apply_routed_scaling_factor_on_output: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
num_tokens = hidden_states.size(0)
topk_weights = hidden_states.new_empty((num_tokens, topk), dtype=torch.float32)
topk_ids = hidden_states.new_empty((num_tokens, topk), dtype=torch.int32)
return topk_weights, topk_ids
def biased_topk_cpu(
hidden_states: torch.Tensor,
gating_output: torch.Tensor,
correction_bias: torch.Tensor,
topk: int,
renormalize: bool,
scoring_func: str,
num_fused_shared_experts: int,
routed_scaling_factor: float | None,
apply_routed_scaling_factor_on_output: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops._C.biased_topk_cpu(
hidden_states,
gating_output,
correction_bias,
topk,
renormalize,
scoring_func,
num_fused_shared_experts,
routed_scaling_factor,
apply_routed_scaling_factor_on_output,
)
if hasattr(torch.ops._C, "hash_topk_cpu"):
@register_fake("_C::hash_topk_cpu")
def hash_topk_cpu_fake(
gating_output: torch.Tensor,
tid2eid: torch.Tensor,
topk: int,
scoring_func: str,
num_fused_shared_experts: int,
num_experts: int,
routed_scaling_factor: float,
) -> tuple[torch.Tensor, torch.Tensor]:
num_tokens = gating_output.size(0)
topk_weights = gating_output.new_empty((num_tokens, topk), dtype=torch.float32)
topk_ids = gating_output.new_empty((num_tokens, topk), dtype=torch.int32)
return topk_weights, topk_ids
def hash_topk_cpu(
gating_output: torch.Tensor,
tid2eid: torch.Tensor,
topk: int,
scoring_func: str,
num_fused_shared_experts: int,
num_experts: int,
routed_scaling_factor: float,
) -> tuple[torch.Tensor, torch.Tensor]:
return torch.ops._C.hash_topk_cpu(
gating_output,
tid2eid,
topk,
scoring_func,
num_fused_shared_experts,
num_experts,
routed_scaling_factor,
)
if hasattr(torch.ops._C, "inverse_gptj_rope_o_proj_cpu"):
@register_fake("_C::inverse_gptj_rope_o_proj_cpu")
def inverse_gptj_rope_o_proj_cpu_fake(
o: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
rope_dim: int,
) -> torch.Tensor:
return o.new_empty(o.shape, dtype=torch.float32)
def inverse_gptj_rope_o_proj_cpu(
o: torch.Tensor,
positions: torch.Tensor,
cos_sin_cache: torch.Tensor,
rope_dim: int,
) -> torch.Tensor:
return torch.ops._C.inverse_gptj_rope_o_proj_cpu(
o, positions, cos_sin_cache, rope_dim
)
def cpu_gemm_wna16(
input: torch.Tensor,
q_weight: torch.Tensor,
@@ -4107,6 +4469,7 @@ def cpu_prepack_moe_weight_int8(
def cpu_fused_moe(
out: torch.Tensor,
input: torch.Tensor,
w13: torch.Tensor,
w2: torch.Tensor,
@@ -4117,10 +4480,9 @@ def cpu_fused_moe(
act: str,
isa: str,
skip_weighted: bool = False,
) -> torch.Tensor:
output = torch.empty_like(input)
) -> None:
torch.ops._C.cpu_fused_moe(
output,
out,
input,
w13,
w2,
@@ -4132,10 +4494,10 @@ def cpu_fused_moe(
act,
isa,
)
return output
def cpu_fused_moe_int8(
out: torch.Tensor,
input: torch.Tensor,
w13: torch.Tensor,
w2: torch.Tensor,
@@ -4148,10 +4510,9 @@ def cpu_fused_moe_int8(
act: str,
isa: str,
skip_weighted: bool = False,
) -> torch.Tensor:
output = torch.empty_like(input)
) -> None:
torch.ops._C.cpu_fused_moe_int8(
output,
out,
input,
w13,
w2,
@@ -4165,7 +4526,6 @@ def cpu_fused_moe_int8(
act,
isa,
)
return output
if hasattr(torch.ops._qutlass_C, "matmul_mxf4_bf16_tn"):
@@ -6,6 +6,9 @@ import torch
from vllm import _custom_ops as ops
from vllm.model_executor.layers.quantization.utils import replace_parameter
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
_upcast_e8m0_to_fp32,
)
from vllm.model_executor.layers.quantization.utils.w8a8_utils import (
convert_to_channelwise,
)
@@ -281,10 +284,12 @@ class CPUFp8BlockScaledMMKernel(Fp8BlockScaledMMLinearKernel):
else params.weight_scale
)
assert weight_scale is not None
if weight_scale.dtype in (torch.float8_e8m0fnu, torch.uint8):
weight_scale = _upcast_e8m0_to_fp32(weight_scale.data)
replace_parameter(
layer,
scale_attr,
torch.nn.Parameter(weight_scale.data, requires_grad=False),
torch.nn.Parameter(weight_scale.data.contiguous(), requires_grad=False),
)
def apply_weights(
@@ -1,6 +1,7 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
from .aiter import *
from .cpu import *
from .tilelang import *
from .torch import *
from .triton import *
@@ -14,6 +15,9 @@ __all__ = [
"mhc_post_aiter",
"mhc_fused_post_pre_aiter",
"hc_head_fused_aiter",
"mhc_pre_cpu",
"mhc_post_cpu",
"hc_head_fused_cpu",
"mhc_pre_tilelang",
"mhc_post_tilelang",
"mhc_fused_post_pre_tilelang",
+96
View File
@@ -0,0 +1,96 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import torch
from vllm import _custom_ops as ops
def mhc_pre_cpu(
residual: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int = 1,
norm_weight: torch.Tensor | None = None,
norm_eps: float = 0.0,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
"""CPU-ported mHC pre block (see `mhc_pre_torch` for the eager reference).
The ported kernel (`hc_pre_fused_cpu`) only exposes one merged `hc_eps`
(used for both `hc_pre_eps`/`hc_sinkhorn_eps`) and hardcodes the post-mix
multiplier to 2.0 -- true of every real call site in
`models/deepseek_v4/cpu/model.py`, so this is not a capability loss here.
"""
assert n_splits == 1, "mhc_pre_cpu does not support n_splits != 1"
assert hc_pre_eps == hc_sinkhorn_eps, (
"mhc_pre_cpu requires hc_pre_eps == hc_sinkhorn_eps (single merged hc_eps)"
)
assert hc_post_mult_value == 2.0, "mhc_pre_cpu hardcodes the post-mix multiplier"
hc_mult, hidden_size = residual.shape[-2:]
outer_shape = residual.shape[:-2]
x_flat = residual.reshape(-1, hc_mult, hidden_size)
layer_input, post, comb = ops.hc_pre_fused_cpu(
x_flat,
fn,
hc_scale,
hc_base,
hc_mult,
sinkhorn_repeat,
rms_eps,
hc_pre_eps,
)
post_mix = post.view(*outer_shape, hc_mult, 1)
comb_mix = comb.view(*outer_shape, hc_mult, hc_mult)
layer_input = layer_input.view(*outer_shape, hidden_size)
return post_mix, comb_mix, layer_input
def mhc_post_cpu(
x: torch.Tensor,
residual: torch.Tensor,
post_layer_mix: torch.Tensor,
comb_res_mix: torch.Tensor,
) -> torch.Tensor:
"""CPU-ported mHC post block (see `mhc_post_torch` for the eager reference)."""
hc_mult, hidden_size = residual.shape[-2:]
outer_shape = residual.shape[:-2]
x_flat = x.reshape(-1, hidden_size)
residual_flat = residual.reshape(-1, hc_mult, hidden_size)
post_flat = post_layer_mix.reshape(-1, hc_mult).float()
comb_flat = comb_res_mix.reshape(-1, hc_mult, hc_mult).float()
out = ops.hc_post_fused_cpu(x_flat, residual_flat, post_flat, comb_flat)
return out.view(*outer_shape, hc_mult, hidden_size)
def hc_head_fused_cpu(
hidden_states: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_norm_eps: float,
hc_eps: float,
) -> torch.Tensor:
"""CPU-ported HC head reduction (see `test_hc_head_cpu` in
tests/kernels/test_mhc_kernels.py for the eager reference this is tested
against).
The ported kernel's C++ signature takes `(hc_eps, norm_eps)` -- the
opposite order from this wrapper's `(rms_norm_eps, hc_eps)`, which matches
the real call site in `models/deepseek_v4/cpu/model.py`.
"""
hc_mult, hidden_size = hidden_states.shape[-2:]
outer_shape = hidden_states.shape[:-2]
hs_flat = hidden_states.reshape(-1, hc_mult, hidden_size)
out = ops.hc_head_fused_cpu(hs_flat, hc_fn, hc_scale, hc_base, hc_eps, rms_norm_eps)
return out.view(*outer_shape, hidden_size)
@@ -218,7 +218,7 @@ class CPUExpertsInt4(mk.FusedMoEExpertsMonolithic):
Returns:
Output tensor after MoE computation
"""
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
from vllm.model_executor.layers.fused_moe.router.cpu_router import (
select_experts,
)
@@ -4,7 +4,6 @@
import math
import sys
from collections.abc import Callable
from typing import cast
import torch
@@ -34,6 +33,9 @@ from vllm.model_executor.layers.fused_moe.config import (
FusedMoEQuantConfig,
RoutingMethodType,
)
from vllm.model_executor.layers.fused_moe.topk_weight_and_reduce import (
TopKWeightAndReduceNoOP,
)
from vllm.model_executor.layers.quantization.utils.quant_utils import (
QuantKey,
kFp8Dynamic128Sym,
@@ -48,133 +50,26 @@ from vllm.platforms import CpuArchEnum, current_platform
from vllm.utils.math_utils import round_up
logger = init_logger(__name__)
# ===========================================================================
# Routing
# ===========================================================================
def grouped_topk(
hidden_states: torch.Tensor,
gating_output: torch.Tensor,
topk: int,
renormalize: bool,
num_expert_group: int = 0,
topk_group: int = 0,
scoring_func: str = "softmax",
routed_scaling_factor: float = 1.0,
e_score_correction_bias: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
assert hidden_states.shape[0] == gating_output.shape[0], "Number of tokens mismatch"
gating_output = gating_output.float()
if scoring_func == "softmax":
scores = torch.softmax(gating_output, dim=-1)
elif scoring_func == "sigmoid":
scores = gating_output.sigmoid()
else:
raise ValueError(f"Unsupported scoring function: {scoring_func}")
num_token = scores.shape[0]
if e_score_correction_bias is not None:
original_scores = scores
scores = scores + e_score_correction_bias.unsqueeze(0)
group_scores = (
scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
)
else:
group_scores = (
scores.view(num_token, num_expert_group, -1).max(dim=-1).values
) # [n, n_group]
group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
1
] # [n, top_k_group]
group_mask = torch.zeros_like(group_scores) # [n, n_group]
group_mask.scatter_(1, group_idx, 1) # [n, n_group]
score_mask = (
group_mask.unsqueeze(-1)
.expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
.reshape(num_token, -1)
) # [n, e]
tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e]
if e_score_correction_bias is not None:
topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)[1]
topk_weights = original_scores.gather(1, topk_ids)
else:
topk_weights, topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)
if renormalize:
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
if routed_scaling_factor != 1.0:
topk_weights = topk_weights * routed_scaling_factor
return topk_weights, topk_ids.to(torch.int32)
def select_experts(
hidden_states: torch.Tensor,
router_logits: torch.Tensor,
top_k: int,
use_grouped_topk: bool,
renormalize: bool,
topk_group: int | None = None,
num_expert_group: int | None = None,
custom_routing_function: Callable | None = None,
scoring_func: str = "softmax",
routed_scaling_factor: float = 1.0,
e_score_correction_bias: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
if use_grouped_topk:
assert topk_group is not None
assert num_expert_group is not None
return grouped_topk(
hidden_states=hidden_states,
gating_output=router_logits,
topk=top_k,
renormalize=renormalize,
num_expert_group=num_expert_group,
topk_group=topk_group,
scoring_func=scoring_func,
routed_scaling_factor=routed_scaling_factor,
e_score_correction_bias=e_score_correction_bias,
)
elif custom_routing_function is None:
assert scoring_func == "softmax"
topk_logit_vals, topk_idx = torch.topk(
router_logits, k=top_k, dim=-1, sorted=False
)
if renormalize:
topk_vals = torch.softmax(topk_logit_vals, dim=-1)
else:
logZ = torch.logsumexp(router_logits, dim=-1, keepdim=True)
topk_vals = (topk_logit_vals - logZ).exp()
return topk_vals.to(torch.float32), topk_idx.to(torch.int32)
else:
topk_weights, topk_ids = custom_routing_function(
hidden_states=hidden_states,
gating_output=router_logits,
topk=top_k,
renormalize=renormalize,
)
# cpu_fused_moe reads routing tensors as contiguous float32/int32
# buffers and does not account for tensor strides.
topk_weights = topk_weights.to(torch.float32).contiguous()
topk_ids = topk_ids.to(torch.int32).contiguous()
return topk_weights, topk_ids
# ===========================================================================
# Unquantized (BF16/FP16/FP32) MoE
# ===========================================================================
class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
class CPUUnquantizedExperts(mk.FusedMoEExpertsModular):
"""Portable vector grouped-gemm unquantized MoE experts."""
isa = "vec"
output_alignment = 32
reduction_alignment = 1
def __init__(
self,
moe_config: FusedMoEConfig,
quant_config: FusedMoEQuantConfig,
):
super().__init__(moe_config, quant_config)
self._use_zentorch = False
@classmethod
def _intermediate_alignment(cls) -> int:
return math.lcm(cls.output_alignment, cls.reduction_alignment)
@@ -210,20 +105,6 @@ class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
)
return True, None
def __init__(
self,
moe_config: FusedMoEConfig,
quant_config: FusedMoEQuantConfig,
):
super().__init__(moe_config, quant_config)
# Router configuration that the monolithic apply() signature cannot
# carry. Captured off the layer in process_weights_after_loading.
self.use_grouped_topk = False
self.renormalize = False
self.scoring_func = "softmax"
self.custom_routing_function: Callable | None = None
self._use_zentorch = False
@property
def expects_unquantized_inputs(self) -> bool:
return True
@@ -286,8 +167,8 @@ class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
weight_key: QuantKey | None,
activation_key: QuantKey | None,
) -> bool:
# Routing runs in select_experts(), which covers every routing method
# a layer can be configured with, including custom routing functions.
# Routing runs in the CPURouter, which covers every routing method a
# layer can be configured with, including custom routing functions.
return True
@staticmethod
@@ -298,10 +179,6 @@ class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
return True
def process_weights_after_loading(self, layer: torch.nn.Module) -> None:
self.use_grouped_topk = layer.use_grouped_topk
self.renormalize = layer.renormalize
self.scoring_func = layer.scoring_func
self.custom_routing_function = layer.custom_routing_function
self._use_zentorch = is_zentorch_moe_supported(layer)
if self._use_zentorch:
return
@@ -345,65 +222,44 @@ class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
# this very object, which is what feeds self.w1_bias in apply().
layer.w13_bias.data = new_bias
def _select_experts(
def workspace_shapes(
self,
hidden_states: torch.Tensor,
router_logits: torch.Tensor,
num_expert_group: int | None,
topk_group: int | None,
e_score_correction_bias: torch.Tensor | None,
routed_scaling_factor: float | None,
) -> tuple[torch.Tensor, torch.Tensor]:
return select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
top_k=self.moe_config.experts_per_token,
use_grouped_topk=self.use_grouped_topk,
renormalize=self.renormalize,
topk_group=topk_group,
num_expert_group=num_expert_group,
custom_routing_function=self.custom_routing_function,
scoring_func=self.scoring_func,
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
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, ...]]:
# cpu_fused_moe 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,
router_logits: 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,
# grouped topk + fused topk bias parameters
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
topk_weights, topk_ids = self._select_experts(
hidden_states,
router_logits,
num_expert_group,
topk_group,
e_score_correction_bias,
routed_scaling_factor,
)
if apply_router_weight_on_input:
assert topk_ids.size(1) == 1, (
"apply_router_weight_on_input is only implemented for topk=1"
)
hidden_states.mul_(topk_weights.to(hidden_states.dtype))
) -> None:
# apply_router_weight_on_input (topk=1 only) is already applied to
# hidden_states by MoEPrepareAndFinalizeNoDPEPModular.prepare().
if self._use_zentorch:
output = torch.empty_like(hidden_states)
torch.ops.zentorch.zentorch_fused_moe(
output,
hidden_states,
@@ -416,9 +272,9 @@ class CPUUnquantizedExperts(mk.FusedMoEExpertsMonolithic):
apply_router_weight_on_input,
str(activation.value).lower(),
)
return output
return cpu_fused_moe(
return
cpu_fused_moe(
output,
hidden_states,
w1,
w2,
@@ -460,8 +316,6 @@ class X86CPUUnquantizedExperts(CPUUnquantizedExperts):
)
if not supported:
return supported, reason
if is_zentorch_moe_config_supported(moe_config):
return True, None
if moe_config.in_dtype != torch.bfloat16:
return False, "kernel requires bfloat16 activations"
cpu_cls = cast(type[CPUUnquantizedExperts], cls)
@@ -496,8 +350,6 @@ class ArmCPUUnquantizedExperts(CPUUnquantizedExperts):
)
if not supported:
return supported, reason
if is_zentorch_moe_config_supported(moe_config):
return True, None
if moe_config.in_dtype != torch.bfloat16:
return False, "kernel requires bfloat16 activations"
cpu_cls = cast(type[CPUUnquantizedExperts], cls)
@@ -552,8 +404,8 @@ def prepare_fp8_moe_layer_for_cpu(
return packed_w13, packed_w2
class CPUExpertsFp8(mk.FusedMoEExpertsMonolithic):
"""CPU FP8 W8A16 block-quantized monolithic MoE experts."""
class CPUExpertsFp8(mk.FusedMoEExpertsModular):
"""CPU FP8 W8A16 block-quantized modular MoE experts."""
def __init__(
self,
@@ -615,6 +467,7 @@ class CPUExpertsFp8(mk.FusedMoEExpertsMonolithic):
RoutingMethodType.Default,
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
RoutingMethodType.DeepseekV4,
]
@staticmethod
@@ -624,42 +477,49 @@ class CPUExpertsFp8(mk.FusedMoEExpertsMonolithic):
) -> bool:
return True
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,
router_logits: 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,
# grouped topk + fused topk bias parameters
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
topk_weights, topk_ids = select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
use_grouped_topk=num_expert_group is not None,
top_k=self.moe_config.experts_per_token,
renormalize=self.moe_config.routing_method
in (
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
),
topk_group=topk_group,
num_expert_group=num_expert_group,
scoring_func="softmax",
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
) -> 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(
"CPUExpertsFp8 does not support apply_router_weight_on_input=True."
)
block_shape = (
list(self.quant_config.block_shape)
if self.quant_config.block_shape
@@ -670,13 +530,13 @@ class CPUExpertsFp8(mk.FusedMoEExpertsMonolithic):
)
)
return fused_experts_cpu(
fused_experts_cpu(
output,
hidden_states,
w1,
w2,
topk_weights,
topk_ids,
False, # inplace
CPUQuantMethod.FP8_W8A16, # moe_comp_method
self.w1_scale, # w1_scale
self.w2_scale, # w2_scale
@@ -710,8 +570,8 @@ def prepare_mxfp4_moe_layer_for_cpu(
return packed_w13, packed_w2, packed_w13_scale, packed_w2_scale
class CPUExpertsMxfp4(mk.FusedMoEExpertsMonolithic):
"""CPU MXFP4 W4A16 monolithic MoE experts."""
class CPUExpertsMxfp4(mk.FusedMoEExpertsModular):
"""CPU MXFP4 W4A16 modular MoE experts."""
def __init__(
self,
@@ -773,6 +633,7 @@ class CPUExpertsMxfp4(mk.FusedMoEExpertsMonolithic):
RoutingMethodType.Default,
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
RoutingMethodType.DeepseekV4,
]
@staticmethod
@@ -782,55 +643,62 @@ class CPUExpertsMxfp4(mk.FusedMoEExpertsMonolithic):
) -> bool:
return True
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,
router_logits: 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,
# grouped topk + fused topk bias parameters
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
topk_weights, topk_ids = select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
use_grouped_topk=num_expert_group is not None,
top_k=self.moe_config.experts_per_token,
renormalize=self.moe_config.routing_method
in (
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
),
topk_group=topk_group,
num_expert_group=num_expert_group,
scoring_func="softmax",
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
) -> 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 MXFP4 model today.
raise NotImplementedError(
"CPUExpertsMxfp4 does not support apply_router_weight_on_input=True."
)
# Get bias and swiglu params from quant config
w1_bias = self.quant_config.w1_bias
w2_bias = self.quant_config.w2_bias
alpha = getattr(self.quant_config, "gemm1_alpha", None)
limit = getattr(self.quant_config, "gemm1_clamp_limit", None)
return fused_experts_cpu(
fused_experts_cpu(
output,
hidden_states,
w1,
w2,
topk_weights,
topk_ids,
False, # inplace
CPUQuantMethod.MXFP4, # moe_comp_method
self.w1_scale, # w1_scale
self.w2_scale, # w2_scale
@@ -911,8 +779,8 @@ def prepare_int4_moe_layer_for_cpu(
return (blocked_w13, blocked_w2, blocked_s13, blocked_s2, blocked_z13, blocked_z2)
class CPUExpertsInt4(mk.FusedMoEExpertsMonolithic):
"""CPU INT4 W4A16 group-quantized monolithic MoE experts.
class CPUExpertsInt4(mk.FusedMoEExpertsModular):
"""CPU INT4 W4A16 group-quantized modular MoE experts.
Weights are int4 (packed), activations are bf16/fp16.
Internally uses int8 compute via fused_experts_cpu with INT4_W4A8.
@@ -992,55 +860,75 @@ class CPUExpertsInt4(mk.FusedMoEExpertsMonolithic):
def supports_expert_map(self) -> bool:
return False
def moe_problem_size(
self,
a1: torch.Tensor,
w1: torch.Tensor,
w2: torch.Tensor,
topk_ids: torch.Tensor,
) -> tuple[int, int, int, int, int]:
# convert_weight_packed_scale_zp blocks w1/w2 into AMX tiles
# (E, Nc, Kc, buffer_bytes), not the (E, N, K) layout the base
# implementation assumes -- N/K aren't recoverable from that shape,
# so read them 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,
router_logits: 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,
# grouped topk + fused topk bias parameters
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
) -> None:
if apply_router_weight_on_input:
raise NotImplementedError(
"CPUExpertsInt4 (W4A16) does not support "
"apply_router_weight_on_input=True. "
)
topk_weights, topk_ids = select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
use_grouped_topk=num_expert_group is not None,
top_k=self.moe_config.experts_per_token,
renormalize=self.moe_config.routing_method
in (
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
),
topk_group=topk_group,
num_expert_group=num_expert_group,
scoring_func="softmax",
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
return fused_experts_cpu(
fused_experts_cpu(
output,
hidden_states,
w1,
w2,
topk_weights,
topk_ids,
False, # inplace
CPUQuantMethod.INT4_W4A8,
self.w1_scale,
self.w2_scale,
@@ -1060,9 +948,9 @@ class CPUExpertsInt4(mk.FusedMoEExpertsMonolithic):
# ===========================================================================
class CPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
class CPUExpertsInt8(mk.FusedMoEExpertsModular):
"""CPU INT8 W8A8 per-channel weight / dynamic per-token activation
monolithic MoE experts."""
modular MoE experts."""
def __init__(
self,
@@ -1172,49 +1060,56 @@ class CPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
replace_parameter(layer, "w13_weight", w13)
replace_parameter(layer, "w2_weight", w2)
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,
router_logits: 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,
# grouped topk + fused topk bias parameters
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
topk_weights, topk_ids = select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
use_grouped_topk=num_expert_group is not None,
top_k=self.moe_config.experts_per_token,
renormalize=self.moe_config.routing_method
in (
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
),
topk_group=topk_group,
num_expert_group=num_expert_group,
scoring_func="softmax",
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
return fused_experts_cpu(
) -> 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 INT8 W8A8 model today.
raise NotImplementedError(
"CPUExpertsInt8 does not support apply_router_weight_on_input=True."
)
fused_experts_cpu(
output,
hidden_states,
w1,
w2,
topk_weights,
topk_ids,
False, # inplace
CPUQuantMethod.INT8_W8A8,
self.w1_scale,
self.w2_scale,
@@ -1229,7 +1124,7 @@ class CPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
)
class ArmCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
class ArmCPUExpertsInt8(mk.FusedMoEExpertsModular):
"""Arm INT8 MoE with per-token activation and channelwise weight quantization."""
@property
@@ -1331,48 +1226,47 @@ class ArmCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
replace_parameter(layer, "w13_weight", w13)
replace_parameter(layer, "w2_weight", w2)
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, ...]]:
# cpu_fused_moe_int8 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,
router_logits: 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,
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
topk_weights, topk_ids = select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
use_grouped_topk=num_expert_group is not None,
top_k=self.moe_config.experts_per_token,
renormalize=self.moe_config.routing_method
in (
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
),
topk_group=topk_group,
num_expert_group=num_expert_group,
scoring_func="softmax",
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
if apply_router_weight_on_input:
assert topk_ids.size(1) == 1
hidden_states.mul_(topk_weights.to(hidden_states.dtype))
) -> None:
# apply_router_weight_on_input (topk=1 only) is already applied to
# hidden_states by MoEPrepareAndFinalizeNoDPEPModular.prepare().
assert self.w1_scale is not None
assert self.w2_scale is not None
return cpu_fused_moe_int8(
cpu_fused_moe_int8(
output,
hidden_states,
w1,
w2,
@@ -1388,7 +1282,7 @@ class ArmCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
)
class ZenCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
class ZenCPUExpertsInt8(mk.FusedMoEExpertsModular):
"""AMD Zen INT8 MoE with per-token activation and channelwise weight
quantization, dispatched through zentorch."""
@@ -1424,9 +1318,7 @@ class ZenCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
if self.w2_bias is None
else self.w2_bias.detach().to(torch.bfloat16).contiguous()
)
logger.info_once(
"[zen_cpu] Using zentorch_fused_moe for W8A8 INT8 MoE (monolithic experts)"
)
logger.info_once("[zen_cpu] Using zentorch_fused_moe for W8A8 INT8 MoE")
@property
def expects_unquantized_inputs(self) -> bool:
@@ -1487,48 +1379,47 @@ class ZenCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
def supports_expert_map(self) -> bool:
return False
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, ...]]:
# zentorch_fused_moe 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,
router_logits: 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,
num_expert_group: int | None = None,
e_score_correction_bias: torch.Tensor | None = None,
routed_scaling_factor: float | None = None,
topk_group: int | None = None,
) -> torch.Tensor:
topk_weights, topk_ids = select_experts(
hidden_states=hidden_states,
router_logits=router_logits,
use_grouped_topk=num_expert_group is not None,
top_k=self.moe_config.experts_per_token,
renormalize=self.moe_config.routing_method
in (
RoutingMethodType.Renormalize,
RoutingMethodType.RenormalizeNaive,
),
topk_group=topk_group,
num_expert_group=num_expert_group,
scoring_func="softmax",
routed_scaling_factor=(
routed_scaling_factor if routed_scaling_factor is not None else 1.0
),
e_score_correction_bias=e_score_correction_bias,
)
) -> None:
if apply_router_weight_on_input:
assert topk_ids.size(1) == 1, (
"apply_router_weight_on_input is only implemented for topk=1"
)
hidden_states = hidden_states.mul(topk_weights.to(hidden_states.dtype))
output = torch.empty_like(hidden_states)
torch.ops.zentorch.zentorch_fused_moe(
output,
hidden_states,
@@ -1543,4 +1434,3 @@ class ZenCPUExpertsInt8(mk.FusedMoEExpertsMonolithic):
self._w1_scale_bf16,
self._w2_scale_bf16,
)
return output
@@ -1175,6 +1175,24 @@ class FusedMoEKernelModularImpl:
# time we need cache3, we're done with cache1.
# Reuse workspace13 for the output since there is only one chunk.
max_shape_size = max(prod(workspace13_shape), prod(fused_out_shape))
if current_platform.is_cpu():
# Every CPU FusedMoEExpertsModular kernel reports zero-sized
# workspace13/workspace2 (it manages its own scratch space
# internally) and just needs the output buffer, so skip the
# workspace manager entirely here -- it's the one part of this
# call graph Dynamo can't trace (ContextVar-based lane lookup),
# and CPU never actually needs its cross-chunk memory reuse.
common_workspace = torch.empty(
(max_shape_size,), dtype=workspace_dtype, device=device
)
workspace2 = torch.empty(
workspace2_shape, dtype=workspace_dtype, device=device
)
workspace13 = _resize_cache(common_workspace, workspace13_shape)
fused_out = _resize_cache(common_workspace, fused_out_shape)
return workspace13, workspace2, fused_out
common_workspace, workspace2 = current_workspace_manager().get_simultaneous(
((max_shape_size,), workspace_dtype),
(workspace2_shape, workspace_dtype),
@@ -348,6 +348,7 @@ def _get_priority_backends_for_gpt_oss() -> list[Mxfp4MoeBackend]:
Mxfp4MoeBackend.MARLIN,
Mxfp4MoeBackend.BATCHED_MARLIN,
Mxfp4MoeBackend.XPU,
Mxfp4MoeBackend.CPU,
Mxfp4MoeBackend.EMULATION,
]
return _AVAILABLE_BACKENDS
@@ -366,6 +367,8 @@ def _get_priority_backends() -> list[Mxfp4MoeBackend]:
]
if current_platform.is_xpu():
return [Mxfp4MoeBackend.XPU]
if current_platform.is_cpu():
return [Mxfp4MoeBackend.CPU]
_AVAILABLE_BACKENDS = [
Mxfp4MoeBackend.FLASHINFER_TRTLLM_MXFP4_MXFP8,
Mxfp4MoeBackend.DEEPGEMM_MXFP4,
@@ -627,17 +630,6 @@ def select_mxfp4_moe_backend(
activation_format,
)
if current_platform.is_cpu():
backend = Mxfp4MoeBackend.CPU
logger.info_once(_make_log_backend(backend))
return _return_or_raise(
Mxfp4MoeBackend.CPU,
config,
kMxfp4Static,
None,
activation_format,
)
unsupported_log = "; ".join(
[
f"backend: {backend.value}, reason: {reason}"
@@ -1813,11 +1805,36 @@ def convert_weight_to_mxfp4_moe_kernel_format(
w2_bias=w2_bias,
_cache_permute_indices=_cache_permute_indices,
)
elif mxfp4_backend == Mxfp4MoeBackend.CPU:
from vllm.model_executor.layers.fused_moe.experts.cpu_moe import (
prepare_mxfp4_moe_layer_for_cpu,
)
packed_w13, packed_w2, packed_w13_scale, packed_w2_scale = (
prepare_mxfp4_moe_layer_for_cpu(
w13_weight.data,
w2_weight.data,
w13_weight_scale.data,
w2_weight_scale.data,
)
)
if w13_bias is not None:
w13_bias = w13_bias.data.to(torch.float32)
if w2_bias is not None:
w2_bias = w2_bias.data.to(torch.float32)
return (
packed_w13,
packed_w2,
packed_w13_scale,
packed_w2_scale,
w13_bias,
w2_bias,
)
else:
raise ValueError(
f"Unsupported mxfp4_backend for Mxfp4MoEMethod: {mxfp4_backend}. "
"Expected TRTLLM, FlashInfer CUTLASS, Triton, AITER, XPU, or "
"emulation backend."
"Expected TRTLLM, FlashInfer CUTLASS, Triton, AITER, XPU, "
"CPU, or emulation backend."
)
@@ -0,0 +1,294 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Router for CPU MoE experts.
This is the routing logic that used to live inline in each CPU
FusedMoEExpertsMonolithic.apply() (see git history of
experts/cpu_moe.py); factoring it out into a proper FusedMoERouter is what
lets CPU MoE experts be FusedMoEExpertsModular like every other backend.
The math below is unchanged from that prior inline version -- this is a
relocation, not a rewrite.
"""
from collections.abc import Callable
import torch
from vllm._custom_ops import biased_topk_cpu, hash_topk_cpu
from vllm.distributed.eplb.eplb_state import EplbLayerState
from vllm.model_executor.layers.fused_moe.config import (
RoutingMethodType,
get_routing_method_type,
)
from vllm.model_executor.layers.fused_moe.router.base_router import BaseRouter
def _grouped_topk(
gating_output: torch.Tensor,
topk: int,
renormalize: bool,
num_expert_group: int,
topk_group: int,
scoring_func: str,
routed_scaling_factor: float,
e_score_correction_bias: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
gating_output = gating_output.float()
if scoring_func == "softmax":
scores = torch.softmax(gating_output, dim=-1)
elif scoring_func == "sigmoid":
scores = gating_output.sigmoid()
else:
raise ValueError(f"Unsupported scoring function: {scoring_func}")
num_token = scores.shape[0]
if e_score_correction_bias is not None:
original_scores = scores
scores = scores + e_score_correction_bias.unsqueeze(0)
group_scores = (
scores.view(num_token, num_expert_group, -1).topk(2, dim=-1)[0].sum(dim=-1)
)
else:
group_scores = (
scores.view(num_token, num_expert_group, -1).max(dim=-1).values
) # [n, n_group]
group_idx = torch.topk(group_scores, k=topk_group, dim=-1, sorted=False)[
1
] # [n, top_k_group]
group_mask = torch.zeros_like(group_scores) # [n, n_group]
group_mask.scatter_(1, group_idx, 1) # [n, n_group]
score_mask = (
group_mask.unsqueeze(-1)
.expand(num_token, num_expert_group, scores.shape[-1] // num_expert_group)
.reshape(num_token, -1)
) # [n, e]
tmp_scores = scores.masked_fill(~score_mask.bool(), float("-inf")) # [n, e]
if e_score_correction_bias is not None:
topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)[1]
topk_weights = original_scores.gather(1, topk_ids)
else:
topk_weights, topk_ids = torch.topk(tmp_scores, k=topk, dim=-1, sorted=False)
if renormalize:
topk_weights = topk_weights / topk_weights.sum(dim=-1, keepdim=True)
if routed_scaling_factor != 1.0:
topk_weights = topk_weights * routed_scaling_factor
return topk_weights, topk_ids.to(torch.int32)
def _sqrtsoftplus_bias_topk(
router_logits: torch.Tensor,
top_k: int,
renormalize: bool,
e_score_correction_bias: torch.Tensor | None,
routed_scaling_factor: float,
input_ids: torch.Tensor | None,
hash_indices_table: torch.Tensor | None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""DeepSeek V4's routing: weight = sqrt(softplus(logit)); expert
selection uses a bias-corrected score (weight + correction_bias), but
the returned weight for each selected expert is the *unbiased* value
(the "noaux_tc" pattern -- bias steers selection only). Always
renormalizes selected weights to sum to ``routed_scaling_factor``,
matching the reference Triton kernel
(vllm/model_executor/layers/fused_moe/router/dsv4_topk.py).
When ``hash_indices_table`` is given (hash-routed layers), expert
selection instead comes from ``hash_indices_table[input_ids]`` --
the bias-corrected-score topk is skipped entirely, matching the CUDA
reference kernel (``dsv4HashTopkSoftplusSqrt``).
Backed by the ported SGLang AVX512 kernels `biased_topk_cpu` (the
correction-bias path) and `hash_topk_cpu` (the hash-routed-layer path);
see csrc/cpu/sgl-kernels/topk.cpp.
"""
assert renormalize, "DeepseekV4 routing always renormalizes"
gating_output = router_logits.contiguous()
num_experts = gating_output.shape[1]
if hash_indices_table is not None:
assert input_ids is not None
# hash_indices_table is int32 by construction (see cpu/model.py) and
# advanced indexing always allocates a fresh contiguous tensor.
tid2eid = hash_indices_table[input_ids]
return hash_topk_cpu(
gating_output,
tid2eid,
top_k,
"sqrtsoftplus",
0, # num_fused_shared_experts: never fused into routing on CPU
num_experts,
routed_scaling_factor,
)
assert e_score_correction_bias is not None
return biased_topk_cpu(
gating_output, # only used for num_tokens/device (dtype forced fp32)
gating_output,
e_score_correction_bias.contiguous(),
top_k,
renormalize,
"sqrtsoftplus",
0, # num_fused_shared_experts: never fused into routing on CPU
routed_scaling_factor,
True, # apply_routed_scaling_factor_on_output
)
def _softmax_topk(
router_logits: torch.Tensor,
top_k: int,
renormalize: bool,
) -> tuple[torch.Tensor, torch.Tensor]:
topk_logit_vals, topk_idx = torch.topk(router_logits, k=top_k, dim=-1, sorted=False)
if renormalize:
topk_vals = torch.softmax(topk_logit_vals, dim=-1)
else:
logZ = torch.logsumexp(router_logits, dim=-1, keepdim=True)
topk_vals = (topk_logit_vals - logZ).exp()
return topk_vals.to(torch.float32), topk_idx.to(torch.int32)
class CPURouter(BaseRouter):
"""Router covering every routing scheme CPU MoE experts support: plain
softmax top-k, grouped top-k (with an optional correction bias), an
arbitrary custom_routing_function, and DeepSeek V4's sqrtsoftplus scheme
(bias-corrected or hash-routed via the ported biased_topk_cpu/
hash_topk_cpu kernels). All of the above except sqrtsoftplus are plain
torch ops so Dynamo traces straight through into them like it did when
this logic lived inline in FusedMoEExpertsMonolithic.apply() -- no new
compile boundary is introduced here.
"""
def __init__(
self,
top_k: int,
global_num_experts: int,
use_grouped_topk: bool = False,
num_expert_group: int | None = None,
topk_group: int | None = None,
renormalize: bool = True,
scoring_func: str = "softmax",
routed_scaling_factor: float = 1.0,
e_score_correction_bias: torch.Tensor | None = None,
custom_routing_function: Callable | None = None,
hash_indices_table: torch.Tensor | None = None,
eplb_state: EplbLayerState | None = None,
):
super().__init__(
top_k=top_k,
global_num_experts=global_num_experts,
eplb_state=eplb_state,
)
self.use_grouped_topk = use_grouped_topk
self.num_expert_group = num_expert_group
self.topk_group = topk_group
self.renormalize = renormalize
self.scoring_func = scoring_func
self.routed_scaling_factor = routed_scaling_factor
self.e_score_correction_bias = e_score_correction_bias
self.custom_routing_function = custom_routing_function
self._hash_indices_table = hash_indices_table
@property
def routing_method_type(self) -> RoutingMethodType:
return get_routing_method_type(
scoring_func=self.scoring_func,
top_k=self.top_k,
renormalize=self.renormalize,
num_expert_group=self.num_expert_group,
has_e_score_bias=self.e_score_correction_bias is not None,
routed_scaling_factor=self.routed_scaling_factor,
)
def _compute_routing(
self,
hidden_states: torch.Tensor,
router_logits: torch.Tensor,
indices_type: torch.dtype | None,
*,
input_ids: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
if self.use_grouped_topk:
assert self.topk_group is not None
assert self.num_expert_group is not None
return _grouped_topk(
gating_output=router_logits,
topk=self.top_k,
renormalize=self.renormalize,
num_expert_group=self.num_expert_group,
topk_group=self.topk_group,
scoring_func=self.scoring_func,
routed_scaling_factor=self.routed_scaling_factor,
e_score_correction_bias=self.e_score_correction_bias,
)
elif self.scoring_func == "sqrtsoftplus":
assert (
self._hash_indices_table is not None
or self.e_score_correction_bias is not None
)
return _sqrtsoftplus_bias_topk(
router_logits=router_logits,
top_k=self.top_k,
renormalize=self.renormalize,
e_score_correction_bias=self.e_score_correction_bias,
routed_scaling_factor=self.routed_scaling_factor,
input_ids=input_ids,
hash_indices_table=self._hash_indices_table,
)
elif self.custom_routing_function is None:
assert self.scoring_func == "softmax"
return _softmax_topk(router_logits, self.top_k, self.renormalize)
else:
topk_weights, topk_ids = self.custom_routing_function(
hidden_states=hidden_states,
gating_output=router_logits,
topk=self.top_k,
renormalize=self.renormalize,
)
# cpu_fused_moe reads routing tensors as contiguous float32/int32
# buffers and does not account for tensor strides.
topk_weights = topk_weights.to(torch.float32).contiguous()
topk_ids = topk_ids.to(torch.int32).contiguous()
return topk_weights, topk_ids
def select_experts(
hidden_states: torch.Tensor,
router_logits: torch.Tensor,
top_k: int,
use_grouped_topk: bool,
renormalize: bool,
topk_group: int | None = None,
num_expert_group: int | None = None,
custom_routing_function: Callable | None = None,
scoring_func: str = "softmax",
routed_scaling_factor: float = 1.0,
e_score_correction_bias: torch.Tensor | None = None,
input_ids: torch.Tensor | None = None,
hash_indices_table: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor]:
"""Routing helper for the CPU MoE experts that still use the monolithic
contract and so must compute routing themselves from ``router_logits``
(the zentorch-accelerated path and the ARM W4A8 dynamic-quant kernel,
kept monolithic since zentorch/Arm hardware isn't available to validate
a modular migration here). Modular CPU experts get routing from
``CPURouter`` instead; this is a thin wrapper around the same logic for
the two classes that can't use it.
"""
router = CPURouter(
top_k=top_k,
global_num_experts=router_logits.shape[-1],
use_grouped_topk=use_grouped_topk,
num_expert_group=num_expert_group,
topk_group=topk_group,
renormalize=renormalize,
scoring_func=scoring_func,
routed_scaling_factor=routed_scaling_factor,
e_score_correction_bias=e_score_correction_bias,
custom_routing_function=custom_routing_function,
hash_indices_table=hash_indices_table,
)
return router._compute_routing(
hidden_states, router_logits, None, input_ids=input_ids
)
@@ -14,6 +14,7 @@ from vllm.model_executor.layers.fused_moe.config import (
from vllm.model_executor.layers.fused_moe.router.aiter_shared_routed_fused_moe_router import ( # noqa: E501
AiterSharedRoutedFusedMoERouter,
)
from vllm.model_executor.layers.fused_moe.router.cpu_router import CPURouter
from vllm.model_executor.layers.fused_moe.router.custom_routing_router import (
CustomRoutingRouter,
)
@@ -35,6 +36,7 @@ from vllm.model_executor.layers.fused_moe.router.routing_simulator_router import
from vllm.model_executor.layers.fused_moe.router.zero_expert_router import (
ZeroExpertRouter,
)
from vllm.platforms import current_platform
def create_fused_moe_router(
@@ -141,6 +143,26 @@ def create_fused_moe_router(
routed_scaling_factor=routed_scaling_factor,
)
if current_platform.is_cpu():
# CPURouter covers every routing scheme CPU MoE experts support
# (plain softmax, grouped topk, custom routing functions, and
# DeepSeek V4's sqrtsoftplus/hash routing) in a single class, so it
# takes priority over the scheme-specific routers below.
return CPURouter(
top_k=top_k,
global_num_experts=global_num_experts,
use_grouped_topk=use_grouped_topk,
num_expert_group=num_expert_group,
topk_group=topk_group,
renormalize=renormalize,
scoring_func=scoring_func,
routed_scaling_factor=routed_scaling_factor,
e_score_correction_bias=e_score_correction_bias,
custom_routing_function=custom_routing_function,
hash_indices_table=hash_indices_table,
eplb_state=eplb_state,
)
if use_grouped_topk:
assert custom_routing_function is None
if num_expert_group is None or topk_group is None:
@@ -317,10 +317,17 @@ class MoERunner(MoERunnerInterface):
return self.routed_experts.load_weights(weights)
def _select_forward(self) -> Callable:
if current_platform.is_tpu() or current_platform.is_cpu():
if current_platform.is_tpu():
# TODO: Once the OOM issue for the TPU backend is resolved, we
# will switch to using the moe_forward custom op.
# Note: CPU doesn't require wrapped _forward_impl.
return _moe_forward if self._shared_experts is None else _moe_forward_shared
if current_platform.is_cpu():
# CPU never touches the workspace manager (Monolithic experts
# skip it entirely; Modular experts' _allocate_buffers bypasses
# it too, see modular_kernel.py) -- the ContextVar-based lane
# lookup was the only part of this call graph Dynamo can't
# trace, so CPU can always call the fused-MoE op directly.
return _moe_forward if self._shared_experts is None else _moe_forward_shared
return (
+88
View File
@@ -261,6 +261,36 @@ class MHCPreOp(CustomOp):
sinkhorn_repeat,
)
def forward_cpu(
self,
residual: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int = 1,
norm_weight: torch.Tensor | None = None,
norm_eps: float = 0.0,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
return mhc_kernels.mhc_pre_cpu(
residual,
fn,
hc_scale,
hc_base,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
n_splits,
norm_weight,
norm_eps,
)
# --8<-- [start:mhc_post]
@CustomOp.register("mhc_post")
@@ -337,6 +367,15 @@ class MHCPostOp(CustomOp):
comb_res_mix,
)
def forward_cpu(
self,
x: torch.Tensor,
residual: torch.Tensor,
post_layer_mix: torch.Tensor,
comb_res_mix: torch.Tensor,
) -> torch.Tensor:
return mhc_kernels.mhc_post_cpu(x, residual, post_layer_mix, comb_res_mix)
# --8<-- [start:hc_head]
@CustomOp.register("hc_head")
@@ -444,6 +483,19 @@ class HCHeadOp(CustomOp):
)
return out.view(*outer_shape, hidden_size)
def forward_cpu(
self,
hidden_states: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_norm_eps: float,
hc_eps: float,
) -> torch.Tensor:
return mhc_kernels.hc_head_fused_cpu(
hidden_states, hc_fn, hc_scale, hc_base, rms_norm_eps, hc_eps
)
# --8<-- [start:mhc_fused_post_pre]
@CustomOp.register("mhc_fused_post_pre")
@@ -654,6 +706,42 @@ class MHCFusedPostPreOp(CustomOp):
sinkhorn_repeat,
)
def forward_cpu(
self,
x: torch.Tensor,
residual: torch.Tensor,
post_layer_mix: torch.Tensor,
comb_res_mix: torch.Tensor,
fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
rms_eps: float,
hc_pre_eps: float,
hc_sinkhorn_eps: float,
hc_post_mult_value: float,
sinkhorn_repeat: int,
n_splits: int = 1,
tile_n: int = 1,
norm_weight: torch.Tensor | None = None,
norm_eps: float = 0.0,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
# Decompose into post + pre (no fused kernel available).
residual_cur = mhc_kernels.mhc_post_cpu(
x, residual, post_layer_mix, comb_res_mix
)
post_mix_cur, comb_mix_cur, layer_input_cur = mhc_kernels.mhc_pre_cpu(
residual_cur,
fn,
hc_scale,
hc_base,
rms_eps,
hc_pre_eps,
hc_sinkhorn_eps,
hc_post_mult_value,
sinkhorn_repeat,
)
return residual_cur, post_mix_cur, comb_mix_cur, layer_input_cur
def hc_expand(x: torch.Tensor, n: int) -> torch.Tensor:
"""[s, hidden_size] -> [s, n * hidden_size] by replication."""
@@ -828,10 +828,12 @@ class SparseAttnIndexer(CustomOp):
return self.forward_cuda(hidden_states, q_quant, k, weights)
elif current_platform.is_rocm():
return self.forward_hip(hidden_states, q_quant, k, weights)
elif current_platform.is_cpu():
return self.forward_cpu(hidden_states, q_quant, k, weights)
else:
raise NotImplementedError(
"SparseAttnIndexer native forward is only implemented for "
"CUDA, ROCm and XPU platforms."
"CUDA, ROCm, XPU and CPU platforms."
)
def forward_cuda(
@@ -919,3 +921,196 @@ class SparseAttnIndexer(CustomOp):
"Sparse attention indexer ROCm path is only supported on AITER. "
"Please enable aiter with VLLM_ROCM_USE_AITER=1"
)
def forward_cpu(
self,
hidden_states: torch.Tensor,
q_quant: torch.Tensor | tuple[torch.Tensor, torch.Tensor],
k: torch.Tensor | None,
weights: torch.Tensor,
):
"""CPU sparse attention indexer: cache write stays eager Python glue
(own K-cache layout, not shared with the main attention cache
write). PREFILL and DECODE both call the ported
``fp8_paged_mqa_logits_cpu``/``topk_transform_512_cpu`` kernels,
which read the paged K-cache directly via ``page_table`` -- no
eager gather step, no per-request Python loop.
``prefill_metadata.chunks`` always has exactly one entry here:
``DeepseekV4CPUIndexerMetadataBuilder`` overrides the base chunk
split to always return the whole step's prefill batch as one
chunk, since the base chunking only bounds CUDA/XPU's dense M*N
logits tensor and flat K-gather workspace, neither of which this
paged kernel allocates.
"""
assert not self.use_fp4_cache, (
"CPU sparse indexer doesn't support fp4 cache yet"
)
assert isinstance(q_quant, torch.Tensor), (
"CPU sparse_attn_indexer expects a single FP8 q_quant tensor"
)
assert self.dcp_world_size <= 1 and not self.use_pcp, (
"CPU sparse indexer doesn't support decode/prefill context parallelism yet."
)
forward_context = get_forward_context()
attn_metadata = forward_context.attn_metadata
attn_metadata_narrowed: DeepseekV32IndexerMetadata | None = None
if isinstance(attn_metadata, dict):
metadata = attn_metadata[self.k_cache.prefix]
assert isinstance(metadata, DeepseekV32IndexerMetadata)
attn_metadata_narrowed = metadata
if attn_metadata_narrowed is None:
# Profiling/dummy run: no real metadata to act on.
return self.topk_indices_buffer
kv_cache = self.k_cache.kv_cache
topk_tokens = self.topk_tokens
topk_indices_buffer = self.topk_indices_buffer
slot_mapping = attn_metadata_narrowed.slot_mapping
has_decode = attn_metadata_narrowed.num_decodes > 0
has_prefill = attn_metadata_narrowed.num_prefills > 0
num_decode_tokens = attn_metadata_narrowed.num_decode_tokens
num_tokens = slot_mapping.shape[0]
if k is not None:
k = k[:num_tokens]
if not self.skip_k_cache_insert:
# Only reachable via DeepseekV32Attention with
# prefill_context_parallel_size > 1 on CPU -- set_k_cpu/set_s_cpu
# (the kernels this used to call) have been removed as unused/
# untested (csrc/cpu/sgl-kernels/store_cache.cpp).
raise NotImplementedError(
"SparseAttnIndexer.forward_cpu: skip_k_cache_insert=False "
"(prefill context parallel on CPU) is not supported."
)
topk_indices_buffer[: hidden_states.shape[0]] = -1
if has_prefill:
assert topk_tokens == 512, (
"topk_transform_512_cpu only supports index_topk == 512."
)
prefill_metadata = attn_metadata_narrowed.prefill
assert prefill_metadata is not None
assert len(prefill_metadata.chunks) == 1, (
"forward_cpu expects the prefill metadata builder to always "
"produce a single chunk -- see "
"DeepseekV4CPUIndexerMetadataBuilder._split_indexer_prefill_chunks."
)
chunk = prefill_metadata.chunks[0]
# kv_cache is a per-layer view into vLLM's shared multi-layer
# cache allocation, so its block stride generally exceeds
# block_size * page_width; fp8_paged_mqa_logits_cpu reads
# kv_view.stride(0) explicitly, so no copy is needed here.
kv_view = kv_cache.view(kv_cache.shape[0], -1)
block_size = kv_cache.shape[1]
q_slice = q_quant[chunk.token_start : chunk.token_end]
topk_indices = topk_indices_buffer[
chunk.token_start : chunk.token_end, :topk_tokens
]
if chunk.local_total_seq_lens == 0:
topk_indices.fill_(-1)
else:
assert chunk.local_cu_seq_lens is not None
# Each token's own (per-request, DCP-local) causal length.
local_seq_lens = chunk.cu_seqlen_ke - chunk.cu_seqlen_ks
# Recover each token's owning request from its row-start
# tag. Ties (from zero-length requests) are harmless: those
# tokens have local length 0 and never dereference
# page_table.
req_idx = (
torch.searchsorted(
chunk.local_cu_seq_lens, chunk.cu_seqlen_ks, right=True
)
- 1
)
page_table = chunk.block_table[req_idx]
# The true max over this chunk's own rows, NOT
# chunk.max_local_total_seq_lens (that field sums every
# request's length in the chunk, bounding the old
# flat-gather buffer this paged path no longer allocates).
max_seq_len = int(local_seq_lens.max().item())
logits = ops.fp8_paged_mqa_logits_cpu(
q_slice,
kv_view,
weights[chunk.token_start : chunk.token_end],
local_seq_lens,
page_table,
block_size,
max_seq_len,
)
out_page_scratch = torch.empty(
(q_slice.shape[0], topk_tokens),
dtype=torch.int32,
device=kv_cache.device,
)
ops.topk_transform_512_cpu(
logits,
local_seq_lens,
page_table,
out_page_scratch,
block_size,
topk_indices,
)
if has_decode:
decode_metadata = attn_metadata_narrowed.decode
assert decode_metadata is not None
assert not decode_metadata.requires_padding, (
"CPU sparse indexer decode path does not support speculative "
"decoding (native MTP) yet."
)
batch_size = decode_metadata.decode_lens.shape[0]
if batch_size > 0:
# No native MTP on CPU (asserted above) => exactly one
# query token per decode request, so the flat slice below
# is already the batch-major layout
# fp8_paged_mqa_logits_cpu wants.
assert num_decode_tokens == batch_size, (
"CPU sparse indexer decode path expects exactly one query "
"token per decode request."
)
assert topk_tokens == 512, (
"topk_transform_512_cpu only supports index_topk == 512."
)
seq_lens = decode_metadata.seq_lens
seq_lens = (
seq_lens[:, -1].contiguous() if seq_lens.ndim == 2 else seq_lens
)
block_table = decode_metadata.block_table[:batch_size]
block_size = kv_cache.shape[1]
kv_view = kv_cache.view(kv_cache.shape[0], -1)
logits = ops.fp8_paged_mqa_logits_cpu(
q_quant[:num_decode_tokens],
kv_view,
weights[:num_decode_tokens],
seq_lens,
block_table,
block_size,
attn_metadata_narrowed.max_seq_len,
)
# out_page_indices is a required kernel output but unused:
# the indexer's topk output must stay local/compressed-
# context positions (resolved later by
# DeepseekV4CPUAttention.forward_mqa via
# map_local_to_global_slots_cpu).
out_page_scratch = torch.empty(
(batch_size, topk_tokens),
dtype=torch.int32,
device=kv_cache.device,
)
ops.topk_transform_512_cpu(
logits,
seq_lens,
block_table,
out_page_scratch,
block_size,
topk_indices_buffer[:num_decode_tokens, :topk_tokens],
)
return topk_indices_buffer
+7
View File
@@ -30,6 +30,13 @@ elif current_platform.is_xpu():
from .xpu.dspark import DSparkDeepseekV4ForCausalLM # type: ignore[assignment]
from .xpu.model import DeepseekV4ForCausalLM # type: ignore[assignment]
from .xpu.mtp import DeepSeekV4MTP # type: ignore[assignment]
elif current_platform.is_cpu():
from .cpu.dspark import DSparkDeepseekV4ForCausalLM # type: ignore[assignment]
from .cpu.model import DeepseekV4ForCausalLM # type: ignore[assignment]
from .cpu.mtp import DeepSeekV4MTP # type: ignore[assignment]
from .vl_stub import ( # type: ignore[assignment]
DeepseekV4ForConditionalGeneration,
)
else:
from .nvidia.dspark import ( # type: ignore[assignment]
DSparkDeepseekV4ForCausalLM,
@@ -678,6 +678,19 @@ def fused_indexer_q_rope_quant(
index_q_fp8,
index_weights_out,
)
elif current_platform.is_cpu():
from vllm._custom_ops import fused_indexer_q_rope_quant_cpu
fused_indexer_q_rope_quant_cpu(
positions.contiguous(),
index_q.to(torch.float32),
index_q_cos_sin_cache.to(torch.float32).contiguous(),
index_q_fp8,
index_weights.to(torch.float32),
index_weights_softmax_scale,
index_weights_head_scale,
index_weights_out,
)
else:
_FUSED_INDEXER_Q_ROPE_QUANT_TRITON_KERNEL(
positions,
+2
View File
@@ -0,0 +1,2 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
@@ -0,0 +1,112 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CPU DeepSeek-V4 compressor subclass and dispatch: partial-state-cache
write and the compress -> RMSNorm -> RoPE -> FP8 quant -> KV cache store
step, calling ``torch.ops._C.save_partial_states_cpu``/
``compress_norm_rope_store_cpu``/``compress_norm_rope_store_indexer_cpu``
(``csrc/cpu/sgl-kernels/compressor.cpp``) in place of triton-cpu.
Covers both the head_dim=512 main-attention compressor path and the
head_dim=128 indexer compressor path (fp8 only -- MXFP4, ``use_fp4_cache``,
never occurs on CPU: the indexer's MXFP4 cache requires a Blackwell GPU).
"""
from typing import Any, cast
import torch
from vllm._custom_ops import (
compress_norm_rope_store_cpu,
compress_norm_rope_store_indexer_cpu,
save_partial_states_cpu,
)
from vllm.forward_context import get_forward_context
from vllm.models.deepseek_v4.compressor import CompressorMetadata, DeepseekCompressor
from vllm.models.deepseek_v4.cpu.cpu_utils import map_local_to_global_slots_cpu
class DeepseekV4CPUCompressor(DeepseekCompressor):
"""CPU compressor: same state/weights as the shared base, but
``forward``'s cache-write and compress->RMSNorm->RoPE->quant->store
steps always dispatch straight to the ported CPU kernels above instead
of through the shared method's platform-dispatch chain. This class
only ever runs on CPU, and ``use_fp4_cache`` is always ``False`` here
(the indexer's MXFP4 cache requires a Blackwell GPU), so the shared
method's MXFP4/two-stage/cutedsl/triton branches never apply.
"""
_norm_weight_fp32: torch.Tensor
def cache_norm_weight_fp32(self) -> None:
"""Cache a fp32 contiguous copy of ``norm.weight`` once, right after
weight loading (see
``DeepseekV4CPUAttention.process_weights_after_loading``) -- the CPU
kernel requires fp32/contiguous but the checkpoint loads this weight
in bf16, and it never changes after loading."""
self._norm_weight_fp32 = self.norm.weight.to(torch.float32).contiguous()
def forward(
self,
kv_score: torch.Tensor,
positions: torch.Tensor,
rotary_emb,
) -> None:
kv, score = kv_score.split(
[self.coff * self.head_dim, self.coff * self.head_dim], dim=-1
)
attn_metadata = get_forward_context().attn_metadata
if not isinstance(attn_metadata, dict):
return
state_metadata = attn_metadata[self.state_cache.prefix]
assert isinstance(state_metadata, CompressorMetadata)
token_to_req_indices = state_metadata.token_to_req_indices
assert token_to_req_indices is not None
slot_mapping = state_metadata.slot_mapping
num_actual = slot_mapping.shape[0]
block_table = state_metadata.block_table
block_size = state_metadata.block_size
state_cache = self.state_cache.kv_cache
save_partial_states_cpu(
kv, score, self.ape, positions, state_cache, slot_mapping
)
cos_sin_cache = rotary_emb.cos_sin_cache
k_cache_metadata = cast(Any, attn_metadata[self.k_cache_prefix])
k_cache_layer = self._static_forward_context[self.k_cache_prefix]
kv_cache = k_cache_layer.kv_cache
assert self.head_dim in (512, 128), (
f"unsupported compressor head_dim: {self.head_dim}"
)
window = (1 + self.overlap) * self.compress_ratio
win_offsets = torch.arange(window, device=positions.device)
win_local = positions.unsqueeze(-1) - window + 1 + win_offsets
req_idx = token_to_req_indices[:num_actual]
gather_slots = map_local_to_global_slots_cpu(
win_local, req_idx, block_table, block_size
)
kv_cache_2d = kv_cache.view(kv_cache.shape[0], -1)
kv_cache_block_size = kv_cache.shape[1]
store_fn = (
compress_norm_rope_store_cpu
if self.head_dim == 512
else compress_norm_rope_store_indexer_cpu
)
store_fn(
state_cache,
gather_slots,
positions,
k_cache_metadata.slot_mapping,
self._norm_weight_fp32,
self.rms_norm_eps,
cos_sin_cache,
kv_cache_2d,
kv_cache_block_size,
self.compress_ratio,
)
+126
View File
@@ -0,0 +1,126 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CPU DeepSeek-V4 sparse-MLA backend descriptor.
DeepSeek-V4 attention runs entirely through
``DeepseekV4Attention``/``DeepseekV4CPUAttention``, never through a generic
``AttentionImpl``. ``DeepseekV4CPUSparseBackend`` exists so the CPU attention
layer has its own backend name and its own metadata-builder class: CPU is not
allowed to run triton-cpu inside the model's forward path, so
``DeepseekV4CPUFlashMLAMetadataBuilder`` overrides the shared base's
``_build_c128a_metadata`` (triton-backed in
``DeepseekV4SparseMLAMetadataBuilder``) with a plain-eager-PyTorch
reimplementation of the same per-token block-table-resolution math.
"""
import torch
from vllm.models.deepseek_v4.sparse_mla import (
DeepseekV4SparseMLABackend,
DeepseekV4SparseMLAMetadataBuilder,
)
from vllm.v1.attention.backend import CommonAttentionMetadata
from vllm.v1.attention.backends.mla.indexer import (
DeepseekV4IndexerBackend,
DeepseekV32IndexerMetadataBuilder,
)
from vllm.v1.attention.backends.mla.sparse_swa import (
_LAYER_TYPE_C4A,
_LAYER_TYPE_C128A,
_LAYER_TYPE_SWAONLY,
DeepseekSparseSWAMetadataBuilder,
)
from vllm.v1.attention.ops.flashmla import FlashMLASchedMeta
class DeepseekV4CPUFlashMLAMetadataBuilder(DeepseekV4SparseMLAMetadataBuilder):
"""CPU sparse-MLA metadata builder: same fields as the shared base, but
skips the C128A dense-topk metadata entirely -- unlike CUDA/XPU/ROCm,
``DeepseekV4CPUAttention.forward_mqa`` never reads
``c128a_global_decode_topk_indices``/``c128a_decode_topk_lens``/
``c128a_prefill_topk_indices``; it recomputes the same local top-k
directly from ``positions`` instead, so building it here (whether
eagerly or via the shared base's triton kernel) would be wasted work.
"""
def _build_c128a_metadata(
self,
cm: CommonAttentionMetadata,
req_id_per_token: torch.Tensor,
) -> dict[str, torch.Tensor | None]:
return {}
class DeepseekV4CPUSparseBackend(DeepseekV4SparseMLABackend):
@staticmethod
def get_name() -> str:
return "CPU_V4_MLA_SPARSE"
@staticmethod
def get_builder_cls() -> type[DeepseekV4CPUFlashMLAMetadataBuilder]:
return DeepseekV4CPUFlashMLAMetadataBuilder
class DeepseekV4CPUIndexerMetadataBuilder(DeepseekV32IndexerMetadataBuilder):
"""CPU indexer metadata builder: same fields as the shared base, but
prefill requests are never split into multiple chunks.
``DeepseekV32IndexerMetadataBuilder._split_indexer_prefill_chunks`` (the
shared base's default) bounds two things the CUDA/XPU indexer kernels
need: the flat-gather workspace size and the dense M*N logits tensor
those kernels allocate for a chunk. The CPU indexer
(``sparse_attn_indexer_cpu``) allocates neither -- it reads the paged
K-cache directly via ``fp8_paged_mqa_logits_cpu``/
``topk_transform_512_cpu``, with no chunk-splitting of its own.
With chunked prefill now enabled for this model on CPU (see
``CpuPlatform.check_and_update_config``'s ``amx_mla_or_dsv4_enabled``),
the total query-token count for one step is already bounded by
``max_num_batched_tokens`` -- there is nothing left here to bound, so
this always returns a single chunk spanning the whole step's prefill
batch.
"""
@staticmethod
def _split_indexer_prefill_chunks(
compressed_seq_lens_cpu: torch.Tensor,
prefill_query_lens_cpu: torch.Tensor,
workspace_size: int,
max_logits_bytes: int,
request_offset: int = 0,
) -> list[tuple[slice, slice]]:
# workspace_size/max_logits_bytes are unused: no flat-gather
# workspace or dense M*N buffer is allocated.
num_reqs = compressed_seq_lens_cpu.shape[0]
total_query_len = int(prefill_query_lens_cpu.sum().item())
return [
(
slice(request_offset, request_offset + num_reqs),
slice(0, total_query_len),
)
]
class DeepseekV4CPUIndexerBackend(DeepseekV4IndexerBackend):
@staticmethod
def get_name() -> str:
return "CPU_V4_INDEXER"
@staticmethod
def get_builder_cls() -> type[DeepseekV4CPUIndexerMetadataBuilder]:
return DeepseekV4CPUIndexerMetadataBuilder
class DeepseekV4CPUSparseSWAMetadataBuilder(DeepseekSparseSWAMetadataBuilder):
"""CPU SWA metadata builder: same fields as the shared base."""
def build_tile_scheduler(
self, num_decode_tokens: int
) -> dict[str, FlashMLASchedMeta | None]:
"""CPU never runs the FlashMLA tile-scheduler planner (that's a CUDA
C++ decode-path concern) -- always return the all-``None`` sentinel
the shared base's own CPU branch returns."""
return {
_LAYER_TYPE_SWAONLY: None,
_LAYER_TYPE_C4A: None,
_LAYER_TYPE_C128A: None,
}
+648
View File
@@ -0,0 +1,648 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CPU DeepSeek-V4 attention subclass.
``forward_mqa`` resolves SWA/compressed top-k indices to paged-cache slot
ids and calls the fused ``flash_mla_with_kvcache_cpu`` kernel. ``_o_proj``
stays eager -- not on the attention hot path.
"""
from typing import Any, cast
import torch
import torch.nn as nn
import torch.nn.functional as F
from vllm import _custom_ops as ops
from vllm.forward_context import get_forward_context
from vllm.models.deepseek_v4.attention import (
DeepseekV4Attention,
DeepseekV4Indexer,
DeepseekV4IndexerCache,
)
from vllm.models.deepseek_v4.cpu.cpu_compressor import DeepseekV4CPUCompressor
from vllm.models.deepseek_v4.cpu.cpu_mla import (
DeepseekV4CPUIndexerBackend,
DeepseekV4CPUSparseBackend,
)
from vllm.models.deepseek_v4.cpu.cpu_utils import map_local_to_global_slots_cpu
from vllm.models.deepseek_v4.sparse_mla import DeepseekV4FlashMLAMetadata
from vllm.platforms import current_platform
from vllm.utils.torch_utils import (
LayerNameType,
_encode_layer_name,
_resolve_layer_name,
direct_register_custom_op,
)
from vllm.v1.attention.backend import AttentionBackend, AttentionMetadata
from vllm.v1.attention.backends.mla.sparse_swa import DeepseekSparseSWAMetadata
_ROPE_DIM = 64
def _deepseek_v4_cpu_prepare_and_attn(
hidden_states: torch.Tensor,
qr: torch.Tensor,
kv: torch.Tensor,
qr_scale: torch.Tensor | None,
kv_score: torch.Tensor | None,
indexer_kv_score: torch.Tensor | None,
indexer_weights: torch.Tensor | None,
positions: torch.Tensor,
o_padded: torch.Tensor,
layer_name: LayerNameType,
) -> torch.Tensor:
"""Opaque custom-op boundary around attention-prep + sparse-indexer +
MLA attention + output projection -- CPU-only.
Wraps cache writes with no explicit tensor args (SWA/compressor/indexer
state), which torch.compile can't otherwise track as mutations -- one
opaque call sidesteps that. ``_o_proj`` is folded in too because CPU's
``DYNAMO_TRACE_ONCE`` mode traces exactly once, during warmup when
``attn_metadata`` is ``None``; tracing ``_o_proj`` separately would bake
that warmup branch into the compiled graph permanently, dead-code-
eliminating the whole attention computation upstream.
"""
layer_name_str = _resolve_layer_name(layer_name)
self = get_forward_context().no_compile_layers[layer_name_str]
self._prepare_and_attn_fn(
hidden_states,
qr,
kv,
qr_scale,
kv_score,
indexer_kv_score,
indexer_weights,
positions,
o_padded,
)
o = o_padded[:, : self.n_local_heads, :]
return self._o_proj(o, positions)
def _deepseek_v4_cpu_prepare_and_attn_fake(
hidden_states: torch.Tensor,
qr: torch.Tensor,
kv: torch.Tensor,
qr_scale: torch.Tensor | None,
kv_score: torch.Tensor | None,
indexer_kv_score: torch.Tensor | None,
indexer_weights: torch.Tensor | None,
positions: torch.Tensor,
o_padded: torch.Tensor,
layer_name: LayerNameType,
) -> torch.Tensor:
# hidden_states' last dim already equals _o_proj's output width, so we
# don't need to resolve layer_name (an opaque object) here.
return hidden_states.new_empty((hidden_states.shape[0], hidden_states.shape[-1]))
direct_register_custom_op(
op_name="deepseek_v4_cpu_prepare_and_attn",
op_func=_deepseek_v4_cpu_prepare_and_attn,
mutates_args=["o_padded"],
fake_impl=_deepseek_v4_cpu_prepare_and_attn_fake,
)
def _dequant_linear_weight(layer: torch.nn.Module) -> torch.Tensor:
"""Dequantize a linear layer's weight, including FP8 block scales.
``_o_proj`` reads ``wo_a.weight`` directly (not via
``quant_method.apply()``), so the per-block FP8 scale must be applied
here explicitly.
"""
weight = layer.weight
scale = getattr(layer, "weight_scale_inv", None)
if scale is None:
scale = getattr(layer, "weight_scale", None)
if scale is None:
return weight.float()
if scale.dtype == torch.float8_e8m0fnu:
from vllm.model_executor.layers.quantization.utils.fp8_utils import (
_upcast_e8m0_to_fp32,
)
scale = _upcast_e8m0_to_fp32(scale)
scale = scale.to(torch.float32)
if scale.numel() == 1:
return weight.float() * scale
block_size = getattr(layer, "weight_block_size", None)
if block_size is None:
# Per-channel scale: one row-scale per output row.
return weight.float() * scale.view(-1, 1)
block_m, block_k = block_size
m, k = weight.shape
scale = scale.repeat_interleave(block_m, dim=0).repeat_interleave(block_k, dim=1)
return weight.float() * scale[:m, :k]
def _fused_indexer_q_rope_quant_cpu(
positions: torch.Tensor,
index_q: torch.Tensor,
index_q_cos_sin_cache: torch.Tensor,
index_weights: torch.Tensor,
index_weights_softmax_scale: float,
index_weights_head_scale: float,
) -> tuple[torch.Tensor, torch.Tensor]:
"""CPU-only equivalent of ``fused_indexer_q_rope_quant``'s FP8 path
(CPU never reaches its MXFP4 arm -- Blackwell-only). Returns
``(q_fp8, weights_out)``, folding the per-token q scale into
``index_weights``. ``positions``/``index_q_cos_sin_cache`` are already
contiguous int64/fp32, so no conversion is needed here.
"""
assert positions.ndim == 1
assert index_q.ndim == 3
assert index_q_cos_sin_cache.ndim == 2
index_weights_out = torch.empty_like(index_weights, dtype=torch.float32)
index_q_fp8 = torch.empty_like(index_q, dtype=current_platform.fp8_dtype())
ops.fused_indexer_q_rope_quant_cpu(
positions,
index_q.to(torch.float32),
index_q_cos_sin_cache,
index_q_fp8,
index_weights.to(torch.float32),
index_weights_softmax_scale,
index_weights_head_scale,
index_weights_out,
)
return index_q_fp8, index_weights_out
class _NoOpEvent:
"""Stand-in for ``torch.cuda.Event`` on CPU: since CPU never sets an aux
stream list, these events are constructed but never actually used."""
class DeepseekV4CPUIndexerCache(DeepseekV4IndexerCache):
"""CPU indexer K-cache descriptor: same fields as the shared base, just
pointing ``get_attn_backend()`` at ``DeepseekV4CPUIndexerBackend``
instead of the shared, CUDA/XPU-oriented ``DeepseekV4IndexerBackend``."""
def get_attn_backend(self) -> type[AttentionBackend]:
return DeepseekV4CPUIndexerBackend
class DeepseekV4CPUIndexer(DeepseekV4Indexer):
"""CPU indexer: the C4A short-context fallback runs as eager PyTorch
instead of Triton. Never constructed directly -- ``__class__`` is
swapped onto an existing ``DeepseekV4Indexer`` instance post-construction.
"""
def forward(
self,
hidden_states: torch.Tensor,
qr: torch.Tensor,
compressed_kv_score: torch.Tensor,
indexer_weights: torch.Tensor,
positions: torch.Tensor,
rotary_emb: nn.Module,
qr_scale: torch.Tensor | None = None,
) -> tuple[torch.Tensor | None, torch.Tensor | None, torch.Tensor | None]:
"""CPU override: no aux streams, so wq_b_and_q_quant and the
compressor run straight-line instead of through
``maybe_execute_in_parallel``; the short-context check drops the
CUDA-only ``is_current_stream_capturing()`` guard (always False
here)."""
compressor = self.compressor
attn_metadata = get_forward_context().attn_metadata
if isinstance(attn_metadata, dict):
indexer_metadata = cast(Any, attn_metadata[self.k_cache.prefix])
if indexer_metadata.max_seq_len // self.compress_ratio <= self.topk_tokens:
# Fewer candidates than topk: all are selected, but the K
# cache still needs to be built.
compressor(compressed_kv_score, positions, rotary_emb)
assert self.topk_indices_buffer is not None
num_tokens = (
indexer_metadata.num_decode_tokens
+ indexer_metadata.num_prefill_tokens
)
if num_tokens > 0:
self._fill_short_context_topk(positions, num_tokens)
return None, None, None
def wq_b_and_q_quant():
q = self._wq_b_proj(qr, qr_scale)
q = q.view(-1, self.n_head, self.head_dim)
return _fused_indexer_q_rope_quant_cpu(
positions,
q,
rotary_emb.cos_sin_cache,
indexer_weights,
self.softmax_scale,
self.n_head**-0.5,
)
# Order matches the base class's parallel-join contract: compressor
# must complete before indexer_op runs (skip_k_cache_insert=True).
q, weights = wq_b_and_q_quant()
compressor(compressed_kv_score, positions, rotary_emb)
# No q_scale: FP8-only path (MXFP4 needs Blackwell).
return q, None, weights
def _fill_short_context_topk(
self, positions: torch.Tensor, num_tokens: int
) -> None:
assert self.topk_indices_buffer is not None
device = positions.device
arange = torch.arange(self.topk_tokens, device=device)
num_compressed = torch.div(
positions[:num_tokens] + 1, self.compress_ratio, rounding_mode="floor"
)
valid = arange.unsqueeze(0) < num_compressed.unsqueeze(-1)
filled = torch.where(
valid, arange.unsqueeze(0), torch.full_like(arange.unsqueeze(0), -1)
)
self.topk_indices_buffer[:num_tokens, : self.topk_tokens] = filled.to(
self.topk_indices_buffer.dtype
)
class DeepseekV4CPUAttention(DeepseekV4Attention):
"""CPU sparse MLA attention layer for DeepSeek V4."""
backend_cls = DeepseekV4CPUSparseBackend
def __init__(self, *args, **kwargs) -> None:
super().__init__(*args, **kwargs)
# Swap classes post-construction (no class-swap hook on the shared
# base) so forward()/get_attn_backend() dispatch to CPU kernels.
# SWA-only layers (compress_ratio <= 1) never build a
# compressor/indexer.
if self.compressor is not None:
self.compressor.__class__ = DeepseekV4CPUCompressor
if self.indexer is not None:
self.indexer.__class__ = DeepseekV4CPUIndexer
self.indexer.k_cache.__class__ = DeepseekV4CPUIndexerCache
self.indexer.compressor.__class__ = DeepseekV4CPUCompressor
self._wo_a_packed: torch.Tensor | None = None
self._wrap_wo_a_process_weights_after_loading()
def process_weights_after_loading(self, act_dtype: torch.dtype) -> None:
"""Cache fp32-contiguous copies of tensors CPU kernels want that way
but that arrive bf16 (rotary cos/sin table, compressor RMSNorm
weights) -- cast once here instead of per forward call.
Runs after every quantized layer's own
``process_weights_after_loading`` (see ``is_deferred_attention_layer``);
``wo_a``'s packing must happen *before* that phase instead, so it's a
separate monkeypatch in ``__init__``.
"""
self.rotary_emb.cos_sin_cache = self.rotary_emb.cos_sin_cache.to(
torch.float32
).contiguous()
if self.compressor is not None:
self.compressor.cache_norm_weight_fp32()
if self.indexer is not None:
self.indexer.compressor.cache_norm_weight_fp32()
def _wrap_wo_a_process_weights_after_loading(self) -> None:
"""Snapshot and pack ``wo_a``'s weight into a bf16 copy for
``bmm_cpu`` before the FP8 kernel's own
``process_weights_after_loading`` VNNI-repacks it in place for
row-major reads ``_o_proj`` never does."""
wo_a = self.wo_a
quant_method = wo_a.quant_method
orig_pwal = quant_method.process_weights_after_loading
heads_per_group = self.n_local_heads // self.n_local_groups
k_dim = heads_per_group * self.head_dim
def _capture_and_pack(layer: torch.nn.Module) -> None:
dequant = _dequant_linear_weight(layer).to(torch.bfloat16)
dequant = dequant.view(self.n_local_groups, self.o_lora_rank, k_dim)
self._wo_a_packed = torch.ops._C.convert_weight_packed(dequant.contiguous())
orig_pwal(layer)
# orig_pwal VNNI-repacks layer.weight/scale for ordinary-linear
# use that _o_proj never needs -- free them, same pattern as
# dispatch_cpu_unquantized_gemm's remove_weight=True.
layer.weight = torch.nn.Parameter(torch.empty(0), requires_grad=False)
scale_attr = (
"weight_scale_inv"
if hasattr(layer, "weight_scale_inv")
else "weight_scale"
)
if hasattr(layer, scale_attr):
setattr(
layer,
scale_attr,
torch.nn.Parameter(torch.empty(0), requires_grad=False),
)
quant_method.process_weights_after_loading = ( # type: ignore[method-assign]
_capture_and_pack
)
def forward(
self,
positions: torch.Tensor,
hidden_states: torch.Tensor,
llama_4_scaling: torch.Tensor | None = None,
) -> torch.Tensor:
"""CPU override: wraps attention-prep + MLA + output projection in the
CPU-only opaque custom op (see ``_deepseek_v4_cpu_prepare_and_attn``).
"""
num_tokens = hidden_states.shape[0]
o_padded = torch.empty(
(num_tokens, self.padded_heads, self.head_dim),
dtype=hidden_states.dtype,
device=hidden_states.device,
)
qr_kv = self._fused_wqa_wkv_gemm(hidden_states)
kv_score: torch.Tensor | None = None
if self.compressor is not None:
# Must go through the module's own forward (not a raw matmul on
# its weight): the packed-GEMM kernel frees the raw weight after
# packing, same as wo_a's.
kv_score = self.compressor.fused_wkv_wgate(hidden_states).to(torch.float32)
indexer_weights: torch.Tensor | None = None
indexer_kv_score: torch.Tensor | None = None
if self.indexer is not None:
indexer_weights, _ = self.indexer.weights_proj(hidden_states)
indexer_kv_score = self.indexer.compressor.fused_wkv_wgate(
hidden_states
).to(torch.float32)
qr, qr_scale, kv = self._split_qkv_and_norm(qr_kv)
return torch.ops.vllm.deepseek_v4_cpu_prepare_and_attn(
hidden_states,
qr,
kv,
qr_scale,
kv_score,
indexer_kv_score,
indexer_weights,
positions,
o_padded,
_encode_layer_name(self.prefix),
)
def _fused_qnorm_rope_kv_insert(
self,
q: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor,
attn_metadata: (
dict[str, AttentionMetadata] | list[dict[str, AttentionMetadata]] | None
),
) -> torch.Tensor:
"""CPU override: only the fp8_ds_mla (uint8) SWA cache layout is
supported here, so the base method's bf16/per-tensor-fp8 branches
are dropped."""
if not isinstance(attn_metadata, dict):
# Profile run: kernel doesn't fire; produce a padded tensor so
# downstream FlashMLA gets the right shape.
if self.n_local_heads < self.padded_heads:
return F.pad(
q,
(0, 0, 0, self.padded_heads - self.n_local_heads),
value=0.0,
)
return q
swa_metadata = cast(
"DeepseekSparseSWAMetadata | None",
attn_metadata.get(self.swa_cache_layer.prefix),
)
assert swa_metadata is not None
swa_kv_cache = self.swa_cache_layer.kv_cache
# positions is already int64 (the runner's buffer), no cast needed.
assert positions.dtype == torch.int64
assert swa_kv_cache.dtype == torch.uint8, (
"DeepseekV4CPUAttention only supports the fp8_ds_mla cache layout"
)
# Fused: Q side does per-head weight-free RMSNorm + GPT-J RoPE
# (zero-filling padded head slots); KV side does GPT-J RoPE + UE8M0
# FP8 quant + paged insert.
swa_kv_cache_2d = swa_kv_cache.view(swa_kv_cache.shape[0], -1)
return ops.fused_qnorm_rope_kv_insert_cpu(
q,
kv,
positions,
swa_kv_cache_2d,
swa_metadata.slot_mapping,
self.rotary_emb.cos_sin_cache,
self.padded_heads,
self.eps,
swa_metadata.block_size,
)
def _split_qkv_and_norm(
self, qr_kv: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor | None, torch.Tensor]:
"""CPU override: two ``RMSNorm`` calls instead of the shared
``fused_q_kv_rmsnorm``, whose raw ``@triton.jit`` kernel fails to
link under triton-cpu (``undefined symbol: __truncdfbf2``)."""
qr, kv = qr_kv.split([self.q_lora_rank, self.head_dim], dim=-1)
return self.q_norm(qr), None, self.kv_norm(kv)
def _prepare_and_attn(
self,
hidden_states: torch.Tensor,
qr: torch.Tensor,
kv: torch.Tensor,
qr_scale: torch.Tensor | None,
kv_score: torch.Tensor,
indexer_kv_score: torch.Tensor,
indexer_weights: torch.Tensor,
positions: torch.Tensor,
o_padded: torch.Tensor,
) -> None:
"""CPU override: no aux streams, so query-projection+KV-insert, the
indexer, and the compressor run straight-line instead of through
execute_in_parallel/maybe_execute_in_parallel."""
attn_metadata = get_forward_context().attn_metadata
indexer = self.indexer
compressor = self.compressor
q = self._wq_b_proj(qr, qr_scale).view(-1, self.n_local_heads, self.head_dim)
q = self._fused_qnorm_rope_kv_insert(q, kv, positions, attn_metadata)
index_q: torch.Tensor | None = None
index_q_scale: torch.Tensor | None = None
index_weights_out: torch.Tensor | None = None
if indexer is not None:
assert compressor is not None
index_q, index_q_scale, index_weights_out = indexer(
hidden_states,
qr,
indexer_kv_score,
indexer_weights,
positions,
self.indexer_rotary_emb,
qr_scale,
)
compressor(kv_score, positions, self.rotary_emb)
elif compressor is not None:
compressor(kv_score, positions, self.rotary_emb)
self._sparse_indexer_and_attn(
hidden_states,
index_q,
index_q_scale,
index_weights_out,
q,
kv,
positions,
o_padded,
)
def _sparse_indexer_and_attn(
self,
hidden_states: torch.Tensor,
index_q: torch.Tensor | None,
index_q_scale: torch.Tensor | None,
index_weights: torch.Tensor | None,
q: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor,
out: torch.Tensor,
) -> None:
"""CPU override: identical body, minus ``@eager_break_during_capture``
(this platform never captures a CUDA graph, so it was a no-op)."""
if self.indexer is not None and index_q is not None:
assert index_weights is not None
# index_q_scale is always None on CPU (FP8-only path).
assert index_q_scale is None
self.indexer.indexer_op(hidden_states, index_q, None, index_weights)
self.forward_mqa(q, kv, positions, out)
@classmethod
def get_padded_num_q_heads(cls, num_heads: int) -> int:
# No head-count padding constraint for the (not yet ported) CPU kernel.
return num_heads
def _o_proj(self, o: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
if get_forward_context().attn_metadata is None:
# Warmup/profile run: no real metadata, o is a zeroed placeholder.
return torch.zeros(
(o.shape[0], self.hidden_size), dtype=o.dtype, device=o.device
)
num_tokens, num_heads, _ = o.shape
cos_sin_cache = self.rotary_emb.cos_sin_cache
# Undo the query's own GPT-J RoPE rotation: the attention output is
# a weighted sum of KV rows at different positions, so R(-pos_q)
# recovers the translation-invariant quantity wo_a/wo_b expect.
o_derot = ops.inverse_gptj_rope_o_proj_cpu(
o, positions, cos_sin_cache, _ROPE_DIM
)
heads_per_group = num_heads // self.n_local_groups
o_grouped = o_derot.reshape(
num_tokens, self.n_local_groups, heads_per_group * self.head_dim
).to(torch.bfloat16)
assert self._wo_a_packed is not None
z = o_grouped.new_empty((self.n_local_groups, num_tokens, self.o_lora_rank))
ops.bmm_cpu(
z,
o_grouped.transpose(0, 1).contiguous(),
self._wo_a_packed,
True,
None,
)
z = z.transpose(0, 1).to(o.dtype)
return self.wo_b(z.flatten(1))
def forward_mqa(
self,
q: torch.Tensor,
kv: torch.Tensor,
positions: torch.Tensor,
output: torch.Tensor,
) -> None:
forward_context = get_forward_context()
attn_metadata = forward_context.attn_metadata
if attn_metadata is None:
# Warmup/profile run: reserve nothing extra, skip real kernels.
output.zero_()
return
assert isinstance(attn_metadata, dict)
swa_metadata = cast(
DeepseekSparseSWAMetadata,
attn_metadata.get(self.swa_cache_layer.prefix),
)
assert swa_metadata is not None
assert swa_metadata.token_to_req_indices is not None
req_idx = swa_metadata.token_to_req_indices
swa_only = self.compress_ratio <= 1
flashmla_metadata = cast(
DeepseekV4FlashMLAMetadata | None, attn_metadata.get(self.prefix)
)
num_tokens = q.shape[0]
device = q.device
# ---- SWA window: causal sliding window over the raw per-token cache
window = self.window_size
win_offsets = torch.arange(window, device=device)
start_pos = (positions - window + 1).clamp(min=0)
local_pos = start_pos.unsqueeze(-1) + win_offsets
valid_win = local_pos <= positions.unsqueeze(-1)
win_local = torch.where(valid_win, local_pos, torch.full_like(local_pos, -1))
window_slots = map_local_to_global_slots_cpu(
win_local, req_idx, swa_metadata.block_table, swa_metadata.block_size
)
window_cache_2d = self.swa_cache_layer.kv_cache.view(
self.swa_cache_layer.kv_cache.shape[0], -1
)
# ---- Compressed KV: C4A's learned top-k, or C128A's dense top-k ----
if swa_only:
compressed_slots = window_slots.new_full((num_tokens, 0), -1)
# Unused when num_compressed == 0 (only dtype/2D-ness matter);
# reuse the window cache to avoid an allocation.
compressed_cache_2d = window_cache_2d
compressed_block_size = 1
else:
assert flashmla_metadata is not None
main_block_size = flashmla_metadata.block_size // self.compress_ratio
if self.compress_ratio == 4:
assert self.topk_indices_buffer is not None
local_topk = self.topk_indices_buffer[:num_tokens]
else:
width = -(-self.max_model_len // self.compress_ratio)
num_compressed = torch.div(
positions + 1, self.compress_ratio, rounding_mode="floor"
).clamp(max=width)
arange_w = torch.arange(width, device=device)
local_topk = torch.where(
arange_w.unsqueeze(0) < num_compressed.unsqueeze(-1),
arange_w.unsqueeze(0).expand(num_tokens, width),
torch.full(
(num_tokens, width), -1, device=device, dtype=torch.int64
),
)
compressed_slots = map_local_to_global_slots_cpu(
local_topk, req_idx, flashmla_metadata.block_table, main_block_size
)
compressed_cache_2d = self.kv_cache.view(self.kv_cache.shape[0], -1)
compressed_block_size = main_block_size
torch.ops._C.flash_mla_with_kvcache_cpu(
output,
q,
window_cache_2d,
window_slots,
swa_metadata.block_size,
compressed_cache_2d,
compressed_slots,
compressed_block_size,
self.attn_sink,
self.scale,
)
+34
View File
@@ -0,0 +1,34 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""Shared helpers for DeepSeek-V4's CPU-ported kernels."""
import torch
def map_local_to_global_slots_cpu(
local_indices: torch.Tensor,
req_idx: torch.Tensor,
block_table: torch.Tensor,
block_size: int,
) -> torch.Tensor:
"""Map per-request-local (token or compressed-token) indices to global
paged-cache slot ids via a block table, mirroring
``compute_global_topk_indices_and_lens``.
Args:
local_indices: [N, K], -1 sentinel for invalid entries.
req_idx: [N], row into ``block_table`` for each of the N rows.
block_table: [num_reqs, max_blocks_per_seq].
block_size: tokens (or compressed tokens) per physical block.
Returns:
[N, K] int64 global slot ids, -1 where ``local_indices`` was invalid.
"""
valid = local_indices >= 0
safe_local = local_indices.clamp(min=0).to(torch.int64)
block_pos = torch.div(safe_local, block_size, rounding_mode="floor")
offset = safe_local % block_size
req_expand = req_idx.to(torch.int64).unsqueeze(-1).expand_as(local_indices)
block_num = block_table[req_expand, block_pos].to(torch.int64)
slot = block_num * block_size + offset
return torch.where(valid, slot, torch.full_like(slot, -1))
+18
View File
@@ -0,0 +1,18 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CPU DeepSeek-V4 DSpark (speculative decoding) — deferred, not implemented.
``DSparkDeepseekV4ForCausalLM`` is only instantiated when the speculative-
decoding registry resolves it by name for a DSpark speculative config; a
plain (non-spec) DeepSeek-V4 CPU model never constructs this class. This
stub exists solely so ``vllm.models.deepseek_v4.cpu`` is import-clean.
"""
from vllm.config import VllmConfig
class DSparkDeepseekV4ForCausalLM:
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
raise NotImplementedError(
"DeepSeek-V4 speculative decoding (DSpark) is not implemented on CPU."
)
+922
View File
@@ -0,0 +1,922 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
import typing
from collections.abc import Callable, Iterable
from itertools import islice
import regex as re
import torch
import torch.nn as nn
from vllm.compilation.decorators import support_torch_compile
from vllm.config import VllmConfig
from vllm.distributed import (
get_pp_group,
get_tensor_model_parallel_rank,
get_tensor_model_parallel_world_size,
)
from vllm.model_executor.layers.activation import SiluAndMul, SiluAndMulWithClamp
from vllm.model_executor.layers.fused_moe import (
FusedMoEFactory,
GateLinear,
fused_moe_make_expert_params_mapping,
)
from vllm.model_executor.layers.fused_moe.utils import (
is_model_fused_shared_expert_compatible,
)
from vllm.model_executor.layers.layernorm import RMSNorm
from vllm.model_executor.layers.linear import (
MergedColumnParallelLinear,
RowParallelLinear,
)
from vllm.model_executor.layers.logits_processor import LogitsProcessor
from vllm.model_executor.layers.mhc import (
HAS_AITER_MHC,
HAS_TILELANG_MHC,
HCHeadOp,
MHCFusedPostPreOp,
MHCPostOp,
MHCPreOp,
)
from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.layers.vocab_parallel_embedding import (
ParallelLMHead,
VocabParallelEmbedding,
)
from vllm.model_executor.model_loader.weight_utils import default_weight_loader
from vllm.model_executor.models.interfaces import (
EagleModelMixin,
SupportsEagle3,
SupportsPP,
)
from vllm.model_executor.models.utils import (
AutoWeightsLoader,
PPMissingLayer,
WeightsMapper,
extract_layer_index,
is_pp_missing_parameter,
make_layers,
maybe_prefix,
)
from vllm.models.deepseek_v4.cpu.cpu_sparse import DeepseekV4CPUAttention
from vllm.platforms import current_platform
from vllm.sequence import IntermediateTensors
class DeepseekV4MLP(nn.Module):
def __init__(
self,
hidden_size: int,
intermediate_size: int,
hidden_act: str,
swiglu_limit: float | None = None,
quant_config: QuantizationConfig | None = None,
reduce_results: bool = True,
is_sequence_parallel: bool = False,
prefix: str = "",
) -> None:
super().__init__()
# is_sequence_parallel: weights are replicated, no collective ops
# needed. Otherwise: standard TP with an allreduce at the end.
self.gate_up_proj = MergedColumnParallelLinear(
hidden_size,
[intermediate_size] * 2,
bias=False,
quant_config=quant_config,
disable_tp=is_sequence_parallel,
prefix=f"{prefix}.gate_up_proj",
)
self.down_proj = RowParallelLinear(
intermediate_size,
hidden_size,
bias=False,
quant_config=quant_config,
reduce_results=reduce_results,
disable_tp=is_sequence_parallel,
prefix=f"{prefix}.down_proj",
)
if hidden_act != "silu":
raise ValueError(
f"Unsupported activation: {hidden_act}. Only silu is supported for now."
)
if swiglu_limit is not None:
self.act_fn = SiluAndMulWithClamp(swiglu_limit)
else:
self.act_fn = SiluAndMul()
def forward(self, x):
gate_up, _ = self.gate_up_proj(x)
x = self.act_fn(gate_up)
x, _ = self.down_proj(x)
return x
class DeepseekV4MoE(nn.Module):
def __init__(
self,
vllm_config: VllmConfig,
prefix: str = "",
):
super().__init__()
self.tp_size = get_tensor_model_parallel_world_size()
config = vllm_config.model_config.hf_config
quant_config = vllm_config.quant_config
self.prefix = prefix
self.routed_scaling_factor = getattr(config, "routed_scaling_factor", 1.0)
self.hidden_size = config.hidden_size
self.n_routed_experts = config.n_routed_experts
self.n_activated_experts = config.num_experts_per_tok
self.moe_intermediate_size = config.moe_intermediate_size
self.swiglu_limit = config.swiglu_limit
self.renormalize = config.norm_topk_prob
self.scoring_func = getattr(config, "scoring_func", "sqrtsoftplus")
self.gate = GateLinear(
input_size=config.hidden_size,
output_size=config.n_routed_experts,
bias=False,
out_dtype=torch.float32,
prefix=f"{prefix}.gate",
)
self.gate.e_score_correction_bias = None
self.gate.tid2eid = None
is_hash_moe = extract_layer_index(prefix) < config.num_hash_layers
self.hash_indices_dtype = torch.int32
if is_hash_moe:
# hash MoE doesn't use e_score_correction_bias
# Use randint instead of empty to avoid garbage values causing
# invalid memory access in dummy mode (--load-format="dummy")
self.gate.tid2eid = nn.Parameter(
torch.randint(
0,
config.n_routed_experts,
(config.vocab_size, config.num_experts_per_tok),
dtype=self.hash_indices_dtype,
),
requires_grad=False,
)
elif getattr(config, "topk_method", None) == "noaux_tc":
self.gate.e_score_correction_bias = nn.Parameter(
torch.empty(config.n_routed_experts, dtype=torch.float32),
requires_grad=False,
)
self.n_shared_experts = config.n_shared_experts
# No CPU fused-shared-expert grouped-GEMM path yet.
self.is_fused_shared_expert_enabled = False
if config.n_shared_experts is None or self.is_fused_shared_expert_enabled:
self.shared_experts = None
else:
intermediate_size = config.moe_intermediate_size * config.n_shared_experts
self.shared_experts = DeepseekV4MLP(
hidden_size=config.hidden_size,
intermediate_size=intermediate_size,
hidden_act=config.hidden_act,
swiglu_limit=self.swiglu_limit,
quant_config=quant_config,
reduce_results=False,
prefix=f"{prefix}.shared_experts",
)
self.tp_rank = get_tensor_model_parallel_rank()
assert config.n_routed_experts % self.tp_size == 0
self.n_local_experts = config.n_routed_experts // self.tp_size
self.experts_start_idx = self.tp_rank * self.n_local_experts
self.experts_end_idx = self.experts_start_idx + self.n_local_experts
self.experts = FusedMoEFactory(
shared_experts=self.shared_experts,
n_shared_experts=(
config.n_shared_experts if self.is_fused_shared_expert_enabled else None
),
fuse_shared_experts=self.is_fused_shared_expert_enabled,
gate=self.gate,
num_experts=config.n_routed_experts,
top_k=config.num_experts_per_tok,
hidden_size=config.hidden_size,
intermediate_size=config.moe_intermediate_size,
renormalize=config.norm_topk_prob,
quant_config=quant_config,
prefix=f"{prefix}.experts",
scoring_func=self.scoring_func,
routed_scaling_factor=self.routed_scaling_factor,
e_score_correction_bias=self.gate.e_score_correction_bias,
hash_indices_table=self.gate.tid2eid,
swiglu_limit=self.swiglu_limit,
router_logits_dtype=torch.float32,
)
if is_hash_moe:
# apply_monolithic() never sees hash_indices_table via the
# router path (modular-only) -- stash it on RoutedExperts
# directly. object.__setattr__ avoids nn.Module re-registering
# (and duplicating in the state_dict) this alias.
object.__setattr__(
self.experts.routed_experts, "hash_indices_table", self.gate.tid2eid
)
def forward(
self, hidden_states: torch.Tensor, input_ids: torch.Tensor | None = None
) -> torch.Tensor:
if self.gate.tid2eid is not None and input_ids is None:
raise ValueError("DeepSeek V4 hash MoE routing requires input_ids.")
org_shape = hidden_states.shape
final_hidden_states = self.experts(
hidden_states=hidden_states,
router_logits=hidden_states,
input_ids=input_ids,
)
return final_hidden_states.view(org_shape)
class DeepseekV4DecoderLayer(nn.Module):
def __init__(
self,
vllm_config,
prefix,
topk_indices_buffer: torch.Tensor | None = None,
):
super().__init__()
# Lazy import to avoid top-level tilelang dependency.
# Registers both torch.ops.vllm.mhc_pre and mhc_post
import vllm.model_executor.layers.mhc # noqa: F401
config = vllm_config.model_config.hf_config
self.hidden_size = config.hidden_size
self.rms_norm_eps = config.rms_norm_eps
self.attn = DeepseekV4CPUAttention(
vllm_config,
prefix=f"{prefix}.attn",
topk_indices_buffer=topk_indices_buffer,
)
self.ffn = DeepseekV4MoE(vllm_config, prefix=f"{prefix}.ffn")
self.attn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
self.ffn_norm = RMSNorm(self.hidden_size, self.rms_norm_eps)
self.hc_mult = config.hc_mult
self.hc_sinkhorn_iters = config.hc_sinkhorn_iters
self.hc_eps = config.hc_eps
self.hc_post_alpha = 2.0
mix_hc = (2 + self.hc_mult) * self.hc_mult
hc_dim = self.hc_mult * self.hidden_size
self.hc_attn_fn = nn.Parameter(
torch.empty(
(mix_hc, hc_dim),
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_ffn_fn = nn.Parameter(
torch.empty(
(mix_hc, hc_dim),
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_attn_base = nn.Parameter(
torch.empty(
mix_hc,
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_ffn_base = nn.Parameter(
torch.empty(
mix_hc,
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_attn_scale = nn.Parameter(
torch.empty(
3,
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_ffn_scale = nn.Parameter(
torch.empty(
3,
dtype=torch.float32,
),
requires_grad=False,
)
self.mhc_pre = MHCPreOp()
self.mhc_post = MHCPostOp()
self.mhc_fused_post_pre = MHCFusedPostPreOp()
self.use_fused_mhc = HAS_TILELANG_MHC and not (
HAS_AITER_MHC and self.hidden_size % 256 == 0
)
def hc_pre(
self,
x: torch.Tensor,
hc_fn: torch.Tensor,
hc_scale: torch.Tensor,
hc_base: torch.Tensor,
):
post_mix, res_mix, layer_input = self.mhc_pre(
residual=x,
fn=hc_fn,
hc_scale=hc_scale,
hc_base=hc_base,
rms_eps=self.rms_norm_eps,
hc_pre_eps=self.hc_eps,
hc_sinkhorn_eps=self.hc_eps,
hc_post_mult_value=self.hc_post_alpha,
sinkhorn_repeat=self.hc_sinkhorn_iters,
)
return layer_input, post_mix, res_mix
def hc_post(
self,
x: torch.Tensor,
residual: torch.Tensor,
post: torch.Tensor,
comb: torch.Tensor,
):
return self.mhc_post(x, residual, post, comb)
def _forward_fused_post_pre(
self,
x: torch.Tensor,
positions: torch.Tensor,
input_ids: torch.Tensor | None,
post_mix: torch.Tensor | None = None,
res_mix: torch.Tensor | None = None,
residual: torch.Tensor | None = None,
) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor, torch.Tensor]:
if residual is None:
# Run standalone hc_pre on first layer
residual = x
x, post_mix, res_mix = self.hc_pre(
x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
)
else:
residual, post_mix, res_mix, x = self.mhc_fused_post_pre(
x,
residual,
post_mix,
res_mix,
self.hc_attn_fn,
self.hc_attn_scale,
self.hc_attn_base,
self.rms_norm_eps,
self.hc_eps,
self.hc_eps,
self.hc_post_alpha,
self.hc_sinkhorn_iters,
)
x = self.attn_norm(x)
x = self.attn(positions, x, None)
residual, post_mix, res_mix, x = self.mhc_fused_post_pre(
x,
residual,
post_mix,
res_mix,
self.hc_ffn_fn,
self.hc_ffn_scale,
self.hc_ffn_base,
self.rms_norm_eps,
self.hc_eps,
self.hc_eps,
self.hc_post_alpha,
self.hc_sinkhorn_iters,
)
x = self.ffn_norm(x)
x = self.ffn(x, input_ids)
return x, residual, post_mix, res_mix
def _forward_unfused_post_pre(
self,
x: torch.Tensor,
positions: torch.Tensor,
input_ids: torch.Tensor | None,
post_mix: torch.Tensor | None = None,
res_mix: torch.Tensor | None = None,
residual: torch.Tensor | None = None,
) -> tuple[
torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None
]:
residual = x
x, post, comb = self.hc_pre(
x, self.hc_attn_fn, self.hc_attn_scale, self.hc_attn_base
)
x = self.attn_norm(x)
x = self.attn(positions, x, None)
x = self.hc_post(x, residual, post, comb)
residual = x
x, post, comb = self.hc_pre(
x, self.hc_ffn_fn, self.hc_ffn_scale, self.hc_ffn_base
)
x = self.ffn_norm(x)
x = self.ffn(x, input_ids)
x = self.hc_post(x, residual, post, comb)
return x, None, None, None
def forward(
self,
x: torch.Tensor,
positions: torch.Tensor,
input_ids: torch.Tensor | None,
post_mix: torch.Tensor | None = None,
res_mix: torch.Tensor | None = None,
residual: torch.Tensor | None = None,
) -> tuple[
torch.Tensor, torch.Tensor | None, torch.Tensor | None, torch.Tensor | None
]:
if not self.use_fused_mhc:
return self._forward_unfused_post_pre(
x, positions, input_ids, post_mix, res_mix, residual
)
return self._forward_fused_post_pre(
x, positions, input_ids, post_mix, res_mix, residual
)
@support_torch_compile(
dynamic_arg_dims={
"input_ids": 0,
"positions": -1,
"intermediate_tensors": 0,
"inputs_embeds": 0,
}
)
class DeepseekV4Model(nn.Module, EagleModelMixin):
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
quant_config = vllm_config.quant_config
self.config = config
self.vocab_size = config.vocab_size
self.hc_eps = config.hc_eps
self.hc_mult = config.hc_mult
self.hc_dim = self.hc_mult * config.hidden_size
self.rms_norm_eps = config.rms_norm_eps
self.device = current_platform.device_type
# Reserved topk indices buffer for all Indexer layers to reuse.
self.topk_indices_buffer = torch.empty(
vllm_config.scheduler_config.max_num_batched_tokens,
config.index_topk,
dtype=torch.int32,
device=self.device,
)
if get_pp_group().is_first_rank:
self.embed_tokens = VocabParallelEmbedding(
config.vocab_size,
config.hidden_size,
quant_config=quant_config,
prefix=f"{prefix}.embed_tokens",
)
else:
self.embed_tokens = PPMissingLayer()
self.start_layer, self.end_layer, self.layers = make_layers(
config.num_hidden_layers,
lambda prefix: DeepseekV4DecoderLayer(
vllm_config,
prefix=prefix,
topk_indices_buffer=self.topk_indices_buffer,
),
prefix=f"{prefix}.layers",
)
self.is_fused_shared_expert_enabled = is_model_fused_shared_expert_compatible(
self.layers,
DeepseekV4MoE,
"ffn",
)
if get_pp_group().is_last_rank:
self.norm = RMSNorm(config.hidden_size, self.rms_norm_eps)
else:
self.norm = PPMissingLayer()
self.hc_head_fn = nn.Parameter(
torch.empty(
self.hc_mult,
self.hc_dim,
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_head_base = nn.Parameter(
torch.empty(
self.hc_mult,
dtype=torch.float32,
),
requires_grad=False,
)
self.hc_head_scale = nn.Parameter(
torch.empty(1, dtype=torch.float32),
requires_grad=False,
)
self.hc_head_op = HCHeadOp()
spec_config = vllm_config.speculative_config
needs_mtp_hidden_states = spec_config is not None and (
spec_config.use_eagle() or spec_config.uses_draft_model()
)
if get_pp_group().is_last_rank and needs_mtp_hidden_states:
self._mtp_hidden_buffer = torch.empty(
vllm_config.scheduler_config.max_num_batched_tokens,
self.hc_dim,
dtype=vllm_config.model_config.dtype,
device=self.device,
)
else:
self._mtp_hidden_buffer = None
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.embed_tokens(input_ids)
def make_empty_intermediate_tensors(
self,
batch_size: int,
dtype: torch.dtype,
device: torch.device,
) -> IntermediateTensors:
# PP intermediate tensors keep the (num_tokens, hc_mult, hidden_size)
# multi-stream shape until hc_head() collapses it.
return IntermediateTensors(
{
"hidden_states": torch.zeros(
(batch_size, self.hc_mult, self.config.hidden_size),
dtype=dtype,
device=device,
),
}
)
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor | IntermediateTensors:
if get_pp_group().is_first_rank:
if inputs_embeds is not None:
hidden_states = inputs_embeds
else:
hidden_states = self.embed_input_ids(input_ids)
hidden_states = hidden_states.unsqueeze(-2).repeat(1, self.hc_mult, 1)
else:
assert intermediate_tensors is not None
hidden_states = intermediate_tensors["hidden_states"]
residual, post_mix, res_mix = None, None, None
# EAGLE3/DSpark/DFlash aux hidden states: post-mhc hidden state at
# each target layer, averaged over hc_mult streams to [T,
# hidden_size]. Empty unless a draft model set aux_hidden_state_layers.
aux_hidden_states: list[torch.Tensor] = []
# Reused below to avoid computing hc_post twice if the last layer is
# also an aux layer.
final_aux_recon: torch.Tensor | None = None
for idx, layer in enumerate(
islice(self.layers, self.start_layer, self.end_layer),
start=self.start_layer,
):
hidden_states, residual, post_mix, res_mix = layer(
hidden_states,
positions,
input_ids,
post_mix,
res_mix,
residual,
)
if (idx + 1) in self.aux_hidden_state_layers:
# Unfused path already applied hc_post; fused path needs it
# applied here before averaging.
if layer.use_fused_mhc:
aux_recon = layer.hc_post(
hidden_states, residual, post_mix, res_mix
)
final_aux_recon = aux_recon
else:
aux_recon = hidden_states
aux_hidden_states.append(aux_recon.mean(dim=1))
if layer is not None and layer.use_fused_mhc:
# Reuse the last layer's hc_post output if already computed above.
if (
final_aux_recon is not None
and self.end_layer in self.aux_hidden_state_layers
):
hidden_states = final_aux_recon
else:
hidden_states = layer.hc_post(
hidden_states, residual, post_mix, res_mix
)
if not get_pp_group().is_last_rank:
return IntermediateTensors({"hidden_states": hidden_states})
if self._mtp_hidden_buffer is not None:
num_tokens = hidden_states.shape[0]
self._mtp_hidden_buffer[:num_tokens].copy_(hidden_states.flatten(1))
hidden_states = self.hc_head_op(
hidden_states,
self.hc_head_fn,
self.hc_head_scale,
self.hc_head_base,
self.rms_norm_eps,
self.hc_eps,
)
hidden_states = self.norm(hidden_states)
if len(aux_hidden_states) > 0:
return hidden_states, aux_hidden_states
return hidden_states
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
stacked_params_mapping = [
# (param_name, shard_name, shard_id)
("gate_up_proj", "w1", 0),
("gate_up_proj", "w3", 1),
("attn.fused_wqa_wkv", "attn.wq_a", 0),
("attn.fused_wqa_wkv", "attn.wkv", 1),
("compressor.fused_wkv_wgate", "compressor.wkv", 0),
("compressor.fused_wkv_wgate", "compressor.wgate", 1),
]
params_dict = dict(self.named_parameters())
loaded_params: set[str] = set()
def _resolve_param_name(name: str) -> str:
inv_name = f"{name}_inv"
if name not in params_dict and inv_name in params_dict:
return inv_name
return name
# TP for attention
tp_size = get_tensor_model_parallel_world_size()
tp_rank = get_tensor_model_parallel_rank()
n_head = self.config.num_attention_heads
n_local_head = n_head // tp_size
head_rank_start = n_local_head * tp_rank
head_rank_end = n_local_head * (tp_rank + 1)
# Pre-compute expert mapping ONCE.
expert_mapping = self.get_expert_mapping()
# Each MoE's own per-layer fusion decision is the source of truth,
# so the redirect below can't diverge from how it was built.
fuse_by_layer = {
extract_layer_index(mod_name): mod.is_fused_shared_expert_enabled
for mod_name, mod in self.named_modules()
if isinstance(mod, DeepseekV4MoE)
}
n_routed = self.config.n_routed_experts
# Maps the single shared-expert tensor group to one appended slot;
# multiple shared experts would need per-expert slicing (see
# deepseek_v2.py). DeepSeek-V4 has n_shared_experts == 1.
if any(fuse_by_layer.values()) and self.config.n_shared_experts != 1:
raise NotImplementedError(
"deepseek-v4 fused shared-expert loading supports only "
f"n_shared_experts == 1, got {self.config.n_shared_experts}"
)
for name, loaded_weight in weights:
# Shared-expert fusion: redirect shared_experts.w{1,2,3} into
# the appended routed-expert slot so it loads through the
# routed expert (grouped GEMM) loader.
if ".ffn.shared_experts.w" in name and fuse_by_layer.get(
extract_layer_index(name), False
):
name = name.replace(
".ffn.shared_experts.w",
f".ffn.experts.{n_routed}.w",
)
for param_name, weight_name, shard_id in stacked_params_mapping:
# Skip non-stacked layers and experts (experts handled below).
if ".experts." in name:
continue
if weight_name not in name:
continue
name = name.replace(weight_name, param_name)
if is_pp_missing_parameter(name, self):
break
param_name = _resolve_param_name(name)
param = params_dict[param_name]
weight_loader = param.weight_loader
weight_loader(param, loaded_weight, shard_id)
loaded_params.add(param_name)
break
else:
if ".experts." in name:
# E8M0 scales are float8_e8m0fnu in checkpoints but the
# MoE param is uint8; copy_() would numerically convert
# (e.g. 2^-7 -> 0) instead of preserving raw bytes.
if (
"weight_scale" in name
and loaded_weight.dtype == torch.float8_e8m0fnu
):
loaded_weight = loaded_weight.view(torch.uint8)
for mapping in expert_mapping:
param_name, weight_name, expert_id, expert_shard_id = mapping
if weight_name not in name:
continue
name_mapped = name.replace(weight_name, param_name)
if is_pp_missing_parameter(name_mapped, self):
continue
param = params_dict[name_mapped]
# return_success lets us skip experts that have
# another replica available.
weight_loader = typing.cast(
Callable[..., bool], param.weight_loader
)
success = weight_loader(
param,
loaded_weight,
name_mapped,
shard_id=expert_shard_id,
expert_id=expert_id,
return_success=True,
)
if success:
name = name_mapped
break
loaded_params.add(name_mapped)
continue
elif "attn_sink" in name:
if is_pp_missing_parameter(name, self):
continue
narrow_weight = loaded_weight[head_rank_start:head_rank_end]
n = narrow_weight.shape[0]
params_dict[name][:n].copy_(narrow_weight)
loaded_params.add(name)
continue
else:
if is_pp_missing_parameter(name, self):
continue
param_name = _resolve_param_name(name)
param = params_dict[param_name]
weight_loader = getattr(
param, "weight_loader", default_weight_loader
)
weight_loader(param, loaded_weight)
loaded_params.add(param_name)
continue
return loaded_params
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
# (param_name, weight_name, expert_id, shard_id). When fusing shared
# experts, include the appended slots so redirected shared-expert
# weights route through the expert loader.
n_shared = getattr(self.config, "n_shared_experts", 0) or 0
num_experts = self.config.n_routed_experts + (
n_shared if self.is_fused_shared_expert_enabled else 0
)
return fused_moe_make_expert_params_mapping(
self,
ckpt_gate_proj_name="w1",
ckpt_down_proj_name="w2",
ckpt_up_proj_name="w3",
num_experts=num_experts,
)
def _make_deepseek_v4_weights_mapper(
expert_dtype: str, fuse_shared_experts: bool = False
) -> WeightsMapper:
if expert_dtype == "fp4":
# MXFP4 experts register scales as ``w{1,2,3}_weight_scale`` (no
# _inv); FP8 linear/shared-expert scales register as
# ``weight_scale_inv``. Route DeepSeek-native ``.scale`` and
# AMD-Quark ``.weight_scale`` keys accordingly, leaving expert/
# shared-expert ``w{1,2,3}.weight_scale`` untouched for the MXFP4
# loader.
scale_regex = {
re.compile(r"(\.experts\.\d+\.w[123])\.scale$"): r"\1.weight_scale",
re.compile(r"\.scale$"): ".weight_scale_inv",
re.compile(r"(?<!\.w[123])\.weight_scale$"): ".weight_scale_inv",
}
else:
# FP8 experts use Fp8MoEMethod (block_quant=True), which registers
# scales as ``w{13,2}_weight_scale_inv``. Map all ``.scale`` keys
# there.
scale_regex = {
re.compile(r"\.scale$"): ".weight_scale_inv",
}
# Fused shared experts are redirected to routed expert slots elsewhere;
# leave their names untouched here.
substr_map: dict[str, str | None] = (
{}
if fuse_shared_experts
else {".shared_experts.w2": ".shared_experts.down_proj"}
)
# No MTP support on CPU yet: drop mtp.* weights (no model.mtp submodule).
substr_map["mtp."] = None
return WeightsMapper(
orig_to_new_prefix={
"layers.": "model.layers.",
"embed.": "model.embed.",
"norm.": "model.norm.",
"hc_head": "model.hc_head",
"mtp.": "model.mtp.",
},
orig_to_new_regex=scale_regex,
orig_to_new_suffix={
"head.weight": "lm_head.weight",
"embed.weight": "embed_tokens.weight",
".ffn.gate.bias": ".ffn.gate.e_score_correction_bias",
".input_scale": ".input_scale_2",
},
orig_to_new_substr=substr_map,
)
class DeepseekV4ForCausalLM(nn.Module, SupportsPP, SupportsEagle3):
model_cls = DeepseekV4Model
# Default mapper assumes the original FP4-expert checkpoint layout.
# Overridden per-instance in __init__ when expert_dtype != "fp4".
hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper("fp4")
packed_modules_mapping = {
"gate_up_proj": ["w1", "w3"],
"fused_wqa_wkv": ["wq_a", "wkv"],
}
def __init__(self, *, vllm_config: VllmConfig, prefix: str = ""):
super().__init__()
config = vllm_config.model_config.hf_config
self.config = config
expert_dtype = getattr(config, "expert_dtype", "fp4")
self.model = self.model_cls(
vllm_config=vllm_config, prefix=maybe_prefix(prefix, "model")
)
if expert_dtype != "fp4" or self.model.is_fused_shared_expert_enabled:
self.hf_to_vllm_mapper = _make_deepseek_v4_weights_mapper(
expert_dtype,
fuse_shared_experts=self.model.is_fused_shared_expert_enabled,
)
if get_pp_group().is_last_rank:
self.lm_head = ParallelLMHead(
config.vocab_size,
config.hidden_size,
prefix=maybe_prefix(prefix, "lm_head"),
)
else:
self.lm_head = PPMissingLayer()
self.logits_processor = LogitsProcessor(config.vocab_size)
self.make_empty_intermediate_tensors = ( # type: ignore[method-assign]
self.model.make_empty_intermediate_tensors
)
def embed_input_ids(self, input_ids: torch.Tensor) -> torch.Tensor:
return self.model.embed_input_ids(input_ids)
def compute_logits(
self,
hidden_states: torch.Tensor,
) -> torch.Tensor | None:
logits = self.logits_processor(self.lm_head, hidden_states)
return logits
def forward(
self,
input_ids: torch.Tensor,
positions: torch.Tensor,
intermediate_tensors: IntermediateTensors | None = None,
inputs_embeds: torch.Tensor | None = None,
) -> torch.Tensor | IntermediateTensors:
hidden_states = self.model(
input_ids, positions, intermediate_tensors, inputs_embeds
)
return hidden_states
def get_mtp_target_hidden_states(self) -> torch.Tensor | None:
"""Pre-hc_head residual stream buffer (max_num_batched_tokens,
hc_mult * hidden_size) for the MTP draft model. Populated by
forward(); valid after each target step."""
return getattr(self.model, "_mtp_hidden_buffer", None)
def load_weights(self, weights: Iterable[tuple[str, torch.Tensor]]) -> set[str]:
loader = AutoWeightsLoader(self)
return loader.load_weights(weights, mapper=self.hf_to_vllm_mapper)
def process_weights_after_loading(self) -> None:
# No CPU AMX weight-packing hook yet.
pass
def get_expert_mapping(self) -> list[tuple[str, str, int, str]]:
return self.model.get_expert_mapping()
+19
View File
@@ -0,0 +1,19 @@
# SPDX-License-Identifier: Apache-2.0
# SPDX-FileCopyrightText: Copyright contributors to the vLLM project
"""CPU DeepSeek-V4 MTP (speculative decoding) — deferred, not implemented.
``DeepSeekV4MTP`` is only instantiated when
``vllm/model_executor/models/registry.py``'s speculative-decoding registry
resolves it by name for a speculative-decoding config; a plain (non-spec)
DeepSeek-V4 CPU model never constructs this class. This stub exists solely
so ``vllm.models.deepseek_v4.cpu`` is import-clean.
"""
from vllm.config import VllmConfig
class DeepSeekV4MTP:
def __init__(self, *, vllm_config: VllmConfig, prefix: str = "") -> None:
raise NotImplementedError(
"DeepSeek-V4 speculative decoding (MTP) is not implemented on CPU."
)
+46 -4
View File
@@ -203,14 +203,26 @@ class CpuPlatform(Platform):
cache_config = vllm_config.cache_config
is_deepseek_v4 = (
model_config is not None
and getattr(model_config.hf_config, "model_type", None) == "deepseek_v4"
)
# The CPU MLA decode kernel only compiles with block_size=16 today
# (see csrc/cpu/mla_decode.cpp). If the model uses MLA we override
# the default block size regardless of user preference to avoid a
# runtime kernel dispatch failure. AMX MLA has no such constraint
# (same AMX-available condition as get_attn_backend_cls), so it's
# excluded from this override.
cpu_mla_enabled = model_config is not None and getattr(
model_config, "use_mla", False
# excluded from this override. DeepSeek-V4 is also excluded here and
# handled in its own branch below: its sparse-MLA and indexer
# backends declare block_size=256 as their only supported kernel
# block size (DeepseekV4SparseMLABackend/DeepseekV4IndexerBackend),
# so it needs the same override-regardless-of-preference treatment
# as CPU MLA, just with a different value.
cpu_mla_enabled = (
not is_deepseek_v4
and model_config is not None
and getattr(model_config, "use_mla", False)
)
amx_mla_enabled = (
cpu_mla_enabled
@@ -219,6 +231,21 @@ class CpuPlatform(Platform):
and vllm_config.attention_config.backend != AttentionBackendEnum.CPU_MLA
)
reference_cpu_mla_enabled = cpu_mla_enabled and not amx_mla_enabled
# DeepSeek-V4's CPU attention/indexer kernels
# (csrc/cpu/sgl-kernels/{flash_mla,store_cache,compressor,
# paged_mqa_logits,topk}.cpp) are AMX-kernel-backed and built on the
# same paged/position-indexed conventions as the GPU/XPU backends
# (block_table/slot_mapping addressing throughout, chunk metadata
# that already carries per-token causal offsets for partial/extend
# continuation) -- so they support chunked prefill and prefix
# caching the same way. `amx_mla_enabled` above deliberately
# excludes DeepSeek-V4 (it has its own block-size requirements,
# unrelated to chunked-prefill support), so it can't be reused here.
amx_mla_or_dsv4_enabled = amx_mla_enabled or (
is_deepseek_v4
and cls.get_cpu_architecture() == CpuArchEnum.X86
and torch.cpu._is_amx_tile_supported()
)
if reference_cpu_mla_enabled:
if cache_config.user_specified_block_size and cache_config.block_size != 16:
logger.warning(
@@ -227,6 +254,17 @@ class CpuPlatform(Platform):
cache_config.block_size,
)
cache_config.block_size = 16
elif is_deepseek_v4:
if (
cache_config.user_specified_block_size
and cache_config.block_size != 256
):
logger.warning(
"DeepSeek-V4 CPU backend requires block_size=256, "
"overriding user-specified block_size=%s.",
cache_config.block_size,
)
cache_config.block_size = 256
elif not cache_config.user_specified_block_size:
cache_config.block_size = 128
@@ -439,7 +477,11 @@ class CpuPlatform(Platform):
vllm_config.parallel_config.tensor_parallel_size
)
if model_config is not None and model_config.use_mla and not amx_mla_enabled:
if (
model_config is not None
and model_config.use_mla
and not amx_mla_or_dsv4_enabled
):
logger.info_once(
"MLA is enabled on a non-GPU platform; forcing chunked "
"prefill and prefix caching to be disabled."
+58 -50
View File
@@ -177,55 +177,6 @@ class PrepareUniformDecodeKernel(
)
def split_indexer_prefill_chunks(
seq_lens_cpu: torch.Tensor,
query_lens_cpu: torch.Tensor,
workspace_size: int,
max_logits_bytes: int,
request_offset: int = 0,
) -> list[tuple[slice, slice]]:
"""
Split prefill requests into chunks for the sparse indexer, respecting:
- N constraint: total_seq_lens <= workspace_size (existing O(N) workspace)
- Logits constraint: M * N * 4 <= max_logits_bytes
When a single request-level chunk still exceeds the logits budget,
sub-chunks on the query dimension (M) to bound peak memory.
Returns list of (req_slice, query_slice) tuples.
"""
chunks: list[tuple[slice, slice]] = []
n = len(seq_lens_cpu)
max_logits_elems = max_logits_bytes // 4
end = 0
while end < n:
start, chunk_m, chunk_n = end, 0, 0
while end < n:
q, s = query_lens_cpu[end].item(), seq_lens_cpu[end].item()
new_m, new_n = chunk_m + q, chunk_n + s
if new_n <= workspace_size and new_m * new_n <= max_logits_elems:
chunk_m, chunk_n = new_m, new_n
end += 1
else:
break
# A single request can exceed the budget, requiring sub-chunking
# on the query dimension.
if end == start:
chunk_m, chunk_n = query_lens_cpu[end].item(), seq_lens_cpu[end].item()
end += 1
req_slice = slice(start + request_offset, end + request_offset)
max_q = max(1, max_logits_elems // chunk_n) if chunk_n > 0 else max(1, chunk_m)
for q_off in range(0, chunk_m, max_q):
sub_m = min(max_q, chunk_m - q_off)
chunks.append((req_slice, slice(q_off, q_off + sub_m)))
return chunks
class DeepseekV32IndexerBackend(AttentionBackend):
@classmethod
def supports_pcp(cls) -> bool:
@@ -1020,6 +971,63 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
)
return indices
@staticmethod
def _split_indexer_prefill_chunks(
compressed_seq_lens_cpu: torch.Tensor,
prefill_query_lens_cpu: torch.Tensor,
workspace_size: int,
max_logits_bytes: int,
request_offset: int = 0,
) -> list[tuple[slice, slice]]:
"""Split this step's prefill requests into chunks, respecting:
- N constraint: total_seq_lens <= workspace_size (existing O(N)
workspace)
- Logits constraint: M * N * 4 <= max_logits_bytes
When a single request-level chunk still exceeds the logits budget,
sub-chunks on the query dimension (M) to bound peak memory.
Returns list of (req_slice, query_slice) tuples.
"""
chunks: list[tuple[slice, slice]] = []
n = len(compressed_seq_lens_cpu)
max_logits_elems = max_logits_bytes // 4
end = 0
while end < n:
start, chunk_m, chunk_n = end, 0, 0
while end < n:
q, s = (
prefill_query_lens_cpu[end].item(),
compressed_seq_lens_cpu[end].item(),
)
new_m, new_n = chunk_m + q, chunk_n + s
if new_n <= workspace_size and new_m * new_n <= max_logits_elems:
chunk_m, chunk_n = new_m, new_n
end += 1
else:
break
# A single request can exceed the budget, requiring sub-chunking
# on the query dimension.
if end == start:
chunk_m, chunk_n = (
prefill_query_lens_cpu[end].item(),
compressed_seq_lens_cpu[end].item(),
)
end += 1
req_slice = slice(start + request_offset, end + request_offset)
max_q = (
max(1, max_logits_elems // chunk_n) if chunk_n > 0 else max(1, chunk_m)
)
for q_off in range(0, chunk_m, max_q):
sub_m = min(max_q, chunk_m - q_off)
chunks.append((req_slice, slice(q_off, q_off + sub_m)))
return chunks
def build(
self,
common_prefix_len: int,
@@ -1097,7 +1105,7 @@ class DeepseekV32IndexerMetadataBuilder(AttentionMetadataBuilder):
# slice below).
assert common_attn_metadata.seq_lens_cpu_upper_bound is not None
seq_lens_cpu = common_attn_metadata.seq_lens_cpu_upper_bound
chunk_specs = split_indexer_prefill_chunks(
chunk_specs = self._split_indexer_prefill_chunks(
compressed_seq_lens_cpu[num_decodes:],
prefill_query_lens_cpu,
self.max_prefill_buffer_size,
@@ -148,6 +148,12 @@ class DeepseekSparseSWABackend(AttentionBackend):
)
return DeepseekV4ROCMAiterSparseSWAMetadataBuilder
if current_platform.is_cpu():
from vllm.models.deepseek_v4.cpu.cpu_mla import (
DeepseekV4CPUSparseSWAMetadataBuilder,
)
return DeepseekV4CPUSparseSWAMetadataBuilder
return DeepseekSparseSWAMetadataBuilder