multimodalart's picture
multimodalart HF Staff
Initial ABot-World interactive rollout demo
28404e6 verified
Raw
History Blame Contribute Delete
17.4 kB
# 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]"
)