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.
@@ -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)