multimodalart HF Staff commited on
Commit
e35b35d
·
verified ·
1 Parent(s): 13239ba

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ examples/observation.images.top.png filter=lfs diff=lfs merge=lfs -text
37
+ examples/observation.images.wrist.png filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,13 +1,43 @@
1
  ---
2
  title: Next Forcing World Model
3
- emoji: 💻
4
- colorFrom: indigo
5
- colorTo: blue
6
  sdk: gradio
7
  sdk_version: 6.25.0
8
- python_version: '3.12'
9
  app_file: app.py
10
- pinned: false
 
 
11
  ---
12
 
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
  title: Next Forcing World Model
3
+ emoji: 🎬
4
+ colorFrom: purple
5
+ colorTo: red
6
  sdk: gradio
7
  sdk_version: 6.25.0
 
8
  app_file: app.py
9
+ short_description: Causal world model for robot video generation
10
+ python_version: "3.12"
11
+ startup_duration_timeout: 1h
12
  ---
13
 
14
+ # Next Forcing: Causal World Modeling with Multi-Chunk Prediction
15
+
16
+ This Space demonstrates **Next Forcing**, a causal autoregressive world model that generates robot manipulation video
17
+ from initial camera observations and a text instruction.
18
+
19
+ ## How it works
20
+
21
+ 1. Provide top-down and wrist camera images (the initial scene).
22
+ 2. Describe the manipulation task in natural language.
23
+ 3. The model autoregressively generates future video chunks (4 frames per chunk).
24
+
25
+ The model is a 5.1B parameter causal video-action transformer (based on the Wan2.1 architecture) that jointly
26
+ denoises video latents and action latents in a causal, chunk-by-chunk fashion.
27
+
28
+ ## Model
29
+
30
+ - **Checkpoint**: [`gangweix/next-forcing-base`](https://huggingface.co/gangweix/next-forcing-base) (5.1B, BF16)
31
+ - **Paper**: [arXiv:2606.11187](https://arxiv.org/abs/2606.11187)
32
+ - **Code**: [github.com/gangweix/next-forcing](https://github.com/gangweix/next-forcing)
33
+
34
+ ## Reference
35
+
36
+ ```
37
+ @article{xu2026next,
38
+ title={Next Forcing: Causal World Modeling with Multi-Chunk Prediction},
39
+ author={Xu, Gangwei and Zhang, Qihang and Zhou, Jiaming and Zhu, Xing and Shen, Yujun and Yang, Xin and Xu, Yinghao},
40
+ journal={arXiv preprint arXiv:2606.11187},
41
+ year={2026}
42
+ }
43
+ ```
app.py ADDED
@@ -0,0 +1,651 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ os.environ.setdefault("PYTORCH_CUDA_ALLOC_CONF", "expandable_segments:True")
3
+ import spaces
4
+ import sys
5
+ import time
6
+ import tempfile
7
+ import numpy as np
8
+ import torch
9
+ import torch.nn.functional as F
10
+ import gradio as gr
11
+ from PIL import Image
12
+ from einops import rearrange
13
+ from tqdm import tqdm
14
+
15
+ # ------------------------------------------------------------------ #
16
+ # Model + config setup (module scope, eagerly on GPU)
17
+ # ------------------------------------------------------------------ #
18
+
19
+ MODEL_ID = "gangweix/next-forcing-base"
20
+
21
+ from huggingface_hub import snapshot_download
22
+
23
+ _model_path = snapshot_download(
24
+ MODEL_ID,
25
+ repo_type="model",
26
+ allow_patterns=[
27
+ "transformer/*",
28
+ "vae/*",
29
+ "text_encoder/*",
30
+ "tokenizer/*",
31
+ ],
32
+ )
33
+
34
+ from wan_va.modules.utils import (
35
+ WanVAEStreamingWrapper,
36
+ load_text_encoder,
37
+ load_tokenizer,
38
+ load_transformer,
39
+ load_vae,
40
+ )
41
+ from wan_va.utils.scheduler import FlowMatchScheduler
42
+ from wan_va.utils.utils import get_mesh_id, data_seq_to_patch
43
+
44
+ DTYPE = torch.bfloat16
45
+ DEVICE = "cuda"
46
+
47
+ # ---- Demo config (matches va_demo_cfg.py) ----
48
+ CONFIG = dict(
49
+ attn_window=30,
50
+ frame_chunk_size=4,
51
+ env_type="none",
52
+ height=256,
53
+ width=256,
54
+ action_dim=30,
55
+ action_per_frame=8,
56
+ obs_cam_keys=["observation.images.top", "observation.images.wrist"],
57
+ guidance_scale=5,
58
+ action_guidance_scale=1,
59
+ num_inference_steps=5,
60
+ video_exec_step=-1,
61
+ action_num_inference_steps=10,
62
+ snr_shift=5.0,
63
+ action_snr_shift=1.0,
64
+ patch_size=(1, 2, 2),
65
+ used_action_channel_ids=list(range(0, 5)) + list(range(28, 29)),
66
+ action_norm_method="quantiles",
67
+ norm_stat={
68
+ "q01": [
69
+ -90.60303497314453,
70
+ -98.73043060302734,
71
+ -79.9008560180664,
72
+ 48.95470428466797,
73
+ -32.794578552246094,
74
+ ] + [0.0] * 23 + [0.8250824809074402, 0],
75
+ "q99": [
76
+ 71.735107421875,
77
+ 65.89081573486328,
78
+ 92.87967681884766,
79
+ 100.0,
80
+ 22.784151077270508,
81
+ ] + [0.0] * 23 + [100.0, 0],
82
+ },
83
+ )
84
+
85
+ # Inverse action channel mapping
86
+ inverse_used_action_channel_ids = [len(CONFIG["used_action_channel_ids"])] * CONFIG[
87
+ "action_dim"
88
+ ]
89
+ for i, j in enumerate(CONFIG["used_action_channel_ids"]):
90
+ inverse_used_action_channel_ids[j] = i
91
+ CONFIG["inverse_used_action_channel_ids"] = inverse_used_action_channel_ids
92
+
93
+ # ---- Load model components ----
94
+ vae = load_vae(os.path.join(_model_path, "vae"), torch_dtype=DTYPE, torch_device=DEVICE)
95
+ streaming_vae = WanVAEStreamingWrapper(vae)
96
+ tokenizer = load_tokenizer(os.path.join(_model_path, "tokenizer"))
97
+ text_encoder = load_text_encoder(
98
+ os.path.join(_model_path, "text_encoder"), torch_dtype=DTYPE, torch_device=DEVICE
99
+ )
100
+ transformer = load_transformer(
101
+ os.path.join(_model_path, "transformer"),
102
+ torch_dtype=DTYPE,
103
+ torch_device=DEVICE,
104
+ attn_mode="torch",
105
+ disable_mcp=True,
106
+ )
107
+ transformer.eval().requires_grad_(False)
108
+
109
+ scheduler = FlowMatchScheduler(shift=CONFIG["snr_shift"], sigma_min=0.0, extra_one_step=True)
110
+ action_scheduler = FlowMatchScheduler(
111
+ shift=CONFIG["action_snr_shift"], sigma_min=0.0, extra_one_step=True
112
+ )
113
+ scheduler.set_timesteps(1000, training=True)
114
+ action_scheduler.set_timesteps(1000, training=True)
115
+
116
+ action_mask = torch.zeros([CONFIG["action_dim"]]).bool()
117
+ action_mask[CONFIG["used_action_channel_ids"]] = True
118
+
119
+ actions_q01 = torch.tensor(CONFIG["norm_stat"]["q01"], dtype=torch.float32).reshape(-1, 1, 1)
120
+ actions_q99 = torch.tensor(CONFIG["norm_stat"]["q99"], dtype=torch.float32).reshape(-1, 1, 1)
121
+
122
+ from diffusers.video_processor import VideoProcessor
123
+
124
+ video_processor = VideoProcessor(vae_scale_factor=1)
125
+
126
+
127
+ # ------------------------------------------------------------------ #
128
+ # Inference helpers
129
+ # ------------------------------------------------------------------ #
130
+
131
+ def _get_t5_prompt_embeds(prompt, max_sequence_length=512):
132
+ from diffusers.pipelines.wan.pipeline_wan import prompt_clean
133
+
134
+ prompt_list = [prompt] if isinstance(prompt, str) else prompt
135
+ prompt_list = [prompt_clean(u) for u in prompt_list]
136
+ batch_size = len(prompt_list)
137
+
138
+ text_inputs = tokenizer(
139
+ prompt_list,
140
+ padding="max_length",
141
+ max_length=max_sequence_length,
142
+ truncation=True,
143
+ add_special_tokens=True,
144
+ return_attention_mask=True,
145
+ return_tensors="pt",
146
+ )
147
+ text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
148
+ seq_lens = mask.gt(0).sum(dim=1).long()
149
+
150
+ text_encoder_device = next(text_encoder.parameters()).device
151
+ prompt_embeds = text_encoder(
152
+ text_input_ids.to(text_encoder_device), mask.to(text_encoder_device)
153
+ ).last_hidden_state
154
+ prompt_embeds = prompt_embeds.to(dtype=DTYPE, device=DEVICE)
155
+ prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
156
+ prompt_embeds = torch.stack(
157
+ [
158
+ torch.cat([u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))])
159
+ for u in prompt_embeds
160
+ ],
161
+ dim=0,
162
+ )
163
+ _, seq_len, _ = prompt_embeds.shape
164
+ prompt_embeds = prompt_embeds.repeat(1, 1, 1)
165
+ prompt_embeds = prompt_embeds.view(batch_size, seq_len, -1)
166
+ return prompt_embeds.to(DEVICE)
167
+
168
+
169
+ def encode_prompt(prompt):
170
+ prompt_embeds = _get_t5_prompt_embeds(prompt)
171
+ neg_prompt_embeds = _get_t5_prompt_embeds("")
172
+ return prompt_embeds, neg_prompt_embeds
173
+
174
+
175
+ def normalize_latents(latents, latents_mean, latents_std):
176
+ latents_mean = latents_mean.view(1, -1, 1, 1, 1).to(device=latents.device)
177
+ latents_std = latents_std.view(1, -1, 1, 1, 1).to(device=latents.device)
178
+ latents = ((latents.float() - latents_mean) * latents_std).to(latents)
179
+ return latents
180
+
181
+
182
+ def encode_obs(images_dict_list):
183
+ """Encode observation images into latent space.
184
+
185
+ Args:
186
+ images_dict_list: list of dicts, each mapping cam_key -> np.ndarray(H,W,3) uint8
187
+ """
188
+ images = images_dict_list
189
+ if not isinstance(images, list):
190
+ images = [images]
191
+ if len(images) < 1:
192
+ return None
193
+ videos = []
194
+ for k_i, k in enumerate(CONFIG["obs_cam_keys"]):
195
+ height_i, width_i = CONFIG["height"], CONFIG["width"]
196
+ history_video_k = (
197
+ torch.from_numpy(np.stack([each[k] for each in images]))
198
+ .float()
199
+ .permute(3, 0, 1, 2)
200
+ )
201
+ history_video_k = F.interpolate(
202
+ history_video_k,
203
+ size=(height_i, width_i),
204
+ mode="bilinear",
205
+ align_corners=False,
206
+ ).unsqueeze(0)
207
+ videos.append(history_video_k)
208
+
209
+ videos = torch.cat(videos, dim=0) / 255.0 * 2.0 - 1.0
210
+ vae_device = next(streaming_vae.vae.parameters()).device
211
+ videos_chunk = videos.to(vae_device).to(DTYPE)
212
+ enc_out = streaming_vae.encode_chunk(videos_chunk)
213
+
214
+ mu, logvar = torch.chunk(enc_out, 2, dim=1)
215
+ latents_mean = torch.tensor(vae.config.latents_mean).to(mu.device)
216
+ latents_std = torch.tensor(vae.config.latents_std).to(mu.device)
217
+ mu_norm = normalize_latents(mu, latents_mean, 1.0 / latents_std)
218
+ video_latent = torch.cat(mu_norm.split(1, dim=0), dim=-1)
219
+ return video_latent.to(DEVICE)
220
+
221
+
222
+ def _repeat_input_for_cfg(input_dict, use_cfg, prompt_embeds, negative_prompt_embeds):
223
+ if use_cfg:
224
+ input_dict["noisy_latents"] = input_dict["noisy_latents"].repeat(2, 1, 1, 1, 1)
225
+ input_dict["text_emb"] = torch.cat(
226
+ [prompt_embeds.to(DTYPE).clone(), negative_prompt_embeds.to(DTYPE).clone()],
227
+ dim=0,
228
+ )
229
+ input_dict["grid_id"] = input_dict["grid_id"][None].repeat(2, 1, 1)
230
+ input_dict["timesteps"] = input_dict["timesteps"][None].repeat(2, 1)
231
+ else:
232
+ input_dict["grid_id"] = input_dict["grid_id"][None]
233
+ input_dict["timesteps"] = input_dict["timesteps"][None]
234
+ return input_dict
235
+
236
+
237
+ def _prepare_latent_input(
238
+ latent_model_input,
239
+ action_model_input,
240
+ latent_t=0,
241
+ action_t=0,
242
+ latent_cond=None,
243
+ action_cond=None,
244
+ frame_st_id=0,
245
+ patch_size=(1, 2, 2),
246
+ prompt_embeds=None,
247
+ use_cfg=False,
248
+ negative_prompt_embeds=None,
249
+ ):
250
+ input_dict = dict()
251
+ if latent_model_input is not None:
252
+ input_dict["latent_res_lst"] = {
253
+ "noisy_latents": latent_model_input,
254
+ "timesteps": torch.ones(
255
+ [latent_model_input.shape[2]], dtype=torch.float32, device=DEVICE
256
+ ) * latent_t,
257
+ "grid_id": get_mesh_id(
258
+ latent_model_input.shape[-3] // patch_size[0],
259
+ latent_model_input.shape[-2] // patch_size[1],
260
+ latent_model_input.shape[-1] // patch_size[2],
261
+ 0,
262
+ 1,
263
+ frame_st_id,
264
+ ).to(DEVICE),
265
+ "text_emb": prompt_embeds.to(DTYPE).clone(),
266
+ }
267
+ if latent_cond is not None:
268
+ input_dict["latent_res_lst"]["noisy_latents"][:, :, 0:1] = latent_cond[:, :, 0:1]
269
+ input_dict["latent_res_lst"]["timesteps"][0:1] *= 0
270
+
271
+ if action_model_input is not None:
272
+ input_dict["action_res_lst"] = {
273
+ "noisy_latents": action_model_input,
274
+ "timesteps": torch.ones(
275
+ [action_model_input.shape[2]], dtype=torch.float32, device=DEVICE
276
+ ) * action_t,
277
+ "grid_id": get_mesh_id(
278
+ action_model_input.shape[-3],
279
+ action_model_input.shape[-2],
280
+ action_model_input.shape[-1],
281
+ 1,
282
+ 1,
283
+ frame_st_id,
284
+ action=True,
285
+ ).to(DEVICE),
286
+ "text_emb": prompt_embeds.to(DTYPE).clone(),
287
+ }
288
+ if action_cond is not None:
289
+ input_dict["action_res_lst"]["noisy_latents"][:, :, 0:1] = action_cond[:, :, 0:1]
290
+ input_dict["action_res_lst"]["timesteps"][0:1] *= 0
291
+ input_dict["action_res_lst"]["noisy_latents"][:, ~action_mask] *= 0
292
+ return input_dict
293
+
294
+
295
+ def infer_chunk(
296
+ init_latent,
297
+ frame_st_id,
298
+ prompt_embeds,
299
+ negative_prompt_embeds,
300
+ use_cfg,
301
+ guidance_scale,
302
+ action_guidance_scale,
303
+ num_chunks_to_infer,
304
+ ):
305
+ """Generate one video chunk (video latents + action latents)."""
306
+ frame_chunk_size = CONFIG["frame_chunk_size"]
307
+ latent_height = CONFIG["height"] // 16
308
+ latent_width = (CONFIG["width"] // 16) * len(CONFIG["obs_cam_keys"])
309
+
310
+ latents = torch.randn(
311
+ 1, 48, frame_chunk_size, latent_height, latent_width, device=DEVICE, dtype=DTYPE
312
+ )
313
+ actions = torch.randn(
314
+ 1,
315
+ CONFIG["action_dim"],
316
+ frame_chunk_size,
317
+ CONFIG["action_per_frame"],
318
+ 1,
319
+ device=DEVICE,
320
+ dtype=DTYPE,
321
+ )
322
+
323
+ video_inference_step = CONFIG["num_inference_steps"]
324
+ action_inference_step = CONFIG["action_num_inference_steps"]
325
+ video_step = CONFIG["video_exec_step"]
326
+
327
+ scheduler.set_timesteps(video_inference_step)
328
+ action_scheduler.set_timesteps(action_inference_step)
329
+ timesteps = scheduler.timesteps
330
+ action_timesteps = action_scheduler.timesteps
331
+
332
+ timesteps = F.pad(timesteps, (0, 1), mode="constant", value=0)
333
+ if video_step != -1:
334
+ timesteps = timesteps[:video_step]
335
+ action_timesteps = F.pad(action_timesteps, (0, 1), mode="constant", value=0)
336
+
337
+ with torch.no_grad():
338
+ # 1. Video generation loop
339
+ for i, t in enumerate(timesteps):
340
+ last_step = i == len(timesteps) - 1
341
+ latent_cond = init_latent[:, :, 0:1].to(DTYPE) if frame_st_id == 0 else None
342
+ input_dict = _prepare_latent_input(
343
+ latents,
344
+ None,
345
+ t,
346
+ t,
347
+ latent_cond,
348
+ None,
349
+ frame_st_id=frame_st_id,
350
+ patch_size=CONFIG["patch_size"],
351
+ prompt_embeds=prompt_embeds,
352
+ use_cfg=use_cfg,
353
+ negative_prompt_embeds=negative_prompt_embeds,
354
+ )
355
+
356
+ video_noise_pred = transformer(
357
+ _repeat_input_for_cfg(
358
+ input_dict["latent_res_lst"],
359
+ use_cfg,
360
+ prompt_embeds,
361
+ negative_prompt_embeds,
362
+ ),
363
+ update_cache=1 if last_step else 0,
364
+ cache_name="pos",
365
+ action_mode=False,
366
+ )
367
+
368
+ if not last_step or video_step != -1:
369
+ video_noise_pred = data_seq_to_patch(
370
+ CONFIG["patch_size"],
371
+ video_noise_pred,
372
+ frame_chunk_size,
373
+ latent_height,
374
+ latent_width,
375
+ batch_size=2 if use_cfg else 1,
376
+ )
377
+ if guidance_scale > 1:
378
+ video_noise_pred = video_noise_pred[1:] + guidance_scale * (
379
+ video_noise_pred[:1] - video_noise_pred[1:]
380
+ )
381
+ else:
382
+ video_noise_pred = video_noise_pred[:1]
383
+ latents = scheduler.step(video_noise_pred, t, latents, return_dict=False)
384
+
385
+ latents[:, :, 0:1] = (
386
+ latent_cond if frame_st_id == 0 else latents[:, :, 0:1]
387
+ )
388
+
389
+ # 2. Action generation loop
390
+ for i, t in enumerate(action_timesteps):
391
+ last_step = i == len(action_timesteps) - 1
392
+ action_cond = (
393
+ torch.zeros(
394
+ [1, CONFIG["action_dim"], 1, CONFIG["action_per_frame"], 1],
395
+ device=DEVICE,
396
+ dtype=DTYPE,
397
+ )
398
+ if frame_st_id == 0
399
+ else None
400
+ )
401
+ input_dict = _prepare_latent_input(
402
+ None,
403
+ actions,
404
+ t,
405
+ t,
406
+ None,
407
+ action_cond,
408
+ frame_st_id=frame_st_id,
409
+ patch_size=CONFIG["patch_size"],
410
+ prompt_embeds=prompt_embeds,
411
+ use_cfg=use_cfg,
412
+ negative_prompt_embeds=negative_prompt_embeds,
413
+ )
414
+ action_noise_pred = transformer(
415
+ _repeat_input_for_cfg(
416
+ input_dict["action_res_lst"],
417
+ use_cfg,
418
+ prompt_embeds,
419
+ negative_prompt_embeds,
420
+ ),
421
+ update_cache=1 if last_step else 0,
422
+ cache_name="pos",
423
+ action_mode=True,
424
+ )
425
+
426
+ if not last_step:
427
+ action_noise_pred = rearrange(
428
+ action_noise_pred, "b (f n) c -> b c f n 1", f=frame_chunk_size
429
+ )
430
+ if action_guidance_scale > 1:
431
+ action_noise_pred = action_noise_pred[1:] + action_guidance_scale * (
432
+ action_noise_pred[:1] - action_noise_pred[1:]
433
+ )
434
+ else:
435
+ action_noise_pred = action_noise_pred[:1]
436
+ actions = action_scheduler.step(
437
+ action_noise_pred, t, actions, return_dict=False
438
+ )
439
+
440
+ actions[:, :, 0:1] = (
441
+ action_cond if frame_st_id == 0 else actions[:, :, 0:1]
442
+ )
443
+
444
+ actions[:, ~action_mask] *= 0
445
+ return actions, latents
446
+
447
+
448
+ def decode_video(pred_latent):
449
+ """Decode latent tensor to video frames."""
450
+ latents = pred_latent.to(vae.dtype)
451
+ latents_mean = (
452
+ torch.tensor(vae.config.latents_mean)
453
+ .view(1, vae.config.z_dim, 1, 1, 1)
454
+ .to(latents.device, latents.dtype)
455
+ )
456
+ latents_std = 1.0 / torch.tensor(vae.config.latents_std).view(
457
+ 1, vae.config.z_dim, 1, 1, 1
458
+ ).to(latents.device, latents.dtype)
459
+ latents = latents / latents_std + latents_mean
460
+ video = vae.decode(latents, return_dict=False)[0]
461
+ video = video_processor.postprocess_video(video, output_type="np")[0]
462
+ return video
463
+
464
+
465
+ # ------------------------------------------------------------------ #
466
+ # Gradio inference function
467
+ # ------------------------------------------------------------------ #
468
+
469
+
470
+ @spaces.GPU(duration=120)
471
+ def generate(
472
+ top_img: "np.ndarray",
473
+ wrist_img: "np.ndarray",
474
+ prompt: str,
475
+ num_chunks: int = 5,
476
+ seed: int = 0,
477
+ progress=gr.Progress(track_tqdm=True),
478
+ ):
479
+ """Generate a robot manipulation video from initial observations and a text prompt.
480
+
481
+ Args:
482
+ top_img: Top-down camera observation image.
483
+ wrist_img: Wrist camera observation image.
484
+ prompt: Natural language instruction for the robot task.
485
+ num_chunks: Number of video chunks to generate autoregressively (each chunk = 4 frames).
486
+ seed: Random seed for reproducibility.
487
+ """
488
+ torch.manual_seed(seed)
489
+ torch.cuda.manual_seed(seed)
490
+
491
+ use_cfg = CONFIG["guidance_scale"] > 1 or CONFIG["action_guidance_scale"] > 1
492
+
493
+ # Prepare observations
494
+ obs = [
495
+ {
496
+ CONFIG["obs_cam_keys"][0]: top_img,
497
+ CONFIG["obs_cam_keys"][1]: wrist_img,
498
+ }
499
+ ]
500
+
501
+ # Reset KV cache
502
+ transformer.clear_cache("pos")
503
+ streaming_vae.clear_cache()
504
+
505
+ # Encode initial observation
506
+ init_latent = encode_obs(obs)
507
+
508
+ # Encode prompt
509
+ prompt_embeds, negative_prompt_embeds = encode_prompt(prompt)
510
+
511
+ # Latent dimensions
512
+ latent_height = CONFIG["height"] // 16
513
+ latent_width = (CONFIG["width"] // 16) * len(CONFIG["obs_cam_keys"])
514
+ patch_size = CONFIG["patch_size"]
515
+ latent_token_per_chunk = (
516
+ CONFIG["frame_chunk_size"] * latent_height * latent_width
517
+ ) // (patch_size[0] * patch_size[1] * patch_size[2])
518
+ action_token_per_chunk = CONFIG["frame_chunk_size"] * CONFIG["action_per_frame"]
519
+
520
+ # Create KV cache
521
+ transformer.create_empty_cache(
522
+ "pos",
523
+ CONFIG["attn_window"],
524
+ latent_token_per_chunk,
525
+ action_token_per_chunk,
526
+ device=DEVICE,
527
+ dtype=DTYPE,
528
+ batch_size=2 if use_cfg else 1,
529
+ )
530
+
531
+ # Autoregressive chunk generation
532
+ pred_latent_lst = []
533
+ for chunk_id in range(num_chunks):
534
+ frame_st_id = chunk_id * CONFIG["frame_chunk_size"]
535
+ actions, latents = infer_chunk(
536
+ init_latent,
537
+ frame_st_id,
538
+ prompt_embeds,
539
+ negative_prompt_embeds,
540
+ use_cfg,
541
+ CONFIG["guidance_scale"],
542
+ CONFIG["action_guidance_scale"],
543
+ num_chunks,
544
+ )
545
+ pred_latent_lst.append(latents)
546
+
547
+ pred_latent = torch.cat(pred_latent_lst, dim=2)
548
+
549
+ # Clean up caches
550
+ transformer.clear_cache("pos")
551
+ streaming_vae.clear_cache()
552
+ torch.cuda.empty_cache()
553
+
554
+ # Decode video
555
+ video = decode_video(pred_latent)
556
+
557
+ # Save to temp file
558
+ from diffusers.utils import export_to_video
559
+
560
+ tmp_file = tempfile.NamedTemporaryFile(suffix=".mp4", delete=False)
561
+ tmp_file.close()
562
+ export_to_video(video, tmp_file.name, fps=10)
563
+
564
+ return tmp_file.name
565
+
566
+
567
+ # ------------------------------------------------------------------ #
568
+ # Gradio UI
569
+ # ------------------------------------------------------------------ #
570
+
571
+ CSS = """
572
+ #col-container { max-width: 1100px; margin: 0 auto; }
573
+ .dark .gradio-container { color: var(--body-text-color); }
574
+ """
575
+
576
+ with gr.Blocks(theme=gr.themes.Citrus(), css=CSS) as demo:
577
+ gr.Markdown(
578
+ """
579
+ # Next Forcing: Causal World Modeling with Multi-Chunk Prediction
580
+
581
+ Generate robot manipulation video from initial observations and a text instruction.
582
+ Upload top-down and wrist camera images, describe the task, and the model autoregressively
583
+ predicts future video frames.
584
+
585
+ [Paper](https://arxiv.org/abs/2606.11187) | [Code](https://github.com/gangweix/next-forcing) | [Model](https://huggingface.co/gangweix/next-forcing-base)
586
+ """
587
+ )
588
+
589
+ with gr.Row():
590
+ with gr.Column():
591
+ top_img = gr.Image(
592
+ label="Top Camera",
593
+ type="numpy",
594
+ height=256,
595
+ )
596
+ wrist_img = gr.Image(
597
+ label="Wrist Camera",
598
+ type="numpy",
599
+ height=256,
600
+ )
601
+ prompt = gr.Textbox(
602
+ label="Task Instruction",
603
+ placeholder="e.g. Pick the green cube and place it inside the blue box",
604
+ lines=2,
605
+ )
606
+ with gr.Accordion("Advanced Settings", open=False):
607
+ num_chunks = gr.Slider(
608
+ label="Number of chunks (4 frames each)",
609
+ minimum=1,
610
+ maximum=10,
611
+ value=5,
612
+ step=1,
613
+ )
614
+ seed = gr.Number(label="Seed", value=0, precision=0)
615
+ run_btn = gr.Button("Generate Video", variant="primary")
616
+
617
+ with gr.Column():
618
+ video_out = gr.Video(label="Generated Video")
619
+
620
+ gr.Examples(
621
+ examples=[
622
+ [
623
+ "examples/observation.images.top.png",
624
+ "examples/observation.images.wrist.png",
625
+ "Pick the green cube and place it inside the blue box",
626
+ 5,
627
+ 0,
628
+ ],
629
+ [
630
+ "examples/observation.images.top.png",
631
+ "examples/observation.images.wrist.png",
632
+ "Move the red block to the left side of the table",
633
+ 5,
634
+ 42,
635
+ ],
636
+ ],
637
+ inputs=[top_img, wrist_img, prompt, num_chunks, seed],
638
+ outputs=video_out,
639
+ fn=generate,
640
+ cache_examples=True,
641
+ cache_mode="lazy",
642
+ )
643
+
644
+ run_btn.click(
645
+ fn=generate,
646
+ inputs=[top_img, wrist_img, prompt, num_chunks, seed],
647
+ outputs=video_out,
648
+ api_name="generate",
649
+ )
650
+
651
+ demo.launch(mcp_server=True)
examples/observation.images.top.png ADDED

Git LFS Details

  • SHA256: e6618f91e81d07a8bd274442686e3ba210b4e5ea6bd620435d566c7f0480ff20
  • Pointer size: 131 Bytes
  • Size of remote file: 119 kB
examples/observation.images.wrist.png ADDED

Git LFS Details

  • SHA256: 6fa653cb77803f662464a6ef07a4c0a44ef745a74855c8ae4255d7f371714e63
  • Pointer size: 131 Bytes
  • Size of remote file: 132 kB
requirements.txt ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ diffusers>=0.36.0
2
+ transformers>=4.55.0
3
+ accelerate
4
+ einops
5
+ easydict
6
+ numpy
7
+ pillow
8
+ tqdm
9
+ imageio[ffmpeg]
10
+ ftfy
11
+ safetensors
12
+ torchvision
13
+ websockets
14
+ msgpack
15
+ 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
wan_va/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
2
+ from . import configs, distributed, modules
wan_va/build_dataset_index.py ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Build reusable latent sample indexes without loading the training model."""
2
+
3
+ import argparse
4
+
5
+ from .configs import VA_CONFIGS
6
+ from .dataset import MultiLatentLeRobotDataset
7
+
8
+
9
+ def parse_args():
10
+ parser = argparse.ArgumentParser(description=__doc__)
11
+ parser.add_argument('--config-name', default='robotwin_train')
12
+ parser.add_argument(
13
+ '--rebuild',
14
+ action='store_true',
15
+ help='Validate all latent paths again and replace the matching caches.',
16
+ )
17
+ parser.add_argument('--init-worker', type=int)
18
+ return parser.parse_args()
19
+
20
+
21
+ def main():
22
+ args = parse_args()
23
+ config = VA_CONFIGS[args.config_name]
24
+ config.rank = 0
25
+ config.world_size = 1
26
+ config.rebuild_dataset_index_cache = args.rebuild
27
+ if args.init_worker is not None:
28
+ config.init_worker = args.init_worker
29
+
30
+ dataset = MultiLatentLeRobotDataset(config=config)
31
+ print(
32
+ f'Dataset index ready: {len(dataset)} samples from '
33
+ f'{len(dataset._datasets)} datasets '
34
+ f'({dataset.index_cache_hits} cache hits, '
35
+ f'{dataset.hf_cache_hits} direct Arrow loads, '
36
+ f'{dataset.index_cache_misses} rebuilt)'
37
+ )
38
+
39
+
40
+ if __name__ == '__main__':
41
+ main()
wan_va/configs/__init__.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from .va_franka_cfg import va_franka_cfg
3
+ from .va_robotwin_cfg import va_robotwin_cfg
4
+ from .va_franka_i2va import va_franka_i2va_cfg
5
+ from .va_robotwin_i2va import va_robotwin_i2va_cfg
6
+ from .va_robotwin_train_cfg import va_robotwin_train_cfg
7
+ from .va_demo_train_cfg import va_demo_train_cfg
8
+ from .va_demo_cfg import va_demo_cfg
9
+ from .va_demo_i2va import va_demo_i2va_cfg
10
+
11
+ VA_CONFIGS = {
12
+ 'robotwin': va_robotwin_cfg,
13
+ 'franka': va_franka_cfg,
14
+ 'robotwin_i2va': va_robotwin_i2va_cfg,
15
+ 'franka_i2va': va_franka_i2va_cfg,
16
+ 'robotwin_train': va_robotwin_train_cfg,
17
+ 'demo': va_demo_cfg,
18
+ 'demo_train': va_demo_train_cfg,
19
+ 'demo_i2va': va_demo_i2va_cfg,
20
+ }
wan_va/configs/mcp_train_config.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from easydict import EasyDict
3
+
4
+
5
+ mcp_train_cfg = EasyDict()
6
+ mcp_train_cfg.enable_mcp = True
7
+ mcp_train_cfg.num_mcp_depths = 3
8
+ mcp_train_cfg.mcp_blocks_per_depth = 3
9
+ # Zero-based indices for Transformer layers 4, 12, 20, and 30 in the paper.
10
+ mcp_train_cfg.mcp_hidden_collect_layers = [3, 11, 19, 29]
11
+ mcp_train_cfg.mcp_snr_shift = 10.0
12
+ mcp_train_cfg.mcp_loss_weights = [0.5, 0.2, 0.1]
13
+ mcp_train_cfg.mcp_init_from_backbone = True
wan_va/configs/shared_config.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import torch
3
+ from easydict import EasyDict
4
+
5
+ va_shared_cfg = EasyDict()
6
+
7
+ va_shared_cfg.host = '0.0.0.0'
8
+ va_shared_cfg.port = 29536
9
+ va_shared_cfg.infer_mode = 'server'
10
+
11
+ va_shared_cfg.param_dtype = torch.bfloat16
12
+ va_shared_cfg.save_root = './train_out'
13
+
14
+ va_shared_cfg.patch_size = (1, 2, 2)
15
+
16
+ va_shared_cfg.enable_offload = False
17
+
18
+ # Cache the validated latent sample index so repeated training runs do not scan
19
+ # every latent path on network storage again.
20
+ va_shared_cfg.enable_dataset_index_cache = True
21
+ va_shared_cfg.rebuild_dataset_index_cache = False
wan_va/configs/va_demo_cfg.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import torch
3
+ from easydict import EasyDict
4
+
5
+ from .shared_config import va_shared_cfg
6
+
7
+ va_demo_cfg = EasyDict(__name__='Config: VA demo')
8
+ va_demo_cfg.update(va_shared_cfg)
9
+
10
+ va_demo_cfg.wan22_pretrained_model_name_or_path = "/path/to/pretrained/model"
11
+
12
+ va_demo_cfg.attn_window = 30
13
+ va_demo_cfg.frame_chunk_size = 4
14
+ va_demo_cfg.env_type = 'none'
15
+
16
+ va_demo_cfg.height = 256
17
+ va_demo_cfg.width = 256
18
+ va_demo_cfg.action_dim = 30
19
+ va_demo_cfg.action_per_frame = 8
20
+ va_demo_cfg.obs_cam_keys = [
21
+ 'observation.images.top', 'observation.images.wrist'
22
+ ]
23
+ va_demo_cfg.guidance_scale = 5
24
+ va_demo_cfg.action_guidance_scale = 1
25
+
26
+ va_demo_cfg.num_inference_steps = 5
27
+ va_demo_cfg.video_exec_step = -1
28
+ va_demo_cfg.action_num_inference_steps = 10
29
+
30
+ va_demo_cfg.snr_shift = 5.0
31
+ va_demo_cfg.action_snr_shift = 1.0
32
+
33
+ va_demo_cfg.used_action_channel_ids = list(range(0, 5)) + list(range(28, 29))
34
+ inverse_used_action_channel_ids = [len(va_demo_cfg.used_action_channel_ids)
35
+ ] * va_demo_cfg.action_dim
36
+ for i, j in enumerate(va_demo_cfg.used_action_channel_ids):
37
+ inverse_used_action_channel_ids[j] = i
38
+ va_demo_cfg.inverse_used_action_channel_ids = inverse_used_action_channel_ids
39
+
40
+ va_demo_cfg.action_norm_method = 'quantiles'
41
+ va_demo_cfg.norm_stat = {
42
+ "q01": [
43
+ -90.60303497314453,
44
+ -98.73043060302734,
45
+ -79.9008560180664,
46
+ 48.95470428466797,
47
+ -32.794578552246094,
48
+ ] + [0.] * 23 + [0.8250824809074402, 0],
49
+ "q99": [
50
+ 71.735107421875,
51
+ 65.89081573486328,
52
+ 92.87967681884766,
53
+ 100.0,
54
+ 22.784151077270508,
55
+ ] + [0.] * 23 + [100.0, 0],
56
+ }
wan_va/configs/va_demo_i2va.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from easydict import EasyDict
3
+ from .va_demo_cfg import va_demo_cfg
4
+
5
+ va_demo_i2va_cfg = EasyDict(__name__='Config: VA demo i2va')
6
+ va_demo_i2va_cfg.update(va_demo_cfg)
7
+
8
+ va_demo_i2va_cfg.input_img_path = 'example/demo'
9
+ va_demo_i2va_cfg.num_chunks_to_infer = 10
10
+ va_demo_i2va_cfg.prompt = 'Pick the green cube and place it inside the blue box'
11
+ va_demo_i2va_cfg.infer_mode = 'i2va'
wan_va/configs/va_demo_train_cfg.py ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from easydict import EasyDict
3
+ from .mcp_train_config import mcp_train_cfg
4
+ from .va_demo_cfg import va_demo_cfg
5
+ import os
6
+
7
+ va_demo_train_cfg = EasyDict(__name__='Config: VA demo train')
8
+ va_demo_train_cfg.update(va_demo_cfg)
9
+ va_demo_train_cfg.update(mcp_train_cfg)
10
+
11
+ va_demo_train_cfg.dataset_path = '/path/to/your/dataset'
12
+ va_demo_train_cfg.empty_emb_path = os.path.join(va_demo_train_cfg.dataset_path, 'empty_emb.pt')
13
+ va_demo_train_cfg.enable_wandb = True
14
+ va_demo_train_cfg.load_worker = 16
15
+ va_demo_train_cfg.save_interval = 50
16
+ va_demo_train_cfg.gc_interval = 50
17
+ va_demo_train_cfg.cfg_prob = 0.1
18
+
19
+ # Training parameters
20
+ va_demo_train_cfg.learning_rate = 1e-4
21
+ va_demo_train_cfg.beta1 = 0.9
22
+ va_demo_train_cfg.beta2 = 0.95
23
+ va_demo_train_cfg.weight_decay = 1e-1
24
+ va_demo_train_cfg.warmup_steps = 10
25
+ va_demo_train_cfg.batch_size = 1
26
+ va_demo_train_cfg.gradient_accumulation_steps = 8
27
+ va_demo_train_cfg.num_steps = 2000
wan_va/configs/va_franka_cfg.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import torch
3
+ from easydict import EasyDict
4
+
5
+ from .shared_config import va_shared_cfg
6
+
7
+ va_franka_cfg = EasyDict(__name__='Config: VA franka')
8
+ va_franka_cfg.update(va_shared_cfg)
9
+
10
+ va_franka_cfg.wan22_pretrained_model_name_or_path = "/path/to/pretrained/model"
11
+
12
+ va_franka_cfg.attn_window = 30
13
+ va_franka_cfg.frame_chunk_size = 4
14
+ va_franka_cfg.env_type = 'none'
15
+
16
+ va_franka_cfg.height = 224
17
+ va_franka_cfg.width = 320
18
+ va_franka_cfg.action_dim = 30
19
+ va_franka_cfg.action_per_frame = 20
20
+ va_franka_cfg.obs_cam_keys = [
21
+ 'observation.images.cam_high', 'observation.images.cam_left_wrist',
22
+ 'observation.images.cam_right_wrist'
23
+ ]
24
+ va_franka_cfg.guidance_scale = 5
25
+ va_franka_cfg.action_guidance_scale = 1
26
+
27
+ va_franka_cfg.num_inference_steps = 5
28
+ va_franka_cfg.video_exec_step = -1
29
+ va_franka_cfg.action_num_inference_steps = 10
30
+
31
+ va_franka_cfg.snr_shift = 5.0
32
+ va_franka_cfg.action_snr_shift = 1.0
33
+
34
+ va_franka_cfg.used_action_channel_ids = list(range(0, 7)) + list(range(
35
+ 28, 29)) + list(range(7, 14)) + list(range(29, 30))
36
+ inverse_used_action_channel_ids = [len(va_franka_cfg.used_action_channel_ids)
37
+ ] * va_franka_cfg.action_dim
38
+ for i, j in enumerate(va_franka_cfg.used_action_channel_ids):
39
+ inverse_used_action_channel_ids[j] = i
40
+ va_franka_cfg.inverse_used_action_channel_ids = inverse_used_action_channel_ids
41
+
42
+ va_franka_cfg.action_norm_method = 'quantiles'
43
+ va_franka_cfg.norm_stat = {
44
+ "q01": [
45
+ 0.3051295876502991, -0.22647984325885773, 0.19957000017166138,
46
+ -0.022680532187223434, -0.05553057789802551, -0.2693849802017212,
47
+ -0.29341773986816405, 0.2935442328453064, -0.4431332051753998,
48
+ 0.21256473660469055, -0.7962440848350525, -0.40816226601600647,
49
+ -0.28359392285346985, -0.44507765769958496
50
+ ] + [0.] * 16,
51
+ "q99": [
52
+ 0.7572150230407715, 0.47736290097236633, 0.6428080797195435,
53
+ 0.9835678935050964, 0.9927203059196472, 0.28041139245033264,
54
+ 0.47529348731040877, 0.7564866304397571, 0.04082797020673729,
55
+ 0.5355993628501885, 0.9976375699043274, 0.8973174452781656,
56
+ 0.6016915678977965, 0.5027598619461056
57
+ ] + [0.] * 14 + [1.0, 1.0],
58
+ }
wan_va/configs/va_franka_i2va.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from easydict import EasyDict
3
+ from .va_franka_cfg import va_franka_cfg
4
+
5
+ va_franka_i2va_cfg = EasyDict(__name__='Config: VA franka i2va')
6
+ va_franka_i2va_cfg.update(va_franka_cfg)
7
+
8
+ va_franka_i2va_cfg.input_img_path = 'example/franka'
9
+ va_franka_i2va_cfg.num_chunks_to_infer = 10
10
+ va_franka_i2va_cfg.prompt = 'pick bunk'
11
+ va_franka_i2va_cfg.infer_mode = 'i2va'
wan_va/configs/va_robotwin_cfg.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import os
3
+
4
+ from easydict import EasyDict
5
+
6
+ from .shared_config import va_shared_cfg
7
+
8
+ va_robotwin_cfg = EasyDict(__name__='Config: VA robotwin')
9
+ va_robotwin_cfg.update(va_shared_cfg)
10
+
11
+ va_robotwin_cfg.wan22_pretrained_model_name_or_path = os.environ.get(
12
+ "NEXT_FORCING_MODEL_PATH",
13
+ "/path/to/next-forcing-posttrain-robotwin",
14
+ )
15
+
16
+ va_robotwin_cfg.attn_window = 72
17
+ va_robotwin_cfg.frame_chunk_size = 2
18
+ va_robotwin_cfg.env_type = 'robotwin_tshape'
19
+
20
+ va_robotwin_cfg.height = 256
21
+ va_robotwin_cfg.width = 320
22
+ va_robotwin_cfg.action_dim = 30
23
+ va_robotwin_cfg.action_per_frame = 16
24
+ va_robotwin_cfg.obs_cam_keys = [
25
+ 'observation.images.cam_high', 'observation.images.cam_left_wrist',
26
+ 'observation.images.cam_right_wrist'
27
+ ]
28
+ va_robotwin_cfg.guidance_scale = 5
29
+ va_robotwin_cfg.action_guidance_scale = 1
30
+
31
+ va_robotwin_cfg.num_inference_steps = 25
32
+ va_robotwin_cfg.video_exec_step = -1
33
+ va_robotwin_cfg.action_num_inference_steps = 50
34
+
35
+ va_robotwin_cfg.snr_shift = 5.0
36
+ va_robotwin_cfg.action_snr_shift = 1.0
37
+
38
+ va_robotwin_cfg.used_action_channel_ids = list(range(0, 7)) + list(
39
+ range(28, 29)) + list(range(7, 14)) + list(range(29, 30))
40
+ inverse_used_action_channel_ids = [
41
+ len(va_robotwin_cfg.used_action_channel_ids)
42
+ ] * va_robotwin_cfg.action_dim
43
+ for i, j in enumerate(va_robotwin_cfg.used_action_channel_ids):
44
+ inverse_used_action_channel_ids[j] = i
45
+ va_robotwin_cfg.inverse_used_action_channel_ids = inverse_used_action_channel_ids
46
+
47
+ va_robotwin_cfg.action_norm_method = 'quantiles'
48
+ va_robotwin_cfg.norm_stat = {
49
+ "q01": [
50
+ -0.06172713458538055, -3.6716461181640625e-05, -0.08783501386642456,
51
+ -1, -1, -1, -1, -0.3547105032205582, -1.3113021850585938e-06,
52
+ -0.11975435614585876, -1, -1, -1, -1
53
+ ] + [0.] * 16,
54
+ "q99": [
55
+ 0.3462600058317184, 0.39966784834861746, 0.14745532035827624, 1, 1, 1,
56
+ 1, 0.034201726913452024, 0.39142737388610793, 0.1792279863357542, 1, 1,
57
+ 1, 1
58
+ ] + [0.] * 14 + [1.0, 1.0],
59
+ }
wan_va/configs/va_robotwin_i2va.py ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from easydict import EasyDict
3
+ from .va_robotwin_cfg import va_robotwin_cfg
4
+
5
+ va_robotwin_i2va_cfg = EasyDict(__name__='Config: VA robotwin i2va')
6
+ va_robotwin_i2va_cfg.update(va_robotwin_cfg)
7
+
8
+ va_robotwin_i2va_cfg.input_img_path = 'example/robotwin'
9
+ va_robotwin_i2va_cfg.num_chunks_to_infer = 10
10
+ va_robotwin_i2va_cfg.prompt = 'Grab the medium-sized white mug, rotate it, place it on the table, and hook it onto the smooth dark gray rack.'
11
+ va_robotwin_i2va_cfg.infer_mode = 'i2va'
wan_va/configs/va_robotwin_train_cfg.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from easydict import EasyDict
3
+ from .mcp_train_config import mcp_train_cfg
4
+ from .va_robotwin_cfg import va_robotwin_cfg
5
+ import os
6
+
7
+ va_robotwin_train_cfg = EasyDict(__name__='Config: VA robotwin train')
8
+ va_robotwin_train_cfg.update(va_robotwin_cfg)
9
+ va_robotwin_train_cfg.update(mcp_train_cfg)
10
+
11
+ va_robotwin_train_cfg.wan22_pretrained_model_name_or_path = os.environ.get(
12
+ 'NEXT_FORCING_PRETRAINED_MODEL_PATH',
13
+ '/path/to/pretrained/model',
14
+ )
15
+ va_robotwin_train_cfg.dataset_path = os.environ.get(
16
+ 'NEXT_FORCING_DATASET_PATH',
17
+ '/path/to/your/dataset',
18
+ )
19
+ va_robotwin_train_cfg.empty_emb_path = os.path.join(
20
+ va_robotwin_train_cfg.dataset_path, 'empty_emb.pt')
21
+ va_robotwin_train_cfg.save_root = os.environ.get(
22
+ 'NEXT_FORCING_SAVE_ROOT',
23
+ '/path/to/your/output',
24
+ )
25
+ va_robotwin_train_cfg.enable_wandb = False
26
+ va_robotwin_train_cfg.init_worker = 1
27
+ va_robotwin_train_cfg.load_worker = 16
28
+ va_robotwin_train_cfg.save_interval = 1000
29
+ va_robotwin_train_cfg.gc_interval = 50
30
+ va_robotwin_train_cfg.cfg_prob = 0.1
31
+
32
+ # Training parameters
33
+ va_robotwin_train_cfg.learning_rate = 2e-5
34
+ va_robotwin_train_cfg.beta1 = 0.9
35
+ va_robotwin_train_cfg.beta2 = 0.95
36
+ va_robotwin_train_cfg.weight_decay = 0.1
37
+ va_robotwin_train_cfg.warmup_steps = 100
38
+ va_robotwin_train_cfg.batch_size = 1
39
+ va_robotwin_train_cfg.gradient_accumulation_steps = 1
40
+ va_robotwin_train_cfg.num_steps = 50000
wan_va/dataset/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from .lerobot_latent_dataset import (
3
+ MultiLatentLeRobotDataset,
4
+ dataset_indexes_ready,
5
+ )
6
+
7
+ __all__ = [
8
+ 'MultiLatentLeRobotDataset',
9
+ 'dataset_indexes_ready',
10
+ ]
wan_va/dataset/lerobot_latent_dataset.py ADDED
@@ -0,0 +1,548 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from lerobot.datasets.lerobot_dataset import LeRobotDataset, LeRobotDatasetMetadata
3
+ from lerobot.datasets.utils import get_episode_data_index
4
+ from lerobot.datasets.compute_stats import aggregate_stats, compute_episode_stats
5
+ import datasets
6
+ import hashlib
7
+ import json
8
+ import numpy as np
9
+ from pathlib import Path
10
+ from collections.abc import Callable
11
+ import os
12
+ from tqdm import tqdm
13
+ from multiprocessing import Pool
14
+ from functools import partial
15
+ import torch
16
+ from einops import rearrange
17
+ from torch.utils.data import DataLoader
18
+ from scipy.spatial.transform import Rotation as R
19
+ from lerobot.constants import HF_LEROBOT_HOME
20
+
21
+
22
+ DATASET_INDEX_CACHE_VERSION = 1
23
+
24
+
25
+ def dataset_index_fingerprint(dataset_root, latent_root, video_keys):
26
+ episodes_path = Path(dataset_root) / 'meta' / 'episodes.jsonl'
27
+ episodes_stat = episodes_path.stat()
28
+ return {
29
+ 'version': DATASET_INDEX_CACHE_VERSION,
30
+ 'episodes_size': episodes_stat.st_size,
31
+ 'episodes_mtime_ns': episodes_stat.st_mtime_ns,
32
+ 'latent_root': str(Path(latent_root).resolve()),
33
+ 'video_keys': list(video_keys),
34
+ }
35
+
36
+
37
+ def dataset_index_cache_path(dataset_root, fingerprint):
38
+ cache_key = json.dumps(fingerprint, sort_keys=True).encode('utf-8')
39
+ cache_key = hashlib.sha256(cache_key).hexdigest()[:16]
40
+ return (
41
+ Path(dataset_root)
42
+ / '.cache'
43
+ / 'next_forcing'
44
+ / f'valid_metas_{cache_key}.json'
45
+ )
46
+
47
+
48
+ def dataset_indexes_ready(config):
49
+ if not getattr(config, 'enable_dataset_index_cache', True):
50
+ return False
51
+ if getattr(config, 'rebuild_dataset_index_cache', False):
52
+ return False
53
+
54
+ repo_list = recursive_find_file(config.dataset_path, 'info.json')
55
+ repo_list = [Path(path.split('/meta/info.json')[0]) for path in repo_list]
56
+ if not repo_list:
57
+ return False
58
+
59
+ for dataset_root in repo_list:
60
+ fingerprint = dataset_index_fingerprint(
61
+ dataset_root,
62
+ dataset_root / 'latents',
63
+ config.obs_cam_keys,
64
+ )
65
+ cache_path = dataset_index_cache_path(dataset_root, fingerprint)
66
+ try:
67
+ with cache_path.open('r', encoding='utf-8') as handle:
68
+ payload = json.load(handle)
69
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
70
+ return False
71
+
72
+ cache_files = payload.get('hf_cache_files')
73
+ if payload.get('fingerprint') != fingerprint:
74
+ return False
75
+ if not isinstance(payload.get('valid_metas'), list):
76
+ return False
77
+ if not cache_files or not all(Path(path).is_file() for path in cache_files):
78
+ return False
79
+ return True
80
+
81
+ def recursive_find_file(directory, filename='info.json'):
82
+ result = []
83
+ try:
84
+ for root, dirs, files in os.walk(directory):
85
+ if filename in files:
86
+ full_path = os.path.join(root, filename)
87
+ result.append(full_path)
88
+ except PermissionError:
89
+ print(f"Error: can not access {directory}")
90
+ except Exception as e:
91
+ print(f"Error: {e}")
92
+ return result
93
+
94
+ def construct_lerobot(
95
+ repo_id,
96
+ config,
97
+ ):
98
+ return LatentLeRobotDataset(
99
+ repo_id=repo_id,
100
+ config=config,
101
+ )
102
+
103
+ def construct_lerobot_multi_processor(config,
104
+ num_init_worker=8,
105
+ ):
106
+ datasets_out_lst = []
107
+ construct_func = partial(
108
+ construct_lerobot,
109
+ config=config,
110
+ )
111
+ repo_list = recursive_find_file(config.dataset_path, 'info.json')
112
+ repo_list = [v.split('/meta/info.json')[0] for v in repo_list]
113
+ if not repo_list:
114
+ raise FileNotFoundError(
115
+ f"No LeRobot datasets found under {config.dataset_path}")
116
+ num_init_worker = min(max(int(num_init_worker), 1), len(repo_list))
117
+ if num_init_worker == 1:
118
+ dataset_iterator = map(construct_func, repo_list)
119
+ if getattr(config, 'rank', 0) == 0:
120
+ dataset_iterator = tqdm(
121
+ dataset_iterator,
122
+ total=len(repo_list),
123
+ desc='Loading dataset indexes',
124
+ )
125
+ datasets_out_lst = list(dataset_iterator)
126
+ else:
127
+ with Pool(num_init_worker) as pool:
128
+ dataset_iterator = pool.imap(construct_func, repo_list)
129
+ if getattr(config, 'rank', 0) == 0:
130
+ dataset_iterator = tqdm(
131
+ dataset_iterator,
132
+ total=len(repo_list),
133
+ desc='Loading dataset indexes',
134
+ )
135
+ datasets_out_lst = list(dataset_iterator)
136
+
137
+ return datasets_out_lst
138
+
139
+ def get_relative_pose(pose):
140
+ if torch.is_tensor(pose):
141
+ pose = pose.detach().cpu().numpy()
142
+
143
+ rot = R.from_quat(pose[:, 3:7])
144
+ first_rot = R.from_quat(np.tile(pose[:1, 3:7], (pose.shape[0], 1)))
145
+ trans = pose[:, :3]
146
+ relative_trans = trans - trans[0:1]
147
+
148
+ relative_rot = first_rot.inv() * rot
149
+ relative_quat = relative_rot.as_quat()
150
+
151
+ relative_pose = np.concatenate([relative_trans, relative_quat], axis=1)
152
+ return torch.from_numpy(relative_pose)
153
+
154
+ class MultiLatentLeRobotDataset(torch.utils.data.Dataset):
155
+ def __init__(
156
+ self,
157
+ config,
158
+ num_init_worker=None,
159
+ ):
160
+ if num_init_worker is None:
161
+ num_init_worker = getattr(config, 'init_worker', 8)
162
+ self._datasets = construct_lerobot_multi_processor(config,
163
+ num_init_worker,
164
+ )
165
+ self.index_cache_hits = sum(
166
+ dataset.index_cache_hit for dataset in self._datasets)
167
+ self.index_cache_misses = len(self._datasets) - self.index_cache_hits
168
+ self.hf_cache_hits = sum(
169
+ dataset.hf_cache_hit for dataset in self._datasets)
170
+ self.item_id_to_dataset_id, self.acc_dset_num = (
171
+ self._get_item_id_to_dataset_id()
172
+ )
173
+
174
+ def __len__(
175
+ self,
176
+ ):
177
+ return sum(len(v) for v in self._datasets)
178
+
179
+ def _get_item_id_to_dataset_id(self):
180
+ item_id_to_dataset_id = {}
181
+ acc_dset_num = {}
182
+ acc_nums = [0]
183
+ id = 0
184
+ for dset_id, dset in enumerate(self._datasets):
185
+ acc_nums.append(acc_nums[-1] + len(dset))
186
+ for _ in range(len(dset)):
187
+ item_id_to_dataset_id[id] = dset_id
188
+ id += 1
189
+ for did in range(len(self._datasets)):
190
+ acc_dset_num[did] = acc_nums[did]
191
+ return item_id_to_dataset_id, acc_dset_num
192
+
193
+ def __getitem__(self, idx) -> dict:
194
+ assert idx < len(self)
195
+ cur_dset = self._datasets[self.item_id_to_dataset_id[idx]]
196
+ local_idx = idx - self.acc_dset_num[self.item_id_to_dataset_id[idx]]
197
+ return cur_dset[local_idx]
198
+
199
+ class LatentLeRobotDataset(LeRobotDataset):
200
+ def __init__(
201
+ self,
202
+ repo_id,
203
+ config=None,
204
+ ):
205
+ self.repo_id = repo_id
206
+ self.root = HF_LEROBOT_HOME / repo_id
207
+ self.image_transforms = None
208
+ self.delta_timestamps = None
209
+ self.episodes = None
210
+ self.tolerance_s = 1e-4
211
+ self.revision = "v2.1"
212
+ self.video_backend = 'pyav'
213
+ self.delta_indices = None
214
+ self.batch_encoding_size = 1
215
+ self.episodes_since_last_encoding = 0
216
+ self.image_writer = None
217
+ self.episode_buffer = None
218
+ self.root.mkdir(exist_ok=True, parents=True)
219
+ self.meta = LeRobotDatasetMetadata(
220
+ self.repo_id, self.root, self.revision, force_cache_sync=False
221
+ )
222
+ if self.episodes is not None and self.meta._version >= packaging.version.parse("v2.1"):
223
+ episodes_stats = [self.meta.episodes_stats[ep_idx] for ep_idx in self.episodes]
224
+ self.stats = aggregate_stats(episodes_stats)
225
+
226
+ self.episode_data_index = get_episode_data_index(self.meta.episodes, self.episodes)
227
+
228
+ self.latent_path = Path(repo_id) / 'latents'
229
+ self.empty_emb = torch.load(config.empty_emb_path, weights_only=False)
230
+ self.config = config
231
+ self.cfg_prob = config.cfg_prob
232
+ self.used_video_keys = config.obs_cam_keys
233
+ self.q01 = np.array(config.norm_stat['q01'], dtype='float')[None]
234
+ self.q99 = np.array(config.norm_stat['q99'], dtype='float')[None]
235
+ fingerprint = self._dataset_index_fingerprint()
236
+ cache_payload = self._read_dataset_index_cache(fingerprint)
237
+ self.hf_dataset = self._load_cached_hf_dataset(cache_payload)
238
+ self.hf_cache_hit = self.hf_dataset is not None
239
+ if self.hf_dataset is None:
240
+ try:
241
+ assert all((self.root / fpath).is_file() for fpath in self.get_episodes_file_paths())
242
+ self.hf_dataset = self.load_hf_dataset()
243
+ except (AssertionError, FileNotFoundError, NotADirectoryError):
244
+ self.revision = get_safe_version(self.repo_id, self.revision)
245
+ self.download_episodes(download_videos)
246
+ self.hf_dataset = self.load_hf_dataset()
247
+ self._hf_torch_view = self.hf_dataset.with_format(
248
+ type='torch',
249
+ columns=['action'],
250
+ output_all_columns=False
251
+ )
252
+ self.index_cache_hit = self.parse_meta(
253
+ fingerprint=fingerprint,
254
+ cache_payload=cache_payload,
255
+ )
256
+ if self.index_cache_hit and not self.hf_cache_hit:
257
+ self._save_dataset_index_cache(fingerprint, self.new_metas)
258
+
259
+ def _dataset_index_fingerprint(self):
260
+ return dataset_index_fingerprint(
261
+ self.root,
262
+ self.latent_path,
263
+ self.used_video_keys,
264
+ )
265
+
266
+ def _dataset_index_cache_path(self, fingerprint):
267
+ return dataset_index_cache_path(self.root, fingerprint)
268
+
269
+ def _read_dataset_index_cache(self, fingerprint):
270
+ if not getattr(self.config, 'enable_dataset_index_cache', True):
271
+ return None
272
+ if getattr(self.config, 'rebuild_dataset_index_cache', False):
273
+ return None
274
+
275
+ cache_path = self._dataset_index_cache_path(fingerprint)
276
+ try:
277
+ with cache_path.open('r', encoding='utf-8') as handle:
278
+ payload = json.load(handle)
279
+ except (FileNotFoundError, json.JSONDecodeError, OSError):
280
+ return None
281
+
282
+ if payload.get('fingerprint') != fingerprint:
283
+ return None
284
+ return payload
285
+
286
+ def _load_dataset_index_cache(self, fingerprint, cache_payload=None):
287
+ payload = cache_payload
288
+ if payload is None:
289
+ payload = self._read_dataset_index_cache(fingerprint)
290
+ if payload is None:
291
+ return None
292
+ valid_metas = payload.get('valid_metas')
293
+ if not isinstance(valid_metas, list):
294
+ return None
295
+ return valid_metas
296
+
297
+ def _load_cached_hf_dataset(self, cache_payload):
298
+ if cache_payload is None:
299
+ return None
300
+ cache_files = cache_payload.get('hf_cache_files')
301
+ if not cache_files or not all(Path(path).is_file() for path in cache_files):
302
+ return None
303
+
304
+ cached_datasets = [
305
+ datasets.Dataset.from_file(path) for path in cache_files
306
+ ]
307
+ if len(cached_datasets) == 1:
308
+ return cached_datasets[0]
309
+ return datasets.concatenate_datasets(cached_datasets)
310
+
311
+ def _hf_cache_files(self):
312
+ hf_dataset = getattr(self, 'hf_dataset', None)
313
+ if hf_dataset is None:
314
+ return []
315
+ return [
316
+ cache_file['filename']
317
+ for cache_file in hf_dataset.cache_files
318
+ if Path(cache_file['filename']).is_file()
319
+ ]
320
+
321
+ def _save_dataset_index_cache(self, fingerprint, valid_metas):
322
+ if not getattr(self.config, 'enable_dataset_index_cache', True):
323
+ return
324
+
325
+ cache_path = self._dataset_index_cache_path(fingerprint)
326
+ temporary_path = cache_path.with_name(
327
+ f'.{cache_path.name}.tmp-{os.getpid()}'
328
+ )
329
+ try:
330
+ cache_path.parent.mkdir(parents=True, exist_ok=True)
331
+ with temporary_path.open('w', encoding='utf-8') as handle:
332
+ json.dump(
333
+ {
334
+ 'fingerprint': fingerprint,
335
+ 'valid_metas': valid_metas,
336
+ 'hf_cache_files': self._hf_cache_files(),
337
+ },
338
+ handle,
339
+ separators=(',', ':'),
340
+ )
341
+ os.replace(temporary_path, cache_path)
342
+ except OSError as exc:
343
+ try:
344
+ temporary_path.unlink()
345
+ except FileNotFoundError:
346
+ pass
347
+ raise RuntimeError(
348
+ f'Failed to write dataset index cache: {cache_path}'
349
+ ) from exc
350
+
351
+ def parse_meta(self, fingerprint=None, cache_payload=None):
352
+ if fingerprint is None:
353
+ fingerprint = self._dataset_index_fingerprint()
354
+ cached_metas = self._load_dataset_index_cache(
355
+ fingerprint,
356
+ cache_payload=cache_payload,
357
+ )
358
+ if cached_metas is not None:
359
+ self.new_metas = cached_metas
360
+ return True
361
+
362
+ out = []
363
+ for key, value in self.meta.episodes.items():
364
+ episode_index = value["episode_index"]
365
+ tasks = value["tasks"]
366
+ action_config = value["action_config"]
367
+ for acfg in action_config:
368
+ cur_meta = {
369
+ "episode_index": episode_index,
370
+ "tasks": tasks,
371
+ }
372
+ cur_meta.update(acfg)
373
+
374
+ check_statu = self._check_meta(
375
+ cur_meta["start_frame"],
376
+ cur_meta["end_frame"],
377
+ cur_meta["episode_index"],
378
+ )
379
+
380
+ if check_statu:
381
+ out.append(cur_meta)
382
+ self.new_metas = out
383
+ self._save_dataset_index_cache(fingerprint, out)
384
+ return False
385
+
386
+ def _check_meta(self, start_frame, end_frame, episode_index):
387
+ episode_chunk = self.meta.get_episode_chunk(episode_index)
388
+ latent_path = Path(self.latent_path) / f"chunk-{episode_chunk:03d}"
389
+ for key in self.used_video_keys:
390
+ cur_path = latent_path / key
391
+ latent_file = (
392
+ cur_path / f"episode_{episode_index:06d}_{start_frame}_{end_frame}.pth"
393
+ )
394
+ if not os.path.exists(latent_file):
395
+ return False
396
+ return True
397
+
398
+ def _get_global_idx(self, episode_index: int, local_index: int):
399
+ ep_start = self.episode_data_index["from"][episode_index]
400
+ return local_index + ep_start
401
+
402
+ def _get_range_hf_data(self, start_frame, end_frame):
403
+ batch = self._hf_torch_view[start_frame:end_frame]
404
+ return batch
405
+
406
+ def _flatten_latent_dict(self, latent_dict):
407
+ out = {}
408
+ for key, value in latent_dict.items():
409
+ for inner_key, inner_value in value.items():
410
+ new_key = f"{key}.{inner_key}"
411
+ out[new_key] = inner_value
412
+ return out
413
+
414
+ def _get_range_latent_data(self, start_frame, end_frame, episode_index):
415
+ episode_chunk = self.meta.get_episode_chunk(episode_index)
416
+ latent_path = Path(self.latent_path) / f"chunk-{episode_chunk:03d}"
417
+ out = {}
418
+ for key in self.used_video_keys:
419
+ cur_path = latent_path / key
420
+ latent_file = (
421
+ cur_path / f"episode_{episode_index:06d}_{start_frame}_{end_frame}.pth"
422
+ )
423
+ assert os.path.exists(latent_file)
424
+ latent_data = torch.load(latent_file, weights_only=False)
425
+ out[key] = latent_data
426
+
427
+ return self._flatten_latent_dict(out)
428
+
429
+
430
+ def _cat_video_latents(self,
431
+ data_dict
432
+ ):
433
+ latent_lst = []
434
+ for key in self.used_video_keys:
435
+ latent= data_dict[f"{key}.latent"]
436
+ latent_num_frames = data_dict[f"{key}.latent_num_frames"]
437
+ latent_height = data_dict[f"{key}.latent_height"]
438
+ latent_width = data_dict[f"{key}.latent_width"]
439
+ latent = rearrange(latent,
440
+ '(f h w) c -> f h w c',
441
+ f=latent_num_frames,
442
+ h=latent_height,
443
+ w=latent_width)
444
+ latent_lst.append(latent)
445
+ if self.config.env_type == 'robotwin_tshape':
446
+ wrist_latent = torch.cat(latent_lst[1:], dim=2)
447
+ cat_latent = torch.cat([wrist_latent, latent_lst[0]], dim=1)
448
+ else:
449
+ cat_latent = torch.cat(latent_lst, dim=2)
450
+
451
+ text_emb = data_dict[f"{self.used_video_keys[0]}.text_emb"]
452
+ if torch.rand(1).item() < self.cfg_prob:
453
+ text_emb = self.empty_emb
454
+
455
+ out_dict = dict(
456
+ latents = cat_latent,
457
+ text_emb = text_emb,
458
+ )
459
+ return out_dict
460
+
461
+ def _action_post_process(self, local_start_frame, local_end_frame, latent_frame_ids, action):
462
+ act_shift = int(latent_frame_ids[0] - local_start_frame)
463
+ frame_stride = latent_frame_ids[1] - latent_frame_ids[0]
464
+ action = action[act_shift:]
465
+ if self.config.env_type == 'robotwin_tshape': ## TODO support get_relative_pose for other dataset, currently only support robotwin
466
+ left_action = get_relative_pose(action[:, :7])
467
+ right_action = get_relative_pose(action[:, 8:15])
468
+ action = np.concatenate([left_action, action[:, 7:8], right_action, action[:, 15:16]], axis=1)
469
+ action = np.pad(action, pad_width=((frame_stride * 4, 0), (0, 0)), mode='constant', constant_values=0)
470
+
471
+ latent_frame_num = (len(latent_frame_ids) - 1) // 4 + 1
472
+ required_action_num = latent_frame_num * frame_stride * 4
473
+
474
+ action = action[:required_action_num]
475
+ action_mask = np.ones_like(action, dtype='bool')
476
+ assert action.shape[0] == required_action_num
477
+
478
+
479
+ action_paded = np.pad(action, ((0, 0), (0, 1)), mode='constant', constant_values=0)
480
+ action_mask_padded = np.pad(action_mask, ((0, 0), (0, 1)), mode='constant', constant_values=0)
481
+
482
+ action_aligned = action_paded[:, self.config.inverse_used_action_channel_ids]
483
+ action_mask_aligned = action_mask_padded[:, self.config.inverse_used_action_channel_ids]
484
+ action_aligned = (action_aligned - self.q01) / (
485
+ self.q99 - self.q01 + 1e-6) * 2. - 1.
486
+ action_aligned = np.clip(action_aligned, -1.5, 1.5)
487
+ action_aligned = rearrange(action_aligned, "(f n) c -> c f n 1", f=latent_frame_num)
488
+ action_mask_aligned = rearrange(action_mask_aligned, "(f n) c -> c f n 1", f=latent_frame_num)
489
+ action_aligned *= action_mask_aligned
490
+ return torch.from_numpy(action_aligned).float(), torch.from_numpy(action_mask_aligned).bool()
491
+
492
+ def __getitem__(self, idx) -> dict:
493
+ idx = idx % len(self.new_metas)
494
+ cur_meta = self.new_metas[idx]
495
+ episode_index = cur_meta["episode_index"]
496
+ start_frame = cur_meta["start_frame"]
497
+ end_frame = cur_meta["end_frame"]
498
+ local_start_frame = start_frame
499
+ local_end_frame = end_frame
500
+
501
+ ori_data_dict = self._get_range_latent_data(start_frame, end_frame, episode_index)
502
+
503
+ latent_frame_ids = ori_data_dict[f"{self.used_video_keys[0]}.frame_ids"]
504
+ start_frame = self._get_global_idx(episode_index, start_frame)
505
+ end_frame = self._get_global_idx(episode_index, end_frame)
506
+
507
+ hf_data_frames = self._get_range_hf_data(start_frame, end_frame)
508
+ ori_data_dict.update(hf_data_frames)
509
+ out_dict = self._cat_video_latents(ori_data_dict)
510
+
511
+ out_dict['actions'], out_dict['actions_mask'] = self._action_post_process(local_start_frame, local_end_frame, latent_frame_ids, ori_data_dict['action'])
512
+
513
+ out_dict['latents'] = out_dict['latents'].permute(3, 0, 1, 2)
514
+ return out_dict
515
+
516
+ def __len__(self):
517
+ return len(self.new_metas)
518
+
519
+ if __name__ == '__main__':
520
+ from wan_va.configs import VA_CONFIGS
521
+ from tqdm import tqdm
522
+ dset = MultiLatentLeRobotDataset(
523
+ VA_CONFIGS['demo_train']
524
+ )
525
+ for key, value in dset[0].items():
526
+ if isinstance(value, torch.Tensor):
527
+ print(f'{key}: {value.shape} tensor')
528
+ elif isinstance(value, np.ndarray):
529
+ print(f'{key}: {value.shape} np')
530
+ else:
531
+ print(f'{key}: {value}')
532
+ print(len(dset))
533
+ dloader = DataLoader(
534
+ dset,
535
+ batch_size=1,
536
+ shuffle=True,
537
+ num_workers=32,
538
+ )
539
+ max_l = 0
540
+ action_list = []
541
+ for data in tqdm(dloader):
542
+ _, _, F, H, W = data['latents'].shape
543
+ max_l = max(max_l, F*H*W)
544
+ action_list.append(data['actions'].flatten(2).permute(0, 2, 1).flatten(0, 1))
545
+ action_all = torch.cat(action_list, dim=0)
546
+ print(max_l)
547
+ print(action_all.shape, action_all.mean(dim=0), action_all.min(dim=0)[0], action_all.max(dim=0)[0])
548
+
wan_va/distributed/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
wan_va/distributed/fsdp.py ADDED
@@ -0,0 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
2
+ import gc
3
+
4
+ import torch
5
+ from torch.distributed.fsdp import fully_shard, MixedPrecisionPolicy
6
+
7
+ from torch.distributed.algorithms._checkpoint.checkpoint_wrapper import (
8
+ checkpoint_wrapper as ptd_checkpoint_wrapper,
9
+ )
10
+
11
+ def apply_ac(model):
12
+ """Apply activation checkpointing to the model."""
13
+ for layer_id, transformer_block in enumerate(model.blocks):
14
+ transformer_block = ptd_checkpoint_wrapper(transformer_block, preserve_rng_state=False)
15
+ model.blocks[layer_id] = transformer_block
16
+ if getattr(model, 'enable_mcp', False):
17
+ for group_id, mcp_group in enumerate(model.mcp_blocks):
18
+ for block_id, transformer_block in enumerate(mcp_group):
19
+ transformer_block = ptd_checkpoint_wrapper(
20
+ transformer_block, preserve_rng_state=False)
21
+ model.mcp_blocks[group_id][block_id] = transformer_block
22
+
23
+
24
+ def shard_model(model,
25
+ param_dtype=torch.bfloat16,
26
+ reduce_dtype=torch.float32):
27
+ mp_policy = MixedPrecisionPolicy(
28
+ param_dtype=param_dtype,
29
+ reduce_dtype=reduce_dtype,
30
+ cast_forward_inputs=False,
31
+ )
32
+ fsdp_config = {"mp_policy": mp_policy, "reshard_after_forward": True}
33
+
34
+ for block in model.blocks:
35
+ fully_shard(block.attn1, **fsdp_config)
36
+ fully_shard(block.attn2, **fsdp_config)
37
+ fully_shard(block.ffn, **fsdp_config)
38
+ fully_shard(block, **fsdp_config)
39
+
40
+ if getattr(model, 'enable_mcp', False):
41
+ for mcp_group in model.mcp_blocks:
42
+ for block in mcp_group:
43
+ fully_shard(block.attn1, **fsdp_config)
44
+ fully_shard(block.attn2, **fsdp_config)
45
+ fully_shard(block.ffn, **fsdp_config)
46
+ fully_shard(block, **fsdp_config)
47
+
48
+ fully_shard(model, **fsdp_config)
49
+ return model
50
+
51
+
52
+ def free_model(model):
53
+ del model
54
+ gc.collect()
55
+ torch.cuda.empty_cache()
wan_va/distributed/util.py ADDED
@@ -0,0 +1,40 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Alibaba Wan Team Authors. All rights reserved.
2
+ import torch
3
+ import torch.distributed as dist
4
+
5
+
6
+ def _configure_model(model, shard_fn, param_dtype, device, eval_mode=True):
7
+ """
8
+ TODO
9
+ """
10
+ if eval_mode:
11
+ model.eval().requires_grad_(False)
12
+ if dist.is_initialized():
13
+ dist.barrier()
14
+
15
+ if dist.is_initialized():
16
+ model = shard_fn(model)
17
+ else:
18
+ model.to(param_dtype)
19
+ model.to(device)
20
+
21
+ return model
22
+
23
+
24
+ def init_distributed(world_size, local_rank, rank):
25
+ # if world_size > 1:
26
+ torch.cuda.set_device(local_rank)
27
+ dist.init_process_group(backend="nccl",
28
+ init_method="env://",
29
+ rank=rank,
30
+ world_size=world_size)
31
+
32
+ def dist_mean(local_tensor):
33
+ if dist.is_initialized():
34
+ dist.all_reduce(local_tensor, op=dist.ReduceOp.AVG)
35
+ return local_tensor
36
+
37
+ def dist_max(local_tensor):
38
+ if dist.is_initialized():
39
+ dist.all_reduce(local_tensor, op=dist.ReduceOp.MAX)
40
+ return local_tensor
wan_va/mcp.py ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import torch
3
+
4
+
5
+ def validate_mcp_settings(
6
+ num_mcp_depths,
7
+ mcp_blocks_per_depth,
8
+ mcp_hidden_collect_layers,
9
+ mcp_loss_weights=None,
10
+ num_layers=None,
11
+ ):
12
+ if num_mcp_depths <= 0:
13
+ raise ValueError("num_mcp_depths must be positive")
14
+ if mcp_blocks_per_depth <= 0:
15
+ raise ValueError("mcp_blocks_per_depth must be positive")
16
+ if len(mcp_hidden_collect_layers) == 0:
17
+ raise ValueError("mcp_hidden_collect_layers cannot be empty")
18
+ if len(set(mcp_hidden_collect_layers)) != len(mcp_hidden_collect_layers):
19
+ raise ValueError("mcp_hidden_collect_layers must be unique")
20
+ if any(layer < 0 for layer in mcp_hidden_collect_layers):
21
+ raise ValueError("mcp_hidden_collect_layers must be non-negative")
22
+ if num_layers is not None and max(mcp_hidden_collect_layers) >= num_layers:
23
+ raise ValueError(
24
+ "mcp_hidden_collect_layers must be smaller than the number of model layers"
25
+ )
26
+ if num_layers is not None and mcp_blocks_per_depth > num_layers:
27
+ raise ValueError(
28
+ "mcp_blocks_per_depth cannot exceed the number of model layers")
29
+ if mcp_loss_weights is not None:
30
+ if len(mcp_loss_weights) != num_mcp_depths:
31
+ raise ValueError("mcp_loss_weights must match num_mcp_depths")
32
+ if any(weight < 0 for weight in mcp_loss_weights):
33
+ raise ValueError("mcp_loss_weights must be non-negative")
34
+
35
+
36
+ def shift_latents_for_mcp(latents, frame_shift):
37
+ """Shift video latents into the future and mark targets backed by real frames."""
38
+ if latents.ndim != 5:
39
+ raise ValueError("latents must have shape [B, C, F, H, W]")
40
+ if frame_shift <= 0:
41
+ raise ValueError("frame_shift must be positive")
42
+
43
+ batch_size, _, num_frames, _, _ = latents.shape
44
+ if num_frames == 0:
45
+ raise ValueError("latents must contain at least one frame")
46
+
47
+ pad_frames = min(frame_shift, num_frames)
48
+ shifted = latents[:, :, pad_frames:]
49
+ last_frame = latents[:, :, -1:]
50
+ shifted = torch.cat(
51
+ [shifted, last_frame.expand(-1, -1, pad_frames, -1, -1)], dim=2
52
+ )
53
+
54
+ valid_frames = torch.arange(num_frames, device=latents.device) + frame_shift
55
+ valid_frames = valid_frames < num_frames
56
+ valid_mask = valid_frames.view(1, 1, num_frames, 1, 1).expand(
57
+ batch_size, -1, -1, -1, -1
58
+ )
59
+ return shifted, valid_mask
wan_va/modules/__init__.py ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from .utils import (
3
+ WanVAEStreamingWrapper,
4
+ load_text_encoder,
5
+ load_tokenizer,
6
+ load_transformer,
7
+ load_vae,
8
+ )
9
+
10
+ __all__ = [
11
+ 'load_transformer', 'load_text_encoder', 'load_tokenizer', 'load_vae',
12
+ 'WanVAEStreamingWrapper'
13
+ ]
wan_va/modules/model.py ADDED
@@ -0,0 +1,1196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import math
3
+ from copy import deepcopy
4
+
5
+ import torch
6
+ import torch.nn as nn
7
+ import torch.nn.functional as F
8
+ from diffusers.configuration_utils import ConfigMixin, register_to_config
9
+ from diffusers.models.attention import FeedForward
10
+ from diffusers.models.embeddings import (
11
+ PixArtAlphaTextProjection,
12
+ TimestepEmbedding,
13
+ Timesteps,
14
+ )
15
+ from diffusers.models.modeling_utils import ModelMixin
16
+ from diffusers.models.normalization import FP32LayerNorm
17
+ from einops import rearrange
18
+ from typing import Callable, ClassVar
19
+ from torch.nn.attention.flex_attention import (
20
+ _mask_mod_signature,
21
+ BlockMask,
22
+ create_block_mask,
23
+ flex_attention,
24
+ and_masks,
25
+ or_masks
26
+ )
27
+ from functools import partial
28
+
29
+ try:
30
+ from flash_attn_interface import flash_attn_func
31
+ except:
32
+ from flash_attn import flash_attn_func
33
+
34
+ __all__ = ['WanTransformer3DModel']
35
+
36
+
37
+ def custom_sdpa(q, k, v):
38
+ out = F.scaled_dot_product_attention(q.transpose(1, 2), k.transpose(1, 2),
39
+ v.transpose(1, 2))
40
+ return out.transpose(1, 2)
41
+
42
+ class FlexAttnFunc(nn.Module):
43
+ flex_attn: ClassVar[Callable] = torch.compile(
44
+ flex_attention, dynamic=True,
45
+ )
46
+ compiled_create_block_mask: ClassVar[Callable] = torch.compile(create_block_mask)
47
+ attention_mask: ClassVar[BlockMask] = None
48
+ cross_attention_mask: ClassVar[BlockMask] = None
49
+
50
+ def __init__(
51
+ self,
52
+ is_cross=False,
53
+ ) -> None:
54
+ super().__init__()
55
+ self.is_cross = is_cross
56
+
57
+ def forward(
58
+ self,
59
+ query: torch.Tensor,
60
+ key: torch.Tensor,
61
+ value: torch.Tensor,
62
+ dtype=torch.bfloat16,
63
+ ) -> torch.Tensor:
64
+ q_varlen = rearrange(query[0], "s n d -> 1 n s d")
65
+ k_varlen = rearrange(key[0], "s n d -> 1 n s d")
66
+ v_varlen = rearrange(value[0], "s n d -> 1 n s d")
67
+
68
+ half_dtypes = (torch.float16, torch.bfloat16)
69
+ assert dtype in half_dtypes
70
+ def half(x):
71
+ return x if x.dtype in half_dtypes else x.to(dtype)
72
+
73
+ q_varlen = half(q_varlen)
74
+ k_varlen = half(k_varlen)
75
+ v_varlen = half(v_varlen)
76
+ q_varlen = q_varlen.to(v_varlen.dtype)
77
+ k_varlen = k_varlen.to(v_varlen.dtype)
78
+
79
+ block_mask = FlexAttnFunc.cross_attention_mask if self.is_cross else FlexAttnFunc.attention_mask
80
+
81
+ x_out = FlexAttnFunc.flex_attn(q_varlen, k_varlen, v_varlen, block_mask=block_mask, kernel_options = {
82
+ "BLOCK_M": 64,
83
+ "BLOCK_N": 64,
84
+ "BLOCK_M1": 32,
85
+ "BLOCK_N1": 64,
86
+ "BLOCK_M2": 64,
87
+ "BLOCK_N2": 32,
88
+ })
89
+
90
+ x_out = rearrange(x_out, "b n s d -> b s n d")
91
+ return x_out
92
+
93
+ @staticmethod
94
+ @torch.no_grad()
95
+ def init_mask(
96
+ latent_shape,
97
+ action_shape,
98
+ padded_length,
99
+ chunk_size,
100
+ window_size,
101
+ patch_size,
102
+ device,
103
+ ):
104
+ torch._inductor.config.realize_opcount_threshold = 100
105
+ B, _, L_F, L_H, L_W = latent_shape
106
+ _, _, A_F, A_H, A_W = action_shape
107
+
108
+ latent_seq_id = torch.arange(B)[:, None, None, None].\
109
+ expand(-1, L_F // patch_size[0], L_H // patch_size[1], L_W // patch_size[2]).flatten()
110
+ action_seq_id = torch.arange(B)[:, None, None, None].expand(-1, A_F, A_H, A_W).flatten()
111
+ seq_ids = torch.cat([latent_seq_id] * 2 + [action_seq_id] * 2)
112
+
113
+ latent_frame_id = torch.arange(L_F)[None, :, None, None].expand(B, -1, L_H // patch_size[1], L_W // patch_size[2])[None].flatten()
114
+ action_frame_id = torch.arange(A_F)[None, :, None, None].expand(B, -1, A_H, A_W)[None].flatten()
115
+ frame_ids = torch.cat([latent_frame_id // chunk_size * 2] * 2 + [action_frame_id // chunk_size * 2 + 1] * 2)
116
+
117
+ noise_ids = torch.cat(
118
+ [
119
+ torch.zeros_like(latent_frame_id),
120
+ torch.ones_like(latent_frame_id),
121
+ torch.zeros_like(action_frame_id),
122
+ torch.ones_like(action_frame_id),
123
+ ]
124
+ )
125
+
126
+ seq_ids = F.pad(seq_ids, (0, padded_length), value=-1)
127
+ frame_ids = F.pad(frame_ids, (0, padded_length), value=-1)
128
+ noise_ids = F.pad(noise_ids, (0, padded_length), value=-1)
129
+
130
+ mask_mod = FlexAttnFunc._get_mask_mod(seq_ids.long().to(device), frame_ids.long().to(device), noise_ids.long().to(device), window_size)
131
+ block_mask = FlexAttnFunc.compiled_create_block_mask(
132
+ mask_mod, 1, 1, len(seq_ids), len(seq_ids), device=device, _compile=True
133
+ )
134
+ FlexAttnFunc.attention_mask = block_mask
135
+
136
+ text_seq_ids = torch.arange(B)[:, None].expand(-1, 512).flatten()
137
+ mask_mod_cross = FlexAttnFunc._get_cross_mask_mod(seq_ids.long().to(device), text_seq_ids.long().to(device))
138
+ block_mask_cross = FlexAttnFunc.compiled_create_block_mask(
139
+ mask_mod_cross, 1, 1, len(seq_ids), len(text_seq_ids), device=device, _compile=True
140
+ )
141
+ FlexAttnFunc.cross_attention_mask = block_mask_cross
142
+
143
+ @staticmethod
144
+ @torch.no_grad()
145
+ def _get_cross_mask_mod(seq_ids, text_seq_ids):
146
+ def seq_mask(
147
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
148
+ ):
149
+ return (seq_ids[q_idx] == text_seq_ids[kv_idx]) & (seq_ids[q_idx] >=0 ) & (text_seq_ids[kv_idx] >= 0)
150
+ return seq_mask
151
+
152
+ @staticmethod
153
+ @torch.no_grad()
154
+ def _get_mask_mod(seq_ids, frame_ids, noise_ids, window_size):
155
+ def seq_mask(
156
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
157
+ ):
158
+ return (seq_ids[q_idx] == seq_ids[kv_idx]) & (seq_ids[q_idx] >=0 ) & (seq_ids[kv_idx] >= 0)
159
+
160
+ def block_causal_mask(
161
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
162
+ ):
163
+ return (frame_ids[kv_idx] <= frame_ids[q_idx])
164
+
165
+ def block_causal_mask_exclude_self(
166
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
167
+ ):
168
+ return (frame_ids[kv_idx] < frame_ids[q_idx])
169
+
170
+ def block_self_mask(
171
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
172
+ ):
173
+ return (frame_ids[kv_idx] == frame_ids[q_idx])
174
+
175
+ def clean2clean_mask(
176
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
177
+ ):
178
+ return (noise_ids[q_idx] == 1) & (noise_ids[kv_idx] == 1)
179
+
180
+ def noise2clean_mask(
181
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
182
+ ):
183
+ return (noise_ids[q_idx] == 0) & (noise_ids[kv_idx] == 1)
184
+ def noise2noise_mask(
185
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor
186
+ ):
187
+ return (noise_ids[q_idx] == 0) & (noise_ids[kv_idx] == 0)
188
+
189
+ def block_window_mask(
190
+ b: torch.Tensor, h: torch.Tensor, q_idx: torch.Tensor, kv_idx: torch.Tensor, window_size: int
191
+ ):
192
+ return ((frame_ids[q_idx] - frame_ids[kv_idx]).abs() <= window_size)
193
+
194
+ mask_list = []
195
+ mask_list.append(and_masks(clean2clean_mask, block_causal_mask))
196
+ mask_list.append(and_masks(noise2clean_mask, block_causal_mask_exclude_self))
197
+ mask_list.append(and_masks(noise2noise_mask, block_self_mask))
198
+ mask = or_masks(*mask_list)
199
+ mask = and_masks(mask, seq_mask)
200
+ mask = and_masks(mask, partial(block_window_mask, window_size=window_size))
201
+ return mask
202
+
203
+ class WanTimeTextImageEmbedding(nn.Module):
204
+
205
+ def __init__(
206
+ self,
207
+ dim,
208
+ time_freq_dim,
209
+ time_proj_dim,
210
+ text_embed_dim,
211
+ pos_embed_seq_len,
212
+ ):
213
+ super().__init__()
214
+
215
+ self.timesteps_proj = Timesteps(num_channels=time_freq_dim,
216
+ flip_sin_to_cos=True,
217
+ downscale_freq_shift=0)
218
+ self.time_embedder = TimestepEmbedding(in_channels=time_freq_dim,
219
+ time_embed_dim=dim)
220
+ self.act_fn = nn.SiLU()
221
+ self.time_proj = nn.Linear(dim, time_proj_dim)
222
+ self.text_embedder = PixArtAlphaTextProjection(text_embed_dim,
223
+ dim,
224
+ act_fn="gelu_tanh")
225
+
226
+ def forward(
227
+ self,
228
+ timestep: torch.Tensor,
229
+ dtype=None,
230
+ ):
231
+ B, L = timestep.shape
232
+ timestep = timestep.reshape(-1)
233
+ timestep = self.timesteps_proj(timestep)
234
+ # time_embedder_dtype = next(iter(self.time_embedder.parameters())).dtype
235
+ time_embedder_dtype = self.time_embedder.linear_1.weight.dtype
236
+ if timestep.dtype != time_embedder_dtype and time_embedder_dtype != torch.int8:
237
+ timestep = timestep.to(time_embedder_dtype)
238
+ temb = self.time_embedder(timestep).to(dtype=dtype)
239
+ timestep_proj = self.time_proj(self.act_fn(temb))
240
+ return temb.reshape(B, L, -1), timestep_proj.reshape(B, L, -1)
241
+
242
+
243
+ class WanRotaryPosEmbed(nn.Module):
244
+ def __init__(
245
+ self,
246
+ attention_head_dim: int,
247
+ patch_size,
248
+ max_seq_len: int,
249
+ theta: float = 10000.0,
250
+ ):
251
+ super().__init__()
252
+
253
+ self.attention_head_dim = attention_head_dim
254
+ self.patch_size = patch_size
255
+ self.max_seq_len = max_seq_len
256
+ self.theta = theta
257
+
258
+ self.f_dim = self.attention_head_dim - 2 * (self.attention_head_dim // 3)
259
+ self.h_dim = self.attention_head_dim // 3
260
+ self.w_dim = self.attention_head_dim // 3
261
+
262
+ # Precompute and register buffers
263
+ f_freqs_base, h_freqs_base, w_freqs_base = self._precompute_freqs_base()
264
+ self.f_freqs_base = f_freqs_base
265
+ self.h_freqs_base = h_freqs_base
266
+ self.w_freqs_base = w_freqs_base
267
+
268
+ def _precompute_freqs_base(self):
269
+ # freqs_base = 1.0 / (theta ** (2k / dim))
270
+ f_freqs_base = 1.0 / (self.theta**(torch.arange(
271
+ 0, self.f_dim, 2)[:(self.f_dim // 2)].double() / self.f_dim))
272
+ h_freqs_base = 1.0 / (self.theta**(torch.arange(
273
+ 0, self.h_dim, 2)[:(self.h_dim // 2)].double() / self.h_dim))
274
+ w_freqs_base = 1.0 / (self.theta**(torch.arange(
275
+ 0, self.w_dim, 2)[:(self.w_dim // 2)].double() / self.w_dim))
276
+ return f_freqs_base, h_freqs_base, w_freqs_base
277
+
278
+ def forward(self, grid_ids):
279
+ with torch.no_grad():
280
+ f_freqs = grid_ids[:, 0, :].unsqueeze(-1) * self.f_freqs_base.to(grid_ids.device)
281
+ h_freqs = grid_ids[:, 1, :].unsqueeze(-1) * self.h_freqs_base.to(grid_ids.device)
282
+ w_freqs = grid_ids[:, 2, :].unsqueeze(-1) * self.w_freqs_base.to(grid_ids.device)
283
+ freqs = torch.cat([f_freqs, h_freqs, w_freqs], dim=-1).float()
284
+ freqs_cis = torch.polar(torch.ones_like(freqs), freqs)
285
+
286
+ return freqs_cis
287
+
288
+
289
+ class WanAttention(torch.nn.Module):
290
+
291
+ def __init__(
292
+ self,
293
+ dim,
294
+ heads=8,
295
+ dim_head=64,
296
+ eps=1e-5,
297
+ dropout=0.0,
298
+ cross_attention_dim_head=None,
299
+ attn_mode='torch',
300
+ ):
301
+ super().__init__()
302
+ if attn_mode == 'torch':
303
+ self.attn_op = custom_sdpa
304
+ elif attn_mode == 'flashattn':
305
+ self.attn_op = flash_attn_func
306
+ elif attn_mode == 'flex':
307
+ self.attn_op = FlexAttnFunc(cross_attention_dim_head is not None)
308
+ else:
309
+ raise ValueError(
310
+ f"Unsupported attention mode: {attn_mode}, only support torch and flashattn"
311
+ )
312
+
313
+ self.inner_dim = dim_head * heads
314
+ self.heads = heads
315
+ self.cross_attention_dim_head = cross_attention_dim_head
316
+ self.kv_inner_dim = self.inner_dim if cross_attention_dim_head is None else cross_attention_dim_head * heads
317
+
318
+ self.to_q = torch.nn.Linear(dim, self.inner_dim, bias=True)
319
+ self.to_k = torch.nn.Linear(dim, self.kv_inner_dim, bias=True)
320
+ self.to_v = torch.nn.Linear(dim, self.kv_inner_dim, bias=True)
321
+ self.to_out = torch.nn.ModuleList([
322
+ torch.nn.Linear(self.inner_dim, dim, bias=True),
323
+ torch.nn.Dropout(dropout),
324
+ ])
325
+ self.norm_q = torch.nn.RMSNorm(dim_head * heads,
326
+ eps=eps,
327
+ elementwise_affine=True)
328
+ self.norm_k = torch.nn.RMSNorm(dim_head * heads,
329
+ eps=eps,
330
+ elementwise_affine=True)
331
+ self.attn_caches = {} if cross_attention_dim_head is None else None
332
+
333
+ def clear_pred_cache(self, cache_name):
334
+ if self.attn_caches is None:
335
+ return
336
+ cache = self.attn_caches[cache_name]
337
+ is_pred = cache['is_pred']
338
+ cache['mask'][is_pred] = False
339
+
340
+ def clear_cache(self, cache_name):
341
+ if self.attn_caches is None:
342
+ return
343
+ self.attn_caches[cache_name] = None
344
+
345
+ def init_kv_cache(self, cache_name, total_tolen, num_head, head_dim,
346
+ device, dtype, batch_size):
347
+ if self.attn_caches is None:
348
+ return
349
+ self.attn_caches[cache_name] = {
350
+ 'k':
351
+ torch.empty([batch_size, total_tolen, num_head, head_dim],
352
+ device=device,
353
+ dtype=dtype),
354
+ 'v':
355
+ torch.empty([batch_size, total_tolen, num_head, head_dim],
356
+ device=device,
357
+ dtype=dtype),
358
+ 'id':
359
+ torch.full((total_tolen, ), -1, device=device),
360
+ "mask":
361
+ torch.zeros((total_tolen, ), dtype=torch.bool, device=device),
362
+ "is_pred":
363
+ torch.zeros((total_tolen, ), dtype=torch.bool, device=device),
364
+ }
365
+
366
+ def allocate_slots(self, cache_name, key_size):
367
+ cache = self.attn_caches[cache_name]
368
+ mask = cache["mask"]
369
+ ids = cache["id"]
370
+ free = (~mask).nonzero(as_tuple=False).squeeze(-1)
371
+
372
+ if free.numel() < key_size:
373
+ used = mask.nonzero(as_tuple=False).squeeze(-1)
374
+
375
+ used_ids = ids[used]
376
+ order = torch.argsort(used_ids)
377
+ need = key_size - free.numel()
378
+ to_free = used[order[:need]]
379
+
380
+ mask[to_free] = False
381
+ ids[to_free] = -1
382
+ free = (~mask).nonzero(as_tuple=False).squeeze(-1)
383
+
384
+ assert free.numel() >= key_size
385
+ return free[:key_size]
386
+
387
+ def _next_cache_id(self, cache_name):
388
+ ids = self.attn_caches[cache_name]['id']
389
+ mask = self.attn_caches[cache_name]['mask']
390
+
391
+ if mask.any():
392
+ return ids[mask].max() + 1
393
+ else:
394
+ return torch.tensor(0, device=ids.device, dtype=ids.dtype)
395
+
396
+ def update_cache(self, cache_name, key, value, is_pred):
397
+ cache = self.attn_caches[cache_name]
398
+
399
+ key_size = key.shape[1]
400
+ slots = self.allocate_slots(cache_name, key_size)
401
+
402
+ new_id = self._next_cache_id(cache_name)
403
+
404
+ cache['k'][:, slots] = key
405
+ cache['v'][:, slots] = value
406
+ cache['mask'][slots] = True
407
+ cache['id'][slots] = new_id
408
+ cache['is_pred'][slots] = is_pred
409
+ return slots
410
+
411
+ def restore_cache(self, cache_name, slots):
412
+ self.attn_caches[cache_name]['mask'][slots] = False
413
+
414
+ def forward(
415
+ self,
416
+ q,
417
+ k,
418
+ v,
419
+ rotary_emb,
420
+ update_cache=0,
421
+ cache_name='pos',
422
+ ):
423
+ kv_cache = self.attn_caches[
424
+ cache_name] if (self.attn_caches is not None) and (cache_name in self.attn_caches) else None
425
+
426
+ query, key, value = self.to_q(q), self.to_k(k), self.to_v(v)
427
+ query = self.norm_q(query)
428
+ query = query.unflatten(2, (self.heads, -1))
429
+ key = self.norm_k(key)
430
+ key = key.unflatten(2, (self.heads, -1))
431
+ value = value.unflatten(2, (self.heads, -1))
432
+ if rotary_emb is not None:
433
+
434
+ def apply_rotary_emb(x, freqs):
435
+ x_out = torch.view_as_complex(
436
+ x.to(torch.float64).reshape(x.shape[0], x.shape[1],
437
+ x.shape[2], -1, 2))
438
+ x_out = torch.view_as_real(x_out * freqs).flatten(3)
439
+ return x_out.to(x.dtype)
440
+ query = apply_rotary_emb(query, rotary_emb)
441
+ key = apply_rotary_emb(key, rotary_emb)
442
+ slots = None
443
+ if kv_cache is not None and kv_cache['k'] is not None:
444
+ slots = self.update_cache(cache_name,
445
+ key,
446
+ value,
447
+ is_pred=(update_cache == 1))
448
+ key_pool = self.attn_caches[cache_name]['k']
449
+ value_pool = self.attn_caches[cache_name]['v']
450
+ mask = self.attn_caches[cache_name]['mask']
451
+ valid = mask.nonzero(as_tuple=False).squeeze(-1)
452
+ key = key_pool[:, valid]
453
+ value = value_pool[:, valid]
454
+
455
+ hidden_states = self.attn_op(query, key, value)
456
+
457
+ if update_cache == 0:
458
+ if kv_cache is not None and kv_cache['k'] is not None:
459
+ self.restore_cache(cache_name, slots)
460
+
461
+ hidden_states = hidden_states.flatten(2, 3)
462
+ hidden_states = hidden_states.type_as(query)
463
+ hidden_states = self.to_out[0](hidden_states)
464
+ hidden_states = self.to_out[1](hidden_states)
465
+ return hidden_states
466
+
467
+
468
+ class WanTransformerBlock(nn.Module):
469
+
470
+ def __init__(
471
+ self,
472
+ dim,
473
+ ffn_dim,
474
+ num_heads,
475
+ cross_attn_norm=False,
476
+ eps=1e-6,
477
+ attn_mode: str = "flashattn",
478
+ ):
479
+ super().__init__()
480
+ self.attn_mode = attn_mode
481
+
482
+ # 1. Self-attention
483
+ self.norm1 = FP32LayerNorm(dim, eps, elementwise_affine=False)
484
+ self.attn1 = WanAttention(
485
+ dim=dim,
486
+ heads=num_heads,
487
+ dim_head=dim // num_heads,
488
+ eps=eps,
489
+ cross_attention_dim_head=None,
490
+ attn_mode=attn_mode,
491
+ )
492
+
493
+ # 2. Cross-attention
494
+ self.attn2 = WanAttention(
495
+ dim=dim,
496
+ heads=num_heads,
497
+ dim_head=dim // num_heads,
498
+ eps=eps,
499
+ cross_attention_dim_head=dim // num_heads,
500
+ attn_mode=attn_mode,
501
+ )
502
+ self.norm2 = FP32LayerNorm(
503
+ dim, eps,
504
+ elementwise_affine=True) if cross_attn_norm else nn.Identity()
505
+
506
+ # 3. Feed-forward
507
+ self.ffn = FeedForward(dim,
508
+ inner_dim=ffn_dim,
509
+ activation_fn="gelu-approximate")
510
+ self.norm3 = FP32LayerNorm(dim, eps, elementwise_affine=False)
511
+
512
+ self.scale_shift_table = nn.Parameter(
513
+ torch.randn(1, 6, dim) / dim**0.5)
514
+
515
+ def forward(
516
+ self,
517
+ hidden_states,
518
+ encoder_hidden_states,
519
+ temb,
520
+ rotary_emb,
521
+ update_cache=0,
522
+ cache_name='pos',
523
+ ) -> torch.Tensor:
524
+ temb_scale_shift_table = self.scale_shift_table[None] + temb.float()
525
+ shift_msa, scale_msa, gate_msa, c_shift_msa, c_scale_msa, c_gate_msa = \
526
+ rearrange(temb_scale_shift_table, 'b l n c -> b n l c').chunk(6, dim=1)
527
+ shift_msa = shift_msa.squeeze(1)
528
+ scale_msa = scale_msa.squeeze(1)
529
+ gate_msa = gate_msa.squeeze(1)
530
+ c_shift_msa = c_shift_msa.squeeze(1)
531
+ c_scale_msa = c_scale_msa.squeeze(1)
532
+ c_gate_msa = c_gate_msa.squeeze(1)
533
+ # 1. Self-attention
534
+ norm_hidden_states = (self.norm1(hidden_states.float()) *
535
+ (1. + scale_msa) +
536
+ shift_msa).type_as(hidden_states)
537
+ attn_output = self.attn1(norm_hidden_states,
538
+ norm_hidden_states,
539
+ norm_hidden_states,
540
+ rotary_emb,
541
+ update_cache=update_cache,
542
+ cache_name=cache_name)
543
+ hidden_states = (hidden_states.float() +
544
+ attn_output * gate_msa).type_as(hidden_states)
545
+
546
+ # 2. Cross-attention
547
+ norm_hidden_states = self.norm2(
548
+ hidden_states.float()).type_as(hidden_states)
549
+ attn_output = self.attn2(norm_hidden_states,
550
+ encoder_hidden_states,
551
+ encoder_hidden_states,
552
+ None,
553
+ update_cache=0,
554
+ cache_name=cache_name)
555
+ hidden_states = hidden_states + attn_output
556
+
557
+ # 3. Feed-forward
558
+ norm_hidden_states = (self.norm3(hidden_states.float()) *
559
+ (1. + c_scale_msa) +
560
+ c_shift_msa).type_as(hidden_states)
561
+
562
+ ff_output = self.ffn(norm_hidden_states)
563
+
564
+ hidden_states = (hidden_states.float() +
565
+ ff_output.float() * c_gate_msa).type_as(hidden_states)
566
+ return hidden_states
567
+
568
+
569
+ class WanTransformer3DModel(ModelMixin, ConfigMixin):
570
+ r"""
571
+ TODO
572
+ """
573
+ _supports_gradient_checkpointing = True
574
+ _skip_layerwise_casting_patterns = [
575
+ # "patch_embedding",
576
+ "patch_embedding_mlp",
577
+ "condition_embedder",
578
+ 'condition_embedder_action',
579
+ "norm"]
580
+ _no_split_modules = ["WanTransformerBlock"]
581
+ _keep_in_fp32_modules = ["time_embedder",
582
+ "scale_shift_table",
583
+ "scale_shift_table_action",
584
+ "norm1",
585
+ 'action_norm1',
586
+ 'text_norm1',
587
+ "norm2",
588
+ 'action_norm2',
589
+ 'text_norm2',
590
+ "norm3",
591
+ 'action_norm3',
592
+ 'text_norm3'
593
+ ]
594
+ _keys_to_ignore_on_load_unexpected = ["norm_added_q"]
595
+ _repeated_blocks = ["WanTransformerBlock"]
596
+
597
+ @register_to_config
598
+ def __init__(self,
599
+ patch_size=[1, 2, 2],
600
+ num_attention_heads=24,
601
+ attention_head_dim=128,
602
+ in_channels=48,
603
+ out_channels=48,
604
+ action_dim=30,
605
+ text_dim=4096,
606
+ freq_dim=256,
607
+ ffn_dim=14336,
608
+ num_layers=30,
609
+ cross_attn_norm=True,
610
+ eps=1e-06,
611
+ rope_max_seq_len=1024,
612
+ pos_embed_seq_len=None,
613
+ attn_mode="torch",
614
+ enable_mcp=False,
615
+ num_mcp_depths=3,
616
+ mcp_blocks_per_depth=3,
617
+ mcp_hidden_collect_layers=(3, 11, 19, 29)):
618
+ r"""
619
+ TODO
620
+ """
621
+ super().__init__()
622
+ self.patch_size = patch_size
623
+ self.num_attention_heads = num_attention_heads
624
+ self.attention_head_dim = attention_head_dim
625
+ inner_dim = num_attention_heads * attention_head_dim
626
+ self.inner_dim = inner_dim
627
+ self.ffn_dim = ffn_dim
628
+ self.cross_attn_norm = cross_attn_norm
629
+ self.eps = eps
630
+ self.attn_mode = attn_mode
631
+ self.rope = WanRotaryPosEmbed(attention_head_dim, patch_size,
632
+ rope_max_seq_len)
633
+ self.patch_embedding_mlp = nn.Linear(
634
+ in_channels * patch_size[0] * patch_size[1] * patch_size[2],
635
+ inner_dim)
636
+ self.action_embedder = nn.Linear(action_dim, inner_dim)
637
+ self.condition_embedder = WanTimeTextImageEmbedding(
638
+ dim=inner_dim,
639
+ time_freq_dim=freq_dim,
640
+ time_proj_dim=inner_dim * 6,
641
+ text_embed_dim=text_dim,
642
+ pos_embed_seq_len=pos_embed_seq_len,
643
+ )
644
+ self.condition_embedder_action = deepcopy(self.condition_embedder)
645
+
646
+ self.blocks = nn.ModuleList([
647
+ WanTransformerBlock(inner_dim,
648
+ ffn_dim,
649
+ num_attention_heads,
650
+ cross_attn_norm,
651
+ eps,
652
+ attn_mode=attn_mode) for _ in range(num_layers)
653
+ ])
654
+
655
+ self.enable_mcp = enable_mcp
656
+ self.num_mcp_depths = num_mcp_depths
657
+ self.mcp_blocks_per_depth = mcp_blocks_per_depth
658
+ self.mcp_hidden_collect_layers = list(mcp_hidden_collect_layers)
659
+ if self.enable_mcp:
660
+ self._build_mcp_modules()
661
+
662
+ self.norm_out = FP32LayerNorm(inner_dim, eps, elementwise_affine=False)
663
+ self.proj_out = nn.Linear(inner_dim,
664
+ out_channels * math.prod(patch_size))
665
+ self.action_proj_out = nn.Linear(inner_dim, action_dim)
666
+ self.scale_shift_table = nn.Parameter(
667
+ torch.randn(1, 2, inner_dim) / inner_dim**0.5)
668
+
669
+ def _validate_mcp_architecture(self):
670
+ if self.num_mcp_depths <= 0:
671
+ raise ValueError("num_mcp_depths must be positive")
672
+ if self.mcp_blocks_per_depth <= 0:
673
+ raise ValueError("mcp_blocks_per_depth must be positive")
674
+ if not self.mcp_hidden_collect_layers:
675
+ raise ValueError("mcp_hidden_collect_layers cannot be empty")
676
+ if len(set(self.mcp_hidden_collect_layers)) != len(
677
+ self.mcp_hidden_collect_layers):
678
+ raise ValueError("mcp_hidden_collect_layers must be unique")
679
+ if min(self.mcp_hidden_collect_layers) < 0 or max(
680
+ self.mcp_hidden_collect_layers) >= len(self.blocks):
681
+ raise ValueError(
682
+ "mcp_hidden_collect_layers must reference existing model layers")
683
+ if self.mcp_blocks_per_depth > len(self.blocks):
684
+ raise ValueError(
685
+ "mcp_blocks_per_depth cannot exceed the number of model layers")
686
+
687
+ def _build_mcp_modules(self):
688
+ self._validate_mcp_architecture()
689
+ self.mcp_hidden_fuser = nn.Sequential(
690
+ nn.Linear(
691
+ self.inner_dim * len(self.mcp_hidden_collect_layers),
692
+ self.inner_dim,
693
+ ),
694
+ nn.SiLU(),
695
+ nn.Linear(self.inner_dim, self.inner_dim),
696
+ )
697
+ self.mcp_input_projections = nn.ModuleList([
698
+ nn.Linear(self.inner_dim * 2, self.inner_dim)
699
+ for _ in range(self.num_mcp_depths)
700
+ ])
701
+ self.mcp_blocks = nn.ModuleList([
702
+ nn.ModuleList([
703
+ WanTransformerBlock(
704
+ self.inner_dim,
705
+ self.ffn_dim,
706
+ self.num_attention_heads,
707
+ self.cross_attn_norm,
708
+ self.eps,
709
+ attn_mode=self.attn_mode,
710
+ ) for _ in range(self.mcp_blocks_per_depth)
711
+ ]) for _ in range(self.num_mcp_depths)
712
+ ])
713
+
714
+ def enable_mcp_training(self,
715
+ num_mcp_depths,
716
+ mcp_blocks_per_depth,
717
+ mcp_hidden_collect_layers,
718
+ init_from_backbone=True):
719
+ requested_layers = list(mcp_hidden_collect_layers)
720
+ if self.enable_mcp:
721
+ current = (
722
+ self.num_mcp_depths,
723
+ self.mcp_blocks_per_depth,
724
+ self.mcp_hidden_collect_layers,
725
+ )
726
+ requested = (
727
+ num_mcp_depths,
728
+ mcp_blocks_per_depth,
729
+ requested_layers,
730
+ )
731
+ if current != requested:
732
+ raise ValueError(
733
+ f"MCP checkpoint architecture {current} does not match {requested}")
734
+ return False
735
+
736
+ self.enable_mcp = True
737
+ self.num_mcp_depths = num_mcp_depths
738
+ self.mcp_blocks_per_depth = mcp_blocks_per_depth
739
+ self.mcp_hidden_collect_layers = requested_layers
740
+ self._build_mcp_modules()
741
+
742
+ reference = next(self.blocks[0].parameters())
743
+ self.mcp_hidden_fuser.to(device=reference.device,
744
+ dtype=reference.dtype)
745
+ self.mcp_input_projections.to(device=reference.device,
746
+ dtype=reference.dtype)
747
+ self.mcp_blocks.to(device=reference.device, dtype=reference.dtype)
748
+ self._initialize_mcp_projection_weights()
749
+ if init_from_backbone:
750
+ self.initialize_mcp_blocks_from_backbone()
751
+
752
+ used_default_values = set(
753
+ self.config.get("_use_default_values", []))
754
+ used_default_values.difference_update({
755
+ "enable_mcp",
756
+ "num_mcp_depths",
757
+ "mcp_blocks_per_depth",
758
+ "mcp_hidden_collect_layers",
759
+ })
760
+ self.register_to_config(
761
+ enable_mcp=True,
762
+ num_mcp_depths=num_mcp_depths,
763
+ mcp_blocks_per_depth=mcp_blocks_per_depth,
764
+ mcp_hidden_collect_layers=requested_layers,
765
+ _use_default_values=sorted(used_default_values),
766
+ )
767
+ return True
768
+
769
+ def _initialize_mcp_projection_weights(self):
770
+ for module in [self.mcp_hidden_fuser, self.mcp_input_projections]:
771
+ for layer in module.modules():
772
+ if isinstance(layer, nn.Linear):
773
+ nn.init.normal_(layer.weight, std=0.02)
774
+ if layer.bias is not None:
775
+ nn.init.zeros_(layer.bias)
776
+
777
+ def initialize_mcp_blocks_from_backbone(self):
778
+ if not self.enable_mcp:
779
+ return
780
+ source_blocks = self.blocks[-self.mcp_blocks_per_depth:]
781
+ for group in self.mcp_blocks:
782
+ for target_block, source_block in zip(group, source_blocks):
783
+ target_block.load_state_dict(source_block.state_dict())
784
+
785
+ def disable_mcp_modules(self):
786
+ if not self.enable_mcp:
787
+ return
788
+ del self.mcp_hidden_fuser
789
+ del self.mcp_input_projections
790
+ del self.mcp_blocks
791
+ self.enable_mcp = False
792
+ self.register_to_config(enable_mcp=False)
793
+
794
+ def clear_cache(self, cache_name):
795
+ for block in self.blocks:
796
+ block.attn1.clear_cache(cache_name)
797
+
798
+ def clear_pred_cache(self, cache_name):
799
+ for block in self.blocks:
800
+ block.attn1.clear_pred_cache(cache_name)
801
+
802
+ def create_empty_cache(self, cache_name, attn_window,
803
+ latent_token_per_chunk, action_token_per_chunk,
804
+ device, dtype, batch_size):
805
+ total_tolen = (attn_window // 2) * latent_token_per_chunk + (
806
+ attn_window // 2) * action_token_per_chunk
807
+ for block in self.blocks:
808
+ block.attn1.init_kv_cache(cache_name, total_tolen,
809
+ self.num_attention_heads,
810
+ self.attention_head_dim, device, dtype, batch_size)
811
+
812
+ def _input_embed(self, latents, input_type='latent'):
813
+ if input_type == 'latent':
814
+ hidden_states = rearrange(
815
+ latents,
816
+ 'b c (f p1) (h p2) (w p3) -> b (f h w) (c p1 p2 p3)',
817
+ p1=self.patch_size[0],
818
+ p2=self.patch_size[1],
819
+ p3=self.patch_size[2])
820
+ hidden_states = self.patch_embedding_mlp(hidden_states)
821
+ elif input_type == 'action':
822
+ hidden_states = rearrange(latents, 'b c f h w -> b (f h w) c')
823
+ hidden_states = self.action_embedder(hidden_states)
824
+ elif input_type == 'text':
825
+ hidden_states = self.condition_embedder.text_embedder(latents)
826
+ else:
827
+ raise ValueError(f"Unsupported input type: {input_type}")
828
+ return hidden_states
829
+
830
+ def _time_embed(self, timesteps, H, W, dtype, action_mode=False):
831
+ pach_scale_h, pach_scale_w = (1, 1) if action_mode else (
832
+ self.patch_size[1], self.patch_size[2])
833
+ latent_time_steps = torch.repeat_interleave(
834
+ timesteps,
835
+ (H // pach_scale_h) *
836
+ (W // pach_scale_w), dim=1) # L
837
+ current_condition_embedder = self.condition_embedder_action if action_mode else self.condition_embedder
838
+ temb, timestep_proj = current_condition_embedder(
839
+ latent_time_steps, dtype=dtype)
840
+ timestep_proj = timestep_proj.unflatten(2, (6, -1)) # B L 6 C
841
+ return temb, timestep_proj
842
+
843
+ def _forward_mcp(
844
+ self,
845
+ input_dict,
846
+ hidden_states,
847
+ collected_hidden_states,
848
+ text_hidden_states,
849
+ latent_grid_id,
850
+ action_grid_id,
851
+ latent_timestep_proj,
852
+ action_timestep_proj,
853
+ split_list,
854
+ batch_size,
855
+ ):
856
+ mcp_latent_dicts = input_dict.get('mcp_latent_dicts')
857
+ if not self.enable_mcp or not mcp_latent_dicts:
858
+ return []
859
+ if len(mcp_latent_dicts) != self.num_mcp_depths:
860
+ raise ValueError(
861
+ "MCP input depth count must match num_mcp_depths")
862
+ if len(collected_hidden_states) != len(
863
+ self.mcp_hidden_collect_layers):
864
+ raise RuntimeError(
865
+ "MCP did not collect all configured backbone hidden states")
866
+
867
+ latent_length, clean_latent_length, action_length, clean_action_length, padded_length = split_list
868
+ if latent_length != clean_latent_length:
869
+ raise ValueError("MCP requires noisy and clean video token lengths to match")
870
+
871
+ video_length = latent_length + clean_latent_length
872
+ video_hidden_states = [
873
+ states[:, :video_length] for states in collected_hidden_states
874
+ ]
875
+ fused_hidden_states = self.mcp_hidden_fuser(
876
+ torch.cat(video_hidden_states, dim=-1))
877
+ previous_hidden_states = fused_hidden_states[:, :latent_length]
878
+ clean_hidden_states = fused_hidden_states[:, latent_length:]
879
+
880
+ action_start = video_length
881
+ action_end = action_start + action_length + clean_action_length
882
+ action_hidden_states = hidden_states[:, action_start:action_end]
883
+ clean_latent_timestep_proj = latent_timestep_proj[
884
+ :, latent_length:video_length]
885
+
886
+ outputs = []
887
+ for depth in range(self.num_mcp_depths):
888
+ mcp_latent_dict = mcp_latent_dicts[depth]
889
+ noisy_latents = mcp_latent_dict['noisy_latents'].to(torch.bfloat16)
890
+ noisy_hidden_states = self._input_embed(
891
+ noisy_latents, input_type='latent').flatten(0, 1)[None]
892
+ if noisy_hidden_states.shape[1] != latent_length:
893
+ raise ValueError(
894
+ "MCP future video token length must match the main video token length")
895
+
896
+ future_hidden_states = self.mcp_input_projections[depth](torch.cat(
897
+ [previous_hidden_states, noisy_hidden_states], dim=-1))
898
+ mcp_hidden_states = torch.cat([
899
+ future_hidden_states,
900
+ clean_hidden_states,
901
+ action_hidden_states,
902
+ ], dim=1)
903
+ mcp_hidden_states = F.pad(
904
+ mcp_hidden_states, (0, 0, 0, padded_length))
905
+
906
+ future_grid_id = mcp_latent_dict['grid_id'].permute(
907
+ 1, 0, 2).flatten(1)[None]
908
+ full_grid_id = torch.cat([
909
+ future_grid_id,
910
+ latent_grid_id,
911
+ action_grid_id,
912
+ action_grid_id,
913
+ ], dim=2)
914
+ mcp_rotary_emb = self.rope(full_grid_id)[:, :, None]
915
+ mcp_rotary_emb = F.pad(
916
+ mcp_rotary_emb, (0, 0, 0, 0, 0, padded_length))
917
+
918
+ future_time_steps = mcp_latent_dict['timesteps'].flatten()[None]
919
+ future_temb, future_timestep_proj = self._time_embed(
920
+ future_time_steps,
921
+ noisy_latents.shape[-2],
922
+ noisy_latents.shape[-1],
923
+ dtype=mcp_hidden_states.dtype,
924
+ action_mode=False,
925
+ )
926
+ mcp_timestep_proj = torch.cat([
927
+ future_timestep_proj,
928
+ clean_latent_timestep_proj,
929
+ action_timestep_proj,
930
+ ], dim=1)
931
+ mcp_timestep_proj = F.pad(
932
+ mcp_timestep_proj, (0, 0, 0, 0, 0, padded_length))
933
+
934
+ for block in self.mcp_blocks[depth]:
935
+ mcp_hidden_states = block(
936
+ mcp_hidden_states,
937
+ text_hidden_states,
938
+ mcp_timestep_proj,
939
+ mcp_rotary_emb,
940
+ update_cache=False,
941
+ )
942
+
943
+ previous_hidden_states = mcp_hidden_states[:, :latent_length]
944
+ mcp_output = previous_hidden_states
945
+ temb_scale_shift_table = (
946
+ self.scale_shift_table[None] + future_temb[:, :, None, ...])
947
+ shift, scale = rearrange(
948
+ temb_scale_shift_table,
949
+ 'b l n c -> b n l c',
950
+ ).chunk(2, dim=1)
951
+ shift = shift.to(mcp_output.device).squeeze(1)
952
+ scale = scale.to(mcp_output.device).squeeze(1)
953
+ mcp_output = (
954
+ self.norm_out(mcp_output.float()) * (1. + scale) + shift
955
+ ).type_as(mcp_output)
956
+ mcp_output = self.proj_out(mcp_output)
957
+ mcp_output = rearrange(
958
+ mcp_output,
959
+ '1 (b l) (n c) -> b (l n) c',
960
+ n=math.prod(self.patch_size),
961
+ b=batch_size,
962
+ )
963
+ outputs.append(mcp_output)
964
+
965
+ return outputs
966
+
967
+ def forward_train(self, input_dict):
968
+ input_dict['latent_dict']['noisy_latents'] = input_dict['latent_dict']['noisy_latents'].to(torch.bfloat16)
969
+ input_dict['latent_dict']['latent'] = input_dict['latent_dict']['latent'].to(torch.bfloat16)
970
+ input_dict['action_dict']['noisy_latents'] = input_dict['action_dict']['noisy_latents'].to(torch.bfloat16)
971
+ input_dict['action_dict']['latent'] = input_dict['action_dict']['latent'].to(torch.bfloat16)
972
+ if self.enable_mcp:
973
+ for mcp_latent_dict in input_dict.get('mcp_latent_dicts', []):
974
+ mcp_latent_dict['noisy_latents'] = mcp_latent_dict[
975
+ 'noisy_latents'].to(torch.bfloat16)
976
+
977
+ latent_dict = input_dict['latent_dict']
978
+ action_dict = input_dict['action_dict']
979
+ batch_size = latent_dict['noisy_latents'].shape[0]
980
+
981
+ latent_hidden_states = self._input_embed(latent_dict['noisy_latents'], input_type='latent').flatten(0, 1)[None]
982
+ action_hidden_states = self._input_embed(action_dict['noisy_latents'], input_type='action').flatten(0, 1)[None]
983
+ text_hidden_states = self._input_embed(latent_dict["text_emb"], input_type='text')
984
+
985
+ text_hidden_states = text_hidden_states.flatten(0, 1)[None]
986
+
987
+ condition_latent_hidden_states = self._input_embed(latent_dict['latent'], input_type='latent').flatten(0, 1)[None]
988
+ condition_action_hidden_states = self._input_embed(action_dict['latent'], input_type='action').flatten(0, 1)[None]
989
+
990
+ hidden_states = torch.cat([latent_hidden_states,
991
+ condition_latent_hidden_states,
992
+ action_hidden_states,
993
+ condition_action_hidden_states], dim=1)
994
+
995
+
996
+ latent_grid_id = latent_dict['grid_id'].permute(1, 0, 2).flatten(1)[None]
997
+ action_grid_id = action_dict['grid_id'].permute(1, 0, 2).flatten(1)[None]
998
+ full_grid_id = torch.cat([latent_grid_id] * 2 + [action_grid_id] * 2, dim=2)
999
+
1000
+ rotary_emb = self.rope(full_grid_id)[:, :, None]
1001
+
1002
+ latent_time_steps = torch.cat(
1003
+ [latent_dict['timesteps'].flatten(0, 1), latent_dict['cond_timesteps'].flatten(0, 1)]
1004
+ )[None]
1005
+ action_time_steps = torch.cat(
1006
+ [action_dict['timesteps'].flatten(0, 1), action_dict['cond_timesteps'].flatten(0, 1)]
1007
+ )[None]
1008
+ latent_temb, latent_timestep_proj =self._time_embed(latent_time_steps,
1009
+ latent_dict['noisy_latents'].shape[-2],
1010
+ latent_dict['noisy_latents'].shape[-1],
1011
+ dtype=hidden_states.dtype,
1012
+ action_mode=False)
1013
+ action_temb, action_timestep_proj = self._time_embed(action_time_steps,
1014
+ action_dict['noisy_latents'].shape[-2],
1015
+ action_dict['noisy_latents'].shape[-1],
1016
+ dtype=hidden_states.dtype,
1017
+ action_mode=True)
1018
+ temb = torch.cat([latent_temb, action_temb], dim=1)
1019
+ timestep_proj = torch.cat([latent_timestep_proj, action_timestep_proj], dim=1)
1020
+
1021
+ total_length = hidden_states.shape[1]
1022
+ padded_length = (128 - total_length % 128) % 128
1023
+ hidden_states = F.pad(hidden_states, (0, 0, 0, padded_length))
1024
+ rotary_emb = F.pad(rotary_emb, (0, 0, 0, 0, 0, padded_length))
1025
+ temb = F.pad(temb, (0, 0, 0, padded_length))
1026
+ timestep_proj = F.pad(timestep_proj, (0, 0, 0, 0, 0, padded_length))
1027
+
1028
+ split_list = [latent_hidden_states.shape[1],
1029
+ condition_latent_hidden_states.shape[1],
1030
+ action_hidden_states.shape[1],
1031
+ condition_action_hidden_states.shape[1],
1032
+ padded_length]
1033
+
1034
+ FlexAttnFunc.init_mask(latent_dict['noisy_latents'].shape,
1035
+ action_dict['noisy_latents'].shape,
1036
+ padded_length,
1037
+ input_dict["chunk_size"],
1038
+ window_size=input_dict['window_size'],
1039
+ patch_size=self.patch_size,
1040
+ device=hidden_states.device
1041
+ )
1042
+
1043
+ collected_hidden_states = {}
1044
+ for layer_id, block in enumerate(self.blocks):
1045
+ hidden_states = block(hidden_states,
1046
+ text_hidden_states,
1047
+ timestep_proj,
1048
+ rotary_emb,
1049
+ update_cache=False)
1050
+ if self.enable_mcp and layer_id in self.mcp_hidden_collect_layers:
1051
+ collected_hidden_states[layer_id] = hidden_states
1052
+
1053
+ collected_hidden_states = [
1054
+ collected_hidden_states[layer_id]
1055
+ for layer_id in self.mcp_hidden_collect_layers
1056
+ if layer_id in collected_hidden_states
1057
+ ]
1058
+
1059
+ mcp_outputs = self._forward_mcp(
1060
+ input_dict=input_dict,
1061
+ hidden_states=hidden_states,
1062
+ collected_hidden_states=collected_hidden_states,
1063
+ text_hidden_states=text_hidden_states,
1064
+ latent_grid_id=latent_grid_id,
1065
+ action_grid_id=action_grid_id,
1066
+ latent_timestep_proj=latent_timestep_proj,
1067
+ action_timestep_proj=action_timestep_proj,
1068
+ split_list=split_list,
1069
+ batch_size=batch_size,
1070
+ )
1071
+ temb_scale_shift_table = self.scale_shift_table[None] + temb[:, :, None, ...]
1072
+ shift, scale = rearrange(temb_scale_shift_table,
1073
+ 'b l n c -> b n l c').chunk(2, dim=1)
1074
+ shift = shift.to(hidden_states.device).squeeze(1)
1075
+ scale = scale.to(hidden_states.device).squeeze(1)
1076
+ hidden_states = (self.norm_out(hidden_states.float()) *
1077
+ (1. + scale) +
1078
+ shift).type_as(hidden_states)
1079
+ latent_hidden_states, _, action_hidden_states, _, _ = torch.split(hidden_states, split_list, dim=1)
1080
+ latent_hidden_states = self.proj_out(latent_hidden_states)
1081
+ latent_hidden_states = rearrange(latent_hidden_states,
1082
+ '1 (b l) (n c) -> b (l n) c',
1083
+ n=math.prod(self.patch_size), b=batch_size) #
1084
+ action_hidden_states = self.action_proj_out(action_hidden_states)
1085
+ action_hidden_states = rearrange(action_hidden_states,
1086
+ '1 (b l) c -> b l c',
1087
+ b=batch_size) #
1088
+
1089
+ if self.enable_mcp:
1090
+ return latent_hidden_states, action_hidden_states, mcp_outputs
1091
+ return latent_hidden_states, action_hidden_states
1092
+
1093
+ def forward(
1094
+ self,
1095
+ input_dict,
1096
+ update_cache=0,
1097
+ cache_name="pos",
1098
+ action_mode=False,
1099
+ train_mode=False,
1100
+ ):
1101
+ r"""
1102
+ Forward pass through the diffusion model
1103
+
1104
+ Args:
1105
+ x (List[Tensor]):
1106
+ List of input video tensors, each with shape [C_in, F, H, W]
1107
+ t (Tensor):
1108
+ Diffusion timesteps tensor of shape [B]
1109
+ context (List[Tensor]):
1110
+ List of text embeddings each with shape [L, C]
1111
+ seq_len (`int`):
1112
+ Maximum sequence length for positional encoding
1113
+ y (List[Tensor], *optional*):
1114
+ Conditional video inputs for image-to-video mode, same shape as x
1115
+
1116
+ Returns:
1117
+ List[Tensor]:
1118
+ List of denoised video tensors with original input shapes [C_out, F, H / 8, W / 8]
1119
+ """
1120
+ if train_mode:
1121
+ return self.forward_train(input_dict)
1122
+ if action_mode: # action input emb
1123
+ latent_hidden_states = rearrange(input_dict['noisy_latents'],
1124
+ 'b c f h w -> b (f h w) c')
1125
+ latent_hidden_states = self.action_embedder(
1126
+ latent_hidden_states) # B L1 C
1127
+ else: # latent input emb
1128
+ latent_hidden_states = rearrange(
1129
+ input_dict['noisy_latents'],
1130
+ 'b c (f p1) (h p2) (w p3) -> b (f h w) (c p1 p2 p3)',
1131
+ p1=self.patch_size[0],
1132
+ p2=self.patch_size[1],
1133
+ p3=self.patch_size[2])
1134
+ latent_hidden_states = self.patch_embedding_mlp(
1135
+ latent_hidden_states)
1136
+ text_hidden_states = self.condition_embedder.text_embedder(
1137
+ input_dict["text_emb"]) # B L2 C
1138
+
1139
+ latent_grid_id = input_dict['grid_id']
1140
+ rotary_emb = self.rope(latent_grid_id)[:, :, None] # 1 L 1 C
1141
+ pach_scale_h, pach_scale_w = (1, 1) if action_mode else (
1142
+ self.patch_size[1], self.patch_size[2])
1143
+
1144
+ latent_time_steps = torch.repeat_interleave(
1145
+ input_dict['timesteps'],
1146
+ (input_dict['noisy_latents'].shape[-2] // pach_scale_h) *
1147
+ (input_dict['noisy_latents'].shape[-1] // pach_scale_w), dim=1) # L
1148
+ current_condition_embedder = self.condition_embedder_action if action_mode else self.condition_embedder
1149
+ temb, timestep_proj = current_condition_embedder(
1150
+ latent_time_steps, dtype=latent_hidden_states.dtype)
1151
+ timestep_proj = timestep_proj.unflatten(2, (6, -1)) # B L 6 C
1152
+
1153
+ for block in self.blocks:
1154
+ latent_hidden_states = block(latent_hidden_states,
1155
+ text_hidden_states,
1156
+ timestep_proj,
1157
+ rotary_emb,
1158
+ update_cache=update_cache,
1159
+ cache_name=cache_name)
1160
+ temb_scale_shift_table = self.scale_shift_table[None] + temb[:, :, None, ...]
1161
+ shift, scale = rearrange(temb_scale_shift_table,
1162
+ 'b l n c -> b n l c').chunk(2, dim=1)
1163
+ shift = shift.to(latent_hidden_states.device).squeeze(1)
1164
+ scale = scale.to(latent_hidden_states.device).squeeze(1)
1165
+ latent_hidden_states = (self.norm_out(latent_hidden_states.float()) *
1166
+ (1. + scale) +
1167
+ shift).type_as(latent_hidden_states)
1168
+
1169
+ if action_mode:
1170
+ latent_hidden_states = self.action_proj_out(latent_hidden_states)
1171
+ else:
1172
+ latent_hidden_states = self.proj_out(latent_hidden_states)
1173
+ latent_hidden_states = rearrange(latent_hidden_states,
1174
+ 'b l (n c) -> b (l n) c',
1175
+ n=math.prod(self.patch_size)) #
1176
+
1177
+ return latent_hidden_states
1178
+
1179
+
1180
+ if __name__ == '__main__':
1181
+ model = WanTransformer3DModel(patch_size=[1, 2, 2],
1182
+ num_attention_heads=24,
1183
+ attention_head_dim=128,
1184
+ in_channels=48,
1185
+ out_channels=48,
1186
+ action_dim=30,
1187
+ text_dim=4096,
1188
+ freq_dim=256,
1189
+ ffn_dim=14336,
1190
+ num_layers=30,
1191
+ cross_attn_norm=True,
1192
+ eps=1e-6,
1193
+ rope_max_seq_len=1024,
1194
+ pos_embed_seq_len=None,
1195
+ attn_mode="torch")
1196
+ print(model)
wan_va/modules/utils.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import torch
3
+ from diffusers import AutoencoderKLWan
4
+ from transformers import (
5
+ T5TokenizerFast,
6
+ UMT5EncoderModel,
7
+ )
8
+
9
+ from .model import WanTransformer3DModel
10
+
11
+
12
+ def load_vae(
13
+ vae_path,
14
+ torch_dtype,
15
+ torch_device,
16
+ ):
17
+ vae = AutoencoderKLWan.from_pretrained(
18
+ vae_path,
19
+ torch_dtype=torch_dtype,
20
+ )
21
+ return vae.to(torch_device)
22
+
23
+
24
+ def load_text_encoder(
25
+ text_encoder_path,
26
+ torch_dtype,
27
+ torch_device,
28
+ ):
29
+ text_encoder = UMT5EncoderModel.from_pretrained(
30
+ text_encoder_path,
31
+ torch_dtype=torch_dtype,
32
+ )
33
+ return text_encoder.to(torch_device)
34
+
35
+
36
+ def load_tokenizer(tokenizer_path, ):
37
+ tokenizer = T5TokenizerFast.from_pretrained(tokenizer_path, )
38
+ return tokenizer
39
+
40
+
41
+ def load_transformer(
42
+ transformer_path,
43
+ torch_dtype,
44
+ torch_device,
45
+ disable_mcp=False,
46
+ **kwargs
47
+ ):
48
+ model = WanTransformer3DModel.from_pretrained(
49
+ transformer_path,
50
+ torch_dtype=torch_dtype,
51
+ **kwargs
52
+ )
53
+ if disable_mcp and getattr(model, 'enable_mcp', False):
54
+ model.disable_mcp_modules()
55
+ return model.to(torch_device)
56
+
57
+
58
+ def patchify(x, patch_size):
59
+ if patch_size is None or patch_size == 1:
60
+ return x
61
+ batch_size, channels, frames, height, width = x.shape
62
+ x = x.view(batch_size, channels, frames, height // patch_size, patch_size,
63
+ width // patch_size, patch_size)
64
+ x = x.permute(0, 1, 6, 4, 2, 3, 5).contiguous()
65
+ x = x.view(batch_size, channels * patch_size * patch_size, frames,
66
+ height // patch_size, width // patch_size)
67
+ return x
68
+
69
+
70
+ class WanVAEStreamingWrapper:
71
+
72
+ def __init__(self, vae_model):
73
+ self.vae = vae_model
74
+ self.encoder = vae_model.encoder
75
+ self.quant_conv = vae_model.quant_conv
76
+
77
+ if hasattr(self.vae, "_cached_conv_counts"):
78
+ self.enc_conv_num = self.vae._cached_conv_counts["encoder"]
79
+ else:
80
+ count = 0
81
+ for m in self.encoder.modules():
82
+ if m.__class__.__name__ == "WanCausalConv3d":
83
+ count += 1
84
+ self.enc_conv_num = count
85
+
86
+ self.clear_cache()
87
+
88
+ def clear_cache(self):
89
+ self.feat_cache = [None] * self.enc_conv_num
90
+
91
+ def encode_chunk(self, x_chunk):
92
+ if hasattr(self.vae.config,
93
+ "patch_size") and self.vae.config.patch_size is not None:
94
+ x_chunk = patchify(x_chunk, self.vae.config.patch_size)
95
+ feat_idx = [0]
96
+ out = self.encoder(x_chunk,
97
+ feat_cache=self.feat_cache,
98
+ feat_idx=feat_idx)
99
+ enc = self.quant_conv(out)
100
+ return enc
wan_va/train.py ADDED
@@ -0,0 +1,822 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import argparse
3
+ import os
4
+ from pathlib import Path
5
+ import wandb
6
+
7
+ import torch
8
+ import torch.distributed as dist
9
+ import torch.nn.functional as F
10
+ from torch.utils.data import DataLoader, DistributedSampler
11
+ from tqdm import tqdm
12
+ from torch.distributed.checkpoint.state_dict import (
13
+ get_model_state_dict,
14
+ get_optimizer_state_dict,
15
+ set_optimizer_state_dict,
16
+ StateDictOptions,
17
+ )
18
+ from safetensors.torch import save_file, load_file
19
+ import json
20
+
21
+ from .configs import VA_CONFIGS
22
+ from .distributed.fsdp import shard_model, apply_ac
23
+ from .distributed.util import (
24
+ _configure_model,
25
+ init_distributed,
26
+ dist_mean,
27
+ dist_max
28
+ )
29
+ from einops import rearrange
30
+ from .modules.utils import (
31
+ load_transformer,
32
+ )
33
+ from .utils import (
34
+ init_logger,
35
+ logger,
36
+ get_mesh_id,
37
+ sample_timestep_id,
38
+ data_seq_to_patch,
39
+ warmup_constant_lambda,
40
+ FlowMatchScheduler
41
+ )
42
+
43
+ from .dataset import MultiLatentLeRobotDataset, dataset_indexes_ready
44
+ from .mcp import shift_latents_for_mcp, validate_mcp_settings
45
+ import gc
46
+
47
+
48
+ class Trainer:
49
+ def __init__(self, config):
50
+ if config.enable_wandb and config.rank == 0:
51
+ wandb.login(host=os.environ['WANDB_BASE_URL'], key=os.environ['WANDB_API_KEY'])
52
+ self.wandb = wandb
53
+ self.wandb.init(
54
+ entity=os.environ["WANDB_TEAM_NAME"],
55
+ project=os.getenv("WANDB_PROJECT", "va_robotwin"),
56
+ # dir=log_dir,
57
+ config=config,
58
+ mode="online",
59
+ name='test_lln'
60
+ # name=os.path.basename(os.path.normpath(job_config.job.dump_folder))
61
+ )
62
+ logger.info("WandB logging enabled")
63
+ self.step = 0
64
+ self.config = config
65
+ self.device = torch.device(f"cuda:{config.local_rank}")
66
+ self.dtype = config.param_dtype
67
+ self.patch_size = config.patch_size
68
+ self.enable_mcp = getattr(config, 'enable_mcp', True)
69
+
70
+ if self.enable_mcp:
71
+ validate_mcp_settings(
72
+ num_mcp_depths=config.num_mcp_depths,
73
+ mcp_blocks_per_depth=config.mcp_blocks_per_depth,
74
+ mcp_hidden_collect_layers=config.mcp_hidden_collect_layers,
75
+ mcp_loss_weights=config.mcp_loss_weights,
76
+ )
77
+
78
+ # Load models
79
+ logger.info("Loading models...")
80
+
81
+ # Load and shard transformer with FSDP
82
+ logger.info("Loading transformer...")
83
+
84
+ if hasattr(config, 'resume_from') and config.resume_from:
85
+ transformer_path = os.path.join(config.resume_from, 'transformer')
86
+ if config.rank == 0:
87
+ logger.info(f"Resuming from checkpoint: {transformer_path}")
88
+ else:
89
+ transformer_path = os.path.join(config.wan22_pretrained_model_name_or_path, 'transformer')
90
+
91
+ self.transformer = load_transformer(
92
+ transformer_path,
93
+ torch_dtype=torch.float32,
94
+ torch_device='cpu',
95
+ attn_mode="flex",
96
+ disable_mcp=not self.enable_mcp,
97
+ )
98
+
99
+ if self.enable_mcp:
100
+ validate_mcp_settings(
101
+ num_mcp_depths=config.num_mcp_depths,
102
+ mcp_blocks_per_depth=config.mcp_blocks_per_depth,
103
+ mcp_hidden_collect_layers=config.mcp_hidden_collect_layers,
104
+ mcp_loss_weights=config.mcp_loss_weights,
105
+ num_layers=len(self.transformer.blocks),
106
+ )
107
+ initialized = self.transformer.enable_mcp_training(
108
+ num_mcp_depths=config.num_mcp_depths,
109
+ mcp_blocks_per_depth=config.mcp_blocks_per_depth,
110
+ mcp_hidden_collect_layers=config.mcp_hidden_collect_layers,
111
+ init_from_backbone=config.mcp_init_from_backbone,
112
+ )
113
+ if initialized:
114
+ total_mcp_blocks = (
115
+ config.num_mcp_depths * config.mcp_blocks_per_depth)
116
+ if config.mcp_init_from_backbone:
117
+ num_backbone_blocks = len(self.transformer.blocks)
118
+ source_start = (
119
+ num_backbone_blocks - config.mcp_blocks_per_depth)
120
+ logger.info(
121
+ f"Initializing {config.num_mcp_depths} MCP depths "
122
+ f"({config.mcp_blocks_per_depth} blocks per depth) "
123
+ f"from backbone blocks[{source_start}:"
124
+ f"{num_backbone_blocks}]"
125
+ )
126
+ for depth in range(config.num_mcp_depths):
127
+ logger.info(
128
+ f" MCP depth {depth + 1}: initialized from "
129
+ f"backbone blocks[{source_start}:"
130
+ f"{num_backbone_blocks}]"
131
+ )
132
+ logger.info(
133
+ f"Initialized {config.num_mcp_depths} x "
134
+ f"{config.mcp_blocks_per_depth} = "
135
+ f"{total_mcp_blocks} MCP blocks"
136
+ )
137
+ else:
138
+ logger.info(
139
+ f"Initialized {total_mcp_blocks} MCP blocks from "
140
+ "scratch"
141
+ )
142
+ logger.info(
143
+ "MCP hidden-state collection uses backbone indices "
144
+ f"{list(config.mcp_hidden_collect_layers)}"
145
+ )
146
+ logger.info(
147
+ "Initialized MCP hidden fuser and input projections "
148
+ "from scratch"
149
+ )
150
+ else:
151
+ logger.info(
152
+ f"Loaded MCP modules from checkpoint: "
153
+ f"{config.num_mcp_depths} depths x "
154
+ f"{config.mcp_blocks_per_depth} blocks per depth"
155
+ )
156
+
157
+ logger.info("Setting up activation checkpointing ...")
158
+ apply_ac(self.transformer)
159
+
160
+ logger.info("Setting up FSDP...")
161
+ shard_fn = shard_model
162
+ self.transformer = _configure_model(
163
+ model=self.transformer,
164
+ shard_fn=shard_fn,
165
+ param_dtype=self.dtype,
166
+ device=self.device,
167
+ eval_mode=False,
168
+ )
169
+ self.transformer.train()
170
+ self.transformer.requires_grad_(True)
171
+
172
+ # Optimizer
173
+ self.optimizer = torch.optim.AdamW(
174
+ [p for p in self.transformer.parameters() if p.requires_grad],
175
+ lr=config.learning_rate,
176
+ betas=(config.beta1, config.beta2),
177
+ eps=1e-8,
178
+ weight_decay=config.weight_decay,
179
+ fused=True,
180
+ foreach=False,
181
+ )
182
+
183
+ self.lr_scheduler = torch.optim.lr_scheduler.LambdaLR(self.optimizer,
184
+ lr_lambda=lambda step: warmup_constant_lambda(step, warmup_steps=config.warmup_steps))
185
+
186
+ # Setup dataloaders
187
+ logger.info("Setting up datasets...")
188
+ cache_ready = False
189
+ if (
190
+ config.world_size > 1
191
+ and getattr(config, 'enable_dataset_index_cache', True)
192
+ ):
193
+ cache_ready_flag = torch.zeros(1, device=self.device, dtype=torch.int)
194
+ if config.rank == 0:
195
+ cache_ready_flag.fill_(int(dataset_indexes_ready(config)))
196
+ dist.broadcast(cache_ready_flag, src=0)
197
+ cache_ready = bool(cache_ready_flag.item())
198
+
199
+ use_rank_zero_indexing = (
200
+ config.world_size > 1
201
+ and getattr(config, 'enable_dataset_index_cache', True)
202
+ and not cache_ready
203
+ )
204
+ if config.rank == 0 and cache_ready:
205
+ logger.info(
206
+ "Dataset index and Arrow caches are complete; "
207
+ "loading all ranks concurrently"
208
+ )
209
+ if use_rank_zero_indexing and config.rank != 0:
210
+ dist.barrier()
211
+ # Rank 0 has completed any requested rebuild at this point.
212
+ config.rebuild_dataset_index_cache = False
213
+
214
+ train_dataset = MultiLatentLeRobotDataset(config=config)
215
+
216
+ if use_rank_zero_indexing:
217
+ if config.rank == 0:
218
+ dist.barrier()
219
+ dist.barrier()
220
+
221
+ if config.rank == 0:
222
+ logger.info(
223
+ "Dataset ready: %d samples from %d datasets "
224
+ "(%d index cache hits, %d direct Arrow loads, %d rebuilt)",
225
+ len(train_dataset),
226
+ len(train_dataset._datasets),
227
+ train_dataset.index_cache_hits,
228
+ train_dataset.hf_cache_hits,
229
+ train_dataset.index_cache_misses,
230
+ )
231
+ train_sampler = DistributedSampler(
232
+ train_dataset,
233
+ num_replicas=config.world_size,
234
+ rank=config.rank,
235
+ shuffle=True,
236
+ seed=42
237
+ ) if config.world_size > 1 else None
238
+ self.train_loader = DataLoader(
239
+ train_dataset,
240
+ batch_size=config.batch_size,
241
+ shuffle=(train_sampler is None),
242
+ num_workers=config.load_worker,
243
+ sampler=train_sampler,
244
+ )
245
+
246
+ self.train_scheduler_latent = FlowMatchScheduler(shift=self.config.snr_shift, sigma_min=0.0, extra_one_step=True)
247
+ self.train_scheduler_latent.set_timesteps(1000, training=True)
248
+ self.train_scheduler_action = FlowMatchScheduler(shift=self.config.action_snr_shift, sigma_min=0.0, extra_one_step=True)
249
+ self.train_scheduler_action.set_timesteps(1000, training=True)
250
+ self.train_scheduler_mcp = None
251
+ if self.enable_mcp:
252
+ self.train_scheduler_mcp = FlowMatchScheduler(
253
+ shift=self.config.mcp_snr_shift,
254
+ sigma_min=0.0,
255
+ extra_one_step=True,
256
+ )
257
+ self.train_scheduler_mcp.set_timesteps(1000, training=True)
258
+
259
+ self.save_dir = Path(config.save_root) / "checkpoints"
260
+ self.save_dir.mkdir(parents=True, exist_ok=True)
261
+
262
+ self.gradient_accumulation_steps = getattr(config, 'gradient_accumulation_steps', 1)
263
+ self.train_loader_iter = None
264
+ # if hasattr(config, 'resume_from') and config.resume_from:
265
+ # self._load_training_state(config.resume_from)
266
+
267
+ def _get_next_batch(self):
268
+ """Get next batch from iterator, reset if epoch is finished."""
269
+ if self.train_loader_iter is None:
270
+ self.train_loader_iter = iter(self.train_loader)
271
+
272
+ try:
273
+ batch = next(self.train_loader_iter)
274
+ except StopIteration:
275
+ # Reset sampler and iterator when epoch finishes
276
+ if hasattr(self.train_loader.sampler, 'set_epoch'):
277
+ self.train_loader.sampler.set_epoch(self.train_loader.sampler.epoch + 1)
278
+ self.train_loader_iter = iter(self.train_loader)
279
+ batch = next(self.train_loader_iter)
280
+
281
+ return batch
282
+
283
+ @torch.no_grad()
284
+ def _add_noise(self, latent, train_scheduler, action_mask=False,
285
+ action_mode=False, noisy_cond_prob=0., frame_shift=0):
286
+ B, C, F, H, W = latent.shape
287
+
288
+ timestep_ids = sample_timestep_id(batch_size=F, num_train_timesteps=train_scheduler.num_train_timesteps)
289
+ noise = torch.zeros_like(latent).normal_()
290
+ timesteps = train_scheduler.timesteps[timestep_ids].to(device=self.device)
291
+ noisy_latents =train_scheduler.add_noise(latent, noise, timesteps, t_dim=2)
292
+ targets =train_scheduler.training_target(latent, noise, timesteps)
293
+
294
+ patch_f, patch_h, patch_w = self.patch_size
295
+ if action_mode:
296
+ patch_f = patch_h = patch_w = 1
297
+
298
+ latent_grid_id = get_mesh_id(
299
+ latent.shape[-3] // patch_f, # F
300
+ latent.shape[-2] // patch_h, # H
301
+ latent.shape[-1] // patch_w, # W
302
+ t=1 if action_mode else 0, # 1 for action mode (0 for latent), not used
303
+ f_w=1,
304
+ f_shift=frame_shift,
305
+ action=action_mode
306
+ ).to(self.device) # shape: [4, seq_len]
307
+ latent_grid_id = latent_grid_id[None].repeat(B, 1, 1)
308
+
309
+ if torch.rand(1).item() < noisy_cond_prob:
310
+ cond_timestep_ids = sample_timestep_id(
311
+ batch_size=F,
312
+ min_timestep_bd=0.5,
313
+ max_timestep_bd=1.0,
314
+ num_train_timesteps=train_scheduler.num_train_timesteps,
315
+ )
316
+ noise = torch.zeros_like(latent).normal_()
317
+ cond_timesteps = train_scheduler.timesteps[cond_timestep_ids].to(device=self.device)
318
+ latent = train_scheduler.add_noise(latent, noise, cond_timesteps, t_dim=2)
319
+ else:
320
+ cond_timesteps = torch.zeros_like(timesteps)
321
+
322
+ if action_mask is not None:
323
+ noisy_latents *= action_mask.float()
324
+ targets *= action_mask.float()
325
+ latent *= action_mask.float()
326
+
327
+ return dict(
328
+ timesteps=timesteps[None].repeat(B, 1),
329
+ noisy_latents=noisy_latents,
330
+ targets=targets,
331
+ latent=latent,
332
+ cond_timesteps=cond_timesteps[None].repeat(B, 1),
333
+ grid_id=latent_grid_id,
334
+ )
335
+
336
+ @torch.no_grad()
337
+ def _prepare_input_dict(self, batch_dict):
338
+ """Prepare input dict following infer code pattern from wan_va_server.py."""
339
+ chunk_size = torch.randint(1, 5, (1,)).item()
340
+ # Generate grid_id following infer code (no batch dimension yet)
341
+ # For action mode: get_mesh_id(shape[-3], shape[-2], shape[-1], t=1, f_w=1, f_shift, action=True)
342
+ latent_dict = self._add_noise(
343
+ latent=batch_dict['latents'],
344
+ train_scheduler=self.train_scheduler_latent,
345
+ action_mask=None,
346
+ action_mode=False,
347
+ noisy_cond_prob=0.5)
348
+
349
+ action_dict = self._add_noise(
350
+ latent=batch_dict['actions'],
351
+ train_scheduler=self.train_scheduler_action,
352
+ action_mask=batch_dict['actions_mask'],
353
+ action_mode=True,
354
+ noisy_cond_prob=0.0)
355
+
356
+ latent_dict['text_emb'] = batch_dict['text_emb']
357
+ action_dict['text_emb'] = batch_dict['text_emb']
358
+ action_dict['actions_mask'] = batch_dict['actions_mask']
359
+
360
+ input_dict = {
361
+ 'latent_dict': latent_dict,
362
+ 'action_dict': action_dict,
363
+ 'chunk_size': chunk_size,
364
+ 'window_size': torch.randint(4, 65, (1,)).item(),
365
+ }
366
+ if self.enable_mcp:
367
+ mcp_latent_dicts = []
368
+ for depth in range(self.config.num_mcp_depths):
369
+ frame_shift = (depth + 1) * chunk_size
370
+ shifted_latents, valid_mask = shift_latents_for_mcp(
371
+ batch_dict['latents'], frame_shift)
372
+ mcp_latent_dict = self._add_noise(
373
+ latent=shifted_latents,
374
+ train_scheduler=self.train_scheduler_mcp,
375
+ action_mask=None,
376
+ action_mode=False,
377
+ noisy_cond_prob=0.0,
378
+ frame_shift=frame_shift,
379
+ )
380
+ mcp_latent_dict.pop('latent')
381
+ mcp_latent_dict.pop('cond_timesteps')
382
+ mcp_latent_dict['valid_mask'] = valid_mask
383
+ mcp_latent_dicts.append(mcp_latent_dict)
384
+ input_dict['mcp_latent_dicts'] = mcp_latent_dicts
385
+ return input_dict
386
+
387
+ def convert_input_format(self, input_dict):
388
+ """Convert input dict to match transformer input format if needed."""
389
+ for key, value in input_dict.items():
390
+ input_dict[key] = value.to(self.device)#.to(self.dtype)
391
+ return input_dict
392
+
393
+ def compute_loss(self,
394
+ input_dict,
395
+ pred
396
+ ):
397
+ if self.enable_mcp:
398
+ latent_pred, action_pred, mcp_pred_list = pred
399
+ else:
400
+ latent_pred, action_pred = pred
401
+ mcp_pred_list = []
402
+ if self.enable_mcp and len(mcp_pred_list) != self.config.num_mcp_depths:
403
+ raise RuntimeError(
404
+ "MCP output depth count must match num_mcp_depths")
405
+ action_pred = rearrange(action_pred, 'b (f n) c -> b c f n 1', f=input_dict['action_dict']['targets'].shape[-3])
406
+ latent_pred = data_seq_to_patch(
407
+ self.patch_size, latent_pred,
408
+ input_dict['latent_dict']['targets'].shape[-3], input_dict['latent_dict']['targets'].shape[-2],
409
+ input_dict['latent_dict']['targets'].shape[-1], batch_size=latent_pred.shape[0])
410
+ Bn, Fn = input_dict['latent_dict']['timesteps'].shape
411
+ latent_loss_weight = self.train_scheduler_latent.training_weight(input_dict['latent_dict']['timesteps'].flatten()).reshape(Bn, Fn)
412
+ action_loss_weight = self.train_scheduler_action.training_weight(input_dict['action_dict']['timesteps'].flatten()).reshape(Bn, Fn)
413
+
414
+ # Frame-wise video loss calculation
415
+ latent_loss = F.mse_loss(latent_pred.float(), input_dict['latent_dict']['targets'].float().detach(), reduction='none')
416
+ latent_loss = latent_loss * latent_loss_weight[:, None, :, None, None]
417
+ # Permute to (B, F, H, W, C) and flatten to (B*F, H*W*C)
418
+ latent_loss = latent_loss.permute(0, 2, 3, 4, 1) # (B, C, F, H, W) -> (B, F, H, W, C)
419
+ latent_loss = latent_loss.flatten(0, 1).flatten(1) # (B, F, H, W, C) -> (B*F, H*W*C)
420
+ # Sum per frame and compute mask per frame
421
+ latent_loss_per_frame = latent_loss.sum(dim=1) # (B*F,)
422
+ latent_mask_per_frame = torch.ones_like(latent_loss).sum(dim=1) # (B*F,)
423
+ latent_loss = (latent_loss_per_frame / (latent_mask_per_frame + 1e-6)).mean()
424
+
425
+ # Frame-wise action loss calculation
426
+ action_loss = F.mse_loss(action_pred.float(), input_dict['action_dict']['targets'].float().detach(), reduction='none')
427
+ action_loss = action_loss * action_loss_weight[:, None, :, None, None]
428
+ action_loss = action_loss * input_dict['action_dict']['actions_mask'].float()
429
+ # Permute to (B, F, H, W, C) and flatten to (B*F, H*W*C)
430
+ action_loss = action_loss.permute(0, 2, 3, 4, 1) # (B, C, F, H, W) -> (B, F, H, W, C)
431
+ action_mask = input_dict['action_dict']['actions_mask'].float().permute(0, 2, 3, 4, 1) # (B, C, F, H, W) -> (B, F, H, W, C)
432
+ action_loss = action_loss.flatten(0, 1).flatten(1) # (B, F, H, W, C) -> (B*F, H*W*C)
433
+ action_mask = action_mask.flatten(0, 1).flatten(1) # (B, F, H, W, C) -> (B*F, H*W*C)
434
+ # Sum per frame and normalize by mask per frame
435
+ action_loss_per_frame = action_loss.sum(dim=1) # (B*F,)
436
+ action_mask_per_frame = action_mask.sum(dim=1) # (B*F,)
437
+ action_loss = (action_loss_per_frame / (action_mask_per_frame + 1e-6)).mean()
438
+
439
+ mcp_losses = []
440
+ for mcp_pred, mcp_latent_dict in zip(
441
+ mcp_pred_list, input_dict.get('mcp_latent_dicts', [])):
442
+ mcp_pred = data_seq_to_patch(
443
+ self.patch_size,
444
+ mcp_pred,
445
+ mcp_latent_dict['targets'].shape[-3],
446
+ mcp_latent_dict['targets'].shape[-2],
447
+ mcp_latent_dict['targets'].shape[-1],
448
+ batch_size=mcp_pred.shape[0],
449
+ )
450
+ mcp_batch_size, mcp_num_frames = mcp_latent_dict[
451
+ 'timesteps'].shape
452
+ mcp_loss_weight = self.train_scheduler_mcp.training_weight(
453
+ mcp_latent_dict['timesteps'].flatten()).reshape(
454
+ mcp_batch_size, mcp_num_frames)
455
+ mcp_loss = F.mse_loss(
456
+ mcp_pred.float(),
457
+ mcp_latent_dict['targets'].float().detach(),
458
+ reduction='none',
459
+ )
460
+ mcp_loss = mcp_loss * mcp_loss_weight[:, None, :, None, None]
461
+ valid_mask = mcp_latent_dict['valid_mask'].to(
462
+ device=mcp_loss.device, dtype=mcp_loss.dtype)
463
+ valid_count = valid_mask.expand_as(mcp_loss).sum()
464
+ mcp_loss = (mcp_loss * valid_mask).sum() / valid_count.clamp_min(1.)
465
+ mcp_losses.append(mcp_loss / self.gradient_accumulation_steps)
466
+
467
+ return (
468
+ latent_loss / self.gradient_accumulation_steps,
469
+ action_loss / self.gradient_accumulation_steps,
470
+ mcp_losses,
471
+ )
472
+
473
+ def _train_step(self, batch, batch_idx):
474
+ """Train a single batch, returns losses for logging."""
475
+ batch = self.convert_input_format(batch)
476
+ input_dict = self._prepare_input_dict(batch)
477
+
478
+ should_sync = (batch_idx + 1) % self.gradient_accumulation_steps == 0
479
+
480
+ if not should_sync:
481
+ self.transformer.set_requires_gradient_sync(False)
482
+ else:
483
+ self.transformer.set_requires_gradient_sync(True)
484
+
485
+ output = self.transformer(input_dict, train_mode=True)
486
+ latent_loss, action_loss, mcp_losses = self.compute_loss(
487
+ input_dict, output)
488
+ mcp_loss = sum(
489
+ weight * depth_loss
490
+ for weight, depth_loss in zip(
491
+ self.config.mcp_loss_weights, mcp_losses)
492
+ ) if mcp_losses else latent_loss.new_zeros(())
493
+ loss = latent_loss + action_loss + mcp_loss
494
+
495
+ loss.backward()
496
+
497
+ losses = {
498
+ 'latent_loss': latent_loss.detach(),
499
+ 'action_loss': action_loss.detach(),
500
+ 'mcp_losses': [depth_loss.detach() for depth_loss in mcp_losses],
501
+ 'mcp_loss': mcp_loss.detach(),
502
+ }
503
+
504
+ # Only update weights after accumulating gradients
505
+ if should_sync:
506
+ total_norm = torch.nn.utils.clip_grad_norm_(self.transformer.parameters(), 2.0)
507
+ self.optimizer.step()
508
+ self.lr_scheduler.step()
509
+ self.optimizer.zero_grad()
510
+
511
+ losses['total_norm'] = total_norm
512
+ losses['should_log'] = True
513
+ else:
514
+ losses['should_log'] = False
515
+
516
+ return losses
517
+
518
+ def save_checkpoint(self,):
519
+ """Save model checkpoint in the same format as pretrained model."""
520
+ try:
521
+ state_dict = get_model_state_dict(
522
+ self.transformer,
523
+ options=StateDictOptions(full_state_dict=True, cpu_offload=True),
524
+ )
525
+ state_dict_bf16 = {k: v.to(torch.bfloat16) for k, v in state_dict.items()}
526
+ # optim_state = get_optimizer_state_dict(
527
+ # self.transformer, self.optimizer,
528
+ # options=StateDictOptions(full_state_dict=True, cpu_offload=True),
529
+ # )
530
+
531
+ # Only rank 0 saves the checkpoint
532
+ if self.config.rank == 0:
533
+ checkpoint_dir = self.save_dir / f"checkpoint_step_{self.step}"
534
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
535
+
536
+ # Save transformer in the same format as pretrained model
537
+ transformer_dir = checkpoint_dir / "transformer"
538
+ transformer_dir.mkdir(parents=True, exist_ok=True)
539
+
540
+ logger.info(f"Saving transformer to {transformer_dir}")
541
+
542
+ # Manually save in diffusers format (outside FSDP context to avoid deadlock)
543
+ # Save model weights
544
+ model_file = transformer_dir / "diffusion_pytorch_model.safetensors"
545
+ save_file(state_dict_bf16, model_file)
546
+
547
+ # Save config (copy from original transformer config and update _name_or_path)
548
+ config_file = transformer_dir / "config.json"
549
+ config_dict = dict(self.transformer.config)
550
+ config_dict.pop('_name_or_path', None)
551
+ with open(config_file, 'w') as f:
552
+ json.dump(config_dict, f, indent=2)
553
+
554
+ # # Save optimizer state and training metadata in PyTorch format
555
+ # training_state_path = checkpoint_dir / "training_state.pt"
556
+ # logger.info(f"Saving training state to {training_state_path}")
557
+ # torch.save({
558
+ # 'step': self.step,
559
+ # 'optimizer_state_dict': optim_state,
560
+ # 'config': vars(self.config),
561
+ # }, training_state_path)
562
+
563
+ logger.info(f"Checkpoint saved successfully at step {self.step}")
564
+
565
+ # Synchronize all processes after saving
566
+ if dist.is_initialized():
567
+ dist.barrier()
568
+
569
+ except Exception as e:
570
+ if self.config.rank == 0:
571
+ logger.error(f"Failed to save checkpoint: {e}")
572
+ import traceback
573
+ logger.error(traceback.format_exc())
574
+ # Ensure all processes stay synchronized even on error
575
+ if dist.is_initialized():
576
+ dist.barrier()
577
+
578
+ def _load_training_state(self, checkpoint_path):
579
+ """Load training state (optimizer + step) after FSDP and optimizer creation."""
580
+ checkpoint_dir = Path(checkpoint_path)
581
+ training_state_path = checkpoint_dir / "training_state.pt"
582
+
583
+ if not training_state_path.exists():
584
+ if self.config.rank == 0:
585
+ logger.warning(f"Training state not found: {training_state_path}, starting from step 0")
586
+ return
587
+
588
+ if self.config.rank == 0:
589
+ logger.info(f"Loading training state from {training_state_path}")
590
+
591
+ # All ranks load the training state directly
592
+ training_state = torch.load(training_state_path, map_location='cpu', weights_only=False)
593
+
594
+ # All ranks load optimizer state (required for FSDP)
595
+ set_optimizer_state_dict(
596
+ self.transformer, self.optimizer,
597
+ optim_state_dict=training_state['optimizer_state_dict'],
598
+ options=StateDictOptions(full_state_dict=True, strict=False)
599
+ )
600
+ self.step = training_state.get('step', 0)
601
+
602
+ if self.config.rank == 0:
603
+ logger.info(f"Training state loaded, resuming from step {self.step}")
604
+
605
+ # Synchronize all ranks
606
+ if dist.is_initialized():
607
+ dist.barrier()
608
+
609
+ def train(self):
610
+ """Main training loop - train by steps instead of epochs."""
611
+ logger.info(f"Starting training for {self.config.num_steps} steps...")
612
+ self.transformer.train()
613
+
614
+ progress_bar = tqdm(
615
+ total=self.config.num_steps,
616
+ desc="Training",
617
+ disable=(self.config.rank != 0),
618
+ leave=True,
619
+ dynamic_ncols=True,
620
+ initial=self.step
621
+ )
622
+
623
+ self.optimizer.zero_grad()
624
+ accumulated_latent_losses = []
625
+ accumulated_action_losses = []
626
+ accumulated_mcp_losses = [
627
+ [] for _ in range(self.config.num_mcp_depths)
628
+ ] if self.enable_mcp else []
629
+ accumulated_mcp_total_losses = []
630
+ step_in_accumulation = 0
631
+
632
+ while self.step < self.config.num_steps:
633
+ # Get next batch (handles epoch reset automatically)
634
+ batch = self._get_next_batch()
635
+
636
+ losses = self._train_step(batch, step_in_accumulation)
637
+
638
+ # Accumulate losses for logging
639
+ accumulated_latent_losses.append(losses['latent_loss'])
640
+ accumulated_action_losses.append(losses['action_loss'])
641
+ for depth, mcp_loss in enumerate(losses['mcp_losses']):
642
+ accumulated_mcp_losses[depth].append(mcp_loss)
643
+ accumulated_mcp_total_losses.append(losses['mcp_loss'])
644
+ step_in_accumulation += 1
645
+
646
+ # Log and checkpoint when optimizer steps
647
+ if losses['should_log']:
648
+ lr = self.lr_scheduler.get_last_lr()[0]
649
+
650
+ # Average accumulated losses
651
+ latent_loss_show = dist_mean(torch.stack(accumulated_latent_losses).sum()).detach().cpu().item()
652
+ action_loss_show = dist_mean(torch.stack(accumulated_action_losses).sum()).detach().cpu().item()
653
+ max_latent_loss_show = dist_max(torch.stack(accumulated_latent_losses).sum()).detach().cpu().item()
654
+ max_action_loss_show = dist_max(torch.stack(accumulated_action_losses).sum()).detach().cpu().item()
655
+ mcp_loss_shows = [
656
+ dist_mean(torch.stack(depth_losses).sum()).detach().cpu().item()
657
+ for depth_losses in accumulated_mcp_losses
658
+ ]
659
+ mcp_total_loss_show = dist_mean(
660
+ torch.stack(accumulated_mcp_total_losses).sum()
661
+ ).detach().cpu().item()
662
+
663
+ # Clear accumulated losses
664
+ accumulated_latent_losses = []
665
+ accumulated_action_losses = []
666
+ accumulated_mcp_losses = [
667
+ [] for _ in range(self.config.num_mcp_depths)
668
+ ] if self.enable_mcp else []
669
+ accumulated_mcp_total_losses = []
670
+ step_in_accumulation = 0
671
+
672
+ torch.cuda.synchronize()
673
+ if self.step % self.config.gc_interval == 0:
674
+ torch.cuda.empty_cache()
675
+ gc.collect()
676
+
677
+ if self.config.rank == 0:
678
+ total_norm = losses['total_norm']
679
+ progress_bar.n += 1
680
+ postfix = {
681
+ 'latent_loss': f'{latent_loss_show:.4f}',
682
+ 'action_loss': f'{action_loss_show:.4f}',
683
+ 'step': self.step,
684
+ 'grad_norm': f'{total_norm.item():.2f}',
685
+ 'lr': f'{lr:.2e}'
686
+ }
687
+ if self.enable_mcp:
688
+ postfix['mcp_loss'] = f'{mcp_total_loss_show:.4f}'
689
+ progress_bar.set_postfix(postfix)
690
+ if self.config.enable_wandb:
691
+ log_values = {
692
+ 'loss_metrics/global_avg_video_loss': latent_loss_show,
693
+ 'loss_metrics/global_avg_action_loss': action_loss_show,
694
+ 'loss_metrics/global_max_video_loss': max_latent_loss_show,
695
+ 'loss_metrics/global_max_action_loss': max_action_loss_show,
696
+ 'grad_norm': total_norm.item(),
697
+ 'lr': lr,
698
+ }
699
+ if self.enable_mcp:
700
+ log_values['loss_metrics/mcp_weighted_total'] = (
701
+ mcp_total_loss_show)
702
+ for depth, mcp_loss_show in enumerate(
703
+ mcp_loss_shows):
704
+ log_values[
705
+ f'loss_metrics/mcp_depth_{depth + 1}'] = (
706
+ mcp_loss_show)
707
+ self.wandb.log(log_values, step=self.step)
708
+
709
+ self.step += 1
710
+
711
+ if self.step % self.config.save_interval == 0:
712
+ if self.config.rank == 0:
713
+ logger.info(f"Starting save model at step {self.step}")
714
+ self.save_checkpoint()
715
+
716
+ if dist.is_initialized():
717
+ dist.barrier()
718
+
719
+ progress_bar.close()
720
+ logger.info("Training completed!")
721
+
722
+
723
+ def run(args):
724
+ """Main entry point."""
725
+ config = VA_CONFIGS[args.config_name]
726
+
727
+ overrides = {
728
+ 'wan22_pretrained_model_name_or_path': args.pretrained_model_path,
729
+ 'dataset_path': args.dataset_path,
730
+ 'empty_emb_path': args.empty_emb_path,
731
+ 'learning_rate': args.learning_rate,
732
+ 'cfg_prob': args.cfg_prob,
733
+ 'init_worker': args.init_worker,
734
+ 'load_worker': args.load_worker,
735
+ 'batch_size': args.batch_size,
736
+ 'gradient_accumulation_steps': args.gradient_accumulation_steps,
737
+ 'num_steps': args.num_steps,
738
+ 'save_interval': args.save_interval,
739
+ 'save_root': args.save_root,
740
+ }
741
+ for key, value in overrides.items():
742
+ if value is not None:
743
+ config[key] = value
744
+
745
+ if args.dataset_path is not None and args.empty_emb_path is None:
746
+ config.empty_emb_path = os.path.join(args.dataset_path, 'empty_emb.pt')
747
+ if args.disable_wandb:
748
+ config.enable_wandb = False
749
+
750
+ rank = int(os.getenv("RANK", 0))
751
+ local_rank = int(os.environ.get('LOCAL_RANK', 0))
752
+ world_size = int(os.environ.get("WORLD_SIZE", 1))
753
+
754
+ init_distributed(world_size, local_rank, rank)
755
+
756
+ config.rank = rank
757
+ config.local_rank = local_rank
758
+ config.world_size = world_size
759
+
760
+ if rank == 0:
761
+ logger.info(f"Using config: {args.config_name}")
762
+ logger.info(f"World size: {world_size}, Local rank: {local_rank}")
763
+
764
+ trainer = Trainer(config)
765
+ trainer.train()
766
+
767
+
768
+ def main():
769
+ """Parse arguments and run training."""
770
+ parser = argparse.ArgumentParser(description="Train WAN model for robotics")
771
+ parser.add_argument(
772
+ "--config-name",
773
+ type=str,
774
+ default='robotwin_train',
775
+ help="Config name",
776
+ )
777
+ parser.add_argument(
778
+ "--save-root",
779
+ type=str,
780
+ default=None,
781
+ help="Root directory for saving checkpoints",
782
+ )
783
+ parser.add_argument(
784
+ "--pretrained-model-path",
785
+ type=str,
786
+ default=None,
787
+ help="Pretrained model root containing the transformer directory",
788
+ )
789
+ parser.add_argument(
790
+ "--dataset-path",
791
+ type=str,
792
+ default=None,
793
+ help="Root directory containing open-format LeRobot datasets",
794
+ )
795
+ parser.add_argument(
796
+ "--empty-emb-path",
797
+ type=str,
798
+ default=None,
799
+ help="Path to empty_emb.pt (defaults to DATASET_PATH/empty_emb.pt)",
800
+ )
801
+ parser.add_argument(
802
+ "--disable-wandb",
803
+ action="store_true",
804
+ help="Disable Weights & Biases logging",
805
+ )
806
+ parser.add_argument("--learning-rate", type=float, default=None)
807
+ parser.add_argument("--cfg-prob", type=float, default=None)
808
+ parser.add_argument("--init-worker", type=int, default=None)
809
+ parser.add_argument("--load-worker", type=int, default=None)
810
+ parser.add_argument("--batch-size", type=int, default=None)
811
+ parser.add_argument(
812
+ "--gradient-accumulation-steps", type=int, default=None)
813
+ parser.add_argument("--num-steps", type=int, default=None)
814
+ parser.add_argument("--save-interval", type=int, default=None)
815
+
816
+ args = parser.parse_args()
817
+ run(args)
818
+
819
+
820
+ if __name__ == "__main__":
821
+ init_logger()
822
+ main()
wan_va/utils/Simple_Remote_Infer/LEGAL.md ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ Legal Disclaimer
2
+
3
+ Within this source code, the comments in Chinese shall be the original, governing version. Any comment in other languages are for reference only. In the event of any conflict between the Chinese language version comments and other language version comments, the Chinese language version shall prevail.
4
+
5
+ 法律免责声明
6
+
7
+ 关于代码注释部分,中文注释为官方版本,其它语言注释仅做参考。中文注释可能与其它语言注释存在不一致,当中文注释与其它语言注释存在不一致时,请以中文注释为准。
wan_va/utils/Simple_Remote_Infer/deploy/__init__.py ADDED
File without changes
wan_va/utils/Simple_Remote_Infer/deploy/msgpack_numpy.py ADDED
@@ -0,0 +1,61 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Adds NumPy array support to msgpack.
2
+
3
+ msgpack is good for (de)serializing data over a network for multiple reasons:
4
+ - msgpack is secure (as opposed to pickle/dill/etc which allow for arbitrary code execution)
5
+ - msgpack is widely used and has good cross-language support
6
+ - msgpack does not require a schema (as opposed to protobuf/flatbuffers/etc) which is convenient in dynamically typed
7
+ languages like Python and JavaScript
8
+ - msgpack is fast and efficient (as opposed to readable formats like JSON/YAML/etc); I found that msgpack was ~4x faster
9
+ than pickle for serializing large arrays using the below strategy
10
+
11
+ The code below is adapted from https://github.com/lebedov/msgpack-numpy. The reason not to use that library directly is
12
+ that it falls back to pickle for object arrays.
13
+ """
14
+
15
+ import functools
16
+
17
+ import msgpack
18
+ import numpy as np
19
+
20
+
21
+ def pack_array(obj):
22
+ if (isinstance(
23
+ obj,
24
+ (np.ndarray, np.generic))) and obj.dtype.kind in ("V", "O", "c"):
25
+ raise ValueError(f"Unsupported dtype: {obj.dtype}")
26
+
27
+ if isinstance(obj, np.ndarray):
28
+ return {
29
+ b"__ndarray__": True,
30
+ b"data": obj.tobytes(),
31
+ b"dtype": obj.dtype.str,
32
+ b"shape": obj.shape,
33
+ }
34
+
35
+ if isinstance(obj, np.generic):
36
+ return {
37
+ b"__npgeneric__": True,
38
+ b"data": obj.item(),
39
+ b"dtype": obj.dtype.str,
40
+ }
41
+
42
+ return obj
43
+
44
+
45
+ def unpack_array(obj):
46
+ if b"__ndarray__" in obj:
47
+ return np.ndarray(buffer=obj[b"data"],
48
+ dtype=np.dtype(obj[b"dtype"]),
49
+ shape=obj[b"shape"])
50
+
51
+ if b"__npgeneric__" in obj:
52
+ return np.dtype(obj[b"dtype"]).type(obj[b"data"])
53
+
54
+ return obj
55
+
56
+
57
+ Packer = functools.partial(msgpack.Packer, default=pack_array)
58
+ packb = functools.partial(msgpack.packb, default=pack_array)
59
+
60
+ Unpacker = functools.partial(msgpack.Unpacker, object_hook=unpack_array)
61
+ unpackb = functools.partial(msgpack.unpackb, object_hook=unpack_array)
wan_va/utils/Simple_Remote_Infer/deploy/websocket_policy_server.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import http
3
+ import logging
4
+ import time
5
+ import traceback
6
+
7
+ import websockets.asyncio.server as _server
8
+ import websockets.frames
9
+
10
+ from .msgpack_numpy import Packer, unpackb
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class WebsocketPolicyServer:
16
+ """Serves a policy using the websocket protocol. See websocket_client_policy.py for a client implementation.
17
+
18
+ Currently only implements the `load` and `infer` methods.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ policy,
24
+ host: str = "0.0.0.0",
25
+ port: int | None = None,
26
+ metadata: dict | None = None,
27
+ ) -> None:
28
+ self._policy = policy
29
+ self._host = host
30
+ self._port = port
31
+ self._metadata = metadata or {}
32
+ logging.getLogger("websockets.server").setLevel(logging.INFO)
33
+
34
+ def serve_forever(self) -> None:
35
+ asyncio.run(self.run())
36
+
37
+ async def run(self):
38
+ async with _server.serve(
39
+ self._handler,
40
+ self._host,
41
+ self._port,
42
+ compression=None,
43
+ max_size=None,
44
+ process_request=_health_check,
45
+ ping_interval=None,
46
+ ping_timeout=None,
47
+ ) as server:
48
+ await server.serve_forever()
49
+
50
+ async def _handler(self, websocket: _server.ServerConnection):
51
+ logger.info(f"Connection from {websocket.remote_address} opened")
52
+ packer = Packer()
53
+
54
+ await websocket.send(packer.pack(self._metadata))
55
+
56
+ prev_total_time = None
57
+ while True:
58
+ try:
59
+ start_time = time.monotonic()
60
+ obs = unpackb(await websocket.recv())
61
+
62
+ infer_time = time.monotonic()
63
+ action = self._policy.infer(obs)
64
+ infer_time = time.monotonic() - infer_time
65
+
66
+ action["server_timing"] = {
67
+ "infer_ms": infer_time * 1000,
68
+ }
69
+ if prev_total_time is not None:
70
+ # We can only record the last total time since we also want to include the send time.
71
+ action["server_timing"][
72
+ "prev_total_ms"] = prev_total_time * 1000
73
+
74
+ await websocket.send(packer.pack(action))
75
+ prev_total_time = time.monotonic() - start_time
76
+
77
+ except websockets.ConnectionClosed:
78
+ logger.info(
79
+ f"Connection from {websocket.remote_address} closed")
80
+ break
81
+ except Exception:
82
+ await websocket.send(traceback.format_exc())
83
+ await websocket.close(
84
+ code=websockets.frames.CloseCode.INTERNAL_ERROR,
85
+ reason=
86
+ "Internal server error. Traceback included in previous frame.",
87
+ )
88
+ raise
89
+
90
+
91
+ def _health_check(connection: _server.ServerConnection,
92
+ request: _server.Request) -> _server.Response | None:
93
+ if request.path == "/healthz":
94
+ return connection.respond(http.HTTPStatus.OK, "OK\n")
95
+ # Continue with the normal request handling.
96
+ return None
wan_va/utils/__init__.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ from .logging import init_logger, logger
3
+ from .scheduler import FlowMatchScheduler
4
+ from .server_utils import run_async_server_mode
5
+ from .utils import data_seq_to_patch, get_mesh_id, save_async, sample_timestep_id, warmup_constant_lambda
6
+
7
+ __all__ = [
8
+ 'logger', 'init_logger', 'get_mesh_id', 'save_async', 'data_seq_to_patch',
9
+ 'FlowMatchScheduler', 'run_async_server_mode', 'sample_timestep_id', 'warmup_constant_lambda'
10
+ ]
wan_va/utils/logging.py ADDED
@@ -0,0 +1,23 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright (c) Meta Platforms, Inc. and affiliates.
2
+ # All rights reserved.
3
+ #
4
+ # This source code is licensed under the BSD-style license found in the
5
+ # LICENSE file in the root directory of this source tree.
6
+
7
+ import logging
8
+ import os
9
+
10
+ logger = logging.getLogger()
11
+
12
+
13
+ def init_logger():
14
+ logger.setLevel(logging.INFO)
15
+ ch = logging.StreamHandler()
16
+ ch.setLevel(logging.INFO)
17
+ formatter = logging.Formatter(
18
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s")
19
+ ch.setFormatter(formatter)
20
+ logger.addHandler(ch)
21
+
22
+ # suppress verbose torch.profiler logging
23
+ os.environ["KINETO_LOG_LEVEL"] = "5"
wan_va/utils/scheduler.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import math
3
+ import torch
4
+
5
+ class FlowMatchScheduler():
6
+
7
+ def __init__(
8
+ self,
9
+ num_inference_steps=100,
10
+ num_train_timesteps=1000,
11
+ shift=3.0,
12
+ sigma_max=1.0,
13
+ sigma_min=0.003 / 1.002,
14
+ inverse_timesteps=False,
15
+ extra_one_step=False,
16
+ reverse_sigmas=False,
17
+ exponential_shift=False,
18
+ exponential_shift_mu=None,
19
+ shift_terminal=None,
20
+ ):
21
+ self.num_train_timesteps = num_train_timesteps
22
+ self.shift = shift
23
+ self.sigma_max = sigma_max
24
+ self.sigma_min = sigma_min
25
+ self.inverse_timesteps = inverse_timesteps
26
+ self.extra_one_step = extra_one_step
27
+ self.reverse_sigmas = reverse_sigmas
28
+ self.exponential_shift = exponential_shift
29
+ self.exponential_shift_mu = exponential_shift_mu
30
+ self.shift_terminal = shift_terminal
31
+ self.set_timesteps(num_inference_steps)
32
+
33
+ def set_timesteps(self,
34
+ num_inference_steps=100,
35
+ denoising_strength=1.0,
36
+ training=False,
37
+ shift=None,
38
+ dynamic_shift_len=None):
39
+ if shift is not None:
40
+ self.shift = shift
41
+ sigma_start = self.sigma_min + (self.sigma_max -
42
+ self.sigma_min) * denoising_strength
43
+ if self.extra_one_step:
44
+ self.sigmas = torch.linspace(sigma_start, self.sigma_min,
45
+ num_inference_steps + 1)[:-1]
46
+ else:
47
+ self.sigmas = torch.linspace(sigma_start, self.sigma_min,
48
+ num_inference_steps)
49
+ if self.inverse_timesteps:
50
+ self.sigmas = torch.flip(self.sigmas, dims=[0])
51
+ if self.exponential_shift:
52
+ mu = self.calculate_shift(
53
+ dynamic_shift_len
54
+ ) if dynamic_shift_len is not None else self.exponential_shift_mu
55
+ self.sigmas = math.exp(mu) / (math.exp(mu) + (1 / self.sigmas - 1))
56
+ else:
57
+ self.sigmas = self.shift * self.sigmas / (
58
+ 1 + (self.shift - 1) * self.sigmas)
59
+ if self.shift_terminal is not None:
60
+ one_minus_z = 1 - self.sigmas
61
+ scale_factor = one_minus_z[-1] / (1 - self.shift_terminal)
62
+ self.sigmas = 1 - (one_minus_z / scale_factor)
63
+ if self.reverse_sigmas:
64
+ self.sigmas = 1 - self.sigmas
65
+ self.timesteps = self.sigmas * self.num_train_timesteps
66
+ if training:
67
+ x = self.timesteps
68
+ y = torch.exp(
69
+ -2 * ((x - num_inference_steps / 2) / num_inference_steps)**2)
70
+ y_shifted = y - y.min()
71
+ bsmntw_weighing = y_shifted * (num_inference_steps /
72
+ y_shifted.sum())
73
+ self.linear_timesteps_weights = bsmntw_weighing
74
+ self.training = True
75
+ else:
76
+ self.training = False
77
+
78
+ def step(self, model_output, timestep, sample, to_final=False, **kwargs):
79
+ if isinstance(timestep, torch.Tensor):
80
+ timestep = timestep.cpu()
81
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
82
+ sigma = self.sigmas[timestep_id]
83
+ if to_final or timestep_id + 1 >= len(self.timesteps):
84
+ sigma_ = 1 if (self.inverse_timesteps
85
+ or self.reverse_sigmas) else 0
86
+ else:
87
+ sigma_ = self.sigmas[timestep_id + 1]
88
+ prev_sample = sample + model_output * (sigma_ - sigma)
89
+ return prev_sample
90
+
91
+ def return_to_timestep(self, timestep, sample, sample_stablized):
92
+ if isinstance(timestep, torch.Tensor):
93
+ timestep = timestep.cpu()
94
+ timestep_id = torch.argmin((self.timesteps - timestep).abs())
95
+ sigma = self.sigmas[timestep_id]
96
+ model_output = (sample - sample_stablized) / sigma
97
+ return model_output
98
+
99
+ def add_noise(self, original_samples, noise, timestep, t_dim=2):
100
+ if isinstance(timestep, torch.Tensor):
101
+ timestep = timestep.cpu()
102
+ timestep = timestep[None]
103
+ timestep_id = torch.argmin((self.timesteps[:, None] - timestep).abs(),
104
+ dim=0)
105
+ shape = [1] * noise.ndim
106
+ shape[t_dim] = timestep_id.shape[0]
107
+ sigma = self.sigmas[timestep_id].to(original_samples).view(shape)
108
+ sample = (1 - sigma) * original_samples + sigma * noise
109
+ return sample
110
+
111
+ def training_target(self, sample, noise, timestep):
112
+ target = noise - sample
113
+ return target
114
+
115
+ def training_weight(self, timestep):
116
+ timestep_id = torch.argmin(
117
+ (self.timesteps[:, None].to(timestep.device) -
118
+ timestep[None]).abs(),
119
+ dim=0)
120
+ weights = self.linear_timesteps_weights.to(
121
+ timestep.device)[timestep_id].to(timestep.device)
122
+ return weights
123
+
124
+ def calculate_shift(
125
+ self,
126
+ image_seq_len,
127
+ base_seq_len: int = 256,
128
+ max_seq_len: int = 8192,
129
+ base_shift: float = 0.5,
130
+ max_shift: float = 0.9,
131
+ ):
132
+ m = (max_shift - base_shift) / (max_seq_len - base_seq_len)
133
+ b = base_shift - m * base_seq_len
134
+ mu = image_seq_len * m + b
135
+ return mu
wan_va/utils/server_utils.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import torch
3
+ import torch.distributed as dist
4
+
5
+ from .logging import logger
6
+ from .Simple_Remote_Infer.deploy.websocket_policy_server import WebsocketPolicyServer
7
+
8
+
9
+ class DistributedModelWrapper:
10
+ """
11
+ TODO
12
+ """
13
+
14
+ def __init__(self, model, local_rank):
15
+ self.model = model
16
+ self.local_rank = local_rank
17
+
18
+ def infer(self, obs):
19
+ return distributed_infer(self.model, obs, self.local_rank)
20
+
21
+
22
+ def distributed_infer(model, obs, local_rank):
23
+ """
24
+ TODO
25
+ """
26
+ rank = dist.get_rank()
27
+ assert rank == local_rank, "distributed_infer can only run at(rank 0)"
28
+
29
+ cmd = torch.tensor(1,
30
+ dtype=torch.int64,
31
+ device='cuda' if torch.cuda.is_available() else 'cpu')
32
+ dist.broadcast(cmd, src=0)
33
+
34
+ obj_list = [obs]
35
+ dist.broadcast_object_list(obj_list, src=0)
36
+
37
+ result = model.infer(obs)
38
+
39
+ return result
40
+
41
+
42
+ def worker_loop(model, local_rank):
43
+ """
44
+ TODO
45
+ """
46
+ device = 'cuda' if torch.cuda.is_available() else 'cpu'
47
+ rank = dist.get_rank()
48
+
49
+ while True:
50
+ cmd = torch.zeros(1, dtype=torch.int64, device=device)
51
+ dist.broadcast(cmd, src=0)
52
+ cmd_val = cmd.item()
53
+
54
+ if cmd_val == -1:
55
+ break
56
+ elif cmd_val == 1:
57
+ obj_list = [None]
58
+ dist.broadcast_object_list(obj_list, src=0)
59
+ obs = obj_list[0]
60
+ _ = model.infer(obs)
61
+ else:
62
+ pass
63
+
64
+ logger.info(f"[worker_loop] Rank {rank} exiting.")
65
+
66
+
67
+ def run_async_server_mode(model, local_rank, host, port):
68
+ logger.info("Running in ASYNC SERVER mode")
69
+ if local_rank == 0:
70
+ dist_model = DistributedModelWrapper(model, local_rank=local_rank)
71
+ model_server = WebsocketPolicyServer(dist_model, host=host, port=port)
72
+ model_server.serve_forever()
73
+
74
+ cmd = torch.tensor(
75
+ -1,
76
+ dtype=torch.int64,
77
+ device='cuda' if torch.cuda.is_available() else 'cpu')
78
+ dist.broadcast(cmd, src=0)
79
+ else:
80
+ try:
81
+ worker_loop(model, local_rank)
82
+ except KeyboardInterrupt:
83
+ logger.info(f"Rank {local_rank}: Shutting down")
wan_va/utils/utils.py ADDED
@@ -0,0 +1,92 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import concurrent.futures
3
+
4
+ import numpy as np
5
+ import torch
6
+
7
+ executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
8
+
9
+ __all__ = ['get_mesh_id', 'save_async', 'data_seq_to_patch']
10
+
11
+
12
+ def data_seq_to_patch(
13
+ patch_size,
14
+ data_seq,
15
+ latent_num_frames,
16
+ latent_height,
17
+ latent_width,
18
+ batch_size=1,
19
+ ):
20
+ p_t, p_h, p_w = patch_size
21
+ post_patch_num_frames = latent_num_frames // p_t
22
+ post_patch_height = latent_height // p_h
23
+ post_patch_width = latent_width // p_w
24
+
25
+ data_patch = data_seq.reshape(batch_size, post_patch_num_frames,
26
+ post_patch_height, post_patch_width, p_t,
27
+ p_h, p_w, -1)
28
+ data_patch = data_patch.permute(0, 7, 1, 4, 2, 5, 3, 6)
29
+ data_patch = data_patch.flatten(6, 7).flatten(4, 5).flatten(2, 3)
30
+ return data_patch
31
+
32
+
33
+ def get_mesh_id(f, h, w, t, f_w=1, f_shift=0, action=False):
34
+ f_idx = torch.arange(f_shift, f + f_shift) * f_w
35
+ h_idx = torch.arange(h)
36
+ w_idx = torch.arange(w)
37
+ ff, hh, ww = torch.meshgrid(f_idx, h_idx, w_idx, indexing='ij')
38
+ if action:
39
+ ff_offset = (torch.ones([h]).cumsum(0) / (h + 1)).view(1, -1, 1)
40
+ ff = ff + ff_offset
41
+ hh = torch.ones_like(hh) * -1
42
+ ww = torch.ones_like(ww) * -1
43
+
44
+ grid_id = torch.cat(
45
+ [
46
+ ff.unsqueeze(0),
47
+ hh.unsqueeze(0),
48
+ ww.unsqueeze(0),
49
+ ],
50
+ dim=0,
51
+ ).flatten(1)
52
+ grid_id = torch.cat([grid_id, torch.full_like(grid_id[:1], t)], dim=0)
53
+ return grid_id
54
+
55
+
56
+ def save_async(obj, file_path):
57
+ """
58
+ todo
59
+ """
60
+ if torch.is_tensor(obj) or (isinstance(obj, dict) and any(
61
+ torch.is_tensor(v) for v in obj.values())):
62
+ if torch.is_tensor(obj):
63
+ if obj.is_cuda:
64
+ obj = obj.cpu()
65
+ elif isinstance(obj, dict):
66
+ obj = {
67
+ k: v.cpu() if torch.is_tensor(v) else v
68
+ for k, v in obj.items()
69
+ }
70
+ executor.submit(torch.save, obj, file_path)
71
+ elif isinstance(obj, np.ndarray):
72
+ obj_copy = obj.copy()
73
+ executor.submit(np.save, file_path, obj_copy)
74
+ else:
75
+ executor.submit(torch.save, obj, file_path)
76
+
77
+ def sample_timestep_id(
78
+ batch_size: int = 1,
79
+ min_timestep_bd: float = 0.0,
80
+ max_timestep_bd: float = 1.0,
81
+ num_train_timesteps: int = 1000,
82
+ ):
83
+ u = torch.rand(size=[batch_size])
84
+ u = u * (max_timestep_bd - min_timestep_bd) + min_timestep_bd
85
+ timestep_id = (u * num_train_timesteps).clamp(min=0, max=num_train_timesteps - 1).to(torch.int64)
86
+ return timestep_id
87
+
88
+
89
+ def warmup_constant_lambda(current_step, warmup_steps=1000):
90
+ if current_step < warmup_steps:
91
+ return float(current_step) / float(max(1, warmup_steps))
92
+ return 1.0
wan_va/wan_va_server.py ADDED
@@ -0,0 +1,729 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024-2025 The Robbyant Team Authors. All rights reserved.
2
+ import argparse
3
+ import os
4
+ import time
5
+ from functools import partial
6
+ from PIL import Image
7
+ from diffusers.video_processor import VideoProcessor
8
+ from diffusers.utils import export_to_video
9
+
10
+ import numpy as np
11
+ import torch
12
+ import torch.nn.functional as F
13
+ from diffusers.pipelines.wan.pipeline_wan import prompt_clean
14
+ from einops import rearrange
15
+ from tqdm import tqdm
16
+
17
+ from .configs import VA_CONFIGS
18
+ from .distributed.fsdp import shard_model
19
+ from .distributed.util import _configure_model, init_distributed
20
+ from .modules.utils import (
21
+ WanVAEStreamingWrapper,
22
+ load_text_encoder,
23
+ load_tokenizer,
24
+ load_transformer,
25
+ load_vae,
26
+ )
27
+ from .utils import (
28
+ FlowMatchScheduler,
29
+ data_seq_to_patch,
30
+ get_mesh_id,
31
+ init_logger,
32
+ logger,
33
+ run_async_server_mode,
34
+ save_async,
35
+ )
36
+
37
+
38
+ class VA_Server:
39
+
40
+ def __init__(self, job_config):
41
+ self.cache_name = 'pos'
42
+ self.job_config = job_config
43
+ self.save_root = job_config.save_root
44
+ self.dtype = job_config.param_dtype
45
+ self.device = torch.device(f"cuda:{job_config.local_rank}")
46
+ self.enable_offload = getattr(job_config, 'enable_offload', True) # offload vae & text_encoder to save vram
47
+
48
+ self.scheduler = FlowMatchScheduler(shift=self.job_config.snr_shift,
49
+ sigma_min=0.0,
50
+ extra_one_step=True)
51
+ self.action_scheduler = FlowMatchScheduler(
52
+ shift=self.job_config.action_snr_shift,
53
+ sigma_min=0.0,
54
+ extra_one_step=True)
55
+ self.scheduler.set_timesteps(1000, training=True)
56
+ self.action_scheduler.set_timesteps(1000, training=True)
57
+
58
+ self.vae = load_vae(
59
+ os.path.join(job_config.wan22_pretrained_model_name_or_path,
60
+ 'vae'),
61
+ torch_dtype=self.dtype,
62
+ torch_device='cpu' if self.enable_offload else self.device,
63
+ )
64
+ self.streaming_vae = WanVAEStreamingWrapper(self.vae)
65
+
66
+ self.tokenizer = load_tokenizer(
67
+ os.path.join(job_config.wan22_pretrained_model_name_or_path,
68
+ 'tokenizer'), )
69
+
70
+ self.text_encoder = load_text_encoder(
71
+ os.path.join(job_config.wan22_pretrained_model_name_or_path,
72
+ 'text_encoder'),
73
+ torch_dtype=self.dtype,
74
+ torch_device='cpu' if self.enable_offload else self.device,
75
+ )
76
+
77
+ self.transformer = load_transformer(
78
+ os.path.join(job_config.wan22_pretrained_model_name_or_path,
79
+ 'transformer'),
80
+ torch_dtype=self.dtype,
81
+ torch_device=self.device,
82
+ attn_mode="torch",
83
+ disable_mcp=True,
84
+ )
85
+ shard_fn = shard_model
86
+ self.transformer = _configure_model(model=self.transformer,
87
+ shard_fn=shard_fn,
88
+ param_dtype=self.dtype,
89
+ device=self.device,
90
+ eval_mode=True,
91
+ )
92
+
93
+ self.env_type = job_config.env_type
94
+ self.streaming_vae_half = None
95
+ if self.env_type == 'robotwin_tshape':
96
+ vae_half = load_vae(
97
+ os.path.join(job_config.wan22_pretrained_model_name_or_path,
98
+ 'vae'),
99
+ torch_dtype=self.dtype,
100
+ torch_device='cpu' if self.enable_offload else self.device,
101
+ )
102
+ self.streaming_vae_half = WanVAEStreamingWrapper(vae_half)
103
+
104
+ def _get_t5_prompt_embeds(
105
+ self,
106
+ prompt=None,
107
+ num_videos_per_prompt=1,
108
+ max_sequence_length=512,
109
+ device=None,
110
+ dtype=None,
111
+ ):
112
+ device = device or self.device
113
+ dtype = dtype or self.dtype
114
+
115
+ prompt = [prompt] if isinstance(prompt, str) else prompt
116
+ prompt = [prompt_clean(u) for u in prompt]
117
+ batch_size = len(prompt)
118
+
119
+ text_inputs = self.tokenizer(
120
+ prompt,
121
+ padding="max_length",
122
+ max_length=max_sequence_length,
123
+ truncation=True,
124
+ add_special_tokens=True,
125
+ return_attention_mask=True,
126
+ return_tensors="pt",
127
+ )
128
+ text_input_ids, mask = text_inputs.input_ids, text_inputs.attention_mask
129
+ seq_lens = mask.gt(0).sum(dim=1).long()
130
+
131
+ text_encoder_device = next(self.text_encoder.parameters()).device
132
+ prompt_embeds = self.text_encoder(text_input_ids.to(text_encoder_device),
133
+ mask.to(text_encoder_device)).last_hidden_state
134
+ prompt_embeds = prompt_embeds.to(dtype=dtype, device=device)
135
+ prompt_embeds = [u[:v] for u, v in zip(prompt_embeds, seq_lens)]
136
+ prompt_embeds = torch.stack([
137
+ torch.cat(
138
+ [u, u.new_zeros(max_sequence_length - u.size(0), u.size(1))])
139
+ for u in prompt_embeds
140
+ ],
141
+ dim=0)
142
+
143
+ # duplicate text embeddings for each generation per prompt, using mps friendly method
144
+ _, seq_len, _ = prompt_embeds.shape
145
+ prompt_embeds = prompt_embeds.repeat(1, num_videos_per_prompt, 1)
146
+ prompt_embeds = prompt_embeds.view(batch_size * num_videos_per_prompt,
147
+ seq_len, -1)
148
+
149
+ return prompt_embeds.to(device)
150
+
151
+ def encode_prompt(
152
+ self,
153
+ prompt,
154
+ negative_prompt=None,
155
+ do_classifier_free_guidance=True,
156
+ num_videos_per_prompt=1,
157
+ prompt_embeds=None,
158
+ negative_prompt_embeds=None,
159
+ max_sequence_length=226,
160
+ device=None,
161
+ dtype=None,
162
+ ):
163
+ r"""
164
+ TODO
165
+ """
166
+ device = device or self.device
167
+ dtype = dtype or self.dtype
168
+
169
+ prompt = [prompt] if isinstance(prompt, str) else prompt
170
+ if prompt is not None:
171
+ batch_size = len(prompt)
172
+ else:
173
+ batch_size = prompt_embeds.shape[0]
174
+
175
+ if prompt_embeds is None:
176
+ prompt_embeds = self._get_t5_prompt_embeds(
177
+ prompt=prompt,
178
+ num_videos_per_prompt=num_videos_per_prompt,
179
+ max_sequence_length=max_sequence_length,
180
+ device=device,
181
+ dtype=dtype,
182
+ )
183
+
184
+ if do_classifier_free_guidance and negative_prompt_embeds is None:
185
+ negative_prompt = negative_prompt or ""
186
+ negative_prompt = batch_size * [negative_prompt] if isinstance(
187
+ negative_prompt, str) else negative_prompt
188
+
189
+ if prompt is not None and type(prompt) is not type(
190
+ negative_prompt):
191
+ raise TypeError(
192
+ f"`negative_prompt` should be the same type to `prompt`, but got {type(negative_prompt)} !="
193
+ f" {type(prompt)}.")
194
+ elif batch_size != len(negative_prompt):
195
+ raise ValueError(
196
+ f"`negative_prompt`: {negative_prompt} has batch size {len(negative_prompt)}, but `prompt`:"
197
+ f" {prompt} has batch size {batch_size}. Please make sure that passed `negative_prompt` matches"
198
+ " the batch size of `prompt`.")
199
+
200
+ negative_prompt_embeds = self._get_t5_prompt_embeds(
201
+ prompt=negative_prompt,
202
+ num_videos_per_prompt=num_videos_per_prompt,
203
+ max_sequence_length=max_sequence_length,
204
+ device=device,
205
+ dtype=dtype,
206
+ )
207
+ return prompt_embeds, negative_prompt_embeds
208
+
209
+ def normalize_latents(
210
+ self,
211
+ latents: torch.Tensor,
212
+ latents_mean: torch.Tensor,
213
+ latents_std: torch.Tensor,
214
+ ) -> torch.Tensor:
215
+ latents_mean = latents_mean.view(1, -1, 1, 1,
216
+ 1).to(device=latents.device)
217
+ latents_std = latents_std.view(1, -1, 1, 1,
218
+ 1).to(device=latents.device)
219
+ latents = ((latents.float() - latents_mean) * latents_std).to(latents)
220
+ return latents
221
+
222
+ def preprocess_action(self, action):
223
+ action_model_input = torch.from_numpy(action)
224
+ CA, FA, HA = action_model_input.shape # C, F, H
225
+ action_model_input_paded = F.pad(action_model_input,
226
+ [0, 0, 0, 0, 0, 1],
227
+ mode='constant',
228
+ value=0)
229
+
230
+ action_model_input = action_model_input_paded[
231
+ self.job_config.inverse_used_action_channel_ids]
232
+
233
+ if self.action_norm_method == 'quantiles':
234
+ action_model_input = (action_model_input - self.actions_q01) / (
235
+ self.actions_q99 - self.actions_q01 + 1e-6) * 2. - 1.
236
+ else:
237
+ raise NotImplementedError
238
+ return action_model_input.unsqueeze(0).unsqueeze(-1) # B, C, F, H, W
239
+
240
+ def postprocess_action(self, action):
241
+ action = action.cpu() # B, C, F, H, W
242
+
243
+ action = action[0, ..., 0] #C, F, H
244
+ if self.action_norm_method == 'quantiles':
245
+ action = (action + 1) / 2 * (self.actions_q99 - self.actions_q01 +
246
+ 1e-6) + self.actions_q01
247
+ else:
248
+ raise NotImplementedError
249
+ action = action.squeeze(0).detach().cpu().numpy()
250
+ return action[self.job_config.used_action_channel_ids]
251
+
252
+ def _repeat_input_for_cfg(self, input_dict):
253
+ if self.use_cfg:
254
+ input_dict['noisy_latents'] = input_dict['noisy_latents'].repeat(2, 1, 1, 1, 1)
255
+ input_dict['text_emb'] = torch.cat([self.prompt_embeds.to(self.dtype).clone(), self.negative_prompt_embeds.to(self.dtype).clone()], dim=0)
256
+ input_dict['grid_id'] = input_dict['grid_id'][None].repeat(2, 1, 1)
257
+ input_dict['timesteps'] = input_dict['timesteps'][None].repeat(2, 1)
258
+ else:
259
+ input_dict['grid_id'] = input_dict['grid_id'][None]
260
+ input_dict['timesteps'] = input_dict['timesteps'][None]
261
+ return input_dict
262
+
263
+ def _prepare_latent_input(self,
264
+ latent_model_input,
265
+ action_model_input,
266
+ latent_t=0,
267
+ action_t=0,
268
+ latent_cond=None,
269
+ action_cond=None,
270
+ frame_st_id=0,
271
+ patch_size=(1, 2, 2)):
272
+ logger.info(f"FRAME START ID: {frame_st_id}")
273
+ input_dict = dict()
274
+ if latent_model_input is not None:
275
+ input_dict['latent_res_lst'] = {
276
+ 'noisy_latents':
277
+ latent_model_input,
278
+ 'timesteps':
279
+ torch.ones([latent_model_input.shape[2]],
280
+ dtype=torch.float32,
281
+ device=self.device) * latent_t,
282
+ 'grid_id':
283
+ get_mesh_id(latent_model_input.shape[-3] // patch_size[0],
284
+ latent_model_input.shape[-2] // patch_size[1],
285
+ latent_model_input.shape[-1] // patch_size[2], 0,
286
+ 1, frame_st_id).to(self.device),
287
+ 'text_emb':
288
+ self.prompt_embeds.to(self.dtype).clone(),
289
+ }
290
+ if latent_cond is not None:
291
+ input_dict['latent_res_lst'][
292
+ 'noisy_latents'][:, :, 0:1] = latent_cond[:, :, 0:1]
293
+ input_dict['latent_res_lst']['timesteps'][0:1] *= 0
294
+
295
+ if action_model_input is not None:
296
+ input_dict['action_res_lst'] = {
297
+ 'noisy_latents':
298
+ action_model_input,
299
+ 'timesteps':
300
+ torch.ones([action_model_input.shape[2]],
301
+ dtype=torch.float32,
302
+ device=self.device) * action_t,
303
+ 'grid_id':
304
+ get_mesh_id(action_model_input.shape[-3],
305
+ action_model_input.shape[-2],
306
+ action_model_input.shape[-1],
307
+ 1,
308
+ 1,
309
+ frame_st_id,
310
+ action=True).to(self.device),
311
+ 'text_emb':
312
+ self.prompt_embeds.to(self.dtype).clone(),
313
+ }
314
+
315
+ if action_cond is not None:
316
+ input_dict['action_res_lst'][
317
+ 'noisy_latents'][:, :, 0:1] = action_cond[:, :, 0:1]
318
+ input_dict['action_res_lst']['timesteps'][0:1] *= 0
319
+ input_dict['action_res_lst']['noisy_latents'][:, ~self.
320
+ action_mask] *= 0
321
+ return input_dict
322
+
323
+ def _encode_obs(self, obs):
324
+ images = obs['obs']
325
+ if not isinstance(images, list):
326
+ images = [images]
327
+ if len(images) < 1:
328
+ return None
329
+ videos = []
330
+ for k_i, k in enumerate(self.job_config.obs_cam_keys):
331
+ if self.env_type == 'robotwin_tshape':
332
+ if k_i == 0: # camera high
333
+ height_i, width_i = self.height, self.width
334
+ else:
335
+ height_i, width_i = self.height // 2, self.width // 2
336
+ else:
337
+ height_i, width_i = self.height, self.width
338
+
339
+ history_video_k = torch.from_numpy(
340
+ np.stack([each[k]
341
+ for each in images])).float().permute(3, 0, 1, 2)
342
+ history_video_k = F.interpolate(history_video_k,
343
+ size=(height_i, width_i),
344
+ mode='bilinear',
345
+ align_corners=False).unsqueeze(0)
346
+ videos.append(history_video_k)
347
+
348
+ if self.env_type == 'robotwin_tshape':
349
+ videos_high = videos[0] / 255.0 * 2.0 - 1.0
350
+ videos_left_and_right = torch.cat(videos[1:],
351
+ dim=0) / 255.0 * 2.0 - 1.0
352
+ vae_device = next(self.streaming_vae.vae.parameters()).device
353
+ enc_out_high = self.streaming_vae.encode_chunk(
354
+ videos_high.to(vae_device).to(self.dtype))
355
+ enc_out_left_and_right = self.streaming_vae_half.encode_chunk(
356
+ videos_left_and_right.to(vae_device).to(self.dtype))
357
+ enc_out = torch.cat([
358
+ torch.cat(enc_out_left_and_right.split(1, dim=0), dim=-1),
359
+ enc_out_high
360
+ ],
361
+ dim=-2)
362
+ else:
363
+ videos = torch.cat(videos, dim=0) / 255.0 * 2.0 - 1.0
364
+ vae_device = next(self.streaming_vae.vae.parameters()).device
365
+ videos_chunk = videos.to(vae_device).to(self.dtype)
366
+ enc_out = self.streaming_vae.encode_chunk(videos_chunk)
367
+
368
+ mu, logvar = torch.chunk(enc_out, 2, dim=1)
369
+ latents_mean = torch.tensor(self.vae.config.latents_mean).to(mu.device)
370
+ latents_std = torch.tensor(self.vae.config.latents_std).to(mu.device)
371
+ mu_norm = self.normalize_latents(mu, latents_mean, 1.0 / latents_std)
372
+ video_latent = torch.cat(mu_norm.split(1, dim=0), dim=-1)
373
+ return video_latent.to(self.device)
374
+
375
+ def _reset(self, prompt=None):
376
+ logger.info('Reset.')
377
+ self.use_cfg = (self.job_config.guidance_scale > 1) or (self.job_config.action_guidance_scale > 1)
378
+ #### Reset all parameters
379
+ self.frame_st_id = 0
380
+ self.init_latent = None
381
+ #### clean vae and transformer cache
382
+ self.transformer.clear_cache(self.cache_name)
383
+ self.streaming_vae.clear_cache()
384
+
385
+ self.action_per_frame = self.job_config.action_per_frame
386
+ self.height, self.width = self.job_config.height, self.job_config.width
387
+
388
+ if self.env_type == 'robotwin_tshape':
389
+ self.latent_height, self.latent_width = (
390
+ (self.height // 16) * 3) // 2, self.width // 16
391
+ self.streaming_vae_half.clear_cache()
392
+ else:
393
+ self.latent_height, self.latent_width = self.height // 16, self.width // 16 * len(
394
+ self.job_config.obs_cam_keys)
395
+
396
+ patch_size = self.job_config.patch_size
397
+ latent_token_per_chunk = (self.job_config.frame_chunk_size *
398
+ self.latent_height * self.latent_width) // (
399
+ patch_size[0] * patch_size[1] *
400
+ patch_size[2])
401
+ action_token_per_chunk = self.job_config.frame_chunk_size * self.action_per_frame
402
+ self.transformer.create_empty_cache(self.cache_name,
403
+ self.job_config.attn_window,
404
+ latent_token_per_chunk,
405
+ action_token_per_chunk,
406
+ dtype=self.dtype,
407
+ device=self.device,
408
+ batch_size = 2 if self.use_cfg else 1
409
+ )
410
+
411
+ self.action_mask = torch.zeros([self.job_config.action_dim]).bool()
412
+ self.action_mask[self.job_config.used_action_channel_ids] = True
413
+
414
+ self.actions_q01 = torch.tensor(self.job_config.norm_stat['q01'],
415
+ dtype=torch.float32).reshape(-1, 1, 1)
416
+ self.actions_q99 = torch.tensor(self.job_config.norm_stat['q99'],
417
+ dtype=torch.float32).reshape(-1, 1, 1)
418
+ self.action_norm_method = self.job_config.action_norm_method
419
+
420
+ ##### get prompt
421
+ if prompt is None:
422
+ self.prompt_embeds = self.negative_prompt_embeds = None
423
+ else:
424
+ self.prompt_embeds, self.negative_prompt_embeds = self.encode_prompt(
425
+ prompt=prompt,
426
+ negative_prompt=None,
427
+ do_classifier_free_guidance=self.job_config.guidance_scale > 1,
428
+ num_videos_per_prompt=1,
429
+ prompt_embeds=None,
430
+ negative_prompt_embeds=None,
431
+ max_sequence_length=512,
432
+ device=self.device,
433
+ dtype=self.dtype,
434
+ )
435
+
436
+ self.exp_name = f"{prompt}_{time.strftime('%Y%m%d_%H%M%S')}" if prompt else "default"
437
+ self.exp_save_root = os.path.join(self.save_root, 'real', self.exp_name)
438
+ os.makedirs(self.exp_save_root, exist_ok=True)
439
+ torch.cuda.empty_cache()
440
+
441
+ def _infer(self, obs, frame_st_id=0):
442
+ frame_chunk_size = self.job_config.frame_chunk_size
443
+ if frame_st_id == 0:
444
+ init_latent = self._encode_obs(obs)
445
+ self.init_latent = init_latent
446
+
447
+ latents = torch.randn(1,
448
+ 48,
449
+ frame_chunk_size,
450
+ self.latent_height,
451
+ self.latent_width,
452
+ device=self.device,
453
+ dtype=self.dtype)
454
+ actions = torch.randn(1,
455
+ self.job_config.action_dim,
456
+ frame_chunk_size,
457
+ self.action_per_frame,
458
+ 1,
459
+ device=self.device,
460
+ dtype=self.dtype)
461
+
462
+ video_inference_step = self.job_config.num_inference_steps
463
+ action_inference_step = self.job_config.action_num_inference_steps
464
+ video_step = self.job_config.video_exec_step
465
+
466
+ self.scheduler.set_timesteps(video_inference_step)
467
+ self.action_scheduler.set_timesteps(action_inference_step)
468
+ timesteps = self.scheduler.timesteps
469
+ action_timesteps = self.action_scheduler.timesteps
470
+
471
+ timesteps = F.pad(timesteps, (0, 1), mode='constant', value=0)
472
+
473
+ if video_step != -1:
474
+ timesteps = timesteps[:video_step]
475
+
476
+ action_timesteps = F.pad(
477
+ action_timesteps,
478
+ (0,
479
+ 1), # pad 1 element at the end (right side) of the last dimension
480
+ mode='constant',
481
+ value=0)
482
+
483
+ with (
484
+ torch.no_grad(),
485
+ ):
486
+ # 1. Video Generation Loop
487
+ for i, t in enumerate(tqdm(timesteps)):
488
+ last_step = i == len(timesteps) - 1
489
+ latent_cond = init_latent[:, :, 0:1].to(
490
+ self.dtype) if frame_st_id == 0 else None
491
+ input_dict = self._prepare_latent_input(
492
+ latents,
493
+ None,
494
+ t,
495
+ t,
496
+ latent_cond,
497
+ None,
498
+ frame_st_id=frame_st_id)
499
+
500
+ video_noise_pred = self.transformer(
501
+ self._repeat_input_for_cfg(input_dict['latent_res_lst']),
502
+ update_cache=1 if last_step else 0,
503
+ cache_name=self.cache_name,
504
+ action_mode=False)
505
+
506
+ if not last_step or video_step != -1:
507
+ video_noise_pred = data_seq_to_patch(
508
+ self.job_config.patch_size, video_noise_pred,
509
+ frame_chunk_size, self.latent_height,
510
+ self.latent_width, batch_size=2 if self.use_cfg else 1)
511
+ if self.job_config.guidance_scale > 1:
512
+ video_noise_pred = video_noise_pred[1:] + self.job_config.guidance_scale * (video_noise_pred[:1] - video_noise_pred[1:])
513
+ else:
514
+ video_noise_pred = video_noise_pred[:1]
515
+ latents = self.scheduler.step(video_noise_pred,
516
+ t,
517
+ latents,
518
+ return_dict=False)
519
+
520
+ latents[:, :, 0:1] = latent_cond if frame_st_id == 0 else latents[:, :, 0:1]
521
+
522
+ for i, t in enumerate(tqdm(action_timesteps)):
523
+ last_step = i == len(action_timesteps) - 1
524
+ action_cond = torch.zeros(
525
+ [
526
+ 1, self.job_config.action_dim, 1,
527
+ self.action_per_frame, 1
528
+ ],
529
+ device=self.device,
530
+ dtype=self.dtype) if frame_st_id == 0 else None
531
+
532
+ input_dict = self._prepare_latent_input(
533
+ None,
534
+ actions,
535
+ t,
536
+ t,
537
+ None,
538
+ action_cond,
539
+ frame_st_id=frame_st_id)
540
+ action_noise_pred = self.transformer(
541
+ self._repeat_input_for_cfg(input_dict['action_res_lst']),
542
+ update_cache=1 if last_step else 0,
543
+ cache_name=self.cache_name,
544
+ action_mode=True)
545
+
546
+ if not last_step:
547
+ action_noise_pred = rearrange(action_noise_pred,
548
+ 'b (f n) c -> b c f n 1',
549
+ f=frame_chunk_size)
550
+ if self.job_config.action_guidance_scale > 1:
551
+ action_noise_pred = action_noise_pred[1:] + self.job_config.action_guidance_scale * (action_noise_pred[:1] - action_noise_pred[1:])
552
+ else:
553
+ action_noise_pred = action_noise_pred[:1]
554
+ actions = self.action_scheduler.step(action_noise_pred,
555
+ t,
556
+ actions,
557
+ return_dict=False)
558
+
559
+ actions[:, :, 0:1] = action_cond if frame_st_id == 0 else actions[:, :, 0:1]
560
+
561
+ actions[:, ~self.action_mask] *= 0
562
+
563
+ save_async(latents, os.path.join(self.exp_save_root, f'latents_{frame_st_id}.pt'))
564
+ save_async(actions, os.path.join(self.exp_save_root, f'actions_{frame_st_id}.pt'))
565
+
566
+ actions = self.postprocess_action(actions)
567
+ torch.cuda.empty_cache()
568
+ return actions, latents
569
+
570
+ def _compute_kv_cache(self, obs):
571
+ ### optional async save obs for debug
572
+ self.transformer.clear_pred_cache(self.cache_name)
573
+ save_async(obs['obs'], os.path.join(self.exp_save_root, f'obs_data_{self.frame_st_id}.pt'))
574
+ latent_model_input = self._encode_obs(obs)
575
+ if self.frame_st_id == 0:
576
+ latent_model_input = torch.cat(
577
+ [self.init_latent, latent_model_input],
578
+ dim=2) if latent_model_input is not None else self.init_latent
579
+
580
+ action_model_input = self.preprocess_action(obs['state'])
581
+ action_model_input = action_model_input.to(latent_model_input)
582
+ logger.info(
583
+ f"get KV cache obs: {latent_model_input.shape} {action_model_input.shape}"
584
+ )
585
+ input_dict = self._prepare_latent_input(latent_model_input,
586
+ action_model_input,
587
+ frame_st_id=self.frame_st_id)
588
+
589
+ with (
590
+ torch.no_grad(),
591
+ ):
592
+ self.transformer(self._repeat_input_for_cfg(input_dict['latent_res_lst']),
593
+ update_cache=2,
594
+ cache_name=self.cache_name,
595
+ action_mode=False)
596
+
597
+ self.transformer(self._repeat_input_for_cfg(input_dict['action_res_lst']),
598
+ update_cache=2,
599
+ cache_name=self.cache_name,
600
+ action_mode=True)
601
+ torch.cuda.empty_cache()
602
+ self.frame_st_id += latent_model_input.shape[2]
603
+
604
+ @torch.no_grad()
605
+ def infer(self, obs):
606
+ reset = obs.get('reset', False)
607
+ prompt = obs.get('prompt', None)
608
+ compute_kv_cache = obs.get('compute_kv_cache', False)
609
+
610
+ if reset:
611
+ logger.info(f"******************* Reset server ******************")
612
+ self._reset(prompt=prompt)
613
+ return dict()
614
+ elif compute_kv_cache:
615
+ logger.info(
616
+ f"################# Compute KV Cache #################")
617
+ self._compute_kv_cache(obs)
618
+ return dict()
619
+ else:
620
+ logger.info(f"################# Infer One Chunk #################")
621
+ action, _ = self._infer(obs, frame_st_id=self.frame_st_id)
622
+ return dict(action=action)
623
+
624
+ def decode_one_video(self, latents, output_type):
625
+ latents = latents.to(self.vae.dtype)
626
+ latents_mean = (
627
+ torch.tensor(self.vae.config.latents_mean)
628
+ .view(1, self.vae.config.z_dim, 1, 1, 1)
629
+ .to(latents.device, latents.dtype)
630
+ )
631
+ latents_std = 1.0 / torch.tensor(self.vae.config.latents_std).view(1, self.vae.config.z_dim, 1, 1, 1).to(
632
+ latents.device, latents.dtype
633
+ )
634
+ latents = latents / latents_std + latents_mean
635
+ video = self.vae.decode(latents, return_dict=False)[0]
636
+ video = self.video_processor.postprocess_video(video, output_type=output_type)
637
+ return video
638
+
639
+ def load_init_obs(self):
640
+ imf_dict = {v: np.array(Image.open(os.path.join(self.job_config.input_img_path, f"{v}.png")).convert("RGB")) for v in self.job_config.obs_cam_keys}
641
+ init_obs = {}
642
+ init_obs['obs'] = [imf_dict]
643
+ return init_obs
644
+
645
+ @torch.no_grad()
646
+ def generate(self):
647
+ self.video_processor = VideoProcessor(vae_scale_factor=1)
648
+ self._reset(self.job_config.prompt)
649
+ init_obs = self.load_init_obs()
650
+ pred_latent_lst = []
651
+ pred_action_lst = []
652
+ for chunk_id in range(self.job_config.num_chunks_to_infer):
653
+ actions, latents = self._infer(init_obs, frame_st_id=(chunk_id * self.job_config.frame_chunk_size))
654
+ actions = torch.from_numpy(actions)
655
+ pred_latent_lst.append(latents)
656
+ pred_action_lst.append(actions)
657
+ pred_latent = torch.cat(pred_latent_lst, dim=2)
658
+ pred_action = torch.cat(pred_action_lst, dim=1).flatten(1)
659
+ self.transformer.clear_cache(self.cache_name)
660
+ self.streaming_vae.clear_cache()
661
+ if self.streaming_vae_half:
662
+ self.streaming_vae_half.clear_cache()
663
+ del self.transformer
664
+ del self.streaming_vae_half
665
+ del self.text_encoder
666
+ torch.cuda.empty_cache()
667
+
668
+ # Move VAE to GPU for decoding
669
+ if self.enable_offload:
670
+ self.vae = self.vae.to(self.device).to(self.dtype)
671
+
672
+ decoded_video = self.decode_one_video(pred_latent, 'np')[0]
673
+ export_to_video(decoded_video, os.path.join(self.save_root, "demo.mp4"), fps=10)
674
+
675
+ def run(args):
676
+
677
+ config = VA_CONFIGS[args.config_name]
678
+ port = config.port if args.port is None else args.port
679
+ if args.save_root is not None:
680
+ config.save_root = args.save_root
681
+ rank = int(os.getenv("RANK", 0))
682
+ local_rank = int(os.environ.get('LOCAL_RANK', 0))
683
+ world_size = int(os.environ.get("WORLD_SIZE", 1))
684
+ init_distributed(world_size, local_rank, rank)
685
+ config.rank = rank
686
+ config.local_rank = local_rank
687
+ config.world_size = world_size
688
+ model = VA_Server(config)
689
+ if config.infer_mode == 'i2va':
690
+ logger.info("******************************USE I2VA mode******************************")
691
+ model.generate()
692
+ elif config.infer_mode == 'server':
693
+ logger.info(f"******************************USE Server mode******************************")
694
+ run_async_server_mode(model, local_rank, config.host, port)
695
+ else:
696
+ raise ValueError(f"Unknown infer mode: {config.infer_mode}")
697
+
698
+ def main():
699
+ """
700
+ TODO
701
+ """
702
+ parser = argparse.ArgumentParser()
703
+ parser.add_argument(
704
+ "--config-name",
705
+ type=str,
706
+ required=False,
707
+ default='robotwin',
708
+ help="config name.",
709
+ )
710
+ parser.add_argument(
711
+ "--port",
712
+ type=int,
713
+ default=None,
714
+ help='(start) port'
715
+ )
716
+ parser.add_argument(
717
+ "--save_root",
718
+ type=str,
719
+ default=None,
720
+ help='save root'
721
+ )
722
+ args = parser.parse_args()
723
+ run(args)
724
+ logger.info("Finish all process!!!!!!!!!!!!")
725
+
726
+
727
+ if __name__ == "__main__":
728
+ init_logger()
729
+ main()