Spaces:
Running on Zero
Running on Zero
MiniWorld camera-controlled world model demo
Browse files- .gitattributes +2 -0
- README.md +49 -6
- app.py +498 -0
- examples/deck.png +3 -0
- examples/garden.png +3 -0
- examples/kitchen.png +0 -0
- miniworld/__init__.py +6 -0
- miniworld/conditioning/__init__.py +6 -0
- miniworld/conditioning/actions.py +58 -0
- miniworld/conditioning/poses.py +218 -0
- miniworld/conditioning/trajectories.py +299 -0
- miniworld/denoiser.py +1073 -0
- miniworld/miniworld.py +1044 -0
- miniworld/vae/__init__.py +6 -0
- miniworld/vae/codec.py +98 -0
- miniworld/vae/wan22_vae.py +1093 -0
- requirements.txt +6 -0
.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:
|
| 3 |
-
emoji:
|
| 4 |
-
colorFrom:
|
| 5 |
-
colorTo:
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
examples/garden.png
ADDED
|
Git LFS Details
|
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
|