Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
146 changes: 146 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
# Contributing to quantbench ⚖️

Thank you for your interest in contributing to `quantbench`!

`quantbench` is an open-source, first-principles quantization engine, bit-packing runtime, 2:4 sparsity toolkit, and Quality-vs-Speed Pareto evaluation benchmark written in pure Python and PyTorch. We welcome contributions that improve mathematical transparency, expand sub-byte compression layouts, add new post-training calibration algorithms, and bridge inference serving systems with precision engineering.

---

## 🛠️ Development Setup

We use [uv](https://docs.astral.sh/uv/) for Python environment and dependency management.

1. **Clone the repository**:
```bash
git clone https://github.com/Prajwal7-2002/quantbench.git
cd quantbench
```

2. **Sync dependencies and virtual environment**:
```bash
uv sync
```
This automatically installs all runtime, benchmark, and development dependencies into a isolated `.venv`.

---

## 🧪 Testing Protocol

We maintain high test coverage across mathematical primitives, sub-byte bit manipulation, neural network layers, and calibration algorithms.

1. **Run the entire test suite**:
```bash
uv run pytest
```

2. **Run with coverage report**:
```bash
uv run pytest --cov=quantbench --cov-report=term-missing
```

3. **Run a specific test category**:
```bash
# Test bit-packing roundtrip invariants
uv run pytest tests/test_packing.py

# Test calibration algorithms (SmoothQuant, AWQ, GPTQ)
uv run pytest tests/test_algorithms.py

# Test drop-in QuantLinear layers
uv run pytest tests/test_quant_linear.py
```

### Invariant Testing Rules
When contributing new quantization schemes or packing runtimes, you **must** include tests verifying:
- **Roundtrip Fidelity**: $x = \text{unpack}(\text{pack}(x))$ for all valid integer representations.
- **Mathematical Invariance**: Continuous exactness proofs (e.g. SmoothQuant $(X s^{-1})(s W) = XW$ within $10^{-5}$ drift).
- **Error Bounds**: Maximum absolute error bounded by $\frac{\Delta}{2}$ for uniform quantizers.
- **Extreme Inputs**: Subnormal values, exact zeros, all-positive, and all-negative tensor ranges.

---

## 🎨 Code Quality & Style

We enforce modern Python conventions, strict type hints, and clean code formatting via [ruff](https://docs.astral.sh/ruff/):

1. **Lint and format checks**:
```bash
uv run ruff check .
```

2. **Auto-fix formatting**:
```bash
uv run ruff format .
```

### Architectural Principles

1. **Zero Compiled C++/CUDA Dependencies**:
`quantbench` is designed for radical algorithmic transparency. Core quantization logic and bit-packing must run in pure Python and PyTorch without requiring NVCC or compiler toolchains.
2. **Vectorized PyTorch Operations**:
Avoid Python `for` loops across tensor dimensions. Sub-byte bit-packing must be implemented via vectorized tensor operations (`&`, `|`, `>>`, `<<`) on `torch.uint8` containers.
3. **Hardware Agnostic**:
All algorithms must run seamlessly on consumer CPUs, Apple Silicon, and CUDA GPUs without hardcoded device assumptions.
4. **Honest Pareto Benchmarks**:
Never cherry-pick perplexity numbers. Always evaluate against standard validation corpora (WikiText-2, C4) using standardized sliding-window stride lengths.

---

## 🎯 High-Impact Areas for Contribution

We actively welcome contributions in the following domains:

### 1. New Sub-Byte Formats & Precision Schemes (`src/quantbench/quant/`)
- [ ] **NVIDIA Blackwell NVFP4 Emulation**: Bit-exact emulation of 4-bit floating point formats (E2M1) with micro-scaling factors.
- [ ] **2-bit Non-Linear Quantization**: Quantization via Lloyd-Max optimal non-uniform codebooks.
- [ ] **NF4 (NormalFloat4)**: Quantile-based optimal representation for zero-mean normally distributed weights.

### 2. Quantized KV-Cache Runtimes
- [ ] **INT4 / FP8 KV-Cache Page Allocator**: Interfacing with `mini-infer`'s paged memory manager to benchmark memory savings during multi-turn generation.
- [ ] **Dynamic Per-Token Key/Value Quantization**: Asymmetric scaling per token vector along the sequence dimension.

### 3. Advanced Calibration & Optimization (`src/quantbench/quant/algorithms/`)
- [ ] **SqueezeLLM Clustering**: Non-uniform sensitivity-weighted k-means clustering with sparse outlier matrices.
- [ ] **QuIP# Vector Quantization**: Randomized Hadamard transform pre-processing to eliminate activation outliers.
- [ ] **HQQ (Half-Quadratic Quantization)**: Closed-form optimization without calibration data.

### 4. Real-World Model Support (`src/quantbench/nn/`)
- [ ] **GGUF Dequantization Loader**: Direct parsing and loading of `.gguf` quantized tensors into `QuantLinear` modules.
- [ ] **SafeTensors Model Converter**: Auto-convert and shard Hugging Face SafeTensors checkpoints directly into disk-mapped quantized weights.

---

## 📬 Pull Request Rules & Submission Process

To keep `quantbench` reliable and maintainable, all contributions must follow these conventions:

### 1. Branch Naming Conventions
Create a descriptive branch off `main`:
* `feat/short-feature-name` (e.g. `feat/nvfp4-packing`)
* `fix/short-bug-description` (e.g. `fix/ternary-base3-rounding`)
* `perf/short-perf-description` (e.g. `perf/vectorized-hessian-chol`)
* `docs/short-doc-description` (e.g. `docs/smoothquant-proof-derivation`)
* `test/short-test-description` (e.g. `test/gptq-corner-cases`)

### 2. Conventional Commit Standards
Commits must follow the Conventional Commits specification:
```
feat(quant): add NVIDIA NVFP4 analytical emulator and bit-packing runtime
fix(packing): correct uint8 mask overflow in sparse24 index condensation
docs(guide): add mathematical derivation of optimal brain surgeon Hessian update
test(eval): add sliding window perplexity regression test on small corpus
```

### 3. PR Verification Checklist
Before submitting a PR, ensure:
- [ ] `uv run pytest` passes 100% of tests.
- [ ] `uv run ruff check .` reports 0 errors.
- [ ] Test coverage does not decrease.
- [ ] Relevant documentation or examples are updated.
- [ ] Single author attribution is preserved.

---

## 📜 License

By contributing to `quantbench`, you agree that your contributions will be licensed under the [Apache 2.0 License](LICENSE).
73 changes: 66 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,23 +74,49 @@ Because hardware cannot address sub-byte boundaries, weights are packed into vec

---

## 🚀 Quickstart
---

## 📦 Installation

Install `quantbench` directly from PyPI:

```bash
pip install quantbench
```

Or using [uv](https://docs.astral.sh/uv/):

```bash
uv add quantbench
```

Dependencies are managed via [uv](https://docs.astral.sh/uv/):
To install from source for development:

```bash
# Clone and sync environment
git clone https://github.com/Prajwal7-2002/quantbench.git
cd quantbench
uv sync
```

---

## 🚀 Quickstart

### Sub-Byte Packing Inspection

Inspect how weights are packed into vectorized byte streams and calculate theoretical compression:

```bash
# Inspect sub-byte bit layouts and theoretical memory compression
uv run quantbench pack-inspect --format int4
uv run quantbench pack-inspect --format ternary
uv run quantbench pack-inspect --format sparse24
```

### Running the Test Suite

```bash
# Run the complete test suite (31 tests, 93% coverage)
# Run the complete test suite (47 tests, 95% coverage)
uv run pytest
```

Expand All @@ -108,7 +134,10 @@ quant_linear = QuantLinear.from_linear(linear, scheme="int4", group_size=64)
x = torch.randn(2, 4096)
y = quant_linear(x) # Dynamic vectorized dequantization forward
stats = quant_linear.memory_footprint_bytes()
print(f"Compression: {stats['compression_ratio']:.2f}x ({stats['fp16_bytes']} -> {stats['compressed_bytes']} bytes)")
print(
f"Compression: {stats['compression_ratio']:.2f}x "
f"({stats['fp16_bytes']} -> {stats['compressed_bytes']} bytes)"
)

# 2. In-place recursive transformer conversion
model = MyTransformer()
Expand All @@ -118,7 +147,11 @@ converted_model, stats = convert_model_to_quant(
group_size=64,
exclude_modules=["lm_head"],
)
print(f"Compressed {stats['converted_layers']} layers: {stats['orig_memory_mb']} MB -> {stats['quant_memory_mb']} MB ({stats['compression_ratio']}x)")
print(
f"Compressed {stats['converted_layers']} layers: "
f"{stats['orig_memory_mb']} MB -> {stats['quant_memory_mb']} MB "
f"({stats['compression_ratio']}x)"
)

# 3. Advanced Calibration Algorithms (SmoothQuant, AWQ, GPTQ)
from quantbench import (
Expand All @@ -138,7 +171,10 @@ w_smoothed, scale = smooth_linear_weights(linear.weight, act_scales, alpha=0.5)
act_means = compute_activation_channel_means(calibration_x)
best_scale, best_alpha = search_best_awq_scale(linear.weight, act_means, calibration_x)

# GPTQ: Second-order Hessian error compensation
# GPTQ: Second-order Hessian error compensation with Cholesky damping
h = compute_activation_hessian(calibration_x, in_features=4096)
w_gptq = gptq_quantize_weights(linear.weight, h, bits=4, block_size=32, damping_factor=0.01)

# 4. Pareto Quality-vs-Compression Benchmark
from quantbench import ParetoBenchmark, generate_synthetic_corpus

Expand All @@ -160,6 +196,22 @@ uv run quantbench pareto --vocab-size 128 --hidden-dim 128 --seq-len 300

---

## 🔬 Executable Examples & Roofline Integration

The repository includes runnable end-to-end scripts demonstrating drop-in inference, advanced calibration, and hardware roofline modeling (integrating with `servebench`):

* **[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.
* **[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.
* **[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).

```bash
uv run python examples/01_quant_linear_inference.py
uv run python examples/02_advanced_calibration.py
uv run python examples/03_servebench_roofline_eval.py
```

---

## 📚 Educational Engineering Guide

For an exhaustive first-principles mathematical and architectural breakdown of quantization, sub-byte bit packing, and hardware sparsity, read [GUIDE.md](GUIDE.md):
Expand All @@ -181,6 +233,13 @@ For an exhaustive first-principles mathematical and architectural breakdown of q
- [x] **Phase 5: Perplexity & Evaluation Engine**: Standardized sliding-window perplexity on WikiText-2 and C4.
- [x] **Phase 6: Pareto Benchmark & Terminal Visualizer**: Quality-vs-compression analysis and terminal Rich Pareto curves.
- [x] **Phase 7: Public Learning Guide & Packaging**: Comprehensive educational walkthrough and PyPI release.
- [ ] **Phase 8: Sub-Byte Fused Kernels & KV Cache Quantization**: Analytical Triton kernel emulators for mixed-precision GEMV and INT4 KV-cache compression.

---

## 🤝 Contributing

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.

---

Expand Down
124 changes: 124 additions & 0 deletions examples/01_quant_linear_inference.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
"""Example 1: Drop-in QuantLinear Replacement & Bit-Packing Invariants.

Demonstrates converting PyTorch nn.Linear to quantbench QuantLinear across:
1. INT4 blockwise packed nibbles (G=128).
2. BitNet 1.58b {-1, 0, +1} ternary packing.
3. NVIDIA Ampere 2:4 structured sparsity.
4. Memory footprint reduction and state_dict serialization.
"""

from __future__ import annotations

import torch
import torch.nn as nn

from quantbench.nn.model_convert import convert_model_to_quant
from quantbench.nn.quant_linear import QuantLinear


def main() -> None:
print("=" * 70)
print("Example 1: Drop-in QuantLinear Replacement & Bit-Packing Invariants")
print("=" * 70)

torch.manual_seed(42)
in_features, out_features = 2048, 4096
linear_layer = nn.Linear(in_features, out_features, bias=True)
orig_bytes = sum(p.numel() * p.element_size() for p in linear_layer.parameters())

print(f"\n[1] Baseline PyTorch nn.Linear({in_features}, {out_features}):")
print(f" Parameters : {in_features * out_features:,} weights")
print(f" Memory Footprint : {orig_bytes / (1024 * 1024):.2f} MB (FP32)")

# 1. INT4 Blockwise QuantLinear (G=128)
q_int4 = QuantLinear.from_linear(linear_layer, scheme="int4", group_size=128)
int4_footprint = q_int4.memory_footprint_bytes()
int4_bytes = int4_footprint["compressed_bytes"]
print("\n[2] Converted to QuantLinear (INT4 Blockwise G=128):")
print(
f" Packed Buffer : {list(q_int4.packed_weight.shape)} ({q_int4.packed_weight.dtype})"
)
r_int4_fp32 = orig_bytes / int4_bytes
r_int4_fp16 = int4_footprint["compression_ratio"]
mb_int4 = int4_bytes / (1024 * 1024)
print(
f" Memory Footprint : {mb_int4:.2f} MB "
f"({r_int4_fp32:.2f}x vs FP32, {r_int4_fp16:.2f}x vs FP16)"
)

# 2. BitNet 1.58b Ternary QuantLinear
q_ternary = QuantLinear.from_linear(linear_layer, scheme="ternary")
ternary_footprint = q_ternary.memory_footprint_bytes()
ternary_bytes = ternary_footprint["compressed_bytes"]
r_t_fp32 = orig_bytes / ternary_bytes
r_t_fp16 = ternary_footprint["compression_ratio"]
mb_t = ternary_bytes / (1024 * 1024)
print("\n[3] Converted to QuantLinear (BitNet 1.58b {-1, 0, +1}):")
print(f" Packed Buffer : {list(q_ternary.packed_weight.shape)} (4 weights/byte)")
print(
f" Memory Footprint : {mb_t:.2f} MB "
f"({r_t_fp32:.2f}x vs FP32, {r_t_fp16:.2f}x vs FP16)"
)

# 3. Ampere 2:4 Structured Sparsity QuantLinear
q_sparse = QuantLinear.from_linear(linear_layer, scheme="sparse24")
sparse_footprint = q_sparse.memory_footprint_bytes()
sparse_bytes = sparse_footprint["compressed_bytes"]
r_s_fp32 = orig_bytes / sparse_bytes
r_s_fp16 = sparse_footprint["compression_ratio"]
mb_s = sparse_bytes / (1024 * 1024)
print("\n[4] Converted to QuantLinear (NVIDIA Ampere 2:4 Structured Sparsity):")
print(f" Condensed Buffer : {list(q_sparse.condensed_weight.shape)} (50% non-zeros)")
print(f" Index Metadata : {list(q_sparse.packed_metadata.shape)} (2-bit column nibbles)")
print(
f" Memory Footprint : {mb_s:.2f} MB "
f"({r_s_fp32:.2f}x vs FP32, {r_s_fp16:.2f}x vs FP16)"
)

# 4. Verify Forward Pass & Reconstruction SQNR
x = torch.randn(4, in_features)
y_orig = linear_layer(x)
y_int4 = q_int4(x)
y_ternary = q_ternary(x)

def sqnr(orig: torch.Tensor, recon: torch.Tensor) -> float:
signal = torch.sum(orig**2).item()
noise = torch.sum((orig - recon) ** 2).item()
return 10.0 * torch.log10(torch.tensor(signal / (noise + 1e-10))).item()

print("\n[5] Forward Pass Execution (Batch=4):")
print(f" Output Shape : {list(y_int4.shape)}")
print(f" INT4 SQNR (dB) : {sqnr(y_orig, y_int4):.2f} dB")
print(f" BitNet SQNR (dB) : {sqnr(y_orig, y_ternary):.2f} dB")

# 5. Full Model Conversion Demo
class SimpleMLP(nn.Module):
def __init__(self) -> None:
super().__init__()
self.fc1 = nn.Linear(512, 1024)
self.act = nn.GELU()
self.fc2 = nn.Linear(1024, 512)
self.lm_head = nn.Linear(512, 32000, bias=False) # Excluded

mlp = SimpleMLP()
converted_mlp, stats = convert_model_to_quant(
mlp,
scheme="int4",
group_size=128,
exclude_modules=["lm_head"],
)
print("\n[6] Recursive Model Conversion (with lm_head exclusion):")
print(f" Converted Layers : {stats['converted_layers']}")
print(f" Excluded Layers : {stats['skipped_layers']} (lm_head preserved in float)")
c_ratio = stats["compression_ratio"]
print(
f" Compression : {c_ratio:.2f}x "
f"({stats['orig_memory_mb']}MB -> {stats['quant_memory_mb']}MB)"
)
print(f" Layer 1 Type : {type(converted_mlp.fc1).__name__}")
print(f" Head Type : {type(converted_mlp.lm_head).__name__}")
print("\n" + "=" * 70)


if __name__ == "__main__":
main()
Loading
Loading