Compare commits

...
Author SHA1 Message Date
SolitaryThinker cb46d63f07 [bugfix]: unset GenerationRequest fields inherit model presets
Directly-constructed GenerationRequests marked every schema default as
explicit (normalize_generation_request bound the full serialization when
_fastvideo_explicit_paths was absent), so request_to_sampling_param
overwrote model preset values with schema defaults: FastWan DMD ran
3->50 steps, TurboDiffusion 4->50, stable-audio 100 steps/CFG7 ->
50/CFG1, and 1.3B Wan examples rendered 720x1280x125f with CFG off.

Fix at the root: SamplingConfig fields now default to None = "inherit
the model preset". None leaves are never bound as explicit paths — for
directly-constructed requests (normalize_generation_request) and for
parsed YAML/JSON (parse_config treats explicit null as unset, so
widening the types cannot let nulls stomp presets either). Dicts
emptied by the pruning are dropped too, so None-valued extensions
cannot resurface via the live-attribute read in
explicit_request_updates. The _SCHEMA_DEFAULT_UPDATES tolerance hack is
deleted (an explicitly-set field unsupported by a model's SamplingParam
now always fails loudly), and return_state is skipped in the update
loop since it is already translated to return_continuation_state.

Consumers of raw (pre-resolution) sampling fields are guarded: the
streaming server validates operator-pinned default_request.sampling
before the multi-minute model boot (run_server) and in build_app; its
segment negative_prompt merge preserves an explicit "" clear; the mock
server supplies concrete fallbacks; generate_async resolves the preset
so its advertised total_steps matches the actual run; the ray-serve
gradio demo passes "" to keep its no-negative-prompt default. Stale
schema-default claims in video_api/ServeConfig/openai.md docs updated.

Tests: test_preset_inheritance.py locks the contract (bare request
inherits preset; partial override; explicit value equal to an old
schema default wins; negative_prompt None-inherits/""-clears; parsed
nulls inherit; None-valued extensions are dropped; unsupported explicit
field raises) plus a build_app pin-validation test.
2026-07-11 03:26:33 -07:00
SolitaryThinker bbbb7ab021 [refactor]: migrate examples/scripts to typed VideoGenerator API; rename api/compat.py to api/translation.py
Move all examples/inference and scripts off the legacy
VideoGenerator.from_pretrained(model, **kwargs) +
generate_video(prompt, **kwargs) surface onto the typed
from_config(GeneratorConfig(...)) + generate(GenerationRequest(...))
convention, including the Kandinsky-5 and DreamX-World examples added
on main after the original migration. git-mv api/compat.py to
api/translation.py ('compat' implied a temporary shim; the forward
translation is its honest permanent role) and repoint all importers,
tests, and doc links.
2026-07-11 03:26:24 -07:00
94 changed files with 2764 additions and 1521 deletions
+1 -1
View File
@@ -330,7 +330,7 @@ at FastVideo's CI — before the Dynamo-side integration even knows.
internal; presets identify them by name on internal; presets identify them by name on
`PipelineSelection.preset`). `PipelineSelection.preset`).
* `fastvideo.fastvideo_args.FastVideoArgs` (legacy compat type). * `fastvideo.fastvideo_args.FastVideoArgs` (legacy compat type).
* `fastvideo.api.compat.*` private helpers * `fastvideo.api.translation.*` private helpers
(`_validate_continuation_state` etc.) — the public boundary is (`_validate_continuation_state` etc.) — the public boundary is
`VideoGenerator` + `fastvideo.api`. `VideoGenerator` + `fastvideo.api`.
* Any flat legacy LTX-2 kwarg (`ltx2_refine_upsampler_path`, * Any flat legacy LTX-2 kwarg (`ltx2_refine_upsampler_path`,
+8 -6
View File
@@ -48,15 +48,17 @@ highest first:
`request.model_fields_set` (Pydantic v2). Unset fields do not count, `request.model_fields_set` (Pydantic v2). Unset fields do not count,
even if the Pydantic model has a schema default for them. even if the Pydantic model has a schema default for them.
2. **`ServeConfig.default_request` (operator-explicit)** — projected via 2. **`ServeConfig.default_request` (operator-explicit)** — projected via
[`explicit_request_updates()`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/api/compat.py); [`explicit_request_updates()`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/api/translation.py);
only fields the operator actually wrote into the YAML count as only fields the operator actually wrote into the YAML count as
defaults. Every other field inherits the schema default rather than defaults (an explicit `null` counts as unset). Every other sampling
being pinned. field stays `None` — "inherit the model preset" — and other sections
keep their schema defaults without being pinned.
3. **Hardcoded fallback** — e.g. `fps = 24`. 3. **Hardcoded fallback** — e.g. `fps = 24`.
The gate matters: both surfaces carry schema defaults. Without The gate matters: the Pydantic surface carries schema defaults and the
`model_fields_set` / explicit-path tracking, schema defaults would dataclass surface carries non-None defaults outside `sampling`. Without
masquerade as intent and silently shadow the other side. `model_fields_set` / explicit-path tracking, defaults would masquerade
as intent and silently shadow the other side.
See [`video_api.py::_build_generation_kwargs`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/entrypoints/openai/video_api.py) See [`video_api.py::_build_generation_kwargs`](https://github.com/hao-ai-lab/FastVideo/blob/main/fastvideo/entrypoints/openai/video_api.py)
for the canonical implementation; the per-request assembly lives there, for the canonical implementation; the per-request assembly lives there,
+7 -4
View File
@@ -39,17 +39,20 @@ All you need to generate videos using multi-gpus from state-of-the-art diffusion
```python ```python
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import EngineConfig, GenerationRequest, GeneratorConfig
def main(): def main():
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
engine=EngineConfig(num_gpus=1),
)
) )
prompt = ("A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " prompt = ("A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones.") "natural light filtering through the petals. Mid-shot, warm and cheerful tones.")
video = generator.generate_video(prompt) result = generator.generate(GenerationRequest(prompt=prompt))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+23 -18
View File
@@ -1,6 +1,7 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
# from fastvideo.api.sampling_param import SamplingParam EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig, OutputConfig,
)
OUTPUT_PATH = "video_samples" OUTPUT_PATH = "video_samples"
def main(): def main():
@@ -8,29 +9,32 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" dit=False,
# image_encoder_cpu_offload=False, vae=False,
text_encoder=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder=False,
),
),
)
) )
# sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
# sampling_param.num_frames = 45
# sampling_param.image_path = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/astronaut.jpg"
# Generate videos with the same simple API, regardless of GPU count # Generate videos with the same simple API, regardless of GPU count
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True) video = generator.generate(
# video = generator.generate_video(prompt, sampling_param=sampling_param, output_path="wan_t2v_videos/") GenerationRequest(prompt=prompt, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True)))
# Generate another video with a different prompt, without reloading the # Generate another video with a different prompt, without reloading the
# model! # model!
@@ -40,7 +44,8 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, " "the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True) video2 = generator.generate(
GenerationRequest(prompt=prompt2, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True)))
if __name__ == "__main__": if __name__ == "__main__":
+27 -19
View File
@@ -1,24 +1,30 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
)
def main(): def main():
# Point this to your local diffusers model dir (or replace with a HF model ID). # Point this to your local diffusers model dir (or replace with a HF model ID).
model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers" model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers"
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_path, GeneratorConfig(
num_gpus=1, model_path=model_path,
use_fsdp_inference=False, # set True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
) )
sampling_param = SamplingParam.from_pretrained(model_path)
# image2world example from official repo # image2world example from official repo
image_path = "assets/images/bus_terminal.jpg" image_path = "assets/images/bus_terminal.jpg"
@@ -33,13 +39,16 @@ def main():
"Overhead signage in Chinese characters remains illuminated, enhancing the vibrant, urban night scene." "Overhead signage in Chinese characters remains illuminated, enhancing the vibrant, urban night scene."
) )
generator.generate_video( generator.generate(
prompt, GenerationRequest(
sampling_param=sampling_param, prompt=prompt,
image_path=str(image_path), inputs=InputConfig(image_path=str(image_path)),
num_cond_frames=1, output=OutputConfig(
output_path="outputs_video/cosmos2_5_i2w.mp4", output_path="outputs_video/cosmos2_5_i2w.mp4",
save_video=True, save_video=True,
),
extensions={"num_cond_frames": 1},
)
) )
generator.shutdown() generator.shutdown()
@@ -47,4 +56,3 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+25 -20
View File
@@ -1,24 +1,29 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig, OutputConfig,
)
def main(): def main():
# Point this to your local diffusers model dir (or replace with a HF model ID). # Point this to your local diffusers model dir (or replace with a HF model ID).
model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers" model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers"
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_path, GeneratorConfig(
num_gpus=1, model_path=model_path,
use_fsdp_inference=False, # set True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
) )
# Load default sampling parameters (negative_prompt, resolution, steps, etc.)
sampling_param = SamplingParam.from_pretrained(model_path)
prompt = ( prompt = (
"A high-definition video captures the precision of robotic welding in an industrial setting. " "A high-definition video captures the precision of robotic welding in an industrial setting. "
"The first frame showcases a robotic arm, equipped with a welding torch, positioned over a large metal structure. " "The first frame showcases a robotic arm, equipped with a welding torch, positioned over a large metal structure. "
@@ -34,11 +39,14 @@ def main():
"underscoring the ongoing nature of the welding operation." "underscoring the ongoing nature of the welding operation."
) )
generator.generate_video( generator.generate(
prompt, GenerationRequest(
sampling_param=sampling_param, prompt=prompt,
output_path="outputs_video/cosmos2_5_t2w.mp4", output=OutputConfig(
save_video=True, output_path="outputs_video/cosmos2_5_t2w.mp4",
save_video=True,
),
)
) )
generator.shutdown() generator.shutdown()
@@ -46,6 +54,3 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+28 -21
View File
@@ -1,23 +1,29 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig,
)
def main(): def main():
# Point this to your local diffusers model dir (or replace with a HF model ID). # Point this to your local diffusers model dir (or replace with a HF model ID).
model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers" model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers"
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_path, GeneratorConfig(
num_gpus=1, model_path=model_path,
use_fsdp_inference=False, # set True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
) vae=False,
text_encoder=True,
sampling_param = SamplingParam.from_pretrained(model_path) pin_cpu_memory=True,
),
),
))
# video2world example from official repo # video2world example from official repo
video_path = "assets/videos/robot_pouring.mp4" video_path = "assets/videos/robot_pouring.mp4"
@@ -36,18 +42,19 @@ def main():
"The final frame captures the robotic arm with the pitcher finishing the pour, with the glass now filled to a higher level, while the pitcher is slightly tilted but still held securely by the gripper." "The final frame captures the robotic arm with the pitcher finishing the pour, with the glass now filled to a higher level, while the pitcher is slightly tilted but still held securely by the gripper."
) )
generator.generate_video( generator.generate(
prompt, GenerationRequest(
sampling_param=sampling_param, prompt=prompt,
video_path=str(video_path), inputs=InputConfig(video_path=str(video_path)),
num_cond_frames=1, output=OutputConfig(
output_path="outputs_video/cosmos2_5_v2w.mp4", output_path="outputs_video/cosmos2_5_v2w.mp4",
save_video=True, save_video=True,
) ),
extensions={"num_cond_frames": 1},
))
generator.shutdown() generator.shutdown()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+32 -19
View File
@@ -2,7 +2,9 @@ import os
import time import time
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, PipelineSelection,
SamplingConfig)
OUTPUT_PATH = "video_samples_dmd2" OUTPUT_PATH = "video_samples_dmd2"
def main(): def main():
@@ -10,30 +12,36 @@ def main():
load_start_time = time.perf_counter() load_start_time = time.perf_counter()
model_name = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers" model_name = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers"
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_name, GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path=model_name,
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
# Adjust these offload parameters if you have < 32GB of VRAM num_gpus=1,
text_encoder_cpu_offload=True, use_fsdp_inference=False, # set to True if GPU is out of memory
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" # Adjust these offload parameters if you have < 32GB of VRAM
dit_cpu_offload=False, offload=OffloadConfig(
vae_cpu_offload=False, text_encoder=True,
VSA_sparsity=0.8, pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
) dit=False,
vae=False,
),
),
pipeline=PipelineSelection(experimental={"VSA_sparsity": 0.8}),
))
load_end_time = time.perf_counter() load_end_time = time.perf_counter()
load_time = load_end_time - load_start_time load_time = load_end_time - load_start_time
sampling_param = SamplingParam.from_pretrained(model_name)
sampling_param.num_frames = 81
prompt = ( prompt = (
"A neon-lit alley in futuristic Tokyo during a heavy rainstorm at night. The puddles reflect glowing signs in kanji, advertising ramen, karaoke, and VR arcades. A woman in a translucent raincoat walks briskly with an LED umbrella. Steam rises from a street food cart, and a cat darts across the screen. Raindrops are visible on the camera lens, creating a cinematic bokeh effect." "A neon-lit alley in futuristic Tokyo during a heavy rainstorm at night. The puddles reflect glowing signs in kanji, advertising ramen, karaoke, and VR arcades. A woman in a translucent raincoat walks briskly with an LED umbrella. Steam rises from a street food cart, and a cat darts across the screen. Raindrops are visible on the camera lens, creating a cinematic bokeh effect."
) )
start_time = time.perf_counter() start_time = time.perf_counter()
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param) video = generator.generate(
GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_frames=81),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
end_time = time.perf_counter() end_time = time.perf_counter()
gen_time = end_time - start_time gen_time = end_time - start_time
@@ -46,7 +54,12 @@ def main():
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
start_time = time.perf_counter() start_time = time.perf_counter()
video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True, num_frames=81) video2 = generator.generate(
GenerationRequest(
prompt=prompt2,
sampling=SamplingConfig(num_frames=81),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
end_time = time.perf_counter() end_time = time.perf_counter()
gen_time2 = end_time - start_time gen_time2 = end_time - start_time
+42 -27
View File
@@ -1,6 +1,9 @@
import os import os
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (ComponentConfig, EngineConfig, GenerationRequest,
GeneratorConfig, InputConfig, OffloadConfig,
OutputConfig, PipelineSelection, SamplingConfig)
OUTPUT_PATH = os.getenv("DREAMX_WORLD_OUTPUT_PATH", "video_samples_dreamx_world") OUTPUT_PATH = os.getenv("DREAMX_WORLD_OUTPUT_PATH", "video_samples_dreamx_world")
@@ -16,16 +19,22 @@ def _env_float(name: str, default: float) -> float:
def main(): def main():
model_name = os.getenv("DREAMX_WORLD_MODEL_DIR", "FastVideo/DreamX-World-5B-Cam-Diffusers") model_name = os.getenv("DREAMX_WORLD_MODEL_DIR", "FastVideo/DreamX-World-5B-Cam-Diffusers")
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_name, GeneratorConfig(
num_gpus=1, model_path=model_name,
use_fsdp_inference=False, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=False,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
override_pipeline_cls_name="DreamXWorldPipeline", vae=True,
) text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
components=ComponentConfig(override_pipeline_cls_name="DreamXWorldPipeline"), ),
))
prompt = os.getenv( prompt = os.getenv(
"DREAMX_WORLD_PROMPT", "DREAMX_WORLD_PROMPT",
@@ -37,25 +46,31 @@ def main():
"https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG", "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG",
) )
kwargs = { request = GenerationRequest(
"output_path": OUTPUT_PATH, prompt=prompt,
"save_video": os.getenv("DREAMX_WORLD_SAVE_VIDEO", "1") != "0", inputs=InputConfig(image_path=image_path or None),
"height": _env_int("DREAMX_WORLD_HEIGHT", 480), sampling=SamplingConfig(
"width": _env_int("DREAMX_WORLD_WIDTH", 832), height=_env_int("DREAMX_WORLD_HEIGHT", 480),
"num_frames": _env_int("DREAMX_WORLD_NUM_FRAMES", 161), width=_env_int("DREAMX_WORLD_WIDTH", 832),
"num_inference_steps": _env_int("DREAMX_WORLD_STEPS", 30), num_frames=_env_int("DREAMX_WORLD_NUM_FRAMES", 161),
"guidance_scale": _env_float("DREAMX_WORLD_GUIDANCE", 5.0), num_inference_steps=_env_int("DREAMX_WORLD_STEPS", 30),
"action_list": os.getenv("DREAMX_WORLD_ACTIONS", "w,d,w").split(","), guidance_scale=_env_float("DREAMX_WORLD_GUIDANCE", 5.0),
"action_speed_list": [ ),
float(value) output=OutputConfig(
for value in os.getenv("DREAMX_WORLD_ACTION_SPEEDS", "4.0,2.0,4.0").split(",") output_path=OUTPUT_PATH,
], save_video=os.getenv("DREAMX_WORLD_SAVE_VIDEO", "1") != "0",
} ),
if image_path: extensions={
kwargs["image_path"] = image_path "action_list": os.getenv("DREAMX_WORLD_ACTIONS", "w,d,w").split(","),
"action_speed_list": [
float(value)
for value in os.getenv("DREAMX_WORLD_ACTION_SPEEDS", "4.0,2.0,4.0").split(",")
],
},
)
try: try:
generator.generate_video(prompt, **kwargs) generator.generate(request)
finally: finally:
generator.shutdown() generator.shutdown()
+40 -21
View File
@@ -26,6 +26,15 @@ import os
import torch import torch
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
SamplingConfig,
)
from fastvideo.models.camera import create_camera_trajectory from fastvideo.models.camera import create_camera_trajectory
# Model configuration (use GAMECRAFT_MODEL_PATH for local weights) # Model configuration (use GAMECRAFT_MODEL_PATH for local weights)
@@ -55,14 +64,20 @@ OUTPUT_PATH = "video_samples_gamecraft"
def main(): def main():
# Initialize generator # Initialize generator
# FastVideo will automatically download weights from HuggingFace # FastVideo will automatically download weights from HuggingFace
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
MODEL_PATH, GeneratorConfig(
num_gpus=1, model_path=MODEL_PATH,
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=True,
vae=True,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
) )
# Video parameters # Video parameters
@@ -96,23 +111,27 @@ def main():
prompt = DEFAULT_I2V_PROMPT if is_i2v else DEFAULT_PROMPTS["temple"] prompt = DEFAULT_I2V_PROMPT if is_i2v else DEFAULT_PROMPTS["temple"]
print(f"Mode: {'I2V' if is_i2v else 'T2V'}, prompt: {prompt[:60]}...") print(f"Mode: {'I2V' if is_i2v else 'T2V'}, prompt: {prompt[:60]}...")
gen_kw = dict( request = GenerationRequest(
prompt=prompt, prompt=prompt,
negative_prompt="", negative_prompt="",
camera_states=camera_states, sampling=SamplingConfig(
height=height, height=height,
width=width, width=width,
num_frames=num_frames, num_frames=num_frames,
num_inference_steps=50, num_inference_steps=50,
guidance_scale=6.0, guidance_scale=6.0,
seed=42, seed=42,
fps=24, fps=24,
output_path=OUTPUT_PATH, ),
save_video=True, output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
extensions={"camera_states": camera_states},
) )
if is_i2v: if is_i2v:
gen_kw["image_path"] = image_path request.inputs = InputConfig(image_path=image_path)
generator.generate_video(**gen_kw) generator.generate(request)
if __name__ == "__main__": if __name__ == "__main__":
+44 -26
View File
@@ -22,6 +22,10 @@ Requirements:
import argparse import argparse
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
def main(): def main():
@@ -74,33 +78,47 @@ def main():
parser.add_argument("--seed", type=int, default=42) parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args() args = parser.parse_args()
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
args.model_path, GeneratorConfig(
num_gpus=1, model_path=args.model_path,
use_fsdp_inference=False, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=False,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
) vae=True,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
video = generator.generate_video( video = generator.generate(
args.prompt, GenerationRequest(
negative_prompt=args.negative_prompt, prompt=args.prompt,
image_path=args.image_path, negative_prompt=args.negative_prompt,
trajectory_type=args.trajectory, inputs=InputConfig(
movement_distance=args.movement_distance, image_path=args.image_path,
camera_rotation=args.camera_rotation, ),
height=args.height, sampling=SamplingConfig(
width=args.width, height=args.height,
num_frames=args.num_frames, width=args.width,
num_inference_steps=args.num_inference_steps, num_frames=args.num_frames,
guidance_scale=args.guidance_scale, num_inference_steps=args.num_inference_steps,
fps=24, guidance_scale=args.guidance_scale,
seed=args.seed, fps=24,
output_path=args.output_path, seed=args.seed,
save_video=True, ),
) output=OutputConfig(
output_path=args.output_path,
save_video=True,
),
extensions={
"trajectory_type": args.trajectory,
"movement_distance": args.movement_distance,
"camera_rotation": args.camera_rotation,
},
))
generator.shutdown() generator.shutdown()
+35 -14
View File
@@ -1,6 +1,13 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
SamplingConfig,
)
import json import json
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_hy15" OUTPUT_PATH = "video_samples_hy15"
def main(): def main():
@@ -8,17 +15,21 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(GeneratorConfig(
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v", model_path="hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
# FastVideo will automatically handle distributed setup # FastVideo will automatically handle distributed setup
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory num_gpus=1,
dit_cpu_offload=True, use_fsdp_inference=False, # set to True if GPU is out of memory
vae_cpu_offload=True, offload=OffloadConfig(
text_encoder_cpu_offload=True, dit=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" vae=True,
# image_encoder_cpu_offload=False, text_encoder=True,
) pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder=False,
),
),
))
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
@@ -26,7 +37,12 @@ def main():
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, negative_prompt="", num_frames=81, fps=16) generator.generate(GenerationRequest(
prompt=prompt,
negative_prompt="",
sampling=SamplingConfig(num_frames=81, fps=16),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
prompt2 = ( prompt2 = (
"A majestic lion strides across the golden savanna, its powerful frame " "A majestic lion strides across the golden savanna, its powerful frame "
@@ -35,8 +51,13 @@ def main():
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True, negative_prompt="", num_frames=81, fps=16) generator.generate(GenerationRequest(
prompt=prompt2,
negative_prompt="",
sampling=SamplingConfig(num_frames=81, fps=16),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+38 -14
View File
@@ -1,6 +1,12 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
)
import json import json
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_hy15_1080p" OUTPUT_PATH = "video_samples_hy15_1080p"
def main(): def main():
@@ -8,17 +14,23 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"weizhou03/HunyuanVideo-1.5-Diffusers-1080p-2SR", # 480p -> 720p -> 1080p GeneratorConfig(
# or "weizhou03/HunyuanVideo-1.5-Diffusers-1080p" # 720p -> 1080p model_path="weizhou03/HunyuanVideo-1.5-Diffusers-1080p-2SR", # 480p -> 720p -> 1080p
# FastVideo will automatically handle distributed setup # or "weizhou03/HunyuanVideo-1.5-Diffusers-1080p" # 720p -> 1080p
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" dit=True,
# image_encoder_cpu_offload=False, vae=True,
text_encoder=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder=False,
),
),
)
) )
prompt = ( prompt = (
@@ -27,7 +39,13 @@ def main():
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, negative_prompt="") video = generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt="",
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
prompt2 = ( prompt2 = (
"A majestic lion strides across the golden savanna, its powerful frame " "A majestic lion strides across the golden savanna, its powerful frame "
@@ -36,7 +54,13 @@ def main():
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True, negative_prompt="") video2 = generator.generate(
GenerationRequest(
prompt=prompt2,
negative_prompt="",
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
+37 -23
View File
@@ -1,4 +1,6 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
SamplingConfig)
from fastvideo.models.dits.hyworld.resolution_utils import get_resolution_from_image from fastvideo.models.dits.hyworld.resolution_utils import get_resolution_from_image
# Default prompt from HY-WorldPlay run.sh # Default prompt from HY-WorldPlay run.sh
@@ -31,33 +33,45 @@ def main():
# Initialize generator # Initialize generator
print("\nInitializing VideoGenerator for HYWorld...") print("\nInitializing VideoGenerator for HYWorld...")
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/HY-WorldPlay-Bidirectional-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/HY-WorldPlay-Bidirectional-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=True,
image_encoder_cpu_offload=True, vae=True,
) text_encoder=True,
pin_cpu_memory=True,
image_encoder=True,
),
),
))
# Generate video # Generate video
# The pose string is automatically converted to camera matrices by the pipeline # The pose string is automatically converted to camera matrices by the pipeline
print("\nGenerating video...") print("\nGenerating video...")
generator.generate_video( generator.generate(
prompt=args.prompt, GenerationRequest(
image_path=args.image, prompt=args.prompt,
pose=args.pose, # Camera trajectory control negative_prompt="",
output_path=args.output_path, inputs=InputConfig(
save_video=True, image_path=args.image,
negative_prompt="", pose=args.pose, # Camera trajectory control
num_frames=args.num_frames, ),
fps=24, sampling=SamplingConfig(
height=HEIGHT, num_frames=args.num_frames,
width=WIDTH, fps=24,
seed=args.seed, height=HEIGHT,
) width=WIDTH,
seed=args.seed,
),
output=OutputConfig(
output_path=args.output_path,
save_video=True,
),
))
print(f"\nVideo saved to: {args.output_path}") print(f"\nVideo saved to: {args.output_path}")
@@ -1,36 +1,43 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
InputConfig, OffloadConfig, OutputConfig,
SamplingConfig)
OUTPUT_PATH = "video_samples_kandinsky5_i2v" OUTPUT_PATH = "video_samples_kandinsky5_i2v"
IMAGE_PATH = "assets/girl.png" IMAGE_PATH = "assets/girl.png"
def main(): def main():
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"kandinskylab/Kandinsky-5.0-I2V-Pro-distilled-5s-Diffusers", GeneratorConfig(
# "kandinskylab/Kandinsky-5.0-I2V-Pro-sft-5s-Diffusers" model_path="kandinskylab/Kandinsky-5.0-I2V-Pro-distilled-5s-Diffusers",
# "kandinskylab/Kandinsky-5.0-I2V-Lite-5s-Diffusers" # "kandinskylab/Kandinsky-5.0-I2V-Pro-sft-5s-Diffusers"
num_gpus=1, # "kandinskylab/Kandinsky-5.0-I2V-Lite-5s-Diffusers"
use_fsdp_inference=False, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
# image_encoder_cpu_offload=False, vae=False,
) text_encoder=True,
pin_cpu_memory=True,
# image_encoder=False,
),
),
))
prompt = ( prompt = (
"A woman stands up and walks away" "A woman stands up and walks away"
) )
_ = generator.generate_video( _ = generator.generate(
prompt, GenerationRequest(
image_path=IMAGE_PATH, prompt=prompt,
output_path=OUTPUT_PATH, inputs=InputConfig(image_path=IMAGE_PATH),
save_video=True, sampling=SamplingConfig(height=1024, width=1024, num_frames=121),
height=1024, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
width=1024, ))
num_frames=121,
)
if __name__ == "__main__": if __name__ == "__main__":
@@ -1,28 +1,41 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, SamplingConfig)
OUTPUT_PATH = "video_samples_kandinsky5_t2v" OUTPUT_PATH = "video_samples_kandinsky5_t2v"
def main(): def main():
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"kandinskylab/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers", GeneratorConfig(
# "kandinskylab/Kandinsky-5.0-T2V-Pro-sft-5s-Diffusers" model_path="kandinskylab/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers",
# "kandinskylab/Kandinsky-5.0-T2V-Lite-distilled16steps-5s-Diffusers" # "kandinskylab/Kandinsky-5.0-T2V-Pro-sft-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-T2V-Pro-distilled-5s-Diffusers" # "kandinskylab/Kandinsky-5.0-T2V-Lite-distilled16steps-5s-Diffusers"
num_gpus=1, # "kandinskylab/Kandinsky-5.0-T2V-Pro-distilled-5s-Diffusers"
use_fsdp_inference=False, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
# image_encoder_cpu_offload=False, vae=False,
) text_encoder=True,
pin_cpu_memory=True,
# image_encoder=False,
),
),
))
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
_ = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True,height=512, width=768, num_frames=121) _ = generator.generate(
GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(height=512, width=768, num_frames=121),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
prompt2 = ( prompt2 = (
"A majestic lion strides across the golden savanna, its powerful frame " "A majestic lion strides across the golden savanna, its powerful frame "
@@ -30,8 +43,13 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, " "the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
_ = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True, height=512, width=768, num_frames=121) _ = generator.generate(
GenerationRequest(
prompt=prompt2,
sampling=SamplingConfig(height=512, width=768, num_frames=121),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -1,24 +1,31 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig, SamplingConfig,
)
from fastvideo.models.dits.lingbotworld.cam_utils import prepare_camera_embedding from fastvideo.models.dits.lingbotworld.cam_utils import prepare_camera_embedding
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_lingbotworld" OUTPUT_PATH = "video_samples_lingbotworld"
def main(): def main():
# FastVideo will automatically use the optimal default arguments for the # FastVideo will automatically use the optimal default arguments for the
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LingBot-World-Base-Cam-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="FastVideo/LingBot-World-Base-Cam-Diffusers",
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer dit=True, # DiT need to be offloaded for MoE
pin_cpu_memory=True, vae=False,
# image_encoder_cpu_offload=False, text_encoder=True,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=True,
),
),
)
) )
num_frames = 81 num_frames = 81
@@ -33,15 +40,23 @@ def main():
spatial_scale=8, spatial_scale=8,
) )
generator.generate_video( generator.generate(
prompt, GenerationRequest(
image_path=image_path, prompt=prompt,
output_path=OUTPUT_PATH, inputs=InputConfig(
save_video=True, image_path=image_path,
num_frames=num_frames, c2ws_plucker_emb=c2ws_plucker_emb,
height=480, ),
width=832, sampling=SamplingConfig(
c2ws_plucker_emb=c2ws_plucker_emb, num_frames=num_frames,
height=480,
width=832,
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
) )
+140 -95
View File
@@ -19,6 +19,10 @@ import glob
import os import os
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig,
InputConfig, OffloadConfig, OutputConfig, PipelineSelection, SamplingConfig,
)
# Common prompts and settings matching the shell script examples # Common prompts and settings matching the shell script examples
PROMPT = ( PROMPT = (
@@ -45,41 +49,50 @@ SEED = 42
def basic_generation(): def basic_generation():
""" """
Run basic LongCat I2V generation (50 steps at 480p). Run basic LongCat I2V generation (50 steps at 480p).
This uses the full 50-step denoising process for highest quality. This uses the full 50-step denoising process for highest quality.
""" """
print("=" * 60) print("=" * 60)
print("LongCat I2V: Basic Generation (50 steps, 480p)") print("LongCat I2V: Basic Generation (50 steps, 480p)")
print("=" * 60) print("=" * 60)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-I2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-I2V-Diffusers",
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
enable_bsa=False, vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(experimental={"enable_bsa": False}),
)
) )
output_path = "outputs_video/longcat_i2v_basic" output_path = "outputs_video/longcat_i2v_basic"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
image_path=IMAGE_PATH, negative_prompt=NEGATIVE_PROMPT,
output_path=output_path, inputs=InputConfig(image_path=IMAGE_PATH),
save_video=True, sampling=SamplingConfig(
height=480, height=480,
width=480, # Square width=480, # Square
num_frames=93, num_frames=93,
num_inference_steps=50, num_inference_steps=50,
fps=15, fps=15,
guidance_scale=4.0, guidance_scale=4.0,
seed=SEED, seed=SEED,
),
output=OutputConfig(output_path=output_path, save_video=True),
)
) )
print(f"\nBasic generation complete! Video saved to: {output_path}") print(f"\nBasic generation complete! Video saved to: {output_path}")
generator.shutdown() generator.shutdown()
@@ -87,55 +100,70 @@ def basic_generation():
def distill_refine_generation(): def distill_refine_generation():
""" """
Run LongCat I2V with distill+refine pipeline (16 steps + refinement to 768p). Run LongCat I2V with distill+refine pipeline (16 steps + refinement to 768p).
This uses the distilled LoRA for fast 480p generation (16 steps), This uses the distilled LoRA for fast 480p generation (16 steps),
then refines to 768p using the refinement LoRA with BSA enabled. then refines to 768p using the refinement LoRA with BSA enabled.
""" """
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("LongCat I2V: Distill + Refine Pipeline") print("LongCat I2V: Distill + Refine Pipeline")
print("=" * 60) print("=" * 60)
# Stage 1: Distilled generation (16 steps at 480p) # Stage 1: Distilled generation (16 steps at 480p)
print("\n[Stage 1] Distilled generation (16 steps, 480p)") print("\n[Stage 1] Distilled generation (16 steps, 480p)")
print("-" * 40) print("-" * 40)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-I2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-I2V-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
enable_bsa=False, vae=True,
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA", text_encoder=True,
lora_nickname="distilled", pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
),
experimental={
"enable_bsa": False,
"lora_nickname": "distilled",
},
),
)
) )
distill_output_path = "outputs_video/longcat_i2v_distill" distill_output_path = "outputs_video/longcat_i2v_distill"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
image_path=IMAGE_PATH, negative_prompt=NEGATIVE_PROMPT,
output_path=distill_output_path, inputs=InputConfig(image_path=IMAGE_PATH),
save_video=True, sampling=SamplingConfig(
height=480, height=480,
width=480, # Square width=480, # Square
num_frames=93, num_frames=93,
num_inference_steps=16, num_inference_steps=16,
fps=15, fps=15,
guidance_scale=1.0, guidance_scale=1.0,
seed=SEED, seed=SEED,
),
output=OutputConfig(output_path=distill_output_path, save_video=True),
)
) )
print(f"Distilled generation complete! Video saved to: {distill_output_path}") print(f"Distilled generation complete! Video saved to: {distill_output_path}")
generator.shutdown() generator.shutdown()
# Stage 2: Refinement (480p -> 768p) # Stage 2: Refinement (480p -> 768p)
print("\n[Stage 2] Refinement (480p -> 768p with BSA)") print("\n[Stage 2] Refinement (480p -> 768p with BSA)")
print("-" * 40) print("-" * 40)
# Find the actual saved video file from stage 1 # Find the actual saved video file from stage 1
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4")) video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
if not video_files: if not video_files:
@@ -143,46 +171,63 @@ def distill_refine_generation():
# Use the most recently created video file # Use the most recently created video file
distill_video_path = max(video_files, key=os.path.getmtime) distill_video_path = max(video_files, key=os.path.getmtime)
print(f"Using stage 1 video: {distill_video_path}") print(f"Using stage 1 video: {distill_video_path}")
# Create a new generator with refinement LoRA and BSA enabled # Create a new generator with refinement LoRA and BSA enabled
# Note: Refinement uses the T2V model (not I2V) since it's upscaling the generated video # Note: Refinement uses the T2V model (not I2V) since it's upscaling the generated video
# For BSA [4, 4, 8]: latent must be divisible by 8 # For BSA [4, 4, 8]: latent must be divisible by 8
# 768x768: latent 48x48, 48%8=0 ✓ # 768x768: latent 48x48, 48%8=0 ✓
refine_generator = VideoGenerator.from_pretrained( refine_generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-T2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-T2V-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=True,
enable_bsa=True, vae=True,
bsa_sparsity=0.875, text_encoder=True,
bsa_chunk_q=[4, 4, 4], pin_cpu_memory=False,
bsa_chunk_k=[4, 4, 4], ),
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA", ),
lora_nickname="refinement", pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
),
experimental={
"enable_bsa": True,
"bsa_sparsity": 0.875,
"bsa_chunk_q": [4, 4, 4],
"bsa_chunk_k": [4, 4, 4],
"lora_nickname": "refinement",
},
),
)
) )
refine_output_path = "outputs_video/longcat_i2v_refine_720p" refine_output_path = "outputs_video/longcat_i2v_refine_720p"
refine_generator.generate_video( refine_generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
output_path=refine_output_path, negative_prompt=NEGATIVE_PROMPT,
save_video=True, inputs=InputConfig(refine_from=distill_video_path),
refine_from=distill_video_path, sampling=SamplingConfig(
t_thresh=0.5, height=720,
spatial_refine_only=False, width=720,
num_cond_frames=0, num_inference_steps=50,
height=720, fps=30,
width=720, guidance_scale=1.0,
num_inference_steps=50, seed=SEED,
fps=30, ),
guidance_scale=1.0, output=OutputConfig(output_path=refine_output_path, save_video=True),
seed=SEED, extensions={
"t_thresh": 0.5,
"spatial_refine_only": False,
"num_cond_frames": 0,
},
)
) )
print(f"Refinement complete! Video saved to: {refine_output_path}") print(f"Refinement complete! Video saved to: {refine_output_path}")
refine_generator.shutdown() refine_generator.shutdown()
@@ -192,13 +237,13 @@ def main():
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("LongCat Image-to-Video Example") print("LongCat Image-to-Video Example")
print("=" * 60 + "\n") print("=" * 60 + "\n")
# Run basic generation # Run basic generation
basic_generation() basic_generation()
# Run distill+refine pipeline # Run distill+refine pipeline
distill_refine_generation() distill_refine_generation()
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("All generations complete!") print("All generations complete!")
print("=" * 60) print("=" * 60)
+151 -95
View File
@@ -13,6 +13,10 @@ import glob
import os import os
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig,
InputConfig, OffloadConfig, OutputConfig, PipelineSelection, SamplingConfig,
)
# Common prompts and settings matching the shell script examples # Common prompts and settings matching the shell script examples
PROMPT = ( PROMPT = (
@@ -38,40 +42,54 @@ SEED = 42
def basic_generation(): def basic_generation():
""" """
Run basic LongCat T2V generation (50 steps at 480p). Run basic LongCat T2V generation (50 steps at 480p).
This uses the full 50-step denoising process for highest quality. This uses the full 50-step denoising process for highest quality.
""" """
print("=" * 60) print("=" * 60)
print("LongCat T2V: Basic Generation (50 steps, 480p)") print("LongCat T2V: Basic Generation (50 steps, 480p)")
print("=" * 60) print("=" * 60)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-T2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-T2V-Diffusers",
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
enable_bsa=False, vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
experimental={"enable_bsa": False},
),
)
) )
output_path = "outputs_video/longcat_t2v_basic" output_path = "outputs_video/longcat_t2v_basic"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
output_path=output_path, negative_prompt=NEGATIVE_PROMPT,
save_video=True, output=OutputConfig(
height=480, output_path=output_path,
width=832, save_video=True,
num_frames=93, ),
num_inference_steps=50, sampling=SamplingConfig(
fps=15, height=480,
guidance_scale=4.0, width=832,
seed=SEED, num_frames=93,
num_inference_steps=50,
fps=15,
guidance_scale=4.0,
seed=SEED,
),
)
) )
print(f"\nBasic generation complete! Video saved to: {output_path}") print(f"\nBasic generation complete! Video saved to: {output_path}")
generator.shutdown() generator.shutdown()
@@ -79,54 +97,72 @@ def basic_generation():
def distill_refine_generation(): def distill_refine_generation():
""" """
Run LongCat T2V with distill+refine pipeline (16 steps + refinement to 720p). Run LongCat T2V with distill+refine pipeline (16 steps + refinement to 720p).
This uses the distilled LoRA for fast 480p generation (16 steps), This uses the distilled LoRA for fast 480p generation (16 steps),
then refines to 720p using the refinement LoRA with BSA enabled. then refines to 720p using the refinement LoRA with BSA enabled.
""" """
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("LongCat T2V: Distill + Refine Pipeline") print("LongCat T2V: Distill + Refine Pipeline")
print("=" * 60) print("=" * 60)
# Stage 1: Distilled generation (16 steps at 480p) # Stage 1: Distilled generation (16 steps at 480p)
print("\n[Stage 1] Distilled generation (16 steps, 480p)") print("\n[Stage 1] Distilled generation (16 steps, 480p)")
print("-" * 40) print("-" * 40)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-T2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-T2V-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
enable_bsa=False, vae=True,
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA", text_encoder=True,
lora_nickname="distilled", pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
),
experimental={
"enable_bsa": False,
"lora_nickname": "distilled",
},
),
)
) )
distill_output_path = "outputs_video/longcat_t2v_distill" distill_output_path = "outputs_video/longcat_t2v_distill"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
output_path=distill_output_path, negative_prompt=NEGATIVE_PROMPT,
save_video=True, output=OutputConfig(
height=480, output_path=distill_output_path,
width=832, save_video=True,
num_frames=93, ),
num_inference_steps=16, sampling=SamplingConfig(
fps=15, height=480,
guidance_scale=1.0, width=832,
seed=SEED, num_frames=93,
num_inference_steps=16,
fps=15,
guidance_scale=1.0,
seed=SEED,
),
)
) )
print(f"Distilled generation complete! Video saved to: {distill_output_path}") print(f"Distilled generation complete! Video saved to: {distill_output_path}")
generator.shutdown() generator.shutdown()
# Stage 2: Refinement (480p -> 720p) # Stage 2: Refinement (480p -> 720p)
print("\n[Stage 2] Refinement (480p -> 720p with BSA)") print("\n[Stage 2] Refinement (480p -> 720p with BSA)")
print("-" * 40) print("-" * 40)
# Find the actual saved video file from stage 1 # Find the actual saved video file from stage 1
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4")) video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
if not video_files: if not video_files:
@@ -134,43 +170,65 @@ def distill_refine_generation():
# Use the most recently created video file # Use the most recently created video file
distill_video_path = max(video_files, key=os.path.getmtime) distill_video_path = max(video_files, key=os.path.getmtime)
print(f"Using stage 1 video: {distill_video_path}") print(f"Using stage 1 video: {distill_video_path}")
# Create a new generator with refinement LoRA and BSA enabled # Create a new generator with refinement LoRA and BSA enabled
refine_generator = VideoGenerator.from_pretrained( refine_generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-T2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-T2V-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=True,
enable_bsa=True, vae=True,
bsa_sparsity=0.875, text_encoder=True,
bsa_chunk_q=[4, 4, 8], pin_cpu_memory=False,
bsa_chunk_k=[4, 4, 8], ),
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA", ),
lora_nickname="refinement", pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
),
experimental={
"enable_bsa": True,
"bsa_sparsity": 0.875,
"bsa_chunk_q": [4, 4, 8],
"bsa_chunk_k": [4, 4, 8],
"lora_nickname": "refinement",
},
),
)
) )
refine_output_path = "outputs_video/longcat_t2v_refine_720p" refine_output_path = "outputs_video/longcat_t2v_refine_720p"
refine_generator.generate_video( refine_generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
output_path=refine_output_path, negative_prompt=NEGATIVE_PROMPT,
save_video=True, output=OutputConfig(
refine_from=distill_video_path, output_path=refine_output_path,
t_thresh=0.5, save_video=True,
spatial_refine_only=False, ),
num_cond_frames=0, inputs=InputConfig(
height=720, refine_from=distill_video_path,
width=1280, ),
num_inference_steps=50, sampling=SamplingConfig(
fps=30, height=720,
guidance_scale=1.0, width=1280,
seed=SEED, num_inference_steps=50,
fps=30,
guidance_scale=1.0,
seed=SEED,
),
extensions={
"t_thresh": 0.5,
"spatial_refine_only": False,
"num_cond_frames": 0,
},
)
) )
print(f"Refinement complete! Video saved to: {refine_output_path}") print(f"Refinement complete! Video saved to: {refine_output_path}")
refine_generator.shutdown() refine_generator.shutdown()
@@ -180,13 +238,13 @@ def main():
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("LongCat Text-to-Video Example") print("LongCat Text-to-Video Example")
print("=" * 60 + "\n") print("=" * 60 + "\n")
# Run basic generation # Run basic generation
basic_generation() basic_generation()
# Run distill+refine pipeline # Run distill+refine pipeline
distill_refine_generation() distill_refine_generation()
print("\n" + "=" * 60) print("\n" + "=" * 60)
print("All generations complete!") print("All generations complete!")
print("=" * 60) print("=" * 60)
@@ -194,5 +252,3 @@ def main():
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+142 -86
View File
@@ -19,6 +19,10 @@ import glob
import os import os
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
PipelineSelection, SamplingConfig,
)
# Common prompts and settings matching the shell script examples # Common prompts and settings matching the shell script examples
PROMPT = ( PROMPT = (
@@ -63,35 +67,49 @@ def basic_generation():
"Please provide a valid video path." "Please provide a valid video path."
) )
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-VC-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-VC-Diffusers",
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
enable_bsa=False, vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
experimental={"enable_bsa": False},
),
)
) )
output_path = "outputs_video/longcat_vc_basic" output_path = "outputs_video/longcat_vc_basic"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
video_path=VIDEO_PATH, negative_prompt=NEGATIVE_PROMPT,
num_cond_frames=NUM_COND_FRAMES, inputs=InputConfig(video_path=VIDEO_PATH),
output_path=output_path, sampling=SamplingConfig(
save_video=True, height=480,
height=480, width=832,
width=832, num_frames=93,
num_frames=93, num_inference_steps=50,
num_inference_steps=50, fps=15,
fps=15, guidance_scale=4.0,
guidance_scale=4.0, seed=SEED,
seed=SEED, ),
output=OutputConfig(
output_path=output_path,
save_video=True,
),
extensions={"num_cond_frames": NUM_COND_FRAMES},
)
) )
print(f"\nBasic generation complete! Video saved to: {output_path}") print(f"\nBasic generation complete! Video saved to: {output_path}")
generator.shutdown() generator.shutdown()
@@ -118,37 +136,55 @@ def distill_refine_generation():
print("\n[Stage 1] Distilled generation (16 steps, 480p)") print("\n[Stage 1] Distilled generation (16 steps, 480p)")
print("-" * 40) print("-" * 40)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-VC-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-VC-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=False,
enable_bsa=False, vae=True,
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA", text_encoder=True,
lora_nickname="distilled", pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
),
experimental={
"enable_bsa": False,
"lora_nickname": "distilled",
},
),
)
) )
distill_output_path = "outputs_video/longcat_vc_distill" distill_output_path = "outputs_video/longcat_vc_distill"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
video_path=VIDEO_PATH, negative_prompt=NEGATIVE_PROMPT,
num_cond_frames=NUM_COND_FRAMES, inputs=InputConfig(video_path=VIDEO_PATH),
output_path=distill_output_path, sampling=SamplingConfig(
save_video=True, height=480,
height=480, width=832,
width=832, num_frames=93,
num_frames=93, num_inference_steps=16,
num_inference_steps=16, fps=15,
fps=15, guidance_scale=1.0,
guidance_scale=1.0, seed=SEED,
seed=SEED, ),
output=OutputConfig(
output_path=distill_output_path,
save_video=True,
),
extensions={"num_cond_frames": NUM_COND_FRAMES},
)
) )
print(f"Distilled generation complete! Video saved to: {distill_output_path}") print(f"Distilled generation complete! Video saved to: {distill_output_path}")
generator.shutdown() generator.shutdown()
@@ -166,41 +202,61 @@ def distill_refine_generation():
# Create a new generator with refinement LoRA and BSA enabled # Create a new generator with refinement LoRA and BSA enabled
# Note: Refinement uses the T2V model (not VC) since it's upscaling the generated video # Note: Refinement uses the T2V model (not VC) since it's upscaling the generated video
refine_generator = VideoGenerator.from_pretrained( refine_generator = VideoGenerator.from_config(
"FastVideo/LongCat-Video-T2V-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/LongCat-Video-T2V-Diffusers",
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=True, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=False, dit=True,
enable_bsa=True, vae=True,
bsa_sparsity=0.875, text_encoder=True,
bsa_chunk_q=[4, 4, 8], pin_cpu_memory=False,
bsa_chunk_k=[4, 4, 8], ),
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA", ),
lora_nickname="refinement", pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
),
experimental={
"enable_bsa": True,
"bsa_sparsity": 0.875,
"bsa_chunk_q": [4, 4, 8],
"bsa_chunk_k": [4, 4, 8],
"lora_nickname": "refinement",
},
),
)
) )
refine_output_path = "outputs_video/longcat_vc_refine_720p" refine_output_path = "outputs_video/longcat_vc_refine_720p"
refine_generator.generate_video( refine_generator.generate(
prompt=PROMPT, GenerationRequest(
negative_prompt=NEGATIVE_PROMPT, prompt=PROMPT,
output_path=refine_output_path, negative_prompt=NEGATIVE_PROMPT,
save_video=True, inputs=InputConfig(refine_from=distill_video_path),
refine_from=distill_video_path, sampling=SamplingConfig(
t_thresh=0.5, height=720,
spatial_refine_only=False, width=1280,
num_cond_frames=0, # For refinement, no conditioning frames num_inference_steps=50,
height=720, fps=30,
width=1280, guidance_scale=1.0,
num_inference_steps=50, seed=SEED,
fps=30, ),
guidance_scale=1.0, output=OutputConfig(
seed=SEED, output_path=refine_output_path,
save_video=True,
),
extensions={
"t_thresh": 0.5,
"spatial_refine_only": False,
"num_cond_frames": 0, # For refinement, no conditioning frames
},
)
) )
print(f"Refinement complete! Video saved to: {refine_output_path}") print(f"Refinement complete! Video saved to: {refine_output_path}")
refine_generator.shutdown() refine_generator.shutdown()
+28 -11
View File
@@ -1,4 +1,11 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
)
PROMPT = ( PROMPT = (
@@ -18,22 +25,32 @@ PROMPT = (
def main() -> None: def main() -> None:
# Uses FastVideo default sampling settings for LTX2 base. # Uses FastVideo default sampling settings for LTX2 base.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Davids048/LTX2-Base-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Davids048/LTX2-Base-Diffusers",
engine=EngineConfig(
num_gpus=1,
),
)
) )
output_path = "outputs_video/ltx2_basic/output_ltx2_base_t2v_1088_1920_1.1.mp4" output_path = "outputs_video/ltx2_basic/output_ltx2_base_t2v_1088_1920_1.1.mp4"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=output_path, prompt=PROMPT,
save_video=True, output=OutputConfig(
num_frames=121, output_path=output_path,
height=1088, save_video=True,
width=1920, ),
sampling=SamplingConfig(
num_frames=121,
height=1088,
width=1920,
),
)
) )
generator.shutdown() generator.shutdown()
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -49,6 +49,11 @@ from pathlib import Path
import torch._inductor.config as _inductor import torch._inductor.config as _inductor
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig, ComponentConfig, EngineConfig, GenerationRequest,
GeneratorConfig, OffloadConfig, OutputConfig, PipelineSelection,
SamplingConfig,
)
from fastvideo.configs.pipelines.base import PipelineConfig from fastvideo.configs.pipelines.base import PipelineConfig
from fastvideo.utils import maybe_download_model from fastvideo.utils import maybe_download_model
@@ -86,9 +91,9 @@ PROMPT = os.getenv("LTX23_I2V_PROMPT", DEFAULT_PROMPT)
# Per-stage timing helpers -------------------------------------------------- # Per-stage timing helpers --------------------------------------------------
def _print_stage_breakdown(result: dict, label: str) -> float | None: def _print_stage_breakdown(result, label: str) -> float | None:
"""Print stage execution times and return the sum, or None if missing.""" """Print stage execution times and return the sum, or None if missing."""
logging_info = result.get("logging_info") logging_info = result.logging_info
stages = getattr(logging_info, "stages", None) if logging_info else None stages = getattr(logging_info, "stages", None) if logging_info else None
if not stages: if not stages:
print(f" [{label}] stage breakdown unavailable") print(f" [{label}] stage breakdown unavailable")
@@ -104,11 +109,11 @@ def _print_stage_breakdown(result: dict, label: str) -> float | None:
def _collect_stage_times( def _collect_stage_times(
result: dict, result,
stage_times: dict[str, list[float]], stage_times: dict[str, list[float]],
stage_order: OrderedDict[str, None], stage_order: OrderedDict[str, None],
) -> None: ) -> None:
logging_info = result.get("logging_info") logging_info = result.logging_info
stages = getattr(logging_info, "stages", None) if logging_info else None stages = getattr(logging_info, "stages", None) if logging_info else None
if not stages: if not stages:
return return
@@ -169,34 +174,54 @@ def main() -> None:
pipeline_config = PipelineConfig.from_pretrained(model_root) pipeline_config = PipelineConfig.from_pretrained(model_root)
pipeline_config.dit_config.quant_config = None pipeline_config.dit_config.quant_config = None
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_root, GeneratorConfig(
num_gpus=1, model_path=model_root,
# LTX-2.3 distilled uses the two-stage refine pipeline; the refine engine=EngineConfig(
# LoRA is intentionally empty for the distilled student. num_gpus=1,
ltx2_refine_enabled=True, compile=CompileConfig(
ltx2_refine_upsampler_path=str(refine_upsampler_path), enabled=True,
ltx2_refine_lora_path="", text_encoder_enabled=True,
ltx2_refine_num_inference_steps=3, # Compile the VAE codec submodules (encoder / decoder)
ltx2_refine_guidance_scale=1.0, # too. The `LTX2CausalVideoAutoencoder` declares
ltx2_refine_add_noise=True, # `_compile_conditions` so `_compile_with_conditions`
pipeline_config=pipeline_config, # targets just those submodules and leaves the
enable_torch_compile=True, # surrounding tiling control flow eager — needed for
enable_torch_compile_text_encoder=True, # fullgraph + dynamic=False to succeed. VAE eager decode
# Compile the VAE codec submodules (encoder / decoder) too. The # is ~1.0s; compiling it brings the stage to ~0.3s.
# `LTX2CausalVideoAutoencoder` declares `_compile_conditions` so vae_enabled=True,
# `_compile_with_conditions` targets just those submodules and backend=torch_compile_kwargs["backend"],
# leaves the surrounding tiling control flow eager — needed for fullgraph=torch_compile_kwargs["fullgraph"],
# fullgraph + dynamic=False to succeed. VAE eager decode is mode=torch_compile_kwargs["mode"],
# ~1.0s; compiling it brings the stage to ~0.3s. dynamic=torch_compile_kwargs["dynamic"],
enable_torch_compile_vae=True, vae_kwargs=torch_compile_kwargs,
torch_compile_kwargs=torch_compile_kwargs, ),
torch_compile_kwargs_vae=torch_compile_kwargs, # Keep everything resident — no CPU offload for serving runs.
# Keep everything resident — no CPU offload for serving-style runs. offload=OffloadConfig(
dit_cpu_offload=False, dit=False,
text_encoder_cpu_offload=False, text_encoder=False,
vae_cpu_offload=False, vae=False,
ltx2_vae_tiling=False, ),
),
pipeline=PipelineSelection(
vae_tiling=False,
# LTX-2.3 distilled uses the two-stage refine pipeline; the
# refine LoRA is intentionally empty for the distilled
# student.
components=ComponentConfig(
upsampler_weights=str(refine_upsampler_path),
),
preset_overrides={
"refine": {
"enabled": True,
"num_inference_steps": 3,
"guidance_scale": 1.0,
"add_noise": True,
}
},
experimental={"pipeline_config": pipeline_config},
),
)
) )
common_kwargs = dict( common_kwargs = dict(
@@ -206,12 +231,15 @@ def main() -> None:
height=1280, width=832, # portrait runway aspect height=1280, width=832, # portrait runway aspect
num_frames=121, fps=24, # ~5s clip num_frames=121, fps=24, # ~5s clip
num_inference_steps=8, # distilled denoise steps num_inference_steps=8, # distilled denoise steps
# i2v: anchor the input image at frame 0 with full strength. )
# `ltx2_image_crf=0.0` skips an extra JPEG re-encode of an already
# JPEG conditioning image. # i2v: anchor the input image at frame 0 with full strength.
# `ltx2_image_crf=0.0` skips an extra JPEG re-encode of an already
# JPEG conditioning image. These are model-specific knobs routed through
# the request extensions escape hatch.
common_extensions = dict(
ltx2_images=[(I2V_IMAGE, 0, 1.0)], ltx2_images=[(I2V_IMAGE, 0, 1.0)],
ltx2_image_crf=0.0, ltx2_image_crf=0.0,
save_video=True,
) )
warmup_runs = 2 warmup_runs = 2
@@ -227,10 +255,25 @@ def main() -> None:
for w in range(warmup_runs): for w in range(warmup_runs):
t0 = time.perf_counter() t0 = time.perf_counter()
print(f"\n[warmup {w + 1}/{warmup_runs}] compiling + generating…") print(f"\n[warmup {w + 1}/{warmup_runs}] compiling + generating…")
generator.generate_video( generator.generate(
output_path=str(OUTPUT_DIR / f"_warmup_{w + 1}.mp4"), GenerationRequest(
seed=7, prompt=common_kwargs["prompt"],
**common_kwargs, negative_prompt=common_kwargs["negative_prompt"],
sampling=SamplingConfig(
guidance_scale=common_kwargs["guidance_scale"],
height=common_kwargs["height"],
width=common_kwargs["width"],
num_frames=common_kwargs["num_frames"],
fps=common_kwargs["fps"],
num_inference_steps=common_kwargs["num_inference_steps"],
seed=7,
),
output=OutputConfig(
output_path=str(OUTPUT_DIR / f"_warmup_{w + 1}.mp4"),
save_video=True,
),
extensions=common_extensions,
)
) )
dt = time.perf_counter() - t0 dt = time.perf_counter() - t0
warmup_secs.append(dt) warmup_secs.append(dt)
@@ -245,19 +288,31 @@ def main() -> None:
out_path = OUTPUT_DIR / f"output_ltx2_3_distilled_i2v_run_{m + 1}.mp4" out_path = OUTPUT_DIR / f"output_ltx2_3_distilled_i2v_run_{m + 1}.mp4"
print(f"\n[measured {m + 1}/{measured_runs}] generating: {out_path}") print(f"\n[measured {m + 1}/{measured_runs}] generating: {out_path}")
t0 = time.perf_counter() t0 = time.perf_counter()
result = generator.generate_video( result = generator.generate(
output_path=str(out_path), GenerationRequest(
seed=2002 + m, prompt=common_kwargs["prompt"],
**common_kwargs, negative_prompt=common_kwargs["negative_prompt"],
sampling=SamplingConfig(
guidance_scale=common_kwargs["guidance_scale"],
height=common_kwargs["height"],
width=common_kwargs["width"],
num_frames=common_kwargs["num_frames"],
fps=common_kwargs["fps"],
num_inference_steps=common_kwargs["num_inference_steps"],
seed=2002 + m,
),
output=OutputConfig(
output_path=str(out_path),
save_video=True,
),
extensions=common_extensions,
)
) )
wall = time.perf_counter() - t0 wall = time.perf_counter() - t0
e2e = ( e2e = (result.extra.get("e2e_latency") if result is not None else None) or wall
result.get("e2e_latency")
if isinstance(result, dict) else None
) or wall
measured_secs.append(e2e) measured_secs.append(e2e)
print(f"[measured {m + 1}/{measured_runs}] e2e={e2e:.2f}s wall={wall:.2f}s") print(f"[measured {m + 1}/{measured_runs}] e2e={e2e:.2f}s wall={wall:.2f}s")
if isinstance(result, dict): if result is not None:
_print_stage_breakdown(result, f"measured {m + 1}") _print_stage_breakdown(result, f"measured {m + 1}")
_collect_stage_times(result, stage_times, stage_order) _collect_stage_times(result, stage_times, stage_order)
@@ -1,4 +1,5 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig
PROMPT = ( PROMPT = (
"A warm sunny backyard. The camera starts in a tight cinematic close-up " "A warm sunny backyard. The camera starts in a tight cinematic close-up "
@@ -17,16 +18,19 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN" os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
def main() -> None: def main() -> None:
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/LTX2-Distilled-Diffusers", GeneratorConfig(
num_gpus=4, model_path="FastVideo/LTX2-Distilled-Diffusers",
engine=EngineConfig(num_gpus=4),
)
) )
output_path = "outputs_video/ltx2_basic/output_ltx2_distilled_t2v.mp4" output_path = "outputs_video/ltx2_basic/output_ltx2_distilled_t2v.mp4"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=output_path, prompt=PROMPT,
save_video=True, output=OutputConfig(output_path=output_path, save_video=True),
)
) )
generator.shutdown() generator.shutdown()
@@ -8,6 +8,11 @@ from pathlib import Path
import torch import torch
import torch._inductor.config import torch._inductor.config
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig, ComponentConfig, EngineConfig, GenerationRequest,
GenerationResult, GeneratorConfig, OffloadConfig, OutputConfig,
PipelineSelection, SamplingConfig,
)
from fastvideo.configs.pipelines.base import PipelineConfig from fastvideo.configs.pipelines.base import PipelineConfig
from fastvideo.layers.quantization.nvfp4_config import NVFP4Config from fastvideo.layers.quantization.nvfp4_config import NVFP4Config
from fastvideo.utils import maybe_download_model from fastvideo.utils import maybe_download_model
@@ -45,11 +50,11 @@ def load_validation_entries(path: Path) -> list[dict]:
def print_stage_breakdown( def print_stage_breakdown(
result: dict, result: GenerationResult,
run_idx: int, run_idx: int,
num_runs: int, num_runs: int,
) -> float | None: ) -> float | None:
logging_info = result.get("logging_info") logging_info = result.logging_info
if logging_info is None: if logging_info is None:
print(f"[{run_idx}/{num_runs}] Stage breakdown unavailable: no logging_info") print(f"[{run_idx}/{num_runs}] Stage breakdown unavailable: no logging_info")
return None return None
@@ -70,9 +75,9 @@ def print_stage_breakdown(
def extract_sr_forward_latency( def extract_sr_forward_latency(
result: dict, result: GenerationResult,
) -> tuple[float | None, list[tuple[str, float]], list[str]]: ) -> tuple[float | None, list[tuple[str, float]], list[str]]:
logging_info = result.get("logging_info") logging_info = result.logging_info
if logging_info is None: if logging_info is None:
return None, [], [] return None, [], []
@@ -106,11 +111,11 @@ def extract_sr_forward_latency(
def collect_stage_times( def collect_stage_times(
result: dict, result: GenerationResult,
stage_times: dict[str, list[float]], stage_times: dict[str, list[float]],
stage_order: OrderedDict[str, None], stage_order: OrderedDict[str, None],
) -> None: ) -> None:
logging_info = result.get("logging_info") logging_info = result.logging_info
if logging_info is None: if logging_info is None:
return return
stages = getattr(logging_info, "stages", None) stages = getattr(logging_info, "stages", None)
@@ -202,26 +207,45 @@ def main() -> None:
"dynamic": False, "dynamic": False,
} }
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_root, GeneratorConfig(
num_gpus=1, model_path=model_root,
ltx2_refine_enabled=True, engine=EngineConfig(
ltx2_refine_upsampler_path=str(refine_upsampler_path), num_gpus=1,
refine_lora_path="", # keep refine LoRA disabled in this repo's typed adapter offload=OffloadConfig(
ltx2_refine_lora_path="", # keep refine LoRA disabled for distilled model dit=False,
ltx2_refine_num_inference_steps=2, text_encoder=False,
ltx2_refine_guidance_scale=1.0, vae=False,
ltx2_refine_add_noise=True, ),
pipeline_config=pipeline_config, compile=CompileConfig(
enable_torch_compile=True, enabled=True,
enable_torch_compile_text_encoder=True, text_encoder_enabled=True,
enable_torch_compile_vae=True, vae_enabled=True,
torch_compile_kwargs=torch_compile_kwargs, backend="inductor",
torch_compile_kwargs_vae=torch_compile_kwargs, fullgraph=True,
dit_cpu_offload=False, dynamic=False,
text_encoder_cpu_offload=False, vae_kwargs=torch_compile_kwargs,
vae_cpu_offload=False, ),
ltx2_vae_tiling=False, ),
pipeline=PipelineSelection(
vae_tiling=False,
components=ComponentConfig(
upsampler_weights=str(refine_upsampler_path),
),
preset_overrides={
"refine": {
"enabled": True,
"num_inference_steps": 2,
"guidance_scale": 1.0,
"add_noise": True,
}
},
experimental={
"refine_lora_path": "", # keep refine LoRA disabled in this repo's typed adapter
"pipeline_config": pipeline_config,
},
),
)
) )
run_times: list[float] = [] run_times: list[float] = []
@@ -243,25 +267,31 @@ def main() -> None:
torch.cuda.synchronize() torch.cuda.synchronize()
start = time.perf_counter() start = time.perf_counter()
result = generator.generate_video( result = generator.generate(
prompt=prompt, GenerationRequest(
output_path=str(output_path), prompt=prompt,
fps=24, sampling=SamplingConfig(
seed=10, fps=24,
save_video=True, seed=10,
guidance_scale=1.0, guidance_scale=1.0,
height=benchmark_entry.get("height", 1088), height=benchmark_entry.get("height", 1088),
width=benchmark_entry.get("width", 1920), width=benchmark_entry.get("width", 1920),
num_frames=121, num_frames=121,
num_inference_steps=5, num_inference_steps=5,
# image_path="examples/inference/basic/prompt1.png", ),
# ltx2_image_crf=0.0 output=OutputConfig(
output_path=str(output_path),
save_video=True,
),
# inputs=InputConfig(image_path="examples/inference/basic/prompt1.png"),
# extensions={"ltx2_image_crf": 0.0},
)
) )
if os.environ.get("FASTVIDEO_STAGE_LOGGING") == "0": if os.environ.get("FASTVIDEO_STAGE_LOGGING") == "0":
torch.cuda.synchronize() torch.cuda.synchronize()
elapsed = result.get("generation_time") if isinstance(result, dict) else None elapsed = result.generation_time if isinstance(result, GenerationResult) else None
e2e_elapsed = result.get("e2e_latency") if isinstance(result, dict) else None e2e_elapsed = result.extra.get("e2e_latency") if isinstance(result, GenerationResult) else None
if elapsed is None: if elapsed is None:
elapsed = time.perf_counter() - start elapsed = time.perf_counter() - start
if e2e_elapsed is None: if e2e_elapsed is None:
@@ -272,7 +302,7 @@ def main() -> None:
print(f"[{i + 1}/{num_runs}] Generation time: {elapsed:.2f}s") print(f"[{i + 1}/{num_runs}] Generation time: {elapsed:.2f}s")
print(f"[{i + 1}/{num_runs}] End-to-end latency: {e2e_elapsed:.2f}s") print(f"[{i + 1}/{num_runs}] End-to-end latency: {e2e_elapsed:.2f}s")
if isinstance(result, dict): if isinstance(result, GenerationResult):
stage_sum = print_stage_breakdown(result, i + 1, num_runs) stage_sum = print_stage_breakdown(result, i + 1, num_runs)
if stage_sum is not None: if stage_sum is not None:
non_stage_overhead = e2e_elapsed - stage_sum non_stage_overhead = e2e_elapsed - stage_sum
+32 -21
View File
@@ -1,18 +1,27 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_lucy_edit" OUTPUT_PATH = "video_samples_lucy_edit"
def main(): def main():
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"decart-ai/Lucy-Edit-Dev", GeneratorConfig(
num_gpus=1, model_path="decart-ai/Lucy-Edit-Dev",
use_fsdp_inference=False, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=True,
) vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
prompt = ("Change the apron and blouse to a classic clown costume: satin " prompt = ("Change the apron and blouse to a classic clown costume: satin "
"polka-dot jumpsuit in bright primary colors, ruffled white collar, " "polka-dot jumpsuit in bright primary colors, ruffled white collar, "
@@ -20,18 +29,20 @@ def main():
"foam nose; soft window light from left, eye-level medium shot.") "foam nose; soft window light from left, eye-level medium shot.")
video_path = "https://d2drjpuinn46lb.cloudfront.net/painter_original_edit.mp4" video_path = "https://d2drjpuinn46lb.cloudfront.net/painter_original_edit.mp4"
generator.generate_video( generator.generate(
prompt, GenerationRequest(
negative_prompt="", prompt=prompt,
video_path=video_path, negative_prompt="",
output_path=OUTPUT_PATH, inputs=InputConfig(video_path=video_path),
save_video=True, sampling=SamplingConfig(
height=480, height=480,
width=832, width=832,
num_frames=81, num_frames=81,
fps=24, fps=24,
guidance_scale=5.0, guidance_scale=5.0,
) ),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
if __name__ == "__main__": if __name__ == "__main__":
+38 -23
View File
@@ -1,4 +1,6 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
SamplingConfig)
from fastvideo.models.dits.matrixgame2.utils import create_action_presets from fastvideo.models.dits.matrixgame2.utils import create_action_presets
import torch import torch
@@ -38,35 +40,48 @@ def main():
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
config = VARIANT_CONFIG[MODEL_VARIANT] config = VARIANT_CONFIG[MODEL_VARIANT]
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
config["model_path"], GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path=config["model_path"],
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer dit=True, # DiT need to be offloaded for MoE
pin_cpu_memory=True, vae=False,
# image_encoder_cpu_offload=False, text_encoder=True,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=True,
),
),
)
) )
num_frames = 597 num_frames = 597
actions = create_action_presets(num_frames, keyboard_dim=config["keyboard_dim"]) actions = create_action_presets(num_frames, keyboard_dim=config["keyboard_dim"])
grid_sizes = torch.tensor([150, 44, 80]) grid_sizes = torch.tensor([150, 44, 80])
generator.generate_video( generator.generate(
prompt="", GenerationRequest(
image_path=config["image_url"], prompt="",
mouse_cond=actions["mouse"].unsqueeze(0), inputs=InputConfig(
keyboard_cond=actions["keyboard"].unsqueeze(0), image_path=config["image_url"],
grid_sizes=grid_sizes, mouse_cond=actions["mouse"].unsqueeze(0),
num_frames=num_frames, keyboard_cond=actions["keyboard"].unsqueeze(0),
height=352, grid_sizes=grid_sizes,
width=640, ),
num_inference_steps=50, sampling=SamplingConfig(
output_path=OUTPUT_PATH, num_frames=num_frames,
save_video=True, height=352,
width=640,
num_inference_steps=50,
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
) )
@@ -1,5 +1,6 @@
from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator
from fastvideo.models.dits.matrixgame2.utils import get_current_action_async, expand_action_to_frames from fastvideo.models.dits.matrixgame2.utils import get_current_action_async, expand_action_to_frames
from fastvideo.api import EngineConfig, GeneratorConfig, OffloadConfig
import torch import torch
import asyncio import asyncio
@@ -42,17 +43,23 @@ async def main():
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
config = VARIANT_CONFIG[MODEL_VARIANT] config = VARIANT_CONFIG[MODEL_VARIANT]
generator = StreamingVideoGenerator.from_pretrained( generator = StreamingVideoGenerator.from_config(
config["model_path"], GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path=config["model_path"],
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer dit=True, # DiT need to be offloaded for MoE
pin_cpu_memory=True, vae=False,
# image_encoder_cpu_offload=False, text_encoder=True,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=True,
# image_encoder=False,
),
),
)
) )
max_blocks = 50 max_blocks = 50
+34 -21
View File
@@ -1,4 +1,7 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig, SamplingConfig,
)
MODEL_PATH = "FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers" MODEL_PATH = "FastVideo/Matrix-Game-3.0-Base-Distilled-Diffusers"
IMAGE_URL = "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-3/demo_images/001/image.png" IMAGE_URL = "https://raw.githubusercontent.com/SkyworkAI/Matrix-Game/main/Matrix-Game-3/demo_images/001/image.png"
@@ -7,28 +10,38 @@ OUTPUT_PATH = "video_samples_matrixgame3"
def main(): def main():
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
MODEL_PATH, GeneratorConfig(
num_gpus=1, model_path=MODEL_PATH,
use_fsdp_inference=False, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=False,
) vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
image_path=IMAGE_URL, prompt=PROMPT,
height=720, inputs=InputConfig(image_path=IMAGE_URL),
width=1280, sampling=SamplingConfig(
num_frames=57, height=720,
num_inference_steps=3, width=1280,
guidance_scale=1.0, num_frames=57,
seed=42, num_inference_steps=3,
output_path=OUTPUT_PATH, guidance_scale=1.0,
save_video=True, seed=42,
) ),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
))
if __name__ == "__main__": if __name__ == "__main__":
+36 -20
View File
@@ -1,40 +1,56 @@
from fastvideo import VideoGenerator, PipelineConfig from fastvideo import VideoGenerator, PipelineConfig
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
PipelineSelection,
SamplingConfig,
)
def main(): def main():
config = PipelineConfig.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers") config = PipelineConfig.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
config.text_encoder_precisions = ["fp16"] config.text_encoder_precisions = ["fp16"]
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
pipeline_config=config, model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
use_fsdp_inference=False, # Disable FSDP for MPS engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
text_encoder_cpu_offload=True, use_fsdp_inference=False, # Disable FSDP for MPS
pin_cpu_memory=True, disable_autocast=False,
disable_autocast=False, offload=OffloadConfig(
num_gpus=1, dit=True,
text_encoder=True,
pin_cpu_memory=True,
),
),
pipeline=PipelineSelection(
experimental={"pipeline_config": config},
),
)
) )
# Create sampling parameters with reduced number of frames # Reduce from default 81 to 25 frames bc we have to use the SDPA attn backend for mps
sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers") sampling = SamplingConfig(
sampling_param.num_frames = 25 # Reduce from default 81 to 25 frames bc we have to use the SDPA attn backend for mps num_frames=25,
sampling_param.height = 256 height=256,
sampling_param.width = 256 width=256,
)
prompt = ("A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " prompt = ("A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones.") "natural light filtering through the petals. Mid-shot, warm and cheerful tones.")
video = generator.generate_video(prompt, sampling_param=sampling_param) video = generator.generate(GenerationRequest(prompt=prompt, sampling=sampling))
prompt2 = ("A majestic lion strides across the golden savanna, its powerful frame " prompt2 = ("A majestic lion strides across the golden savanna, its powerful frame "
"glistening under the warm afternoon sun. The tall grass ripples gently in " "glistening under the warm afternoon sun. The tall grass ripples gently in "
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, " "the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
video2 = generator.generate_video(prompt2, sampling_param=sampling_param) video2 = generator.generate(GenerationRequest(prompt=prompt2, sampling=sampling))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+25 -15
View File
@@ -1,6 +1,8 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
# from fastvideo.api.sampling_param import SamplingParam EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig,
OutputConfig,
)
OUTPUT_PATH = "video_samples" OUTPUT_PATH = "video_samples"
def main(): def main():
@@ -8,17 +10,23 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=2, # FastVideo will automatically handle distributed setup
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=False, num_gpus=2,
vae_cpu_offload=False, use_fsdp_inference=True,
text_encoder_cpu_offload=True, execution_backend="ray",
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" offload=OffloadConfig(
distributed_executor_backend="ray", dit=False,
# image_encoder_cpu_offload=False, vae=False,
text_encoder=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder=False,
),
),
)
) )
# Generate videos with the same simple API, regardless of GPU count # Generate videos with the same simple API, regardless of GPU count
@@ -27,7 +35,8 @@ def main():
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True) video = generator.generate(
GenerationRequest(prompt=prompt, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True)))
# Generate another video with a different prompt, without reloading the # Generate another video with a different prompt, without reloading the
# model! # model!
@@ -37,7 +46,8 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, " "the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True) video2 = generator.generate(
GenerationRequest(prompt=prompt2, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True)))
if __name__ == "__main__": if __name__ == "__main__":
+45 -29
View File
@@ -85,24 +85,35 @@ def main() -> None:
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = args.backend os.environ["FASTVIDEO_ATTENTION_BACKEND"] = args.backend
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig, OutputConfig,
ParallelismConfig, PipelineSelection, SamplingConfig,
)
os.makedirs(args.out_dir, exist_ok=True) os.makedirs(args.out_dir, exist_ok=True)
init_kwargs = { generator = VideoGenerator.from_config(
"num_gpus": args.num_gpus, GeneratorConfig(
"workload_type": "t2i", model_path=args.model_path,
"sp_size": 1, engine=EngineConfig(
"tp_size": 1, num_gpus=args.num_gpus,
"dit_cpu_offload": False, use_fsdp_inference=False,
"dit_layerwise_offload": False, parallelism=ParallelismConfig(
"text_encoder_cpu_offload": False, sp_size=1,
"vae_cpu_offload": False, tp_size=1,
"image_encoder_cpu_offload": False, ),
"pin_cpu_memory": False, offload=OffloadConfig(
"use_fsdp_inference": False, dit=False,
} dit_layerwise=False,
text_encoder=False,
generator = VideoGenerator.from_pretrained(model_path=args.model_path, **init_kwargs) vae=False,
image_encoder=False,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(workload_type="t2i"),
)
)
try: try:
for i, prompt in enumerate(prompts): for i, prompt in enumerate(prompts):
seed = args.seed + i seed = args.seed + i
@@ -113,20 +124,25 @@ def main() -> None:
output_path = os.path.join(args.out_dir, f"{filename_base}.png") output_path = os.path.join(args.out_dir, f"{filename_base}.png")
print(f"[sd35] prompt_idx={i} seed={seed} output_path={output_path}") print(f"[sd35] prompt_idx={i} seed={seed} output_path={output_path}")
generation_kwargs = { generator.generate(
"output_path": output_path, GenerationRequest(
"height": args.height, prompt=prompt,
"width": args.width, negative_prompt=args.negative,
"num_frames": 1, sampling=SamplingConfig(
"fps": 1, height=args.height,
"num_inference_steps": args.steps, width=args.width,
"guidance_scale": args.guidance, num_frames=1,
"seed": seed, fps=1,
"negative_prompt": args.negative, num_inference_steps=args.steps,
"save_video": True, guidance_scale=args.guidance,
} seed=seed,
),
generator.generate_video(prompt, **generation_kwargs) output=OutputConfig(
output_path=output_path,
save_video=True,
),
)
)
print(f"[sd35] done. outputs written to: {args.out_dir}") print(f"[sd35] done. outputs written to: {args.out_dir}")
finally: finally:
@@ -1,6 +1,14 @@
import os import os
import time import time
from fastvideo import VideoGenerator, SamplingParam
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
)
OUTPUT_PATH = "video_samples_causal" OUTPUT_PATH = "video_samples_causal"
def main(): def main():
@@ -9,23 +17,33 @@ def main():
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
model_name = "wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers" model_name = "wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers"
generator = VideoGenerator.from_pretrained( generator_config = GeneratorConfig(
model_name, model_path=model_name,
# FastVideo will automatically handle distributed setup # FastVideo will automatically handle distributed setup
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory num_gpus=1,
text_encoder_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=False, offload=OffloadConfig(
text_encoder=False,
dit=False,
),
),
) )
generator = VideoGenerator.from_config(generator_config)
sampling_param = SamplingParam.from_pretrained(model_name)
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param) request = GenerationRequest(
prompt=prompt,
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
video = generator.generate(request)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -1,8 +1,17 @@
# NOTE: This is still a work in progress, and the checkpoints are not released yet. # NOTE: This is still a work in progress, and the checkpoints are not released yet.
from fastvideo import VideoGenerator, SamplingParam from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
SamplingConfig,
)
import json import json
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_i2v" OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_i2v"
def main(): def main():
@@ -10,26 +19,37 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers",
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
dit_precision="fp32", use_fsdp_inference=False, # set to True if GPU is out of memory
vae_cpu_offload=False, offload=OffloadConfig(
text_encoder_cpu_offload=True, dit=True, # DiT need to be offloaded for MoE
dmd_denoising_steps=[1000, 850, 700, 550, 350, 275, 200, 125], vae=False,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer text_encoder=True,
pin_cpu_memory=True, # Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
# image_encoder_cpu_offload=False, pin_cpu_memory=True,
# image_encoder=False,
),
),
pipeline=PipelineSelection(
experimental={
"dit_precision": "fp32",
"dmd_denoising_steps": [1000, 850, 700, 550, 350, 275, 200, 125],
},
),
)
) )
sampling_param = SamplingParam.from_pretrained("FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers") sampling = SamplingConfig(
sampling_param.num_frames = 81 num_frames=81,
sampling_param.width = 832 width=832,
sampling_param.height = 480 height=480,
sampling_param.seed = 1000 seed=1000,
)
with open("assets/prompts/mixkit_i2v.jsonl", "r") as f: with open("assets/prompts/mixkit_i2v.jsonl", "r") as f:
prompt_image_pairs = json.load(f) prompt_image_pairs = json.load(f)
@@ -37,7 +57,14 @@ def main():
for prompt_image_pair in prompt_image_pairs: for prompt_image_pair in prompt_image_pairs:
prompt = prompt_image_pair["prompt"] prompt = prompt_image_pair["prompt"]
image_path = prompt_image_pair["image_path"] image_path = prompt_image_pair["image_path"]
_ = generator.generate_video(prompt, image_path=image_path, output_path=OUTPUT_PATH, save_video=True, sampling_param=sampling_param) _ = generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=image_path),
sampling=sampling,
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
@@ -1,8 +1,10 @@
# NOTE: This is still a work in progress, and the checkpoints are not released yet. # NOTE: This is still a work in progress, and the checkpoints are not released yet.
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
# from fastvideo.api.sampling_param import SamplingParam ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, PipelineSelection, SamplingConfig,
)
OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_t2v" OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_t2v"
def main(): def main():
@@ -10,34 +12,49 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"rand0nmr/SFWan2.2-T2V-A14B-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="rand0nmr/SFWan2.2-T2V-A14B-Diffusers",
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
dmd_denoising_steps=[1000, 850, 700, 550, 350, 275, 200, 125], dit=True, # DiT need to be offloaded for MoE
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer vae=False,
pin_cpu_memory=True, text_encoder=True,
init_weights_from_safetensors="/mnt/sharefs/users/hao.zhang/wei/SFwan2.2_distill_self_forcing_release_cfg2/checkpoint-246_weight_only/generator_inference_transformer/", # Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
init_weights_from_safetensors_2="/mnt/sharefs/users/hao.zhang/wei/SFwan2.2_distill_self_forcing_release_cfg2/checkpoint-246_weight_only/generator_2_inference_transformer/", pin_cpu_memory=True,
num_frame_per_block=7, ),
# image_encoder_cpu_offload=False, ),
pipeline=PipelineSelection(
components=ComponentConfig(
transformer_weights="/mnt/sharefs/users/hao.zhang/wei/SFwan2.2_distill_self_forcing_release_cfg2/checkpoint-246_weight_only/generator_inference_transformer/",
transformer_2_weights="/mnt/sharefs/users/hao.zhang/wei/SFwan2.2_distill_self_forcing_release_cfg2/checkpoint-246_weight_only/generator_2_inference_transformer/",
),
experimental={
"dmd_denoising_steps": [1000, 850, 700, 550, 350, 275, 200, 125],
"num_frame_per_block": 7,
},
),
# image_encoder_cpu_offload=False,
)
) )
# sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
# sampling_param.num_frames = 45
# sampling_param.image_path = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/astronaut.jpg"
# Generate videos with the same simple API, regardless of GPU count # Generate videos with the same simple API, regardless of GPU count
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
_ = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, num_frames=81) _ = generator.generate(
GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_frames=81),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+19 -13
View File
@@ -51,25 +51,31 @@ Prerequisites:
uv pip install k_diffusion einops_exts alias_free_torch torchsde uv pip install k_diffusion einops_exts alias_free_torch torchsde
""" """
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig)
PROMPT = "Lo-fi hip hop instrumental with vinyl crackle and gentle piano." PROMPT = "Lo-fi hip hop instrumental with vinyl crackle and gentle piano."
def main() -> None: def main() -> None:
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/stable-audio-open-1.0-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/stable-audio-open-1.0-Diffusers",
) engine=EngineConfig(num_gpus=1),
))
output_path = "outputs_audio/stable_audio_basic/output_stable_audio.wav" output_path = "outputs_audio/stable_audio_basic/output_stable_audio.wav"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=output_path, prompt=PROMPT,
save_video=True, output=OutputConfig(
# 6-second clip; the model max is ~47.5s. output_path=output_path,
audio_end_in_s=6.0, save_video=True,
# The registered preset gives 100 steps + CFG=7.0 by default; ),
# override num_inference_steps / guidance_scale here for QA. # 6-second clip; the model max is ~47.5s.
) extensions={"audio_end_in_s": 6.0},
# The registered preset gives 100 steps + CFG=7.0 by default;
# override num_inference_steps / guidance_scale here for QA.
))
generator.shutdown() generator.shutdown()
@@ -48,6 +48,12 @@ Picking `init_audio_strength` (0.0 to 1.0):
Prerequisites: same as `basic_stable_audio.py`. Prerequisites: same as `basic_stable_audio.py`.
""" """
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
)
PROMPT = "Change the piano to a cello playing the same notes" PROMPT = "Change the piano to a cello playing the same notes"
# Path to any audio-bearing file (wav, mp3, mp4, m4a, flac, ...). # Path to any audio-bearing file (wav, mp3, mp4, m4a, flac, ...).
@@ -58,18 +64,24 @@ INIT_AUDIO_STRENGTH = 0.6
def main() -> None: def main() -> None:
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/stable-audio-open-1.0-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/stable-audio-open-1.0-Diffusers",
) engine=EngineConfig(num_gpus=1),
generator.generate_video( ))
prompt=PROMPT, generator.generate(
output_path="outputs_audio/stable_audio_a2a/output_a2a.wav", GenerationRequest(
save_video=True, prompt=PROMPT,
audio_end_in_s=6.0, output=OutputConfig(
init_audio=INIT_AUDIO_PATH, output_path="outputs_audio/stable_audio_a2a/output_a2a.wav",
init_audio_strength=INIT_AUDIO_STRENGTH, save_video=True,
) ),
extensions={
"audio_end_in_s": 6.0,
"init_audio": INIT_AUDIO_PATH,
"init_audio_strength": INIT_AUDIO_STRENGTH,
},
))
generator.shutdown() generator.shutdown()
@@ -48,6 +48,9 @@ Prerequisites: same as `basic_stable_audio.py`.
import os import os
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GeneratorConfig, GenerationRequest, OutputConfig,
)
PROMPT = "Steady lo-fi hip hop drum loop with vinyl crackle." PROMPT = "Steady lo-fi hip hop drum loop with vinyl crackle."
# Required: path to the reference audio file (wav, mp3, mp4, m4a, flac, # Required: path to the reference audio file (wav, mp3, mp4, m4a, flac,
@@ -64,19 +67,25 @@ def main() -> None:
f"REFERENCE_AUDIO_PATH={REFERENCE_AUDIO_PATH!r} does not exist. " f"REFERENCE_AUDIO_PATH={REFERENCE_AUDIO_PATH!r} does not exist. "
"Edit this script to point at a real audio file (wav/mp3/mp4/" "Edit this script to point at a real audio file (wav/mp3/mp4/"
"m4a/flac) before running.") "m4a/flac) before running.")
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/stable-audio-open-1.0-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/stable-audio-open-1.0-Diffusers",
) engine=EngineConfig(num_gpus=1),
generator.generate_video( ))
prompt=PROMPT, generator.generate(
output_path="outputs_audio/stable_audio_inpaint/output_inpaint.wav", GenerationRequest(
save_video=True, prompt=PROMPT,
audio_end_in_s=TOTAL_SECONDS, output=OutputConfig(
inpaint_audio=REFERENCE_AUDIO_PATH, output_path="outputs_audio/stable_audio_inpaint/output_inpaint.wav",
# Tuple form: keep first KEEP_SECONDS, regenerate the rest. save_video=True,
inpaint_mask=(KEEP_SECONDS, TOTAL_SECONDS), ),
) extensions={
"audio_end_in_s": TOTAL_SECONDS,
"inpaint_audio": REFERENCE_AUDIO_PATH,
# Tuple form: keep first KEEP_SECONDS, regenerate the rest.
"inpaint_mask": (KEEP_SECONDS, TOTAL_SECONDS),
},
))
generator.shutdown() generator.shutdown()
@@ -28,24 +28,27 @@ Prerequisites: same as `basic_stable_audio.py`. The converted repo is
public so no gated-access flow is required. public so no gated-access flow is required.
""" """
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig)
PROMPT = "Lo-fi hip hop instrumental with vinyl crackle and gentle piano." PROMPT = "Lo-fi hip hop instrumental with vinyl crackle and gentle piano."
def main() -> None: def main() -> None:
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"FastVideo/stable-audio-open-small-Diffusers", GeneratorConfig(
num_gpus=1, model_path="FastVideo/stable-audio-open-small-Diffusers",
) engine=EngineConfig(num_gpus=1),
))
output_path = "outputs_audio/stable_audio_small/output_stable_audio_small.wav" output_path = "outputs_audio/stable_audio_small/output_stable_audio_small.wav"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=output_path, prompt=PROMPT,
save_video=True, output=OutputConfig(output_path=output_path, save_video=True),
# Small variant trains on a ~11.9s window — keep `audio_end_in_s` # Small variant trains on a ~11.9s window — keep `audio_end_in_s`
# at or below that. # at or below that.
audio_end_in_s=6.0, extensions={"audio_end_in_s": 6.0},
) ))
generator.shutdown() generator.shutdown()
@@ -4,6 +4,9 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN" os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_turbodiffusion" OUTPUT_PATH = "video_samples_turbodiffusion"
@@ -11,14 +14,17 @@ OUTPUT_PATH = "video_samples_turbodiffusion"
def main() -> None: def main() -> None:
# TurboDiffusion: 1-4 step video generation using RCM scheduler + SLA attention # TurboDiffusion: 1-4 step video generation using RCM scheduler + SLA attention
# FastVideo will automatically use TurboDiffusionPipeline when specified # FastVideo will automatically use TurboDiffusionPipeline when specified
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"loayrashid/TurboWan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="loayrashid/TurboWan2.1-T2V-1.3B-Diffusers",
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
num_gpus=1,
# set to false if using RTX 4090 use_fsdp_inference=False, # set to True if GPU is out of memory
# pin_cpu_memory=False, ),
# set to false if using RTX 4090
# pin_cpu_memory=False,
)
) )
# Generate videos with the same simple API, regardless of GPU count # Generate videos with the same simple API, regardless of GPU count
@@ -28,11 +34,17 @@ def main() -> None:
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt,
save_video=True, output=OutputConfig(
seed=42, output_path=OUTPUT_PATH,
save_video=True,
),
sampling=SamplingConfig(
seed=42,
),
)
) )
# Generate another video with a different prompt, without reloading the model! # Generate another video with a different prompt, without reloading the model!
@@ -43,11 +55,17 @@ def main() -> None:
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic." "cinematic."
) )
video2 = generator.generate_video( video2 = generator.generate(
prompt2, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt2,
save_video=True, output=OutputConfig(
seed=42, output_path=OUTPUT_PATH,
save_video=True,
),
sampling=SamplingConfig(
seed=42,
),
)
) )
@@ -4,6 +4,9 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN" os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_turbodiffusion_14B" OUTPUT_PATH = "video_samples_turbodiffusion_14B"
@@ -11,10 +14,12 @@ OUTPUT_PATH = "video_samples_turbodiffusion_14B"
def main() -> None: def main() -> None:
# TurboDiffusion 14B: 1-4 step video generation using RCM scheduler + SLA attention # TurboDiffusion 14B: 1-4 step video generation using RCM scheduler + SLA attention
# FastVideo will automatically use TurboDiffusionPipeline when specified # FastVideo will automatically use TurboDiffusionPipeline when specified
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"loayrashid/TurboWan2.1-T2V-14B-Diffusers", GeneratorConfig(
# 14B model needs more GPUs model_path="loayrashid/TurboWan2.1-T2V-14B-Diffusers",
num_gpus=2, # 14B model needs more GPUs
engine=EngineConfig(num_gpus=2),
)
) )
prompt = ( prompt = (
@@ -22,11 +27,12 @@ def main() -> None:
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt,
save_video=True, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
seed=42, sampling=SamplingConfig(seed=42),
)
) )
# Generate another video with a different prompt, without reloading the model! # Generate another video with a different prompt, without reloading the model!
@@ -37,11 +43,12 @@ def main() -> None:
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic." "cinematic."
) )
video2 = generator.generate_video( video2 = generator.generate(
prompt2, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt2,
save_video=True, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
seed=42, sampling=SamplingConfig(seed=42),
)
) )
@@ -4,6 +4,10 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN" os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OutputConfig, SamplingConfig,
)
# Use local model path # Use local model path
MODEL_PATH = "loayrashid/TurboWan2.2-I2V-A14B-Diffusers" MODEL_PATH = "loayrashid/TurboWan2.2-I2V-A14B-Diffusers"
@@ -12,9 +16,11 @@ OUTPUT_PATH = "video_samples_turbodiffusion_i2v"
def main() -> None: def main() -> None:
# TurboDiffusion I2V: 1-4 step image-to-video generation # TurboDiffusion I2V: 1-4 step image-to-video generation
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
MODEL_PATH, GeneratorConfig(
num_gpus=2, model_path=MODEL_PATH,
engine=EngineConfig(num_gpus=2),
)
) )
# Example prompt and image for I2V # Example prompt and image for I2V
@@ -24,12 +30,13 @@ def main() -> None:
image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG" image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG"
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
image_path=image_path, prompt=prompt,
output_path=OUTPUT_PATH, inputs=InputConfig(image_path=image_path),
save_video=True, sampling=SamplingConfig(seed=42),
seed=42, output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
) )
+35 -20
View File
@@ -1,6 +1,8 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
# from fastvideo.api.sampling_param import SamplingParam EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig,
OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_wan2_2_14B_t2v" OUTPUT_PATH = "video_samples_wan2_2_14B_t2v"
def main(): def main():
@@ -8,30 +10,37 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.2-T2V-A14B-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
num_gpus=2, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=2,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer dit=True, # DiT need to be offloaded for MoE
pin_cpu_memory=True, vae=False,
# image_encoder_cpu_offload=False, text_encoder=True,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=True,
),
),
)
) )
# sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
# sampling_param.num_frames = 45
# sampling_param.image_path = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/astronaut.jpg"
# Generate videos with the same simple API, regardless of GPU count # Generate videos with the same simple API, regardless of GPU count
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
"wide with interest. The playful yet serene atmosphere is complemented by soft " "wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones." "natural light filtering through the petals. Mid-shot, warm and cheerful tones."
) )
_ = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, height=720, width=1280, num_frames=81) _ = generator.generate(
# video = generator.generate_video(prompt, sampling_param=sampling_param, output_path="wan_t2v_videos/") GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(height=720, width=1280, num_frames=81),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
# Generate another video with a different prompt, without reloading the # Generate another video with a different prompt, without reloading the
# model! # model!
@@ -41,8 +50,14 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, " "the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
_ = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True, height=720, width=1280, num_frames=81) _ = generator.generate(
GenerationRequest(
prompt=prompt2,
sampling=SamplingConfig(height=720, width=1280, num_frames=81),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+35 -16
View File
@@ -1,6 +1,12 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
# from fastvideo.api.sampling_param import SamplingParam EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
)
OUTPUT_PATH = "video_samples_wan2_1_Fun" OUTPUT_PATH = "video_samples_wan2_1_Fun"
OUTPUT_NAME = "wan2.1_test" OUTPUT_NAME = "wan2.1_test"
@@ -9,18 +15,24 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"IRMChen/Wan2.1-Fun-1.3B-Control-Diffusers", GeneratorConfig(
# "alibaba-pai/Wan2.2-Fun-A14B-Control", model_path="IRMChen/Wan2.1-Fun-1.3B-Control-Diffusers",
# FastVideo will automatically handle distributed setup # "alibaba-pai/Wan2.2-Fun-A14B-Control",
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer dit=True, # DiT need to be offloaded for MoE
pin_cpu_memory=True, vae=False,
# image_encoder_cpu_offload=False, text_encoder=True,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=True,
# image_encoder=False,
),
),
)
) )
prompt = "一位年轻女性穿着一件粉色的连衣裙,裙子上有白色的装饰和粉色的纽扣。她的头发是紫色的,头上戴着一个红色的大蝴蝶结,显得非常可爱和精致。她还戴着一个红色的领结,整体造型充满了少女感和活力。她的表情温柔,双手轻轻交叉放在身前,姿态优雅。背景是简单的灰色,没有任何多余的装饰,使得人物更加突出。她的妆容清淡自然,突显了她的清新气质。整体画面给人一种甜美、梦幻的感觉,仿佛置身于童话世界中。" prompt = "一位年轻女性穿着一件粉色的连衣裙,裙子上有白色的装饰和粉色的纽扣。她的头发是紫色的,头上戴着一个红色的大蝴蝶结,显得非常可爱和精致。她还戴着一个红色的领结,整体造型充满了少女感和活力。她的表情温柔,双手轻轻交叉放在身前,姿态优雅。背景是简单的灰色,没有任何多余的装饰,使得人物更加突出。她的妆容清淡自然,突显了她的清新气质。整体画面给人一种甜美、梦幻的感觉,仿佛置身于童话世界中。"
@@ -30,7 +42,14 @@ def main():
image_path = "https://pai-aigc-photog.oss-cn-hangzhou.aliyuncs.com/wan_fun/asset_Wan2_2/v1.0/8.png" image_path = "https://pai-aigc-photog.oss-cn-hangzhou.aliyuncs.com/wan_fun/asset_Wan2_2/v1.0/8.png"
control_video_path = "https://pai-aigc-photog.oss-cn-hangzhou.aliyuncs.com/wan_fun/asset_Wan2_2/v1.0/pose.mp4" control_video_path = "https://pai-aigc-photog.oss-cn-hangzhou.aliyuncs.com/wan_fun/asset_Wan2_2/v1.0/pose.mp4"
video = generator.generate_video(prompt, negative_prompt=negative_prompt, image_path=image_path, video_path=control_video_path, output_path=OUTPUT_PATH, output_video_name=OUTPUT_NAME, save_video=True) video = generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt=negative_prompt,
inputs=InputConfig(image_path=image_path, video_path=control_video_path),
output=OutputConfig(output_path=OUTPUT_PATH, output_video_name=OUTPUT_NAME, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+30 -15
View File
@@ -1,6 +1,8 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
# from fastvideo.api.sampling_param import SamplingParam EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_wan2_2_14B_i2v" OUTPUT_PATH = "video_samples_wan2_2_14B_i2v"
def main(): def main():
@@ -8,23 +10,36 @@ def main():
# model. # model.
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.2-I2V-A14B-Diffusers", GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=False, # set to True if GPU is out of memory engine=EngineConfig(
dit_cpu_offload=True, # DiT need to be offloaded for MoE num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer dit=True, # DiT need to be offloaded for MoE
pin_cpu_memory=True, vae=False,
# image_encoder_cpu_offload=False, text_encoder=True,
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=True,
# image_encoder=False,
),
),
)
) )
prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside." prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside."
image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG" image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG"
video = generator.generate_video(prompt, image_path=image_path, output_path=OUTPUT_PATH, save_video=True, height=832, width=480, num_frames=81) video = generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=image_path),
sampling=SamplingConfig(height=832, width=480, num_frames=81),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+33 -13
View File
@@ -1,4 +1,7 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
)
OUTPUT_PATH = "video_samples_wan2_2_5B_ti2v" OUTPUT_PATH = "video_samples_wan2_2_5B_ti2v"
def main(): def main():
@@ -7,22 +10,34 @@ def main():
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers" model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_name, GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path=model_name,
num_gpus=1, engine=EngineConfig(
use_fsdp_inference=False, # set to True if GPU is out of memory # FastVideo will automatically handle distributed setup
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" dit=True,
# image_encoder_cpu_offload=False, vae=False,
text_encoder=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder=False,
),
),
)
) )
# I2V is triggered just by passing in an image_path argument # I2V is triggered just by passing in an image_path argument
prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside." prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside."
image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG" image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG"
video = generator.generate_video(prompt, output_path=OUTPUT_PATH, save_video=True, image_path=image_path) video = generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=image_path),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
# Generate another video with a different prompt, without reloading the # Generate another video with a different prompt, without reloading the
# model! # model!
@@ -34,8 +49,13 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, " "the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, " "embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.") "cinematic.")
video2 = generator.generate_video(prompt2, output_path=OUTPUT_PATH, save_video=True) video2 = generator.generate(
GenerationRequest(
prompt=prompt2,
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
)
)
if __name__ == "__main__": if __name__ == "__main__":
main() main()
+21 -11
View File
@@ -50,23 +50,33 @@ N_DUP = 4 # how many times to duplicate the video for the gen/ref corpora
def generate_one_ltx2_video() -> str: def generate_one_ltx2_video() -> str:
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "FLASH_ATTN") os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "FLASH_ATTN")
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig, SamplingConfig)
Path(OUTPUT_PATH).parent.mkdir(parents=True, exist_ok=True) Path(OUTPUT_PATH).parent.mkdir(parents=True, exist_ok=True)
# Davids048/LTX2-Base-Diffusers is the audio-capable LTX-2 checkpoint # Davids048/LTX2-Base-Diffusers is the audio-capable LTX-2 checkpoint
# (the Distilled variant ships without the audio VAE, so its mp4 # (the Distilled variant ships without the audio VAE, so its mp4
# audio track is silence/noise — unusable for audio.* metrics). # audio track is silence/noise — unusable for audio.* metrics).
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Davids048/LTX2-Base-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Davids048/LTX2-Base-Diffusers",
engine=EngineConfig(num_gpus=1),
)
) )
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=OUTPUT_PATH, prompt=PROMPT,
save_video=True, sampling=SamplingConfig(
num_frames=121, # ~5s @ 24 fps — long enough for audio.desync (Synchformer ≥14 segments) num_frames=121, # ~5s @ 24 fps — long enough for audio.desync (Synchformer ≥14 segments)
height=480, height=480,
width=832, width=832,
fps=24, fps=24,
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
) )
generator.shutdown() generator.shutdown()
torch.cuda.empty_cache() torch.cuda.empty_cache()
@@ -21,6 +21,10 @@ Install: ``uv pip install -e .[eval-audio]`` covers both metrics here
import torch import torch
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig,
SamplingConfig,
)
from fastvideo.eval import create_evaluator from fastvideo.eval import create_evaluator
PROMPT = ( PROMPT = (
@@ -39,20 +43,26 @@ METRICS = [
def main() -> None: def main() -> None:
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Davids048/LTX2-Base-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Davids048/LTX2-Base-Diffusers",
) engine=EngineConfig(num_gpus=1),
))
output_path = "outputs_video/ltx2_audio_eval/output.mp4" output_path = "outputs_video/ltx2_audio_eval/output.mp4"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=output_path, prompt=PROMPT,
save_video=True, sampling=SamplingConfig(
num_frames=121, num_frames=121,
height=1088, height=1088,
width=1920, width=1920,
) ),
output=OutputConfig(
output_path=output_path,
save_video=True,
),
))
generator.shutdown() generator.shutdown()
torch.cuda.empty_cache() torch.cuda.empty_cache()
+15 -10
View File
@@ -22,6 +22,10 @@ sharing, or run on a smaller-resolution generation.
import torch import torch
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig,
SamplingConfig,
)
from fastvideo.eval import Evaluator from fastvideo.eval import Evaluator
from fastvideo.eval.io import build_eval_kwargs from fastvideo.eval.io import build_eval_kwargs
@@ -58,19 +62,20 @@ METRICS = [
def main() -> None: def main() -> None:
# ----- generation (matches examples/inference/basic/basic_ltx2.py) ----- # ----- generation (matches examples/inference/basic/basic_ltx2.py) -----
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Davids048/LTX2-Base-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Davids048/LTX2-Base-Diffusers",
engine=EngineConfig(num_gpus=1),
)
) )
output_path = "outputs_video/ltx2_basic/output_ltx2_base_t2v_1088_1920_1.1.mp4" output_path = "outputs_video/ltx2_basic/output_ltx2_base_t2v_1088_1920_1.1.mp4"
generator.generate_video( generator.generate(
prompt=PROMPT, GenerationRequest(
output_path=output_path, prompt=PROMPT,
save_video=True, output=OutputConfig(output_path=output_path, save_video=True),
num_frames=121, sampling=SamplingConfig(num_frames=121, height=1088, width=1920),
height=1088, )
width=1920,
) )
generator.shutdown() generator.shutdown()
# Free residual CUDA memory the generator left behind so the # Free residual CUDA memory the generator left behind so the
+11 -5
View File
@@ -45,6 +45,9 @@ def _generate_videos(rows: list[dict], videos_dir: Path,
model: str, num_gpus: int, model: str, num_gpus: int,
num_frames: int, height: int, width: int) -> None: num_frames: int, height: int, width: int) -> None:
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig, SamplingConfig,
)
videos_dir.mkdir(parents=True, exist_ok=True) videos_dir.mkdir(parents=True, exist_ok=True)
todo = [(row, videos_dir / _expected_filename(row)) for row in rows] todo = [(row, videos_dir / _expected_filename(row)) for row in rows]
@@ -55,13 +58,16 @@ def _generate_videos(rows: list[dict], videos_dir: Path,
print(f"[gen] {len(todo)}/{len(rows)} scenarios to render with {model} " print(f"[gen] {len(todo)}/{len(rows)} scenarios to render with {model} "
f"({num_frames}x{height}x{width})...") f"({num_frames}x{height}x{width})...")
gen = VideoGenerator.from_pretrained(model, num_gpus=num_gpus) gen = VideoGenerator.from_config(GeneratorConfig(
model_path=model, engine=EngineConfig(num_gpus=num_gpus),
))
try: try:
for row, out_path in todo: for row, out_path in todo:
gen.generate_video( gen.generate(GenerationRequest(
prompt=row["prompt"], output_path=str(out_path), save_video=True, prompt=row["prompt"],
num_frames=num_frames, height=height, width=width, sampling=SamplingConfig(num_frames=num_frames, height=height, width=width),
) output=OutputConfig(output_path=str(out_path), save_video=True),
))
finally: finally:
gen.shutdown() gen.shutdown()
+9 -5
View File
@@ -43,6 +43,8 @@ def _generate_videos(prompts: list[str], videos_dir: Path,
model: str, num_gpus: int, model: str, num_gpus: int,
num_frames: int, height: int, width: int) -> None: num_frames: int, height: int, width: int) -> None:
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig, SamplingConfig)
videos_dir.mkdir(parents=True, exist_ok=True) videos_dir.mkdir(parents=True, exist_ok=True)
todo = [(p, videos_dir / f"{_slugify(p)}.mp4") for p in prompts] todo = [(p, videos_dir / f"{_slugify(p)}.mp4") for p in prompts]
@@ -53,13 +55,15 @@ def _generate_videos(prompts: list[str], videos_dir: Path,
print(f"[gen] {len(todo)}/{len(prompts)} prompts to render with {model} " print(f"[gen] {len(todo)}/{len(prompts)} prompts to render with {model} "
f"({num_frames}x{height}x{width})...") f"({num_frames}x{height}x{width})...")
gen = VideoGenerator.from_pretrained(model, num_gpus=num_gpus) gen = VideoGenerator.from_config(GeneratorConfig(
model_path=model, engine=EngineConfig(num_gpus=num_gpus)))
try: try:
for prompt, out_path in todo: for prompt, out_path in todo:
gen.generate_video( gen.generate(GenerationRequest(
prompt=prompt, output_path=str(out_path), save_video=True, prompt=prompt,
num_frames=num_frames, height=height, width=width, sampling=SamplingConfig(num_frames=num_frames, height=height, width=width),
) output=OutputConfig(output_path=str(out_path), save_video=True),
))
finally: finally:
gen.shutdown() gen.shutdown()
+26 -8
View File
@@ -33,6 +33,13 @@ import json
from pathlib import Path from pathlib import Path
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
)
from fastvideo.eval import create_evaluator from fastvideo.eval import create_evaluator
from fastvideo.eval.io import load_video from fastvideo.eval.io import load_video
@@ -99,16 +106,27 @@ def generate(args: argparse.Namespace) -> Path:
out.parent.mkdir(parents=True, exist_ok=True) out.parent.mkdir(parents=True, exist_ok=True)
print(f"[gen] loading {args.model} ({args.num_gpus} GPU)...") print(f"[gen] loading {args.model} ({args.num_gpus} GPU)...")
generator = VideoGenerator.from_pretrained(args.model, num_gpus=args.num_gpus) generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model,
engine=EngineConfig(num_gpus=args.num_gpus),
)
)
try: try:
print(f"[gen] generating to {out}...") print(f"[gen] generating to {out}...")
generator.generate_video( generator.generate(
prompt=args.prompt, GenerationRequest(
output_path=str(out), prompt=args.prompt,
save_video=True, sampling=SamplingConfig(
num_frames=args.num_frames, num_frames=args.num_frames,
height=args.height, height=args.height,
width=args.width, width=args.width,
),
output=OutputConfig(
output_path=str(out),
save_video=True,
),
)
) )
finally: finally:
generator.shutdown() generator.shutdown()
+1 -1
View File
@@ -33,7 +33,7 @@ This demo initializes a `VideoGenerator` with the minimum required arguments for
The core functionality is in the `generate_video` function, which: The core functionality is in the `generate_video` function, which:
1. Processes user inputs 1. Processes user inputs
2. Uses the FastVideo VideoGenerator from earlier to run inference (`generator.generate_video()`) 2. Uses the FastVideo VideoGenerator from earlier to run inference (`generator.generate(GenerationRequest(...))`)
## Gradio Interface ## Gradio Interface
@@ -5,7 +5,13 @@ import time
import gradio as gr import gradio as gr
from fastvideo.entrypoints.video_generator import VideoGenerator from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
SamplingParam,
)
from copy import deepcopy from copy import deepcopy
@@ -129,9 +135,22 @@ def create_gradio_interface(default_params: dict[str, SamplingParam], generators
output_dir = "outputs/" output_dir = "outputs/"
os.makedirs(output_dir, exist_ok=True) os.makedirs(output_dir, exist_ok=True)
start_time = time.time() start_time = time.time()
result = generator.generate_video(prompt=prompt, sampling_param=params, save_video=True, return_frames=False) result = generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt=params.negative_prompt,
sampling=SamplingConfig(
seed=int(params.seed),
guidance_scale=params.guidance_scale,
num_frames=int(params.num_frames),
height=int(params.height),
width=int(params.width),
),
output=OutputConfig(save_video=True, return_frames=False),
)
)
inference_time = time.time() - start_time inference_time = time.time() - start_time
logging_info = result.get("logging_info", None) logging_info = result.logging_info
if logging_info: if logging_info:
stage_names = logging_info.get_execution_order() stage_names = logging_info.get_execution_order()
stage_execution_times = [ stage_execution_times = [
@@ -550,7 +569,7 @@ def main():
for model_path in model_paths: for model_path in model_paths:
print(f"Loading model: {model_path}") print(f"Loading model: {model_path}")
setup_model_environment(model_path) setup_model_environment(model_path)
generators[model_path] = VideoGenerator.from_pretrained(model_path) generators[model_path] = VideoGenerator.from_config(GeneratorConfig(model_path=model_path))
default_params[model_path] = SamplingParam.from_pretrained(model_path) default_params[model_path] = SamplingParam.from_pretrained(model_path)
demo = create_gradio_interface(default_params, generators) demo = create_gradio_interface(default_params, generators)
print(f"Starting Gradio frontend at http://{args.host}:{args.port}") print(f"Starting Gradio frontend at http://{args.host}:{args.port}")
@@ -55,10 +55,11 @@ demo can actually boot:
`fastvideo/fastvideo_args.py` currently wires only `ltx2_vae_tiling`. `fastvideo/fastvideo_args.py` currently wires only `ltx2_vae_tiling`.
The backing stages (`ltx2_refine.py`, `ltx2_i2v_conditioning.py`) are The backing stages (`ltx2_refine.py`, `ltx2_i2v_conditioning.py`) are
also missing from `fastvideo/pipelines/stages/`. also missing from `fastvideo/pipelines/stages/`.
3. **`fastvideo.configs.sample.base.SamplingParam`** — the import path used 3. **`SamplingParam`** — now imported from `fastvideo.api` (the public
by this demo. Upstream moved sampling params to re-export of `fastvideo.api.sampling_param`); the old
`fastvideo.api.sampling_param`. A re-export shim at the old path, or an `fastvideo.configs.sample.base` path was removed upstream. `SamplingParam`
import update here once the other two prereqs land, will resolve it. here only sources model-default slider values — generation itself runs
through the typed `GenerationRequest` / `generator.generate(...)` path.
## Environment variables ## Environment variables
@@ -4,8 +4,16 @@ from pathlib import Path
import gradio as gr import gradio as gr
from fastvideo.api import (
CompileConfig,
ComponentConfig,
EngineConfig,
GeneratorConfig,
OffloadConfig,
PipelineSelection,
SamplingParam,
)
from fastvideo.configs.pipelines.base import PipelineConfig from fastvideo.configs.pipelines.base import PipelineConfig
from fastvideo.configs.sample.base import SamplingParam
from fastvideo.entrypoints.video_generator import VideoGenerator from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.layers.quantization.fp4_config import FP4Config from fastvideo.layers.quantization.fp4_config import FP4Config
from fastvideo.utils import maybe_download_model from fastvideo.utils import maybe_download_model
@@ -48,28 +56,44 @@ def main():
refine_upsampler_path = resolve_refine_upsampler_path(resolved_model_path) refine_upsampler_path = resolve_refine_upsampler_path(resolved_model_path)
print(f"Using refine upsampler: {refine_upsampler_path}") print(f"Using refine upsampler: {refine_upsampler_path}")
generators[model_path] = VideoGenerator.from_pretrained( generators[model_path] = VideoGenerator.from_config(
str(resolved_model_path), GeneratorConfig(
num_gpus=1, model_path=str(resolved_model_path),
ltx2_refine_enabled=True, engine=EngineConfig(
ltx2_refine_upsampler_path=str(refine_upsampler_path), num_gpus=1,
ltx2_refine_lora_path="", # disable refine LoRA for distilled model offload=OffloadConfig(
ltx2_refine_num_inference_steps=2, dit=False,
ltx2_refine_guidance_scale=1.0, vae=False,
ltx2_refine_add_noise=True, text_encoder=False,
pipeline_config=pipeline_config, ),
enable_torch_compile=True, compile=CompileConfig(
enable_torch_compile_text_encoder=True, enabled=True,
torch_compile_kwargs={ text_encoder_enabled=True,
"backend": "inductor", backend="inductor",
"fullgraph": True, fullgraph=True,
"mode": "max-autotune-no-cudagraphs", mode="max-autotune-no-cudagraphs",
"dynamic": False, dynamic=False,
}, ),
dit_cpu_offload=False, ),
vae_cpu_offload=False, pipeline=PipelineSelection(
text_encoder_cpu_offload=False, components=ComponentConfig(
ltx2_vae_tiling=False, upsampler_weights=str(refine_upsampler_path),
# Empty refine LoRA path (distilled needs none) -> omit.
),
vae_tiling=False,
preset_overrides={
"refine": {
"enabled": True,
"num_inference_steps": 2,
"guidance_scale": 1.0,
"add_noise": True,
},
},
# PipelineConfig object (with FP4 quant wired on above) has
# no first-class typed field; route via experimental.
experimental={"pipeline_config": pipeline_config},
),
)
) )
default_params[model_path] = apply_ltx2_defaults( default_params[model_path] = apply_ltx2_defaults(
SamplingParam.from_pretrained(str(resolved_model_path)) SamplingParam.from_pretrained(str(resolved_model_path))
@@ -4,7 +4,7 @@ from pathlib import Path
import torch import torch
import torch._inductor.config import torch._inductor.config
from fastvideo.configs.sample.base import SamplingParam from fastvideo.api import SamplingParam
LOCAL_DEMO_DIR = Path(__file__).resolve().parent LOCAL_DEMO_DIR = Path(__file__).resolve().parent
CLASSIFIER_DIR = Path( CLASSIFIER_DIR = Path(
@@ -5,8 +5,14 @@ from copy import deepcopy
import gradio as gr import gradio as gr
from fastvideo.configs.sample.base import SamplingParam from fastvideo import VideoGenerator
from fastvideo.entrypoints.video_generator import VideoGenerator from fastvideo.api import (
GenerationRequest,
InputConfig,
OutputConfig,
SamplingConfig,
SamplingParam,
)
from .config import ( from .config import (
DEFAULT_FPS, DEFAULT_FPS,
@@ -69,40 +75,38 @@ def create_gradio_interface(default_params: dict[str, SamplingParam], generators
output_path = str(OUTPUT_DIR / video_filename) output_path = str(OUTPUT_DIR / video_filename)
params.output_path = output_path params.output_path = output_path
start_time = time.perf_counter() start_time = time.perf_counter()
result = generator.generate_video( result = generator.generate(
prompt=prompt, GenerationRequest(
output_path=output_path, prompt=prompt,
fps=DEFAULT_FPS, negative_prompt=params.negative_prompt,
seed=int(params.seed), inputs=InputConfig(image_path=params.image_path),
save_video=True, sampling=SamplingConfig(
return_frames=False, seed=int(params.seed),
guidance_scale=float(params.guidance_scale), fps=DEFAULT_FPS,
height=int(params.height), guidance_scale=float(params.guidance_scale),
width=int(params.width), height=int(params.height),
num_frames=int(params.num_frames), width=int(params.width),
num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS, num_frames=int(params.num_frames),
negative_prompt=params.negative_prompt, num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS,
image_path=params.image_path, ),
ltx2_image_crf=0.0 output=OutputConfig(
output_path=output_path,
save_video=True,
return_frames=False,
),
# LTX-2 i2v knob without a first-class typed field yet.
extensions={"ltx2_image_crf": 0.0},
)
) )
wall_time = time.perf_counter() - start_time wall_time = time.perf_counter() - start_time
generation_time = ( generation_time = result.generation_time
result.get("generation_time") e2e_latency = result.extra.get("e2e_latency")
if isinstance(result, dict) else None
)
e2e_latency = (
result.get("e2e_latency")
if isinstance(result, dict) else None
)
if generation_time is None: if generation_time is None:
generation_time = wall_time generation_time = wall_time
if e2e_latency is None: if e2e_latency is None:
e2e_latency = wall_time e2e_latency = wall_time
resolved_output_path = ( resolved_output_path = result.video_path or output_path
result.get("output_path", output_path) logging_info = result.logging_info
if isinstance(result, dict) else output_path
)
logging_info = result.get("logging_info", None) if isinstance(result, dict) else None
if logging_info: if logging_info:
stage_names = logging_info.get_execution_order() stage_names = logging_info.get_execution_order()
stage_execution_times = [ stage_execution_times = [
@@ -9,6 +9,7 @@ import uvicorn
from fastapi import FastAPI, Request, HTTPException from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, FileResponse from fastapi.responses import HTMLResponse, FileResponse
from fastvideo.api import EngineConfig, GeneratorConfig, OffloadConfig
from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator
from fastvideo.models.dits.matrixgame2.utils import expand_action_to_frames from fastvideo.models.dits.matrixgame2.utils import expand_action_to_frames
@@ -572,14 +573,20 @@ def main():
print(f"Loading model: {model_path}") print(f"Loading model: {model_path}")
setup_model_environment(model_path) setup_model_environment(model_path)
generator = StreamingVideoGenerator.from_pretrained( generator = StreamingVideoGenerator.from_config(
model_path, GeneratorConfig(
num_gpus=1, model_path=model_path,
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=True,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, dit=True,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
) )
generators = {model_path: generator} generators = {model_path: generator}
@@ -3,7 +3,6 @@ import os
import torch import torch
import base64 import base64
import io import io
from copy import deepcopy
from typing import Dict, Any, Optional, List from typing import Dict, Any, Optional, List
import signal import signal
import sys import sys
@@ -20,6 +19,17 @@ import imageio
from ray.serve.handle import DeploymentHandle from ray.serve.handle import DeploymentHandle
from prometheus_client import Counter, Histogram, generate_latest from prometheus_client import Counter, Histogram, generate_latest
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
SamplingConfig,
)
NUM_GPUS = 16 NUM_GPUS = 16
DEFAULT_FPS = 16 DEFAULT_FPS = 16
SEED_RANGE_MAX = 1_000_000 SEED_RANGE_MAX = 1_000_000
@@ -136,10 +146,10 @@ def setup_model_environment(model_path: str) -> None:
def process_generation_result(result: Any) -> tuple[List[np.ndarray], float, List[str], List[float]]: def process_generation_result(result: Any) -> tuple[List[np.ndarray], float, List[str], List[float]]:
frames = result if isinstance(result, list) else result.get("frames", []) frames = result.frames or []
generation_time = result.get("generation_time", 0.0) if isinstance(result, dict) else 0.0 generation_time = result.generation_time or 0.0
logging_info = result.get("logging_info", None) logging_info = result.logging_info
if logging_info: if logging_info:
stage_names = logging_info.get_execution_order() stage_names = logging_info.get_execution_order()
stage_execution_times = [ stage_execution_times = [
@@ -153,24 +163,29 @@ def process_generation_result(result: Any) -> tuple[List[np.ndarray], float, Lis
return frames, generation_time, stage_names, stage_execution_times return frames, generation_time, stage_names, stage_execution_times
def prepare_sampling_params(video_request: VideoGenerationRequest, default_params: Any) -> Any: def prepare_generation_request(video_request: VideoGenerationRequest, image_path: Optional[str] = None) -> Any:
params = deepcopy(default_params) seed = (video_request.seed if not video_request.randomize_seed
params.prompt = video_request.prompt else torch.randint(0, SEED_RANGE_MAX, (1,)).item())
if video_request.use_negative_prompt:
params.negative_prompt = video_request.negative_prompt
params.seed = (video_request.seed if not video_request.randomize_seed # "" explicitly clears the model preset's negative prompt (None would
else torch.randint(0, SEED_RANGE_MAX, (1,)).item()) # inherit it, changing this demo's long-standing behavior).
params.randomize_seed = video_request.randomize_seed negative_prompt = video_request.negative_prompt if video_request.use_negative_prompt else ""
params.guidance_scale = video_request.guidance_scale
params.num_frames = video_request.num_frames request = GenerationRequest(
params.height = video_request.height prompt=video_request.prompt,
params.width = video_request.width negative_prompt=negative_prompt,
params.save_video = False inputs=InputConfig(image_path=image_path),
params.return_frames = True sampling=SamplingConfig(
seed=seed,
return params guidance_scale=video_request.guidance_scale,
num_frames=video_request.num_frames,
height=video_request.height,
width=video_request.width,
),
output=OutputConfig(save_video=False, return_frames=True),
)
return request, seed
class BaseModelDeployment: class BaseModelDeployment:
@@ -185,31 +200,34 @@ class BaseModelDeployment:
def _initialize_generator(self, config: Dict[str, Any]) -> None: def _initialize_generator(self, config: Dict[str, Any]) -> None:
from fastvideo.entrypoints.video_generator import VideoGenerator from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
print(f"Initializing model: {self.model_path}") print(f"Initializing model: {self.model_path}")
self.generator = VideoGenerator.from_pretrained( self.generator = VideoGenerator.from_config(
model_path=self.model_path, GeneratorConfig(
num_gpus=1, model_path=self.model_path,
use_fsdp_inference=True, engine=EngineConfig(
text_encoder_cpu_offload=config["text_encoder_cpu_offload"], num_gpus=1,
dmd_denoising_steps=[1000, 850, 700, 550, 350, 275, 200, 125], # TODO: hardocde for I2V use_fsdp_inference=True,
dit_precision="fp32", # TODO: hardocde for I2V enable_stage_verification=False,
dit_cpu_offload=config["dit_cpu_offload"], offload=OffloadConfig(
vae_cpu_offload=config["vae_cpu_offload"], text_encoder=config["text_encoder_cpu_offload"],
VSA_sparsity=config["VSA_sparsity"], dit=config["dit_cpu_offload"],
enable_stage_verification=False, vae=config["vae_cpu_offload"],
),
),
pipeline=PipelineSelection(
# I2V knobs without first-class typed fields yet.
experimental={
"dmd_denoising_steps": [1000, 850, 700, 550, 350, 275, 200, 125],
"dit_precision": "fp32",
"VSA_sparsity": config["VSA_sparsity"],
},
),
)
) )
self.default_params = SamplingParam.from_pretrained(self.model_path)
self.default_params.seed = 1000
self.default_params.num_frames = 73
self.default_params.width = 832
self.default_params.height = 480
def generate_video(self, video_request: VideoGenerationRequest) -> VideoGenerationResponse: def generate_video(self, video_request: VideoGenerationRequest) -> VideoGenerationResponse:
total_start_time = time.time() total_start_time = time.time()
params = prepare_sampling_params(video_request, self.default_params)
# Save image if provided (for I2V) # Save image if provided (for I2V)
image_path = None image_path = None
@@ -218,19 +236,15 @@ class BaseModelDeployment:
if image_path is None: if image_path is None:
return VideoGenerationResponse( return VideoGenerationResponse(
video_data=None, video_data=None,
seed=params.seed, seed=video_request.seed,
success=False, success=False,
error_message="Failed to save input image", error_message="Failed to save input image",
) )
request, seed = prepare_generation_request(video_request, image_path)
inference_start_time = time.time() inference_start_time = time.time()
result = self.generator.generate_video( result = self.generator.generate(request)
prompt=video_request.prompt,
sampling_param=params,
image_path=image_path,
save_video=False,
return_frames=True,
)
inference_time = time.time() - inference_start_time inference_time = time.time() - inference_start_time
frames, generation_time, stage_names, stage_execution_times = process_generation_result(result) frames, generation_time, stage_names, stage_execution_times = process_generation_result(result)
@@ -250,7 +264,7 @@ class BaseModelDeployment:
return VideoGenerationResponse( return VideoGenerationResponse(
video_data=video_data, video_data=video_data,
seed=params.seed, seed=seed,
success=True, success=True,
generation_time=generation_time, generation_time=generation_time,
inference_time=inference_time, inference_time=inference_time,
+45 -25
View File
@@ -1,18 +1,31 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (
ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, PipelineSelection, SamplingConfig,
)
OUTPUT_PATH = "./lora_out" OUTPUT_PATH = "./lora_out"
def main(): def main():
# Initialize VideoGenerator with the Wan model # Initialize VideoGenerator with the Wan model
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
dit_cpu_offload=False, engine=EngineConfig(
vae_cpu_offload=False, num_gpus=1,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" dit=False,
lora_path="benjamin-paine/steamboat-willie-1.3b", vae=False,
lora_nickname="steamboat" text_encoder=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="benjamin-paine/steamboat-willie-1.3b",
),
experimental={"lora_nickname": "steamboat"},
),
)
) )
kwargs = { kwargs = {
"height": 480, "height": 480,
@@ -26,25 +39,32 @@ def main():
prompt = "steamboat willie style, golden era animation, close-up of a short fluffy monster kneeling beside a melting red candle. the mood is one of wonder and curiosity, as the monster gazes at the flame with wide eyes and open mouth. Its pose and expression convey a sense of innocence and playfulness, as if it is exploring the world around it for the first time. The use of warm colors and dramatic lighting further enhances the cozy atmosphere of the image." prompt = "steamboat willie style, golden era animation, close-up of a short fluffy monster kneeling beside a melting red candle. the mood is one of wonder and curiosity, as the monster gazes at the flame with wide eyes and open mouth. Its pose and expression convey a sense of innocence and playfulness, as if it is exploring the world around it for the first time. The use of warm colors and dramatic lighting further enhances the cozy atmosphere of the image."
negative_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" negative_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
# sampling_param=sampling_param, prompt=prompt,
output_path=OUTPUT_PATH, negative_prompt=negative_prompt,
save_video=True, sampling=SamplingConfig(**kwargs),
negative_prompt=negative_prompt, output=OutputConfig(
**kwargs output_path=OUTPUT_PATH,
save_video=True,
),
)
) )
generator.set_lora_adapter(lora_nickname="flat_color", lora_path="motimalu/wan-flat-color-1.3b-v2") generator.set_lora_adapter(lora_nickname="flat_color", lora_path="motimalu/wan-flat-color-1.3b-v2")
prompt = "flat color, no lineart, blending, negative space, artist:[john kafka|ponsuke kaikai|hara id 21|yoneyama mai|fuzichoco], 1girl, sakura miko, pink hair, cowboy shot, white shirt, floral print, off shoulder, outdoors, cherry blossom, tree shade, wariza, looking up, falling petals, half-closed eyes, white sky, clouds, live2d animation, upper body, high quality cinematic video of a woman sitting under a sakura tree. Dreamy and lonely, the camera close-ups on the face of the woman as she turns towards the viewer. The Camera is steady, This is a cowboy shot. The animation is smooth and fluid." prompt = "flat color, no lineart, blending, negative space, artist:[john kafka|ponsuke kaikai|hara id 21|yoneyama mai|fuzichoco], 1girl, sakura miko, pink hair, cowboy shot, white shirt, floral print, off shoulder, outdoors, cherry blossom, tree shade, wariza, looking up, falling petals, half-closed eyes, white sky, clouds, live2d animation, upper body, high quality cinematic video of a woman sitting under a sakura tree. Dreamy and lonely, the camera close-ups on the face of the woman as she turns towards the viewer. The Camera is steady, This is a cowboy shot. The animation is smooth and fluid."
negative_prompt = "bad quality video,色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走" negative_prompt = "bad quality video,色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt,
save_video=True, negative_prompt=negative_prompt,
negative_prompt=negative_prompt, sampling=SamplingConfig(**kwargs),
**kwargs output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
) )
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -2,45 +2,60 @@
Inference using a LoRA checkpoint from FastVideo trainer. Inference using a LoRA checkpoint from FastVideo trainer.
""" """
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam from fastvideo.api import (ComponentConfig, EngineConfig, GenerationRequest,
GeneratorConfig, OffloadConfig, OutputConfig,
PipelineSelection, SamplingConfig)
OUTPUT_PATH = "./lora_out" OUTPUT_PATH = "./lora_out"
def main(): def main():
# Initialize VideoGenerator with the Wan model # Initialize VideoGenerator with the Wan model
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
dit_cpu_offload=False, engine=EngineConfig(
vae_cpu_offload=True, num_gpus=1,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" dit=False,
lora_path="checkpoints/wan_t2v_finetune_lora/checkpoint-160/transformer", vae=True,
lora_nickname="crush_smol" text_encoder=True,
) pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="checkpoints/wan_t2v_finetune_lora/checkpoint-160/transformer",
),
experimental={"lora_nickname": "crush_smol"},
),
))
generator.unmerge_lora_weights() generator.unmerge_lora_weights()
kwargs = { sampling = SamplingConfig(
"height": 480, height=480,
"width": 832, width=832,
"num_frames": 77, num_frames=77,
"guidance_scale": 6.0, guidance_scale=6.0,
"num_inference_steps": 50, num_inference_steps=50,
"seed": 42, seed=42,
} )
output = OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
)
# Generate video with LoRA style # Generate video with LoRA style
prompt = "A large metal cylinder is seen pressing down on a pile of Oreo cookies, flattening them as if they were under a hydraulic press." prompt = "A large metal cylinder is seen pressing down on a pile of Oreo cookies, flattening them as if they were under a hydraulic press."
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt,
save_video=True, sampling=sampling,
**kwargs output=output,
) ))
prompt = "A large metal cylinder is seen compressing colorful clay into a compact shape, demonstrating the power of a hydraulic press." prompt = "A large metal cylinder is seen compressing colorful clay into a compact shape, demonstrating the power of a hydraulic press."
video = generator.generate_video( video = generator.generate(
prompt, GenerationRequest(
output_path=OUTPUT_PATH, prompt=prompt,
save_video=True, sampling=sampling,
**kwargs output=output,
) ))
if __name__ == "__main__": if __name__ == "__main__":
main() main()
@@ -37,6 +37,17 @@ import imageio
import torch import torch
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig,
ComponentConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
SamplingConfig,
)
from fastvideo.configs.pipelines.base import PipelineConfig from fastvideo.configs.pipelines.base import PipelineConfig
from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig from fastvideo.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
@@ -148,35 +159,49 @@ def build_generator(args: argparse.Namespace) -> VideoGenerator:
compile_enabled = not args.no_compile compile_enabled = not args.no_compile
extra_kwargs = {} # ``pipeline_config`` is a PipelineConfig object (not a string path) and
# ``output_type`` has no first-class typed field, so both are routed through
# the pipeline experimental escape hatch.
experimental = {"pipeline_config": pipeline_config}
components = ComponentConfig()
if args.distilled_model: if args.distilled_model:
weights_path = resolve_distilled_weights(args.distilled_model) weights_path = resolve_distilled_weights(args.distilled_model)
print(f"Using distilled weights: {args.distilled_model} -> {weights_path}") print(f"Using distilled weights: {args.distilled_model} -> {weights_path}")
extra_kwargs["init_weights_from_safetensors"] = weights_path components.transformer_weights = weights_path
if args.taehv: if args.taehv:
# Skip the in-pipeline VAE decode entirely: the pipeline returns raw # Skip the in-pipeline VAE decode entirely: the pipeline returns raw
# latents, the Wan VAE is offloaded to CPU (and not compiled) since we # latents, the Wan VAE is offloaded to CPU (and not compiled) since we
# decode with TAEHV in this script instead. # decode with TAEHV in this script instead.
extra_kwargs["output_type"] = "latent" experimental["output_type"] = "latent"
generator = VideoGenerator.from_pretrained( generator_config = GeneratorConfig(
model_id, model_path=model_id,
pipeline_config=pipeline_config, engine=EngineConfig(
num_gpus=args.num_gpus, num_gpus=args.num_gpus,
# Keep everything resident on the GPU -- no offloading, except the # Keep everything resident on the GPU -- no offloading, except the
# unused Wan VAE when TAEHV handles decoding. # unused Wan VAE when TAEHV handles decoding.
use_fsdp_inference=False, use_fsdp_inference=False,
dit_cpu_offload=False, offload=OffloadConfig(
dit_layerwise_offload=False, dit=False,
vae_cpu_offload=args.taehv, dit_layerwise=False,
text_encoder_cpu_offload=False, vae=args.taehv,
pin_cpu_memory=False, text_encoder=False,
enable_torch_compile=compile_enabled, pin_cpu_memory=False,
enable_torch_compile_text_encoder=compile_enabled, ),
enable_torch_compile_vae=compile_enabled and not args.taehv, compile=CompileConfig(
**extra_kwargs, enabled=compile_enabled,
text_encoder_enabled=compile_enabled,
vae_enabled=compile_enabled and not args.taehv,
),
),
pipeline=PipelineSelection(
components=components,
experimental=experimental,
),
) )
generator = VideoGenerator.from_config(generator_config)
return generator return generator
@@ -237,11 +262,11 @@ def main() -> None:
# runs below measure steady-state latency only. # runs below measure steady-state latency only.
with silence_request_log(): with silence_request_log():
for _ in range(args.warmups): for _ in range(args.warmups):
warm = generator.generate(request={ warm = generator.generate(GenerationRequest(
"prompt": PROMPT, prompt=PROMPT,
"sampling": {"num_inference_steps": 2, "guidance_scale": args.guidance_scale}, sampling=SamplingConfig(num_inference_steps=2, guidance_scale=args.guidance_scale),
"output": {"save_video": False, "return_frames": args.taehv}, output=OutputConfig(save_video=False, return_frames=args.taehv),
}) ))
if args.taehv: if args.taehv:
taehv.decode(warm.samples) taehv.decode(warm.samples)
@@ -257,18 +282,18 @@ def main() -> None:
frames = None frames = None
with silence_request_log(): with silence_request_log():
for i in range(args.benchmark_runs): for i in range(args.benchmark_runs):
result = generator.generate(request={ result = generator.generate(GenerationRequest(
"prompt": PROMPT, prompt=PROMPT,
"sampling": { sampling=SamplingConfig(
"num_inference_steps": args.infer_steps, num_inference_steps=args.infer_steps,
"guidance_scale": args.guidance_scale, guidance_scale=args.guidance_scale,
}, ),
"output": { output=OutputConfig(
"save_video": False, save_video=False,
"return_frames": args.taehv, return_frames=args.taehv,
"output_path": output_path, output_path=output_path,
}, ),
}) ))
denoise_elapsed = result.generation_time denoise_elapsed = result.generation_time
denoise_times.append(denoise_elapsed) denoise_times.append(denoise_elapsed)
@@ -2,31 +2,41 @@ import os
import time import time
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig,
OutputConfig, SamplingConfig,
)
def main(): def main():
# set the attention backend # set the attention backend
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN" os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
start_time = time.perf_counter() start_time = time.perf_counter()
gen = VideoGenerator.from_pretrained( gen = VideoGenerator.from_config(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers", GeneratorConfig(
num_gpus=1, model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
dit_cpu_offload=False, engine=EngineConfig(
vae_cpu_offload=False, num_gpus=1,
text_encoder_cpu_offload=True, offload=OffloadConfig(
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" dit=False,
) vae=False,
text_encoder=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
),
),
))
load_time = time.perf_counter() - start_time load_time = time.perf_counter() - start_time
print(f"Model loading time: {load_time:.2f} seconds") print(f"Model loading time: {load_time:.2f} seconds")
gen_start_time = time.perf_counter() gen_start_time = time.perf_counter()
gen.generate_video( gen.generate(
prompt= GenerationRequest(
"Will Smith casually eats noodles, his relaxed demeanor contrasting with the energetic background of a bustling street food market. The scene captures a mix of humor and authenticity. Mid-shot framing, vibrant lighting.", prompt=
seed=1024, "Will Smith casually eats noodles, his relaxed demeanor contrasting with the energetic background of a bustling street food market. The scene captures a mix of humor and authenticity. Mid-shot framing, vibrant lighting.",
output_path="example_outputs/") sampling=SamplingConfig(seed=1024),
output=OutputConfig(output_path="example_outputs/")))
generation_time = time.perf_counter() - gen_start_time generation_time = time.perf_counter() - gen_start_time
print(f"Video generation time: {generation_time:.2f} seconds") print(f"Video generation time: {generation_time:.2f} seconds")
@@ -18,6 +18,10 @@ import os
import time import time
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples" OUTPUT_PATH = "video_samples"
@@ -39,17 +43,24 @@ def main():
mode += "_compile" mode += "_compile"
print(f"Mode: {mode.upper()}") print(f"Mode: {mode.upper()}")
generator = VideoGenerator.from_pretrained( if args.nvfp4_fa4:
args.model, os.environ["FASTVIDEO_NVFP4_FA4"] = "1"
num_gpus=args.num_gpus, os.environ.setdefault("CUTE_DSL_ENABLE_TVM_FFI", "1")
nvfp4_fa4=args.nvfp4_fa4,
use_fsdp_inference=not args.nvfp4_fa4, generator = VideoGenerator.from_config(GeneratorConfig(
dit_cpu_offload=False, model_path=args.model,
dit_layerwise_offload=False, engine=EngineConfig(
vae_cpu_offload=True, num_gpus=args.num_gpus,
text_encoder_cpu_offload=True, use_fsdp_inference=not args.nvfp4_fa4,
enable_torch_compile=args.compile, offload=OffloadConfig(
) dit=False,
dit_layerwise=False,
vae=True,
text_encoder=True,
),
compile=CompileConfig(enabled=args.compile),
),
))
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
@@ -59,16 +70,22 @@ def main():
n_warmup = 2 if args.compile else 1 n_warmup = 2 if args.compile else 1
for i in range(n_warmup): for i in range(n_warmup):
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 2}, generator.generate(GenerationRequest(
"output": {"save_video": False}}) prompt=prompt,
sampling=SamplingConfig(num_inference_steps=2),
output=OutputConfig(save_video=False),
))
os.makedirs(OUTPUT_PATH, exist_ok=True) os.makedirs(OUTPUT_PATH, exist_ok=True)
start = time.time() start = time.time()
generator.generate(request={ generator.generate(GenerationRequest(
"prompt": prompt, prompt=prompt,
"sampling": {"num_inference_steps": args.infer_steps}, sampling=SamplingConfig(num_inference_steps=args.infer_steps),
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")}, output=OutputConfig(
}) save_video=True,
output_path=os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4"),
),
))
elapsed = time.time() - start elapsed = time.time() - start
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s " print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
f"({args.infer_steps / elapsed:.2f} it/s)") f"({args.infer_steps / elapsed:.2f} it/s)")
@@ -69,6 +69,16 @@ def main():
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "SAGE_ATTN") os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "SAGE_ATTN")
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
SamplingConfig,
)
from fastvideo.layers.quantization import get_quantization_config from fastvideo.layers.quantization import get_quantization_config
mode = "bf16" if args.bf16 else f"fp8_{args.granularity}" mode = "bf16" if args.bf16 else f"fp8_{args.granularity}"
@@ -79,25 +89,33 @@ def main():
taehv_model = load_taehv(args.taehv_checkpoint) if use_taehv else None taehv_model = load_taehv(args.taehv_checkpoint) if use_taehv else None
# transformer_quant needs a QuantizationConfig *instance* — the bare string # ``output_type`` and ``transformer_quant`` have no first-class typed
# is not resolved on the from_pretrained kwarg path. # fields yet, so they ride the pipeline.experimental escape hatch (same
extra = {} if args.bf16 else { # place the legacy from_pretrained shim routed them). The typed
"transformer_quant": get_quantization_config("FP8")(granularity=args.granularity) # QuantizationConfig only accepts a quant-name string, so it can't carry
} # FP8's ``granularity`` arg — pass the resolved config instance instead.
generator = VideoGenerator.from_pretrained( experimental = {"output_type": "latent" if use_taehv else "pil"}
args.model, if not args.bf16:
num_gpus=args.num_gpus, experimental["transformer_quant"] = get_quantization_config("FP8")(granularity=args.granularity)
use_fsdp_inference=False, generator = VideoGenerator.from_config(GeneratorConfig(
dit_cpu_offload=False, model_path=args.model,
dit_layerwise_offload=False, engine=EngineConfig(
vae_cpu_offload=use_taehv, num_gpus=args.num_gpus,
text_encoder_cpu_offload=False, use_fsdp_inference=False,
pin_cpu_memory=False, offload=OffloadConfig(
enable_torch_compile=not args.no_compile, dit=False,
enable_torch_compile_vae=not args.no_compile and not use_taehv, dit_layerwise=False,
output_type="latent" if use_taehv else "pil", vae=use_taehv,
**extra, text_encoder=False,
) pin_cpu_memory=False,
),
compile=CompileConfig(
enabled=not args.no_compile,
vae_enabled=not args.no_compile and not use_taehv,
),
),
pipeline=PipelineSelection(experimental=experimental),
))
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
@@ -107,28 +125,34 @@ def main():
n_warmup = 1 if not args.no_compile else 0 n_warmup = 1 if not args.no_compile else 0
for _ in range(n_warmup): for _ in range(n_warmup):
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 3, "guidance_scale": 1.0}, generator.generate(GenerationRequest(
"output": {"save_video": False}}) prompt=prompt,
sampling=SamplingConfig(num_inference_steps=3, guidance_scale=1.0),
output=OutputConfig(save_video=False),
))
os.makedirs(OUTPUT_PATH, exist_ok=True) os.makedirs(OUTPUT_PATH, exist_ok=True)
start = time.time() start = time.time()
if use_taehv: if use_taehv:
result = generator.generate(request={ result = generator.generate(GenerationRequest(
"prompt": prompt, prompt=prompt,
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0}, sampling=SamplingConfig(num_inference_steps=args.infer_steps, guidance_scale=1.0),
"output": {"save_video": False}, output=OutputConfig(save_video=False),
}) ))
import imageio import imageio
frames = decode_with_taehv(taehv_model, result.samples) frames = decode_with_taehv(taehv_model, result.samples)
video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4") video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
imageio.mimsave(video_path, frames, fps=16, format="mp4") imageio.mimsave(video_path, frames, fps=16, format="mp4")
print(f"Saved TAEHV-decoded video to: {video_path}") print(f"Saved TAEHV-decoded video to: {video_path}")
else: else:
generator.generate(request={ generator.generate(GenerationRequest(
"prompt": prompt, prompt=prompt,
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0}, sampling=SamplingConfig(num_inference_steps=args.infer_steps, guidance_scale=1.0),
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")}, output=OutputConfig(
}) save_video=True,
output_path=os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4"),
),
))
elapsed = time.time() - start elapsed = time.time() - start
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s " print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
f"({args.infer_steps / elapsed:.2f} it/s)") f"({args.infer_steps / elapsed:.2f} it/s)")
@@ -45,26 +45,29 @@ def main():
# Import after the env var so the platform picks up the selection. # Import after the env var so the platform picks up the selection.
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.layers.quantization import get_quantization_config from fastvideo.api import (
CompileConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, QuantizationConfig, SamplingConfig,
)
mode = "bf16" if args.bf16 else args.quant_method mode = "bf16" if args.bf16 else args.quant_method
if args.compile: if args.compile:
mode += "_compile" mode += "_compile"
print(f"Mode: {mode.upper()}") print(f"Mode: {mode.upper()}")
# transformer_quant needs a QuantizationConfig *instance* — the bare string # transformer_quant takes the config name (string); the typed path resolves
# is not resolved on the from_pretrained kwarg path. # the QuantizationConfig class.
extra = {} if args.bf16 else {"transformer_quant": get_quantization_config(args.quant_method)()} quantization = None if args.bf16 else QuantizationConfig(transformer_quant=args.quant_method)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(GeneratorConfig(
args.model, model_path=args.model,
num_gpus=args.num_gpus, engine=EngineConfig(
use_fsdp_inference=args.bf16, num_gpus=args.num_gpus,
dit_cpu_offload=False, use_fsdp_inference=args.bf16,
vae_cpu_offload=True, offload=OffloadConfig(dit=False, vae=True, text_encoder=True),
text_encoder_cpu_offload=True, compile=CompileConfig(enabled=args.compile),
enable_torch_compile=args.compile, quantization=quantization,
**extra, ),
) ))
prompt = ( prompt = (
"A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes " "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes "
@@ -74,16 +77,20 @@ def main():
n_warmup = 2 if args.compile else 1 n_warmup = 2 if args.compile else 1
for _ in range(n_warmup): for _ in range(n_warmup):
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 2}, generator.generate(GenerationRequest(
"output": {"save_video": False}}) prompt=prompt,
sampling=SamplingConfig(num_inference_steps=2),
output=OutputConfig(save_video=False),
))
os.makedirs(OUTPUT_PATH, exist_ok=True) os.makedirs(OUTPUT_PATH, exist_ok=True)
start = time.time() start = time.time()
generator.generate(request={ generator.generate(GenerationRequest(
"prompt": prompt, prompt=prompt,
"sampling": {"num_inference_steps": args.infer_steps}, sampling=SamplingConfig(num_inference_steps=args.infer_steps),
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")}, output=OutputConfig(save_video=True,
}) output_path=os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")),
))
elapsed = time.time() - start elapsed = time.time() - start
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s " print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
f"({args.infer_steps / elapsed:.2f} it/s)") f"({args.infer_steps / elapsed:.2f} it/s)")
@@ -1,4 +1,7 @@
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (ComponentConfig, EngineConfig, GenerationRequest,
GeneratorConfig, InputConfig, OffloadConfig,
OutputConfig, PipelineSelection, QuantizationConfig)
import argparse import argparse
OUTPUT_PATH = "video_samples_wan2_2_5B_ti2v" OUTPUT_PATH = "video_samples_wan2_2_5B_ti2v"
@@ -10,28 +13,39 @@ def main(text_encoder_path: str):
# If a local path is provided, FastVideo will make a best effort # If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments. # attempt to identify the optimal arguments.
model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers" model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
model_name, GeneratorConfig(
# FastVideo will automatically handle distributed setup model_path=model_name,
num_gpus=1, # FastVideo will automatically handle distributed setup
use_fsdp_inference=True, engine=EngineConfig(
dit_cpu_offload=True, num_gpus=1,
vae_cpu_offload=False, use_fsdp_inference=True,
text_encoder_cpu_offload=False, offload=OffloadConfig(
# AbsMaxFP8 is the quantization method used by ComfyUI; dit=True,
# check fastvideo/layers/quantization/* for more quantization methods vae=False,
override_text_encoder_quant="AbsMaxFP8", text_encoder=False,
# for Wan 2.2, this is the path to "umt5_xxl_fp8_e4m3fn_scaled.safetensors" pin_cpu_memory=
override_text_encoder_safetensors=text_encoder_path, True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument" ),
) # AbsMaxFP8 is the quantization method used by ComfyUI;
# check fastvideo/layers/quantization/* for more quantization methods
quantization=QuantizationConfig(text_encoder_quant="AbsMaxFP8"),
),
pipeline=PipelineSelection(
components=ComponentConfig(
# for Wan 2.2, this is the path to "umt5_xxl_fp8_e4m3fn_scaled.safetensors"
text_encoder_weights=text_encoder_path, ), ),
))
# I2V is triggered just by passing in an image_path argument # I2V is triggered just by passing in an image_path argument
prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside." prompt = "Summer beach vacation style, a white cat wearing sunglasses sits on a surfboard. The fluffy-furred feline gazes directly at the camera with a relaxed expression. Blurred beach scenery forms the background featuring crystal-clear waters, distant green hills, and a blue sky dotted with white clouds. The cat assumes a naturally relaxed posture, as if savoring the sea breeze and warm sunlight. A close-up shot highlights the feline's intricate details and the refreshing atmosphere of the seaside."
image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG" image_path = "https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG"
video = generator.generate_video( video = generator.generate(
prompt, output_path=OUTPUT_PATH, save_video=True, image_path=image_path GenerationRequest(
) prompt=prompt,
inputs=InputConfig(image_path=image_path),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
if __name__ == "__main__": if __name__ == "__main__":
@@ -22,6 +22,14 @@ import os
import time import time
from fastvideo import VideoGenerator from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
)
PROMPT = ( PROMPT = (
"A high-definition video of a robotic arm welding a metal structure, " "A high-definition video of a robotic arm welding a metal structure, "
@@ -42,24 +50,27 @@ def main() -> None:
os.makedirs("video_samples", exist_ok=True) os.makedirs("video_samples", exist_ok=True)
generator = VideoGenerator.from_pretrained( generator = VideoGenerator.from_config(
args.model, GeneratorConfig(
num_gpus=args.num_gpus, model_path=args.model,
enable_torch_compile=args.compile, engine=EngineConfig(
num_gpus=args.num_gpus,
compile=CompileConfig(enabled=args.compile),
),
)
) )
def _run(tag: str) -> float: def _run(tag: str) -> float:
save = tag == "measured" save = tag == "measured"
# Modern typed-request API (generate_video is deprecated). Same # Same prompt/seed/shapes both runs so the compiled graph is reused.
# prompt/seed/shapes both runs so the compiled graph is reused. output = OutputConfig(save_video=save)
request: dict = {
"prompt": PROMPT,
"sampling": {"seed": 1024},
"output": {"save_video": save},
}
if save: if save:
request["output"]["output_path"] = ( output.output_path = f"video_samples/torch_compile_{tag}.mp4"
f"video_samples/torch_compile_{tag}.mp4") request = GenerationRequest(
prompt=PROMPT,
sampling=SamplingConfig(seed=1024),
output=output,
)
t0 = time.perf_counter() t0 = time.perf_counter()
generator.generate(request) generator.generate(request)
return time.perf_counter() - t0 return time.perf_counter() - t0
+22 -3
View File
@@ -37,15 +37,34 @@ def parse_config(config_type: type[T], raw: Mapping[str, Any] | T) -> T:
if not isinstance(raw, Mapping): if not isinstance(raw, Mapping):
raise ConfigValidationError("", f"expected mapping for {config_type.__name__}") raise ConfigValidationError("", f"expected mapping for {config_type.__name__}")
parsed = _SchemaParser().parse_dataclass(config_type, raw, "") parsed = _SchemaParser().parse_dataclass(config_type, raw, "")
# None is the schema-wide sentinel for "not specified, inherit the
# model preset", so an explicit YAML/JSON null must not bind as an
# explicit path — otherwise it would stomp preset values with None.
if config_type is GenerationRequest: if config_type is GenerationRequest:
return bind_generation_request_raw(parsed, raw) return bind_generation_request_raw(parsed, drop_none_leaves(raw))
if config_type is RunConfig: if config_type is RunConfig:
return bind_run_config_raw(parsed, raw) return bind_run_config_raw(parsed, drop_none_leaves(raw))
if config_type is ServeConfig: if config_type is ServeConfig:
return bind_serve_config_raw(parsed, raw) return bind_serve_config_raw(parsed, drop_none_leaves(raw))
return parsed return parsed
def drop_none_leaves(raw: Any) -> Any:
"""Prune None leaves — and dicts emptied by the pruning — from a raw
config mapping, so they are never recorded as explicit paths."""
if not isinstance(raw, dict):
return raw
pruned: dict[str, Any] = {}
for key, value in raw.items():
if value is None:
continue
value = drop_none_leaves(value)
if isinstance(value, dict) and not value:
continue
pruned[key] = value
return pruned
def config_to_dict(config: Any) -> Any: def config_to_dict(config: Any) -> Any:
"""Serialize a typed config object into plain Python containers.""" """Serialize a typed config object into plain Python containers."""
if dataclasses.is_dataclass(config) and not isinstance(config, type): if dataclasses.is_dataclass(config) and not isinstance(config, type):
+27 -16
View File
@@ -136,19 +136,27 @@ class InputConfig:
@dataclass @dataclass
class SamplingConfig: class SamplingConfig:
num_videos_per_prompt: int = 1 """Sampling knobs for a generation request.
seed: int = 1024
num_frames: int = 125 ``None`` means "not specified": the value is inherited from the
height: int = 720 model's preset (``SamplingParam.from_pretrained``) at generate time.
width: int = 1280 Set a field to override the preset. There is deliberately no way to
height_sr: int = 1072 express "explicitly the schema default" — pass the concrete value
width_sr: int = 1920 you want instead.
fps: int = 24 """
num_inference_steps: int = 50 num_videos_per_prompt: int | None = None
num_inference_steps_sr: int = 50 seed: int | None = None
guidance_scale: float = 1.0 num_frames: int | None = None
height: int | None = None
width: int | None = None
height_sr: int | None = None
width_sr: int | None = None
fps: int | None = None
num_inference_steps: int | None = None
num_inference_steps_sr: int | None = None
guidance_scale: float | None = None
guidance_scale_2: float | None = None guidance_scale_2: float | None = None
guidance_rescale: float = 0.0 guidance_rescale: float | None = None
true_cfg_scale: float | None = None true_cfg_scale: float | None = None
boundary_ratio: float | None = None boundary_ratio: float | None = None
sigmas: list[float] | None = None sigmas: list[float] | None = None
@@ -194,6 +202,8 @@ class GenerationPlan:
class GenerationRequest: class GenerationRequest:
prompt: str | list[str] | None = None prompt: str | list[str] | None = None
negative_prompt: str | None = None negative_prompt: str | None = None
"""``None`` inherits the model preset's negative prompt; pass ``""``
to explicitly clear it."""
inputs: InputConfig = field(default_factory=InputConfig) inputs: InputConfig = field(default_factory=InputConfig)
sampling: SamplingConfig = field(default_factory=SamplingConfig) sampling: SamplingConfig = field(default_factory=SamplingConfig)
runtime: RequestRuntimeConfig = field(default_factory=RequestRuntimeConfig) runtime: RequestRuntimeConfig = field(default_factory=RequestRuntimeConfig)
@@ -261,15 +271,16 @@ class ServeConfig:
the incoming body as the operator-pinned baseline. the incoming body as the operator-pinned baseline.
Important nuance: only fields the operator **explicitly wrote** in the Important nuance: only fields the operator **explicitly wrote** in the
serve YAML/JSON count as defaults. Although the in-memory object is serve YAML/JSON count as defaults. Unset sampling fields stay ``None``
fully populated (schema defaults fill every unset field), the merge ("inherit the model preset"), other sections keep their schema
walks ``_fastvideo_explicit_paths`` — populated during parse — so defaults in memory — but the merge walks ``_fastvideo_explicit_paths``
(populated during parse; explicit ``null`` is treated as unset), so
unset fields are *not* forced onto requests. Per-request precedence: unset fields are *not* forced onto requests. Per-request precedence:
body (client-explicit) > default_request (operator-explicit) body (client-explicit) > default_request (operator-explicit)
> hardcoded fallback (e.g. ``fps=24``) > hardcoded fallback (e.g. ``fps=24``)
See :func:`fastvideo.api.compat.explicit_request_updates` for the See :func:`fastvideo.api.translation.explicit_request_updates` for the
projection and ``entrypoints/openai/video_api.py::_build_generation_kwargs`` projection and ``entrypoints/openai/video_api.py::_build_generation_kwargs``
for the merge. for the merge.
""" """
@@ -1,4 +1,22 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
"""Translation layer between the public typed API and the legacy internals.
This module (formerly ``fastvideo/api/compat.py``) is the single bridge that
converts typed public config objects (:class:`GeneratorConfig`,
:class:`GenerationRequest`) into the internal :class:`FastVideoArgs` /
:class:`SamplingParam` the runtime still consumes, plus the inbound
normalization helpers used by every public entrypoint.
It is *internal* and not part of the stable public surface. The legacy
forward-translation helpers (``legacy_from_pretrained_to_config``,
``legacy_generate_call_to_request`` and friends) exist only to support the
deprecated ``VideoGenerator.from_pretrained(model, **kwargs)`` /
``generate_video(...)`` entry points. The reverse-translation
(``generator_config_to_fastvideo_args``, ``request_to_sampling_param``) is
slated for elimination once ``FastVideoArgs`` becomes a view over
``GeneratorConfig`` and ``ForwardBatch`` reads ``GenerationRequest`` directly
(see ``.agents/memory/dreamverse-integration/pr-roadmap.md``, PRs 15-17).
"""
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping from collections.abc import Mapping
@@ -8,7 +26,12 @@ from pathlib import Path
from typing import Any from typing import Any
from fastvideo.api.overrides import apply_overrides, normalize_overrides from fastvideo.api.overrides import apply_overrides, normalize_overrides
from fastvideo.api.parser import config_to_dict, load_raw_config, parse_config from fastvideo.api.parser import (
config_to_dict,
drop_none_leaves,
load_raw_config,
parse_config,
)
from fastvideo.api.request_metadata import ( from fastvideo.api.request_metadata import (
EXPLICIT_PATHS_ATTR, EXPLICIT_PATHS_ATTR,
bind_generation_request_raw, bind_generation_request_raw,
@@ -314,8 +337,12 @@ def normalize_generation_request(request: GenerationRequest | Mapping[str, Any],
if not hasattr(normalized, EXPLICIT_PATHS_ATTR): if not hasattr(normalized, EXPLICIT_PATHS_ATTR):
# Request wasn't bound through the parser (e.g. constructed # Request wasn't bound through the parser (e.g. constructed
# directly). Treat every currently-set field as explicit. # directly). Bind only non-None leaves as explicit: None is the
bind_generation_request_raw(normalized, _serialize_generation_request(normalized)) # schema-wide sentinel for "not specified, inherit the model
# preset", so treating it as explicit would stomp preset values
# with schema defaults. (parse_config applies the same pruning
# to YAML/JSON nulls.)
bind_generation_request_raw(normalized, drop_none_leaves(_serialize_generation_request(normalized)))
return normalized return normalized
@@ -364,15 +391,13 @@ def request_to_sampling_param(
updates = explicit_request_updates(request) updates = explicit_request_updates(request)
for key, value in updates.items(): for key, value in updates.items():
if key == "return_state":
# Already translated to return_continuation_state above.
continue
if hasattr(sampling_param, key): if hasattr(sampling_param, key):
setattr(sampling_param, key, deepcopy(value)) setattr(sampling_param, key, deepcopy(value))
elif key in _REQUEST_PIPELINE_OVERRIDE_FIELDS: elif key in _REQUEST_PIPELINE_OVERRIDE_FIELDS:
continue continue
elif value == _SCHEMA_DEFAULT_UPDATES.get(key, _MISSING):
# Schema-default field that isn't on SamplingParam; tolerated
# because direct GenerationRequest(...) construction has no
# way to distinguish "user set" from "schema default".
continue
else: else:
raise ValueError(f"Request field {key!r} is not supported by sampling params for {model_path}") raise ValueError(f"Request field {key!r} is not supported by sampling params for {model_path}")
@@ -476,11 +501,13 @@ def explicit_request_updates(request: GenerationRequest) -> dict[str, Any]:
This is what makes ``ServeConfig.default_request`` work as an This is what makes ``ServeConfig.default_request`` work as an
operator-pinned baseline rather than a full override: a YAML with just operator-pinned baseline rather than a full override: a YAML with just
``sampling.seed: 42`` yields ``{"seed": 42}``, not the full sampling ``sampling.seed: 42`` yields ``{"seed": 42}``, not the full sampling
config with its 15 schema defaults. config. (Sampling fields default to ``None`` = "inherit the model
preset", so for directly-constructed requests only non-None leaves
are bound as explicit.)
Precondition: the request must carry ``_fastvideo_explicit_paths`` — Precondition: the request must carry ``_fastvideo_explicit_paths`` —
populated by :func:`fastvideo.api.parser.parse_config` or populated by :func:`fastvideo.api.parser.parse_config` or
:func:`fastvideo.api.compat.normalize_generation_request`. Calling on :func:`fastvideo.api.translation.normalize_generation_request`. Calling on
a raw ``GenerationRequest()`` asserts. a raw ``GenerationRequest()`` asserts.
""" """
assert hasattr(request, assert hasattr(request,
@@ -579,8 +606,6 @@ def _serialize_generation_request(request: GenerationRequest) -> dict[str, Any]:
return deepcopy(config_to_dict(request)) return deepcopy(config_to_dict(request))
_SCHEMA_DEFAULT_UPDATES = _extract_request_updates(config_to_dict(GenerationRequest()))
_KNOWN_CONTINUATION_KINDS: set[str] = set() _KNOWN_CONTINUATION_KINDS: set[str] = set()
+1 -1
View File
@@ -5,7 +5,7 @@ import argparse
import os import os
from typing import cast from typing import cast
from fastvideo.api.compat import generator_config_to_fastvideo_args from fastvideo.api.translation import generator_config_to_fastvideo_args
from fastvideo.entrypoints.cli.cli_types import CLISubcommand from fastvideo.entrypoints.cli.cli_types import CLISubcommand
from fastvideo.entrypoints.cli.inference_config import build_serve_config from fastvideo.entrypoints.cli.inference_config import build_serve_config
from fastvideo.logger import init_logger from fastvideo.logger import init_logger
+8 -7
View File
@@ -19,7 +19,7 @@ from fastapi import (
) )
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from fastvideo.api.compat import explicit_request_updates from fastvideo.api.translation import explicit_request_updates
from fastvideo.api.schema import GenerationRequest from fastvideo.api.schema import GenerationRequest
from fastvideo.entrypoints.openai.state import ( from fastvideo.entrypoints.openai.state import (
get_default_request, get_default_request,
@@ -60,12 +60,13 @@ def _build_generation_kwargs(
defaults on the dataclass are *not* treated as defaults here. defaults on the dataclass are *not* treated as defaults here.
3. Hardcoded fallback (e.g. ``fps=24`` when neither side set it). 3. Hardcoded fallback (e.g. ``fps=24`` when neither side set it).
Why gate on ``model_fields_set`` / explicit paths? Both the request Why gate on ``model_fields_set`` / explicit paths? The request
Pydantic model and the ``GenerationRequest`` dataclass carry schema Pydantic model carries schema defaults, and the ``GenerationRequest``
defaults (e.g. ``seed=1024``, ``num_frames=125``). Without the gate dataclass carries non-None defaults outside ``sampling`` (e.g.
those would masquerade as intent and shadow the other side — the ``output_path``; sampling fields default to None = "inherit the
gate preserves "operator pinned it" vs. "dataclass happened to have model preset"). Without the gate those would masquerade as intent
that default." and shadow the other side — the gate preserves "operator pinned it"
vs. "dataclass happened to have that default."
""" """
kwargs: dict[str, Any] = {} kwargs: dict[str, Any] = {}
if default_request is not None: if default_request is not None:
+11 -4
View File
@@ -48,10 +48,14 @@ class MockGenerator:
def generate(self, request: GenerationRequest) -> dict[str, Any]: def generate(self, request: GenerationRequest) -> dict[str, Any]:
if self.sleep_ms: if self.sleep_ms:
time.sleep(self.sleep_ms / 1000.0) time.sleep(self.sleep_ms / 1000.0)
width = max(16, request.sampling.width) # Requests carry None for unset sampling fields (normally resolved
height = max(16, request.sampling.height) # against the model preset); the mock has no model, so fall back
num_frames = max(1, request.sampling.num_frames) # to fixed values here.
frames = [_gradient_frame(height, width, idx, seed=request.sampling.seed) for idx in range(num_frames)] width = max(16, request.sampling.width or 16)
height = max(16, request.sampling.height or 16)
num_frames = max(1, request.sampling.num_frames or 1)
seed = request.sampling.seed if request.sampling.seed is not None else 1024
frames = [_gradient_frame(height, width, idx, seed=seed) for idx in range(num_frames)]
state = ContinuationState( state = ContinuationState(
kind="ltx2.v1", kind="ltx2.v1",
payload={ payload={
@@ -90,6 +94,9 @@ def build_mock_app(*, sleep_ms: float = 0.0):
width=256, width=256,
fps=24, fps=24,
num_inference_steps=1, num_inference_steps=1,
# The mock renders frames from the seed directly (no model, no
# preset resolution), so it must be concrete here.
seed=1024,
) )
return build_app(serve_config, MockGenerator(sleep_ms=sleep_ms)) return build_app(serve_config, MockGenerator(sleep_ms=sleep_ms))
+20 -1
View File
@@ -95,6 +95,19 @@ class ServerState:
session_store: SessionStore session_store: SessionStore
def _validate_default_sampling_pins(serve_config: ServeConfig) -> None:
"""The WS protocol consumes these before any model-preset resolution
happens (segment frames, MP4 encoder dims), so the operator must pin
them; SamplingConfig defaults them to None = "inherit preset", which
has no meaning at this layer."""
required = ("num_frames", "height", "width", "fps", "num_inference_steps")
missing = [name for name in required if getattr(serve_config.default_request.sampling, name) is None]
if missing:
raise ValueError("Streaming server requires explicit default_request.sampling values for: " +
", ".join(missing) + ". Set them in the serve config YAML "
"(e.g. `default_request: {sampling: {num_frames: 121, ...}}`).")
def build_app( def build_app(
serve_config: ServeConfig, serve_config: ServeConfig,
generator: _GeneratorProto | None = None, generator: _GeneratorProto | None = None,
@@ -118,6 +131,8 @@ def build_app(
if (generator is None) == (pool is None): if (generator is None) == (pool is None):
raise ValueError("build_app requires exactly one of `generator` or `pool`") raise ValueError("build_app requires exactly one of `generator` or `pool`")
_validate_default_sampling_pins(serve_config)
store = session_store or InMemorySessionStore() store = session_store or InMemorySessionStore()
if pool is None: if pool is None:
assert generator is not None assert generator is not None
@@ -183,6 +198,8 @@ def run_server(serve_config: ServeConfig, *, generator: _GeneratorProto | None =
if serve_config.streaming is None: if serve_config.streaming is None:
raise ValueError("ServeConfig.streaming must be set to launch the streaming server; " raise ValueError("ServeConfig.streaming must be set to launch the streaming server; "
"got None. Add a `streaming:` block to your serve config.") "got None. Add a `streaming:` block to your serve config.")
# Fail on config mistakes before paying the multi-minute model boot.
_validate_default_sampling_pins(serve_config)
import uvicorn import uvicorn
@@ -490,7 +507,9 @@ def _build_generation_request(
} }
request = GenerationRequest( request = GenerationRequest(
prompt=message.prompt, prompt=message.prompt,
negative_prompt=message.negative_prompt or base.negative_prompt, # None inherits (base, then model preset); an explicit "" from
# the client clears the negative prompt and must survive.
negative_prompt=(message.negative_prompt if message.negative_prompt is not None else base.negative_prompt),
inputs=InputConfig(image_path=session.metadata.get("session_init_image"), ), inputs=InputConfig(image_path=session.metadata.get("session_init_image"), ),
sampling=SamplingConfig(**sampling_kwargs), sampling=SamplingConfig(**sampling_kwargs),
output=OutputConfig(save_video=False, return_frames=True, return_state=True), output=OutputConfig(save_video=False, return_frames=True, return_state=True),
+7 -2
View File
@@ -26,7 +26,7 @@ import torch
import torchvision import torchvision
from einops import rearrange from einops import rearrange
from fastvideo.api.compat import ( from fastvideo.api.translation import (
expand_request_prompt_batch, expand_request_prompt_batch,
generator_config_to_fastvideo_args, generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config, legacy_from_pretrained_to_config,
@@ -314,7 +314,12 @@ class VideoGenerator:
import asyncio import asyncio
normalized = normalize_generation_request(request) normalized = normalize_generation_request(request)
total_steps = max(1, normalized.sampling.num_inference_steps) steps = normalized.sampling.num_inference_steps
if steps is None:
# Unset = inherit the model preset; resolve it here so the
# advertised total matches what the run will actually do.
steps = SamplingParam.from_pretrained(self.fastvideo_args.model_path).num_inference_steps
total_steps = max(1, steps or 1)
yield VideoProgressEvent(step=0, total_steps=total_steps, stage="denoise") yield VideoProgressEvent(step=0, total_steps=total_steps, stage="denoise")
if log_queue: if log_queue:
@@ -23,7 +23,7 @@ from collections.abc import Mapping
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
from fastvideo.api.compat import register_continuation_kind from fastvideo.api.translation import register_continuation_kind
from fastvideo.api.schema import ContinuationState from fastvideo.api.schema import ContinuationState
if TYPE_CHECKING: if TYPE_CHECKING:
+1 -1
View File
@@ -5,7 +5,7 @@ from types import SimpleNamespace
import pytest import pytest
from fastvideo.api.compat import request_to_sampling_param from fastvideo.api.translation import request_to_sampling_param
from fastvideo.entrypoints.cli import main as cli_main from fastvideo.entrypoints.cli import main as cli_main
from fastvideo.entrypoints.cli.generate import GenerateSubcommand from fastvideo.entrypoints.cli.generate import GenerateSubcommand
from fastvideo.entrypoints.cli.inference_config import ( from fastvideo.entrypoints.cli.inference_config import (
@@ -1,11 +1,11 @@
# SPDX-License-Identifier: Apache-2.0 # SPDX-License-Identifier: Apache-2.0
"""Tests for ``fastvideo.api.compat`` translation helpers covering the """Tests for ``fastvideo.api.translation`` translation helpers covering the
typed CompileConfig + PipelineSelection.vae_tiling surfaces promoted in typed CompileConfig + PipelineSelection.vae_tiling surfaces promoted in
PR 6. PR 6.
""" """
from __future__ import annotations from __future__ import annotations
from fastvideo.api.compat import ( from fastvideo.api.translation import (
generator_config_to_fastvideo_args, generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config, legacy_from_pretrained_to_config,
) )
+2 -1
View File
@@ -37,5 +37,6 @@ def test_serve_config_includes_server_and_default_request_defaults() -> None:
"port": 8000, "port": 8000,
"output_dir": "outputs/", "output_dir": "outputs/",
} }
assert dumped["default_request"]["sampling"]["fps"] == 24 # Sampling fields default to None = "inherit the model preset".
assert dumped["default_request"]["sampling"]["fps"] is None
assert dumped["default_request"]["runtime"]["enable_teacache"] is False assert dumped["default_request"]["runtime"]["enable_teacache"] is False
@@ -21,7 +21,7 @@ import torch
# Importing compat first, then the LTX-2 module, exercises the # Importing compat first, then the LTX-2 module, exercises the
# self-registration side effect on import (important for the API # self-registration side effect on import (important for the API
# test suite where the pipeline package isn't otherwise imported). # test suite where the pipeline package isn't otherwise imported).
from fastvideo.api import compat as api_compat # noqa: F401 from fastvideo.api import translation as api_compat # noqa: F401
from fastvideo.api.schema import ( from fastvideo.api.schema import (
ContinuationState, ContinuationState,
GenerationRequest, GenerationRequest,
@@ -217,7 +217,7 @@ class TestCompatLayerWireUp:
# PR 7 removes the NotImplementedError for request.state; build a # PR 7 removes the NotImplementedError for request.state; build a
# minimal GenerationRequest carrying an LTX-2 state and make sure # minimal GenerationRequest carrying an LTX-2 state and make sure
# the public boundary accepts it. # the public boundary accepts it.
from fastvideo.api.compat import ( from fastvideo.api.translation import (
normalize_generation_request, normalize_generation_request,
_validate_continuation_state, _validate_continuation_state,
) )
@@ -230,13 +230,13 @@ class TestCompatLayerWireUp:
_validate_continuation_state(normalized.state) _validate_continuation_state(normalized.state)
def test_unknown_kind_rejected_at_boundary(self): def test_unknown_kind_rejected_at_boundary(self):
from fastvideo.api.compat import _validate_continuation_state from fastvideo.api.translation import _validate_continuation_state
with pytest.raises(ValueError, match="Unknown ContinuationState kind"): with pytest.raises(ValueError, match="Unknown ContinuationState kind"):
_validate_continuation_state( _validate_continuation_state(
ContinuationState(kind="mystery.v1", payload={})) ContinuationState(kind="mystery.v1", payload={}))
def test_empty_kind_rejected_at_boundary(self): def test_empty_kind_rejected_at_boundary(self):
from fastvideo.api.compat import _validate_continuation_state from fastvideo.api.translation import _validate_continuation_state
with pytest.raises(ValueError, match="non-empty string"): with pytest.raises(ValueError, match="non-empty string"):
_validate_continuation_state( _validate_continuation_state(
ContinuationState(kind="", payload={})) ContinuationState(kind="", payload={}))
@@ -19,7 +19,7 @@ from copy import deepcopy
import pytest import pytest
from fastvideo.api.compat import ( from fastvideo.api.translation import (
generator_config_to_fastvideo_args, generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config, legacy_from_pretrained_to_config,
) )
@@ -208,7 +208,7 @@ class TestRefineFlattenCoversAllTypedFields:
def test_all_fields_reemitted(self, monkeypatch) -> None: def test_all_fields_reemitted(self, monkeypatch) -> None:
from fastvideo import fastvideo_args as fva from fastvideo import fastvideo_args as fva
from fastvideo.api.compat import ( from fastvideo.api.translation import (
generator_config_to_fastvideo_args, generator_config_to_fastvideo_args,
) )
from fastvideo.api.schema import GeneratorConfig, PipelineSelection from fastvideo.api.schema import GeneratorConfig, PipelineSelection
+13 -11
View File
@@ -169,19 +169,21 @@ def test_load_run_config_supports_yaml_roundtrip(tmp_path) -> None:
"stage1_video": None, "stage1_video": None,
}, },
"sampling": { "sampling": {
"num_videos_per_prompt": 1, # None = inherit the model preset at generate time; only
"seed": 1024, # the YAML-set num_frames carries a value.
"num_videos_per_prompt": None,
"seed": None,
"num_frames": 16, "num_frames": 16,
"height": 720, "height": None,
"width": 1280, "width": None,
"height_sr": 1072, "height_sr": None,
"width_sr": 1920, "width_sr": None,
"fps": 24, "fps": None,
"num_inference_steps": 50, "num_inference_steps": None,
"num_inference_steps_sr": 50, "num_inference_steps_sr": None,
"guidance_scale": 1.0, "guidance_scale": None,
"guidance_scale_2": None, "guidance_scale_2": None,
"guidance_rescale": 0.0, "guidance_rescale": None,
"true_cfg_scale": None, "true_cfg_scale": None,
"boundary_ratio": None, "boundary_ratio": None,
"sigmas": None, "sigmas": None,
@@ -0,0 +1,106 @@
# SPDX-License-Identifier: Apache-2.0
"""Regression tests for None-sentinel SamplingConfig semantics.
A directly-constructed ``GenerationRequest`` must inherit the model
preset (``SamplingParam.from_pretrained``) for every sampling field the
caller did not set. Before the None-sentinel change, direct construction
marked every schema default as explicit and stomped the preset —
e.g. FastWan's 3 distilled steps silently became 50.
"""
from __future__ import annotations
import pytest
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api.schema import GenerationRequest, SamplingConfig
from fastvideo.api.translation import (
normalize_generation_request,
request_to_sampling_param,
)
def _resolve(request: GenerationRequest) -> SamplingParam:
# Mirror the production path: generate() normalizes (binding explicit
# paths) before translating to a SamplingParam.
return request_to_sampling_param(normalize_generation_request(request), model_path=MODEL)
# Distilled preset whose values differ from every old schema default
# (3 steps vs 50, gs 3.0 vs 1.0, 448x832 vs 720x1280, 61 frames vs 125,
# fps 16 vs 24) — if inheritance breaks, every assertion below fails.
MODEL = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers"
@pytest.fixture()
def preset() -> SamplingParam:
return SamplingParam.from_pretrained(MODEL)
class TestDirectConstructionInheritsPreset:
def test_bare_request_preserves_preset(self, preset: SamplingParam) -> None:
resolved = _resolve(GenerationRequest(prompt="a fox"))
assert resolved.num_inference_steps == preset.num_inference_steps == 3
assert resolved.guidance_scale == preset.guidance_scale == 3.0
assert (resolved.height, resolved.width) == (preset.height, preset.width) == (448, 832)
assert resolved.num_frames == preset.num_frames
assert resolved.fps == preset.fps == 16
assert resolved.negative_prompt == preset.negative_prompt
assert resolved.negative_prompt # preset prompt is non-empty
def test_partial_sampling_overrides_only_set_fields(self, preset: SamplingParam) -> None:
resolved = _resolve(GenerationRequest(prompt="a fox", sampling=SamplingConfig(num_frames=81)))
assert resolved.num_frames == 81
assert resolved.num_inference_steps == preset.num_inference_steps
assert resolved.guidance_scale == preset.guidance_scale
assert (resolved.height, resolved.width) == (preset.height, preset.width)
def test_explicit_value_equal_to_old_schema_default_wins(self) -> None:
# gs=1.0 was the old schema default; it must still be honored
# when the caller sets it deliberately against a gs=3.0 preset.
resolved = _resolve(GenerationRequest(prompt="a fox", sampling=SamplingConfig(guidance_scale=1.0)))
assert resolved.guidance_scale == 1.0
def test_negative_prompt_none_inherits_empty_string_clears(self, preset: SamplingParam) -> None:
inherited = _resolve(GenerationRequest(prompt="x"))
assert inherited.negative_prompt == preset.negative_prompt
cleared = _resolve(GenerationRequest(prompt="x", negative_prompt=""))
assert cleared.negative_prompt == ""
def test_parsed_dict_request_also_inherits(self) -> None:
from fastvideo.api.parser import parse_config
resolved = _resolve(GenerationRequest(prompt="a fox"))
parsed_resolved = _resolve(parse_config(GenerationRequest, {"prompt": "a fox"}))
assert parsed_resolved.num_inference_steps == resolved.num_inference_steps
assert parsed_resolved.guidance_scale == resolved.guidance_scale
def test_explicitly_set_unsupported_field_still_raises(self) -> None:
# true_cfg_scale exists on SamplingConfig but not on Wan's
# SamplingParam; setting it explicitly must fail loudly (the old
# schema-default tolerance is gone).
with pytest.raises(ValueError, match="true_cfg_scale"):
_resolve(GenerationRequest(prompt="x", sampling=SamplingConfig(true_cfg_scale=2.0)))
class TestParsedNullsInherit:
"""YAML/JSON `null` must behave exactly like an unset field — it is
parsed into None but never bound as an explicit path."""
def test_parsed_sampling_null_inherits_preset(self, preset: SamplingParam) -> None:
from fastvideo.api.parser import parse_config
parsed = parse_config(GenerationRequest, {"prompt": "x", "sampling": {"num_frames": None, "height": None}})
resolved = _resolve(parsed)
assert resolved.num_frames == preset.num_frames
assert resolved.height == preset.height
def test_parsed_negative_prompt_null_inherits_preset(self, preset: SamplingParam) -> None:
from fastvideo.api.parser import parse_config
parsed = parse_config(GenerationRequest, {"prompt": "x", "negative_prompt": None})
assert _resolve(parsed).negative_prompt == preset.negative_prompt
def test_direct_none_valued_extension_is_dropped_not_resurrected(self) -> None:
# A None-valued extension entry is pruned from the explicit paths;
# it must not resurface via the live-attribute read in
# explicit_request_updates and raise as an unsupported field.
request = GenerationRequest(prompt="x", extensions={"bogus_key": None})
resolved = _resolve(request)
assert resolved.num_inference_steps == 3 # preset intact, no ValueError
+1 -1
View File
@@ -15,7 +15,7 @@ from __future__ import annotations
import pytest import pytest
from fastvideo.api.compat import generator_config_to_fastvideo_args from fastvideo.api.translation import generator_config_to_fastvideo_args
from fastvideo.api.schema import ( from fastvideo.api.schema import (
EngineConfig, EngineConfig,
GeneratorConfig, GeneratorConfig,
@@ -34,7 +34,7 @@ from fastvideo.api import (
GeneratorConfig, GeneratorConfig,
GenerationRequest, GenerationRequest,
) )
from fastvideo.api.compat import ( from fastvideo.api.translation import (
legacy_from_pretrained_to_config, legacy_from_pretrained_to_config,
legacy_generate_call_to_request, legacy_generate_call_to_request,
normalize_generation_request, normalize_generation_request,
@@ -205,7 +205,7 @@ class TestDreamverseNoPrivateImports:
[ [
"fastvideo", "fastvideo",
"fastvideo.api", "fastvideo.api",
"fastvideo.api.compat", # public in that it's re-exported "fastvideo.api.translation", # public in that it's re-exported
], ],
) )
def test_public_imports_resolve(self, import_path): def test_public_imports_resolve(self, import_path):
@@ -269,7 +269,7 @@ class TestNoInternalImports:
"fastvideo.pipelines.", "fastvideo.pipelines.",
"fastvideo.configs.", "fastvideo.configs.",
"fastvideo.fastvideo_args", "fastvideo.fastvideo_args",
"fastvideo.api.compat", "fastvideo.api.translation",
"fastvideo.api.parser", "fastvideo.api.parser",
"fastvideo.api.overrides", "fastvideo.api.overrides",
"fastvideo.api.errors", "fastvideo.api.errors",
@@ -199,7 +199,7 @@ class TestHealthCheckRequest:
assert req.output.return_frames is False assert req.output.return_frames is False
def test_round_trips_through_normalization(self): def test_round_trips_through_normalization(self):
from fastvideo.api.compat import normalize_generation_request from fastvideo.api.translation import normalize_generation_request
from fastvideo.entrypoints.video_generator import VideoGenerator from fastvideo.entrypoints.video_generator import VideoGenerator
req = VideoGenerator.default_health_check_request() req = VideoGenerator.default_health_check_request()
@@ -224,6 +224,28 @@ class TestMockServer:
app = build_mock_app() app = build_mock_app()
assert isinstance(app, FastAPI) assert isinstance(app, FastAPI)
def test_build_app_rejects_unpinned_default_sampling(self):
import pytest
from fastvideo.entrypoints.streaming.mock_server import MockGenerator
from fastvideo.entrypoints.streaming.server import build_app
from fastvideo.api.schema import (
GeneratorConfig,
SamplingConfig,
ServeConfig,
StreamingConfig,
)
serve_config = ServeConfig(
generator=GeneratorConfig(model_path="/models/mock"),
streaming=StreamingConfig(),
)
# fps (among others) left None = "inherit preset" — meaningless to
# the WS encoder, so build_app must reject before serving.
serve_config.default_request.sampling = SamplingConfig(num_frames=24, height=256, width=256)
with pytest.raises(ValueError, match="fps"):
build_app(serve_config, MockGenerator())
def test_mock_generator_produces_frames(self): def test_mock_generator_produces_frames(self):
from fastvideo.api.schema import GenerationRequest, SamplingConfig from fastvideo.api.schema import GenerationRequest, SamplingConfig
from fastvideo.entrypoints.streaming.mock_server import MockGenerator from fastvideo.entrypoints.streaming.mock_server import MockGenerator
@@ -69,7 +69,7 @@ def _patch_fastvideo_args_from_kwargs(monkeypatch):
) )
monkeypatch.setattr( monkeypatch.setattr(
"fastvideo.api.compat.FastVideoArgs.from_kwargs", "fastvideo.api.translation.FastVideoArgs.from_kwargs",
classmethod(fake_from_kwargs), classmethod(fake_from_kwargs),
) )
return captured return captured
@@ -261,7 +261,7 @@ def test_generate_uses_typed_request_path(monkeypatch):
assert result.video_path == "outputs/test.mp4" assert result.video_path == "outputs/test.mp4"
def test_generate_preserves_schema_defaults_for_dataclass_request(monkeypatch): def test_generate_dataclass_request_inherits_preset_for_unset_fields(monkeypatch):
generator = _new_runtime_video_generator() generator = _new_runtime_video_generator()
captured = {} captured = {}
@@ -288,11 +288,18 @@ def test_generate_preserves_schema_defaults_for_dataclass_request(monkeypatch):
) )
) )
assert captured["sampling_param"].negative_prompt is None # Explicitly-set fields win; None (negative_prompt) inherits the
# model preset instead of stomping it with the schema default.
assert captured["sampling_param"].negative_prompt == "model default"
assert captured["sampling_param"].num_frames == 125 assert captured["sampling_param"].num_frames == 125
assert captured["sampling_param"].height == 720 assert captured["sampling_param"].height == 720
assert captured["sampling_param"].width == 1280 assert captured["sampling_param"].width == 1280
generator.generate(GenerationRequest(prompt="hello world", negative_prompt=""))
assert captured["sampling_param"].negative_prompt == ""
assert captured["sampling_param"].num_frames == 61
assert captured["sampling_param"].height == 448
def test_generate_mapping_request_preserves_model_defaults_for_omitted_fields( def test_generate_mapping_request_preserves_model_defaults_for_omitted_fields(
monkeypatch, monkeypatch,
@@ -13,7 +13,8 @@ The base DiT uses Wan-AI/Wan2.2-TI2V-5B's VAE and google/t5gemma-9b-9b-ul2's
encoder at inference time; neither is bundled upstream. encoder at inference time; neither is bundled upstream.
This converter takes the raw MagiHuman base DiT and emits a Diffusers-style This converter takes the raw MagiHuman base DiT and emits a Diffusers-style
directory so `VideoGenerator.from_pretrained(...)` can load it standalone: directory so `VideoGenerator.from_config(GeneratorConfig(model_path=...))` can
load it standalone:
<output>/ <output>/
model_index.json model_index.json
@@ -683,21 +683,23 @@ def main():
print() print()
print(" 1. Test basic generation:") print(" 1. Test basic generation:")
print(" from fastvideo import VideoGenerator") print(" from fastvideo import VideoGenerator")
print(f" generator = VideoGenerator.from_pretrained('{output_dir}')") print(" from fastvideo.api import GeneratorConfig, GenerationRequest, SamplingConfig")
print(" video = generator.generate_video(") print(f" generator = VideoGenerator.from_config(GeneratorConfig(model_path='{output_dir}'))")
print(" result = generator.generate(GenerationRequest(")
print(" prompt='A cat playing piano',") print(" prompt='A cat playing piano',")
print(" num_inference_steps=50") print(" sampling=SamplingConfig(num_inference_steps=50),")
print(" )") print(" ))")
print() print()
if (output_dir / "lora" / "distilled").exists(): if (output_dir / "lora" / "distilled").exists():
print(" 2. Test distilled generation (16 steps with LoRA):") print(" 2. Test distilled generation (16 steps with LoRA):")
print(f" generator = VideoGenerator.from_pretrained('{output_dir}',") print(" from fastvideo.api import ComponentConfig, PipelineSelection")
print(f" lora_path='{output_dir}/lora/distilled',") print(f" generator = VideoGenerator.from_config(GeneratorConfig(model_path='{output_dir}',")
print(" lora_nickname='distilled')") print(f" pipeline=PipelineSelection(")
print(" video = generator.generate_video(") print(f" components=ComponentConfig(lora_path='{output_dir}/lora/distilled'),")
print(" experimental={'lora_nickname': 'distilled'})))")
print(" result = generator.generate(GenerationRequest(")
print(" prompt='A cat playing piano',") print(" prompt='A cat playing piano',")
print(" num_inference_steps=16,") print(" sampling=SamplingConfig(num_inference_steps=16, guidance_scale=1.0)))")
print(" guidance_scale=1.0)")
print() print()
print() print()
@@ -18,7 +18,8 @@ Example (one-shot per variant):
--public --public
After upload, the local directory can be deleted — the HF repo is the After upload, the local directory can be deleted — the HF repo is the
source of truth. `VideoGenerator.from_pretrained("FastVideo/...")` pulls source of truth.
`VideoGenerator.from_config(GeneratorConfig(model_path="FastVideo/..."))` pulls
shards on demand. shards on demand.
""" """
from __future__ import annotations from __future__ import annotations
@@ -97,42 +97,53 @@ def generate_with_model(
flow_shift: Optional[float] = None, flow_shift: Optional[float] = None,
embedded_guidance_scale: Optional[float] = None, embedded_guidance_scale: Optional[float] = None,
) -> str: ) -> str:
"""Produce a video with VideoGenerator.from_pretrained; returns video path.""" """Produce a video with VideoGenerator.from_config; returns video path."""
try: try:
from fastvideo import VideoGenerator # lazy import from fastvideo import VideoGenerator # lazy import
from fastvideo.api import (
ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, PipelineSelection, SamplingConfig,
)
except Exception as exc: except Exception as exc:
raise RuntimeError(f"Failed to import fastvideo.VideoGenerator: {exc}") from exc raise RuntimeError(f"Failed to import fastvideo.VideoGenerator: {exc}") from exc
init_kwargs: Dict[str, Any] = { pipeline_kwargs: Dict[str, Any] = {}
"num_gpus": 1,
"dit_cpu_offload": True,
"vae_cpu_offload": True,
"text_encoder_cpu_offload": True,
"pin_cpu_memory": True,
}
if lora_path: if lora_path:
init_kwargs["lora_path"] = lora_path pipeline_kwargs["components"] = ComponentConfig(lora_path=lora_path)
init_kwargs["lora_nickname"] = "extracted" pipeline_kwargs["experimental"] = {"lora_nickname": "extracted"}
generator = VideoGenerator.from_pretrained(model_path, **init_kwargs) generator = VideoGenerator.from_config(GeneratorConfig(
model_path=model_path,
engine=EngineConfig(
num_gpus=1,
offload=OffloadConfig(dit=True, vae=True, text_encoder=True, pin_cpu_memory=True),
),
pipeline=PipelineSelection(**pipeline_kwargs),
))
gen_kwargs = { extensions: Dict[str, Any] = {}
"height": height,
"width": width,
"num_frames": num_frames,
"num_inference_steps": num_inference_steps,
"guidance_scale": guidance_scale,
"seed": seed,
"output_path": output_dir,
"output_video_name": output_name,
"save_video": True,
}
if flow_shift is not None: if flow_shift is not None:
gen_kwargs["flow_shift"] = flow_shift extensions["flow_shift"] = flow_shift
if embedded_guidance_scale is not None: if embedded_guidance_scale is not None:
gen_kwargs["embedded_guidance_scale"] = embedded_guidance_scale extensions["embedded_guidance_scale"] = embedded_guidance_scale
result = generator.generate_video(prompt, **gen_kwargs) result = generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(
height=height,
width=width,
num_frames=num_frames,
num_inference_steps=num_inference_steps,
guidance_scale=guidance_scale,
seed=seed,
),
output=OutputConfig(
output_path=output_dir,
output_video_name=output_name,
save_video=True,
),
extensions=extensions,
))
# best-effort cleanup of internal executors # best-effort cleanup of internal executors
try: try:
@@ -145,9 +156,9 @@ def generate_with_model(
expected = Path(output_dir) / f"{output_name}.mp4" expected = Path(output_dir) / f"{output_name}.mp4"
if expected.exists(): if expected.exists():
return str(expected) return str(expected)
# fallback: check result dict # fallback: check result video path
if isinstance(result, dict) and "video_path" in result: if getattr(result, "video_path", None):
return str(result["video_path"]) return str(result.video_path)
raise FileNotFoundError(f"Video not found at expected path: {expected}") raise FileNotFoundError(f"Video not found at expected path: {expected}")