Skip to content

How to Use QLoRA Hugging Face on a Single GPU

Swarnava Dutta8 min read

Hugging Face PeftBitsandbytesTrl Sfttrainer

Contents

Illustration of how to use qlora huggingface: A massive stone block on the left is compressed by a wide press into a small

A 7B-parameter model in float16 needs roughly 14 GB of VRAM just to load - before a single gradient is computed. QLoRA solves this by quantizing frozen weights to 4-bit NF4 and training only small LoRA adapters in 16-bit. Learning how to use QLoRA with Hugging Face means wiring together bitsandbytes, PEFT, and TRL with the right configuration to stay inside that VRAM budget.

Quick answer

Load a model via AutoModelForCausalLM with a BitsAndBytesConfig set to 4-bit NF4 quantization and double quantization enabled. Attach LoRA adapters through PEFT's get_peft_model, then train with TRL's SFTTrainer using paged AdamW 8-bit. This fine-tunes Llama 3 8B on a single 24 GB GPU in roughly 10 GB of VRAM.

Prepare to Use QLoRA with Hugging Face on Your GPU

QLoRA freezes every base-model weight in 4-bit NF4 precision and trains only small low-rank adapters in bfloat16. The base parameters never receive gradients - optimizer states scale with adapter size, not the full model.

Before you start, confirm you have:

  • A CUDA-capable GPU with ≥16 GB VRAM (24 GB for comfortable 8B runs)
  • Python 3.10+, a matching CUDA toolkit (11.8 or 12.1), and Hugging Face account with gated-model access for Llama 3
  • Roughly 16 GB free disk for the quantized checkpoint and adapter weights
pip install transformers==4.44.0 peft==0.12.0 trl==0.10.1 \
  accelerate==0.33.0 datasets==2.20.0 bitsandbytes==0.43.3

On a 16 GB card, expect to need device_map="auto" with CPU offloading for the largest layers. A 24 GB RTX 3090 or 4090 fits an 8B model without offloading. For background on how quantization compresses weights without destroying accuracy, the choice between NF4 and FP4 matters in the next step.

Load Llama 3 with 4-Bit NF4 Quantization

Llama 3 8B Instruct (meta-llama/Meta-Llama-3-8B-Instruct) is gated. Run huggingface-cli login with an approved token before calling from_pretrained.

NF4 outperforms FP4 because pretrained transformer weights roughly follow a normal distribution, and NF4's quantization levels are designed to minimize error for that shape. Double quantization quantizes the quantization constants themselves from FP32 to FP8, saving an additional ~0.4 bits per parameter.

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch

bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,  # float16 if no Ampere+ GPU
)

model_id = "meta-llama/Meta-Llama-3-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id, quantization_config=bnb_config, device_map="auto"
)
model.config.use_cache = False
Flow showing how to use qlora huggingface tools to load a Llama 3 checkpoint as a device-mapped 4-bit NF4 model.
Loading a 4-bit NF4 base model

After loading, model.get_memory_footprint() should show roughly 5-6 GB, with storage dtype uint8 for quantized layers and computation in bfloat16.

Configure the 4-Bit Quantization Settings Safely

Use bfloat16 as compute dtype on Ampere or newer GPUs (RTX 30xx/40xx, A100). On Turing cards (T4, RTX 20xx), fall back to float16 - bfloat16 silently produces garbage on hardware without native support.

Only base-model weights get quantized. The LoRA adapters stay in bfloat16/float16; quantizing trainable parameters destroys gradient flow. The bnb_4bit_compute_dtype controls the dtype during forward-pass dequantization, not adapter storage.

A mismatch between your CUDA toolkit and the bitsandbytes wheel causes a libcudart.so error at import time. Pin bitsandbytes==0.43.3 against CUDA 12.1. If model.hf_device_map shows any layer on cpu, quantized kernels won't run - bitsandbytes 4-bit requires GPU execution for every quantized module.

Build an Instruction-Tuning Dataset and Chat Template

Instruction tuning trains on prompt - response pairs so the model follows task instructions rather than completing raw text. It improves compliance with user requests but does not inject reliable factual knowledge, so curate examples for format and reasoning rather than memorization.

Load a dataset with the Hugging Face Datasets library and map each example into the model's chat format. Drop malformed rows, check sequence lengths, and split train/validation before deduplication.

from datasets import load_dataset

dataset = load_dataset("yahma/alpaca-cleaned", split="train")

def format_chat(example):
    messages = [
        {"role": "user", "content": example["instruction"]},
        {"role": "assistant", "content": example["output"]},
    ]
    text = tokenizer.apply_chat_template(
        messages, tokenize=False, add_generation_prompt=False
    )
    return {"text": text + tokenizer.eos_token}

# Drop empty responses before formatting
dataset = dataset.filter(lambda x: len(x["output"].strip()) > 0)
dataset = dataset.map(format_chat)
dataset = dataset.train_test_split(test_size=0.05, seed=42)

Truncate anything above max_seq_length (1024 is a safe starting point on 24 GB). Verify zero instruction overlap across splits to prevent train-validation leakage.

Configure QLoRA Adapters with Hugging Face PEFT

For Llama 3, set target_modules to the actual module names in LlamaForCausalLM. Copying names from a Mistral or Falcon config silently creates zero adapters because PEFT matches by string.

At r=16 across seven projections, you train under 1% of an 8B model's parameters. I default to r=16 and only increase after validation loss plateaus.

from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

prepare_model_for_kbit_training(model)  # skipping this causes NaN gradients

lora_config = LoraConfig(
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=[
        "q_proj", "k_proj", "v_proj", "o_proj",
        "gate_proj", "up_proj", "down_proj",
    ],
)

model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# Expected: ~50M trainable out of ~8B total

Rank and target-module coverage control adapter capacity directly. Wider coverage (all seven projections vs. only q_proj/v_proj) increases trainable parameters and memory but improves convergence on complex tasks.

Set Up TRL SFTTrainer and Training Arguments

Set per_device_train_batch_size=2, gradient_accumulation_steps=4, one GPU - effective batch size is 2 × 4 × 1 = 8. Enable gradient_checkpointing=True to trade slower backward passes for noticeably lower activation memory usage.

Paged AdamW offloads optimizer states to CPU RAM only when GPU memory pressure triggers a page fault - it prevents OOM crashes during activation spikes rather than accelerating every step.

from trl import SFTTrainer
from transformers import TrainingArguments

training_args = TrainingArguments(
    output_dir="./qlora-llama3-out",
    per_device_train_batch_size=2,
    gradient_accumulation_steps=4,
    gradient_checkpointing=True,
    optim="paged_adamw_8bit",
    learning_rate=2e-4,
    warmup_ratio=0.03,
    lr_scheduler_type="cosine",
    num_train_epochs=1,
    logging_steps=10,
    eval_strategy="steps",
    eval_steps=100,
    save_steps=200,
    bf16=True,
)

trainer = SFTTrainer(
    model=model,
    train_dataset=dataset["train"],
    eval_dataset=dataset["test"],
    peft_config=lora_config,
    max_seq_length=1024,
    packing=False,
    tokenizer=tokenizer,
    args=training_args,
)

Pass LoraConfig directly to SFTTrainer via peft_config instead of wrapping the model yourself. With TRL's SFTTrainer wired to a reward model, you can later extend this adapter into RLHF.

Run QLoRA Training and Measure VRAM and Speed

Call trainer.train() and watch the first 20 steps. You should see loss begin to drop within the first 50 steps on a well-formatted instruction set; a flat line means the chat template is malformed or adapters attached to zero modules.

Reset CUDA memory counters before training with torch.cuda.reset_peak_memory_stats(), then read torch.cuda.max_memory_allocated() afterward. Record GPU model, CUDA version, bitsandbytes version, sequence length, batch size, and whether gradient checkpointing is enabled - without these, no one can reproduce the number.

OOM on step 1 means reducing batch size to 1 or cutting max_seq_length to 512. NaN loss usually traces to a missing prepare_model_for_kbit_training call or float16 compute dtype on a non-Ampere GPU. If print_trainable_parameters() shows 8 B trainable instead of 50 M, PEFT matched zero modules and the full model trains unfrozen.

Save, Reload, and Test the QLoRA Adapter

Call trainer.model.save_pretrained("llama3-qlora-adapter") and tokenizer.save_pretrained("llama3-qlora-adapter"). This writes only adapter weights (~100 MB for r=16) and adapter_config.json - never a second copy of the base model.

Reload the base checkpoint with the same BitsAndBytesConfig, then call PeftModel.from_pretrained(base_model, "llama3-qlora-adapter"). Compare a held-out prompt before and after tuning. A few qualitative examples catch broken templates and degenerate repetition but do not substitute for a scored benchmark.

For deployment, model.merge_and_unload() merges adapters into base weights. Merging dequantizes frozen layers to bfloat16 temporarily, spiking peak memory. Serving frameworks like vLLM often need a merged or freshly quantized GPTQ/AWQ artifact - raw PEFT adapters on a 4-bit base are not universally supported. Always version-lock the base-model commit hash, LoraConfig parameters, and bitsandbytes + PEFT versions alongside the adapter directory.

Diagnose Why QLoRA Can Be Slower Than LoRA

QLoRA trades compute for memory. Every forward pass dequantizes frozen NF4 weights to bfloat16 before matrix multiplication - LoRA skips that step because its base weights already sit in 16-bit.

Common bottlenecks, in the order I check them:

  • Data loading: nvidia-smi shows <80% GPU utilization while CPU is pegged - add dataloader_num_workers=4.
  • Gradient checkpointing: recomputes activations during backward. Disable temporarily to isolate whether the slowdown is checkpointing or dequantization.
  • Padding waste: short sequences padded to max_seq_length burn FLOPs on pad tokens. Enable packing=True when average example length is under half the max.
Comparison showing how to use qlora huggingface choices: QLoRA saves memory through quantization, while LoRA avoids dequantization for faster iteration.
Why QLoRA may run slower than LoRA

QLoRA frees enough VRAM to double batch size or extend sequence length, which reduces optimizer-step overhead per token and narrows the throughput gap once the GPU is fully saturated.

Choose Between QLoRA, LoRA, DoRA, and Full Fine-Tuning

PEFT is Hugging Face's library for parameter-efficient fine-tuning - LoRA, DoRA, and QLoRA are methods within it. DoRA splits each adapter update into magnitude and direction components, adding compute overhead in exchange for improved adaptation on format-sensitive tasks.

Criterion Full Fine-Tuning LoRA QLoRA DoRA
Base-weight precision bf16/fp16 bf16/fp16 4-bit NF4 bf16/fp16
Trainable params (8B) 100% <1% <1% ~1%
VRAM demand (8B model) ~48 GB+ ~18 GB ~10 GB ~20 GB
Training speed Fastest per step Fast Slower (dequant overhead) Slower than LoRA
Adapter size on disk Full checkpoint ~100 MB ~100 MB ~120 MB
Deployment complexity Ship full weights Merge or serve adapter Merge; limited 4-bit serving Merge; limited serving support
Best-fit workload Multi-task, large cluster Fast iteration, ≥48 GB GPU Single 24 GB GPU Style/format-sensitive tasks

On a single 24 GB card, QLoRA is the only option that fits an 8B model end to end. When iteration speed matters and an A100 is available, standard LoRA removes the dequantization tax. I reach for DoRA only when LoRA plateaus on format-heavy tasks - its serving support in vLLM and TGI lags behind vanilla LoRA. Full fine-tuning stays reserved for teams with multi-node budgets shifting the entire weight distribution. For a complementary approach that shrinks a model without fine-tuning adapters, model distillation trades a different set of costs.

FAQ

What is QLoRA used for?

QLoRA fine-tunes large language models on GPUs with limited VRAM by quantizing frozen base weights to 4-bit NF4 and training only small LoRA adapters in 16-bit. This makes it possible to adapt an 8B-parameter model on a single 24 GB consumer GPU like the RTX 4090.

Is QLoRA faster than LoRA?

No. QLoRA is slower per step than LoRA because every forward pass dequantizes NF4 weights before matrix multiplication. The advantage is memory, not speed: QLoRA frees enough VRAM to increase batch size or sequence length, which can raise overall throughput once the GPU is fully saturated.

What are the key differences between PEFT, LoRA, and QLoRA?

PEFT is Hugging Face's library for parameter-efficient fine-tuning. LoRA adds low-rank adapter matrices to frozen 16-bit base weights. QLoRA applies the same adapter approach but first quantizes frozen weights to 4-bit NF4 via bitsandbytes, cutting VRAM at the cost of dequantization overhead during training.

How to instruction tune an LLM?

Format training examples as prompt - response pairs using the model's chat template, then train with a causal language modeling objective. With Hugging Face, load a quantized base model, attach LoRA or QLoRA adapters via PEFT, and pass the formatted dataset to TRL's SFTTrainer. Validate on held-out examples to confirm instruction following improves on your target task.

Keep reading

Illustration of how to do lora fine tuning: A massive riveted steel gear spans the left frame, motionless; meshed with it onLoRA Fine Tuning Tutorial

9 min read

How to Do LoRA Fine-Tuning: Practical LLM Workflow for 2026

Learn how to do LoRA fine tuning for LLMs, from low-rank math and dataset setup to rank, alpha, memory, evaluation, adapter merging, and inference.

The first time I tried to learn how to do LoRA fine-tuning, I picked rank 64, left alpha at its default, targeted every linear layer, and wondered why my 24 GB GPU ran out of memory on a 7B model. The adapter was "small" - barely 1% of total parameters - yet I'd misconfigured enough knobs to burn an entire…

Read more

Illustration of how llm quantization works: A wide shelf: left side holds a tall stack of full glasses of water spanningLLM Model Quantization

11 min read

How LLM Quantization Works: 4-Bit, 8-Bit Tradeoffs

Learn how LLM quantization works, from numeric mapping to 4-bit and 8-bit deployment, and choose the right balance of memory, speed, and accuracy.

I once shrank a 13B model to 4-bit and celebrated way too early. At FP16 that model needed roughly 26GB just for weights (13 billion params × 16 bits ÷ 8), and quantizing to 4-bit should get you to roughly a quarter of that plus some scale metadata - enough to fit the single GPU I'd fought to get approved.…

Read more

Illustration of how to rlhf llm: A wide workbench: at left a rough stone block being chiseled into shape (SFT), center aRLHF For LLMs

8 min read

How to RLHF an LLM with Hugging Face TRL and PPO Steps

Learn how to RLHF an LLM with Hugging Face TRL through SFT, reward modeling, and PPO, plus GPU sizing tips and sycophancy evaluation gates before deployment.

Most RLHF tutorials stop at a diagram and never show the testable artifact each stage produces. This walkthrough takes an open-source base model through SFT, reward modeling, PPO, and sycophancy evaluation using Hugging Face TRL - focusing on config choices, GPU sizing, and failure modes rather than theory. To RLHF an LLM, run three sequential training stages using Hugging Face…

Read more

All posts