kerzgrr commited on
Commit
b4b7ca9
·
verified ·
1 Parent(s): f040ba6

Add Diffusion Miner teacher checkpoint, Tile VAE, live infer, and gameplay GIF

Browse files
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ demo.gif filter=lfs diff=lfs merge=lfs -text
README.md ADDED
@@ -0,0 +1,77 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ library_name: pytorch
4
+ pipeline_tag: other
5
+ tags:
6
+ - world-model
7
+ - diffusion
8
+ - mining
9
+ - video
10
+ - causal
11
+ - game
12
+ ---
13
+
14
+ # Diffusion Miner
15
+
16
+ An action-conditioned world model that learned a falling-sand mining sandbox. You hold **A / D** to walk, **W** or Space to jump, and the arrows to dig; it invents the next frame.
17
+
18
+ ![demo](demo.gif)
19
+
20
+ 256×256 at 12 FPS. The attention prefix is 12 latent frames; a ConvGRU world memory scans 48. About 220M parameters. The codec is a frozen Tile VAE trained on this game, not SDXL. Needs a CUDA GPU with BF16.
21
+
22
+ This release is the teacher-stage instantaneous flow checkpoint. One step is the right setting — there is no distilled student.
23
+
24
+ ## Play it
25
+
26
+ ```bash
27
+ pip install torch numpy pillow safetensors huggingface_hub
28
+
29
+ hf download kerzgrr/diffusionminer --local-dir checkpoints/diffusionminer
30
+ python checkpoints/diffusionminer/live_infer.py \
31
+ --local-dir checkpoints/diffusionminer \
32
+ --steps 1
33
+ ```
34
+
35
+ If the weights are already in the current directory:
36
+
37
+ ```bash
38
+ python live_infer.py --steps 1 --window-scale 3 --seed 2026
39
+ ```
40
+
41
+ Click the window once so it has focus. Esc quits. Clicking the canvas drops a miner into the latent history if you want to poke it.
42
+
43
+ Write a short gameplay GIF with the scripted training miner:
44
+
45
+ ```bash
46
+ python live_infer.py \
47
+ --local-dir checkpoints/diffusionminer \
48
+ --steps 1 \
49
+ --seed-play \
50
+ --autoplay \
51
+ --frames 144 \
52
+ --output diffusionminer.gif
53
+ ```
54
+
55
+ ## Controls
56
+
57
+ | key | action |
58
+ |---|---|
59
+ | `A` | move left |
60
+ | `D` | move right |
61
+ | `W` / Space | jump |
62
+ | ← ↑ → ↓ | dig that way |
63
+
64
+ Action vector is `[left, right, jump, dig_left, dig_right, dig_up, dig_down]` as floats in `{0,1}`. Opposite move keys cancel. Only one dig direction is active at a time.
65
+
66
+ ## What's in the files
67
+
68
+ | file | what |
69
+ |---|---|
70
+ | `ema.safetensors` | playable teacher weights |
71
+ | `codec.pt` | frozen Tile VAE |
72
+ | `config.json` | video / model / codec settings used at train time |
73
+ | `livediffusion/` | inference package used by `live_infer.py` |
74
+ | `live_infer.py` | live play launcher |
75
+ | `demo.gif` | scripted gameplay clip |
76
+
77
+ If something breaks, it is probably a GPU that cannot do BF16, or running `live_infer.py` without the rest of this snapshot on `sys.path`.
codec.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:e0f39af6805433a30657cd60ca47e9b74b49a8e1a1aa2a03162ec67daf64c3a6
3
+ size 40224875
config.json ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "codec": {
3
+ "checkpoint_path": "codec.pt",
4
+ "enable_slicing": false,
5
+ "enable_tiling": false,
6
+ "frame_batch_size": 64,
7
+ "kind": "tile_vae",
8
+ "latent_channels": 4,
9
+ "model_id": "",
10
+ "revision": "main",
11
+ "sample_posterior": false,
12
+ "spatial_compression": 8,
13
+ "subfolder": "",
14
+ "temporal_compression": 1
15
+ },
16
+ "data": {
17
+ "batch_size": 16,
18
+ "data_path": "",
19
+ "dataset_kind": "mining",
20
+ "frame_stride": 1,
21
+ "hf_repo_id": "",
22
+ "hf_revision": "main",
23
+ "latent_cache_path": "",
24
+ "latent_prefetch_batches": 6,
25
+ "live_monitor": 1,
26
+ "live_mouse_scale": 80.0,
27
+ "network_clip_prefetch": 2,
28
+ "num_workers": 12,
29
+ "persistent_workers": true,
30
+ "pin_memory": true,
31
+ "prefetch_batches": 2,
32
+ "sample_shuffle": 64,
33
+ "shard_shuffle": 256,
34
+ "train_samples": 500000,
35
+ "validation_samples": 1024,
36
+ "windows_per_video": 4
37
+ },
38
+ "inference": {
39
+ "frames": 240,
40
+ "steps": 1
41
+ },
42
+ "model": {
43
+ "action_dim": 7,
44
+ "context_dim": 0,
45
+ "depth": 13,
46
+ "fine_history_frames": 4,
47
+ "fine_memory_patch_size": 2,
48
+ "generation_frames": 1,
49
+ "hidden_size": 768,
50
+ "kv_heads": 3,
51
+ "memory_channels": 128,
52
+ "memory_depth": 4,
53
+ "memory_patch_size": 4,
54
+ "mlp_ratio": 4.0,
55
+ "num_heads": 12,
56
+ "prefix_encoder_depth": 4,
57
+ "qk_norm": true,
58
+ "register_tokens": 16,
59
+ "shared_modulation": true,
60
+ "target_patch_size": 2
61
+ },
62
+ "schema_version": 4,
63
+ "simulation": {
64
+ "max_shapes": 2,
65
+ "max_size": 6.0,
66
+ "max_speed": 20.0,
67
+ "min_shapes": 1,
68
+ "min_size": 3.0,
69
+ "min_speed": 8.0,
70
+ "supersample": 2,
71
+ "warmup_frames": 8
72
+ },
73
+ "training": {
74
+ "activation_checkpointing": true,
75
+ "critic_learning_rate": 0.0001,
76
+ "distribution_matching_start_step": 30000,
77
+ "distribution_matching_weight": 0.05,
78
+ "ema_decay": 0.9999,
79
+ "epochs": 100,
80
+ "flow_matching_ratio": 0.25,
81
+ "full_interval_ratio": 0.25,
82
+ "gradient_accumulation_steps": 2,
83
+ "history_noise_std": 0.01,
84
+ "keep_checkpoints": 5,
85
+ "learning_rate": 0.00018,
86
+ "log_every": 20,
87
+ "max_grad_norm": 1.0,
88
+ "min_learning_rate": 1e-05,
89
+ "on_policy_max_horizon": 4,
90
+ "on_policy_ratio": 0.15,
91
+ "on_policy_sampling_steps": 4,
92
+ "on_policy_start_step": 15000,
93
+ "output_dir": "runs/mining-220m-memory",
94
+ "reward_loss_weight": 0.5,
95
+ "save_every": 50,
96
+ "seed": 2026,
97
+ "teacher_endpoint_ratio": 0.1,
98
+ "teacher_integration_steps": 8,
99
+ "teacher_logit_mean": 1.0,
100
+ "teacher_logit_std": 1.0,
101
+ "teacher_steps": 10000,
102
+ "teacher_x0_weight": 0.25,
103
+ "terminal_loss_weight": 0.2,
104
+ "validate_every": 50,
105
+ "validation_batches": 1,
106
+ "warmup_steps": 500,
107
+ "weight_decay": 0.0
108
+ },
109
+ "video": {
110
+ "fps": 12.0,
111
+ "future_frames": 5,
112
+ "height": 256,
113
+ "history_frames": 12,
114
+ "memory_frames": 48,
115
+ "width": 256
116
+ }
117
+ }
demo.gif ADDED

Git LFS Details

  • SHA256: ef1bef6e72ce8601abecbc44aa4856e60756cc93465e9e336541f35ce2f0e4c8
  • Pointer size: 132 Bytes
  • Size of remote file: 6.25 MB
ema.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:2a0d4296e394410c8df763872529d2c1e652a6e193b7853c1e798ec4037d707c
3
+ size 880505584
live_infer.py ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Launch Diffusion Miner from this repo or a Hugging Face snapshot."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import runpy
7
+ import sys
8
+ from pathlib import Path
9
+
10
+
11
+ def _prepare_path() -> None:
12
+ here = Path(__file__).resolve().parent
13
+ for candidate in (here, here.parent / "src"):
14
+ if (candidate / "livediffusion" / "infer_mining.py").exists():
15
+ sys.path.insert(0, str(candidate))
16
+ return
17
+ raise FileNotFoundError("Could not find the livediffusion package next to this script.")
18
+
19
+
20
+ if __name__ == "__main__":
21
+ _prepare_path()
22
+ runpy.run_module("livediffusion.infer_mining", run_name="__main__")
livediffusion/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ """Diffusion Miner inference package."""
2
+
3
+ __version__ = "0.3.0"
livediffusion/config.py ADDED
@@ -0,0 +1,356 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import tomllib
5
+ from dataclasses import asdict, dataclass
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ SCHEMA_VERSION = 4
10
+
11
+
12
+ @dataclass(frozen=True, slots=True)
13
+ class VideoConfig:
14
+ height: int = 64
15
+ width: int = 64
16
+ fps: float = 2.0
17
+ history_frames: int = 5
18
+ future_frames: int = 5
19
+ # Frames scanned into the recurrent world memory before the attention prefix.
20
+ # Zero keeps memory and attention on the same window.
21
+ memory_frames: int = 0
22
+
23
+ @property
24
+ def scanned_frames(self) -> int:
25
+ return max(self.memory_frames, self.history_frames)
26
+
27
+
28
+ @dataclass(frozen=True, slots=True)
29
+ class SimulationConfig:
30
+ min_shapes: int = 1
31
+ max_shapes: int = 4
32
+ min_size: float = 5.0
33
+ max_size: float = 11.0
34
+ min_speed: float = 12.0
35
+ max_speed: float = 32.0
36
+ supersample: int = 2
37
+ warmup_frames: int = 8
38
+
39
+
40
+ @dataclass(frozen=True, slots=True)
41
+ class CodecConfig:
42
+ kind: str = "frame_vae"
43
+ model_id: str = "madebyollin/sdxl-vae-fp16-fix"
44
+ checkpoint_path: str = ""
45
+ subfolder: str = ""
46
+ revision: str = "main"
47
+ latent_channels: int = 4
48
+ spatial_compression: int = 8
49
+ temporal_compression: int = 1
50
+ frame_batch_size: int = 32
51
+ sample_posterior: bool = False
52
+ enable_slicing: bool = True
53
+ enable_tiling: bool = True
54
+
55
+
56
+ @dataclass(frozen=True, slots=True)
57
+ class DataConfig:
58
+ dataset_kind: str = "simulation"
59
+ data_path: str = ""
60
+ latent_cache_path: str = ""
61
+ hf_repo_id: str = ""
62
+ hf_revision: str = "main"
63
+ shard_shuffle: int = 256
64
+ sample_shuffle: int = 64
65
+ windows_per_video: int = 4
66
+ network_clip_prefetch: int = 2
67
+ train_samples: int = 50_000
68
+ validation_samples: int = 1_024
69
+ batch_size: int = 16
70
+ num_workers: int = 8
71
+ prefetch_batches: int = 2
72
+ latent_prefetch_batches: int = 6
73
+ pin_memory: bool = True
74
+ persistent_workers: bool = True
75
+ frame_stride: int = 1
76
+ live_monitor: int = 1
77
+ live_mouse_scale: float = 80.0
78
+
79
+
80
+ @dataclass(frozen=True, slots=True)
81
+ class ModelConfig:
82
+ target_patch_size: int = 1
83
+ memory_patch_size: int = 1
84
+ fine_memory_patch_size: int = 1
85
+ fine_history_frames: int = 2
86
+ hidden_size: int = 384
87
+ depth: int = 12
88
+ memory_depth: int = 3
89
+ prefix_encoder_depth: int = 0
90
+ num_heads: int = 8
91
+ kv_heads: int = 0
92
+ mlp_ratio: float = 4.0
93
+ qk_norm: bool = True
94
+ register_tokens: int = 0
95
+ shared_modulation: bool = False
96
+ memory_channels: int = 0
97
+ generation_frames: int = 1
98
+ action_dim: int = 0
99
+ context_dim: int = 0
100
+
101
+
102
+ @dataclass(frozen=True, slots=True)
103
+ class TrainingConfig:
104
+ output_dir: str = "runs/latent-world-model"
105
+ seed: int = 1_337
106
+ epochs: int = 100
107
+ learning_rate: float = 2e-4
108
+ min_learning_rate: float = 1e-5
109
+ weight_decay: float = 0.0
110
+ warmup_steps: int = 1_000
111
+ teacher_steps: int = 20_000
112
+ teacher_integration_steps: int = 8
113
+ teacher_endpoint_ratio: float = 0.1
114
+ teacher_x0_weight: float = 0.25
115
+ teacher_logit_mean: float = 1.0
116
+ teacher_logit_std: float = 1.0
117
+ gradient_accumulation_steps: int = 1
118
+ max_grad_norm: float = 1.0
119
+ flow_matching_ratio: float = 0.25
120
+ full_interval_ratio: float = 0.25
121
+ on_policy_start_step: int = 30_000
122
+ on_policy_ratio: float = 0.15
123
+ on_policy_max_horizon: int = 4
124
+ on_policy_sampling_steps: int = 4
125
+ history_noise_std: float = 0.01
126
+ reward_loss_weight: float = 1.0
127
+ terminal_loss_weight: float = 0.1
128
+ distribution_matching_start_step: int = 60_000
129
+ distribution_matching_weight: float = 0.05
130
+ critic_learning_rate: float = 1e-4
131
+ ema_decay: float = 0.9999
132
+ log_every: int = 20
133
+ validate_every: int = 1_000
134
+ validation_batches: int = 8
135
+ save_every: int = 5_000
136
+ keep_checkpoints: int = 5
137
+ activation_checkpointing: bool = True
138
+
139
+
140
+ @dataclass(frozen=True, slots=True)
141
+ class InferenceConfig:
142
+ steps: int = 2
143
+ frames: int = 120
144
+
145
+
146
+ @dataclass(frozen=True, slots=True)
147
+ class ProjectConfig:
148
+ schema_version: int
149
+ video: VideoConfig
150
+ simulation: SimulationConfig
151
+ codec: CodecConfig
152
+ data: DataConfig
153
+ model: ModelConfig
154
+ training: TrainingConfig
155
+ inference: InferenceConfig
156
+
157
+ def as_dict(self) -> dict[str, Any]:
158
+ return asdict(self)
159
+
160
+ def validate(self) -> None:
161
+ video = self.video
162
+ simulation = self.simulation
163
+ codec = self.codec
164
+ data = self.data
165
+ model = self.model
166
+ training = self.training
167
+ if self.schema_version != SCHEMA_VERSION:
168
+ raise ValueError(
169
+ f"Config schema {self.schema_version} is unsupported; expected {SCHEMA_VERSION}."
170
+ )
171
+ if video.history_frames < 1:
172
+ raise ValueError("At least one history frame is required.")
173
+ if video.height <= 0 or video.width <= 0 or video.fps <= 0:
174
+ raise ValueError("Video dimensions and FPS must be positive.")
175
+ latent_future_frames = (
176
+ (video.future_frames - 1) // codec.temporal_compression + 1
177
+ )
178
+ if (
179
+ latent_future_frames
180
+ < training.on_policy_max_horizon + model.generation_frames
181
+ ):
182
+ raise ValueError(
183
+ "Future latent frames must cover on-policy horizon and generation window."
184
+ )
185
+ if simulation.min_shapes < 1 or simulation.max_shapes < simulation.min_shapes:
186
+ raise ValueError("Invalid shape-count range.")
187
+ if not 0 < simulation.min_size <= simulation.max_size < min(video.height, video.width) / 2:
188
+ raise ValueError("Shape sizes must fit inside the frame.")
189
+ if simulation.min_speed <= 0 or simulation.max_speed < simulation.min_speed:
190
+ raise ValueError("Invalid speed range.")
191
+ if codec.kind not in {"frame_vae", "ltx2_video", "tile_vae", "identity"}:
192
+ raise ValueError(
193
+ "codec.kind must be 'frame_vae', 'ltx2_video', 'tile_vae', "
194
+ "or 'identity'."
195
+ )
196
+ if codec.kind == "tile_vae" and not codec.checkpoint_path:
197
+ raise ValueError("codec.checkpoint_path is required for kind='tile_vae'.")
198
+ if codec.spatial_compression < 1 or codec.temporal_compression < 1:
199
+ raise ValueError("Codec compression ratios must be positive.")
200
+ if video.height % codec.spatial_compression or video.width % codec.spatial_compression:
201
+ raise ValueError("Video dimensions must be divisible by codec spatial compression.")
202
+ latent_height = video.height // codec.spatial_compression
203
+ latent_width = video.width // codec.spatial_compression
204
+ for patch_size in (model.target_patch_size, model.memory_patch_size):
205
+ if latent_height % patch_size or latent_width % patch_size:
206
+ raise ValueError(f"Latent dimensions must be divisible by patch size {patch_size}.")
207
+ if (
208
+ latent_height % model.fine_memory_patch_size
209
+ or latent_width % model.fine_memory_patch_size
210
+ ):
211
+ raise ValueError("Latent dimensions must be divisible by fine memory patch size.")
212
+ if not 1 <= model.fine_history_frames <= video.history_frames:
213
+ raise ValueError("fine_history_frames must fit inside the history window.")
214
+ if model.hidden_size % model.num_heads:
215
+ raise ValueError("Hidden size must be divisible by the number of heads.")
216
+ if model.kv_heads < 0:
217
+ raise ValueError("kv_heads cannot be negative.")
218
+ if model.kv_heads and model.num_heads % model.kv_heads:
219
+ raise ValueError("num_heads must be divisible by kv_heads.")
220
+ if model.memory_channels < 0:
221
+ raise ValueError("memory_channels cannot be negative.")
222
+ if video.memory_frames and video.memory_frames < video.history_frames:
223
+ raise ValueError("memory_frames must be at least history_frames.")
224
+ if video.memory_frames and not model.memory_channels:
225
+ raise ValueError("memory_frames requires memory_channels to be positive.")
226
+ if model.generation_frames < 1:
227
+ raise ValueError("generation_frames must be positive.")
228
+ if model.action_dim > 0 and model.generation_frames != 1:
229
+ raise ValueError(
230
+ "Interactive action-conditioned models must generate one frame per step."
231
+ )
232
+ if (model.hidden_size // model.num_heads) < 12:
233
+ raise ValueError("Attention head size is too small for 3D rotary embeddings.")
234
+ if model.memory_depth < 0:
235
+ raise ValueError("memory_depth cannot be negative.")
236
+ if model.prefix_encoder_depth < 0:
237
+ raise ValueError("prefix_encoder_depth cannot be negative.")
238
+ if model.register_tokens < 0:
239
+ raise ValueError("register_tokens cannot be negative.")
240
+ if data.dataset_kind not in {
241
+ "simulation",
242
+ "pong",
243
+ "breakout",
244
+ "asteroids",
245
+ "racing",
246
+ "tank_duel",
247
+ "mining",
248
+ "frame_folder",
249
+ "video_folder",
250
+ "latent_cache",
251
+ "hf_webdataset",
252
+ "live_screen",
253
+ "minecraft_vpt",
254
+ }:
255
+ raise ValueError(f"Unsupported dataset kind: {data.dataset_kind}.")
256
+ if (
257
+ data.dataset_kind
258
+ not in {
259
+ "simulation",
260
+ "pong",
261
+ "breakout",
262
+ "asteroids",
263
+ "racing",
264
+ "tank_duel",
265
+ "mining",
266
+ "hf_webdataset",
267
+ "live_screen",
268
+ }
269
+ and not data.data_path
270
+ and not data.latent_cache_path
271
+ ):
272
+ raise ValueError("A data or latent-cache path is required for non-simulation datasets.")
273
+ if data.dataset_kind == "minecraft_vpt" and not data.data_path:
274
+ raise ValueError("data_path is required for minecraft_vpt ArrayRecord shards.")
275
+ if data.dataset_kind == "hf_webdataset" and not data.hf_repo_id:
276
+ raise ValueError("hf_repo_id is required for a Hugging Face WebDataset.")
277
+ if data.dataset_kind == "live_screen":
278
+ if data.num_workers != 0:
279
+ raise ValueError("live_screen requires num_workers = 0.")
280
+ if data.persistent_workers:
281
+ raise ValueError("live_screen requires persistent_workers = false.")
282
+ if data.live_monitor < 0:
283
+ raise ValueError("live_monitor cannot be negative.")
284
+ if data.live_mouse_scale <= 0:
285
+ raise ValueError("live_mouse_scale must be positive.")
286
+ if data.windows_per_video < 0:
287
+ raise ValueError("windows_per_video cannot be negative.")
288
+ if data.network_clip_prefetch < 1:
289
+ raise ValueError("network_clip_prefetch must be positive.")
290
+ if data.batch_size < 1 or data.prefetch_batches != 2:
291
+ raise ValueError(
292
+ "Batch size must be positive and prefetch_batches must be exactly two."
293
+ )
294
+ if data.latent_prefetch_batches < 1:
295
+ raise ValueError("latent_prefetch_batches must be positive.")
296
+ if data.frame_stride < 1:
297
+ raise ValueError("frame_stride must be positive.")
298
+ if data.num_workers == 0 and data.persistent_workers:
299
+ raise ValueError("persistent_workers requires at least one worker.")
300
+ if training.teacher_steps < training.warmup_steps:
301
+ raise ValueError("teacher_steps must be at least warmup_steps.")
302
+ if training.teacher_integration_steps < 2:
303
+ raise ValueError("Teacher integration requires at least two steps.")
304
+ if not 0 <= training.teacher_endpoint_ratio <= 1:
305
+ raise ValueError("teacher_endpoint_ratio must be in [0, 1].")
306
+ if training.teacher_logit_std <= 0:
307
+ raise ValueError("teacher_logit_std must be positive.")
308
+ if training.flow_matching_ratio + training.full_interval_ratio > 1:
309
+ raise ValueError("Flow-matching and full-interval ratios cannot exceed one.")
310
+ if not 0 <= training.on_policy_ratio <= 1:
311
+ raise ValueError("on_policy_ratio must be in [0, 1].")
312
+ if training.gradient_accumulation_steps < 1:
313
+ raise ValueError("gradient_accumulation_steps must be positive.")
314
+ if self.inference.steps < 1:
315
+ raise ValueError("Inference steps must be positive.")
316
+
317
+
318
+ def _config_from_raw(raw: dict[str, Any]) -> ProjectConfig:
319
+ sections = {
320
+ "schema_version",
321
+ "video",
322
+ "simulation",
323
+ "codec",
324
+ "data",
325
+ "model",
326
+ "training",
327
+ "inference",
328
+ }
329
+ unknown = set(raw) - sections
330
+ missing = sections - set(raw)
331
+ if unknown or missing:
332
+ raise ValueError(
333
+ f"Invalid config sections; missing={sorted(missing)}, unknown={sorted(unknown)}"
334
+ )
335
+ config = ProjectConfig(
336
+ schema_version=int(raw["schema_version"]),
337
+ video=VideoConfig(**raw["video"]),
338
+ simulation=SimulationConfig(**raw["simulation"]),
339
+ codec=CodecConfig(**raw["codec"]),
340
+ data=DataConfig(**raw["data"]),
341
+ model=ModelConfig(**raw["model"]),
342
+ training=TrainingConfig(**raw["training"]),
343
+ inference=InferenceConfig(**raw["inference"]),
344
+ )
345
+ config.validate()
346
+ return config
347
+
348
+
349
+ def load_config(path: str | Path) -> ProjectConfig:
350
+ config_path = Path(path)
351
+ if config_path.suffix.lower() == ".json":
352
+ raw = json.loads(config_path.read_text(encoding="utf-8"))
353
+ return _config_from_raw(raw)
354
+ with config_path.open("rb") as file:
355
+ raw = tomllib.load(file)
356
+ return _config_from_raw(raw)
livediffusion/infer_mining.py ADDED
@@ -0,0 +1,526 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """Play Diffusion Miner — an action-conditioned mining world model.
3
+
4
+ From this repo:
5
+
6
+ python3 -m livediffusion.infer_mining --checkpoint runs/mining-220m-memory --live
7
+
8
+ From a Hugging Face snapshot:
9
+
10
+ python live_infer.py
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import argparse
16
+ import time
17
+ import tkinter as tk
18
+ from collections import deque
19
+ from pathlib import Path
20
+
21
+ import numpy as np
22
+ import torch
23
+ from huggingface_hub import hf_hub_download
24
+ from PIL import Image, ImageOps, ImageTk
25
+ from safetensors.torch import load_file
26
+
27
+ from livediffusion.config import ProjectConfig, load_config
28
+ from livediffusion.meanflow import sample_next_latent
29
+ from livediffusion.mining import MiningSimulation, paint_player
30
+ from livediffusion.model import CausalLatentVideoDiT
31
+ from livediffusion.tile_vae import TileVAE
32
+
33
+ DEFAULT_REPO = "kerzgrr/diffusionminer"
34
+
35
+
36
+ def _parse_args() -> argparse.Namespace:
37
+ parser = argparse.ArgumentParser(description="Diffusion Miner live world model.")
38
+ parser.add_argument("--repo", default=DEFAULT_REPO)
39
+ parser.add_argument("--revision", default="main")
40
+ parser.add_argument("--local-dir", default="")
41
+ parser.add_argument("--checkpoint", default="")
42
+ parser.add_argument("--config", default="")
43
+ parser.add_argument("--codec", default="")
44
+ parser.add_argument("--steps", type=int, default=1)
45
+ parser.add_argument(
46
+ "--frames",
47
+ type=int,
48
+ default=0,
49
+ help="Frames to generate. 0 with a live window runs until quit.",
50
+ )
51
+ parser.add_argument("--seed", type=int, default=2026)
52
+ parser.add_argument("--window-scale", type=int, default=3)
53
+ parser.add_argument("--gif-scale", type=int, default=2)
54
+ parser.add_argument("--init", default="", help="Optional photo to seed history.")
55
+ parser.add_argument("--output", default="", help="Write a GIF instead of a window.")
56
+ parser.add_argument(
57
+ "--autoplay",
58
+ action="store_true",
59
+ help="Drive the model with the scripted miner used in training.",
60
+ )
61
+ parser.add_argument(
62
+ "--seed-play",
63
+ action="store_true",
64
+ help="Fill history with scripted gameplay instead of a still spawn.",
65
+ )
66
+ parser.add_argument(
67
+ "--live",
68
+ action=argparse.BooleanOptionalAction,
69
+ default=None,
70
+ )
71
+ return parser.parse_args()
72
+
73
+
74
+ def _resolve_checkpoint_dir(path: Path) -> Path:
75
+ if path.is_file():
76
+ return path.parent
77
+ latest = path / "latest.txt"
78
+ if (path / "ema.safetensors").exists():
79
+ return path
80
+ if latest.exists():
81
+ checkpoint = path / latest.read_text(encoding="utf-8").strip()
82
+ if (checkpoint / "ema.safetensors").exists():
83
+ return checkpoint
84
+ raise FileNotFoundError(f"No ema.safetensors under {path}.")
85
+
86
+
87
+ def _resolve_codec(config_path: Path, config: ProjectConfig, codec_arg: str) -> Path:
88
+ if codec_arg:
89
+ return Path(codec_arg)
90
+ path = Path(config.codec.checkpoint_path)
91
+ if path.exists():
92
+ return path
93
+ sibling = config_path.parent / path.name
94
+ if sibling.exists():
95
+ return sibling
96
+ raise FileNotFoundError(
97
+ f"Tile VAE checkpoint {path} not found. Pass --codec or place codec.pt "
98
+ f"next to {config_path}."
99
+ )
100
+
101
+
102
+ def _discover_local_bundle() -> Path | None:
103
+ here = Path(__file__).resolve()
104
+ for candidate in (Path.cwd(), here.parent, here.parents[1]):
105
+ if (candidate / "ema.safetensors").exists() and (candidate / "codec.pt").exists():
106
+ return candidate
107
+ return None
108
+
109
+
110
+ def _resolve_assets(args: argparse.Namespace) -> tuple[Path, Path, Path]:
111
+ if not args.checkpoint and not args.local_dir:
112
+ discovered = _discover_local_bundle()
113
+ if discovered is not None:
114
+ args.local_dir = str(discovered)
115
+ if args.checkpoint:
116
+ checkpoint = _resolve_checkpoint_dir(Path(args.checkpoint))
117
+ weights = checkpoint / "ema.safetensors"
118
+ config_path = Path(args.config) if args.config else checkpoint / "config.json"
119
+ codec_path = _resolve_codec(config_path, load_config(config_path), args.codec)
120
+ return weights, config_path, codec_path
121
+ root = Path(args.local_dir) if args.local_dir else None
122
+ names = ("ema.safetensors", "config.json", "codec.pt")
123
+ if root is not None and all((root / name).exists() for name in names):
124
+ return root / "ema.safetensors", root / "config.json", root / "codec.pt"
125
+ downloaded = [
126
+ Path(
127
+ hf_hub_download(
128
+ args.repo,
129
+ filename=name,
130
+ revision=args.revision,
131
+ local_dir=root,
132
+ )
133
+ )
134
+ for name in names
135
+ ]
136
+ return downloaded[0], downloaded[1], downloaded[2]
137
+
138
+
139
+ def _load_photo(path: Path, width: int, height: int) -> np.ndarray:
140
+ with Image.open(path) as raw:
141
+ image = ImageOps.exif_transpose(raw.convert("RGB"))
142
+ fitted = ImageOps.fit(image, (width, height), method=Image.Resampling.LANCZOS)
143
+ return np.asarray(fitted, dtype=np.uint8)
144
+
145
+
146
+ def _frames_to_tensor(frames: np.ndarray, device: torch.device) -> torch.Tensor:
147
+ tensor = torch.from_numpy(np.ascontiguousarray(frames)).permute(0, 3, 1, 2).float()
148
+ return tensor.div_(127.5).sub_(1.0).unsqueeze(0).to(device)
149
+
150
+
151
+ def _tensor_to_image(frame: torch.Tensor) -> Image.Image:
152
+ array = frame.float().clamp(-1.0, 1.0).add(1.0).mul(127.5).byte()
153
+ return Image.fromarray(array.permute(1, 2, 0).cpu().numpy(), mode="RGB")
154
+
155
+
156
+ def _image_to_frame(image: Image.Image, device: torch.device) -> torch.Tensor:
157
+ array = np.asarray(image, dtype=np.uint8).copy()
158
+ return (
159
+ torch.from_numpy(array)
160
+ .permute(2, 0, 1)
161
+ .float()
162
+ .div_(127.5)
163
+ .sub_(1.0)
164
+ .to(device, non_blocking=True)
165
+ )
166
+
167
+
168
+ class LiveWindow:
169
+ def __init__(self, scale: int) -> None:
170
+ self.scale = max(1, scale)
171
+ self.is_open = True
172
+ self.pressed: set[str] = set()
173
+ self.pending_click: tuple[float, float] | None = None
174
+ self.generated_width = 0
175
+ self.generated_height = 0
176
+ self.root = tk.Tk()
177
+ self.root.title("Diffusion Miner")
178
+ self.label = tk.Label(self.root, background="black")
179
+ self.label.pack()
180
+ self.photo: ImageTk.PhotoImage | None = None
181
+ self.root.bind("<KeyPress>", self._on_press)
182
+ self.root.bind("<KeyRelease>", self._on_release)
183
+ self.root.bind("<Button-1>", self._on_click)
184
+ self.root.protocol("WM_DELETE_WINDOW", self.close)
185
+ self.root.focus_force()
186
+
187
+ def _on_press(self, event: tk.Event) -> None:
188
+ key = str(event.keysym).lower()
189
+ if key == "escape":
190
+ self.close()
191
+ return
192
+ if key in {"w", "s", "a", "d", "left", "right", "up", "down", "space"}:
193
+ self.pressed.add(key)
194
+
195
+ def _on_release(self, event: tk.Event) -> None:
196
+ self.pressed.discard(str(event.keysym).lower())
197
+
198
+ def _on_click(self, event: tk.Event) -> None:
199
+ if self.generated_width <= 0 or self.generated_height <= 0:
200
+ return
201
+ x = (float(event.x) / self.scale) % self.generated_width
202
+ y = min(max(float(event.y) / self.scale, 0.0), self.generated_height - 1.0)
203
+ self.pending_click = (x, y)
204
+
205
+ def consume_click(self) -> tuple[float, float] | None:
206
+ click = self.pending_click
207
+ self.pending_click = None
208
+ return click
209
+
210
+ def mining_action(self) -> tuple[float, ...]:
211
+ left = "a" in self.pressed
212
+ right = "d" in self.pressed
213
+ if left and right:
214
+ left = right = False
215
+ dig_left = "left" in self.pressed
216
+ dig_right = "right" in self.pressed and not dig_left
217
+ dig_up = "up" in self.pressed and not (dig_left or dig_right)
218
+ dig_down = "down" in self.pressed and not (dig_left or dig_right or dig_up)
219
+ jump = bool(self.pressed & {"w", "space"})
220
+ return (
221
+ float(left),
222
+ float(right),
223
+ float(jump),
224
+ float(dig_left),
225
+ float(dig_right),
226
+ float(dig_up),
227
+ float(dig_down),
228
+ )
229
+
230
+ def close(self) -> None:
231
+ if self.is_open:
232
+ self.is_open = False
233
+ self.root.destroy()
234
+
235
+ def show(self, image: Image.Image, frame_index: int, latency_ms: float) -> bool:
236
+ if not self.is_open:
237
+ return False
238
+ self.generated_width, self.generated_height = image.size
239
+ display = image.resize(
240
+ (image.width * self.scale, image.height * self.scale),
241
+ Image.Resampling.NEAREST,
242
+ )
243
+ self.photo = ImageTk.PhotoImage(display)
244
+ self.label.configure(image=self.photo)
245
+ held = "+".join(sorted(self.pressed)).upper()
246
+ extra = f" held {held}" if held else ""
247
+ self.root.title(
248
+ f"Diffusion Miner — frame {frame_index + 1} {latency_ms:.0f} ms{extra}"
249
+ )
250
+ try:
251
+ self.root.update_idletasks()
252
+ self.root.update()
253
+ except tk.TclError:
254
+ self.is_open = False
255
+ return self.is_open
256
+
257
+
258
+ def _encode_frames(vae: TileVAE, frames: torch.Tensor, batch_size: int) -> torch.Tensor:
259
+ batch, count, channels, height, width = frames.shape
260
+ flat = frames.reshape(batch * count, channels, height, width)
261
+ encoded = [
262
+ vae.encode(chunk.contiguous(memory_format=torch.channels_last), sample=False)
263
+ for chunk in flat.split(batch_size)
264
+ ]
265
+ latents = torch.cat(encoded)
266
+ return latents.reshape(
267
+ batch,
268
+ count,
269
+ vae.latent_channels,
270
+ height // vae.spatial_compression,
271
+ width // vae.spatial_compression,
272
+ )
273
+
274
+
275
+ def _decode_latents(vae: TileVAE, latents: torch.Tensor, batch_size: int) -> torch.Tensor:
276
+ batch, count, channels, height, width = latents.shape
277
+ flat = latents.reshape(batch * count, channels, height, width)
278
+ decoded = [
279
+ vae.decode(chunk.contiguous(memory_format=torch.channels_last))
280
+ for chunk in flat.split(batch_size)
281
+ ]
282
+ return torch.cat(decoded).reshape(
283
+ batch,
284
+ count,
285
+ 3,
286
+ height * vae.spatial_compression,
287
+ width * vae.spatial_compression,
288
+ ).clamp(-1.0, 1.0)
289
+
290
+
291
+ def _paint_player(image: Image.Image, position: tuple[float, float]) -> Image.Image:
292
+ frame = np.asarray(image, dtype=np.uint8).copy()
293
+ tile = max(4, min(frame.shape[0], frame.shape[1]) // 32)
294
+ paint_player(
295
+ frame,
296
+ round(position[0] - tile / 2),
297
+ round(position[1] - tile),
298
+ tile,
299
+ tile * 2,
300
+ )
301
+ return Image.fromarray(frame, mode="RGB")
302
+
303
+
304
+ def _inject_player(
305
+ history: deque[torch.Tensor],
306
+ vae: TileVAE,
307
+ batch_size: int,
308
+ position: tuple[float, float],
309
+ ) -> Image.Image:
310
+ history_tensor = torch.stack(tuple(history), dim=1)
311
+ device = history_tensor.device
312
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
313
+ decoded = _decode_latents(vae, history_tensor, batch_size)
314
+ painted: list[torch.Tensor] = []
315
+ preview = None
316
+ for index in range(decoded.shape[1]):
317
+ image = _paint_player(_tensor_to_image(decoded[0, index]), position)
318
+ preview = image
319
+ painted.append(_image_to_frame(image, device))
320
+ rgb = torch.stack(painted, dim=0).unsqueeze(0)
321
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
322
+ latents = _encode_frames(vae, rgb, batch_size)
323
+ history.clear()
324
+ history.extend(latents[:, index] for index in range(latents.shape[1]))
325
+ if preview is None:
326
+ raise RuntimeError("History was empty.")
327
+ return preview
328
+
329
+
330
+ def _seed_history(
331
+ args: argparse.Namespace,
332
+ config: ProjectConfig,
333
+ vae: TileVAE,
334
+ device: torch.device,
335
+ ) -> tuple[deque[torch.Tensor], deque[torch.Tensor], MiningSimulation]:
336
+ scanned = config.video.scanned_frames
337
+ simulation = MiningSimulation.from_seed(config.video, args.seed, at_start=True)
338
+ actions = np.zeros((scanned, MiningSimulation.ACTION_DIM), dtype=np.float32)
339
+ if args.init:
340
+ frame = _load_photo(Path(args.init), config.video.width, config.video.height)
341
+ rgb = np.repeat(frame[None], scanned, axis=0)
342
+ elif args.seed_play:
343
+ rgb, actions, _, _ = simulation.sample_gameplay(scanned)
344
+ else:
345
+ rgb = np.repeat(simulation.render()[None], scanned, axis=0)
346
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
347
+ history_tensor = _encode_frames(
348
+ vae,
349
+ _frames_to_tensor(rgb, device),
350
+ config.codec.frame_batch_size,
351
+ )
352
+ history = deque(
353
+ (history_tensor[:, index] for index in range(history_tensor.shape[1])),
354
+ maxlen=history_tensor.shape[1],
355
+ )
356
+ action_history = deque(
357
+ (
358
+ torch.as_tensor(actions[index], device=device, dtype=torch.float32).unsqueeze(0)
359
+ for index in range(scanned)
360
+ ),
361
+ maxlen=scanned,
362
+ )
363
+ return history, action_history, simulation
364
+
365
+
366
+ def _next_action(
367
+ args: argparse.Namespace,
368
+ simulation: MiningSimulation,
369
+ window: LiveWindow | None,
370
+ device: torch.device,
371
+ ) -> torch.Tensor:
372
+ if args.autoplay:
373
+ action = simulation.scripted_action()
374
+ simulation.step(action)
375
+ values = action
376
+ elif window is not None:
377
+ values = window.mining_action()
378
+ else:
379
+ values = (0.0,) * MiningSimulation.ACTION_DIM
380
+ return torch.tensor(values, device=device, dtype=torch.float32).reshape(
381
+ 1,
382
+ 1,
383
+ MiningSimulation.ACTION_DIM,
384
+ )
385
+
386
+
387
+ @torch.inference_mode()
388
+ def main() -> None:
389
+ args = _parse_args()
390
+ live = not bool(args.output) if args.live is None else args.live
391
+ if args.frames == 0:
392
+ if not live:
393
+ raise ValueError("--frames 0 requires a live window.")
394
+ frame_count = 10**9
395
+ else:
396
+ frame_count = args.frames
397
+ if args.steps < 1:
398
+ raise ValueError("steps must be positive.")
399
+ if args.gif_scale < 1:
400
+ raise ValueError("gif-scale must be positive.")
401
+ if not torch.cuda.is_available() or not torch.cuda.is_bf16_supported():
402
+ raise RuntimeError("Diffusion Miner needs a BF16 CUDA GPU.")
403
+
404
+ weights_path, config_path, codec_path = _resolve_assets(args)
405
+ config = load_config(config_path)
406
+ device = torch.device("cuda")
407
+ torch.set_float32_matmul_precision("high")
408
+
409
+ vae = TileVAE.load(codec_path, device=device, dtype=torch.bfloat16)
410
+ vae.requires_grad_(False)
411
+ vae.to(memory_format=torch.channels_last)
412
+ model = CausalLatentVideoDiT(
413
+ config.model,
414
+ latent_channels=vae.latent_channels,
415
+ history_frames=config.video.history_frames,
416
+ ).to(device).eval()
417
+ model.load_state_dict(load_file(weights_path, device=str(device)), strict=True)
418
+
419
+ history, action_history, simulation = _seed_history(args, config, vae, device)
420
+ window = LiveWindow(args.window_scale) if live else None
421
+ images: list[Image.Image] = []
422
+ generator = torch.Generator(device=device).manual_seed(args.seed)
423
+ print(
424
+ "Diffusion Miner ready. A/D move, W/Space jump, arrows dig, click to "
425
+ "drop a miner, Esc quit."
426
+ if live
427
+ else f"Writing {frame_count} frames to {args.output}."
428
+ )
429
+ frame_index = 0
430
+ force_sprite_frames = 0
431
+ injected: tuple[float, float] | None = None
432
+ while frame_index < frame_count:
433
+ if window is not None and not window.is_open:
434
+ break
435
+ click = window.consume_click() if window is not None else None
436
+ if click is not None:
437
+ preview = _inject_player(
438
+ history,
439
+ vae,
440
+ config.codec.frame_batch_size,
441
+ click,
442
+ )
443
+ injected = click
444
+ force_sprite_frames = len(history)
445
+ if not window.show(preview, max(frame_index - 1, 0), 0.0):
446
+ break
447
+ action = _next_action(args, simulation, window, device)
448
+ history_tensor = torch.stack(tuple(history), dim=1)
449
+ history_actions = torch.stack(tuple(action_history), dim=1)
450
+ torch.cuda.synchronize()
451
+ started = time.perf_counter()
452
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
453
+ cache = model.build_memory_cache(
454
+ history_tensor,
455
+ history_actions=history_actions,
456
+ )
457
+ latent = sample_next_latent(
458
+ model,
459
+ history_tensor,
460
+ steps=args.steps,
461
+ actions=action,
462
+ generator=generator,
463
+ memory_cache=cache,
464
+ instantaneous=True,
465
+ history_actions=history_actions,
466
+ )
467
+ frame = _decode_latents(
468
+ vae,
469
+ latent,
470
+ config.codec.frame_batch_size,
471
+ )[0, 0]
472
+ torch.cuda.synchronize()
473
+ latency_ms = (time.perf_counter() - started) * 1_000
474
+ image = _tensor_to_image(frame)
475
+ frame_latent = latent[:, 0]
476
+ if injected is not None and force_sprite_frames > 0:
477
+ painted = _paint_player(image, injected)
478
+ rgb = _image_to_frame(painted, device).reshape(
479
+ 1,
480
+ 1,
481
+ 3,
482
+ painted.height,
483
+ painted.width,
484
+ )
485
+ with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
486
+ frame_latent = _encode_frames(
487
+ vae,
488
+ rgb,
489
+ config.codec.frame_batch_size,
490
+ )[:, 0]
491
+ image = painted
492
+ force_sprite_frames -= 1
493
+ if force_sprite_frames == 0:
494
+ injected = None
495
+ history.append(frame_latent)
496
+ action_history.append(action[:, 0])
497
+ if window is not None and not window.show(image, frame_index, latency_ms):
498
+ break
499
+ if args.output:
500
+ if args.gif_scale > 1:
501
+ image = image.resize(
502
+ (image.width * args.gif_scale, image.height * args.gif_scale),
503
+ Image.Resampling.NEAREST,
504
+ )
505
+ images.append(image)
506
+ frame_index += 1
507
+ if window is not None:
508
+ window.close()
509
+ if args.output:
510
+ if not images:
511
+ raise RuntimeError("No frames generated.")
512
+ output = Path(args.output)
513
+ output.parent.mkdir(parents=True, exist_ok=True)
514
+ images[0].save(
515
+ output,
516
+ save_all=True,
517
+ append_images=images[1:],
518
+ duration=round(1_000 / config.video.fps),
519
+ loop=0,
520
+ optimize=False,
521
+ )
522
+ print(f"Saved {len(images)} frames to {output}")
523
+
524
+
525
+ if __name__ == "__main__":
526
+ main()
livediffusion/meanflow.py ADDED
@@ -0,0 +1,238 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+
5
+ import torch
6
+ from torch import nn
7
+
8
+
9
+ @dataclass(slots=True)
10
+ class FlowLoss:
11
+ loss: torch.Tensor
12
+ raw_mse: torch.Tensor
13
+ x0_mse: torch.Tensor
14
+ prediction_rms: torch.Tensor
15
+ target_rms: torch.Tensor
16
+ mean_interval: torch.Tensor
17
+
18
+
19
+ def _adaptive_loss(
20
+ prediction: torch.Tensor,
21
+ target: torch.Tensor,
22
+ interval: torch.Tensor,
23
+ gamma: float = 0.5,
24
+ epsilon: float = 1e-3,
25
+ ) -> FlowLoss:
26
+ squared_error = (prediction.float() - target.float()).square()
27
+ per_sample_mse = squared_error.flatten(1).mean(dim=1)
28
+ weights = (per_sample_mse + epsilon).pow(gamma - 1.0).detach()
29
+ return FlowLoss(
30
+ loss=(weights * per_sample_mse).mean(),
31
+ raw_mse=per_sample_mse.mean().detach(),
32
+ x0_mse=torch.zeros((), device=prediction.device),
33
+ prediction_rms=prediction.float().square().mean().sqrt().detach(),
34
+ target_rms=target.float().square().mean().sqrt().detach(),
35
+ mean_interval=interval.float().mean().detach(),
36
+ )
37
+
38
+
39
+ def sample_time_intervals(
40
+ batch_size: int,
41
+ device: torch.device,
42
+ flow_matching_ratio: float,
43
+ full_interval_ratio: float,
44
+ ) -> tuple[torch.Tensor, torch.Tensor]:
45
+ t = torch.rand(batch_size, device=device, dtype=torch.float32)
46
+ r = torch.rand(batch_size, device=device, dtype=torch.float32) * t
47
+ selector = torch.rand(batch_size, device=device)
48
+ full_mask = selector < full_interval_ratio
49
+ flow_mask = (selector >= full_interval_ratio) & (
50
+ selector < full_interval_ratio + flow_matching_ratio
51
+ )
52
+ t = torch.where(full_mask, torch.ones_like(t), t)
53
+ r = torch.where(full_mask, torch.zeros_like(r), r)
54
+ r = torch.where(flow_mask, t, r)
55
+ return r, t
56
+
57
+
58
+ def flow_matching_loss(
59
+ model: nn.Module,
60
+ clean_target: torch.Tensor,
61
+ history: torch.Tensor,
62
+ actions: torch.Tensor | None = None,
63
+ history_noise_std: float = 0.0,
64
+ endpoint_ratio: float = 0.0,
65
+ history_actions: torch.Tensor | None = None,
66
+ x0_weight: float = 0.0,
67
+ logit_mean: float = 0.0,
68
+ logit_std: float = 1.0,
69
+ ) -> FlowLoss:
70
+ batch_size = clean_target.shape[0]
71
+ logits = (
72
+ torch.randn(batch_size, device=clean_target.device, dtype=torch.float32)
73
+ * logit_std
74
+ + logit_mean
75
+ )
76
+ t = logits.sigmoid()
77
+ endpoint_mask = torch.rand(batch_size, device=clean_target.device) < endpoint_ratio
78
+ t = torch.where(endpoint_mask, torch.ones_like(t), t)
79
+ noise = torch.randn_like(clean_target)
80
+ velocity = noise - clean_target
81
+ broadcast_t = t.reshape(batch_size, *([1] * (clean_target.ndim - 1)))
82
+ noisy_target = clean_target + broadcast_t.to(clean_target.dtype) * velocity
83
+ if history_noise_std > 0:
84
+ history = history + torch.randn_like(history) * history_noise_std
85
+ prediction = model(
86
+ noisy_target,
87
+ history,
88
+ t,
89
+ t,
90
+ actions=actions,
91
+ history_actions=history_actions,
92
+ )
93
+ result = _adaptive_loss(
94
+ prediction,
95
+ velocity,
96
+ torch.zeros_like(t),
97
+ gamma=1.0,
98
+ )
99
+ predicted_x0 = noisy_target - broadcast_t.to(prediction.dtype) * prediction
100
+ x0_mse = (
101
+ predicted_x0.float() - clean_target.float()
102
+ ).square().flatten(1).mean(dim=1).mean()
103
+ result.loss = result.loss + x0_weight * x0_mse
104
+ result.x0_mse = x0_mse.detach()
105
+ return result
106
+
107
+
108
+ @torch.no_grad()
109
+ def _teacher_average_velocity(
110
+ teacher: nn.Module,
111
+ noisy_target: torch.Tensor,
112
+ history: torch.Tensor,
113
+ r: torch.Tensor,
114
+ t: torch.Tensor,
115
+ integration_steps: int,
116
+ actions: torch.Tensor | None,
117
+ history_actions: torch.Tensor | None = None,
118
+ ) -> torch.Tensor:
119
+ state = noisy_target
120
+ interval = t - r
121
+ step_size = interval / integration_steps
122
+ broadcast_step = step_size.reshape(
123
+ step_size.shape[0],
124
+ *([1] * (noisy_target.ndim - 1)),
125
+ ).to(noisy_target.dtype)
126
+ for index in range(integration_steps):
127
+ current_t = t - step_size * index
128
+ velocity = teacher(
129
+ state,
130
+ history,
131
+ current_t,
132
+ current_t,
133
+ actions=actions,
134
+ history_actions=history_actions,
135
+ )
136
+ state = state - broadcast_step * velocity
137
+ broadcast_interval = interval.reshape(
138
+ interval.shape[0],
139
+ *([1] * (noisy_target.ndim - 1)),
140
+ ).to(noisy_target.dtype)
141
+ return (noisy_target - state) / broadcast_interval.clamp_min(1e-5)
142
+
143
+
144
+ def flow_map_distillation_loss(
145
+ student: nn.Module,
146
+ teacher: nn.Module,
147
+ clean_target: torch.Tensor,
148
+ history: torch.Tensor,
149
+ flow_matching_ratio: float,
150
+ full_interval_ratio: float,
151
+ integration_steps: int,
152
+ actions: torch.Tensor | None = None,
153
+ history_noise_std: float = 0.0,
154
+ history_actions: torch.Tensor | None = None,
155
+ ) -> FlowLoss:
156
+ batch_size = clean_target.shape[0]
157
+ r, t = sample_time_intervals(
158
+ batch_size,
159
+ clean_target.device,
160
+ flow_matching_ratio,
161
+ full_interval_ratio,
162
+ )
163
+ noise = torch.randn_like(clean_target)
164
+ velocity = noise - clean_target
165
+ broadcast_t = t.reshape(batch_size, *([1] * (clean_target.ndim - 1)))
166
+ noisy_target = clean_target + broadcast_t.to(clean_target.dtype) * velocity
167
+ if history_noise_std > 0:
168
+ history = history + torch.randn_like(history) * history_noise_std
169
+ interval = t - r
170
+ teacher_target = _teacher_average_velocity(
171
+ teacher,
172
+ noisy_target,
173
+ history,
174
+ r,
175
+ t,
176
+ integration_steps,
177
+ actions,
178
+ history_actions,
179
+ )
180
+ flow_mask = interval <= 1e-6
181
+ broadcast_mask = flow_mask.reshape(
182
+ batch_size,
183
+ *([1] * (clean_target.ndim - 1)),
184
+ )
185
+ target = torch.where(broadcast_mask, velocity, teacher_target).detach()
186
+ prediction = student(
187
+ noisy_target,
188
+ history,
189
+ r,
190
+ t,
191
+ actions=actions,
192
+ history_actions=history_actions,
193
+ )
194
+ return _adaptive_loss(prediction, target, interval)
195
+
196
+
197
+ @torch.no_grad()
198
+ def sample_next_latent(
199
+ model: nn.Module,
200
+ history: torch.Tensor,
201
+ steps: int = 2,
202
+ actions: torch.Tensor | None = None,
203
+ generator: torch.Generator | None = None,
204
+ window_frames: int = 1,
205
+ memory_cache: object | None = None,
206
+ instantaneous: bool = False,
207
+ history_actions: torch.Tensor | None = None,
208
+ ) -> torch.Tensor:
209
+ if steps < 1:
210
+ raise ValueError("steps must be positive.")
211
+ batch, _, channels, height, width = history.shape
212
+ state = torch.randn(
213
+ (batch, window_frames, channels, height, width),
214
+ device=history.device,
215
+ dtype=history.dtype,
216
+ generator=generator,
217
+ )
218
+ boundaries = torch.linspace(1.0, 0.0, steps + 1, device=history.device)
219
+ for index in range(steps):
220
+ t_value = boundaries[index]
221
+ r_value = boundaries[index + 1]
222
+ t = t_value.expand(batch)
223
+ r = r_value.expand(batch)
224
+ model_r = t if instantaneous else r
225
+ average_velocity = model(
226
+ state,
227
+ history,
228
+ model_r,
229
+ t,
230
+ actions=actions,
231
+ memory_cache=memory_cache,
232
+ history_actions=history_actions,
233
+ )
234
+ state = state - (t_value - r_value).to(state.dtype) * average_velocity
235
+ return state
236
+
237
+
238
+ sample_next_frame = sample_next_latent
livediffusion/mining.py ADDED
@@ -0,0 +1,644 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import numpy as np
4
+
5
+ from livediffusion.config import VideoConfig
6
+
7
+ AIR = 0
8
+ DIRT = 1
9
+ STONE = 2
10
+ SAND = 3
11
+ GRAVEL = 4
12
+ WATER = 5
13
+ ORE = 6
14
+ BEDROCK = 7
15
+
16
+ TILE_COUNT = 8
17
+
18
+ # Flat, high-contrast palette. No thin lines anywhere, so an 8x VAE keeps the edges.
19
+ TILE_COLORS = np.array(
20
+ [
21
+ (16, 18, 30),
22
+ (126, 84, 48),
23
+ (108, 112, 122),
24
+ (214, 182, 96),
25
+ (52, 54, 62),
26
+ (44, 104, 216),
27
+ (72, 240, 236),
28
+ (74, 62, 102),
29
+ ],
30
+ dtype=np.int16,
31
+ )
32
+ # Every tile hue is separable from every other one and from the player, so a
33
+ # reconstruction error is never ambiguous about which material it was.
34
+ _DIG_FLASH_COLOR = (255, 84, 240)
35
+ _PLAYER_HEAD_COLOR = (248, 252, 255)
36
+ _PLAYER_BODY_COLOR = (232, 96, 72)
37
+
38
+ # Water is passable; everything else with a body blocks the player.
39
+ TILE_SOLID = np.array([0, 1, 1, 1, 1, 0, 1, 1], dtype=bool)
40
+ # Sand and gravel fall; bedrock never moves; ore and stone stay until mined.
41
+ TILE_FALLS = np.array([0, 0, 0, 1, 1, 0, 0, 0], dtype=bool)
42
+ TILE_DIGGABLE = np.array([0, 1, 1, 1, 1, 0, 1, 0], dtype=bool)
43
+ # Per-tile shade jitter is a pure function of position, so it is memorizable structure
44
+ # rather than noise the model cannot predict.
45
+ _SHADE_RANGE = 10
46
+ _PLAYER_EYE_COLOR = (24, 28, 40)
47
+
48
+
49
+ def paint_player(
50
+ frame: np.ndarray,
51
+ left: int,
52
+ top: int,
53
+ width: int,
54
+ height: int,
55
+ facing: int = 1,
56
+ ) -> None:
57
+ """Draw the miner onto an RGB frame. Used by the sim and by infer click-spawn."""
58
+ right = min(frame.shape[1], left + width)
59
+ bottom = min(frame.shape[0], top + height)
60
+ if right <= left or bottom <= top:
61
+ return
62
+ body_top = min(bottom, top + height // 2)
63
+ frame[max(0, top) : body_top, max(0, left) : right] = _PLAYER_HEAD_COLOR
64
+ frame[body_top:bottom, max(0, left) : right] = _PLAYER_BODY_COLOR
65
+ eye = left + (width - width // 3 if facing > 0 else 0)
66
+ eye_right = min(frame.shape[1], eye + max(1, width // 3))
67
+ eye_bottom = min(bottom, max(0, top) + max(1, height // 6))
68
+ frame[max(0, top) : eye_bottom, max(0, eye) : eye_right] = _PLAYER_EYE_COLOR
69
+
70
+
71
+ def _shift_down(mask: np.ndarray) -> np.ndarray:
72
+ shifted = np.zeros_like(mask)
73
+ shifted[1:] = mask[:-1]
74
+ return shifted
75
+
76
+
77
+ def _shift_horizontal(mask: np.ndarray, step: int) -> np.ndarray:
78
+ shifted = np.zeros_like(mask)
79
+ if step > 0:
80
+ shifted[:, step:] = mask[:, :-step]
81
+ else:
82
+ shifted[:, :step] = mask[:, -step:]
83
+ return shifted
84
+
85
+
86
+ def _smooth(values: np.ndarray, passes: int) -> np.ndarray:
87
+ for _ in range(passes):
88
+ values = (values + np.roll(values, 1) + np.roll(values, -1)) / 3.0
89
+ return values
90
+
91
+
92
+ class MiningSimulation:
93
+ """Single-screen tile sandbox with irreversible digging, falling sand, and water."""
94
+
95
+ # [left, right, jump, dig_left, dig_right, dig_up, dig_down]
96
+ ACTION_DIM = 7
97
+
98
+ def __init__(self, video_config: VideoConfig, seed: int) -> None:
99
+ self.video_config = video_config
100
+ self.rng = np.random.default_rng(seed)
101
+ self.dt = 1.0 / video_config.fps
102
+ self.tile = max(4, min(video_config.width, video_config.height) // 32)
103
+ self.grid_width = video_config.width // self.tile
104
+ self.grid_height = video_config.height // self.tile
105
+ self.gravity = self.tile * 42.0
106
+ self.move_speed = self.tile * 7.5
107
+ self.jump_speed = self.tile * 17.0
108
+ self.terminal_speed = self.tile * 30.0
109
+ self.swim_drag = 0.45
110
+ self.dig_cooldown = 0.16
111
+ self.player_width = float(self.tile)
112
+ self.player_height = float(self.tile * 2)
113
+ self.frame_index = 0
114
+ self.ore_collected = 0
115
+ self.deaths = 0
116
+ self.grid = np.zeros((self.grid_height, self.grid_width), dtype=np.uint8)
117
+ self.shade = self._make_shade()
118
+ self.position = np.zeros(2, dtype=np.float64)
119
+ self.velocity = np.zeros(2, dtype=np.float64)
120
+ self.facing = 1
121
+ self.dig_timer = 0.0
122
+ self.dig_target: tuple[tuple[int, int], ...] = ()
123
+ self.dig_flash = 0.0
124
+ self.drown_timer = 0.0
125
+ self.pending_reward = 0.0
126
+ self.pending_terminal = 0.0
127
+ self._action = (0,) * self.ACTION_DIM
128
+ self._hold_frames = 0
129
+ self._stuck_frames = 0
130
+ self._last_cell = (-1, -1)
131
+ self._reset_round()
132
+ for _ in range(6):
133
+ self.step()
134
+
135
+ @classmethod
136
+ def from_seed(
137
+ cls,
138
+ video_config: VideoConfig,
139
+ seed: int,
140
+ *,
141
+ at_start: bool = False,
142
+ ) -> MiningSimulation:
143
+ simulation = cls(video_config, seed)
144
+ if at_start:
145
+ simulation.reset_to_start()
146
+ return simulation
147
+
148
+ def reset_to_start(self) -> None:
149
+ self.ore_collected = 0
150
+ self.deaths = 0
151
+ self.frame_index = 0
152
+ self._reset_round()
153
+
154
+ def _make_shade(self) -> np.ndarray:
155
+ rows = np.arange(self.grid_height)[:, None]
156
+ columns = np.arange(self.grid_width)[None, :]
157
+ mixed = (rows * 73_856_093) ^ (columns * 19_349_663)
158
+ return ((mixed % (2 * _SHADE_RANGE + 1)) - _SHADE_RANGE).astype(np.int16)
159
+
160
+ def _generate_world(self) -> None:
161
+ height = self.grid_height
162
+ width = self.grid_width
163
+ grid = np.full((height, width), STONE, dtype=np.uint8)
164
+
165
+ walk = self.rng.normal(0.0, 1.0, size=width)
166
+ surface = _smooth(walk, 6)
167
+ surface = surface / max(float(np.abs(surface).max()), 1e-6)
168
+ base = height * 0.28
169
+ amplitude = height * 0.08
170
+ surface_rows = np.clip(
171
+ np.round(base + surface * amplitude).astype(int),
172
+ 2,
173
+ height - 8,
174
+ )
175
+
176
+ rows = np.arange(height)[:, None]
177
+ grid[rows < surface_rows[None, :]] = AIR
178
+ dirt_depth = self.rng.integers(3, 6, size=width)
179
+ dirt_limit = surface_rows + dirt_depth
180
+ grid[(rows >= surface_rows[None, :]) & (rows < dirt_limit[None, :])] = DIRT
181
+
182
+ self._carve_caves(grid, surface_rows)
183
+ self._scatter(grid, ORE, count=self.rng.integers(9, 15), radius=1, in_stone=True)
184
+ self._scatter(grid, SAND, count=self.rng.integers(3, 7), radius=2, in_stone=False)
185
+ self._scatter(grid, GRAVEL, count=self.rng.integers(2, 5), radius=1, in_stone=True)
186
+ self._pour_water(grid, surface_rows)
187
+
188
+ grid[0, :] = BEDROCK
189
+ grid[-1, :] = BEDROCK
190
+ grid[:, 0] = BEDROCK
191
+ grid[:, -1] = BEDROCK
192
+ # Keep the spawn column open so the miner never starts entombed.
193
+ spawn_column = int(self.rng.integers(3, width - 3))
194
+ spawn_row = int(surface_rows[spawn_column])
195
+ grid[max(1, spawn_row - 3) : spawn_row, spawn_column] = AIR
196
+ self.grid = grid
197
+ self._spawn_column = spawn_column
198
+ self._spawn_row = spawn_row
199
+
200
+ def _carve_caves(self, grid: np.ndarray, surface_rows: np.ndarray) -> None:
201
+ cave_count = int(self.rng.integers(3, 7))
202
+ for _ in range(cave_count):
203
+ column = float(self.rng.integers(2, self.grid_width - 2))
204
+ row = float(self.rng.integers(int(surface_rows.max()) + 3, self.grid_height - 3))
205
+ length = int(self.rng.integers(6, 18))
206
+ floor = int(surface_rows.max()) + 2
207
+ for _ in range(length):
208
+ radius = float(self.rng.uniform(1.0, 2.4))
209
+ self._stamp(grid, row, column, radius, AIR, in_stone=False)
210
+ angle = float(self.rng.uniform(0.0, np.pi * 2.0))
211
+ column = float(np.clip(column + np.cos(angle) * 1.6, 2, self.grid_width - 3))
212
+ row = float(np.clip(row + np.sin(angle) * 1.2, floor, self.grid_height - 3))
213
+
214
+ def _stamp(
215
+ self,
216
+ grid: np.ndarray,
217
+ row: float,
218
+ column: float,
219
+ radius: float,
220
+ tile: int,
221
+ in_stone: bool,
222
+ ) -> None:
223
+ top = max(1, int(row - radius))
224
+ bottom = min(self.grid_height - 1, int(row + radius) + 1)
225
+ left = max(1, int(column - radius))
226
+ right = min(self.grid_width - 1, int(column + radius) + 1)
227
+ if top >= bottom or left >= right:
228
+ return
229
+ rows = np.arange(top, bottom)[:, None]
230
+ columns = np.arange(left, right)[None, :]
231
+ inside = (rows - row) ** 2 + (columns - column) ** 2 <= radius**2
232
+ region = grid[top:bottom, left:right]
233
+ if in_stone:
234
+ inside &= region == STONE
235
+ region[inside] = tile
236
+
237
+ def _scatter(
238
+ self,
239
+ grid: np.ndarray,
240
+ tile: int,
241
+ count: int,
242
+ radius: int,
243
+ in_stone: bool,
244
+ ) -> None:
245
+ for _ in range(int(count)):
246
+ row = float(self.rng.integers(2, self.grid_height - 2))
247
+ column = float(self.rng.integers(2, self.grid_width - 2))
248
+ self._stamp(grid, row, column, float(radius), tile, in_stone)
249
+
250
+ def _pour_water(self, grid: np.ndarray, surface_rows: np.ndarray) -> None:
251
+ pool_count = int(self.rng.integers(1, 4))
252
+ for _ in range(pool_count):
253
+ column = int(self.rng.integers(2, self.grid_width - 2))
254
+ row = int(self.rng.integers(int(surface_rows.max()) + 2, self.grid_height - 3))
255
+ span = int(self.rng.integers(2, 5))
256
+ left = max(1, column - span)
257
+ right = min(self.grid_width - 1, column + span)
258
+ region = grid[row : row + 2, left:right]
259
+ region[region == AIR] = WATER
260
+
261
+ def _reset_round(self) -> None:
262
+ self._generate_world()
263
+ self._respawn_player()
264
+
265
+ def _respawn_player(self) -> None:
266
+ # The world is never regenerated on death: tunnels the miner dug stay dug,
267
+ # which is the persistence signal the model is meant to learn.
268
+ column = self._spawn_column
269
+ row = self._spawn_row - 2
270
+ for candidate in range(1, self.grid_height - 2):
271
+ if not TILE_SOLID[self.grid[candidate, column]] and not TILE_SOLID[
272
+ self.grid[candidate + 1, column]
273
+ ]:
274
+ row = candidate
275
+ break
276
+ else:
277
+ self.grid[row : row + 2, column] = AIR
278
+ self.position[0] = float(column * self.tile)
279
+ self.position[1] = float(row * self.tile)
280
+ self.velocity[:] = 0.0
281
+ self.facing = 1
282
+ self.dig_timer = 0.0
283
+ self.dig_target = ()
284
+ self.dig_flash = 0.0
285
+ self.drown_timer = 0.0
286
+ self._action = (0,) * self.ACTION_DIM
287
+ self._hold_frames = 0
288
+
289
+ def _tile_at(self, row: int, column: int) -> int:
290
+ if row < 0 or row >= self.grid_height or column < 0 or column >= self.grid_width:
291
+ return BEDROCK
292
+ return int(self.grid[row, column])
293
+
294
+ def _solid_at(self, row: int, column: int) -> bool:
295
+ return bool(TILE_SOLID[self._tile_at(row, column)])
296
+
297
+ def _box_overlaps_solid(self, x: float, y: float) -> bool:
298
+ left = int(np.floor(x / self.tile))
299
+ right = int(np.floor((x + self.player_width - 1e-6) / self.tile))
300
+ top = int(np.floor(y / self.tile))
301
+ bottom = int(np.floor((y + self.player_height - 1e-6) / self.tile))
302
+ for row in range(top, bottom + 1):
303
+ for column in range(left, right + 1):
304
+ if self._solid_at(row, column):
305
+ return True
306
+ return False
307
+
308
+ def _player_tiles(self) -> tuple[int, int, int]:
309
+ column = int((self.position[0] + self.player_width * 0.5) // self.tile)
310
+ head = int(self.position[1] // self.tile)
311
+ feet = int((self.position[1] + self.player_height - 1e-6) // self.tile)
312
+ return column, head, feet
313
+
314
+ def _footprint(self) -> tuple[int, int, int, int]:
315
+ left = int(self.position[0] // self.tile)
316
+ right = int((self.position[0] + self.player_width - 1e-6) // self.tile)
317
+ head = int(self.position[1] // self.tile)
318
+ feet = int((self.position[1] + self.player_height - 1e-6) // self.tile)
319
+ return left, right, head, feet
320
+
321
+ def _on_ground(self) -> bool:
322
+ return self._box_overlaps_solid(self.position[0], self.position[1] + 1.0)
323
+
324
+ def _in_water(self) -> bool:
325
+ column, head, feet = self._player_tiles()
326
+ return self._tile_at(head, column) == WATER or self._tile_at(feet, column) == WATER
327
+
328
+ def _step_physics(self, action: tuple[int, ...]) -> None:
329
+ left, right, jump = action[0], action[1], action[2]
330
+ submerged = self._in_water()
331
+ direction = float(right) - float(left)
332
+ if direction != 0.0:
333
+ self.facing = 1 if direction > 0 else -1
334
+ speed = self.move_speed * (0.6 if submerged else 1.0)
335
+ self.velocity[0] = direction * speed
336
+ grounded = self._on_ground()
337
+ if jump and (grounded or submerged):
338
+ self.velocity[1] = -self.jump_speed * (0.62 if submerged else 1.0)
339
+ gravity = self.gravity * (0.35 if submerged else 1.0)
340
+ self.velocity[1] = min(self.velocity[1] + gravity * self.dt, self.terminal_speed)
341
+ if submerged:
342
+ self.velocity[1] *= 1.0 - self.swim_drag * self.dt * self.video_config.fps * 0.1
343
+
344
+ step_x = float(self.velocity[0] * self.dt)
345
+ candidate = self.position[0] + step_x
346
+ if self._box_overlaps_solid(candidate, self.position[1]):
347
+ # Step up one tile so single-block ledges do not need a jump.
348
+ lifted = self.position[1] - self.tile
349
+ climbable = step_x != 0.0 and self._on_ground()
350
+ if climbable and not self._box_overlaps_solid(candidate, lifted):
351
+ self.position[0] = candidate
352
+ self.position[1] = lifted
353
+ else:
354
+ self.velocity[0] = 0.0
355
+ else:
356
+ self.position[0] = candidate
357
+
358
+ step_y = float(self.velocity[1] * self.dt)
359
+ candidate = self.position[1] + step_y
360
+ if self._box_overlaps_solid(self.position[0], candidate):
361
+ while abs(step_y) > 0.5:
362
+ step_y *= 0.5
363
+ probe = self.position[1] + step_y
364
+ if not self._box_overlaps_solid(self.position[0], probe):
365
+ self.position[1] = probe
366
+ self.velocity[1] = 0.0
367
+ else:
368
+ self.position[1] = candidate
369
+
370
+ self.position[0] = float(
371
+ np.clip(self.position[0], self.tile, self.video_config.width - self.tile * 2)
372
+ )
373
+ self.position[1] = float(
374
+ np.clip(self.position[1], self.tile, self.video_config.height - self.tile * 3)
375
+ )
376
+
377
+ def _dig(self, action: tuple[int, ...]) -> None:
378
+ self.dig_flash = max(0.0, self.dig_flash - self.dt)
379
+ self.dig_timer = max(0.0, self.dig_timer - self.dt)
380
+ dig_left, dig_right, dig_up, dig_down = action[3], action[4], action[5], action[6]
381
+ if not (dig_left or dig_right or dig_up or dig_down):
382
+ self.dig_target = ()
383
+ return
384
+ left, right, head, feet = self._footprint()
385
+ # A dig clears the player's whole cross-section, so the shaft it opens is
386
+ # always wide enough to move through.
387
+ if dig_left:
388
+ self.facing = -1
389
+ target = tuple((row, left - 1) for row in range(head, feet + 1))
390
+ elif dig_right:
391
+ self.facing = 1
392
+ target = tuple((row, right + 1) for row in range(head, feet + 1))
393
+ elif dig_up:
394
+ target = tuple((head - 1, column) for column in range(left, right + 1))
395
+ else:
396
+ target = tuple((feet + 1, column) for column in range(left, right + 1))
397
+ self.dig_target = target
398
+ if self.dig_timer > 0.0:
399
+ return
400
+ removed = False
401
+ for row, column in target:
402
+ tile = self._tile_at(row, column)
403
+ if not TILE_DIGGABLE[tile]:
404
+ continue
405
+ self.grid[row, column] = AIR
406
+ removed = True
407
+ if tile == ORE:
408
+ self.ore_collected += 1
409
+ self.pending_reward += 1.0
410
+ if removed:
411
+ self.dig_timer = self.dig_cooldown
412
+ self.dig_flash = self.dig_cooldown
413
+
414
+ def _step_tiles(self) -> None:
415
+ grid = self.grid
416
+ falls = TILE_FALLS[grid]
417
+ below = np.zeros_like(grid)
418
+ below[:-1] = grid[1:]
419
+ below[-1] = BEDROCK
420
+ open_below = (below == AIR) | (below == WATER)
421
+ moving = falls & open_below
422
+ moving[-1] = False
423
+ if moving.any():
424
+ destination = _shift_down(moving)
425
+ carried = grid[moving]
426
+ displaced = grid[destination]
427
+ grid[destination] = carried
428
+ grid[moving] = displaced
429
+
430
+ # Sand slides off peaks so it forms cones instead of towers.
431
+ side = 1 if self.frame_index % 2 == 0 else -1
432
+ sand = grid == SAND
433
+ below[:-1] = grid[1:]
434
+ below[-1] = BEDROCK
435
+ blocked = sand & ~((below == AIR) | (below == WATER))
436
+ diagonal = np.zeros_like(grid)
437
+ if side > 0:
438
+ diagonal[:-1, :-1] = grid[1:, 1:]
439
+ else:
440
+ diagonal[:-1, 1:] = grid[1:, :-1]
441
+ slide = blocked & (diagonal == AIR)
442
+ slide[-1] = False
443
+ if slide.any():
444
+ destination = _shift_down(_shift_horizontal(slide, side))
445
+ grid[destination] = grid[slide]
446
+ grid[slide] = AIR
447
+
448
+ water = grid == WATER
449
+ below[:-1] = grid[1:]
450
+ below[-1] = BEDROCK
451
+ settled = water & (below != AIR)
452
+ neighbour = np.zeros_like(grid)
453
+ if side > 0:
454
+ neighbour[:, :-1] = grid[:, 1:]
455
+ else:
456
+ neighbour[:, 1:] = grid[:, :-1]
457
+ spread = settled & (neighbour == AIR)
458
+ if spread.any():
459
+ destination = _shift_horizontal(spread, side)
460
+ grid[destination] = WATER
461
+ grid[spread] = AIR
462
+
463
+ def _check_fatal(self) -> None:
464
+ buried = False
465
+ if self._box_overlaps_solid(self.position[0], self.position[1]):
466
+ # Material settling onto the miner shoves it upward; only a fully
467
+ # sealed pocket actually kills.
468
+ buried = True
469
+ for offset in range(1, self.tile * 2 + 1):
470
+ lifted = self.position[1] - offset
471
+ if lifted < self.tile:
472
+ break
473
+ if not self._box_overlaps_solid(self.position[0], lifted):
474
+ self.position[1] = float(lifted)
475
+ self.velocity[1] = 0.0
476
+ buried = False
477
+ break
478
+ column, head, _ = self._player_tiles()
479
+ if self._tile_at(head, column) == WATER:
480
+ self.drown_timer += self.dt
481
+ else:
482
+ self.drown_timer = max(0.0, self.drown_timer - self.dt * 0.5)
483
+ if buried or self.drown_timer > 2.5:
484
+ self.deaths += 1
485
+ self.pending_terminal = 1.0
486
+ self.drown_timer = 0.0
487
+ self._respawn_player()
488
+
489
+ def scripted_action(self) -> tuple[int, ...]:
490
+ """Next training-policy action. Does not step the world."""
491
+ return self._scripted_action()
492
+
493
+ def _scripted_action(self) -> tuple[int, ...]:
494
+ column, _, feet = self._player_tiles()
495
+ cell = (feet, column)
496
+ if cell == self._last_cell:
497
+ self._stuck_frames += 1
498
+ else:
499
+ self._stuck_frames = 0
500
+ self._last_cell = cell
501
+ if self._hold_frames > 0:
502
+ self._hold_frames -= 1
503
+ return self._action
504
+ if self._stuck_frames > 18:
505
+ # Break out of dead ends instead of grinding at an unreachable target.
506
+ action = [0] * self.ACTION_DIM
507
+ action[int(self.rng.integers(0, self.ACTION_DIM))] = 1
508
+ action[2] = 1
509
+ self._action = tuple(action)
510
+ self._hold_frames = 4
511
+ self._stuck_frames = 0
512
+ return self._action
513
+ action = self._plan_action()
514
+ self._action = action
515
+ self._hold_frames = int(self.rng.integers(0, 3))
516
+ return action
517
+
518
+ def _plan_action(self) -> tuple[int, ...]:
519
+ if self.rng.random() < 0.06:
520
+ choice = int(self.rng.integers(0, self.ACTION_DIM))
521
+ action = [0] * self.ACTION_DIM
522
+ action[choice] = 1
523
+ return tuple(action)
524
+
525
+ column, head, feet = self._player_tiles()
526
+ action = [0] * self.ACTION_DIM
527
+
528
+ # Escape falling material before anything else.
529
+ if self._tile_at(head - 1, column) in {SAND, GRAVEL}:
530
+ action[1 if self.facing > 0 else 0] = 1
531
+ action[2] = 1
532
+ return tuple(action)
533
+ if self.drown_timer > 0.6:
534
+ action[2] = 1
535
+ action[5] = 1
536
+ return tuple(action)
537
+
538
+ ore = np.argwhere(self.grid == ORE)
539
+ if ore.size == 0:
540
+ action[6] = 1
541
+ return tuple(action)
542
+ distances = np.abs(ore[:, 0] - feet) + np.abs(ore[:, 1] - column)
543
+ target_row, target_column = ore[int(np.argmin(distances))]
544
+ delta_column = int(target_column) - column
545
+ delta_row = int(target_row) - feet
546
+
547
+ if abs(delta_column) <= 1 and abs(delta_row) <= 1:
548
+ if delta_column < 0:
549
+ action[3] = 1
550
+ elif delta_column > 0:
551
+ action[4] = 1
552
+ elif delta_row < 0:
553
+ action[5] = 1
554
+ else:
555
+ action[6] = 1
556
+ return tuple(action)
557
+
558
+ if delta_column != 0:
559
+ forward = 1 if delta_column > 0 else 0
560
+ action[forward] = 1
561
+ ahead = column + (1 if delta_column > 0 else -1)
562
+ if self._solid_at(feet, ahead):
563
+ # Bedrock never yields, so climb over it instead of digging forever.
564
+ if TILE_DIGGABLE[self._tile_at(feet, ahead)]:
565
+ action[4 if delta_column > 0 else 3] = 1
566
+ else:
567
+ action[2] = 1
568
+ elif self._solid_at(head, ahead):
569
+ action[2] = 1
570
+ return tuple(action)
571
+
572
+ if delta_row > 0:
573
+ action[6] = 1
574
+ else:
575
+ action[2] = 1
576
+ action[5] = 1
577
+ return tuple(action)
578
+
579
+ def step(self, action: tuple[int, ...] | None = None) -> None:
580
+ resolved = self._scripted_action() if action is None else tuple(action)
581
+ if len(resolved) != self.ACTION_DIM:
582
+ raise ValueError(f"Mining actions must have {self.ACTION_DIM} entries.")
583
+ self.pending_reward = 0.0
584
+ self.pending_terminal = 0.0
585
+ self._dig(resolved)
586
+ self._step_physics(resolved)
587
+ self._step_tiles()
588
+ self._check_fatal()
589
+ self.frame_index += 1
590
+
591
+ def render(self) -> np.ndarray:
592
+ colors = TILE_COLORS[self.grid] + self.shade[:, :, None]
593
+ if self.dig_flash > 0.0:
594
+ for row, column in self.dig_target:
595
+ if 0 <= row < self.grid_height and 0 <= column < self.grid_width:
596
+ colors[row, column] = _DIG_FLASH_COLOR
597
+ frame = np.clip(colors, 0, 255).astype(np.uint8)
598
+ frame = frame.repeat(self.tile, axis=0).repeat(self.tile, axis=1)
599
+
600
+ paint_player(
601
+ frame,
602
+ round(float(self.position[0])),
603
+ round(float(self.position[1])),
604
+ int(self.player_width),
605
+ int(self.player_height),
606
+ self.facing,
607
+ )
608
+
609
+ # Ore pips give a fontless score readout that stays stable at training size.
610
+ for index in range(min(self.ore_collected, 8)):
611
+ x = self.tile + index * (self.tile + 2)
612
+ pip = max(2, self.tile // 2)
613
+ frame[2 : 2 + pip, x : x + pip] = TILE_COLORS[ORE]
614
+ return frame
615
+
616
+ def next_frame(self) -> np.ndarray:
617
+ frame = self.render()
618
+ self.step()
619
+ return frame
620
+
621
+ def sample_frames(self, count: int) -> np.ndarray:
622
+ return np.stack([self.next_frame() for _ in range(count)])
623
+
624
+ def sample_gameplay(
625
+ self,
626
+ count: int,
627
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
628
+ frames: list[np.ndarray] = []
629
+ actions: list[tuple[int, ...]] = []
630
+ rewards: list[float] = []
631
+ terminals: list[float] = []
632
+ for _ in range(count):
633
+ frames.append(self.render())
634
+ action = self.scripted_action()
635
+ actions.append(action)
636
+ self.step(action)
637
+ rewards.append(self.pending_reward)
638
+ terminals.append(self.pending_terminal)
639
+ return (
640
+ np.stack(frames),
641
+ np.asarray(actions, dtype=np.float32),
642
+ np.asarray(rewards, dtype=np.float32),
643
+ np.asarray(terminals, dtype=np.float32),
644
+ )
livediffusion/model.py ADDED
@@ -0,0 +1,920 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import math
4
+ from dataclasses import dataclass
5
+
6
+ import torch
7
+ import torch.nn.functional as F
8
+ from torch import nn
9
+ from torch.utils.checkpoint import checkpoint
10
+
11
+ from livediffusion.config import ModelConfig
12
+
13
+
14
+ @dataclass(slots=True)
15
+ class PrefixCache:
16
+ key_values: tuple[tuple[torch.Tensor, torch.Tensor], ...]
17
+ pooled: torch.Tensor
18
+
19
+
20
+ MemoryCache = PrefixCache
21
+
22
+
23
+ def _video_positions(
24
+ frames: int,
25
+ height: int,
26
+ width: int,
27
+ time_offset: int,
28
+ device: torch.device,
29
+ ) -> torch.Tensor:
30
+ time = torch.arange(time_offset, time_offset + frames, device=device)
31
+ y = torch.arange(height, device=device)
32
+ x = torch.arange(width, device=device)
33
+ grid = torch.meshgrid(time, y, x, indexing="ij")
34
+ return torch.stack(grid, dim=-1).reshape(-1, 3).float()
35
+
36
+
37
+ class Rotary3D(nn.Module):
38
+ """3D RoPE with a shorter temporal base so time and space do not share a spectrum."""
39
+
40
+ def __init__(
41
+ self,
42
+ head_size: int,
43
+ time_base: float = 1_000.0,
44
+ space_base: float = 10_000.0,
45
+ ) -> None:
46
+ super().__init__()
47
+ self.rotary_size = (head_size // 6) * 6
48
+ self.axis_size = self.rotary_size // 3
49
+ axis_half = self.axis_size // 2
50
+ steps = torch.arange(axis_half, dtype=torch.float32) / max(axis_half, 1)
51
+ time_frequencies = torch.exp(-math.log(time_base) * steps)
52
+ space_frequencies = torch.exp(-math.log(space_base) * steps)
53
+ self.register_buffer(
54
+ "frequencies",
55
+ torch.stack((time_frequencies, space_frequencies, space_frequencies)),
56
+ persistent=False,
57
+ )
58
+
59
+ @staticmethod
60
+ def _rotate_half(inputs: torch.Tensor) -> torch.Tensor:
61
+ pairs = inputs.reshape(*inputs.shape[:-1], -1, 2)
62
+ first, second = pairs.unbind(dim=-1)
63
+ return torch.stack((-second, first), dim=-1).flatten(-2)
64
+
65
+ def forward(self, inputs: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
66
+ angles = torch.cat(
67
+ [
68
+ positions[:, axis, None] * self.frequencies[axis][None]
69
+ for axis in range(3)
70
+ ],
71
+ dim=-1,
72
+ )
73
+ cosine = angles.cos().repeat_interleave(2, dim=-1)[None, None]
74
+ sine = angles.sin().repeat_interleave(2, dim=-1)[None, None]
75
+ rotary, remainder = inputs.split(
76
+ (self.rotary_size, inputs.shape[-1] - self.rotary_size),
77
+ dim=-1,
78
+ )
79
+ rotated = rotary * cosine.to(rotary.dtype)
80
+ rotated = rotated + self._rotate_half(rotary) * sine.to(rotary.dtype)
81
+ return torch.cat((rotated, remainder), dim=-1)
82
+
83
+
84
+ class ScalarEmbedding(nn.Module):
85
+ def __init__(self, hidden_size: int) -> None:
86
+ super().__init__()
87
+ self.hidden_size = hidden_size
88
+ self.mlp = nn.Sequential(
89
+ nn.Linear(hidden_size, hidden_size),
90
+ nn.SiLU(),
91
+ nn.Linear(hidden_size, hidden_size),
92
+ )
93
+
94
+ def forward(self, value: torch.Tensor) -> torch.Tensor:
95
+ value = value.reshape(-1).float()
96
+ half = self.hidden_size // 2
97
+ frequencies = torch.exp(
98
+ -math.log(10_000.0)
99
+ * torch.arange(half, device=value.device, dtype=torch.float32)
100
+ / half
101
+ )
102
+ angles = value[:, None] * frequencies[None] * 1_000.0
103
+ embedding = torch.cat((angles.cos(), angles.sin()), dim=-1)
104
+ return self.mlp(embedding.to(dtype=self.mlp[0].weight.dtype))
105
+
106
+
107
+ class SwiGLU(nn.Module):
108
+ def __init__(self, hidden_size: int, ratio: float) -> None:
109
+ super().__init__()
110
+ inner_size = int(hidden_size * ratio * 2 / 3)
111
+ inner_size = ((inner_size + 63) // 64) * 64
112
+ self.input = nn.Linear(hidden_size, inner_size * 2, bias=False)
113
+ self.output = nn.Linear(inner_size, hidden_size, bias=False)
114
+
115
+ def forward(self, inputs: torch.Tensor) -> torch.Tensor:
116
+ values, gates = self.input(inputs).chunk(2, dim=-1)
117
+ return self.output(values * F.silu(gates))
118
+
119
+
120
+ class SelfAttention(nn.Module):
121
+ def __init__(self, hidden_size: int, num_heads: int, qk_norm: bool) -> None:
122
+ super().__init__()
123
+ self.num_heads = num_heads
124
+ self.head_size = hidden_size // num_heads
125
+ self.qkv = nn.Linear(hidden_size, hidden_size * 3, bias=False)
126
+ self.output = nn.Linear(hidden_size, hidden_size, bias=False)
127
+ self.q_norm = (
128
+ nn.RMSNorm(self.head_size, eps=1e-6)
129
+ if qk_norm
130
+ else nn.Identity()
131
+ )
132
+ self.k_norm = (
133
+ nn.RMSNorm(self.head_size, eps=1e-6)
134
+ if qk_norm
135
+ else nn.Identity()
136
+ )
137
+ self.rotary = Rotary3D(self.head_size)
138
+
139
+ def forward(self, inputs: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
140
+ batch, tokens, channels = inputs.shape
141
+ query, key, value = self.qkv(inputs).reshape(
142
+ batch,
143
+ tokens,
144
+ 3,
145
+ self.num_heads,
146
+ self.head_size,
147
+ ).unbind(dim=2)
148
+ query = self.rotary(self.q_norm(query).transpose(1, 2), positions)
149
+ key = self.rotary(self.k_norm(key).transpose(1, 2), positions)
150
+ value = value.transpose(1, 2)
151
+ attended = F.scaled_dot_product_attention(query, key, value)
152
+ return self.output(attended.transpose(1, 2).reshape(batch, tokens, channels))
153
+
154
+
155
+ class PrefixAttention(nn.Module):
156
+ """Target attention over the joint clean-prefix and noisy-target sequence.
157
+
158
+ Keys and values may use fewer heads than queries. Prefix and target keys are
159
+ concatenated, so both sides share the same reduced head count.
160
+ """
161
+
162
+ def __init__(
163
+ self,
164
+ hidden_size: int,
165
+ num_heads: int,
166
+ qk_norm: bool,
167
+ kv_heads: int = 0,
168
+ ) -> None:
169
+ super().__init__()
170
+ self.num_heads = num_heads
171
+ self.head_size = hidden_size // num_heads
172
+ self.kv_heads = kv_heads or num_heads
173
+ kv_size = self.kv_heads * self.head_size
174
+ self.target_qkv = nn.Linear(hidden_size, hidden_size + kv_size * 2, bias=False)
175
+ self.prefix_key_value = nn.Linear(hidden_size, kv_size * 2, bias=False)
176
+ self.output = nn.Linear(hidden_size, hidden_size, bias=False)
177
+ self.q_norm = (
178
+ nn.RMSNorm(self.head_size, eps=1e-6)
179
+ if qk_norm
180
+ else nn.Identity()
181
+ )
182
+ self.k_norm = (
183
+ nn.RMSNorm(self.head_size, eps=1e-6)
184
+ if qk_norm
185
+ else nn.Identity()
186
+ )
187
+ self.rotary = Rotary3D(self.head_size)
188
+
189
+ def project_prefix(
190
+ self,
191
+ prefix: torch.Tensor,
192
+ positions: torch.Tensor,
193
+ ) -> tuple[torch.Tensor, torch.Tensor]:
194
+ batch, tokens, _ = prefix.shape
195
+ key, value = self.prefix_key_value(prefix).reshape(
196
+ batch,
197
+ tokens,
198
+ 2,
199
+ self.kv_heads,
200
+ self.head_size,
201
+ ).unbind(dim=2)
202
+ key = self.rotary(self.k_norm(key).transpose(1, 2), positions)
203
+ return key, value.transpose(1, 2)
204
+
205
+ def forward_cached(
206
+ self,
207
+ target: torch.Tensor,
208
+ target_positions: torch.Tensor,
209
+ prefix_key_value: tuple[torch.Tensor, torch.Tensor],
210
+ ) -> torch.Tensor:
211
+ batch, tokens, channels = target.shape
212
+ kv_size = self.kv_heads * self.head_size
213
+ projected = self.target_qkv(target)
214
+ query, key, value = projected.split((channels, kv_size, kv_size), dim=-1)
215
+ query = query.reshape(batch, tokens, self.num_heads, self.head_size)
216
+ key = key.reshape(batch, tokens, self.kv_heads, self.head_size)
217
+ value = value.reshape(batch, tokens, self.kv_heads, self.head_size)
218
+ query = self.rotary(self.q_norm(query).transpose(1, 2), target_positions)
219
+ key = self.rotary(self.k_norm(key).transpose(1, 2), target_positions)
220
+ value = value.transpose(1, 2)
221
+ prefix_key, prefix_value = prefix_key_value
222
+ key = torch.cat((prefix_key, key), dim=2)
223
+ value = torch.cat((prefix_value, value), dim=2)
224
+ attended = F.scaled_dot_product_attention(
225
+ query,
226
+ key,
227
+ value,
228
+ enable_gqa=self.kv_heads != self.num_heads,
229
+ )
230
+ return self.output(attended.transpose(1, 2).reshape(batch, tokens, channels))
231
+
232
+
233
+ @dataclass(slots=True)
234
+ class WorldMemoryState:
235
+ plane: torch.Tensor
236
+ registers: torch.Tensor
237
+
238
+
239
+ class WorldMemory(nn.Module):
240
+ """Gated recurrent memory at latent resolution, plus global register state.
241
+
242
+ The plane is a convolutional GRU over one cell per latent position, so an
243
+ irreversible world edit stays represented after it scrolls out of the
244
+ attention prefix. Registers carry frame-global state the plane cannot hold.
245
+ """
246
+
247
+ def __init__(
248
+ self,
249
+ latent_channels: int,
250
+ hidden_size: int,
251
+ memory_channels: int,
252
+ read_patch: int,
253
+ register_tokens: int,
254
+ ) -> None:
255
+ super().__init__()
256
+ self.memory_channels = memory_channels
257
+ self.read_patch = read_patch
258
+ self.register_tokens = register_tokens
259
+ self.cell = nn.Conv2d(
260
+ latent_channels + memory_channels,
261
+ memory_channels * 3,
262
+ kernel_size=3,
263
+ padding=1,
264
+ )
265
+ self.read = nn.Conv2d(
266
+ memory_channels,
267
+ hidden_size,
268
+ kernel_size=read_patch,
269
+ stride=read_patch,
270
+ )
271
+ self.read_norm = nn.RMSNorm(hidden_size, eps=1e-6)
272
+ self.scale_embedding = nn.Parameter(torch.zeros(1, 1, hidden_size))
273
+ self.register_update = (
274
+ nn.Linear(memory_channels + hidden_size, hidden_size * 2)
275
+ if register_tokens > 0
276
+ else None
277
+ )
278
+
279
+ def initial_state(
280
+ self,
281
+ batch: int,
282
+ height: int,
283
+ width: int,
284
+ registers: torch.Tensor,
285
+ device: torch.device,
286
+ dtype: torch.dtype,
287
+ ) -> WorldMemoryState:
288
+ plane = torch.zeros(
289
+ batch,
290
+ self.memory_channels,
291
+ height,
292
+ width,
293
+ device=device,
294
+ dtype=dtype,
295
+ )
296
+ return WorldMemoryState(plane, registers)
297
+
298
+ def advance(
299
+ self,
300
+ state: WorldMemoryState,
301
+ frames: torch.Tensor,
302
+ ) -> WorldMemoryState:
303
+ plane = state.plane
304
+ for index in range(frames.shape[1]):
305
+ latent = frames[:, index].to(plane.dtype)
306
+ reset, update, candidate = self.cell(
307
+ torch.cat((latent, plane), dim=1)
308
+ ).chunk(3, dim=1)
309
+ gate = torch.sigmoid(update)
310
+ proposal = torch.tanh(candidate + torch.sigmoid(reset) * plane)
311
+ plane = plane * (1.0 - gate) + proposal * gate
312
+ registers = state.registers
313
+ if self.register_update is not None:
314
+ pooled = plane.mean(dim=(2, 3))
315
+ summary = torch.cat(
316
+ (pooled[:, None].expand(-1, registers.shape[1], -1), registers),
317
+ dim=-1,
318
+ )
319
+ gate, proposal = self.register_update(summary).chunk(2, dim=-1)
320
+ gate = torch.sigmoid(gate)
321
+ registers = registers * (1.0 - gate) + torch.tanh(proposal) * gate
322
+ return WorldMemoryState(plane, registers)
323
+
324
+ def tokens(self, state: WorldMemoryState) -> torch.Tensor:
325
+ read = self.read(state.plane).flatten(2).transpose(1, 2)
326
+ return self.read_norm(read) + self.scale_embedding
327
+
328
+
329
+ class SharedModulation(nn.Module):
330
+ """One adaLN projection shared by every block, with per-block offsets.
331
+
332
+ Per-block modulation costs 6 * hidden^2 each; sharing recovers that budget
333
+ for depth while keeping each block independently steerable.
334
+ """
335
+
336
+ def __init__(self, hidden_size: int, blocks: int) -> None:
337
+ super().__init__()
338
+ self.projection = nn.Linear(hidden_size, hidden_size * 6)
339
+ self.offsets = nn.Parameter(torch.zeros(blocks, hidden_size * 6))
340
+
341
+ def forward(self, condition: torch.Tensor, block_index: int) -> torch.Tensor:
342
+ return self.projection(F.silu(condition)) + self.offsets[block_index]
343
+
344
+
345
+ class PrefixEncoderBlock(nn.Module):
346
+ """Clean-context stack that runs before the joint prefix/target DiT."""
347
+
348
+ def __init__(self, config: ModelConfig) -> None:
349
+ super().__init__()
350
+ hidden_size = config.hidden_size
351
+ self.norm1 = nn.RMSNorm(hidden_size, eps=1e-6)
352
+ self.attention = SelfAttention(
353
+ hidden_size,
354
+ config.num_heads,
355
+ config.qk_norm,
356
+ )
357
+ self.norm2 = nn.RMSNorm(hidden_size, eps=1e-6)
358
+ self.mlp = SwiGLU(hidden_size, config.mlp_ratio)
359
+ self.attn_scale = nn.Parameter(torch.ones(hidden_size))
360
+ self.mlp_scale = nn.Parameter(torch.ones(hidden_size))
361
+
362
+ def forward(self, prefix: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
363
+ prefix = prefix + self.attn_scale * self.attention(self.norm1(prefix), positions)
364
+ return prefix + self.mlp_scale * self.mlp(self.norm2(prefix))
365
+
366
+
367
+ class PrefixDiTBlock(nn.Module):
368
+ def __init__(self, config: ModelConfig) -> None:
369
+ super().__init__()
370
+ hidden_size = config.hidden_size
371
+ self.prefix_norm1 = nn.RMSNorm(hidden_size, eps=1e-6)
372
+ self.prefix_attention = SelfAttention(
373
+ hidden_size,
374
+ config.num_heads,
375
+ config.qk_norm,
376
+ )
377
+ self.prefix_norm2 = nn.RMSNorm(hidden_size, eps=1e-6)
378
+ self.prefix_mlp = SwiGLU(hidden_size, config.mlp_ratio)
379
+ self.prefix_projection_norm = nn.RMSNorm(hidden_size, eps=1e-6)
380
+ self.target_norm1 = nn.RMSNorm(
381
+ hidden_size,
382
+ eps=1e-6,
383
+ elementwise_affine=False,
384
+ )
385
+ self.target_attention = PrefixAttention(
386
+ hidden_size,
387
+ config.num_heads,
388
+ config.qk_norm,
389
+ config.kv_heads,
390
+ )
391
+ self.target_norm2 = nn.RMSNorm(
392
+ hidden_size,
393
+ eps=1e-6,
394
+ elementwise_affine=False,
395
+ )
396
+ self.target_mlp = SwiGLU(hidden_size, config.mlp_ratio)
397
+ self.modulation = (
398
+ None
399
+ if config.shared_modulation
400
+ else nn.Linear(hidden_size, hidden_size * 6)
401
+ )
402
+
403
+ @staticmethod
404
+ def _modulate(
405
+ inputs: torch.Tensor,
406
+ shift: torch.Tensor,
407
+ scale: torch.Tensor,
408
+ ) -> torch.Tensor:
409
+ return inputs * (1.0 + scale[:, None]) + shift[:, None]
410
+
411
+ def advance_prefix(
412
+ self,
413
+ prefix: torch.Tensor,
414
+ positions: torch.Tensor,
415
+ ) -> torch.Tensor:
416
+ prefix = prefix + self.prefix_attention(self.prefix_norm1(prefix), positions)
417
+ return prefix + self.prefix_mlp(self.prefix_norm2(prefix))
418
+
419
+ def prefix_key_value(
420
+ self,
421
+ prefix: torch.Tensor,
422
+ positions: torch.Tensor,
423
+ ) -> tuple[torch.Tensor, torch.Tensor]:
424
+ return self.target_attention.project_prefix(
425
+ self.prefix_projection_norm(prefix),
426
+ positions,
427
+ )
428
+
429
+ def advance_target(
430
+ self,
431
+ target: torch.Tensor,
432
+ condition: torch.Tensor,
433
+ target_positions: torch.Tensor,
434
+ prefix_key_value: tuple[torch.Tensor, torch.Tensor],
435
+ modulation: torch.Tensor | None = None,
436
+ ) -> torch.Tensor:
437
+ if modulation is None:
438
+ if self.modulation is None:
439
+ raise RuntimeError("Shared modulation must be supplied by the trunk.")
440
+ modulation = self.modulation(F.silu(condition))
441
+ shift1, scale1, gate1, shift2, scale2, gate2 = modulation.chunk(6, dim=-1)
442
+ normalized = self._modulate(self.target_norm1(target), shift1, scale1)
443
+ target = target + gate1[:, None] * self.target_attention.forward_cached(
444
+ normalized,
445
+ target_positions,
446
+ prefix_key_value,
447
+ )
448
+ normalized = self._modulate(self.target_norm2(target), shift2, scale2)
449
+ return target + gate2[:, None] * self.target_mlp(normalized)
450
+
451
+ def forward(
452
+ self,
453
+ prefix: torch.Tensor,
454
+ target: torch.Tensor,
455
+ condition: torch.Tensor,
456
+ prefix_positions: torch.Tensor,
457
+ target_positions: torch.Tensor,
458
+ modulation: torch.Tensor | None = None,
459
+ ) -> tuple[torch.Tensor, torch.Tensor]:
460
+ prefix = self.advance_prefix(prefix, prefix_positions)
461
+ key_value = self.prefix_key_value(prefix, prefix_positions)
462
+ target = self.advance_target(
463
+ target,
464
+ condition,
465
+ target_positions,
466
+ key_value,
467
+ modulation,
468
+ )
469
+ return prefix, target
470
+
471
+
472
+ class CausalLatentVideoDiT(nn.Module):
473
+ """Multiscale clean-prefix/noisy-suffix latent video transformer."""
474
+
475
+ def __init__(
476
+ self,
477
+ model_config: ModelConfig,
478
+ latent_channels: int,
479
+ history_frames: int = 12,
480
+ ) -> None:
481
+ super().__init__()
482
+ self.model_config = model_config
483
+ self.latent_channels = latent_channels
484
+ self.history_frames = history_frames
485
+ self.gradient_checkpointing = False
486
+ hidden_size = model_config.hidden_size
487
+ target_patch = model_config.target_patch_size
488
+ coarse_patch = model_config.memory_patch_size
489
+ fine_patch = model_config.fine_memory_patch_size
490
+ self.target_patch_embed = nn.Conv3d(
491
+ latent_channels,
492
+ hidden_size,
493
+ kernel_size=(1, target_patch, target_patch),
494
+ stride=(1, target_patch, target_patch),
495
+ )
496
+ self.coarse_prefix_embed = nn.Conv3d(
497
+ latent_channels,
498
+ hidden_size,
499
+ kernel_size=(1, coarse_patch, coarse_patch),
500
+ stride=(1, coarse_patch, coarse_patch),
501
+ )
502
+ self.fine_prefix_embed = nn.Conv3d(
503
+ latent_channels,
504
+ hidden_size,
505
+ kernel_size=(1, fine_patch, fine_patch),
506
+ stride=(1, fine_patch, fine_patch),
507
+ )
508
+ self.motion_prefix_embed = nn.Conv3d(
509
+ latent_channels,
510
+ hidden_size,
511
+ kernel_size=(1, fine_patch, fine_patch),
512
+ stride=(1, fine_patch, fine_patch),
513
+ )
514
+ self.coarse_scale_embedding = nn.Parameter(torch.zeros(1, 1, hidden_size))
515
+ self.fine_scale_embedding = nn.Parameter(torch.zeros(1, 1, hidden_size))
516
+ self.register_tokens = (
517
+ nn.Parameter(torch.randn(1, model_config.register_tokens, hidden_size) * 0.02)
518
+ if model_config.register_tokens > 0
519
+ else None
520
+ )
521
+ self.memory_blocks = nn.ModuleList(
522
+ PrefixEncoderBlock(model_config)
523
+ for _ in range(model_config.prefix_encoder_depth)
524
+ )
525
+ self.t_embedding = ScalarEmbedding(hidden_size)
526
+ self.r_embedding = ScalarEmbedding(hidden_size)
527
+ self.delta_embedding = ScalarEmbedding(hidden_size)
528
+ self.time_projection = nn.Sequential(
529
+ nn.Linear(hidden_size * 3, hidden_size),
530
+ nn.SiLU(),
531
+ nn.Linear(hidden_size, hidden_size),
532
+ )
533
+ self.prefix_condition = nn.Linear(hidden_size, hidden_size)
534
+ self.action_projection = (
535
+ nn.Sequential(
536
+ nn.Linear(model_config.action_dim, hidden_size),
537
+ nn.SiLU(),
538
+ nn.Linear(hidden_size, hidden_size),
539
+ )
540
+ if model_config.action_dim > 0
541
+ else None
542
+ )
543
+ self.context_projection = (
544
+ nn.Linear(model_config.context_dim, hidden_size, bias=False)
545
+ if model_config.context_dim > 0
546
+ else None
547
+ )
548
+ self.blocks = nn.ModuleList(
549
+ PrefixDiTBlock(model_config) for _ in range(model_config.depth)
550
+ )
551
+ self.shared_modulation = (
552
+ SharedModulation(hidden_size, model_config.depth)
553
+ if model_config.shared_modulation
554
+ else None
555
+ )
556
+ self.world_memory = (
557
+ WorldMemory(
558
+ latent_channels,
559
+ hidden_size,
560
+ model_config.memory_channels,
561
+ coarse_patch,
562
+ model_config.register_tokens,
563
+ )
564
+ if model_config.memory_channels > 0
565
+ else None
566
+ )
567
+ self.output_norm = nn.RMSNorm(
568
+ hidden_size,
569
+ eps=1e-6,
570
+ elementwise_affine=False,
571
+ )
572
+ self.output_modulation = nn.Linear(hidden_size, hidden_size * 2)
573
+ self.output_projection = nn.Linear(
574
+ hidden_size,
575
+ target_patch * target_patch * latent_channels,
576
+ )
577
+ self.outcome_norm = nn.RMSNorm(hidden_size, eps=1e-6)
578
+ self.reward_head = nn.Linear(hidden_size, 1)
579
+ self.terminal_head = nn.Linear(hidden_size, 1)
580
+ self.reset_parameters()
581
+
582
+ def enable_activation_checkpointing(self, enabled: bool = True) -> None:
583
+ self.gradient_checkpointing = enabled
584
+
585
+ def reset_parameters(self) -> None:
586
+ for module in self.modules():
587
+ if isinstance(module, nn.Linear):
588
+ nn.init.xavier_uniform_(module.weight)
589
+ if module.bias is not None:
590
+ nn.init.zeros_(module.bias)
591
+ for embedding in (
592
+ self.target_patch_embed,
593
+ self.coarse_prefix_embed,
594
+ self.fine_prefix_embed,
595
+ self.motion_prefix_embed,
596
+ ):
597
+ nn.init.normal_(embedding.weight, std=0.02)
598
+ nn.init.normal_(self.coarse_scale_embedding, std=0.02)
599
+ nn.init.normal_(self.fine_scale_embedding, std=0.02)
600
+ for block in self.blocks:
601
+ if block.modulation is not None:
602
+ nn.init.zeros_(block.modulation.weight)
603
+ nn.init.zeros_(block.modulation.bias)
604
+ if self.shared_modulation is not None:
605
+ nn.init.zeros_(self.shared_modulation.projection.weight)
606
+ nn.init.zeros_(self.shared_modulation.projection.bias)
607
+ nn.init.zeros_(self.shared_modulation.offsets)
608
+ if self.world_memory is not None:
609
+ nn.init.zeros_(self.world_memory.scale_embedding)
610
+ nn.init.zeros_(self.output_modulation.weight)
611
+ nn.init.zeros_(self.output_modulation.bias)
612
+ nn.init.zeros_(self.output_projection.weight)
613
+ nn.init.zeros_(self.output_projection.bias)
614
+ nn.init.zeros_(self.reward_head.weight)
615
+ nn.init.zeros_(self.reward_head.bias)
616
+ nn.init.zeros_(self.terminal_head.weight)
617
+ nn.init.zeros_(self.terminal_head.bias)
618
+
619
+ @staticmethod
620
+ def _patch(
621
+ embedding: nn.Conv3d,
622
+ video: torch.Tensor,
623
+ time_offset: int,
624
+ ) -> tuple[torch.Tensor, torch.Tensor]:
625
+ tokens = embedding(video.transpose(1, 2))
626
+ _, _, frames, height, width = tokens.shape
627
+ positions = _video_positions(
628
+ frames,
629
+ height,
630
+ width,
631
+ time_offset,
632
+ video.device,
633
+ )
634
+ return tokens.flatten(2).transpose(1, 2), positions
635
+
636
+ def _encode_prefix(
637
+ self,
638
+ history: torch.Tensor,
639
+ context: torch.Tensor | None = None,
640
+ history_actions: torch.Tensor | None = None,
641
+ ) -> tuple[torch.Tensor, torch.Tensor]:
642
+ if history.shape[1] < self.history_frames:
643
+ raise ValueError(
644
+ f"Expected at least {self.history_frames} history frames, "
645
+ f"got {history.shape[1]}."
646
+ )
647
+ batch = history.shape[0]
648
+ registers = (
649
+ self.register_tokens.expand(batch, -1, -1)
650
+ if self.register_tokens is not None
651
+ else None
652
+ )
653
+ memory_tokens: torch.Tensor | None = None
654
+ if self.world_memory is not None:
655
+ # Every observed frame is folded into the recurrent state, including
656
+ # frames older than the attention window.
657
+ state = self.world_memory.initial_state(
658
+ batch,
659
+ history.shape[-2],
660
+ history.shape[-1],
661
+ registers
662
+ if registers is not None
663
+ else history.new_zeros(batch, 0, self.model_config.hidden_size),
664
+ history.device,
665
+ history.dtype,
666
+ )
667
+ state = self.world_memory.advance(state, history)
668
+ memory_tokens = self.world_memory.tokens(state)
669
+ if registers is not None:
670
+ registers = state.registers
671
+ window = history[:, -self.history_frames :]
672
+ coarse, coarse_positions = self._patch(
673
+ self.coarse_prefix_embed,
674
+ window,
675
+ 0,
676
+ )
677
+ if self.action_projection is not None and history_actions is not None:
678
+ window_actions = history_actions[:, -self.history_frames :]
679
+ tokens_per_frame = coarse.shape[1] // self.history_frames
680
+ coarse = coarse + self.action_projection(
681
+ window_actions
682
+ ).repeat_interleave(tokens_per_frame, dim=1)
683
+ history = window
684
+ fine_frames = self.model_config.fine_history_frames
685
+ fine_history = history[:, -fine_frames:]
686
+ previous = history[:, -fine_frames - 1 : -1]
687
+ if previous.shape[1] != fine_frames:
688
+ previous = torch.cat((fine_history[:, :1], fine_history[:, :-1]), dim=1)
689
+ motion = fine_history - previous
690
+ fine, fine_positions = self._patch(
691
+ self.fine_prefix_embed,
692
+ fine_history,
693
+ self.history_frames - fine_frames,
694
+ )
695
+ motion_tokens, _ = self._patch(
696
+ self.motion_prefix_embed,
697
+ motion,
698
+ self.history_frames - fine_frames,
699
+ )
700
+ coarse = coarse + self.coarse_scale_embedding
701
+ fine = fine + motion_tokens + self.fine_scale_embedding
702
+ prefix = torch.cat((coarse, fine), dim=1)
703
+ positions = torch.cat((coarse_positions, fine_positions), dim=0)
704
+ if memory_tokens is not None:
705
+ coarse_patch = self.model_config.memory_patch_size
706
+ memory_positions = _video_positions(
707
+ 1,
708
+ history.shape[-2] // coarse_patch,
709
+ history.shape[-1] // coarse_patch,
710
+ -2,
711
+ history.device,
712
+ )
713
+ prefix = torch.cat((memory_tokens, prefix), dim=1)
714
+ positions = torch.cat((memory_positions, positions), dim=0)
715
+ if registers is not None:
716
+ register_count = registers.shape[1]
717
+ register_positions = torch.stack(
718
+ (
719
+ torch.full((register_count,), -1.0, device=history.device),
720
+ torch.zeros(register_count, device=history.device),
721
+ torch.arange(register_count, device=history.device, dtype=torch.float32),
722
+ ),
723
+ dim=-1,
724
+ )
725
+ prefix = torch.cat((registers, prefix), dim=1)
726
+ positions = torch.cat((register_positions, positions), dim=0)
727
+ if self.context_projection is not None and context is not None:
728
+ projected = self.context_projection(context)
729
+ prefix = torch.cat((prefix, projected), dim=1)
730
+ positions = torch.cat(
731
+ (
732
+ positions,
733
+ torch.zeros(projected.shape[1], 3, device=history.device),
734
+ ),
735
+ dim=0,
736
+ )
737
+ for block in self.memory_blocks:
738
+ prefix = block(prefix, positions)
739
+ return prefix, positions
740
+
741
+ def _pool_prefix(self, prefix: torch.Tensor) -> torch.Tensor:
742
+ register_count = self.model_config.register_tokens
743
+ if register_count > 0:
744
+ return prefix[:, :register_count].mean(dim=1)
745
+ return prefix.mean(dim=1)
746
+
747
+ def _condition(
748
+ self,
749
+ prefix: torch.Tensor,
750
+ r: torch.Tensor,
751
+ t: torch.Tensor,
752
+ ) -> torch.Tensor:
753
+ time = self.time_projection(
754
+ torch.cat(
755
+ (
756
+ self.t_embedding(t),
757
+ self.r_embedding(r),
758
+ self.delta_embedding(t - r),
759
+ ),
760
+ dim=-1,
761
+ )
762
+ )
763
+ return time + self.prefix_condition(self._pool_prefix(prefix))
764
+
765
+ def build_memory_cache(
766
+ self,
767
+ history: torch.Tensor,
768
+ context: torch.Tensor | None = None,
769
+ history_actions: torch.Tensor | None = None,
770
+ ) -> PrefixCache:
771
+ prefix, positions = self._encode_prefix(history, context, history_actions)
772
+ pooled = self.prefix_condition(self._pool_prefix(prefix))
773
+ key_values: list[tuple[torch.Tensor, torch.Tensor]] = []
774
+ for block in self.blocks:
775
+ prefix = block.advance_prefix(prefix, positions)
776
+ key_values.append(block.prefix_key_value(prefix, positions))
777
+ return PrefixCache(tuple(key_values), pooled)
778
+
779
+ def predict_outcomes(
780
+ self,
781
+ history: torch.Tensor,
782
+ actions: torch.Tensor | None = None,
783
+ frame_count: int = 1,
784
+ memory_cache: PrefixCache | None = None,
785
+ history_actions: torch.Tensor | None = None,
786
+ ) -> tuple[torch.Tensor, torch.Tensor]:
787
+ if memory_cache is None:
788
+ prefix, _ = self._encode_prefix(history, None, history_actions)
789
+ pooled = self.prefix_condition(self._pool_prefix(prefix))
790
+ else:
791
+ pooled = memory_cache.pooled
792
+ hidden = pooled[:, None].expand(-1, frame_count, -1)
793
+ if self.action_projection is not None:
794
+ if actions is None or actions.shape[1] != frame_count:
795
+ raise ValueError("Outcome actions must match frame_count.")
796
+ hidden = hidden + self.action_projection(actions)
797
+ hidden = self.outcome_norm(hidden)
798
+ return self.reward_head(hidden).squeeze(-1), self.terminal_head(hidden).squeeze(-1)
799
+
800
+ def _unpatchify(
801
+ self,
802
+ patches: torch.Tensor,
803
+ frames: int,
804
+ height: int,
805
+ width: int,
806
+ ) -> torch.Tensor:
807
+ batch = patches.shape[0]
808
+ patch = self.model_config.target_patch_size
809
+ patches = patches.reshape(
810
+ batch,
811
+ frames,
812
+ height,
813
+ width,
814
+ patch,
815
+ patch,
816
+ self.latent_channels,
817
+ )
818
+ return patches.permute(0, 1, 6, 2, 4, 3, 5).reshape(
819
+ batch,
820
+ frames,
821
+ self.latent_channels,
822
+ height * patch,
823
+ width * patch,
824
+ )
825
+
826
+ def forward(
827
+ self,
828
+ noisy_target: torch.Tensor,
829
+ history: torch.Tensor,
830
+ r: torch.Tensor,
831
+ t: torch.Tensor,
832
+ actions: torch.Tensor | None = None,
833
+ context: torch.Tensor | None = None,
834
+ memory_cache: PrefixCache | None = None,
835
+ history_actions: torch.Tensor | None = None,
836
+ ) -> torch.Tensor:
837
+ target_volume = self.target_patch_embed(noisy_target.transpose(1, 2))
838
+ batch, _, frames, height, width = target_volume.shape
839
+ target = target_volume.flatten(2).transpose(1, 2)
840
+ target_positions = _video_positions(
841
+ frames,
842
+ height,
843
+ width,
844
+ self.history_frames,
845
+ noisy_target.device,
846
+ )
847
+ if memory_cache is None:
848
+ prefix, prefix_positions = self._encode_prefix(
849
+ history,
850
+ context,
851
+ history_actions,
852
+ )
853
+ condition = self._condition(prefix, r, t)
854
+ else:
855
+ prefix = None
856
+ prefix_positions = None
857
+ condition = self.time_projection(
858
+ torch.cat(
859
+ (
860
+ self.t_embedding(t),
861
+ self.r_embedding(r),
862
+ self.delta_embedding(t - r),
863
+ ),
864
+ dim=-1,
865
+ )
866
+ ) + memory_cache.pooled
867
+ if self.action_projection is not None:
868
+ if actions is None or actions.shape[:2] != (batch, frames):
869
+ raise ValueError("Actions must match [batch, target_frames, action_dim].")
870
+ action_embedding = self.action_projection(actions)
871
+ target = target + action_embedding.repeat_interleave(height * width, dim=1)
872
+ condition = condition + action_embedding.mean(dim=1)
873
+ for block_index, block in enumerate(self.blocks):
874
+ modulation = (
875
+ self.shared_modulation(condition, block_index)
876
+ if self.shared_modulation is not None
877
+ else None
878
+ )
879
+ if memory_cache is None:
880
+ if prefix is None or prefix_positions is None:
881
+ raise RuntimeError("Prefix state is unavailable.")
882
+ if self.training and self.gradient_checkpointing:
883
+ prefix, target = checkpoint(
884
+ block,
885
+ prefix,
886
+ target,
887
+ condition,
888
+ prefix_positions,
889
+ target_positions,
890
+ modulation,
891
+ use_reentrant=False,
892
+ )
893
+ else:
894
+ prefix, target = block(
895
+ prefix,
896
+ target,
897
+ condition,
898
+ prefix_positions,
899
+ target_positions,
900
+ modulation,
901
+ )
902
+ else:
903
+ target = block.advance_target(
904
+ target,
905
+ condition,
906
+ target_positions,
907
+ memory_cache.key_values[block_index],
908
+ modulation,
909
+ )
910
+ shift, scale = self.output_modulation(F.silu(condition)).chunk(2, dim=-1)
911
+ target = self.output_norm(target) * (1.0 + scale[:, None]) + shift[:, None]
912
+ return self._unpatchify(
913
+ self.output_projection(target),
914
+ frames,
915
+ height,
916
+ width,
917
+ )
918
+
919
+
920
+ CausalVideoDiT = CausalLatentVideoDiT
livediffusion/tile_vae.py ADDED
@@ -0,0 +1,282 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Compact frame autoencoder trained from scratch on tile-rendered gameplay.
2
+
3
+ The frozen SDXL VAE spends 83M parameters and a mid-block attention layer on
4
+ photographic detail that tile art does not contain. A purpose-built encoder
5
+ reaches far lower reconstruction error on the same 8x grid at a fraction of the
6
+ cost, which matters because every training step encodes the whole memory
7
+ window rather than a single frame.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from dataclasses import asdict, dataclass, field
13
+ from pathlib import Path
14
+
15
+ import torch
16
+ from torch import nn
17
+ from torch.nn import functional as F
18
+
19
+
20
+ @dataclass(frozen=True, slots=True)
21
+ class TileVAEConfig:
22
+ latent_channels: int = 4
23
+ base_channels: int = 64
24
+ channel_multipliers: tuple[int, ...] = (1, 2, 4)
25
+ blocks_per_stage: int = 2
26
+
27
+ @property
28
+ def spatial_compression(self) -> int:
29
+ return 2 ** len(self.channel_multipliers)
30
+
31
+ def validate(self) -> None:
32
+ if self.latent_channels < 1:
33
+ raise ValueError("latent_channels must be positive.")
34
+ if self.base_channels < 8:
35
+ raise ValueError("base_channels must be at least 8.")
36
+ if not self.channel_multipliers:
37
+ raise ValueError("channel_multipliers cannot be empty.")
38
+ if any(multiplier < 1 for multiplier in self.channel_multipliers):
39
+ raise ValueError("channel_multipliers must be positive.")
40
+ if self.blocks_per_stage < 1:
41
+ raise ValueError("blocks_per_stage must be positive.")
42
+
43
+
44
+ def _norm(channels: int) -> nn.GroupNorm:
45
+ groups = 32 if channels % 32 == 0 else 8 if channels % 8 == 0 else 1
46
+ return nn.GroupNorm(groups, channels, eps=1e-6)
47
+
48
+
49
+ class ResidualBlock(nn.Module):
50
+ def __init__(self, in_channels: int, out_channels: int) -> None:
51
+ super().__init__()
52
+ self.norm1 = _norm(in_channels)
53
+ self.conv1 = nn.Conv2d(in_channels, out_channels, 3, padding=1)
54
+ self.norm2 = _norm(out_channels)
55
+ self.conv2 = nn.Conv2d(out_channels, out_channels, 3, padding=1)
56
+ self.skip = (
57
+ nn.Conv2d(in_channels, out_channels, 1)
58
+ if in_channels != out_channels
59
+ else nn.Identity()
60
+ )
61
+
62
+ def forward(self, features: torch.Tensor) -> torch.Tensor:
63
+ hidden = self.conv1(F.silu(self.norm1(features)))
64
+ hidden = self.conv2(F.silu(self.norm2(hidden)))
65
+ return self.skip(features) + hidden
66
+
67
+
68
+ class Encoder(nn.Module):
69
+ def __init__(self, config: TileVAEConfig) -> None:
70
+ super().__init__()
71
+ channels = config.base_channels
72
+ self.stem = nn.Conv2d(3, channels, 3, padding=1)
73
+ stages: list[nn.Module] = []
74
+ for multiplier in config.channel_multipliers:
75
+ target = config.base_channels * multiplier
76
+ for _ in range(config.blocks_per_stage):
77
+ stages.append(ResidualBlock(channels, target))
78
+ channels = target
79
+ stages.append(nn.Conv2d(channels, channels, 3, stride=2, padding=1))
80
+ self.stages = nn.Sequential(*stages)
81
+ self.mid = ResidualBlock(channels, channels)
82
+ self.out_norm = _norm(channels)
83
+ self.out = nn.Conv2d(channels, config.latent_channels * 2, 3, padding=1)
84
+
85
+ def forward(self, frames: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
86
+ hidden = self.mid(self.stages(self.stem(frames)))
87
+ mean, log_variance = self.out(F.silu(self.out_norm(hidden))).chunk(2, dim=1)
88
+ return mean, log_variance.clamp(-30.0, 20.0)
89
+
90
+
91
+ class Decoder(nn.Module):
92
+ def __init__(self, config: TileVAEConfig) -> None:
93
+ super().__init__()
94
+ multipliers = config.channel_multipliers
95
+ channels = config.base_channels * multipliers[-1]
96
+ self.stem = nn.Conv2d(config.latent_channels, channels, 3, padding=1)
97
+ self.mid = ResidualBlock(channels, channels)
98
+ stages: list[nn.Module] = []
99
+ for index in reversed(range(len(multipliers))):
100
+ target = config.base_channels * multipliers[index]
101
+ for _ in range(config.blocks_per_stage):
102
+ stages.append(ResidualBlock(channels, target))
103
+ channels = target
104
+ stages.append(Upsample(channels))
105
+ self.stages = nn.Sequential(*stages)
106
+ self.out_norm = _norm(channels)
107
+ self.out = nn.Conv2d(channels, 3, 3, padding=1)
108
+
109
+ def forward(self, latents: torch.Tensor) -> torch.Tensor:
110
+ hidden = self.stages(self.mid(self.stem(latents)))
111
+ return self.out(F.silu(self.out_norm(hidden)))
112
+
113
+
114
+ class Upsample(nn.Module):
115
+ """Nearest-neighbour upsample then convolve, which avoids checkerboarding."""
116
+
117
+ def __init__(self, channels: int) -> None:
118
+ super().__init__()
119
+ self.conv = nn.Conv2d(channels, channels, 3, padding=1)
120
+
121
+ def forward(self, features: torch.Tensor) -> torch.Tensor:
122
+ upsampled = F.interpolate(features.float(), scale_factor=2.0, mode="nearest")
123
+ return self.conv(upsampled.to(features.dtype))
124
+
125
+
126
+ class TileVAE(nn.Module):
127
+ """Convolutional VAE over single frames, with unit-variance latent scaling."""
128
+
129
+ def __init__(self, config: TileVAEConfig | None = None) -> None:
130
+ super().__init__()
131
+ config = config or TileVAEConfig()
132
+ config.validate()
133
+ self.config = config
134
+ self.encoder = Encoder(config)
135
+ self.decoder = Decoder(config)
136
+ self.register_buffer("latent_shift", torch.zeros(config.latent_channels))
137
+ self.register_buffer("latent_scale", torch.ones(config.latent_channels))
138
+
139
+ @property
140
+ def latent_channels(self) -> int:
141
+ return self.config.latent_channels
142
+
143
+ @property
144
+ def spatial_compression(self) -> int:
145
+ return self.config.spatial_compression
146
+
147
+ def encode_moments(
148
+ self,
149
+ frames: torch.Tensor,
150
+ ) -> tuple[torch.Tensor, torch.Tensor]:
151
+ return self.encoder(frames)
152
+
153
+ def normalize(self, latents: torch.Tensor) -> torch.Tensor:
154
+ shift = self.latent_shift.reshape(1, -1, 1, 1).to(latents.dtype)
155
+ scale = self.latent_scale.reshape(1, -1, 1, 1).to(latents.dtype)
156
+ return (latents - shift) * scale
157
+
158
+ def denormalize(self, latents: torch.Tensor) -> torch.Tensor:
159
+ shift = self.latent_shift.reshape(1, -1, 1, 1).to(latents.dtype)
160
+ scale = self.latent_scale.reshape(1, -1, 1, 1).to(latents.dtype)
161
+ return latents / scale + shift
162
+
163
+ def encode(self, frames: torch.Tensor, sample: bool = False) -> torch.Tensor:
164
+ mean, log_variance = self.encode_moments(frames)
165
+ if sample:
166
+ mean = mean + torch.randn_like(mean) * (0.5 * log_variance).exp()
167
+ return self.normalize(mean)
168
+
169
+ def decode(self, latents: torch.Tensor) -> torch.Tensor:
170
+ return self.decoder(self.denormalize(latents))
171
+
172
+ def forward(
173
+ self,
174
+ frames: torch.Tensor,
175
+ sample: bool = True,
176
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
177
+ mean, log_variance = self.encode_moments(frames)
178
+ latents = mean
179
+ if sample:
180
+ latents = mean + torch.randn_like(mean) * (0.5 * log_variance).exp()
181
+ return self.decoder(latents), mean, log_variance
182
+
183
+ def set_latent_statistics(self, mean: torch.Tensor, std: torch.Tensor) -> None:
184
+ self.latent_shift.copy_(mean.detach().flatten().to(self.latent_shift.dtype))
185
+ self.latent_scale.copy_(
186
+ (1.0 / std.detach().flatten().clamp_min(1e-5)).to(self.latent_scale.dtype)
187
+ )
188
+
189
+ def save(self, path: str | Path) -> Path:
190
+ destination = Path(path)
191
+ destination.parent.mkdir(parents=True, exist_ok=True)
192
+ torch.save(
193
+ {
194
+ "format": "tile_vae/1",
195
+ "config": asdict(self.config),
196
+ "state_dict": self.state_dict(),
197
+ },
198
+ destination,
199
+ )
200
+ return destination
201
+
202
+ @classmethod
203
+ def load(
204
+ cls,
205
+ path: str | Path,
206
+ device: torch.device | str = "cpu",
207
+ dtype: torch.dtype = torch.float32,
208
+ ) -> TileVAE:
209
+ payload = torch.load(Path(path), map_location="cpu", weights_only=True)
210
+ if payload.get("format") != "tile_vae/1":
211
+ raise ValueError(f"{path} is not a tile_vae/1 checkpoint.")
212
+ raw = dict(payload["config"])
213
+ raw["channel_multipliers"] = tuple(raw["channel_multipliers"])
214
+ model = cls(TileVAEConfig(**raw))
215
+ model.load_state_dict(payload["state_dict"], strict=True)
216
+ return model.to(device=device, dtype=dtype).eval()
217
+
218
+
219
+ @dataclass(slots=True)
220
+ class ReconstructionLoss:
221
+ total: torch.Tensor
222
+ l1: torch.Tensor
223
+ mse: torch.Tensor
224
+ edge: torch.Tensor
225
+ kl: torch.Tensor
226
+ weights: dict[str, float] = field(default_factory=dict)
227
+
228
+
229
+ def _spatial_gradients(frames: torch.Tensor) -> tuple[torch.Tensor, torch.Tensor]:
230
+ return (
231
+ frames[..., :, 1:] - frames[..., :, :-1],
232
+ frames[..., 1:, :] - frames[..., :-1, :],
233
+ )
234
+
235
+
236
+ def reconstruction_loss(
237
+ reconstruction: torch.Tensor,
238
+ target: torch.Tensor,
239
+ mean: torch.Tensor,
240
+ log_variance: torch.Tensor,
241
+ *,
242
+ l1_weight: float = 1.0,
243
+ mse_weight: float = 1.0,
244
+ edge_weight: float = 0.5,
245
+ kl_weight: float = 1e-6,
246
+ ) -> ReconstructionLoss:
247
+ """Pixel, gradient, and KL terms.
248
+
249
+ The gradient term is what keeps tile boundaries from bleeding: a one-pixel
250
+ smear costs almost nothing in L1 but is exactly the error that turns a
251
+ crisp 8px tile edge into an ambiguous one.
252
+ """
253
+ reconstruction = reconstruction.float()
254
+ target = target.float()
255
+ l1 = (reconstruction - target).abs().mean()
256
+ mse = (reconstruction - target).square().mean()
257
+ prediction_dx, prediction_dy = _spatial_gradients(reconstruction)
258
+ target_dx, target_dy = _spatial_gradients(target)
259
+ edge = (prediction_dx - target_dx).abs().mean() + (
260
+ prediction_dy - target_dy
261
+ ).abs().mean()
262
+ mean = mean.float()
263
+ log_variance = log_variance.float()
264
+ kl = 0.5 * (
265
+ mean.square() + log_variance.exp() - 1.0 - log_variance
266
+ ).flatten(1).sum(dim=1).mean()
267
+ total = (
268
+ l1_weight * l1 + mse_weight * mse + edge_weight * edge + kl_weight * kl
269
+ )
270
+ return ReconstructionLoss(
271
+ total=total,
272
+ l1=l1.detach(),
273
+ mse=mse.detach(),
274
+ edge=edge.detach(),
275
+ kl=kl.detach(),
276
+ weights={
277
+ "l1": l1_weight,
278
+ "mse": mse_weight,
279
+ "edge": edge_weight,
280
+ "kl": kl_weight,
281
+ },
282
+ )