mirror of
https://github.com/NVIDIA/Model-Optimizer.git
synced 2026-11-12 02:06:41 +08:00
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
296 lines
11 KiB
Python
296 lines
11 KiB
Python
# SPDX-FileCopyrightText: Copyright (c) 2024 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
|
|
# SPDX-License-Identifier: Apache-2.0
|
|
#
|
|
# 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.
|
|
|
|
"""Implements NVFP4 quantization for efficient tensor storage and computation."""
|
|
|
|
import torch
|
|
|
|
from ..backends.utils import fp4_compatible
|
|
from ..qtensor.base_qtensor import BaseQuantizedTensor
|
|
from ..utils import reduce_amax, reduce_block_amax, reduce_block_padding
|
|
|
|
# Define conversion tables
|
|
e2m1_bounds = torch.tensor([0.25, 0.75, 1.25, 1.75, 2.5, 3.5, 5])
|
|
e2m1_values = torch.tensor([0, 0.5, 1, 1.5, 2, 3, 4, 6, 0, -0.5, -1, -1.5, -2, -3, -4, -6])
|
|
|
|
__all__ = ["NVFP4QTensor"]
|
|
|
|
|
|
class NVFP4QTensor(BaseQuantizedTensor):
|
|
"""Implements the INT4 quantization on tensors for more efficient storage or computation.
|
|
|
|
Attributes:
|
|
quantized_data (torch.Tensor): The quantized data stored as a packed uint8 tensor.
|
|
"""
|
|
|
|
e2m1_values_on_device = {}
|
|
e2m1_bounds_on_device = {}
|
|
|
|
@classmethod
|
|
def get_e2m1_values(cls, device):
|
|
"""Returns the e2m1 values on the device."""
|
|
if device not in cls.e2m1_values_on_device:
|
|
cls.e2m1_values_on_device[device] = e2m1_values.to(device)
|
|
return cls.e2m1_values_on_device[device]
|
|
|
|
@classmethod
|
|
def get_e2m1_bounds(cls, device):
|
|
"""Returns the e2m1 values on the device."""
|
|
if device not in cls.e2m1_bounds_on_device:
|
|
cls.e2m1_bounds_on_device[device] = e2m1_bounds.to(device)
|
|
return cls.e2m1_bounds_on_device[device]
|
|
|
|
@classmethod
|
|
def get_weights_scaling_factor_2_from_quantizer(cls, weight_quantizer):
|
|
"""Returns per tensor weight scaling factor from the weight_quantizer amax."""
|
|
# Assert that weight_quantizer has attribute amax
|
|
assert hasattr(weight_quantizer, "_amax"), "Weight quantizer does not have attribute amax"
|
|
return weight_quantizer._amax.float() / (6.0 * 448.0)
|
|
|
|
@classmethod
|
|
def get_weights_scaling_factor(
|
|
cls,
|
|
input: torch.Tensor,
|
|
block_size: int,
|
|
weights_scaling_factor_2: torch.Tensor | None = None,
|
|
keep_high_precision: bool = False,
|
|
):
|
|
"""Returns quantized per block weight scaling factor."""
|
|
if weights_scaling_factor_2 is None:
|
|
weights_scaling_factor_2 = cls.get_weights_scaling_factor_2(input)
|
|
|
|
# Get per_block amax
|
|
assert block_size != 0, "Block size is zero. Cannot return per_block amax for given input."
|
|
|
|
assert input.shape[-1] % block_size == 0, (
|
|
"Weight shape is not divisible for block size for block quantization."
|
|
)
|
|
|
|
# Get per block amax
|
|
per_block_amax = reduce_block_amax(input, block_sizes={-1: block_size}).float()
|
|
# Get per-block-scale
|
|
per_block_scale = per_block_amax / (6.0 * weights_scaling_factor_2)
|
|
# Set all zero values in scale to 1.0
|
|
per_block_scale[per_block_scale == 0] = 1.0
|
|
# Convert to torch.float8_e4m3fn
|
|
if not keep_high_precision:
|
|
per_block_scale = per_block_scale.to(torch.float8_e4m3fn)
|
|
return per_block_scale, weights_scaling_factor_2
|
|
|
|
@classmethod
|
|
def get_weights_scaling_factor_2(cls, input: torch.Tensor):
|
|
"""Returns per tensor weight scaling factor."""
|
|
return reduce_amax(input).float() / (6.0 * 448.0)
|
|
|
|
@classmethod
|
|
def get_activation_scaling_factor(cls, quantizer):
|
|
"""Returns the activation scaling factor for export."""
|
|
# TODO: Update to use module and not quantizer
|
|
if not quantizer.is_enabled:
|
|
return None
|
|
|
|
amax = quantizer.export_amax()
|
|
|
|
if amax is None:
|
|
return None
|
|
|
|
activation_scaling_factor = amax.float() / (quantizer.maxbound * 448.0)
|
|
|
|
assert torch.all(activation_scaling_factor > 0), (
|
|
f" activation scaling factor {activation_scaling_factor} not positive."
|
|
)
|
|
|
|
return activation_scaling_factor
|
|
|
|
@classmethod
|
|
def _cast_fp4(cls, weight: torch.Tensor):
|
|
"""Converts tensor to uint4."""
|
|
device = weight.device
|
|
|
|
# Extract sign and compute absolute values in one pass
|
|
sign_bit = (weight < 0).to(torch.uint8)
|
|
weight_abs = weight.abs_()
|
|
|
|
# Get bounds and compute ordinal values
|
|
e2m1_bounds = cls.get_e2m1_bounds(device)
|
|
ord = torch.searchsorted(e2m1_bounds, weight_abs, out_int32=True).to(torch.uint8)
|
|
|
|
# Efficiently check for rounding at odd-indexed bounds [0.75, 1.75, 2.5]
|
|
# Only need to check bounds at indices 1, 3, 5
|
|
odd_bounds = e2m1_bounds[[1, 3, 5]] # [0.75, 1.75, 2.5]
|
|
equals_odd_bounds = torch.any(weight_abs.unsqueeze(-1) == odd_bounds, dim=-1).to(
|
|
torch.uint8
|
|
)
|
|
|
|
# Combine sign, ordinal, and rounding adjustment
|
|
return (sign_bit << 3) + ord + equals_odd_bounds
|
|
|
|
@classmethod
|
|
def quantize(
|
|
cls,
|
|
input: torch.Tensor,
|
|
block_size: int,
|
|
weights_scaling_factor: torch.Tensor | None = None,
|
|
weights_scaling_factor_2: torch.Tensor | None = None,
|
|
keep_high_precision: bool = False,
|
|
try_tensorrt: bool = False,
|
|
):
|
|
"""Converting a tensor to a quantized format based on NVFP4 quantization.
|
|
|
|
Args:
|
|
input (torch.Tensor): The input tensor to be quantized.
|
|
block_size (int): The size of each block for quantization.
|
|
weights_scaling_factor (torch.Tensor): The scaling factor for the weights.
|
|
weights_scaling_factor_2 (torch.Tensor): The scaling factor for the weights.
|
|
keep_high_precision (bool): Whether to keep output scales at high precision.
|
|
|
|
Returns:
|
|
tuple: Contains quantized data, quantized per block scaling factor, and per tensor scaling factor.
|
|
"""
|
|
# Get original input shape
|
|
input_shape = input.shape
|
|
input_dtype = input.dtype
|
|
|
|
# pad the input if needed
|
|
input = reduce_block_padding(input, block_sizes={-1: block_size})
|
|
|
|
if weights_scaling_factor_2 is None:
|
|
weights_scaling_factor_2 = cls.get_weights_scaling_factor_2(input)
|
|
|
|
# try call trtllm fp4 quantization if possible
|
|
if (
|
|
fp4_compatible()
|
|
and weights_scaling_factor is None
|
|
and try_tensorrt
|
|
and block_size == 16
|
|
and input.is_cuda
|
|
and input.dtype in [torch.half, torch.bfloat16]
|
|
):
|
|
try:
|
|
import tensorrt_llm # noqa: F401
|
|
|
|
# Make sure this utils is available for dequantize
|
|
from tensorrt_llm._torch.auto_deploy.utils.quantization_utils import (
|
|
cutlass_fp4_scale_to_modelopt_fp4_scale, # noqa: F401
|
|
)
|
|
|
|
packed_weight, weights_scaling_factor = torch.ops.trtllm.fp4_quantize(
|
|
input, 1.0 / weights_scaling_factor_2, block_size, False
|
|
)
|
|
# weights_scaling_factor is ready for nvfp4_gemm to use;
|
|
# however, it is different from the non trtllm version, so when dequantize,
|
|
# it will be converted.
|
|
return (
|
|
cls(input_shape, input_dtype, packed_weight),
|
|
weights_scaling_factor,
|
|
weights_scaling_factor_2,
|
|
)
|
|
except ImportError:
|
|
pass
|
|
|
|
if weights_scaling_factor is None:
|
|
weights_scaling_factor, _ = cls.get_weights_scaling_factor(
|
|
input, block_size, weights_scaling_factor_2
|
|
)
|
|
|
|
# Reshape the weight and scale factors
|
|
original_shape = input.shape
|
|
input = input.view((*tuple(input.shape[:-1]), -1, block_size))
|
|
|
|
# Scale weights
|
|
scaled_weight = input / (
|
|
(weights_scaling_factor.to(torch.float32) * weights_scaling_factor_2).unsqueeze(-1)
|
|
)
|
|
|
|
# Reshape weights to original
|
|
scaled_weight = scaled_weight.view(original_shape)
|
|
|
|
if keep_high_precision:
|
|
return scaled_weight
|
|
# Cast weights to fp4
|
|
q_weight = cls._cast_fp4(scaled_weight)
|
|
# Pack weights
|
|
packed_weight = (q_weight[..., 1::2] << 4) | q_weight[..., 0::2]
|
|
return (
|
|
cls(input_shape, input_dtype, packed_weight),
|
|
weights_scaling_factor,
|
|
weights_scaling_factor_2,
|
|
)
|
|
|
|
def dequantize(self, dtype: torch.dtype = None, fast=False, **kwarg):
|
|
"""Dequantze NVFP4 packed tensor to a target dtype."""
|
|
if dtype is None:
|
|
dtype = self.metadata["dtype"]
|
|
|
|
def _unpack_tensor(input: torch.Tensor):
|
|
# Initialize storage for unpacked tensor
|
|
unpacked_shape = list(input.shape)
|
|
unpacked_shape[-1] = unpacked_shape[-1] * 2
|
|
unpacked = torch.empty(unpacked_shape, dtype=dtype, device=input.device)
|
|
|
|
unpacked[..., 1::2] = input >> 4
|
|
unpacked[..., 0::2] = input & 0x0F
|
|
|
|
unpacked = unpacked.reshape(-1)
|
|
unpacked = self.get_e2m1_values(input.device)[unpacked.long()]
|
|
|
|
return unpacked.reshape(unpacked_shape)
|
|
|
|
# Get scales from kwargs
|
|
if kwarg["scale"].dtype == torch.uint8 and kwarg["scale"].ndim == 1:
|
|
# If quantization is done by trtllm, convert cutlass fp4 scale to modelopt fp4 scale
|
|
try:
|
|
from tensorrt_llm._torch.auto_deploy.utils.quantization_utils import (
|
|
cutlass_fp4_scale_to_modelopt_fp4_scale,
|
|
)
|
|
|
|
kwarg["scale"] = cutlass_fp4_scale_to_modelopt_fp4_scale(
|
|
kwarg["scale"], self.metadata["shape"][-2:]
|
|
)
|
|
except ImportError as e:
|
|
raise ImportError(
|
|
"This tensor is quantized by trtllm, but tensorrt_llm cannot be imported."
|
|
) from e
|
|
|
|
if fast:
|
|
from ..triton.fp4_kernel import fp4_dequantize
|
|
|
|
return fp4_dequantize(
|
|
self._quantized_data,
|
|
kwarg["scale"],
|
|
kwarg["double_scale"],
|
|
block_size=kwarg["block_sizes"][-1],
|
|
dtype=dtype,
|
|
).reshape(self.metadata["shape"])
|
|
else:
|
|
q_per_block_scale = (
|
|
kwarg["scale"].to(torch.float32)
|
|
if kwarg["scale"].dtype == torch.float8_e4m3fn
|
|
else kwarg["scale"]
|
|
)
|
|
block_size = kwarg["block_sizes"][-1]
|
|
per_block_quant_scale = kwarg["double_scale"]
|
|
|
|
# Dequantize scales
|
|
per_block_scale = q_per_block_scale * per_block_quant_scale
|
|
|
|
# Unpack and unscale weights
|
|
deq_data = _unpack_tensor(self._quantized_data)
|
|
|
|
deq_data = deq_data.view(
|
|
(*tuple(deq_data.shape[:-1]), -1, block_size)
|
|
) * per_block_scale.unsqueeze(-1)
|
|
return deq_data.reshape(self.metadata["shape"]).to(dtype)
|