diff --git a/README.md b/README.md index dbca636b6..a580fa7d1 100644 --- a/README.md +++ b/README.md @@ -142,11 +142,13 @@ def main(): prompt = "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes wide with interest." # Generate the video - video = generator.generate_video( - prompt, - output_path="my_videos/", # Controls where videos are saved - save_video=True - ) + video = generator.generate({ + "prompt": prompt, + "output": { + "output_path": "my_videos/", # Controls where videos are saved + "save_video": True, + }, + }) if __name__ == '__main__': main() diff --git a/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py b/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py index fdfa46255..dbb3745e2 100644 --- a/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py +++ b/apps/dreamverse/dreamverse/benchmarks/benchmark_pipeline.py @@ -52,7 +52,8 @@ import torch # noqa: E402 from fastvideo import VideoGenerator # noqa: E402 from fastvideo.api import ( # noqa: E402 - ComponentConfig, CompileConfig, EngineConfig, GeneratorConfig, OffloadConfig, PipelineSelection, QuantizationConfig, + ComponentConfig, CompileConfig, EngineConfig, GenerationResult, GeneratorConfig, OffloadConfig, PipelineSelection, + QuantizationConfig, ) DEFAULT_PROMPT = ("A cinematic drone shot over coastal cliffs at sunrise, golden " @@ -128,9 +129,9 @@ def _build_generator_config(model_path: str, enable_compile: bool, num_gpus: int ) -def _extract_stage_times(result: dict) -> OrderedDict[str, float]: +def _extract_stage_times(result: GenerationResult) -> OrderedDict[str, float]: out: OrderedDict[str, float] = OrderedDict() - info = result.get("logging_info") if isinstance(result, dict) else None + info = result.logging_info if isinstance(result, GenerationResult) else None if info is None: return out stages = getattr(info, "stages", None) @@ -162,19 +163,25 @@ def _do_one_run(generator: VideoGenerator, prompt: str, *, height: int, width: i _reset_peak_gpu() t0 = time.perf_counter() try: - result = generator.generate_video( - prompt=prompt, - negative_prompt="", - save_video=False, - height=height, - width=width, - num_frames=num_frames, - fps=24, - num_inference_steps=num_inference_steps, - guidance_scale=1.0, - seed=seed, - ltx2_image_crf=0.0, - ) + result = generator.generate({ + "prompt": prompt, + "negative_prompt": "", + "sampling": { + "height": height, + "width": width, + "num_frames": num_frames, + "fps": 24, + "num_inference_steps": num_inference_steps, + "guidance_scale": 1.0, + "seed": seed, + }, + "output": { + "save_video": False + }, + "extensions": { + "ltx2_image_crf": 0.0 + }, + }) if torch.cuda.is_available(): torch.cuda.synchronize() except Exception as exc: diff --git a/apps/dreamverse/dreamverse/ltx2_generation.py b/apps/dreamverse/dreamverse/ltx2_generation.py index 59f74680c..788dd5bc9 100644 --- a/apps/dreamverse/dreamverse/ltx2_generation.py +++ b/apps/dreamverse/dreamverse/ltx2_generation.py @@ -14,6 +14,7 @@ import gc import os import re import time +from typing import TYPE_CHECKING import numpy as np import torch @@ -34,6 +35,9 @@ from dreamverse.config import ( ) from dreamverse.generation_contracts import StepResult +if TYPE_CHECKING: + from fastvideo.api import GenerationResult + # Multi-frame decoded continuation defaults from # examples/inference/basic/basic_ltx2_distilled_video_continuation.py. # Overridable via environment variables. @@ -95,8 +99,8 @@ class ContinuationState: self.video_images = None self.audio_latents = None - def apply_video(self, request_kwargs: dict, segment_idx: int) -> None: - """Seed next-segment kwargs with the cached tail frames.""" + def apply_video(self, request: dict, segment_idx: int) -> None: + """Seed the next-segment request with the cached tail frames.""" if segment_idx <= 1 or not self.video_images: return from PIL import Image @@ -110,21 +114,21 @@ class ContinuationState: arr = np.clip(arr, 0, 255).astype(np.uint8) noisy.append(Image.fromarray(arr)) cond_images = noisy - request_kwargs["ltx2_video_conditions"] = [( + request["extensions"]["ltx2_video_conditions"] = [( cond_images, LTX2_VIDEO_CONDITIONING_FRAME_IDX, LTX2_VIDEO_CONDITIONING_STRENGTH, )] - request_kwargs["ltx2_images"] = None - request_kwargs["image_path"] = None + request["extensions"]["ltx2_images"] = None + request["inputs"]["image_path"] = None def apply_audio( self, - request_kwargs: dict, + request: dict, segment_idx: int, audio_lps: float, ) -> None: - """Seed next-segment kwargs with clean audio latents + denoise mask. + """Seed the next-segment request with clean audio latents + denoise mask. When audio conditioning is longer than video, extend audio generation and shift video RoPE forward so the audio prefix @@ -141,9 +145,9 @@ class ContinuationState: audio_extra = max(0, AUDIO_CONDITIONING_NUM_FRAMES - LTX2_VIDEO_CONDITIONING_NUM_FRAMES) if audio_extra > 0: audio_num_frames = NUM_FRAMES + audio_extra - request_kwargs["audio_num_frames"] = (audio_num_frames) + request["extensions"]["audio_num_frames"] = (audio_num_frames) prefix_sec = float(audio_extra) / 24.0 - request_kwargs["video_position_offset_sec"] = prefix_sec + request["extensions"]["video_position_offset_sec"] = prefix_sec new_duration = float(NUM_FRAMES + audio_extra) / 24.0 total_T = max( @@ -161,8 +165,8 @@ class ContinuationState: mask = torch.ones((B, 1, total_T, 1), dtype=torch.float32) mask[:, :, :audio_cond_T, :] = (1.0 - AUDIO_CONDITIONING_STRENGTH) - request_kwargs["ltx2_audio_clean_latent"] = clean - request_kwargs["ltx2_audio_denoise_mask"] = mask + request["extensions"]["ltx2_audio_clean_latent"] = clean + request["extensions"]["ltx2_audio_denoise_mask"] = mask def save_video(self, frames: list) -> None: """Snapshot trailing N frames as PIL images for next-segment conditioning.""" @@ -460,51 +464,59 @@ class LTX2GenerationBackend: prompt = self._inject_style_trigger(prompt) - request_kwargs = dict( - prompt=prompt, - negative_prompt="", - save_video=False, - height=FRAME_HEIGHT, - width=FRAME_WIDTH, - num_frames=NUM_FRAMES, - fps=24, - num_inference_steps=NUM_INFERENCE_STEPS, - guidance_scale=1.0, - seed=10, - ltx2_image_crf=0.0, - image_path=image_path if segment_idx == 1 else None, - return_continuation_state=False, - ) + request = { + "prompt": prompt, + "negative_prompt": "", + "inputs": { + "image_path": image_path if segment_idx == 1 else None + }, + "sampling": { + "height": FRAME_HEIGHT, + "width": FRAME_WIDTH, + "num_frames": NUM_FRAMES, + "fps": 24, + "num_inference_steps": NUM_INFERENCE_STEPS, + "guidance_scale": 1.0, + "seed": 10, + }, + "output": { + "save_video": False + }, + "extensions": { + "ltx2_image_crf": 0.0, + "return_continuation_state": False, + }, + } if reset_conditioning: self.continuation.clear() audio_lps = (DEFAULT_LTX2_AUDIO_SAMPLE_RATE / DEFAULT_LTX2_AUDIO_HOP_LENGTH / DEFAULT_LTX2_AUDIO_DOWNSAMPLE) - # Phase 1: seed kwargs with prior-segment conditioning. - self.continuation.apply_video(request_kwargs, segment_idx) - self.continuation.apply_audio(request_kwargs, segment_idx, audio_lps) + # Phase 1: seed the request with prior-segment conditioning. + self.continuation.apply_video(request, segment_idx) + self.continuation.apply_audio(request, segment_idx, audio_lps) # Phase 2: generate. t0 = time.perf_counter() - result = self.generator.generate_video(**request_kwargs) + result = self.generator.generate(request) torch.cuda.synchronize() timings["generation_ms"] = (time.perf_counter() - t0) * 1000 - if not isinstance(result, dict): - raise RuntimeError("Expected dictionary output from generate_video.") - frames = result.get("frames") + if isinstance(result, list): + raise RuntimeError("Expected a single GenerationResult from generate.") + frames = result.frames if not isinstance(frames, list) or len(frames) == 0: raise RuntimeError("Generation did not return frames.") - audio = result.get("audio") - audio_sample_rate = result.get("audio_sample_rate") + audio = result.audio + audio_sample_rate = result.audio_sample_rate if audio is not None and audio_sample_rate is None: # LTX2 audio decoding stage uses 24kHz output by default. audio_sample_rate = 24000 print(f"[GPU {self.gpu_id}] audio_sample_rate missing from result; " f"defaulting to {audio_sample_rate}Hz") - timings["generation_time_ms"] = result.get("generation_time", 0.0) * 1000 + timings["generation_time_ms"] = (result.generation_time or 0.0) * 1000 # Phase 3: snapshot continuation state for the next segment. t_save_start = time.perf_counter() @@ -542,7 +554,7 @@ class LTX2GenerationBackend: self, audio: object, audio_sample_rate: int | None, - result: dict, + result: "GenerationResult", segment_idx: int, ) -> torch.Tensor | None: """Pick which tensor to cache for next-segment audio conditioning.""" @@ -557,7 +569,7 @@ class LTX2GenerationBackend: f"for segment {segment_idx + 1}") return re_encoded return None - audio_latents = result.get("ltx2_audio_latents") + audio_latents = result.extra.get("ltx2_audio_latents") if audio_latents is not None: print(f"[GPU {self.gpu_id}] Cached audio latents " f"shape={tuple(audio_latents.shape)} " diff --git a/apps/fastvideo_studio/job_runner.py b/apps/fastvideo_studio/job_runner.py index e3316d833..08b391d4b 100644 --- a/apps/fastvideo_studio/job_runner.py +++ b/apps/fastvideo_studio/job_runner.py @@ -862,23 +862,40 @@ class JobRunner: sp_size, ) - gen = VideoGenerator.from_pretrained( - model_id, - workload_type=workload_type, - num_gpus=num_gpus, - dit_layerwise_offload=dit_layerwise_offload, - **({ - "override_pipeline_cls_name": override_pipeline_cls_name - } if override_pipeline_cls_name else {}), - dit_cpu_offload=dit_cpu_offload, - text_encoder_cpu_offload=text_encoder_cpu_offload, - vae_cpu_offload=vae_cpu_offload, - image_encoder_cpu_offload=image_encoder_cpu_offload, - use_fsdp_inference=use_fsdp_inference, - enable_torch_compile=enable_torch_compile, - VSA_sparsity=vsa_sparsity, - tp_size=tp_size, - sp_size=sp_size, + gen = VideoGenerator.from_config( + { + "model_path": model_id, + "engine": { + "num_gpus": num_gpus, + "parallelism": { + "tp_size": tp_size, + "sp_size": sp_size, + }, + "offload": { + "dit": dit_cpu_offload, + "dit_layerwise": dit_layerwise_offload, + "text_encoder": text_encoder_cpu_offload, + "image_encoder": image_encoder_cpu_offload, + "vae": vae_cpu_offload, + }, + "compile": { + "enabled": enable_torch_compile + }, + "attention": { + "vsa_sparsity": vsa_sparsity + }, + "use_fsdp_inference": use_fsdp_inference, + }, + "pipeline": { + "workload_type": + workload_type, + **({ + "components": { + "override_pipeline_cls_name": override_pipeline_cls_name + } + } if override_pipeline_cls_name else {}), + }, + }, log_queue=log_queue, ) @@ -1103,30 +1120,33 @@ class JobRunner: # Without a name FastVideo derives the filename from the prompt. safe_name = re.sub(r'[\\/:*?"<>|]+', "", job.name).strip().strip(".") output_target = (os.path.join(job_output_dir, f"{safe_name[:80]}.mp4") if safe_name else job_output_dir) - gen_kwargs: dict[str, Any] = { + request: dict[str, Any] = { "prompt": job.prompt, - "output_path": output_target, - "save_video": True, - "num_inference_steps": job.num_inference_steps, - "num_frames": job.num_frames, - "height": job.height, - "width": job.width, - "guidance_scale": job.guidance_scale, - "guidance_rescale": job.guidance_rescale, - "fps": job.fps, - "seed": job.seed, "negative_prompt": job.negative_prompt or "", - "log_queue": log_queue, + "sampling": { + "num_inference_steps": job.num_inference_steps, + "num_frames": job.num_frames, + "height": job.height, + "width": job.width, + "guidance_scale": job.guidance_scale, + "guidance_rescale": job.guidance_rescale, + "fps": job.fps, + "seed": job.seed, + }, + "output": { + "output_path": output_target, + "save_video": True, + }, } if job.image_path: - gen_kwargs["image_path"] = job.image_path + request.setdefault("inputs", {})["image_path"] = job.image_path if job.references: - gen_kwargs["references"] = _build_h3_references(job.references) + request.setdefault("inputs", {})["references"] = _build_h3_references(job.references) if job.last_image_path: # _prepare_fl2va requires a PIL image, not a path. from PIL import Image as _PILImage - gen_kwargs["last_image"] = _PILImage.open(job.last_image_path) - generator.generate_video(**gen_kwargs) + request.setdefault("inputs", {})["last_image"] = _PILImage.open(job.last_image_path) + generator.generate(request, log_queue=log_queue) buf.phase = "saving" logger.info("Generation completed, searching for output file...") diff --git a/comfyui/video_generator/video_generator.py b/comfyui/video_generator/video_generator.py index 8d7705fed..733bd1691 100644 --- a/comfyui/video_generator/video_generator.py +++ b/comfyui/video_generator/video_generator.py @@ -15,6 +15,9 @@ from fastvideo import VideoGenerator as FastVideoGenerator sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))))) +# InferenceArgs keys that are SamplingConfig fields of a GenerationRequest. +_SAMPLING_INFERENCE_ARGS = ("height", "width", "num_frames", "num_inference_steps", "guidance_scale", "seed", "fps") + # Custom exception for interruption class GenerationInterruptedException(Exception): @@ -154,7 +157,17 @@ class VideoGenerator: """Thread function to run the generation""" try: if self.generator is not None: - self.generator.generate_video(prompt=prompt, output_path=output_path, **inference_args) + # Place each InferenceArgs value in the GenerationRequest section that owns it. + request: dict[str, Any] = {"prompt": prompt, "output": {"output_path": output_path}} + for key, value in inference_args.items(): + if key == "image_path": + section = "inputs" + elif key in _SAMPLING_INFERENCE_ARGS: + section = "sampling" + else: + section = "extensions" + request.setdefault(section, {})[key] = value + self.generator.generate(request) self._generation_result = os.path.join(output_path, f"{prompt[:100]}.mp4") else: raise RuntimeError("Generator is not initialized") @@ -253,9 +266,24 @@ class VideoGenerator: if self.generator is None: print('generation_args', generation_args) print('pipeline_config', pipeline_config) - self.generator = FastVideoGenerator.from_pretrained(model_path=model_path, - **generation_args, - pipeline_config=pipeline_config) + # Place each generation argument at its GeneratorConfig engine path. + engine_config: dict[str, Any] = {} + if "num_gpus" in generation_args: + engine_config["num_gpus"] = generation_args["num_gpus"] + for parallelism_key in ("tp_size", "sp_size"): + if parallelism_key in generation_args: + engine_config.setdefault("parallelism", {})[parallelism_key] = generation_args[parallelism_key] + if "dit_cpu_offload" in generation_args: + engine_config["offload"] = {"dit": generation_args["dit_cpu_offload"]} + self.generator = FastVideoGenerator.from_config({ + "model_path": model_path, + "engine": engine_config, + "pipeline": { + "experimental": { + "pipeline_config": pipeline_config + } + }, + }) print('inference_args', inference_args) diff --git a/docs/contributing/coding_agents.md b/docs/contributing/coding_agents.md index 6c3dd335e..450f52d4b 100644 --- a/docs/contributing/coding_agents.md +++ b/docs/contributing/coding_agents.md @@ -57,19 +57,15 @@ Minimal usage example (based on `examples/inference/basic/basic.py`): ```python from fastvideo import VideoGenerator -from fastvideo.api.sampling_param import SamplingParam model_id = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" # or official_weights// generator = VideoGenerator.from_pretrained(model_id, num_gpus=1) -sampling = SamplingParam.from_pretrained(model_id) -sampling.num_frames = 45 -video = generator.generate_video( - "A vibrant city street at sunset.", - sampling_param=sampling, - output_path="video_samples", - save_video=True, -) +video = generator.generate({ + "prompt": "A vibrant city street at sunset.", + "sampling": {"num_frames": 45}, + "output": {"output_path": "video_samples", "save_video": True}, +}) ``` ## Some questions to ask yourself before starting diff --git a/docs/design/overview.md b/docs/design/overview.md index f76039686..bc493faa5 100644 --- a/docs/design/overview.md +++ b/docs/design/overview.md @@ -28,19 +28,15 @@ Minimal usage (from `examples/inference/basic/basic.py`): ```python from fastvideo import VideoGenerator -from fastvideo.api.sampling_param import SamplingParam model_id = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" # or official_weights// generator = VideoGenerator.from_pretrained(model_id, num_gpus=1) -sampling = SamplingParam.from_pretrained(model_id) -sampling.num_frames = 45 -video = generator.generate_video( - "A vibrant city street at sunset.", - sampling_param=sampling, - output_path="video_samples", - save_video=True, -) +video = generator.generate({ + "prompt": "A vibrant city street at sunset.", + "sampling": {"num_frames": 45}, + "output": {"output_path": "video_samples", "save_video": True}, +}) ``` ## Configuration system diff --git a/docs/inference/architecture.md b/docs/inference/architecture.md index c86038750..1dbf6a7da 100644 --- a/docs/inference/architecture.md +++ b/docs/inference/architecture.md @@ -372,10 +372,10 @@ generator = VideoGenerator.from_pretrained( model_path="Wan-AI/Wan2.1-T2V-14B-Diffusers", num_gpus=1, tp_size=1, sp_size=1, ) -result = generator.generate_video( - prompt="A cat dancing", - height=720, width=1280, num_frames=81, -) +result = generator.generate({ + "prompt": "A cat dancing", + "sampling": {"height": 720, "width": 1280, "num_frames": 81}, +}) ``` **CLI** (`fastvideo/entrypoints/cli/`): @@ -421,7 +421,7 @@ User: VideoGenerator.from_pretrained(model_path, **kwargs) ├─ torch.compile (if enabled) └─ warmup_sequence_parallel_communication() -User: generator.generate_video(prompt, ...) +User: generator.generate(request) │ ├─ ForwardBatch constructed from SamplingParam + user args └─ pipeline.forward(batch, fastvideo_args) diff --git a/docs/inference/configuration.md b/docs/inference/configuration.md index eed3ff4ec..170c12c9f 100644 --- a/docs/inference/configuration.md +++ b/docs/inference/configuration.md @@ -37,7 +37,7 @@ You can customize generation behavior using `PipelineConfig` and `SamplingParam`: ```python -from fastvideo import VideoGenerator, SamplingParam, PipelineConfig +from fastvideo import VideoGenerator, PipelineConfig def main(): model_name = "Wan-AI/Wan2.1-T2V-1.3B-Diffusers" @@ -45,43 +45,40 @@ def main(): config.vae_precision = "fp16" # Create the generator - generator = VideoGenerator.from_pretrained( - model_name, - num_gpus=1, - dit_layerwise_offload=True, # FastVideoArgs option - pipeline_config=config - ) - - # Create and customize sampling parameters - sampling_param = SamplingParam.from_pretrained("Wan-AI/Wan2.1-T2V-1.3B-Diffusers") - - # How many frames to generate - sampling_param.num_frames = 45 - - # Video resolution (width, height) - sampling_param.width = 1024 - sampling_param.height = 576 - - # How many steps we denoise the video (higher = better quality, slower generation) - sampling_param.num_inference_steps = 30 - - # How strongly the video conforms to the prompt (higher = more faithful to prompt) - sampling_param.guidance_scale = 7.5 - - # Random seed for reproducibility - sampling_param.seed = 42 # Optional, leave unset for random results + generator = VideoGenerator.from_config({ + "model_path": model_name, + "engine": { + "num_gpus": 1, + "offload": {"dit_layerwise": True}, # FastVideoArgs option + }, + "pipeline": {"experimental": {"pipeline_config": config}}, + }) # Generate video with custom parameters prompt = "A beautiful sunset over a calm ocean, with gentle waves." - video = generator.generate_video( - prompt, - sampling_param=sampling_param, - output_path="my_videos/", # Controls where videos are saved - save_video=True - ) + video = generator.generate({ + "prompt": prompt, + "sampling": { + # How many frames to generate + "num_frames": 45, + # Video resolution (width, height) + "width": 1024, + "height": 576, + # How many steps we denoise the video (higher = better quality, slower generation) + "num_inference_steps": 30, + # How strongly the video conforms to the prompt (higher = more faithful to prompt) + "guidance_scale": 7.5, + # Random seed for reproducibility + "seed": 42, # Optional, leave unset for random results + }, + "output": { + "output_path": "my_videos/", # Controls where videos are saved + "save_video": True, + }, + }) - # If return_frames=True, frames are available in video["frames"] - print(f"Generated {len(video['frames'])} frames") + # If return_frames=True, frames are available in video.frames + print(f"Generated {len(video.frames)} frames") if __name__ == '__main__': main() diff --git a/docs/inference/inference_quick_start.md b/docs/inference/inference_quick_start.md index 8e12fc62c..3224408f9 100644 --- a/docs/inference/inference_quick_start.md +++ b/docs/inference/inference_quick_start.md @@ -45,11 +45,13 @@ def main(): prompt = "A curious raccoon peers through a vibrant field of yellow sunflowers, its eyes wide with interest." # Generate the video - video = generator.generate_video( - prompt, - output_path="my_videos/", # Controls where videos are saved - save_video=True - ) + video = generator.generate({ + "prompt": prompt, + "output": { + "output_path": "my_videos/", # Controls where videos are saved + "save_video": True, + }, + }) if __name__ == '__main__': main() @@ -75,23 +77,24 @@ Please see the [support matrix](support_matrix.md) for the list of supported mod You can generate a video starting from an initial image: ```python -from fastvideo import VideoGenerator, SamplingParam +from fastvideo import VideoGenerator def main(): # Create the generator model_name = "Wan-AI/Wan2.1-I2V-14B-480P-Diffusers" generator = VideoGenerator.from_pretrained(model_name, num_gpus=1) - # Set up parameters with an initial image - sampling_param = SamplingParam.from_pretrained(model_name) - sampling_param.image_path = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/astronaut.jpg" - sampling_param.num_frames = 107 - # Generate video based on the image prompt = "A photograph coming to life with gentle movement" - generator.generate_video(prompt, sampling_param=sampling_param, - output_path="my_videos/", - save_video=True) + generator.generate({ + "prompt": prompt, + # Set up parameters with an initial image + "inputs": { + "image_path": "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/astronaut.jpg", + }, + "sampling": {"num_frames": 107}, + "output": {"output_path": "my_videos/", "save_video": True}, + }) if __name__ == '__main__': main() diff --git a/docs/inference/offloading.md b/docs/inference/offloading.md index 904581271..fb32e2ff8 100644 --- a/docs/inference/offloading.md +++ b/docs/inference/offloading.md @@ -215,7 +215,7 @@ generator = VideoGenerator.from_pretrained( ) prompt = "A curious raccoon peers through a vibrant field of yellow sunflowers." -video = generator.generate_video(prompt, output_path="output/", save_video=True) +video = generator.generate({"prompt": prompt, "output": {"output_path": "output/", "save_video": True}}) ``` ### Multi-GPU with FSDP @@ -238,5 +238,5 @@ generator = VideoGenerator.from_pretrained( ) prompt = "A majestic lion strides across the golden savanna." -video = generator.generate_video(prompt, output_path="output/", save_video=True) +video = generator.generate({"prompt": prompt, "output": {"output_path": "output/", "save_video": True}}) ``` diff --git a/docs/inference/optimizations.md b/docs/inference/optimizations.md index 095da3bf2..1b9a32a9a 100644 --- a/docs/inference/optimizations.md +++ b/docs/inference/optimizations.md @@ -176,7 +176,7 @@ gen = VideoGenerator.from_pretrained( num_gpus=1, use_fsdp_inference=False, # FSDP is incompatible with FP4 pointer path ) -gen.generate_video(prompt="A raccoon in sunflowers", save_video=True) +gen.generate(request={"prompt": "A raccoon in sunflowers", "output": {"save_video": True}}) ``` #### Known Limitations @@ -206,14 +206,20 @@ os.environ["FASTVIDEO_ATTENTION_BACKEND"] = "ATTN_QAT_INFER" from fastvideo import VideoGenerator from fastvideo.layers.quantization import get_quantization_config -gen = VideoGenerator.from_pretrained( - "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", - num_gpus=1, - # Wan-2.1 uses the nvfp4_qat config (NVFP4 is LTX2-specific). Pass an - # instance — the bare string is not resolved on the from_pretrained path. - transformer_quant=get_quantization_config("nvfp4_qat")(), - use_fsdp_inference=False, # FSDP shards invalidate the FP4 tensor pointers -) +gen = VideoGenerator.from_config({ + "model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", + "engine": { + "num_gpus": 1, + "use_fsdp_inference": False, # FSDP shards invalidate the FP4 tensor pointers + }, + "pipeline": { + "experimental": { + # Wan-2.1 uses the nvfp4_qat config (NVFP4 is LTX2-specific). Pass an + # instance — the bare string is not resolved under pipeline.experimental. + "transformer_quant": get_quantization_config("nvfp4_qat")(), + }, + }, +}) gen.generate(request={"prompt": "A raccoon in sunflowers", "output": {"save_video": True}}) ``` @@ -305,12 +311,16 @@ automatically. from fastvideo import VideoGenerator from fastvideo.layers.quantization import get_quantization_config -gen = VideoGenerator.from_pretrained( - "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", - # Pass an instance — the bare string is not resolved on the from_pretrained path. - transformer_quant=get_quantization_config("FP8")(), # per-tensor (default) - # transformer_quant=get_quantization_config("FP8")(granularity="channel"), # slower, higher accuracy -) +gen = VideoGenerator.from_config({ + "model_path": "Wan-AI/Wan2.1-T2V-1.3B-Diffusers", + "pipeline": { + "experimental": { + # Pass an instance — the bare string is not resolved under pipeline.experimental. + "transformer_quant": get_quantization_config("FP8")(), # per-tensor (default) + # "transformer_quant": get_quantization_config("FP8")(granularity="channel"), # slower, higher accuracy + }, + }, +}) gen.generate(request={"prompt": "A raccoon in sunflowers", "output": {"save_video": True}}) ``` @@ -376,10 +386,10 @@ device is unsupported. Legacy VSA, MiniMax-H3 tile-256 VSA, and the explicit eager with one warning instead of failing mid-denoise. ```python -generator = VideoGenerator.from_pretrained( - "MiniMaxAI/MiniMax-H3", - inference_torch_compile=True, # or FASTVIDEO_INFERENCE_TORCH_COMPILE=1 -) +generator = VideoGenerator.from_config({ + "model_path": "MiniMaxAI/MiniMax-H3", + "engine": {"compile": {"regional": True}}, # or FASTVIDEO_INFERENCE_TORCH_COMPILE=1 +}) ``` In a YAML config, set `generator.engine.compile.regional: true`. @@ -501,10 +511,10 @@ for backend in ["TORCH_SDPA", "FLASH_ATTN", "SAGE_ATTN"]: os.environ["FASTVIDEO_ATTENTION_BACKEND"] = backend generator = VideoGenerator.from_pretrained("your-model-id") start_time = time.perf_counter() - generator.generate_video( - prompt="Your prompt", - seed=1024, - ) + generator.generate({ + "prompt": "Your prompt", + "sampling": {"seed": 1024}, + }) elapsed = time.perf_counter() - start_time print(f"{backend}: {elapsed:.2f}s") ```