diff --git a/studio/backend/core/inference/diffusion.py b/studio/backend/core/inference/diffusion.py index a04d3825f6..e3cfd692f6 100644 --- a/studio/backend/core/inference/diffusion.py +++ b/studio/backend/core/inference/diffusion.py @@ -300,6 +300,9 @@ _TRUSTED_NON_GGUF_REPOS = frozenset( # Lumina Image 2.0: standard diffusers layout (Gemma2-2B encoder), safetensors-only, # loads through the generic from_pretrained pipeline path. "alpha-vllm/lumina-image-2.0", + # HunyuanImage 2.1: the community diffusers mirror (open, tencent-hunyuan-community + # license), safetensors-only, including the diffusers-native guider components. + "hunyuanvideo-community/hunyuanimage-2.1-diffusers", # Ideogram 4: no bf16 ships. -fp8 stores the two DiTs as raw float8 (the family base); # the two nf4 repos are identical bnb-4bit exports (both listed so either id loads). "ideogram-ai/ideogram-4-fp8", diff --git a/studio/backend/core/inference/diffusion_auto_policy.py b/studio/backend/core/inference/diffusion_auto_policy.py index 00dd981953..851ea65115 100644 --- a/studio/backend/core/inference/diffusion_auto_policy.py +++ b/studio/backend/core/inference/diffusion_auto_policy.py @@ -52,6 +52,8 @@ _FAMILY_BF16_GB: dict[str, tuple[float, float, float]] = { "krea-2": (26.3, 8.9, 0.5), # Ships fp32 (10.4 + 10.5 + 0.3 GB of shards); bf16-resident is half. "lumina-2": (5.2, 5.2, 0.2), + # 17B dual-stream DiT (32.5 GB bf16 on disk) + Qwen2.5-VL 15.5 GB + ByT5 0.8 GB. + "hunyuanimage-2.1": (32.5, 16.3, 0.8), # Two ~9.3B DiTs (conditional + unconditional_transformer for Ideogram's dual-branch CFG), # both resident, plus a Qwen3-VL encoder. The vendor stores them as raw float8; these are the # bf16-resident sizes after the dtype cast, so each doubles (37.2 = 2 x 18.6, encoder 16.3). diff --git a/studio/backend/core/inference/diffusion_families.py b/studio/backend/core/inference/diffusion_families.py index 5df50fcc58..b6da203163 100644 --- a/studio/backend/core/inference/diffusion_families.py +++ b/studio/backend/core/inference/diffusion_families.py @@ -329,6 +329,24 @@ _FAMILIES: tuple[DiffusionFamily, ...] = ( # Published and validated bf16-only upstream; keep the fp16 fallback off like z-image. fp16_incompatible = True, ), + # HunyuanImage 2.1 (diffusers >= 0.39): a 17B dual-stream DiT with a Qwen2.5-VL text + # encoder, a ByT5 glyph encoder, and the 32x-compression HunyuanImage VAE. The community + # mirror also ships guider/ocr_guider components (AdaptiveProjectedMixGuidance), which + # 0.39 loads natively, so the generic from_pretrained pipeline path covers the whole + # stack. 2K-native (the card recipe renders 2048x2048); classifier-free guidance runs + # inside the repo's guider at its baked scale, and the call's own guidance knob is + # distilled_guidance_scale (there is no guidance_scale kwarg). Distinct from + # HunyuanImage-3.0, which stays excluded above: 2.1 has a real diffusers pipeline. + DiffusionFamily( + name = "hunyuanimage-2.1", + pipeline_class = "HunyuanImagePipeline", + transformer_class = "HunyuanImageTransformer2DModel", + base_repo = "hunyuanvideo-community/HunyuanImage-2.1-Diffusers", + cfg_kwarg = "distilled_guidance_scale", + aliases = ("hunyuanimage-2.1-diffusers", "hunyuanimage2.1"), + # Exported bf16-only; keep the fp16 fallback off like z-image / krea-2. + fp16_incompatible = True, + ), # Ideogram 4 (diffusers >= 0.39): a 34-layer DiT PAIR (conditional + unconditional_transformer # for dual-branch CFG, both ~9B, so memory planning counts two DiTs) with a Qwen3-VL encoder. # No bf16 checkpoint: ideogram-4-fp8 (raw float8, upcast on load) is the highest-precision @@ -511,6 +529,9 @@ _GENERATION_DEFAULTS: tuple[tuple[str, int, float], ...] = ( # Lumina Image 2.0 model-card: 50 steps, guidance 4 (plus cfg_trunc_ratio 0.25, which the # loader passes itself; see LUMINA2_FAMILY_NAME). ("lumina", 50, 4.0), + # HunyuanImage 2.1 model-card: 50 steps; the guidance value feeds the call's + # distilled_guidance_scale (default 3.25), while real CFG runs inside the repo guiders. + ("hunyuanimage", 50, 3.25), # Ideogram 4 model-card: 48 steps, guidance 7 (its schedule tapers the last 3 steps to 3.0; # the loader keeps that taper when the request matches these defaults exactly). ("ideogram", 48, 7.0), diff --git a/studio/backend/tests/test_diffusion_more_families.py b/studio/backend/tests/test_diffusion_more_families.py index a65b06c1c5..f59988b7ac 100644 --- a/studio/backend/tests/test_diffusion_more_families.py +++ b/studio/backend/tests/test_diffusion_more_families.py @@ -144,6 +144,64 @@ def test_lumina2_bf16_component_table_present(): assert vae_gb <= 0.5 +# ── hunyuanimage-2.1 family ────────────────────────────────────────────────── +@pytest.mark.parametrize( + "repo_id", + [ + "hunyuanvideo-community/HunyuanImage-2.1-Diffusers", + "QuantStack/HunyuanImage-2.1-GGUF", + # A local GGUF pick where the family keyword lives in the filename (QuantStack's + # actual naming drops the dash: the hunyuanimage2.1 alias covers it). + "QuantStack/HunyuanImage-2.1-GGUF/HunyuanImage2.1-Q4_K_M.gguf", + ], +) +def test_detect_family_hunyuanimage21_repos(repo_id): + fam = detect_family(repo_id) + assert fam is not None and fam.name == "hunyuanimage-2.1" + assert fam.pipeline_class == "HunyuanImagePipeline" + assert fam.transformer_class == "HunyuanImageTransformer2DModel" + assert fam.base_repo == "hunyuanvideo-community/HunyuanImage-2.1-Diffusers" + # The call's guidance knob is distilled_guidance_scale; there is no guidance_scale kwarg. + assert fam.cfg_kwarg == "distilled_guidance_scale" + # Published bf16-only upstream; the fp16 fallback stays off. + assert fam.fp16_incompatible is True + + +def test_detect_family_hunyuanimage21_override_and_30_still_excluded(): + assert detect_family("x", override = "hunyuanimage-2.1").name == "hunyuanimage-2.1" + assert detect_family("x", override = "hunyuanimage2.1").name == "hunyuanimage-2.1" + # The HunyuanImage-3.0 structured exclusion must survive the 2.1 family: 3.0 has + # no diffusers pipeline and must stay unknown with its stated reason. + assert detect_family("tencent/HunyuanImage-3.0") is None + assert excluded_model_reason("tencent/HunyuanImage-3.0") is not None + assert excluded_model_reason("hunyuanvideo-community/HunyuanImage-2.1-Diffusers") is None + + +def test_hunyuanimage21_is_trusted_non_gguf(): + # The mirror pipeline loads via from_pretrained -> needs the allowlist. + assert _is_trusted_diffusion_repo("hunyuanvideo-community/HunyuanImage-2.1-Diffusers") + assert not _is_trusted_diffusion_repo("hunyuanvideo-community/some-future-repo") + + +def test_hunyuanimage21_generation_defaults(): + # Card recipe: 50 steps; guidance feeds the call's distilled_guidance_scale (3.25 + # default), while classifier-free guidance runs inside the repo's guider components. + assert default_generation_params( + "hunyuanvideo-community/HunyuanImage-2.1-Diffusers" + ) == (50, 3.25) + + +def test_hunyuanimage21_bf16_component_table_present(): + fam = detect_family("hunyuanvideo-community/HunyuanImage-2.1-Diffusers") + sizes = family_bf16_components_gb(fam) + assert sizes is not None + transformer_gb, encoders_gb, vae_gb = sizes + # 17B DiT (32.5 GB bf16 on disk) + Qwen2.5-VL 15.5 GB + ByT5 0.8 GB. + assert 30.0 <= transformer_gb <= 35.0 + assert 15.0 <= encoders_gb <= 18.0 + assert vae_gb <= 1.0 + + 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).