Files
Model-Optimizer/modelopt/torch/quantization/tensor_quant.py
T

618 lines
19 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.
"""Basic tensor quantization functions."""
import warnings
import torch
from torch.autograd import Function
from torch.onnx import symbolic_helper
import modelopt.torch.quantization.triton as triton_kernel
from .config import QuantizerAttributeConfig
from .extensions import get_cuda_ext, get_cuda_ext_fp8, get_cuda_ext_mx
mx_format_map = {
(4, 3): "E4M3",
(5, 2): "E5M2",
(3, 2): "E3M2",
(2, 3): "E2M3",
8: "INT8",
(8, 0): "E8M0",
(2, 1): "E2M1",
(1, 2): "E1M2",
(0, 3): "E0M3",
(3, 0): "E3M0",
}
DISABLE_TRITON_KERNEL = False
def _fp8_eager(x, amax=None):
dtype = x.dtype
if amax is not None:
scale = 448.0 / (amax.to(torch.float32))
scale_inv = 1 / scale
x = x.to(torch.float32) * scale
x = x.to(torch.float8_e4m3fn)
if amax is not None:
x = x.to(torch.float32) * scale_inv
return x.to(dtype)
def fp8_eager(x, amax):
"""Eager mode implementation of FP8 quantization."""
return _fp8_eager(x, amax)
def scaled_e4m3_impl(
inputs: torch.Tensor,
amax: torch.Tensor | None = None,
) -> torch.Tensor:
"""Implementation of fake quantizing input to FP8.
Args:
inputs: Torch tensor.
amax: Absolute max range of the input tensor.
Returns:
Input tensors faked quantized to FP8.
"""
if (not inputs.is_cuda) or amax is None or amax.squeeze().ndim > 1:
return fp8_eager(inputs, amax)
cuda_ext_fp8 = get_cuda_ext_fp8(raise_if_failed=False)
if cuda_ext_fp8 is None:
return fp8_eager(inputs, amax)
with torch.cuda.device(
None if inputs.device.index == torch.cuda.current_device() else inputs.device.index
):
if amax.numel() == 1:
outputs = cuda_ext_fp8.fake_e4m3fy(inputs, amax)
elif amax.squeeze().ndim == 1:
axis = amax.shape.index(amax.numel())
outputs = cuda_ext_fp8.fake_e4m3fy_with_axis(inputs, amax.squeeze(), axis)
return outputs
def fake_quant_impl(
inputs: torch.Tensor,
amax: torch.Tensor,
num_bits=8,
unsigned=False,
narrow_range=True,
):
"""Implementation of fake quantizing input according to number of bits."""
cuda_ext = get_cuda_ext()
with torch.cuda.device(
None if inputs.device.index == torch.cuda.current_device() else inputs.device.index
):
if amax.numel() == 1:
outputs = cuda_ext.fake_tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
else:
axis = amax.shape.index(amax.numel())
outputs = cuda_ext.fake_tensor_quant_with_axis(
inputs, amax.squeeze(), axis, num_bits, unsigned, narrow_range
)
return outputs
def _quantize_impl(
inputs: torch.Tensor,
amax: torch.Tensor,
num_bits: int = 8,
exponent_bits: int = 0,
unsigned: bool = False,
narrow_range: bool = True,
):
if num_bits == 8 and exponent_bits == 4:
return scaled_e4m3_impl(inputs=inputs, amax=amax)
elif isinstance(num_bits, int):
return fake_quant_impl(
inputs=inputs,
amax=amax,
num_bits=num_bits,
unsigned=unsigned,
narrow_range=narrow_range,
)
else:
raise ValueError(
f"Invalid combination of (num_bits, exponent_bits): ({num_bits}, {exponent_bits})."
)
def _quantize_impl_abstract(
input: torch.Tensor,
amax: torch.Tensor,
num_bits: int = 8,
exponent_bits: int = 0,
unsigned: bool = False,
narrow_range: bool = True,
) -> torch.Tensor:
"""Register an abstract implementation for quantizing tensor.
This abstract function returns an empty tensor with the same shape and dtype.
"""
output = torch.empty_like(input)
return output
# Argument types: Tensor, int, NoneType, int, int, int, int,
def _dynamic_block_quantize_impl(
inputs: torch.Tensor,
block_size: int,
amax: torch.Tensor,
num_bits: int,
exponent_bits: int,
scale_num_bits: int,
scale_exponent_bits: int,
):
scale_bits = (scale_exponent_bits, scale_num_bits - scale_exponent_bits - 1)
if exponent_bits != 0:
num_bits = (exponent_bits, num_bits - exponent_bits - 1)
if num_bits in mx_format_map:
assert scale_bits in mx_format_map, f"Scale bits should be in {mx_format_map.keys()}"
if scale_bits != (8, 0):
assert amax.is_cuda, "amax must be a CUDA tensor for dynamic block quantization."
if amax.numel() != 1:
amax = amax.amax()
with torch.cuda.device(
None if inputs.device.index == torch.cuda.current_device() else inputs.device.index
):
if (
num_bits == (2, 1) # type: ignore[comparison-overlap]
and scale_bits == (4, 3)
and triton_kernel.IS_AVAILABLE
and not DISABLE_TRITON_KERNEL
and amax is not None
):
return triton_kernel.fp4_fake_quant_block(inputs, amax)
cuda_ext_mx = get_cuda_ext_mx(raise_if_failed=True)
return cuda_ext_mx.fused_amax_convert(
inputs,
block_size,
getattr(cuda_ext_mx.Types, mx_format_map[num_bits]),
getattr(cuda_ext_mx.Types, mx_format_map[scale_bits]),
amax,
)
else:
raise NotImplementedError(
f"Unsupported num_bits: {num_bits}, scale_bits: {scale_bits} for dynamic block quantization."
)
def _dynamic_block_quantize_impl_abstract(
inputs: torch.Tensor,
block_size: int,
amax: torch.Tensor,
num_bits: int,
exponent_bits: int,
scale_num_bits: int,
scale_exponent_bits: int,
):
"""Register an abstract implementation for dynamic block quantization.
This abstract function returns an empty tensor with the same shape and dtype.
"""
output = torch.empty_like(inputs)
return output
quantize_op = _quantize_impl
dynamic_block_quantize_op = _dynamic_block_quantize_impl
# Define custom operators via torch.library if supported:
# 1. quantize_op: Applies static quantization to the input tensor using a specified amax.
# 2. dynamic_block_quantize_op: Performs blockwise double quantization with dynamically
# determined scales, governed by the given quantization format (scale_num_bits, scale_exponent_bits).
try:
torch.library.define(
"tensorrt::quantize_op",
"(Tensor input, Tensor amax, int num_bits, int exponent_bits, "
"bool unsigned, bool narrow_range) -> Tensor",
)
torch.library.define(
"tensorrt::dynamic_block_quantize_op",
"(Tensor input, int block_size, Tensor amax, int num_bits, int exponent_bits, "
"int scale_num_bits, int scale_exponent_bits) -> Tensor",
)
torch.library.define(
"tensorrt::dynamic_block_quantize_op.overload",
"(Tensor input, int block_size, None amax, int num_bits, int exponent_bits, "
"int scale_num_bits, int scale_exponent_bits) -> Tensor",
)
# Implement the None amax case
def _dynamic_block_quantize_impl_none_amax(
inputs: torch.Tensor,
block_size: int,
amax: None,
num_bits: int,
exponent_bits: int,
scale_num_bits: int,
scale_exponent_bits: int,
):
return torch.empty_like(inputs)
# Register the implementation for both CPU and CUDA
torch.library.impl("tensorrt::quantize_op", ["cpu", "cuda"])(_quantize_impl)
torch.library.impl("tensorrt::dynamic_block_quantize_op", ["cpu", "cuda"])(
_dynamic_block_quantize_impl
)
torch.library.impl("tensorrt::dynamic_block_quantize_op.overload", ["cpu", "cuda"])(
_dynamic_block_quantize_impl_none_amax
)
# Register the fake implementation
torch.library.register_fake("tensorrt::quantize_op")(_quantize_impl_abstract)
torch.library.register_fake("tensorrt::dynamic_block_quantize_op")(
_dynamic_block_quantize_impl_abstract
)
torch.library.register_fake("tensorrt::dynamic_block_quantize_op.overload")(
_dynamic_block_quantize_impl_abstract
)
quantize_op = torch.ops.tensorrt.quantize_op
dynamic_block_quantize_op = torch.ops.tensorrt.dynamic_block_quantize_op
except (AttributeError, RuntimeError) as e:
# torch.library is an experimental feature, the function signatures may change overtime.
warnings.warn(
"Unable to register operators with torch.library. Exporting quantized models with"
f" torch.export will not be supported.\n{e}"
)
# Predefined descriptors
QUANT_DESC_8BIT_PER_TENSOR = QuantizerAttributeConfig(num_bits=8)
QUANT_DESC_UNSIGNED_8BIT_PER_TENSOR = QuantizerAttributeConfig(num_bits=8, unsigned=True)
QUANT_DESC_8BIT_CONV1D_WEIGHT_PER_CHANNEL = QuantizerAttributeConfig(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONV2D_WEIGHT_PER_CHANNEL = QuantizerAttributeConfig(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONV3D_WEIGHT_PER_CHANNEL = QuantizerAttributeConfig(num_bits=8, axis=(0))
QUANT_DESC_8BIT_LINEAR_WEIGHT_PER_ROW = QuantizerAttributeConfig(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONVTRANSPOSE1D_WEIGHT_PER_CHANNEL = QuantizerAttributeConfig(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONVTRANSPOSE2D_WEIGHT_PER_CHANNEL = QuantizerAttributeConfig(num_bits=8, axis=(0))
QUANT_DESC_8BIT_CONVTRANSPOSE3D_WEIGHT_PER_CHANNEL = QuantizerAttributeConfig(num_bits=8, axis=(0))
@torch.jit.script
def _fake_tensor_quant_backward(inputs, amax: torch.Tensor | None, grad_outputs):
# Skip clip for MX formats
if amax is None:
return grad_outputs
zero = grad_outputs.new_zeros(1)
grad_inputs = torch.where(inputs.abs() <= amax, grad_outputs, zero)
return grad_inputs
def _fake_quant_backward_function(ctx, grad_outputs, num_args=1):
saved_tensors = ctx.saved_tensors
if len(saved_tensors) == 0:
return (grad_outputs,) + (None,) * (num_args - 1)
inputs, amax = saved_tensors
return (_fake_tensor_quant_backward(inputs, amax, grad_outputs),) + (None,) * (num_args - 1)
def _save_for_backward_if_needed(ctx, pass_through_bwd, inputs, amax):
if not pass_through_bwd and amax is not None:
amax = (
amax
if isinstance(amax, torch.Tensor)
else torch.tensor(amax, device=inputs.device, dtype=inputs.dtype)
)
ctx.save_for_backward(inputs, amax)
class FakeTensorQuantFunction(Function):
"""Fake version of TensorQuantFunction use CUDA extension."""
@staticmethod
@symbolic_helper.parse_args("v", "t", "t", "i", "b", "b", "s", "b", "i", "i")
def symbolic(
g,
inputs,
amax,
bias=None,
num_bits=8,
unsigned=False,
narrow_range=True,
trt_high_precision_dtype=None,
pass_through_bwd=False,
block_size=None,
axis=None,
):
"""ONNX symbolic function."""
from .export_onnx import export_int4, export_int8
if num_bits == 4:
return export_int4(
g, inputs, amax, num_bits, trt_high_precision_dtype, block_size, axis
)
return export_int8(
g, inputs, amax, num_bits, unsigned, narrow_range, trt_high_precision_dtype
)
@staticmethod
def forward(
ctx,
inputs,
amax,
bias=None,
num_bits=8,
unsigned=False,
narrow_range=True,
trt_high_precision_dtype=None,
pass_through_bwd=False,
block_size=None,
axis=None,
):
"""Forward method."""
if bias is not None:
inputs = inputs - bias
_save_for_backward_if_needed(ctx, pass_through_bwd, inputs, amax)
def legacy_quant_func():
# The LegacyFakeTensorQuantFunction support cpu and amax with any shape that can be broadcasted to inputs.
outputs = _tensor_quant(inputs, amax, num_bits, unsigned, narrow_range)
return outputs
if not inputs.is_cuda:
outputs = legacy_quant_func()
else:
try:
outputs = quantize_op(
inputs,
amax,
num_bits=num_bits,
exponent_bits=0,
unsigned=unsigned,
narrow_range=narrow_range,
)
except (AttributeError, ValueError):
# AttributeError: cuda_ext is not imported, possibly due to CPU only installation
# ValueError: cuda_ext is installed, but trying to perform multidimensional quantization (amax dim > 1)
outputs = legacy_quant_func()
if bias is not None:
outputs = outputs + bias
return outputs
@staticmethod
def backward(ctx, grad_outputs):
"""Implements straight through estimation with clipping."""
return _fake_quant_backward_function(ctx, grad_outputs, num_args=10)
class ScaledE4M3Function(Function):
"""E4M3fy input with scale."""
@staticmethod
@symbolic_helper.parse_args("v", "t", "t", "i", "i", "s", "b")
def symbolic(
g,
inputs,
amax=None,
bias=None,
E=4, # noqa: N803
M=3, # noqa: N803
trt_high_precision_dtype=None,
pass_through_bwd=False,
):
"""ONNX symbolic function."""
from .export_onnx import export_fp8
return export_fp8(g, inputs, amax, trt_high_precision_dtype)
@staticmethod
# Default values could cause errors from TorchDynamo during torch.export
def forward(
ctx,
inputs,
amax,
bias,
E, # noqa: N803
M, # noqa: N803
trt_high_precision_dtype=None,
pass_through_bwd=False,
):
"""Forward method."""
if E != 4 or M != 3:
raise NotImplementedError("Only support E=4 & M=3 for now.")
if bias is not None:
inputs = inputs - bias
_save_for_backward_if_needed(ctx, pass_through_bwd, inputs, amax)
outputs = quantize_op(
inputs,
amax,
num_bits=8,
exponent_bits=4,
unsigned=False,
narrow_range=False,
)
if bias is not None:
outputs = outputs + bias
return outputs
@staticmethod
def backward(ctx, grad_outputs):
"""Implements straight through estimation with clipping."""
return _fake_quant_backward_function(ctx, grad_outputs, num_args=7)
def _dynamic_block_quantize_forward(
ctx,
inputs,
block_size,
amax,
num_bits,
scale_bits,
trt_high_precision_dtype=None,
onnx_quantizer_type="dynamic",
pass_through_bwd=True,
):
"""Forward method."""
if isinstance(num_bits, int):
# special case for INT dynamic block quantization, e.g. MXINT8
exponent_bits = 0
else:
assert isinstance(num_bits, tuple) and len(num_bits) == 2
exponent_bits = num_bits[0]
num_bits = num_bits[0] + num_bits[1] + 1
assert isinstance(scale_bits, tuple) and len(scale_bits) == 2
scale_exponent_bits = scale_bits[0]
scale_num_bits = scale_bits[0] + scale_bits[1] + 1
outputs = dynamic_block_quantize_op(
inputs,
block_size,
amax,
num_bits,
exponent_bits,
scale_num_bits,
scale_exponent_bits,
)
return outputs
class DynamicBlockQuantizationFunction(Function):
"""Dynamic block quantization functional."""
@staticmethod
@symbolic_helper.parse_args("v", "i", "v", "t", "is", "is", "s", "s", "b")
def symbolic(
g,
inputs,
block_size,
amax,
bias,
num_bits,
scale_bits,
trt_high_precision_dtype=None,
onnx_quantizer_type="dynamic",
pass_through_bwd=True,
):
"""ONNX symbolic function."""
from .export_onnx import export_fp4, export_mxfp8
if num_bits == (2, 1) and scale_bits == (4, 3):
return export_fp4(
g,
inputs,
block_size,
amax,
num_bits,
trt_high_precision_dtype,
onnx_quantizer_type,
)
if num_bits == (4, 3) and scale_bits == (8, 0):
return export_mxfp8(
g,
inputs,
onnx_quantizer_type,
block_size,
)
raise NotImplementedError(
f"Unsupported num_bits: {num_bits} and scale_bits: {scale_bits} for ONNX export."
)
@staticmethod
def forward(
ctx,
inputs,
block_size,
amax,
bias,
num_bits,
scale_bits,
trt_high_precision_dtype=None,
onnx_quantizer_type="dynamic",
pass_through_bwd=True,
):
"""Forward method."""
_save_for_backward_if_needed(ctx, pass_through_bwd, inputs, amax)
return _dynamic_block_quantize_forward(
ctx,
inputs,
block_size,
amax,
num_bits,
scale_bits,
trt_high_precision_dtype,
onnx_quantizer_type,
pass_through_bwd,
)
@staticmethod
def backward(ctx, grad_outputs):
"""Implements straight through estimation with clipping."""
return _fake_quant_backward_function(ctx, grad_outputs, num_args=9)
def _tensor_quant(inputs, amax, num_bits=8, unsigned=False, narrow_range=True):
"""Shared function body between TensorQuantFunction and FakeTensorQuantFunction."""
# Fine scale, per channel scale will be handled by broadcasting, which could be tricky. Pop a warning.
if unsigned and inputs.min() < 0.0:
raise TypeError("Negative values encountered in unsigned quantization.")
# Computation can be done in FP32 to prevent potential over flow.
input_dtype = inputs.dtype
inputs = inputs.float()
amax = amax.float()
min_amax = amax.min()
if min_amax < 0:
raise ValueError("Negative values in amax")
max_bound = torch.tensor((2.0 ** (num_bits - 1 + int(unsigned))) - 1.0, device=amax.device)
if unsigned:
min_bound = 0
elif narrow_range:
min_bound = -max_bound
else:
min_bound = -max_bound - 1
scale = max_bound / amax
epsilon = 1.0 / (1 << 24)
if min_amax <= epsilon: # Treat amax smaller than minimum representable of fp16 0
zero_amax_mask = amax <= epsilon
scale[zero_amax_mask] = 0 # Value quantized with amax=0 should all be 0
outputs = torch.clamp((inputs * scale).round_(), min_bound, max_bound)
if min_amax <= epsilon:
scale[zero_amax_mask] = (
1.0 # Return 1 makes more sense for values quantized to 0 with amax=0
)
outputs = outputs / scale
outputs = outputs.to(input_dtype)
return outputs
fake_tensor_quant = FakeTensorQuantFunction.apply
scaled_e4m3 = ScaledE4M3Function.apply
dynamic_block_quant = DynamicBlockQuantizationFunction.apply