Sudhanshu Mittal Claude Opus 4.8 commited on
Commit
a6cc5f0
·
1 Parent(s): 724c54c

Add Orbis 2 hierarchical world model demo (app2.py)

Browse files

Vendors the Orbis 2 inference code into orbis2/ and adds app2.py, a Gradio
demo for the hierarchical L1-L2 model. Orbis 1 (app.py, orbis/) is untouched;
the Space still boots app.py until app_file is switched.

Unlike Orbis 1, the Orbis 2 rollout takes an mp4 directly and samples its own
L1 (high-rate) and L2 (low-rate, further back) context windows, so app2.py
re-encodes the upload to a constant 10 fps instead of extracting frames and
writing a val config.

- orbis2/evaluate/rollout_demo_v2.py: add --num_videos to roll out N futures
from one context as a single minibatch (tiling context/steering before
building condition_kwargs), and save each to fake_images/sequence_XXXX/,
matching the layout app2.py reads.
- orbis2/models/second_stage/fm_model_v2.py: label the rollout tqdm bars so
the app can scrape progress from the child process.
- app2.py: downloads the three models (L1 detail predictor, L2 abstract
predictor, tokenizer) from sud0301/orbis2_test into the HF cache and points
$ORBIS2_MODELS_DIR at the snapshot, which the L1/L2 configs expand to locate
each other. Uses the cache path directly rather than local_dir= to avoid
duplicating ~20 GB on disk.
- .gitignore: exclude the root models/ staging dir (weights + configs live on
the Hub). Anchored so it does not match orbis2/models/ source.
- requirements.txt: decord, h5py, requests for the video-in rollout path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

.gitignore CHANGED
@@ -1,3 +1,11 @@
1
  __pycache__/
2
  *.pyc
3
  orbis/imgs/Rollout.png
 
 
 
 
 
 
 
 
 
1
  __pycache__/
2
  *.pyc
3
  orbis/imgs/Rollout.png
4
+
5
+ # Checkpoints are never committed (orbis1's logs_*/checkpoints, and anything else).
6
+ *.ckpt
7
+
8
+ # Orbis 2 models (L1/L2/tok — ~20 GB of weights plus their configs) live on the Hub
9
+ # at sud0301/orbis2_test; app2.py downloads them at startup. Leading slash anchors
10
+ # this to the repo root so it does NOT match orbis2/models/, which is source code.
11
+ /models/
app2.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Orbis 2 World Model — interactive rollout demo for Hugging Face Spaces.
3
+
4
+ This is the Orbis 2 counterpart to app.py (which drives the single-level Orbis 1
5
+ model). Orbis 2 is a hierarchical L1-L2 world model whose rollout script takes an
6
+ mp4 directly, so the flow differs from app.py:
7
+
8
+ 1. User uploads a short driving clip.
9
+ 2. We re-encode it to a constant CONTEXT_FPS (= the L1 frame rate) so the
10
+ rollout script's exact integer-multiple frame-rate check passes.
11
+ 3. We call evaluate/rollout_demo_v2.py, which samples the L1 (high-rate) and L2
12
+ (low-rate, further back) context windows from the tail of that video.
13
+ 4. The context is tiled NUM_VIDEOS times so one minibatch rolls out that many
14
+ independently sampled futures under a single seed.
15
+ 5. Generated frames (fake_images/sequence_XXXX/*.jpg) are encoded back into mp4s.
16
+
17
+ Checkpoint / config resolution (see resolve_exp_dir / download_checkpoints):
18
+ * If ORBIS2_EXP_DIR is set (and contains the config + checkpoint), it is used
19
+ as-is — this is the path that works today against a local training run.
20
+ * Otherwise, if ORBIS2_HF_REPO is set, the config + checkpoint are pulled from
21
+ that Hub repo into a local exp dir. Orbis 2 is not on the Hub yet; set these
22
+ env vars once the checkpoint is uploaded.
23
+ """
24
+
25
+ import os
26
+ import random
27
+ import re
28
+ import subprocess
29
+ import tempfile
30
+ import traceback
31
+ import uuid
32
+ from pathlib import Path
33
+
34
+ import cv2
35
+ import gradio as gr
36
+ import spaces
37
+ from huggingface_hub import snapshot_download
38
+
39
+ # ----------------------------------------------------------------------------
40
+ # Workaround for gradio 5.9.1 / gradio_client bug (gradio-app/gradio#11722):
41
+ # get_api_info() walks the app's JSON schema and crashes with
42
+ # "TypeError: argument of type 'bool' is not iterable"
43
+ # when a schema node is a boolean (e.g. "additionalProperties": true), because
44
+ # get_type()/_json_schema_to_python_type() assume every schema is a dict. The
45
+ # main page route calls api_info() on every load, so without this the endpoint
46
+ # 500s continuously. Make the walker tolerate boolean schemas.
47
+ # ----------------------------------------------------------------------------
48
+ import gradio_client.utils as _gc_utils
49
+
50
+ _orig_get_type = _gc_utils.get_type
51
+ _orig_json_to_py = _gc_utils._json_schema_to_python_type
52
+
53
+
54
+ def _safe_get_type(schema):
55
+ if isinstance(schema, bool):
56
+ return "bool"
57
+ return _orig_get_type(schema)
58
+
59
+
60
+ def _safe_json_to_py(schema, defs=None):
61
+ if isinstance(schema, bool):
62
+ return "Any"
63
+ return _orig_json_to_py(schema, defs)
64
+
65
+
66
+ _gc_utils.get_type = _safe_get_type
67
+ _gc_utils._json_schema_to_python_type = _safe_json_to_py
68
+
69
+ # ----------------------------------------------------------------------------
70
+ # Configuration — adjust these to your setup
71
+ # ----------------------------------------------------------------------------
72
+ REPO_DIR = Path(__file__).parent / "orbis2" # copied silviogalesso/orbis2_release
73
+
74
+ # HF cache location. Prefer the Space's persistent /data volume so large
75
+ # checkpoints download once and survive restarts; fall back to the default
76
+ # ephemeral cache if /data isn't writable (e.g. persistent storage not enabled).
77
+ def _pick_cache_dir() -> str:
78
+ for candidate in ("/data/.cache/huggingface",
79
+ os.path.expanduser("~/.cache/huggingface")):
80
+ try:
81
+ probe = Path(candidate)
82
+ probe.mkdir(parents=True, exist_ok=True)
83
+ test = probe / ".write_probe"
84
+ test.touch()
85
+ test.unlink()
86
+ return candidate
87
+ except OSError:
88
+ continue
89
+ return os.path.expanduser("~/.cache/huggingface")
90
+
91
+
92
+ HF_CACHE_DIR = os.environ.get("HF_HOME") or _pick_cache_dir()
93
+ os.environ["HF_HOME"] = HF_CACHE_DIR
94
+ HF_HUB_CACHE_DIR = str(Path(HF_CACHE_DIR) / "hub")
95
+ print(f"[startup] HF cache dir: {HF_CACHE_DIR}"
96
+ f"{'' if HF_CACHE_DIR.startswith('/data') else ' (ephemeral — /data not writable)'}")
97
+
98
+ # --- Orbis 2 models (configs + checkpoints) ----------------------------------
99
+ # Three models are needed, laid out as <models>/{L1,L2,tok}/{config,checkpoints/last.ckpt}:
100
+ # L1 — detail predictor (the world model rollout_demo_v2.py loads directly)
101
+ # L2 — abstract predictor (frozen; referenced by L1's condition_preprocessor)
102
+ # tok — tokenizer (referenced by BOTH L1's and L2's tokenizer_config)
103
+ #
104
+ # L1's and L2's configs locate their dependencies via $ORBIS2_MODELS_DIR, which we
105
+ # export below so OmegaConf's expandvars resolves them wherever the tree lands.
106
+ CONFIG_NAME = "config_distill.yaml" # L1's config, relative to EXP_DIR
107
+ CKPT_NAME = "checkpoints/last.ckpt" # every model stores its weights here
108
+
109
+ #: (sub-dir, config filename) for each of the three required models.
110
+ MODEL_SPECS = [
111
+ ("L1", CONFIG_NAME),
112
+ ("L2", "config.yaml"),
113
+ ("tok", "config.yaml"),
114
+ ]
115
+
116
+ # Hub repo holding the three models (root contains L1/, L2/, tok/).
117
+ HF_CKPT_REPO = os.environ.get("ORBIS2_HF_REPO", "sud0301/orbis2_test")
118
+
119
+
120
+ def _missing_models(root: Path) -> list[str]:
121
+ """Return the models whose config or checkpoint is absent under `root`."""
122
+ return [sub for sub, cfg in MODEL_SPECS
123
+ if not (root / sub / cfg).exists() or not (root / sub / CKPT_NAME).exists()]
124
+
125
+
126
+ def resolve_models_dir() -> Path:
127
+ """Return a directory containing L1/, L2/ and tok/, downloading it if needed.
128
+
129
+ A local tree wins if it's complete (a dev checkout, or ORBIS2_MODELS_DIR pointing
130
+ at one). Otherwise we snapshot the Hub repo and use the *cache* path directly
131
+ rather than copying into the Space — `local_dir=` would duplicate ~20 GB on disk.
132
+ The cache lives on /data when persistent storage is enabled, so the download
133
+ survives restarts.
134
+ """
135
+ local = Path(os.environ.get("ORBIS2_MODELS_DIR") or (Path(__file__).parent / "models"))
136
+ if not _missing_models(local):
137
+ print(f"[startup] Using local Orbis 2 models: {local}")
138
+ return local
139
+
140
+ print(f"[startup] Fetching Orbis 2 models from {HF_CKPT_REPO} (~20 GB, first run only)…")
141
+ snapshot = Path(snapshot_download(
142
+ repo_id=HF_CKPT_REPO,
143
+ allow_patterns=[f"{sub}/**" for sub, _ in MODEL_SPECS],
144
+ cache_dir=HF_HUB_CACHE_DIR,
145
+ ))
146
+
147
+ still_missing = _missing_models(snapshot)
148
+ if still_missing:
149
+ raise RuntimeError(
150
+ f"Downloaded {HF_CKPT_REPO} but these models are incomplete: "
151
+ f"{', '.join(still_missing)}. Each of L1/, L2/, tok/ must contain its "
152
+ f"config and '{CKPT_NAME}'."
153
+ )
154
+ print(f"[startup] Orbis 2 models ready: {snapshot}")
155
+ return snapshot
156
+
157
+
158
+ MODELS_DIR = resolve_models_dir()
159
+ os.environ["ORBIS2_MODELS_DIR"] = str(MODELS_DIR) # L1/L2 configs expandvars this
160
+ EXP_DIR = MODELS_DIR / "L1" # rollout_demo_v2.py --exp_dir
161
+
162
+ FRAME_H, FRAME_W = 288, 512
163
+ L1_FRAME_RATE = 10 # Hz to sample L1 context/rollout frames (and output fps)
164
+ CONTEXT_FPS = L1_FRAME_RATE # we re-encode uploads to this exact fps for the rollout script
165
+ DEFAULT_GEN_STEPS = 10 # rollout steps; each yields model.num_pred_frames frames
166
+ DEFAULT_STEPS = 30 # flow-matching solver steps (matches the reference command)
167
+ OUTPUT_FPS = L1_FRAME_RATE
168
+ NUM_VIDEOS = 3 # futures rolled out per run, batched into one minibatch
169
+ DEFAULT_ETA = 0.0 # 0 = deterministic ODE; >0 injects noise each solver step
170
+
171
+ DESCRIPTION = f"""
172
+ # Orbis 2: A Hierarchical World Model for Driving
173
+
174
+ Sudhanshu Mittal\\*, Arian Mousakhan\\*, Silvio Galesso\\*, Karim Farid,
175
+ Johannes Dienert, Rajat Sahay, Thomas Brox
176
+ — University of Freiburg, Germany (\\*main contributors)
177
+
178
+ [Project page](https://lmb-freiburg.github.io/orbis2.github.io/) ·
179
+ [Code](https://github.com/lmb-freiburg/orbis) ·
180
+ [Orbis 1](https://lmb-freiburg.github.io/orbis.github.io/)
181
+
182
+ Upload a short driving clip (a few seconds is enough). The hierarchical model takes
183
+ the tail of the clip as L1 (high-rate) and L2 (low-rate, further back) context and
184
+ autoregressively predicts future frames.
185
+
186
+ Each run samples **{NUM_VIDEOS} rollouts** from the same context in a single
187
+ minibatch, so you see several plausible futures diverge from identical starting
188
+ conditions.
189
+ """
190
+
191
+ PAPER_MD = """
192
+ ### Abstract
193
+
194
+ > Current world models typically operate at a single abstraction level, favoring
195
+ > perceptual fidelity but lacking the spatial and semantic reasoning needed for
196
+ > downstream driving tasks. We propose a hierarchical driving world model that
197
+ > separates prediction into a high-level long-horizon scene forecaster and a
198
+ > low-level detail generator conditioned on it. This design improves both visual
199
+ > fidelity and spatial-semantic representation quality. We also introduce a
200
+ > two-stage training strategy: diffusion-forcing pretraining for richer
201
+ > representations, followed by teacher-forcing fine-tuning for stable
202
+ > autoregressive rollouts. Our method achieves state-of-the-art results on
203
+ > standard driving world model benchmarks, including long-horizon fidelity,
204
+ > counterfactual steering responsiveness, and internal representation quality.
205
+
206
+ ### Method
207
+
208
+ Orbis 2 splits future prediction across two levels of abstraction:
209
+
210
+ - **Abstract predictor (L2)** — operates over a long temporal context to forecast a
211
+ future state in latent space, capturing abstract scene dynamics over long
212
+ horizons and enabling steering control.
213
+ - **Detail predictor (L1)** — conditioned on that abstract prediction, generates
214
+ fine-grained short-horizon frames, so high-fidelity local prediction stays
215
+ grounded in long-range temporal context.
216
+
217
+ Training runs in two stages: **diffusion-forcing pretraining** for richer
218
+ representations, followed by **teacher-forcing fine-tuning** for stable
219
+ autoregressive rollouts.
220
+
221
+ ### Links & citation
222
+
223
+ ```bibtex
224
+ @article{orbis2_2026,
225
+ author = {Mittal, Sudhanshu and Mousakhan, Arian and Galesso, Silvio and
226
+ Farid, Karim and Dienert, Johannes and Sahay, Rajat and Brox, Thomas},
227
+ title = {Orbis 2: A Hierarchical World Model for Driving},
228
+ journal = {arXiv preprint arXiv:XXXX.XXXXX},
229
+ year = {2026},
230
+ }
231
+ ```
232
+ """
233
+
234
+ DEMO_MD = f"""
235
+ **Pipeline.** Your clip is re-encoded to a constant {CONTEXT_FPS} fps. The rollout
236
+ script samples two context windows from the tail of that video: an **L1** window at
237
+ {L1_FRAME_RATE} Hz (fine, recent frames) and an **L2** window sampled further back at
238
+ the frozen L2 predictor's own trained rate (coarse, long-horizon context). Each frame
239
+ is resized to {FRAME_H}×{FRAME_W}. The **L2 abstract predictor** forecasts a latent
240
+ future, and the **L1 detail predictor**, conditioned on it, generates the next frames
241
+ autoregressively.
242
+
243
+ **Rollout length.** *Rollout steps* sets how many autoregressive steps to run; each
244
+ step emits several image frames, so the final clip is longer than the step count.
245
+
246
+ **Sampling.** Each step integrates the flow-matching ODE with *Diffusion steps*
247
+ solver steps. *Eta* (under Advanced) injects noise per step: 0 is the deterministic
248
+ ODE, higher values turn it into an SDE.
249
+
250
+ **Randomness.** The {NUM_VIDEOS} rollouts share one context and one seed, but the
251
+ solver draws independent initial noise per batch element, so the futures diverge.
252
+ Leave the seed at -1 under Advanced for a fresh draw each run, or set it explicitly
253
+ to reproduce a run exactly.
254
+
255
+ **Note.** The clip must be long enough for the L2 look-back window; very short clips
256
+ are rejected with an explicit error. Frame and step counts are kept modest to fit the
257
+ ZeroGPU time budget.
258
+ """
259
+
260
+
261
+ # ----------------------------------------------------------------------------
262
+ # Video helpers (CPU)
263
+ # ----------------------------------------------------------------------------
264
+ def reencode_to_fps(src_path: str, out_path: Path, fps: int = CONTEXT_FPS):
265
+ """Re-encode a clip to a constant `fps` H.264 mp4.
266
+
267
+ The rollout script requires the video's native frame rate to be an exact integer
268
+ multiple of the L1/L2 sampling rates. Forcing a constant fps here (via -vsync cfr)
269
+ makes an arbitrary upload satisfy that check.
270
+ """
271
+ out_path.parent.mkdir(parents=True, exist_ok=True)
272
+ subprocess.run(
273
+ ["ffmpeg", "-y", "-i", str(src_path),
274
+ "-vsync", "cfr", "-r", str(fps),
275
+ "-an", "-c:v", "libx264", "-pix_fmt", "yuv420p",
276
+ "-movflags", "+faststart", str(out_path)],
277
+ check=True, capture_output=True,
278
+ )
279
+
280
+
281
+ def frames_to_mp4(frame_paths: list[Path], out_path: Path, fps: int = OUTPUT_FPS):
282
+ first = cv2.imread(str(frame_paths[0]))
283
+ h, w = first.shape[:2]
284
+ # mp4v then re-encode with ffmpeg for browser-compatible H.264
285
+ tmp = out_path.with_suffix(".raw.mp4")
286
+ writer = cv2.VideoWriter(str(tmp), cv2.VideoWriter_fourcc(*"mp4v"), fps, (w, h))
287
+ for p in frame_paths:
288
+ writer.write(cv2.imread(str(p)))
289
+ writer.release()
290
+ subprocess.run(
291
+ ["ffmpeg", "-y", "-i", str(tmp), "-c:v", "libx264",
292
+ "-pix_fmt", "yuv420p", "-movflags", "+faststart", str(out_path)],
293
+ check=True, capture_output=True,
294
+ )
295
+ tmp.unlink(missing_ok=True)
296
+
297
+
298
+ # ----------------------------------------------------------------------------
299
+ # GPU inference
300
+ # ----------------------------------------------------------------------------
301
+ #: tqdm renders the rollout bar as "Rolling out frames: 30%|### | 3/10 [00:05<...]"
302
+ _ROLLOUT_RE = re.compile(r"Rolling out frames.*?(\d+)/(\d+)")
303
+
304
+
305
+ def _stream_rollout(cmd: list[str], say) -> str:
306
+ """Run rollout_demo_v2.py, forwarding its output to the log and driving progress.
307
+
308
+ The heavy lifting happens in a child process, so gr.Progress(track_tqdm=True)
309
+ cannot see its tqdm bars. We scrape the child's own bar instead. tqdm redraws in
310
+ place with a carriage return rather than a newline, so read whatever bytes are
311
+ available and split on both.
312
+ """
313
+ proc = subprocess.Popen(
314
+ cmd, cwd=REPO_DIR, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
315
+ )
316
+ tail: list[str] = []
317
+ buf = ""
318
+ while True:
319
+ chunk = proc.stdout.read1(4096) # returns as soon as bytes are ready
320
+ if not chunk:
321
+ break
322
+ buf += chunk.decode("utf-8", errors="replace")
323
+ *lines, buf = re.split(r"[\r\n]", buf)
324
+ for line in lines:
325
+ line = line.rstrip()
326
+ if not line:
327
+ continue
328
+ print(line) # full trace stays in the Space logs
329
+ tail.append(line)
330
+ del tail[:-200]
331
+ m = _ROLLOUT_RE.search(line)
332
+ if m:
333
+ done, total = int(m.group(1)), max(int(m.group(2)), 1)
334
+ say(0.15 + 0.70 * min(done / total, 1.0),
335
+ f"Rolling out step {done}/{total}…")
336
+
337
+ proc.wait()
338
+ joined = "\n".join(tail)
339
+ if proc.returncode != 0:
340
+ raise gr.Error(f"Rollout failed:\n{joined[-800:]}")
341
+ return joined
342
+
343
+
344
+ @spaces.GPU(duration=300)
345
+ def run_rollout(video_path: str, num_gen_steps: int, num_steps: int,
346
+ seed: int, eta: float, progress=gr.Progress()):
347
+ if video_path is None:
348
+ raise gr.Error("Please upload a short mp4 clip first.")
349
+
350
+ def say(frac, msg):
351
+ progress(frac, desc=msg)
352
+
353
+ try:
354
+ # The rollout script only seeds when seed > 0; within the batch each sample
355
+ # still draws its own noise, so one seed yields NUM_VIDEOS distinct rollouts.
356
+ # seed < 0 means "surprise me" — draw a fresh positive seed each run.
357
+ seed = random.randrange(1, 2**31 - 1) if int(seed) < 0 else max(1, int(seed))
358
+
359
+ job = Path(tempfile.gettempdir()) / f"orbis2_{uuid.uuid4().hex[:8]}"
360
+
361
+ say(0.02, "Preparing context video…")
362
+ ctx_video = job / "context.mp4"
363
+ reencode_to_fps(video_path, ctx_video, CONTEXT_FPS)
364
+ print(f"context @ {CONTEXT_FPS} fps | seed {seed} | "
365
+ f"{int(num_gen_steps)} rollout steps | {int(num_steps)} solver steps | eta {eta}")
366
+
367
+ output_dir = job / "rollout"
368
+
369
+ say(0.10, "Loading model and rolling out…")
370
+ cmd = [
371
+ "python", "evaluate/rollout_demo_v2.py",
372
+ "--exp_dir", str(EXP_DIR),
373
+ "--config", CONFIG_NAME,
374
+ "--ckpt", CKPT_NAME,
375
+ "--video", str(ctx_video),
376
+ "--l1_frame_rate", str(L1_FRAME_RATE),
377
+ # Given explicitly so resolve_context_image_size never falls back to the
378
+ # config's `data:` block (which points at training-only dataset paths).
379
+ "--height", str(FRAME_H),
380
+ "--width", str(FRAME_W),
381
+ "--num_gen_frames", str(int(num_gen_steps)),
382
+ "--num_steps", str(int(num_steps)),
383
+ "--eta", str(float(eta)),
384
+ "--seed", str(seed),
385
+ "--num_videos", str(NUM_VIDEOS),
386
+ "--output_dir", str(output_dir),
387
+ "--device", "cuda",
388
+ ]
389
+ _stream_rollout(cmd, say)
390
+
391
+ say(0.88, "Encoding videos…")
392
+ out_mp4s = []
393
+ for i in range(NUM_VIDEOS):
394
+ seq_dir = output_dir / "fake_images" / f"sequence_{i:04d}"
395
+ gen_frames = sorted(seq_dir.glob("*.jpg"))
396
+ if not gen_frames:
397
+ raise gr.Error(f"No generated frames found in {seq_dir} — "
398
+ "check rollout output path.")
399
+ out_mp4 = job / f"rollout_{i}.mp4"
400
+ frames_to_mp4(gen_frames, out_mp4)
401
+ out_mp4s.append(str(out_mp4))
402
+
403
+ say(1.0, f"Done — {NUM_VIDEOS} rollouts of {int(num_gen_steps)} steps.")
404
+ return tuple(out_mp4s)
405
+
406
+ except gr.Error:
407
+ raise
408
+ except Exception as e:
409
+ tb = traceback.format_exc()
410
+ print(tb)
411
+ raise gr.Error(f"Rollout failed: {e}\n\n{tb[-1500:]}")
412
+
413
+
414
+ # ----------------------------------------------------------------------------
415
+ # UI
416
+ # ----------------------------------------------------------------------------
417
+ def build_demo():
418
+ with gr.Blocks(title="Orbis 2: A Hierarchical World Model for Driving") as demo:
419
+ gr.Markdown(DESCRIPTION)
420
+
421
+ with gr.Accordion("About the paper", open=False):
422
+ gr.Markdown(PAPER_MD)
423
+ with gr.Accordion("About this demo", open=False):
424
+ gr.Markdown(DEMO_MD)
425
+
426
+ with gr.Row():
427
+ with gr.Column(scale=1):
428
+ inp = gr.Video(label="Upload a short mp4 clip", sources=["upload"])
429
+ n_steps_gen = gr.Slider(4, 30, value=DEFAULT_GEN_STEPS, step=2,
430
+ label="Rollout steps",
431
+ info="Each step emits several frames")
432
+ n_steps = gr.Slider(10, 40, value=DEFAULT_STEPS, step=5,
433
+ label="Diffusion steps",
434
+ info="Flow-matching solver steps per frame")
435
+
436
+ with gr.Accordion("Advanced", open=False):
437
+ seed_in = gr.Number(value=-1, precision=0, label="Seed",
438
+ info="-1 draws a fresh seed each run")
439
+ eta = gr.Slider(0.0, 1.0, value=DEFAULT_ETA, step=0.05,
440
+ label="Eta (stochasticity)",
441
+ info="0 = deterministic ODE; >0 adds noise per step")
442
+
443
+ btn = gr.Button("Generate rollouts", variant="primary")
444
+
445
+ with gr.Column(scale=1):
446
+ outs = [gr.Video(label=f"Rollout {i + 1}", autoplay=True, loop=True)
447
+ for i in range(NUM_VIDEOS)]
448
+
449
+ btn.click(
450
+ run_rollout,
451
+ inputs=[inp, n_steps_gen, n_steps, seed_in, eta],
452
+ outputs=outs,
453
+ concurrency_limit=1,
454
+ )
455
+
456
+ return demo
457
+
458
+
459
+ if __name__ == "__main__":
460
+ build_demo().queue(max_size=8).launch()
orbis2/data/datamodule.py ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+
4
+ import torch
5
+ import torch.distributed as dist
6
+ from torch.utils.data import DataLoader, ConcatDataset
7
+ from torch.utils.data.distributed import DistributedSampler
8
+ from torch.utils.data.dataloader import default_collate
9
+
10
+ import pytorch_lightning as pl
11
+
12
+ from omegaconf import ListConfig, DictConfig
13
+
14
+ import logging
15
+
16
+ from util import instantiate_from_config
17
+
18
+ logger = logging.getLogger(__name__)
19
+
20
+
21
+ def _collate_pad_missing(batch):
22
+ """Collate dicts that may have different keys across datasets.
23
+
24
+ Missing keys are filled with zero tensors matching the shape of the first
25
+ sample in the batch that has that key. Non-tensor values are filled with
26
+ None. Allows heterogeneous datasets (e.g. with/without steering) to be
27
+ mixed in the same batch.
28
+ """
29
+ if not isinstance(batch[0], dict):
30
+ return default_collate(batch)
31
+
32
+ all_keys = set().union(*[item.keys() for item in batch])
33
+ filled = []
34
+ for item in batch:
35
+ item = dict(item)
36
+ for key in all_keys:
37
+ if key not in item:
38
+ ref = next((b[key] for b in batch if key in b), None)
39
+ if isinstance(ref, torch.Tensor):
40
+ item[key] = torch.full_like(ref, float("nan"))
41
+ else:
42
+ item[key] = ref
43
+ filled.append(item)
44
+ return default_collate(filled)
45
+
46
+
47
+ def _env_bool(name, default):
48
+ raw = os.environ.get(name)
49
+ if raw is None:
50
+ return default
51
+ value = raw.strip().lower()
52
+ if value in {"1", "true", "yes", "y", "on"}:
53
+ return True
54
+ if value in {"0", "false", "no", "n", "off"}:
55
+ return False
56
+ logger.warning("Invalid boolean %s=%r; using default %s", name, raw, default)
57
+ return default
58
+
59
+
60
+ def _env_int(name, default):
61
+ raw = os.environ.get(name)
62
+ if raw is None:
63
+ return default
64
+ try:
65
+ return int(raw)
66
+ except ValueError:
67
+ logger.warning("Invalid integer %s=%r; using default %s", name, raw, default)
68
+ return default
69
+
70
+
71
+ class DataModuleFromConfig(pl.LightningDataModule):
72
+ def __init__(self, batch_size, val_batch_size=None, train=None, validation=None, test=None,
73
+ wrap=False, num_workers=None, dbg=False, train_weights=None):
74
+ super().__init__()
75
+ self.batch_size = batch_size
76
+ self.val_batch_size = val_batch_size if val_batch_size is not None else batch_size
77
+ self.dataset_configs = dict()
78
+ self.num_workers = num_workers if num_workers is not None else batch_size*2
79
+ if train is not None:
80
+ self.dataset_configs["train"] = train
81
+ self.train_dataloader = self._train_dataloader
82
+ if validation is not None:
83
+ self.dataset_configs["validation"] = validation
84
+ self.val_dataloader = self._val_dataloader
85
+ if test is not None:
86
+ self.dataset_configs["test"] = test
87
+ self.test_dataloader = self._test_dataloader
88
+ self.wrap = wrap
89
+ self.dbg = dbg
90
+
91
+ if train_weights is not None:
92
+ if not isinstance(train, (list, ListConfig)):
93
+ raise ValueError("train_weights requires train to be a list of dataset configs")
94
+ if len(train_weights) != len(train):
95
+ raise ValueError(
96
+ f"train_weights has {len(train_weights)} entries but train has {len(train)} datasets"
97
+ )
98
+ self.train_weights = train_weights
99
+
100
+ if self.wrap:
101
+ raise NotImplementedError("Wrapped datasets not implemented")
102
+
103
+ self._resume_epoch = None
104
+ self._resume_batches_completed = 0
105
+
106
+ def state_dict(self):
107
+ trainer = getattr(self, "trainer", None)
108
+ if trainer is None:
109
+ return {}
110
+
111
+ epoch = int(getattr(trainer, "current_epoch", 0))
112
+ batches_completed = 0
113
+ try:
114
+ # Lightning tracks completed batches for the current epoch in this counter.
115
+ batches_completed = int(trainer.fit_loop.epoch_loop.batch_progress.current.completed)
116
+ except Exception:
117
+ batches_completed = 0
118
+
119
+ return {
120
+ "resume_epoch": epoch,
121
+ "resume_batches_completed": max(batches_completed, 0),
122
+ }
123
+
124
+ def load_state_dict(self, state_dict):
125
+ if not isinstance(state_dict, dict):
126
+ return
127
+
128
+ self._resume_epoch = state_dict.get("resume_epoch")
129
+ self._resume_batches_completed = int(state_dict.get("resume_batches_completed", 0) or 0)
130
+ if self._resume_batches_completed < 0:
131
+ self._resume_batches_completed = 0
132
+
133
+ def setup(self, stage=None):
134
+ self.datasets = dict()
135
+ for k, cfg in self.dataset_configs.items():
136
+ logger.info("Loading dataset: %s", k)
137
+ if isinstance(cfg, (list, ListConfig)):
138
+ datasets = [instantiate_from_config(c) for c in cfg]
139
+ self.datasets[k] = ConcatDataset(datasets)
140
+ [logger.info(d) for d in datasets]
141
+ elif isinstance(cfg, DictConfig):
142
+ ds = instantiate_from_config(cfg)
143
+ self.datasets[k] = ds
144
+ logger.info(ds)
145
+ else:
146
+ raise ValueError(f"Invalid dataset config: {cfg}")
147
+
148
+ def _train_dataloader(self):
149
+ is_distributed = dist.is_available() and dist.is_initialized()
150
+ use_distributed_sampler = _env_bool("ORBIS_USE_DISTRIBUTED_SAMPLER", True)
151
+ sampler = None
152
+ if self.train_weights is not None:
153
+ train_ds = self.datasets["train"]
154
+ sub_datasets = getattr(train_ds, "datasets", [train_ds])
155
+ dataset_sizes = [len(ds) for ds in sub_datasets]
156
+ # Anchor epoch length to min(size/weight) across datasets.
157
+ # This fully covers the most "weight-adjusted-constrained" dataset (typically
158
+ # the highest-weight one) with exactly one pass, while letting smaller/lower-weight
159
+ # datasets cycle. Avoids the 2x repetition that len(ConcatDataset) causes when
160
+ # one dataset is large and dominates with high weight.
161
+ num_samples = math.ceil(min(s / w for s, w in zip(dataset_sizes, self.train_weights)))
162
+ sampler = _WeightedDistributedSampler(
163
+ self.train_weights, dataset_sizes, num_samples,
164
+ num_replicas=dist.get_world_size() if (is_distributed and use_distributed_sampler) else 1,
165
+ rank=dist.get_rank() if (is_distributed and use_distributed_sampler) else 0,
166
+ )
167
+ logger.info(
168
+ "Train DataLoader: weighted sampling fractions=%s dataset sizes=%s num_samples/epoch=%s",
169
+ list(self.train_weights),
170
+ dataset_sizes,
171
+ num_samples,
172
+ )
173
+ elif is_distributed and use_distributed_sampler:
174
+ sampler = _ResumableDistributedSampler(
175
+ self.datasets["train"],
176
+ shuffle=True,
177
+ drop_last=True,
178
+ resume_epoch=self._resume_epoch,
179
+ resume_batches_completed=self._resume_batches_completed,
180
+ batch_size=self.batch_size,
181
+ )
182
+
183
+ timeout_s = _env_int("ORBIS_DATALOADER_TIMEOUT_S", 0)
184
+ prefetch_factor = _env_int("ORBIS_DATALOADER_PREFETCH_FACTOR", 1)
185
+ persistent_workers = _env_bool(
186
+ "ORBIS_DATALOADER_PERSISTENT_WORKERS",
187
+ self.num_workers > 0,
188
+ )
189
+ mp_context = os.environ.get("ORBIS_DATALOADER_MP_CONTEXT", "").strip()
190
+
191
+ loader_kwargs = dict(
192
+ batch_size=self.batch_size,
193
+ num_workers=self.num_workers,
194
+ shuffle=sampler is None,
195
+ pin_memory=True,
196
+ drop_last=True,
197
+ sampler=sampler,
198
+ timeout=max(timeout_s, 0),
199
+ collate_fn=_collate_pad_missing if self.train_weights is not None else None,
200
+ )
201
+ if self.num_workers > 0:
202
+ loader_kwargs["persistent_workers"] = persistent_workers
203
+ if prefetch_factor > 0:
204
+ loader_kwargs["prefetch_factor"] = prefetch_factor
205
+ if mp_context:
206
+ loader_kwargs["multiprocessing_context"] = mp_context
207
+
208
+ logger.info(
209
+ "Train DataLoader: distributed=%s sampler=%s workers=%s timeout_s=%s mp_context=%s",
210
+ is_distributed,
211
+ sampler.__class__.__name__ if sampler is not None else "None",
212
+ self.num_workers,
213
+ loader_kwargs["timeout"],
214
+ mp_context or "<default>",
215
+ )
216
+
217
+ if self.dbg:
218
+ dbg_sampler = DistributedSampler(self.datasets["train"], shuffle=True)
219
+ loader_kwargs["sampler"] = dbg_sampler
220
+ loader_kwargs["shuffle"] = False
221
+
222
+ return DataLoader(self.datasets["train"], **loader_kwargs)
223
+
224
+ def _val_dataloader(self):
225
+ return DataLoader(self.datasets["validation"],
226
+ batch_size=self.val_batch_size,
227
+ num_workers=self.num_workers, pin_memory=True)
228
+
229
+ def _test_dataloader(self):
230
+ return DataLoader(self.datasets["test"], batch_size=self.val_batch_size,
231
+ num_workers=self.num_workers)
232
+
233
+
234
+ class _ResumableDistributedSampler(DistributedSampler):
235
+ def __init__(
236
+ self,
237
+ dataset,
238
+ *,
239
+ resume_epoch=None,
240
+ resume_batches_completed=0,
241
+ batch_size=1,
242
+ **kwargs,
243
+ ):
244
+ super().__init__(dataset, **kwargs)
245
+ self.resume_epoch = None if resume_epoch is None else int(resume_epoch)
246
+ self.resume_batches_completed = int(resume_batches_completed or 0)
247
+ self.batch_size = max(int(batch_size), 1)
248
+
249
+ def __iter__(self):
250
+ indices = list(super().__iter__())
251
+ if (
252
+ self.resume_epoch is not None
253
+ and int(self.epoch) == self.resume_epoch
254
+ and self.resume_batches_completed > 0
255
+ ):
256
+ skip = self.resume_batches_completed * self.batch_size
257
+ if skip > 0:
258
+ logger.info(
259
+ "Resuming dataloader at epoch=%s after %s batches (%s samples/rank).",
260
+ self.resume_epoch,
261
+ self.resume_batches_completed,
262
+ skip,
263
+ )
264
+ indices = indices[skip:]
265
+ return iter(indices)
266
+
267
+
268
+ class _WeightedDistributedSampler(torch.utils.data.Sampler):
269
+ """Weighted sampler for dataset balancing, with optional distributed sharding.
270
+
271
+ Two-stage sampling: (1) pick a dataset by weight, (2) pick a uniform index
272
+ within that dataset. Avoids torch.multinomial's 2^24 category limit since
273
+ only num_datasets categories are ever sampled, not num_total_samples.
274
+ """
275
+
276
+ def __init__(self, dataset_weights, dataset_sizes, num_samples, num_replicas=1, rank=0):
277
+ self.dataset_weights = torch.as_tensor(dataset_weights, dtype=torch.float64)
278
+ self.dataset_sizes = torch.tensor(dataset_sizes, dtype=torch.long)
279
+ offsets = [0]
280
+ for s in dataset_sizes[:-1]:
281
+ offsets.append(offsets[-1] + s)
282
+ self.dataset_offsets = torch.tensor(offsets, dtype=torch.long)
283
+ self.num_replicas = num_replicas
284
+ self.rank = rank
285
+ self.num_samples_per_replica = math.ceil(num_samples / num_replicas)
286
+ self.epoch = 0
287
+
288
+ def set_epoch(self, epoch):
289
+ self.epoch = epoch
290
+
291
+ def __iter__(self):
292
+ g = torch.Generator()
293
+ g.manual_seed(self.epoch)
294
+ total = self.num_samples_per_replica * self.num_replicas
295
+
296
+ # Stage 1: pick dataset for each draw (num_datasets categories, well within 2^24)
297
+ ds_idx = torch.multinomial(self.dataset_weights, total, replacement=True, generator=g)
298
+
299
+ # Stage 2: pick a uniform index within each chosen dataset
300
+ local = (torch.rand(total, generator=g) * self.dataset_sizes[ds_idx].float()).long()
301
+ indices = (self.dataset_offsets[ds_idx] + local).tolist()
302
+
303
+ return iter(indices[self.rank::self.num_replicas])
304
+
305
+ def __len__(self):
306
+ return self.num_samples_per_replica
orbis2/data/helper_types.py ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict, Tuple, Optional, NamedTuple, Union
2
+ from PIL.Image import Image as pil_image
3
+ from torch import Tensor
4
+
5
+ try:
6
+ from typing import Literal
7
+ except ImportError:
8
+ from typing_extensions import Literal
9
+
10
+ Image = Union[Tensor, pil_image]
11
+ BoundingBox = Tuple[float, float, float, float] # x0, y0, w, h
12
+ CropMethodType = Literal['none', 'random', 'center', 'random-2d']
13
+ SplitType = Literal['train', 'validation', 'test']
14
+
15
+
16
+ class ImageDescription(NamedTuple):
17
+ id: int
18
+ file_name: str
19
+ original_size: Tuple[int, int] # w, h
20
+ url: Optional[str] = None
21
+ license: Optional[int] = None
22
+ coco_url: Optional[str] = None
23
+ date_captured: Optional[str] = None
24
+ flickr_url: Optional[str] = None
25
+ flickr_id: Optional[str] = None
26
+ coco_id: Optional[str] = None
27
+
28
+
29
+ class Category(NamedTuple):
30
+ id: str
31
+ super_category: Optional[str]
32
+ name: str
33
+
34
+
35
+ class Annotation(NamedTuple):
36
+ area: float
37
+ image_id: str
38
+ bbox: BoundingBox
39
+ category_no: int
40
+ category_id: str
41
+ id: Optional[int] = None
42
+ source: Optional[str] = None
43
+ confidence: Optional[float] = None
44
+ is_group_of: Optional[bool] = None
45
+ is_truncated: Optional[bool] = None
46
+ is_occluded: Optional[bool] = None
47
+ is_depiction: Optional[bool] = None
48
+ is_inside: Optional[bool] = None
49
+ segmentation: Optional[Dict] = None
orbis2/data/l2_context.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class L2ContextMixin:
2
+ """Reusable helper for hierarchical L1/L2 frame alignment."""
3
+
4
+ def _init_l2_context(
5
+ self,
6
+ *,
7
+ num_l2_context=0,
8
+ l2_frame_rate=1.0,
9
+ l1_context_frames=1,
10
+ require_l2_context=False,
11
+ ):
12
+ self.num_l2_context = int(num_l2_context)
13
+ self.l1_context_frames = int(l1_context_frames)
14
+
15
+ if self.num_l2_context < 0:
16
+ raise ValueError(f"num_l2_context must be non-negative, got {self.num_l2_context}.")
17
+ if self.l1_context_frames <= 0:
18
+ raise ValueError(f"l1_context_frames must be positive, got {self.l1_context_frames}.")
19
+ if require_l2_context and self.num_l2_context <= 0:
20
+ raise ValueError("num_l2_context must be positive for hierarchical L1/L2 loaders.")
21
+
22
+ self.l2_frame_interval = None
23
+ self._l2_anchor_offset = (self.l1_context_frames - 1) * self.frame_interval
24
+ if self.num_l2_context == 0:
25
+ return
26
+
27
+ ratio = float(self.stored_data_frame_rate) / float(l2_frame_rate)
28
+ rounded_ratio = round(ratio)
29
+ if abs(ratio - rounded_ratio) > 1e-8:
30
+ raise ValueError(
31
+ "stored_data_frame_rate must be an integer multiple of l2_frame_rate, "
32
+ f"got stored_data_frame_rate={self.stored_data_frame_rate}, "
33
+ f"l2_frame_rate={l2_frame_rate}."
34
+ )
35
+ self.l2_frame_interval = int(rounded_ratio)
36
+
37
+ @property
38
+ def l2_context_enabled(self):
39
+ return self.num_l2_context > 0
40
+
41
+ def get_required_l1_start_offset(self):
42
+ if not self.l2_context_enabled:
43
+ return 0
44
+ return max(
45
+ 0,
46
+ (self.num_l2_context - 1) * self.l2_frame_interval - self._l2_anchor_offset,
47
+ )
48
+
49
+ def has_l2_context_for_start(self, start_frame):
50
+ if not self.l2_context_enabled:
51
+ return True
52
+ l2_anchor = start_frame + self._l2_anchor_offset
53
+ oldest_l2 = l2_anchor - self.l2_frame_interval * (self.num_l2_context - 1)
54
+ return oldest_l2 >= 0
55
+
56
+ def filter_index_map_with_l2_headroom(self, index_map, start_frame_idx=-1):
57
+ if not self.l2_context_enabled:
58
+ return list(index_map)
59
+
60
+ filtered = []
61
+ for item in index_map:
62
+ start_frame = item[start_frame_idx]
63
+ if self.has_l2_context_for_start(start_frame):
64
+ filtered.append(item)
65
+ return filtered
66
+
67
+ def get_l1_indices(self, start_frame, num_frames):
68
+ return list(range(
69
+ start_frame,
70
+ start_frame + num_frames * self.frame_interval,
71
+ self.frame_interval,
72
+ ))
73
+
74
+ def get_l2_indices(self, start_frame):
75
+ if not self.l2_context_enabled:
76
+ return []
77
+
78
+ l2_end = start_frame + self._l2_anchor_offset
79
+ l2_start = l2_end - (self.num_l2_context - 1) * self.l2_frame_interval
80
+ return list(range(l2_start, l2_end + 1, self.l2_frame_interval))
81
+
82
+ def get_l1_and_l2_indices(self, start_frame, num_frames):
83
+ return self.get_l1_indices(start_frame, num_frames), self.get_l2_indices(start_frame)
orbis2/data/steering_loaders.py ADDED
@@ -0,0 +1,948 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import h5py
4
+ import numpy as np
5
+ import torch
6
+
7
+ from data.utils import get_trajectory_from_speeds_and_yaw_rates
8
+ from data.l2_context import L2ContextMixin
9
+ from data.video_loaders import (
10
+ MultiHDF5DatasetMultiFrameIdxMapping,
11
+ MultiMP4DatasetMultiFrameIdxMapping,
12
+ MultiMP4DatasetMultiFrameIdxMappingWithL2Context,
13
+ )
14
+ from data.vista_style import (
15
+ VistaStyleNuScenesLoader,
16
+ VistaStyleNuScenesLoaderWithL2Context,
17
+ extract_pose_table,
18
+ get_pose,
19
+ )
20
+ from util import instantiate_from_config
21
+
22
+
23
+ class OdometryHorizonMixin:
24
+ @staticmethod
25
+ def _validate_frame_rate_ratio(stored_rate, sampled_rate, rate_name):
26
+ ratio = stored_rate / sampled_rate
27
+ if not np.isclose(ratio, round(ratio), atol=1e-8):
28
+ raise ValueError(
29
+ f"stored_data_frame_rate={stored_rate} must be an integer multiple of "
30
+ f"{rate_name}={sampled_rate}"
31
+ )
32
+
33
+ @staticmethod
34
+ def _infer_num_frames_odo_for_matched_horizon(num_frames, frame_rate, odometry_frame_rate):
35
+ inferred = (num_frames - 1) * odometry_frame_rate / frame_rate + 1
36
+ if not np.isclose(inferred, round(inferred), atol=1e-8):
37
+ raise ValueError(
38
+ "Matched video/odometry horizons require an integer odometry length, got "
39
+ f"num_frames={num_frames}, frame_rate={frame_rate}, "
40
+ f"odometry_frame_rate={odometry_frame_rate}"
41
+ )
42
+ return int(round(inferred))
43
+
44
+ @classmethod
45
+ def _resolve_odometry_horizon(
46
+ cls,
47
+ num_frames,
48
+ frame_rate,
49
+ num_frames_odo,
50
+ odometry_frame_rate,
51
+ odometry_horizon,
52
+ inferred_num_frames_odo,
53
+ ):
54
+ valid_horizons = {"auto", "match_video", "explicit"}
55
+ if odometry_horizon not in valid_horizons:
56
+ raise ValueError(
57
+ f"Unknown odometry_horizon={odometry_horizon!r}. Expected one of {sorted(valid_horizons)}"
58
+ )
59
+
60
+ if odometry_horizon == "auto":
61
+ if num_frames_odo is None or num_frames_odo == inferred_num_frames_odo:
62
+ odometry_horizon = "match_video"
63
+ else:
64
+ odometry_horizon = "explicit"
65
+
66
+ if odometry_horizon == "match_video":
67
+ resolved_num_frames_odo = inferred_num_frames_odo
68
+ if num_frames_odo is not None and num_frames_odo != resolved_num_frames_odo:
69
+ raise ValueError(
70
+ f"num_frames_odo={num_frames_odo} does not match the video horizon; "
71
+ f"expected {resolved_num_frames_odo} for num_frames={num_frames}, "
72
+ f"frame_rate={frame_rate}, odometry_frame_rate={odometry_frame_rate}"
73
+ )
74
+ return resolved_num_frames_odo, odometry_horizon
75
+
76
+ if num_frames_odo is None:
77
+ raise ValueError("odometry_horizon='explicit' requires num_frames_odo to be provided")
78
+ return int(num_frames_odo), odometry_horizon
79
+
80
+ @classmethod
81
+ def reconfigure_params_for_required_odometry_horizon(cls, params, required_odo_steps):
82
+ if required_odo_steps is None:
83
+ raise ValueError("`required_odo_steps` must be provided")
84
+ required_odo_steps = int(required_odo_steps)
85
+ if required_odo_steps <= 0:
86
+ raise ValueError(f"`required_odo_steps` must be positive, got {required_odo_steps}.")
87
+
88
+ if "odometry_frame_rate" not in params or params.odometry_frame_rate is None:
89
+ params.odometry_frame_rate = params.frame_rate
90
+
91
+ frame_rate = float(params.frame_rate)
92
+ odometry_frame_rate = float(params.odometry_frame_rate)
93
+
94
+ if odometry_frame_rate != frame_rate:
95
+ required_num_frames = int(np.ceil((required_odo_steps - 1) * frame_rate / odometry_frame_rate)) + 1
96
+ params.num_frames = max(int(params.num_frames), required_num_frames)
97
+ params.odometry_horizon = "match_video"
98
+ if "num_frames_odo" in params:
99
+ del params["num_frames_odo"]
100
+ return params
101
+
102
+ if "num_frames_odo" in params and params.num_frames_odo is not None:
103
+ params.num_frames_odo = max(int(params.num_frames_odo), required_odo_steps)
104
+ else:
105
+ params.num_frames_odo = required_odo_steps
106
+ params.odometry_horizon = "explicit"
107
+ return params
108
+
109
+
110
+ class PlaceholderSteeringMixin:
111
+ """Mixin for datasets that have no real steering data.
112
+
113
+ Injects a NaN steering tensor of the correct shape so that rollout_steering_v2
114
+ can work with --steering_file or --no_steering without any changes to downstream
115
+ code. Using such a dataset without either of those flags raises an explicit error
116
+ at rollout validation time (see validate_steering_source in rollout_steering_v2.py).
117
+ """
118
+
119
+ def _init_placeholder_steering(self, num_frames_odo, steering_dim=2, steering_format="speed_yawrate"):
120
+ self._placeholder_num_frames_odo = int(num_frames_odo)
121
+ self._placeholder_steering_dim = int(steering_dim)
122
+ self._placeholder_steering_format = steering_format
123
+
124
+ def _add_placeholder_steering(self, item: dict) -> dict:
125
+ item["steering"] = torch.full(
126
+ (self._placeholder_num_frames_odo, self._placeholder_steering_dim), float("nan")
127
+ )
128
+ item["steering_format"] = self._placeholder_steering_format
129
+ item["_steering_placeholder"] = True
130
+ return item
131
+
132
+ @classmethod
133
+ def reconfigure_params_for_required_odometry_horizon(cls, params, required_odo_steps):
134
+ if required_odo_steps is None:
135
+ raise ValueError("`required_odo_steps` must be provided")
136
+ required_odo_steps = int(required_odo_steps)
137
+ if required_odo_steps <= 0:
138
+ raise ValueError(f"`required_odo_steps` must be positive, got {required_odo_steps}.")
139
+ current = int(getattr(params, "num_frames_odo", 0) or 0)
140
+ params.num_frames_odo = max(current, required_odo_steps)
141
+ return params
142
+
143
+
144
+ class OdometryLoaderConti:
145
+ steering_format = "speed_yawrate"
146
+
147
+ def __call__(self, odo_data):
148
+ speeds = np.array([odo_frame[0] for odo_frame in odo_data])
149
+ yaw_rates = np.array([odo_frame[6] for odo_frame in odo_data])
150
+ ret_odo = np.stack([speeds, yaw_rates], axis=1)
151
+ assert ret_odo.shape[1] == 2 and ret_odo.shape[0] == len(odo_data), f"Unexpected odometry shape {ret_odo.shape}, expected ({len(odo_data)}, 2)"
152
+ return ret_odo
153
+
154
+
155
+ class OdometryLoaderNuPlan:
156
+ steering_format = "speed_yawrate"
157
+
158
+ def __call__(self, odo_data):
159
+ """
160
+ odo_data: list of dicts, each dict contains IMU data for a frame
161
+ """
162
+ ret = np.stack([np.array([odo_frame["vx"], odo_frame["angular_rate_z"]]) for odo_frame in odo_data], axis=0)
163
+ assert ret.shape[1] == 2 and ret.shape[0] == len(odo_data), f"Unexpected odometry shape {ret.shape}, expected ({len(odo_data)}, 2)"
164
+ return ret
165
+
166
+
167
+ class OdometryLoaderNVIDIAPhysAI:
168
+ steering_format = "speed_yawrate"
169
+
170
+ def __init__(self, speed_key="vx", curvature_key="curvature"):
171
+ self.speed_key = speed_key
172
+ self.curvature_key = curvature_key
173
+
174
+ def __call__(self, odo_data):
175
+ """
176
+ odo_data: list of dicts, each dict contains vx (speed), and curvature (which can be used to compute yaw rate as curvature * speed)
177
+
178
+ We return the speed and yaw rate as the odometry.
179
+
180
+ """
181
+ speeds = np.array([odo_frame[self.speed_key] for odo_frame in odo_data])
182
+ yaw_rates = np.array([odo_frame[self.curvature_key] * odo_frame[self.speed_key] for odo_frame in odo_data])
183
+ ret_odo = np.stack([speeds, yaw_rates], axis=1)
184
+ assert ret_odo.shape[1] == 2 and ret_odo.shape[0] == len(odo_data), f"Unexpected odometry shape {ret_odo.shape}, expected ({len(odo_data)}, 2)"
185
+ return ret_odo
186
+
187
+
188
+ class TrajectoryLoaderNuPlanFromSpeedYawRate:
189
+ steering_format = "trajectory_with_heading"
190
+
191
+ def __init__(self, frame_rate, speed_key="vx", yaw_rate_key="angular_rate_z"):
192
+ self.frame_rate = frame_rate
193
+ self.speed_key = speed_key
194
+ self.yaw_rate_key = yaw_rate_key
195
+
196
+ def __call__(self, odo_data):
197
+ """
198
+ odo_data: list of dicts, each dict contains IMU data for a frame
199
+ """
200
+ traj, headings = get_trajectory_from_speeds_and_yaw_rates(
201
+ speeds=np.array([odo_frame[self.speed_key] for odo_frame in odo_data]),
202
+ yaw_rates=np.array([odo_frame[self.yaw_rate_key] for odo_frame in odo_data]),
203
+ dt=1.0 / self.frame_rate,
204
+ )
205
+
206
+ assert traj.shape[1] == 2 and traj.shape[0] == len(odo_data), f"Unexpected odometry shape {traj.shape}, expected ({len(odo_data)}, 2)"
207
+ steering = np.concatenate([traj, headings[:, None]], axis=-1) # (num_frames, 3)
208
+ return steering
209
+
210
+
211
+ class TrajectoryLoaderNVIDIAPhysAIFromSpeedCurvature:
212
+ def __init__(self, frame_rate, speed_key="speed", curvature_key="curvature", return_headings=False):
213
+ self.frame_rate = frame_rate
214
+ self.speed_key = speed_key
215
+ self.curvature_key = curvature_key
216
+ self.return_headings = return_headings
217
+ self.steering_format = "trajectory_with_heading" if return_headings else "trajectory"
218
+
219
+ def __call__(self, odo_data):
220
+ """
221
+ odo_data: list of dicts, each dict contains vx (speed), and curvature (which can be used to compute yaw rate as curvature * speed)
222
+
223
+ We then compute the trajectory by integrating the speeds and yaw rates over time.
224
+
225
+ """
226
+ speeds = np.array([odo_frame[self.speed_key] for odo_frame in odo_data])
227
+ yaw_rates = np.array([odo_frame[self.curvature_key] * odo_frame[self.speed_key] for odo_frame in odo_data])
228
+ traj, headings = get_trajectory_from_speeds_and_yaw_rates(
229
+ speeds=speeds,
230
+ yaw_rates=yaw_rates,
231
+ dt=1.0 / self.frame_rate,
232
+ )
233
+ assert traj.shape[1] == 2 and traj.shape[0] == len(odo_data), f"Unexpected odometry shape {traj.shape}, expected ({len(odo_data)}, 2)"
234
+ if self.return_headings:
235
+ steering = np.concatenate([traj, headings[:, None]], axis=-1) # (num_frames, 3)
236
+ else:
237
+ steering = traj # (num_frames, 2)
238
+ return steering
239
+
240
+
241
+
242
+ class MultiMP4DatasetMultiFrameIdxMappingNVIDIAPhysAI(OdometryHorizonMixin, MultiMP4DatasetMultiFrameIdxMapping):
243
+ """
244
+ Loads the odometry from the NVIDIAPhysAI dataset. The odometry is stored in a single HDF5 file. The HDF5 file contains a dataset for each video (named after the video id).
245
+
246
+ """
247
+ def __init__(
248
+ self,
249
+ size,
250
+ mp4_paths_file,
251
+ odometry_h5_path,
252
+ num_frames,
253
+ num_frames_odo=None,
254
+ stored_data_frame_rate=5,
255
+ frame_rate=5,
256
+ odometry_frame_rate=None,
257
+ odometry_horizon="auto",
258
+ aug="resize_center",
259
+ backend=None,
260
+ odo_transform_config=None,
261
+ return_video_id=False,
262
+ subsample_interval=None,
263
+ intrinsics_h5_path=None,
264
+ spatial_transform_config=None,
265
+ validate_frame_rate_sample=True,
266
+ ):
267
+ self.odometry_frame_rate = frame_rate if odometry_frame_rate is None else odometry_frame_rate
268
+ self.odometry_horizon = odometry_horizon
269
+
270
+ self._validate_frame_rate_ratio(
271
+ stored_rate=stored_data_frame_rate,
272
+ sampled_rate=frame_rate,
273
+ rate_name="frame_rate",
274
+ )
275
+ self._validate_frame_rate_ratio(
276
+ stored_rate=stored_data_frame_rate,
277
+ sampled_rate=self.odometry_frame_rate,
278
+ rate_name="odometry_frame_rate",
279
+ )
280
+ self.odo_frame_interval = int(round(stored_data_frame_rate / self.odometry_frame_rate))
281
+ inferred_num_frames_odo = self._infer_num_frames_odo_for_matched_horizon(
282
+ num_frames=num_frames,
283
+ frame_rate=frame_rate,
284
+ odometry_frame_rate=self.odometry_frame_rate,
285
+ )
286
+ self.num_frames_odo, self.odometry_horizon = self._resolve_odometry_horizon(
287
+ num_frames=num_frames,
288
+ frame_rate=frame_rate,
289
+ num_frames_odo=num_frames_odo,
290
+ odometry_frame_rate=self.odometry_frame_rate,
291
+ odometry_horizon=odometry_horizon,
292
+ inferred_num_frames_odo=inferred_num_frames_odo,
293
+ )
294
+
295
+ super().__init__(
296
+ size=size,
297
+ mp4_paths_file=mp4_paths_file,
298
+ num_frames=num_frames,
299
+ stored_data_frame_rate=stored_data_frame_rate,
300
+ frame_rate=frame_rate,
301
+ aug=aug,
302
+ backend=backend,
303
+ subsample_interval=subsample_interval,
304
+ intrinsics_h5_path=intrinsics_h5_path,
305
+ spatial_transform_config=spatial_transform_config,
306
+ validate_frame_rate_sample=validate_frame_rate_sample,
307
+ )
308
+
309
+ self.odo_file = h5py.File(odometry_h5_path, "r")
310
+ self.return_video_id = return_video_id
311
+
312
+ if odo_transform_config is None:
313
+ raise ValueError(
314
+ "MultiMP4DatasetMultiFrameIdxMappingNVIDIAPhysAI requires explicit "
315
+ "`odo_transform_config`. Use `data.steering_loaders.OdometryLoaderNVIDIAPhysAI` "
316
+ "for raw [speed, yaw_rate], or `data.steering_loaders."
317
+ "TrajectoryLoaderNVIDIAPhysAIFromSpeedCurvature` for trajectory targets."
318
+ )
319
+ self.odo_transform = instantiate_from_config(odo_transform_config)
320
+ self.steering_format = getattr(self.odo_transform, "steering_format", "unknown")
321
+
322
+ # check that all videos have corresponding odometry data, and that the odometry data has the same number of frames as the video
323
+ for mp4_path in self.mp4_paths:
324
+ video_id = self._get_video_id(mp4_path)
325
+ if video_id not in self.odo_file:
326
+ raise KeyError(f"Video id {video_id} not found in odometry file {odometry_h5_path}")
327
+ odo_length = len(self.odo_file[video_id]['odometry'])
328
+ video_length = self.mp4_lengths_in_frames[mp4_path] if self.mp4_lengths_in_frames is not None else self.get_video_length(mp4_path)
329
+ # max difference of 1 frame is allowed to account for rounding issues when the frame rates are different
330
+ if abs(odo_length - video_length) > 1:
331
+ raise ValueError(f"Odometry length {odo_length} does not match video length {video_length} for video id {video_id}")
332
+
333
+ def scan_mp4_files(self):
334
+ self.index_to_starting_frame_map = []
335
+ required_span = max(
336
+ self.num_frames * self.frame_interval,
337
+ self.num_frames_odo * self.odo_frame_interval,
338
+ )
339
+ for path in self.mp4_paths:
340
+ if self.mp4_lengths_in_frames is not None and path in self.mp4_lengths_in_frames:
341
+ video_length = self.mp4_lengths_in_frames[path]
342
+ else:
343
+ video_length = self.get_video_length(path)
344
+
345
+ frame_interval = self.frame_interval if self.subsample_interval is None else self.subsample_interval * self.frame_interval
346
+ max_frame_index = video_length - required_span - 1
347
+ for i in range(0, max_frame_index + 1, frame_interval):
348
+ self.index_to_starting_frame_map.append((path, i))
349
+
350
+ def _get_odometry_indices(self, start_frame):
351
+ return list(range(
352
+ start_frame,
353
+ start_frame + self.num_frames_odo * self.odo_frame_interval,
354
+ self.odo_frame_interval,
355
+ ))
356
+
357
+ def _load_steering_for_video(self, video_id, start_frame):
358
+ indices = self._get_odometry_indices(start_frame)
359
+ odo_data = [self.odo_file[video_id]['odometry'][i] for i in indices]
360
+ return torch.from_numpy(self.odo_transform(odo_data)).float()
361
+
362
+ def __getitem__(self, idx):
363
+ images, (_, path, start_frame) = self.get_images_and_indices(idx)
364
+ images = self.apply_transforms(images, context=self.build_context((None, path, start_frame)))
365
+ video_id = self._get_video_id(path)
366
+ result = {
367
+ "images": images,
368
+ "frame_rate": self.frame_rate,
369
+ "steering": self._load_steering_for_video(video_id, start_frame),
370
+ "steering_format": self.steering_format,
371
+ }
372
+ if self.return_video_id:
373
+ result["video_id"] = video_id
374
+ return result
375
+
376
+
377
+ class MultiHDF5DatasetMultiFrameIdxMappingOdometry(
378
+ OdometryHorizonMixin,
379
+ MultiHDF5DatasetMultiFrameIdxMapping,
380
+ ):
381
+ def __init__(
382
+ self,
383
+ size,
384
+ hdf5_paths_file,
385
+ num_frames,
386
+ num_frames_odo=None,
387
+ frames_file_suffix="frames.h5",
388
+ odo_files_suffix="odometry.h5",
389
+ stored_data_frame_rate=5,
390
+ frame_rate=5,
391
+ odometry_frame_rate=None,
392
+ odometry_horizon="auto",
393
+ aug="resize_center",
394
+ scale_min=0.15,
395
+ scale_max=0.5,
396
+ odo_transform_config=None,
397
+ ):
398
+ self.frames_file_suffix = frames_file_suffix
399
+ self.odo_files_suffix = odo_files_suffix if odo_files_suffix is None or odo_files_suffix.lower() != "none" else None
400
+ self.odometry_frame_rate = frame_rate if odometry_frame_rate is None else odometry_frame_rate
401
+ self.odometry_horizon = odometry_horizon
402
+
403
+ self._validate_frame_rate_ratio(
404
+ stored_rate=stored_data_frame_rate,
405
+ sampled_rate=frame_rate,
406
+ rate_name="frame_rate",
407
+ )
408
+ self._validate_frame_rate_ratio(
409
+ stored_rate=stored_data_frame_rate,
410
+ sampled_rate=self.odometry_frame_rate,
411
+ rate_name="odometry_frame_rate",
412
+ )
413
+ self.odo_frame_interval = int(round(stored_data_frame_rate / self.odometry_frame_rate))
414
+ inferred_num_frames_odo = self._infer_num_frames_odo_for_matched_horizon(
415
+ num_frames=num_frames,
416
+ frame_rate=frame_rate,
417
+ odometry_frame_rate=self.odometry_frame_rate,
418
+ )
419
+ self.num_frames_odo, self.odometry_horizon = self._resolve_odometry_horizon(
420
+ num_frames=num_frames,
421
+ frame_rate=frame_rate,
422
+ num_frames_odo=num_frames_odo,
423
+ odometry_frame_rate=self.odometry_frame_rate,
424
+ odometry_horizon=odometry_horizon,
425
+ inferred_num_frames_odo=inferred_num_frames_odo,
426
+ )
427
+
428
+ super().__init__(
429
+ size=size,
430
+ hdf5_paths_file=hdf5_paths_file,
431
+ num_frames=num_frames,
432
+ stored_data_frame_rate=stored_data_frame_rate,
433
+ frame_rate=frame_rate,
434
+ aug=aug,
435
+ scale_min=scale_min,
436
+ scale_max=scale_max,
437
+ )
438
+
439
+ if odo_transform_config is not None:
440
+ self.odo_transform = instantiate_from_config(odo_transform_config)
441
+ else:
442
+ self.odo_transform = OdometryLoaderNuPlan()
443
+ self.steering_format = getattr(self.odo_transform, "steering_format", "unknown")
444
+
445
+ @staticmethod
446
+ def frames_odo_matching_check(frames_h5, odo_h5):
447
+ for key in frames_h5.keys():
448
+ if "meta_data" in key:
449
+ continue
450
+ if key not in odo_h5:
451
+ raise KeyError(f"Odometry key {key} not found in frames data")
452
+ if len(odo_h5[key]) != len(frames_h5[key]):
453
+ raise ValueError(
454
+ f"Odometry key {key} has different length than frames data: "
455
+ f"{len(odo_h5[key])} != {len(frames_h5[key])}"
456
+ )
457
+
458
+ def scan_h5_files_odo(self):
459
+ self.files_odo = {}
460
+ if self.odo_files_suffix is None:
461
+ return
462
+
463
+ for h5_file_frames in self.hdf5_files:
464
+ odo_h5_path = h5_file_frames.filename.replace(self.frames_file_suffix, self.odo_files_suffix)
465
+ self.files_odo[odo_h5_path] = h5py.File(odo_h5_path, "r")
466
+ self.frames_odo_matching_check(h5_file_frames, self.files_odo[odo_h5_path])
467
+
468
+ def scan_h5_files(self):
469
+ self.index_to_starting_frame_map = []
470
+ required_span = max(
471
+ self.num_frames * self.frame_interval,
472
+ self.num_frames_odo * self.odo_frame_interval,
473
+ )
474
+ for file in self.hdf5_files:
475
+ for key in file.keys():
476
+ if "meta_data" in key:
477
+ continue
478
+ video_length = len(file[key])
479
+ max_frame_index = video_length - required_span - 1
480
+ for i in range(0, max_frame_index + 1):
481
+ self.index_to_starting_frame_map.append((file, key, i))
482
+ self.scan_h5_files_odo()
483
+
484
+ def _get_odometry_indices(self, start_frame):
485
+ return list(range(
486
+ start_frame,
487
+ start_frame + self.num_frames_odo * self.odo_frame_interval,
488
+ self.odo_frame_interval,
489
+ ))
490
+
491
+ def get_odometry(self, filename, key, start_idx):
492
+ odo_filename = filename.replace(self.frames_file_suffix, self.odo_files_suffix)
493
+ indices = self._get_odometry_indices(start_idx)
494
+ odo = [self.files_odo[odo_filename][key][i] for i in indices]
495
+ return torch.as_tensor(self.odo_transform(odo)).float()
496
+
497
+ def __getitem__(self, idx):
498
+ images, (filename, key, start_frame) = self.get_images_and_indices(idx)
499
+ images = self.apply_transforms(images)
500
+ if self.odo_files_suffix is not None:
501
+ odo = self.get_odometry(filename, key, start_frame)
502
+ else:
503
+ odo = torch.full((self.num_frames_odo, 3), float("nan"))
504
+ return {
505
+ "images": images,
506
+ "steering": odo,
507
+ "frame_rate": self.frame_rate,
508
+ "steering_format": self.steering_format,
509
+ }
510
+
511
+
512
+ class MultiMP4DatasetMultiFrameIdxMappingNVIDIAPhysAIWithL2Context(
513
+ L2ContextMixin,
514
+ MultiMP4DatasetMultiFrameIdxMappingNVIDIAPhysAI,
515
+ ):
516
+ def __init__(self, *, num_l2_context, l2_frame_rate=1.0, l1_context_frames=1, **kwargs):
517
+ super().__init__(**kwargs)
518
+ self._init_l2_context(
519
+ num_l2_context=num_l2_context,
520
+ l2_frame_rate=l2_frame_rate,
521
+ l1_context_frames=l1_context_frames,
522
+ require_l2_context=True,
523
+ )
524
+ self.index_to_starting_frame_map = self.filter_index_map_with_l2_headroom(
525
+ self.index_to_starting_frame_map,
526
+ )
527
+
528
+ def _decode_indices(self, path, indices):
529
+ if self.backend == "decord":
530
+ from decord import VideoReader, cpu as decord_cpu
531
+
532
+ file = VideoReader(path, ctx=decord_cpu(0))
533
+ frames = file.get_batch(indices).asnumpy()
534
+ elif self.backend == "torchcodec":
535
+ from torchcodec.decoders import VideoDecoder
536
+
537
+ file = VideoDecoder(path)
538
+ frames = torch.stack([file[i] for i in indices])
539
+ else:
540
+ raise RuntimeError(f"Unknown backend {self.backend}")
541
+ return frames
542
+
543
+ def __getitem__(self, idx):
544
+ if idx >= len(self.index_to_starting_frame_map):
545
+ raise IndexError(f"Index {idx} out of range for dataset of length {len(self.index_to_starting_frame_map)}")
546
+
547
+ path, start_frame = self.index_to_starting_frame_map[idx]
548
+ l1_indices, l2_indices = self.get_l1_and_l2_indices(start_frame, self.num_frames)
549
+ all_frames = self._decode_indices(path, l1_indices + l2_indices)
550
+ all_transformed = self.augmenter(
551
+ all_frames,
552
+ context=self.build_context((None, path, start_frame)),
553
+ )
554
+
555
+ video_id = self._get_video_id(path)
556
+ return {
557
+ "images": all_transformed[:len(l1_indices)],
558
+ "l2_context": all_transformed[len(l1_indices):],
559
+ "frame_rate": self.frame_rate,
560
+ "steering": self._load_steering_for_video(video_id, start_frame),
561
+ "steering_format": self.steering_format,
562
+ }
563
+
564
+
565
+ class VistaStyleNuScenesSteeringMixin(OdometryHorizonMixin):
566
+ def _init_vista_style_steering(
567
+ self,
568
+ *,
569
+ dbs_root,
570
+ annotation_key,
571
+ num_frames,
572
+ stored_data_frame_rate,
573
+ frame_rate,
574
+ num_frames_odo=None,
575
+ odometry_frame_rate=None,
576
+ odometry_horizon="auto",
577
+ ):
578
+ self.dbs_root = dbs_root
579
+ self.annotation_key = annotation_key
580
+ self.steering_format = annotation_key
581
+ self.odometry_frame_rate = frame_rate if odometry_frame_rate is None else odometry_frame_rate
582
+ self.odometry_horizon = odometry_horizon
583
+ self._validate_frame_rate_ratio(
584
+ stored_rate=stored_data_frame_rate,
585
+ sampled_rate=frame_rate,
586
+ rate_name="frame_rate",
587
+ )
588
+ self._validate_frame_rate_ratio(
589
+ stored_rate=stored_data_frame_rate,
590
+ sampled_rate=self.odometry_frame_rate,
591
+ rate_name="odometry_frame_rate",
592
+ )
593
+ self.odo_frame_interval = int(round(stored_data_frame_rate / self.odometry_frame_rate))
594
+ inferred_num_frames_odo = self._infer_num_frames_odo_for_matched_horizon(
595
+ num_frames=num_frames,
596
+ frame_rate=frame_rate,
597
+ odometry_frame_rate=self.odometry_frame_rate,
598
+ )
599
+ self.num_frames_odo, self.odometry_horizon = self._resolve_odometry_horizon(
600
+ num_frames=num_frames,
601
+ frame_rate=frame_rate,
602
+ num_frames_odo=num_frames_odo,
603
+ odometry_frame_rate=self.odometry_frame_rate,
604
+ odometry_horizon=odometry_horizon,
605
+ inferred_num_frames_odo=inferred_num_frames_odo,
606
+ )
607
+ assert self.annotation_key in ["speed_yawrate", "trajectory", "trajectory_with_heading"], (
608
+ f"Unsupported annotation key {self.annotation_key}"
609
+ )
610
+
611
+ @staticmethod
612
+ def _infer_num_frames_odo_for_matched_horizon(num_frames, frame_rate, odometry_frame_rate):
613
+ if num_frames is None:
614
+ raise ValueError("VistaStyleNuScenesLoaderSteering requires num_frames to resolve odometry horizon")
615
+ return OdometryHorizonMixin._infer_num_frames_odo_for_matched_horizon(
616
+ num_frames=num_frames,
617
+ frame_rate=frame_rate,
618
+ odometry_frame_rate=odometry_frame_rate,
619
+ )
620
+
621
+ def _get_vista_style_steering(self, sample):
622
+ pose_table = extract_pose_table(os.path.join(self.dbs_root, sample["db_name"]))
623
+ pose_tokens = sample["pose_tokens"]
624
+ poses = [get_pose(pose_table, pose_token) for pose_token in pose_tokens]
625
+ speeds = np.array([pose["vx"].values[0] for pose in poses])
626
+ yaw_rates = np.array([pose["angular_rate_z"].values[0] for pose in poses])
627
+
628
+ if self.annotation_key == "speed_yawrate":
629
+ speed_yawrate = np.stack([speeds, yaw_rates], axis=-1)
630
+ steering = speed_yawrate[::self.odo_frame_interval]
631
+ assert len(steering) >= self.num_frames_odo, (
632
+ f"Speed/YawRate length {len(steering)} is less than the required {self.num_frames_odo}"
633
+ )
634
+ steering = steering[:self.num_frames_odo]
635
+ elif self.annotation_key == "trajectory_with_heading":
636
+ traj, headings = get_trajectory_from_speeds_and_yaw_rates(
637
+ speeds,
638
+ yaw_rates,
639
+ dt=1 / self.stored_data_frame_rate,
640
+ )
641
+ traj, headings = traj[::self.odo_frame_interval], headings[::self.odo_frame_interval]
642
+ assert len(traj) >= self.num_frames_odo, (
643
+ f"Trajectory length {len(traj)} is less than the required {self.num_frames_odo}"
644
+ )
645
+ steering = np.concatenate([traj[:self.num_frames_odo], headings[:self.num_frames_odo, None]], axis=-1)
646
+ elif self.annotation_key == "trajectory":
647
+ traj, _ = get_trajectory_from_speeds_and_yaw_rates(
648
+ speeds,
649
+ yaw_rates,
650
+ dt=1 / self.stored_data_frame_rate,
651
+ )
652
+ traj = traj[::self.odo_frame_interval]
653
+ assert len(traj) >= self.num_frames_odo, (
654
+ f"Trajectory length {len(traj)} is less than the required {self.num_frames_odo}"
655
+ )
656
+ steering = traj[:self.num_frames_odo]
657
+ else:
658
+ raise ValueError(f"Unsupported annotation key {self.annotation_key}")
659
+
660
+ return torch.from_numpy(steering).float()
661
+
662
+
663
+ class VistaStyleNuScenesLoaderSteering(VistaStyleNuScenesSteeringMixin, VistaStyleNuScenesLoader):
664
+ def __init__(
665
+ self,
666
+ *,
667
+ size,
668
+ json_path,
669
+ images_root,
670
+ dbs_root,
671
+ annotation_key,
672
+ num_frames=None,
673
+ num_frames_odo=None,
674
+ stored_data_frame_rate=10,
675
+ frame_rate=5,
676
+ odometry_frame_rate=None,
677
+ odometry_horizon="auto",
678
+ aug="resize_center",
679
+ sample_indices=None,
680
+ ):
681
+ self.frame_rate = frame_rate
682
+ self.stored_data_frame_rate = stored_data_frame_rate
683
+ self.odometry_frame_rate = frame_rate if odometry_frame_rate is None else odometry_frame_rate
684
+ self._validate_frame_rate_ratio(
685
+ stored_rate=stored_data_frame_rate,
686
+ sampled_rate=frame_rate,
687
+ rate_name="frame_rate",
688
+ )
689
+ self._validate_frame_rate_ratio(
690
+ stored_rate=stored_data_frame_rate,
691
+ sampled_rate=self.odometry_frame_rate,
692
+ rate_name="odometry_frame_rate",
693
+ )
694
+ frame_rate_multiplier = frame_rate / stored_data_frame_rate
695
+ super().__init__(
696
+ size=size,
697
+ json_path=json_path,
698
+ images_root=images_root,
699
+ num_frames=num_frames,
700
+ frame_rate_multiplier=frame_rate_multiplier,
701
+ aug=aug,
702
+ sample_indices=sample_indices,
703
+ )
704
+ self._init_vista_style_steering(
705
+ dbs_root=dbs_root,
706
+ annotation_key=annotation_key,
707
+ num_frames=self.num_frames,
708
+ stored_data_frame_rate=stored_data_frame_rate,
709
+ frame_rate=frame_rate,
710
+ num_frames_odo=num_frames_odo,
711
+ odometry_frame_rate=odometry_frame_rate,
712
+ odometry_horizon=odometry_horizon,
713
+ )
714
+
715
+ def __getitem__(self, index):
716
+ images = super().__getitem__(index)
717
+ sample = self.data[index]
718
+ return {
719
+ "images": images,
720
+ "steering": self._get_vista_style_steering(sample),
721
+ "frame_rate": torch.tensor(self.frame_rate).float(),
722
+ "steering_format": self.steering_format,
723
+ }
724
+
725
+
726
+ class VistaStyleNuScenesLoaderSteeringWithL2Context(
727
+ VistaStyleNuScenesSteeringMixin,
728
+ VistaStyleNuScenesLoaderWithL2Context,
729
+ ):
730
+ def __init__(
731
+ self,
732
+ *,
733
+ size,
734
+ json_path,
735
+ images_root,
736
+ dbs_root,
737
+ annotation_key,
738
+ num_frames=None,
739
+ num_frames_odo=None,
740
+ stored_data_frame_rate=10,
741
+ frame_rate=5,
742
+ odometry_frame_rate=None,
743
+ odometry_horizon="auto",
744
+ num_l2_context,
745
+ l2_frame_rate=1.0,
746
+ l1_context_frames=1,
747
+ aug="resize_center",
748
+ sample_indices=None,
749
+ ):
750
+ super().__init__(
751
+ size=size,
752
+ json_path=json_path,
753
+ images_root=images_root,
754
+ num_frames=num_frames,
755
+ stored_data_frame_rate=stored_data_frame_rate,
756
+ frame_rate=frame_rate,
757
+ num_l2_context=num_l2_context,
758
+ l2_frame_rate=l2_frame_rate,
759
+ l1_context_frames=l1_context_frames,
760
+ aug=aug,
761
+ sample_indices=sample_indices,
762
+ )
763
+ self._init_vista_style_steering(
764
+ dbs_root=dbs_root,
765
+ annotation_key=annotation_key,
766
+ num_frames=self.num_frames,
767
+ stored_data_frame_rate=stored_data_frame_rate,
768
+ frame_rate=frame_rate,
769
+ num_frames_odo=num_frames_odo,
770
+ odometry_frame_rate=odometry_frame_rate,
771
+ odometry_horizon=odometry_horizon,
772
+ )
773
+
774
+ def __getitem__(self, index):
775
+ batch = super().__getitem__(index)
776
+ sample = self.data[index]
777
+ batch["steering"] = self._get_vista_style_steering(sample)
778
+ batch["steering_format"] = self.steering_format
779
+ return batch
780
+
781
+
782
+ class MultiMP4DatasetMultiFrameIdxMappingNoSteering(PlaceholderSteeringMixin, MultiMP4DatasetMultiFrameIdxMapping):
783
+ """MP4 dataset without real steering data.
784
+
785
+ Returns NaN steering placeholders so that rollout_steering_v2 can work with
786
+ --steering_file or --no_steering. Using it without either option raises an
787
+ explicit error at rollout time.
788
+
789
+ Args:
790
+ num_frames_odo: Number of steering timesteps to expose per sample. Set
791
+ this to at least the rollout odometry horizon, or rely on
792
+ reconfigure_params_for_required_odometry_horizon to expand it.
793
+ steering_dim: Feature dimension of the placeholder (default 2 for
794
+ speed/yaw-rate).
795
+ steering_format: The steering_format tag returned in the batch
796
+ (default "speed_yawrate").
797
+ All remaining args are forwarded to MultiMP4DatasetMultiFrameIdxMapping.
798
+ """
799
+
800
+ def __init__(
801
+ self,
802
+ size,
803
+ mp4_paths_file,
804
+ num_frames,
805
+ num_frames_odo,
806
+ steering_dim=2,
807
+ steering_format="speed_yawrate",
808
+ stored_data_frame_rate=5,
809
+ frame_rate=5,
810
+ aug="resize_center",
811
+ backend=None,
812
+ subsample_interval=None,
813
+ intrinsics_h5_path=None,
814
+ spatial_transform_config=None,
815
+ ):
816
+ self._init_placeholder_steering(num_frames_odo, steering_dim, steering_format)
817
+ MultiMP4DatasetMultiFrameIdxMapping.__init__(
818
+ self,
819
+ size=size,
820
+ mp4_paths_file=mp4_paths_file,
821
+ num_frames=num_frames,
822
+ stored_data_frame_rate=stored_data_frame_rate,
823
+ frame_rate=frame_rate,
824
+ aug=aug,
825
+ backend=backend,
826
+ subsample_interval=subsample_interval,
827
+ intrinsics_h5_path=intrinsics_h5_path,
828
+ spatial_transform_config=spatial_transform_config,
829
+ )
830
+
831
+ def __getitem__(self, idx):
832
+ item = MultiMP4DatasetMultiFrameIdxMapping.__getitem__(self, idx)
833
+ return self._add_placeholder_steering(item)
834
+
835
+
836
+ class MultiHDF5DatasetMultiFrameIdxMappingNoSteering(PlaceholderSteeringMixin, MultiHDF5DatasetMultiFrameIdxMapping):
837
+ """HDF5 dataset without real steering data.
838
+
839
+ Returns NaN steering placeholders so that rollout_steering_v2 can work with
840
+ --steering_file or --no_steering. Using it without either option raises an
841
+ explicit error at rollout time.
842
+
843
+ Args:
844
+ num_frames_odo: Number of steering timesteps to expose per sample.
845
+ steering_dim: Feature dimension of the placeholder (default 2).
846
+ steering_format: steering_format tag returned in the batch (default "speed_yawrate").
847
+ All remaining args are forwarded to MultiHDF5DatasetMultiFrameIdxMapping.
848
+ """
849
+
850
+ def __init__(
851
+ self,
852
+ size,
853
+ hdf5_paths_file,
854
+ num_frames,
855
+ num_frames_odo,
856
+ steering_dim=2,
857
+ steering_format="speed_yawrate",
858
+ stored_data_frame_rate=5,
859
+ frame_rate=5,
860
+ aug="resize_center",
861
+ scale_min=0.15,
862
+ scale_max=0.5,
863
+ ):
864
+ self._init_placeholder_steering(num_frames_odo, steering_dim, steering_format)
865
+ MultiHDF5DatasetMultiFrameIdxMapping.__init__(
866
+ self,
867
+ size=size,
868
+ hdf5_paths_file=hdf5_paths_file,
869
+ num_frames=num_frames,
870
+ stored_data_frame_rate=stored_data_frame_rate,
871
+ frame_rate=frame_rate,
872
+ aug=aug,
873
+ scale_min=scale_min,
874
+ scale_max=scale_max,
875
+ )
876
+
877
+ def __getitem__(self, idx):
878
+ item = MultiHDF5DatasetMultiFrameIdxMapping.__getitem__(self, idx)
879
+ return self._add_placeholder_steering(item)
880
+
881
+
882
+ class VistaStyleNuScenesLoaderWithL2ContextNoSteering(
883
+ PlaceholderSteeringMixin,
884
+ VistaStyleNuScenesLoaderWithL2Context,
885
+ ):
886
+ """VistaStyle NuScenes + L2-context dataset without real steering data.
887
+
888
+ Returns NaN steering placeholders alongside the l2_context frames so that
889
+ rollout_steering_v2 can work with --steering_file or --no_steering.
890
+ Using it without either option raises an explicit error at rollout time.
891
+
892
+ Args:
893
+ num_frames_odo: Number of steering timesteps to expose per sample.
894
+ steering_dim: Feature dimension of the placeholder (default 2 for
895
+ speed/yaw-rate).
896
+ steering_format: steering_format tag returned in the batch
897
+ (default "speed_yawrate").
898
+ num_l2_context, l2_frame_rate, l1_context_frames: forwarded to
899
+ VistaStyleNuScenesLoaderWithL2Context / L2ContextMixin.
900
+ All remaining args forwarded to VistaStyleNuScenesLoaderWithL2Context.
901
+ """
902
+
903
+ def __init__(
904
+ self,
905
+ num_frames_odo,
906
+ steering_dim=2,
907
+ steering_format="speed_yawrate",
908
+ **kwargs,
909
+ ):
910
+ self._init_placeholder_steering(num_frames_odo, steering_dim, steering_format)
911
+ VistaStyleNuScenesLoaderWithL2Context.__init__(self, **kwargs)
912
+
913
+ def __getitem__(self, idx):
914
+ item = VistaStyleNuScenesLoaderWithL2Context.__getitem__(self, idx)
915
+ return self._add_placeholder_steering(item)
916
+
917
+
918
+ class MultiMP4DatasetMultiFrameIdxMappingWithL2ContextNoSteering(
919
+ PlaceholderSteeringMixin,
920
+ MultiMP4DatasetMultiFrameIdxMappingWithL2Context,
921
+ ):
922
+ """MP4 + L2-context dataset without real steering data.
923
+
924
+ Returns NaN steering placeholders alongside the l2_context frames so that
925
+ rollout_steering_v2 can work with --steering_file or --no_steering.
926
+ Using it without either option raises an explicit error at rollout time.
927
+
928
+ Args:
929
+ num_frames_odo: Number of steering timesteps to expose per sample.
930
+ steering_dim: Feature dimension of the placeholder (default 2).
931
+ steering_format: steering_format tag returned in the batch (default "speed_yawrate").
932
+ num_l2_context, l2_frame_rate, l1_context_frames: forwarded to L2ContextMixin.
933
+ All remaining args forwarded to MultiMP4DatasetMultiFrameIdxMapping.
934
+ """
935
+
936
+ def __init__(
937
+ self,
938
+ num_frames_odo,
939
+ steering_dim=2,
940
+ steering_format="speed_yawrate",
941
+ **kwargs,
942
+ ):
943
+ self._init_placeholder_steering(num_frames_odo, steering_dim, steering_format)
944
+ MultiMP4DatasetMultiFrameIdxMappingWithL2Context.__init__(self, **kwargs)
945
+
946
+ def __getitem__(self, idx):
947
+ item = MultiMP4DatasetMultiFrameIdxMappingWithL2Context.__getitem__(self, idx)
948
+ return self._add_placeholder_steering(item)
orbis2/data/utils.py ADDED
@@ -0,0 +1,281 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import collections
2
+ import os
3
+ import tarfile
4
+ import urllib
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ from data.helper_types import Annotation
12
+ from torch.utils.data._utils.collate import np_str_obj_array_pattern, default_collate_err_msg_format
13
+ from tqdm import tqdm
14
+
15
+
16
+ def unpack(path):
17
+ if path.endswith("tar.gz"):
18
+ with tarfile.open(path, "r:gz") as tar:
19
+ tar.extractall(path=os.path.split(path)[0])
20
+ elif path.endswith("tar"):
21
+ with tarfile.open(path, "r:") as tar:
22
+ tar.extractall(path=os.path.split(path)[0])
23
+ elif path.endswith("zip"):
24
+ with zipfile.ZipFile(path, "r") as f:
25
+ f.extractall(path=os.path.split(path)[0])
26
+ else:
27
+ raise NotImplementedError(
28
+ "Unknown file extension: {}".format(os.path.splitext(path)[1])
29
+ )
30
+
31
+
32
+ def reporthook(bar):
33
+ """tqdm progress bar for downloads."""
34
+
35
+ def hook(b=1, bsize=1, tsize=None):
36
+ if tsize is not None:
37
+ bar.total = tsize
38
+ bar.update(b * bsize - bar.n)
39
+
40
+ return hook
41
+
42
+
43
+ def get_root(name):
44
+ base = "data/"
45
+ root = os.path.join(base, name)
46
+ os.makedirs(root, exist_ok=True)
47
+ return root
48
+
49
+
50
+ def is_prepared(root):
51
+ return Path(root).joinpath(".ready").exists()
52
+
53
+
54
+ def mark_prepared(root):
55
+ Path(root).joinpath(".ready").touch()
56
+
57
+
58
+ def prompt_download(file_, source, target_dir, content_dir=None):
59
+ targetpath = os.path.join(target_dir, file_)
60
+ while not os.path.exists(targetpath):
61
+ if content_dir is not None and os.path.exists(
62
+ os.path.join(target_dir, content_dir)
63
+ ):
64
+ break
65
+ print(
66
+ "Please download '{}' from '{}' to '{}'.".format(file_, source, targetpath)
67
+ )
68
+ if content_dir is not None:
69
+ print(
70
+ "Or place its content into '{}'.".format(
71
+ os.path.join(target_dir, content_dir)
72
+ )
73
+ )
74
+ input("Press Enter when done...")
75
+ return targetpath
76
+
77
+
78
+ def download_url(file_, url, target_dir):
79
+ targetpath = os.path.join(target_dir, file_)
80
+ os.makedirs(target_dir, exist_ok=True)
81
+ with tqdm(
82
+ unit="B", unit_scale=True, unit_divisor=1024, miniters=1, desc=file_
83
+ ) as bar:
84
+ urllib.request.urlretrieve(url, targetpath, reporthook=reporthook(bar))
85
+ return targetpath
86
+
87
+
88
+ def download_urls(urls, target_dir):
89
+ paths = dict()
90
+ for fname, url in urls.items():
91
+ outpath = download_url(fname, url, target_dir)
92
+ paths[fname] = outpath
93
+ return paths
94
+
95
+
96
+ def quadratic_crop(x, bbox, alpha=1.0):
97
+ """bbox is xmin, ymin, xmax, ymax"""
98
+ im_h, im_w = x.shape[:2]
99
+ bbox = np.array(bbox, dtype=np.float32)
100
+ bbox = np.clip(bbox, 0, max(im_h, im_w))
101
+ center = 0.5 * (bbox[0] + bbox[2]), 0.5 * (bbox[1] + bbox[3])
102
+ w = bbox[2] - bbox[0]
103
+ h = bbox[3] - bbox[1]
104
+ l = int(alpha * max(w, h))
105
+ l = max(l, 2)
106
+
107
+ required_padding = -1 * min(
108
+ center[0] - l, center[1] - l, im_w - (center[0] + l), im_h - (center[1] + l)
109
+ )
110
+ required_padding = int(np.ceil(required_padding))
111
+ if required_padding > 0:
112
+ padding = [
113
+ [required_padding, required_padding],
114
+ [required_padding, required_padding],
115
+ ]
116
+ padding += [[0, 0]] * (len(x.shape) - 2)
117
+ x = np.pad(x, padding, "reflect")
118
+ center = center[0] + required_padding, center[1] + required_padding
119
+ xmin = int(center[0] - l / 2)
120
+ ymin = int(center[1] - l / 2)
121
+ return np.array(x[ymin : ymin + l, xmin : xmin + l, ...])
122
+
123
+
124
+ def custom_collate(batch):
125
+ r"""source: pytorch 1.9.0, only one modification to original code """
126
+
127
+ elem = batch[0]
128
+ elem_type = type(elem)
129
+ if isinstance(elem, torch.Tensor):
130
+ out = None
131
+ if torch.utils.data.get_worker_info() is not None:
132
+ # If we're in a background process, concatenate directly into a
133
+ # shared memory tensor to avoid an extra copy
134
+ numel = sum([x.numel() for x in batch])
135
+ storage = elem.storage()._new_shared(numel)
136
+ out = elem.new(storage)
137
+ return torch.stack(batch, 0, out=out)
138
+ elif elem_type.__module__ == 'numpy' and elem_type.__name__ != 'str_' \
139
+ and elem_type.__name__ != 'string_':
140
+ if elem_type.__name__ == 'ndarray' or elem_type.__name__ == 'memmap':
141
+ # array of string classes and object
142
+ if np_str_obj_array_pattern.search(elem.dtype.str) is not None:
143
+ raise TypeError(default_collate_err_msg_format.format(elem.dtype))
144
+
145
+ return custom_collate([torch.as_tensor(b) for b in batch])
146
+ elif elem.shape == (): # scalars
147
+ return torch.as_tensor(batch)
148
+ elif isinstance(elem, float):
149
+ return torch.tensor(batch, dtype=torch.float64)
150
+ elif isinstance(elem, int):
151
+ return torch.tensor(batch)
152
+ elif isinstance(elem, str):
153
+ return batch
154
+ elif isinstance(elem, collections.abc.Mapping):
155
+ return {key: custom_collate([d[key] for d in batch]) for key in elem}
156
+ elif isinstance(elem, tuple) and hasattr(elem, '_fields'): # namedtuple
157
+ return elem_type(*(custom_collate(samples) for samples in zip(*batch)))
158
+ if isinstance(elem, collections.abc.Sequence) and isinstance(elem[0], Annotation): # added
159
+ return batch # added
160
+ elif isinstance(elem, collections.abc.Sequence):
161
+ # check to make sure that the elements in batch have consistent size
162
+ it = iter(batch)
163
+ elem_size = len(next(it))
164
+ if not all(len(elem) == elem_size for elem in it):
165
+ raise RuntimeError('each element in list of batch should be of equal size')
166
+ transposed = zip(*batch)
167
+ return [custom_collate(samples) for samples in transposed]
168
+
169
+ raise TypeError(default_collate_err_msg_format.format(elem_type))
170
+
171
+
172
+ def get_trajectory_from_speeds_and_yaw_rates(speeds, yaw_rates, dt):
173
+ heading_deltas = yaw_rates * dt
174
+ headings = np.cumsum(heading_deltas)
175
+ headings_for_translation = headings - heading_deltas
176
+ dx = speeds * np.cos(headings_for_translation) * dt
177
+ dy = speeds * np.sin(headings_for_translation) * dt
178
+
179
+ x = np.cumsum(dx)
180
+ y = np.cumsum(dy)
181
+ traj = np.stack([x, y], axis=1)
182
+
183
+ # Transform to local coordinates (first position is origin, first heading is along x-axis)
184
+ traj -= traj[0] # translate to origin
185
+ initial_heading = headings_for_translation[0]
186
+ rotation_matrix = np.array([[np.cos(-initial_heading), -np.sin(-initial_heading)],
187
+ [np.sin(-initial_heading), np.cos(-initial_heading)]])
188
+ local_traj = traj @ rotation_matrix.T # rotate to align with initial heading
189
+
190
+ return local_traj.astype(np.float32), headings.astype(np.float32)
191
+
192
+ def get_trajectory_from_speeds_and_yaw_rates_batch(speeds, yaw_rates, dt):
193
+ """
194
+ Args:
195
+ speeds: Tensor of shape (B, N)
196
+ yaw_rates: Tensor of shape (B, N)
197
+ dt: Time step (scalar)
198
+ Returns:
199
+ local_traj: Tensor of shape (B, N, 2)
200
+ headings: Tensor of shape (B, N)
201
+ """
202
+ assert speeds.shape == yaw_rates.shape, f"Speeds shape {speeds.shape} and yaw rates shape {yaw_rates.shape} do not match"
203
+ B, N = speeds.shape
204
+
205
+ # if dt is a scalar, ok, if dt is a tensor, make sure it has shape (B) and expand to (B, 1)
206
+ if isinstance(dt, torch.Tensor):
207
+ assert dt.shape == (B,), f"dt shape {dt.shape} does not match batch size {B}"
208
+ dt = dt.view(B, 1) # Shape: (B, 1)
209
+
210
+ heading_deltas = yaw_rates * dt # Shape: (B, N)
211
+ headings = torch.cumsum(heading_deltas, dim=1) # Shape: (B, N)
212
+ headings_for_translation = headings - heading_deltas
213
+
214
+ # Calculate dx and dy for each batch
215
+ dx = speeds * torch.cos(headings_for_translation) * dt # Shape: (B, N)
216
+ dy = speeds * torch.sin(headings_for_translation) * dt # Shape: (B, N)
217
+
218
+ # Calculate x and y for each batch
219
+ x = torch.cumsum(dx, dim=1) # Shape: (B, N)
220
+ y = torch.cumsum(dy, dim=1) # Shape: (B, N)
221
+
222
+ # Stack x and y to form the trajectory for each batch
223
+ traj = torch.stack([x, y], dim=2) # Shape: (B, N, 2)
224
+
225
+ # Transform to local coordinates for each batch
226
+ traj = traj- traj[:, 0:1, :] # Translate to origin for each batch
227
+ initial_heading = headings_for_translation[:, 0] # Shape: (B,)
228
+
229
+ # Create rotation matrices for each batch
230
+ cos_theta = torch.cos(-initial_heading) # Shape: (B,)
231
+ sin_theta = torch.sin(-initial_heading) # Shape: (B,)
232
+
233
+ # Rotation matrix for each batch
234
+ rotation_matrix = torch.stack([
235
+ torch.stack([cos_theta, -sin_theta], dim=1),
236
+ torch.stack([sin_theta, cos_theta], dim=1)
237
+ ], dim=1) # Shape: (B, 2, 2)
238
+
239
+ # Rotate to align with initial heading for each batch
240
+ local_traj = torch.einsum('bni,bij->bnj', traj, rotation_matrix) # Shape: (B, N, 2)
241
+
242
+ return torch.cat([local_traj, headings.unsqueeze(-1)], dim=-1).float() # Return (B, N, 3)
243
+
244
+
245
+ class RunningNorm(nn.Module):
246
+ def __init__(self, num_features, momentum=0.1, eps=1e-5):
247
+ super().__init__()
248
+ self.momentum = momentum
249
+ self.eps = eps
250
+
251
+ self.register_buffer('running_mean', torch.zeros(num_features))
252
+ self.register_buffer('running_std', torch.ones(num_features))
253
+
254
+ def _reduce_dims(self, x):
255
+ # Dimensions to reduce: batch and spatial (leave channel/features alone)
256
+ return [0] + list(range(2, x.dim()))
257
+
258
+ def update_stats(self, x):
259
+ dims = self._reduce_dims(x)
260
+ batch_mean = x.mean(dim=dims)
261
+ batch_std = x.std(dim=dims, unbiased=False)
262
+
263
+ # Update running stats
264
+ with torch.no_grad():
265
+ self.running_mean = (1 - self.momentum) * self.running_mean + self.momentum * batch_mean
266
+ self.running_std = (1 - self.momentum) * self.running_std + self.momentum * batch_std
267
+
268
+ def normalize(self, x):
269
+ mean = self.running_mean.view(1, -1, *[1] * (x.dim() - 2))
270
+ std = self.running_std.view(1, -1, *[1] * (x.dim() - 2))
271
+ return (x - mean) / (std + self.eps)
272
+
273
+ def denormalize(self, x):
274
+ mean = self.running_mean.view(1, -1, *[1] * (x.dim() - 2))
275
+ std = self.running_std.view(1, -1, *[1] * (x.dim() - 2))
276
+ return x * (std + self.eps) + mean
277
+
278
+ def forward(self, x):
279
+ if self.training:
280
+ self.update_stats(x)
281
+ return self.normalize(x)
orbis2/data/video_loaders.py ADDED
@@ -0,0 +1,638 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import os
3
+ import random
4
+ import signal
5
+ import warnings
6
+
7
+ from tqdm import tqdm
8
+ import h5py
9
+ import numpy as np
10
+ import torch
11
+ from PIL import Image
12
+ from torch.utils.data import Dataset, get_worker_info
13
+ from torchvision import transforms
14
+ import torch.distributed as dist
15
+
16
+ from .l2_context import L2ContextMixin
17
+ from util import instantiate_from_config
18
+
19
+ try:
20
+ from torchcodec.decoders import VideoDecoder
21
+ TORCHCODEC_AVAILABLE = True
22
+ except ImportError:
23
+ TORCHCODEC_AVAILABLE = False
24
+
25
+ try:
26
+ from decord import VideoReader, cpu as decord_cpu
27
+ DECORD_AVAILABLE = True
28
+ except ImportError:
29
+ DECORD_AVAILABLE = False
30
+
31
+
32
+
33
+ def _normalize_size(size):
34
+ return (size, size) if isinstance(size, int) else tuple(size)
35
+
36
+
37
+ class FrameAdapter:
38
+ def __call__(self, frames):
39
+ raise NotImplementedError
40
+
41
+
42
+ class PILFrameAdapter(FrameAdapter):
43
+ def __call__(self, frames):
44
+ arrays = np.stack([np.asarray(frame) for frame in frames], axis=0)
45
+ return torch.from_numpy(arrays).permute(0, 3, 1, 2).float() / 255.0
46
+
47
+
48
+ class DecordFrameAdapter(FrameAdapter):
49
+ def __call__(self, frames):
50
+ return torch.from_numpy(frames).permute(0, 3, 1, 2).float() / 255.0
51
+
52
+
53
+ class TensorFrameAdapter(FrameAdapter):
54
+ def __call__(self, frames):
55
+ if not torch.is_tensor(frames):
56
+ raise TypeError(f"Expected torch.Tensor frames, got {type(frames).__name__}")
57
+ frames = frames.float()
58
+ if frames.max().item() > 1.0:
59
+ frames = frames / 255.0
60
+ return frames
61
+
62
+
63
+ class SpatialTransformPolicy:
64
+ def __init__(self, size):
65
+ self.size = _normalize_size(size)
66
+
67
+ def sample_params(self, frames, context=None):
68
+ return None
69
+
70
+ def apply(self, frames, params, context=None):
71
+ raise NotImplementedError
72
+
73
+
74
+ class ResizeCenterPolicy(SpatialTransformPolicy):
75
+ def apply(self, frames, params, context=None):
76
+ frames = transforms.functional.resize(frames, [min(self.size)], antialias=True)
77
+ return transforms.functional.center_crop(frames, list(self.size))
78
+
79
+
80
+ class RandomShiftPolicy(SpatialTransformPolicy):
81
+ def __init__(self, size, max_shift_horizontal=60, max_shift_vertical=60):
82
+ super().__init__(size)
83
+ self.max_shift_horizontal = max_shift_horizontal
84
+ self.max_shift_vertical = max_shift_vertical
85
+
86
+ def sample_params(self, frames, context=None):
87
+ frames = transforms.functional.resize(frames, [min(self.size)], antialias=True)
88
+ crop_height, crop_width = self.size
89
+ width, height = frames.shape[-1], frames.shape[-2]
90
+ center_left = (width - crop_width) // 2
91
+ center_top = (height - crop_height) // 2
92
+
93
+ shift_horizontal = random.randint(-self.max_shift_horizontal, self.max_shift_horizontal)
94
+ shift_vertical = random.randint(-self.max_shift_vertical, self.max_shift_vertical)
95
+
96
+ left = max(0, min(center_left + shift_horizontal, width - crop_width))
97
+ top = max(0, min(center_top + shift_vertical, height - crop_height))
98
+ return left, top
99
+
100
+ def apply(self, frames, params, context=None):
101
+ frames = transforms.functional.resize(frames, [min(self.size)], antialias=True)
102
+ left, top = self.sample_params(frames) if params is None else params
103
+ crop_height, crop_width = self.size
104
+ return frames[..., top:top + crop_height, left:left + crop_width]
105
+
106
+
107
+ class RandomResizedCenterPolicy(SpatialTransformPolicy):
108
+ def __init__(self, size, scale=(0.5, 1.0)):
109
+ super().__init__(size)
110
+ self.scale = scale
111
+
112
+ def sample_params(self, frames, context=None):
113
+ height, width = frames.shape[-2], frames.shape[-1]
114
+ area = height * width
115
+ aspect_ratio = width / height
116
+ target_area = random.uniform(*self.scale) * area
117
+
118
+ new_width = int(round((target_area * aspect_ratio) ** 0.5))
119
+ new_height = int(round((target_area / aspect_ratio) ** 0.5))
120
+ return new_height, new_width
121
+
122
+ def apply(self, frames, params, context=None):
123
+ new_height, new_width = self.sample_params(frames) if params is None else params
124
+ frames = transforms.functional.resize(frames, [new_height, new_width], antialias=True)
125
+ return transforms.functional.center_crop(frames, list(self.size))
126
+
127
+
128
+ class CameraCalibrationPolicy(SpatialTransformPolicy):
129
+ def __init__(
130
+ self,
131
+ size,
132
+ transform=None,
133
+ transform_config=None,
134
+ calibration_key="intrinsics",
135
+ fallback_policy=None,
136
+ ):
137
+ super().__init__(size)
138
+ self.transform = transform if transform is not None else instantiate_from_config(transform_config)
139
+ self.calibration_key = calibration_key
140
+ self.fallback_policy = fallback_policy or ResizeCenterPolicy(size)
141
+
142
+ def apply(self, frames, params, context=None):
143
+ if context is None or self.calibration_key not in context:
144
+ # print(f"[CamCalib] FALLBACK — key={self.calibration_key!r} context_keys={list(context.keys()) if context else None}", flush=True)
145
+ return self.fallback_policy.apply(frames, params, context=context)
146
+ return self.transform(frames, context[self.calibration_key])
147
+
148
+
149
+ class ClipAugmenter:
150
+ def __init__(self, adapter, spatial_policy):
151
+ self.adapter = adapter
152
+ self.spatial_policy = spatial_policy
153
+
154
+ def __call__(self, frames, context=None):
155
+ frames = self.adapter(frames)
156
+ params = self.spatial_policy.sample_params(frames, context=context)
157
+ frames = self.spatial_policy.apply(frames, params, context=context)
158
+ return frames * 2 - 1
159
+
160
+
161
+ def _build_frame_adapter(input_format):
162
+ if input_format == "pil":
163
+ return PILFrameAdapter()
164
+ if input_format == "decord":
165
+ return DecordFrameAdapter()
166
+ if input_format in {"tensor", "torchcodec"}:
167
+ return TensorFrameAdapter()
168
+ raise ValueError(f"Unknown input format: {input_format}")
169
+
170
+
171
+ def _build_spatial_policy(aug, size, scale_min=0.15, scale_max=0.5):
172
+ if aug == "resize_center":
173
+ return ResizeCenterPolicy(size)
174
+ if aug == "random_resize_center":
175
+ return RandomResizedCenterPolicy(size, scale=(scale_min, scale_max))
176
+ if aug == "random_shift":
177
+ return RandomShiftPolicy(size, max_shift_horizontal=60, max_shift_vertical=30)
178
+ raise ValueError(f"Unknown augmentation type: {aug}")
179
+
180
+
181
+ def build_clip_augmenter(*, aug, size, input_format, scale_min=0.15, scale_max=0.5, spatial_transform_config=None):
182
+ spatial_policy = (
183
+ instantiate_from_config(spatial_transform_config)
184
+ if spatial_transform_config is not None
185
+ else _build_spatial_policy(aug, size, scale_min=scale_min, scale_max=scale_max)
186
+ )
187
+ return ClipAugmenter(
188
+ adapter=_build_frame_adapter(input_format),
189
+ spatial_policy=spatial_policy,
190
+ )
191
+
192
+
193
+ def concat_frame_batches(*frame_batches):
194
+ first_batch = frame_batches[0]
195
+ if isinstance(first_batch, np.ndarray):
196
+ return np.concatenate(frame_batches, axis=0)
197
+ if torch.is_tensor(first_batch):
198
+ return torch.cat(frame_batches, dim=0)
199
+ raise TypeError(f"Unsupported frame batch type: {type(first_batch).__name__}")
200
+
201
+
202
+ def _read_h5_value(node):
203
+ if isinstance(node, h5py.Dataset):
204
+ value = node[()]
205
+ if isinstance(value, bytes):
206
+ value = value.decode()
207
+ if isinstance(value, str) and value[:1] in "{[":
208
+ try:
209
+ return json.loads(value)
210
+ except json.JSONDecodeError:
211
+ pass
212
+ if isinstance(value, np.ndarray):
213
+ return value.tolist()
214
+ return value.item() if hasattr(value, "item") else value
215
+ return {key: _read_h5_value(node[key]) for key in node.keys()}
216
+
217
+
218
+ class DatasetMultiFrameIdxMapping(Dataset):
219
+ """
220
+ Template dataset that maps each index to a specific frame in a specific video.
221
+ Subclasses must implement get_images_and_indices() and apply_transforms().
222
+ """
223
+ def __init__(self):
224
+ super().__init__()
225
+ self.index_to_starting_frame_map = []
226
+
227
+ def __len__(self):
228
+ return len(self.index_to_starting_frame_map)
229
+
230
+ def get_images_and_indices(self, idx):
231
+ raise NotImplementedError
232
+
233
+ def apply_transforms(self, images, context=None):
234
+ raise NotImplementedError
235
+
236
+ def build_context(self, metadata):
237
+ return None
238
+
239
+ def __getitem__(self, idx):
240
+ images, metadata = self.get_images_and_indices(idx)
241
+ images = self.apply_transforms(images, context=self.build_context(metadata))
242
+ return {"images": images, "frame_rate": self.frame_rate}
243
+
244
+
245
+ class MultiHDF5DatasetMultiFrameIdxMapping(DatasetMultiFrameIdxMapping):
246
+ """
247
+ This dataset maps each index to a specific frame in a specific video.
248
+ Useful for validation and selecting subsets of frames.
249
+ """
250
+ def __init__(self, size, hdf5_paths_file, num_frames, stored_data_frame_rate=5, frame_rate=5, aug="resize_center", scale_min=0.15, scale_max=0.5):
251
+ super().__init__()
252
+ self.frame_interval = int(stored_data_frame_rate / frame_rate)
253
+ self.frame_rate = frame_rate
254
+ self.stored_data_frame_rate = stored_data_frame_rate
255
+
256
+ self.size = (size, size) if isinstance(size, int) else size
257
+ self.num_frames = num_frames
258
+ self.hdf5_paths_file = hdf5_paths_file
259
+ with open(os.path.expandvars(hdf5_paths_file), "r") as f:
260
+ self.hdf5_paths = f.read().splitlines()
261
+
262
+ self.hdf5_files = [h5py.File(path, "r") for path in self.hdf5_paths]
263
+
264
+ self.scan_h5_files()
265
+
266
+ self.aug = aug
267
+ self.augmenter = build_clip_augmenter(
268
+ aug=self.aug,
269
+ size=self.size,
270
+ input_format="pil",
271
+ scale_min=scale_min,
272
+ scale_max=scale_max,
273
+ )
274
+
275
+ def scan_h5_files(self):
276
+ self.index_to_starting_frame_map = []
277
+ for file in self.hdf5_files:
278
+ keys = list(file.keys())
279
+ for key in keys:
280
+ video_length = len(file[key])
281
+ max_frame_index = video_length - self.num_frames * self.frame_interval - 1
282
+ for i in range(0, max_frame_index + 1):
283
+ self.index_to_starting_frame_map.append((file, key, i))
284
+
285
+ def __str__(self):
286
+ return f"MultiHDF5DatasetMultiFrameIdxMapping({self.hdf5_paths_file}, num_samples={len(self)}, size={self.size}, num_frames={self.num_frames}, frame_interval={self.frame_interval})"
287
+
288
+ def get_images_and_indices(self, idx):
289
+ if idx >= len(self.index_to_starting_frame_map):
290
+ raise IndexError(f"Index {idx} out of range for dataset of length {len(self.index_to_starting_frame_map)}")
291
+ file, key, start_frame = self.index_to_starting_frame_map[idx]
292
+ images = [Image.fromarray(file[key][start_frame + i * self.frame_interval]) for i in range(self.num_frames)]
293
+ return images, (file.filename, key, start_frame)
294
+
295
+ def apply_transforms(self, images, context=None):
296
+ return self.augmenter(images, context=context)
297
+
298
+ def close(self):
299
+ for file in self.hdf5_files:
300
+ file.close()
301
+
302
+
303
+ class MultiMP4DatasetMultiFrameIdxMapping(DatasetMultiFrameIdxMapping):
304
+ def __init__(self, size, mp4_paths_file, num_frames, stored_data_frame_rate=5, frame_rate=5, aug="resize_center", backend=None, subsample_interval=None, intrinsics_h5_path=None, spatial_transform_config=None, validate_frame_rate_sample=True):
305
+ super().__init__()
306
+ self.frame_interval = int(stored_data_frame_rate / frame_rate)
307
+ self.frame_rate = frame_rate
308
+ self.stored_data_frame_rate = stored_data_frame_rate
309
+ self.size = (size, size) if isinstance(size, int) else size
310
+ self.num_frames = num_frames
311
+ self.mp4_paths_file = mp4_paths_file
312
+ self.subsample_interval = subsample_interval
313
+ self.intrinsics_h5_path = os.path.expandvars(intrinsics_h5_path) if intrinsics_h5_path is not None else None
314
+ self.intrinsics_file = None
315
+
316
+ with open(os.path.expandvars(mp4_paths_file), "r") as f:
317
+ # the list is either a list of mp4 file paths, or a csv with two columns: file_path,length_in_frames, with a header row.
318
+ lines = f.read().splitlines()
319
+ if len(lines) > 1 and len(lines[0].split(",")) == 2:
320
+ # CSV format
321
+ self.mp4_paths = [self._normalize_path(p.split(",")[0]) for p in lines[1:]]
322
+ mp4_lengths = [int(p.split(",")[1]) for p in lines[1:]]
323
+ # CSV format: create a {'path': length_in_frames} dict
324
+ self.mp4_lengths_in_frames = dict(zip(self.mp4_paths, mp4_lengths))
325
+ else:
326
+ # Plain list of file paths
327
+ self.mp4_paths = [self._normalize_path(p) for p in lines]
328
+ self.mp4_lengths_in_frames = None
329
+
330
+
331
+ if backend is not None:
332
+ self.backend = backend
333
+ assert self.backend in ["decord", "torchcodec"], f"Unknown backend {self.backend}"
334
+ assert (self.backend == "decord" and DECORD_AVAILABLE) or (self.backend == "torchcodec" and TORCHCODEC_AVAILABLE), f"Specified backend {self.backend} is not available"
335
+ else:
336
+ if DECORD_AVAILABLE:
337
+ self.backend = "decord"
338
+ elif TORCHCODEC_AVAILABLE:
339
+ self.backend = "torchcodec"
340
+ else:
341
+ raise ImportError("Either Decord or TorchCodec must be installed to use MultiMP4DatasetMultiFrameIdxMapping")
342
+
343
+ self.validate_frame_rate_sample = validate_frame_rate_sample
344
+ if self.validate_frame_rate_sample:
345
+ self._validate_frame_rate_sample()
346
+ self.scan_mp4_files_distributed()
347
+
348
+ if self.backend == "decord":
349
+ self.read_frames = self.read_frames_decord
350
+ elif self.backend == "torchcodec":
351
+ self.read_frames = self.read_frames_torchcodec
352
+ else:
353
+ raise ImportError("Either Decord or TorchCodec must be installed to use MultiMP4DatasetMultiFrameIdxMapping")
354
+
355
+ self.aug = aug
356
+ self.augmenter = build_clip_augmenter(
357
+ aug=self.aug,
358
+ size=self.size,
359
+ input_format=self.backend,
360
+ spatial_transform_config=spatial_transform_config,
361
+ )
362
+
363
+ # Decode robustness knobs (override via environment if needed).
364
+ self.decode_max_retries = max(int(os.environ.get("ORBIS_DECODE_MAX_RETRIES", "4")), 0)
365
+ self.decode_timeout_s = max(int(os.environ.get("ORBIS_DECODE_TIMEOUT_S", "20")), 0)
366
+ self.decode_warn_limit = max(int(os.environ.get("ORBIS_DECODE_WARN_LIMIT", "50")), 0)
367
+ self._decode_warn_count = 0
368
+
369
+ def _validate_frame_rate_sample(self, sample_size=100):
370
+ paths = random.sample(self.mp4_paths, min(sample_size, len(self.mp4_paths)))
371
+ for path in paths:
372
+ if self.backend == "decord":
373
+ fps = VideoReader(path, ctx=decord_cpu(0), num_threads=-1).get_avg_fps()
374
+ else:
375
+ fps = VideoDecoder(path).metadata.average_fps
376
+ assert self.stored_data_frame_rate == fps, (
377
+ f"Stored data frame rate {self.stored_data_frame_rate} does not match "
378
+ f"actual frame rate {fps} for file {path}"
379
+ )
380
+
381
+ def get_video_length(self, path):
382
+ if self.backend == "decord":
383
+ file = VideoReader(path, ctx=decord_cpu(0), num_threads=-1)
384
+ assert self.stored_data_frame_rate == file.get_avg_fps(), f"Stored data frame rate {self.stored_data_frame_rate} does not match actual frame rate {file.get_avg_fps()} for file {file}"
385
+ video_length = len(file)
386
+ elif self.backend == "torchcodec":
387
+ file = VideoDecoder(path)
388
+ assert self.stored_data_frame_rate == file.metadata.average_fps, f"Stored data frame rate {self.stored_data_frame_rate} does not match actual frame rate {file.metadata.average_fps} for file {file}"
389
+ video_length = file.metadata.num_frames
390
+ else:
391
+ raise ImportError("Either Decord or TorchCodec must be installed to use MultiMP4DatasetMultiFrameIdxMapping")
392
+ return video_length
393
+
394
+ def _normalize_path(self, path):
395
+ return os.path.normpath(os.path.expandvars(os.path.expanduser(path)))
396
+
397
+ def _get_video_id(self, path):
398
+ return os.path.basename(path).split(".")[0]
399
+
400
+ def _get_intrinsics_file(self):
401
+ if self.intrinsics_h5_path is None:
402
+ return None
403
+ if self.intrinsics_file is None:
404
+ self.intrinsics_file = h5py.File(self.intrinsics_h5_path, "r")
405
+ return self.intrinsics_file
406
+
407
+ def _load_intrinsics(self, path):
408
+ intrinsics_file = self._get_intrinsics_file()
409
+ if intrinsics_file is None:
410
+ return None
411
+ video_id = self._get_video_id(path)
412
+ if video_id not in intrinsics_file or "intrinsics" not in intrinsics_file[video_id]:
413
+ return None
414
+ return _read_h5_value(intrinsics_file[video_id]["intrinsics"])
415
+
416
+ def build_context(self, metadata):
417
+ _, path, start_frame = metadata
418
+ context = {"path": path, "video_id": self._get_video_id(path), "start_frame": start_frame}
419
+ intrinsics = self._load_intrinsics(path)
420
+ if intrinsics is not None:
421
+ context["intrinsics"] = intrinsics
422
+ return context
423
+
424
+ def scan_mp4_files(self):
425
+ is_rank0 = not (dist.is_available() and dist.is_initialized()) or dist.get_rank() == 0
426
+ self.index_to_starting_frame_map = []
427
+ for path in tqdm(
428
+ self.mp4_paths,
429
+ desc=f"Scanning MP4 files in {self.__class__.__name__}",
430
+ disable=not is_rank0,
431
+ ):
432
+ if self.mp4_lengths_in_frames is not None and path in self.mp4_lengths_in_frames:
433
+ video_length = self.mp4_lengths_in_frames[path]
434
+ else:
435
+ video_length = self.get_video_length(path)
436
+
437
+ frame_interval = self.frame_interval if self.subsample_interval is None else self.subsample_interval*self.frame_interval
438
+
439
+ max_frame_index = video_length - self.num_frames * self.frame_interval - 1
440
+ # assert max_frame_index > 0
441
+ if not max_frame_index > 0: continue
442
+ for i in range(0, max_frame_index + 1, frame_interval):
443
+ self.index_to_starting_frame_map.append((path, i))
444
+
445
+ def scan_mp4_files_distributed(self):
446
+ if not (dist.is_available() and dist.is_initialized()):
447
+ self.scan_mp4_files()
448
+ return
449
+
450
+ # Scan only on rank 0 and broadcast index mapping to all other ranks.
451
+ if dist.get_rank() == 0:
452
+ self.scan_mp4_files()
453
+ index_map = self.index_to_starting_frame_map
454
+ else:
455
+ index_map = None
456
+
457
+ obj_list = [index_map]
458
+ dist.broadcast_object_list(obj_list, src=0)
459
+ self.index_to_starting_frame_map = obj_list[0]
460
+
461
+ def __str__(self):
462
+ return f"MultiMP4DatasetMultiFrameIdxMapping({self.mp4_paths_file}, num_samples={len(self)}, size={self.size}, num_frames={self.num_frames}, frame_interval={self.frame_interval})"
463
+
464
+ def apply_transforms(self, images, context=None):
465
+ return self.augmenter(images, context=context)
466
+
467
+ def read_frames_decord(self, path, start_frame):
468
+ indices = list(range(start_frame, start_frame + self.num_frames * self.frame_interval, self.frame_interval))
469
+ file = VideoReader(path, ctx=decord_cpu(0))
470
+ frames = file.get_batch(indices).asnumpy()
471
+ return frames
472
+
473
+ def read_frames_torchcodec(self, path, start_frame):
474
+ file = VideoDecoder(path)
475
+ frames = file[start_frame:start_frame + self.num_frames * self.frame_interval:self.frame_interval]
476
+ return frames
477
+
478
+ def _decode_with_timeout(self, path, start_frame):
479
+ if self.decode_timeout_s <= 0 or not hasattr(signal, "SIGALRM"):
480
+ return self.read_frames(path, start_frame)
481
+
482
+ def _timeout_handler(signum, frame): # pragma: no cover - signal handler
483
+ raise TimeoutError(
484
+ f"Decode timed out after {self.decode_timeout_s}s for {path} at frame {start_frame}"
485
+ )
486
+
487
+ previous_handler = signal.signal(signal.SIGALRM, _timeout_handler)
488
+ signal.setitimer(signal.ITIMER_REAL, float(self.decode_timeout_s))
489
+ try:
490
+ return self.read_frames(path, start_frame)
491
+ finally:
492
+ signal.setitimer(signal.ITIMER_REAL, 0.0)
493
+ signal.signal(signal.SIGALRM, previous_handler)
494
+
495
+ def _warn_decode_error(self, msg):
496
+ if self._decode_warn_count < self.decode_warn_limit:
497
+ warnings.warn(msg)
498
+ self._decode_warn_count += 1
499
+ elif self._decode_warn_count == self.decode_warn_limit:
500
+ warnings.warn(
501
+ "Reached ORBIS_DECODE_WARN_LIMIT; suppressing further decode warnings."
502
+ )
503
+ self._decode_warn_count += 1
504
+
505
+ def get_images_and_indices(self, idx):
506
+ if idx >= len(self.index_to_starting_frame_map):
507
+ raise IndexError(f"Index {idx} out of range for dataset of length {len(self.index_to_starting_frame_map)}")
508
+ map_len = len(self.index_to_starting_frame_map)
509
+ worker_info = get_worker_info()
510
+ worker_id = worker_info.id if worker_info is not None else -1
511
+
512
+ last_exc = None
513
+ for attempt in range(self.decode_max_retries + 1):
514
+ attempt_idx = (idx + attempt) % map_len
515
+ path, start_frame = self.index_to_starting_frame_map[attempt_idx]
516
+ try:
517
+ frames = self._decode_with_timeout(path, start_frame)
518
+ if frames is None:
519
+ raise RuntimeError("Decoder returned None")
520
+ num_decoded = frames.shape[0] if hasattr(frames, "shape") else len(frames)
521
+ if num_decoded != self.num_frames:
522
+ raise RuntimeError(
523
+ f"Decoder returned {num_decoded} frames, expected {self.num_frames}"
524
+ )
525
+ return frames, (None, path, start_frame)
526
+ except Exception as exc:
527
+ last_exc = exc
528
+ self._warn_decode_error(
529
+ f"[MultiMP4Dataset] decode failure worker={worker_id} attempt={attempt + 1}/"
530
+ f"{self.decode_max_retries + 1} idx={attempt_idx} path={path} "
531
+ f"start_frame={start_frame}: {type(exc).__name__}: {exc}"
532
+ )
533
+
534
+ raise RuntimeError(
535
+ f"Failed to decode sample idx={idx} after {self.decode_max_retries + 1} attempts"
536
+ ) from last_exc
537
+
538
+ def close(self):
539
+ if self.intrinsics_file is not None:
540
+ self.intrinsics_file.close()
541
+
542
+
543
+ class MultiMP4DatasetMultiFrameIdxMappingWithL2Context(L2ContextMixin, MultiMP4DatasetMultiFrameIdxMapping):
544
+ """
545
+ Extends MultiMP4DatasetMultiFrameIdxMapping to also return a 'l2_context' key
546
+ containing num_l2_context frames at l2_frame_rate (typically 1 Hz) that end on
547
+ the first L1 frame.
548
+
549
+ The l2_context frames are sampled from the same video at the same spatial crop,
550
+ with l2_context[-1] matching images[0].
551
+
552
+ Args:
553
+ num_l2_context : number of L2 context frames (default 3)
554
+ l2_frame_rate : frame rate for L2 context frames in Hz (default 1.0)
555
+ All other args forwarded to MultiMP4DatasetMultiFrameIdxMapping.
556
+
557
+ Batch output adds:
558
+ 'l2_context' : (num_l2_context, C, H, W) pixel tensor in [-1, 1],
559
+ ordered oldest → newest (l2_context[-1] is the same
560
+ frame as images[0]).
561
+ """
562
+
563
+ def __init__(self, num_l2_context=3, l2_frame_rate=1.0, l1_context_frames=1, **kwargs):
564
+ super().__init__(**kwargs)
565
+ self._init_l2_context(
566
+ num_l2_context=num_l2_context,
567
+ l2_frame_rate=l2_frame_rate,
568
+ l1_context_frames=l1_context_frames,
569
+ )
570
+ self.index_to_starting_frame_map = self.filter_index_map_with_l2_headroom(
571
+ self.index_to_starting_frame_map,
572
+ )
573
+
574
+ def get_images_and_indices(self, idx):
575
+ """Return (l1_frames, l2_frames, metadata)."""
576
+ if idx >= len(self.index_to_starting_frame_map):
577
+ raise IndexError(f"Index {idx} out of range.")
578
+ path, start_frame = self.index_to_starting_frame_map[idx]
579
+ l1_indices, l2_indices = self.get_l1_and_l2_indices(start_frame, self.num_frames)
580
+
581
+ all_indices = l1_indices + l2_indices
582
+ all_frames = self._decode_indices(path, all_indices)
583
+
584
+ l1_frames = all_frames[:len(l1_indices)]
585
+ l2_frames = all_frames[len(l1_indices):]
586
+ return l1_frames, l2_frames, (None, path, start_frame)
587
+
588
+ def _decode_indices(self, path, indices):
589
+ """Decode an arbitrary list of frame indices from an MP4."""
590
+ if self.backend == "decord":
591
+ file = VideoReader(path, ctx=decord_cpu(0))
592
+ frames = file.get_batch(indices).asnumpy()
593
+ elif self.backend == "torchcodec":
594
+ file = VideoDecoder(path)
595
+ frames = torch.stack([file[i] for i in indices])
596
+ else:
597
+ raise RuntimeError(f"Unknown backend {self.backend}")
598
+ return frames
599
+
600
+ def __getitem__(self, idx):
601
+ map_len = len(self.index_to_starting_frame_map)
602
+ worker_info = get_worker_info()
603
+ worker_id = worker_info.id if worker_info is not None else -1
604
+ last_exc = None
605
+
606
+ for attempt in range(self.decode_max_retries + 1):
607
+ attempt_idx = (idx + attempt) % map_len
608
+ path, start_frame = self.index_to_starting_frame_map[attempt_idx]
609
+ try:
610
+ l1_frames, l2_frames, _ = self.get_images_and_indices(attempt_idx)
611
+ all_frames = concat_frame_batches(l1_frames, l2_frames)
612
+ all_transformed = self.augmenter(
613
+ all_frames,
614
+ context=self.build_context((None, path, start_frame)),
615
+ )
616
+
617
+ n_l1 = self.num_frames
618
+ images_l1 = all_transformed[:n_l1] # (F, C, H, W)
619
+ images_l2 = all_transformed[n_l1:] # (num_l2_context, C, H, W)
620
+
621
+ return {
622
+ "images": images_l1,
623
+ "l2_context": images_l2,
624
+ "frame_rate": self.frame_rate,
625
+ }
626
+ except Exception as exc:
627
+ last_exc = exc
628
+ self._warn_decode_error(
629
+ f"[MultiMP4DatasetWithL2] decode failure worker={worker_id} "
630
+ f"attempt={attempt + 1}/{self.decode_max_retries + 1} "
631
+ f"idx={attempt_idx} path={path} start_frame={start_frame}: "
632
+ f"{type(exc).__name__}: {exc}"
633
+ )
634
+
635
+ raise RuntimeError(
636
+ f"Failed to decode sample idx={idx} after {self.decode_max_retries + 1} attempts"
637
+ ) from last_exc
638
+
orbis2/data/vista_style.py ADDED
@@ -0,0 +1,183 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from torch.utils.data import Dataset
2
+ import json
3
+ import os, json
4
+ from collections import OrderedDict
5
+ import numpy as np
6
+ import torch
7
+ from torch.utils.data import Dataset
8
+ import cv2
9
+ from PIL import Image
10
+ import sqlite3
11
+ import pandas as pd
12
+ from .utils import get_trajectory_from_speeds_and_yaw_rates
13
+ from .l2_context import L2ContextMixin
14
+ from .video_loaders import build_clip_augmenter
15
+
16
+
17
+ class VistaStyleNuScenesLoader(Dataset):
18
+ def __init__(self, *, size, json_path, images_root, num_frames=None, frame_rate_multiplier=1, aug="resize_center", sample_indices=None):
19
+ super().__init__()
20
+ self.size = (size, size) if isinstance(size, int) else size
21
+ self.json_path = json_path
22
+ self.num_frames = num_frames
23
+ self.images_root = images_root
24
+ self.aug = aug
25
+
26
+ assert frame_rate_multiplier <= 1, "Frame rate multiplier should be less than or equal to 1"
27
+ assert 1/frame_rate_multiplier == int(1/frame_rate_multiplier), 'reciprocal of frame_rate_multiplier must be an integer'
28
+ self.frame_interval = int(1/frame_rate_multiplier)
29
+
30
+ with open(json_path, 'r') as f:
31
+ self.data = json.load(f)
32
+
33
+ self.sample_indices = sample_indices
34
+ if sample_indices is not None:
35
+ self.data = [self.data[i] for i in sample_indices]
36
+ self.augmenter = build_clip_augmenter(
37
+ aug=self.aug,
38
+ size=self.size,
39
+ input_format="pil",
40
+ )
41
+
42
+ def __getitem__(self, index):
43
+ sample = self.data[index]
44
+ frame_paths = sample['frames'][::self.frame_interval]
45
+ if self.num_frames is not None:
46
+ if len(frame_paths) < self.num_frames:
47
+ print(f"Warning: Number of frames {len(frame_paths)} is less than the required {self.num_frames}")
48
+ raise ValueError(f"Number of frames {len(frame_paths)} is less than the required {self.num_frames}")
49
+ frame_paths = frame_paths[:self.num_frames]
50
+ images = [cv2.imread(os.path.join(self.images_root, frame_path)) for frame_path in frame_paths]
51
+ images = [Image.fromarray(cv2.cvtColor(image, cv2.COLOR_BGR2RGB)) for image in images]
52
+ return self.augmenter(images)
53
+
54
+ def __len__(self):
55
+ return len(self.data)
56
+
57
+
58
+
59
+ def extract_pose_table(db_path):
60
+ """
61
+ Connects to the nuPlan SQLite database and extracts the entire ego_pose table.
62
+ Returns a pandas DataFrame containing the pose metadata.
63
+ """
64
+ conn = sqlite3.connect(db_path)
65
+ query = "SELECT * FROM ego_pose;"
66
+ pose_df = pd.read_sql_query(query, conn)
67
+ conn.close()
68
+ # Ensure timestamp is numeric and sort by timestamp
69
+ pose_df['timestamp'] = pd.to_numeric(pose_df['timestamp'], errors='coerce')
70
+ pose_df.sort_values("timestamp", inplace=True)
71
+ return pose_df
72
+
73
+ def get_pose(pose_df, pose_token):
74
+ tk = bytes.fromhex(pose_token)
75
+ pose = pose_df[pose_df['token'] == tk]
76
+ if pose.empty:
77
+ raise ValueError(f"Pose with token {pose_token} not found.")
78
+ return pose
79
+
80
+
81
+ def __getattr__(name):
82
+ if name == "VistaStyleNuScenesLoaderSteering":
83
+ from data.steering_loaders import VistaStyleNuScenesLoaderSteering
84
+ return VistaStyleNuScenesLoaderSteering
85
+ if name == "VistaStyleNuScenesLoaderSteeringWithL2Context":
86
+ from data.steering_loaders import VistaStyleNuScenesLoaderSteeringWithL2Context
87
+ return VistaStyleNuScenesLoaderSteeringWithL2Context
88
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
89
+
90
+
91
+
92
+ class VistaStyleNuScenesLoaderWithL2Context(L2ContextMixin, Dataset):
93
+ def __init__(self, *, size, json_path, images_root, num_frames=None,
94
+ frame_rate_multiplier=1, stored_data_frame_rate=None, frame_rate=None,
95
+ num_l2_context=0, l2_frame_rate=1.0, l1_context_frames=1,
96
+ aug=None, sample_indices=None):
97
+ super().__init__()
98
+ self.size = (size, size) if isinstance(size, int) else size
99
+ self.json_path = json_path
100
+ self.num_frames = num_frames
101
+ self.images_root = images_root
102
+ self.aug = aug or "resize_center"
103
+
104
+ # Compute frame_interval from stored_data_frame_rate/frame_rate when provided,
105
+ # otherwise fall back to frame_rate_multiplier.
106
+ if stored_data_frame_rate is not None and frame_rate is not None:
107
+ assert stored_data_frame_rate % frame_rate == 0, \
108
+ 'stored_data_frame_rate must be divisible by frame_rate'
109
+ self.frame_interval = int(stored_data_frame_rate / frame_rate)
110
+ self.stored_data_frame_rate = stored_data_frame_rate
111
+ self.frame_rate = float(frame_rate)
112
+ else:
113
+ assert frame_rate_multiplier <= 1, "Frame rate multiplier should be less than or equal to 1"
114
+ assert 1/frame_rate_multiplier == int(1/frame_rate_multiplier), \
115
+ 'reciprocal of frame_rate_multiplier must be an integer'
116
+ self.frame_interval = int(1/frame_rate_multiplier)
117
+ # Only set if not already provided by a subclass before calling super().__init__
118
+ if not hasattr(self, 'stored_data_frame_rate'):
119
+ self.stored_data_frame_rate = stored_data_frame_rate
120
+ if not hasattr(self, 'frame_rate'):
121
+ self.frame_rate = float(frame_rate) if frame_rate is not None else None
122
+
123
+ self._init_l2_context(
124
+ num_l2_context=num_l2_context,
125
+ l2_frame_rate=l2_frame_rate,
126
+ l1_context_frames=l1_context_frames,
127
+ )
128
+ self.l1_start_offset = self.get_required_l1_start_offset()
129
+
130
+ with open(json_path, 'r') as f:
131
+ self.data = json.load(f)
132
+
133
+ self.sample_indices = sample_indices
134
+ if sample_indices is not None:
135
+ self.data = [self.data[i] for i in sample_indices]
136
+ self.augmenter = build_clip_augmenter(
137
+ aug=self.aug,
138
+ size=self.size,
139
+ input_format="tensor",
140
+ )
141
+
142
+ def _load_raw_frames(self, frame_paths):
143
+ images = [cv2.imread(os.path.join(self.images_root, p)) for p in frame_paths]
144
+ images = [Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB)) for img in images]
145
+ return torch.stack(
146
+ [torch.from_numpy(np.array(img)).permute(2, 0, 1).float() / 255.0 for img in images],
147
+ dim=0,
148
+ )
149
+
150
+ def _load_images(self, frame_paths):
151
+ frames = self._load_raw_frames(frame_paths)
152
+ return self.augmenter(frames)
153
+
154
+ def __getitem__(self, index):
155
+ sample = self.data[index]
156
+ all_frames = sample['frames']
157
+
158
+ l1_start = self.l1_start_offset
159
+ l1_indices = self.get_l1_indices(l1_start, self.num_frames)
160
+ l2_indices = self.get_l2_indices(l1_start)
161
+ all_indices = l1_indices + l2_indices
162
+
163
+ if max(all_indices) >= len(all_frames):
164
+ raise ValueError(
165
+ f"Number of available L1 frames {len(all_frames)} is insufficient for "
166
+ f"num_frames={self.num_frames} with l1_start_offset={l1_start}."
167
+ )
168
+
169
+ images_all = self._load_images([all_frames[i] for i in all_indices])
170
+ images = images_all[:len(l1_indices)]
171
+
172
+ if self.l2_context_enabled:
173
+ l2_context = images_all[len(l1_indices):]
174
+ return {
175
+ 'images': images,
176
+ 'l2_context': l2_context,
177
+ 'frame_rate': torch.tensor(self.frame_rate).float(),
178
+ }
179
+
180
+ return images
181
+
182
+ def __len__(self):
183
+ return len(self.data)
orbis2/evaluate/rollout_demo_v2.py ADDED
@@ -0,0 +1,706 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import os
3
+ import sys
4
+ import imageio
5
+ import logging
6
+
7
+ sys.path.append(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
8
+
9
+ import cv2
10
+ import numpy as np
11
+ import torch
12
+ from omegaconf import OmegaConf
13
+ from omegaconf.errors import ConfigTypeError
14
+ from PIL import Image
15
+ from pytorch_lightning import seed_everything
16
+ from torchvision.utils import save_image
17
+
18
+ from data.l2_context import L2ContextMixin
19
+ from data.video_loaders import ClipAugmenter, DecordFrameAdapter, ResizeCenterPolicy, TensorFrameAdapter
20
+ from util import instantiate_from_config
21
+
22
+ try:
23
+ from decord import VideoReader, cpu as decord_cpu
24
+ DECORD_AVAILABLE = True
25
+ except ImportError:
26
+ DECORD_AVAILABLE = False
27
+
28
+ try:
29
+ from torchcodec.decoders import VideoDecoder
30
+ TORCHCODEC_AVAILABLE = True
31
+ except ImportError:
32
+ TORCHCODEC_AVAILABLE = False
33
+
34
+
35
+ logger = logging.getLogger(__name__)
36
+
37
+ STEERING_FORMAT = "speed_yawrate"
38
+
39
+
40
+ class _L1L2FrameIndexer(L2ContextMixin):
41
+ """Computes L1/L2 frame indices into a single source video via `L2ContextMixin`."""
42
+
43
+ def __init__(self, frame_interval, stored_data_frame_rate, num_l2_context, l2_frame_rate, l1_context_frames):
44
+ self.frame_interval = frame_interval
45
+ self.stored_data_frame_rate = stored_data_frame_rate
46
+ self._init_l2_context(
47
+ num_l2_context=num_l2_context,
48
+ l2_frame_rate=l2_frame_rate,
49
+ l1_context_frames=l1_context_frames,
50
+ require_l2_context=True,
51
+ )
52
+
53
+
54
+ def get_rollout_future_frame_count(model, num_gen_frames):
55
+ """Return the total number of future image frames produced by the rollout."""
56
+ return int(num_gen_frames) * int(model.num_pred_frames)
57
+
58
+
59
+ def resolve_context_image_size(args, config):
60
+ """Return (height, width) to resize context images to, from CLI args or the training config."""
61
+ if args.height is not None and args.width is not None:
62
+ return int(args.height), int(args.width)
63
+
64
+ try:
65
+ size = OmegaConf.select(config, "data.params.validation.params.size")
66
+ except ConfigTypeError:
67
+ size = None
68
+ if size is None:
69
+ size = config.data.params.train[0].params.size
70
+ size = (size, size) if isinstance(size, int) else tuple(size)
71
+
72
+ height = int(args.height) if args.height is not None else int(size[0])
73
+ width = int(args.width) if args.width is not None else int(size[1])
74
+ return height, width
75
+
76
+
77
+ def require_l1l2_model(model):
78
+ """Raise a clear error if the loaded model isn't an L1-L2 hierarchical model."""
79
+ condition_preprocessor = getattr(model, "condition_preprocessor", None)
80
+ if condition_preprocessor is None or not hasattr(condition_preprocessor, "num_context_frames") \
81
+ or not hasattr(condition_preprocessor, "l2_predictor_frame_rate"):
82
+ raise TypeError(
83
+ "This script requires an L1-L2 hierarchical model, i.e. a `condition_preprocessor` "
84
+ "with a frozen `l2_predictor` (num_context_frames/l2_predictor_frame_rate). "
85
+ f"Got {type(condition_preprocessor).__name__ if condition_preprocessor else None}."
86
+ )
87
+
88
+
89
+ def resolve_video_backend():
90
+ """Pick decord as the primary video backend, falling back to the slower torchcodec if unavailable."""
91
+ if DECORD_AVAILABLE:
92
+ return "decord"
93
+ if TORCHCODEC_AVAILABLE:
94
+ return "torchcodec"
95
+ raise ImportError("Either `decord` or `torchcodec` must be installed to read video frames.")
96
+
97
+
98
+ def get_video_fps_and_length(path, backend):
99
+ """Return (native_fps, num_frames) for a video file."""
100
+ if backend == "decord":
101
+ reader = VideoReader(path, ctx=decord_cpu(0))
102
+ return float(reader.get_avg_fps()), len(reader)
103
+ decoder = VideoDecoder(path)
104
+ return float(decoder.metadata.average_fps), int(decoder.metadata.num_frames)
105
+
106
+
107
+ def decode_video_frames(path, indices, backend):
108
+ """Decode an arbitrary list of frame indices from a video file."""
109
+ if backend == "decord":
110
+ reader = VideoReader(path, ctx=decord_cpu(0))
111
+ return reader.get_batch(indices).asnumpy()
112
+ decoder = VideoDecoder(path)
113
+ return torch.stack([decoder[i] for i in indices])
114
+
115
+
116
+ def compute_frame_interval(native_fps, target_frame_rate, label):
117
+ """Return the integer native-frame stride for `target_frame_rate`, or raise if not exact."""
118
+ ratio = native_fps / float(target_frame_rate)
119
+ rounded = round(ratio)
120
+ if abs(ratio - rounded) > 1e-6:
121
+ raise ValueError(
122
+ f"The video's native frame rate ({native_fps:g} Hz) must be an integer multiple of "
123
+ f"{label} ({target_frame_rate:g} Hz), got ratio={ratio:g}."
124
+ )
125
+ return int(rounded)
126
+
127
+
128
+ def resolve_l2_frame_rate(args, model):
129
+ """Return the L2 sampling rate, defaulting to (and validating against) the frozen L2 predictor's own rate."""
130
+ model_rate = model.condition_preprocessor.l2_predictor_frame_rate
131
+ if args.l2_frame_rate is None:
132
+ return float(model_rate)
133
+ if abs(float(args.l2_frame_rate) - float(model_rate)) > 1e-6:
134
+ raise ValueError(
135
+ f"--l2_frame_rate={args.l2_frame_rate:g} does not match the frozen L2 predictor's "
136
+ f"trained frame rate ({model_rate:g}); the L2 image context must be sampled at the "
137
+ "rate the L2 predictor was trained on."
138
+ )
139
+ return float(args.l2_frame_rate)
140
+
141
+
142
+ def load_l1_l2_context(video_path, start_frame, l1_frame_rate, l2_frame_rate, l1_context_frames, l2_context_frames, height, width, backend, device):
143
+ """Sample L1 (high-rate) and L2 (low-rate, further back) context windows from one video."""
144
+ native_fps, video_length = get_video_fps_and_length(video_path, backend)
145
+ frame_interval = compute_frame_interval(native_fps, l1_frame_rate, "--l1_frame_rate")
146
+ compute_frame_interval(native_fps, l2_frame_rate, "--l2_frame_rate")
147
+
148
+ indexer = _L1L2FrameIndexer(
149
+ frame_interval=frame_interval,
150
+ stored_data_frame_rate=native_fps,
151
+ num_l2_context=l2_context_frames,
152
+ l2_frame_rate=l2_frame_rate,
153
+ l1_context_frames=l1_context_frames,
154
+ )
155
+
156
+ l1_span = (l1_context_frames - 1) * frame_interval + 1
157
+ if start_frame is None:
158
+ start_frame = video_length - l1_span
159
+ if start_frame < 0 or start_frame + l1_span > video_length:
160
+ raise ValueError(
161
+ f"Video is too short for the requested L1 context: need frames [{start_frame}, "
162
+ f"{start_frame + l1_span - 1}], but the video only has {video_length} frames."
163
+ )
164
+
165
+ required_offset = indexer.get_required_l1_start_offset()
166
+ if start_frame < required_offset:
167
+ raise ValueError(
168
+ f"Video does not have enough lookback for L2 context: start_frame={start_frame} but "
169
+ f"at least {required_offset} frames of history are required before it. "
170
+ "Use a longer video, a later --start_frame, or a lower --l2_frame_rate."
171
+ )
172
+
173
+ l1_indices, l2_indices = indexer.get_l1_and_l2_indices(start_frame, l1_context_frames)
174
+ all_frames = decode_video_frames(video_path, l1_indices + l2_indices, backend)
175
+
176
+ adapter = DecordFrameAdapter() if backend == "decord" else TensorFrameAdapter()
177
+ augmenter = ClipAugmenter(adapter, ResizeCenterPolicy((height, width)))
178
+ all_tensor = augmenter(all_frames) # [F, C, H, W] in [-1, 1]
179
+
180
+ l1_tensor = all_tensor[: len(l1_indices)].unsqueeze(0).to(device)
181
+ l2_tensor = all_tensor[len(l1_indices) :].unsqueeze(0).to(device)
182
+ return l1_tensor, l2_tensor
183
+
184
+
185
+ def load_steering_trajectory(steering_file, min_odo_steps, dtype, device):
186
+ """Load a raw [speed, yaw_rate] steering trajectory from a .npy/.csv file as a [1, T, 2] tensor."""
187
+ if not os.path.isfile(steering_file):
188
+ raise FileNotFoundError(f"Steering file {steering_file} does not exist")
189
+
190
+ if steering_file.endswith(".npy"):
191
+ loaded = np.load(steering_file)
192
+ elif steering_file.endswith(".csv"):
193
+ loaded = np.loadtxt(steering_file, delimiter=",")
194
+ else:
195
+ raise ValueError("Steering file must end with .npy or .csv")
196
+
197
+ if loaded.ndim != 2 or loaded.shape[1] != 2:
198
+ raise ValueError(
199
+ f"Steering file must contain [T, 2] (speed, yaw_rate) rows, got shape {tuple(loaded.shape)}"
200
+ )
201
+ if min_odo_steps is not None and loaded.shape[0] < min_odo_steps:
202
+ raise ValueError(
203
+ f"Steering file has too few timesteps: got {loaded.shape[0]}, expected at least {min_odo_steps}"
204
+ )
205
+
206
+ return torch.as_tensor(loaded, dtype=dtype, device=device).unsqueeze(0)
207
+
208
+
209
+ def make_unconditional_steering(min_odo_steps, dtype, device):
210
+ """Build an all-NaN [1, T, 2] steering placeholder, the framework's own 'no steering data' signal."""
211
+ if min_odo_steps is None:
212
+ raise ValueError(
213
+ "Cannot run unconditionally without --steering_file: the loaded condition_preprocessor "
214
+ "could not report a required odometry length (`get_required_rollout_odometry_steps` "
215
+ "returned None), so there is no safe length to build a NaN placeholder at. "
216
+ "Supply --steering_file explicitly."
217
+ )
218
+ return torch.full((1, int(min_odo_steps), 2), float("nan"), dtype=dtype, device=device)
219
+
220
+
221
+ def maybe_apply_condition_preprocessor_scales(model, speed_scale, yaw_rate_scale):
222
+ condition_preprocessor = getattr(model, "condition_preprocessor", None)
223
+ if condition_preprocessor is None:
224
+ return
225
+
226
+ if hasattr(condition_preprocessor, "speed_scale"):
227
+ condition_preprocessor.speed_scale = float(speed_scale)
228
+ if hasattr(condition_preprocessor, "yaw_rate_scale"):
229
+ condition_preprocessor.yaw_rate_scale = float(yaw_rate_scale)
230
+
231
+
232
+ def _rgb_to_cv2_color(color):
233
+ """Convert an RGB tuple in [0, 1] to OpenCV BGR channel order."""
234
+ return (color[2], color[1], color[0])
235
+
236
+
237
+ def _resolve_rollout_cursor_index(cursor_index, shared_trajectory, ctx_size, t, num_points):
238
+ cursor_idx = 0
239
+ if cursor_index is not None:
240
+ cursor_idx = int(cursor_index.item())
241
+ elif shared_trajectory and t >= ctx_size:
242
+ cursor_idx = t - ctx_size
243
+ return min(max(cursor_idx, 0), num_points - 1)
244
+
245
+
246
+ def _panel_coords_fit_trajectory(traj_xy, panel_w, panel_h, margin):
247
+ if traj_xy.shape[0] == 0:
248
+ return None
249
+
250
+ forward = traj_xy[:, 0]
251
+ lateral = traj_xy[:, 1]
252
+
253
+ forward_min = np.min(forward)
254
+ forward_max = np.max(forward)
255
+ lateral_min = np.min(lateral)
256
+ lateral_max = np.max(lateral)
257
+ forward_span = forward_max - forward_min
258
+ lateral_span = lateral_max - lateral_min
259
+ usable_w = max(1, panel_w - 2 * margin)
260
+ usable_h = max(1, panel_h - 2 * margin)
261
+
262
+ scales = []
263
+ if lateral_span > 1e-6:
264
+ scales.append(usable_w / lateral_span)
265
+ if forward_span > 1e-6:
266
+ scales.append(usable_h / forward_span)
267
+ scale = min(scales) if scales else 1.0
268
+
269
+ if lateral_span > 1e-6:
270
+ px = margin + (lateral_max - lateral) * scale
271
+ else:
272
+ px = np.full_like(lateral, panel_w / 2)
273
+ if forward_span > 1e-6:
274
+ py = margin + (forward_max - forward) * scale
275
+ else:
276
+ py = np.full_like(forward, panel_h / 2)
277
+ px = np.clip(px, 0, panel_w - 1)
278
+ py = np.clip(py, 0, panel_h - 1)
279
+ return np.stack([px, py], axis=1).astype(np.int32)
280
+
281
+
282
+ def _transform_trajectory_to_ego_frame(traj_xy, traj_heading, cursor_idx):
283
+ """Translate and rotate the trajectory so the current ego pose is at the origin facing +forward."""
284
+ if traj_xy.shape[0] == 0:
285
+ return traj_xy
286
+
287
+ centered = traj_xy - traj_xy[cursor_idx : cursor_idx + 1]
288
+ heading = float(traj_heading[cursor_idx])
289
+ cos_heading = np.cos(heading)
290
+ sin_heading = np.sin(heading)
291
+ rotation = np.array(
292
+ [
293
+ [cos_heading, sin_heading],
294
+ [-sin_heading, cos_heading],
295
+ ],
296
+ dtype=np.float32,
297
+ )
298
+ return centered @ rotation.T
299
+
300
+
301
+ def _panel_coords_ego_frame(traj_xy, panel_w, panel_h, margin):
302
+ if traj_xy.shape[0] == 0:
303
+ return None
304
+
305
+ forward = traj_xy[:, 0]
306
+ lateral = traj_xy[:, 1]
307
+ extent = max(
308
+ float(np.max(np.abs(forward))),
309
+ float(np.max(np.abs(lateral))),
310
+ 1e-3,
311
+ )
312
+ usable_w = max(1, panel_w - 2 * margin)
313
+ usable_h = max(1, panel_h - 2 * margin)
314
+ scale = min(usable_w, usable_h) / (2.0 * extent)
315
+ center_x = panel_w / 2.0
316
+ center_y = panel_h / 2.0
317
+
318
+ px = center_x - lateral * scale
319
+ py = center_y - forward * scale
320
+ px = np.clip(px, 0, panel_w - 1)
321
+ py = np.clip(py, 0, panel_h - 1)
322
+ return np.stack([px, py], axis=1).astype(np.int32)
323
+
324
+
325
+ def overlay_trajectory_on_images(images, visualization, mode="trajectory"):
326
+ """Draw a compact trajectory panel on top of each rollout frame."""
327
+ cursor_index = None
328
+ headings = None
329
+ trajectory = visualization
330
+ if isinstance(visualization, dict):
331
+ trajectory = visualization.get("trajectory")
332
+ cursor_index = visualization.get("cursor_index")
333
+ headings = visualization.get("heading")
334
+
335
+ if trajectory is None:
336
+ return images
337
+
338
+ if mode not in {"trajectory", "trajectory_ego"}:
339
+ raise ValueError(f"Unsupported trajectory visualization mode: {mode}")
340
+
341
+ shared_trajectory = trajectory.ndim == 3
342
+ if trajectory.ndim == 3:
343
+ trajectory = trajectory.unsqueeze(1).expand(-1, images.shape[1], -1, -1)
344
+ elif trajectory.ndim != 4:
345
+ raise ValueError(f"Expected trajectory with shape [B, T, N, 2] or [B, N, 2], got {tuple(trajectory.shape)}")
346
+ if trajectory.shape[0] != images.shape[0] or trajectory.shape[1] != images.shape[1]:
347
+ raise ValueError(
348
+ f"Trajectory/image batch mismatch: images={tuple(images.shape)}, trajectory={tuple(trajectory.shape)}"
349
+ )
350
+ if cursor_index is not None:
351
+ if not torch.is_tensor(cursor_index):
352
+ cursor_index = torch.as_tensor(cursor_index, dtype=torch.long)
353
+ if cursor_index.ndim == 1:
354
+ cursor_index = cursor_index.unsqueeze(0).expand(images.shape[0], -1)
355
+ if cursor_index.shape[0] != images.shape[0] or cursor_index.shape[1] != images.shape[1]:
356
+ raise ValueError(
357
+ "Cursor/image batch mismatch: "
358
+ f"images={tuple(images.shape)}, cursor_index={tuple(cursor_index.shape)}"
359
+ )
360
+ cursor_index = cursor_index.to(device=trajectory.device)
361
+ if mode == "trajectory_ego":
362
+ if headings is None:
363
+ raise ValueError("Ego-aligned trajectory visualization requires per-point heading data.")
364
+ if headings.ndim == 2:
365
+ headings = headings.unsqueeze(1).expand(-1, images.shape[1], -1)
366
+ elif headings.ndim != 3:
367
+ raise ValueError(f"Expected heading with shape [B, T, N] or [B, N], got {tuple(headings.shape)}")
368
+ if headings.shape[0] != images.shape[0] or headings.shape[1] != images.shape[1]:
369
+ raise ValueError(f"Heading/image batch mismatch: images={tuple(images.shape)}, heading={tuple(headings.shape)}")
370
+ ctx_size = images.shape[1] - trajectory.shape[2] if shared_trajectory else 0
371
+
372
+ height = images.shape[3]
373
+ width = images.shape[4]
374
+
375
+ panel_w = int(height * 0.35)
376
+ panel_h = int(height * 0.35)
377
+ margin = max(2, int(min(height, width) * 0.02))
378
+ panel_x0 = margin
379
+ panel_y0 = height - panel_h - margin
380
+
381
+ traj_color = tuple(c * 2 - 1 for c in _rgb_to_cv2_color((0.0, 1.0, 0.0)))
382
+ cursor_color = tuple(c * 2 - 1 for c in _rgb_to_cv2_color((1.0, 0.25, 0.0)))
383
+ border_color = tuple(c * 2 - 1 for c in _rgb_to_cv2_color((1.0, 1.0, 1.0)))
384
+
385
+ for b in range(images.shape[0]):
386
+ for t in range(images.shape[1]):
387
+ traj_bt = trajectory[b, t].detach().cpu().numpy()
388
+ valid = ~np.isnan(traj_bt).any(axis=1)
389
+ if not np.any(valid):
390
+ continue
391
+
392
+ traj_xy = traj_bt[valid, :2]
393
+ cursor_idx = _resolve_rollout_cursor_index(
394
+ None if cursor_index is None else cursor_index[b, t],
395
+ shared_trajectory=shared_trajectory,
396
+ ctx_size=ctx_size,
397
+ t=t,
398
+ num_points=traj_xy.shape[0],
399
+ )
400
+
401
+ if mode == "trajectory_ego":
402
+ heading_bt = headings[b, t].detach().cpu().numpy()
403
+ heading_valid = heading_bt[valid]
404
+ ego_traj = _transform_trajectory_to_ego_frame(traj_xy, heading_valid, cursor_idx)
405
+ traj_pts = _panel_coords_ego_frame(ego_traj, panel_w, panel_h, margin)
406
+ else:
407
+ traj_pts = _panel_coords_fit_trajectory(traj_xy, panel_w, panel_h, margin)
408
+
409
+ if traj_pts is None:
410
+ continue
411
+
412
+ panel_t = np.full((panel_h, panel_w, 3), -1.0, dtype=np.float32)
413
+ cv2.rectangle(panel_t, (0, 0), (panel_w - 1, panel_h - 1), border_color, 1)
414
+ if traj_pts.shape[0] > 1:
415
+ cv2.polylines(panel_t, [traj_pts.reshape(-1, 1, 2)], False, traj_color, 2)
416
+ else:
417
+ cv2.circle(panel_t, tuple(traj_pts[0]), 2, traj_color, -1)
418
+ if mode == "trajectory_ego":
419
+ ego_marker = np.array([panel_w / 2.0, panel_h / 2.0], dtype=np.float32).astype(np.int32)
420
+ cv2.circle(panel_t, tuple(ego_marker), 3, cursor_color, -1)
421
+ else:
422
+ cv2.circle(panel_t, tuple(traj_pts[cursor_idx]), 3, cursor_color, -1)
423
+
424
+ frame = images[b, t].permute(1, 2, 0).cpu().numpy().copy()
425
+ frame[panel_y0:panel_y0 + panel_h, panel_x0:panel_x0 + panel_w] = panel_t
426
+ images[b, t] = torch.from_numpy(frame).permute(2, 0, 1)
427
+
428
+ return images
429
+
430
+
431
+ @torch.no_grad()
432
+ def generate_images(args, unknown_args):
433
+ """Run an L1-L2 hierarchical rollout from a single input video and save the resulting frames."""
434
+ if args.seed > 0:
435
+ torch.backends.cudnn.enable = False
436
+ torch.backends.cudnn.deterministic = True
437
+ seed_everything(args.seed)
438
+
439
+ config = OmegaConf.load(args.config)
440
+ config = OmegaConf.merge(config, OmegaConf.from_dotlist(unknown_args))
441
+ model = instantiate_from_config(config.model)
442
+
443
+ _ckpt_result = model.load_state_dict(torch.load(args.ckpt)["state_dict"], strict=False)
444
+ _exempt_prefixes = tuple(getattr(model, "checkpoint_exempt_key_prefixes", ()))
445
+ _unexpected_missing_keys = [k for k in _ckpt_result.missing_keys if not k.startswith(_exempt_prefixes)]
446
+ assert _unexpected_missing_keys == [], _unexpected_missing_keys
447
+ model = model.to(args.device)
448
+ _ = model.eval()
449
+
450
+ require_l1l2_model(model)
451
+
452
+ if os.path.exists(args.output_dir):
453
+ print("Folder exists, new images will be saved to the same folder, delete it if you want to start from scratch")
454
+ else:
455
+ os.makedirs(args.output_dir)
456
+
457
+ if args.compile:
458
+ def _maybe_compile(module, attr):
459
+ net = getattr(module, attr, None)
460
+ if net is not None:
461
+ setattr(module, attr, torch.compile(net, mode=args.compile_mode))
462
+ logger.info(f"Compiled {type(module).__name__}.{attr} with mode={args.compile_mode!r}")
463
+
464
+ _maybe_compile(model, 'ema_vit' if args.evaluate_ema else 'vit')
465
+
466
+ _l2_predictor = getattr(getattr(model, 'condition_preprocessor', None), 'l2_predictor', None)
467
+ if _l2_predictor is not None:
468
+ _maybe_compile(_l2_predictor, 'ema_vit')
469
+
470
+ logger.info("First rollout step will be slow (compilation). Subsequent steps reuse the graph.")
471
+
472
+ if args.compile and args.compile_artifacts:
473
+ if os.path.exists(args.compile_artifacts):
474
+ with open(args.compile_artifacts, "rb") as _f:
475
+ torch.compiler.load_cache_artifacts(_f.read())
476
+ logger.info(f"Loaded compile artifacts from {args.compile_artifacts!r}")
477
+ else:
478
+ logger.info(
479
+ f"Compile artifacts not found at {args.compile_artifacts!r}; "
480
+ "will save after the first batch."
481
+ )
482
+
483
+ maybe_apply_condition_preprocessor_scales(model, args.speed_scale, args.yaw_rate_scale)
484
+
485
+ height, width = resolve_context_image_size(args, config)
486
+ backend = resolve_video_backend()
487
+ l2_frame_rate = resolve_l2_frame_rate(args, model)
488
+
489
+ l1_context_frames = int(model.vit.num_context_frames)
490
+ l2_context_frames = int(model.condition_preprocessor.num_context_frames)
491
+
492
+ l1_tensor, l2_tensor = load_l1_l2_context(
493
+ video_path=args.video,
494
+ start_frame=args.start_frame,
495
+ l1_frame_rate=args.l1_frame_rate,
496
+ l2_frame_rate=l2_frame_rate,
497
+ l1_context_frames=l1_context_frames,
498
+ l2_context_frames=l2_context_frames,
499
+ height=height,
500
+ width=width,
501
+ backend=backend,
502
+ device=args.device,
503
+ )
504
+
505
+ num_future_frames = get_rollout_future_frame_count(model, args.num_gen_frames)
506
+ frame_rate = torch.tensor(float(args.l1_frame_rate), device=args.device)
507
+ data_batch = {"images": l1_tensor, "l2_context": l2_tensor, "frame_rate": frame_rate}
508
+
509
+ get_required_steps = getattr(model.condition_preprocessor, "get_required_rollout_odometry_steps", None)
510
+ min_odo_steps = None
511
+ if callable(get_required_steps):
512
+ min_odo_steps = get_required_steps(
513
+ validation_params=None,
514
+ num_condition_frames=l1_context_frames,
515
+ num_gen_frames=num_future_frames,
516
+ rollout_steps=args.num_gen_frames,
517
+ )
518
+ if args.steering_file is not None:
519
+ data_batch["steering"] = load_steering_trajectory(
520
+ args.steering_file, min_odo_steps, dtype=l1_tensor.dtype, device=args.device
521
+ )
522
+ else:
523
+ data_batch["steering"] = make_unconditional_steering(min_odo_steps, dtype=l1_tensor.dtype, device=args.device)
524
+ data_batch["steering_format"] = STEERING_FORMAT
525
+
526
+ # Roll out `num_videos` futures in parallel from the same context, as a single
527
+ # minibatch. The context (and steering) is tiled along the batch dim; the sampler
528
+ # draws independent initial noise per element, so the futures diverge under one
529
+ # seed. Everything below is computed at the tiled batch size so condition_kwargs
530
+ # stays internally consistent.
531
+ num_videos = max(1, int(args.num_videos))
532
+ if num_videos > 1:
533
+ def _tile_batch(t):
534
+ return t.repeat(num_videos, *([1] * (t.dim() - 1)))
535
+
536
+ l1_tensor = _tile_batch(l1_tensor)
537
+ l2_tensor = _tile_batch(l2_tensor)
538
+ data_batch["images"] = l1_tensor
539
+ data_batch["l2_context"] = l2_tensor
540
+ data_batch["steering"] = _tile_batch(data_batch["steering"])
541
+
542
+ condition_kwargs = model.condition_preprocessor.get_condition_kwargs_from_batch(data_batch, split="rollout")
543
+
544
+ logger.info(f"Steering source: {'none' if args.steering_file is None else args.steering_file}")
545
+ logger.info(f"Steering scales: speed={args.speed_scale:g}, yaw_rate={args.yaw_rate_scale:g}")
546
+ logger.info(f"L1/L2 frame rates: {args.l1_frame_rate:g}/{l2_frame_rate:g} Hz")
547
+ logger.info(f"Saving generated images to {args.output_dir}")
548
+
549
+ autocast_enabled = args.device.startswith("cuda")
550
+ with torch.autocast(dtype=torch.float16, device_type="cuda", enabled=autocast_enabled):
551
+ _latents, gen_frames = model.roll_out(
552
+ x_0={"images": l1_tensor},
553
+ num_gen_frames=args.num_gen_frames,
554
+ latent_input=False,
555
+ NFE=args.num_steps,
556
+ eta=args.eta,
557
+ sample_with_ema=args.evaluate_ema,
558
+ num_samples=l1_tensor.size(0),
559
+ frame_rate=frame_rate.reshape(1).repeat(l1_tensor.size(0)),
560
+ condition_kwargs=condition_kwargs,
561
+ decode_device=args.decode_device,
562
+ num_condition_frames=l1_tensor.size(1),
563
+ )
564
+
565
+ if args.vis_mode in {"trajectory", "trajectory_ego"}:
566
+ overlay_trajectory = model.condition_preprocessor.get_rollout_visualization_trajectory(
567
+ condition_kwargs=model.condition_preprocessor.get_condition_kwargs_from_batch(data_batch, split="rollout"),
568
+ num_condition_frames=l1_context_frames,
569
+ num_gen_steps=args.num_gen_frames,
570
+ num_pred_frames=model.num_pred_frames,
571
+ )
572
+ if overlay_trajectory is not None:
573
+ gen_frames = overlay_trajectory_on_images(gen_frames, overlay_trajectory, mode=args.vis_mode)
574
+
575
+ # Release rollout-time latent state before CPU-side file I/O.
576
+ del _latents, condition_kwargs, l1_tensor, l2_tensor
577
+
578
+ # Save each rollout in the minibatch to its own sequence folder. The layout
579
+ # (fake_images/sequence_XXXX/frame_XXXX.jpg) matches what the demo app reads.
580
+ num_out = gen_frames.shape[0]
581
+ num_frames = gen_frames.shape[1]
582
+ for b in range(num_out):
583
+ seq_dir = os.path.join(args.output_dir, "fake_images", f"sequence_{b:04d}")
584
+ os.makedirs(seq_dir, exist_ok=True)
585
+ for f in range(num_frames):
586
+ save_image(
587
+ (gen_frames[b, f] + 1.0) / 2.0,
588
+ os.path.join(seq_dir, f"frame_{f:04d}.jpg"),
589
+ )
590
+
591
+ imageio.mimsave(
592
+ os.path.join(args.output_dir, f"rollout_{b:04d}.gif"),
593
+ [
594
+ np.array(Image.open(os.path.join(seq_dir, f"frame_{f:04d}.jpg")))
595
+ for f in range(num_frames)
596
+ ],
597
+ fps=args.l1_frame_rate,
598
+ loop=0,
599
+ )
600
+
601
+ if args.device.startswith("cuda"):
602
+ logger.info(f"Max memory: {torch.cuda.max_memory_allocated() / 1024**3:.02f} GB")
603
+
604
+ if args.compile and args.compile_artifacts and not os.path.exists(args.compile_artifacts):
605
+ _artifacts = torch.compiler.save_cache_artifacts()
606
+ if _artifacts is not None:
607
+ with open(args.compile_artifacts, "wb") as _f:
608
+ _f.write(_artifacts[0])
609
+ logger.info(f"Saved compile artifacts to {args.compile_artifacts!r}")
610
+
611
+
612
+ def main(args, unknown_args):
613
+ """Entrypoint that launches rollout generation with resolved CLI arguments."""
614
+ generate_images(args, unknown_args)
615
+
616
+
617
+ if __name__ == "__main__":
618
+ logging.basicConfig(level=logging.INFO)
619
+
620
+ def str2bool(v):
621
+ if isinstance(v, bool):
622
+ return v
623
+ if v.lower() in ("yes", "true", "t", "y", "1"):
624
+ return True
625
+ if v.lower() in ("no", "false", "f", "n", "0"):
626
+ return False
627
+ raise argparse.ArgumentTypeError("Boolean value expected.")
628
+
629
+ parser = argparse.ArgumentParser()
630
+ parser.add_argument("--exp_dir", type=str, default=None, help="Path to the experiment directory, where the config and checkpoints are stored")
631
+ parser.add_argument("--ckpt", type=str, default="checkpoints/last.ckpt", help="Path to the checkpoint file, relative to exp_dir")
632
+ parser.add_argument("--config", type=str, default="config.yaml", help="Path to the config file, relative to exp_dir")
633
+ parser.add_argument("--video", type=str, required=True, help="Path to the input video file to sample L1/L2 context from.")
634
+ parser.add_argument("--l1_frame_rate", type=float, required=True, help="Frame rate (Hz) to sample L1 context/rollout frames at, and the generated GIF's fps.")
635
+ parser.add_argument("--l2_frame_rate", type=float, default=None, help="Frame rate (Hz) to sample L2 context frames at. Defaults to the frozen L2 predictor's own trained frame rate; must match it if given explicitly.")
636
+ parser.add_argument("--start_frame", type=int, default=None, help="Native-video frame index to start the L1 context window at. Defaults to the latest window that fits (the end of the video).")
637
+ parser.add_argument("--height", type=int, default=None, help="Height to resize context images to. Defaults to the training config's size.")
638
+ parser.add_argument("--width", type=int, default=None, help="Width to resize context images to. Defaults to the training config's size.")
639
+ parser.add_argument(
640
+ "--num_gen_frames",
641
+ type=int,
642
+ default=1,
643
+ help="Number of rollout steps to generate; each step predicts `model.num_pred_frames` future frames.",
644
+ )
645
+ parser.add_argument("--output_dir", type=str, required=True, help="Directory to save the generated frames and GIF to.")
646
+ parser.add_argument(
647
+ "--num_videos",
648
+ type=int,
649
+ default=1,
650
+ help="Number of futures to roll out in parallel from the same context, as one minibatch.",
651
+ )
652
+ parser.add_argument(
653
+ "--vis_mode",
654
+ type=str,
655
+ default="none",
656
+ choices=["none", "trajectory", "trajectory_ego"],
657
+ help="Visualization mode",
658
+ )
659
+ parser.add_argument("--steering_file", type=str, default=None, help="Optional .npy or .csv trajectory file (columns: speed, yaw_rate), already at the expected odometry rate, used as steering input.")
660
+ parser.add_argument("--speed_scale", type=float, default=1.0, help="Global multiplicative factor applied to raw speed conditioning")
661
+ parser.add_argument("--yaw_rate_scale", type=float, default=1.0, help="Global multiplicative factor applied to raw yaw-rate conditioning")
662
+
663
+ parser.add_argument("--seed", type=int, default=42, help="Seed for reproducibility")
664
+ parser.add_argument("--device", type=str, default="cuda", help="Device")
665
+ parser.add_argument(
666
+ "--decode_device",
667
+ type=str,
668
+ default="cpu",
669
+ help="Device used for decoded rollout frames. Use 'cpu' to reduce peak GPU memory during saving.",
670
+ )
671
+ parser.add_argument("--num_steps", type=int, default=30, help="Number of steps for sampling")
672
+ parser.add_argument("--eta", type=float, default=0.0, help="Stochasticity for sampling")
673
+ parser.add_argument("--evaluate_ema", "--use_ema", type=str2bool, default=True, help="If the evaluation happen with ema model")
674
+ parser.add_argument(
675
+ "--compile",
676
+ type=str2bool,
677
+ default=False,
678
+ help="Wrap the DiT network with torch.compile for faster inference (PyTorch 2.x).",
679
+ )
680
+ parser.add_argument(
681
+ "--compile_mode",
682
+ type=str,
683
+ default="reduce-overhead",
684
+ choices=["default", "reduce-overhead", "max-autotune"],
685
+ help="torch.compile mode. 'reduce-overhead' uses CUDA graphs; 'max-autotune' adds kernel autotuning.",
686
+ )
687
+ parser.add_argument(
688
+ "--compile_artifacts",
689
+ type=str,
690
+ default=None,
691
+ help=(
692
+ "Path to torch.compiler cache artifacts (.pkl), relative to --exp_dir. "
693
+ "If the file exists, artifacts are loaded before rollout (fast startup). "
694
+ "If it does not exist, artifacts are saved after rollout (for future runs). "
695
+ "Only effective when --compile is True."
696
+ ),
697
+ )
698
+
699
+ args, unknown = parser.parse_known_args()
700
+
701
+ args.ckpt = os.path.join(args.exp_dir, args.ckpt)
702
+ args.config = os.path.join(args.exp_dir, args.config)
703
+ if args.compile_artifacts:
704
+ args.compile_artifacts = os.path.join(args.exp_dir, args.compile_artifacts)
705
+
706
+ main(args, unknown)
orbis2/models/first_stage/vqgan.py ADDED
@@ -0,0 +1,1190 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import random
3
+ import pytorch_lightning as pl
4
+ import timm
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from timm.layers.pos_embed import resample_abs_pos_embed
9
+ from torch.optim.lr_scheduler import LambdaLR
10
+
11
+ from util import instantiate_from_config
12
+
13
+ class VQModel(pl.LightningModule):
14
+ def __init__(
15
+ self,
16
+ encoder_config,
17
+ decoder_config,
18
+ quantizer_config,
19
+ loss_config=None,
20
+ grad_acc_steps=1,
21
+ cont_ratio_trainig= 0.0,
22
+ ignore_keys=None,
23
+ monitor=None,
24
+ entropy_loss_weight_scheduler_config=None,
25
+ distill_model_type="VIT_DINOv2", # Options: VIT_DINO, CNN, VIT_DINOv2, etc.
26
+ min_lr_multiplier=0.1,
27
+ only_decoder=False,
28
+ scale_equivariance=None,
29
+ ):
30
+ super().__init__()
31
+
32
+ ignore_keys = ignore_keys or []
33
+ self.automatic_optimization = False
34
+ self.grad_acc_steps = grad_acc_steps
35
+ self.monitor = monitor
36
+ self.distill_model_type = distill_model_type
37
+ self.cont_ratio_trainig = cont_ratio_trainig
38
+ self.only_decoder=only_decoder
39
+ self.min_lr_multiplier = min_lr_multiplier
40
+
41
+ assert (not scale_equivariance) or len(scale_equivariance) == 2, "if defined, scale_equivariance should be a list of two lists"
42
+ self.scale_equivariance = scale_equivariance
43
+
44
+ params_cfg = getattr(loss_config, "params", None)
45
+ if params_cfg is not None and hasattr(params_cfg, "get"):
46
+ default_lr = params_cfg.get("learning_rate", None)
47
+ else:
48
+ default_lr = None
49
+ # Ensure optimizers have sensible defaults even if the caller overrides later.
50
+ self.learning_rate = default_lr if default_lr is not None else 1e-4
51
+ self.num_iters_per_epoch = 1
52
+
53
+
54
+ # Decoder uses encoder params if none provided
55
+ if not hasattr(decoder_config, "params"):
56
+ decoder_config.params = encoder_config.params
57
+
58
+ # Instantiate core components
59
+ self.encoder = instantiate_from_config(encoder_config)
60
+ self.decoder = instantiate_from_config(decoder_config)
61
+ self.quantize = instantiate_from_config(quantizer_config)
62
+ self.loss = instantiate_from_config(loss_config) if loss_config is not None else None
63
+ self.entropy_loss_weight_scheduler = (
64
+ instantiate_from_config(entropy_loss_weight_scheduler_config)
65
+ if entropy_loss_weight_scheduler_config is not None else None
66
+ )
67
+
68
+ # Convolutional layers for quantization
69
+ self.quant_conv = nn.Conv2d(encoder_config.params["z_channels"], quantizer_config.params["e_dim"], 1)
70
+ self.post_quant_conv = nn.Conv2d(quantizer_config.params["e_dim"], decoder_config.params["z_channels"], 1)
71
+
72
+ self.encoder_normalize_embedding = encoder_config.params.get("normalize_embedding", False)
73
+ self.quantizer_normalize_embedding = quantizer_config.params.get("normalize_embedding", False)
74
+
75
+ self.if_distill_loss = (
76
+ False if loss_config is None
77
+ else loss_config.params.get('distill_loss_weight', 0.0) != 0.0
78
+ )
79
+
80
+ # Image and patch size
81
+ self.image_size = encoder_config.params["resolution"]
82
+ self.patch_size = encoder_config.params["patch_size"]
83
+
84
+ self._init_distill_model(distill_model_type, encoder_config, decoder_config, quantizer_config)
85
+
86
+ def _init_distill_model(self, distill_type, encoder_cfg, decoder_cfg, quantizer_cfg):
87
+ image_size = encoder_cfg.params["resolution"]
88
+ patch_size = encoder_cfg.params["patch_size"]
89
+ q_e_dim = quantizer_cfg.params["e_dim"]
90
+ z_channels = decoder_cfg.params["z_channels"]
91
+
92
+ def conv1x1(in_c, out_c): return nn.Conv2d(in_c, out_c, 1)
93
+
94
+ if distill_type == "VIT_DINO":
95
+ self.distill = timm.create_model("timm/vit_base_patch16_224.dino", img_size=image_size, pretrained=True).eval()
96
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
97
+ elif distill_type == "VIT_DINOv2":
98
+ img_size = self._compute_scaled_size(image_size, patch_size)
99
+ self.distill = timm.create_model("timm/vit_base_patch14_dinov2.lvd142m", img_size=img_size, pretrained=True).eval()
100
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
101
+ elif distill_type == "VIT_DINOv3":
102
+ img_size = self._compute_scaled_size(image_size, patch_size, ckpt_patch_size=16)
103
+ self.distill = torch.hub.load('../dinov3', 'dinov3_vitb16', source='local', weights='./pretrained_models/dinov3_vitb16_pretrain_lvd1689m-73cec8be.pth').eval()
104
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
105
+ elif distill_type == "VIT_DINOv2g":
106
+ img_size = int(image_size * 14 / patch_size)
107
+ self.distill = timm.create_model("timm/vit_giant_patch14_dinov2.lvd142m", img_size=img_size, pretrained=True).eval()
108
+ self.post_quant_conv_distill = conv1x1(q_e_dim, 1536)
109
+ elif distill_type == "VIT_DINOv2_large":
110
+ img_size = int(image_size * 14 / patch_size)
111
+ self.distill = timm.create_model("timm/vit_large_patch14_dinov2.lvd142m", img_size=img_size, pretrained=True).eval()
112
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
113
+ elif distill_type == "VIT_DINOv2_large_reg4":
114
+ img_size = int(image_size * 14 / patch_size)
115
+ self.distill = timm.create_model("timm/vit_large_patch14_reg4_dinov2.lvd142m", img_size=img_size, pretrained=True).eval()
116
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
117
+ elif distill_type == "SAM_VIT":
118
+ self.distill = timm.create_model("samvit_large_patch16.sa1b", pretrained=True)
119
+ self.post_quant_conv_distill = nn.Identity()
120
+ elif distill_type == "SAM_VIT_w_conv":
121
+ self.distill = timm.create_model("samvit_large_patch16.sa1b", pretrained=True)
122
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
123
+
124
+ elif distill_type == "depth_anything_VIT_L14":
125
+ self.distill = timm.create_model("vit_large_patch14_dinov2.lvd142m", img_size=224, pretrained=False)
126
+ state_dict = torch.load("./pretrained_models/depth_anything_vitl14.pth")
127
+ state_dict = {k.replace("pretrained.", "", 1): v for k, v in state_dict.items()}
128
+ state_dict["pos_embed"] = resample_abs_pos_embed(state_dict["pos_embed"], new_size=(16, 16))
129
+ self.distill.load_state_dict(state_dict, strict=False)
130
+ self.post_quant_conv_distill = conv1x1(q_e_dim, z_channels)
131
+
132
+
133
+ @staticmethod
134
+ def _compute_scaled_size(image_size, patch_size, ckpt_patch_size=14):
135
+ if isinstance(image_size, int):
136
+ return [image_size * ckpt_patch_size // patch_size] * 2
137
+ return [image_size[0] * ckpt_patch_size // patch_size, image_size[1] * ckpt_patch_size // patch_size]
138
+
139
+ def get_input(self, batch):
140
+ x = batch['images']
141
+ return x.float()
142
+
143
+ def entropy_loss_weight_scheduling(self):
144
+ self.loss.entropy_loss_weight = self.entropy_loss_weight_scheduler(self.global_step)
145
+
146
+ def entropy_loss_weight_scheduling(self):
147
+ self.loss.entropy_loss_weight = self.entropy_loss_weight_scheduler(self.global_step)
148
+
149
+ def encode(self, x):
150
+ h = self.encoder(x)
151
+ h = self.quant_conv(h)
152
+ if self.encoder_normalize_embedding:
153
+ h = F.normalize(h, p=2, dim=1)
154
+ ret = self.quantize(h)
155
+ ret["continuous"] = h
156
+ return ret
157
+
158
+ def decode(self, quant):
159
+ distill_conv_out = self.post_quant_conv_distill(quant)
160
+ quant2 = self.post_quant_conv(quant)
161
+ return self.decoder(quant2), distill_conv_out
162
+
163
+ def forward(self, input):
164
+ encoded = self.encode(input)
165
+ if torch.rand(1) > self.cont_ratio_trainig:
166
+ dec, distill_conv_out = self.decode(encoded["quantized"])
167
+ else:
168
+ dec, distill_conv_out = self.decode(encoded["continuous"])
169
+ return dec, (encoded['quantization_loss'], encoded['entropy_loss']), distill_conv_out
170
+
171
+ def forward_se(self, input):
172
+ random_scale = [random.choice(self.scale_equivariance[0]), random.choice(self.scale_equivariance[1])]
173
+ downscale_factor = [1/random_scale[0], 1/random_scale[1]]
174
+ encoded = self.encode(input)
175
+ if torch.rand(1) > self.cont_ratio_trainig:
176
+ dec, distill_conv_out = self.decode(encoded["quantized"])
177
+ quant_se = F.interpolate(encoded["quantized"], scale_factor=downscale_factor, mode='bilinear', align_corners=False)
178
+ dec_se = self.decode(quant_se)[0]
179
+ else:
180
+ dec, distill_conv_out = self.decode(encoded["continuous"])
181
+ latents_se = F.interpolate(encoded["continuous"], scale_factor=downscale_factor, mode='bilinear', align_corners=False)
182
+ dec_se = self.decode(latents_se)[0]
183
+
184
+ input_se = F.interpolate(input, scale_factor=downscale_factor, mode='bilinear', align_corners=False)
185
+ decs = [dec, dec_se]
186
+ inputs = [input, input_se]
187
+ return inputs, decs, (encoded['quantization_loss'], encoded['entropy_loss']), distill_conv_out
188
+
189
+ def distill_loss(self, distill_output, decoder_distill_output):
190
+ #print(f'DINO loss calculation')
191
+ if 'VIT' in self.distill_model_type:
192
+ if 'reg4' in self.distill_model_type:
193
+ distill_output = distill_output[:, 5:, :] # [CLS, Register*4, Embeddings]
194
+ elif 'reg4' not in self.distill_model_type and 'DINOv2' in self.distill_model_type:
195
+ distill_output = distill_output[:, 1:, :] # uncomment for DINOv1
196
+ elif 'reg4' not in self.distill_model_type and 'DINOv3' in self.distill_model_type:
197
+ distill_output = distill_output['x_norm_patchtokens'] #distill_output[:, 1:, :] # uncomment for DINOv1
198
+ elif 'depth_anything' in self.distill_model_type:
199
+ distill_output = distill_output[:, 1:, :]
200
+ elif self.distill_model_type == 'SAM_VIT':
201
+ distill_output = distill_output.permute(0, 2, 3, 1).contiguous().view(distill_output.shape[0], -1, distill_output.shape[1])
202
+ distill_output = F.normalize(distill_output, p=2, dim=2) # without post_conv layer
203
+ elif self.distill_model_type == 'SAM_VIT_w_conv':
204
+ distill_output = distill_output.permute(0, 2, 3, 1).contiguous().view(distill_output.shape[0], -1, distill_output.shape[1])
205
+ # without L2 normalization
206
+ distill_output = distill_output.permute(0, 2, 1).contiguous()
207
+
208
+ elif self.distill_model_type == 'CNN':
209
+ distill_output = distill_output.view(distill_output.shape[0], distill_output.shape[1], -1)
210
+ decoder_distill_output = decoder_distill_output.view(decoder_distill_output.shape[0], decoder_distill_output.shape[1], -1)
211
+ cos_similarity = F.cosine_similarity(decoder_distill_output, distill_output, dim=1)
212
+ cosine_loss = 1 - cos_similarity
213
+ distill_loss = cosine_loss.mean()
214
+ return distill_loss
215
+
216
+ def get_warmup_scheduler(self, optimizer, warmup_steps, min_lr_multiplier):
217
+ min_lr = self.learning_rate * min_lr_multiplier
218
+ total_steps = self.trainer.max_epochs * self.num_iters_per_epoch
219
+ def lr_lambda(step):
220
+ if step < warmup_steps:
221
+ # Linear warmup
222
+ return step/warmup_steps
223
+ # After warmup_steps, we just return 1. This could be modified to implement your own schedule
224
+ else:
225
+ progress = (step - warmup_steps) / (total_steps - warmup_steps)
226
+ cosine_decay = 0.5 * (1 + math.cos(math.pi * progress))
227
+ decayed = (1 - min_lr) * cosine_decay + min_lr
228
+ return decayed
229
+ #return 1.0
230
+
231
+ return LambdaLR(optimizer, lr_lambda)
232
+
233
+ def configure_optimizers(self):
234
+ lr = self.learning_rate
235
+ opt_ae = torch.optim.Adam(list(self.encoder.parameters())+
236
+ list(self.decoder.parameters())+
237
+ list(self.quantize.parameters())+
238
+ list(self.quant_conv.parameters())+
239
+ list(self.post_quant_conv.parameters())+
240
+ list(self.post_quant_conv_distill.parameters()),
241
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
242
+ opt_disc = torch.optim.Adam(self.loss.discriminator.parameters(),
243
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
244
+
245
+ scheduler_ae_warmup = self.get_warmup_scheduler(opt_ae, self.loss.warmup_steps, self.min_lr_multiplier)
246
+ scheduler_disc_warmup = self.get_warmup_scheduler(opt_disc, self.loss.warmup_steps, self.min_lr_multiplier)
247
+
248
+
249
+ return [opt_ae, opt_disc], [scheduler_ae_warmup, scheduler_disc_warmup]
250
+
251
+ def validation_step(self, batch, batch_idx):
252
+ x = self.get_input(batch)
253
+ xrec, qloss, decoder_distill_output = self(x)
254
+
255
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
256
+ aeloss, log_dict_ae = self.loss(qloss, distill_loss, x, xrec, 0, self.global_step,
257
+ last_layer=self.get_last_layer(), split="val")
258
+ self.log("val/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
259
+ self.log("val/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
260
+
261
+ _, log_dict_disc = self.loss(qloss, distill_loss, x, xrec, 1, self.global_step,
262
+ last_layer=self.get_last_layer(), split="val")
263
+ rec_loss = log_dict_ae["val/rec_loss"]
264
+ self.log("val/rec_loss", rec_loss,
265
+ prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
266
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
267
+ return self.log_dict
268
+
269
+ def training_step(self, batch, batch_idx):
270
+ self.entropy_loss_weight_scheduling()
271
+ self.log("train/enropy_loss_weight", self.loss.entropy_loss_weight,
272
+ prog_bar=True, logger=True, on_step=True, on_epoch=False)
273
+
274
+ opt_ae, opt_disc = self.optimizers()
275
+ [scheduler_ae_warmup, scheduler_disc_warmup] = self.lr_schedulers()
276
+
277
+ if self.only_decoder:
278
+ for param in self.encoder.parameters():
279
+ param.requires_grad = False
280
+ for param in self.quant_conv.parameters():
281
+ param.requires_grad = False
282
+ for param in self.quantize.parameters():
283
+ param.requires_grad = False
284
+ for param in self.post_quant_conv_distill.parameters():
285
+ param.requires_grad = False
286
+
287
+ x = self.get_input(batch)
288
+
289
+ if self.scale_equivariance:
290
+ xs, xrec, qloss, decoder_distill_output = self.forward_se(x)
291
+ else:
292
+ xrec, qloss, decoder_distill_output = self(x)
293
+ xs = x
294
+
295
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
296
+
297
+ optimizer_idx = 1
298
+ discloss, log_dict_disc = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
299
+ last_layer=self.get_last_layer(), split="train")
300
+ self.log("train/discloss", discloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
301
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
302
+
303
+ discloss = discloss / self.grad_acc_steps
304
+ self.manual_backward(discloss)
305
+ if (batch_idx+1) % self.grad_acc_steps == 0:
306
+ opt_disc.step()
307
+ opt_disc.zero_grad()
308
+ scheduler_disc_warmup.step()
309
+
310
+ optimizer_idx = 0
311
+ aeloss, log_dict_ae = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
312
+ last_layer=self.get_last_layer(), split="train")
313
+ self.log("train/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
314
+ self.log("train/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
315
+ self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True)
316
+
317
+ aeloss = aeloss / self.grad_acc_steps
318
+ self.manual_backward(aeloss)
319
+ if (batch_idx+1) % self.grad_acc_steps == 0:
320
+ opt_ae.step()
321
+ opt_ae.zero_grad()
322
+ scheduler_ae_warmup.step()
323
+
324
+
325
+ def get_distill_gt(self, x):
326
+ with torch.no_grad():
327
+ if 'VIT' in self.distill_model_type:
328
+ # resize image x to 224x224
329
+ if 'VIT_DINOv2' in self.distill_model_type or 'depth_anything' in self.distill_model_type:
330
+ image_size = (self.image_size*14//self.patch_size, self.image_size*14//self.patch_size) if isinstance(self.image_size, int) else (self.image_size[0]*14//self.patch_size, self.image_size[1]*14//self.patch_size)
331
+ x_224 = F.interpolate(x, size=image_size, mode='bilinear', align_corners=False)
332
+ distill_output = self.distill.forward_features(x_224)
333
+ elif 'VIT_DINOv3' in self.distill_model_type:
334
+ image_size = self._compute_scaled_size(self.image_size, self.patch_size, ckpt_patch_size=16)
335
+ x_224 = F.interpolate(x, size=image_size, mode='bilinear', align_corners=False)
336
+ distill_output = self.distill.forward_features(x_224)
337
+ else: # for VIT-DINOv1, VIT-SAM models
338
+ distill_output = self.distill.forward_features(x)
339
+
340
+ elif self.distill_model_type == 'CNN':
341
+ distill_output = self.distill(x)
342
+ return distill_output
343
+
344
+
345
+ def get_last_layer(self):
346
+ try:
347
+ return self.decoder.conv_out.weight
348
+ except:
349
+ return None
350
+
351
+ def log_images(self, batch, **kwargs):
352
+ log = dict()
353
+ x = self.get_input(batch)
354
+ x = x.to(self.device)
355
+ xrec, _, _ = self(x)
356
+ log["inputs"] = x
357
+ log["reconstructions"] = xrec
358
+ return log
359
+
360
+
361
+ class VQModelIF(VQModel):
362
+ def __init__(self,
363
+ encoder_config,
364
+ decoder_config,
365
+ quantizer_config,
366
+ loss_config=None,
367
+ grad_acc_steps=1,
368
+ cont_ratio_trainig= 0.0,
369
+ ignore_keys=[],
370
+ monitor=None,
371
+ entropy_loss_weight_scheduler_config=None,
372
+ distill_model_type='VIT_DINOv2', # 'VIT_DINO' or 'CNN' or VIT_DINOv2, VIT_DINOv2_large_reg4, SAM_VIT
373
+ min_lr_multiplier=0.1,
374
+ only_decoder=False,
375
+ scale_equivariance=[]
376
+ ):
377
+ super().__init__(encoder_config, decoder_config, quantizer_config, loss_config,
378
+ grad_acc_steps, cont_ratio_trainig, ignore_keys,
379
+ monitor,
380
+ entropy_loss_weight_scheduler_config,
381
+ distill_model_type, min_lr_multiplier, only_decoder, scale_equivariance)
382
+
383
+ self.encoder2 = instantiate_from_config(encoder_config)
384
+ self.post_quant_conv = torch.nn.Conv2d(quantizer_config.params['e_dim']*2, decoder_config.params["z_channels"], 1)
385
+ self.quant_conv2 = torch.nn.Conv2d(encoder_config.params["z_channels"], quantizer_config.params['e_dim'], 1)
386
+ self.quantize2 = instantiate_from_config(quantizer_config)
387
+
388
+ def configure_optimizers(self):
389
+ lr = self.learning_rate
390
+ opt_ae = torch.optim.Adam(list(self.encoder.parameters())+
391
+ list(self.encoder2.parameters())+
392
+ list(self.decoder.parameters())+
393
+ list(self.quantize.parameters())+
394
+ list(self.quantize2.parameters())+
395
+ list(self.quant_conv.parameters())+
396
+ list(self.quant_conv2.parameters())+
397
+ list(self.post_quant_conv.parameters())+
398
+ list(self.post_quant_conv_distill.parameters()),
399
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
400
+ opt_disc = torch.optim.Adam(self.loss.discriminator.parameters(),
401
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
402
+
403
+ scheduler_ae_warmup = self.get_warmup_scheduler(opt_ae, self.loss.warmup_steps, self.min_lr_multiplier)
404
+ scheduler_disc_warmup = self.get_warmup_scheduler(opt_disc, self.loss.warmup_steps, self.min_lr_multiplier)
405
+
406
+
407
+ return [opt_ae, opt_disc], [scheduler_ae_warmup, scheduler_disc_warmup]
408
+
409
+ def encode(self, x):
410
+ h = self.encoder(x)
411
+ h = self.quant_conv(h)
412
+ if self.encoder_normalize_embedding:
413
+ h = F.normalize(h, p=2, dim=1)
414
+
415
+ h2 = self.encoder2(x)
416
+ h2 = self.quant_conv2(h2)
417
+ if self.encoder_normalize_embedding:
418
+ h2 = F.normalize(h2, p=2, dim=1)
419
+
420
+ quant = self.quantize(h)
421
+ quant2 = self.quantize2(h2)
422
+
423
+ quant_loss = quant['quantization_loss'] + quant2['quantization_loss']
424
+ entropy_loss = quant['entropy_loss'] + quant2['entropy_loss'] if quant['entropy_loss'] is not None and quant2['entropy_loss'] is not None else None
425
+
426
+ ret = {
427
+ "quantized": (quant["quantized"], quant2["quantized"]),
428
+ "quantization_loss": quant_loss,
429
+ "entropy_loss": entropy_loss,
430
+ "indices": (quant["indices"], quant2["indices"]),
431
+ "continuous": (h, h2)
432
+ }
433
+ return ret
434
+
435
+
436
+ def decode(self, quant):
437
+ if isinstance(quant, tuple):
438
+ quant_rec = quant[0]
439
+ quant_sem = quant[1]
440
+ else:
441
+ print('Error: quant should be a tuple')
442
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(quant_sem.shape[0], -1, quant_sem.shape[2]*quant_sem.shape[3])
443
+ quant_cat = torch.cat((quant_rec, quant_sem), dim=1)
444
+ quant = self.post_quant_conv(quant_cat)
445
+ return self.decoder(quant), distill_conv_out
446
+
447
+ def decode_code(self, code_b):
448
+ code_b_rec, code_b_sem = code_b
449
+ quant_b_rec = self.quantize.get_codebook_entry(code_b_rec, (-1, code_b_rec.size(1), code_b_rec.size(2), self.quantize.e_dim))
450
+ quant_b_sem = self.quantize2.get_codebook_entry(code_b_sem, (-1, code_b_sem.size(1), code_b_sem.size(2), self.quantize.e_dim))
451
+ quant_b = (quant_b_rec, quant_b_sem)
452
+ dec = self.decode(quant_b)
453
+ return dec
454
+
455
+ def forward_se(self, input):
456
+ random_scale = [random.choice(self.scale_equivariance[0]), random.choice(self.scale_equivariance[1])]
457
+ downscale_factor = [1/random_scale[0], 1/random_scale[1]]
458
+ encoded = self.encode(input)
459
+ quantized = encoded["quantized"]
460
+ continuous = encoded["continuous"]
461
+ if torch.rand(1) > self.cont_ratio_trainig:
462
+ dec, distill_conv_out = self.decode(quantized)
463
+ quant_se = F.interpolate(quantized[0], scale_factor=downscale_factor, mode='bilinear', align_corners=False), \
464
+ F.interpolate(quantized[1], scale_factor=downscale_factor, mode='bilinear', align_corners=False)
465
+ dec_se = self.decode(quant_se)[0]
466
+ else:
467
+ dec, distill_conv_out = self.decode(continuous)
468
+ latents_se = F.interpolate(continuous[0], scale_factor=downscale_factor, mode='bilinear', align_corners=False), \
469
+ F.interpolate(continuous[1], scale_factor=downscale_factor, mode='bilinear', align_corners=False)
470
+ dec_se = self.decode(latents_se)[0]
471
+
472
+ input_se = F.interpolate(input, scale_factor=downscale_factor, mode='bilinear', align_corners=False)
473
+ decs = [dec, dec_se]
474
+ inputs = [input, input_se]
475
+ return inputs, decs, (encoded["quantization_loss"], encoded["entropy_loss"]), distill_conv_out
476
+
477
+ def training_step(self, batch, batch_idx):
478
+ self.entropy_loss_weight_scheduling()
479
+ self.log("train/enropy_loss_weight", self.loss.entropy_loss_weight,
480
+ prog_bar=True, logger=True, on_step=True, on_epoch=False)
481
+
482
+ opt_ae, opt_disc = self.optimizers()
483
+ [scheduler_ae_warmup, scheduler_disc_warmup] = self.lr_schedulers()
484
+
485
+ if self.only_decoder:
486
+ for param in self.encoder.parameters():
487
+ param.requires_grad = False
488
+ for param in self.encoder2.parameters():
489
+ param.requires_grad = False
490
+ for param in self.quant_conv.parameters():
491
+ param.requires_grad = False
492
+ for param in self.quant_conv2.parameters():
493
+ param.requires_grad = False
494
+ for param in self.quantize.parameters():
495
+ param.requires_grad = False
496
+ for param in self.quantize2.parameters():
497
+ param.requires_grad = False
498
+ for param in self.post_quant_conv_distill.parameters():
499
+ param.requires_grad = False
500
+
501
+ x = self.get_input(batch)
502
+
503
+ if self.scale_equivariance:
504
+ xs, xrec, qloss, decoder_distill_output = self.forward_se(x)
505
+ else:
506
+ xrec, qloss, decoder_distill_output = self(x)
507
+ xs = x
508
+
509
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
510
+
511
+ optimizer_idx = 1
512
+ discloss, log_dict_disc = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
513
+ last_layer=self.get_last_layer(), split="train")
514
+ self.log("train/discloss", discloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
515
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
516
+
517
+ discloss = discloss / self.grad_acc_steps
518
+ self.manual_backward(discloss)
519
+ if (batch_idx+1) % self.grad_acc_steps == 0:
520
+ opt_disc.step()
521
+ opt_disc.zero_grad()
522
+ scheduler_disc_warmup.step()
523
+
524
+ optimizer_idx = 0
525
+ aeloss, log_dict_ae = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
526
+ last_layer=self.get_last_layer(), split="train")
527
+ self.log("train/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
528
+ self.log("train/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
529
+ self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True)
530
+
531
+ aeloss = aeloss / self.grad_acc_steps
532
+ self.manual_backward(aeloss)
533
+ if (batch_idx+1) % self.grad_acc_steps == 0:
534
+ opt_ae.step()
535
+ opt_ae.zero_grad()
536
+ scheduler_ae_warmup.step()
537
+
538
+ class VQModelIFExtra(VQModelIF):
539
+ def __init__(self,
540
+ encoder_config,
541
+ decoder_config,
542
+ quantizer_config,
543
+ quantizer_config2,
544
+ loss_config=None,
545
+ grad_acc_steps=1,
546
+ cont_ratio_trainig= 0.0,
547
+ ignore_keys=[],
548
+ monitor=None,
549
+ entropy_loss_weight_scheduler_config=None,
550
+ distill_model_type='VIT_DINOv2', # 'VIT_DINO' or 'CNN' or VIT_DINOv2, VIT_DINOv2_large_reg4, SAM_VIT
551
+ min_lr_multiplier=0.1,
552
+ only_decoder=False,
553
+ scale_equivariance=[]
554
+ ):
555
+ super().__init__(encoder_config, decoder_config, quantizer_config, loss_config,
556
+ grad_acc_steps, cont_ratio_trainig, ignore_keys,
557
+ monitor,
558
+ entropy_loss_weight_scheduler_config,
559
+ distill_model_type, min_lr_multiplier, only_decoder, scale_equivariance)
560
+
561
+ self.encoder2 = instantiate_from_config(encoder_config)
562
+ self.post_quant_conv = torch.nn.Conv2d(quantizer_config.params['e_dim']+quantizer_config2.params['e_dim'], decoder_config.params["z_channels"], 1)
563
+ self.quant_conv2 = nn.Conv2d(encoder_config.params["z_channels"], quantizer_config2.params['e_dim'], 1)
564
+ self.quantize2 = instantiate_from_config(quantizer_config2)
565
+
566
+ self._init_distill_model(distill_model_type, encoder_config, decoder_config, quantizer_config2)
567
+
568
+ class VQModelIFSepEnc(VQModelIF):
569
+ def __init__(self,
570
+ encoder_config,
571
+ encoder_config2,
572
+ decoder_config,
573
+ quantizer_config,
574
+ loss_config=None,
575
+ grad_acc_steps=1,
576
+ cont_ratio_trainig= 0.0,
577
+ ignore_keys=[],
578
+ monitor=None,
579
+ entropy_loss_weight_scheduler_config=None,
580
+ distill_model_type='VIT_DINOv2', # 'VIT_DINO' or 'CNN' or VIT_DINOv2, VIT_DINOv2_large_reg4, SAM_VIT
581
+ min_lr_multiplier=0.1,
582
+ only_decoder=False,
583
+ scale_equivariance=[]
584
+ ):
585
+ super().__init__(encoder_config, decoder_config, quantizer_config, loss_config,
586
+ grad_acc_steps, cont_ratio_trainig, ignore_keys,
587
+ monitor,
588
+ entropy_loss_weight_scheduler_config,
589
+ distill_model_type, min_lr_multiplier, only_decoder, scale_equivariance)
590
+
591
+ self.encoder2 = instantiate_from_config(encoder_config2)
592
+ self.post_quant_conv = torch.nn.Conv2d(quantizer_config.params['e_dim'], decoder_config.params["z_channels"], 1)
593
+ self.quant_conv2 = nn.Conv2d(encoder_config2.params["z_channels"], quantizer_config.params['e_dim'], 1)
594
+ self.quantize2 = instantiate_from_config(quantizer_config)
595
+
596
+ self._init_distill_model(distill_model_type, encoder_config, decoder_config, quantizer_config)
597
+
598
+ def training_step(self, batch, batch_idx):
599
+ self.entropy_loss_weight_scheduling()
600
+ self.log("train/enropy_loss_weight", self.loss.entropy_loss_weight,
601
+ prog_bar=True, logger=True, on_step=True, on_epoch=False)
602
+
603
+ opt_ae, opt_disc = self.optimizers()
604
+ [scheduler_ae_warmup, scheduler_disc_warmup] = self.lr_schedulers()
605
+
606
+ for param in self.encoder2.parameters():
607
+ param.requires_grad = False
608
+
609
+ x = self.get_input(batch)
610
+
611
+ if self.scale_equivariance:
612
+ xs, xrec, qloss, decoder_distill_output = self.forward_se(x)
613
+ else:
614
+ xrec, qloss, decoder_distill_output = self(x)
615
+ xs = x
616
+
617
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
618
+
619
+ optimizer_idx = 1
620
+ discloss, log_dict_disc = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
621
+ last_layer=self.get_last_layer(), split="train")
622
+ self.log("train/discloss", discloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
623
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
624
+
625
+ discloss = discloss / self.grad_acc_steps
626
+ self.manual_backward(discloss)
627
+ if (batch_idx+1) % self.grad_acc_steps == 0:
628
+ opt_disc.step()
629
+ opt_disc.zero_grad()
630
+ scheduler_disc_warmup.step()
631
+
632
+ optimizer_idx = 0
633
+ aeloss, log_dict_ae = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
634
+ last_layer=self.get_last_layer(), split="train")
635
+ self.log("train/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
636
+ self.log("train/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
637
+ self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True)
638
+
639
+ aeloss = aeloss / self.grad_acc_steps
640
+ self.manual_backward(aeloss)
641
+ if (batch_idx+1) % self.grad_acc_steps == 0:
642
+ opt_ae.step()
643
+ opt_ae.zero_grad()
644
+ scheduler_ae_warmup.step()
645
+
646
+ def configure_optimizers(self):
647
+ lr = self.learning_rate
648
+ opt_ae = torch.optim.Adam(list(self.encoder.parameters())+
649
+ list(self.decoder.parameters())+
650
+ list(self.quantize.parameters())+
651
+ list(self.quantize2.parameters())+
652
+ list(self.quant_conv.parameters())+
653
+ list(self.quant_conv2.parameters())+
654
+ list(self.post_quant_conv.parameters())+
655
+ list(self.post_quant_conv_distill.parameters()),
656
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
657
+ opt_disc = torch.optim.Adam(self.loss.discriminator.parameters(),
658
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
659
+
660
+ scheduler_ae_warmup = self.get_warmup_scheduler(opt_ae, self.loss.warmup_steps, self.min_lr_multiplier)
661
+ scheduler_disc_warmup = self.get_warmup_scheduler(opt_disc, self.loss.warmup_steps, self.min_lr_multiplier)
662
+
663
+
664
+ return [opt_ae, opt_disc], [scheduler_ae_warmup, scheduler_disc_warmup]
665
+
666
+ def decode(self, quant):
667
+ if isinstance(quant, tuple):
668
+ quant_rec = quant[0]
669
+ quant_sem = quant[1]
670
+ else:
671
+ print('Error: quant should be a tuple')
672
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(quant_sem.shape[0], -1, quant_sem.shape[2]*quant_sem.shape[3])
673
+ #quant_cat = torch.cat((quant_rec, quant_sem), dim=1)
674
+ quant = self.post_quant_conv(quant_rec)
675
+ return self.decoder(quant), distill_conv_out
676
+
677
+
678
+ class VQModelIFExtraSepEnc(VQModelIF):
679
+ def __init__(self,
680
+ encoder_config,
681
+ encoder_config2,
682
+ decoder_config,
683
+ quantizer_config,
684
+ quantizer_config2,
685
+ loss_config=None,
686
+ grad_acc_steps=1,
687
+ cont_ratio_trainig= 0.0,
688
+ ignore_keys=[],
689
+ monitor=None,
690
+ entropy_loss_weight_scheduler_config=None,
691
+ distill_model_type='VIT_DINOv2', # 'VIT_DINO' or 'CNN' or VIT_DINOv2, VIT_DINOv2_large_reg4, SAM_VIT
692
+ min_lr_multiplier=0.1,
693
+ only_decoder=False,
694
+ scale_equivariance=[]
695
+ ):
696
+ super().__init__(encoder_config, decoder_config, quantizer_config, loss_config,
697
+ grad_acc_steps, cont_ratio_trainig, ignore_keys,
698
+ monitor,
699
+ entropy_loss_weight_scheduler_config,
700
+ distill_model_type, min_lr_multiplier, only_decoder, scale_equivariance)
701
+
702
+ self.encoder2 = instantiate_from_config(encoder_config2)
703
+ self.post_quant_conv = torch.nn.Conv2d(quantizer_config.params['e_dim']+quantizer_config2.params['e_dim'], decoder_config.params["z_channels"], 1)
704
+ self.quant_conv2 = nn.Conv2d(encoder_config2.params["z_channels"], quantizer_config2.params['e_dim'], 1)
705
+ self.quantize2 = instantiate_from_config(quantizer_config2)
706
+
707
+ self._init_distill_model(distill_model_type, encoder_config2, decoder_config, quantizer_config2)
708
+
709
+ def training_step(self, batch, batch_idx):
710
+ self.entropy_loss_weight_scheduling()
711
+ self.log("train/enropy_loss_weight", self.loss.entropy_loss_weight,
712
+ prog_bar=True, logger=True, on_step=True, on_epoch=False)
713
+
714
+ opt_ae, opt_disc = self.optimizers()
715
+ [scheduler_ae_warmup, scheduler_disc_warmup] = self.lr_schedulers()
716
+
717
+ for param in self.encoder2.parameters():
718
+ param.requires_grad = False
719
+
720
+ x = self.get_input(batch)
721
+
722
+ if self.scale_equivariance:
723
+ xs, xrec, qloss, decoder_distill_output = self.forward_se(x)
724
+ else:
725
+ xrec, qloss, decoder_distill_output = self(x)
726
+ xs = x
727
+
728
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
729
+
730
+ optimizer_idx = 1
731
+ discloss, log_dict_disc = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
732
+ last_layer=self.get_last_layer(), split="train")
733
+ self.log("train/discloss", discloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
734
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
735
+
736
+ discloss = discloss / self.grad_acc_steps
737
+ self.manual_backward(discloss)
738
+ if (batch_idx+1) % self.grad_acc_steps == 0:
739
+ opt_disc.step()
740
+ opt_disc.zero_grad()
741
+ scheduler_disc_warmup.step()
742
+
743
+ optimizer_idx = 0
744
+ aeloss, log_dict_ae = self.loss(qloss, distill_loss, xs, xrec, optimizer_idx, self.global_step,
745
+ last_layer=self.get_last_layer(), split="train")
746
+ self.log("train/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
747
+ self.log("train/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
748
+ self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True)
749
+
750
+ aeloss = aeloss / self.grad_acc_steps
751
+ self.manual_backward(aeloss)
752
+ if (batch_idx+1) % self.grad_acc_steps == 0:
753
+ opt_ae.step()
754
+ opt_ae.zero_grad()
755
+ scheduler_ae_warmup.step()
756
+
757
+ def configure_optimizers(self):
758
+ lr = self.learning_rate
759
+ opt_ae = torch.optim.Adam(list(self.encoder.parameters())+
760
+ list(self.decoder.parameters())+
761
+ list(self.quantize.parameters())+
762
+ list(self.quantize2.parameters())+
763
+ list(self.quant_conv.parameters())+
764
+ list(self.quant_conv2.parameters())+
765
+ list(self.post_quant_conv.parameters())+
766
+ list(self.post_quant_conv_distill.parameters()),
767
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
768
+ opt_disc = torch.optim.Adam(self.loss.discriminator.parameters(),
769
+ lr=lr, betas=(self.loss.beta_1, self.loss.beta_2))
770
+
771
+ scheduler_ae_warmup = self.get_warmup_scheduler(opt_ae, self.loss.warmup_steps, self.min_lr_multiplier)
772
+ scheduler_disc_warmup = self.get_warmup_scheduler(opt_disc, self.loss.warmup_steps, self.min_lr_multiplier)
773
+
774
+
775
+ return [opt_ae, opt_disc], [scheduler_ae_warmup, scheduler_disc_warmup]
776
+
777
+ def decode(self, quant):
778
+ if isinstance(quant, tuple):
779
+ quant_rec = quant[0]
780
+ quant_sem = quant[1]
781
+ else:
782
+ print('Error: quant should be a tuple')
783
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(quant_sem.shape[0], -1, quant_sem.shape[2]*quant_sem.shape[3])
784
+ quant_cat = torch.cat((quant_rec, quant_sem), dim=1)
785
+ quant = self.post_quant_conv(quant_cat)
786
+ return self.decoder(quant), distill_conv_out
787
+
788
+
789
+ class VQModelIFExtraSepEncSepDec(VQModelIFExtraSepEnc):
790
+ """
791
+ IFExtraSepEnc variant with a dedicated semantic decoder.
792
+
793
+ The main reconstruction decoder sees only the reconstruction branch. The
794
+ semantic branch is decoded by a second decoder with the same architecture,
795
+ while the semantic latents are detached on that auxiliary reconstruction
796
+ path so decoder gradients do not flow back into the semantic
797
+ encoder/quantizer. The semantic decoder is trained with reconstruction /
798
+ perceptual losses only and does not participate in the adversarial path.
799
+ """
800
+
801
+ def __init__(self,
802
+ encoder_config,
803
+ encoder_config2,
804
+ decoder_config,
805
+ quantizer_config,
806
+ quantizer_config2,
807
+ loss_config=None,
808
+ grad_acc_steps=1,
809
+ cont_ratio_trainig=0.0,
810
+ ignore_keys=[],
811
+ monitor=None,
812
+ entropy_loss_weight_scheduler_config=None,
813
+ distill_model_type='VIT_DINOv2',
814
+ min_lr_multiplier=0.1,
815
+ only_decoder=False,
816
+ scale_equivariance=[]
817
+ ):
818
+ super().__init__(
819
+ encoder_config=encoder_config,
820
+ encoder_config2=encoder_config2,
821
+ decoder_config=decoder_config,
822
+ quantizer_config=quantizer_config,
823
+ quantizer_config2=quantizer_config2,
824
+ loss_config=loss_config,
825
+ grad_acc_steps=grad_acc_steps,
826
+ cont_ratio_trainig=cont_ratio_trainig,
827
+ ignore_keys=ignore_keys,
828
+ monitor=monitor,
829
+ entropy_loss_weight_scheduler_config=entropy_loss_weight_scheduler_config,
830
+ distill_model_type=distill_model_type,
831
+ min_lr_multiplier=min_lr_multiplier,
832
+ only_decoder=only_decoder,
833
+ scale_equivariance=scale_equivariance,
834
+ )
835
+
836
+ self.post_quant_conv = torch.nn.Conv2d(
837
+ quantizer_config.params['e_dim'],
838
+ decoder_config.params["z_channels"],
839
+ 1,
840
+ )
841
+ self.post_quant_conv_sem = torch.nn.Conv2d(
842
+ quantizer_config2.params['e_dim'],
843
+ decoder_config.params["z_channels"],
844
+ 1,
845
+ )
846
+ self.decoder_sem = instantiate_from_config(decoder_config)
847
+
848
+ def configure_optimizers(self):
849
+ lr = self.learning_rate
850
+ opt_ae = torch.optim.Adam(
851
+ list(self.encoder.parameters())
852
+ + list(self.encoder2.parameters())
853
+ + list(self.decoder.parameters())
854
+ + list(self.decoder_sem.parameters())
855
+ + list(self.quantize.parameters())
856
+ + list(self.quantize2.parameters())
857
+ + list(self.quant_conv.parameters())
858
+ + list(self.quant_conv2.parameters())
859
+ + list(self.post_quant_conv.parameters())
860
+ + list(self.post_quant_conv_sem.parameters())
861
+ + list(self.post_quant_conv_distill.parameters()),
862
+ lr=lr,
863
+ betas=(self.loss.beta_1, self.loss.beta_2),
864
+ )
865
+ opt_disc = torch.optim.Adam(
866
+ self.loss.discriminator.parameters(),
867
+ lr=lr,
868
+ betas=(self.loss.beta_1, self.loss.beta_2),
869
+ )
870
+
871
+ scheduler_ae_warmup = self.get_warmup_scheduler(opt_ae, self.loss.warmup_steps, self.min_lr_multiplier)
872
+ scheduler_disc_warmup = self.get_warmup_scheduler(opt_disc, self.loss.warmup_steps, self.min_lr_multiplier)
873
+
874
+ return [opt_ae, opt_disc], [scheduler_ae_warmup, scheduler_disc_warmup]
875
+
876
+ def _decode_components(self, quant):
877
+ if isinstance(quant, tuple):
878
+ quant_rec = quant[0]
879
+ quant_sem = quant[1]
880
+ else:
881
+ raise ValueError('VQModelIFExtraSepEncSepDec expects quant to be a tuple')
882
+
883
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(
884
+ quant_sem.shape[0], -1, quant_sem.shape[2] * quant_sem.shape[3]
885
+ )
886
+
887
+ rec = self.decoder(self.post_quant_conv(quant_rec))
888
+
889
+ # Stop semantic-decoder reconstruction gradients from reaching encoder2/quantize2.
890
+ sem_latent = quant_sem.detach()
891
+ sem = self.decoder_sem(self.post_quant_conv_sem(sem_latent))
892
+
893
+ return rec, sem, distill_conv_out
894
+
895
+ def _select_decode_input(self, encoded):
896
+ if torch.rand(1, device=self.device).item() > self.cont_ratio_trainig:
897
+ return encoded["quantized"]
898
+ return encoded["continuous"]
899
+
900
+ def _forward_branch_outputs(self, x):
901
+ encoded = self.encode(x)
902
+ quant = self._select_decode_input(encoded)
903
+ rec, sem, distill_conv_out = self._decode_components(quant)
904
+ qloss = (encoded["quantization_loss"], encoded["entropy_loss"])
905
+ return rec, sem, qloss, distill_conv_out
906
+
907
+ def _semantic_decoder_loss(self, inputs, reconstructions, split):
908
+ if isinstance(reconstructions, list):
909
+ device = reconstructions[0].device
910
+ else:
911
+ device = reconstructions.device
912
+
913
+ rec_loss = torch.tensor(0.0, device=device)
914
+ p_loss = torch.tensor(0.0, device=device)
915
+
916
+ if isinstance(inputs, list):
917
+ for idx, (input_img, recon_img) in enumerate(zip(inputs, reconstructions)):
918
+ se_weight = 1 if idx == 0 else self.loss.se_weight
919
+ l1 = torch.abs(input_img - recon_img).mean()
920
+ l2 = F.mse_loss(recon_img, input_img)
921
+ rec_loss += (self.loss.l1_loss_weight * l1 + self.loss.l2_loss_weight * l2) * se_weight
922
+ if self.loss.perceptual_weight > 0:
923
+ p_loss += self.loss.perceptual_loss(input_img, recon_img).mean() * se_weight
924
+ else:
925
+ l1 = torch.abs(inputs - reconstructions).mean()
926
+ l2 = F.mse_loss(reconstructions, inputs)
927
+ rec_loss = self.loss.l1_loss_weight * l1 + self.loss.l2_loss_weight * l2
928
+ if self.loss.perceptual_weight > 0:
929
+ p_loss = self.loss.perceptual_loss(inputs, reconstructions).mean()
930
+
931
+ loss = rec_loss + self.loss.perceptual_weight * p_loss
932
+ log = {
933
+ f"{split}/sem_decoder_loss": loss.detach().mean(),
934
+ f"{split}/sem_decoder_rec_loss": rec_loss.detach().mean(),
935
+ f"{split}/sem_decoder_p_loss": p_loss.detach().mean(),
936
+ }
937
+ return loss, log
938
+
939
+ def decode(self, quant):
940
+ rec, _, distill_conv_out = self._decode_components(quant)
941
+ return rec, distill_conv_out
942
+
943
+ def validation_step(self, batch, batch_idx):
944
+ x = self.get_input(batch)
945
+
946
+ if self.scale_equivariance:
947
+ xs, xrec_rec, xrec_sem, qloss, decoder_distill_output = self._forward_se_branch_outputs(x)
948
+ else:
949
+ xrec_rec, xrec_sem, qloss, decoder_distill_output = self._forward_branch_outputs(x)
950
+ xs = x
951
+
952
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
953
+
954
+ rec_aeloss, log_dict_ae = self.loss(
955
+ qloss,
956
+ distill_loss,
957
+ xs,
958
+ xrec_rec,
959
+ 0,
960
+ self.global_step,
961
+ last_layer=self.get_last_layer(),
962
+ split="val",
963
+ )
964
+ _, log_dict_disc = self.loss(
965
+ qloss,
966
+ distill_loss,
967
+ xs,
968
+ xrec_rec,
969
+ 1,
970
+ self.global_step,
971
+ last_layer=self.get_last_layer(),
972
+ split="val",
973
+ )
974
+ sem_aeloss, log_dict_sem = self._semantic_decoder_loss(xs, xrec_sem, split="val")
975
+ aeloss = rec_aeloss + sem_aeloss
976
+
977
+ self.log("val/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
978
+ self.log("val/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
979
+ self.log("val/aeloss_rec", rec_aeloss, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
980
+ self.log("val/aeloss_sem", sem_aeloss, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
981
+ self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
982
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
983
+ self.log_dict(log_dict_sem, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
984
+ return self.log_dict
985
+
986
+ def training_step(self, batch, batch_idx):
987
+ self.entropy_loss_weight_scheduling()
988
+ self.log("train/enropy_loss_weight", self.loss.entropy_loss_weight,
989
+ prog_bar=True, logger=True, on_step=True, on_epoch=False)
990
+
991
+ opt_ae, opt_disc = self.optimizers()
992
+ [scheduler_ae_warmup, scheduler_disc_warmup] = self.lr_schedulers()
993
+
994
+ if self.only_decoder:
995
+ for param in self.encoder.parameters():
996
+ param.requires_grad = False
997
+ for param in self.encoder2.parameters():
998
+ param.requires_grad = False
999
+ for param in self.quant_conv.parameters():
1000
+ param.requires_grad = False
1001
+ for param in self.quant_conv2.parameters():
1002
+ param.requires_grad = False
1003
+ for param in self.quantize.parameters():
1004
+ param.requires_grad = False
1005
+ for param in self.quantize2.parameters():
1006
+ param.requires_grad = False
1007
+ for param in self.post_quant_conv_distill.parameters():
1008
+ param.requires_grad = False
1009
+
1010
+ x = self.get_input(batch)
1011
+
1012
+ if self.scale_equivariance:
1013
+ xs, xrec_rec, xrec_sem, qloss, decoder_distill_output = self._forward_se_branch_outputs(x)
1014
+ else:
1015
+ xrec_rec, xrec_sem, qloss, decoder_distill_output = self._forward_branch_outputs(x)
1016
+ xs = x
1017
+
1018
+ distill_loss = self.distill_loss(self.get_distill_gt(x), decoder_distill_output) if self.if_distill_loss else torch.tensor(0.0, device=x.device)
1019
+
1020
+ discloss, log_dict_disc = self.loss(
1021
+ qloss,
1022
+ distill_loss,
1023
+ xs,
1024
+ xrec_rec,
1025
+ 1,
1026
+ self.global_step,
1027
+ last_layer=self.get_last_layer(),
1028
+ split="train",
1029
+ )
1030
+ self.log("train/discloss", discloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1031
+ self.log_dict(log_dict_disc, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1032
+
1033
+ discloss = discloss / self.grad_acc_steps
1034
+ self.manual_backward(discloss)
1035
+ if (batch_idx + 1) % self.grad_acc_steps == 0:
1036
+ opt_disc.step()
1037
+ opt_disc.zero_grad()
1038
+ scheduler_disc_warmup.step()
1039
+
1040
+ rec_aeloss, log_dict_ae = self.loss(
1041
+ qloss,
1042
+ distill_loss,
1043
+ xs,
1044
+ xrec_rec,
1045
+ 0,
1046
+ self.global_step,
1047
+ last_layer=self.get_last_layer(),
1048
+ split="train",
1049
+ )
1050
+ sem_aeloss, log_dict_sem = self._semantic_decoder_loss(xs, xrec_sem, split="train")
1051
+ aeloss = rec_aeloss + sem_aeloss
1052
+
1053
+ self.log("train/distill_loss", distill_loss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1054
+ self.log("train/aeloss", aeloss, prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1055
+ self.log("train/aeloss_rec", rec_aeloss, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1056
+ self.log("train/aeloss_sem", sem_aeloss, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1057
+ self.log_dict(log_dict_ae, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1058
+ self.log_dict(log_dict_sem, prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True)
1059
+
1060
+ aeloss = aeloss / self.grad_acc_steps
1061
+ self.manual_backward(aeloss)
1062
+ if (batch_idx + 1) % self.grad_acc_steps == 0:
1063
+ opt_ae.step()
1064
+ opt_ae.zero_grad()
1065
+ scheduler_ae_warmup.step()
1066
+
1067
+ def log_images(self, batch, **kwargs):
1068
+ log = dict()
1069
+ x = self.get_input(batch)
1070
+ x = x.to(self.device)
1071
+ rec, sem, _, _ = self._forward_branch_outputs(x)
1072
+ log["inputs"] = x
1073
+ log["reconstructions"] = rec
1074
+ log["reconstructions_rec"] = rec
1075
+ log["reconstructions_sem"] = sem
1076
+ return log
1077
+
1078
+
1079
+ class VQModelIFExtraSepEncSepDecJointSem(VQModelIFExtraSepEncSepDec):
1080
+ """
1081
+ Variant of VQModelIFExtraSepEncSepDec where the semantic decoder is trained
1082
+ jointly with the semantic encoder (encoder2 / quantize2).
1083
+
1084
+ Unlike the base class, quant_sem is NOT detached before being passed to
1085
+ decoder_sem, so reconstruction gradients flow back into the semantic encoder
1086
+ and quantizer. The semantic decoder loss is weighted by
1087
+ ``sem_dec_loss_weight`` (default 0.1) to avoid dominating the primary
1088
+ reconstruction objective.
1089
+ """
1090
+
1091
+ def __init__(self, sem_dec_loss_weight=0.1, **kwargs):
1092
+ super().__init__(**kwargs)
1093
+ self.sem_dec_loss_weight = sem_dec_loss_weight
1094
+
1095
+ def _decode_components(self, quant):
1096
+ if isinstance(quant, tuple):
1097
+ quant_rec = quant[0]
1098
+ quant_sem = quant[1]
1099
+ else:
1100
+ raise ValueError('VQModelIFExtraSepEncSepDecJointSem expects quant to be a tuple')
1101
+
1102
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(
1103
+ quant_sem.shape[0], -1, quant_sem.shape[2] * quant_sem.shape[3]
1104
+ )
1105
+
1106
+ rec = self.decoder(self.post_quant_conv(quant_rec))
1107
+ # No detach: gradients from decoder_sem flow back into encoder2/quantize2.
1108
+ sem = self.decoder_sem(self.post_quant_conv_sem(quant_sem))
1109
+
1110
+ return rec, sem, distill_conv_out
1111
+
1112
+ def _combine_ae_losses(self, rec_aeloss, sem_aeloss):
1113
+ return rec_aeloss + self.sem_dec_loss_weight * sem_aeloss
1114
+
1115
+
1116
+ class VQModelIFExtraSepEncSepDecJointSem(VQModelIFExtraSepEncSepDec):
1117
+ """
1118
+ Variant of VQModelIFExtraSepEncSepDec where the semantic decoder is trained
1119
+ jointly with the semantic encoder (encoder2 / quantize2).
1120
+
1121
+ Unlike the base class, quant_sem is NOT detached before being passed to
1122
+ decoder_sem, so reconstruction gradients flow back into the semantic encoder
1123
+ and quantizer. The semantic decoder loss is weighted by
1124
+ ``sem_dec_loss_weight`` (default 0.1) to avoid dominating the primary
1125
+ reconstruction objective.
1126
+ """
1127
+
1128
+ def __init__(self, sem_dec_loss_weight=0.1, **kwargs):
1129
+ super().__init__(**kwargs)
1130
+ self.sem_dec_loss_weight = sem_dec_loss_weight
1131
+
1132
+ def _decode_components(self, quant):
1133
+ if isinstance(quant, tuple):
1134
+ quant_rec = quant[0]
1135
+ quant_sem = quant[1]
1136
+ else:
1137
+ raise ValueError('VQModelIFExtraSepEncSepDecJointSem expects quant to be a tuple')
1138
+
1139
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(
1140
+ quant_sem.shape[0], -1, quant_sem.shape[2] * quant_sem.shape[3]
1141
+ )
1142
+
1143
+ rec = self.decoder(self.post_quant_conv(quant_rec))
1144
+ # No detach: gradients from decoder_sem flow back into encoder2/quantize2.
1145
+ sem = self.decoder_sem(self.post_quant_conv_sem(quant_sem))
1146
+
1147
+ return rec, sem, distill_conv_out
1148
+
1149
+ def _combine_ae_losses(self, rec_aeloss, sem_aeloss):
1150
+ return rec_aeloss + self.sem_dec_loss_weight * sem_aeloss
1151
+
1152
+
1153
+ class VQModelIFSep(VQModelIF):
1154
+ def __init__(self,
1155
+ encoder_config,
1156
+ decoder_config,
1157
+ quantizer_config,
1158
+ loss_config=None,
1159
+ grad_acc_steps=1,
1160
+ cont_ratio_trainig= 0.0,
1161
+ ignore_keys=[],
1162
+ monitor=None,
1163
+ entropy_loss_weight_scheduler_config=None,
1164
+ distill_model_type='VIT_DINOv2', # 'VIT_DINO' or 'CNN' or VIT_DINOv2, VIT_DINOv2_large_reg4, SAM_VIT
1165
+ min_lr_multiplier=0.1,
1166
+ only_decoder=False,
1167
+ scale_equivariance=[]
1168
+ ):
1169
+ super().__init__(encoder_config, decoder_config, quantizer_config, loss_config,
1170
+ grad_acc_steps, cont_ratio_trainig, ignore_keys,
1171
+ monitor,
1172
+ entropy_loss_weight_scheduler_config,
1173
+ distill_model_type, min_lr_multiplier, only_decoder, scale_equivariance)
1174
+
1175
+ self.encoder2 = instantiate_from_config(encoder_config)
1176
+ self.post_quant_conv = torch.nn.Conv2d(quantizer_config.params['e_dim'], decoder_config.params["z_channels"], 1)
1177
+ self.quant_conv2 = nn.Conv2d(encoder_config.params["z_channels"], quantizer_config.params['e_dim'], 1)
1178
+ self.quantize2 = instantiate_from_config(quantizer_config)
1179
+
1180
+
1181
+ def decode(self, quant):
1182
+ if isinstance(quant, tuple):
1183
+ quant_rec = quant[0]
1184
+ quant_sem = quant[1]
1185
+ else:
1186
+ print('Error: quant should be a tuple')
1187
+ distill_conv_out = self.post_quant_conv_distill(quant_sem).view(quant_sem.shape[0], -1, quant_sem.shape[2]*quant_sem.shape[3])
1188
+ #quant_cat = torch.cat((quant_rec, quant_sem), dim=1)
1189
+ quant = self.post_quant_conv(quant_rec)
1190
+ return self.decoder(quant), distill_conv_out
orbis2/models/second_stage/cd_model_v2.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from copy import deepcopy
3
+
4
+ import torch
5
+
6
+ from .fm_model_v2 import (
7
+ FlowMatchingObjective,
8
+ FlowMatchingSamplerTeacherForcing,
9
+ PredictorModule,
10
+ requires_grad,
11
+ update_ema,
12
+ )
13
+
14
+ _DEFAULT_SAMPLE_GRIDS = {
15
+ 1: [0.99],
16
+ 2: [0.99, 0.8333],
17
+ 4: [0.99, 0.9375, 0.8333, 0.625],
18
+ }
19
+
20
+
21
+ def _endpoint(x, t, v):
22
+ """G(x, t) = x + t * v -- endpoint (x0) prediction."""
23
+ shape = [t.shape[0]] + [1] * (x.dim() - 1)
24
+ return x + t.view(*shape) * v
25
+
26
+
27
+ class ConsistencyDistillationObjective(FlowMatchingObjective):
28
+ """
29
+ Causal consistency distillation loss for a teacher-forced flow-matching
30
+ predictor: distills a frozen `teacher_vit` into a few-step student `vit`,
31
+ whose EMA (`ema_vit`) is the deliverable.
32
+ """
33
+
34
+ def __init__(
35
+ self,
36
+ predictor_module,
37
+ sigma_min=1e-5,
38
+ cd_num_timesteps=48,
39
+ cd_t_min=0.01,
40
+ cd_t_max=0.99,
41
+ cd_grid="uniform",
42
+ cd_loss="l2",
43
+ cd_pseudo_huber_c=0.00054,
44
+ ):
45
+ super().__init__(predictor_module, sigma_min=sigma_min)
46
+ self.cd_loss = cd_loss
47
+ self.cd_pseudo_huber_c = cd_pseudo_huber_c
48
+
49
+ if cd_grid == "uniform":
50
+ grid = torch.linspace(cd_t_min, cd_t_max, cd_num_timesteps)
51
+ else:
52
+ grid = torch.as_tensor(list(cd_grid), dtype=torch.float32)
53
+ if grid.numel() != cd_num_timesteps:
54
+ raise ValueError(
55
+ f"cd_grid has {grid.numel()} entries, expected "
56
+ f"cd_num_timesteps={cd_num_timesteps}."
57
+ )
58
+ predictor_module.register_buffer("cd_t_grid", grid, persistent=False)
59
+
60
+ def _split_sequence(self, x):
61
+ context = x[:, : -self.module.num_pred_frames]
62
+ target = x[:, -self.module.num_pred_frames :]
63
+ return context, target
64
+
65
+ def _forward_v(self, net, context, target_t, t, frame_rate, model_condition_kwargs):
66
+ sampler = self.module.sampler
67
+ model_input = sampler._build_model_inputs(context, target_t, t)
68
+ model_t = sampler._build_model_t(context, target_t, t)
69
+ pred = net(model_input, t=model_t * sampler.timescale, frame_rate=frame_rate, **model_condition_kwargs)
70
+ return sampler._extract_target_prediction(pred)
71
+
72
+ def compute_loss(self, pred, target):
73
+ diff = pred.float() - target.float()
74
+ if self.cd_loss == "pseudo_huber":
75
+ c = self.cd_pseudo_huber_c * (diff[0].numel() ** 0.5)
76
+ return (diff.flatten(1).pow(2).sum(dim=1) + c * c).sqrt() - c
77
+ return diff.pow(2)
78
+
79
+ def compute_step(self, x, frame_rate, condition_kwargs=None):
80
+ if getattr(self.module, "teacher_vit", None) is None:
81
+ raise RuntimeError(
82
+ "ConsistencyDistillationObjective requires the predictor module to "
83
+ "have a `teacher_vit` (set teacher_ckpt_path on "
84
+ "ConsistencyDistillPredictorModule)."
85
+ )
86
+
87
+ context, target = self._split_sequence(x)
88
+ batch_size = target.shape[0]
89
+ model_condition_kwargs = self.module.condition_preprocessor.get_model_condition_kwargs(condition_kwargs)
90
+
91
+ grid = self.module.cd_t_grid
92
+ n = torch.randint(1, grid.numel(), (batch_size,), device=x.device)
93
+ t, t_prev = grid[n], grid[n - 1]
94
+
95
+ noise = torch.randn_like(target)
96
+ x_t, _ = self.add_noise(target, t, noise=noise)
97
+
98
+ with torch.no_grad():
99
+ v_teacher = self._forward_v(self.module.teacher_vit, context, x_t, t, frame_rate, model_condition_kwargs)
100
+ step_shape = [t.shape[0]] + [1] * (x_t.dim() - 1)
101
+ x_prev = x_t + (t - t_prev).view(*step_shape) * v_teacher
102
+ v_target = self._forward_v(self.module.ema_vit, context, x_prev, t_prev, frame_rate, model_condition_kwargs)
103
+ tgt = _endpoint(x_prev, t_prev, v_target)
104
+
105
+ v_student = self._forward_v(self.module.vit, context, x_t, t, frame_rate, model_condition_kwargs)
106
+ pred = _endpoint(x_t, t, v_student)
107
+
108
+ return self.compute_loss(pred, tgt)
109
+
110
+
111
+ class ConsistencyDistillationSampler(FlowMatchingSamplerTeacherForcing):
112
+ """
113
+ Few-step consistency sampler: repeatedly renoises the current endpoint
114
+ estimate and re-predicts, over a short descending grid of timesteps,
115
+ instead of integrating a many-step Euler ODE.
116
+ """
117
+
118
+ def __init__(
119
+ self,
120
+ predictor_module,
121
+ timescale=1.0,
122
+ integration_t_eps=0.0,
123
+ timestep_conditioning="global",
124
+ sample_fresh_noise=True,
125
+ sample_t_grids=None,
126
+ ):
127
+ super().__init__(
128
+ predictor_module,
129
+ timescale=timescale,
130
+ integration_t_eps=integration_t_eps,
131
+ timestep_conditioning=timestep_conditioning,
132
+ )
133
+ self.sample_fresh_noise = sample_fresh_noise
134
+ self.sample_t_grids = dict(_DEFAULT_SAMPLE_GRIDS)
135
+ if sample_t_grids is not None:
136
+ self.sample_t_grids.update({int(k): list(v) for k, v in dict(sample_t_grids).items()})
137
+
138
+ def _forward_v(self, net, context, target_t, t, frame_rate, model_condition_kwargs):
139
+ model_input = self._build_model_inputs(context, target_t, t)
140
+ model_t = self._build_model_t(context, target_t, t)
141
+ pred = net(model_input, t=model_t * self.timescale, frame_rate=frame_rate, **model_condition_kwargs)
142
+ return self._extract_target_prediction(pred)
143
+
144
+ def _fewstep_grid(self, NFE):
145
+ if NFE in self.sample_t_grids:
146
+ return [float(v) for v in self.sample_t_grids[NFE]]
147
+ t_max = float(self.module.cd_t_grid[-1])
148
+ t_min = float(self.module.cd_t_grid[0])
149
+ return torch.linspace(t_max, t_min, NFE + 1)[:-1].tolist()
150
+
151
+ @torch.no_grad()
152
+ def sample(
153
+ self,
154
+ images=None,
155
+ latent=False,
156
+ eta=0.0,
157
+ NFE=2,
158
+ sample_with_ema=True,
159
+ num_samples=8,
160
+ frame_rate=None,
161
+ condition_kwargs=None,
162
+ return_sample=False,
163
+ ):
164
+ del eta # accepted only so PredictorModule.sample/roll_out call sites keep working
165
+ net = self._get_net(sample_with_ema)
166
+ device = next(net.parameters()).device
167
+ context = self._prepare_context(images, latent)
168
+
169
+ condition_kwargs = self.module.condition_preprocessor.prepare_condition_kwargs(
170
+ condition_kwargs,
171
+ batch_size=num_samples,
172
+ device=device,
173
+ split="sample",
174
+ )
175
+ model_condition_kwargs = self.module.condition_preprocessor.get_model_condition_kwargs(condition_kwargs)
176
+
177
+ if frame_rate is None:
178
+ frame_rate = self._default_frame_rate(num_samples, device)
179
+
180
+ input_h, input_w = self._get_input_hw()
181
+ grid = self._fewstep_grid(NFE)
182
+
183
+ eps0 = torch.randn(
184
+ num_samples,
185
+ self.module.num_pred_frames,
186
+ self.module.vit.in_channels,
187
+ input_h,
188
+ input_w,
189
+ device=device,
190
+ )
191
+ x = eps0
192
+ x0_hat = None
193
+ for k, t_k in enumerate(grid):
194
+ t = torch.full((num_samples,), t_k, device=device)
195
+ if k > 0:
196
+ eps_k = torch.randn_like(x) if self.sample_fresh_noise else eps0
197
+ x = (1.0 - t_k) * x0_hat + t_k * eps_k
198
+ v = self._forward_v(net, context, x, t, frame_rate, model_condition_kwargs)
199
+ x0_hat = _endpoint(x, t, v)
200
+
201
+ if return_sample:
202
+ return x0_hat, self.module.decode_frames(x0_hat.clone())
203
+ return x0_hat
204
+
205
+
206
+ class ConsistencyDistillPredictorModule(PredictorModule):
207
+ """
208
+ PredictorModule specialization that distills a frozen teacher (loaded from
209
+ `teacher_ckpt_path`) into a few-step student via consistency distillation.
210
+ The deliverable is `ema_vit` (evaluate with sample_with_ema=True, NFE 1/2/4).
211
+ """
212
+
213
+ # `teacher_vit.*` is intentionally absent from saved checkpoints (see
214
+ # on_save_checkpoint below) since the teacher is always reloaded fresh from
215
+ # teacher_ckpt_path at __init__ time. Eval scripts that assert on
216
+ # load_state_dict(...).missing_keys should exempt these prefixes.
217
+ checkpoint_exempt_key_prefixes = ("teacher_vit.",)
218
+
219
+ def __init__(
220
+ self,
221
+ *,
222
+ teacher_ckpt_path,
223
+ cd_ema_mu=0.999,
224
+ cd_weight_decay=0.0,
225
+ **kwargs,
226
+ ):
227
+ super().__init__(**kwargs)
228
+ self.cd_ema_mu = cd_ema_mu
229
+ self.cd_weight_decay = cd_weight_decay
230
+ self.strict_loading = False
231
+
232
+ checkpoint_path = os.path.expandvars(teacher_ckpt_path)
233
+ if not os.path.exists(checkpoint_path):
234
+ raise FileNotFoundError(f"teacher_ckpt_path {checkpoint_path} does not exist.")
235
+
236
+ try:
237
+ state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=True, mmap=True)["state_dict"]
238
+ except (TypeError, RuntimeError):
239
+ state_dict = torch.load(checkpoint_path, map_location="cpu", weights_only=True)["state_dict"]
240
+
241
+ ema_state_dict = {
242
+ key[len("ema_vit.") :]: value.clone()
243
+ for key, value in state_dict.items()
244
+ if key.startswith("ema_vit.")
245
+ }
246
+ del state_dict
247
+ if not ema_state_dict:
248
+ raise ValueError(f"No ema_vit.* keys found in teacher checkpoint {checkpoint_path}.")
249
+
250
+ self.vit.load_state_dict(ema_state_dict, strict=True)
251
+ self.ema_vit.load_state_dict(ema_state_dict, strict=True)
252
+ self.teacher_vit = deepcopy(self.vit)
253
+ requires_grad(self.teacher_vit, False)
254
+ self.teacher_vit.eval()
255
+
256
+ def setup(self, stage=None):
257
+ super().setup(stage)
258
+ if hasattr(self, "teacher_vit") and self.teacher_vit is not None:
259
+ self.teacher_vit.requires_grad_(False)
260
+
261
+ def configure_optimizers(self):
262
+ params = [p for p in self.vit.parameters() if p.requires_grad]
263
+ optimizer = torch.optim.AdamW(params, lr=self.learning_rate, weight_decay=self.cd_weight_decay)
264
+ scheduler = self.get_warmup_scheduler(optimizer, self.warmup_steps, self.min_lr_multiplier)
265
+ return [optimizer], [{"scheduler": scheduler, "interval": "step"}]
266
+
267
+ def on_train_batch_end(self, outputs, batch, batch_idx):
268
+ if not hasattr(self, "_ema_stream"):
269
+ self._ema_stream = torch.cuda.Stream() if torch.cuda.is_available() else None
270
+ if self._ema_stream is not None:
271
+ with torch.cuda.stream(self._ema_stream):
272
+ update_ema(self.ema_vit, self.vit, decay=self.cd_ema_mu)
273
+ else:
274
+ update_ema(self.ema_vit, self.vit, decay=self.cd_ema_mu)
275
+
276
+ def on_save_checkpoint(self, checkpoint):
277
+ super().on_save_checkpoint(checkpoint)
278
+ for key in [k for k in checkpoint["state_dict"] if k.startswith("teacher_vit.")]:
279
+ del checkpoint["state_dict"][key]
orbis2/models/second_stage/fm_conditions_v2.py ADDED
@@ -0,0 +1,1414 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+
4
+ import numpy as np
5
+ import PIL.Image
6
+ import PIL.ImageDraw
7
+ import torch
8
+ from einops import rearrange
9
+ from omegaconf import OmegaConf
10
+
11
+ from data.utils import get_trajectory_from_speeds_and_yaw_rates_batch
12
+ from util import instantiate_from_config
13
+
14
+
15
+ def _requires_grad(model, flag=True):
16
+ for param in model.parameters():
17
+ param.requires_grad = flag
18
+
19
+
20
+ def _text_on_image(images, texts):
21
+ for i in range(images.shape[0]):
22
+ image = images[i]
23
+ text = texts[i]
24
+ image = PIL.Image.fromarray((((image + 1) / 2).permute(1, 2, 0).cpu().numpy() * 255).astype("uint8"))
25
+ draw = PIL.ImageDraw.Draw(image)
26
+ draw.text((10, 10), text, fill="red", font_size=15)
27
+ image = (torch.from_numpy(np.array(image)).permute(2, 0, 1).float() / 255.0) * 2 - 1
28
+ images[i] = image
29
+ return images
30
+
31
+
32
+ class ConditionPreprocessor:
33
+ """
34
+ Default no-op condition preprocessor.
35
+
36
+ Concrete implementations can extract batch conditions, normalize sampling inputs,
37
+ and update autoregressive rollout state without changing predictor/objective/sampler
38
+ control flow.
39
+ """
40
+
41
+ def __init__(self, predictor_module):
42
+ self.module = predictor_module
43
+
44
+ def get_condition_kwargs_from_batch(self, batch, split):
45
+ return {}
46
+
47
+ def prepare_condition_kwargs(self, condition_kwargs=None, batch_size=None, device=None, split=None):
48
+ del batch_size, split
49
+ if condition_kwargs is None:
50
+ return {}
51
+
52
+ prepared = {}
53
+ for key, value in condition_kwargs.items():
54
+ if torch.is_tensor(value) and device is not None:
55
+ prepared[key] = value.to(device)
56
+ else:
57
+ prepared[key] = value
58
+ return prepared
59
+
60
+ def get_model_condition_kwargs(self, condition_kwargs):
61
+ if not condition_kwargs:
62
+ return {}
63
+ return {
64
+ key: value
65
+ for key, value in condition_kwargs.items()
66
+ if not key.startswith("_")
67
+ }
68
+
69
+ def slice_condition_kwargs(self, condition_kwargs, item):
70
+ if not condition_kwargs:
71
+ return {}
72
+
73
+ sliced = {}
74
+ for key, value in condition_kwargs.items():
75
+ if torch.is_tensor(value):
76
+ sliced[key] = value[item]
77
+ else:
78
+ sliced[key] = value
79
+ return sliced
80
+
81
+ def update_rollout_condition_kwargs(self, condition_kwargs, prediction, context, step_idx):
82
+ del prediction, context, step_idx
83
+ return condition_kwargs
84
+
85
+ def get_rollout_visualization_state(self, condition_kwargs):
86
+ del condition_kwargs
87
+ return None
88
+
89
+ def get_visualization_trajectory(self, visualization_state):
90
+ del visualization_state
91
+ return None
92
+
93
+ def update_rollout_visualization_state(self, visualization_state, step_idx):
94
+ del step_idx
95
+ return visualization_state
96
+
97
+ def get_rollout_visualization_trajectory(
98
+ self,
99
+ condition_kwargs,
100
+ num_gen_steps,
101
+ num_pred_frames=1,
102
+ num_condition_frames=None,
103
+ ):
104
+ del condition_kwargs, num_gen_steps, num_pred_frames, num_condition_frames
105
+ return None
106
+
107
+ def annotate_logged_images(self, images, batch, num_images):
108
+ del batch, num_images
109
+ return images
110
+
111
+ def to_device(self, device):
112
+ pass
113
+
114
+ def get_max_condition_odometry_offset(self):
115
+ """
116
+ Return the largest future raw-odometry offset needed to construct one
117
+ conditioning tensor from the current anchor position.
118
+
119
+ Preprocessors that do not depend on extra raw odometry should return
120
+ ``None``.
121
+ """
122
+ return None
123
+
124
+ def get_required_rollout_odometry_steps(
125
+ self,
126
+ *,
127
+ validation_params,
128
+ num_condition_frames,
129
+ num_gen_frames,
130
+ rollout_steps,
131
+ ):
132
+ del validation_params, num_condition_frames, num_gen_frames, rollout_steps
133
+ return None
134
+
135
+
136
+ def _validate_speed_yaw_scale(value, name):
137
+ scale = float(value)
138
+ if not np.isfinite(scale):
139
+ raise ValueError(f"`{name}` must be finite, got {value!r}.")
140
+ return scale
141
+
142
+
143
+ def _apply_speed_yaw_scales(steering, speed_scale=1.0, yaw_rate_scale=1.0):
144
+ if not torch.is_tensor(steering):
145
+ raise TypeError(f"`steering` must be a tensor, got {type(steering).__name__}.")
146
+ scaled = steering.clone()
147
+ scaled[..., 0] = scaled[..., 0] * float(speed_scale)
148
+ scaled[..., 1] = scaled[..., 1] * float(yaw_rate_scale)
149
+ return scaled
150
+
151
+
152
+ class SpeedYawConditionPreprocessorBase(ConditionPreprocessor):
153
+ """
154
+ Shared steering-conditioning utilities for preprocessors that consume raw
155
+ `[speed, yaw_rate]` sequences and advance through them during rollout.
156
+ """
157
+
158
+ def __init__(
159
+ self,
160
+ predictor_module,
161
+ odometry_steps_per_image_frame,
162
+ context_frame_anchor_index=-1,
163
+ steering_format="speed_yawrate",
164
+ speed_scale=1.0,
165
+ yaw_rate_scale=1.0,
166
+ steering_drop=0.0,
167
+ ):
168
+ super().__init__(predictor_module)
169
+ self.steering_format = str(steering_format)
170
+ if self.steering_format != "speed_yawrate":
171
+ raise ValueError(
172
+ f"{type(self).__name__} only supports `steering_format='speed_yawrate'`, "
173
+ f"got {self.steering_format!r}."
174
+ )
175
+
176
+ self.odometry_steps_per_image_frame = int(odometry_steps_per_image_frame)
177
+ if self.odometry_steps_per_image_frame <= 0:
178
+ raise ValueError(
179
+ "`odometry_steps_per_image_frame` must be a positive integer, "
180
+ f"got {self.odometry_steps_per_image_frame}."
181
+ )
182
+ self.context_frame_anchor_index = int(context_frame_anchor_index)
183
+ self.speed_scale = _validate_speed_yaw_scale(speed_scale, "speed_scale")
184
+ self.yaw_rate_scale = _validate_speed_yaw_scale(yaw_rate_scale, "yaw_rate_scale")
185
+ self.steering_drop = self._validate_steering_drop(steering_drop)
186
+
187
+ def _validate_steering_drop(self, steering_drop):
188
+ drop_prob = float(steering_drop)
189
+ if not np.isfinite(drop_prob):
190
+ raise ValueError(f"`steering_drop` must be finite, got {steering_drop!r}.")
191
+ if not 0.0 <= drop_prob <= 1.0:
192
+ raise ValueError(f"`steering_drop` must be in [0, 1], got {drop_prob}.")
193
+ return drop_prob
194
+
195
+ def _validate_offsets(self, offsets, name):
196
+ if not offsets:
197
+ raise ValueError(f"`{name}` must contain at least one positive odometry offset.")
198
+ normalized_offsets = [int(offset) for offset in offsets]
199
+ if any(offset <= 0 for offset in normalized_offsets):
200
+ raise ValueError(f"`{name}` must be positive integers, got {normalized_offsets}.")
201
+ if normalized_offsets != sorted(normalized_offsets):
202
+ raise ValueError(f"`{name}` must be sorted in ascending order, got {normalized_offsets}.")
203
+ return normalized_offsets
204
+
205
+ def get_max_condition_odometry_offset(self):
206
+ offsets = None
207
+ if hasattr(self, "goal_offsets"):
208
+ offsets = self.goal_offsets
209
+ elif hasattr(self, "steering_offsets"):
210
+ offsets = self.steering_offsets
211
+ if not offsets:
212
+ return None
213
+ return int(max(offsets))
214
+
215
+ def get_required_rollout_odometry_steps(
216
+ self,
217
+ *,
218
+ validation_params,
219
+ num_condition_frames,
220
+ num_gen_frames,
221
+ rollout_steps,
222
+ ):
223
+ del validation_params, num_gen_frames
224
+ max_condition_offset = self.get_max_condition_odometry_offset()
225
+ if max_condition_offset is None:
226
+ return None
227
+
228
+ rollout_steps = int(rollout_steps)
229
+ if rollout_steps <= 0:
230
+ raise ValueError(f"`rollout_steps` must be positive, got {rollout_steps}.")
231
+
232
+ anchor_frame_index = self._resolve_context_frame_anchor_index(int(num_condition_frames))
233
+ initial_anchor_odo_index = anchor_frame_index * int(self.odometry_steps_per_image_frame)
234
+ rollout_step_odo = int(self.odometry_steps_per_image_frame) * int(self.module.num_pred_frames)
235
+ last_anchor_odo_index = initial_anchor_odo_index + (rollout_steps - 1) * rollout_step_odo
236
+ return last_anchor_odo_index + int(max_condition_offset) + 1
237
+
238
+ def _normalize_batch_steering_format(self, steering_format):
239
+ if steering_format is None:
240
+ return None
241
+ if isinstance(steering_format, str):
242
+ return steering_format
243
+ if isinstance(steering_format, (list, tuple)):
244
+ unique_formats = {str(item) for item in steering_format}
245
+ if len(unique_formats) != 1:
246
+ raise ValueError(f"Batch contains mixed steering formats: {sorted(unique_formats)}.")
247
+ return next(iter(unique_formats))
248
+ return str(steering_format)
249
+
250
+ def _validate_batch_steering_format(self, batch):
251
+ batch_format = self._normalize_batch_steering_format(batch.get("steering_format"))
252
+ if batch_format is None:
253
+ return
254
+ if batch_format != self.steering_format:
255
+ raise ValueError(
256
+ f"{type(self).__name__} expected batch steering format "
257
+ f"{self.steering_format!r}, got {batch_format!r}. Check the dataset "
258
+ "`odo_transform_config` or `annotation_key`."
259
+ )
260
+
261
+ def _validate_steering(self, steering):
262
+ """Validate and return raw steering with expected shape `[B, T, 2]`."""
263
+ if steering is None:
264
+ raise ValueError(f"`steering` is required for {type(self).__name__}.")
265
+ if steering.ndim != 3:
266
+ raise ValueError(
267
+ f"`steering` must have shape [B, T, 2] for {type(self).__name__}, "
268
+ f"got {tuple(steering.shape)}."
269
+ )
270
+ if steering.shape[2] != 2:
271
+ raise ValueError(
272
+ f"{type(self).__name__} expects raw steering features "
273
+ f"[speed, yaw_rate], got shape {tuple(steering.shape)}."
274
+ )
275
+ return steering
276
+
277
+ def _resolve_odometry_dt(self, batch, batch_size, device, required):
278
+ frame_rate = batch.get("frame_rate")
279
+ if frame_rate is None:
280
+ if required:
281
+ raise ValueError(
282
+ f"`frame_rate` is required to compute physical steering trajectories for "
283
+ f"{type(self).__name__}."
284
+ )
285
+ return None
286
+
287
+ frame_rate = torch.as_tensor(frame_rate, device=device, dtype=torch.float32)
288
+ if frame_rate.ndim == 0:
289
+ frame_rate = frame_rate.expand(batch_size)
290
+ elif frame_rate.ndim == 1 and frame_rate.shape[0] == 1 and batch_size != 1:
291
+ frame_rate = frame_rate.expand(batch_size)
292
+ elif frame_rate.ndim != 1 or frame_rate.shape[0] != batch_size:
293
+ raise ValueError(
294
+ "`frame_rate` must be broadcastable to shape [B], got "
295
+ f"{tuple(frame_rate.shape)} for batch_size={batch_size}."
296
+ )
297
+
298
+ if torch.any(frame_rate <= 0):
299
+ raise ValueError(f"`frame_rate` must be positive, got {frame_rate}.")
300
+
301
+ return 1.0 / (frame_rate * float(self.odometry_steps_per_image_frame))
302
+
303
+ def _get_context_num_frames(self, batch, split):
304
+ """Return the number of conditioning image frames available for the current split."""
305
+ if not isinstance(batch, dict) or "images" not in batch:
306
+ raise ValueError(f"`images` are required in the batch to compute {type(self).__name__} conditions.")
307
+ images = batch["images"]
308
+ if images.ndim != 5:
309
+ raise ValueError(f"`images` must have shape [B, F, C, H, W], got {tuple(images.shape)}.")
310
+
311
+ if split == "rollout":
312
+ return images.shape[1]
313
+ if images.shape[1] < self.module.num_pred_frames:
314
+ raise ValueError(
315
+ f"Need at least {self.module.num_pred_frames} image frames, got {images.shape[1]}."
316
+ )
317
+ return images.shape[1] - self.module.num_pred_frames
318
+
319
+ def _resolve_context_frame_anchor_index(self, context_num_frames):
320
+ """Resolve the configured anchor index against the current context length."""
321
+ anchor_index = self.context_frame_anchor_index
322
+ if anchor_index < 0:
323
+ anchor_index = context_num_frames + anchor_index
324
+ if not 0 <= anchor_index < context_num_frames:
325
+ raise IndexError(
326
+ f"Resolved context_frame_anchor_index={anchor_index} is out of bounds for "
327
+ f"context_num_frames={context_num_frames}."
328
+ )
329
+ return anchor_index
330
+
331
+ def _build_rollout_state(self, steering, context_num_frames):
332
+ steering = self._validate_steering(steering)
333
+ batch_size = steering.shape[0]
334
+ device = steering.device
335
+ steps_per_image_frame = torch.full(
336
+ (batch_size,),
337
+ self.odometry_steps_per_image_frame,
338
+ device=device,
339
+ dtype=torch.long,
340
+ )
341
+ context_frame_anchor_index = self._resolve_context_frame_anchor_index(context_num_frames)
342
+ anchor_odo_index = context_frame_anchor_index * steps_per_image_frame
343
+ rollout_step_odo = steps_per_image_frame * self.module.num_pred_frames
344
+ return {
345
+ "_raw_speed_yaw": steering,
346
+ "_anchor_odo_index": anchor_odo_index,
347
+ "_rollout_step_odo": rollout_step_odo,
348
+ }
349
+
350
+ def _transform_raw_steering(self, steering):
351
+ steering = self._validate_steering(steering)
352
+ if self.speed_scale == 1.0 and self.yaw_rate_scale == 1.0:
353
+ return steering
354
+ return _apply_speed_yaw_scales(
355
+ steering,
356
+ speed_scale=self.speed_scale,
357
+ yaw_rate_scale=self.yaw_rate_scale,
358
+ )
359
+
360
+ def _build_condition_kwargs(self, steering, context_num_frames, odometry_dt=None):
361
+ condition_kwargs = self._build_rollout_state(steering, context_num_frames)
362
+ if odometry_dt is not None:
363
+ condition_kwargs["_odometry_dt"] = odometry_dt.to(
364
+ device=steering.device,
365
+ dtype=steering.dtype,
366
+ )
367
+ condition_kwargs["steering"] = self._compute_condition_steering(
368
+ steering,
369
+ condition_kwargs["_anchor_odo_index"],
370
+ odometry_dt=condition_kwargs.get("_odometry_dt"),
371
+ )
372
+ return condition_kwargs
373
+
374
+ def _maybe_apply_steering_dropout(self, steering, split):
375
+ if split != "train" or self.steering_drop == 0.0:
376
+ return steering
377
+
378
+ drop_mask = torch.rand(steering.shape[0], device=steering.device) < self.steering_drop
379
+ if not drop_mask.any():
380
+ return steering
381
+
382
+ dropped = steering.clone()
383
+ dropped[drop_mask] = torch.nan
384
+ return dropped
385
+
386
+ def _compute_condition_steering(self, steering, anchor_odo_index, odometry_dt=None):
387
+ raise NotImplementedError
388
+
389
+ def _requires_odometry_dt_for_conditioning(self):
390
+ return False
391
+
392
+ def _make_no_steering_condition(self, batch_size, device):
393
+ """Return a fully-NaN steering condition tensor for batches with no steering data.
394
+
395
+ The NaN values cause LinearSteeringEmbedder to fall back to its no_value_embeddings,
396
+ making missing-steering batches equivalent to fully-dropped steering (steering_drop=1.0).
397
+ Returns None if the subclass does not support unconditional batches.
398
+ """
399
+ return None
400
+
401
+ def get_condition_kwargs_from_batch(self, batch, split):
402
+ if not isinstance(batch, dict) or "steering" not in batch:
403
+ if isinstance(batch, dict) and "images" in batch:
404
+ images = batch["images"]
405
+ no_steering = self._make_no_steering_condition(images.shape[0], images.device)
406
+ if no_steering is not None:
407
+ return {"steering": no_steering}
408
+ return {}
409
+ self._validate_batch_steering_format(batch)
410
+ source_steering = self._validate_steering(batch["steering"])
411
+ steering = self._transform_raw_steering(source_steering)
412
+ context_num_frames = self._get_context_num_frames(batch, split)
413
+ odometry_dt = self._resolve_odometry_dt(
414
+ batch,
415
+ batch_size=steering.shape[0],
416
+ device=steering.device,
417
+ required=self._requires_odometry_dt_for_conditioning(),
418
+ )
419
+ condition_kwargs = self._build_condition_kwargs(steering, context_num_frames, odometry_dt=odometry_dt)
420
+ condition_kwargs["steering"] = self._maybe_apply_steering_dropout(condition_kwargs["steering"], split)
421
+ condition_kwargs["_source_raw_speed_yaw"] = source_steering
422
+ return condition_kwargs
423
+
424
+ def update_rollout_condition_kwargs(self, condition_kwargs, prediction, context, step_idx):
425
+ del prediction, context, step_idx
426
+ if not condition_kwargs:
427
+ return {}
428
+ updated = dict(condition_kwargs)
429
+ updated["_anchor_odo_index"] = condition_kwargs["_anchor_odo_index"] + condition_kwargs["_rollout_step_odo"]
430
+ updated["steering"] = self._compute_condition_steering(
431
+ condition_kwargs["_raw_speed_yaw"],
432
+ updated["_anchor_odo_index"],
433
+ odometry_dt=updated.get("_odometry_dt"),
434
+ )
435
+ return updated
436
+
437
+ def get_rollout_visualization_state(self, condition_kwargs):
438
+ if not condition_kwargs:
439
+ return None
440
+ return dict(condition_kwargs)
441
+
442
+ def get_visualization_trajectory(self, visualization_state):
443
+ if not visualization_state:
444
+ return None
445
+ return visualization_state.get("steering")
446
+
447
+ def update_rollout_visualization_state(self, visualization_state, step_idx):
448
+ if not visualization_state:
449
+ return None
450
+ return self.update_rollout_condition_kwargs(
451
+ visualization_state,
452
+ prediction=None,
453
+ context=None,
454
+ step_idx=step_idx,
455
+ )
456
+
457
+ def get_rollout_visualization_trajectory(
458
+ self,
459
+ condition_kwargs,
460
+ num_gen_steps,
461
+ num_pred_frames=1,
462
+ num_condition_frames=None,
463
+ ):
464
+ """Return shared trajectory geometry plus explicit per-frame cursor indices."""
465
+ if not condition_kwargs:
466
+ return None
467
+
468
+ odometry_dt = condition_kwargs.get("_odometry_dt")
469
+ if odometry_dt is None:
470
+ return None
471
+
472
+ rollout_steps = max(1, int(num_gen_steps))
473
+ rendered_frames_per_step = max(1, int(num_pred_frames))
474
+ num_context_frames = max(0, int(num_condition_frames or 0))
475
+
476
+ steering = self._validate_steering(condition_kwargs["_raw_speed_yaw"])
477
+ anchor_odo_index = condition_kwargs["_anchor_odo_index"]
478
+ rollout_step_odo = condition_kwargs["_rollout_step_odo"]
479
+ trajectories = []
480
+
481
+ for batch_idx in range(steering.shape[0]):
482
+ anchor_idx = int(anchor_odo_index[batch_idx].item())
483
+ step_odo = int(rollout_step_odo[batch_idx].item())
484
+ end_idx = anchor_idx + rollout_steps * step_odo
485
+ if end_idx >= steering.shape[1]:
486
+ raise ValueError(
487
+ f"Need steering horizon of at least {end_idx + 1} odometry steps for visualization, "
488
+ f"got {steering.shape[1]}. Anchor={anchor_idx}, rollout_step_odo={step_odo}, "
489
+ f"num_gen_steps={num_gen_steps}, num_pred_frames={num_pred_frames}."
490
+ )
491
+
492
+ trajectory = get_trajectory_from_speeds_and_yaw_rates_batch(
493
+ speeds=steering[batch_idx : batch_idx + 1, anchor_idx : end_idx + 1, 0],
494
+ yaw_rates=steering[batch_idx : batch_idx + 1, anchor_idx : end_idx + 1, 1],
495
+ dt=odometry_dt[batch_idx : batch_idx + 1],
496
+ )
497
+ trajectories.append(trajectory)
498
+
499
+ trajectory = torch.cat(trajectories, dim=0).to(device=steering.device)
500
+ step_indices = torch.arange(
501
+ 1,
502
+ rollout_steps + 1,
503
+ device=steering.device,
504
+ dtype=torch.long,
505
+ )
506
+ cursor_index = (step_indices * rollout_step_odo[0]).repeat_interleave(rendered_frames_per_step)
507
+ if num_context_frames > 0:
508
+ context_cursor_index = torch.zeros(
509
+ num_context_frames,
510
+ device=steering.device,
511
+ dtype=torch.long,
512
+ )
513
+ cursor_index = torch.cat([context_cursor_index, cursor_index], dim=0)
514
+ cursor_index = cursor_index.unsqueeze(0).expand(steering.shape[0], -1)
515
+ return {
516
+ "trajectory": trajectory[:, :, :2],
517
+ "heading": trajectory[:, :, 2],
518
+ "cursor_index": cursor_index,
519
+ }
520
+
521
+ def annotate_logged_images(self, images, batch, num_images):
522
+ if not isinstance(batch, dict) or "steering" not in batch:
523
+ return images
524
+
525
+ condition_batch = {
526
+ "images": batch["images"][:num_images],
527
+ "steering": batch["steering"][:num_images],
528
+ }
529
+ if "frame_rate" in batch:
530
+ frame_rate = batch["frame_rate"]
531
+ condition_batch["frame_rate"] = frame_rate[:num_images] if torch.is_tensor(frame_rate) else frame_rate
532
+ if "steering_format" in batch:
533
+ condition_batch["steering_format"] = batch["steering_format"]
534
+
535
+ condition_kwargs = self.get_condition_kwargs_from_batch(
536
+ condition_batch,
537
+ split="log_images",
538
+ )
539
+ steering = condition_kwargs["steering"]
540
+ steering_strings = []
541
+ for sample in steering.cpu().numpy():
542
+ step_strings = ["[" + ", ".join(f"{value:.1f}" for value in step) + "]" for step in sample]
543
+ steering_strings.append(" ".join(step_strings))
544
+ images[:, -1] = _text_on_image(images[:, -1], steering_strings)
545
+ return images
546
+
547
+
548
+ class SpeedYawMovingGoalConditionPreprocessor(SpeedYawConditionPreprocessorBase):
549
+ """
550
+ Compute future anchored goal displacements from raw speed/yaw-rate steering using an
551
+ explicit anchor definition.
552
+
553
+ Assumptions:
554
+ - `batch["steering"]` has shape [B, T, 2] with features [speed, yaw_rate]
555
+ - odometry/image frame-rate ratio is fixed and integer
556
+ - the anchor is a configured image-frame index within the context window
557
+ """
558
+
559
+ def __init__(
560
+ self,
561
+ predictor_module,
562
+ goal_offsets,
563
+ odometry_steps_per_image_frame,
564
+ context_frame_anchor_index=-1,
565
+ steering_format="speed_yawrate",
566
+ speed_scale=1.0,
567
+ yaw_rate_scale=1.0,
568
+ **kwargs,
569
+ ):
570
+ """Configure moving-goal conditioning from raw `[speed, yaw_rate]` sequences."""
571
+ super().__init__(
572
+ predictor_module=predictor_module,
573
+ odometry_steps_per_image_frame=odometry_steps_per_image_frame,
574
+ context_frame_anchor_index=context_frame_anchor_index,
575
+ steering_format=steering_format,
576
+ speed_scale=speed_scale,
577
+ yaw_rate_scale=yaw_rate_scale,
578
+ **kwargs
579
+ )
580
+ self.goal_offsets = self._validate_offsets(goal_offsets, "goal_offsets")
581
+
582
+ def _make_no_steering_condition(self, batch_size, device):
583
+ return torch.full((batch_size, len(self.goal_offsets), 2), float("nan"), device=device)
584
+
585
+ def _requires_odometry_dt_for_conditioning(self):
586
+ return True
587
+
588
+ def _compute_condition_steering(self, steering, anchor_odo_index, odometry_dt=None):
589
+ """Convert raw speed/yaw windows into future goal points anchored at odometry indices."""
590
+ steering = self._validate_steering(steering)
591
+ batch_size, steering_len, _dims = steering.shape
592
+ device = steering.device
593
+ if odometry_dt is None:
594
+ raise ValueError(
595
+ "SpeedYawMovingGoalConditionPreprocessor requires `odometry_dt` to compute goal-point "
596
+ "conditioning."
597
+ )
598
+ odometry_dt = torch.as_tensor(odometry_dt, device=device, dtype=steering.dtype)
599
+ if odometry_dt.ndim == 0:
600
+ odometry_dt = odometry_dt.expand(batch_size)
601
+ elif odometry_dt.ndim != 1 or odometry_dt.shape[0] != batch_size:
602
+ raise ValueError(
603
+ "`odometry_dt` must be broadcastable to shape [B], got "
604
+ f"{tuple(odometry_dt.shape)} for batch_size={batch_size}."
605
+ )
606
+ max_offset = self.goal_offsets[-1]
607
+ goals = []
608
+
609
+ for batch_idx in range(batch_size):
610
+ current_anchor = int(anchor_odo_index[batch_idx].item())
611
+ goal_end_idx = current_anchor + max_offset
612
+ if current_anchor < 0:
613
+ raise ValueError(f"Anchor index must be non-negative, got {current_anchor}.")
614
+ if goal_end_idx >= steering_len:
615
+ raise ValueError(
616
+ f"Need steering horizon of at least {goal_end_idx + 1} odometry steps, "
617
+ f"got {steering_len}. Anchor={current_anchor}, max_offset={max_offset}."
618
+ )
619
+
620
+ centered_traj = get_trajectory_from_speeds_and_yaw_rates_batch(
621
+ speeds=steering[batch_idx : batch_idx + 1, current_anchor : goal_end_idx + 1, 0],
622
+ yaw_rates=steering[batch_idx : batch_idx + 1, current_anchor : goal_end_idx + 1, 1],
623
+ dt=odometry_dt[batch_idx : batch_idx + 1],
624
+ )
625
+ goals.append(centered_traj[:, self.goal_offsets, :2])
626
+
627
+ return torch.cat(goals, dim=0).to(device=device)
628
+
629
+
630
+ class SpeedYawDirectConditionPreprocessor(SpeedYawConditionPreprocessorBase):
631
+ """
632
+ Slice future raw speed/yaw-rate commands directly from the odometry sequence.
633
+
634
+ This shares the same rollout alignment logic as moving-goal conditioning, but
635
+ returns raw `[speed, yaw_rate]` values instead of integrating them into XY goals.
636
+ """
637
+
638
+ def __init__(
639
+ self,
640
+ predictor_module,
641
+ steering_offsets,
642
+ odometry_steps_per_image_frame,
643
+ context_frame_anchor_index=-1,
644
+ steering_format="speed_yawrate",
645
+ speed_scale=1.0,
646
+ yaw_rate_scale=1.0,
647
+ ):
648
+ super().__init__(
649
+ predictor_module=predictor_module,
650
+ odometry_steps_per_image_frame=odometry_steps_per_image_frame,
651
+ context_frame_anchor_index=context_frame_anchor_index,
652
+ steering_format=steering_format,
653
+ speed_scale=speed_scale,
654
+ yaw_rate_scale=yaw_rate_scale,
655
+ )
656
+ self.steering_offsets = self._validate_offsets(steering_offsets, "steering_offsets")
657
+
658
+ def _make_no_steering_condition(self, batch_size, device):
659
+ return torch.full((batch_size, len(self.steering_offsets), 2), float("nan"), device=device)
660
+
661
+ def _compute_condition_steering(self, steering, anchor_odo_index, odometry_dt=None):
662
+ del odometry_dt
663
+ steering = self._validate_steering(steering)
664
+ batch_size, steering_len, _dims = steering.shape
665
+ commands = []
666
+ max_offset = self.steering_offsets[-1]
667
+ steering_offsets = torch.as_tensor(
668
+ self.steering_offsets,
669
+ device=steering.device,
670
+ dtype=torch.long,
671
+ )
672
+
673
+ for batch_idx in range(batch_size):
674
+ current_anchor = int(anchor_odo_index[batch_idx].item())
675
+ end_idx = current_anchor + max_offset
676
+ if current_anchor < 0:
677
+ raise ValueError(f"Anchor index must be non-negative, got {current_anchor}.")
678
+ if end_idx >= steering_len:
679
+ raise ValueError(
680
+ f"Need steering horizon of at least {end_idx + 1} odometry steps, "
681
+ f"got {steering_len}. Anchor={current_anchor}, max_offset={max_offset}."
682
+ )
683
+ commands.append(steering[batch_idx : batch_idx + 1, current_anchor + steering_offsets])
684
+
685
+ return torch.cat(commands, dim=0).to(device=steering.device)
686
+
687
+
688
+ class L2EndpointConditionPreprocessor(ConditionPreprocessor):
689
+ """
690
+ Inference-only L2 endpoint conditioning for the CleanCtx PredL2 v1 model.
691
+
692
+ The preprocessor owns the frozen L2 predictor and supplies only
693
+ `z_l2_start` / `z_l2_end` to the DiT. Private keys are retained as rollout
694
+ state and filtered before model calls by ConditionPreprocessor.
695
+ """
696
+
697
+ def __init__(
698
+ self,
699
+ predictor_module,
700
+ l2_predictor_config,
701
+ l2_predictor_frame_rate=1.0,
702
+ l2_context_latent=False,
703
+ l2_pred_NFE=10,
704
+ num_context_frames=1,
705
+ use_z_l2_start=True,
706
+ ):
707
+ super().__init__(predictor_module)
708
+ self.l2_predictor_frame_rate = float(l2_predictor_frame_rate)
709
+ self.l2_context_latent = bool(l2_context_latent)
710
+ self.l2_pred_NFE = int(l2_pred_NFE)
711
+ self.num_context_frames = int(num_context_frames)
712
+ self.use_z_l2_start = bool(use_z_l2_start)
713
+ self.l2_predictor_config = l2_predictor_config
714
+ self.l2_predictor = self._build_l2_predictor(l2_predictor_config)
715
+ self.l2_predictor_encoder_branch = self._get_l2_predictor_encoder_branch(
716
+ predictor=self.l2_predictor,
717
+ predictor_config=l2_predictor_config,
718
+ )
719
+
720
+ # --- BEGIN: rollout debug path info (safe to remove) ---
721
+ def get_l2_predictor_path_info(self):
722
+ """Return (exp_folder_name, ckpt_name) describing where the L2 predictor was loaded from."""
723
+ folder = os.path.expandvars(self.l2_predictor_config.folder)
724
+ ckpt_path = self.l2_predictor_config.ckpt_path if self.l2_predictor_config.ckpt_path else "checkpoints/last.ckpt"
725
+ return os.path.basename(os.path.normpath(folder)), os.path.basename(ckpt_path)
726
+ # --- END: rollout debug path info (safe to remove) ---
727
+
728
+ @property
729
+ def ae(self):
730
+ return self.module.ae
731
+
732
+ @property
733
+ def enc_scale(self):
734
+ return self.module.enc_scale
735
+
736
+ @property
737
+ def enc_scale_dino(self):
738
+ return self.module.enc_scale_dino
739
+
740
+ def _build_l2_predictor(self, cfg):
741
+ folder = os.path.expandvars(cfg.folder)
742
+ ckpt_path = cfg.ckpt_path if cfg.ckpt_path else "checkpoints/last.ckpt"
743
+ logging.info(f"Loading L2 checkpoint from {os.path.join(folder, ckpt_path)}")
744
+ model_cfg = OmegaConf.load(os.path.join(folder, "config.yaml"))
745
+ predictor = instantiate_from_config(model_cfg.model)
746
+ state_dict = torch.load(
747
+ os.path.join(folder, ckpt_path),
748
+ map_location="cpu",
749
+ weights_only=True,
750
+ )["state_dict"]
751
+ predictor.load_state_dict(state_dict, strict=False)
752
+ predictor.eval()
753
+ _requires_grad(predictor, False)
754
+ return predictor
755
+
756
+ def _normalize_l2_predictor_encoder_branch(self, encoder_branch):
757
+ if isinstance(encoder_branch, int):
758
+ if encoder_branch == 0:
759
+ return "rec"
760
+ if encoder_branch == 1:
761
+ return "sem"
762
+
763
+ branch = str(encoder_branch).strip().lower()
764
+ aliases = {
765
+ "rec": "rec",
766
+ "x0": "rec",
767
+ "h": "rec",
768
+ "sem": "sem",
769
+ "x1": "sem",
770
+ "h2": "sem",
771
+ }
772
+ if branch not in aliases:
773
+ raise ValueError(
774
+ f"Unsupported delegated L2 encoder_branch={encoder_branch!r}. "
775
+ "Expected one of: rec, x0, h, sem, x1, h2."
776
+ )
777
+ return aliases[branch]
778
+
779
+ def _get_l2_predictor_encoder_branch(self, predictor, predictor_config):
780
+ first_stage = getattr(predictor, "first_stage", None)
781
+ if first_stage is not None and hasattr(first_stage, "encoder_branch"):
782
+ return self._normalize_l2_predictor_encoder_branch(first_stage.encoder_branch)
783
+
784
+ if hasattr(predictor, "encoder_branch"):
785
+ return self._normalize_l2_predictor_encoder_branch(predictor.encoder_branch)
786
+
787
+ folder = os.path.expandvars(predictor_config.folder)
788
+ model_cfg = OmegaConf.load(os.path.join(folder, "config.yaml"))
789
+ first_stage_cfg = model_cfg.model.params.get("first_stage_handler_config")
790
+ if first_stage_cfg and "params" in first_stage_cfg and "encoder_branch" in first_stage_cfg.params:
791
+ return self._normalize_l2_predictor_encoder_branch(first_stage_cfg.params.encoder_branch)
792
+
793
+ if "encoder_branch" in model_cfg.model.params:
794
+ return self._normalize_l2_predictor_encoder_branch(model_cfg.model.params.encoder_branch)
795
+
796
+ raise ValueError(
797
+ "Could not determine the delegated L2 predictor encoder branch from the loaded model "
798
+ "or its config. Expected a rec/sem branch selection."
799
+ )
800
+
801
+ def to_device(self, device):
802
+ self.l2_predictor.to(device)
803
+
804
+ def get_l2_predictor_encoder_branch(self):
805
+ return self.l2_predictor_encoder_branch
806
+
807
+ def get_l2_predictor_latent_scale(self):
808
+ if self.l2_predictor_encoder_branch == "rec":
809
+ return self.enc_scale
810
+ return self.enc_scale_dino
811
+
812
+ def _reject_training_split(self, split):
813
+ if split in {"train", "val"}:
814
+ raise RuntimeError(
815
+ "L2EndpointConditionPreprocessor is inference-only. "
816
+ "It supports sample/log_images/rollout conditioning, not train/val."
817
+ )
818
+
819
+ def _validate_guidance_scale(self, condition_kwargs):
820
+ if not condition_kwargs or "l2_guidance_scale" not in condition_kwargs:
821
+ return
822
+ guidance_scale = condition_kwargs["l2_guidance_scale"]
823
+ if torch.is_tensor(guidance_scale):
824
+ guidance_scale = float(guidance_scale.detach().cpu().item())
825
+ else:
826
+ guidance_scale = float(guidance_scale)
827
+ if abs(guidance_scale - 1.0) > 1e-6:
828
+ raise RuntimeError(
829
+ "L2EndpointConditionPreprocessor only supports "
830
+ "l2_guidance_scale == 1.0."
831
+ )
832
+
833
+ @torch.no_grad()
834
+ def _encode_l2_predictor_frames(self, images):
835
+ if images.ndim == 5:
836
+ b, f, _c, _h, _w = images.size()
837
+ images = rearrange(images, "b f c h w -> (b f) c h w")
838
+ else:
839
+ b, _c, _h, _w = images.size()
840
+ f = 1
841
+
842
+ continuous = self.ae.encode(images)["continuous"]
843
+ if not isinstance(continuous, tuple) or len(continuous) != 2:
844
+ raise ValueError(
845
+ "L2EndpointConditionPreprocessor expects tokenizer.encode(...)[\"continuous\"] "
846
+ f"to return a 2-tuple, got {type(continuous)}."
847
+ )
848
+ branch_index = 0 if self.l2_predictor_encoder_branch == "rec" else 1
849
+ latent_scale = self.get_l2_predictor_latent_scale()
850
+ latents = continuous[branch_index] * latent_scale
851
+ return rearrange(latents, "(b f) c h w -> b f c h w", b=b, f=f)
852
+
853
+ @torch.no_grad()
854
+ def _encode_l2_context(self, l2_context):
855
+ if self.l2_context_latent:
856
+ return l2_context * self.get_l2_predictor_latent_scale()
857
+ return self._encode_l2_predictor_frames(l2_context)
858
+
859
+ @torch.no_grad()
860
+ def _predict_l2_end(self, l2_context_latent):
861
+ batch_size = l2_context_latent.shape[0]
862
+ device = l2_context_latent.device
863
+ if next(self.l2_predictor.parameters()).device != device:
864
+ self.l2_predictor.to(device)
865
+ predictor = self.l2_predictor
866
+ frame_rate = torch.full(
867
+ (batch_size,),
868
+ self.l2_predictor_frame_rate,
869
+ device=device,
870
+ )
871
+ pred = predictor.sample(
872
+ images=l2_context_latent,
873
+ latent=True,
874
+ eta=0.0,
875
+ NFE=self.l2_pred_NFE,
876
+ sample_with_ema=True,
877
+ num_samples=batch_size,
878
+ frame_rate=frame_rate,
879
+ )
880
+ return pred[:, -1]
881
+
882
+ @torch.no_grad()
883
+ def _sample_l2_next_latent(self, l2_context_latent):
884
+ return self._predict_l2_end(l2_context_latent)
885
+
886
+ def _get_z_l2_start_from_context(self, l2_context_latent, z_l2_end):
887
+ if self.use_z_l2_start:
888
+ if l2_context_latent is None:
889
+ raise ValueError(
890
+ "L2EndpointConditionPreprocessor requires z_l2_start or "
891
+ "l2_context/_l2_context_latent when use_z_l2_start=True."
892
+ )
893
+ return l2_context_latent[:, -1]
894
+ return torch.zeros_like(z_l2_end)
895
+
896
+ def get_condition_kwargs_from_batch(self, batch, split):
897
+ self._reject_training_split(split)
898
+ if not isinstance(batch, dict) or "l2_context" not in batch:
899
+ raise ValueError(
900
+ "L2EndpointConditionPreprocessor requires batch['l2_context'] "
901
+ f"for split={split!r}."
902
+ )
903
+ return {"l2_context": batch["l2_context"]}
904
+
905
+ def _move_public_condition_kwargs(self, condition_kwargs, device):
906
+ prepared = {}
907
+ for key, value in condition_kwargs.items():
908
+ if key == "l2_guidance_scale":
909
+ continue
910
+ if torch.is_tensor(value) and device is not None:
911
+ prepared[key] = value.to(device)
912
+ else:
913
+ prepared[key] = value
914
+ return prepared
915
+
916
+ @torch.no_grad()
917
+ def prepare_condition_kwargs(self, condition_kwargs=None, batch_size=None, device=None, split=None):
918
+ self._reject_training_split(split)
919
+ condition_kwargs = condition_kwargs or {}
920
+ self._validate_guidance_scale(condition_kwargs)
921
+ prepared = self._move_public_condition_kwargs(condition_kwargs, device)
922
+
923
+ has_start = "z_l2_start" in prepared
924
+ has_end = "z_l2_end" in prepared
925
+ prepared["_z_l2_start_fixed"] = bool(prepared.get("_z_l2_start_fixed", has_start))
926
+ prepared["_z_l2_end_fixed"] = bool(prepared.get("_z_l2_end_fixed", has_end))
927
+
928
+ l2_context_latent = prepared.get("_l2_context_latent")
929
+ if l2_context_latent is None and "l2_context" in prepared:
930
+ l2_context_latent = self._encode_l2_context(prepared.pop("l2_context"))
931
+ prepared["_l2_context_latent"] = l2_context_latent
932
+ elif l2_context_latent is not None:
933
+ prepared["_l2_context_latent"] = l2_context_latent
934
+
935
+ if "z_l2_end" not in prepared:
936
+ if l2_context_latent is None:
937
+ raise ValueError(
938
+ "L2EndpointConditionPreprocessor requires z_l2_end or "
939
+ "l2_context/_l2_context_latent to predict it."
940
+ )
941
+ prepared["_l2_next_latent"] = self._sample_l2_next_latent(l2_context_latent)
942
+ prepared["z_l2_end"] = prepared["_l2_next_latent"]
943
+ elif "_l2_next_latent" not in prepared and not prepared["_z_l2_end_fixed"]:
944
+ prepared["_l2_next_latent"] = prepared["z_l2_end"]
945
+
946
+ if "z_l2_start" not in prepared:
947
+ prepared["z_l2_start"] = self._get_z_l2_start_from_context(
948
+ l2_context_latent,
949
+ prepared["z_l2_end"],
950
+ )
951
+
952
+ if batch_size is not None:
953
+ for key in ("z_l2_start", "z_l2_end"):
954
+ if prepared[key].shape[0] != batch_size:
955
+ raise ValueError(
956
+ f"{key} batch size {prepared[key].shape[0]} does not match "
957
+ f"expected batch_size={batch_size}."
958
+ )
959
+
960
+ return prepared
961
+
962
+ @torch.no_grad()
963
+ def update_rollout_condition_kwargs(self, condition_kwargs, prediction, context, step_idx):
964
+ del prediction, context, step_idx
965
+ if not condition_kwargs:
966
+ return {}
967
+
968
+ updated = dict(condition_kwargs)
969
+ l2_context_latent = updated.get("_l2_context_latent")
970
+ start_fixed = bool(updated.get("_z_l2_start_fixed", False))
971
+ end_fixed = bool(updated.get("_z_l2_end_fixed", False))
972
+ if l2_context_latent is None or (start_fixed and end_fixed):
973
+ return updated
974
+
975
+ l2_next_latent = updated.get("_l2_next_latent")
976
+ if l2_next_latent is None:
977
+ if end_fixed:
978
+ l2_next_latent = updated["z_l2_end"]
979
+ else:
980
+ raise ValueError(
981
+ "L2EndpointConditionPreprocessor requires cached `_l2_next_latent` "
982
+ "or explicit `z_l2_end` during rollout updates."
983
+ )
984
+ l2_next_latent = l2_next_latent.to(l2_context_latent.device)
985
+ updated["_l2_context_latent"] = torch.cat(
986
+ [l2_context_latent[:, 1:], l2_next_latent.unsqueeze(1)],
987
+ dim=1,
988
+ )
989
+ if not start_fixed:
990
+ updated["z_l2_start"] = self._get_z_l2_start_from_context(
991
+ updated["_l2_context_latent"],
992
+ updated["z_l2_end"],
993
+ )
994
+ if not end_fixed:
995
+ updated["_l2_next_latent"] = self._sample_l2_next_latent(updated["_l2_context_latent"])
996
+ updated["z_l2_end"] = updated["_l2_next_latent"]
997
+ return updated
998
+
999
+
1000
+ class L2SteeringEndpointConditionPreprocessor(L2EndpointConditionPreprocessor):
1001
+ """
1002
+ Steering-aware variant of L2EndpointConditionPreprocessor.
1003
+
1004
+ Raw speed/yaw-rate steering is converted into the conditioning expected by
1005
+ the frozen L2 predictor. That steering state remains private to the L2
1006
+ branch and is updated in lockstep with the autoregressive L2-context slide.
1007
+ """
1008
+
1009
+ def __init__(
1010
+ self,
1011
+ predictor_module,
1012
+ l2_predictor_config,
1013
+ l2_predictor_frame_rate=1.0,
1014
+ l2_context_latent=False,
1015
+ l2_pred_NFE=10,
1016
+ num_context_frames=1,
1017
+ use_z_l2_start=True,
1018
+ speed_scale=1.0,
1019
+ yaw_rate_scale=1.0,
1020
+ ):
1021
+ super().__init__(
1022
+ predictor_module=predictor_module,
1023
+ l2_predictor_config=l2_predictor_config,
1024
+ l2_predictor_frame_rate=l2_predictor_frame_rate,
1025
+ l2_context_latent=l2_context_latent,
1026
+ l2_pred_NFE=l2_pred_NFE,
1027
+ num_context_frames=num_context_frames,
1028
+ use_z_l2_start=use_z_l2_start,
1029
+ )
1030
+ self.speed_scale = _validate_speed_yaw_scale(speed_scale, "speed_scale")
1031
+ self.yaw_rate_scale = _validate_speed_yaw_scale(yaw_rate_scale, "yaw_rate_scale")
1032
+ self.l2_condition_preprocessor = self._get_l2_condition_preprocessor()
1033
+ if hasattr(self.l2_condition_preprocessor, "goal_offsets"):
1034
+ self.goal_offsets = [int(offset) for offset in self.l2_condition_preprocessor.goal_offsets]
1035
+ if hasattr(self.l2_condition_preprocessor, "steering_offsets"):
1036
+ self.steering_offsets = [int(offset) for offset in self.l2_condition_preprocessor.steering_offsets]
1037
+ self._validate_l2_condition_preprocessor()
1038
+
1039
+ def _get_l2_condition_preprocessor(self):
1040
+ preprocessor = getattr(self.l2_predictor, "condition_preprocessor", None)
1041
+ if preprocessor is None:
1042
+ raise TypeError(
1043
+ "L2SteeringEndpointConditionPreprocessor requires the frozen L2 predictor "
1044
+ "to expose `condition_preprocessor`."
1045
+ )
1046
+ return preprocessor
1047
+
1048
+ def _validate_l2_condition_preprocessor(self):
1049
+ required_methods = (
1050
+ "get_condition_kwargs_from_batch",
1051
+ "prepare_condition_kwargs",
1052
+ "update_rollout_condition_kwargs",
1053
+ )
1054
+ missing = [
1055
+ method_name
1056
+ for method_name in required_methods
1057
+ if not callable(getattr(self.l2_condition_preprocessor, method_name, None))
1058
+ ]
1059
+ if missing:
1060
+ raise TypeError(
1061
+ "Frozen L2 condition preprocessor is incompatible with steering delegation; "
1062
+ f"missing methods: {missing}."
1063
+ )
1064
+
1065
+ def get_max_condition_odometry_offset(self):
1066
+ if hasattr(self.l2_condition_preprocessor, "get_max_condition_odometry_offset"):
1067
+ return self.l2_condition_preprocessor.get_max_condition_odometry_offset()
1068
+ return None
1069
+
1070
+ def get_required_rollout_odometry_steps(
1071
+ self,
1072
+ *,
1073
+ validation_params,
1074
+ num_condition_frames,
1075
+ num_gen_frames,
1076
+ rollout_steps,
1077
+ ):
1078
+ del num_condition_frames
1079
+ delegated_required_steps = getattr(self.l2_condition_preprocessor, "get_required_rollout_odometry_steps", None)
1080
+ if callable(delegated_required_steps):
1081
+ delegated_num_pred_frames = int(getattr(self.l2_predictor, "num_pred_frames", 1))
1082
+ return delegated_required_steps(
1083
+ validation_params=validation_params,
1084
+ num_condition_frames=int(self.num_context_frames),
1085
+ num_gen_frames=int(rollout_steps) * delegated_num_pred_frames,
1086
+ rollout_steps=rollout_steps,
1087
+ )
1088
+
1089
+ del num_gen_frames
1090
+ max_condition_offset = self.get_max_condition_odometry_offset()
1091
+ if max_condition_offset is None:
1092
+ return None
1093
+
1094
+ source_odometry_rate = float(
1095
+ getattr(validation_params, "odometry_frame_rate", getattr(validation_params, "frame_rate"))
1096
+ )
1097
+ target_odometry_rate = float(self.l2_predictor_frame_rate)
1098
+ ratio = source_odometry_rate / target_odometry_rate
1099
+ rounded_ratio = round(ratio)
1100
+ if abs(ratio - rounded_ratio) > 1e-8:
1101
+ raise ValueError(
1102
+ "Delegated L2 rollout requires the parent odometry rate to be an integer multiple of "
1103
+ f"the frozen L2 rate, got odometry_frame_rate={source_odometry_rate} and "
1104
+ f"l2_predictor_frame_rate={target_odometry_rate}."
1105
+ )
1106
+
1107
+ required_l2_steps = self.num_context_frames + int(rollout_steps) + int(max_condition_offset) - 1
1108
+ return int((required_l2_steps - 1) * rounded_ratio + 1)
1109
+
1110
+ def _maybe_resample_raw_steering_for_l2(self, steering, batch):
1111
+ if not torch.is_tensor(steering):
1112
+ raise TypeError(f"`steering` must be a tensor, got {type(steering).__name__}.")
1113
+ if "frame_rate" not in batch:
1114
+ return steering
1115
+
1116
+ source_frame_rate = torch.as_tensor(
1117
+ batch["frame_rate"],
1118
+ device=steering.device,
1119
+ dtype=torch.float32,
1120
+ )
1121
+ if source_frame_rate.ndim == 0:
1122
+ source_frame_rate = source_frame_rate.expand(steering.shape[0])
1123
+ elif source_frame_rate.ndim == 1 and source_frame_rate.shape[0] == 1 and steering.shape[0] != 1:
1124
+ source_frame_rate = source_frame_rate.expand(steering.shape[0])
1125
+ elif source_frame_rate.ndim != 1 or source_frame_rate.shape[0] != steering.shape[0]:
1126
+ raise ValueError(
1127
+ "`frame_rate` must be broadcastable to shape [B] for delegated L2 steering, got "
1128
+ f"{tuple(source_frame_rate.shape)} for batch_size={steering.shape[0]}."
1129
+ )
1130
+
1131
+ target_frame_rate = torch.full_like(source_frame_rate, float(self.l2_predictor_frame_rate))
1132
+ if torch.any(target_frame_rate <= 0):
1133
+ raise ValueError(f"`l2_predictor_frame_rate` must be positive, got {self.l2_predictor_frame_rate}.")
1134
+
1135
+ if torch.allclose(source_frame_rate, target_frame_rate):
1136
+ return steering
1137
+
1138
+ ratios = source_frame_rate / target_frame_rate
1139
+ rounded_ratios = torch.round(ratios)
1140
+ if not torch.allclose(ratios, rounded_ratios, atol=1e-8, rtol=0.0):
1141
+ raise ValueError(
1142
+ "Delegated L2 steering requires the parent frame rate to be an integer multiple of "
1143
+ f"the frozen L2 frame rate. Got source_frame_rate={source_frame_rate.tolist()} and "
1144
+ f"l2_predictor_frame_rate={self.l2_predictor_frame_rate}."
1145
+ )
1146
+
1147
+ resampled = []
1148
+ for batch_idx in range(steering.shape[0]):
1149
+ step = int(rounded_ratios[batch_idx].item())
1150
+ if step <= 0:
1151
+ raise ValueError(
1152
+ f"Delegated L2 steering subsample step must be positive, got {step}."
1153
+ )
1154
+ resampled.append(steering[batch_idx : batch_idx + 1, ::step])
1155
+ return torch.cat(resampled, dim=0)
1156
+
1157
+ def _build_l2_rollout_batch(self, batch):
1158
+ if "l2_context" not in batch:
1159
+ raise ValueError(
1160
+ "L2SteeringEndpointConditionPreprocessor requires batch['l2_context'] "
1161
+ "to build delegated L2 conditions."
1162
+ )
1163
+ if "steering" not in batch:
1164
+ raise ValueError(
1165
+ "L2SteeringEndpointConditionPreprocessor requires batch['steering'] "
1166
+ "to build delegated L2 conditions."
1167
+ )
1168
+ source_steering = batch["steering"]
1169
+ scaled_steering = _apply_speed_yaw_scales(
1170
+ source_steering,
1171
+ speed_scale=self.speed_scale,
1172
+ yaw_rate_scale=self.yaw_rate_scale,
1173
+ )
1174
+ l2_batch = {
1175
+ "images": batch["l2_context"],
1176
+ # Keep steering at raw odometry resolution and let the frozen L2
1177
+ # preprocessor interpret it via its own odometry alignment config.
1178
+ "steering": scaled_steering,
1179
+ }
1180
+ if "steering_format" in batch:
1181
+ l2_batch["steering_format"] = batch["steering_format"]
1182
+ if "frame_rate" in batch:
1183
+ frame_rate = torch.as_tensor(
1184
+ batch["frame_rate"],
1185
+ device=batch["steering"].device,
1186
+ dtype=torch.float32,
1187
+ )
1188
+ l2_batch["frame_rate"] = torch.full_like(
1189
+ frame_rate,
1190
+ float(self.l2_predictor_frame_rate),
1191
+ dtype=torch.float32,
1192
+ )
1193
+ else:
1194
+ batch_size = batch["steering"].shape[0]
1195
+ l2_batch["frame_rate"] = torch.full(
1196
+ (batch_size,),
1197
+ float(self.l2_predictor_frame_rate),
1198
+ device=batch["steering"].device,
1199
+ dtype=torch.float32,
1200
+ )
1201
+ return l2_batch
1202
+
1203
+ def _prepare_l2_predictor_condition_kwargs(self, l2_condition_kwargs, device):
1204
+ return self.l2_condition_preprocessor.prepare_condition_kwargs(
1205
+ l2_condition_kwargs,
1206
+ batch_size=None,
1207
+ device=device,
1208
+ split="rollout",
1209
+ )
1210
+
1211
+ def _update_l2_predictor_condition_kwargs(self, l2_condition_kwargs):
1212
+ return self.l2_condition_preprocessor.update_rollout_condition_kwargs(
1213
+ l2_condition_kwargs,
1214
+ prediction=None,
1215
+ context=None,
1216
+ step_idx=0,
1217
+ )
1218
+
1219
+ @torch.no_grad()
1220
+ def _predict_l2_end(self, l2_context_latent, l2_condition_kwargs=None):
1221
+ batch_size = l2_context_latent.shape[0]
1222
+ device = l2_context_latent.device
1223
+ if next(self.l2_predictor.parameters()).device != device:
1224
+ self.l2_predictor.to(device)
1225
+ predictor = self.l2_predictor
1226
+ frame_rate = torch.full(
1227
+ (batch_size,),
1228
+ self.l2_predictor_frame_rate,
1229
+ device=device,
1230
+ )
1231
+ pred = predictor.sample(
1232
+ images=l2_context_latent,
1233
+ latent=True,
1234
+ eta=0.0,
1235
+ NFE=self.l2_pred_NFE,
1236
+ sample_with_ema=True,
1237
+ num_samples=batch_size,
1238
+ frame_rate=frame_rate,
1239
+ condition_kwargs=l2_condition_kwargs,
1240
+ )
1241
+ return pred[:, -1]
1242
+
1243
+ @torch.no_grad()
1244
+ def _sample_l2_next_latent(self, l2_context_latent, l2_condition_kwargs=None):
1245
+ return self._predict_l2_end(l2_context_latent, l2_condition_kwargs=l2_condition_kwargs)
1246
+
1247
+ def get_condition_kwargs_from_batch(self, batch, split):
1248
+ self._reject_training_split(split)
1249
+ if not isinstance(batch, dict) or "l2_context" not in batch:
1250
+ raise ValueError(
1251
+ "L2SteeringEndpointConditionPreprocessor requires batch['l2_context'] "
1252
+ f"for split={split!r}."
1253
+ )
1254
+ if "steering" not in batch:
1255
+ raise ValueError(
1256
+ "L2SteeringEndpointConditionPreprocessor requires batch['steering'] "
1257
+ f"for split={split!r}."
1258
+ )
1259
+ l2_context = batch["l2_context"]
1260
+ if l2_context.ndim != 5:
1261
+ raise ValueError(f"`l2_context` must have shape [B, F, C, H, W], got {tuple(l2_context.shape)}.")
1262
+ source_l2_steering = self._maybe_resample_raw_steering_for_l2(batch["steering"], batch)
1263
+ l2_condition_kwargs = self.l2_condition_preprocessor.get_condition_kwargs_from_batch(
1264
+ self._build_l2_rollout_batch(batch),
1265
+ split="rollout",
1266
+ )
1267
+ l2_condition_kwargs["_source_raw_speed_yaw"] = source_l2_steering
1268
+ return {
1269
+ "l2_context": l2_context,
1270
+ "_l2_condition_kwargs": l2_condition_kwargs,
1271
+ }
1272
+
1273
+ @torch.no_grad()
1274
+ def prepare_condition_kwargs(self, condition_kwargs=None, batch_size=None, device=None, split=None):
1275
+ self._reject_training_split(split)
1276
+ condition_kwargs = condition_kwargs or {}
1277
+ self._validate_guidance_scale(condition_kwargs)
1278
+ prepared = self._move_public_condition_kwargs(condition_kwargs, device)
1279
+
1280
+ has_start = "z_l2_start" in prepared
1281
+ has_end = "z_l2_end" in prepared
1282
+ prepared["_z_l2_start_fixed"] = bool(prepared.get("_z_l2_start_fixed", has_start))
1283
+ prepared["_z_l2_end_fixed"] = bool(prepared.get("_z_l2_end_fixed", has_end))
1284
+
1285
+ l2_context_latent = prepared.get("_l2_context_latent")
1286
+ if l2_context_latent is None and "l2_context" in prepared:
1287
+ l2_context_latent = self._encode_l2_context(prepared.pop("l2_context"))
1288
+ prepared["_l2_context_latent"] = l2_context_latent
1289
+ elif l2_context_latent is not None:
1290
+ prepared["_l2_context_latent"] = l2_context_latent
1291
+
1292
+ l2_condition_kwargs = self._prepare_l2_predictor_condition_kwargs(
1293
+ prepared.get("_l2_condition_kwargs"),
1294
+ device=device,
1295
+ )
1296
+ prepared["_l2_condition_kwargs"] = l2_condition_kwargs
1297
+
1298
+ if "z_l2_end" not in prepared:
1299
+ if l2_context_latent is None:
1300
+ raise ValueError(
1301
+ "L2SteeringEndpointConditionPreprocessor requires z_l2_end or "
1302
+ "l2_context/_l2_context_latent to predict it."
1303
+ )
1304
+ prepared["_l2_next_latent"] = self._sample_l2_next_latent(
1305
+ l2_context_latent,
1306
+ l2_condition_kwargs=l2_condition_kwargs,
1307
+ )
1308
+ prepared["z_l2_end"] = prepared["_l2_next_latent"]
1309
+ elif "_l2_next_latent" not in prepared and not prepared["_z_l2_end_fixed"]:
1310
+ prepared["_l2_next_latent"] = prepared["z_l2_end"]
1311
+
1312
+ if "z_l2_start" not in prepared:
1313
+ prepared["z_l2_start"] = self._get_z_l2_start_from_context(
1314
+ l2_context_latent,
1315
+ prepared["z_l2_end"],
1316
+ )
1317
+
1318
+ if batch_size is not None:
1319
+ for key in ("z_l2_start", "z_l2_end"):
1320
+ if prepared[key].shape[0] != batch_size:
1321
+ raise ValueError(
1322
+ f"{key} batch size {prepared[key].shape[0]} does not match "
1323
+ f"expected batch_size={batch_size}."
1324
+ )
1325
+
1326
+ return prepared
1327
+
1328
+ @torch.no_grad()
1329
+ def update_rollout_condition_kwargs(self, condition_kwargs, prediction, context, step_idx):
1330
+ del prediction, context, step_idx
1331
+ if not condition_kwargs:
1332
+ return {}
1333
+
1334
+ updated = dict(condition_kwargs)
1335
+ l2_context_latent = updated.get("_l2_context_latent")
1336
+ start_fixed = bool(updated.get("_z_l2_start_fixed", False))
1337
+ end_fixed = bool(updated.get("_z_l2_end_fixed", False))
1338
+ if l2_context_latent is None:
1339
+ return updated
1340
+
1341
+ if not (start_fixed and end_fixed):
1342
+ l2_next_latent = updated.get("_l2_next_latent")
1343
+ if l2_next_latent is None:
1344
+ if end_fixed:
1345
+ l2_next_latent = updated["z_l2_end"]
1346
+ else:
1347
+ raise ValueError(
1348
+ "L2SteeringEndpointConditionPreprocessor requires cached `_l2_next_latent` "
1349
+ "or explicit `z_l2_end` during rollout updates."
1350
+ )
1351
+ l2_next_latent = l2_next_latent.to(l2_context_latent.device)
1352
+ updated["_l2_context_latent"] = torch.cat(
1353
+ [l2_context_latent[:, 1:], l2_next_latent.unsqueeze(1)],
1354
+ dim=1,
1355
+ )
1356
+
1357
+ if "_l2_condition_kwargs" in updated:
1358
+ updated["_l2_condition_kwargs"] = self._update_l2_predictor_condition_kwargs(
1359
+ updated["_l2_condition_kwargs"],
1360
+ )
1361
+
1362
+ if not start_fixed:
1363
+ updated["z_l2_start"] = self._get_z_l2_start_from_context(
1364
+ updated["_l2_context_latent"],
1365
+ updated["z_l2_end"],
1366
+ )
1367
+
1368
+ if not end_fixed:
1369
+ updated["_l2_next_latent"] = self._sample_l2_next_latent(
1370
+ updated["_l2_context_latent"],
1371
+ l2_condition_kwargs=updated.get("_l2_condition_kwargs"),
1372
+ )
1373
+ updated["z_l2_end"] = updated["_l2_next_latent"]
1374
+ return updated
1375
+
1376
+ def get_rollout_visualization_state(self, condition_kwargs):
1377
+ if not condition_kwargs:
1378
+ return None
1379
+ l2_condition_kwargs = condition_kwargs.get("_l2_condition_kwargs")
1380
+ if not l2_condition_kwargs:
1381
+ return None
1382
+ return self.l2_condition_preprocessor.get_rollout_visualization_state(l2_condition_kwargs)
1383
+
1384
+ def get_visualization_trajectory(self, visualization_state):
1385
+ if visualization_state is None:
1386
+ return None
1387
+ return self.l2_condition_preprocessor.get_visualization_trajectory(visualization_state)
1388
+
1389
+ def update_rollout_visualization_state(self, visualization_state, step_idx):
1390
+ if visualization_state is None:
1391
+ return None
1392
+ return self.l2_condition_preprocessor.update_rollout_visualization_state(
1393
+ visualization_state,
1394
+ step_idx=step_idx,
1395
+ )
1396
+
1397
+ def get_rollout_visualization_trajectory(
1398
+ self,
1399
+ condition_kwargs,
1400
+ num_gen_steps,
1401
+ num_pred_frames=1,
1402
+ num_condition_frames=None,
1403
+ ):
1404
+ if not condition_kwargs:
1405
+ return None
1406
+ l2_condition_kwargs = condition_kwargs.get("_l2_condition_kwargs")
1407
+ if not l2_condition_kwargs:
1408
+ return None
1409
+ return self.l2_condition_preprocessor.get_rollout_visualization_trajectory(
1410
+ l2_condition_kwargs,
1411
+ num_gen_steps=num_gen_steps,
1412
+ num_pred_frames=num_pred_frames,
1413
+ num_condition_frames=num_condition_frames,
1414
+ )
orbis2/models/second_stage/fm_model_v2.py ADDED
@@ -0,0 +1,1319 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import os
3
+ from collections import OrderedDict
4
+ from copy import deepcopy
5
+
6
+ import pytorch_lightning as pl
7
+ import torch
8
+ import torchvision.utils as vutils
9
+ from einops import rearrange
10
+ from omegaconf import ListConfig, OmegaConf
11
+ from timm.layers.pos_embed import resample_abs_pos_embed
12
+ from torch.optim.lr_scheduler import LambdaLR
13
+ from tqdm import tqdm
14
+
15
+ from util import get_obj_from_str, instantiate_from_config
16
+ from .fm_conditions_v2 import (
17
+ ConditionPreprocessor,
18
+ L2EndpointConditionPreprocessor,
19
+ SpeedYawDirectConditionPreprocessor,
20
+ SpeedYawMovingGoalConditionPreprocessor,
21
+ )
22
+ @torch.no_grad()
23
+ def update_ema(ema_model, model, decay=0.9999):
24
+ ema_params = OrderedDict(ema_model.named_parameters())
25
+ model_params = OrderedDict(model.named_parameters())
26
+
27
+ for name, param in model_params.items():
28
+ ema_params[name].mul_(decay).add_(param.data, alpha=1 - decay)
29
+
30
+
31
+ def requires_grad(model, flag=True):
32
+ for p in model.parameters():
33
+ p.requires_grad = flag
34
+
35
+
36
+ def init_ema_model(model):
37
+ ema_model = deepcopy(model)
38
+ requires_grad(ema_model, False)
39
+ update_ema(ema_model, model, decay=0)
40
+ ema_model.eval()
41
+ return ema_model
42
+
43
+
44
+ def _extract_tokenizer_decode_samples(decoded):
45
+ if isinstance(decoded, tuple):
46
+ return decoded[0]
47
+ return decoded
48
+
49
+
50
+ def _decode_selected_if_branch(tokenizer, branch, latents):
51
+ if branch == "rec":
52
+ decoder = getattr(tokenizer, "decoder", None)
53
+ post_quant_conv = getattr(tokenizer, "post_quant_conv", None)
54
+ else:
55
+ decoder = getattr(tokenizer, "decoder_sem", None)
56
+ post_quant_conv = getattr(tokenizer, "post_quant_conv_sem", None)
57
+
58
+ if (
59
+ decoder is not None
60
+ and post_quant_conv is not None
61
+ and getattr(post_quant_conv, "in_channels", None) == latents.size(1)
62
+ ):
63
+ return _extract_tokenizer_decode_samples(decoder(post_quant_conv(latents)))
64
+
65
+ rec_channels = tokenizer.quant_conv.out_channels
66
+ sem_channels = tokenizer.quant_conv2.out_channels
67
+ if branch == "rec":
68
+ quant = (
69
+ latents,
70
+ torch.zeros(
71
+ latents.size(0),
72
+ sem_channels,
73
+ latents.size(2),
74
+ latents.size(3),
75
+ device=latents.device,
76
+ dtype=latents.dtype,
77
+ ),
78
+ )
79
+ else:
80
+ quant = (
81
+ torch.zeros(
82
+ latents.size(0),
83
+ rec_channels,
84
+ latents.size(2),
85
+ latents.size(3),
86
+ device=latents.device,
87
+ dtype=latents.dtype,
88
+ ),
89
+ latents,
90
+ )
91
+ return _extract_tokenizer_decode_samples(tokenizer.decode(quant))
92
+
93
+
94
+ class FirstStageHandler:
95
+ def __init__(self, predictor_module):
96
+ self.module = predictor_module
97
+
98
+ @property
99
+ def ae(self):
100
+ return self.module.ae
101
+
102
+ @property
103
+ def vit(self):
104
+ return self.module.vit
105
+
106
+ @property
107
+ def enc_scale(self):
108
+ return self.module.enc_scale
109
+
110
+ @property
111
+ def use_precomputed_training_inputs(self):
112
+ return self.module.use_precomputed_training_inputs
113
+
114
+ @torch.no_grad()
115
+ def encode_frames(self, images):
116
+ if self.use_precomputed_training_inputs and images.shape[-2:] == self.vit.input_size:
117
+ return images * self.enc_scale
118
+
119
+ if images.ndim == 5:
120
+ b, f, _e, _h, _w = images.size()
121
+ images = rearrange(images, "b f e h w -> (b f) e h w")
122
+ else:
123
+ b, _e, _h, _w = images.size()
124
+ f = 1
125
+
126
+ x = self.ae.encode(images)["continuous"]
127
+ x = x * self.enc_scale
128
+ return rearrange(x, "(b f) e h w -> b f e h w", b=b, f=f)
129
+
130
+ @torch.no_grad()
131
+ def decode_frames(self, x, output_device=None):
132
+ samples = []
133
+ for idx in range(x.shape[1]):
134
+ frame = x[:, idx] / self.enc_scale
135
+ frame = self.ae.post_quant_conv(frame)
136
+ frame = self.ae.decoder(frame)
137
+ if output_device is not None:
138
+ frame = frame.to(output_device)
139
+ samples.append(frame.unsqueeze(1))
140
+ return torch.cat(samples, dim=1)
141
+
142
+
143
+ class IFSelectBranchFirstStageHandler(FirstStageHandler):
144
+ """
145
+ First-stage handler that selects a single IF tokenizer branch, mirroring
146
+ `ModelIFSelectBranch` from the v1 codepath.
147
+ """
148
+
149
+ def __init__(self, predictor_module, encoder_branch="rec"):
150
+ super().__init__(predictor_module)
151
+ self.encoder_branch = self._normalize_encoder_branch(encoder_branch)
152
+ expected_channels = self._selected_branch_channels()
153
+ actual_channels = getattr(self.vit, "in_channels", None)
154
+ if actual_channels is not None and actual_channels != expected_channels:
155
+ raise ValueError(
156
+ f"generator_config.params.in_channels={actual_channels} does not match "
157
+ f"the selected {self.encoder_branch} branch channels ({expected_channels})."
158
+ )
159
+
160
+ @property
161
+ def enc_scale_dino(self):
162
+ return self.module.enc_scale_dino
163
+
164
+ def _normalize_encoder_branch(self, encoder_branch):
165
+ if isinstance(encoder_branch, int):
166
+ if encoder_branch == 0:
167
+ return "rec"
168
+ if encoder_branch == 1:
169
+ return "sem"
170
+
171
+ branch = str(encoder_branch).strip().lower()
172
+ aliases = {
173
+ "rec": "rec",
174
+ "x0": "rec",
175
+ "h": "rec",
176
+ "sem": "sem",
177
+ "x1": "sem",
178
+ "h2": "sem",
179
+ }
180
+ if branch not in aliases:
181
+ raise ValueError(
182
+ f"Unsupported encoder_branch={encoder_branch!r}. "
183
+ "Expected one of: rec, x0, h, sem, x1, h2."
184
+ )
185
+ return aliases[branch]
186
+
187
+ def _selected_branch_index(self):
188
+ return 0 if self.encoder_branch == "rec" else 1
189
+
190
+ def _selected_branch_scale(self):
191
+ return self.enc_scale if self.encoder_branch == "rec" else self.enc_scale_dino
192
+
193
+ def _selected_branch_channels(self):
194
+ if self.encoder_branch == "rec":
195
+ return self.ae.quant_conv.out_channels
196
+ return self.ae.quant_conv2.out_channels
197
+
198
+ @torch.no_grad()
199
+ def encode_frames(self, images):
200
+ if self.use_precomputed_training_inputs and images.shape[-2:] == self.vit.input_size:
201
+ return images * self._selected_branch_scale()
202
+
203
+ if images.ndim == 5:
204
+ b, f, _e, _h, _w = images.size()
205
+ images = rearrange(images, "b f e h w -> (b f) e h w")
206
+ else:
207
+ b, _e, _h, _w = images.size()
208
+ f = 1
209
+
210
+ continuous = self.ae.encode(images)["continuous"]
211
+ if not isinstance(continuous, tuple) or len(continuous) != 2:
212
+ raise ValueError(
213
+ "IFSelectBranchFirstStageHandler expects tokenizer.encode(...)[\"continuous\"] "
214
+ f"to return a 2-tuple, got {type(continuous)}."
215
+ )
216
+
217
+ x = continuous[self._selected_branch_index()] * self._selected_branch_scale()
218
+ expected_channels = self._selected_branch_channels()
219
+ if x.size(1) != expected_channels:
220
+ raise ValueError(
221
+ f"Selected {self.encoder_branch} branch has {x.size(1)} channels, "
222
+ f"expected {expected_channels}."
223
+ )
224
+
225
+ return rearrange(x, "(b f) e h w -> b f e h w", b=b, f=f)
226
+
227
+ @torch.no_grad()
228
+ def decode_frames(self, x, output_device=None):
229
+ b, f, c, _h, _w = x.size()
230
+
231
+ expected_channels = self._selected_branch_channels()
232
+ if c != expected_channels:
233
+ raise ValueError(
234
+ f"decode_frames expected {expected_channels} channels for "
235
+ f"encoder_branch={self.encoder_branch}, got {c}."
236
+ )
237
+
238
+ if output_device is None:
239
+ x = rearrange(x, "b f c h w -> (b f) c h w")
240
+ selected = x / self._selected_branch_scale()
241
+ samples = _decode_selected_if_branch(self.ae, self.encoder_branch, selected)
242
+ return rearrange(samples, "(b f) c h w -> b f c h w", b=b, f=f)
243
+
244
+ samples = []
245
+ for idx in range(f):
246
+ selected = x[:, idx] / self._selected_branch_scale()
247
+ frame = _decode_selected_if_branch(self.ae, self.encoder_branch, selected)
248
+ samples.append(frame.to(output_device).unsqueeze(1))
249
+ return torch.cat(samples, dim=1)
250
+
251
+
252
+ class FlowMatchingObjective:
253
+ def __init__(self, predictor_module, sigma_min=1e-5):
254
+ self.module = predictor_module
255
+ self.sigma_min = sigma_min
256
+
257
+ def alpha(self, t):
258
+ return 1.0 - t
259
+
260
+ def sigma(self, t):
261
+ return self.sigma_min + t * (1.0 - self.sigma_min)
262
+
263
+ def A(self, t):
264
+ return 1.0
265
+
266
+ def B(self, t):
267
+ return -(1.0 - self.sigma_min)
268
+
269
+ def sample_t(self, x):
270
+ return torch.rand((x.shape[0],), device=x.device)
271
+
272
+ def add_noise(self, x, t, noise=None):
273
+ noise = torch.randn_like(x) if noise is None else noise
274
+ if t.dim() == 2:
275
+ shape = [x.shape[0], x.shape[1]] + [1] * (x.dim() - 2)
276
+ else:
277
+ shape = [x.shape[0]] + [1] * (x.dim() - 1)
278
+ x_t = self.alpha(t).view(*shape) * x + self.sigma(t).view(*shape) * noise
279
+ return x_t, noise
280
+
281
+ def get_supervision_target(self, target, noise, t):
282
+ return self.A(t) * target + self.B(t) * noise
283
+
284
+ def compute_loss(self, pred, target):
285
+ return (pred.float() - target.float()) ** 2
286
+
287
+ def prepare_training_inputs(self, x):
288
+ raise NotImplementedError
289
+
290
+ def compute_step(self, x, frame_rate, condition_kwargs=None):
291
+ model_input, t, supervision_target, prediction_slice = self.prepare_training_inputs(x)
292
+ model_condition_kwargs = self.module.condition_preprocessor.get_model_condition_kwargs(
293
+ condition_kwargs
294
+ )
295
+ pred = self.module.vit(model_input, t, frame_rate=frame_rate, **model_condition_kwargs)
296
+ if prediction_slice is not None:
297
+ pred = pred[:, prediction_slice]
298
+ return self.compute_loss(pred, supervision_target)
299
+
300
+
301
+ class FlowMatchingObjectiveTeacherForcing(FlowMatchingObjective):
302
+ def __init__(
303
+ self,
304
+ predictor_module,
305
+ sigma_min=1e-5,
306
+ ctx_noise_aug_ratio=0.1,
307
+ ctx_noise_aug_prob=0.5,
308
+ drop_ctx_rate=0.2,
309
+ ):
310
+ super().__init__(predictor_module, sigma_min=sigma_min)
311
+ self.ctx_noise_aug_ratio = ctx_noise_aug_ratio
312
+ self.ctx_noise_aug_prob = ctx_noise_aug_prob
313
+ self.drop_ctx_rate = drop_ctx_rate
314
+
315
+ def _split_sequence(self, x):
316
+ context = x[:, :-self.module.num_pred_frames]
317
+ target = x[:, -self.module.num_pred_frames:]
318
+ return context, target
319
+
320
+ def _maybe_drop_context(self, context, target):
321
+ if context.size(1) == 0:
322
+ return context
323
+ if torch.rand(1, device=target.device) < self.drop_ctx_rate:
324
+ return context[:, :0]
325
+ return context
326
+
327
+ def _maybe_noise_context(self, context, t):
328
+ if context.size(1) == 0:
329
+ return context
330
+ if torch.rand(1, device=context.device) >= self.ctx_noise_aug_prob:
331
+ return context
332
+
333
+ mask = t >= self.ctx_noise_aug_ratio
334
+ if not mask.any():
335
+ return context
336
+
337
+ context = context.clone()
338
+ aug_noise = torch.randn_like(context)
339
+ context[mask] = context[mask] + aug_noise[mask] * self.ctx_noise_aug_ratio
340
+ return context
341
+
342
+ def _prepare_context(self, context, target, t):
343
+ if not self.module.training:
344
+ return context
345
+
346
+ context = self._maybe_drop_context(context, target)
347
+ if context.size(1) == 0:
348
+ return context
349
+ return self._maybe_noise_context(context, t)
350
+
351
+ def prepare_training_inputs(self, x):
352
+ context, target = self._split_sequence(x)
353
+ t = self.sample_t(target)
354
+ context = self._prepare_context(context, target, t)
355
+ target_t, noise = self.add_noise(target, t)
356
+
357
+ model_input = torch.cat([context, target_t], dim=1) if context.size(1) > 0 else target_t
358
+
359
+ supervision_target = self.get_supervision_target(target, noise, t)
360
+ prediction_slice = slice(-self.module.num_pred_frames, None)
361
+ model_t = self.module.sampler._build_model_t(context, target_t, t)
362
+ return model_input, model_t, supervision_target, prediction_slice
363
+
364
+
365
+ class FlowMatchingObjectiveDiffusionForcing(FlowMatchingObjective):
366
+ def sample_t(self, x):
367
+ return torch.rand(x.shape[0], x.shape[1], device=x.device)
368
+
369
+ def prepare_training_inputs(self, x):
370
+ t = self.sample_t(x)
371
+ x_t, noise = self.add_noise(x, t)
372
+ supervision_target = self.get_supervision_target(x, noise, t)
373
+ return x_t, t, supervision_target, None
374
+
375
+
376
+ class FlowMatchingSampler:
377
+ def __init__(self, predictor_module, timescale=1.0, integration_t_eps=0.0):
378
+ self.module = predictor_module
379
+ self.timescale = timescale
380
+ self.integration_t_eps = float(integration_t_eps)
381
+
382
+ def _get_net(self, sample_with_ema):
383
+ return self.module.ema_vit if sample_with_ema else self.module.vit
384
+
385
+ def _prepare_context(self, images, latent):
386
+ if images is None:
387
+ return None
388
+ if latent:
389
+ return images.clone()
390
+ return self.module.encode_frames(images)
391
+
392
+ def _default_frame_rate(self, num_samples, device):
393
+ return torch.full_like(torch.ones((num_samples,)), 5, device=device)
394
+
395
+ def _get_input_hw(self):
396
+ if isinstance(self.module.vit.input_size, (list, tuple, ListConfig)):
397
+ return self.module.vit.input_size[0], self.module.vit.input_size[1]
398
+ return self.module.vit.input_size, self.module.vit.input_size
399
+
400
+ def _build_model_inputs(self, context, target_t, t):
401
+ if context is not None and context.size(1) > 0:
402
+ model_input = torch.cat([context, target_t], dim=1)
403
+ else:
404
+ model_input = target_t
405
+ return model_input
406
+
407
+ def _build_model_t(self, context, target_t, t_scalar):
408
+ del context, target_t, t_scalar
409
+ raise NotImplementedError(
410
+ f"{self.__class__.__name__} must implement `_build_model_t`."
411
+ )
412
+
413
+ def _extract_target_prediction(self, pred):
414
+ return pred[:, -self.module.num_pred_frames :]
415
+
416
+ def _prepare_sampling_state(
417
+ self, images, latent, sample_with_ema, num_samples, frame_rate, condition_kwargs
418
+ ):
419
+ net = self._get_net(sample_with_ema)
420
+ device = next(net.parameters()).device
421
+ context = self._prepare_context(images, latent)
422
+ condition_kwargs = self.module.condition_preprocessor.prepare_condition_kwargs(
423
+ condition_kwargs,
424
+ batch_size=num_samples,
425
+ device=device,
426
+ split="sample",
427
+ )
428
+ model_condition_kwargs = self.module.condition_preprocessor.get_model_condition_kwargs(
429
+ condition_kwargs
430
+ )
431
+
432
+ if frame_rate is None:
433
+ frame_rate = self._default_frame_rate(num_samples, device)
434
+
435
+ input_h, input_w = self._get_input_hw()
436
+ target_t = torch.randn(
437
+ num_samples,
438
+ self.module.num_pred_frames,
439
+ self.module.vit.in_channels,
440
+ input_h,
441
+ input_w,
442
+ device=device,
443
+ )
444
+ return net, device, context, model_condition_kwargs, frame_rate, target_t
445
+
446
+ def _build_t_steps(self, NFE, device):
447
+ if not 0.0 <= self.integration_t_eps < 0.5:
448
+ raise ValueError(
449
+ "integration_t_eps must be in [0, 0.5), "
450
+ f"got {self.integration_t_eps}."
451
+ )
452
+ return torch.linspace(
453
+ 1.0 - self.integration_t_eps,
454
+ self.integration_t_eps,
455
+ NFE + 1,
456
+ device=device,
457
+ )
458
+
459
+ def _eval_velocity(self, net, context, target_t, t_scalar, frame_rate, model_condition_kwargs):
460
+ model_input = self._build_model_inputs(context, target_t, t_scalar)
461
+ model_t = self._build_model_t(context, target_t, t_scalar)
462
+ pred = net(model_input, t=model_t * self.timescale, frame_rate=frame_rate, **model_condition_kwargs)
463
+ return self._extract_target_prediction(pred)
464
+
465
+ def _euler_maruyama_step(self, target_t, neg_v, t_i, t_ip1, eta):
466
+ dt = t_i - t_ip1
467
+ dw = torch.randn(target_t.size(), device=target_t.device) * torch.sqrt(dt)
468
+ diffusion = dt
469
+ return target_t + neg_v * dt + eta * torch.sqrt(2 * diffusion) * dw
470
+
471
+ def _heun_step(self, net, context, target_t, t_i, t_ip1, frame_rate, model_condition_kwargs):
472
+ t_i_scalar = t_i.repeat(target_t.shape[0])
473
+ t_ip1_scalar = t_ip1.repeat(target_t.shape[0])
474
+ dt = t_i - t_ip1
475
+ v1 = self._eval_velocity(net, context, target_t, t_i_scalar, frame_rate, model_condition_kwargs)
476
+ x_pred = target_t + v1 * dt
477
+ v2 = self._eval_velocity(net, context, x_pred, t_ip1_scalar, frame_rate, model_condition_kwargs)
478
+ return target_t + 0.5 * (v1 + v2) * dt
479
+
480
+ def _snapshot_condition_kwargs(self, condition_kwargs):
481
+ if not condition_kwargs:
482
+ return {}
483
+ snapshot = {}
484
+ for key, value in condition_kwargs.items():
485
+ if torch.is_tensor(value):
486
+ snapshot[key] = value.detach().cpu().clone()
487
+ else:
488
+ snapshot[key] = value
489
+ return snapshot
490
+
491
+ @torch.no_grad()
492
+ def sample(
493
+ self,
494
+ images=None,
495
+ latent=False,
496
+ eta=0.0,
497
+ NFE=20,
498
+ sample_with_ema=True,
499
+ num_samples=8,
500
+ frame_rate=None,
501
+ condition_kwargs=None,
502
+ return_sample=False,
503
+ ):
504
+ net, device, context, model_condition_kwargs, frame_rate, target_t = self._prepare_sampling_state(
505
+ images, latent, sample_with_ema, num_samples, frame_rate, condition_kwargs
506
+ )
507
+ t_steps = self._build_t_steps(NFE, device)
508
+ for i in range(NFE):
509
+ t_scalar = t_steps[i].repeat(target_t.shape[0])
510
+ neg_v = self._eval_velocity(net, context, target_t, t_scalar, frame_rate, model_condition_kwargs)
511
+ target_t = self._euler_maruyama_step(target_t, neg_v, t_steps[i], t_steps[i + 1], eta)
512
+
513
+ if return_sample:
514
+ return target_t, self.module.decode_frames(target_t.clone())
515
+ return target_t
516
+
517
+ def _update_rollout_context(self, context, prediction):
518
+ latest = prediction[:, -self.module.num_pred_frames:]
519
+ if self.module.num_pred_frames > context.size(1):
520
+ return latest
521
+ return torch.cat([context[:, self.module.num_pred_frames :], latest], dim=1)
522
+
523
+ @torch.no_grad()
524
+ def roll_out(
525
+ self,
526
+ x_0,
527
+ num_gen_frames=25,
528
+ latent_input=True,
529
+ eta=0.0,
530
+ NFE=20,
531
+ sample_with_ema=True,
532
+ num_samples=8,
533
+ frame_rate=None,
534
+ condition_kwargs=None,
535
+ decode_device=None,
536
+ return_condition_history=False,
537
+ ):
538
+ context = x_0.clone() if latent_input else self.module.encode_frames(x_0)
539
+ all_latents = context.clone()
540
+ condition_kwargs = self.module.condition_preprocessor.prepare_condition_kwargs(
541
+ condition_kwargs,
542
+ batch_size=context.size(0),
543
+ device=context.device,
544
+ split="rollout",
545
+ )
546
+ condition_history = []
547
+
548
+ for _idx in tqdm(range(num_gen_frames), desc="Rolling out frames"):
549
+ if return_condition_history:
550
+ condition_history.append(self._snapshot_condition_kwargs(condition_kwargs))
551
+ prediction = self.sample(
552
+ images=context,
553
+ latent=True,
554
+ eta=eta,
555
+ NFE=NFE,
556
+ sample_with_ema=sample_with_ema,
557
+ num_samples=num_samples,
558
+ frame_rate=frame_rate,
559
+ condition_kwargs=condition_kwargs,
560
+ )
561
+ all_latents = torch.cat([all_latents, prediction[:, -self.module.num_pred_frames :]], dim=1)
562
+ if _idx < num_gen_frames - 1:
563
+ condition_kwargs = self.module.condition_preprocessor.update_rollout_condition_kwargs(
564
+ condition_kwargs,
565
+ prediction=prediction,
566
+ context=context,
567
+ step_idx=_idx,
568
+ )
569
+ context = self._update_rollout_context(context, prediction)
570
+
571
+ result = (all_latents, self.module.decode_frames(all_latents, output_device=decode_device))
572
+ if return_condition_history:
573
+ return result + (condition_history,)
574
+ return result
575
+
576
+
577
+ class FlowMatchingSamplerTeacherForcing(FlowMatchingSampler):
578
+ """
579
+ Inference sampler used for all v2 models.
580
+
581
+ The context frames are kept clean in `model_input`; only the target frame
582
+ block is initialized with noise and integrated during sampling.
583
+ """
584
+
585
+ def __init__(
586
+ self,
587
+ predictor_module,
588
+ timescale=1.0,
589
+ integration_t_eps=0.0,
590
+ timestep_conditioning="global",
591
+ ):
592
+ super().__init__(
593
+ predictor_module,
594
+ timescale=timescale,
595
+ integration_t_eps=integration_t_eps,
596
+ )
597
+ self.timestep_conditioning = self._normalize_timestep_conditioning(timestep_conditioning)
598
+
599
+ def _normalize_timestep_conditioning(self, timestep_conditioning):
600
+ mode = str(timestep_conditioning).strip().lower()
601
+ if mode not in {"global", "per_frame"}:
602
+ raise ValueError(
603
+ "timestep_conditioning must be one of {'global', 'per_frame'}, "
604
+ f"got {timestep_conditioning!r}."
605
+ )
606
+ return mode
607
+
608
+ def _build_model_t(self, context, target_t, t_scalar):
609
+ if self.timestep_conditioning == "global":
610
+ return t_scalar
611
+
612
+ target_t_full = t_scalar.unsqueeze(1).expand(-1, target_t.size(1))
613
+ if context is None or context.size(1) == 0:
614
+ return target_t_full
615
+ context_t = torch.zeros(
616
+ t_scalar.size(0),
617
+ context.size(1),
618
+ device=t_scalar.device,
619
+ dtype=t_scalar.dtype,
620
+ )
621
+ return torch.cat([context_t, target_t_full], dim=1)
622
+
623
+ def _validate_num_heun_steps(self, num_heun_steps, NFE, eta):
624
+ if isinstance(num_heun_steps, bool) or not isinstance(num_heun_steps, int):
625
+ raise TypeError(
626
+ f"num_heun_steps must be an int, got {type(num_heun_steps).__name__}."
627
+ )
628
+ if not 0 <= num_heun_steps <= NFE:
629
+ raise ValueError(
630
+ f"num_heun_steps must be in [0, NFE] (NFE={NFE}), got {num_heun_steps}."
631
+ )
632
+ if num_heun_steps > 0 and eta != 0.0:
633
+ raise ValueError(
634
+ "Heun sampling (num_heun_steps > 0) is deterministic-only and cannot "
635
+ f"be combined with the stochastic SDE term; got eta={eta}. "
636
+ "Set eta=0.0 when num_heun_steps > 0."
637
+ )
638
+
639
+ @torch.no_grad()
640
+ def sample(
641
+ self,
642
+ images=None,
643
+ latent=False,
644
+ eta=0.0,
645
+ NFE=20,
646
+ sample_with_ema=True,
647
+ num_samples=8,
648
+ frame_rate=None,
649
+ condition_kwargs=None,
650
+ return_sample=False,
651
+ num_heun_steps=0,
652
+ ):
653
+ self._validate_num_heun_steps(num_heun_steps, NFE, eta)
654
+ net, device, context, model_condition_kwargs, frame_rate, target_t = self._prepare_sampling_state(
655
+ images, latent, sample_with_ema, num_samples, frame_rate, condition_kwargs
656
+ )
657
+ t_steps = self._build_t_steps(NFE, device)
658
+ for i in range(NFE):
659
+ if i < num_heun_steps:
660
+ target_t = self._heun_step(
661
+ net, context, target_t, t_steps[i], t_steps[i + 1], frame_rate, model_condition_kwargs
662
+ )
663
+ else:
664
+ t_scalar = t_steps[i].repeat(target_t.shape[0])
665
+ neg_v = self._eval_velocity(net, context, target_t, t_scalar, frame_rate, model_condition_kwargs)
666
+ target_t = self._euler_maruyama_step(target_t, neg_v, t_steps[i], t_steps[i + 1], eta)
667
+
668
+ if return_sample:
669
+ return target_t, self.module.decode_frames(target_t.clone())
670
+ return target_t
671
+
672
+ @torch.no_grad()
673
+ def roll_out(
674
+ self,
675
+ x_0,
676
+ num_gen_frames=25,
677
+ latent_input=True,
678
+ eta=0.0,
679
+ NFE=20,
680
+ sample_with_ema=True,
681
+ num_samples=8,
682
+ frame_rate=None,
683
+ condition_kwargs=None,
684
+ decode_device=None,
685
+ return_condition_history=False,
686
+ num_heun_steps=0,
687
+ ):
688
+ # num_heun_steps validation happens inside self.sample() on the first
689
+ # generated block, before any expensive work in this loop.
690
+ context = x_0.clone() if latent_input else self.module.encode_frames(x_0)
691
+ all_latents = context.clone()
692
+ condition_kwargs = self.module.condition_preprocessor.prepare_condition_kwargs(
693
+ condition_kwargs,
694
+ batch_size=context.size(0),
695
+ device=context.device,
696
+ split="rollout",
697
+ )
698
+ condition_history = []
699
+
700
+ for _idx in tqdm(range(num_gen_frames), desc="Rolling out frames"):
701
+ if return_condition_history:
702
+ condition_history.append(self._snapshot_condition_kwargs(condition_kwargs))
703
+ prediction = self.sample(
704
+ images=context,
705
+ latent=True,
706
+ eta=eta,
707
+ NFE=NFE,
708
+ sample_with_ema=sample_with_ema,
709
+ num_samples=num_samples,
710
+ frame_rate=frame_rate,
711
+ condition_kwargs=condition_kwargs,
712
+ num_heun_steps=num_heun_steps,
713
+ )
714
+ all_latents = torch.cat([all_latents, prediction[:, -self.module.num_pred_frames :]], dim=1)
715
+ if _idx < num_gen_frames - 1:
716
+ condition_kwargs = self.module.condition_preprocessor.update_rollout_condition_kwargs(
717
+ condition_kwargs,
718
+ prediction=prediction,
719
+ context=context,
720
+ step_idx=_idx,
721
+ )
722
+ context = self._update_rollout_context(context, prediction)
723
+
724
+ result = (all_latents, self.module.decode_frames(all_latents, output_device=decode_device))
725
+ if return_condition_history:
726
+ return result + (condition_history,)
727
+ return result
728
+
729
+
730
+ class FlowMatchingSamplerDiffusionForcing(FlowMatchingSampler):
731
+ """
732
+ Deprecated: diffusion forcing is a training objective, not a separate
733
+ inference scheme in v2. Use FlowMatchingSamplerTeacherForcing for clean
734
+ context / noisy target sampling.
735
+ """
736
+
737
+ def __init__(self, *args, **kwargs):
738
+ raise RuntimeError(
739
+ "FlowMatchingSamplerDiffusionForcing is deprecated. "
740
+ "Use FlowMatchingSamplerTeacherForcing for inference."
741
+ )
742
+
743
+ def _build_model_t(self, context, target_t, t_scalar):
744
+ del context
745
+ return t_scalar.unsqueeze(1).expand(-1, target_t.size(1))
746
+
747
+
748
+ class PredictorModule(pl.LightningModule):
749
+ def __init__(
750
+ self,
751
+ *,
752
+ tokenizer_config,
753
+ generator_config,
754
+ objective_config=None,
755
+ sampler_config=None,
756
+ condition_preprocessor_config=None,
757
+ first_stage_handler_config=None,
758
+ adjust_lr_to_batch_size=False,
759
+ num_pred_frames=1,
760
+ warmup_steps=5000,
761
+ min_lr_multiplier=0.1,
762
+ enc_scale=4,
763
+ enc_scale_dino=3.45062,
764
+ use_precomputed_training_inputs=False,
765
+ init_weights_path=None,
766
+ allow_different_resolution_checkpoint=False,
767
+ ):
768
+ super().__init__()
769
+
770
+ self.num_pred_frames = num_pred_frames
771
+ self.enc_scale = enc_scale
772
+ self.enc_scale_dino = enc_scale_dino
773
+ self.allow_different_resolution_checkpoint = allow_different_resolution_checkpoint
774
+ self.adjust_lr_to_batch_size = adjust_lr_to_batch_size
775
+ self.warmup_steps = warmup_steps
776
+ self.min_lr_multiplier = min_lr_multiplier
777
+ self.use_precomputed_training_inputs = use_precomputed_training_inputs
778
+
779
+ self.vit = self.build_generator(generator_config)
780
+ self.ae = self.build_tokenizer(tokenizer_config)
781
+ self.ema_vit = init_ema_model(self.vit)
782
+
783
+ self.first_stage = self.build_first_stage(first_stage_handler_config)
784
+ self.condition_preprocessor = self.build_condition_preprocessor(condition_preprocessor_config)
785
+ self.objective = self.build_objective(objective_config)
786
+ self.sampler = self.build_sampler(sampler_config)
787
+
788
+ if init_weights_path is not None:
789
+ self.init_weights_from_checkpoint(init_weights_path)
790
+
791
+ def setup(self, stage=None):
792
+ """Exclude 'unused' parameters from DDP gradient reduction."""
793
+ super().setup(stage)
794
+ # EMA
795
+ if hasattr(self, "ema_vit") and self.ema_vit is not None:
796
+ self.ema_vit.requires_grad_(False)
797
+ self.ae.requires_grad_(False)
798
+
799
+ def init_weights_from_checkpoint(self, init_weights_path):
800
+ checkpoint_path = os.path.expandvars(init_weights_path)
801
+ if not os.path.exists(checkpoint_path):
802
+ raise FileNotFoundError(f"Initial weights {init_weights_path} does not exist.")
803
+
804
+ state_dict = torch.load(checkpoint_path, map_location="cpu")["state_dict"]
805
+ state_dict = self._prepare_second_stage_checkpoint(state_dict, checkpoint_path)
806
+ outcome = self.load_state_dict(state_dict, strict=False)
807
+ assert outcome.missing_keys == [], outcome.missing_keys
808
+ print(f"Loaded model from {init_weights_path}")
809
+
810
+ def build_tokenizer(self, tokenizer_config):
811
+ tokenizer_folder = os.path.expandvars(tokenizer_config.folder)
812
+ ckpt_path = tokenizer_config.ckpt_path if tokenizer_config.ckpt_path else "checkpoints/last.ckpt"
813
+
814
+ tokenizer_config = OmegaConf.load(os.path.join(tokenizer_folder, "config.yaml"))
815
+ model_cfg = OmegaConf.to_container(tokenizer_config.model, resolve=False)
816
+ for key in ("loss_config", "entropy_loss_weight_scheduler_config"):
817
+ model_cfg.get("params", {}).pop(key, None)
818
+ model_cfg.get("params", {})["distill_model_type"] = None
819
+ encoder_params = model_cfg.get("params", {}).get("encoder_config", {}).get("params", {})
820
+ if isinstance(encoder_params, dict):
821
+ encoder_params["use_pretrained_weights"] = False
822
+ model = instantiate_from_config(OmegaConf.create(model_cfg))
823
+
824
+ checkpoint_path = os.path.join(tokenizer_folder, ckpt_path)
825
+ checkpoint = torch.load(checkpoint_path, map_location="cpu", weights_only=True)["state_dict"]
826
+ checkpoint = self._prepare_tokenizer_checkpoint(model, checkpoint, tokenizer_config.model, checkpoint_path)
827
+ model.load_state_dict(checkpoint, strict=False)
828
+ model.eval()
829
+ return model
830
+
831
+ def build_generator(self, generator_config):
832
+ return instantiate_from_config(generator_config)
833
+
834
+ def _build_helper_from_config(self, config, default_target):
835
+ config = config or {"target": default_target, "params": {}}
836
+ params = dict(config.get("params", {}))
837
+ params["predictor_module"] = self
838
+ helper_cls = get_obj_from_str(config["target"])
839
+ return helper_cls(**params)
840
+
841
+ def build_objective(self, objective_config):
842
+ return self._build_helper_from_config(
843
+ objective_config,
844
+ "models.second_stage.fm_model_v2.FlowMatchingObjectiveTeacherForcing",
845
+ )
846
+
847
+ def build_sampler(self, sampler_config):
848
+ return self._build_helper_from_config(
849
+ sampler_config,
850
+ "models.second_stage.fm_model_v2.FlowMatchingSamplerTeacherForcing",
851
+ )
852
+
853
+ def build_condition_preprocessor(self, condition_preprocessor_config):
854
+ return self._build_helper_from_config(
855
+ condition_preprocessor_config,
856
+ "models.second_stage.fm_conditions_v2.ConditionPreprocessor",
857
+ )
858
+
859
+ def build_first_stage(self, first_stage_handler_config):
860
+ return self._build_helper_from_config(
861
+ first_stage_handler_config,
862
+ "models.second_stage.fm_model_v2.FirstStageHandler",
863
+ )
864
+
865
+ def _require_or_allow_resolution_mismatch(self, *, name, checkpoint_shape, model_shape, source):
866
+ if checkpoint_shape == model_shape:
867
+ return
868
+ if not self.allow_different_resolution_checkpoint:
869
+ raise ValueError(
870
+ f"{name} shape mismatch between checkpoint and current model: "
871
+ f"checkpoint={checkpoint_shape}, model={model_shape}. "
872
+ f"Source: {source}. "
873
+ "Set allow_different_resolution_checkpoint=True to adapt or skip "
874
+ "resolution-dependent positional embeddings."
875
+ )
876
+
877
+ def _prepare_tokenizer_checkpoint(self, model, checkpoint, tokenizer_model_config, checkpoint_path):
878
+ state_dict = checkpoint.copy()
879
+ target = tokenizer_model_config.get("target", "")
880
+ encoder_config = tokenizer_model_config.params.get("encoder_config")
881
+ encoder_target = encoder_config.get("target", "") if encoder_config else ""
882
+
883
+ if target != "models.first_stage.vqgan.VQModel" or encoder_target != "networks.tokenizer.pretrained_models.Encoder":
884
+ return state_dict
885
+
886
+ model_pos_embed = getattr(getattr(model.encoder, "encoder", None), "pos_embed", None)
887
+ checkpoint_key = "encoder.encoder.pos_embed"
888
+ checkpoint_pos_embed = state_dict.get(checkpoint_key)
889
+
890
+ if model_pos_embed is None or checkpoint_pos_embed is None:
891
+ return state_dict
892
+
893
+ self._require_or_allow_resolution_mismatch(
894
+ name=f"Tokenizer positional embedding ({checkpoint_key})",
895
+ checkpoint_shape=tuple(checkpoint_pos_embed.shape),
896
+ model_shape=tuple(model_pos_embed.shape),
897
+ source=checkpoint_path,
898
+ )
899
+
900
+ if checkpoint_pos_embed.shape != model_pos_embed.shape:
901
+ num_prefix_tokens = getattr(model.encoder.encoder, "num_prefix_tokens", 1)
902
+ target_grid = model.encoder.encoder.patch_embed.grid_size
903
+ state_dict[checkpoint_key] = resample_abs_pos_embed(
904
+ checkpoint_pos_embed,
905
+ new_size=target_grid,
906
+ num_prefix_tokens=num_prefix_tokens,
907
+ )
908
+
909
+ return state_dict
910
+
911
+ def _prepare_second_stage_checkpoint(self, checkpoint, checkpoint_path):
912
+ state_dict = checkpoint.copy()
913
+ for key in ("vit.pos_embed", "ema_vit.pos_embed"):
914
+ checkpoint_pos_embed = state_dict.get(key)
915
+ model_pos_embed = self.state_dict().get(key)
916
+ if checkpoint_pos_embed is None or model_pos_embed is None:
917
+ continue
918
+
919
+ self._require_or_allow_resolution_mismatch(
920
+ name=f"Second-stage positional embedding ({key})",
921
+ checkpoint_shape=tuple(checkpoint_pos_embed.shape),
922
+ model_shape=tuple(model_pos_embed.shape),
923
+ source=checkpoint_path,
924
+ )
925
+
926
+ if checkpoint_pos_embed.shape != model_pos_embed.shape:
927
+ del state_dict[key]
928
+
929
+ return state_dict
930
+
931
+ def get_warmup_scheduler(self, optimizer, warmup_steps=1, min_lr_multiplier=1.0):
932
+ batches = len(self.trainer.datamodule.train_dataloader())
933
+ steps_per_epoch = batches // self.trainer.accumulate_grad_batches
934
+ total_steps = self.trainer.max_epochs * steps_per_epoch
935
+
936
+ def lr_lambda(step):
937
+ if step < warmup_steps:
938
+ return step / warmup_steps
939
+ progress = (min(step, total_steps) - warmup_steps) / (total_steps - warmup_steps)
940
+ cosine_decay = 0.5 * (1 + math.cos(math.pi * progress))
941
+ return (1 - min_lr_multiplier) * cosine_decay + min_lr_multiplier
942
+
943
+ return LambdaLR(optimizer, lr_lambda)
944
+
945
+ def configure_optimizers(self):
946
+ params = [p for p in self.vit.parameters() if p.requires_grad]
947
+ optimizer = torch.optim.AdamW(params, lr=self.learning_rate, weight_decay=0.01)
948
+ scheduler = self.get_warmup_scheduler(optimizer, self.warmup_steps, self.min_lr_multiplier)
949
+ return [optimizer], [{"scheduler": scheduler, "interval": "step"}]
950
+
951
+ def get_input(self, batch, k):
952
+ if isinstance(batch, dict):
953
+ x = batch[k]
954
+ frame_rate = batch["frame_rate"]
955
+ else:
956
+ x = batch
957
+ frame_rate = None
958
+ assert len(x.shape) == 5 or self.use_precomputed_training_inputs, "When using images, input must be 5D tensor"
959
+ return x, frame_rate
960
+
961
+ @torch.no_grad()
962
+ def encode_frames(self, images):
963
+ return self.first_stage.encode_frames(images)
964
+
965
+ @torch.no_grad()
966
+ def decode_frames(self, x, output_device=None):
967
+ return self.first_stage.decode_frames(x, output_device=output_device)
968
+
969
+ def compute_prediction_loss(self, pred, target):
970
+ return self.objective.compute_loss(pred, target)
971
+
972
+ def log_training_losses(self, loss):
973
+ self.log("train/loss", loss.mean(), prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
974
+
975
+ def log_validation_losses(self, loss):
976
+ self.log("val/loss", loss.mean(), prog_bar=True, logger=True, on_step=True, on_epoch=True, sync_dist=True)
977
+
978
+ def log_adaln_mean_abs(self, split, condition_kwargs=None):
979
+ stats_getter = getattr(self.vit, "get_last_adaln_mean_abs", None)
980
+ if not callable(stats_getter):
981
+ return
982
+
983
+ stats = stats_getter()
984
+ if not stats:
985
+ return
986
+
987
+ aggregated = {}
988
+ for name, value in stats.items():
989
+ _, stat_name = name.split("/", 1)
990
+ aggregated.setdefault(stat_name, []).append(value)
991
+
992
+ group_masks = self._get_adaln_stat_group_masks(condition_kwargs)
993
+ for stat_name, values in aggregated.items():
994
+ values_tensor = torch.stack(values)
995
+ for group_name, mask in group_masks.items():
996
+ group_values = values_tensor if mask is None else values_tensor[:, mask]
997
+ self._log_adaln_stat_values(split, group_name, stat_name, group_values)
998
+
999
+ def _get_adaln_stat_group_masks(self, condition_kwargs):
1000
+ if not condition_kwargs or "steering" not in condition_kwargs:
1001
+ return {"all": None}
1002
+
1003
+ steering = condition_kwargs["steering"]
1004
+ if not torch.is_tensor(steering):
1005
+ return {"all": None}
1006
+
1007
+ present_mask = ~torch.isnan(steering).flatten(1).all(dim=1)
1008
+ return {
1009
+ "present": present_mask,
1010
+ "missing": ~present_mask,
1011
+ }
1012
+
1013
+ def _log_adaln_stat_values(self, split, group_name, stat_name, values):
1014
+ # Always emit exactly 2 log calls (mean + std) regardless of group size so that
1015
+ # sync_dist=True allreduces are consistent across ranks even when different ranks
1016
+ # receive batches with different steering availability (e.g. mixed-dataset training).
1017
+ values = values.reshape(-1)
1018
+ nan = torch.tensor(float("nan"), device=values.device)
1019
+ mean_val = values.mean() if values.numel() > 0 else nan
1020
+ std_val = values.std(unbiased=False) if values.numel() >= 2 else nan
1021
+
1022
+ self.log(
1023
+ f"{split}/adaln/{group_name}/{stat_name}/mean",
1024
+ mean_val,
1025
+ prog_bar=False,
1026
+ logger=True,
1027
+ on_step=True,
1028
+ on_epoch=True,
1029
+ sync_dist=True,
1030
+ )
1031
+ self.log(
1032
+ f"{split}/adaln/{group_name}/{stat_name}/std",
1033
+ std_val,
1034
+ prog_bar=False,
1035
+ logger=True,
1036
+ on_step=True,
1037
+ on_epoch=True,
1038
+ sync_dist=True,
1039
+ )
1040
+
1041
+ def log_condition_embeddings(self, split, condition_kwargs=None):
1042
+ getter = getattr(self.vit, 'get_last_condition_embeddings', None)
1043
+ if not callable(getter):
1044
+ return
1045
+ embeddings = getter()
1046
+ if not embeddings:
1047
+ return
1048
+ group_masks = self._get_adaln_stat_group_masks(condition_kwargs)
1049
+ for emb_name, embedding in embeddings.items():
1050
+ per_sample_mean_abs = embedding.abs().mean(dim=-1)
1051
+ per_sample_norm = embedding.norm(dim=-1)
1052
+ for stat_name, values in (("mean_abs", per_sample_mean_abs), ("norm", per_sample_norm)):
1053
+ for group_name, mask in group_masks.items():
1054
+ group_values = values if mask is None else values[mask]
1055
+ group_values = group_values.reshape(-1)
1056
+ # Always emit exactly 2 log calls per group so sync_dist=True allreduces
1057
+ # are consistent across ranks regardless of per-rank batch composition.
1058
+ nan = torch.tensor(float("nan"), device=group_values.device)
1059
+ mean_val = group_values.mean() if group_values.numel() > 0 else nan
1060
+ std_val = group_values.std(unbiased=False) if group_values.numel() >= 2 else nan
1061
+ self.log(
1062
+ f"{split}/cond_emb/{emb_name}/{group_name}/{stat_name}/mean",
1063
+ mean_val,
1064
+ prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True,
1065
+ )
1066
+ self.log(
1067
+ f"{split}/cond_emb/{emb_name}/{group_name}/{stat_name}/std",
1068
+ std_val,
1069
+ prog_bar=False, logger=True, on_step=True, on_epoch=True, sync_dist=True,
1070
+ )
1071
+
1072
+ def log_adaln_gradients(self):
1073
+ getter = getattr(self.vit, 'get_adaln_grad_stats', None)
1074
+ if not callable(getter):
1075
+ return
1076
+ stats = getter()
1077
+ for name, g in stats.items():
1078
+ g_flat = g.float().flatten()
1079
+ self.log(
1080
+ f"train/adaln_grad/{name}/norm",
1081
+ g_flat.norm(),
1082
+ prog_bar=False, logger=True, on_step=True, on_epoch=False, sync_dist=True,
1083
+ )
1084
+ self.log(
1085
+ f"train/adaln_grad/{name}/mean_abs",
1086
+ g_flat.abs().mean(),
1087
+ prog_bar=False, logger=True, on_step=True, on_epoch=False, sync_dist=True,
1088
+ )
1089
+
1090
+ def on_after_backward(self):
1091
+ self.log_adaln_gradients()
1092
+
1093
+ def _shared_step(self, batch, split):
1094
+ images, frame_rate = self.get_input(batch, "images")
1095
+ x = self.encode_frames(images)
1096
+ condition_kwargs = self.condition_preprocessor.get_condition_kwargs_from_batch(batch, split=split)
1097
+ loss = self.objective.compute_step(x, frame_rate, condition_kwargs=condition_kwargs)
1098
+
1099
+ if split == "train":
1100
+ self.log_training_losses(loss)
1101
+ self.log_adaln_mean_abs("train", condition_kwargs)
1102
+ self.log_condition_embeddings("train", condition_kwargs)
1103
+ return loss.mean()
1104
+
1105
+ self.log_validation_losses(loss)
1106
+ self.log_adaln_mean_abs("val", condition_kwargs)
1107
+ self.log_condition_embeddings("val", condition_kwargs)
1108
+ return loss.mean()
1109
+
1110
+ def training_step(self, batch, batch_idx):
1111
+ return self._shared_step(batch, "train")
1112
+
1113
+ def validation_step(self, batch, batch_idx):
1114
+ self._shared_step(batch, "val")
1115
+
1116
+ def _sync_ema_stream(self):
1117
+ """Block until the async EMA update finishes before reading ema_vit."""
1118
+ stream = getattr(self, "_ema_stream", None)
1119
+ if stream is not None:
1120
+ stream.synchronize()
1121
+
1122
+ def on_train_batch_end(self, outputs, batch, batch_idx):
1123
+ """Update EMA asynchronously on a separate CUDA stream."""
1124
+ if not hasattr(self, "_ema_stream"):
1125
+ self._ema_stream = torch.cuda.Stream() if torch.cuda.is_available() else None
1126
+ if self._ema_stream is not None:
1127
+ with torch.cuda.stream(self._ema_stream):
1128
+ update_ema(self.ema_vit, self.vit)
1129
+ else:
1130
+ update_ema(self.ema_vit, self.vit)
1131
+
1132
+ # def on_train_batch_end(self, outputs, batch, batch_idx):
1133
+ # update_ema(self.ema_vit, self.vit)
1134
+
1135
+ @torch.no_grad()
1136
+ def sample(
1137
+ self,
1138
+ images=None,
1139
+ latent=False,
1140
+ eta=0.0,
1141
+ NFE=20,
1142
+ sample_with_ema=True,
1143
+ num_samples=8,
1144
+ frame_rate=None,
1145
+ condition_kwargs=None,
1146
+ return_sample=False,
1147
+ ):
1148
+ if sample_with_ema:
1149
+ self._sync_ema_stream()
1150
+
1151
+ return self.sampler.sample(
1152
+ images=images,
1153
+ latent=latent,
1154
+ eta=eta,
1155
+ NFE=NFE,
1156
+ sample_with_ema=sample_with_ema,
1157
+ num_samples=num_samples,
1158
+ frame_rate=frame_rate,
1159
+ condition_kwargs=condition_kwargs,
1160
+ return_sample=return_sample,
1161
+ )
1162
+
1163
+ def _validate_rollout_context(self, x_0):
1164
+ if not isinstance(x_0, dict):
1165
+ raise TypeError(
1166
+ f"{self.__class__.__name__}.roll_out expects x_0 to be a dict, "
1167
+ f"got {type(x_0).__name__}."
1168
+ )
1169
+ if "images" not in x_0:
1170
+ raise KeyError(f"{self.__class__.__name__}.roll_out requires x_0['images'].")
1171
+
1172
+ def _move_rollout_context_to_device(self, x_0, device):
1173
+ moved = {}
1174
+ for key, value in x_0.items():
1175
+ if torch.is_tensor(value):
1176
+ moved[key] = value.to(device)
1177
+ else:
1178
+ moved[key] = value
1179
+ return moved
1180
+
1181
+ def _get_rollout_condition_frames(self, images, num_condition_frames=None):
1182
+ if num_condition_frames is None:
1183
+ num_condition_frames = getattr(self.vit, "num_context_frames", None)
1184
+ if num_condition_frames is None:
1185
+ num_condition_frames = images.size(1) - self.num_pred_frames
1186
+ num_condition_frames = int(num_condition_frames)
1187
+ if num_condition_frames <= 0:
1188
+ raise ValueError(
1189
+ f"rollout batch has {images.size(1)} frames, but "
1190
+ f"num_pred_frames={self.num_pred_frames}; expected at least one "
1191
+ "conditioning frame."
1192
+ )
1193
+ if num_condition_frames > images.size(1):
1194
+ raise ValueError(
1195
+ f"num_condition_frames={num_condition_frames} exceeds rollout "
1196
+ f"batch length {images.size(1)}."
1197
+ )
1198
+ return num_condition_frames
1199
+
1200
+ def roll_out(
1201
+ self,
1202
+ x_0,
1203
+ num_gen_frames=25,
1204
+ latent_input=True,
1205
+ eta=0.0,
1206
+ NFE=20,
1207
+ sample_with_ema=True,
1208
+ num_samples=8,
1209
+ frame_rate=None,
1210
+ condition_kwargs=None,
1211
+ decode_device=None,
1212
+ return_condition_history=False,
1213
+ num_condition_frames=None,
1214
+ ):
1215
+ device = next(self.parameters()).device
1216
+ self._validate_rollout_context(x_0)
1217
+ x_0 = self._move_rollout_context_to_device(x_0, device)
1218
+ images = x_0["images"]
1219
+ num_condition_frames = self._get_rollout_condition_frames(
1220
+ images,
1221
+ num_condition_frames=num_condition_frames,
1222
+ )
1223
+ cond_x = images[:, :num_condition_frames]
1224
+ rollout_context = dict(x_0)
1225
+ rollout_context["images"] = cond_x
1226
+
1227
+ if torch.is_tensor(frame_rate):
1228
+ frame_rate = frame_rate.to(device)
1229
+ elif frame_rate is None and torch.is_tensor(rollout_context.get("frame_rate")):
1230
+ frame_rate = rollout_context["frame_rate"]
1231
+
1232
+ if condition_kwargs is None:
1233
+ condition_kwargs = self.condition_preprocessor.get_condition_kwargs_from_batch(
1234
+ rollout_context,
1235
+ split="rollout",
1236
+ )
1237
+
1238
+ return self.sampler.roll_out(
1239
+ cond_x,
1240
+ num_gen_frames=num_gen_frames,
1241
+ latent_input=latent_input,
1242
+ eta=eta,
1243
+ NFE=NFE,
1244
+ sample_with_ema=sample_with_ema,
1245
+ num_samples=num_samples,
1246
+ frame_rate=frame_rate,
1247
+ condition_kwargs=condition_kwargs,
1248
+ decode_device=decode_device,
1249
+ return_condition_history=return_condition_history,
1250
+ )
1251
+
1252
+ @torch.no_grad()
1253
+ def log_images(self, batch, **kwargs):
1254
+ log = {}
1255
+ images, frame_rate = self.get_input(batch, "images")
1256
+ N = min(5, images.size(0))
1257
+ images = images[:N]
1258
+ condition_kwargs = self.condition_preprocessor.get_condition_kwargs_from_batch(batch, split="log_images")
1259
+ condition_kwargs = self.condition_preprocessor.slice_condition_kwargs(condition_kwargs, slice(0, N))
1260
+
1261
+ if self.use_precomputed_training_inputs and images.shape[-2:] == self.vit.input_size:
1262
+ images = self.decode_frames(images)
1263
+
1264
+ images = self.condition_preprocessor.annotate_logged_images(images, batch=batch, num_images=N)
1265
+ frame_rate = frame_rate[:N] if frame_rate is not None else None
1266
+ num_frames = images.size(1)
1267
+
1268
+ visual = [images[:, frame_idx] for frame_idx in range(num_frames)]
1269
+ visual_ema = [images[:, frame_idx] for frame_idx in range(num_frames)]
1270
+
1271
+ context = images[:, :-self.num_pred_frames] if num_frames - self.num_pred_frames > 0 else None
1272
+
1273
+ samples = self.sample(
1274
+ context,
1275
+ eta=0.0,
1276
+ NFE=30,
1277
+ sample_with_ema=False,
1278
+ num_samples=N,
1279
+ frame_rate=frame_rate,
1280
+ condition_kwargs=condition_kwargs,
1281
+ return_sample=True,
1282
+ )[1]
1283
+ for frame_idx in range(samples.size(1)):
1284
+ visual.append(samples[:, frame_idx])
1285
+
1286
+ samples_ema = self.sample(
1287
+ context,
1288
+ eta=0.0,
1289
+ NFE=30,
1290
+ sample_with_ema=True,
1291
+ num_samples=N,
1292
+ frame_rate=frame_rate,
1293
+ condition_kwargs=condition_kwargs,
1294
+ return_sample=True,
1295
+ )[1]
1296
+ for frame_idx in range(samples_ema.size(1)):
1297
+ visual_ema.append(samples_ema[:, frame_idx])
1298
+
1299
+ sampled = vutils.make_grid(torch.cat(torch.chunk(torch.cat(visual, dim=0), 4, dim=0), dim=0), nrow=N, padding=2, normalize=False)
1300
+ sampled_ema = vutils.make_grid(torch.cat(torch.chunk(torch.cat(visual_ema, dim=0), 4, dim=0), dim=0), nrow=N, padding=2, normalize=False)
1301
+
1302
+ log["sampled"] = sampled
1303
+ log["ema_sampled"] = sampled_ema
1304
+ self.vit.train()
1305
+ return log
1306
+
1307
+
1308
+ class L1L2PredL2PredictorModule(PredictorModule):
1309
+ """
1310
+ PredictorModule specialization for L1 rollout conditioned by an L2 endpoint
1311
+ predictor.
1312
+
1313
+ The base PredictorModule keeps a tensor-level rollout API with explicit
1314
+ condition_kwargs. This subclass treats rollout as a batch-level L1/L2
1315
+ operation: extract the L1 context, derive the L2 condition through the
1316
+ configured condition preprocessor, then delegate to the generic sampler.
1317
+ """
1318
+
1319
+ pass
orbis2/modules/dit.py ADDED
@@ -0,0 +1,483 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Reusable DiT building blocks needed for inference.
3
+ Extracted from networks/DiT/dit.py so the inference set avoids importing
4
+ the full training-only model classes in that file.
5
+ """
6
+ import math
7
+
8
+ import numpy as np
9
+ import torch
10
+ import torch.nn as nn
11
+ from einops import rearrange
12
+ from timm.layers.mlp import SwiGLU
13
+ from timm.models.vision_transformer import Attention, Mlp
14
+
15
+
16
+ def get_norm_layer(norm_layer):
17
+ if isinstance(norm_layer, str):
18
+ if norm_layer == 'layer_norm':
19
+ return nn.LayerNorm
20
+ elif norm_layer == 'rms_norm':
21
+ return nn.RMSNorm
22
+ else:
23
+ raise ValueError(f"Unsupported norm layer: {norm_layer}")
24
+ return norm_layer
25
+
26
+
27
+ def modulate(x, shift, scale):
28
+ n = x.ndim - shift.ndim
29
+ for _ in range(n):
30
+ shift = shift.unsqueeze(-2)
31
+ scale = scale.unsqueeze(-2)
32
+ return x * (1 + scale) + shift
33
+
34
+
35
+ def _broadcast_gate(gate, x):
36
+ n = x.ndim - gate.ndim
37
+ for _ in range(n):
38
+ gate = gate.unsqueeze(-2)
39
+ return gate
40
+
41
+
42
+ class FrequencyEncoder:
43
+ def __init__(self, embed_dim, freq_min=1, freq_max=5):
44
+ """
45
+ Deterministic frequency encoder with fixed normalization.
46
+
47
+
48
+ Args:
49
+ embed_dim (int): Dimensionality of the token embeddings.
50
+ freq_min (float): Minimum frequency value.
51
+ freq_max (float): Maximum frequency value.
52
+ """
53
+ self.embed_dim = embed_dim
54
+ self.freq_min = freq_min
55
+ self.freq_max = freq_max
56
+
57
+
58
+ def encode(self, frequencies):
59
+ """
60
+ Encodes frequencies into embeddings using sine-cosine features.
61
+
62
+
63
+ Args:
64
+ frequencies (torch.Tensor): Tensor of shape (batch_size,) containing frequencies.
65
+
66
+
67
+ Returns:
68
+ torch.Tensor: Encoded frequency embeddings of shape (batch_size, embed_dim).
69
+ """
70
+ batch_size = frequencies.size(0)
71
+
72
+
73
+ # Fixed normalization: Scale frequencies to [0, 1]
74
+ normalized_freq = (frequencies - self.freq_min) / (self.freq_max - self.freq_min)
75
+
76
+
77
+ # Generate positional features using sine and cosine
78
+ positions = torch.arange(0, self.embed_dim, dtype=torch.float32, device=frequencies.device)
79
+ scaling_factors = 1 / (10000 ** (2 * (positions // 2) / self.embed_dim))
80
+ frequency_features = normalized_freq.unsqueeze(1) * scaling_factors # Shape: (batch_size, embed_dim)
81
+
82
+
83
+ # Apply sine to even indices and cosine to odd indices
84
+ encoded_freq = torch.zeros(batch_size, self.embed_dim, device=frequencies.device)
85
+ encoded_freq[:, 0::2] = torch.sin(frequency_features[:, 0::2]) # Sine for even indices
86
+ encoded_freq[:, 1::2] = torch.cos(frequency_features[:, 1::2]) # Cosine for odd indices
87
+
88
+
89
+ return encoded_freq
90
+
91
+
92
+ def get_2d_sincos_pos_embed(embed_dim, grid_size, cls_token=False, extra_tokens=0):
93
+ """
94
+ grid_size: int of the grid height and width
95
+ return:
96
+ pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
97
+ """
98
+ grid_h = np.arange(grid_size[0], dtype=np.float32)
99
+ grid_w = np.arange(grid_size[1], dtype=np.float32)
100
+ grid = np.meshgrid(grid_w, grid_h) # here w goes first
101
+ grid = np.stack(grid, axis=0)
102
+
103
+
104
+ grid = grid.reshape([2, 1, grid_size[0], grid_size[1]])
105
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
106
+ if cls_token and extra_tokens > 0:
107
+ pos_embed = np.concatenate([np.zeros([extra_tokens, embed_dim]), pos_embed], axis=0)
108
+ return pos_embed
109
+
110
+
111
+ def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
112
+ assert embed_dim % 2 == 0
113
+
114
+
115
+ # use half of dimensions to encode grid_h
116
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
117
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
118
+
119
+
120
+ emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
121
+ return emb
122
+
123
+
124
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
125
+ """
126
+ embed_dim: output dimension for each position
127
+ pos: a list of positions to be encoded: size (M,)
128
+ out: (M, D)
129
+ """
130
+ assert embed_dim % 2 == 0
131
+ omega = np.arange(embed_dim // 2, dtype=np.float64)
132
+ omega /= embed_dim / 2.
133
+ omega = 1. / 10000**omega # (D/2,)
134
+
135
+
136
+ pos = pos.reshape(-1) # (M,)
137
+ out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
138
+
139
+
140
+ emb_sin = np.sin(out) # (M, D/2)
141
+ emb_cos = np.cos(out) # (M, D/2)
142
+
143
+
144
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
145
+ return emb
146
+
147
+
148
+ class TimestepEmbedder(nn.Module):
149
+ """
150
+ Embeds scalar timesteps into vector representations.
151
+ """
152
+ def __init__(self, hidden_size, frequency_embedding_size=256):
153
+ super().__init__()
154
+ self.mlp = nn.Sequential(
155
+ nn.Linear(frequency_embedding_size, hidden_size, bias=True),
156
+ nn.SiLU(),
157
+ nn.Linear(hidden_size, hidden_size, bias=True),
158
+ )
159
+ self.frequency_embedding_size = frequency_embedding_size
160
+
161
+
162
+ @staticmethod
163
+ def timestep_embedding(t, dim, max_period=10000):
164
+ """
165
+ Create sinusoidal timestep embeddings.
166
+ :param t: a 1-D Tensor of N indices, one per batch element.
167
+ These may be fractional.
168
+ :param dim: the dimension of the output.
169
+ :param max_period: controls the minimum frequency of the embeddings.
170
+ :return: an (N, D) Tensor of positional embeddings.
171
+ """
172
+ # https://github.com/openai/glide-text2im/blob/main/glide_text2im/nn.py
173
+ half = dim // 2
174
+ freqs = torch.exp(
175
+ -math.log(max_period) * torch.arange(start=0, end=half, dtype=torch.float32) / half
176
+ ).to(device=t.device)
177
+ args = t[:, None].float() * freqs[None]
178
+ embedding = torch.cat([torch.cos(args), torch.sin(args)], dim=-1)
179
+ if dim % 2:
180
+ embedding = torch.cat([embedding, torch.zeros_like(embedding[:, :1])], dim=-1)
181
+ return embedding
182
+
183
+
184
+ def forward(self, t):
185
+ t_freq = self.timestep_embedding(t, self.frequency_embedding_size)
186
+ t_emb = self.mlp(t_freq)
187
+ return t_emb
188
+
189
+
190
+ class STBlock(nn.Module):
191
+ # Used for temporal compression in context
192
+ def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, dropout_rate=0.0,
193
+ norm_layer=nn.LayerNorm,
194
+ mlp_block='mlp',
195
+ act_layer=lambda: nn.GELU(approximate="tanh"),
196
+ **block_kwargs):
197
+ super().__init__()
198
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
199
+ self.norm1 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
200
+ self.norm2 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
201
+ self.norm3 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
202
+ self.norm4 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
203
+ self.space_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True,
204
+ attn_drop=dropout_rate, proj_drop=dropout_rate, norm_layer=norm_layer, **block_kwargs)
205
+ self.time_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True,
206
+ attn_drop=dropout_rate, proj_drop=dropout_rate, norm_layer=norm_layer, **block_kwargs)
207
+ if mlp_block == 'mlp':
208
+ self.space_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=act_layer, norm_layer=norm_layer, drop=0)
209
+ self.time_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=act_layer, norm_layer=norm_layer, drop=0)
210
+ elif mlp_block == 'swiglu':
211
+ self.space_mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim*2)//3)
212
+ self.time_mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim*2)//3)
213
+ else:
214
+ raise NotImplementedError(f"mlp_block {mlp_block} not implemented")
215
+
216
+ def forward(self, x):
217
+ B, F, N, D = x.shape
218
+ x = rearrange(x, 'b f n d -> (b f) n d')
219
+ x = x + self.space_attn(self.norm1(x))
220
+ x = x + self.space_mlp(self.norm2(x))
221
+ x = rearrange(x, '(b f) n d -> (b n) f d', b=B, f=F, n=N)
222
+ x = x + self.time_attn(self.norm3(x))
223
+ x = x + self.time_mlp(self.norm4(x))
224
+ x = rearrange(x, '(b n) f d -> b f n d', b=B, n=N, f=F)
225
+ return x
226
+
227
+
228
+ class DiTBlock(nn.Module):
229
+ """
230
+ A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
231
+ """
232
+ def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, dropout_rate=0.0, norm_layer=nn.LayerNorm, mlp_block='mlp', **block_kwargs):
233
+ super().__init__()
234
+ if isinstance(norm_layer, str):
235
+ norm_layer = get_norm_layer(norm_layer)
236
+ self.norm1 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
237
+ self.attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True, norm_layer=norm_layer, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
238
+ self.norm2 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
239
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
240
+ if mlp_block == 'mlp':
241
+ approx_gelu = lambda: nn.GELU(approximate="tanh")
242
+ self.mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=norm_layer, drop=0)
243
+ elif mlp_block == 'swiglu':
244
+ self.mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim*2)//3, bias=True)
245
+ self.adaLN_modulation = nn.Sequential(
246
+ nn.SiLU(),
247
+ nn.Linear(hidden_size, 6 * hidden_size, bias=True)
248
+ )
249
+
250
+ def initialize_adaln_weights(self, gate_init_std=0.0):
251
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
252
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
253
+ if gate_init_std != 0.0:
254
+ hidden_size = self.adaLN_modulation[-1].out_features // 6
255
+ for gate_idx in (2, 5):
256
+ start = gate_idx * hidden_size
257
+ end = start + hidden_size
258
+ nn.init.normal_(self.adaLN_modulation[-1].weight[start:end], std=gate_init_std)
259
+
260
+ def forward(self, x, c):
261
+ shift_msa, scale_msa, gate_msa, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(6, dim=1)
262
+ x = x + gate_msa.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift_msa, scale_msa))
263
+ x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm2(x), shift_mlp, scale_mlp))
264
+ return x
265
+
266
+
267
+ class CDiTBlock(nn.Module):
268
+ """
269
+ A DiT block with cross-attention conditioning.
270
+ """
271
+ def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, norm_layer=nn.LayerNorm, mlp_block='mlp', **block_kwargs):
272
+ super().__init__()
273
+ self.norm1 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
274
+ self.attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, norm_layer=norm_layer, **block_kwargs)
275
+ self.norm2 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
276
+ self.norm_cond = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
277
+ self.cttn = nn.MultiheadAttention(hidden_size, num_heads=num_heads, add_bias_kv=True, bias=True, batch_first=True, **block_kwargs)
278
+ self.adaLN_modulation = nn.Sequential(
279
+ nn.SiLU(),
280
+ nn.Linear(hidden_size, 11 * hidden_size, bias=True)
281
+ )
282
+ self.norm3 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
283
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
284
+ if mlp_block == 'mlp':
285
+ approx_gelu = lambda: nn.GELU(approximate="tanh")
286
+ self.mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=norm_layer, drop=0)
287
+ elif mlp_block == 'swiglu':
288
+ self.mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim*2)//3, bias=True)
289
+
290
+ def initialize_adaln_weights(self, gate_init_std=0.0):
291
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
292
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
293
+ if gate_init_std != 0.0:
294
+ hidden_size = self.adaLN_modulation[-1].out_features // 11
295
+ for gate_idx in (2, 7, 10):
296
+ start = gate_idx * hidden_size
297
+ end = start + hidden_size
298
+ nn.init.normal_(self.adaLN_modulation[-1].weight[start:end], std=gate_init_std)
299
+
300
+ def forward(self, x, c, x_cond):
301
+ shift_msa, scale_msa, gate_msa, shift_ca_xcond, scale_ca_xcond, shift_ca_x, scale_ca_x, gate_ca_x, shift_mlp, scale_mlp, gate_mlp = self.adaLN_modulation(c).chunk(11, dim=1)
302
+ x = x + gate_msa.unsqueeze(1) * self.attn(modulate(self.norm1(x), shift_msa, scale_msa))
303
+ x_cond_norm = modulate(self.norm_cond(x_cond), shift_ca_xcond, scale_ca_xcond)
304
+ x = x + gate_ca_x.unsqueeze(1) * self.cttn(query=modulate(self.norm2(x), shift_ca_x, scale_ca_x), key=x_cond_norm, value=x_cond_norm, need_weights=False)[0]
305
+ x = x + gate_mlp.unsqueeze(1) * self.mlp(modulate(self.norm3(x), shift_mlp, scale_mlp))
306
+ return x
307
+
308
+
309
+ class STDiTBlock(nn.Module):
310
+ """
311
+ A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
312
+ """
313
+ def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, dropout_rate=0.0, causal_time_attn=False, modulate_time_attn=False, norm_layer=nn.LayerNorm, mlp_block='mlp', **block_kwargs):
314
+ super().__init__()
315
+ self.norm1 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
316
+ self.space_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True, norm_layer=norm_layer, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
317
+ self.time_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True, norm_layer=norm_layer, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
318
+ self.norm2 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
319
+ self.norm3 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
320
+ self.norm4 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
321
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
322
+
323
+ if mlp_block == 'mlp':
324
+ approx_gelu = lambda: nn.GELU(approximate="tanh")
325
+ self.space_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=None, drop=0)
326
+ self.time_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=None, drop=0)
327
+ elif mlp_block == 'swiglu':
328
+ self.space_mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim*2)//3)
329
+ self.time_mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim*2)//3)
330
+ else:
331
+ raise NotImplementedError(f"mlp_block {mlp_block} not implemented")
332
+
333
+ self.adaLN_modulation = nn.Sequential(
334
+ nn.SiLU(),
335
+ nn.Linear(hidden_size, 9 * hidden_size, bias=True)
336
+ )
337
+ self.causal_time_attn = causal_time_attn
338
+ self.modulate_time_attn = modulate_time_attn
339
+ self.layer_idx = block_kwargs.get("layer_idx")
340
+ self.log_adaln_mean_abs = False
341
+ self._last_adaln_mean_abs = None
342
+
343
+ if modulate_time_attn:
344
+ self.adaLN_time_attn_modulation = nn.Sequential(
345
+ nn.SiLU(),
346
+ nn.Linear(hidden_size, 3 * hidden_size, bias=True)
347
+ )
348
+ self.norm_time_attn = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
349
+ # initialize
350
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].weight, 0)
351
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].bias, 0)
352
+ else:
353
+ self.norm_time_attn = nn.Identity()
354
+
355
+ def initialize_adaln_weights(self, gate_init_std=0.0):
356
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
357
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
358
+ if gate_init_std != 0.0:
359
+ hidden_size = self.adaLN_modulation[-1].out_features // 9
360
+ for gate_idx in (2, 5, 8):
361
+ start = gate_idx * hidden_size
362
+ end = start + hidden_size
363
+ nn.init.normal_(self.adaLN_modulation[-1].weight[start:end], std=gate_init_std)
364
+
365
+ if hasattr(self, "adaLN_time_attn_modulation"):
366
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].weight, 0)
367
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].bias, 0)
368
+ if gate_init_std != 0.0:
369
+ hidden_size = self.adaLN_time_attn_modulation[-1].out_features // 3
370
+ start = 2 * hidden_size
371
+ end = start + hidden_size
372
+ nn.init.normal_(self.adaLN_time_attn_modulation[-1].weight[start:end], std=gate_init_std)
373
+
374
+ def _collect_adaln_mean_abs(self, **tensors):
375
+ self._last_adaln_mean_abs = {
376
+ name: tensor.detach().abs().flatten(1).mean(dim=1)
377
+ for name, tensor in tensors.items()
378
+ }
379
+
380
+ def forward(self, x, c):
381
+ B, F, N, D = x.shape
382
+
383
+ # chunk into 9 [B, C] vectors
384
+ (shift_msa, scale_msa, gate_msa,
385
+ shift_mlp_s, scale_mlp_s, gate_mlp_s,
386
+ shift_mlp_t, scale_mlp_t, gate_mlp_t) = self.adaLN_modulation(c).chunk(9, dim=-1)
387
+
388
+ x_modulated = modulate(self.norm1(x), shift_msa, scale_msa)
389
+ x_modulated = rearrange(x_modulated, 'b f n d -> (b f) n d', b=B, f=F)
390
+ x_ = self.space_attn(x_modulated)
391
+ x_ = rearrange(x_, '(b f) n d -> b f n d', b=B, f=F)
392
+ x = x + _broadcast_gate(gate_msa, x) * x_
393
+
394
+ x_modulated = modulate(self.norm2(x), shift_mlp_s, scale_mlp_s)
395
+ x = x + _broadcast_gate(gate_mlp_s, x) * self.space_mlp(x_modulated)
396
+
397
+ # — temporal attention path —
398
+ if self.modulate_time_attn:
399
+ shift_mta, scale_mta, gate_mta = self.adaLN_time_attn_modulation(c).chunk(3, dim=-1)
400
+ else:
401
+ shift_mta, scale_mta, gate_mta = torch.zeros_like(shift_mlp_t), torch.zeros_like(scale_mlp_t), torch.ones_like(gate_mlp_t)
402
+
403
+ if self.log_adaln_mean_abs:
404
+ self._collect_adaln_mean_abs(
405
+ msa_shift=shift_msa,
406
+ msa_scale=scale_msa,
407
+ msa_gate=gate_msa,
408
+ mlp_s_shift=shift_mlp_s,
409
+ mlp_s_scale=scale_mlp_s,
410
+ mlp_s_gate=gate_mlp_s,
411
+ mta_shift=shift_mta,
412
+ mta_scale=scale_mta,
413
+ mta_gate=gate_mta,
414
+ mlp_t_shift=shift_mlp_t,
415
+ mlp_t_scale=scale_mlp_t,
416
+ mlp_t_gate=gate_mlp_t,
417
+ )
418
+
419
+ x_modulated = modulate(self.norm_time_attn(x), shift_mta, scale_mta)
420
+ x_modulated = rearrange(x_modulated, 'b f n d -> (b n) f d', b=B, f=F, n=N)
421
+ time_attn_mask = torch.tril(torch.ones(F, F, device=x.device)) if self.causal_time_attn else None
422
+ x_ = self.time_attn(x_modulated, attn_mask=time_attn_mask)
423
+ x_ = rearrange(x_, '(b n) f d -> b f n d', b=B, n=N, f=F)
424
+ x = x + _broadcast_gate(gate_mta, x) * x_
425
+
426
+ x_modulated = modulate(self.norm3(x), shift_mlp_t, scale_mlp_t)
427
+ x = x + _broadcast_gate(gate_mlp_t, x) * self.time_mlp(x_modulated)
428
+
429
+ return x
430
+
431
+
432
+ class FinalLayer(nn.Module):
433
+ """
434
+ The final layer of DiT.
435
+ """
436
+ def __init__(self, hidden_size, patch_size, out_channels, norm_layer=nn.LayerNorm, act_layer=nn.SiLU):
437
+ super().__init__()
438
+ self.norm_final = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
439
+ self.linear = nn.Linear(hidden_size, patch_size * patch_size * out_channels, bias=True)
440
+ self.adaLN_modulation = nn.Sequential(
441
+ act_layer(),
442
+ nn.Linear(hidden_size, 2 * hidden_size, bias=True)
443
+ )
444
+
445
+
446
+ def forward(self, x, c):
447
+ shift, scale = self.adaLN_modulation(c).chunk(2, dim=-1)
448
+ x = modulate(self.norm_final(x), shift, scale)
449
+ x = self.linear(x)
450
+ return x
451
+
452
+
453
+ class STDiTBlockWithSpatialL2(STDiTBlock):
454
+ """
455
+ STDiTBlock augmented with per-token gated spatial conditioning from L2 tokens.
456
+
457
+ z_spatial (B, F, N, D) → (shift_s, scale_s, gate_s) per token via a small MLP.
458
+ Applied as an additive gated adaLN before the standard block operations:
459
+ x = x + gate_s * modulate(spatial_norm(x), shift_s, scale_s)
460
+
461
+ Zero-init of spatial_to_mod guarantees a no-op at the start of training,
462
+ so pre-trained STDiTDF weights can be fine-tuned without disruption.
463
+ """
464
+
465
+ def __init__(self, hidden_size, num_heads, norm_layer=nn.LayerNorm, **kwargs):
466
+ super().__init__(hidden_size, num_heads, norm_layer=norm_layer, **kwargs)
467
+ self.spatial_norm = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
468
+ self.spatial_to_mod = nn.Sequential(
469
+ nn.SiLU(),
470
+ nn.Linear(hidden_size, 3 * hidden_size, bias=True),
471
+ )
472
+ # Small weight init so shift/scale start as genuine L2-dependent perturbations.
473
+ # Gate bias initialised to 2.0 → sigmoid(2.0) ≈ 0.88, so the gate starts open
474
+ # and the model must learn to close it rather than having to learn to open it.
475
+ nn.init.normal_(self.spatial_to_mod[-1].weight, std=0.02)
476
+ nn.init.zeros_(self.spatial_to_mod[-1].bias)
477
+ self.spatial_to_mod[-1].bias.data[2 * hidden_size:].fill_(2.0)
478
+
479
+ def forward(self, x, c, z_spatial):
480
+ # z_spatial: (B, F, N, D) — pre-computed L2 spatial tokens (patchified + position)
481
+ shift_s, scale_s, gate_s = self.spatial_to_mod(z_spatial).chunk(3, dim=-1)
482
+ x = x + torch.sigmoid(gate_s) * modulate(self.spatial_norm(x), shift_s, scale_s)
483
+ return super().forward(x, c)
orbis2/modules/quantize.py ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import numpy as np
2
+ import torch
3
+ import torch.nn as nn
4
+ import torch.nn.functional as F
5
+ from einops import rearrange, reduce
6
+
7
+
8
+ def log(t, eps=1e-5):
9
+ return t.clamp(min=eps).log()
10
+
11
+
12
+ def entropy(prob):
13
+ return (-prob * log(prob)).sum(dim=-1)
14
+
15
+
16
+ class VectorQuantizer(nn.Module):
17
+ def __init__(
18
+ self,
19
+ n_e,
20
+ e_dim,
21
+ beta,
22
+ normalize_embedding,
23
+ remap=None,
24
+ unknown_index="random",
25
+ sane_index_shape=False,
26
+ legacy=True,
27
+ diversity_gamma=1.0,
28
+ frac_per_sample_entropy=1.0,
29
+ token_noise=0.0,
30
+ ):
31
+ super().__init__()
32
+ self.n_e = n_e
33
+ self.e_dim = e_dim
34
+ self.beta = beta
35
+ self.legacy = legacy
36
+ self.normalize_embedding = normalize_embedding
37
+ self.diversity_gamma = diversity_gamma
38
+ self.frac_per_sample_entropy = frac_per_sample_entropy
39
+ self.token_noise = token_noise
40
+ self.sane_index_shape = sane_index_shape
41
+
42
+ # Codebook
43
+ self.embedding = nn.Embedding(n_e, e_dim)
44
+ self.embedding.weight.data.uniform_(-1.0 / n_e, 1.0 / n_e)
45
+
46
+ if self.normalize_embedding:
47
+ self.embedding.weight.data = F.normalize(self.embedding.weight.data, dim=1)
48
+
49
+ # Optional remapping
50
+ self.remap = remap
51
+ if remap is not None:
52
+ self.register_buffer("used", torch.tensor(np.load(remap)))
53
+ self.re_embed = self.used.shape[0]
54
+ self.unknown_index = unknown_index
55
+ if unknown_index == "extra":
56
+ self.unknown_index = self.re_embed
57
+ self.re_embed += 1
58
+ print(
59
+ f"Remapping {n_e} indices to {self.re_embed} indices. "
60
+ f"Using {self.unknown_index} for unknown indices."
61
+ )
62
+ else:
63
+ self.re_embed = n_e
64
+
65
+ def remap_to_used(self, indices):
66
+ ishape = indices.shape
67
+ indices = indices.view(ishape[0], -1)
68
+ used = self.used.to(indices)
69
+ match = (indices[:, :, None] == used[None, None, :]).long()
70
+ new = match.argmax(-1)
71
+ unknown = match.sum(2) < 1
72
+ if self.unknown_index == "random":
73
+ new[unknown] = torch.randint(0, self.re_embed, size=new[unknown].shape).to(indices.device)
74
+ else:
75
+ new[unknown] = self.unknown_index
76
+ return new.view(ishape)
77
+
78
+ def unmap_to_all(self, indices):
79
+ ishape = indices.shape
80
+ indices = indices.view(ishape[0], -1)
81
+ used = self.used.to(indices)
82
+ if self.re_embed > used.shape[0]:
83
+ indices[indices >= used.shape[0]] = 0
84
+ gathered = torch.gather(used.expand(indices.shape[0], -1), 1, indices)
85
+ return gathered.view(ishape)
86
+
87
+ def entropy_loss(self, distances, inv_temperature=100.0):
88
+ prob = (-distances * inv_temperature).softmax(dim=-1)
89
+
90
+ if self.frac_per_sample_entropy < 1.0:
91
+ num_tokens = prob.shape[0]
92
+ sample_size = int(num_tokens * self.frac_per_sample_entropy)
93
+ mask = torch.randperm(num_tokens, device=prob.device)[:sample_size]
94
+ per_sample_probs = prob[mask]
95
+ else:
96
+ per_sample_probs = prob
97
+
98
+ per_sample_entropy = entropy(per_sample_probs).mean()
99
+ avg_prob = reduce(per_sample_probs, "... d -> d", "mean")
100
+ codebook_entropy = entropy(avg_prob).mean()
101
+
102
+ return per_sample_entropy - self.diversity_gamma * codebook_entropy
103
+
104
+ def forward(self, z, temp=None, rescale_logits=False, return_logits=False):
105
+ assert temp in (None, 1.0)
106
+ assert not rescale_logits and not return_logits
107
+
108
+ if self.normalize_embedding:
109
+ self.embedding.weight.data = F.normalize(self.embedding.weight.data, dim=1)
110
+
111
+ # Flatten input
112
+ z = rearrange(z, "b c h w -> b h w c").contiguous()
113
+ z_flat = z.view(-1, self.e_dim)
114
+
115
+ # Compute distances
116
+ e = self.embedding.weight
117
+ d = (
118
+ torch.sum(z_flat ** 2, dim=1, keepdim=True)
119
+ + torch.sum(e ** 2, dim=1)
120
+ - 2 * torch.einsum("bd,dn->bn", z_flat, e.T)
121
+ )
122
+
123
+ min_indices = torch.argmin(d, dim=1)
124
+
125
+ # Optional token noise
126
+ if self.token_noise > 0.0 and self.training:
127
+ noise_mask = torch.rand_like(min_indices.float()) < self.token_noise
128
+ rand_indices = torch.randint(0, self.n_e, min_indices.shape, device=z.device)
129
+ min_indices[noise_mask] = rand_indices[noise_mask]
130
+
131
+ z_q = self.embedding(min_indices).view_as(z)
132
+
133
+ # Compute VQ loss
134
+ if self.legacy:
135
+ loss = F.mse_loss(z_q.detach(), z) + self.beta * F.mse_loss(z_q, z.detach())
136
+ else:
137
+ loss = self.beta * F.mse_loss(z_q.detach(), z) + F.mse_loss(z_q, z.detach())
138
+
139
+ # Optional entropy loss
140
+ entropy_aux = self.entropy_loss(d) if self.training else None
141
+
142
+ # Straight-through estimator
143
+ z_q = z + (z_q - z).detach()
144
+
145
+ # Reshape to original
146
+ z_q = rearrange(z_q, "b h w c -> b c h w")
147
+ z = rearrange(z, "b h w c -> b c h w")
148
+
149
+ # Remap if needed
150
+ if self.remap is not None:
151
+ min_indices = min_indices.view(z.shape[0], -1)
152
+ min_indices = self.remap_to_used(min_indices).view(-1, 1)
153
+
154
+ if self.sane_index_shape:
155
+ min_indices = min_indices.view(z_q.shape[0], z_q.shape[2], z_q.shape[3])
156
+
157
+ return {
158
+ "quantized": z_q,
159
+ "quantization_loss": loss,
160
+ "entropy_loss": entropy_aux,
161
+ "indices": min_indices,
162
+ }
163
+
164
+ def get_codebook_entry(self, indices, shape):
165
+ if self.remap is not None:
166
+ indices = indices.view(shape[0], -1)
167
+ indices = self.unmap_to_all(indices).view(-1)
168
+
169
+ z_q = self.embedding(indices)
170
+
171
+ if shape is not None:
172
+ z_q = z_q.view(shape).permute(0, 3, 1, 2).contiguous()
173
+
174
+ return z_q
orbis2/modules/steering.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import torch.nn as nn
3
+
4
+
5
+ def _make_scalar_embedder(hidden_size):
6
+ embedder = nn.Sequential(
7
+ nn.Linear(1, hidden_size, bias=True),
8
+ nn.SiLU(),
9
+ nn.Linear(hidden_size, hidden_size, bias=True),
10
+ )
11
+ nn.init.normal_(embedder[0].weight, std=0.02)
12
+ nn.init.normal_(embedder[0].bias, std=0.02)
13
+ nn.init.normal_(embedder[2].weight, std=0.02)
14
+ nn.init.normal_(embedder[2].bias, std=0.02)
15
+ return embedder
16
+
17
+
18
+ class LinearSteeringEmbedder(torch.nn.Module):
19
+ """
20
+ Generic linear embedding layer for steering signals. When steering is missing, outputs a fixed zero embedding.
21
+ """
22
+ def __init__(self, num_input_features, hidden_size, learnable_no_value_embedding=False):
23
+ super().__init__()
24
+ self.embedders = nn.ModuleList()
25
+ self.register_buffer("no_value_embeddings", torch.zeros(num_input_features, hidden_size))
26
+ if learnable_no_value_embedding:
27
+ self.no_value_embeddings = nn.Parameter(torch.zeros(num_input_features, hidden_size))
28
+ for _i in range(num_input_features):
29
+ self.embedders.append(_make_scalar_embedder(hidden_size))
30
+
31
+ def forward(self, steering):
32
+ squeeze_k = False
33
+ if steering.ndim == 2:
34
+ steering = steering.unsqueeze(1)
35
+ squeeze_k = True
36
+ elif steering.ndim != 3:
37
+ raise ValueError(f"Expected steering to have shape [B, D] or [B, K, D], got {tuple(steering.shape)}")
38
+
39
+ _b, _k, num_features = steering.shape
40
+ assert num_features == len(self.embedders), f"Expected {len(self.embedders)} features, but got {num_features}"
41
+ embeddings = []
42
+ for i in range(num_features):
43
+ feature = steering[:, :, [i]]
44
+ missing = torch.isnan(feature).squeeze(-1).unsqueeze(-1)
45
+ embedding = self.embedders[i](torch.nan_to_num(feature, nan=0.0))
46
+ if missing.any():
47
+ embedding = torch.where(missing, self.no_value_embeddings[i].to(embedding.dtype).view(1, 1, -1), embedding)
48
+ embeddings.append(embedding)
49
+ steering_embedding = torch.stack(embeddings, dim=1).sum(dim=1)
50
+ if squeeze_k:
51
+ return steering_embedding[:, 0]
52
+ return steering_embedding
orbis2/networks/DiT/dit.py ADDED
@@ -0,0 +1,1026 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+
4
+
5
+ # This source code is licensed under the license found in the
6
+ # LICENSE file in the root directory of this source tree.
7
+ # --------------------------------------------------------
8
+ # References:
9
+ # GLIDE: https://github.com/openai/glide-text2im
10
+ # MAE: https://github.com/facebookresearch/mae/blob/main/models_mae.py
11
+ # --------------------------------------------------------
12
+ import math
13
+ import numpy as np
14
+
15
+
16
+ from timm.layers.mlp import SwiGLU
17
+ import torch
18
+ import torch.nn as nn
19
+ import torch.nn.functional as F
20
+ import torch.utils.checkpoint as checkpoint
21
+ from torch import nn
22
+
23
+
24
+ from einops import rearrange
25
+ from timm.models.vision_transformer import PatchEmbed, Attention, Mlp
26
+ from omegaconf import ListConfig
27
+
28
+ from ..swin.swin_free_aspect_ratio import SwinTransformerBlock, SwinAttention
29
+
30
+
31
+
32
+ from modules.dit import (
33
+ get_norm_layer, modulate, FrequencyEncoder,
34
+ get_2d_sincos_pos_embed, get_2d_sincos_pos_embed_from_grid,
35
+ get_1d_sincos_pos_embed_from_grid, TimestepEmbedder,
36
+ STBlock, DiTBlock, CDiTBlock,
37
+ STDiTBlock, FinalLayer, STDiTBlockWithSpatialL2,
38
+ )
39
+
40
+
41
+ class Lambda(nn.Module):
42
+ def __init__(self, func):
43
+ super().__init__()
44
+ self.func = func
45
+
46
+
47
+ def forward(self, x):
48
+ return self.func(x)
49
+
50
+ #################################################################################
51
+ # Cross Attention #
52
+ #################################################################################
53
+
54
+
55
+ class CrossAttention(nn.Module):
56
+ """
57
+ Cross-attention mechanism that computes attention between target (x) and context (y).
58
+ Args:
59
+ dim (int): Input dimension.
60
+ num_heads (int): Number of attention heads.
61
+ qkv_bias (bool): Whether to include bias terms.
62
+ attn_drop (float): Dropout rate for attention weights.
63
+ proj_drop (float): Dropout rate after projection.
64
+ """
65
+ def __init__(self, dim, num_heads=8, qkv_bias=True, qk_norm=True, attn_drop=0.0, proj_drop=0.0, norm_layer=nn.LayerNorm):
66
+ super(CrossAttention, self).__init__()
67
+ self.num_heads = num_heads
68
+ self.head_dim = dim // num_heads
69
+ self.scale = self.head_dim ** -0.5
70
+
71
+
72
+ self.q_proj = nn.Linear(dim, dim, bias=qkv_bias)
73
+ self.k_proj = nn.Linear(dim, dim, bias=qkv_bias)
74
+ self.v_proj = nn.Linear(dim, dim, bias=qkv_bias)
75
+ self.proj = nn.Linear(dim, dim)
76
+ self.attn_drop = nn.Dropout(attn_drop)
77
+ self.proj_drop = nn.Dropout(proj_drop)
78
+ self.qk_norm = qk_norm
79
+ if qk_norm:
80
+ self.q_norm = norm_layer(self.head_dim, eps=1e-6, elementwise_affine=False, bias=False)
81
+ self.k_norm = norm_layer(self.head_dim, eps=1e-6, elementwise_affine=False, bias=False)
82
+
83
+
84
+ def forward(self, x, y):
85
+ B, N, C = x.shape
86
+ B, M, _ = y.shape
87
+
88
+
89
+ q = self.q_proj(x).reshape(B, N, self.num_heads, self.head_dim).transpose(1, 2)
90
+ k = self.k_proj(y).reshape(B, M, self.num_heads, self.head_dim).transpose(1, 2)
91
+ v = self.v_proj(y).reshape(B, M, self.num_heads, self.head_dim).transpose(1, 2)
92
+ if self.qk_norm:
93
+ q, k = self.q_norm(q), self.k_norm(k)
94
+ attn_output = F.scaled_dot_product_attention(q, k, v, dropout_p=self.attn_drop.p if self.training else 0.0)
95
+ attn_output = attn_output.transpose(1, 2).reshape(B, N, C)
96
+ return self.proj_drop(self.proj(attn_output))
97
+
98
+
99
+
100
+
101
+
102
+
103
+
104
+
105
+ #################################################################################
106
+ # Core DiT Model #
107
+ #################################################################################
108
+
109
+
110
+
111
+ class STDiTBlock_tmpadaLN(nn.Module):
112
+ """
113
+ A DiT block with adaptive layer norm zero (adaLN-Zero) conditioning.
114
+ """
115
+ def __init__(self, hidden_size, num_heads, mlp_ratio=4.0, dropout_rate=0.0, causal_time_attn=False, **block_kwargs):
116
+ raise NotImplementedError("This is a deprecated version of STDiTBlock with temporary adaLN for time attention. Use STDiTBlock instead.")
117
+ super().__init__()
118
+ self.norm1 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
119
+ self.space_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True, norm_layer=nn.LayerNorm, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
120
+ self.time_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True, norm_layer=nn.LayerNorm, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
121
+ self.norm2 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
122
+ self.norm3 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
123
+ self.norm4 = nn.LayerNorm(hidden_size, elementwise_affine=False, eps=1e-6)
124
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
125
+ approx_gelu = lambda: nn.GELU(approximate="tanh")
126
+ self.space_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, drop=0)
127
+ self.time_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, drop=0)
128
+ self.adaLN_modulation = nn.Sequential(
129
+ nn.SiLU(),
130
+ nn.Linear(hidden_size, 12 * hidden_size, bias=True)
131
+ )
132
+ self.causal_time_attn = causal_time_attn
133
+
134
+
135
+ def forward(self, x, c):
136
+ B, F, N, D = x.shape
137
+
138
+
139
+ # chunk into 12 [B, C] vectors
140
+ (shift_msa_s, scale_msa_s, gate_msa_s,
141
+ shift_msa_t, scale_msa_t, gate_msa_t,
142
+ shift_mlp_s, scale_mlp_s, gate_mlp_s,
143
+ shift_mlp_t, scale_mlp_t, gate_mlp_t) = self.adaLN_modulation(c).chunk(12, dim=1)
144
+
145
+
146
+ x_modulated = modulate(self.norm1(x), shift_msa_s, scale_msa_s)
147
+ x_modulated = rearrange(x_modulated, 'b f n d -> (b f) n d', b=B, f=F)
148
+ x_ = self.space_attn(x_modulated)
149
+ x = x + gate_msa_s.unsqueeze(1).unsqueeze(1) * rearrange(x_, '(b f) n d -> b f n d', b=B, f=F)
150
+
151
+
152
+ x_modulated = modulate(self.norm2(x), shift_mlp_s, scale_mlp_s)
153
+ x = x + gate_mlp_s.unsqueeze(1).unsqueeze(1) * self.space_mlp(x_modulated)
154
+
155
+
156
+ # — temporal attention path —
157
+ x_modulated = modulate(self.norm3(x), shift_msa_t, scale_msa_t)
158
+ x_modulated = rearrange(x_modulated, 'b f n d -> (b n) f d', b=B, f=F, n=N)
159
+ time_attn_mask = torch.tril(torch.ones(F, F, device=x.device)) if self.causal_time_attn else None
160
+ x_ = self.time_attn(x_modulated, attn_mask=time_attn_mask)
161
+ x = x + gate_msa_t.unsqueeze(1).unsqueeze(1) * rearrange(x_, '(b n) f d -> b f n d', b=B, f=F)
162
+ x_modulated = modulate(self.norm4(x), shift_mlp_t, scale_mlp_t)
163
+ x = x + gate_mlp_t.unsqueeze(1).unsqueeze(1) * self.time_mlp(x_modulated)
164
+ return x
165
+
166
+
167
+ class SwinSTDiTBlock(STDiTBlock):
168
+ def __init__(self, hidden_size, num_heads, input_shape, layer_idx, mlp_ratio=4.0, window_size=[6, 4], dropout_rate=0.0,
169
+ causal_time_attn=False, modulate_time_attn=False,
170
+ norm_layer=nn.LayerNorm, mlp_block='mlp', **block_kwargs):
171
+ super().__init__(hidden_size=hidden_size, num_heads=num_heads, mlp_ratio=mlp_ratio, dropout_rate=dropout_rate,
172
+ causal_time_attn=causal_time_attn, modulate_time_attn=modulate_time_attn, mlp_block=mlp_block)
173
+ self.norm1 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
174
+ self.space_attn = SwinTransformerBlock(
175
+ hidden_size,
176
+ input_resolution=input_shape,
177
+ num_heads=num_heads,
178
+ window_size=window_size,
179
+ shift_size=(0,0) if (layer_idx % 2 == 0) else [ws//2 for ws in window_size],
180
+ qk_norm=False,
181
+ mlp_ratio=mlp_ratio,
182
+ drop=dropout_rate,
183
+ attn_drop=dropout_rate,
184
+ norm_layer=norm_layer,
185
+ mlp_block=mlp_block,
186
+ **block_kwargs
187
+ )
188
+
189
+
190
+ class SwinSTDiTBlockNoExtraMLP(STDiTBlock):
191
+ def __init__(self, hidden_size, num_heads, input_shape, layer_idx, mlp_ratio=4.0, window_size=[6, 4], dropout_rate=0.0, norm_layer=nn.LayerNorm, mlp_block='mlp', **block_kwargs):
192
+ super().__init__(hidden_size, num_heads, input_shape, layer_idx, mlp_ratio, dropout_rate, norm_layer=norm_layer, mlp_block=mlp_block, **block_kwargs)
193
+ self.space_attn = SwinAttention(
194
+ hidden_size,
195
+ input_resolution=input_shape,
196
+ num_heads=num_heads,
197
+ window_size=window_size,
198
+ shift_size=(0,0) if (layer_idx % 2 == 0) else [ws//2 for ws in window_size],
199
+ proj_drop=dropout_rate,
200
+ attn_drop=dropout_rate,
201
+ qk_norm=True,
202
+ norm_layer=norm_layer,
203
+ **block_kwargs,
204
+ )
205
+
206
+
207
+
208
+ class STDiTBlockWithRegisters(nn.Module):
209
+ def __init__(
210
+ self,
211
+ hidden_size,
212
+ num_heads,
213
+ mlp_ratio=4.0,
214
+ dropout_rate=0.0,
215
+ causal_time_attn=False,
216
+ modulate_time_attn=True,
217
+ norm_layer=nn.LayerNorm,
218
+ mlp_block='mlp',
219
+ num_reg_tokens=1, # must be > 0
220
+ **block_kwargs
221
+ ):
222
+ super().__init__()
223
+ if num_reg_tokens <= 0:
224
+ raise ValueError("STDiTBlockWithRegisters now assumes num_reg_tokens > 0.")
225
+ assert modulate_time_attn, "STDiTBlockWithRegisters currently requires modulate_time_attn=True."
226
+ self.num_reg_tokens = num_reg_tokens
227
+
228
+ # norms...
229
+ self.norm1 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
230
+ self.norm2 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
231
+ self.norm3 = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
232
+ self.norm_reg_attn_s = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
233
+ self.norm_reg_s = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
234
+ self.norm_reg_attn_t = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
235
+ self.norm_reg_t = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
236
+
237
+ self.space_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True,
238
+ norm_layer=norm_layer, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
239
+ self.time_attn = Attention(hidden_size, num_heads=num_heads, qkv_bias=True, qk_norm=True,
240
+ norm_layer=norm_layer, attn_drop=dropout_rate, proj_drop=dropout_rate, **block_kwargs)
241
+
242
+ mlp_hidden_dim = int(hidden_size * mlp_ratio)
243
+ if mlp_block == 'mlp':
244
+ approx_gelu = lambda: nn.GELU(approximate="tanh")
245
+ self.space_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=None, drop=0)
246
+ self.time_mlp = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=None, drop=0)
247
+ self.space_mlp_reg = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=None, drop=0)
248
+ self.time_mlp_reg = Mlp(in_features=hidden_size, hidden_features=mlp_hidden_dim, act_layer=approx_gelu, norm_layer=None, drop=0)
249
+ elif mlp_block == 'swiglu':
250
+ self.space_mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim * 2) // 3)
251
+ self.time_mlp = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim * 2) // 3)
252
+ self.space_mlp_reg = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim * 2) // 3)
253
+ self.time_mlp_reg = SwiGLU(in_features=hidden_size, hidden_features=(mlp_hidden_dim * 2) // 3)
254
+ else:
255
+ raise NotImplementedError(f"mlp_block {mlp_block} not implemented")
256
+
257
+ self.adaLN_modulation = nn.Sequential(nn.SiLU(), nn.Linear(hidden_size, 9 * hidden_size, bias=True))
258
+ self.adaLN_registers_modulation = nn.Sequential(
259
+ nn.SiLU(),
260
+ nn.Linear(hidden_size, 12 * hidden_size, bias=True),
261
+ )
262
+
263
+ self.causal_time_attn = causal_time_attn
264
+ self.modulate_time_attn = modulate_time_attn
265
+
266
+ # Always-on time-attn modulation
267
+ self.adaLN_time_attn_modulation = nn.Sequential(
268
+ nn.SiLU(),
269
+ nn.Linear(hidden_size, 3 * hidden_size, bias=True)
270
+ )
271
+ self.norm_time_attn = norm_layer(hidden_size, elementwise_affine=False, eps=1e-6)
272
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].weight, 0)
273
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].bias, 0)
274
+
275
+ self.log_adaln_mean_abs = False
276
+ self._last_adaln_mean_abs = {}
277
+
278
+ def initialize_adaln_weights(self, gate_init_std=0.0):
279
+ nn.init.constant_(self.adaLN_modulation[-1].weight, 0)
280
+ nn.init.constant_(self.adaLN_modulation[-1].bias, 0)
281
+ nn.init.constant_(self.adaLN_registers_modulation[-1].weight, 0)
282
+ nn.init.constant_(self.adaLN_registers_modulation[-1].bias, 0)
283
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].weight, 0)
284
+ nn.init.constant_(self.adaLN_time_attn_modulation[-1].bias, 0)
285
+
286
+ if gate_init_std != 0.0:
287
+ hidden_size = self.adaLN_modulation[-1].out_features // 9
288
+ for gate_idx in (2, 5, 8):
289
+ start = gate_idx * hidden_size
290
+ end = start + hidden_size
291
+ nn.init.normal_(self.adaLN_modulation[-1].weight[start:end], std=gate_init_std)
292
+
293
+ hidden_size = self.adaLN_registers_modulation[-1].out_features // 12
294
+ for gate_idx in (2, 5, 8, 11):
295
+ start = gate_idx * hidden_size
296
+ end = start + hidden_size
297
+ nn.init.normal_(self.adaLN_registers_modulation[-1].weight[start:end], std=gate_init_std)
298
+
299
+ hidden_size = self.adaLN_time_attn_modulation[-1].out_features // 3
300
+ start = 2 * hidden_size
301
+ end = start + hidden_size
302
+ nn.init.normal_(self.adaLN_time_attn_modulation[-1].weight[start:end], std=gate_init_std)
303
+
304
+ def _collect_adaln_mean_abs(self, **tensors):
305
+ self._last_adaln_mean_abs = {
306
+ name: tensor.detach().abs().flatten(1).mean(dim=1)
307
+ for name, tensor in tensors.items()
308
+ }
309
+
310
+ def forward(self, x, c, c_registers):
311
+ B, F, N, D = x.shape
312
+ R = self.num_reg_tokens
313
+
314
+ # always has registers
315
+ reg_tokens = x[:, :, :R, :]
316
+ x_patch = x[:, :, R:, :]
317
+
318
+ (shift_msa, scale_msa, gate_msa,
319
+ shift_mlp_s, scale_mlp_s, gate_mlp_s,
320
+ shift_mlp_t, scale_mlp_t, gate_mlp_t) = self.adaLN_modulation(c).chunk(9, dim=-1)
321
+ shift_mta, scale_mta, gate_mta = self.adaLN_time_attn_modulation(c).chunk(3, dim=-1)
322
+ (shift_reg_attn_s, scale_reg_attn_s, gate_reg_attn_s,
323
+ shift_reg_mlp_s, scale_reg_mlp_s, gate_reg_mlp_s,
324
+ shift_reg_attn_t, scale_reg_attn_t, gate_reg_attn_t,
325
+ shift_reg_mlp_t, scale_reg_mlp_t, gate_reg_mlp_t) = self.adaLN_registers_modulation(c_registers).chunk(12, dim=-1)
326
+
327
+ if self.log_adaln_mean_abs:
328
+ self._collect_adaln_mean_abs(
329
+ msa_shift=shift_msa, msa_scale=scale_msa, msa_gate=gate_msa,
330
+ mlp_s_shift=shift_mlp_s, mlp_s_scale=scale_mlp_s, mlp_s_gate=gate_mlp_s,
331
+ mta_shift=shift_mta, mta_scale=scale_mta, mta_gate=gate_mta,
332
+ mlp_t_shift=shift_mlp_t, mlp_t_scale=scale_mlp_t, mlp_t_gate=gate_mlp_t,
333
+ reg_attn_s_shift=shift_reg_attn_s, reg_attn_s_scale=scale_reg_attn_s, reg_attn_s_gate=gate_reg_attn_s,
334
+ reg_mlp_s_shift=shift_reg_mlp_s, reg_mlp_s_scale=scale_reg_mlp_s, reg_mlp_s_gate=gate_reg_mlp_s,
335
+ reg_attn_t_shift=shift_reg_attn_t, reg_attn_t_scale=scale_reg_attn_t, reg_attn_t_gate=gate_reg_attn_t,
336
+ reg_mlp_t_shift=shift_reg_mlp_t, reg_mlp_t_scale=scale_reg_mlp_t, reg_mlp_t_gate=gate_reg_mlp_t,
337
+ )
338
+
339
+ # spatial attn (always concat regs)
340
+ x_mod = rearrange(modulate(self.norm1(x_patch), shift_msa, scale_msa), 'b f n d -> (b f) n d')
341
+ reg_in = rearrange(
342
+ modulate(self.norm_reg_attn_s(reg_tokens), shift_reg_attn_s, scale_reg_attn_s),
343
+ 'b f r d -> (b f) r d'
344
+ )
345
+ x_mod = torch.cat([reg_in, x_mod], dim=1)
346
+ x_out = self.space_attn(x_mod)
347
+
348
+ reg_attn_out, x_out = torch.split(x_out, [R, x_patch.size(2)], dim=1)
349
+ reg_attn_out = rearrange(reg_attn_out, '(b f) r d -> b f r d', b=B, f=F)
350
+ x_out = rearrange(x_out, '(b f) n d -> b f n d', b=B, f=F)
351
+ x_patch = x_patch + gate_msa.unsqueeze(1).unsqueeze(1) * x_out if gate_msa.ndim == 2 else x_patch + gate_msa.unsqueeze(2) * x_out
352
+ reg_tokens = reg_tokens + gate_reg_attn_s.unsqueeze(1).unsqueeze(1) * reg_attn_out
353
+
354
+ # spatial mlp
355
+ x_patch = x_patch + gate_mlp_s.unsqueeze(1).unsqueeze(1) * self.space_mlp(modulate(self.norm2(x_patch), shift_mlp_s, scale_mlp_s)) if gate_mlp_s.ndim == 2 else x_patch + gate_mlp_s.unsqueeze(2) * self.space_mlp(modulate(self.norm2(x_patch), shift_mlp_s, scale_mlp_s))
356
+ reg_mlp_out = self.space_mlp_reg(modulate(self.norm_reg_s(reg_tokens), shift_reg_mlp_s, scale_reg_mlp_s))
357
+ reg_tokens = reg_tokens + gate_reg_mlp_s.unsqueeze(1).unsqueeze(1) * reg_mlp_out
358
+
359
+ # temporal attn patches
360
+ x_mod = modulate(self.norm_time_attn(x_patch), shift_mta, scale_mta)
361
+ x_mod = rearrange(x_mod, 'b f n d -> (b n) f d')
362
+ time_attn_mask = torch.tril(torch.ones(F, F, device=x.device)) if self.causal_time_attn else None
363
+ x_out = self.time_attn(x_mod, attn_mask=time_attn_mask)
364
+ x_out = rearrange(x_out, '(b n) f d -> b f n d', b=B, n=x_patch.size(2), f=F)
365
+ x_patch = x_patch + gate_mta.unsqueeze(1).unsqueeze(1) * x_out if gate_mta.ndim == 2 else x_patch + gate_mta.unsqueeze(2) * x_out
366
+
367
+ # temporal attn regs (always)
368
+ reg_time_in = rearrange(
369
+ modulate(self.norm_reg_attn_t(reg_tokens), shift_reg_attn_t, scale_reg_attn_t),
370
+ 'b f r d -> (b r) f d'
371
+ )
372
+ reg_time_out = self.time_attn(reg_time_in, attn_mask=time_attn_mask)
373
+ reg_time_out = rearrange(reg_time_out, '(b r) f d -> b f r d', b=B, r=R, f=F)
374
+ reg_tokens = reg_tokens + gate_reg_attn_t.unsqueeze(1).unsqueeze(1) * reg_time_out
375
+
376
+ # temporal mlp
377
+ x_patch = x_patch + gate_mlp_t.unsqueeze(1).unsqueeze(1) * self.time_mlp(modulate(self.norm3(x_patch), shift_mlp_t, scale_mlp_t)) if gate_mlp_t.ndim == 2 else x_patch + gate_mlp_t.unsqueeze(2) * self.time_mlp(modulate(self.norm3(x_patch), shift_mlp_t, scale_mlp_t))
378
+ reg_mlp_out = self.time_mlp_reg(modulate(self.norm_reg_t(reg_tokens), shift_reg_mlp_t, scale_reg_mlp_t))
379
+ reg_tokens = reg_tokens + gate_reg_mlp_t.unsqueeze(1).unsqueeze(1) * reg_mlp_out
380
+
381
+ return torch.cat([reg_tokens, x_patch], dim=2)
382
+
383
+
384
+
385
+
386
+
387
+ class DiT(nn.Module):
388
+ """
389
+ Diffusion model with a Transformer backbone.
390
+ """
391
+ def __init__(
392
+ self,
393
+ input_size=16,
394
+ patch_size=2,
395
+ in_channels=32,
396
+ hidden_size=1152,
397
+ depth=28,
398
+ num_heads=16,
399
+ mlp_ratio=4.0,
400
+ max_num_frames=6,
401
+ dropout=0.0,
402
+ ctx_noise_aug_ratio=0.1,
403
+ ctx_noise_aug_prob = 0.5,
404
+ drop_ctx_rate=0.2,
405
+ frequency_range=(2, 15),
406
+ learn_sigma=False,
407
+ norm_layer=nn.LayerNorm,
408
+ mlp_block='mlp',
409
+ ):
410
+ super().__init__()
411
+ self.input_size= input_size if isinstance(input_size, (list, tuple, ListConfig)) else [input_size, input_size]
412
+ self.in_channels = in_channels
413
+ self.out_channels = in_channels * 2 if learn_sigma else in_channels
414
+ self.patch_size = patch_size
415
+ self.num_heads = num_heads
416
+ self.ctx_noise_aug_ratio = ctx_noise_aug_ratio
417
+ self.ctx_noise_aug_prob = ctx_noise_aug_prob
418
+ self.drop_ctx_rate = drop_ctx_rate
419
+
420
+
421
+ self.x_embedder = PatchEmbed(input_size, patch_size, in_channels, hidden_size, bias=True)
422
+ self.num_patches = self.x_embedder.num_patches
423
+ self.t_embedder = TimestepEmbedder(hidden_size)
424
+ self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, hidden_size), requires_grad=False)
425
+
426
+ self.blocks = nn.ModuleList([
427
+ DiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, dropout_rate=dropout, norm_layer=norm_layer, mlp_block=mlp_block) for _ in range(depth)
428
+ ])
429
+
430
+ self.final_layer = FinalLayer(hidden_size, patch_size, self.out_channels, norm_layer=norm_layer)
431
+ self.max_num_frames = max_num_frames
432
+ self.frame_emb = nn.init.trunc_normal_(nn.Parameter(torch.zeros(1, self.max_num_frames, 1, hidden_size)), 0., 0.02)
433
+ self.frame_rate_encoder = FrequencyEncoder(hidden_size, freq_min=frequency_range[0], freq_max=frequency_range[1])
434
+
435
+ self.initialize_weights()
436
+
437
+
438
+ def initialize_weights(self):
439
+ # Initialize transformer layers:
440
+ def _basic_init(module):
441
+ if isinstance(module, nn.Linear):
442
+ torch.nn.init.xavier_uniform_(module.weight)
443
+ if module.bias is not None:
444
+ nn.init.constant_(module.bias, 0)
445
+ self.apply(_basic_init)
446
+
447
+
448
+ # Initialize (and freeze) pos_embed by sin-cos embedding:
449
+ pos_embed = get_2d_sincos_pos_embed(self.pos_embed.shape[-1], [self.input_size[0] // self.patch_size, self.input_size[1] // self.patch_size], cls_token=False, extra_tokens=0)
450
+ self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float().unsqueeze(0))
451
+
452
+
453
+ w = self.x_embedder.proj.weight.data
454
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
455
+ nn.init.constant_(self.x_embedder.proj.bias, 0)
456
+
457
+
458
+ # Initialize timestep embedding MLP:
459
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
460
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
461
+
462
+
463
+ # Zero-out adaLN modulation layers in DiT blocks:
464
+ for block in self.blocks:
465
+ nn.init.constant_(block.adaLN_modulation[-1].weight, 0)
466
+ nn.init.constant_(block.adaLN_modulation[-1].bias, 0)
467
+
468
+
469
+ # Zero-out output layers:
470
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
471
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
472
+ nn.init.constant_(self.final_layer.linear.weight, 0)
473
+ nn.init.constant_(self.final_layer.linear.bias, 0)
474
+
475
+
476
+ def unpatchify(self, x):
477
+ """
478
+ x: (N, T, patch_size**2 * C)
479
+ imgs: (N, H, W, C)
480
+ """
481
+ c = self.out_channels
482
+ p = self.x_embedder.patch_size[0]
483
+ h = self.x_embedder.grid_size[0]
484
+ w = self.x_embedder.grid_size[1]
485
+
486
+
487
+ x = x.reshape(shape=(x.shape[0], x.shape[1], h, w, p, p, c))
488
+ x = torch.einsum('bfhwpqc->bfchpwq', x)
489
+ imgs = x.reshape(shape=(x.shape[0], x.shape[1], c, h * p, w * p))
490
+ return imgs
491
+
492
+
493
+ def get_condition_embeddings(self, t):
494
+ """
495
+ Get the condition embeddings for the given timesteps.
496
+ t: (N,) tensor of diffusion timesteps
497
+ returns: (N, D) tensor of condition embeddings
498
+ """
499
+ return self.t_embedder(t)
500
+
501
+ def preprocess_inputs(self, target, context, t, frame_rate):
502
+ b, f_target = target.size()[:2]
503
+ f_context = context.size(1)
504
+
505
+ if self.training:
506
+ # Drop the context frame
507
+ if torch.rand(1, device=target.device)<self.drop_ctx_rate:
508
+ context = None
509
+ f_context = 0
510
+ elif torch.rand(1, device=target.device) < self.ctx_noise_aug_prob:
511
+ # Add noise to context frames (if t is less than ctx_noise_aug_ratio, we do not add noise)
512
+ mask = (t >= self.ctx_noise_aug_ratio)
513
+ aug_noise = torch.randn_like(context)
514
+ context[mask] = context[mask] + aug_noise[mask] * self.ctx_noise_aug_ratio
515
+
516
+
517
+ frame_embeddings = self.frame_rate_encoder.encode(frame_rate)
518
+ frame_embeddings = frame_embeddings.unsqueeze(1).unsqueeze(1).to(target.device)
519
+ x = torch.cat((context, target), dim=1) if context is not None else target
520
+ x = rearrange(x, 'b f c h w -> (b f) c h w')
521
+ x = self.x_embedder(x) + self.pos_embed.to(x.device)
522
+ x = rearrange(x, '(b f) hw c -> b f hw c', b=b)
523
+ x = x + self.frame_emb[:, self.max_num_frames-(f_target+f_context):].to(x.device) + frame_embeddings
524
+ return x
525
+
526
+ def get_condition_embeddings(self, t):
527
+ """
528
+ Get the condition embeddings for the given timesteps.
529
+ t: (N,) tensor of diffusion timesteps
530
+ returns: (N, D) tensor of condition embeddings
531
+ """
532
+ return self.t_embedder(t)
533
+
534
+ def preprocess_inputs(self, target, context, t, frame_rate):
535
+ b, f_target = target.size()[:2]
536
+ f_context = context.size(1) if context is not None else 0
537
+
538
+ if self.training:
539
+ # Drop the context frame
540
+ if torch.rand(1, device=target.device)<self.drop_ctx_rate:
541
+ context = None
542
+ f_context = 0
543
+ elif torch.rand(1, device=target.device) < self.ctx_noise_aug_prob:
544
+ # Add noise to context frames (if t is less than ctx_noise_aug_ratio, we do not add noise)
545
+ mask = (t >= self.ctx_noise_aug_ratio)
546
+ aug_noise = torch.randn_like(context)
547
+ context[mask] = context[mask] + aug_noise[mask] * self.ctx_noise_aug_ratio
548
+
549
+
550
+ frame_embeddings = self.frame_rate_encoder.encode(frame_rate)
551
+ frame_embeddings = frame_embeddings.unsqueeze(1).unsqueeze(1).to(target.device)
552
+ x = torch.cat((context, target), dim=1) if context is not None else target
553
+ x = rearrange(x, 'b f c h w -> (b f) c h w')
554
+ x = self.x_embedder(x) + self.pos_embed.to(x.device)
555
+ x = rearrange(x, '(b f) hw c -> b f hw c', b=b)
556
+ x = x + self.frame_emb[:, self.max_num_frames-(f_target+f_context):].to(x.device) + frame_embeddings
557
+ return x
558
+
559
+ def postprocess_outputs(self, out):
560
+ return self.unpatchify(out)
561
+
562
+ def forward(self, target, context, t, frame_rate, return_features=False):
563
+ """
564
+ Forward pass of DiT.
565
+ x: (N, F, C, H, W) tensor of spatial inputs (images or latent representations of images)
566
+ t: (N,) tensor of diffusion timesteps
567
+ y: (N,) tensor of class labels
568
+ """
569
+
570
+ num_frames_ctx = context.size(1)
571
+ num_frames_pred = target.size(1)
572
+
573
+ c = self.get_condition_embeddings(t)
574
+
575
+ x = self.preprocess_inputs(target, context, t, frame_rate)
576
+
577
+ x = rearrange(x, 'b f hw c -> b (f hw) c')
578
+ features = []
579
+ for block in self.blocks:
580
+ x = block(x, c)
581
+ features.append(x) if return_features else None
582
+ x = rearrange(x, 'b (f hw) c -> b f hw c', f=(num_frames_ctx+num_frames_pred))[:,-num_frames_pred:]
583
+ out = self.final_layer(x, c)
584
+
585
+ out = self.postprocess_outputs(out)
586
+ if return_features:
587
+ return out, features
588
+ return out
589
+
590
+
591
+ class CDiT(DiT):
592
+ def __init__(self, input_size=16, patch_size=2, in_channels=32, hidden_size=1152, depth=28, num_heads=16, mlp_ratio=4.0, max_num_frames=6, dropout=0.1, ctx_noise_aug_ratio=0.1,ctx_noise_aug_prob=0.5, norm_layer=nn.LayerNorm, mlp_block='mlp', **kwargs):
593
+ if isinstance(norm_layer, str):
594
+ norm_layer = get_norm_layer(norm_layer)
595
+ super().__init__(input_size=input_size, patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size, depth=depth, num_heads=num_heads, mlp_ratio=mlp_ratio, max_num_frames=max_num_frames, dropout=dropout,
596
+ ctx_noise_aug_ratio=ctx_noise_aug_ratio, ctx_noise_aug_prob=ctx_noise_aug_prob, norm_layer=norm_layer, mlp_block=mlp_block, **kwargs)
597
+ self.blocks = nn.ModuleList([
598
+ CDiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, norm_layer=norm_layer, mlp_block=mlp_block) for _ in range(depth)
599
+ ])
600
+
601
+
602
+ def forward(self, target, context, t, frame_rate, return_features=False):
603
+ """
604
+ Forward pass of DiT.
605
+ x: (N, F, C, H, W) tensor of spatial inputs (images or latent representations of images)
606
+ t: (N,) tensor of diffusion timesteps
607
+ """
608
+ num_frames_ctx = context.size(1)
609
+ num_frames_pred = target.size(1)
610
+
611
+ c = self.get_condition_embeddings(t) # (N, D)
612
+
613
+ x = self.preprocess_inputs(target, context, t, frame_rate) # (B, F, N, D)
614
+
615
+
616
+ target = rearrange(x[:,-num_frames_pred:], 'b f hw c -> b (f hw) c')
617
+ ctx = rearrange(x[:,:-num_frames_pred], 'b f hw c -> b (f hw) c') if num_frames_ctx>1 else None
618
+
619
+ features = []
620
+ for block in self.blocks:
621
+ target = block(target, c, ctx) # (N, T, D)
622
+ features.append(target)
623
+
624
+ target = rearrange(target, 'b (f hw) c -> b f hw c', f=(num_frames_pred))
625
+ out = self.final_layer(target, c) # (N, T, patch_size * out_channels)
626
+ out = self.postprocess_outputs(out) # (N, T, patch_size ** 2 * out_channels)
627
+ if return_features:
628
+ return out, features
629
+ return out
630
+
631
+
632
+
633
+ class STDiT(DiT):
634
+ def __init__(self, input_size=16, patch_size=2, in_channels=32, hidden_size=1152, depth=28, num_heads=16, mlp_ratio=4.0, max_num_frames=6,
635
+ dropout=0.1, ctx_noise_aug_ratio=0.1, ctx_noise_aug_prob=0.5, drop_ctx_rate=0.2, frequency_range=(2, 15),
636
+ causal_time_attn=False, modulate_time_attn=False, norm_layer=nn.LayerNorm, mlp_block='mlp',
637
+ **kwargs):
638
+
639
+ if isinstance(norm_layer, str):
640
+ norm_layer = get_norm_layer(norm_layer)
641
+
642
+ super().__init__(input_size=input_size, patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size, depth=depth, num_heads=num_heads, mlp_ratio=mlp_ratio,
643
+ max_num_frames=max_num_frames, dropout=dropout, ctx_noise_aug_ratio=ctx_noise_aug_ratio, ctx_noise_aug_prob=ctx_noise_aug_prob, drop_ctx_rate=drop_ctx_rate,
644
+ norm_layer=norm_layer, mlp_block=mlp_block, **kwargs)
645
+ self.blocks = nn.ModuleList([
646
+ STDiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, dropout_rate=dropout,
647
+ causal_time_attn=causal_time_attn, modulate_time_attn=modulate_time_attn,
648
+ norm_layer=norm_layer, mlp_block=mlp_block) for _ in range(depth)
649
+ ])
650
+
651
+ def forward(self, target, context, t, frame_rate, return_features=False):
652
+ """
653
+ Forward pass of DiT.
654
+ x: (N, F, C, H, W) tensor of spatial inputs (images or latent representations of images)
655
+ t: (N,) tensor of diffusion timesteps
656
+ """
657
+ f_pred = target.size(1)
658
+
659
+ c = self.get_condition_embeddings(t) # (N, D)
660
+
661
+ x = self.preprocess_inputs(target, context, t, frame_rate) # (B, F, N, D)
662
+
663
+ features = []
664
+ for block in self.blocks:
665
+ x = block(x, c)
666
+ features.append(x) if return_features else None
667
+
668
+
669
+ out = self.final_layer(x[:,-f_pred:], c) # (N, T, patch_size * out_channels)
670
+ out = self.postprocess_outputs(out) # (N, T, patch_size ** 2 * out_channels)
671
+ if return_features:
672
+ return out, features
673
+ return out
674
+
675
+ class STDiTDF(STDiT):
676
+ def __init__(self, input_size=16, patch_size=2, in_channels=32, hidden_size=1152, depth=28, num_heads=16, mlp_ratio=4.0, max_num_frames=6,
677
+ dropout=0.1, ctx_noise_aug_ratio=0.1, ctx_noise_aug_prob=0.5, drop_ctx_rate=0.2, frequency_range=(2, 15),
678
+ causal_time_attn=False, modulate_time_attn=False, norm_layer=nn.LayerNorm, mlp_block='mlp',
679
+ **kwargs):
680
+
681
+ if isinstance(norm_layer, str):
682
+ norm_layer = get_norm_layer(norm_layer)
683
+
684
+ super().__init__(input_size=input_size, patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size, depth=depth, num_heads=num_heads, mlp_ratio=mlp_ratio,
685
+ max_num_frames=max_num_frames, dropout=dropout, ctx_noise_aug_ratio=ctx_noise_aug_ratio, ctx_noise_aug_prob=ctx_noise_aug_prob, drop_ctx_rate=drop_ctx_rate,
686
+ frequency_range=frequency_range, causal_time_attn=causal_time_attn, modulate_time_attn=modulate_time_attn, norm_layer=norm_layer, mlp_block=mlp_block, **kwargs)
687
+
688
+ self.blocks = nn.ModuleList([
689
+ STDiTBlock(hidden_size, num_heads, mlp_ratio=mlp_ratio, dropout_rate=dropout,
690
+ causal_time_attn=causal_time_attn, modulate_time_attn=modulate_time_attn,
691
+ norm_layer=norm_layer, mlp_block=mlp_block) for _ in range(depth)
692
+ ])
693
+
694
+ def preprocess_inputs(self, x, frame_rate):
695
+ b, f = x.size()[:2]
696
+
697
+ frame_embeddings = self.frame_rate_encoder.encode(frame_rate)
698
+ frame_embeddings = frame_embeddings.unsqueeze(1).unsqueeze(1).to(x.device)
699
+ x = rearrange(x, 'b f c h w -> (b f) c h w')
700
+ x = self.x_embedder(x) + self.pos_embed.to(x.device)
701
+ x = rearrange(x, '(b f) hw c -> b f hw c', b=b)
702
+ x = x + self.frame_emb[:, :f].to(x.device) + frame_embeddings
703
+ return x
704
+
705
+ def forward(self, x, t, frame_rate, return_features=False):
706
+ """
707
+ Forward pass of DiT.
708
+ x: (N, F, C, H, W) tensor of spatial inputs (images or latent representations of images)
709
+ t: (N,) tensor of diffusion timesteps
710
+ """
711
+ f_pred = x.size(1)
712
+
713
+ t = rearrange(t, 'b f -> (b f)')
714
+ c = self.get_condition_embeddings(t) # (N, D)
715
+ c = rearrange(c, '(b f) d -> b f d', b=x.size(0), f=x.size(1))
716
+
717
+ x = self.preprocess_inputs(x, frame_rate) # (B, F, N, D)
718
+
719
+ features = []
720
+ for block in self.blocks:
721
+ x = block(x, c)
722
+ features.append(x) if return_features else None
723
+
724
+ out = self.final_layer(x, c) # (N, T, patch_size * out_channels)
725
+ out = self.postprocess_outputs(out) # (N, T, patch_size ** 2 * out_channels)
726
+ if return_features:
727
+ return out, features
728
+ return out
729
+
730
+
731
+ class STDiTWithGlobalBlocks(STDiT):
732
+ """
733
+ A DiT with extra global attention blocks (DiT blocks).
734
+ """
735
+ def __init__(self, *args, global_block_indices, **kwargs):
736
+
737
+ class DiTBlockWrapper(nn.Module):
738
+ def __init__(self, block):
739
+ super().__init__()
740
+ self.block = block
741
+
742
+ def forward(self, x, c):
743
+ B, F, N, D = x.shape
744
+ x_ = rearrange(x, 'b f n d -> b (f n) d', b=B, f=F)
745
+ x_ = self.block(x_, c)
746
+ x_ = rearrange(x_, 'b (f n) d -> b f n d', b=B, f=F)
747
+ return x_
748
+
749
+ super().__init__(*args, **kwargs)
750
+ self.global_block_indices = global_block_indices
751
+ for idx in global_block_indices:
752
+ dit_block = DiTBlock(hidden_size=kwargs.get('hidden_size',1152), num_heads=kwargs.get('num_heads',16),
753
+ mlp_ratio=kwargs.get('mlp_ratio',4.0), dropout_rate=kwargs.get('dropout',0.0),
754
+ norm_layer=kwargs.get('norm_layer',nn.LayerNorm), mlp_block=kwargs.get('mlp_block','mlp'))
755
+ self.blocks[idx] = DiTBlockWrapper(dit_block)
756
+
757
+
758
+ class STDiT_tmpadaLN(STDiT):
759
+ def __init__(self, input_size=16, patch_size=2, in_channels=32, hidden_size=1152, depth=28, num_heads=16, mlp_ratio=4.0, max_num_frames=6, dropout=0.1, ctx_noise_aug_ratio=0.1, ctx_noise_aug_prob=0.5, drop_ctx_rate=0.2, frequency_range=(2, 15), causal_time_attn=False, norm_layer=nn.LayerNorm, **kwargs):
760
+ super().__init__(input_size=input_size, patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size, depth=depth, num_heads=num_heads, mlp_ratio=mlp_ratio, max_num_frames=max_num_frames, dropout=dropout, ctx_noise_aug_ratio=ctx_noise_aug_ratio, ctx_noise_aug_prob=ctx_noise_aug_prob, drop_ctx_rate=drop_ctx_rate, norm_layer=norm_layer, **kwargs)
761
+ self.blocks = nn.ModuleList([
762
+ STDiTBlock_tmpadaLN(hidden_size, num_heads, mlp_ratio=mlp_ratio, dropout_rate=dropout, causal_time_attn=causal_time_attn, norm_layer=norm_layer) for _ in range(depth)
763
+ ])
764
+
765
+
766
+ class SwinSTDiT(STDiT):
767
+ def __init__(self, input_size=16, patch_size=2, in_channels=32, hidden_size=1152, depth=28, num_heads=16, mlp_ratio=4.0, max_num_frames=6, window_size=[6, 4], dropout=0.1, ctx_noise_aug_ratio=0.1,ctx_noise_aug_prob=0.5, drop_ctx_rate=0.2, frequency_range=(2, 15), norm_layer=nn.LayerNorm, mlp_block='mlp', **kwargs):
768
+ super().__init__(input_size=input_size, patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size, depth=depth, num_heads=num_heads, mlp_ratio=mlp_ratio, max_num_frames=max_num_frames, dropout=dropout, ctx_noise_aug_ratio=ctx_noise_aug_ratio, ctx_noise_aug_prob=ctx_noise_aug_prob, drop_ctx_rate=drop_ctx_rate, norm_layer=norm_layer, mlp_block=mlp_block, **kwargs)
769
+ self.blocks = nn.ModuleList([
770
+ SwinSTDiTBlock(hidden_size=hidden_size, num_heads=num_heads, input_shape=input_size, layer_idx=layer_idx, mlp_ratio=mlp_ratio, window_size=window_size, dropout_rate=dropout, norm_layer=norm_layer) for layer_idx in range(depth)
771
+ ])
772
+
773
+
774
+ class SwinSTDiTNoExtraMLP(STDiT):
775
+ def __init__(self, input_size=16, patch_size=2, in_channels=32, hidden_size=1152, depth=28, num_heads=16, mlp_ratio=4.0, max_num_frames=6, window_size=[6, 4],
776
+ dropout=0.1, ctx_noise_aug_ratio=0.1,ctx_noise_aug_prob=0.5, drop_ctx_rate=0.2, frequency_range=(2, 15),
777
+ norm_layer='layer_norm', mlp_block='mlp', qk_norm=True, **kwargs):
778
+
779
+ if isinstance(norm_layer, str):
780
+ norm_layer = get_norm_layer(norm_layer)
781
+
782
+ super().__init__(input_size=input_size, patch_size=patch_size, in_channels=in_channels, hidden_size=hidden_size, depth=depth,
783
+ num_heads=num_heads, mlp_ratio=mlp_ratio, max_num_frames=max_num_frames, dropout=dropout,
784
+ ctx_noise_aug_ratio=ctx_noise_aug_ratio, ctx_noise_aug_prob=ctx_noise_aug_prob, drop_ctx_rate=drop_ctx_rate,
785
+ norm_layer=norm_layer, mlp_block=mlp_block, **kwargs)
786
+
787
+ for block_idx, block in enumerate(self.blocks):
788
+ block.space_attn = SwinAttention(
789
+ hidden_size,
790
+ input_resolution=input_size,
791
+ num_heads=num_heads,
792
+ window_size=window_size,
793
+ shift_size=(0,0) if (block_idx % 2 == 0) else [ws//2 for ws in window_size],
794
+ proj_drop=dropout,
795
+ attn_drop=dropout,
796
+ qk_norm=qk_norm,
797
+ norm_layer=norm_layer,
798
+ )
799
+
800
+
801
+
802
+ class STDiTDFSpatialL2(STDiTDF):
803
+ """
804
+ STDiTDF with per-token spatial conditioning from L2 (semantic) latents.
805
+
806
+ The L2 latent (same spatial H×W as the rec latent, sem_in_channels channels) is
807
+ patchified into tokens, combined with a learned frame-position embedding, and
808
+ produces per-token (shift, scale, gate) modulation injected at every block via
809
+ STDiTBlockWithSpatialL2.
810
+
811
+ Conditioning scheme (num_frames = 1 context + num_pred_frames generated):
812
+ Frame 0 (input): z_l2_start, position = 0.0
813
+ Frame k (generated): z_l2_end, position = k / num_pred_frames
814
+
815
+ All new parameters (l2_patchify, position_proj, per-block spatial_to_mod) are
816
+ zero-initialized so that a pre-trained STDiTDF backbone is unchanged at init.
817
+
818
+ Args:
819
+ sem_in_channels: Number of channels in the L2 (sem) latent.
820
+ All other args are passed through to STDiTDF.
821
+ """
822
+
823
+ def __init__(
824
+ self,
825
+ sem_in_channels,
826
+ patch_size=2,
827
+ hidden_size=1152,
828
+ depth=28,
829
+ num_heads=16,
830
+ mlp_ratio=4.0,
831
+ max_num_frames=6,
832
+ dropout=0.1,
833
+ ctx_noise_aug_ratio=0.1,
834
+ ctx_noise_aug_prob=0.5,
835
+ drop_ctx_rate=0.2,
836
+ frequency_range=(2, 15),
837
+ causal_time_attn=False,
838
+ modulate_time_attn=False,
839
+ norm_layer=nn.LayerNorm,
840
+ mlp_block='mlp',
841
+ **kwargs,
842
+ ):
843
+ if isinstance(norm_layer, str):
844
+ norm_layer = get_norm_layer(norm_layer)
845
+
846
+ super().__init__(
847
+ patch_size=patch_size,
848
+ hidden_size=hidden_size,
849
+ depth=depth,
850
+ num_heads=num_heads,
851
+ mlp_ratio=mlp_ratio,
852
+ max_num_frames=max_num_frames,
853
+ dropout=dropout,
854
+ ctx_noise_aug_ratio=ctx_noise_aug_ratio,
855
+ ctx_noise_aug_prob=ctx_noise_aug_prob,
856
+ drop_ctx_rate=drop_ctx_rate,
857
+ frequency_range=frequency_range,
858
+ causal_time_attn=causal_time_attn,
859
+ modulate_time_attn=modulate_time_attn,
860
+ norm_layer=norm_layer,
861
+ mlp_block=mlp_block,
862
+ **kwargs,
863
+ )
864
+
865
+ # Replace blocks with spatially-conditioned variants
866
+ self.blocks = nn.ModuleList([
867
+ STDiTBlockWithSpatialL2(
868
+ hidden_size, num_heads,
869
+ mlp_ratio=mlp_ratio,
870
+ dropout_rate=dropout,
871
+ causal_time_attn=causal_time_attn,
872
+ modulate_time_attn=modulate_time_attn,
873
+ norm_layer=norm_layer,
874
+ mlp_block=mlp_block,
875
+ )
876
+ for _ in range(depth)
877
+ ])
878
+
879
+ # Patchify L2 latent: patch_size^2 * sem_in_channels → hidden_size
880
+ # Xavier init so the patchified tokens carry real signal from step 1.
881
+ self.l2_patchify = nn.Linear(patch_size ** 2 * sem_in_channels, hidden_size, bias=True)
882
+ nn.init.xavier_uniform_(self.l2_patchify.weight)
883
+ nn.init.zeros_(self.l2_patchify.bias)
884
+
885
+ # Frame-position embedding: scalar in [0,1] → hidden_size
886
+ # Xavier init so position differences produce non-trivial embeddings from step 1.
887
+ self.position_proj = nn.Sequential(
888
+ nn.Linear(1, hidden_size),
889
+ nn.SiLU(),
890
+ nn.Linear(hidden_size, hidden_size),
891
+ )
892
+ for m in self.position_proj.modules():
893
+ if isinstance(m, nn.Linear):
894
+ nn.init.xavier_uniform_(m.weight)
895
+ nn.init.zeros_(m.bias)
896
+
897
+ def _patchify_l2(self, z_l2):
898
+ """
899
+ Patchify L2 latent frames.
900
+
901
+ Args:
902
+ z_l2: (B, F, C_sem, H, W)
903
+ Returns:
904
+ (B, F, N, hidden_size) where N = (H/patch_size) * (W/patch_size)
905
+ """
906
+ b, f, c, h, w = z_l2.shape
907
+ p = self.patch_size
908
+ z = rearrange(z_l2, 'b f c (h p1) (w p2) -> (b f) (h w) (p1 p2 c)', p1=p, p2=p)
909
+ z = self.l2_patchify(z) # (B*F, N, hidden_size)
910
+ return rearrange(z, '(b f) n d -> b f n d', b=b, f=f)
911
+
912
+ def _build_spatial_cond(self, z_l2_start, z_l2_end, num_frames):
913
+ """
914
+ Build per-frame L2 spatial tokens with position embedding.
915
+
916
+ Args:
917
+ z_l2_start: (B, C_sem, H, W) — sem latent for frame 0 (context/input)
918
+ z_l2_end: (B, C_sem, H, W) — sem latent for the last frame (target end)
919
+ num_frames: total frames in x (= 1 context + num_pred_frames generated)
920
+ Returns:
921
+ (B, num_frames, N, hidden_size)
922
+ """
923
+ device = z_l2_start.device
924
+ dtype = z_l2_start.dtype
925
+ num_pred = num_frames - 1 # generated frames indexed 1..num_frames-1
926
+
927
+ z_start_tok = self._patchify_l2(z_l2_start.unsqueeze(1)) # (B, 1, N, D)
928
+ z_end_tok = self._patchify_l2(z_l2_end.unsqueeze(1)) # (B, 1, N, D)
929
+
930
+ frames_cond = []
931
+ for i in range(num_frames):
932
+ if i == 0:
933
+ pos_val = 0.0
934
+ z_tok = z_start_tok
935
+ else:
936
+ pos_val = i / num_pred if num_pred > 0 else 1.0
937
+ z_tok = z_end_tok
938
+
939
+ pos = torch.tensor([[pos_val]], device=device, dtype=dtype)
940
+ pos_emb = self.position_proj(pos) # (1, D)
941
+ # Broadcast over batch and spatial tokens
942
+ z_frame = z_tok + pos_emb.unsqueeze(0) # (B, 1, N, D)
943
+ frames_cond.append(z_frame)
944
+
945
+ return torch.cat(frames_cond, dim=1) # (B, F, N, D)
946
+
947
+ def forward(self, x, t, frame_rate, z_l2_start, z_l2_end, return_features=False):
948
+ """
949
+ Args:
950
+ x: (B, F, C_rec, H, W) — rec latent (DF style: context + targets)
951
+ t: (B, F) — per-frame diffusion timesteps
952
+ frame_rate: (B,)
953
+ z_l2_start: (B, C_sem, H, W) — clean L2 latent for frame 0
954
+ z_l2_end: (B, C_sem, H, W) — clean L2 latent for last frame
955
+ """
956
+ b, f = x.shape[:2]
957
+
958
+ t_flat = rearrange(t, 'b f -> (b f)')
959
+ c = self.get_condition_embeddings(t_flat) # (B*F, D)
960
+ c = rearrange(c, '(b f) d -> b f d', b=b, f=f) # (B, F, D)
961
+
962
+ x = self.preprocess_inputs(x, frame_rate) # (B, F, N, D)
963
+
964
+ z_spatial = self._build_spatial_cond(z_l2_start, z_l2_end, f) # (B, F, N, D)
965
+
966
+ features = []
967
+ for block in self.blocks:
968
+ x = block(x, c, z_spatial)
969
+ features.append(x) if return_features else None
970
+
971
+ out = self.final_layer(x, c)
972
+ out = self.postprocess_outputs(out)
973
+ if return_features:
974
+ return out, features
975
+ return out
976
+
977
+
978
+ class STDiTDFSpatialL2_CtxAndLast(STDiTDFSpatialL2):
979
+ """
980
+ Two-point L2 conditioning: only the last context frame and the last
981
+ predicted frame receive a non-zero L2 conditioning token.
982
+
983
+ Conditioning scheme (K = num_context_frames, F = num_frames):
984
+ Frame K-1 (last context frame): z_l2_start, position = (K-1)/(F-1)
985
+ Frame F-1 (last predicted frame): z_l2_end, position = 1.0
986
+ All other frames: zero token, position = i/(F-1)
987
+
988
+ The zero-token frames still receive a position embedding so the network can
989
+ distinguish their temporal location, but carry no semantic conditioning.
990
+
991
+ Args:
992
+ num_context_frames: number of context frames in the L1 sequence (K).
993
+ Must match the NUM_CONTEXT_FRAMES of the model class.
994
+ All other args forwarded to STDiTDFSpatialL2.
995
+ """
996
+
997
+ def __init__(self, num_context_frames, **kwargs):
998
+ super().__init__(**kwargs)
999
+ self.num_context_frames = int(num_context_frames)
1000
+
1001
+ def _build_spatial_cond(self, z_l2_start, z_l2_end, num_frames):
1002
+ device = z_l2_start.device
1003
+ dtype = z_l2_start.dtype
1004
+
1005
+ z_start_tok = self._patchify_l2(z_l2_start.unsqueeze(1)) # (B, 1, N, D)
1006
+ z_end_tok = self._patchify_l2(z_l2_end.unsqueeze(1)) # (B, 1, N, D)
1007
+ z_zero_tok = torch.zeros_like(z_start_tok) # (B, 1, N, D)
1008
+
1009
+ ctx_idx = self.num_context_frames - 1
1010
+
1011
+ frames_cond = []
1012
+ for i in range(num_frames):
1013
+ pos_val = i / (num_frames - 1) if num_frames > 1 else 0.0
1014
+ pos = torch.tensor([[pos_val]], device=device, dtype=dtype)
1015
+ pos_emb = self.position_proj(pos) # (1, D)
1016
+
1017
+ if i == ctx_idx:
1018
+ z_tok = z_start_tok
1019
+ elif i == num_frames - 1:
1020
+ z_tok = z_end_tok
1021
+ else:
1022
+ z_tok = z_zero_tok
1023
+
1024
+ frames_cond.append(z_tok + pos_emb.unsqueeze(0)) # (B, 1, N, D)
1025
+
1026
+ return torch.cat(frames_cond, dim=1) # (B, F, N, D)
orbis2/networks/DiT/dit_v2.py ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import inspect
2
+
3
+ import torch
4
+ import torch.nn as nn
5
+ from einops import rearrange
6
+ from omegaconf import ListConfig
7
+ from timm.models.vision_transformer import PatchEmbed
8
+
9
+ # Returns True when executing inside a torch.compile region (PyTorch >= 2.1).
10
+ # Guarding side-effect dict writes with this prevents graph breaks during compiled inference.
11
+ _is_compiling = getattr(torch.compiler, 'is_compiling', lambda: False)
12
+
13
+ from modules.dit import (
14
+ FinalLayer,
15
+ FrequencyEncoder,
16
+ TimestepEmbedder,
17
+ get_2d_sincos_pos_embed,
18
+ get_norm_layer,
19
+ )
20
+ from util import get_obj_from_str, instantiate_from_config
21
+
22
+
23
+ def _signature_accepts_kwargs(cls):
24
+ return any(param.kind == inspect.Parameter.VAR_KEYWORD for param in inspect.signature(cls.__init__).parameters.values())
25
+
26
+
27
+ def _accepted_init_param_names(cls):
28
+ names = set()
29
+ for base_cls in inspect.getmro(cls):
30
+ if base_cls is object:
31
+ continue
32
+ signature = inspect.signature(base_cls.__init__)
33
+ for name, param in signature.parameters.items():
34
+ if name == "self" or param.kind == inspect.Parameter.VAR_KEYWORD:
35
+ continue
36
+ names.add(name)
37
+ return names
38
+
39
+
40
+ def _embed_timesteps_with(t, t_embedder):
41
+ if t.ndim == 1:
42
+ return t_embedder(t)
43
+ if t.ndim == 2:
44
+ b, f = t.shape
45
+ c = t_embedder(rearrange(t, "b f -> (b f)"))
46
+ return rearrange(c, "(b f) d -> b f d", b=b, f=f)
47
+ raise ValueError(f"Unsupported timestep shape: {t.shape}")
48
+
49
+
50
+ class GlobalAdditiveConditioner(nn.Module):
51
+ """
52
+ Sum optional timestep embeddings and extra global condition embeddings.
53
+
54
+ The DiT module continues to own `t_embedder` for checkpoint compatibility. Extra
55
+ condition embedders are instantiated here and consume keyworded condition tensors.
56
+ """
57
+
58
+ def __init__(self, hidden_size, condition_configs=None):
59
+ super().__init__()
60
+ self.hidden_size = hidden_size
61
+ self.condition_embedders = nn.ModuleDict()
62
+
63
+ condition_configs = condition_configs or {}
64
+ for condition_name, embedder_config in condition_configs.items():
65
+ self.condition_embedders[condition_name] = instantiate_from_config(embedder_config)
66
+
67
+ def _reduce_global_condition_embedding(self, condition_name, embedding):
68
+ if embedding.ndim == 2:
69
+ return embedding
70
+ if embedding.ndim == 3 and embedding.shape[1] == 1:
71
+ return embedding[:, 0]
72
+ raise ValueError(
73
+ f"GlobalAdditiveConditioner only supports global condition embeddings with shape [B, H] "
74
+ f"or [B, 1, H]. Condition `{condition_name}` produced {tuple(embedding.shape)}."
75
+ )
76
+
77
+ def _combine_condition_embedding(self, conditioning, condition_embedding):
78
+ if torch.is_tensor(conditioning) and conditioning.ndim == 3 and condition_embedding.ndim == 2:
79
+ raise ValueError(f"{type(self).__name__} got per-frame conditioning {tuple(conditioning.shape)} but a global embedding {tuple(condition_embedding.shape)}; use PerFrameSequenceGlobalAdditiveConditioner for per-frame timestep conditioning.")
80
+ return conditioning + condition_embedding
81
+
82
+ def forward(self, *, t=None, t_embedder=None, **condition_kwargs):
83
+ assert (t is None) == (t_embedder is None)
84
+ conditioning = 0 if t is None else _embed_timesteps_with(t, t_embedder)
85
+ if not _is_compiling():
86
+ self._last_condition_embeddings = {}
87
+ for condition_name, embedder in self.condition_embedders.items():
88
+ condition_embedding = embedder(condition_kwargs[condition_name])
89
+ condition_embedding = self._reduce_global_condition_embedding(condition_name, condition_embedding)
90
+ if not _is_compiling():
91
+ self._last_condition_embeddings[condition_name] = condition_embedding.detach()
92
+ conditioning = self._combine_condition_embedding(conditioning, condition_embedding)
93
+ return conditioning
94
+
95
+
96
+ class SequenceGlobalAdditiveConditioner(GlobalAdditiveConditioner):
97
+ """
98
+ Create a single global conditioning vector from sequence-structured conditions.
99
+
100
+ Each condition embedder may return embeddings with shape [B, K, H].
101
+ Optionally, a positional embedding can be added to each of the K embeddings before aggregation.
102
+ This class aggregates across K before adding the result to the timestep embedding.
103
+ """
104
+
105
+ def __init__(self, hidden_size, condition_configs=None, aggregation="sum", add_positional_embedding=False, sequence_length=None):
106
+ super().__init__(hidden_size=hidden_size, condition_configs=condition_configs)
107
+ if aggregation not in {"sum", "mean"}:
108
+ raise ValueError(f"Unsupported aggregation `{aggregation}`. Expected one of: sum, mean.")
109
+ self.aggregation = aggregation
110
+ self.add_positional_embedding = add_positional_embedding
111
+ self.sequence_length = sequence_length
112
+ if add_positional_embedding:
113
+ if sequence_length is None: raise ValueError("sequence_length must be specified if add_positional_embedding is True.")
114
+ # One learnable positional embedding table per condition_name.
115
+ # Assumes self.condition_configs exists and is iterable/dict-like from parent.
116
+ self.positional_embeddings = nn.ParameterDict()
117
+ for condition_name in self.condition_embedders.keys():
118
+ pe = nn.Parameter(torch.zeros(sequence_length, hidden_size))
119
+ nn.init.normal_(pe, mean=0.0, std=0.02)
120
+ self.positional_embeddings[condition_name] = pe
121
+ else:
122
+ self.positional_embeddings = None
123
+
124
+ def _reduce_global_condition_embedding(self, condition_name, embedding):
125
+ if embedding.ndim == 2:
126
+ return embedding
127
+ if embedding.ndim != 3:
128
+ raise ValueError(
129
+ f"SequenceGlobalAdditiveConditioner expects condition `{condition_name}` to produce "
130
+ f"shape [B, H] or [B, K, H], got {tuple(embedding.shape)}."
131
+ )
132
+
133
+ # Enforce positional embeddings for true sequences (K > 1)
134
+ k = embedding.shape[1]
135
+ if k > 1 and not self.add_positional_embedding:
136
+ raise ValueError(
137
+ f"Condition `{condition_name}` produced sequence embedding with K={k}, but "
138
+ f"`add_positional_embedding=False`. Positional embedding must be enabled when K > 1."
139
+ )
140
+
141
+ if self.add_positional_embedding:
142
+ embedding = self._add_positional_embeddings(condition_name, embedding)
143
+
144
+ if self.aggregation == "sum":
145
+ return embedding.sum(dim=1)
146
+ return embedding.mean(dim=1)
147
+
148
+ def _add_positional_embeddings(self, condition_name, embedding):
149
+ """
150
+ embedding: [B, K, H]
151
+ returns: [B, K, H] with learnable positional embeddings added
152
+ """
153
+ if embedding.ndim != 3:
154
+ raise ValueError(
155
+ f"_add_positional_embeddings expects [B, K, H] for `{condition_name}`, "
156
+ f"got {tuple(embedding.shape)}."
157
+ )
158
+
159
+ b, k, h = embedding.shape
160
+
161
+ if h != self.hidden_size:
162
+ raise ValueError(
163
+ f"Hidden size mismatch for condition `{condition_name}`: "
164
+ f"embedding has H={h}, expected hidden_size={self.hidden_size}."
165
+ )
166
+
167
+ if self.positional_embeddings is None:
168
+ raise RuntimeError(
169
+ "Positional embedding is not initialized. "
170
+ "Set add_positional_embedding=True in constructor."
171
+ )
172
+
173
+ if k > self.sequence_length:
174
+ raise ValueError(
175
+ f"Condition `{condition_name}` has sequence length K={k}, "
176
+ f"but configured sequence_length={self.sequence_length}. "
177
+ f"Increase sequence_length or truncate inputs."
178
+ )
179
+
180
+ # [K, H] -> [1, K, H], broadcast over batch
181
+ pos = self.positional_embeddings[condition_name][:k].unsqueeze(0)
182
+ pos = pos.to(device=embedding.device, dtype=embedding.dtype)
183
+ return embedding + pos
184
+
185
+
186
+ class PerFrameSequenceGlobalAdditiveConditioner(SequenceGlobalAdditiveConditioner):
187
+ """
188
+ Same as SequenceGlobalAdditiveConditioner, but broadcasts global [B, H] condition
189
+ embeddings across frames when timestep conditioning is per-frame (conditioning is
190
+ [B, F, H] instead of [B, H]), instead of colliding F against B.
191
+ """
192
+
193
+ def _combine_condition_embedding(self, conditioning, condition_embedding):
194
+ if torch.is_tensor(conditioning) and conditioning.ndim == 3:
195
+ condition_embedding = condition_embedding.unsqueeze(1) # [B, H] -> [B, 1, H]
196
+ return conditioning + condition_embedding
197
+
198
+
199
+ class DiT(nn.Module):
200
+ """
201
+ Generic DiT backbone with configurable block construction and execution layout.
202
+
203
+ The model does not distinguish target and context inputs. Forward accepts only a
204
+ generic sequence of frames `x` with shape [B, F, C, H, W].
205
+ """
206
+
207
+ def __init__(
208
+ self,
209
+ input_size=16,
210
+ patch_size=2,
211
+ in_channels=32,
212
+ hidden_size=1152,
213
+ depth=28,
214
+ num_heads=16,
215
+ mlp_ratio=4.0,
216
+ max_num_frames=6,
217
+ dropout=0.0,
218
+ frequency_range=(2, 15),
219
+ learn_sigma=False,
220
+ norm_layer="layer_norm",
221
+ mlp_block="mlp",
222
+ block_config=None,
223
+ conditioner_config=None,
224
+ frame_embedding_alignment="suffix",
225
+ ctx_noise_aug_ratio=0.0,
226
+ ctx_noise_aug_prob=0.0,
227
+ drop_ctx_rate=0.0,
228
+ log_adaln_mean_abs=False,
229
+ log_adaln_grad=False,
230
+ adaln_gate_init_std=0.0,
231
+ train_steering_adaln_only=False,
232
+ ):
233
+ super().__init__()
234
+ self._validate_legacy_context_args(
235
+ ctx_noise_aug_ratio=ctx_noise_aug_ratio,
236
+ ctx_noise_aug_prob=ctx_noise_aug_prob,
237
+ drop_ctx_rate=drop_ctx_rate,
238
+ )
239
+
240
+ if isinstance(norm_layer, str):
241
+ norm_layer = get_norm_layer(norm_layer)
242
+
243
+ self.input_size = input_size if isinstance(input_size, (list, tuple, ListConfig)) else [input_size, input_size]
244
+ self.in_channels = in_channels
245
+ self.hidden_size = hidden_size
246
+ self.depth = depth
247
+ self.num_heads = num_heads
248
+ self.mlp_ratio = mlp_ratio
249
+ self.dropout = dropout
250
+ self.max_num_frames = max_num_frames
251
+ self.norm_layer = norm_layer
252
+ self.mlp_block = mlp_block
253
+ self.log_adaln_mean_abs = bool(log_adaln_mean_abs)
254
+ self.log_adaln_grad = bool(log_adaln_grad)
255
+ self.adaln_gate_init_std = float(adaln_gate_init_std)
256
+ self._last_adaln_mean_abs = {}
257
+ self.frame_embedding_alignment = self._normalize_frame_embedding_alignment(
258
+ frame_embedding_alignment
259
+ )
260
+
261
+ self.out_channels = in_channels * 2 if learn_sigma else in_channels
262
+ self.patch_size = patch_size
263
+
264
+ self.x_embedder = PatchEmbed(input_size, patch_size, in_channels, hidden_size, bias=True)
265
+ self.num_patches = self.x_embedder.num_patches
266
+ self.t_embedder = TimestepEmbedder(hidden_size)
267
+ self.conditioner = self._build_conditioner(conditioner_config)
268
+ self.pos_embed = nn.Parameter(torch.zeros(1, self.num_patches, hidden_size), requires_grad=False)
269
+ self.frame_emb = nn.init.trunc_normal_(nn.Parameter(torch.zeros(1, self.max_num_frames, 1, hidden_size)), 0.0, 0.02)
270
+ self.frame_rate_encoder = FrequencyEncoder(hidden_size, freq_min=frequency_range[0], freq_max=frequency_range[1])
271
+
272
+ if block_config is None:
273
+ raise ValueError("DiT v2 expects block_config in instantiate_from_config format.")
274
+ if "target" not in block_config:
275
+ raise KeyError("block_config must define `target`.")
276
+
277
+ self.block_config = block_config
278
+ self.block_cls = get_obj_from_str(block_config["target"])
279
+ self.blocks = self._build_blocks()
280
+ self._configure_block_logging()
281
+
282
+ self.final_layer = FinalLayer(hidden_size, patch_size, self.out_channels, norm_layer=norm_layer)
283
+ self.initialize_weights()
284
+
285
+ if train_steering_adaln_only:
286
+ self._freeze_non_steering_adaln_parameters()
287
+
288
+ def _freeze_non_steering_adaln_parameters(self):
289
+ self.requires_grad_(False)
290
+ self.t_embedder.requires_grad_(True)
291
+ cond_embedders = getattr(self.conditioner, "condition_embedders", {})
292
+ if "steering" in cond_embedders:
293
+ cond_embedders["steering"].requires_grad_(True)
294
+ for block in self.blocks:
295
+ block.adaLN_modulation.requires_grad_(True)
296
+ if hasattr(block, "adaLN_time_attn_modulation"):
297
+ block.adaLN_time_attn_modulation.requires_grad_(True)
298
+ self.final_layer.adaLN_modulation.requires_grad_(True)
299
+
300
+ def _validate_legacy_context_args(self, *, ctx_noise_aug_ratio, ctx_noise_aug_prob, drop_ctx_rate):
301
+ unsupported = {
302
+ "ctx_noise_aug_ratio": ctx_noise_aug_ratio,
303
+ "ctx_noise_aug_prob": ctx_noise_aug_prob,
304
+ "drop_ctx_rate": drop_ctx_rate,
305
+ }
306
+ non_zero = {name: value for name, value in unsupported.items() if value not in (None, 0, 0.0)}
307
+ if non_zero:
308
+ raise ValueError(
309
+ "DiT v2 does not implement context dropping or context noise augmentation. "
310
+ f"Received: {non_zero}"
311
+ )
312
+
313
+ def _normalize_frame_embedding_alignment(self, frame_embedding_alignment):
314
+ alignment = str(frame_embedding_alignment).strip().lower()
315
+ if alignment not in {"suffix", "prefix"}:
316
+ raise ValueError(
317
+ "frame_embedding_alignment must be one of {'suffix', 'prefix'}, "
318
+ f"got {frame_embedding_alignment!r}."
319
+ )
320
+ return alignment
321
+
322
+ def _get_frame_embeddings(self, num_frames, device):
323
+ if self.frame_embedding_alignment == "prefix":
324
+ return self.frame_emb[:, :num_frames].to(device)
325
+ return self.frame_emb[:, self.max_num_frames - num_frames :].to(device)
326
+
327
+ def _get_common_block_kwargs(self, layer_idx):
328
+ return {
329
+ "hidden_size": self.hidden_size,
330
+ "num_heads": self.num_heads,
331
+ "mlp_ratio": self.mlp_ratio,
332
+ "dropout_rate": self.dropout,
333
+ "norm_layer": self.norm_layer,
334
+ "mlp_block": self.mlp_block,
335
+ "input_shape": self.input_size,
336
+ "layer_idx": layer_idx,
337
+ }
338
+
339
+ def _build_single_block(self, layer_idx):
340
+ accepts_kwargs = _signature_accepts_kwargs(self.block_cls)
341
+ common_kwargs = self._get_common_block_kwargs(layer_idx)
342
+ user_kwargs = dict(self.block_config.get("params", {}))
343
+
344
+ valid_names = _accepted_init_param_names(self.block_cls)
345
+ if not accepts_kwargs:
346
+ unknown_user_kwargs = sorted(set(user_kwargs) - valid_names)
347
+ if unknown_user_kwargs:
348
+ raise ValueError(
349
+ f"Unsupported block parameters for {self.block_cls.__name__}: {unknown_user_kwargs}"
350
+ )
351
+
352
+ init_kwargs = {}
353
+ for name, value in common_kwargs.items():
354
+ if name in valid_names:
355
+ init_kwargs[name] = value
356
+ init_kwargs.update(user_kwargs)
357
+ return instantiate_from_config(
358
+ {
359
+ "target": self.block_config["target"],
360
+ "params": init_kwargs,
361
+ }
362
+ )
363
+
364
+ def _build_blocks(self):
365
+ return nn.ModuleList([self._build_single_block(layer_idx) for layer_idx in range(self.depth)])
366
+
367
+ def _configure_block_logging(self):
368
+ for block in self.blocks:
369
+ if hasattr(block, "log_adaln_mean_abs"):
370
+ block.log_adaln_mean_abs = self.log_adaln_mean_abs
371
+
372
+ def _build_conditioner(self, conditioner_config):
373
+ if conditioner_config is None:
374
+ return None
375
+
376
+ params = dict(conditioner_config.get("params", {}))
377
+ params.setdefault("hidden_size", self.hidden_size)
378
+ return instantiate_from_config(
379
+ {
380
+ "target": conditioner_config["target"],
381
+ "params": params,
382
+ }
383
+ )
384
+
385
+ def initialize_weights(self):
386
+ def _basic_init(module):
387
+ if isinstance(module, nn.Linear):
388
+ nn.init.xavier_uniform_(module.weight)
389
+ if module.bias is not None:
390
+ nn.init.constant_(module.bias, 0)
391
+
392
+ self.apply(_basic_init)
393
+
394
+ pos_embed = get_2d_sincos_pos_embed(
395
+ self.pos_embed.shape[-1],
396
+ [self.input_size[0] // self.patch_size, self.input_size[1] // self.patch_size],
397
+ cls_token=False,
398
+ extra_tokens=0,
399
+ )
400
+ self.pos_embed.data.copy_(torch.from_numpy(pos_embed).float().unsqueeze(0))
401
+
402
+ w = self.x_embedder.proj.weight.data
403
+ nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
404
+ nn.init.constant_(self.x_embedder.proj.bias, 0)
405
+
406
+ nn.init.normal_(self.t_embedder.mlp[0].weight, std=0.02)
407
+ nn.init.normal_(self.t_embedder.mlp[2].weight, std=0.02)
408
+
409
+ for block in self.blocks:
410
+ if hasattr(block, "initialize_adaln_weights"):
411
+ block.initialize_adaln_weights(self.adaln_gate_init_std)
412
+
413
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].weight, 0)
414
+ nn.init.constant_(self.final_layer.adaLN_modulation[-1].bias, 0)
415
+ nn.init.constant_(self.final_layer.linear.weight, 0)
416
+ nn.init.constant_(self.final_layer.linear.bias, 0)
417
+
418
+ def unpatchify(self, x):
419
+ c = self.out_channels
420
+ p = self.x_embedder.patch_size[0]
421
+ h = self.x_embedder.grid_size[0]
422
+ w = self.x_embedder.grid_size[1]
423
+
424
+ x = x.reshape(shape=(x.shape[0], x.shape[1], h, w, p, p, c))
425
+ x = torch.einsum("bfhwpqc->bfchpwq", x)
426
+ imgs = x.reshape(shape=(x.shape[0], x.shape[1], c, h * p, w * p))
427
+ return imgs
428
+
429
+ def postprocess_outputs(self, out):
430
+ return self.unpatchify(out)
431
+
432
+ def get_condition_embeddings(self, t, **_kwargs):
433
+ if self.conditioner is None:
434
+ return _embed_timesteps_with(t, self.t_embedder)
435
+ return self.conditioner(t=t, t_embedder=self.t_embedder, **_kwargs)
436
+
437
+ def _get_frame_rate_embeddings(self, frame_rate, batch_size, num_frames, device):
438
+ if frame_rate is None:
439
+ return torch.zeros(batch_size, 1, 1, self.hidden_size, device=device)
440
+
441
+ frame_rate_embeddings = self.frame_rate_encoder.encode(frame_rate).to(device)
442
+ if frame_rate_embeddings.ndim != 2 or frame_rate_embeddings.shape[0] != batch_size:
443
+ raise ValueError(
444
+ "frame_rate must have shape [B]. "
445
+ f"Received embedding source with shape {tuple(frame_rate.shape)}"
446
+ )
447
+ return frame_rate_embeddings.unsqueeze(1).unsqueeze(1)
448
+
449
+ def embed_inputs(self, x, frame_rate=None):
450
+ if x.ndim != 5:
451
+ raise ValueError(f"Expected x to have shape [B, F, C, H, W], got {tuple(x.shape)}")
452
+
453
+ b, f = x.shape[:2]
454
+ if f > self.max_num_frames:
455
+ raise ValueError(f"Received {f} frames, but max_num_frames={self.max_num_frames}")
456
+
457
+ frame_embeddings = self._get_frame_rate_embeddings(frame_rate, batch_size=b, num_frames=f, device=x.device)
458
+ x = rearrange(x, "b f c h w -> (b f) c h w")
459
+ x = self.x_embedder(x) + self.pos_embed.to(x.device)
460
+ x = rearrange(x, "(b f) hw c -> b f hw c", b=b, f=f)
461
+ x = x + self._get_frame_embeddings(f, x.device) + frame_embeddings
462
+ return x
463
+
464
+ def run_blocks(self, x, c, return_features=False):
465
+ features = []
466
+ adaln_stats = {}
467
+ for block_idx, block in enumerate(self.blocks):
468
+ x = block(x, c)
469
+ if self.log_adaln_mean_abs:
470
+ block_stats = getattr(block, "_last_adaln_mean_abs", None)
471
+ if block_stats:
472
+ adaln_stats.update(
473
+ {
474
+ f"block_{block_idx:02d}/{name}": value
475
+ for name, value in block_stats.items()
476
+ }
477
+ )
478
+ if return_features:
479
+ features.append(x)
480
+ if self.log_adaln_mean_abs:
481
+ self._last_adaln_mean_abs = adaln_stats
482
+ return x, features
483
+
484
+ def get_last_adaln_mean_abs(self):
485
+ return self._last_adaln_mean_abs
486
+
487
+ def get_last_condition_embeddings(self):
488
+ if self.conditioner is None:
489
+ return {}
490
+ return dict(getattr(self.conditioner, '_last_condition_embeddings', {}))
491
+
492
+ def _iter_conditioners(self):
493
+ if self.conditioner is not None:
494
+ yield ("", self.conditioner)
495
+
496
+ def get_adaln_grad_stats(self):
497
+ if not self.log_adaln_grad:
498
+ return {}
499
+ stats = {}
500
+ for block_idx, block in enumerate(self.blocks):
501
+ for mod_name in ("adaLN_modulation", "adaLN_time_attn_modulation", "adaLN_registers_modulation"):
502
+ mod = getattr(block, mod_name, None)
503
+ if mod is None:
504
+ continue
505
+ linear = mod[-1]
506
+ for param_name in ("weight", "bias"):
507
+ p = getattr(linear, param_name, None)
508
+ if p is None or p.grad is None:
509
+ continue
510
+ stats[f"block_{block_idx:02d}/{mod_name}/{param_name}"] = p.grad.detach()
511
+ for cond_prefix, conditioner in self._iter_conditioners():
512
+ for cond_name, embedder in conditioner.condition_embedders.items():
513
+ for pname, p in embedder.named_parameters():
514
+ if p.grad is not None:
515
+ stats[f"{cond_prefix}cond_emb/{cond_name}/{pname}"] = p.grad.detach()
516
+ return stats
517
+
518
+ def forward(
519
+ self,
520
+ x,
521
+ t,
522
+ frame_rate=None,
523
+ return_features=False,
524
+ **condition_kwargs,
525
+ ):
526
+ c = self.get_condition_embeddings(t, **condition_kwargs)
527
+ x = self.embed_inputs(x, frame_rate=frame_rate)
528
+ x, features = self.run_blocks(x, c, return_features=return_features)
529
+ out = self.final_layer(x, c)
530
+ out = self.postprocess_outputs(out)
531
+ if return_features:
532
+ return out, features
533
+ return out
534
+
535
+
536
+ class DiTWithRegisters(DiT):
537
+ def __init__(self, registers_conditioner_config=None, train_steering_adaln_only=False, **kwargs):
538
+ super().__init__(train_steering_adaln_only=False, **kwargs)
539
+ self.registers_conditioner = self._build_conditioner(registers_conditioner_config)
540
+ if self.registers_conditioner is not None:
541
+ self.registers_conditioner.apply(self._initialize_linear_module)
542
+ if train_steering_adaln_only:
543
+ self._freeze_non_steering_adaln_parameters()
544
+
545
+ @staticmethod
546
+ def _initialize_linear_module(module):
547
+ if isinstance(module, nn.Linear):
548
+ nn.init.xavier_uniform_(module.weight)
549
+ if module.bias is not None:
550
+ nn.init.constant_(module.bias, 0)
551
+
552
+ def _freeze_non_steering_adaln_parameters(self):
553
+ self.requires_grad_(False)
554
+ if self.registers_conditioner is not None:
555
+ self.registers_conditioner.requires_grad_(True)
556
+ for block in self.blocks:
557
+ if hasattr(block, "adaLN_registers_modulation"):
558
+ block.adaLN_registers_modulation.requires_grad_(True)
559
+
560
+ def get_registers_condition_embeddings(self, **condition_kwargs):
561
+ if self.registers_conditioner is None:
562
+ return None
563
+ return self.registers_conditioner(**condition_kwargs)
564
+
565
+ def get_last_condition_embeddings(self):
566
+ result = super().get_last_condition_embeddings()
567
+ if self.registers_conditioner is not None:
568
+ reg_embs = getattr(self.registers_conditioner, '_last_condition_embeddings', {})
569
+ result.update({f"registers/{k}": v for k, v in reg_embs.items()})
570
+ return result
571
+
572
+ def _iter_conditioners(self):
573
+ yield from super()._iter_conditioners()
574
+ if self.registers_conditioner is not None:
575
+ yield ("registers/", self.registers_conditioner)
576
+
577
+ def run_blocks(self, x, c, c_registers=None, return_features=False):
578
+ features = []
579
+ adaln_stats = {}
580
+ for block_idx, block in enumerate(self.blocks):
581
+ if c_registers is None:
582
+ x = block(x, c=c)
583
+ else:
584
+ x = block(x, c=c, c_registers=c_registers)
585
+ if self.log_adaln_mean_abs:
586
+ block_stats = getattr(block, "_last_adaln_mean_abs", None)
587
+ if block_stats:
588
+ adaln_stats.update(
589
+ {
590
+ f"block_{block_idx:02d}/{name}": value
591
+ for name, value in block_stats.items()
592
+ }
593
+ )
594
+ if return_features:
595
+ features.append(x)
596
+ if self.log_adaln_mean_abs:
597
+ self._last_adaln_mean_abs = adaln_stats
598
+ return x, features
599
+
600
+ def forward(
601
+ self,
602
+ x,
603
+ t,
604
+ frame_rate=None,
605
+ return_features=False,
606
+ **condition_kwargs,
607
+ ):
608
+ c = self.get_condition_embeddings(t, **condition_kwargs)
609
+ c_registers = self.get_registers_condition_embeddings(**condition_kwargs)
610
+ x = self.embed_inputs(x, frame_rate=frame_rate)
611
+ x, features = self.run_blocks(x, c, c_registers=c_registers, return_features=return_features)
612
+ out = self.final_layer(x, c)
613
+ out = self.postprocess_outputs(out)
614
+ if return_features:
615
+ return out, features
616
+ return out
617
+
618
+
619
+ class SpatialL2CtxAndLastDiT(DiT):
620
+ """
621
+ v2 DiT equivalent of the v1 STDiTDFSpatialL2_CtxAndLast network.
622
+
623
+ Spatial L2 endpoint latents are patchified and injected into blocks that
624
+ accept `block(x, c, z_spatial)`. Only the last context frame and last frame
625
+ receive non-zero L2 tokens; all frames receive a scalar position embedding.
626
+ """
627
+
628
+ def __init__(self, sem_in_channels, num_context_frames, frame_embedding_alignment, **kwargs):
629
+ super().__init__(frame_embedding_alignment=frame_embedding_alignment, **kwargs)
630
+ self.sem_in_channels = int(sem_in_channels)
631
+ self.num_context_frames = int(num_context_frames)
632
+
633
+ self.l2_patchify = nn.Linear(
634
+ self.patch_size ** 2 * self.sem_in_channels,
635
+ self.hidden_size,
636
+ bias=True,
637
+ )
638
+ nn.init.xavier_uniform_(self.l2_patchify.weight)
639
+ nn.init.zeros_(self.l2_patchify.bias)
640
+
641
+ self.position_proj = nn.Sequential(
642
+ nn.Linear(1, self.hidden_size),
643
+ nn.SiLU(),
644
+ nn.Linear(self.hidden_size, self.hidden_size),
645
+ )
646
+ for module in self.position_proj.modules():
647
+ if isinstance(module, nn.Linear):
648
+ nn.init.xavier_uniform_(module.weight)
649
+ nn.init.zeros_(module.bias)
650
+
651
+ def _patchify_l2(self, z_l2):
652
+ if z_l2.ndim != 5:
653
+ raise ValueError(f"Expected z_l2 to have shape [B, F, C, H, W], got {tuple(z_l2.shape)}")
654
+ b, f, c, h, w = z_l2.shape
655
+ if c != self.sem_in_channels:
656
+ raise ValueError(
657
+ f"Expected L2 latents with {self.sem_in_channels} channels, got {c}."
658
+ )
659
+ if h % self.patch_size != 0 or w % self.patch_size != 0:
660
+ raise ValueError(
661
+ f"L2 latent spatial size {(h, w)} must be divisible by patch_size={self.patch_size}."
662
+ )
663
+
664
+ z = rearrange(
665
+ z_l2,
666
+ "b f c (h p1) (w p2) -> (b f) (h w) (p1 p2 c)",
667
+ p1=self.patch_size,
668
+ p2=self.patch_size,
669
+ )
670
+ z = self.l2_patchify(z)
671
+ return rearrange(z, "(b f) n d -> b f n d", b=b, f=f)
672
+
673
+ def _build_spatial_cond(self, z_l2_start, z_l2_end, num_frames):
674
+ if z_l2_start is None or z_l2_end is None:
675
+ raise ValueError("SpatialL2CtxAndLastDiT requires z_l2_start and z_l2_end.")
676
+ if z_l2_start.shape != z_l2_end.shape:
677
+ raise ValueError(
678
+ "z_l2_start and z_l2_end must have matching shapes, got "
679
+ f"{tuple(z_l2_start.shape)} and {tuple(z_l2_end.shape)}."
680
+ )
681
+ if self.num_context_frames < 1:
682
+ raise ValueError("num_context_frames must be at least 1.")
683
+ if self.num_context_frames > num_frames:
684
+ raise ValueError(
685
+ f"num_context_frames={self.num_context_frames} exceeds num_frames={num_frames}."
686
+ )
687
+
688
+ device = z_l2_start.device
689
+ dtype = z_l2_start.dtype
690
+ z_start_tok = self._patchify_l2(z_l2_start.unsqueeze(1))
691
+ z_end_tok = self._patchify_l2(z_l2_end.unsqueeze(1))
692
+ z_zero_tok = torch.zeros_like(z_start_tok)
693
+ ctx_idx = self.num_context_frames - 1
694
+
695
+ frames_cond = []
696
+ for frame_idx in range(num_frames):
697
+ pos_val = frame_idx / (num_frames - 1) if num_frames > 1 else 0.0
698
+ pos = torch.tensor([[pos_val]], device=device, dtype=dtype)
699
+ pos_emb = self.position_proj(pos)
700
+
701
+ if frame_idx == ctx_idx:
702
+ z_tok = z_start_tok
703
+ elif frame_idx == num_frames - 1:
704
+ z_tok = z_end_tok
705
+ else:
706
+ z_tok = z_zero_tok
707
+ frames_cond.append(z_tok + pos_emb.unsqueeze(0))
708
+
709
+ return torch.cat(frames_cond, dim=1)
710
+
711
+ def run_spatial_l2_blocks(self, x, c, z_spatial, return_features=False):
712
+ features = []
713
+ for block in self.blocks:
714
+ x = block(x, c, z_spatial)
715
+ if return_features:
716
+ features.append(x)
717
+ return x, features
718
+
719
+ def forward(
720
+ self,
721
+ x,
722
+ t,
723
+ frame_rate=None,
724
+ z_l2_start=None,
725
+ z_l2_end=None,
726
+ return_features=False,
727
+ **condition_kwargs,
728
+ ):
729
+ if z_l2_start is None or z_l2_end is None:
730
+ raise ValueError("SpatialL2CtxAndLastDiT requires z_l2_start and z_l2_end.")
731
+
732
+ c = self.get_condition_embeddings(t, **condition_kwargs)
733
+ x = self.embed_inputs(x, frame_rate=frame_rate)
734
+ z_spatial = self._build_spatial_cond(z_l2_start, z_l2_end, x.size(1))
735
+ x, features = self.run_spatial_l2_blocks(x, c, z_spatial, return_features=return_features)
736
+ out = self.final_layer(x, c)
737
+ out = self.postprocess_outputs(out)
738
+ if return_features:
739
+ return out, features
740
+ return out
orbis2/networks/swin/swin_free_aspect_ratio.py ADDED
@@ -0,0 +1,740 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --------------------------------------------------------
2
+ # Swin Transformer
3
+ # Copyright (c) 2021 Microsoft
4
+ # Licensed under The MIT License [see LICENSE for details]
5
+ # Written by Ze Liu
6
+ # --------------------------------------------------------
7
+
8
+ import torch
9
+ import torch.nn as nn
10
+ import torch.utils.checkpoint as checkpoint
11
+ from timm.models.layers import DropPath, to_2tuple, trunc_normal_
12
+ from timm.layers.mlp import SwiGLU
13
+
14
+ try:
15
+ import os, sys
16
+
17
+ kernel_path = os.path.abspath(os.path.join('..'))
18
+ sys.path.append(kernel_path)
19
+ from kernels.window_process.window_process import WindowProcess, WindowProcessReverse
20
+
21
+ except:
22
+ WindowProcess = None
23
+ WindowProcessReverse = None
24
+ print("[Warning] Fused window process have not been installed. Please refer to get_started.md for installation.")
25
+
26
+
27
+ class Mlp(nn.Module):
28
+ def __init__(self, in_features, hidden_features=None, out_features=None, act_layer=nn.GELU, drop=0.):
29
+ super().__init__()
30
+ out_features = out_features or in_features
31
+ hidden_features = hidden_features or in_features
32
+ self.fc1 = nn.Linear(in_features, hidden_features)
33
+ self.act = act_layer()
34
+ self.fc2 = nn.Linear(hidden_features, out_features)
35
+ self.drop = nn.Dropout(drop)
36
+
37
+ def forward(self, x):
38
+ x = self.fc1(x)
39
+ x = self.act(x)
40
+ x = self.drop(x)
41
+ x = self.fc2(x)
42
+ x = self.drop(x)
43
+ return x
44
+
45
+
46
+ def window_partition(x, window_size):
47
+ """
48
+ Args:
49
+ x: (B, H, W, C)
50
+ window_size (int): window size
51
+
52
+ Returns:
53
+ windows: (num_windows*B, window_size, window_size, C)
54
+ """
55
+ B, H, W, C = x.shape
56
+ window_size = to_2tuple(window_size)
57
+ x = x.view(B, H // window_size[0], window_size[0], W // window_size[1], window_size[1], C)
58
+ windows = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(-1, *window_size, C)
59
+ return windows
60
+
61
+
62
+ def window_reverse(windows, window_size, H, W):
63
+ """
64
+ Args:
65
+ windows: (num_windows*B, window_size, window_size, C)
66
+ window_size (int): Window size
67
+ H (int): Height of image
68
+ W (int): Width of image
69
+
70
+ Returns:
71
+ x: (B, H, W, C)
72
+ """
73
+ window_size = to_2tuple(window_size)
74
+ B = int(windows.shape[0] / (H * W / window_size[0] / window_size[1]))
75
+ x = windows.view(B, H // window_size[0], W // window_size[1], window_size[0], window_size[1], -1)
76
+ x = x.permute(0, 1, 3, 2, 4, 5).contiguous().view(B, H, W, -1)
77
+ return x
78
+
79
+
80
+ class WindowAttention(nn.Module):
81
+ r""" Window based multi-head self attention (W-MSA) module with relative position bias.
82
+ It supports both of shifted and non-shifted window.
83
+
84
+ Args:
85
+ dim (int): Number of input channels.
86
+ window_size (tuple[int]): The height and width of the window.
87
+ num_heads (int): Number of attention heads.
88
+ qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
89
+ qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set
90
+ attn_drop (float, optional): Dropout ratio of attention weight. Default: 0.0
91
+ proj_drop (float, optional): Dropout ratio of output. Default: 0.0
92
+ """
93
+
94
+ def __init__(self, dim, window_size, num_heads, qkv_bias=True, qk_scale=None, qk_norm=True, attn_drop=0., proj_drop=0., fused_attn=True, norm_layer=nn.LayerNorm):
95
+
96
+ super().__init__()
97
+ self.dim = dim
98
+ self.window_size = window_size # Wh, Ww
99
+ self.num_heads = num_heads
100
+ head_dim = dim // num_heads
101
+ self.scale = qk_scale or head_dim ** -0.5
102
+ self.use_fused_attn = fused_attn
103
+ self.qk_norm = qk_norm
104
+
105
+ # define a parameter table of relative position bias
106
+ self.relative_position_bias_table = nn.Parameter(
107
+ torch.zeros((2 * window_size[0] - 1) * (2 * window_size[1] - 1), num_heads)) # 2*Wh-1 * 2*Ww-1, nH
108
+
109
+ # get pair-wise relative position index for each token inside the window
110
+ coords_h = torch.arange(self.window_size[0])
111
+ coords_w = torch.arange(self.window_size[1])
112
+ coords = torch.stack(torch.meshgrid([coords_h, coords_w])) # 2, Wh, Ww
113
+ coords_flatten = torch.flatten(coords, 1) # 2, Wh*Ww
114
+ relative_coords = coords_flatten[:, :, None] - coords_flatten[:, None, :] # 2, Wh*Ww, Wh*Ww
115
+ relative_coords = relative_coords.permute(1, 2, 0).contiguous() # Wh*Ww, Wh*Ww, 2
116
+ relative_coords[:, :, 0] += self.window_size[0] - 1 # shift to start from 0
117
+ relative_coords[:, :, 1] += self.window_size[1] - 1
118
+ relative_coords[:, :, 0] *= 2 * self.window_size[1] - 1
119
+ relative_position_index = relative_coords.sum(-1) # Wh*Ww, Wh*Ww
120
+ self.register_buffer("relative_position_index", relative_position_index)
121
+
122
+ self.qkv = nn.Linear(dim, dim * 3, bias=qkv_bias)
123
+ self.attn_drop = nn.Dropout(attn_drop)
124
+ self.proj = nn.Linear(dim, dim)
125
+ self.proj_drop = nn.Dropout(proj_drop)
126
+
127
+ self.q_norm = norm_layer(head_dim, eps=1e-6) if qk_norm else nn.Identity()
128
+ self.k_norm = norm_layer(head_dim, eps=1e-6) if qk_norm else nn.Identity()
129
+
130
+ trunc_normal_(self.relative_position_bias_table, std=.02)
131
+ self.softmax = nn.Softmax(dim=-1)
132
+
133
+ def old_attn(self, q, k, v, B_, N, relative_position_bias, mask=None):
134
+ q = q * self.scale
135
+ attn = (q @ k.transpose(-2, -1))
136
+ attn = attn + relative_position_bias.unsqueeze(0)
137
+
138
+ if mask is not None:
139
+ nW = mask.shape[0]
140
+ attn = attn.view(B_ // nW, nW, self.num_heads, N, N) + mask.unsqueeze(1).unsqueeze(0)
141
+ attn = attn.view(-1, self.num_heads, N, N)
142
+ attn = self.softmax(attn)
143
+ else:
144
+ attn = self.softmax(attn)
145
+ attn = self.attn_drop(attn)
146
+ x = (attn @ v)
147
+ return x
148
+
149
+ def fused_attn(self, q, k, v, B_, N, relative_position_bias, mask=None):
150
+ attn_mask = relative_position_bias
151
+ if mask is not None:
152
+ num_win = mask.shape[0]
153
+ mask = mask.view(1, num_win, 1, N, N).expand(B_ // num_win, -1, self.num_heads, -1, -1)
154
+ attn_mask = attn_mask + mask.reshape(-1, self.num_heads, N, N)
155
+ x = torch.nn.functional.scaled_dot_product_attention(
156
+ q, k, v,
157
+ attn_mask=attn_mask,
158
+ dropout_p=self.attn_drop.p if self.training else 0.,
159
+ )
160
+ return x
161
+
162
+ def forward(self, x, mask=None):
163
+ """
164
+ Args:
165
+ x: input features with shape of (num_windows*B, N, C)
166
+ mask: (0/-inf) mask with shape of (num_windows, Wh*Ww, Wh*Ww) or None
167
+ """
168
+ B_, N, C = x.shape
169
+ qkv = self.qkv(x).reshape(B_, N, 3, self.num_heads, C // self.num_heads).permute(2, 0, 3, 1, 4)
170
+ q, k, v = qkv[0], qkv[1], qkv[2] # make torchscript happy (cannot use tensor as tuple)
171
+ q, k = self.q_norm(q), self.k_norm(k)
172
+
173
+ relative_position_bias = self.relative_position_bias_table[self.relative_position_index.view(-1)].view(
174
+ self.window_size[0] * self.window_size[1], self.window_size[0] * self.window_size[1], -1) # Wh*Ww,Wh*Ww,nH
175
+ relative_position_bias = relative_position_bias.permute(2, 0, 1).contiguous() # nH, Wh*Ww, Wh*Ww
176
+
177
+ if self.use_fused_attn: # test using scaled dot product attention
178
+ x = self.fused_attn(q, k, v, B_, N, relative_position_bias, mask=mask)
179
+ else:
180
+ x = self.old_attn(q, k, v, B_, N, relative_position_bias, mask=mask)
181
+
182
+ x = x.transpose(1, 2).reshape(B_, N, C)
183
+ x = self.proj(x)
184
+ x = self.proj_drop(x)
185
+ return x
186
+
187
+ def extra_repr(self) -> str:
188
+ return f'dim={self.dim}, window_size={self.window_size}, num_heads={self.num_heads}'
189
+
190
+ def flops(self, N):
191
+ # calculate flops for 1 window with token length of N
192
+ flops = 0
193
+ # qkv = self.qkv(x)
194
+ flops += N * self.dim * 3 * self.dim
195
+ # attn = (q @ k.transpose(-2, -1))
196
+ flops += self.num_heads * N * (self.dim // self.num_heads) * N
197
+ # x = (attn @ v)
198
+ flops += self.num_heads * N * N * (self.dim // self.num_heads)
199
+ # x = self.proj(x)
200
+ flops += N * self.dim * self.dim
201
+ return flops
202
+
203
+
204
+ class SwinAttention(nn.Module):
205
+ """ Swin attention only, no MLP. """
206
+ def __init__(self, dim, input_resolution, num_heads, window_size=(7,7), shift_size=(0,0),
207
+ qkv_bias=True, qk_scale=None, qk_norm=False, proj_drop=0., attn_drop=0., norm_layer=nn.LayerNorm,
208
+ fused_window_process=False):
209
+ super().__init__()
210
+ self.dim = dim
211
+ self.input_resolution = input_resolution
212
+ self.num_heads = num_heads
213
+ self.window_size = window_size
214
+ self.shift_size = shift_size
215
+
216
+ if min(self.input_resolution) <= min(self.window_size) or max(self.input_resolution) <= max(self.window_size):
217
+ raise ValueError("window size cannot be larger than input resolution")
218
+ # if window size is larger than input resolution, we don't partition windows
219
+ self.shift_size = 0
220
+ self.window_size = min(self.input_resolution)
221
+ assert 0 <= self.shift_size[0] < self.window_size[0] and 0 <= self.shift_size[1] < self.window_size[1], "shift_size must in [0, window_size)"
222
+
223
+ self.attn = WindowAttention(
224
+ dim, window_size=self.window_size, num_heads=num_heads,
225
+ qkv_bias=qkv_bias, qk_scale=qk_scale, qk_norm=qk_norm,
226
+ attn_drop=attn_drop, proj_drop=proj_drop, fused_attn=True, norm_layer=norm_layer)
227
+
228
+ if self.shift_size[0] > 0 or self.shift_size[1] > 0:
229
+ # calculate attention mask for SW-MSA
230
+ H, W = self.input_resolution
231
+ img_mask = torch.zeros((1, H, W, 1)) # 1 H W 1
232
+ h_slices = (slice(0, -self.window_size[0]),
233
+ slice(-self.window_size[0], -self.shift_size[0]),
234
+ slice(-self.shift_size[0], None))
235
+ w_slices = (slice(0, -self.window_size[1]),
236
+ slice(-self.window_size[1], -self.shift_size[1]),
237
+ slice(-self.shift_size[1], None))
238
+ cnt = 0
239
+ for h in h_slices:
240
+ for w in w_slices:
241
+ img_mask[:, h, w, :] = cnt
242
+ cnt += 1
243
+
244
+ mask_windows = window_partition(img_mask, self.window_size) # nW, window_size, window_size, 1
245
+ mask_windows = mask_windows.view(-1, self.window_size[0] * self.window_size[1])
246
+ attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2)
247
+ attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
248
+ else:
249
+ attn_mask = None
250
+
251
+ self.register_buffer("attn_mask", attn_mask)
252
+ self.fused_window_process = fused_window_process
253
+
254
+ def forward(self, x):
255
+ H, W = self.input_resolution
256
+ B, L, C = x.shape
257
+ assert L == H * W, "input feature has wrong size"
258
+
259
+ x = x.view(B, H, W, C)
260
+
261
+ # cyclic shift
262
+ if self.shift_size[0] > 0 or self.shift_size[1] > 0:
263
+ if not self.fused_window_process:
264
+ shifted_x = torch.roll(x, shifts=(-self.shift_size[0], -self.shift_size[1]), dims=(1, 2))
265
+ # partition windows
266
+ x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C
267
+ else:
268
+ x_windows = WindowProcess.apply(x, B, H, W, C, -self.shift_size[0], self.window_size[1])
269
+ else:
270
+ shifted_x = x
271
+ # partition windows
272
+ x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C
273
+
274
+ x_windows = x_windows.view(-1, self.window_size[0] * self.window_size[1], C) # nW*B, window_size*window_size, C
275
+
276
+ # W-MSA/SW-MSA
277
+ attn_windows = self.attn(x_windows, mask=self.attn_mask) # nW*B, window_size*window_size, C
278
+
279
+ # merge windows
280
+ attn_windows = attn_windows.view(-1, self.window_size[0], self.window_size[1], C)
281
+
282
+ # reverse cyclic shift
283
+ if self.shift_size[0] > 0 or self.shift_size[1] > 0:
284
+ if not self.fused_window_process:
285
+ shifted_x = window_reverse(attn_windows, self.window_size, H, W) # B H' W' C
286
+ x = torch.roll(shifted_x, shifts=(self.shift_size[0], self.shift_size[1]), dims=(1, 2))
287
+ else:
288
+ x = WindowProcessReverse.apply(attn_windows, B, H, W, C, self.shift_size, self.window_size)
289
+ else:
290
+ shifted_x = window_reverse(attn_windows, self.window_size, H, W) # B H' W' C
291
+ x = shifted_x
292
+ x = x.view(B, H * W, C)
293
+
294
+ return x
295
+
296
+ class SwinTransformerBlock(nn.Module):
297
+ r""" Swin Transformer Block.
298
+
299
+ Args:
300
+ dim (int): Number of input channels.
301
+ input_resolution (tuple[int]): Input resulotion.
302
+ num_heads (int): Number of attention heads.
303
+ window_size (int): Window size.
304
+ shift_size (int): Shift size for SW-MSA.
305
+ mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
306
+ qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
307
+ qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set.
308
+ drop (float, optional): Dropout rate. Default: 0.0
309
+ attn_drop (float, optional): Attention dropout rate. Default: 0.0
310
+ drop_path (float, optional): Stochastic depth rate. Default: 0.0
311
+ act_layer (nn.Module, optional): Activation layer. Default: nn.GELU
312
+ norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
313
+ fused_window_process (bool, optional): If True, use one kernel to fused window shift & window partition for acceleration, similar for the reversed part. Default: False
314
+ """
315
+
316
+ def __init__(self, dim, input_resolution, num_heads, window_size=(7,7), shift_size=(0,0),
317
+ mlp_ratio=4., qkv_bias=True, qk_scale=None, qk_norm=True, drop=0., attn_drop=0., drop_path=0.,
318
+ act_layer=nn.GELU, norm_layer=nn.LayerNorm,
319
+ mlp_block='mlp',
320
+ fused_window_process=False):
321
+ super().__init__()
322
+ self.dim = dim
323
+ self.input_resolution = input_resolution
324
+ self.num_heads = num_heads
325
+ self.window_size = window_size
326
+ self.shift_size = shift_size
327
+ self.mlp_ratio = mlp_ratio
328
+ if min(self.input_resolution) <= min(self.window_size) or max(self.input_resolution) <= max(self.window_size):
329
+ raise ValueError("window size cannot be larger than input resolution")
330
+ # if window size is larger than input resolution, we don't partition windows
331
+ self.shift_size = 0
332
+ self.window_size = min(self.input_resolution)
333
+ assert 0 <= self.shift_size[0] < self.window_size[0] and 0 <= self.shift_size[1] < self.window_size[1], "shift_size must in [0, window_size)"
334
+
335
+ self.norm1 = norm_layer(dim)
336
+ self.attn = WindowAttention(
337
+ dim, window_size=self.window_size, num_heads=num_heads,
338
+ qkv_bias=qkv_bias, qk_scale=qk_scale, qk_norm=qk_norm, attn_drop=attn_drop, proj_drop=drop)
339
+
340
+ self.drop_path = DropPath(drop_path) if drop_path > 0. else nn.Identity()
341
+ self.norm2 = norm_layer(dim)
342
+ mlp_hidden_dim = int(dim * mlp_ratio)
343
+ if mlp_block == 'mlp':
344
+ self.mlp = Mlp(in_features=dim, hidden_features=mlp_hidden_dim, act_layer=act_layer, drop=drop)
345
+ elif mlp_block == 'swiglu':
346
+ self.mlp = SwiGLU(in_features=dim, hidden_features=(mlp_hidden_dim*2)//3)
347
+
348
+ if self.shift_size[0] > 0 or self.shift_size[1] > 0:
349
+ # calculate attention mask for SW-MSA
350
+ H, W = self.input_resolution
351
+ img_mask = torch.zeros((1, H, W, 1)) # 1 H W 1
352
+ h_slices = (slice(0, -self.window_size[0]),
353
+ slice(-self.window_size[0], -self.shift_size[0]),
354
+ slice(-self.shift_size[0], None))
355
+ w_slices = (slice(0, -self.window_size[1]),
356
+ slice(-self.window_size[1], -self.shift_size[1]),
357
+ slice(-self.shift_size[1], None))
358
+ cnt = 0
359
+ for h in h_slices:
360
+ for w in w_slices:
361
+ img_mask[:, h, w, :] = cnt
362
+ cnt += 1
363
+
364
+ mask_windows = window_partition(img_mask, self.window_size) # nW, window_size, window_size, 1
365
+ mask_windows = mask_windows.view(-1, self.window_size[0] * self.window_size[1])
366
+ attn_mask = mask_windows.unsqueeze(1) - mask_windows.unsqueeze(2)
367
+ attn_mask = attn_mask.masked_fill(attn_mask != 0, float(-100.0)).masked_fill(attn_mask == 0, float(0.0))
368
+ else:
369
+ attn_mask = None
370
+
371
+ self.register_buffer("attn_mask", attn_mask)
372
+ self.fused_window_process = fused_window_process
373
+
374
+ def forward(self, x):
375
+ H, W = self.input_resolution
376
+ B, L, C = x.shape
377
+ assert L == H * W, "input feature has wrong size"
378
+
379
+ shortcut = x
380
+ x = self.norm1(x)
381
+ x = x.view(B, H, W, C)
382
+
383
+ # cyclic shift
384
+ if self.shift_size[0] > 0 or self.shift_size[1] > 0:
385
+ if not self.fused_window_process:
386
+ shifted_x = torch.roll(x, shifts=(-self.shift_size[0], -self.shift_size[1]), dims=(1, 2))
387
+ # partition windows
388
+ x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C
389
+ else:
390
+ x_windows = WindowProcess.apply(x, B, H, W, C, -self.shift_size[0], self.window_size[1])
391
+ else:
392
+ shifted_x = x
393
+ # partition windows
394
+ x_windows = window_partition(shifted_x, self.window_size) # nW*B, window_size, window_size, C
395
+
396
+ x_windows = x_windows.view(-1, self.window_size[0] * self.window_size[1], C) # nW*B, window_size*window_size, C
397
+
398
+ # W-MSA/SW-MSA
399
+ attn_windows = self.attn(x_windows, mask=self.attn_mask) # nW*B, window_size*window_size, C
400
+
401
+ # merge windows
402
+ attn_windows = attn_windows.view(-1, self.window_size[0], self.window_size[1], C)
403
+
404
+ # reverse cyclic shift
405
+ if self.shift_size[0] > 0 or self.shift_size[1] > 0:
406
+ if not self.fused_window_process:
407
+ shifted_x = window_reverse(attn_windows, self.window_size, H, W) # B H' W' C
408
+ x = torch.roll(shifted_x, shifts=(self.shift_size[0], self.shift_size[1]), dims=(1, 2))
409
+ else:
410
+ x = WindowProcessReverse.apply(attn_windows, B, H, W, C, self.shift_size, self.window_size)
411
+ else:
412
+ shifted_x = window_reverse(attn_windows, self.window_size, H, W) # B H' W' C
413
+ x = shifted_x
414
+ x = x.view(B, H * W, C)
415
+ x = shortcut + self.drop_path(x)
416
+
417
+ # FFN
418
+ x = x + self.drop_path(self.mlp(self.norm2(x)))
419
+
420
+ return x
421
+
422
+ def extra_repr(self) -> str:
423
+ return f"dim={self.dim}, input_resolution={self.input_resolution}, num_heads={self.num_heads}, " \
424
+ f"window_size={self.window_size}, shift_size={self.shift_size}, mlp_ratio={self.mlp_ratio}"
425
+
426
+ def flops(self):
427
+ flops = 0
428
+ H, W = self.input_resolution
429
+ # norm1
430
+ flops += self.dim * H * W
431
+ # W-MSA/SW-MSA
432
+ nW = H * W / self.window_size / self.window_size
433
+ flops += nW * self.attn.flops(self.window_size * self.window_size)
434
+ # mlp
435
+ flops += 2 * H * W * self.dim * self.dim * self.mlp_ratio
436
+ # norm2
437
+ flops += self.dim * H * W
438
+ return flops
439
+
440
+
441
+ class PatchMerging(nn.Module):
442
+ r""" Patch Merging Layer.
443
+
444
+ Args:
445
+ input_resolution (tuple[int]): Resolution of input feature.
446
+ dim (int): Number of input channels.
447
+ norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
448
+ """
449
+
450
+ def __init__(self, input_resolution, dim, norm_layer=nn.LayerNorm):
451
+ super().__init__()
452
+ self.input_resolution = input_resolution
453
+ self.dim = dim
454
+ self.reduction = nn.Linear(4 * dim, 2 * dim, bias=False)
455
+ self.norm = norm_layer(4 * dim)
456
+
457
+ def forward(self, x):
458
+ """
459
+ x: B, H*W, C
460
+ """
461
+ H, W = self.input_resolution
462
+ B, L, C = x.shape
463
+ assert L == H * W, "input feature has wrong size"
464
+ assert H % 2 == 0 and W % 2 == 0, f"x size ({H}*{W}) are not even."
465
+
466
+ x = x.view(B, H, W, C)
467
+
468
+ x0 = x[:, 0::2, 0::2, :] # B H/2 W/2 C
469
+ x1 = x[:, 1::2, 0::2, :] # B H/2 W/2 C
470
+ x2 = x[:, 0::2, 1::2, :] # B H/2 W/2 C
471
+ x3 = x[:, 1::2, 1::2, :] # B H/2 W/2 C
472
+ x = torch.cat([x0, x1, x2, x3], -1) # B H/2 W/2 4*C
473
+ x = x.view(B, -1, 4 * C) # B H/2*W/2 4*C
474
+
475
+ x = self.norm(x)
476
+ x = self.reduction(x)
477
+
478
+ return x
479
+
480
+ def extra_repr(self) -> str:
481
+ return f"input_resolution={self.input_resolution}, dim={self.dim}"
482
+
483
+ def flops(self):
484
+ H, W = self.input_resolution
485
+ flops = H * W * self.dim
486
+ flops += (H // 2) * (W // 2) * 4 * self.dim * 2 * self.dim
487
+ return flops
488
+
489
+
490
+ class BasicLayer(nn.Module):
491
+ """ A basic Swin Transformer layer for one stage.
492
+
493
+ Args:
494
+ dim (int): Number of input channels.
495
+ input_resolution (tuple[int]): Input resolution.
496
+ depth (int): Number of blocks.
497
+ num_heads (int): Number of attention heads.
498
+ window_size (int): Local window size.
499
+ mlp_ratio (float): Ratio of mlp hidden dim to embedding dim.
500
+ qkv_bias (bool, optional): If True, add a learnable bias to query, key, value. Default: True
501
+ qk_scale (float | None, optional): Override default qk scale of head_dim ** -0.5 if set.
502
+ drop (float, optional): Dropout rate. Default: 0.0
503
+ attn_drop (float, optional): Attention dropout rate. Default: 0.0
504
+ drop_path (float | tuple[float], optional): Stochastic depth rate. Default: 0.0
505
+ norm_layer (nn.Module, optional): Normalization layer. Default: nn.LayerNorm
506
+ downsample (nn.Module | None, optional): Downsample layer at the end of the layer. Default: None
507
+ use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False.
508
+ fused_window_process (bool, optional): If True, use one kernel to fused window shift & window partition for acceleration, similar for the reversed part. Default: False
509
+ """
510
+
511
+ def __init__(self, dim, input_resolution, depth, num_heads, window_size,
512
+ mlp_ratio=4., qkv_bias=True, qk_scale=None, drop=0., attn_drop=0.,
513
+ drop_path=0., norm_layer=nn.LayerNorm, downsample=None, use_checkpoint=False,
514
+ fused_window_process=False):
515
+
516
+ super().__init__()
517
+ self.dim = dim
518
+ self.input_resolution = input_resolution
519
+ self.depth = depth
520
+ self.use_checkpoint = use_checkpoint
521
+
522
+ # build blocks
523
+ self.blocks = nn.ModuleList([
524
+ SwinTransformerBlock(dim=dim, input_resolution=input_resolution,
525
+ num_heads=num_heads, window_size=window_size,
526
+ shift_size=0 if (i % 2 == 0) else window_size // 2,
527
+ mlp_ratio=mlp_ratio,
528
+ qkv_bias=qkv_bias, qk_scale=qk_scale,
529
+ drop=drop, attn_drop=attn_drop,
530
+ drop_path=drop_path[i] if isinstance(drop_path, list) else drop_path,
531
+ norm_layer=norm_layer,
532
+ fused_window_process=fused_window_process)
533
+ for i in range(depth)])
534
+
535
+ # patch merging layer
536
+ if downsample is not None:
537
+ self.downsample = downsample(input_resolution, dim=dim, norm_layer=norm_layer)
538
+ else:
539
+ self.downsample = None
540
+
541
+ def forward(self, x):
542
+ for blk in self.blocks:
543
+ if self.use_checkpoint:
544
+ x = checkpoint.checkpoint(blk, x)
545
+ else:
546
+ x = blk(x)
547
+ if self.downsample is not None:
548
+ x = self.downsample(x)
549
+ return x
550
+
551
+ def extra_repr(self) -> str:
552
+ return f"dim={self.dim}, input_resolution={self.input_resolution}, depth={self.depth}"
553
+
554
+ def flops(self):
555
+ flops = 0
556
+ for blk in self.blocks:
557
+ flops += blk.flops()
558
+ if self.downsample is not None:
559
+ flops += self.downsample.flops()
560
+ return flops
561
+
562
+
563
+ class PatchEmbed(nn.Module):
564
+ r""" Image to Patch Embedding
565
+
566
+ Args:
567
+ img_size (int): Image size. Default: 224.
568
+ patch_size (int): Patch token size. Default: 4.
569
+ in_chans (int): Number of input image channels. Default: 3.
570
+ embed_dim (int): Number of linear projection output channels. Default: 96.
571
+ norm_layer (nn.Module, optional): Normalization layer. Default: None
572
+ """
573
+
574
+ def __init__(self, img_size=224, patch_size=4, in_chans=3, embed_dim=96, norm_layer=None):
575
+ super().__init__()
576
+ img_size = to_2tuple(img_size)
577
+ patch_size = to_2tuple(patch_size)
578
+ patches_resolution = [img_size[0] // patch_size[0], img_size[1] // patch_size[1]]
579
+ self.img_size = img_size
580
+ self.patch_size = patch_size
581
+ self.patches_resolution = patches_resolution
582
+ self.num_patches = patches_resolution[0] * patches_resolution[1]
583
+
584
+ self.in_chans = in_chans
585
+ self.embed_dim = embed_dim
586
+
587
+ self.proj = nn.Conv2d(in_chans, embed_dim, kernel_size=patch_size, stride=patch_size)
588
+ if norm_layer is not None:
589
+ self.norm = norm_layer(embed_dim)
590
+ else:
591
+ self.norm = None
592
+
593
+ def forward(self, x):
594
+ B, C, H, W = x.shape
595
+ # FIXME look at relaxing size constraints
596
+ assert H == self.img_size[0] and W == self.img_size[1], \
597
+ f"Input image size ({H}*{W}) doesn't match model ({self.img_size[0]}*{self.img_size[1]})."
598
+ x = self.proj(x).flatten(2).transpose(1, 2) # B Ph*Pw C
599
+ if self.norm is not None:
600
+ x = self.norm(x)
601
+ return x
602
+
603
+ def flops(self):
604
+ Ho, Wo = self.patches_resolution
605
+ flops = Ho * Wo * self.embed_dim * self.in_chans * (self.patch_size[0] * self.patch_size[1])
606
+ if self.norm is not None:
607
+ flops += Ho * Wo * self.embed_dim
608
+ return flops
609
+
610
+
611
+ class SwinTransformer(nn.Module):
612
+ r""" Swin Transformer
613
+ A PyTorch impl of : `Swin Transformer: Hierarchical Vision Transformer using Shifted Windows` -
614
+ https://arxiv.org/pdf/2103.14030
615
+
616
+ Args:
617
+ img_size (int | tuple(int)): Input image size. Default 224
618
+ patch_size (int | tuple(int)): Patch size. Default: 4
619
+ in_chans (int): Number of input image channels. Default: 3
620
+ num_classes (int): Number of classes for classification head. Default: 1000
621
+ embed_dim (int): Patch embedding dimension. Default: 96
622
+ depths (tuple(int)): Depth of each Swin Transformer layer.
623
+ num_heads (tuple(int)): Number of attention heads in different layers.
624
+ window_size (int): Window size. Default: 7
625
+ mlp_ratio (float): Ratio of mlp hidden dim to embedding dim. Default: 4
626
+ qkv_bias (bool): If True, add a learnable bias to query, key, value. Default: True
627
+ qk_scale (float): Override default qk scale of head_dim ** -0.5 if set. Default: None
628
+ drop_rate (float): Dropout rate. Default: 0
629
+ attn_drop_rate (float): Attention dropout rate. Default: 0
630
+ drop_path_rate (float): Stochastic depth rate. Default: 0.1
631
+ norm_layer (nn.Module): Normalization layer. Default: nn.LayerNorm.
632
+ ape (bool): If True, add absolute position embedding to the patch embedding. Default: False
633
+ patch_norm (bool): If True, add normalization after patch embedding. Default: True
634
+ use_checkpoint (bool): Whether to use checkpointing to save memory. Default: False
635
+ fused_window_process (bool, optional): If True, use one kernel to fused window shift & window partition for acceleration, similar for the reversed part. Default: False
636
+ """
637
+
638
+ def __init__(self, img_size=224, patch_size=4, in_chans=3, num_classes=1000,
639
+ embed_dim=96, depths=[2, 2, 6, 2], num_heads=[3, 6, 12, 24],
640
+ window_size=7, mlp_ratio=4., qkv_bias=True, qk_scale=None,
641
+ drop_rate=0., attn_drop_rate=0., drop_path_rate=0.1,
642
+ norm_layer=nn.LayerNorm, ape=False, patch_norm=True,
643
+ use_checkpoint=False, fused_window_process=False, **kwargs):
644
+ super().__init__()
645
+
646
+ self.num_classes = num_classes
647
+ self.num_layers = len(depths)
648
+ self.embed_dim = embed_dim
649
+ self.ape = ape
650
+ self.patch_norm = patch_norm
651
+ self.num_features = int(embed_dim * 2 ** (self.num_layers - 1))
652
+ self.mlp_ratio = mlp_ratio
653
+
654
+ # split image into non-overlapping patches
655
+ self.patch_embed = PatchEmbed(
656
+ img_size=img_size, patch_size=patch_size, in_chans=in_chans, embed_dim=embed_dim,
657
+ norm_layer=norm_layer if self.patch_norm else None)
658
+ num_patches = self.patch_embed.num_patches
659
+ patches_resolution = self.patch_embed.patches_resolution
660
+ self.patches_resolution = patches_resolution
661
+
662
+ # absolute position embedding
663
+ if self.ape:
664
+ self.absolute_pos_embed = nn.Parameter(torch.zeros(1, num_patches, embed_dim))
665
+ trunc_normal_(self.absolute_pos_embed, std=.02)
666
+
667
+ self.pos_drop = nn.Dropout(p=drop_rate)
668
+
669
+ # stochastic depth
670
+ dpr = [x.item() for x in torch.linspace(0, drop_path_rate, sum(depths))] # stochastic depth decay rule
671
+
672
+ # build layers
673
+ self.layers = nn.ModuleList()
674
+ for i_layer in range(self.num_layers):
675
+ layer = BasicLayer(dim=int(embed_dim * 2 ** i_layer),
676
+ input_resolution=(patches_resolution[0] // (2 ** i_layer),
677
+ patches_resolution[1] // (2 ** i_layer)),
678
+ depth=depths[i_layer],
679
+ num_heads=num_heads[i_layer],
680
+ window_size=window_size,
681
+ mlp_ratio=self.mlp_ratio,
682
+ qkv_bias=qkv_bias, qk_scale=qk_scale,
683
+ drop=drop_rate, attn_drop=attn_drop_rate,
684
+ drop_path=dpr[sum(depths[:i_layer]):sum(depths[:i_layer + 1])],
685
+ norm_layer=norm_layer,
686
+ downsample=PatchMerging if (i_layer < self.num_layers - 1) else None,
687
+ use_checkpoint=use_checkpoint,
688
+ fused_window_process=fused_window_process)
689
+ self.layers.append(layer)
690
+
691
+ self.norm = norm_layer(self.num_features)
692
+ self.avgpool = nn.AdaptiveAvgPool1d(1)
693
+ self.head = nn.Linear(self.num_features, num_classes) if num_classes > 0 else nn.Identity()
694
+
695
+ self.apply(self._init_weights)
696
+
697
+ def _init_weights(self, m):
698
+ if isinstance(m, nn.Linear):
699
+ trunc_normal_(m.weight, std=.02)
700
+ if isinstance(m, nn.Linear) and m.bias is not None:
701
+ nn.init.constant_(m.bias, 0)
702
+ elif isinstance(m, nn.LayerNorm):
703
+ nn.init.constant_(m.bias, 0)
704
+ nn.init.constant_(m.weight, 1.0)
705
+
706
+ @torch.jit.ignore
707
+ def no_weight_decay(self):
708
+ return {'absolute_pos_embed'}
709
+
710
+ @torch.jit.ignore
711
+ def no_weight_decay_keywords(self):
712
+ return {'relative_position_bias_table'}
713
+
714
+ def forward_features(self, x):
715
+ x = self.patch_embed(x)
716
+ if self.ape:
717
+ x = x + self.absolute_pos_embed
718
+ x = self.pos_drop(x)
719
+
720
+ for layer in self.layers:
721
+ x = layer(x)
722
+
723
+ x = self.norm(x) # B L C
724
+ x = self.avgpool(x.transpose(1, 2)) # B C 1
725
+ x = torch.flatten(x, 1)
726
+ return x
727
+
728
+ def forward(self, x):
729
+ x = self.forward_features(x)
730
+ x = self.head(x)
731
+ return x
732
+
733
+ def flops(self):
734
+ flops = 0
735
+ flops += self.patch_embed.flops()
736
+ for i, layer in enumerate(self.layers):
737
+ flops += layer.flops()
738
+ flops += self.num_features * self.patches_resolution[0] * self.patches_resolution[1] // (2 ** self.num_layers)
739
+ flops += self.num_features * self.num_classes
740
+ return flops
orbis2/networks/tokenizer/ae.py ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # pytorch_diffusion + derived encoder decoder
2
+ import math
3
+ from typing import Tuple, Union
4
+
5
+ import numpy as np
6
+ import torch
7
+ import torch.nn as nn
8
+
9
+ from einops import rearrange
10
+ from einops.layers.torch import Rearrange
11
+ from omegaconf import ListConfig
12
+
13
+
14
+ def get_timestep_embedding(timesteps, embedding_dim):
15
+ """
16
+ This matches the implementation in Denoising Diffusion Probabilistic Models:
17
+ From Fairseq.
18
+ Build sinusoidal embeddings.
19
+ This matches the implementation in tensor2tensor, but differs slightly
20
+ from the description in Section 3.5 of "Attention Is All You Need".
21
+ """
22
+ assert len(timesteps.shape) == 1
23
+
24
+ half_dim = embedding_dim // 2
25
+ emb = math.log(10000) / (half_dim - 1)
26
+ emb = torch.exp(torch.arange(half_dim, dtype=torch.float32) * -emb)
27
+ emb = emb.to(device=timesteps.device)
28
+ emb = timesteps.float()[:, None] * emb[None, :]
29
+ emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=1)
30
+ if embedding_dim % 2 == 1: # zero pad
31
+ emb = torch.nn.functional.pad(emb, (0,1,0,0))
32
+ return emb
33
+
34
+
35
+ def nonlinearity(x):
36
+ # swish
37
+ return x*torch.sigmoid(x)
38
+
39
+
40
+ class SpatialNorm(nn.Module):
41
+ def __init__(self, f_channels, zq_channels, norm_layer=nn.GroupNorm, freeze_norm_layer=False, add_conv=False, **norm_layer_params):
42
+ super().__init__()
43
+ self.norm_layer = norm_layer(num_channels=f_channels, **norm_layer_params)
44
+ if freeze_norm_layer:
45
+ for p in self.norm_layer.parameters: # for p in self.norm_layer.parameters():
46
+ p.requires_grad = False
47
+ self.add_conv = add_conv
48
+ if self.add_conv:
49
+ self.conv = nn.Conv2d(zq_channels, zq_channels, kernel_size=3, stride=1, padding=1)
50
+ self.conv_y = nn.Conv2d(zq_channels, f_channels, kernel_size=1, stride=1, padding=0)
51
+ self.conv_b = nn.Conv2d(zq_channels, f_channels, kernel_size=1, stride=1, padding=0)
52
+ def forward(self, f, zq):
53
+ #print(f'f shape: {f.shape}', f'zq shape: {zq.shape}')
54
+ f_size = f.shape[-2:]
55
+ zq = torch.nn.functional.interpolate(zq, size=f_size, mode="nearest")
56
+ if self.add_conv:
57
+ zq = self.conv(zq)
58
+ norm_f = self.norm_layer(f)
59
+ new_f = norm_f * self.conv_y(zq) + self.conv_b(zq)
60
+ return new_f
61
+
62
+ def SpatialNormalize(in_channels, zq_ch, add_conv):
63
+ return SpatialNorm(in_channels, zq_ch, norm_layer=nn.GroupNorm, freeze_norm_layer=False, add_conv=add_conv, num_groups=32, eps=1e-6, affine=True)
64
+
65
+
66
+ def Normalize(in_channels):
67
+ return torch.nn.GroupNorm(num_groups=32, num_channels=in_channels, eps=1e-6, affine=True)
68
+
69
+
70
+ class Upsample(nn.Module):
71
+ def __init__(self, in_channels, with_conv):
72
+ super().__init__()
73
+ self.with_conv = with_conv
74
+ if self.with_conv:
75
+ self.conv = torch.nn.Conv2d(in_channels,
76
+ in_channels,
77
+ kernel_size=3,
78
+ stride=1,
79
+ padding=1)
80
+
81
+ def forward(self, x):
82
+ x = torch.nn.functional.interpolate(x, scale_factor=2.0, mode="nearest")
83
+ if self.with_conv:
84
+ x = self.conv(x)
85
+ return x
86
+
87
+
88
+ class Downsample(nn.Module):
89
+ def __init__(self, in_channels, with_conv):
90
+ super().__init__()
91
+ self.with_conv = with_conv
92
+ if self.with_conv:
93
+ # no asymmetric padding in torch conv, must do it ourselves
94
+ self.conv = torch.nn.Conv2d(in_channels,
95
+ in_channels,
96
+ kernel_size=3,
97
+ stride=2,
98
+ padding=0)
99
+
100
+ def forward(self, x):
101
+ if self.with_conv:
102
+ pad = (0,1,0,1)
103
+ x = torch.nn.functional.pad(x, pad, mode="constant", value=0)
104
+ x = self.conv(x)
105
+ else:
106
+ x = torch.nn.functional.avg_pool2d(x, kernel_size=2, stride=2)
107
+ return x
108
+
109
+
110
+ class ResnetBlock(nn.Module):
111
+ def __init__(self, *, in_channels, out_channels=None, conv_shortcut=False,
112
+ dropout, temb_channels=512):
113
+ super().__init__()
114
+ self.in_channels = in_channels
115
+ out_channels = in_channels if out_channels is None else out_channels
116
+ self.out_channels = out_channels
117
+ self.use_conv_shortcut = conv_shortcut
118
+
119
+ self.norm1 = Normalize(in_channels)
120
+ self.conv1 = torch.nn.Conv2d(in_channels,
121
+ out_channels,
122
+ kernel_size=3,
123
+ stride=1,
124
+ padding=1)
125
+ if temb_channels > 0:
126
+ self.temb_proj = torch.nn.Linear(temb_channels,
127
+ out_channels)
128
+ self.norm2 = Normalize(out_channels)
129
+ self.dropout = torch.nn.Dropout(dropout)
130
+ self.conv2 = torch.nn.Conv2d(out_channels,
131
+ out_channels,
132
+ kernel_size=3,
133
+ stride=1,
134
+ padding=1)
135
+ if self.in_channels != self.out_channels:
136
+ if self.use_conv_shortcut:
137
+ self.conv_shortcut = torch.nn.Conv2d(in_channels,
138
+ out_channels,
139
+ kernel_size=3,
140
+ stride=1,
141
+ padding=1)
142
+ else:
143
+ self.nin_shortcut = torch.nn.Conv2d(in_channels,
144
+ out_channels,
145
+ kernel_size=1,
146
+ stride=1,
147
+ padding=0)
148
+
149
+ def forward(self, x, temb):
150
+ h = x
151
+ h = self.norm1(h)
152
+ h = nonlinearity(h)
153
+ h = self.conv1(h)
154
+
155
+ if temb is not None:
156
+ h = h + self.temb_proj(nonlinearity(temb))[:,:,None,None]
157
+
158
+ h = self.norm2(h)
159
+ h = nonlinearity(h)
160
+ h = self.dropout(h)
161
+ h = self.conv2(h)
162
+
163
+ if self.in_channels != self.out_channels:
164
+ if self.use_conv_shortcut:
165
+ x = self.conv_shortcut(x)
166
+ else:
167
+ x = self.nin_shortcut(x)
168
+
169
+ return x+h
170
+
171
+ class SpatialResnetBlock(nn.Module):
172
+ def __init__(self, *, in_channels, out_channels=None, conv_shortcut=False,
173
+ dropout, temb_channels=512, zq_ch=None, add_conv=False):
174
+ super().__init__()
175
+ self.in_channels = in_channels
176
+ out_channels = in_channels if out_channels is None else out_channels
177
+ self.out_channels = out_channels
178
+ self.use_conv_shortcut = conv_shortcut
179
+
180
+ self.norm1 = SpatialNormalize(in_channels, zq_ch, add_conv)
181
+ self.conv1 = torch.nn.Conv2d(in_channels,
182
+ out_channels,
183
+ kernel_size=3,
184
+ stride=1,
185
+ padding=1)
186
+ if temb_channels > 0:
187
+ self.temb_proj = torch.nn.Linear(temb_channels,
188
+ out_channels)
189
+ self.norm2 = SpatialNormalize(out_channels, zq_ch, add_conv)
190
+ self.dropout = torch.nn.Dropout(dropout)
191
+ self.conv2 = torch.nn.Conv2d(out_channels,
192
+ out_channels,
193
+ kernel_size=3,
194
+ stride=1,
195
+ padding=1)
196
+ if self.in_channels != self.out_channels:
197
+ if self.use_conv_shortcut:
198
+ self.conv_shortcut = torch.nn.Conv2d(in_channels,
199
+ out_channels,
200
+ kernel_size=3,
201
+ stride=1,
202
+ padding=1)
203
+ else:
204
+ self.nin_shortcut = torch.nn.Conv2d(in_channels,
205
+ out_channels,
206
+ kernel_size=1,
207
+ stride=1,
208
+ padding=0)
209
+
210
+ def forward(self, x, temb, zq):
211
+ h = x
212
+ h = self.norm1(h, zq)
213
+ h = nonlinearity(h)
214
+ h = self.conv1(h)
215
+
216
+ if temb is not None:
217
+ h = h + self.temb_proj(nonlinearity(temb))[:,:,None,None]
218
+
219
+ h = self.norm2(h, zq)
220
+ h = nonlinearity(h)
221
+ h = self.dropout(h)
222
+ h = self.conv2(h)
223
+
224
+ if self.in_channels != self.out_channels:
225
+ if self.use_conv_shortcut:
226
+ x = self.conv_shortcut(x)
227
+ else:
228
+ x = self.nin_shortcut(x)
229
+
230
+ return x+h
231
+
232
+
233
+ class AttnBlock(nn.Module):
234
+ def __init__(self, in_channels):
235
+ super().__init__()
236
+ self.in_channels = in_channels
237
+
238
+ self.norm = Normalize(in_channels)
239
+ self.q = torch.nn.Conv2d(in_channels,
240
+ in_channels,
241
+ kernel_size=1,
242
+ stride=1,
243
+ padding=0)
244
+ self.k = torch.nn.Conv2d(in_channels,
245
+ in_channels,
246
+ kernel_size=1,
247
+ stride=1,
248
+ padding=0)
249
+ self.v = torch.nn.Conv2d(in_channels,
250
+ in_channels,
251
+ kernel_size=1,
252
+ stride=1,
253
+ padding=0)
254
+ self.proj_out = torch.nn.Conv2d(in_channels,
255
+ in_channels,
256
+ kernel_size=1,
257
+ stride=1,
258
+ padding=0)
259
+
260
+
261
+ def forward(self, x):
262
+ h_ = x
263
+ h_ = self.norm(h_)
264
+ q = self.q(h_)
265
+ k = self.k(h_)
266
+ v = self.v(h_)
267
+
268
+ # compute attention
269
+ b,c,h,w = q.shape
270
+ q = q.reshape(b,c,h*w)
271
+ q = q.permute(0,2,1) # b,hw,c
272
+ k = k.reshape(b,c,h*w) # b,c,hw
273
+ w_ = torch.bmm(q,k) # b,hw,hw w[b,i,j]=sum_c q[b,i,c]k[b,c,j]
274
+ w_ = w_ * (int(c)**(-0.5))
275
+ w_ = torch.nn.functional.softmax(w_, dim=2)
276
+
277
+ # attend to values
278
+ v = v.reshape(b,c,h*w)
279
+ w_ = w_.permute(0,2,1) # b,hw,hw (first hw of k, second of q)
280
+ h_ = torch.bmm(v,w_) # b, c,hw (hw of q) h_[b,c,j] = sum_i v[b,c,i] w_[b,i,j]
281
+ h_ = h_.reshape(b,c,h,w)
282
+
283
+ h_ = self.proj_out(h_)
284
+
285
+ return x+h_
286
+
287
+ class SpatialAttnBlock(nn.Module):
288
+ def __init__(self, in_channels, zq_ch=None, add_conv=False):
289
+ super().__init__()
290
+ self.in_channels = in_channels
291
+
292
+ self.norm = SpatialNormalize(in_channels, zq_ch, add_conv)
293
+ self.q = torch.nn.Conv2d(in_channels,
294
+ in_channels,
295
+ kernel_size=1,
296
+ stride=1,
297
+ padding=0)
298
+ self.k = torch.nn.Conv2d(in_channels,
299
+ in_channels,
300
+ kernel_size=1,
301
+ stride=1,
302
+ padding=0)
303
+ self.v = torch.nn.Conv2d(in_channels,
304
+ in_channels,
305
+ kernel_size=1,
306
+ stride=1,
307
+ padding=0)
308
+ self.proj_out = torch.nn.Conv2d(in_channels,
309
+ in_channels,
310
+ kernel_size=1,
311
+ stride=1,
312
+ padding=0)
313
+
314
+
315
+ def forward(self, x, zq):
316
+ h_ = x
317
+ h_ = self.norm(h_, zq)
318
+ q = self.q(h_)
319
+ k = self.k(h_)
320
+ v = self.v(h_)
321
+
322
+ # compute attention
323
+ b,c,h,w = q.shape
324
+ q = q.reshape(b,c,h*w)
325
+ q = q.permute(0,2,1) # b,hw,c
326
+ k = k.reshape(b,c,h*w) # b,c,hw
327
+ w_ = torch.bmm(q,k) # b,hw,hw w[b,i,j]=sum_c q[b,i,c]k[b,c,j]
328
+ w_ = w_ * (int(c)**(-0.5))
329
+ w_ = torch.nn.functional.softmax(w_, dim=2)
330
+
331
+ # attend to values
332
+ v = v.reshape(b,c,h*w)
333
+ w_ = w_.permute(0,2,1) # b,hw,hw (first hw of k, second of q)
334
+ h_ = torch.bmm(v,w_) # b, c,hw (hw of q) h_[b,c,j] = sum_i v[b,c,i] w_[b,i,j]
335
+ h_ = h_.reshape(b,c,h,w)
336
+
337
+ h_ = self.proj_out(h_)
338
+
339
+ return x+h_
340
+
341
+
342
+ class Model(nn.Module):
343
+ def __init__(self, *, ch, out_ch, ch_mult=(1,2,4,8), num_res_blocks,
344
+ attn_resolutions, dropout=0.0, resamp_with_conv=True, in_channels,
345
+ resolution, use_timestep=True):
346
+ super().__init__()
347
+ self.ch = ch
348
+ self.temb_ch = self.ch*4
349
+ self.num_resolutions = len(ch_mult)
350
+ self.num_res_blocks = num_res_blocks
351
+ self.resolution = resolution
352
+ self.in_channels = in_channels
353
+
354
+ self.use_timestep = use_timestep
355
+ if self.use_timestep:
356
+ # timestep embedding
357
+ self.temb = nn.Module()
358
+ self.temb.dense = nn.ModuleList([
359
+ torch.nn.Linear(self.ch,
360
+ self.temb_ch),
361
+ torch.nn.Linear(self.temb_ch,
362
+ self.temb_ch),
363
+ ])
364
+
365
+ # downsampling
366
+ self.conv_in = torch.nn.Conv2d(in_channels,
367
+ self.ch,
368
+ kernel_size=3,
369
+ stride=1,
370
+ padding=1)
371
+
372
+ curr_res = resolution
373
+ in_ch_mult = (1,)+tuple(ch_mult)
374
+ self.down = nn.ModuleList()
375
+ for i_level in range(self.num_resolutions):
376
+ block = nn.ModuleList()
377
+ attn = nn.ModuleList()
378
+ block_in = ch*in_ch_mult[i_level]
379
+ block_out = ch*ch_mult[i_level]
380
+ for i_block in range(self.num_res_blocks):
381
+ block.append(ResnetBlock(in_channels=block_in,
382
+ out_channels=block_out,
383
+ temb_channels=self.temb_ch,
384
+ dropout=dropout))
385
+ block_in = block_out
386
+ if curr_res in attn_resolutions:
387
+ attn.append(AttnBlock(block_in))
388
+ down = nn.Module()
389
+ down.block = block
390
+ down.attn = attn
391
+ if i_level != self.num_resolutions-1:
392
+ down.downsample = Downsample(block_in, resamp_with_conv)
393
+ curr_res = curr_res // 2
394
+ self.down.append(down)
395
+
396
+ # middle
397
+ self.mid = nn.Module()
398
+ self.mid.block_1 = ResnetBlock(in_channels=block_in,
399
+ out_channels=block_in,
400
+ temb_channels=self.temb_ch,
401
+ dropout=dropout)
402
+ self.mid.attn_1 = AttnBlock(block_in)
403
+ self.mid.block_2 = ResnetBlock(in_channels=block_in,
404
+ out_channels=block_in,
405
+ temb_channels=self.temb_ch,
406
+ dropout=dropout)
407
+
408
+ # upsampling
409
+ self.up = nn.ModuleList()
410
+ for i_level in reversed(range(self.num_resolutions)):
411
+ block = nn.ModuleList()
412
+ attn = nn.ModuleList()
413
+ block_out = ch*ch_mult[i_level]
414
+ skip_in = ch*ch_mult[i_level]
415
+ for i_block in range(self.num_res_blocks+1):
416
+ if i_block == self.num_res_blocks:
417
+ skip_in = ch*in_ch_mult[i_level]
418
+ block.append(ResnetBlock(in_channels=block_in+skip_in,
419
+ out_channels=block_out,
420
+ temb_channels=self.temb_ch,
421
+ dropout=dropout))
422
+ block_in = block_out
423
+ if curr_res in attn_resolutions:
424
+ attn.append(AttnBlock(block_in))
425
+ up = nn.Module()
426
+ up.block = block
427
+ up.attn = attn
428
+ if i_level != 0:
429
+ up.upsample = Upsample(block_in, resamp_with_conv)
430
+ curr_res = curr_res * 2
431
+ self.up.insert(0, up) # prepend to get consistent order
432
+
433
+ # end
434
+ self.norm_out = Normalize(block_in)
435
+ self.conv_out = torch.nn.Conv2d(block_in,
436
+ out_ch,
437
+ kernel_size=3,
438
+ stride=1,
439
+ padding=1)
440
+
441
+
442
+ def forward(self, x, t=None):
443
+ #assert x.shape[2] == x.shape[3] == self.resolution
444
+
445
+ if self.use_timestep:
446
+ # timestep embedding
447
+ assert t is not None
448
+ temb = get_timestep_embedding(t, self.ch)
449
+ temb = self.temb.dense[0](temb)
450
+ temb = nonlinearity(temb)
451
+ temb = self.temb.dense[1](temb)
452
+ else:
453
+ temb = None
454
+
455
+ # downsampling
456
+ hs = [self.conv_in(x)]
457
+ for i_level in range(self.num_resolutions):
458
+ for i_block in range(self.num_res_blocks):
459
+ h = self.down[i_level].block[i_block](hs[-1], temb)
460
+ if len(self.down[i_level].attn) > 0:
461
+ h = self.down[i_level].attn[i_block](h)
462
+ hs.append(h)
463
+ if i_level != self.num_resolutions-1:
464
+ hs.append(self.down[i_level].downsample(hs[-1]))
465
+
466
+ # middle
467
+ h = hs[-1]
468
+ h = self.mid.block_1(h, temb)
469
+ h = self.mid.attn_1(h)
470
+ h = self.mid.block_2(h, temb)
471
+
472
+ # upsampling
473
+ for i_level in reversed(range(self.num_resolutions)):
474
+ for i_block in range(self.num_res_blocks+1):
475
+ h = self.up[i_level].block[i_block](
476
+ torch.cat([h, hs.pop()], dim=1), temb)
477
+ if len(self.up[i_level].attn) > 0:
478
+ h = self.up[i_level].attn[i_block](h)
479
+ if i_level != 0:
480
+ h = self.up[i_level].upsample(h)
481
+
482
+ # end
483
+ h = self.norm_out(h)
484
+ h = nonlinearity(h)
485
+ h = self.conv_out(h)
486
+ return h
487
+
488
+ def get_1d_sincos_pos_embed_from_grid(embed_dim, pos):
489
+ """
490
+ embed_dim: output dimension for each position
491
+ pos: a list of positions to be encoded: size (M,)
492
+ out: (M, D)
493
+ """
494
+ assert embed_dim % 2 == 0
495
+ omega = np.arange(embed_dim // 2, dtype=float)
496
+ omega /= embed_dim / 2.
497
+ omega = 1. / 10000**omega # (D/2,)
498
+
499
+ pos = pos.reshape(-1) # (M,)
500
+ out = np.einsum('m,d->md', pos, omega) # (M, D/2), outer product
501
+
502
+ emb_sin = np.sin(out) # (M, D/2)
503
+ emb_cos = np.cos(out) # (M, D/2)
504
+
505
+ emb = np.concatenate([emb_sin, emb_cos], axis=1) # (M, D)
506
+ return emb
507
+
508
+ def get_2d_sincos_pos_embed_from_grid(embed_dim, grid):
509
+ assert embed_dim % 2 == 0
510
+
511
+ # use half of dimensions to encode grid_h
512
+ emb_h = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[0]) # (H*W, D/2)
513
+ emb_w = get_1d_sincos_pos_embed_from_grid(embed_dim // 2, grid[1]) # (H*W, D/2)
514
+
515
+ emb = np.concatenate([emb_h, emb_w], axis=1) # (H*W, D)
516
+ return emb
517
+
518
+ def get_2d_sincos_pos_embed(embed_dim, grid_size):
519
+ """
520
+ grid_size: int or (int, int) of the grid height and width
521
+ return:
522
+ pos_embed: [grid_size*grid_size, embed_dim] or [1+grid_size*grid_size, embed_dim] (w/ or w/o cls_token)
523
+ """
524
+ grid_size = (grid_size, grid_size) if type(grid_size) != tuple else grid_size
525
+ grid_h = np.arange(grid_size[0], dtype=np.float32)
526
+ grid_w = np.arange(grid_size[1], dtype=np.float32)
527
+ grid = np.meshgrid(grid_w, grid_h) # here w goes first
528
+ grid = np.stack(grid, axis=0)
529
+
530
+ grid = grid.reshape([2, 1, grid_size[0], grid_size[1]])
531
+ pos_embed = get_2d_sincos_pos_embed_from_grid(embed_dim, grid)
532
+
533
+ return pos_embed
534
+
535
+ def init_weights(m):
536
+ if isinstance(m, nn.Linear):
537
+ # we use xavier_uniform following official JAX ViT:
538
+ torch.nn.init.xavier_uniform_(m.weight)
539
+ if m.bias is not None:
540
+ nn.init.constant_(m.bias, 0)
541
+ elif isinstance(m, nn.LayerNorm):
542
+ nn.init.constant_(m.bias, 0)
543
+ nn.init.constant_(m.weight, 1.0)
544
+ elif isinstance(m, nn.Conv2d) or isinstance(m, nn.ConvTranspose2d):
545
+ with torch.no_grad():
546
+ w = m.weight
547
+ torch.nn.init.xavier_uniform_(w.view([w.shape[0], -1]))
548
+
549
+ class PreNorm(nn.Module):
550
+ def __init__(self, dim: int, fn: nn.Module) -> None:
551
+ super().__init__()
552
+ self.norm = nn.LayerNorm(dim)
553
+ self.fn = fn
554
+
555
+ def forward(self, x: torch.FloatTensor, **kwargs) -> torch.FloatTensor:
556
+ return self.fn(self.norm(x), **kwargs)
557
+
558
+ class FeedForward(nn.Module):
559
+ def __init__(self, dim: int, hidden_dim: int) -> None:
560
+ super().__init__()
561
+ self.net = nn.Sequential(
562
+ nn.Linear(dim, hidden_dim),
563
+ nn.Tanh(),
564
+ nn.Linear(hidden_dim, dim)
565
+ )
566
+
567
+ def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
568
+ return self.net(x)
569
+
570
+ class Attention(nn.Module):
571
+ def __init__(self, dim: int, heads: int = 8, dim_head: int = 64) -> None:
572
+ super().__init__()
573
+ inner_dim = dim_head * heads
574
+ project_out = not (heads == 1 and dim_head == dim)
575
+
576
+ self.heads = heads
577
+ self.scale = dim_head ** -0.5
578
+
579
+ self.attend = nn.Softmax(dim = -1)
580
+ self.to_qkv = nn.Linear(dim, inner_dim * 3, bias = False)
581
+
582
+ self.to_out = nn.Linear(inner_dim, dim) if project_out else nn.Identity()
583
+
584
+ def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
585
+ qkv = self.to_qkv(x).chunk(3, dim = -1)
586
+ q, k, v = map(lambda t: rearrange(t, 'b n (h d) -> b h n d', h = self.heads), qkv)
587
+
588
+ attn = torch.matmul(q, k.transpose(-1, -2)) * self.scale
589
+ attn = self.attend(attn)
590
+
591
+ out = torch.matmul(attn, v)
592
+ out = rearrange(out, 'b h n d -> b n (h d)')
593
+
594
+ return self.to_out(out)
595
+
596
+ class Transformer(nn.Module):
597
+ def __init__(self, dim: int, depth: int, heads: int, dim_head: int, mlp_dim: int) -> None:
598
+ super().__init__()
599
+ self.layers = nn.ModuleList([])
600
+ for idx in range(depth):
601
+ layer = nn.ModuleList([PreNorm(dim, Attention(dim, heads=heads, dim_head=dim_head)),
602
+ PreNorm(dim, FeedForward(dim, mlp_dim))])
603
+ self.layers.append(layer)
604
+ self.norm = nn.LayerNorm(dim)
605
+
606
+ def forward(self, x: torch.FloatTensor) -> torch.FloatTensor:
607
+ for attn, ff in self.layers:
608
+ x = attn(x) + x
609
+ x = ff(x) + x
610
+
611
+ return self.norm(x)
612
+
613
+ class EncoderVIT(nn.Module):
614
+ def __init__(self, image_size: Union[Tuple[int, int], int], patch_size: Union[Tuple[int, int], int],
615
+ dim: int, depth: int, heads: int, mlp_dim: int, channels: int = 3, dim_head: int = 64) -> None:
616
+ super().__init__()
617
+
618
+ image_height, image_width = image_size if isinstance(image_size, tuple) \
619
+ else (image_size, image_size)
620
+ patch_height, patch_width = patch_size if isinstance(patch_size, tuple) \
621
+ else (patch_size, patch_size)
622
+ self.image_height = image_height
623
+ self.image_width = image_width
624
+ self.patch_height = patch_height
625
+ self.patch_width = patch_width
626
+ assert image_height % patch_height == 0 and image_width % patch_width == 0, 'Image dimensions must be divisible by the patch size.'
627
+ en_pos_embedding = get_2d_sincos_pos_embed(dim, (image_height // patch_height, image_width // patch_width))
628
+
629
+ self.num_patches = (image_height // patch_height) * (image_width // patch_width)
630
+ self.patch_dim = channels * patch_height * patch_width
631
+
632
+ self.to_patch_embedding = nn.Sequential(
633
+ nn.Conv2d(channels, dim, kernel_size=patch_size, stride=patch_size),
634
+ Rearrange('b c h w -> b (h w) c'),
635
+ )
636
+ self.en_pos_embedding = nn.Parameter(torch.from_numpy(en_pos_embedding).float().unsqueeze(0), requires_grad=False)
637
+ self.transformer = Transformer(dim, depth, heads, dim_head, mlp_dim)
638
+
639
+ self.apply(init_weights)
640
+
641
+ def forward(self, img: torch.FloatTensor) -> torch.FloatTensor:
642
+ x = self.to_patch_embedding(img)
643
+ x = x + self.en_pos_embedding
644
+ x = self.transformer(x) # (B, N, D)
645
+ #import pdb; pdb.set_trace()
646
+ x = x.reshape(x.shape[0], x.shape[1]//(self.image_height // self.patch_height), x.shape[1]//(self.image_width // self.patch_width), -1)
647
+ x = x.permute(0, 3, 1, 2).contiguous()
648
+ #import pdb; pdb.set_trace()
649
+ return x
650
+
651
+ class Encoder(nn.Module):
652
+ def __init__(self, *, ch, out_ch, ch_mult=(1,2,4,8), num_res_blocks,
653
+ attn_resolutions, dropout=0.0, resamp_with_conv=True, in_channels,
654
+ resolution, z_channels, double_z=True, **ignore_kwargs):
655
+ super().__init__()
656
+ self.ch = ch
657
+ self.temb_ch = 0
658
+ self.num_resolutions = len(ch_mult)
659
+ self.num_res_blocks = num_res_blocks
660
+ self.resolution = resolution
661
+ self.in_channels = in_channels
662
+
663
+ # downsampling
664
+ self.conv_in = torch.nn.Conv2d(in_channels,
665
+ self.ch,
666
+ kernel_size=3,
667
+ stride=1,
668
+ padding=1)
669
+
670
+ curr_res = resolution
671
+ in_ch_mult = (1,)+tuple(ch_mult)
672
+ self.down = nn.ModuleList()
673
+ for i_level in range(self.num_resolutions):
674
+ block = nn.ModuleList()
675
+ attn = nn.ModuleList()
676
+ block_in = ch*in_ch_mult[i_level]
677
+ block_out = ch*ch_mult[i_level]
678
+ for i_block in range(self.num_res_blocks):
679
+ block.append(ResnetBlock(in_channels=block_in,
680
+ out_channels=block_out,
681
+ temb_channels=self.temb_ch,
682
+ dropout=dropout))
683
+ block_in = block_out
684
+ if curr_res in attn_resolutions:
685
+ attn.append(AttnBlock(block_in))
686
+ down = nn.Module()
687
+ down.block = block
688
+ down.attn = attn
689
+ if i_level != self.num_resolutions-1:
690
+ down.downsample = Downsample(block_in, resamp_with_conv)
691
+ if isinstance(curr_res, int):
692
+ curr_res = curr_res // 2
693
+ elif isinstance(curr_res, (list, ListConfig)):
694
+ curr_res = [r // 2 for r in curr_res]
695
+ else:
696
+ raise TypeError(f"Unsupported type for curr_res: {type(curr_res)}")
697
+
698
+ self.down.append(down)
699
+
700
+ # middle
701
+ self.mid = nn.Module()
702
+ self.mid.block_1 = ResnetBlock(in_channels=block_in,
703
+ out_channels=block_in,
704
+ temb_channels=self.temb_ch,
705
+ dropout=dropout)
706
+ self.mid.attn_1 = AttnBlock(block_in)
707
+ self.mid.block_2 = ResnetBlock(in_channels=block_in,
708
+ out_channels=block_in,
709
+ temb_channels=self.temb_ch,
710
+ dropout=dropout)
711
+
712
+ # end
713
+ self.norm_out = Normalize(block_in)
714
+ self.conv_out = torch.nn.Conv2d(block_in,
715
+ 2*z_channels if double_z else z_channels,
716
+ kernel_size=3,
717
+ stride=1,
718
+ padding=1)
719
+
720
+
721
+ def forward(self, x):
722
+ # timestep embedding
723
+ temb = None
724
+ # downsampling
725
+ hs = [self.conv_in(x)]
726
+ for i_level in range(self.num_resolutions):
727
+ for i_block in range(self.num_res_blocks):
728
+ h = self.down[i_level].block[i_block](hs[-1], temb)
729
+ if len(self.down[i_level].attn) > 0:
730
+ h = self.down[i_level].attn[i_block](h)
731
+ hs.append(h)
732
+ if i_level != self.num_resolutions-1:
733
+ hs.append(self.down[i_level].downsample(hs[-1]))
734
+
735
+ # middle
736
+ h = hs[-1]
737
+ h = self.mid.block_1(h, temb)
738
+ h = self.mid.attn_1(h)
739
+ h = self.mid.block_2(h, temb)
740
+
741
+ # end
742
+ h = self.norm_out(h)
743
+ h = nonlinearity(h)
744
+ h = self.conv_out(h)
745
+ return h
746
+
747
+
748
+ class Decoder(nn.Module):
749
+ def __init__(self, *, ch, out_ch, ch_mult=(1,2,4,8), num_res_blocks,
750
+ attn_resolutions, dropout=0.0, resamp_with_conv=True, in_channels,
751
+ resolution, z_channels, give_pre_end=False, **ignorekwargs):
752
+ super().__init__()
753
+ self.ch = ch
754
+ self.temb_ch = 0
755
+ self.num_resolutions = len(ch_mult)
756
+ self.num_res_blocks = num_res_blocks
757
+ self.resolution = resolution
758
+ self.in_channels = in_channels
759
+ self.give_pre_end = give_pre_end
760
+
761
+ # compute in_ch_mult, block_in and curr_res at lowest res
762
+ in_ch_mult = (1,)+tuple(ch_mult)
763
+ block_in = ch*ch_mult[self.num_resolutions-1]
764
+
765
+ curr_res_h, curr_res_w = (resolution[0] // 2**(self.num_resolutions-1), resolution[1] // 2**(self.num_resolutions-1)) if isinstance(resolution, (list, tuple, ListConfig)) else (resolution// 2**(self.num_resolutions-1) ,resolution// 2**(self.num_resolutions-1))
766
+ self.z_shape = (1,z_channels,curr_res_h,curr_res_w)
767
+
768
+ # z to block_in
769
+ self.conv_in = torch.nn.Conv2d(z_channels,
770
+ block_in,
771
+ kernel_size=3,
772
+ stride=1,
773
+ padding=1)
774
+
775
+ # middle
776
+ self.mid = nn.Module()
777
+ self.mid.block_1 = ResnetBlock(in_channels=block_in,
778
+ out_channels=block_in,
779
+ temb_channels=self.temb_ch,
780
+ dropout=dropout)
781
+ self.mid.attn_1 = AttnBlock(block_in)
782
+ self.mid.block_2 = ResnetBlock(in_channels=block_in,
783
+ out_channels=block_in,
784
+ temb_channels=self.temb_ch,
785
+ dropout=dropout)
786
+
787
+ #self.decoder_dino_conv = nn.Conv2d(block_in, 768, kernel_size=1, stride=1, padding=0) # remove hard-coded emb_dim=768
788
+
789
+ # upsampling
790
+ self.up = nn.ModuleList()
791
+ for i_level in reversed(range(self.num_resolutions)):
792
+ block = nn.ModuleList()
793
+ attn = nn.ModuleList()
794
+ block_out = ch*ch_mult[i_level]
795
+ for i_block in range(self.num_res_blocks+1):
796
+ block.append(ResnetBlock(in_channels=block_in,
797
+ out_channels=block_out,
798
+ temb_channels=self.temb_ch,
799
+ dropout=dropout))
800
+ block_in = block_out
801
+ if curr_res_h in attn_resolutions:
802
+ attn.append(AttnBlock(block_in))
803
+ up = nn.Module()
804
+ up.block = block
805
+ up.attn = attn
806
+ if i_level != 0:
807
+ up.upsample = Upsample(block_in, resamp_with_conv)
808
+ curr_res_h = curr_res_h * 2
809
+ self.up.insert(0, up) # prepend to get consistent order
810
+
811
+ # end
812
+ self.norm_out = Normalize(block_in)
813
+ self.conv_out = torch.nn.Conv2d(block_in,
814
+ out_ch,
815
+ kernel_size=3,
816
+ stride=1,
817
+ padding=1)
818
+
819
+ def forward(self, z):
820
+ #assert z.shape[1:] == self.z_shape[1:]
821
+ self.last_z_shape = z.shape
822
+
823
+ # timestep embedding
824
+ temb = None
825
+
826
+ # z to block_in
827
+ h = self.conv_in(z)
828
+
829
+ # middle
830
+ h = self.mid.block_1(h, temb)
831
+ h = self.mid.attn_1(h)
832
+ h = self.mid.block_2(h, temb)
833
+
834
+ # upsampling
835
+ for i_level in reversed(range(self.num_resolutions)):
836
+ for i_block in range(self.num_res_blocks+1):
837
+ h = self.up[i_level].block[i_block](h, temb)
838
+ if len(self.up[i_level].attn) > 0:
839
+ h = self.up[i_level].attn[i_block](h)
840
+ if i_level != 0:
841
+ h = self.up[i_level].upsample(h)
842
+
843
+ # end
844
+ if self.give_pre_end:
845
+ return h
846
+
847
+ h = self.norm_out(h)
848
+ h = nonlinearity(h)
849
+ h = self.conv_out(h)
850
+
851
+ return h
852
+
orbis2/networks/tokenizer/pretrained_models.py ADDED
@@ -0,0 +1,158 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import timm
3
+ from torch import nn
4
+ from omegaconf import ListConfig
5
+ from einops import rearrange
6
+
7
+
8
+ from typing import Tuple, Union
9
+
10
+ class Encoder(nn.Module):
11
+ def __init__(
12
+ self,
13
+ resolution: Union[Tuple[int, int], int],
14
+ channels: int = 3,
15
+ pretrained_encoder = 'MAE',
16
+ patch_size: int = 16,
17
+ z_channels: int = 768,
18
+ e_dim: int = 8,
19
+ normalize_embedding: bool = True,
20
+ use_pretrained_weights = True,
21
+ # **ignore_kwargs
22
+ ) -> None:
23
+ # Initialize parent class with the first patch size
24
+ super().__init__()
25
+ self.image_size = resolution
26
+ self.patch_size = patch_size
27
+ self.channels = channels
28
+ self.normalize_embedding = normalize_embedding
29
+ self.z_channels = z_channels
30
+ self.e_dim = e_dim
31
+ self.pretrained_encoder = pretrained_encoder
32
+
33
+ self.init_transformer(pretrained_encoder, use_pretrained_weights)
34
+
35
+ def init_transformer(self, pretrained_encoder, use_pretrained_weights):
36
+ if pretrained_encoder == 'VIT_DINO':
37
+ pretrained_encoder_model = 'timm/vit_base_patch16_224.dino'
38
+ elif pretrained_encoder == 'VIT_DINOv2':
39
+ pretrained_encoder_model = 'timm/vit_base_patch14_dinov2.lvd142m'
40
+ elif pretrained_encoder == 'MAE':
41
+ pretrained_encoder_model = 'timm/vit_base_patch16_224.mae'
42
+ elif pretrained_encoder == 'MAE_VIT_L':
43
+ pretrained_encoder_model = 'timm/vit_large_patch16_224.mae'
44
+ elif pretrained_encoder == 'VIT':
45
+ pretrained_encoder_model = 'timm/vit_large_patch32_224.orig_in21k'
46
+ elif pretrained_encoder == 'CLIP32':
47
+ pretrained_encoder_model = 'timm/vit_base_patch32_clip_224.openai'
48
+ elif pretrained_encoder == 'CLIP':
49
+ pretrained_encoder_model = 'timm/vit_base_patch16_clip_224.openai'
50
+ elif pretrained_encoder == 'base':
51
+ pretrained_encoder_model = 'timm/vit_base_patch16_224'
52
+ elif pretrained_encoder == 'large':
53
+ pretrained_encoder_model = 'timm/vit_large_patch16_224'
54
+
55
+ if pretrained_encoder == "VIT_DINOv3":
56
+ self.encoder = torch.hub.load('../dinov3', 'dinov3_vitb16', source='local', weights='./pretrained_models/dinov3_vitb16_pretrain_lvd1689m-73cec8be.pth').train()
57
+ else:
58
+ self.encoder = timm.create_model(pretrained_encoder_model, img_size=self.image_size, patch_size=self.patch_size, pretrained=False, dynamic_img_size=True).train()
59
+ if use_pretrained_weights:
60
+ pretrained_model = timm.create_model(pretrained_encoder_model, img_size=self.image_size, patch_size=self.patch_size, pretrained=True)
61
+ """Initialize weights of target_model with weights from source_model."""
62
+ with torch.no_grad():
63
+ for target_param, source_param in zip(self.encoder.parameters(), pretrained_model.parameters()):
64
+ target_param.data.copy_(source_param.data)
65
+
66
+ # Clean up
67
+ del pretrained_model
68
+
69
+ def forward(self, img: torch.FloatTensor) -> torch.FloatTensor:
70
+ if self.pretrained_encoder == "VIT_DINOv3":
71
+ h = self.encoder.forward_features(img)['x_norm_patchtokens']
72
+ else:
73
+ h = self.encoder.forward_features(img)[:,1:]
74
+ h = h.permute(0, 2, 1).contiguous()
75
+ h = h.reshape(h.shape[0], -1, img.size(2)//self.patch_size, img.size(3)//self.patch_size)
76
+ return h
77
+
78
+
79
+ class MaskedEncoder(Encoder):
80
+ def __init__(
81
+ self,
82
+ *,
83
+ mask_ratio_min: float = 0.0,
84
+ mask_ratio_max: float = 0.5,
85
+ mask_during_eval: bool = False,
86
+ mask_token_init_std: float = 0.02,
87
+ **kwargs,
88
+ ) -> None:
89
+ super().__init__(**kwargs)
90
+ if not 0.0 <= mask_ratio_min <= mask_ratio_max <= 1.0:
91
+ raise ValueError(
92
+ f'Expected 0 <= mask_ratio_min <= mask_ratio_max <= 1, got '
93
+ f'{mask_ratio_min}, {mask_ratio_max}.'
94
+ )
95
+
96
+ self.mask_ratio_min = float(mask_ratio_min)
97
+ self.mask_ratio_max = float(mask_ratio_max)
98
+ self.mask_during_eval = bool(mask_during_eval)
99
+ embed_dim = getattr(self.encoder, 'embed_dim', self.z_channels)
100
+ self.mask_token = nn.Parameter(torch.zeros(1, 1, embed_dim))
101
+ nn.init.normal_(self.mask_token, std=mask_token_init_std)
102
+
103
+ def _sample_patch_mask(self, batch_size: int, num_patches: int, device: torch.device):
104
+ if self.mask_ratio_max <= 0.0:
105
+ return None
106
+ if not self.training and not self.mask_during_eval:
107
+ return None
108
+
109
+ ratios = torch.empty(batch_size, device=device).uniform_(self.mask_ratio_min, self.mask_ratio_max)
110
+ return torch.rand(batch_size, num_patches, device=device) < ratios.unsqueeze(1)
111
+
112
+ def _apply_mask_after_patch_embed(self, x: torch.Tensor) -> torch.Tensor:
113
+ if x.dim() != 3:
114
+ raise ValueError(f'Expected patch embeddings with shape (B, N, C), got {tuple(x.shape)}.')
115
+ mask = self._sample_patch_mask(x.shape[0], x.shape[1], x.device)
116
+ if mask is None:
117
+ return x
118
+ mask_token = self.mask_token.to(dtype=x.dtype).expand(x.shape[0], x.shape[1], -1)
119
+ return torch.where(mask.unsqueeze(-1), mask_token, x)
120
+
121
+ def _forward_timm_features(self, img: torch.FloatTensor) -> torch.Tensor:
122
+ x = self.encoder.patch_embed(img)
123
+ if x.dim() == 4:
124
+ embed_dim = getattr(self.encoder, 'embed_dim', x.shape[-1])
125
+ if x.shape[-1] == embed_dim:
126
+ b, h, w, c = x.shape
127
+ x = x.reshape(b, h * w, c)
128
+ x = self._apply_mask_after_patch_embed(x)
129
+ x = x.reshape(b, h, w, c)
130
+ else:
131
+ x = x.flatten(2).transpose(1, 2)
132
+ x = self._apply_mask_after_patch_embed(x)
133
+ else:
134
+ x = self._apply_mask_after_patch_embed(x)
135
+ x = self.encoder._pos_embed(x)
136
+ x = self.encoder.patch_drop(x)
137
+ x = self.encoder.norm_pre(x)
138
+
139
+ blocks = self.encoder.blocks
140
+ if isinstance(blocks, nn.ModuleList):
141
+ for blk in blocks:
142
+ x = blk(x)
143
+ else:
144
+ x = blocks(x)
145
+ x = self.encoder.norm(x)
146
+ return x
147
+
148
+ def forward(self, img: torch.FloatTensor) -> torch.FloatTensor:
149
+ if self.pretrained_encoder == 'VIT_DINOv3':
150
+ raise NotImplementedError('MaskedEncoder currently supports timm-based encoders only.')
151
+
152
+ h = self._forward_timm_features(img)
153
+ num_prefix_tokens = getattr(self.encoder, 'num_prefix_tokens', 1)
154
+ if num_prefix_tokens > 0:
155
+ h = h[:, num_prefix_tokens:]
156
+ h = h.permute(0, 2, 1).contiguous()
157
+ h = h.reshape(h.shape[0], -1, img.size(2) // self.patch_size, img.size(3) // self.patch_size)
158
+ return h
orbis2/util.py ADDED
@@ -0,0 +1,214 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import argparse
2
+ import hashlib
3
+ import importlib
4
+ import logging
5
+ import os
6
+ import requests
7
+
8
+ from tqdm import tqdm
9
+ from pytorch_lightning.trainer import Trainer
10
+
11
+
12
+ def maybe_raise_nofile_limit(target_soft=32768, logger=None):
13
+ """
14
+ Raise RLIMIT_NOFILE soft limit for current process when possible.
15
+
16
+ Returns True if the limit was raised, False otherwise.
17
+ """
18
+ if logger is None:
19
+ logger = logging.getLogger(__name__)
20
+ try:
21
+ import resource
22
+ except ImportError:
23
+ logger.warning("resource module unavailable; cannot set RLIMIT_NOFILE")
24
+ return False
25
+ try:
26
+ soft, hard = resource.getrlimit(resource.RLIMIT_NOFILE)
27
+ desired = target_soft
28
+ if hard not in (resource.RLIM_INFINITY, -1):
29
+ desired = min(desired, hard)
30
+ if soft >= desired:
31
+ return False
32
+ resource.setrlimit(resource.RLIMIT_NOFILE, (desired, hard))
33
+ logger.info("Raised RLIMIT_NOFILE from %s to %s", soft, desired)
34
+ return True
35
+ except (ValueError, PermissionError, OSError) as exc:
36
+ logger.warning("Unable to raise RLIMIT_NOFILE: %s", exc)
37
+ return False
38
+
39
+ URL_MAP = {
40
+ "vgg_lpips": "https://heibox.uni-heidelberg.de/f/607503859c864bc1b30b/?dl=1"
41
+ }
42
+
43
+ CKPT_MAP = {
44
+ "vgg_lpips": "vgg.pth"
45
+ }
46
+
47
+ MD5_MAP = {
48
+ "vgg_lpips": "d507d7349b931f0638a25a48a722f98a"
49
+ }
50
+
51
+
52
+ def download(url, local_path, chunk_size=1024):
53
+ os.makedirs(os.path.split(local_path)[0], exist_ok=True)
54
+ with requests.get(url, stream=True) as r:
55
+ total_size = int(r.headers.get("content-length", 0))
56
+ with tqdm(total=total_size, unit="B", unit_scale=True) as pbar:
57
+ with open(local_path, "wb") as f:
58
+ for data in r.iter_content(chunk_size=chunk_size):
59
+ if data:
60
+ f.write(data)
61
+ pbar.update(chunk_size)
62
+
63
+
64
+ def md5_hash(path):
65
+ with open(path, "rb") as f:
66
+ content = f.read()
67
+ return hashlib.md5(content).hexdigest()
68
+
69
+
70
+ def get_ckpt_path(name, root, check=False):
71
+ assert name in URL_MAP
72
+ path = os.path.join(root, CKPT_MAP[name])
73
+ if not os.path.exists(path) or (check and not md5_hash(path) == MD5_MAP[name]):
74
+ print("Downloading {} model from {} to {}".format(name, URL_MAP[name], path))
75
+ download(URL_MAP[name], path)
76
+ md5 = md5_hash(path)
77
+ assert md5 == MD5_MAP[name], md5
78
+ return path
79
+
80
+
81
+ class KeyNotFoundError(Exception):
82
+ def __init__(self, cause, keys=None, visited=None):
83
+ self.cause = cause
84
+ self.keys = keys
85
+ self.visited = visited
86
+ messages = list()
87
+ if keys is not None:
88
+ messages.append("Key not found: {}".format(keys))
89
+ if visited is not None:
90
+ messages.append("Visited: {}".format(visited))
91
+ messages.append("Cause:\n{}".format(cause))
92
+ message = "\n".join(messages)
93
+ super().__init__(message)
94
+
95
+
96
+ def retrieve(
97
+ list_or_dict, key, splitval="/", default=None, expand=True, pass_success=False
98
+ ):
99
+ """Given a nested list or dict return the desired value at key expanding
100
+ callable nodes if necessary and :attr:`expand` is ``True``. The expansion
101
+ is done in-place.
102
+
103
+ Parameters
104
+ ----------
105
+ list_or_dict : list or dict
106
+ Possibly nested list or dictionary.
107
+ key : str
108
+ key/to/value, path like string describing all keys necessary to
109
+ consider to get to the desired value. List indices can also be
110
+ passed here.
111
+ splitval : str
112
+ String that defines the delimiter between keys of the
113
+ different depth levels in `key`.
114
+ default : obj
115
+ Value returned if :attr:`key` is not found.
116
+ expand : bool
117
+ Whether to expand callable nodes on the path or not.
118
+
119
+ Returns
120
+ -------
121
+ The desired value or if :attr:`default` is not ``None`` and the
122
+ :attr:`key` is not found returns ``default``.
123
+
124
+ Raises
125
+ ------
126
+ Exception if ``key`` not in ``list_or_dict`` and :attr:`default` is
127
+ ``None``.
128
+ """
129
+
130
+ keys = key.split(splitval)
131
+
132
+ success = True
133
+ try:
134
+ visited = []
135
+ parent = None
136
+ last_key = None
137
+ for key in keys:
138
+ if callable(list_or_dict):
139
+ if not expand:
140
+ raise KeyNotFoundError(
141
+ ValueError(
142
+ "Trying to get past callable node with expand=False."
143
+ ),
144
+ keys=keys,
145
+ visited=visited,
146
+ )
147
+ list_or_dict = list_or_dict()
148
+ parent[last_key] = list_or_dict
149
+
150
+ last_key = key
151
+ parent = list_or_dict
152
+
153
+ try:
154
+ if isinstance(list_or_dict, dict):
155
+ list_or_dict = list_or_dict[key]
156
+ else:
157
+ list_or_dict = list_or_dict[int(key)]
158
+ except (KeyError, IndexError, ValueError) as e:
159
+ raise KeyNotFoundError(e, keys=keys, visited=visited)
160
+
161
+ visited += [key]
162
+ # final expansion of retrieved value
163
+ if expand and callable(list_or_dict):
164
+ list_or_dict = list_or_dict()
165
+ parent[last_key] = list_or_dict
166
+ except KeyNotFoundError as e:
167
+ if default is None:
168
+ raise e
169
+ else:
170
+ list_or_dict = default
171
+ success = False
172
+
173
+ if not pass_success:
174
+ return list_or_dict
175
+ else:
176
+ return list_or_dict, success
177
+
178
+
179
+ def get_obj_from_str(string, reload=False):
180
+ module, cls = string.rsplit(".", 1)
181
+ if reload:
182
+ module_imp = importlib.import_module(module)
183
+ importlib.reload(module_imp)
184
+ return getattr(importlib.import_module(module, package=None), cls)
185
+
186
+
187
+
188
+ def get_jobid():
189
+ try:
190
+ jobid = os.environ["PBS_JOBID"].replace(".lmbtorque.informatik.uni-freiburg.de", "")
191
+ except KeyError:
192
+ try:
193
+ jobid = "DLC" + os.environ["SLURM_JOB_ID"]
194
+ except KeyError:
195
+ try:
196
+ jobid = os.uname()[1]
197
+ except KeyError:
198
+ jobid = "local"
199
+ return jobid
200
+
201
+
202
+ def nondefault_trainer_args(opt):
203
+ parser = argparse.ArgumentParser()
204
+ parser = Trainer.add_argparse_args(parser)
205
+ args = parser.parse_args([])
206
+ return sorted(k for k in vars(args) if getattr(opt, k) != getattr(args, k))
207
+
208
+
209
+ def instantiate_from_config(config):
210
+ if not "target" in config:
211
+ raise KeyError("Expected key `target` to instantiate.")
212
+ return get_obj_from_str(config["target"])(**config.get("params", dict()))
213
+
214
+
requirements.txt CHANGED
@@ -12,6 +12,11 @@ imageio
12
  timm
13
  pillow
14
 
 
 
 
 
 
15
  # Space infrastructure
16
  gradio
17
  spaces
 
12
  timm
13
  pillow
14
 
15
+ # Orbis 2 (app2.py) extra deps — video-in rollout reads mp4s directly
16
+ decord
17
+ h5py
18
+ requests
19
+
20
  # Space infrastructure
21
  gradio
22
  spaces