multimodalart HF Staff commited on
Commit
01f8b0f
ยท
verified ยท
1 Parent(s): c4104be

ForgeWM few-step action-conditioned world model demo

Browse files
.gitattributes CHANGED
@@ -36,3 +36,6 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
36
  demo_images/cave.png filter=lfs diff=lfs merge=lfs -text
37
  demo_images/forest.png filter=lfs diff=lfs merge=lfs -text
38
  demo_images/plains.png filter=lfs diff=lfs merge=lfs -text
 
 
 
 
36
  demo_images/cave.png filter=lfs diff=lfs merge=lfs -text
37
  demo_images/forest.png filter=lfs diff=lfs merge=lfs -text
38
  demo_images/plains.png filter=lfs diff=lfs merge=lfs -text
39
+ examples/cave.png filter=lfs diff=lfs merge=lfs -text
40
+ examples/forest.png filter=lfs diff=lfs merge=lfs -text
41
+ examples/plains.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,58 +1,60 @@
1
  ---
2
- title: ForgeWM World Model
3
  emoji: ๐ŸŽฎ
4
  colorFrom: yellow
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.25.0
8
  app_file: app.py
 
9
  python_version: "3.12"
10
- short_description: Drive a Minecraft world model with keyboard + mouse
11
  startup_duration_timeout: 1h
12
  pinned: false
13
  license: apache-2.0
 
 
 
14
  ---
15
 
16
- # ๐ŸŽฎ ForgeWM โ€” a Minecraft world model you can drive
17
 
18
- Interactive demo of **[ForgeWM](https://huggingface.co/ForgeWM/ForgeWM)**
19
- (*ForgeWM: Progressive Causal Training for Few-Step Action-Conditioned Video
20
- World Models*).
 
21
 
22
- Give the model **one reference frame** plus an **action track** (keyboard /
23
- mouse actions, one per second) and it rolls the world forward autoregressively,
24
- one block-causal chunk at a time.
 
 
25
 
26
- ## What's running
27
 
28
- - **Backbone**: Matrix-Game 2 / Wan2.1-1.3B block-causal DiT (30 layers, dim 1536)
29
- with the MG2 action module (keyboard cross-attention + mouse channel condition).
30
- - **Students**: `ForgeWM-4` (`stage3/model.pt`, 4 denoising steps) and
31
- `ForgeWM-1` (`1step/model.pt`, 1 step + the paper's First-Frame Enhancement
32
- on chunk 0), distilled with progressive causal training
33
- (bidirectional SFT โ†’ teacher-forced causal AR โ†’ consistency distillation โ†’
34
- on-policy DMD).
35
- - **Rollout regime**: 352ร—640, 12 fps, chunks of 3 latent frames (12 pixel
36
- frames), sliding local attention window of 6 latent frames, `sink_size=0`,
37
- `timestep_shift=5.0`, warped denoising schedule โ€” matching
38
- `inference.py` / `pipeline/causal_inference.py` upstream.
39
 
40
- ## Difference from the reference CLI
41
 
42
- The upstream `inference.py` holds **one** action for the whole clip. Because
43
- `CausalInferencePipeline.cond_current()` slices the action tensors per raw
44
- frame, this Space feeds a **time-varying action track** instead: one action per
45
- one-second causal chunk, so you can compose e.g.
46
- `forward x3, turn_right x2, forward`. Action semantics (key flags, ยฑ0.10
47
- camera deltas) are taken verbatim from the reference `make_action()` palette.
48
 
49
- ## Credits
 
 
 
50
 
51
- - ForgeWM โ€” https://github.com/asdfo123/ForgeWM (Apache-2.0). The three
52
- reference frames in `demo_images/` are the ForgeWM repo's own demo images.
53
- - Matrix-Game 2.0 โ€” https://huggingface.co/Skywork/Matrix-Game-2.0
54
- - Wan2.1 โ€” https://github.com/Wan-Video/Wan2.1
55
- - GameFactory โ€” the Minecraft action-conditioned training data source.
56
 
57
- Model code under `wan/`, `utils/`, `pipeline/`, `configs/` is vendored from the
58
- ForgeWM repository; see `LICENSE` and `NOTICE`.
 
 
 
 
1
  ---
2
+ title: ForgeWM
3
  emoji: ๐ŸŽฎ
4
  colorFrom: yellow
5
  colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.25.0
8
  app_file: app.py
9
+ short_description: Few-step action-conditioned Minecraft world model
10
  python_version: "3.12"
 
11
  startup_duration_timeout: 1h
12
  pinned: false
13
  license: apache-2.0
14
+ models:
15
+ - ForgeWM/ForgeWM
16
+ - Skywork/Matrix-Game-2.0
17
  ---
18
 
19
+ # ForgeWM โ€” Progressive Causal Training for Few-Step Action-Conditioned Video World Models
20
 
21
+ Interactive demo for [`ForgeWM/ForgeWM`](https://huggingface.co/ForgeWM/ForgeWM)
22
+ ([paper](https://huggingface.co/papers/2608.14022) ยท
23
+ [code](https://github.com/asdfo123/ForgeWM) ยท
24
+ [project page](https://asdfo123.github.io/ForgeWM/)).
25
 
26
+ Feed the model **one Minecraft frame** plus a short **action script**
27
+ (`forward`, `turn_right`, `look_up`, โ€ฆ) and it rolls the world forward
28
+ autoregressively: a block-causal diffusion transformer denoises 3 latent frames
29
+ (โ‰ˆ1 second of 12 fps video) per step, conditioned on the keyboard/mouse actions
30
+ for that window and a sliding KV cache of the past.
31
 
32
+ Three released students are selectable:
33
 
34
+ | Student | Denoising steps | First-Frame Enhancement |
35
+ |---|---|---|
36
+ | ForgeWM-4 | 4 | โ€“ |
37
+ | ForgeWM-2 | 2 | 4-step schedule on block 0 |
38
+ | ForgeWM-1 | 1 | 4-step schedule on block 0 |
 
 
 
 
 
 
39
 
40
+ ## Fidelity notes
41
 
42
+ The app mirrors the repo's own `inference.py` and
43
+ `pipeline/causal_inference.py`: 352ร—640, `num_frame_per_block=3`,
44
+ `local_attn_size=6`, `sink_size=0`, `warp_denoising_step=true`, the published
45
+ `denoising_step_list` / `denoising_step_list_first_chunk` schedules, MG2-style
46
+ conditioning (CLIP ViT-H visual context + 4-channel mask concatenated with the
47
+ first-frame latent), and the exact Minecraft action palette (`CAM_VALUE=0.10`).
48
 
49
+ Attention runs on PyTorch SDPA rather than FlashAttention-2. In the KV-cached
50
+ inference path the repo calls `attention(q, k, v)` with no causal/window
51
+ arguments, so the two are numerically equivalent โ€” causality is enforced by the
52
+ cache layout, not the kernel.
53
 
54
+ ## Credits
 
 
 
 
55
 
56
+ - Model and reference frames (`examples/*.png`): the
57
+ [ForgeWM repository](https://github.com/asdfo123/ForgeWM), Apache-2.0.
58
+ - Base weights: [Skywork/Matrix-Game-2.0](https://huggingface.co/Skywork/Matrix-Game-2.0),
59
+ distributed under Skywork's own terms.
60
+ - Vendored `wan/`, `pipeline/`, `utils/` are Apache-2.0; see `NOTICE`.
app.py CHANGED
@@ -1,549 +1,483 @@
1
- """ForgeWM โ€” a playable Minecraft world model you drive with keyboard + mouse.
2
 
3
- Faithful port of the reference implementation in
4
- https://github.com/asdfo123/ForgeWM (`inference.py` + `pipeline/causal_inference.py`)
5
- onto ZeroGPU, extended so that the action condition can change over the rollout
6
- instead of being a single action held for the whole clip.
 
 
 
7
  """
8
 
9
  import os
10
 
11
- # NOTE: expandable_segments is deliberately NOT enabled. On this Space it makes
12
- # the very first attention allocation inside the ZeroGPU (MIG-virtualised)
13
- # worker fail with `NVML_SUCCESS == r INTERNAL ASSERT FAILED`; the plain caching
14
- # allocator handles the ~12 GB working set fine.
15
- os.environ.pop("PYTORCH_CUDA_ALLOC_CONF", None)
16
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
17
 
18
- import spaces # noqa: E402 (must precede torch / CUDA-touching imports)
19
 
20
- import random # noqa: E402
21
- import re # noqa: E402
22
  import tempfile # noqa: E402
23
- import threading # noqa: E402
24
  import time # noqa: E402
 
25
 
26
  import gradio as gr # noqa: E402
27
  import imageio.v2 as imageio # noqa: E402
28
  import numpy as np # noqa: E402
29
  import torch # noqa: E402
30
- from huggingface_hub import hf_hub_download, snapshot_download # noqa: E402
 
31
  from omegaconf import OmegaConf # noqa: E402
32
  from PIL import Image # noqa: E402
33
- from torchvision.transforms import ( # noqa: E402
34
- Compose,
35
- InterpolationMode,
36
- Normalize,
37
- Resize,
38
- ToTensor,
39
- )
40
 
41
- # โ”€โ”€โ”€ Weights โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
 
42
 
43
- MG2_REPO = "Skywork/Matrix-Game-2.0"
44
- FORGEWM_REPO = "ForgeWM/ForgeWM"
 
 
 
 
 
 
 
 
 
 
45
 
46
- BASE_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "mg2_base")
47
- os.makedirs(BASE_DIR, exist_ok=True)
48
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
49
 
50
- def _link(src: str, name: str) -> str:
51
- dst = os.path.join(BASE_DIR, name)
52
- if os.path.islink(dst):
53
- os.remove(dst)
54
- if not os.path.exists(dst):
55
- os.symlink(src, dst)
56
- return dst
57
-
58
-
59
- print("[boot] fetching Matrix-Game-2.0 base weights โ€ฆ", flush=True)
60
- _link(hf_hub_download(MG2_REPO, "base_model/base_config.json"), "base_config.json")
61
- _link(
62
- hf_hub_download(MG2_REPO, "base_model/diffusion_pytorch_model.safetensors"),
63
- "diffusion_pytorch_model.safetensors",
64
- )
65
- VAE_PATH = hf_hub_download(MG2_REPO, "Wan2.1_VAE.pth")
66
- CLIP_PATH = hf_hub_download(
67
- MG2_REPO, "models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth"
68
- )
69
- TOKENIZER_DIR = snapshot_download(MG2_REPO, allow_patterns=["xlm-roberta-large/*"])
70
- TOKENIZER_DIR = os.path.join(TOKENIZER_DIR, "xlm-roberta-large")
71
-
72
- print("[boot] fetching ForgeWM student checkpoints โ€ฆ", flush=True)
73
- CKPT_4STEP = hf_hub_download(FORGEWM_REPO, "stage3/model.pt")
74
- CKPT_1STEP = hf_hub_download(FORGEWM_REPO, "1step/model.pt")
75
-
76
- # โ”€โ”€โ”€ Model โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
77
-
78
- from pipeline import CausalInferencePipeline # noqa: E402
79
- from utils.wan_wrapper import WanVAEWrapper # noqa: E402
80
-
81
- DEVICE = "cuda"
82
- DTYPE = torch.bfloat16
83
- HEIGHT, WIDTH = 352, 640
84
- FPS = 12
85
- NUM_FRAME_PER_BLOCK = 3 # latent frames per causal chunk
86
- RAW_PER_BLOCK = 12 # pixel frames per causal chunk (VAE tcr = 4)
87
- CAM_VALUE = 0.10 # reference camera delta magnitude
88
- MAX_CHUNKS = 12
89
 
90
- torch.set_grad_enabled(False)
91
 
 
 
 
 
 
92
 
93
- def _load_config(name: str):
94
- cfg = OmegaConf.merge(
95
- OmegaConf.load("configs/default.yaml"), OmegaConf.load(f"configs/{name}")
96
- )
97
- cfg.model_kwargs.model_name = BASE_DIR
98
- return cfg
99
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
100
 
101
- print("[boot] building VAE + CLIP โ€ฆ", flush=True)
102
  VAE = WanVAEWrapper(
103
- vae_path=VAE_PATH, clip_checkpoint_path=CLIP_PATH, clip_tokenizer_path=TOKENIZER_DIR
104
- )
105
- VAE = VAE.to(device=DEVICE, dtype=DTYPE).eval()
106
- VAE.clip.model = VAE.clip.model.to(DEVICE)
107
-
108
-
109
- def _build_pipeline(config_name: str, checkpoint_path: str):
110
- cfg = _load_config(config_name)
111
- pipe = CausalInferencePipeline(cfg, device=DEVICE, vae=VAE)
112
- state = torch.load(checkpoint_path, map_location="cpu", weights_only=False)
113
- gen_sd = state.get("generator", state.get("generator_ema", state)) if isinstance(
114
- state, dict
115
- ) else state
116
- fixed = {
117
- k.replace("._fsdp_wrapped_module.", ".").replace(
118
- "._checkpoint_wrapped_module.", "."
119
- ): v
120
- for k, v in gen_sd.items()
121
- }
 
 
 
 
 
 
 
 
 
 
 
 
122
  missing, unexpected = pipe.generator.load_state_dict(fixed, strict=False)
123
- print(
124
- f"[boot] {config_name}: missing={len(missing)} unexpected={len(unexpected)}",
125
- flush=True,
126
- )
127
  del state, gen_sd, fixed
128
- pipe.generator = pipe.generator.to(device=DEVICE, dtype=DTYPE).eval()
129
- return pipe
130
-
131
-
132
- print("[boot] building ForgeWM-4 (stage3) โ€ฆ", flush=True)
133
- PIPE_4 = _build_pipeline("stage3_dmd.yaml", CKPT_4STEP)
134
- print("[boot] building ForgeWM-1 (1step) โ€ฆ", flush=True)
135
- PIPE_1 = _build_pipeline("stage3_dmd_1step.yaml", CKPT_1STEP)
136
-
137
- MODELS = {
138
- "ForgeWM-4 ยท 4-step (best quality)": PIPE_4,
139
- "ForgeWM-1 ยท 1-step (fastest)": PIPE_1,
140
- }
141
- DEFAULT_MODEL = "ForgeWM-4 ยท 4-step (best quality)"
142
-
143
- _GPU_LOCK = threading.Lock()
144
-
145
- # โ”€โ”€โ”€ Action palette (Minecraft / MG2 schema: 2-D mouse, 6 key flags) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
146
-
147
- ACTIONS = [
148
- "forward",
149
- "back",
150
- "left",
151
- "right",
152
- "turn_left",
153
- "turn_right",
154
- "look_up",
155
- "look_down",
156
- "forward_turn_right",
157
- "random",
158
- "no_action",
159
- ]
160
- ALIASES = {
161
- "w": "forward",
162
- "s": "back",
163
- "a": "left",
164
- "d": "right",
165
- "idle": "no_action",
166
- "none": "no_action",
167
- "stay": "no_action",
168
- "turnleft": "turn_left",
169
- "turnright": "turn_right",
170
- "lookup": "look_up",
171
- "lookdown": "look_down",
172
- }
173
-
174
- BUTTONS = [
175
- ("โฌ†๏ธ Forward (W)", "forward"),
176
- ("โฌ‡๏ธ Back (S)", "back"),
177
- ("โฌ…๏ธ Strafe left (A)", "left"),
178
- ("โžก๏ธ Strafe right (D)", "right"),
179
- ("โ†บ Turn left", "turn_left"),
180
- ("โ†ป Turn right", "turn_right"),
181
- ("๐Ÿ”ผ Look up", "look_up"),
182
- ("๐Ÿ”ฝ Look down", "look_down"),
183
- ("โฌ†๏ธโ†ป Forward + turn right", "forward_turn_right"),
184
- ("โธ๏ธ Stand still", "no_action"),
185
- ("๐ŸŽฒ Random", "random"),
186
- ]
187
 
 
 
 
 
 
188
 
189
- _REPEAT_ONLY = re.compile(r"^[x*ร—]?(\d+)$")
190
- _REPEAT_SUFFIX = re.compile(r"^(.*?)[\s_\-]*[x*ร—](\d+)$")
191
 
 
 
 
 
 
 
192
 
193
- def parse_script(script: str) -> list:
194
- """Parse an action script into one action per one-second chunk.
 
 
195
 
196
- Accepts comma / whitespace separated action names, with an optional
197
- ``xN`` repeat, attached or detached: ``forward x3, turn_right x2, forward``.
198
- """
199
- tokens = [t for t in re.split(r"[,\s]+", (script or "").strip().lower()) if t]
200
- actions = []
201
- for tok in tokens:
202
- m = _REPEAT_ONLY.match(tok)
203
- if m:
204
- if not actions:
205
- raise gr.Error(f"'{tok}' has no action to repeat.")
206
- actions.extend([actions[-1]] * max(0, int(m.group(1)) - 1))
207
- continue
208
- repeat = 1
209
- m = _REPEAT_SUFFIX.match(tok)
210
- if m and m.group(1):
211
- tok, repeat = m.group(1), int(m.group(2))
212
- tok = ALIASES.get(tok, tok)
213
- if tok not in ACTIONS:
214
- raise gr.Error(
215
- f"Unknown action '{tok}'. Available actions: {', '.join(ACTIONS)}."
216
- )
217
- actions.extend([tok] * max(1, repeat))
218
- if not actions:
219
- raise gr.Error(
220
- "The action track is empty โ€” click the action buttons to build a "
221
- "sequence, or type e.g. 'forward x3, turn_right x2'."
222
- )
223
- return actions[:MAX_CHUNKS]
224
 
225
 
226
- def _chunk_of_raw_frame(r: int) -> int:
227
- """Map a raw (pixel) frame index onto its causal chunk index.
 
 
 
228
 
229
- Chunk k covers latent frames 3k..3k+2, which the pipeline feeds with raw
230
- action frames up to ``1 + 4 * (3k + 2)``. Chunk 0 therefore owns raw frames
231
- 0..8 and every later chunk owns 12 raw frames.
232
- """
233
- return 0 if r < 9 else (r + 3) // RAW_PER_BLOCK
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
234
 
235
 
236
- def build_action_tensors(actions, num_raw_frames, cam_value, seed):
237
- """Build [1, T, 2] mouse and [1, T, 6] keyboard tensors from the script.
238
 
239
- Semantics are exactly the reference `make_action()` palette, applied
240
- per-chunk instead of once for the whole clip.
 
241
  """
242
- mouse = torch.zeros(1, num_raw_frames, 2)
243
- keyboard = torch.zeros(1, num_raw_frames, 6)
244
- rng = torch.Generator().manual_seed(int(seed))
245
- for r in range(num_raw_frames):
246
- a = actions[min(_chunk_of_raw_frame(r), len(actions) - 1)]
247
- if a == "forward":
248
- keyboard[0, r, 0] = 1.0
249
- elif a == "back":
250
- keyboard[0, r, 1] = 1.0
251
- elif a == "left":
252
- keyboard[0, r, 2] = 1.0
253
- elif a == "right":
254
- keyboard[0, r, 3] = 1.0
255
- elif a == "turn_right":
256
- mouse[0, r, 1] = cam_value
257
- elif a == "turn_left":
258
- mouse[0, r, 1] = -cam_value
259
- elif a == "look_up":
260
- mouse[0, r, 0] = cam_value
261
- elif a == "look_down":
262
- mouse[0, r, 0] = -cam_value
263
- elif a == "forward_turn_right":
264
- keyboard[0, r, 0] = 1.0
265
- mouse[0, r, 1] = cam_value
266
- elif a == "random":
267
- mouse[0, r] = (torch.rand(2, generator=rng) - 0.5) * (2 * cam_value)
268
- keyboard[0, r, :4] = (torch.rand(4, generator=rng) > 0.5).float()
269
- # "no_action" leaves both channels at zero
270
  return mouse, keyboard
271
 
272
 
273
- _TRANSFORM = Compose(
274
- [
275
- Resize((HEIGHT, WIDTH), interpolation=InterpolationMode.BILINEAR),
276
- ToTensor(),
277
- Normalize(mean=[0.5, 0.5, 0.5], std=[0.5, 0.5, 0.5]),
278
- ]
279
- )
280
-
 
 
 
 
 
 
 
 
 
 
 
 
 
281
 
282
- def build_conditional_dict(pixel, num_frames, mouse_cond, keyboard_cond):
283
- """Reference `build_conditional_dict()` โ€” MG2 three-pathway I2V condition."""
284
- num_pixel_frames = (num_frames - 1) * 4 + 1
285
- visual_context = VAE.encode_visual_context_from_pixels(pixel).to(DTYPE)
286
  first_frame = pixel[:, 0:1]
287
- pad_pix = torch.zeros(
288
- 1, num_pixel_frames - 1, 3, pixel.shape[3], pixel.shape[4],
289
- device=pixel.device, dtype=pixel.dtype,
290
- )
291
- padded = torch.cat([first_frame, pad_pix], dim=1).permute(0, 2, 1, 3, 4)
292
- img_cond = VAE.encode_to_latent(padded).to(DTYPE)
293
 
294
  _, _, _, h_lat, w_lat = img_cond.shape
295
- mask = torch.zeros(1, num_frames, 4, h_lat, w_lat, device=DEVICE, dtype=DTYPE)
 
296
  mask[:, 0:1] = 1
297
- cond_concat = torch.cat([mask, img_cond], dim=2)
298
  return {
299
  "visual_context": visual_context,
300
- "cond_concat": cond_concat,
301
- "mouse_condition": mouse_cond.to(device=DEVICE, dtype=DTYPE),
302
- "keyboard_condition": keyboard_cond.to(device=DEVICE, dtype=DTYPE),
303
  }
304
 
305
 
306
- def _estimate_duration(reference_image, action_script="forward", *args, **kwargs):
307
- try:
308
- chunks = len(parse_script(action_script or "forward"))
309
- except Exception:
310
- chunks = 7
311
- return int(min(180, 40 + 6 * chunks))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
 
313
 
314
- @spaces.GPU(duration=_estimate_duration)
 
 
 
 
 
315
  def generate(
316
- reference_image: str,
317
- action_script: str = "forward",
318
- model_choice: str = DEFAULT_MODEL,
319
- camera_speed: float = CAM_VALUE,
 
 
 
 
320
  seed: int = 0,
321
- randomize_seed: bool = True,
322
  progress=gr.Progress(track_tqdm=True),
323
  ):
324
- """Roll out the ForgeWM world model from a reference frame and an action script.
325
 
326
  Args:
327
- reference_image: Path to the opening frame the world is generated from.
328
- action_script: Comma-separated actions, one per second of video, with an
329
- optional `xN` repeat (e.g. "forward x3, turn_right x2"). Valid
330
- actions: forward, back, left, right, turn_left, turn_right, look_up,
331
- look_down, forward_turn_right, random, no_action.
332
- model_choice: Which few-step student to run (4-step or 1-step).
333
- camera_speed: Magnitude of the mouse/camera delta per frame.
334
- seed: RNG seed for the initial noise.
335
- randomize_seed: Draw a fresh random seed instead of using `seed`.
336
 
337
  Returns:
338
- A tuple of (path to the generated mp4, a short markdown run summary).
339
  """
340
- if reference_image is None:
341
- raise gr.Error("Please provide a reference frame to start the world from.")
342
-
343
- actions = parse_script(action_script)
344
- num_chunks = len(actions)
345
- num_frames = num_chunks * NUM_FRAME_PER_BLOCK
346
- num_raw_frames = (num_frames - 1) * 4 + 1
347
-
348
- if randomize_seed:
349
- seed = random.randint(0, 2**31 - 1)
350
- seed = int(seed)
351
-
352
- pipeline = MODELS.get(model_choice, PIPE_4)
353
-
354
- image = Image.open(reference_image).convert("RGB")
355
- pixel = _TRANSFORM(image).unsqueeze(0).unsqueeze(0).to(device=DEVICE, dtype=DTYPE)
356
-
357
- mouse_cond, keyboard_cond = build_action_tensors(
358
- actions, num_raw_frames, float(camera_speed), seed
359
  )
360
 
361
- t0 = time.perf_counter()
362
- # `torch.set_grad_enabled` is thread-local, so the module-scope call does not
363
- # reach the ZeroGPU worker thread โ€” without this guard the 30-layer causal
364
- # rollout keeps every activation alive and OOMs.
365
- with _GPU_LOCK, torch.no_grad():
366
- conditional_dict = build_conditional_dict(
367
- pixel, num_frames, mouse_cond, keyboard_cond
368
- )
369
- torch.manual_seed(seed)
370
- noise = torch.randn(
371
- [1, num_frames, 16, HEIGHT // 8, WIDTH // 8], device=DEVICE, dtype=DTYPE
372
- )
373
- video = pipeline.inference(
374
- noise=noise, conditional_dict=conditional_dict, return_latents=False
375
- )
376
- video_np = (
377
- video[0].permute(0, 2, 3, 1).cpu().float().numpy() * 255
378
- ).clip(0, 255).astype(np.uint8)
379
- if hasattr(pipeline.vae, "model"):
380
- pipeline.vae.model.clear_cache()
381
- try:
382
- free_b, total_b = torch.cuda.mem_get_info()
383
- print(
384
- f"[gen] peak={torch.cuda.max_memory_allocated() / 2**30:.1f}GiB "
385
- f"free={free_b / 2**30:.1f}/{total_b / 2**30:.1f}GiB",
386
- flush=True,
387
- )
388
- except Exception as exc: # pragma: no cover - diagnostics only
389
- print(f"[gen] memory probe failed: {exc}", flush=True)
390
- elapsed = time.perf_counter() - t0
391
-
392
- out_path = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False).name
393
- writer = imageio.get_writer(
394
- out_path, fps=FPS, codec="libx264", quality=8, macro_block_size=None
395
- )
396
- for frame in video_np:
397
- writer.append_data(frame)
398
- writer.close()
399
 
400
- info = (
401
- f"**{model_choice}** ยท {len(video_np)} frames @ {FPS} fps "
402
- f"({len(video_np) / FPS:.1f}s, {WIDTH}ร—{HEIGHT}) ยท "
403
- f"{num_chunks} causal chunk(s) ยท seed `{seed}` ยท "
404
- f"generated in **{elapsed:.1f}s** \n"
405
- f"Actions: `{' โ†’ '.join(actions)}`"
 
 
 
 
 
 
 
 
 
 
406
  )
407
- return out_path, info
408
-
409
 
410
- # โ”€โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
411
 
 
412
  CSS = """
413
- #col-container { max-width: 1180px; margin: 0 auto; }
414
- .dark .gradio-container { color: var(--body-text-color); }
415
- #pad button { min-width: 0 !important; }
416
- """
417
-
418
- INTRO = """# ๐ŸŽฎ ForgeWM โ€” a Minecraft world model you can drive
419
-
420
- **[ForgeWM](https://huggingface.co/ForgeWM/ForgeWM)** is a few-step, action-conditioned video world model:
421
- give it one frame plus a keyboard/mouse action track and it rolls the world forward, one causal chunk at a time.
422
- Distilled with progressive causal training (bidirectional SFT โ†’ teacher-forced causal AR โ†’ consistency
423
- distillation โ†’ on-policy DMD) from a Matrix-Game 2 / Wan2.1-1.3B backbone.
424
-
425
- Build an action track below โ€” **each action lasts one second** of generated video.
426
-
427
- ๐Ÿ“„ [Paper](https://huggingface.co/papers/2608.14022) ยท ๐Ÿ’ป [Code](https://github.com/asdfo123/ForgeWM) ยท ๐ŸŒ [Project page](https://asdfo123.github.io/ForgeWM/)
428
  """
429
 
 
 
 
 
 
 
 
 
 
 
 
430
 
431
- def append_action(script: str, action: str) -> str:
432
- current = [t for t in (script or "").replace(",", " ").split() if t]
433
- if len(current) >= MAX_CHUNKS:
434
- gr.Warning(f"The action track is capped at {MAX_CHUNKS} seconds.")
435
- return script
436
- current.append(action)
437
- return ", ".join(current)
438
-
439
-
440
- def undo_action(script: str) -> str:
441
- current = [t for t in (script or "").replace(",", " ").split() if t]
442
- return ", ".join(current[:-1])
443
-
444
-
445
- with gr.Blocks(title="ForgeWM world model") as demo:
446
- with gr.Column(elem_id="col-container"):
447
- gr.Markdown(INTRO)
448
-
449
- with gr.Row():
450
- with gr.Column(scale=1):
451
- reference_image = gr.Image(
452
- label="Reference frame (the world starts here)",
453
- type="filepath",
454
- height=300,
455
- )
456
- action_script = gr.Textbox(
457
- label="Action track โ€” one action per second",
458
- value="forward, forward, turn_right, turn_right, forward",
459
- lines=2,
460
- info="Click the controls below, or type directly "
461
- "(e.g. 'forward x3, turn_right x2').",
462
- )
463
- with gr.Row(elem_id="pad"):
464
- undo_btn = gr.Button("โคบ Undo", size="sm", scale=1)
465
- clear_btn = gr.Button("โœ• Clear", size="sm", scale=1)
466
- with gr.Row(elem_id="pad"):
467
- pad_buttons = [
468
- (gr.Button(label, size="sm"), action)
469
- for label, action in BUTTONS[:4]
470
- ]
471
- with gr.Row(elem_id="pad"):
472
- pad_buttons += [
473
- (gr.Button(label, size="sm"), action)
474
- for label, action in BUTTONS[4:8]
475
- ]
476
- with gr.Row(elem_id="pad"):
477
- pad_buttons += [
478
- (gr.Button(label, size="sm"), action)
479
- for label, action in BUTTONS[8:]
480
- ]
481
- run_btn = gr.Button("โ–ถ Roll out the world", variant="primary")
482
-
483
- with gr.Column(scale=1):
484
- video_out = gr.Video(
485
- label="Generated rollout", autoplay=True, loop=True, height=380
486
- )
487
- info_out = gr.Markdown()
488
-
489
- with gr.Accordion("Advanced settings", open=False):
490
- model_choice = gr.Radio(
491
- choices=list(MODELS.keys()),
492
- value=DEFAULT_MODEL,
493
- label="Few-step student",
494
- info="ForgeWM-1 runs a single denoising step per chunk (with the "
495
- "paper's First-Frame Enhancement on chunk 0); ForgeWM-4 runs four.",
496
- )
497
- camera_speed = gr.Slider(
498
- 0.02, 0.30, value=CAM_VALUE, step=0.01,
499
- label="Camera speed (mouse delta per frame)",
500
  )
 
501
  with gr.Row():
502
- seed = gr.Number(label="Seed", value=0, precision=0)
503
- randomize_seed = gr.Checkbox(label="Randomize seed", value=True)
504
-
505
- gr.Examples(
506
- examples=[
507
- ["demo_images/forest.png", "forward, forward, turn_right, turn_right, forward"],
508
- ["demo_images/plains.png", "forward x4, look_up, forward x2"],
509
- ["demo_images/cave.png", "forward x3, turn_left, turn_left, forward x2"],
510
- ["demo_images/forest.png", "random x7"],
511
- ],
512
- inputs=[reference_image, action_script],
513
- outputs=[video_out, info_out],
514
- fn=generate,
515
- cache_examples=True,
516
- cache_mode="lazy",
517
- label="Examples (reference frames from the ForgeWM repo)",
518
- )
519
-
520
- gr.Markdown(
521
- "Rollouts run at 352ร—640, 12 fps, in causal chunks of 3 latent frames "
522
- "(12 pixel frames) with a 6-frame sliding attention window โ€” exactly the "
523
- "regime the Stage-3 students were trained in."
524
- )
525
-
526
- for btn, action in pad_buttons:
527
- btn.click(
528
- lambda s, a=action: append_action(s, a),
529
- inputs=action_script,
530
- outputs=action_script,
531
- api_name=False,
532
- )
533
- undo_btn.click(undo_action, inputs=action_script, outputs=action_script,
534
- api_name=False)
535
- clear_btn.click(lambda: "", outputs=action_script, api_name=False)
536
-
537
- run_btn.click(
538
- generate,
539
- inputs=[reference_image, action_script, model_choice, camera_speed, seed,
540
- randomize_seed],
541
- outputs=[video_out, info_out],
542
- api_name="generate",
543
- concurrency_limit=1,
544
  )
545
 
546
- if __name__ == "__main__":
547
- demo.launch(
548
- theme=gr.themes.Citrus(), css=CSS, mcp_server=True, show_error=True
 
 
549
  )
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """ForgeWM โ€” few-step, action-conditioned Minecraft world model.
2
 
3
+ Gradio demo for `ForgeWM/ForgeWM` (paper: "ForgeWM: Progressive Causal Training
4
+ for Few-Step Action-Conditioned Video World Models").
5
+
6
+ Faithful to the repo's own `inference.py` / `pipeline/causal_inference.py`:
7
+ same 352x640 resolution, 3-latent-frame causal blocks, sliding window of 6
8
+ latent frames, the released `warp_denoising_step` schedules, and First-Frame
9
+ Enhancement for the 1-/2-step students.
10
  """
11
 
12
  import os
13
 
14
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
 
 
 
 
15
  os.environ.setdefault("TOKENIZERS_PARALLELISM", "false")
16
 
17
+ import spaces # noqa: E402 (must precede torch)
18
 
19
+ import gc # noqa: E402
20
+ import shutil # noqa: E402
21
  import tempfile # noqa: E402
 
22
  import time # noqa: E402
23
+ from typing import List, Tuple # noqa: E402
24
 
25
  import gradio as gr # noqa: E402
26
  import imageio.v2 as imageio # noqa: E402
27
  import numpy as np # noqa: E402
28
  import torch # noqa: E402
29
+ import torch.nn.functional as F # noqa: E402
30
+ from huggingface_hub import hf_hub_download # noqa: E402
31
  from omegaconf import OmegaConf # noqa: E402
32
  from PIL import Image # noqa: E402
 
 
 
 
 
 
 
33
 
34
+ from pipeline import CausalInferencePipeline
35
+ from utils.wan_wrapper import WanVAEWrapper
36
 
37
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ constants โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
38
+ HEIGHT, WIDTH = 352, 640
39
+ FPS = 12
40
+ VAE_TCR = 4 # VAE temporal compression ratio
41
+ FRAMES_PER_BLOCK = 3 # latent frames per causal block
42
+ CAM_VALUE = 0.10 # camera delta magnitude, from inference.py
43
+
44
+ MINECRAFT_ACTIONS = [
45
+ "forward", "back", "left", "right",
46
+ "turn_right", "turn_left", "look_up", "look_down",
47
+ "forward_turn_right", "random", "no_action",
48
+ ]
49
 
50
+ BASE_REPO = "Skywork/Matrix-Game-2.0"
51
+ FORGEWM_REPO = "ForgeWM/ForgeWM"
52
 
53
+ VARIANTS = {
54
+ "ForgeWM-4 ยท 4 steps": ("configs/stage3_dmd.yaml", "stage3/model.pt"),
55
+ "ForgeWM-2 ยท 2 steps": ("configs/stage3_dmd_2step.yaml", "2step/model.pt"),
56
+ "ForgeWM-1 ยท 1 step": ("configs/stage3_dmd_1step.yaml", "1step/model.pt"),
57
+ }
58
+ DEFAULT_VARIANT = "ForgeWM-4 ยท 4 steps"
59
+
60
+ # Measured ballpark on the ZeroGPU Blackwell card; used only to size the
61
+ # @spaces.GPU reservation.
62
+ SECONDS_PER_CHUNK = {
63
+ "ForgeWM-4 ยท 4 steps": 1.5,
64
+ "ForgeWM-2 ยท 2 steps": 1.0,
65
+ "ForgeWM-1 ยท 1 step": 0.8,
66
+ }
67
+ FIXED_OVERHEAD = 22.0 # CLIP + VAE encode/decode + mp4 mux
68
 
69
+ CKPT_DIR = os.path.join(os.getcwd(), "ckpts", "MG2-base")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
 
 
71
 
72
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ weight preparation โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
73
+ def _link(src: str, dst: str) -> None:
74
+ if os.path.lexists(dst):
75
+ os.remove(dst)
76
+ os.symlink(src, dst)
77
 
 
 
 
 
 
 
78
 
79
+ def _purge(path: str) -> None:
80
+ """Drop a hub blob from local disk once its tensors are in memory."""
81
+ try:
82
+ real = os.path.realpath(path)
83
+ if os.path.isfile(real):
84
+ os.remove(real)
85
+ if os.path.islink(path):
86
+ os.remove(path)
87
+ except OSError as exc: # pragma: no cover
88
+ print(f"[disk] could not purge {path}: {exc}")
89
+
90
+
91
+ def _disk() -> str:
92
+ total, used, free = shutil.disk_usage("/")
93
+ return f"disk {used / 2**30:.1f}G used / {free / 2**30:.1f}G free"
94
+
95
+
96
+ os.makedirs(os.path.join(CKPT_DIR, "xlm-roberta-large"), exist_ok=True)
97
+
98
+ print(f"[setup] fetching Matrix-Game-2.0 base weights โ€ฆ ({_disk()})", flush=True)
99
+ _dit = hf_hub_download(BASE_REPO, "base_model/diffusion_pytorch_model.safetensors")
100
+ _link(_dit, os.path.join(CKPT_DIR, "diffusion_pytorch_model.safetensors"))
101
+ _link(hf_hub_download(BASE_REPO, "base_model/base_config.json"),
102
+ os.path.join(CKPT_DIR, "base_config.json"))
103
+ _vae_path = hf_hub_download(BASE_REPO, "Wan2.1_VAE.pth")
104
+ _clip_path = hf_hub_download(
105
+ BASE_REPO, "models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth")
106
+ for _tok in ("sentencepiece.bpe.model", "special_tokens_map.json",
107
+ "tokenizer.json", "tokenizer_config.json"):
108
+ _link(hf_hub_download(BASE_REPO, f"xlm-roberta-large/{_tok}"),
109
+ os.path.join(CKPT_DIR, "xlm-roberta-large", _tok))
110
+ print(f"[setup] base weights ready ({_disk()})", flush=True)
111
+
112
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ models โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
113
+ torch.set_grad_enabled(False)
114
+ DTYPE = torch.bfloat16
115
 
116
+ print("[setup] building VAE + CLIP โ€ฆ", flush=True)
117
  VAE = WanVAEWrapper(
118
+ vae_path=_vae_path,
119
+ clip_checkpoint_path=_clip_path,
120
+ clip_tokenizer_path=os.path.join(CKPT_DIR, "xlm-roberta-large"),
121
+ ).eval()
122
+ VAE = VAE.to(device="cuda", dtype=DTYPE)
123
+ # `WanVAEWrapper.clip` is a plain object, not a submodule, so `.to()` above does
124
+ # not reach it. Move it explicitly (in fp32, as the repo does) so ZeroGPU packs
125
+ # it with everything else instead of paying a 4.8 GB host copy per request.
126
+ VAE.clip.model = VAE.clip.model.to("cuda")
127
+ gc.collect()
128
+ print(f"[setup] VAE + CLIP on GPU ({_disk()})", flush=True)
129
+
130
+
131
+ def _build_variant(name: str) -> CausalInferencePipeline:
132
+ cfg_path, ckpt_file = VARIANTS[name]
133
+ config = OmegaConf.merge(OmegaConf.load("configs/default.yaml"),
134
+ OmegaConf.load(cfg_path))
135
+ config.model_kwargs.model_name = CKPT_DIR
136
+
137
+ pipe = CausalInferencePipeline(config, device=torch.device("cuda"), vae=VAE)
138
+
139
+ ckpt = hf_hub_download(FORGEWM_REPO, ckpt_file)
140
+ try:
141
+ state = torch.load(ckpt, map_location="cpu", weights_only=True)
142
+ except Exception:
143
+ state = torch.load(ckpt, map_location="cpu", weights_only=False)
144
+ gen_sd = state.get("generator", state.get("generator_ema", state)) \
145
+ if isinstance(state, dict) else state
146
+ fixed = {k.replace("._fsdp_wrapped_module.", ".")
147
+ .replace("._checkpoint_wrapped_module.", "."): v
148
+ for k, v in gen_sd.items()}
149
  missing, unexpected = pipe.generator.load_state_dict(fixed, strict=False)
150
+ print(f"[{name}] loaded: missing={len(missing)} unexpected={len(unexpected)}",
151
+ flush=True)
 
 
152
  del state, gen_sd, fixed
153
+ gc.collect()
154
+ _purge(ckpt)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
155
 
156
+ pipe.generator = pipe.generator.to(device="cuda", dtype=DTYPE).eval()
157
+ pipe.vae = VAE
158
+ gc.collect()
159
+ print(f"[{name}] ready ({_disk()})", flush=True)
160
+ return pipe
161
 
 
 
162
 
163
+ PIPELINES = {}
164
+ for _name in VARIANTS:
165
+ try:
166
+ PIPELINES[_name] = _build_variant(_name)
167
+ except Exception as exc: # keep the Space usable if one student fails
168
+ print(f"[setup] FAILED to load {_name}: {exc!r}", flush=True)
169
 
170
+ if not PIPELINES:
171
+ raise RuntimeError("No ForgeWM student could be loaded.")
172
+ if DEFAULT_VARIANT not in PIPELINES:
173
+ DEFAULT_VARIANT = next(iter(PIPELINES))
174
 
175
+ # The base DiT safetensors were only needed to instantiate the architecture;
176
+ # every parameter has since been overwritten by the ForgeWM checkpoints.
177
+ _purge(os.path.join(CKPT_DIR, "diffusion_pytorch_model.safetensors"))
178
+ print(f"[setup] {len(PIPELINES)} variant(s) ready ({_disk()})", flush=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
 
180
 
181
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ action scripting โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
182
+ def make_action(action_type: str, num_raw_frames: int) -> Tuple[torch.Tensor, torch.Tensor]:
183
+ """Minecraft action palette (mouse_dim_in=2), verbatim from inference.py."""
184
+ mouse = torch.zeros(1, num_raw_frames, 2)
185
+ keyboard = torch.zeros(1, num_raw_frames, 6)
186
 
187
+ if action_type == "forward":
188
+ keyboard[:, :, 0] = 1.0
189
+ elif action_type == "back":
190
+ keyboard[:, :, 1] = 1.0
191
+ elif action_type == "left":
192
+ keyboard[:, :, 2] = 1.0
193
+ elif action_type == "right":
194
+ keyboard[:, :, 3] = 1.0
195
+ elif action_type == "turn_right":
196
+ mouse[:, :, 1] = CAM_VALUE
197
+ elif action_type == "turn_left":
198
+ mouse[:, :, 1] = -CAM_VALUE
199
+ elif action_type == "look_up":
200
+ mouse[:, :, 0] = CAM_VALUE
201
+ elif action_type == "look_down":
202
+ mouse[:, :, 0] = -CAM_VALUE
203
+ elif action_type == "forward_turn_right":
204
+ keyboard[:, :, 0] = 1.0
205
+ mouse[:, :, 1] = CAM_VALUE
206
+ elif action_type == "random":
207
+ torch.manual_seed(42)
208
+ mouse = (torch.rand(1, num_raw_frames, 2) - 0.5) * (2 * CAM_VALUE)
209
+ keyboard[:, :, :4] = (torch.rand(1, num_raw_frames, 4) > 0.5).float()
210
+ elif action_type == "no_action":
211
+ pass
212
+ else:
213
+ raise gr.Error(f"Unknown action '{action_type}'.")
214
+ return mouse, keyboard
215
 
216
 
217
+ def _chunk_start(chunk_index: int) -> int:
218
+ """First raw (pixel) frame owned by causal block `chunk_index`.
219
 
220
+ Latent frame 0 maps to raw frame 0; latent frame f>=1 maps to raw frames
221
+ 4f-3 โ€ฆ 4f. A block spans 3 latent frames, so block c covers raw frames
222
+ [12c-3, 12c+9) โ€” and [0, 9) for c == 0.
223
  """
224
+ return 0 if chunk_index == 0 else 12 * chunk_index - 3
225
+
226
+
227
+ def build_action_track(segments: List[Tuple[str, int]]) -> Tuple[torch.Tensor, torch.Tensor]:
228
+ total_chunks = sum(n for _, n in segments)
229
+ num_raw = _chunk_start(total_chunks)
230
+ mouse = torch.zeros(1, num_raw, 2)
231
+ keyboard = torch.zeros(1, num_raw, 6)
232
+ chunk = 0
233
+ for action, count in segments:
234
+ for _ in range(count):
235
+ lo, hi = _chunk_start(chunk), _chunk_start(chunk + 1)
236
+ m, k = make_action(action, hi - lo)
237
+ mouse[:, lo:hi] = m
238
+ keyboard[:, lo:hi] = k
239
+ chunk += 1
 
 
 
 
 
 
 
 
 
 
 
 
240
  return mouse, keyboard
241
 
242
 
243
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ preprocessing โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
244
+ def load_reference_frame(image_path: str) -> torch.Tensor:
245
+ """Aspect-preserving resize + centre crop to 352x640, scaled to [-1, 1]."""
246
+ image = Image.open(image_path).convert("RGB")
247
+ arr = torch.from_numpy(np.asarray(image)).permute(2, 0, 1)[None].float() / 255.0
248
+ _, _, h, w = arr.shape
249
+ if h / w > HEIGHT / WIDTH:
250
+ new_w, new_h = WIDTH, max(HEIGHT, int(round(h * WIDTH / w)))
251
+ else:
252
+ new_h, new_w = HEIGHT, max(WIDTH, int(round(w * HEIGHT / h)))
253
+ arr = F.interpolate(arr, size=(new_h, new_w), mode="bilinear", align_corners=False)
254
+ top, left = (new_h - HEIGHT) // 2, (new_w - WIDTH) // 2
255
+ arr = arr[:, :, top:top + HEIGHT, left:left + WIDTH]
256
+ arr = (arr - 0.5) / 0.5
257
+ return arr.unsqueeze(0).to(device="cuda", dtype=DTYPE) # [1, 1, 3, H, W]
258
+
259
+
260
+ def build_conditional_dict(pipe, pixel, num_frames, mouse_cond, keyboard_cond):
261
+ """MG2-style conditioning: CLIP context + first-frame latent + mask."""
262
+ num_pixel_frames = (num_frames - 1) * VAE_TCR + 1
263
+ visual_context = pipe.vae.encode_visual_context_from_pixels(pixel).to(DTYPE)
264
 
 
 
 
 
265
  first_frame = pixel[:, 0:1]
266
+ pad = torch.zeros(1, num_pixel_frames - 1, 3, pixel.shape[3], pixel.shape[4],
267
+ device=pixel.device, dtype=DTYPE)
268
+ padded = torch.cat([first_frame, pad], dim=1).permute(0, 2, 1, 3, 4)
269
+ img_cond = pipe.vae.encode_to_latent(padded).to(DTYPE)
 
 
270
 
271
  _, _, _, h_lat, w_lat = img_cond.shape
272
+ mask = torch.zeros(1, num_frames, 4, h_lat, w_lat,
273
+ device=pixel.device, dtype=DTYPE)
274
  mask[:, 0:1] = 1
 
275
  return {
276
  "visual_context": visual_context,
277
+ "cond_concat": torch.cat([mask, img_cond], dim=2),
278
+ "mouse_condition": mouse_cond.to(device="cuda", dtype=DTYPE),
279
+ "keyboard_condition": keyboard_cond.to(device="cuda", dtype=DTYPE),
280
  }
281
 
282
 
283
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ core โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
284
+ @torch.no_grad()
285
+ def _rollout(image_path, variant, segments, seed):
286
+ if image_path is None:
287
+ raise gr.Error("Please provide a reference frame.")
288
+ if variant not in PIPELINES:
289
+ variant = DEFAULT_VARIANT
290
+ segments = [(a, int(n)) for a, n in segments if int(n) > 0]
291
+ total_chunks = sum(n for _, n in segments)
292
+ if total_chunks < 1:
293
+ raise gr.Error("Give at least one action segment a non-zero length.")
294
+ if total_chunks > 12:
295
+ raise gr.Error("Keep the total rollout at 12 chunks or fewer.")
296
+
297
+ pipe = PIPELINES[variant]
298
+ num_frames = total_chunks * FRAMES_PER_BLOCK
299
+
300
+ pixel = load_reference_frame(image_path)
301
+ mouse, keyboard = build_action_track(segments)
302
+ cond = build_conditional_dict(pipe, pixel, num_frames, mouse, keyboard)
303
+
304
+ torch.manual_seed(int(seed))
305
+ noise = torch.randn([1, num_frames, 16, HEIGHT // 8, WIDTH // 8],
306
+ device="cuda", dtype=DTYPE)
307
+
308
+ start = time.time()
309
+ video = pipe.inference(noise=noise, conditional_dict=cond, return_latents=False)
310
+ torch.cuda.synchronize()
311
+ elapsed = time.time() - start
312
+ pipe.vae.model.clear_cache()
313
+
314
+ frames = (video[0].permute(0, 2, 3, 1).float().cpu().numpy() * 255)
315
+ frames = frames.clip(0, 255).astype(np.uint8)
316
+ out_path = os.path.join(tempfile.mkdtemp(), "forgewm.mp4")
317
+ writer = imageio.get_writer(out_path, fps=FPS, codec="libx264",
318
+ quality=8, macro_block_size=None)
319
+ for frame in frames:
320
+ writer.append_data(frame)
321
+ writer.close()
322
+
323
+ script = " โ†’ ".join(f"`{a}` ร—{n}" for a, n in segments)
324
+ info = (
325
+ f"**{variant}** ยท {total_chunks} causal blocks ยท {num_frames} latent "
326
+ f"frames โ†’ {len(frames)} pixel frames ({len(frames) / FPS:.1f}s @ {FPS} fps)\n\n"
327
+ f"Action script: {script}\n\n"
328
+ f"Rollout + VAE decode: **{elapsed:.2f}s** "
329
+ f"({1000 * elapsed / total_chunks:.0f} ms per block, decode included) ยท seed `{int(seed)}`"
330
+ )
331
+ return out_path, info
332
+
333
+
334
+ def _estimate(variant, chunks) -> int:
335
+ per = SECONDS_PER_CHUNK.get(variant, 1.5)
336
+ return int(FIXED_OVERHEAD + per * max(1, min(int(chunks), 12)) + 8)
337
+
338
+
339
+ def _duration_main(*args):
340
+ variant = args[1] if len(args) > 1 else DEFAULT_VARIANT
341
+ chunks = sum(int(args[i]) for i in (3, 5, 7) if len(args) > i)
342
+ return _estimate(variant, chunks)
343
 
344
 
345
+ def _duration_example(*args):
346
+ chunks = sum(int(args[i]) for i in (2, 4, 6) if len(args) > i)
347
+ return _estimate(DEFAULT_VARIANT, chunks)
348
+
349
+
350
+ @spaces.GPU(duration=_duration_main)
351
  def generate(
352
+ image: str,
353
+ variant: str,
354
+ action_1: str,
355
+ chunks_1: int,
356
+ action_2: str,
357
+ chunks_2: int,
358
+ action_3: str,
359
+ chunks_3: int,
360
  seed: int = 0,
 
361
  progress=gr.Progress(track_tqdm=True),
362
  ):
363
+ """Roll out an action-conditioned Minecraft video from one reference frame.
364
 
365
  Args:
366
+ image: Path to the reference frame that anchors the world.
367
+ variant: Which few-step ForgeWM student to sample with.
368
+ action_1: Action held during the first segment of the rollout.
369
+ chunks_1: Length of the first segment, in 3-latent-frame blocks (~1s each).
370
+ action_2: Action held during the second segment.
371
+ chunks_2: Length of the second segment, in blocks.
372
+ action_3: Action held during the third segment.
373
+ chunks_3: Length of the third segment, in blocks.
374
+ seed: Random seed for the initial noise.
375
 
376
  Returns:
377
+ An mp4 of the generated rollout and a markdown summary of the run.
378
  """
379
+ return _rollout(
380
+ image, variant,
381
+ [(action_1, chunks_1), (action_2, chunks_2), (action_3, chunks_3)],
382
+ seed,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
383
  )
384
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
385
 
386
+ @spaces.GPU(duration=_duration_example)
387
+ def generate_example(
388
+ image: str,
389
+ action_1: str,
390
+ chunks_1: int,
391
+ action_2: str,
392
+ chunks_2: int,
393
+ action_3: str,
394
+ chunks_3: int,
395
+ progress=gr.Progress(track_tqdm=True),
396
+ ):
397
+ """Run a bundled example with the default student and seed 0."""
398
+ return _rollout(
399
+ image, DEFAULT_VARIANT,
400
+ [(action_1, chunks_1), (action_2, chunks_2), (action_3, chunks_3)],
401
+ 0,
402
  )
 
 
403
 
 
404
 
405
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ UI โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
406
  CSS = """
407
+ .gradio-container { max-width: 1200px !important; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
408
  """
409
 
410
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS, title="ForgeWM") as demo:
411
+ gr.Markdown(
412
+ "# ๐ŸŽฎ ForgeWM โ€” few-step action-conditioned world model\n"
413
+ "Give it **one Minecraft frame** and a short **action script**; the "
414
+ "block-causal diffusion transformer rolls the world forward at "
415
+ "**1, 2 or 4 denoising steps** per 3-frame block.\n\n"
416
+ "[Paper](https://huggingface.co/papers/2608.14022) ยท "
417
+ "[Model](https://huggingface.co/ForgeWM/ForgeWM) ยท "
418
+ "[Code](https://github.com/asdfo123/ForgeWM) ยท "
419
+ "[Project page](https://asdfo123.github.io/ForgeWM/)"
420
+ )
421
 
422
+ with gr.Row():
423
+ with gr.Column(scale=1):
424
+ image = gr.Image(label="Reference frame", type="filepath",
425
+ height=280, sources=["upload", "clipboard"])
426
+ variant = gr.Radio(
427
+ choices=list(PIPELINES.keys()),
428
+ value=DEFAULT_VARIANT,
429
+ label="Student",
430
+ info="Fewer steps = faster. 1-/2-step use First-Frame Enhancement.",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
431
  )
432
+ gr.Markdown("### Action script \nEach block is 12 frames โ‰ˆ 1 second.")
433
  with gr.Row():
434
+ action_1 = gr.Dropdown(MINECRAFT_ACTIONS, value="forward",
435
+ label="Segment 1", scale=2)
436
+ chunks_1 = gr.Slider(0, 8, value=3, step=1, label="blocks", scale=1)
437
+ with gr.Row():
438
+ action_2 = gr.Dropdown(MINECRAFT_ACTIONS, value="turn_right",
439
+ label="Segment 2", scale=2)
440
+ chunks_2 = gr.Slider(0, 8, value=2, step=1, label="blocks", scale=1)
441
+ with gr.Row():
442
+ action_3 = gr.Dropdown(MINECRAFT_ACTIONS, value="forward",
443
+ label="Segment 3", scale=2)
444
+ chunks_3 = gr.Slider(0, 8, value=2, step=1, label="blocks", scale=1)
445
+ with gr.Accordion("Advanced", open=False):
446
+ seed = gr.Slider(0, 2**31 - 1, value=0, step=1, label="Seed")
447
+ run = gr.Button("Roll out the world", variant="primary")
448
+
449
+ with gr.Column(scale=1):
450
+ video = gr.Video(label="Generated rollout", autoplay=True, loop=True)
451
+ info = gr.Markdown()
452
+
453
+ gr.Examples(
454
+ examples=[
455
+ ["examples/forest.png", "forward", 3, "turn_right", 2, "forward", 2],
456
+ ["examples/plains.png", "forward", 3, "look_up", 1, "forward_turn_right", 3],
457
+ ["examples/cave.png", "forward", 2, "turn_left", 2, "forward", 3],
458
+ ],
459
+ inputs=[image, action_1, chunks_1, action_2, chunks_2, action_3, chunks_3],
460
+ outputs=[video, info],
461
+ fn=generate_example,
462
+ cache_examples=True,
463
+ cache_mode="lazy",
464
+ label="Examples (reference frames from the ForgeWM repo)",
 
 
 
 
 
 
 
 
 
 
 
465
  )
466
 
467
+ gr.Markdown(
468
+ "Rollouts are 352ร—640 at 12 fps. Attention uses a 6-latent-frame sliding "
469
+ "window with a block-causal KV cache, exactly as the released students "
470
+ "were trained. Base weights: "
471
+ "[Skywork/Matrix-Game-2.0](https://huggingface.co/Skywork/Matrix-Game-2.0)."
472
  )
473
+
474
+ run.click(
475
+ fn=generate,
476
+ inputs=[image, variant, action_1, chunks_1, action_2, chunks_2,
477
+ action_3, chunks_3, seed],
478
+ outputs=[video, info],
479
+ concurrency_limit=1,
480
+ api_name="generate",
481
+ )
482
+
483
+ demo.queue(max_size=12).launch(mcp_server=True, show_error=True)
examples/cave.png ADDED

Git LFS Details

  • SHA256: d78822d116af9c529a7f30b5b555189361578af9bc29eb94fc689cd28cf3af00
  • Pointer size: 131 Bytes
  • Size of remote file: 106 kB
examples/forest.png ADDED

Git LFS Details

  • SHA256: b5105b2828cd7c5e0ad5ef6f18727ba1fbc35b19d624205dedd60071c121b603
  • Pointer size: 131 Bytes
  • Size of remote file: 346 kB
examples/plains.png ADDED

Git LFS Details

  • SHA256: 0db49e49b5d6ce2eb5576b1d9f69ddd1fda983503cce0ecd4d36072929e7e2b7
  • Pointer size: 131 Bytes
  • Size of remote file: 273 kB
pipeline/__init__.py CHANGED
@@ -1,3 +1,13 @@
 
1
  from .causal_inference import CausalInferencePipeline
 
 
 
2
 
3
- __all__ = ["CausalInferencePipeline"]
 
 
 
 
 
 
 
1
+ from .causal_diffusion_inference import CausalDiffusionInferencePipeline
2
  from .causal_inference import CausalInferencePipeline
3
+ from .self_forcing_training import SelfForcingTrainingPipeline
4
+ from .teacher_forcing_training import TeacherForcingTrainingPipeline
5
+ from .bidirectional_training import BidirectionalTrainingPipeline
6
 
7
+ __all__ = [
8
+ "CausalDiffusionInferencePipeline",
9
+ "CausalInferencePipeline",
10
+ "SelfForcingTrainingPipeline",
11
+ "TeacherForcingTrainingPipeline",
12
+ "BidirectionalTrainingPipeline",
13
+ ]
pipeline/bidirectional_training.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper
2
+ from utils.scheduler import SchedulerInterface
3
+ from typing import List, Optional
4
+ import torch
5
+ import torch.distributed as dist
6
+
7
+
8
+ class BidirectionalTrainingPipeline:
9
+ def __init__(self,
10
+ denoising_step_list: List[int],
11
+ scheduler: SchedulerInterface,
12
+ generator: WanDiffusionWrapper,
13
+ num_frame_per_block=3,
14
+ independent_first_frame: bool = False,
15
+ same_step_across_blocks: bool = False,
16
+ last_step_only: bool = False,
17
+ num_max_frames: int = 21,
18
+ context_noise: int = 0,
19
+ spatial_self: bool = True,
20
+ **kwargs):
21
+ super().__init__()
22
+ self.scheduler = scheduler
23
+ self.generator = generator
24
+ self.denoising_step_list = denoising_step_list
25
+ if self.denoising_step_list[-1] == 0:
26
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
27
+
28
+ # Wan specific hyperparameters
29
+ self.num_transformer_blocks = 30
30
+ self.frame_seq_length = 1560
31
+ self.num_frame_per_block = num_frame_per_block
32
+ self.context_noise = context_noise
33
+ self.i2v = False
34
+
35
+ self.kv_cache1 = None
36
+ self.kv_cache2 = None
37
+ self.independent_first_frame = independent_first_frame
38
+ self.same_step_across_blocks = same_step_across_blocks
39
+ self.last_step_only = last_step_only
40
+ self.kv_cache_size = num_max_frames * self.frame_seq_length
41
+
42
+ self.spatial_self = spatial_self
43
+
44
+ def generate_and_sync_list(self, num_blocks, num_denoising_steps, device):
45
+ rank = dist.get_rank() if dist.is_initialized() else 0
46
+
47
+ if rank == 0:
48
+ # Generate random indices
49
+ indices = torch.randint(
50
+ low=0,
51
+ high=num_denoising_steps,
52
+ size=(num_blocks,),
53
+ device=device
54
+ )
55
+ # In our training, self.last_step_only is False
56
+ if self.last_step_only:
57
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
58
+ else:
59
+ indices = torch.empty(num_blocks, dtype=torch.long, device=device)
60
+
61
+ dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
62
+ return indices.tolist()
63
+
64
+ def inference_with_trajectory(
65
+ self,
66
+ noise: torch.Tensor,
67
+ clean_image_or_video: torch.Tensor = None, # same shape as noise
68
+ initial_latent: Optional[torch.Tensor] = None,
69
+ return_sim_step: bool = False,
70
+ **conditional_dict
71
+ ) -> torch.Tensor:
72
+ batch_size, num_frames, num_channels, height, width = noise.shape
73
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
74
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
75
+ # noise should still be a multiple of num_frame_per_block
76
+ assert num_frames % self.num_frame_per_block == 0
77
+ num_blocks = num_frames // self.num_frame_per_block
78
+ else:
79
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
80
+ assert (num_frames - 1) % self.num_frame_per_block == 0
81
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
82
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
83
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
84
+ output = torch.zeros(
85
+ [batch_size, num_output_frames, num_channels, height, width],
86
+ device=noise.device,
87
+ dtype=noise.dtype
88
+ )
89
+
90
+ # Step 3: Temporal denoising loop
91
+ all_num_frames = [self.num_frame_per_block] * num_blocks
92
+ num_denoising_steps = len(self.denoising_step_list)
93
+ exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
94
+ start_gradient_frame_index = num_output_frames - 21 # always 0 as long as we train 21 latent frames
95
+ if start_gradient_frame_index != 0:
96
+ raise NotImplementedError("start_gradient_frame_index is always 0 as long as we train 21 latent frames")
97
+
98
+
99
+ noisy_input = noise
100
+ for index, current_timestep in enumerate(self.denoising_step_list):
101
+ # self.same_step_across_blocks is True
102
+ if self.same_step_across_blocks:
103
+ exit_flag = (index == exit_flags[0])
104
+ else:
105
+ raise NotImplementedError('Here t is a scalar denoting that all chunks are at the same t, but in the future we may set t a tensor denoting different chunks') # Only backprop at the randomly selected timestep (consistent across all ranks)
106
+ timestep = torch.ones(
107
+ [batch_size, self.num_frame_per_block*num_blocks],
108
+ device=noise.device,
109
+ dtype=torch.int64) * current_timestep
110
+
111
+ if not exit_flag:
112
+ with torch.no_grad():
113
+ _,denoised_pred = self.generator(
114
+ noisy_image_or_video=noisy_input,
115
+ conditional_dict=conditional_dict,
116
+ timestep=timestep
117
+ )
118
+ next_timestep = self.denoising_step_list[index + 1]
119
+ noisy_input = self.scheduler.add_noise(
120
+ denoised_pred.flatten(0, 1),
121
+ torch.randn_like(denoised_pred.flatten(0, 1)),
122
+ next_timestep * torch.ones(
123
+ [batch_size * self.num_frame_per_block*num_blocks], device=noise.device, dtype=torch.long)
124
+ ).unflatten(0, denoised_pred.shape[:2])
125
+ print('denoise')
126
+ else:
127
+ _,output = self.generator(
128
+ noisy_image_or_video=noisy_input,
129
+ conditional_dict=conditional_dict,
130
+ timestep=timestep
131
+ )
132
+ print('final denoise')
133
+ break
134
+ # ======================= SF -> TF modification ends ============================
135
+
136
+ # Step 3.5: Return the denoised timestep
137
+ if not self.same_step_across_blocks: # Useless, never met
138
+ denoised_timestep_from, denoised_timestep_to = None, None
139
+ # T -> \tau_1 -> \tau_2 ->...-> \tau โ€”โ€” enable grad โ€”โ€”> 0
140
+ # denoised_timestep_from = \tau
141
+ # denoised_timestep_to = next timestep smaller than \tau
142
+ # These are just engineering tricks
143
+ # to align DMD timestep sampling with the actual denoising range used by the generator
144
+ elif exit_flags[0] == len(self.denoising_step_list) - 1:
145
+ # corner case when \tau is the smallest non-zero timestep
146
+ denoised_timestep_to = 0
147
+ denoised_timestep_from = 1000 - torch.argmin(
148
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
149
+ else:
150
+ denoised_timestep_to = 1000 - torch.argmin(
151
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
152
+ denoised_timestep_from = 1000 - torch.argmin(
153
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
154
+
155
+ if return_sim_step: # False
156
+ return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
157
+
158
+ return output, denoised_timestep_from, denoised_timestep_to
159
+
160
+
pipeline/causal_diffusion_inference.py ADDED
@@ -0,0 +1,637 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from tqdm import tqdm
2
+ from typing import List, Optional
3
+ import torch
4
+
5
+ from wan.utils.fm_solvers import FlowDPMSolverMultistepScheduler, get_sampling_sigmas, retrieve_timesteps
6
+ from wan.utils.fm_solvers_unipc import FlowUniPCMultistepScheduler
7
+ from utils.wan_wrapper import WanDiffusionWrapper, WanTextEncoder, WanVAEWrapper
8
+
9
+
10
+ def cond_current(conditional_dict, current_start_frame, num_frame_per_block, vae_time_compression_ratio=4):
11
+ raw_end = 1 + vae_time_compression_ratio * (current_start_frame + num_frame_per_block - 1)
12
+ current = {
13
+ "visual_context": conditional_dict["visual_context"],
14
+ "cond_concat": conditional_dict["cond_concat"][:, current_start_frame:current_start_frame + num_frame_per_block],
15
+ }
16
+ mouse_condition = conditional_dict.get("mouse_condition", conditional_dict.get("mouse_cond"))
17
+ keyboard_condition = conditional_dict.get("keyboard_condition", conditional_dict.get("keyboard_cond"))
18
+ if mouse_condition is not None:
19
+ current["mouse_condition"] = mouse_condition[:, :raw_end]
20
+ if keyboard_condition is not None:
21
+ current["keyboard_condition"] = keyboard_condition[:, :raw_end]
22
+ return current
23
+
24
+
25
+ class CausalDiffusionInferencePipeline(torch.nn.Module):
26
+ def __init__(
27
+ self,
28
+ args,
29
+ device,
30
+ generator=None,
31
+ text_encoder=None,
32
+ vae=None,
33
+ need_vae = True
34
+ ):
35
+ super().__init__()
36
+ # Step 1: Initialize all models
37
+ model_kwargs = dict(getattr(args, "model_kwargs", {}))
38
+ action_config = getattr(args, "action_config", None)
39
+ if action_config is not None:
40
+ action_config = dict(action_config)
41
+ action_config.pop("local_attn_size", None)
42
+ model_kwargs["action_config"] = action_config
43
+ # Match the training-time sliding-window regime; see
44
+ # CausalInferencePipeline for the full rationale.
45
+ if "local_attn_size" not in model_kwargs and hasattr(args, "local_attn_size"):
46
+ model_kwargs["local_attn_size"] = args.local_attn_size
47
+ if "sink_size" not in model_kwargs and hasattr(args, "sink_size"):
48
+ model_kwargs["sink_size"] = args.sink_size
49
+ self.generator = WanDiffusionWrapper(
50
+ **model_kwargs, is_causal=True) if generator is None else generator
51
+ self.text_encoder = WanTextEncoder() if text_encoder is None else text_encoder
52
+ if need_vae:
53
+ self.vae = WanVAEWrapper() if vae is None else vae
54
+
55
+ # Step 2: Initialize scheduler
56
+ self.num_train_timesteps = args.num_train_timestep
57
+ self.sampling_steps = 50
58
+ self.sample_solver = 'unipc'
59
+ self.shift = args.timestep_shift
60
+
61
+ self.num_transformer_blocks = 30
62
+ self.frame_seq_length = None
63
+
64
+ self.kv_cache_pos = None
65
+ self.kv_cache_neg = None
66
+ self.kv_cache_mouse_pos = None
67
+ self.kv_cache_keyboard_pos = None
68
+ self.crossattn_cache_pos = None
69
+ self.crossattn_cache_neg = None
70
+ self.args = args
71
+ self.num_frame_per_block = getattr(args, "num_frame_per_block", 1)
72
+ self.independent_first_frame = args.independent_first_frame
73
+ self.local_attn_size = self.generator.model.local_attn_size
74
+
75
+ print(f"KV inference with {self.num_frame_per_block} frames per block")
76
+
77
+ if self.num_frame_per_block > 1:
78
+ self.generator.model.num_frame_per_block = self.num_frame_per_block
79
+
80
+ def inference(
81
+ self,
82
+ noise: torch.Tensor,
83
+ conditional_dict: dict,
84
+ initial_latent: Optional[torch.Tensor] = None,
85
+ return_latents: bool = False,
86
+ start_frame_index: Optional[int] = 0,
87
+ return_video=True
88
+ ) -> torch.Tensor:
89
+ """
90
+ Perform inference on the given noise and Stage1 conditional inputs.
91
+ Inputs:
92
+ noise (torch.Tensor): The input noise tensor of shape
93
+ (batch_size, num_output_frames, num_channels, height, width).
94
+ conditional_dict (dict): MG2-style conditioning with visual_context, cond_concat,
95
+ and optional mouse/keyboard action sequences.
96
+ initial_latent (torch.Tensor): The initial latent tensor of shape
97
+ (batch_size, num_input_frames, num_channels, height, width).
98
+ If num_input_frames is 1, perform image to video.
99
+ If num_input_frames is greater than 1, perform video extension.
100
+ return_latents (bool): Whether to return the latents.
101
+ start_frame_index (int): In long video generation, where does the current window start?
102
+ Outputs:
103
+ video (torch.Tensor): The generated video tensor of shape
104
+ (batch_size, num_frames, num_channels, height, width). It is normalized to be in the range [0, 1].
105
+ """
106
+ batch_size, num_frames, num_channels, height, width = noise.shape
107
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
108
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
109
+ # noise should still be a multiple of num_frame_per_block
110
+ assert num_frames % self.num_frame_per_block == 0
111
+ num_blocks = num_frames // self.num_frame_per_block
112
+ elif self.independent_first_frame and initial_latent is None:
113
+ # Using a [1, 4, 4, 4, 4, 4] model to generate a video without image conditioning
114
+ assert (num_frames - 1) % self.num_frame_per_block == 0
115
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
116
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
117
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
118
+ self.frame_seq_length = (height // self.generator.model.patch_size[1]) * (width // self.generator.model.patch_size[2])
119
+ vae_time_compression_ratio = getattr(self.args.action_config, 'vae_time_compression_ratio', 4) if getattr(self.args, 'action_config', None) else 4
120
+
121
+ output = torch.zeros(
122
+ [batch_size, num_output_frames, num_channels, height, width],
123
+ device=noise.device,
124
+ dtype=noise.dtype
125
+ )
126
+
127
+ # Step 1: Initialize KV cache to all zeros
128
+ if self.kv_cache_pos is None:
129
+ self._initialize_kv_cache(
130
+ batch_size=batch_size,
131
+ dtype=noise.dtype,
132
+ device=noise.device,
133
+ num_output_frames=num_output_frames,
134
+ )
135
+ self._initialize_kv_cache_mouse_and_keyboard(
136
+ batch_size=batch_size,
137
+ dtype=noise.dtype,
138
+ device=noise.device,
139
+ num_output_frames=num_output_frames,
140
+ )
141
+ self._initialize_crossattn_cache(
142
+ batch_size=batch_size,
143
+ dtype=noise.dtype,
144
+ device=noise.device
145
+ )
146
+ else:
147
+ for block_index in range(self.num_transformer_blocks):
148
+ self.crossattn_cache_pos[block_index]["is_init"] = False
149
+ for block_index in range(len(self.kv_cache_pos)):
150
+ self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor(
151
+ [0], dtype=torch.long, device=noise.device)
152
+ self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor(
153
+ [0], dtype=torch.long, device=noise.device)
154
+ self.kv_cache_mouse_pos[block_index]["global_end_index"] = torch.tensor(
155
+ [0], dtype=torch.long, device=noise.device)
156
+ self.kv_cache_mouse_pos[block_index]["local_end_index"] = torch.tensor(
157
+ [0], dtype=torch.long, device=noise.device)
158
+ self.kv_cache_keyboard_pos[block_index]["global_end_index"] = torch.tensor(
159
+ [0], dtype=torch.long, device=noise.device)
160
+ self.kv_cache_keyboard_pos[block_index]["local_end_index"] = torch.tensor(
161
+ [0], dtype=torch.long, device=noise.device)
162
+
163
+ # Step 2: Cache context feature
164
+ current_start_frame = start_frame_index
165
+ cache_start_frame = 0
166
+ if initial_latent is not None:
167
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
168
+ if self.independent_first_frame:
169
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
170
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
171
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
172
+ output[:, :1] = initial_latent[:, :1]
173
+ self.generator(
174
+ noisy_image_or_video=initial_latent[:, :1],
175
+ conditional_dict=cond_current(conditional_dict, current_start_frame, 1, vae_time_compression_ratio),
176
+ timestep=timestep * 0,
177
+ kv_cache=self.kv_cache_pos,
178
+ kv_cache_mouse=self.kv_cache_mouse_pos,
179
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
180
+ crossattn_cache=self.crossattn_cache_pos,
181
+ current_start=current_start_frame * self.frame_seq_length,
182
+ cache_start=cache_start_frame * self.frame_seq_length
183
+ )
184
+ current_start_frame += 1
185
+ cache_start_frame += 1
186
+ else:
187
+ # Assume num_input_frames is self.num_frame_per_block * num_input_blocks
188
+ assert num_input_frames % self.num_frame_per_block == 0
189
+ num_input_blocks = num_input_frames // self.num_frame_per_block
190
+
191
+ for block_index in range(num_input_blocks):
192
+ current_ref_latents = \
193
+ initial_latent[:, cache_start_frame:cache_start_frame + self.num_frame_per_block]
194
+ output[:, cache_start_frame:cache_start_frame + self.num_frame_per_block] = current_ref_latents
195
+ self.generator(
196
+ noisy_image_or_video=current_ref_latents,
197
+ conditional_dict=cond_current(conditional_dict, current_start_frame, self.num_frame_per_block, vae_time_compression_ratio),
198
+ timestep=timestep * 0,
199
+ kv_cache=self.kv_cache_pos,
200
+ kv_cache_mouse=self.kv_cache_mouse_pos,
201
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
202
+ crossattn_cache=self.crossattn_cache_pos,
203
+ current_start=current_start_frame * self.frame_seq_length,
204
+ cache_start=cache_start_frame * self.frame_seq_length
205
+ )
206
+ current_start_frame += self.num_frame_per_block
207
+ cache_start_frame += self.num_frame_per_block
208
+
209
+ # Step 3: Temporal denoising loop
210
+ all_num_frames = [self.num_frame_per_block] * num_blocks
211
+ if self.independent_first_frame and initial_latent is None:
212
+ all_num_frames = [1] + all_num_frames
213
+ for current_num_frames in all_num_frames:
214
+ noisy_input = noise[
215
+ :, cache_start_frame - num_input_frames:cache_start_frame + current_num_frames - num_input_frames]
216
+ latents = noisy_input
217
+
218
+ # Step 3.1: Spatial denoising loop
219
+ sample_scheduler = self._initialize_sample_scheduler(noise)
220
+ for _, t in enumerate(tqdm(sample_scheduler.timesteps)):
221
+ latent_model_input = latents
222
+ timestep = t * torch.ones(
223
+ [batch_size, current_num_frames], device=noise.device, dtype=torch.float32
224
+ )
225
+
226
+ flow_pred, _ = self.generator(
227
+ noisy_image_or_video=latent_model_input,
228
+ conditional_dict=cond_current(conditional_dict, current_start_frame, current_num_frames, vae_time_compression_ratio),
229
+ timestep=timestep,
230
+ kv_cache=self.kv_cache_pos,
231
+ kv_cache_mouse=self.kv_cache_mouse_pos,
232
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
233
+ crossattn_cache=self.crossattn_cache_pos,
234
+ current_start=current_start_frame * self.frame_seq_length,
235
+ cache_start=cache_start_frame * self.frame_seq_length
236
+ )
237
+
238
+ temp_x0 = sample_scheduler.step(
239
+ flow_pred,
240
+ t,
241
+ latents,
242
+ return_dict=False)[0]
243
+ latents = temp_x0
244
+
245
+ # Step 3.2: record the model's output
246
+ output[:, cache_start_frame:cache_start_frame + current_num_frames] = latents
247
+
248
+ # Step 3.3: rerun with timestep zero to update KV cache using clean context
249
+ self.generator(
250
+ noisy_image_or_video=latents,
251
+ conditional_dict=cond_current(conditional_dict, current_start_frame, current_num_frames, vae_time_compression_ratio),
252
+ timestep=timestep * 0,
253
+ kv_cache=self.kv_cache_pos,
254
+ kv_cache_mouse=self.kv_cache_mouse_pos,
255
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
256
+ crossattn_cache=self.crossattn_cache_pos,
257
+ current_start=current_start_frame * self.frame_seq_length,
258
+ cache_start=cache_start_frame * self.frame_seq_length
259
+ )
260
+
261
+ # Step 3.4: update the start and end frame indices
262
+ current_start_frame += current_num_frames
263
+ cache_start_frame += current_num_frames
264
+
265
+ # Step 4: Decode the output
266
+ if return_video:
267
+ video = self.vae.decode_to_pixel(output)
268
+ video = (video * 0.5 + 0.5).clamp(0, 1)
269
+
270
+ if return_latents:
271
+ return video, output
272
+ else:
273
+ return video
274
+ else:
275
+ return output
276
+
277
+
278
+ def inference_for_cd(
279
+ self,
280
+ noise: torch.Tensor,
281
+ conditional_dict: dict,
282
+ record_step_indices: List[int],
283
+ initial_latent: Optional[torch.Tensor] = None,
284
+ start_frame_index: int = 0
285
+ ) -> torch.Tensor:
286
+ """Run causal denoising and record selected latent states for consistency distillation."""
287
+ self.sampling_steps = 48
288
+ batch_size, num_frames, _, height, width = noise.shape
289
+
290
+ if (not self.independent_first_frame) or (self.independent_first_frame and initial_latent is not None):
291
+ assert num_frames % self.num_frame_per_block == 0
292
+ num_blocks = num_frames // self.num_frame_per_block
293
+ else:
294
+ assert (num_frames - 1) % self.num_frame_per_block == 0
295
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
296
+
297
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
298
+ self.frame_seq_length = (height // self.generator.model.patch_size[1]) * (width // self.generator.model.patch_size[2])
299
+ vae_time_compression_ratio = getattr(self.args.action_config, 'vae_time_compression_ratio', 4) if getattr(self.args, 'action_config', None) else 4
300
+
301
+ if self.kv_cache_pos is None:
302
+ self._initialize_kv_cache(batch_size=batch_size, dtype=noise.dtype, device=noise.device,
303
+ num_output_frames=num_frames + num_input_frames)
304
+ self._initialize_kv_cache_mouse_and_keyboard(batch_size=batch_size, dtype=noise.dtype, device=noise.device,
305
+ num_output_frames=num_frames + num_input_frames)
306
+ self._initialize_crossattn_cache(batch_size=batch_size, dtype=noise.dtype, device=noise.device)
307
+ else:
308
+ for block_index in range(self.num_transformer_blocks):
309
+ self.crossattn_cache_pos[block_index]["is_init"] = False
310
+ for block_index in range(len(self.kv_cache_pos)):
311
+ self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor([0], dtype=torch.long, device=noise.device)
312
+ self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor([0], dtype=torch.long, device=noise.device)
313
+ self.kv_cache_mouse_pos[block_index]["global_end_index"] = torch.tensor([0], dtype=torch.long, device=noise.device)
314
+ self.kv_cache_mouse_pos[block_index]["local_end_index"] = torch.tensor([0], dtype=torch.long, device=noise.device)
315
+ self.kv_cache_keyboard_pos[block_index]["global_end_index"] = torch.tensor([0], dtype=torch.long, device=noise.device)
316
+ self.kv_cache_keyboard_pos[block_index]["local_end_index"] = torch.tensor([0], dtype=torch.long, device=noise.device)
317
+
318
+ sample_scheduler_probe = self._initialize_sample_scheduler(noise)
319
+ total_steps = len(sample_scheduler_probe.timesteps)
320
+ record_step_indices = sorted(set(int(i) for i in record_step_indices))
321
+ if not record_step_indices:
322
+ raise ValueError("record_step_indices must be non-empty")
323
+ if record_step_indices[0] < 0 or record_step_indices[-1] >= total_steps:
324
+ raise ValueError(f"record_step_indices out of range: valid=[0,{total_steps - 1}], got={record_step_indices}")
325
+ record_set = set(record_step_indices)
326
+
327
+ current_start_frame = start_frame_index
328
+ cache_start_frame = 0
329
+ if initial_latent is not None:
330
+ timestep0 = torch.zeros([batch_size, 1], device=noise.device, dtype=torch.int64)
331
+ if self.independent_first_frame:
332
+ assert (num_input_frames - 1) % self.num_frame_per_block == 0
333
+ num_input_blocks = (num_input_frames - 1) // self.num_frame_per_block
334
+ self.generator(
335
+ noisy_image_or_video=initial_latent[:, :1],
336
+ conditional_dict=cond_current(conditional_dict, current_start_frame, 1, vae_time_compression_ratio),
337
+ timestep=timestep0,
338
+ kv_cache=self.kv_cache_pos,
339
+ kv_cache_mouse=self.kv_cache_mouse_pos,
340
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
341
+ crossattn_cache=self.crossattn_cache_pos,
342
+ current_start=current_start_frame * self.frame_seq_length,
343
+ cache_start=cache_start_frame * self.frame_seq_length,
344
+ )
345
+ current_start_frame += 1
346
+ cache_start_frame += 1
347
+ else:
348
+ assert num_input_frames % self.num_frame_per_block == 0
349
+ num_input_blocks = num_input_frames // self.num_frame_per_block
350
+
351
+ for _ in range(num_input_blocks):
352
+ current_ref_latents = initial_latent[:, cache_start_frame:cache_start_frame + self.num_frame_per_block]
353
+ self.generator(
354
+ noisy_image_or_video=current_ref_latents,
355
+ conditional_dict=cond_current(conditional_dict, current_start_frame, self.num_frame_per_block, vae_time_compression_ratio),
356
+ timestep=timestep0,
357
+ kv_cache=self.kv_cache_pos,
358
+ kv_cache_mouse=self.kv_cache_mouse_pos,
359
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
360
+ crossattn_cache=self.crossattn_cache_pos,
361
+ current_start=current_start_frame * self.frame_seq_length,
362
+ cache_start=cache_start_frame * self.frame_seq_length,
363
+ )
364
+ current_start_frame += self.num_frame_per_block
365
+ cache_start_frame += self.num_frame_per_block
366
+
367
+ all_num_frames = [self.num_frame_per_block] * num_blocks
368
+ if self.independent_first_frame and initial_latent is None:
369
+ all_num_frames = [1] + all_num_frames
370
+
371
+ full_chunk_record = []
372
+ for current_num_frames in all_num_frames:
373
+ latents = noise[:, cache_start_frame - num_input_frames:cache_start_frame + current_num_frames - num_input_frames]
374
+ chunk_records = []
375
+ sample_scheduler = self._initialize_sample_scheduler(noise)
376
+ current_cond = cond_current(conditional_dict, current_start_frame, current_num_frames, vae_time_compression_ratio)
377
+
378
+ for progress_id, t in enumerate(tqdm(sample_scheduler.timesteps)):
379
+ if progress_id in record_set:
380
+ print(f"{progress_id}: {t} saved")
381
+ chunk_records.append(latents.detach().clone())
382
+
383
+ timestep = t * torch.ones([batch_size, current_num_frames], device=noise.device, dtype=torch.float32)
384
+ flow_pred, _ = self.generator(
385
+ noisy_image_or_video=latents,
386
+ conditional_dict=current_cond,
387
+ timestep=timestep,
388
+ kv_cache=self.kv_cache_pos,
389
+ kv_cache_mouse=self.kv_cache_mouse_pos,
390
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
391
+ crossattn_cache=self.crossattn_cache_pos,
392
+ current_start=current_start_frame * self.frame_seq_length,
393
+ cache_start=cache_start_frame * self.frame_seq_length,
394
+ )
395
+ latents = sample_scheduler.step(flow_pred, t, latents, return_dict=False)[0]
396
+
397
+ chunk_records.append(latents.detach().clone())
398
+ full_chunk_record.append(torch.stack(chunk_records, dim=1))
399
+
400
+ timestep0 = torch.zeros([batch_size, current_num_frames], device=noise.device, dtype=torch.float32)
401
+ self.generator(
402
+ noisy_image_or_video=latents,
403
+ conditional_dict=current_cond,
404
+ timestep=timestep0,
405
+ kv_cache=self.kv_cache_pos,
406
+ kv_cache_mouse=self.kv_cache_mouse_pos,
407
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
408
+ crossattn_cache=self.crossattn_cache_pos,
409
+ current_start=current_start_frame * self.frame_seq_length,
410
+ cache_start=cache_start_frame * self.frame_seq_length,
411
+ )
412
+
413
+ current_start_frame += current_num_frames
414
+ cache_start_frame += current_num_frames
415
+
416
+ return torch.cat(full_chunk_record, dim=2)
417
+
418
+
419
+ def inference_for_genuine_cd(
420
+ self,
421
+ noisy_input: torch.Tensor,
422
+ conditional_dict: dict,
423
+ initial_latent: Optional[torch.Tensor] = None,
424
+ timestep_idx=0,
425
+ sampling_steps=48,
426
+ chunksize=3
427
+ ) -> torch.Tensor:
428
+ batch_size, num_frames, _, height, width = noisy_input.shape
429
+ assert num_frames == chunksize
430
+
431
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
432
+ self.frame_seq_length = (height // self.generator.model.patch_size[1]) * (width // self.generator.model.patch_size[2])
433
+ vae_time_compression_ratio = getattr(self.args.action_config, 'vae_time_compression_ratio', 4) if getattr(self.args, 'action_config', None) else 4
434
+
435
+ if self.kv_cache_pos is None:
436
+ self._initialize_kv_cache(
437
+ batch_size=batch_size,
438
+ dtype=noisy_input.dtype,
439
+ device=noisy_input.device,
440
+ num_output_frames=num_frames + num_input_frames,
441
+ )
442
+ self._initialize_kv_cache_mouse_and_keyboard(
443
+ batch_size=batch_size,
444
+ dtype=noisy_input.dtype,
445
+ device=noisy_input.device,
446
+ num_output_frames=num_frames + num_input_frames,
447
+ )
448
+ self._initialize_crossattn_cache(
449
+ batch_size=batch_size,
450
+ dtype=noisy_input.dtype,
451
+ device=noisy_input.device
452
+ )
453
+ else:
454
+ for block_index in range(self.num_transformer_blocks):
455
+ self.crossattn_cache_pos[block_index]["is_init"] = False
456
+ for block_index in range(len(self.kv_cache_pos)):
457
+ self.kv_cache_pos[block_index]["global_end_index"] = torch.tensor(
458
+ [0], dtype=torch.long, device=noisy_input.device)
459
+ self.kv_cache_pos[block_index]["local_end_index"] = torch.tensor(
460
+ [0], dtype=torch.long, device=noisy_input.device)
461
+ self.kv_cache_mouse_pos[block_index]["global_end_index"] = torch.tensor(
462
+ [0], dtype=torch.long, device=noisy_input.device)
463
+ self.kv_cache_mouse_pos[block_index]["local_end_index"] = torch.tensor(
464
+ [0], dtype=torch.long, device=noisy_input.device)
465
+ self.kv_cache_keyboard_pos[block_index]["global_end_index"] = torch.tensor(
466
+ [0], dtype=torch.long, device=noisy_input.device)
467
+ self.kv_cache_keyboard_pos[block_index]["local_end_index"] = torch.tensor(
468
+ [0], dtype=torch.long, device=noisy_input.device)
469
+
470
+ current_start_frame = 0
471
+ cache_start_frame = 0
472
+ timestep0 = torch.zeros([batch_size, 1], device=noisy_input.device, dtype=torch.int64)
473
+
474
+ if initial_latent is not None:
475
+ if self.independent_first_frame:
476
+ assert (num_input_frames - 1) % chunksize == 0
477
+ num_input_blocks = (num_input_frames - 1) // chunksize
478
+ self.generator(
479
+ noisy_image_or_video=initial_latent[:, :1],
480
+ conditional_dict=cond_current(conditional_dict, current_start_frame, 1, vae_time_compression_ratio),
481
+ timestep=timestep0,
482
+ kv_cache=self.kv_cache_pos,
483
+ kv_cache_mouse=self.kv_cache_mouse_pos,
484
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
485
+ crossattn_cache=self.crossattn_cache_pos,
486
+ current_start=current_start_frame * self.frame_seq_length,
487
+ cache_start=cache_start_frame * self.frame_seq_length,
488
+ )
489
+ current_start_frame += 1
490
+ cache_start_frame += 1
491
+ else:
492
+ assert num_input_frames % chunksize == 0
493
+ num_input_blocks = num_input_frames // chunksize
494
+
495
+ for _ in range(num_input_blocks):
496
+ current_ref_latents = initial_latent[:, cache_start_frame:cache_start_frame + chunksize]
497
+ self.generator(
498
+ noisy_image_or_video=current_ref_latents,
499
+ conditional_dict=cond_current(conditional_dict, current_start_frame, chunksize, vae_time_compression_ratio),
500
+ timestep=timestep0,
501
+ kv_cache=self.kv_cache_pos,
502
+ kv_cache_mouse=self.kv_cache_mouse_pos,
503
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
504
+ crossattn_cache=self.crossattn_cache_pos,
505
+ current_start=current_start_frame * self.frame_seq_length,
506
+ cache_start=cache_start_frame * self.frame_seq_length,
507
+ )
508
+ current_start_frame += chunksize
509
+ cache_start_frame += chunksize
510
+
511
+ sample_scheduler = self._initialize_sample_scheduler(noisy_input, sampling_steps=sampling_steps)
512
+ t = sample_scheduler.timesteps[timestep_idx]
513
+ timestep = t * torch.ones(
514
+ [batch_size, chunksize], device=noisy_input.device, dtype=torch.float32
515
+ )
516
+ flow_pred, _ = self.generator(
517
+ noisy_image_or_video=noisy_input,
518
+ conditional_dict=cond_current(conditional_dict, current_start_frame, chunksize, vae_time_compression_ratio),
519
+ timestep=timestep,
520
+ kv_cache=self.kv_cache_pos,
521
+ kv_cache_mouse=self.kv_cache_mouse_pos,
522
+ kv_cache_keyboard=self.kv_cache_keyboard_pos,
523
+ crossattn_cache=self.crossattn_cache_pos,
524
+ current_start=current_start_frame * self.frame_seq_length,
525
+ cache_start=cache_start_frame * self.frame_seq_length,
526
+ )
527
+
528
+ latents = sample_scheduler.step(
529
+ flow_pred,
530
+ t,
531
+ noisy_input,
532
+ return_dict=False)[0]
533
+
534
+ return latents
535
+
536
+
537
+
538
+ def _initialize_kv_cache(self, batch_size, dtype, device, num_output_frames=21):
539
+ """
540
+ Initialize a Per-GPU KV cache for the Wan model.
541
+
542
+ For local_attn_size != -1, sliding-window cache sized to the window.
543
+ For local_attn_size == -1, cache must fit the entire rollout โ€” Stage-3
544
+ uses 22 frames; the previous hardcoded 15 was undersized and caused
545
+ an out-of-bounds slice when block 5+ writes past frame 15.
546
+ """
547
+ kv_cache_pos = []
548
+ kv_cache_neg = []
549
+ if self.local_attn_size != -1:
550
+ kv_cache_size = self.local_attn_size * self.frame_seq_length
551
+ else:
552
+ # full causal: cover the whole rollout
553
+ kv_cache_size = num_output_frames * self.frame_seq_length
554
+
555
+ for _ in range(self.num_transformer_blocks):
556
+ kv_cache_pos.append({
557
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
558
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
559
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
560
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
561
+ })
562
+ kv_cache_neg.append({
563
+ "k": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
564
+ "v": torch.zeros([batch_size, kv_cache_size, 12, 128], dtype=dtype, device=device),
565
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
566
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
567
+ })
568
+
569
+ self.kv_cache_pos = kv_cache_pos # always store the clean cache
570
+ self.kv_cache_neg = kv_cache_neg # always store the clean cache
571
+
572
+ def _initialize_kv_cache_mouse_and_keyboard(self, batch_size, dtype, device, num_output_frames=21):
573
+ kv_cache_mouse_pos = []
574
+ kv_cache_keyboard_pos = []
575
+ # Same sizing logic as main kv_cache: -1 means cover entire rollout.
576
+ kv_cache_size = self.local_attn_size if self.local_attn_size != -1 else num_output_frames
577
+ for _ in range(self.num_transformer_blocks):
578
+ kv_cache_keyboard_pos.append({
579
+ "k": torch.zeros([batch_size, kv_cache_size, 16, 64], dtype=dtype, device=device),
580
+ "v": torch.zeros([batch_size, kv_cache_size, 16, 64], dtype=dtype, device=device),
581
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
582
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
583
+ })
584
+ kv_cache_mouse_pos.append({
585
+ "k": torch.zeros([batch_size * self.frame_seq_length, kv_cache_size, 16, 64], dtype=dtype, device=device),
586
+ "v": torch.zeros([batch_size * self.frame_seq_length, kv_cache_size, 16, 64], dtype=dtype, device=device),
587
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
588
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
589
+ })
590
+ self.kv_cache_mouse_pos = kv_cache_mouse_pos
591
+ self.kv_cache_keyboard_pos = kv_cache_keyboard_pos
592
+
593
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
594
+ """
595
+ Initialize a Per-GPU cross-attention cache for the Wan model.
596
+ """
597
+ crossattn_cache_pos = []
598
+ crossattn_cache_neg = []
599
+ for _ in range(self.num_transformer_blocks):
600
+ crossattn_cache_pos.append({
601
+ "k": torch.zeros([batch_size, 257, 12, 128], dtype=dtype, device=device),
602
+ "v": torch.zeros([batch_size, 257, 12, 128], dtype=dtype, device=device),
603
+ "is_init": False
604
+ })
605
+ crossattn_cache_neg.append({
606
+ "k": torch.zeros([batch_size, 257, 12, 128], dtype=dtype, device=device),
607
+ "v": torch.zeros([batch_size, 257, 12, 128], dtype=dtype, device=device),
608
+ "is_init": False
609
+ })
610
+
611
+ self.crossattn_cache_pos = crossattn_cache_pos
612
+ self.crossattn_cache_neg = crossattn_cache_neg
613
+
614
+ def _initialize_sample_scheduler(self, noise, sampling_steps=-1):
615
+ if sampling_steps == -1:
616
+ sampling_steps = self.sampling_steps
617
+ if self.sample_solver == 'unipc':
618
+ sample_scheduler = FlowUniPCMultistepScheduler(
619
+ num_train_timesteps=self.num_train_timesteps,
620
+ shift=1,
621
+ use_dynamic_shifting=False)
622
+ sample_scheduler.set_timesteps(
623
+ sampling_steps, device=noise.device, shift=self.shift)
624
+ self.timesteps = sample_scheduler.timesteps
625
+ elif self.sample_solver == 'dpm++':
626
+ sample_scheduler = FlowDPMSolverMultistepScheduler(
627
+ num_train_timesteps=self.num_train_timesteps,
628
+ shift=1,
629
+ use_dynamic_shifting=False)
630
+ sampling_sigmas = get_sampling_sigmas(sampling_steps, self.shift)
631
+ self.timesteps, _ = retrieve_timesteps(
632
+ sample_scheduler,
633
+ device=noise.device,
634
+ sigmas=sampling_sigmas)
635
+ else:
636
+ raise NotImplementedError("Unsupported solver.")
637
+ return sample_scheduler
pipeline/self_forcing_training.py ADDED
@@ -0,0 +1,495 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper
2
+ from utils.scheduler import SchedulerInterface
3
+ from typing import List, Optional
4
+ import torch
5
+ import torch.distributed as dist
6
+
7
+
8
+ def _slice_cond_for_block(conditional_dict: dict, current_start_frame: int,
9
+ num_frames_in_block: int,
10
+ vae_time_compression_ratio: int = 4) -> dict:
11
+ """Return a shallow copy of conditional_dict with cond_concat /
12
+ mouse_condition / keyboard_condition sliced for the current block.
13
+
14
+ MG2's CausalWanModel concatenates cond_concat to x along the channel
15
+ dim, so cond_concat must have the SAME F dim as the noisy video
16
+ being fed in (= num_frames_in_block during autoregressive rollout).
17
+
18
+ visual_context stays as-is (it's the first-frame CLIP embedding,
19
+ not per-frame).
20
+
21
+ mouse/keyboard are sampled at pixel-frame rate; slice up to the
22
+ pixel frame corresponding to the end of the current block.
23
+ """
24
+ if "cond_concat" not in conditional_dict:
25
+ return conditional_dict
26
+ current = dict(conditional_dict)
27
+ current["cond_concat"] = conditional_dict["cond_concat"][
28
+ :, current_start_frame:current_start_frame + num_frames_in_block]
29
+ # Slice mouse/keyboard up to the corresponding pixel frame boundary.
30
+ raw_end = 1 + vae_time_compression_ratio * (
31
+ current_start_frame + num_frames_in_block - 1)
32
+ for k in ("mouse_condition", "keyboard_condition", "mouse_cond", "keyboard_cond"):
33
+ if k in conditional_dict and conditional_dict[k] is not None:
34
+ current[k] = conditional_dict[k][:, :raw_end]
35
+ return current
36
+
37
+
38
+ class SelfForcingTrainingPipeline:
39
+ def __init__(self,
40
+ denoising_step_list: List[int],
41
+ scheduler: SchedulerInterface,
42
+ generator: WanDiffusionWrapper,
43
+ num_frame_per_block=3,
44
+ independent_first_frame: bool = False,
45
+ same_step_across_blocks: bool = False,
46
+ last_step_only: bool = False,
47
+ num_max_frames: int = 21,
48
+ context_noise: int = 0,
49
+ frame_seq_length: Optional[int] = None,
50
+ vae_time_compression_ratio: int = 4,
51
+ use_action: bool = False,
52
+ denoising_step_list_first_chunk: Optional[List[int]] = None,
53
+ **kwargs):
54
+ super().__init__()
55
+ self.scheduler = scheduler
56
+ self.generator = generator
57
+ self.denoising_step_list = denoising_step_list
58
+ if self.denoising_step_list[-1] == 0:
59
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
60
+
61
+ # โ”€โ”€โ”€ First-Frame Enhancement (FFE) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
62
+ # The 1-/2-step students spend the full 4-step schedule on chunk 0
63
+ # and the cheap main schedule on every later chunk. Chunk 0 anchors
64
+ # the whole rollout (it is the only block conditioned purely on the
65
+ # reference frame), so paying for it once buys a large quality gain
66
+ # at negligible amortized cost. None => one schedule throughout,
67
+ # which is what the 4-step student uses.
68
+ #
69
+ # NOTE (train/inference asymmetry, kept deliberately): the main
70
+ # `denoising_step_list` reaching this pipeline has already been
71
+ # remapped onto the shifted flow-matching grid when
72
+ # `warp_denoising_step: true`, whereas the first-chunk list is
73
+ # consumed here as the raw integers from the config. At inference
74
+ # (see pipeline/causal_inference.py) BOTH lists are warped. The
75
+ # released 1-/2-step checkpoints were trained under exactly this
76
+ # asymmetry, so we reproduce it rather than silently "fix" it: block 0
77
+ # is masked out of the DMD gradient
78
+ # (`gradient_mask[:, :num_frame_per_block] = False`) and the DMD
79
+ # timestep window below is derived from the interior schedule, so the
80
+ # first-chunk timesteps only shape the rollout that later blocks
81
+ # condition on. Changing this would invalidate the published numbers.
82
+ self.denoising_step_list_first_chunk = denoising_step_list_first_chunk
83
+ if self.denoising_step_list_first_chunk is not None:
84
+ if self.denoising_step_list_first_chunk[-1] == 0:
85
+ self.denoising_step_list_first_chunk = self.denoising_step_list_first_chunk[:-1]
86
+
87
+ # Wan specific hyperparameters.
88
+ # num_transformer_blocks: MG2 base has num_layers=30.
89
+ self.num_transformer_blocks = 30
90
+ # frame_seq_length is (latent_H / patch_H) * (latent_W / patch_W)
91
+ # with patch_size=(1,2,2):
92
+ # - 480p (60x104 latent) -> 30 * 52 = 1560
93
+ # - 360p (44x80 latent) -> 22 * 40 = 880 โ† MG2 native
94
+ # Prefer to pass this explicitly from config; otherwise fall back
95
+ # to the legacy 1560 default for back-compat with T2V Wan-1.3B configs.
96
+ self.frame_seq_length = frame_seq_length if frame_seq_length is not None else 1560
97
+ self.num_frame_per_block = num_frame_per_block
98
+ self.context_noise = context_noise
99
+ self.i2v = False
100
+ self.vae_time_compression_ratio = vae_time_compression_ratio
101
+
102
+ self.kv_cache1 = None
103
+ self.kv_cache2 = None
104
+ self.kv_cache_mouse = None
105
+ self.kv_cache_keyboard = None
106
+ self.use_action = use_action
107
+ # When the generator is MG2 (action-conditioned), the cross-attention
108
+ # context is CLIP visual_context of length 257, not legacy T5 text
109
+ # embeddings of length 512.
110
+ self._use_mg2_ctx = use_action
111
+ self.independent_first_frame = independent_first_frame
112
+ self.same_step_across_blocks = same_step_across_blocks
113
+ self.last_step_only = last_step_only
114
+ # IMPORTANT: train-time KV cache is sized to the FULL rollout
115
+ # (num_max_frames frames), regardless of local_attn_size. This
116
+ # mirrors SF original (Self-Forcing/pipeline/self_forcing_training.py:39).
117
+ #
118
+ # Why: the sliding window during training is enforced by the
119
+ # _prepare_blockwise_causal_attn_mask FUNCTION (causal_model.py:535-540),
120
+ # NOT by physically truncating the KV cache. Training cache stays
121
+ # full-size so the eviction-shift code path in causal_model.py L228
122
+ # never triggers โ€” that path mutates kv_cache["k"]/["v"] in-place,
123
+ # which breaks gradient_checkpointing's recompute (the recomputed
124
+ # forward sees a different cache state than the original forward,
125
+ # producing a CheckpointError "saved metadata != recomputed metadata").
126
+ #
127
+ # Inference (pipeline/causal_inference.py:339) sizes the cache to
128
+ # `local_attn_size * frame_seq_length` and DOES trigger eviction โ€”
129
+ # because at inference the cache is single-pass and has no
130
+ # backward to recompute. Training and inference behaviors differ;
131
+ # the mask function is what aligns them semantically.
132
+ self.num_max_frames = num_max_frames
133
+ self.kv_cache_size = num_max_frames * self.frame_seq_length
134
+
135
+ def generate_and_sync_list(self, num_blocks, num_denoising_steps, device):
136
+ rank = dist.get_rank() if dist.is_initialized() else 0
137
+
138
+ if rank == 0:
139
+ # Generate random indices
140
+ indices = torch.randint(
141
+ low=0,
142
+ high=num_denoising_steps,
143
+ size=(num_blocks,),
144
+ device=device
145
+ )
146
+ # In our training, self.last_step_only is False
147
+ if self.last_step_only:
148
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
149
+ else:
150
+ indices = torch.empty(num_blocks, dtype=torch.long, device=device)
151
+
152
+ dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
153
+ return indices.tolist()
154
+
155
+ def inference_with_trajectory(
156
+ self,
157
+ noise: torch.Tensor,
158
+ clean_image_or_video: torch.Tensor = None, # same shape as noise
159
+ initial_latent: Optional[torch.Tensor] = None,
160
+ return_sim_step: bool = False,
161
+ **conditional_dict
162
+ ) -> torch.Tensor:
163
+ batch_size, num_frames, num_channels, height, width = noise.shape
164
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
165
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
166
+ # noise should still be a multiple of num_frame_per_block
167
+ assert num_frames % self.num_frame_per_block == 0
168
+ num_blocks = num_frames // self.num_frame_per_block
169
+ else:
170
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
171
+ assert (num_frames - 1) % self.num_frame_per_block == 0
172
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
173
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
174
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
175
+ output = torch.zeros(
176
+ [batch_size, num_output_frames, num_channels, height, width],
177
+ device=noise.device,
178
+ dtype=noise.dtype
179
+ )
180
+
181
+ # Step 1: Initialize KV cache to all zeros
182
+ self._initialize_kv_cache(
183
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
184
+ )
185
+ self._initialize_crossattn_cache(
186
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
187
+ )
188
+ if self.use_action:
189
+ self._initialize_kv_cache_mouse_and_keyboard(
190
+ batch_size=batch_size, dtype=noise.dtype, device=noise.device
191
+ )
192
+
193
+
194
+ # Step 2: Cache context feature
195
+ current_start_frame = 0
196
+ if initial_latent is not None: # Never met
197
+ timestep = torch.ones([batch_size, 1], device=noise.device, dtype=torch.int64) * 0
198
+ # Cast initial_latent to the same dtype as noise so the ref-frame
199
+ # DiT forward doesn't hit a float/bfloat16 mismatch in
200
+ # patch_embedding (FSDP keeps model params bf16 under
201
+ # mixed_precision).
202
+ initial_latent = initial_latent.to(dtype=noise.dtype)
203
+ # Assume num_input_frames is 1 + self.num_frame_per_block * num_input_blocks
204
+ output[:, :1] = initial_latent
205
+ # The ref-frame init is a 1-frame forward. If the model's
206
+ # num_frame_per_block > 1, temporarily flip it to 1 so that the
207
+ # ActionModule's internal assertions (which assume one forward
208
+ # call processes exactly num_frame_per_block latent frames) hold.
209
+ # NOTE: the generator is FSDP-wrapped, so writing to
210
+ # `self.generator.model.num_frame_per_block` actually mutates
211
+ # the outer FSDP shell. To reach the inner CausalWanModel
212
+ # (which `kwargs["num_frame_per_block"] = self.num_frame_per_block`
213
+ # reads at forward time), we walk through `_fsdp_wrapped_module`
214
+ # if present.
215
+ def _inner_model(m):
216
+ # Unwrap FSDP / checkpoint wrappers to get the real CausalWanModel
217
+ while hasattr(m, "_fsdp_wrapped_module"):
218
+ m = m._fsdp_wrapped_module
219
+ if hasattr(m, "_checkpoint_wrapped_module"):
220
+ m = m._checkpoint_wrapped_module
221
+ return m
222
+ _inner = _inner_model(self.generator.model)
223
+ _orig_nfpb = getattr(_inner, "num_frame_per_block", 1)
224
+ if _orig_nfpb != 1:
225
+ _inner.num_frame_per_block = 1
226
+ try:
227
+ with torch.no_grad():
228
+ self.generator(
229
+ noisy_image_or_video=initial_latent,
230
+ conditional_dict=_slice_cond_for_block(
231
+ conditional_dict, current_start_frame, 1,
232
+ self.vae_time_compression_ratio),
233
+ timestep=timestep * 0,
234
+ kv_cache=self.kv_cache1,
235
+ crossattn_cache=self.crossattn_cache,
236
+ kv_cache_mouse=self.kv_cache_mouse,
237
+ kv_cache_keyboard=self.kv_cache_keyboard,
238
+ current_start=current_start_frame * self.frame_seq_length
239
+ )
240
+ finally:
241
+ if _orig_nfpb != 1:
242
+ _inner.num_frame_per_block = _orig_nfpb
243
+ current_start_frame += 1
244
+
245
+ # Step 3: Temporal denoising loop
246
+ all_num_frames = [self.num_frame_per_block] * num_blocks
247
+ # In out training, self.independent_first_frame is False
248
+ if self.independent_first_frame and initial_latent is None:
249
+ all_num_frames = [1] + all_num_frames
250
+ num_denoising_steps = len(self.denoising_step_list)
251
+ has_first = self.denoising_step_list_first_chunk is not None
252
+ # FFE gives chunk 0 a schedule of a different length, so its exit
253
+ # flag has to be drawn from its own range. Every other chunk shares
254
+ # the interior range as before.
255
+ exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
256
+ if has_first:
257
+ exit_flag_first = self.generate_and_sync_list(
258
+ 1, len(self.denoising_step_list_first_chunk), device=noise.device)[0]
259
+ else:
260
+ exit_flag_first = None
261
+ start_gradient_frame_index = num_output_frames - 21
262
+
263
+ # for block_index in range(num_blocks):
264
+ for block_index, current_num_frames in enumerate(all_num_frames):
265
+ # Slice the per-frame conditioning (cond_concat / mouse / kbd)
266
+ # to cover only the current block's latent frames. visual_context
267
+ # is first-frame CLIP, stays as-is.
268
+ block_cond = _slice_cond_for_block(
269
+ conditional_dict, current_start_frame, current_num_frames,
270
+ self.vae_time_compression_ratio,
271
+ )
272
+
273
+ if True:
274
+ noisy_input = noise[
275
+ :, current_start_frame - num_input_frames:current_start_frame + current_num_frames - num_input_frames]
276
+
277
+ # FFE: chunk 0 runs the (longer) first-chunk schedule; all
278
+ # later chunks run the main schedule.
279
+ if has_first and block_index == 0:
280
+ current_denoising_list = self.denoising_step_list_first_chunk
281
+ current_exit_flag_index = exit_flag_first
282
+ else:
283
+ current_denoising_list = self.denoising_step_list
284
+ current_exit_flag_index = (
285
+ exit_flags[0] if self.same_step_across_blocks else exit_flags[block_index])
286
+
287
+ # Step 3.1: Spatial denoising loop
288
+ for index, current_timestep in enumerate(current_denoising_list):
289
+ exit_flag = (index == current_exit_flag_index)
290
+ timestep = torch.ones(
291
+ [batch_size, current_num_frames],
292
+ device=noise.device,
293
+ dtype=torch.int64) * current_timestep
294
+
295
+ if not exit_flag:
296
+ with torch.no_grad():
297
+ _, denoised_pred = self.generator(
298
+ noisy_image_or_video=noisy_input,
299
+ conditional_dict=block_cond,
300
+ timestep=timestep,
301
+ kv_cache=self.kv_cache1,
302
+ crossattn_cache=self.crossattn_cache,
303
+ kv_cache_mouse=self.kv_cache_mouse,
304
+ kv_cache_keyboard=self.kv_cache_keyboard,
305
+ current_start=current_start_frame * self.frame_seq_length
306
+ )
307
+ next_timestep = current_denoising_list[index + 1]
308
+ noisy_input = self.scheduler.add_noise(
309
+ denoised_pred.flatten(0, 1),
310
+ torch.randn_like(denoised_pred.flatten(0, 1)),
311
+ next_timestep * torch.ones(
312
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
313
+ ).unflatten(0, denoised_pred.shape[:2])
314
+ else:
315
+ if current_start_frame < start_gradient_frame_index:
316
+ with torch.no_grad():
317
+ _, denoised_pred = self.generator(
318
+ noisy_image_or_video=noisy_input,
319
+ conditional_dict=block_cond,
320
+ timestep=timestep,
321
+ kv_cache=self.kv_cache1,
322
+ crossattn_cache=self.crossattn_cache,
323
+ kv_cache_mouse=self.kv_cache_mouse,
324
+ kv_cache_keyboard=self.kv_cache_keyboard,
325
+ current_start=current_start_frame * self.frame_seq_length
326
+ )
327
+ else: # enable grad
328
+ _, denoised_pred = self.generator(
329
+ noisy_image_or_video=noisy_input,
330
+ conditional_dict=block_cond,
331
+ timestep=timestep,
332
+ kv_cache=self.kv_cache1,
333
+ crossattn_cache=self.crossattn_cache,
334
+ kv_cache_mouse=self.kv_cache_mouse,
335
+ kv_cache_keyboard=self.kv_cache_keyboard,
336
+ current_start=current_start_frame * self.frame_seq_length
337
+ )
338
+ break
339
+
340
+ # Step 3.2: record the model's output
341
+ output[:, current_start_frame:current_start_frame + current_num_frames] = denoised_pred
342
+
343
+ # Step 3.3: rerun with timestep zero to update the cache
344
+ #
345
+ # โ”€โ”€โ”€ Cache-refresh fix โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
346
+ # Previously we skipped this rerun for grad-enabled blocks (i.e.
347
+ # all chunks 1..6 when num_training_frames=22 since
348
+ # start_gradient_frame_index = num_output_frames - 21 = 1).
349
+ # That created a train/inference distribution shift on the KV
350
+ # cache: at inference each chunk goes through the full 4-step
351
+ # denoising chain so its cache is near-clean (โ‰ˆ t=0); at training
352
+ # we used to leave cache at the exit_flags step (often noisy).
353
+ #
354
+ # CF orig / minWM / SF orig all run this refresh unconditionally.
355
+ # Restoring the original behavior closes the gap and empirically
356
+ # fixes HUD shrinkage / OOD drift on long-video rollouts.
357
+ #
358
+ # The previous "skip" branch is kept commented below for
359
+ # reference; do not re-enable without re-analyzing.
360
+ #
361
+ # was_grad_block = (current_start_frame >= start_gradient_frame_index)
362
+ # if was_grad_block:
363
+ # current_start_frame += current_num_frames
364
+ # continue
365
+ # โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
366
+
367
+
368
+ context_timestep = torch.ones_like(timestep) * self.context_noise
369
+ # add context noise
370
+ denoised_pred = self.scheduler.add_noise(
371
+ denoised_pred.flatten(0, 1),
372
+ torch.randn_like(denoised_pred.flatten(0, 1)),
373
+ context_timestep * torch.ones(
374
+ [batch_size * current_num_frames], device=noise.device, dtype=torch.long)
375
+ ).unflatten(0, denoised_pred.shape[:2])
376
+ with torch.no_grad():
377
+ self.generator(
378
+ noisy_image_or_video=denoised_pred,
379
+ conditional_dict=block_cond,
380
+ timestep=context_timestep,
381
+ kv_cache=self.kv_cache1,
382
+ crossattn_cache=self.crossattn_cache,
383
+ kv_cache_mouse=self.kv_cache_mouse,
384
+ kv_cache_keyboard=self.kv_cache_keyboard,
385
+ current_start=current_start_frame * self.frame_seq_length
386
+ )
387
+
388
+ # Step 3.4: update the start and end frame indices
389
+ current_start_frame += current_num_frames
390
+
391
+ # Step 3.5: Return the denoised timestep
392
+ if not self.same_step_across_blocks: # Useless, never met
393
+ denoised_timestep_from, denoised_timestep_to = None, None
394
+ # T -> \tau_1 -> \tau_2 ->...-> \tau โ€”โ€” enable grad โ€”โ€”> 0
395
+ # denoised_timestep_from = \tau
396
+ # denoised_timestep_to = next timestep smaller than \tau
397
+ # These are just engineering tricks
398
+ # to align DMD timestep sampling with the actual denoising range used by the generator
399
+ # Under FFE this window is deliberately derived from the INTERIOR
400
+ # schedule (`exit_flags[0]`), never from the first-chunk one: chunk 0
401
+ # is excluded from the DMD gradient, so the loss should be aligned
402
+ # with the schedule the supervised blocks actually ran.
403
+ elif exit_flags[0] == len(self.denoising_step_list) - 1:
404
+ # corner case when \tau is the smallest non-zero timestep
405
+ denoised_timestep_to = 0
406
+ denoised_timestep_from = 1000 - torch.argmin(
407
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
408
+ else:
409
+ denoised_timestep_to = 1000 - torch.argmin(
410
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
411
+ denoised_timestep_from = 1000 - torch.argmin(
412
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
413
+
414
+ if return_sim_step: # False
415
+ return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
416
+
417
+ return output, denoised_timestep_from, denoised_timestep_to
418
+
419
+ def _initialize_kv_cache(self, batch_size, dtype, device):
420
+ """
421
+ Initialize a Per-GPU KV cache for the Wan model.
422
+ MG2 base: num_heads=12, head_dim=128.
423
+ """
424
+ kv_cache1 = []
425
+
426
+ for _ in range(self.num_transformer_blocks):
427
+ kv_cache1.append({
428
+ "k": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
429
+ "v": torch.zeros([batch_size, self.kv_cache_size, 12, 128], dtype=dtype, device=device),
430
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
431
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device)
432
+ })
433
+
434
+ self.kv_cache1 = kv_cache1 # always store the clean cache
435
+
436
+ def _initialize_kv_cache_mouse_and_keyboard(self, batch_size, dtype, device):
437
+ """Initialize per-block KV caches for the MG2 ActionModule's
438
+ mouse / keyboard attention. Only needed when the generator is
439
+ an action-conditioned MG2 model.
440
+
441
+ Shapes follow pipeline/causal_diffusion_inference.py:
442
+ - keyboard: [B, cache_size, 16, 64] (heads_num=16, head_dim=64)
443
+ - mouse: [B * frame_seq, cache_size, 16, 64] (per spatial token)
444
+ """
445
+ kv_cache_mouse = []
446
+ kv_cache_keyboard = []
447
+ # Mouse/keyboard kv_cache is sized per-LATENT-FRAME (not per-token).
448
+ #
449
+ # IMPORTANT: at TRAIN time we always size to num_max_frames (the full
450
+ # rollout), regardless of local_attn_size. Same reason as the main
451
+ # KV cache (see __init__ comment): if we sized to local_attn_size
452
+ # here, the eviction code path in ActionModule would mutate
453
+ # k/v in-place between forward and gradient_checkpointing's
454
+ # recompute, producing
455
+ # CheckpointError: Recomputed values have different metadata
456
+ # saved [880, 6, 16, 64] vs recomputed [880, 3, 16, 64]
457
+ # The sliding window is enforced semantically by the attention
458
+ # mask, NOT by physically truncating the cache. Inference uses
459
+ # local_attn_size sizing (single-pass, no backward) โ€” see
460
+ # pipeline/causal_inference.py for the inference path.
461
+ kv_cache_size = self.num_max_frames
462
+ for _ in range(self.num_transformer_blocks):
463
+ kv_cache_keyboard.append({
464
+ "k": torch.zeros([batch_size, kv_cache_size, 16, 64], dtype=dtype, device=device),
465
+ "v": torch.zeros([batch_size, kv_cache_size, 16, 64], dtype=dtype, device=device),
466
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
467
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
468
+ })
469
+ kv_cache_mouse.append({
470
+ "k": torch.zeros([batch_size * self.frame_seq_length, kv_cache_size, 16, 64], dtype=dtype, device=device),
471
+ "v": torch.zeros([batch_size * self.frame_seq_length, kv_cache_size, 16, 64], dtype=dtype, device=device),
472
+ "global_end_index": torch.tensor([0], dtype=torch.long, device=device),
473
+ "local_end_index": torch.tensor([0], dtype=torch.long, device=device),
474
+ })
475
+ self.kv_cache_mouse = kv_cache_mouse
476
+ self.kv_cache_keyboard = kv_cache_keyboard
477
+
478
+ def _initialize_crossattn_cache(self, batch_size, dtype, device):
479
+ """
480
+ Initialize a Per-GPU cross-attention cache for the Wan model.
481
+
482
+ For MG2 (I2V, CLIP visual_context) the context length is 257
483
+ (16x16 ViT tokens + cls token). For legacy T2V (T5 text) it
484
+ was 512. We pick based on whether action_config is present โ€”
485
+ a proxy for "this is MG2".
486
+ """
487
+ ctx_len = 257 if getattr(self, "_use_mg2_ctx", True) else 512
488
+ crossattn_cache = []
489
+ for _ in range(self.num_transformer_blocks):
490
+ crossattn_cache.append({
491
+ "k": torch.zeros([batch_size, ctx_len, 12, 128], dtype=dtype, device=device),
492
+ "v": torch.zeros([batch_size, ctx_len, 12, 128], dtype=dtype, device=device),
493
+ "is_init": False
494
+ })
495
+ self.crossattn_cache = crossattn_cache
pipeline/teacher_forcing_training.py ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from utils.wan_wrapper import WanDiffusionWrapper
2
+ from utils.scheduler import SchedulerInterface
3
+ from typing import List, Optional
4
+ import torch
5
+ import torch.distributed as dist
6
+
7
+
8
+ class TeacherForcingTrainingPipeline:
9
+ def __init__(self,
10
+ denoising_step_list: List[int],
11
+ scheduler: SchedulerInterface,
12
+ generator: WanDiffusionWrapper,
13
+ num_frame_per_block=3,
14
+ independent_first_frame: bool = False,
15
+ same_step_across_blocks: bool = False,
16
+ last_step_only: bool = False,
17
+ num_max_frames: int = 21,
18
+ context_noise: int = 0,
19
+ spatial_self: bool = True,
20
+ **kwargs):
21
+ super().__init__()
22
+ self.scheduler = scheduler
23
+ self.generator = generator
24
+ self.denoising_step_list = denoising_step_list
25
+ if self.denoising_step_list[-1] == 0:
26
+ self.denoising_step_list = self.denoising_step_list[:-1] # remove the zero timestep for inference
27
+
28
+ # Wan specific hyperparameters
29
+ self.num_transformer_blocks = 30
30
+ self.frame_seq_length = 1560
31
+ self.num_frame_per_block = num_frame_per_block
32
+ self.context_noise = context_noise
33
+ self.i2v = False
34
+
35
+ self.kv_cache1 = None
36
+ self.kv_cache2 = None
37
+ self.independent_first_frame = independent_first_frame
38
+ self.same_step_across_blocks = same_step_across_blocks
39
+ self.last_step_only = last_step_only
40
+ self.kv_cache_size = num_max_frames * self.frame_seq_length
41
+
42
+ self.spatial_self = spatial_self
43
+
44
+ def generate_and_sync_list(self, num_blocks, num_denoising_steps, device):
45
+ rank = dist.get_rank() if dist.is_initialized() else 0
46
+
47
+ if rank == 0:
48
+ # Generate random indices
49
+ indices = torch.randint(
50
+ low=0,
51
+ high=num_denoising_steps,
52
+ size=(num_blocks,),
53
+ device=device
54
+ )
55
+ # In our training, self.last_step_only is False
56
+ if self.last_step_only:
57
+ indices = torch.ones_like(indices) * (num_denoising_steps - 1)
58
+ else:
59
+ indices = torch.empty(num_blocks, dtype=torch.long, device=device)
60
+
61
+ dist.broadcast(indices, src=0) # Broadcast the random indices to all ranks
62
+ return indices.tolist()
63
+
64
+ def inference_with_trajectory(
65
+ self,
66
+ noise: torch.Tensor,
67
+ clean_image_or_video: torch.Tensor, # same shape as noise
68
+ initial_latent: Optional[torch.Tensor] = None,
69
+ return_sim_step: bool = False,
70
+ **conditional_dict
71
+ ) -> torch.Tensor:
72
+ batch_size, num_frames, num_channels, height, width = noise.shape
73
+ if not self.independent_first_frame or (self.independent_first_frame and initial_latent is not None):
74
+ # If the first frame is independent and the first frame is provided, then the number of frames in the
75
+ # noise should still be a multiple of num_frame_per_block
76
+ assert num_frames % self.num_frame_per_block == 0
77
+ num_blocks = num_frames // self.num_frame_per_block
78
+ else:
79
+ # Using a [1, 4, 4, 4, 4, 4, ...] model to generate a video without image conditioning
80
+ assert (num_frames - 1) % self.num_frame_per_block == 0
81
+ num_blocks = (num_frames - 1) // self.num_frame_per_block
82
+ num_input_frames = initial_latent.shape[1] if initial_latent is not None else 0
83
+ num_output_frames = num_frames + num_input_frames # add the initial latent frames
84
+ output = torch.zeros(
85
+ [batch_size, num_output_frames, num_channels, height, width],
86
+ device=noise.device,
87
+ dtype=noise.dtype
88
+ )
89
+
90
+ # Step 3: Temporal denoising loop
91
+ all_num_frames = [self.num_frame_per_block] * num_blocks
92
+ num_denoising_steps = len(self.denoising_step_list)
93
+ exit_flags = self.generate_and_sync_list(len(all_num_frames), num_denoising_steps, device=noise.device)
94
+ start_gradient_frame_index = num_output_frames - 21 # always 0 as long as we train 21 latent frames
95
+ if start_gradient_frame_index != 0:
96
+ raise NotImplementedError("start_gradient_frame_index is always 0 as long as we train 21 latent frames")
97
+
98
+ if self.spatial_self:
99
+ noisy_input = noise
100
+ # Step 3.1: Spatial denoising loop
101
+ # Such a loop corresponds to the truncated denoising algorithm:
102
+ # T -> \tau_1 -> \tau_2 ->...-> \tau โ€”โ€” enable grad โ€”โ€”> 0
103
+ # For many-step model, we certainly cannot use this method, but for 4-step DMD,
104
+ # we can inherit it for a fair comaprison. Note that as long as the conditions
105
+ # are clean GT rather than self-generated frames, we can perform TF. So this
106
+ # method does not conflict with TF in the frame- dimension.
107
+ for index, current_timestep in enumerate(self.denoising_step_list):
108
+ # self.same_step_across_blocks is True
109
+ if self.same_step_across_blocks:
110
+ exit_flag = (index == exit_flags[0])
111
+ else:
112
+ raise NotImplementedError('Here t is a scalar denoting that all chunks are at the same t, but in the future we may set t a tensor denoting different chunks') # Only backprop at the randomly selected timestep (consistent across all ranks)
113
+ timestep = torch.ones(
114
+ [batch_size, self.num_frame_per_block*num_blocks],
115
+ device=noise.device,
116
+ dtype=torch.int64) * current_timestep
117
+
118
+ if not exit_flag:
119
+ with torch.no_grad():
120
+ _,denoised_pred = self.generator(
121
+ noisy_image_or_video=noisy_input,
122
+ conditional_dict=conditional_dict,
123
+ timestep=timestep,
124
+ clean_x = clean_image_or_video
125
+ )
126
+ next_timestep = self.denoising_step_list[index + 1]
127
+ noisy_input = self.scheduler.add_noise(
128
+ denoised_pred.flatten(0, 1),
129
+ torch.randn_like(denoised_pred.flatten(0, 1)),
130
+ next_timestep * torch.ones(
131
+ [batch_size * self.num_frame_per_block*num_blocks], device=noise.device, dtype=torch.long)
132
+ ).unflatten(0, denoised_pred.shape[:2])
133
+ else:
134
+ # for getting real output
135
+ # with torch.set_grad_enabled(current_start_frame >= start_gradient_frame_index):
136
+ # enable grad
137
+
138
+ _,output = self.generator(
139
+ noisy_image_or_video=noisy_input,
140
+ conditional_dict=conditional_dict,
141
+ timestep=timestep,
142
+ clean_x = clean_image_or_video
143
+ )
144
+ break
145
+ # ======================= SF -> TF modification ends ============================
146
+
147
+ # Step 3.5: Return the denoised timestep
148
+ if not self.same_step_across_blocks: # Useless, never met
149
+ denoised_timestep_from, denoised_timestep_to = None, None
150
+ # T -> \tau_1 -> \tau_2 ->...-> \tau โ€”โ€” enable grad โ€”โ€”> 0
151
+ # denoised_timestep_from = \tau
152
+ # denoised_timestep_to = next timestep smaller than \tau
153
+ # These are just engineering tricks
154
+ # to align DMD timestep sampling with the actual denoising range used by the generator
155
+ elif exit_flags[0] == len(self.denoising_step_list) - 1:
156
+ # corner case when \tau is the smallest non-zero timestep
157
+ denoised_timestep_to = 0
158
+ denoised_timestep_from = 1000 - torch.argmin(
159
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
160
+ else:
161
+ denoised_timestep_to = 1000 - torch.argmin(
162
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
163
+ denoised_timestep_from = 1000 - torch.argmin(
164
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
165
+ else:
166
+ print('add noise from gt')
167
+ current_timestep = self.denoising_step_list[exit_flags[0]]
168
+ timestep = torch.ones(
169
+ [batch_size, self.num_frame_per_block*num_blocks],
170
+ device=noise.device,
171
+ dtype=torch.int64) * current_timestep
172
+
173
+ noisy_input = self.scheduler.add_noise(
174
+ clean_image_or_video,
175
+ torch.randn_like(clean_image_or_video),
176
+ timestep,
177
+ )
178
+ assert clean_image_or_video.shape == noisy_input.shape
179
+
180
+
181
+ _,output = self.generator(
182
+ noisy_image_or_video=noisy_input,
183
+ conditional_dict=conditional_dict,
184
+ timestep=timestep,
185
+ clean_x = clean_image_or_video
186
+ )
187
+
188
+ # T -> \tau_1 -> \tau_2 ->...-> \tau โ€”โ€” enable grad โ€”โ€”> 0
189
+ # denoised_timestep_from = \tau
190
+ # denoised_timestep_to = next timestep smaller than \tau
191
+ # These are just engineering tricks
192
+ # to align DMD timestep sampling with the actual denoising range used by the generator
193
+ if exit_flags[0] == len(self.denoising_step_list) - 1:
194
+ # corner case when \tau is the smallest non-zero timestep
195
+ denoised_timestep_to = 0
196
+ denoised_timestep_from = 1000 - torch.argmin(
197
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
198
+ else:
199
+ denoised_timestep_to = 1000 - torch.argmin(
200
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0] + 1].cuda()).abs(), dim=0).item()
201
+ denoised_timestep_from = 1000 - torch.argmin(
202
+ (self.scheduler.timesteps.cuda() - self.denoising_step_list[exit_flags[0]].cuda()).abs(), dim=0).item()
203
+
204
+
205
+ if return_sim_step: # False
206
+ return output, denoised_timestep_from, denoised_timestep_to, exit_flags[0] + 1
207
+
208
+ return output, denoised_timestep_from, denoised_timestep_to
209
+
210
+
requirements.txt CHANGED
@@ -1,16 +1,15 @@
1
  torchvision
2
- einops
3
  omegaconf
 
 
 
 
 
 
 
4
  diffusers
5
  transformers
6
  accelerate
7
- safetensors
8
- sentencepiece
9
- numpy
10
- pillow
11
  ftfy
12
  regex
13
- tqdm
14
- imageio[ffmpeg]
15
- imageio-ffmpeg
16
- https://huggingface.co/datasets/multimodalart/zerogpu-blackwell-wheels/resolve/main/wheels/pt211-cu130-cp312/flash_attn-2.8.3-cp312-cp312-linux_x86_64.whl
 
1
  torchvision
 
2
  omegaconf
3
+ einops
4
+ numpy
5
+ pillow
6
+ tqdm
7
+ imageio
8
+ imageio-ffmpeg
9
+ safetensors
10
  diffusers
11
  transformers
12
  accelerate
 
 
 
 
13
  ftfy
14
  regex
15
+ sentencepiece