Text-to-Video
MLX
Safetensors
lance
multimodal
apple-silicon
text-to-image
image-generation
video-generation
diffusion
flow-matching
Mixture of Experts
qwen2_5_vl
wan
port
Instructions to use RockTalk/Lance-3B-Video-MLX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- MLX
How to use RockTalk/Lance-3B-Video-MLX with MLX:
# Download the model from the Hub pip install huggingface_hub[hf_xet] huggingface-cli download --local-dir Lance-3B-Video-MLX RockTalk/Lance-3B-Video-MLX
- Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- LM Studio
| """MLX port of Wan 2.2 VAE (used as Lance's video VAE). | |
| Source: bytedance/Lance modeling/vae/wan/vae2_2.py | |
| (which mirrors Alibaba Wan 2.2's open-source VAE) | |
| Layout convention: MLX uses (B, T, H, W, C) for Conv3d. PT uses (B, C, T, H, W). | |
| All internal tensors here use the MLX NTHWC layout. Conv weights from the PT | |
| checkpoint must be reshaped (out_C, in_C, kT, kH, kW) -> (out_C, kT, kH, kW, in_C) | |
| at load time — see tools/convert_weights.py. | |
| Caching: the PT model uses a feat_cache list to stream long videos in chunks. | |
| This first MLX port runs in single-pass mode (whole tensor at once), which is | |
| correct for images (T=1) and short videos. Streaming-cache mode is a follow-up. | |
| """ | |
| from __future__ import annotations | |
| from typing import List, Optional | |
| import mlx.core as mx | |
| import mlx.nn as nn | |
| from einops import rearrange | |
| CACHE_T = 2 # matches PT constant — only used when streaming is enabled | |
| # --------------------------------------------------------------------------- | |
| # CausalConv3d — temporally-causal 3D convolution | |
| # --------------------------------------------------------------------------- | |
| # Number of trailing time frames to retain for streaming-cache continuity. | |
| CACHE_T = 2 | |
| def _cache_pre(x: mx.array, feat_cache, feat_idx): | |
| """Streaming-cache helper for CausalConv3d calls. | |
| Returns (prev_cache, new_cache, idx). The caller passes prev_cache to the | |
| conv (as cache_x), then stores new_cache at feat_cache[idx] and bumps | |
| feat_idx. Inlining this avoids repeating the boilerplate at every conv. | |
| """ | |
| if feat_cache is None: | |
| return None, None, None | |
| idx = feat_idx[0] | |
| # last CACHE_T frames on time axis (NTHWC -> axis 1) | |
| cache_x = x[:, -CACHE_T:, ...] | |
| prev = feat_cache[idx] | |
| is_rep = isinstance(prev, str) and prev == "Rep" | |
| has_prev = (prev is not None) and (not is_rep) | |
| if cache_x.shape[1] < 2 and has_prev: | |
| # prepend the last frame of the previous cache so the new cache has T>=2 | |
| cache_x = mx.concatenate([prev[:, -1:, ...], cache_x], axis=1) | |
| feat_idx[0] = idx + 1 | |
| return (prev if has_prev else None), cache_x, idx | |
| def _set_cache(feat_cache, idx, value): | |
| if feat_cache is not None and idx is not None: | |
| feat_cache[idx] = value | |
| class CausalConv3d(nn.Conv3d): | |
| """3D conv with causal padding on the temporal dimension. | |
| Inherits from nn.Conv3d so parameter names (weight, bias) match the PT | |
| checkpoint directly — PT's CausalConv3d also subclasses nn.Conv3d. | |
| PT pads time as (2*pt, 0) so each output frame only depends on current + | |
| previous frames. Spatial padding is symmetric. | |
| """ | |
| def __init__( | |
| self, | |
| in_channels: int, | |
| out_channels: int, | |
| kernel_size, | |
| stride=1, | |
| padding=0, | |
| bias: bool = True, | |
| ): | |
| if isinstance(kernel_size, int): | |
| kernel_size = (kernel_size, kernel_size, kernel_size) | |
| if isinstance(stride, int): | |
| stride = (stride, stride, stride) | |
| if isinstance(padding, int): | |
| padding = (padding, padding, padding) | |
| # Pass padding=0 to base — we do explicit pad before forward. | |
| super().__init__( | |
| in_channels, out_channels, | |
| kernel_size=kernel_size, stride=stride, padding=0, bias=bias, | |
| ) | |
| self._pt, self._ph, self._pw = padding | |
| def __call__(self, x: mx.array, cache_x: Optional[mx.array] = None) -> mx.array: | |
| pt = 2 * self._pt | |
| if cache_x is not None and pt > 0: | |
| x = mx.concatenate([cache_x, x], axis=1) | |
| pt = max(0, pt - cache_x.shape[1]) | |
| if pt > 0 or self._ph > 0 or self._pw > 0: | |
| x = mx.pad( | |
| x, | |
| [(0, 0), (pt, 0), (self._ph, self._ph), (self._pw, self._pw), (0, 0)], | |
| ) | |
| return super().__call__(x) | |
| # --------------------------------------------------------------------------- | |
| # RMS_norm — channel-axis RMS normalization with learnable scale | |
| # --------------------------------------------------------------------------- | |
| class RMS_norm(nn.Module): | |
| """Equivalent to PT version: F.normalize(x, dim=channel) * sqrt(C) * gamma + bias. | |
| In NTHWC layout, channel is always axis -1. The PT version has a | |
| `channel_first` flag and a `images` flag controlling broadcast shape; | |
| we collapse these — gamma is shape (C,) and broadcasts trivially on -1. | |
| """ | |
| def __init__(self, dim: int, bias: bool = False): | |
| super().__init__() | |
| self.scale = dim ** 0.5 | |
| self.gamma = mx.ones((dim,)) | |
| self.bias = mx.zeros((dim,)) if bias else None | |
| def __call__(self, x: mx.array) -> mx.array: | |
| # L2 normalize along channel (-1) | |
| norm = mx.sqrt(mx.sum(x * x, axis=-1, keepdims=True) + 1e-12) | |
| x = x / norm | |
| out = x * self.scale * self.gamma | |
| if self.bias is not None: | |
| out = out + self.bias | |
| return out | |
| # --------------------------------------------------------------------------- | |
| # Resample — 2D/3D up/down sample blocks | |
| # --------------------------------------------------------------------------- | |
| class Resample(nn.Module): | |
| """Spatial (and optional temporal) resampling. | |
| Modes: | |
| none — passthrough | |
| upsample2d — nearest 2x spatial + 3x3 conv2d refine | |
| upsample3d — upsample2d + temporal expand (2x via channel-doubling conv) | |
| downsample2d — zero-pad + stride-2 conv2d | |
| downsample3d — downsample2d + stride-2 temporal conv | |
| First implementation: single-pass only (no feat_cache streaming). | |
| """ | |
| def __init__(self, dim: int, mode: str): | |
| super().__init__() | |
| assert mode in ("none", "upsample2d", "upsample3d", "downsample2d", "downsample3d") | |
| self.dim = dim | |
| self.mode = mode | |
| if mode == "upsample2d": | |
| self.up = nn.Upsample(scale_factor=(2.0, 2.0), mode="nearest") | |
| self.spatial_conv = nn.Conv2d(dim, dim, 3, padding=1) | |
| elif mode == "upsample3d": | |
| self.up = nn.Upsample(scale_factor=(2.0, 2.0), mode="nearest") | |
| self.spatial_conv = nn.Conv2d(dim, dim, 3, padding=1) | |
| # doubles channels — second half becomes the new temporal frames | |
| self.time_conv = CausalConv3d(dim, dim * 2, (3, 1, 1), padding=(1, 0, 0)) | |
| elif mode == "downsample2d": | |
| self.spatial_conv = nn.Conv2d(dim, dim, 3, stride=(2, 2), padding=0) | |
| elif mode == "downsample3d": | |
| self.spatial_conv = nn.Conv2d(dim, dim, 3, stride=(2, 2), padding=0) | |
| self.time_conv = CausalConv3d(dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0)) | |
| def _spatial_pad_zeropad2d(self, x: mx.array) -> mx.array: | |
| # PT ZeroPad2d((0,1,0,1)) -> pad right=1, bottom=1 in (H,W). | |
| # x is (BT, H, W, C). Pad axes 1 (H bottom) and 2 (W right). | |
| return mx.pad(x, [(0, 0), (0, 1), (0, 1), (0, 0)]) | |
| def __call__(self, x: mx.array, feat_cache=None, feat_idx=None, | |
| first_chunk: bool = True) -> mx.array: | |
| """ | |
| x: (B, T, H, W, C) | |
| Single-pass mode (feat_cache is None): controlled by first_chunk — | |
| time_conv is skipped on the first chunk (no prior cache frame). | |
| Streaming mode (feat_cache is a list): each upsample3d / downsample3d | |
| time_conv consumes feat_cache[idx] and updates it. | |
| """ | |
| b, t, h, w, c = x.shape | |
| if self.mode == "upsample3d": | |
| if feat_cache is None: | |
| if not first_chunk: | |
| x = self.time_conv(x) | |
| x = x.reshape(b, t, h, w, 2, c) | |
| x = mx.stack([x[..., 0, :], x[..., 1, :]], axis=2) | |
| x = x.reshape(b, t * 2, h, w, c) | |
| t = t * 2 | |
| else: | |
| idx = feat_idx[0] | |
| if feat_cache[idx] is None: | |
| # First chunk: mark sentinel and bump | |
| feat_cache[idx] = "Rep" | |
| feat_idx[0] = idx + 1 | |
| else: | |
| prev, new_cache, _ = _cache_pre(x, feat_cache, feat_idx) | |
| # Same gotcha as PT: if prev is "Rep" sentinel, pass None to time_conv | |
| is_rep = isinstance(prev, str) and prev == "Rep" | |
| x = self.time_conv(x, cache_x=None if is_rep else prev) | |
| feat_cache[idx] = new_cache | |
| x = x.reshape(b, t, h, w, 2, c) | |
| x = mx.stack([x[..., 0, :], x[..., 1, :]], axis=2) | |
| x = x.reshape(b, t * 2, h, w, c) | |
| t = t * 2 | |
| if self.mode in ("upsample2d", "upsample3d"): | |
| x = rearrange(x, "b t h w c -> (b t) h w c") | |
| x = self.up(x) | |
| x = self.spatial_conv(x) | |
| x = rearrange(x, "(b t) h w c -> b t h w c", b=b, t=t) | |
| elif self.mode in ("downsample2d", "downsample3d"): | |
| x = rearrange(x, "b t h w c -> (b t) h w c") | |
| x = self._spatial_pad_zeropad2d(x) | |
| x = self.spatial_conv(x) | |
| x = rearrange(x, "(b t) h w c -> b t h w c", b=b, t=t) | |
| if self.mode == "downsample3d": | |
| if feat_cache is None: | |
| if not first_chunk: | |
| x = self.time_conv(x) | |
| else: | |
| idx = feat_idx[0] | |
| if feat_cache[idx] is None: | |
| # PT: store current x as the cache, no time_conv yet | |
| feat_cache[idx] = x | |
| feat_idx[0] = idx + 1 | |
| else: | |
| cache_x = x[:, -1:, ...] | |
| # PT: prepend the last frame of the previous cache to x, then time_conv | |
| x = self.time_conv(mx.concatenate([feat_cache[idx][:, -1:, ...], x], axis=1)) | |
| feat_cache[idx] = cache_x | |
| feat_idx[0] = idx + 1 | |
| return x | |
| # --------------------------------------------------------------------------- | |
| # ResidualBlock — 3D ResBlock with RMS + SiLU + CausalConv3d | |
| # --------------------------------------------------------------------------- | |
| class ResidualBlock(nn.Module): | |
| def __init__(self, in_dim: int, out_dim: int, dropout: float = 0.0): | |
| super().__init__() | |
| self.in_dim = in_dim | |
| self.out_dim = out_dim | |
| self.norm1 = RMS_norm(in_dim) | |
| self.conv1 = CausalConv3d(in_dim, out_dim, 3, padding=1) | |
| self.norm2 = RMS_norm(out_dim) | |
| self.conv2 = CausalConv3d(out_dim, out_dim, 3, padding=1) | |
| if in_dim != out_dim: | |
| self.shortcut = CausalConv3d(in_dim, out_dim, 1) | |
| else: | |
| self.shortcut = None | |
| def __call__(self, x: mx.array, feat_cache=None, feat_idx=None) -> mx.array: | |
| h = x if self.shortcut is None else self.shortcut(x) | |
| x = self.norm1(x) | |
| x = nn.silu(x) | |
| prev, new_cache, idx = _cache_pre(x, feat_cache, feat_idx) | |
| x = self.conv1(x, cache_x=prev) | |
| _set_cache(feat_cache, idx, new_cache) | |
| x = self.norm2(x) | |
| x = nn.silu(x) | |
| prev, new_cache, idx = _cache_pre(x, feat_cache, feat_idx) | |
| x = self.conv2(x, cache_x=prev) | |
| _set_cache(feat_cache, idx, new_cache) | |
| return x + h | |
| # --------------------------------------------------------------------------- | |
| # AttentionBlock — single-head spatial self-attention applied per frame | |
| # --------------------------------------------------------------------------- | |
| class AttentionBlock(nn.Module): | |
| def __init__(self, dim: int): | |
| super().__init__() | |
| self.dim = dim | |
| self.norm = RMS_norm(dim) | |
| # PT uses Conv2d 1x1 — same as Linear over channels. Use Conv2d for | |
| # parameter-name compatibility with the checkpoint. | |
| self.to_qkv = nn.Conv2d(dim, dim * 3, 1) | |
| self.proj = nn.Conv2d(dim, dim, 1) | |
| def __call__(self, x: mx.array) -> mx.array: | |
| # x: (B, T, H, W, C) | |
| b, t, h, w, c = x.shape | |
| identity = x | |
| x = rearrange(x, "b t h w c -> (b t) h w c") | |
| x = self.norm(x) | |
| qkv = self.to_qkv(x) # (BT, H, W, 3C) | |
| qkv = qkv.reshape(b * t, h * w, 3, c) | |
| q, k, v = qkv[:, :, 0, :], qkv[:, :, 1, :], qkv[:, :, 2, :] # each (BT, HW, C) | |
| # Add head dim of size 1 for SDPA | |
| q = q[:, None, :, :] # (BT, 1, HW, C) | |
| k = k[:, None, :, :] | |
| v = v[:, None, :, :] | |
| out = mx.fast.scaled_dot_product_attention(q, k, v, scale=c ** -0.5) | |
| out = out[:, 0, :, :] # (BT, HW, C) | |
| out = out.reshape(b * t, h, w, c) | |
| out = self.proj(out) | |
| out = rearrange(out, "(b t) h w c -> b t h w c", b=b, t=t) | |
| return out + identity | |
| # --------------------------------------------------------------------------- | |
| # patchify / unpatchify — 2x spatial pixel-shuffle in/out | |
| # --------------------------------------------------------------------------- | |
| def patchify(x: mx.array, patch_size: int) -> mx.array: | |
| if patch_size == 1: | |
| return x | |
| # NTHWC: (B, T, H, W, C) -> (B, T, H//p, W//p, C*p*p) | |
| if x.ndim == 4: # (B, H, W, C) | |
| return rearrange(x, "b (h q) (w r) c -> b h w (c r q)", q=patch_size, r=patch_size) | |
| elif x.ndim == 5: # (B, T, H, W, C) | |
| return rearrange(x, "b t (h q) (w r) c -> b t h w (c r q)", q=patch_size, r=patch_size) | |
| raise ValueError(f"patchify: invalid ndim {x.ndim}") | |
| def unpatchify(x: mx.array, patch_size: int) -> mx.array: | |
| if patch_size == 1: | |
| return x | |
| if x.ndim == 4: | |
| return rearrange(x, "b h w (c r q) -> b (h q) (w r) c", q=patch_size, r=patch_size) | |
| elif x.ndim == 5: | |
| return rearrange(x, "b t h w (c r q) -> b t (h q) (w r) c", q=patch_size, r=patch_size) | |
| raise ValueError(f"unpatchify: invalid ndim {x.ndim}") | |
| # --------------------------------------------------------------------------- | |
| # AvgDown3D / DupUp3D — average-pool downsample / repeat-then-shuffle upsample | |
| # (used as the shortcut path in Down_/Up_ResidualBlock) | |
| # --------------------------------------------------------------------------- | |
| class AvgDown3D(nn.Module): | |
| def __init__(self, in_channels: int, out_channels: int, factor_t: int, factor_s: int = 1): | |
| super().__init__() | |
| self.in_channels = in_channels | |
| self.out_channels = out_channels | |
| self.factor_t = factor_t | |
| self.factor_s = factor_s | |
| self.factor = factor_t * factor_s * factor_s | |
| assert in_channels * self.factor % out_channels == 0 | |
| self.group_size = in_channels * self.factor // out_channels | |
| def __call__(self, x: mx.array) -> mx.array: | |
| # x: (B, T, H, W, C) | |
| B, T, H, W, C = x.shape | |
| pad_t = (self.factor_t - T % self.factor_t) % self.factor_t | |
| if pad_t: | |
| x = mx.pad(x, [(0, 0), (pad_t, 0), (0, 0), (0, 0), (0, 0)]) | |
| T = T + pad_t | |
| ft, fs = self.factor_t, self.factor_s | |
| x = x.reshape(B, T // ft, ft, H // fs, fs, W // fs, fs, C) | |
| # PT permutes (B, C, ft, fs, fs, T/ft, H/fs, W/fs) so the merged channel | |
| # dim has order [C(slow), ft, fs, fs(fast)]. In NTHWC the slow factor is | |
| # batch, then spatial outer, then we want C slowest among the merged tail. | |
| # Permute (B, T/ft, H/fs, W/fs, C, ft, fs(H), fs(W)). | |
| x = mx.transpose(x, (0, 1, 3, 5, 7, 2, 4, 6)) | |
| x = x.reshape(B, T // ft, H // fs, W // fs, C * self.factor) | |
| x = x.reshape(B, T // ft, H // fs, W // fs, self.out_channels, self.group_size) | |
| return mx.mean(x, axis=-1) | |
| class DupUp3D(nn.Module): | |
| def __init__(self, in_channels: int, out_channels: int, factor_t: int, factor_s: int = 1): | |
| super().__init__() | |
| self.in_channels = in_channels | |
| self.out_channels = out_channels | |
| self.factor_t = factor_t | |
| self.factor_s = factor_s | |
| self.factor = factor_t * factor_s * factor_s | |
| assert out_channels * self.factor % in_channels == 0 | |
| self.repeats = out_channels * self.factor // in_channels | |
| def __call__(self, x: mx.array, first_chunk: bool = False) -> mx.array: | |
| # x: (B, T, H, W, C). Repeat channel dim self.repeats times -> (B, T, H, W, C * repeats) | |
| # then split into (out_channels, factor_t, factor_s, factor_s) and pixel-shuffle. | |
| x = mx.repeat(x, self.repeats, axis=-1) | |
| B, T, H, W, _ = x.shape | |
| x = x.reshape(B, T, H, W, self.out_channels, self.factor_t, self.factor_s, self.factor_s) | |
| # Interleave factor dims into spatial/temporal: target | |
| # (B, T, ft, H, fs, W, fs, Cout) -> (B, T*ft, H*fs, W*fs, Cout) | |
| x = mx.transpose(x, (0, 1, 5, 2, 6, 3, 7, 4)) | |
| x = x.reshape(B, T * self.factor_t, H * self.factor_s, W * self.factor_s, self.out_channels) | |
| if first_chunk and self.factor_t > 1: | |
| # PT drops the first (factor_t - 1) "anticipated" frames of the first chunk. | |
| x = x[:, self.factor_t - 1 :, :, :, :] | |
| return x | |
| # --------------------------------------------------------------------------- | |
| # Down/Up_ResidualBlock — repeated ResBlocks + optional sample, with avg shortcut | |
| # --------------------------------------------------------------------------- | |
| class Down_ResidualBlock(nn.Module): | |
| def __init__(self, in_dim: int, out_dim: int, dropout: float, mult: int, | |
| temperal_downsample: bool = False, down_flag: bool = False): | |
| super().__init__() | |
| self.avg_shortcut = AvgDown3D( | |
| in_dim, out_dim, | |
| factor_t=2 if temperal_downsample else 1, | |
| factor_s=2 if down_flag else 1, | |
| ) | |
| layers: List[nn.Module] = [] | |
| cur_in = in_dim | |
| for _ in range(mult): | |
| layers.append(ResidualBlock(cur_in, out_dim, dropout)) | |
| cur_in = out_dim | |
| if down_flag: | |
| mode = "downsample3d" if temperal_downsample else "downsample2d" | |
| layers.append(Resample(out_dim, mode=mode)) | |
| self.downsamples = layers | |
| def __call__(self, x: mx.array, feat_cache=None, feat_idx=None) -> mx.array: | |
| x_copy = x | |
| for module in self.downsamples: | |
| if isinstance(module, (ResidualBlock, Resample)): | |
| x = module(x, feat_cache=feat_cache, feat_idx=feat_idx) | |
| else: | |
| x = module(x) | |
| return x + self.avg_shortcut(x_copy) | |
| class Up_ResidualBlock(nn.Module): | |
| def __init__(self, in_dim: int, out_dim: int, dropout: float, mult: int, | |
| temperal_upsample: bool = False, up_flag: bool = False): | |
| super().__init__() | |
| if up_flag: | |
| self.avg_shortcut = DupUp3D( | |
| in_dim, out_dim, | |
| factor_t=2 if temperal_upsample else 1, | |
| factor_s=2 if up_flag else 1, | |
| ) | |
| else: | |
| self.avg_shortcut = None | |
| layers: List[nn.Module] = [] | |
| cur_in = in_dim | |
| for _ in range(mult): | |
| layers.append(ResidualBlock(cur_in, out_dim, dropout)) | |
| cur_in = out_dim | |
| if up_flag: | |
| mode = "upsample3d" if temperal_upsample else "upsample2d" | |
| layers.append(Resample(out_dim, mode=mode)) | |
| self.upsamples = layers | |
| def __call__(self, x: mx.array, feat_cache=None, feat_idx=None, | |
| first_chunk: bool = False) -> mx.array: | |
| x_main = x | |
| for module in self.upsamples: | |
| if isinstance(module, (ResidualBlock, Resample)): | |
| x_main = module(x_main, feat_cache=feat_cache, feat_idx=feat_idx) | |
| else: | |
| x_main = module(x_main) | |
| if self.avg_shortcut is not None: | |
| return x_main + self.avg_shortcut(x, first_chunk=first_chunk) | |
| return x_main | |
| # --------------------------------------------------------------------------- | |
| # Encoder3d / Decoder3d | |
| # --------------------------------------------------------------------------- | |
| class Encoder3d(nn.Module): | |
| def __init__(self, dim: int = 128, z_dim: int = 4, dim_mult=(1, 2, 4, 4), | |
| num_res_blocks: int = 2, attn_scales=(), | |
| temperal_downsample=(True, True, False), dropout: float = 0.0): | |
| super().__init__() | |
| self.dim = dim | |
| self.z_dim = z_dim | |
| self.dim_mult = list(dim_mult) | |
| self.num_res_blocks = num_res_blocks | |
| dims = [dim * u for u in [1] + list(dim_mult)] | |
| # input: 3-channel RGB patchified 2x -> 12 channels | |
| self.conv1 = CausalConv3d(12, dims[0], 3, padding=1) | |
| downsamples: List[nn.Module] = [] | |
| for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])): | |
| t_down_flag = temperal_downsample[i] if i < len(temperal_downsample) else False | |
| downsamples.append( | |
| Down_ResidualBlock( | |
| in_dim=in_dim, out_dim=out_dim, dropout=dropout, mult=num_res_blocks, | |
| temperal_downsample=t_down_flag, down_flag=i != len(dim_mult) - 1, | |
| ) | |
| ) | |
| self.downsamples = downsamples | |
| bottleneck_dim = dims[-1] | |
| self.middle = [ | |
| ResidualBlock(bottleneck_dim, bottleneck_dim, dropout), | |
| AttentionBlock(bottleneck_dim), | |
| ResidualBlock(bottleneck_dim, bottleneck_dim, dropout), | |
| ] | |
| self.head_norm = RMS_norm(bottleneck_dim) | |
| self.head_conv = CausalConv3d(bottleneck_dim, z_dim, 3, padding=1) | |
| def __call__(self, x: mx.array, feat_cache=None, feat_idx=None) -> mx.array: | |
| prev, new_cache, idx = _cache_pre(x, feat_cache, feat_idx) | |
| x = self.conv1(x, cache_x=prev) | |
| _set_cache(feat_cache, idx, new_cache) | |
| for blk in self.downsamples: | |
| x = blk(x, feat_cache=feat_cache, feat_idx=feat_idx) | |
| for blk in self.middle: | |
| if isinstance(blk, ResidualBlock): | |
| x = blk(x, feat_cache=feat_cache, feat_idx=feat_idx) | |
| else: | |
| x = blk(x) | |
| x = self.head_norm(x) | |
| x = nn.silu(x) | |
| prev, new_cache, idx = _cache_pre(x, feat_cache, feat_idx) | |
| x = self.head_conv(x, cache_x=prev) | |
| _set_cache(feat_cache, idx, new_cache) | |
| return x | |
| class Decoder3d(nn.Module): | |
| def __init__(self, dim: int = 128, z_dim: int = 4, dim_mult=(1, 2, 4, 4), | |
| num_res_blocks: int = 2, attn_scales=(), | |
| temperal_upsample=(False, True, True), dropout: float = 0.0): | |
| super().__init__() | |
| self.dim = dim | |
| self.z_dim = z_dim | |
| self.dim_mult = list(dim_mult) | |
| self.num_res_blocks = num_res_blocks | |
| # dim list runs in reverse for the decoder | |
| rev_mult = list(dim_mult[::-1]) | |
| dims = [dim * u for u in [dim_mult[-1]] + rev_mult] | |
| self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1) | |
| bottleneck_dim = dims[0] | |
| self.middle = [ | |
| ResidualBlock(bottleneck_dim, bottleneck_dim, dropout), | |
| AttentionBlock(bottleneck_dim), | |
| ResidualBlock(bottleneck_dim, bottleneck_dim, dropout), | |
| ] | |
| upsamples: List[nn.Module] = [] | |
| for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])): | |
| t_up_flag = temperal_upsample[i] if i < len(temperal_upsample) else False | |
| upsamples.append( | |
| Up_ResidualBlock( | |
| in_dim=in_dim, out_dim=out_dim, dropout=dropout, mult=num_res_blocks + 1, | |
| temperal_upsample=t_up_flag, up_flag=i != len(dim_mult) - 1, | |
| ) | |
| ) | |
| self.upsamples = upsamples | |
| out_final = dims[-1] | |
| self.head_norm = RMS_norm(out_final) | |
| self.head_conv = CausalConv3d(out_final, 12, 3, padding=1) | |
| def __call__(self, x: mx.array, feat_cache=None, feat_idx=None, | |
| first_chunk: bool = False) -> mx.array: | |
| prev, new_cache, idx = _cache_pre(x, feat_cache, feat_idx) | |
| x = self.conv1(x, cache_x=prev) | |
| _set_cache(feat_cache, idx, new_cache) | |
| for blk in self.middle: | |
| if isinstance(blk, ResidualBlock): | |
| x = blk(x, feat_cache=feat_cache, feat_idx=feat_idx) | |
| else: | |
| x = blk(x) | |
| for blk in self.upsamples: | |
| x = blk(x, feat_cache=feat_cache, feat_idx=feat_idx, first_chunk=first_chunk) | |
| x = self.head_norm(x) | |
| x = nn.silu(x) | |
| prev, new_cache, idx = _cache_pre(x, feat_cache, feat_idx) | |
| x = self.head_conv(x, cache_x=prev) | |
| _set_cache(feat_cache, idx, new_cache) | |
| return x | |
| # --------------------------------------------------------------------------- | |
| # WanVAE_ — inner model: encoder + conv1 (mu/log_var) + conv2 + decoder | |
| # --------------------------------------------------------------------------- | |
| class WanVAE_(nn.Module): | |
| def __init__(self, dim: int = 160, dec_dim: int = 256, z_dim: int = 16, | |
| dim_mult=(1, 2, 4, 4), num_res_blocks: int = 2, attn_scales=(), | |
| temperal_downsample=(True, True, False), dropout: float = 0.0): | |
| super().__init__() | |
| self.z_dim = z_dim | |
| self.temperal_downsample = list(temperal_downsample) | |
| self.temperal_upsample = list(temperal_downsample[::-1]) | |
| self.encoder = Encoder3d( | |
| dim, z_dim * 2, dim_mult, num_res_blocks, attn_scales, | |
| self.temperal_downsample, dropout, | |
| ) | |
| self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1) | |
| self.conv2 = CausalConv3d(z_dim, z_dim, 1) | |
| self.decoder = Decoder3d( | |
| dec_dim, z_dim, dim_mult, num_res_blocks, attn_scales, | |
| self.temperal_upsample, dropout, | |
| ) | |
| # ----------------------------------------------------------------------- | |
| # Conv counting (for cache slot allocation) | |
| # ----------------------------------------------------------------------- | |
| def _count_conv3d(module: nn.Module) -> int: | |
| """Count CausalConv3d instances that participate in streaming-cache. | |
| A CausalConv3d takes cache iff its kernel has a temporal extent > 1 | |
| (i.e. ._pt > 0). 1×1 shortcuts inside ResidualBlock have no temporal | |
| kernel and are called without cache in the forward path, so they | |
| must NOT consume a cache slot. | |
| Resample with mode upsample3d/downsample3d contributes one slot for | |
| its time_conv (also a CausalConv3d with temporal kernel). | |
| """ | |
| n = 0 | |
| visited = set() | |
| def _walk(obj): | |
| nonlocal n | |
| obj_id = id(obj) | |
| if obj_id in visited: | |
| return | |
| visited.add(obj_id) | |
| if isinstance(obj, Resample): | |
| # upsample3d / downsample3d each have a time_conv that | |
| # consumes exactly one cache slot. downsample3d's time_conv | |
| # has padding=0 (_pt=0) but still uses the cache to prepend | |
| # the last frame of the previous chunk before convolution. | |
| # Count unconditionally for these modes. | |
| if obj.mode in ("upsample3d", "downsample3d"): | |
| n += 1 | |
| return | |
| if isinstance(obj, CausalConv3d): | |
| if obj._pt > 0: | |
| n += 1 | |
| return | |
| if isinstance(obj, nn.Module): | |
| # MLX nn.Module is dict-like: children are stored via .items() | |
| # (and __setattr__ routes through __setitem__). | |
| for _, v in obj.items(): | |
| _walk(v) | |
| elif isinstance(obj, (list, tuple)): | |
| for v in obj: | |
| _walk(v) | |
| _walk(module) | |
| return n | |
| def encode(self, x: mx.array, scale) -> tuple: | |
| """Encode (B, T, H, W, 3) in [-1, 1] -> (mu, log_var) each (B, T', H', W', z_dim). | |
| For T=1 (single image): single-pass. | |
| For T>1: streaming-cache chunked encode matching the PT reference. The | |
| encoder consumes frames in chunks of (1, 4, 4, 4, ...) and shares | |
| intermediate temporal-conv state across chunks via feat_cache. | |
| """ | |
| x = patchify(x, patch_size=2) # (B, T, H/2, W/2, 12) | |
| T = x.shape[1] | |
| if T == 1: | |
| out = self.encoder(x) | |
| else: | |
| # chunked: first chunk is frame [0:1], then chunks of 4 | |
| n_cache = self._count_conv3d(self.encoder) | |
| feat_cache = [None] * n_cache | |
| feat_idx = [0] | |
| iter_n = 1 + (T - 1) // 4 | |
| outs = [] | |
| for i in range(iter_n): | |
| feat_idx[0] = 0 | |
| if i == 0: | |
| xi = x[:, :1, ...] | |
| else: | |
| start = 1 + 4 * (i - 1) | |
| end = min(1 + 4 * i, T) | |
| xi = x[:, start:end, ...] | |
| outs.append(self.encoder(xi, feat_cache=feat_cache, feat_idx=feat_idx)) | |
| out = mx.concatenate(outs, axis=1) | |
| h = self.conv1(out) | |
| mu, log_var = mx.split(h, 2, axis=-1) | |
| mean, inv_std = scale | |
| mu = (mu - mean) * inv_std | |
| return mu, log_var | |
| def decode(self, z: mx.array, scale) -> mx.array: | |
| """Decode (B, T', H', W', z_dim) -> (B, T, H, W, 3). | |
| For T'=1 (single image): single-pass. | |
| For T'>1: streaming-cache chunked decode (one latent frame at a time). | |
| """ | |
| mean, inv_std = scale | |
| z = z / inv_std + mean | |
| x = self.conv2(z) | |
| T_lat = x.shape[1] | |
| if T_lat == 1: | |
| out = self.decoder(x, first_chunk=True) | |
| else: | |
| n_cache = self._count_conv3d(self.decoder) | |
| feat_cache = [None] * n_cache | |
| feat_idx = [0] | |
| outs = [] | |
| for i in range(T_lat): | |
| feat_idx[0] = 0 | |
| xi = x[:, i:i+1, ...] | |
| first_chunk = (i == 0) | |
| outs.append(self.decoder( | |
| xi, feat_cache=feat_cache, feat_idx=feat_idx, | |
| first_chunk=first_chunk, | |
| )) | |
| out = mx.concatenate(outs, axis=1) | |
| return unpatchify(out, patch_size=2) | |
| # --------------------------------------------------------------------------- | |
| # Wan2_2_VAE — public wrapper with checkpoint-derived mean/std normalization | |
| # --------------------------------------------------------------------------- | |
| # Lance/Wan 2.2 normalization constants (z_dim=48). Copied verbatim from PT | |
| # vae2_2.py so behavior matches. | |
| _WAN22_MEAN = [ | |
| -0.2289, -0.0052, -0.1323, -0.2339, -0.2799, 0.0174, 0.1838, 0.1557, | |
| -0.1382, 0.0542, 0.2813, 0.0891, 0.1570, -0.0098, 0.0375, -0.1825, | |
| -0.2246, -0.1207, -0.0698, 0.5109, 0.2665, -0.2108, -0.2158, 0.2502, | |
| -0.2055, -0.0322, 0.1109, 0.1567, -0.0729, 0.0899, -0.2799, -0.1230, | |
| -0.0313, -0.1649, 0.0117, 0.0723, -0.2839, -0.2083, -0.0520, 0.3748, | |
| 0.0152, 0.1957, 0.1433, -0.2944, 0.3573, -0.0548, -0.1681, -0.0667, | |
| ] | |
| _WAN22_STD = [ | |
| 0.4765, 1.0364, 0.4514, 1.1677, 0.5313, 0.4990, 0.4818, 0.5013, | |
| 0.8158, 1.0344, 0.5894, 1.0901, 0.6885, 0.6165, 0.8454, 0.4978, | |
| 0.5759, 0.3523, 0.7135, 0.6804, 0.5833, 1.4146, 0.8986, 0.5659, | |
| 0.7069, 0.5338, 0.4889, 0.4917, 0.4069, 0.4999, 0.6866, 0.4093, | |
| 0.5709, 0.6065, 0.6415, 0.4944, 0.5726, 1.2042, 0.5458, 1.6887, | |
| 0.3971, 1.0600, 0.3943, 0.5537, 0.5444, 0.4089, 0.7468, 0.7744, | |
| ] | |
| class Wan2_2_VAE: | |
| """Outer wrapper: applies channel-wise mean/std normalization around the inner VAE.""" | |
| def __init__( | |
| self, | |
| z_dim: int = 48, | |
| c_dim: int = 160, | |
| dim_mult=(1, 2, 4, 4), | |
| temperal_downsample=(False, True, True), | |
| dtype=mx.float32, | |
| ): | |
| self.dtype = dtype | |
| mean = mx.array(_WAN22_MEAN, dtype=dtype) # (48,) | |
| inv_std = 1.0 / mx.array(_WAN22_STD, dtype=dtype) # (48,) | |
| # channel-last broadcasting: mean/inv_std are (C,) and broadcast over (B,T,H,W,C) | |
| self.scale = (mean, inv_std) | |
| self.model = WanVAE_( | |
| dim=c_dim, | |
| z_dim=z_dim, | |
| dim_mult=dim_mult, | |
| num_res_blocks=2, | |
| attn_scales=(), | |
| temperal_downsample=temperal_downsample, | |
| dropout=0.0, | |
| ) | |
| def encode(self, video: mx.array): | |
| """video: (B, T, H, W, 3) float in [-1, 1]""" | |
| mu, log_var = self.model.encode(video, self.scale) | |
| return mu, log_var | |
| def decode(self, u: mx.array) -> mx.array: | |
| """u: (B, T', H', W', 48). Returns (B, T, H, W, 3) clamped to [-1, 1].""" | |
| x = self.model.decode(u, self.scale) | |
| return mx.clip(x, -1.0, 1.0) | |