diff --git a/docs/inference/configuration.md b/docs/inference/configuration.md index 170c12c9f..7e476336a 100644 --- a/docs/inference/configuration.md +++ b/docs/inference/configuration.md @@ -30,28 +30,26 @@ bring-up: [Pair two NVIDIA DGX Sparks](../getting_started/installation/spark_pai ## Customizing Generation -- `PipelineConfig`: Initialization time parameters -- `SamplingParam`: Generation time parameters - -You can customize generation behavior using `PipelineConfig` and -`SamplingParam`: +`VideoGenerator.from_pretrained` accepts only common engine and offload +keywords, such as `num_gpus` and `dit_cpu_offload`. The full list is +`fastvideo.api.compat.FROM_PRETRAINED_KWARGS`. Pass every other startup +setting to `VideoGenerator.from_config` at its config path, and pass +generation settings to `VideoGenerator.generate` as a request: ```python -from fastvideo import VideoGenerator, PipelineConfig +from fastvideo import VideoGenerator def main(): model_name = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" - config = PipelineConfig.from_pretrained(model_name) - config.vae_precision = "fp16" # Create the generator generator = VideoGenerator.from_config({ "model_path": model_name, "engine": { "num_gpus": 1, - "offload": {"dit_layerwise": True}, # FastVideoArgs option + "offload": {"dit_layerwise": True}, + "precision": {"vae": "fp16"}, }, - "pipeline": {"experimental": {"pipeline_config": config}}, }) # Generate video with custom parameters diff --git a/fastvideo/api/compat.py b/fastvideo/api/compat.py index 876334f03..778b540a9 100644 --- a/fastvideo/api/compat.py +++ b/fastvideo/api/compat.py @@ -5,7 +5,7 @@ from collections.abc import Iterator, Mapping from copy import deepcopy from dataclasses import fields, is_dataclass from pathlib import Path -from typing import Any, get_args, get_origin, get_type_hints +from typing import Any, get_args, get_type_hints from fastvideo.api.inference_resolution import resolve_inference_config from fastvideo.api.overrides import apply_overrides, normalize_overrides @@ -42,6 +42,34 @@ REQUEST_BATCH_EXTRA_PASSTHROUGH_FIELDS = ( "vsa_dense_first_n_steps", "vsa_dense_layers", ) +# The VideoGenerator.from_pretrained keywords besides model_path, config, and log_queue. Every other setting goes +# through VideoGenerator.from_config at its typed path. +FROM_PRETRAINED_KWARGS = frozenset({ + "num_gpus", + "revision", + "trust_remote_code", + "distributed_executor_backend", + "tp_size", + "sp_size", + "hsdp_replicate_dim", + "hsdp_shard_dim", + "dist_timeout", + "use_fsdp_inference", + "disable_autocast", + "enable_stage_verification", + "dit_cpu_offload", + "dit_layerwise_offload", + "text_encoder_cpu_offload", + "image_encoder_cpu_offload", + "vae_cpu_offload", + "pin_cpu_memory", + "enable_torch_compile", + "torch_compile_kwargs", + "lora_path", + "lora_strength", + "output_type", + "nvfp4_fa4", +}) # torch.compile kwargs that map to first-class CompileConfig fields. _COMPILE_TYPED_KEYS = ("backend", "fullgraph", "mode", "dynamic") # LTX-2 refine flat kwargs (init + per-request) known to FastVideoArgs. @@ -75,18 +103,23 @@ def load_generator_config_from_file( return parse_config(GeneratorConfig, raw) -def legacy_from_pretrained_to_config( +def from_pretrained_kwargs_to_config( model_path: str, kwargs: Mapping[str, Any], ) -> GeneratorConfig: """Build a ``GeneratorConfig`` from ``VideoGenerator.from_pretrained`` keyword arguments. - A keyword that a schema field declares as its flat name sets that field. The keywords in the branches below - need a conversion instead, and any other keyword is kept in ``pipeline.experimental``. + A keyword that a schema field declares as its flat name sets that field. ``torch_compile_kwargs`` is split across + ``engine.compile``, and the keywords without a typed field are kept in ``pipeline.experimental``. A keyword outside + ``FROM_PRETRAINED_KWARGS`` raises ``TypeError`` that names the typed path to use with ``from_config``. """ - raw: dict[str, Any] = {"model_path": model_path} - experimental: dict[str, Any] = {} + unsupported = sorted(set(kwargs) - FROM_PRETRAINED_KWARGS) + if unsupported: + paths = ", ".join(f"{key} -> {_typed_path_of_keyword(key, kwargs[key])}" for key in unsupported) + raise TypeError("VideoGenerator.from_pretrained(...) does not accept these keywords; pass them to " + f"VideoGenerator.from_config(...) at these config paths: {paths}") + raw: dict[str, Any] = {"model_path": model_path} for key, value in kwargs.items(): if key == "torch_compile_kwargs": remaining: dict[str, Any] = (dict(deepcopy(value)) if isinstance(value, Mapping) else {}) @@ -95,29 +128,25 @@ def legacy_from_pretrained_to_config( _set_dotted_path(raw, ["engine", "compile", first_class], remaining.pop(first_class)) if remaining: _set_dotted_path(raw, ["engine", "compile", "extras"], remaining) - elif key == "pipeline_config" and not isinstance(value, str): - experimental[key] = deepcopy(value) - elif key.startswith(tuple(_COMPONENT_OVERRIDE_PREFIXES)): - component, field_name = key.split(".", 1) - _set_dotted_path(raw, ["pipeline", _COMPONENT_OVERRIDE_PREFIXES[component + "."], field_name], value) - elif key in _LTX2_REFINE_PRESET_KEYWORDS: - _set_dotted_path(raw, ["pipeline", "preset_overrides", "refine", key[len("ltx2_refine_"):]], value) - elif key in _EMPTY_MEANS_UNSET_KEYWORDS: - # An empty string means "no file"; keep typed None. - _set_dotted_path(raw, _EMPTY_MEANS_UNSET_KEYWORDS[key].split("."), value or None) elif key in _FLAT_NAME_FIELDS: - dotted_path, annotation = _FLAT_NAME_FIELDS[key] - if get_origin(annotation) is dict: - value = dict(deepcopy(value)) if isinstance(value, Mapping) else {} - _set_dotted_path(raw, dotted_path.split("."), value) + _set_dotted_path(raw, _FLAT_NAME_FIELDS[key][0].split("."), value) else: - experimental[key] = deepcopy(value) - - if experimental: - _set_dotted_path(raw, ["pipeline", "experimental"], experimental) + _set_dotted_path(raw, ["pipeline", "experimental", key], deepcopy(value)) return parse_config(GeneratorConfig, raw) +def _typed_path_of_keyword(key: str, value: Any) -> str: + """The ``GeneratorConfig`` path that holds the setting of a flat ``FastVideoArgs`` keyword.""" + if key in _FLAT_NAME_FIELDS and not (key == "pipeline_config" and not isinstance(value, str)): + return _FLAT_NAME_FIELDS[key][0] + if key in _LTX2_REFINE_PRESET_KEYWORDS: + return f"pipeline.preset_overrides.refine.{key[len('ltx2_refine_'):]}" + for prefix, section in _COMPONENT_OVERRIDE_PREFIXES.items(): + if key.startswith(prefix): + return f"pipeline.{section}.{key[len(prefix):]}" + return f"pipeline.experimental.{key}" + + def generator_config_to_fastvideo_args( config: GeneratorConfig | Mapping[str, Any] | ResolvedGeneratorConfig, ) -> FastVideoArgs: """Resolve a ``GeneratorConfig`` and flatten it into the ``FastVideoArgs`` that runtime code reads. @@ -282,11 +311,7 @@ _COMPONENT_OVERRIDE_PREFIXES = { "dit_config.": "dit", "vae_config.": "vae", } -# from_pretrained keywords whose empty-string value means "unset", and the field each one sets. -_EMPTY_MEANS_UNSET_KEYWORDS = { - "ltx2_refine_upsampler_path": "pipeline.components.upsampler_weights", -} -# from_pretrained keywords that set pipeline.preset_overrides.refine.. +# Flat keywords whose setting is pipeline.preset_overrides.refine.. _LTX2_REFINE_PRESET_KEYWORDS = frozenset({ "ltx2_refine_enabled", "ltx2_refine_add_noise", @@ -506,11 +531,12 @@ def _validate_batched_input_length( __all__ = [ + "FROM_PRETRAINED_KWARGS", "REQUEST_BATCH_EXTRA_PASSTHROUGH_FIELDS", "explicit_request_raw", "explicit_request_updates", + "from_pretrained_kwargs_to_config", "generator_config_to_fastvideo_args", - "legacy_from_pretrained_to_config", "load_generator_config_from_file", "normalize_generation_request", "normalize_generator_config", diff --git a/fastvideo/entrypoints/video_generator.py b/fastvideo/entrypoints/video_generator.py index 3ff211331..4bfc6878a 100644 --- a/fastvideo/entrypoints/video_generator.py +++ b/fastvideo/entrypoints/video_generator.py @@ -30,8 +30,8 @@ import fastvideo.envs as envs from fastvideo.api.compat import ( REQUEST_BATCH_EXTRA_PASSTHROUGH_FIELDS, expand_request_prompt_batch, + from_pretrained_kwargs_to_config, generator_config_to_fastvideo_args, - legacy_from_pretrained_to_config, load_generator_config_from_file, normalize_generation_request, normalize_generator_config, @@ -71,33 +71,6 @@ _FFMPEG_ENCODER_OPTION_CACHE: dict[tuple[str, str, str], bool] = {} _BATCH_EXTRA_PASSTHROUGH_KEYS = tuple(REQUEST_BATCH_EXTRA_PASSTHROUGH_FIELDS) -_FROM_PRETRAINED_CONVENIENCE_KWARGS = frozenset({ - "num_gpus", - "revision", - "trust_remote_code", - "distributed_executor_backend", - "tp_size", - "sp_size", - "hsdp_replicate_dim", - "hsdp_shard_dim", - "dist_timeout", - "use_fsdp_inference", - "disable_autocast", - "enable_stage_verification", - "dit_cpu_offload", - "dit_layerwise_offload", - "text_encoder_cpu_offload", - "image_encoder_cpu_offload", - "vae_cpu_offload", - "pin_cpu_memory", - "enable_torch_compile", - "torch_compile_kwargs", - "lora_path", - "lora_strength", - "output_type", - "nvfp4_fa4", -}) - def _infer_latent_batch_size(batch: ForwardBatch) -> int: if isinstance(batch.prompt, list): @@ -185,20 +158,16 @@ class VideoGenerator: ) -> "VideoGenerator": """ Create a video generator from a pretrained model. - + Args: model_path: Path or identifier for the pretrained model - pipeline_config: Pipeline config to use for inference - **kwargs: Additional arguments to customize model loading, set any FastVideoArgs or PipelineConfig attributes here. - + **kwargs: The common engine and offload keywords in + ``fastvideo.api.compat.FROM_PRETRAINED_KWARGS``, such as + ``num_gpus`` and ``dit_cpu_offload``. Pass any other setting + through ``VideoGenerator.from_config(...)``. + Returns: The created video generator - - Priority level: Default pipeline config < User's pipeline config < User's kwargs - - Stable convenience kwargs remain supported here for common engine and - offload settings. Advanced model- or pipeline-specific options should - move to VideoGenerator.from_config(...). """ log_queue = kwargs.pop("log_queue", None) if kwargs.pop("nvfp4_fa4", False): @@ -223,17 +192,8 @@ class VideoGenerator: if model_path is None: raise TypeError("model_path or config is required") - legacy_only_kwargs = sorted(set(kwargs) - _FROM_PRETRAINED_CONVENIENCE_KWARGS) - if legacy_only_kwargs: - warnings.warn( - "VideoGenerator.from_pretrained(...) received legacy-only kwargs " - f"({', '.join(legacy_only_kwargs)}); prefer VideoGenerator.from_config(...) " - "for advanced configuration.", - DeprecationWarning, - stacklevel=2, - ) return cls.from_config( - legacy_from_pretrained_to_config(model_path, kwargs), + from_pretrained_kwargs_to_config(model_path, kwargs), log_queue=log_queue, ) diff --git a/fastvideo/tests/api/config_snapshot.py b/fastvideo/tests/api/config_snapshot.py index 82eddadb3..a226150f3 100644 --- a/fastvideo/tests/api/config_snapshot.py +++ b/fastvideo/tests/api/config_snapshot.py @@ -8,7 +8,9 @@ and a ``SamplingParam`` where the case has a request) through one of the public - ``yaml``: every repository config file with a ``generator`` section, through ``load_generator_config_from_file``, ``resolve_inference_config``, and ``generator_config_to_fastvideo_args``, with the resolution decisions; its ``request`` or ``default_request`` through the ``fastvideo generate`` and ``fastvideo serve`` loaders and ``request_to_sampling_param``. -- ``kwargs``: ``VideoGenerator.from_pretrained`` keywords, through ``legacy_from_pretrained_to_config`` and the same +- ``kwargs``: ``VideoGenerator.from_pretrained`` keywords, through ``from_pretrained_kwargs_to_config`` and the same + resolution. +- ``config``: typed ``GeneratorConfig`` mappings, as ``VideoGenerator.from_config`` receives them, through the same resolution. - ``cli``: argparse flags, through ``FastVideoArgs.add_cli_args`` and ``FastVideoArgs.from_cli_args``. - ``environment``: environment variables that ``FastVideoArgs.__post_init__`` folds into fields. @@ -263,9 +265,19 @@ def _yaml_request(path: Path) -> Any: def _kwargs_case(model_path: str, kwargs: dict[str, Any]) -> Callable[[], dict[str, Any]]: def build() -> dict[str, Any]: - from fastvideo.api.compat import legacy_from_pretrained_to_config + from fastvideo.api.compat import from_pretrained_kwargs_to_config - return snapshot_resolved_fastvideo_args(legacy_from_pretrained_to_config(model_path, kwargs)) + return snapshot_resolved_fastvideo_args(from_pretrained_kwargs_to_config(model_path, kwargs)) + + return build + + +def _config_case(raw: dict[str, Any]) -> Callable[[], dict[str, Any]]: + + def build() -> dict[str, Any]: + from fastvideo.api.compat import normalize_generator_config + + return snapshot_resolved_fastvideo_args(normalize_generator_config(raw)) return build @@ -332,9 +344,7 @@ def collect_cases() -> list[SnapshotCase]: SnapshotCase("yaml", path.relative_to(REPO_ROOT).as_posix(), _yaml_case(path)) for path in _repository_config_files() ] - # One case per kind of branch in legacy_from_pretrained_to_config: flat names of typed fields, keys left in - # pipeline.experimental, compile sub-keys, LTX-2 refine keys, and a pipeline config JSON path. - pipeline_json = str(REPO_ROOT / "fastvideo/configs/fasthunyuan_t2v.json") + # One case per kind of branch in from_pretrained_kwargs_to_config: flat names of typed fields and compile sub-keys. kwargs_cases = { "typed_offload_and_parallelism": (WAN_T2V, { "num_gpus": 2, @@ -342,17 +352,6 @@ def collect_cases() -> list[SnapshotCase]: "dit_cpu_offload": False, "vae_cpu_offload": False, }), - "attention_precision_and_flow_shift": (WAN_T2V, { - "attention_backend": "TORCH_SDPA", - "flow_shift": 5.0, - "VSA_sparsity": 0.5, - "dit_precision": "fp32", - }), - "keys_without_typed_fields": (WAN_T2V, { - "master_port": 29600, - "refine_enabled": True, - "boundary_ratio": 0.5, - }), "torch_compile_kwargs_typed_and_extra": (WAN_T2V, { "enable_torch_compile": True, "torch_compile_kwargs": { @@ -364,32 +363,95 @@ def collect_cases() -> list[SnapshotCase]: }, }, }), - "ltx2_refine_lora_path": (LTX2, { - "ltx2_refine_lora_path": "/checkpoints/refine_lora.safetensors" - }), - "ltx2_refine_lora_disabled": (LTX2, { - "ltx2_refine_lora_path": "" - }), - "pipeline_config_json_path": ("FastVideo/FastHunyuan-diffusers", { - "pipeline_config": pipeline_json - }), "disable_autocast": (WAN_T2V, { "disable_autocast": True }), - "boundary_ratio": (WAN22_T2V, { - "boundary_ratio": 0.8 - }), - "vae_tiling_typed": (LTX2, { - "ltx2_vae_tiling": False - }), - "ltx2_vae_tile_sizes": (LTX2, { - "ltx2_vae_spatial_tile_size_in_pixels": 512, - "ltx2_vae_temporal_tile_size_in_frames": 64, - }), } cases += [ SnapshotCase("kwargs", name, _kwargs_case(model, kwargs)) for name, (model, kwargs) in kwargs_cases.items() ] + # Typed configs for the settings that only from_config accepts: typed fields, keys left in + # pipeline.experimental, LTX-2 refine and VAE tile fields, and a pipeline config JSON path. + pipeline_json = str(REPO_ROOT / "fastvideo/configs/fasthunyuan_t2v.json") + config_cases = { + "attention_precision_and_flow_shift": { + "model_path": WAN_T2V, + "engine": { + "attention": { + "backend": "TORCH_SDPA", + "vsa_sparsity": 0.5 + }, + "precision": { + "dit": "fp32" + } + }, + "pipeline": { + "flow_shift": 5.0 + }, + }, + "keys_without_typed_fields": { + "model_path": WAN_T2V, + "pipeline": { + "experimental": { + "master_port": 29600, + "refine_enabled": True, + "boundary_ratio": 0.5 + } + }, + }, + "ltx2_refine_lora_path": { + "model_path": LTX2, + "pipeline": { + "ltx2": { + "refine": { + "lora_path": "/checkpoints/refine_lora.safetensors" + } + } + }, + }, + "ltx2_refine_lora_disabled": { + "model_path": LTX2, + "pipeline": { + "ltx2": { + "refine": { + "lora_path": "" + } + } + }, + }, + "pipeline_config_json_path": { + "model_path": "FastVideo/FastHunyuan-diffusers", + "pipeline": { + "components": { + "pipeline_config_path": pipeline_json + } + }, + }, + "boundary_ratio": { + "model_path": WAN22_T2V, + "pipeline": { + "experimental": { + "boundary_ratio": 0.8 + } + }, + }, + "vae_tiling_typed": { + "model_path": LTX2, + "pipeline": { + "vae_tiling": False + }, + }, + "ltx2_vae_tile_sizes": { + "model_path": LTX2, + "pipeline": { + "ltx2": { + "vae_spatial_tile_size_in_pixels": 512, + "vae_temporal_tile_size_in_frames": 64 + } + }, + }, + } + cases += [SnapshotCase("config", name, _config_case(raw)) for name, raw in config_cases.items()] # argparse defaults differ from the dataclass defaults, so model_path_only is the CLI baseline. cli_cases = { "model_path_only": ["--model-path", WAN_T2V], diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/attention_precision_and_flow_shift.json b/fastvideo/tests/api/config_snapshot_goldens/config/attention_precision_and_flow_shift.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/attention_precision_and_flow_shift.json rename to fastvideo/tests/api/config_snapshot_goldens/config/attention_precision_and_flow_shift.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/boundary_ratio.json b/fastvideo/tests/api/config_snapshot_goldens/config/boundary_ratio.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/boundary_ratio.json rename to fastvideo/tests/api/config_snapshot_goldens/config/boundary_ratio.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/keys_without_typed_fields.json b/fastvideo/tests/api/config_snapshot_goldens/config/keys_without_typed_fields.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/keys_without_typed_fields.json rename to fastvideo/tests/api/config_snapshot_goldens/config/keys_without_typed_fields.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/ltx2_refine_lora_disabled.json b/fastvideo/tests/api/config_snapshot_goldens/config/ltx2_refine_lora_disabled.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/ltx2_refine_lora_disabled.json rename to fastvideo/tests/api/config_snapshot_goldens/config/ltx2_refine_lora_disabled.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/ltx2_refine_lora_path.json b/fastvideo/tests/api/config_snapshot_goldens/config/ltx2_refine_lora_path.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/ltx2_refine_lora_path.json rename to fastvideo/tests/api/config_snapshot_goldens/config/ltx2_refine_lora_path.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/ltx2_vae_tile_sizes.json b/fastvideo/tests/api/config_snapshot_goldens/config/ltx2_vae_tile_sizes.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/ltx2_vae_tile_sizes.json rename to fastvideo/tests/api/config_snapshot_goldens/config/ltx2_vae_tile_sizes.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/pipeline_config_json_path.json b/fastvideo/tests/api/config_snapshot_goldens/config/pipeline_config_json_path.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/pipeline_config_json_path.json rename to fastvideo/tests/api/config_snapshot_goldens/config/pipeline_config_json_path.json diff --git a/fastvideo/tests/api/config_snapshot_goldens/kwargs/vae_tiling_typed.json b/fastvideo/tests/api/config_snapshot_goldens/config/vae_tiling_typed.json similarity index 100% rename from fastvideo/tests/api/config_snapshot_goldens/kwargs/vae_tiling_typed.json rename to fastvideo/tests/api/config_snapshot_goldens/config/vae_tiling_typed.json diff --git a/fastvideo/tests/api/test_compat_translation.py b/fastvideo/tests/api/test_compat_translation.py index 2af59568a..60e96f156 100644 --- a/fastvideo/tests/api/test_compat_translation.py +++ b/fastvideo/tests/api/test_compat_translation.py @@ -5,9 +5,11 @@ PR 6. """ from __future__ import annotations +import pytest + from fastvideo.api.compat import ( + from_pretrained_kwargs_to_config, generator_config_to_fastvideo_args, - legacy_from_pretrained_to_config, request_to_sampling_param, ) from fastvideo.api.parser import parse_config @@ -15,13 +17,14 @@ from fastvideo.api.schema import CompileConfig, GenerationRequest, GeneratorConf from fastvideo.api.sampling_param import SamplingParam -class TestLegacyTorchCompileKwargsTranslation: - """Legacy ``torch_compile_kwargs={...}`` gets split across the four - first-class :class:`CompileConfig` fields and anything unknown falls - into ``extras``.""" +class TestFromPretrainedKwargsTranslation: + """The ``from_pretrained`` keyword ``torch_compile_kwargs={...}`` gets + split across the four first-class :class:`CompileConfig` fields and + anything unknown falls into ``extras``. Keywords outside the + ``from_pretrained`` set are rejected with their typed path.""" def test_empty_kwargs_produces_empty_extras(self) -> None: - config = legacy_from_pretrained_to_config( + config = from_pretrained_kwargs_to_config( "/models/ltx2", {"torch_compile_kwargs": {}}, ) @@ -29,6 +32,10 @@ class TestLegacyTorchCompileKwargsTranslation: assert compile_config.extras == {} assert compile_config.backend is None + def test_other_keyword_names_its_typed_path(self) -> None: + with pytest.raises(TypeError, match="ltx2_vae_tiling -> pipeline.vae_tiling"): + from_pretrained_kwargs_to_config("/models/ltx2", {"ltx2_vae_tiling": True}) + class TestCompileConfigRoundTrip: """typed CompileConfig -> FastVideoArgs.torch_compile_kwargs @@ -78,21 +85,9 @@ class TestCompileConfigRoundTrip: assert args.kwargs["torch_compile_kwargs"] == {} -class TestLegacyLtx2VaeTilingTranslation: - """``ltx2_vae_tiling`` flat kwarg promotes to - ``generator.pipeline.vae_tiling``; reverse direction emits the - legacy name back to FastVideoArgs.""" - - def test_true_round_trips(self) -> None: - config = legacy_from_pretrained_to_config( - "/models/ltx2", - {"ltx2_vae_tiling": True}, - ) - assert config.pipeline.vae_tiling is True - - def test_unset_stays_none(self) -> None: - config = legacy_from_pretrained_to_config("/models/ltx2", {}) - assert config.pipeline.vae_tiling is None +class TestLtx2VaeTilingFlattening: + """``generator.pipeline.vae_tiling`` reaches FastVideoArgs as the flat + keyword ``ltx2_vae_tiling``.""" def test_reverse_emits_legacy_name(self, monkeypatch) -> None: _stub_fastvideo_args_from_kwargs(monkeypatch) @@ -114,31 +109,12 @@ class TestLegacyLtx2VaeTilingTranslation: assert "ltx2_vae_tiling" not in args.kwargs -class TestLegacyTextEncoderCompileTranslation: - """``enable_torch_compile_text_encoder`` flat kwarg promotes to - ``generator.engine.compile.text_encoder_enabled``; reverse direction - emits the legacy name back onto the FastVideoArgs kwargs dict so +class TestTextEncoderCompileFlattening: + """``generator.engine.compile.text_encoder_enabled`` reaches the + FastVideoArgs kwargs dict as ``enable_torch_compile_text_encoder`` so realtime-runtime consumers can read it before FastVideoArgs filters unknown fields.""" - def test_forward_routes_to_compile_text_encoder_enabled(self) -> None: - config = legacy_from_pretrained_to_config( - "/models/ltx2", - {"enable_torch_compile_text_encoder": True}, - ) - assert config.engine.compile.text_encoder_enabled is True - - def test_false_round_trips(self) -> None: - config = legacy_from_pretrained_to_config( - "/models/ltx2", - {"enable_torch_compile_text_encoder": False}, - ) - assert config.engine.compile.text_encoder_enabled is False - - def test_unset_stays_none(self) -> None: - config = legacy_from_pretrained_to_config("/models/ltx2", {}) - assert config.engine.compile.text_encoder_enabled is None - def test_reverse_emits_legacy_name(self, monkeypatch) -> None: _stub_fastvideo_args_from_kwargs(monkeypatch) config = GeneratorConfig( diff --git a/fastvideo/tests/api/test_ltx2_gpu_pool_translation.py b/fastvideo/tests/api/test_ltx2_gpu_pool_translation.py index 302174589..576b827b0 100644 --- a/fastvideo/tests/api/test_ltx2_gpu_pool_translation.py +++ b/fastvideo/tests/api/test_ltx2_gpu_pool_translation.py @@ -1,142 +1,67 @@ # SPDX-License-Identifier: Apache-2.0 -"""gpu_pool-style flat-kwarg integration tests. +"""LTX-2 streaming-server config flattening tests. -Mirrors the ``load_kwargs`` dict that the FastVideo-internal -``ui/ltx2-streaming/server/gpu_pool.py`` passes to -``VideoGenerator.from_pretrained(**load_kwargs)`` and asserts that the -public typed ``GeneratorConfig`` surface (introduced across PRs 0-6) -can represent it end-to-end, with no fields silently falling through -to ``pipeline.experimental``. - -This is the parity guard PR 7.6 depends on: the public gpu_pool -upstream must be able to construct a typed ``GeneratorConfig`` without -knowing any legacy LTX-2 kwarg name, and downstream Dynamo -(``FastVideoArgGroup``) must be able to do the same. +``GPU_POOL_CONFIG`` is the typed ``GeneratorConfig`` form of the settings that the FastVideo-internal +``ui/ltx2-streaming/server/gpu_pool.py`` loads. The tests check that ``generator_config_to_fastvideo_args`` turns it +into the flat ``FastVideoArgs`` keywords that the LTX-2 runtime reads. """ from __future__ import annotations -from copy import deepcopy - import pytest from fastvideo.api.compat import ( + from_pretrained_kwargs_to_config, generator_config_to_fastvideo_args, - legacy_from_pretrained_to_config, + normalize_generator_config, ) -# Mirrors FastVideo-internal/ui/ltx2-streaming/server/gpu_pool.py -# :lines 233-260 (load_kwargs constructed for VideoGenerator.from_pretrained). -# -# One item from gpu_pool.py's load_kwargs is deliberately excluded: -# - ``pipeline_config=`` — an opaque Python -# object; internal mutates it in place (``dit_config.quant_config = -# NVFP4Config()``). The typed path for quantization is tracked in -# "Known Technical Debt" in PR plan.md; ``pipeline_config`` as an -# instance legitimately belongs in ``pipeline.experimental``. -# -# ``enable_torch_compile_text_encoder`` IS included below: its typed -# home is ``CompileConfig.text_encoder_enabled`` (added post-review). -# The legacy ``FastVideoArgs`` path does not yet consume it; the -# realtime runtime (PR 7.6) reads it off the kwargs dict before -# FastVideoArgs filtering. -GPU_POOL_LOAD_KWARGS = { - "config_model_path": "/models/ltx2-distilled/config", - "num_gpus": 1, - "dit_layerwise_offload": False, - "use_fsdp_inference": False, - "dit_cpu_offload": False, - "vae_cpu_offload": False, - "text_encoder_cpu_offload": False, - "pin_cpu_memory": True, - "ltx2_vae_tiling": False, - "ltx2_refine_enabled": True, - "ltx2_refine_upsampler_path": "/models/ltx2-distilled/spatial_upsampler", - "ltx2_refine_lora_path": "", - "ltx2_refine_num_inference_steps": 2, - "ltx2_refine_guidance_scale": 1.0, - "ltx2_refine_add_noise": True, - "enable_torch_compile": True, - "enable_torch_compile_text_encoder": True, - "torch_compile_kwargs": { - "backend": "inductor", - "fullgraph": True, - "mode": "max-autotune-no-cudagraphs", - "dynamic": False, +GPU_POOL_CONFIG = { + "model_path": "FastVideo/LTX2-Distilled-Diffusers", + "engine": { + "num_gpus": 1, + "use_fsdp_inference": False, + "offload": { + "dit": False, + "dit_layerwise": False, + "vae": False, + "text_encoder": False, + "pin_cpu_memory": True, + }, + "compile": { + "enabled": True, + "text_encoder_enabled": True, + "backend": "inductor", + "fullgraph": True, + "mode": "max-autotune-no-cudagraphs", + "dynamic": False, + }, + }, + "pipeline": { + "components": { + "config_root": "/models/ltx2-distilled/config", + "upsampler_weights": "/models/ltx2-distilled/spatial_upsampler", + }, + "vae_tiling": False, + # An empty refine LoRA path keeps the refine LoRA disabled; None would load the checkpoint default. + "ltx2": { + "refine": { + "lora_path": "" + } + }, + "preset_overrides": { + "refine": { + "enabled": True, + "num_inference_steps": 2, + "guidance_scale": 1.0, + "add_noise": True, + } + }, }, } -class TestGpuPoolForwardTranslation: - """gpu_pool flat kwargs -> typed GeneratorConfig.""" - - @pytest.fixture(scope="class") - def config(self): - return legacy_from_pretrained_to_config( - "FastVideo/LTX2-Distilled-Diffusers", - GPU_POOL_LOAD_KWARGS, - ) - - def test_model_path_set(self, config) -> None: - assert config.model_path == "FastVideo/LTX2-Distilled-Diffusers" - - def test_engine_basics(self, config) -> None: - assert config.engine.num_gpus == 1 - assert config.engine.use_fsdp_inference is False - - def test_offload_config(self, config) -> None: - assert config.engine.offload.dit is False - assert config.engine.offload.dit_layerwise is False - assert config.engine.offload.vae is False - assert config.engine.offload.text_encoder is False - assert config.engine.offload.pin_cpu_memory is True - - def test_compile_config_typed_fields_extracted(self, config) -> None: - compile_config = config.engine.compile - assert compile_config.enabled is True - assert compile_config.text_encoder_enabled is True - assert compile_config.backend == "inductor" - assert compile_config.fullgraph is True - assert compile_config.mode == "max-autotune-no-cudagraphs" - assert compile_config.dynamic is False - assert compile_config.extras == {} - - def test_vae_tiling_routed_to_pipeline(self, config) -> None: - assert config.pipeline.vae_tiling is False - - def test_config_model_path_routed_to_components(self, config) -> None: - assert config.pipeline.components.config_root == "/models/ltx2-distilled/config" - - def test_refine_upsampler_routed_to_components(self, config) -> None: - assert config.pipeline.components.upsampler_weights == ("/models/ltx2-distilled/spatial_upsampler") - - def test_empty_refine_lora_stays_disabled(self, config) -> None: - # gpu_pool passes "" to keep the refine LoRA disabled. None would - # load the checkpoint's default refine LoRA instead. - assert config.pipeline.ltx2.refine.lora_path == "" - assert config.pipeline.components.lora_path is None - - def test_refine_preset_overrides(self, config) -> None: - refine = config.pipeline.preset_overrides.get("refine", {}) - assert refine == { - "enabled": True, - "num_inference_steps": 2, - "guidance_scale": 1.0, - "add_noise": True, - } - - def test_no_experimental_leakage(self, config) -> None: - """Every gpu_pool kwarg should have a typed home — nothing should - silently fall through to ``pipeline.experimental``.""" - assert config.pipeline.experimental == {} - - -class TestGpuPoolReverseTranslation: - """typed GeneratorConfig -> FastVideoArgs kwargs reproduces the - original gpu_pool flat-kwarg shape. - - This is what lets PR 7.6 wire the public ``gpu_pool`` through - ``generator_config_to_fastvideo_args`` without the runtime noticing. - """ +class TestGpuPoolFlattening: + """The typed gpu_pool config -> the FastVideoArgs keywords that the LTX-2 runtime reads.""" @pytest.fixture def args_kwargs(self, monkeypatch): @@ -155,13 +80,12 @@ class TestGpuPoolReverseTranslation: monkeypatch.setattr(fva.FastVideoArgs, "from_kwargs", _capture) - config = legacy_from_pretrained_to_config( - "FastVideo/LTX2-Distilled-Diffusers", - GPU_POOL_LOAD_KWARGS, - ) - generator_config_to_fastvideo_args(config) + generator_config_to_fastvideo_args(normalize_generator_config(GPU_POOL_CONFIG)) return captured + def test_empty_refine_lora_path_reemitted(self, args_kwargs) -> None: + assert args_kwargs["ltx2_refine_lora_path"] == "" + def test_ltx2_refine_flags_reemitted(self, args_kwargs) -> None: assert args_kwargs["ltx2_refine_enabled"] is True assert args_kwargs["ltx2_refine_add_noise"] is True @@ -274,15 +198,17 @@ class TestCompileExtrasPreserved: monkeypatch.setattr(fva.FastVideoArgs, "from_kwargs", _capture) - kwargs = deepcopy(GPU_POOL_LOAD_KWARGS) - kwargs["torch_compile_kwargs"] = { - "backend": "inductor", - "options": { - "triton.cudagraphs": False + kwargs = { + "enable_torch_compile": True, + "torch_compile_kwargs": { + "backend": "inductor", + "options": { + "triton.cudagraphs": False + }, + "disable": False, }, - "disable": False, } - config = legacy_from_pretrained_to_config("FastVideo/LTX2-Distilled-Diffusers", kwargs) + config = from_pretrained_kwargs_to_config("FastVideo/LTX2-Distilled-Diffusers", kwargs) assert config.engine.compile.backend == "inductor" assert config.engine.compile.extras == { "options": { diff --git a/fastvideo/tests/contract/test_dreamverse_shape.py b/fastvideo/tests/contract/test_dreamverse_shape.py deleted file mode 100644 index 7df69f362..000000000 --- a/fastvideo/tests/contract/test_dreamverse_shape.py +++ /dev/null @@ -1,176 +0,0 @@ -# SPDX-License-Identifier: Apache-2.0 -"""Contract test: Dreamverse-style inputs normalize through the public -typed API without needing any private-only compatibility promise. - -The private Dreamverse UI server (``FastVideo-internal/ui/ltx2-streaming/ -server/gpu_pool.py``) has historically called -``VideoGenerator.from_pretrained(**load_kwargs)`` with a flat kwarg bag -containing LTX-2-specific names (``ltx2_refine_enabled``, -``ltx2_refine_upsampler_path``, etc.). PR 6 gave every one of those -kwargs a typed home under ``GeneratorConfig``. - -This test makes sure: - -1. The public typed API can represent everything Dreamverse currently - passes at init time (``legacy_from_pretrained_to_config``). -2. Private-only Dreamverse fields that don't belong on the public - surface either go to ``pipeline.experimental`` / ``request.extensions`` - (the documented escape hatch) or raise explicitly, rather than - silently becoming part of the public compatibility promise. - -Regression guard for the scoping rule in ``apirefactor.md`` §"Schema -Parity Requirement". -""" -from __future__ import annotations - -import pytest - -from fastvideo.api import ( - ComponentConfig, - CompileConfig, - GeneratorConfig, - GenerationRequest, -) -from fastvideo.api.compat import ( - legacy_from_pretrained_to_config, - normalize_generation_request, -) - - -def _dreamverse_load_kwargs() -> dict: - """The exact shape internal ``gpu_pool.py`` passes to - ``VideoGenerator.from_pretrained(**load_kwargs)`` today.""" - return { - "config_model_path": "/models/ltx2-config", - "ltx2_refine_enabled": True, - "ltx2_refine_upsampler_path": "/models/ltx2-refine", - "ltx2_refine_lora_path": "/models/ltx2-refine-lora", - "ltx2_refine_num_inference_steps": 2, - "ltx2_refine_guidance_scale": 1.0, - "ltx2_refine_add_noise": True, - "ltx2_vae_tiling": True, - "torch_compile_kwargs": { - "backend": "inductor", - "mode": "reduce-overhead", - "fullgraph": True, - }, - "dit_cpu_offload": False, - "vae_cpu_offload": False, - "text_encoder_cpu_offload": False, - "pin_cpu_memory": True, - "use_fsdp_inference": False, - "enable_torch_compile": True, - } - - -class TestDreamverseLoadKwargsShape: - """Every current Dreamverse init-time kwarg must land on a typed - field, not in the ``experimental`` escape hatch.""" - - def test_all_kwargs_land_on_typed_fields(self): - config = legacy_from_pretrained_to_config("/models/ltx2", _dreamverse_load_kwargs()) - assert isinstance(config, GeneratorConfig) - # None of the kwargs should have been routed to experimental. - assert config.pipeline.experimental == {}, ("Dreamverse kwargs leaked into pipeline.experimental: " - f"{config.pipeline.experimental}") - - def test_refine_enabled_reaches_preset_overrides(self): - config = legacy_from_pretrained_to_config("/models/ltx2", _dreamverse_load_kwargs()) - refine = config.pipeline.preset_overrides.get("refine") or {} - assert refine.get("enabled") is True - assert refine.get("add_noise") is True - assert refine.get("num_inference_steps") == 2 - assert refine.get("guidance_scale") == 1.0 - - def test_refine_assets_reach_component_config(self): - config = legacy_from_pretrained_to_config("/models/ltx2", _dreamverse_load_kwargs()) - assert isinstance(config.pipeline.components, ComponentConfig) - assert (config.pipeline.components.upsampler_weights == "/models/ltx2-refine") - assert config.pipeline.ltx2.refine.lora_path == "/models/ltx2-refine-lora" - assert config.pipeline.components.lora_path is None - assert config.pipeline.components.config_root == "/models/ltx2-config" - - def test_torch_compile_kwargs_reach_typed_fields(self): - config = legacy_from_pretrained_to_config("/models/ltx2", _dreamverse_load_kwargs()) - assert isinstance(config.engine.compile, CompileConfig) - assert config.engine.compile.enabled is True - assert config.engine.compile.backend == "inductor" - assert config.engine.compile.mode == "reduce-overhead" - assert config.engine.compile.fullgraph is True - # extras should be empty — all four common kwargs are first class. - assert config.engine.compile.extras == {} - - def test_uncommon_compile_kwargs_fall_to_extras(self): - kwargs = _dreamverse_load_kwargs() - kwargs["torch_compile_kwargs"] = { - **kwargs["torch_compile_kwargs"], - "options": { - "epilogue_fusion": True - }, - } - config = legacy_from_pretrained_to_config("/models/ltx2", kwargs) - assert config.engine.compile.extras == { - "options": { - "epilogue_fusion": True - }, - } - - def test_vae_tiling_reaches_pipeline_selection(self): - config = legacy_from_pretrained_to_config("/models/ltx2", _dreamverse_load_kwargs()) - assert config.pipeline.vae_tiling is True - - def test_offload_fields_reach_typed_offload_config(self): - config = legacy_from_pretrained_to_config("/models/ltx2", _dreamverse_load_kwargs()) - assert config.engine.offload.dit is False - assert config.engine.offload.vae is False - assert config.engine.offload.text_encoder is False - assert config.engine.offload.pin_cpu_memory is True - - -class TestDreamversePrivateOnlyFields: - """Dreamverse carries a handful of private-only names (e.g. legacy - internal aliases). These must NOT silently turn into a public - compatibility promise — the documented contract is that unknown - fields land on ``pipeline.experimental`` so integrators see them - but FastVideo does not promise to preserve them.""" - - def test_unknown_kwarg_routes_to_experimental(self): - kwargs = _dreamverse_load_kwargs() - kwargs["dreamverse_internal_only_flag"] = "private" - config = legacy_from_pretrained_to_config("/models/ltx2", kwargs) - assert config.pipeline.experimental == { - "dreamverse_internal_only_flag": "private", - } - - -class TestDreamverseRequestShape: - """The per-segment Dreamverse request path mirrors OpenAI's shape - plus a few LTX-2 knobs. All of them must have a typed home.""" - - def test_return_state_reaches_output_config(self): - """PR 7 added ``output.return_state`` — must survive the legacy - translation path so Dreamverse callers can opt in.""" - request = GenerationRequest( - prompt="x", - output=__import__("fastvideo.api", fromlist=["OutputConfig"]).OutputConfig(return_state=True), - ) - normalized = normalize_generation_request(request) - assert normalized.output.return_state is True - - -class TestDreamverseNoPrivateImports: - """The public entry points must not force a Dreamverse integrator - to import from ``fastvideo.pipelines.*`` or other internal paths.""" - - @pytest.mark.parametrize( - "import_path", - [ - "fastvideo", - "fastvideo.api", - "fastvideo.api.compat", # public in that it's re-exported - ], - ) - def test_public_imports_resolve(self, import_path): - import importlib - - importlib.import_module(import_path) diff --git a/fastvideo/tests/entrypoints/test_video_generator.py b/fastvideo/tests/entrypoints/test_video_generator.py index a3aaf9f21..e8993da41 100644 --- a/fastvideo/tests/entrypoints/test_video_generator.py +++ b/fastvideo/tests/entrypoints/test_video_generator.py @@ -671,22 +671,18 @@ def test_from_pretrained_convenience_kwargs_do_not_warn(monkeypatch): assert generator.config.engine.num_gpus == 4 -def test_from_pretrained_legacy_only_kwargs_warn(monkeypatch): +def test_from_pretrained_rejects_other_kwargs_with_their_config_path(monkeypatch): captured = _patch_from_fastvideo_args(monkeypatch) _patch_fastvideo_args_from_kwargs(monkeypatch) - with pytest.warns(DeprecationWarning, match="legacy-only kwargs"): - generator = VideoGenerator.from_pretrained( + with pytest.raises(TypeError, match="workload_type -> pipeline.workload_type"): + VideoGenerator.from_pretrained( "test-model", num_gpus=4, workload_type="t2v", ) - assert captured["fastvideo_args"].model_path == "test-model" - assert captured["fastvideo_args"].num_gpus == 4 - assert captured["fastvideo_args"].workload_type.value == "t2v" - assert generator.config is not None - assert generator.config.pipeline.workload_type == "t2v" + assert "fastvideo_args" not in captured def test_generate_uses_typed_request_path(monkeypatch):