multimodalart HF Staff commited on
Commit
36a4745
·
verified ·
1 Parent(s): db016d7

MiniWorld camera-controlled world model demo

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* 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
 
 
 
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
+ examples/deck.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/garden.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,56 @@
1
  ---
2
- title: Miniworld Simulator
3
- emoji: 🐨
4
- colorFrom: purple
5
- colorTo: purple
6
  sdk: gradio
7
  sdk_version: 6.22.0
8
- python_version: '3.12'
9
  app_file: app.py
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  pinned: false
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: MiniWorld Simulator
3
+ emoji: 🌍
4
+ colorFrom: indigo
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.22.0
 
8
  app_file: app.py
9
+ python_version: "3.12"
10
+ short_description: Drive a video world model with a camera path from one frame
11
+ startup_duration_timeout: 45m
12
+ license: apache-2.0
13
+ models:
14
+ - zhaoyian01/MiniWorld
15
+ - Wan-AI/Wan2.2-TI2V-5B
16
+ tags:
17
+ - world-model
18
+ - video-generation
19
+ - camera-control
20
+ - image-to-video
21
+ - streaming-generation
22
  pinned: false
23
  ---
24
 
25
+ # MiniWorld camera-controlled world model
26
+
27
+ Interactive demo of [**MiniWorld**](https://huggingface.co/papers/2608.01127)
28
+ (`zhaoyian01/MiniWorld`, 1B RealEstate10K checkpoint): give it a single frame
29
+ and a procedural camera path, and it autoregressively rolls the world forward.
30
+
31
+ This Space reproduces the authors' reference inference route
32
+ ([`zhao-yian/MiniWorld`](https://github.com/zhao-yian/MiniWorld)):
33
+
34
+ ```bash
35
+ python -m miniworld.sample --dataset re10k \
36
+ --init_image <frame> --custom_camera_trajectory orbit_right \
37
+ --wm_model 1B --total_len 64 --trajectory_magnitude 3.0
38
+ ```
39
+
40
+ * 240×320 frames, Wan2.2 VAE (16× spatial / 4× temporal), 48 latent channels.
41
+ * AR-diffusion streaming sampler: `df_chunk_size=2`, `df_ardiff_step=5`,
42
+ 8 in-flight chunks, 24 cached chunks, 1 attention-sink frame, CFG 2.0,
43
+ 100 sampling steps (the streaming schedule caps the effective per-chunk
44
+ steps at 40) — i.e. exactly `scripts/sample_re10k.sh`.
45
+ * Camera conditioning is the repo's procedural trajectory builder turned into
46
+ ray-encoding features (`freq=15`, unnormalized translations), so no
47
+ ground-truth poses or reference video are needed.
48
+ * Streaming causal VAE decode-on-commit, matching the reference pipeline.
49
+
50
+ The action-conditioned DROID checkpoint is intentionally not exposed: its
51
+ conditioning requires per-dataset `q01/q99` action normalization statistics that
52
+ only ship with the LeRobot DROID dataset, so there is no faithful dataset-free
53
+ input for it.
54
+
55
+ Example frames are the first frames of the authors' own RealEstate10K rollout
56
+ grid (`assets/demo_re10k.mp4` in the model repo).
app.py ADDED
@@ -0,0 +1,498 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MiniWorld — camera-controlled video world model simulator (ZeroGPU).
2
+
3
+ Mirrors the authors' reference inference path
4
+
5
+ python -m miniworld.sample --dataset re10k --custom_camera_trajectory ...
6
+
7
+ one-to-one: a single init image is Wan2.2-VAE-encoded into the clean seed
8
+ latent, a procedural camera path is turned into ray-encoding conditioning, and
9
+ the AR-diffusion denoiser rolls the world forward chunk-by-chunk with a
10
+ position-bounded streaming KV cache and streaming VAE decode.
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import math
16
+ import os
17
+ import tempfile
18
+ import time
19
+
20
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
21
+
22
+ import spaces # noqa: E402 (must precede any torch / CUDA work)
23
+
24
+ import gradio as gr # noqa: E402
25
+ import numpy as np # noqa: E402
26
+ import torch # noqa: E402
27
+ from einops import rearrange # noqa: E402
28
+ from huggingface_hub import hf_hub_download # noqa: E402
29
+ from PIL import Image # noqa: E402
30
+
31
+ # MiniWorld checkpoints are plain `torch.save` dicts that carry a `meta` blob of
32
+ # plain-python objects next to the tensors, so they need the full unpickler.
33
+ _ORIG_TORCH_LOAD = torch.load
34
+
35
+
36
+ def _torch_load(*args, **kwargs):
37
+ kwargs.setdefault("weights_only", False)
38
+ return _ORIG_TORCH_LOAD(*args, **kwargs)
39
+
40
+
41
+ torch.load = _torch_load
42
+
43
+ from miniworld.conditioning.actions import ( # noqa: E402
44
+ ConditioningConfig,
45
+ build_cond_seq_for_batch,
46
+ )
47
+ from miniworld.conditioning.trajectories import build_custom_trajectory # noqa: E402
48
+ from miniworld.denoiser import DenoiserConfig, build_denoiser_from_mode # noqa: E402
49
+ from miniworld.vae.codec import StreamingVAEDecoder, vae_encode # noqa: E402
50
+ from miniworld.vae.wan22_vae import Wan2_2_VAE # noqa: E402
51
+
52
+ # --------------------------------------------------------------------------- #
53
+ # Constants (match scripts/sample_re10k.sh) #
54
+ # --------------------------------------------------------------------------- #
55
+ MINIWORLD_REPO = "zhaoyian01/MiniWorld"
56
+ MINIWORLD_CKPT = "MiniWorld_1b_re10k.pt"
57
+ VAE_REPO = "Wan-AI/Wan2.2-TI2V-5B"
58
+ VAE_FILE = "Wan2.2_VAE.pth"
59
+
60
+ RESIZE_H, RESIZE_W = 240, 320
61
+ SPATIAL_DOWNSAMPLE = 16
62
+ LATENT_CHANNELS = 48
63
+ POSE_ENC_FREQ = 15
64
+ DF_CHUNK_SIZE = 2
65
+ DF_ARDIFF_STEP = 5
66
+ STREAM_INFLIGHT_CHUNKS = 8
67
+ STREAM_MAX_CACHE_CHUNKS = 24
68
+ STREAM_SINK_SIZE = 1
69
+ SAMPLE_HISTORY_LEN = 1
70
+ SAVE_FPS = 8
71
+ MAX_SEED = np.iinfo(np.int32).max
72
+
73
+ H_LAT, W_LAT = RESIZE_H // SPATIAL_DOWNSAMPLE, RESIZE_W // SPATIAL_DOWNSAMPLE
74
+
75
+ TRAJECTORIES = [
76
+ "orbit_right",
77
+ "orbit_left",
78
+ "pan_right",
79
+ "pan_left",
80
+ "forward",
81
+ "backward",
82
+ "tilt_up",
83
+ "tilt_down",
84
+ "spiral",
85
+ "zoom_in",
86
+ "zoom_out",
87
+ "static",
88
+ ]
89
+
90
+ # --------------------------------------------------------------------------- #
91
+ # Model construction #
92
+ # --------------------------------------------------------------------------- #
93
+ print("Fetching Wan2.2 VAE ...", flush=True)
94
+ vae_path = hf_hub_download(VAE_REPO, VAE_FILE)
95
+ print("Fetching MiniWorld-1B (RealEstate10K) ...", flush=True)
96
+ ckpt_path = hf_hub_download(MINIWORLD_REPO, MINIWORLD_CKPT)
97
+
98
+ _ckpt = torch.load(ckpt_path, map_location="cpu")
99
+ _weights = _ckpt.get("ema_model", _ckpt.get("model"))
100
+ _meta = _ckpt.get("meta", {}) or {}
101
+ if _weights is None:
102
+ raise RuntimeError("MiniWorld checkpoint has neither `ema_model` nor `model`")
103
+
104
+
105
+ def _resolve_latent_frames() -> int:
106
+ for key in ("latent_frames", "trained_num_frames"):
107
+ val = int(_meta.get(key, 0) or 0)
108
+ if val > 0:
109
+ return val
110
+ freqs = _weights.get("net.feat_rope.freqs_cos")
111
+ tokens_per_frame = H_LAT * W_LAT
112
+ if freqs is not None and freqs.shape[0] % tokens_per_frame == 0:
113
+ return int(freqs.shape[0] // tokens_per_frame)
114
+ raise RuntimeError("Cannot determine the checkpoint's latent frame count")
115
+
116
+
117
+ LATENT_FRAMES = _resolve_latent_frames()
118
+ WM_MODEL = str(_meta.get("wm_model") or "1B")
119
+ TRAINED_NUM_FRAMES = int(_meta.get("trained_num_frames", 0) or 0) or LATENT_FRAMES
120
+ MAX_TOTAL_LEN = TRAINED_NUM_FRAMES
121
+ print(
122
+ f"[Checkpoint] wm_model={WM_MODEL} latent_frames={LATENT_FRAMES} "
123
+ f"trained_num_frames={TRAINED_NUM_FRAMES}",
124
+ flush=True,
125
+ )
126
+
127
+ denoiser = build_denoiser_from_mode(
128
+ DenoiserConfig(
129
+ wm_model=WM_MODEL,
130
+ latent_size=(H_LAT, W_LAT),
131
+ latent_channels=LATENT_CHANNELS,
132
+ latent_frames=LATENT_FRAMES,
133
+ wm_mlp_ratio=4.0,
134
+ wm_use_qknorm=True,
135
+ wm_use_checkpoint=False,
136
+ cond_dim=4 * 6 * 2 * POSE_ENC_FREQ,
137
+ cond_per_token=True,
138
+ adaln_mode="adaln_lora",
139
+ cond_dropout_prob=0.1,
140
+ timestep_baseshift=2.667,
141
+ timestep_shift=-1.0,
142
+ num_sampling_steps=100,
143
+ cfg_scale=2.0,
144
+ cfg_interval_min=0.2,
145
+ cfg_interval_max=1.0,
146
+ df_chunk_size=DF_CHUNK_SIZE,
147
+ df_ardiff_step=DF_ARDIFF_STEP,
148
+ )
149
+ ).eval()
150
+ denoiser.load_state_dict(_weights, strict=True)
151
+ denoiser.trained_num_frames = TRAINED_NUM_FRAMES
152
+ print("[Checkpoint] loaded: all keys matched", flush=True)
153
+ del _ckpt, _weights
154
+
155
+ denoiser = denoiser.to("cuda")
156
+
157
+ vae = Wan2_2_VAE(vae_pth=vae_path, device="cuda")
158
+ vae.model.requires_grad_(False)
159
+ vae.model.eval()
160
+
161
+ _COND_CFG = ConditioningConfig(
162
+ use_pose_cond=True, use_action_cond=False, pose_enc_freq=POSE_ENC_FREQ
163
+ )
164
+
165
+
166
+ # --------------------------------------------------------------------------- #
167
+ # Helpers #
168
+ # --------------------------------------------------------------------------- #
169
+ def _prepare_init_frame(image) -> torch.Tensor:
170
+ """PIL / ndarray -> ``(H, W, C)`` float32 in [-1, 1] (== `load_init_image`)."""
171
+ if image is None:
172
+ raise gr.Error("Please provide an initial frame.")
173
+ if isinstance(image, np.ndarray):
174
+ image = Image.fromarray(image)
175
+ arr = np.asarray(image.convert("RGB"), dtype=np.float32) / 255.0
176
+ img = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0)
177
+ if tuple(img.shape[-2:]) != (RESIZE_H, RESIZE_W):
178
+ img = torch.nn.functional.interpolate(
179
+ img, size=(RESIZE_H, RESIZE_W), mode="bilinear", align_corners=False
180
+ )
181
+ return img.squeeze(0).permute(1, 2, 0).contiguous() * 2.0 - 1.0
182
+
183
+
184
+ def _write_mp4(frames: np.ndarray, fps: int) -> str:
185
+ import imageio.v2 as imageio
186
+
187
+ path = os.path.join(tempfile.mkdtemp(), "miniworld.mp4")
188
+ writer = imageio.get_writer(
189
+ path,
190
+ fps=fps,
191
+ codec="libx264",
192
+ quality=8,
193
+ macro_block_size=1,
194
+ ffmpeg_params=["-pix_fmt", "yuv420p"],
195
+ )
196
+ try:
197
+ for frame in frames:
198
+ writer.append_data(frame)
199
+ finally:
200
+ writer.close()
201
+ return path
202
+
203
+
204
+ def _rollout_tflops(total_len: int, steps: int) -> float:
205
+ """Replay the streaming schedule to cost a rollout in DiT TFLOPs.
206
+
207
+ The AR-diffusion schedule is not linear in ``total_len`` (short rollouts
208
+ that fit inside the in-flight window run the *full* sampler), so the ZeroGPU
209
+ reservation is derived from the same bookkeeping the sampler does.
210
+ """
211
+ chunk = DF_CHUNK_SIZE
212
+ ar = DF_ARDIFF_STEP
213
+ inflight = STREAM_INFLIGHT_CHUNKS
214
+ max_cache_frames = STREAM_MAX_CACHE_CHUNKS * chunk
215
+ total_chunks = (total_len + chunk - 1) // chunk
216
+ eff = steps if total_chunks <= inflight else min(steps, inflight * ar)
217
+
218
+ prev = [0] * total_chunks
219
+ masks = []
220
+ n_rows = 0
221
+ while any(p != eff for p in prev):
222
+ row = [0] * total_chunks
223
+ for i in range(total_chunks):
224
+ row[i] = prev[i] + 1 if (i == 0 or prev[i - 1] == eff) else row[i - 1] - ar
225
+ row[i] = max(0, min(eff, row[i]))
226
+ masks.append([row[i] != prev[i] for i in range(total_chunks)])
227
+ prev = row
228
+ n_rows += 1
229
+ if n_rows > 4000: # safety valve
230
+ break
231
+
232
+ terminal = min(inflight, total_chunks)
233
+ committed = 0
234
+ cache_frames = 0
235
+ tflops = 0.0
236
+ # per-forward TFLOPs for a 1B DiT: 0.6 per query frame (linear layers) plus
237
+ # 0.01548 per (query frame x key frame) (attention), at 300 tokens/frame.
238
+ for step in range(n_rows):
239
+ if terminal < total_chunks and masks[step][terminal]:
240
+ terminal += 1
241
+ win_sc = max(0, terminal - inflight)
242
+ while committed < win_sc:
243
+ frames = min((committed + 1) * chunk, total_len) - committed * chunk
244
+ tflops += 2 * frames * (0.6 + 0.01548 * (cache_frames + frames))
245
+ cache_frames = min(cache_frames + frames, max_cache_frames)
246
+ committed += 1
247
+ if terminal <= win_sc:
248
+ continue
249
+ q_frames = min(terminal * chunk, total_len) - win_sc * chunk
250
+ tflops += 2 * q_frames * (0.6 + 0.01548 * (cache_frames + q_frames))
251
+ return tflops
252
+
253
+
254
+ # Calibrated against measured wall-clock on ZeroGPU (see README).
255
+ _TFLOPS_PER_SEC = 90.0
256
+ _VAE_SEC_PER_LATENT_FRAME = 0.10
257
+ _FIXED_OVERHEAD_SEC = 15.0
258
+
259
+
260
+ def _duration(*args, **kwargs) -> int:
261
+ total_len, steps = 32, 100
262
+ if len(args) >= 4:
263
+ total_len = int(args[3])
264
+ if len(args) >= 8:
265
+ steps = int(args[7])
266
+ total_len = int(kwargs.get("total_len", total_len))
267
+ steps = int(kwargs.get("num_sampling_steps", steps))
268
+ total_len = max(4, min(total_len, 64))
269
+ est = (
270
+ _FIXED_OVERHEAD_SEC
271
+ + _rollout_tflops(total_len, steps) / _TFLOPS_PER_SEC
272
+ + _VAE_SEC_PER_LATENT_FRAME * total_len
273
+ )
274
+ return int(min(400, math.ceil(est * 1.15)))
275
+
276
+
277
+ # --------------------------------------------------------------------------- #
278
+ # Inference #
279
+ # --------------------------------------------------------------------------- #
280
+ @spaces.GPU(duration=_duration)
281
+ @torch.no_grad()
282
+ def simulate(
283
+ image,
284
+ trajectory: str = "orbit_right",
285
+ magnitude: float = 3.0,
286
+ total_len: int = 32,
287
+ seed: int = 0,
288
+ randomize_seed: bool = True,
289
+ cfg_scale: float = 2.0,
290
+ num_sampling_steps: int = 100,
291
+ focal_norm: float = 0.5,
292
+ progress=gr.Progress(track_tqdm=True),
293
+ ):
294
+ init_frame = _prepare_init_frame(image)
295
+ total_len = max(4, min(int(total_len), MAX_TOTAL_LEN))
296
+
297
+ if randomize_seed:
298
+ seed = int(np.random.randint(0, MAX_SEED))
299
+ seed = int(seed) % (MAX_SEED + 1)
300
+
301
+ device = torch.device("cuda")
302
+ denoiser.cfg_scale = float(cfg_scale)
303
+ denoiser.steps = int(num_sampling_steps)
304
+
305
+ videos = init_frame.unsqueeze(0).unsqueeze(0).to(device) # (1, 1, H, W, C)
306
+ poses = (
307
+ build_custom_trajectory(
308
+ trajectory,
309
+ num_frames=4 * (total_len - 1) + 1,
310
+ focal_norm=float(focal_norm),
311
+ magnitude=float(magnitude),
312
+ )
313
+ .unsqueeze(0)
314
+ .to(device)
315
+ )
316
+
317
+ generator = torch.Generator(device="cpu").manual_seed(seed)
318
+ noise = torch.randn(
319
+ 1, LATENT_CHANNELS, total_len, H_LAT, W_LAT,
320
+ generator=generator, dtype=torch.float32,
321
+ ).to(device)
322
+
323
+ start = time.perf_counter()
324
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16, enabled=True):
325
+ latents = vae_encode(
326
+ vae, rearrange(videos, "b t h w c -> b c t h w").contiguous()
327
+ )
328
+ _, c_latent, _, h_lat, w_lat = latents.shape
329
+ full_latents = latents.new_zeros(1, c_latent, total_len, h_lat, w_lat)
330
+ full_latents[:, :, :1] = latents[:, :, :1]
331
+
332
+ cond_seq = build_cond_seq_for_batch(
333
+ cfg=_COND_CFG,
334
+ poses=poses,
335
+ actions=None,
336
+ t_latent=total_len,
337
+ h_lat=h_lat,
338
+ w_lat=w_lat,
339
+ )
340
+
341
+ _, pred_rgb = denoiser.generate_eval_latents_streaming(
342
+ full_latents,
343
+ cond_seq,
344
+ total_len=total_len,
345
+ history_len=SAMPLE_HISTORY_LEN,
346
+ max_cache_chunks=STREAM_MAX_CACHE_CHUNKS,
347
+ inflight_chunks=STREAM_INFLIGHT_CHUNKS,
348
+ sink_frames=STREAM_SINK_SIZE,
349
+ stream_decoder=StreamingVAEDecoder(vae),
350
+ noise=noise.to(full_latents.dtype),
351
+ )
352
+ elapsed = time.perf_counter() - start
353
+
354
+ video = ((pred_rgb[0].permute(1, 2, 3, 0).clamp(-1, 1) + 1.0) * 127.5).to(
355
+ torch.uint8
356
+ )
357
+ frames = video.cpu().numpy()
358
+ path = _write_mp4(frames, SAVE_FPS)
359
+ n = int(frames.shape[0])
360
+ print(f"[Timing] total_len={total_len} steps={num_sampling_steps}: "
361
+ f"{elapsed:.2f}s (reserved {_duration(None, trajectory, magnitude, total_len, seed, False, cfg_scale, num_sampling_steps)}s)",
362
+ flush=True)
363
+ return (
364
+ path,
365
+ seed,
366
+ f"**{n} frames** @ {SAVE_FPS} fps ({n / SAVE_FPS:.1f}s) · "
367
+ f"{total_len} latent frames · `{trajectory}` @ {magnitude:g} · "
368
+ f"seed `{seed}` · {elapsed:.1f}s of GPU time",
369
+ )
370
+
371
+
372
+ # --------------------------------------------------------------------------- #
373
+ # UI #
374
+ # --------------------------------------------------------------------------- #
375
+ CSS = "#col-container { max-width: 1060px; margin: 0 auto; }"
376
+
377
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
378
+ with gr.Column(elem_id="col-container"):
379
+ gr.Markdown(
380
+ f"""
381
+ # 🌍 MiniWorld · camera-controlled world model
382
+
383
+ Hand MiniWorld-1B **one frame and a camera path** and it rolls the
384
+ world forward autoregressively — no text prompt, no reference video,
385
+ no ground-truth poses. A position-bounded streaming KV cache plus
386
+ causal Wan2.2 VAE decoding keep the horizon open, so a rollout can
387
+ run to {4 * (MAX_TOTAL_LEN - 1) + 1} frames from a
388
+ {TRAINED_NUM_FRAMES}-latent-frame checkpoint.
389
+
390
+ Model: [`zhaoyian01/MiniWorld`](https://huggingface.co/zhaoyian01/MiniWorld)
391
+ (RealEstate10K, {WM_MODEL}) · Paper:
392
+ [MiniWorld: Democratizing the Training of Video World Models from Scratch](https://huggingface.co/papers/2608.01127)
393
+ · Code: [zhao-yian/MiniWorld](https://github.com/zhao-yian/MiniWorld)
394
+ """
395
+ )
396
+
397
+ with gr.Row():
398
+ with gr.Column():
399
+ image = gr.Image(
400
+ label="Initial frame",
401
+ type="pil",
402
+ height=270,
403
+ sources=["upload", "clipboard"],
404
+ )
405
+ trajectory = gr.Dropdown(
406
+ label="Camera trajectory",
407
+ choices=TRAJECTORIES,
408
+ value="orbit_right",
409
+ )
410
+ magnitude = gr.Slider(
411
+ label="Motion strength",
412
+ minimum=0.5,
413
+ maximum=8.0,
414
+ step=0.5,
415
+ value=3.0,
416
+ info="3.0 gives clear, stable motion over a 32-frame rollout. "
417
+ "Scale up for longer rollouts; lower it if late frames smear.",
418
+ )
419
+ total_len = gr.Slider(
420
+ label="Rollout length (latent frames)",
421
+ minimum=20,
422
+ maximum=MAX_TOTAL_LEN,
423
+ step=4,
424
+ value=32,
425
+ info=f"Each latent frame decodes to 4 RGB frames at {SAVE_FPS} fps; "
426
+ f"{MAX_TOTAL_LEN} → {4 * (MAX_TOTAL_LEN - 1) + 1} frames.",
427
+ )
428
+ run_button = gr.Button("Simulate", variant="primary")
429
+
430
+ with gr.Column():
431
+ result = gr.Video(
432
+ label="Rollout", autoplay=True, loop=True, height=270
433
+ )
434
+ info = gr.Markdown()
435
+
436
+ with gr.Accordion("Advanced settings", open=False):
437
+ with gr.Row():
438
+ seed = gr.Slider(
439
+ label="Seed", minimum=0, maximum=MAX_SEED, step=1, value=0
440
+ )
441
+ randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
442
+ with gr.Row():
443
+ cfg_scale = gr.Slider(
444
+ label="Guidance scale (CFG)",
445
+ minimum=1.0,
446
+ maximum=5.0,
447
+ step=0.1,
448
+ value=2.0,
449
+ )
450
+ num_sampling_steps = gr.Slider(
451
+ label="Sampling steps",
452
+ minimum=20,
453
+ maximum=100,
454
+ step=10,
455
+ value=100,
456
+ info="Effective steps per chunk are capped by the streaming "
457
+ "schedule at in-flight chunks × AR step = 40.",
458
+ )
459
+ focal_norm = gr.Slider(
460
+ label="Normalized focal length",
461
+ minimum=0.3,
462
+ maximum=1.2,
463
+ step=0.05,
464
+ value=0.5,
465
+ info="0.5 matches typical RealEstate10K intrinsics; smaller = wider FOV.",
466
+ )
467
+
468
+ gr.Examples(
469
+ examples=[
470
+ ["examples/kitchen.png", "orbit_right", 3.0, 32],
471
+ ["examples/deck.png", "forward", 3.0, 32],
472
+ ["examples/garden.png", "pan_left", 3.0, 32],
473
+ ],
474
+ inputs=[image, trajectory, magnitude, total_len],
475
+ outputs=[result, seed, info],
476
+ fn=simulate,
477
+ cache_examples=True,
478
+ cache_mode="lazy",
479
+ )
480
+
481
+ gr.on(
482
+ triggers=[run_button.click],
483
+ fn=simulate,
484
+ inputs=[
485
+ image,
486
+ trajectory,
487
+ magnitude,
488
+ total_len,
489
+ seed,
490
+ randomize_seed,
491
+ cfg_scale,
492
+ num_sampling_steps,
493
+ focal_norm,
494
+ ],
495
+ outputs=[result, seed, info],
496
+ )
497
+
498
+ demo.queue().launch(mcp_server=True)
examples/deck.png ADDED

Git LFS Details

  • SHA256: b4e2ebbdcd2cde43100da7d42bb96198aac717a00204ea3811c8f928376da93a
  • Pointer size: 131 Bytes
  • Size of remote file: 123 kB
examples/garden.png ADDED

Git LFS Details

  • SHA256: ae90a3df2694c719ab103144bb7568b288e56e1afc01d6e98975f7cbd0914bed
  • Pointer size: 131 Bytes
  • Size of remote file: 101 kB
examples/kitchen.png ADDED
miniworld/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Public MiniWorld package."""
2
+
3
+ from miniworld.miniworld import MiniWorldModel, MiniWorldModels
4
+
5
+ __all__ = ["MiniWorldModel", "MiniWorldModels"]
6
+
miniworld/conditioning/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """Conditioning utilities for MiniWorld."""
2
+
3
+ from miniworld.conditioning.actions import ConditioningConfig, build_cond_seq_from_actions, build_cond_seq_for_batch
4
+
5
+ __all__ = ["ConditioningConfig", "build_cond_seq_from_actions", "build_cond_seq_for_batch"]
6
+
miniworld/conditioning/actions.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Conditioning helpers for robot-action and camera-pose inputs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from typing import Optional
7
+
8
+ import torch
9
+
10
+ from miniworld.conditioning.poses import compute_ray_encoding, downsample_poses_to_latent
11
+
12
+
13
+ @dataclass
14
+ class ConditioningConfig:
15
+ """Configuration for converting raw batch conditions into model conditions."""
16
+
17
+ use_pose_cond: bool = False
18
+ use_action_cond: bool = False
19
+ pose_enc_freq: int = 15
20
+
21
+
22
+ def build_cond_seq_from_actions(actions: torch.Tensor) -> torch.Tensor:
23
+ """Map raw per-frame actions to WAN-VAE latent-frame conditioning.
24
+
25
+ WAN-VAE compresses time as 4x+1. Latent frame 0 is the seed frame and uses a
26
+ zero action slot; each following latent frame receives the four raw actions
27
+ that drive its decoded frame group.
28
+ """
29
+ batch, num_actions, action_dim = actions.shape
30
+ if num_actions % 4 != 0:
31
+ raise ValueError(f"Expected action length 4n for real actions, got {num_actions}")
32
+ num_generated_latents = num_actions // 4
33
+ cond = actions.new_zeros(batch, num_generated_latents + 1, 4 * action_dim)
34
+ if num_generated_latents > 0:
35
+ cond[:, 1:, :] = actions.reshape(batch, num_generated_latents, 4 * action_dim)
36
+ return cond
37
+
38
+
39
+ def build_cond_seq_for_batch(
40
+ *,
41
+ cfg: ConditioningConfig,
42
+ poses: Optional[torch.Tensor],
43
+ actions: Optional[torch.Tensor],
44
+ t_latent: int,
45
+ h_lat: int,
46
+ w_lat: int,
47
+ ) -> torch.Tensor:
48
+ """Build the MiniWorld conditioning tensor for one batch."""
49
+ if cfg.use_pose_cond:
50
+ if poses is None:
51
+ raise ValueError("Pose conditioning requested but batch has no poses")
52
+ latent_poses = downsample_poses_to_latent(poses, t_latent)
53
+ return compute_ray_encoding(latent_poses, h_lat, w_lat, freq=cfg.pose_enc_freq, normalize_trans=False)
54
+ if cfg.use_action_cond:
55
+ if actions is None:
56
+ raise ValueError("Action conditioning requested but batch has no actions")
57
+ return build_cond_seq_from_actions(actions)
58
+ raise ValueError("Either pose or action conditioning must be enabled")
miniworld/conditioning/poses.py ADDED
@@ -0,0 +1,218 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Camera pose -> ray-encoding utilities for pose-conditioned world model.
2
+
3
+ Adapted from `GeometryForcing/utils/geometry_utils.py` with the following
4
+ simplifications:
5
+ * Only the bits needed for ``ray_encoding`` conditioning are kept (the
6
+ variant the user picked as the best-performing one).
7
+ * ``rays`` accepts independent ``(h_res, w_res)`` so non-square latents
8
+ (e.g. 15x20) are supported without distorting the intrinsics.
9
+
10
+ All functions follow this convention:
11
+ * Raw camera pose layout: ``(B, T, 16)`` = ``[K(4), R(9 + T(3))]`` where the
12
+ first 4 columns are normalised intrinsics ``(fx, fy, px, py)`` (pixel-coords
13
+ divided by image size) and the last 12 columns are a flattened ``3x4``
14
+ world-to-camera extrinsics matrix in row-major.
15
+ * Ray encoding output: ``(B, T, 180, H_lat, W_lat)`` (6 ray dims * 2 trig fns
16
+ * 15 NeRF frequencies = 180). This matches what `DiT3DPose` consumes when
17
+ ``conditioning_type=ray_encoding``.
18
+ """
19
+
20
+ from __future__ import annotations
21
+
22
+ import math
23
+ from typing import Tuple
24
+
25
+ import torch
26
+ from einops import einsum, rearrange, repeat
27
+
28
+
29
+ def _split_pose16(raw_poses: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
30
+ """``(B, T, 16)`` -> ``R (B, T, 3, 3)``, ``T (B, T, 3)``, ``K (B, T, 4)``."""
31
+ assert raw_poses.shape[-1] == 16, f"expected 16-dim pose, got {raw_poses.shape[-1]}"
32
+ K, RT = raw_poses.split([4, 12], dim=-1)
33
+ RT = rearrange(RT, "b t (i j) -> b t i j", i=3, j=4)
34
+ R = RT[..., :3, :3]
35
+ T = RT[..., :3, 3]
36
+ return R, T, K
37
+
38
+
39
+ def _normalize_by_first(R: torch.Tensor, T: torch.Tensor) -> Tuple[torch.Tensor, torch.Tensor]:
40
+ """Re-express all poses so the first frame is the world origin."""
41
+ R_ref = R[:, 0] # (B, 3, 3)
42
+ T_ref = T[:, 0] # (B, 3)
43
+ R_inv = rearrange(R_ref, "b i j -> b j i")
44
+ R_new = einsum(R, R_inv, "b t i j1, b j1 j2 -> b t i j2")
45
+ T_new = T - einsum(R_new, T_ref, "b t i j, b j -> b t i")
46
+ return R_new, T_new
47
+
48
+
49
+ def _compute_rays(
50
+ R: torch.Tensor,
51
+ T: torch.Tensor,
52
+ K: torch.Tensor,
53
+ h_res: int,
54
+ w_res: int,
55
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
56
+ """Per-pixel ray origin / direction in world coords.
57
+
58
+ Args:
59
+ R: ``(B, T, 3, 3)`` world->cam rotation.
60
+ T: ``(B, T, 3)`` world->cam translation.
61
+ K: ``(B, T, 4)`` normalised intrinsics ``(fx, fy, px, py)``.
62
+ h_res, w_res: target ray grid resolution (independent so non-square
63
+ latents are handled correctly).
64
+
65
+ Returns:
66
+ origin: ``(B, T, H, W, 3)``
67
+ direction: ``(B, T, H, W, 3)`` (unnormalised; norm encodes depth scale)
68
+ """
69
+ device, dtype = K.device, K.dtype
70
+
71
+ coord_w, coord_h = torch.meshgrid(
72
+ torch.linspace(0, w_res - 1, w_res, device=device, dtype=dtype),
73
+ torch.linspace(0, h_res - 1, h_res, device=device, dtype=dtype),
74
+ indexing="xy",
75
+ ) # (H, W) each
76
+ coord_w = rearrange(coord_w, "h w -> 1 1 h w") + 0.5
77
+ coord_h = rearrange(coord_h, "h w -> 1 1 h w") + 0.5
78
+
79
+ # Normalised K -> pixel-space K (separate W / H scaling for non-square grids).
80
+ fx = (K[..., 0] * w_res).view(*K.shape[:-1], 1, 1) # (B, T, 1, 1)
81
+ fy = (K[..., 1] * h_res).view(*K.shape[:-1], 1, 1)
82
+ px = (K[..., 2] * w_res).view(*K.shape[:-1], 1, 1)
83
+ py = (K[..., 3] * h_res).view(*K.shape[:-1], 1, 1)
84
+
85
+ x = (coord_w - px) / fx
86
+ y = (coord_h - py) / fy
87
+ z = torch.ones_like(x)
88
+ direction = torch.stack([x, y, z], dim=-1) # (B, T, H, W, 3)
89
+
90
+ R_inv = rearrange(R, "b t i j -> b t j i")
91
+ direction = einsum(R_inv, direction, "b t i j, b t h w j -> b t h w i")
92
+
93
+ origin = -einsum(R_inv, T, "b t i j, b t j -> b t i")
94
+ origin = repeat(origin, "b t i -> b t h w i", h=h_res, w=w_res).clone()
95
+ return origin, direction
96
+
97
+
98
+ def _normalize_translation_scale(T: torch.Tensor, eps: float = 1e-6) -> torch.Tensor:
99
+ """Per-clip translation-scale normalisation (CameraCtrl / lingbot style).
100
+
101
+ Monocular SfM poses (e.g. RealEstate10K) have an arbitrary, per-clip metric
102
+ scale, so raw camera translations vary wildly in magnitude across clips.
103
+ Since the ray origin feeds NeRF frequency encoding ``sin(2^k pi x)`` -- which
104
+ is very sensitive to the absolute magnitude of ``x`` -- this inconsistency
105
+ hurts learning. We rescale each clip so its largest camera displacement is
106
+ ~1, making camera motion scale-invariant across clips.
107
+
108
+ Args:
109
+ T: ``(B, S, 3)`` camera translations, already expressed relative to the
110
+ first frame (so the first frame sits at the origin).
111
+ eps: guard so static / near-static clips (max norm ~ 0) are left
112
+ unchanged ("only normalize when moving").
113
+
114
+ Returns:
115
+ ``(B, S, 3)`` translations divided by the per-clip max translation norm.
116
+ """
117
+ max_norm = torch.norm(T, dim=-1).amax(dim=1, keepdim=True) # (B, 1)
118
+ scale = torch.where(max_norm > eps, max_norm, torch.ones_like(max_norm))
119
+ return T / scale.unsqueeze(-1)
120
+
121
+
122
+ def _nerf_pos_encoding(x: torch.Tensor, freq: int) -> torch.Tensor:
123
+ """NeRF-style sin/cos positional encoding along the last dim."""
124
+ scale = (
125
+ 2 ** torch.linspace(0, freq - 1, freq, device=x.device, dtype=x.dtype)
126
+ * math.pi
127
+ )
128
+ encoding = rearrange(x[..., None] * scale, "b t h w i s -> b t h w (i s)")
129
+ return torch.sin(torch.cat([encoding, encoding + 0.5 * math.pi], dim=-1))
130
+
131
+
132
+ @torch.no_grad()
133
+ @torch.autocast(device_type="cuda", enabled=False) # always fp32 for geometry
134
+ def compute_ray_encoding(
135
+ raw_poses: torch.Tensor,
136
+ h_lat: int,
137
+ w_lat: int,
138
+ freq: int = 15,
139
+ normalize_trans: bool = False,
140
+ ) -> torch.Tensor:
141
+ """End-to-end raw poses -> ray-encoding feature volume.
142
+
143
+ Args:
144
+ raw_poses: either ``(B, T, 16)`` or ``(B, T, K, 16)``. MiniWorld's RE10K
145
+ pipeline uses ``K=4`` poses inside each WAN-VAE latent chunk.
146
+ h_lat, w_lat: latent spatial size (= model input H, W after VAE).
147
+ freq: NeRF frequency count. ``freq=15`` gives ``6 * 2 * 15 = 180``
148
+ channels per pose.
149
+ normalize_trans: if True, rescale each clip's camera
150
+ translations so the largest displacement is ~1 (see
151
+ ``_normalize_translation_scale``). Disabled by default to match
152
+ DFoT's RealEstate10K preprocessing; static clips are untouched.
153
+
154
+ Returns:
155
+ ``(B, T, K * 6 * 2 * freq, H_lat, W_lat)`` float32. The ``K`` poses are
156
+ ray-encoded independently then concatenated along the channel axis
157
+ (so the spatial ``y_embedder`` sees ``K * 180`` channels). For the
158
+ common ``(B, T, 16)`` input ``K=1`` and the output channel count is
159
+ ``180``.
160
+ """
161
+ assert raw_poses.dim() in (3, 4), (
162
+ f"raw_poses must be (B, T, 16) or (B, T, K, 16); got {raw_poses.shape}"
163
+ )
164
+ raw_poses = raw_poses.float()
165
+ if raw_poses.dim() == 3:
166
+ b, t_lat, _ = raw_poses.shape
167
+ k_per_lat = 1
168
+ flat = raw_poses # (B, T, 16)
169
+ else:
170
+ b, t_lat, k_per_lat, _ = raw_poses.shape
171
+ # Flatten K into the time axis so we can reuse the single-pose pipeline
172
+ # (one shared normalisation anchor = first pose in the sequence).
173
+ flat = raw_poses.reshape(b, t_lat * k_per_lat, 16)
174
+
175
+ R, T, K = _split_pose16(flat)
176
+ R, T = _normalize_by_first(R, T)
177
+ if normalize_trans:
178
+ T = _normalize_translation_scale(T)
179
+ origin, direction = _compute_rays(R, T, K, h_res=h_lat, w_res=w_lat)
180
+ enc = torch.cat(
181
+ [
182
+ _nerf_pos_encoding(origin, freq),
183
+ _nerf_pos_encoding(direction, freq),
184
+ ],
185
+ dim=-1,
186
+ ) # (B, T*K, H, W, 6 * 2 * freq)
187
+
188
+ if k_per_lat == 1:
189
+ return rearrange(enc, "b t h w c -> b t c h w").contiguous()
190
+ return rearrange(
191
+ enc, "b (t k) h w c -> b t (k c) h w", t=t_lat, k=k_per_lat,
192
+ ).contiguous()
193
+
194
+
195
+ def downsample_poses_to_latent(
196
+ raw_poses: torch.Tensor,
197
+ t_latent: int,
198
+ ) -> torch.Tensor:
199
+ """Map per-raw-frame poses to four poses per WAN-style latent frame.
200
+
201
+ The causal WAN VAE encodes ``T_raw = 4*(T_lat-1)+1`` raw frames into
202
+ ``T_lat`` latents with the temporal grouping:
203
+ * latent 0 -> raw [0]
204
+ * latent j (>0) -> raw [4j-3, 4j-2, 4j-1, 4j]
205
+
206
+ Latent 0 has only raw[0], so it is duplicated four times to keep the output
207
+ shape consistent with action conditioning: ``(B, T_lat, 4, 16)``.
208
+ """
209
+ idx_per_latent = [[0, 0, 0, 0]]
210
+ for j in range(1, t_latent):
211
+ idx_per_latent.append([4 * j - 3, 4 * j - 2, 4 * j - 1, 4 * j])
212
+ idx_flat = [i for chunk in idx_per_latent for i in chunk]
213
+ assert raw_poses.shape[1] > max(idx_flat), (
214
+ f"raw_poses has only {raw_poses.shape[1]} frames; need at least "
215
+ f"{max(idx_flat) + 1} to build {t_latent} latent poses."
216
+ )
217
+ b = raw_poses.shape[0]
218
+ return raw_poses[:, idx_flat].view(b, t_latent, 4, 16).contiguous()
miniworld/conditioning/trajectories.py ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Procedural camera-trajectory utilities for pose-conditioned WM inference.
2
+
3
+ Builds ``(T, 16)`` pose tensors compatible with
4
+ ``model.pose_utils.compute_ray_encoding`` so we can drive the world model
5
+ with **any** camera path (no GT video / poses required).
6
+
7
+ Pose layout (per frame, matches ``model.pose_utils._split_pose16``):
8
+ ``[fx, fy, px, py, R(9, row-major), T(3)]`` = ``[K(4), RT(12)]``
9
+ where K is normalised (intrinsics divided by image W / H) and
10
+ (R, T) is world->camera (OpenCV convention: x=right, y=down, z=forward).
11
+
12
+ All trajectories here **start at the identity pose** at frame 0
13
+ (R=I, T=0). ``compute_ray_encoding`` will re-anchor the first frame as
14
+ the world origin anyway, so only relative camera motion w.r.t. frame 0
15
+ ever reaches the model.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import math
21
+ import os
22
+ from pathlib import Path
23
+ from typing import Callable, Tuple
24
+
25
+ import numpy as np
26
+ import torch
27
+
28
+
29
+ ###############################################################################
30
+ # Low-level rotation / look-at #
31
+ ###############################################################################
32
+
33
+
34
+ def _yaw(a: float) -> np.ndarray:
35
+ """Rotation around world-down axis (=+y). ``a > 0`` -> camera pans right
36
+ (the world's +x moves toward camera-forward)."""
37
+ c, s = math.cos(a), math.sin(a)
38
+ # camera basis in world: right=(c,0,-s), down=(0,1,0), forward=(s,0,c)
39
+ # R_w2c rows = [right; down; forward]
40
+ return np.array(
41
+ [
42
+ [c, 0.0, -s],
43
+ [0.0, 1.0, 0.0],
44
+ [s, 0.0, c],
45
+ ],
46
+ dtype=np.float64,
47
+ )
48
+
49
+
50
+ def _pitch(a: float) -> np.ndarray:
51
+ """Rotation around world-right axis (=+x). ``a > 0`` -> camera tilts up."""
52
+ c, s = math.cos(a), math.sin(a)
53
+ # camera basis: right=(1,0,0), down=(0,c,s), forward=(0,-s,c)
54
+ return np.array(
55
+ [
56
+ [1.0, 0.0, 0.0],
57
+ [0.0, c, s],
58
+ [0.0, -s, c],
59
+ ],
60
+ dtype=np.float64,
61
+ )
62
+
63
+
64
+ def _look_at(
65
+ eye: np.ndarray,
66
+ target: np.ndarray,
67
+ world_up: np.ndarray = np.array([0.0, -1.0, 0.0]),
68
+ ) -> Tuple[np.ndarray, np.ndarray]:
69
+ """world->camera (R, T) for a camera at ``eye`` looking at ``target``.
70
+
71
+ OpenCV convention: camera frame is (right, down, forward) = (+x, +y, +z).
72
+ ``world_up`` points along the world's "visual up" direction; in OpenCV
73
+ image-y is down, so the canonical world-up is ``(0, -1, 0)``.
74
+ """
75
+ fwd = target - eye
76
+ n = float(np.linalg.norm(fwd))
77
+ if n < 1e-8:
78
+ # Degenerate: fall back to identity orientation.
79
+ R = np.eye(3, dtype=np.float64)
80
+ T = -R @ eye
81
+ return R, T
82
+ fwd = fwd / n
83
+
84
+ down_world = -world_up
85
+ right = np.cross(down_world, fwd)
86
+ rn = float(np.linalg.norm(right))
87
+ if rn < 1e-6:
88
+ # forward parallel to up -> pick any perpendicular right
89
+ right = np.array([1.0, 0.0, 0.0])
90
+ if abs(float(fwd @ right)) > 0.99:
91
+ right = np.array([0.0, 0.0, 1.0])
92
+ else:
93
+ right = right / rn
94
+ down = np.cross(fwd, right)
95
+
96
+ R = np.stack([right, down, fwd], axis=0).astype(np.float64) # world->cam rows
97
+ T = -R @ eye
98
+ return R, T
99
+
100
+
101
+ ###############################################################################
102
+ # Trajectory primitives #
103
+ ###############################################################################
104
+
105
+
106
+ def _build_RT(traj_fn: Callable[[float], Tuple[np.ndarray, np.ndarray]], num_frames: int) -> np.ndarray:
107
+ """Sample ``traj_fn(s)`` at ``num_frames`` evenly-spaced ``s`` in [0, 1]
108
+ and return a ``(num_frames, 12)`` row-major flattened RT.
109
+
110
+ ``traj_fn(0.0)`` is expected to return the identity pose (R=I, T=0) so
111
+ frame 0 anchors the world origin cleanly.
112
+ """
113
+ out = np.zeros((num_frames, 12), dtype=np.float32)
114
+ for i in range(num_frames):
115
+ s = i / max(num_frames - 1, 1)
116
+ R, T = traj_fn(s)
117
+ out[i, :9] = R.reshape(-1)
118
+ out[i, 9:] = T.reshape(-1)
119
+ return out
120
+
121
+
122
+ SUPPORTED_TRAJECTORIES = (
123
+ "static",
124
+ "forward",
125
+ "backward",
126
+ "pan_left",
127
+ "pan_right",
128
+ "tilt_up",
129
+ "tilt_down",
130
+ "orbit_right",
131
+ "orbit_left",
132
+ "spiral",
133
+ "zoom_in",
134
+ "zoom_out",
135
+ )
136
+
137
+
138
+ def build_custom_trajectory(
139
+ traj_type: str,
140
+ num_frames: int,
141
+ focal_norm: float = 0.7,
142
+ magnitude: float = 1.0,
143
+ ) -> torch.Tensor:
144
+ """Build a ``(num_frames, 16)`` pose sequence for a named procedural path.
145
+
146
+ Args:
147
+ traj_type: one of :data:`SUPPORTED_TRAJECTORIES`.
148
+ num_frames: number of *raw* frames the pose sequence must cover
149
+ (i.e. ``eval_t_dataset = 4*(total_len-1)+2`` -- e.g. 126 for
150
+ ``total_len=32``). ``compute_ray_encoding`` indexes into this.
151
+ focal_norm: normalised focal length (``fx = fy = focal_norm``). RE10K
152
+ videos typically sit near 0.5-1.0; smaller = wider FOV.
153
+ magnitude: global scaling. At ``magnitude=1.0`` the defaults are:
154
+ * translate ~0.5 units.
155
+ * rotate up to 30 deg (pan / tilt).
156
+ * orbit / spiral: 60 deg arc on a radius-``magnitude`` circle.
157
+ * zoom: focal scales linearly to 1.5x (in) / 0.67x (out).
158
+ For **rawscale** RE10K checkpoints (``normalize_trans=False``),
159
+ ``magnitude=1.0`` looks nearly static.
160
+
161
+ Returns:
162
+ ``(num_frames, 16)`` float32 tensor on cpu.
163
+ """
164
+ if traj_type not in SUPPORTED_TRAJECTORIES:
165
+ raise ValueError(
166
+ f"Unknown trajectory '{traj_type}'. "
167
+ f"Supported: {SUPPORTED_TRAJECTORIES}"
168
+ )
169
+
170
+ I = np.eye(3, dtype=np.float64)
171
+ Z = np.zeros(3, dtype=np.float64)
172
+ PI = math.pi
173
+
174
+ # ----- translation / rotation only paths (K is constant) -----
175
+ def f_static(s):
176
+ return I, Z
177
+
178
+ def f_forward(s):
179
+ # camera center moves to (0, 0, +d) in world; T = -R @ c = (0,0,-d)
180
+ d = 0.5 * magnitude * s
181
+ return I, np.array([0.0, 0.0, -d])
182
+
183
+ def f_backward(s):
184
+ d = 0.5 * magnitude * s
185
+ return I, np.array([0.0, 0.0, d])
186
+
187
+ def f_pan_right(s):
188
+ return _yaw(+(PI / 6) * magnitude * s), Z
189
+
190
+ def f_pan_left(s):
191
+ return _yaw(-(PI / 6) * magnitude * s), Z
192
+
193
+ def f_tilt_up(s):
194
+ return _pitch(+(PI / 6) * magnitude * s), Z
195
+
196
+ def f_tilt_down(s):
197
+ return _pitch(-(PI / 6) * magnitude * s), Z
198
+
199
+ # Orbit / spiral are anchored so that frame 0 is exactly (R=I, T=0):
200
+ # the camera starts at the world origin looking at a target one unit
201
+ # away along +z (= (0, 0, r)), and pivots around that target while
202
+ # keeping it in view.
203
+
204
+ def _orbit(sign: float):
205
+ def fn(s):
206
+ # Radius scales with magnitude so rawscale mag>>1 also translates
207
+ # farther (angle alone on r=1 caps |T| at ~2).
208
+ a = sign * (PI / 3) * s
209
+ r = 1.0 * magnitude
210
+ target = np.array([0.0, 0.0, r])
211
+ eye = np.array([r * math.sin(a), 0.0, r * (1.0 - math.cos(a))])
212
+ return _look_at(eye, target)
213
+ return fn
214
+
215
+ def f_spiral(s):
216
+ a = (PI / 3) * s
217
+ r = 1.0 * magnitude
218
+ target = np.array([0.0, 0.0, r])
219
+ eye = np.array(
220
+ [r * math.sin(a), -0.2 * magnitude * s, r * (1.0 - math.cos(a))]
221
+ )
222
+ return _look_at(eye, target)
223
+
224
+ rt_dispatch = {
225
+ "static": f_static,
226
+ "forward": f_forward,
227
+ "backward": f_backward,
228
+ "pan_left": f_pan_left,
229
+ "pan_right": f_pan_right,
230
+ "tilt_up": f_tilt_up,
231
+ "tilt_down": f_tilt_down,
232
+ "orbit_left": _orbit(-1.0),
233
+ "orbit_right": _orbit(+1.0),
234
+ "spiral": f_spiral,
235
+ # zoom paths keep RT = identity, vary K instead
236
+ "zoom_in": f_static,
237
+ "zoom_out": f_static,
238
+ }
239
+ RT = _build_RT(rt_dispatch[traj_type], num_frames) # (T, 12)
240
+
241
+ # ----- intrinsics K (T, 4) -----
242
+ K = np.zeros((num_frames, 4), dtype=np.float32)
243
+ for i in range(num_frames):
244
+ s = i / max(num_frames - 1, 1)
245
+ if traj_type == "zoom_in":
246
+ scale = 1.0 + 0.5 * magnitude * s # up to 1.5x at magnitude=1
247
+ elif traj_type == "zoom_out":
248
+ scale = 1.0 / (1.0 + 0.5 * magnitude * s) # down to ~0.67x
249
+ else:
250
+ scale = 1.0
251
+ K[i, 0] = focal_norm * scale # fx
252
+ K[i, 1] = focal_norm * scale # fy
253
+ K[i, 2] = 0.5 # px at image center
254
+ K[i, 3] = 0.5 # py at image center
255
+
256
+ pose16 = np.concatenate([K, RT], axis=1) # (T, 16)
257
+ return torch.from_numpy(pose16).to(torch.float32)
258
+
259
+
260
+ ###############################################################################
261
+ # Init-image loading #
262
+ ###############################################################################
263
+
264
+
265
+ def load_init_image(path: str, resize_h: int, resize_w: int) -> torch.Tensor:
266
+ """Load a single image (or first frame of a video) and return it as a
267
+ ``(H, W, C)`` float32 tensor in ``[-1, 1]`` -- the same format that
268
+ ``SimpleVideoDataset`` produces for a single frame.
269
+
270
+ Supported inputs:
271
+ * PIL-readable still images (.jpg / .png / .webp / ...).
272
+ * Video files (.mp4 / .mov / ...). First frame is taken.
273
+ """
274
+ p = Path(path)
275
+ assert p.exists(), f"--init_image not found: {path}"
276
+ suffix = p.suffix.lower()
277
+
278
+ if suffix in (".mp4", ".mov", ".avi", ".mkv", ".webm"):
279
+ import torchvision.io
280
+ frames, _, _ = torchvision.io.read_video(
281
+ os.fspath(p), pts_unit="sec", output_format="TCHW",
282
+ )
283
+ if frames.shape[0] == 0:
284
+ raise RuntimeError(f"--init_image video decoded 0 frames: {path}")
285
+ img = frames[0:1].float() / 255.0 # (1, C, H, W)
286
+ else:
287
+ from PIL import Image
288
+ with Image.open(p) as im:
289
+ im = im.convert("RGB")
290
+ arr = np.asarray(im, dtype=np.float32) / 255.0 # (H, W, C)
291
+ img = torch.from_numpy(arr).permute(2, 0, 1).unsqueeze(0) # (1, C, H, W)
292
+
293
+ if tuple(img.shape[-2:]) != (resize_h, resize_w):
294
+ img = torch.nn.functional.interpolate(
295
+ img, size=(resize_h, resize_w), mode="bilinear", align_corners=False,
296
+ )
297
+ img = img.squeeze(0).permute(1, 2, 0).contiguous() # (H, W, C)
298
+ img = img * 2.0 - 1.0
299
+ return img
miniworld/denoiser.py ADDED
@@ -0,0 +1,1073 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import time
4
+ from typing import Dict, List, Optional, Tuple
5
+
6
+ import numpy as np
7
+ import torch
8
+ import torch.nn as nn
9
+ from miniworld.vae.codec import print0 as _print0
10
+
11
+
12
+ class IncrementalTimesteps:
13
+ """AR-Diffusion style combinatorial timestep sampler.
14
+ """
15
+
16
+ def __init__(self, F: int, T: int):
17
+ self.F = F
18
+ self.T = T
19
+
20
+ mat = torch.zeros((T, F), dtype=torch.float64)
21
+ for t in range(T):
22
+ mat[t, F - 1] = 1
23
+ for f in range(F - 2, -1, -1):
24
+ mat[T - 1, f] = 1
25
+ for t in range(T - 2, -1, -1):
26
+ mat[t, f] = mat[t + 1, f] + mat[t, f + 1]
27
+ self.mat_s = mat.numpy()
28
+
29
+ mat = torch.zeros((T, F), dtype=torch.float64)
30
+ for t in range(T):
31
+ mat[t, 0] = 1
32
+ for f in range(1, F):
33
+ mat[0, f] = 1
34
+ for t in range(1, T):
35
+ mat[t, f] = mat[t - 1, f] + mat[t, f - 1]
36
+ self.mat_e = mat.numpy()
37
+
38
+ def sample_stepseq_from_mid(self):
39
+ timesteps = torch.zeros(self.F, dtype=torch.long)
40
+ cur_f = np.random.randint(self.F)
41
+ timesteps[cur_f] = np.random.randint(self.T)
42
+
43
+ for f in range(cur_f - 1, -1, -1):
44
+ candidate_weights = self.mat_e[: int(timesteps[f + 1]) + 1, f]
45
+ prob_sequence = candidate_weights / candidate_weights.sum()
46
+ cur_step = np.random.choice(range(0, int(timesteps[f + 1]) + 1), p=prob_sequence)
47
+ timesteps[f] = int(cur_step)
48
+
49
+ for f in range(cur_f + 1, self.F):
50
+ candidate_weights = self.mat_s[int(timesteps[f - 1]):, f]
51
+ prob_sequence = candidate_weights / candidate_weights.sum()
52
+ cur_step = np.random.choice(range(int(timesteps[f - 1]), self.T), p=prob_sequence)
53
+ timesteps[f] = int(cur_step)
54
+ return timesteps
55
+
56
+
57
+ class DenoiserConfig:
58
+ def __init__(self, **kwargs):
59
+ self.wm_model: str = "1B"
60
+ self.latent_size: int = 16
61
+ self.latent_channels: int = 48
62
+ self.latent_frames: int = 9
63
+
64
+ self.wm_mlp_ratio: float = 4.0
65
+ self.wm_use_qknorm: bool = True
66
+ self.wm_use_checkpoint: bool = True
67
+ self.cond_dim: int = 0
68
+ # When True, y is treated as per-token spatial conditioning
69
+ # ``(B, T, cond_dim, H_lat, W_lat)`` (e.g. ray-encoding for camera
70
+ # pose). When False (default), y is the per-frame ``(B, T, cond_dim)``
71
+ # latent-action condition.
72
+ self.cond_per_token: bool = False
73
+
74
+ # Structured action/pose dropout for classifier-free guidance training.
75
+ self.adaln_mode: str = "adaln_lora"
76
+ self.cond_dropout_prob: float = 0.0
77
+ # Route the true first latent frame (seed / initial observation, no
78
+ # preceding action) through the learned null_action (action mode only).
79
+ self.action_null_first: bool = True
80
+
81
+ # Long-video finetune / streaming inference metadata.
82
+ # ``trained_num_frames`` defaults to ``latent_frames`` and is saved in the
83
+ # ckpt meta so streaming inference can assert the active window
84
+ # (cache + in-flight) never exceeds it.
85
+ self.trained_num_frames: int = -1 # -1 => fallback to latent_frames at runtime
86
+
87
+ # Training timesteps: t = sigmoid(P_mean + P_std * z), z ~ N(0, 1).
88
+ # P_std <= 0 falls back to uniform.
89
+ self.P_mean: float = 0.0
90
+ self.P_std: float = 1.0
91
+ self.timestep_shift: float = -1.0 # -1 = auto from per-chunk token count; >0 = manual override
92
+ self.timestep_baseshift: float = 2.667 # shift at _REF_TOKENS; see Denoiser.__init__
93
+
94
+ # sample
95
+ self.num_sampling_steps: int = 50
96
+ self.cfg_scale: float = 1.0
97
+ self.cfg_interval_min: float = 0.1
98
+ self.cfg_interval_max: float = 1.0
99
+
100
+ self.df_chunk_size: int = 2
101
+ self.df_train_time_bins: int = 50
102
+ self.df_ardiff_step: int = 1
103
+
104
+ for k, v in kwargs.items():
105
+ if hasattr(self, k):
106
+ setattr(self, k, v)
107
+
108
+
109
+
110
+ class Denoiser(nn.Module):
111
+ """World model Denoiser.
112
+
113
+ Args:
114
+
115
+ Return:
116
+ diffusion loss
117
+ """
118
+
119
+ def __init__(self, cfg: DenoiserConfig) -> None:
120
+ super().__init__()
121
+
122
+ self.cfg = cfg
123
+ from miniworld.miniworld import MiniWorldModels
124
+
125
+ if cfg.wm_model not in MiniWorldModels:
126
+ raise ValueError(
127
+ f"Unknown MiniWorld model {cfg.wm_model!r}. "
128
+ f"Choose one of {sorted(MiniWorldModels)}."
129
+ )
130
+ self.net = MiniWorldModels[cfg.wm_model](
131
+ input_size=cfg.latent_size,
132
+ in_channels=cfg.latent_channels,
133
+ num_frames=cfg.latent_frames,
134
+ mlp_ratio=cfg.wm_mlp_ratio,
135
+ use_qknorm=cfg.wm_use_qknorm,
136
+ use_rope=True,
137
+ use_abs_pos=False,
138
+ use_checkpoint=cfg.wm_use_checkpoint,
139
+ cond_dim=cfg.cond_dim,
140
+ cond_per_token=cfg.cond_per_token,
141
+ adaln_mode=cfg.adaln_mode,
142
+ cond_dropout_prob=cfg.cond_dropout_prob,
143
+ action_null_first=cfg.action_null_first,
144
+ )
145
+
146
+ self.trained_num_frames = (
147
+ cfg.trained_num_frames if cfg.trained_num_frames > 0 else cfg.latent_frames
148
+ )
149
+
150
+ # SD3-style timestep shift, scaled by the tokens denoised jointly at one
151
+ # noise level (a single chunk). Under diffusion forcing every chunk has
152
+ # its own t, so a longer window must not move the training t distribution.
153
+ latent_size = cfg.latent_size
154
+ if isinstance(latent_size, (tuple, list)):
155
+ h_lat, w_lat = latent_size
156
+ else:
157
+ h_lat = w_lat = int(latent_size)
158
+ n_tokens = int(cfg.df_chunk_size) * h_lat * w_lat
159
+ if cfg.timestep_shift > 0:
160
+ self.timestep_shift = cfg.timestep_shift
161
+ else:
162
+ _REF_TOKENS = 600 # 2 * 15 * 20: chunk_size=2 at 240x320 @16x downsample
163
+ # Nothing derives the 2.667 default; it is the knob for how hard
164
+ # training leans towards high-noise timesteps.
165
+ self.timestep_shift = cfg.timestep_baseshift * (n_tokens / _REF_TOKENS) ** 0.5
166
+ _print0(f"[Denoiser] latent=({cfg.latent_frames}, {h_lat}, {w_lat}), "
167
+ f"chunk_size={cfg.df_chunk_size}, chunk_tokens={n_tokens}, "
168
+ f"timestep_shift={self.timestep_shift:.4f}")
169
+ # Scheme B: when the net is MiniWorld and its internal structured
170
+ # dropout is enabled, CFG uses the model's *learned null* token for the
171
+ # unconditional branch (train + infer), instead of zeroing cond_seq.
172
+ # This keeps the train-time null and infer-time uncond identical.
173
+ self.use_model_null_cfg = cfg.cond_dropout_prob > 0.0
174
+ # Filled by generate_* so callers can report pipeline throughput.
175
+ self.last_eval_meta: Dict[str, object] = {}
176
+
177
+ self.steps = cfg.num_sampling_steps
178
+ self.cfg_scale = cfg.cfg_scale
179
+ self.cfg_interval_min = cfg.cfg_interval_min
180
+ self.cfg_interval_max = cfg.cfg_interval_max
181
+ self.df_chunk_size = int(cfg.df_chunk_size)
182
+ self.df_train_time_bins = max(2, int(cfg.df_train_time_bins))
183
+ self.df_ardiff_step = int(cfg.df_ardiff_step)
184
+ if self.df_ardiff_step <= 0:
185
+ raise ValueError("df_ardiff_step must be > 0 for MiniWorld AR-diffusion")
186
+ self.condition_noise_max_t = 0.05
187
+ self.P_mean = float(cfg.P_mean)
188
+ self.P_std = float(cfg.P_std)
189
+ self._df_train_step_samplers: Dict[int, IncrementalTimesteps] = {}
190
+
191
+ def _set_last_eval_meta(
192
+ self,
193
+ *,
194
+ path: str,
195
+ total_chunks: int,
196
+ n_ctx_chunks: int,
197
+ num_outer_steps: int,
198
+ effective_steps: Optional[int] = None,
199
+ ) -> None:
200
+ self.last_eval_meta = {
201
+ "path": path,
202
+ "total_chunks": int(total_chunks),
203
+ "n_ctx_chunks": int(n_ctx_chunks),
204
+ "gen_chunks": int(max(0, total_chunks - n_ctx_chunks)),
205
+ "num_outer_steps": int(num_outer_steps),
206
+ "ar_step": int(self.df_ardiff_step),
207
+ "chunk_size": int(self.df_chunk_size),
208
+ "effective_steps": (
209
+ int(effective_steps) if effective_steps is not None else int(self.steps)
210
+ ),
211
+ }
212
+
213
+ def _make_uncond(self, cond_seq: torch.Tensor) -> Tuple[torch.Tensor, Optional[torch.Tensor]]:
214
+ """Return ``(cond_for_uncond, cond_drop)`` for the CFG unconditional pass.
215
+
216
+ When structured dropout is trained, keep the real conditioning tensor
217
+ and force the model's learned null token via ``cond_drop=all-True``.
218
+ """
219
+ if self.use_model_null_cfg:
220
+ b = cond_seq.shape[0]
221
+ return cond_seq, torch.ones(b, dtype=torch.bool, device=cond_seq.device)
222
+ return torch.zeros_like(cond_seq), None
223
+
224
+ def drop_cond(self, cond_seq: torch.Tensor) -> torch.Tensor:
225
+ return cond_seq
226
+
227
+ def _build_chunk_slices(self, t: int) -> List[slice]:
228
+ if t <= 0:
229
+ raise ValueError(f"t must be positive, got {t}")
230
+ chunk_size = self.df_chunk_size
231
+ assert chunk_size > 0
232
+
233
+ chunk_slices: List[slice] = []
234
+ start = 0
235
+ while start < t:
236
+ end = min(t, start + chunk_size)
237
+ chunk_slices.append(slice(start, end))
238
+ start = end
239
+ return chunk_slices
240
+
241
+ def _get_df_train_step_sampler(self, num_chunks: int) -> IncrementalTimesteps:
242
+ sampler = self._df_train_step_samplers.get(num_chunks)
243
+ if sampler is None:
244
+ sampler = IncrementalTimesteps(num_chunks, self.df_train_time_bins)
245
+ self._df_train_step_samplers[num_chunks] = sampler
246
+ return sampler
247
+
248
+ def _sample_df_chunk_timesteps(self, num_chunks: int, device: torch.device) -> torch.Tensor:
249
+ if num_chunks <= 0:
250
+ return torch.zeros(0, device=device, dtype=torch.long)
251
+ sampler = self._get_df_train_step_sampler(num_chunks)
252
+ sampled = sampler.sample_stepseq_from_mid()
253
+ return sampled.to(device=device, dtype=torch.long)
254
+
255
+ def _broadcast_chunk_values_to_frames(
256
+ self,
257
+ chunk_values: torch.Tensor,
258
+ chunk_slices: List[slice],
259
+ t: int,
260
+ ) -> torch.Tensor:
261
+ b = chunk_values.shape[0]
262
+ frame_values = torch.zeros(b, t, device=chunk_values.device, dtype=chunk_values.dtype)
263
+ for chunk_idx, chunk_slice in enumerate(chunk_slices):
264
+ frame_values[:, chunk_slice] = chunk_values[:, chunk_idx].unsqueeze(1)
265
+ return frame_values
266
+
267
+ def _build_async_step_index_matrix(
268
+ self,
269
+ total_chunks: int,
270
+ num_steps: int,
271
+ device: torch.device,
272
+ ) -> Tuple[torch.Tensor, torch.Tensor]:
273
+ # for ar diffusion inference
274
+ if total_chunks <= 0:
275
+ step_index = torch.full((1, total_chunks), num_steps, device=device, dtype=torch.long)
276
+ update_mask = torch.zeros((1, total_chunks), device=device, dtype=torch.bool)
277
+ return step_index, update_mask
278
+
279
+ ar_step = int(self.df_ardiff_step)
280
+ pre_row = torch.zeros(total_chunks, dtype=torch.long)
281
+ rows: List[torch.Tensor] = []
282
+ masks: List[torch.Tensor] = []
283
+
284
+ while not torch.all(pre_row == num_steps):
285
+ new_row = torch.zeros_like(pre_row)
286
+ for idx in range(total_chunks):
287
+ if idx == 0 or pre_row[idx - 1] == num_steps:
288
+ new_row[idx] = pre_row[idx] + 1
289
+ else:
290
+ new_row[idx] = new_row[idx - 1] - ar_step
291
+ new_row = new_row.clamp(0, num_steps)
292
+ masks.append(new_row != pre_row)
293
+ rows.append(new_row.clone())
294
+ pre_row = new_row
295
+
296
+ step_index = torch.stack(rows, dim=0).to(device=device)
297
+ update_mask = torch.stack(masks, dim=0).to(device=device)
298
+ return step_index, update_mask
299
+
300
+ def _build_chunk_sampling_schedule(
301
+ self,
302
+ total_chunks: int,
303
+ device: torch.device,
304
+ dtype: torch.dtype,
305
+ n_context_chunks: int = 1,
306
+ effective_steps: Optional[int] = None,
307
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
308
+ # for ar diffusion inference
309
+ num_steps = effective_steps if effective_steps is not None else int(self.steps)
310
+ ts = torch.linspace(1.0, 0.0, num_steps + 1, device=device, dtype=dtype)
311
+ ts = self.shift_timestep(ts, self.timestep_shift)
312
+ step_index, update_mask = self._build_async_step_index_matrix(
313
+ total_chunks=total_chunks,
314
+ num_steps=num_steps,
315
+ device=device,
316
+ )
317
+
318
+ current_lookup = torch.cat([ts[:1], ts[:-1]], dim=0)
319
+ next_lookup = ts
320
+ t_chunk = current_lookup[step_index]
321
+ t_next_chunk = next_lookup[step_index]
322
+ for ci in range(min(n_context_chunks, total_chunks)):
323
+ t_chunk[:, ci] = 0
324
+ t_next_chunk[:, ci] = 0
325
+ update_mask[:, ci] = False
326
+ return t_chunk, t_next_chunk, update_mask
327
+
328
+ def _compute_fifo_valid_intervals(
329
+ self,
330
+ update_mask: torch.Tensor,
331
+ total_chunks: int,
332
+ max_chunks_in_window: int,
333
+ ) -> List[Tuple[int, int]]:
334
+ """Compute per-step FIFO window bounds (chunk-level).
335
+
336
+ Mirrors AR-Diffusion ``fifoddim.py``'s ``valid_interval`` logic.
337
+ The window starts covering chunks ``[0, max_chunks_in_window)`` and
338
+ slides right by one chunk each time a new chunk at the window
339
+ boundary becomes active (``update_mask`` turns True).
340
+
341
+ Returns a list of ``(start_chunk, end_chunk)`` tuples, one per
342
+ outer iteration.
343
+ """
344
+ terminal = min(max_chunks_in_window, total_chunks)
345
+ intervals: List[Tuple[int, int]] = []
346
+ for i in range(update_mask.shape[0]):
347
+ if terminal < total_chunks and bool(update_mask[i, terminal]):
348
+ terminal += 1
349
+ start = max(0, terminal - max_chunks_in_window)
350
+ intervals.append((start, terminal))
351
+ return intervals
352
+
353
+
354
+ def _build_diffusion_forcing_timesteps(
355
+ self,
356
+ b: int,
357
+ t: int,
358
+ device: torch.device,
359
+ dtype: torch.dtype,
360
+ ):
361
+ """Build per-frame timesteps for diffusion forcing training.
362
+
363
+ Clean-context length is sampled per example:
364
+ Mode A (p=0.5): only the first frame is clean
365
+ Mode B (p=0.5): the entire first chunk is clean
366
+
367
+ Returns:
368
+ t_frame: (B, T)
369
+ chunk_slices: list of slices
370
+ chunk_t: (B, num_chunks)
371
+ clean_mask: (B, T) 1 on clean context frames, else 0
372
+ """
373
+ chunk_slices = self._build_chunk_slices(t)
374
+ num_chunks = len(chunk_slices)
375
+ chunk_t = torch.zeros(b, num_chunks, device=device, dtype=dtype)
376
+ scale = float(max(self.df_train_time_bins - 1, 1))
377
+
378
+ for sample_idx in range(b):
379
+ if num_chunks <= 0:
380
+ continue
381
+ seq1 = self._sample_df_chunk_timesteps(num_chunks, device=device)
382
+ chunk_t[sample_idx, :] = seq1.to(dtype=dtype) / scale
383
+
384
+ chunk_t = self.logit_normal_warp(chunk_t)
385
+ chunk_t = self.shift_timestep(chunk_t, self.timestep_shift)
386
+ t_frame = self._broadcast_chunk_values_to_frames(chunk_t, chunk_slices, t)
387
+
388
+ clean_mask = torch.zeros(b, t, device=device, dtype=dtype)
389
+ cond_noise = self.sample_condition_t((b,), device=device, dtype=dtype)
390
+
391
+ for sample_idx in range(b):
392
+ if num_chunks <= 0:
393
+ continue
394
+ if torch.rand(1).item() < 0.5:
395
+ # Mode A: only first frame is clean
396
+ t_frame[sample_idx, 0] = cond_noise[sample_idx]
397
+ clean_mask[sample_idx, 0] = 1.0
398
+ else:
399
+ # Mode B: entire first chunk is clean
400
+ first_sl = chunk_slices[0]
401
+ t_frame[sample_idx, first_sl] = cond_noise[sample_idx]
402
+ clean_mask[sample_idx, first_sl] = 1.0
403
+
404
+ return t_frame, chunk_slices, chunk_t, clean_mask
405
+
406
+ def _get_df_action_guidance_scale(self, chunk_t: torch.Tensor) -> torch.Tensor:
407
+ # Apply cfg_scale when chunk_t is inside
408
+ # (cfg_interval_min, cfg_interval_max]; else 1.0. Upper bound
409
+ # is inclusive so that the first denoising step (chunk_t == 1.0) still
410
+ # receives CFG, matching diffusion-forcing guidance semantics.
411
+ low = self.cfg_interval_min
412
+ high = self.cfg_interval_max
413
+ interval_mask = (chunk_t <= high) & ((low == 0.0) | (chunk_t > low))
414
+ action_scale = torch.where(
415
+ interval_mask,
416
+ torch.full_like(chunk_t, self.cfg_scale),
417
+ torch.ones_like(chunk_t),
418
+ )
419
+ return action_scale
420
+
421
+ def logit_normal_warp(self, u: torch.Tensor) -> torch.Tensor:
422
+ """Give the training timesteps a logit-normal density.
423
+
424
+ ``u`` is the uniform bin grid from ``IncrementalTimesteps``. The
425
+ logit-normal inverse CDF is monotone, so it reshapes the density without
426
+ disturbing the non-decreasing noise ordering across chunks.
427
+ """
428
+ if self.P_std <= 0.0:
429
+ return u
430
+ z = torch.special.ndtri(u.to(torch.float64))
431
+ return torch.sigmoid(self.P_mean + self.P_std * z).to(dtype=u.dtype)
432
+
433
+ @staticmethod
434
+ def shift_timestep(t: torch.Tensor, shift: float) -> torch.Tensor:
435
+ """SD3-style timestep shift: t' = shift*t / (1 + (shift-1)*t).
436
+ Maps [0,1]->[0,1]; shift>1 biases towards higher t (more noise)."""
437
+ if shift == 1.0:
438
+ return t
439
+ return shift * t / (1.0 + (shift - 1.0) * t)
440
+
441
+ def sample_condition_t(self, shape: Tuple[int, ...], device: torch.device, dtype: torch.dtype) -> torch.Tensor:
442
+ if self.condition_noise_max_t <= 0.0:
443
+ return torch.zeros(shape, device=device, dtype=dtype)
444
+ return torch.rand(shape, device=device, dtype=dtype) * self.condition_noise_max_t
445
+
446
+ def forward_diffusion_forcing(
447
+ self,
448
+ latents: torch.Tensor,
449
+ cond_seq: torch.Tensor,
450
+ history_len: int = 1,
451
+ return_pred: bool = False,
452
+ ) -> torch.Tensor | Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
453
+ # ``history_len`` is accepted for API compatibility with train/sample CLI,
454
+ # but training clean-context length is sampled via Mode A/B (see
455
+ # ``_build_diffusion_forcing_timesteps``). Inference uses ``history_len``
456
+ # in ``generate_eval_latents_streaming``.
457
+ assert int(history_len) > 0, f"history_len must be > 0, got {history_len}"
458
+ assert latents.dim() == 5, f"latents must be (B, C, T, H, W), got {latents.shape}"
459
+ b, _, t, _, _ = latents.shape
460
+ assert cond_seq.shape[0] == b and cond_seq.shape[1] == t, (
461
+ f"cond_seq shape {cond_seq.shape} must match (B, T, D) with B={b}, T={t}"
462
+ )
463
+
464
+ cond_seq = self.drop_cond(cond_seq)
465
+ device = latents.device
466
+
467
+ t_frame, _, _, clean_mask = self._build_diffusion_forcing_timesteps(
468
+ b=b,
469
+ t=t,
470
+ device=device,
471
+ dtype=latents.dtype,
472
+ )
473
+
474
+ noise = torch.randn_like(latents)
475
+ v_target = latents - noise
476
+
477
+ t_view = t_frame.view(b, 1, t, 1, 1)
478
+ z = (1.0 - t_view) * latents + t_view * noise
479
+ v_pred = self.net(
480
+ z,
481
+ t_frame,
482
+ cond_seq,
483
+ temporal_causal=True,
484
+ chunk_size=self.df_chunk_size,
485
+ )
486
+
487
+ # --- v_loss (per-frame, excluding clean context) ---
488
+ diff = (v_target - v_pred) ** 2
489
+ diff = diff.mean(dim=(1, 3, 4)) # (B, T)
490
+ loss_mask = 1.0 - clean_mask # 0 on clean frames, 1 on noisy frames
491
+ v_loss = (diff * loss_mask).sum(dim=1) / loss_mask.sum(dim=1).clamp_min(1.0)
492
+ v_loss = v_loss.mean()
493
+ if not return_pred:
494
+ return v_loss
495
+
496
+ x_pred = z + (1.0 - t_view) * v_pred
497
+ clean_mask_5d = clean_mask.view(b, 1, t, 1, 1)
498
+ x_pred = x_pred * (1.0 - clean_mask_5d) + latents * clean_mask_5d
499
+ return v_loss, x_pred.detach(), t_frame.max(dim=1).values.detach()
500
+
501
+
502
+ class DiffusionForcingDenoiser(Denoiser):
503
+ def forward(
504
+ self,
505
+ latents: torch.Tensor,
506
+ cond_seq: torch.Tensor,
507
+ history_len: int = 1,
508
+ return_pred: bool = False,
509
+ ) -> torch.Tensor | Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
510
+ """Run one diffusion-forcing training step.
511
+
512
+ Returns the scalar loss, or ``(loss, x_pred, t_noise)`` when
513
+ ``return_pred`` is set: the detached ``(B, C, T, H, W)`` one-step clean
514
+ latent and the ``(B,)`` peak noise level, for logging videos.
515
+ """
516
+ return super().forward_diffusion_forcing(
517
+ latents=latents,
518
+ cond_seq=cond_seq,
519
+ history_len=history_len,
520
+ return_pred=return_pred,
521
+ )
522
+
523
+ # ------------------------------------------------------------------
524
+ # Streaming AR-diffusion inference with KV cache
525
+ # ------------------------------------------------------------------
526
+ @staticmethod
527
+ def _append_kv_cache(
528
+ cache: List[Optional[Tuple[torch.Tensor, torch.Tensor]]],
529
+ new_kv: List[Optional[Tuple[torch.Tensor, torch.Tensor]]],
530
+ ) -> List[Optional[Tuple[torch.Tensor, torch.Tensor]]]:
531
+ depth = len(cache)
532
+ out: List[Optional[Tuple[torch.Tensor, torch.Tensor]]] = [None] * depth
533
+ for i in range(depth):
534
+ k_new, v_new = new_kv[i]
535
+ if cache[i] is None:
536
+ out[i] = (k_new, v_new)
537
+ else:
538
+ k_old, v_old = cache[i]
539
+ out[i] = (
540
+ torch.cat([k_old, k_new], dim=-2),
541
+ torch.cat([v_old, v_new], dim=-2),
542
+ )
543
+ return out
544
+
545
+ @staticmethod
546
+ def _evict_and_shift_cache(
547
+ cache: List[Optional[Tuple[torch.Tensor, torch.Tensor]]],
548
+ drop_frames: int,
549
+ tokens_per_frame: int,
550
+ rope_module,
551
+ sink_frames: int = 0,
552
+ ) -> List[Optional[Tuple[torch.Tensor, torch.Tensor]]]:
553
+ """Evict ``drop_frames`` frames from the cache and renumber positions.
554
+
555
+ With ``sink_frames == 0`` (default): drop the leading ``drop_frames``
556
+ frames and re-rotate the remaining K so positions restart at 0 (pure
557
+ FIFO sliding window).
558
+
559
+ With ``sink_frames > 0`` (StreamingLLM-style attention sink): the first
560
+ ``sink_frames`` frames are pinned at positions ``[0, sink_frames)`` and
561
+ never dropped or re-rotated; only the *post-sink* window frames are
562
+ evicted (oldest first) and shifted down by ``drop_frames`` so they sit
563
+ contiguously right behind the sink (positions ``[sink_frames, ...)``).
564
+ Net layout stays contiguous ``[0, cache_frames)`` and inside the trained
565
+ RoPE range, while the true origin frame(s) stay resident as an anchor.
566
+ """
567
+ if drop_frames <= 0:
568
+ return cache
569
+ sink_frames = max(0, sink_frames)
570
+ sink_tokens = sink_frames * tokens_per_frame
571
+ drop_tokens = drop_frames * tokens_per_frame
572
+ depth = len(cache)
573
+ out: List[Optional[Tuple[torch.Tensor, torch.Tensor]]] = [None] * depth
574
+ for i in range(depth):
575
+ if cache[i] is None:
576
+ continue
577
+ k_old, v_old = cache[i]
578
+ # sink slice: kept verbatim (no eviction, no RoPE shift).
579
+ k_sink = k_old[..., :sink_tokens, :]
580
+ v_sink = v_old[..., :sink_tokens, :]
581
+ # window slice: drop the oldest ``drop_frames`` right after the sink,
582
+ # then renumber survivors down by ``drop_frames``.
583
+ k_rest = k_old[..., sink_tokens + drop_tokens:, :]
584
+ v_rest = v_old[..., sink_tokens + drop_tokens:, :]
585
+ if k_rest.numel() > 0:
586
+ k_rest = rope_module.rope_shift_time(-drop_frames, k_rest)
587
+ if sink_tokens > 0:
588
+ k_new = torch.cat([k_sink, k_rest], dim=-2)
589
+ v_new = torch.cat([v_sink, v_rest], dim=-2)
590
+ else:
591
+ k_new, v_new = k_rest, v_rest
592
+ out[i] = (k_new, v_new)
593
+ return out
594
+
595
+ @torch.no_grad()
596
+ def generate_eval_latents_streaming(
597
+ self,
598
+ latents: torch.Tensor,
599
+ cond_seq: torch.Tensor,
600
+ total_len: int,
601
+ history_len: int = 1,
602
+ max_cache_chunks: int = 16,
603
+ inflight_chunks: int = 4,
604
+ sink_frames: int = 0,
605
+ stream_decoder=None,
606
+ collect_stream_timing: bool = False,
607
+ noise: Optional[torch.Tensor] = None,
608
+ **kwargs,
609
+ ):
610
+ """Streaming AR-diffusion with KV cache (position-bounded, renumbered from 0).
611
+
612
+ The active attention window at any time is exactly
613
+ ``(max_cache_chunks + inflight_chunks) * df_chunk_size`` frames, which must
614
+ fit inside ``self.trained_num_frames`` to avoid RoPE extrapolation.
615
+
616
+ Committed chunks live in a per-block KV cache, logically numbered at
617
+ temporal positions ``[0, cache_frames)``. In-flight chunks sit at
618
+ ``[cache_frames, cache_frames + inflight_frames)``. When a chunk
619
+ completes denoising (its ``t`` hits 0) it is committed: we run a t=0
620
+ forward to obtain its K/V, append them to the cache, and if the cache
621
+ overflows we drop the leading frames and :meth:`rope_shift_time` the
622
+ remaining K to renumber positions back to 0.
623
+
624
+ When ``stream_decoder`` is provided (Wan2.2 ``StreamingVAEDecoder``), each
625
+ committed latent chunk is VAE-decoded immediately (decode-on-commit).
626
+ Concatenating those RGB chunks is bit-exact with batch ``vae_decode`` of
627
+ the same latents, so clip metrics are unchanged.
628
+
629
+ Args:
630
+ latents: ``(B, C, T_full, H, W)`` -- first ``history_len`` frames are
631
+ used as clean visual context.
632
+ cond_seq: ``(B, T_full, D)`` full action sequence.
633
+ total_len: number of latent frames to produce.
634
+ history_len: clean context length in frames; must be ``> 0`` and at
635
+ most ``max_cache_chunks * df_chunk_size`` for full-chunk
636
+ prefill. When ``history_len % df_chunk_size != 0`` (e.g.
637
+ image-to-video with ``history_len=1`` and ``df_chunk_size=2``),
638
+ the leading ``history_len // df_chunk_size`` chunks are
639
+ pre-filled into the KV cache, and the remaining
640
+ ``history_len % df_chunk_size`` frames are pinned to ``t=0``
641
+ inside the first in-flight chunk.
642
+ max_cache_chunks: max number of committed chunks retained in the
643
+ cache at any one time.
644
+ inflight_chunks: number of chunks simultaneously being denoised.
645
+ sink_frames: StreamingLLM-style attention-sink size in frames. 0
646
+ (default) = pure sliding window (no resident anchor). >0 pins the
647
+ first ``sink_frames`` committed frames (the true origin / clean
648
+ context) at cache positions ``[0, sink_frames)`` permanently;
649
+ they are never evicted or re-rotated, so a long rollout always
650
+ retains them as an anchor. Must be <= the cache capacity.
651
+ stream_decoder: optional streaming VAE decoder with
652
+ ``begin() / step(latents) / end()``. When set, returns
653
+ ``(latents, rgb_video)`` with RGB in ``[-1, 1]``; otherwise
654
+ returns latents only.
655
+ noise: optional ``(B, C, >=total_len, H, W)`` initial noise; pass a
656
+ fixed tensor to make repeated rollouts comparable. Sampled from
657
+ the global RNG when omitted.
658
+ """
659
+ del kwargs
660
+ device = latents.device
661
+ dtype = latents.dtype
662
+ net = self.net
663
+ chunk_size = self.df_chunk_size
664
+ inflight_frames = inflight_chunks * chunk_size
665
+ max_cache_frames = max_cache_chunks * chunk_size
666
+ active_frames = max_cache_frames + inflight_frames
667
+ sink_frames = max(0, int(sink_frames))
668
+ assert sink_frames <= max_cache_frames, (
669
+ f"[StreamingGen] sink_frames={sink_frames} exceeds cache capacity "
670
+ f"max_cache_frames={max_cache_frames}. Increase stream_max_cache_chunks."
671
+ )
672
+
673
+ trained_num_frames = int(getattr(self, "trained_num_frames", 0))
674
+ if trained_num_frames <= 0:
675
+ trained_num_frames = self.cfg.latent_frames
676
+ assert active_frames <= trained_num_frames, (
677
+ f"[StreamingGen] active window (cache={max_cache_frames} + "
678
+ f"inflight={inflight_frames} = {active_frames}) exceeds "
679
+ f"trained_num_frames={trained_num_frames}. Reduce "
680
+ f"--stream_max_cache_chunks or --stream_inflight_chunks."
681
+ )
682
+ assert not net.use_abs_pos, (
683
+ "[StreamingGen] requires use_abs_pos=False (relative RoPE only). "
684
+ "MiniWorld checkpoints should be trained with RoPE-only positioning."
685
+ )
686
+ assert self.df_ardiff_step > 0, (
687
+ "[StreamingGen] requires df_ardiff_step > 0 (AR-diffusion schedule)."
688
+ )
689
+
690
+ total_len = min(total_len, latents.shape[2], cond_seq.shape[1])
691
+
692
+ ctx_len = int(history_len)
693
+ assert ctx_len > 0, f"[StreamingGen] history_len must be > 0, got {ctx_len}"
694
+ # Sub-chunk ctx (e.g. ctx_len=1, chunk_size=2 for i2v) is supported via
695
+ # per-frame t=0 pinning inside the first in-flight chunk; we only
696
+ # pre-fill the *whole* leading chunks into the KV cache. The leftover
697
+ # ``ctx_len - n_full_ctx_frames`` frames stay in the in-flight window
698
+ # cache capacity.
699
+ n_full_ctx_chunks = ctx_len // chunk_size
700
+ n_full_ctx_frames = n_full_ctx_chunks * chunk_size
701
+ assert n_full_ctx_frames <= max_cache_frames, (
702
+ f"[StreamingGen] full-chunk history ({n_full_ctx_frames} frames "
703
+ f"= {n_full_ctx_chunks} chunks) exceeds cache capacity "
704
+ f"({max_cache_frames} frames = {max_cache_chunks} chunks). "
705
+ f"Increase --stream_max_cache_chunks."
706
+ )
707
+
708
+ b, c_ch, _, h, w = latents.shape
709
+ p_t, p_h, p_w = net.x_embedder.patch_size
710
+ _, h_total, w_total = net.x_embedder.input_size
711
+ grid_h = h_total // p_h
712
+ grid_w = w_total // p_w
713
+ tokens_per_frame = grid_h * grid_w
714
+ rope_module = net.feat_rope
715
+
716
+ use_cfg = float(self.cfg_scale) > 1.0
717
+ depth = net.depth
718
+ cache_cond: List[Optional[Tuple[torch.Tensor, torch.Tensor]]] = [None] * depth
719
+ cache_uncond: List[Optional[Tuple[torch.Tensor, torch.Tensor]]] = [None] * depth if use_cfg else []
720
+ cache_frames = 0
721
+
722
+ # --- Output buffer ---
723
+ if noise is None:
724
+ z_global = torch.randn(b, c_ch, total_len, h, w, device=device, dtype=dtype)
725
+ else:
726
+ assert noise.shape[:2] == (b, c_ch) and noise.shape[3:] == (h, w), (
727
+ f"[StreamingGen] noise shape {tuple(noise.shape)} does not match "
728
+ f"latents {tuple(latents.shape)}"
729
+ )
730
+ assert noise.shape[2] >= total_len, (
731
+ f"[StreamingGen] noise covers {noise.shape[2]} frames, need {total_len}"
732
+ )
733
+ # Cloned because the rollout denoises this buffer in place.
734
+ z_global = noise[:, :, :total_len].to(device=device, dtype=dtype).clone()
735
+ if ctx_len > 0:
736
+ z_global[:, :, :ctx_len] = latents[:, :, :ctx_len]
737
+
738
+ timing_enabled = bool(collect_stream_timing)
739
+ timing_start = None
740
+ dit_chunk_events: List[Dict[str, object]] = []
741
+ vae_chunk_events: List[Dict[str, object]] = []
742
+
743
+ def _timing_now() -> float:
744
+ if torch.cuda.is_available():
745
+ torch.cuda.synchronize(device)
746
+ return time.perf_counter()
747
+
748
+ if timing_enabled:
749
+ timing_start = _timing_now()
750
+
751
+ # --- Optional streaming VAE decode (decode-on-commit) ---
752
+ rgb_parts: List[torch.Tensor] = []
753
+ decoded_frames = 0
754
+
755
+ def _stream_decode_upto(end_frame: int, *, chunk_idx: Optional[int] = None, step_idx: Optional[int] = None) -> None:
756
+ nonlocal decoded_frames
757
+ if stream_decoder is None or end_frame <= decoded_frames:
758
+ return
759
+ start_frame = decoded_frames
760
+ t0 = _timing_now() if timing_enabled else None
761
+ rgb_parts.append(stream_decoder.step(z_global[:, :, start_frame:end_frame]))
762
+ t1 = _timing_now() if timing_enabled else None
763
+ if timing_enabled and timing_start is not None and t0 is not None and t1 is not None:
764
+ vae_chunk_events.append(
765
+ {
766
+ "chunk_idx": int(chunk_idx) if chunk_idx is not None else None,
767
+ "step_idx": int(step_idx) if step_idx is not None else None,
768
+ "start_frame": int(start_frame),
769
+ "end_frame": int(end_frame),
770
+ "generated": bool(chunk_idx is not None and chunk_idx >= n_full_ctx_chunks),
771
+ "start_sec": float(t0 - timing_start),
772
+ "end_sec": float(t1 - timing_start),
773
+ "duration_sec": float(t1 - t0),
774
+ }
775
+ )
776
+ decoded_frames = end_frame
777
+
778
+ if stream_decoder is not None:
779
+ stream_decoder.begin()
780
+
781
+ try:
782
+ # --- Pre-fill cache with clean history context ---
783
+ # Only fully-aligned ctx chunks go into the cache. Sub-chunk leftover
784
+ # (n_partial_ctx_frames) is pinned via per-frame t=0 inside the first
785
+ # in-flight chunk, see the in-flight forward block below.
786
+ if n_full_ctx_frames > 0:
787
+ ctx_frames = z_global[:, :, :n_full_ctx_frames]
788
+ ctx_cond = cond_seq[:, :n_full_ctx_frames]
789
+ ctx_t = torch.zeros(b, n_full_ctx_frames, device=device, dtype=dtype)
790
+
791
+ _, kv_cond_ctx = net.forward_with_cache(
792
+ ctx_frames, ctx_t, ctx_cond,
793
+ past_kv_list=None, current_position_offset=0,
794
+ return_kv=True, chunk_size=chunk_size,
795
+ )
796
+ cache_cond = list(kv_cond_ctx)
797
+ if use_cfg:
798
+ ctx_uncond, ctx_drop_uncond = self._make_uncond(ctx_cond)
799
+ _, kv_uncond_ctx = net.forward_with_cache(
800
+ ctx_frames, ctx_t, ctx_uncond,
801
+ past_kv_list=None, current_position_offset=0,
802
+ return_kv=True, chunk_size=chunk_size, cond_drop=ctx_drop_uncond,
803
+ )
804
+ cache_uncond = list(kv_uncond_ctx)
805
+ cache_frames = n_full_ctx_frames
806
+ # Decode clean context immediately (same order as batch decode).
807
+ _stream_decode_upto(n_full_ctx_frames)
808
+
809
+ # --- Global AR schedule ---
810
+ # Keep the final partial chunk. Training uses the same chunk layout
811
+ # (e.g. T=9, chunk_size=2 -> four 2-frame chunks plus one 1-frame
812
+ # chunk), so dropping it at inference changes the requested video
813
+ # length and the learned schedule.
814
+ total_chunks = (total_len + chunk_size - 1) // chunk_size
815
+ # Residence cap: a chunk can be updated for ~inflight*ar outer steps
816
+ # before the FIFO window must slide past it. When the *entire*
817
+ # sequence fits in the inflight window, nothing is force-evicted
818
+ # mid-denoise, so use the full sampler length (e.g. T=64, 100 steps).
819
+ residence_cap = inflight_chunks * max(self.df_ardiff_step, 1)
820
+ if total_chunks <= inflight_chunks:
821
+ effective_steps = int(self.steps)
822
+ else:
823
+ effective_steps = min(int(self.steps), residence_cap)
824
+ t_chunk_sched, t_next_chunk_sched, chunk_update_mask = (
825
+ self._build_chunk_sampling_schedule(
826
+ total_chunks=total_chunks,
827
+ device=device, dtype=dtype,
828
+ n_context_chunks=n_full_ctx_chunks,
829
+ effective_steps=effective_steps,
830
+ )
831
+ )
832
+ valid_intervals = self._compute_fifo_valid_intervals(
833
+ chunk_update_mask, total_chunks, max_chunks_in_window=inflight_chunks,
834
+ )
835
+
836
+ num_outer_steps = t_chunk_sched.shape[0]
837
+ self._set_last_eval_meta(
838
+ path="streaming",
839
+ total_chunks=total_chunks,
840
+ n_ctx_chunks=n_full_ctx_chunks,
841
+ num_outer_steps=num_outer_steps,
842
+ effective_steps=effective_steps,
843
+ )
844
+ self.last_eval_meta["cfg_enabled"] = bool(use_cfg)
845
+ self.last_eval_meta["stream_timing_enabled"] = bool(timing_enabled)
846
+ _print0(f"[StreamingGen] total_len={total_len}, total_chunks={total_chunks}, "
847
+ f"ctx_chunks={n_full_ctx_chunks}, chunk_size={chunk_size}, "
848
+ f"inflight_chunks={inflight_chunks}, max_cache_chunks={max_cache_chunks}, "
849
+ f"trained_num_frames={trained_num_frames}, active_frames={active_frames}, "
850
+ f"sink_frames={sink_frames}, "
851
+ f"effective_steps={effective_steps}, outer_steps={num_outer_steps}, "
852
+ f"ar_step={self.df_ardiff_step}, "
853
+ f"stream_decode={stream_decoder is not None}, cfg_enabled={use_cfg}")
854
+
855
+ last_win_sc = n_full_ctx_chunks
856
+ committed_chunks = set(range(n_full_ctx_chunks))
857
+ # VAE decode is tied to schedule completion (t_next==0), not KV
858
+ # eviction. With max_cache=0 + full inflight, the window may never
859
+ # slide, but chunks still finish and should decode immediately.
860
+ next_decode_ci = n_full_ctx_chunks
861
+
862
+ def _decode_finished_chunks(step_idx: int) -> None:
863
+ nonlocal next_decode_ci
864
+ if stream_decoder is None:
865
+ return
866
+ while next_decode_ci < total_chunks:
867
+ if float(t_next_chunk_sched[step_idx, next_decode_ci]) > 0.0:
868
+ break
869
+ end_f = min((next_decode_ci + 1) * chunk_size, total_len)
870
+ if timing_enabled and timing_start is not None:
871
+ t_dit = _timing_now()
872
+ dit_chunk_events.append(
873
+ {
874
+ "chunk_idx": int(next_decode_ci),
875
+ "step_idx": int(step_idx),
876
+ "end_frame": int(end_f),
877
+ "generated": bool(next_decode_ci >= n_full_ctx_chunks),
878
+ "complete_sec": float(t_dit - timing_start),
879
+ }
880
+ )
881
+ _stream_decode_upto(end_f, chunk_idx=next_decode_ci, step_idx=step_idx)
882
+ _print0(
883
+ f"[StreamingGen] decoded chunk {next_decode_ci}/{total_chunks} | "
884
+ f"latent_frames={decoded_frames}/{total_len} | "
885
+ f"step={step_idx}/{num_outer_steps}"
886
+ )
887
+ next_decode_ci += 1
888
+
889
+ for step in range(num_outer_steps):
890
+ win_sc, win_ec = valid_intervals[step]
891
+ # Enforce: committed chunks stay inside cache coverage.
892
+ # win_sc should equal committed chunks count. If win_sc < committed
893
+ # (shouldn't happen), clamp.
894
+ win_sc = max(win_sc, n_full_ctx_chunks)
895
+
896
+ # --- Commit newly-finished chunks into the cache ---
897
+ while last_win_sc < win_sc:
898
+ ci = last_win_sc
899
+ gsl = slice(ci * chunk_size, min((ci + 1) * chunk_size, total_len))
900
+ commit_frames = z_global[:, :, gsl]
901
+ commit_cond = cond_seq[:, gsl]
902
+ # t=0: chunk has finished denoising, treat as clean ctx going forward.
903
+ t_commit = torch.zeros(
904
+ b, commit_frames.shape[2], device=device, dtype=dtype,
905
+ )
906
+
907
+ _, kv_cond_new = net.forward_with_cache(
908
+ commit_frames, t_commit, commit_cond,
909
+ past_kv_list=cache_cond,
910
+ current_position_offset=cache_frames,
911
+ return_kv=True, chunk_size=chunk_size,
912
+ )
913
+ cache_cond = self._append_kv_cache(cache_cond, kv_cond_new)
914
+ if use_cfg:
915
+ commit_uncond, commit_drop_uncond = self._make_uncond(commit_cond)
916
+ _, kv_uncond_new = net.forward_with_cache(
917
+ commit_frames, t_commit, commit_uncond,
918
+ past_kv_list=cache_uncond,
919
+ current_position_offset=cache_frames,
920
+ return_kv=True, chunk_size=chunk_size, cond_drop=commit_drop_uncond,
921
+ )
922
+ cache_uncond = self._append_kv_cache(cache_uncond, kv_uncond_new)
923
+ cache_frames += commit_frames.shape[2]
924
+
925
+ if cache_frames > max_cache_frames:
926
+ # Never evict into the resident sink region.
927
+ drop = min(cache_frames - max_cache_frames,
928
+ cache_frames - sink_frames)
929
+ if drop > 0:
930
+ cache_cond = self._evict_and_shift_cache(
931
+ cache_cond, drop, tokens_per_frame, rope_module,
932
+ sink_frames=sink_frames,
933
+ )
934
+ if use_cfg:
935
+ cache_uncond = self._evict_and_shift_cache(
936
+ cache_uncond, drop, tokens_per_frame, rope_module,
937
+ sink_frames=sink_frames,
938
+ )
939
+ cache_frames -= drop
940
+
941
+ committed_chunks.add(ci)
942
+ _print0(f"[StreamingGen] committed chunk {ci}/{total_chunks} | "
943
+ f"cache_frames={cache_frames} | step={step}/{num_outer_steps}")
944
+ last_win_sc += 1
945
+
946
+ if win_ec <= win_sc:
947
+ _decode_finished_chunks(step)
948
+ continue
949
+
950
+ # --- In-flight forward ---
951
+ win_sf = win_sc * chunk_size
952
+ win_ef = min(win_ec * chunk_size, total_len)
953
+ inflight_z = z_global[:, :, win_sf:win_ef].clone()
954
+ inflight_cond = cond_seq[:, win_sf:win_ef]
955
+
956
+ n_inflight = win_ec - win_sc
957
+ inflight_chunk_slices = self._build_chunk_slices(win_ef - win_sf)
958
+
959
+ t_chunks = t_chunk_sched[step, win_sc:win_ec]
960
+ t_next_chunks = t_next_chunk_sched[step, win_sc:win_ec]
961
+ t_frame = self._broadcast_chunk_values_to_frames(
962
+ t_chunks.unsqueeze(0).expand(b, -1),
963
+ inflight_chunk_slices, win_ef - win_sf,
964
+ )
965
+ t_next_frame = self._broadcast_chunk_values_to_frames(
966
+ t_next_chunks.unsqueeze(0).expand(b, -1),
967
+ inflight_chunk_slices, win_ef - win_sf,
968
+ )
969
+
970
+ # Pin sub-chunk context frames inside this window to t=0 so dt=0
971
+ # and they aren't perturbed by the velocity update.
972
+ ctx_in_inflight = min(max(0, ctx_len - win_sf), win_ef - win_sf)
973
+ if ctx_in_inflight > 0:
974
+ t_frame[:, :ctx_in_inflight] = 0.0
975
+ t_next_frame[:, :ctx_in_inflight] = 0.0
976
+ inflight_z[:, :, :ctx_in_inflight] = latents[
977
+ :, :, win_sf:win_sf + ctx_in_inflight
978
+ ]
979
+
980
+ v_cond_pred, _ = net.forward_with_cache(
981
+ inflight_z, t_frame, inflight_cond,
982
+ past_kv_list=cache_cond,
983
+ current_position_offset=cache_frames,
984
+ return_kv=False, chunk_size=chunk_size,
985
+ )
986
+ if use_cfg:
987
+ inflight_uncond, inflight_drop_uncond = self._make_uncond(inflight_cond)
988
+ v_uncond_pred, _ = net.forward_with_cache(
989
+ inflight_z, t_frame, inflight_uncond,
990
+ past_kv_list=cache_uncond,
991
+ current_position_offset=cache_frames,
992
+ return_kv=False, chunk_size=chunk_size, cond_drop=inflight_drop_uncond,
993
+ )
994
+ else:
995
+ v_uncond_pred = None
996
+
997
+ update_mask_row = chunk_update_mask[step, win_sc:win_ec]
998
+ for lci in range(n_inflight):
999
+ if not bool(update_mask_row[lci]):
1000
+ continue
1001
+ sl = inflight_chunk_slices[lci]
1002
+ gsl = slice(
1003
+ (win_sc + lci) * chunk_size,
1004
+ min((win_sc + lci + 1) * chunk_size, total_len),
1005
+ )
1006
+ gl = gsl.stop - gsl.start
1007
+ if use_cfg:
1008
+ chunk_t_val = t_frame[:, sl].mean(dim=1)
1009
+ action_scale = self._get_df_action_guidance_scale(chunk_t_val)
1010
+ action_scale = action_scale.view(-1, 1, 1, 1, 1)
1011
+ assert v_uncond_pred is not None
1012
+ v_chunk = (
1013
+ v_uncond_pred[:, :, sl]
1014
+ + action_scale * (v_cond_pred[:, :, sl] - v_uncond_pred[:, :, sl])
1015
+ )
1016
+ else:
1017
+ v_chunk = v_cond_pred[:, :, sl]
1018
+ dt = (t_next_frame[:, sl] - t_frame[:, sl]).view(b, 1, -1, 1, 1)[:, :, :gl]
1019
+ z_global[:, :, gsl] = (
1020
+ z_global[:, :, gsl] - dt * v_chunk[:, :, :gl]
1021
+ )
1022
+
1023
+ # Re-pin clean ctx frames (numerical safety; dt should already be
1024
+ # 0 for them, but FP error can drift otherwise).
1025
+ if ctx_len > 0:
1026
+ z_global[:, :, :ctx_len] = latents[:, :, :ctx_len]
1027
+
1028
+ # Decode as soon as each leading chunk's schedule hits t=0.
1029
+ _decode_finished_chunks(step)
1030
+
1031
+ # Flush any remaining (e.g. final partial) frames for VAE.
1032
+ if decoded_frames < total_len:
1033
+ flush_ci = (total_len - 1) // chunk_size
1034
+ if timing_enabled and timing_start is not None:
1035
+ t_dit = _timing_now()
1036
+ dit_chunk_events.append(
1037
+ {
1038
+ "chunk_idx": int(flush_ci),
1039
+ "step_idx": int(num_outer_steps),
1040
+ "end_frame": int(total_len),
1041
+ "generated": bool(flush_ci >= n_full_ctx_chunks),
1042
+ "complete_sec": float(t_dit - timing_start),
1043
+ }
1044
+ )
1045
+ _stream_decode_upto(total_len, chunk_idx=flush_ci, step_idx=num_outer_steps)
1046
+ if timing_enabled and timing_start is not None:
1047
+ self.last_eval_meta["stream_timing"] = {
1048
+ "enabled": True,
1049
+ "cfg_enabled": bool(use_cfg),
1050
+ "start_sec": 0.0,
1051
+ "dit_chunk_events": dit_chunk_events,
1052
+ "vae_chunk_events": vae_chunk_events,
1053
+ }
1054
+ _print0(
1055
+ f"[StreamingGen] done. committed={len(committed_chunks)}/{total_chunks} "
1056
+ f"chunks, decoded_latent_frames={decoded_frames}/{total_len}, "
1057
+ f"effective_steps={effective_steps}."
1058
+ )
1059
+
1060
+ if stream_decoder is not None:
1061
+ assert rgb_parts, (
1062
+ "[StreamingGen] stream_decoder was set but no RGB chunks were produced"
1063
+ )
1064
+ return z_global, torch.cat(rgb_parts, dim=2)
1065
+ return z_global
1066
+ finally:
1067
+ if stream_decoder is not None:
1068
+ stream_decoder.end()
1069
+
1070
+
1071
+ def build_denoiser_from_mode(cfg: DenoiserConfig) -> Denoiser:
1072
+ """Build the public MiniWorld AR-diffusion denoiser."""
1073
+ return DiffusionForcingDenoiser(cfg)
miniworld/miniworld.py ADDED
@@ -0,0 +1,1044 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """MiniWorld: action / pose-conditioned streaming video DiT.
2
+
3
+ The model combines RoPE-only video tokens, action/pose conditioning streams,
4
+ AdaLN-LoRA modulation, and structured condition dropout for streaming world
5
+ modeling.
6
+
7
+ * **RoPE-only** positioning (absolute ``pos_embed`` removed entirely) so
8
+ train / streaming inference share the same position scheme.
9
+ * **AdaLN-LoRA modulation** (``adaln_mode="adaln_lora"``, default): a single
10
+ model-level modulation MLP produces a shared ``(B, T, 6D)`` term reused by
11
+ every block, plus a cheap per-block low-rank refinement ``D -> r -> 6D``.
12
+ This is the parameter-efficient middle ground between FLUX.2's
13
+ fully-shared modulation and the classic per-block full ``D -> 6D`` AdaLN.
14
+ Two more modes are provided for ablation: ``"fully_shared"`` (FLUX.2 style)
15
+ and ``"per_block"`` (classic DiT).
16
+ * **Separated conditioning streams** instead of the old
17
+ ``c_token = t_emb + y_emb``:
18
+ - timestep -> its own encoder, drives the base modulation;
19
+ - action -> DreamDojo-style encoder, *added* into the timestep /
20
+ AdaLN stream at per-latent-frame granularity (global-per-frame signal);
21
+ - pose -> ray-encoding, injected as a *separate* per-token spatial
22
+ modulation stream (lingbot-style), kept out of the timestep stream.
23
+ * **Structured condition dropout** with a learned null embedding, for
24
+ classifier-free guidance training.
25
+
26
+ The forward signature is designed for ``miniworld.denoiser.Denoiser``.
27
+ """
28
+
29
+ from __future__ import annotations
30
+
31
+ import math
32
+ from typing import Any, Callable, Dict, List, Optional, Tuple
33
+
34
+ import numpy as np
35
+ import torch
36
+ import torch.nn as nn
37
+ import torch.nn.functional as F
38
+ from einops import rearrange, repeat
39
+ from torch import Tensor
40
+ from torch.utils.checkpoint import checkpoint
41
+
42
+ from flash_attn import flash_attn_func
43
+
44
+ # FlexAttention is optional; disabled by default
45
+ create_block_mask = None
46
+ flex_attention = None
47
+
48
+
49
+ # --------------------------------------------------------------------------- #
50
+ # Attention masks (streaming) #
51
+ # --------------------------------------------------------------------------- #
52
+ def _build_temporal_chunkwise_attn_mask(
53
+ seq_len: int,
54
+ tokens_per_frame: int,
55
+ device: torch.device,
56
+ dtype: torch.dtype,
57
+ chunk_size: int,
58
+ ) -> torch.Tensor:
59
+ """Block-causal (chunk-wise) additive mask over the temporal axis."""
60
+ token_idx = torch.arange(seq_len, device=device)
61
+ frame_idx = token_idx // tokens_per_frame
62
+ chunk_idx = frame_idx // chunk_size
63
+ mask = chunk_idx.unsqueeze(1) >= chunk_idx.unsqueeze(0)
64
+ float_mask = torch.zeros((1, 1, seq_len, seq_len), device=device, dtype=dtype)
65
+ float_mask.masked_fill_(~mask.unsqueeze(0).unsqueeze(0), float("-inf"))
66
+ return float_mask
67
+
68
+
69
+ def _build_cached_block_causal_mask(
70
+ n_past: int,
71
+ n_cur: int,
72
+ tokens_per_frame: int,
73
+ chunk_size: int,
74
+ device: torch.device,
75
+ dtype: torch.dtype,
76
+ ) -> torch.Tensor:
77
+ """Additive mask for streaming forward with a KV cache.
78
+
79
+ Query length is ``n_cur`` (in-flight tokens); key/value length is
80
+ ``n_past + n_cur``. Past cache is always visible; current tokens use
81
+ block-causal attention along the temporal chunk axis.
82
+ """
83
+ total_kv = n_past + n_cur
84
+ float_mask = torch.zeros((1, 1, n_cur, total_kv), device=device, dtype=dtype)
85
+ if n_cur == 0:
86
+ return float_mask
87
+ token_idx = torch.arange(n_cur, device=device)
88
+ chunk_idx = (token_idx // tokens_per_frame) // chunk_size
89
+ cur_mask = chunk_idx.unsqueeze(1) >= chunk_idx.unsqueeze(0) # (n_cur, n_cur)
90
+ float_mask[0, 0, :, n_past:] = torch.where(
91
+ cur_mask,
92
+ torch.zeros((), device=device, dtype=dtype),
93
+ torch.full((), float("-inf"), device=device, dtype=dtype),
94
+ )
95
+ return float_mask
96
+
97
+
98
+ # --------------------------------------------------------------------------- #
99
+ # Basic layers #
100
+ # --------------------------------------------------------------------------- #
101
+ def modulate(x: Tensor, shift: Optional[Tensor], scale: Tensor) -> Tensor:
102
+ """AdaLN modulation. ``shift`` / ``scale`` may be ``(B, D)`` or ``(B, N, D)``."""
103
+ if scale.dim() == 2:
104
+ scale = scale.unsqueeze(1)
105
+ if shift is not None:
106
+ shift = shift.unsqueeze(1)
107
+ if shift is None:
108
+ return x * (1 + scale)
109
+ return x * (1 + scale) + shift
110
+
111
+
112
+ class RMSNorm(nn.Module):
113
+ def __init__(self, dim: int, eps: float = 1e-6) -> None:
114
+ super().__init__()
115
+ self.eps = eps
116
+ self.weight = nn.Parameter(torch.ones(dim))
117
+
118
+ def _norm(self, x: Tensor) -> Tensor:
119
+ return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps)
120
+
121
+ def forward(self, x: Tensor) -> Tensor:
122
+ return self._norm(x.float()).type_as(x) * self.weight
123
+
124
+
125
+ class SwiGLUFFN(nn.Module):
126
+ def __init__(
127
+ self,
128
+ in_features: int,
129
+ hidden_features: Optional[int] = None,
130
+ out_features: Optional[int] = None,
131
+ bias: bool = True,
132
+ ) -> None:
133
+ super().__init__()
134
+ out_features = out_features or in_features
135
+ hidden_features = hidden_features or in_features
136
+ self.w12 = nn.Linear(in_features, 2 * hidden_features, bias=bias)
137
+ self.w3 = nn.Linear(hidden_features, out_features, bias=bias)
138
+
139
+ def forward(self, x: Tensor) -> Tensor:
140
+ x1, x2 = self.w12(x).chunk(2, dim=-1)
141
+ return self.w3(F.silu(x1) * x2)
142
+
143
+
144
+ class PatchEmbed3D(nn.Module):
145
+ """(B, C, T, H, W) -> (B, N, D) via a 3D conv patchifier."""
146
+
147
+ def __init__(
148
+ self,
149
+ input_size: int | Tuple[int, int, int],
150
+ patch_size: int | Tuple[int, int, int],
151
+ in_chans: int,
152
+ embed_dim: int,
153
+ bias: bool = True,
154
+ ) -> None:
155
+ super().__init__()
156
+ if isinstance(input_size, int):
157
+ input_size = (input_size, input_size, input_size)
158
+ if isinstance(patch_size, int):
159
+ patch_size = (patch_size, patch_size, patch_size)
160
+ elif len(patch_size) == 2:
161
+ patch_size = (1, patch_size[0], patch_size[1])
162
+
163
+ self.input_size = input_size
164
+ self.patch_size = patch_size
165
+ self.in_chans = in_chans
166
+ self.embed_dim = embed_dim
167
+ self.proj = nn.Conv3d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size, bias=bias)
168
+ self.num_patches = (
169
+ (input_size[0] // patch_size[0])
170
+ * (input_size[1] // patch_size[1])
171
+ * (input_size[2] // patch_size[2])
172
+ )
173
+
174
+ def forward(self, x: Tensor) -> Tensor:
175
+ x = self.proj(x) # (B, D, T', H', W')
176
+ x = x.flatten(2).transpose(1, 2) # (B, N, D)
177
+ return x
178
+
179
+
180
+ class Attention(nn.Module):
181
+ def __init__(
182
+ self,
183
+ dim: int,
184
+ num_heads: int = 8,
185
+ qkv_bias: bool = False,
186
+ qk_norm: bool = False,
187
+ proj_drop: float = 0.0,
188
+ ) -> None:
189
+ super().__init__()
190
+ assert dim % num_heads == 0, "dim must be divisible by num_heads"
191
+ self.num_heads = num_heads
192
+ self.head_dim = dim // num_heads
193
+ self.scale = self.head_dim ** -0.5
194
+
195
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
196
+ self.q_norm = RMSNorm(self.head_dim) if qk_norm else nn.Identity()
197
+ self.k_norm = RMSNorm(self.head_dim) if qk_norm else nn.Identity()
198
+ self.proj = nn.Linear(dim, dim)
199
+ self.proj_drop = nn.Dropout(proj_drop)
200
+
201
+ def forward(
202
+ self,
203
+ x: Tensor,
204
+ rope: Optional[Callable] = None,
205
+ attn_mask: Optional[Tensor] = None,
206
+ past_kv: Optional[Tuple[Tensor, Tensor]] = None,
207
+ return_kv: bool = False,
208
+ ):
209
+ B, N, C = x.shape
210
+ in_dtype = x.dtype
211
+ qkv = self.qkv(x).reshape(B, N, 3, self.num_heads, self.head_dim).permute(2, 0, 3, 1, 4)
212
+ q, k, v = qkv.unbind(0)
213
+ q, k = self.q_norm(q), self.k_norm(k)
214
+
215
+ if rope is not None:
216
+ q = rope(q)
217
+ k = rope(k)
218
+
219
+ k_current, v_current = k, v
220
+ if past_kv is not None:
221
+ k_past, v_past = past_kv
222
+ k = torch.cat([k_past.to(dtype=k.dtype, device=k.device), k], dim=-2)
223
+ v = torch.cat([v_past.to(dtype=v.dtype, device=v.device), v], dim=-2)
224
+
225
+ if attn_mask is None and past_kv is None:
226
+ # flash-attn fast path expects (B, N, num_heads, head_dim)
227
+ q = q.transpose(1, 2).to(torch.bfloat16)
228
+ k = k.transpose(1, 2).to(torch.bfloat16)
229
+ v = v.transpose(1, 2).to(torch.bfloat16)
230
+ x = flash_attn_func(q, k, v, causal=False).transpose(1, 2)
231
+ else:
232
+ x = F.scaled_dot_product_attention(q, k, v, attn_mask=attn_mask)
233
+
234
+ x = x.transpose(1, 2).reshape(B, N, C).to(in_dtype)
235
+ x = self.proj_drop(self.proj(x))
236
+ if return_kv:
237
+ return x, (k_current, v_current)
238
+ return x
239
+
240
+
241
+ # --------------------------------------------------------------------------- #
242
+ # 3D rotary embedding #
243
+ # --------------------------------------------------------------------------- #
244
+ def broadcat(tensors, dim: int = -1):
245
+ num_tensors = len(tensors)
246
+ shape_lens = {len(t.shape) for t in tensors}
247
+ assert len(shape_lens) == 1, "tensors must all have the same number of dimensions"
248
+ shape_len = list(shape_lens)[0]
249
+ dim = (dim + shape_len) if dim < 0 else dim
250
+ dims = list(zip(*map(lambda t: list(t.shape), tensors)))
251
+ expandable_dims = [(i, val) for i, val in enumerate(dims) if i != dim]
252
+ assert all(len(set(t[1])) <= 2 for t in expandable_dims), "invalid broadcast dims"
253
+ max_dims = [(t[0], max(t[1])) for t in expandable_dims]
254
+ expanded_dims = [(t[0], (t[1],) * num_tensors) for t in max_dims]
255
+ expanded_dims.insert(dim, (dim, dims[dim]))
256
+ expandable_shapes = list(zip(*map(lambda t: t[1], expanded_dims)))
257
+ tensors = [t[0].expand(*t[1]) for t in zip(tensors, expandable_shapes)]
258
+ return torch.cat(tensors, dim=dim)
259
+
260
+
261
+ def rotate_half(x: Tensor) -> Tensor:
262
+ x = rearrange(x, "... (d r) -> ... d r", r=2)
263
+ x1, x2 = x.unbind(dim=-1)
264
+ x = torch.stack((-x2, x1), dim=-1)
265
+ return rearrange(x, "... d r -> ... (d r)")
266
+
267
+
268
+ class VisionRotaryEmbeddingFast3D(nn.Module):
269
+ """Axial 3D RoPE (time / height / width), borrowed from EVA/lightning_wm."""
270
+
271
+ def __init__(self, dim: int, num_frames: int, frame_height: int, frame_width: int, theta: int = 10000) -> None:
272
+ super().__init__()
273
+ dim_h = (dim // 3) // 2 * 2
274
+ dim_w = (dim // 3) // 2 * 2
275
+ dim_t = dim - dim_h - dim_w
276
+ if dim_t % 2 != 0:
277
+ dim_t -= 1
278
+
279
+ freqs_t = 1.0 / (theta ** (torch.arange(0, dim_t, 2)[: (dim_t // 2)].float() / dim_t))
280
+ freqs_h = 1.0 / (theta ** (torch.arange(0, dim_h, 2)[: (dim_h // 2)].float() / dim_h))
281
+ freqs_w = 1.0 / (theta ** (torch.arange(0, dim_w, 2)[: (dim_w // 2)].float() / dim_w))
282
+
283
+ self.register_buffer("base_freqs_t", freqs_t)
284
+ self.register_buffer("base_freqs_h", freqs_h)
285
+ self.register_buffer("base_freqs_w", freqs_w)
286
+ self.frame_height = frame_height
287
+ self.frame_width = frame_width
288
+ self.dim = dim
289
+ self.dim_t = dim_t
290
+ self.dim_h = dim_h
291
+ self.dim_w = dim_w
292
+ self._num_frames = num_frames
293
+
294
+ freqs_cos, freqs_sin = self._build_freqs(
295
+ num_frames, freqs_t, freqs_h, freqs_w, frame_height, frame_width, dim
296
+ )
297
+ self.register_buffer("freqs_cos", freqs_cos)
298
+ self.register_buffer("freqs_sin", freqs_sin)
299
+
300
+ @staticmethod
301
+ def _build_freqs(num_frames, freqs_t, freqs_h, freqs_w, fh, fw, dim, start_frame: int = 0):
302
+ device = freqs_t.device
303
+ t_time = torch.arange(start_frame, start_frame + num_frames, device=device, dtype=torch.float32)
304
+ t_height = torch.arange(fh, device=device, dtype=torch.float32)
305
+ t_width = torch.arange(fw, device=device, dtype=torch.float32)
306
+ ft = repeat(torch.einsum("n,d->nd", t_time, freqs_t), "... n -> ... (n r)", r=2)
307
+ fht = repeat(torch.einsum("n,d->nd", t_height, freqs_h), "... n -> ... (n r)", r=2)
308
+ fwt = repeat(torch.einsum("n,d->nd", t_width, freqs_w), "... n -> ... (n r)", r=2)
309
+ freqs = broadcat(
310
+ (ft.view(num_frames, 1, 1, -1), fht.view(1, fh, 1, -1), fwt.view(1, 1, fw, -1)), dim=-1
311
+ )
312
+ return freqs.cos().view(-1, dim), freqs.sin().view(-1, dim)
313
+
314
+ def _get_freqs(self, num_frames: int, device: torch.device, start_frame: int = 0):
315
+ if start_frame == 0 and num_frames == self._num_frames:
316
+ return self.freqs_cos, self.freqs_sin
317
+ return self._build_freqs(
318
+ num_frames,
319
+ self.base_freqs_t.to(device),
320
+ self.base_freqs_h.to(device),
321
+ self.base_freqs_w.to(device),
322
+ self.frame_height,
323
+ self.frame_width,
324
+ self.dim,
325
+ start_frame=start_frame,
326
+ )
327
+
328
+ def forward(self, t: Tensor, num_frames_override: Optional[int] = None, start_frame: int = 0) -> Tensor:
329
+ num_frames = num_frames_override if num_frames_override is not None else self._num_frames
330
+ cos, sin = self._get_freqs(num_frames, t.device, start_frame=start_frame)
331
+ return t * cos + rotate_half(t) * sin
332
+
333
+ def rope_shift_time(self, delta: int, cached: Tensor) -> Tensor:
334
+ """Re-rotate cached K/Q on the temporal axis by ``delta`` frames.
335
+
336
+ Given a tensor originally RoPE-rotated at temporal positions
337
+ ``[p, ..., p+N-1]``, returns it rotated as if positions were
338
+ ``[p+delta, ..., p+delta+N-1]``. Use ``delta=-k`` after evicting ``k``
339
+ leading frames from a streaming cache to renumber positions back to 0.
340
+ Only the temporal slice (first ``dim_t`` dims) is rotated; spatial dims
341
+ receive identity rotation.
342
+ """
343
+ if delta == 0 or cached.numel() == 0:
344
+ return cached
345
+ device = cached.device
346
+ out_dtype = cached.dtype
347
+
348
+ base_freqs_t = self.base_freqs_t.to(device=device, dtype=torch.float32)
349
+ angle_t = float(delta) * base_freqs_t
350
+ angle_t_rep = repeat(angle_t, "n -> (n r)", r=2)
351
+ cos_t = angle_t_rep.cos()
352
+ sin_t = angle_t_rep.sin()
353
+
354
+ rest = self.dim - self.dim_t
355
+ cos_rest = torch.ones(rest, device=device, dtype=torch.float32)
356
+ sin_rest = torch.zeros(rest, device=device, dtype=torch.float32)
357
+
358
+ cos_full = torch.cat([cos_t, cos_rest], dim=-1).to(out_dtype)
359
+ sin_full = torch.cat([sin_t, sin_rest], dim=-1).to(out_dtype)
360
+
361
+ return cached * cos_full + rotate_half(cached) * sin_full
362
+
363
+
364
+ # --------------------------------------------------------------------------- #
365
+ # Timestep embedding #
366
+ # --------------------------------------------------------------------------- #
367
+ class TimestepEmbedder(nn.Module):
368
+ """Scalar timestep -> D-dim vector (sinusoidal + MLP)."""
369
+
370
+ def __init__(self, hidden_size: int, freq_dim: int = 256) -> None:
371
+ super().__init__()
372
+ self.freq_dim = freq_dim
373
+ self.mlp = nn.Sequential(
374
+ nn.Linear(freq_dim, hidden_size, bias=True),
375
+ nn.SiLU(),
376
+ nn.Linear(hidden_size, hidden_size, bias=True),
377
+ )
378
+
379
+ @staticmethod
380
+ def timestep_embedding(t: Tensor, dim: int, max_period: int = 10000) -> Tensor:
381
+ half = dim // 2
382
+ freqs = torch.exp(
383
+ -math.log(max_period) * torch.arange(half, device=t.device, dtype=torch.float32) / half
384
+ )
385
+ args = t[:, None].float() * freqs[None]
386
+ emb = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
387
+ if dim % 2 == 1:
388
+ emb = torch.cat([emb, torch.zeros_like(emb[:, :1])], dim=-1)
389
+ return emb
390
+
391
+ def forward(self, t: Tensor) -> Tensor:
392
+ if t.dim() == 1:
393
+ return self.mlp(self.timestep_embedding(t, self.freq_dim))
394
+ if t.dim() == 2:
395
+ b, s = t.shape
396
+ emb = self.mlp(self.timestep_embedding(t.reshape(-1), self.freq_dim))
397
+ return emb.view(b, s, -1)
398
+ raise ValueError(f"Unsupported timestep shape: {t.shape}")
399
+
400
+
401
+ # --------------------------------------------------------------------------- #
402
+ # Conditioning encoders #
403
+ # --------------------------------------------------------------------------- #
404
+ class ActionEncoder(nn.Module):
405
+ """DreamDojo-style action encoder for a global per-frame action signal.
406
+
407
+ Input action condition is ``(B, T, cond_dim)`` where each latent frame ``t``
408
+ already packs its chunk of raw actions (the training pipeline builds
409
+ ``cond_dim = num_action_per_latent * action_dim``). Two MLP heads produce:
410
+
411
+ * ``emb_B_T_D`` -- added into the timestep embedding stream;
412
+ * ``mod_B_T_MD`` -- added into the (shared) AdaLN modulation stream,
413
+ where ``M = n_mod_chunks`` (6 here: attn shift/scale/gate + mlp
414
+ shift/scale/gate).
415
+
416
+ This mirrors Cosmos' ``action_embedder_B_D`` / ``action_embedder_B_3D``
417
+ but targets a 6-chunk modulation layout.
418
+ """
419
+
420
+ def __init__(self, cond_dim: int, hidden_size: int, n_mod_chunks: int = 6, hidden_mult: int = 4) -> None:
421
+ super().__init__()
422
+ hidden = hidden_size * hidden_mult
423
+ act = lambda: nn.GELU(approximate="tanh")
424
+ self.to_emb = nn.Sequential(
425
+ nn.Linear(cond_dim, hidden), act(), nn.Linear(hidden, hidden_size)
426
+ )
427
+ self.to_mod = nn.Sequential(
428
+ nn.Linear(cond_dim, hidden), act(), nn.Linear(hidden, n_mod_chunks * hidden_size)
429
+ )
430
+
431
+ def forward(self, action_B_T_C: Tensor) -> Tuple[Tensor, Tensor]:
432
+ return self.to_emb(action_B_T_C), self.to_mod(action_B_T_C)
433
+
434
+
435
+ class PoseEncoder(nn.Module):
436
+ """Ray-encoding -> per-token spatial AdaLN modulation (lingbot-style).
437
+
438
+ The pose condition is a per-pixel ray-encoding volume
439
+ ``(B, T, cond_dim, H_lat, W_lat)`` (see ``pose_utils.compute_ray_encoding``,
440
+ e.g. cond_dim = 180 for origin+direction with 15 NeRF frequencies).
441
+
442
+ Unlike ``ActionEncoder`` (a per-frame signal folded into the timestep
443
+ stream), pose is spatially varying, so it drives its *own* per-token
444
+ modulation stream ``(B, N, MD)`` that is added on top of the timestep /
445
+ action modulation inside every block. A residual MLP over the patchified
446
+ ray features mirrors lingbot's ``cam_injector`` before producing scale/shift.
447
+ """
448
+
449
+ def __init__(self, cond_dim: int, hidden_size: int, patch_size: int, n_mod_chunks: int = 6) -> None:
450
+ super().__init__()
451
+ self.patchify = nn.Conv2d(cond_dim, hidden_size, kernel_size=patch_size, stride=patch_size, bias=True)
452
+ self.res_mlp = nn.Sequential(
453
+ nn.Linear(hidden_size, hidden_size), nn.SiLU(), nn.Linear(hidden_size, hidden_size)
454
+ )
455
+ self.to_mod = nn.Linear(hidden_size, n_mod_chunks * hidden_size, bias=True)
456
+
457
+ def forward(self, pose_B_T_C_H_W: Tensor, b: int, grid_t: int) -> Tensor:
458
+ """Return per-token modulation ``(B, N, MD)`` with N = grid_t * h * w."""
459
+ y = rearrange(pose_B_T_C_H_W, "b t c h w -> (b t) c h w")
460
+ y = self.patchify(y) # ((B*T), D, h', w')
461
+ y = rearrange(y, "(b t) d h w -> b (t h w) d", b=b, t=grid_t)
462
+ y = y + self.res_mlp(y) # residual (lingbot cam_injector style)
463
+ return self.to_mod(y) # (B, N, MD)
464
+
465
+
466
+ # --------------------------------------------------------------------------- #
467
+ # Modulation (shared / lora) #
468
+ # --------------------------------------------------------------------------- #
469
+ _MODES = ("adaln_lora", "fully_shared", "per_block")
470
+
471
+
472
+ class BlockModulation(nn.Module):
473
+ """Per-block modulation producer, respecting the model-wide ``adaln_mode``.
474
+
475
+ * ``adaln_lora`` : ``shared_mod + lora(emb)`` where ``lora = SiLU -> D->r
476
+ -> r->MD`` (zero-init, so a block starts exactly at ``shared_mod``).
477
+ * ``fully_shared`` : ``shared_mod`` (no per-block params; FLUX.2 style).
478
+ * ``per_block`` : ``full(emb)`` with ``full = SiLU -> D->MD`` (classic
479
+ per-block AdaLN, zero-init).
480
+ """
481
+
482
+ def __init__(self, hidden_size: int, adaln_mode: str, n_mod_chunks: int = 6, lora_dim: int = 256) -> None:
483
+ super().__init__()
484
+ assert adaln_mode in _MODES, f"adaln_mode must be one of {_MODES}"
485
+ self.adaln_mode = adaln_mode
486
+ out = n_mod_chunks * hidden_size
487
+ if adaln_mode == "adaln_lora":
488
+ self.lora = nn.Sequential(
489
+ nn.SiLU(),
490
+ nn.Linear(hidden_size, lora_dim, bias=False),
491
+ nn.Linear(lora_dim, out, bias=False),
492
+ )
493
+ elif adaln_mode == "per_block":
494
+ self.full = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, out, bias=True))
495
+ # fully_shared: no parameters
496
+
497
+ def forward(self, emb: Tensor, shared_mod: Optional[Tensor], pose_mod: Optional[Tensor]) -> Tensor:
498
+ if self.adaln_mode == "adaln_lora":
499
+ mod = shared_mod + self.lora(emb)
500
+ elif self.adaln_mode == "fully_shared":
501
+ mod = shared_mod
502
+ else: # per_block
503
+ mod = self.full(emb)
504
+ if pose_mod is not None:
505
+ mod = mod + pose_mod
506
+ return mod
507
+
508
+
509
+ # --------------------------------------------------------------------------- #
510
+ # Blocks #
511
+ # --------------------------------------------------------------------------- #
512
+ class MiniWorldBlock(nn.Module):
513
+ def __init__(
514
+ self,
515
+ hidden_size: int,
516
+ num_heads: int,
517
+ adaln_mode: str,
518
+ mlp_ratio: float = 4.0,
519
+ use_qknorm: bool = False,
520
+ lora_dim: int = 256,
521
+ ) -> None:
522
+ super().__init__()
523
+ self.norm1 = RMSNorm(hidden_size)
524
+ self.norm2 = RMSNorm(hidden_size)
525
+ self.attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=use_qknorm)
526
+ mlp_hidden = int(hidden_size * mlp_ratio)
527
+ self.mlp = SwiGLUFFN(hidden_size, int(2 / 3 * mlp_hidden))
528
+ self.modulation = BlockModulation(hidden_size, adaln_mode, n_mod_chunks=6, lora_dim=lora_dim)
529
+
530
+ def forward(
531
+ self,
532
+ x: Tensor,
533
+ emb: Tensor,
534
+ shared_mod: Optional[Tensor],
535
+ pose_mod: Optional[Tensor],
536
+ feat_rope: Optional[Callable] = None,
537
+ attn_mask: Optional[Tensor] = None,
538
+ past_kv: Optional[Tuple[Tensor, Tensor]] = None,
539
+ return_kv: bool = False,
540
+ ):
541
+ mod = self.modulation(emb, shared_mod, pose_mod)
542
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = mod.chunk(6, dim=-1)
543
+
544
+ attn_result = self.attn(
545
+ modulate(self.norm1(x), shift_msa, scale_msa),
546
+ rope=feat_rope,
547
+ attn_mask=attn_mask,
548
+ past_kv=past_kv,
549
+ return_kv=return_kv,
550
+ )
551
+ if return_kv:
552
+ attn_out, new_kv = attn_result
553
+ else:
554
+ attn_out, new_kv = attn_result, None
555
+ x = x + gate_msa * attn_out
556
+ x = x + gate_mlp * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
557
+ if return_kv:
558
+ return x, new_kv
559
+ return x
560
+
561
+
562
+ class FinalLayer(nn.Module):
563
+ def __init__(self, hidden_size: int, patch_size: int, out_channels: int) -> None:
564
+ super().__init__()
565
+ self.norm = RMSNorm(hidden_size)
566
+ self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True)
567
+ self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 2 * hidden_size, bias=True))
568
+
569
+ def forward(self, x: Tensor, emb: Tensor) -> Tensor:
570
+ cond = self.adaLN_modulation(emb)
571
+ if cond.dim() == 2:
572
+ shift, scale = cond.chunk(2, dim=1)
573
+ else:
574
+ shift, scale = cond.chunk(2, dim=-1)
575
+ return self.linear(modulate(self.norm(x), shift, scale))
576
+
577
+
578
+ # --------------------------------------------------------------------------- #
579
+ # Main model #
580
+ # --------------------------------------------------------------------------- #
581
+ class MiniWorldModel(nn.Module):
582
+ """Action / pose-conditioned streaming video DiT (RoPE-only)."""
583
+
584
+ def __init__(
585
+ self,
586
+ in_channels: int,
587
+ hidden_size: int,
588
+ cond_dim: int,
589
+ depth: int,
590
+ num_heads: int,
591
+ patch_size: int,
592
+ input_size: int | Tuple[int, int],
593
+ num_frames: int = 9,
594
+ mlp_ratio: float = 4.0,
595
+ use_qknorm: bool = True,
596
+ use_checkpoint: bool = False,
597
+ cond_per_token: bool = False,
598
+ adaln_mode: str = "adaln_lora",
599
+ adaln_lora_dim: int = 256,
600
+ cond_dropout_prob: float = 0.0,
601
+ action_null_first: bool = True,
602
+ # Kept for checkpoint compatibility; MiniWorld always uses RoPE.
603
+ use_rope: bool = True,
604
+ use_abs_pos: bool = False,
605
+ ) -> None:
606
+ super().__init__()
607
+ assert adaln_mode in _MODES, f"adaln_mode must be one of {_MODES}"
608
+ assert use_rope, "MiniWorldModel is RoPE-only; use_rope must be True."
609
+ assert not use_abs_pos, "MiniWorldModel is RoPE-only; use_abs_pos must be False."
610
+
611
+ self.in_channels = in_channels
612
+ self.out_channels = in_channels
613
+ self.patch_size = patch_size
614
+ self.num_heads = num_heads
615
+ self.hidden_size = hidden_size
616
+ self.depth = depth
617
+ self.use_checkpoint = use_checkpoint
618
+ self.cond_per_token = cond_per_token
619
+ self.adaln_mode = adaln_mode
620
+ self.cond_dropout_prob = cond_dropout_prob
621
+ # Route the true first latent frame (the seed / initial observation,
622
+ # which has no preceding action) through the learned ``null_action``.
623
+ self.action_null_first = action_null_first
624
+ # RoPE-only: kept as attributes for downstream code / streaming asserts.
625
+ self.use_rope = True
626
+ self.use_abs_pos = False
627
+
628
+ input_size = (
629
+ (num_frames, input_size, input_size)
630
+ if isinstance(input_size, int)
631
+ else (num_frames,) + tuple(input_size)
632
+ )
633
+ self.x_embedder = PatchEmbed3D(
634
+ input_size=input_size,
635
+ patch_size=(1, patch_size, patch_size) if isinstance(patch_size, int) else patch_size,
636
+ in_chans=in_channels,
637
+ embed_dim=hidden_size,
638
+ bias=True,
639
+ )
640
+
641
+ # ---- conditioning streams -------------------------------------- #
642
+ self.t_embedder = TimestepEmbedder(hidden_size)
643
+ # RMSNorm on the (timestep [+ action]) embedding before it drives the
644
+ # AdaLN heads / final layer. Mirrors Cosmos/DreamDojo ``t_embedding_norm``:
645
+ # keeps the affine embedding well-conditioned once the action encoder
646
+ # sums a second, independently-scaled signal into the timestep stream.
647
+ self.emb_norm = RMSNorm(hidden_size)
648
+ # shared modulation head (base AdaLN term reused across all blocks)
649
+ self.shared_mod = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 6 * hidden_size, bias=True))
650
+
651
+ if cond_per_token:
652
+ # pose (ray-encoding) -> separate per-token spatial modulation.
653
+ self.pose_encoder = PoseEncoder(cond_dim, hidden_size, patch_size, n_mod_chunks=6)
654
+ self.action_encoder = None
655
+ else:
656
+ # action -> DreamDojo-style, folded into timestep / AdaLN stream.
657
+ self.action_encoder = ActionEncoder(cond_dim, hidden_size, n_mod_chunks=6)
658
+ self.pose_encoder = None
659
+ # learned null action for classifier-free guidance dropout.
660
+ self.null_action = nn.Parameter(torch.zeros(1, 1, cond_dim))
661
+
662
+ head_dim = hidden_size // num_heads
663
+ self.feat_rope = VisionRotaryEmbeddingFast3D(
664
+ dim=head_dim,
665
+ num_frames=num_frames,
666
+ frame_height=input_size[1] // patch_size,
667
+ frame_width=input_size[2] // patch_size,
668
+ )
669
+
670
+ self.blocks = nn.ModuleList(
671
+ [
672
+ MiniWorldBlock(
673
+ hidden_size=hidden_size,
674
+ num_heads=num_heads,
675
+ adaln_mode=adaln_mode,
676
+ mlp_ratio=mlp_ratio,
677
+ use_qknorm=use_qknorm,
678
+ lora_dim=adaln_lora_dim,
679
+ )
680
+ for _ in range(depth)
681
+ ]
682
+ )
683
+ self.final_layer = FinalLayer(hidden_size, patch_size, self.out_channels)
684
+ self.initialize_weights()
685
+
686
+ # ------------------------------------------------------------------ #
687
+ def initialize_weights(self) -> None:
688
+ def _basic_init(module):
689
+ if isinstance(module, nn.Linear):
690
+ nn.init.xavier_uniform_(module.weight)
691
+ if module.bias is not None:
692
+ nn.init.constant_(module.bias, 0)
693
+
694
+ self.apply(_basic_init)
695
+
696
+ w = self.x_embedder.proj.weight.data
697
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
698
+ nn.init.constant_(self.x_embedder.proj.bias, 0)
699
+
700
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
701
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
702
+
703
+ # AdaLN-zero: shared modulation starts at 0 -> identity blocks.
704
+ nn.init.constant_(self.shared_mod[-1].weight, 0)
705
+ nn.init.constant_(self.shared_mod[-1].bias, 0)
706
+
707
+ # zero-init per-block modulation refinement so blocks start at shared_mod.
708
+ for block in self.blocks:
709
+ if self.adaln_mode == "adaln_lora":
710
+ nn.init.constant_(block.modulation.lora[-1].weight, 0)
711
+ elif self.adaln_mode == "per_block":
712
+ nn.init.constant_(block.modulation.full[-1].weight, 0)
713
+ nn.init.constant_(block.modulation.full[-1].bias, 0)
714
+
715
+ # action stream: zero-init the modulation head so action ramps in.
716
+ if self.action_encoder is not None:
717
+ nn.init.constant_(self.action_encoder.to_mod[-1].weight, 0)
718
+ nn.init.constant_(self.action_encoder.to_mod[-1].bias, 0)
719
+
720
+ # pose stream: zero-init so pose modulation ramps in from identity.
721
+ if self.pose_encoder is not None:
722
+ nn.init.constant_(self.pose_encoder.to_mod.weight, 0)
723
+ nn.init.constant_(self.pose_encoder.to_mod.bias, 0)
724
+
725
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
726
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
727
+ nn.init.constant_(self.final_layer.linear.weight, 0)
728
+ nn.init.constant_(self.final_layer.linear.bias, 0)
729
+
730
+ # ------------------------------------------------------------------ #
731
+ def unpatchify(self, x: Tensor) -> Tensor:
732
+ b, n, _ = x.shape
733
+ c = self.out_channels
734
+ p_t, p_h, p_w = self.x_embedder.patch_size
735
+ t_in, h_in, w_in = self.x_embedder.input_size
736
+ grid_t, grid_h, grid_w = t_in // p_t, h_in // p_h, w_in // p_w
737
+ assert n == grid_t * grid_h * grid_w, f"seq len {n} != grid {grid_t}x{grid_h}x{grid_w}"
738
+ x = x.reshape(b, grid_t, grid_h, grid_w, p_t, p_h, p_w, c)
739
+ x = torch.einsum("nthwpqrc->nctphqwr", x)
740
+ return x.reshape(b, c, grid_t * p_t, grid_h * p_h, grid_w * p_w)
741
+
742
+ # ------------------------------------------------------------------ #
743
+ def _resolve_drop_mask(self, b: int, device, cond_drop: Optional[Tensor]) -> Optional[Tensor]:
744
+ """Resolve a per-sample CFG drop mask ``(B,)`` bool, or None.
745
+
746
+ Explicit ``cond_drop`` wins; otherwise sample from ``cond_dropout_prob``
747
+ while training. Shared by the action and pose streams so a dropped
748
+ sample is fully unconditional.
749
+ """
750
+ if cond_drop is not None:
751
+ return cond_drop.to(device=device, dtype=torch.bool).view(b)
752
+ if self.training and self.cond_dropout_prob > 0:
753
+ return torch.rand(b, device=device) < self.cond_dropout_prob
754
+ return None
755
+
756
+ def _build_conditioning(
757
+ self,
758
+ t: Tensor,
759
+ y: Optional[Tensor],
760
+ b: int,
761
+ grid_t: int,
762
+ n: int,
763
+ frame_ids: Tensor,
764
+ device,
765
+ dtype,
766
+ cond_drop: Optional[Tensor],
767
+ frame_offset: int = 0,
768
+ ) -> Tuple[Tensor, Optional[Tensor], Optional[Tensor]]:
769
+ """Return ``(emb_tok, shared_mod_tok, pose_mod_tok)`` all in per-token layout.
770
+
771
+ * ``emb_tok`` : ``(B, N, D)`` timestep(+action) embedding.
772
+ * ``shared_mod_tok``: ``(B, N, 6D)`` shared AdaLN modulation, or None
773
+ (``per_block`` mode does not use it).
774
+ * ``pose_mod_tok`` : ``(B, N, 6D)`` pose modulation, or None.
775
+
776
+ ``frame_offset`` is the absolute temporal index of this window's first
777
+ latent frame (0 for whole-clip / training / the first streaming window;
778
+ >0 for later streaming windows). It gates the ``action_null_first``
779
+ behaviour so only the true global frame 0 is treated as action-free.
780
+ """
781
+ # ---- timestep -> per-token embedding --------------------------- #
782
+ per_token_t = False
783
+ if t.dim() == 1:
784
+ t = t.view(b, 1).expand(b, grid_t)
785
+ elif t.dim() == 2:
786
+ if t.size(1) == n:
787
+ per_token_t = True
788
+ elif t.size(1) != grid_t:
789
+ raise ValueError(f"t shape {t.shape} != frames {grid_t} or tokens {n}")
790
+ else:
791
+ raise ValueError(f"Unsupported timestep shape: {t.shape}")
792
+ t_emb = self.t_embedder(t) # (B, grid_t, D) or (B, N, D)
793
+
794
+ drop_mask = self._resolve_drop_mask(b, device, cond_drop) # (B,) bool or None
795
+
796
+ # ---- action -> fold into timestep / modulation stream ---------- #
797
+ if self.action_encoder is not None:
798
+ null = self.null_action.to(device=device, dtype=dtype)
799
+ if y is None:
800
+ y = null.expand(b, grid_t, -1)
801
+ else:
802
+ if y.size(1) == 1:
803
+ y = y.expand(b, grid_t, -1)
804
+ assert y.size(1) == grid_t, f"action T {y.size(1)} != latent frames {grid_t}"
805
+ if drop_mask is not None:
806
+ m = drop_mask.view(b, 1, 1).to(y.dtype)
807
+ y = y * (1 - m) + null * m
808
+ # The true first latent frame is the seed / initial observation and
809
+ # has no preceding action -> use the learned null there. Only when
810
+ # this window actually starts at global frame 0 (frame_offset == 0),
811
+ # so later streaming windows keep their real per-frame actions.
812
+ if self.action_null_first and frame_offset == 0 and grid_t > 0:
813
+ y = y.clone()
814
+ y[:, 0:1, :] = null
815
+ a_emb, a_mod = self.action_encoder(y) # (B,grid_t,D), (B,grid_t,6D)
816
+ t_emb = t_emb + a_emb
817
+ action_mod = a_mod
818
+ else:
819
+ action_mod = None
820
+
821
+ # ---- normalize the combined timestep(+action) embedding -------- #
822
+ # Applied unconditionally (part of the timestep pipeline; also helps the
823
+ # pose / no-action paths). The 6D modulation deltas stay un-normed.
824
+ t_emb = self.emb_norm(t_emb)
825
+
826
+ # ---- broadcast per-frame -> per-token -------------------------- #
827
+ emb_tok = t_emb if per_token_t else t_emb[:, frame_ids, :] # (B, N, D)
828
+
829
+ shared_mod_tok: Optional[Tensor] = None
830
+ if self.adaln_mode in ("adaln_lora", "fully_shared"):
831
+ shared = self.shared_mod(emb_tok) # (B, N, 6D)
832
+ if action_mod is not None:
833
+ shared = shared + action_mod[:, frame_ids, :]
834
+ shared_mod_tok = shared
835
+ elif action_mod is not None:
836
+ # per_block mode has no shared term; route action modulation
837
+ # through the pose channel (both are additive per-token deltas).
838
+ action_mod = action_mod[:, frame_ids, :]
839
+
840
+ # ---- pose -> per-token spatial modulation ---------------------- #
841
+ pose_mod_tok: Optional[Tensor] = None
842
+ if self.pose_encoder is not None and y is not None:
843
+ pose_mod_tok = self.pose_encoder(y, b, grid_t) # (B, N, 6D)
844
+ if drop_mask is not None:
845
+ # dropped samples become unconditional -> zero pose modulation.
846
+ pose_mod_tok = pose_mod_tok * (~drop_mask).view(b, 1, 1).to(pose_mod_tok.dtype)
847
+
848
+ # in per_block mode, fold action delta into the pose channel
849
+ if self.adaln_mode == "per_block" and action_mod is not None:
850
+ pose_mod_tok = action_mod if pose_mod_tok is None else pose_mod_tok + action_mod
851
+
852
+ return emb_tok, shared_mod_tok, pose_mod_tok
853
+
854
+ # ------------------------------------------------------------------ #
855
+ def forward(
856
+ self,
857
+ x: Tensor,
858
+ t: Optional[Tensor] = None,
859
+ y: Optional[Tensor] = None,
860
+ use_fp16: bool = False,
861
+ temporal_causal: bool = False,
862
+ chunk_size: Optional[int] = None,
863
+ cond_drop: Optional[Tensor] = None,
864
+ frame_offset: int = 0,
865
+ ):
866
+ """Forward pass.
867
+
868
+ Args:
869
+ x: ``(B, C, T, H, W)`` latent video.
870
+ t: ``(B,)`` / ``(B, T')`` per-frame or ``(B, N)`` per-token timesteps.
871
+ y: condition. If ``cond_per_token`` -> ``(B, T', cond_dim, H, W)``
872
+ ray-encoding; else ``(B, T', cond_dim)`` action.
873
+ cond_drop: optional per-sample bool ``(B,)`` forcing the null / uncond
874
+ condition (for CFG). When None and training, sampled from
875
+ ``cond_dropout_prob``.
876
+
877
+ Returns:
878
+ ``v_pred`` of shape ``(B, C, T, H, W)``.
879
+ """
880
+ x = self.x_embedder(x)
881
+
882
+ p_t, p_h, p_w = self.x_embedder.patch_size
883
+ t_in, h_in, w_in = self.x_embedder.input_size
884
+ grid_t, grid_h, grid_w = t_in // p_t, h_in // p_h, w_in // p_w
885
+ tokens_per_frame = grid_h * grid_w
886
+ b, n, _ = x.shape
887
+ assert n == grid_t * tokens_per_frame, f"token len {n} != grid {grid_t}x{grid_h}x{grid_w}"
888
+ chunk_size = 1 if chunk_size is None else chunk_size
889
+
890
+ attn_mask = None
891
+ if temporal_causal:
892
+ attn_mask = _build_temporal_chunkwise_attn_mask(
893
+ seq_len=n,
894
+ tokens_per_frame=tokens_per_frame,
895
+ device=x.device,
896
+ dtype=x.dtype,
897
+ chunk_size=chunk_size,
898
+ )
899
+
900
+ frame_ids = torch.arange(n, device=x.device, dtype=torch.long) // tokens_per_frame
901
+ emb_tok, shared_mod_tok, pose_mod_tok = self._build_conditioning(
902
+ t, y, b, grid_t, n, frame_ids, x.device, x.dtype, cond_drop,
903
+ frame_offset=frame_offset,
904
+ )
905
+
906
+ for block in self.blocks:
907
+ if self.use_checkpoint:
908
+ x = checkpoint(
909
+ block, x, emb_tok, shared_mod_tok, pose_mod_tok, self.feat_rope, attn_mask,
910
+ use_reentrant=True,
911
+ )
912
+ else:
913
+ x = block(x, emb_tok, shared_mod_tok, pose_mod_tok, self.feat_rope, attn_mask)
914
+
915
+ # final layer uses the per-frame(-broadcast) timestep embedding
916
+ x = self.final_layer(x, emb_tok)
917
+ x = self.unpatchify(x)
918
+ return x
919
+
920
+ # ------------------------------------------------------------------ #
921
+ @torch.no_grad()
922
+ def forward_with_cache(
923
+ self,
924
+ x: Tensor,
925
+ t: Tensor,
926
+ y: Optional[Tensor] = None,
927
+ past_kv_list: Optional[List[Optional[Tuple[Tensor, Tensor]]]] = None,
928
+ current_position_offset: int = 0,
929
+ return_kv: bool = False,
930
+ chunk_size: int = 1,
931
+ cond_drop: Optional[Tensor] = None,
932
+ ):
933
+ """Streaming forward with optional KV cache injection (RoPE-only)."""
934
+ b, c_in, t_cur, h_in, w_in = x.shape
935
+ x = self.x_embedder(x)
936
+
937
+ p_t, p_h, p_w = self.x_embedder.patch_size
938
+ _, h_total, w_total = self.x_embedder.input_size
939
+ grid_h, grid_w = h_total // p_h, w_total // p_w
940
+ assert h_in == h_total and w_in == w_total, (
941
+ f"forward_with_cache expects {h_total}x{w_total}, got {h_in}x{w_in}"
942
+ )
943
+ grid_t_cur = t_cur // p_t
944
+ tokens_per_frame = grid_h * grid_w
945
+ n_cur = grid_t_cur * tokens_per_frame
946
+ assert x.shape[1] == n_cur, f"patch embed produced {x.shape[1]} tokens, expected {n_cur}"
947
+
948
+ if past_kv_list is None:
949
+ past_kv_list = [None] * self.depth
950
+ assert len(past_kv_list) == self.depth
951
+
952
+ n_past = 0
953
+ first_past = next((kv for kv in past_kv_list if kv is not None), None)
954
+ if first_past is not None:
955
+ n_past = int(first_past[0].shape[-2])
956
+ assert n_past == current_position_offset * tokens_per_frame, (
957
+ f"past token len {n_past} != offset*tokens_per_frame "
958
+ f"{current_position_offset * tokens_per_frame}"
959
+ )
960
+
961
+ attn_mask = _build_cached_block_causal_mask(
962
+ n_past=n_past,
963
+ n_cur=n_cur,
964
+ tokens_per_frame=tokens_per_frame,
965
+ chunk_size=max(1, int(chunk_size)),
966
+ device=x.device,
967
+ dtype=x.dtype,
968
+ )
969
+
970
+ def feat_rope_current(tt: Tensor) -> Tensor:
971
+ return self.feat_rope(tt, num_frames_override=grid_t_cur, start_frame=int(current_position_offset))
972
+
973
+ frame_ids = torch.arange(n_cur, device=x.device, dtype=torch.long) // tokens_per_frame
974
+ emb_tok, shared_mod_tok, pose_mod_tok = self._build_conditioning(
975
+ t, y, b, grid_t_cur, n_cur, frame_ids, x.device, x.dtype, cond_drop,
976
+ frame_offset=int(current_position_offset),
977
+ )
978
+
979
+ new_kv_list: List[Optional[Tuple[Tensor, Tensor]]] = [None] * self.depth
980
+ for idx, block in enumerate(self.blocks):
981
+ out = block(
982
+ x, emb_tok, shared_mod_tok, pose_mod_tok, feat_rope_current, attn_mask,
983
+ past_kv_list[idx], return_kv,
984
+ )
985
+ if return_kv:
986
+ x, new_kv_list[idx] = out
987
+ else:
988
+ x = out
989
+
990
+ x = self.final_layer(x, emb_tok)
991
+ c_out = self.out_channels
992
+ x_vid = x.reshape(b, grid_t_cur, grid_h, grid_w, p_t, p_h, p_w, c_out)
993
+ x_vid = torch.einsum("nthwpqrc->nctphqwr", x_vid)
994
+ v_pred = x_vid.reshape(b, c_out, grid_t_cur * p_t, grid_h * p_h, grid_w * p_w)
995
+
996
+ if return_kv:
997
+ return v_pred, new_kv_list
998
+ return v_pred, None
999
+
1000
+
1001
+ # --------------------------------------------------------------------------- #
1002
+ # Factory configs #
1003
+ # --------------------------------------------------------------------------- #
1004
+ # Scaling-law ladder. All use patch_size=1; approx param counts are for the
1005
+ # action mode (cond_dim~128); pose mode differs by only a few tens of M.
1006
+ #
1007
+ # public name hidden depth heads head_dim ~params
1008
+ # B 768 12 12 64 ~0.12B
1009
+ # L 1024 24 16 64 ~0.39B
1010
+ # 0.5B 1152 28 16 72 ~0.55B
1011
+ # 1B 1536 28 12 128 ~0.96B
1012
+ # 3B 2560 32 20 128 ~2.9B
1013
+ def MiniWorld_B(**kwargs):
1014
+ """~0.12B. hidden=768, depth=12, heads=12 (head_dim=64)."""
1015
+ return MiniWorldModel(depth=12, hidden_size=768, num_heads=12, patch_size=1, **kwargs)
1016
+
1017
+
1018
+ def MiniWorld_L(**kwargs):
1019
+ """~0.39B. hidden=1024, depth=24, heads=16 (head_dim=64)."""
1020
+ return MiniWorldModel(depth=24, hidden_size=1024, num_heads=16, patch_size=1, **kwargs)
1021
+
1022
+
1023
+ def MiniWorld_0_5B(**kwargs):
1024
+ """~0.55B. hidden=1152, depth=28, heads=16 (head_dim=72)."""
1025
+ return MiniWorldModel(depth=28, hidden_size=1152, num_heads=16, patch_size=1, **kwargs)
1026
+
1027
+
1028
+ def MiniWorld_1B(**kwargs):
1029
+ """~0.96B. hidden=1536, depth=28, heads=12 (head_dim=128)."""
1030
+ return MiniWorldModel(depth=28, hidden_size=1536, num_heads=12, patch_size=1, **kwargs)
1031
+
1032
+
1033
+ def MiniWorld_3B(**kwargs):
1034
+ """~2.9B. hidden=2560, depth=32, heads=20 (head_dim=128)."""
1035
+ return MiniWorldModel(depth=32, hidden_size=2560, num_heads=20, patch_size=1, **kwargs)
1036
+
1037
+
1038
+ MiniWorldModels = {
1039
+ "B": MiniWorld_B,
1040
+ "L": MiniWorld_L,
1041
+ "0.5B": MiniWorld_0_5B,
1042
+ "1B": MiniWorld_1B,
1043
+ "3B": MiniWorld_3B,
1044
+ }
miniworld/vae/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ """VAE helpers for MiniWorld."""
2
+
3
+ from miniworld.vae.codec import StreamingVAEDecoder, load_wan22_vae, vae_decode, vae_encode
4
+
5
+ __all__ = ["StreamingVAEDecoder", "load_wan22_vae", "vae_decode", "vae_encode"]
6
+
miniworld/vae/codec.py ADDED
@@ -0,0 +1,98 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """WAN2.2 VAE loading, encode/decode, and streaming decode helpers."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any, Mapping
8
+
9
+ import torch
10
+ from torch.distributed import get_rank
11
+
12
+ from miniworld.vae.wan22_vae import Wan2_2_VAE
13
+
14
+
15
+ def is_main_process() -> bool:
16
+ """Return whether the current process should print user-facing logs."""
17
+ return int(os.environ.get("RANK", "0")) == 0
18
+
19
+
20
+ def print0(message: str) -> None:
21
+ """Print only from rank 0."""
22
+ if is_main_process():
23
+ print(message, flush=True)
24
+
25
+
26
+ def get_rank_id() -> int:
27
+ """Return distributed rank, or 0 when torch.distributed is inactive."""
28
+ if torch.distributed.is_available() and torch.distributed.is_initialized():
29
+ return int(get_rank())
30
+ return 0
31
+
32
+
33
+ def _get_arg(args: Mapping[str, Any] | object, name: str, default: Any = None) -> Any:
34
+ if isinstance(args, Mapping):
35
+ return args.get(name, default)
36
+ return getattr(args, name, default)
37
+
38
+
39
+ def load_wan22_vae(args: Mapping[str, Any] | object) -> Wan2_2_VAE:
40
+ """Load a frozen WAN2.2 VAE on the current CUDA rank."""
41
+ checkpoint = _get_arg(args, "vae_checkpoint")
42
+ if checkpoint is None:
43
+ raise ValueError("vae_checkpoint is required")
44
+ checkpoint_path = Path(checkpoint)
45
+ if not checkpoint_path.exists():
46
+ raise FileNotFoundError(f"WAN2.2 VAE checkpoint not found: {checkpoint_path}")
47
+
48
+ device = torch.device(f"cuda:{get_rank_id()}" if torch.cuda.is_available() else "cpu")
49
+ vae = Wan2_2_VAE(vae_pth=os.fspath(checkpoint_path), device=device)
50
+ vae.model.requires_grad_(False)
51
+ vae.model.eval()
52
+ print0(f"WAN2.2 VAE parameters: {sum(p.numel() for p in vae.model.parameters()):,}")
53
+ return vae
54
+
55
+
56
+ def vae_encode(vae: Wan2_2_VAE, video: torch.Tensor) -> torch.Tensor:
57
+ """Encode RGB video in ``[-1, 1]`` to WAN2.2 latents."""
58
+ total_frames = video.shape[2]
59
+ target_frames = ((total_frames - 1) // 4) * 4 + 1
60
+ return vae.encode(video[:, :, :target_frames])
61
+
62
+
63
+ @torch.no_grad()
64
+ def vae_decode(vae: Wan2_2_VAE, latents: torch.Tensor) -> torch.Tensor:
65
+ """Decode WAN2.2 latents to RGB video in ``[-1, 1]``."""
66
+ return vae.decode(latents)
67
+
68
+
69
+ class StreamingVAEDecoder:
70
+ """Causal streaming WAN2.2 decode session."""
71
+
72
+ def __init__(self, vae: Wan2_2_VAE) -> None:
73
+ self.vae = vae
74
+ self._active = False
75
+
76
+ def begin(self) -> None:
77
+ self.vae.decode_stream_begin()
78
+ self._active = True
79
+
80
+ def step(self, latents_chunk: torch.Tensor) -> torch.Tensor:
81
+ """Decode one latent chunk to RGB frames."""
82
+ if not self._active:
83
+ raise RuntimeError("StreamingVAEDecoder.begin() must be called before step()")
84
+ return self.vae.decode_stream_step(latents_chunk)
85
+
86
+ def end(self) -> None:
87
+ if self._active:
88
+ self.vae.decode_stream_end()
89
+ self._active = False
90
+
91
+ def decode_all(self, latents: torch.Tensor) -> torch.Tensor:
92
+ """Stream-decode a complete latent tensor."""
93
+ self.begin()
94
+ try:
95
+ return self.step(latents)
96
+ finally:
97
+ self.end()
98
+
miniworld/vae/wan22_vae.py ADDED
@@ -0,0 +1,1093 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
2
+ import logging
3
+
4
+ import torch
5
+ import torch.nn as nn
6
+ import torch.nn.functional as F
7
+ from einops import rearrange
8
+
9
+ __all__ = [
10
+ "Wan2_2_VAE",
11
+ ]
12
+
13
+ CACHE_T = 2
14
+
15
+
16
+ class CausalConv3d(nn.Conv3d):
17
+ """
18
+ Causal 3d convolusion.
19
+ """
20
+
21
+ def __init__(self, *args, **kwargs):
22
+ super().__init__(*args, **kwargs)
23
+ self._padding = (
24
+ self.padding[2],
25
+ self.padding[2],
26
+ self.padding[1],
27
+ self.padding[1],
28
+ 2 * self.padding[0],
29
+ 0,
30
+ )
31
+ self.padding = (0, 0, 0)
32
+
33
+ def forward(self, x, cache_x=None):
34
+ padding = list(self._padding)
35
+ if cache_x is not None and self._padding[4] > 0:
36
+ cache_x = cache_x.to(x.device)
37
+ x = torch.cat([cache_x, x], dim=2)
38
+ padding[4] -= cache_x.shape[2]
39
+ x = F.pad(x, padding)
40
+
41
+ return super().forward(x)
42
+
43
+
44
+ class RMS_norm(nn.Module):
45
+
46
+ def __init__(self, dim, channel_first=True, images=True, bias=False):
47
+ super().__init__()
48
+ broadcastable_dims = (1, 1, 1) if not images else (1, 1)
49
+ shape = (dim, *broadcastable_dims) if channel_first else (dim,)
50
+
51
+ self.channel_first = channel_first
52
+ self.scale = dim**0.5
53
+ self.gamma = nn.Parameter(torch.ones(shape))
54
+ self.bias = nn.Parameter(torch.zeros(shape)) if bias else 0.0
55
+
56
+ def forward(self, x):
57
+ return (F.normalize(x, dim=(1 if self.channel_first else -1)) *
58
+ self.scale * self.gamma + self.bias)
59
+
60
+
61
+ class Upsample(nn.Upsample):
62
+
63
+ def forward(self, x):
64
+ """
65
+ Fix bfloat16 support for nearest neighbor interpolation.
66
+ """
67
+ return super().forward(x.float()).type_as(x)
68
+
69
+
70
+ class Resample(nn.Module):
71
+
72
+ def __init__(self, dim, mode):
73
+ assert mode in (
74
+ "none",
75
+ "upsample2d",
76
+ "upsample3d",
77
+ "downsample2d",
78
+ "downsample3d",
79
+ )
80
+ super().__init__()
81
+ self.dim = dim
82
+ self.mode = mode
83
+
84
+ # layers
85
+ if mode == "upsample2d":
86
+ self.resample = nn.Sequential(
87
+ Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
88
+ nn.Conv2d(dim, dim, 3, padding=1),
89
+ )
90
+ elif mode == "upsample3d":
91
+ self.resample = nn.Sequential(
92
+ Upsample(scale_factor=(2.0, 2.0), mode="nearest-exact"),
93
+ nn.Conv2d(dim, dim, 3, padding=1),
94
+ # nn.Conv2d(dim, dim//2, 3, padding=1)
95
+ )
96
+ self.time_conv = CausalConv3d(
97
+ dim, dim * 2, (3, 1, 1), padding=(1, 0, 0))
98
+ elif mode == "downsample2d":
99
+ self.resample = nn.Sequential(
100
+ nn.ZeroPad2d((0, 1, 0, 1)),
101
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
102
+ elif mode == "downsample3d":
103
+ self.resample = nn.Sequential(
104
+ nn.ZeroPad2d((0, 1, 0, 1)),
105
+ nn.Conv2d(dim, dim, 3, stride=(2, 2)))
106
+ self.time_conv = CausalConv3d(
107
+ dim, dim, (3, 1, 1), stride=(2, 1, 1), padding=(0, 0, 0))
108
+ else:
109
+ self.resample = nn.Identity()
110
+
111
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
112
+ b, c, t, h, w = x.size()
113
+ if self.mode == "upsample3d":
114
+ if feat_cache is not None:
115
+ idx = feat_idx[0]
116
+ if feat_cache[idx] is None:
117
+ feat_cache[idx] = "Rep"
118
+ feat_idx[0] += 1
119
+ else:
120
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
121
+ if (cache_x.shape[2] < 2 and feat_cache[idx] is not None and
122
+ feat_cache[idx] != "Rep"):
123
+ # cache last frame of last two chunk
124
+ cache_x = torch.cat(
125
+ [
126
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
127
+ cache_x.device),
128
+ cache_x,
129
+ ],
130
+ dim=2,
131
+ )
132
+ if (cache_x.shape[2] < 2 and feat_cache[idx] is not None and
133
+ feat_cache[idx] == "Rep"):
134
+ cache_x = torch.cat(
135
+ [
136
+ torch.zeros_like(cache_x).to(cache_x.device),
137
+ cache_x
138
+ ],
139
+ dim=2,
140
+ )
141
+ if feat_cache[idx] == "Rep":
142
+ x = self.time_conv(x)
143
+ else:
144
+ x = self.time_conv(x, feat_cache[idx])
145
+ feat_cache[idx] = cache_x
146
+ feat_idx[0] += 1
147
+ x = x.reshape(b, 2, c, t, h, w)
148
+ x = torch.stack((x[:, 0, :, :, :, :], x[:, 1, :, :, :, :]),
149
+ 3)
150
+ x = x.reshape(b, c, t * 2, h, w)
151
+ t = x.shape[2]
152
+ x = rearrange(x, "b c t h w -> (b t) c h w")
153
+ x = self.resample(x)
154
+ x = rearrange(x, "(b t) c h w -> b c t h w", t=t)
155
+
156
+ if self.mode == "downsample3d":
157
+ if feat_cache is not None:
158
+ idx = feat_idx[0]
159
+ if feat_cache[idx] is None:
160
+ feat_cache[idx] = x.clone()
161
+ feat_idx[0] += 1
162
+ else:
163
+ cache_x = x[:, :, -1:, :, :].clone()
164
+ x = self.time_conv(
165
+ torch.cat([feat_cache[idx][:, :, -1:, :, :], x], 2))
166
+ feat_cache[idx] = cache_x
167
+ feat_idx[0] += 1
168
+ return x
169
+
170
+ def init_weight(self, conv):
171
+ conv_weight = conv.weight.detach().clone()
172
+ nn.init.zeros_(conv_weight)
173
+ c1, c2, t, h, w = conv_weight.size()
174
+ one_matrix = torch.eye(c1, c2)
175
+ init_matrix = one_matrix
176
+ nn.init.zeros_(conv_weight)
177
+ conv_weight.data[:, :, 1, 0, 0] = init_matrix # * 0.5
178
+ conv.weight = nn.Parameter(conv_weight)
179
+ nn.init.zeros_(conv.bias.data)
180
+
181
+ def init_weight2(self, conv):
182
+ conv_weight = conv.weight.data.detach().clone()
183
+ nn.init.zeros_(conv_weight)
184
+ c1, c2, t, h, w = conv_weight.size()
185
+ init_matrix = torch.eye(c1 // 2, c2)
186
+ conv_weight[:c1 // 2, :, -1, 0, 0] = init_matrix
187
+ conv_weight[c1 // 2:, :, -1, 0, 0] = init_matrix
188
+ conv.weight = nn.Parameter(conv_weight)
189
+ nn.init.zeros_(conv.bias.data)
190
+
191
+
192
+ class ResidualBlock(nn.Module):
193
+
194
+ def __init__(self, in_dim, out_dim, dropout=0.0):
195
+ super().__init__()
196
+ self.in_dim = in_dim
197
+ self.out_dim = out_dim
198
+
199
+ # layers
200
+ self.residual = nn.Sequential(
201
+ RMS_norm(in_dim, images=False),
202
+ nn.SiLU(),
203
+ CausalConv3d(in_dim, out_dim, 3, padding=1),
204
+ RMS_norm(out_dim, images=False),
205
+ nn.SiLU(),
206
+ nn.Dropout(dropout),
207
+ CausalConv3d(out_dim, out_dim, 3, padding=1),
208
+ )
209
+ self.shortcut = (
210
+ CausalConv3d(in_dim, out_dim, 1)
211
+ if in_dim != out_dim else nn.Identity())
212
+
213
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
214
+ h = self.shortcut(x)
215
+ for layer in self.residual:
216
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
217
+ idx = feat_idx[0]
218
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
219
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
220
+ # cache last frame of last two chunk
221
+ cache_x = torch.cat(
222
+ [
223
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
224
+ cache_x.device),
225
+ cache_x,
226
+ ],
227
+ dim=2,
228
+ )
229
+ x = layer(x, feat_cache[idx])
230
+ feat_cache[idx] = cache_x
231
+ feat_idx[0] += 1
232
+ else:
233
+ x = layer(x)
234
+ return x + h
235
+
236
+
237
+ class AttentionBlock(nn.Module):
238
+ """
239
+ Causal self-attention with a single head.
240
+ """
241
+
242
+ def __init__(self, dim):
243
+ super().__init__()
244
+ self.dim = dim
245
+
246
+ # layers
247
+ self.norm = RMS_norm(dim)
248
+ self.to_qkv = nn.Conv2d(dim, dim * 3, 1)
249
+ self.proj = nn.Conv2d(dim, dim, 1)
250
+
251
+ # zero out the last layer params
252
+ nn.init.zeros_(self.proj.weight)
253
+
254
+ def forward(self, x):
255
+ identity = x
256
+ b, c, t, h, w = x.size()
257
+ x = rearrange(x, "b c t h w -> (b t) c h w")
258
+ x = self.norm(x)
259
+ # compute query, key, value
260
+ q, k, v = (
261
+ self.to_qkv(x).reshape(b * t, 1, c * 3,
262
+ -1).permute(0, 1, 3,
263
+ 2).contiguous().chunk(3, dim=-1))
264
+
265
+ # apply attention
266
+ x = F.scaled_dot_product_attention(
267
+ q,
268
+ k,
269
+ v,
270
+ )
271
+ x = x.squeeze(1).permute(0, 2, 1).reshape(b * t, c, h, w)
272
+
273
+ # output
274
+ x = self.proj(x)
275
+ x = rearrange(x, "(b t) c h w-> b c t h w", t=t)
276
+ return x + identity
277
+
278
+
279
+ def patchify(x, patch_size):
280
+ if patch_size == 1:
281
+ return x
282
+ if x.dim() == 4:
283
+ x = rearrange(
284
+ x, "b c (h q) (w r) -> b (c r q) h w", q=patch_size, r=patch_size)
285
+ elif x.dim() == 5:
286
+ x = rearrange(
287
+ x,
288
+ "b c f (h q) (w r) -> b (c r q) f h w",
289
+ q=patch_size,
290
+ r=patch_size,
291
+ )
292
+ else:
293
+ raise ValueError(f"Invalid input shape: {x.shape}")
294
+
295
+ return x
296
+
297
+
298
+ def unpatchify(x, patch_size):
299
+ if patch_size == 1:
300
+ return x
301
+
302
+ if x.dim() == 4:
303
+ x = rearrange(
304
+ x, "b (c r q) h w -> b c (h q) (w r)", q=patch_size, r=patch_size)
305
+ elif x.dim() == 5:
306
+ x = rearrange(
307
+ x,
308
+ "b (c r q) f h w -> b c f (h q) (w r)",
309
+ q=patch_size,
310
+ r=patch_size,
311
+ )
312
+ return x
313
+
314
+
315
+ class AvgDown3D(nn.Module):
316
+
317
+ def __init__(
318
+ self,
319
+ in_channels,
320
+ out_channels,
321
+ factor_t,
322
+ factor_s=1,
323
+ ):
324
+ super().__init__()
325
+ self.in_channels = in_channels
326
+ self.out_channels = out_channels
327
+ self.factor_t = factor_t
328
+ self.factor_s = factor_s
329
+ self.factor = self.factor_t * self.factor_s * self.factor_s
330
+
331
+ assert in_channels * self.factor % out_channels == 0
332
+ self.group_size = in_channels * self.factor // out_channels
333
+
334
+ def forward(self, x: torch.Tensor) -> torch.Tensor:
335
+ pad_t = (self.factor_t - x.shape[2] % self.factor_t) % self.factor_t
336
+ pad = (0, 0, 0, 0, pad_t, 0)
337
+ x = F.pad(x, pad)
338
+ B, C, T, H, W = x.shape
339
+ x = x.view(
340
+ B,
341
+ C,
342
+ T // self.factor_t,
343
+ self.factor_t,
344
+ H // self.factor_s,
345
+ self.factor_s,
346
+ W // self.factor_s,
347
+ self.factor_s,
348
+ )
349
+ x = x.permute(0, 1, 3, 5, 7, 2, 4, 6).contiguous()
350
+ x = x.view(
351
+ B,
352
+ C * self.factor,
353
+ T // self.factor_t,
354
+ H // self.factor_s,
355
+ W // self.factor_s,
356
+ )
357
+ x = x.view(
358
+ B,
359
+ self.out_channels,
360
+ self.group_size,
361
+ T // self.factor_t,
362
+ H // self.factor_s,
363
+ W // self.factor_s,
364
+ )
365
+ x = x.mean(dim=2)
366
+ return x
367
+
368
+
369
+ class DupUp3D(nn.Module):
370
+
371
+ def __init__(
372
+ self,
373
+ in_channels: int,
374
+ out_channels: int,
375
+ factor_t,
376
+ factor_s=1,
377
+ ):
378
+ super().__init__()
379
+ self.in_channels = in_channels
380
+ self.out_channels = out_channels
381
+
382
+ self.factor_t = factor_t
383
+ self.factor_s = factor_s
384
+ self.factor = self.factor_t * self.factor_s * self.factor_s
385
+
386
+ assert out_channels * self.factor % in_channels == 0
387
+ self.repeats = out_channels * self.factor // in_channels
388
+
389
+ def forward(self, x: torch.Tensor, first_chunk=False) -> torch.Tensor:
390
+ x = x.repeat_interleave(self.repeats, dim=1)
391
+ x = x.view(
392
+ x.size(0),
393
+ self.out_channels,
394
+ self.factor_t,
395
+ self.factor_s,
396
+ self.factor_s,
397
+ x.size(2),
398
+ x.size(3),
399
+ x.size(4),
400
+ )
401
+ x = x.permute(0, 1, 5, 2, 6, 3, 7, 4).contiguous()
402
+ x = x.view(
403
+ x.size(0),
404
+ self.out_channels,
405
+ x.size(2) * self.factor_t,
406
+ x.size(4) * self.factor_s,
407
+ x.size(6) * self.factor_s,
408
+ )
409
+ if first_chunk:
410
+ x = x[:, :, self.factor_t - 1:, :, :]
411
+ return x
412
+
413
+
414
+ class Down_ResidualBlock(nn.Module):
415
+
416
+ def __init__(self,
417
+ in_dim,
418
+ out_dim,
419
+ dropout,
420
+ mult,
421
+ temperal_downsample=False,
422
+ down_flag=False):
423
+ super().__init__()
424
+
425
+ # Shortcut path with downsample
426
+ self.avg_shortcut = AvgDown3D(
427
+ in_dim,
428
+ out_dim,
429
+ factor_t=2 if temperal_downsample else 1,
430
+ factor_s=2 if down_flag else 1,
431
+ )
432
+
433
+ # Main path with residual blocks and downsample
434
+ downsamples = []
435
+ for _ in range(mult):
436
+ downsamples.append(ResidualBlock(in_dim, out_dim, dropout))
437
+ in_dim = out_dim
438
+
439
+ # Add the final downsample block
440
+ if down_flag:
441
+ mode = "downsample3d" if temperal_downsample else "downsample2d"
442
+ downsamples.append(Resample(out_dim, mode=mode))
443
+
444
+ self.downsamples = nn.Sequential(*downsamples)
445
+
446
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
447
+ x_copy = x.clone()
448
+ for module in self.downsamples:
449
+ x = module(x, feat_cache, feat_idx)
450
+
451
+ return x + self.avg_shortcut(x_copy)
452
+
453
+
454
+ class Up_ResidualBlock(nn.Module):
455
+
456
+ def __init__(self,
457
+ in_dim,
458
+ out_dim,
459
+ dropout,
460
+ mult,
461
+ temperal_upsample=False,
462
+ up_flag=False):
463
+ super().__init__()
464
+ # Shortcut path with upsample
465
+ if up_flag:
466
+ self.avg_shortcut = DupUp3D(
467
+ in_dim,
468
+ out_dim,
469
+ factor_t=2 if temperal_upsample else 1,
470
+ factor_s=2 if up_flag else 1,
471
+ )
472
+ else:
473
+ self.avg_shortcut = None
474
+
475
+ # Main path with residual blocks and upsample
476
+ upsamples = []
477
+ for _ in range(mult):
478
+ upsamples.append(ResidualBlock(in_dim, out_dim, dropout))
479
+ in_dim = out_dim
480
+
481
+ # Add the final upsample block
482
+ if up_flag:
483
+ mode = "upsample3d" if temperal_upsample else "upsample2d"
484
+ upsamples.append(Resample(out_dim, mode=mode))
485
+
486
+ self.upsamples = nn.Sequential(*upsamples)
487
+
488
+ def forward(self, x, feat_cache=None, feat_idx=[0], first_chunk=False):
489
+ x_main = x.clone()
490
+ for module in self.upsamples:
491
+ x_main = module(x_main, feat_cache, feat_idx)
492
+ if self.avg_shortcut is not None:
493
+ x_shortcut = self.avg_shortcut(x, first_chunk)
494
+ return x_main + x_shortcut
495
+ else:
496
+ return x_main
497
+
498
+
499
+ class Encoder3d(nn.Module):
500
+
501
+ def __init__(
502
+ self,
503
+ dim=128,
504
+ z_dim=4,
505
+ dim_mult=[1, 2, 4, 4],
506
+ num_res_blocks=2,
507
+ attn_scales=[],
508
+ temperal_downsample=[True, True, False],
509
+ dropout=0.0,
510
+ ):
511
+ super().__init__()
512
+ self.dim = dim
513
+ self.z_dim = z_dim
514
+ self.dim_mult = dim_mult
515
+ self.num_res_blocks = num_res_blocks
516
+ self.attn_scales = attn_scales
517
+ self.temperal_downsample = temperal_downsample
518
+
519
+ # dimensions
520
+ dims = [dim * u for u in [1] + dim_mult]
521
+ scale = 1.0
522
+
523
+ # init block
524
+ self.conv1 = CausalConv3d(12, dims[0], 3, padding=1)
525
+
526
+ # downsample blocks
527
+ downsamples = []
528
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
529
+ t_down_flag = (
530
+ temperal_downsample[i]
531
+ if i < len(temperal_downsample) else False)
532
+ downsamples.append(
533
+ Down_ResidualBlock(
534
+ in_dim=in_dim,
535
+ out_dim=out_dim,
536
+ dropout=dropout,
537
+ mult=num_res_blocks,
538
+ temperal_downsample=t_down_flag,
539
+ down_flag=i != len(dim_mult) - 1,
540
+ ))
541
+ scale /= 2.0
542
+ self.downsamples = nn.Sequential(*downsamples)
543
+
544
+ # middle blocks
545
+ self.middle = nn.Sequential(
546
+ ResidualBlock(out_dim, out_dim, dropout),
547
+ AttentionBlock(out_dim),
548
+ ResidualBlock(out_dim, out_dim, dropout),
549
+ )
550
+
551
+ # # output blocks
552
+ self.head = nn.Sequential(
553
+ RMS_norm(out_dim, images=False),
554
+ nn.SiLU(),
555
+ CausalConv3d(out_dim, z_dim, 3, padding=1),
556
+ )
557
+
558
+ def forward(self, x, feat_cache=None, feat_idx=[0]):
559
+
560
+ if feat_cache is not None:
561
+ idx = feat_idx[0]
562
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
563
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
564
+ cache_x = torch.cat(
565
+ [
566
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
567
+ cache_x.device),
568
+ cache_x,
569
+ ],
570
+ dim=2,
571
+ )
572
+ x = self.conv1(x, feat_cache[idx])
573
+ feat_cache[idx] = cache_x
574
+ feat_idx[0] += 1
575
+ else:
576
+ x = self.conv1(x)
577
+
578
+ ## downsamples
579
+ for layer in self.downsamples:
580
+ if feat_cache is not None:
581
+ x = layer(x, feat_cache, feat_idx)
582
+ else:
583
+ x = layer(x)
584
+
585
+ ## middle
586
+ for layer in self.middle:
587
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
588
+ x = layer(x, feat_cache, feat_idx)
589
+ else:
590
+ x = layer(x)
591
+
592
+ ## head
593
+ for layer in self.head:
594
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
595
+ idx = feat_idx[0]
596
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
597
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
598
+ cache_x = torch.cat(
599
+ [
600
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
601
+ cache_x.device),
602
+ cache_x,
603
+ ],
604
+ dim=2,
605
+ )
606
+ x = layer(x, feat_cache[idx])
607
+ feat_cache[idx] = cache_x
608
+ feat_idx[0] += 1
609
+ else:
610
+ x = layer(x)
611
+
612
+ return x
613
+
614
+
615
+ class Decoder3d(nn.Module):
616
+
617
+ def __init__(
618
+ self,
619
+ dim=128,
620
+ z_dim=4,
621
+ dim_mult=[1, 2, 4, 4],
622
+ num_res_blocks=2,
623
+ attn_scales=[],
624
+ temperal_upsample=[False, True, True],
625
+ dropout=0.0,
626
+ ):
627
+ super().__init__()
628
+ self.dim = dim
629
+ self.z_dim = z_dim
630
+ self.dim_mult = dim_mult
631
+ self.num_res_blocks = num_res_blocks
632
+ self.attn_scales = attn_scales
633
+ self.temperal_upsample = temperal_upsample
634
+
635
+ # dimensions
636
+ dims = [dim * u for u in [dim_mult[-1]] + dim_mult[::-1]]
637
+ scale = 1.0 / 2**(len(dim_mult) - 2)
638
+ # init block
639
+ self.conv1 = CausalConv3d(z_dim, dims[0], 3, padding=1)
640
+
641
+ # middle blocks
642
+ self.middle = nn.Sequential(
643
+ ResidualBlock(dims[0], dims[0], dropout),
644
+ AttentionBlock(dims[0]),
645
+ ResidualBlock(dims[0], dims[0], dropout),
646
+ )
647
+
648
+ # upsample blocks
649
+ upsamples = []
650
+ for i, (in_dim, out_dim) in enumerate(zip(dims[:-1], dims[1:])):
651
+ t_up_flag = temperal_upsample[i] if i < len(
652
+ temperal_upsample) else False
653
+ upsamples.append(
654
+ Up_ResidualBlock(
655
+ in_dim=in_dim,
656
+ out_dim=out_dim,
657
+ dropout=dropout,
658
+ mult=num_res_blocks + 1,
659
+ temperal_upsample=t_up_flag,
660
+ up_flag=i != len(dim_mult) - 1,
661
+ ))
662
+ self.upsamples = nn.Sequential(*upsamples)
663
+
664
+ # output blocks
665
+ self.head = nn.Sequential(
666
+ RMS_norm(out_dim, images=False),
667
+ nn.SiLU(),
668
+ CausalConv3d(out_dim, 12, 3, padding=1),
669
+ )
670
+
671
+ def forward(self, x, feat_cache=None, feat_idx=[0], first_chunk=False):
672
+ if feat_cache is not None:
673
+ idx = feat_idx[0]
674
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
675
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
676
+ cache_x = torch.cat(
677
+ [
678
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
679
+ cache_x.device),
680
+ cache_x,
681
+ ],
682
+ dim=2,
683
+ )
684
+ x = self.conv1(x, feat_cache[idx])
685
+ feat_cache[idx] = cache_x
686
+ feat_idx[0] += 1
687
+ else:
688
+ x = self.conv1(x)
689
+
690
+ for layer in self.middle:
691
+ if isinstance(layer, ResidualBlock) and feat_cache is not None:
692
+ x = layer(x, feat_cache, feat_idx)
693
+ else:
694
+ x = layer(x)
695
+
696
+ ## upsamples
697
+ for layer in self.upsamples:
698
+ if feat_cache is not None:
699
+ x = layer(x, feat_cache, feat_idx, first_chunk)
700
+ else:
701
+ x = layer(x)
702
+
703
+ ## head
704
+ for layer in self.head:
705
+ if isinstance(layer, CausalConv3d) and feat_cache is not None:
706
+ idx = feat_idx[0]
707
+ cache_x = x[:, :, -CACHE_T:, :, :].clone()
708
+ if cache_x.shape[2] < 2 and feat_cache[idx] is not None:
709
+ cache_x = torch.cat(
710
+ [
711
+ feat_cache[idx][:, :, -1, :, :].unsqueeze(2).to(
712
+ cache_x.device),
713
+ cache_x,
714
+ ],
715
+ dim=2,
716
+ )
717
+ x = layer(x, feat_cache[idx])
718
+ feat_cache[idx] = cache_x
719
+ feat_idx[0] += 1
720
+ else:
721
+ x = layer(x)
722
+ return x
723
+
724
+
725
+ def count_conv3d(model):
726
+ count = 0
727
+ for m in model.modules():
728
+ if isinstance(m, CausalConv3d):
729
+ count += 1
730
+ return count
731
+
732
+
733
+ class WanVAE_(nn.Module):
734
+
735
+ def __init__(
736
+ self,
737
+ dim=160,
738
+ dec_dim=256,
739
+ z_dim=16,
740
+ dim_mult=[1, 2, 4, 4],
741
+ num_res_blocks=2,
742
+ attn_scales=[],
743
+ temperal_downsample=[True, True, False],
744
+ dropout=0.0,
745
+ ):
746
+ super().__init__()
747
+ self.dim = dim
748
+ self.z_dim = z_dim
749
+ self.dim_mult = dim_mult
750
+ self.num_res_blocks = num_res_blocks
751
+ self.attn_scales = attn_scales
752
+ self.temperal_downsample = temperal_downsample
753
+ self.temperal_upsample = temperal_downsample[::-1]
754
+
755
+ # modules
756
+ self.encoder = Encoder3d(
757
+ dim,
758
+ z_dim * 2,
759
+ dim_mult,
760
+ num_res_blocks,
761
+ attn_scales,
762
+ self.temperal_downsample,
763
+ dropout,
764
+ )
765
+ self.conv1 = CausalConv3d(z_dim * 2, z_dim * 2, 1)
766
+ self.conv2 = CausalConv3d(z_dim, z_dim, 1)
767
+ self.decoder = Decoder3d(
768
+ dec_dim,
769
+ z_dim,
770
+ dim_mult,
771
+ num_res_blocks,
772
+ attn_scales,
773
+ self.temperal_upsample,
774
+ dropout,
775
+ )
776
+
777
+ def forward(self, x, scale=[0, 1]):
778
+ mu = self.encode(x, scale)
779
+ x_recon = self.decode(mu, scale)
780
+ return x_recon, mu
781
+
782
+ def encode(self, x, scale):
783
+ self.clear_cache()
784
+ x = patchify(x, patch_size=2)
785
+ t = x.shape[2]
786
+ iter_ = 1 + (t - 1) // 4
787
+ for i in range(iter_):
788
+ self._enc_conv_idx = [0]
789
+ if i == 0:
790
+ out = self.encoder(
791
+ x[:, :, :1, :, :],
792
+ feat_cache=self._enc_feat_map,
793
+ feat_idx=self._enc_conv_idx,
794
+ )
795
+ else:
796
+ out_ = self.encoder(
797
+ x[:, :, 1 + 4 * (i - 1):1 + 4 * i, :, :],
798
+ feat_cache=self._enc_feat_map,
799
+ feat_idx=self._enc_conv_idx,
800
+ )
801
+ out = torch.cat([out, out_], 2)
802
+ mu, log_var = self.conv1(out).chunk(2, dim=1)
803
+ if isinstance(scale[0], torch.Tensor):
804
+ mu = (mu - scale[0].view(1, self.z_dim, 1, 1, 1)) * scale[1].view(
805
+ 1, self.z_dim, 1, 1, 1)
806
+ else:
807
+ mu = (mu - scale[0]) * scale[1]
808
+ self.clear_cache()
809
+ return mu
810
+
811
+ def decode(self, z, scale):
812
+ """Batch decode. Numerically identical to streaming begin/step/end."""
813
+ self.decode_stream_begin(scale)
814
+ out = self.decode_stream_step(z)
815
+ self.decode_stream_end()
816
+ return out
817
+
818
+ def decode_stream_begin(self, scale):
819
+ """Start a causal streaming decode session (keeps feat_cache)."""
820
+ self.clear_cache()
821
+ self._stream_scale = scale
822
+ self._stream_frame_idx = 0
823
+
824
+ def decode_stream_step(self, z):
825
+ """Decode one or more latent frames with the live feat_cache.
826
+
827
+ Args:
828
+ z: ``(B, z_dim, T, H, W)`` latent chunk (T >= 1), same scale space
829
+ as ``encode`` / batch ``decode`` inputs.
830
+
831
+ Returns:
832
+ RGB video ``(B, 3, T_rgb, H', W')`` in ``[-1, 1]`` for this chunk.
833
+ """
834
+ assert hasattr(self, "_stream_scale"), (
835
+ "decode_stream_begin() must be called before decode_stream_step()"
836
+ )
837
+ scale = self._stream_scale
838
+ if isinstance(scale[0], torch.Tensor):
839
+ z = z / scale[1].view(1, self.z_dim, 1, 1, 1) + scale[0].view(
840
+ 1, self.z_dim, 1, 1, 1)
841
+ else:
842
+ z = z / scale[1] + scale[0]
843
+
844
+ outs = []
845
+ for i in range(z.shape[2]):
846
+ # CausalConv3d(k=1): per-frame conv2 matches full-sequence conv2.
847
+ x_i = self.conv2(z[:, :, i:i + 1, :, :])
848
+ self._conv_idx = [0]
849
+ out_i = self.decoder(
850
+ x_i,
851
+ feat_cache=self._feat_map,
852
+ feat_idx=self._conv_idx,
853
+ first_chunk=(self._stream_frame_idx == 0),
854
+ )
855
+ outs.append(out_i)
856
+ self._stream_frame_idx += 1
857
+ out = torch.cat(outs, dim=2)
858
+ return unpatchify(out, patch_size=2)
859
+
860
+ def decode_stream_end(self):
861
+ """Finish a streaming decode session and drop feat_cache."""
862
+ self.clear_cache()
863
+ if hasattr(self, "_stream_scale"):
864
+ del self._stream_scale
865
+ if hasattr(self, "_stream_frame_idx"):
866
+ del self._stream_frame_idx
867
+
868
+ def reparameterize(self, mu, log_var):
869
+ std = torch.exp(0.5 * log_var)
870
+ eps = torch.randn_like(std)
871
+ return eps * std + mu
872
+
873
+ def sample(self, imgs, deterministic=False):
874
+ mu, log_var = self.encode(imgs)
875
+ if deterministic:
876
+ return mu
877
+ std = torch.exp(0.5 * log_var.clamp(-30.0, 20.0))
878
+ return mu + std * torch.randn_like(std)
879
+
880
+ def clear_cache(self):
881
+ self._conv_num = count_conv3d(self.decoder)
882
+ self._conv_idx = [0]
883
+ self._feat_map = [None] * self._conv_num
884
+ # cache encode
885
+ self._enc_conv_num = count_conv3d(self.encoder)
886
+ self._enc_conv_idx = [0]
887
+ self._enc_feat_map = [None] * self._enc_conv_num
888
+
889
+
890
+ def _video_vae(pretrained_path=None, z_dim=16, dim=160, device="cpu", **kwargs):
891
+ # params
892
+ cfg = dict(
893
+ dim=dim,
894
+ z_dim=z_dim,
895
+ dim_mult=[1, 2, 4, 4],
896
+ num_res_blocks=2,
897
+ attn_scales=[],
898
+ temperal_downsample=[True, True, True],
899
+ dropout=0.0,
900
+ )
901
+ cfg.update(**kwargs)
902
+
903
+ # init model
904
+ with torch.device("meta"):
905
+ model = WanVAE_(**cfg)
906
+
907
+ # load checkpoint
908
+ logging.info(f"loading {pretrained_path}")
909
+ model.load_state_dict(
910
+ torch.load(pretrained_path, map_location=device), assign=True)
911
+
912
+ return model
913
+
914
+
915
+ class Wan2_2_VAE:
916
+
917
+ def __init__(
918
+ self,
919
+ z_dim=48,
920
+ c_dim=160,
921
+ vae_pth=None,
922
+ dim_mult=[1, 2, 4, 4],
923
+ temperal_downsample=[False, True, True],
924
+ dtype=torch.float,
925
+ device="cuda",
926
+ ):
927
+
928
+ self.dtype = dtype
929
+ self.device = device
930
+
931
+ mean = torch.tensor(
932
+ [
933
+ -0.2289,
934
+ -0.0052,
935
+ -0.1323,
936
+ -0.2339,
937
+ -0.2799,
938
+ 0.0174,
939
+ 0.1838,
940
+ 0.1557,
941
+ -0.1382,
942
+ 0.0542,
943
+ 0.2813,
944
+ 0.0891,
945
+ 0.1570,
946
+ -0.0098,
947
+ 0.0375,
948
+ -0.1825,
949
+ -0.2246,
950
+ -0.1207,
951
+ -0.0698,
952
+ 0.5109,
953
+ 0.2665,
954
+ -0.2108,
955
+ -0.2158,
956
+ 0.2502,
957
+ -0.2055,
958
+ -0.0322,
959
+ 0.1109,
960
+ 0.1567,
961
+ -0.0729,
962
+ 0.0899,
963
+ -0.2799,
964
+ -0.1230,
965
+ -0.0313,
966
+ -0.1649,
967
+ 0.0117,
968
+ 0.0723,
969
+ -0.2839,
970
+ -0.2083,
971
+ -0.0520,
972
+ 0.3748,
973
+ 0.0152,
974
+ 0.1957,
975
+ 0.1433,
976
+ -0.2944,
977
+ 0.3573,
978
+ -0.0548,
979
+ -0.1681,
980
+ -0.0667,
981
+ ],
982
+ dtype=dtype,
983
+ device=device,
984
+ )
985
+ std = torch.tensor(
986
+ [
987
+ 0.4765,
988
+ 1.0364,
989
+ 0.4514,
990
+ 1.1677,
991
+ 0.5313,
992
+ 0.4990,
993
+ 0.4818,
994
+ 0.5013,
995
+ 0.8158,
996
+ 1.0344,
997
+ 0.5894,
998
+ 1.0901,
999
+ 0.6885,
1000
+ 0.6165,
1001
+ 0.8454,
1002
+ 0.4978,
1003
+ 0.5759,
1004
+ 0.3523,
1005
+ 0.7135,
1006
+ 0.6804,
1007
+ 0.5833,
1008
+ 1.4146,
1009
+ 0.8986,
1010
+ 0.5659,
1011
+ 0.7069,
1012
+ 0.5338,
1013
+ 0.4889,
1014
+ 0.4917,
1015
+ 0.4069,
1016
+ 0.4999,
1017
+ 0.6866,
1018
+ 0.4093,
1019
+ 0.5709,
1020
+ 0.6065,
1021
+ 0.6415,
1022
+ 0.4944,
1023
+ 0.5726,
1024
+ 1.2042,
1025
+ 0.5458,
1026
+ 1.6887,
1027
+ 0.3971,
1028
+ 1.0600,
1029
+ 0.3943,
1030
+ 0.5537,
1031
+ 0.5444,
1032
+ 0.4089,
1033
+ 0.7468,
1034
+ 0.7744,
1035
+ ],
1036
+ dtype=dtype,
1037
+ device=device,
1038
+ )
1039
+ self.scale = [mean, 1.0 / std]
1040
+
1041
+ # init model
1042
+ self.model = (
1043
+ _video_vae(
1044
+ pretrained_path=vae_pth,
1045
+ z_dim=z_dim,
1046
+ dim=c_dim,
1047
+ dim_mult=dim_mult,
1048
+ temperal_downsample=temperal_downsample,
1049
+ ).eval().requires_grad_(False).to(device))
1050
+
1051
+ def encode(self, videos):
1052
+ try:
1053
+ with torch.amp.autocast(device_type='cuda', dtype=self.dtype):
1054
+ if not isinstance(videos, list):
1055
+ return self.model.encode(videos, self.scale).float()
1056
+ else:
1057
+ return [
1058
+ self.model.encode(u.unsqueeze(0),
1059
+ self.scale).float().squeeze(0)
1060
+ for u in videos
1061
+ ]
1062
+ except TypeError as e:
1063
+ logging.info(e)
1064
+ return None
1065
+
1066
+ def decode(self, zs):
1067
+ try:
1068
+ with torch.amp.autocast(device_type='cuda', dtype=self.dtype):
1069
+ if not isinstance(zs, list):
1070
+ return self.model.decode(zs, self.scale).float().clamp_(-1, 1)
1071
+ else:
1072
+ return [
1073
+ self.model.decode(u.unsqueeze(0),
1074
+ self.scale).float().clamp_(-1,
1075
+ 1).squeeze(0)
1076
+ for u in zs
1077
+ ]
1078
+ except TypeError as e:
1079
+ logging.info(e)
1080
+ return None
1081
+
1082
+ def decode_stream_begin(self):
1083
+ """Begin causal streaming decode (feat_cache retained across steps)."""
1084
+ self.model.decode_stream_begin(self.scale)
1085
+
1086
+ def decode_stream_step(self, z):
1087
+ """Decode a latent chunk ``(B,C,T,H,W)``; returns RGB in ``[-1, 1]``."""
1088
+ with torch.amp.autocast(device_type='cuda', dtype=self.dtype):
1089
+ return self.model.decode_stream_step(z).float().clamp_(-1, 1)
1090
+
1091
+ def decode_stream_end(self):
1092
+ """End streaming decode and clear feat_cache."""
1093
+ self.model.decode_stream_end()
requirements.txt ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ numpy
2
+ einops
3
+ Pillow
4
+ imageio
5
+ imageio-ffmpeg
6
+ https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt211-cu130-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl