mirror of
https://github.com/NVIDIA/Model-Optimizer.git
synced 2026-11-03 08:14:29 +08:00
Signed-off-by: Keval Morabia <28916987+kevalmorabia97@users.noreply.github.com>
1117 lines
42 KiB
Python
1117 lines
42 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.
|
|
|
|
"""Utils for quantization including scaling factors adjustments."""
|
|
|
|
import logging
|
|
from collections.abc import Generator
|
|
from typing import Any
|
|
from warnings import warn
|
|
|
|
import torch
|
|
import torch.nn as nn
|
|
|
|
from modelopt import __version__
|
|
from modelopt.torch.quantization.model_calib import enable_stats_collection, finish_stats_collection
|
|
from modelopt.torch.quantization.nn.modules.quant_linear import RealQuantLinear
|
|
from modelopt.torch.quantization.qtensor import (
|
|
FP8QTensor,
|
|
MXFP4QTensor,
|
|
NVFP4QTensor,
|
|
QTensorWrapper,
|
|
)
|
|
from modelopt.torch.quantization.utils import is_quantized_linear
|
|
|
|
from ..quantization.nn import SequentialQuantizer, TensorQuantizer
|
|
from .model_config import (
|
|
KV_CACHE_FP8,
|
|
KV_CACHE_INT8,
|
|
KV_CACHE_NVFP4,
|
|
KV_CACHE_NVFP4_AFFINE,
|
|
QUANTIZATION_FP8,
|
|
QUANTIZATION_FP8_PB_REAL,
|
|
QUANTIZATION_FP8_PB_WO,
|
|
QUANTIZATION_FP8_PC_PT,
|
|
QUANTIZATION_INT4_AWQ,
|
|
QUANTIZATION_INT8_SQ,
|
|
QUANTIZATION_NONE,
|
|
QUANTIZATION_NVFP4,
|
|
QUANTIZATION_NVFP4_AWQ,
|
|
QUANTIZATION_W4A8_AWQ,
|
|
QUANTIZATION_W4A8_MXFP4_FP8,
|
|
)
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_scaling_factor_from_weight(weight, group_size) -> torch.tensor:
|
|
"""Calculate the weight scaling factor for a given group size."""
|
|
[n, k] = weight.shape
|
|
|
|
if group_size != 0:
|
|
# int4_awq
|
|
if k % group_size != 0:
|
|
raise NotImplementedError(
|
|
"Weight shape is not divisible for block size for block quantization."
|
|
)
|
|
weight = weight.reshape(n, k // group_size, group_size)
|
|
maxbound = 7.0
|
|
else:
|
|
# int8_sq
|
|
maxbound = 127.0
|
|
amax = weight.abs().max(dim=-1)[0].float()
|
|
|
|
weights_scaling_factor = amax / maxbound
|
|
|
|
# Let's filter the zeros in the scaling factor if the weights are zero
|
|
# to avoid the divided-by-zero error..
|
|
weights_scaling_factor[weights_scaling_factor == 0] = 1.0
|
|
|
|
return weights_scaling_factor
|
|
|
|
|
|
def resmooth_and_get_scale(
|
|
merged_weights: torch.Tensor,
|
|
pre_quant_scales: list[torch.Tensor],
|
|
ranks: int,
|
|
group_size: int,
|
|
new_pre_quant_scale: torch.Tensor | None = None,
|
|
quantization: str | None = QUANTIZATION_NONE,
|
|
):
|
|
"""Resmooths weights from a single or multiple ranks and get scaling factors and amax.
|
|
|
|
Args:
|
|
merged_weights: Merged weights from ranks.
|
|
pre_quant_scales: List of pre-quantization scales for each rank.
|
|
ranks: Number of ranks.
|
|
group_size: Group size of the quantization block.
|
|
new_pre_quant_scale (optional): If not provided, weights will be resmoothed using
|
|
the average of pre_quant_scales.
|
|
|
|
Returns:
|
|
weights: Resmoothed weights.
|
|
weight_scaling_factors: Resmoothed scaling factors.
|
|
avg_pre_quant_scale: Calculated average of the quantization scale.
|
|
"""
|
|
if new_pre_quant_scale is None:
|
|
new_pre_quant_scale = torch.stack(pre_quant_scales).mean(dim=0)
|
|
|
|
assert len(pre_quant_scales) > 0 and new_pre_quant_scale.numel() == merged_weights.shape[1], (
|
|
"Shape of pre_quant_scales and weights do not match."
|
|
)
|
|
weights = torch.chunk(merged_weights, ranks, dim=0)
|
|
|
|
scales = []
|
|
new_weights = []
|
|
for i, p_scaling_factor in enumerate(pre_quant_scales):
|
|
# De smooth & Re smooth
|
|
weight = (
|
|
weights[i]
|
|
* p_scaling_factor.type(weights[i].dtype)
|
|
/ new_pre_quant_scale.type(weights[i].dtype)
|
|
)
|
|
new_weights.append(weight)
|
|
# If NVFP4_AWQ then we view the scales as uint8 to allow for cat later
|
|
if quantization == QUANTIZATION_NVFP4_AWQ:
|
|
scale, _ = NVFP4QTensor.get_weights_scaling_factor(weight, group_size).view(torch.uint8)
|
|
else:
|
|
scale = get_scaling_factor_from_weight(weight, group_size)
|
|
scales.append(scale)
|
|
|
|
resmoothed_scales = torch.cat(scales, dim=0)
|
|
|
|
return (
|
|
torch.cat(new_weights, dim=0),
|
|
resmoothed_scales.view(torch.float8_e4m3fn)
|
|
if quantization == QUANTIZATION_NVFP4_AWQ
|
|
else resmoothed_scales, # if NVFP4_AWQ we view the scales back as float8_e4m3fn after cat
|
|
new_pre_quant_scale,
|
|
)
|
|
|
|
|
|
def adjust_attn_amax_values(module):
|
|
"""Adjusts the amax values for the attention layers."""
|
|
projection_prefixes = ["q", "k", "v"]
|
|
max_amax = float("-inf")
|
|
proj_layers = []
|
|
|
|
# Find all projection layers whose names contain 'q', 'k', or 'v'
|
|
for name, sub_module in module.named_children():
|
|
for prefix in projection_prefixes:
|
|
if (
|
|
prefix in name
|
|
and hasattr(sub_module, "weight_quantizer")
|
|
and hasattr(sub_module.weight_quantizer, "amax")
|
|
):
|
|
proj_layers.append(sub_module)
|
|
max_amax = max(max_amax, sub_module.weight_quantizer.amax.item())
|
|
|
|
if not proj_layers:
|
|
raise ValueError(
|
|
"No projection layers with the specified prefixes ('q', 'k', 'v') have amax attributes"
|
|
)
|
|
|
|
assert max_amax > 0, "max_amax must be positive."
|
|
|
|
# Set all amax values to the maximum found
|
|
for proj_layer in proj_layers:
|
|
proj_layer.weight_quantizer.amax.fill_(max_amax)
|
|
|
|
|
|
def get_scaling_factor(quantizer: TensorQuantizer) -> torch.Tensor:
|
|
"""Returns scaling factor from the quantizer as torch.Tensor."""
|
|
if not quantizer.is_enabled:
|
|
return None
|
|
|
|
amax = quantizer.export_amax()
|
|
if amax is None:
|
|
return None
|
|
|
|
# tensorrt_llm uses float as the scaling_factors.
|
|
if quantizer.num_bits == (2, 1):
|
|
scaling_factor = NVFP4QTensor.get_weights_scaling_factor_2_from_quantizer(quantizer)
|
|
else:
|
|
scaling_factor = amax.float() / quantizer.maxbound
|
|
|
|
assert torch.all(scaling_factor > 0), f"scaling factor {scaling_factor} not positive."
|
|
|
|
return scaling_factor
|
|
|
|
|
|
def get_activation_scaling_factor(module: nn.Module) -> torch.Tensor:
|
|
"""Returns the activation scaling factor."""
|
|
# If NVFP4, return activation scaling factor from NVFP4QTensor
|
|
if get_quantization_format(module) in [
|
|
QUANTIZATION_NVFP4,
|
|
QUANTIZATION_NVFP4_AWQ,
|
|
] and hasattr(module, "input_quantizer"):
|
|
return NVFP4QTensor.get_activation_scaling_factor(module.input_quantizer)
|
|
return (
|
|
get_scaling_factor(module.input_quantizer) if hasattr(module, "input_quantizer") else None
|
|
)
|
|
|
|
|
|
def get_weight_scaling_factor(module: nn.Module) -> torch.Tensor:
|
|
"""Returns the weight scaling factor."""
|
|
# module.weight_quantizer could be a TensorQuantizer (for algorithms except W4A8) or
|
|
# a SequentialQuantizer (for W4A8). In the latter case, we need to get the scaling factor from the
|
|
# first quantizer of the SequentialQuantizer instance.
|
|
if hasattr(module, "weight_quantizer") and isinstance(
|
|
module.weight_quantizer, SequentialQuantizer
|
|
):
|
|
return get_scaling_factor(module.weight_quantizer[0])
|
|
|
|
# If NVFP4, we need to return quantized per_block scaling factors
|
|
if get_quantization_format(module) in [
|
|
QUANTIZATION_NVFP4,
|
|
QUANTIZATION_NVFP4_AWQ,
|
|
] and hasattr(module, "weight_quantizer"):
|
|
return NVFP4QTensor.get_weights_scaling_factor(
|
|
module.weight,
|
|
module.weight_quantizer.block_sizes[-1],
|
|
NVFP4QTensor.get_weights_scaling_factor_2_from_quantizer(module.weight_quantizer).to(
|
|
module.weight.device
|
|
),
|
|
)[0]
|
|
|
|
if get_quantization_format(module) == QUANTIZATION_W4A8_MXFP4_FP8:
|
|
return MXFP4QTensor.quantize(
|
|
module.weight, block_size=module.weight_quantizer.block_sizes[-1]
|
|
)[1].reshape(*module.weight.shape[:-1], -1)
|
|
return (
|
|
get_scaling_factor(module.weight_quantizer) if hasattr(module, "weight_quantizer") else None
|
|
)
|
|
|
|
|
|
def get_weight_scaling_factor_2(module: nn.Module) -> torch.Tensor:
|
|
"""Returns the secondary weight scaling factor."""
|
|
if get_quantization_format(module) in [
|
|
QUANTIZATION_NVFP4,
|
|
QUANTIZATION_NVFP4_AWQ,
|
|
] and hasattr(module, "weight_quantizer"):
|
|
return NVFP4QTensor.get_weights_scaling_factor_2_from_quantizer(module.weight_quantizer)
|
|
if (
|
|
not hasattr(module, "weight_quantizer")
|
|
or not isinstance(module.weight_quantizer, SequentialQuantizer)
|
|
or not module.weight_quantizer[-1].is_enabled
|
|
):
|
|
return None
|
|
assert len(module.weight_quantizer) == 2, (
|
|
"modelopt only supports 2 sequential quantization layers for now"
|
|
)
|
|
return get_scaling_factor(module.weight_quantizer[-1])
|
|
|
|
|
|
def get_prequant_scaling_factor(module: nn.Module) -> torch.Tensor:
|
|
"""Returns the prequant scaling factor."""
|
|
prequant_scaling_factor = (
|
|
module.input_quantizer._pre_quant_scale.squeeze()
|
|
if hasattr(module, "input_quantizer")
|
|
and hasattr(module.input_quantizer, "_pre_quant_scale")
|
|
else None
|
|
)
|
|
|
|
if prequant_scaling_factor is not None:
|
|
assert torch.all(prequant_scaling_factor > 0), (
|
|
f"prequant scaling factor {prequant_scaling_factor} not positive."
|
|
)
|
|
return prequant_scaling_factor
|
|
|
|
|
|
def get_kv_cache_bias(kv_module: nn.Module) -> list[torch.Tensor]:
|
|
"""Returns the kv_cache bias if _bias_value is set. Else returns None."""
|
|
kv_bias = []
|
|
for quantizer in ["k_bmm_quantizer", "v_bmm_quantizer"]:
|
|
quantizer_module = getattr(kv_module, quantizer, None)
|
|
kv_bias.append(getattr(quantizer_module, "_bias_value", None))
|
|
return kv_bias
|
|
|
|
|
|
def get_kv_cache_scaling_factor(kv_module: nn.Module) -> list[torch.Tensor]:
|
|
"""Returns the kv_cache scaling factor if output quantizer is set. Else returns None by default."""
|
|
if not hasattr(kv_module, "k_bmm_quantizer") or not hasattr(kv_module, "v_bmm_quantizer"):
|
|
return [None, None]
|
|
|
|
scaling_factors = [
|
|
get_scaling_factor(getattr(kv_module, quantizer))
|
|
for quantizer in ("k_bmm_quantizer", "v_bmm_quantizer")
|
|
]
|
|
|
|
# For FP8, we recommend default kv cache scaling factor to be 1.
|
|
if get_kv_cache_dtype(kv_module) == KV_CACHE_FP8:
|
|
for i, factor in enumerate(scaling_factors):
|
|
if factor.item() > 0.5:
|
|
warn(
|
|
f"Warning: Large KV activation detected: {factor.item()}, "
|
|
"Quantized KV cache may lead to higher accuracy drop."
|
|
)
|
|
scaling_factors[i] = torch.max(
|
|
factor, torch.tensor([1.0], dtype=torch.float, device=factor.device)
|
|
)
|
|
|
|
return scaling_factors
|
|
|
|
|
|
def get_kv_cache_dtype(modules: list[nn.Module] | nn.Module) -> str | None:
|
|
"""Returns the kv_cache dtype.
|
|
|
|
If num_bits of output_quantizer is (4, 3) then returns FP8; if it is 8, returns int8,
|
|
otherwise returns None.
|
|
|
|
Args:
|
|
modules: The module or list of modules to inspect.
|
|
|
|
Returns:
|
|
The kv_cache dtype.
|
|
"""
|
|
num_bits_list = []
|
|
is_affine = True
|
|
|
|
if isinstance(modules, nn.Module):
|
|
modules = [modules]
|
|
|
|
for module in modules:
|
|
# Case where the module has both k_bmm_quantizer and v_bmm_quantizer
|
|
# Still check for output quantizer for the unified_megatron_export path
|
|
for quantizer in ("k_bmm_quantizer", "v_bmm_quantizer", "output_quantizer"):
|
|
quantizer_attr = getattr(module, quantizer, None)
|
|
if quantizer_attr and quantizer_attr.is_enabled:
|
|
num_bits_list.append(quantizer_attr.num_bits)
|
|
is_affine &= hasattr(quantizer_attr, "_bias_value")
|
|
|
|
if (4, 3) in num_bits_list:
|
|
return KV_CACHE_FP8
|
|
elif 8 in num_bits_list:
|
|
return KV_CACHE_INT8
|
|
elif (2, 1) in num_bits_list and is_affine:
|
|
return KV_CACHE_NVFP4_AFFINE
|
|
elif (2, 1) in num_bits_list:
|
|
return KV_CACHE_NVFP4
|
|
else:
|
|
return QUANTIZATION_NONE
|
|
|
|
|
|
def get_weight_block_size(module: nn.Module) -> int:
|
|
"""Returns the weight block size."""
|
|
if not hasattr(module, "weight_quantizer"):
|
|
return 0
|
|
|
|
weight_quantizer = module.weight_quantizer
|
|
|
|
if isinstance(weight_quantizer, SequentialQuantizer):
|
|
weight_quantizer = weight_quantizer[0]
|
|
|
|
if not weight_quantizer.is_enabled:
|
|
return 0
|
|
|
|
block_sizes = weight_quantizer.block_sizes
|
|
|
|
if block_sizes:
|
|
return block_sizes[-1]
|
|
return 0
|
|
|
|
|
|
def get_quantization_format(module) -> str | None:
|
|
"""Gets the quantization string.
|
|
|
|
Gets the quantization string by iterating through the module and its children.
|
|
The first non-None quantization string is returned.
|
|
"""
|
|
|
|
def _get_quantization_from_linear_layer(layer):
|
|
if not hasattr(layer, "weight_quantizer") or not layer.weight_quantizer.is_enabled:
|
|
return QUANTIZATION_NONE
|
|
|
|
w_quantizer = layer.weight_quantizer
|
|
|
|
# Handle SequentialQuantizer
|
|
if isinstance(w_quantizer, SequentialQuantizer):
|
|
assert (
|
|
len(w_quantizer) == 2
|
|
and w_quantizer[0].num_bits == 4
|
|
and w_quantizer[1].num_bits == (4, 3)
|
|
), "Unsupported SequentialQuantizer configuration"
|
|
assert (
|
|
w_quantizer[0].block_sizes
|
|
and len(w_quantizer[0].block_sizes) > 0
|
|
and w_quantizer[0].block_sizes[-1] > 0
|
|
), "Invalid block_sizes for SequentialQuantizer"
|
|
|
|
return QUANTIZATION_W4A8_AWQ
|
|
|
|
# Handle individual num_bits cases
|
|
if w_quantizer.num_bits == 4:
|
|
assert len(w_quantizer.block_sizes) > 0 and w_quantizer.block_sizes[-1] > 0, (
|
|
"Invalid block_sizes for INT4 quantizer"
|
|
)
|
|
return QUANTIZATION_INT4_AWQ
|
|
|
|
if w_quantizer.num_bits == 8:
|
|
return QUANTIZATION_INT8_SQ
|
|
|
|
if w_quantizer.num_bits == (4, 3):
|
|
if w_quantizer.block_sizes:
|
|
assert w_quantizer.block_sizes[-1] > 0, "Invalid block_sizes for FP8 quantizer"
|
|
if w_quantizer.fake_quant:
|
|
return QUANTIZATION_FP8_PB_WO
|
|
else:
|
|
return QUANTIZATION_FP8_PB_REAL
|
|
if w_quantizer.axis == 0:
|
|
return QUANTIZATION_FP8_PC_PT
|
|
return QUANTIZATION_FP8
|
|
|
|
if w_quantizer.num_bits == (2, 1):
|
|
if hasattr(layer, "input_quantizer") and hasattr(
|
|
layer.input_quantizer, "_pre_quant_scale"
|
|
):
|
|
return QUANTIZATION_NVFP4_AWQ
|
|
if getattr(layer, "fused_with_layernorm", False):
|
|
return QUANTIZATION_NVFP4_AWQ
|
|
block_sizes = getattr(layer.weight_quantizer, "block_sizes", None)
|
|
scale_bits = block_sizes.get("scale_bits", None) if block_sizes else None
|
|
if (
|
|
layer.weight_quantizer.is_enabled
|
|
and block_sizes
|
|
and block_sizes.get("type", "static") == "dynamic"
|
|
and scale_bits
|
|
and scale_bits == (8, 0)
|
|
and layer.input_quantizer.is_enabled
|
|
and layer.input_quantizer.num_bits == (4, 3)
|
|
and layer.input_quantizer.block_sizes is None
|
|
):
|
|
return QUANTIZATION_W4A8_MXFP4_FP8
|
|
return QUANTIZATION_NVFP4
|
|
|
|
# Raise error for unsupported num_bits
|
|
raise NotImplementedError(f"Unsupported quantizer with num_bits: {w_quantizer.num_bits}")
|
|
|
|
if is_quantized_linear(module):
|
|
return _get_quantization_from_linear_layer(module)
|
|
|
|
for _, layer in module.named_children():
|
|
if is_quantized_linear(layer):
|
|
quantization = _get_quantization_from_linear_layer(layer)
|
|
else:
|
|
quantization = get_quantization_format(layer)
|
|
|
|
# Try to see if other layers has quantization
|
|
if quantization != QUANTIZATION_NONE:
|
|
return quantization
|
|
|
|
return QUANTIZATION_NONE
|
|
|
|
|
|
def _prefix_wildcard_summarize_exclude_modules(unquantized_layers, quantized_layers):
|
|
"""Generate a summarization of the quantization layer configs using prefix wildcards.
|
|
|
|
Prefix wildcards means we only consider wildcards that is a prefix with a star in the end.
|
|
We do not consider other wildcards such as: a*b.
|
|
"""
|
|
|
|
def all_matching_prefix_wildcards(name):
|
|
# include all possible prefix wildcards, and the exact name itself
|
|
wildcards = {name}
|
|
for i in range(len(name) + 1):
|
|
wildcards.add(name[:i] + "*")
|
|
return wildcards
|
|
|
|
def next_formated_matching_prefix_wildcards(name: str) -> Generator[list[str], None, None]:
|
|
"""Enumerate formated prefix wildcards. A result may be a combination of prefix wildcards.
|
|
|
|
Formated here means we only consider wildcards at dot split. We need two patterns.
|
|
|
|
1. a single wildcard: module_name*
|
|
2. a set of 2 wildcards: {module_name, module_name.*}. We need this pattern set because
|
|
module_name* may match other modules with module_name as a prefix.
|
|
"""
|
|
for i in range(len(name)):
|
|
if name[i] == ".":
|
|
yield [name[:i] + "*"]
|
|
yield [name[:i], name[:i] + ".*"]
|
|
# in the end, itself only is a wildcard
|
|
yield [name]
|
|
|
|
# any of the wildcard in this set cannot be present in the result
|
|
negtive_wild_candidates = set()
|
|
for layer in quantized_layers:
|
|
negtive = all_matching_prefix_wildcards(layer)
|
|
negtive_wild_candidates.update(negtive)
|
|
logger.debug(
|
|
f"Quantized layer {layer}, prefix wildcards {negtive} identified as negative wildcards"
|
|
)
|
|
|
|
res_summary = set()
|
|
for layer in unquantized_layers:
|
|
candidate_wildcards = []
|
|
for wildcards in next_formated_matching_prefix_wildcards(layer):
|
|
if any(wildcard in negtive_wild_candidates for wildcard in wildcards):
|
|
# need a more specific wildcard
|
|
logger.debug(
|
|
f"Unquantized layer {layer}, prefix wildcards {wildcards} invalidated by negative wildcards"
|
|
)
|
|
continue
|
|
if all(wildcard in res_summary for wildcard in wildcards):
|
|
# we get covered already, do not need to move forward, and clear candidate
|
|
logger.debug(
|
|
f"Unquantized layer {layer}, prefix wildcards {wildcards} already covered"
|
|
)
|
|
candidate_wildcards = []
|
|
break
|
|
# find one, now terminate the search
|
|
candidate_wildcards = wildcards
|
|
logger.debug(
|
|
f"Unquantized layer {layer}, prefix wildcards {wildcards} identified as a new match"
|
|
)
|
|
break
|
|
res_summary.update(candidate_wildcards)
|
|
return res_summary
|
|
|
|
|
|
def process_layer_quant_config(layer_config_dict):
|
|
"""Processes per layer quantization information for TRTLLM export to quant_cfg.json."""
|
|
per_layer_config: dict[str, Any] = {
|
|
"quant_algo": None,
|
|
"kv_cache_quant_algo": None,
|
|
"quantized_layers": {},
|
|
}
|
|
layer_config: dict[str, Any] = {}
|
|
# Set of quantization formats used.
|
|
quantization_formats = set()
|
|
quantization_config = None
|
|
exclude_modules = []
|
|
|
|
for k, v in layer_config_dict.items():
|
|
if "awq_block_size" in k:
|
|
continue
|
|
|
|
# Get layer name for constructing quantized_layers dictionary under per_layer_config
|
|
prefix = ".".join(k.rsplit(".", 1)[:-1])
|
|
if v == "fp8":
|
|
layer_config = {"quant_algo": "FP8"}
|
|
elif v == "fp8_pc_pt":
|
|
layer_config = {"quant_algo": "FP8_PER_CHANNEL_PER_TOKEN"}
|
|
elif v == "int4_awq":
|
|
layer_config = {
|
|
"quant_algo": "W4A16_AWQ",
|
|
"group_size": layer_config_dict[prefix + ".awq_block_size"],
|
|
"has_zero_point": False,
|
|
"pre_quant_scale": True,
|
|
}
|
|
elif v == "w4a8_awq":
|
|
layer_config = {
|
|
"quant_algo": "W4A8_AWQ",
|
|
"group_size": layer_config_dict[prefix + ".awq_block_size"],
|
|
"has_zero_point": False,
|
|
"pre_quant_scale": True,
|
|
}
|
|
elif v == "int8_sq":
|
|
layer_config = {"quant_algo": "W8A8_SQ_PER_CHANNEL"}
|
|
elif v == "nvfp4":
|
|
layer_config = {
|
|
"quant_algo": "NVFP4",
|
|
"group_size": layer_config_dict[prefix + ".awq_block_size"],
|
|
}
|
|
elif v == "nvfp4_awq":
|
|
layer_config = {
|
|
"quant_algo": "NVFP4_AWQ",
|
|
"group_size": layer_config_dict[prefix + ".awq_block_size"],
|
|
"has_zero_point": False,
|
|
"pre_quant_scale": True,
|
|
}
|
|
elif v == "w4a8_mxfp4_fp8":
|
|
layer_config = {
|
|
"quant_algo": "W4A8_MXFP4_FP8",
|
|
"group_size": layer_config_dict[prefix + ".awq_block_size"],
|
|
}
|
|
else:
|
|
layer_config = {"quant_algo": v}
|
|
|
|
if layer_config["quant_algo"] != QUANTIZATION_NONE:
|
|
quantization_formats.add(str(layer_config))
|
|
quantization_config = layer_config
|
|
per_layer_config["quantized_layers"].update({prefix: layer_config})
|
|
else:
|
|
exclude_modules.append(prefix)
|
|
|
|
# If we have more than one quantization format, infer MIXED_PRECISION
|
|
if len(quantization_formats) > 1:
|
|
per_layer_config["quant_algo"] = "MIXED_PRECISION"
|
|
elif len(quantization_formats) == 1 and quantization_config is not None:
|
|
per_layer_config.update(quantization_config)
|
|
per_layer_config["exclude_modules"] = sorted(
|
|
_prefix_wildcard_summarize_exclude_modules(
|
|
exclude_modules, per_layer_config["quantized_layers"].keys()
|
|
)
|
|
)
|
|
per_layer_config.pop("quantized_layers")
|
|
|
|
return per_layer_config
|
|
|
|
|
|
def pack_int4_in_uint8(weight, weights_scaling_factor):
|
|
"""Packs the INT4 weights into uint8 tensor."""
|
|
out_dim = weight.shape[-2]
|
|
assert out_dim % 2 == 0, f"Cannot pack weight. Out dimension {out_dim} is not an even number."
|
|
in_dim = weight.shape[-1]
|
|
block_size = weight.shape[-1] // weights_scaling_factor.shape[-1]
|
|
|
|
# Scale, round, and clamp to the signed 4-bit range [-8..7].
|
|
int8_tensor = (
|
|
(weight / weights_scaling_factor[..., :, torch.arange(in_dim) // block_size])
|
|
.round()
|
|
.clamp(-8, 7)
|
|
.to(torch.int8)
|
|
)
|
|
|
|
# -- Handle the MoE (3D) case vs. the 2D case --
|
|
if int8_tensor.dim() == 3:
|
|
# Dimensions might be (experts, out_dim, in_dim)
|
|
transpose = int8_tensor.permute(0, 2, 1) # -> (experts, in_dim, out_dim)
|
|
# Reshape to group two output channels (out_dim // 2) and keep an extra dimension of size 2
|
|
transpose = transpose.reshape(-1, in_dim, out_dim // 2, 2) # (E, in_dim, out_dim//2, 2)
|
|
|
|
# Pack two 4-bit values (val0,val1) into a single byte:
|
|
val0 = transpose[..., 0] & 0x0F
|
|
val1 = transpose[..., 1] & 0x0F
|
|
packed_byte = val0 | (val1 << 4)
|
|
|
|
# Transpose back to the shape (experts, out_dim // 2, in_dim)
|
|
return packed_byte.permute(0, 2, 1).contiguous().view(torch.uint8)
|
|
|
|
else:
|
|
# 2D weights: shape typically (out_dim, in_dim)
|
|
# Transpose to (in_dim, out_dim)
|
|
reshaped = int8_tensor.T.reshape(in_dim, out_dim // 2, 2)
|
|
|
|
# Pack two 4-bit values into one byte
|
|
val0 = reshaped[..., 0] & 0x0F
|
|
val1 = reshaped[..., 1] & 0x0F
|
|
packed_byte = val0 | (val1 << 4)
|
|
|
|
# Return shape (out_dim // 2, in_dim)
|
|
return packed_byte.T.contiguous().view(torch.uint8)
|
|
|
|
|
|
def to_quantized_weight(
|
|
weight: torch.Tensor,
|
|
weights_scaling_factor: torch.Tensor,
|
|
quantization: str,
|
|
weights_scaling_factor2: torch.Tensor | None = None,
|
|
block_size: int | None = None,
|
|
):
|
|
"""Converts the weight to the quantized (packed) format."""
|
|
if weights_scaling_factor is not None:
|
|
weights_scaling_factor = weights_scaling_factor.to(weight.device)
|
|
|
|
if weights_scaling_factor2 is not None:
|
|
weights_scaling_factor2 = weights_scaling_factor2.to(weight.device)
|
|
|
|
# For compressed weights, we directly return the data from wrapper
|
|
if isinstance(weight, QTensorWrapper):
|
|
return weight.data
|
|
|
|
if quantization == QUANTIZATION_FP8:
|
|
# Fix RuntimeError: Promotion for Float8 Types is not supported, attempted to promote Float8_e4m3fn and Float
|
|
# in speculative decoding fp8 model export
|
|
if weight.dtype == torch.float8_e4m3fn:
|
|
warn("Skipping quantization: weight already in fp8 format")
|
|
return weight
|
|
|
|
if weight.dim() == 3:
|
|
# for MOE stacked weights
|
|
return (weight / weights_scaling_factor.unsqueeze(-1)).to(torch.float8_e4m3fn)
|
|
return (weight / weights_scaling_factor).to(torch.float8_e4m3fn)
|
|
|
|
if quantization == QUANTIZATION_INT8_SQ:
|
|
return (weight / weights_scaling_factor[:, None]).round().clamp(-128, 127).to(torch.int8)
|
|
|
|
if quantization == QUANTIZATION_FP8_PB_WO:
|
|
return FP8QTensor.quantize(
|
|
weight, weights_scaling_factor.squeeze(), block_sizes={-1: block_size, -2: block_size}
|
|
)[0]._quantized_data
|
|
|
|
if quantization == QUANTIZATION_FP8_PC_PT:
|
|
return (weight / weights_scaling_factor.unsqueeze(-1)).to(torch.float8_e4m3fn)
|
|
|
|
if quantization in [QUANTIZATION_INT4_AWQ, QUANTIZATION_W4A8_AWQ]:
|
|
return pack_int4_in_uint8(weight, weights_scaling_factor)
|
|
|
|
if quantization in [QUANTIZATION_NVFP4, QUANTIZATION_NVFP4_AWQ]:
|
|
assert block_size is not None, "Block size not passed. Unable to quantize to NVFP4 format."
|
|
assert weights_scaling_factor2 is not None, (
|
|
"Weights scaling factor 2 not passed. Unable to quantize to NVFP4 format"
|
|
)
|
|
# If MoE reshape weights_scaling_factor2 to enable quantize operations
|
|
return NVFP4QTensor.quantize(
|
|
weight,
|
|
block_size,
|
|
weights_scaling_factor,
|
|
weights_scaling_factor2.view(-1, 1, 1)
|
|
if weights_scaling_factor2.dim() != 0
|
|
else weights_scaling_factor2,
|
|
)[0]._quantized_data
|
|
|
|
if quantization == QUANTIZATION_W4A8_MXFP4_FP8:
|
|
return MXFP4QTensor.quantize(weight, block_size=block_size)[0]._quantized_data
|
|
|
|
raise NotImplementedError(f"quantization format {quantization} not supported")
|
|
|
|
|
|
def from_quantized_weight(
|
|
weight: torch.Tensor,
|
|
weights_scaling_factor: torch.Tensor,
|
|
quantization: str,
|
|
torch_dtype,
|
|
):
|
|
"""Converts the quantized weight to the target torch_dtype format."""
|
|
if weight.element_size() >= 2 or weights_scaling_factor is None or not quantization:
|
|
# No need to unquantize the weight.
|
|
return weight.to(torch_dtype)
|
|
|
|
if quantization == QUANTIZATION_FP8:
|
|
# safe tensors does not support fp8 yet. So we pack the tensors as int8
|
|
return weight.view(torch.float8_e4m3fn).to(torch_dtype) * weights_scaling_factor.to(
|
|
torch_dtype
|
|
)
|
|
|
|
if quantization == QUANTIZATION_INT8_SQ:
|
|
return weight.to(torch_dtype) * weights_scaling_factor[:, None].to(torch_dtype)
|
|
|
|
raise NotImplementedError(f"quantization format {quantization} not supported")
|
|
|
|
|
|
def postprocess_state_dict(state_dict: dict, maxbound: float, quantization: str | None) -> dict:
|
|
"""Filters out keys related to weight quantizers and updates KV cache related keys.
|
|
|
|
Args:
|
|
state_dict: The full model state_dict.
|
|
maxbound: The maximum bound value for the output quantizer.
|
|
quantization: The KV cache quantization format.
|
|
|
|
Returns:
|
|
The filtered state_dict without unnecessary keys like '_amax' and non KV cache output quantizers.
|
|
"""
|
|
replacements = {
|
|
"k_bmm_quantizer._amax": "k_proj.k_scale",
|
|
"v_bmm_quantizer._amax": "v_proj.v_scale",
|
|
"k_bmm_quantizer._bias_value": "k_proj.k_bias",
|
|
"v_bmm_quantizer._bias_value": "v_proj.v_bias",
|
|
"input_quantizer._pre_quant_scale": "pre_quant_scale",
|
|
}
|
|
|
|
post_state_dict = {}
|
|
|
|
for key, value in state_dict.items():
|
|
# Skip keys not related to quantizers
|
|
if (
|
|
"output_quantizer" not in key
|
|
and "_amax" not in key
|
|
and "_bias_value" not in key
|
|
and "input_quantizer._pre_quant_scale" not in key
|
|
):
|
|
post_state_dict[key] = value
|
|
continue
|
|
|
|
# Apply replacements if the key matches any suffix in the replacements dict
|
|
for old_suffix, new_suffix in replacements.items():
|
|
if key.endswith(old_suffix):
|
|
prefix = key[: -len(old_suffix)]
|
|
|
|
if "_amax" in key:
|
|
assert quantization in [KV_CACHE_FP8, KV_CACHE_NVFP4, KV_CACHE_NVFP4_AFFINE], (
|
|
"Invalid KV cache quantization format."
|
|
)
|
|
assert maxbound > 0, "Maxbound must be greater than zero."
|
|
|
|
value = value.float() / maxbound
|
|
|
|
# Warn if scale exceeds threshold
|
|
if quantization == KV_CACHE_FP8 and value.item() > 0.5:
|
|
logger.warning(
|
|
"Large KV activations detected. Quantized KV cache may lead to higher accuracy drop. "
|
|
"Setting KV cache scaling factor to at least 1."
|
|
)
|
|
|
|
# Ensure scale is at least 1 for KV_CACHE_FP8
|
|
# We export real value for KV_CACHE_NVFP4
|
|
if quantization == KV_CACHE_FP8:
|
|
value.clamp_(min=1.0)
|
|
|
|
post_state_dict[prefix + new_suffix] = value
|
|
break
|
|
|
|
# Squeeze tensors with a leading dimension of 1
|
|
for key, value in post_state_dict.items():
|
|
if isinstance(value, torch.Tensor) and value.dim() == 3 and value.shape[0] == 1:
|
|
post_state_dict[key] = value.squeeze(0)
|
|
|
|
# remove real quant parameters from the state dict
|
|
keys_to_delete = []
|
|
for key, value in post_state_dict.items():
|
|
if any(
|
|
key.endswith("weight_quantizer." + q_key)
|
|
for q_key in RealQuantLinear.list_of_scale_tensors
|
|
):
|
|
keys_to_delete.append(key)
|
|
|
|
for key in keys_to_delete:
|
|
del post_state_dict[key]
|
|
|
|
return post_state_dict
|
|
|
|
|
|
def all_items_same(item_list):
|
|
"""Checks if all elements in the provided list are the same."""
|
|
return all(x == item_list[0] for x in item_list)
|
|
|
|
|
|
def fuse_prequant_layernorm(
|
|
layernorm_module: torch.nn.Module,
|
|
modules: list[torch.Tensor],
|
|
):
|
|
"""Scales layernorm weights with avg_pre_quant_scale of the modules list and sets pre_quant_scales to be deleted."""
|
|
layernorm_module.weight = torch.nn.Parameter(
|
|
layernorm_module.weight * getattr(modules[0].input_quantizer, "_pre_quant_scale")
|
|
)
|
|
# Pre_quant_scales of modules must not be exported, since they have been fused with layernorm
|
|
for module in modules:
|
|
delattr(module.input_quantizer, "_pre_quant_scale")
|
|
setattr(module, "fused_with_layernorm", True)
|
|
|
|
|
|
def preprocess_linear_fusion(modules: list[torch.nn.Module], resmooth_only=False):
|
|
"""Preprocess the quantized linears that we plan to fuse.
|
|
|
|
Use resmooth_only for MOE experts as each individual expert is not fused.
|
|
"""
|
|
quantization_format_list = [get_quantization_format(module) for module in modules]
|
|
assert all_items_same(quantization_format_list), "Modules have different quantization formats"
|
|
|
|
# Activation
|
|
if hasattr(modules[0], "input_quantizer"):
|
|
# Resmooth
|
|
if modules[0].input_quantizer.pre_quant_scale is not None:
|
|
avg_prequant_scale = torch.mean(
|
|
torch.stack([module.input_quantizer.pre_quant_scale for module in modules]),
|
|
dim=0,
|
|
)
|
|
|
|
for module in modules:
|
|
if not torch.equal(module.input_quantizer.pre_quant_scale, avg_prequant_scale):
|
|
module.weight = nn.Parameter(
|
|
module.weight
|
|
* module.input_quantizer.pre_quant_scale.to(
|
|
dtype=module.weight.dtype, device=module.weight.device
|
|
)
|
|
/ avg_prequant_scale.to(
|
|
dtype=module.weight.dtype, device=module.weight.device
|
|
)
|
|
)
|
|
module.input_quantizer.pre_quant_scale = avg_prequant_scale
|
|
|
|
# Redo weights collection
|
|
module.weight_quantizer.reset_amax()
|
|
enable_stats_collection(module.weight_quantizer)
|
|
module.weight_quantizer(module.weight)
|
|
finish_stats_collection(module.weight_quantizer)
|
|
|
|
if resmooth_only:
|
|
return
|
|
|
|
if modules[0].input_quantizer.is_enabled and modules[0].input_quantizer.amax is not None:
|
|
assert modules[0].input_quantizer.amax.numel() == 1, (
|
|
"Only support scalar input quant amax"
|
|
)
|
|
|
|
input_amax = torch.max(torch.stack([module.input_quantizer.amax for module in modules]))
|
|
for module in modules:
|
|
module.input_quantizer.amax = input_amax
|
|
|
|
# Weight
|
|
if hasattr(modules[0], "weight_quantizer"):
|
|
is_seq_quant = isinstance(modules[0].weight_quantizer, SequentialQuantizer)
|
|
|
|
if is_seq_quant:
|
|
if modules[0].weight_quantizer[-1].is_enabled:
|
|
assert len(modules[0].weight_quantizer) == 2
|
|
weight_amax = torch.max(
|
|
torch.stack([module.weight_quantizer[-1].amax for module in modules])
|
|
)
|
|
for module in modules:
|
|
module.weight_quantizer[-1].amax = weight_amax
|
|
|
|
elif (
|
|
modules[0].weight_quantizer.is_enabled
|
|
and modules[0].weight_quantizer.amax is not None
|
|
and modules[0].weight_quantizer.amax.numel() == 1
|
|
):
|
|
weight_amax = torch.max(
|
|
torch.stack([module.weight_quantizer.amax for module in modules])
|
|
)
|
|
for module in modules:
|
|
module.weight_quantizer.amax = weight_amax
|
|
|
|
|
|
def get_quant_config(named_modules: nn.Module | dict[str, nn.Module]) -> dict[str, Any]:
|
|
"""Generate quantization config for a torch model.
|
|
|
|
Args:
|
|
model: The PyTorch model to analyze
|
|
|
|
Returns:
|
|
Dictionary containing the quantization configuration
|
|
"""
|
|
# Find first quantized linear layer to determine quantization format
|
|
quantization_format = None
|
|
block_size = None
|
|
|
|
# Create base config
|
|
quant_config = {
|
|
"producer": {
|
|
"name": "modelopt",
|
|
"version": __version__,
|
|
},
|
|
}
|
|
|
|
default_quantization = {
|
|
"quant_algo": None,
|
|
"kv_cache_quant_algo": None,
|
|
}
|
|
|
|
quant_config["quantization"] = default_quantization
|
|
|
|
# Layer config dict holds quantization format of each layer.
|
|
# It also holds awq_block_size information for applicable layers.
|
|
layer_config_dict = {}
|
|
|
|
kv_cache_format = QUANTIZATION_NONE
|
|
for name, module in dict(named_modules).items():
|
|
if hasattr(module, "input_quantizer") or hasattr(module, "weight_quantizer"):
|
|
quantization_format = get_quantization_format(module)
|
|
block_size = get_weight_block_size(module)
|
|
|
|
# Construct per layer config dictionary
|
|
layer_config_dict[name + ".quantization"] = quantization_format
|
|
layer_config_dict[name + ".awq_block_size"] = block_size
|
|
|
|
# Find kv cache quant format
|
|
if (
|
|
hasattr(module, "k_bmm_quantizer")
|
|
or hasattr(module, "v_bmm_quantizer")
|
|
or (hasattr(module, "output_quantizer") and module.output_quantizer.is_enabled)
|
|
):
|
|
if kv_cache_format == QUANTIZATION_NONE:
|
|
kv_cache_format = get_kv_cache_dtype(module)
|
|
else:
|
|
assert kv_cache_format == get_kv_cache_dtype(module), (
|
|
"Do not support mixed precision kv cache quantization"
|
|
)
|
|
|
|
# Process per layer quantization config dict
|
|
quant_config["quantization"].update(process_layer_quant_config(layer_config_dict))
|
|
|
|
if kv_cache_format is not None:
|
|
quant_config["quantization"]["kv_cache_quant_algo"] = kv_cache_format
|
|
|
|
return quant_config
|
|
|
|
|
|
def quantize_llama4_experts_for_hf_export(module: nn.Module):
|
|
"""Quantize the experts in the Llama4 model."""
|
|
from transformers.models.llama4.modeling_llama4 import Llama4TextExperts
|
|
|
|
assert isinstance(module, Llama4TextExperts), "Module is not a Llama4TextExperts"
|
|
|
|
assert module.gate_up_proj_weight_quantizer.is_enabled
|
|
assert module.down_proj_weight_quantizer.is_enabled
|
|
assert module.gate_up_proj_input_quantizer.is_enabled
|
|
assert module.down_proj_input_quantizer.is_enabled
|
|
|
|
# Handle uncalibrated input quantizers that have None amax values
|
|
input_quantizers = [
|
|
module.gate_up_proj_input_quantizer,
|
|
module.down_proj_input_quantizer,
|
|
]
|
|
|
|
# Only handle amax for non-dynamic quantizers
|
|
non_dynamic_quantizers = [q for q in input_quantizers if not getattr(q, "_dynamic", False)]
|
|
|
|
if non_dynamic_quantizers:
|
|
# Find the maximum amax value from non-None quantizers
|
|
valid_amax_values = [
|
|
quantizer.amax for quantizer in non_dynamic_quantizers if quantizer.amax is not None
|
|
]
|
|
|
|
device = module.gate_up_proj.device
|
|
|
|
# If all quantizers have None amax, set a default value
|
|
if not valid_amax_values:
|
|
default_amax = torch.tensor(1.0, dtype=torch.float32, device=device)
|
|
warn(
|
|
"All input quantizers have None amax values. Setting default amax to 1.0. "
|
|
"This typically occurs when experts are not activated during calibration. "
|
|
"Consider increasing your calibration dataset size to ensure all experts are exercised."
|
|
)
|
|
for quantizer in non_dynamic_quantizers:
|
|
if quantizer.amax is None:
|
|
quantizer.amax = default_amax.clone()
|
|
else:
|
|
# Set None amax values to the maximum of existing values
|
|
max_amax = torch.max(torch.stack(valid_amax_values))
|
|
if max_amax.device != device:
|
|
max_amax = max_amax.to(device)
|
|
for quantizer in non_dynamic_quantizers:
|
|
if quantizer.amax is None:
|
|
warn(
|
|
f"Missing amax value for input quantizer. Setting it to {max_amax.item()} for export. "
|
|
"This typically occurs when certain experts are not activated during calibration. "
|
|
"Consider increasing your calibration dataset size to ensure all experts are exercised."
|
|
)
|
|
quantizer.amax = max_amax.clone()
|
|
|
|
for weight_name in ["gate_up_proj", "down_proj"]:
|
|
weight = getattr(module, weight_name)
|
|
weight_quantizer = getattr(module, f"{weight_name}_weight_quantizer")
|
|
|
|
if weight_quantizer.num_bits == (4, 3):
|
|
assert not weight_quantizer.block_sizes
|
|
|
|
weight_scale = weight_quantizer.amax.to(torch.float32) / weight_quantizer.maxbound
|
|
|
|
module.register_buffer(
|
|
f"{weight_name}_weight_scale",
|
|
weight_scale,
|
|
)
|
|
|
|
setattr(
|
|
module,
|
|
weight_name,
|
|
nn.Parameter(
|
|
(weight / weight_scale.to(weight.dtype).to(weight.device)).to(
|
|
torch.float8_e4m3fn
|
|
),
|
|
requires_grad=False,
|
|
),
|
|
)
|
|
|
|
elif weight_quantizer.num_bits == (2, 1):
|
|
# Maverick export can go OOM on the GPU. So just move to the CPU for weights compression.
|
|
weight = weight.to("cpu")
|
|
weight_scale_2 = NVFP4QTensor.get_weights_scaling_factor_2_from_quantizer(
|
|
weight_quantizer
|
|
).to("cpu")
|
|
|
|
module.register_buffer(
|
|
f"{weight_name}_weight_scale_2",
|
|
weight_scale_2,
|
|
)
|
|
|
|
block_size = weight_quantizer.block_sizes[-1]
|
|
|
|
# For bmm, the weight shape is (num_experts, input_dim, output_dim), so let's first transpose
|
|
# the weight to (num_experts, output_dim, input_dim) before calculating scaling factor and quantization.
|
|
weight = weight.transpose(-2, -1)
|
|
weight_scale = NVFP4QTensor.get_weights_scaling_factor(
|
|
weight,
|
|
block_size=block_size,
|
|
weights_scaling_factor_2=weight_scale_2,
|
|
)[0]
|
|
quantized_weights = to_quantized_weight(
|
|
weight,
|
|
weight_scale,
|
|
quantization=QUANTIZATION_NVFP4,
|
|
weights_scaling_factor2=weight_scale_2,
|
|
block_size=block_size,
|
|
)
|
|
# After quantization, we transpose the weight and scales back to the original order.
|
|
quantized_weights = quantized_weights.transpose(-2, -1)
|
|
weight_scale = weight_scale.transpose(-2, -1)
|
|
module.register_buffer(
|
|
f"{weight_name}_weight_scale",
|
|
weight_scale,
|
|
)
|
|
|
|
setattr(
|
|
module,
|
|
weight_name,
|
|
nn.Parameter(quantized_weights, requires_grad=False),
|
|
)
|
|
|
|
for input_name in ["gate_up_proj", "down_proj"]:
|
|
input_quantizer = getattr(module, f"{input_name}_input_quantizer")
|
|
|
|
# Skip processing for dynamic quantization since it doesn't have fixed amax
|
|
if getattr(input_quantizer, "_dynamic", False):
|
|
continue
|
|
|
|
if input_quantizer.num_bits == (4, 3):
|
|
assert not input_quantizer.block_sizes
|
|
|
|
input_scale = input_quantizer.amax.to(torch.float32) / input_quantizer.maxbound
|
|
module.register_buffer(
|
|
f"{input_name}_input_scale",
|
|
input_scale,
|
|
)
|
|
|
|
elif input_quantizer.num_bits == (2, 1):
|
|
input_scale_2 = NVFP4QTensor.get_weights_scaling_factor_2_from_quantizer(
|
|
input_quantizer
|
|
)
|
|
|
|
module.register_buffer(
|
|
f"{input_name}_input_scale",
|
|
input_scale_2,
|
|
)
|