# First Flight: Train Your First LoRA

Today Ember flies. You'll pick a base model, attach LoRA adapters, and run a real fine-tune on your own machine — the Mac path with MLX, the PC path with Unsloth. By the end of this lesson you will have adapter weights on disk and a model that already sounds different from the base.

## What LoRA Actually Does (60 Seconds of Theory)

Fine-tuning all weights of even a 4B model needs ~70 GB. LoRA's insight: you don't need to *change* the big weight matrices — you can *add a small correction* alongside them. For each targeted matrix, LoRA trains two thin matrices (A and B) whose product is the correction. Only those train; the base stays frozen.

<svg viewBox="0 0 720 300" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="LoRA: frozen base weights plus small trainable adapter matrices">
    <rect x="60" y="60" width="180" height="180" rx="10" fill="rgba(123,140,255,0.10)" stroke="#7b8cff" stroke-width="1.5"/>
  <text x="150" y="145" text-anchor="middle" fill="#e6e9f0" font-family="-apple-system, sans-serif" font-size="14px">Base weights W</text>
  <text x="150" y="168" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="12px">4,000,000,000 params</text>
  <text x="150" y="52" text-anchor="middle" fill="#7b8cff" font-family="monospace" font-size="12px">FROZEN ❄</text>
  <text x="290" y="158" text-anchor="middle" font-size="26" fill="#e6e9f0" font-family="-apple-system, sans-serif">+</text>
  <polygon points="340,60 400,60 340,240" fill="rgba(79,255,176,0.15)" stroke="#4fffb0" stroke-width="1.5"/>
  <text x="352" y="160" text-anchor="middle" fill="#4fffb0" font-family="monospace" font-size="12px">A</text>
  <polygon points="410,60 470,60 470,240 410,120" fill="rgba(79,255,176,0.15)" stroke="#4fffb0" stroke-width="1.5"/>
  <text x="452" y="160" text-anchor="middle" fill="#4fffb0" font-family="monospace" font-size="12px">B</text>
  <text x="405" y="52" text-anchor="middle" fill="#4fffb0" font-family="monospace" font-size="12px">TRAINABLE 🔥</text>
  <text x="405" y="266" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="12px">adapters A×B — often &lt;1% of params</text>
  <text x="530" y="158" text-anchor="middle" font-size="26" fill="#e6e9f0" font-family="-apple-system, sans-serif">=</text>
  <rect x="560" y="60" width="140" height="180" rx="10" fill="rgba(79,255,176,0.08)" stroke="#4fffb0" stroke-width="1.5"/>
  <text x="630" y="140" text-anchor="middle" fill="#e6e9f0" font-family="-apple-system, sans-serif" font-size="14px">Ember</text>
  <text x="630" y="163" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="12px">base + your</text>
  <text x="630" y="180" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="12px">behavior</text>
</svg>

<div class="concept-box">
<h4>Concept</h4>
The knob called <strong>rank (r)</strong> is the thickness of those adapter matrices. r=8 is a light touch (tone), r=16–32 is standard (our project), r=64+ for heavier task learning. Higher rank = more capacity to learn = more memory and more risk of memorizing your data instead of learning from it.
</div>

## Picking the Base Model

For Ember we want a small, modern, instruction-tuned model with good multilingual skills. In 2026 the sweet spot for this project is **Qwen3-4B-Instruct** (great multilingual, permissive license) — with **Llama-3.2-3B-Instruct** and **Gemma-3-4B** as solid alternatives. From lesson 3's math: QLoRA on 4B needs ~4–6 GB. Both machines fit easily — deliberately, since your first run should never also be a memory fight.

**Always start from the *-Instruct* version**, not the raw base — it already knows how to hold a conversation; you're adjusting behavior, not teaching dialogue from zero.

## Path A — Mac Studio (MLX)

Install once, then train:

```bash
pip install mlx-lm
mlx_lm.lora \
  --model Qwen/Qwen3-4B-Instruct \
  --train \
  --data ./data \
  --batch-size 4 \
  --iters 600 \
  --learning-rate 1e-5 \
  --adapter-path adapters/ember-v1
```

`--data ./data` points at the folder with `train.jsonl` / `valid.jsonl` from lesson 4. MLX downloads the model on first run, then you'll see a step counter with **train loss** and, periodically, **val loss**. On the M3 Ultra, this run takes roughly 20–40 minutes.

Talk to your dragon (base + adapters, no merging needed):

```bash
mlx_lm.generate \
  --model Qwen/Qwen3-4B-Instruct \
  --adapter-path adapters/ember-v1 \
  --prompt "My order hasn't arrived and it's been 10 days."
```

## Path B — PC with RTX 4090 (Unsloth)

Install (a CUDA-enabled PyTorch must be present), then this is the whole training script:

```python
from unsloth import FastLanguageModel
from trl import SFTTrainer, SFTConfig
from datasets import load_dataset

model, tokenizer = FastLanguageModel.from_pretrained(
    "unsloth/Qwen3-4B-Instruct",
    max_seq_length=2048,
    load_in_4bit=True,          # QLoRA: 4-bit frozen base
)
model = FastLanguageModel.get_peft_model(model, r=16, lora_alpha=16)

dataset = load_dataset("json", data_files="data/train.jsonl", split="train")
dataset = dataset.map(lambda ex: {"text": tokenizer.apply_chat_template(
    ex["messages"], tokenize=False)})

trainer = SFTTrainer(
    model=model, tokenizer=tokenizer, train_dataset=dataset,
    args=SFTConfig(
        per_device_train_batch_size=4,
        gradient_accumulation_steps=2,
        num_train_epochs=3,
        learning_rate=2e-4,
        logging_steps=10,
        output_dir="adapters/ember-v1",
    ),
)
trainer.train()
model.save_pretrained("adapters/ember-v1")
```

On the 4090 this finishes in roughly 8–15 minutes. Unsloth prints loss every 10 steps — same numbers to watch, same meaning.

<div class="pro-tip">
<h4>Pro Tip</h4>
If you hit <strong>out-of-memory</strong>, turn these three knobs in order: (1) reduce <code>max_seq_length</code> (2048 → 1024), (2) reduce batch size and raise gradient accumulation to compensate, (3) enable gradient checkpointing (<code>use_gradient_checkpointing="unsloth"</code> / MLX <code>--grad-checkpoint</code>). Same levers on both machines, in the same order.
</div>

## Reading the Loss (Your First Flight Instruments)

Loss measures "how surprised the model is by the correct answer" — lower is better. Healthy first flight:

- Train loss drops fast early (say 2.1 → 1.3 in the first 100 steps), then flattens gently.
- Val loss follows it down, a little higher.
- **Warning sign:** val loss starts *climbing* while train loss keeps falling. That's overfitting — the dragon is memorizing the food instead of learning to hunt. Lesson 6 deals with it properly.

<div class="honest-note">
<h4>Honest Note</h4>
Your v1 will be noticeably better at the <em>shape</em> of answers and only somewhat better at judgment. That's expected — you trained ~200 examples for a few hundred steps. Resist the urge to fix it by training 10× longer; v1's job is to prove the pipeline, and lesson 6's job is to make it good.
</div>

<div class="try-it">
<h4>Try It</h4>
After training, run the same 5 prompts through the <strong>base</strong> model and through <strong>base + adapters</strong>, side by side. Save both outputs to a file. Seeing the voice change in a diff is the moment this stops being theory — and that file becomes your first evaluation artifact for the next lesson.
</div>

<div class="checkpoint">
<h4>Checkpoint</h4>
You trained real LoRA adapters on your own hardware, you can chat with base+adapters, and you know the three out-of-memory levers and the overfitting warning sign. Next: grading your dragon honestly — and making v2 better than v1.
</div>
