[Model][DSv4.1] Store the whole KV in MXFP8 (FlashMLA V4.1 record) (#56893)

Signed-off-by: Yongye Zhu <yongye@inferact.ai>
Signed-off-by: Yongye Zhu <zyy1102000@gmail.com>
Co-authored-by: Yongye Zhu <yongye@inferact.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
Yongye Zhu
2026-09-14 20:12:55 -07:00
committed by GitHub
co-authored by Yongye Zhu Claude Opus 5
parent a529c1a748
commit e6eb0d120c
14 changed files with 709 additions and 110 deletions
+59 -26
View File
@@ -19,7 +19,7 @@ else()
FetchContent_Declare(
flashmla
GIT_REPOSITORY https://github.com/vllm-project/FlashMLA
GIT_TAG 6bc49418c5ead572ff0339191ddf3b155749e183
GIT_TAG 0eee43b12f034b657133cf2afca6a72ebb6efccf
GIT_PROGRESS TRUE
CONFIGURE_COMMAND ""
BUILD_COMMAND ""
@@ -76,43 +76,77 @@ if(FLASH_MLA_ARCHS)
set(FlashMLA_SOURCES
${flashmla_SOURCE_DIR}/csrc/api/api.cpp
${flashmla_SOURCE_DIR}/csrc/api/sparse_prefill.cpp
${flashmla_SOURCE_DIR}/csrc/api/sparse_decode.cpp
${flashmla_SOURCE_DIR}/csrc/api/dense_decode.cpp
${flashmla_SOURCE_DIR}/csrc/api/fused_norm_rope_attn_rope_cast_fwd.cpp
# Misc kernels for decoding
${flashmla_SOURCE_DIR}/csrc/smxx/decode/get_decoding_sched_meta/get_decoding_sched_meta.cu
${flashmla_SOURCE_DIR}/csrc/smxx/decode/combine/combine.cu
${flashmla_SOURCE_DIR}/csrc/kernels/smxx/decode/get_decoding_sched_meta/get_decoding_sched_meta.cu
${flashmla_SOURCE_DIR}/csrc/kernels/smxx/decode/combine/combine.cu
# sm90 dense decode
${flashmla_SOURCE_DIR}/csrc/sm90/decode/dense/instantiations/fp16.cu
${flashmla_SOURCE_DIR}/csrc/sm90/decode/dense/instantiations/bf16.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/dense/instantiations/fp16.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/dense/instantiations/bf16.cu
# sm90 sparse decode
${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/model1_persistent_h64.cu
${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/model1_persistent_h128.cu
${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/v32_persistent_h64.cu
${flashmla_SOURCE_DIR}/csrc/sm90/decode/sparse_fp8/instantiations/v32_persistent_h128.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v4_persistent_h64.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v4_persistent_h128.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v32_persistent_h64.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/decode/sparse/instantiations/v32_persistent_h128.cu
# sm90 sparse prefill
${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/fwd.cu
${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k512.cu
${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k512_topklen.cu
${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k576.cu
${flashmla_SOURCE_DIR}/csrc/sm90/prefill/sparse/instantiations/phase1_k576_topklen.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k512.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k512_topklen.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k576.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm90/prefill/sparse/instantiations/phase1_k576_topklen.cu
# sm100 dense prefill & backward
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/dense/fmha_cutlass_fwd_sm100.cu
# sm100 dense prefill (inference-only: dense backward stays out)
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/dense/fmha_cutlass_fwd_sm100.cu
# sm100 sparse prefill
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head64/instantiations/phase1_k512.cu
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head64/instantiations/phase1_k576.cu
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k512.cu
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k576.cu
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_prefill_k512.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head64/instantiations/phase1_h64_k512.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head64/instantiations/phase1_h64_k576.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k512.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd/head128/instantiations/phase1_k576.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_k512.cu
# sm100 fused norm + rope + attn + rope + cast
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_prefill_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_prefill_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_prefill_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_prefill_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_decode_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h64_decode_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_decode_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v4_h128_decode_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h64_decode_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h64_decode_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h128_decode_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41_h128_decode_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h64_decode_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h64_decode_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h128_decode_norm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/core_attn/instantiations/v41fp4_h128_decode_nonorm.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/permute_q_b_proj/kernel.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fused_norm_rope_attn_rope_cast_fwd/permute_wv_proj/kernel.cu
# sm100 sparse decode
${flashmla_SOURCE_DIR}/csrc/sm100/decode/head64/instantiations/v32.cu
${flashmla_SOURCE_DIR}/csrc/sm100/decode/head64/instantiations/model1.cu
${flashmla_SOURCE_DIR}/csrc/sm100/decode/head64/instantiations/v32_nvfp4_fp8rope.cu
${flashmla_SOURCE_DIR}/csrc/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v32_h64.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v32_h64_no_split.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v4_h64.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v4_h64_no_split.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41_h64.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41_h64_no_split.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41fp4_h64.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/head64/instantiations/v41fp4_h64_no_split.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/decode/sparse/nvfp4_head64/instantiations/v32_nvfp4_fp8rope.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_splitkv.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41_splitkv.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41fp4.cu
${flashmla_SOURCE_DIR}/csrc/kernels/sm100/prefill/sparse/fwd_for_small_topk/head128/instantiations/phase1_decode_k512_v41fp4_splitkv.cu
)
set(FlashMLA_Extension_SOURCES
@@ -125,7 +159,6 @@ if(FLASH_MLA_ARCHS)
set(FlashMLA_INCLUDES
${flashmla_SOURCE_DIR}/csrc
${flashmla_SOURCE_DIR}/csrc/kerutils/include
${flashmla_SOURCE_DIR}/csrc/sm90
${flashmla_SOURCE_DIR}/csrc/cutlass/include
${flashmla_SOURCE_DIR}/csrc/cutlass/tools/util/include
)
@@ -23,9 +23,15 @@
* cos_sin_cache layout [max_pos, rope_dim] = cos || sin (cos first, sin
* second along last dim; each half is rope_dim/2 = 32 values)
*
* Cache layout per paged-cache block (block_size tokens):
* [0, bs*576): token data, 448 fp8 + 128 bf16 each
* [bs*576, bs*576 + bs*8): UE8M0 scales, 7 real + 1 pad per token
* Cache layout per paged-cache block (block_size tokens), selected by the
* `kv_mxfp8` argument:
* V4 (584 B/token, kv_mxfp8=false)
* [0, bs*576): token data, 448 fp8 + 128 bf16 each
* [bs*576, bs*576 + bs*8): UE8M0 scales, 7 real + 1 pad per token
* V4.1 (528 B/token, kv_mxfp8=true) -- RoPE is quantized as well, so the
* whole row is MXFP8 (fp8 e4m3 with one UE8M0 scale per 32 dims)
* [0, bs*512): token data, 512 fp8 each
* [bs*512, bs*512 + bs*16): UE8M0 scales, 16 per token
*/
#include "torch_utils.h"
@@ -91,6 +97,10 @@ constexpr int kQuantBlock = 64;
constexpr int kNumQuantBlocks = kNopeDim / kQuantBlock; // 7
constexpr int kScaleBytesPerToken = kNumQuantBlocks + 1; // 8 (7 real + 1 pad)
constexpr int kTokenDataBytes = kNopeDim + kRopeDim * 2; // 448 + 128 = 576
// V4.1 MXFP8 record: every dim quantized, one UE8M0 scale per 32 dims.
constexpr int kMxfp8QuantBlock = 32;
constexpr int kMxfp8NumQuantBlocks = kHeadDim / kMxfp8QuantBlock; // 16
constexpr int kMxfp8TokenDataBytes = kHeadDim; // 512
// FNUZ on gfx942 / OCP elsewhere. FNUZ uses 224.0 (not the dtype's raw
// 240.0) to match the rest of vLLM's FNUZ pipeline.
#if defined(USE_ROCM) && defined(__gfx942__)
@@ -141,6 +151,11 @@ __device__ __forceinline__ uint4 packFp8E4M3x16(float const* values,
// ────────────────────────────────────────────────────────────────────────────
// Small inline helpers
// ────────────────────────────────────────────────────────────────────────────
__device__ __forceinline__ float warp2MaxAbs(float val) {
// Reduce absolute max across 2 consecutive lanes (32 dims = one MXFP8 block).
return fmaxf(val, __shfl_xor_sync(FINAL_MASK, val, 1));
}
__device__ __forceinline__ float warp4MaxAbs(float val) {
// Reduce absolute max across 4 consecutive lanes (lane id & 3 group).
float peer = __shfl_xor_sync(FINAL_MASK, val, 1);
@@ -176,6 +191,10 @@ __device__ __forceinline__ float warpSum(float val) {
// slot == kNumHeadsQPadded → KV (RoPE + UE8M0 quant
// + paged-cache
// insert)
//
// `kv_mxfp8` is grid-uniform and only steers the KV branch, so it stays a
// runtime argument rather than doubling every (head count, q-norm)
// instantiation of a kernel whose hot path is the Q branch.
template <typename scalar_t_in, int kNumHeadsQPadded, bool APPLY_Q_NORM>
__device__ __forceinline__ void processDeepseekV4Slot(
uint4 v0, uint4 v1, int const tokenIdx, int const slotIdx,
@@ -184,7 +203,7 @@ __device__ __forceinline__ void processDeepseekV4Slot(
uint8_t* __restrict__ k_cache, int64_t const* __restrict__ slot_mapping,
int64_t const* __restrict__ position_ids,
float const* __restrict__ cos_sin_cache, int const cache_block_size,
int const kv_block_stride) {
int const kv_block_stride, bool const kv_mxfp8) {
using Converter = vllm::_typeConvert<scalar_t_in>;
bool const isKV = (slotIdx == kNumHeadsQPadded);
bool const isPadQ = !isKV && (slotIdx >= num_heads_q);
@@ -302,19 +321,24 @@ __device__ __forceinline__ void processDeepseekV4Slot(
*reinterpret_cast<uint4*>(dst) = out0;
*reinterpret_cast<uint4*>(dst + 8) = out1;
} else {
// ── KV: FP8 quant on NoPE + bf16 store on RoPE + cache insert.
// ── KV: FP8 quant + cache insert. V4 keeps the RoPE dims in bf16;
// V4.1 (kv_mxfp8) quantizes them too.
int64_t const slot_id = slot_mapping[tokenIdx];
if (slot_id >= 0) {
int const token_data_bytes =
kv_mxfp8 ? kMxfp8TokenDataBytes : kTokenDataBytes;
int const scale_bytes =
kv_mxfp8 ? kMxfp8NumQuantBlocks : kScaleBytesPerToken;
int64_t const block_idx = slot_id / cache_block_size;
int64_t const pos_in_block = slot_id % cache_block_size;
uint8_t* block_base =
k_cache + block_idx * static_cast<int64_t>(kv_block_stride);
uint8_t* token_fp8_ptr = block_base + pos_in_block * kTokenDataBytes;
uint8_t* token_fp8_ptr = block_base + pos_in_block * token_data_bytes;
uint8_t* token_bf16_ptr = token_fp8_ptr + kNopeDim;
uint8_t* token_scale_ptr =
block_base +
static_cast<int64_t>(cache_block_size) * kTokenDataBytes +
pos_in_block * kScaleBytesPerToken;
static_cast<int64_t>(cache_block_size) * token_data_bytes +
pos_in_block * scale_bytes;
#pragma unroll
for (int i = 0; i < kElemsPerLane; i++) {
@@ -326,11 +350,13 @@ __device__ __forceinline__ void processDeepseekV4Slot(
for (int i = 0; i < kElemsPerLane; i++) {
local_absmax = fmaxf(local_absmax, fabsf(elements[i]));
}
float const absmax = fmaxf(warp4MaxAbs(local_absmax), 1e-4f);
float const absmax = fmaxf(
kv_mxfp8 ? warp2MaxAbs(local_absmax) : warp4MaxAbs(local_absmax),
1e-4f);
float const exponent = ceilf(log2f(absmax / kFp8Max));
float const inv_scale = exp2f(-exponent);
if (!is_rope_lane) {
if (kv_mxfp8 || !is_rope_lane) {
uint8_t out_bytes[kElemsPerLane];
#pragma unroll
for (int i = 0; i < kElemsPerLane; i++) {
@@ -347,13 +373,19 @@ __device__ __forceinline__ void processDeepseekV4Slot(
*reinterpret_cast<uint4*>(token_fp8_ptr + dim_base) =
*reinterpret_cast<uint4 const*>(out_bytes);
if ((laneId & 3) == 0) {
int const q_block_idx = laneId >> 2;
float encoded = fmaxf(fminf(exponent + 127.0f, 255.0f), 0.0f);
token_scale_ptr[q_block_idx] = static_cast<uint8_t>(encoded);
}
if (laneId == 0) {
token_scale_ptr[kNumQuantBlocks] = 0;
float const encoded = fmaxf(fminf(exponent + 127.0f, 255.0f), 0.0f);
if (kv_mxfp8) {
// One scale per 32 dims = one per lane pair.
if ((laneId & 1) == 0) {
token_scale_ptr[laneId >> 1] = static_cast<uint8_t>(encoded);
}
} else {
if ((laneId & 3) == 0) {
token_scale_ptr[laneId >> 2] = static_cast<uint8_t>(encoded);
}
if (laneId == 0) {
token_scale_ptr[kNumQuantBlocks] = 0;
}
}
} else {
uint4 out0, out1;
@@ -421,7 +453,8 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel(
int const num_tokens_insert, // = slot_mapping.size(0), ≤ num_tokens_full
int const num_heads_q, // live Q heads (input layout)
int const cache_block_size, // tokens per paged-cache block
int const kv_block_stride) { // bytes per paged-cache block
int const kv_block_stride, // bytes per paged-cache block
bool const kv_mxfp8) { // V4.1 all-dims MXFP8 record
#if (!defined(__CUDA_ARCH__) || __CUDA_ARCH__ < 800) && !defined(USE_ROCM)
// BF16 _typeConvert specialization is unavailable on pre-Ampere. The
// DeepseekV4 kernel only runs with bf16 inputs in practice, so compile a
@@ -477,7 +510,7 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernel(
processDeepseekV4Slot<scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM>(
v0, v1, tokenIdx, slotIdx, dim_base, laneId, num_heads_q, eps, q_out,
k_cache, slot_mapping, position_ids, cos_sin_cache, cache_block_size,
kv_block_stride);
kv_block_stride, kv_mxfp8);
#if defined(__CUDA_ARCH__) && (__CUDA_ARCH__ >= 900)
cudaTriggerProgrammaticLaunchCompletion();
@@ -506,7 +539,7 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid(
float const* __restrict__ cos_sin_cache, float const eps,
int const num_tokens_full, int const num_tokens_insert,
int const num_heads_q, int const cache_block_size,
int const kv_block_stride) {
int const kv_block_stride, bool const kv_mxfp8) {
#if (!defined(__CUDA_ARCH__) || __CUDA_ARCH__ < 800) && !defined(USE_ROCM)
if constexpr (std::is_same_v<scalar_t_in, c10::BFloat16>) {
return;
@@ -564,7 +597,7 @@ __global__ void fusedDeepseekV4QNormRopeKVRopeQuantInsertKernelReducedGrid(
processDeepseekV4Slot<scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM>(
v0_curr, v1_curr, tokenIdx, curr_slot, dim_base, laneId,
num_heads_q, eps, q_out, k_cache, slot_mapping, position_ids,
cos_sin_cache, cache_block_size, kv_block_stride);
cos_sin_cache, cache_block_size, kv_block_stride, kv_mxfp8);
// ── Buffer rotation: hand the prefetched LDGs to the next iter.
v0_curr = v0_next;
@@ -590,7 +623,7 @@ static void launchFusedDeepseekV4Templated(
uint8_t* k_cache, int64_t const* slot_mapping, int64_t const* position_ids,
float const* cos_sin_cache, float const eps, int const num_tokens_full,
int const num_tokens_insert, int const num_heads_q,
int const cache_block_size, int const kv_block_stride,
int const cache_block_size, int const kv_block_stride, bool const kv_mxfp8,
cudaStream_t stream) {
constexpr int kBlockSize = 256;
constexpr int kWarpsPerBlock = kBlockSize / 32;
@@ -630,7 +663,8 @@ static void launchFusedDeepseekV4Templated(
scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM>,
q_in, q_out, kv_in, k_cache, slot_mapping, position_ids,
cos_sin_cache, eps, num_tokens_full, num_tokens_insert,
num_heads_q, cache_block_size, kv_block_stride);
num_heads_q, cache_block_size, kv_block_stride,
kv_mxfp8);
} else {
config.gridDim = dim3(num_tokens_full);
cudaLaunchKernelEx(
@@ -639,7 +673,7 @@ static void launchFusedDeepseekV4Templated(
scalar_t_in, kNumHeadsQPadded, APPLY_Q_NORM>,
q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, cos_sin_cache,
eps, num_tokens_full, num_tokens_insert, num_heads_q, cache_block_size,
kv_block_stride);
kv_block_stride, kv_mxfp8);
}
#else
// ROCm: use standard kernel launch syntax (no PDL/stream serialization)
@@ -649,7 +683,7 @@ static void launchFusedDeepseekV4Templated(
<<<grid, kBlockSize, 0, stream>>>(
q_in, q_out, kv_in, k_cache, slot_mapping, position_ids,
cos_sin_cache, eps, num_tokens_full, num_tokens_insert, num_heads_q,
cache_block_size, kv_block_stride);
cache_block_size, kv_block_stride, kv_mxfp8);
#endif
}
@@ -663,19 +697,19 @@ void launchFusedDeepseekV4QNormRopeKVRopeQuantInsert(
int const num_tokens_full, int const num_tokens_insert,
int const num_heads_q, int const num_heads_q_padded,
int const cache_block_size, int const kv_block_stride,
bool const apply_q_norm, cudaStream_t stream) {
bool const apply_q_norm, bool const kv_mxfp8, cudaStream_t stream) {
#define DISPATCH(N) \
case N: \
if (apply_q_norm) { \
launchFusedDeepseekV4Templated<scalar_t_in, N, true>( \
q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, \
cos_sin_cache, eps, num_tokens_full, num_tokens_insert, \
num_heads_q, cache_block_size, kv_block_stride, stream); \
num_heads_q, cache_block_size, kv_block_stride, kv_mxfp8, stream); \
} else { \
launchFusedDeepseekV4Templated<scalar_t_in, N, false>( \
q_in, q_out, kv_in, k_cache, slot_mapping, position_ids, \
cos_sin_cache, eps, num_tokens_full, num_tokens_insert, \
num_heads_q, cache_block_size, kv_block_stride, stream); \
num_heads_q, cache_block_size, kv_block_stride, kv_mxfp8, stream); \
} \
return;
@@ -967,7 +1001,7 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
torch::stable::Tensor const& position_ids, // [N] int64
torch::stable::Tensor const& cos_sin_cache, // [max_pos, rope_dim] bf16
int64_t q_head_padded, // padded Q head count for output
double eps, int64_t cache_block_size, bool apply_q_norm) {
double eps, int64_t cache_block_size, bool apply_q_norm, bool kv_mxfp8) {
STD_TORCH_CHECK(q_in.device().is_cuda() && q_in.is_contiguous(),
"q_in must be contiguous CUDA");
STD_TORCH_CHECK(kv.device().is_cuda() && kv.is_contiguous(),
@@ -1036,7 +1070,7 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
cos_sin_cache.const_data_ptr<float>(), static_cast<float>(eps),
num_tokens_full, num_tokens_insert, num_heads_q,
num_heads_q_padded, cache_block_size_i, kv_block_stride,
apply_q_norm, stream);
apply_q_norm, kv_mxfp8, stream);
});
return q_out;
}
+1 -1
View File
@@ -264,7 +264,7 @@ torch::stable::Tensor fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
torch::stable::Tensor& k_cache, torch::stable::Tensor const& slot_mapping,
torch::stable::Tensor const& position_ids,
torch::stable::Tensor const& cos_sin_cache, int64_t q_head_padded,
double eps, int64_t cache_block_size, bool apply_q_norm);
double eps, int64_t cache_block_size, bool apply_q_norm, bool kv_mxfp8);
void fused_deepseek_v4_qnorm_rope_kv_rope_full_cache_bf16_insert(
torch::stable::Tensor& q, torch::stable::Tensor const& kv,
+1 -1
View File
@@ -430,7 +430,7 @@ STABLE_TORCH_LIBRARY_FRAGMENT(_C, ops) {
"Tensor q_in, Tensor kv, Tensor! k_cache, "
"Tensor slot_mapping, Tensor position_ids, Tensor cos_sin_cache, "
"int q_head_padded, float eps, int cache_block_size, "
"bool apply_q_norm=True) -> Tensor");
"bool apply_q_norm=True, bool kv_mxfp8=False) -> Tensor");
// FlashInfer V4 full-cache variants: write Q in place (bf16) or to a separate
// FP8 tensor, and KV into a contiguous 512-wide token-strided cache.
+130
View File
@@ -215,6 +215,136 @@ def test_v41_fused_save_compress_and_insert(
torch.testing.assert_close(cache_backing, expected_cache, rtol=0, atol=0)
def _mxfp8_record_reference(
latent_row: torch.Tensor, cos_sin_row: torch.Tensor
) -> tuple[torch.Tensor, torch.Tensor]:
"""Reference V4.1 record for one token: 512 fp8 bytes + 16 UE8M0 scales.
Mirrors FlashMLA's ``KVCacheLayout.V41_FP8Sparse`` quantizer: rotate the
RoPE tail first, then scale every 32-dim tile, RoPE tiles included.
"""
row = latent_row.float()
c, s = cos_sin_row.chunk(2)
rotated = row.clone()
even, odd = row[448::2], row[449::2]
rotated[448::2] = even * c - odd * s
rotated[449::2] = odd * c + even * s
quantized, scales = _ue8m0_reference(rotated, 32, 448.0)
return quantized.view(torch.uint8), (scales.log2() + 127).to(torch.uint8)
@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA only")
@pytest.mark.parametrize("compress_ratio", [1, 2])
def test_v41_rope_insert_mxfp8_record(compress_ratio: int):
"""The 528-byte V4.1 record quantizes the RoPE dims instead of keeping them
in bf16, so a page is [block_size x 512 fp8][block_size x 16 UE8M0].
Byte-exact against the reference quantizer, including the rows and page
padding the kernel must leave alone.
"""
from vllm.models.deepseek_v41.common.ops.fused_compress_quant_cache import (
rope_quant_insert,
)
torch.manual_seed(7)
device = "cuda"
cache_block = 64
cache_stride = math.ceil(cache_block * 528 / 512) * 512
num_tokens = 12
positions = torch.arange(num_tokens, dtype=torch.int64, device=device)
latent = torch.randn(num_tokens, 512, dtype=torch.bfloat16, device=device)
angles = torch.randn(64, 32, device=device)
cos_sin = torch.cat((angles.cos(), angles.sin()), dim=-1)
cache_backing = torch.full((2, cache_stride), 165, dtype=torch.uint8, device=device)
cache = cache_backing.as_strided((2, cache_block, 528), (cache_stride, 528, 1))
# Leave one token unmapped so the kernel's negative-slot skip is covered.
slots = torch.arange(num_tokens, dtype=torch.int64, device=device)
slots[3] = -1
rope_quant_insert(latent, positions, cos_sin, cache, slots, compress_ratio)
expected = torch.full_like(cache_backing, 165)
for t in range(num_tokens):
slot = slots[t].item()
pos = positions[t].item()
if slot < 0 or (pos + 1) % compress_ratio:
continue
page, row = divmod(slot, cache_block)
values, scales = _mxfp8_record_reference(
latent[t], cos_sin[pos // compress_ratio * compress_ratio]
)
expected[page, row * 512 : (row + 1) * 512] = values
scale_offset = cache_block * 512 + row * 16
expected[page, scale_offset : scale_offset + 16] = scales
torch.testing.assert_close(cache_backing, expected, rtol=0, atol=0)
@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA only")
def test_v41_mxfp8_cache_round_trip():
"""quantize_and_insert -> dequantize_and_gather recovers the V4.1 record.
The RoPE dims now go through fp8, so they carry quantization error too --
the check is that every dim survives within one MXFP8 tile step, and that
the bytes on the way match the reference quantizer.
"""
from vllm.models.deepseek_v41.common.ops import (
dequantize_and_gather_k_cache,
quantize_and_insert_k_cache,
)
torch.manual_seed(11)
device = "cuda"
block_size = 64
num_tokens = 70
num_blocks = 4
page_bytes = math.ceil(block_size * 528 / 512) * 512
k = torch.randn(num_tokens, 512, dtype=torch.bfloat16, device=device)
cache = torch.zeros(num_blocks, page_bytes, dtype=torch.uint8, device=device)
slot_mapping = torch.arange(num_tokens, dtype=torch.int64, device=device)
quantize_and_insert_k_cache(
k, cache, slot_mapping, block_size=block_size, bytes_per_token=528
)
for t in (0, 1, block_size, num_tokens - 1):
values, scales = _ue8m0_reference(k[t].float(), 32, 448.0)
page, row = divmod(t, block_size)
torch.testing.assert_close(
cache[page, row * 512 : (row + 1) * 512],
values.view(torch.uint8),
rtol=0,
atol=0,
)
scale_offset = block_size * 512 + row * 16
torch.testing.assert_close(
cache[page, scale_offset : scale_offset + 16],
(scales.log2() + 127).to(torch.uint8),
rtol=0,
atol=0,
)
out = torch.zeros(1, num_tokens, 512, dtype=torch.bfloat16, device=device)
dequantize_and_gather_k_cache(
out,
cache.view(num_blocks, block_size, 528),
seq_lens=torch.tensor([num_tokens], dtype=torch.int32, device=device),
gather_lens=None,
block_table=torch.arange(num_blocks, dtype=torch.int32, device=device).view(
1, -1
),
block_size=block_size,
offset=0,
)
# Half an e4m3 ULP at the top of the range (16 units) bounds the error.
tile_amax = k.float().abs().view(num_tokens, 16, 32).amax(-1).clamp(min=1e-4)
tolerance = 16.0 * (tile_amax / 448.0).log2().ceil().exp2()
error = (out[0].float() - k.float()).abs()
assert (error <= tolerance.repeat_interleave(32, dim=-1)).all()
@pytest.mark.skipif(not current_platform.is_cuda(), reason="CUDA only")
@pytest.mark.parametrize("compress_ratio", [1, 2])
@pytest.mark.parametrize("store_fp8", [False, True])
@@ -424,6 +424,104 @@ def test_kv_path_matches_reference(num_tokens: int, block_size: int):
)
# ── Test 2c: DeepSeek V4.1 MXFP8 record (kv_mxfp8=True) ──────────────────────
V41_HEAD_BYTES = HEAD_DIM + HEAD_DIM // 32 # 512 fp8 + 16 UE8M0 scales = 528
@pytest.mark.parametrize("num_tokens", [1, 17, 64])
@pytest.mark.parametrize("block_size", [32, 64])
def test_kv_path_mxfp8_record_matches_reference(num_tokens: int, block_size: int):
"""``kv_mxfp8=True`` writes DeepSeek's V4.1 record: all 512 dims as fp8 e4m3
with one UE8M0 scale per 32 dims, no bf16 RoPE tail.
Both sides quantize the same bf16-rounded rotated row, so the bytes must
match the Triton encoder exactly apart from the documented 1-ULP bf16
rounding split on the rotation itself.
"""
from vllm.models.deepseek_v41.common.ops import (
dequantize_and_gather_k_cache as v41_dequantize_and_gather_k_cache,
)
from vllm.models.deepseek_v41.common.ops import (
quantize_and_insert_k_cache as v41_quantize_and_insert_k_cache,
)
torch.manual_seed(3)
device = "cuda"
dtype = torch.bfloat16
max_pos = 4096
kv = torch.randn(num_tokens, HEAD_DIM, dtype=dtype, device=device)
positions = torch.arange(num_tokens, dtype=torch.int64, device=device)
cos_sin_cache = make_cos_sin_cache(max_pos, ROPE_DIM, torch.float32, device)
num_blocks = (num_tokens + block_size - 1) // block_size + 1
slot_mapping = torch.arange(num_tokens, dtype=torch.int64, device=device)
k_cache_ref = torch.zeros(
num_blocks, block_size * V41_HEAD_BYTES, dtype=torch.uint8, device=device
)
v41_quantize_and_insert_k_cache(
apply_rope_gptj_last_k(kv, positions, cos_sin_cache),
k_cache_ref,
slot_mapping,
block_size=block_size,
use_fnuz=USE_FNUZ,
bytes_per_token=V41_HEAD_BYTES,
)
k_cache_fused = torch.zeros_like(k_cache_ref)
q_dummy = torch.zeros(num_tokens, 1, HEAD_DIM, dtype=dtype, device=device)
torch.ops._C.fused_deepseek_v4_qnorm_rope_kv_rope_quant_insert(
q_dummy,
kv,
k_cache_fused,
slot_mapping,
positions,
cos_sin_cache,
64,
1e-6,
block_size,
True,
True, # kv_mxfp8
)
# A pre-quantization bf16 tie can land the two encoders on adjacent e4m3
# codes, so compare after dequantizing and allow one step at the tile scale.
def _dequant(cache_2d):
out = torch.zeros(1, num_tokens, HEAD_DIM, dtype=dtype, device=device)
v41_dequantize_and_gather_k_cache(
out,
cache_2d.view(num_blocks, block_size, V41_HEAD_BYTES),
torch.tensor([num_tokens], dtype=torch.int32, device=device),
None,
torch.arange(num_blocks, dtype=torch.int32, device=device).unsqueeze(0),
block_size,
offset=0,
use_fnuz=USE_FNUZ,
)
return out[0]
rec_ref = _dequant(k_cache_ref).float()
rec_fused = _dequant(k_cache_fused).float()
tile_scale = (
(rec_ref.abs().view(num_tokens, 16, 32).amax(-1).clamp(min=1e-4) / FP8_MAX)
.log2()
.ceil()
.exp2()
)
# One e4m3 ULP at the top of the range is 32 quantized units.
step = 32.0 * tile_scale.repeat_interleave(32, dim=-1)
assert ((rec_fused - rec_ref).abs() <= step).all()
# The NoPE dims see no rotation at all, so they must be byte-identical.
nope_bytes = slice(0, block_size * HEAD_DIM)
ref_rows = k_cache_ref[:, nope_bytes].view(num_blocks, block_size, HEAD_DIM)
fused_rows = k_cache_fused[:, nope_bytes].view(num_blocks, block_size, HEAD_DIM)
torch.testing.assert_close(
fused_rows[..., :NOPE_DIM], ref_rows[..., :NOPE_DIM], rtol=0, atol=0
)
# ── Test 2b: DP padding (slot_mapping shorter than q/kv) ─────────────────────
@@ -23,10 +23,11 @@ from vllm.model_executor.warmup.jit_warmup_cutedsl_helper import (
class DequantGatherKCacheKernel(
VllmCuTeDSLJitKernel["DequantGatherKCacheKernel.CompileKey"]
):
# Hard-coded for DSv4.
# DSv4 geometry. The record format (how much of the row is quantized and
# how wide a scale group is) comes from the compile key: V4 quantizes the
# 448 NoPE dims in groups of 64 and keeps a bf16 RoPE tail, V4.1 quantizes
# all 512 dims in groups of 32 (MXFP8) and has no tail.
head_dim = 512
fp8_dim = 448
group_size = 64 # 1 scale per 64 elems
num_warps = 4
tb_size = num_warps * 32
num_stages = 4
@@ -35,17 +36,20 @@ class DequantGatherKCacheKernel(
class CompileKey:
block_size: int
has_gather_lens: bool
fp8_dim: int = 448
group_size: int = 64 # dims per scale
@staticmethod
def kernel(compile_key: CompileKey) -> Any:
head_dim = DequantGatherKCacheKernel.head_dim
fp8_dim = DequantGatherKCacheKernel.fp8_dim
group_size = DequantGatherKCacheKernel.group_size
fp8_dim = compile_key.fp8_dim
group_size = compile_key.group_size
num_warps = DequantGatherKCacheKernel.num_warps
tb_size = DequantGatherKCacheKernel.tb_size
num_stages = DequantGatherKCacheKernel.num_stages
bf16_dim = head_dim - fp8_dim
data_dim = fp8_dim + bf16_dim * 2
scale_bytes = head_dim // group_size
block_size = compile_key.block_size
@cute.jit
@@ -62,8 +66,8 @@ class DequantGatherKCacheKernel(
):
op = cpasync.CopyG2SOp(cute.nvgpu.LoadCacheMode.GLOBAL)
cp16_atom = cute.make_copy_atom(op, Uint32, num_bits_per_copy=128)
cp8_atom = cute.make_copy_atom(
cpasync.CopyG2SOp(), Uint8, num_bits_per_copy=64
cp_scale_atom = cute.make_copy_atom(
cpasync.CopyG2SOp(), Uint8, num_bits_per_copy=scale_bytes * 8
)
page_id = block_table[req_id, pos // block_size]
block_offset = pos % block_size
@@ -88,7 +92,7 @@ class DequantGatherKCacheKernel(
)
elif idx == cutlass.const_expr(data_dim // 16):
cute.copy(
cp8_atom,
cp_scale_atom,
k_scale[page_id, block_offset, None],
s_kscale[None, stage_id],
)
@@ -119,8 +123,8 @@ class DequantGatherKCacheKernel(
)[None, warp_id, None]
s_kscale = smem.allocate_tensor(
Uint8,
cute.make_layout((8, num_warps, num_stages)),
byte_alignment=8,
cute.make_layout((scale_bytes, num_warps, num_stages)),
byte_alignment=scale_bytes,
)[None, warp_id, None]
# Prepare for 16B cp.async, also for BF16 smem loads later.
@@ -239,14 +243,16 @@ class DequantGatherKCacheKernel(
# Last 64 elems are BF16 tail, corresponds to dequant1 of last
# 8 threads. We have 448 FP8 + 64 BF16 -> 28x 16B for FP8 +
# 8x 16B for BF16.
if lane_id + 32 >= fp8_dim // 8:
idx = fp8_dim // 16 + (lane_id + 32) - fp8_dim // 8
cute.copy(
cp16_atom,
s_kdata_16B_slice[(None, idx), compute_stage],
dequant1,
)
# 8x 16B for BF16. The V4.1 record is fully quantized and has
# no tail.
if cutlass.const_expr(bf16_dim > 0): # noqa: SIM102
if lane_id + 32 >= fp8_dim // 8:
idx = fp8_dim // 16 + (lane_id + 32) - fp8_dim // 8
cute.copy(
cp16_atom,
s_kdata_16B_slice[(None, idx), compute_stage],
dequant1,
)
# Store two 16B BF16 chunks per lane: first half, then second half.
dst = out_slice[req_id, offset + i, (None, lane_id)]
@@ -269,7 +275,7 @@ class DequantGatherKCacheKernel(
):
# Split k_cache into k_data and k_scale. Each [block_size, head_bytes]
# block is actually a concat of
# [block_size, fp8_dim + bf16_dim * 2] and [block_size, 8].
# [block_size, fp8_dim + bf16_dim * 2] and [block_size, scale_bytes].
k_data = cute.make_tensor(
k_cache.iterator,
layout=cute.make_layout(
@@ -282,8 +288,8 @@ class DequantGatherKCacheKernel(
k_scale = cute.make_tensor(
k_cache.iterator + (block_size * data_dim),
layout=cute.make_layout(
(k_cache.shape[0], block_size, 8),
stride=(k_cache.stride[0], 8, 1),
(k_cache.shape[0], block_size, scale_bytes),
stride=(k_cache.stride[0], scale_bytes, 1),
),
)
@@ -311,10 +317,14 @@ class DequantGatherKCacheKernel(
*,
block_size: int,
has_gather_lens: bool,
fp8_dim: int = 448,
group_size: int = 64,
) -> CompileKey:
return self.CompileKey(
block_size=block_size,
has_gather_lens=has_gather_lens,
fp8_dim=fp8_dim,
group_size=group_size,
)
def get_warmup_keys(self, vllm_config: Any) -> list[CompileKey]:
@@ -345,9 +355,11 @@ class DequantGatherKCacheKernel(
def warmup_inputs(self, compile_key: CompileKey) -> tuple[Any, ...]:
num_reqs = cute.sym_int()
head_dim = self.head_dim
fp8_dim = self.fp8_dim
fp8_dim = compile_key.fp8_dim
block_size = compile_key.block_size
head_bytes = fp8_dim + (head_dim - fp8_dim) * 2 + 8
head_bytes = (
fp8_dim + (head_dim - fp8_dim) * 2 + head_dim // compile_key.group_size
)
out = make_fake_tensor(BFloat16, (num_reqs, cute.sym_int(), head_dim), 16)
k_cache = cute.runtime.make_fake_tensor(
@@ -384,10 +396,14 @@ class DequantGatherKCacheKernel(
block_table: torch.Tensor,
block_size: int,
offset: int,
fp8_dim: int = 448,
group_size: int = 64,
) -> CuTeDSLLaunchSpec["DequantGatherKCacheKernel.CompileKey"]:
compile_key = self.dispatch(
block_size=block_size,
has_gather_lens=gather_lens is not None,
fp8_dim=fp8_dim,
group_size=group_size,
)
launch_args = (
out,
+2
View File
@@ -259,6 +259,8 @@ def _insert_context_kv(
attn.padded_heads,
attn.eps,
block_size,
True, # apply_q_norm (unused: the query is a discarded dummy)
attn.kv_mxfp8,
)
elif cache_dtype == torch.bfloat16:
swa_3d = swa_cache.view(-1, block_size, attn.head_dim)
+34 -9
View File
@@ -51,6 +51,7 @@ from vllm.model_executor.layers.quantization import QuantizationConfig
from vllm.model_executor.models.utils import extract_layer_index
from vllm.models.deepseek_v41.common.rope import build_deepseek_v4_rope
from vllm.models.deepseek_v41.compressor import DeepseekCompressor
from vllm.platforms import current_platform
from vllm.triton_utils import tl, triton
from vllm.utils.multi_stream_utils import (
execute_in_parallel,
@@ -109,6 +110,16 @@ def _fill_short_context_topk_indices(
)
# Which packed fp8_ds_mla record a V4.1 layer writes. FlashMLA decodes
# DeepSeek's V4.1 record -- all 512 dims (RoPE included) as fp8 e4m3 with one
# UE8M0 scale per 32 dims, 512 data bytes + 16 scale bytes per token, pages
# rounded to the kernel's 512 B TMA stride -- only in its SM100 sparse-decode
# kernels. Every other arch keeps the V4 record: 448 fp8 NoPE + 64 bf16 RoPE
# plus 7 UE8M0 scales of 64 dims and a pad byte (584 B, 576 B pages).
def _use_v41_mxfp8_kv_record() -> bool:
return current_platform.is_device_capability_family(100)
def _resolve_dsv4_kv_cache_dtype(
use_fp8_ds_mla_layout: bool,
kv_cache_dtype: str,
@@ -445,6 +456,9 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
self.kv_cache_dtype, self.kv_cache_torch_dtype = _resolve_dsv4_kv_cache_dtype(
self._uses_fp8_ds_mla_layout(), cache_config.cache_dtype, cache_config
)
self.kv_mxfp8 = _use_v41_mxfp8_kv_record()
self.kv_bytes_per_token = 528 if self.kv_mxfp8 else 584
self.kv_page_alignment = 512 if self.kv_mxfp8 else 576
self.swa_cache_layer = DeepseekV4SWACache(
head_dim=self.head_dim,
@@ -454,6 +468,8 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
cache_config=cache_config,
backend_cls=self.swa_backend_cls,
block_size=32,
packed_bytes_per_token=self.kv_bytes_per_token,
packed_page_alignment=self.kv_page_alignment,
)
# The attention layer itself was already registered with the
@@ -879,6 +895,7 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
self.eps,
swa_metadata.block_size,
False,
self.kv_mxfp8,
)
# Plain-row path: the [num_blocks, block_size, 512] cache stores the KV
@@ -934,9 +951,9 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
# DeepseekV4SWACache.
if not self.is_kv_source:
return None
# fp8_ds_mla is a UE8M0 block-scaled uint8 layout and needs 576B
# alignment; plain bf16 / per-tensor fp8 rows use natural element-size
# pages.
# fp8_ds_mla is a UE8M0 block-scaled uint8 layout whose page rounds up
# to the decode kernel's TMA stride; plain bf16 / per-tensor fp8 rows
# use natural element-size pages.
uses_fp8_ds_mla_layout = self.kv_cache_dtype == "fp8_ds_mla"
return MLAAttentionSpec(
block_size=vllm_config.cache_config.block_size,
@@ -945,12 +962,13 @@ class DeepseekV4Attention(nn.Module, AttentionLayerBase, ABC):
dtype=torch.uint8 if uses_fp8_ds_mla_layout else self.kv_cache_torch_dtype,
tokens_per_state=self.compress_ratio,
cache_dtype_str=self.kv_cache_dtype,
alignment=576 if uses_fp8_ds_mla_layout else 512,
alignment=self.kv_page_alignment if uses_fp8_ds_mla_layout else 512,
model_version="deepseek_v4",
kv_quant_mode=get_kv_quant_mode(self.kv_cache_dtype),
# DeepseekV4: 448B NoPE + 128B RoPE + 8B fp8 scale = 584B per token;
# head_size stays semantic (512).
state_content_bytes=584 if uses_fp8_ds_mla_layout else None,
# Packed record width; head_size stays semantic (512).
state_content_bytes=(
self.kv_bytes_per_token if uses_fp8_ds_mla_layout else None
),
)
def _compressed_kv_cache(self) -> torch.Tensor:
@@ -998,8 +1016,15 @@ class DeepseekV4IndexerCache(torch.nn.Module, AttentionLayerBase):
head_size=self.head_dim,
dtype=self.dtype,
tokens_per_state=self.compress_ratio,
# 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
alignment=576 if uses_fp8_ds_mla_layout else 512,
# The indexer page shares a physical block with the main KV cache
# (the block stride is the sum of every page in the group), so it
# has to carry the same padding: the packed record's TMA stride,
# else 512B for FlashInfer sparse (#44577).
alignment=(
576
if uses_fp8_ds_mla_layout and not _use_v41_mxfp8_kv_record()
else 512
),
)
def forward(self): ...
@@ -36,6 +36,18 @@ from vllm.triton_utils import tl, triton
from vllm.utils.import_utils import has_cutedsl
from vllm.utils.math_utils import next_power_of_2
# Per-token byte width of the two paged fp8 records. Both put a page's whole
# data region ahead of its whole scale region, so ``k_cache.shape[-1]`` -- the
# width the KV-cache spec publishes -- identifies the record:
# V4 (584 B): 448 fp8 e4m3 NoPE + 64 bf16 RoPE, then 7 UE8M0 scales of 64
# dims each plus a pad byte.
# V4.1 (528 B): all 512 dims as fp8 e4m3 (RoPE included), then 16 UE8M0
# scales of 32 dims each. FlashMLA's ``ModelType::V41``.
V4_BYTES_PER_TOKEN = 584
V41_BYTES_PER_TOKEN = 528
V41_QUANT_BLOCK = 32
V41_NUM_SCALES = 512 // V41_QUANT_BLOCK # 16
@triton.jit
def quantize_and_insert_k_kernel(
@@ -163,6 +175,67 @@ def quantize_and_insert_k_kernel(
tl.store(bf16_out_ptr + chunk_offsets, bf16_vals)
@triton.jit
def _quantize_and_insert_k_mxfp8_kernel(
k_ptr, # [num_tokens, 512] bf16
slot_mapping_ptr, # [num_tokens] int64
k_cache_ptr, # [num_blocks, block_bytes] as uint8 (flattened view)
num_tokens,
head_dim: tl.constexpr, # 512, RoPE dims included
scale_dim: tl.constexpr, # 16
quant_block: tl.constexpr, # 32 dims per UE8M0 scale
cache_block_size: tl.constexpr, # paged cache block size, in tokens
block_stride: tl.constexpr, # total bytes per block (padded)
fp8_max: tl.constexpr,
use_fnuz: tl.constexpr = False,
):
"""Quantize a bf16 K row to MXFP8 and insert it into a V4.1 paged cache.
One program per token. Unlike the V4 record there is no bf16 tail, so every
dim is scaled.
"""
pid = tl.program_id(0)
if pid >= num_tokens:
return
slot_idx = tl.load(slot_mapping_ptr + pid)
if slot_idx == -1:
return
block_idx = slot_idx // cache_block_size
pos_in_block = slot_idx % cache_block_size
# int64: block_idx * block_stride can exceed 2^31 with many KV-cache blocks.
cache_block_ptr = k_cache_ptr + block_idx.to(tl.int64) * block_stride
token_data_ptr = cache_block_ptr + pos_in_block * head_dim
token_scale_ptr = (
cache_block_ptr + cache_block_size * head_dim + pos_in_block * scale_dim
)
d = tl.arange(0, head_dim)
x = tl.load(k_ptr + pid.to(tl.int64) * head_dim + d).to(tl.float32)
tiles = tl.reshape(x, (scale_dim, quant_block))
# Floor the absmax like the CUDA encoder so an all-zero tile still lands on
# a finite scale.
amax = tl.maximum(tl.max(tl.abs(tiles), 1), 1e-4)
exponent = tl.ceil(tl.log2(amax * (1.0 / fp8_max)))
scaled = tl.clamp(
tiles * tl.reshape(tl.exp2(-exponent), (scale_dim, 1)), -fp8_max, fp8_max
)
if use_fnuz: # noqa: SIM108
fp8 = scaled.to(tl.float8e4b8)
else:
fp8 = scaled.to(tl.float8e4nv)
tl.store(
token_data_ptr + d, tl.reshape(fp8.to(tl.uint8, bitcast=True), (head_dim,))
)
# UE8M0 encoding: stored_value = exponent + 127 (bias).
encoded = tl.minimum(tl.maximum(exponent + 127.0, 0.0), 255.0)
tl.store(token_scale_ptr + tl.arange(0, scale_dim), encoded.to(tl.uint8))
def quantize_and_insert_k_cache(
k: torch.Tensor, # [num_tokens, 512] bf16
k_cache: torch.Tensor, # [num_blocks, block_bytes] uint8
@@ -170,16 +243,13 @@ def quantize_and_insert_k_cache(
block_size: int = 64,
is_ue8m0: bool = True,
use_fnuz: bool = False,
bytes_per_token: int = V4_BYTES_PER_TOKEN,
):
"""
Quantize K tensor and insert into paged K cache.
K Cache block layout (block_size=64 tokens):
- First 64 * 576 = 36864 bytes: Token data
- Each token: 448 bytes (fp8) + 128 bytes (bf16)
- Next 64 * 8 = 512 bytes: Scales
- Each token: 8 bytes (uint8 scales, 7 real + 1 padding)
- Padded to multiple of 576
``bytes_per_token`` picks the record (see the module header): the V4 one,
or V4.1's all-dims MXFP8 one.
``use_fnuz=True`` selects FNUZ E4M3 cache encoding and is only valid on
platforms whose FP8 format is FNUZ. ``use_fnuz=False`` selects OCP E4M3,
@@ -196,6 +266,26 @@ def quantize_and_insert_k_cache(
num_tokens = slot_mapping.shape[0]
block_stride = k_cache.stride(0) # bytes per block
if bytes_per_token == V41_BYTES_PER_TOKEN:
if use_fnuz and not current_platform.is_fp8_fnuz():
raise ValueError("use_fnuz=True requires a platform using FNUZ FP8")
_quantize_and_insert_k_mxfp8_kernel[(num_tokens,)](
k,
slot_mapping,
k_cache,
num_tokens,
head_dim=512,
scale_dim=V41_NUM_SCALES,
quant_block=V41_QUANT_BLOCK,
cache_block_size=block_size,
block_stride=block_stride,
fp8_max=get_fp8_min_max()[1]
if use_fnuz
else torch.finfo(torch.float8_e4m3fn).max,
use_fnuz=use_fnuz,
)
return
TOKEN_FP8_DIM = 448
TOKEN_BF16_DIM = 64
TOKEN_SCALE_DIM = 8
@@ -343,6 +433,69 @@ def _dequantize_and_gather_k_kernel(
tl.store(output_row_ptr + bf16_output_offset + chunk_offsets, bf16_vals)
@triton.jit
def _dequantize_and_gather_k_mxfp8_kernel(
out_ptr,
out_stride0,
out_stride1,
k_cache_ptr,
seq_lens_ptr,
block_table_ptr,
offset,
gather_lens_ptr,
max_blocks_per_seq: tl.constexpr,
head_dim: tl.constexpr, # 512
scale_dim: tl.constexpr, # 16
quant_block: tl.constexpr, # 32
cache_block_size: tl.constexpr,
block_stride: tl.constexpr,
use_fnuz: tl.constexpr = False,
):
"""Gather V4.1 MXFP8 rows into a bf16 workspace, dequantizing in place."""
batch_idx = tl.program_id(0)
worker_id = tl.program_id(1)
num_workers = tl.num_programs(1)
seq_len = tl.load(seq_lens_ptr + batch_idx)
if gather_lens_ptr is not None: # noqa: SIM108
gather_len = tl.load(gather_lens_ptr + batch_idx)
else:
gather_len = seq_len
start_pos = seq_len - gather_len
d = tl.arange(0, head_dim)
s = tl.arange(0, scale_dim)
for i in range(worker_id, gather_len, num_workers):
pos = start_pos + i
block_in_seq = pos // cache_block_size
pos_in_block = pos % cache_block_size
block_table_row_ptr = block_table_ptr + batch_idx * max_blocks_per_seq
physical_block_idx = tl.load(block_table_row_ptr + block_in_seq) # int32
# int64: physical_block_idx * block_stride can exceed 2^31.
cache_block_ptr = k_cache_ptr + physical_block_idx.to(tl.int64) * block_stride
token_data_ptr = cache_block_ptr + pos_in_block * head_dim
token_scale_ptr = (
cache_block_ptr + cache_block_size * head_dim + pos_in_block * scale_dim
)
x_uint8 = tl.load(token_data_ptr + d)
if use_fnuz:
x_fp8 = x_uint8.to(tl.float8e4b8, bitcast=True)
else:
x_fp8 = x_uint8.to(tl.float8e4nv, bitcast=True)
tiles = tl.reshape(x_fp8.to(tl.float32), (scale_dim, quant_block))
# UE8M0: scale = 2^(stored_value - 127).
encoded = tl.load(token_scale_ptr + s)
scale = tl.exp2(encoded.to(tl.float32) - 127.0)
dequant = tl.reshape(tiles * tl.reshape(scale, (scale_dim, 1)), (head_dim,))
output_row_ptr = out_ptr + batch_idx * out_stride0 + (offset + i) * out_stride1
tl.store(output_row_ptr + d, dequant.to(tl.bfloat16))
def dequantize_and_gather_k_cache_triton(
# [num_reqs, max_num_tokens, head_size]
out: torch.Tensor,
@@ -358,6 +511,28 @@ def dequantize_and_gather_k_cache_triton(
offset: int,
use_fnuz: bool = False,
) -> None:
num_reqs = seq_lens.shape[0]
NUM_WORKERS = 128
if k_cache.shape[-1] == V41_BYTES_PER_TOKEN:
_dequantize_and_gather_k_mxfp8_kernel[(num_reqs, NUM_WORKERS)](
out,
out.stride(0),
out.stride(1),
k_cache,
seq_lens,
block_table,
offset,
gather_lens,
max_blocks_per_seq=block_table.shape[-1],
head_dim=512,
scale_dim=V41_NUM_SCALES,
quant_block=V41_QUANT_BLOCK,
cache_block_size=block_size,
block_stride=k_cache.stride(0),
use_fnuz=use_fnuz,
)
return
TOKEN_FP8_DIM = 448
TOKEN_BF16_DIM = 64
TOKEN_SCALE_DIM = 8
@@ -365,8 +540,6 @@ def dequantize_and_gather_k_cache_triton(
FP8_MAX = 448.0
TOKEN_DATA_SIZE = TOKEN_FP8_DIM + TOKEN_BF16_DIM * 2
num_reqs = seq_lens.shape[0]
NUM_WORKERS = 128
_dequantize_and_gather_k_kernel[(num_reqs, NUM_WORKERS)](
out,
out.stride(0),
@@ -408,6 +581,8 @@ def dequantize_and_gather_k_cache(
) -> None:
"""Dequantize and gather a paged DSv4 K cache.
The record is read off ``k_cache.shape[-1]``; see the module header.
``use_fnuz`` MUST match the encoder of the specific cache being read:
``False`` for ``compressed_k_cache`` (Triton encoder is OCP everywhere),
``current_platform.is_fp8_fnuz()`` for ``swa_k_cache`` (C++ encoder
@@ -419,6 +594,7 @@ def dequantize_and_gather_k_cache(
_DEQUANT_GATHER_K_CACHE_CUTEDSL_KERNEL,
)
mxfp8 = k_cache.shape[-1] == V41_BYTES_PER_TOKEN
_DEQUANT_GATHER_K_CACHE_CUTEDSL_KERNEL(
out=out,
k_cache=k_cache,
@@ -427,6 +603,8 @@ def dequantize_and_gather_k_cache(
block_table=block_table,
block_size=block_size,
offset=offset,
fp8_dim=512 if mxfp8 else 448,
group_size=V41_QUANT_BLOCK if mxfp8 else 64,
)
return
@@ -232,10 +232,13 @@ def rope_quant_insert(
The BF16 latent supplies both NoPE quantization and RoPE input. It is read
only for valid slots at group boundaries. The cache dtype selects the
layout: ``uint8`` is the fp8_ds_mla paged layout (576 value bytes and eight
segregated UE8M0 scale bytes per token, including one zero padding scale);
``bfloat16`` and ``float8_e4m3fn`` are the plain [448 NoPE | 64 RoPE] rows
read by FlashInfer, the latter scaled by the per-tensor ``fp8_scale``.
layout: ``uint8`` is the fp8_ds_mla paged layout, whose record the
per-token byte width names -- 584 B for V4 (576 value bytes and eight
segregated UE8M0 scale bytes, including one zero padding scale) or 528 B
for V4.1 (512 MXFP8 value bytes covering the RoPE dims too, then 16 UE8M0
scales of 32 dims each). ``bfloat16`` and ``float8_e4m3fn`` are the plain
[448 NoPE | 64 RoPE] rows read by FlashInfer, the latter scaled by the
per-tensor ``fp8_scale``.
"""
assert compress_ratio in (1, 2)
assert latent.shape[1] == 512 and latent.dtype == torch.bfloat16
@@ -246,8 +249,15 @@ def rope_quant_insert(
return
launch_kwargs = {"launch_pdl": False} if current_platform.is_cuda() else {}
if kv_cache.dtype == torch.uint8:
assert kv_cache.shape[-1] == 584
_rope_quant_insert_kernel[(num_tokens,)](
assert kv_cache.shape[-1] in (584, 528), (
f"unsupported paged KV record width {kv_cache.shape[-1]}"
)
kernel = (
_rope_quant_insert_mxfp8_kernel
if kv_cache.shape[-1] == 528
else _rope_quant_insert_kernel
)
kernel[(num_tokens,)](
latent,
positions,
cos_sin_cache,
@@ -338,6 +348,60 @@ def _rope_quant_insert_kernel(
tl.store(rope_dst + d - 448, rotated.to(tl.bfloat16), d >= 448)
@triton.jit
def _rope_quant_insert_mxfp8_kernel(
latent,
positions,
cos_sin,
cache,
cache_slots,
COS_STRIDE: tl.constexpr,
CACHE_STRIDE: tl.constexpr,
CACHE_BLOCK: tl.constexpr,
COMPRESS_RATIO: tl.constexpr,
SANITIZE_CACHE_NANS: tl.constexpr,
):
"""V4.1 record: RoPE first, then MXFP8-quantize all 512 dims.
The RoPE dims are quantized here too, so unlike the V4 kernel the rotation
has to happen before the scales are picked.
"""
t = tl.program_id(0)
slot = tl.load(cache_slots + t)
if slot < 0:
return
position = tl.load(positions + t)
if (position + 1) % COMPRESS_RATIO != 0:
return
d = tl.arange(0, 512)
normed = tl.load(latent + t.to(tl.int64) * 512 + d).to(tl.float32)
# NoPE pairs load (cos, sin) = (1, 0), so the rotation is the identity there.
even, odd = tl.split(tl.reshape(normed, (256, 2)))
pair = tl.arange(0, 256) - 224
cs = cos_sin + (position // COMPRESS_RATIO * COMPRESS_RATIO) * COS_STRIDE
c = tl.load(cs + tl.maximum(pair, 0), pair >= 0, other=1.0).to(tl.float32)
s = tl.load(cs + 32 + tl.maximum(pair, 0), pair >= 0, other=0.0).to(tl.float32)
rotated = tl.interleave(even * c - odd * s, odd * c + even * s)
if SANITIZE_CACHE_NANS:
rotated = tl.where(rotated == rotated, rotated, 0.0)
page = cache + (slot // CACHE_BLOCK).to(tl.int64) * CACHE_STRIDE
values = page + (slot % CACHE_BLOCK) * 512
scales = page + CACHE_BLOCK * 512 + (slot % CACHE_BLOCK) * 16
quant = tl.reshape(rotated, (16, 32))
amax = tl.maximum(tl.max(tl.abs(quant), 1), 1e-4)
exponent = tl.ceil(tl.log2(amax * (1.0 / 448.0)))
scaled = quant * tl.reshape(tl.exp2(-exponent), (16, 1))
fp8 = tl.clamp(scaled, -448.0, 448.0).to(tl.float8e4nv)
tl.store(values + d, tl.reshape(fp8.to(tl.uint8, bitcast=True), (512,)))
max_encoded: tl.constexpr = 254.0 if SANITIZE_CACHE_NANS else 255.0
encoded = tl.minimum(tl.maximum(exponent + 127.0, 0.0), max_encoded)
tl.store(scales + tl.arange(0, 16), encoded.to(tl.uint8))
@triton.jit
def _rope_plain_insert_kernel(
latent,
@@ -264,6 +264,8 @@ def _insert_context_kv(
attn.padded_heads,
attn.eps,
block_size,
True, # apply_q_norm (unused: the query is a discarded dummy)
attn.kv_mxfp8,
)
elif cache_dtype == torch.bfloat16:
swa_3d = swa_cache.view(-1, block_size, attn.head_dim)
@@ -91,6 +91,17 @@ Bytes, structured as:
The first `ue8m0` is the scale for the first 64 `float8_e4m3` values,
the second for the next 64, and so on.
For DeepSeek V4.1, each token's KV cache is 528 Bytes, structured as:
- **First 512 bytes:** all 512 dims as `float8_e4m3`. Unlike V4 the 64 RoPE
dims are quantized too, so there is no `bfloat16` part.
- **Last 16 bytes:** Scale factors, containing 16 `ue8m0` values, one per 32
consecutive `float8_e4m3` values (i.e. MXFP8).
The V4 and V4.1 records are not laid out token-by-token within a page: a page
holds all its data rows first and all its scale rows after, so a page is
`block_size * bytes_per_token` bytes rounded up to the decode kernel's TMA
stride (576 B for V4, 512 B for V4.1).
In the "nvfp4_ds_mla" format (SM100 only, DeepSeek V3.2 geometry), each
token's KV cache is 352 Bytes, structured as:
- **First 256 bytes:** 512 `e2m1` NoPE values packed 2/byte (low nibble =
+12 -6
View File
@@ -79,6 +79,8 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
cache_config: CacheConfig,
backend_cls: "type[AttentionBackend] | None" = None,
block_size: int = 64,
packed_bytes_per_token: int = 584,
packed_page_alignment: int = 576,
):
super().__init__()
self.backend_cls = backend_cls or DeepseekSparseSWABackend
@@ -88,6 +90,8 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
self.prefix = prefix
self.cache_config = cache_config
self.dtype = dtype
self.packed_bytes_per_token = packed_bytes_per_token
self.packed_page_alignment = packed_page_alignment
compilation_config = get_current_vllm_config().compilation_config
if prefix in compilation_config.static_forward_context:
raise ValueError(f"Duplicate layer name: {prefix}")
@@ -105,8 +109,9 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
self.kv_cache = kv_cache.squeeze(1)
def get_kv_cache_spec(self, vllm_config: VllmConfig) -> KVCacheSpec:
# fp8_ds_mla's UE8M0 paged layout needs 576B alignment; contiguous
# bf16/fp8 cache uses the natural element-size page.
# fp8_ds_mla's UE8M0 paged layout rounds its page up to the decode
# kernel's TMA stride; contiguous bf16/fp8 cache uses the natural
# element-size page.
uses_fp8_ds_mla_layout = self.cache_config.cache_dtype == "fp8_ds_mla"
return SlidingWindowMLASpec(
block_size=self.block_size,
@@ -115,10 +120,11 @@ class DeepseekV4SWACache(torch.nn.Module, AttentionLayerBase):
dtype=self.dtype,
sliding_window=self.window_size,
cache_dtype_str=self.cache_config.cache_dtype,
# DeepseekV4 fp8_ds_mla: 584B per token (448B NoPE + 128B RoPE + 8B scales)
state_content_bytes=584 if uses_fp8_ds_mla_layout else None,
# 576B for FlashMLA packing; 512B for FlashInfer sparse (#44577).
alignment=576 if uses_fp8_ds_mla_layout else 512,
state_content_bytes=(
self.packed_bytes_per_token if uses_fp8_ds_mla_layout else None
),
# FlashMLA packing stride; 512B for FlashInfer sparse (#44577).
alignment=self.packed_page_alignment if uses_fp8_ds_mla_layout else 512,
model_version="deepseek_v4",
kv_quant_mode=get_kv_quant_mode(self.cache_config.cache_dtype),
)