Every parameter, attribute, and keyword argument that carries the runtime config is named resolved_config instead of fastvideo_args, including the RPC kwargs keys between the executors and the workers. The module path fastvideo.fastvideo_args is unchanged. The rename is mechanical (a token-level rewrite) plus yapf line wrapping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012M91wnVFmPEvJ9h39r5BH7
361 lines
16 KiB
Python
361 lines
16 KiB
Python
# SPDX-License-Identifier: Apache-2.0
|
|
"""The resolved runtime config: materialization, flat names, runtime state, device policy, overrides, and roots."""
|
|
from __future__ import annotations
|
|
|
|
import dataclasses
|
|
import json
|
|
import pickle
|
|
from contextlib import nullcontext
|
|
from types import SimpleNamespace
|
|
|
|
import pytest
|
|
import torch
|
|
|
|
from fastvideo.api import device_policy
|
|
from fastvideo.api.compat import generator_config_to_fastvideo_args
|
|
from fastvideo.api.errors import ConfigValidationError
|
|
from fastvideo.api.inference_resolution import resolve_inference_config
|
|
from fastvideo.api.parser import parse_config
|
|
from fastvideo.api.schema import ExecutionMode, GeneratorConfig, WorkloadType
|
|
from fastvideo.api.training_schema import (PreprocessRunConfig, TrainingRunConfig, load_resolved_run_config,
|
|
resolve_preprocess_config, resolve_training_config)
|
|
from fastvideo.fastvideo_args import FastVideoArgs
|
|
from fastvideo.tests.api.config_snapshot import isolated_environment, to_jsonable
|
|
|
|
WAN_T2V = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers"
|
|
LTX2 = "FastVideo/LTX2-Distilled-Diffusers"
|
|
FASTH3 = "FastVideo/FastVideo-FastH3-8-Step-V2"
|
|
|
|
|
|
def _resolve(raw, env_values=None):
|
|
with isolated_environment(env_values):
|
|
return resolve_inference_config(raw)
|
|
|
|
|
|
def test_resolution_builds_and_freezes_the_pipeline_config_once(monkeypatch):
|
|
from fastvideo.configs.pipelines.base import PipelineConfig
|
|
|
|
builds = []
|
|
original = PipelineConfig.from_kwargs.__func__
|
|
monkeypatch.setattr(PipelineConfig, "from_kwargs",
|
|
classmethod(lambda cls, kwargs: builds.append(dict(kwargs)) or original(cls, kwargs)))
|
|
resolved = _resolve({"model_path": WAN_T2V, "pipeline": {"flow_shift": 5.0, "dit": {"prefix": "Probe"}}})
|
|
|
|
assert len(builds) == 1
|
|
assert type(resolved.pipeline_config).__name__ == "WanT2V480PConfig"
|
|
assert resolved.pipeline_config.flow_shift == resolved.pipeline.flow_shift == 5.0
|
|
assert resolved.pipeline_config.dit_config.prefix == "Probe"
|
|
with pytest.raises(AttributeError, match="read-only"):
|
|
resolved.pipeline_config.flow_shift = 1.0
|
|
|
|
|
|
@pytest.mark.parametrize("raw", [
|
|
{
|
|
"model_path": WAN_T2V,
|
|
"engine": {
|
|
"num_gpus": 2
|
|
},
|
|
"pipeline": {
|
|
"experimental": {
|
|
"master_port": 29600,
|
|
"prompt_txt": "prompts.txt",
|
|
"refine_enabled": True
|
|
}
|
|
},
|
|
},
|
|
{
|
|
"model_path": LTX2,
|
|
"pipeline": {
|
|
"preset_overrides": {
|
|
"refine": {
|
|
"enabled": True,
|
|
"num_inference_steps": 2,
|
|
"add_noise": False
|
|
}
|
|
},
|
|
"ltx2": {
|
|
"vae_spatial_tile_size_in_pixels": 512
|
|
},
|
|
},
|
|
},
|
|
])
|
|
def test_flat_names_return_the_values_that_fastvideo_args_held(raw):
|
|
resolved = _resolve(raw)
|
|
with isolated_environment():
|
|
resolved_config = generator_config_to_fastvideo_args(resolved)
|
|
|
|
# disable_autocast and boundary_ratio held their FastVideoArgs defaults; the typed field holds the input.
|
|
skipped = {"pipeline_config", "disable_autocast", "boundary_ratio"}
|
|
for config_field in dataclasses.fields(FastVideoArgs):
|
|
if config_field.name not in skipped:
|
|
expected = to_jsonable(getattr(resolved_config, config_field.name))
|
|
assert to_jsonable(getattr(resolved, config_field.name)) == expected, config_field.name
|
|
assert to_jsonable(resolved.pipeline_config) == to_jsonable(resolved_config.pipeline_config)
|
|
assert resolved.workload_type is WorkloadType.T2V
|
|
assert resolved.mode is ExecutionMode.INFERENCE and resolved.inference_mode and not resolved.training_mode
|
|
|
|
|
|
def test_unknown_and_training_only_names_raise_attribute_error():
|
|
resolved = _resolve({"model_path": WAN_T2V})
|
|
|
|
assert getattr(resolved, "log_level_progress", "default") == "default"
|
|
assert not hasattr(resolved, "_loading_teacher_critic_model")
|
|
with pytest.raises(AttributeError, match="exists on TrainingRunConfig"):
|
|
resolved.learning_rate
|
|
assert resolved.preprocess_config is None
|
|
|
|
|
|
def test_runtime_state_is_shared_by_overrides_and_survives_pickling():
|
|
resolved = _resolve({"model_path": WAN_T2V, "pipeline": {"experimental": {"ray_runtime_env": {"pip": ["x"]}}}})
|
|
|
|
resolved.model_loaded["vae"] = False
|
|
resolved.model_paths["transformer"] = "/weights/transformer"
|
|
resolved.ray_placement_group = "placement-group"
|
|
overridden = resolved.with_override("test:source", {"engine.offload.vae": False})
|
|
copy = pickle.loads(pickle.dumps(overridden))
|
|
|
|
assert overridden.model_loaded is resolved.model_loaded
|
|
assert copy.model_loaded == {"transformer": True, "vae": False, "upsampler": True}
|
|
assert copy.model_paths == {"transformer": "/weights/transformer"}
|
|
assert copy.ray_placement_group == "placement-group"
|
|
assert copy.ray_runtime_env == {"pip": ["x"]}
|
|
assert copy.pipeline_config._frozen and copy.vae_cpu_offload is False and resolved.vae_cpu_offload is True
|
|
with pytest.raises(AttributeError, match="read-only"):
|
|
resolved.num_gpus = 4
|
|
|
|
|
|
def test_override_of_a_typed_home_updates_its_pipeline_config_mirror():
|
|
resolved = _resolve({"model_path": WAN_T2V})
|
|
|
|
overridden = resolved.with_override("checkpoint:test", {"pipeline.dmd_denoising_steps": [999, 500]})
|
|
|
|
assert overridden.pipeline.dmd_denoising_steps == (999, 500)
|
|
assert overridden.pipeline_config is resolved.pipeline_config
|
|
assert overridden.pipeline_config.dmd_denoising_steps == [999, 500]
|
|
assert overridden.provenance("pipeline.dmd_denoising_steps").source == "checkpoint:test"
|
|
|
|
|
|
def test_unified_memory_policy_returns_recorded_overrides_once(monkeypatch):
|
|
resolved = _resolve({"model_path": WAN_T2V, "engine": {"offload": {"dit_layerwise": False}}})
|
|
monkeypatch.setattr("fastvideo.platforms.current_platform.has_unified_memory", lambda device_id: device_id == 1)
|
|
monkeypatch.setattr("fastvideo.platforms.current_platform.get_device_name", lambda device_id: "NVIDIA GB10")
|
|
monkeypatch.setattr("fastvideo.platforms.current_platform.is_mps", lambda: False)
|
|
|
|
decided = device_policy.finalize_device_offload_policy(resolved, 1)
|
|
again = device_policy.finalize_device_offload_policy(decided, 1)
|
|
|
|
assert [source for source, _ in decided.override_log] == [
|
|
"device_policy:unified_memory",
|
|
"device_policy:lazy_module_load",
|
|
]
|
|
assert decided.override_log[0][1] == {
|
|
"engine.offload.dit": False,
|
|
"engine.offload.text_encoder": False,
|
|
"engine.offload.image_encoder": False,
|
|
"engine.offload.vae": False,
|
|
}
|
|
assert decided.lazy_module_load is True and decided.dit_cpu_offload is False
|
|
assert again.override_log == decided.override_log
|
|
assert resolved.override_log == () and resolved.dit_cpu_offload is True
|
|
assert device_policy.offload_disabled_on_unified_memory(1, "text_encoder_cpu_offload")
|
|
assert not device_policy.offload_disabled_on_unified_memory(0, "text_encoder_cpu_offload")
|
|
assert device_policy.finalize_device_offload_policy(resolved, 0).lazy_module_load is False
|
|
|
|
|
|
def test_layerwise_offload_turns_off_conflicting_modes():
|
|
resolved = _resolve({"model_path": WAN_T2V, "engine": {"use_fsdp_inference": True}})
|
|
|
|
decided = device_policy.resolve_device_offload_conflicts(resolved)
|
|
|
|
assert [values for _, values in decided.override_log] == [{
|
|
"engine.use_fsdp_inference": False
|
|
}, {
|
|
"engine.offload.dit": False
|
|
}]
|
|
|
|
|
|
def test_direct_pipeline_keeps_the_policy_result_of_a_resolved_config(monkeypatch):
|
|
import fastvideo.pipelines.composed_pipeline_base as composed_pipeline_base
|
|
from fastvideo.pipelines.composed_pipeline_base import ComposedPipelineBase
|
|
|
|
class _Pipeline(ComposedPipelineBase):
|
|
|
|
def load_modules(self, resolved_config, loaded_modules=None):
|
|
self.loaded_with = resolved_config
|
|
return {}
|
|
|
|
def create_pipeline_stages(self, resolved_config):
|
|
pass
|
|
|
|
profiler = SimpleNamespace(region=lambda name: nullcontext())
|
|
monkeypatch.setattr(composed_pipeline_base, "maybe_init_distributed_environment_and_model_parallel",
|
|
lambda *args: None)
|
|
monkeypatch.setattr(composed_pipeline_base, "get_local_torch_device", lambda: torch.device("cuda:2"))
|
|
monkeypatch.setattr(composed_pipeline_base, "get_world_group", lambda: SimpleNamespace(local_rank=2))
|
|
monkeypatch.setattr(composed_pipeline_base, "get_or_create_profiler", lambda trace_dir: profiler)
|
|
monkeypatch.setattr("fastvideo.platforms.current_platform.has_unified_memory", lambda device_id: True)
|
|
monkeypatch.setattr("fastvideo.platforms.current_platform.is_mps", lambda: False)
|
|
resolved = _resolve({"model_path": WAN_T2V})
|
|
|
|
pipeline = _Pipeline("unused", resolved, required_config_modules=[])
|
|
|
|
assert pipeline.resolved_config is pipeline.loaded_with
|
|
assert pipeline.resolved_config.lazy_module_load is True and resolved.lazy_module_load is None
|
|
|
|
|
|
def test_ltx2_checkpoint_refine_defaults_rebind_the_pipeline_config(tmp_path):
|
|
from fastvideo.pipelines.basic.ltx2.ltx2_pipeline import LTX2Pipeline
|
|
|
|
resolved = _resolve({"model_path": LTX2})
|
|
pipeline = object.__new__(LTX2Pipeline)
|
|
pipeline.model_path = str(tmp_path)
|
|
pipeline.resolved_config = resolved
|
|
pipeline._required_config_modules = list(LTX2Pipeline._required_config_modules)
|
|
model_index = {name: ["diffusers", "Model"] for name in pipeline._required_config_modules}
|
|
pipeline._load_config = lambda model_path: {
|
|
"_class_name": "LTX2Pipeline",
|
|
"_diffusers_version": "0",
|
|
"fastvideo_refine_lora_path": "FastVideo/LTX2-Distilled-LoRA",
|
|
"fastvideo_refine_num_inference_steps": 2,
|
|
**model_index,
|
|
}
|
|
|
|
pipeline.load_modules(resolved, {name: object() for name in model_index})
|
|
|
|
rebound = pipeline.resolved_config
|
|
assert rebound is not resolved
|
|
assert rebound.override_log == (("checkpoint:model_index.json", {
|
|
"pipeline.ltx2.refine.lora_path": "FastVideo/LTX2-Distilled-LoRA",
|
|
"pipeline.ltx2.refine.num_inference_steps": 2,
|
|
}), )
|
|
assert rebound.ltx2_refine_lora_path == "FastVideo/LTX2-Distilled-LoRA"
|
|
assert rebound.ltx2_refine_num_inference_steps == 2 and resolved.ltx2_refine_num_inference_steps == 3
|
|
|
|
|
|
def test_minimax_h3_checkpoint_schedule_rebinds_and_reaches_the_first_forward(tmp_path, monkeypatch):
|
|
from fastvideo.models.schedulers.scheduling_minimax_h3 import MiniMaxH3Scheduler
|
|
from fastvideo.pipelines.basic.minimax_h3.minimax_h3_pipeline import MiniMaxH3ModularPipeline
|
|
from fastvideo.pipelines.composed_pipeline_base import ComposedPipelineBase
|
|
|
|
steps = [999, 874, 749, 624, 500, 375, 250, 125]
|
|
(tmp_path / "fastvideo_inference.json").write_text(
|
|
json.dumps({
|
|
"schema_version": "fasth3-inference-contract-v1",
|
|
"dmd_denoising_steps": steps,
|
|
"num_inference_steps": 9,
|
|
"transformer_forwards": 8,
|
|
}))
|
|
resolved = _resolve({"model_path": FASTH3})
|
|
pipeline = object.__new__(MiniMaxH3ModularPipeline)
|
|
pipeline.model_path = str(tmp_path)
|
|
pipeline.resolved_config = resolved
|
|
pipeline.modules = {"scheduler": MiniMaxH3Scheduler(shift=10.0), "audio_scheduler": MiniMaxH3Scheduler(shift=3.0)}
|
|
pipeline.post_init_called = False
|
|
seen = []
|
|
monkeypatch.setattr(ComposedPipelineBase, "post_init",
|
|
lambda self: (setattr(self, "post_init_called", True), self._load_checkpoint_schedule(
|
|
self.resolved_config)))
|
|
monkeypatch.setattr(MiniMaxH3ModularPipeline, "_defer_denoise_modules", lambda self, args: False)
|
|
monkeypatch.setattr(ComposedPipelineBase, "_stages", [], raising=False)
|
|
monkeypatch.setattr(ComposedPipelineBase, "_stage_name_mapping", {}, raising=False)
|
|
monkeypatch.setattr(ComposedPipelineBase, "stages",
|
|
property(lambda self: [lambda batch, args: seen.append(args) or batch]))
|
|
|
|
pipeline.forward(SimpleNamespace(), pipeline.resolved_config)
|
|
|
|
assert seen == [pipeline.resolved_config] and pipeline.resolved_config is not resolved
|
|
assert pipeline.resolved_config.pipeline.dmd_denoising_steps == tuple(steps)
|
|
assert pipeline.resolved_config.pipeline_config.dmd_denoising_steps == steps
|
|
assert pipeline.resolved_config.provenance("pipeline.dmd_denoising_steps").source == (
|
|
"checkpoint:fastvideo_inference.json")
|
|
|
|
|
|
def test_training_root_uses_command_line_defaults_and_flat_formats(tmp_path):
|
|
config_path = tmp_path / "train.json"
|
|
config_path.write_text(
|
|
json.dumps({
|
|
"model_path": WAN_T2V,
|
|
"engine": {
|
|
"parallelism": {
|
|
"sp_size": 1,
|
|
"hsdp_shard_dim": 1
|
|
},
|
|
"precision": {
|
|
"dit": "fp32"
|
|
}
|
|
},
|
|
"training": {
|
|
"optimizer": {
|
|
"learning_rate": 1e-5
|
|
},
|
|
"validation": {
|
|
"sampling_steps": [8, 50],
|
|
"guidance_scale": 6.0
|
|
},
|
|
"lora": {
|
|
"rank": 32
|
|
},
|
|
},
|
|
}))
|
|
|
|
with isolated_environment():
|
|
resolved = load_resolved_run_config(TrainingRunConfig,
|
|
["--config", str(config_path), "--training.optimizer.lr_warmup_steps", "5"],
|
|
mode=ExecutionMode.DISTILLATION)
|
|
|
|
assert type(resolved.to_config()) is TrainingRunConfig
|
|
assert resolved.mode is ExecutionMode.DISTILLATION and resolved.training_mode and not resolved.inference_mode
|
|
assert (resolved.dit_cpu_offload, resolved.dit_layerwise_offload, resolved.pin_cpu_memory) == (False, False, False)
|
|
assert (resolved.seed, resolved.ema_decay, resolved.lr_warmup_steps, resolved.learning_rate) == (42, 0.999, 5, 1e-5)
|
|
assert resolved.betas == "0.9,0.999" and resolved.validation_sampling_steps == "8,50"
|
|
assert resolved.validation_guidance_scale == "6.0"
|
|
assert resolved.lora_alpha == 32 and resolved.provenance("training.lora.alpha").source == (
|
|
"derive_lora_alpha_from_rank")
|
|
assert resolved.pretrained_model_name_or_path == WAN_T2V and resolved.init_weights_from_safetensors is None
|
|
with pytest.raises(AttributeError, match="no readers"):
|
|
resolved.mixed_precision
|
|
|
|
|
|
def test_training_root_requires_parallel_sizes():
|
|
with isolated_environment(), pytest.raises(ValueError, match="sp_size must be set for training"):
|
|
resolve_training_config({"model_path": WAN_T2V, "engine": {"parallelism": {"hsdp_shard_dim": 1}}})
|
|
|
|
|
|
def test_preprocess_root_fills_model_path_loads_the_encoder_and_validates():
|
|
with isolated_environment():
|
|
resolved = resolve_preprocess_config({
|
|
"model_path": WAN_T2V,
|
|
"preprocess": {
|
|
"dataset_path": "/data",
|
|
"dataset_type": "merged"
|
|
}
|
|
})
|
|
with pytest.raises(ValueError, match="dataset_path must be set"):
|
|
resolve_preprocess_config({"model_path": WAN_T2V})
|
|
|
|
assert type(resolved.to_config()) is PreprocessRunConfig
|
|
assert resolved.mode is ExecutionMode.PREPROCESS and resolved.inference_mode
|
|
assert resolved.preprocess_config.model_path == WAN_T2V
|
|
assert resolved.preprocess_config.dataset_type.value == "merged"
|
|
assert resolved.pipeline_config.vae_config.load_encoder is True
|
|
|
|
|
|
def test_parser_parses_enum_values_and_rejects_unknown_ones():
|
|
config = parse_config(GeneratorConfig, {"model_path": WAN_T2V, "mode": "preprocess",
|
|
"pipeline": {"workload_type": "i2v"}})
|
|
|
|
assert config.mode is ExecutionMode.PREPROCESS and config.pipeline.workload_type is WorkloadType.I2V
|
|
assert parse_config(GeneratorConfig, {"model_path": WAN_T2V, "mode": ExecutionMode.FINETUNING}).mode is (
|
|
ExecutionMode.FINETUNING)
|
|
with pytest.raises(ConfigValidationError, match="pipeline.workload_type"):
|
|
parse_config(GeneratorConfig, {"model_path": WAN_T2V, "pipeline": {"workload_type": "x2y"}})
|
|
|
|
|
|
def test_decode_strategy_validation_matches_the_parallel_vae_module():
|
|
from fastvideo.api.inference_resolution import VAE_PARALLEL_DECODE_STRATEGIES
|
|
from fastvideo.models.vaes.minimax_h3_parallel import DECODE_GATHER_STRATEGIES
|
|
|
|
assert VAE_PARALLEL_DECODE_STRATEGIES == tuple(DECODE_GATHER_STRATEGIES)
|
|
with pytest.raises(ValueError, match="vae_parallel_decode_strategy"):
|
|
_resolve({"model_path": WAN_T2V}, {"FASTVIDEO_VAE_PARALLEL_DECODE_STRATEGY": "scatter"})
|