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
quantbench/__init__.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""quantbench: Unified Quantization Engine, Bit-Packing Runtime & Quality-vs-Speed Benchmark."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from quantbench.bench import ParetoBenchmark, ParetoPoint
|
|
6
|
+
from quantbench.eval import (
|
|
7
|
+
compute_sliding_window_perplexity,
|
|
8
|
+
compute_sqnr,
|
|
9
|
+
generate_synthetic_corpus,
|
|
10
|
+
)
|
|
11
|
+
from quantbench.nn.model_convert import convert_model_to_quant
|
|
12
|
+
from quantbench.nn.quant_linear import QuantLinear
|
|
13
|
+
from quantbench.quant.affine import (
|
|
14
|
+
dequantize_absmax,
|
|
15
|
+
dequantize_affine,
|
|
16
|
+
quantize_absmax,
|
|
17
|
+
quantize_affine,
|
|
18
|
+
)
|
|
19
|
+
from quantbench.quant.algorithms import (
|
|
20
|
+
awq_quantize_weights,
|
|
21
|
+
compute_activation_channel_means,
|
|
22
|
+
compute_activation_hessian,
|
|
23
|
+
compute_smoothquant_scale,
|
|
24
|
+
extract_activation_channel_scales,
|
|
25
|
+
gptq_quantize_weights,
|
|
26
|
+
search_best_awq_scale,
|
|
27
|
+
smooth_linear_weights,
|
|
28
|
+
)
|
|
29
|
+
from quantbench.quant.blockwise import dequantize_blockwise, quantize_blockwise
|
|
30
|
+
from quantbench.quant.fp8 import FP8Format, dequantize_fp8, quantize_fp8
|
|
31
|
+
from quantbench.quant.packing import (
|
|
32
|
+
decompress_2_4,
|
|
33
|
+
pack_2_4_indices,
|
|
34
|
+
pack_int4,
|
|
35
|
+
pack_ternary,
|
|
36
|
+
prune_and_compress_2_4,
|
|
37
|
+
unpack_2_4_indices,
|
|
38
|
+
unpack_int4,
|
|
39
|
+
unpack_ternary,
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
__all__ = [
|
|
43
|
+
"__version__",
|
|
44
|
+
"QuantLinear",
|
|
45
|
+
"convert_model_to_quant",
|
|
46
|
+
"quantize_affine",
|
|
47
|
+
"dequantize_affine",
|
|
48
|
+
"quantize_absmax",
|
|
49
|
+
"dequantize_absmax",
|
|
50
|
+
"quantize_blockwise",
|
|
51
|
+
"dequantize_blockwise",
|
|
52
|
+
"quantize_fp8",
|
|
53
|
+
"dequantize_fp8",
|
|
54
|
+
"FP8Format",
|
|
55
|
+
"pack_int4",
|
|
56
|
+
"unpack_int4",
|
|
57
|
+
"pack_ternary",
|
|
58
|
+
"unpack_ternary",
|
|
59
|
+
"pack_2_4_indices",
|
|
60
|
+
"unpack_2_4_indices",
|
|
61
|
+
"prune_and_compress_2_4",
|
|
62
|
+
"decompress_2_4",
|
|
63
|
+
"compute_smoothquant_scale",
|
|
64
|
+
"extract_activation_channel_scales",
|
|
65
|
+
"smooth_linear_weights",
|
|
66
|
+
"compute_activation_channel_means",
|
|
67
|
+
"search_best_awq_scale",
|
|
68
|
+
"awq_quantize_weights",
|
|
69
|
+
"compute_activation_hessian",
|
|
70
|
+
"gptq_quantize_weights",
|
|
71
|
+
"compute_sqnr",
|
|
72
|
+
"compute_sliding_window_perplexity",
|
|
73
|
+
"generate_synthetic_corpus",
|
|
74
|
+
"ParetoBenchmark",
|
|
75
|
+
"ParetoPoint",
|
|
76
|
+
]
|
|
@@ -0,0 +1,22 @@
|
|
|
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
|
+
"""Benchmarking tools and Pareto frontier evaluation."""
|
|
16
|
+
|
|
17
|
+
from quantbench.bench.pareto import ParetoBenchmark, ParetoPoint
|
|
18
|
+
|
|
19
|
+
__all__ = [
|
|
20
|
+
"ParetoBenchmark",
|
|
21
|
+
"ParetoPoint",
|
|
22
|
+
]
|
|
@@ -0,0 +1,182 @@
|
|
|
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
|
+
"""Pareto evaluation engine: Quality vs Compression trade-off analysis."""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import copy
|
|
20
|
+
from dataclasses import dataclass
|
|
21
|
+
from typing import Any, Dict, List, Optional
|
|
22
|
+
|
|
23
|
+
import torch
|
|
24
|
+
import torch.nn as nn
|
|
25
|
+
|
|
26
|
+
from quantbench.eval.perplexity import compute_sliding_window_perplexity
|
|
27
|
+
from quantbench.eval.sqnr import compute_sqnr
|
|
28
|
+
from quantbench.nn.model_convert import convert_model_to_quant
|
|
29
|
+
from quantbench.nn.quant_linear import QuantLinear
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class ParetoPoint:
|
|
34
|
+
"""Single configuration measurement on the Pareto frontier."""
|
|
35
|
+
|
|
36
|
+
name: str
|
|
37
|
+
scheme: str
|
|
38
|
+
group_size: int
|
|
39
|
+
compression_ratio: float
|
|
40
|
+
memory_mb: float
|
|
41
|
+
perplexity: float
|
|
42
|
+
perplexity_delta: float
|
|
43
|
+
avg_sqnr_db: float
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class ParetoBenchmark:
|
|
47
|
+
"""Benchmark model configurations across precision, memory, and perplexity frontiers."""
|
|
48
|
+
|
|
49
|
+
def __init__(
|
|
50
|
+
self,
|
|
51
|
+
base_model: nn.Module,
|
|
52
|
+
test_tokens: torch.Tensor,
|
|
53
|
+
window_size: int = 256,
|
|
54
|
+
stride: int = 128,
|
|
55
|
+
) -> None:
|
|
56
|
+
self.base_model = base_model
|
|
57
|
+
self.test_tokens = test_tokens
|
|
58
|
+
self.window_size = window_size
|
|
59
|
+
self.stride = stride
|
|
60
|
+
|
|
61
|
+
def run_sweep(
|
|
62
|
+
self,
|
|
63
|
+
schemes: Optional[List[Dict[str, Any]]] = None,
|
|
64
|
+
exclude_modules: Optional[List[str]] = None,
|
|
65
|
+
) -> List[ParetoPoint]:
|
|
66
|
+
"""Run Pareto sweep across specified quantization schemes."""
|
|
67
|
+
if schemes is None:
|
|
68
|
+
schemes = [
|
|
69
|
+
{"name": "FP16 Baseline", "scheme": "fp16", "group_size": -1},
|
|
70
|
+
{"name": "INT8 AbsMax", "scheme": "int8", "group_size": -1},
|
|
71
|
+
{"name": "INT4 (G=128)", "scheme": "int4", "group_size": 128},
|
|
72
|
+
{"name": "INT4 (G=64)", "scheme": "int4", "group_size": 64},
|
|
73
|
+
{"name": "BitNet Ternary", "scheme": "ternary", "group_size": -1},
|
|
74
|
+
{"name": "Ampere 2:4 Sparse", "scheme": "sparse24", "group_size": -1},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
# 1. Evaluate baseline FP16
|
|
78
|
+
base_ppl = compute_sliding_window_perplexity(
|
|
79
|
+
self.base_model,
|
|
80
|
+
self.test_tokens,
|
|
81
|
+
window_size=self.window_size,
|
|
82
|
+
stride=self.stride,
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
base_bytes = sum(
|
|
86
|
+
p.numel() * 2 for p in self.base_model.parameters()
|
|
87
|
+
)
|
|
88
|
+
base_mb = base_bytes / (1024 * 1024)
|
|
89
|
+
|
|
90
|
+
points: List[ParetoPoint] = []
|
|
91
|
+
|
|
92
|
+
for cfg in schemes:
|
|
93
|
+
name = cfg["name"]
|
|
94
|
+
scheme = cfg["scheme"]
|
|
95
|
+
group_size = cfg.get("group_size", 64)
|
|
96
|
+
|
|
97
|
+
if scheme == "fp16":
|
|
98
|
+
points.append(
|
|
99
|
+
ParetoPoint(
|
|
100
|
+
name=name,
|
|
101
|
+
scheme=scheme,
|
|
102
|
+
group_size=group_size,
|
|
103
|
+
compression_ratio=1.0,
|
|
104
|
+
memory_mb=round(base_mb, 2),
|
|
105
|
+
perplexity=round(base_ppl, 3),
|
|
106
|
+
perplexity_delta=0.0,
|
|
107
|
+
avg_sqnr_db=100.0,
|
|
108
|
+
)
|
|
109
|
+
)
|
|
110
|
+
continue
|
|
111
|
+
|
|
112
|
+
# Clone base model for isolated in-place conversion
|
|
113
|
+
cloned_model = copy.deepcopy(self.base_model)
|
|
114
|
+
|
|
115
|
+
# Extract unquantized linear weights for layer-level SQNR comparison
|
|
116
|
+
orig_weights: Dict[str, torch.Tensor] = {}
|
|
117
|
+
for m_name, module in cloned_model.named_modules():
|
|
118
|
+
if isinstance(module, nn.Linear):
|
|
119
|
+
orig_weights[m_name] = module.weight.detach().clone()
|
|
120
|
+
|
|
121
|
+
# In-place quantization
|
|
122
|
+
_, stats = convert_model_to_quant(
|
|
123
|
+
cloned_model,
|
|
124
|
+
scheme=scheme,
|
|
125
|
+
group_size=group_size,
|
|
126
|
+
exclude_modules=exclude_modules,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
# Compute layer-level SQNR
|
|
130
|
+
sqnrs: List[float] = []
|
|
131
|
+
for m_name, module in cloned_model.named_modules():
|
|
132
|
+
if isinstance(module, QuantLinear) and m_name in orig_weights:
|
|
133
|
+
dequant_w = module.dequantize_weight()
|
|
134
|
+
sqnrs.append(compute_sqnr(orig_weights[m_name], dequant_w))
|
|
135
|
+
|
|
136
|
+
avg_sqnr = sum(sqnrs) / len(sqnrs) if sqnrs else 0.0
|
|
137
|
+
|
|
138
|
+
# Evaluate quantized perplexity
|
|
139
|
+
q_ppl = compute_sliding_window_perplexity(
|
|
140
|
+
cloned_model,
|
|
141
|
+
self.test_tokens,
|
|
142
|
+
window_size=self.window_size,
|
|
143
|
+
stride=self.stride,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
points.append(
|
|
147
|
+
ParetoPoint(
|
|
148
|
+
name=name,
|
|
149
|
+
scheme=scheme,
|
|
150
|
+
group_size=group_size,
|
|
151
|
+
compression_ratio=round(stats["compression_ratio"], 2),
|
|
152
|
+
memory_mb=round(stats["quant_memory_mb"], 2),
|
|
153
|
+
perplexity=round(q_ppl, 3),
|
|
154
|
+
perplexity_delta=round(q_ppl - base_ppl, 3),
|
|
155
|
+
avg_sqnr_db=round(avg_sqnr, 1),
|
|
156
|
+
)
|
|
157
|
+
)
|
|
158
|
+
|
|
159
|
+
return points
|
|
160
|
+
|
|
161
|
+
@staticmethod
|
|
162
|
+
def render_ascii_table(points: List[ParetoPoint]) -> str:
|
|
163
|
+
"""Render a clean GitHub Markdown / terminal ASCII table of the Pareto frontier."""
|
|
164
|
+
lines = [
|
|
165
|
+
"| Configuration | Scheme | Group | Memory (MB) | Compression "
|
|
166
|
+
"| Perplexity | Delta PPL | SQNR (dB) |",
|
|
167
|
+
"|---|---|---|---|---|---|---|---|",
|
|
168
|
+
]
|
|
169
|
+
for p in points:
|
|
170
|
+
grp = str(p.group_size) if p.group_size > 0 else "None"
|
|
171
|
+
delta = (
|
|
172
|
+
f"+{p.perplexity_delta:.3f}"
|
|
173
|
+
if p.perplexity_delta > 0
|
|
174
|
+
else f"{p.perplexity_delta:.3f}"
|
|
175
|
+
)
|
|
176
|
+
row = (
|
|
177
|
+
f"| {p.name:<18} | {p.scheme:<8} | {grp:<5} | {p.memory_mb:>10.2f} | "
|
|
178
|
+
f"{p.compression_ratio:>10.2f}x | {p.perplexity:>10.3f} | {delta:>9} | "
|
|
179
|
+
f"{p.avg_sqnr_db:>8.1f} |"
|
|
180
|
+
)
|
|
181
|
+
lines.append(row)
|
|
182
|
+
return "\n".join(lines)
|
quantbench/cli.py
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
"""Command Line Interface for quantbench."""
|
|
2
|
+
|
|
3
|
+
import typer
|
|
4
|
+
from rich.console import Console
|
|
5
|
+
from rich.panel import Panel
|
|
6
|
+
from rich.table import Table
|
|
7
|
+
|
|
8
|
+
import quantbench
|
|
9
|
+
from quantbench.quant.packing import (
|
|
10
|
+
pack_int4,
|
|
11
|
+
pack_ternary,
|
|
12
|
+
prune_and_compress_2_4,
|
|
13
|
+
unpack_int4,
|
|
14
|
+
unpack_ternary,
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
app = typer.Typer(
|
|
18
|
+
name="quantbench",
|
|
19
|
+
help="First-principles LLM quantization engine and Pareto benchmark.",
|
|
20
|
+
add_completion=False,
|
|
21
|
+
)
|
|
22
|
+
console = Console()
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def version_callback(value: bool):
|
|
26
|
+
if value:
|
|
27
|
+
ver = quantbench.__version__
|
|
28
|
+
console.print(f"[bold cyan]quantbench[/bold cyan] version [bold green]{ver}[/bold green]")
|
|
29
|
+
raise typer.Exit()
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.callback()
|
|
33
|
+
def main(
|
|
34
|
+
version: bool = typer.Option(
|
|
35
|
+
None,
|
|
36
|
+
"--version",
|
|
37
|
+
"-v",
|
|
38
|
+
help="Show version and exit.",
|
|
39
|
+
callback=version_callback,
|
|
40
|
+
is_eager=True,
|
|
41
|
+
),
|
|
42
|
+
):
|
|
43
|
+
"""quantbench CLI root."""
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@app.command()
|
|
48
|
+
def info():
|
|
49
|
+
"""Display system and supported quantization capabilities."""
|
|
50
|
+
table = Table(title="quantbench Capabilities & Formats", border_style="cyan")
|
|
51
|
+
table.add_column("Category", style="bold yellow")
|
|
52
|
+
table.add_column("Supported Schemes", style="green")
|
|
53
|
+
table.add_column("Target Hardware", style="magenta")
|
|
54
|
+
|
|
55
|
+
table.add_row(
|
|
56
|
+
"Integer Math",
|
|
57
|
+
"INT8 AbsMax, INT8 Affine, INT4 Groupwise",
|
|
58
|
+
"CPU / Tensor Core / Metal",
|
|
59
|
+
)
|
|
60
|
+
table.add_row(
|
|
61
|
+
"Bit-Packing",
|
|
62
|
+
"INT4 (2 vals/byte), BitNet Ternary (4 or 5 vals/byte), 2:4 Sparsity",
|
|
63
|
+
"Vectorized Sub-Byte Runtimes",
|
|
64
|
+
)
|
|
65
|
+
table.add_row(
|
|
66
|
+
"Modern FP8",
|
|
67
|
+
"OCP E4M3FN (GEMM), OCP E5M2 (KV/Gradients)",
|
|
68
|
+
"Ada / Hopper / CPU Emulation",
|
|
69
|
+
)
|
|
70
|
+
table.add_row(
|
|
71
|
+
"Optimization",
|
|
72
|
+
"SmoothQuant, AWQ Grid Search, GPTQ Hessian",
|
|
73
|
+
"Zero-compile PyTorch",
|
|
74
|
+
)
|
|
75
|
+
table.add_row(
|
|
76
|
+
"Hardware Sparsity",
|
|
77
|
+
"2:4 Structured Sparsity (50% compression)",
|
|
78
|
+
"Ampere / Ada / Hopper",
|
|
79
|
+
)
|
|
80
|
+
table.add_row(
|
|
81
|
+
"Benchmarking",
|
|
82
|
+
"Sliding WikiText-2 Perplexity, SQNR (dB)",
|
|
83
|
+
"Local CPU & GPU",
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
console.print(table)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@app.command("pack-inspect")
|
|
90
|
+
def pack_inspect(
|
|
91
|
+
fmt: str = typer.Option(
|
|
92
|
+
"int4",
|
|
93
|
+
"--format",
|
|
94
|
+
"-f",
|
|
95
|
+
help="Format to inspect: int4, ternary, sparse24",
|
|
96
|
+
),
|
|
97
|
+
):
|
|
98
|
+
"""Inspect bit-packing layout, sub-byte representation, and theoretical memory savings."""
|
|
99
|
+
import torch
|
|
100
|
+
|
|
101
|
+
fmt_lower = fmt.lower()
|
|
102
|
+
if fmt_lower == "int4":
|
|
103
|
+
sample = torch.tensor([5, 12], dtype=torch.int32)
|
|
104
|
+
packed, orig_len = pack_int4(sample, signed=False)
|
|
105
|
+
unpacked = unpack_int4(packed, original_len=orig_len, signed=False)
|
|
106
|
+
byte_val = packed.item()
|
|
107
|
+
|
|
108
|
+
panel = Panel(
|
|
109
|
+
f"[bold green]INT4 Sub-Byte Packing (2 values per uint8 byte)[/bold green]\n\n"
|
|
110
|
+
f"Input values: [cyan]{sample.tolist()}[/cyan]\n"
|
|
111
|
+
f"• Low nibble [0:3]: {sample[0].item()} -> [yellow]{sample[0].item():04b}[/yellow]\n"
|
|
112
|
+
f"• High nibble [4:7]: {sample[1].item()} -> [yellow]{sample[1].item():04b}[/yellow]\n"
|
|
113
|
+
f"Packed uint8 byte: [bold magenta]{byte_val}[/bold magenta] "
|
|
114
|
+
f"(binary: [yellow]{byte_val:08b}[/yellow], hex: [yellow]0x{byte_val:02X}[/yellow])\n"
|
|
115
|
+
f"Unpacked roundtrip: [cyan]{unpacked.tolist()}[/cyan]\n"
|
|
116
|
+
f"Memory compression: [bold green]4.0x vs FP16[/bold green] (2.0x vs INT8)",
|
|
117
|
+
title="INT4 Bit Layout",
|
|
118
|
+
border_style="cyan",
|
|
119
|
+
)
|
|
120
|
+
console.print(panel)
|
|
121
|
+
|
|
122
|
+
elif fmt_lower == "ternary":
|
|
123
|
+
sample = torch.tensor([-1, 0, 1, -1])
|
|
124
|
+
packed, orig_len = pack_ternary(sample, mode="2bit")
|
|
125
|
+
unpacked = unpack_ternary(packed, original_len=orig_len, mode="2bit")
|
|
126
|
+
byte_val = packed.item()
|
|
127
|
+
|
|
128
|
+
panel = Panel(
|
|
129
|
+
f"[bold green]BitNet 1.58b Ternary Packing (4 weights per uint8 byte)[/bold green]\n\n"
|
|
130
|
+
f"Input weights: [cyan]{sample.tolist()}[/cyan]\n"
|
|
131
|
+
f"Offset mapping: u = v + 1 in {{0, 1, 2}}\n"
|
|
132
|
+
f"Packed uint8 byte: [bold magenta]{byte_val}[/bold magenta] "
|
|
133
|
+
f"(binary: [yellow]{byte_val:08b}[/yellow], hex: [yellow]0x{byte_val:02X}[/yellow])\n"
|
|
134
|
+
f"Unpacked roundtrip: [cyan]{unpacked.tolist()}[/cyan]\n"
|
|
135
|
+
f"Memory compression: [bold green]8.0x vs FP16[/bold green] (Multiplication-free GEMM)",
|
|
136
|
+
title="BitNet 1.58b Layout",
|
|
137
|
+
border_style="green",
|
|
138
|
+
)
|
|
139
|
+
console.print(panel)
|
|
140
|
+
|
|
141
|
+
elif fmt_lower in ("sparse24", "2:4", "sparse"):
|
|
142
|
+
sample = torch.tensor([[0.0, 3.5, 0.0, -1.2, 4.0, 0.0, 0.0, -2.1]])
|
|
143
|
+
condensed, metadata = prune_and_compress_2_4(sample)
|
|
144
|
+
meta_byte = metadata.item()
|
|
145
|
+
|
|
146
|
+
panel = Panel(
|
|
147
|
+
f"[bold green]2:4 Structured Sparsity (NVIDIA Sparse Tensor Cores)[/bold green]\n\n"
|
|
148
|
+
f"Dense input row (8 values): [cyan]{sample.tolist()[0]}[/cyan]\n"
|
|
149
|
+
f"Condensed non-zeros (4 values): [cyan]{condensed.tolist()[0]}[/cyan]\n"
|
|
150
|
+
f"Packed metadata byte: [bold magenta]{meta_byte}[/bold magenta] "
|
|
151
|
+
f"(binary: [yellow]{meta_byte:08b}[/yellow], hex: [yellow]0x{meta_byte:02X}[/yellow])\n"
|
|
152
|
+
f"Storage reduction: [bold green]50% weight + 2-bit metadata/val[/bold green]\n"
|
|
153
|
+
f"Theoretical math throughput: [bold green]2.0x Tensor Core TFLOPS[/bold green]",
|
|
154
|
+
title="2:4 Sparse Tensor Layout",
|
|
155
|
+
border_style="magenta",
|
|
156
|
+
)
|
|
157
|
+
console.print(panel)
|
|
158
|
+
|
|
159
|
+
else:
|
|
160
|
+
err_msg = f"Unknown format '{fmt}'. Choose 'int4', 'ternary', or 'sparse24'."
|
|
161
|
+
console.print(f"[bold red]{err_msg}[/bold red]")
|
|
162
|
+
raise typer.Exit(code=1)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
@app.command("eval")
|
|
166
|
+
def eval_cmd(
|
|
167
|
+
shape: str = typer.Option("1024x1024", "--shape", help="Weight matrix shape (MxN)."),
|
|
168
|
+
):
|
|
169
|
+
"""Evaluate tensor-level SQNR (dB) and compression factor across quantization schemes."""
|
|
170
|
+
import torch
|
|
171
|
+
|
|
172
|
+
from quantbench.eval.sqnr import compute_sqnr
|
|
173
|
+
from quantbench.quant.affine import dequantize_absmax, quantize_absmax
|
|
174
|
+
from quantbench.quant.blockwise import dequantize_blockwise, quantize_blockwise
|
|
175
|
+
|
|
176
|
+
try:
|
|
177
|
+
m_str, n_str = shape.lower().split("x")
|
|
178
|
+
m, n = int(m_str), int(n_str)
|
|
179
|
+
except Exception:
|
|
180
|
+
console.print(
|
|
181
|
+
f"[bold red]Invalid shape '{shape}'. Expected format e.g. '1024x1024'[/bold red]"
|
|
182
|
+
)
|
|
183
|
+
raise typer.Exit(code=1) from None
|
|
184
|
+
|
|
185
|
+
torch.manual_seed(42)
|
|
186
|
+
w = torch.randn(m, n)
|
|
187
|
+
|
|
188
|
+
# 1. INT8 AbsMax
|
|
189
|
+
q8, s8 = quantize_absmax(w, num_bits=8, dim=1)
|
|
190
|
+
w8 = dequantize_absmax(q8, s8)
|
|
191
|
+
sqnr8 = compute_sqnr(w, w8)
|
|
192
|
+
|
|
193
|
+
# 2. INT4 Blockwise (G=128)
|
|
194
|
+
q4_128, s4_128, zp4_128, meta_128 = quantize_blockwise(w, group_size=128, num_bits=4)
|
|
195
|
+
w4_128 = dequantize_blockwise(q4_128, s4_128, zp4_128, meta_128)
|
|
196
|
+
sqnr4_128 = compute_sqnr(w, w4_128)
|
|
197
|
+
|
|
198
|
+
# 3. INT4 Blockwise (G=64)
|
|
199
|
+
q4_64, s4_64, zp4_64, meta_64 = quantize_blockwise(w, group_size=64, num_bits=4)
|
|
200
|
+
w4_64 = dequantize_blockwise(q4_64, s4_64, zp4_64, meta_64)
|
|
201
|
+
sqnr4_64 = compute_sqnr(w, w4_64)
|
|
202
|
+
|
|
203
|
+
table = Table(
|
|
204
|
+
title=f"Quantization Fidelity Benchmark ({m}x{n} Weight Matrix)",
|
|
205
|
+
border_style="cyan",
|
|
206
|
+
)
|
|
207
|
+
table.add_column("Scheme", style="bold yellow")
|
|
208
|
+
table.add_column("Group Size", style="cyan")
|
|
209
|
+
table.add_column("Bit-Width", style="green")
|
|
210
|
+
table.add_column("SQNR (dB)", style="bold magenta")
|
|
211
|
+
table.add_column("Compression", style="bold green")
|
|
212
|
+
|
|
213
|
+
table.add_row("FP16 Baseline", "None", "16-bit", "100.0 dB", "1.00x")
|
|
214
|
+
table.add_row("INT8 AbsMax", "Per-Channel", "8-bit", f"{sqnr8:.2f} dB", "2.00x")
|
|
215
|
+
table.add_row("INT4 Blockwise", "128", "4-bit", f"{sqnr4_128:.2f} dB", "3.55x")
|
|
216
|
+
table.add_row("INT4 Blockwise", "64", "4-bit", f"{sqnr4_64:.2f} dB", "3.20x")
|
|
217
|
+
|
|
218
|
+
console.print(table)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
@app.command("pareto")
|
|
222
|
+
def pareto_cmd(
|
|
223
|
+
vocab_size: int = typer.Option(128, "--vocab-size", help="Vocabulary cardinality."),
|
|
224
|
+
hidden_dim: int = typer.Option(128, "--hidden-dim", help="Model hidden dimension."),
|
|
225
|
+
seq_len: int = typer.Option(300, "--seq-len", help="Number of calibration tokens."),
|
|
226
|
+
):
|
|
227
|
+
"""Run an end-to-end Pareto quality-vs-compression benchmark across all quantization schemes."""
|
|
228
|
+
import torch
|
|
229
|
+
import torch.nn as nn
|
|
230
|
+
|
|
231
|
+
from quantbench.bench.pareto import ParetoBenchmark
|
|
232
|
+
from quantbench.eval.perplexity import generate_synthetic_corpus
|
|
233
|
+
|
|
234
|
+
class DemoTransformer(nn.Module):
|
|
235
|
+
def __init__(self, vocab: int, dim: int):
|
|
236
|
+
super().__init__()
|
|
237
|
+
self.embedding = nn.Embedding(vocab, dim)
|
|
238
|
+
self.q_proj = nn.Linear(dim, dim, bias=False)
|
|
239
|
+
self.k_proj = nn.Linear(dim, dim, bias=False)
|
|
240
|
+
self.v_proj = nn.Linear(dim, dim, bias=False)
|
|
241
|
+
self.o_proj = nn.Linear(dim, dim, bias=False)
|
|
242
|
+
self.gate_proj = nn.Linear(dim, dim * 2, bias=False)
|
|
243
|
+
self.down_proj = nn.Linear(dim * 2, dim, bias=False)
|
|
244
|
+
self.lm_head = nn.Linear(dim, vocab, bias=False)
|
|
245
|
+
|
|
246
|
+
def forward(self, x: torch.Tensor) -> torch.Tensor:
|
|
247
|
+
h = self.embedding(x)
|
|
248
|
+
attn = self.o_proj(torch.relu(self.q_proj(h) + self.k_proj(h) + self.v_proj(h)))
|
|
249
|
+
mlp = self.down_proj(torch.relu(self.gate_proj(attn)))
|
|
250
|
+
return self.lm_head(mlp)
|
|
251
|
+
|
|
252
|
+
console.print(
|
|
253
|
+
f"[bold cyan]Initializing Demo Transformer[/bold cyan] "
|
|
254
|
+
f"(Vocab={vocab_size}, Dim={hidden_dim})..."
|
|
255
|
+
)
|
|
256
|
+
torch.manual_seed(42)
|
|
257
|
+
model = DemoTransformer(vocab=vocab_size, dim=hidden_dim)
|
|
258
|
+
tokens = generate_synthetic_corpus(vocab_size=vocab_size, seq_length=seq_len, seed=1)
|
|
259
|
+
|
|
260
|
+
bench = ParetoBenchmark(
|
|
261
|
+
base_model=model, test_tokens=tokens, window_size=64, stride=32
|
|
262
|
+
)
|
|
263
|
+
console.print(
|
|
264
|
+
"[bold yellow]Running Pareto sweep across all precision runtimes...[/bold yellow]"
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
points = bench.run_sweep(exclude_modules=["lm_head"])
|
|
268
|
+
|
|
269
|
+
table = Table(title="Pareto Frontier (Quality vs Compression)", border_style="cyan")
|
|
270
|
+
table.add_column("Configuration", style="bold yellow")
|
|
271
|
+
table.add_column("Scheme", style="cyan")
|
|
272
|
+
table.add_column("Group", style="magenta")
|
|
273
|
+
table.add_column("Memory (MB)", style="green")
|
|
274
|
+
table.add_column("Compression", style="bold green")
|
|
275
|
+
table.add_column("Perplexity", style="bold white")
|
|
276
|
+
table.add_column("Delta PPL", style="bold red")
|
|
277
|
+
table.add_column("SQNR (dB)", style="bold magenta")
|
|
278
|
+
|
|
279
|
+
for p in points:
|
|
280
|
+
grp = str(p.group_size) if p.group_size > 0 else "None"
|
|
281
|
+
delta_str = (
|
|
282
|
+
f"+{p.perplexity_delta:.3f}"
|
|
283
|
+
if p.perplexity_delta > 0
|
|
284
|
+
else f"{p.perplexity_delta:.3f}"
|
|
285
|
+
)
|
|
286
|
+
table.add_row(
|
|
287
|
+
p.name,
|
|
288
|
+
p.scheme,
|
|
289
|
+
grp,
|
|
290
|
+
f"{p.memory_mb:.2f}",
|
|
291
|
+
f"{p.compression_ratio:.2f}x",
|
|
292
|
+
f"{p.perplexity:.3f}",
|
|
293
|
+
delta_str,
|
|
294
|
+
f"{p.avg_sqnr_db:.1f}",
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
console.print(table)
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
if __name__ == "__main__":
|
|
301
|
+
app()
|
|
@@ -0,0 +1,27 @@
|
|
|
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
|
+
"""Evaluation metrics and perplexity measurement engines."""
|
|
16
|
+
|
|
17
|
+
from quantbench.eval.perplexity import (
|
|
18
|
+
compute_sliding_window_perplexity,
|
|
19
|
+
generate_synthetic_corpus,
|
|
20
|
+
)
|
|
21
|
+
from quantbench.eval.sqnr import compute_sqnr
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"compute_sqnr",
|
|
25
|
+
"compute_sliding_window_perplexity",
|
|
26
|
+
"generate_synthetic_corpus",
|
|
27
|
+
]
|