Chapter 02: Train an Adapter
Understand Dreambooth and training checkpoints, install mflux and smoke-test generation, prepare a raw→512 dataset with captions, then train (and resume) from a JSON config — including what drives wall-clock training time.
TUTOR WITH THEFOCUS.AI
Copy this prompt into Claude, ChatGPT, or any external AI assistant. It points the assistant to the course instructions and links it to your student profile to track your progress and customize observations.
You are not enrolled yet. Enroll to generate a Student ID to track lesson completions and store learning notes.
Chapter 02: Train an Adapter
Objective
Understand Dreambooth and checkpoints (what they store, how mflux saves and resumes), prove mflux can generate on your machine, prepare a raw → 512×512 subject dataset with a trigger token, and start one baseline training run from a JSON config — knowing how to continue if it stops. Before a multi-hour job, walk through what affects tuning time (epochs, dataset size, max resolution, previews, checkpoints) and set a deliberate baseline.
Your tutor walks this lesson gate by gate. Do not skip to training until the smoke test passes.
Concept: How Dreambooth works
A base image model has seen millions of dogs (or faces, or objects). If you prompt “a dog in a park,” you get a dog — not your dog. Your subject is a rare identity the base model never specialized on.
Dreambooth teaches one new concept from a small set of photos (about 15–20):
| Piece | Role |
|---|---|
| Subject photos | “This is the thing” — same identity, varied angle / light / background |
| Trigger token | A rare word (TOK, sks, …) that becomes the handle for that subject |
| Captions | Link each photo to text: a photo of TOK dog on the couch |
| Training | Nudge a small LoRA adapter so the model associates the token with that identity |
| Generation | Prompt with the token in a new scene: TOK dog on the moon → still your dog |
Mental model (one sentence)
Freeze the big model; train a thin patch so a rare word reliably means this subject, not “generic dog.”
Why a rare token?
If you only train on captions like “a golden retriever outside,” you fight the model’s existing meaning of those words. A rare token like TOK has almost no prior meaning, so the adapter can claim it.
What “good” looks like for this chapter
Not gallery-perfect portraits. Success:
When I prompt with
TOK, I recognize this subject in a new scene in under five seconds.
- Overfit often looks like: same training photo, same glued-on background.
- Underfit: “some dog,” not yours.
You are here for a working baseline and judgment — not a shippable product LoRA.
Tooling in one line
mflux is a minimal MLX port of modern image models (including FLUX-family / Z-Image stacks). Architectures are mostly hardcoded; Dreambooth/LoRA training is driven by a JSON config, not a pile of CLI training flags.
Concept: What a checkpoint is
Training a LoRA takes a long time. A checkpoint is a snapshot of training mid-run so you can:
- stop without losing hours of progress
- resume later from that point
- try generation from an intermediate adapter (after the run finishes or from the adapter inside the zip)
- compare “step 30 vs step 90” without re-running from zero
What is not a checkpoint
| Thing | Role |
|---|---|
| Base model weights | Frozen download (FLUX / Z-Image). Not rewritten each step. |
| Your photos + captions | Training data. Must still exist if you resume. |
| Preview PNGs / loss plots | Monitoring artifacts. Useful to look at; not enough to resume training. |
| Final adapter only | May be enough to generate; usually not enough to continue training with the same optimizer trajectory |
What a full training checkpoint usually stores
Enough to continue the same run:
- Adapter weights (the LoRA you care about)
- Optimizer state (AdamW momentum buffers, etc. — without these, “resume” is really “start a new train from those weights”)
- Iterator / step count (how far you got through epochs/steps)
- Config snapshot (what hyperparams this run used)
- Often loss history for plots
Think of it as a save game, not just the high score.
How mflux does it
In your train JSON:
"checkpoint": {
"save_frequency": 15,
"output_path": "training"
}
output_path— run directory (mflux creates subfolders under it).save_frequency— write a checkpoint every N training steps (example uses15).
Typical layout after a while:
training/ # or whatever output_path resolves to
checkpoints/
0000015_checkpoint.zip
0000030_checkpoint.zip
...
loss/
loss.html # loss curves
preview/
... # periodic sample generations
Each NNNNNNN_checkpoint.zip is a self-contained training snapshot. Inside the zip mflux packs things like:
- LoRA adapter weights
- optimizer state
- iterator state (step / progress)
- loss stats
- a config / run manifest
So the zip is for resume, not only for “here’s a LoRA file.”
Resuming in mflux (yes, it is supported)
Start a new run from config:
mflux-train --config ~/lora-eyes-open/train.json
Continue an interrupted run from a checkpoint zip:
mflux-train --resume ~/lora-eyes-open/training/checkpoints/0000030_checkpoint.zip
Rules that matter in practice:
- Provide exactly one of
--configor--resume— not both. Resume loads the config from inside the checkpoint. - Your dataset folder must still be available at the path the run expects. The zip is not a substitute for the images. Prefer absolute
"data"paths so laptop sleep / cwd changes do not break resume. - Resume restores adapter + optimizer + iterator so training continues from that step, rather than restarting epochs from scratch.
- Previews and loss plots under
preview//loss/are side outputs; use the zip to resume. - If you only want to generate with a mid-run adapter (not train further), you need the adapter weights from that checkpoint (or a saved LoRA path your mflux version exposes) — that is inference, not resume.
Why this shows up in Chapter 03
Sweeps mean many runs. Checkpoints let you:
- kill a bad run without losing earlier steps
- keep intermediate adapters for the contact sheet
- resume overnight jobs after a reboot
You do not need every knob yet — you need the mental model: checkpoint = resumable training state; data must still be there.
Concept check
Answer in your own words before looking at the key. Your tutor asks one question at a time and waits for you.
Q1. In Dreambooth, what is the job of the trigger token, and what goes wrong if you train only on captions like “a golden retriever sitting outside” with no rare token?
Q2. Someone has 20 photos of their dog: 12 nearly identical couch shots and 8 varied outdoor angles. For a first adapter meant to place that dog in new scenes, which photos should dominate the set, and why?
Q3. Laptop sleeps midway through mflux-train. You still have 0000030_checkpoint.zip and the same dataset/train folder. How do you continue, and what goes wrong if you only kept a loose adapter file but deleted the zip / optimizer state?
Concept answer key — attempt first
Answer key (concept)
Q1
Model answer: The trigger token is a rare handle the model should learn to mean this subject. Without it, training fights generic prior meanings (“golden retriever”) and the concept does not bind cleanly to a promptable word.
Pass criteria: token as rare handle / binding mechanism; problem with generic captions stated (prior conflict or weak binding)
Q2
Model answer: Prefer the varied outdoor (and other diverse) shots so the model learns identity across contexts, not “dog = that couch.” Keep at most a couple of similar couch frames; drop near-duplicates.
Pass criteria: favors variety / identity over duplicates; links to generalization to new scenes
Q3
Model answer: Resume with mflux-train --resume path/to/0000030_checkpoint.zip (not a fresh --config start). The zip holds adapter + optimizer + step state. A bare adapter without optimizer/iterator is not a full resume — you’d be starting a new optimization trajectory from those weights, not continuing the same run. Data folder must still exist.
Pass criteria: --resume + zip; data still needed; full state vs weights-only / cannot truly continue without optimizer+step state
Gate: Install mflux
Only after the concept check. On Apple Silicon macOS:
mkdir -p ~/lora-eyes-open && cd ~/lora-eyes-open
uv venv && source .venv/bin/activate
uv pip install mflux
Alternatively:
uv tool install --upgrade mflux
Confirm the CLI is on your path:
mflux-train --help
# and/or
mflux-generate-z-image-turbo --help
If install fails, stop here and fix with your tutor (Python version, uv, network, non-Apple hardware). This course’s Part 1 assumes MLX on Mac.
Gate: Verify it runs (smoke test)
Do not prepare a dataset or start training until this works.
Generate one small image (first run downloads model weights — can take a while and needs disk):
mflux-generate-z-image-turbo \
--prompt "A simple red apple on a white table, soft light" \
--width 512 \
--height 512 \
--seed 42 \
--steps 9 \
-q 8
Pass criteria for this gate: a PNG/JPEG appears without a crash, and you can open it.
If generation fails (memory, download, Metal), debug now. Long training will not fix a broken install.
Tell your tutor when the smoke test succeeds (or paste the error).
Gate: Collect raw photos
- Pick one subject you can judge in under five seconds (your dog, a person who consented, a product, a place).
- Create:
mkdir -p ~/lora-eyes-open/dataset/raw
mkdir -p ~/lora-eyes-open/dataset/train
-
Copy 15–20 photos into
dataset/raw/— leave them as originals (any resolution). Prefer:- same subject
- varied background, lighting, angle
- clear subject (not a blurry speck)
-
Confirm with your tutor: how many files are in
raw/? Name the subject and your chosen trigger token (e.g.TOK).
Do not overwrite originals. All resizing happens into train/.
Gate: Convert to 512×512
mflux training uses a max resolution setting; for this course baseline, standardize the train set at 512×512.
From ~/lora-eyes-open, a simple approach with macOS sips (one file at a time or loop). Example loop for jpegs/pngs in raw/:
cd ~/lora-eyes-open
# zsh: unmatched globs error — either:
setopt NULL_GLOB
# or use: for f in dataset/raw/*; do [ -f "$f" ] || continue; ...
i=1
for f in dataset/raw/*.{jpg,jpeg,png,JPG,JPEG,PNG,webp,WEBP}; do
[ -e "$f" ] || continue
out=$(printf "dataset/train/%02d.jpg" "$i")
# Fit inside 512x512, then pad/crop as needed for a square:
sips -Z 512 "$f" --out "$out"
sips -c 512 512 "$out"
i=$((i+1))
done
ls dataset/train | wc -l
Notes:
- On zsh,
dataset/raw/*.{jpg,jpeg,...}fails withno matches foundif any extension is missing. Usesetopt NULL_GLOBfor that shell, or loopdataset/raw/*and skip non-files. sips -c 512 512center-crops to a square after the long side is 512. If a photo is very wide, crop may cut ears/tails — re-crop important shots in Preview if needed.- You may use any tool (Preview, Photos, Python/Pillow) as long as train/ ends up with ~15–20 images at 512×512.
- Prefer a durable project dir (
~/lora-eyes-open), not a temp folder, before multi-hour training.
Pass this gate when: dataset/train has 15–20 square 512 images and dataset/raw still holds the originals.
Gate: Captions + trigger token
Beside each train image, add a matching .txt caption (same basename):
dataset/train/
01.jpg
01.txt
02.jpg
02.txt
...
preview.txt # optional: prompt for mid-training previews
Caption pattern (keep it short for the first run):
a photo of TOK dog sitting on grass
a photo of TOK dog sleeping on a couch
a photo of TOK dog looking at the camera outdoors
Rules:
- Put your rare token (
TOKor similar) in every caption. - Lightly describe pose/scene so the model does not learn one frozen pose only.
- Optional
preview.txt: a new scene, e.g.a photo of TOK dog sitting in a sunny park.
Pass this gate when: every NN.jpg has NN.txt, and you can paste one sample caption for your tutor.
Gate: Train from a JSON config
Training parameters live in JSON, not a long list of train CLI flags.
-
Create
~/lora-eyes-open/train.json. Start from the bundled example inside the mflux package/repo:- Example path in source:
src/mflux/models/common/training/_example/train.json - CLI:
mflux-train --config /path/to/train.json
- Example path in source:
-
Point
"data"at your train folder with an absolute path (resume is much less painful), e.g./Users/you/lora-eyes-open/dataset/train. -
Set checkpointing explicitly (or keep the example defaults), e.g.:
"checkpoint": {
"save_frequency": 15,
"output_path": "/Users/you/lora-eyes-open/training-run-01"
}
- Smaller
save_frequency→ more zips on disk, safer if the machine dies often. - Use a dedicated
output_pathper experiment so Chapter 03 sweeps do not overwrite each other.
-
Keep the example’s model/LoRA targets unless you know you need otherwise. For a low-memory machine, set
"low_ram": truein the config. -
Dry-run if supported:
mflux-train --config ~/lora-eyes-open/train.json --dry-run
- Start training (new run):
mflux-train --config ~/lora-eyes-open/train.json
- If the process dies or you stop it, resume from the latest zip (do not pass
--configagain for the same continuation):
ls /Users/you/lora-eyes-open/training-run-01/checkpoints/
mflux-train --resume /Users/you/lora-eyes-open/training-run-01/checkpoints/0000030_checkpoint.zip
Expect hours for the stock example config. Under output_path you should see checkpoints/, plus loss/ and preview/ for monitoring. Before you leave a multi-hour job unattended, walk the tuning-time factors below with your tutor and set a deliberate baseline — the bundled example is often longer than Chapter 02 needs.
Walkthrough: what affects tuning time
Wall-clock time is not a mystery number mflux prints before you start. It is almost:
total time ≈ (number of train steps × time per step)
+ (number of mid-run previews × time per preview)
+ checkpoint / plot overhead
With batch_size: 1 (the usual default):
number of train steps ≈ num_epochs × (images in dataset/train)
Example: 50 epochs × 13 images = 650 steps. At ~45 s/step that is already ~8 hours of train steps alone — before counting previews.
You get a trustworthy ETA only after the progress bar has run for a bit. Read tqdm, e.g. 2/650 [… <8:25:28, 46.80s/it]:
| Piece | Meaning |
|---|---|
2/650 | Current step / total steps |
46.80s/it | Seconds per train step right now |
<8:25:28 | Estimated time remaining |
Early steps can be slow (warmup, Metal cache). Re-check after ~15–20 steps. 650 is not required — it is whatever num_epochs × image count the config implies. Chapter 02 wants a working baseline; Chapter 03 sweeps short / medium / long on purpose.
Walk each knob before the long run (or when deciding to kill and restart). Editing train.json does not change a live process — stop, edit, start a new run (new output_path recommended).
1. num_epochs — how many times you press the whole dataset
| What it is | Full passes over every train image. |
| Time | Linear: 2× epochs ≈ 2× train steps. Largest lever. |
| Quality tradeoff | Too few → underfit (“some dog,” not yours). Too many on a small set → overfit (photocopy of train photos / glued backgrounds). |
| Chapter 02 default | Stock example often uses 50 → multi-hour. Prefer 10–20 for a first baseline (e.g. 15 × 13 images ≈ 195 steps). |
2. Dataset size (how many images in train/)
| What it is | Count of captioned images the loop visits each epoch. |
| Time | Linear with steps: more images × same epochs = more steps. |
| Quality tradeoff | ~15–20 varied shots is the course target. Cutting to 5 “to go faster” usually hurts identity more than it helps the clock. Prefer fewer epochs, not a starved set. |
3. max_resolution — training-time pixel cap
| What it is | Upper bound on resolution used while training (how large each image is when the trainer sees it). Not generate CLI --width/--height, and not preview pixel size. |
| Time | Higher caps cost more compute and memory per step. Training 512px data with max_resolution: 1024 does not invent 1024 detail from the files; it can still leave you on a heavier path. |
| Quality tradeoff | Match the train set you prepared. Course baseline: 512×512 images → "max_resolution": 512. Raise later only if you re-export a larger train/ set. |
4. Mid-run previews (monitoring.generate_image_frequency, preview size)
| What it is | Every N steps the trainer runs a full sample generate and writes preview/*_preview_image_*.png so you can see whether the subject is appearing. Prompt often comes from optional dataset/train/preview.txt. |
| Time | Each preview can cost on the order of a minute (similar to your smoke-test generate). Frequency 15 on a 650-step run → dozens of samples → easily an hour+ of pure preview tax. preview_width / preview_height also scale that cost. |
| Quality tradeoff | Previews do not train the adapter; they only monitor. For a speed-first baseline use something like 45–50, and smaller previews (e.g. 512×512). Keep learning checkpoints frequent if you want resume safety without paying for images every time. |
5. Checkpoints (checkpoint.save_frequency)
| What it is | How often a full training-state zip is written under checkpoints/. |
| Time | Usually a smaller overhead than previews (disk write vs full generate). Still not free if set very low (every step). |
| Quality tradeoff | Does not change the learned subject. Smaller frequency → more zips, safer resume if the machine dies. Example: 15 is a reasonable middle ground. |
6. Loss plots (monitoring.plot_frequency)
| What it is | How often loss/loss.html updates. |
| Time | Minor compared to steps and previews. |
| Quality tradeoff | None on the adapter. Bump from 1 to 5 if you want less chatter; keep previews as the visual scoreboard. |
7. low_ram and machine load
| What it is | low_ram: true reduces peak memory (may recompute more). OS power mode, sleep, and other MLX/GPU jobs affect seconds per step. |
| Time | low_ram is often slower — use when you must fit, not for speed. Plugged in, not Low Power Mode; prevent sleep; do not interleave ad-hoc mflux-generate in the same session (documented slowdowns). |
| Quality tradeoff | Memory mode should not change the concept of the run if training completes; thrashing / OOM aborts waste the whole job. |
8. LoRA rank / which layers (usually not first speed knobs)
| What it is | Capacity of the thin patch (rank, which modules in lora_layers). |
| Time | Higher rank can cost a bit per step; effect is usually small next to epochs and previews. |
| Quality tradeoff | Strong quality / overfit lever — Chapter 03 territory. Do not cut rank only to “make it faster” on the first baseline. |
Config map (time vs quality at a glance)
| Knob | Main effect on time | Main effect on result |
|---|---|---|
num_epochs | Dominant (step count) | Underfit ↔ overfit |
Image count in train/ | Step count per epoch | Identity / variety |
max_resolution | Cost per step | Detail ceiling (match your files) |
generate_image_frequency | Preview tax | Monitoring only |
preview_width / height | Cost per preview | Monitoring only |
save_frequency | Zip overhead | Resume safety |
plot_frequency | Small | Loss chart only |
low_ram | Often slower | Fits machine |
| LoRA rank / targets | Mild | Capacity (Ch.03) |
Example: stock long vs intentional short baseline
Stock-shaped (often multi-hour): num_epochs: 50, max_resolution: 1024, preview every 15 steps at 1280×720 → e.g. 50×13 = 650 steps + many full generates.
Chapter 02 speed-oriented baseline (keep model / lora_layers from the package example unless you know better):
"max_resolution": 512,
"training_loop": {
"num_epochs": 15,
"batch_size": 1,
"timestep_low": 4,
"timestep_high": 9
},
"checkpoint": {
"save_frequency": 15,
"output_path": "/Users/you/lora-eyes-open/training-run-01"
},
"monitoring": {
"preview_width": 512,
"preview_height": 512,
"plot_frequency": 5,
"generate_image_frequency": 45
}
Roughly: 15×13 ≈ 195 steps, far fewer previews, resolution matched to the course dataset.
If a long run is already going: Ctrl+C after a useful checkpoint and generate with that zip, or leave overnight. Prefer a new short config (new output_path) if most of an 8-hour ETA is still ahead.
What lands under output_path
Typical layout:
training/ # or whatever you set as checkpoint.output_path
checkpoints/
0000000_checkpoint.zip
0000015_checkpoint.zip
...
preview/
0000000_preview_image_01.png
...
loss/
loss.html
| Path | What it is |
|---|---|
checkpoints/*_checkpoint.zip | Full training state at that step (adapter + optimizer / iterator progress). Use these with --resume. Controlled by checkpoint.save_frequency (e.g. 15 → about every 15 steps). Step 0000000 is usually written near the start — not a trained subject yet. |
preview/*_preview_image_*.png | Mid-run sample generations so you can see whether the subject is appearing, without waiting for the full job. Frequency: monitoring.generate_image_frequency. Prompt often comes from optional dataset/train/preview.txt (a new scene with your trigger token). |
loss/loss.html | Loss curve for monitoring; not a photo of your subject. monitoring.plot_frequency controls how often it updates. |
Open previews in order (0000000 → 0000015 → …). You are watching identity come in (or overfit). Early judgment only — do not thrash hyperparams mid-run.
While it trains
- Watch
checkpoints/fill in; note the step numbers in the zip names. - Open a preview image when one appears — early judgment only; do not thrash hyperparams mid-run.
- Do not interleave ad-hoc generation with training in the same session (documented slowdowns). Prefer train (or resume), then generate.
- Quality may be weak. For this chapter, success is: pipeline ran, checkpoints exist, you can resume if interrupted, and you can judge “looks like my subject.”
- Optional: write predictions for Chapter 03 — few images + high capacity vs long run on a modest set.
Known failure modes
- “None of my LoRAs look good.” Early MLX Dreambooth quality can be poor; adapters may not transfer across tools. Optimize for deltas between runs, not a shippable portrait.
- Interleaving gen + train → slowdowns. Separate the phases.
- Training before smoke test → hours wasted on a broken install. Always verify generate first.
- Resume fails: data path missing. The zip does not include your photos. Keep
dataset/trainand prefer absolute paths in config. - Started with
--configagain after a crash. That starts a new run (new folder / from scratch), not a continuation. Use--resume path/to/*_checkpoint.zip. - Eight-hour ETA on the stock example. Usually epochs × image count and/or frequent previews — not a broken install. Cut
num_epochsand raisegenerate_image_frequencyfor the next baseline; or stop and use a mid checkpoint.
Check your understanding
After the gates (or after you’ve at least completed concept + install + smoke test if training is still running), answer in your own words:
Q4. Why does this lesson insist on a generate smoke test before building the dataset and starting Dreambooth training?
Q5. Why keep photos in raw/ and write 512×512 copies into train/ instead of resizing the only copy of each file?
Q6. Training is configured primarily through a JSON file, not CLI train flags. What practical advantage does that give you when you get to Chapter 03’s experiments?
Q7. In mflux, what is the difference between starting with --config train.json and continuing with --resume …/0000030_checkpoint.zip? Name one thing the checkpoint zip holds that a “final LoRA only” file might not.
Q8. A run shows 2/650 at about 45 s/step, with num_epochs: 50, 13 train images, and a preview every 15 steps. Name two config (or data) changes that would shorten wall-clock time the most, and say briefly why each works. What does max_resolution control in that picture?
Answer key — attempt every question first
Answer key
Q4
Model answer: Long training cannot fix a broken install, download, or Metal/memory setup. A one-image generate proves the stack works before you invest hours and before dataset busywork.
Pass criteria: smoke test proves tooling; avoids wasted long runs / orders verify before train
Q5
Model answer: Preserve originals so you can re-crop, re-resize, or change resolution later without quality loss or re-exporting from phone/camera. Train set is a disposable derivative.
Pass criteria: originals preserved; train set as derivative / reworkable
Q6
Model answer: You can copy the config, change one field (rank, epochs, data path), and keep runs comparable and reproducible — the config is the experiment record.
Pass criteria: reproducibility / easy variants / comparable sweeps (any of these)
Q7
Model answer: --config starts a new training run from the JSON; --resume loads a checkpoint zip and continues that run. The zip includes optimizer state and iterator/step progress (and a config snapshot), not only adapter weights — so you can truly continue rather than re-optimize from scratch. Dataset must still be on disk.
Pass criteria: new run vs continue; optimizer and/or step/iterator state (or full training state) beyond weights alone
Q8
Model answer: (1) Lower num_epochs — total steps ≈ epochs × image count, so this cuts the main train loop linearly. (2) Raise generate_image_frequency (rarer previews) — each preview is a full generate and adds a large tax on a long run. Matching max_resolution to the 512 train set (instead of 1024) can also help per-step cost. max_resolution is the training-time pixel cap (how large images are when the trainer sees them), not generate CLI size and not the quality of previews alone.
Pass criteria: names at least two high-impact levers with correct mechanism (epochs/steps and previews most important); max_resolution described as train-time resolution cap / match-to-data (not “makes images sharper than files”)