Merge remote-tracking branch 'origin/diffusion-lora-training' into diffusion-lora-training-api

This commit is contained in:
Daniel Han 2026-07-02 09:56:56 +00:00
commit c5a0ad59cf
2 changed files with 71 additions and 0 deletions

View file

@ -28,6 +28,7 @@ import json
import math
import os
import random
import re
import time
from dataclasses import dataclass, field, replace
from pathlib import Path
@ -42,6 +43,18 @@ _CAPTION_EXTS = (".txt", ".caption")
# diffusers' canonical single-file LoRA name, so load_lora_weights(dir) finds it.
DEFAULT_LORA_FILENAME = "pytorch_lora_weights.safetensors"
# Families Studio can LOAD but not train (DiT architectures). A base-model name that
# clearly belongs to one is refused in normalized(), so a wrong pick fails at start
# (an instant HTTP 400 through the API) instead of minutes later inside
# StableDiffusionXLPipeline.from_pretrained. Single tokens match on word boundaries;
# hyphenated markers match as substrings of the hyphen-condensed name.
_NON_SDXL_TOKENS = frozenset({"flux", "sd3", "kontext", "pixart", "sana", "lumina", "cogview"})
_NON_SDXL_PHRASES = ("qwen-image", "z-image", "stable-diffusion-3", "hunyuan-dit")
_ONLY_SDXL_HINT = (
"Only SDXL bases can be trained right now (e.g. stabilityai/stable-diffusion-xl-base-1.0 "
"or stabilityai/sdxl-turbo). Other families can load LoRAs but not train them yet."
)
EventCb = Callable[[dict[str, Any]], None]
# Returns a falsy value to keep training, or a truthy stop signal: bare True, or a dict
# that may carry ``save=False`` to cancel without saving a partial adapter.
@ -89,6 +102,7 @@ class DiffusionLoraConfig:
Also coerces values that arrive as strings/blanks through the Studio config path
(``learning_rate`` is preserved as a string there; ``hf_token`` defaults to "")."""
assert_trainable_base_model(self.base_model)
if self.train_steps < 1:
raise ValueError("train_steps must be >= 1")
if self.train_batch_size < 1:
@ -276,6 +290,32 @@ def _encode_sdxl_prompts(
return prompt_embeds, pooled
def assert_trainable_base_model(base_model: str) -> None:
"""Refuse base models that are recognisably not SDXL, before anything is downloaded.
Purely name-based: a GGUF filename or a known DiT-family name (FLUX / Qwen-Image /
Z-Image / SD3 / ...) can never train on the SDXL U-Net trainer, so failing here turns
a confusing mid-run crash into an immediate, actionable error. Names this cannot
classify pass through; from_pretrained still fails cleanly on a genuinely wrong pick."""
name = str(base_model or "").strip().lower()
if name.endswith(".gguf"):
raise ValueError(
f"'{base_model}' is a GGUF checkpoint, which can't be trained. {_ONLY_SDXL_HINT}"
)
condensed = re.sub(r"[^a-z0-9]+", "-", name)
hit = next(
(p for p in _NON_SDXL_PHRASES if p in condensed),
None,
) or next(
(t for t in condensed.split("-") if t in _NON_SDXL_TOKENS),
None,
)
if hit:
raise ValueError(
f"'{base_model}' looks like a {hit} model, which isn't trainable. {_ONLY_SDXL_HINT}"
)
def _assert_trusted_base_model(base_model: str) -> None:
"""Gate the training base model the same way the inference backend gates non-GGUF loads:
a local path or a trusted repo (``unsloth/*`` or an allowlisted official base). This runs

View file

@ -193,3 +193,34 @@ def test_config_rejects_nonpositive_learning_rate():
DiffusionLoraConfig(
base_model = "b", data_dir = "d", output_dir = "o", learning_rate = 0
).normalized()
def test_config_rejects_known_non_sdxl_base_models():
# Known DiT families and GGUF checkpoints must fail at normalise time (an instant
# 400 via the API) instead of minutes later inside from_pretrained.
for bad in (
"unsloth/FLUX.1-dev-GGUF",
"black-forest-labs/FLUX.1-schnell",
"unsloth/Qwen-Image-2512-unsloth-bnb-4bit",
"Tongyi-MAI/Z-Image-Turbo",
"stabilityai/stable-diffusion-3-medium",
"unsloth/FLUX.1-Kontext-dev",
"z-image-turbo-Q4_K_M.gguf",
):
with pytest.raises(ValueError, match = "SDXL"):
DiffusionLoraConfig(
base_model = bad, data_dir = "d", output_dir = "o"
).normalized()
def test_config_accepts_sdxl_and_unknown_base_models():
# SDXL names and unclassifiable custom names/paths must pass the guard (a wrong
# custom pick still fails cleanly in from_pretrained).
for ok in (
"stabilityai/stable-diffusion-xl-base-1.0",
"stabilityai/sdxl-turbo",
"/data/checkpoints/my-custom-sdxl",
"my-finetune",
):
cfg = DiffusionLoraConfig(base_model = ok, data_dir = "d", output_dir = "o").normalized()
assert cfg.base_model == ok