Why you merge before you convert
Core AI converts exported PyTorch graphs. A LoRA is extra math beside a frozen weight. Unless Apple documents a runtime adapter API, that extra math is not a Core AI feature. Merge so the personalized weights are stored in the UNet. Export that ordinary graph. The model file is stored on the phone.
Keep the unmerged adapter. Merge is not reversible from the baked file. The adapter is how you retrain or rebuild.
The steps, one box at a time
Reload the exact base revision and the chosen LoRA.
Fuse the adapter into the target component. After this step the pipeline should run without PEFT.
Confirm holdout prompts still match the adapter-attached previews. Then export.
The export tool writes a multi-component Core AI resource folder. That folder is what Xcode will load.
Environment setup
Use the same uv project. You need the training stack plus the official export entry points from apple/coreai-models and coreai-torch.
curl -LsSf https://astral.sh/uv/install.sh | sh
mkdir -p diffusion-lora-coreai && cd diffusion-lora-coreai
uv init --python 3.11
uv venv
uv add torch torchvision diffusers peft transformers accelerate datasets safetensors coreai-torch
uv run python -c "import torch; print(torch.__version__); print('MPS:', torch.backends.mps.is_available())"
uv run coreai.diffusion.export --help
Worked path: merge
Save as merge_lora.py. Merge on CPU so the bake is not tied to MPS. Keep the original adapter directory untouched.
from pathlib import Path
from diffusers import StableDiffusionPipeline
from peft import PeftModel
import torch
BASE = "runwayml/stable-diffusion-v1-5"
REVISION = "pin-this"
ADAPTER = "artifacts/style-lora"
MERGED = Path("artifacts/merged-pipeline")
pipe = StableDiffusionPipeline.from_pretrained(BASE, revision=REVISION, torch_dtype=torch.float32)
pipe.unet = PeftModel.from_pretrained(pipe.unet, ADAPTER)
pipe.unet = pipe.unet.merge_and_unload()
pipe.save_pretrained(MERGED)
print("merged into", MERGED)
uv run python merge_lora.py
# Compare one holdout prompt against artifacts/holdout-lora.png before you export.
Worked path: official diffusion export
The CLI takes a positional model. That can be a registry short-name, a Hugging Face id, or the local merged folder if the installed recipe accepts a path. Confirm with --help and --dry-run. Do not invent a --model flag.
uv run coreai.diffusion.export --help
uv run coreai.diffusion.export sd-1.5 --dry-run
# After merge, point the installed recipe at the ordinary merged pipeline.
uv run coreai.diffusion.export artifacts/merged-pipeline \
--output-dir artifacts/personalized \
--overwrite
# Official short-name form, once the bake is represented the recipe expects:
# uv run coreai.diffusion.export sd-1.5 --output-dir artifacts/personalized --overwrite
Export every component the pipeline needs. For SD 1.x the documented set is text_encoder, unet, vae_decoder, and vae_encoder. A single guessed file is not a diffusion resource.
Worked path: torch.export plus coreai-torch
Use this when you are converting one exported graph the way the clothing-classifier course does. Diffusion still has several graphs. You will repeat the pattern per component the recipe names.
import torch
from coreai_torch import TorchConverter, get_decomp_table
model = load_merged_component().eval()
example = (cpu_example_tensor,)
ep = torch.export.export(model, args=example)
ep = ep.run_decompositions(get_decomp_table())
program = TorchConverter().add_exported_program(ep).to_coreai()
program.optimize()
program.save("artifacts/personalized/unet.aimodel")
Failure modes
- Exporting the unmerged PEFT model. Converter graph capture will see adapter modules Core AI did not say it would keep.
- Overwriting the adapter directory with the merged pipeline. You lose the only file you can reverse.
- Inventing CLI flags. Run
--help. The model argument is positional. - Flattening the output into one file. Keep the multi-component folder the recipe wrote.
- Skipping the merged holdout check. A bad fuse still exports. You will only see it on device.
Done when
artifacts/merged-pipelineloads without PEFT and matches the adapter holdout closely.artifacts/style-lorastill exists.- The export command was the installed
coreai.diffusion.exportor a documentedtorch.exportpluscoreai-torchconversion. - The output is a resource folder with the components the recipe named, plus checksums you recorded.
Keep learning
PyTorch to Core AI in Xcode · From the metal to the model · Core AI models, typed · Model architectures in plain English · LLM LoRA for your writing style
Key concepts
- Bake means
merge_and_unload()into the UNet, then export one ordinary graph. - Keep the unmerged adapter. Merge is not reversible from the baked file.
- Export with
coreai.diffusion.exportortorch.exportpluscoreai-torch. - SD 1.x needs a multi-component folder: text encoder, UNet, and VAE parts.
Takeaways
- Merge on CPU. Do not overwrite the adapter directory.
- Confirm flags with
--helpand--dry-run. Do not invent a--modelflag. - Exporting an unmerged PEFT wrapper will fail graph capture.