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
`PipelineSelection.preset`).
* `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
`VideoGenerator` + `fastvideo.api`.
* 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,
even if the Pydantic model has a schema default for them.
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
defaults. Every other field inherits the schema default rather than
being pinned.
defaults (an explicit `null` counts as unset). Every other sampling
field stays `None` — "inherit the model preset" — and other sections
keep their schema defaults without being pinned.
3. **Hardcoded fallback** — e.g. `fps = 24`.
The gate matters: both surfaces carry schema defaults. Without
`model_fields_set` / explicit-path tracking, schema defaults would
masquerade as intent and silently shadow the other side.
The gate matters: the Pydantic surface carries schema defaults and the
dataclass surface carries non-None defaults outside `sampling`. Without
`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)
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
from fastvideo import VideoGenerator
from fastvideo.api import EngineConfig, GenerationRequest, GeneratorConfig
def main():
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1,
generator = VideoGenerator.from_config(
GeneratorConfig(
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 "
"wide with interest. The playful yet serene atmosphere is complemented by soft "
"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__":
main()
+23 -18
View File
@@ -1,6 +1,7 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig, OutputConfig,
)
OUTPUT_PATH = "video_samples"
def main():
@@ -8,29 +9,32 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
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"
# 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
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 "
"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(prompt, sampling_param=sampling_param, output_path="wan_t2v_videos/")
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
# model!
@@ -40,7 +44,8 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"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__":
+27 -19
View File
@@ -1,24 +1,30 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
)
def main():
# Point this to your local diffusers model dir (or replace with a HF model ID).
model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_path,
num_gpus=1,
use_fsdp_inference=False, # set True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_path,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set True if GPU is out of memory
offload=OffloadConfig(
dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
)
sampling_param = SamplingParam.from_pretrained(model_path)
# image2world example from official repo
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."
)
generator.generate_video(
prompt,
sampling_param=sampling_param,
image_path=str(image_path),
num_cond_frames=1,
output_path="outputs_video/cosmos2_5_i2w.mp4",
save_video=True,
generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=str(image_path)),
output=OutputConfig(
output_path="outputs_video/cosmos2_5_i2w.mp4",
save_video=True,
),
extensions={"num_cond_frames": 1},
)
)
generator.shutdown()
@@ -47,4 +56,3 @@ def main():
if __name__ == "__main__":
main()
+25 -20
View File
@@ -1,24 +1,29 @@
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig, OutputConfig,
)
def main():
# Point this to your local diffusers model dir (or replace with a HF model ID).
model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_path,
num_gpus=1,
use_fsdp_inference=False, # set True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_path,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set True if GPU is out of memory
offload=OffloadConfig(
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 = (
"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. "
@@ -34,11 +39,14 @@ def main():
"underscoring the ongoing nature of the welding operation."
)
generator.generate_video(
prompt,
sampling_param=sampling_param,
output_path="outputs_video/cosmos2_5_t2w.mp4",
save_video=True,
generator.generate(
GenerationRequest(
prompt=prompt,
output=OutputConfig(
output_path="outputs_video/cosmos2_5_t2w.mp4",
save_video=True,
),
)
)
generator.shutdown()
@@ -46,6 +54,3 @@ def main():
if __name__ == "__main__":
main()
+28 -21
View File
@@ -1,23 +1,29 @@
# SPDX-License-Identifier: Apache-2.0
from fastvideo import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig,
)
def main():
# Point this to your local diffusers model dir (or replace with a HF model ID).
model_path = "KyleShao/Cosmos-Predict2.5-2B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_path,
num_gpus=1,
use_fsdp_inference=False, # set True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
)
sampling_param = SamplingParam.from_pretrained(model_path)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_path,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set True if GPU is out of memory
offload=OffloadConfig(
dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
# video2world example from official repo
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."
)
generator.generate_video(
prompt,
sampling_param=sampling_param,
video_path=str(video_path),
num_cond_frames=1,
output_path="outputs_video/cosmos2_5_v2w.mp4",
save_video=True,
)
generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(video_path=str(video_path)),
output=OutputConfig(
output_path="outputs_video/cosmos2_5_v2w.mp4",
save_video=True,
),
extensions={"num_cond_frames": 1},
))
generator.shutdown()
if __name__ == "__main__":
main()
+32 -19
View File
@@ -2,7 +2,9 @@ import os
import time
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"
def main():
@@ -10,30 +12,36 @@ def main():
load_start_time = time.perf_counter()
model_name = "FastVideo/FastWan2.1-T2V-1.3B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_name,
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
# Adjust these offload parameters if you have < 32GB of VRAM
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
dit_cpu_offload=False,
vae_cpu_offload=False,
VSA_sparsity=0.8,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_name,
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
# Adjust these offload parameters if you have < 32GB of VRAM
offload=OffloadConfig(
text_encoder=True,
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_time = load_end_time - load_start_time
sampling_param = SamplingParam.from_pretrained(model_name)
sampling_param.num_frames = 81
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."
)
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()
gen_time = end_time - start_time
@@ -46,7 +54,12 @@ def main():
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.")
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()
gen_time2 = end_time - start_time
+42 -27
View File
@@ -1,6 +1,9 @@
import os
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")
@@ -16,16 +19,22 @@ def _env_float(name: str, default: float) -> float:
def main():
model_name = os.getenv("DREAMX_WORLD_MODEL_DIR", "FastVideo/DreamX-World-5B-Cam-Diffusers")
generator = VideoGenerator.from_pretrained(
model_name,
num_gpus=1,
use_fsdp_inference=False,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
override_pipeline_cls_name="DreamXWorldPipeline",
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_name,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
components=ComponentConfig(override_pipeline_cls_name="DreamXWorldPipeline"), ),
))
prompt = os.getenv(
"DREAMX_WORLD_PROMPT",
@@ -37,25 +46,31 @@ def main():
"https://huggingface.co/datasets/YiYiXu/testing-images/resolve/main/wan_i2v_input.JPG",
)
kwargs = {
"output_path": OUTPUT_PATH,
"save_video": os.getenv("DREAMX_WORLD_SAVE_VIDEO", "1") != "0",
"height": _env_int("DREAMX_WORLD_HEIGHT", 480),
"width": _env_int("DREAMX_WORLD_WIDTH", 832),
"num_frames": _env_int("DREAMX_WORLD_NUM_FRAMES", 161),
"num_inference_steps": _env_int("DREAMX_WORLD_STEPS", 30),
"guidance_scale": _env_float("DREAMX_WORLD_GUIDANCE", 5.0),
"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(",")
],
}
if image_path:
kwargs["image_path"] = image_path
request = GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=image_path or None),
sampling=SamplingConfig(
height=_env_int("DREAMX_WORLD_HEIGHT", 480),
width=_env_int("DREAMX_WORLD_WIDTH", 832),
num_frames=_env_int("DREAMX_WORLD_NUM_FRAMES", 161),
num_inference_steps=_env_int("DREAMX_WORLD_STEPS", 30),
guidance_scale=_env_float("DREAMX_WORLD_GUIDANCE", 5.0),
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=os.getenv("DREAMX_WORLD_SAVE_VIDEO", "1") != "0",
),
extensions={
"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:
generator.generate_video(prompt, **kwargs)
generator.generate(request)
finally:
generator.shutdown()
+40 -21
View File
@@ -26,6 +26,15 @@ import os
import torch
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
SamplingConfig,
)
from fastvideo.models.camera import create_camera_trajectory
# Model configuration (use GAMECRAFT_MODEL_PATH for local weights)
@@ -55,14 +64,20 @@ OUTPUT_PATH = "video_samples_gamecraft"
def main():
# Initialize generator
# FastVideo will automatically download weights from HuggingFace
generator = VideoGenerator.from_pretrained(
MODEL_PATH,
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=MODEL_PATH,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=True,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
)
# Video parameters
@@ -96,23 +111,27 @@ def main():
prompt = DEFAULT_I2V_PROMPT if is_i2v else DEFAULT_PROMPTS["temple"]
print(f"Mode: {'I2V' if is_i2v else 'T2V'}, prompt: {prompt[:60]}...")
gen_kw = dict(
request = GenerationRequest(
prompt=prompt,
negative_prompt="",
camera_states=camera_states,
height=height,
width=width,
num_frames=num_frames,
num_inference_steps=50,
guidance_scale=6.0,
seed=42,
fps=24,
output_path=OUTPUT_PATH,
save_video=True,
sampling=SamplingConfig(
height=height,
width=width,
num_frames=num_frames,
num_inference_steps=50,
guidance_scale=6.0,
seed=42,
fps=24,
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
extensions={"camera_states": camera_states},
)
if is_i2v:
gen_kw["image_path"] = image_path
generator.generate_video(**gen_kw)
request.inputs = InputConfig(image_path=image_path)
generator.generate(request)
if __name__ == "__main__":
+44 -26
View File
@@ -22,6 +22,10 @@ Requirements:
import argparse
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
def main():
@@ -74,33 +78,47 @@ def main():
parser.add_argument("--seed", type=int, default=42)
args = parser.parse_args()
generator = VideoGenerator.from_pretrained(
args.model_path,
num_gpus=1,
use_fsdp_inference=False,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model_path,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
video = generator.generate_video(
args.prompt,
negative_prompt=args.negative_prompt,
image_path=args.image_path,
trajectory_type=args.trajectory,
movement_distance=args.movement_distance,
camera_rotation=args.camera_rotation,
height=args.height,
width=args.width,
num_frames=args.num_frames,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
fps=24,
seed=args.seed,
output_path=args.output_path,
save_video=True,
)
video = generator.generate(
GenerationRequest(
prompt=args.prompt,
negative_prompt=args.negative_prompt,
inputs=InputConfig(
image_path=args.image_path,
),
sampling=SamplingConfig(
height=args.height,
width=args.width,
num_frames=args.num_frames,
num_inference_steps=args.num_inference_steps,
guidance_scale=args.guidance_scale,
fps=24,
seed=args.seed,
),
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()
+35 -14
View File
@@ -1,6 +1,13 @@
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
SamplingConfig,
)
import json
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_hy15"
def main():
@@ -8,17 +15,21 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
generator = VideoGenerator.from_config(GeneratorConfig(
model_path="hunyuanvideo-community/HunyuanVideo-1.5-Diffusers-480p_t2v",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder_cpu_offload=False,
)
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True,
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 = (
"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."
)
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 = (
"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, "
"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__":
main()
main()
+38 -14
View File
@@ -1,6 +1,12 @@
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
)
import json
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_hy15_1080p"
def main():
@@ -8,17 +14,23 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"weizhou03/HunyuanVideo-1.5-Diffusers-1080p-2SR", # 480p -> 720p -> 1080p
# or "weizhou03/HunyuanVideo-1.5-Diffusers-1080p" # 720p -> 1080p
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="weizhou03/HunyuanVideo-1.5-Diffusers-1080p-2SR", # 480p -> 720p -> 1080p
# or "weizhou03/HunyuanVideo-1.5-Diffusers-1080p" # 720p -> 1080p
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True,
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 = (
@@ -27,7 +39,13 @@ def main():
"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 = (
"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, "
"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__":
+37 -23
View File
@@ -1,4 +1,6 @@
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
# Default prompt from HY-WorldPlay run.sh
@@ -31,33 +33,45 @@ def main():
# Initialize generator
print("\nInitializing VideoGenerator for HYWorld...")
generator = VideoGenerator.from_pretrained(
"FastVideo/HY-WorldPlay-Bidirectional-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
image_encoder_cpu_offload=True,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/HY-WorldPlay-Bidirectional-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=True,
text_encoder=True,
pin_cpu_memory=True,
image_encoder=True,
),
),
))
# Generate video
# The pose string is automatically converted to camera matrices by the pipeline
print("\nGenerating video...")
generator.generate_video(
prompt=args.prompt,
image_path=args.image,
pose=args.pose, # Camera trajectory control
output_path=args.output_path,
save_video=True,
negative_prompt="",
num_frames=args.num_frames,
fps=24,
height=HEIGHT,
width=WIDTH,
seed=args.seed,
)
generator.generate(
GenerationRequest(
prompt=args.prompt,
negative_prompt="",
inputs=InputConfig(
image_path=args.image,
pose=args.pose, # Camera trajectory control
),
sampling=SamplingConfig(
num_frames=args.num_frames,
fps=24,
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}")
@@ -1,36 +1,43 @@
from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
InputConfig, OffloadConfig, OutputConfig,
SamplingConfig)
OUTPUT_PATH = "video_samples_kandinsky5_i2v"
IMAGE_PATH = "assets/girl.png"
def main():
generator = VideoGenerator.from_pretrained(
"kandinskylab/Kandinsky-5.0-I2V-Pro-distilled-5s-Diffusers",
# "kandinskylab/Kandinsky-5.0-I2V-Pro-sft-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-I2V-Lite-5s-Diffusers"
num_gpus=1,
use_fsdp_inference=False,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
# image_encoder_cpu_offload=False,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="kandinskylab/Kandinsky-5.0-I2V-Pro-distilled-5s-Diffusers",
# "kandinskylab/Kandinsky-5.0-I2V-Pro-sft-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-I2V-Lite-5s-Diffusers"
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
# image_encoder=False,
),
),
))
prompt = (
"A woman stands up and walks away"
)
_ = generator.generate_video(
prompt,
image_path=IMAGE_PATH,
output_path=OUTPUT_PATH,
save_video=True,
height=1024,
width=1024,
num_frames=121,
)
_ = generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=IMAGE_PATH),
sampling=SamplingConfig(height=1024, width=1024, num_frames=121),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
if __name__ == "__main__":
@@ -1,28 +1,41 @@
from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, SamplingConfig)
OUTPUT_PATH = "video_samples_kandinsky5_t2v"
def main():
generator = VideoGenerator.from_pretrained(
"kandinskylab/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers",
# "kandinskylab/Kandinsky-5.0-T2V-Pro-sft-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-T2V-Lite-distilled16steps-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-T2V-Pro-distilled-5s-Diffusers"
num_gpus=1,
use_fsdp_inference=False,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
# image_encoder_cpu_offload=False,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="kandinskylab/Kandinsky-5.0-T2V-Lite-sft-5s-Diffusers",
# "kandinskylab/Kandinsky-5.0-T2V-Pro-sft-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-T2V-Lite-distilled16steps-5s-Diffusers"
# "kandinskylab/Kandinsky-5.0-T2V-Pro-distilled-5s-Diffusers"
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
# image_encoder=False,
),
),
))
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 "
"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 = (
"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, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"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__":
main()
main()
@@ -1,24 +1,31 @@
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.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_lingbotworld"
def main():
# FastVideo will automatically use the optimal default arguments for the
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"FastVideo/LingBot-World-Base-Cam-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=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_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LingBot-World-Base-Cam-Diffusers",
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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
@@ -33,15 +40,23 @@ def main():
spatial_scale=8,
)
generator.generate_video(
prompt,
image_path=image_path,
output_path=OUTPUT_PATH,
save_video=True,
num_frames=num_frames,
height=480,
width=832,
c2ws_plucker_emb=c2ws_plucker_emb,
generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(
image_path=image_path,
c2ws_plucker_emb=c2ws_plucker_emb,
),
sampling=SamplingConfig(
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
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
PROMPT = (
@@ -45,41 +49,50 @@ SEED = 42
def basic_generation():
"""
Run basic LongCat I2V generation (50 steps at 480p).
This uses the full 50-step denoising process for highest quality.
"""
print("=" * 60)
print("LongCat I2V: Basic Generation (50 steps, 480p)")
print("=" * 60)
generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-I2V-Diffusers",
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-I2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(experimental={"enable_bsa": False}),
)
)
output_path = "outputs_video/longcat_i2v_basic"
generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
image_path=IMAGE_PATH,
output_path=output_path,
save_video=True,
height=480,
width=480, # Square
num_frames=93,
num_inference_steps=50,
fps=15,
guidance_scale=4.0,
seed=SEED,
generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
inputs=InputConfig(image_path=IMAGE_PATH),
sampling=SamplingConfig(
height=480,
width=480, # Square
num_frames=93,
num_inference_steps=50,
fps=15,
guidance_scale=4.0,
seed=SEED,
),
output=OutputConfig(output_path=output_path, save_video=True),
)
)
print(f"\nBasic generation complete! Video saved to: {output_path}")
generator.shutdown()
@@ -87,55 +100,70 @@ def basic_generation():
def distill_refine_generation():
"""
Run LongCat I2V with distill+refine pipeline (16 steps + refinement to 768p).
This uses the distilled LoRA for fast 480p generation (16 steps),
then refines to 768p using the refinement LoRA with BSA enabled.
"""
print("\n" + "=" * 60)
print("LongCat I2V: Distill + Refine Pipeline")
print("=" * 60)
# Stage 1: Distilled generation (16 steps at 480p)
print("\n[Stage 1] Distilled generation (16 steps, 480p)")
print("-" * 40)
generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-I2V-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=False,
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
lora_nickname="distilled",
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-I2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
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"
generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
image_path=IMAGE_PATH,
output_path=distill_output_path,
save_video=True,
height=480,
width=480, # Square
num_frames=93,
num_inference_steps=16,
fps=15,
guidance_scale=1.0,
seed=SEED,
generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
inputs=InputConfig(image_path=IMAGE_PATH),
sampling=SamplingConfig(
height=480,
width=480, # Square
num_frames=93,
num_inference_steps=16,
fps=15,
guidance_scale=1.0,
seed=SEED,
),
output=OutputConfig(output_path=distill_output_path, save_video=True),
)
)
print(f"Distilled generation complete! Video saved to: {distill_output_path}")
generator.shutdown()
# Stage 2: Refinement (480p -> 768p)
print("\n[Stage 2] Refinement (480p -> 768p with BSA)")
print("-" * 40)
# Find the actual saved video file from stage 1
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
if not video_files:
@@ -143,46 +171,63 @@ def distill_refine_generation():
# Use the most recently created video file
distill_video_path = max(video_files, key=os.path.getmtime)
print(f"Using stage 1 video: {distill_video_path}")
# 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
# For BSA [4, 4, 8]: latent must be divisible by 8
# 768x768: latent 48x48, 48%8=0 ✓
refine_generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-T2V-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=True,
bsa_sparsity=0.875,
bsa_chunk_q=[4, 4, 4],
bsa_chunk_k=[4, 4, 4],
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
lora_nickname="refinement",
refine_generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-T2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
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_generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output_path=refine_output_path,
save_video=True,
refine_from=distill_video_path,
t_thresh=0.5,
spatial_refine_only=False,
num_cond_frames=0,
height=720,
width=720,
num_inference_steps=50,
fps=30,
guidance_scale=1.0,
seed=SEED,
refine_generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
inputs=InputConfig(refine_from=distill_video_path),
sampling=SamplingConfig(
height=720,
width=720,
num_inference_steps=50,
fps=30,
guidance_scale=1.0,
seed=SEED,
),
output=OutputConfig(output_path=refine_output_path, save_video=True),
extensions={
"t_thresh": 0.5,
"spatial_refine_only": False,
"num_cond_frames": 0,
},
)
)
print(f"Refinement complete! Video saved to: {refine_output_path}")
refine_generator.shutdown()
@@ -192,13 +237,13 @@ def main():
print("\n" + "=" * 60)
print("LongCat Image-to-Video Example")
print("=" * 60 + "\n")
# Run basic generation
basic_generation()
# Run distill+refine pipeline
distill_refine_generation()
print("\n" + "=" * 60)
print("All generations complete!")
print("=" * 60)
+151 -95
View File
@@ -13,6 +13,10 @@ import glob
import os
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
PROMPT = (
@@ -38,40 +42,54 @@ SEED = 42
def basic_generation():
"""
Run basic LongCat T2V generation (50 steps at 480p).
This uses the full 50-step denoising process for highest quality.
"""
print("=" * 60)
print("LongCat T2V: Basic Generation (50 steps, 480p)")
print("=" * 60)
generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-T2V-Diffusers",
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-T2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
experimental={"enable_bsa": False},
),
)
)
output_path = "outputs_video/longcat_t2v_basic"
generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output_path=output_path,
save_video=True,
height=480,
width=832,
num_frames=93,
num_inference_steps=50,
fps=15,
guidance_scale=4.0,
seed=SEED,
generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output=OutputConfig(
output_path=output_path,
save_video=True,
),
sampling=SamplingConfig(
height=480,
width=832,
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}")
generator.shutdown()
@@ -79,54 +97,72 @@ def basic_generation():
def distill_refine_generation():
"""
Run LongCat T2V with distill+refine pipeline (16 steps + refinement to 720p).
This uses the distilled LoRA for fast 480p generation (16 steps),
then refines to 720p using the refinement LoRA with BSA enabled.
"""
print("\n" + "=" * 60)
print("LongCat T2V: Distill + Refine Pipeline")
print("=" * 60)
# Stage 1: Distilled generation (16 steps at 480p)
print("\n[Stage 1] Distilled generation (16 steps, 480p)")
print("-" * 40)
generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-T2V-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=False,
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
lora_nickname="distilled",
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-T2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
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"
generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output_path=distill_output_path,
save_video=True,
height=480,
width=832,
num_frames=93,
num_inference_steps=16,
fps=15,
guidance_scale=1.0,
seed=SEED,
generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output=OutputConfig(
output_path=distill_output_path,
save_video=True,
),
sampling=SamplingConfig(
height=480,
width=832,
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}")
generator.shutdown()
# Stage 2: Refinement (480p -> 720p)
print("\n[Stage 2] Refinement (480p -> 720p with BSA)")
print("-" * 40)
# Find the actual saved video file from stage 1
video_files = glob.glob(os.path.join(distill_output_path, "*.mp4"))
if not video_files:
@@ -134,43 +170,65 @@ def distill_refine_generation():
# Use the most recently created video file
distill_video_path = max(video_files, key=os.path.getmtime)
print(f"Using stage 1 video: {distill_video_path}")
# Create a new generator with refinement LoRA and BSA enabled
refine_generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-T2V-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=True,
bsa_sparsity=0.875,
bsa_chunk_q=[4, 4, 8],
bsa_chunk_k=[4, 4, 8],
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
lora_nickname="refinement",
refine_generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-T2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
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_generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output_path=refine_output_path,
save_video=True,
refine_from=distill_video_path,
t_thresh=0.5,
spatial_refine_only=False,
num_cond_frames=0,
height=720,
width=1280,
num_inference_steps=50,
fps=30,
guidance_scale=1.0,
seed=SEED,
refine_generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output=OutputConfig(
output_path=refine_output_path,
save_video=True,
),
inputs=InputConfig(
refine_from=distill_video_path,
),
sampling=SamplingConfig(
height=720,
width=1280,
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}")
refine_generator.shutdown()
@@ -180,13 +238,13 @@ def main():
print("\n" + "=" * 60)
print("LongCat Text-to-Video Example")
print("=" * 60 + "\n")
# Run basic generation
basic_generation()
# Run distill+refine pipeline
distill_refine_generation()
print("\n" + "=" * 60)
print("All generations complete!")
print("=" * 60)
@@ -194,5 +252,3 @@ def main():
if __name__ == "__main__":
main()
+142 -86
View File
@@ -19,6 +19,10 @@ import glob
import os
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
PROMPT = (
@@ -63,35 +67,49 @@ def basic_generation():
"Please provide a valid video path."
)
generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-VC-Diffusers",
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-VC-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(
experimental={"enable_bsa": False},
),
)
)
output_path = "outputs_video/longcat_vc_basic"
generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
video_path=VIDEO_PATH,
num_cond_frames=NUM_COND_FRAMES,
output_path=output_path,
save_video=True,
height=480,
width=832,
num_frames=93,
num_inference_steps=50,
fps=15,
guidance_scale=4.0,
seed=SEED,
generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
inputs=InputConfig(video_path=VIDEO_PATH),
sampling=SamplingConfig(
height=480,
width=832,
num_frames=93,
num_inference_steps=50,
fps=15,
guidance_scale=4.0,
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}")
generator.shutdown()
@@ -118,37 +136,55 @@ def distill_refine_generation():
print("\n[Stage 1] Distilled generation (16 steps, 480p)")
print("-" * 40)
generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-VC-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=False,
lora_path="FastVideo/LongCat-Video-T2V-Distilled-LoRA",
lora_nickname="distilled",
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-VC-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=False,
vae=True,
text_encoder=True,
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"
generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
video_path=VIDEO_PATH,
num_cond_frames=NUM_COND_FRAMES,
output_path=distill_output_path,
save_video=True,
height=480,
width=832,
num_frames=93,
num_inference_steps=16,
fps=15,
guidance_scale=1.0,
seed=SEED,
generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
inputs=InputConfig(video_path=VIDEO_PATH),
sampling=SamplingConfig(
height=480,
width=832,
num_frames=93,
num_inference_steps=16,
fps=15,
guidance_scale=1.0,
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}")
generator.shutdown()
@@ -166,41 +202,61 @@ def distill_refine_generation():
# 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
refine_generator = VideoGenerator.from_pretrained(
"FastVideo/LongCat-Video-T2V-Diffusers",
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=False,
enable_bsa=True,
bsa_sparsity=0.875,
bsa_chunk_q=[4, 4, 8],
bsa_chunk_k=[4, 4, 8],
lora_path="FastVideo/LongCat-Video-T2V-Refinement-LoRA",
lora_nickname="refinement",
refine_generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LongCat-Video-T2V-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=True,
text_encoder=True,
pin_cpu_memory=False,
),
),
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_generator.generate_video(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
output_path=refine_output_path,
save_video=True,
refine_from=distill_video_path,
t_thresh=0.5,
spatial_refine_only=False,
num_cond_frames=0, # For refinement, no conditioning frames
height=720,
width=1280,
num_inference_steps=50,
fps=30,
guidance_scale=1.0,
seed=SEED,
refine_generator.generate(
GenerationRequest(
prompt=PROMPT,
negative_prompt=NEGATIVE_PROMPT,
inputs=InputConfig(refine_from=distill_video_path),
sampling=SamplingConfig(
height=720,
width=1280,
num_inference_steps=50,
fps=30,
guidance_scale=1.0,
seed=SEED,
),
output=OutputConfig(
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}")
refine_generator.shutdown()
+28 -11
View File
@@ -1,4 +1,11 @@
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
)
PROMPT = (
@@ -18,22 +25,32 @@ PROMPT = (
def main() -> None:
# Uses FastVideo default sampling settings for LTX2 base.
generator = VideoGenerator.from_pretrained(
"Davids048/LTX2-Base-Diffusers",
num_gpus=1,
generator = VideoGenerator.from_config(
GeneratorConfig(
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"
generator.generate_video(
prompt=PROMPT,
output_path=output_path,
save_video=True,
num_frames=121,
height=1088,
width=1920,
generator.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(
output_path=output_path,
save_video=True,
),
sampling=SamplingConfig(
num_frames=121,
height=1088,
width=1920,
),
)
)
generator.shutdown()
if __name__ == "__main__":
main()
main()
@@ -49,6 +49,11 @@ from pathlib import Path
import torch._inductor.config as _inductor
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.utils import maybe_download_model
@@ -86,9 +91,9 @@ PROMPT = os.getenv("LTX23_I2V_PROMPT", DEFAULT_PROMPT)
# 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."""
logging_info = result.get("logging_info")
logging_info = result.logging_info
stages = getattr(logging_info, "stages", None) if logging_info else None
if not stages:
print(f" [{label}] stage breakdown unavailable")
@@ -104,11 +109,11 @@ def _print_stage_breakdown(result: dict, label: str) -> float | None:
def _collect_stage_times(
result: dict,
result,
stage_times: dict[str, list[float]],
stage_order: OrderedDict[str, None],
) -> None:
logging_info = result.get("logging_info")
logging_info = result.logging_info
stages = getattr(logging_info, "stages", None) if logging_info else None
if not stages:
return
@@ -169,34 +174,54 @@ def main() -> None:
pipeline_config = PipelineConfig.from_pretrained(model_root)
pipeline_config.dit_config.quant_config = None
generator = VideoGenerator.from_pretrained(
model_root,
num_gpus=1,
# LTX-2.3 distilled uses the two-stage refine pipeline; the refine
# LoRA is intentionally empty for the distilled student.
ltx2_refine_enabled=True,
ltx2_refine_upsampler_path=str(refine_upsampler_path),
ltx2_refine_lora_path="",
ltx2_refine_num_inference_steps=3,
ltx2_refine_guidance_scale=1.0,
ltx2_refine_add_noise=True,
pipeline_config=pipeline_config,
enable_torch_compile=True,
enable_torch_compile_text_encoder=True,
# Compile the VAE codec submodules (encoder / decoder) too. The
# `LTX2CausalVideoAutoencoder` declares `_compile_conditions` so
# `_compile_with_conditions` targets just those submodules and
# leaves the surrounding tiling control flow eager — needed for
# fullgraph + dynamic=False to succeed. VAE eager decode is
# ~1.0s; compiling it brings the stage to ~0.3s.
enable_torch_compile_vae=True,
torch_compile_kwargs=torch_compile_kwargs,
torch_compile_kwargs_vae=torch_compile_kwargs,
# Keep everything resident — no CPU offload for serving-style runs.
dit_cpu_offload=False,
text_encoder_cpu_offload=False,
vae_cpu_offload=False,
ltx2_vae_tiling=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_root,
engine=EngineConfig(
num_gpus=1,
compile=CompileConfig(
enabled=True,
text_encoder_enabled=True,
# Compile the VAE codec submodules (encoder / decoder)
# too. The `LTX2CausalVideoAutoencoder` declares
# `_compile_conditions` so `_compile_with_conditions`
# targets just those submodules and leaves the
# surrounding tiling control flow eager — needed for
# fullgraph + dynamic=False to succeed. VAE eager decode
# is ~1.0s; compiling it brings the stage to ~0.3s.
vae_enabled=True,
backend=torch_compile_kwargs["backend"],
fullgraph=torch_compile_kwargs["fullgraph"],
mode=torch_compile_kwargs["mode"],
dynamic=torch_compile_kwargs["dynamic"],
vae_kwargs=torch_compile_kwargs,
),
# Keep everything resident — no CPU offload for serving runs.
offload=OffloadConfig(
dit=False,
text_encoder=False,
vae=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(
@@ -206,12 +231,15 @@ def main() -> None:
height=1280, width=832, # portrait runway aspect
num_frames=121, fps=24, # ~5s clip
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_image_crf=0.0,
save_video=True,
)
warmup_runs = 2
@@ -227,10 +255,25 @@ def main() -> None:
for w in range(warmup_runs):
t0 = time.perf_counter()
print(f"\n[warmup {w + 1}/{warmup_runs}] compiling + generating…")
generator.generate_video(
output_path=str(OUTPUT_DIR / f"_warmup_{w + 1}.mp4"),
seed=7,
**common_kwargs,
generator.generate(
GenerationRequest(
prompt=common_kwargs["prompt"],
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
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"
print(f"\n[measured {m + 1}/{measured_runs}] generating: {out_path}")
t0 = time.perf_counter()
result = generator.generate_video(
output_path=str(out_path),
seed=2002 + m,
**common_kwargs,
result = generator.generate(
GenerationRequest(
prompt=common_kwargs["prompt"],
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
e2e = (
result.get("e2e_latency")
if isinstance(result, dict) else None
) or wall
e2e = (result.extra.get("e2e_latency") if result is not None else None) or wall
measured_secs.append(e2e)
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}")
_collect_stage_times(result, stage_times, stage_order)
@@ -1,4 +1,5 @@
from fastvideo import VideoGenerator
from fastvideo.api import EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig
PROMPT = (
"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"
def main() -> None:
generator = VideoGenerator.from_pretrained(
"FastVideo/LTX2-Distilled-Diffusers",
num_gpus=4,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/LTX2-Distilled-Diffusers",
engine=EngineConfig(num_gpus=4),
)
)
output_path = "outputs_video/ltx2_basic/output_ltx2_distilled_t2v.mp4"
generator.generate_video(
prompt=PROMPT,
output_path=output_path,
save_video=True,
generator.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(output_path=output_path, save_video=True),
)
)
generator.shutdown()
@@ -8,6 +8,11 @@ from pathlib import Path
import torch
import torch._inductor.config
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.layers.quantization.nvfp4_config import NVFP4Config
from fastvideo.utils import maybe_download_model
@@ -45,11 +50,11 @@ def load_validation_entries(path: Path) -> list[dict]:
def print_stage_breakdown(
result: dict,
result: GenerationResult,
run_idx: int,
num_runs: int,
) -> float | None:
logging_info = result.get("logging_info")
logging_info = result.logging_info
if logging_info is None:
print(f"[{run_idx}/{num_runs}] Stage breakdown unavailable: no logging_info")
return None
@@ -70,9 +75,9 @@ def print_stage_breakdown(
def extract_sr_forward_latency(
result: dict,
result: GenerationResult,
) -> 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:
return None, [], []
@@ -106,11 +111,11 @@ def extract_sr_forward_latency(
def collect_stage_times(
result: dict,
result: GenerationResult,
stage_times: dict[str, list[float]],
stage_order: OrderedDict[str, None],
) -> None:
logging_info = result.get("logging_info")
logging_info = result.logging_info
if logging_info is None:
return
stages = getattr(logging_info, "stages", None)
@@ -202,26 +207,45 @@ def main() -> None:
"dynamic": False,
}
generator = VideoGenerator.from_pretrained(
model_root,
num_gpus=1,
ltx2_refine_enabled=True,
ltx2_refine_upsampler_path=str(refine_upsampler_path),
refine_lora_path="", # keep refine LoRA disabled in this repo's typed adapter
ltx2_refine_lora_path="", # keep refine LoRA disabled for distilled model
ltx2_refine_num_inference_steps=2,
ltx2_refine_guidance_scale=1.0,
ltx2_refine_add_noise=True,
pipeline_config=pipeline_config,
enable_torch_compile=True,
enable_torch_compile_text_encoder=True,
enable_torch_compile_vae=True,
torch_compile_kwargs=torch_compile_kwargs,
torch_compile_kwargs_vae=torch_compile_kwargs,
dit_cpu_offload=False,
text_encoder_cpu_offload=False,
vae_cpu_offload=False,
ltx2_vae_tiling=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_root,
engine=EngineConfig(
num_gpus=1,
offload=OffloadConfig(
dit=False,
text_encoder=False,
vae=False,
),
compile=CompileConfig(
enabled=True,
text_encoder_enabled=True,
vae_enabled=True,
backend="inductor",
fullgraph=True,
dynamic=False,
vae_kwargs=torch_compile_kwargs,
),
),
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] = []
@@ -243,25 +267,31 @@ def main() -> None:
torch.cuda.synchronize()
start = time.perf_counter()
result = generator.generate_video(
prompt=prompt,
output_path=str(output_path),
fps=24,
seed=10,
save_video=True,
guidance_scale=1.0,
height=benchmark_entry.get("height", 1088),
width=benchmark_entry.get("width", 1920),
num_frames=121,
num_inference_steps=5,
# image_path="examples/inference/basic/prompt1.png",
# ltx2_image_crf=0.0
result = generator.generate(
GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(
fps=24,
seed=10,
guidance_scale=1.0,
height=benchmark_entry.get("height", 1088),
width=benchmark_entry.get("width", 1920),
num_frames=121,
num_inference_steps=5,
),
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":
torch.cuda.synchronize()
elapsed = result.get("generation_time") if isinstance(result, dict) else None
e2e_elapsed = result.get("e2e_latency") if isinstance(result, dict) else None
elapsed = result.generation_time if isinstance(result, GenerationResult) else None
e2e_elapsed = result.extra.get("e2e_latency") if isinstance(result, GenerationResult) else None
if elapsed is None:
elapsed = time.perf_counter() - start
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}] 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)
if stage_sum is not None:
non_stage_overhead = e2e_elapsed - stage_sum
+32 -21
View File
@@ -1,18 +1,27 @@
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_lucy_edit"
def main():
generator = VideoGenerator.from_pretrained(
"decart-ai/Lucy-Edit-Dev",
num_gpus=1,
use_fsdp_inference=False,
dit_cpu_offload=True,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="decart-ai/Lucy-Edit-Dev",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=True,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
prompt = ("Change the apron and blouse to a classic clown costume: satin "
"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.")
video_path = "https://d2drjpuinn46lb.cloudfront.net/painter_original_edit.mp4"
generator.generate_video(
prompt,
negative_prompt="",
video_path=video_path,
output_path=OUTPUT_PATH,
save_video=True,
height=480,
width=832,
num_frames=81,
fps=24,
guidance_scale=5.0,
)
generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt="",
inputs=InputConfig(video_path=video_path),
sampling=SamplingConfig(
height=480,
width=832,
num_frames=81,
fps=24,
guidance_scale=5.0,
),
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
))
if __name__ == "__main__":
+38 -23
View File
@@ -1,4 +1,6 @@
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
import torch
@@ -38,35 +40,48 @@ def main():
# attempt to identify the optimal arguments.
config = VARIANT_CONFIG[MODEL_VARIANT]
generator = VideoGenerator.from_pretrained(
config["model_path"],
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=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_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=config["model_path"],
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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
actions = create_action_presets(num_frames, keyboard_dim=config["keyboard_dim"])
grid_sizes = torch.tensor([150, 44, 80])
generator.generate_video(
prompt="",
image_path=config["image_url"],
mouse_cond=actions["mouse"].unsqueeze(0),
keyboard_cond=actions["keyboard"].unsqueeze(0),
grid_sizes=grid_sizes,
num_frames=num_frames,
height=352,
width=640,
num_inference_steps=50,
output_path=OUTPUT_PATH,
save_video=True,
generator.generate(
GenerationRequest(
prompt="",
inputs=InputConfig(
image_path=config["image_url"],
mouse_cond=actions["mouse"].unsqueeze(0),
keyboard_cond=actions["keyboard"].unsqueeze(0),
grid_sizes=grid_sizes,
),
sampling=SamplingConfig(
num_frames=num_frames,
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.models.dits.matrixgame2.utils import get_current_action_async, expand_action_to_frames
from fastvideo.api import EngineConfig, GeneratorConfig, OffloadConfig
import torch
import asyncio
@@ -42,17 +43,23 @@ async def main():
# attempt to identify the optimal arguments.
config = VARIANT_CONFIG[MODEL_VARIANT]
generator = StreamingVideoGenerator.from_pretrained(
config["model_path"],
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=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_cpu_offload=False,
generator = StreamingVideoGenerator.from_config(
GeneratorConfig(
model_path=config["model_path"],
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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
+34 -21
View File
@@ -1,4 +1,7 @@
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"
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():
generator = VideoGenerator.from_pretrained(
MODEL_PATH,
num_gpus=1,
use_fsdp_inference=False,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=MODEL_PATH,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
))
generator.generate_video(
prompt=PROMPT,
image_path=IMAGE_URL,
height=720,
width=1280,
num_frames=57,
num_inference_steps=3,
guidance_scale=1.0,
seed=42,
output_path=OUTPUT_PATH,
save_video=True,
)
generator.generate(
GenerationRequest(
prompt=PROMPT,
inputs=InputConfig(image_path=IMAGE_URL),
sampling=SamplingConfig(
height=720,
width=1280,
num_frames=57,
num_inference_steps=3,
guidance_scale=1.0,
seed=42,
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
))
if __name__ == "__main__":
+36 -20
View File
@@ -1,40 +1,56 @@
from fastvideo import VideoGenerator, PipelineConfig
from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
PipelineSelection,
SamplingConfig,
)
def main():
config = PipelineConfig.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
config.text_encoder_precisions = ["fp16"]
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
pipeline_config=config,
use_fsdp_inference=False, # Disable FSDP for MPS
dit_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
disable_autocast=False,
num_gpus=1,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # Disable FSDP for MPS
disable_autocast=False,
offload=OffloadConfig(
dit=True,
text_encoder=True,
pin_cpu_memory=True,
),
),
pipeline=PipelineSelection(
experimental={"pipeline_config": config},
),
)
)
# Create sampling parameters with reduced number of frames
sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers")
sampling_param.num_frames = 25 # Reduce from default 81 to 25 frames bc we have to use the SDPA attn backend for mps
sampling_param.height = 256
sampling_param.width = 256
# Reduce from default 81 to 25 frames bc we have to use the SDPA attn backend for mps
sampling = SamplingConfig(
num_frames=25,
height=256,
width=256,
)
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 "
"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 "
"glistening under the warm afternoon sun. The tall grass ripples gently in "
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"cinematic.")
video2 = generator.generate_video(prompt2, sampling_param=sampling_param)
video2 = generator.generate(GenerationRequest(prompt=prompt2, sampling=sampling))
if __name__ == "__main__":
main()
+25 -15
View File
@@ -1,6 +1,8 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig,
OutputConfig,
)
OUTPUT_PATH = "video_samples"
def main():
@@ -8,17 +10,23 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=2,
use_fsdp_inference=True,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
distributed_executor_backend="ray",
# image_encoder_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=2,
use_fsdp_inference=True,
execution_backend="ray",
offload=OffloadConfig(
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"
# image_encoder=False,
),
),
)
)
# 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 "
"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
# model!
@@ -37,7 +46,8 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"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__":
+45 -29
View File
@@ -85,24 +85,35 @@ def main() -> None:
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = args.backend
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig, OutputConfig,
ParallelismConfig, PipelineSelection, SamplingConfig,
)
os.makedirs(args.out_dir, exist_ok=True)
init_kwargs = {
"num_gpus": args.num_gpus,
"workload_type": "t2i",
"sp_size": 1,
"tp_size": 1,
"dit_cpu_offload": False,
"dit_layerwise_offload": False,
"text_encoder_cpu_offload": False,
"vae_cpu_offload": False,
"image_encoder_cpu_offload": False,
"pin_cpu_memory": False,
"use_fsdp_inference": False,
}
generator = VideoGenerator.from_pretrained(model_path=args.model_path, **init_kwargs)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model_path,
engine=EngineConfig(
num_gpus=args.num_gpus,
use_fsdp_inference=False,
parallelism=ParallelismConfig(
sp_size=1,
tp_size=1,
),
offload=OffloadConfig(
dit=False,
dit_layerwise=False,
text_encoder=False,
vae=False,
image_encoder=False,
pin_cpu_memory=False,
),
),
pipeline=PipelineSelection(workload_type="t2i"),
)
)
try:
for i, prompt in enumerate(prompts):
seed = args.seed + i
@@ -113,20 +124,25 @@ def main() -> None:
output_path = os.path.join(args.out_dir, f"{filename_base}.png")
print(f"[sd35] prompt_idx={i} seed={seed} output_path={output_path}")
generation_kwargs = {
"output_path": output_path,
"height": args.height,
"width": args.width,
"num_frames": 1,
"fps": 1,
"num_inference_steps": args.steps,
"guidance_scale": args.guidance,
"seed": seed,
"negative_prompt": args.negative,
"save_video": True,
}
generator.generate_video(prompt, **generation_kwargs)
generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt=args.negative,
sampling=SamplingConfig(
height=args.height,
width=args.width,
num_frames=1,
fps=1,
num_inference_steps=args.steps,
guidance_scale=args.guidance,
seed=seed,
),
output=OutputConfig(
output_path=output_path,
save_video=True,
),
)
)
print(f"[sd35] done. outputs written to: {args.out_dir}")
finally:
@@ -1,6 +1,14 @@
import os
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"
def main():
@@ -9,23 +17,33 @@ def main():
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
model_name = "wlsaidhi/SFWan2.1-T2V-1.3B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_name,
generator_config = GeneratorConfig(
model_path=model_name,
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
text_encoder_cpu_offload=False,
dit_cpu_offload=False,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
text_encoder=False,
dit=False,
),
),
)
sampling_param = SamplingParam.from_pretrained(model_name)
generator = VideoGenerator.from_config(generator_config)
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 "
"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__":
main()
@@ -1,8 +1,17 @@
# 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
# from fastvideo.api.sampling_param import SamplingParam
OUTPUT_PATH = "video_samples_self_forcing_causal_wan2_2_14B_i2v"
def main():
@@ -10,26 +19,37 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
dit_precision="fp32",
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
dmd_denoising_steps=[1000, 850, 700, 550, 350, 275, 200, 125],
# 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_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/SFWan2.2-I2V-A14B-Preview-Diffusers",
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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,
),
),
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_param.num_frames = 81
sampling_param.width = 832
sampling_param.height = 480
sampling_param.seed = 1000
sampling = SamplingConfig(
num_frames=81,
width=832,
height=480,
seed=1000,
)
with open("assets/prompts/mixkit_i2v.jsonl", "r") as f:
prompt_image_pairs = json.load(f)
@@ -37,7 +57,14 @@ def main():
for prompt_image_pair in prompt_image_pairs:
prompt = prompt_image_pair["prompt"]
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__":
@@ -1,8 +1,10 @@
# NOTE: This is still a work in progress, and the checkpoints are not released yet.
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 = "video_samples_self_forcing_causal_wan2_2_14B_t2v"
def main():
@@ -10,34 +12,49 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"rand0nmr/SFWan2.2-T2V-A14B-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
dmd_denoising_steps=[1000, 850, 700, 550, 350, 275, 200, 125],
# Set pin_cpu_memory to false if CPU RAM is limited and there're no frequent CPU-GPU transfer
pin_cpu_memory=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/",
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/",
num_frame_per_block=7,
# image_encoder_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="rand0nmr/SFWan2.2-T2V-A14B-Diffusers",
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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,
),
),
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
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 "
"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__":
main()
main()
+19 -13
View File
@@ -51,25 +51,31 @@ Prerequisites:
uv pip install k_diffusion einops_exts alias_free_torch torchsde
"""
from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig)
PROMPT = "Lo-fi hip hop instrumental with vinyl crackle and gentle piano."
def main() -> None:
generator = VideoGenerator.from_pretrained(
"FastVideo/stable-audio-open-1.0-Diffusers",
num_gpus=1,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
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"
generator.generate_video(
prompt=PROMPT,
output_path=output_path,
save_video=True,
# 6-second clip; the model max is ~47.5s.
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.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(
output_path=output_path,
save_video=True,
),
# 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()
@@ -48,6 +48,12 @@ Picking `init_audio_strength` (0.0 to 1.0):
Prerequisites: same as `basic_stable_audio.py`.
"""
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
)
PROMPT = "Change the piano to a cello playing the same notes"
# Path to any audio-bearing file (wav, mp3, mp4, m4a, flac, ...).
@@ -58,18 +64,24 @@ INIT_AUDIO_STRENGTH = 0.6
def main() -> None:
generator = VideoGenerator.from_pretrained(
"FastVideo/stable-audio-open-1.0-Diffusers",
num_gpus=1,
)
generator.generate_video(
prompt=PROMPT,
output_path="outputs_audio/stable_audio_a2a/output_a2a.wav",
save_video=True,
audio_end_in_s=6.0,
init_audio=INIT_AUDIO_PATH,
init_audio_strength=INIT_AUDIO_STRENGTH,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/stable-audio-open-1.0-Diffusers",
engine=EngineConfig(num_gpus=1),
))
generator.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(
output_path="outputs_audio/stable_audio_a2a/output_a2a.wav",
save_video=True,
),
extensions={
"audio_end_in_s": 6.0,
"init_audio": INIT_AUDIO_PATH,
"init_audio_strength": INIT_AUDIO_STRENGTH,
},
))
generator.shutdown()
@@ -48,6 +48,9 @@ Prerequisites: same as `basic_stable_audio.py`.
import os
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GeneratorConfig, GenerationRequest, OutputConfig,
)
PROMPT = "Steady lo-fi hip hop drum loop with vinyl crackle."
# 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. "
"Edit this script to point at a real audio file (wav/mp3/mp4/"
"m4a/flac) before running.")
generator = VideoGenerator.from_pretrained(
"FastVideo/stable-audio-open-1.0-Diffusers",
num_gpus=1,
)
generator.generate_video(
prompt=PROMPT,
output_path="outputs_audio/stable_audio_inpaint/output_inpaint.wav",
save_video=True,
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 = VideoGenerator.from_config(
GeneratorConfig(
model_path="FastVideo/stable-audio-open-1.0-Diffusers",
engine=EngineConfig(num_gpus=1),
))
generator.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(
output_path="outputs_audio/stable_audio_inpaint/output_inpaint.wav",
save_video=True,
),
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()
@@ -28,24 +28,27 @@ Prerequisites: same as `basic_stable_audio.py`. The converted repo is
public so no gated-access flow is required.
"""
from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig)
PROMPT = "Lo-fi hip hop instrumental with vinyl crackle and gentle piano."
def main() -> None:
generator = VideoGenerator.from_pretrained(
"FastVideo/stable-audio-open-small-Diffusers",
num_gpus=1,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
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"
generator.generate_video(
prompt=PROMPT,
output_path=output_path,
save_video=True,
# Small variant trains on a ~11.9s window — keep `audio_end_in_s`
# at or below that.
audio_end_in_s=6.0,
)
generator.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(output_path=output_path, save_video=True),
# Small variant trains on a ~11.9s window — keep `audio_end_in_s`
# at or below that.
extensions={"audio_end_in_s": 6.0},
))
generator.shutdown()
@@ -4,6 +4,9 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_turbodiffusion"
@@ -11,14 +14,17 @@ OUTPUT_PATH = "video_samples_turbodiffusion"
def main() -> None:
# TurboDiffusion: 1-4 step video generation using RCM scheduler + SLA attention
# FastVideo will automatically use TurboDiffusionPipeline when specified
generator = VideoGenerator.from_pretrained(
"loayrashid/TurboWan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
# set to false if using RTX 4090
# pin_cpu_memory=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="loayrashid/TurboWan2.1-T2V-1.3B-Diffusers",
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
),
# set to false if using RTX 4090
# pin_cpu_memory=False,
)
)
# 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 "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
)
video = generator.generate_video(
prompt,
output_path=OUTPUT_PATH,
save_video=True,
seed=42,
video = generator.generate(
GenerationRequest(
prompt=prompt,
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
sampling=SamplingConfig(
seed=42,
),
)
)
# 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, "
"cinematic."
)
video2 = generator.generate_video(
prompt2,
output_path=OUTPUT_PATH,
save_video=True,
seed=42,
video2 = generator.generate(
GenerationRequest(
prompt=prompt2,
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
sampling=SamplingConfig(
seed=42,
),
)
)
@@ -4,6 +4,9 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_turbodiffusion_14B"
@@ -11,10 +14,12 @@ OUTPUT_PATH = "video_samples_turbodiffusion_14B"
def main() -> None:
# TurboDiffusion 14B: 1-4 step video generation using RCM scheduler + SLA attention
# FastVideo will automatically use TurboDiffusionPipeline when specified
generator = VideoGenerator.from_pretrained(
"loayrashid/TurboWan2.1-T2V-14B-Diffusers",
# 14B model needs more GPUs
num_gpus=2,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="loayrashid/TurboWan2.1-T2V-14B-Diffusers",
# 14B model needs more GPUs
engine=EngineConfig(num_gpus=2),
)
)
prompt = (
@@ -22,11 +27,12 @@ def main() -> None:
"wide with interest. The playful yet serene atmosphere is complemented by soft "
"natural light filtering through the petals. Mid-shot, warm and cheerful tones."
)
video = generator.generate_video(
prompt,
output_path=OUTPUT_PATH,
save_video=True,
seed=42,
video = generator.generate(
GenerationRequest(
prompt=prompt,
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
sampling=SamplingConfig(seed=42),
)
)
# 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, "
"cinematic."
)
video2 = generator.generate_video(
prompt2,
output_path=OUTPUT_PATH,
save_video=True,
seed=42,
video2 = generator.generate(
GenerationRequest(
prompt=prompt2,
output=OutputConfig(output_path=OUTPUT_PATH, save_video=True),
sampling=SamplingConfig(seed=42),
)
)
@@ -4,6 +4,10 @@ import os
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "SLA_ATTN"
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OutputConfig, SamplingConfig,
)
# Use local model path
MODEL_PATH = "loayrashid/TurboWan2.2-I2V-A14B-Diffusers"
@@ -12,9 +16,11 @@ OUTPUT_PATH = "video_samples_turbodiffusion_i2v"
def main() -> None:
# TurboDiffusion I2V: 1-4 step image-to-video generation
generator = VideoGenerator.from_pretrained(
MODEL_PATH,
num_gpus=2,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=MODEL_PATH,
engine=EngineConfig(num_gpus=2),
)
)
# 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"
video = generator.generate_video(
prompt,
image_path=image_path,
output_path=OUTPUT_PATH,
save_video=True,
seed=42,
video = generator.generate(
GenerationRequest(
prompt=prompt,
inputs=InputConfig(image_path=image_path),
sampling=SamplingConfig(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.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig,
OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_wan2_2_14B_t2v"
def main():
@@ -8,30 +10,37 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.2-T2V-A14B-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=2,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=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_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.2-T2V-A14B-Diffusers",
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=2,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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
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 "
"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)
# video = generator.generate_video(prompt, sampling_param=sampling_param, output_path="wan_t2v_videos/")
_ = generator.generate(
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
# model!
@@ -41,8 +50,14 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"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__":
main()
main()
+35 -16
View File
@@ -1,6 +1,12 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
)
OUTPUT_PATH = "video_samples_wan2_1_Fun"
OUTPUT_NAME = "wan2.1_test"
@@ -9,18 +15,24 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"IRMChen/Wan2.1-Fun-1.3B-Control-Diffusers",
# "alibaba-pai/Wan2.2-Fun-A14B-Control",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=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_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="IRMChen/Wan2.1-Fun-1.3B-Control-Diffusers",
# "alibaba-pai/Wan2.2-Fun-A14B-Control",
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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 = "一位年轻女性穿着一件粉色的连衣裙,裙子上有白色的装饰和粉色的纽扣。她的头发是紫色的,头上戴着一个红色的大蝴蝶结,显得非常可爱和精致。她还戴着一个红色的领结,整体造型充满了少女感和活力。她的表情温柔,双手轻轻交叉放在身前,姿态优雅。背景是简单的灰色,没有任何多余的装饰,使得人物更加突出。她的妆容清淡自然,突显了她的清新气质。整体画面给人一种甜美、梦幻的感觉,仿佛置身于童话世界中。"
@@ -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"
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__":
main()
main()
+30 -15
View File
@@ -1,6 +1,8 @@
from fastvideo import VideoGenerator
# from fastvideo.api.sampling_param import SamplingParam
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples_wan2_2_14B_i2v"
def main():
@@ -8,23 +10,36 @@ def main():
# model.
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.2-I2V-A14B-Diffusers",
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True, # DiT need to be offloaded for MoE
vae_cpu_offload=False,
text_encoder_cpu_offload=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_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.2-I2V-A14B-Diffusers",
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True, # DiT need to be offloaded for MoE
vae=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."
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__":
main()
main()
+33 -13
View File
@@ -1,4 +1,7 @@
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, InputConfig, OffloadConfig, OutputConfig,
)
OUTPUT_PATH = "video_samples_wan2_2_5B_ti2v"
def main():
@@ -7,22 +10,34 @@ def main():
# If a local path is provided, FastVideo will make a best effort
# attempt to identify the optimal arguments.
model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_name,
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
dit_cpu_offload=True,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
# image_encoder_cpu_offload=False,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_name,
engine=EngineConfig(
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=False, # set to True if GPU is out of memory
offload=OffloadConfig(
dit=True,
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
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"
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
# model!
@@ -34,8 +49,13 @@ def main():
"the breeze, enhancing the lion's commanding presence. The tone is vibrant, "
"embodying the raw energy of the wild. Low angle, steady tracking shot, "
"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__":
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:
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "FLASH_ATTN")
from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig, SamplingConfig)
Path(OUTPUT_PATH).parent.mkdir(parents=True, exist_ok=True)
# Davids048/LTX2-Base-Diffusers is the audio-capable LTX-2 checkpoint
# (the Distilled variant ships without the audio VAE, so its mp4
# audio track is silence/noise — unusable for audio.* metrics).
generator = VideoGenerator.from_pretrained(
"Davids048/LTX2-Base-Diffusers",
num_gpus=1,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Davids048/LTX2-Base-Diffusers",
engine=EngineConfig(num_gpus=1),
)
)
generator.generate_video(
prompt=PROMPT,
output_path=OUTPUT_PATH,
save_video=True,
num_frames=121, # ~5s @ 24 fps — long enough for audio.desync (Synchformer ≥14 segments)
height=480,
width=832,
fps=24,
generator.generate(
GenerationRequest(
prompt=PROMPT,
sampling=SamplingConfig(
num_frames=121, # ~5s @ 24 fps — long enough for audio.desync (Synchformer ≥14 segments)
height=480,
width=832,
fps=24,
),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
)
generator.shutdown()
torch.cuda.empty_cache()
@@ -21,6 +21,10 @@ Install: ``uv pip install -e .[eval-audio]`` covers both metrics here
import torch
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig,
SamplingConfig,
)
from fastvideo.eval import create_evaluator
PROMPT = (
@@ -39,20 +43,26 @@ METRICS = [
def main() -> None:
generator = VideoGenerator.from_pretrained(
"Davids048/LTX2-Base-Diffusers",
num_gpus=1,
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Davids048/LTX2-Base-Diffusers",
engine=EngineConfig(num_gpus=1),
))
output_path = "outputs_video/ltx2_audio_eval/output.mp4"
generator.generate_video(
prompt=PROMPT,
output_path=output_path,
save_video=True,
num_frames=121,
height=1088,
width=1920,
)
generator.generate(
GenerationRequest(
prompt=PROMPT,
sampling=SamplingConfig(
num_frames=121,
height=1088,
width=1920,
),
output=OutputConfig(
output_path=output_path,
save_video=True,
),
))
generator.shutdown()
torch.cuda.empty_cache()
+15 -10
View File
@@ -22,6 +22,10 @@ sharing, or run on a smaller-resolution generation.
import torch
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig,
SamplingConfig,
)
from fastvideo.eval import Evaluator
from fastvideo.eval.io import build_eval_kwargs
@@ -58,19 +62,20 @@ METRICS = [
def main() -> None:
# ----- generation (matches examples/inference/basic/basic_ltx2.py) -----
generator = VideoGenerator.from_pretrained(
"Davids048/LTX2-Base-Diffusers",
num_gpus=1,
generator = VideoGenerator.from_config(
GeneratorConfig(
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"
generator.generate_video(
prompt=PROMPT,
output_path=output_path,
save_video=True,
num_frames=121,
height=1088,
width=1920,
generator.generate(
GenerationRequest(
prompt=PROMPT,
output=OutputConfig(output_path=output_path, save_video=True),
sampling=SamplingConfig(num_frames=121, height=1088, width=1920),
)
)
generator.shutdown()
# 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,
num_frames: int, height: int, width: int) -> None:
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OutputConfig, SamplingConfig,
)
videos_dir.mkdir(parents=True, exist_ok=True)
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} "
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:
for row, out_path in todo:
gen.generate_video(
prompt=row["prompt"], output_path=str(out_path), save_video=True,
num_frames=num_frames, height=height, width=width,
)
gen.generate(GenerationRequest(
prompt=row["prompt"],
sampling=SamplingConfig(num_frames=num_frames, height=height, width=width),
output=OutputConfig(output_path=str(out_path), save_video=True),
))
finally:
gen.shutdown()
+9 -5
View File
@@ -43,6 +43,8 @@ def _generate_videos(prompts: list[str], videos_dir: Path,
model: str, num_gpus: int,
num_frames: int, height: int, width: int) -> None:
from fastvideo import VideoGenerator
from fastvideo.api import (EngineConfig, GenerationRequest, GeneratorConfig,
OutputConfig, SamplingConfig)
videos_dir.mkdir(parents=True, exist_ok=True)
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} "
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:
for prompt, out_path in todo:
gen.generate_video(
prompt=prompt, output_path=str(out_path), save_video=True,
num_frames=num_frames, height=height, width=width,
)
gen.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_frames=num_frames, height=height, width=width),
output=OutputConfig(output_path=str(out_path), save_video=True),
))
finally:
gen.shutdown()
+26 -8
View File
@@ -33,6 +33,13 @@ import json
from pathlib import Path
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
)
from fastvideo.eval import create_evaluator
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)
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:
print(f"[gen] generating to {out}...")
generator.generate_video(
prompt=args.prompt,
output_path=str(out),
save_video=True,
num_frames=args.num_frames,
height=args.height,
width=args.width,
generator.generate(
GenerationRequest(
prompt=args.prompt,
sampling=SamplingConfig(
num_frames=args.num_frames,
height=args.height,
width=args.width,
),
output=OutputConfig(
output_path=str(out),
save_video=True,
),
)
)
finally:
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:
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
@@ -5,7 +5,13 @@ import time
import gradio as gr
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
@@ -129,9 +135,22 @@ def create_gradio_interface(default_params: dict[str, SamplingParam], generators
output_dir = "outputs/"
os.makedirs(output_dir, exist_ok=True)
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
logging_info = result.get("logging_info", None)
logging_info = result.logging_info
if logging_info:
stage_names = logging_info.get_execution_order()
stage_execution_times = [
@@ -550,7 +569,7 @@ def main():
for model_path in model_paths:
print(f"Loading model: {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)
demo = create_gradio_interface(default_params, generators)
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`.
The backing stages (`ltx2_refine.py`, `ltx2_i2v_conditioning.py`) are
also missing from `fastvideo/pipelines/stages/`.
3. **`fastvideo.configs.sample.base.SamplingParam`** — the import path used
by this demo. Upstream moved sampling params to
`fastvideo.api.sampling_param`. A re-export shim at the old path, or an
import update here once the other two prereqs land, will resolve it.
3. **`SamplingParam`** — now imported from `fastvideo.api` (the public
re-export of `fastvideo.api.sampling_param`); the old
`fastvideo.configs.sample.base` path was removed upstream. `SamplingParam`
here only sources model-default slider values — generation itself runs
through the typed `GenerationRequest` / `generator.generate(...)` path.
## Environment variables
@@ -4,8 +4,16 @@ from pathlib import Path
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.sample.base import SamplingParam
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.layers.quantization.fp4_config import FP4Config
from fastvideo.utils import maybe_download_model
@@ -48,28 +56,44 @@ def main():
refine_upsampler_path = resolve_refine_upsampler_path(resolved_model_path)
print(f"Using refine upsampler: {refine_upsampler_path}")
generators[model_path] = VideoGenerator.from_pretrained(
str(resolved_model_path),
num_gpus=1,
ltx2_refine_enabled=True,
ltx2_refine_upsampler_path=str(refine_upsampler_path),
ltx2_refine_lora_path="", # disable refine LoRA for distilled model
ltx2_refine_num_inference_steps=2,
ltx2_refine_guidance_scale=1.0,
ltx2_refine_add_noise=True,
pipeline_config=pipeline_config,
enable_torch_compile=True,
enable_torch_compile_text_encoder=True,
torch_compile_kwargs={
"backend": "inductor",
"fullgraph": True,
"mode": "max-autotune-no-cudagraphs",
"dynamic": False,
},
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=False,
ltx2_vae_tiling=False,
generators[model_path] = VideoGenerator.from_config(
GeneratorConfig(
model_path=str(resolved_model_path),
engine=EngineConfig(
num_gpus=1,
offload=OffloadConfig(
dit=False,
vae=False,
text_encoder=False,
),
compile=CompileConfig(
enabled=True,
text_encoder_enabled=True,
backend="inductor",
fullgraph=True,
mode="max-autotune-no-cudagraphs",
dynamic=False,
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
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(
SamplingParam.from_pretrained(str(resolved_model_path))
@@ -4,7 +4,7 @@ from pathlib import Path
import torch
import torch._inductor.config
from fastvideo.configs.sample.base import SamplingParam
from fastvideo.api import SamplingParam
LOCAL_DEMO_DIR = Path(__file__).resolve().parent
CLASSIFIER_DIR = Path(
@@ -5,8 +5,14 @@ from copy import deepcopy
import gradio as gr
from fastvideo.configs.sample.base import SamplingParam
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo import VideoGenerator
from fastvideo.api import (
GenerationRequest,
InputConfig,
OutputConfig,
SamplingConfig,
SamplingParam,
)
from .config import (
DEFAULT_FPS,
@@ -69,40 +75,38 @@ def create_gradio_interface(default_params: dict[str, SamplingParam], generators
output_path = str(OUTPUT_DIR / video_filename)
params.output_path = output_path
start_time = time.perf_counter()
result = generator.generate_video(
prompt=prompt,
output_path=output_path,
fps=DEFAULT_FPS,
seed=int(params.seed),
save_video=True,
return_frames=False,
guidance_scale=float(params.guidance_scale),
height=int(params.height),
width=int(params.width),
num_frames=int(params.num_frames),
num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS,
negative_prompt=params.negative_prompt,
image_path=params.image_path,
ltx2_image_crf=0.0
result = generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt=params.negative_prompt,
inputs=InputConfig(image_path=params.image_path),
sampling=SamplingConfig(
seed=int(params.seed),
fps=DEFAULT_FPS,
guidance_scale=float(params.guidance_scale),
height=int(params.height),
width=int(params.width),
num_frames=int(params.num_frames),
num_inference_steps=DEFAULT_NUM_INFERENCE_STEPS,
),
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
generation_time = (
result.get("generation_time")
if isinstance(result, dict) else None
)
e2e_latency = (
result.get("e2e_latency")
if isinstance(result, dict) else None
)
generation_time = result.generation_time
e2e_latency = result.extra.get("e2e_latency")
if generation_time is None:
generation_time = wall_time
if e2e_latency is None:
e2e_latency = wall_time
resolved_output_path = (
result.get("output_path", output_path)
if isinstance(result, dict) else output_path
)
logging_info = result.get("logging_info", None) if isinstance(result, dict) else None
resolved_output_path = result.video_path or output_path
logging_info = result.logging_info
if logging_info:
stage_names = logging_info.get_execution_order()
stage_execution_times = [
@@ -9,6 +9,7 @@ import uvicorn
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import HTMLResponse, FileResponse
from fastvideo.api import EngineConfig, GeneratorConfig, OffloadConfig
from fastvideo.entrypoints.streaming_generator import StreamingVideoGenerator
from fastvideo.models.dits.matrixgame2.utils import expand_action_to_frames
@@ -572,14 +573,20 @@ def main():
print(f"Loading model: {model_path}")
setup_model_environment(model_path)
generator = StreamingVideoGenerator.from_pretrained(
model_path,
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True,
generator = StreamingVideoGenerator.from_config(
GeneratorConfig(
model_path=model_path,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=False,
text_encoder=True,
pin_cpu_memory=True,
),
),
)
)
generators = {model_path: generator}
@@ -3,7 +3,6 @@ import os
import torch
import base64
import io
from copy import deepcopy
from typing import Dict, Any, Optional, List
import signal
import sys
@@ -20,6 +19,17 @@ import imageio
from ray.serve.handle import DeploymentHandle
from prometheus_client import Counter, Histogram, generate_latest
from fastvideo.api import (
EngineConfig,
GenerationRequest,
GeneratorConfig,
InputConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
SamplingConfig,
)
NUM_GPUS = 16
DEFAULT_FPS = 16
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]]:
frames = result if isinstance(result, list) else result.get("frames", [])
generation_time = result.get("generation_time", 0.0) if isinstance(result, dict) else 0.0
logging_info = result.get("logging_info", None)
frames = result.frames or []
generation_time = result.generation_time or 0.0
logging_info = result.logging_info
if logging_info:
stage_names = logging_info.get_execution_order()
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
def prepare_sampling_params(video_request: VideoGenerationRequest, default_params: Any) -> Any:
params = deepcopy(default_params)
params.prompt = video_request.prompt
if video_request.use_negative_prompt:
params.negative_prompt = video_request.negative_prompt
def prepare_generation_request(video_request: VideoGenerationRequest, image_path: Optional[str] = None) -> Any:
seed = (video_request.seed if not video_request.randomize_seed
else torch.randint(0, SEED_RANGE_MAX, (1,)).item())
params.seed = (video_request.seed if not video_request.randomize_seed
else torch.randint(0, SEED_RANGE_MAX, (1,)).item())
params.randomize_seed = video_request.randomize_seed
params.guidance_scale = video_request.guidance_scale
params.num_frames = video_request.num_frames
params.height = video_request.height
params.width = video_request.width
params.save_video = False
params.return_frames = True
return params
# "" explicitly clears the model preset's negative prompt (None would
# inherit it, changing this demo's long-standing behavior).
negative_prompt = video_request.negative_prompt if video_request.use_negative_prompt else ""
request = GenerationRequest(
prompt=video_request.prompt,
negative_prompt=negative_prompt,
inputs=InputConfig(image_path=image_path),
sampling=SamplingConfig(
seed=seed,
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:
@@ -185,31 +200,34 @@ class BaseModelDeployment:
def _initialize_generator(self, config: Dict[str, Any]) -> None:
from fastvideo.entrypoints.video_generator import VideoGenerator
from fastvideo.api.sampling_param import SamplingParam
print(f"Initializing model: {self.model_path}")
self.generator = VideoGenerator.from_pretrained(
model_path=self.model_path,
num_gpus=1,
use_fsdp_inference=True,
text_encoder_cpu_offload=config["text_encoder_cpu_offload"],
dmd_denoising_steps=[1000, 850, 700, 550, 350, 275, 200, 125], # TODO: hardocde for I2V
dit_precision="fp32", # TODO: hardocde for I2V
dit_cpu_offload=config["dit_cpu_offload"],
vae_cpu_offload=config["vae_cpu_offload"],
VSA_sparsity=config["VSA_sparsity"],
enable_stage_verification=False,
self.generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=self.model_path,
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
enable_stage_verification=False,
offload=OffloadConfig(
text_encoder=config["text_encoder_cpu_offload"],
dit=config["dit_cpu_offload"],
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:
total_start_time = time.time()
params = prepare_sampling_params(video_request, self.default_params)
# Save image if provided (for I2V)
image_path = None
@@ -218,19 +236,15 @@ class BaseModelDeployment:
if image_path is None:
return VideoGenerationResponse(
video_data=None,
seed=params.seed,
seed=video_request.seed,
success=False,
error_message="Failed to save input image",
)
request, seed = prepare_generation_request(video_request, image_path)
inference_start_time = time.time()
result = self.generator.generate_video(
prompt=video_request.prompt,
sampling_param=params,
image_path=image_path,
save_video=False,
return_frames=True,
)
result = self.generator.generate(request)
inference_time = time.time() - inference_start_time
frames, generation_time, stage_names, stage_execution_times = process_generation_result(result)
@@ -250,7 +264,7 @@ class BaseModelDeployment:
return VideoGenerationResponse(
video_data=video_data,
seed=params.seed,
seed=seed,
success=True,
generation_time=generation_time,
inference_time=inference_time,
+45 -25
View File
@@ -1,18 +1,31 @@
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"
def main():
# Initialize VideoGenerator with the Wan model
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
lora_path="benjamin-paine/steamboat-willie-1.3b",
lora_nickname="steamboat"
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
engine=EngineConfig(
num_gpus=1,
offload=OffloadConfig(
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"
),
),
pipeline=PipelineSelection(
components=ComponentConfig(
lora_path="benjamin-paine/steamboat-willie-1.3b",
),
experimental={"lora_nickname": "steamboat"},
),
)
)
kwargs = {
"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."
negative_prompt = "色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
video = generator.generate_video(
prompt,
# sampling_param=sampling_param,
output_path=OUTPUT_PATH,
save_video=True,
negative_prompt=negative_prompt,
**kwargs
video = generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt=negative_prompt,
sampling=SamplingConfig(**kwargs),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
)
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."
negative_prompt = "bad quality video,色调艳丽,过曝,静态,细节模糊不清,字幕,风格,作品,画作,画面,静止,整体发灰,最差质量,低质量,JPEG压缩残留,丑陋的,残缺的,多余的手指,画得不好的手部,画得不好的脸部,畸形的,毁容的,形态畸形的肢体,手指融合,静止不动的画面,杂乱的背景,三条腿,背景人很多,倒着走"
video = generator.generate_video(
prompt,
output_path=OUTPUT_PATH,
save_video=True,
negative_prompt=negative_prompt,
**kwargs
video = generator.generate(
GenerationRequest(
prompt=prompt,
negative_prompt=negative_prompt,
sampling=SamplingConfig(**kwargs),
output=OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
),
)
)
if __name__ == "__main__":
main()
main()
@@ -2,45 +2,60 @@
Inference using a LoRA checkpoint from FastVideo trainer.
"""
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"
def main():
# Initialize VideoGenerator with the Wan model
generator = VideoGenerator.from_pretrained(
"Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
lora_path="checkpoints/wan_t2v_finetune_lora/checkpoint-160/transformer",
lora_nickname="crush_smol"
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
engine=EngineConfig(
num_gpus=1,
offload=OffloadConfig(
dit=False,
vae=True,
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()
kwargs = {
"height": 480,
"width": 832,
"num_frames": 77,
"guidance_scale": 6.0,
"num_inference_steps": 50,
"seed": 42,
}
sampling = SamplingConfig(
height=480,
width=832,
num_frames=77,
guidance_scale=6.0,
num_inference_steps=50,
seed=42,
)
output = OutputConfig(
output_path=OUTPUT_PATH,
save_video=True,
)
# 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."
video = generator.generate_video(
prompt,
output_path=OUTPUT_PATH,
save_video=True,
**kwargs
)
video = generator.generate(
GenerationRequest(
prompt=prompt,
sampling=sampling,
output=output,
))
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(
prompt,
output_path=OUTPUT_PATH,
save_video=True,
**kwargs
)
video = generator.generate(
GenerationRequest(
prompt=prompt,
sampling=sampling,
output=output,
))
if __name__ == "__main__":
main()
main()
@@ -37,6 +37,17 @@ import imageio
import torch
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.layers.quantization.nvfp4_qat_config import NVFP4QATConfig
@@ -148,35 +159,49 @@ def build_generator(args: argparse.Namespace) -> VideoGenerator:
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:
weights_path = resolve_distilled_weights(args.distilled_model)
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:
# Skip the in-pipeline VAE decode entirely: the pipeline returns raw
# latents, the Wan VAE is offloaded to CPU (and not compiled) since we
# decode with TAEHV in this script instead.
extra_kwargs["output_type"] = "latent"
experimental["output_type"] = "latent"
generator = VideoGenerator.from_pretrained(
model_id,
pipeline_config=pipeline_config,
num_gpus=args.num_gpus,
# Keep everything resident on the GPU -- no offloading, except the
# unused Wan VAE when TAEHV handles decoding.
use_fsdp_inference=False,
dit_cpu_offload=False,
dit_layerwise_offload=False,
vae_cpu_offload=args.taehv,
text_encoder_cpu_offload=False,
pin_cpu_memory=False,
enable_torch_compile=compile_enabled,
enable_torch_compile_text_encoder=compile_enabled,
enable_torch_compile_vae=compile_enabled and not args.taehv,
**extra_kwargs,
generator_config = GeneratorConfig(
model_path=model_id,
engine=EngineConfig(
num_gpus=args.num_gpus,
# Keep everything resident on the GPU -- no offloading, except the
# unused Wan VAE when TAEHV handles decoding.
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
dit_layerwise=False,
vae=args.taehv,
text_encoder=False,
pin_cpu_memory=False,
),
compile=CompileConfig(
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
@@ -237,11 +262,11 @@ def main() -> None:
# runs below measure steady-state latency only.
with silence_request_log():
for _ in range(args.warmups):
warm = generator.generate(request={
"prompt": PROMPT,
"sampling": {"num_inference_steps": 2, "guidance_scale": args.guidance_scale},
"output": {"save_video": False, "return_frames": args.taehv},
})
warm = generator.generate(GenerationRequest(
prompt=PROMPT,
sampling=SamplingConfig(num_inference_steps=2, guidance_scale=args.guidance_scale),
output=OutputConfig(save_video=False, return_frames=args.taehv),
))
if args.taehv:
taehv.decode(warm.samples)
@@ -257,18 +282,18 @@ def main() -> None:
frames = None
with silence_request_log():
for i in range(args.benchmark_runs):
result = generator.generate(request={
"prompt": PROMPT,
"sampling": {
"num_inference_steps": args.infer_steps,
"guidance_scale": args.guidance_scale,
},
"output": {
"save_video": False,
"return_frames": args.taehv,
"output_path": output_path,
},
})
result = generator.generate(GenerationRequest(
prompt=PROMPT,
sampling=SamplingConfig(
num_inference_steps=args.infer_steps,
guidance_scale=args.guidance_scale,
),
output=OutputConfig(
save_video=False,
return_frames=args.taehv,
output_path=output_path,
),
))
denoise_elapsed = result.generation_time
denoise_times.append(denoise_elapsed)
@@ -2,31 +2,41 @@ import os
import time
from fastvideo import VideoGenerator
from fastvideo.api import (
EngineConfig, GenerationRequest, GeneratorConfig, OffloadConfig,
OutputConfig, SamplingConfig,
)
def main():
# set the attention backend
# set the attention backend
os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "FLASH_ATTN"
start_time = time.perf_counter()
gen = VideoGenerator.from_pretrained(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
num_gpus=1,
dit_cpu_offload=False,
vae_cpu_offload=False,
text_encoder_cpu_offload=True,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
)
gen = VideoGenerator.from_config(
GeneratorConfig(
model_path="Wan-AI/Wan2.1-T2V-1.3B-Diffusers",
engine=EngineConfig(
num_gpus=1,
offload=OffloadConfig(
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
print(f"Model loading time: {load_time:.2f} seconds")
gen_start_time = time.perf_counter()
gen.generate_video(
prompt=
"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.",
seed=1024,
output_path="example_outputs/")
gen.generate(
GenerationRequest(
prompt=
"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.",
sampling=SamplingConfig(seed=1024),
output=OutputConfig(output_path="example_outputs/")))
generation_time = time.perf_counter() - gen_start_time
print(f"Video generation time: {generation_time:.2f} seconds")
@@ -18,6 +18,10 @@ import os
import time
from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, SamplingConfig,
)
OUTPUT_PATH = "video_samples"
@@ -39,17 +43,24 @@ def main():
mode += "_compile"
print(f"Mode: {mode.upper()}")
generator = VideoGenerator.from_pretrained(
args.model,
num_gpus=args.num_gpus,
nvfp4_fa4=args.nvfp4_fa4,
use_fsdp_inference=not args.nvfp4_fa4,
dit_cpu_offload=False,
dit_layerwise_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
enable_torch_compile=args.compile,
)
if args.nvfp4_fa4:
os.environ["FASTVIDEO_NVFP4_FA4"] = "1"
os.environ.setdefault("CUTE_DSL_ENABLE_TVM_FFI", "1")
generator = VideoGenerator.from_config(GeneratorConfig(
model_path=args.model,
engine=EngineConfig(
num_gpus=args.num_gpus,
use_fsdp_inference=not args.nvfp4_fa4,
offload=OffloadConfig(
dit=False,
dit_layerwise=False,
vae=True,
text_encoder=True,
),
compile=CompileConfig(enabled=args.compile),
),
))
prompt = (
"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
for i in range(n_warmup):
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 2},
"output": {"save_video": False}})
generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=2),
output=OutputConfig(save_video=False),
))
os.makedirs(OUTPUT_PATH, exist_ok=True)
start = time.time()
generator.generate(request={
"prompt": prompt,
"sampling": {"num_inference_steps": args.infer_steps},
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")},
})
generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=args.infer_steps),
output=OutputConfig(
save_video=True,
output_path=os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4"),
),
))
elapsed = time.time() - start
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
f"({args.infer_steps / elapsed:.2f} it/s)")
@@ -69,6 +69,16 @@ def main():
os.environ.setdefault("FASTVIDEO_ATTENTION_BACKEND", "SAGE_ATTN")
from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OffloadConfig,
OutputConfig,
PipelineSelection,
SamplingConfig,
)
from fastvideo.layers.quantization import get_quantization_config
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
# transformer_quant needs a QuantizationConfig *instance* — the bare string
# is not resolved on the from_pretrained kwarg path.
extra = {} if args.bf16 else {
"transformer_quant": get_quantization_config("FP8")(granularity=args.granularity)
}
generator = VideoGenerator.from_pretrained(
args.model,
num_gpus=args.num_gpus,
use_fsdp_inference=False,
dit_cpu_offload=False,
dit_layerwise_offload=False,
vae_cpu_offload=use_taehv,
text_encoder_cpu_offload=False,
pin_cpu_memory=False,
enable_torch_compile=not args.no_compile,
enable_torch_compile_vae=not args.no_compile and not use_taehv,
output_type="latent" if use_taehv else "pil",
**extra,
)
# ``output_type`` and ``transformer_quant`` have no first-class typed
# fields yet, so they ride the pipeline.experimental escape hatch (same
# place the legacy from_pretrained shim routed them). The typed
# QuantizationConfig only accepts a quant-name string, so it can't carry
# FP8's ``granularity`` arg — pass the resolved config instance instead.
experimental = {"output_type": "latent" if use_taehv else "pil"}
if not args.bf16:
experimental["transformer_quant"] = get_quantization_config("FP8")(granularity=args.granularity)
generator = VideoGenerator.from_config(GeneratorConfig(
model_path=args.model,
engine=EngineConfig(
num_gpus=args.num_gpus,
use_fsdp_inference=False,
offload=OffloadConfig(
dit=False,
dit_layerwise=False,
vae=use_taehv,
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 = (
"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
for _ in range(n_warmup):
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 3, "guidance_scale": 1.0},
"output": {"save_video": False}})
generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=3, guidance_scale=1.0),
output=OutputConfig(save_video=False),
))
os.makedirs(OUTPUT_PATH, exist_ok=True)
start = time.time()
if use_taehv:
result = generator.generate(request={
"prompt": prompt,
"sampling": {"num_inference_steps": args.infer_steps, "guidance_scale": 1.0},
"output": {"save_video": False},
})
result = generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=args.infer_steps, guidance_scale=1.0),
output=OutputConfig(save_video=False),
))
import imageio
frames = decode_with_taehv(taehv_model, result.samples)
video_path = os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")
imageio.mimsave(video_path, frames, fps=16, format="mp4")
print(f"Saved TAEHV-decoded video to: {video_path}")
else:
generator.generate(request={
"prompt": prompt,
"sampling": {"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")},
})
generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=args.infer_steps, guidance_scale=1.0),
output=OutputConfig(
save_video=True,
output_path=os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4"),
),
))
elapsed = time.time() - start
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}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.
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
if args.compile:
mode += "_compile"
print(f"Mode: {mode.upper()}")
# transformer_quant needs a QuantizationConfig *instance* — the bare string
# is not resolved on the from_pretrained kwarg path.
extra = {} if args.bf16 else {"transformer_quant": get_quantization_config(args.quant_method)()}
generator = VideoGenerator.from_pretrained(
args.model,
num_gpus=args.num_gpus,
use_fsdp_inference=args.bf16,
dit_cpu_offload=False,
vae_cpu_offload=True,
text_encoder_cpu_offload=True,
enable_torch_compile=args.compile,
**extra,
)
# transformer_quant takes the config name (string); the typed path resolves
# the QuantizationConfig class.
quantization = None if args.bf16 else QuantizationConfig(transformer_quant=args.quant_method)
generator = VideoGenerator.from_config(GeneratorConfig(
model_path=args.model,
engine=EngineConfig(
num_gpus=args.num_gpus,
use_fsdp_inference=args.bf16,
offload=OffloadConfig(dit=False, vae=True, text_encoder=True),
compile=CompileConfig(enabled=args.compile),
quantization=quantization,
),
))
prompt = (
"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
for _ in range(n_warmup):
generator.generate(request={"prompt": prompt, "sampling": {"num_inference_steps": 2},
"output": {"save_video": False}})
generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=2),
output=OutputConfig(save_video=False),
))
os.makedirs(OUTPUT_PATH, exist_ok=True)
start = time.time()
generator.generate(request={
"prompt": prompt,
"sampling": {"num_inference_steps": args.infer_steps},
"output": {"save_video": True, "output_path": os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")},
})
generator.generate(GenerationRequest(
prompt=prompt,
sampling=SamplingConfig(num_inference_steps=args.infer_steps),
output=OutputConfig(save_video=True,
output_path=os.path.join(OUTPUT_PATH, f"raccoon_{mode}.mp4")),
))
elapsed = time.time() - start
print(f"[{mode.upper()}] {args.infer_steps} steps in {elapsed:.2f}s "
f"({args.infer_steps / elapsed:.2f} it/s)")
@@ -1,4 +1,7 @@
from fastvideo import VideoGenerator
from fastvideo.api import (ComponentConfig, EngineConfig, GenerationRequest,
GeneratorConfig, InputConfig, OffloadConfig,
OutputConfig, PipelineSelection, QuantizationConfig)
import argparse
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
# attempt to identify the optimal arguments.
model_name = "Wan-AI/Wan2.2-TI2V-5B-Diffusers"
generator = VideoGenerator.from_pretrained(
model_name,
# FastVideo will automatically handle distributed setup
num_gpus=1,
use_fsdp_inference=True,
dit_cpu_offload=True,
vae_cpu_offload=False,
text_encoder_cpu_offload=False,
# AbsMaxFP8 is the quantization method used by ComfyUI;
# check fastvideo/layers/quantization/* for more quantization methods
override_text_encoder_quant="AbsMaxFP8",
# for Wan 2.2, this is the path to "umt5_xxl_fp8_e4m3fn_scaled.safetensors"
override_text_encoder_safetensors=text_encoder_path,
pin_cpu_memory=True, # set to false if low CPU RAM or hit obscure "CUDA error: Invalid argument"
)
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=model_name,
# FastVideo will automatically handle distributed setup
engine=EngineConfig(
num_gpus=1,
use_fsdp_inference=True,
offload=OffloadConfig(
dit=True,
vae=False,
text_encoder=False,
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
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"
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),
))
if __name__ == "__main__":
@@ -22,6 +22,14 @@ import os
import time
from fastvideo import VideoGenerator
from fastvideo.api import (
CompileConfig,
EngineConfig,
GenerationRequest,
GeneratorConfig,
OutputConfig,
SamplingConfig,
)
PROMPT = (
"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)
generator = VideoGenerator.from_pretrained(
args.model,
num_gpus=args.num_gpus,
enable_torch_compile=args.compile,
generator = VideoGenerator.from_config(
GeneratorConfig(
model_path=args.model,
engine=EngineConfig(
num_gpus=args.num_gpus,
compile=CompileConfig(enabled=args.compile),
),
)
)
def _run(tag: str) -> float:
save = tag == "measured"
# Modern typed-request API (generate_video is deprecated). Same
# prompt/seed/shapes both runs so the compiled graph is reused.
request: dict = {
"prompt": PROMPT,
"sampling": {"seed": 1024},
"output": {"save_video": save},
}
# Same prompt/seed/shapes both runs so the compiled graph is reused.
output = OutputConfig(save_video=save)
if save:
request["output"]["output_path"] = (
f"video_samples/torch_compile_{tag}.mp4")
output.output_path = f"video_samples/torch_compile_{tag}.mp4"
request = GenerationRequest(
prompt=PROMPT,
sampling=SamplingConfig(seed=1024),
output=output,
)
t0 = time.perf_counter()
generator.generate(request)
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):
raise ConfigValidationError("", f"expected mapping for {config_type.__name__}")
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:
return bind_generation_request_raw(parsed, raw)
return bind_generation_request_raw(parsed, drop_none_leaves(raw))
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:
return bind_serve_config_raw(parsed, raw)
return bind_serve_config_raw(parsed, drop_none_leaves(raw))
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:
"""Serialize a typed config object into plain Python containers."""
if dataclasses.is_dataclass(config) and not isinstance(config, type):
+27 -16
View File
@@ -136,19 +136,27 @@ class InputConfig:
@dataclass
class SamplingConfig:
num_videos_per_prompt: int = 1
seed: int = 1024
num_frames: int = 125
height: int = 720
width: int = 1280
height_sr: int = 1072
width_sr: int = 1920
fps: int = 24
num_inference_steps: int = 50
num_inference_steps_sr: int = 50
guidance_scale: float = 1.0
"""Sampling knobs for a generation request.
``None`` means "not specified": the value is inherited from the
model's preset (``SamplingParam.from_pretrained``) at generate time.
Set a field to override the preset. There is deliberately no way to
express "explicitly the schema default" — pass the concrete value
you want instead.
"""
num_videos_per_prompt: int | None = None
seed: int | None = None
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_rescale: float = 0.0
guidance_rescale: float | None = None
true_cfg_scale: float | None = None
boundary_ratio: float | None = None
sigmas: list[float] | None = None
@@ -194,6 +202,8 @@ class GenerationPlan:
class GenerationRequest:
prompt: str | list[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)
sampling: SamplingConfig = field(default_factory=SamplingConfig)
runtime: RequestRuntimeConfig = field(default_factory=RequestRuntimeConfig)
@@ -261,15 +271,16 @@ class ServeConfig:
the incoming body as the operator-pinned baseline.
Important nuance: only fields the operator **explicitly wrote** in the
serve YAML/JSON count as defaults. Although the in-memory object is
fully populated (schema defaults fill every unset field), the merge
walks ``_fastvideo_explicit_paths`` — populated during parse — so
serve YAML/JSON count as defaults. Unset sampling fields stay ``None``
("inherit the model preset"), other sections keep their schema
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:
body (client-explicit) > default_request (operator-explicit)
> 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``
for the merge.
"""
@@ -1,4 +1,22 @@
# 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 collections.abc import Mapping
@@ -8,7 +26,12 @@ from pathlib import Path
from typing import Any
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 (
EXPLICIT_PATHS_ATTR,
bind_generation_request_raw,
@@ -314,8 +337,12 @@ def normalize_generation_request(request: GenerationRequest | Mapping[str, Any],
if not hasattr(normalized, EXPLICIT_PATHS_ATTR):
# Request wasn't bound through the parser (e.g. constructed
# directly). Treat every currently-set field as explicit.
bind_generation_request_raw(normalized, _serialize_generation_request(normalized))
# directly). Bind only non-None leaves as explicit: None is the
# 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
@@ -364,15 +391,13 @@ def request_to_sampling_param(
updates = explicit_request_updates(request)
for key, value in updates.items():
if key == "return_state":
# Already translated to return_continuation_state above.
continue
if hasattr(sampling_param, key):
setattr(sampling_param, key, deepcopy(value))
elif key in _REQUEST_PIPELINE_OVERRIDE_FIELDS:
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:
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
operator-pinned baseline rather than a full override: a YAML with just
``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`` —
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.
"""
assert hasattr(request,
@@ -579,8 +606,6 @@ def _serialize_generation_request(request: GenerationRequest) -> dict[str, Any]:
return deepcopy(config_to_dict(request))
_SCHEMA_DEFAULT_UPDATES = _extract_request_updates(config_to_dict(GenerationRequest()))
_KNOWN_CONTINUATION_KINDS: set[str] = set()
+1 -1
View File
@@ -5,7 +5,7 @@ import argparse
import os
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.inference_config import build_serve_config
from fastvideo.logger import init_logger
+8 -7
View File
@@ -19,7 +19,7 @@ from fastapi import (
)
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.entrypoints.openai.state import (
get_default_request,
@@ -60,12 +60,13 @@ def _build_generation_kwargs(
defaults on the dataclass are *not* treated as defaults here.
3. Hardcoded fallback (e.g. ``fps=24`` when neither side set it).
Why gate on ``model_fields_set`` / explicit paths? Both the request
Pydantic model and the ``GenerationRequest`` dataclass carry schema
defaults (e.g. ``seed=1024``, ``num_frames=125``). Without the gate
those would masquerade as intent and shadow the other side — the
gate preserves "operator pinned it" vs. "dataclass happened to have
that default."
Why gate on ``model_fields_set`` / explicit paths? The request
Pydantic model carries schema defaults, and the ``GenerationRequest``
dataclass carries non-None defaults outside ``sampling`` (e.g.
``output_path``; sampling fields default to None = "inherit the
model preset"). Without the gate those would masquerade as intent
and shadow the other side — the gate preserves "operator pinned it"
vs. "dataclass happened to have that default."
"""
kwargs: dict[str, Any] = {}
if default_request is not None:
+11 -4
View File
@@ -48,10 +48,14 @@ class MockGenerator:
def generate(self, request: GenerationRequest) -> dict[str, Any]:
if self.sleep_ms:
time.sleep(self.sleep_ms / 1000.0)
width = max(16, request.sampling.width)
height = max(16, request.sampling.height)
num_frames = max(1, request.sampling.num_frames)
frames = [_gradient_frame(height, width, idx, seed=request.sampling.seed) for idx in range(num_frames)]
# Requests carry None for unset sampling fields (normally resolved
# against the model preset); the mock has no model, so fall back
# to fixed values here.
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(
kind="ltx2.v1",
payload={
@@ -90,6 +94,9 @@ def build_mock_app(*, sleep_ms: float = 0.0):
width=256,
fps=24,
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))
+20 -1
View File
@@ -95,6 +95,19 @@ class ServerState:
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(
serve_config: ServeConfig,
generator: _GeneratorProto | None = None,
@@ -118,6 +131,8 @@ def build_app(
if (generator is None) == (pool is None):
raise ValueError("build_app requires exactly one of `generator` or `pool`")
_validate_default_sampling_pins(serve_config)
store = session_store or InMemorySessionStore()
if pool is 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:
raise ValueError("ServeConfig.streaming must be set to launch the streaming server; "
"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
@@ -490,7 +507,9 @@ def _build_generation_request(
}
request = GenerationRequest(
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"), ),
sampling=SamplingConfig(**sampling_kwargs),
output=OutputConfig(save_video=False, return_frames=True, return_state=True),
+7 -2
View File
@@ -26,7 +26,7 @@ import torch
import torchvision
from einops import rearrange
from fastvideo.api.compat import (
from fastvideo.api.translation import (
expand_request_prompt_batch,
generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config,
@@ -314,7 +314,12 @@ class VideoGenerator:
import asyncio
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")
if log_queue:
@@ -23,7 +23,7 @@ from collections.abc import Mapping
from dataclasses import dataclass, field
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
if TYPE_CHECKING:
+1 -1
View File
@@ -5,7 +5,7 @@ from types import SimpleNamespace
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.generate import GenerateSubcommand
from fastvideo.entrypoints.cli.inference_config import (
@@ -1,11 +1,11 @@
# 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
PR 6.
"""
from __future__ import annotations
from fastvideo.api.compat import (
from fastvideo.api.translation import (
generator_config_to_fastvideo_args,
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,
"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
@@ -21,7 +21,7 @@ import torch
# Importing compat first, then the LTX-2 module, exercises the
# self-registration side effect on import (important for the API
# 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 (
ContinuationState,
GenerationRequest,
@@ -217,7 +217,7 @@ class TestCompatLayerWireUp:
# PR 7 removes the NotImplementedError for request.state; build a
# minimal GenerationRequest carrying an LTX-2 state and make sure
# the public boundary accepts it.
from fastvideo.api.compat import (
from fastvideo.api.translation import (
normalize_generation_request,
_validate_continuation_state,
)
@@ -230,13 +230,13 @@ class TestCompatLayerWireUp:
_validate_continuation_state(normalized.state)
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"):
_validate_continuation_state(
ContinuationState(kind="mystery.v1", payload={}))
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"):
_validate_continuation_state(
ContinuationState(kind="", payload={}))
@@ -19,7 +19,7 @@ from copy import deepcopy
import pytest
from fastvideo.api.compat import (
from fastvideo.api.translation import (
generator_config_to_fastvideo_args,
legacy_from_pretrained_to_config,
)
@@ -208,7 +208,7 @@ class TestRefineFlattenCoversAllTypedFields:
def test_all_fields_reemitted(self, monkeypatch) -> None:
from fastvideo import fastvideo_args as fva
from fastvideo.api.compat import (
from fastvideo.api.translation import (
generator_config_to_fastvideo_args,
)
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,
},
"sampling": {
"num_videos_per_prompt": 1,
"seed": 1024,
# None = inherit the model preset at generate time; only
# the YAML-set num_frames carries a value.
"num_videos_per_prompt": None,
"seed": None,
"num_frames": 16,
"height": 720,
"width": 1280,
"height_sr": 1072,
"width_sr": 1920,
"fps": 24,
"num_inference_steps": 50,
"num_inference_steps_sr": 50,
"guidance_scale": 1.0,
"height": None,
"width": None,
"height_sr": None,
"width_sr": None,
"fps": None,
"num_inference_steps": None,
"num_inference_steps_sr": None,
"guidance_scale": None,
"guidance_scale_2": None,
"guidance_rescale": 0.0,
"guidance_rescale": None,
"true_cfg_scale": None,
"boundary_ratio": 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
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 (
EngineConfig,
GeneratorConfig,
@@ -34,7 +34,7 @@ from fastvideo.api import (
GeneratorConfig,
GenerationRequest,
)
from fastvideo.api.compat import (
from fastvideo.api.translation import (
legacy_from_pretrained_to_config,
legacy_generate_call_to_request,
normalize_generation_request,
@@ -205,7 +205,7 @@ class TestDreamverseNoPrivateImports:
[
"fastvideo",
"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):
@@ -269,7 +269,7 @@ class TestNoInternalImports:
"fastvideo.pipelines.",
"fastvideo.configs.",
"fastvideo.fastvideo_args",
"fastvideo.api.compat",
"fastvideo.api.translation",
"fastvideo.api.parser",
"fastvideo.api.overrides",
"fastvideo.api.errors",
@@ -199,7 +199,7 @@ class TestHealthCheckRequest:
assert req.output.return_frames is False
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
req = VideoGenerator.default_health_check_request()
@@ -224,6 +224,28 @@ class TestMockServer:
app = build_mock_app()
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):
from fastvideo.api.schema import GenerationRequest, SamplingConfig
from fastvideo.entrypoints.streaming.mock_server import MockGenerator
@@ -69,7 +69,7 @@ def _patch_fastvideo_args_from_kwargs(monkeypatch):
)
monkeypatch.setattr(
"fastvideo.api.compat.FastVideoArgs.from_kwargs",
"fastvideo.api.translation.FastVideoArgs.from_kwargs",
classmethod(fake_from_kwargs),
)
return captured
@@ -261,7 +261,7 @@ def test_generate_uses_typed_request_path(monkeypatch):
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()
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"].height == 720
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(
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.
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>/
model_index.json
@@ -683,21 +683,23 @@ def main():
print()
print(" 1. Test basic generation:")
print(" from fastvideo import VideoGenerator")
print(f" generator = VideoGenerator.from_pretrained('{output_dir}')")
print(" video = generator.generate_video(")
print(" from fastvideo.api import GeneratorConfig, GenerationRequest, SamplingConfig")
print(f" generator = VideoGenerator.from_config(GeneratorConfig(model_path='{output_dir}'))")
print(" result = generator.generate(GenerationRequest(")
print(" prompt='A cat playing piano',")
print(" num_inference_steps=50")
print(" )")
print(" sampling=SamplingConfig(num_inference_steps=50),")
print(" ))")
print()
if (output_dir / "lora" / "distilled").exists():
print(" 2. Test distilled generation (16 steps with LoRA):")
print(f" generator = VideoGenerator.from_pretrained('{output_dir}',")
print(f" lora_path='{output_dir}/lora/distilled',")
print(" lora_nickname='distilled')")
print(" video = generator.generate_video(")
print(" from fastvideo.api import ComponentConfig, PipelineSelection")
print(f" generator = VideoGenerator.from_config(GeneratorConfig(model_path='{output_dir}',")
print(f" pipeline=PipelineSelection(")
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(" num_inference_steps=16,")
print(" guidance_scale=1.0)")
print(" sampling=SamplingConfig(num_inference_steps=16, guidance_scale=1.0)))")
print()
print()
@@ -18,7 +18,8 @@ Example (one-shot per variant):
--public
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.
"""
from __future__ import annotations
@@ -97,42 +97,53 @@ def generate_with_model(
flow_shift: Optional[float] = None,
embedded_guidance_scale: Optional[float] = None,
) -> str:
"""Produce a video with VideoGenerator.from_pretrained; returns video path."""
"""Produce a video with VideoGenerator.from_config; returns video path."""
try:
from fastvideo import VideoGenerator # lazy import
from fastvideo.api import (
ComponentConfig, EngineConfig, GenerationRequest, GeneratorConfig,
OffloadConfig, OutputConfig, PipelineSelection, SamplingConfig,
)
except Exception as exc:
raise RuntimeError(f"Failed to import fastvideo.VideoGenerator: {exc}") from exc
init_kwargs: Dict[str, Any] = {
"num_gpus": 1,
"dit_cpu_offload": True,
"vae_cpu_offload": True,
"text_encoder_cpu_offload": True,
"pin_cpu_memory": True,
}
pipeline_kwargs: Dict[str, Any] = {}
if lora_path:
init_kwargs["lora_path"] = lora_path
init_kwargs["lora_nickname"] = "extracted"
pipeline_kwargs["components"] = ComponentConfig(lora_path=lora_path)
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 = {
"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,
}
extensions: Dict[str, Any] = {}
if flow_shift is not None:
gen_kwargs["flow_shift"] = flow_shift
extensions["flow_shift"] = flow_shift
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
try:
@@ -145,9 +156,9 @@ def generate_with_model(
expected = Path(output_dir) / f"{output_name}.mp4"
if expected.exists():
return str(expected)
# fallback: check result dict
if isinstance(result, dict) and "video_path" in result:
return str(result["video_path"])
# fallback: check result video path
if getattr(result, "video_path", None):
return str(result.video_path)
raise FileNotFoundError(f"Video not found at expected path: {expected}")