[DSV4] perf: Make FP8 quant output tensor contiguous (#27926)

Co-authored-by: liqichao <liqichao@baidu.com>
Co-authored-by: chenbong <bhchen@stu.xmu.edu.cn>
This commit is contained in:
Kaixi
2026-07-07 17:40:59 -07:00
committed by GitHub
co-authored by liqichao chenbong
parent b363249423
commit d7dcdf3efd
5 changed files with 479 additions and 7 deletions
@@ -0,0 +1,169 @@
// DeepSeek-V4 wo_a activation quantization for DeepGEMM fp8_einsum.
//
// This is intentionally narrower than the generic per_token_group_quant_8bit_v2
// kernel: input is a [T, G, D] view with contiguous hidden groups, output_q is
// contiguous [T, G, D], group_size is fixed to 128, scales are fp32 UE8M0
// power-of-two values, and output_s is a logical [T, G, D/128] view backed by
// group-major [G, T, D/128] storage.
//
// The generic kernel cannot read the strided DSV4 view while producing
// contiguous [T, G, D] codes and group-major scales without an extra full-tensor
// copy.
#include <sgl_kernel/tensor.h> // TensorMatcher, SymbolicSize/Device
#include <sgl_kernel/utils.h> // RuntimeCheck
#include <sgl_kernel/utils.cuh> // fp8 aliases, PDL helpers
#include <sgl_kernel/warp.cuh> // warp::reduce_max
#include <sgl_kernel/deepseek_v4/fp8_utils.cuh> // UE8M0 and FP8 helpers
#include <tvm/ffi/container/tensor.h> // tvm::ffi::TensorView
#include <cstdint>
#include <cuda_fp8.h>
namespace {
using deepseek_v4::fp8::cast_to_ue8m0;
using deepseek_v4::fp8::inv_scale_ue8m0;
using deepseek_v4::fp8::pack_fp8;
constexpr float LOCAL_ABSMAX_ABS = 1e-10f;
constexpr uint32_t GROUP_SIZE = 128;
constexpr uint32_t THREADS_PER_GROUP = 8;
constexpr uint32_t SUBWARPS_PER_BLOCK = 16;
constexpr uint32_t INPUT_VEC_NUM_BYTES = 32;
constexpr uint32_t INPUT_INT4_SIZE = INPUT_VEC_NUM_BYTES / sizeof(int4);
template <int THREADS_PER_SUBWARP>
SGL_DEVICE float GroupReduceMax(float val) {
static_assert(
(THREADS_PER_SUBWARP & (THREADS_PER_SUBWARP - 1)) == 0 && THREADS_PER_SUBWARP <= 16 && THREADS_PER_SUBWARP >= 1,
"THREADS_PER_SUBWARP must be 1, 2, 4, 8, or 16");
// Tail subwarps can be inactive at the bounds check, so reduce with only the
// current subgroup's lanes rather than a full-warp mask.
constexpr device::warp::mask_t kSub = (device::warp::mask_t{1} << THREADS_PER_SUBWARP) - 1;
const device::warp::mask_t mask = kSub << (THREADS_PER_SUBWARP * ((threadIdx.x % 32) / THREADS_PER_SUBWARP));
return device::warp::reduce_max<THREADS_PER_SUBWARP>(val, mask);
}
template <typename T, bool kUsePDL>
__global__ void fp8_wo_a_group_major_quant_ue8m0_kernel(
const T* __restrict__ input,
fp8_e4m3_t* __restrict__ output_q,
float* __restrict__ output_s,
int64_t total_scale_groups,
int64_t num_tokens,
int hidden_dim_groups,
int num_outer_groups,
int64_t input_stride_t) {
device::PDLWaitPrimary<kUsePDL>();
const int64_t subwarp_id = threadIdx.x / THREADS_PER_GROUP;
const int lane_id = threadIdx.x % THREADS_PER_GROUP;
const int64_t group_id = static_cast<int64_t>(blockIdx.x) * SUBWARPS_PER_BLOCK + subwarp_id;
if (group_id < total_scale_groups) {
const int hidden_group = group_id % hidden_dim_groups;
const int64_t token_outer = group_id / hidden_dim_groups;
const int outer_idx = token_outer % num_outer_groups;
const int64_t token_idx = token_outer / num_outer_groups;
constexpr uint32_t INPUT_VEC_SIZE = INPUT_VEC_NUM_BYTES / sizeof(T);
static_assert(INPUT_VEC_SIZE * THREADS_PER_GROUP == GROUP_SIZE);
const int64_t input_group_start_offset =
token_idx * input_stride_t + outer_idx * GROUP_SIZE * hidden_dim_groups + hidden_group * GROUP_SIZE;
const int64_t output_group_start_offset = group_id * GROUP_SIZE;
int4 input_int4[INPUT_INT4_SIZE];
T* input_vec = reinterpret_cast<T*>(input_int4);
#pragma unroll
for (uint32_t j = 0; j < INPUT_INT4_SIZE; ++j) {
input_int4[j] = reinterpret_cast<const int4*>(input + input_group_start_offset + lane_id * INPUT_VEC_SIZE)[j];
}
float local_absmax = LOCAL_ABSMAX_ABS;
#pragma unroll
for (uint32_t j = 0; j < INPUT_VEC_SIZE; ++j) {
const float val = static_cast<float>(input_vec[j]);
local_absmax = fmaxf(local_absmax, fabsf(val));
}
local_absmax = GroupReduceMax<THREADS_PER_GROUP>(local_absmax);
constexpr float kFp8MaxInv = 1.0f / kFP8E4M3Max;
const int32_t scale_ue8m0 = cast_to_ue8m0(local_absmax * kFp8MaxInv);
const float y_scale = inv_scale_ue8m0(scale_ue8m0);
const float y_scale_inv = __uint_as_float(static_cast<uint32_t>(scale_ue8m0) << 23);
int4 output_buf;
auto* output_buf_ptr = reinterpret_cast<fp8x2_e4m3_t*>(&output_buf);
#pragma unroll
for (uint32_t j = 0; j < INPUT_VEC_SIZE; j += 2) {
output_buf_ptr[j / 2] =
pack_fp8(static_cast<float>(input_vec[j]) * y_scale, static_cast<float>(input_vec[j + 1]) * y_scale);
}
*reinterpret_cast<int4*>(output_q + output_group_start_offset + lane_id * INPUT_VEC_SIZE) = output_buf;
if (lane_id == 0) {
output_s[(outer_idx * num_tokens + token_idx) * hidden_dim_groups + hidden_group] = y_scale_inv;
}
}
device::PDLTriggerSecondary<kUsePDL>();
}
template <typename T, bool kUsePDL>
struct FP8WoAGroupMajorQuantUE8M0Kernel {
static void run(tvm::ffi::TensorView input, tvm::ffi::TensorView output_q, tvm::ffi::TensorView output_s) {
using namespace host;
auto device = SymbolicDevice{};
device.set_options<kDLCUDA>();
auto TSize = SymbolicSize{"num_tokens"};
auto GSize = SymbolicSize{"num_outer_groups"};
auto DSize = SymbolicSize{"hidden_dim"};
auto SSize = SymbolicSize{"hidden_dim_groups"};
TensorMatcher({TSize, GSize, DSize}).with_strides({-1, DSize, 1}).with_dtype<T>().with_device(device).verify(input);
TensorMatcher({TSize, GSize, DSize}).with_dtype<fp8_e4m3_t>().with_device(device).verify(output_q);
TensorMatcher({GSize, TSize, SSize}).with_dtype<float>().with_device(device).verify(output_s);
const auto num_tokens = TSize.unwrap();
const auto num_outer_groups = GSize.unwrap();
const auto hidden_dim = DSize.unwrap();
const auto hidden_dim_groups = SSize.unwrap();
const auto input_stride_t = input.stride(0);
constexpr int64_t kInputAlignElements = sizeof(int4) / sizeof(T);
RuntimeCheck(hidden_dim % GROUP_SIZE == 0, "hidden_dim must be divisible by 128");
RuntimeCheck(hidden_dim_groups == hidden_dim / GROUP_SIZE, "output_s hidden dim mismatch");
RuntimeCheck(
reinterpret_cast<uintptr_t>(input.data_ptr()) % sizeof(int4) == 0,
"input base pointer must be 16-byte aligned");
RuntimeCheck(
num_tokens <= 1 || input_stride_t % kInputAlignElements == 0,
"input token stride must preserve 16-byte vector-load alignment");
const int64_t total_scale_groups = num_tokens * num_outer_groups * hidden_dim_groups;
if (total_scale_groups == 0) return;
const auto grid = dim3((total_scale_groups + SUBWARPS_PER_BLOCK - 1) / SUBWARPS_PER_BLOCK);
const auto block = dim3(SUBWARPS_PER_BLOCK * THREADS_PER_GROUP);
host::LaunchKernel(grid, block, device.unwrap())
.enable_pdl(kUsePDL)(
fp8_wo_a_group_major_quant_ue8m0_kernel<T, kUsePDL>,
static_cast<const T*>(input.data_ptr()),
static_cast<fp8_e4m3_t*>(output_q.data_ptr()),
static_cast<float*>(output_s.data_ptr()),
total_scale_groups,
static_cast<int64_t>(num_tokens),
static_cast<int>(hidden_dim_groups),
static_cast<int>(num_outer_groups),
static_cast<int64_t>(input_stride_t));
}
};
} // namespace
@@ -19,6 +19,7 @@ from .elementwise import (
fused_q_norm_rope,
fused_rope_inplace,
)
from .fp8_wo_a import sglang_per_token_group_quant_fp8_dsv4_wo_a
from .gemm import linear_bf16_fp32
from .moe import (
hash_topk,
@@ -45,6 +46,7 @@ __all__ = [
"fused_q_indexer_rope_hadamard_fp4_quant",
"fused_q_indexer_rope_hadamard_quant",
"fused_k_norm_rope_flashmla",
"sglang_per_token_group_quant_fp8_dsv4_wo_a",
"make_name",
"linear_bf16_fp32",
"get_paged_mqa_logits_metadata",
+93
View File
@@ -0,0 +1,93 @@
from __future__ import annotations
from typing import TYPE_CHECKING, Tuple
import torch
from sglang.jit_kernel.utils import (
cache_once,
is_arch_support_pdl,
load_jit,
make_cpp_args,
)
from sglang.kernel_api_logging import debug_kernel_api
from sglang.srt.utils.custom_op import register_custom_op
from .utils import make_name
if TYPE_CHECKING:
from tvm_ffi.module import Module
_GROUP_SIZE = 128
@cache_once
def _jit_module(in_dtype: torch.dtype, use_pdl: bool) -> Module:
args = make_cpp_args(in_dtype, use_pdl)
return load_jit(
make_name("fp8_wo_a_group_major_quant_ue8m0"),
*args,
cuda_files=["deepseek_v4/fp8_wo_a_group_major_quant.cuh"],
cuda_wrappers=[
(
"fp8_wo_a_group_major_quant_ue8m0",
f"FP8WoAGroupMajorQuantUE8M0Kernel<{args}>::run",
)
],
# Match the AOT/JIT v2 quant path's fast-math build so FP8 rounding stays
# bit-identical for the DSV4 wo_a replacement.
extra_cuda_cflags=["--use_fast_math"],
)
@register_custom_op(
op_name="fp8_wo_a_group_major_quant_ue8m0",
mutates_args=["output_q", "output_s"],
)
def _fp8_wo_a_group_major_quant_ue8m0_custom_op(
input: torch.Tensor,
output_q: torch.Tensor,
output_s: torch.Tensor,
) -> None:
"""Opaque custom-op boundary for the DeepSeek-V4 wo_a quant JIT kernel."""
assert input.dtype in (torch.bfloat16, torch.float16)
module = _jit_module(input.dtype, is_arch_support_pdl())
module.fp8_wo_a_group_major_quant_ue8m0(input, output_q, output_s)
@debug_kernel_api
def fp8_wo_a_group_major_quant_ue8m0(
input: torch.Tensor,
output_q: torch.Tensor,
output_s: torch.Tensor,
) -> None:
_fp8_wo_a_group_major_quant_ue8m0_custom_op(input, output_q, output_s)
def sglang_per_token_group_quant_fp8_dsv4_wo_a(
x: torch.Tensor,
) -> Tuple[torch.Tensor, torch.Tensor]:
"""Quantize DSV4 wo_a activations for DeepGEMM fp8_einsum.
The input is a [T, G, D] bf16/fp16 tensor whose hidden dimension is
contiguous. The output codes are contiguous [T, G, D] fp8 values. The scale
tensor is returned as logical [T, G, D/128] fp32 UE8M0 values backed by
contiguous [G, T, D/128] storage, so each group/head [T, S] panel is
contiguous for the DeepGEMM recipe=(1, 1, 128) consumer. Group size is fixed
to 128 and the absmax floor is fixed to 1e-10.
"""
num_tokens, num_groups, hidden = x.shape
hidden_groups = hidden // _GROUP_SIZE
x_q = torch.empty(x.shape, device=x.device, dtype=torch.float8_e4m3fn)
x_s_storage = torch.empty(
(num_groups, num_tokens, hidden_groups),
device=x.device,
dtype=torch.float32,
)
if x.numel() > 0:
fp8_wo_a_group_major_quant_ue8m0(x, x_q, x_s_storage)
# DeepGEMM fp8_einsum consumes each group/head [T, S] scale panel contiguously.
return x_q, x_s_storage.transpose(0, 1)
+3 -7
View File
@@ -24,6 +24,7 @@ from sglang.jit_kernel.dsv4 import (
fused_norm_rope_inplace,
fused_q_norm_rope,
fused_rope_inplace,
sglang_per_token_group_quant_fp8_dsv4_wo_a,
)
from sglang.srt.compilation.compilation_config import register_split_op
from sglang.srt.configs.deepseek_v4 import DeepSeekV4Config
@@ -76,7 +77,6 @@ from sglang.srt.layers.linear import ColumnParallelLinear, RowParallelLinear
from sglang.srt.layers.logits_processor import LogitsProcessor
from sglang.srt.layers.moe import get_moe_a2a_backend, should_use_dp_reduce_scatterv
from sglang.srt.layers.moe.fused_moe_triton import FusedMoE
from sglang.srt.layers.quantization.fp8_kernel import sglang_per_token_group_quant_fp8
from sglang.srt.layers.rotary_embedding import get_rope_wrapper
from sglang.srt.layers.utils import PPMissingLayer, get_layer_id
from sglang.srt.layers.utils.cp_utils import (
@@ -1090,15 +1090,11 @@ class MQALayer(nn.Module):
T, G, D = o.shape
R = self.o_lora_rank
o_fp8, o_s = sglang_per_token_group_quant_fp8(
o.reshape(T * G, D).contiguous(),
group_size=128,
scale_ue8m0=True,
)
o_fp8, o_s = sglang_per_token_group_quant_fp8_dsv4_wo_a(o)
output = torch.empty(T, G, R, device=o.device, dtype=torch.bfloat16)
deep_gemm.fp8_einsum(
"bhr,hdr->bhd",
(o_fp8.view(T, G, D), o_s.view(T, G, -1)),
(o_fp8, o_s),
(self.wo_a.weight.view(G, R, D), self.wo_a.weight_scale_inv.data),
output,
recipe=(1, 1, 128),