diff --git a/studio/backend/core/inference/video.py b/studio/backend/core/inference/video.py index 8a405bed64..80feba8a90 100644 --- a/studio/backend/core/inference/video.py +++ b/studio/backend/core/inference/video.py @@ -227,6 +227,13 @@ class _SecondDiTView: # ``transformer`` / ``_pipe``), so everything else delegates to the real pipe. return getattr(object.__getattribute__(self, "_pipe"), name) + def __setattr__(self, name: str, value: Any) -> None: + # Writes must land on the real pipe, or a helper's side effect (for example + # reassigning the transformer it optimised) would vanish with the view. + # ``transformer`` mirrors the read property onto the second expert. + pipe = object.__getattribute__(self, "_pipe") + setattr(pipe, "transformer_2" if name == "transformer" else name, value) + def _views_for(pipe: Any, fam: VideoFamily) -> tuple[Any, ...]: """The pipe view(s) to pass through the ``getattr(pipe, "transformer")`` helpers so @@ -283,6 +290,14 @@ class VideoBackend: ) if kind in ("gguf", "single_file") and not gguf_filename: raise ValueError("A gguf/single_file load needs the checkpoint filename.") + if kind in ("gguf", "single_file") and fam.is_moe: + # A single checkpoint carries only one expert; the pipeline would then pull + # the other expert dense bf16 from the base repo, outside the memory plan. + raise ValueError( + f"'{fam.name}' is a dual-expert model: a single {kind} file covers only " + f"one of its two transformers. Load the diffusers pipeline repo " + f"('{fam.base_repo}') instead." + ) # Reject a malformed transformer_quant scheme cheaply, before the GPU handoff # (normalize_transformer_quant raises ValueError on an unknown scheme). It applies # only on pipeline-kind loads (the dense DiT from the base repo); an ignored value diff --git a/studio/backend/core/inference/video_families.py b/studio/backend/core/inference/video_families.py index fde4947d0e..0556fa9f10 100644 --- a/studio/backend/core/inference/video_families.py +++ b/studio/backend/core/inference/video_families.py @@ -188,7 +188,8 @@ _FAMILIES: tuple[VideoFamily, ...] = ( # total ~114.3; UMT5 text encoder 11.4; VAE 0.5. The two-expert DiT total is the # memory headline (~114 GB bf16-resident before offload). bf16_components_gb = (114.3, 11.4, 0.5), - gguf_repo = "QuantStack/Wan2.2-T2V-A14B-GGUF", + # No gguf_repo: community GGUFs ship the two experts as separate files, and a + # single-file load covers only one (validate_load_request refuses it). ), ) diff --git a/studio/backend/tests/test_video_backend.py b/studio/backend/tests/test_video_backend.py index 3b46871dc5..a8e70346e1 100644 --- a/studio/backend/tests/test_video_backend.py +++ b/studio/backend/tests/test_video_backend.py @@ -774,3 +774,36 @@ def test_wan_validate_trusted_repos(fake_runtime): model_kind = "pipeline", transformer_quant = "bogus", ) + + +def test_wan_a14b_refuses_single_file_loads(fake_runtime): + # A single gguf/safetensors checkpoint carries only one of the A14B's two experts; + # the pipeline would pull the other dense bf16 from the base repo outside the + # memory plan, so validate refuses it up front (before any download). + backend = VideoBackend() + with pytest.raises(ValueError, match = "dual-expert"): + backend.validate_load_request( + "QuantStack/Wan2.2-T2V-A14B-GGUF", + gguf_filename = "HighNoise/Wan2.2-T2V-A14B-HighNoise-Q4_K_M.gguf", + ) + # The single-DiT 5B family still accepts GGUF. + fam = backend.validate_load_request( + "QuantStack/Wan2.2-TI2V-5B-GGUF", + gguf_filename = "Wan2.2-TI2V-5B-Q4_K_M.gguf", + ) + assert fam.name == "wan2.2-ti2v-5b" + + +def test_second_dit_view_write_through(): + # Attribute writes on the proxy must land on the real pipe (a helper's side + # effect would otherwise vanish with the temporary view); a ``transformer`` + # write mirrors the read property onto the second expert. + from core.inference.video import _SecondDiTView + + pipe = types.SimpleNamespace(transformer = "t1", transformer_2 = "t2", flag = None) + view = _SecondDiTView(pipe) + assert view.transformer == "t2" + view.transformer = "t2-compiled" + assert pipe.transformer_2 == "t2-compiled" and pipe.transformer == "t1" + view.flag = "set" + assert pipe.flag == "set"