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,188 @@
|
|
|
1
|
+
"""Uniform Affine and Symmetric AbsMax Quantization Primitives.
|
|
2
|
+
|
|
3
|
+
Implements the fundamental mathematical mappings between continuous floating-point
|
|
4
|
+
representations and discrete integer quantization grids.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional, Tuple, Union
|
|
8
|
+
|
|
9
|
+
import torch
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def quantize_affine(
|
|
13
|
+
x: torch.Tensor,
|
|
14
|
+
num_bits: int = 8,
|
|
15
|
+
dim: Optional[Union[int, Tuple[int, ...]]] = None,
|
|
16
|
+
signed: bool = False,
|
|
17
|
+
eps: float = 1e-8,
|
|
18
|
+
) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
|
|
19
|
+
"""Quantize a floating-point tensor using uniform affine (asymmetric) quantization.
|
|
20
|
+
|
|
21
|
+
Maps [x_min, x_max] to integer range [q_min, q_max] using:
|
|
22
|
+
scale = (x_max - x_min) / (2^num_bits - 1)
|
|
23
|
+
zero_point = round(-x_min / scale) + q_min
|
|
24
|
+
q = clamp(round(x / scale) + zero_point, q_min, q_max)
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
x: Input floating-point tensor.
|
|
28
|
+
num_bits: Target integer precision (typically 4 or 8).
|
|
29
|
+
dim: Dimension(s) along which to reduce for per-channel scaling.
|
|
30
|
+
If None, computes a single scalar per-tensor.
|
|
31
|
+
signed: Whether the integer grid is signed (e.g. [-128, 127]) or unsigned ([0, 255]).
|
|
32
|
+
eps: Small epsilon to prevent division by zero for constant tensors.
|
|
33
|
+
|
|
34
|
+
Returns:
|
|
35
|
+
Tuple of (quantized_tensor, scale, zero_point).
|
|
36
|
+
"""
|
|
37
|
+
if signed:
|
|
38
|
+
q_min = -(1 << (num_bits - 1))
|
|
39
|
+
q_max = (1 << (num_bits - 1)) - 1
|
|
40
|
+
else:
|
|
41
|
+
q_min = 0
|
|
42
|
+
q_max = (1 << num_bits) - 1
|
|
43
|
+
|
|
44
|
+
if dim is None:
|
|
45
|
+
x_min = x.min()
|
|
46
|
+
x_max = x.max()
|
|
47
|
+
else:
|
|
48
|
+
x_min = x.amin(dim=dim, keepdim=True)
|
|
49
|
+
x_max = x.amax(dim=dim, keepdim=True)
|
|
50
|
+
|
|
51
|
+
# Standard zero-preserving affine quantization
|
|
52
|
+
# (ensures 0.0 maps exactly to an integer zero_point)
|
|
53
|
+
zeros = torch.zeros_like(x_min)
|
|
54
|
+
x_min_adj = torch.minimum(x_min, zeros)
|
|
55
|
+
x_max_adj = torch.maximum(x_max, zeros)
|
|
56
|
+
|
|
57
|
+
# Calculate scale factor
|
|
58
|
+
scale = (x_max_adj - x_min_adj) / float(q_max - q_min)
|
|
59
|
+
scale = torch.clamp(scale, min=eps)
|
|
60
|
+
|
|
61
|
+
# Calculate zero-point
|
|
62
|
+
zero_point = torch.round(-x_min_adj / scale) + q_min
|
|
63
|
+
zero_point = torch.clamp(zero_point, min=q_min, max=q_max)
|
|
64
|
+
|
|
65
|
+
# Quantize and clamp
|
|
66
|
+
q = torch.round(x / scale) + zero_point
|
|
67
|
+
q = torch.clamp(q, min=q_min, max=q_max)
|
|
68
|
+
|
|
69
|
+
# Cast to integer representation
|
|
70
|
+
dtype = torch.int8 if signed else torch.uint8
|
|
71
|
+
if num_bits <= 8:
|
|
72
|
+
q = q.to(dtype)
|
|
73
|
+
else:
|
|
74
|
+
q = q.to(torch.int32)
|
|
75
|
+
|
|
76
|
+
return q, scale, zero_point
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def dequantize_affine(
|
|
80
|
+
q: torch.Tensor,
|
|
81
|
+
scale: torch.Tensor,
|
|
82
|
+
zero_point: torch.Tensor,
|
|
83
|
+
) -> torch.Tensor:
|
|
84
|
+
"""Dequantize an affine-quantized integer tensor back to floating-point.
|
|
85
|
+
|
|
86
|
+
Computes:
|
|
87
|
+
x_hat = (q - zero_point) * scale
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
q: Integer quantized tensor.
|
|
91
|
+
scale: Floating-point scale tensor (broadcastable to q).
|
|
92
|
+
zero_point: Integer or floating-point zero-point tensor.
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
Reconstructed floating-point tensor.
|
|
96
|
+
"""
|
|
97
|
+
return (q.to(scale.dtype) - zero_point.to(scale.dtype)) * scale
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def quantize_absmax(
|
|
101
|
+
x: torch.Tensor,
|
|
102
|
+
num_bits: int = 8,
|
|
103
|
+
dim: Optional[Union[int, Tuple[int, ...]]] = None,
|
|
104
|
+
symmetric_range: bool = True,
|
|
105
|
+
eps: float = 1e-8,
|
|
106
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
107
|
+
"""Quantize a floating-point tensor using symmetric AbsMax quantization (zero_point = 0).
|
|
108
|
+
|
|
109
|
+
Maps [-max(|x|), max(|x|)] to signed integer range [-q_max, q_max] using:
|
|
110
|
+
scale = max(|x|) / q_max
|
|
111
|
+
q = clamp(round(x / scale), q_min, q_max)
|
|
112
|
+
|
|
113
|
+
Args:
|
|
114
|
+
x: Input floating-point tensor.
|
|
115
|
+
num_bits: Target bit precision (typically 4 or 8).
|
|
116
|
+
dim: Dimension(s) along which to reduce for per-channel scaling.
|
|
117
|
+
If None, computes per-tensor scale.
|
|
118
|
+
symmetric_range: If True, restricts range to [-(2^(b-1)-1), 2^(b-1)-1] to preserve
|
|
119
|
+
exact symmetry around zero. If False, allows [-2^(b-1), 2^(b-1)-1].
|
|
120
|
+
eps: Epsilon to avoid division by zero.
|
|
121
|
+
|
|
122
|
+
Returns:
|
|
123
|
+
Tuple of (quantized_tensor, scale).
|
|
124
|
+
"""
|
|
125
|
+
q_max = (1 << (num_bits - 1)) - 1
|
|
126
|
+
q_min = -q_max if symmetric_range else -(1 << (num_bits - 1))
|
|
127
|
+
|
|
128
|
+
if dim is None:
|
|
129
|
+
max_val = x.abs().max()
|
|
130
|
+
else:
|
|
131
|
+
max_val = x.abs().amax(dim=dim, keepdim=True)
|
|
132
|
+
|
|
133
|
+
scale = max_val / float(q_max)
|
|
134
|
+
scale = torch.clamp(scale, min=eps)
|
|
135
|
+
|
|
136
|
+
q = torch.round(x / scale)
|
|
137
|
+
q = torch.clamp(q, min=q_min, max=q_max)
|
|
138
|
+
|
|
139
|
+
if num_bits <= 8:
|
|
140
|
+
q = q.to(torch.int8)
|
|
141
|
+
else:
|
|
142
|
+
q = q.to(torch.int32)
|
|
143
|
+
|
|
144
|
+
return q, scale
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def dequantize_absmax(
|
|
148
|
+
q: torch.Tensor,
|
|
149
|
+
scale: torch.Tensor,
|
|
150
|
+
) -> torch.Tensor:
|
|
151
|
+
"""Dequantize an AbsMax-quantized tensor back to floating-point.
|
|
152
|
+
|
|
153
|
+
Computes:
|
|
154
|
+
x_hat = q * scale
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
q: Integer quantized tensor.
|
|
158
|
+
scale: Floating-point scale tensor (broadcastable to q).
|
|
159
|
+
|
|
160
|
+
Returns:
|
|
161
|
+
Reconstructed floating-point tensor.
|
|
162
|
+
"""
|
|
163
|
+
return q.to(scale.dtype) * scale
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def check_quantization_invariants(
|
|
167
|
+
x: torch.Tensor,
|
|
168
|
+
x_hat: torch.Tensor,
|
|
169
|
+
scale: torch.Tensor,
|
|
170
|
+
tolerance_factor: float = 0.51,
|
|
171
|
+
) -> bool:
|
|
172
|
+
"""Verify the fundamental quantization invariant: error <= scale / 2.
|
|
173
|
+
|
|
174
|
+
For uniform round-to-nearest quantization, the maximum discretization error
|
|
175
|
+
for any element within the clamp range is bounded by scale / 2.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
x: Original floating-point tensor.
|
|
179
|
+
x_hat: Dequantized floating-point tensor.
|
|
180
|
+
scale: Scale tensor used for quantization.
|
|
181
|
+
tolerance_factor: Discretization error threshold multiplier (default 0.51).
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if all elements within dynamic range satisfy the invariant.
|
|
185
|
+
"""
|
|
186
|
+
err = (x - x_hat).abs()
|
|
187
|
+
max_allowed_err = scale * tolerance_factor
|
|
188
|
+
return bool(torch.all(err <= max_allowed_err + 1e-6))
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
"""Advanced calibration algorithms for post-training quantization."""
|
|
16
|
+
|
|
17
|
+
from quantbench.quant.algorithms.awq import (
|
|
18
|
+
awq_quantize_weights,
|
|
19
|
+
compute_activation_channel_means,
|
|
20
|
+
search_best_awq_scale,
|
|
21
|
+
)
|
|
22
|
+
from quantbench.quant.algorithms.gptq import (
|
|
23
|
+
compute_activation_hessian,
|
|
24
|
+
gptq_quantize_weights,
|
|
25
|
+
)
|
|
26
|
+
from quantbench.quant.algorithms.smoothquant import (
|
|
27
|
+
compute_smoothquant_scale,
|
|
28
|
+
extract_activation_channel_scales,
|
|
29
|
+
smooth_linear_weights,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"compute_smoothquant_scale",
|
|
34
|
+
"extract_activation_channel_scales",
|
|
35
|
+
"smooth_linear_weights",
|
|
36
|
+
"compute_activation_channel_means",
|
|
37
|
+
"search_best_awq_scale",
|
|
38
|
+
"awq_quantize_weights",
|
|
39
|
+
"compute_activation_hessian",
|
|
40
|
+
"gptq_quantize_weights",
|
|
41
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
"""AWQ: Activation-aware Weight Quantization for LLM Compression and Acceleration.
|
|
16
|
+
|
|
17
|
+
Mathematical Foundation:
|
|
18
|
+
Not all weights in a neural network are equally critical to perplexity.
|
|
19
|
+
AWQ discovers that protecting the top ~1% of salient weights avoids perplexity
|
|
20
|
+
degradation even at aggressive INT4/INT3 regimes. Crucially, salient weights
|
|
21
|
+
directly correspond to channels that process activations with high magnitudes:
|
|
22
|
+
s_X = (1 / N) * sum(|X|) (mean absolute activation per input channel)
|
|
23
|
+
|
|
24
|
+
Instead of maintaining mixed-precision floating-point formats (which incur runtime
|
|
25
|
+
overhead and degrade hardware SIMD execution efficiency), AWQ applies a per-channel
|
|
26
|
+
protection scale s:
|
|
27
|
+
W' = W * diag(s)
|
|
28
|
+
X' = diag(s)^-1 * X
|
|
29
|
+
|
|
30
|
+
When W' is quantized to sub-byte integers (e.g. INT4 blockwise), the relative quantization
|
|
31
|
+
error on salient channels is inversely proportional to s_j:
|
|
32
|
+
Delta W_j / W_j ~ 1 / s_j
|
|
33
|
+
|
|
34
|
+
AWQ searches for the optimal per-channel scale s_j = (s_{X, j})^alpha over a grid of
|
|
35
|
+
alpha in [0.0, 1.0] by directly minimizing the Frobenius output reconstruction error:
|
|
36
|
+
min_alpha || W * X - [Quantize(W * diag(s)) * diag(s)^-1] * X ||_F^2
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from typing import Callable, Optional, Tuple
|
|
42
|
+
|
|
43
|
+
import torch
|
|
44
|
+
|
|
45
|
+
from quantbench.quant.blockwise import dequantize_blockwise, quantize_blockwise
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def compute_activation_channel_means(
|
|
49
|
+
calibration_inputs: list[torch.Tensor] | torch.Tensor,
|
|
50
|
+
) -> torch.Tensor:
|
|
51
|
+
"""Calculate mean absolute activation magnitude per input channel.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
calibration_inputs: Tensor of shape (..., in_features) or list of tensors.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
Tensor of shape (in_features,) containing per-channel mean absolute activation.
|
|
58
|
+
"""
|
|
59
|
+
if isinstance(calibration_inputs, torch.Tensor):
|
|
60
|
+
calibration_inputs = [calibration_inputs]
|
|
61
|
+
|
|
62
|
+
total_sum: Optional[torch.Tensor] = None
|
|
63
|
+
total_tokens = 0
|
|
64
|
+
|
|
65
|
+
for batch in calibration_inputs:
|
|
66
|
+
flat = batch.detach().view(-1, batch.shape[-1]).abs()
|
|
67
|
+
batch_sum = flat.sum(dim=0)
|
|
68
|
+
n_tokens = flat.shape[0]
|
|
69
|
+
|
|
70
|
+
if total_sum is None:
|
|
71
|
+
total_sum = batch_sum
|
|
72
|
+
else:
|
|
73
|
+
total_sum = total_sum + batch_sum
|
|
74
|
+
total_tokens += n_tokens
|
|
75
|
+
|
|
76
|
+
if total_sum is None or total_tokens == 0:
|
|
77
|
+
raise ValueError("calibration_inputs cannot be empty.")
|
|
78
|
+
|
|
79
|
+
return total_sum / total_tokens
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def search_best_awq_scale(
|
|
83
|
+
weight: torch.Tensor,
|
|
84
|
+
act_means: torch.Tensor,
|
|
85
|
+
calibration_x: torch.Tensor,
|
|
86
|
+
quant_fn: Optional[Callable[[torch.Tensor], torch.Tensor]] = None,
|
|
87
|
+
n_grid: int = 20,
|
|
88
|
+
max_alpha: float = 1.0,
|
|
89
|
+
) -> Tuple[torch.Tensor, float]:
|
|
90
|
+
"""Perform grid search for optimal AWQ protection scale alpha.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
weight: Float tensor of shape (out_features, in_features).
|
|
94
|
+
act_means: Channel-wise activation mean of shape (in_features,).
|
|
95
|
+
calibration_x: Sample calibration activations of shape (N_samples, in_features).
|
|
96
|
+
quant_fn: Quantize-and-dequantize callable. Defaults to INT4 blockwise (group_size=128).
|
|
97
|
+
n_grid: Number of search points in alpha grid [0, max_alpha].
|
|
98
|
+
max_alpha: Maximum alpha value for search.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Tuple of (optimal_scale_vector, best_alpha):
|
|
102
|
+
- optimal_scale_vector: Per-channel scale s in R^{in_features}.
|
|
103
|
+
- best_alpha: Optimal alpha hyperparameter.
|
|
104
|
+
"""
|
|
105
|
+
if quant_fn is None:
|
|
106
|
+
def default_quant(w: torch.Tensor) -> torch.Tensor:
|
|
107
|
+
q_b, s_b, zp_b, meta = quantize_blockwise(
|
|
108
|
+
w, group_size=min(128, w.shape[1]), num_bits=4
|
|
109
|
+
)
|
|
110
|
+
return dequantize_blockwise(q_b, s_b, zp_b, meta)
|
|
111
|
+
quant_fn = default_quant
|
|
112
|
+
|
|
113
|
+
# Reference unquantized linear output
|
|
114
|
+
# calibration_x: (N, in_features), weight: (out_features, in_features)
|
|
115
|
+
# y_ref: (N, out_features)
|
|
116
|
+
y_ref = calibration_x @ weight.t()
|
|
117
|
+
|
|
118
|
+
best_error = float("inf")
|
|
119
|
+
best_scale = torch.ones(weight.shape[1], device=weight.device, dtype=weight.dtype)
|
|
120
|
+
best_alpha = 0.0
|
|
121
|
+
|
|
122
|
+
act_means = act_means.clamp(min=1e-5)
|
|
123
|
+
|
|
124
|
+
alphas = torch.linspace(0.0, max_alpha, steps=n_grid)
|
|
125
|
+
|
|
126
|
+
for alpha_val in alphas:
|
|
127
|
+
alpha = float(alpha_val.item())
|
|
128
|
+
# Candidate scale s = act_means^alpha
|
|
129
|
+
cand_scale = act_means.pow(alpha)
|
|
130
|
+
# Normalize scale by geometric mean of min/max to maintain weight scale stability
|
|
131
|
+
geom_mean = (cand_scale.max() * cand_scale.min()).sqrt().clamp(min=1e-5)
|
|
132
|
+
cand_scale = (cand_scale / geom_mean).clamp(min=1e-4)
|
|
133
|
+
|
|
134
|
+
# Scale weights: W' = W * diag(s)
|
|
135
|
+
# weight: (out, in), cand_scale: (in,) -> broadcast along rows
|
|
136
|
+
w_scaled = weight * cand_scale.unsqueeze(0)
|
|
137
|
+
|
|
138
|
+
# Quantize and dequantize
|
|
139
|
+
w_dequant = quant_fn(w_scaled)
|
|
140
|
+
|
|
141
|
+
# Inverse scale: W_cand = w_dequant * diag(s)^-1
|
|
142
|
+
w_cand = w_dequant / cand_scale.unsqueeze(0)
|
|
143
|
+
|
|
144
|
+
# Output error || y_ref - x @ w_cand.t() ||_F^2
|
|
145
|
+
y_cand = calibration_x @ w_cand.t()
|
|
146
|
+
err = torch.norm(y_ref - y_cand, p="fro").item()
|
|
147
|
+
|
|
148
|
+
if err < best_error:
|
|
149
|
+
best_error = err
|
|
150
|
+
best_scale = cand_scale
|
|
151
|
+
best_alpha = alpha
|
|
152
|
+
|
|
153
|
+
return best_scale, best_alpha
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def awq_quantize_weights(
|
|
157
|
+
weight: torch.Tensor,
|
|
158
|
+
scale: torch.Tensor,
|
|
159
|
+
quant_fn: Optional[Callable[[torch.Tensor], torch.Tensor]] = None,
|
|
160
|
+
) -> Tuple[torch.Tensor, torch.Tensor]:
|
|
161
|
+
"""Apply optimal AWQ scale and quantize weight matrix.
|
|
162
|
+
|
|
163
|
+
Args:
|
|
164
|
+
weight: Original weight of shape (out_features, in_features).
|
|
165
|
+
scale: Optimal per-channel protection scale of shape (in_features,).
|
|
166
|
+
quant_fn: Quantization-dequantization function.
|
|
167
|
+
|
|
168
|
+
Returns:
|
|
169
|
+
Tuple of (dequantized_weight, scale).
|
|
170
|
+
"""
|
|
171
|
+
if quant_fn is None:
|
|
172
|
+
def default_quant(w: torch.Tensor) -> torch.Tensor:
|
|
173
|
+
q_b, s_b, zp_b, meta = quantize_blockwise(
|
|
174
|
+
w, group_size=min(128, w.shape[1]), num_bits=4
|
|
175
|
+
)
|
|
176
|
+
return dequantize_blockwise(q_b, s_b, zp_b, meta)
|
|
177
|
+
quant_fn = default_quant
|
|
178
|
+
|
|
179
|
+
w_scaled = weight * scale.unsqueeze(0)
|
|
180
|
+
w_dequant = quant_fn(w_scaled)
|
|
181
|
+
# Invert scale so layer can be run directly with unscaled input:
|
|
182
|
+
# y = x @ (w_dequant / s).t()
|
|
183
|
+
w_effective = w_dequant / scale.unsqueeze(0)
|
|
184
|
+
return w_effective, scale
|
|
@@ -0,0 +1,181 @@
|
|
|
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
|
+
"""GPTQ: Accurate Post-Training Quantization for Generative Pre-trained Transformers.
|
|
16
|
+
|
|
17
|
+
Mathematical Foundation:
|
|
18
|
+
Post-training quantization aims to minimize the layer-wise Frobenius reconstruction error:
|
|
19
|
+
argmin_{W_hat} || W * X - W_hat * X ||_F^2
|
|
20
|
+
|
|
21
|
+
Using a second-order Taylor expansion around the continuous weights W, the loss
|
|
22
|
+
increase induced by quantizing weights is governed by the input activation Hessian:
|
|
23
|
+
H = 2 * X * X^T in R^{d_in x d_in}
|
|
24
|
+
|
|
25
|
+
Optimal Brain Surgeon (OBS) establishes that rounding column j to W_hat_{:, j} produces
|
|
26
|
+
an error delta w_j = W_{:, j} - W_hat_{:, j}. The optimal linear compensation applied to
|
|
27
|
+
the remaining unquantized columns F = {j+1, ..., d_in - 1} is given by:
|
|
28
|
+
Delta W_{:, F} = - (W_{:, j} - W_hat_{:, j}) / [H^-1]_{j, j} * [H^-1]_{j, F}
|
|
29
|
+
|
|
30
|
+
To prevent numerical divergence from zero/near-zero eigenvalues, the empirical Hessian
|
|
31
|
+
is regularized with diagonal damping before inversion:
|
|
32
|
+
H_damp = H + lambda * mean(diag(H)) * I (typically lambda = 0.01)
|
|
33
|
+
|
|
34
|
+
GPTQ implements a vectorized block-wise update (block size B, e.g. 32 or 128)
|
|
35
|
+
converting column-by-column corrections into dense matrix multiplications (GEMM)
|
|
36
|
+
while avoiding O(d^3) naive Cholesky recomputations.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
from typing import Optional
|
|
42
|
+
|
|
43
|
+
import torch
|
|
44
|
+
|
|
45
|
+
from quantbench.quant.affine import dequantize_absmax, quantize_absmax
|
|
46
|
+
from quantbench.quant.blockwise import dequantize_blockwise, quantize_blockwise
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_activation_hessian(
|
|
50
|
+
calibration_inputs: list[torch.Tensor] | torch.Tensor,
|
|
51
|
+
in_features: int,
|
|
52
|
+
device: Optional[torch.device] = None,
|
|
53
|
+
) -> torch.Tensor:
|
|
54
|
+
"""Compute empirical input activation Hessian H = 2 * X * X^T across calibration batches.
|
|
55
|
+
|
|
56
|
+
Args:
|
|
57
|
+
calibration_inputs: Tensor of shape (..., in_features) or list of tensors.
|
|
58
|
+
in_features: Number of input features (columns of weight matrix).
|
|
59
|
+
device: Torch device for computation.
|
|
60
|
+
|
|
61
|
+
Returns:
|
|
62
|
+
Tensor of shape (in_features, in_features) representing the accumulated Hessian.
|
|
63
|
+
"""
|
|
64
|
+
if isinstance(calibration_inputs, torch.Tensor):
|
|
65
|
+
calibration_inputs = [calibration_inputs]
|
|
66
|
+
|
|
67
|
+
dev = device or calibration_inputs[0].device
|
|
68
|
+
H = torch.zeros((in_features, in_features), device=dev, dtype=torch.float32)
|
|
69
|
+
n_samples = 0
|
|
70
|
+
|
|
71
|
+
for batch in calibration_inputs:
|
|
72
|
+
# Reshape to (N_tokens, in_features)
|
|
73
|
+
x = batch.to(dev).float().view(-1, in_features)
|
|
74
|
+
# H += 2 * x^T * x
|
|
75
|
+
H += 2.0 * (x.t() @ x)
|
|
76
|
+
n_samples += x.shape[0]
|
|
77
|
+
|
|
78
|
+
if n_samples == 0:
|
|
79
|
+
raise ValueError("calibration_inputs contains 0 tokens.")
|
|
80
|
+
|
|
81
|
+
# Normalize by total sample count
|
|
82
|
+
return H / n_samples
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def gptq_quantize_weights(
|
|
86
|
+
weight: torch.Tensor,
|
|
87
|
+
H: torch.Tensor,
|
|
88
|
+
bits: int = 4,
|
|
89
|
+
group_size: int = 128,
|
|
90
|
+
block_size: int = 32,
|
|
91
|
+
damping_factor: float = 0.01,
|
|
92
|
+
) -> torch.Tensor:
|
|
93
|
+
"""Quantize weight matrix using GPTQ second-order Hessian error compensation.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
weight: Float tensor of shape (out_features, in_features).
|
|
97
|
+
H: Input Hessian matrix of shape (in_features, in_features).
|
|
98
|
+
bits: Target integer bit-width (e.g. 4 or 8).
|
|
99
|
+
group_size: Quantization block/group size for scales (-1 for per-channel).
|
|
100
|
+
block_size: GPTQ column block update size (e.g. 32).
|
|
101
|
+
damping_factor: Diagonal damping regularization factor lambda.
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Quantized and dequantized weight tensor in continuous precision with
|
|
105
|
+
second-order error compensation applied.
|
|
106
|
+
"""
|
|
107
|
+
out_features, in_features = weight.shape
|
|
108
|
+
device = weight.device
|
|
109
|
+
dtype = weight.dtype
|
|
110
|
+
|
|
111
|
+
# Work in float32 for numerical stability
|
|
112
|
+
W = weight.clone().float()
|
|
113
|
+
H = H.clone().float()
|
|
114
|
+
|
|
115
|
+
# Damping regularization: H_reg = H + lambda * mean(diag(H)) * I
|
|
116
|
+
diag_mean = torch.mean(torch.diag(H)).item()
|
|
117
|
+
if diag_mean <= 0.0:
|
|
118
|
+
diag_mean = 1.0
|
|
119
|
+
damp = damping_factor * diag_mean
|
|
120
|
+
H += damp * torch.eye(in_features, device=device)
|
|
121
|
+
|
|
122
|
+
# Invert Hessian via Cholesky decomposition: H = L * L^T -> H^-1 = L^-T * L^-1
|
|
123
|
+
try:
|
|
124
|
+
L = torch.linalg.cholesky(H)
|
|
125
|
+
L_inv = torch.linalg.inv(L)
|
|
126
|
+
H_inv = L_inv.t() @ L_inv
|
|
127
|
+
except RuntimeError:
|
|
128
|
+
# Fallback to pseudo-inverse if Cholesky fails due to singular sub-blocks
|
|
129
|
+
H_inv = torch.linalg.pinv(H)
|
|
130
|
+
|
|
131
|
+
# Quantization helper for column vectors / sub-blocks
|
|
132
|
+
def quantize_dequantize_cols(col_w: torch.Tensor) -> torch.Tensor:
|
|
133
|
+
# col_w: (out_features, num_cols)
|
|
134
|
+
if group_size > 0:
|
|
135
|
+
q_b, s_b, zp_b, meta = quantize_blockwise(
|
|
136
|
+
col_w, group_size=group_size, num_bits=bits
|
|
137
|
+
)
|
|
138
|
+
return dequantize_blockwise(q_b, s_b, zp_b, meta)
|
|
139
|
+
else:
|
|
140
|
+
q, scale = quantize_absmax(col_w, num_bits=bits, dim=0)
|
|
141
|
+
return dequantize_absmax(q, scale)
|
|
142
|
+
|
|
143
|
+
Q = torch.zeros_like(W)
|
|
144
|
+
|
|
145
|
+
# Iterate through column blocks
|
|
146
|
+
for col_start in range(0, in_features, block_size):
|
|
147
|
+
col_end = min(col_start + block_size, in_features)
|
|
148
|
+
count = col_end - col_start
|
|
149
|
+
|
|
150
|
+
# Sub-block weights and sub-block Hessian inverse
|
|
151
|
+
W_block = W[:, col_start:col_end].clone()
|
|
152
|
+
Q_block = torch.zeros_like(W_block)
|
|
153
|
+
err_block = torch.zeros_like(W_block)
|
|
154
|
+
|
|
155
|
+
H_inv_block = H_inv[col_start:col_end, col_start:col_end]
|
|
156
|
+
|
|
157
|
+
# Column-by-column inside the block
|
|
158
|
+
for i in range(count):
|
|
159
|
+
w_col = W_block[:, i]
|
|
160
|
+
d = H_inv_block[i, i]
|
|
161
|
+
|
|
162
|
+
# Quantize individual column
|
|
163
|
+
q_col = quantize_dequantize_cols(w_col.unsqueeze(1)).squeeze(1)
|
|
164
|
+
Q_block[:, i] = q_col
|
|
165
|
+
|
|
166
|
+
# OBS column error
|
|
167
|
+
err = (w_col - q_col) / d.clamp(min=1e-7)
|
|
168
|
+
err_block[:, i] = err
|
|
169
|
+
|
|
170
|
+
# Propagate error to remaining columns in the current block
|
|
171
|
+
if i + 1 < count:
|
|
172
|
+
W_block[:, i + 1 :] -= err.unsqueeze(1) * H_inv_block[i, i + 1 :].unsqueeze(0)
|
|
173
|
+
|
|
174
|
+
Q[:, col_start:col_end] = Q_block
|
|
175
|
+
|
|
176
|
+
# Propagate block error to all remaining columns outside the block (GEMM)
|
|
177
|
+
if col_end < in_features:
|
|
178
|
+
# W[:, col_end:] -= err_block @ H_inv[col_start:col_end, col_end:]
|
|
179
|
+
W[:, col_end:] -= err_block @ H_inv[col_start:col_end, col_end:]
|
|
180
|
+
|
|
181
|
+
return Q.to(dtype=dtype)
|