kerzgrr commited on
Commit
e93d6be
·
verified ·
1 Parent(s): bfedcf3

Unconditional Pong base EMA step 1050 + live infer

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