bitsandbytes Tutorial: 4-bit and 8-bit Quantization for Large Language Models
Running Large Language Models (LLMs) like Llama, Mistral, or Falcon requires GPUs with substantial VRAM. A 7-billion parameter model alone can consume 14 GB of VRAM in float16 precision. What if you only have a consumer-grade GPU with 8 GB of VRAM? This is where bitsandbytes comes in.
bitsandbytes is a Python library that enables you to run deep learning models in 4-bit or 8-bit precision, drastically reducing memory requirements without significantly sacrificing output quality. This library serves as the foundation for QLoRA (Quantized Low-Rank Adaptation), which enables fine-tuning billion-parameter models on consumer GPUs.
In this tutorial, you will learn how to use bitsandbytes to:
- Load LLM models in 8-bit and 4-bit precision
- Configure quantization parameters for optimal results
- Perform fine-tuning with QLoRA
- Optimize GPU memory usage
Installation
Prerequisites
bitsandbytes requires an NVIDIA GPU with CUDA support. Make sure you have:
- Python 3.8 or newer
- PyTorch 2.0 or newer
- CUDA Toolkit 11.8 or 12.x
- NVIDIA GPU (compute capability 7.0+, i.e., Volta or newer)
Installing Libraries
pip install bitsandbytes
pip install transformers accelerate
pip install peft datasets # for QLoRA fine-tuning
To verify the installation:
import bitsandbytes as bnb
import torch
print(f"bitsandbytes version: {bnb.version}")
print(f"PyTorch version: {torch.version}")
print(f"CUDA available: {torch.cuda.isavailable()}")
print(f"GPU: {torch.cuda.getdevicename(0)}")
If you're using Google Colab, bitsandbytes is already available by default. Just make sure the runtime uses a T4 GPU or better.
Understanding Quantization Basics
Before diving into code, it's important to understand what quantization is and why it matters.
What is Quantization?
Quantization is the process of converting the numerical representation of model weights from high precision (float32/float16) to lower precision (int8/int4). Think of it like compressing an image from RAW format to JPEG. You lose some detail, but the file size is much smaller.
| Precision | Bits per Parameter | VRAM for 7B Model | Quality |
|-----------|-------------------|---------------------|---------|
| float32 | 32 bits | ~28 GB | Baseline |
| float16 | 16 bits | ~14 GB | Nearly identical |
| int8 | 8 bits | ~7 GB | Slight degradation |
| int4 (NF4) | 4 bits | ~3.5 GB | Minimal degradation |
Quantization Methods in bitsandbytes
bitsandbytes provides two main methods:
Basic Usage: Loading a Model in 8-bit
Let's start with the simplest example, loading a model in 8-bit precision.
from transformers import AutoModelForCausalLM, AutoTokenizer
modelname = "mistralai/Mistral-7B-Instruct-v0.3"
tokenizer = AutoTokenizer.frompretrained(modelname)
model = AutoModelForCausalLM.frompretrained(
modelname,
loadin8bit=True,
devicemap="auto"
)
prompt = "Explain what machine learning is in 3 sentences:"
inputs = tokenizer(prompt, returntensors="pt").to(model.device)
outputs = model.generate(
inputs,
maxnewtokens=150,
temperature=0.7,
dosample=True
)
response = tokenizer.decode(outputs[0], skipspecialtokens=True)
print(response)
The loadin8bit=True parameter tells Transformers to use bitsandbytes when loading model weights. The devicemap="auto" parameter automatically distributes model layers across available GPUs.
Checking Memory Usage
import torch
def printgpumemory():
if torch.cuda.isavailable():
allocated = torch.cuda.memoryallocated() / 10243
reserved = torch.cuda.memoryreserved() / 10243
print(f"GPU Memory Allocated: {allocated:.2f} GB")
print(f"GPU Memory Reserved: {reserved:.2f} GB")
printgpumemory()
With the Mistral-7B model, you'll see VRAM usage of around 7-8 GB compared to 14 GB in float16.
4-bit Quantization with BitsAndBytesConfig
For more aggressive memory savings, use 4-bit quantization with detailed configuration through BitsAndBytesConfig.
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
quantizationconfig = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="nf4",
bnb4bitcomputedtype=torch.bfloat16,
bnb4bitusedoublequant=True
)
modelname = "mistralai/Mistral-7B-Instruct-v0.3"
tokenizer = AutoTokenizer.frompretrained(modelname)
model = AutoModelForCausalLM.frompretrained(
modelname,
quantizationconfig=quantizationconfig,
devicemap="auto"
)
printgpumemory()
Let's break down each parameter:
bnb4bitquanttype
Determines the type of 4-bit quantization used:
"nf4"(NormalFloat4): Designed for neural network weights that follow a normal distribution. Provides better quality than standard 4-bit formats. This is the default and recommended choice."fp4"(Float4): Standard 4-bit floating-point format. Slightly faster in some cases, but lower quality than NF4.
# Comparing NF4 vs FP4
confignf4 = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="nf4",
bnb4bitcomputedtype=torch.bfloat16
)
configfp4 = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="fp4",
bnb4bitcomputedtype=torch.bfloat16
)
bnb4bitcomputedtype
Determines the data type used during computation (forward pass). Although weights are stored in 4-bit, computation is performed at higher precision:
torch.bfloat16: Best choice for Ampere GPUs (A100, RTX 3090) and newer. Numerically stable.torch.float16: Alternative for GPUs that don't support bfloat16.torch.float32: Most accurate but slowest. Rarely needed.
bnb4bitusedoublequant
Double quantization (nested quantization) quantizes the quantization constants themselves. This saves approximately 0.4 bits per parameter, or about 3 GB for a 65B parameter model. The computational overhead is minimal.
# With double quant: ~3.5 GB for 7B model
configdouble = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="nf4",
bnb4bitcomputedtype=torch.bfloat16,
bnb4bitusedoublequant=True
)
Without double quant: ~3.9 GB for 7B model
configsingle = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="nf4",
bnb4bitcomputedtype=torch.bfloat16,
bnb4bitusedoublequant=False
)
Inference with Quantized Models
Once the model is loaded, inference works exactly like a regular model:
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch
quantizationconfig = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="nf4",
bnb4bitcomputedtype=torch.bfloat16,
bnb4bitusedoublequant=True
)
modelname = "mistralai/Mistral-7B-Instruct-v0.3"
tokenizer = AutoTokenizer.frompretrained(modelname)
model = AutoModelForCausalLM.frompretrained(
modelname,
quantizationconfig=quantizationconfig,
devicemap="auto"
)
def generateresponse(prompt, maxtokens=256):
messages = [
{"role": "user", "content": prompt}
]
inputids = tokenizer.applychattemplate(
messages,
returntensors="pt",
addgenerationprompt=True
).to(model.device)
outputs = model.generate(
inputids,
maxnewtokens=maxtokens,
temperature=0.7,
topp=0.9,
dosample=True,
repetitionpenalty=1.1
)
response = tokenizer.decode(
outputs[0][inputids.shape[1]:],
skipspecialtokens=True
)
return response
print(generateresponse("What are the advantages of using Python for data science?"))
print(generateresponse("Write a Python function to calculate fibonacci:"))
Batch Inference
To process multiple prompts at once:
def batchgenerate(prompts, maxtokens=256):
tokenizer.pad
token = tokenizer.eostoken
inputs = tokenizer(
prompts,
return
tensors="pt",
padding=True,
truncation=True,
maxlength=512
).to(model.device)
outputs = model.generate(
inputs,
maxnewtokens=maxtokens,
temperature=0.7,
dosample=True
)
responses = tokenizer.batchdecode(outputs, skipspecialtokens=True)
return responses
prompts = [
"Explain the concept of OOP in one paragraph:",
"What is a REST API?",
"Name 3 popular databases:"
]
results = batchgenerate(prompts)
for prompt, result in zip(prompts, results):
print(f"Q: {prompt}")
print(f"A: {result}\n")
Advanced Usage: Fine-Tuning with QLoRA
QLoRA (Quantized Low-Rank Adaptation) combines 4-bit quantization from bitsandbytes with LoRA from the PEFT library. This enables fine-tuning billion-parameter models on consumer GPUs.
QLoRA Architecture
QLoRA works by:
Complete QLoRA Implementation
from transformers import (
AutoModelForCausalLM,
AutoTokenizer,
BitsAndBytesConfig,
TrainingArguments
)
from peft import LoraConfig, getpeftmodel, preparemodelforkbittraining
from datasets import loaddataset
from trl import SFTTrainer
import torch
1. Quantization configuration
bnbconfig = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitquanttype="nf4",
bnb4bitcomputedtype=torch.bfloat16,
bnb4bitusedoublequant=True
)
2. Load model and tokenizer
modelname = "mistralai/Mistral-7B-Instruct-v0.3"
tokenizer = AutoTokenizer.frompretrained(modelname)
tokenizer.padtoken = tokenizer.eostoken
model = AutoModelForCausalLM.frompretrained(
modelname,
quantizationconfig=bnbconfig,
devicemap="auto",
torchdtype=torch.bfloat16
)
3. Prepare model for k-bit training
model = preparemodelforkbittraining(model)
4. LoRA configuration
loraconfig = LoraConfig(
r=16,
loraalpha=32,
targetmodules=[
"qproj", "kproj", "vproj", "oproj",
"gateproj", "upproj", "downproj"
],
loradropout=0.05,
bias="none",
tasktype="CAUSALLM"
)
5. Apply LoRA to model
model = getpeftmodel(model, loraconfig)
model.printtrainableparameters()
6. Load dataset
dataset = loaddataset("timdettmers/openassistant-guanaco", split="train[:5000]")
7. Format data
def formatinstruction(sample):
return f"""### Human: {sample['text'].split('### Human:')[1].split('### Assistant:')[0].strip()}
Assistant: {sample['text'].split('### Assistant:')[1].strip()}"""
8. Training configuration
trainingargs = TrainingArguments(
outputdir="./qlora-output",
numtrainepochs=1,
perdevicetrainbatchsize=4,
gradientaccumulationsteps=4,
learningrate=2e-4,
weightdecay=0.001,
warmupratio=0.03,
lrschedulertype="cosine",
loggingsteps=10,
savestrategy="steps",
savesteps=100,
fp16=False,
bf16=True,
optim="pagedadamw32bit",
gradientcheckpointing=True,
maxgradnorm=0.3,
reportto="none"
)
9. Initialize trainer
trainer = SFTTrainer(
model=model,
traindataset=dataset,
tokenizer=tokenizer,
args=trainingargs,
maxseqlength=512,
)
10. Start training
trainer.train()
11. Save adapter
trainer.savemodel("./qlora-adapter")
Important LoRA Parameters
r (rank): The dimension of the low-rank decomposition. Higher values mean more expressiveness but more parameters. Recommended: 8-64.# r=8: ~0.1% trainable params, fast, for simple tasks
r=16: ~0.2% trainable params, good balance
r=64: ~0.8% trainable params, for complex tasks
loraalpha: Scaling factor for LoRA weights. Usually set to 2x the r value.
targetmodules: Which layers receive LoRA adapters. For Llama/Mistral models, targeting all linear layers gives the best results.
Using Paged Optimizers
bitsandbytes provides paged optimizers that automatically offload optimizer states to CPU when GPU memory runs out:
import bitsandbytes as bnb
optimizer = bnb.optim.PagedAdamW32bit(
model.parameters(),
lr=2e-4,
weightdecay=0.001
)
Or use 8-bit Adam for even more memory savings
optimizer8bit = bnb.optim.Adam8bit(
model.parameters(),
lr=2e-4,
weightdecay=0.001
)
Paged optimizers work by leveraging CUDA unified memory for automatic page fault handling, similar to virtual memory in operating systems.
Saving and Loading Quantized Models
Saving QLoRA Adapters
# Save only the adapter (small size, ~50-200 MB)
model.savepretrained("./my-qlora-adapter")
tokenizer.savepretrained("./my-qlora-adapter")
Loading Adapters Back
from peft import PeftModel
Load base model in 4-bit
basemodel = AutoModelForCausalLM.frompretrained(
"mistralai/Mistral-7B-Instruct-v0.3",
quantizationconfig=bnbconfig,
devicemap="auto"
)
Apply adapter
model = PeftModel.frompretrained(basemodel, "./my-qlora-adapter")
Merging Adapters into Base Model
If you want to merge the adapter with the base model for simpler deployment:
from peft import PeftModel
from transformers import AutoModelForCausalLM, AutoTokenizer
Load base model in full precision for merging
basemodel = AutoModelForCausalLM.frompretrained(
"mistralai/Mistral-7B-Instruct-v0.3",
torchdtype=torch.float16,
devicemap="auto"
)
model = PeftModel.frompretrained(basemodel, "./my-qlora-adapter")
mergedmodel = model.mergeandunload()
mergedmodel.savepretrained("./merged-model")
tokenizer.savepretrained("./merged-model")
8-bit Optimizers in bitsandbytes
Beyond model quantization, bitsandbytes also provides 8-bit optimizers that reduce memory used by optimizer states:
import bitsandbytes as bnb
Drop-in replacement for torch.optim.Adam
optimizer = bnb.optim.Adam8bit(
model.parameters(),
lr=1e-4,
betas=(0.9, 0.999)
)
Also available:
bnb.optim.AdamW8bit - AdamW with weight decay
bnb.optim.SGD8bit - 8-bit SGD
bnb.optim.Adagrad8bit - 8-bit Adagrad
bnb.optim.LAMB8bit - 8-bit LAMB (for large batches)
8-bit optimizers save approximately 75% of memory for optimizer states. For a 7B model, this means saving around 8 GB of VRAM.
8-bit Linear Layers
bitsandbytes also provides Linear8bitLt and Linear4bit layers that can be used directly in custom architectures:
import bitsandbytes as bnb
import torch.nn as nn
class EfficientModel(nn.Module):
def init(self, inputdim, hiddendim, outputdim):
super().init()
self.layer1 = bnb.nn.Linear8bitLt(inputdim, hiddendim, hasfp16weights=False)
self.activation = nn.GELU()
self.layer2 = bnb.nn.Linear8bitLt(hiddendim, outputdim, hasfp16weights=False)
def forward(self, x):
x = self.layer1(x)
x = self.activation(x)
x = self.layer2(x)
return x
Best Practices
1. Choose the Right Quantization Method
| Scenario | Recommendation |
|----------|---------------|
| Inference only, adequate GPU | 8-bit (LLM.int8()) |
| Inference only, limited GPU | 4-bit NF4 + double quant |
| Fine-tuning | 4-bit NF4 + QLoRA |
| Production, need speed | GPTQ or AWQ |
| Research, need flexibility | bitsandbytes 4-bit |
2. Use bfloat16 as Compute Dtype
# Recommended for Ampere+ GPUs
config = BitsAndBytesConfig(
loadin4bit=True,
bnb4bitcomputedtype=torch.bfloat16 # not float16
)
bfloat16 has the same range as float32 (making it more numerically stable) compared to float16 which can overflow on large values.
3. Enable Gradient Checkpointing for Training
model.gradientcheckpointingenable()
Or through TrainingArguments
training
args = TrainingArguments(
gradientcheckpointing=True,
# ...
)
Gradient checkpointing significantly reduces memory usage with a trade-off of 20-30% slower training time.
4. Monitor Memory Usage
def printmemorystats():
print(f"Allocated: {torch.cuda.memory
allocated() / 10243:.2f} GB")
print(f"Reserved: {torch.cuda.memoryreserved() / 1024
3:.2f} GB")
print(f"Max Allocated: {torch.cuda.maxmemoryallocated() / 10243:.2f} GB")
Call before and after heavy operations
printmemorystats()
5. Model Size Comparison
Here's a practical comparison of VRAM usage for popular models:
| Model | float16 | 8-bit | 4-bit NF4 | 4-bit + DQ |
|-------|---------|-------|-----------|------------|
| Llama-2-7B | 14 GB | 7 GB | 3.9 GB | 3.5 GB |
| Mistral-7B | 14 GB | 7 GB | 3.9 GB | 3.5 GB |
| Llama-2-13B | 26 GB | 13 GB | 7.3 GB | 6.8 GB |
| Llama-2-70B | 140 GB | 70 GB | 38 GB | 35 GB |
6. Common Troubleshooting
CUDA Out of Memory Error:
# Reduce batch size
trainingargs = TrainingArguments(perdevicetrainbatchsize=1)
Enable gradient accumulation
trainingargs = TrainingArguments(
perdevicetrainbatchsize=1,
gradientaccumulation_steps=16
)
Installation fails on Linux:
# Make sure CUDA toolkit is installed
nvcc --version
Install specific version
pip install bitsandbytes==0.43.0
Model produces poor output after quantization:
Make sure you're using NF4 (not FP4) and bfloat16 compute dtype. If quality is still poor, try 8-bit as a more conservative alternative.
Conclusion
bitsandbytes is an invaluable tool in the modern LLM ecosystem. With support for 4-bit and 8-bit quantization, this library enables anyone to run and fine-tune large models on affordable hardware.
Key takeaways:
- For inference: Use 4-bit NF4 with double quantization for maximum memory savings
- For fine-tuning: Combine with PEFT/LoRA for QLoRA, enabling training on consumer GPUs
- 8-bit optimizers: Use
Adam8bitorPagedAdamW32bitto reduce optimizer state memory - Choose the right compute dtype**: bfloat16 for Ampere+ GPUs, float16 for older GPUs
With bitsandbytes, the barrier to working with LLMs is lower than ever. You can run a 7B parameter model on a GPU with just 6 GB of VRAM, or fine-tune a 13B model on a 16 GB GPU. This opens up tremendous opportunities for experimentation, research, and more accessible AI model deployment.