| from typing import List, Optional |
| import torch |
|
|
| from model.predictor_v4 import SelfForcingPredictorV4 |
| from predictor_training.rollout_cache import ( |
| build_predictor_workspace, |
| reset_predictor_workspace, |
| ) |
| from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper |
|
|
| from demo_utils.memory import gpu, get_cuda_free_memory_gb, DynamicSwapInstaller, move_model_to_device_with_memory_preservation |
|
|
|
|
| def velocity_reuse_steps( |
| schedule: str, |
| temporal_block_index: int, |
| ) -> tuple[int, ...]: |
| """Return denoising steps that reuse step-0 velocity for one chunk.""" |
| schedule = str(schedule).lower() |
| if schedule not in {"frrf", "frrr"}: |
| raise ValueError(f"Unknown velocity-reuse schedule {schedule!r}") |
| if int(temporal_block_index) == 0: |
| return () |
| return (1, 2, 3) if schedule == "frrr" else (1, 2) |
|
|
|
|
| class CausalInferencePipeline(torch.nn.Module): |
| def __init__( |
| self, |
| args, |
| device, |
| generator=None, |
| text_encoder=None, |
| vae=None |
| ): |
| super().__init__() |
| |
| self.generator = WanDiffusionWrapper( |
| **getattr(args, "model_kwargs", {}), is_causal=True) if generator is None else generator |
| self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder |
| self.vae = WanVAEWrapper() if vae is None else vae |
|
|
| |
| self.scheduler = self.generator.get_scheduler() |
| self.denoising_step_list = torch.tensor( |
| args.denoising_step_list, dtype=torch.long) |
| if args.warp_denoising_step: |
| timesteps = torch.cat((self.scheduler.timesteps.cpu(), torch.tensor([0], dtype=torch.float32))) |
| self.denoising_step_list = timesteps[1000 - self.denoising_step_list] |
|
|
| self.num_transformer_blocks = 30 |
| self.frame_seq_length = 1560 |
|
|
| self.kv_cache1 = None |
| self.predictor_v4: SelfForcingPredictorV4 | None = None |
| self.predictor_schedule = "fppf" |
| self.args = args |
| self.num_frame_per_block = getattr(args, "num_frame_per_block", 1) |
| self.independent_first_frame = args.independent_first_frame |
| self.local_attn_size = self.generator.model.local_attn_size |
| self.reuse_first_step_velocity = bool( |
| getattr(args, "reuse_first_step_velocity", False) |
| ) |
| self.reuse_first_step_velocity_schedule = str( |
| getattr(args, "reuse_first_step_velocity_schedule", "frrf") |
| ).lower() |
| if self.reuse_first_step_velocity and len(self.denoising_step_list) != 4: |
| raise ValueError( |
| "reuse_first_step_velocity requires exactly four denoising steps" |
| ) |
| if self.reuse_first_step_velocity_schedule not in {"frrf", "frrr"}: |
| raise ValueError( |
| "reuse_first_step_velocity_schedule must be frrf or frrr, got " |
| f"{self.reuse_first_step_velocity_schedule!r}" |
| ) |
|
|
| print(f"KV inference with {self.num_frame_per_block} frames per block") |
| if self.reuse_first_step_velocity: |
| print( |
| "Denoising schedule: chunk 0 F-F-F-F; later chunks " |
| f"{self.reuse_first_step_velocity_schedule.upper()} " |
| "(R reuses step 0 velocity)" |
| ) |
|
|
| if self.num_frame_per_block > 1: |
| self.generator.model.num_frame_per_block = self.num_frame_per_block |
|
|
| def enable_predictor_v4( |
| self, |
| predictor: SelfForcingPredictorV4, |
| *, |
| schedule: str = "fppf", |
| ) -> None: |
| """Enable F-P-P-F or F-P-P-P after the first all-Full chunk.""" |
| if self.reuse_first_step_velocity: |
| raise ValueError("Predictor F-P-P-F and velocity F-R-R-F are mutually exclusive") |
| if len(self.denoising_step_list) != 4: |
| raise ValueError("Predictor F-P-P-F requires exactly four denoising steps") |
| if self.independent_first_frame: |
| raise NotImplementedError( |
| "Predictor-v4 currently supports the regular three-frame T2V chunks" |
| ) |
| if self.num_frame_per_block != 3: |
| raise ValueError("Predictor-v4 was trained for three latent frames per chunk") |
| schedule = str(schedule).lower() |
| if schedule not in {"fppf", "fppp"}: |
| raise ValueError(f"Unknown Predictor schedule {schedule!r}") |
| self.predictor_v4 = predictor.eval() |
| self.predictor_schedule = schedule |
| print( |
| "Denoising schedule: chunk 0 F-F-F-F; later chunks " |
| f"{schedule.upper()} " |
| f"(Predictor blocks {predictor.source_block_ids})" |
| ) |
|
|
| def _full_step_with_optional_hidden( |
| self, |
| *, |
| noisy_input: torch.Tensor, |
| conditional_dict: dict, |
| timestep: torch.Tensor, |
| current_start: int, |
| capture_hidden: bool, |
| ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor | None]: |
| """Run the loaded generator and optionally capture its pre-head hidden.""" |
| captured: list[torch.Tensor] = [] |
| handle = None |
| if capture_hidden: |
| def capture_head_input(module, args): |
| if not args or not torch.is_tensor(args[0]): |
| raise RuntimeError("Wan head hook did not receive final hidden states") |
| captured.append(args[0]) |
|
|
| handle = self.generator.model.head.register_forward_pre_hook( |
| capture_head_input |
| ) |
| try: |
| flow_pred, denoised_pred = self.generator( |
| noisy_image_or_video=noisy_input, |
| conditional_dict=conditional_dict, |
| timestep=timestep, |
| kv_cache=self.kv_cache1, |
| crossattn_cache=self.crossattn_cache, |
| current_start=current_start, |
| ) |
| finally: |
| if handle is not None: |
| handle.remove() |
| if capture_hidden and len(captured) != 1: |
| raise RuntimeError( |
| f"Expected one Wan final hidden capture, received {len(captured)}" |
| ) |
| return flow_pred, denoised_pred, captured[0] if captured else None |
|
|
| def inference( |
| self, |
| noise: torch.Tensor, |
| text_prompts: List[str], |
| initial_latent: Optional[torch.Tensor] = None, |
| return_latents: bool = False, |
| profile: bool = False, |
| low_memory: bool = False, |
| ) -> torch.Tensor: |
| """ |
| Perform inference on the given noise and text prompts. |
| Inputs: |
| noise (torch.Tensor): The input noise tensor of shape |
| (batch_size, num_output_frames, num_channels, height, width). |
| text_prompts (List[str]): The list of text prompts. |
| initial_latent (torch.Tensor): The initial latent tensor of shape |
| (batch_size, num_input_frames, num_channels, height, width). |
| If num_input_frames is 1, perform image to video. |
| If num_input_frames is greater than 1, perform video extension. |
| return_latents (bool): Whether to return the latents. |
| Outputs: |
| video (torch.Tensor): The generated video tensor of shape |
| (batch_size, num_output_frames, num_channels, height, width). |
| It is normalized to be in the range [0, 1]. |
| """ |
| batch_size, num_frames, num_channels, height, width = noise.shape |
| if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None): |
| |
| |
| assert num_frames % self.num_frame_per_block == 0 |
| num_blocks = num_frames // self.num_frame_per_block |
| else: |
| |
| assert (num_frames - 1) % self.num_frame_per_block == 0 |
| num_blocks = (num_frames - 1) // self.num_frame_per_block |
| num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0 |
| num_output_frames = num_frames + num_input_frames |
| conditional_dict = self.text_encoder( |
| text_prompts=text_prompts |
| ) |
|
|
| if low_memory: |
| gpu_memory_preservation = get_cuda_free_memory_gb(gpu) + 5 |
| move_model_to_device_with_memory_preservation(self.text_encoder, target_device=gpu, preserved_memory_gb=gpu_memory_preservation) |
|
|
| output = torch.zeros( |
| [batch_size, num_output_frames, num_channels, height, width], |
| device=noise.device, |
| dtype=noise.dtype |
| ) |
|
|
| |
| if profile: |
| init_start = torch.cuda.Event(enable_timing=True) |
| init_end = torch.cuda.Event(enable_timing=True) |
| diffusion_start = torch.cuda.Event(enable_timing=True) |
| diffusion_end = torch.cuda.Event(enable_timing=True) |
| vae_start = torch.cuda.Event(enable_timing=True) |
| vae_end = torch.cuda.Event(enable_timing=True) |
| block_times = [] |
| block_start = torch.cuda.Event(enable_timing=True) |
| block_end = torch.cuda.Event(enable_timing=True) |
| init_start.record() |
|
|
| |
| if self.kv_cache1 is None: |
| self._initialize_kv_cache( |
| batch_size=batch_size, |
| dtype=noise.dtype, |
| device=noise.device |
| ) |
| self._initialize_crossattn_cache( |
| batch_size=batch_size, |
| dtype=noise.dtype, |
| device=noise.device |
| ) |
| else: |
| |
| for block_index in range(self.num_transformer_blocks): |
| self.crossattn_cache[block_index]["is_init"] = False |
| |
| for block_index in range(len(self.kv_cache1)): |
| self.kv_cache1[block_index]["global_end_index"] = torch.tensor( |
| [0], dtype=torch.long, device=noise.device) |
| self.kv_cache1[block_index]["local_end_index"] = torch.tensor( |
| [0], dtype=torch.long, device=noise.device) |
|
|
| |
| current_start_frame = 0 |
| if initial_latent is not None: |
| timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0 |
| if self.independent_first_frame: |
| |
| assert (num_input_frames - 1) % self.num_frame_per_block == 0 |
| num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block |
| output[:, :1] = initial_latent[:, :1] |
| self.generator( |
| noisy_image_or_video=initial_latent[:, :1], |
| conditional_dict=conditional_dict, |
| timestep=timestep * 0, |
| kv_cache=self.kv_cache1, |
| crossattn_cache=self.crossattn_cache, |
| current_start=current_start_frame * self.frame_seq_length, |
| ) |
| current_start_frame += 1 |
| else: |
| |
| assert num_input_frames % self.num_frame_per_block == 0 |
| num_input_blocks = num_input_frames // self.num_frame_per_block |
|
|
| for _ in range(num_input_blocks): |
| current_ref_latents = \ |
| initial_latent[:, current_start_frame:current_start_frame + self.num_frame_per_block] |
| output[:, current_start_frame:current_start_frame + self.num_frame_per_block] = current_ref_latents |
| self.generator( |
| noisy_image_or_video=current_ref_latents, |
| conditional_dict=conditional_dict, |
| timestep=timestep * 0, |
| kv_cache=self.kv_cache1, |
| crossattn_cache=self.crossattn_cache, |
| current_start=current_start_frame * self.frame_seq_length, |
| ) |
| current_start_frame += self.num_frame_per_block |
|
|
| if profile: |
| init_end.record() |
| torch.cuda.synchronize() |
| diffusion_start.record() |
|
|
| |
| all_num_frames = [self.num_frame_per_block] * num_blocks |
| if self.independent_first_frame and initial_latent is None: |
| all_num_frames = [1] + all_num_frames |
| previous_chunk_hidden: dict[int, torch.Tensor] = {} |
| for temporal_block_index, current_num_frames in enumerate(all_num_frames): |
| if profile: |
| block_start.record() |
|
|
| noisy_input = noise[ |
| :, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames] |
|
|
| |
| first_step_flow_pred = None |
| same_chunk_anchor = None |
| chunk_step_hidden: dict[int, torch.Tensor] = {} |
| predictor_kv_cache = None |
| predictor_steps = ( |
| (1, 2, 3) if self.predictor_schedule == "fppp" else (1, 2) |
| ) |
| if self.predictor_v4 is not None and temporal_block_index > 0: |
| history_tokens = current_start_frame * self.frame_seq_length |
| predictor_kv_cache = build_predictor_workspace( |
| self.kv_cache1, |
| source_block_ids=tuple(self.predictor_v4.source_block_ids), |
| history_tokens=history_tokens, |
| current_tokens=current_num_frames * self.frame_seq_length, |
| ) |
| reuse_step_indices = velocity_reuse_steps( |
| self.reuse_first_step_velocity_schedule, |
| temporal_block_index, |
| ) |
| for index, current_timestep in enumerate(self.denoising_step_list): |
| reuse_velocity = ( |
| self.reuse_first_step_velocity |
| and index in reuse_step_indices |
| ) |
| use_predictor = ( |
| self.predictor_v4 is not None |
| and temporal_block_index > 0 |
| and index in predictor_steps |
| ) |
| if use_predictor: |
| step_mode = "predictor" |
| elif reuse_velocity: |
| step_mode = "reuse" |
| else: |
| step_mode = "full" |
| print(f"current_timestep: {current_timestep} ({step_mode})") |
| |
| timestep = torch.ones( |
| [batch_size, current_num_frames], |
| device=noise.device, |
| dtype=torch.int64) * current_timestep |
|
|
| if use_predictor: |
| reset_predictor_workspace( |
| predictor_kv_cache, |
| history_tokens=current_start_frame * self.frame_seq_length, |
| ) |
| if same_chunk_anchor is None: |
| raise RuntimeError( |
| f"Predictor step {index} has no same-chunk anchor hidden" |
| ) |
| if index not in previous_chunk_hidden: |
| raise RuntimeError( |
| f"Predictor step {index} has no previous-chunk hidden" |
| ) |
| predictor_output = self.predictor_v4( |
| target_latent=noisy_input, |
| target_timestep=timestep, |
| anchor_hidden=same_chunk_anchor, |
| previous_chunk_hidden=previous_chunk_hidden[index], |
| kv_cache=predictor_kv_cache, |
| crossattn_cache=self.crossattn_cache, |
| current_start=current_start_frame * self.frame_seq_length, |
| ) |
| flow_pred = predictor_output["pred_flow"] |
| denoised_pred = self.generator._convert_flow_pred_to_x0( |
| flow_pred=flow_pred.flatten(0, 1), |
| xt=noisy_input.flatten(0, 1), |
| timestep=timestep.flatten(0, 1), |
| ).unflatten(0, flow_pred.shape[:2]) |
| same_chunk_anchor = predictor_output["pred_hidden"] |
| chunk_step_hidden[index] = same_chunk_anchor |
| elif reuse_velocity: |
| if first_step_flow_pred is None: |
| raise RuntimeError( |
| "Missing step-0 velocity for F-R-R-F denoising" |
| ) |
| denoised_pred = self.generator._convert_flow_pred_to_x0( |
| flow_pred=first_step_flow_pred.flatten(0, 1), |
| xt=noisy_input.flatten(0, 1), |
| timestep=timestep.flatten(0, 1), |
| ).unflatten(0, first_step_flow_pred.shape[:2]) |
| else: |
| capture_hidden = ( |
| self.predictor_v4 is not None |
| and ( |
| index == 0 |
| or ( |
| temporal_block_index == 0 |
| and index in predictor_steps |
| ) |
| ) |
| ) |
| flow_pred, denoised_pred, final_hidden = ( |
| self._full_step_with_optional_hidden( |
| noisy_input=noisy_input, |
| conditional_dict=conditional_dict, |
| timestep=timestep, |
| current_start=current_start_frame |
| * self.frame_seq_length, |
| capture_hidden=capture_hidden, |
| ) |
| ) |
| if final_hidden is not None: |
| chunk_step_hidden[index] = final_hidden |
| if index == 0: |
| same_chunk_anchor = final_hidden |
| if index == 0 and self.reuse_first_step_velocity: |
| first_step_flow_pred = flow_pred.detach() |
|
|
| if index < len(self.denoising_step_list) - 1: |
| next_timestep = self.denoising_step_list[index + 1] |
| noisy_input = self.scheduler.add_noise( |
| denoised_pred.flatten(0, 1), |
| torch.randn_like(denoised_pred.flatten(0, 1)), |
| next_timestep * torch.ones( |
| [batch_size * current_num_frames], device=noise.device, dtype=torch.long) |
| ).unflatten(0, denoised_pred.shape[:2]) |
|
|
| |
| output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred |
| if self.predictor_v4 is not None: |
| missing = set(predictor_steps).difference(chunk_step_hidden) |
| if missing: |
| raise RuntimeError( |
| f"Temporal chunk {temporal_block_index} lacks hidden steps " |
| f"{sorted(missing)}" |
| ) |
| previous_chunk_hidden = { |
| step: chunk_step_hidden[step].detach() |
| for step in predictor_steps |
| } |
|
|
| |
| context_timestep = torch.ones_like(timestep) * self.args.context_noise |
| self.generator( |
| noisy_image_or_video=denoised_pred, |
| conditional_dict=conditional_dict, |
| timestep=context_timestep, |
| kv_cache=self.kv_cache1, |
| crossattn_cache=self.crossattn_cache, |
| current_start=current_start_frame * self.frame_seq_length, |
| ) |
|
|
| if profile: |
| block_end.record() |
| torch.cuda.synchronize() |
| block_time = block_start.elapsed_time(block_end) |
| block_times.append(block_time) |
|
|
| |
| current_start_frame += current_num_frames |
|
|
| if profile: |
| |
| diffusion_end.record() |
| torch.cuda.synchronize() |
| diffusion_time = diffusion_start.elapsed_time(diffusion_end) |
| init_time = init_start.elapsed_time(init_end) |
| vae_start.record() |
|
|
| |
| video = self.vae.decode_to_pixel(output, use_cache=False) |
| video = (video * 0.5 + 0.5).clamp(0, 1) |
|
|
| if profile: |
| |
| vae_end.record() |
| torch.cuda.synchronize() |
| vae_time = vae_start.elapsed_time(vae_end) |
| total_time = init_time + diffusion_time + vae_time |
|
|
| print("Profiling results:") |
| print(f" - Initialization/caching time: {init_time:.2f} ms ({100 * init_time / total_time:.2f}%)") |
| print(f" - Diffusion generation time: {diffusion_time:.2f} ms ({100 * diffusion_time / total_time:.2f}%)") |
| for i, block_time in enumerate(block_times): |
| print(f" - Block {i} generation time: {block_time:.2f} ms ({100 * block_time / diffusion_time:.2f}% of diffusion)") |
| print(f" - VAE decoding time: {vae_time:.2f} ms ({100 * vae_time / total_time:.2f}%)") |
| print(f" - Total time: {total_time:.2f} ms") |
|
|
| if return_latents: |
| return video, output |
| else: |
| return video |
|
|
| def _initialize_kv_cache(self, batch_size, dtype, device): |
| """ |
| Initialize a Per-GPU KV cache for the Wan model. |
| """ |
| kv_cache1 = [] |
| if self.local_attn_size != -1: |
| |
| kv_cache_size = self.local_attn_size * self.frame_seq_length |
| else: |
| |
| kv_cache_size = 32760 |
|
|
| for _ in range(self.num_transformer_blocks): |
| kv_cache1.append({ |
| "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device), |
| "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device), |
| "global_end_index": torch.tensor([0], dtype=torch.long, device=device), |
| "local_end_index": torch.tensor([0], dtype=torch.long, device=device) |
| }) |
|
|
| self.kv_cache1 = kv_cache1 |
|
|
| def _initialize_crossattn_cache(self, batch_size, dtype, device): |
| """ |
| Initialize a Per-GPU cross-attention cache for the Wan model. |
| """ |
| crossattn_cache = [] |
|
|
| for _ in range(self.num_transformer_blocks): |
| crossattn_cache.append({ |
| "k": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device), |
| "v": torch.zeros([batch_size, 512, 12, 128], dtype=dtype, device=device), |
| "is_init": False |
| }) |
| self.crossattn_cache = crossattn_cache |
|
|