-
Notifications
You must be signed in to change notification settings - Fork 2.6k
[None][feat] Add FastWan2.2 TI2V-5B DMD pipeline (3-step text-to-video) #16162
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e5a5a24
80e919b
29621e7
e3ea640
6449d26
39985a4
80aeed8
9abbc7b
5faa880
705c86c
503dde5
e4929b5
063b7b9
4fdfdf4
b5c6e4f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,192 @@ | ||
| # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. | ||
| # SPDX-License-Identifier: Apache-2.0 | ||
| # | ||
| # Licensed under the Apache License, Version 2.0 (the "License"); | ||
| # you may not use this file except in compliance with the License. | ||
| # You may obtain a copy of the License at | ||
| # | ||
| # http://www.apache.org/licenses/LICENSE-2.0 | ||
| # | ||
| # Unless required by applicable law or agreed to in writing, software | ||
| # distributed under the License is distributed on an "AS IS" BASIS, | ||
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| # See the License for the specific language governing permissions and | ||
| # limitations under the License. | ||
| """FastWan: distilled (DMD) Wan 2.2 TI2V-5B for 3-step text-to-video. | ||
|
|
||
| FastWan shares Wan 2.2 TI2V-5B's architecture exactly — only the weights | ||
| (distilled) and the sampling recipe differ. So this pipeline subclasses | ||
| ``WanPipeline`` and overrides only the denoising loop. | ||
| """ | ||
|
|
||
| import time | ||
|
|
||
| import PIL.Image | ||
| import torch | ||
| from diffusers.utils.torch_utils import randn_tensor | ||
|
|
||
| from tensorrt_llm._torch.visual_gen.output import CudaPhaseTimer, PipelineOutput | ||
| from tensorrt_llm._torch.visual_gen.pipeline_registry import register_pipeline | ||
| from tensorrt_llm._utils import nvtx_range | ||
| from tensorrt_llm.logger import logger | ||
|
|
||
| from .defaults import get_fastwan_default_params | ||
| from .pipeline_wan import WanPipeline | ||
|
|
||
|
|
||
| @register_pipeline( | ||
| "WanDMDPipeline", | ||
| hf_ids=[ | ||
| "FastVideo/FastWan2.2-TI2V-5B-FullAttn-Diffusers", | ||
| ], | ||
| doc="FastWan 2.2 distilled (DMD) — 3-step Wan 2.2 TI2V-5B text-to-video.", | ||
| ) | ||
| class WanDMDPipeline(WanPipeline): | ||
| """Wan 2.2 TI2V-5B with the DMD 3-step sampling loop. | ||
|
|
||
| DMD inference (Distribution Matching Distillation): at each step the model | ||
| predicts the clean video ``x0``, then we re-noise it with FRESH noise to the | ||
| next (lower) noise level. The last step keeps ``x0``. CFG-free — one | ||
| transformer forward per step. Faithful to FastVideo's ``DmdDenoisingStage``. | ||
| """ | ||
|
|
||
| # Fixed DMD schedule for FastWan2.2-TI2V-5B (see model card / FastVideo config). | ||
| DMD_TIMESTEPS = (1000, 757, 522) | ||
| NUM_TRAIN_TIMESTEPS = 1000 | ||
|
|
||
| @property | ||
| def default_generation_params(self): | ||
| """Return FastWan-specific default generation parameters (steps, guidance, frame rate).""" | ||
| return get_fastwan_default_params() | ||
|
|
||
| @nvtx_range("WanDMDPipeline.forward") | ||
| @torch.no_grad() | ||
| def forward( | ||
| self, | ||
| prompt: str | list[str], | ||
| seed: int, | ||
| negative_prompt: str | None = None, | ||
| height: int = 704, | ||
| width: int = 1280, | ||
| num_frames: int = 121, | ||
| num_inference_steps: int | None = None, | ||
| guidance_scale: float | None = None, | ||
| guidance_scale_2: float | None = None, | ||
| boundary_ratio: float | None = None, | ||
|
Comment on lines
+74
to
+75
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What about disallow
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. its inherited from the wan base class even though its not used |
||
| max_sequence_length: int = 512, | ||
| image: PIL.Image.Image | torch.Tensor | str | None = None, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Should we update the signature?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. All four (num_inference_steps, guidance_scale, guidance_scale_2, boundary_ratio) are sent in by the inherited infer() but ignored by FastWan. I kept them explicit so all four are treated the same and the shape matches the base class; the warnings tell the user they're ignored. Can move them to **kwargs if you'd prefer that? |
||
| ) -> PipelineOutput: | ||
| """Generate a video from a text prompt using the 3-step DMD sampling loop. | ||
|
|
||
| CFG-free: one transformer forward pass per step, no negative prompt support. | ||
| Unsupported parameters (num_inference_steps, guidance_scale, guidance_scale_2, | ||
| boundary_ratio) are accepted for base-class compatibility but logged as warnings | ||
| and ignored. | ||
| """ | ||
| if image is not None: | ||
| raise NotImplementedError( | ||
| "FastWan currently supports text-to-video only (no image conditioning)." | ||
| ) | ||
| if num_inference_steps not in (None, len(self.DMD_TIMESTEPS)): | ||
| logger.warning( | ||
| f"FastWan always uses {len(self.DMD_TIMESTEPS)} DMD steps; " | ||
| f"ignoring num_inference_steps={num_inference_steps}." | ||
| ) | ||
|
Comment on lines
+90
to
+94
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you misunderstand the semantic of
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. same thing here as well as i noted below |
||
| if guidance_scale not in (None, 1.0): | ||
| logger.warning(f"FastWan is CFG-free; ignoring guidance_scale={guidance_scale}.") | ||
| if guidance_scale_2 is not None: | ||
| logger.warning( | ||
| f"FastWan does not support guidance_scale_2; ignoring guidance_scale_2={guidance_scale_2}." | ||
| ) | ||
| if boundary_ratio is not None: | ||
| logger.warning( | ||
| f"FastWan does not support boundary_ratio; ignoring boundary_ratio={boundary_ratio}." | ||
| ) | ||
|
|
||
| pipeline_start = time.time() | ||
| timer = CudaPhaseTimer() | ||
| timer.mark_pre_start() | ||
|
|
||
| if isinstance(prompt, str): | ||
| prompt = [prompt] | ||
| batch_size = len(prompt) | ||
| generator = torch.Generator(device=self.device).manual_seed(seed) | ||
| self.validate_resolution(height, width, num_frames) | ||
|
|
||
| if negative_prompt: | ||
| raise ValueError("FastWan is CFG-free and does not support negative prompts.") | ||
| prompt_embeds, _ = self._encode_prompt(prompt, "", max_sequence_length) | ||
|
|
||
| if self._fixed_latent is not None: | ||
| latents = self._fixed_latent.to(device=self.device, dtype=self.dtype) | ||
| else: | ||
| latents = self._prepare_latents(batch_size, height, width, num_frames, generator) | ||
|
|
||
| timer.mark_denoise_start() | ||
| latents = self._denoise(latents, prompt_embeds, generator) | ||
| timer.mark_post_start() | ||
|
|
||
| logger.info("Decoding video...") | ||
| decode_start = time.time() | ||
| video = self.decode_latents(latents, self._decode_latents) | ||
| if self.rank == 0: | ||
| logger.info(f"Video decoded in {time.time() - decode_start:.2f}s") | ||
| logger.info(f"Total pipeline time: {time.time() - pipeline_start:.2f}s") | ||
|
|
||
| timer.mark_end() | ||
| frame_rate = self.default_generation_params.get("frame_rate", 24.0) | ||
| return timer.fill(PipelineOutput(video=video, frame_rate=frame_rate)) | ||
|
|
||
| @nvtx_range("WanDMDPipeline._denoise", color="blue") | ||
| def _denoise( | ||
| self, latents: torch.Tensor, prompt_embeds: torch.Tensor, generator: torch.Generator | ||
| ) -> torch.Tensor: | ||
| """3-step DMD loop: predict x0, then re-noise with fresh noise. | ||
|
|
||
| The per-step sigma reduces to ``sigma = t / NUM_TRAIN_TIMESTEPS`` because | ||
| FastVideo's scheduler keeps ``timesteps = sigmas * num_train_timesteps`` | ||
| (co-scaled), so the lookup returns ``matched_timestep / 1000`` and the | ||
| flow-shift cancels (verified numerically for shift in {1, 5, 8, 12}). | ||
| """ | ||
| timesteps = self.DMD_TIMESTEPS | ||
| num_steps = len(timesteps) | ||
| _, ph, pw = self.transformer.config.patch_size | ||
|
|
||
| # The DMD ops are linear (mul/sub/add), so they run in the transformer's | ||
| # native (bf16) dtype to match the FastVideo reference; no fp32 needed. | ||
| latents = latents.to(self.dtype) | ||
| # Patch grid dimensions are fixed for the entire denoising loop. | ||
| nf = latents.shape[2] | ||
| nh = latents.shape[3] // ph | ||
| nw = latents.shape[4] // pw | ||
| start = time.time() | ||
| for i, t in enumerate(timesteps): | ||
| t_tensor = torch.full((latents.shape[0], nf * nh * nw), float(t), device=latents.device) | ||
|
|
||
| pred_noise = self.transformer( | ||
| hidden_states=latents, | ||
| timestep=t_tensor / self.NUM_TRAIN_TIMESTEPS, | ||
| encoder_hidden_states=prompt_embeds, | ||
| ) | ||
|
|
||
| sigma = t / self.NUM_TRAIN_TIMESTEPS | ||
| pred_video = latents - sigma * pred_noise # clean-video estimate (x0) | ||
|
|
||
| if i < num_steps - 1: | ||
| sigma_next = timesteps[i + 1] / self.NUM_TRAIN_TIMESTEPS | ||
| noise = randn_tensor( | ||
| latents.shape, | ||
| generator=generator, | ||
| device=latents.device, | ||
| dtype=self.dtype, | ||
| ) | ||
|
Kambili marked this conversation as resolved.
|
||
| latents = (1.0 - sigma_next) * pred_video + sigma_next * noise | ||
| else: | ||
| latents = pred_video | ||
|
|
||
| if self.rank == 0: | ||
| logger.info(f"DMD step {i + 1}/{num_steps} (t={t}, sigma={sigma:.3f})") | ||
|
|
||
| if self.rank == 0: | ||
| logger.info(f"DMD denoising done in {time.time() - start:.2f}s") | ||
| return latents | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,16 @@ | ||
| { | ||
| "video": "fastwan_lpips_golden_video.mp4", | ||
| "model": "FastWan2.2-TI2V-5B-FullAttn-Diffusers", | ||
| "source": "FastVideo VideoGenerator (trusted reference)", | ||
| "prompt": "A red fox walking through snow", | ||
| "negative_prompt": "", | ||
| "height": 704, | ||
| "width": 1280, | ||
| "num_frames": 121, | ||
| "num_inference_steps": 3, | ||
| "guidance_scale": 1.0, | ||
| "seed": 42, | ||
| "frame_rate": 24.0, | ||
| "lpips_net": "alex", | ||
| "lpips_threshold": 0.05 | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.