beomgyu-kim gkalstn0 commited on
Commit
931fe95
·
0 Parent(s):

Co-authored-by: gkalstn0 <gkalstn0@users.noreply.huggingface.co>

.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
+ ![Natural-image reconstruction (ImageNet-1K val): GT vs MotifVAE vs LTX-Video vs LTX-2 vs Wan2.2](assets/natural_recon.png)
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
+ ![Document reconstruction — top two rows OmniDoc-TokenBench (English), bottom two rows Korean print + handwriting from AIHUB; GT vs MotifVAE vs LTX-Video vs LTX-2 vs Wan2.2](assets/ocr_recon.png)
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
+ ![UCF101 FVD convergence over frozen tokenizers](assets/ucf101_fvd_curves.png)
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

  • SHA256: 4fc2b8d8cd8c7ea41be7a786a8a1b4862eadc5a798d2da776d5489534965177a
  • Pointer size: 132 Bytes
  • Size of remote file: 4.05 MB
assets/ocr_recon.png ADDED

Git LFS Details

  • SHA256: a9eab58faa52d100473227504311d5fd428013692e7301c0c66b0bc311537463
  • Pointer size: 132 Bytes
  • Size of remote file: 5.21 MB
assets/ucf101_fvd_curves.png ADDED

Git LFS Details

  • SHA256: d11295dfe5fd8bd8faa9f14fddc571e33d157a76c313a280cdcaa5f0e92cde87
  • Pointer size: 131 Bytes
  • Size of remote file: 112 kB
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