Spaces:
Running on Zero
Running on Zero
Upload folder using huggingface_hub
Browse files- .gitattributes +2 -0
- README.md +36 -6
- app.py +651 -0
- examples/observation.images.top.png +3 -0
- examples/observation.images.wrist.png +3 -0
- requirements.txt +15 -0
- wan_va/__init__.py +2 -0
- wan_va/build_dataset_index.py +41 -0
- wan_va/configs/__init__.py +20 -0
- wan_va/configs/mcp_train_config.py +13 -0
- wan_va/configs/shared_config.py +21 -0
- wan_va/configs/va_demo_cfg.py +56 -0
- wan_va/configs/va_demo_i2va.py +11 -0
- wan_va/configs/va_demo_train_cfg.py +27 -0
- wan_va/configs/va_franka_cfg.py +58 -0
- wan_va/configs/va_franka_i2va.py +11 -0
- wan_va/configs/va_robotwin_cfg.py +59 -0
- wan_va/configs/va_robotwin_i2va.py +11 -0
- wan_va/configs/va_robotwin_train_cfg.py +40 -0
- wan_va/dataset/__init__.py +10 -0
- wan_va/dataset/lerobot_latent_dataset.py +548 -0
- wan_va/distributed/__init__.py +1 -0
- wan_va/distributed/fsdp.py +55 -0
- wan_va/distributed/util.py +40 -0
- wan_va/mcp.py +59 -0
- wan_va/modules/__init__.py +13 -0
- wan_va/modules/model.py +1196 -0
- wan_va/modules/utils.py +100 -0
- wan_va/train.py +822 -0
- wan_va/utils/Simple_Remote_Infer/LEGAL.md +7 -0
- wan_va/utils/Simple_Remote_Infer/deploy/__init__.py +0 -0
- wan_va/utils/Simple_Remote_Infer/deploy/msgpack_numpy.py +61 -0
- wan_va/utils/Simple_Remote_Infer/deploy/websocket_policy_server.py +96 -0
- wan_va/utils/__init__.py +10 -0
- wan_va/utils/logging.py +23 -0
- wan_va/utils/scheduler.py +135 -0
- wan_va/utils/server_utils.py +83 -0
- wan_va/utils/utils.py +92 -0
- wan_va/wan_va_server.py +729 -0
.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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: gradio
|
| 7 |
sdk_version: 6.25.0
|
| 8 |
-
python_version: '3.12'
|
| 9 |
app_file: app.py
|
| 10 |
-
|
|
|
|
|
|
|
| 11 |
---
|
| 12 |
|
| 13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
examples/observation.images.wrist.png
ADDED
|
Git LFS Details
|
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()
|