Add Diffusion Miner teacher checkpoint, Tile VAE, live infer, and gameplay GIF
Browse files- .gitattributes +1 -0
- README.md +77 -0
- codec.pt +3 -0
- config.json +117 -0
- demo.gif +3 -0
- ema.safetensors +3 -0
- live_infer.py +22 -0
- livediffusion/__init__.py +3 -0
- livediffusion/config.py +356 -0
- livediffusion/infer_mining.py +526 -0
- livediffusion/meanflow.py +238 -0
- livediffusion/mining.py +644 -0
- livediffusion/model.py +920 -0
- livediffusion/tile_vae.py +282 -0
.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 |
+

|
| 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
|
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 |
+
)
|