# SPDX-License-Identifier: Apache-2.0 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project """Chunked Wan decode with a preallocated output buffer.""" from __future__ import annotations import torch from diffusers.models.autoencoders.autoencoder_kl_wan import unpatchify def decode_frames(vae, z: torch.Tensor) -> torch.Tensor: """``AutoencoderKLWan._decode``'s frame loop, writing every chunk straight into the result. Upstream grows the result with ``torch.cat([out, out_], 2)`` on every chunk (quadratic copying) and then materializes ``unpatchify`` and ``clamp`` as two more full-size copies, so three or four copies of the decoded video are live at once. Here the final ``[B, C, T, H, W]`` buffer is allocated after the first chunk and each chunk is unpatchified and clamped directly into its slot. Every step is a permutation or an elementwise clamp, so the values are identical to upstream. Tiling dispatch is the caller's responsibility. """ vae.clear_cache() x = vae.post_quant_conv(z) patch_size = vae.config.patch_size num_frames = x.shape[2] # Each temporal upsampler doubles the frames of every chunk after the first # (the first chunk drops the extra leading frames), so steady-state chunks # produce ``2 ** num_temporal_upsamplers`` frames each. frames_per_chunk = 2 ** sum(bool(flag) for flag in vae.decoder.temperal_upsample) output: torch.Tensor | None = None offset = 0 for index in range(num_frames): vae._conv_idx = [0] chunk = vae.decoder( x[:, :, index : index + 1], feat_cache=vae._feat_map, feat_idx=vae._conv_idx, first_chunk=index == 0, ) if patch_size is not None: chunk = unpatchify(chunk, patch_size=patch_size) if output is None: total_frames = chunk.shape[2] + (num_frames - 1) * frames_per_chunk output = chunk.new_empty((chunk.shape[0], chunk.shape[1], total_frames, chunk.shape[3], chunk.shape[4])) end = offset + chunk.shape[2] if end > output.shape[2]: raise RuntimeError( f"Wan decoder produced more frames than expected: chunk {index} ends at {end}, " f"allocated {output.shape[2]}" ) torch.clamp(chunk, min=-1.0, max=1.0, out=output[:, :, offset:end]) offset = end if output is None or offset != output.shape[2]: expected = None if output is None else output.shape[2] raise RuntimeError(f"Wan decoder produced {offset} frames, expected {expected}") vae.clear_cache() return output __all__ = ["decode_frames"]