Skip to main content
Start your own AI-powered blog — freeGet started →

QLoRA 4-bit Fine-Tuning Tutorial: Single GPU, 7B to 70B

Podcast episode2 voices
4:52
QLoRA 4-bit Fine-Tuning Tutorial: Single GPU, 7B to 70B
Photo by AltumCode on unsplash

QLoRA 4-bit Fine-Tuning Tutorial: Single GPU, 7B to 70B

Code editor showing Python training script on one screen and GPU metrics on another Photo by AltumCode on Unsplash

Quick Answer: QLoRA (Quantized Low-Rank Adaptation) lets you fine-tune models up to 70B parameters on a single consumer GPU by keeping the base model in 4-bit and training only small adapter weights. A 7B model needs ~7GB VRAM, 13B needs ~10GB, 34B needs ~16GB, and 70B needs ~24-32GB VRAM. This tutorial covers the complete setup: environment, dataset preparation, configuration, training loop, saving, and inference.

What Is QLoRA and Why Use It?

QLoRA combines two techniques:

  • Quantization (Q): The base model is stored in 4-bit precision (NF4), reducing memory from 2 bytes per parameter to 0.5 bytes
  • LoRA: Small, low-rank adapter matrices are trained in full precision while the base model stays frozen
TechniqueBase Model PrecisionTrainable ParametersMemory for 70BQuality
Full fine-tune (FP16)FP16All 70B700+ GBBest
LoRA (FP16)FP16~0.1-1%160 GBNear-best
QLoRA (NF4)NF4 (4-bit)~0.1-1%35-48 GBNear-LoRA

"QLoRA is the most efficient way to adapt large models when you don't have a GPU cluster. The quality gap with full fine-tuning is minimal for most tasks, especially with rank >= 64." — Tim Dettmers, QLoRA paper author

Hardware Requirements by Model Size

Single GPU — Inference-Grade Fine-Tuning

Model SizeMin VRAMRecommended VRAMGPU Options
1B-3B4 GB6 GBRTX 3060 12GB, RTX 4060
7B-8B8 GB12 GBRTX 3060 12GB, RTX 4060 Ti 16GB
13B12 GB16 GBRTX 4070 Ti Super 16GB
34B16 GB24 GBRTX 4090 24GB
70B28 GB32 GBRTX 5090 32GB, A6000 48GB
120B48 GB80 GBA100 80GB, H100 80GB

Key Memory Consumers in QLoRA

code
Model weights (NF4):     0.5 bytes × 70B = 35 GB
LoRA adapters (FP16):    2 bytes × rank × layers (~200MB for rank=16)
Gradients:               2 bytes × rank × layers (~200MB)
Optimizer states (Adam): 8 bytes × rank × layers (~800MB for rank=16)
Activations + buffer:    2-8 GB (batch_size dependent)
────────────────────────────────────────────────────
Total:                   ~38-44 GB for 70B

Environment Setup

Install Dependencies

bash
# Python 3.10+ required
pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu124

pip install transformers datasets accelerate peft trl bitsandbytes
pip install wandb  # optional: experiment tracking
pip install xformers  # optional: memory-efficient attention

Verify GPU Setup

python
import torch
import bitsandbytes as bnb

print(f"CUDA available: {torch.cuda.is_available()}")
print(f"GPU: {torch.cuda.get_device_name(0)}")
print(f"VRAM: {torch.cuda.get_device_properties(0).total_memory / 1e9:.1f} GB")
print(f"BitsAndBytes version: {bnb.__version__}")

Dataset Preparation

Format Your Data

QLoRA works best with a structured chat format. Here's the recommended format for instruction tuning:

json
[
  {
    "instruction": "Explain the difference between LoRA and QLoRA",
    "input": "",
    "output": "LoRA adds trainable low-rank matrices to attention layers..."
  },
  {
    "instruction": "Write a Python function for matrix multiplication",
    "input": "The matrices are 3x3",
    "output": "```python
def multiply_3x3(A, B):
    ...```"
  }
]

Load and Tokenize

python
from datasets import load_dataset

dataset = load_dataset("json", data_files="training_data.json")

def formatting_func(example):
    text = f"### Instruction: {example['instruction']}
"
    if example['input']:
        text += f"### Input: {example['input']}
"
    text += f"### Response: {example['output']}"
    return {"text": text}

dataset = dataset.map(formatting_func)

# Tokenize
def tokenize(element):
    return tokenizer(
        element["text"],
        truncation=True,
        max_length=2048,
        padding="max_length"
    )

dataset = dataset.map(tokenize, remove_columns=["text", "instruction", "input", "output"])

Model Loading with 4-bit Quantization

This is the core of QLoRA — loading the base model in NF4 (4-bit NormalFloat):

python
import torch
from transformers import (
    AutoTokenizer,
    AutoModelForCausalLM,
    BitsAndBytesConfig
)

model_name = "meta-llama/Llama-3.1-8B"  # or "meta-llama/Llama-3.1-70B"

# 4-bit quantization config
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",           # NF4 is QLoRA's recommended type
    bnb_4bit_compute_dtype=torch.bfloat16, # Compute in BF16 for stability
    bnb_4bit_use_double_quant=True,       # Double quantization for extra savings
)

# Load tokenizer and model
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token

model = AutoModelForCausalLM.from_pretrained(
    model_name,
    quantization_config=bnb_config,
    device_map="auto",                    # Auto-distribute across GPU/CPU
    torch_dtype=torch.bfloat16,
    trust_remote_code=True,
)

Detailed view of a screwdriver set in a red case, featuring interchangeable bits and tools. Photo by Ismael Campos Carrillo on Pexels

LoRA Configuration

python
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# Prepare model for k-bit training (freeze base, enable gradients for LoRA)
model = prepare_model_for_kbit_training(model)

# LoRA configuration
lora_config = LoraConfig(
    r=16,                          # Rank — higher = more expressive but more memory
    lora_alpha=32,                 # Scaling factor (typically 2x rank)
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],  # All linear layers
    lora_dropout=0.05,             # Dropout for regularization
    bias="none",                   # Don't train bias terms
    task_type="CAUSAL_LM",
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Output: trainable params: 8,388,608 || all params: 7,000,000,000 || trainable: 0.12%

Choosing the Right Rank

RankTrainable ParamsMemory OverheadQualityBest For
r=8~0.06%MinimalAdequateSimple task adaptation
r=16~0.12%LowGoodDefault — most tasks
r=32~0.24%ModerateBetterComplex reasoning tasks
r=64~0.48%HigherBestDomain-specific expertise
r=128~0.96%HighMarginal gainsVery large datasets

Training Configuration

python
from transformers import TrainingArguments
from trl import SFTTrainer

# Training arguments optimized for single GPU
training_args = TrainingArguments(
    output_dir="./qlora-output",
    per_device_train_batch_size=1,       # Small batch for large models
    gradient_accumulation_steps=4,       # Accumulate to simulate batch_size=4
    gradient_checkpointing=True,          # Memory savings (trade: slower)
    num_train_epochs=3,                   # Adjust based on dataset size
    learning_rate=2e-4,                   # Higher than full fine-tune
    fp16=True,                            # Mixed precision training
    logging_steps=10,
    save_steps=100,
    save_total_limit=2,
    optim="paged_adamw_8bit",             # 8-bit optimizer saves VRAM
    lr_scheduler_type="cosine",
    warmup_ratio=0.03,
    max_grad_norm=0.3,
    report_to="none",                     # or "wandb"
)

# Use SFTTrainer for supervised fine-tuning
trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset["train"],
    tokenizer=tokenizer,
    dataset_text_field="text",
    max_seq_length=2048,
    packing=True,                          # Pack multiple short sequences
)

Running the Training Loop

python
# Start training
trainer.train()

# If you want to resume from a checkpoint:
# trainer.train(resume_from_checkpoint=True)

Monitoring Training

Watch these metrics in your training logs:

code
Step    Training Loss   VRAM Used   Tokens/sec
10      1.85            11.2 GB     420
50      1.42            11.2 GB     415
100     1.18            11.2 GB     418
200     0.95            11.2 GB     422

Warning signs:

  • VRAM approaching 100% → reduce batch size or max_seq_length
  • Loss not decreasing → check learning rate or dataset quality
  • Loss = 0.0 → model is memorizing (overfitting), add dropout or reduce epochs

Saving and Loading the Adapter

Save the LoRA Adapter (NOT the full model)

python
# Save only the LoRA weights (tiny, ~10-50MB)
model.save_pretrained("./my-finetuned-adapter")
tokenizer.save_pretrained("./my-finetuned-adapter")

# Optional: merge with base model for faster inference
from peft import PeftModel

merged_model = model.merge_and_unload()
merged_model.save_pretrained("./my-merged-model")

Load for Inference

python
# Option 1: Load base + adapter separately
from peft import PeftModel

base_model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Llama-3.1-8B",
    quantization_config=bnb_config,
    device_map="auto"
)
model = PeftModel.from_pretrained(base_model, "./my-finetuned-adapter")

# Option 2: Load merged model
model = AutoModelForCausalLM.from_pretrained("./my-merged-model", device_map="auto")

Inference with the Fine-Tuned Model

python
def generate_response(prompt, max_length=512):
    messages = [{"role": "user", "content": prompt}]
    input_ids = tokenizer.apply_chat_template(
        messages,
        return_tensors="pt",
        add_generation_prompt=True
    ).to("cuda")

    outputs = model.generate(
        input_ids,
        max_new_tokens=max_length,
        temperature=0.7,
        top_p=0.9,
        do_sample=True,
    )

    response = tokenizer.decode(outputs[0][input_ids.shape[1]:], skip_special_tokens=True)
    return response

# Test it
print(generate_response("Explain quantum computing in simple terms"))

Common Errors and Solutions

ErrorCauseSolution
CUDA out of memoryModel too large for VRAMReduce batch size, max_seq_length, or use smaller rank
bitsandbytes not compatibleWrong CUDA versionReinstall: pip install bitsandbytes --force-reinstall
Loss is NaNLearning rate too highReduce lr to 1e-4 or 5e-5
Training very slowNo xformers/flash-attnInstall: pip install flash-attn --no-build-isolation
Tokenizer missing pad_tokenSome models don't set itExplicitly set tokenizer.pad_token = tokenizer.eos_token

Related Reads

Advanced Memory Optimization Techniques

Beyond the core QLoRA setup, several advanced techniques can further reduce VRAM usage or accelerate training without sacrificing quality. FlashAttention-2 is a drop-in replacement for standard attention that reduces memory overhead by computing attention in blocks and avoiding materialization of large intermediate matrices. Install it via pip install flash-attn --no-build-isolation and enable it by passing attn_implementation="flash_attention_2" to from_pretrained(). This can cut activation memory by 20-30% for long sequences (e.g., 2048 tokens), allowing larger batch sizes or higher ranks.

For models exceeding 34B parameters, CPU offloading can help bridge the gap between GPU VRAM and model size. Use device_map="auto" with accelerate to automatically offload layers to CPU when GPU memory is exhausted. While this slows training due to PCIe bottlenecks, it enables fine-tuning 70B models on GPUs with as little as 24GB VRAM. Combine this with max_memory in from_pretrained() to explicitly control offloading:

``python max_memory = {0: "24GB", "cpu": "64GB"} # 24GB GPU, 64GB RAM model = AutoModelForCausalLM.from_pretrained( model_name, quantization_config=bnb_config, device_map="auto", max_memory=max_memory ) ``

Gradient checkpointing (enabled via gradient_checkpointing=True) is another critical tool, trading compute for memory by recomputing activations during the backward pass instead of storing them. For QLoRA, this can reduce memory usage by 30-40% at the cost of ~20% slower training. Pair it with gradient_accumulation_steps to simulate larger batch sizes without increasing VRAM.

Dataset Curation and Quality Control

The quality of your fine-tuning dataset often matters more than its size. For instruction tuning, prioritize diverse, high-quality examples over sheer volume—1,000 well-curated samples can outperform 10,000 noisy ones. Use these strategies to improve dataset efficacy:

  • Deduplication: Remove near-identical examples (e.g., using MinHash or exact string matching) to prevent overfitting and reduce training time. Tools like datasetsdeduplicate method or datasketch can automate this.
  • Difficulty balancing: Include a mix of simple and complex instructions to prevent the model from over-optimizing for easy tasks. Use perplexity or human evaluation to filter out ambiguous or low-quality examples.
  • Domain-specific augmentation: For specialized tasks (e.g., coding, legal), generate synthetic examples using templates or weaker models, then validate them with domain experts or automated tests.

For multi-turn conversations, structure your dataset to simulate realistic dialogue flows. Use a format like:

``json { "conversations": [ {"role": "user", "content": "Explain QLoRA in one sentence."}, {"role": "assistant", "content": "QLoRA fine-tunes large models on consumer GPUs by quantizing the base model to 4-bit and training small adapter weights."}, {"role": "user", "content": "What’s the memory savings?"}, {"role": "assistant", "content": "~4x compared to FP16, e.g., 70B fits in ~40GB VRAM."} ] } ``

Tokenize multi-turn data with tokenizer.apply_chat_template() to ensure proper role separation and special tokens. For long sequences, use truncation=True and max_length to avoid OOM errors, but ensure critical context isn’t lost—consider splitting long conversations into chunks with overlapping context.

Post-Training Evaluation and Iteration

After training, evaluate your model rigorously to identify strengths and weaknesses before deployment. Start with automated metrics like perplexity (for language modeling) or task-specific scores (e.g., ROUGE for summarization, pass@k for coding). For instruction-tuned models, use human evaluation or LLM-as-a-judge frameworks to assess response quality, coherence, and adherence to instructions. Tools like lm-evaluation-harness or FastChat can streamline this process.

Key evaluation dimensions include:

  • Faithfulness: Does the model follow instructions precisely, or does it hallucinate or deviate?
  • Safety: Does it avoid harmful, biased, or toxic outputs? Use benchmarks like ToxiGen or BBQ to test this.
  • Robustness: How well does it handle edge cases, typos, or ambiguous inputs?
  • Efficiency: Measure inference latency and VRAM usage, especially if deploying on edge devices.

For iterative improvement, ablation studies help isolate the impact of hyperparameters. Test variations in LoRA rank (e.g., 16 vs. 64), learning rate (e.g., 2e-4 vs. 1e-4), or dataset size (e.g., 1k vs. 10k examples) while holding other variables constant. Use WandB or TensorBoard to track experiments and compare results visually.

If the model underperforms, diagnose the issue:

  • High training loss but low eval loss: Likely overfitting—reduce epochs, add dropout, or increase dataset diversity.
  • Low training and eval loss but poor outputs: The model may be memorizing—check for dataset leakage or insufficient instruction diversity.
  • High eval loss but good outputs: The evaluation metric may not align with your task (e.g., perplexity vs. human judgment).

Finally, consider quantizing the merged model for deployment. Use bitsandbytes’s nn.Linear4bit or GGUF to convert the model to 4-bit or 8-bit precision, reducing inference memory by 2-4x with minimal quality loss. For example:

``python from bitsandbytes.nn import Linear4bit model = AutoModelForCausalLM.from_pretrained("./my-merged-model", load_in_4bit=True) ``

Key Takeaways

  • QLoRA enables fine-tuning models up to 70B parameters on a single consumer GPU by quantizing the base model to 4-bit (NF4) while training only small LoRA adapter weights, reducing VRAM needs to ~24-32GB for 70B models.
  • Use BitsAndBytesConfig with load_in_4bit=True, bnb_4bit_quant_type="nf4", and bnb_4bit_compute_dtype=torch.bfloat16 to load models in 4-bit precision, cutting memory usage by ~4x compared to FP16.
  • Set LoRA rank (r) between 16-64 for most tasks—higher ranks (e.g., 64) improve quality for complex reasoning but increase memory overhead (~0.5% trainable parameters per 64 rank for 70B models).
  • Optimize training for single-GPU setups with gradient_accumulation_steps=4, gradient_checkpointing=True, and paged_adamw_8bit to fit large models without OOM errors, even with per_device_train_batch_size=1.
  • Save only the LoRA adapter weights (~10-50MB) instead of the full model to preserve disk space; merge adapters with the base model post-training for faster inference if needed.
  • Monitor VRAM usage and training loss closely—spikes in VRAM or stagnant loss indicate overfitting, requiring adjustments to batch size, learning rate (2e-4 is a safe starting point), or dataset quality.

Frequently Asked Questions

What's the difference between QLoRA and regular LoRA?

QLoRA quantizes the base model to 4-bit, reducing memory by 4x compared to LoRA (which keeps the base in FP16). The LoRA adapters themselves are trained in FP16 in both cases. QLoRA trades ~1-2% quality for ~4x memory savings.

Can I fine-tune a 70B model on an RTX 5090 (32GB)?

Yes, but it's tight. 70B at NF4 is ~35GB for weights. With LoRA adapters, gradients, and optimizer states, you need ~38-44GB. You can make it fit by enabling gradient checkpointing, using paged AdamW 8-bit, and setting gradient_accumulation_steps > 1 with batch_size=1.

How long does QLoRA training take?

For a 7B model with 1,000 examples, rank=16, 3 epochs:

Does QLoRA work with vision models?

QLoRA works best with text-only LLMs. For vision-language models (LLaVA, Qwen-VL), use LoRA with the language component only. Vision encoders are typically frozen during fine-tuning.

Can I use QLoRA with any HuggingFace model?

Any model that supports transformers and has linear layers for attention works. Models from Llama, Mistral, Qwen, DeepSeek, and Phi families are all supported. The target_modules list may need adjustment for different architectures.

S
Synor

1 followers

Deep dives on GPUs, decentralized AI, crypto, and open-source ML — buying guides, benchmarks, and tax/compliance explainers.

Comments

Sign in to join the conversation

No comments yet. Be the first to share your thoughts!

More from Synor

Recommended for you