diff --git a/.github/scripts/plan_merge_ci.py b/.github/scripts/plan_merge_ci.py index e00c4199d..a1e8b237c 100644 --- a/.github/scripts/plan_merge_ci.py +++ b/.github/scripts/plan_merge_ci.py @@ -12,6 +12,7 @@ from __future__ import annotations import argparse import fnmatch import re +from collections.abc import Iterable from dataclasses import dataclass, field from pathlib import Path from typing import TextIO @@ -289,16 +290,22 @@ def _select_output_coverage(plan: MergePlan, path: str) -> None: plan.add_ssim(SSIM_SMOKE_TESTS, reason=f"shared output SSIM smoke coverage: {path}") -def classify_paths(paths: list[str]) -> MergePlan: +def _normalize_path(raw_path: str) -> str: + path = raw_path.strip() + while path.startswith("./"): + path = path[2:] + return path + + +def classify_paths(paths: list[str], removed_paths: Iterable[str] = ()) -> MergePlan: + """Plan merge lanes for ``paths``. + + ``removed_paths`` lists changed paths that no longer exist at the PR head + (deleted files and rename sources); removed golden/SSIM tests are not run. + """ plan = MergePlan() - normalized_paths: list[str] = [] - for raw_path in paths: - path = raw_path.strip() - while path.startswith("./"): - path = path[2:] - if path: - normalized_paths.append(path) - normalized_paths = sorted(set(normalized_paths)) + normalized_paths = sorted({path for path in map(_normalize_path, paths) if path}) + removed = {path for path in map(_normalize_path, removed_paths) if path} if not normalized_paths: plan.require_all("changed-file list was empty; failing closed") return plan @@ -338,7 +345,10 @@ def classify_paths(paths: list[str]) -> MergePlan: if path.startswith("fastvideo/tests/golden_gate/"): name = Path(path).name if name.startswith("test_") and name.endswith(".py"): - plan.add_golden((name, ), reason=f"changed golden test: {path}") + if path in removed: + plan.reasons.append(f"removed golden test has nothing to run: {path}") + else: + plan.add_golden((name, ), reason=f"changed golden test: {path}") elif name in {"AGENTS.md", "README.md"}: plan.reasons.append(f"golden documentation only: {path}") else: @@ -349,7 +359,10 @@ def classify_paths(paths: list[str]) -> MergePlan: if path.startswith("fastvideo/tests/ssim/"): name = Path(path).name if name.startswith("test_") and name.endswith(".py"): - plan.add_ssim((name, ), reason=f"changed SSIM test: {path}") + if path in removed: + plan.reasons.append(f"removed SSIM test has nothing to run: {path}") + else: + plan.add_ssim((name, ), reason=f"changed SSIM test: {path}") elif path.endswith((".py", ".json", ".pt", ".png", ".mp4")): plan.ssim_all = True plan.add_lanes("ssim", reason=f"shared SSIM harness/reference: {path}") @@ -525,6 +538,7 @@ def _write_summary(output: TextIO, plan: MergePlan) -> None: def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("--paths-file", type=Path, required=True) + parser.add_argument("--removed-paths-file", type=Path) parser.add_argument("--github-output", type=Path) parser.add_argument("--summary-file", type=Path) return parser.parse_args() @@ -533,7 +547,9 @@ def parse_args() -> argparse.Namespace: def main() -> int: args = parse_args() paths = args.paths_file.read_text(encoding="utf-8").splitlines() - plan = classify_paths(paths) + removed_paths = (args.removed_paths_file.read_text(encoding="utf-8").splitlines() + if args.removed_paths_file else []) + plan = classify_paths(paths, removed_paths) print(f"MERGE_TEST_PLAN={plan.encoded_lanes()}") print(f"MERGE_GOLDEN_TESTS={plan.encoded_golden_tests()}") print(f"MERGE_SSIM_TESTS={plan.encoded_ssim_tests()}") diff --git a/.github/workflows/ci-trigger-full-suite.yml b/.github/workflows/ci-trigger-full-suite.yml index e96ad9c81..f70f267c6 100644 --- a/.github/workflows/ci-trigger-full-suite.yml +++ b/.github/workflows/ci-trigger-full-suite.yml @@ -76,6 +76,8 @@ jobs: set -euo pipefail changed_json="$RUNNER_TEMP/merge-changed-files.json" changed_paths="$RUNNER_TEMP/merge-changed-paths.txt" + removed_paths="$RUNNER_TEMP/merge-removed-paths.txt" + : > "$removed_paths" if gh api --paginate --slurp \ "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \ > "$changed_json"; then @@ -83,6 +85,10 @@ jobs: if [ "$observed" = "$EXPECTED_CHANGED_FILES" ]; then jq -r '.[][] | .filename, (.previous_filename // empty)' "$changed_json" \ | sort -u > "$changed_paths" + # Paths absent from the PR head, so the planner never selects a deleted test. + jq -r '.[][] | if .status == "removed" then .filename + elif .status == "renamed" then (.previous_filename // empty) else empty end' \ + "$changed_json" | sort -u > "$removed_paths" else echo "::warning::Changed-file API returned $observed of $EXPECTED_CHANGED_FILES paths; selecting all merge lanes." echo '__FASTVIDEO_CI_PLAN_ALL__' > "$changed_paths" @@ -98,6 +104,7 @@ jobs: run: | python3 .github/scripts/plan_merge_ci.py \ --paths-file "$RUNNER_TEMP/merge-changed-paths.txt" \ + --removed-paths-file "$RUNNER_TEMP/merge-removed-paths.txt" \ --github-output "$GITHUB_OUTPUT" \ --summary-file "$GITHUB_STEP_SUMMARY" diff --git a/docs/inference/support_matrix.md b/docs/inference/support_matrix.md index 6a713a01c..e6a49f4ca 100644 --- a/docs/inference/support_matrix.md +++ b/docs/inference/support_matrix.md @@ -152,25 +152,25 @@ optimizations: absence means **untested**, not incompatible. } -| Model Name | HuggingFace Model ID | Resolutions | TeaCache | Sliding Tile Attn (Legacy Branch) | Sage Attn | VSA | BSA | -|------------|---------------------|-------------|----------|-------------------|-----------|-----|-----| -| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ✅ | ⭕ | -| FastWan2.2 TI2V 5B Full Attn | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P | ⭕ | ⭕ | ⭕ | ✅ | ⭕ | -| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P | ⭕ | ⭕ | ✅ | ⭕ | ⭕ | -| Lucy Edit Dev 5B*** | `decart-ai/Lucy-Edit-Dev` | 480P | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | -| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ | ⭕ | -| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ | ⭕ | -| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ | ⭕ | -| FastHunyuan | `FastVideo/FastHunyuan-diffusers` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ | ⭕ | -| Wan2.1 T2V 1.3B | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ | ⭕ | -| Wan2.1 T2V 14B | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | 480P, 720P | ✅ | ✅ | ✅ | ⭕ | ⭕ | -| Wan2.1 I2V 480P | `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ | ⭕ | -| Wan2.1 I2V 720P | `Wan-AI/Wan2.1-I2V-14B-720P-Diffusers` | 720P | ✅ | ✅ | ✅ | ⭕ | ⭕ | -| Matrix Game 2.0 Base Distilled | `FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | -| Matrix Game 2.0 GTA Distilled | `FastVideo/Matrix-Game-2.0-GTA-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | -| Matrix Game 2.0 TempleRun Distilled | `FastVideo/Matrix-Game-2.0-TempleRun-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | -| Matrix Game 3.0 Base Distilled | `FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers` | 720x1280 | ⭕ | ⭕ | ⭕ | ⭕ | ⭕ | -| GEN3C Cosmos 7B | `FastVideo/GEN3C-Cosmos-7B-Diffusers` | 704px1280p | ❌ | ❌ | ❌ | ⭕ | ⭕ | +| Model Name | HuggingFace Model ID | Resolutions | TeaCache | Sliding Tile Attn (Legacy Branch) | Sage Attn | VSA | +|------------|---------------------|-------------|----------|-------------------|-----------|-----| +| FastWan2.1 T2V 1.3B | `FastVideo/FastWan2.1-T2V-1.3B-Diffusers` | 480P | ⭕ | ⭕ | ⭕ | ✅ | +| FastWan2.2 TI2V 5B Full Attn | `FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers` | 720P | ⭕ | ⭕ | ⭕ | ✅ | +| Wan2.2 TI2V 5B | `Wan-AI/Wan2.2-TI2V-5B-Diffusers` | 720P | ⭕ | ⭕ | ✅ | ⭕ | +| Lucy Edit Dev 5B*** | `decart-ai/Lucy-Edit-Dev` | 480P | ⭕ | ⭕ | ⭕ | ⭕ | +| Wan2.2 T2V A14B | `Wan-AI/Wan2.2-T2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ | +| Wan2.2 I2V A14B | `Wan-AI/Wan2.2-I2V-A14B-Diffusers` | 480P
720P | ❌ | ❌ | ✅ | ⭕ | +| HunyuanVideo | `hunyuanvideo-community/HunyuanVideo` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ | +| FastHunyuan | `FastVideo/FastHunyuan-diffusers` | 720px1280p
544px960p | ❌ | ✅ | ✅ | ⭕ | +| Wan2.1 T2V 1.3B | `Wan-AI/Wan2.1-T2V-1.3B-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ | +| Wan2.1 T2V 14B | `Wan-AI/Wan2.1-T2V-14B-Diffusers` | 480P, 720P | ✅ | ✅ | ✅ | ⭕ | +| Wan2.1 I2V 480P | `Wan-AI/Wan2.1-I2V-14B-480P-Diffusers` | 480P | ✅ | ✅ | ✅ | ⭕ | +| Wan2.1 I2V 720P | `Wan-AI/Wan2.1-I2V-14B-720P-Diffusers` | 720P | ✅ | ✅ | ✅ | ⭕ | +| Matrix Game 2.0 Base Distilled | `FastVideo/Matrix-Game-2.0-Base-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | +| Matrix Game 2.0 GTA Distilled | `FastVideo/Matrix-Game-2.0-GTA-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | +| Matrix Game 2.0 TempleRun Distilled | `FastVideo/Matrix-Game-2.0-TempleRun-Distilled-Diffusers` | 352x640 | ⭕ | ⭕ | ⭕ | ⭕ | +| Matrix Game 3.0 Base Distilled | `FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers` | 720x1280 | ⭕ | ⭕ | ⭕ | ⭕ | +| GEN3C Cosmos 7B | `FastVideo/GEN3C-Cosmos-7B-Diffusers` | 704px1280p | ❌ | ❌ | ❌ | ⭕ | ## Apple Silicon native runtime diff --git a/fastvideo/models/AGENTS.md b/fastvideo/models/AGENTS.md index 635f41eee..ab7483cd0 100644 --- a/fastvideo/models/AGENTS.md +++ b/fastvideo/models/AGENTS.md @@ -21,7 +21,6 @@ models/ ├── schedulers/ # FlowMatch / EulerDiscrete / DPM custom schedulers ├── upsamplers/ # Hunyuan15 super-resolution ├── audio/ # Audio-VAE/decoder modules (LTX-2 audio, Stable Audio) -├── camera/ # Camera-conditioning modules (Gen3C) └── loader/ # component_loader.py, fsdp_load.py, weight_utils.py ``` diff --git a/fastvideo/models/registry.py b/fastvideo/models/registry.py index f3baf6e28..66e1bef39 100644 --- a/fastvideo/models/registry.py +++ b/fastvideo/models/registry.py @@ -62,10 +62,6 @@ _IMAGE_TO_VIDEO_DIT_MODELS = { "MatrixGame3WanModel": ("dits", "matrixgame3", "MatrixGame3WanModel"), } -# Text-to-image DiT models (2D image generation) -_TEXT_TO_IMAGE_DIT_MODELS = { -} - _TEXT_ENCODER_MODELS = { "MMAudioDFNCLIPTextEncoder": ("encoders", "mmaudio_clip", "MMAudioDFNCLIPTextEncoder"), "CLIPTextModel": ("encoders", "clip", "CLIPTextModel"), @@ -104,7 +100,6 @@ _VAE_MODELS = { "LingBotWorld2WanVAE": ("vaes", "lingbotworld2_wanvae", "LingBotWorld2WanVAE"), "AutoencoderKL": ("vaes", "autoencoder_kl", "AutoencoderKL"), "AutoencoderKLGen3CTokenizer": ("vaes", "gen3c_tokenizer_vae", "AutoencoderKLGen3CTokenizer"), - "AutoencoderKLStepvideo": ("vaes", "stepvideovae", "AutoencoderKLStepvideo"), "CausalVideoAutoencoder": ("vaes", "ltx2vae", "LTX2CausalVideoAutoencoder"), "AutoencoderKLFlux2": ("vaes", "flux2vae", "AutoencoderKLFlux2"), # `stable-audio-open-1.0/vae/config.json` ships `_class_name="AutoencoderOobleck"` @@ -139,7 +134,6 @@ _UPSAMPLERS = { _LEGACY_FAST_VIDEO_MODELS = { **_TEXT_TO_VIDEO_DIT_MODELS, **_IMAGE_TO_VIDEO_DIT_MODELS, - **_TEXT_TO_IMAGE_DIT_MODELS, **_TEXT_ENCODER_MODELS, **_IMAGE_ENCODER_MODELS, **_VAE_MODELS, diff --git a/fastvideo/pipelines/AGENTS.md b/fastvideo/pipelines/AGENTS.md index e7b7b4639..018452338 100644 --- a/fastvideo/pipelines/AGENTS.md +++ b/fastvideo/pipelines/AGENTS.md @@ -23,8 +23,8 @@ pipelines/ │ ├── gen3c_stages.py # Gen3C-specific stages │ └── matrixgame2_denoising.py # Matrix-Game 2.0-specific ├── basic/ # Per-model end-to-end pipelines -│ ├── cosmos/, flux/, flux_2/, gen3c/ -│ ├── hunyuan/, hunyuan15/, hyworld/, kandinsky5/, lingbot_video/, lingbotworld/ +│ ├── cosmos/, flux/, flux_2/, gen3c/, hunyuan/, hunyuan15/, hyworld/ +│ ├── kandinsky5/, kandinsky6/, kandinsky6_sr/, lingbot_video/, lingbotworld/ │ ├── lingbotworld2/, ltx2/, magi_human/, matrixgame2/, matrixgame3/ │ ├── minimax_h3/, mmaudio/, sd35/, stable_audio/, wan/ │ └── /{_pipeline.py, presets.py, __init__.py} diff --git a/fastvideo/registry.py b/fastvideo/registry.py index b01150093..fa9917efd 100644 --- a/fastvideo/registry.py +++ b/fastvideo/registry.py @@ -464,7 +464,7 @@ def _register_configs() -> None: default_preset="hunyuan15_sr_1080p", ) - # Hunyuan (excludes hyworld and versioned models) + # Hunyuan (excludes hyworld, versioned models, and removed GameCraft checkpoints) register_configs( sampling_param_cls=None, pipeline_config_cls=HunyuanConfig, @@ -473,8 +473,8 @@ def _register_configs() -> None: "hunyuanvideo-community/HunyuanVideo", ], model_detectors=[ - lambda path: "hunyuan" in path.lower() and "hyworld" not in path.lower() and "1.5" not in path.lower() and - "1-5" not in path.lower() + lambda path: "hunyuan" in path.lower() and "gamecraft" not in path.lower() and "hyworld" not in path.lower( + ) and "1.5" not in path.lower() and "1-5" not in path.lower() ], model_family="hunyuan", default_preset="hunyuan_t2v", diff --git a/fastvideo/tests/api/test_presets.py b/fastvideo/tests/api/test_presets.py index 846f15ad7..1dcc64d58 100644 --- a/fastvideo/tests/api/test_presets.py +++ b/fastvideo/tests/api/test_presets.py @@ -404,6 +404,22 @@ class TestHunyuanPresets: p = get_preset("fast_hunyuan_t2v", "hunyuan") assert p.defaults["num_inference_steps"] == 6 + def test_removed_gamecraft_checkpoint_is_not_detected_as_hunyuan(self, tmp_path) -> None: + from fastvideo.registry import get_model_family + + def make_checkpoint(name: str, class_name: str) -> str: + model_dir = tmp_path / name + model_dir.mkdir() + (model_dir / "model_index.json").write_text( + json.dumps({ + "_class_name": class_name, + "_diffusers_version": "0.33.1" + })) + return str(model_dir) + + assert get_model_family(make_checkpoint("HunyuanVideo-Custom", "HunyuanVideoPipeline")) == "hunyuan" + assert get_model_family(make_checkpoint("Hunyuan-GameCraft-Diffusers", "HunyuanGameCraftPipeline")) is None + # ------------------------------------------------------------------- # Hunyuan15 preset integration (includes two-stage SR) diff --git a/fastvideo/tests/contract/test_merge_ci_plan.py b/fastvideo/tests/contract/test_merge_ci_plan.py index 8cfce78ee..af5774cd8 100644 --- a/fastvideo/tests/contract/test_merge_ci_plan.py +++ b/fastvideo/tests/contract/test_merge_ci_plan.py @@ -111,6 +111,28 @@ def test_changed_ssim_test_selects_only_that_file(): assert plan.encoded_ssim_tests() == "test_flux_t2i_similarity.py" +def test_removed_golden_and_ssim_tests_are_not_selected(): + removed = [ + "fastvideo/tests/golden_gate/test_longcat.py", + "fastvideo/tests/ssim/test_longcat_similarity.py", + ] + plan = PLAN_MERGE_CI.classify_paths(removed, removed_paths=removed) + + assert plan.encoded_lanes() == ",none," + assert plan.encoded_golden_tests() == "none" + assert plan.encoded_ssim_tests() == "none" + + +def test_renamed_golden_test_selects_only_the_new_name(): + plan = PLAN_MERGE_CI.classify_paths( + ["fastvideo/tests/golden_gate/test_old_name.py", "fastvideo/tests/golden_gate/test_new_name.py"], + removed_paths=["fastvideo/tests/golden_gate/test_old_name.py"], + ) + + assert plan.encoded_lanes() == ",golden-gate," + assert plan.encoded_golden_tests() == "test_new_name.py" + + def test_shared_golden_harness_or_reference_requires_full_golden_lane(): plan = PLAN_MERGE_CI.classify_paths(["fastvideo/tests/golden_gate/_harness.py"]) diff --git a/fastvideo/tests/train/utils/test_finetune_config_construction.py b/fastvideo/tests/train/utils/test_finetune_config_construction.py index cb726daa4..a73225d93 100644 --- a/fastvideo/tests/train/utils/test_finetune_config_construction.py +++ b/fastvideo/tests/train/utils/test_finetune_config_construction.py @@ -40,4 +40,4 @@ def test_fine_tuning_recipe_constructs(recipe: Path) -> None: def test_recipe_glob_found_recipes() -> None: # Guard against the glob silently matching nothing after a move. - assert len(_RECIPES) >= 15 + assert len(_RECIPES) >= 18 diff --git a/pyproject_other.toml b/pyproject_other.toml index cdf5db5db..4af8eeda2 100644 --- a/pyproject_other.toml +++ b/pyproject_other.toml @@ -21,7 +21,9 @@ dependencies = [ "requests>=2.32.2", # Machine Learning & Transformers - # Floor bumped from >=4.57.3 to >=5.0.0 (stable, not rc). + # Floor bumped from >=4.57.3 to >=5.0.0 (stable, not rc) for GLM-Image (#1030). + # GLM-Image was removed in #1926, but nothing merged since has been tested + # against transformers 4.x, so keep the floor until that is verified. "transformers>=5.0.0", "tokenizers>=0.20.1", "sentencepiece>=0.2.0",