[Perf][Kernel] Vectorized flat abs-max for dynamic per-tensor FP8 quantization (#58194)

Signed-off-by: Monishver Chandrasekaran <monishverchandrasekaran@gmail.com>
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
Co-authored-by: Misha Goin <mgoin64@gmail.com>
This commit is contained in:
Monishver
2026-09-25 09:19:48 -06:00
committed by GitHub
co-authored by Claude Fable 5.1 Misha Goin
parent f550ad8bf7
commit 38cc054fc2
2 changed files with 147 additions and 33 deletions
@@ -1,11 +1,60 @@
#include "../../../../quantization/w8a8/fp8/common.cuh"
#include "../../../../cuda_compat.h"
#include "../../../dispatch_utils.h"
#include "../../../cub_helpers.h"
#include "../../vectorization_utils.cuh"
#include "../../../torch_utils.h"
#include <torch/csrc/stable/macros.h>
#include <algorithm>
namespace vllm {
namespace {
// 16 fp8 outputs form one 16-byte store; the matching 16-element input load
// is 32 B for 16-bit types and 64 B for fp32.
constexpr int kFp8QuantVecSize = 16;
constexpr int kFp8QuantBlockSize = 256;
template <typename T>
using fp8_quant_vec_t = vec_n_t<T, kFp8QuantVecSize>;
template <typename T>
bool ptr_vec_aligned(const T* ptr) {
return reinterpret_cast<uintptr_t>(ptr) % alignof(fp8_quant_vec_t<T>) == 0;
}
__device__ __forceinline__ float warp_reduce_max(float v) {
#pragma unroll
for (int offset = WARP_SIZE / 2; offset > 0; offset >>= 1) {
v = fmaxf(v, VLLM_SHFL_XOR_SYNC(v, offset));
}
return v;
}
// Block-wide max, returned to every thread. Warp partials go through one
// shared-memory exchange, so the whole reduction costs a single
// __syncthreads. blockDim.x must be a multiple of WARP_SIZE and at most
// kFp8QuantBlockSize.
__device__ __forceinline__ float block_reduce_max(float v, float* warp_max) {
const int lane = threadIdx.x % WARP_SIZE;
const int warp = threadIdx.x / WARP_SIZE;
const int num_warps = blockDim.x / WARP_SIZE;
v = warp_reduce_max(v);
if (lane == 0) {
warp_max[warp] = v;
}
__syncthreads();
v = warp_max[0];
for (int w = 1; w < num_warps; ++w) {
v = fmaxf(v, warp_max[w]);
}
return v;
}
} // namespace
// STRIDE_I_ZERO: true if scale_stride_i == 0 (per-tensor or per-channel)
// STRIDE_J_ZERO: true if scale_stride_j == 0 (per-tensor or per-token)
template <typename scalar_t, typename fp8_type, bool STRIDE_I_ZERO,
@@ -75,42 +124,59 @@ __global__ void scaled_fp8_quant_kernel_strided_group_shape(
}
}
// Per-tensor abs-max of a contiguous tensor viewed as a flat array of
// 16-element vectors: grid-stride loop, one atomic per block.
template <typename scalar_t, typename fp8_type>
__global__ void segmented_max_reduction_strided(
float* __restrict__ scale, const scalar_t* __restrict__ input,
int hidden_size, int64_t in_row_stride, int64_t num_tokens) {
__shared__ float cache[256];
const int tid = threadIdx.x;
int64_t token_idx = blockIdx.x;
__global__ void __launch_bounds__(kFp8QuantBlockSize)
absmax_reduction_flat(float* __restrict__ scale,
const scalar_t* __restrict__ input,
int64_t num_vecs) {
using in_vec_t = fp8_quant_vec_t<scalar_t>;
const auto* v_in = reinterpret_cast<const in_vec_t*>(input);
const int64_t stride = static_cast<int64_t>(gridDim.x) * blockDim.x;
// one block per token. Guard in case gridDim.x > num_tokens.
float thread_max = 0.0f;
#pragma unroll 4
for (int64_t v = static_cast<int64_t>(blockIdx.x) * blockDim.x + threadIdx.x;
v < num_vecs; v += stride) {
const in_vec_t vec = v_in[v];
#pragma unroll
for (int i = 0; i < kFp8QuantVecSize; ++i) {
thread_max = fmaxf(thread_max, fabsf(static_cast<float>(vec.val[i])));
}
}
__shared__ float warp_max[kFp8QuantBlockSize / 32];
const float block_max = block_reduce_max(thread_max, warp_max);
if (threadIdx.x == 0) {
atomicMaxFloat(scale, block_max / quant_type_max_v<fp8_type>);
}
}
// Per-tensor abs-max for strided or unaligned rows: one block per token.
template <typename scalar_t, typename fp8_type>
__global__ void __launch_bounds__(kFp8QuantBlockSize)
segmented_max_reduction_strided(float* __restrict__ scale,
const scalar_t* __restrict__ input,
int hidden_size, int64_t in_row_stride,
int64_t num_tokens) {
const int64_t token_idx = blockIdx.x;
if (token_idx >= num_tokens) {
return;
}
const scalar_t* row_ptr = input + token_idx * in_row_stride;
// each thread scans elements of the row in a strided fashion.
float thread_max = 0.0f;
for (int e = tid; e < hidden_size; e += blockDim.x) {
float v = fabsf(static_cast<float>(row_ptr[e]));
thread_max = fmaxf(thread_max, v);
}
vectorize_read_with_alignment<kFp8QuantVecSize>(
row_ptr, hidden_size, threadIdx.x, blockDim.x,
[&] __device__(const scalar_t& v) {
thread_max = fmaxf(thread_max, fabsf(static_cast<float>(v)));
});
cache[tid] = thread_max;
__syncthreads();
// parallel reduction to find row max.
for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {
if (tid < offset) {
cache[tid] = fmaxf(cache[tid], cache[tid + offset]);
}
__syncthreads();
}
// thread 0 updates global scale (per-tensor) atomically.
if (tid == 0) {
atomicMaxFloat(scale, cache[0] / quant_type_max_v<fp8_type>);
__shared__ float warp_max[kFp8QuantBlockSize / 32];
const float block_max = block_reduce_max(thread_max, warp_max);
if (threadIdx.x == 0) {
atomicMaxFloat(scale, block_max / quant_type_max_v<fp8_type>);
}
}
@@ -345,7 +411,7 @@ void dynamic_scaled_fp8_quant(torch::stable::Tensor& out, // [..., d]
const int hidden_size = input.size(-1);
const int num_tokens = input.numel() / hidden_size;
const int block_size = 256;
const int block_size = vllm::kFp8QuantBlockSize;
dim3 grid(num_tokens);
dim3 block(block_size);
@@ -364,11 +430,29 @@ void dynamic_scaled_fp8_quant(torch::stable::Tensor& out, // [..., d]
input.scalar_type(), "scaled_fp8_quant_kernel_scalar_type", [&] {
VLLM_STABLE_DISPATCH_FP8_TYPES(
out.scalar_type(), "scaled_fp8_quant_kernel_fp8_type", [&] {
vllm::segmented_max_reduction_strided<scalar_t, fp8_t>
<<<grid, block, 0, stream>>>(
scale.mutable_data_ptr<float>(),
input.const_data_ptr<scalar_t>(), hidden_size,
in_row_stride, static_cast<int64_t>(num_tokens));
const scalar_t* in_ptr = input.const_data_ptr<scalar_t>();
const bool flat = in_row_stride == hidden_size &&
hidden_size % vllm::kFp8QuantVecSize == 0 &&
vllm::ptr_vec_aligned(in_ptr);
if (flat) {
const int64_t num_vecs = input.numel() / vllm::kFp8QuantVecSize;
const int64_t max_blocks =
static_cast<int64_t>(
get_device_prop()->multiProcessorCount) *
8;
const int64_t num_blocks = std::max<int64_t>(
1,
std::min<int64_t>(
max_blocks, (num_vecs + block_size - 1) / block_size));
vllm::absmax_reduction_flat<scalar_t, fp8_t>
<<<num_blocks, block, 0, stream>>>(
scale.mutable_data_ptr<float>(), in_ptr, num_vecs);
} else {
vllm::segmented_max_reduction_strided<scalar_t, fp8_t>
<<<grid, block, 0, stream>>>(
scale.mutable_data_ptr<float>(), in_ptr, hidden_size,
in_row_stride, static_cast<int64_t>(num_tokens));
}
vllm::scaled_fp8_quant_kernel_strided_dynamic<scalar_t, fp8_t>
<<<grid, block, 0, stream>>>(out.mutable_data_ptr<fp8_t>(),
@@ -108,6 +108,36 @@ def test_dynamic_per_tensor_fp8_quant(
opcheck_fp8_quant(ops_out, x)
@pytest.mark.parametrize("pad", [8, 16])
@pytest.mark.parametrize("dtype", DTYPES)
@pytest.mark.parametrize("seed", SEEDS)
@torch.inference_mode()
def test_dynamic_per_tensor_fp8_quant_strided_input(
pad: int, dtype: torch.dtype, seed: int
) -> None:
"""Non-contiguous rows take the strided abs-max kernel; contiguous rows take
the flat one. Both must produce exactly the same scale and bytes.
pad=8 (16 B for bf16) leaves rows misaligned for 32 B vector loads, pad=16
keeps them aligned but non-contiguous.
"""
set_random_seed(seed)
num_tokens, hidden_size = 64, 4096
full = torch.rand(num_tokens, hidden_size + pad, dtype=dtype, device="cuda")
x = full[:, :hidden_size]
assert not x.is_contiguous()
ops_out, ops_scale = ops.scaled_fp8_quant(x)
flat_out, flat_scale = ops.scaled_fp8_quant(x.contiguous())
assert torch.equal(ops_scale, flat_scale)
assert torch.equal(ops_out.view(torch.uint8), flat_out.view(torch.uint8))
ref_out, ref_scale = ref_dynamic_per_tensor_fp8_quant(x)
assert torch.equal(ref_scale, ops_scale)
assert torch.equal(ref_out.view(torch.uint8), ops_out.view(torch.uint8))
# Regression test for a case with large activations where an int32 index cannot
# represent the number of elements.
@torch.inference_mode()