Add Ideogram 4 family, structured HunyuanImage exclusion, curated Krea 2 LoRAs
Ideogram 4 (diffusers 0.39 Ideogram4Pipeline) as a new image family. The vendor publishes no bf16 checkpoint, so ideogram-ai/ideogram-4-fp8 (raw float8 DiTs, upcast by from_pretrained) is the family base and ideogram-4-nf4-diffusers is the bnb-4bit pipeline artifact (ideogram-4-nf4 is byte-identical and detects to the same family). All three repos join the trusted non-GGUF allowlist and the frontend safetensors catalog. Family specifics handled: - Dual-branch CFG runs through a SEPARATE unconditional_transformer, so the auto-policy size table entry counts two ~9.3B DiTs (37.2 GB bf16), and the pipeline-kind memory plan now takes max(cached bytes, family table) for the family base repo: the fp8 repo's cached bytes undershoot the bf16-resident footprint by ~2x, which would let auto planning pick a resident placement that OOMs. - The pipeline accepts EITHER guidance_scale OR a per-step guidance_schedule (its default: the recommended 45x7.0 + 3x3.0 taper, valid only at 48 steps) and raises when both are set. At the advertised defaults (48 steps, guidance 7) generate() drops the constant so the recommended taper engages; any other request nulls the schedule so the constant broadcasts legally. - Generation defaults per the model card: 48 steps, guidance 7 (both tables). tencent/HunyuanImage-3.0 is deliberately excluded: it has no diffusers pipeline (an 80B autoregressive MoE behind trust_remote_code). A structured exclusion map now surfaces that reason verbatim from validate_load_request instead of the generic unknown-family error. The curated diffusion LoRA catalog gains the nine official krea/Krea-2-LoRA-* style adapters (family-tagged krea-2, explicit weight filenames), so they show up in the picker instead of requiring a typed repo id. Tests: new test_diffusion_more_families.py (detection, trust, defaults, size table, exclusion reason, curated catalog + family filter), two generate() tests for the guidance_scale/guidance_schedule pairing, and the local-scan LoRA test updated for a non-empty curated list. Backend suite + CI-sim (block_diffusers/block_torchao) green; frontend builds.
This commit is contained in:
parent
04396ec507
commit
cbfc43215d
8 changed files with 304 additions and 8 deletions
|
|
@ -31,9 +31,11 @@ from utils.hardware import clear_gpu_cache
|
|||
from .diffusion_families import (
|
||||
DIFFUSION_CANCELLED_MSG,
|
||||
DIFFUSION_NOT_LOADED_MSG,
|
||||
IDEOGRAM4_FAMILY_NAME,
|
||||
DiffusionFamily,
|
||||
default_generation_params,
|
||||
detect_family_for_pick,
|
||||
excluded_model_reason,
|
||||
resolve_base_repo,
|
||||
resolve_local_gguf_child,
|
||||
supported_family_names,
|
||||
|
|
@ -84,7 +86,11 @@ from .diffusion_prequant import (
|
|||
load_prequantized_transformer,
|
||||
resolve_prequant_source,
|
||||
)
|
||||
from .diffusion_auto_policy import build_resolved_record, resolve_dense_quant_candidate
|
||||
from .diffusion_auto_policy import (
|
||||
build_resolved_record,
|
||||
family_bf16_components_gb,
|
||||
resolve_dense_quant_candidate,
|
||||
)
|
||||
from .diffusion_transformer_quant import (
|
||||
TQ_AUTO,
|
||||
DEFAULT_MIN_LINEAR_FEATURES,
|
||||
|
|
@ -221,6 +227,13 @@ _TRUSTED_NON_GGUF_REPOS = frozenset(
|
|||
# training LoRAs on (train on Raw, run adapters on Turbo).
|
||||
"krea/krea-2-turbo",
|
||||
"krea/krea-2-raw",
|
||||
# Ideogram 4: official vendor repos, safetensors-only diffusers pipelines, no
|
||||
# remote code. The vendor ships no bf16 checkpoint: -fp8 stores the two DiTs
|
||||
# as raw float8 (highest precision available, the family base); the two nf4
|
||||
# repos are identical bnb-4bit exports (both listed so either id loads).
|
||||
"ideogram-ai/ideogram-4-fp8",
|
||||
"ideogram-ai/ideogram-4-nf4",
|
||||
"ideogram-ai/ideogram-4-nf4-diffusers",
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -508,6 +521,12 @@ class DiffusionBackend:
|
|||
kind = resolve_model_kind(gguf_filename, model_kind)
|
||||
fam = detect_family_for_pick(repo_id, gguf_filename, family_override)
|
||||
if fam is None:
|
||||
# A deliberately-excluded model gets its stated reason, not the generic
|
||||
# unknown-family message (which reads like a detection gap and invites a
|
||||
# family_override retry that would fail deeper and less clearly).
|
||||
excluded = excluded_model_reason(repo_id)
|
||||
if excluded:
|
||||
raise ValueError(f"'{repo_id}' cannot be loaded: {excluded}")
|
||||
raise ValueError(
|
||||
f"'{repo_id}' is not a supported diffusion image model. Supported families: "
|
||||
f"{', '.join(supported_family_names())}. If this is a variant of one of them, "
|
||||
|
|
@ -1590,6 +1609,19 @@ class DiffusionBackend:
|
|||
cached = self._cache_bytes(repo_id) if repo_id else 0
|
||||
cached_mib = int(cached // (1024 * 1024)) if cached else None
|
||||
model_dense_mib = estimate_safetensors_dense_mib(cached_mib)
|
||||
# A repo can store weights in a NARROWER dtype than they occupy after the
|
||||
# loader's torch_dtype cast: ideogram-4's base repo ships its two DiTs as
|
||||
# raw float8, so the cached bytes undershoot the bf16-resident footprint
|
||||
# by ~2x and auto planning would pick a resident placement that OOMs.
|
||||
# When the family size table knows the bf16-resident total for THIS repo
|
||||
# (the family base -- prequant repos like the bnb-4bit exports have
|
||||
# different ids and really do stay compressed), plan against the larger
|
||||
# of the two estimates.
|
||||
if repo_id and repo_id.strip().lower() == fam.base_repo.lower():
|
||||
table = family_bf16_components_gb(fam, fam.base_repo)
|
||||
if table is not None and model_dense_mib is not None:
|
||||
table_mib = int(sum(table) * (1000.0**3) / (1024.0 * 1024.0))
|
||||
model_dense_mib = max(model_dense_mib, table_mib)
|
||||
companion_mib = None
|
||||
else:
|
||||
if transformer_resident_override_mib is not None:
|
||||
|
|
@ -2118,6 +2150,18 @@ class DiffusionBackend:
|
|||
# share this call's seed, drawn sequentially from one generator.
|
||||
"num_images_per_prompt": batch_size,
|
||||
}
|
||||
if state.family.name == IDEOGRAM4_FAMILY_NAME:
|
||||
# Ideogram 4 drives CFG through EITHER a constant guidance_scale OR
|
||||
# a per-step guidance_schedule; its check_inputs rejects the call
|
||||
# when both are set, and the schedule DEFAULTS to the recommended
|
||||
# 45x7.0 + 3x3.0 polish taper (valid only at exactly 48 steps). At
|
||||
# the family's advertised defaults, drop the constant so the
|
||||
# recommended taper engages; any other request nulls the schedule
|
||||
# so the constant broadcasts legally to the chosen step count.
|
||||
if steps == 48 and abs(float(guidance) - 7.0) < 1e-6:
|
||||
kwargs.pop(state.family.cfg_kwarg, None)
|
||||
else:
|
||||
kwargs["guidance_schedule"] = None
|
||||
if init_pil is not None:
|
||||
# Reference with extra images passes the whole list (FLUX.2 combines them);
|
||||
# every other workflow takes the single image.
|
||||
|
|
|
|||
|
|
@ -59,6 +59,12 @@ _FAMILY_BF16_GB: dict[str, tuple[float, float, float]] = {
|
|||
"qwen-image-edit": (40.9, 16.6, 0.3),
|
||||
"z-image": (12.3, 8.0, 0.2),
|
||||
"krea-2": (26.3, 8.9, 0.5),
|
||||
# Two ~9.3B DiTs (the conditional transformer PLUS the separate
|
||||
# unconditional_transformer driving Ideogram's dual-branch CFG), both resident
|
||||
# for every generation, and a Qwen3-VL text encoder. The vendor repo stores the
|
||||
# DiTs as raw float8 (9.29 GB each); these are the bf16-resident sizes after the
|
||||
# loader's dtype cast, per this table's contract.
|
||||
"ideogram-4": (37.2, 8.8, 0.2),
|
||||
}
|
||||
|
||||
# Base-repo overrides for families whose picker offers multiple sizes under one family
|
||||
|
|
|
|||
|
|
@ -304,6 +304,25 @@ _FAMILIES: tuple[DiffusionFamily, ...] = (
|
|||
# unvalidated upstream, so keep the fp16 fallback off like z-image.
|
||||
fp16_incompatible = True,
|
||||
),
|
||||
# Ideogram 4 (diffusers >= 0.39): a 34-layer single-stream flow-matching DiT PAIR --
|
||||
# the conditional transformer plus a separate ``unconditional_transformer`` driving
|
||||
# its dual-branch CFG (both ~9B params, so memory planning must count two DiTs) --
|
||||
# with a Qwen3-VL text encoder. The vendor publishes no bf16 checkpoint:
|
||||
# ideogram-4-fp8 stores the DiTs as raw float8 tensors (from_pretrained upcasts
|
||||
# them to the compute dtype) and is the highest-precision artifact, so it is the
|
||||
# family base; ideogram-4-nf4-diffusers / ideogram-4-nf4 (identical contents)
|
||||
# carry bnb-4bit quantization_configs the pipeline kind re-applies automatically.
|
||||
# All three repos are gated="auto" on the Hub, so a load may need the user's HF
|
||||
# token. No GGUF variant and no sd.cpp mapping, so the no-GPU route falls back to
|
||||
# diffusers. CFG quirk: the pipeline takes EITHER guidance_scale OR a per-step
|
||||
# guidance_schedule (see the loader's IDEOGRAM4 branch in diffusion.py).
|
||||
DiffusionFamily(
|
||||
name = "ideogram-4",
|
||||
pipeline_class = "Ideogram4Pipeline",
|
||||
transformer_class = "Ideogram4Transformer2DModel",
|
||||
base_repo = "ideogram-ai/ideogram-4-fp8",
|
||||
aliases = ("ideogram4", "ideogram-v4", "ideogram"),
|
||||
),
|
||||
# SDXL is the one U-Net family here: the denoiser is ``pipe.unet``
|
||||
# (UNet2DConditionModel), not a DiT ``pipe.transformer``, and a single-file
|
||||
# ``.safetensors`` is the WHOLE pipeline rather than a transformer-only file.
|
||||
|
|
@ -343,6 +362,36 @@ def trainable_family_names() -> tuple[str, ...]:
|
|||
return tuple(fam.name for fam in _FAMILIES if fam.trainable)
|
||||
|
||||
|
||||
# The family whose CFG runs through a guidance_scale/guidance_schedule pair rather
|
||||
# than a plain guidance_scale (the loader special-cases the call, like krea-2's
|
||||
# per-component assembly). Named here so the two modules cannot drift apart.
|
||||
IDEOGRAM4_FAMILY_NAME = "ideogram-4"
|
||||
|
||||
|
||||
# Models Studio deliberately does NOT support, with the reason surfaced verbatim in
|
||||
# the load error (instead of the generic unknown-family message, which reads like a
|
||||
# detection gap). Keyed by a lowercase substring of the repo id. The bar for support
|
||||
# is a diffusers pipeline: HunyuanImage-3.0 is an 80B autoregressive MoE loaded via
|
||||
# AutoModelForCausalLM + trust_remote_code -- there is nothing for this backend to
|
||||
# assemble, and remote-code execution is out of the question for a load path.
|
||||
_EXCLUDED_MODELS: tuple[tuple[str, str], ...] = (
|
||||
(
|
||||
"hunyuanimage",
|
||||
"HunyuanImage-3.0 has no diffusers pipeline (it is an 80B autoregressive MoE "
|
||||
"that requires trust_remote_code), so Studio does not support it.",
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
def excluded_model_reason(repo_id: str) -> Optional[str]:
|
||||
"""The stated reason ``repo_id`` is unsupported, or None when it is simply unknown."""
|
||||
needle = (repo_id or "").lower()
|
||||
for token, reason in _EXCLUDED_MODELS:
|
||||
if token in needle:
|
||||
return reason
|
||||
return None
|
||||
|
||||
|
||||
# Editing / inpaint checkpoints share an arch keyword but need a different
|
||||
# pipeline and an input image, which this text-to-image backend doesn't drive.
|
||||
# "layered" rejects Qwen-Image-Layered: its transformer sets additional_t_cond=True
|
||||
|
|
@ -456,6 +505,10 @@ _GENERATION_DEFAULTS: tuple[tuple[str, int, float], ...] = (
|
|||
("flux.2-klein", 4, 0.0),
|
||||
("qwen-image", 20, 4.0),
|
||||
("z-image", 20, 4.0),
|
||||
# Ideogram 4's model-card settings: 48 steps, guidance 7 (its recommended
|
||||
# schedule tapers the last 3 steps to 3.0 -- the loader keeps that taper when
|
||||
# the request matches these defaults exactly; see the IDEOGRAM4 branch).
|
||||
("ideogram", 48, 7.0),
|
||||
)
|
||||
# Unrecognised model: distilled few-step / no-CFG shape, matching the UI fallback.
|
||||
_GENERATION_DEFAULT_FALLBACK = (9, 0.0)
|
||||
|
|
|
|||
|
|
@ -66,9 +66,36 @@ class ResolvedLora:
|
|||
|
||||
# Curated, family-tagged catalog of known-good diffusion LoRAs. Kept intentionally small
|
||||
# and data-driven; extend as unsloth hosts/curates more. Entries are HF repos with a
|
||||
# single-file weight. (Left minimal on purpose -- local discovery is the primary source,
|
||||
# and users can also reference any public HF LoRA repo id directly.)
|
||||
_CURATED: tuple[LoraCatalogEntry, ...] = ()
|
||||
# single-file weight. (Local discovery remains a primary source, and users can also
|
||||
# reference any public HF LoRA repo id directly.)
|
||||
|
||||
|
||||
def _krea2_lora(style: str, display_name: str) -> LoraCatalogEntry:
|
||||
"""One official krea/Krea-2-LoRA-* style adapter. All nine follow the same repo
|
||||
shape (a single ``{style}.safetensors`` at the root) and are trained on Krea-2-Raw
|
||||
for use on Krea-2-Turbo, per Krea's release guidance."""
|
||||
return LoraCatalogEntry(
|
||||
id = f"krea/Krea-2-LoRA-{style}",
|
||||
display_name = display_name,
|
||||
source = "hub",
|
||||
fmt = "safetensors",
|
||||
families = ("krea-2",),
|
||||
repo_id = f"krea/Krea-2-LoRA-{style}",
|
||||
weight_name = f"{style}.safetensors",
|
||||
)
|
||||
|
||||
|
||||
_CURATED: tuple[LoraCatalogEntry, ...] = (
|
||||
_krea2_lora("retroanime", "Krea 2 Retro Anime"),
|
||||
_krea2_lora("neondrip", "Krea 2 Neon Drip"),
|
||||
_krea2_lora("darkbrush", "Krea 2 Dark Brush"),
|
||||
_krea2_lora("softwatercolor", "Krea 2 Soft Watercolor"),
|
||||
_krea2_lora("dotmatrix", "Krea 2 Dot Matrix"),
|
||||
_krea2_lora("rainywindow", "Krea 2 Rainy Window"),
|
||||
_krea2_lora("vintagetarot", "Krea 2 Vintage Tarot"),
|
||||
_krea2_lora("sunsetblur", "Krea 2 Sunset Blur"),
|
||||
_krea2_lora("kidsdrawing", "Krea 2 Kids Drawing"),
|
||||
)
|
||||
|
||||
|
||||
def loras_dir() -> Path:
|
||||
|
|
|
|||
|
|
@ -383,6 +383,9 @@ def fake_runtime(monkeypatch):
|
|||
diffusers.QwenImageInpaintPipeline = _FakeInpaintPipeline
|
||||
# Instruction-editing pipeline (Qwen-Image-Edit): its own pipeline IS the loaded one.
|
||||
diffusers.QwenImageEditPlusPipeline = _FakePipeline
|
||||
# Ideogram 4, so its guidance_scale/guidance_schedule pairing is exercisable.
|
||||
diffusers.Ideogram4Pipeline = _FakePipeline
|
||||
diffusers.Ideogram4Transformer2DModel = _FakeTransformer
|
||||
# SDXL: a U-Net family. Its single-file checkpoint is the whole pipeline, so the
|
||||
# pipeline class carries from_single_file; UNet2DConditionModel is the denoiser
|
||||
# class (fetched but unused on the pipeline/single-file-pipeline paths).
|
||||
|
|
@ -1223,6 +1226,41 @@ def test_generate_qwen_uses_true_cfg_scale(fake_runtime, tmp_path):
|
|||
assert call["true_cfg_scale"] == 4.0 and call["guidance_scale"] is None
|
||||
|
||||
|
||||
def _load_ideogram(backend, tmp_path):
|
||||
(tmp_path / "model.gguf").write_bytes(b"weights")
|
||||
backend.load_pipeline(
|
||||
str(tmp_path),
|
||||
gguf_filename = "model.gguf",
|
||||
base_repo = "ideogram-ai/ideogram-4-fp8",
|
||||
family_override = "ideogram-4",
|
||||
)
|
||||
|
||||
|
||||
def test_generate_ideogram_defaults_keep_recommended_schedule(fake_runtime, tmp_path):
|
||||
# Ideogram 4's pipeline defaults to its recommended tapered guidance_schedule
|
||||
# (45x7.0 + 3x3.0, valid only at 48 steps) and REJECTS guidance_scale while the
|
||||
# schedule is set. At the family's advertised defaults the backend must drop the
|
||||
# constant so the recommended taper engages.
|
||||
backend = DiffusionBackend()
|
||||
_load_ideogram(backend, tmp_path)
|
||||
backend.generate(prompt = "a sloth", steps = 48, guidance = 7.0)
|
||||
call = backend._state.pipe.last_kwargs
|
||||
assert call["guidance_scale"] is None # not passed: the pipe default engages
|
||||
assert "guidance_schedule" not in call
|
||||
|
||||
|
||||
def test_generate_ideogram_custom_guidance_nulls_schedule(fake_runtime, tmp_path):
|
||||
# Any non-default request must broadcast the constant legally: guidance_scale set
|
||||
# AND guidance_schedule explicitly nulled (the pipeline raises when both are set,
|
||||
# and its default schedule is non-None).
|
||||
backend = DiffusionBackend()
|
||||
_load_ideogram(backend, tmp_path)
|
||||
backend.generate(prompt = "a sloth", steps = 20, guidance = 5.0)
|
||||
call = backend._state.pipe.last_kwargs
|
||||
assert call["guidance_scale"] == 5.0
|
||||
assert "guidance_schedule" in call and call["guidance_schedule"] is None
|
||||
|
||||
|
||||
def test_begin_load_rejects_concurrent(monkeypatch):
|
||||
backend = DiffusionBackend()
|
||||
# The worker resolves the base + downloads, both over the network; stub them
|
||||
|
|
|
|||
|
|
@ -153,10 +153,11 @@ def test_list_loras_scans_local(tmp_path, monkeypatch):
|
|||
(d / "other.gguf").write_bytes(b"y")
|
||||
(d / "ignore.txt").write_bytes(b"z")
|
||||
monkeypatch.setattr(dl, "loras_dir", lambda: d)
|
||||
ids = {e.id for e in dl.list_loras()}
|
||||
assert ids == {"mystyle", "other"}
|
||||
fmts = {e.id: e.fmt for e in dl.list_loras()}
|
||||
assert fmts["other"] == "gguf" and fmts["mystyle"] == "safetensors"
|
||||
# The merged catalog also carries the curated hub entries; the local scan is
|
||||
# exactly the weight files dropped in the directory.
|
||||
local = {e.id: e for e in dl.list_loras() if e.source == "local"}
|
||||
assert set(local) == {"mystyle", "other"}
|
||||
assert local["other"].fmt == "gguf" and local["mystyle"].fmt == "safetensors"
|
||||
|
||||
|
||||
def test_resolve_one_local_and_unknown(tmp_path, monkeypatch):
|
||||
|
|
|
|||
112
studio/backend/tests/test_diffusion_more_families.py
Normal file
112
studio/backend/tests/test_diffusion_more_families.py
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved. See /studio/LICENSE.AGPL-3.0
|
||||
|
||||
"""Ideogram 4 family registration, the HunyuanImage structured exclusion, and the
|
||||
curated krea/Krea-2-LoRA-* catalog entries. Pure-module tests: no torch, no network."""
|
||||
|
||||
import pytest
|
||||
|
||||
from core.inference.diffusion import _is_trusted_diffusion_repo
|
||||
from core.inference.diffusion_auto_policy import family_bf16_components_gb
|
||||
from core.inference.diffusion_families import (
|
||||
IDEOGRAM4_FAMILY_NAME,
|
||||
default_generation_params,
|
||||
detect_family,
|
||||
excluded_model_reason,
|
||||
)
|
||||
from core.inference.diffusion_lora import _CURATED, list_loras
|
||||
|
||||
|
||||
# ── ideogram-4 family detection ──────────────────────────────────────────────
|
||||
@pytest.mark.parametrize(
|
||||
"repo_id",
|
||||
[
|
||||
"ideogram-ai/ideogram-4-fp8",
|
||||
"ideogram-ai/ideogram-4-nf4",
|
||||
"ideogram-ai/ideogram-4-nf4-diffusers",
|
||||
],
|
||||
)
|
||||
def test_detect_family_ideogram4_repos(repo_id):
|
||||
fam = detect_family(repo_id)
|
||||
assert fam is not None and fam.name == IDEOGRAM4_FAMILY_NAME
|
||||
assert fam.pipeline_class == "Ideogram4Pipeline"
|
||||
assert fam.transformer_class == "Ideogram4Transformer2DModel"
|
||||
# The vendor ships no bf16 repo: the raw-float8 export is the family base.
|
||||
assert fam.base_repo == "ideogram-ai/ideogram-4-fp8"
|
||||
|
||||
|
||||
def test_detect_family_ideogram4_override():
|
||||
fam = detect_family("some/local-path", override = "ideogram-4")
|
||||
assert fam is not None and fam.name == IDEOGRAM4_FAMILY_NAME
|
||||
assert detect_family("x", override = "ideogram4").name == IDEOGRAM4_FAMILY_NAME
|
||||
|
||||
|
||||
def test_ideogram4_repos_are_trusted_non_gguf():
|
||||
# The three official vendor pipelines load via from_pretrained, which is gated
|
||||
# to the unsloth org + the explicit allowlist.
|
||||
for rid in (
|
||||
"ideogram-ai/ideogram-4-fp8",
|
||||
"ideogram-ai/ideogram-4-nf4",
|
||||
"ideogram-ai/ideogram-4-nf4-diffusers",
|
||||
):
|
||||
assert _is_trusted_diffusion_repo(rid)
|
||||
assert not _is_trusted_diffusion_repo("ideogram-ai/some-future-repo")
|
||||
|
||||
|
||||
def test_ideogram4_generation_defaults():
|
||||
# Model-card settings: 48 steps, guidance 7 (the backend keeps the pipeline's
|
||||
# recommended tapered schedule when the request matches exactly).
|
||||
assert default_generation_params("ideogram-ai/ideogram-4-fp8") == (48, 7.0)
|
||||
|
||||
|
||||
def test_ideogram4_memory_table_counts_both_dits():
|
||||
fam = detect_family("ideogram-ai/ideogram-4-fp8")
|
||||
components = family_bf16_components_gb(fam)
|
||||
assert components is not None
|
||||
transformer_gb, text_encoders_gb, _vae_gb = components
|
||||
# Two ~9.3B DiTs (conditional + unconditional) at bf16: well above one DiT's
|
||||
# ~18.6 GB. A single-DiT entry here would let auto planning under-reserve and OOM.
|
||||
assert transformer_gb > 30.0
|
||||
assert text_encoders_gb > 5.0
|
||||
|
||||
|
||||
# ── structured exclusions ────────────────────────────────────────────────────
|
||||
def test_hunyuanimage_is_excluded_with_reason():
|
||||
reason = excluded_model_reason("tencent/HunyuanImage-3.0")
|
||||
assert reason is not None and "diffusers" in reason
|
||||
# Not detectable as any family: the exclusion reason is the load error surface.
|
||||
assert detect_family("tencent/HunyuanImage-3.0") is None
|
||||
|
||||
|
||||
def test_excluded_model_reason_none_for_supported_and_unknown():
|
||||
assert excluded_model_reason("unsloth/Z-Image-Turbo-GGUF") is None
|
||||
assert excluded_model_reason("someorg/some-model") is None
|
||||
|
||||
|
||||
def test_validate_load_request_surfaces_exclusion_reason():
|
||||
from core.inference.diffusion import DiffusionBackend
|
||||
|
||||
backend = DiffusionBackend()
|
||||
with pytest.raises(ValueError, match = "trust_remote_code"):
|
||||
backend.validate_load_request("tencent/HunyuanImage-3.0")
|
||||
|
||||
|
||||
# ── curated krea LoRA catalog ────────────────────────────────────────────────
|
||||
def test_curated_krea2_loras_present_and_well_formed():
|
||||
krea = [e for e in _CURATED if e.repo_id and e.repo_id.startswith("krea/Krea-2-LoRA-")]
|
||||
assert len(krea) == 9
|
||||
for entry in krea:
|
||||
assert entry.source == "hub" and entry.fmt == "safetensors"
|
||||
assert entry.families == ("krea-2",)
|
||||
# Every official style repo carries a single "{style}.safetensors" at the root.
|
||||
style = entry.repo_id.split("Krea-2-LoRA-")[-1]
|
||||
assert entry.weight_name == f"{style}.safetensors"
|
||||
|
||||
|
||||
def test_list_loras_family_filter_gates_krea_entries():
|
||||
krea_ids = {e.id for e in _CURATED if e.families == ("krea-2",)}
|
||||
assert krea_ids # curated entries exist
|
||||
listed_for_krea = {e.id for e in list_loras(family = "krea-2")}
|
||||
assert krea_ids <= listed_for_krea
|
||||
listed_for_flux = {e.id for e in list_loras(family = "flux.1")}
|
||||
assert not (krea_ids & listed_for_flux)
|
||||
|
|
@ -100,6 +100,11 @@ const SAFETENSORS_MODELS: Record<string, SafetensorsSpec> = {
|
|||
"unsloth/Z-Image-Turbo-unsloth-bnb-4bit": { kind: "pipeline" },
|
||||
// Krea 2 Turbo: official vendor repo (bf16 pipeline), on the backend allowlist.
|
||||
"krea/Krea-2-Turbo": { kind: "pipeline" },
|
||||
// Ideogram 4: official vendor pipelines, on the backend allowlist. No bf16 repo
|
||||
// exists: -fp8 stores its two DiTs as raw float8 (highest precision; ~46 GB
|
||||
// resident after the bf16 cast); -nf4-diffusers is the bnb-4bit export (~11 GB).
|
||||
"ideogram-ai/ideogram-4-fp8": { kind: "pipeline" },
|
||||
"ideogram-ai/ideogram-4-nf4-diffusers": { kind: "pipeline" },
|
||||
"unsloth/Qwen-Image-2512-unsloth-bnb-4bit": { kind: "pipeline" },
|
||||
"unsloth/Qwen-Image-2512-FP8": {
|
||||
kind: "single_file",
|
||||
|
|
@ -135,6 +140,12 @@ const MODELS: ModelOption[] = [
|
|||
"Safetensors · bnb-4bit",
|
||||
),
|
||||
safetensors("krea/Krea-2-Turbo", "Krea 2 Turbo", "Safetensors · bf16"),
|
||||
safetensors("ideogram-ai/ideogram-4-fp8", "Ideogram 4 (FP8)", "Safetensors · fp8"),
|
||||
safetensors(
|
||||
"ideogram-ai/ideogram-4-nf4-diffusers",
|
||||
"Ideogram 4 (bnb-4bit)",
|
||||
"Safetensors · bnb-4bit",
|
||||
),
|
||||
safetensors(
|
||||
"unsloth/Qwen-Image-2512-unsloth-bnb-4bit",
|
||||
"Qwen-Image 2512 (bnb-4bit)",
|
||||
|
|
@ -223,6 +234,10 @@ const MODEL_DEFAULTS: Array<{ match: string; steps: number; guidance: number }>
|
|||
{ match: "flux.2-dev", steps: 28, guidance: 4 },
|
||||
{ match: "qwen-image", steps: 20, guidance: 4 },
|
||||
{ match: "z-image", steps: 20, guidance: 4 },
|
||||
// Ideogram 4's model-card settings (48 steps, guidance 7). At exactly these
|
||||
// defaults the backend keeps the pipeline's recommended tapered guidance schedule
|
||||
// instead of a flat constant.
|
||||
{ match: "ideogram", steps: 48, guidance: 7 },
|
||||
// SDXL: Turbo is distilled (few steps, no CFG); base/full SDXL wants ~30 steps and
|
||||
// real CFG (~7). "sdxl-turbo" must precede the generic "sdxl" substring match.
|
||||
{ match: "sdxl-turbo", steps: 3, guidance: 0 },
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue