Files
nvlabs--longlive/utils/nvfp4_kernel.py
T
2026-07-13 12:31:40 +08:00

296 lines
9.3 KiB
Python

#!/usr/bin/env python
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES
#
# Licensed under the Apache License, Version 2.0 (the "License").
# You may not use this file except in compliance with the License.
# To view a copy of this license, visit http://www.apache.org/licenses/LICENSE-2.0
#
# No warranties are given. The work is provided "AS IS", without warranty of any kind, express or implied.
#
# SPDX-License-Identifier: Apache-2.0
"""NVFP4 Fake Quantization Triton Implementation.
This module provides high-performance GPU implementations of NVFP4 fake quantization
operations using Triton kernels.
"""
import torch
import triton
import triton.language as tl
__all__ = ["fp4_dequantize", "static_blockwise_fp4_fake_quant"]
_TORCH_TO_TL_DTYPE = {
torch.float32: tl.float32,
torch.float: tl.float32,
torch.float16: tl.float16,
torch.half: tl.float16,
torch.bfloat16: tl.bfloat16,
}
def _torch_dtype_to_tl(dtype: torch.dtype):
if dtype not in _TORCH_TO_TL_DTYPE:
raise ValueError(f"Unsupported dtype for fp4 fake quantization: {dtype}")
return _TORCH_TO_TL_DTYPE[dtype]
@triton.jit
def fp4_dequantize_kernel(
packed_ptr,
scale_ptr,
global_scale_ptr,
output_ptr,
N,
BLOCK_SIZE: tl.constexpr,
TILE_SIZE: tl.constexpr,
):
"""Dequantizes FP4 packed data using per-block scaling factors.
Args:
packed_ptr (tl.pointer): Pointer to packed uint8 tensor (M x N//2)
scale_ptr (tl.pointer): Pointer to per-block scale tensor (M x N//BLOCK_SIZE)
output_ptr (tl.pointer): Pointer to output tensor (M x N)
global_scale_ptr (tl.pointer): Pointer to global scale tensor
N (int): Number of columns in unpacked tensor
BLOCK_SIZE (tl.constexpr): Size of each FP4 quantization block
TILE_SIZE (tl.constexpr): Size of the processing tile (in packed elements)
"""
# Get program ID for processing packed elements
pid = tl.program_id(0)
# Calculate packed element offsets (each packed element contains 2 FP4 values)
packed_start = pid * TILE_SIZE
packed_offs = packed_start + tl.arange(0, TILE_SIZE)
# Calculate 2D coordinates for packed data
packed_row_idx = packed_offs // (N // 2)
packed_col_idx = packed_offs % (N // 2)
# Create mask for packed data bounds checking
packed_mask = packed_col_idx < (N // 2)
# Load global scale
global_scale = tl.load(global_scale_ptr)
# Load packed data
packed_data = tl.load(packed_ptr + packed_offs, mask=packed_mask, other=0)
# Unpack packed FP4 values (uint8) to float16x2
x_f16x2_packed = tl.inline_asm_elementwise(
asm="""
{
.reg .b8 byte0, byte1, byte2, byte3;
mov.b32 {byte0, byte1, byte2, byte3}, $4;
cvt.rn.f16x2.e2m1x2 $0, byte0;
cvt.rn.f16x2.e2m1x2 $1, byte1;
cvt.rn.f16x2.e2m1x2 $2, byte2;
cvt.rn.f16x2.e2m1x2 $3, byte3;
}
""",
constraints="=r,=r,=r,=r,r",
args=[packed_data],
dtype=tl.uint32,
is_pure=True,
pack=4,
)
val_low = (
(x_f16x2_packed & 0xFFFF).cast(tl.uint16).cast(tl.float16, bitcast=True).cast(tl.float32)
)
val_high = (
(x_f16x2_packed >> 16).cast(tl.uint16).cast(tl.float16, bitcast=True).cast(tl.float32)
)
# Calculate output positions for both values
out_col_low = packed_col_idx * 2
out_col_high = packed_col_idx * 2 + 1
out_offs_low = packed_row_idx * N + out_col_low
out_offs_high = packed_row_idx * N + out_col_high
# Calculate block indices for scaling
block_col_low = out_col_low // BLOCK_SIZE
block_col_high = out_col_high // BLOCK_SIZE
scale_offs_low = packed_row_idx * (N // BLOCK_SIZE) + block_col_low
scale_offs_high = packed_row_idx * (N // BLOCK_SIZE) + block_col_high
# Load scaling factors
scale_low = tl.load(scale_ptr + scale_offs_low, mask=packed_mask & (out_col_low < N), other=1.0)
scale_high = tl.load(
scale_ptr + scale_offs_high, mask=packed_mask & (out_col_high < N), other=1.0
)
# Apply scaling
result_low = val_low * scale_low.to(tl.float32) * global_scale
result_high = val_high * scale_high.to(tl.float32) * global_scale
# Store results
out_mask_low = packed_mask & (out_col_low < N)
out_mask_high = packed_mask & (out_col_high < N)
tl.store(output_ptr + out_offs_low, result_low, mask=out_mask_low)
tl.store(output_ptr + out_offs_high, result_high, mask=out_mask_high)
def fp4_dequantize(
packed_tensor: torch.Tensor,
scale_tensor: torch.Tensor,
global_scale: torch.Tensor,
block_size: int = 16,
tile_size: int = 128,
dtype: torch.dtype = torch.get_default_dtype(),
) -> torch.Tensor:
"""Dequantizes FP4 packed tensor using per-block scaling factors.
Args:
packed_tensor (torch.Tensor): Packed uint8 tensor of shape (M, N//2)
scale_tensor (torch.Tensor): Per-block scale tensor of shape (M, N//block_size)
global_scale (torch.Tensor): Global scaling factor tensor
block_size (int): Size of FP4 quantization blocks
tile_size (int): Size of processing tiles
Returns:
torch.Tensor: Dequantized tensor of shape (M, N)
"""
packed_N = packed_tensor.shape[-1]
N = packed_N * 2
# Create output tensor with proper shape handling
output_shape = list(packed_tensor.shape)
output_shape[-1] = N
output = torch.empty(output_shape, dtype=dtype, device=packed_tensor.device)
# Calculate total number of elements and grid size
grid = lambda meta: (triton.cdiv(packed_tensor.numel(), meta["TILE_SIZE"]),)
fp4_dequantize_kernel[grid](
packed_tensor,
scale_tensor,
global_scale,
output,
N,
BLOCK_SIZE=block_size,
TILE_SIZE=tile_size,
)
return output
@triton.jit
def static_blockwise_fp4_fake_quant_kernel(
x_ptr, # [NUM_FP4_BLOCKS * BLOCK_SIZE]
y_ptr, # [NUM_FP4_BLOCKS * BLOCK_SIZE]
scale_ptr, # [NUM_FP4_BLOCKS]
NUM_FP4_BLOCKS,
BLOCK_SIZE: tl.constexpr,
OUT_DTYPE: tl.constexpr,
):
pid = tl.program_id(axis=0)
if pid >= NUM_FP4_BLOCKS:
return
block_offset = pid * BLOCK_SIZE
idx = block_offset + tl.arange(0, BLOCK_SIZE)
scale = tl.load(scale_ptr + pid).to(tl.float32)
x = tl.load(x_ptr + idx).to(tl.float32)
x_abs = tl.abs(x)
# If scale is 0, inf, or nan, use 1.0 (matching CUDA kernel behavior)
# Note: (x != x) checks if x is NaN per IEEE 754
scale_safe = tl.where(
(scale == 0) | (scale != scale) | (tl.abs(scale) == float("inf")), # noqa: PLR0124
1.0,
scale,
)
abs_scaled = x_abs / scale_safe
# FP4 values: 0.0, 0.5, 1.0, 1.5, 2.0, 3.0, 4.0, 6.0
q_val = tl.where(
abs_scaled <= 0.25,
0.0,
tl.where(
abs_scaled < 0.75,
0.5,
tl.where(
abs_scaled <= 1.25,
1.0,
tl.where(
abs_scaled < 1.75,
1.5,
tl.where(
abs_scaled <= 2.5,
2.0,
tl.where(
abs_scaled < 3.5,
3.0,
tl.where(abs_scaled <= 5.0, 4.0, 6.0),
),
),
),
),
),
)
x_rescaled = q_val * scale_safe
x_quant = tl.where(x >= 0, x_rescaled, -x_rescaled)
tl.store(y_ptr + idx, x_quant.to(OUT_DTYPE))
def static_blockwise_fp4_fake_quant(
x: torch.Tensor,
amax: torch.Tensor,
global_amax: torch.Tensor | None = None,
quantize_block_scales: bool = True,
out_dtype: torch.dtype | None = None,
):
"""Static blockwise FP4 fake quantization using Triton kernel.
Args:
x: [NUM_FP4_BLOCKS, BLOCK_SIZE] on CUDA.
amax: [NUM_FP4_BLOCKS] or [NUM_FP4_BLOCKS, 1] per-block amax values.
global_amax: FP32 scalar global amax. If provided, used to compute scale_fp8_quant_amax.
quantize_block_scales: If True, quantize block scales to FP8.
out_dtype: Output dtype. Defaults to x.dtype if None.
"""
assert x.ndim == 2
NUM_FP4_BLOCKS, BLOCK_SIZE = x.shape
if out_dtype is None:
out_dtype = x.dtype
amax = amax.float() # Requires to be in float32
scale = amax / 6.0 # FP4 max representable value is 6.0
if quantize_block_scales:
from modelopt.torch.quantization.tensor_quant import scaled_e4m3_impl
from modelopt.torch.quantization.utils import reduce_amax
if global_amax is None:
global_amax = reduce_amax(amax, axis=None, keepdims=False, squeeze_scalar=True)
global_amax = global_amax.float()
scale_fp8_quant_amax = global_amax / 6.0
scale = scaled_e4m3_impl(scale, scale_fp8_quant_amax)
x_flat = x.contiguous().view(-1)
y_flat = torch.empty_like(x_flat, dtype=out_dtype)
scale_flat = scale.view(NUM_FP4_BLOCKS).contiguous()
tl_out_dtype = _torch_dtype_to_tl(out_dtype)
grid = (NUM_FP4_BLOCKS,)
with torch.cuda.device(x.device):
static_blockwise_fp4_fake_quant_kernel[grid](
x_flat,
y_flat,
scale_flat,
NUM_FP4_BLOCKS,
BLOCK_SIZE,
OUT_DTYPE=tl_out_dtype,
)
return y_flat.view_as(x)