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,143 @@
|
|
|
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
|
+
"""Sliding-window causal language model perplexity evaluation engine.
|
|
16
|
+
|
|
17
|
+
Mathematical Foundation:
|
|
18
|
+
Perplexity (PPL) is the standard metric for language model evaluation:
|
|
19
|
+
PPL = exp( - (1 / N) * sum_{t=1}^N log P(w_t | w_{<t}) )
|
|
20
|
+
|
|
21
|
+
Standard non-overlapping chunk evaluation splits text into discrete blocks of
|
|
22
|
+
length L, discarding past context for the initial tokens of every block and
|
|
23
|
+
artificially inflating measured perplexity.
|
|
24
|
+
|
|
25
|
+
The sliding-window approach uses a context window of length L and strides forward
|
|
26
|
+
by S < L tokens:
|
|
27
|
+
- The model sees the full preceding context of length L.
|
|
28
|
+
- Cross-entropy loss is evaluated strictly over the trailing S tokens.
|
|
29
|
+
- Boundary effects are eliminated, guaranteeing statistically rigorous
|
|
30
|
+
comparisons across FP16, INT8, INT4, and sparse model checkpoints.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
import math
|
|
36
|
+
from typing import Optional
|
|
37
|
+
|
|
38
|
+
import torch
|
|
39
|
+
import torch.nn as nn
|
|
40
|
+
import torch.nn.functional as F
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def compute_sliding_window_perplexity(
|
|
44
|
+
model: nn.Module,
|
|
45
|
+
input_ids: torch.Tensor,
|
|
46
|
+
window_size: int = 512,
|
|
47
|
+
stride: int = 256,
|
|
48
|
+
device: Optional[torch.device] = None,
|
|
49
|
+
) -> float:
|
|
50
|
+
"""Compute exact sliding-window perplexity for a causal language model.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
model: Causal language model producing logits of shape (B, S, V).
|
|
54
|
+
input_ids: 1D or 2D tensor of token IDs.
|
|
55
|
+
window_size: Maximum context window size L (e.g. 512 or 1024).
|
|
56
|
+
stride: Stride step S < L between consecutive evaluation windows (e.g. 256).
|
|
57
|
+
device: Device to run evaluation on.
|
|
58
|
+
|
|
59
|
+
Returns:
|
|
60
|
+
Perplexity value as a positive float.
|
|
61
|
+
"""
|
|
62
|
+
if stride >= window_size:
|
|
63
|
+
raise ValueError(
|
|
64
|
+
f"stride ({stride}) must be strictly less than window_size ({window_size})"
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
model.eval()
|
|
68
|
+
dev = device or next(model.parameters()).device
|
|
69
|
+
|
|
70
|
+
# Ensure 1D flat token vector
|
|
71
|
+
flat_tokens = input_ids.detach().view(-1).to(dev)
|
|
72
|
+
seq_len = flat_tokens.size(0)
|
|
73
|
+
|
|
74
|
+
if seq_len <= 1:
|
|
75
|
+
raise ValueError("input_ids must contain at least 2 tokens for causal language modeling.")
|
|
76
|
+
|
|
77
|
+
nlls: list[torch.Tensor] = []
|
|
78
|
+
total_eval_tokens = 0
|
|
79
|
+
|
|
80
|
+
prev_end_loc = 0
|
|
81
|
+
for begin_loc in range(0, seq_len, stride):
|
|
82
|
+
end_loc = min(begin_loc + window_size, seq_len)
|
|
83
|
+
trg_len = end_loc - prev_end_loc # Number of newly evaluated tokens in this window
|
|
84
|
+
|
|
85
|
+
input_chunk = flat_tokens[begin_loc:end_loc].unsqueeze(0) # (1, chunk_len)
|
|
86
|
+
target_chunk = input_chunk.clone()
|
|
87
|
+
|
|
88
|
+
# Mask out context tokens that were already evaluated in previous windows
|
|
89
|
+
target_chunk[:, :-trg_len] = -100
|
|
90
|
+
|
|
91
|
+
with torch.no_grad():
|
|
92
|
+
outputs = model(input_chunk)
|
|
93
|
+
logits = outputs.logits if hasattr(outputs, "logits") else outputs
|
|
94
|
+
|
|
95
|
+
# Shift logits and targets for next-token causal prediction:
|
|
96
|
+
# logits[:, :-1, :] predicts targets[:, 1:]
|
|
97
|
+
shift_logits = logits[:, :-1, :].contiguous()
|
|
98
|
+
shift_targets = target_chunk[:, 1:].contiguous()
|
|
99
|
+
|
|
100
|
+
# Cross entropy loss with ignore_index=-100
|
|
101
|
+
loss = F.cross_entropy(
|
|
102
|
+
shift_logits.view(-1, shift_logits.size(-1)),
|
|
103
|
+
shift_targets.view(-1),
|
|
104
|
+
ignore_index=-100,
|
|
105
|
+
reduction="sum",
|
|
106
|
+
)
|
|
107
|
+
|
|
108
|
+
nlls.append(loss)
|
|
109
|
+
# Count non-masked target tokens
|
|
110
|
+
num_valid = (shift_targets != -100).sum().item()
|
|
111
|
+
total_eval_tokens += num_valid
|
|
112
|
+
|
|
113
|
+
prev_end_loc = end_loc
|
|
114
|
+
if end_loc == seq_len:
|
|
115
|
+
break
|
|
116
|
+
|
|
117
|
+
if total_eval_tokens == 0:
|
|
118
|
+
return float("inf")
|
|
119
|
+
|
|
120
|
+
total_nll = torch.stack(nlls).sum().item()
|
|
121
|
+
mean_nll = total_nll / total_eval_tokens
|
|
122
|
+
perplexity = math.exp(mean_nll)
|
|
123
|
+
|
|
124
|
+
return float(perplexity)
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def generate_synthetic_corpus(
|
|
128
|
+
vocab_size: int = 1000,
|
|
129
|
+
seq_length: int = 1024,
|
|
130
|
+
seed: int = 42,
|
|
131
|
+
) -> torch.Tensor:
|
|
132
|
+
"""Generate a deterministic synthetic token sequence for offline testing.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
vocab_size: Vocabulary cardinality.
|
|
136
|
+
seq_length: Number of tokens to generate.
|
|
137
|
+
seed: Random seed for repeatability.
|
|
138
|
+
|
|
139
|
+
Returns:
|
|
140
|
+
Tensor of shape (seq_length,) containing token IDs.
|
|
141
|
+
"""
|
|
142
|
+
g = torch.Generator().manual_seed(seed)
|
|
143
|
+
return torch.randint(low=0, high=vocab_size, size=(seq_length,), generator=g)
|
quantbench/eval/sqnr.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
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
|
+
"""Signal-to-Quantization-Noise Ratio (SQNR) evaluation metrics."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import torch
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def compute_sqnr(
|
|
23
|
+
original: torch.Tensor,
|
|
24
|
+
quantized: torch.Tensor,
|
|
25
|
+
eps: float = 1e-12,
|
|
26
|
+
) -> float:
|
|
27
|
+
"""Compute Signal-to-Quantization-Noise Ratio (SQNR) in decibels (dB).
|
|
28
|
+
|
|
29
|
+
Mathematical Formulation:
|
|
30
|
+
SQNR (dB) = 10 * log10( P_signal / P_noise )
|
|
31
|
+
= 10 * log10( sum(|W|^2) / (sum(|W - W_hat|^2) + eps) )
|
|
32
|
+
|
|
33
|
+
Interpretation:
|
|
34
|
+
- Higher SQNR indicates superior numerical fidelity.
|
|
35
|
+
- FP16 -> INT8 typically achieves 40 dB to 55 dB.
|
|
36
|
+
- FP16 -> INT4 blockwise typically achieves 22 dB to 32 dB.
|
|
37
|
+
- Severe breakdown or NaN yields <= 0 dB.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
original: Reference unquantized floating-point tensor.
|
|
41
|
+
quantized: Reconstructed/dequantized floating-point tensor of matching shape.
|
|
42
|
+
eps: Numerical stability constant to prevent division by zero for exact equality.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Float value of SQNR in decibels (dB).
|
|
46
|
+
"""
|
|
47
|
+
if original.shape != quantized.shape:
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Shape mismatch in SQNR calculation: {original.shape} vs {quantized.shape}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
orig_f = original.detach().float()
|
|
53
|
+
quant_f = quantized.detach().float()
|
|
54
|
+
|
|
55
|
+
signal_power = torch.sum(orig_f.pow(2)).item()
|
|
56
|
+
noise_power = torch.sum((orig_f - quant_f).pow(2)).item()
|
|
57
|
+
|
|
58
|
+
if noise_power <= eps:
|
|
59
|
+
return 100.0 # Cap at +100 dB for near-lossless / exact reconstruction
|
|
60
|
+
|
|
61
|
+
ratio = signal_power / (noise_power + eps)
|
|
62
|
+
if ratio <= 0.0:
|
|
63
|
+
return 0.0
|
|
64
|
+
|
|
65
|
+
sqnr_db = 10.0 * torch.log10(torch.tensor(ratio)).item()
|
|
66
|
+
return float(sqnr_db)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
"""In-place model conversion runtime for PyTorch transformer architectures.
|
|
2
|
+
|
|
3
|
+
Recursively scans any PyTorch neural network (such as Hugging Face CausalLM models)
|
|
4
|
+
and replaces target torch.nn.Linear layers with QuantLinear modules.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
import torch.nn as nn
|
|
13
|
+
|
|
14
|
+
from quantbench.nn.quant_linear import QuantLinear
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def convert_model_to_quant(
|
|
18
|
+
model: nn.Module,
|
|
19
|
+
target_modules: list[str] | None = None,
|
|
20
|
+
exclude_modules: list[str] | None = None,
|
|
21
|
+
scheme: str = "int4",
|
|
22
|
+
group_size: int = 64,
|
|
23
|
+
symmetric: bool = True,
|
|
24
|
+
) -> tuple[nn.Module, dict[str, Any]]:
|
|
25
|
+
"""In-place recursive conversion of model linear layers to QuantLinear.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
model: PyTorch model (e.g. Hugging Face model or custom transformer).
|
|
29
|
+
target_modules: Optional list of substring names to convert (e.g. ['q_proj', 'v_proj']).
|
|
30
|
+
If None, all linear layers are targeted except those in exclude_modules.
|
|
31
|
+
exclude_modules: Module names to explicitly preserve unquantized (defaults to ['lm_head']).
|
|
32
|
+
scheme: Target quantization scheme ('int4', 'int8', 'ternary', 'sparse24').
|
|
33
|
+
group_size: Group size for blockwise quantization (-1 for per-channel).
|
|
34
|
+
symmetric: Whether to use symmetric quantization.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Tuple of (converted_model, conversion_statistics_dictionary).
|
|
38
|
+
"""
|
|
39
|
+
if exclude_modules is None:
|
|
40
|
+
exclude_modules = ["lm_head"]
|
|
41
|
+
|
|
42
|
+
t0 = time.perf_counter()
|
|
43
|
+
|
|
44
|
+
# Calculate pre-conversion memory
|
|
45
|
+
orig_bytes = sum(p.nelement() * p.element_size() for p in model.parameters())
|
|
46
|
+
|
|
47
|
+
converted_count = 0
|
|
48
|
+
skipped_count = 0
|
|
49
|
+
layer_names: list[str] = []
|
|
50
|
+
|
|
51
|
+
# Helper function to recursively replace layers
|
|
52
|
+
def _replace_layers(parent: nn.Module, prefix: str = ""):
|
|
53
|
+
nonlocal converted_count, skipped_count
|
|
54
|
+
for name, child in list(parent.named_children()):
|
|
55
|
+
full_name = f"{prefix}.{name}" if prefix else name
|
|
56
|
+
|
|
57
|
+
# Check if this child is an explicit exclusion
|
|
58
|
+
if any(exc in full_name for exc in exclude_modules):
|
|
59
|
+
skipped_count += 1
|
|
60
|
+
continue
|
|
61
|
+
|
|
62
|
+
if isinstance(child, nn.Linear):
|
|
63
|
+
# Check if target matches
|
|
64
|
+
if target_modules is not None:
|
|
65
|
+
is_target = any(tgt in full_name for tgt in target_modules)
|
|
66
|
+
else:
|
|
67
|
+
is_target = True
|
|
68
|
+
|
|
69
|
+
if is_target:
|
|
70
|
+
# Convert to QuantLinear
|
|
71
|
+
quant_child = QuantLinear.from_linear(
|
|
72
|
+
child,
|
|
73
|
+
scheme=scheme,
|
|
74
|
+
group_size=group_size,
|
|
75
|
+
symmetric=symmetric,
|
|
76
|
+
)
|
|
77
|
+
setattr(parent, name, quant_child)
|
|
78
|
+
converted_count += 1
|
|
79
|
+
layer_names.append(full_name)
|
|
80
|
+
else:
|
|
81
|
+
skipped_count += 1
|
|
82
|
+
else:
|
|
83
|
+
_replace_layers(child, full_name)
|
|
84
|
+
|
|
85
|
+
_replace_layers(model)
|
|
86
|
+
|
|
87
|
+
dt = time.perf_counter() - t0
|
|
88
|
+
|
|
89
|
+
# Calculate post-conversion memory (buffers + parameters)
|
|
90
|
+
quant_bytes = sum(p.nelement() * p.element_size() for p in model.parameters())
|
|
91
|
+
quant_bytes += sum(b.nelement() * b.element_size() for b in model.buffers())
|
|
92
|
+
|
|
93
|
+
orig_mb = orig_bytes / (1024 * 1024)
|
|
94
|
+
quant_mb = quant_bytes / (1024 * 1024)
|
|
95
|
+
ratio = orig_mb / max(quant_mb, 1e-6)
|
|
96
|
+
|
|
97
|
+
stats = {
|
|
98
|
+
"converted_layers": converted_count,
|
|
99
|
+
"skipped_layers": skipped_count,
|
|
100
|
+
"scheme": scheme,
|
|
101
|
+
"group_size": group_size,
|
|
102
|
+
"orig_memory_mb": round(orig_mb, 2),
|
|
103
|
+
"quant_memory_mb": round(quant_mb, 2),
|
|
104
|
+
"compression_ratio": round(ratio, 2),
|
|
105
|
+
"duration_sec": round(dt, 4),
|
|
106
|
+
"layer_names": layer_names,
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return model, stats
|
|
@@ -0,0 +1,296 @@
|
|
|
1
|
+
"""QuantLinear: Drop-in PyTorch Linear layer with sub-byte packed weights.
|
|
2
|
+
|
|
3
|
+
Replaces standard torch.nn.Linear by storing weights as packed integer buffers:
|
|
4
|
+
- INT4: Vectorized uint8 packed nibbles (2 weights/byte) + blockwise/channel scales.
|
|
5
|
+
- BitNet Ternary: 2-bit offset-binary packed uint8 buffers (4 weights/byte) + scale.
|
|
6
|
+
- 2:4 Sparsity: 50% condensed float values + 2-bit position metadata nibbles.
|
|
7
|
+
- INT8: Signed 8-bit integers + per-channel or per-tensor scales.
|
|
8
|
+
|
|
9
|
+
Weights are registered as persistent buffers rather than trainable nn.Parameters,
|
|
10
|
+
eliminating optimizer memory overhead and enabling direct serialization into safetensors.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
import torch
|
|
18
|
+
import torch.nn as nn
|
|
19
|
+
import torch.nn.functional as F
|
|
20
|
+
|
|
21
|
+
from quantbench.quant.affine import (
|
|
22
|
+
dequantize_absmax,
|
|
23
|
+
dequantize_affine,
|
|
24
|
+
quantize_absmax,
|
|
25
|
+
quantize_affine,
|
|
26
|
+
)
|
|
27
|
+
from quantbench.quant.blockwise import dequantize_blockwise, quantize_blockwise
|
|
28
|
+
from quantbench.quant.packing import (
|
|
29
|
+
decompress_2_4,
|
|
30
|
+
pack_int4,
|
|
31
|
+
pack_ternary,
|
|
32
|
+
prune_and_compress_2_4,
|
|
33
|
+
unpack_int4,
|
|
34
|
+
unpack_ternary,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class QuantLinear(nn.Module):
|
|
39
|
+
"""Drop-in quantized linear layer storing packed weights in memory buffers.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
in_features: Size of each input sample.
|
|
43
|
+
out_features: Size of each output sample.
|
|
44
|
+
scheme: Quantization scheme: 'int4', 'int8', 'ternary', 'sparse24'.
|
|
45
|
+
group_size: Group size for blockwise quantization (-1 for per-channel).
|
|
46
|
+
bias: Whether to include an additive bias parameter.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
in_features: int,
|
|
52
|
+
out_features: int,
|
|
53
|
+
scheme: str = "int4",
|
|
54
|
+
group_size: int = 64,
|
|
55
|
+
bias: bool = True,
|
|
56
|
+
) -> None:
|
|
57
|
+
super().__init__()
|
|
58
|
+
self.in_features = in_features
|
|
59
|
+
self.out_features = out_features
|
|
60
|
+
self.scheme = scheme.lower()
|
|
61
|
+
self.group_size = group_size
|
|
62
|
+
|
|
63
|
+
if self.scheme not in ("int4", "int8", "ternary", "sparse24"):
|
|
64
|
+
raise ValueError(
|
|
65
|
+
f"Unknown scheme '{scheme}'. Supported: 'int4', 'int8', 'ternary', 'sparse24'."
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
# Register bias as parameter if present
|
|
69
|
+
if bias:
|
|
70
|
+
self.bias = nn.Parameter(torch.zeros(out_features))
|
|
71
|
+
else:
|
|
72
|
+
self.register_parameter("bias", None)
|
|
73
|
+
|
|
74
|
+
# Buffers are allocated upon calling from_linear or loading state_dict
|
|
75
|
+
self.register_buffer("packed_weight", None)
|
|
76
|
+
self.register_buffer("scales", None)
|
|
77
|
+
self.register_buffer("zero_points", None)
|
|
78
|
+
self.register_buffer("condensed_weight", None)
|
|
79
|
+
self.register_buffer("packed_metadata", None)
|
|
80
|
+
|
|
81
|
+
def _load_from_state_dict(
|
|
82
|
+
self,
|
|
83
|
+
state_dict: dict[str, Any],
|
|
84
|
+
prefix: str,
|
|
85
|
+
local_metadata: dict[str, Any],
|
|
86
|
+
strict: bool,
|
|
87
|
+
missing_keys: list[str],
|
|
88
|
+
unexpected_keys: list[str],
|
|
89
|
+
error_msgs: list[str],
|
|
90
|
+
) -> None:
|
|
91
|
+
"""Register incoming serialized buffer tensors dynamically during load_state_dict."""
|
|
92
|
+
for buf_name in (
|
|
93
|
+
"packed_weight",
|
|
94
|
+
"scales",
|
|
95
|
+
"zero_points",
|
|
96
|
+
"condensed_weight",
|
|
97
|
+
"packed_metadata",
|
|
98
|
+
):
|
|
99
|
+
full_key = prefix + buf_name
|
|
100
|
+
if full_key in state_dict and state_dict[full_key] is not None:
|
|
101
|
+
self.register_buffer(buf_name, state_dict[full_key])
|
|
102
|
+
super()._load_from_state_dict(
|
|
103
|
+
state_dict,
|
|
104
|
+
prefix,
|
|
105
|
+
local_metadata,
|
|
106
|
+
strict,
|
|
107
|
+
missing_keys,
|
|
108
|
+
unexpected_keys,
|
|
109
|
+
error_msgs,
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
@classmethod
|
|
113
|
+
def from_linear(
|
|
114
|
+
cls,
|
|
115
|
+
linear: nn.Linear,
|
|
116
|
+
scheme: str = "int4",
|
|
117
|
+
group_size: int = 64,
|
|
118
|
+
symmetric: bool = True,
|
|
119
|
+
) -> QuantLinear:
|
|
120
|
+
"""Create a QuantLinear module from an unquantized torch.nn.Linear layer.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
linear: Source torch.nn.Linear layer.
|
|
124
|
+
scheme: Target quantization scheme: 'int4', 'int8', 'ternary', 'sparse24'.
|
|
125
|
+
group_size: Group size for blockwise quantization (e.g. 32, 64, 128, or -1).
|
|
126
|
+
symmetric: Whether to use symmetric quantization (zero-point = 0).
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
QuantLinear instance with compressed weight buffers.
|
|
130
|
+
"""
|
|
131
|
+
in_features = linear.in_features
|
|
132
|
+
out_features = linear.out_features
|
|
133
|
+
has_bias = linear.bias is not None
|
|
134
|
+
|
|
135
|
+
quant_layer = cls(
|
|
136
|
+
in_features=in_features,
|
|
137
|
+
out_features=out_features,
|
|
138
|
+
scheme=scheme,
|
|
139
|
+
group_size=group_size,
|
|
140
|
+
bias=has_bias,
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
w = linear.weight.data.clone().to(torch.float32)
|
|
144
|
+
if has_bias:
|
|
145
|
+
quant_layer.bias.data.copy_(linear.bias.data)
|
|
146
|
+
|
|
147
|
+
scheme_lower = scheme.lower()
|
|
148
|
+
|
|
149
|
+
if scheme_lower == "int4":
|
|
150
|
+
if group_size > 0:
|
|
151
|
+
# Group-wise INT4
|
|
152
|
+
q_blocks, s, zp, _ = quantize_blockwise(
|
|
153
|
+
w, group_size=group_size, num_bits=4, symmetric=symmetric
|
|
154
|
+
)
|
|
155
|
+
# Pack each block along dim=-1
|
|
156
|
+
packed_w, _ = pack_int4(q_blocks, signed=symmetric, dim=-1)
|
|
157
|
+
quant_layer.register_buffer("packed_weight", packed_w)
|
|
158
|
+
quant_layer.register_buffer("scales", s)
|
|
159
|
+
if zp is not None:
|
|
160
|
+
quant_layer.register_buffer("zero_points", zp)
|
|
161
|
+
else:
|
|
162
|
+
# Per-channel INT4 along output channels (reduce across input dim 1)
|
|
163
|
+
if symmetric:
|
|
164
|
+
q_w, s = quantize_absmax(w, num_bits=4, dim=1)
|
|
165
|
+
zp = None
|
|
166
|
+
else:
|
|
167
|
+
q_w, s, zp = quantize_affine(w, num_bits=4, dim=1)
|
|
168
|
+
packed_w, _ = pack_int4(q_w, signed=symmetric, dim=-1)
|
|
169
|
+
quant_layer.register_buffer("packed_weight", packed_w)
|
|
170
|
+
quant_layer.register_buffer("scales", s)
|
|
171
|
+
if zp is not None:
|
|
172
|
+
quant_layer.register_buffer("zero_points", zp)
|
|
173
|
+
|
|
174
|
+
elif scheme_lower == "int8":
|
|
175
|
+
if symmetric:
|
|
176
|
+
q_w, s = quantize_absmax(w, num_bits=8, dim=1)
|
|
177
|
+
zp = None
|
|
178
|
+
else:
|
|
179
|
+
q_w, s, zp = quantize_affine(w, num_bits=8, dim=1)
|
|
180
|
+
quant_layer.register_buffer("packed_weight", q_w.to(torch.int8))
|
|
181
|
+
quant_layer.register_buffer("scales", s)
|
|
182
|
+
if zp is not None:
|
|
183
|
+
quant_layer.register_buffer("zero_points", zp)
|
|
184
|
+
|
|
185
|
+
elif scheme_lower == "ternary":
|
|
186
|
+
# BitNet ternary scaling: gamma = mean(|W|)
|
|
187
|
+
gamma = w.abs().mean().clamp(min=1e-8)
|
|
188
|
+
scaled_w = w / gamma
|
|
189
|
+
ternary_w = torch.clamp(torch.round(scaled_w), -1.0, 1.0)
|
|
190
|
+
packed_w, _ = pack_ternary(ternary_w, mode="2bit", dim=-1)
|
|
191
|
+
quant_layer.register_buffer("packed_weight", packed_w)
|
|
192
|
+
quant_layer.register_buffer("scales", gamma.view(1))
|
|
193
|
+
|
|
194
|
+
elif scheme_lower in ("sparse24", "2:4"):
|
|
195
|
+
condensed, metadata = prune_and_compress_2_4(w)
|
|
196
|
+
quant_layer.register_buffer("condensed_weight", condensed)
|
|
197
|
+
quant_layer.register_buffer("packed_metadata", metadata)
|
|
198
|
+
|
|
199
|
+
return quant_layer
|
|
200
|
+
|
|
201
|
+
def dequantize_weight(self) -> torch.Tensor:
|
|
202
|
+
"""Dequantize packed buffers back to a standard floating-point weight matrix."""
|
|
203
|
+
if self.scheme == "int4":
|
|
204
|
+
is_signed = self.zero_points is None
|
|
205
|
+
if self.group_size > 0:
|
|
206
|
+
# Group-wise reconstruction
|
|
207
|
+
unpacked_q = unpack_int4(
|
|
208
|
+
self.packed_weight, original_len=self.group_size, signed=is_signed, dim=-1
|
|
209
|
+
)
|
|
210
|
+
n_elements = self.out_features * self.in_features
|
|
211
|
+
remainder = n_elements % self.group_size
|
|
212
|
+
pad_len = (self.group_size - remainder) % self.group_size
|
|
213
|
+
meta = {
|
|
214
|
+
"original_shape": (self.out_features, self.in_features),
|
|
215
|
+
"n_elements": n_elements,
|
|
216
|
+
"pad_len": pad_len,
|
|
217
|
+
"group_size": self.group_size,
|
|
218
|
+
"symmetric": is_signed,
|
|
219
|
+
"dtype": torch.float32,
|
|
220
|
+
"device": self.packed_weight.device,
|
|
221
|
+
}
|
|
222
|
+
zp = self.zero_points if self.zero_points is not None else None
|
|
223
|
+
return dequantize_blockwise(unpacked_q, self.scales, zp, meta)
|
|
224
|
+
else:
|
|
225
|
+
# Per-channel reconstruction
|
|
226
|
+
unpacked_q = unpack_int4(
|
|
227
|
+
self.packed_weight, original_len=self.in_features, signed=is_signed, dim=-1
|
|
228
|
+
)
|
|
229
|
+
if self.zero_points is not None:
|
|
230
|
+
return dequantize_affine(unpacked_q, self.scales, self.zero_points)
|
|
231
|
+
else:
|
|
232
|
+
return dequantize_absmax(unpacked_q, self.scales)
|
|
233
|
+
|
|
234
|
+
elif self.scheme == "int8":
|
|
235
|
+
if self.zero_points is not None:
|
|
236
|
+
return dequantize_affine(self.packed_weight, self.scales, self.zero_points)
|
|
237
|
+
else:
|
|
238
|
+
return dequantize_absmax(self.packed_weight, self.scales)
|
|
239
|
+
|
|
240
|
+
elif self.scheme == "ternary":
|
|
241
|
+
unpacked_t = unpack_ternary(
|
|
242
|
+
self.packed_weight, original_len=self.in_features, mode="2bit", dim=-1
|
|
243
|
+
)
|
|
244
|
+
return unpacked_t.to(torch.float32) * self.scales
|
|
245
|
+
|
|
246
|
+
elif self.scheme in ("sparse24", "2:4"):
|
|
247
|
+
return decompress_2_4(
|
|
248
|
+
self.condensed_weight,
|
|
249
|
+
self.packed_metadata,
|
|
250
|
+
original_shape=(self.out_features, self.in_features),
|
|
251
|
+
)
|
|
252
|
+
|
|
253
|
+
raise RuntimeError(f"Unsupported scheme '{self.scheme}'.")
|
|
254
|
+
|
|
255
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
256
|
+
"""Forward pass with dynamic vector dequantization.
|
|
257
|
+
|
|
258
|
+
Args:
|
|
259
|
+
x: Input activation tensor with shape (..., in_features).
|
|
260
|
+
|
|
261
|
+
Returns:
|
|
262
|
+
Projected activation tensor with shape (..., out_features).
|
|
263
|
+
"""
|
|
264
|
+
w_hat = self.dequantize_weight().to(x.dtype)
|
|
265
|
+
return F.linear(x, w_hat, self.bias)
|
|
266
|
+
|
|
267
|
+
def memory_footprint_bytes(self) -> dict[str, float]:
|
|
268
|
+
"""Compute exact memory footprint in bytes and compression factor relative to FP16.
|
|
269
|
+
|
|
270
|
+
Returns:
|
|
271
|
+
Dictionary containing 'compressed_bytes', 'fp16_bytes', and 'compression_ratio'.
|
|
272
|
+
"""
|
|
273
|
+
compressed_bytes = 0
|
|
274
|
+
for buf in self.buffers():
|
|
275
|
+
if buf is not None:
|
|
276
|
+
compressed_bytes += buf.nelement() * buf.element_size()
|
|
277
|
+
if self.bias is not None:
|
|
278
|
+
compressed_bytes += self.bias.nelement() * self.bias.element_size()
|
|
279
|
+
|
|
280
|
+
fp16_bytes = (self.in_features * self.out_features * 2) + (
|
|
281
|
+
(self.out_features * 2) if self.bias is not None else 0
|
|
282
|
+
)
|
|
283
|
+
ratio = fp16_bytes / max(compressed_bytes, 1)
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
"compressed_bytes": compressed_bytes,
|
|
287
|
+
"fp16_bytes": fp16_bytes,
|
|
288
|
+
"compression_ratio": ratio,
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
def extra_repr(self) -> str:
|
|
292
|
+
return (
|
|
293
|
+
f"in_features={self.in_features}, out_features={self.out_features}, "
|
|
294
|
+
f"scheme={self.scheme}, group_size={self.group_size}, "
|
|
295
|
+
f"bias={self.bias is not None}"
|
|
296
|
+
)
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"""First-principles quantization primitives, algorithms, and bit-packing runtimes."""
|
|
2
|
+
|
|
3
|
+
from quantbench.quant.affine import (
|
|
4
|
+
dequantize_absmax,
|
|
5
|
+
dequantize_affine,
|
|
6
|
+
quantize_absmax,
|
|
7
|
+
quantize_affine,
|
|
8
|
+
)
|
|
9
|
+
from quantbench.quant.algorithms import (
|
|
10
|
+
awq_quantize_weights,
|
|
11
|
+
compute_activation_channel_means,
|
|
12
|
+
compute_activation_hessian,
|
|
13
|
+
compute_smoothquant_scale,
|
|
14
|
+
extract_activation_channel_scales,
|
|
15
|
+
gptq_quantize_weights,
|
|
16
|
+
search_best_awq_scale,
|
|
17
|
+
smooth_linear_weights,
|
|
18
|
+
)
|
|
19
|
+
from quantbench.quant.blockwise import dequantize_blockwise, quantize_blockwise
|
|
20
|
+
from quantbench.quant.fp8 import FP8Format, dequantize_fp8, quantize_fp8
|
|
21
|
+
from quantbench.quant.packing import (
|
|
22
|
+
decompress_2_4,
|
|
23
|
+
pack_2_4_indices,
|
|
24
|
+
pack_int4,
|
|
25
|
+
pack_ternary,
|
|
26
|
+
prune_and_compress_2_4,
|
|
27
|
+
unpack_2_4_indices,
|
|
28
|
+
unpack_int4,
|
|
29
|
+
unpack_ternary,
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
__all__ = [
|
|
33
|
+
"quantize_affine",
|
|
34
|
+
"dequantize_affine",
|
|
35
|
+
"quantize_absmax",
|
|
36
|
+
"dequantize_absmax",
|
|
37
|
+
"quantize_blockwise",
|
|
38
|
+
"dequantize_blockwise",
|
|
39
|
+
"quantize_fp8",
|
|
40
|
+
"dequantize_fp8",
|
|
41
|
+
"FP8Format",
|
|
42
|
+
"pack_int4",
|
|
43
|
+
"unpack_int4",
|
|
44
|
+
"pack_ternary",
|
|
45
|
+
"unpack_ternary",
|
|
46
|
+
"pack_2_4_indices",
|
|
47
|
+
"unpack_2_4_indices",
|
|
48
|
+
"prune_and_compress_2_4",
|
|
49
|
+
"decompress_2_4",
|
|
50
|
+
"compute_smoothquant_scale",
|
|
51
|
+
"extract_activation_channel_scales",
|
|
52
|
+
"smooth_linear_weights",
|
|
53
|
+
"compute_activation_channel_means",
|
|
54
|
+
"search_best_awq_scale",
|
|
55
|
+
"awq_quantize_weights",
|
|
56
|
+
"compute_activation_hessian",
|
|
57
|
+
"gptq_quantize_weights",
|
|
58
|
+
]
|