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,170 @@
1
+ """2:4 Structured Sparsity metadata bit-packing and matrix compression runtime.
2
+
3
+ Implements the NVIDIA Ampere/Ada/Hopper Sparse Tensor Core data layout:
4
+ - Invariant: In every consecutive chunk of 4 values along the row dimension, exactly 2
5
+ values must be retained (50% sparsity).
6
+ - Retained values are stored in a dense condensed matrix of shape (M, N / 2).
7
+ - Column position indices (0, 1, 2, or 3) require log2(4) = 2 bits each.
8
+ - Two 2-bit indices for a 4-element block fit in 4 bits (1 nibble).
9
+ - Two consecutive 4-element blocks (8 dense elements, 4 retained non-zeros) pack into
10
+ a single uint8 byte!
11
+
12
+ Byte layout for two 4-element blocks (A and B):
13
+ - Bits [0:1]: Block A, first non-zero index (0..3)
14
+ - Bits [2:3]: Block A, second non-zero index (0..3)
15
+ - Bits [4:5]: Block B, first non-zero index (0..3)
16
+ - Bits [6:7]: Block B, second non-zero index (0..3)
17
+ """
18
+
19
+ from __future__ import annotations
20
+
21
+ import torch
22
+
23
+
24
+ def pack_2_4_indices(indices: torch.Tensor) -> torch.Tensor:
25
+ """Pack 2-bit position indices for 2:4 structured sparsity into a uint8 tensor.
26
+
27
+ Args:
28
+ indices: Tensor of shape (..., K) where K is a multiple of 4, containing
29
+ position indices in {0, 1, 2, 3}. Every pair (2k, 2k+1) represents
30
+ the two non-zero positions in a 4-element block.
31
+
32
+ Returns:
33
+ Packed uint8 tensor of shape (..., K // 4).
34
+ """
35
+ if indices.shape[-1] % 4 != 0:
36
+ raise ValueError(
37
+ f"Last dimension must be a multiple of 4 (representing pairs of 4-element blocks), "
38
+ f"got {indices.shape[-1]}."
39
+ )
40
+
41
+ i32 = indices.to(torch.int32)
42
+ if (i32 < 0).any() or (i32 > 3).any():
43
+ raise ValueError("2:4 sparsity position indices must be strictly within {0, 1, 2, 3}.")
44
+
45
+ # Each byte holds 4 indices: (idx0, idx1) from Block A, and (idx2, idx3) from Block B
46
+ idx0 = i32[..., 0::4]
47
+ idx1 = i32[..., 1::4]
48
+ idx2 = i32[..., 2::4]
49
+ idx3 = i32[..., 3::4]
50
+
51
+ packed = (
52
+ (idx0 & 0x03)
53
+ | ((idx1 & 0x03) << 2)
54
+ | ((idx2 & 0x03) << 4)
55
+ | ((idx3 & 0x03) << 6)
56
+ )
57
+ return packed.to(torch.uint8)
58
+
59
+
60
+ def unpack_2_4_indices(packed: torch.Tensor) -> torch.Tensor:
61
+ """Unpack packed uint8 metadata tensor back into 2-bit position indices.
62
+
63
+ Args:
64
+ packed: Tensor of dtype torch.uint8 and shape (..., M).
65
+
66
+ Returns:
67
+ Tensor of dtype torch.int8 and shape (..., M * 4) containing indices in {0, 1, 2, 3}.
68
+ """
69
+ if packed.dtype != torch.uint8:
70
+ raise TypeError(f"Packed tensor must have dtype torch.uint8, got {packed.dtype}.")
71
+
72
+ p32 = packed.to(torch.int32)
73
+ idx0 = p32 & 0x03
74
+ idx1 = (p32 >> 2) & 0x03
75
+ idx2 = (p32 >> 4) & 0x03
76
+ idx3 = (p32 >> 6) & 0x03
77
+
78
+ interleaved = torch.stack([idx0, idx1, idx2, idx3], dim=-1)
79
+ flat_shape = list(interleaved.shape[:-2]) + [interleaved.shape[-2] * 4]
80
+ return interleaved.view(flat_shape).to(torch.int8)
81
+
82
+
83
+ def prune_and_compress_2_4(
84
+ weight: torch.Tensor,
85
+ ) -> tuple[torch.Tensor, torch.Tensor]:
86
+ """Prune a dense matrix to 2:4 structured sparsity and pack its values and metadata.
87
+
88
+ For every block of 4 elements along the last dimension:
89
+ 1. Finds the 2 elements with largest absolute magnitude.
90
+ 2. Stores their values in condensed_values of shape (..., N // 2).
91
+ 3. Encodes their original within-block positions into packed_metadata of shape (..., N // 8).
92
+
93
+ Args:
94
+ weight: Dense weight tensor with last dimension divisible by 8.
95
+
96
+ Returns:
97
+ Tuple of (condensed_values, packed_metadata).
98
+ condensed_values has shape (..., N // 2) and same dtype as input.
99
+ packed_metadata has shape (..., N // 8) and dtype torch.uint8.
100
+ """
101
+ orig_shape = weight.shape
102
+ n = orig_shape[-1]
103
+ if n % 8 != 0:
104
+ raise ValueError(f"Last dimension must be divisible by 8 for 2:4 byte alignment, got {n}.")
105
+
106
+ # Reshape into blocks of 4
107
+ num_blocks = n // 4
108
+ blocks = weight.view(*orig_shape[:-1], num_blocks, 4)
109
+
110
+ # Find top 2 values by absolute magnitude in each block of 4
111
+ abs_blocks = blocks.abs()
112
+ # topk returns values and indices sorted descending
113
+ _, top2_indices = torch.topk(abs_blocks, k=2, dim=-1, largest=True, sorted=True)
114
+
115
+ # Sort indices ascending to preserve relative left-to-right order
116
+ sorted_indices, _ = torch.sort(top2_indices, dim=-1)
117
+
118
+ # Gather the top 2 values
119
+ condensed = torch.gather(blocks, dim=-1, index=sorted_indices)
120
+
121
+ # Flatten condensed values to (..., N // 2)
122
+ condensed_shape = list(orig_shape[:-1]) + [n // 2]
123
+ condensed_values = condensed.view(condensed_shape)
124
+
125
+ # Flatten indices to (..., N // 2) and pack into uint8 bytes
126
+ flat_indices = sorted_indices.view(condensed_shape)
127
+ packed_metadata = pack_2_4_indices(flat_indices)
128
+
129
+ return condensed_values, packed_metadata
130
+
131
+
132
+ def decompress_2_4(
133
+ condensed_values: torch.Tensor,
134
+ packed_metadata: torch.Tensor,
135
+ original_shape: tuple[int, ...] | torch.Size,
136
+ ) -> torch.Tensor:
137
+ """Decompress condensed values and packed metadata back to a sparse 2:4 tensor.
138
+
139
+ Args:
140
+ condensed_values: Tensor of shape (..., N // 2).
141
+ packed_metadata: Tensor of shape (..., N // 8) and dtype torch.uint8.
142
+ original_shape: Shape of original dense tensor to reconstruct (..., N).
143
+
144
+ Returns:
145
+ Reconstructed tensor with exact 2:4 sparsity (exactly 2 non-zeros per 4 elements).
146
+ """
147
+ n = original_shape[-1]
148
+ if n % 8 != 0:
149
+ raise ValueError(f"Original shape last dimension must be divisible by 8, got {n}.")
150
+
151
+ num_blocks = n // 4
152
+ unpacked_indices = unpack_2_4_indices(packed_metadata)
153
+
154
+ # Reshape condensed and indices to (..., num_blocks, 2)
155
+ val_blocks = condensed_values.view(*original_shape[:-1], num_blocks, 2)
156
+ idx_blocks = unpacked_indices.view(*original_shape[:-1], num_blocks, 2).to(torch.int64)
157
+
158
+ # Allocate zero tensor for blocks (..., num_blocks, 4)
159
+ reconstructed_blocks = torch.zeros(
160
+ *original_shape[:-1],
161
+ num_blocks,
162
+ 4,
163
+ dtype=condensed_values.dtype,
164
+ device=condensed_values.device,
165
+ )
166
+
167
+ # Scatter condensed values back into their exact positions
168
+ reconstructed_blocks.scatter_(dim=-1, index=idx_blocks, src=val_blocks)
169
+
170
+ return reconstructed_blocks.view(original_shape)
@@ -0,0 +1,193 @@
1
+ """BitNet 1.58b ternary bit-packing runtime for sub-byte quantization.
2
+
3
+ Packs ternary weights in {-1, 0, +1} into compact uint8 byte containers.
4
+ Supports two complementary packing representations:
5
+ 1. '2bit' (Default, hardware SIMD-friendly):
6
+ - Maps {-1 -> 0, 0 -> 1, +1 -> 2} (biased offset by +1).
7
+ - Packs 4 ternary values into 1 byte (2 bits per value).
8
+ - Enables fast parallel bitshift and masking instructions on CPU/GPU.
9
+ - Delivers an 8x memory reduction over FP16 and 4x over INT8.
10
+
11
+ 2. 'base3' (High-density Radix-3):
12
+ - Because 3^5 = 243 < 256, packs 5 ternary values into 1 byte.
13
+ - Theoretical information-theoretic density limit (~1.58 bits per weight).
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import torch
19
+
20
+
21
+ def pack_ternary_quad(v0: int, v1: int, v2: int, v3: int) -> int:
22
+ """Pack four ternary values {-1, 0, +1} into a single 8-bit unsigned byte.
23
+
24
+ Uses biased representation: u = v + 1 in {0, 1, 2}.
25
+ """
26
+ for v in (v0, v1, v2, v3):
27
+ if v not in (-1, 0, 1):
28
+ raise ValueError(f"Ternary values must be in {{-1, 0, 1}}, got {v}.")
29
+ u0 = v0 + 1
30
+ u1 = v1 + 1
31
+ u2 = v2 + 1
32
+ u3 = v3 + 1
33
+ return (u0 & 0x03) | ((u1 & 0x03) << 2) | ((u2 & 0x03) << 4) | ((u3 & 0x03) << 6)
34
+
35
+
36
+ def unpack_ternary_quad(byte: int) -> tuple[int, int, int, int]:
37
+ """Unpack a single 8-bit unsigned byte into four ternary values {-1, 0, +1}."""
38
+ u0 = byte & 0x03
39
+ u1 = (byte >> 2) & 0x03
40
+ u2 = (byte >> 4) & 0x03
41
+ u3 = (byte >> 6) & 0x03
42
+ return u0 - 1, u1 - 1, u2 - 1, u3 - 1
43
+
44
+
45
+ def pack_ternary(
46
+ tensor: torch.Tensor,
47
+ mode: str = "2bit",
48
+ dim: int = -1,
49
+ ) -> tuple[torch.Tensor, int]:
50
+ """Vectorized packing of ternary {-1, 0, +1} weights into a uint8 tensor.
51
+
52
+ Args:
53
+ tensor: Integer or float tensor containing ternary values {-1, 0, 1}.
54
+ mode: Packing mode:
55
+ - '2bit': 4 ternary weights per byte (standard SIMD bitshift).
56
+ - 'base3': 5 ternary weights per byte (Radix-3 maximal density).
57
+ dim: Dimension along which to pack. Defaults to last dimension (-1).
58
+
59
+ Returns:
60
+ Tuple of (packed_uint8_tensor, original_dim_length).
61
+ """
62
+ if mode not in ("2bit", "base3"):
63
+ raise ValueError(f"Unknown packing mode '{mode}'. Choose '2bit' or 'base3'.")
64
+
65
+ t_int = tensor.round().to(torch.int32)
66
+ valid_mask = (t_int == -1) | (t_int == 0) | (t_int == 1)
67
+ if not valid_mask.all():
68
+ raise ValueError("Input tensor contains values outside the ternary set {-1, 0, 1}.")
69
+
70
+ dim = dim if dim >= 0 else tensor.ndim + dim
71
+ orig_len = tensor.shape[dim]
72
+
73
+ if dim != tensor.ndim - 1:
74
+ t_int = t_int.transpose(dim, -1)
75
+
76
+ # Offset to non-negative range: {-1, 0, 1} -> {0, 1, 2}
77
+ u = t_int + 1
78
+
79
+ if mode == "2bit":
80
+ chunk_size = 4
81
+ rem = orig_len % chunk_size
82
+ if rem != 0:
83
+ pad_len = chunk_size - rem
84
+ pad_shape = list(u.shape[:-1]) + [pad_len]
85
+ # Pad with 1 (which maps to ternary 0 after offset)
86
+ pad_tensor = torch.ones(pad_shape, dtype=u.dtype, device=u.device)
87
+ u = torch.cat([u, pad_tensor], dim=-1)
88
+
89
+ v0 = u[..., 0::4]
90
+ v1 = u[..., 1::4]
91
+ v2 = u[..., 2::4]
92
+ v3 = u[..., 3::4]
93
+
94
+ packed = (v0 & 0x03) | ((v1 & 0x03) << 2) | ((v2 & 0x03) << 4) | ((v3 & 0x03) << 6)
95
+ packed_uint8 = packed.to(torch.uint8)
96
+
97
+ else: # base3: 5 weights per byte
98
+ chunk_size = 5
99
+ rem = orig_len % chunk_size
100
+ if rem != 0:
101
+ pad_len = chunk_size - rem
102
+ pad_shape = list(u.shape[:-1]) + [pad_len]
103
+ pad_tensor = torch.ones(pad_shape, dtype=u.dtype, device=u.device)
104
+ u = torch.cat([u, pad_tensor], dim=-1)
105
+
106
+ v0 = u[..., 0::5]
107
+ v1 = u[..., 1::5]
108
+ v2 = u[..., 2::5]
109
+ v3 = u[..., 3::5]
110
+ v4 = u[..., 4::5]
111
+
112
+ packed = v0 + (3 * v1) + (9 * v2) + (27 * v3) + (81 * v4)
113
+ packed_uint8 = packed.to(torch.uint8)
114
+
115
+ if dim != tensor.ndim - 1:
116
+ packed_uint8 = packed_uint8.transpose(dim, -1)
117
+
118
+ return packed_uint8, orig_len
119
+
120
+
121
+ def unpack_ternary(
122
+ packed: torch.Tensor,
123
+ original_len: int | None = None,
124
+ mode: str = "2bit",
125
+ dim: int = -1,
126
+ ) -> torch.Tensor:
127
+ """Vectorized unpacking of packed uint8 tensor back into ternary {-1, 0, +1} values.
128
+
129
+ Args:
130
+ packed: Tensor of dtype torch.uint8 containing packed ternary values.
131
+ original_len: Optional original dimension length to trim padding.
132
+ mode: Packing mode used ('2bit' or 'base3').
133
+ dim: Dimension along which elements were packed.
134
+
135
+ Returns:
136
+ Unpacked tensor of dtype torch.int8 containing values strictly in {-1, 0, 1}.
137
+ """
138
+ if packed.dtype != torch.uint8:
139
+ raise TypeError(f"Packed tensor must have dtype torch.uint8, got {packed.dtype}.")
140
+
141
+ dim = dim if dim >= 0 else packed.ndim + dim
142
+ if dim != packed.ndim - 1:
143
+ packed = packed.transpose(dim, -1)
144
+
145
+ p32 = packed.to(torch.int32)
146
+
147
+ if mode == "2bit":
148
+ u0 = p32 & 0x03
149
+ u1 = (p32 >> 2) & 0x03
150
+ u2 = (p32 >> 4) & 0x03
151
+ u3 = (p32 >> 6) & 0x03
152
+
153
+ # Subtract offset: {0, 1, 2} -> {-1, 0, 1}
154
+ v0 = u0 - 1
155
+ v1 = u1 - 1
156
+ v2 = u2 - 1
157
+ v3 = u3 - 1
158
+
159
+ interleaved = torch.stack([v0, v1, v2, v3], dim=-1)
160
+ flat_shape = list(interleaved.shape[:-2]) + [interleaved.shape[-2] * 4]
161
+ unpacked = interleaved.view(flat_shape).to(torch.int8)
162
+
163
+ elif mode == "base3":
164
+ # Extract via radix-3 integer division and modulo
165
+ u0 = p32 % 3
166
+ rem1 = p32 // 3
167
+ u1 = rem1 % 3
168
+ rem2 = rem1 // 3
169
+ u2 = rem2 % 3
170
+ rem3 = rem2 // 3
171
+ u3 = rem3 % 3
172
+ u4 = rem3 // 3
173
+
174
+ v0 = u0 - 1
175
+ v1 = u1 - 1
176
+ v2 = u2 - 1
177
+ v3 = u3 - 1
178
+ v4 = u4 - 1
179
+
180
+ interleaved = torch.stack([v0, v1, v2, v3, v4], dim=-1)
181
+ flat_shape = list(interleaved.shape[:-2]) + [interleaved.shape[-2] * 5]
182
+ unpacked = interleaved.view(flat_shape).to(torch.int8)
183
+
184
+ else:
185
+ raise ValueError(f"Unknown packing mode '{mode}'. Choose '2bit' or 'base3'.")
186
+
187
+ if original_len is not None:
188
+ unpacked = unpacked[..., :original_len]
189
+
190
+ if dim != packed.ndim - 1:
191
+ unpacked = unpacked.transpose(dim, -1)
192
+
193
+ return unpacked
@@ -0,0 +1,276 @@
1
+ Metadata-Version: 2.5
2
+ Name: quantbench-engine
3
+ Version: 0.1.0
4
+ Summary: First-principles LLM quantization engine, bit-packing runtimes, 2:4 sparsity, and Quality-vs-Speed Pareto evaluation benchmark.
5
+ Project-URL: Homepage, https://github.com/Prajwal7-2002/quantbench
6
+ Project-URL: Documentation, https://prajwal7-2002.github.io/quantbench/
7
+ Project-URL: Repository, https://github.com/Prajwal7-2002/quantbench.git
8
+ Project-URL: Issues, https://github.com/Prajwal7-2002/quantbench/issues
9
+ Author-email: Prajwal Pujari <prajwalpujari16@gmail.com>
10
+ License-Expression: Apache-2.0
11
+ License-File: LICENSE
12
+ Keywords: awq,benchmark,bitnet,fp8,gptq,inference,int4,int8,llm,petaflop,quantization,serving,smoothquant,sparsity
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
18
+ Classifier: Topic :: System :: Benchmark
19
+ Classifier: Topic :: System :: Hardware
20
+ Requires-Python: >=3.11
21
+ Requires-Dist: numpy>=1.26
22
+ Requires-Dist: pydantic>=2.6
23
+ Requires-Dist: rich>=13
24
+ Requires-Dist: safetensors>=0.4.3
25
+ Requires-Dist: torch>=2.4.0
26
+ Requires-Dist: typer>=0.12
27
+ Description-Content-Type: text/markdown
28
+
29
+ # quantbench βš–οΈ
30
+
31
+ [![CI](https://github.com/Prajwal7-2002/quantbench/actions/workflows/ci.yml/badge.svg)](https://github.com/Prajwal7-2002/quantbench/actions)
32
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](LICENSE)
33
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://www.python.org/)
34
+ [![Hardware](https://img.shields.io/badge/Hardware-CPU_%26_GPU-teal.svg)](#)
35
+
36
+ A **unified, first-principles quantization engine, bit-packing runtime, 2:4 sparsity toolkit, and Quality-vs-Speed Pareto evaluation benchmark** in pure Python and PyTorch.
37
+
38
+ Most developers treat LLM quantization as a black box: they download GGUF or AWQ checkpoints without knowing how scales and zero-points are calculated, how 4-bit signed integers are packed into memory bytes, or why activation outliers in attention layers make naive INT8 quantization collapse.
39
+
40
+ `quantbench` implements these production algorithms from scratch with **radical transparency and zero compiled C++/CUDA dependencies**, and provides a benchmark CLI that measures empirical perplexity degradation alongside generation throughput.
41
+
42
+ ---
43
+
44
+ ## ⚑ Core Architecture
45
+
46
+ ```
47
+ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
48
+ β”‚ quantbench β”‚
49
+ β”‚ (Single Unified Repository: First-Principles Engine + Benchmark) β”‚
50
+ β”‚ β”‚
51
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
52
+ β”‚ β”‚ 1. From-Scratch Quant Engine (quant) β”‚ β”‚ 2. Drop-in Neural Layers (nn) β”‚ β”‚
53
+ β”‚ β”‚ β€’ Uniform Affine (Scale & Zero-Point) β”‚ β”‚ β€’ QuantLinear (PyTorch Module) β”‚ β”‚
54
+ β”‚ β”‚ β€’ Symmetric AbsMax (Per-Tensor/Channel)β”‚ β”‚ β€’ In-Place Model Converter (Qwen2.5, β”‚ β”‚
55
+ β”‚ β”‚ β€’ Blockwise (Group Size 32, 64, 128) β”‚ β”‚ SmolLM2-135M, GPT-2) β”‚ β”‚
56
+ β”‚ β”‚ β€’ INT4 Nibble Packing (uint8 vectors) β”‚ β”‚ β€’ Forward GEMM with dynamic dequant β”‚ β”‚
57
+ β”‚ β”‚ β€’ Sub-2-bit Ternary Packing (BitNet) β”‚ β”‚ β€’ Layer-by-Layer SQNR Diagnostics β”‚ β”‚
58
+ β”‚ β”‚ β€’ 2:4 Structured Sparsity (Ampere) β”‚ β”‚ β”‚ β”‚
59
+ β”‚ β”‚ β€’ SmoothQuant Outlier Migration β”‚ β”‚ β”‚ β”‚
60
+ β”‚ β”‚ β€’ AWQ Grid Search & GPTQ Hessian/Chol β”‚ β”‚ β”‚ β”‚
61
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
62
+ β”‚ β”‚ β”‚ β”‚
63
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
64
+ β”‚ β”‚ Powers β”‚
65
+ β”‚ β–Ό β”‚
66
+ β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚
67
+ β”‚ β”‚ 3. Rigorous Evaluation & Profiling (eval & bench) β”‚ β”‚
68
+ β”‚ β”‚ β€’ Perplexity Engine: Sliding-window evaluation on WikiText-2 & C4 β”‚ β”‚
69
+ β”‚ β”‚ β€’ Diagnostics: SQNR (dB), Max Error, Outlier Dimension Detection β”‚ β”‚
70
+ β”‚ β”‚ β€’ Hardware Profiler: servebench bridge (Tokens/sec, TTFT, Peak VRAM in MB) β”‚ β”‚
71
+ β”‚ β”‚ β€’ Terminal ASCII Pareto Visualizer: Generates live Quality vs Speed tradeoff curveβ”‚ β”‚
72
+ β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
73
+ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
74
+ ```
75
+
76
+ ---
77
+
78
+ ## πŸ“ Mathematical Foundations
79
+
80
+ ### 1. Affine Asymmetric Quantization
81
+ Quantizes a floating-point tensor $x \in [x_{\min}, x_{\max}]$ into integer range $[q_{\min}, q_{\max}]$:
82
+ $$s = \frac{x_{\max} - x_{\min}}{2^b - 1}, \quad z = \text{round}\left(-\frac{x_{\min}}{s}\right) + q_{\min}$$
83
+ $$q = \text{clamp}\left(\text{round}\left(\frac{x}{s}\right) + z, \; q_{\min}, \; q_{\max}\right)$$
84
+ $$\hat{x} = (q - z) \cdot s$$
85
+
86
+ ### 2. Symmetric AbsMax Quantization
87
+ Centers dynamic range at zero ($z = 0$), enabling direct hardware integer GEMM without per-element zero-point subtractions:
88
+ $$s = \frac{\max(|x|)}{2^{b-1} - 1}, \quad q = \text{clamp}\left(\text{round}\left(\frac{x}{s}\right), \; -2^{b-1}, \; 2^{b-1}-1\right)$$
89
+ $$\hat{x} = q \cdot s$$
90
+
91
+ ### 3. Block-Wise (Group-Wise) Quantization
92
+ Partitions weights into blocks of size $G \in \{32, 64, 128\}$ to isolate channel outliers and preserve precision across wide layers.
93
+
94
+ ### 4. OCP FP8 Emulation
95
+ Bit-exact simulation of **E4M3FN** (higher mantissa precision, range $\pm 448$, no infinity) for GEMM weights/activations and **E5M2** (wider dynamic range $\pm 57344$) for gradients and KV caches.
96
+
97
+ ### 5. Sub-Byte Bit-Packing & 2:4 Sparsity
98
+ Because hardware cannot address sub-byte boundaries, weights are packed into vectorized `uint8` containers:
99
+ * **INT4**: Two 4-bit nibbles packed per byte: `(v0 & 0x0F) | ((v1 & 0x0F) << 4)` ($4\times$ memory compression vs FP16).
100
+ * **BitNet Ternary**: Four $\{-1, 0, 1\}$ weights per byte using offset binary mapping ($8\times$ compression vs FP16).
101
+ * **2:4 Structured Sparsity**: Two non-zero values retained per 4-element block ($50\%$ storage) with 2-bit column positions packed into 4-bit metadata nibbles for Ampere/Ada/Hopper Sparse Tensor Cores ($2\times$ TFLOPS).
102
+
103
+ ---
104
+
105
+ ---
106
+
107
+ ## πŸ“¦ Installation
108
+
109
+ Install `quantbench` directly from PyPI:
110
+
111
+ ```bash
112
+ pip install quantbench
113
+ ```
114
+
115
+ Or using [uv](https://docs.astral.sh/uv/):
116
+
117
+ ```bash
118
+ uv add quantbench
119
+ ```
120
+
121
+ To install from source for development:
122
+
123
+ ```bash
124
+ git clone https://github.com/Prajwal7-2002/quantbench.git
125
+ cd quantbench
126
+ uv sync
127
+ ```
128
+
129
+ ---
130
+
131
+ ## πŸš€ Quickstart
132
+
133
+ ### Sub-Byte Packing Inspection
134
+
135
+ Inspect how weights are packed into vectorized byte streams and calculate theoretical compression:
136
+
137
+ ```bash
138
+ # Inspect sub-byte bit layouts and theoretical memory compression
139
+ uv run quantbench pack-inspect --format int4
140
+ uv run quantbench pack-inspect --format ternary
141
+ uv run quantbench pack-inspect --format sparse24
142
+ ```
143
+
144
+ ### Running the Test Suite
145
+
146
+ ```bash
147
+ # Run the complete test suite (47 tests, 95% coverage)
148
+ uv run pytest
149
+ ```
150
+
151
+ ### Python API Quickstart
152
+
153
+ ```python
154
+ import torch
155
+ import torch.nn as nn
156
+ from quantbench import QuantLinear, convert_model_to_quant
157
+
158
+ # 1. Convert any standalone nn.Linear to INT4 QuantLinear
159
+ linear = nn.Linear(4096, 4096)
160
+ quant_linear = QuantLinear.from_linear(linear, scheme="int4", group_size=64)
161
+
162
+ x = torch.randn(2, 4096)
163
+ y = quant_linear(x) # Dynamic vectorized dequantization forward
164
+ stats = quant_linear.memory_footprint_bytes()
165
+ print(
166
+ f"Compression: {stats['compression_ratio']:.2f}x "
167
+ f"({stats['fp16_bytes']} -> {stats['compressed_bytes']} bytes)"
168
+ )
169
+
170
+ # 2. In-place recursive transformer conversion
171
+ model = MyTransformer()
172
+ converted_model, stats = convert_model_to_quant(
173
+ model,
174
+ scheme="int4",
175
+ group_size=64,
176
+ exclude_modules=["lm_head"],
177
+ )
178
+ print(
179
+ f"Compressed {stats['converted_layers']} layers: "
180
+ f"{stats['orig_memory_mb']} MB -> {stats['quant_memory_mb']} MB "
181
+ f"({stats['compression_ratio']}x)"
182
+ )
183
+
184
+ # 3. Advanced Calibration Algorithms (SmoothQuant, AWQ, GPTQ)
185
+ from quantbench import (
186
+ smooth_linear_weights,
187
+ extract_activation_channel_scales,
188
+ search_best_awq_scale,
189
+ compute_activation_channel_means,
190
+ gptq_quantize_weights,
191
+ compute_activation_hessian,
192
+ )
193
+
194
+ # SmoothQuant: Migrate activation outlier dynamic range to weights
195
+ act_scales = extract_activation_channel_scales(linear, calibration_x)
196
+ w_smoothed, scale = smooth_linear_weights(linear.weight, act_scales, alpha=0.5)
197
+
198
+ # AWQ: Search optimal per-channel protection scales
199
+ act_means = compute_activation_channel_means(calibration_x)
200
+ best_scale, best_alpha = search_best_awq_scale(linear.weight, act_means, calibration_x)
201
+
202
+ # GPTQ: Second-order Hessian error compensation with Cholesky damping
203
+ h = compute_activation_hessian(calibration_x, in_features=4096)
204
+ w_gptq = gptq_quantize_weights(linear.weight, h, bits=4, block_size=32, damping_factor=0.01)
205
+
206
+ # 4. Pareto Quality-vs-Compression Benchmark
207
+ from quantbench import ParetoBenchmark, generate_synthetic_corpus
208
+
209
+ tokens = generate_synthetic_corpus(vocab_size=32000, seq_length=1024)
210
+ bench = ParetoBenchmark(model, tokens, window_size=512, stride=256)
211
+ points = bench.run_sweep()
212
+ print(bench.render_ascii_table(points))
213
+ ```
214
+
215
+ ### CLI Quickstart
216
+
217
+ ```bash
218
+ # Evaluate tensor-level numerical fidelity (SQNR dB)
219
+ uv run quantbench eval --shape 1024x1024
220
+
221
+ # Run automated Pareto sweep across all precision runtimes
222
+ uv run quantbench pareto --vocab-size 128 --hidden-dim 128 --seq-len 300
223
+ ```
224
+
225
+ ---
226
+
227
+ ## πŸ”¬ Executable Examples & Roofline Integration
228
+
229
+ The repository includes runnable end-to-end scripts demonstrating drop-in inference, advanced calibration, and hardware roofline modeling (integrating with `servebench`):
230
+
231
+ * **[examples/01_quant_linear_inference.py](examples/01_quant_linear_inference.py)**: Demonstrates converting PyTorch `nn.Linear` layers into INT4 blockwise, BitNet 1.58b ternary, and Ampere 2:4 sparse `QuantLinear` modules, verifying forward pass execution and recursive transformer conversion with `lm_head` float exclusion.
232
+ * **[examples/02_advanced_calibration.py](examples/02_advanced_calibration.py)**: Step-by-step demonstration of SmoothQuant outlier migration (proving exact mathematical continuous invariance), AWQ Frobenius grid search for salient channel protection, and GPTQ second-order Hessian Cholesky damping updates yielding $1.48\times$ error reduction.
233
+ * **[examples/03_servebench_roofline_eval.py](examples/03_servebench_roofline_eval.py)**: Bridges `quantbench` drop-in layers with `servebench`'s hardware roofline model (`HARDWARE_PROFILES`, RTX 3050 192 GB/s, T4 320 GB/s), calculating arithmetic intensity $I = \frac{2}{\text{bytes\_per\_param}}$ FLOPs/byte, decode token latency, and DRAM bandwidth traffic reduction (slashing memory traffic by $73.4\%$ for INT4).
234
+
235
+ ```bash
236
+ uv run python examples/01_quant_linear_inference.py
237
+ uv run python examples/02_advanced_calibration.py
238
+ uv run python examples/03_servebench_roofline_eval.py
239
+ ```
240
+
241
+ ---
242
+
243
+ ## πŸ“š Educational Engineering Guide
244
+
245
+ For an exhaustive first-principles mathematical and architectural breakdown of quantization, sub-byte bit packing, and hardware sparsity, read [GUIDE.md](GUIDE.md):
246
+ * Exact mathematical proofs (Zero-Preserving Affine Invariant, Optimal Brain Surgeon update derivation).
247
+ * IEEE 754 Banker's rounding drift analysis.
248
+ * OCP FP8 (E4M3FN vs E5M2) analytical software emulation.
249
+ * Bit-level packing mechanics (INT4 two's complement sign extension, BitNet biased SIMD vs Base-3, NVIDIA Ampere 2:4 metadata nibbles).
250
+ * Second-order Hessian error compensation with Cholesky inversion and block updates.
251
+ * Dual-metric Pareto benchmarking (Layer-level SQNR dB + Sliding-Window Perplexity).
252
+
253
+ ---
254
+
255
+ ## πŸ—ΊοΈ Roadmap & Staged Milestones
256
+
257
+ - [x] **Phase 1: Mathematical Primitives & Invariants**: Affine, AbsMax, Group-wise, and FP8 (E4M3/E5M2) simulation with rigorous error bound verification.
258
+ - [x] **Phase 2: Integer Bit-Packing Runtimes**: Vectorized INT4 nibble packing (`uint8`), BitNet ternary packing, and 2:4 sparsity metadata.
259
+ - [x] **Phase 3: Drop-in Neural Layers**: `QuantLinear` PyTorch module, in-place Hugging Face model converter, and buffer serialization.
260
+ - [x] **Phase 4: Advanced Calibration Algorithms**: SmoothQuant outlier migration, AWQ grid search, and GPTQ second-order Hessian updates.
261
+ - [x] **Phase 5: Perplexity & Evaluation Engine**: Standardized sliding-window perplexity on WikiText-2 and C4.
262
+ - [x] **Phase 6: Pareto Benchmark & Terminal Visualizer**: Quality-vs-compression analysis and terminal Rich Pareto curves.
263
+ - [x] **Phase 7: Public Learning Guide & Packaging**: Comprehensive educational walkthrough and PyPI release.
264
+ - [ ] **Phase 8: Sub-Byte Fused Kernels & KV Cache Quantization**: Analytical Triton kernel emulators for mixed-precision GEMV and INT4 KV-cache compression.
265
+
266
+ ---
267
+
268
+ ## 🀝 Contributing
269
+
270
+ We welcome contributions that expand educational clarity, add new calibration methods, improve numerical fidelity diagnostics, or introduce new sub-byte bit layouts! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for full development guidelines, testing protocols, and PR standards.
271
+
272
+ ---
273
+
274
+ ## πŸ“œ License
275
+
276
+ Apache 2.0. See [LICENSE](LICENSE) for details.
@@ -0,0 +1,27 @@
1
+ quantbench/__init__.py,sha256=KDbTWGNMAWi0Lwdakzu82pMH9flcQn6aMAlh0OHm_qo,2137
2
+ quantbench/cli.py,sha256=IYS_v8z_THl4DVPHmAjXvz1Gjnlk8isMrYrQW4tLlBk,11309
3
+ quantbench/bench/__init__.py,sha256=Vf-t0YdRnJX97bP-WhnQE-JhKsCEHEUrcNmYCcU5FLw,780
4
+ quantbench/bench/pareto.py,sha256=tuwMoW_onWGkQ0X1eB4PoMyZoNsna-Q-VAwb7jSJEtg,6691
5
+ quantbench/eval/__init__.py,sha256=vRUALVM6ipMaY4Un6M2y9JR8G__GVF06Tr9N4Q_f46Y,935
6
+ quantbench/eval/perplexity.py,sha256=VJL-CZ1qStsbHcPKXO1yco3xlJxbc5ngLOOzQEllGo0,5107
7
+ quantbench/eval/sqnr.py,sha256=wcJeavo2wdnTnaJ-a5zt18XZuC33mQ-wIJXX96fP3Io,2330
8
+ quantbench/nn/__init__.py,sha256=ytAvN-xpfAYjk9z1bAG1TgJSpsSAAdaKq6sErJf-Uao,266
9
+ quantbench/nn/model_convert.py,sha256=e0lvsJaqXt-PBeo-fyud4BQDKuPcZwonQgeiP2rgY9E,3971
10
+ quantbench/nn/quant_linear.py,sha256=roL3IEbQZ0gza59kiMPC5S0XGmwAofgGQhIPHxANfRk,11633
11
+ quantbench/quant/__init__.py,sha256=5xxK-EhOMLUQz7cI8ZpVYKQusHxXxTalwbh8YvEiy4I,1589
12
+ quantbench/quant/affine.py,sha256=mNWy56_EwBR8DBesPfQT1MfJW5so92TeftTrRUYOJ3g,5787
13
+ quantbench/quant/blockwise.py,sha256=riB_A4fFpqBefKnGsAdGiVXVzS4ZDZyBzzfC1bY9mbM,3674
14
+ quantbench/quant/fp8.py,sha256=3FzBJtjUMsxD1YThwi-XH3BWCwfp-IUm6Y5SFu2GoEI,4520
15
+ quantbench/quant/algorithms/__init__.py,sha256=Fb7GYY-W86nNYnNZ2o2-NiduM6QalEjtdi7AmQLUtJo,1368
16
+ quantbench/quant/algorithms/awq.py,sha256=gT14qOG4UdxGpud733lhTemOfTpnGMm0UwPZZ-mG-CI,7107
17
+ quantbench/quant/algorithms/gptq.py,sha256=vfRPq2wfYxtBWFZs4qwRG6TJsRlWu3HudDiKVYVxtBA,7113
18
+ quantbench/quant/algorithms/smoothquant.py,sha256=bMTX12D6kVArTZ3MxPl7e2RDsBB7E-chVLocb_nJlyI,5127
19
+ quantbench/quant/packing/__init__.py,sha256=39NUq9k3W2W-7UzsE_6Pi_feGB9KxbWRotlCSTfL1Us,868
20
+ quantbench/quant/packing/int4_pack.py,sha256=k9bYAW6aRnZVhbtOlXySt5Ak_cRroyLY3Nuw_KKN9ZY,5790
21
+ quantbench/quant/packing/sparse_pack.py,sha256=v-v-rGgedsWT_0OxEZfOjMb5NOFjJmVd-Qw4F4tWYlU,6472
22
+ quantbench/quant/packing/ternary_pack.py,sha256=4VhIf_drzwjqUD9UIYHrkjwybxurGvBuT1k_nF-61qg,6527
23
+ quantbench_engine-0.1.0.dist-info/METADATA,sha256=chC6j3ZOR27tD8cnfcZT7p_QZ5zd6ScSDNO85-K9TX0,15014
24
+ quantbench_engine-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
25
+ quantbench_engine-0.1.0.dist-info/entry_points.txt,sha256=Xjmf5qAi3DAjbVsTOD8S-YmFIVegvUlIbUxRtyH-zNY,50
26
+ quantbench_engine-0.1.0.dist-info/licenses/LICENSE,sha256=9oZgYppWR-Mtc05yFDBlD0JCfl-TZZwqH4GgLZ7U3gI,10759
27
+ quantbench_engine-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ quantbench = quantbench.cli:app