Thomas Piacentini
← Workshop

Cheat sheet

Fine-Tuning with LoRA

Original text · English

Add LoRA adapters, load a 4-bit base for QLoRA, mask SFT labels, train, merge and debug. PyTorch, PEFT and bitsandbytes.

Tasks

TaskCode
Add LoRA to a modelmodel = get_peft_model(model, lora_config)
Show trainable sharemodel.print_trainable_parameters()
Load the base in 4-bitBitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4")
Count trainable paramscount_parameters(model)
Save the adapter onlymodel.save_pretrained("./lora-adapter")
Merge the adapter into Wmerge_lora_weights(model)
Skip masked labels in the lossF.cross_entropy(..., ignore_index=ignore_index)
Head-only trainingfreeze_body(model)
Sources

LoRA path in one layer

mergeInput xFrozen Wout x inAr x in, Gaussian initBout x r, zero initScalealpha / rAddWx + scaled BAxOutput yMerged W'W + (alpha/r) BA
Nodes and connections
NodeRoleKind
Input x
Frozen Wout x in
Ar x in, Gaussian init
Bout x r, zero init
Scalealpha / r
AddWx + scaled BAx
Output y
Merged W'W + (alpha/r) BA
FromToConnection
Input xFrozen W
Input xA
AB
BScale
Frozen WAdd
ScaleAdd
AddOutput y
ScaleMerged W'merge
Sources

LoRA from scratch

class LoRALayer(nn.Module):
def __init__(self, in_features, out_features, rank=8, alpha=16):
super().__init__()
self.scaling = alpha / rankalpha / r
self.A = nn.Parameter(torch.randn(in_features, rank) * (1 / math.sqrt(rank)))
self.B = nn.Parameter(torch.zeros(rank, out_features))BA starts at zero
def forward(self, x):
return (x @ self.A @ self.B) * self.scalingscaled low-rank update
class LinearWithLoRA(nn.Module):

__init__ wraps linear and builds self.lora

for param in self.linear.parameters():
param.requires_grad = Falsefreeze W
def forward(self, x):
return self.linear(x) + self.lora(x)Wx + scaled BAx
Sources

QLoRA with PEFT

bnb_config = BitsAndBytesConfig(load_in_4bit=True,4-bit base
bnb_4bit_quant_type="nf4",NF4 levels
bnb_4bit_compute_dtype=torch.bfloat16,
bnb_4bit_use_double_quant=True)quantize the scales too
model = AutoModelForCausalLM.from_pretrained("<model>",
quantization_config=bnb_config, device_map="auto")
lora_config = LoraConfig(task_type=TaskType.CAUSAL_LM, r=16,
lora_alpha=32, lora_dropout=0.05,
target_modules=["q_proj", "v_proj"])query and value
model = get_peft_model(model, lora_config)adds A and B
model.save_pretrained("./lora-adapter")10-100MB adapter
Sources

One SFT training step

  1. Template each pair becomes <INST> instruction <RESP> response.
  2. Tokenize byte ids plus INST_ID = 256, RESP_ID = 257, PAD_ID = 258.
  3. Collate sft_collate pads each batch to its longest sequence.
  4. Mask labels get -100 on instruction, padding and the boundary.
  5. Forward the model sees the full sequence, instruction included.
  6. Loss shifted_loss predicts position i+1 with ignore_index=-100.
  7. Step Adam updates the body. Exact-match runs every fifth epoch.
Sources

Key formulas

LoRA forward
y = Wx + BAx with W frozen.
Scaled update
y = Wx + \frac{\alpha}{r} \cdot BAx
Parameter count
rank r trains 2 \cdot r \cdot d parameters instead of d^2.
Example
r=16 on 4096x4096 trains 131,072 of 16,777,216. That is 0.78%.
Merge
W' = W + (\alpha/r) \cdot BA. Same size, no inference overhead.
Full fine-tune memory
8B in fp16 is about 56GB with gradients and Adam states.
Sources

Defaults

SettingDefaultNote
r16safe start for most tasks
lora_alpha322 x rank, alpha = rank is conservative
lora_dropout0.050.1 below 5K examples
target_modulesq_proj, v_projadd more only if quality is low
QLoRA learning rate2e-4higher than full fine-tuning
LoRA fp16 learning rate5e-5lower than QLoRA
Full SFT learning rate2e-5Llama 2 Chat, 15x below pre-training
Epochs1-3more risks overfitting
Sources

Troubleshooting

SymptomCauseFix
NaN losses with Llama modelspad token not setset the pad token
Loss does not decreaselearning rate too low for LoRAtry 2e-4
Loss falls, eval quality badoverfittingfewer epochs, more dropout, more data
Weak on complex tasksrank too low or wrong targetstry r=32 or add MLP layers
General skills drop after SFTlearning rate too highuse 1e-5 to 5e-5 and 1-3 epochs
Model predicts the instructionno loss maskset -100 on instruction and padding
OOM on long sequencesoptimizer states exceed GPU memoryuse optim="paged_adamw_8bit"
Sources

Abbreviations

LoRA
Low-Rank Adaptation. Freeze W, train A and B.
QLoRA
Quantized LoRA. 4-bit NF4 base, fp16 adapters.
NF4
Normal Float 4. 16 levels at normal quantiles.
PEFT
Hugging Face library for LoRA, QLoRA, DoRA and IA3.
TRL
Hugging Face trainers for SFT, DPO, GRPO, PPO and ORPO.
SFT
Supervised Fine-Tuning on instruction and response pairs.
ChatML
role format with <|im_start|> and <|im_end|> tokens.
Double quantization
quantizes the per-block scales to fp8.
Sources

Questions

Why is B zero at init?
BA starts at zero. Training starts from the original behavior.
What does alpha do?
It scales the update by alpha / r, apart from the base learning rate.
Merge or keep separate?
Keep separate to serve many tasks. Merge for one model.
Why mask the instruction?
Unmasked, the model learns to predict the instruction and the response signal gets smaller.
When not to fine-tune?
When a prompt works, when you need facts (use RAG) or below 500 examples.
Sources

Tips

  • Try prompt engineering first, then RAG, then fine-tuning.
  • Evaluate every 100 steps on a held-out set. Keep the best checkpoint.
  • Compare base plus prompt against base plus LoRA on 200+ held-out examples.
  • On 800 examples, train the head only with a frozen body.
  • Track raw text loss. A rise above 10-15% means SFT is too aggressive.
Sources

Choose a method

ChoiceUse when
Full fine-tune, about 56GB for 7BVRAM >= 2x model size in fp16
LoRA on fp16 base, about 18GBVRAM >= model size in fp16
QLoRA on 4-bit base, about 6GBVRAM >= model size / 4
r=16multi-domain tasks, instruction following, chat
r=32code generation, complex reasoning, math
Sources
Original source
---
id: lora-fine-tuning
title: Fine-Tuning with LoRA
code: FT
subtitle: Add LoRA adapters, load a 4-bit base for QLoRA, mask SFT labels, train, merge and debug. PyTorch, PEFT and bitsandbytes.
hue: 310
sources: phases/11-llm-engineering/08-fine-tuning-lora, phases/10-llms-from-scratch/06-instruction-tuning-sft, phases/19-capstone-projects/38-classifier-finetuning, phases/19-capstone-projects/39-instruction-tuning-sft
source_hash: e5edb281987b0b15
---

## Tasks
type: task-table
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/19-capstone-projects/39-instruction-tuning-sft/code/main.py, phases/19-capstone-projects/38-classifier-finetuning/code/main.py

| Task | Code |
|---|---|
| Add LoRA to a model | `model = get_peft_model(model, lora_config)` |
| Show trainable share | `model.print_trainable_parameters()` |
| Load the base in 4-bit | `BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4")` |
| Count trainable params | `count_parameters(model)` |
| Save the adapter only | `model.save_pretrained("./lora-adapter")` |
| Merge the adapter into W | `merge_lora_weights(model)` |
| Skip masked labels in the loss | `F.cross_entropy(..., ignore_index=ignore_index)` |
| Head-only training | `freeze_body(model)` |

## LoRA path in one layer
type: architecture
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md

```arch
node | x | 1,1 | Input x |
node | w | 2,1 | Frozen W | out x in
node | a | 2,2 | A | r x in, Gaussian init
node | b | 3,2 | B | out x r, zero init
node | s | 4,2 | Scale | alpha / r
node | add | 4,1 | Add | Wx + scaled BAx
node | y | 5,1 | Output y |
node | m | 5,2 | Merged W' | W + (alpha/r) BA
edge | x | w |
edge | x | a |
edge | a | b |
edge | b | s |
edge | w | add |
edge | s | add |
edge | add | y |
edge | s | m | merge
```

## LoRA from scratch
type: annotated-code
lang: python
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/11-llm-engineering/08-fine-tuning-lora/code/lora.py

```python
class LoRALayer(nn.Module):
    def __init__(self, in_features, out_features, rank=8, alpha=16):
        super().__init__()
        self.scaling = alpha / rank  # alpha / r
        self.A = nn.Parameter(torch.randn(in_features, rank) * (1 / math.sqrt(rank)))
        self.B = nn.Parameter(torch.zeros(rank, out_features))  # BA starts at zero
    def forward(self, x):
        return (x @ self.A @ self.B) * self.scaling  # scaled low-rank update

class LinearWithLoRA(nn.Module):
    # __init__ wraps linear and builds self.lora
        for param in self.linear.parameters():
            param.requires_grad = False  # freeze W
    def forward(self, x):
        return self.linear(x) + self.lora(x)  # Wx + scaled BAx
```

## QLoRA with PEFT
type: annotated-code
lang: python
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md

```python
bnb_config = BitsAndBytesConfig(load_in_4bit=True,  # 4-bit base
    bnb_4bit_quant_type="nf4",  # NF4 levels
    bnb_4bit_compute_dtype=torch.bfloat16,
    bnb_4bit_use_double_quant=True)  # quantize the scales too
model = AutoModelForCausalLM.from_pretrained("<model>",
    quantization_config=bnb_config, device_map="auto")
lora_config = LoraConfig(task_type=TaskType.CAUSAL_LM, r=16,
    lora_alpha=32, lora_dropout=0.05,
    target_modules=["q_proj", "v_proj"])  # query and value
model = get_peft_model(model, lora_config)  # adds A and B
model.save_pretrained("./lora-adapter")  # 10-100MB adapter
```

## One SFT training step
type: flow
from: phases/19-capstone-projects/39-instruction-tuning-sft/docs/en.md, phases/19-capstone-projects/39-instruction-tuning-sft/code/main.py

1. **Template**: each pair becomes `<INST> instruction <RESP> response`.
2. **Tokenize**: byte ids plus `INST_ID = 256`, `RESP_ID = 257`, `PAD_ID = 258`.
3. **Collate**: `sft_collate` pads each batch to its longest sequence.
4. **Mask**: labels get `-100` on instruction, padding and the boundary.
5. **Forward**: the model sees the full sequence, instruction included.
6. **Loss**: `shifted_loss` predicts position i+1 with `ignore_index=-100`.
7. **Step**: Adam updates the body. Exact-match runs every fifth epoch.

## Key formulas
type: concepts
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/10-llms-from-scratch/06-instruction-tuning-sft/docs/en.md

- **LoRA forward**: $y = Wx + BAx$ with $W$ frozen.
- **Scaled update**: $y = Wx + \frac{\alpha}{r} \cdot BAx$
- **Parameter count**: rank $r$ trains $2 \cdot r \cdot d$ parameters instead of $d^2$.
- **Example**: $r=16$ on 4096x4096 trains 131,072 of 16,777,216. That is 0.78%.
- **Merge**: $W' = W + (\alpha/r) \cdot BA$. Same size, no inference overhead.
- **Full fine-tune memory**: 8B in fp16 is about 56GB with gradients and Adam states.

## Defaults
type: defaults
from: phases/11-llm-engineering/08-fine-tuning-lora/outputs/skill-fine-tuning-guide.md, phases/11-llm-engineering/08-fine-tuning-lora/outputs/prompt-lora-advisor.md, phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/10-llms-from-scratch/06-instruction-tuning-sft/docs/en.md

| Setting | Default | Note |
|---|---|---|
| r | 16 | safe start for most tasks |
| lora_alpha | 32 | 2 x rank, alpha = rank is conservative |
| lora_dropout | 0.05 | 0.1 below 5K examples |
| target_modules | q_proj, v_proj | add more only if quality is low |
| QLoRA learning rate | 2e-4 | higher than full fine-tuning |
| LoRA fp16 learning rate | 5e-5 | lower than QLoRA |
| Full SFT learning rate | 2e-5 | Llama 2 Chat, 15x below pre-training |
| Epochs | 1-3 | more risks overfitting |

## Troubleshooting
type: troubleshooting
from: phases/11-llm-engineering/08-fine-tuning-lora/outputs/skill-fine-tuning-guide.md, phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/10-llms-from-scratch/06-instruction-tuning-sft/docs/en.md, phases/19-capstone-projects/39-instruction-tuning-sft/docs/en.md

| Symptom | Cause | Fix |
|---|---|---|
| NaN losses with Llama models | pad token not set | set the pad token |
| Loss does not decrease | learning rate too low for LoRA | try 2e-4 |
| Loss falls, eval quality bad | overfitting | fewer epochs, more dropout, more data |
| Weak on complex tasks | rank too low or wrong targets | try r=32 or add MLP layers |
| General skills drop after SFT | learning rate too high | use 1e-5 to 5e-5 and 1-3 epochs |
| Model predicts the instruction | no loss mask | set `-100` on instruction and padding |
| OOM on long sequences | optimizer states exceed GPU memory | use `optim="paged_adamw_8bit"` |

## Abbreviations
type: glossary
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/10-llms-from-scratch/06-instruction-tuning-sft/docs/en.md

- **LoRA**: Low-Rank Adaptation. Freeze W, train A and B.
- **QLoRA**: Quantized LoRA. 4-bit NF4 base, fp16 adapters.
- **NF4**: Normal Float 4. 16 levels at normal quantiles.
- **PEFT**: Hugging Face library for LoRA, QLoRA, DoRA and IA3.
- **TRL**: Hugging Face trainers for SFT, DPO, GRPO, PPO and ORPO.
- **SFT**: Supervised Fine-Tuning on instruction and response pairs.
- **ChatML**: role format with `<|im_start|>` and `<|im_end|>` tokens.
- **Double quantization**: quantizes the per-block scales to fp8.

## Questions
type: qa
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/19-capstone-projects/39-instruction-tuning-sft/docs/en.md, phases/11-llm-engineering/08-fine-tuning-lora/outputs/skill-fine-tuning-guide.md

- **Why is B zero at init?** BA starts at zero. Training starts from the original behavior.
- **What does alpha do?** It scales the update by alpha / r, apart from the base learning rate.
- **Merge or keep separate?** Keep separate to serve many tasks. Merge for one model.
- **Why mask the instruction?** Unmasked, the model learns to predict the instruction and the response signal gets smaller.
- **When not to fine-tune?** When a prompt works, when you need facts (use RAG) or below 500 examples.

## Tips
type: tips
from: phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md, phases/11-llm-engineering/08-fine-tuning-lora/outputs/skill-fine-tuning-guide.md, phases/19-capstone-projects/38-classifier-finetuning/docs/en.md, phases/10-llms-from-scratch/06-instruction-tuning-sft/docs/en.md

- Try prompt engineering first, then RAG, then fine-tuning.
- Evaluate every 100 steps on a held-out set. Keep the best checkpoint.
- Compare base plus prompt against base plus LoRA on 200+ held-out examples.
- On 800 examples, train the head only with a frozen body.
- Track raw text loss. A rise above 10-15% means SFT is too aggressive.

## Choose a method
type: command-table
from: phases/11-llm-engineering/08-fine-tuning-lora/outputs/prompt-lora-advisor.md, phases/11-llm-engineering/08-fine-tuning-lora/docs/en.md

| Choice | Use when |
|---|---|
| Full fine-tune, about 56GB for 7B | VRAM >= 2x model size in fp16 |
| LoRA on fp16 base, about 18GB | VRAM >= model size in fp16 |
| QLoRA on 4-bit base, about 6GB | VRAM >= model size / 4 |
| `r=16` | multi-domain tasks, instruction following, chat |
| `r=32` | code generation, complex reasoning, math |
Code and artifacts 1
What you take away

Saved in your notebook on this device.

Sources and limits

Reading checkpoints are self-reported. Test results are recorded separately and apply to the executed cases.

AI Engineering from Scratch · 3f9715c · MIT

Download this file ↓