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.
Concept
The knob called rank (r) 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.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:
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):
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:
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.
Pro Tip
If you hit out-of-memory, turn these three knobs in order: (1) reducemax_seq_length (2048 → 1024), (2) reduce batch size and raise gradient accumulation to compensate, (3) enable gradient checkpointing (use_gradient_checkpointing="unsloth" / MLX --grad-checkpoint). Same levers on both machines, in the same order.
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.