LLM Quantization in Python: GPTQ vs AWQ vs bitsandbytes vs GGUF (2026)
Compare GPTQ, AWQ, bitsandbytes, and GGUF for LLM quantization in Python. Real H100 benchmarks, kernel choices, and a production-ready decision tree for 2026.
LLM quantization in Python compresses model weights from FP16 to 4-bit or 8-bit integers, cutting VRAM by 50–75% with under 1% perplexity loss when you set it up right. In 2026, the four formats that actually matter for production are GPTQ (calibration-based, vLLM/TGI native), AWQ (activation-aware, faster decode), bitsandbytes (zero-config NF4, fine-tuning friendly), and GGUF (llama.cpp's CPU/GPU hybrid format). I've shipped all four in different on-call rotations, and honestly, the right pick depends on your serving stack, not on benchmark leaderboards.
GPTQ wins for high-throughput vLLM/TGI serving on NVIDIA GPUs. Marlin kernels push 4-bit decode close to FP8 throughput at 1/4 the VRAM.
AWQ matches GPTQ accuracy but is faster to quantize (no gradient pass) and has slightly better activation-distribution handling for instruction-tuned models.
bitsandbytes NF4 is the only format that supports QLoRA fine-tuning out of the box. Use it when you need to adapt, not just serve.
GGUF is the right call for mixed CPU/GPU inference, Apple Silicon, or laptop-class deployment via llama.cpp and Ollama.
Expect 0.3 to 0.8 perplexity points of degradation at 4-bit. INT8 is essentially lossless but only halves VRAM versus FP16.
All four formats now support a calibration dataset. Using random text instead of in-domain samples is the single most common production footgun.
Why quantize LLMs in 2026
A 70B parameter model in FP16 needs roughly 140 GB of VRAM just to load the weights, before you account for the KV cache, which on a single H100 will eat another 20 to 40 GB at realistic batch sizes. Quantizing those weights to 4-bit drops the weight footprint to about 35 GB, which is the difference between needing four H100s or a single one. The cost-per-prediction math gets brutal fast. At $2 to $4 per hour per H100, a four-card setup runs ~$2,500/month before redundancy. Quantization is the cheapest lever you have.
The 2026 shift is that quantization is no longer an accuracy tax you pay to make a model fit. Activation-aware methods (AWQ, HQQ), better calibration recipes, and dedicated kernels like Marlin and Machete have closed the gap to roughly 0.3 to 0.8 perplexity points versus FP16 on most chat models. The official Hugging Face quantization overview now treats 4-bit serving as a default, not an experiment. If you're still serving FP16 because you're worried about accuracy, you're paying 4× for marginal improvements your evals can barely measure.
The flip side: quantization is not free latency-wise. Dequantization on the critical path can dominate small-batch decode, which is why kernel choice (Marlin, ExLlamaV2, llama.cpp's Q4_K_M) matters as much as the algorithm. I hit this exact bug last quarter, shipping a 4-bit model that was slower than FP16 because we used reference PyTorch kernels. Pick a quantization format that has a fused kernel in your serving stack.
Quantization format comparison table
Feature
GPTQ
AWQ
bitsandbytes
GGUF
Best bit widths
3, 4, 8
4
4 (NF4), 8
2 to 8 (Q4_K_M, Q5_K_M, etc.)
Calibration required
Yes (~128 samples)
Yes (~32 samples)
No
Optional (importance matrix)
Quantization time (7B model)
30 to 90 min
10 to 25 min
0 (runtime)
1 to 5 min + i-matrix
vLLM / TGI support
Native, Marlin kernel
Native, AWQ kernel
Limited (slower path)
No (llama.cpp only)
Fine-tuning (QLoRA)
Possible, fragile
Read-only typically
First-class
No
CPU inference
No
No
Slow
Excellent
Apple Silicon (Metal)
No
No
No
Yes
Typical perplexity loss
0.3 to 0.6
0.3 to 0.5
0.4 to 0.8
0.3 to 0.7 (Q4_K_M)
GPTQ: calibration-based quantization for vLLM
GPTQ uses second-order information from a small calibration dataset to compensate for quantization error one layer at a time. The result is a static weight file you can drop into vLLM, TGI, or SGLang and serve at near-FP16 throughput thanks to the Marlin INT4 kernel. For high-QPS production serving on NVIDIA GPUs, this is what I reach for first.
The non-obvious bit is the calibration dataset. The default c4 sample works for general chat, but if you're serving a code model or a domain-tuned model, calibrating on representative prompts gives you measurably better downstream eval scores. Use 128 to 512 samples of 2048 tokens each.
from transformers import AutoTokenizer
from gptqmodel import GPTQModel, QuantizeConfig
from datasets import load_dataset
model_id = "meta-llama/Llama-3.1-8B-Instruct"
quant_path = "llama-3.1-8b-gptq-int4"
tokenizer = AutoTokenizer.from_pretrained(model_id)
# Use in-domain prompts when possible; fall back to c4 for general chat.
calibration = load_dataset("allenai/c4", "en", split="train", streaming=True)
calibration_samples = [
tokenizer(next(iter(calibration))["text"], return_tensors="pt").input_ids
for _ in range(128)
]
quant_config = QuantizeConfig(
bits=4,
group_size=128, # 128 is the sweet spot; 32 = better accuracy, larger file
desc_act=True, # activation-order quantization, slower but more accurate
sym=True,
)
model = GPTQModel.from_pretrained(model_id, quant_config)
model.quantize(calibration_samples)
model.save_quantized(quant_path)
Serving the resulting model in vLLM is basically a one-line change. vLLM auto-detects GPTQ from the config and routes to the Marlin kernel for batch sizes ≥ 8. For smaller batches, ExLlamaV2's kernel is sometimes faster, so benchmark on your actual traffic shape before deciding.
AWQ: activation-aware quantization in Python
Activation-aware Weight Quantization (AWQ) protects the 1% of weights that activations care about most, leaving them in higher precision while quantizing the rest aggressively to 4-bit. The result: comparable accuracy to GPTQ, but much faster to quantize, plus slightly better decode latency at batch size 1.
I've found AWQ particularly strong for instruction-tuned chat models where the activation distribution is more skewed than base models. For raw next-token prediction benchmarks the two are often within noise, but on MT-Bench and Arena-Hard, AWQ models tend to retain conversational quality a bit better in my evals.
from awq import AutoAWQForCausalLM
from transformers import AutoTokenizer
model_id = "Qwen/Qwen2.5-14B-Instruct"
quant_path = "qwen2.5-14b-awq"
quant_config = {
"zero_point": True,
"q_group_size": 128,
"w_bit": 4,
"version": "GEMM", # "GEMV" is faster for batch=1, "GEMM" wins above batch=4
}
model = AutoAWQForCausalLM.from_pretrained(model_id, safetensors=True)
tokenizer = AutoTokenizer.from_pretrained(model_id, trust_remote_code=True)
# AWQ needs far fewer calibration samples than GPTQ
model.quantize(tokenizer, quant_config=quant_config, calib_data="pileval")
model.save_quantized(quant_path)
tokenizer.save_pretrained(quant_path)
One operational win: AWQ checkpoints are typically 10 to 15% smaller than GPTQ at the same bit width, because they avoid the per-channel scales GPTQ needs for desc_act mode. On a 70B model that's the difference between a 35 GB and a 41 GB download, which really matters for cold-start times on autoscaling fleets.
bitsandbytes: NF4 and QLoRA-friendly quantization
bitsandbytes is the quantization library that ships with Transformers and that PEFT/QLoRA is built on. It doesn't require a calibration pass; quantization happens at model load time using NormalFloat 4-bit (NF4), a data type tuned for the normal distribution of LLM weights. The trade-off is throughput. bitsandbytes' fused kernels are slower than Marlin or the AWQ kernel for serving, but the format supports gradient updates, which the others largely don't.
The production use case is rarely "serve a bitsandbytes model under load." It's "fine-tune a 70B model on a single 80 GB GPU using QLoRA, then merge or convert for serving." For that workflow, nothing else is close.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
bnb_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4", # NF4 beats FP4 for normal-distributed weights
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True, # quantize the quantization constants too
)
model = AutoModelForCausalLM.from_pretrained(
"meta-llama/Llama-3.1-70B-Instruct",
quantization_config=bnb_config,
device_map="auto",
)
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Llama-3.1-70B-Instruct")
# Ready for QLoRA training with PEFT, or one-off batch inference.
GGUF and llama.cpp: CPU and hybrid inference
GGUF is the file format used by llama.cpp, Ollama, LM Studio, and most local-LLM stacks. Unlike GPTQ, AWQ, and bitsandbytes, it isn't really a quantization algorithm. It's a container that bundles weights quantized with one of many schemes (Q2_K, Q4_K_M, Q5_K_M, Q6_K, Q8_0, IQ-series). The K-quants use k-means clustering per superblock, while the IQ-quants use importance matrices for sub-3-bit compression.
The reason to care about GGUF from Python is that llama.cpp has the only mature CPU inference path, the best Apple Silicon (Metal) support, and a fast cross-platform GPU path via CUDA/HIP/Vulkan. If your deployment target isn't a fleet of NVIDIA datacenter cards, GGUF is probably your answer.
from llama_cpp import Llama
# Q4_K_M is the recommended default: best accuracy/size trade-off.
llm = Llama(
model_path="./Meta-Llama-3.1-8B-Instruct-Q4_K_M.gguf",
n_ctx=8192,
n_gpu_layers=-1, # offload all layers to GPU; -1 means "as many as fit"
n_threads=8, # CPU threads for layers not on GPU
flash_attn=True,
)
response = llm.create_chat_completion(
messages=[{"role": "user", "content": "Explain KV cache in 2 sentences."}],
max_tokens=200,
temperature=0.2,
)
print(response["choices"][0]["message"]["content"])
For Python services that need an HTTP boundary, llama.cpp ships a built-in OpenAI-compatible server (llama-server) that's frankly easier to operate than vLLM for sub-50-QPS workloads. The catch: throughput maxes out lower than vLLM because llama.cpp's continuous batching is less aggressive than vLLM's PagedAttention. Above ~30 concurrent requests, switch to GPTQ + vLLM.
Which quantization method is best for LLMs?
So, there's no universal winner. The right format is determined by your serving stack and access pattern. Here's the decision tree I actually use on call:
NVIDIA GPU + vLLM/TGI/SGLang + high QPS → GPTQ with Marlin kernel. AWQ is a defensible alternative.
NVIDIA GPU + small batch / single-stream latency-sensitive → AWQ with GEMV kernel, or ExLlamaV2.
Need QLoRA fine-tuning → bitsandbytes NF4, then re-quantize for serving.
CPU, Apple Silicon, edge device, or air-gapped laptop → GGUF Q4_K_M via llama.cpp.
AMD GPU → GGUF (mature ROCm path) or vLLM with FP8 if available. AWQ/GPTQ kernels on AMD lag NVIDIA by months.
Hard accuracy budget under 0.2 perplexity loss → INT8 (bitsandbytes 8-bit or FP8) instead of 4-bit anything.
Numbers below come from internal evals I ran in May 2026 on Llama-3.1-8B-Instruct, a single H100 80GB, batch size 32, 512 input / 256 output tokens. Your mileage will vary by model family, generation, and prompt distribution, so always benchmark on your own traffic before committing.
Format
VRAM (weights)
Throughput (tok/s)
MMLU (5-shot)
Perplexity (WikiText)
FP16 baseline
16.1 GB
3,840
69.4
6.12
FP8 (vLLM)
8.4 GB
5,210
69.2
6.15
GPTQ 4-bit (Marlin)
4.6 GB
4,930
68.8
6.41
AWQ 4-bit (GEMM)
4.5 GB
4,720
68.9
6.38
bitsandbytes NF4
4.9 GB
1,180
68.6
6.46
GGUF Q4_K_M (llama.cpp + CUDA)
4.8 GB
1,640
68.7
6.43
The headline: FP8 actually beats FP16 on throughput because of Hopper's tensor cores while preserving accuracy almost perfectly. Among the 4-bit formats, GPTQ + Marlin and AWQ + GEMM are within 5% of each other on throughput and within noise on accuracy. bitsandbytes is dramatically slower because it dequantizes to BF16 on the critical path. Useful as a stepping stone, not as a serving format.
Production checklist before shipping a quantized model
Quantization changes model behavior in subtle ways that don't always show up on MMLU or perplexity. Before you flip traffic to a quantized checkpoint, walk this list:
Run task-specific evals, not just generic benchmarks. A 0.3-point perplexity drop can manifest as a 5-point regression on your domain-specific eval. Replay 500 to 2,000 production prompts through both FP16 and quantized models, then diff the outputs with an LLM judge.
Check long-context behavior. Quantization error compounds across long contexts. If you serve 32k+ token requests, test at full context length, not at 2k.
Stress-test with adversarial prompts. Refusal and safety behavior is among the first things to degrade. Re-run your red-team suite.
Match calibration data to traffic. If you serve mostly code, calibrate on code. If you serve multilingual traffic, include those languages in the calibration set.
Verify the kernel path. Check vLLM logs (or equivalent) to confirm Marlin/AWQ/FlashAttention kernels actually loaded. Falling back to a reference kernel silently is a real footgun.
Monitor production latency and accuracy separately. Set up online evals. Drift in quantized models is sneakier than in FP16 because the failure modes are often subtle quality regressions rather than crashes. Our coverage of data drift detection in Python applies cleanly to LLM output monitoring.
Frequently Asked Questions
Is AWQ better than GPTQ in 2026?
They're close enough that the serving stack matters more than the algorithm. AWQ is faster to produce (no gradient pass), tends to do slightly better on instruction-tuned models, and has a faster batch-1 kernel. GPTQ has marginally better high-batch throughput with Marlin and broader ecosystem support. If you don't have a strong reason, try both and pick whichever your serving framework optimizes more aggressively.
Does quantization reduce LLM accuracy?
Yes, but less than you'd expect. INT8 quantization is essentially lossless (under 0.1 perplexity points). 4-bit costs roughly 0.3 to 0.8 perplexity points on chat models, which usually translates to a 1 to 2% drop on standard benchmarks like MMLU. The bigger risk is task-specific regressions (code generation, function calling, long-context recall) that don't show up on generic evals, so always run your own suite.
Can you fine-tune a quantized model?
Practically, only with bitsandbytes NF4 via QLoRA. GPTQ and AWQ are designed as inference-only formats; gradient updates through their kernels are either unsupported or numerically unstable. The standard recipe is: load in NF4, train a LoRA adapter, merge the adapter into FP16 weights, then re-quantize with GPTQ or AWQ for production serving.
What does GGUF Q4_K_M mean?
GGUF is the file format used by llama.cpp; Q4_K_M is one of its quantization schemes. The "Q4" means 4-bit weights, "K" refers to the k-quant family that uses different bit widths for different layer groups, and "M" is the medium variant (the L and S variants trade size for accuracy). Q4_K_M is the community-recommended default for the best accuracy-to-size ratio in most use cases.
How much VRAM does 4-bit quantization save?
About 75% versus FP16 for the weight tensors. A 70B model drops from roughly 140 GB to 35 GB at 4-bit. KV cache is unaffected by weight quantization unless you also quantize the cache separately (FP8 KV cache is supported in vLLM 0.6+), so total memory savings depend on your batch size and context length. For serving, expect total VRAM to drop by 50 to 65% in practice.
Should I use FP8 instead of INT4 quantization?
On Hopper (H100/H200) and Blackwell (B200) hardware, yes. FP8 in vLLM is essentially lossless, uses purpose-built tensor cores, and frequently beats FP16 on throughput. INT4 still wins when you need to fit weights into smaller VRAM budgets (consumer GPUs, single-card deployments of 70B+ models). On Ampere (A100) and older, FP8 isn't accelerated, so INT4 quantization is your best lever.
A practical 2026 walkthrough of GeoPandas 1.0 for Python geospatial analysis: installing the stack, handling CRS gotchas, running spatial joins, plotting interactive maps, and scaling beyond memory with DuckDB Spatial and GeoParquet.
Compare vLLM, TGI, and SGLang for serving LLMs on your own GPUs in 2026. Throughput numbers, prefix caching, quantization tradeoffs, and a production Docker deployment with monitoring.
A 2026 guide to causal inference in Python: when to pick DoWhy, EconML, or CausalML, with runnable code, refutation tests, and the pitfalls that bite real projects.