Action-conditioned Breakout EMA step 500 + standalone inference
Browse files- README.md +59 -0
- config.json +108 -0
- ema.safetensors +3 -0
- live_infer.py +1368 -0
- preview.png +0 -0
README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
license: apache-2.0
|
| 3 |
+
library_name: pytorch
|
| 4 |
+
pipeline_tag: other
|
| 5 |
+
tags:
|
| 6 |
+
- world-model
|
| 7 |
+
- diffusion
|
| 8 |
+
- breakout
|
| 9 |
+
- video
|
| 10 |
+
- causal
|
| 11 |
+
- game
|
| 12 |
+
---
|
| 13 |
+
|
| 14 |
+
# Diffusion Breakout
|
| 15 |
+
|
| 16 |
+
An action-conditioned Breakout world model. Hold **A / D** (or the arrow keys) and the model generates the next frame.
|
| 17 |
+
|
| 18 |
+

|
| 19 |
+
|
| 20 |
+
128x128 at 6 FPS with 12 frames of latent history. The model has about 70M parameters and uses the frozen SDXL VAE from `madebyollin/sdxl-vae-fp16-fix`.
|
| 21 |
+
|
| 22 |
+
## Run it
|
| 23 |
+
|
| 24 |
+
Requires a CUDA GPU with BF16 support.
|
| 25 |
+
|
| 26 |
+
```bash
|
| 27 |
+
pip install torch numpy pillow safetensors huggingface_hub diffusers
|
| 28 |
+
|
| 29 |
+
hf download kerzgrr/diffusionbreakout live_infer.py --local-dir .
|
| 30 |
+
python live_infer.py --steps 1
|
| 31 |
+
```
|
| 32 |
+
|
| 33 |
+
To keep the weights in a local folder:
|
| 34 |
+
|
| 35 |
+
```bash
|
| 36 |
+
hf download kerzgrr/diffusionbreakout \
|
| 37 |
+
--local-dir checkpoints/diffusionbreakout \
|
| 38 |
+
--include "ema.safetensors" \
|
| 39 |
+
--include "config.json" \
|
| 40 |
+
--include "live_infer.py"
|
| 41 |
+
|
| 42 |
+
python checkpoints/diffusionbreakout/live_infer.py \
|
| 43 |
+
--local-dir checkpoints/diffusionbreakout \
|
| 44 |
+
--steps 1 \
|
| 45 |
+
--window-scale 6
|
| 46 |
+
```
|
| 47 |
+
|
| 48 |
+
Click the window for focus. **A / Left** moves left; **D / Right** moves right.
|
| 49 |
+
|
| 50 |
+
## Files
|
| 51 |
+
|
| 52 |
+
| file | contents |
|
| 53 |
+
|---|---|
|
| 54 |
+
| `ema.safetensors` | action-conditioned EMA weights |
|
| 55 |
+
| `config.json` | model, video, codec, and training settings |
|
| 56 |
+
| `live_infer.py` | single-file live inference |
|
| 57 |
+
| `preview.png` | validation strip |
|
| 58 |
+
|
| 59 |
+
Actions are encoded as `[left, right]`.
|
config.json
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"codec": {
|
| 3 |
+
"enable_slicing": true,
|
| 4 |
+
"enable_tiling": false,
|
| 5 |
+
"frame_batch_size": 256,
|
| 6 |
+
"kind": "frame_vae",
|
| 7 |
+
"latent_channels": 4,
|
| 8 |
+
"model_id": "madebyollin/sdxl-vae-fp16-fix",
|
| 9 |
+
"revision": "main",
|
| 10 |
+
"sample_posterior": false,
|
| 11 |
+
"spatial_compression": 8,
|
| 12 |
+
"subfolder": "",
|
| 13 |
+
"temporal_compression": 1
|
| 14 |
+
},
|
| 15 |
+
"data": {
|
| 16 |
+
"batch_size": 128,
|
| 17 |
+
"data_path": "",
|
| 18 |
+
"dataset_kind": "breakout",
|
| 19 |
+
"frame_stride": 1,
|
| 20 |
+
"hf_repo_id": "",
|
| 21 |
+
"hf_revision": "main",
|
| 22 |
+
"latent_cache_path": "",
|
| 23 |
+
"latent_prefetch_batches": 8,
|
| 24 |
+
"network_clip_prefetch": 2,
|
| 25 |
+
"num_workers": 8,
|
| 26 |
+
"persistent_workers": true,
|
| 27 |
+
"pin_memory": true,
|
| 28 |
+
"prefetch_batches": 2,
|
| 29 |
+
"sample_shuffle": 64,
|
| 30 |
+
"shard_shuffle": 256,
|
| 31 |
+
"train_samples": 500000,
|
| 32 |
+
"validation_samples": 1024,
|
| 33 |
+
"windows_per_video": 4
|
| 34 |
+
},
|
| 35 |
+
"inference": {
|
| 36 |
+
"frames": 120,
|
| 37 |
+
"steps": 1
|
| 38 |
+
},
|
| 39 |
+
"model": {
|
| 40 |
+
"action_dim": 2,
|
| 41 |
+
"context_dim": 0,
|
| 42 |
+
"depth": 12,
|
| 43 |
+
"fine_history_frames": 2,
|
| 44 |
+
"fine_memory_patch_size": 2,
|
| 45 |
+
"generation_frames": 1,
|
| 46 |
+
"hidden_size": 416,
|
| 47 |
+
"memory_depth": 3,
|
| 48 |
+
"memory_patch_size": 4,
|
| 49 |
+
"mlp_ratio": 4.0,
|
| 50 |
+
"num_heads": 8,
|
| 51 |
+
"qk_norm": true,
|
| 52 |
+
"target_patch_size": 2
|
| 53 |
+
},
|
| 54 |
+
"schema_version": 3,
|
| 55 |
+
"simulation": {
|
| 56 |
+
"max_shapes": 2,
|
| 57 |
+
"max_size": 6.0,
|
| 58 |
+
"max_speed": 20.0,
|
| 59 |
+
"min_shapes": 1,
|
| 60 |
+
"min_size": 3.0,
|
| 61 |
+
"min_speed": 8.0,
|
| 62 |
+
"supersample": 2,
|
| 63 |
+
"warmup_frames": 8
|
| 64 |
+
},
|
| 65 |
+
"training": {
|
| 66 |
+
"activation_checkpointing": true,
|
| 67 |
+
"critic_learning_rate": 0.0001,
|
| 68 |
+
"distribution_matching_start_step": 30000,
|
| 69 |
+
"distribution_matching_weight": 0.05,
|
| 70 |
+
"ema_decay": 0.9999,
|
| 71 |
+
"epochs": 100,
|
| 72 |
+
"flow_matching_ratio": 0.25,
|
| 73 |
+
"full_interval_ratio": 0.25,
|
| 74 |
+
"gradient_accumulation_steps": 1,
|
| 75 |
+
"history_noise_std": 0.01,
|
| 76 |
+
"keep_checkpoints": 5,
|
| 77 |
+
"learning_rate": 0.0003,
|
| 78 |
+
"log_every": 20,
|
| 79 |
+
"max_grad_norm": 1.0,
|
| 80 |
+
"min_learning_rate": 1e-05,
|
| 81 |
+
"on_policy_max_horizon": 4,
|
| 82 |
+
"on_policy_ratio": 0.15,
|
| 83 |
+
"on_policy_sampling_steps": 4,
|
| 84 |
+
"on_policy_start_step": 15000,
|
| 85 |
+
"output_dir": "runs/breakout-action-world-model",
|
| 86 |
+
"reward_loss_weight": 0.0,
|
| 87 |
+
"save_every": 50,
|
| 88 |
+
"seed": 2026,
|
| 89 |
+
"teacher_endpoint_ratio": 0.1,
|
| 90 |
+
"teacher_integration_steps": 8,
|
| 91 |
+
"teacher_logit_mean": 1.0,
|
| 92 |
+
"teacher_logit_std": 1.0,
|
| 93 |
+
"teacher_steps": 10000,
|
| 94 |
+
"teacher_x0_weight": 0.25,
|
| 95 |
+
"terminal_loss_weight": 0.0,
|
| 96 |
+
"validate_every": 50,
|
| 97 |
+
"validation_batches": 1,
|
| 98 |
+
"warmup_steps": 500,
|
| 99 |
+
"weight_decay": 0.0
|
| 100 |
+
},
|
| 101 |
+
"video": {
|
| 102 |
+
"fps": 6.0,
|
| 103 |
+
"future_frames": 5,
|
| 104 |
+
"height": 128,
|
| 105 |
+
"history_frames": 12,
|
| 106 |
+
"width": 128
|
| 107 |
+
}
|
| 108 |
+
}
|
ema.safetensors
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:d9f2a9ab8aa958c532fc5f010d0f429e49762803a4203269296e7ae48fdf8191
|
| 3 |
+
size 281086680
|
live_infer.py
ADDED
|
@@ -0,0 +1,1368 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""Run Diffusion Breakout.
|
| 3 |
+
|
| 4 |
+
Deps:
|
| 5 |
+
pip install torch numpy pillow safetensors huggingface_hub diffusers
|
| 6 |
+
|
| 7 |
+
Run:
|
| 8 |
+
python live_infer.py
|
| 9 |
+
python live_infer.py --steps 1 --window-scale 7 --seed 42
|
| 10 |
+
|
| 11 |
+
Controls: A/D or arrow keys. Click window for focus.
|
| 12 |
+
"""
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
import argparse
|
| 16 |
+
import json
|
| 17 |
+
import math
|
| 18 |
+
import random
|
| 19 |
+
import time
|
| 20 |
+
import tkinter as tk
|
| 21 |
+
from collections import deque
|
| 22 |
+
from dataclasses import dataclass
|
| 23 |
+
from pathlib import Path
|
| 24 |
+
|
| 25 |
+
import numpy as np
|
| 26 |
+
import torch
|
| 27 |
+
import torch.nn.functional as F
|
| 28 |
+
from diffusers import AutoencoderKL
|
| 29 |
+
from huggingface_hub import hf_hub_download
|
| 30 |
+
from PIL import Image, ImageDraw, ImageTk
|
| 31 |
+
from safetensors.torch import load_file
|
| 32 |
+
from torch import nn
|
| 33 |
+
from torch.utils.checkpoint import checkpoint
|
| 34 |
+
|
| 35 |
+
DEFAULT_REPO = "kerzgrr/diffusionbreakout"
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
@dataclass(frozen=True, slots=True)
|
| 39 |
+
class VideoConfig:
|
| 40 |
+
height: int = 128
|
| 41 |
+
width: int = 128
|
| 42 |
+
fps: float = 6.0
|
| 43 |
+
history_frames: int = 12
|
| 44 |
+
future_frames: int = 5
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
@dataclass(frozen=True, slots=True)
|
| 48 |
+
class ModelConfig:
|
| 49 |
+
target_patch_size: int = 2
|
| 50 |
+
memory_patch_size: int = 4
|
| 51 |
+
fine_memory_patch_size: int = 2
|
| 52 |
+
fine_history_frames: int = 2
|
| 53 |
+
hidden_size: int = 384
|
| 54 |
+
depth: int = 12
|
| 55 |
+
memory_depth: int = 3
|
| 56 |
+
num_heads: int = 8
|
| 57 |
+
mlp_ratio: float = 4.0
|
| 58 |
+
qk_norm: bool = True
|
| 59 |
+
generation_frames: int = 1
|
| 60 |
+
action_dim: int = 2
|
| 61 |
+
context_dim: int = 0
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@dataclass(frozen=True, slots=True)
|
| 65 |
+
class CodecSettings:
|
| 66 |
+
model_id: str = "madebyollin/sdxl-vae-fp16-fix"
|
| 67 |
+
subfolder: str = ""
|
| 68 |
+
revision: str = "main"
|
| 69 |
+
latent_channels: int = 4
|
| 70 |
+
spatial_compression: int = 8
|
| 71 |
+
frame_batch_size: int = 256
|
| 72 |
+
sample_posterior: bool = False
|
| 73 |
+
enable_slicing: bool = True
|
| 74 |
+
enable_tiling: bool = False
|
| 75 |
+
_BREAKOUT_BRICK_COLORS = (
|
| 76 |
+
(220, 72, 88),
|
| 77 |
+
(230, 140, 54),
|
| 78 |
+
(236, 196, 72),
|
| 79 |
+
(72, 186, 120),
|
| 80 |
+
(72, 148, 220),
|
| 81 |
+
(156, 102, 220),
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
|
| 85 |
+
class BreakoutSimulation:
|
| 86 |
+
"""Classic Breakout with continuous physics and a scripted bottom paddle."""
|
| 87 |
+
|
| 88 |
+
def __init__(self, video_config: VideoConfig, seed: int) -> None:
|
| 89 |
+
self.video_config = video_config
|
| 90 |
+
self.rng = np.random.default_rng(seed)
|
| 91 |
+
self.dt = 1.0 / video_config.fps
|
| 92 |
+
self.paddle_width = max(18.0, video_config.width * 0.18)
|
| 93 |
+
self.paddle_height = max(4.0, video_config.height * 0.03)
|
| 94 |
+
self.ball_radius = max(3.0, min(video_config.width, video_config.height) * 0.025)
|
| 95 |
+
self.paddle_y = video_config.height * 0.90
|
| 96 |
+
self.brick_rows = 5
|
| 97 |
+
self.brick_cols = 8
|
| 98 |
+
self.brick_top = video_config.height * 0.14
|
| 99 |
+
self.brick_gap = 1.5
|
| 100 |
+
usable_width = video_config.width - 10.0
|
| 101 |
+
self.brick_width = (usable_width - self.brick_gap * (self.brick_cols - 1)) / self.brick_cols
|
| 102 |
+
self.brick_height = max(5.0, video_config.height * 0.045)
|
| 103 |
+
self.paddle_x = video_config.width / 2
|
| 104 |
+
self.score = 0
|
| 105 |
+
self.elapsed = 0.0
|
| 106 |
+
self.aim_bias = float(self.rng.uniform(-8.0, 8.0))
|
| 107 |
+
self.reaction_delay = int(self.rng.integers(0, 4))
|
| 108 |
+
self.randomness = float(self.rng.uniform(0.10, 0.28))
|
| 109 |
+
self._observed_ball_x: deque[float] = deque(
|
| 110 |
+
[video_config.width / 2] * (self.reaction_delay + 1),
|
| 111 |
+
maxlen=self.reaction_delay + 1,
|
| 112 |
+
)
|
| 113 |
+
self._held_action = 0
|
| 114 |
+
self._action_hold_frames = 0
|
| 115 |
+
self.ball_position = np.zeros(2, dtype=np.float32)
|
| 116 |
+
self.ball_velocity = np.zeros(2, dtype=np.float32)
|
| 117 |
+
self.bricks = np.ones((self.brick_rows, self.brick_cols), dtype=bool)
|
| 118 |
+
# Punch a few random holes so early frames aren't always a full wall.
|
| 119 |
+
missing = int(self.rng.integers(0, 6))
|
| 120 |
+
for _ in range(missing):
|
| 121 |
+
row = int(self.rng.integers(0, self.brick_rows))
|
| 122 |
+
col = int(self.rng.integers(0, self.brick_cols))
|
| 123 |
+
self.bricks[row, col] = False
|
| 124 |
+
self._serve()
|
| 125 |
+
for _ in range(10):
|
| 126 |
+
self.step()
|
| 127 |
+
|
| 128 |
+
@classmethod
|
| 129 |
+
def from_seed(
|
| 130 |
+
cls,
|
| 131 |
+
video_config: VideoConfig,
|
| 132 |
+
seed: int,
|
| 133 |
+
*,
|
| 134 |
+
at_start: bool = False,
|
| 135 |
+
) -> BreakoutSimulation:
|
| 136 |
+
simulation = cls(video_config, seed)
|
| 137 |
+
if at_start:
|
| 138 |
+
simulation.reset_to_start()
|
| 139 |
+
return simulation
|
| 140 |
+
|
| 141 |
+
def reset_to_start(self) -> None:
|
| 142 |
+
"""Reset to a fresh serve against a full brick wall (for inference)."""
|
| 143 |
+
self.paddle_x = self.video_config.width / 2
|
| 144 |
+
self.score = 0
|
| 145 |
+
self.elapsed = 0.0
|
| 146 |
+
self._held_action = 0
|
| 147 |
+
self._action_hold_frames = 0
|
| 148 |
+
self._observed_ball_x.clear()
|
| 149 |
+
self._observed_ball_x.extend(
|
| 150 |
+
[self.video_config.width / 2] * (self.reaction_delay + 1)
|
| 151 |
+
)
|
| 152 |
+
self.bricks[:] = True
|
| 153 |
+
self._serve()
|
| 154 |
+
|
| 155 |
+
def _brick_rect(self, row: int, col: int) -> tuple[float, float, float, float]:
|
| 156 |
+
x0 = 5.0 + col * (self.brick_width + self.brick_gap)
|
| 157 |
+
y0 = self.brick_top + row * (self.brick_height + self.brick_gap)
|
| 158 |
+
return x0, y0, x0 + self.brick_width, y0 + self.brick_height
|
| 159 |
+
|
| 160 |
+
def _serve(self) -> None:
|
| 161 |
+
angle = float(self.rng.uniform(-0.65, 0.65))
|
| 162 |
+
speed = float(self.rng.uniform(48.0, 64.0))
|
| 163 |
+
self.ball_position[:] = (
|
| 164 |
+
self.paddle_x + float(self.rng.uniform(-6.0, 6.0)),
|
| 165 |
+
self.paddle_y - self.paddle_height - self.ball_radius - 2.0,
|
| 166 |
+
)
|
| 167 |
+
self.ball_velocity[:] = (
|
| 168 |
+
speed * math.sin(angle),
|
| 169 |
+
-speed * math.cos(angle),
|
| 170 |
+
)
|
| 171 |
+
|
| 172 |
+
def _reset_bricks(self) -> None:
|
| 173 |
+
self.bricks[:] = True
|
| 174 |
+
|
| 175 |
+
def _move_paddle(self, dt: float, action: int | None) -> None:
|
| 176 |
+
width = float(self.video_config.width)
|
| 177 |
+
paddle_speed = width * 0.85
|
| 178 |
+
if action is None:
|
| 179 |
+
target = float(self.ball_position[0]) + self.aim_bias
|
| 180 |
+
delta = max(-paddle_speed * dt, min(paddle_speed * dt, target - self.paddle_x))
|
| 181 |
+
else:
|
| 182 |
+
delta = float(max(-1, min(1, action))) * paddle_speed * dt
|
| 183 |
+
self.paddle_x += delta
|
| 184 |
+
half = self.paddle_width / 2
|
| 185 |
+
self.paddle_x = max(half + 2.0, min(width - half - 2.0, self.paddle_x))
|
| 186 |
+
|
| 187 |
+
def _collide_bricks(self) -> None:
|
| 188 |
+
x, y = map(float, self.ball_position)
|
| 189 |
+
r = self.ball_radius
|
| 190 |
+
for row in range(self.brick_rows):
|
| 191 |
+
for col in range(self.brick_cols):
|
| 192 |
+
if not self.bricks[row, col]:
|
| 193 |
+
continue
|
| 194 |
+
x0, y0, x1, y1 = self._brick_rect(row, col)
|
| 195 |
+
closest_x = max(x0, min(x, x1))
|
| 196 |
+
closest_y = max(y0, min(y, y1))
|
| 197 |
+
dx = x - closest_x
|
| 198 |
+
dy = y - closest_y
|
| 199 |
+
if dx * dx + dy * dy > r * r:
|
| 200 |
+
continue
|
| 201 |
+
self.bricks[row, col] = False
|
| 202 |
+
self.score += 1
|
| 203 |
+
# Reflect on the dominant penetration axis.
|
| 204 |
+
if abs(dx) > abs(dy):
|
| 205 |
+
self.ball_velocity[0] = -self.ball_velocity[0]
|
| 206 |
+
self.ball_position[0] = x0 - r if dx < 0 else x1 + r
|
| 207 |
+
else:
|
| 208 |
+
self.ball_velocity[1] = -self.ball_velocity[1]
|
| 209 |
+
self.ball_position[1] = y0 - r if dy < 0 else y1 + r
|
| 210 |
+
if not self.bricks.any():
|
| 211 |
+
self._reset_bricks()
|
| 212 |
+
return
|
| 213 |
+
|
| 214 |
+
def step(self, action: int | None = None) -> None:
|
| 215 |
+
width = float(self.video_config.width)
|
| 216 |
+
height = float(self.video_config.height)
|
| 217 |
+
substeps = 4
|
| 218 |
+
sub_dt = self.dt / substeps
|
| 219 |
+
for _ in range(substeps):
|
| 220 |
+
self._move_paddle(sub_dt, action)
|
| 221 |
+
self.ball_position += self.ball_velocity * sub_dt
|
| 222 |
+
x, y = map(float, self.ball_position)
|
| 223 |
+
if x - self.ball_radius < 2:
|
| 224 |
+
self.ball_position[0] = 2 + self.ball_radius
|
| 225 |
+
self.ball_velocity[0] = abs(self.ball_velocity[0])
|
| 226 |
+
elif x + self.ball_radius > width - 2:
|
| 227 |
+
self.ball_position[0] = width - 2 - self.ball_radius
|
| 228 |
+
self.ball_velocity[0] = -abs(self.ball_velocity[0])
|
| 229 |
+
if y - self.ball_radius < 2:
|
| 230 |
+
self.ball_position[1] = 2 + self.ball_radius
|
| 231 |
+
self.ball_velocity[1] = abs(self.ball_velocity[1])
|
| 232 |
+
paddle_top = self.paddle_y - self.paddle_height / 2
|
| 233 |
+
hitting_paddle = (
|
| 234 |
+
self.ball_velocity[1] > 0
|
| 235 |
+
and y + self.ball_radius >= paddle_top
|
| 236 |
+
and y - self.ball_radius <= self.paddle_y + self.paddle_height / 2
|
| 237 |
+
and abs(x - self.paddle_x) <= self.paddle_width / 2 + self.ball_radius
|
| 238 |
+
)
|
| 239 |
+
if hitting_paddle:
|
| 240 |
+
self.ball_position[1] = paddle_top - self.ball_radius
|
| 241 |
+
self.ball_velocity[1] = -abs(self.ball_velocity[1]) * 1.01
|
| 242 |
+
self.ball_velocity[0] += (x - self.paddle_x) * 1.35
|
| 243 |
+
speed = float(np.linalg.norm(self.ball_velocity))
|
| 244 |
+
target = float(np.clip(speed, 46.0, 78.0))
|
| 245 |
+
if speed > 1e-5:
|
| 246 |
+
self.ball_velocity *= target / speed
|
| 247 |
+
self._collide_bricks()
|
| 248 |
+
if self.ball_position[1] > height + self.ball_radius * 2:
|
| 249 |
+
self._serve()
|
| 250 |
+
self.elapsed += sub_dt
|
| 251 |
+
|
| 252 |
+
def render(self) -> np.ndarray:
|
| 253 |
+
scale = 2
|
| 254 |
+
width = self.video_config.width
|
| 255 |
+
height = self.video_config.height
|
| 256 |
+
image = Image.new("RGB", (width * scale, height * scale), (6, 8, 18))
|
| 257 |
+
draw = ImageDraw.Draw(image)
|
| 258 |
+
draw.rectangle((0, 0, width * scale, 3 * scale), fill=(36, 48, 72))
|
| 259 |
+
for row in range(self.brick_rows):
|
| 260 |
+
color = _BREAKOUT_BRICK_COLORS[row % len(_BREAKOUT_BRICK_COLORS)]
|
| 261 |
+
for col in range(self.brick_cols):
|
| 262 |
+
if not self.bricks[row, col]:
|
| 263 |
+
continue
|
| 264 |
+
x0, y0, x1, y1 = self._brick_rect(row, col)
|
| 265 |
+
draw.rounded_rectangle(
|
| 266 |
+
(x0 * scale, y0 * scale, x1 * scale, y1 * scale),
|
| 267 |
+
radius=max(1, round(scale)),
|
| 268 |
+
fill=color,
|
| 269 |
+
)
|
| 270 |
+
draw.rounded_rectangle(
|
| 271 |
+
(
|
| 272 |
+
(self.paddle_x - self.paddle_width / 2) * scale,
|
| 273 |
+
(self.paddle_y - self.paddle_height / 2) * scale,
|
| 274 |
+
(self.paddle_x + self.paddle_width / 2) * scale,
|
| 275 |
+
(self.paddle_y + self.paddle_height / 2) * scale,
|
| 276 |
+
),
|
| 277 |
+
radius=2 * scale,
|
| 278 |
+
fill=(230, 238, 245),
|
| 279 |
+
)
|
| 280 |
+
ball_x, ball_y = self.ball_position * scale
|
| 281 |
+
radius = self.ball_radius * scale
|
| 282 |
+
draw.ellipse(
|
| 283 |
+
(ball_x - radius, ball_y - radius, ball_x + radius, ball_y + radius),
|
| 284 |
+
fill=(255, 209, 72),
|
| 285 |
+
)
|
| 286 |
+
image = image.resize((width, height), Image.Resampling.LANCZOS)
|
| 287 |
+
return np.asarray(image, dtype=np.uint8)
|
| 288 |
+
|
| 289 |
+
def next_frame(self) -> np.ndarray:
|
| 290 |
+
frame = self.render()
|
| 291 |
+
self.step()
|
| 292 |
+
return frame
|
| 293 |
+
|
| 294 |
+
def sample_frames(self, count: int) -> np.ndarray:
|
| 295 |
+
return np.stack([self.next_frame() for _ in range(count)])
|
| 296 |
+
|
| 297 |
+
def _scripted_action(self) -> int:
|
| 298 |
+
self._observed_ball_x.append(float(self.ball_position[0]))
|
| 299 |
+
if self._action_hold_frames > 0:
|
| 300 |
+
self._action_hold_frames -= 1
|
| 301 |
+
return self._held_action
|
| 302 |
+
self._action_hold_frames = int(self.rng.integers(1, 5))
|
| 303 |
+
random_value = float(self.rng.random())
|
| 304 |
+
delayed_target = self._observed_ball_x[0] + self.aim_bias
|
| 305 |
+
error = delayed_target - self.paddle_x
|
| 306 |
+
ideal_action = -1 if error < -4 else (1 if error > 4 else 0)
|
| 307 |
+
if random_value < self.randomness * 0.35:
|
| 308 |
+
action = 0
|
| 309 |
+
elif random_value < self.randomness * 0.75:
|
| 310 |
+
action = int(self.rng.choice((-1, 0, 1)))
|
| 311 |
+
elif random_value < self.randomness:
|
| 312 |
+
action = -ideal_action if ideal_action != 0 else int(self.rng.choice((-1, 1)))
|
| 313 |
+
else:
|
| 314 |
+
action = ideal_action
|
| 315 |
+
self._held_action = action
|
| 316 |
+
return action
|
| 317 |
+
|
| 318 |
+
def sample_gameplay(self, count: int) -> tuple[np.ndarray, np.ndarray]:
|
| 319 |
+
frames: list[np.ndarray] = []
|
| 320 |
+
actions: list[tuple[float, float]] = []
|
| 321 |
+
for _ in range(count):
|
| 322 |
+
frames.append(self.render())
|
| 323 |
+
action = self._scripted_action()
|
| 324 |
+
# [A/left, D/right] — same layout we'll use for action fine-tuning later.
|
| 325 |
+
actions.append((float(action < 0), float(action > 0)))
|
| 326 |
+
self.step(action)
|
| 327 |
+
return np.stack(frames), np.asarray(actions, dtype=np.float32)
|
| 328 |
+
|
| 329 |
+
|
| 330 |
+
class FrameAutoencoderCodec(nn.Module):
|
| 331 |
+
def __init__(self, config: CodecSettings, device: torch.device, dtype: torch.dtype) -> None:
|
| 332 |
+
super().__init__()
|
| 333 |
+
load_kwargs: dict[str, object] = {
|
| 334 |
+
"revision": config.revision,
|
| 335 |
+
"torch_dtype": dtype,
|
| 336 |
+
"low_cpu_mem_usage": True,
|
| 337 |
+
}
|
| 338 |
+
if config.subfolder:
|
| 339 |
+
load_kwargs["subfolder"] = config.subfolder
|
| 340 |
+
try:
|
| 341 |
+
self.vae = AutoencoderKL.from_pretrained(
|
| 342 |
+
config.model_id, local_files_only=True, **load_kwargs
|
| 343 |
+
)
|
| 344 |
+
except OSError:
|
| 345 |
+
self.vae = AutoencoderKL.from_pretrained(config.model_id, **load_kwargs)
|
| 346 |
+
self.vae.to(device=device).eval()
|
| 347 |
+
self.vae.to(memory_format=torch.channels_last)
|
| 348 |
+
self.vae.requires_grad_(False)
|
| 349 |
+
if config.enable_slicing:
|
| 350 |
+
self.vae.enable_slicing()
|
| 351 |
+
if config.enable_tiling:
|
| 352 |
+
self.vae.enable_tiling()
|
| 353 |
+
self.channels = int(self.vae.config.latent_channels)
|
| 354 |
+
self.spatial_compression = 2 ** (len(self.vae.config.block_out_channels) - 1)
|
| 355 |
+
self.temporal_compression = 1
|
| 356 |
+
self.frame_batch_size = config.frame_batch_size
|
| 357 |
+
self.sample_posterior = config.sample_posterior
|
| 358 |
+
self.dtype = dtype
|
| 359 |
+
self.scaling_factor = float(self.vae.config.scaling_factor)
|
| 360 |
+
shift_factor = getattr(self.vae.config, "shift_factor", None)
|
| 361 |
+
self.shift_factor = float(shift_factor) if shift_factor is not None else 0.0
|
| 362 |
+
|
| 363 |
+
def latent_frame_count(self, frame_count: int) -> int:
|
| 364 |
+
return frame_count
|
| 365 |
+
|
| 366 |
+
@torch.no_grad()
|
| 367 |
+
def encode(self, frames: torch.Tensor) -> torch.Tensor:
|
| 368 |
+
batch, frame_count, channels, height, width = frames.shape
|
| 369 |
+
flat_frames = frames.reshape(batch * frame_count, channels, height, width)
|
| 370 |
+
encoded: list[torch.Tensor] = []
|
| 371 |
+
for frame_batch in flat_frames.split(self.frame_batch_size):
|
| 372 |
+
frame_batch = frame_batch.to(self.dtype).contiguous(
|
| 373 |
+
memory_format=torch.channels_last
|
| 374 |
+
)
|
| 375 |
+
posterior = self.vae.encode(frame_batch).latent_dist
|
| 376 |
+
latents = posterior.sample() if self.sample_posterior else posterior.mode()
|
| 377 |
+
encoded.append((latents - self.shift_factor) * self.scaling_factor)
|
| 378 |
+
merged = torch.cat(encoded)
|
| 379 |
+
return merged.reshape(
|
| 380 |
+
batch,
|
| 381 |
+
frame_count,
|
| 382 |
+
self.channels,
|
| 383 |
+
height // self.spatial_compression,
|
| 384 |
+
width // self.spatial_compression,
|
| 385 |
+
)
|
| 386 |
+
|
| 387 |
+
@torch.no_grad()
|
| 388 |
+
def decode(self, latents: torch.Tensor) -> torch.Tensor:
|
| 389 |
+
batch, frame_count, channels, height, width = latents.shape
|
| 390 |
+
flat_latents = latents.reshape(batch * frame_count, channels, height, width)
|
| 391 |
+
decoded: list[torch.Tensor] = []
|
| 392 |
+
for latent_batch in flat_latents.split(self.frame_batch_size):
|
| 393 |
+
denormalized = latent_batch / self.scaling_factor + self.shift_factor
|
| 394 |
+
denormalized = denormalized.to(self.dtype).contiguous(
|
| 395 |
+
memory_format=torch.channels_last
|
| 396 |
+
)
|
| 397 |
+
decoded.append(self.vae.decode(denormalized).sample)
|
| 398 |
+
merged = torch.cat(decoded)
|
| 399 |
+
return merged.reshape(
|
| 400 |
+
batch,
|
| 401 |
+
frame_count,
|
| 402 |
+
3,
|
| 403 |
+
height * self.spatial_compression,
|
| 404 |
+
width * self.spatial_compression,
|
| 405 |
+
).clamp(-1.0, 1.0)
|
| 406 |
+
@dataclass(slots=True)
|
| 407 |
+
class PrefixCache:
|
| 408 |
+
key_values: tuple[tuple[torch.Tensor, torch.Tensor], ...]
|
| 409 |
+
pooled: torch.Tensor
|
| 410 |
+
|
| 411 |
+
|
| 412 |
+
MemoryCache = PrefixCache
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
def _video_positions(
|
| 416 |
+
frames: int,
|
| 417 |
+
height: int,
|
| 418 |
+
width: int,
|
| 419 |
+
time_offset: int,
|
| 420 |
+
device: torch.device,
|
| 421 |
+
) -> torch.Tensor:
|
| 422 |
+
time = torch.arange(time_offset, time_offset + frames, device=device)
|
| 423 |
+
y = torch.arange(height, device=device)
|
| 424 |
+
x = torch.arange(width, device=device)
|
| 425 |
+
grid = torch.meshgrid(time, y, x, indexing="ij")
|
| 426 |
+
return torch.stack(grid, dim=-1).reshape(-1, 3).float()
|
| 427 |
+
|
| 428 |
+
|
| 429 |
+
class Rotary3D(nn.Module):
|
| 430 |
+
def __init__(self, head_size: int, base: float = 10_000.0) -> None:
|
| 431 |
+
super().__init__()
|
| 432 |
+
self.rotary_size = (head_size // 6) * 6
|
| 433 |
+
self.axis_size = self.rotary_size // 3
|
| 434 |
+
frequencies = torch.exp(
|
| 435 |
+
-math.log(base)
|
| 436 |
+
* torch.arange(self.axis_size // 2, dtype=torch.float32)
|
| 437 |
+
/ (self.axis_size // 2)
|
| 438 |
+
)
|
| 439 |
+
self.register_buffer("frequencies", frequencies, persistent=False)
|
| 440 |
+
|
| 441 |
+
@staticmethod
|
| 442 |
+
def _rotate_half(inputs: torch.Tensor) -> torch.Tensor:
|
| 443 |
+
pairs = inputs.reshape(*inputs.shape[:-1], -1, 2)
|
| 444 |
+
first, second = pairs.unbind(dim=-1)
|
| 445 |
+
return torch.stack((-second, first), dim=-1).flatten(-2)
|
| 446 |
+
|
| 447 |
+
def forward(self, inputs: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
|
| 448 |
+
angles = torch.cat(
|
| 449 |
+
[positions[:, axis, None] * self.frequencies[None] for axis in range(3)],
|
| 450 |
+
dim=-1,
|
| 451 |
+
)
|
| 452 |
+
cosine = angles.cos().repeat_interleave(2, dim=-1)[None, None]
|
| 453 |
+
sine = angles.sin().repeat_interleave(2, dim=-1)[None, None]
|
| 454 |
+
rotary, remainder = inputs.split(
|
| 455 |
+
(self.rotary_size, inputs.shape[-1] - self.rotary_size),
|
| 456 |
+
dim=-1,
|
| 457 |
+
)
|
| 458 |
+
rotated = rotary * cosine.to(rotary.dtype)
|
| 459 |
+
rotated = rotated + self._rotate_half(rotary) * sine.to(rotary.dtype)
|
| 460 |
+
return torch.cat((rotated, remainder), dim=-1)
|
| 461 |
+
|
| 462 |
+
|
| 463 |
+
class ScalarEmbedding(nn.Module):
|
| 464 |
+
def __init__(self, hidden_size: int) -> None:
|
| 465 |
+
super().__init__()
|
| 466 |
+
self.hidden_size = hidden_size
|
| 467 |
+
self.mlp = nn.Sequential(
|
| 468 |
+
nn.Linear(hidden_size, hidden_size),
|
| 469 |
+
nn.SiLU(),
|
| 470 |
+
nn.Linear(hidden_size, hidden_size),
|
| 471 |
+
)
|
| 472 |
+
|
| 473 |
+
def forward(self, value: torch.Tensor) -> torch.Tensor:
|
| 474 |
+
value = value.reshape(-1).float()
|
| 475 |
+
half = self.hidden_size // 2
|
| 476 |
+
frequencies = torch.exp(
|
| 477 |
+
-math.log(10_000.0)
|
| 478 |
+
* torch.arange(half, device=value.device, dtype=torch.float32)
|
| 479 |
+
/ half
|
| 480 |
+
)
|
| 481 |
+
angles = value[:, None] * frequencies[None] * 1_000.0
|
| 482 |
+
embedding = torch.cat((angles.cos(), angles.sin()), dim=-1)
|
| 483 |
+
return self.mlp(embedding.to(dtype=self.mlp[0].weight.dtype))
|
| 484 |
+
|
| 485 |
+
|
| 486 |
+
class SwiGLU(nn.Module):
|
| 487 |
+
def __init__(self, hidden_size: int, ratio: float) -> None:
|
| 488 |
+
super().__init__()
|
| 489 |
+
inner_size = int(hidden_size * ratio * 2 / 3)
|
| 490 |
+
inner_size = ((inner_size + 63) // 64) * 64
|
| 491 |
+
self.input = nn.Linear(hidden_size, inner_size * 2, bias=False)
|
| 492 |
+
self.output = nn.Linear(inner_size, hidden_size, bias=False)
|
| 493 |
+
|
| 494 |
+
def forward(self, inputs: torch.Tensor) -> torch.Tensor:
|
| 495 |
+
values, gates = self.input(inputs).chunk(2, dim=-1)
|
| 496 |
+
return self.output(values * F.silu(gates))
|
| 497 |
+
|
| 498 |
+
|
| 499 |
+
class SelfAttention(nn.Module):
|
| 500 |
+
def __init__(self, hidden_size: int, num_heads: int, qk_norm: bool) -> None:
|
| 501 |
+
super().__init__()
|
| 502 |
+
self.num_heads = num_heads
|
| 503 |
+
self.head_size = hidden_size // num_heads
|
| 504 |
+
self.qkv = nn.Linear(hidden_size, hidden_size * 3, bias=False)
|
| 505 |
+
self.output = nn.Linear(hidden_size, hidden_size, bias=False)
|
| 506 |
+
self.q_norm = (
|
| 507 |
+
nn.RMSNorm(self.head_size, eps=1e-6)
|
| 508 |
+
if qk_norm
|
| 509 |
+
else nn.Identity()
|
| 510 |
+
)
|
| 511 |
+
self.k_norm = (
|
| 512 |
+
nn.RMSNorm(self.head_size, eps=1e-6)
|
| 513 |
+
if qk_norm
|
| 514 |
+
else nn.Identity()
|
| 515 |
+
)
|
| 516 |
+
self.rotary = Rotary3D(self.head_size)
|
| 517 |
+
|
| 518 |
+
def forward(self, inputs: torch.Tensor, positions: torch.Tensor) -> torch.Tensor:
|
| 519 |
+
batch, tokens, channels = inputs.shape
|
| 520 |
+
query, key, value = self.qkv(inputs).reshape(
|
| 521 |
+
batch,
|
| 522 |
+
tokens,
|
| 523 |
+
3,
|
| 524 |
+
self.num_heads,
|
| 525 |
+
self.head_size,
|
| 526 |
+
).unbind(dim=2)
|
| 527 |
+
query = self.rotary(self.q_norm(query).transpose(1, 2), positions)
|
| 528 |
+
key = self.rotary(self.k_norm(key).transpose(1, 2), positions)
|
| 529 |
+
value = value.transpose(1, 2)
|
| 530 |
+
attended = F.scaled_dot_product_attention(query, key, value)
|
| 531 |
+
return self.output(attended.transpose(1, 2).reshape(batch, tokens, channels))
|
| 532 |
+
|
| 533 |
+
|
| 534 |
+
class PrefixAttention(nn.Module):
|
| 535 |
+
"""Target attention over the joint clean-prefix and noisy-target sequence."""
|
| 536 |
+
|
| 537 |
+
def __init__(self, hidden_size: int, num_heads: int, qk_norm: bool) -> None:
|
| 538 |
+
super().__init__()
|
| 539 |
+
self.num_heads = num_heads
|
| 540 |
+
self.head_size = hidden_size // num_heads
|
| 541 |
+
self.target_qkv = nn.Linear(hidden_size, hidden_size * 3, bias=False)
|
| 542 |
+
self.prefix_key_value = nn.Linear(hidden_size, hidden_size * 2, bias=False)
|
| 543 |
+
self.output = nn.Linear(hidden_size, hidden_size, bias=False)
|
| 544 |
+
self.q_norm = (
|
| 545 |
+
nn.RMSNorm(self.head_size, eps=1e-6)
|
| 546 |
+
if qk_norm
|
| 547 |
+
else nn.Identity()
|
| 548 |
+
)
|
| 549 |
+
self.k_norm = (
|
| 550 |
+
nn.RMSNorm(self.head_size, eps=1e-6)
|
| 551 |
+
if qk_norm
|
| 552 |
+
else nn.Identity()
|
| 553 |
+
)
|
| 554 |
+
self.rotary = Rotary3D(self.head_size)
|
| 555 |
+
|
| 556 |
+
def project_prefix(
|
| 557 |
+
self,
|
| 558 |
+
prefix: torch.Tensor,
|
| 559 |
+
positions: torch.Tensor,
|
| 560 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 561 |
+
batch, tokens, _ = prefix.shape
|
| 562 |
+
key, value = self.prefix_key_value(prefix).reshape(
|
| 563 |
+
batch,
|
| 564 |
+
tokens,
|
| 565 |
+
2,
|
| 566 |
+
self.num_heads,
|
| 567 |
+
self.head_size,
|
| 568 |
+
).unbind(dim=2)
|
| 569 |
+
key = self.rotary(self.k_norm(key).transpose(1, 2), positions)
|
| 570 |
+
return key, value.transpose(1, 2)
|
| 571 |
+
|
| 572 |
+
def forward_cached(
|
| 573 |
+
self,
|
| 574 |
+
target: torch.Tensor,
|
| 575 |
+
target_positions: torch.Tensor,
|
| 576 |
+
prefix_key_value: tuple[torch.Tensor, torch.Tensor],
|
| 577 |
+
) -> torch.Tensor:
|
| 578 |
+
batch, tokens, channels = target.shape
|
| 579 |
+
query, key, value = self.target_qkv(target).reshape(
|
| 580 |
+
batch,
|
| 581 |
+
tokens,
|
| 582 |
+
3,
|
| 583 |
+
self.num_heads,
|
| 584 |
+
self.head_size,
|
| 585 |
+
).unbind(dim=2)
|
| 586 |
+
query = self.rotary(self.q_norm(query).transpose(1, 2), target_positions)
|
| 587 |
+
key = self.rotary(self.k_norm(key).transpose(1, 2), target_positions)
|
| 588 |
+
value = value.transpose(1, 2)
|
| 589 |
+
prefix_key, prefix_value = prefix_key_value
|
| 590 |
+
key = torch.cat((prefix_key, key), dim=2)
|
| 591 |
+
value = torch.cat((prefix_value, value), dim=2)
|
| 592 |
+
attended = F.scaled_dot_product_attention(query, key, value)
|
| 593 |
+
return self.output(attended.transpose(1, 2).reshape(batch, tokens, channels))
|
| 594 |
+
|
| 595 |
+
|
| 596 |
+
class PrefixDiTBlock(nn.Module):
|
| 597 |
+
def __init__(self, config: ModelConfig) -> None:
|
| 598 |
+
super().__init__()
|
| 599 |
+
hidden_size = config.hidden_size
|
| 600 |
+
self.prefix_norm1 = nn.RMSNorm(hidden_size, eps=1e-6)
|
| 601 |
+
self.prefix_attention = SelfAttention(
|
| 602 |
+
hidden_size,
|
| 603 |
+
config.num_heads,
|
| 604 |
+
config.qk_norm,
|
| 605 |
+
)
|
| 606 |
+
self.prefix_norm2 = nn.RMSNorm(hidden_size, eps=1e-6)
|
| 607 |
+
self.prefix_mlp = SwiGLU(hidden_size, config.mlp_ratio)
|
| 608 |
+
self.prefix_projection_norm = nn.RMSNorm(hidden_size, eps=1e-6)
|
| 609 |
+
self.target_norm1 = nn.RMSNorm(
|
| 610 |
+
hidden_size,
|
| 611 |
+
eps=1e-6,
|
| 612 |
+
elementwise_affine=False,
|
| 613 |
+
)
|
| 614 |
+
self.target_attention = PrefixAttention(
|
| 615 |
+
hidden_size,
|
| 616 |
+
config.num_heads,
|
| 617 |
+
config.qk_norm,
|
| 618 |
+
)
|
| 619 |
+
self.target_norm2 = nn.RMSNorm(
|
| 620 |
+
hidden_size,
|
| 621 |
+
eps=1e-6,
|
| 622 |
+
elementwise_affine=False,
|
| 623 |
+
)
|
| 624 |
+
self.target_mlp = SwiGLU(hidden_size, config.mlp_ratio)
|
| 625 |
+
self.modulation = nn.Linear(hidden_size, hidden_size * 6)
|
| 626 |
+
|
| 627 |
+
@staticmethod
|
| 628 |
+
def _modulate(
|
| 629 |
+
inputs: torch.Tensor,
|
| 630 |
+
shift: torch.Tensor,
|
| 631 |
+
scale: torch.Tensor,
|
| 632 |
+
) -> torch.Tensor:
|
| 633 |
+
return inputs * (1.0 + scale[:, None]) + shift[:, None]
|
| 634 |
+
|
| 635 |
+
def advance_prefix(
|
| 636 |
+
self,
|
| 637 |
+
prefix: torch.Tensor,
|
| 638 |
+
positions: torch.Tensor,
|
| 639 |
+
) -> torch.Tensor:
|
| 640 |
+
prefix = prefix + self.prefix_attention(self.prefix_norm1(prefix), positions)
|
| 641 |
+
return prefix + self.prefix_mlp(self.prefix_norm2(prefix))
|
| 642 |
+
|
| 643 |
+
def prefix_key_value(
|
| 644 |
+
self,
|
| 645 |
+
prefix: torch.Tensor,
|
| 646 |
+
positions: torch.Tensor,
|
| 647 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 648 |
+
return self.target_attention.project_prefix(
|
| 649 |
+
self.prefix_projection_norm(prefix),
|
| 650 |
+
positions,
|
| 651 |
+
)
|
| 652 |
+
|
| 653 |
+
def advance_target(
|
| 654 |
+
self,
|
| 655 |
+
target: torch.Tensor,
|
| 656 |
+
condition: torch.Tensor,
|
| 657 |
+
target_positions: torch.Tensor,
|
| 658 |
+
prefix_key_value: tuple[torch.Tensor, torch.Tensor],
|
| 659 |
+
) -> torch.Tensor:
|
| 660 |
+
shift1, scale1, gate1, shift2, scale2, gate2 = self.modulation(
|
| 661 |
+
F.silu(condition)
|
| 662 |
+
).chunk(6, dim=-1)
|
| 663 |
+
normalized = self._modulate(self.target_norm1(target), shift1, scale1)
|
| 664 |
+
target = target + gate1[:, None] * self.target_attention.forward_cached(
|
| 665 |
+
normalized,
|
| 666 |
+
target_positions,
|
| 667 |
+
prefix_key_value,
|
| 668 |
+
)
|
| 669 |
+
normalized = self._modulate(self.target_norm2(target), shift2, scale2)
|
| 670 |
+
return target + gate2[:, None] * self.target_mlp(normalized)
|
| 671 |
+
|
| 672 |
+
def forward(
|
| 673 |
+
self,
|
| 674 |
+
prefix: torch.Tensor,
|
| 675 |
+
target: torch.Tensor,
|
| 676 |
+
condition: torch.Tensor,
|
| 677 |
+
prefix_positions: torch.Tensor,
|
| 678 |
+
target_positions: torch.Tensor,
|
| 679 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 680 |
+
prefix = self.advance_prefix(prefix, prefix_positions)
|
| 681 |
+
key_value = self.prefix_key_value(prefix, prefix_positions)
|
| 682 |
+
target = self.advance_target(
|
| 683 |
+
target,
|
| 684 |
+
condition,
|
| 685 |
+
target_positions,
|
| 686 |
+
key_value,
|
| 687 |
+
)
|
| 688 |
+
return prefix, target
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
class CausalLatentVideoDiT(nn.Module):
|
| 692 |
+
"""Multiscale clean-prefix/noisy-suffix latent video transformer."""
|
| 693 |
+
|
| 694 |
+
def __init__(
|
| 695 |
+
self,
|
| 696 |
+
model_config: ModelConfig,
|
| 697 |
+
latent_channels: int,
|
| 698 |
+
history_frames: int = 12,
|
| 699 |
+
) -> None:
|
| 700 |
+
super().__init__()
|
| 701 |
+
self.model_config = model_config
|
| 702 |
+
self.latent_channels = latent_channels
|
| 703 |
+
self.history_frames = history_frames
|
| 704 |
+
self.gradient_checkpointing = False
|
| 705 |
+
hidden_size = model_config.hidden_size
|
| 706 |
+
target_patch = model_config.target_patch_size
|
| 707 |
+
coarse_patch = model_config.memory_patch_size
|
| 708 |
+
fine_patch = model_config.fine_memory_patch_size
|
| 709 |
+
self.target_patch_embed = nn.Conv3d(
|
| 710 |
+
latent_channels,
|
| 711 |
+
hidden_size,
|
| 712 |
+
kernel_size=(1, target_patch, target_patch),
|
| 713 |
+
stride=(1, target_patch, target_patch),
|
| 714 |
+
)
|
| 715 |
+
self.coarse_prefix_embed = nn.Conv3d(
|
| 716 |
+
latent_channels,
|
| 717 |
+
hidden_size,
|
| 718 |
+
kernel_size=(1, coarse_patch, coarse_patch),
|
| 719 |
+
stride=(1, coarse_patch, coarse_patch),
|
| 720 |
+
)
|
| 721 |
+
self.fine_prefix_embed = nn.Conv3d(
|
| 722 |
+
latent_channels,
|
| 723 |
+
hidden_size,
|
| 724 |
+
kernel_size=(1, fine_patch, fine_patch),
|
| 725 |
+
stride=(1, fine_patch, fine_patch),
|
| 726 |
+
)
|
| 727 |
+
self.motion_prefix_embed = nn.Conv3d(
|
| 728 |
+
latent_channels,
|
| 729 |
+
hidden_size,
|
| 730 |
+
kernel_size=(1, fine_patch, fine_patch),
|
| 731 |
+
stride=(1, fine_patch, fine_patch),
|
| 732 |
+
)
|
| 733 |
+
self.coarse_scale_embedding = nn.Parameter(torch.zeros(1, 1, hidden_size))
|
| 734 |
+
self.fine_scale_embedding = nn.Parameter(torch.zeros(1, 1, hidden_size))
|
| 735 |
+
self.t_embedding = ScalarEmbedding(hidden_size)
|
| 736 |
+
self.r_embedding = ScalarEmbedding(hidden_size)
|
| 737 |
+
self.delta_embedding = ScalarEmbedding(hidden_size)
|
| 738 |
+
self.time_projection = nn.Sequential(
|
| 739 |
+
nn.Linear(hidden_size * 3, hidden_size),
|
| 740 |
+
nn.SiLU(),
|
| 741 |
+
nn.Linear(hidden_size, hidden_size),
|
| 742 |
+
)
|
| 743 |
+
self.prefix_condition = nn.Linear(hidden_size, hidden_size)
|
| 744 |
+
self.action_projection = (
|
| 745 |
+
nn.Sequential(
|
| 746 |
+
nn.Linear(model_config.action_dim, hidden_size),
|
| 747 |
+
nn.SiLU(),
|
| 748 |
+
nn.Linear(hidden_size, hidden_size),
|
| 749 |
+
)
|
| 750 |
+
if model_config.action_dim > 0
|
| 751 |
+
else None
|
| 752 |
+
)
|
| 753 |
+
self.context_projection = (
|
| 754 |
+
nn.Linear(model_config.context_dim, hidden_size, bias=False)
|
| 755 |
+
if model_config.context_dim > 0
|
| 756 |
+
else None
|
| 757 |
+
)
|
| 758 |
+
self.blocks = nn.ModuleList(
|
| 759 |
+
PrefixDiTBlock(model_config) for _ in range(model_config.depth)
|
| 760 |
+
)
|
| 761 |
+
self.output_norm = nn.RMSNorm(
|
| 762 |
+
hidden_size,
|
| 763 |
+
eps=1e-6,
|
| 764 |
+
elementwise_affine=False,
|
| 765 |
+
)
|
| 766 |
+
self.output_modulation = nn.Linear(hidden_size, hidden_size * 2)
|
| 767 |
+
self.output_projection = nn.Linear(
|
| 768 |
+
hidden_size,
|
| 769 |
+
target_patch * target_patch * latent_channels,
|
| 770 |
+
)
|
| 771 |
+
self.outcome_norm = nn.RMSNorm(hidden_size, eps=1e-6)
|
| 772 |
+
self.reward_head = nn.Linear(hidden_size, 1)
|
| 773 |
+
self.terminal_head = nn.Linear(hidden_size, 1)
|
| 774 |
+
self.reset_parameters()
|
| 775 |
+
|
| 776 |
+
def enable_activation_checkpointing(self, enabled: bool = True) -> None:
|
| 777 |
+
self.gradient_checkpointing = enabled
|
| 778 |
+
|
| 779 |
+
def reset_parameters(self) -> None:
|
| 780 |
+
for module in self.modules():
|
| 781 |
+
if isinstance(module, nn.Linear):
|
| 782 |
+
nn.init.xavier_uniform_(module.weight)
|
| 783 |
+
if module.bias is not None:
|
| 784 |
+
nn.init.zeros_(module.bias)
|
| 785 |
+
for embedding in (
|
| 786 |
+
self.target_patch_embed,
|
| 787 |
+
self.coarse_prefix_embed,
|
| 788 |
+
self.fine_prefix_embed,
|
| 789 |
+
self.motion_prefix_embed,
|
| 790 |
+
):
|
| 791 |
+
nn.init.normal_(embedding.weight, std=0.02)
|
| 792 |
+
nn.init.normal_(self.coarse_scale_embedding, std=0.02)
|
| 793 |
+
nn.init.normal_(self.fine_scale_embedding, std=0.02)
|
| 794 |
+
for block in self.blocks:
|
| 795 |
+
nn.init.zeros_(block.modulation.weight)
|
| 796 |
+
nn.init.zeros_(block.modulation.bias)
|
| 797 |
+
nn.init.zeros_(self.output_modulation.weight)
|
| 798 |
+
nn.init.zeros_(self.output_modulation.bias)
|
| 799 |
+
nn.init.zeros_(self.output_projection.weight)
|
| 800 |
+
nn.init.zeros_(self.output_projection.bias)
|
| 801 |
+
nn.init.zeros_(self.reward_head.weight)
|
| 802 |
+
nn.init.zeros_(self.reward_head.bias)
|
| 803 |
+
nn.init.zeros_(self.terminal_head.weight)
|
| 804 |
+
nn.init.zeros_(self.terminal_head.bias)
|
| 805 |
+
|
| 806 |
+
@staticmethod
|
| 807 |
+
def _patch(
|
| 808 |
+
embedding: nn.Conv3d,
|
| 809 |
+
video: torch.Tensor,
|
| 810 |
+
time_offset: int,
|
| 811 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 812 |
+
tokens = embedding(video.transpose(1, 2))
|
| 813 |
+
_, _, frames, height, width = tokens.shape
|
| 814 |
+
positions = _video_positions(
|
| 815 |
+
frames,
|
| 816 |
+
height,
|
| 817 |
+
width,
|
| 818 |
+
time_offset,
|
| 819 |
+
video.device,
|
| 820 |
+
)
|
| 821 |
+
return tokens.flatten(2).transpose(1, 2), positions
|
| 822 |
+
|
| 823 |
+
def _encode_prefix(
|
| 824 |
+
self,
|
| 825 |
+
history: torch.Tensor,
|
| 826 |
+
context: torch.Tensor | None = None,
|
| 827 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 828 |
+
if history.shape[1] != self.history_frames:
|
| 829 |
+
raise ValueError(
|
| 830 |
+
f"Expected {self.history_frames} history frames, got {history.shape[1]}."
|
| 831 |
+
)
|
| 832 |
+
coarse, coarse_positions = self._patch(
|
| 833 |
+
self.coarse_prefix_embed,
|
| 834 |
+
history,
|
| 835 |
+
0,
|
| 836 |
+
)
|
| 837 |
+
fine_frames = self.model_config.fine_history_frames
|
| 838 |
+
fine_history = history[:, -fine_frames:]
|
| 839 |
+
previous = history[:, -fine_frames - 1 : -1]
|
| 840 |
+
if previous.shape[1] != fine_frames:
|
| 841 |
+
previous = torch.cat((fine_history[:, :1], fine_history[:, :-1]), dim=1)
|
| 842 |
+
motion = fine_history - previous
|
| 843 |
+
fine, fine_positions = self._patch(
|
| 844 |
+
self.fine_prefix_embed,
|
| 845 |
+
fine_history,
|
| 846 |
+
self.history_frames - fine_frames,
|
| 847 |
+
)
|
| 848 |
+
motion_tokens, _ = self._patch(
|
| 849 |
+
self.motion_prefix_embed,
|
| 850 |
+
motion,
|
| 851 |
+
self.history_frames - fine_frames,
|
| 852 |
+
)
|
| 853 |
+
coarse = coarse + self.coarse_scale_embedding
|
| 854 |
+
fine = fine + motion_tokens + self.fine_scale_embedding
|
| 855 |
+
prefix = torch.cat((coarse, fine), dim=1)
|
| 856 |
+
positions = torch.cat((coarse_positions, fine_positions), dim=0)
|
| 857 |
+
if self.context_projection is not None and context is not None:
|
| 858 |
+
projected = self.context_projection(context)
|
| 859 |
+
prefix = torch.cat((prefix, projected), dim=1)
|
| 860 |
+
positions = torch.cat(
|
| 861 |
+
(
|
| 862 |
+
positions,
|
| 863 |
+
torch.zeros(projected.shape[1], 3, device=history.device),
|
| 864 |
+
),
|
| 865 |
+
dim=0,
|
| 866 |
+
)
|
| 867 |
+
return prefix, positions
|
| 868 |
+
|
| 869 |
+
def _condition(
|
| 870 |
+
self,
|
| 871 |
+
prefix: torch.Tensor,
|
| 872 |
+
r: torch.Tensor,
|
| 873 |
+
t: torch.Tensor,
|
| 874 |
+
) -> torch.Tensor:
|
| 875 |
+
time = self.time_projection(
|
| 876 |
+
torch.cat(
|
| 877 |
+
(
|
| 878 |
+
self.t_embedding(t),
|
| 879 |
+
self.r_embedding(r),
|
| 880 |
+
self.delta_embedding(t - r),
|
| 881 |
+
),
|
| 882 |
+
dim=-1,
|
| 883 |
+
)
|
| 884 |
+
)
|
| 885 |
+
return time + self.prefix_condition(prefix.mean(dim=1))
|
| 886 |
+
|
| 887 |
+
def build_memory_cache(
|
| 888 |
+
self,
|
| 889 |
+
history: torch.Tensor,
|
| 890 |
+
context: torch.Tensor | None = None,
|
| 891 |
+
) -> PrefixCache:
|
| 892 |
+
prefix, positions = self._encode_prefix(history, context)
|
| 893 |
+
pooled = self.prefix_condition(prefix.mean(dim=1))
|
| 894 |
+
key_values: list[tuple[torch.Tensor, torch.Tensor]] = []
|
| 895 |
+
for block in self.blocks:
|
| 896 |
+
prefix = block.advance_prefix(prefix, positions)
|
| 897 |
+
key_values.append(block.prefix_key_value(prefix, positions))
|
| 898 |
+
return PrefixCache(tuple(key_values), pooled)
|
| 899 |
+
|
| 900 |
+
def predict_outcomes(
|
| 901 |
+
self,
|
| 902 |
+
history: torch.Tensor,
|
| 903 |
+
actions: torch.Tensor | None = None,
|
| 904 |
+
frame_count: int = 1,
|
| 905 |
+
memory_cache: PrefixCache | None = None,
|
| 906 |
+
) -> tuple[torch.Tensor, torch.Tensor]:
|
| 907 |
+
if memory_cache is None:
|
| 908 |
+
prefix, _ = self._encode_prefix(history)
|
| 909 |
+
pooled = self.prefix_condition(prefix.mean(dim=1))
|
| 910 |
+
else:
|
| 911 |
+
pooled = memory_cache.pooled
|
| 912 |
+
hidden = pooled[:, None].expand(-1, frame_count, -1)
|
| 913 |
+
if self.action_projection is not None:
|
| 914 |
+
if actions is None or actions.shape[1] != frame_count:
|
| 915 |
+
raise ValueError("Outcome actions must match frame_count.")
|
| 916 |
+
hidden = hidden + self.action_projection(actions)
|
| 917 |
+
hidden = self.outcome_norm(hidden)
|
| 918 |
+
return self.reward_head(hidden).squeeze(-1), self.terminal_head(hidden).squeeze(-1)
|
| 919 |
+
|
| 920 |
+
def _unpatchify(
|
| 921 |
+
self,
|
| 922 |
+
patches: torch.Tensor,
|
| 923 |
+
frames: int,
|
| 924 |
+
height: int,
|
| 925 |
+
width: int,
|
| 926 |
+
) -> torch.Tensor:
|
| 927 |
+
batch = patches.shape[0]
|
| 928 |
+
patch = self.model_config.target_patch_size
|
| 929 |
+
patches = patches.reshape(
|
| 930 |
+
batch,
|
| 931 |
+
frames,
|
| 932 |
+
height,
|
| 933 |
+
width,
|
| 934 |
+
patch,
|
| 935 |
+
patch,
|
| 936 |
+
self.latent_channels,
|
| 937 |
+
)
|
| 938 |
+
return patches.permute(0, 1, 6, 2, 4, 3, 5).reshape(
|
| 939 |
+
batch,
|
| 940 |
+
frames,
|
| 941 |
+
self.latent_channels,
|
| 942 |
+
height * patch,
|
| 943 |
+
width * patch,
|
| 944 |
+
)
|
| 945 |
+
|
| 946 |
+
def forward(
|
| 947 |
+
self,
|
| 948 |
+
noisy_target: torch.Tensor,
|
| 949 |
+
history: torch.Tensor,
|
| 950 |
+
r: torch.Tensor,
|
| 951 |
+
t: torch.Tensor,
|
| 952 |
+
actions: torch.Tensor | None = None,
|
| 953 |
+
context: torch.Tensor | None = None,
|
| 954 |
+
memory_cache: PrefixCache | None = None,
|
| 955 |
+
) -> torch.Tensor:
|
| 956 |
+
target_volume = self.target_patch_embed(noisy_target.transpose(1, 2))
|
| 957 |
+
batch, _, frames, height, width = target_volume.shape
|
| 958 |
+
target = target_volume.flatten(2).transpose(1, 2)
|
| 959 |
+
target_positions = _video_positions(
|
| 960 |
+
frames,
|
| 961 |
+
height,
|
| 962 |
+
width,
|
| 963 |
+
self.history_frames,
|
| 964 |
+
noisy_target.device,
|
| 965 |
+
)
|
| 966 |
+
if memory_cache is None:
|
| 967 |
+
prefix, prefix_positions = self._encode_prefix(history, context)
|
| 968 |
+
condition = self._condition(prefix, r, t)
|
| 969 |
+
else:
|
| 970 |
+
prefix = None
|
| 971 |
+
prefix_positions = None
|
| 972 |
+
condition = self.time_projection(
|
| 973 |
+
torch.cat(
|
| 974 |
+
(
|
| 975 |
+
self.t_embedding(t),
|
| 976 |
+
self.r_embedding(r),
|
| 977 |
+
self.delta_embedding(t - r),
|
| 978 |
+
),
|
| 979 |
+
dim=-1,
|
| 980 |
+
)
|
| 981 |
+
) + memory_cache.pooled
|
| 982 |
+
if self.action_projection is not None:
|
| 983 |
+
if actions is None or actions.shape[:2] != (batch, frames):
|
| 984 |
+
raise ValueError("Actions must match [batch, target_frames, action_dim].")
|
| 985 |
+
action_embedding = self.action_projection(actions)
|
| 986 |
+
target = target + action_embedding.repeat_interleave(height * width, dim=1)
|
| 987 |
+
condition = condition + action_embedding.mean(dim=1)
|
| 988 |
+
for block_index, block in enumerate(self.blocks):
|
| 989 |
+
if memory_cache is None:
|
| 990 |
+
if prefix is None or prefix_positions is None:
|
| 991 |
+
raise RuntimeError("Prefix state is unavailable.")
|
| 992 |
+
if self.training and self.gradient_checkpointing:
|
| 993 |
+
prefix, target = checkpoint(
|
| 994 |
+
block,
|
| 995 |
+
prefix,
|
| 996 |
+
target,
|
| 997 |
+
condition,
|
| 998 |
+
prefix_positions,
|
| 999 |
+
target_positions,
|
| 1000 |
+
use_reentrant=False,
|
| 1001 |
+
)
|
| 1002 |
+
else:
|
| 1003 |
+
prefix, target = block(
|
| 1004 |
+
prefix,
|
| 1005 |
+
target,
|
| 1006 |
+
condition,
|
| 1007 |
+
prefix_positions,
|
| 1008 |
+
target_positions,
|
| 1009 |
+
)
|
| 1010 |
+
else:
|
| 1011 |
+
target = block.advance_target(
|
| 1012 |
+
target,
|
| 1013 |
+
condition,
|
| 1014 |
+
target_positions,
|
| 1015 |
+
memory_cache.key_values[block_index],
|
| 1016 |
+
)
|
| 1017 |
+
shift, scale = self.output_modulation(F.silu(condition)).chunk(2, dim=-1)
|
| 1018 |
+
target = self.output_norm(target) * (1.0 + scale[:, None]) + shift[:, None]
|
| 1019 |
+
return self._unpatchify(
|
| 1020 |
+
self.output_projection(target),
|
| 1021 |
+
frames,
|
| 1022 |
+
height,
|
| 1023 |
+
width,
|
| 1024 |
+
)
|
| 1025 |
+
|
| 1026 |
+
|
| 1027 |
+
CausalVideoDiT = CausalLatentVideoDiT
|
| 1028 |
+
@torch.inference_mode()
|
| 1029 |
+
def sample_next_latent(
|
| 1030 |
+
model: nn.Module,
|
| 1031 |
+
history: torch.Tensor,
|
| 1032 |
+
steps: int = 2,
|
| 1033 |
+
actions: torch.Tensor | None = None,
|
| 1034 |
+
generator: torch.Generator | None = None,
|
| 1035 |
+
window_frames: int = 1,
|
| 1036 |
+
memory_cache: object | None = None,
|
| 1037 |
+
instantaneous: bool = False,
|
| 1038 |
+
) -> torch.Tensor:
|
| 1039 |
+
if steps < 1:
|
| 1040 |
+
raise ValueError("steps must be positive.")
|
| 1041 |
+
batch, _, channels, height, width = history.shape
|
| 1042 |
+
state = torch.randn(
|
| 1043 |
+
(batch, window_frames, channels, height, width),
|
| 1044 |
+
device=history.device,
|
| 1045 |
+
dtype=history.dtype,
|
| 1046 |
+
generator=generator,
|
| 1047 |
+
)
|
| 1048 |
+
boundaries = torch.linspace(1.0, 0.0, steps + 1, device=history.device)
|
| 1049 |
+
for index in range(steps):
|
| 1050 |
+
t_value = boundaries[index]
|
| 1051 |
+
r_value = boundaries[index + 1]
|
| 1052 |
+
t = t_value.expand(batch)
|
| 1053 |
+
r = r_value.expand(batch)
|
| 1054 |
+
model_r = t if instantaneous else r
|
| 1055 |
+
average_velocity = model(
|
| 1056 |
+
state,
|
| 1057 |
+
history,
|
| 1058 |
+
model_r,
|
| 1059 |
+
t,
|
| 1060 |
+
actions=actions,
|
| 1061 |
+
memory_cache=memory_cache,
|
| 1062 |
+
)
|
| 1063 |
+
state = state - (t_value - r_value).to(state.dtype) * average_velocity
|
| 1064 |
+
return state
|
| 1065 |
+
|
| 1066 |
+
def _parse_args() -> argparse.Namespace:
|
| 1067 |
+
parser = argparse.ArgumentParser(description="Diffusion Breakout.")
|
| 1068 |
+
parser.add_argument("--repo", default=DEFAULT_REPO)
|
| 1069 |
+
parser.add_argument("--revision", default="main")
|
| 1070 |
+
parser.add_argument("--local-dir", default=None)
|
| 1071 |
+
parser.add_argument("--steps", type=int, default=1)
|
| 1072 |
+
parser.add_argument("--frames", type=int, default=0, help="0 = until window closed")
|
| 1073 |
+
parser.add_argument("--seed", type=int, default=None)
|
| 1074 |
+
parser.add_argument("--window-scale", type=int, default=6)
|
| 1075 |
+
parser.add_argument("--fps-cap", type=float, default=0.0)
|
| 1076 |
+
return parser.parse_args()
|
| 1077 |
+
|
| 1078 |
+
|
| 1079 |
+
def _download(repo: str, revision: str, local_dir: str | None) -> Path:
|
| 1080 |
+
if local_dir:
|
| 1081 |
+
root = Path(local_dir)
|
| 1082 |
+
root.mkdir(parents=True, exist_ok=True)
|
| 1083 |
+
for name in ("config.json", "ema.safetensors"):
|
| 1084 |
+
hf_hub_download(
|
| 1085 |
+
repo_id=repo,
|
| 1086 |
+
filename=name,
|
| 1087 |
+
revision=revision,
|
| 1088 |
+
local_dir=str(root),
|
| 1089 |
+
)
|
| 1090 |
+
return root
|
| 1091 |
+
config_path = Path(
|
| 1092 |
+
hf_hub_download(repo_id=repo, filename="config.json", revision=revision)
|
| 1093 |
+
)
|
| 1094 |
+
hf_hub_download(repo_id=repo, filename="ema.safetensors", revision=revision)
|
| 1095 |
+
return config_path.parent
|
| 1096 |
+
|
| 1097 |
+
|
| 1098 |
+
def _load_settings(config_path: Path) -> tuple[VideoConfig, ModelConfig, CodecSettings]:
|
| 1099 |
+
raw = json.loads(config_path.read_text(encoding="utf-8"))
|
| 1100 |
+
video = VideoConfig(**{key: raw["video"][key] for key in VideoConfig.__dataclass_fields__})
|
| 1101 |
+
model = ModelConfig(**{key: raw["model"][key] for key in ModelConfig.__dataclass_fields__})
|
| 1102 |
+
codec_raw = raw["codec"]
|
| 1103 |
+
codec = CodecSettings(
|
| 1104 |
+
model_id=codec_raw.get("model_id", "madebyollin/sdxl-vae-fp16-fix"),
|
| 1105 |
+
subfolder=codec_raw.get("subfolder", "") or "",
|
| 1106 |
+
revision=codec_raw.get("revision", "main"),
|
| 1107 |
+
latent_channels=int(codec_raw.get("latent_channels", 4)),
|
| 1108 |
+
spatial_compression=int(codec_raw.get("spatial_compression", 8)),
|
| 1109 |
+
frame_batch_size=int(codec_raw.get("frame_batch_size", 256)),
|
| 1110 |
+
sample_posterior=bool(codec_raw.get("sample_posterior", False)),
|
| 1111 |
+
enable_slicing=bool(codec_raw.get("enable_slicing", True)),
|
| 1112 |
+
enable_tiling=bool(codec_raw.get("enable_tiling", False)),
|
| 1113 |
+
)
|
| 1114 |
+
return video, model, codec
|
| 1115 |
+
|
| 1116 |
+
|
| 1117 |
+
def _frames_to_tensor(frames: np.ndarray, device: torch.device) -> torch.Tensor:
|
| 1118 |
+
tensor = torch.from_numpy(frames.copy()).permute(0, 3, 1, 2).float()
|
| 1119 |
+
return tensor.div_(127.5).sub_(1.0).unsqueeze(0).to(device, non_blocking=True)
|
| 1120 |
+
|
| 1121 |
+
|
| 1122 |
+
def _tensor_to_image(frame: torch.Tensor) -> Image.Image:
|
| 1123 |
+
array = frame.float().clamp(-1.0, 1.0).add(1.0).mul(127.5)
|
| 1124 |
+
return Image.fromarray(array.byte().permute(1, 2, 0).cpu().numpy(), mode="RGB")
|
| 1125 |
+
|
| 1126 |
+
|
| 1127 |
+
def _inject_ball(image: Image.Image, position: tuple[float, float]) -> Image.Image:
|
| 1128 |
+
image = image.copy()
|
| 1129 |
+
draw = ImageDraw.Draw(image)
|
| 1130 |
+
radius = max(3.0, min(image.width, image.height) * 0.025)
|
| 1131 |
+
x, y = position
|
| 1132 |
+
draw.ellipse(
|
| 1133 |
+
(x - radius, y - radius, x + radius, y + radius),
|
| 1134 |
+
fill=(255, 209, 72),
|
| 1135 |
+
outline=(255, 247, 196),
|
| 1136 |
+
width=max(1, round(radius * 0.25)),
|
| 1137 |
+
)
|
| 1138 |
+
return image
|
| 1139 |
+
|
| 1140 |
+
|
| 1141 |
+
def _rgb_image_to_frame_tensor(image: Image.Image, device: torch.device) -> torch.Tensor:
|
| 1142 |
+
array = np.asarray(image, dtype=np.uint8).copy()
|
| 1143 |
+
return (
|
| 1144 |
+
torch.from_numpy(array)
|
| 1145 |
+
.permute(2, 0, 1)
|
| 1146 |
+
.float()
|
| 1147 |
+
.div_(127.5)
|
| 1148 |
+
.sub_(1.0)
|
| 1149 |
+
.to(device, non_blocking=True)
|
| 1150 |
+
)
|
| 1151 |
+
|
| 1152 |
+
|
| 1153 |
+
def _inject_ball_into_history(
|
| 1154 |
+
history: deque[torch.Tensor],
|
| 1155 |
+
codec: FrameAutoencoderCodec,
|
| 1156 |
+
position: tuple[float, float],
|
| 1157 |
+
) -> Image.Image:
|
| 1158 |
+
history_tensor = torch.stack(tuple(history), dim=1)
|
| 1159 |
+
device = history_tensor.device
|
| 1160 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 1161 |
+
decoded = codec.decode(history_tensor)
|
| 1162 |
+
painted_images: list[Image.Image] = []
|
| 1163 |
+
painted_frames: list[torch.Tensor] = []
|
| 1164 |
+
for index in range(decoded.shape[1]):
|
| 1165 |
+
image = _inject_ball(_tensor_to_image(decoded[0, index]), position)
|
| 1166 |
+
painted_images.append(image)
|
| 1167 |
+
painted_frames.append(_rgb_image_to_frame_tensor(image, device))
|
| 1168 |
+
rgb = torch.stack(painted_frames, dim=0).unsqueeze(0)
|
| 1169 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 1170 |
+
latents = codec.encode(rgb)
|
| 1171 |
+
history.clear()
|
| 1172 |
+
history.extend(latents[:, index] for index in range(latents.shape[1]))
|
| 1173 |
+
return painted_images[-1]
|
| 1174 |
+
|
| 1175 |
+
|
| 1176 |
+
def _encode_ball_frame(
|
| 1177 |
+
image: Image.Image,
|
| 1178 |
+
position: tuple[float, float],
|
| 1179 |
+
codec: FrameAutoencoderCodec,
|
| 1180 |
+
device: torch.device,
|
| 1181 |
+
) -> tuple[Image.Image, torch.Tensor]:
|
| 1182 |
+
painted = _inject_ball(image, position)
|
| 1183 |
+
rgb = _rgb_image_to_frame_tensor(painted, device).reshape(
|
| 1184 |
+
1, 1, 3, painted.height, painted.width
|
| 1185 |
+
)
|
| 1186 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 1187 |
+
latent = codec.encode(rgb)[:, 0]
|
| 1188 |
+
return painted, latent
|
| 1189 |
+
|
| 1190 |
+
|
| 1191 |
+
class LiveWindow:
|
| 1192 |
+
def __init__(self, scale: int) -> None:
|
| 1193 |
+
self.scale = max(1, scale)
|
| 1194 |
+
self.is_open = True
|
| 1195 |
+
self.root = tk.Tk()
|
| 1196 |
+
self.root.title("Diffusion Breakout")
|
| 1197 |
+
self.label = tk.Label(self.root, background="black")
|
| 1198 |
+
self.label.pack()
|
| 1199 |
+
self.photo: ImageTk.PhotoImage | None = None
|
| 1200 |
+
self.generated_width = 0
|
| 1201 |
+
self.generated_height = 0
|
| 1202 |
+
self.pending_click: tuple[float, float] | None = None
|
| 1203 |
+
self.pressed: set[str] = set()
|
| 1204 |
+
self.label.bind("<Button-1>", self._on_click)
|
| 1205 |
+
self.root.bind("<KeyPress>", self._on_key_press)
|
| 1206 |
+
self.root.bind("<KeyRelease>", self._on_key_release)
|
| 1207 |
+
self.root.protocol("WM_DELETE_WINDOW", self.close)
|
| 1208 |
+
self.root.focus_force()
|
| 1209 |
+
|
| 1210 |
+
def _on_key_press(self, event: tk.Event) -> None:
|
| 1211 |
+
key = str(event.keysym).lower()
|
| 1212 |
+
if key in {"w", "s", "a", "d", "left", "right"}:
|
| 1213 |
+
self.pressed.add(key)
|
| 1214 |
+
|
| 1215 |
+
def _on_key_release(self, event: tk.Event) -> None:
|
| 1216 |
+
self.pressed.discard(str(event.keysym).lower())
|
| 1217 |
+
|
| 1218 |
+
def action(self) -> tuple[float, float]:
|
| 1219 |
+
negative = bool(self.pressed & {"w", "a", "left"})
|
| 1220 |
+
positive = bool(self.pressed & {"s", "d", "right"})
|
| 1221 |
+
if negative and positive:
|
| 1222 |
+
return (0.0, 0.0)
|
| 1223 |
+
return (float(negative), float(positive))
|
| 1224 |
+
|
| 1225 |
+
def _on_click(self, event: tk.Event) -> None:
|
| 1226 |
+
if self.generated_width <= 0:
|
| 1227 |
+
return
|
| 1228 |
+
x = (float(event.x) / self.scale) % self.generated_width
|
| 1229 |
+
y = min(max(float(event.y) / self.scale, 0.0), self.generated_height - 1.0)
|
| 1230 |
+
self.pending_click = (x, y)
|
| 1231 |
+
|
| 1232 |
+
def consume_click(self) -> tuple[float, float] | None:
|
| 1233 |
+
click = self.pending_click
|
| 1234 |
+
self.pending_click = None
|
| 1235 |
+
return click
|
| 1236 |
+
|
| 1237 |
+
def close(self) -> None:
|
| 1238 |
+
if self.is_open:
|
| 1239 |
+
self.is_open = False
|
| 1240 |
+
self.root.destroy()
|
| 1241 |
+
|
| 1242 |
+
def show(self, image: Image.Image, frame_index: int, latency_ms: float) -> bool:
|
| 1243 |
+
if not self.is_open:
|
| 1244 |
+
return False
|
| 1245 |
+
self.generated_width, self.generated_height = image.size
|
| 1246 |
+
display = image.resize(
|
| 1247 |
+
(image.width * self.scale, image.height * self.scale),
|
| 1248 |
+
Image.Resampling.NEAREST,
|
| 1249 |
+
)
|
| 1250 |
+
self.photo = ImageTk.PhotoImage(display)
|
| 1251 |
+
self.label.configure(image=self.photo)
|
| 1252 |
+
held = "+".join(sorted(self.pressed)).upper()
|
| 1253 |
+
held_text = f" [{held}]" if held else ""
|
| 1254 |
+
self.root.title(
|
| 1255 |
+
f"Diffusion Breakout — frame {frame_index + 1} {latency_ms:.0f} ms{held_text}"
|
| 1256 |
+
)
|
| 1257 |
+
try:
|
| 1258 |
+
self.root.update_idletasks()
|
| 1259 |
+
self.root.update()
|
| 1260 |
+
except tk.TclError:
|
| 1261 |
+
self.is_open = False
|
| 1262 |
+
return self.is_open
|
| 1263 |
+
|
| 1264 |
+
|
| 1265 |
+
@torch.inference_mode()
|
| 1266 |
+
def main() -> None:
|
| 1267 |
+
args = _parse_args()
|
| 1268 |
+
if not torch.cuda.is_available() or not torch.cuda.is_bf16_supported():
|
| 1269 |
+
raise SystemExit("Need a CUDA GPU with BF16 support.")
|
| 1270 |
+
|
| 1271 |
+
print(f"Downloading {args.repo} …")
|
| 1272 |
+
checkpoint = _download(args.repo, args.revision, args.local_dir)
|
| 1273 |
+
video_cfg, model_cfg, codec_cfg = _load_settings(checkpoint / "config.json")
|
| 1274 |
+
weights = checkpoint / "ema.safetensors"
|
| 1275 |
+
if not weights.exists():
|
| 1276 |
+
raise SystemExit(f"Missing {weights}")
|
| 1277 |
+
|
| 1278 |
+
device = torch.device("cuda")
|
| 1279 |
+
torch.set_float32_matmul_precision("high")
|
| 1280 |
+
seed = args.seed if args.seed is not None else random.SystemRandom().randrange(2**31)
|
| 1281 |
+
simulation = BreakoutSimulation.from_seed(video_cfg, seed, at_start=True)
|
| 1282 |
+
history_rgb = _frames_to_tensor(
|
| 1283 |
+
simulation.sample_frames(video_cfg.history_frames),
|
| 1284 |
+
device,
|
| 1285 |
+
)
|
| 1286 |
+
|
| 1287 |
+
print("Loading VAE + DiT …")
|
| 1288 |
+
codec = FrameAutoencoderCodec(codec_cfg, device, torch.bfloat16)
|
| 1289 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 1290 |
+
latents = codec.encode(history_rgb)
|
| 1291 |
+
history: deque[torch.Tensor] = deque(
|
| 1292 |
+
(latents[:, index] for index in range(video_cfg.history_frames)),
|
| 1293 |
+
maxlen=video_cfg.history_frames,
|
| 1294 |
+
)
|
| 1295 |
+
model = (
|
| 1296 |
+
CausalLatentVideoDiT(
|
| 1297 |
+
model_cfg,
|
| 1298 |
+
latent_channels=codec.channels,
|
| 1299 |
+
history_frames=codec.latent_frame_count(video_cfg.history_frames),
|
| 1300 |
+
)
|
| 1301 |
+
.to(device)
|
| 1302 |
+
.eval()
|
| 1303 |
+
)
|
| 1304 |
+
model.load_state_dict(load_file(str(weights)), strict=True)
|
| 1305 |
+
|
| 1306 |
+
steps = max(1, args.steps)
|
| 1307 |
+
frame_limit = args.frames if args.frames > 0 else 10_000_000
|
| 1308 |
+
window = LiveWindow(args.window_scale)
|
| 1309 |
+
generator = torch.Generator(device=device).manual_seed(seed + 7)
|
| 1310 |
+
injected_position: tuple[float, float] | None = None
|
| 1311 |
+
force_ball_frames = 0
|
| 1312 |
+
print("Ready. Click the window, then hold A/D or arrow keys.")
|
| 1313 |
+
|
| 1314 |
+
for frame_index in range(frame_limit):
|
| 1315 |
+
click = window.consume_click()
|
| 1316 |
+
if click is not None:
|
| 1317 |
+
preview = _inject_ball_into_history(history, codec, click)
|
| 1318 |
+
injected_position = click
|
| 1319 |
+
force_ball_frames = video_cfg.history_frames
|
| 1320 |
+
if not window.show(preview, frame_index, 0.0):
|
| 1321 |
+
break
|
| 1322 |
+
history_tensor = torch.stack(tuple(history), dim=1)
|
| 1323 |
+
action = torch.tensor(
|
| 1324 |
+
[window.action()],
|
| 1325 |
+
device=device,
|
| 1326 |
+
dtype=torch.float32,
|
| 1327 |
+
).unsqueeze(0)
|
| 1328 |
+
torch.cuda.synchronize()
|
| 1329 |
+
started = time.perf_counter()
|
| 1330 |
+
with torch.autocast(device_type="cuda", dtype=torch.bfloat16):
|
| 1331 |
+
memory_cache = model.build_memory_cache(history_tensor)
|
| 1332 |
+
prediction = sample_next_latent(
|
| 1333 |
+
model,
|
| 1334 |
+
history_tensor,
|
| 1335 |
+
steps,
|
| 1336 |
+
actions=action if model_cfg.action_dim > 0 else None,
|
| 1337 |
+
generator=generator,
|
| 1338 |
+
window_frames=1,
|
| 1339 |
+
memory_cache=memory_cache,
|
| 1340 |
+
instantaneous=True,
|
| 1341 |
+
)
|
| 1342 |
+
decoded = codec.decode(prediction)
|
| 1343 |
+
torch.cuda.synchronize()
|
| 1344 |
+
latency_ms = (time.perf_counter() - started) * 1000
|
| 1345 |
+
image = _tensor_to_image(decoded[0, 0])
|
| 1346 |
+
frame_latent = prediction[:, 0]
|
| 1347 |
+
if injected_position is not None and force_ball_frames > 0:
|
| 1348 |
+
image, frame_latent = _encode_ball_frame(
|
| 1349 |
+
image, injected_position, codec, device
|
| 1350 |
+
)
|
| 1351 |
+
force_ball_frames -= 1
|
| 1352 |
+
if force_ball_frames == 0:
|
| 1353 |
+
injected_position = None
|
| 1354 |
+
history.append(frame_latent)
|
| 1355 |
+
if not window.show(image, frame_index, latency_ms):
|
| 1356 |
+
break
|
| 1357 |
+
if args.fps_cap > 0:
|
| 1358 |
+
target = 1.0 / args.fps_cap
|
| 1359 |
+
elapsed = time.perf_counter() - started
|
| 1360 |
+
if elapsed < target:
|
| 1361 |
+
time.sleep(target - elapsed)
|
| 1362 |
+
if window.is_open:
|
| 1363 |
+
window.close()
|
| 1364 |
+
print("Done.")
|
| 1365 |
+
|
| 1366 |
+
|
| 1367 |
+
if __name__ == "__main__":
|
| 1368 |
+
main()
|
preview.png
ADDED
|