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. | |
| import os | |
| import time | |
| import torch | |
| from lightx2v_kernel.gemm import ( | |
| cutlass_scaled_mxfp4_mm, | |
| cutlass_scaled_mxfp6_mxfp8_mm, | |
| cutlass_scaled_mxfp8_mm, | |
| cutlass_scaled_nvfp4_mm, | |
| ) | |
| try: | |
| from torchao.quantization.utils import quant_int8_per_token_matmul as torchao_int8_gemm | |
| from torchao.quantization.utils import quantize_activation_per_token_absmax as torchao_int8_quant | |
| except ImportError: | |
| try: | |
| from torchao.quantization.utils import _quant_int8_per_token_matmul as torchao_int8_gemm | |
| from torchao.quantization.utils import _quantize_activation_per_token_absmax as torchao_int8_quant | |
| except ImportError: | |
| torchao_int8_gemm, torchao_int8_quant = None, None | |
| try: | |
| from vllm import _custom_ops as vllm_ops | |
| except ImportError: | |
| vllm_ops = None | |
| try: | |
| from ...kernels.python.sgl.int8_kernel import per_token_quant_int8 as sglang_int8_act_quant | |
| except ImportError: | |
| sglang_int8_act_quant = None | |
| try: | |
| import sgl_kernel | |
| except ImportError: | |
| sgl_kernel = None | |
| try: | |
| from q8_kernels.functional.linear import q8_linear | |
| except ImportError: | |
| q8_linear = None | |
| try: | |
| from ...kernels.python.mm.triton_kernels import ( | |
| int8_gemm_bias_triton, | |
| int8_gemm_triton, | |
| int8_quantize_triton, | |
| ) | |
| except ImportError: | |
| int8_gemm_bias_triton, int8_gemm_triton, int8_quantize_triton = None, None, None | |
| from ..quant_func import ( | |
| fp8_gemm, | |
| fp8_per_block_quant, | |
| fp8_per_tensor_quant, | |
| fp8_per_token_group_quant, | |
| fp8_per_token_quant_sgl, | |
| fp8_weight_only_gemm, | |
| mxfp4_per_tensor_quant, | |
| mxfp6_per_tensor_quant, | |
| mxfp8_per_tensor_quant, | |
| nvfp4_per_tensor_quant, | |
| ) | |
| # modified from https://github.com/neuralmagic/AutoFP8/blob/main/auto_fp8/quantize.py | |
| class FP8DynamicLinear(torch.nn.Module): | |
| def __init__( | |
| self, | |
| weight: torch.Tensor, | |
| weight_scale: torch.Tensor, | |
| bias: torch.nn.Parameter, | |
| native_fp8_support: bool = False, | |
| quant_type: str = "fp8-per-tensor", | |
| block_size: int = 128, | |
| ): | |
| super().__init__() | |
| self.weight = torch.nn.Parameter(weight, requires_grad=False) | |
| self.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False) | |
| self.bias = bias | |
| self.native_fp8_support = native_fp8_support | |
| self.quant_type = quant_type | |
| self.block_size = block_size | |
| self.profile_enabled = os.environ.get("ANGELSLIM_FP8_PROFILE", "0") == "1" | |
| def forward(self, x): | |
| ori_dtype = x.dtype | |
| assert ori_dtype in [ | |
| torch.float32, | |
| torch.bfloat16, | |
| torch.float16, | |
| ], "x.dtype must be float32, bfloat16, or float16" | |
| if ori_dtype == torch.float32: | |
| x = x.to(torch.bfloat16) | |
| if self.profile_enabled and x.is_cuda: | |
| torch.cuda.synchronize(x.device) | |
| t0 = time.perf_counter() | |
| if self.quant_type == "fp8-per-tensor": | |
| origin_shape = None | |
| qinput, x_scale = fp8_per_tensor_quant(x) | |
| elif self.quant_type == "fp8-per-token": | |
| origin_shape = None | |
| x_2d = x.view(-1, x.shape[-1]) | |
| qinput, x_scale = fp8_per_token_group_quant(x_2d, x_2d.shape[-1]) | |
| elif self.quant_type == "fp8-per-token-sgl" and self.native_fp8_support: | |
| origin_shape = x.shape | |
| x_2d = x.view(-1, x.shape[-1]) | |
| qinput, x_scale = fp8_per_token_quant_sgl(x_2d) | |
| elif self.quant_type == "fp8-per-block" and self.native_fp8_support: | |
| origin_shape = x.shape | |
| x = x.view(-1, x.shape[-1]) | |
| qinput, x_scale = fp8_per_token_group_quant( | |
| x, group_size=128, column_major_scales=True, scale_tma_aligned=True | |
| ) | |
| elif self.quant_type == "fp8-per-block" and not self.native_fp8_support: | |
| origin_shape = x.shape | |
| x_2d = x.view(-1, x.shape[-1]) | |
| qinput, x_scale = fp8_per_block_quant(x_2d, block_size=128) | |
| elif self.quant_type == "fp8-per-channel-vllm": | |
| if vllm_ops is None: | |
| raise ImportError( | |
| "quant_type='fp8-per-channel-vllm' requires vllm._custom_ops, but vllm is not installed" | |
| ) | |
| origin_shape = x.shape if x.dim() == 3 else None | |
| x_2d = x.view(-1, x.shape[-1]) if x.dim() == 3 else x | |
| qinput, x_scale = vllm_ops.scaled_fp8_quant( | |
| x_2d, None, scale_ub=None, use_per_token_if_dynamic=True | |
| ) | |
| else: | |
| raise ValueError(f"Invalid quant_type: {self.quant_type}") | |
| if self.profile_enabled and qinput.is_cuda: | |
| torch.cuda.synchronize(qinput.device) | |
| t1 = time.perf_counter() | |
| output = fp8_gemm( | |
| A=qinput, | |
| A_scale=x_scale, | |
| B=self.weight, | |
| B_scale=self.weight_scale, | |
| bias=self.bias, | |
| out_dtype=x.dtype, | |
| native_fp8_support=self.native_fp8_support, | |
| quant_type=self.quant_type, | |
| origin_shape=origin_shape, | |
| ) | |
| if self.profile_enabled and output.is_cuda: | |
| torch.cuda.synchronize(output.device) | |
| t2 = time.perf_counter() | |
| if self.profile_enabled: | |
| qshape = tuple(qinput.shape) | |
| print( | |
| f"[FP8Linear:{self.quant_type}] quant_ms={(t1 - t0) * 1000:.3f}, " | |
| f"gemm_ms={(t2 - t1) * 1000:.3f}, qshape={qshape}" | |
| ) | |
| if ( | |
| self.quant_type in ["fp8-per-token", "fp8-per-token-sgl"] | |
| and x.dim() == 3 | |
| and output.dim() == 2 | |
| ): | |
| output = output.unsqueeze(0) | |
| # Restore original shape for fp8-per-block with native_fp8_support=False | |
| # (native_fp8_support=True case is handled in fp8_gemm_deepgemm_block) | |
| if ( | |
| ( | |
| (self.quant_type == "fp8-per-block" and not self.native_fp8_support) | |
| or self.quant_type == "fp8-per-channel-vllm" | |
| ) | |
| and origin_shape is not None | |
| and len(origin_shape) == 3 | |
| and output.dim() == 2 | |
| ): | |
| output = output.view(origin_shape[0], origin_shape[1], -1) | |
| return output | |
| class FP8WeightOnlyLinear(torch.nn.Module): | |
| """ | |
| FP8 Weight-Only Quantized Linear Layer. | |
| This layer quantizes only the weights to FP8 while keeping activations | |
| in higher precision (bfloat16/float16). This provides a good balance | |
| between memory savings and accuracy. | |
| """ | |
| def __init__( | |
| self, | |
| weight: torch.Tensor, | |
| weight_scale: torch.Tensor, | |
| bias: torch.nn.Parameter, | |
| native_fp8_support: bool = False, # not used | |
| quant_type: str = "fp8-per-tensor-weight-only", | |
| ): | |
| super().__init__() | |
| self.weight = torch.nn.Parameter(weight, requires_grad=False) | |
| self.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False) | |
| self.bias = bias | |
| self.native_fp8_support = native_fp8_support # not used | |
| self.quant_type = quant_type | |
| def forward(self, x): | |
| ori_dtype = x.dtype | |
| assert ori_dtype in [ | |
| torch.float32, | |
| torch.bfloat16, | |
| torch.float16, | |
| ], "x.dtype must be float32, bfloat16, or float16" | |
| if ori_dtype == torch.float32: | |
| x = x.to(torch.bfloat16) | |
| # For weight-only quantization, we don't quantize activations | |
| # Just use the original activations with quantized weights | |
| output = fp8_weight_only_gemm( | |
| A=x, # Keep activations in original precision | |
| B=self.weight, | |
| B_scale=self.weight_scale, | |
| bias=self.bias, | |
| out_dtype=x.dtype, | |
| ) | |
| return output | |
| class INT8DynamicLinear(torch.nn.Module): | |
| """ | |
| INT8 weight-only linear layer with per-channel scales. | |
| """ | |
| def __init__( | |
| self, | |
| weight: torch.Tensor, | |
| weight_scale: torch.Tensor, | |
| bias: torch.nn.Parameter, | |
| native_fp8_support: bool = False, # not used | |
| quant_type: str = "int8", | |
| ): | |
| super().__init__() | |
| self.weight = torch.nn.Parameter(weight, requires_grad=False) | |
| self.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False) | |
| self.bias = bias | |
| self.native_fp8_support = native_fp8_support # not used | |
| self.quant_type = quant_type | |
| def _is_backend_available(backend: str) -> bool: | |
| if backend == "torchao": | |
| return torchao_int8_quant is not None and torchao_int8_gemm is not None | |
| if backend == "vllm": | |
| return vllm_ops is not None and hasattr(torch.ops, "_C") | |
| if backend == "triton": | |
| return ( | |
| int8_quantize_triton is not None | |
| and int8_gemm_triton is not None | |
| and int8_gemm_bias_triton is not None | |
| ) | |
| if backend == "sgl": | |
| has_act = ( | |
| sglang_int8_act_quant is not None | |
| or vllm_ops is not None | |
| or torchao_int8_quant is not None | |
| or int8_quantize_triton is not None | |
| ) | |
| return sgl_kernel is not None and has_act | |
| if backend == "q8f": | |
| has_act = ( | |
| vllm_ops is not None | |
| or torchao_int8_quant is not None | |
| or int8_quantize_triton is not None | |
| ) | |
| return q8_linear is not None and has_act | |
| return False | |
| def _resolve_int8_backend(self) -> str: | |
| explicit_backend = { | |
| "int8-torchao": "torchao", | |
| "int8-vllm": "vllm", | |
| "int8-triton": "triton", | |
| "int8-sgl": "sgl", | |
| "int8-q8f": "q8f", | |
| } | |
| if self.quant_type in explicit_backend: | |
| backend = explicit_backend[self.quant_type] | |
| if not self._is_backend_available(backend): | |
| raise ImportError( | |
| f"quant_type='{self.quant_type}' requires '{backend}' backend dependencies" | |
| ) | |
| return backend | |
| # quant_type='int8' uses auto priority: sgl > vllm > torchao > triton | |
| for backend in ("sgl", "vllm", "torchao", "triton"): | |
| if self._is_backend_available(backend): | |
| return backend | |
| raise ImportError( | |
| "quant_type='int8' requires one of backends [sgl, vllm, torchao, triton], but none is available" | |
| ) | |
| def _act_quant_int8_torchao(self, x_2d: torch.Tensor): | |
| input_tensor_quant, input_tensor_scale = torchao_int8_quant(x_2d) | |
| return input_tensor_quant, input_tensor_scale.float() | |
| def _act_quant_int8_vllm(self, x_2d: torch.Tensor): | |
| input_tensor_quant, input_tensor_scale, _ = vllm_ops.scaled_int8_quant( | |
| x_2d, scale=None, azp=None, symmetric=True | |
| ) | |
| return input_tensor_quant, input_tensor_scale.float() | |
| def _act_quant_int8_triton(self, x_2d: torch.Tensor): | |
| input_tensor_quant, input_tensor_scale = int8_quantize_triton(x_2d) | |
| return input_tensor_quant, input_tensor_scale.float() | |
| def _act_quant_int8_sgl(self, x_2d: torch.Tensor): | |
| if sglang_int8_act_quant is not None: | |
| input_tensor_quant, input_tensor_scale = sglang_int8_act_quant(x_2d) | |
| return input_tensor_quant, input_tensor_scale.float() | |
| if vllm_ops is not None: | |
| return self._act_quant_int8_vllm(x_2d) | |
| if torchao_int8_quant is not None: | |
| return self._act_quant_int8_torchao(x_2d) | |
| if int8_quantize_triton is not None: | |
| return self._act_quant_int8_triton(x_2d) | |
| raise ImportError("int8-sgl activation quantization requires sglang/vllm/torchao/triton") | |
| def _act_quant_by_backend(self, x_2d: torch.Tensor, backend: str): | |
| if backend == "torchao": | |
| return self._act_quant_int8_torchao(x_2d) | |
| if backend == "vllm": | |
| return self._act_quant_int8_vllm(x_2d) | |
| if backend == "triton": | |
| return self._act_quant_int8_triton(x_2d) | |
| if backend == "sgl": | |
| return self._act_quant_int8_sgl(x_2d) | |
| if backend == "q8f": | |
| if vllm_ops is not None: | |
| return self._act_quant_int8_vllm(x_2d) | |
| if torchao_int8_quant is not None: | |
| return self._act_quant_int8_torchao(x_2d) | |
| return self._act_quant_int8_triton(x_2d) | |
| raise ValueError(f"Unsupported int8 backend: {backend}") | |
| def _gemm_int8_torchao(self, qinput, x_scale, out_dtype): | |
| output = torchao_int8_gemm( | |
| qinput, | |
| x_scale, | |
| self.weight.t(), | |
| self.weight_scale.t().float(), | |
| output_dtype=out_dtype, | |
| ) | |
| if self.bias is not None: | |
| output.add_(self.bias.to(output.dtype)) | |
| return output | |
| def _gemm_int8_vllm(self, qinput, x_scale, out_dtype): | |
| shape = (qinput.shape[0], self.weight.shape[0]) | |
| output = torch.empty(shape, dtype=out_dtype, device=qinput.device, requires_grad=False) | |
| torch.ops._C.cutlass_scaled_mm( | |
| output, | |
| qinput, | |
| self.weight.t(), | |
| x_scale, | |
| self.weight_scale.t(), | |
| self.bias, | |
| ) | |
| return output | |
| def _gemm_int8_triton(self, qinput, x_scale, out_dtype): | |
| if self.bias is not None: | |
| return int8_gemm_bias_triton( | |
| qinput, | |
| self.weight, | |
| self.bias, | |
| x_scale, | |
| self.weight_scale, | |
| output_dtype=out_dtype, | |
| ) | |
| return int8_gemm_triton( | |
| qinput, | |
| self.weight, | |
| x_scale, | |
| self.weight_scale, | |
| output_dtype=out_dtype, | |
| ) | |
| def _gemm_int8_sgl(self, qinput, x_scale, out_dtype): | |
| return sgl_kernel.int8_scaled_mm( | |
| qinput, | |
| self.weight.t(), | |
| x_scale, | |
| self.weight_scale.t(), | |
| out_dtype, | |
| self.bias, | |
| ) | |
| def _gemm_int8_q8f(self, qinput, x_scale, out_dtype): | |
| bias_fp32 = self.bias.float() if self.bias is not None else None | |
| return q8_linear( | |
| qinput, | |
| self.weight, | |
| bias_fp32, | |
| x_scale.float(), | |
| self.weight_scale, | |
| fuse_gelu=False, | |
| out_dtype=out_dtype, | |
| ) | |
| def _gemm_by_backend(self, qinput, x_scale, out_dtype, backend: str): | |
| if backend == "torchao": | |
| return self._gemm_int8_torchao(qinput, x_scale, out_dtype) | |
| if backend == "vllm": | |
| return self._gemm_int8_vllm(qinput, x_scale, out_dtype) | |
| if backend == "triton": | |
| return self._gemm_int8_triton(qinput, x_scale, out_dtype) | |
| if backend == "sgl": | |
| return self._gemm_int8_sgl(qinput, x_scale, out_dtype) | |
| if backend == "q8f": | |
| return self._gemm_int8_q8f(qinput, x_scale, out_dtype) | |
| raise ValueError(f"Unsupported int8 backend: {backend}") | |
| def forward(self, x): | |
| ori_dtype = x.dtype | |
| assert ori_dtype in [ | |
| torch.float32, | |
| torch.bfloat16, | |
| torch.float16, | |
| ], "x.dtype must be float32, bfloat16, or float16" | |
| if ori_dtype == torch.float32: | |
| x = x.to(torch.bfloat16) | |
| need_reshape = x.dim() == 3 | |
| if need_reshape: | |
| origin_shape = x.shape | |
| x_2d = x.view(-1, x.shape[-1]) | |
| else: | |
| origin_shape = None | |
| x_2d = x | |
| backend = self._resolve_int8_backend() | |
| qinput, x_scale = self._act_quant_by_backend(x_2d, backend) | |
| output = self._gemm_by_backend(qinput, x_scale, x.dtype, backend) | |
| if need_reshape and output.dim() == 2: | |
| output = output.view(origin_shape[0], origin_shape[1], -1) | |
| return output.to(ori_dtype) | |
| class FP4DynamicLinear(torch.nn.Module): | |
| def __init__( | |
| self, | |
| weight: torch.Tensor, | |
| weight_scale: torch.Tensor, | |
| bias: torch.nn.Parameter, | |
| weight_global_scale: torch.Tensor = None, | |
| native_fp8_support: bool = False, | |
| quant_type: str = "nvfp4", | |
| block_size: int = 16, | |
| ): | |
| super().__init__() | |
| self.weight = torch.nn.Parameter(weight, requires_grad=False) | |
| self.weight_scale = torch.nn.Parameter(weight_scale, requires_grad=False) | |
| self.bias = bias | |
| self.native_fp8_support = native_fp8_support | |
| self.quant_type = quant_type | |
| self.block_size = block_size | |
| self.profile_enabled = os.environ.get("ANGELSLIM_NVFP4_PROFILE", "0") == "1" | |
| if weight_global_scale is None: | |
| weight_global_scale = torch.tensor(1.0, dtype=torch.float32, device=weight.device) | |
| self.weight_global_scale = torch.nn.Parameter( | |
| weight_global_scale.to(dtype=torch.float32), requires_grad=False | |
| ) | |
| self.calibrate_x_absmax() | |
| def calibrate_x_absmax(self): | |
| if self.quant_type in ("mxfp4", "mxfp6", "mxfp8"): | |
| self.x_absmax = torch.tensor(1.0, dtype=torch.float32, device=self.weight.device) | |
| self.input_global_scale = torch.tensor( | |
| 1.0, dtype=torch.float32, device=self.weight.device | |
| ) | |
| else: | |
| self.x_absmax = torch.tensor(5.0, dtype=torch.float32, device=self.weight.device) | |
| self.input_global_scale = (2688.0 / self.x_absmax).to(torch.float32) | |
| self.alpha = 1.0 / (self.input_global_scale * self.weight_global_scale) | |
| def forward(self, x): | |
| ori_dtype = x.dtype | |
| assert ori_dtype in [ | |
| torch.float32, | |
| torch.bfloat16, | |
| torch.float16, | |
| ], "x.dtype must be float32, bfloat16, or float16" | |
| if ori_dtype == torch.float32: | |
| x = x.to(torch.bfloat16) | |
| need_reshape = x.dim() == 3 | |
| if need_reshape: | |
| origin_shape = x.shape | |
| x_2d = x.view(-1, x.shape[-1]) | |
| else: | |
| x_2d = x | |
| if self.profile_enabled and x_2d.is_cuda: | |
| torch.cuda.synchronize(x_2d.device) | |
| t0 = time.perf_counter() | |
| if self.quant_type == "nvfp4": | |
| qinput, x_scale, _ = nvfp4_per_tensor_quant(x_2d, self.input_global_scale) | |
| output = cutlass_scaled_nvfp4_mm( | |
| qinput, | |
| self.weight, | |
| x_scale, | |
| self.weight_scale, | |
| self.alpha, | |
| bias=self.bias, | |
| ) | |
| elif self.quant_type == "mxfp4": | |
| qinput, x_scale, _ = mxfp4_per_tensor_quant(x_2d) | |
| output = cutlass_scaled_mxfp4_mm( | |
| qinput, | |
| self.weight, | |
| x_scale, | |
| self.weight_scale, | |
| self.alpha, | |
| bias=self.bias, | |
| ) | |
| elif self.quant_type == "mxfp8": | |
| qinput, x_scale, _ = mxfp8_per_tensor_quant(x_2d) | |
| output = cutlass_scaled_mxfp8_mm( | |
| qinput, | |
| self.weight, | |
| x_scale, | |
| self.weight_scale, | |
| self.alpha, | |
| bias=self.bias, | |
| ) | |
| elif self.quant_type == "mxfp6": | |
| qinput, x_scale, _ = mxfp8_per_tensor_quant(x_2d) | |
| output = cutlass_scaled_mxfp6_mxfp8_mm( | |
| qinput, | |
| self.weight, | |
| x_scale, | |
| self.weight_scale, | |
| self.alpha, | |
| bias=self.bias, | |
| ) | |
| else: | |
| raise ValueError(f"Invalid quant_type for FP4DynamicLinear: {self.quant_type}") | |
| if self.profile_enabled and x_2d.is_cuda: | |
| torch.cuda.synchronize(x_2d.device) | |
| t1 = time.perf_counter() | |
| if self.profile_enabled and x_2d.is_cuda: | |
| torch.cuda.synchronize(x_2d.device) | |
| t2 = time.perf_counter() | |
| if self.profile_enabled: | |
| print( | |
| f"[NVFP4Linear] quant_ms={(t1 - t0) * 1000:.3f}, " | |
| f"gemm_ms={(t2 - t1) * 1000:.3f}, " | |
| f"shape=({x_2d.shape[0]}, {x_2d.shape[1]})" | |
| ) | |
| if need_reshape: | |
| output = output.view(origin_shape[0], origin_shape[1], -1) | |
| return output.to(ori_dtype) | |