| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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, |
| ) |
|
|
|
|
| |
| 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" |
|
|
| @torch.compiler.disable(recursive=True) |
| 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) |
|
|
| |
| |
| 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, |
| 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 |
| self.quant_type = quant_type |
|
|
| @torch.compiler.disable(recursive=True) |
| 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) |
|
|
| |
| |
| output = fp8_weight_only_gemm( |
| A=x, |
| 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, |
| 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 |
| self.quant_type = quant_type |
|
|
| @staticmethod |
| 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 |
|
|
| |
| 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}") |
|
|
| @torch.compiler.disable(recursive=True) |
| 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) |
|
|
| @torch.compiler.disable(recursive=True) |
| 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) |
|
|