# Advanced Dragon Training: QLoRA, DPO, and Quantization

Ember flies and follows orders. This lesson is the advanced flight school: training *bigger* dragons on the same hardware (QLoRA), teaching *judgment* instead of imitation (DPO), and shrinking your trained model for fast deployment (quantization + GGUF). These three techniques are what separate weekend experiments from models you run every day.

## QLoRA: Training Above Your Weight Class

You already used 4-bit loading in lesson 5 on the PC path — that *was* QLoRA. Now let's use it deliberately, for its real purpose: **training models that don't otherwise fit.**

The idea: compress the frozen base weights to 4-bit precision (NF4 format), keep the LoRA adapters in 16-bit. The base loses a sliver of quality from compression, but since the adapters — the part that's learning — stay full precision, final results land remarkably close to full LoRA.

From lesson 3's table, QLoRA is what unlocks each machine's ceiling:

- **RTX 4090 (24 GB):** 14B comfortably, 32B just barely (short context, batch 1, gradient checkpointing — expect a fight).
- **M3 Ultra (96 GB):** 32B with room to spare; 70B is possible if you accept overnight runs.

When should Ember upgrade from 4B? Only when evaluation says so: if v3+ keeps failing on *substance* (weak reasoning in complex answers) while format and tone are solid, that's the "base model too small" symptom from lesson 6 — jump to Qwen3-8B or 14B and rerun the same pipeline. Nothing else changes; that's the beauty of the setup.

<div class="pro-tip">
<h4>Pro Tip</h4>
Scaling from 4B to 8B roughly doubles training time and memory — usually worth it. 14B → 32B doubles it again for a smaller quality jump on simple tasks. Let your <em>test scores</em> justify each jump, not model-size bragging rights.
</div>

## DPO: Teaching Taste, Not Just Tricks

Supervised fine-tuning (everything so far) shows the model *good* answers. **DPO — Direct Preference Optimization** — shows it *pairs*: for the same prompt, a **chosen** answer and a **rejected** one. The model learns the *direction* of your preferences: what makes one answer better than another.

```json
{"prompt": "Customer: You people are useless! Where is my package?!",
 "chosen": "I understand the frustration — let's fix this properly. [calm, structured answer with next step]",
 "rejected": "We apologize for any inconvenience caused. Your satisfaction is important to us. [corporate mush, no next step]"}
```

<div class="concept-box">
<h4>Concept</h4>
SFT teaches "here is what a good answer looks like". DPO teaches "between these two plausible answers, prefer <em>this</em> kind". It shines exactly where SFT plateaus: subtle style judgment, de-escalation, choosing brevity, avoiding corporate filler — things that are hard to demonstrate but easy to compare.
</div>

The recipe order matters: **always SFT first, then DPO on top** — typically 100–500 preference pairs, with a lower learning rate (~5e-6). The best rejected answers aren't strawmen; they're your *own model's* real, plausible-but-flawed outputs from evaluation runs. On the PC path this is `trl`'s `DPOTrainer` with the same Unsloth setup; on the Mac, `mlx-lm` ships preference-training support (check `mlx_lm.lora --help` for the current flags — the ecosystem moves fast).

<div class="honest-note">
<h4>Honest Note</h4>
DPO is a seasoning, not a meal. On a small model with a solid SFT, 200 good pairs noticeably sharpen tone judgment. DPO cannot rescue a bad SFT, and overdoing it (too many epochs, too high a learning rate) produces a model that's confidently weird. If you only have one evening, spend it on better SFT data.
</div>

## Quantization: Shrinking the Dragon for Daily Flight

Training precision and serving precision are different games. Once Ember is final, you'll want it **small and fast** for everyday use. That's quantization at inference time, and the universal format for it is **GGUF** — the format Ollama, LM Studio, and llama.cpp all speak.

The pipeline, on either machine:

<svg viewBox="0 0 720 150" width="100%" xmlns="http://www.w3.org/2000/svg" role="img" aria-label="Pipeline: adapters merge into base, convert to GGUF, quantize, serve">
    <defs>
    <marker id="arr2" markerWidth="8" markerHeight="8" refX="7" refY="4" orient="auto">
      <path d="M0,0 L8,4 L0,8 z" fill="#7b8cff"/>
    </marker>
  </defs>
  <rect x="20" y="45" width="150" height="60" rx="10" fill="rgba(123,140,255,0.08)" stroke="#7b8cff" stroke-width="1.5"/>
  <text x="95" y="70" text-anchor="middle" fill="#e6e9f0" font-family="-apple-system, sans-serif" font-size="13px">Merge adapters</text>
  <text x="95" y="90" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="11px">base + LoRA → one model</text>
  <path d="M170 75 L200 75" stroke="#7b8cff" stroke-width="1.5" fill="none" marker-end="url(#arr2)"/>
  <rect x="200" y="45" width="150" height="60" rx="10" fill="rgba(123,140,255,0.08)" stroke="#7b8cff" stroke-width="1.5"/>
  <text x="275" y="70" text-anchor="middle" fill="#e6e9f0" font-family="-apple-system, sans-serif" font-size="13px">Convert to GGUF</text>
  <text x="275" y="90" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="11px">llama.cpp convert script</text>
  <path d="M350 75 L380 75" stroke="#7b8cff" stroke-width="1.5" fill="none" marker-end="url(#arr2)"/>
  <rect x="380" y="45" width="150" height="60" rx="10" fill="rgba(123,140,255,0.08)" stroke="#7b8cff" stroke-width="1.5"/>
  <text x="455" y="70" text-anchor="middle" fill="#e6e9f0" font-family="-apple-system, sans-serif" font-size="13px">Quantize Q4_K_M</text>
  <text x="455" y="90" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="11px">~8 GB → ~2.5 GB (4B)</text>
  <path d="M530 75 L560 75" stroke="#7b8cff" stroke-width="1.5" fill="none" marker-end="url(#arr2)"/>
  <rect x="560" y="45" width="140" height="60" rx="10" fill="rgba(79,255,176,0.08)" stroke="#4fffb0" stroke-width="1.5"/>
  <text x="630" y="70" text-anchor="middle" fill="#e6e9f0" font-family="-apple-system, sans-serif" font-size="13px">Serve</text>
  <text x="630" y="90" text-anchor="middle" fill="#9aa3b5" font-family="-apple-system, sans-serif" font-size="11px">Ollama · LM Studio</text>
</svg>

Step one is merging — folding your adapters into the base weights so you get a single standalone model:

```bash
# Mac (MLX)
mlx_lm.fuse --model Qwen/Qwen3-4B-Instruct \
  --adapter-path adapters/ember-v3 --save-path ember-merged
```

```python
# PC (Unsloth) — can even save GGUF directly:
model.save_pretrained_merged("ember-merged", tokenizer)
model.save_pretrained_gguf("ember-gguf", tokenizer, quantization_method="q4_k_m")
```

**Q4_K_M** is the community's default sweet spot: ~4.5 bits per weight, quality loss barely measurable on most tasks, size cut ~4× vs 16-bit. Use Q8_0 when you want near-lossless and have the memory; go below Q4 only for the memory-desperate.

<div class="try-it">
<h4>Try It</h4>
Take your best Ember, merge, convert, quantize to Q4_K_M — then run your lesson-6 test set <em>on the quantized model</em>. Compare scores with the unquantized version. Seeing (numerically!) that Q4_K_M barely moved your metrics is how you earn the confidence to deploy small.
</div>

<div class="checkpoint">
<h4>Checkpoint</h4>
You know when QLoRA unlocks bigger bases, when DPO adds judgment on top of SFT, and how merge → GGUF → Q4_K_M turns a training artifact into a deployable model file. One thing left: releasing your dragon into the world.
</div>
