Spaces:
Running on Zero
Running on Zero
| # Copyright 2025 Tencent Inc. All Rights Reserved. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| 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) | |
| # quant function for per-tensor fp8 | |
| # modified from https://github.com/neuralmagic/AutoFP8/blob/main/auto_fp8/quantize.py | |
| 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: | |
| # handle empty tensor for empty MoE experts | |
| 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 | |
| # quant function for per-token and per-group fp8 | |
| 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) | |
| """ | |
| # fp8_per_token_group_quant_triton is automatically selected based on | |
| # backend availability (Triton vs PyTorch) via __init__.py | |
| 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) # [out_features, 1] | |
| 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) | |
| # Keep a compatible triplet return signature with NVFP4 paths. | |
| 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 | |
| # pure torch implementation of block-wise FP8 quantization on cpu | |
| 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 | |
| # Output tensors | |
| 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) | |
| # Iterate over blocks | |
| 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) | |
| # Compute per-block scale: max(abs(x))/448.0, guard zero to 1.0 | |
| 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) | |
| # Quantize block | |
| y_block = (x_block / scale).to(torch.float8_e4m3fn) | |
| # Store results | |
| y[m_start:m_end, n_start:n_end] = y_block | |
| s[mb, nb] = scale | |
| return y, s | |
| # quant function for per-block fp8 | |
| 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) | |
| # gemm function for per-block quantization fp8 using deepgemm | |
| 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) | |
| # gemm function for per-token and per-group fp8 quantization using torch native api | |
| 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 output is a tuple, take the first element | |
| 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 | |
| """ | |
| # Return empty tensor if the input is empty | |
| 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 | |
| # Prioritize native fp8 support if available | |
| if native_fp8_support: | |
| if quant_type in (QuantType.FP8_PER_TENSOR, QuantType.FP8_PER_TOKEN): | |
| # Use torch native fp8 GEMM for per-tensor and per-token fp8 quantization | |
| return fp8_gemm_torch_tensor_token(A, A_scale, B, B_scale, out_dtype, bias) | |
| elif quant_type == QuantType.FP8_PER_TOKEN_SGL: | |
| # Use sgl-kernel for per-token-sgl fp8 quantization | |
| return fp8_gemm_sgl_token(A, A_scale, B, B_scale, out_dtype, bias) | |
| elif quant_type == QuantType.FP8_PER_BLOCK: | |
| # Use deepgemm accelerated blockwise fp8 GEMM | |
| return fp8_gemm_deepgemm_block(A, A_scale, B, B_scale, out_dtype, bias, origin_shape) | |
| else: | |
| if quant_type == QuantType.FP8_PER_BLOCK: | |
| # Use triton kernel for blockwise fp8 quantization | |
| return fp8_gemm_triton_block(A, A_scale, B, B_scale, out_dtype, bias) | |
| elif quant_type == QuantType.FP8_PER_TENSOR: | |
| # Fall back to scaled linear for per-tensor fp8 without native fp8 kernel | |
| return torch.nn.functional.linear( | |
| A.to(out_dtype) * A_scale, | |
| B.to(out_dtype) * B_scale.to(out_dtype), | |
| bias=bias, | |
| ) | |
| # Raise error for unsupported combination of quant_type and native_fp8_support | |
| 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]" | |
| ) | |