Spaces:
Running on Zero
Running on Zero
| """TAEHV-based Wan VAE wrapper used by offline inference with --use_taehv.""" | |
| from __future__ import annotations | |
| import logging | |
| import os | |
| import urllib.request | |
| from collections import namedtuple | |
| from pathlib import Path | |
| from types import SimpleNamespace | |
| import torch | |
| import torch.nn as nn | |
| import torch.nn.functional as F | |
| from models.model_interface import VAEInterface | |
| from models.wan.wan_wrapper import WanVAEWrapper | |
| DecoderResult = namedtuple("DecoderResult", ("frame", "memory")) | |
| TWorkItem = namedtuple("TWorkItem", ("input_tensor", "block_index")) | |
| LOGGER = logging.getLogger(__name__) | |
| try: | |
| import tensorrt as trt | |
| except ImportError: | |
| trt = None | |
| def _resolve_project_root() -> Path: | |
| env_root = os.environ.get("STREAMDIFFUSIONV2_ROOT") | |
| if env_root: | |
| return Path(env_root).expanduser().resolve() | |
| repo_root = Path(__file__).resolve().parents[2] | |
| if (repo_root / "ckpts").exists(): | |
| return repo_root | |
| cwd = Path.cwd().resolve() | |
| if (cwd / "ckpts").exists(): | |
| return cwd | |
| return repo_root | |
| PROJECT_ROOT = _resolve_project_root() | |
| DEFAULT_TAEHV_CHECKPOINT = str(PROJECT_ROOT / "ckpts" / "taew2_1.pth") | |
| DEFAULT_TAEHV_URL = "https://github.com/madebyollin/taehv/raw/main/taew2_1.pth" | |
| def conv(n_in, n_out, **kwargs): | |
| return nn.Conv2d(n_in, n_out, 3, padding=1, **kwargs) | |
| class Clamp(nn.Module): | |
| def forward(self, x): | |
| return torch.tanh(x / 3) * 3 | |
| class MemBlock(nn.Module): | |
| def __init__(self, n_in, n_out): | |
| super().__init__() | |
| self.conv = nn.Sequential( | |
| conv(n_in * 2, n_out), | |
| nn.ReLU(inplace=True), | |
| conv(n_out, n_out), | |
| nn.ReLU(inplace=True), | |
| conv(n_out, n_out), | |
| ) | |
| self.skip = nn.Conv2d(n_in, n_out, 1, bias=False) if n_in != n_out else nn.Identity() | |
| self.act = nn.ReLU(inplace=True) | |
| def forward(self, x, past): | |
| return self.act(self.conv(torch.cat([x, past], 1)) + self.skip(x)) | |
| class TPool(nn.Module): | |
| def __init__(self, n_f, stride): | |
| super().__init__() | |
| self.stride = stride | |
| self.conv = nn.Conv2d(n_f * stride, n_f, 1, bias=False) | |
| def forward(self, x): | |
| _nt, c, h, w = x.shape | |
| return self.conv(x.reshape(-1, self.stride * c, h, w)) | |
| class TGrow(nn.Module): | |
| def __init__(self, n_f, stride): | |
| super().__init__() | |
| self.stride = stride | |
| self.conv = nn.Conv2d(n_f, n_f * stride, 1, bias=False) | |
| def forward(self, x): | |
| _nt, c, h, w = x.shape | |
| x = self.conv(x) | |
| return x.reshape(-1, c, h, w) | |
| def apply_model_with_memblocks(model, x, parallel, show_progress_bar): | |
| assert x.ndim == 5, f"TAEHV operates on NTCHW tensors, but got {x.ndim}-dim tensor" | |
| n, t, c, h, w = x.shape | |
| if parallel: | |
| x = x.reshape(n * t, c, h, w) | |
| for block in model: | |
| if isinstance(block, MemBlock): | |
| nt, c, h, w = x.shape | |
| t = nt // n | |
| _x = x.reshape(n, t, c, h, w) | |
| mem = F.pad(_x, (0, 0, 0, 0, 0, 0, 1, 0), value=0)[:, :t].reshape(x.shape) | |
| x = block(x, mem) | |
| else: | |
| x = block(x) | |
| nt, c, h, w = x.shape | |
| t = nt // n | |
| x = x.view(n, t, c, h, w) | |
| else: | |
| out = [] | |
| work_queue = [TWorkItem(xt, 0) for xt in x.reshape(n, t * c, h, w).chunk(t, dim=1)] | |
| mem = [None] * len(model) | |
| while work_queue: | |
| xt, block_index = work_queue.pop(0) | |
| if block_index == len(model): | |
| out.append(xt) | |
| continue | |
| block = model[block_index] | |
| if isinstance(block, MemBlock): | |
| if mem[block_index] is None: | |
| xt_new = block(xt, xt * 0) | |
| mem[block_index] = xt | |
| else: | |
| xt_new = block(xt, mem[block_index]) | |
| mem[block_index].copy_(xt) | |
| work_queue.insert(0, TWorkItem(xt_new, block_index + 1)) | |
| elif isinstance(block, TPool): | |
| if mem[block_index] is None: | |
| mem[block_index] = [] | |
| mem[block_index].append(xt) | |
| if len(mem[block_index]) == block.stride: | |
| n, c, h, w = xt.shape | |
| xt = block(torch.cat(mem[block_index], 1).view(n * block.stride, c, h, w)) | |
| mem[block_index] = [] | |
| work_queue.insert(0, TWorkItem(xt, block_index + 1)) | |
| elif isinstance(block, TGrow): | |
| xt = block(xt) | |
| n_out, c_out, h_out, w_out = xt.shape | |
| batch_size = n_out // block.stride | |
| grown = xt.view(batch_size, block.stride * c_out, h_out, w_out) | |
| for xt_next in reversed(grown.chunk(block.stride, dim=1)): | |
| work_queue.insert(0, TWorkItem(xt_next, block_index + 1)) | |
| else: | |
| xt = block(xt) | |
| work_queue.insert(0, TWorkItem(xt, block_index + 1)) | |
| x = torch.stack(out, 1) | |
| return x | |
| class TAEHV(nn.Module): | |
| latent_channels = 16 | |
| image_channels = 3 | |
| def __init__(self, checkpoint_path=DEFAULT_TAEHV_CHECKPOINT, decoder_time_upscale=(True, True), decoder_space_upscale=(True, True, True)): | |
| super().__init__() | |
| self.encoder = nn.Sequential( | |
| conv(TAEHV.image_channels, 64), | |
| nn.ReLU(inplace=True), | |
| TPool(64, 2), | |
| conv(64, 64, stride=2, bias=False), | |
| MemBlock(64, 64), | |
| MemBlock(64, 64), | |
| MemBlock(64, 64), | |
| TPool(64, 2), | |
| conv(64, 64, stride=2, bias=False), | |
| MemBlock(64, 64), | |
| MemBlock(64, 64), | |
| MemBlock(64, 64), | |
| TPool(64, 1), | |
| conv(64, 64, stride=2, bias=False), | |
| MemBlock(64, 64), | |
| MemBlock(64, 64), | |
| MemBlock(64, 64), | |
| conv(64, TAEHV.latent_channels), | |
| ) | |
| n_f = [256, 128, 64, 64] | |
| self.frames_to_trim = 2 ** sum(decoder_time_upscale) - 1 | |
| self.decoder = nn.Sequential( | |
| Clamp(), | |
| conv(TAEHV.latent_channels, n_f[0]), | |
| nn.ReLU(inplace=True), | |
| MemBlock(n_f[0], n_f[0]), | |
| MemBlock(n_f[0], n_f[0]), | |
| MemBlock(n_f[0], n_f[0]), | |
| nn.Upsample(scale_factor=2 if decoder_space_upscale[0] else 1), | |
| TGrow(n_f[0], 1), | |
| conv(n_f[0], n_f[1], bias=False), | |
| MemBlock(n_f[1], n_f[1]), | |
| MemBlock(n_f[1], n_f[1]), | |
| MemBlock(n_f[1], n_f[1]), | |
| nn.Upsample(scale_factor=2 if decoder_space_upscale[1] else 1), | |
| TGrow(n_f[1], 2 if decoder_time_upscale[0] else 1), | |
| conv(n_f[1], n_f[2], bias=False), | |
| MemBlock(n_f[2], n_f[2]), | |
| MemBlock(n_f[2], n_f[2]), | |
| MemBlock(n_f[2], n_f[2]), | |
| nn.Upsample(scale_factor=2 if decoder_space_upscale[2] else 1), | |
| TGrow(n_f[2], 2 if decoder_time_upscale[1] else 1), | |
| conv(n_f[2], n_f[3], bias=False), | |
| nn.ReLU(inplace=True), | |
| conv(n_f[3], TAEHV.image_channels), | |
| ) | |
| self.load_state_dict(self.patch_tgrow_layers(torch.load(checkpoint_path, map_location="cpu", weights_only=True))) | |
| def patch_tgrow_layers(self, state_dict): | |
| new_state_dict = self.state_dict() | |
| for index, layer in enumerate(self.decoder): | |
| if isinstance(layer, TGrow): | |
| key = f"decoder.{index}.conv.weight" | |
| if state_dict[key].shape[0] > new_state_dict[key].shape[0]: | |
| state_dict[key] = state_dict[key][-new_state_dict[key].shape[0]:] | |
| return state_dict | |
| def encode_video(self, x, parallel=True, show_progress_bar=False): | |
| return apply_model_with_memblocks(self.encoder, x, parallel, show_progress_bar) | |
| def decode_video(self, x, parallel=True, show_progress_bar=False): | |
| return apply_model_with_memblocks(self.decoder, x, parallel, show_progress_bar) | |
| class TAEHVParallelDecoderModule(nn.Module): | |
| """Shape-specialized decoder graph that is friendly to ONNX/TensorRT export.""" | |
| def __init__(self, decoder: nn.Module): | |
| super().__init__() | |
| self.decoder = decoder | |
| def forward(self, latent: torch.Tensor) -> torch.Tensor: | |
| return apply_model_with_memblocks( | |
| self.decoder, | |
| latent, | |
| parallel=True, | |
| show_progress_bar=False, | |
| ) | |
| class TAEHVTensorRTDecoder: | |
| """Cache TensorRT engines for the common fixed TAEHV decoder shapes.""" | |
| def __init__( | |
| self, | |
| decoder_module: nn.Module, | |
| cache_dir: str | Path, | |
| workspace_bytes: int, | |
| ) -> None: | |
| if trt is None: | |
| raise ImportError("TensorRT is not installed, but TensorRT decode was requested.") | |
| self.decoder_module = decoder_module.eval() | |
| self.cache_dir = Path(cache_dir) | |
| self.cache_dir.mkdir(parents=True, exist_ok=True) | |
| self.workspace_bytes = int(workspace_bytes) | |
| self.logger = trt.Logger(trt.Logger.WARNING) | |
| self.runtime = trt.Runtime(self.logger) | |
| self._engine_cache: dict[tuple[int, ...], tuple[object, object, str, str]] = {} | |
| self._stream_cache: dict[int, torch.cuda.Stream] = {} | |
| def _shape_tag(self, shape: tuple[int, ...]) -> str: | |
| return "x".join(str(dim) for dim in shape) | |
| def _engine_path(self, shape: tuple[int, ...]) -> Path: | |
| return self.cache_dir / f"taehv_decoder_trt_{self._shape_tag(shape)}.plan" | |
| def _onnx_path(self, shape: tuple[int, ...]) -> Path: | |
| return self.cache_dir / f"taehv_decoder_trt_{self._shape_tag(shape)}.onnx" | |
| def _build_engine(self, shape: tuple[int, ...], device: torch.device) -> bytes: | |
| onnx_path = self._onnx_path(shape) | |
| engine_path = self._engine_path(shape) | |
| sample = torch.randn(shape, device=device, dtype=torch.float16) | |
| with torch.no_grad(): | |
| torch.onnx.export( | |
| self.decoder_module, | |
| (sample,), | |
| onnx_path.as_posix(), | |
| input_names=["latent"], | |
| output_names=["video"], | |
| opset_version=18, | |
| do_constant_folding=True, | |
| ) | |
| builder = trt.Builder(self.logger) | |
| network = builder.create_network( | |
| 1 << int(trt.NetworkDefinitionCreationFlag.EXPLICIT_BATCH) | |
| ) | |
| parser = trt.OnnxParser(network, self.logger) | |
| with open(onnx_path, "rb") as handle: | |
| if not parser.parse(handle.read()): | |
| errors = "\n".join(str(parser.get_error(i)) for i in range(parser.num_errors)) | |
| raise RuntimeError(f"Failed to parse ONNX for TAEHV TensorRT engine:\n{errors}") | |
| config = builder.create_builder_config() | |
| config.set_memory_pool_limit(trt.MemoryPoolType.WORKSPACE, self.workspace_bytes) | |
| config.set_flag(trt.BuilderFlag.FP16) | |
| serialized = builder.build_serialized_network(network, config) | |
| if serialized is None: | |
| raise RuntimeError("TensorRT failed to build the TAEHV decoder engine") | |
| engine_bytes = bytes(serialized) | |
| engine_path.write_bytes(engine_bytes) | |
| return engine_bytes | |
| def _load_engine(self, shape: tuple[int, ...], device: torch.device): | |
| cached = self._engine_cache.get(shape) | |
| if cached is not None: | |
| return cached | |
| engine_path = self._engine_path(shape) | |
| if engine_path.exists(): | |
| engine_bytes = engine_path.read_bytes() | |
| else: | |
| LOGGER.info("Building TensorRT engine for TAEHV decoder shape %s", shape) | |
| engine_bytes = self._build_engine(shape, device) | |
| engine = self.runtime.deserialize_cuda_engine(engine_bytes) | |
| if engine is None: | |
| raise RuntimeError(f"Failed to deserialize TensorRT engine for TAEHV shape {shape}") | |
| context = engine.create_execution_context() | |
| input_name = "" | |
| output_name = "" | |
| for index in range(engine.num_io_tensors): | |
| name = engine.get_tensor_name(index) | |
| mode = engine.get_tensor_mode(name) | |
| if mode == trt.TensorIOMode.INPUT: | |
| input_name = name | |
| elif mode == trt.TensorIOMode.OUTPUT: | |
| output_name = name | |
| if not input_name or not output_name: | |
| raise RuntimeError("Failed to discover TensorRT decoder tensor names") | |
| cached = (engine, context, input_name, output_name) | |
| self._engine_cache[shape] = cached | |
| return cached | |
| def decode(self, latent: torch.Tensor) -> torch.Tensor: | |
| latent = latent.contiguous().to(dtype=torch.float16) | |
| shape = tuple(int(dim) for dim in latent.shape) | |
| _engine, context, input_name, output_name = self._load_engine(shape, latent.device) | |
| if not context.set_input_shape(input_name, shape): | |
| raise RuntimeError(f"TensorRT rejected decoder input shape {shape}") | |
| output_shape = tuple(int(dim) for dim in context.get_tensor_shape(output_name)) | |
| output = torch.empty(output_shape, device=latent.device, dtype=latent.dtype) | |
| context.set_tensor_address(input_name, int(latent.data_ptr())) | |
| context.set_tensor_address(output_name, int(output.data_ptr())) | |
| device_index = latent.device.index | |
| if device_index is None: | |
| raise RuntimeError("TensorRT decode requires an explicit CUDA device index") | |
| stream = self._stream_cache.get(device_index) | |
| if stream is None: | |
| stream = torch.cuda.Stream(device=latent.device) | |
| self._stream_cache[device_index] = stream | |
| current_stream = torch.cuda.current_stream(device=latent.device) | |
| stream.wait_stream(current_stream) | |
| ok = context.execute_async_v3(stream.cuda_stream) | |
| if not ok: | |
| raise RuntimeError("TensorRT execution failed for the TAEHV decoder") | |
| current_stream.wait_stream(stream) | |
| return output | |
| class TAEHVWanVAEWrapper(VAEInterface): | |
| """Wan stream encoder with a TAEHV decoder for faster pixel reconstruction.""" | |
| def __init__( | |
| self, | |
| model_type: str = "T2V-1.3B", | |
| checkpoint_path: str | None = None, | |
| auto_download: bool = True, | |
| parallel_decode: bool = False, | |
| use_tensorrt: bool = False, | |
| tensorrt_cache_dir: str | None = None, | |
| tensorrt_workspace_bytes: int = 4 << 30, | |
| ): | |
| super().__init__() | |
| self.checkpoint_path = checkpoint_path or DEFAULT_TAEHV_CHECKPOINT | |
| self.use_tensorrt = use_tensorrt | |
| self.parallel_decode = parallel_decode or use_tensorrt | |
| self.model = SimpleNamespace(first_encode=True, first_decode=True) | |
| self.decode_context_latents = 3 | |
| self._decode_latent_cache: torch.Tensor | None = None | |
| self.encoder_vae = WanVAEWrapper(model_type=model_type) | |
| self.taehv = TAEHV(checkpoint_path=self._resolve_checkpoint(auto_download)) | |
| self._tensorrt_cache_dir = tensorrt_cache_dir or (PROJECT_ROOT / "ckpts" / "taehv_trt") | |
| self._tensorrt_workspace_bytes = int(tensorrt_workspace_bytes) | |
| self._tensorrt_decoder: TAEHVTensorRTDecoder | None = None | |
| self._tensorrt_failed = False | |
| def _resolve_checkpoint(self, auto_download: bool) -> str: | |
| if os.path.exists(self.checkpoint_path): | |
| return self.checkpoint_path | |
| if not auto_download: | |
| raise FileNotFoundError(f"TAEHV checkpoint not found: {self.checkpoint_path}") | |
| os.makedirs(os.path.dirname(self.checkpoint_path), exist_ok=True) | |
| urllib.request.urlretrieve(DEFAULT_TAEHV_URL, self.checkpoint_path) | |
| return self.checkpoint_path | |
| def to(self, *args, **kwargs): | |
| device = kwargs.get("device") | |
| dtype = kwargs.get("dtype") | |
| if args: | |
| if len(args) >= 1 and not isinstance(args[0], torch.dtype): | |
| device = args[0] | |
| if len(args) >= 2 and isinstance(args[1], torch.dtype): | |
| dtype = args[1] | |
| elif len(args) == 1 and isinstance(args[0], torch.dtype): | |
| dtype = args[0] | |
| encoder_kwargs = {} | |
| if device is not None: | |
| encoder_kwargs["device"] = device | |
| if dtype is not None: | |
| encoder_kwargs["dtype"] = dtype | |
| self.encoder_vae.to(**encoder_kwargs) | |
| taehv_kwargs = {"dtype": torch.float16} | |
| if device is not None: | |
| taehv_kwargs["device"] = device | |
| self.taehv.to(**taehv_kwargs) | |
| if self.use_tensorrt and not self._tensorrt_failed: | |
| self._tensorrt_decoder = TAEHVTensorRTDecoder( | |
| decoder_module=TAEHVParallelDecoderModule(self.taehv.decoder).to(**taehv_kwargs).eval(), | |
| cache_dir=self._tensorrt_cache_dir, | |
| workspace_bytes=self._tensorrt_workspace_bytes, | |
| ) | |
| return self | |
| def _pixels_to_unit_range(self, video: torch.Tensor) -> torch.Tensor: | |
| return (video * 0.5 + 0.5).clamp(0, 1) | |
| def _unit_range_to_pixels(self, video: torch.Tensor) -> torch.Tensor: | |
| return video.mul(2).sub(1).clamp(-1, 1) | |
| def _to_ntchw(self, video: torch.Tensor) -> torch.Tensor: | |
| return video.permute(0, 2, 1, 3, 4).contiguous() | |
| def _to_ncthw(self, video: torch.Tensor) -> torch.Tensor: | |
| return video.permute(0, 2, 1, 3, 4).contiguous() | |
| def _decode_video(self, latent: torch.Tensor) -> torch.Tensor: | |
| if self.use_tensorrt and self._tensorrt_decoder is not None and not self._tensorrt_failed: | |
| try: | |
| return self._tensorrt_decoder.decode(latent) | |
| except Exception as exc: | |
| self._tensorrt_failed = True | |
| self._tensorrt_decoder = None | |
| LOGGER.warning( | |
| "TAEHV TensorRT decode failed for shape %s, falling back to PyTorch parallel decode: %s", | |
| tuple(latent.shape), | |
| exc, | |
| ) | |
| return self.taehv.decode_video( | |
| latent, | |
| parallel=self.parallel_decode, | |
| show_progress_bar=False, | |
| ) | |
| def decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor: | |
| video = self._decode_video(latent) | |
| return self._unit_range_to_pixels(video) | |
| def decode(self, latent: torch.Tensor) -> torch.Tensor: | |
| return self.decode_to_pixel(latent) | |
| def stream_encode(self, video: torch.Tensor, is_scale=False) -> torch.Tensor: | |
| self.encoder_vae.model.first_encode = self.model.first_encode | |
| latent = self.encoder_vae.stream_encode(video, is_scale=is_scale) | |
| self.model.first_encode = self.encoder_vae.model.first_encode | |
| return latent | |
| def stream_decode_to_pixel(self, latent: torch.Tensor) -> torch.Tensor: | |
| model_dtype = next(self.taehv.parameters()).dtype | |
| latent = latent.to(dtype=model_dtype) | |
| # Match Self-Forcing's TAEHV usage: keep a short latent prefix so each | |
| # incremental decode sees recent temporal context, then trim the | |
| # already-emitted frames from the pixel output. | |
| if self.model.first_decode: | |
| self.model.first_decode = False | |
| self._decode_latent_cache = None | |
| decode_latent = latent | |
| emitted_latents = max(latent.shape[1] - 1, 0) | |
| else: | |
| context = self._decode_latent_cache | |
| decode_latent = latent if context is None else torch.cat([context.to(device=latent.device, dtype=model_dtype), latent], dim=1) | |
| emitted_latents = latent.shape[1] | |
| self._decode_latent_cache = decode_latent[:, -min(self.decode_context_latents, decode_latent.shape[1]):].detach().clone() | |
| video = self._decode_video(decode_latent) | |
| if emitted_latents: | |
| video = video[:, -emitted_latents * 4:, :, :, :] | |
| else: | |
| video = video[:, 0:0, :, :, :] | |
| return self._unit_range_to_pixels(video).to(dtype=latent.dtype) | |