| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| from typing import Optional, Tuple |
|
|
| import torch |
|
|
| from ..kernels.python.gemm import fp8_gemm_triton_block |
| from lightx2v_kernel.gemm import ( |
| cutlass_scaled_mxfp4_mm, |
| cutlass_scaled_mxfp6_mxfp8_mm, |
| cutlass_scaled_mxfp8_mm, |
| cutlass_scaled_nvfp4_mm, |
| scaled_mxfp4_quant, |
| scaled_mxfp6_quant, |
| scaled_mxfp8_quant, |
| scaled_nvfp4_quant, |
| ) |
| from ..kernels.python.quantizers import ( |
| fp8_per_block_quant_triton, |
| fp8_per_token_group_quant_triton, |
| ) |
| from .utils import QuantType, _ensure_deep_gemm, _ensure_sgl_kernel |
|
|
| FP8_MAX = float(torch.finfo(torch.float8_e4m3fn).max) |
| FP8_MIN = float(torch.finfo(torch.float8_e4m3fn).min) |
|
|
|
|
| |
| |
| def fp8_per_tensor_quant(x: torch.Tensor) -> Tuple[torch.Tensor, float]: |
| """Dynamically Quantize a tensor using per-tensor static scaling factor. |
| Args: |
| x: The input tensor. |
| """ |
| if x.numel() == 0: |
| |
| min_val, max_val = ( |
| torch.tensor(-16.0, dtype=x.dtype), |
| torch.tensor(16.0, dtype=x.dtype), |
| ) |
| else: |
| min_val, max_val = x.aminmax() |
| amax = torch.maximum(min_val.abs(), max_val.abs()) |
| scale = FP8_MAX / amax.clamp(min=1e-12) |
| qx = (x * scale).clamp(min=FP8_MIN, max=FP8_MAX) |
| qx = qx.to(torch.float8_e4m3fn) |
| scale = scale.float().reciprocal() |
| return qx, scale |
|
|
|
|
| |
| def fp8_per_token_group_quant( |
| x: torch.Tensor, |
| group_size: int, |
| eps: float = 1e-10, |
| dtype: torch.dtype = torch.float8_e4m3fn, |
| column_major_scales: bool = False, |
| scale_tma_aligned: bool = False, |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Per-token-group FP8 quantization with automatic backend selection. |
| |
| Uses Triton kernel on GPU when available, otherwise falls back to PyTorch. |
| |
| Args: |
| x: Input tensor |
| group_size: Size of each quantization group |
| eps: Small value to avoid division by zero |
| dtype: Target FP8 data type |
| column_major_scales: Whether to use column-major scale layout |
| scale_tma_aligned: Whether to use TMA-aligned scales |
| |
| Returns: |
| Tuple of (quantized_tensor, scale_tensor) |
| """ |
| |
| |
| return fp8_per_token_group_quant_triton( |
| x, |
| group_size, |
| eps, |
| dtype, |
| column_major_scales, |
| scale_tma_aligned, |
| ) |
|
|
|
|
| def fp8_per_channel_quant(weight: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Per-channel FP8 weight quantization (E4M3 format) |
| |
| Args: |
| weight: Original weight tensor with shape [out_features, in_features] |
| |
| Returns: |
| weight_quant: Quantized weight [out_features, in_features], dtype=float8_e4m3fn |
| weight_scale: Scale factors [out_features, 1], dtype=float32 |
| """ |
| abs_max = torch.abs(weight).amax(dim=1, keepdim=True) |
|
|
| weight_scale = abs_max / FP8_MAX |
| weight_scale = torch.clamp(weight_scale, min=1e-12) |
|
|
| weight_scaled = (weight / weight_scale).clamp(min=FP8_MIN, max=FP8_MAX) |
| weight_quant = weight_scaled.to(torch.float8_e4m3fn) |
|
|
| return weight_quant, weight_scale.float() |
|
|
|
|
| def int8_per_channel_quant(weight: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Per-channel symmetric INT8 quantization for linear weights. |
| |
| Args: |
| weight: Weight tensor with shape [out_features, in_features] |
| |
| Returns: |
| weight_quant: Quantized INT8 weight [out_features, in_features] |
| weight_scale: Per-channel scales [out_features, 1], float32 |
| """ |
| abs_max = torch.abs(weight).amax(dim=1, keepdim=True).clamp(min=1e-5) |
| qmin, qmax = -128, 127 |
| weight_scale = (abs_max / qmax).to(torch.float32) |
| weight_quant = torch.clamp(torch.round(weight / weight_scale), qmin, qmax).to(torch.int8) |
| return weight_quant, weight_scale |
|
|
|
|
| def nvfp4_per_tensor_quant( |
| x: torch.Tensor, input_global_scale: Optional[torch.Tensor] = None |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| """ |
| Per-tensor NVFP4 quantization using LightX2V kernel. |
| |
| Returns packed uint8 values and per-block quantized scales. |
| """ |
| if x.dim() != 2: |
| raise ValueError(f"nvfp4_per_tensor_quant expects 2D tensor, but got shape={tuple(x.shape)}") |
|
|
| if input_global_scale is None: |
| x_absmax = torch.abs(x).amax().float().clamp(min=1e-12) |
| input_global_scale = (2688.0 / x_absmax).to(torch.float32) |
| else: |
| input_global_scale = input_global_scale.to(device=x.device, dtype=torch.float32) |
|
|
| qx, qscale = scaled_nvfp4_quant(x, input_global_scale) |
| return qx, qscale, input_global_scale |
|
|
|
|
| def mxfp4_per_tensor_quant( |
| x: torch.Tensor, |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| """ |
| Per-tensor MXFP4 quantization using LightX2V kernel. |
| |
| Returns packed uint8 values and per-block quantized scales. |
| """ |
| if x.dim() != 2: |
| raise ValueError(f"mxfp4_per_tensor_quant expects 2D tensor, but got shape={tuple(x.shape)}") |
|
|
| qx, qscale = scaled_mxfp4_quant(x) |
| |
| input_global_scale = torch.tensor(1.0, device=x.device, dtype=torch.float32) |
| return qx, qscale, input_global_scale |
|
|
|
|
| def mxfp6_per_tensor_quant( |
| x: torch.Tensor, |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| """ |
| Per-tensor MXFP6 quantization using LightX2V kernel. |
| |
| Returns packed uint8 values and per-block quantized scales. |
| """ |
| if x.dim() != 2: |
| raise ValueError(f"mxfp6_per_tensor_quant expects 2D tensor, but got shape={tuple(x.shape)}") |
|
|
| qx, qscale = scaled_mxfp6_quant(x) |
| input_global_scale = torch.tensor(1.0, device=x.device, dtype=torch.float32) |
| return qx, qscale, input_global_scale |
|
|
|
|
| def mxfp8_per_tensor_quant( |
| x: torch.Tensor, |
| ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]: |
| """ |
| Per-tensor MXFP8 quantization using LightX2V kernel. |
| |
| Returns packed uint8 values and per-block quantized scales. |
| """ |
| if x.dim() != 2: |
| raise ValueError(f"mxfp8_per_tensor_quant expects 2D tensor, but got shape={tuple(x.shape)}") |
|
|
| qx, qscale = scaled_mxfp8_quant(x) |
| input_global_scale = torch.tensor(1.0, device=x.device, dtype=torch.float32) |
| return qx, qscale, input_global_scale |
|
|
|
|
| def fp8_per_token_quant_sgl(x: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]: |
| m, k = x.shape |
| input_tensor_quant = torch.empty( |
| (m, k), dtype=torch.float8_e4m3fn, device="cuda", requires_grad=False |
| ) |
| input_tensor_scale = torch.empty( |
| (m, 1), dtype=torch.float32, device="cuda", requires_grad=False |
| ) |
| _sgl_kernel = _ensure_sgl_kernel() |
| _sgl_kernel.sgl_per_token_quant_fp8(x, input_tensor_quant, input_tensor_scale) |
| return input_tensor_quant, input_tensor_scale |
|
|
|
|
| |
| def fp8_per_block_quant_torch( |
| x: torch.Tensor, block_size: int = 128 |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Pure Torch implementation of block-wise FP8 (e4m3fn) quantization. |
| |
| Args: |
| x (torch.Tensor): 2D tensor (M, N) in float types. |
| block_size (int): Block size for both M and N dimensions. |
| |
| Returns: |
| Tuple[torch.Tensor, torch.Tensor]: |
| - y: quantized tensor with dtype=torch.float8_e4m3fn and same |
| shape/device as x |
| - s: per-block scale tensor with shape (ceil(M/bs), ceil(N/bs)) on |
| x.device |
| """ |
| assert x.is_contiguous() |
| assert x.dim() == 2 |
|
|
| M, N = x.size() |
| device = x.device |
|
|
| |
| y = torch.empty_like(x, dtype=torch.float8_e4m3fn) |
| m_blocks = (M + block_size - 1) // block_size |
| n_blocks = (N + block_size - 1) // block_size |
| s = torch.empty((m_blocks, n_blocks), dtype=torch.float32, device=device) |
|
|
| |
| for mb in range(m_blocks): |
| m_start = mb * block_size |
| m_end = min(m_start + block_size, M) |
| for nb in range(n_blocks): |
| n_start = nb * block_size |
| n_end = min(n_start + block_size, N) |
|
|
| x_block = x[m_start:m_end, n_start:n_end].to(torch.float32) |
| |
| max_val = x_block.abs().amax().to(torch.float32) |
| scale = max_val / FP8_MAX |
| if scale.item() == 0.0: |
| scale = torch.tensor(1.0, dtype=torch.float32, device=device) |
|
|
| |
| y_block = (x_block / scale).to(torch.float8_e4m3fn) |
|
|
| |
| y[m_start:m_end, n_start:n_end] = y_block |
| s[mb, nb] = scale |
|
|
| return y, s |
|
|
|
|
| |
| def fp8_per_block_quant( |
| x: torch.Tensor, block_size: int = 128 |
| ) -> Tuple[torch.Tensor, torch.Tensor]: |
| """ |
| Per-block FP8 quantization with automatic GPU/CPU dispatch. |
| |
| Quantizes a FP32 2D tensor to FP8 (E4M3FN) using block-wise quantization. |
| For each (block_size x block_size) block: |
| - scale = max(abs(block)) / 448.0 (FP8 E4M3FN max magnitude) |
| - if block is all zeros, use scale = 1.0 to avoid div-by-zero |
| - scale, clamp and cast to FP8 |
| |
| Args: |
| x: Input tensor (2D) |
| block_size: Block size for quantization |
| |
| Returns: |
| Tuple of (quantized_tensor, scale_tensor): |
| - y: Quantized FP8 tensor, same shape as input |
| - s: Per-block scales, shape (num_blocks_M, num_blocks_N) |
| """ |
| if x.is_cuda: |
| return fp8_per_block_quant_triton(x, block_size) |
| else: |
| return fp8_per_block_quant_torch(x, block_size) |
|
|
|
|
| |
| def fp8_gemm_deepgemm_block( |
| a: torch.Tensor, |
| a_s: torch.Tensor, |
| b: torch.Tensor, |
| b_s: torch.Tensor, |
| out_dtype: torch.dtype = torch.bfloat16, |
| bias: Optional[torch.Tensor] = None, |
| origin_shape: Optional[Tuple[int, ...]] = None, |
| ) -> torch.Tensor: |
| a_fp8 = (a, a_s) |
| b_fp8 = (b, b_s) |
| out = torch.empty((a.shape[0], b.shape[0]), device=a.device, dtype=torch.bfloat16) |
|
|
| _deep_gemm = _ensure_deep_gemm() |
| _deep_gemm.fp8_gemm_nt(a_fp8, b_fp8, out) |
|
|
| if origin_shape is not None: |
| out = out.reshape([*origin_shape[:-1], b.shape[0]]) |
| if bias is not None: |
| out += bias |
|
|
| return out.to(out_dtype) |
|
|
|
|
| |
| def fp8_gemm_torch_tensor_token( |
| a: torch.Tensor, |
| a_s: torch.Tensor, |
| b: torch.Tensor, |
| b_s: torch.Tensor, |
| out_dtype: torch.dtype = torch.bfloat16, |
| bias: Optional[torch.Tensor] = None, |
| ) -> torch.Tensor: |
| need_reshape = a.dim() == 3 |
| if need_reshape: |
| batch_size = a.shape[0] |
| A_input = a.reshape(-1, a.shape[-1]) |
| else: |
| batch_size = None |
| A_input = a |
|
|
| output = torch._scaled_mm( |
| A_input, |
| b.t(), |
| out_dtype=out_dtype, |
| scale_a=a_s, |
| scale_b=b_s, |
| bias=bias, |
| ) |
| |
| if isinstance(output, tuple): |
| output = output[0] |
|
|
| if need_reshape: |
| output = output.reshape(batch_size, output.shape[0] // batch_size, output.shape[1]) |
|
|
| return output |
|
|
|
|
| def fp8_weight_only_gemm(A, B, B_scale, bias, out_dtype): |
| """Perform FP8 GEMM operation with fallback to standard linear. |
| |
| Args: |
| A: Input tensor A. |
| B: Input tensor B. |
| B_scale: Scale factor for tensor B. |
| bias: Optional bias tensor. |
| out_dtype: Output data type. |
| native_fp8_support: Whether to use native FP8 support. |
| quant_type: Quantization type. |
| origin_shape: Original shape for reshaping. |
| |
| Returns: |
| torch.Tensor: Result of the GEMM operation. |
| """ |
| if A.numel() == 0: |
| return torch.empty(size=(0, B.shape[0]), dtype=out_dtype, device=A.device) |
|
|
| output = torch.nn.functional.linear( |
| A.to(out_dtype), |
| B.to(out_dtype) * B_scale.to(out_dtype), |
| bias=bias, |
| ) |
|
|
| return output |
|
|
|
|
| def fp8_gemm_sgl_token(A, A_scale, B, B_scale, out_dtype, bias): |
| """GEMM function for FP8 per-token-sgl quantization using sgl-kernel. |
| |
| Args: |
| A: Input activation tensor |
| A_scale: Scale tensor for input activations |
| B: Weight tensor |
| B_scale: Scale tensor for weights |
| out_dtype: Output data type. |
| bias: Optional bias tensor |
| |
| Returns: |
| torch.Tensor: Result of the GEMM operation. |
| """ |
| _sgl_kernel = _ensure_sgl_kernel() |
| shape = (A.shape[0], B.shape[0]) |
| output = torch.empty(shape, dtype=out_dtype, device=A.device, requires_grad=False) |
| output = _sgl_kernel.fp8_scaled_mm( |
| A, |
| B.t(), |
| A_scale, |
| B_scale.float(), |
| out_dtype, |
| bias=bias, |
| ) |
|
|
| return output |
|
|
|
|
| def fp8_gemm( |
| A: torch.Tensor, |
| A_scale: torch.Tensor, |
| B: torch.Tensor, |
| B_scale: torch.Tensor, |
| bias: Optional[torch.Tensor], |
| out_dtype: torch.dtype, |
| native_fp8_support: bool = False, |
| quant_type: str = QuantType.FP8_PER_TENSOR, |
| origin_shape: Optional[Tuple[int, ...]] = None, |
| ) -> torch.Tensor: |
| """ |
| Unified GEMM function for FP8 quantization. |
| |
| Args: |
| A: Input activation tensor |
| A_scale: Scale tensor for input activations |
| B: Weight tensor |
| B_scale: Scale tensor for weights |
| bias: Optional bias tensor |
| out_dtype: Output data type |
| native_fp8_support: Whether to use native FP8 kernels |
| quant_type: Quantization type (QuantType enum values) |
| origin_shape: Original shape for reshaping output |
| |
| Returns: |
| Output tensor after GEMM operation |
| |
| Raises: |
| ValueError: If unsupported combination of parameters is provided |
| """ |
| |
| if A.numel() == 0: |
| return torch.empty(size=(0, B.shape[0]), dtype=out_dtype, device=A.device) |
|
|
| if quant_type == QuantType.FP8_PER_CHANNEL_VLLM: |
| if not hasattr(torch.ops, "_C") or not hasattr(torch.ops._C, "cutlass_scaled_mm"): |
| raise RuntimeError( |
| "quant_type='fp8-per-channel-vllm' requires torch.ops._C.cutlass_scaled_mm" |
| ) |
| output = torch.empty( |
| (A.shape[0], B.shape[0]), |
| dtype=out_dtype, |
| device=A.device, |
| requires_grad=False, |
| ) |
| weight_scale = B_scale |
| if ( |
| weight_scale.dim() == 2 |
| and weight_scale.shape[0] == B.shape[0] |
| and weight_scale.shape[1] == 1 |
| ): |
| weight_scale = weight_scale.t() |
| torch.ops._C.cutlass_scaled_mm( |
| output, |
| A, |
| B.t(), |
| A_scale, |
| weight_scale, |
| bias, |
| ) |
| return output |
|
|
| |
| if native_fp8_support: |
| if quant_type in (QuantType.FP8_PER_TENSOR, QuantType.FP8_PER_TOKEN): |
| |
| return fp8_gemm_torch_tensor_token(A, A_scale, B, B_scale, out_dtype, bias) |
| elif quant_type == QuantType.FP8_PER_TOKEN_SGL: |
| |
| return fp8_gemm_sgl_token(A, A_scale, B, B_scale, out_dtype, bias) |
| elif quant_type == QuantType.FP8_PER_BLOCK: |
| |
| return fp8_gemm_deepgemm_block(A, A_scale, B, B_scale, out_dtype, bias, origin_shape) |
| else: |
| if quant_type == QuantType.FP8_PER_BLOCK: |
| |
| return fp8_gemm_triton_block(A, A_scale, B, B_scale, out_dtype, bias) |
| elif quant_type == QuantType.FP8_PER_TENSOR: |
| |
| return torch.nn.functional.linear( |
| A.to(out_dtype) * A_scale, |
| B.to(out_dtype) * B_scale.to(out_dtype), |
| bias=bias, |
| ) |
|
|
| |
| raise ValueError( |
| f"Unsupported combination: " |
| f"\n quant_type={quant_type}," |
| f"\n native_fp8_support={native_fp8_support}.\n" |
| "Supported combinations:\n" |
| " - native_fp8_support=True, " |
| "quant_type in [fp8-per-tensor, fp8-per-token," |
| " fp8-per-block, fp8-per-token-sgl]\n" |
| " - native_fp8_support=False, " |
| "quant_type in [fp8-per-tensor, fp8-per-block]" |
| ) |
|
|