Trainer Integration Skill
Use this workflow when adding LoRA training support for generative models available in the Draw Things app / CLI or when tightening an existing trainer path. Follow Flux1 first, then copy only the model-specific pieces you actually need.
Goal
Add a new trainer path that:
- compiles with
DrawThingsCLI
- writes real LoRA checkpoints
- keeps loss finite
- survives a real 100 to 500 step run
- reproduces an obvious visual shift during generation
Primary Files
Libraries/Trainer/Sources/LoRATrainer.swift
Libraries/Trainer/Sources/LoRATrainerCheckpoint.swift
Libraries/SwiftDiffusion/Sources/Models/<Model>.swift
Apps/DrawThingsCLI/DrawThingsCLI.swift
Apps/DrawThings/Sources/LoRA/LoRATrainingWorkflow.swift
Use these references first:
trainFlux1(...) in LoRATrainer.swift
LoRAFlux1 and LoRAFlux1Fixed in Flux1.swift
Integration Checklist
1. Add the LoRA model builders
- Add
LoRA<Model> and, if needed, LoRA<Model>Fixed.
- Keep changes local to the LoRA path whenever possible.
- Do not rewrite the base runtime path unless training forces it.
- Do not assume trainer-side
LoRANetworkConfiguration is enough. The LoRA builders themselves may need changes to:
- the outer
Model(..., trainable:) setting
- inner
Model(..., trainable:) boundaries
- gradient-checkpoint wiring on the relevant block builders
- Follow
Flux1 exactly here:
- keep the outer LoRA model
trainable: false
- thread gradient-checkpoint flags through the LoRA-specific builders, not just the trainer call site
- if a nested
Model wrapper needs an explicit trainable: value to preserve the intended trainable surface, do that in the LoRA path instead of changing the base model path
- Remember the rule: if a parent
Model is trainable: false, submodules are effectively non-trainable unless the graph structure explicitly reintroduces the LoRA trainable surface the same way the working reference path does.
- If the top-level runtime model is also wrapped by a LoRA builder, keep that wrapper
trainable: false and rely on the individual LoRA layers for trainability. Accidentally making the parent trainable turns the path into a full fine-tune.
- Mirror Flux1-style
LoRANetworkConfiguration usage and checkpointing flags.
2. Add trainer dispatch
- Add a
train<Model>(...) entry in LoRATrainer.swift.
- Dispatch to it from the main trainer switch.
- Add the matching
version handling in CLI and app workflow code.
3. Wire trainable keys in both entry points
- Add trainable-key helpers in
DrawThingsCLI.swift.
- Mirror the same version handling in
LoRATrainingWorkflow.swift.
- Do not stop after CLI only; the app workflow needs the same version-specific key selection.
4. Wire tokenizers cleanly
- Extend
LoRATrainingDependency if the model needs a tokenizer that is not already injected.
- Keep the dependency factory-based, like the existing tokenizer fields.
- Mirror the version-specific tokenizer stack in both CLI and app workflow code.
Small but important example:
Qwen Image should use the Qwen 2.5 tokenizer, not Qwen 3.
Z Image uses Qwen 3.
5. Mirror the fixed encoder path, but keep it trainer-friendly
- Start from the runtime fixed path, but do not copy disk-cache shortcuts from
UNetFixedEncoder.swift.
- Treat
UNetFixedEncoder as a legacy-named integration boundary for the main diffusion model / DiT path, not as a literal UNet requirement.
- Run the fixed model directly in the trainer.
- Prefer batched fixed inference over per-sample loops when the fixed builder supports it.
- Feed fixed outputs back into the main trainable graph as
graph.constant(...).
- Avoid per-sample
toCPU()/toGPU() rebuilds when a batched constant path works.
- Flush partial batches. If fixed inference batches at a fixed size, make the target batch size shrink near the end of training so the tail samples are actually trained.
- Preserve output dtypes exactly:
- context stays in the model float type
- AdaLN chunks stay
Float
- shift/scale stays in the model float type
- do not force every fixed condition to
Float just because one condition needs it
6. Match weight loading exactly
- Use the same model key and codec list that the runtime path needs.
- Mapping is not used when loading trainer weights; mapping matters for import, not for
read(...).
- If the runtime model needs
.i8x, the trainer needs .i8x too.
This was the difference between immediate nan and a finite first step for Qwen Image.
7. Handle rotary the training-safe way
- If
cmul backward cannot reduce broadcast semantics, fully expand rotary on the training path.
- Keep the expanded rotary parity-preserving.
- Do not trust
[1, seq, 1, dim] rotary broadcasting into [batch, seq, heads, dim] just because forward compiles. The backward path can still be wrong or unstable.
- If memory matters, cache compact one-head rotary constants, then expand them to the actual query/key head count before entering the trainable graph.
- Prefer slicing query rotary from the shared
rot tensor instead of inventing a separate query-rotary input when the slice is enough.
- Do not let training-only rotary plumbing leak into the normal inference interface if you can avoid it.
8. Choose scaler and attention backend intentionally
- Pick the initial
GradScaler from the model's numeric contract, not by trial-and-error lowering.
- If the model has no internal residual/projection scaling that already shrinks gradients, start from the high healthy scale used by the stable trainers, usually
32_768.
- Never accept a scaler below
1 as a fix. That usually masks overflow and can make the trainer learn too slowly or not at all.
- When validating attention backends, first prove a known-stable fallback can train, then run the intended backend for the full validation ladder.
- Do not accidentally force the fallback path in trainer code. Use the model-appropriate default such as
valueOr(.scale1) when the configured backend is supposed to participate in training.
9. Keep the fixed conditions simple
- Use
graph.constant(...) for precomputed fixed conditions.
- Use
.copied() when slicing batched fixed outputs back into per-sample constants.
- Do a dry-run forward before the first real step to allocate the largest graph state up front:
let _ = dit((width: latentsWidth, height: latentsHeight), inputs: latents, cArr)
10. Update checkpoint export
- Add the new version branch in
LoRATrainerCheckpoint.swift.
- Point it at the correct model key, usually
dit.
- Confirm the saved LoRA file is real:
- nontrivial size
__up__ tensors present and nonempty
Model-Specific Lessons
Z Image
- Follow the Flux1 trainer pattern closely.
- Keep changes concentrated in
LoRAZImage, LoRAZImageFixed, and trainZImage(...).
- Do not add broad changes to base
ZImage / ZImageFixed unless training truly needs them.
- Use the shared
rot path; slice from it instead of carrying a second query-rotary input.
- Training currently uses fully expanded rotary because backward needs it.
- Keep
x_pad_token on the GPU if you cache it for trainer constants.
- Current healthy trainer scale is
1024.
Qwen Image
- Use the Qwen 2.5 tokenizer.
- Use the real training token length, not a padded constant everywhere.
- Batched
encodeQwenFixed(...) is better than the old per-sample CPU/GPU rebuild path.
- Feed fixed outputs back as constants.
- Keep
.i8x in both fixed and main trainer reads when the checkpoint needs it.
isBF16 on the fixed side is mostly about scaling math; do not assume the whole fixed contract becomes BF16.
- The main LoRA model still owns the explicit BF16 conversion path.
First Debug Pass
If a new trainer is broken, check these in order:
- 1-step run: finite loss?
- checkpoint written?
- checkpoint nonempty?
lora_up tensors nonzero?
- fixed conditions fed as constants, not variables?
- tokenizer stack correct?
- runtime and trainer codec lists match?
- rotary fully expanded only where backward needs it?
- scaler chosen from the model's numeric contract, not lowered below
1 to hide instability?
- optional attention backend selected intentionally, not accidentally through a global default?
Failure Patterns
Immediate nan at step 0
- Wrong codec list, especially missing
.i8x
- Wrong tokenizer or token length
- Broken fixed-condition path
- Wrong dtype assumptions on BF16 models
lora_up stays zero almost everywhere
- Gradient is being cut before most LoRA layers
- Check the trainable surface first
- Then check the fixed-condition boundary and backward-only branches
Dynamic scale steadily collapses
- Treat this as a numerical stability issue first, especially when validating a new attention backend.
- Do not lower the initial scale below
1; fix the overflow source instead.
- Compare against the known-stable attention mode before changing optimizer hyperparameters.
- If only the experimental attention mode collapses, suspect backward precision, scaling, or gradient staging rather than the dataset.
- If collapse disappears after expanding rotary to full heads, the root cause was shape/broadcast semantics in backward, not optimizer settings.
Low timestep loss is higher than mid/high timestep loss
- This is not automatically a bug for flow-style objectives.
- If the training target includes a full noise or velocity term that is only weakly present in the low-timestep input, low timestep bins can have higher irreducible error.
- Compare like-for-like timestep bins over time instead of expecting low timesteps to be easiest.
Generation ignores the LoRA
- The LoRA may be loading with the wrong version
- Use explicit
loras[].version in CLI validation
Validation
After code integration, validate with the $train-lora workflow:
bazel build --compilation_mode=opt //Apps:DrawThingsCLI
- 1-step smoke test
- 20-step stability probe
- 100-step run
- 500-step run on the intended attention backend
- base vs LoRA generation comparison
Do not call the trainer integrated until it survives the full ladder.
1---2name: trainer-integration3description: Add or tighten Draw Things LoRA trainer support for generative models available in the Draw Things app / CLI, covering LoRA builders, trainer dispatch, tokenizer and fixed-encoder wiring, checkpoint export, numerical debugging, and validation.4---56# Trainer Integration Skill78Use this workflow when adding LoRA training support for generative models available in the Draw Things app / CLI or when tightening an existing trainer path. Follow `Flux1` first, then copy only the model-specific pieces you actually need.910## Goal1112Add a new trainer path that:1314- compiles with `DrawThingsCLI`15- writes real LoRA checkpoints16- keeps loss finite17- survives a real 100 to 500 step run18- reproduces an obvious visual shift during generation1920## Primary Files2122- `Libraries/Trainer/Sources/LoRATrainer.swift`23- `Libraries/Trainer/Sources/LoRATrainerCheckpoint.swift`24- `Libraries/SwiftDiffusion/Sources/Models/<Model>.swift`25- `Apps/DrawThingsCLI/DrawThingsCLI.swift`26- `Apps/DrawThings/Sources/LoRA/LoRATrainingWorkflow.swift`2728Use these references first:2930- `trainFlux1(...)` in `LoRATrainer.swift`31- `LoRAFlux1` and `LoRAFlux1Fixed` in `Flux1.swift`3233## Integration Checklist3435### 1. Add the LoRA model builders3637- Add `LoRA<Model>` and, if needed, `LoRA<Model>Fixed`.38- Keep changes local to the LoRA path whenever possible.39- Do not rewrite the base runtime path unless training forces it.40- Do not assume trainer-side `LoRANetworkConfiguration` is enough. The LoRA builders themselves may need changes to:41 - the outer `Model(..., trainable:)` setting42 - inner `Model(..., trainable:)` boundaries43 - gradient-checkpoint wiring on the relevant block builders44- Follow `Flux1` exactly here:45 - keep the outer LoRA model `trainable: false`46 - thread gradient-checkpoint flags through the LoRA-specific builders, not just the trainer call site47 - if a nested `Model` wrapper needs an explicit `trainable:` value to preserve the intended trainable surface, do that in the LoRA path instead of changing the base model path48- Remember the rule: if a parent `Model` is `trainable: false`, submodules are effectively non-trainable unless the graph structure explicitly reintroduces the LoRA trainable surface the same way the working reference path does.49- If the top-level runtime model is also wrapped by a LoRA builder, keep that wrapper `trainable: false` and rely on the individual LoRA layers for trainability. Accidentally making the parent trainable turns the path into a full fine-tune.50- Mirror Flux1-style `LoRANetworkConfiguration` usage and checkpointing flags.5152### 2. Add trainer dispatch5354- Add a `train<Model>(...)` entry in `LoRATrainer.swift`.55- Dispatch to it from the main trainer switch.56- Add the matching `version` handling in CLI and app workflow code.5758### 3. Wire trainable keys in both entry points5960- Add trainable-key helpers in `DrawThingsCLI.swift`.61- Mirror the same version handling in `LoRATrainingWorkflow.swift`.62- Do not stop after CLI only; the app workflow needs the same version-specific key selection.6364### 4. Wire tokenizers cleanly6566- Extend `LoRATrainingDependency` if the model needs a tokenizer that is not already injected.67- Keep the dependency factory-based, like the existing tokenizer fields.68- Mirror the version-specific tokenizer stack in both CLI and app workflow code.6970Small but important example:7172- `Qwen Image` should use the Qwen 2.5 tokenizer, not Qwen 3.73- `Z Image` uses Qwen 3.7475### 5. Mirror the fixed encoder path, but keep it trainer-friendly7677- Start from the runtime fixed path, but do not copy disk-cache shortcuts from `UNetFixedEncoder.swift`.78- Treat `UNetFixedEncoder` as a legacy-named integration boundary for the main diffusion model / DiT path, not as a literal UNet requirement.79- Run the fixed model directly in the trainer.80- Prefer batched fixed inference over per-sample loops when the fixed builder supports it.81- Feed fixed outputs back into the main trainable graph as `graph.constant(...)`.82- Avoid per-sample `toCPU()/toGPU()` rebuilds when a batched constant path works.83- Flush partial batches. If fixed inference batches at a fixed size, make the target batch size shrink near the end of training so the tail samples are actually trained.84- Preserve output dtypes exactly:85 - context stays in the model float type86 - AdaLN chunks stay `Float`87 - shift/scale stays in the model float type88 - do not force every fixed condition to `Float` just because one condition needs it8990### 6. Match weight loading exactly9192- Use the same model key and codec list that the runtime path needs.93- Mapping is not used when loading trainer weights; mapping matters for import, not for `read(...)`.94- If the runtime model needs `.i8x`, the trainer needs `.i8x` too.9596This was the difference between immediate `nan` and a finite first step for Qwen Image.9798### 7. Handle rotary the training-safe way99100- If `cmul` backward cannot reduce broadcast semantics, fully expand rotary on the training path.101- Keep the expanded rotary parity-preserving.102- Do not trust `[1, seq, 1, dim]` rotary broadcasting into `[batch, seq, heads, dim]` just because forward compiles. The backward path can still be wrong or unstable.103- If memory matters, cache compact one-head rotary constants, then expand them to the actual query/key head count before entering the trainable graph.104- Prefer slicing query rotary from the shared `rot` tensor instead of inventing a separate query-rotary input when the slice is enough.105- Do not let training-only rotary plumbing leak into the normal inference interface if you can avoid it.106107### 8. Choose scaler and attention backend intentionally108109- Pick the initial `GradScaler` from the model's numeric contract, not by trial-and-error lowering.110- If the model has no internal residual/projection scaling that already shrinks gradients, start from the high healthy scale used by the stable trainers, usually `32_768`.111- Never accept a scaler below `1` as a fix. That usually masks overflow and can make the trainer learn too slowly or not at all.112- When validating attention backends, first prove a known-stable fallback can train, then run the intended backend for the full validation ladder.113- Do not accidentally force the fallback path in trainer code. Use the model-appropriate default such as `valueOr(.scale1)` when the configured backend is supposed to participate in training.114115### 9. Keep the fixed conditions simple116117- Use `graph.constant(...)` for precomputed fixed conditions.118- Use `.copied()` when slicing batched fixed outputs back into per-sample constants.119- Do a dry-run forward before the first real step to allocate the largest graph state up front:120121```swift122let _ = dit((width: latentsWidth, height: latentsHeight), inputs: latents, cArr)123```124125### 10. Update checkpoint export126127- Add the new version branch in `LoRATrainerCheckpoint.swift`.128- Point it at the correct model key, usually `dit`.129- Confirm the saved LoRA file is real:130 - nontrivial size131 - `__up__` tensors present and nonempty132133## Model-Specific Lessons134135### Z Image136137- Follow the Flux1 trainer pattern closely.138- Keep changes concentrated in `LoRAZImage`, `LoRAZImageFixed`, and `trainZImage(...)`.139- Do not add broad changes to base `ZImage` / `ZImageFixed` unless training truly needs them.140- Use the shared `rot` path; slice from it instead of carrying a second query-rotary input.141- Training currently uses fully expanded rotary because backward needs it.142- Keep `x_pad_token` on the GPU if you cache it for trainer constants.143- Current healthy trainer scale is `1024`.144145### Qwen Image146147- Use the Qwen 2.5 tokenizer.148- Use the real training token length, not a padded constant everywhere.149- Batched `encodeQwenFixed(...)` is better than the old per-sample CPU/GPU rebuild path.150- Feed fixed outputs back as constants.151- Keep `.i8x` in both fixed and main trainer reads when the checkpoint needs it.152- `isBF16` on the fixed side is mostly about scaling math; do not assume the whole fixed contract becomes BF16.153- The main LoRA model still owns the explicit BF16 conversion path.154155## First Debug Pass156157If a new trainer is broken, check these in order:1581591. 1-step run: finite loss?1602. checkpoint written?1613. checkpoint nonempty?1624. `lora_up` tensors nonzero?1635. fixed conditions fed as constants, not variables?1646. tokenizer stack correct?1657. runtime and trainer codec lists match?1668. rotary fully expanded only where backward needs it?1679. scaler chosen from the model's numeric contract, not lowered below `1` to hide instability?16810. optional attention backend selected intentionally, not accidentally through a global default?169170## Failure Patterns171172### Immediate `nan` at step 0173174- Wrong codec list, especially missing `.i8x`175- Wrong tokenizer or token length176- Broken fixed-condition path177- Wrong dtype assumptions on BF16 models178179### `lora_up` stays zero almost everywhere180181- Gradient is being cut before most LoRA layers182- Check the trainable surface first183- Then check the fixed-condition boundary and backward-only branches184185### Dynamic scale steadily collapses186187- Treat this as a numerical stability issue first, especially when validating a new attention backend.188- Do not lower the initial scale below `1`; fix the overflow source instead.189- Compare against the known-stable attention mode before changing optimizer hyperparameters.190- If only the experimental attention mode collapses, suspect backward precision, scaling, or gradient staging rather than the dataset.191- If collapse disappears after expanding rotary to full heads, the root cause was shape/broadcast semantics in backward, not optimizer settings.192193### Low timestep loss is higher than mid/high timestep loss194195- This is not automatically a bug for flow-style objectives.196- If the training target includes a full noise or velocity term that is only weakly present in the low-timestep input, low timestep bins can have higher irreducible error.197- Compare like-for-like timestep bins over time instead of expecting low timesteps to be easiest.198199### Generation ignores the LoRA200201- The LoRA may be loading with the wrong version202- Use explicit `loras[].version` in CLI validation203204## Validation205206After code integration, validate with the `$train-lora` workflow:2072081. `bazel build --compilation_mode=opt //Apps:DrawThingsCLI`2092. 1-step smoke test2103. 20-step stability probe2114. 100-step run2125. 500-step run on the intended attention backend2136. base vs LoRA generation comparison214215Do not call the trainer integrated until it survives the full ladder.