Course: Course 3 — LLM Fine-Tuning Masterclass Module: FTDD-05 — Axolotl: Config-Driven Production Fine-Tuning Duration: 35 minutes Environment: Python 3.11+. A consumer GPU (RTX 4090 / 16–24GB) OR Apple Silicon (M-series) OR free Google Colab T4. ~6GB free disk. Multi-GPU is optional — this lab is designed to run single-GPU.
By the end of this lab you will have:
This lab is deliberately config-first. The point is to feel the recipe-as-file pattern before you spend the rest of the course deep in PEFT and alignment.
python3.11 -m venv ftdd05-env && source ftdd05-env/bin/activate
# Axolotl pulls in TRL, transformers, peft, accelerate, deepspeed as dependencies.
pip install -q axolotl[flash-attn] transformers datasets torch
Note: On Apple Silicon or Colab T4, omit
[flash-attn](FlashAttention is CUDA/Ampere+). A plainpip install -q axolotlworks and falls back to standard attention. The lab is designed to run without FlashAttention.
Verify the install:
axolotl --version 2>/dev/null || python -c "import axolotl; print('axolotl OK')"
Create sft.yml. This is the entire recipe — five sections. Read each comment.
# sft.yml — Axolotl SFT on a 1B base
# ============================================
# --- 1. MODEL ---
base_model: openbmb/MiniCPM5-1B
tokenizer_type: AutoTokenizer
# --- 2. DATA ---
datasets:
- path: HuggingFaceTB/smoltalk
type: chat_template
split: train
val_set_size: 0.02 # hold out 2% for eval (declared in the source of truth)
# --- 3. PEFT ---
adapter: qlora # qlora | lora | none
lora_r: 8
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules: [q_proj, v_proj]
# --- 4. TRAINING HYPERPARAMETERS (PIN THESE) ---
sequence_len: 2048
sample_packing: true
micro_batch_size: 1
gradient_accumulation_steps: 4 # effective batch = 1 * 4 = 4
num_epochs: 1
learning_rate: 2e-4 # PINNED — never rely on a default
lr_scheduler: cosine
warmup_steps: 10
bf16: auto # auto-detect; record what it resolves to
# --- 5. DISTRIBUTED + OUTPUT ---
# single-GPU: no deepspeed/fsdp block needed
output_dir: ./out/minicpm-smoltalk
save_steps: 100
eval_steps: 100
logging_steps: 10
Run it:
axolotl train sft.yml
Record: (a) the final training loss, (b) what bf16: auto resolved to (check the logs — it will state the detected precision), (c) the steerable-params percentage (Axolotl logs the LoRA param count).
What just happened (the teaching moment): You wrote no training loop. The YAML IS the recipe. Axolotl read it, configured a TRL
SFTTrainerwith a QLoRA adapter, and ran. The same file would reproduce this run on a teammate's machine.
Now feel the declarative pattern: change ONE line, observe the propagation.
Mutation A — LoRA off (full fine-tune):
Edit sft.yml: change adapter: qlora to adapter: none. (You may also need to lower micro_batch_size if you OOM on full FT of 1B — but a 1B model usually fits.)
axolotl train sft.yml
Record: the new steerable-params percentage (should be ~100% now — all params train) and the final loss. Notice you changed ONE line; the entire training regime switched from QLoRA to full FT.
Mutation B — change the batch recipe:
Restore adapter: qlora. Now change gradient_accumulation_steps: 4 to gradient_accumulation_steps: 8 (and keep micro_batch_size: 1).
axolotl train sft.yml
Record: the effective batch size (now 1 × 8 = 8) and how the loss curve differs from Phase 1. Notice again: one line changed, the effective batch doubled, the rest of the recipe is identical.
The pattern: every meaningful change is a localized edit to the YAML. There is no second script, no scattered code. The config is the complete source of truth.
Run the six-item checklist against your sft.yml. For each, answer yes or fix it:
learning_rate, lora_r, sequence_len — all explicit?)split: train is declared — good. What about the val split?)deepspeed: or fsdp: block.)bf16: auto resolve to? Record it. In production, pin it explicitly.)val_set_size: 0.02, eval_steps: 100 — yes.)axolotl train sft.yml and get your loss curve? Any hidden state?)Fix anything that fails. Commit the final sft.yml to your report.
No code. Answer in 3–5 sentences:
Submit ftdd05-lab-report.md:
sft.yml (all five sections); the final training loss; the detected precision; the LoRA steerable-params %.sft.yml.sft.yml with all five sections. A successful run produces a decreasing loss curve. bf16: auto typically resolves to true on A100/H100/RTX 4090 and to FP16 on older GPUs / MPS. The LoRA steerable-params % should be under 1% (typically 0.1–0.5% for r=8 on q/v_proj of a 1B model) — the Steering Stack thesis (FT00) felt through Axolotl.adapter: none): steerable params jumps to ~100% (all params train — full fine-tune). The loss curve may differ; on a small dataset full FT may overfit faster. The point is the ONE-LINE regime switch.split: train); (3) distributed declared (yes — single-GPU is a declared no-block choice); (4) precision known (the student should record what bf16: auto resolved to; in production, pin it); (5) eval declared (yes); (6) reproducible from YAML alone (yes, after fixes).deepspeed: block pointing at a ZeRO-2 config (Axolotl ships examples in its repo) and re-run. Observe how the config declares sharding — this is the multi-GPU orchestration value-add over raw TRL. (Sets up the production path.)trl-lib/ultrafeedback_binarized) and run. Observe that the config surface is uniform across objectives — the same declarative pattern, a different trainer underneath. (Sets up FT13.)git diff them. See reproducibility-as-a-diff — the exact property that makes config-as-source-of-truth win in production.# Lab Specification — Module FTDD-05: Axolotl
**Course**: Course 3 — LLM Fine-Tuning Masterclass
**Module**: FTDD-05 — Axolotl: Config-Driven Production Fine-Tuning
**Duration**: 35 minutes
**Environment**: Python 3.11+. A consumer GPU (RTX 4090 / 16–24GB) OR Apple Silicon (M-series) OR free Google Colab T4. ~6GB free disk. Multi-GPU is optional — this lab is designed to run single-GPU.
---
## Learning objectives
By the end of this lab you will have:
1. **Written an Axolotl YAML config from scratch** — all five sections (model, data, PEFT, hyperparams, distributed/output) — and run it to produce a trained model.
2. **Mutated the config** (LoRA on/off, batch size) and observed how a single declarative change propagates through the run — the config-as-source-of-truth pattern, felt directly.
3. **Run the reproducibility checklist** against your config and fixed any unspecified defaults or unpinned splits.
4. **Stated, in your own words, why the declarative YAML is more reproducible than a training script** — and what would force you off Axolotl onto raw TRL.
This lab is deliberately *config-first*. The point is to feel the recipe-as-file pattern before you spend the rest of the course deep in PEFT and alignment.
---
## Phase 0 — Environment setup (5 min)
```bash
python3.11 -m venv ftdd05-env && source ftdd05-env/bin/activate
# Axolotl pulls in TRL, transformers, peft, accelerate, deepspeed as dependencies.
pip install -q axolotl[flash-attn] transformers datasets torch
```
> **Note:** On Apple Silicon or Colab T4, omit `[flash-attn]` (FlashAttention is CUDA/Ampere+). A plain `pip install -q axolotl` works and falls back to standard attention. The lab is designed to run without FlashAttention.
Verify the install:
```bash
axolotl --version 2>/dev/null || python -c "import axolotl; print('axolotl OK')"
```
---
## Phase 1 — Write the YAML config (10 min)
Create `sft.yml`. This is the entire recipe — five sections. Read each comment.
```yaml
# sft.yml — Axolotl SFT on a 1B base
# ============================================
# --- 1. MODEL ---
base_model: openbmb/MiniCPM5-1B
tokenizer_type: AutoTokenizer
# --- 2. DATA ---
datasets:
- path: HuggingFaceTB/smoltalk
type: chat_template
split: train
val_set_size: 0.02 # hold out 2% for eval (declared in the source of truth)
# --- 3. PEFT ---
adapter: qlora # qlora | lora | none
lora_r: 8
lora_alpha: 16
lora_dropout: 0.05
lora_target_modules: [q_proj, v_proj]
# --- 4. TRAINING HYPERPARAMETERS (PIN THESE) ---
sequence_len: 2048
sample_packing: true
micro_batch_size: 1
gradient_accumulation_steps: 4 # effective batch = 1 * 4 = 4
num_epochs: 1
learning_rate: 2e-4 # PINNED — never rely on a default
lr_scheduler: cosine
warmup_steps: 10
bf16: auto # auto-detect; record what it resolves to
# --- 5. DISTRIBUTED + OUTPUT ---
# single-GPU: no deepspeed/fsdp block needed
output_dir: ./out/minicpm-smoltalk
save_steps: 100
eval_steps: 100
logging_steps: 10
```
Run it:
```bash
axolotl train sft.yml
```
**Record**: (a) the final training loss, (b) what `bf16: auto` resolved to (check the logs — it will state the detected precision), (c) the steerable-params percentage (Axolotl logs the LoRA param count).
> **What just happened (the teaching moment):** You wrote no training loop. The YAML IS the recipe. Axolotl read it, configured a TRL `SFTTrainer` with a QLoRA adapter, and ran. The same file would reproduce this run on a teammate's machine.
---
## Phase 2 — Mutate the config (10 min)
Now feel the declarative pattern: change ONE line, observe the propagation.
**Mutation A — LoRA off (full fine-tune):**
Edit `sft.yml`: change `adapter: qlora` to `adapter: none`. (You may also need to lower `micro_batch_size` if you OOM on full FT of 1B — but a 1B model usually fits.)
```bash
axolotl train sft.yml
```
**Record**: the new steerable-params percentage (should be ~100% now — all params train) and the final loss. Notice you changed ONE line; the entire training regime switched from QLoRA to full FT.
**Mutation B — change the batch recipe:**
Restore `adapter: qlora`. Now change `gradient_accumulation_steps: 4` to `gradient_accumulation_steps: 8` (and keep `micro_batch_size: 1`).
```bash
axolotl train sft.yml
```
**Record**: the effective batch size (now 1 × 8 = 8) and how the loss curve differs from Phase 1. Notice again: one line changed, the effective batch doubled, the rest of the recipe is identical.
> **The pattern:** every meaningful change is a localized edit to the YAML. There is no second script, no scattered code. The config is the complete source of truth.
---
## Phase 3 — The reproducibility checklist (5 min)
Run the six-item checklist against your `sft.yml`. For each, answer yes or fix it:
1. **Every hyperparameter pinned?** (Is any value riding an unspecified default? `learning_rate`, `lora_r`, `sequence_len` — all explicit?)
2. **Dataset path AND split explicit?** (`split: train` is declared — good. What about the val split?)
3. **Distributed strategy declared?** (Single-GPU: no block. That itself is a declared choice. If you were on multi-GPU, you'd need a `deepspeed:` or `fsdp:` block.)
4. **Precision known?** (What did `bf16: auto` resolve to? Record it. In production, pin it explicitly.)
5. **Eval declared?** (`val_set_size: 0.02`, `eval_steps: 100` — yes.)
6. **Reproducible from YAML alone?** (Could a teammate run `axolotl train sft.yml` and get your loss curve? Any hidden state?)
Fix anything that fails. Commit the final `sft.yml` to your report.
---
## Phase 4 — When would you leave Axolotl? (5 min)
No code. Answer in 3–5 sentences:
1. Your next job needs GRPO with a custom Python reward function (a domain-specific legal-citation scorer). Can you do this entirely in an Axolotl YAML? Why or why not? What do you reach for instead?
2. You have a single RTX 4090 and need to fine-tune a 7B model as fast as possible. Axolotl works, but what tool would give you ~2x throughput and ~half the VRAM, and why?
3. State in one sentence why "I tweaked the config live with CLI overrides" destroys the reproducibility guarantee, and what you must do instead.
---
## Deliverables
Submit `ftdd05-lab-report.md`:
- [ ] Phase 1: your `sft.yml` (all five sections); the final training loss; the detected precision; the LoRA steerable-params %.
- [ ] Phase 2: Mutation A (adapter: none) — new steerable-params % and loss; Mutation B (grad_accum 8) — effective batch size and loss-curve difference. Note that each was a one-line change.
- [ ] Phase 3: the six-item checklist with yes/fix for each; your final committed `sft.yml`.
- [ ] Phase 4: your 3–5 sentence answers.
---
## Solution key
- **Phase 1**: a valid `sft.yml` with all five sections. A successful run produces a decreasing loss curve. `bf16: auto` typically resolves to `true` on A100/H100/RTX 4090 and to FP16 on older GPUs / MPS. The LoRA steerable-params % should be **under 1%** (typically 0.1–0.5% for r=8 on q/v_proj of a 1B model) — the Steering Stack thesis (FT00) felt through Axolotl.
- **Phase 2**:
- Mutation A (`adapter: none`): steerable params jumps to ~100% (all params train — full fine-tune). The loss curve may differ; on a small dataset full FT may overfit faster. The point is the ONE-LINE regime switch.
- Mutation B (grad_accum 8): effective batch = 1 × 8 = 8 (doubled from Phase 1's 4). The loss curve should be smoother (larger batch reduces gradient noise) but the run takes ~2x longer per optimizer step. Again, a one-line change.
- **Phase 3**: model answers — (1) all hyperparams pinned (yes, if the student left no defaults); (2) split explicit (yes — `split: train`); (3) distributed declared (yes — single-GPU is a declared no-block choice); (4) precision known (the student should record what `bf16: auto` resolved to; in production, pin it); (5) eval declared (yes); (6) reproducible from YAML alone (yes, after fixes).
- **Phase 4** (model answers):
1. No — a custom Python reward function is CODE, not config. Axolotl is configured, not programmed; a custom reward cannot be fully expressed in a static YAML. Reach for raw TRL's Python API (GRPOTrainer with the reward passed as a callable).
2. Unsloth. Its hand-tuned Triton kernels replace TRL's attention/LoRA-backward kernels for ~2x throughput and ~half the VRAM on a single GPU, sub-30B. Axolotl works but leaves that speed on the table.
3. Live overrides make the on-disk YAML no longer match what ran — the config becomes documentation, not source, and the run is unreproducible. You must write every override back into the YAML and commit it.
---
## Stretch goals
1. **Add a multi-GPU block.** If you have ≥2 GPUs, add a `deepspeed:` block pointing at a ZeRO-2 config (Axolotl ships examples in its repo) and re-run. Observe how the config declares sharding — this is the multi-GPU orchestration value-add over raw TRL. (Sets up the production path.)
2. **Swap to DPO via config.** Change the config to a DPO recipe over a small preference dataset (e.g., `trl-lib/ultrafeedback_binarized`) and run. Observe that the config surface is uniform across objectives — the same declarative pattern, a different trainer underneath. (Sets up FT13.)
3. **Diff two runs.** Run Phase 1, then change ONLY the learning rate (e.g., 2e-4 → 5e-5), commit both YAMLs to git, and `git diff` them. See reproducibility-as-a-diff — the exact property that makes config-as-source-of-truth win in production.