chore: import upstream snapshot with attribution
Lint / lint (push) Has been cancelled
Build Docs / Deploy Docs (push) Has been cancelled
Windows CI / Windows (push) Has been cancelled

This commit is contained in:
wehub-resource-sync
2026-07-13 13:23:58 +08:00
commit 770d92cb1f
694 changed files with 114634 additions and 0 deletions
+11
View File
@@ -0,0 +1,11 @@
"""A subpackage for quantization and dequantization algorithms"""
from .awq_quantization import AWQQuantize
from .block_scale_quantization import BlockScaleQuantize
from .fp8_quantization import FP8PerTensorQuantizeMixtralExperts
from .ft_quantization import FTQuantize
from .group_quantization import GroupQuantize
from .model_quantization import make_awq_quant, make_quantization_functions
from .no_quantization import NoQuantize
from .per_tensor_quantization import PerTensorQuantize
from .quantization import QUANTIZATION, Quantization
@@ -0,0 +1,282 @@
"""AWQ Quantization"""
from dataclasses import dataclass, field
from typing import Any, Callable, Dict, List, Optional # noqa: UP035
from tvm import DataType, DataTypeCode, te, tirx, topi
from tvm.relax.frontend import nn
from tvm.runtime import Tensor
from mlc_llm.loader import QuantizeMapping
from .utils import convert_uint_to_float, is_final_fc, is_moe_gate
def _make_divisible(c, divisor):
return (c + divisor - 1) // divisor
def _calculate_zeros_width(in_features, group_size=128, pack_num=8):
if group_size >= 128:
size_multiplier = 1
elif group_size == 64:
size_multiplier = 2
elif group_size == 32:
size_multiplier = 4
else:
raise NotImplementedError
base_width = _make_divisible(in_features // group_size, pack_num)
base_width = _make_divisible(base_width, size_multiplier) * size_multiplier
return base_width
@dataclass
class AWQQuantize:
"""Configuration for AWQ quantization"""
name: str
kind: str
group_size: int
quantize_dtype: str # "int3", "int4", "int8"
storage_dtype: str # "uint32"
model_dtype: str # "float16", "float32"
num_elem_per_storage: int = 0
num_storage_per_group: int = 0
max_int_value: int = 0
prebuilt_quantize_func: Dict[str, Callable[[Tensor], Tensor]] = field( # noqa: UP006
default_factory=lambda: {}
)
def __post_init__(self):
assert self.kind == "awq"
quantize_dtype = DataType(self.quantize_dtype)
storage_dtype = DataType(self.storage_dtype)
model_dtype = DataType(self.model_dtype)
assert quantize_dtype.type_code == DataTypeCode.INT
assert storage_dtype.type_code == DataTypeCode.UINT
assert model_dtype.type_code == DataTypeCode.FLOAT
if storage_dtype.bits < quantize_dtype.bits:
raise ValueError("Storage unit should be greater or equal to quantized element")
self.num_elem_per_storage = storage_dtype.bits // quantize_dtype.bits
if self.group_size % self.num_elem_per_storage != 0:
raise ValueError("Group size should be divisible by numbers of elements per storage")
self.num_storage_per_group = self.group_size // self.num_elem_per_storage
self.max_int_value = (2 ** (quantize_dtype.bits - 1)) - 1
def quantize_model(
self,
model: nn.Module,
quant_map: QuantizeMapping,
name_prefix: str,
) -> nn.Module:
"""
Quantize model with awq quantization.
Parameters
----------
model : nn.Module
The non-quantized nn.Module.
quant_map : QuantizeMapping
The quantize mapping with name mapping and func mapping.
name_prefix : str
The name prefix for visited weight.
Returns
-------
ret : nn.Module
The quantized nn.Module.
"""
class _Mutator(nn.Mutator):
def __init__(self, config: AWQQuantize, quant_map: QuantizeMapping) -> None:
super().__init__()
self.config = config
self.quant_map = quant_map
def visit_module(self, name: str, node: nn.Module) -> Any:
"""
The visiting method for awq quantization of nn.Module nodes.
Parameters
----------
name : str
The name of the current node
node : nn.Module
The current node of nn.Module to mutate.
Returns
-------
ret_node : Any
The new node to replace current node.
"""
if (
isinstance(node, nn.Linear)
and not is_final_fc(name)
and not is_moe_gate(name, node)
):
return AWQQuantizeLinear.from_linear(node, self.config)
return self.visit(name, node)
model.to(dtype=self.model_dtype)
mutator = _Mutator(self, quant_map)
model = mutator.visit(name_prefix, model)
return model
def _dequantize(
self,
weight: te.Tensor,
zeros: te.Tensor,
scale: te.Tensor,
out_shape: Optional[List[tirx.Expr]] = None, # noqa: UP006
):
float_weight = convert_uint_to_float(
weight,
DataType(self.quantize_dtype).bits,
self.num_elem_per_storage,
self.storage_dtype,
self.model_dtype,
out_shape=[weight.shape[0], weight.shape[1] * self.num_elem_per_storage],
ft_reorder=True,
)
float_zeros = convert_uint_to_float(
zeros,
DataType(self.quantize_dtype).bits,
self.num_elem_per_storage,
self.storage_dtype,
self.model_dtype,
out_shape=[zeros.shape[0], zeros.shape[1] * self.num_elem_per_storage],
ft_reorder=True,
)
float_weight = topi.transpose(float_weight)
float_zeros = topi.transpose(float_zeros)
scale = topi.transpose(scale)
return te.compute(
shape=(
[weight.shape[0], weight.shape[1] * self.num_elem_per_storage]
if out_shape is None
else out_shape
),
fcompute=lambda i, j: tirx.Mul(
tirx.Sub(float_weight[i, j], float_zeros[i, j // self.group_size]),
scale[i, j // self.group_size],
),
name="dequantize",
)
class AWQQuantizeLinear(nn.Module):
"""An nn.Linear module with AWQ quantization"""
def __init__(
self,
in_features: int,
out_features: int,
config: AWQQuantize,
bias: bool = True,
out_dtype: Optional[str] = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.out_dtype = out_dtype
self.config = config
self.qweight = nn.Parameter(
(in_features, out_features // config.num_elem_per_storage),
config.storage_dtype,
)
self.qzeros = nn.Parameter(
(
in_features // config.group_size,
out_features // config.num_elem_per_storage,
),
config.storage_dtype,
)
self.scales = nn.Parameter(
(in_features // config.group_size, out_features), config.model_dtype
)
if bias:
self.bias = nn.Parameter(
(out_features,), config.model_dtype if out_dtype is None else out_dtype
)
else:
self.bias = None
@staticmethod
def from_linear(linear: nn.Linear, config: AWQQuantize) -> "AWQQuantizeLinear":
"""
Converts a non-quantized nn.Linear to a group quantized AWQQuantizeLinear
Parameters
----------
linear : nn.Linear
The non-quantized nn.Linear.
config : AWQQuantize
The awq quantization config.
Returns
-------
ret : GroupQuantizeLinear
The awq quantized AWQQuantizeLinear layer.
"""
return AWQQuantizeLinear(
in_features=linear.in_features,
out_features=linear.out_features,
config=config,
bias=getattr(linear, "bias", None) is not None,
out_dtype=linear.out_dtype,
)
def forward(self, x: nn.Tensor) -> nn.Tensor:
"""
Forward method for awq quantized linear layer
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the group quantized linear layer.
"""
w = nn.op.tensor_expr_op(
lambda weight, zeros, scale: self.config._dequantize(
weight,
zeros,
scale,
[
tirx.IntImm("int64", self.out_features),
tirx.IntImm("int64", self.in_features),
],
),
name_hint="dequantize",
args=[self.qweight, self.qzeros, self.scales],
)
w = nn.op.permute_dims(w)
x = nn.op.matmul(x, w, out_dtype=self.out_dtype)
if self.bias is not None:
x = x + self.bias
return x
def to(self, dtype: Optional[str] = None) -> None:
"""
Override to() such that we do not convert bias if there is an out_dtype.
Otherwise, we might run into dtype mismatch when computing x + self.bias.
"""
self.qweight.to(dtype=dtype)
self.qzeros.to(dtype=dtype)
self.scales.to(dtype=dtype)
if self.bias is not None and self.out_dtype is None:
self.bias.to(dtype=dtype)
if dtype is not None and isinstance(getattr(self, "dtype", None), str):
self.dtype = dtype
@@ -0,0 +1,828 @@
"""The block-scale quantization config"""
from dataclasses import dataclass
from typing import Any, Literal, Optional, Tuple # noqa: UP035
import tvm
from tvm import DataType, DataTypeCode, te, tirx
from tvm.relax.frontend import nn
from tvm.script import tirx as T
from mlc_llm.loader import QuantizeMapping
from mlc_llm.nn import MixtralExperts
from mlc_llm.op import cutlass, extern, moe_matmul, triton
from mlc_llm.support import logging
from mlc_llm.support import tensor_parallel as tp
from .utils import apply_sharding, is_final_fc, is_moe_gate
logger = logging.getLogger(__name__)
@dataclass
class BlockScaleQuantize:
"""Configuration for block-scale quantization"""
name: str
kind: str = "block-scale"
weight_dtype: Literal["float8_e4m3fn", "float8_e5m2"] = "float8_e4m3fn"
model_dtype: Literal["float16", "bfloat16"] = "bfloat16"
quantize_linear: bool = True
weight_block_size: Optional[Tuple[int, int]] = None # noqa: UP006
use_activation_scale: bool = False
def __post_init__(self):
assert self.kind == "block-scale-quant"
weight_dtype = DataType(self.weight_dtype)
model_dtype = DataType(self.model_dtype)
assert weight_dtype.type_code in [
DataTypeCode.Float8E4M3FN,
DataTypeCode.Float8E5M2,
]
assert model_dtype.type_code in [
DataTypeCode.FLOAT,
DataTypeCode.BFLOAT,
]
def quantize_model(
self,
model: nn.Module,
quant_map: QuantizeMapping,
name_prefix: str,
) -> nn.Module:
"""Quantize model with block-scale quantization
Parameters
----------
model : nn.Module
The non-quantized nn.Module.
quant_map : QuantizeMapping
The quantize mapping with name mapping and func mapping.
name_prefix : str
The name prefix for visited weight.
Returns
-------
ret : nn.Module
The quantized nn.Module.
"""
weight_block_size = model.weight_block_size
class _Mutator(nn.Mutator):
def __init__(self, config: BlockScaleQuantize, quant_map: QuantizeMapping) -> None:
super().__init__()
self.config = config
self.quant_map = quant_map
def visit_module(self, name: str, node: nn.Module) -> Any:
"""The visiting method for block-scale quantization of nn.Module nodes.
Parameters
----------
name : str
The name of the current node.
node : nn.Module
The current node of nn.Module to mutate.
Returns
------
ret : Any
"""
if getattr(node, "no_quantization", False):
return node
if hasattr(node, "w_uk"):
assert hasattr(node, "w_uv")
assert node.block_size == weight_block_size
if (
node.qk_nope_head_dim % node.block_size[0] != 0
or node.v_head_dim % node.block_size[1] != 0
):
raise ValueError(
"Invalid DeepSeek model config: "
"qk_nope_head_dim must be multiple of weight_block_size[0], and "
"v_head_dim must be multiple of weight_block_size[1]. "
f"However, qk_nope_head_dim is {node.qk_nope_head_dim}, "
f"v_head_dim is {node.v_head_dim}, "
f"weight_block_size is {node.block_size}."
)
w_uk_shard_strategy = node.w_uk.attrs.get("shard_strategy", None)
w_uv_shard_strategy = node.w_uv.attrs.get("shard_strategy", None)
node.w_uk = nn.Parameter(
(node.num_heads, node.kv_lora_rank, node.qk_nope_head_dim),
self.config.weight_dtype,
)
node.w_uv = nn.Parameter(
(node.num_heads, node.v_head_dim, node.kv_lora_rank),
self.config.weight_dtype,
)
node.w_uk_scale_inv = nn.Parameter(
(
node.num_heads,
node.kv_lora_rank // node.block_size[1],
node.qk_nope_head_dim // node.block_size[0],
),
"float32",
)
node.w_uv_scale_inv = nn.Parameter(
(
node.num_heads,
node.v_head_dim // node.block_size[0],
node.kv_lora_rank // node.block_size[1],
),
"float32",
)
if w_uk_shard_strategy is not None:
assert w_uk_shard_strategy.segs is None
apply_sharding(w_uk_shard_strategy, w_uk_shard_strategy.name, node.w_uk)
apply_sharding(
w_uk_shard_strategy,
f"{w_uk_shard_strategy.name}_scale_inv",
node.w_uk_scale_inv,
)
if w_uv_shard_strategy is not None:
assert w_uv_shard_strategy.segs is None
apply_sharding(w_uv_shard_strategy, w_uv_shard_strategy.name, node.w_uv)
apply_sharding(
w_uv_shard_strategy,
f"{w_uv_shard_strategy.name}_scale_inv",
node.w_uv_scale_inv,
)
if (
isinstance(node, nn.Linear)
and not is_final_fc(name)
and not is_moe_gate(name, node)
):
if self.config.use_activation_scale:
return BlockScaleQuantizeLinearStaticActivation.from_linear(
node, self.config, weight_block_size
)
return BlockScaleQuantizeLinear.from_linear(
node, self.config, weight_block_size
)
if isinstance(node, MixtralExperts):
return BlockScaleQuantizeMixtralExperts.from_mixtral_experts(
node, self.config, weight_block_size
)
return self.visit(name, node)
model.to(dtype=self.model_dtype)
mutator = _Mutator(self, quant_map)
model = mutator.visit(name_prefix, model)
self.weight_block_size = weight_block_size
return model
class BlockScaleQuantizeLinear(nn.Module):
"""Block-scale quantization for Linear"""
def __init__(
self,
in_features: int,
out_features: int,
weight_dtype: Literal["float8_e4m3fn", "float8_e5m2"],
block_size: Tuple[int, int], # noqa: UP006
bias: bool = True,
dtype: Optional[str] = None,
out_dtype: Optional[str] = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.out_dtype = out_dtype
self.weight = nn.Parameter((out_features, in_features), weight_dtype)
self.weight_scale_inv = nn.Parameter(
(
(out_features + block_size[0] - 1) // block_size[0],
(in_features + block_size[1] - 1) // block_size[1],
),
"float32",
)
self.weight_dtype = weight_dtype
self.block_size = block_size
if bias:
self.bias = nn.Parameter((out_features,), dtype if out_dtype is None else out_dtype)
else:
self.bias = None
@staticmethod
def from_linear(
src: nn.Linear,
config: BlockScaleQuantize,
weight_block_size: Optional[Tuple[int, int]], # noqa: UP006
) -> "BlockScaleQuantizeLinear":
"""
Converts a non-quantized nn.Linear to a block-scale quantized BlockScaleQuantizeLinear
Parameters
----------
src : nn.Linear
The non-quantized nn.Linear.
config : BlockScaleQuantize
The block-scale quantization config.
weight_block_size : Optional[Tuple[int, int]]
The weight block size.
Returns
-------
ret : BlockScaleQuantizeLinear
The block-scale quantized BlockScaleQuantizeLinear.
"""
assert weight_block_size is not None
out_features, in_features = src.weight.shape
quantized_linear = BlockScaleQuantizeLinear(
in_features=in_features,
out_features=out_features,
weight_dtype=config.weight_dtype,
block_size=weight_block_size,
bias=getattr(src, "bias", None) is not None,
dtype=config.model_dtype,
out_dtype=src.out_dtype,
)
if quantized_linear.bias is not None:
quantized_linear.bias.attrs = src.bias.attrs
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, shard.name, quantized_linear.weight)
if isinstance(shard, tp.ShardSingleDim) and shard.segs is not None:
shard.segs = [x // weight_block_size[shard.dim] for x in shard.segs]
apply_sharding(shard, f"{shard.name}_scale_inv", quantized_linear.weight_scale_inv)
return quantized_linear
def forward(self, x: nn.Tensor) -> nn.Tensor:
"""Forward pass of the block-scale quantized linear layer.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor.
"""
m = 1
for i in range(x.ndim - 1):
m *= x.shape[i]
if m == 1:
x_shape = x.shape
return dequantize_float8_groupwise_scaled_gemv(
x.reshape(1, x.shape[-1]),
self.weight,
self.weight_scale_inv,
self.block_size,
self.out_dtype if self.out_dtype is not None else x.dtype,
).reshape(*x_shape[:-1], -1)
shape_supported_by_cutlass = ( # noqa: F841
self.weight.shape[0] % 128 == 0 and self.weight.shape[1] % 128 == 0
)
# Todo: check "shape supported by cutlass" for Hopper
if (
extern.get_store().cutlass_gemm
and tvm.get_global_func(
"cutlass.groupwise_scaled_gemm_e4m3fn_e4m3fn", allow_missing=True
)
is not None
):
x_fp8, x_scale = rowwise_group_quant_fp8(
x, self.block_size[1], self.weight_dtype, transpose_scale=True
)
x = cutlass.fp8_groupwise_scaled_gemm(
x_fp8,
x_scale,
self.weight,
self.weight_scale_inv,
self.block_size,
self.out_dtype if self.out_dtype is not None else x.dtype,
)
else:
x_fp8, x_scale = rowwise_group_quant_fp8(
x, self.block_size[1], self.weight_dtype, transpose_scale=False
)
x = triton.fp8_groupwise_scaled_gemm(
x_fp8,
x_scale,
self.weight,
self.weight_scale_inv,
self.block_size,
self.out_dtype if self.out_dtype is not None else x.dtype,
)
if self.bias is not None:
x = x + self.bias
return x
def to(self, dtype: Optional[str] = None) -> None:
"""
Override to() such that we do not convert bias if there is an out_dtype.
Otherwise, we might run into dtype mismatch when computing x + self.bias.
"""
if self.bias is not None and self.out_dtype is None:
self.bias.to(dtype=dtype)
if dtype is not None and isinstance(getattr(self, "dtype", None), str):
self.dtype = dtype
class BlockScaleQuantizeLinearStaticActivation(BlockScaleQuantizeLinear):
"""Block-scale quantization for static activation FP8."""
def __init__(
self,
in_features: int,
out_features: int,
weight_dtype: Literal["float8_e4m3fn", "float8_e5m2"],
block_size: Tuple[int, int], # noqa: UP006
bias: bool = True,
dtype: Optional[str] = None,
out_dtype: Optional[str] = None,
) -> None:
super().__init__(
in_features=in_features,
out_features=out_features,
weight_dtype=weight_dtype,
block_size=block_size,
bias=bias,
dtype=dtype,
out_dtype=out_dtype,
)
num_in_groups = (in_features + block_size[1] - 1) // block_size[1]
self.activation_scale = nn.Parameter((num_in_groups,), "float32")
@staticmethod
def from_linear(
src: nn.Linear,
config: BlockScaleQuantize,
weight_block_size: Optional[Tuple[int, int]], # noqa: UP006
) -> "BlockScaleQuantizeLinearStaticActivation":
"""
Convert a non-quantized nn.Linear to a block-scale quantized BlockScaleQuantizeLinearStaticActivation.
Parameters
----------
src : nn.Linear
The non-quantized nn.Linear.
config : BlockScaleQuantize
The block-scale quantization config.
weight_block_size : Optional[Tuple[int, int]]
The weight block size.
Returns
-------
ret : BlockScaleQuantizeLinearStaticActivation
The block-scale quantized BlockScaleQuantizeLinearStaticActivation
""" # noqa: E501
assert weight_block_size is not None
out_features, in_features = src.weight.shape
quantized_linear = BlockScaleQuantizeLinearStaticActivation(
in_features=in_features,
out_features=out_features,
weight_dtype=config.weight_dtype,
block_size=weight_block_size,
bias=getattr(src, "bias", None) is not None,
dtype=config.model_dtype,
out_dtype=src.out_dtype,
)
if quantized_linear.bias is not None:
quantized_linear.bias.attrs = src.bias.attrs
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, shard.name, quantized_linear.weight)
if isinstance(shard, tp.ShardSingleDim) and shard.segs is not None:
shard.segs = [x // weight_block_size[shard.dim] for x in shard.segs]
apply_sharding(shard, f"{shard.name}_scale_inv", quantized_linear.weight_scale_inv)
apply_sharding(
shard,
f"{shard.name}_activation_scale",
quantized_linear.activation_scale,
)
return quantized_linear
def forward(self, x: nn.Tensor) -> nn.Tensor:
x_fp8 = static_activation_group_quant_fp8(
x,
self.activation_scale,
self.block_size[1],
self.weight_dtype,
)
shape_supported_by_cutlass = (
self.weight.shape[0] % 128 == 0 and self.weight.shape[1] % 128 == 0
)
if (
extern.get_store().cutlass_gemm
and shape_supported_by_cutlass
and tvm.get_global_func(
"cutlass.groupwise_scaled_gemm_e4m3fn_e4m3fn", allow_missing=True
)
is not None
):
x_scale = broadcast_activation_scale(
x,
self.activation_scale,
transpose=True,
)
out = cutlass.fp8_groupwise_scaled_gemm(
x_fp8,
x_scale,
self.weight,
self.weight_scale_inv,
self.block_size,
self.out_dtype if self.out_dtype is not None else x.dtype,
)
else:
x_scale_triton = broadcast_activation_scale(
x,
self.activation_scale,
transpose=False,
)
out = triton.fp8_groupwise_scaled_gemm(
x_fp8,
x_scale_triton,
self.weight,
self.weight_scale_inv,
self.block_size,
self.out_dtype if self.out_dtype is not None else x.dtype,
)
if self.bias is not None:
out = out + self.bias
return out
class BlockScaleQuantizeMixtralExperts(nn.Module):
"""Block-scale quantization for MoE experts"""
def __init__(
self,
num_local_experts: int,
in_features: int,
out_features: int,
weight_dtype: Literal["float8_e4m3fn", "float8_e5m2"],
block_size: Tuple[int, int], # noqa: UP006
) -> None:
super().__init__()
self.num_local_experts = num_local_experts
self.in_features = in_features
self.out_features = out_features
self.weight = nn.Parameter((num_local_experts, out_features, in_features), weight_dtype)
self.weight_scale_inv = nn.Parameter(
(
num_local_experts,
(out_features + block_size[0] - 1) // block_size[0],
(in_features + block_size[1] - 1) // block_size[1],
),
"float32",
)
self.weight_dtype = weight_dtype
self.block_size = block_size
@staticmethod
def from_mixtral_experts(
src: "MixtralExperts",
config: BlockScaleQuantize,
weight_block_size: Optional[Tuple[int, int]], # noqa: UP006
) -> "BlockScaleQuantizeMixtralExperts":
"""
Converts a non-quantized MixtralExperts to a block-scale
quantized BlockScaleQuantizeMixtralExperts
Parameters
----------
src : MixtralExperts
The non-quantized MixtralExperts
config : BlockScaleQuantize
The block-scale quantization config.
weight_block_size : Optional[Tuple[int, int]]
The weight block size.
Returns
-------
ret : BlockScaleQuantizeMixtralExperts
The block-scale quantized BlockScaleQuantizeMixtralExperts layer.
"""
assert weight_block_size is not None
quantized_mistral_experts = BlockScaleQuantizeMixtralExperts(
num_local_experts=src.num_local_experts,
in_features=src.in_features,
out_features=src.out_features,
weight_dtype=config.weight_dtype,
block_size=weight_block_size,
)
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, shard.name, quantized_mistral_experts.weight)
if isinstance(shard, tp.ShardSingleDim) and shard.segs is not None:
shard.segs = [x // weight_block_size[shard.dim - 1] for x in shard.segs]
apply_sharding(
shard,
f"{shard.name}_scale_inv",
quantized_mistral_experts.weight_scale_inv,
)
return quantized_mistral_experts
def forward(self, x: nn.Tensor, indptr: nn.Tensor) -> nn.Tensor:
"""Forward pass of the block-scale quantized MixtralExperts.
Parameters
----------
x : nn.Tensor
The input tensor.
indptr : nn.Tensor
The indptr tensor of group gemm, with shape of [num_experts + 1,].
Returns
-------
ret : nn.Tensor
The output tensor.
"""
if indptr.ndim == 2:
# The input is for single token, which does not need group gemm
# and can be specialized.
expert_indices = indptr
assert expert_indices.shape[0] == 1
return moe_matmul.dequantize_block_scale_float8_gemv(
x,
self.weight,
self.weight_scale_inv,
expert_indices,
self.block_size,
x.dtype,
)
x_fp8, x_scale = rowwise_group_quant_fp8(
x, self.block_size[1], self.weight_dtype, transpose_scale=False
)
if (
extern.get_store().cutlass_gemm
and tvm.get_global_func(
"cutlass.groupwise_scaled_group_gemm_e4m3fn_e4m3fn", allow_missing=True
)
is not None
):
x = cutlass.fp8_groupwise_scaled_group_gemm(
x_fp8,
x_scale,
self.weight,
self.weight_scale_inv,
indptr,
self.block_size,
x.dtype,
)
else:
x = triton.fp8_groupwise_scaled_group_gemm(
x_fp8,
x_scale,
self.weight,
self.weight_scale_inv,
indptr,
self.block_size,
x.dtype,
)
return x
def to(self, dtype: Optional[str] = None) -> None:
"""
Override to() such that we do not convert bias if there is an out_dtype.
Otherwise, we might run into dtype mismatch when computing x + self.bias.
"""
if dtype is not None and isinstance(getattr(self, "dtype", None), str):
self.dtype = dtype
def rowwise_group_quant_fp8(
x: nn.Tensor,
group_size: int,
dtype: Literal["float8_e4m3fn", "float8_e5m2"],
transpose_scale: bool,
eps: float = 1e-10,
keep_first_batch_dim: bool = False,
) -> Tuple[nn.Tensor, nn.Tensor]: # noqa: UP006
"""Rowwise group quantization of fp8 tensor.
Parameters
----------
x : nn.Tensor
The input tensor.
group_size : int
The group size per row for quantization.
transpose_scale : bool
Whether return the transposed scales or not.
Returns
-------
x_fp8 : nn.Tensor
The quantized tensor.
x_scale : nn.Tensor
The scales of the quantized tensor.
If transpose_scale is True, the shape is
(*x.shape[:-2], ceildiv(x.shape[-1], group_size), x.shape[-2]).
Otherwise, the shape is (*x.shape[:-1], ceildiv(x.shape[-1], group_size)).
"""
assert x.ndim >= 2
assert group_size > 0
def quantize(x: te.Tensor):
num_group = tirx.ceildiv(x.shape[-1], group_size)
max_abs_shape = (*x.shape[:-1], num_group)
max_abs_reduce_axis = te.reduce_axis((0, group_size), name="r")
scale_dtype = "float32"
max_abs = te.compute(
shape=max_abs_shape,
fcompute=lambda *idx: te.max(
tirx.if_then_else(
idx[-1] * group_size + max_abs_reduce_axis < x.shape[-1],
tirx.Max(
te.abs(
x(*idx[:-1], idx[-1] * group_size + max_abs_reduce_axis).astype(
scale_dtype
)
),
eps,
),
tirx.min_value(scale_dtype),
),
axis=max_abs_reduce_axis,
),
name="max_abs",
)
assert dtype in ["float8_e4m3fn", "float8_e5m2"]
fp8_max = 448.0 if dtype == "float8_e4m3fn" else 57344.0
fp8_min = -fp8_max
scale = te.compute(
shape=max_abs_shape,
fcompute=lambda *idx: max_abs(*idx) / tirx.const(fp8_max, scale_dtype),
name="scale",
)
x_quantized = te.compute(
shape=x.shape,
fcompute=lambda *idx: tirx.max(
tirx.min(
x(*idx).astype(scale_dtype) / scale(*idx[:-1], idx[-1] // group_size),
fp8_max,
),
fp8_min,
).astype(dtype),
name="x_quantized",
)
if transpose_scale:
if not keep_first_batch_dim:
scale = te.compute(
shape=(num_group, *x.shape[:-1]),
fcompute=lambda *idx: scale(*idx[1:], idx[0]),
name="scale",
)
else:
assert len(x.shape) > 2
scale = te.compute(
shape=(x.shape[0], num_group, *x.shape[1:-1]),
fcompute=lambda *idx: scale(idx[0], *idx[2:], idx[1]),
name="scale",
)
return x_quantized, scale
x_quantized, scale = nn.tensor_expr_op(quantize, name_hint="rowwise_group_quant_fp8", args=[x])
return x_quantized, scale
def static_activation_group_quant_fp8(
x: nn.Tensor,
activation_scale: nn.Tensor,
group_size: int,
dtype: Literal["float8_e4m3fn", "float8_e5m2"],
) -> nn.Tensor:
"""Quantize activations with a pre-computed scale."""
assert activation_scale.ndim == 1
def quantize(x: te.Tensor, scale: te.Tensor):
fp8_max = 448.0 if dtype == "float8_e4m3fn" else 57344.0
fp8_min = -fp8_max
def fcompute(*idx):
group_idx = tirx.indexdiv(idx[-1], group_size)
return tirx.max(
tirx.min(
x(*idx).astype("float32") / scale(group_idx),
fp8_max,
),
fp8_min,
).astype(dtype)
return te.compute(shape=x.shape, fcompute=fcompute, name="static_activation_group_fp8")
quantized = nn.tensor_expr_op(
quantize,
name_hint="static_activation_group_fp8",
args=[x, activation_scale],
)
return quantized
def broadcast_activation_scale(
x: nn.Tensor,
activation_scale: nn.Tensor,
transpose: bool,
) -> nn.Tensor:
"""Broadcast stored activation scales."""
reshape_shape = (1,) * (x.ndim - 1) + (activation_scale.shape[0],)
scale = nn.op.reshape(activation_scale, reshape_shape)
scale = nn.op.broadcast_to(scale, (*x.shape[:-1], activation_scale.shape[0]))
if transpose:
axes = list(range(scale.ndim))
axes[-1], axes[-2] = axes[-2], axes[-1]
scale = nn.op.permute_dims(scale, axes=axes)
return scale
def dequantize_float8_groupwise_scaled_gemv(
x: nn.Tensor,
w: nn.Tensor,
w_scale: nn.Tensor,
block_size: Tuple[int, int], # noqa: UP006
out_dtype: str,
) -> nn.Tensor:
"""GEMV for FP8 groupwise scaled quantization.
Parameters
----------
x : Tensor
The input tensor of shape (k,)
w : Tensor
The quantized weight tensor of shape (n, k)
w_scale : Tensor
The scale tensor of shape
(n // block_size[0], k // block_size[1])
block_size : Tuple[int, int]
The block size of the weight tensor.
out_dtype : str
The output dtype of the GEMV computation.
"""
assert x.ndim == 2
assert w.ndim == 2
assert w_scale.ndim == 2
assert x.shape[0] == 1
assert x.shape[1] == w.shape[1]
_, k = x.shape
n, _ = w.shape
model_dtype = x.dtype
quantize_dtype = w.dtype
assert (n + block_size[0] - 1) // block_size[0] == w_scale.shape[0]
assert (k + block_size[1] - 1) // block_size[1] == w_scale.shape[1]
def _dequantize(w, s, i, j):
return w[i, j].astype(model_dtype) * s[i // block_size[0], j // block_size[1]].astype(
model_dtype
)
@T.prim_func(private=True, s_tir=True)
def _func(
x: T.Buffer((1, k), model_dtype),
w: T.Buffer((n, k), quantize_dtype),
w_scale: T.Buffer(
(
(n + block_size[0] - 1) // block_size[0],
(k + block_size[1] - 1) // block_size[1],
),
"float32",
),
o: T.Buffer((n,), out_dtype),
):
T.func_attr({"op_pattern": 4, "tirx.noalias": True}) # kOutEWiseFusable
y = T.sblock_alloc_buffer((n, k), model_dtype)
for i1, i2 in T.grid(n, k):
with T.sblock("dequantize"):
i, j = T.axis.remap("SS", [i1, i2])
y[i, j] = _dequantize(w, w_scale, i, j)
for i1, i2 in T.grid(n, k):
with T.sblock("gemv"):
i, j = T.axis.remap("SR", [i1, i2])
with T.init():
o[i] = T.cast(T.float16(0), out_dtype)
o[i] += (x[0, j] * y[i, j]).astype(out_dtype)
return nn.op.tensor_ir_op(
_func,
"moe_dequantize_gemv",
args=[x, w, w_scale],
out=nn.Tensor.placeholder([n], out_dtype),
)
@@ -0,0 +1,122 @@
"""Quantization techniques for FP8"""
import numpy as np
from tvm import relax, runtime
from tvm.relax.frontend import nn
from mlc_llm.nn import MixtralExperts
from ..op import cutlass, extern, moe_matmul
from . import per_tensor_quantization as ptq
from .utils import apply_sharding
class FP8PerTensorQuantizeMixtralExperts(ptq.PerTensorQuantizeMixtralExperts):
"""MixtralExperts with per-tensor quantization in FP8."""
def __init__(
self,
num_local_experts,
in_features,
out_features,
config: ptq.PerTensorQuantize,
name: str,
tensor_parallel_shards=1,
):
super().__init__(num_local_experts, in_features, out_features, config, name)
self.tensor_parallel_shards = tensor_parallel_shards
@staticmethod
def from_mixtral_experts(
src: "MixtralExperts",
config: ptq.PerTensorQuantize,
name: str,
) -> "FP8PerTensorQuantizeMixtralExperts":
"""
Converts a non-quantized MixtralExperts to a per-tensor quantized MixtralExperts.
Parameters
----------
src : MixtralExperts
The non-quantized MixtralExperts
config : PerTensorQuantize
The FP8 quantization weight_config.
name : str
The name of the layer.
Returns
-------
ret : MixtralExpertsFP8
The per-tensor quantized MixtralExperts.
"""
quantized_mistral_experts = FP8PerTensorQuantizeMixtralExperts(
num_local_experts=src.num_local_experts,
in_features=src.in_features,
out_features=src.out_features,
config=config,
name=name,
tensor_parallel_shards=src.tensor_parallel_shards,
)
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, f"{shard.name}_q_weight", quantized_mistral_experts.q_weight)
# scale doesn't need to be sharded since it's the same for all shards
return quantized_mistral_experts
def forward(self, x: nn.Tensor, indptr: nn.Tensor) -> nn.Tensor:
w = self.q_weight
if self.config.calibration_mode == "max":
_, x_scale = self.config.quantize_float8(
x,
quantize_dtype=self.config.activation_dtype,
storage_dtype=self.config.activation_dtype,
)
if self.config.tensor_parallel_shards > 1:
x_scale = nn.ccl_allreduce(x_scale, "max")
x_scale = nn.extern(
"mlc_llm.calibration_observer",
[f"{self.name}.q_calibration_scale", "max", x_scale],
out=nn.Tensor.placeholder(x_scale.shape, x_scale.dtype),
)
x_q = (x / x_scale.astype(x.dtype)).astype(self.config.activation_dtype)
x = x_q.astype(self.config.model_dtype) * x_scale.astype(self.config.model_dtype)
if indptr.ndim == 2:
assert indptr.shape[0] == 1
return moe_matmul.dequantize_float8_gemv(
x, w, self.q_scale, indptr, self.config.weight_dtype
)
if extern.get_store().cutlass_group_gemm:
if self.config.calibration_mode == "inference":
if self.q_calibration_scale is not None:
x /= self.q_calibration_scale.astype(x.dtype)
x_q = nn.op.astype(x, dtype=self.config.activation_dtype)
x_scale = self.q_calibration_scale
scale = (
x_scale * self.q_scale
if self.q_scale is not None
else nn.wrap_nested(
relax.Constant(runtime.tensor(np.array([1.0]).astype("float32"))),
"scale",
)
)
return cutlass.group_gemm(
x_q, w, indptr, scale, self.config.weight_dtype, self.config.model_dtype
)
# Note: convert_weight is target agnostic, so a fallback must be provided
w = nn.tensor_expr_op(
self.config.dequantize_float8,
"dequantize",
args=[w, self.q_scale, self.config.weight_dtype],
)
return moe_matmul.group_gemm(x, w, indptr)
ptq.PerTensorQuantizeMixtralExperts._IMPL["fp8"] = FP8PerTensorQuantizeMixtralExperts
@@ -0,0 +1,404 @@
"""The FasterTransformer quantization config"""
from dataclasses import dataclass
from typing import Any, Callable, List, Literal, Optional, Tuple # noqa: UP035
import tvm
from tvm import DataType, DataTypeCode, IRModule, relax, te, tirx
from tvm.relax.frontend import nn
from tvm.runtime import Tensor
from tvm.s_tir import dlight as dl
from tvm.target import Target
from ..loader import QuantizeMapping
from ..op import faster_transformer_dequantize_gemm
from ..support import logging
from ..support.auto_target import detect_cuda_arch_list
from ..support.style import bold
from .group_quantization import (
GroupQuantize,
GroupQuantizeEmbedding,
GroupQuantizeLinear,
)
from .utils import is_final_fc, is_moe_gate
logger = logging.getLogger(__name__)
@dataclass
class FTQuantize:
"""Configuration for FasterTransformer quantization"""
name: str
kind: str
quantize_dtype: Literal["int4", "int8"]
storage_dtype: Literal["int8"]
model_dtype: Literal["float16"]
group_size: Optional[int] = None
num_elem_per_storage: int = 0
max_int_value: int = 0
def fallback_group_quantize(self) -> GroupQuantize:
"""
The fallback group quantization config for other parameters.
Returns
------
quantize: GroupQuantize
The group quantization config to fallback.
"""
return GroupQuantize(
name=self.name,
kind="group-quant",
group_size=32, # hardcoded to 32 as only supporting int4 quantization
quantize_dtype=self.quantize_dtype,
storage_dtype="uint32",
model_dtype=self.model_dtype,
linear_weight_layout="NK",
)
def __post_init__(self):
assert self.kind == "ft-quant"
quantize_dtype = DataType(self.quantize_dtype)
storage_dtype = DataType(self.storage_dtype)
assert self.quantize_dtype in ["int4", "int8"]
assert storage_dtype.type_code == DataTypeCode.INT
assert self.model_dtype == "float16"
assert self.group_size in [None, 64, 128]
if storage_dtype.bits < quantize_dtype.bits:
raise ValueError("Storage unit should be greater or equal to quantized element")
self.num_elem_per_storage = storage_dtype.bits // quantize_dtype.bits
self.max_int_value = (2 ** (quantize_dtype.bits - 1)) - 1
self._quantize_func_cache = {}
def quantize_model(
self,
model: nn.Module,
quant_map: QuantizeMapping,
name_prefix: str,
) -> nn.Module:
"""
Quantize model with FasterTransformer quantization
Parameters
----------
model : nn.Module
The non-quantized nn.Module.
quant_map : QuantizeMapping
The quantize mapping with name mapping and func mapping.
name_prefix : str
The name prefix for visited weight.
Returns
-------
ret : nn.Module
The quantized nn.Module.
"""
class _Mutator(nn.Mutator):
def __init__(self, config: FTQuantize, quant_map: QuantizeMapping) -> None:
super().__init__()
self.config = config
self.quant_map = quant_map
def visit_module(self, name: str, node: nn.Module) -> Any:
"""
The visiting method for FasterTransformer quantization of nn.Module nodes.
Parameters
----------
name : str
The name of the current node.
node : nn.Module
The current node of nn.Module to mutate.
Returns
------
ret_node: Any
The new node to replace current node.
"""
if isinstance(node, nn.Linear):
weight_name = f"{name}.weight"
self.quant_map.param_map[weight_name] = [
f"{name}.q_weight",
f"{name}.q_scale",
]
if (
is_final_fc(name)
or node.out_dtype == "float32"
or (self.config.quantize_dtype == "int4" and node.out_features % 8 != 0)
or (self.config.quantize_dtype == "int8" and node.out_features % 4 != 0)
):
# Under any of the conditions we fall back to GroupQuantize
# For `is_final_fc()` see https://github.com/mlc-ai/mlc-llm/issues/1723
# If simply skipping lm_head quantization degrades performance
# Other requirements are from CUTLASS
logger.info(
'Fallback to GroupQuantize for nn.Linear: "%s", '
+ "weight.shape: %s, out_dtype: %s",
bold(name),
node.weight.shape,
node.out_dtype,
)
group_quantize = self.config.fallback_group_quantize()
self.quant_map.map_func[weight_name] = group_quantize.quantize_weight
return GroupQuantizeLinear.from_linear(node, group_quantize)
if not is_moe_gate(name, node):
self.quant_map.map_func[weight_name] = self.config.quantize_weight
return FTQuantizeLinear.from_linear(node, self.config)
if isinstance(node, nn.Embedding):
weight_name = f"{name}.weight"
self.quant_map.param_map[weight_name] = [
f"{name}.q_weight",
f"{name}.q_scale",
]
group_quantize = self.config.fallback_group_quantize()
self.quant_map.map_func[weight_name] = group_quantize.quantize_weight
return GroupQuantizeEmbedding.from_embedding(node, group_quantize)
return self.visit(name, node)
model.to(dtype=self.model_dtype)
mutator = _Mutator(self, quant_map)
model = mutator.visit(name_prefix, model)
return model
def quantize_weight(self, weight: Tensor) -> List[Tensor]: # noqa: UP006
"""
Quantize weight with FasterTransformer quantization
Parameters
----------
weight : Tensor
The original weight.
Returns
------
ret: List[Tensor]
The list of FasterTransformer quantized weights.
"""
assert tvm.get_global_func("relax.ext.cutlass", True), (
"Cutlass should be enabled in TVM runtime to quantize weight, "
"but not enabled in current TVM runtime environment. "
"To enable Cutlass in TVM runtime, please `set(USE_CUTLASS ON)` "
"in config.cmake when compiling TVM from source"
)
assert len(weight.shape) == 2
device = weight.device
device_type = device._DEVICE_TYPE_TO_NAME[device.dlpack_device_type()]
if device_type == "cuda":
target = Target.current()
if target is None:
target = Target.from_device(device)
with target:
def _create_quantize_func() -> IRModule:
bb = relax.BlockBuilder()
weight_var = relax.Var("weight", relax.TensorType(weight.shape, weight.dtype))
with bb.function(name="main", params=[weight_var]):
with bb.dataflow():
lv0 = bb.emit_te(self._quantize, weight_var)
lv1 = bb.normalize(lv0[0])
lv2 = bb.emit(
relax.call_pure_packed(
"cutlass.ft_preprocess_weight",
lv1,
detect_cuda_arch_list(target=target)[0],
DataType(self.quantize_dtype).bits == 4,
ty_args=lv1.ty,
)
)
gv = bb.emit_output(relax.Tuple([lv2, lv0[1]]))
bb.emit_func_output(gv)
return bb.finalize()
def _compile_quantize_func(mod: IRModule) -> Callable:
mod = dl.ApplyDefaultSchedule(
dl.gpu.Reduction(),
dl.gpu.GeneralReduction(),
dl.gpu.Fallback(),
)(mod)
ex = relax.build(mod, target=target)
vm = relax.VirtualMachine(ex, device)
return vm["main"]
key = str(
(
int(weight.shape[0]),
int(weight.shape[1]),
weight.dtype,
device_type,
)
)
quantize_func = self._quantize_func_cache.get(key, None)
if quantize_func is None:
logger.info("Compiling quantize function for key: %s", key)
quantize_func = _compile_quantize_func(_create_quantize_func())
self._quantize_func_cache[key] = quantize_func
data = quantize_func(weight)
return data
else:
raise NotImplementedError(f"Device type {device_type} is not supported")
def _quantize(
self,
weight: te.Tensor,
) -> Tuple[te.Tensor, te.Tensor]: # noqa: UP006
"""FasterTransformer quantization for weight tensor, defined in tensor expression."""
assert len(weight.shape) == 2
n, k = weight.shape
cur_group_size = k if not self.group_size else self.group_size
scale_shape = (tirx.ceildiv(k, cur_group_size), n)
r = te.reduce_axis((0, cur_group_size), name="r")
max_abs = te.compute(
shape=scale_shape,
fcompute=lambda j, i: te.max(
tirx.if_then_else(
j * cur_group_size + r < k,
te.abs(weight[i, j * cur_group_size + r]),
te.min_value(self.model_dtype),
),
axis=r,
),
name="max_abs_value",
)
max_int = tirx.const(self.max_int_value, self.model_dtype)
scale = te.compute(
scale_shape,
lambda i, j: max_abs[i, j].astype(self.model_dtype) / max_int,
name="scale",
)
# compute scaled weight
quantize_dtype = DataType(self.quantize_dtype)
bin_mask = tirx.const((1 << quantize_dtype.bits) - 1, self.storage_dtype)
scaled_weight = te.compute(
shape=weight.shape,
fcompute=lambda i, j: (
tirx.min(
tirx.max(
tirx.round(weight[i, j] / scale[j // cur_group_size, i]),
-max_int - 1,
),
max_int,
).astype(self.storage_dtype)
& bin_mask
),
)
quantized_weight_shape = (k, tirx.ceildiv(n, self.num_elem_per_storage))
r = te.reduce_axis((0, self.num_elem_per_storage), name="r")
quantized_weight = te.compute(
shape=quantized_weight_shape,
fcompute=lambda j, i: tirx.sum(
tirx.if_then_else(
i * self.num_elem_per_storage + r < n,
scaled_weight[i * self.num_elem_per_storage + r, j]
<< (
r.astype(self.storage_dtype)
* tirx.const(quantize_dtype.bits, self.storage_dtype)
),
tirx.const(0, self.storage_dtype),
),
axis=r,
),
name="weight",
)
return quantized_weight, scale
class FTQuantizeLinear(nn.Module):
"""An nn.Linear module with FasterTransformer quantization"""
def __init__(
self,
in_features: int,
out_features: int,
config: FTQuantize,
bias: bool = True,
out_dtype: Optional[str] = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.out_dtype = out_dtype
self.config = config
cur_group_size = in_features if not config.group_size else config.group_size
self.q_weight = nn.Parameter(
(in_features, tirx.ceildiv(out_features, config.num_elem_per_storage)),
config.storage_dtype,
)
self.q_scale = nn.Parameter(
(tirx.ceildiv(in_features, cur_group_size), out_features), config.model_dtype
)
if bias:
self.bias = nn.Parameter(
(out_features,), config.model_dtype if out_dtype is None else out_dtype
)
else:
self.bias = None
@staticmethod
def from_linear(src: nn.Linear, config: FTQuantize) -> "FTQuantizeLinear":
"""
Converts a non-quantized nn.Linear to a FasterTransformer quantized FTQuantizeLinear
Parameters
----------
src : nn.Linear
The non-quantized nn.Linear.
config : FTQuantize
The FasterTransformer quantization config.
Returns
-------
ret : FTQuantizeLinear
The FasterTransformer quantized FTQuantizeLinear layer.
"""
quantized_linear = FTQuantizeLinear(
in_features=src.in_features,
out_features=src.out_features,
config=config,
bias=getattr(src, "bias", None) is not None,
out_dtype=src.out_dtype,
)
if quantized_linear.bias is not None:
quantized_linear.bias.attrs = src.bias.attrs
return quantized_linear
def forward(self, x: nn.Tensor) -> nn.Tensor:
"""
Forward method for FasterTransformer quantized linear layer.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the FasterTransformer quantized linear layer.
"""
return faster_transformer_dequantize_gemm(
x, self.q_weight, self.q_scale, self.bias, group_size=self.config.group_size
)
def to(self, dtype: Optional[str] = None) -> None:
"""
Override to() such that we do not convert bias if there is an out_dtype.
Otherwise, we might run into dtype mismatch when computing x + self.bias.
"""
self.q_weight.to(dtype=dtype)
self.q_scale.to(dtype=dtype)
if self.bias is not None and self.out_dtype is None:
self.bias.to(dtype=dtype)
if dtype is not None and isinstance(getattr(self, "dtype", None), str):
self.dtype = dtype
@@ -0,0 +1,660 @@
"""The group quantization config"""
from dataclasses import dataclass
from functools import partial
from typing import Any, List, Literal, Optional, Tuple, Union # noqa: UP035
from tvm import DataType, DataTypeCode, IRModule, relax, te, tirx, topi
from tvm.relax.frontend import nn
from tvm.runtime import Tensor
from mlc_llm.loader import QuantizeMapping
from mlc_llm.nn import MixtralExperts
from mlc_llm.support import logging
from .utils import (
apply_sharding,
compile_quantize_func,
convert_uint_to_float,
is_final_fc,
is_moe_gate,
pack_weight,
)
logger = logging.getLogger(__name__)
@dataclass
class GroupQuantize:
"""Configuration for group quantization"""
name: str
kind: str
group_size: int
quantize_dtype: Literal["int3", "int4", "int8"]
storage_dtype: Literal["uint32"]
model_dtype: Literal["float16", "float32", "bfloat16"]
linear_weight_layout: Literal["KN", "NK"]
quantize_embedding: bool = True
quantize_final_fc: bool = True
num_elem_per_storage: int = 0
num_storage_per_group: int = 0
max_int_value: int = 0
tensor_parallel_shards: int = 0
def __post_init__(self):
assert self.kind == "group-quant"
quantize_dtype = DataType(self.quantize_dtype)
storage_dtype = DataType(self.storage_dtype)
model_dtype = DataType(self.model_dtype)
assert quantize_dtype.type_code == DataTypeCode.INT
assert storage_dtype.type_code == DataTypeCode.UINT
assert model_dtype.type_code in (DataTypeCode.FLOAT, DataTypeCode.BFLOAT)
if storage_dtype.bits < quantize_dtype.bits:
raise ValueError("Storage unit should be greater or equal to quantized element")
self.num_elem_per_storage = storage_dtype.bits // quantize_dtype.bits
if self.group_size % self.num_elem_per_storage != 0:
raise ValueError("Group size should be divisible by numbers of elements per storage")
self.num_storage_per_group = self.group_size // self.num_elem_per_storage
self.max_int_value = (2 ** (quantize_dtype.bits - 1)) - 1
self.linear_quant_axis = 0 if self.linear_weight_layout == "KN" else 1
self._quantize_func_cache = {}
def quantize_model(
self,
model: nn.Module,
quant_map: QuantizeMapping,
name_prefix: str,
) -> nn.Module:
"""
Quantize model with group quantization
Parameters
----------
model : nn.Module
The non-quantized nn.Module.
quant_map : QuantizeMapping
The quantize mapping with name mapping and func mapping.
name_prefix : str
The name prefix for visited weight.
Returns
-------
ret : nn.Module
The quantized nn.Module.
"""
class _Mutator(nn.Mutator):
def __init__(self, config: GroupQuantize, quant_map: QuantizeMapping) -> None:
super().__init__()
self.config = config
self.quant_map = quant_map
def visit_module(self, name: str, node: nn.Module) -> Any:
"""
The visiting method for group quantization of nn.Module nodes.
Parameters
----------
name : str
The name of the current node.
node : nn.Module
The current node of nn.Module to mutate.
Returns
------
ret_node: Any
The new node to replace current node.
"""
if getattr(node, "no_quantization", False):
return node
if (
isinstance(node, nn.Linear)
and (not is_final_fc(name) or self.config.quantize_final_fc)
and not is_moe_gate(name, node)
):
weight_name = f"{name}.weight"
self.quant_map.param_map[weight_name] = [
f"{name}.q_weight",
f"{name}.q_scale",
]
self.quant_map.map_func[weight_name] = partial(
self.config.quantize_weight,
output_transpose=self.config.linear_weight_layout == "KN",
)
return GroupQuantizeLinear.from_linear(node, self.config)
if isinstance(node, nn.Embedding) and self.config.quantize_embedding:
weight_name = f"{name}.weight"
self.quant_map.param_map[weight_name] = [
f"{name}.q_weight",
f"{name}.q_scale",
]
self.quant_map.map_func[weight_name] = self.config.quantize_weight
return GroupQuantizeEmbedding.from_embedding(node, self.config)
if isinstance(node, MixtralExperts):
weight_name = f"{name}.weight"
self.quant_map.param_map[weight_name] = [
f"{name}.q_weight",
f"{name}.q_scale",
]
self.quant_map.map_func[weight_name] = self.config.quantize_weight
return GroupQuantizeMixtralExperts.from_mixtral_experts(node, self.config)
return self.visit(name, node)
model.to(dtype=self.model_dtype)
mutator = _Mutator(self, quant_map)
model = mutator.visit(name_prefix, model)
return model
def _dequantize(
self,
weight: te.Tensor,
scale: te.Tensor,
axis: int,
out_shape: Optional[List[tirx.Expr]] = None, # noqa: UP006
):
tir_max_int = tirx.const(self.max_int_value, self.model_dtype)
float_weight = convert_uint_to_float(
weight,
DataType(self.quantize_dtype).bits,
self.num_elem_per_storage,
self.storage_dtype,
self.model_dtype,
axis=axis,
out_shape=out_shape,
)
if out_shape is None:
out_shape = weight.shape
out_shape[axis] *= self.num_elem_per_storage
axis = axis if axis >= 0 else len(out_shape) + axis
return te.compute(
shape=out_shape,
fcompute=lambda *idx: tirx.Mul(
tirx.Sub(
float_weight(*idx),
tir_max_int,
),
scale(*idx[:axis], idx[axis] // self.group_size, *idx[axis + 1 :]),
),
name="dequantize",
)
def quantize_weight(
self, weight: Tensor, axis: int = -1, output_transpose: bool = False
) -> List[Tensor]: # noqa: UP006
"""
Quantize weight with group quantization
Parameters
----------
weight : Tensor
The original weight.
axis : int
The group axis.
output_transpose : bool
Whether to transpose the output quantized weight. Only 2D weight is supported.
Returns
------
ret: List[Tensor]
The list of group quantized weights.
"""
device = weight.device
device_type = device._DEVICE_TYPE_TO_NAME[device.dlpack_device_type()]
axis = axis if axis >= 0 else len(weight.shape) + axis
def _create_quantize_func() -> IRModule:
bb = relax.BlockBuilder()
weight_var = relax.Var("weight", relax.TensorType(weight.shape, weight.dtype))
with bb.function(name="main", params=[weight_var]):
with bb.dataflow():
lv = bb.emit_te(self._quantize, weight_var, axis, output_transpose)
gv = bb.emit_output(lv)
bb.emit_func_output(gv)
return bb.finalize()
key = (
f"({weight.shape}, {weight.dtype}, {device_type}, "
f"axis={axis}, output_transpose={output_transpose})"
)
quantize_func = self._quantize_func_cache.get(key, None)
if quantize_func is None:
logger.info("Compiling quantize function for key: %s", key)
quantize_func = compile_quantize_func(_create_quantize_func(), device=device)
self._quantize_func_cache[key] = quantize_func
return quantize_func(weight)
def _quantize(
self,
weight: te.Tensor,
axis: int = -1,
output_transpose: bool = False,
) -> Tuple[te.Tensor, te.Tensor]: # noqa: UP006
"""Group quantization for weight tensor, defined in tensor expression."""
max_int = tirx.const(self.max_int_value, self.model_dtype)
shape = weight.shape
axis = axis if axis >= 0 else len(shape) + axis
k = shape[axis]
# compute scale per group
r = te.reduce_axis((0, self.group_size), name="r")
num_group = tirx.ceildiv(k, self.group_size)
scale_shape = (*shape[:axis], num_group, *shape[axis + 1 :])
max_abs = te.compute(
shape=scale_shape,
fcompute=lambda *idx: te.max(
tirx.if_then_else(
idx[axis] * self.group_size + r < k,
te.abs(
weight(
*idx[:axis],
idx[axis] * self.group_size + r,
*idx[axis + 1 :],
)
),
te.min_value(self.model_dtype),
),
axis=r,
),
name="max_abs_value",
)
scale = te.compute(
scale_shape,
lambda *idx: max_abs(*idx).astype(self.model_dtype) / max_int,
name="scale",
)
# compute scaled weight
scaled_weight = te.compute(
shape=weight.shape,
fcompute=lambda *idx: tirx.min(
tirx.max(
tirx.round(
weight(*idx)
/ scale(*idx[:axis], idx[axis] // self.group_size, *idx[axis + 1 :])
+ max_int
),
tirx.const(0, self.model_dtype),
),
max_int * 2,
).astype(self.storage_dtype),
)
# compute quantized weight per storage
num_storage = self.num_storage_per_group * num_group
quantized_weight_shape = (*shape[:axis], num_storage, *shape[axis + 1 :])
quantized_weight = pack_weight(
scaled_weight,
axis=axis,
num_elem_per_storage=self.num_elem_per_storage,
weight_dtype=self.quantize_dtype,
storage_dtype=self.storage_dtype,
out_shape=quantized_weight_shape,
)
if output_transpose:
if len(quantized_weight.shape) != 2 or len(scale.shape) != 2:
raise ValueError(
"Does not support transpose output quantized weight with ndim != 2"
)
quantized_weight = topi.transpose(quantized_weight)
scale = topi.transpose(scale)
return quantized_weight, scale
class GroupQuantizeLinear(nn.Module):
"""An nn.Linear module with group quantization"""
def __init__(
self,
in_features: int,
out_features: Union[int, tirx.Var],
config: GroupQuantize,
bias: bool = True,
out_dtype: Optional[str] = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.out_dtype = out_dtype
self.config = config
num_group = tirx.ceildiv(in_features, config.group_size)
num_shards = config.tensor_parallel_shards
if num_shards > 1 and (in_features * num_shards // config.group_size) % num_shards != 0:
raise ValueError(
f"The linear dimension {in_features * num_shards} has "
f"{in_features * num_shards // config.group_size} groups under group size "
f"{config.group_size}. The groups cannot be evenly distributed on "
f"{num_shards} GPUs.\n"
"Possible solutions: reduce number of GPUs, or use quantization with smaller "
"group size."
)
if config.linear_weight_layout == "KN":
self.q_weight = nn.Parameter(
(config.num_storage_per_group * num_group, out_features),
config.storage_dtype,
)
self.q_scale = nn.Parameter((num_group, out_features), config.model_dtype)
else:
self.q_weight = nn.Parameter(
(out_features, config.num_storage_per_group * num_group),
config.storage_dtype,
)
self.q_scale = nn.Parameter((out_features, num_group), config.model_dtype)
if bias:
self.bias = nn.Parameter(
(out_features,), config.model_dtype if out_dtype is None else out_dtype
)
else:
self.bias = None
@staticmethod
def from_linear(src: nn.Linear, config: GroupQuantize) -> "GroupQuantizeLinear":
"""
Converts a non-quantized nn.Linear to a group quantized GroupQuantizeLinear
Parameters
----------
src : nn.Linear
The non-quantized nn.Linear.
config : GroupQuantize
The group quantization config.
Returns
-------
ret : GroupQuantizeLinear
The group quantized GroupQuantizeLinear layer.
"""
# For dynamic shape, src.out_features is `"name"`; src.weight.shape[0] is `tirx.Var("name")`
out_features, in_features = src.weight.shape
quantized_linear = GroupQuantizeLinear(
in_features=in_features,
out_features=out_features,
config=config,
bias=getattr(src, "bias", None) is not None,
out_dtype=src.out_dtype,
)
if quantized_linear.bias is not None:
quantized_linear.bias.attrs = src.bias.attrs
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, f"{shard.name}_q_weight", quantized_linear.q_weight)
apply_sharding(shard, f"{shard.name}_q_scale", quantized_linear.q_scale)
return quantized_linear
def forward(self, x: nn.Tensor) -> nn.Tensor:
"""
Forward method for group quantized linear layer.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the group quantized linear layer.
"""
w = nn.op.tensor_expr_op(
lambda weight, scale: self.config._dequantize(
weight,
scale,
axis=self.config.linear_quant_axis,
out_shape=(
[
(
tirx.IntImm("int64", self.out_features)
if isinstance(self.out_features, int)
else weight.shape[0]
), # Reuse same tirx.Var for symbolic shape (after Exporter)
tirx.IntImm("int64", self.in_features),
]
if self.config.linear_weight_layout == "NK"
else [
tirx.IntImm("int64", self.in_features),
(
tirx.IntImm("int64", self.out_features)
if isinstance(self.out_features, int)
else weight.shape[1]
), # Reuse same tirx.Var for symbolic shape (after Exporter)
]
),
),
name_hint="dequantize",
args=[self.q_weight, self.q_scale],
)
if self.config.linear_weight_layout == "NK":
w = nn.op.permute_dims(w)
x = nn.op.matmul(x, w, out_dtype=self.out_dtype)
if self.bias is not None:
x = x + self.bias
return x
def to(self, dtype: Optional[str] = None) -> None:
"""
Override to() such that we do not convert bias if there is an out_dtype.
Otherwise, we might run into dtype mismatch when computing x + self.bias.
"""
self.q_weight.to(dtype=dtype)
self.q_scale.to(dtype=dtype)
if self.bias is not None and self.out_dtype is None:
self.bias.to(dtype=dtype)
if dtype is not None and isinstance(getattr(self, "dtype", None), str):
self.dtype = dtype
class GroupQuantizeEmbedding(nn.Module):
"""An nn.Embedding module with group quantization"""
def __init__(self, num: Union[int, tirx.Var], dim: int, config: GroupQuantize):
self.num = num
self.dim = dim
self.config = config
num_group = tirx.ceildiv(dim, config.group_size)
self.q_weight = nn.Parameter(
(num, config.num_storage_per_group * num_group), config.storage_dtype
)
self.q_scale = nn.Parameter((num, num_group), config.model_dtype)
@staticmethod
def from_embedding(embedding: nn.Embedding, config: GroupQuantize) -> "GroupQuantizeEmbedding":
"""
Converts a non-quantized nn.Embedding to a group quantized GroupQuantizeEmbedding
Parameters
----------
linear : nn.Embedding
The non-quantized nn.Embedding.
config : GroupQuantize
The group quantization config.
Returns
-------
ret : GroupQuantizeEmbedding
The group quantized GroupQuantizeEmbedding layer.
"""
num, dim = embedding.weight.shape
return GroupQuantizeEmbedding(num, dim, config)
def forward(self, x: nn.Tensor):
"""
Forward method for group quantized embedding layer.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the embedding layer.
"""
w = nn.op.tensor_expr_op(
lambda weight, scale: self.config._dequantize(
weight,
scale,
axis=-1,
out_shape=[
(
tirx.IntImm("int64", self.num)
if isinstance(self.num, int)
else weight.shape[0]
), # Reuse same tirx.Var for symbolic shape (after Exporter)
tirx.IntImm("int64", self.dim),
],
),
name_hint="dequantize",
args=[self.q_weight, self.q_scale],
)
if x.ndim == 1:
return nn.op.take(w, x, axis=0)
return nn.op.reshape(
nn.op.take(w, nn.op.reshape(x, shape=[-1]), axis=0),
shape=[*x.shape, self.dim],
)
def lm_head_forward(self, x: nn.Tensor):
"""The lm_head forwarding, which dequantizes the weight
and multiplies it with the input tensor.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the lm_head layer.
"""
w = nn.op.tensor_expr_op(
lambda weight, scale: self.config._dequantize(
weight,
scale,
axis=-1,
out_shape=[
(
tirx.IntImm("int64", self.num)
if isinstance(self.num, int)
else weight.shape[0]
),
tirx.IntImm("int64", self.dim),
],
),
name_hint="dequantize",
args=[self.q_weight, self.q_scale],
)
w = nn.op.permute_dims(w)
return nn.op.matmul(x, w, out_dtype="float32")
class GroupQuantizeMixtralExperts(nn.Module):
"""An MixtralExperts module with group quantization"""
def __init__(
self,
num_local_experts,
in_features,
out_features,
config: GroupQuantize,
):
self.num_local_experts = num_local_experts
self.in_features = in_features
self.out_features = out_features
self.config = config
num_group = tirx.ceildiv(in_features, config.group_size)
self.q_weight = nn.Parameter(
(num_local_experts, out_features, config.num_storage_per_group * num_group),
config.storage_dtype,
)
self.q_scale = nn.Parameter(
(num_local_experts, out_features, num_group), config.model_dtype
)
self.quantize_dtype = config.quantize_dtype
self.group_size = config.group_size
self.dtype = config.model_dtype
if config.linear_weight_layout == "KN":
raise NotImplementedError("GroupQuantizeMixtralExperts does not support KN layout now.")
@staticmethod
def from_mixtral_experts(
src: "MixtralExperts", config: GroupQuantize
) -> "GroupQuantizeMixtralExperts":
"""
Converts a non-quantized MixtralExperts to a group quantized GroupQuantizeMixtralExperts
Parameters
----------
src : MixtralExperts
The non-quantized MixtralExperts
config : GroupQuantize
The group quantization config.
Returns
-------
ret : GroupQuantizeMixtralExperts
The group quantized GroupQuantizeMixtralExperts layer.
"""
quantized_mistral_experts = GroupQuantizeMixtralExperts(
num_local_experts=src.num_local_experts,
in_features=src.in_features,
out_features=src.out_features,
config=config,
)
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, f"{shard.name}_q_weight", quantized_mistral_experts.q_weight)
apply_sharding(shard, f"{shard.name}_q_scale", quantized_mistral_experts.q_scale)
return quantized_mistral_experts
def forward(self, x: nn.Tensor, indptr: nn.Tensor) -> nn.Tensor:
"""Forward method for group quantized mistral experts.
Parameters
----------
x : nn.Tensor
The input tensor.
indptr: nn.Tensor
The indptr tensor
single_batch_decode: bool
Whether to use single-batch decode
Returns
-------
ret : nn.Tensor
The output tensor for the group quantized mistral experts layer.
"""
from mlc_llm.op import moe_matmul
assert x.ndim == 2
if indptr.ndim == 2: # single-batch
assert indptr.shape[0] == 1
return moe_matmul.dequantize_gemv(
x,
self.q_weight,
self.q_scale,
indptr,
quantize_dtype=self.quantize_dtype,
group_size=self.group_size,
)
assert indptr.ndim == 1
return moe_matmul.dequantize_group_gemm(
x,
self.q_weight,
self.q_scale,
indptr,
quantize_dtype=self.quantize_dtype,
indptr_dtype=indptr.dtype,
group_size=self.group_size,
)
@@ -0,0 +1,157 @@
"""Quantization factory utilities for model quantization."""
from typing import Any, Callable, Dict, Optional, Tuple, Type # noqa: UP035
from tvm.relax.frontend import nn
from mlc_llm.loader import QuantizeMapping
from .awq_quantization import AWQQuantize
from .block_scale_quantization import BlockScaleQuantize
from .ft_quantization import FTQuantize
from .group_quantization import GroupQuantize
from .no_quantization import NoQuantize
from .per_tensor_quantization import PerTensorQuantize
from .quantization import Quantization
FuncQuantization = Callable[[Any, Quantization], Tuple[nn.Module, QuantizeMapping]] # noqa: UP006
def make_quantization_functions(
model_cls: Type[nn.Module], # noqa: UP006
*,
model_ctor: Optional[Callable[[Any], nn.Module]] = None,
supports_group_quant: bool = True,
supports_ft_quant: bool = True,
supports_awq: bool = False,
awq_unsupported_message: Optional[str] = None,
supports_per_tensor: bool = False,
supports_block_scale: bool = False,
set_tensor_parallel_shards: bool = True,
per_tensor_use_shards: bool = True,
) -> Dict[str, FuncQuantization]: # noqa: UP006
"""Create standard quantization function implementations for a model class."""
def _create_model(model_config: Any) -> nn.Module:
if model_ctor is not None:
return model_ctor(model_config)
return model_cls(model_config)
def _no_quant(model_config: Any, quantization: NoQuantize) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
model = _create_model(model_config)
model.to(quantization.model_dtype)
return model, QuantizeMapping({}, {})
def _group_quant(
model_config: Any,
quantization: GroupQuantize,
) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
model = _create_model(model_config)
model.to(quantization.model_dtype)
quant_map = QuantizeMapping({}, {})
if set_tensor_parallel_shards:
if not hasattr(model_config, "tensor_parallel_shards"):
raise AttributeError(
"model_config is missing required "
"attribute 'tensor_parallel_shards' for group quantization"
)
quantization.tensor_parallel_shards = getattr(model_config, "tensor_parallel_shards")
model = quantization.quantize_model(
model,
quant_map,
"",
)
return model, quant_map
def _ft_quant(model_config: Any, quantization: FTQuantize) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
model = _create_model(model_config)
model.to(quantization.model_dtype)
quant_map = QuantizeMapping({}, {})
model = quantization.quantize_model(
model,
quant_map,
"",
)
return model, quant_map
def _awq_quant(
model_config: Any, quantization: AWQQuantize
) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
if awq_unsupported_message is not None:
raise NotImplementedError(awq_unsupported_message)
model = _create_model(model_config)
model.to(quantization.model_dtype)
quant_map = QuantizeMapping({}, {})
model = quantization.quantize_model(
model,
quant_map,
"",
)
return model, quant_map
def _per_tensor_quant(
model_config: Any,
quantization: PerTensorQuantize,
) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
model = _create_model(model_config)
model.to(quantization.model_dtype)
quant_map = QuantizeMapping({}, {})
kwargs = {}
if per_tensor_use_shards:
if not hasattr(model_config, "tensor_parallel_shards"):
raise AttributeError(
"model_config is missing required attribute "
"'tensor_parallel_shards' for per-tensor quantization"
)
kwargs["tensor_parallel_shards"] = getattr(model_config, "tensor_parallel_shards")
model = quantization.quantize_model(
model,
quant_map,
"",
**kwargs,
)
return model, quant_map
def _block_scale_quant(
model_config: Any,
quantization: BlockScaleQuantize,
) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
model = _create_model(model_config)
model.to(quantization.model_dtype)
quant_map = QuantizeMapping({}, {})
model = quantization.quantize_model(model, quant_map, "")
return model, quant_map
quantize_fns: Dict[str, FuncQuantization] = {"no-quant": _no_quant} # noqa: UP006
if supports_group_quant:
quantize_fns["group-quant"] = _group_quant
if supports_ft_quant:
quantize_fns["ft-quant"] = _ft_quant
if supports_awq:
quantize_fns["awq"] = _awq_quant
if supports_per_tensor:
quantize_fns["per-tensor-quant"] = _per_tensor_quant
if supports_block_scale:
quantize_fns["block-scale-quant"] = _block_scale_quant
return quantize_fns
def make_awq_quant(
model_cls: Type[nn.Module], # noqa: UP006
) -> Callable[[Any, AWQQuantize], Tuple[nn.Module, QuantizeMapping]]: # noqa: UP006
"""Create a standard AWQ quantization function for loaders."""
def awq_quant(
model_config: Any, quantization: AWQQuantize
) -> Tuple[nn.Module, QuantizeMapping]: # noqa: UP006
model = model_cls(model_config)
model.to(quantization.model_dtype)
quant_map = QuantizeMapping({}, {})
model = quantization.quantize_model(
model,
quant_map,
"",
)
return model, quant_map
return awq_quant
@@ -0,0 +1,15 @@
"""The no quantization config"""
from dataclasses import dataclass
@dataclass
class NoQuantize:
"""Configuration for no quantization"""
name: str
kind: str
model_dtype: str # "float16", "float32"
def __post_init__(self):
assert self.kind == "no-quant"
@@ -0,0 +1,700 @@
"""The per-tensor quantization config"""
import functools
from collections.abc import Sequence
from dataclasses import dataclass
from typing import Any, ClassVar, Dict, List, Literal, Optional, Tuple, Type, Union # noqa: UP035
import numpy as np
from tvm import DataType, DataTypeCode, IRModule, relax, runtime, te, tirx, topi
from tvm.relax.frontend import nn
from tvm.runtime import Tensor
from mlc_llm.loader import QuantizeMapping
from mlc_llm.nn import MixtralExperts
from mlc_llm.op import cutlass, extern
from mlc_llm.support import logging
from .utils import (
apply_sharding,
compile_quantize_func,
convert_uint_packed_fp8_to_float,
is_final_fc,
is_moe_gate,
pack_weight,
)
logger = logging.getLogger(__name__)
@dataclass
class PerTensorQuantize:
"""Configuration for per-tensor quantization"""
name: str
kind: str
activation_dtype: Literal["float8_e4m3fn", "float8_e5m2"]
weight_dtype: Literal["float8_e4m3fn", "float8_e5m2"]
storage_dtype: Literal["uint32", "float8_e4m3fn", "float8_e5m2"]
model_dtype: Literal["float16"]
quantize_embedding: bool = True
quantize_final_fc: bool = True
quantize_linear: bool = True
num_elem_per_storage: int = 0
max_int_value: int = 0
use_scale: bool = True
# The calibration mode for quantization. If set to "inference", the model is built for
# inference. This should be used after calibration is done.
# If set to "max", the model is built for calibration that computes the scale using max value of
# the activations.
calibration_mode: Literal["inference", "max"] = "inference"
tensor_parallel_shards: int = 1
def __post_init__(self):
assert self.kind == "per-tensor-quant"
self.num_elem_per_storage = (
DataType(self.storage_dtype).bits // DataType(self.weight_dtype).bits
)
self.max_int_value = int(tirx.max_value(self.weight_dtype).value)
self._quantize_func_cache = {}
def quantize_model(
self,
model: nn.Module,
quant_map: QuantizeMapping,
name_prefix: str,
tensor_parallel_shards: int,
) -> nn.Module:
"""
Quantize model with per-tensor quantization
Parameters
----------
model : nn.Module
The non-quantized nn.Module.
quant_map : QuantizeMapping
The quantize mapping with name mapping and func mapping.
name_prefix : str
The name prefix for visited weight.
tensor_parallel_shards : int
The number of tensor parallel shards.
Returns
-------
ret : nn.Module
The quantized nn.Module.
"""
self.tensor_parallel_shards = tensor_parallel_shards
class _Mutator(nn.Mutator):
def __init__(self, config: PerTensorQuantize, quant_map: QuantizeMapping) -> None:
super().__init__()
self.config = config
self.quant_map = quant_map
def visit_module(self, name: str, node: nn.Module) -> Any:
"""
The visiting method for per-tensor quantization of nn.Module nodes.
Parameters
----------
name : str
The name of the current node.
node : nn.Module
The current node of nn.Module to mutate.
Returns
------
ret_node: Any
The new node to replace current node.
"""
weight_name = f"{name}.weight"
param_names = (
[f"{name}.q_weight", f"{name}.q_scale"]
if self.config.use_scale
else [
f"{name}.q_weight",
]
)
if (
isinstance(node, nn.Linear)
and self.config.quantize_linear
and (not is_final_fc(name) or self.config.quantize_final_fc)
and not is_moe_gate(name, node)
):
self.quant_map.param_map[weight_name] = param_names
self.quant_map.map_func[weight_name] = self.config.quantize_weight
op = PerTensorQuantizeLinear.from_linear(node, self.config, name)
elif isinstance(node, nn.Embedding) and self.config.quantize_embedding:
self.quant_map.param_map[weight_name] = param_names
self.quant_map.map_func[weight_name] = self.config.quantize_weight
op = PerTensorQuantizeEmbedding.from_embedding(node, self.config)
elif isinstance(node, MixtralExperts):
self.quant_map.param_map[weight_name] = param_names
self.quant_map.map_func[weight_name] = self.config.quantize_weight
op = PerTensorQuantizeMixtralExperts.from_mixtral_experts(
node, self.config, name
)
else:
return self.visit(name, node)
if hasattr(op, "q_calibration_scale") and op.q_calibration_scale:
# update quant_map for calibration scale
param_name = f"{name}.q_calibration_scale"
old_map_func = self.quant_map.map_func[weight_name]
def map_func(*args, **kwargs):
# placeholder for calibration scale, the actual value will be set after
# calibration.
scale = runtime.empty(
shape=op.q_calibration_scale.shape,
dtype=op.q_calibration_scale.dtype,
)
return [*old_map_func(*args, **kwargs), scale]
self.quant_map.param_map[weight_name].append(param_name)
self.quant_map.map_func[weight_name] = map_func
return op
model.to(dtype=self.model_dtype)
mutator = _Mutator(self, quant_map)
model = mutator.visit(name_prefix, model)
return model
def quantize_weight(self, weight) -> List[Tensor]: # noqa: UP006
"""
Quantize weight with per-tensor quantization.
Parameters
----------
weight : Tensor
The weight to quantize.
Returns
-------
ret : List[Tensor]
The quantized weight and the scale if use_scale is True.
"""
device = weight.device
device_type = device._DEVICE_TYPE_TO_NAME[device.dlpack_device_type()]
def _create_quantize_func() -> IRModule:
if DataType(self.weight_dtype).type_code in [
DataTypeCode.Float8E4M3FN,
DataTypeCode.Float8E5M2,
]:
quantize_func = functools.partial(
self.quantize_float8,
quantize_dtype=self.weight_dtype,
storage_dtype=self.storage_dtype,
)
else:
assert NotImplementedError()
class Quantizer(nn.Module):
"""Quantizer module for per-tensor quantization."""
def main(self, weight: nn.Tensor):
return quantize_func(weight)
mod = Quantizer()
mod, _ = mod.export_tvm(
spec={"main": {"weight": nn.spec.Tensor(weight.shape, weight.dtype)}}
)
return mod
key = f"({weight.shape}, {weight.dtype}, {device_type}"
quantize_func = self._quantize_func_cache.get(key, None)
if quantize_func is None:
logger.info("Compiling quantize function for key: %s", key)
quantize_func = compile_quantize_func(_create_quantize_func(), device)
self._quantize_func_cache[key] = quantize_func
return quantize_func(weight)
def quantize_float8(
self,
tensor: nn.Tensor,
quantize_dtype: str,
storage_dtype: str,
) -> Union[Tuple[nn.Tensor], Tuple[nn.Tensor, nn.Tensor]]: # noqa: UP006
"""Per-tensor quantization for weight tensor, defined in tensor expression."""
if self.use_scale:
# min_scaling_factor taken from TRT-LLM
def _compute_scale(x: te.Tensor) -> te.Tensor:
max_abs = topi.max(topi.abs(x))
min_scaling_factor = tirx.const(
1.0 / (self.max_int_value * 512.0), self.model_dtype
)
scale = topi.maximum(
max_abs.astype(self.model_dtype) / self.max_int_value,
min_scaling_factor,
).astype("float32")
scale = topi.expand_dims(scale, axis=0)
return scale
scale = nn.tensor_expr_op(_compute_scale, "compute_scale", args=[tensor])
else:
scale = None
def _compute_quantized_tensor(weight: te.Tensor, scale: Optional[te.Tensor]) -> te.Tensor:
elem_storage_dtype = (
f"uint{DataType(quantize_dtype).bits}"
if DataType(storage_dtype).type_code == DataTypeCode.UINT
else quantize_dtype
)
scaled_tensor = te.compute(
shape=weight.shape,
fcompute=lambda *idx: tirx.Cast(
self.storage_dtype,
tirx.reinterpret(
elem_storage_dtype,
tirx.Cast(
quantize_dtype,
weight(*idx) / scale(0) if scale is not None else weight(*idx),
),
),
),
)
if quantize_dtype == self.storage_dtype:
return scaled_tensor
packed_weight = pack_weight(
scaled_tensor,
axis=-1,
num_elem_per_storage=self.num_elem_per_storage,
weight_dtype=self.weight_dtype,
storage_dtype=self.storage_dtype,
)
return packed_weight
quantized_tensor = nn.tensor_expr_op(
_compute_quantized_tensor, "compute_quantized_tensor", args=[tensor, scale]
)
if self.use_scale:
return quantized_tensor, scale
return (quantized_tensor,)
def _dequantize(
self,
q_weight: te.Tensor,
scale: Optional[te.Tensor] = None,
out_shape: Optional[Sequence[tirx.Expr]] = None,
) -> te.Tensor:
if self.use_scale:
assert scale is not None
if DataType(self.weight_dtype).type_code in [
DataTypeCode.Float8E4M3FN,
DataTypeCode.Float8E5M2,
]:
return self.dequantize_float8(q_weight, scale, self.weight_dtype, out_shape)
raise NotImplementedError()
def dequantize_float8(
self,
q_tensor: te.Tensor,
scale: Optional[te.Tensor],
quantize_dtype: str,
out_shape: Optional[Sequence[tirx.Expr]] = None,
) -> te.Tensor:
"""Dequantize a fp8 tensor (input or weight) to higher-precision float."""
if quantize_dtype != self.storage_dtype:
dequantized_tensor = convert_uint_packed_fp8_to_float(
q_tensor,
self.num_elem_per_storage,
self.storage_dtype,
self.model_dtype,
quantize_dtype,
axis=-1,
out_shape=out_shape,
)
else:
dequantized_tensor = q_tensor.astype(self.model_dtype)
if scale is not None:
dequantized_tensor = dequantized_tensor * scale.astype(dequantized_tensor.dtype)
return dequantized_tensor
class PerTensorQuantizeLinear(nn.Module):
"""An nn.Linear module with per-tensor quantization."""
def __init__(
self,
in_features: int,
out_features: Union[int, tirx.Var],
config: PerTensorQuantize,
name: str,
bias: bool = True,
out_dtype: Optional[str] = None,
) -> None:
super().__init__()
self.in_features = in_features
self.out_features = out_features
self.out_dtype = out_dtype or config.model_dtype
self.config = config
self.name = name
self.q_weight = nn.Parameter(
(out_features, tirx.ceildiv(in_features, config.num_elem_per_storage)),
config.storage_dtype,
)
self.q_calibration_scale = None
if config.use_scale:
self.q_scale = nn.Parameter((1,), "float32")
if config.calibration_mode == "inference":
self.q_calibration_scale = nn.Parameter((1,), "float32")
else:
self.q_scale = None
if bias:
self.bias = nn.Parameter(
(out_features,), config.model_dtype if out_dtype is None else out_dtype
)
else:
self.bias = None
@classmethod
def from_linear(
cls, src: nn.Linear, config: PerTensorQuantize, name: str
) -> "PerTensorQuantizeLinear":
"""
Converts a non-quantized nn.Linear to a per-tensor quantized PerTensorQuantizeLinear
Parameters
----------
src : nn.Linear
The non-quantized nn.Linear.
config : PerTensorQuantize
The per-tensor quantization config.
name: str
The name of the layer.
Returns
-------
ret : PerTensorQuantizeLinear
The per-tensor quantized PerTensorQuantizeLinear layer.
"""
out_features, in_features = src.weight.shape
quantized_linear = cls(
in_features=in_features,
out_features=out_features,
config=config,
name=name,
bias=getattr(src, "bias", None) is not None,
out_dtype=src.out_dtype,
)
if quantized_linear.bias is not None:
quantized_linear.bias.attrs = src.bias.attrs
if "shard_strategy" in src.weight.attrs:
shard = src.weight.attrs["shard_strategy"]
apply_sharding(shard, f"{shard.name}_q_weight", quantized_linear.q_weight)
# scale doesn't need to be sharded since it's the same for all shards
return quantized_linear
def forward(self, x: nn.Tensor) -> nn.Tensor:
"""
Forward method for per-tensor quantized linear layer.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the per-tensor quantized linear layer.
"""
# Note: Use calibration scale when calibration is enabled
if self.config.calibration_mode == "inference":
if self.q_calibration_scale:
x /= self.q_calibration_scale.astype(x.dtype)
x_q = x.astype(self.config.activation_dtype)
x_scale = self.q_calibration_scale
elif self.config.calibration_mode == "max":
_, x_scale = self.config.quantize_float8(
x,
quantize_dtype=self.config.activation_dtype,
storage_dtype=self.config.storage_dtype,
)
if self.config.tensor_parallel_shards > 1:
x_scale = nn.ccl_allreduce(x_scale, "max")
x_scale = nn.extern(
"mlc_llm.calibration_observer",
[f"{self.name}.q_calibration_scale", "max", x_scale],
out=nn.Tensor.placeholder(x_scale.shape, x_scale.dtype),
)
x_q = (x / x_scale.astype(x.dtype)).astype(self.config.activation_dtype)
x = x_q.astype(self.config.model_dtype) * x_scale.astype(self.config.model_dtype)
else:
raise ValueError(f"Unknown calibration mode: {self.config.calibration_mode}")
if (
self.config.weight_dtype == self.config.storage_dtype
and self.config.calibration_mode == "inference"
):
if (
extern.get_store().cutlass_gemm
and functools.reduce(lambda x, y: x * y, x_q.shape[:-1]) != 1
):
# Dispatch to cutlass kernel for gemm when cutlass is available.
scale = (
x_scale * self.q_scale
if self.config.use_scale
else nn.wrap_nested(
relax.Constant(runtime.tensor(np.array([1.0]).astype("float32"))),
"scale",
)
)
return cutlass.fp8_gemm(
x_q,
self.q_weight,
scale,
self.config.weight_dtype,
self.config.model_dtype,
)
x = nn.op.matmul(x_q, nn.permute_dims(self.q_weight), out_dtype="float32")
if self.config.use_scale:
scale = x_scale * self.q_scale
x = x * scale
x = x.astype(self.out_dtype)
else:
w = nn.op.tensor_expr_op(
lambda weight, scale: self.config._dequantize(
weight,
scale,
out_shape=[
(
tirx.IntImm("int64", self.out_features)
if isinstance(self.out_features, int)
else weight.shape[0]
),
tirx.IntImm("int64", self.in_features),
],
),
"dequantize",
args=[self.q_weight, self.q_scale],
)
x = nn.op.matmul(x, nn.permute_dims(w), out_dtype=self.out_dtype)
if self.bias is not None:
x = x + self.bias
return x
def to(self, dtype: Optional[str] = None) -> None:
"""
Override to() such that we do not convert bias if there is an out_dtype.
Otherwise, we might run into dtype mismatch when computing x + self.bias.
"""
self.q_weight.to(dtype=dtype)
if self.q_scale:
self.q_scale.to(dtype=dtype)
if self.bias is not None and self.out_dtype is None:
self.bias.to(dtype=dtype)
if dtype is not None and isinstance(getattr(self, "dtype", None), str):
self.dtype = dtype
class PerTensorQuantizeEmbedding(nn.Module):
"""An nn.Embedding module with group quantization"""
def __init__(self, num: Union[int, tirx.Var], dim: int, config: PerTensorQuantize):
self.num = num
self.dim = dim
self.config = config
self.q_weight = nn.Parameter(
(num, tirx.ceildiv(dim, config.num_elem_per_storage)), config.storage_dtype
)
if self.config.use_scale:
self.q_scale = nn.Parameter((1,), "float32")
else:
self.q_scale = None
@staticmethod
def from_embedding(
embedding: nn.Embedding, config: PerTensorQuantize
) -> "PerTensorQuantizeEmbedding":
"""
Converts a non-quantized nn.Embedding to a per-tensor quantized PerTensorQuantizeEmbedding
Parameters
----------
linear : nn.Embedding
The non-quantized nn.Embedding.
config : PerTensorQuantize
The per-tensor quantization config.
Returns
-------
ret : PerTensorQuantizeEmbedding
The per-tensor quantized embedding layer.
"""
num, dim = embedding.weight.shape
return PerTensorQuantizeEmbedding(num, dim, config)
def forward(self, x: nn.Tensor):
"""
Forward method for per-tensor quantized embedding layer.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the embedding layer.
"""
w = nn.op.tensor_expr_op(
lambda weight, scale: self.config._dequantize(
weight,
scale,
out_shape=[
(
tirx.IntImm("int64", self.num)
if isinstance(self.num, int)
else weight.shape[0]
),
tirx.IntImm("int64", self.dim),
],
),
"dequantize",
args=[self.q_weight, self.q_scale],
)
if x.ndim == 1:
return nn.op.take(w, x, axis=0)
return nn.op.reshape(
nn.op.take(w, nn.op.reshape(x, shape=[-1]), axis=0),
shape=[*x.shape, self.dim],
)
def lm_head_forward(self, x: nn.Tensor):
"""The lm_head forwarding, which dequantizes the weight
and multiplies it with the input tensor.
Parameters
----------
x : nn.Tensor
The input tensor.
Returns
-------
ret : nn.Tensor
The output tensor for the lm_head layer.
"""
w = nn.op.tensor_expr_op(
lambda weight, scale: self.config._dequantize(
weight,
scale,
out_shape=[
(
tirx.IntImm("int64", self.num)
if isinstance(self.num, int)
else weight.shape[0]
),
tirx.IntImm("int64", self.dim),
],
),
"dequantize",
args=[self.q_weight, self.q_scale],
)
w = nn.op.permute_dims(w)
return nn.op.matmul(x, w, out_dtype="float32")
class PerTensorQuantizeMixtralExperts(nn.Module):
"""An MixtralExperts module with group quantization"""
_IMPL: ClassVar[Dict[str, Type["PerTensorQuantizeMixtralExperts"]]] = {} # noqa: UP006
def __init__(
self,
num_local_experts,
in_features,
out_features,
config: PerTensorQuantize,
name: str,
):
self.num_local_experts = num_local_experts
self.in_features = in_features
self.out_features = out_features
self.config = config
self.name = name
self.q_weight = nn.Parameter(
(
num_local_experts,
out_features,
tirx.ceildiv(in_features, config.num_elem_per_storage),
),
config.storage_dtype,
)
self.q_calibration_scale = None
if config.use_scale:
self.q_scale = nn.Parameter((1,), "float32")
if config.calibration_mode == "inference":
self.q_calibration_scale = nn.Parameter((1,), "float32")
else:
self.q_scale = None
@staticmethod
def from_mixtral_experts(
src: "MixtralExperts",
config: PerTensorQuantize,
name: str,
) -> "PerTensorQuantizeMixtralExperts":
"""
Converts a non-quantized MixtralExperts to a per-tensor quantized
PerTensorQuantizeMixtralExperts
Parameters
----------
src : MixtralExperts
The non-quantized MixtralExperts
config : PerTensorQuantize
The per-tensor quantization config
name: str
The name of the layer.
Returns
-------
ret : PerTensorQuantizeMixtralExperts
The per-tensor quantized MixtralExperts layer
"""
if DataType(config.weight_dtype).type_code in [
DataTypeCode.Float8E4M3FN,
DataTypeCode.Float8E5M2,
]:
return PerTensorQuantizeMixtralExperts._IMPL["fp8"].from_mixtral_experts(
src, config, name
)
raise NotImplementedError()
def forward(self, x: nn.Tensor, indptr: nn.Tensor) -> nn.Tensor:
"""Forward method for per-tensor quantized mistral experts.
Parameters
----------
x : nn.Tensor
The input tensor.
indptr: nn.Tensor
The indptr tensor
Returns
-------
ret : nn.Tensor
The output tensor for the per-tensor quantized mistral experts layer.
"""
raise NotImplementedError()
+201
View File
@@ -0,0 +1,201 @@
"""A centralized registry of all existing quantization methods and their configurations."""
from typing import Any, Dict # noqa: UP035
from .awq_quantization import AWQQuantize
from .block_scale_quantization import BlockScaleQuantize
from .ft_quantization import FTQuantize
from .group_quantization import GroupQuantize
from .no_quantization import NoQuantize
from .per_tensor_quantization import PerTensorQuantize
Quantization = Any
"""Quantization is an object that represents an quantization algorithm. It is required to
have the following fields:
name : str
The name of the quantization algorithm, for example, "q4f16_1".
kind : str
The kind of quantization algorithm, for example, "group-quant", "faster-transformer".
It is also required to have the following method:
def quantize_model(self, module: nn.Module) -> nn.Module:
...
def quantize_weight(self, weight: tvm.runtime.Tensor) -> List[tvm.runtime.Tensor]:
...
"""
QUANTIZATION: Dict[str, Quantization] = { # noqa: UP006
"q0f16": NoQuantize(
name="q0f16",
kind="no-quant",
model_dtype="float16",
),
"q0bf16": NoQuantize(
name="q0bf16",
kind="no-quant",
model_dtype="bfloat16",
),
"q0f32": NoQuantize(
name="q0f32",
kind="no-quant",
model_dtype="float32",
),
"q3f16_0": GroupQuantize(
name="q3f16_0",
kind="group-quant",
group_size=40,
quantize_dtype="int3",
storage_dtype="uint32",
model_dtype="float16",
linear_weight_layout="KN",
quantize_embedding=True,
quantize_final_fc=True,
),
"q3f16_1": GroupQuantize(
name="q3f16_1",
kind="group-quant",
group_size=40,
quantize_dtype="int3",
storage_dtype="uint32",
model_dtype="float16",
linear_weight_layout="NK",
quantize_embedding=True,
quantize_final_fc=True,
),
"q4f16_0": GroupQuantize(
name="q4f16_0",
kind="group-quant",
group_size=32,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="float16",
linear_weight_layout="KN",
quantize_embedding=True,
quantize_final_fc=True,
),
"q4f16_1": GroupQuantize(
name="q4f16_1",
kind="group-quant",
group_size=32,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="float16",
linear_weight_layout="NK",
quantize_embedding=True,
quantize_final_fc=True,
),
"q4bf16_0": GroupQuantize(
name="q4bf16_0",
kind="group-quant",
group_size=32,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="bfloat16",
linear_weight_layout="KN",
quantize_embedding=True,
quantize_final_fc=True,
),
"q4bf16_1": GroupQuantize(
name="q4bf16_1",
kind="group-quant",
group_size=32,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="bfloat16",
linear_weight_layout="NK",
quantize_embedding=True,
quantize_final_fc=True,
),
"q4f32_1": GroupQuantize(
name="q4f32_1",
kind="group-quant",
group_size=32,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="float32",
linear_weight_layout="NK",
quantize_embedding=True,
quantize_final_fc=True,
),
"q4f16_2": GroupQuantize(
name="q4f16_2",
kind="group-quant",
group_size=32,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="float16",
linear_weight_layout="NK",
quantize_embedding=False,
quantize_final_fc=False,
),
"q4f16_autoawq": AWQQuantize(
name="q4f16_autoawq",
kind="awq",
group_size=128,
quantize_dtype="int4",
storage_dtype="uint32",
model_dtype="float16",
),
"q4f16_ft": FTQuantize(
name="q4f16_ft",
kind="ft-quant",
quantize_dtype="int4",
storage_dtype="int8",
model_dtype="float16",
),
"e5m2_e5m2_f16": PerTensorQuantize(
name="e5m2_e5m2_f16",
kind="per-tensor-quant",
activation_dtype="float8_e5m2",
weight_dtype="float8_e5m2",
storage_dtype="float8_e5m2",
model_dtype="float16",
quantize_final_fc=False,
quantize_embedding=False,
quantize_linear=True,
use_scale=False,
),
"e4m3_e4m3_f16": PerTensorQuantize(
name="e4m3_e4m3_f16",
kind="per-tensor-quant",
activation_dtype="float8_e4m3fn",
weight_dtype="float8_e4m3fn",
storage_dtype="float8_e4m3fn",
model_dtype="float16",
quantize_final_fc=False,
quantize_embedding=False,
quantize_linear=True,
use_scale=True,
calibration_mode="inference",
),
"e4m3_e4m3_f16_max_calibrate": PerTensorQuantize(
name="e4m3_e4m3_f16_max_calibrate",
kind="per-tensor-quant",
activation_dtype="float8_e4m3fn",
weight_dtype="float8_e4m3fn",
storage_dtype="float8_e4m3fn",
model_dtype="float16",
quantize_final_fc=False,
quantize_embedding=False,
quantize_linear=True,
use_scale=True,
calibration_mode="max",
),
"fp8_e4m3fn_bf16_block_scale": BlockScaleQuantize(
name="fp8_e4m3fn_bf16_block_scale",
kind="block-scale-quant",
weight_dtype="float8_e4m3fn",
model_dtype="bfloat16",
),
"fp8_e4m3fn_bf16_block_scale_static_activation": BlockScaleQuantize(
name="fp8_e4m3fn_bf16_block_scale_static_activation",
kind="block-scale-quant",
weight_dtype="float8_e4m3fn",
model_dtype="bfloat16",
use_activation_scale=True,
),
}
+188
View File
@@ -0,0 +1,188 @@
"""Common utilities for quantization"""
from collections.abc import Sequence
from typing import Callable, List, Optional # noqa: UP035
from tvm import IRModule, relax, te, tirx
from tvm.relax.frontend import nn
from tvm.runtime import DataType, DataTypeCode
from tvm.s_tir import dlight as dl
from tvm.target import Target
from mlc_llm.support import tensor_parallel as tp
def convert_uint_to_float(
weight: te.Tensor,
bits: int,
num_elem_per_storage: int,
storage_dtype: str,
model_dtype: str,
axis: int = -1,
out_shape: Optional[List[tirx.Expr]] = None, # noqa: UP006
ft_reorder: Optional[bool] = False,
) -> te.Tensor:
"""Convert a quantized uint weight to an unquantized float weight."""
tir_bin_mask = tirx.const((1 << bits) - 1, storage_dtype)
if out_shape is None:
out_shape = weight.shape
out_shape[axis] *= num_elem_per_storage
axis = axis if axis >= 0 else len(out_shape) + axis
return te.compute(
shape=out_shape,
fcompute=lambda *idx: tirx.bitwise_and(
tirx.shift_right(
weight(*idx[:axis], idx[axis] // num_elem_per_storage, *idx[axis + 1 :]),
(
(
(idx[axis] % num_elem_per_storage) % 2 * 4
+ (idx[axis] % num_elem_per_storage) // 2
)
* bits
if ft_reorder
else (idx[axis] % num_elem_per_storage) * bits
).astype(storage_dtype),
),
tir_bin_mask,
).astype(model_dtype),
)
def is_final_fc(name: str) -> bool:
"""Determines whether the parameter is the last layer based on its name."""
# TODO: use more specious condition to determine final fc
return name in ["head", "lm_head", "lm_head.linear", "embed_out"]
def is_moe_gate(name: str, node: nn.Linear) -> bool:
"""Check whether the parameter is the MoE gate layer."""
return name.endswith("gate") and isinstance(node.out_features, int) and node.out_features <= 256
def compile_quantize_func(mod: IRModule, device) -> Callable:
"""Compile a quantization function for a given device."""
device_type = device._DEVICE_TYPE_TO_NAME[device.dlpack_device_type()]
if device_type in ["cuda", "rocm", "metal", "vulkan", "opencl"]:
target = Target.current()
if target is None:
target = Target.from_device(device)
with target:
mod = dl.ApplyDefaultSchedule(
dl.gpu.Reduction(),
dl.gpu.GeneralReduction(),
dl.gpu.Fallback(),
)(mod)
elif device_type == "cpu":
target = "llvm"
mod = relax.transform.LegalizeOps()(mod)
else:
raise NotImplementedError(f"Device type {device_type} is not supported")
ex = relax.build(mod, target=target)
vm = relax.VirtualMachine(ex, device)
return vm["main"]
def apply_sharding(shard_strategy, name: str, weight: nn.Parameter):
"""Apply sharding strategy to a weight."""
if isinstance(shard_strategy, tp.ShardSingleDim):
weight.attrs["shard_strategy"] = tp.ShardSingleDim(
name=name,
dim=shard_strategy.dim,
segs=shard_strategy.segs,
)
else:
raise NotImplementedError(f"Unknowing sharding strategy: {shard_strategy}")
def convert_uint_packed_fp8_to_float(
weight: te.Tensor,
num_elem_per_storage: int,
storage_dtype: str,
model_dtype: str,
quant_dtype: str,
axis: int = -1,
out_shape: Optional[Sequence[tirx.Expr]] = None,
) -> te.Tensor:
"""Unpack a fp8 value from the storage dtype and convert to float."""
assert quant_dtype in ["float8_e4m3fn", "float8_e5m2"]
assert DataType(storage_dtype).type_code == DataTypeCode.UINT
bits = DataType(quant_dtype).bits
elem_storage_dtype = DataType(f"uint{bits}")
tir_bin_mask = tirx.const((1 << bits) - 1, "uint8")
if axis < 0:
axis += len(weight.shape)
if out_shape is None:
out_shape = (
*weight.shape[:axis],
weight.shape[axis] * num_elem_per_storage,
*weight.shape[axis + 1 :],
)
axis = axis if axis >= 0 else len(out_shape) + axis
return te.compute(
shape=out_shape,
fcompute=lambda *idx: tirx.reinterpret(
quant_dtype,
tirx.bitwise_and(
tirx.shift_right(
weight(*idx[:axis], idx[axis] // num_elem_per_storage, *idx[axis + 1 :]),
((idx[axis] % num_elem_per_storage) * bits).astype(storage_dtype),
).astype(elem_storage_dtype),
tir_bin_mask,
),
).astype(model_dtype),
)
def pack_weight(
weight: te.Tensor,
axis: int,
num_elem_per_storage: int,
weight_dtype: str,
storage_dtype: str,
out_shape: Optional[Sequence[tirx.Expr]] = None,
):
"""Convert a tensor to a packed format by packing consecutive bits.
This can be useful for sub-byte quantization.
Parameters
----------
weight : te.Tensor
The weight
axis : int
The axis to pack.
num_elem_per_storage : int
The number of elements per storage.
weight_dtype : str
The dtype of the input tensor.
storage_dtype : str
The dtype of the packed tensor.
out_shape : Optional[Sequence[tirx.Expr]]
The output shape of the packed tensor. Zero-padding is added if needed.
"""
assert weight.dtype == storage_dtype
shape = weight.shape
if axis < 0:
axis += len(shape)
k = shape[axis]
axis = axis if axis >= 0 else len(shape) + axis
if out_shape is None:
out_shape = (
*shape[:axis],
tirx.ceildiv(k, num_elem_per_storage),
*shape[axis + 1 :],
)
r = te.reduce_axis((0, num_elem_per_storage), name="r")
packed_weight = te.compute(
shape=out_shape,
fcompute=lambda *idx: tirx.sum(
tirx.if_then_else(
idx[axis] * num_elem_per_storage + r < k,
weight(*idx[:axis], idx[axis] * num_elem_per_storage + r, *idx[axis + 1 :])
<< (r * DataType(weight_dtype).bits),
tirx.const(0, storage_dtype),
),
axis=r,
),
name="packed_weight",
).astype(storage_dtype)
return packed_weight