LoRA and QLoRA Explained: Fine-Tuning Big Models on Small Budgets
Last time, Rae decided that a light fine-tune on top of her existing RAG pipeline was the right move for her support bot — RAG would keep handling the facts from her product manual, and a fine-tune would lock in the tone, format, and domain-specific reasoning her support team used. But she’s running a startup, not a research lab: she has a single consumer GPU, a small budget, and zero appetite for renting A100s by the hour. She needs to fine-tune a big model without going broke — and that’s exactly the wall she’s about to hit.
Rae fires up her GPU and points it at a 7B parameter model — small enough to stay open-source, big enough to be useful for her support bot. She’s collected hundreds of past support-ticket resolutions as training data: each one a question and the answer her team actually wrote. She hits “train.” Within seconds, her screen fills with red text: CUDA out of memory. Her GPU — the same one that runs her RAG inference pipeline just fine — can’t even begin a fine-tuning step.
If you’ve ever tried to fine-tune a Large Language Model (LLM) like Llama 3 or Mistral on your own computer, only to watch your GPU throw “Out of Memory” before the first step finishes, you know the feeling. It’s a frustrating rite of passage for every data scientist today — and for Rae, it’s the gap between her and the one improvement her support bot still needs.
These models are supposed to be the future, yet their sheer size makes them feel like they belong only to big tech companies with million-dollar server rooms. But here’s the thing: you don’t need to change the whole model to make it follow your instructions.
This guide covers LoRA and QLoRA. These are the “hacks” that let us train massive models on consumer hardware. Think of it as teaching a giant new tricks without performing brain surgery on each of its billions of neurons.
1. The Problem: Why Fine-Tuning Feels Impossible
A “7B” model has 7 billion parameters. In standard 32-bit precision (float32), each parameter takes 4 bytes.
Just to load the model into your GPU, you need 28GB of VRAM. Training is another story. With Full Fine-Tuning, you aren’t just storing the weights. You also need the gradients (the directions the model needs to move) and the optimizer states (the memory of previous steps).
Let’s look at the math for a 7B model:
# Memory calculation for Full Fine-Tuning (7B model)
params = 7e9
bytes_per_param = 4 # float32
model_weights = params * bytes_per_param / 1e9
gradients = params * bytes_per_param / 1e9
optimizer_states = params * bytes_per_param * 2 / 1e9 # Adam optimizer stores 2 values per param
total_vram_needed = model_weights + gradients + optimizer_states
print(f"Model Weights: {model_weights} GB")
print(f"Gradients: {gradients} GB")
print(f"Optimizer States: {optimizer_states} GB")
print(f"Total VRAM required: {total_vram_needed} GB")
# Output: Total VRAM required: 112.0 GB
This block calculates the total VRAM needed to full-fine-tune a 7B model, breaking down the three components that consume GPU memory.
params = 7e9— sets the parameter count to 7 billion (7 × 10⁹).bytes_per_param = 4— specifies that float32 (32-bit floating point) uses 4 bytes per parameter, the standard precision for training.model_weights = params * bytes_per_param / 1e9— computes the model weights in GB: 7e9 × 4 / 1e9 = 28 GB. This is the cost of just loading the model.gradients = params * bytes_per_param / 1e9— computes the gradients in GB (another 28 GB). During backpropagation, every parameter needs a gradient of the same size, so this doubles the memory.optimizer_states = params * bytes_per_param * 2 / 1e9— computes the optimizer states in GB (56 GB). The* 2is because the Adam optimizer stores two values per parameter: the first moment (exponential moving average of gradients) and the second moment (exponential moving average of squared gradients).total_vram_needed = model_weights + gradients + optimizer_states— sums all three: 28 + 28 + 56 = 112 GB.# Adam optimizer stores 2 values per param— this comment explains the* 2multiplier. If you used SGD instead of Adam, you’d only need the weights and gradients (56 GB), but Adam’s momentum tracking adds another 56 GB.
For Rae, this is why her consumer GPU — which handles RAG inference just fine at 28 GB for the weights — chokes the moment she tries to train on it: training needs 4× the memory of inference, not 2× or 1×.
What this actually means: An RTX 4090, the standard high-end consumer GPU, has 24GB of VRAM. An enterprise A100 has 80GB. Neither can handle the 112GB a full fine-tune of a relatively “small” 7B model requires. That’s the real bottleneck: the memory cost of training, not just the model itself.
2. The Intuition: What If We Only Changed a Tiny Piece?
Full fine-tuning rewrites a 1,000-page book just to change the ending. Slow, expensive, overkill.
LoRA (Low-Rank Adaptation) takes a different approach: sticky notes in the margins. The base model stays frozen. We only write on the adapters. At inference, we read the original text alongside the notes to get the final output.
Technically, we add small adapter matrices next to the original weight matrices. These adapters are tiny—often under 1% of the total size—so we only need gradients and optimizer states for them, not the billions of parameters in the base model.
3. How LoRA Actually Works: Low-Rank Decomposition
This is the hardest part to visualize. The weight update is normally a large matrix. LoRA bets the changes aren’t that complex.
Instead of learning one big matrix , we learn two skinny ones, and . Multiply them together () and you get a matrix the same size as — but with much lower rank, meaning less complexity.
In full fine-tuning, the weight update is applied directly to the full weight matrix:
where is a full-rank matrix with the same dimensions as . LoRA replaces this with a low-rank approximation:
where , , and . The forward pass becomes:
The original weights are frozen (no gradients computed), and only and are trained. The total trainable parameters drop from to .
| Plain English | Statistical symbol | Python equivalent |
|---|---|---|
| Original frozen weight matrix | model.weight (not updated) | |
| Full weight update (full fine-tuning) | grad (same shape as ) | |
| Low-rank adapter matrix A | lora_A.weight | |
| Low-rank adapter matrix B | lora_B.weight | |
| Rank (bottleneck dimension) | r or rank in LoraConfig | |
| Input dimension | in_dim | |
| Output dimension | out_dim | |
| Forward pass with adapter | h = base_layer(x) + lora_B(lora_A(x)) |
The key insight is that (the rank) is the bottleneck: it controls the tradeoff between expressiveness (how many different changes the adapter can represent) and memory (how many parameters need gradients). With and , the full matrix needs parameters, but needs only — a 256× reduction.
Here’s what that does to the parameter count:
import torch
import torch.nn as nn
# Imagine a layer with 4096 inputs and 4096 outputs
in_dim, out_dim = 4096, 4096
rank = 8 # This is our 'r' hyperparameter
# Full Fine-Tuning parameters
full_params = in_dim * out_dim
# LoRA parameters (Matrix A and Matrix B)
matrix_a = in_dim * rank
matrix_b = rank * out_dim
lora_params = matrix_a + matrix_b
reduction = full_params / lora_params
print(f"Full parameters: {full_params:,}")
print(f"LoRA parameters: {lora_params:,}")
print(f"Reduction factor: {reduction:.1f}x")
# Output: Reduction factor: 256.0x
This block computes the parameter count for full fine-tuning vs. LoRA on a single 4096×4096 layer, demonstrating the dramatic reduction.
import torchandimport torch.nn as nn— import PyTorch and its neural network module. Neither is actually used in this calculation; they’re imported because this snippet would normally be part of a larger training script.in_dim, out_dim = 4096, 4096— sets the input and output dimensions of a typical transformer layer (this matches the hidden size of models like Llama-7B).rank = 8— sets the LoRA rank, therhyperparameter that controls the bottleneck.full_params = in_dim * out_dim— computes the number of parameters for full fine-tuning: 4096 × 4096 = 16,777,216 (about 16.8M).matrix_a = in_dim * rank— computes the parameters in matrix : 4096 × 8 = 32,768.matrix_b = rank * out_dim— computes the parameters in matrix : 8 × 4096 = 32,768.lora_params = matrix_a + matrix_b— sums both: 32,768 + 32,768 = 65,536 total trainable parameters.reduction = full_params / lora_params— computes the ratio: 16,777,216 / 65,536 = 256.0.print(f"Full parameters: {full_params:,}")— uses the:,format specifier to add thousands separators for readability.print(f"Reduction factor: {reduction:.1f}x")— formats the reduction to one decimal place with a literal “x” suffix.
The output shows a 256× reduction — meaning we compute and store gradients for 65K parameters instead of 16.8M, which is what transforms “impossible on consumer hardware” to “trivial.” For Rae, this is why her GPU stops screaming: the gradients and optimizer states are now 256× smaller, fitting comfortably in her VRAM.
With a rank of 8, we’re training 256 times fewer parameters for that layer. Memory savings follow because we only store gradients for the small matrices.
4. LoRA in Practice: Adapters, Ranks, and Targets
In practice, we use the peft (Parameter-Efficient Fine-Tuning) library from HuggingFace. You don’t have to write the matrix math yourself. Just define a configuration.
Applying LoRA to a model looks like this:
from peft import LoraConfig, get_peft_model
from transformers import AutoModelForCausalLM
# 1. Load a base model (frozen)
model = AutoModelForCausalLM.from_pretrained("facebook/opt-350m")
# 2. Define LoRA Config
config = LoraConfig(
r=16, # The rank. Higher = more capacity, more memory
lora_alpha=32, # Scaling factor
target_modules=["q_proj", "v_proj"], # Which layers to 'stick' notes on
lora_dropout=0.05,
bias="none",
task_type="CAUSAL_LM"
)
# 3. Wrap the model
lora_model = get_peft_model(model, config)
lora_model.print_trainable_parameters()
# Output: trainable params: 1,572,864 || all params: 332,769,280 || trainable%: 0.4727
This block loads a pre-trained model and wraps it with LoRA adapters using the HuggingFace peft library.
from peft import LoraConfig, get_peft_model— imports the configuration class and the wrapper function from thepeft(Parameter-Efficient Fine-Tuning) library.from transformers import AutoModelForCausalLM— imports the auto-class for loading causal language models (models that predict the next token).model = AutoModelForCausalLM.from_pretrained("facebook/opt-350m")— loads Facebook’s OPT-350M model, a relatively small model used here for demonstration (Rae would use a larger model like Llama-7B or Mistral-7B in practice).config = LoraConfig(...)— creates a LoRA configuration object with six parameters.r=16— sets the rank to 16. Higher rank means more expressive adapters but more memory; the comment explains this tradeoff.lora_alpha=32— a scaling factor that controls how strongly the adapter influences the output. The effective scaling isalpha / r, so32/16 = 2.0here.target_modules=["q_proj", "v_proj"]— specifies which weight matrices in the transformer to attach adapters to.q_proj(query projection) andv_proj(value projection) are the attention layers, the standard targets for LoRA; you could also addk_proj,o_proj, or the MLP layers.lora_dropout=0.05— applies 5% dropout to the adapter outputs to prevent overfitting.bias="none"— means no bias parameters are trained, keeping the adapter even smaller.task_type="CAUSAL_LM"— tells the library this is a causal language modeling task (next-token prediction).
lora_model = get_peft_model(model, config)— wraps the base model with the LoRA configuration. This freezes all original weights and injects the trainable adapter matrices into the target modules.lora_model.print_trainable_parameters()— prints a summary showing how many parameters are trainable vs. frozen.
The output trainable params: 1,572,864 || all params: 332,769,280 || trainable%: 0.4727 means only about 1.57M out of roughly 333M parameters are being trained — under half a percent. (OPT-350M has 24 transformer layers, each with a 1024×1024 q_proj and v_proj; at r=16 each targeted matrix adds 1024×16 + 16×1024 = 32,768 adapter parameters, so 24 layers × 2 modules × 32,768 = 1,572,864 — exactly the printed count.) For Rae, this means her GPU only needs to store gradients and optimizer states for ~1.57M parameters, not 333M — which is what makes the fine-tune feasible on her hardware.
Interpretation: We’re only training 0.47% of the model. The rest stays frozen, which keeps VRAM usage down.
5. The Memory Savings: Numbers You Can Actually Believe
So how do the numbers hold up? Fine-tuning a Llama-7B model:
- Full Fine-Tuning: ~112GB VRAM (requires 2x A100 GPUs).
- LoRA (Rank 8): ~16GB - 20GB VRAM (fits on a single RTX 3090 or 4090).
That’s the difference between impossible and doable at home.
6. QLoRA: LoRA + Quantization = Even Smaller
If LoRA trains fewer parameters, QLoRA shrinks the ones we already have.
It uses 4-bit quantization. Numbers normally live in 16 or 32 bits. QLoRA squashes the base model weights down to just 4 bits, cutting the memory footprint by 4x to 8x.
Here’s the catch: 4-bit weights are “low quality.” QLoRA works around this — it dequantizes them just-in-time for each calculation, then uses LoRA adapters to “fix” the errors from the low precision.
from transformers import BitsAndBytesConfig
# Configure 4-bit loading
quant_config = BitsAndBytesConfig(
load_in_4bit=True,
bnb_4bit_quant_type="nf4",
bnb_4bit_compute_dtype=torch.float16,
bnb_4bit_use_double_quant=True
)
# Load model in 4-bit
model_4bit = AutoModelForCausalLM.from_pretrained(
"mistralai/Mistral-7B-v0.1",
quantization_config=quant_config,
device_map="auto"
)
This block configures 4-bit quantization for loading a model at a fraction of its normal memory cost — the key technique that makes QLoRA work on consumer GPUs.
from transformers import BitsAndBytesConfig— imports the quantization configuration class from HuggingFace Transformers.quant_config = BitsAndBytesConfig(...)— creates a configuration object with four parameters that control how the model’s weights are compressed.load_in_4bit=True— tells the loader to quantize all weights to 4 bits, reducing each parameter from 16 bits (2 bytes) or 32 bits (4 bytes) to just 0.5 bytes, a 4× to 8× reduction.bnb_4bit_quant_type="nf4"— specifies the quantization type as NormalFloat 4 (NF4), a quantization scheme designed by the QLoRA authors specifically for normally-distributed weight values. It’s better than uniform quantization because it allocates more precision to the most common weight values.bnb_4bit_compute_dtype=torch.float16— specifies that during the forward pass, weights are dequantized back to float16 (16-bit) for computation. This is the “just-in-time dequantization” the article describes.bnb_4bit_use_double_quant=True— enables double quantization, quantizing the quantization constants themselves, saving an additional ~0.4 bits per parameter.
model_4bit = AutoModelForCausalLM.from_pretrained(...)— loads Mistral-7B with the quantization config applied.quantization_config=quant_config— passes the 4-bit configuration.device_map="auto"— lets HuggingFace automatically place model layers across available GPUs, essential when a model is too large for a single device but can be split.
For Rae, this is what lets her 7B model (which needs 28 GB in float32) fit in about 5 GB of VRAM — leaving plenty of room for the LoRA adapters and their gradients on her single consumer GPU.
With QLoRA, a 7B model needs only ~5GB of VRAM for its weights. You can fine-tune a 7B model on a cheap 12GB GPU.
7. Putting It Together: A Complete Pipeline
Here’s a simple workflow for fine-tuning your own model:
# 1. Load model in 4-bit (QLoRA) -> model_4bit (from Section 6)
# 2. Wrap it with LoRA adapters, using the same config from Section 4
lora_model = get_peft_model(model_4bit, config)
# 3. Load your dataset (e.g., instructions)
# 4. Use the trl SFTTrainer
from trl import SFTTrainer, SFTConfig
trainer = SFTTrainer(
model=lora_model,
train_dataset=my_data,
args=SFTConfig(
dataset_text_field="text",
max_length=512,
per_device_train_batch_size=1,
gradient_accumulation_steps=4,
learning_rate=2e-4,
logging_steps=10,
output_dir="./outputs",
),
)
trainer.train()
This block assembles a complete training pipeline using the SFTTrainer (Supervised Fine-Tuning Trainer) from the trl library — the final piece that ties QLoRA loading, LoRA adapters, and training data together.
lora_model = get_peft_model(model_4bit, config)— wraps the 4-bit quantized base model from Section 6 with the sameLoraConfigfrom Section 4. This is the line that actually makes it QLoRA rather than plain LoRA: the base model is quantized and has trainable adapters attached.from trl import SFTTrainer, SFTConfig— imports the supervised fine-tuning trainer, which handles the training loop for instruction-tuning datasets, and its configuration class. (In currenttrlreleases, dataset options likedataset_text_fieldand the sequence-length cap live onSFTConfig, not as direct keyword arguments toSFTTrainer— passing them straight toSFTTrainerraisesTypeError: SFTTrainer.__init__() got an unexpected keyword argument 'dataset_text_field'.)trainer = SFTTrainer(...)— creates the trainer with two top-level arguments plus a config object.model=lora_model— passes the LoRA-wrapped, 4-bit quantized model built above. This is what makes it QLoRA: the model is both quantized and has trainable adapters.train_dataset=my_data— passes the training dataset — for Rae, this would be her hundreds of (question, answer) pairs from past support tickets.args=SFTConfig(...)— bundles both the dataset options and the standard training hyperparameters into one config object.dataset_text_field="text"— tells the trainer which field in the dataset contains the text to train on.max_length=512— caps the sequence length to 512 tokens — shorter sequences use less VRAM, which matters on consumer GPUs; Rae’s support-bot Q&A pairs are short enough to fit comfortably. (This option was calledmax_seq_lengthin oldertrlreleases; it was renamedmax_lengthwhen the dataset options moved ontoSFTConfig.)per_device_train_batch_size=1— sets the batch size to 1, the most memory-conservative setting, essential for small GPUs.gradient_accumulation_steps=4— accumulates gradients over 4 steps before updating weights. This simulates a batch size of 4 (1 × 4) without needing the memory for 4 simultaneous examples.learning_rate=2e-4— sets a learning rate of 0.0002, note this is higher than the2e-5used in full fine-tuning (covered in the prior article’s conceptual example), because LoRA adapters need a stronger nudge to learn effectively with fewer parameters.logging_steps=10— logs training metrics every 10 steps.output_dir="./outputs"— specifies where to save checkpoints.
trainer.train()— starts the training loop.
For Rae, this is the exact pipeline she runs on her single GPU: load Mistral-7B in 4-bit, attach LoRA adapters, feed in her support-ticket Q&A pairs, and train — the whole thing fits in under 8GB of VRAM.
8. When to Use What?
- Full Fine-Tuning: Reach for this only with a massive dataset (millions of rows) and a cluster of H100s. Most users will never need it.
- LoRA: The default choice. If you have 24GB+ VRAM, go with this. It trains faster than QLoRA and comes out slightly more accurate.
- QLoRA: The budget pick. Running big models on 8GB or 12GB of VRAM? This is your path — there’s really no other option.
Full Fine-Tuning vs. LoRA vs. QLoRA: Which Should Rae Pick?
Rae’s startup has specific constraints that narrow the field to one option: a single consumer GPU (let’s say 8–12GB VRAM), a small budget (no cloud GPU rentals), and a few hundred high-quality training examples (her support-ticket resolutions). Here’s how the three approaches stack up:
| Factor | Full Fine-Tuning | LoRA | QLoRA |
|---|---|---|---|
| VRAM for 7B model | ~112 GB (weights + gradients + optimizer) | ~16–20 GB (weights in fp16 + small adapter gradients) | ~5–8 GB (4-bit weights + small adapter gradients) |
| Trainable parameters | 100% — all 7B | ~0.2–1% — just the adapters | ~0.2–1% — just the adapters (base model frozen at 4-bit) |
| Quality ceiling | Highest — full expressiveness of the weight update | High — very close to full, slight rank constraint | High — slight quantization noise, but LoRA adapters compensate |
| Training speed | Slowest — massive gradient computations | Fast — tiny gradients, fast backprop | Slightly slower than LoRA — dequantization overhead per forward pass |
| Hardware needed | Multiple A100s or H100s (enterprise cluster) | Single RTX 3090/4090 (24 GB VRAM) | Single RTX 3060/4060 Ti (8–12 GB VRAM) |
| Inference after merge | Native — no adapter overhead | Native — merge adapters back into weights | Native — merge adapters back into weights |
| Best for | Millions of examples, unlimited compute, maximum quality | Most use cases with a decent consumer GPU | Budget-constrained startups, single consumer GPUs |
The decision for Rae’s startup:
Rae has a single consumer GPU with 8–12 GB of VRAM and a few hundred training examples. QLoRA is her only realistic option. It lets her fine-tune a 7B model (which she needs for quality) on hardware she already owns, without renting cloud GPUs by the hour. The 4-bit base model occupies ~5 GB, and the LoRA adapter gradients add only a few hundred MB — the whole pipeline fits in her VRAM with room to spare.
When NOT to use QLoRA (and what to use instead):
- If Rae had 24 GB+ VRAM (e.g., an RTX 4090): LoRA without quantization would be preferable — it’s slightly faster (no dequantization overhead) and avoids any quantization noise in the base model. The quality difference is small but measurable on benchmarks.
- If Rae had millions of examples and a cluster of H100s: Full fine-tuning could squeeze out additional quality — the full-rank weight update can represent changes that a rank-8 or rank-16 adapter cannot. But this is almost never the case for startups, and the marginal quality gain rarely justifies the 10×+ cost.
- If Rae needed maximum quality and had 24 GB VRAM but not millions of examples: LoRA with a higher rank (e.g.,
r=64orr=128) would give her more adapter capacity without needing full fine-tuning — the rank controls how expressive the adapter can be, and bumping it up is cheaper than going to full fine-tuning.
The key tradeoff to understand: QLoRA adds quantization noise to the base model — the 4-bit weights are slightly less precise than their 16-bit originals. In practice, the LoRA adapters compensate for this noise during training, and the quality gap between QLoRA and full fine-tuning is small enough that it rarely matters for production use cases like support bots. The tradeoff is: accept a tiny quality hit in exchange for fitting on hardware you already own.
9. Common Pitfalls
- Rank too low: Set and the model probably won’t learn enough. or tends to be the sweet spot.
- Learning rate: LoRA wants a higher learning rate than full fine-tuning. You’re only adjusting a handful of parameters, so each nudge needs to count — try
2e-4. - Forgetting to save: Saving a LoRA model only saves the “sticky notes.” To use it later, load the base model and the adapters together.
10. What’s Next?
Once your adapters are trained, you can merge them. This combines the matrices back into the main weights, so the model becomes a single file again — no speed penalty during inference.
# Merging the adapters into the base model
merged_model = lora_model.merge_and_unload()
merged_model.save_pretrained("my-final-model")
This block merges the trained LoRA adapters back into the base model’s weights, producing a single self-contained model with no inference overhead.
merged_model = lora_model.merge_and_unload()— performs the merge: it multiplies the trained and matrices (), adds the result to the frozen base weights (), and then removes the adapter layers from the model. Themerge_and_unload()method does both in one call. After merging, the model behaves exactly like a standard fine-tuned model: the low-rank approximation is baked into the weights, and there’s no separate adapter to load at inference time.merged_model.save_pretrained("my-final-model")— saves the merged model to disk as a standard HuggingFace checkpoint, a single directory containing the model weights and config that can be loaded withAutoModelForCausalLM.from_pretrained("my-final-model")without any LoRA-specific setup.
For Rae, this is the final step: she trains her QLoRA adapters on her support-ticket data, merges them into the base model, saves the result, and deploys the merged model alongside her RAG pipeline — at inference time, there’s no adapter overhead, just a model that happens to speak in her company’s support-team voice.
Closing the Loop
Rae runs the QLoRA pipeline on her single consumer GPU — the same card that threw “CUDA out of memory” at the start of this article. With 4-bit quantization and LoRA adapters, the 7B model’s weights occupy only ~5GB of VRAM. The adapter gradients add barely a few hundred megabytes more. She trains on her hundreds of past support-ticket resolutions — the questions her customers asked, the answers her team actually wrote. The adapter picks up her company’s tone: how her support team phrases things, the format they use, the empathy they show. It does not learn her product manual’s contents. That’s what RAG is for, and RAG is still running right alongside it.
Then she ships it. The fine-tuned model sits on top of her existing RAG pipeline: RAG retrieves the right passage from her product manual, and the fine-tuned model responds in her team’s voice. The burrito-refund-style hallucinations — confident answers to questions the bot had no business answering — finally stop. Not because the model got smarter, but because it now has the right facts from RAG and the right tone from the fine-tune, working together.
Rae came a long way to get here. She didn’t know what an embedding was — she just knew her keyword search couldn’t find semantically similar answers in her product manual. She learned embeddings, stood up a vector database, assembled her first RAG pipeline over the product manual, discovered that LLMs forget the middle of long contexts, debated whether to use a bigger context window or stick with RAG, weathered the burrito-refund hallucination crisis, built a systematic evaluation harness, iterated on prompt engineering patterns, mapped out her three levers for steering the model, decided between fine-tuning and RAG, and finally — on a single consumer GPU with a startup budget — fine-tuned a small adapter that made her support bot sound like her team.
Her support bot is live. RAG handles the facts, evaluation keeps quality in check, and prompt engineering sets the structure. A cheap QLoRA fine-tune gets the tone right. No million-dollar server room needed. It took understanding the tools, knowing which to reach for when, and a single GPU that kept hitting “out of memory” until Rae found the right technique.
Check Your Understanding
Questions below move from simple recall up to open-ended design, roughly following Bloom’s Taxonomy.
Remember What does QLoRA add on top of LoRA, and what specific memory savings does it target?
Understand In your own words, explain why full fine-tuning needs 112GB of VRAM for a 7B model, even though the weights themselves are only 28GB — what are the other two components eating the remaining 84GB?
Apply
Using the article’s LoRA parameter-count formula (in_dim * rank + rank * out_dim), calculate the number of trainable parameters for a layer with in_dim = 2048, out_dim = 2048, and rank = 16, and compare it to the 2048 * 2048 full-fine-tuning parameter count.
Analyze The article says QLoRA “dequantizes [4-bit weights] just-in-time for the calculation and then uses LoRA adapters to fix the errors introduced by the low precision.” Walk through why LoRA adapters — which were originally designed just to reduce trainable parameter count — turn out to also be a good mechanism for correcting quantization error, rather than needing a separate fix for that problem.
Evaluate
The article’s Pitfall #2 says LoRA needs a higher learning rate than full fine-tuning “since you are only changing a few parameters, you need to nudge them harder.” Critique this reasoning: is “fewer parameters” by itself a good justification for a higher learning rate, or is there a more precise explanation tied to how LoRA’s A and B matrices are initialized and scaled (via lora_alpha)?
Create Design a hardware/technique decision for a new team: a startup has one RTX 4090 (24GB VRAM) and wants to fine-tune a 13B parameter model. Using the article’s “When to Use What” guidance and VRAM figures, would you recommend LoRA or QLoRA, and what would you tell them to expect if they tried the other option first?
Related articles
- Fine-Tuning vs. RAG: How to Actually Decide
- What Are Embeddings (and What Can You Actually Do With Them)?)
References & Further reading
- Hu, E. J., Shen, Y., Wallis, P., Allen-Zhu, Z., Li, Y., Wang, S., Wang, L., & Chen, W. (2021). LoRA: Low-Rank Adaptation of Large Language Models. arXiv:2106.09685. arxiv.org/abs/2106.09685 — the foundational paper that introduced low-rank adaptation as a parameter-efficient alternative to full fine-tuning.
- Dettmers, T., Pagnoni, A., Holtzman, A., & Zettlemoyer, L. (2023). QLoRA: Efficient Finetuning of Quantized LLMs. arXiv:2305.14314. arxiv.org/abs/2305.14314 — the paper that combined 4-bit quantization with LoRA to enable fine-tuning of large models on consumer GPUs.
- Hugging Face. PEFT documentation. huggingface.co/docs/peft — the official documentation for the
peftlibrary used in this article’s code examples. - Hugging Face. Transformers documentation. huggingface.co/docs/transformers — the library that provides
BitsAndBytesConfig,AutoModelForCausalLM, andTrainingArgumentsused throughout.
Apply What You Learned is for Supporter and Insider subscribers.
Subscribe to unlock the exercises on this post.
See plansRelated articles
- LLMs & GenAI Under review
Fine-Tuning vs. RAG: How to Actually Decide
Stop LLM hallucination: learn when to fine-tune vs. use RAG, with a decision framework, code examples, and a practical readiness checklist for your project.
- LLMs & GenAI Under review
Reference: LLM Vocabulary
Close the LLM vocabulary gap with this single-file reference on tokens, embeddings, attention, sampling, and the cost ladder from prompting to fine-tuning.
- LLMs & GenAI Under review
The Three Ways to Steer an LLM (And Why You Need to Pick One)
Master the three levers for steering LLMs—prompt engineering, in-context learning, and fine-tuning—and when to pick each based on cost, speed, and permanence.
- LLMs & GenAI Under review
Can a Tiny Model Judge a Giant One? Inside LAGER and INSPECTOR
Explore how LAGER and INSPECTOR leverage internal model representations so tiny models can evaluate giant LLMs—cheaper, less biased, and sometimes more accurate.
Looking for something else?
Search every article by title, summary or topic.