quantbench-engine 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantbench/__init__.py +76 -0
- quantbench/bench/__init__.py +22 -0
- quantbench/bench/pareto.py +182 -0
- quantbench/cli.py +301 -0
- quantbench/eval/__init__.py +27 -0
- quantbench/eval/perplexity.py +143 -0
- quantbench/eval/sqnr.py +66 -0
- quantbench/nn/__init__.py +9 -0
- quantbench/nn/model_convert.py +109 -0
- quantbench/nn/quant_linear.py +296 -0
- quantbench/quant/__init__.py +58 -0
- quantbench/quant/affine.py +188 -0
- quantbench/quant/algorithms/__init__.py +41 -0
- quantbench/quant/algorithms/awq.py +184 -0
- quantbench/quant/algorithms/gptq.py +181 -0
- quantbench/quant/algorithms/smoothquant.py +132 -0
- quantbench/quant/blockwise.py +115 -0
- quantbench/quant/fp8.py +166 -0
- quantbench/quant/packing/__init__.py +27 -0
- quantbench/quant/packing/int4_pack.py +169 -0
- quantbench/quant/packing/sparse_pack.py +170 -0
- quantbench/quant/packing/ternary_pack.py +193 -0
- quantbench_engine-0.1.0.dist-info/METADATA +276 -0
- quantbench_engine-0.1.0.dist-info/RECORD +27 -0
- quantbench_engine-0.1.0.dist-info/WHEEL +4 -0
- quantbench_engine-0.1.0.dist-info/entry_points.txt +2 -0
- quantbench_engine-0.1.0.dist-info/licenses/LICENSE +190 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Copyright 2026 Prajwal7-2002
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""SmoothQuant: Accurate and Efficient Post-Training Quantization for Large Language Models.
|
|
16
|
+
|
|
17
|
+
Mathematical Foundation:
|
|
18
|
+
LLM activations exhibit severe outlier channels whose magnitudes can be 100x
|
|
19
|
+
larger than typical channels, making activation quantization to INT8 prone to
|
|
20
|
+
catastrophic precision loss under per-tensor or per-token absmax scaling.
|
|
21
|
+
Conversely, weights exhibit smooth channel distributions and are easy to quantize.
|
|
22
|
+
|
|
23
|
+
SmoothQuant introduces a mathematically invariant per-channel migration scale s:
|
|
24
|
+
Y = X * W = (X * diag(s)^-1) * (diag(s) * W) = X_hat * W_hat
|
|
25
|
+
|
|
26
|
+
The scale vector s in R^C is chosen to balance the maximum dynamic ranges:
|
|
27
|
+
s_j = (max(|X_j|)^alpha) / (max(|W_j|)^(1 - alpha))
|
|
28
|
+
|
|
29
|
+
where alpha in [0, 1] controls migration strength:
|
|
30
|
+
- alpha = 0.5: Symmetrically divides difficulty between activations and weights.
|
|
31
|
+
- alpha = 1.0: Migrates all difficulty to the weights (activations become easy to quantize).
|
|
32
|
+
- alpha = 0.0: Preserves weights completely (no difficulty migration).
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
from typing import Optional, Tuple
|
|
38
|
+
|
|
39
|
+
import torch
|
|
40
|
+
import torch.nn as nn
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compute_smoothquant_scale(
|
|
44
|
+
act_max: torch.Tensor,
|
|
45
|
+
weight_max: torch.Tensor,
|
|
46
|
+
alpha: float = 0.5,
|
|
47
|
+
eps: float = 1e-5,
|
|
48
|
+
) -> torch.Tensor:
|
|
49
|
+
"""Compute the per-channel SmoothQuant migration scale vector.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
act_max: Tensor of shape (C,) representing the maximum absolute activation
|
|
53
|
+
magnitude per input channel across calibration tokens.
|
|
54
|
+
weight_max: Tensor of shape (C,) representing the maximum absolute weight
|
|
55
|
+
magnitude per input channel: max_i |W_{i, j}|.
|
|
56
|
+
alpha: Migration strength hyperparameter in [0.0, 1.0].
|
|
57
|
+
eps: Small epsilon to prevent division by zero or degenerate scales.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Tensor of shape (C,) containing per-channel migration scales s_j.
|
|
61
|
+
"""
|
|
62
|
+
if not (0.0 <= alpha <= 1.0):
|
|
63
|
+
raise ValueError(f"alpha must be in [0.0, 1.0], got {alpha}")
|
|
64
|
+
|
|
65
|
+
act_max = act_max.clamp(min=eps)
|
|
66
|
+
weight_max = weight_max.clamp(min=eps)
|
|
67
|
+
|
|
68
|
+
# s_j = (s_X^alpha) / (s_W^(1 - alpha))
|
|
69
|
+
numerator = act_max.pow(alpha)
|
|
70
|
+
denominator = weight_max.pow(1.0 - alpha)
|
|
71
|
+
scale = (numerator / denominator).clamp(min=eps)
|
|
72
|
+
return scale
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def extract_activation_channel_scales(
|
|
76
|
+
module: nn.Module,
|
|
77
|
+
calibration_inputs: list[torch.Tensor] | torch.Tensor,
|
|
78
|
+
) -> torch.Tensor:
|
|
79
|
+
"""Profile activation outlier channel scales across calibration data.
|
|
80
|
+
|
|
81
|
+
Args:
|
|
82
|
+
module: Neural network module (e.g. nn.Linear).
|
|
83
|
+
calibration_inputs: A tensor or list of tensors of shape (B, S, C_in) or (N, C_in).
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Tensor of shape (C_in,) containing per-channel max absolute activation values.
|
|
87
|
+
"""
|
|
88
|
+
if isinstance(calibration_inputs, torch.Tensor):
|
|
89
|
+
calibration_inputs = [calibration_inputs]
|
|
90
|
+
|
|
91
|
+
channel_max: Optional[torch.Tensor] = None
|
|
92
|
+
|
|
93
|
+
for batch in calibration_inputs:
|
|
94
|
+
# Reshape to (N_tokens, C_in)
|
|
95
|
+
flat = batch.detach().view(-1, batch.shape[-1]).abs()
|
|
96
|
+
batch_max = flat.max(dim=0).values
|
|
97
|
+
if channel_max is None:
|
|
98
|
+
channel_max = batch_max
|
|
99
|
+
else:
|
|
100
|
+
channel_max = torch.maximum(channel_max, batch_max)
|
|
101
|
+
|
|
102
|
+
if channel_max is None:
|
|
103
|
+
raise ValueError("calibration_inputs cannot be empty.")
|
|
104
|
+
|
|
105
|
+
return channel_max
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def smooth_linear_weights(
|
|
109
|
+
weight: torch.Tensor,
|
|
110
|
+
act_scales: torch.Tensor,
|
|
111
|
+
alpha: float = 0.5,
|
|
112
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
113
|
+
"""Apply SmoothQuant per-channel migration to weight tensor.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
weight: Float tensor of shape (out_features, in_features).
|
|
117
|
+
act_scales: Per-channel activation max tensor of shape (in_features,).
|
|
118
|
+
alpha: Migration factor in [0.0, 1.0].
|
|
119
|
+
|
|
120
|
+
Returns:
|
|
121
|
+
Tuple of (smoothed_weight, migration_scale):
|
|
122
|
+
- smoothed_weight: W * diag(s) of shape (out_features, in_features).
|
|
123
|
+
- migration_scale: s of shape (in_features,).
|
|
124
|
+
"""
|
|
125
|
+
# W shape: (out_features, in_features)
|
|
126
|
+
# Channel max across output rows: max_i |W_{i, j}| -> shape (in_features,)
|
|
127
|
+
weight_max = weight.abs().max(dim=0).values
|
|
128
|
+
scale = compute_smoothquant_scale(act_scales, weight_max, alpha=alpha)
|
|
129
|
+
|
|
130
|
+
# W_hat = W * diag(s) -> broadcasting along dim 1
|
|
131
|
+
smoothed_weight = weight * scale.unsqueeze(0)
|
|
132
|
+
return smoothed_weight, scale
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""Group-wise (Block-wise) Quantization Primitives.
|
|
2
|
+
|
|
3
|
+
Partitions tensors into independent contiguous blocks of size G (e.g. 32, 64, 128)
|
|
4
|
+
to isolate channel outliers and preserve precision across wide transformer layers.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Any, Dict, Optional, Tuple
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
from quantbench.quant.affine import (
|
|
12
|
+
dequantize_absmax,
|
|
13
|
+
dequantize_affine,
|
|
14
|
+
quantize_absmax,
|
|
15
|
+
quantize_affine,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def quantize_blockwise(
|
|
20
|
+
x: torch.Tensor,
|
|
21
|
+
group_size: int = 128,
|
|
22
|
+
num_bits: int = 4,
|
|
23
|
+
symmetric: bool = True,
|
|
24
|
+
signed: bool = False,
|
|
25
|
+
) -> Tuple[torch.Tensor, torch.Tensor, Optional[torch.Tensor], Dict[str, Any]]:
|
|
26
|
+
"""Quantize a tensor into independent group-wise blocks.
|
|
27
|
+
|
|
28
|
+
Divides flattened tensor into blocks of size `group_size`. If the tensor element
|
|
29
|
+
count is not an exact multiple of `group_size`, pads with zeros.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
x: Input tensor of arbitrary shape.
|
|
33
|
+
group_size: Number of consecutive elements per block (32, 64, 128).
|
|
34
|
+
num_bits: Bit precision per element (e.g. 4 or 8).
|
|
35
|
+
symmetric: If True, uses symmetric AbsMax per block (zero_point = 0).
|
|
36
|
+
If False, uses asymmetric affine (scale and zero_point).
|
|
37
|
+
signed: Whether integer values are signed (only used when symmetric=False).
|
|
38
|
+
|
|
39
|
+
Returns:
|
|
40
|
+
Tuple of (q_blocks, scales, zero_points, metadata_dict).
|
|
41
|
+
"""
|
|
42
|
+
orig_shape = tuple(x.shape)
|
|
43
|
+
flat_x = x.contiguous().view(-1)
|
|
44
|
+
n_elements = flat_x.numel()
|
|
45
|
+
|
|
46
|
+
# Calculate padding if necessary
|
|
47
|
+
remainder = n_elements % group_size
|
|
48
|
+
pad_len = (group_size - remainder) % group_size
|
|
49
|
+
if pad_len > 0:
|
|
50
|
+
flat_x = torch.nn.functional.pad(flat_x, (0, pad_len), value=0.0)
|
|
51
|
+
|
|
52
|
+
# Reshape into blocks: [num_blocks, group_size]
|
|
53
|
+
blocked_x = flat_x.view(-1, group_size)
|
|
54
|
+
|
|
55
|
+
if symmetric:
|
|
56
|
+
q_blocks, scales = quantize_absmax(
|
|
57
|
+
blocked_x,
|
|
58
|
+
num_bits=num_bits,
|
|
59
|
+
dim=1,
|
|
60
|
+
symmetric_range=True,
|
|
61
|
+
)
|
|
62
|
+
zero_points = None
|
|
63
|
+
else:
|
|
64
|
+
q_blocks, scales, zero_points = quantize_affine(
|
|
65
|
+
blocked_x,
|
|
66
|
+
num_bits=num_bits,
|
|
67
|
+
dim=1,
|
|
68
|
+
signed=signed,
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
meta = {
|
|
72
|
+
"original_shape": orig_shape,
|
|
73
|
+
"n_elements": n_elements,
|
|
74
|
+
"pad_len": pad_len,
|
|
75
|
+
"group_size": group_size,
|
|
76
|
+
"num_bits": num_bits,
|
|
77
|
+
"symmetric": symmetric,
|
|
78
|
+
"dtype": x.dtype,
|
|
79
|
+
"device": x.device,
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return q_blocks, scales, zero_points, meta
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def dequantize_blockwise(
|
|
86
|
+
q_blocks: torch.Tensor,
|
|
87
|
+
scales: torch.Tensor,
|
|
88
|
+
zero_points: Optional[torch.Tensor],
|
|
89
|
+
meta: Dict[str, Any],
|
|
90
|
+
) -> torch.Tensor:
|
|
91
|
+
"""Reconstruct an original floating-point tensor from block-wise quantized representations.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
q_blocks: Quantized integer blocks of shape [num_blocks, group_size].
|
|
95
|
+
scales: Scale factors of shape [num_blocks, 1].
|
|
96
|
+
zero_points: Optional zero-points of shape [num_blocks, 1].
|
|
97
|
+
meta: Metadata dictionary returned by quantize_blockwise.
|
|
98
|
+
|
|
99
|
+
Returns:
|
|
100
|
+
Reconstructed tensor matching original shape and device.
|
|
101
|
+
"""
|
|
102
|
+
if meta["symmetric"]:
|
|
103
|
+
x_recon_blocked = dequantize_absmax(q_blocks, scales)
|
|
104
|
+
else:
|
|
105
|
+
assert zero_points is not None, (
|
|
106
|
+
"zero_points required for asymmetric blockwise dequantization"
|
|
107
|
+
)
|
|
108
|
+
x_recon_blocked = dequantize_affine(q_blocks, scales, zero_points)
|
|
109
|
+
|
|
110
|
+
flat_recon = x_recon_blocked.view(-1)
|
|
111
|
+
# Remove padding if added during quantization
|
|
112
|
+
if meta["pad_len"] > 0:
|
|
113
|
+
flat_recon = flat_recon[: meta["n_elements"]]
|
|
114
|
+
|
|
115
|
+
return flat_recon.view(meta["original_shape"]).to(meta["dtype"])
|
quantbench/quant/fp8.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""OCP FP8 (E4M3FN and E5M2) Emulation and Simulation Primitives.
|
|
2
|
+
|
|
3
|
+
Implements bit-exact simulation of the two standardized 8-bit floating-point formats:
|
|
4
|
+
1. E4M3FN: 1 sign, 4 exponent, 3 mantissa (bias 7, max ±448.0, no infinity, higher precision).
|
|
5
|
+
2. E5M2: 1 sign, 5 exponent, 2 mantissa (bias 15, max ±57344.0, IEEE inf/nan, wider dynamic range).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from enum import Enum
|
|
9
|
+
from typing import Optional, Tuple
|
|
10
|
+
|
|
11
|
+
import torch
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class FP8Format(str, Enum):
|
|
15
|
+
"""Standardized Open Compute Project (OCP) 8-bit floating point formats."""
|
|
16
|
+
|
|
17
|
+
E4M3 = "e4m3fn"
|
|
18
|
+
E5M2 = "e5m2"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
FP8_MAX_VALUES = {
|
|
22
|
+
FP8Format.E4M3: 448.0,
|
|
23
|
+
FP8Format.E5M2: 57344.0,
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _emulate_fp8(
|
|
28
|
+
x: torch.Tensor,
|
|
29
|
+
exp_bits: int,
|
|
30
|
+
mantissa_bits: int,
|
|
31
|
+
bias: int,
|
|
32
|
+
max_val: float,
|
|
33
|
+
has_inf: bool = False,
|
|
34
|
+
) -> torch.Tensor:
|
|
35
|
+
"""Analytical bit-level emulator for floating-point formats with arbitrary bitwidths.
|
|
36
|
+
|
|
37
|
+
Simulates the exact IEEE / OCP quantization, subnormal behavior, and saturation.
|
|
38
|
+
"""
|
|
39
|
+
sign = torch.sign(x)
|
|
40
|
+
abs_x = torch.abs(x)
|
|
41
|
+
|
|
42
|
+
# Saturate / clamp to max representable value
|
|
43
|
+
abs_x = torch.clamp(abs_x, max=max_val)
|
|
44
|
+
|
|
45
|
+
# Handle zeros
|
|
46
|
+
is_zero = abs_x == 0.0
|
|
47
|
+
|
|
48
|
+
# Calculate base-2 exponent
|
|
49
|
+
# e = floor(log2(x))
|
|
50
|
+
log2_x = torch.log2(torch.clamp(abs_x, min=1e-30))
|
|
51
|
+
exp = torch.floor(log2_x)
|
|
52
|
+
|
|
53
|
+
# Minimum normal exponent
|
|
54
|
+
min_exp = 1 - bias
|
|
55
|
+
max_exp = (1 << exp_bits) - 1 - bias
|
|
56
|
+
|
|
57
|
+
# Separate normals from subnormals
|
|
58
|
+
is_subnormal = exp < min_exp
|
|
59
|
+
exp = torch.clamp(exp, min=min_exp, max=max_exp)
|
|
60
|
+
|
|
61
|
+
# Mantissa quantization step size
|
|
62
|
+
# For normals: step = 2^(exp - mantissa_bits)
|
|
63
|
+
# For subnormals: step = 2^(min_exp - mantissa_bits)
|
|
64
|
+
eff_exp = torch.where(is_subnormal, torch.full_like(exp, min_exp), exp)
|
|
65
|
+
step = torch.pow(2.0, eff_exp - mantissa_bits)
|
|
66
|
+
|
|
67
|
+
# Discretize mantissa via round-to-nearest
|
|
68
|
+
q_abs = torch.round(abs_x / step) * step
|
|
69
|
+
|
|
70
|
+
# Re-apply clamp
|
|
71
|
+
q_abs = torch.clamp(q_abs, max=max_val)
|
|
72
|
+
|
|
73
|
+
# Zero out true zeros
|
|
74
|
+
q_abs = torch.where(is_zero, torch.zeros_like(q_abs), q_abs)
|
|
75
|
+
|
|
76
|
+
return sign * q_abs
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def quantize_fp8(
|
|
80
|
+
x: torch.Tensor,
|
|
81
|
+
format: FP8Format = FP8Format.E4M3,
|
|
82
|
+
scale: Optional[torch.Tensor] = None,
|
|
83
|
+
eps: float = 1e-8,
|
|
84
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
85
|
+
"""Quantize floating-point tensor to FP8 with dynamic or user-specified scaling.
|
|
86
|
+
|
|
87
|
+
Computes:
|
|
88
|
+
scale = max(|x|) / max_representable_fp8 (if scale is None)
|
|
89
|
+
scaled_x = x / scale
|
|
90
|
+
q_fp8 = quantize(scaled_x)
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
x: Input tensor.
|
|
94
|
+
format: FP8Format.E4M3 or FP8Format.E5M2.
|
|
95
|
+
scale: Optional precomputed scale factor tensor.
|
|
96
|
+
eps: Small value to avoid division by zero.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
Tuple of (quantized_fp8_tensor, scale).
|
|
100
|
+
"""
|
|
101
|
+
max_repr = FP8_MAX_VALUES[format]
|
|
102
|
+
|
|
103
|
+
if scale is None:
|
|
104
|
+
max_val = x.abs().max()
|
|
105
|
+
scale = max_val / float(max_repr)
|
|
106
|
+
scale = torch.clamp(scale, min=eps)
|
|
107
|
+
|
|
108
|
+
scaled_x = x / scale
|
|
109
|
+
|
|
110
|
+
# Check if native torch float8 dtype is supported and available
|
|
111
|
+
native_dtype = None
|
|
112
|
+
if hasattr(torch, "float8_e4m3fn") and format == FP8Format.E4M3:
|
|
113
|
+
native_dtype = torch.float8_e4m3fn
|
|
114
|
+
elif hasattr(torch, "float8_e5m2") and format == FP8Format.E5M2:
|
|
115
|
+
native_dtype = torch.float8_e5m2
|
|
116
|
+
|
|
117
|
+
if native_dtype is not None:
|
|
118
|
+
try:
|
|
119
|
+
# Native PyTorch conversion
|
|
120
|
+
q = scaled_x.to(native_dtype)
|
|
121
|
+
return q, scale
|
|
122
|
+
except Exception:
|
|
123
|
+
pass
|
|
124
|
+
|
|
125
|
+
# Analytical fallback emulation
|
|
126
|
+
if format == FP8Format.E4M3:
|
|
127
|
+
q = _emulate_fp8(
|
|
128
|
+
scaled_x,
|
|
129
|
+
exp_bits=4,
|
|
130
|
+
mantissa_bits=3,
|
|
131
|
+
bias=7,
|
|
132
|
+
max_val=448.0,
|
|
133
|
+
has_inf=False,
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
q = _emulate_fp8(
|
|
137
|
+
scaled_x,
|
|
138
|
+
exp_bits=5,
|
|
139
|
+
mantissa_bits=2,
|
|
140
|
+
bias=15,
|
|
141
|
+
max_val=57344.0,
|
|
142
|
+
has_inf=True,
|
|
143
|
+
)
|
|
144
|
+
|
|
145
|
+
return q, scale
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def dequantize_fp8(
|
|
149
|
+
q_fp8: torch.Tensor,
|
|
150
|
+
scale: torch.Tensor,
|
|
151
|
+
target_dtype: torch.dtype = torch.float32,
|
|
152
|
+
) -> torch.Tensor:
|
|
153
|
+
"""Dequantize an FP8 tensor back to target floating-point precision.
|
|
154
|
+
|
|
155
|
+
Computes:
|
|
156
|
+
x_hat = q_fp8 * scale
|
|
157
|
+
|
|
158
|
+
Args:
|
|
159
|
+
q_fp8: Quantized FP8 tensor.
|
|
160
|
+
scale: Scale factor tensor.
|
|
161
|
+
target_dtype: Desired output floating-point precision (e.g. float16, float32).
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Dequantized tensor.
|
|
165
|
+
"""
|
|
166
|
+
return q_fp8.to(target_dtype) * scale.to(target_dtype)
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Bit-packing runtimes for sub-byte quantization formats.
|
|
2
|
+
|
|
3
|
+
Provides vectorized bit-shift packing and unpacking for:
|
|
4
|
+
- INT4 (4-bit integers: unsigned [0, 15] or signed [-8, 7]) -> 2 values per uint8 byte.
|
|
5
|
+
- Ternary (BitNet 1.58b: {-1, 0, +1}) -> 4 values per uint8 byte.
|
|
6
|
+
- 2:4 Structured Sparsity Metadata -> two 2-bit indices per 4-bit nibble.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from quantbench.quant.packing.int4_pack import pack_int4, unpack_int4
|
|
10
|
+
from quantbench.quant.packing.sparse_pack import (
|
|
11
|
+
decompress_2_4,
|
|
12
|
+
pack_2_4_indices,
|
|
13
|
+
prune_and_compress_2_4,
|
|
14
|
+
unpack_2_4_indices,
|
|
15
|
+
)
|
|
16
|
+
from quantbench.quant.packing.ternary_pack import pack_ternary, unpack_ternary
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"pack_int4",
|
|
20
|
+
"unpack_int4",
|
|
21
|
+
"pack_ternary",
|
|
22
|
+
"unpack_ternary",
|
|
23
|
+
"pack_2_4_indices",
|
|
24
|
+
"unpack_2_4_indices",
|
|
25
|
+
"prune_and_compress_2_4",
|
|
26
|
+
"decompress_2_4",
|
|
27
|
+
]
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""Vectorized INT4 bit-packing runtime for sub-byte quantization.
|
|
2
|
+
|
|
3
|
+
Packs two 4-bit values (either unsigned [0, 15] or signed [-8, 7]) into a single uint8
|
|
4
|
+
byte container, delivering a 4x memory footprint reduction over FP16 and 2x over INT8.
|
|
5
|
+
|
|
6
|
+
Bit layout within each packed uint8 byte:
|
|
7
|
+
- Bits [0:3] (Low nibble): Even element index (2k)
|
|
8
|
+
- Bits [4:7] (High nibble): Odd element index (2k + 1)
|
|
9
|
+
|
|
10
|
+
Mathematical mapping:
|
|
11
|
+
byte = (v_even & 0x0F) | ((v_odd & 0x0F) << 4)
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import torch
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def pack_nibbles(low: int, high: int) -> int:
|
|
20
|
+
"""Pack two 4-bit integer scalars into a single 8-bit unsigned byte.
|
|
21
|
+
|
|
22
|
+
Args:
|
|
23
|
+
low: Integer value for low nibble (bits 0..3), range [0, 15] or [-8, 7].
|
|
24
|
+
high: Integer value for high nibble (bits 4..7), range [0, 15] or [-8, 7].
|
|
25
|
+
|
|
26
|
+
Returns:
|
|
27
|
+
Unsigned 8-bit integer in range [0, 255].
|
|
28
|
+
"""
|
|
29
|
+
u_low = low & 0x0F
|
|
30
|
+
u_high = high & 0x0F
|
|
31
|
+
return (u_low) | (u_high << 4)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def unpack_nibbles(byte: int, signed: bool = False) -> tuple[int, int]:
|
|
35
|
+
"""Unpack a single 8-bit unsigned byte into two 4-bit integer scalars.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
byte: Unsigned 8-bit integer in range [0, 255].
|
|
39
|
+
signed: If True, sign-extends 4-bit values into signed range [-8, 7].
|
|
40
|
+
If False, returns unsigned values in range [0, 15].
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Tuple of (low_nibble, high_nibble).
|
|
44
|
+
"""
|
|
45
|
+
low = byte & 0x0F
|
|
46
|
+
high = (byte >> 4) & 0x0F
|
|
47
|
+
if signed:
|
|
48
|
+
if low >= 8:
|
|
49
|
+
low -= 16
|
|
50
|
+
if high >= 8:
|
|
51
|
+
high -= 16
|
|
52
|
+
return low, high
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def pack_int4(
|
|
56
|
+
tensor: torch.Tensor,
|
|
57
|
+
signed: bool = False,
|
|
58
|
+
dim: int = -1,
|
|
59
|
+
) -> tuple[torch.Tensor, int]:
|
|
60
|
+
"""Vectorized bit-shift packing of INT4 values into a uint8 tensor.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
tensor: Integer PyTorch tensor containing 4-bit values.
|
|
64
|
+
If signed=False: values must be in [0, 15].
|
|
65
|
+
If signed=True: values must be in [-8, 7].
|
|
66
|
+
signed: Whether the input values are signed two's complement.
|
|
67
|
+
dim: Dimension along which to pack adjacent pairs. Defaults to last dimension (-1).
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
Tuple of (packed_uint8_tensor, original_dim_length).
|
|
71
|
+
The packed tensor will have size ceil(original_length / 2) along `dim`.
|
|
72
|
+
"""
|
|
73
|
+
if not tensor.dtype.is_floating_point and tensor.dtype != torch.bool:
|
|
74
|
+
t_int = tensor.to(torch.int32)
|
|
75
|
+
else:
|
|
76
|
+
t_int = tensor.round().to(torch.int32)
|
|
77
|
+
|
|
78
|
+
# Validate bounds
|
|
79
|
+
if signed:
|
|
80
|
+
if (t_int < -8).any() or (t_int > 7).any():
|
|
81
|
+
raise ValueError("Signed INT4 values must fall strictly within range [-8, 7].")
|
|
82
|
+
else:
|
|
83
|
+
if (t_int < 0).any() or (t_int > 15).any():
|
|
84
|
+
raise ValueError("Unsigned INT4 values must fall strictly within range [0, 15].")
|
|
85
|
+
|
|
86
|
+
# Normalize dim
|
|
87
|
+
dim = dim if dim >= 0 else tensor.ndim + dim
|
|
88
|
+
orig_len = tensor.shape[dim]
|
|
89
|
+
|
|
90
|
+
# Convert to 4-bit unsigned representation
|
|
91
|
+
u4 = t_int & 0x0F
|
|
92
|
+
|
|
93
|
+
# Pad by 1 if odd length along dim
|
|
94
|
+
if orig_len % 2 != 0:
|
|
95
|
+
pad_shape = list(u4.shape)
|
|
96
|
+
pad_shape[dim] = 1
|
|
97
|
+
pad_tensor = torch.zeros(pad_shape, dtype=u4.dtype, device=u4.device)
|
|
98
|
+
u4 = torch.cat([u4, pad_tensor], dim=dim)
|
|
99
|
+
|
|
100
|
+
# Slice into even and odd indices
|
|
101
|
+
# We move target dim to last position for easy contiguous slicing
|
|
102
|
+
if dim != u4.ndim - 1:
|
|
103
|
+
u4 = u4.transpose(dim, -1)
|
|
104
|
+
|
|
105
|
+
low = u4[..., 0::2]
|
|
106
|
+
high = u4[..., 1::2]
|
|
107
|
+
|
|
108
|
+
packed = (low & 0x0F) | ((high & 0x0F) << 4)
|
|
109
|
+
packed_uint8 = packed.to(torch.uint8)
|
|
110
|
+
|
|
111
|
+
if dim != tensor.ndim - 1:
|
|
112
|
+
packed_uint8 = packed_uint8.transpose(dim, -1)
|
|
113
|
+
|
|
114
|
+
return packed_uint8, orig_len
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def unpack_int4(
|
|
118
|
+
packed: torch.Tensor,
|
|
119
|
+
original_len: int | None = None,
|
|
120
|
+
signed: bool = False,
|
|
121
|
+
dim: int = -1,
|
|
122
|
+
) -> torch.Tensor:
|
|
123
|
+
"""Vectorized unpacking of a uint8 packed tensor back into INT4 values.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
packed: Tensor of dtype torch.uint8 containing packed pairs.
|
|
127
|
+
original_len: Optional original dimension length before packing.
|
|
128
|
+
Used to remove padding if original length was odd.
|
|
129
|
+
signed: If True, sign-extends values to signed range [-8, 7].
|
|
130
|
+
If False, returns unsigned values in [0, 15].
|
|
131
|
+
dim: Dimension along which elements were packed. Defaults to -1.
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Unpacked tensor with dtype torch.int8 (if signed) or torch.uint8 (if unsigned),
|
|
135
|
+
restored to the original length along `dim`.
|
|
136
|
+
"""
|
|
137
|
+
if packed.dtype != torch.uint8:
|
|
138
|
+
raise TypeError(f"Packed tensor must have dtype torch.uint8, got {packed.dtype}.")
|
|
139
|
+
|
|
140
|
+
dim = dim if dim >= 0 else packed.ndim + dim
|
|
141
|
+
if dim != packed.ndim - 1:
|
|
142
|
+
packed = packed.transpose(dim, -1)
|
|
143
|
+
|
|
144
|
+
# Extract low nibble and high nibble
|
|
145
|
+
p32 = packed.to(torch.int32)
|
|
146
|
+
low = p32 & 0x0F
|
|
147
|
+
high = (p32 >> 4) & 0x0F
|
|
148
|
+
|
|
149
|
+
# Interleave low and high along a new sub-dimension
|
|
150
|
+
# Shape becomes (*batch_dims, packed_len, 2) -> flatten to (*batch_dims, packed_len * 2)
|
|
151
|
+
interleaved = torch.stack([low, high], dim=-1)
|
|
152
|
+
flat_shape = list(interleaved.shape[:-2]) + [interleaved.shape[-2] * 2]
|
|
153
|
+
unpacked = interleaved.view(flat_shape)
|
|
154
|
+
|
|
155
|
+
# Truncate padding if original_len is specified
|
|
156
|
+
if original_len is not None:
|
|
157
|
+
unpacked = unpacked[..., :original_len]
|
|
158
|
+
|
|
159
|
+
if signed:
|
|
160
|
+
# Values in 8..15 have the sign bit set; subtract 16
|
|
161
|
+
unpacked = torch.where(unpacked >= 8, unpacked - 16, unpacked)
|
|
162
|
+
unpacked = unpacked.to(torch.int8)
|
|
163
|
+
else:
|
|
164
|
+
unpacked = unpacked.to(torch.uint8)
|
|
165
|
+
|
|
166
|
+
if dim != packed.ndim - 1:
|
|
167
|
+
unpacked = unpacked.transpose(dim, -1)
|
|
168
|
+
|
|
169
|
+
return unpacked
|