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,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
|
+
[](https://github.com/Prajwal7-2002/quantbench/actions)
|
|
32
|
+
[](LICENSE)
|
|
33
|
+
[](https://www.python.org/)
|
|
34
|
+
[](#)
|
|
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,,
|