Dev

LoRA vs QLoRA vs Full Fine-Tuning: Cost, Quality, and When Each Makes Sense

Back to Tutorials

The decision is really about adaptation capacity versus memory

Fine-tuning solves a specific problem: the base model is broadly capable, but it does not reliably perform your repeated task in your expected format, terminology, tone, or decision policy.

Examples include:

  • Producing a strict JSON schema after receiving an internal support ticket.
  • Writing database queries in an organization-specific dialect and naming convention.
  • Classifying specialized medical, legal, manufacturing, or financial text using labels absent from general instruction data.
  • Following a stable writing style that prompt engineering alone cannot make reliable.
  • Learning a task pattern from thousands of demonstrations where putting every example into a prompt would be expensive or impossible.

The three approaches differ in what is allowed to change during training:

MethodFrozen base model?Trainable weightsBase model precision during trainingMain reason to choose it
Full fine-tuningNoAll model parametersUsually BF16 or FP16Maximum adaptation capacity
LoRAYesSmall low-rank adapter matricesUsually BF16 or FP16Strong practical default when the base model fits in VRAM
QLoRAYesSmall low-rank adapter matricesBase model loaded in 4-bit; adapters train in higher precisionFit a larger model into limited VRAM

Do not interpret this as a permanent quality ranking. A well-designed LoRA training run can beat a rushed full fine-tune because dataset quality, evaluation design, training duration, learning rate, and example formatting matter more than the name of the method. Full fine-tuning offers the greatest capacity to alter a model, not an automatic guarantee of better production behavior.

A sensible default is:

  1. Prove that fine-tuning is needed with a held-out evaluation set.
  2. Start with LoRA if the model fits comfortably in training memory.
  3. Use QLoRA if ordinary LoRA does not fit on the available GPU.
  4. Escalate to full fine-tuning only after LoRA has failed on a measurable requirement and the training budget can support it.

What each technique actually changes

Full fine-tuning updates every parameter

In full fine-tuning, every weight in the transformer is trainable. Training therefore stores substantially more than the model weights: gradients, optimizer state, often master weights, temporary activation tensors, and memory required by the current batch and sequence length.

This method is appropriate when you need fundamental adaptation rather than a focused behavioral overlay. Typical cases include a lab producing a major domain model version, continued pretraining on a large domain corpus, or a high-stakes task where a carefully run adapter experiment has demonstrated a repeatable quality gap.

Its costs are operational as well as financial:

  • Training memory grows sharply because optimizer state exists for every model parameter.
  • Checkpoints are large because they contain a complete changed model rather than a small adapter.
  • Iteration is slower and more expensive.
  • Catastrophic forgetting is a more serious risk because every weight can move.
  • Serving several task variants requires storing and deploying several full model copies unless you build a separate model-management strategy.

For planning purposes, reports commonly place full fine-tuning of a 7B model around 100–120 GB of VRAM, depending heavily on precision, optimizer, sequence length, batch size, gradient checkpointing, and sharding. Treat that as an order-of-magnitude planning input, not a capacity guarantee.

LoRA learns a low-rank update

LoRA, or Low-Rank Adaptation, keeps the original weight matrix frozen. Instead of directly updating a weight matrix (W), it learns a small update:

W' = W + scale × (B × A)

A and B are much smaller matrices than W. The rank r controls their size and therefore their adaptation capacity. Rather than training all billions of base parameters, LoRA trains these added adapter weights at selected linear layers, commonly attention projections and sometimes MLP projections.

The benefits are substantial:

  • Only a small fraction of parameters require gradients and optimizer state.
  • The frozen base model remains reusable.
  • Adapter artifacts are small enough to version separately from the base model.
  • One base model can support multiple adapters for different tasks.
  • At inference, compatible LoRA weights can often be merged into a copy of the base model to avoid adapter-layer overhead.

LoRA does not make the base model itself smaller. If an 8B model in BF16 cannot fit alongside activations and training overhead on your GPU, conventional LoRA may still fail with an out-of-memory error.

QLoRA is LoRA with a quantized frozen base

QLoRA applies LoRA while loading the frozen base model in 4-bit quantized form. The common design uses 4-bit NormalFloat (NF4), double quantization of quantization constants, and memory-management techniques such as paged optimizers. The adapters themselves are trained in higher precision.

The practical consequence is simple: QLoRA allows a model that would not fit as a BF16/FP16 LoRA base to fit into a much smaller VRAM budget.

That makes QLoRA valuable for a single 24 GB consumer GPU, limited cloud hardware, or larger models where full-precision LoRA is not viable. It is not free:

  • Quantized kernels and package compatibility can be painful to install and debug.
  • Training can be slower than ordinary LoRA.
  • Quantization introduces another variable when diagnosing quality differences.
  • A model that fits at idle can still run out of memory during the backward pass because activations are not reduced to 4-bit in the same way as frozen model weights.
  • QLoRA is a training technique; serving the final model in 4-bit has its own accuracy, throughput, and runtime compatibility trade-offs.

Choose based on evidence, not a generic rule

Use this decision process before renting GPUs.

Step 1: Define the failure that training must fix

Write a testable statement such as:

Given a sanitized customer request, generate valid JSON matching schema version 3, select one of 18 allowed dispositions, and cite only identifiers present in the input.

Avoid goals such as “make it smarter” or “make it understand our business.” Those cannot tell you whether fine-tuning worked.

Build a held-out evaluation set before training. It must contain examples that are never used as training examples. Include realistic edge cases, adversarial inputs, ambiguous requests, long inputs, and examples from the most expensive failure modes.

Step 2: Determine whether your limitation is data or knowledge access

Fine-tuning is weak at keeping rapidly changing facts current. If the model needs today’s inventory, internal policy text, account balances, or current documentation, use retrieval or tool calls. Fine-tuning may still be useful for how the system calls tools, chooses a workflow, emits structured output, or writes a response after retrieval.

If examples conflict, labels are noisy, or the desired behavior is unspecified, increasing rank or buying more GPUs will not solve the real problem.

Step 3: Measure your real memory envelope

Record:

  • GPU model and available VRAM, not just total installed VRAM.
  • Base model parameter count and architecture.
  • Maximum sequence length required by production inputs.
  • Micro-batch size.
  • Whether gradient checkpointing is enabled.
  • Precision and optimizer choice.
  • Number and location of LoRA target modules.

Sequence length is frequently the hidden budget killer. Doubling sequence length can make attention activations much more expensive. Reducing the micro-batch size, using gradient accumulation, and enabling gradient checkpointing are often better first responses than randomly changing LoRA rank.

Step 4: Make the initial choice

SituationRecommended first experiment
You have enough VRAM to load the base model in BF16/FP16 with training headroomLoRA
You need a larger model but have a constrained single GPUQLoRA
You want many task-specific variants of one foundation modelLoRA or QLoRA
You have a large, high-quality domain corpus and evidence adapters plateau below a required metricFull fine-tuning pilot
You only need current proprietary factsRetrieval/tooling before fine-tuning
You have fewer than a few hundred carefully checked examplesImprove data and prompts before selecting a more expensive method

Prerequisites and dataset preparation

Use a Linux environment with an NVIDIA GPU, a working CUDA-compatible PyTorch installation, Python, and enough local disk for the base model, cached dataset, checkpoints, and logs. The implementation below uses the Hugging Face transformers, datasets, peft, accelerate, and bitsandbytes ecosystem.

Install the training stack after installing a PyTorch build appropriate for your CUDA environment:

python -m pip install transformers datasets peft accelerate bitsandbytes

Package and CUDA compatibility is a common QLoRA setup failure. Confirm the GPU is visible before debugging the training code:

nvidia-smi
python -c "import torch; print(torch.cuda.is_available()); print(torch.cuda.get_device_name(0))"

Use a JSONL dataset with explicit instruction-response pairs. Keep sensitive source text out of training unless your data governance policy explicitly permits it.

{"instruction":"Classify the ticket and return JSON only.","input":"Customer reports a duplicate charge after a cancelled subscription.","output":"{\"category\":\"billing_duplicate_charge\",\"priority\":\"high\"}"}
{"instruction":"Classify the ticket and return JSON only.","input":"Customer cannot reset their password because the email link expired.","output":"{\"category\":\"account_password_reset\",\"priority\":\"medium\"}"}

For chat models, format each example with the model’s documented chat template. Do not train with one prompt format and deploy with another. Template mismatch is a frequent reason a fine-tuned model appears incoherent or ignores instructions.

Split by a meaningful boundary where possible: customer, document, product, time period, or source system. A random row split can leak near-duplicate examples into evaluation and make results look much better than production reality.

Step-by-step: implement a LoRA baseline

The following is a compact supervised fine-tuning pattern. It deliberately exposes the key choices: model ID, prompt template, target modules, rank, and training arguments. It is a starting point, not a substitute for model-specific template and module verification.

import torch
from datasets import load_dataset
from transformers import (
    AutoModelForCausalLM,
    AutoTokenizer,
    DataCollatorForSeq2Seq,
    Trainer,
    TrainingArguments,
)
from peft import LoraConfig, TaskType, get_peft_model

MODEL_ID = "your-base-instruct-model"
MAX_LENGTH = 1024

# These names are common in several decoder-only architectures, but not universal.
# Inspect model.named_modules() and use the architecture's actual linear-layer names.
TARGET_MODULES = ["q_proj", "k_proj", "v_proj", "o_proj"]

train_data = load_dataset("json", data_files="train.jsonl", split="train")

tokenizer = AutoTokenizer.from_pretrained(MODEL_ID)
if tokenizer.pad_token is None:
    tokenizer.pad_token = tokenizer.eos_token

def render_prompt(example):
    return (
        "### Instruction\n"
        f"{example['instruction']}\n\n"
        "### Input\n"
        f"{example['input']}\n\n"
        "### Response\n"
    )

def tokenize(example):
    prompt_ids = tokenizer(
        render_prompt(example), add_special_tokens=False
    )["input_ids"]
    answer_ids = tokenizer(
        example["output"] + tokenizer.eos_token,
        add_special_tokens=False,
    )["input_ids"]

    input_ids = (prompt_ids + answer_ids)[:MAX_LENGTH]
    labels = ([-100] * len(prompt_ids) + answer_ids)[:MAX_LENGTH]
    attention_mask = [1] * len(input_ids)
    return {
        "input_ids": input_ids,
        "attention_mask": attention_mask,
        "labels": labels,
    }

train_data = train_data.map(tokenize, remove_columns=train_data.column_names)

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
)
model.config.use_cache = False

lora_config = LoraConfig(
    task_type=TaskType.CAUSAL_LM,
    r=16,
    lora_alpha=32,
    lora_dropout=0.05,
    target_modules=TARGET_MODULES,
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()

args = TrainingArguments(
    output_dir="./ticket-lora",
    per_device_train_batch_size=1,
    gradient_accumulation_steps=16,
    learning_rate=2e-4,
    num_train_epochs=2,
    logging_steps=10,
    save_strategy="steps",
    save_steps=100,
    bf16=True,
    gradient_checkpointing=True,
    report_to="none",
)

trainer = Trainer(
    model=model,
    args=args,
    train_dataset=train_data,
    data_collator=DataCollatorForSeq2Seq(tokenizer=tokenizer, padding=True),
)
trainer.train()
model.save_pretrained("./ticket-lora/final-adapter")
tokenizer.save_pretrained("./ticket-lora/final-adapter")

The -100 labels are important. They prevent loss from being computed over the instruction and input tokens, concentrating learning on the desired response. Training on the full prompt can work for some tasks, but it wastes capacity teaching the model to reproduce user text and can distort the objective.

Start with a modest rank such as 8, 16, or 32. Increase it only if a controlled experiment shows underfitting. Higher rank increases trainable parameters and memory consumption; it is not a universal quality dial.

Convert the LoRA baseline to QLoRA

Keep the tokenizer, data processing, LoRA configuration, and trainer structure. Change model loading to use 4-bit quantization, then prepare the quantized model for adapter training.

import torch
from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import prepare_model_for_kbit_training

quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True,
    bnb_4bit_compute_dtype=torch.bfloat16,
)

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    quantization_config=quantization_config,
    device_map="auto",
)
model.config.use_cache = False
model = prepare_model_for_kbit_training(model)

# Apply get_peft_model(model, lora_config) after this point.

Use BF16 compute only if the GPU supports it. Otherwise, FP16 may be required, but test numerical stability rather than assuming an identical result.

Do not declare QLoRA successful merely because it starts training. Run exactly the same held-out evaluation as LoRA. Compare valid-output rate, task correctness, refusal behavior where applicable, latency, and cost per completed training run.

Full fine-tuning: what changes in implementation and operations

For full fine-tuning, load the model in an appropriate training precision and pass it directly to the trainer. Do not call get_peft_model; all parameters remain trainable.

model = AutoModelForCausalLM.from_pretrained(
    MODEL_ID,
    torch_dtype=torch.bfloat16,
)
model.config.use_cache = False

# Reuse the tokenized dataset and Trainer setup, but do not apply a LoRA config.
# In practice, distributed training, checkpointing, and optimizer configuration
# become central requirements for models that exceed one GPU.

For anything beyond a small model, a credible full fine-tuning plan needs distributed training and recovery design. Plan for interrupted jobs, checkpoint retention, validation runs, data versioning, and a rollback path. A full checkpoint is a complete model artifact, so storage and model registry costs become meaningful.

Use a lower learning-rate search range than the LoRA baseline and monitor both target-task metrics and general regression tests. Full fine-tuning can improve target performance while degrading useful general behavior. If preserving base capabilities matters, include representative general instruction data or domain-adjacent data in the training mixture and measure the regression explicitly.

Evaluate quality and cost before choosing a winner

Loss is useful for detecting training instability, but it is rarely the release metric. Build an evaluation table tied to your actual task.

MetricWhy it matters
Exact schema validityCatches malformed JSON, missing keys, and invalid enum values
Task accuracy or expert ratingMeasures whether the result is actually useful
Hallucination rateTests unsupported claims, identifiers, and citations
Base-capability regressionDetects harmful forgetting
Token and latency costDetermines whether the result is economically deployable
GPU hours and failed-run rateCaptures real training cost, not just advertised GPU price

Run LoRA and QLoRA with the same model, data split, prompt format, maximum length, and evaluation harness. Change one meaningful variable at a time. If QLoRA trails LoRA, first verify that you did not change the prompt template, target modules, compute dtype, sequence truncation rate, or effective batch size at the same time.

For cost, calculate:

Total experiment cost = GPU hourly rate × wall-clock training hours
                      + storage + engineering/debugging time
                      + repeated failed or discarded runs

QLoRA often wins when it converts an otherwise impossible hardware requirement into a runnable experiment. LoRA can win when it avoids quantization complexity and trains more cleanly on hardware you already have. Full fine-tuning only wins economically when its measured quality improvement is worth its much larger training and operational footprint.

Common errors and how to fix them

CUDA out of memory even with QLoRA

Reduce per_device_train_batch_size to 1, raise gradient_accumulation_steps to keep a similar effective batch size, lower MAX_LENGTH, and enable gradient checkpointing. Check whether long outlier examples dominate memory. Do not assume the 4-bit base eliminates activation memory.

target_modules does not exist

Layer names differ between architectures. Inspect the model before creating the LoRA configuration:

for name, _ in model.named_modules():
    if "proj" in name:
        print(name)

Choose real linear projection names from that output. Applying LoRA to nonexistent names either fails immediately or leaves you training something other than what you intended.

The model copies the prompt or produces poor completions

Check that labels mask prompt tokens with -100, that training and inference use the same chat template, and that the expected output appears consistently in the dataset. Also inspect examples manually for duplicated instruction text and contradictory responses.

Training loss falls but production quality does not improve

This usually indicates an evaluation or data problem, not a need for full fine-tuning. Look for data leakage, near-duplicate train/evaluation records, wrong prompt formatting, an overly narrow training distribution, or a metric that does not measure the actual product behavior.

The adapter loads but behavior looks unchanged

Confirm that the inference process loads the same base model revision and the intended adapter. Also confirm the adapter is activated. Keep base model IDs, adapter IDs, prompt template versions, and generation settings together in deployment metadata.

Full fine-tuning degrades general behavior

Lower the learning rate, shorten training, check for low-quality or overly repetitive examples, and add regression evaluations. If the task can be solved with an adapter, return to LoRA rather than accepting broad parameter drift as inevitable.

FAQ

Is LoRA always almost as good as full fine-tuning?

No. LoRA is often highly competitive for focused instruction tuning, formatting, classification, and domain behavior, but it has less capacity to reshape a model. The only reliable answer comes from a held-out evaluation on your task. If LoRA misses a critical metric after sensible data, rank, target-module, and hyperparameter experiments, test full fine-tuning with a defined budget.

Should I use QLoRA whenever I have a single GPU?

Use QLoRA when conventional LoRA does not fit comfortably in memory or when it lets you train a meaningfully larger base model. If ordinary BF16/FP16 LoRA fits, it is usually the simpler baseline because it avoids quantization-specific installation and debugging issues.

Does fine-tuning replace RAG?

No. Fine-tuning teaches behavior and recurring task patterns. Retrieval-augmented generation supplies changing or large bodies of factual information at request time. Many production systems use both: retrieval for current evidence and a fine-tuned model for reliable extraction, classification, tool use, or response format.

Can I merge a LoRA adapter into the base model?

Usually, compatible LoRA adapters can be merged into a copy of the matching base model for deployment. Preserve the original adapter separately, record the exact base-model revision, and run post-merge regression tests. Merging removes the convenience of switching adapters dynamically.

How much training data do I need?

There is no honest universal minimum. A few hundred carefully curated examples can improve a narrow, repetitive output-format task. Broad domain adaptation requires far more diverse, consistent data. Prioritize coverage of real production cases, correct labels, and a protected evaluation set over raw row count.