Instructions to use Motif-Technologies/Motif-VAE with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use Motif-Technologies/Motif-VAE with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline from diffusers.utils import load_image # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("Motif-Technologies/Motif-VAE", dtype=torch.bfloat16, device_map="cuda") prompt = "Turn this cat into a dog" input_image = load_image("https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/diffusers/cat.png") image = pipe(image=input_image, prompt=prompt).images[0] - Notebooks
- Google Colab
- Kaggle
Commit ·
931fe95
0
Parent(s):
MotifVAE
Browse filesCo-authored-by: gkalstn0 <gkalstn0@users.noreply.huggingface.co>
- .gitattributes +44 -0
- README.md +206 -0
- assets/hero_video_recon.mp4 +3 -0
- assets/natural_recon.png +3 -0
- assets/ocr_recon.png +3 -0
- assets/ucf101_fvd_curves.png +3 -0
- assets/video_recon_1.mp4 +3 -0
- assets/video_recon_2.mp4 +3 -0
- config.json +311 -0
- diffusion_pytorch_model.safetensors +3 -0
- modeling_motifvae.py +1132 -0
.gitattributes
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
assets/korean_ocr_recon.png filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
assets/natural_recon.png filter=lfs diff=lfs merge=lfs -text
|
| 38 |
+
assets/ucf101_fvd_curves.png filter=lfs diff=lfs merge=lfs -text
|
| 39 |
+
assets/video_recon.gif filter=lfs diff=lfs merge=lfs -text
|
| 40 |
+
assets/omnidoc_recon.png filter=lfs diff=lfs merge=lfs -text
|
| 41 |
+
assets/hero_video_recon.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 42 |
+
assets/ocr_recon.png filter=lfs diff=lfs merge=lfs -text
|
| 43 |
+
assets/video_recon_2.mp4 filter=lfs diff=lfs merge=lfs -text
|
| 44 |
+
assets/video_recon_1.mp4 filter=lfs diff=lfs merge=lfs -text
|
README.md
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: mit
|
| 3 |
+
library_name: diffusers
|
| 4 |
+
pipeline_tag: image-to-image
|
| 5 |
+
tags:
|
| 6 |
+
- video-vae
|
| 7 |
+
- vae
|
| 8 |
+
- video-generation
|
| 9 |
+
- tokenizer
|
| 10 |
+
- pytorch
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
# MotifVAE
|
| 14 |
+
|
| 15 |
+
MotifVAE is a 3D causal video VAE with 4x temporal and 32x spatial compression and a
|
| 16 |
+
128-channel latent space, built as a high-compression tokenizer for latent video diffusion
|
| 17 |
+
models. A 256x256x17-frame clip encodes to a `(128, 5, 8, 8)` latent, which is 1/16 the
|
| 18 |
+
spatio-temporal tokens of an 8x-spatial VAE, so downstream diffusion-transformer training and
|
| 19 |
+
inference cost less. The decoder is much larger than the encoder (about 4:1 in parameters),
|
| 20 |
+
which helps it keep fine detail such as small text in documents.
|
| 21 |
+
|
| 22 |
+
It also runs as a plain image VAE at `T=1`. In its compression class it has the best document
|
| 23 |
+
text fidelity, video reconstruction, and diffusability (how well a diffusion model converges on
|
| 24 |
+
its latents) of the tokenizers we compared.
|
| 25 |
+
|
| 26 |
+
## Architecture
|
| 27 |
+
|
| 28 |
+
| | |
|
| 29 |
+
|---|---|
|
| 30 |
+
| Type | 3D causal VAE, attention-free (pure convolution) |
|
| 31 |
+
| Compression | temporal 4x (T = 4n+1 grammar) / spatial 32x32 |
|
| 32 |
+
| Latent | 128 channels, deterministic encoder |
|
| 33 |
+
| Parameters | 1.244B total: encoder 242M, decoder 1,002M (1:4.15) |
|
| 34 |
+
| Encoder | 5 down stages (3x spatial-only, 2x spatio-temporal), 2 resblocks/stage, parameter-free global skip connections |
|
| 35 |
+
| Decoder | 1.5x channel width, 3 resblocks/stage, sub-pixel (PixelShuffle + ICNR) spatial upsampling, trilinear temporal upsampling |
|
| 36 |
+
| Norm / act | LayerNorm / SiLU |
|
| 37 |
+
|
| 38 |
+
## Usage
|
| 39 |
+
|
| 40 |
+
The model code ships inside this repository (`modeling_motifvae.py`) and loads through the
|
| 41 |
+
stock diffusers `AutoModel` — no extra package beyond `torch`, `diffusers`, `einops`:
|
| 42 |
+
|
| 43 |
+
```python
|
| 44 |
+
import torch
|
| 45 |
+
from diffusers import AutoModel
|
| 46 |
+
|
| 47 |
+
vae = AutoModel.from_pretrained("Motif-Technologies/motif-vae", trust_remote_code=True)
|
| 48 |
+
vae = vae.to("cuda", dtype=torch.bfloat16).eval()
|
| 49 |
+
|
| 50 |
+
# video: (B, 3, T, H, W) in [-1, 1], T = 4n+1 (1, 5, 9, 13, 17, ...),
|
| 51 |
+
# H/W multiples of 32. T=1 works as a plain image VAE.
|
| 52 |
+
x = torch.randn(1, 3, 17, 256, 256, device="cuda", dtype=torch.bfloat16)
|
| 53 |
+
with torch.no_grad():
|
| 54 |
+
z = vae.encode(x).latent_dist.sample() # (1, 128, 5, 8, 8)
|
| 55 |
+
rec = vae.decode(z).sample # (1, 3, 17, 256, 256)
|
| 56 |
+
```
|
| 57 |
+
|
| 58 |
+
For diffusion training, whiten the latent to roughly unit variance with the per-channel
|
| 59 |
+
statistics shipped in the config — `shift` is the channel mean and `scale` the channel std
|
| 60 |
+
(128 values each, measured over Kinetics-400 video (256x256x17) and ImageNet-1K images (256x256)).
|
| 61 |
+
Apply per channel, reshaping both to `(1, 128, 1, 1, 1)`:
|
| 62 |
+
|
| 63 |
+
```python
|
| 64 |
+
z = vae.encode(x).latent_dist.sample()
|
| 65 |
+
shift = torch.tensor(vae.config.shift, device=z.device).view(1, -1, 1, 1, 1)
|
| 66 |
+
scale = torch.tensor(vae.config.scale, device=z.device).view(1, -1, 1, 1, 1)
|
| 67 |
+
z = (z - shift) / scale # whiten for the diffusion model
|
| 68 |
+
# ... sample/denoise in this space, then invert before decoding:
|
| 69 |
+
z = z * scale + shift
|
| 70 |
+
rec = vae.decode(z).sample
|
| 71 |
+
```
|
| 72 |
+
|
| 73 |
+
Recompute these statistics on your own data if its distribution differs from natural video.
|
| 74 |
+
|
| 75 |
+
## Evaluation
|
| 76 |
+
|
| 77 |
+
Baseline numbers are either the published values cited in each table or measured by us under
|
| 78 |
+
the same protocol. MotifVAE is a **32x spatial** tokenizer, so the comparison is against the
|
| 79 |
+
other 32x tokenizers (LTX-Video, LTX-2, and the f32 image VAEs below); the lower-compression
|
| 80 |
+
16x Wan2.2 appears only as a reference upper bound.
|
| 81 |
+
|
| 82 |
+
### Image reconstruction — ImageNet-1K val (256x256) & FFHQ (1024x1024)
|
| 83 |
+
|
| 84 |
+
All models below are **32x (f32) image VAEs**. MotifVAE is measured by us with per-image
|
| 85 |
+
PSNR / SSIM (the published-table convention); baseline rows are the published numbers from the
|
| 86 |
+
**Qwen-Image-VAE-2.0** technical report (Table 2, same ImageNet-256 / FFHQ protocol). Our own
|
| 87 |
+
runs of the baselines reproduce those values to within 0.2 dB, so the comparison is direct.
|
| 88 |
+
|
| 89 |
+
| Model | Setting | ImageNet PSNR ↑ | SSIM ↑ | FFHQ PSNR ↑ | SSIM ↑ |
|
| 90 |
+
|---|---|---|---|---|---|
|
| 91 |
+
| **MotifVAE** | **f32c128** | **30.27** | **0.852** | **36.17** | **0.921** |
|
| 92 |
+
| Qwen-Image-VAE-2.0 | f32c128 | 29.69 | 0.842 | 35.91 | 0.918 |
|
| 93 |
+
| LTX-Video | f32c128 | 29.57 | 0.833 | 35.56 | 0.905 |
|
| 94 |
+
| HunyuanImage-2.1 | f32c64 | 28.67 | 0.820 | 35.30 | 0.911 |
|
| 95 |
+
| LTX-2 | f32c128 | 26.06 | 0.793 | 33.63 | 0.906 |
|
| 96 |
+
| DC-AE (Sana) | f32c32 | 24.82 | 0.690 | 31.35 | 0.830 |
|
| 97 |
+
|
| 98 |
+
At the f32c128 channel budget MotifVAE leads both benchmarks among 32x image VAEs, above
|
| 99 |
+
Qwen-Image-VAE-2.0's f32c128 (29.69 / 35.91 dB), LTX-Video, LTX-2, and the lighter f32c64 /
|
| 100 |
+
f32c32 models.
|
| 101 |
+
|
| 102 |
+
### Qualitative comparison (real photographs)
|
| 103 |
+
|
| 104 |
+
GT vs frozen-tokenizer reconstruction on ImageNet-1K val photographs; the red box marks the
|
| 105 |
+
zoom strip shown below each row.
|
| 106 |
+
|
| 107 |
+

|
| 108 |
+
|
| 109 |
+
These rows are high-frequency patterns that high-compression VAEs find hard: an armadillo's
|
| 110 |
+
banded shell, a dense solar-panel grid, brickwork, and vehicle markings. LTX-Video and LTX-2
|
| 111 |
+
(same 32x class) lose the pattern, and the solar grid collapses for them entirely. MotifVAE
|
| 112 |
+
keeps it and stays close to the 16x Wan2.2 reference, which also struggles on some of these.
|
| 113 |
+
|
| 114 |
+
### Document text fidelity — OmniDoc-TokenBench (256x256)
|
| 115 |
+
|
| 116 |
+
f32-compression VAEs on OmniDoc-TokenBench (~3K text-rich documents). Baseline rows are the
|
| 117 |
+
published numbers from the **Qwen-Image-VAE-2.0** technical report (Table 3, same benchmark
|
| 118 |
+
/ resolution / OCR-NED protocol); sorted by NED.
|
| 119 |
+
|
| 120 |
+
| Model | Setting | SSIM ↑ | PSNR ↑ | LPIPS ↓ | FID ↓ | OCR-NED ↑ |
|
| 121 |
+
|---|---|---|---|---|---|---|
|
| 122 |
+
| **MotifVAE** | **f32c128** | **0.849** | **22.40** | **0.052** | **3.43** | **0.825** |
|
| 123 |
+
| Qwen-Image-VAE-2.0 | f32c128 | 0.844 | 22.13 | 0.064 | 3.36 | 0.707 |
|
| 124 |
+
| LTX-Video | f32c128 | 0.806 | 20.92 | 0.119 | 17.10 | 0.565 |
|
| 125 |
+
| HunyuanImage-2.1 | f32c64 | 0.781 | 19.85 | 0.096 | 5.19 | 0.490 |
|
| 126 |
+
| LTX-2 | f32c128 | 0.735 | 18.41 | 0.119 | 9.94 | 0.357 |
|
| 127 |
+
| DC-AE (Sana) | f32c32 | 0.526 | 15.62 | 0.144 | 7.26 | 0.069 |
|
| 128 |
+
|
| 129 |
+
MotifVAE has the highest OCR-NED of any f32 VAE here: +0.12 over Qwen's f32c128 (same channel
|
| 130 |
+
budget) and about 2.3x that of LTX-Video / LTX-2. It also leads every pixel metric in its class.
|
| 131 |
+
The figure below covers English print, Korean print, and Korean handwriting; each row shows the
|
| 132 |
+
full page and a zoom-in on the red-boxed region. The top two rows are OmniDoc-TokenBench
|
| 133 |
+
(English); the bottom two are Korean printed text and handwriting from AIHUB (the table metrics
|
| 134 |
+
above are OmniDoc-only):
|
| 135 |
+
|
| 136 |
+

|
| 137 |
+
|
| 138 |
+
### Video reconstruction — Kinetics-400 (256x256) & OpenVid-1M (720p)
|
| 139 |
+
|
| 140 |
+
The same four frozen tokenizers on both benchmarks, under one pipeline: the three 32x spatial
|
| 141 |
+
tokenizers (MotifVAE, LTX-Video, LTX-2) and the 16x Wan2.2 as a reference.
|
| 142 |
+
|
| 143 |
+
**Kinetics-400 val — 17x256x256**
|
| 144 |
+
|
| 145 |
+
| Model | compression | PSNR ↑ | SSIM ↑ | LPIPS ↓ |
|
| 146 |
+
|---|---|---|---|---|
|
| 147 |
+
| **MotifVAE** | **4x32x32** | **35.37** | **0.954** | **0.046** |
|
| 148 |
+
| LTX-Video | 8x32x32 | 32.58 | 0.919 | 0.101 |
|
| 149 |
+
| LTX-2 | 8x32x32 | 28.05 | 0.888 | 0.128 |
|
| 150 |
+
| Wan2.2 | 4x16x16 | 37.41 | 0.966 | 0.034 |
|
| 151 |
+
|
| 152 |
+
**OpenVid-1M — native-720p, 17x1280x704**
|
| 153 |
+
|
| 154 |
+
| Model | compression | PSNR ↑ | SSIM ↑ | LPIPS ↓ |
|
| 155 |
+
|---|---|---|---|---|
|
| 156 |
+
| **MotifVAE** | **4x32x32** | **36.66** | **0.965** | **0.044** |
|
| 157 |
+
| LTX-Video | 8x32x32 | 34.04 | 0.942 | 0.093 |
|
| 158 |
+
| LTX-2 | 8x32x32 | 31.95 | 0.937 | 0.094 |
|
| 159 |
+
| Wan2.2 | 4x16x16 | 38.13 | 0.972 | 0.035 |
|
| 160 |
+
|
| 161 |
+
Among the 32x spatial tokenizers MotifVAE leads both tables: +2.7 dB over LTX-Video and
|
| 162 |
+
+7.3 dB over LTX-2 on K400, and +2.6 / +4.7 dB on 720p. At 720p it stays above 30 dB /
|
| 163 |
+
0.05 LPIPS (36.7 / 0.044), and at 512x512 it reaches PSNR 39.68. The 16x Wan2.2 reference is a
|
| 164 |
+
little ahead; the gap to the other 32x VAEs is largest on high-motion clips (examples below).
|
| 165 |
+
|
| 166 |
+
> **Protocol.** K400: 256 random val clips; OpenVid-1M: 30 native-1280x720 web clips
|
| 167 |
+
> centre-cropped to 1280x704. Both use 17 frames/clip and report per-video PSNR / SSIM / LPIPS
|
| 168 |
+
> (pooled per clip, then averaged) under one pipeline, so each table's ranking is exact;
|
| 169 |
+
> absolute values aren't comparable across papers.
|
| 170 |
+
|
| 171 |
+
### Video reconstruction examples
|
| 172 |
+
|
| 173 |
+
Frozen-tokenizer reconstruction on high-motion Kinetics-400 clips. Each clip is one strip:
|
| 174 |
+
Input / MotifVAE / LTX-Video / LTX-2 / Wan2.2 (labelled). MotifVAE keeps the fast motion close
|
| 175 |
+
to the 16x Wan2.2; LTX-Video and LTX-2 blur it.
|
| 176 |
+
|
| 177 |
+
<video controls autoplay loop muted src="https://huggingface.co/Motif-Technologies/motif-vae/resolve/main/assets/hero_video_recon.mp4"></video>
|
| 178 |
+
|
| 179 |
+
<video controls autoplay loop muted src="https://huggingface.co/Motif-Technologies/motif-vae/resolve/main/assets/video_recon_1.mp4"></video>
|
| 180 |
+
|
| 181 |
+
<video controls autoplay loop muted src="https://huggingface.co/Motif-Technologies/motif-vae/resolve/main/assets/video_recon_2.mp4"></video>
|
| 182 |
+
|
| 183 |
+
### Diffusability — UCF101 class-conditional generation
|
| 184 |
+
|
| 185 |
+
Latte-XL/1 (patch-1, rectified flow, 250K steps, no-CFG FVD-2048) over each frozen
|
| 186 |
+
tokenizer, under the Latte-official **stride-3** protocol. The Wan2.2 control (FVD 204) is
|
| 187 |
+
in line with values reported for this setting in the literature, confirming the harness
|
| 188 |
+
reproduces the expected scale.
|
| 189 |
+
|
| 190 |
+

|
| 191 |
+
|
| 192 |
+
| Tokenizer | FVD @50K | FVD @100K | FVD @250K |
|
| 193 |
+
|---|---|---|---|
|
| 194 |
+
| **MotifVAE** (4x32x32) | **636** | **385** | **276** |
|
| 195 |
+
| LTX-2 (8x32x32) | 952 | 556 | 450 |
|
| 196 |
+
| LTX-Video (8x32x32) | 1251 | 732 | 593 |
|
| 197 |
+
| Wan2.2 (4x16x16) | 656 | 304 | 204 |
|
| 198 |
+
|
| 199 |
+
Among the 32x spatial tokenizers MotifVAE leads at every milestone, finishing 39% below LTX-2
|
| 200 |
+
and 53% below LTX-Video. It reaches LTX-2's final 250K FVD by about 75K steps (~3x faster) and
|
| 201 |
+
stays close to the 16x Wan2.2 reference. Reconstruction quality and diffusability are separate
|
| 202 |
+
properties: LTX-Video reconstructs better than LTX-2 but is the worst of the three for diffusion.
|
| 203 |
+
|
| 204 |
+
## License
|
| 205 |
+
|
| 206 |
+
MIT
|
assets/hero_video_recon.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:fba132e0756157019efd741352c4d5e1ebfda63b4f6c4b39cd772a68e0be9e8c
|
| 3 |
+
size 516915
|
assets/natural_recon.png
ADDED
|
Git LFS Details
|
assets/ocr_recon.png
ADDED
|
Git LFS Details
|
assets/ucf101_fvd_curves.png
ADDED
|
Git LFS Details
|
assets/video_recon_1.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b6126a026df8915f581fb711ade055171b2d2fbc35a4a2d3e8241fd96600f1a1
|
| 3 |
+
size 319298
|
assets/video_recon_2.mp4
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:b59e0f8a8192ef927dfa88d1a6cce9e4fa5e84cfde7c94b212923d3453b75e9b
|
| 3 |
+
size 332585
|
config.json
ADDED
|
@@ -0,0 +1,311 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"_class_name": "MotifVAE",
|
| 3 |
+
"_diffusers_version": "0.38.0",
|
| 4 |
+
"auto_map": {
|
| 5 |
+
"AutoModel": "modeling_motifvae.MotifVAE"
|
| 6 |
+
},
|
| 7 |
+
"base_channels": [
|
| 8 |
+
96,
|
| 9 |
+
192,
|
| 10 |
+
384,
|
| 11 |
+
768,
|
| 12 |
+
768,
|
| 13 |
+
768
|
| 14 |
+
],
|
| 15 |
+
"decoder_base_channels": [
|
| 16 |
+
144,
|
| 17 |
+
288,
|
| 18 |
+
576,
|
| 19 |
+
1152,
|
| 20 |
+
1152,
|
| 21 |
+
1152
|
| 22 |
+
],
|
| 23 |
+
"decoder_num_resblocks": 3,
|
| 24 |
+
"decoder_variant": "V4",
|
| 25 |
+
"dropout": 0.0,
|
| 26 |
+
"encoder_num_resblocks": 2,
|
| 27 |
+
"input_type": "video",
|
| 28 |
+
"latent_dim": 128,
|
| 29 |
+
"layer_res_type": [
|
| 30 |
+
"ResnetBlock2D",
|
| 31 |
+
"ResnetBlock2D",
|
| 32 |
+
"ResnetBlock2D",
|
| 33 |
+
"ResnetBlock3D",
|
| 34 |
+
"ResnetBlock3D"
|
| 35 |
+
],
|
| 36 |
+
"layer_type": [
|
| 37 |
+
"hw",
|
| 38 |
+
"hw",
|
| 39 |
+
"hw",
|
| 40 |
+
"thw",
|
| 41 |
+
"thw"
|
| 42 |
+
],
|
| 43 |
+
"mid_layers_type": [
|
| 44 |
+
"ResnetBlock3D",
|
| 45 |
+
"Identity",
|
| 46 |
+
"ResnetBlock3D"
|
| 47 |
+
],
|
| 48 |
+
"norm_type": "layernorm",
|
| 49 |
+
"scale": [
|
| 50 |
+
0.193781,
|
| 51 |
+
0.160648,
|
| 52 |
+
0.212569,
|
| 53 |
+
0.295691,
|
| 54 |
+
0.157944,
|
| 55 |
+
0.285008,
|
| 56 |
+
0.255515,
|
| 57 |
+
0.229688,
|
| 58 |
+
0.221633,
|
| 59 |
+
0.232136,
|
| 60 |
+
0.139637,
|
| 61 |
+
0.244213,
|
| 62 |
+
0.19225,
|
| 63 |
+
0.209093,
|
| 64 |
+
0.24903,
|
| 65 |
+
0.33651,
|
| 66 |
+
0.224575,
|
| 67 |
+
0.192788,
|
| 68 |
+
0.177301,
|
| 69 |
+
0.191538,
|
| 70 |
+
0.181008,
|
| 71 |
+
0.224443,
|
| 72 |
+
0.21792,
|
| 73 |
+
0.219838,
|
| 74 |
+
0.15724,
|
| 75 |
+
0.215918,
|
| 76 |
+
0.197021,
|
| 77 |
+
0.228155,
|
| 78 |
+
0.165341,
|
| 79 |
+
0.202019,
|
| 80 |
+
0.196588,
|
| 81 |
+
0.242306,
|
| 82 |
+
0.170205,
|
| 83 |
+
0.169329,
|
| 84 |
+
0.271,
|
| 85 |
+
0.237309,
|
| 86 |
+
0.172621,
|
| 87 |
+
0.303556,
|
| 88 |
+
0.190824,
|
| 89 |
+
0.175744,
|
| 90 |
+
0.194385,
|
| 91 |
+
0.189157,
|
| 92 |
+
0.224383,
|
| 93 |
+
0.187539,
|
| 94 |
+
0.159795,
|
| 95 |
+
0.209762,
|
| 96 |
+
0.17788,
|
| 97 |
+
0.160627,
|
| 98 |
+
0.189383,
|
| 99 |
+
0.137677,
|
| 100 |
+
0.191022,
|
| 101 |
+
0.181854,
|
| 102 |
+
0.183199,
|
| 103 |
+
0.181121,
|
| 104 |
+
0.189024,
|
| 105 |
+
0.183419,
|
| 106 |
+
0.18505,
|
| 107 |
+
0.185571,
|
| 108 |
+
0.203301,
|
| 109 |
+
0.260823,
|
| 110 |
+
0.166004,
|
| 111 |
+
0.143982,
|
| 112 |
+
0.556997,
|
| 113 |
+
0.252643,
|
| 114 |
+
0.296122,
|
| 115 |
+
0.373026,
|
| 116 |
+
0.162889,
|
| 117 |
+
0.261822,
|
| 118 |
+
0.18582,
|
| 119 |
+
0.210161,
|
| 120 |
+
0.228212,
|
| 121 |
+
0.217485,
|
| 122 |
+
0.165954,
|
| 123 |
+
0.189418,
|
| 124 |
+
0.210086,
|
| 125 |
+
0.186101,
|
| 126 |
+
0.350991,
|
| 127 |
+
0.174668,
|
| 128 |
+
0.199964,
|
| 129 |
+
0.22358,
|
| 130 |
+
0.172738,
|
| 131 |
+
0.159074,
|
| 132 |
+
0.284967,
|
| 133 |
+
0.148048,
|
| 134 |
+
0.192643,
|
| 135 |
+
0.185355,
|
| 136 |
+
0.264092,
|
| 137 |
+
0.148031,
|
| 138 |
+
0.285184,
|
| 139 |
+
0.177816,
|
| 140 |
+
0.198399,
|
| 141 |
+
0.222337,
|
| 142 |
+
0.230656,
|
| 143 |
+
0.239789,
|
| 144 |
+
0.141241,
|
| 145 |
+
0.360838,
|
| 146 |
+
0.194351,
|
| 147 |
+
0.152777,
|
| 148 |
+
0.197615,
|
| 149 |
+
0.166155,
|
| 150 |
+
0.194582,
|
| 151 |
+
0.166021,
|
| 152 |
+
0.170577,
|
| 153 |
+
0.318022,
|
| 154 |
+
0.271652,
|
| 155 |
+
0.200244,
|
| 156 |
+
0.18932,
|
| 157 |
+
0.179213,
|
| 158 |
+
0.179843,
|
| 159 |
+
0.246742,
|
| 160 |
+
0.237725,
|
| 161 |
+
0.189994,
|
| 162 |
+
0.174361,
|
| 163 |
+
0.206378,
|
| 164 |
+
0.178746,
|
| 165 |
+
0.174695,
|
| 166 |
+
0.22207,
|
| 167 |
+
0.165952,
|
| 168 |
+
0.161002,
|
| 169 |
+
0.242472,
|
| 170 |
+
0.240569,
|
| 171 |
+
0.257974,
|
| 172 |
+
0.297275,
|
| 173 |
+
0.225371,
|
| 174 |
+
0.175571,
|
| 175 |
+
0.177474,
|
| 176 |
+
0.155566,
|
| 177 |
+
0.179106
|
| 178 |
+
],
|
| 179 |
+
"shift": [
|
| 180 |
+
-0.164127,
|
| 181 |
+
0.092961,
|
| 182 |
+
-0.081433,
|
| 183 |
+
-0.086702,
|
| 184 |
+
-0.130157,
|
| 185 |
+
-0.008459,
|
| 186 |
+
-0.068426,
|
| 187 |
+
-0.192902,
|
| 188 |
+
0.051399,
|
| 189 |
+
-0.047744,
|
| 190 |
+
-0.066346,
|
| 191 |
+
0.171836,
|
| 192 |
+
0.059536,
|
| 193 |
+
-0.168096,
|
| 194 |
+
-0.031525,
|
| 195 |
+
-0.071924,
|
| 196 |
+
0.187083,
|
| 197 |
+
0.021963,
|
| 198 |
+
0.186838,
|
| 199 |
+
-0.039138,
|
| 200 |
+
-0.220178,
|
| 201 |
+
0.139961,
|
| 202 |
+
0.088891,
|
| 203 |
+
0.054164,
|
| 204 |
+
-0.067481,
|
| 205 |
+
0.165844,
|
| 206 |
+
-0.132874,
|
| 207 |
+
0.00983,
|
| 208 |
+
0.22256,
|
| 209 |
+
0.060354,
|
| 210 |
+
0.068444,
|
| 211 |
+
-0.010999,
|
| 212 |
+
0.194253,
|
| 213 |
+
-0.27535,
|
| 214 |
+
-0.10218,
|
| 215 |
+
-0.077211,
|
| 216 |
+
0.000231,
|
| 217 |
+
-0.145139,
|
| 218 |
+
-0.249444,
|
| 219 |
+
0.22325,
|
| 220 |
+
0.016289,
|
| 221 |
+
-0.084124,
|
| 222 |
+
-0.087526,
|
| 223 |
+
0.210053,
|
| 224 |
+
-0.215475,
|
| 225 |
+
-0.15729,
|
| 226 |
+
-0.124322,
|
| 227 |
+
-0.173046,
|
| 228 |
+
0.182702,
|
| 229 |
+
-0.212797,
|
| 230 |
+
0.141195,
|
| 231 |
+
0.155048,
|
| 232 |
+
-0.039869,
|
| 233 |
+
-0.103284,
|
| 234 |
+
0.077888,
|
| 235 |
+
0.256192,
|
| 236 |
+
-0.125461,
|
| 237 |
+
0.197733,
|
| 238 |
+
0.219629,
|
| 239 |
+
0.007501,
|
| 240 |
+
-0.153288,
|
| 241 |
+
0.199373,
|
| 242 |
+
0.072393,
|
| 243 |
+
0.044252,
|
| 244 |
+
-0.002293,
|
| 245 |
+
-0.032271,
|
| 246 |
+
-0.276078,
|
| 247 |
+
0.040138,
|
| 248 |
+
-0.150994,
|
| 249 |
+
0.128878,
|
| 250 |
+
-0.031116,
|
| 251 |
+
-0.129651,
|
| 252 |
+
-0.149721,
|
| 253 |
+
0.128694,
|
| 254 |
+
0.040513,
|
| 255 |
+
0.044896,
|
| 256 |
+
0.04819,
|
| 257 |
+
-0.122257,
|
| 258 |
+
0.094014,
|
| 259 |
+
0.024673,
|
| 260 |
+
-0.085999,
|
| 261 |
+
0.269909,
|
| 262 |
+
-0.016975,
|
| 263 |
+
-0.143176,
|
| 264 |
+
-0.143257,
|
| 265 |
+
0.100177,
|
| 266 |
+
0.049843,
|
| 267 |
+
-0.141697,
|
| 268 |
+
-0.148462,
|
| 269 |
+
0.191348,
|
| 270 |
+
0.05774,
|
| 271 |
+
-0.066514,
|
| 272 |
+
0.06819,
|
| 273 |
+
-0.019659,
|
| 274 |
+
-0.15781,
|
| 275 |
+
0.167692,
|
| 276 |
+
-0.044084,
|
| 277 |
+
0.185789,
|
| 278 |
+
0.169589,
|
| 279 |
+
0.085906,
|
| 280 |
+
-0.156088,
|
| 281 |
+
-0.045654,
|
| 282 |
+
0.181734,
|
| 283 |
+
-0.090986,
|
| 284 |
+
0.122933,
|
| 285 |
+
-0.186479,
|
| 286 |
+
-0.035989,
|
| 287 |
+
0.098177,
|
| 288 |
+
0.121564,
|
| 289 |
+
-0.093159,
|
| 290 |
+
-0.142898,
|
| 291 |
+
0.112587,
|
| 292 |
+
-0.242465,
|
| 293 |
+
0.127805,
|
| 294 |
+
0.142723,
|
| 295 |
+
0.022688,
|
| 296 |
+
-0.178088,
|
| 297 |
+
0.091873,
|
| 298 |
+
-0.125931,
|
| 299 |
+
-0.04513,
|
| 300 |
+
-0.050111,
|
| 301 |
+
-0.107747,
|
| 302 |
+
0.06705,
|
| 303 |
+
0.040281,
|
| 304 |
+
0.135995,
|
| 305 |
+
0.16423,
|
| 306 |
+
0.210625,
|
| 307 |
+
0.208269
|
| 308 |
+
],
|
| 309 |
+
"t_interpolation": "trilinear",
|
| 310 |
+
"upsample_residual": true
|
| 311 |
+
}
|
diffusion_pytorch_model.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a46ea56315f911a7fe79af796668a9bbed5b68b79d4031dd8259651291baa8b9
|
| 3 |
+
size 4975284468
|
modeling_motifvae.py
ADDED
|
@@ -0,0 +1,1132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""MotifVAE — a 3D causal video VAE for HuggingFace ``AutoModel``.
|
| 2 |
+
|
| 3 |
+
MotifVAE compresses video by 4x in time and 32x in each spatial dimension
|
| 4 |
+
into a 128-channel deterministic latent. The
|
| 5 |
+
encoder is fully 3D causal and uses a parameter-free Global Skip Connection
|
| 6 |
+
(GSC) at every down stage; the decoder mirrors the stage layout but is
|
| 7 |
+
deliberately wider (asymmetric, decoder-heavy). Spatial upsampling is
|
| 8 |
+
sub-pixel convolution with ICNR initialisation to suppress checkerboard
|
| 9 |
+
artifacts.
|
| 10 |
+
|
| 11 |
+
The whole model lives in this single file so the released checkpoint loads
|
| 12 |
+
straight from the Hub via remote code::
|
| 13 |
+
|
| 14 |
+
import diffusers
|
| 15 |
+
vae = diffusers.AutoModel.from_pretrained("<repo>", trust_remote_code=True)
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from dataclasses import dataclass
|
| 19 |
+
from typing import List, Literal, Optional, Tuple, Union
|
| 20 |
+
|
| 21 |
+
import torch
|
| 22 |
+
import torch.nn as nn
|
| 23 |
+
import torch.nn.functional as F
|
| 24 |
+
from einops import rearrange
|
| 25 |
+
|
| 26 |
+
from diffusers import ConfigMixin, ModelMixin
|
| 27 |
+
from diffusers.configuration_utils import register_to_config
|
| 28 |
+
from diffusers.utils import BaseOutput
|
| 29 |
+
|
| 30 |
+
|
| 31 |
+
# ---------------------------------------------------------------------------
|
| 32 |
+
# Block-type resolution
|
| 33 |
+
# ---------------------------------------------------------------------------
|
| 34 |
+
#
|
| 35 |
+
# The config encodes residual / mid block choices as strings (e.g.
|
| 36 |
+
# ``"ResnetBlock2D"``). ``resolve_str_to_obj`` maps those strings to the
|
| 37 |
+
# classes defined in this module. ``"Identity"`` maps to ``nn.Identity`` so a
|
| 38 |
+
# mid stage can be made attention-free while keeping the surrounding key
|
| 39 |
+
# layout intact.
|
| 40 |
+
|
| 41 |
+
_STR_TO_OBJ_OVERRIDES = {"Identity": nn.Identity}
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
def resolve_str_to_obj(name: str):
|
| 45 |
+
"""Resolve a block-type string from the config to its class."""
|
| 46 |
+
if name in _STR_TO_OBJ_OVERRIDES:
|
| 47 |
+
return _STR_TO_OBJ_OVERRIDES[name]
|
| 48 |
+
try:
|
| 49 |
+
return globals()[name]
|
| 50 |
+
except KeyError as exc:
|
| 51 |
+
raise AttributeError(
|
| 52 |
+
f"resolve_str_to_obj: unknown class name {name!r} "
|
| 53 |
+
f"(not defined in this module)"
|
| 54 |
+
) from exc
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
class VideoBaseAE(ModelMixin, ConfigMixin):
|
| 58 |
+
"""Base class wiring diffusers' ``ModelMixin`` + ``ConfigMixin`` together."""
|
| 59 |
+
|
| 60 |
+
config_name = "config.json"
|
| 61 |
+
|
| 62 |
+
def __init__(self, *args, **kwargs) -> None:
|
| 63 |
+
super().__init__(*args, **kwargs)
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# ---------------------------------------------------------------------------
|
| 67 |
+
# Primitive ops
|
| 68 |
+
# ---------------------------------------------------------------------------
|
| 69 |
+
|
| 70 |
+
|
| 71 |
+
def video_to_image(func):
|
| 72 |
+
"""Wrap a 2D forward so it also accepts 5D ``(B, C, T, H, W)`` tensors.
|
| 73 |
+
|
| 74 |
+
Frames are folded into the batch dimension, processed independently, then
|
| 75 |
+
unfolded — letting purely spatial layers operate transparently on video.
|
| 76 |
+
"""
|
| 77 |
+
|
| 78 |
+
def wrapper(self, x, *args, **kwargs):
|
| 79 |
+
if x.dim() == 5:
|
| 80 |
+
t = x.shape[2]
|
| 81 |
+
x = rearrange(x, "b c t h w -> (b t) c h w").contiguous()
|
| 82 |
+
x = func(self, x, *args, **kwargs)
|
| 83 |
+
x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
|
| 84 |
+
else:
|
| 85 |
+
x = func(self, x, *args, **kwargs)
|
| 86 |
+
return x
|
| 87 |
+
|
| 88 |
+
return wrapper
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
def nonlinearity(x: torch.Tensor) -> torch.Tensor:
|
| 92 |
+
"""SiLU / swish activation used throughout the network."""
|
| 93 |
+
return x * torch.sigmoid(x)
|
| 94 |
+
|
| 95 |
+
|
| 96 |
+
def cast_tuple(value, length: int = 1):
|
| 97 |
+
"""Broadcast a scalar to a length-``length`` tuple; pass tuples through."""
|
| 98 |
+
return value if isinstance(value, (tuple, list)) else ((value,) * length)
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
# ---------------------------------------------------------------------------
|
| 102 |
+
# Convolutions
|
| 103 |
+
# ---------------------------------------------------------------------------
|
| 104 |
+
|
| 105 |
+
|
| 106 |
+
class Conv2d(nn.Conv2d):
|
| 107 |
+
"""``nn.Conv2d`` that also accepts 5D video tensors (per-frame)."""
|
| 108 |
+
|
| 109 |
+
def __init__(
|
| 110 |
+
self,
|
| 111 |
+
in_channels: int,
|
| 112 |
+
out_channels: int,
|
| 113 |
+
kernel_size: Union[int, Tuple[int]] = 3,
|
| 114 |
+
stride: Union[int, Tuple[int]] = 1,
|
| 115 |
+
padding: Union[str, int, Tuple[int]] = 0,
|
| 116 |
+
dilation: Union[int, Tuple[int]] = 1,
|
| 117 |
+
groups: int = 1,
|
| 118 |
+
bias: bool = True,
|
| 119 |
+
padding_mode: str = "zeros",
|
| 120 |
+
device=None,
|
| 121 |
+
dtype=None,
|
| 122 |
+
) -> None:
|
| 123 |
+
super().__init__(
|
| 124 |
+
in_channels,
|
| 125 |
+
out_channels,
|
| 126 |
+
kernel_size,
|
| 127 |
+
stride,
|
| 128 |
+
padding,
|
| 129 |
+
dilation,
|
| 130 |
+
groups,
|
| 131 |
+
bias,
|
| 132 |
+
padding_mode,
|
| 133 |
+
device,
|
| 134 |
+
dtype,
|
| 135 |
+
)
|
| 136 |
+
|
| 137 |
+
@video_to_image
|
| 138 |
+
def forward(self, x):
|
| 139 |
+
return super().forward(x)
|
| 140 |
+
|
| 141 |
+
|
| 142 |
+
class CausalConv3d(nn.Module):
|
| 143 |
+
"""A plain 3D convolution wrapped to keep input dtype aligned with weights.
|
| 144 |
+
|
| 145 |
+
The wrapper exists so spatial/temporal padding is configured consistently
|
| 146 |
+
across the network and so bf16/fp32 mismatches under AMP are resolved by
|
| 147 |
+
casting the input to the convolution's weight dtype.
|
| 148 |
+
"""
|
| 149 |
+
|
| 150 |
+
def __init__(
|
| 151 |
+
self,
|
| 152 |
+
chan_in: int,
|
| 153 |
+
chan_out: int,
|
| 154 |
+
kernel_size: Union[int, Tuple[int, int, int]],
|
| 155 |
+
bias: bool = True,
|
| 156 |
+
**kwargs,
|
| 157 |
+
) -> None:
|
| 158 |
+
super().__init__()
|
| 159 |
+
self.kernel_size = cast_tuple(kernel_size, 3)
|
| 160 |
+
self.time_kernel_size = self.kernel_size[0]
|
| 161 |
+
self.chan_in = chan_in
|
| 162 |
+
self.chan_out = chan_out
|
| 163 |
+
stride = cast_tuple(kwargs.pop("stride", 1), 3)
|
| 164 |
+
padding = list(cast_tuple(kwargs.pop("padding", 0), 3)) # (T, H, W)
|
| 165 |
+
self.stride = stride
|
| 166 |
+
self.padding = padding
|
| 167 |
+
self.conv = nn.Conv3d(
|
| 168 |
+
chan_in,
|
| 169 |
+
chan_out,
|
| 170 |
+
self.kernel_size,
|
| 171 |
+
stride=stride,
|
| 172 |
+
padding=padding,
|
| 173 |
+
bias=bias,
|
| 174 |
+
)
|
| 175 |
+
|
| 176 |
+
def forward(self, x):
|
| 177 |
+
if x.dtype is not self.conv.weight.dtype:
|
| 178 |
+
x = x.to(dtype=self.conv.weight.dtype)
|
| 179 |
+
return self.conv(x)
|
| 180 |
+
|
| 181 |
+
|
| 182 |
+
# ---------------------------------------------------------------------------
|
| 183 |
+
# Normalisation
|
| 184 |
+
# ---------------------------------------------------------------------------
|
| 185 |
+
|
| 186 |
+
|
| 187 |
+
class LayerNorm(nn.Module):
|
| 188 |
+
"""Channel-last layer normalisation for both 4D and 5D tensors."""
|
| 189 |
+
|
| 190 |
+
def __init__(self, num_channels, eps=1e-6, *args, **kwargs) -> None:
|
| 191 |
+
super().__init__(*args, **kwargs)
|
| 192 |
+
self.norm = torch.nn.LayerNorm(num_channels, eps=eps, elementwise_affine=True)
|
| 193 |
+
|
| 194 |
+
def forward(self, x):
|
| 195 |
+
if x.dim() == 5:
|
| 196 |
+
x = rearrange(x, "b c t h w -> b t h w c")
|
| 197 |
+
x = self.norm(x)
|
| 198 |
+
x = rearrange(x, "b t h w c -> b c t h w")
|
| 199 |
+
else:
|
| 200 |
+
x = rearrange(x, "b c h w -> b h w c")
|
| 201 |
+
x = self.norm(x)
|
| 202 |
+
x = rearrange(x, "b h w c -> b c h w")
|
| 203 |
+
return x
|
| 204 |
+
|
| 205 |
+
|
| 206 |
+
def Normalize(in_channels, num_groups=32, norm_type="groupnorm"):
|
| 207 |
+
"""Build the normalisation layer selected by ``norm_type``."""
|
| 208 |
+
if norm_type == "groupnorm":
|
| 209 |
+
return torch.nn.GroupNorm(
|
| 210 |
+
num_groups=num_groups, num_channels=in_channels, eps=1e-6, affine=True
|
| 211 |
+
)
|
| 212 |
+
elif norm_type == "layernorm":
|
| 213 |
+
return LayerNorm(num_channels=in_channels, eps=1e-6)
|
| 214 |
+
|
| 215 |
+
|
| 216 |
+
# ---------------------------------------------------------------------------
|
| 217 |
+
# Up / down sampling
|
| 218 |
+
# ---------------------------------------------------------------------------
|
| 219 |
+
#
|
| 220 |
+
# Spatial upsampling is sub-pixel convolution everywhere: a stride-1 conv that
|
| 221 |
+
# expands channels by ``r**2`` followed by a pixel shuffle and ICNR
|
| 222 |
+
# initialisation. This is the only spatial upsample mode the model ships.
|
| 223 |
+
|
| 224 |
+
|
| 225 |
+
def spatial_pixel_shuffle_5d(x: torch.Tensor, upscale_factor: int = 2) -> torch.Tensor:
|
| 226 |
+
"""Apply a spatial-only pixel shuffle to a 5D ``(B, C*r**2, T, H, W)`` tensor.
|
| 227 |
+
|
| 228 |
+
Returns ``(B, C, T, H*r, W*r)`` — equivalent to running
|
| 229 |
+
``F.pixel_shuffle`` independently on every frame.
|
| 230 |
+
"""
|
| 231 |
+
return rearrange(
|
| 232 |
+
x,
|
| 233 |
+
"b (c r1 r2) t h w -> b c t (h r1) (w r2)",
|
| 234 |
+
r1=upscale_factor,
|
| 235 |
+
r2=upscale_factor,
|
| 236 |
+
)
|
| 237 |
+
|
| 238 |
+
|
| 239 |
+
def icnr_init_(weight: torch.Tensor, upscale_factor: int = 2) -> None:
|
| 240 |
+
"""Initialise a sub-pixel conv weight with ICNR (Aitken et al., 2017).
|
| 241 |
+
|
| 242 |
+
The ``r**2`` output-channel groups that the pixel shuffle redistributes
|
| 243 |
+
into one ``r x r`` block are initialised identically, so the layer behaves
|
| 244 |
+
as nearest-neighbour upsampling at initialisation (no checkerboard).
|
| 245 |
+
|
| 246 |
+
Works for 2D and 3D kernels alike: only the leading channel dimension that
|
| 247 |
+
the shuffle reorders is touched, leaving any temporal axis untouched.
|
| 248 |
+
"""
|
| 249 |
+
out_full = weight.shape[0]
|
| 250 |
+
r2 = upscale_factor ** 2
|
| 251 |
+
if out_full % r2 != 0:
|
| 252 |
+
raise ValueError(
|
| 253 |
+
f"icnr_init_: out_channels={out_full} must be divisible by "
|
| 254 |
+
f"upscale_factor**2={r2}"
|
| 255 |
+
)
|
| 256 |
+
sub = out_full // r2
|
| 257 |
+
base = torch.empty(sub, *weight.shape[1:], device=weight.device, dtype=weight.dtype)
|
| 258 |
+
nn.init.kaiming_normal_(base)
|
| 259 |
+
with torch.no_grad():
|
| 260 |
+
weight.copy_(base.repeat_interleave(r2, dim=0))
|
| 261 |
+
|
| 262 |
+
|
| 263 |
+
class Upsample(nn.Module):
|
| 264 |
+
"""2x spatial upsample (image branch) via sub-pixel convolution.
|
| 265 |
+
|
| 266 |
+
``Conv2d(out=C*4) -> PixelShuffle(2)`` with ICNR init. When ``residual``
|
| 267 |
+
is set, a parameter-free nearest-neighbour identity skip is added on top,
|
| 268 |
+
scaled by a non-learnable ``residual_alpha`` buffer. The buffer is
|
| 269 |
+
non-persistent so a checkpoint saved without it still loads cleanly.
|
| 270 |
+
"""
|
| 271 |
+
|
| 272 |
+
def __init__(self, in_channels, out_channels, residual: bool = False, **kwargs):
|
| 273 |
+
super().__init__()
|
| 274 |
+
self.upscale_factor = 2
|
| 275 |
+
self.conv = torch.nn.Conv2d(
|
| 276 |
+
in_channels,
|
| 277 |
+
out_channels * (self.upscale_factor ** 2),
|
| 278 |
+
kernel_size=3,
|
| 279 |
+
stride=1,
|
| 280 |
+
padding=1,
|
| 281 |
+
)
|
| 282 |
+
icnr_init_(self.conv.weight, self.upscale_factor)
|
| 283 |
+
if self.conv.bias is not None:
|
| 284 |
+
nn.init.zeros_(self.conv.bias)
|
| 285 |
+
self.shuffle = nn.PixelShuffle(self.upscale_factor)
|
| 286 |
+
|
| 287 |
+
self.residual = residual
|
| 288 |
+
self._in_channels = int(in_channels)
|
| 289 |
+
self._out_channels = int(out_channels)
|
| 290 |
+
if residual:
|
| 291 |
+
self.register_buffer("residual_alpha", torch.tensor(1.0), persistent=False)
|
| 292 |
+
|
| 293 |
+
def _identity_residual(self, x: torch.Tensor) -> torch.Tensor:
|
| 294 |
+
"""Nearest 2x spatial upsample plus channel match to ``out_channels``.
|
| 295 |
+
|
| 296 |
+
The canonical configs use ``in_c == out_c`` for every up block, so only
|
| 297 |
+
the passthrough branch is exercised; the channel-average / repeat
|
| 298 |
+
branches are kept for configs whose stages change channel count.
|
| 299 |
+
"""
|
| 300 |
+
in_c = x.shape[1]
|
| 301 |
+
out_c = self._out_channels
|
| 302 |
+
if in_c == out_c:
|
| 303 |
+
r = x
|
| 304 |
+
elif in_c > out_c:
|
| 305 |
+
if in_c % out_c != 0:
|
| 306 |
+
raise ValueError(
|
| 307 |
+
f"residual channel-average failed: in_c={in_c} not divisible "
|
| 308 |
+
f"by out_c={out_c}; configure matching channels or disable residual."
|
| 309 |
+
)
|
| 310 |
+
r = x.reshape(x.shape[0], out_c, in_c // out_c, *x.shape[2:]).mean(dim=2)
|
| 311 |
+
else:
|
| 312 |
+
if out_c % in_c != 0:
|
| 313 |
+
raise ValueError(
|
| 314 |
+
f"residual channel-repeat failed: out_c={out_c} not divisible "
|
| 315 |
+
f"by in_c={in_c}; configure matching channels or disable residual."
|
| 316 |
+
)
|
| 317 |
+
r = x.repeat_interleave(out_c // in_c, dim=1)
|
| 318 |
+
return F.interpolate(r, scale_factor=2.0, mode="nearest")
|
| 319 |
+
|
| 320 |
+
@video_to_image
|
| 321 |
+
def forward(self, x):
|
| 322 |
+
h = self.shuffle(self.conv(x))
|
| 323 |
+
if self.residual:
|
| 324 |
+
# Cast alpha to the main path's dtype so an AMP-bf16 forward is not
|
| 325 |
+
# silently promoted to fp32 by the float buffer.
|
| 326 |
+
alpha = self.residual_alpha.to(dtype=h.dtype, device=h.device)
|
| 327 |
+
h = h + alpha * self._identity_residual(x).to(dtype=h.dtype)
|
| 328 |
+
return h
|
| 329 |
+
|
| 330 |
+
|
| 331 |
+
class Downsample(nn.Module):
|
| 332 |
+
"""2x spatial downsample (encoder): asymmetric zero-pad then stride-2 conv."""
|
| 333 |
+
|
| 334 |
+
def __init__(self, in_channels, out_channels, **kwargs):
|
| 335 |
+
super().__init__()
|
| 336 |
+
self.conv = torch.nn.Conv2d(
|
| 337 |
+
in_channels, out_channels, kernel_size=3, stride=2, padding=0
|
| 338 |
+
)
|
| 339 |
+
|
| 340 |
+
@video_to_image
|
| 341 |
+
def forward(self, x):
|
| 342 |
+
x = F.pad(x, (0, 1, 0, 1), mode="constant", value=0)
|
| 343 |
+
return self.conv(x)
|
| 344 |
+
|
| 345 |
+
|
| 346 |
+
class Spatial2xTime2x3DDownsample(nn.Module):
|
| 347 |
+
"""Joint 2x spatial + 2x temporal downsample via a stride-2 causal conv."""
|
| 348 |
+
|
| 349 |
+
def __init__(self, in_channels, out_channels):
|
| 350 |
+
super().__init__()
|
| 351 |
+
self.conv = CausalConv3d(
|
| 352 |
+
in_channels, out_channels, kernel_size=3, padding=0, stride=2
|
| 353 |
+
)
|
| 354 |
+
|
| 355 |
+
def forward(self, x):
|
| 356 |
+
x = F.pad(x, (0, 1, 0, 1, 1, 1), mode="constant", value=0)
|
| 357 |
+
return self.conv(x)
|
| 358 |
+
|
| 359 |
+
|
| 360 |
+
class Spatial2xTime2x3DUpsample(nn.Module):
|
| 361 |
+
"""Joint spatial + temporal 2x upsample.
|
| 362 |
+
|
| 363 |
+
The temporal axis is stretched with ``F.interpolate`` (cheap and smooth);
|
| 364 |
+
the spatial 2x is sub-pixel convolution — a ``CausalConv3d`` expanding
|
| 365 |
+
channels by 4 followed by a per-frame pixel shuffle — applied after the
|
| 366 |
+
temporal stretch. An optional parameter-free identity residual (see
|
| 367 |
+
:class:`Upsample`) is computed against the post-temporal tensor.
|
| 368 |
+
"""
|
| 369 |
+
|
| 370 |
+
def __init__(
|
| 371 |
+
self,
|
| 372 |
+
in_channels,
|
| 373 |
+
out_channels,
|
| 374 |
+
t_interpolation="trilinear",
|
| 375 |
+
residual: bool = False,
|
| 376 |
+
):
|
| 377 |
+
super().__init__()
|
| 378 |
+
self.t_interpolation = t_interpolation
|
| 379 |
+
self.upscale_factor = 2
|
| 380 |
+
# Produce 4x out_channels so the per-frame pixel shuffle recovers
|
| 381 |
+
# out_channels at 2x spatial resolution.
|
| 382 |
+
self.conv = CausalConv3d(
|
| 383 |
+
in_channels,
|
| 384 |
+
out_channels * (self.upscale_factor ** 2),
|
| 385 |
+
kernel_size=3,
|
| 386 |
+
padding=1,
|
| 387 |
+
)
|
| 388 |
+
icnr_init_(self.conv.conv.weight, self.upscale_factor)
|
| 389 |
+
if self.conv.conv.bias is not None:
|
| 390 |
+
nn.init.zeros_(self.conv.conv.bias)
|
| 391 |
+
|
| 392 |
+
self.residual = residual
|
| 393 |
+
self._in_channels = int(in_channels)
|
| 394 |
+
self._out_channels = int(out_channels)
|
| 395 |
+
if residual:
|
| 396 |
+
self.register_buffer("residual_alpha", torch.tensor(1.0), persistent=False)
|
| 397 |
+
|
| 398 |
+
def _identity_residual_5d(self, x: torch.Tensor) -> torch.Tensor:
|
| 399 |
+
"""Nearest 2x spatial upsample on a 5D tensor plus channel match.
|
| 400 |
+
|
| 401 |
+
Called on the post-temporal tensor so its ``T`` axis already matches
|
| 402 |
+
the main path. See :meth:`Upsample._identity_residual` for the
|
| 403 |
+
channel-match rule.
|
| 404 |
+
"""
|
| 405 |
+
in_c = x.shape[1]
|
| 406 |
+
out_c = self._out_channels
|
| 407 |
+
if in_c == out_c:
|
| 408 |
+
r = x
|
| 409 |
+
elif in_c > out_c:
|
| 410 |
+
if in_c % out_c != 0:
|
| 411 |
+
raise ValueError(
|
| 412 |
+
f"residual channel-average failed: in_c={in_c} not divisible "
|
| 413 |
+
f"by out_c={out_c}; configure matching channels or disable residual."
|
| 414 |
+
)
|
| 415 |
+
r = x.reshape(x.shape[0], out_c, in_c // out_c, *x.shape[2:]).mean(dim=2)
|
| 416 |
+
else:
|
| 417 |
+
if out_c % in_c != 0:
|
| 418 |
+
raise ValueError(
|
| 419 |
+
f"residual channel-repeat failed: out_c={out_c} not divisible "
|
| 420 |
+
f"by in_c={in_c}; configure matching channels or disable residual."
|
| 421 |
+
)
|
| 422 |
+
r = x.repeat_interleave(out_c // in_c, dim=1)
|
| 423 |
+
return F.interpolate(r, scale_factor=(1, 2, 2), mode="nearest")
|
| 424 |
+
|
| 425 |
+
def forward(self, x):
|
| 426 |
+
# Temporal upsample. A single-frame (image) input has nothing to
|
| 427 |
+
# stretch; an even frame count upsamples directly; an odd frame count
|
| 428 |
+
# is split so the leading frame is preserved before stretching.
|
| 429 |
+
if x.size(2) > 1:
|
| 430 |
+
T = x.size(2)
|
| 431 |
+
if T % 2 == 0:
|
| 432 |
+
x = F.interpolate(x, scale_factor=(2, 1, 1), mode=self.t_interpolation)
|
| 433 |
+
else:
|
| 434 |
+
x, x_tail = x[:, :, :1], x[:, :, 1:]
|
| 435 |
+
x_tail = F.interpolate(
|
| 436 |
+
x_tail, scale_factor=(2, 1, 1), mode=self.t_interpolation
|
| 437 |
+
)
|
| 438 |
+
x = torch.cat([x, x_tail], dim=2)
|
| 439 |
+
|
| 440 |
+
# Spatial sub-pixel upsample. The temporal stretch is shared between
|
| 441 |
+
# the main and residual paths, so the residual only mirrors the
|
| 442 |
+
# spatial branch.
|
| 443 |
+
h = spatial_pixel_shuffle_5d(self.conv(x), self.upscale_factor)
|
| 444 |
+
if self.residual:
|
| 445 |
+
alpha = self.residual_alpha.to(dtype=h.dtype, device=h.device)
|
| 446 |
+
h = h + alpha * self._identity_residual_5d(x).to(dtype=h.dtype)
|
| 447 |
+
return h
|
| 448 |
+
|
| 449 |
+
|
| 450 |
+
# ---------------------------------------------------------------------------
|
| 451 |
+
# Residual blocks
|
| 452 |
+
# ---------------------------------------------------------------------------
|
| 453 |
+
|
| 454 |
+
|
| 455 |
+
class ResnetBlock2D(nn.Module):
|
| 456 |
+
"""Pre-activation residual block over 2D (or per-frame 5D) features."""
|
| 457 |
+
|
| 458 |
+
def __init__(
|
| 459 |
+
self,
|
| 460 |
+
*,
|
| 461 |
+
in_channels,
|
| 462 |
+
out_channels=None,
|
| 463 |
+
conv_shortcut=False,
|
| 464 |
+
norm_type,
|
| 465 |
+
dropout,
|
| 466 |
+
):
|
| 467 |
+
super().__init__()
|
| 468 |
+
self.in_channels = in_channels
|
| 469 |
+
self.out_channels = in_channels if out_channels is None else out_channels
|
| 470 |
+
self.use_conv_shortcut = conv_shortcut
|
| 471 |
+
|
| 472 |
+
self.norm1 = Normalize(in_channels, norm_type=norm_type)
|
| 473 |
+
self.conv1 = torch.nn.Conv2d(
|
| 474 |
+
in_channels, out_channels, kernel_size=3, stride=1, padding=1
|
| 475 |
+
)
|
| 476 |
+
self.norm2 = Normalize(out_channels, norm_type=norm_type)
|
| 477 |
+
self.dropout = torch.nn.Dropout(dropout)
|
| 478 |
+
self.conv2 = torch.nn.Conv2d(
|
| 479 |
+
out_channels, out_channels, kernel_size=3, stride=1, padding=1
|
| 480 |
+
)
|
| 481 |
+
if self.in_channels != self.out_channels:
|
| 482 |
+
if self.use_conv_shortcut:
|
| 483 |
+
self.conv_shortcut = torch.nn.Conv2d(
|
| 484 |
+
in_channels, out_channels, kernel_size=3, stride=1, padding=1
|
| 485 |
+
)
|
| 486 |
+
else:
|
| 487 |
+
self.nin_shortcut = torch.nn.Conv2d(
|
| 488 |
+
in_channels, out_channels, kernel_size=1, stride=1, padding=0
|
| 489 |
+
)
|
| 490 |
+
|
| 491 |
+
@video_to_image
|
| 492 |
+
def forward(self, x):
|
| 493 |
+
h = self.norm1(x)
|
| 494 |
+
h = nonlinearity(h)
|
| 495 |
+
h = self.conv1(h)
|
| 496 |
+
h = self.norm2(h)
|
| 497 |
+
h = nonlinearity(h)
|
| 498 |
+
h = self.dropout(h)
|
| 499 |
+
h = self.conv2(h)
|
| 500 |
+
if self.in_channels != self.out_channels:
|
| 501 |
+
if self.use_conv_shortcut:
|
| 502 |
+
x = self.conv_shortcut(x)
|
| 503 |
+
else:
|
| 504 |
+
x = self.nin_shortcut(x)
|
| 505 |
+
return x + h
|
| 506 |
+
|
| 507 |
+
|
| 508 |
+
class ResnetBlock3D(nn.Module):
|
| 509 |
+
"""Pre-activation residual block over 3D features (causal convs)."""
|
| 510 |
+
|
| 511 |
+
def __init__(
|
| 512 |
+
self,
|
| 513 |
+
*,
|
| 514 |
+
in_channels,
|
| 515 |
+
out_channels=None,
|
| 516 |
+
conv_shortcut=False,
|
| 517 |
+
dropout,
|
| 518 |
+
norm_type,
|
| 519 |
+
):
|
| 520 |
+
super().__init__()
|
| 521 |
+
self.in_channels = in_channels
|
| 522 |
+
self.out_channels = in_channels if out_channels is None else out_channels
|
| 523 |
+
self.use_conv_shortcut = conv_shortcut
|
| 524 |
+
|
| 525 |
+
self.norm1 = Normalize(in_channels, norm_type=norm_type)
|
| 526 |
+
self.conv1 = CausalConv3d(in_channels, out_channels, 3, padding=1)
|
| 527 |
+
self.norm2 = Normalize(out_channels, norm_type=norm_type)
|
| 528 |
+
self.dropout = torch.nn.Dropout(dropout)
|
| 529 |
+
self.conv2 = CausalConv3d(out_channels, out_channels, 3, padding=1)
|
| 530 |
+
if self.in_channels != self.out_channels:
|
| 531 |
+
if self.use_conv_shortcut:
|
| 532 |
+
self.conv_shortcut = CausalConv3d(in_channels, out_channels, 3, padding=1)
|
| 533 |
+
else:
|
| 534 |
+
self.nin_shortcut = CausalConv3d(in_channels, out_channels, 1, padding=0)
|
| 535 |
+
|
| 536 |
+
def forward(self, x):
|
| 537 |
+
h = self.norm1(x)
|
| 538 |
+
h = nonlinearity(h)
|
| 539 |
+
h = self.conv1(h)
|
| 540 |
+
h = self.norm2(h)
|
| 541 |
+
h = nonlinearity(h)
|
| 542 |
+
h = self.dropout(h)
|
| 543 |
+
h = self.conv2(h)
|
| 544 |
+
if self.in_channels != self.out_channels:
|
| 545 |
+
if self.use_conv_shortcut:
|
| 546 |
+
x = self.conv_shortcut(x)
|
| 547 |
+
else:
|
| 548 |
+
x = self.nin_shortcut(x)
|
| 549 |
+
return x + h
|
| 550 |
+
|
| 551 |
+
|
| 552 |
+
# ---------------------------------------------------------------------------
|
| 553 |
+
# Output containers
|
| 554 |
+
# ---------------------------------------------------------------------------
|
| 555 |
+
#
|
| 556 |
+
# The encoder is deterministic, so ``latent_dist`` always carries a
|
| 557 |
+
# ``DeterministicLatent`` (defined further down). The annotation is a forward
|
| 558 |
+
# reference; it has no runtime effect.
|
| 559 |
+
|
| 560 |
+
|
| 561 |
+
@dataclass
|
| 562 |
+
class AutoencoderKLOutput(BaseOutput):
|
| 563 |
+
latent_dist: "DeterministicLatent"
|
| 564 |
+
extra_output: Optional[tuple] = None
|
| 565 |
+
|
| 566 |
+
|
| 567 |
+
@dataclass
|
| 568 |
+
class DecoderOutput(BaseOutput):
|
| 569 |
+
sample: torch.Tensor
|
| 570 |
+
extra_output: Optional[tuple] = None
|
| 571 |
+
|
| 572 |
+
|
| 573 |
+
@dataclass
|
| 574 |
+
class ForwardOutput(BaseOutput):
|
| 575 |
+
sample: torch.Tensor
|
| 576 |
+
latent_dist: "DeterministicLatent"
|
| 577 |
+
extra_output: Optional[tuple] = None
|
| 578 |
+
# The exact latent consumed by the decoder.
|
| 579 |
+
sampled_latent: Optional[torch.Tensor] = None
|
| 580 |
+
|
| 581 |
+
|
| 582 |
+
# ---------------------------------------------------------------------------
|
| 583 |
+
# Latent wrapper
|
| 584 |
+
# ---------------------------------------------------------------------------
|
| 585 |
+
|
| 586 |
+
|
| 587 |
+
class DeterministicLatent:
|
| 588 |
+
"""Latent wrapper exposing the posterior API used by ``encode``.
|
| 589 |
+
|
| 590 |
+
The encoder emits the latent directly (no mean/log-variance split). This
|
| 591 |
+
wrapper exposes ``sample`` / ``mode`` (both returning the latent verbatim)
|
| 592 |
+
and a ``mean`` alias.
|
| 593 |
+
"""
|
| 594 |
+
|
| 595 |
+
def __init__(self, x: torch.Tensor):
|
| 596 |
+
self.x = x
|
| 597 |
+
self.mean = x
|
| 598 |
+
self.parameters = x
|
| 599 |
+
|
| 600 |
+
def sample(self) -> torch.Tensor:
|
| 601 |
+
return self.x
|
| 602 |
+
|
| 603 |
+
def mode(self) -> torch.Tensor:
|
| 604 |
+
return self.x
|
| 605 |
+
|
| 606 |
+
|
| 607 |
+
# ---------------------------------------------------------------------------
|
| 608 |
+
# Encoder / decoder building blocks
|
| 609 |
+
# ---------------------------------------------------------------------------
|
| 610 |
+
|
| 611 |
+
|
| 612 |
+
def build_mid_layer(
|
| 613 |
+
layer_type: str, channels: int, dropout: float, norm_type: str
|
| 614 |
+
) -> nn.Module:
|
| 615 |
+
"""Build one entry of a mid stack from its config string.
|
| 616 |
+
|
| 617 |
+
``"Identity"`` (and ``None``) build ``nn.Identity`` so a Res-Attn-Res mid
|
| 618 |
+
can be turned into an attention-free Res-pass-Res mid while preserving the
|
| 619 |
+
surrounding residual-block keys. Any other string resolves to a residual
|
| 620 |
+
block with matching in/out channels.
|
| 621 |
+
"""
|
| 622 |
+
if layer_type is None or layer_type == "Identity":
|
| 623 |
+
return nn.Identity()
|
| 624 |
+
return resolve_str_to_obj(layer_type)(
|
| 625 |
+
in_channels=channels,
|
| 626 |
+
out_channels=channels,
|
| 627 |
+
dropout=dropout,
|
| 628 |
+
norm_type=norm_type,
|
| 629 |
+
)
|
| 630 |
+
|
| 631 |
+
|
| 632 |
+
def pad_time_to_even(x: torch.Tensor) -> torch.Tensor:
|
| 633 |
+
"""Prepend a repeated leading frame so the temporal axis becomes even.
|
| 634 |
+
|
| 635 |
+
This matches the temporal arithmetic of ``Spatial2xTime2x3DDownsample``:
|
| 636 |
+
folding the time axis by 2 needs an even length, and pre-pending (rather
|
| 637 |
+
than appending) the repeated frame keeps the fold causal.
|
| 638 |
+
"""
|
| 639 |
+
if x.shape[2] % 2 == 0:
|
| 640 |
+
return x
|
| 641 |
+
first = x[:, :, :1].expand(-1, -1, 1, -1, -1)
|
| 642 |
+
return torch.cat([first, x], dim=2)
|
| 643 |
+
|
| 644 |
+
|
| 645 |
+
class MotifDownBlock(nn.Module):
|
| 646 |
+
"""Encoder down stage with a parameter-free Global Skip Connection.
|
| 647 |
+
|
| 648 |
+
The main path runs ``ResBlock(s) -> downsample -> ResBlock`` to reach
|
| 649 |
+
``out_channels``. In parallel, the block's own input is folded by
|
| 650 |
+
space-to-channel and channel-averaged to ``out_channels``, then added to
|
| 651 |
+
the main path. ``hw`` stages fold space only; ``thw`` stages also fold time
|
| 652 |
+
by 2 (after padding odd ``T``) to match the main path's joint downsample.
|
| 653 |
+
|
| 654 |
+
For the canonical config the space-to-channel fold over-produces channels
|
| 655 |
+
by an integer factor, so the alignment is always an exact channel-average;
|
| 656 |
+
a zero-pad fallback covers configs where it does not.
|
| 657 |
+
"""
|
| 658 |
+
|
| 659 |
+
def __init__(
|
| 660 |
+
self,
|
| 661 |
+
in_channels: int,
|
| 662 |
+
out_channels: int,
|
| 663 |
+
num_res_blocks: int = 2,
|
| 664 |
+
down_type: Literal["thw", "hw"] = "thw",
|
| 665 |
+
res_block: nn.Module = ResnetBlock3D,
|
| 666 |
+
dropout: float = 0.0,
|
| 667 |
+
norm_type: str = "layernorm",
|
| 668 |
+
) -> None:
|
| 669 |
+
super().__init__()
|
| 670 |
+
assert num_res_blocks >= 2, "num_res_blocks too small (need >= 2)"
|
| 671 |
+
self.in_channels = in_channels
|
| 672 |
+
self.out_channels = out_channels
|
| 673 |
+
self.down_type = down_type
|
| 674 |
+
|
| 675 |
+
# Pre-downsample residual stack (channel-preserving).
|
| 676 |
+
self.res_block = nn.Sequential(
|
| 677 |
+
*[
|
| 678 |
+
res_block(
|
| 679 |
+
in_channels=in_channels,
|
| 680 |
+
out_channels=in_channels,
|
| 681 |
+
dropout=dropout,
|
| 682 |
+
norm_type=norm_type,
|
| 683 |
+
)
|
| 684 |
+
for _ in range(num_res_blocks - 1)
|
| 685 |
+
]
|
| 686 |
+
)
|
| 687 |
+
|
| 688 |
+
if down_type == "thw":
|
| 689 |
+
self.down = Spatial2xTime2x3DDownsample(
|
| 690 |
+
in_channels=in_channels, out_channels=in_channels
|
| 691 |
+
)
|
| 692 |
+
elif down_type == "hw":
|
| 693 |
+
self.down = Downsample(in_channels=in_channels, out_channels=in_channels)
|
| 694 |
+
else:
|
| 695 |
+
raise ValueError(f"unknown down_type={down_type}")
|
| 696 |
+
|
| 697 |
+
# Post-downsample residual carrying the main path to out_channels.
|
| 698 |
+
self.out_res_block = res_block(
|
| 699 |
+
in_channels=in_channels,
|
| 700 |
+
out_channels=out_channels,
|
| 701 |
+
dropout=dropout,
|
| 702 |
+
norm_type=norm_type,
|
| 703 |
+
)
|
| 704 |
+
|
| 705 |
+
def _space_to_channel(self, raw: torch.Tensor) -> torch.Tensor:
|
| 706 |
+
"""Fold spatial (and, for ``thw``, temporal) resolution into channels."""
|
| 707 |
+
if self.down_type == "hw":
|
| 708 |
+
return rearrange(
|
| 709 |
+
raw, "b c t (h p1) (w p2) -> b (c p1 p2) t h w", p1=2, p2=2
|
| 710 |
+
)
|
| 711 |
+
raw = pad_time_to_even(raw)
|
| 712 |
+
return rearrange(
|
| 713 |
+
raw,
|
| 714 |
+
"b c (t p0) (h p1) (w p2) -> b (c p0 p1 p2) t h w",
|
| 715 |
+
p0=2,
|
| 716 |
+
p1=2,
|
| 717 |
+
p2=2,
|
| 718 |
+
)
|
| 719 |
+
|
| 720 |
+
def forward(self, h: torch.Tensor):
|
| 721 |
+
# The skip is tapped from the block input, before any transform.
|
| 722 |
+
h_in = h
|
| 723 |
+
|
| 724 |
+
h = self.res_block(h)
|
| 725 |
+
h = self.down(h)
|
| 726 |
+
h_main = self.out_res_block(h)
|
| 727 |
+
|
| 728 |
+
skip = self._space_to_channel(h_in)
|
| 729 |
+
out_c = h_main.shape[1]
|
| 730 |
+
skip_c = skip.shape[1]
|
| 731 |
+
if skip_c == out_c:
|
| 732 |
+
skip_aligned = skip
|
| 733 |
+
elif skip_c > out_c:
|
| 734 |
+
assert skip_c % out_c == 0, (
|
| 735 |
+
f"GSC channel-average failed: skip_c={skip_c} not divisible "
|
| 736 |
+
f"by out_c={out_c}. Re-check base_channels and layer_type."
|
| 737 |
+
)
|
| 738 |
+
g = skip_c // out_c
|
| 739 |
+
skip_aligned = skip.reshape(
|
| 740 |
+
skip.shape[0], out_c, g, *skip.shape[2:]
|
| 741 |
+
).mean(dim=2)
|
| 742 |
+
else:
|
| 743 |
+
# Zero-pad fallback for configs whose fold under-produces channels.
|
| 744 |
+
pad = torch.zeros(
|
| 745 |
+
skip.shape[0],
|
| 746 |
+
out_c - skip_c,
|
| 747 |
+
*skip.shape[2:],
|
| 748 |
+
dtype=skip.dtype,
|
| 749 |
+
device=skip.device,
|
| 750 |
+
)
|
| 751 |
+
skip_aligned = torch.cat([skip, pad], dim=1)
|
| 752 |
+
|
| 753 |
+
assert skip_aligned.shape[2:] == h_main.shape[2:], (
|
| 754 |
+
f"GSC skip shape {skip_aligned.shape} does not match main path "
|
| 755 |
+
f"{h_main.shape}."
|
| 756 |
+
)
|
| 757 |
+
return h_main + skip_aligned
|
| 758 |
+
|
| 759 |
+
|
| 760 |
+
class MotifUpBlock(nn.Module):
|
| 761 |
+
"""Decoder up stage: residual stack, spatial/temporal upsample, residual.
|
| 762 |
+
|
| 763 |
+
``upsample_residual=True`` forwards the parameter-free identity skip into
|
| 764 |
+
the upsample module (see :class:`Upsample` / :class:`Spatial2xTime2x3DUpsample`).
|
| 765 |
+
"""
|
| 766 |
+
|
| 767 |
+
def __init__(
|
| 768 |
+
self,
|
| 769 |
+
in_channels: int,
|
| 770 |
+
out_channels: int,
|
| 771 |
+
num_res_blocks: int = 2,
|
| 772 |
+
up_type: Literal["thw", "hw"] = "thw",
|
| 773 |
+
res_block: nn.Module = ResnetBlock3D,
|
| 774 |
+
dropout: float = 0.0,
|
| 775 |
+
norm_type: str = "layernorm",
|
| 776 |
+
t_interpolation: str = "trilinear",
|
| 777 |
+
upsample_residual: bool = False,
|
| 778 |
+
) -> None:
|
| 779 |
+
super().__init__()
|
| 780 |
+
assert num_res_blocks >= 2, "num_res_blocks too small (need >= 2)"
|
| 781 |
+
self.in_channels = in_channels
|
| 782 |
+
self.out_channels = out_channels
|
| 783 |
+
self.up_type = up_type
|
| 784 |
+
|
| 785 |
+
# Pre-upsample residual stack (channel-preserving).
|
| 786 |
+
self.res_block = nn.Sequential(
|
| 787 |
+
*[
|
| 788 |
+
res_block(
|
| 789 |
+
in_channels=in_channels,
|
| 790 |
+
out_channels=in_channels,
|
| 791 |
+
dropout=dropout,
|
| 792 |
+
norm_type=norm_type,
|
| 793 |
+
)
|
| 794 |
+
for _ in range(num_res_blocks - 1)
|
| 795 |
+
]
|
| 796 |
+
)
|
| 797 |
+
|
| 798 |
+
if up_type == "thw":
|
| 799 |
+
self.up = Spatial2xTime2x3DUpsample(
|
| 800 |
+
in_channels=in_channels,
|
| 801 |
+
out_channels=in_channels,
|
| 802 |
+
t_interpolation=t_interpolation,
|
| 803 |
+
residual=upsample_residual,
|
| 804 |
+
)
|
| 805 |
+
elif up_type == "hw":
|
| 806 |
+
self.up = Upsample(
|
| 807 |
+
in_channels=in_channels,
|
| 808 |
+
out_channels=in_channels,
|
| 809 |
+
residual=upsample_residual,
|
| 810 |
+
)
|
| 811 |
+
else:
|
| 812 |
+
raise ValueError(f"unknown up_type={up_type}")
|
| 813 |
+
|
| 814 |
+
self.out_res_block = res_block(
|
| 815 |
+
in_channels=in_channels,
|
| 816 |
+
out_channels=out_channels,
|
| 817 |
+
dropout=dropout,
|
| 818 |
+
norm_type=norm_type,
|
| 819 |
+
)
|
| 820 |
+
|
| 821 |
+
def forward(self, x: torch.Tensor):
|
| 822 |
+
x = self.res_block(x)
|
| 823 |
+
x = self.up(x)
|
| 824 |
+
return self.out_res_block(x)
|
| 825 |
+
|
| 826 |
+
|
| 827 |
+
# ---------------------------------------------------------------------------
|
| 828 |
+
# Encoder / decoder
|
| 829 |
+
# ---------------------------------------------------------------------------
|
| 830 |
+
|
| 831 |
+
|
| 832 |
+
class MotifEncoder(VideoBaseAE):
|
| 833 |
+
"""Deterministic 3D causal encoder.
|
| 834 |
+
|
| 835 |
+
For a ``(B, 3, T, H, W)`` clip the stem is a stride-1 conv; five down
|
| 836 |
+
stages reduce resolution (3 spatial-only ``hw`` stages then 2 joint
|
| 837 |
+
``thw`` stages), each adding its Global Skip Connection; a mid stack and a
|
| 838 |
+
final conv emit ``latent_dim`` channels directly at ``(ceil(T/4), H/32,
|
| 839 |
+
W/32)``.
|
| 840 |
+
"""
|
| 841 |
+
|
| 842 |
+
@register_to_config
|
| 843 |
+
def __init__(
|
| 844 |
+
self,
|
| 845 |
+
latent_dim: int = 128,
|
| 846 |
+
num_resblocks: int = 2,
|
| 847 |
+
dropout: float = 0.0,
|
| 848 |
+
input_type: Literal["video", "image"] = "video",
|
| 849 |
+
norm_type: str = "layernorm",
|
| 850 |
+
base_channels: List[int] = [96, 192, 384, 768, 768, 768],
|
| 851 |
+
mid_layers_type: List[str] = [
|
| 852 |
+
"ResnetBlock3D",
|
| 853 |
+
"Identity",
|
| 854 |
+
"ResnetBlock3D",
|
| 855 |
+
],
|
| 856 |
+
down_layer_type: List[str] = ["hw", "hw", "hw", "thw", "thw"],
|
| 857 |
+
down_layer_res_type: List[str] = [
|
| 858 |
+
"ResnetBlock2D",
|
| 859 |
+
"ResnetBlock2D",
|
| 860 |
+
"ResnetBlock2D",
|
| 861 |
+
"ResnetBlock3D",
|
| 862 |
+
"ResnetBlock3D",
|
| 863 |
+
],
|
| 864 |
+
) -> None:
|
| 865 |
+
super().__init__()
|
| 866 |
+
assert len(base_channels) == len(down_layer_type) + 1, (
|
| 867 |
+
f"base_channels must have one more entry than down_layer_type; "
|
| 868 |
+
f"got {len(base_channels)} vs {len(down_layer_type)}+1"
|
| 869 |
+
)
|
| 870 |
+
assert len(down_layer_type) == len(down_layer_res_type), (
|
| 871 |
+
f"down_layer_type and down_layer_res_type length mismatch: "
|
| 872 |
+
f"{len(down_layer_type)} vs {len(down_layer_res_type)}"
|
| 873 |
+
)
|
| 874 |
+
self.input_type = input_type
|
| 875 |
+
self.conv_in = Conv2d(3, base_channels[0], kernel_size=3, stride=1, padding=1)
|
| 876 |
+
|
| 877 |
+
self.down_blocks = nn.ModuleList()
|
| 878 |
+
for idx, (down_type, down_res_type) in enumerate(
|
| 879 |
+
zip(down_layer_type, down_layer_res_type)
|
| 880 |
+
):
|
| 881 |
+
self.down_blocks.append(
|
| 882 |
+
MotifDownBlock(
|
| 883 |
+
in_channels=base_channels[idx],
|
| 884 |
+
out_channels=base_channels[idx + 1],
|
| 885 |
+
num_res_blocks=num_resblocks,
|
| 886 |
+
down_type=down_type,
|
| 887 |
+
res_block=resolve_str_to_obj(down_res_type),
|
| 888 |
+
dropout=dropout,
|
| 889 |
+
norm_type=norm_type,
|
| 890 |
+
)
|
| 891 |
+
)
|
| 892 |
+
|
| 893 |
+
self.mid = nn.Sequential(
|
| 894 |
+
*[
|
| 895 |
+
build_mid_layer(t, base_channels[-1], dropout, norm_type)
|
| 896 |
+
for t in mid_layers_type
|
| 897 |
+
]
|
| 898 |
+
)
|
| 899 |
+
|
| 900 |
+
self.norm_out = Normalize(base_channels[-1], norm_type=norm_type)
|
| 901 |
+
# conv_out emits latent_dim channels directly (no mean/log-var split).
|
| 902 |
+
if self.input_type == "video":
|
| 903 |
+
self.conv_out = CausalConv3d(
|
| 904 |
+
base_channels[-1], latent_dim, kernel_size=3, stride=1, padding=1
|
| 905 |
+
)
|
| 906 |
+
else:
|
| 907 |
+
self.conv_out = Conv2d(
|
| 908 |
+
base_channels[-1], latent_dim, kernel_size=3, stride=1, padding=1
|
| 909 |
+
)
|
| 910 |
+
|
| 911 |
+
def forward(self, x: torch.Tensor):
|
| 912 |
+
h = self.conv_in(x)
|
| 913 |
+
for down_block in self.down_blocks:
|
| 914 |
+
h = down_block(h)
|
| 915 |
+
h = self.mid(h)
|
| 916 |
+
h = self.norm_out(h)
|
| 917 |
+
h = nonlinearity(h)
|
| 918 |
+
return self.conv_out(h)
|
| 919 |
+
|
| 920 |
+
|
| 921 |
+
class MotifDecoder(VideoBaseAE):
|
| 922 |
+
"""Asymmetric decoder mirroring the encoder's stage layout.
|
| 923 |
+
|
| 924 |
+
The decoder is intentionally wider than the encoder
|
| 925 |
+
(``base_channels=[144, 288, 576, 1152, 1152, 1152]`` in the released
|
| 926 |
+
config). ``upsample_residual=True`` enables the parameter-free identity
|
| 927 |
+
skip in every spatial upsample, scaled by a non-persistent
|
| 928 |
+
``residual_alpha`` buffer.
|
| 929 |
+
"""
|
| 930 |
+
|
| 931 |
+
@register_to_config
|
| 932 |
+
def __init__(
|
| 933 |
+
self,
|
| 934 |
+
latent_dim: int = 128,
|
| 935 |
+
num_resblocks: int = 2,
|
| 936 |
+
dropout: float = 0.0,
|
| 937 |
+
input_type: Literal["video", "image"] = "video",
|
| 938 |
+
norm_type: str = "layernorm",
|
| 939 |
+
t_interpolation: str = "trilinear",
|
| 940 |
+
base_channels: List[int] = [96, 192, 384, 768, 768, 768],
|
| 941 |
+
up_layer_type: List[str] = ["hw", "hw", "hw", "thw", "thw"],
|
| 942 |
+
mid_layers_type: List[str] = [
|
| 943 |
+
"ResnetBlock3D",
|
| 944 |
+
"Identity",
|
| 945 |
+
"ResnetBlock3D",
|
| 946 |
+
],
|
| 947 |
+
up_layer_res_type: List[str] = [
|
| 948 |
+
"ResnetBlock2D",
|
| 949 |
+
"ResnetBlock2D",
|
| 950 |
+
"ResnetBlock2D",
|
| 951 |
+
"ResnetBlock3D",
|
| 952 |
+
"ResnetBlock3D",
|
| 953 |
+
],
|
| 954 |
+
upsample_residual: bool = False,
|
| 955 |
+
) -> None:
|
| 956 |
+
super().__init__()
|
| 957 |
+
assert len(base_channels) == len(up_layer_type) + 1, (
|
| 958 |
+
f"base_channels must have one more entry than up_layer_type; "
|
| 959 |
+
f"got {len(base_channels)} vs {len(up_layer_type)}+1"
|
| 960 |
+
)
|
| 961 |
+
assert len(up_layer_type) == len(up_layer_res_type), (
|
| 962 |
+
f"up_layer_type and up_layer_res_type length mismatch: "
|
| 963 |
+
f"{len(up_layer_type)} vs {len(up_layer_res_type)}"
|
| 964 |
+
)
|
| 965 |
+
self.input_type = input_type
|
| 966 |
+
if self.input_type == "video":
|
| 967 |
+
self.conv_in = CausalConv3d(
|
| 968 |
+
latent_dim, base_channels[-1], kernel_size=3, stride=1, padding=1
|
| 969 |
+
)
|
| 970 |
+
else:
|
| 971 |
+
self.conv_in = Conv2d(
|
| 972 |
+
latent_dim, base_channels[-1], kernel_size=3, stride=1, padding=1
|
| 973 |
+
)
|
| 974 |
+
|
| 975 |
+
self.mid = nn.Sequential(
|
| 976 |
+
*[
|
| 977 |
+
build_mid_layer(t, base_channels[-1], dropout, norm_type)
|
| 978 |
+
for t in mid_layers_type
|
| 979 |
+
]
|
| 980 |
+
)
|
| 981 |
+
|
| 982 |
+
self.up_blocks = nn.ModuleList()
|
| 983 |
+
# Iterate from the deepest stage back up to the shallowest.
|
| 984 |
+
for idx, (up_type, up_res_type) in enumerate(
|
| 985 |
+
zip(reversed(up_layer_type), reversed(up_layer_res_type))
|
| 986 |
+
):
|
| 987 |
+
idx = len(up_layer_type) - idx
|
| 988 |
+
self.up_blocks.append(
|
| 989 |
+
MotifUpBlock(
|
| 990 |
+
in_channels=base_channels[idx],
|
| 991 |
+
out_channels=base_channels[idx - 1],
|
| 992 |
+
num_res_blocks=num_resblocks,
|
| 993 |
+
up_type=up_type,
|
| 994 |
+
res_block=resolve_str_to_obj(up_res_type),
|
| 995 |
+
t_interpolation=t_interpolation,
|
| 996 |
+
dropout=dropout,
|
| 997 |
+
norm_type=norm_type,
|
| 998 |
+
upsample_residual=upsample_residual,
|
| 999 |
+
)
|
| 1000 |
+
)
|
| 1001 |
+
|
| 1002 |
+
self.norm_out = Normalize(base_channels[0], norm_type=norm_type)
|
| 1003 |
+
self.conv_out = Conv2d(base_channels[0], 3, kernel_size=3, stride=1, padding=1)
|
| 1004 |
+
|
| 1005 |
+
def forward(self, z: torch.Tensor):
|
| 1006 |
+
h = self.conv_in(z)
|
| 1007 |
+
h = self.mid(h)
|
| 1008 |
+
for up_block in self.up_blocks:
|
| 1009 |
+
h = up_block(h)
|
| 1010 |
+
h = self.norm_out(h)
|
| 1011 |
+
h = nonlinearity(h)
|
| 1012 |
+
return self.conv_out(h)
|
| 1013 |
+
|
| 1014 |
+
|
| 1015 |
+
# ---------------------------------------------------------------------------
|
| 1016 |
+
# Top-level model
|
| 1017 |
+
# ---------------------------------------------------------------------------
|
| 1018 |
+
|
| 1019 |
+
|
| 1020 |
+
class MotifVAE(VideoBaseAE):
|
| 1021 |
+
"""3D causal video VAE with a 128-channel deterministic latent.
|
| 1022 |
+
|
| 1023 |
+
Compression is 4x temporal and 32x spatial. The encoder (width 96) uses a
|
| 1024 |
+
parameter-free Global Skip Connection at every stage; the decoder is wider
|
| 1025 |
+
(width 144).
|
| 1026 |
+
"""
|
| 1027 |
+
|
| 1028 |
+
@register_to_config
|
| 1029 |
+
def __init__(
|
| 1030 |
+
self,
|
| 1031 |
+
latent_dim: int = 128,
|
| 1032 |
+
base_channels: List[int] = [96, 192, 384, 768, 768, 768],
|
| 1033 |
+
decoder_base_channels: Optional[List[int]] = [144, 288, 576, 1152, 1152, 1152],
|
| 1034 |
+
decoder_variant: Literal["V4"] = "V4",
|
| 1035 |
+
layer_type: List[str] = ["hw", "hw", "hw", "thw", "thw"],
|
| 1036 |
+
layer_res_type: List[str] = [
|
| 1037 |
+
"ResnetBlock2D",
|
| 1038 |
+
"ResnetBlock2D",
|
| 1039 |
+
"ResnetBlock2D",
|
| 1040 |
+
"ResnetBlock3D",
|
| 1041 |
+
"ResnetBlock3D",
|
| 1042 |
+
],
|
| 1043 |
+
encoder_num_resblocks: int = 2,
|
| 1044 |
+
decoder_num_resblocks: int = 2,
|
| 1045 |
+
dropout: float = 0.0,
|
| 1046 |
+
norm_type: str = "layernorm",
|
| 1047 |
+
t_interpolation: str = "trilinear",
|
| 1048 |
+
input_type: Literal["video", "image"] = "video",
|
| 1049 |
+
mid_layers_type: List[str] = [
|
| 1050 |
+
"ResnetBlock3D",
|
| 1051 |
+
"Identity",
|
| 1052 |
+
"ResnetBlock3D",
|
| 1053 |
+
],
|
| 1054 |
+
scale: Optional[List[float]] = None,
|
| 1055 |
+
shift: Optional[List[float]] = None,
|
| 1056 |
+
upsample_residual: bool = False,
|
| 1057 |
+
) -> None:
|
| 1058 |
+
super().__init__()
|
| 1059 |
+
self.use_tiling = False
|
| 1060 |
+
self.t_chunk_enc = 16
|
| 1061 |
+
self.t_chunk_dec = 4
|
| 1062 |
+
self.use_quant_layer = False
|
| 1063 |
+
|
| 1064 |
+
if scale is None:
|
| 1065 |
+
scale = [0.18215] * latent_dim
|
| 1066 |
+
if shift is None:
|
| 1067 |
+
shift = [0.0] * latent_dim
|
| 1068 |
+
if decoder_base_channels is None:
|
| 1069 |
+
decoder_base_channels = [144, 288, 576, 1152, 1152, 1152]
|
| 1070 |
+
|
| 1071 |
+
self.encoder = MotifEncoder(
|
| 1072 |
+
latent_dim=latent_dim,
|
| 1073 |
+
base_channels=base_channels,
|
| 1074 |
+
num_resblocks=encoder_num_resblocks,
|
| 1075 |
+
dropout=dropout,
|
| 1076 |
+
norm_type=norm_type,
|
| 1077 |
+
down_layer_type=layer_type,
|
| 1078 |
+
down_layer_res_type=layer_res_type,
|
| 1079 |
+
input_type=input_type,
|
| 1080 |
+
mid_layers_type=mid_layers_type,
|
| 1081 |
+
)
|
| 1082 |
+
|
| 1083 |
+
if decoder_variant != "V4":
|
| 1084 |
+
raise ValueError(
|
| 1085 |
+
f"unknown decoder_variant={decoder_variant!r}; expected 'V4'"
|
| 1086 |
+
)
|
| 1087 |
+
self.decoder = MotifDecoder(
|
| 1088 |
+
latent_dim=latent_dim,
|
| 1089 |
+
base_channels=decoder_base_channels,
|
| 1090 |
+
num_resblocks=decoder_num_resblocks,
|
| 1091 |
+
dropout=dropout,
|
| 1092 |
+
norm_type=norm_type,
|
| 1093 |
+
t_interpolation=t_interpolation,
|
| 1094 |
+
up_layer_type=layer_type,
|
| 1095 |
+
up_layer_res_type=layer_res_type,
|
| 1096 |
+
input_type=input_type,
|
| 1097 |
+
mid_layers_type=mid_layers_type,
|
| 1098 |
+
upsample_residual=upsample_residual,
|
| 1099 |
+
)
|
| 1100 |
+
|
| 1101 |
+
def get_encoder(self):
|
| 1102 |
+
return [self.encoder]
|
| 1103 |
+
|
| 1104 |
+
def get_decoder(self):
|
| 1105 |
+
return [self.decoder]
|
| 1106 |
+
|
| 1107 |
+
def encode(self, x, **kwargs):
|
| 1108 |
+
# Deterministic encoder: DeterministicLatent exposes the sample()/mode()
|
| 1109 |
+
# posterior API over the latent.
|
| 1110 |
+
h = self.encoder(x)
|
| 1111 |
+
posterior = DeterministicLatent(h)
|
| 1112 |
+
return AutoencoderKLOutput(latent_dist=posterior, extra_output=None)
|
| 1113 |
+
|
| 1114 |
+
def decode(self, z, **kwargs):
|
| 1115 |
+
dec = self.decoder(z)
|
| 1116 |
+
return DecoderOutput(sample=dec, extra_output=None)
|
| 1117 |
+
|
| 1118 |
+
def forward(self, input, sample_posterior=True):
|
| 1119 |
+
posterior = self.encode(input).latent_dist
|
| 1120 |
+
z = posterior.sample() if sample_posterior else posterior.mode()
|
| 1121 |
+
dec = self.decode(z).sample
|
| 1122 |
+
return ForwardOutput(
|
| 1123 |
+
sample=dec,
|
| 1124 |
+
latent_dist=posterior,
|
| 1125 |
+
sampled_latent=z,
|
| 1126 |
+
extra_output=None,
|
| 1127 |
+
)
|
| 1128 |
+
|
| 1129 |
+
def get_last_layer(self):
|
| 1130 |
+
if hasattr(self.decoder.conv_out, "conv"):
|
| 1131 |
+
return self.decoder.conv_out.conv.weight
|
| 1132 |
+
return self.decoder.conv_out.weight
|