morinoppp commited on
Commit
7127e1e
·
verified ·
1 Parent(s): 746b576

Upload folder using huggingface_hub

Browse files
.gitattributes CHANGED
@@ -33,3 +33,9 @@ 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/episode_000000.mp4 filter=lfs diff=lfs merge=lfs -text
37
+ examples/episode_000008.mp4 filter=lfs diff=lfs merge=lfs -text
38
+ examples/episode_000087.mp4 filter=lfs diff=lfs merge=lfs -text
39
+ examples/gt/episode_000000.mp4 filter=lfs diff=lfs merge=lfs -text
40
+ examples/gt/episode_000008.mp4 filter=lfs diff=lfs merge=lfs -text
41
+ examples/gt/episode_000087.mp4 filter=lfs diff=lfs merge=lfs -text
README.md CHANGED
@@ -1,3 +1,137 @@
1
- ---
2
- license: apache-2.0
3
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: apache-2.0
3
+ tags:
4
+ - robotics
5
+ - world-model
6
+ - video-prediction
7
+ - action-conditioned
8
+ - nvidia
9
+ - cosmos
10
+ base_model: nvidia/Cosmos-Predict2.5-2B
11
+ ---
12
+
13
+ # Cosmos-Predict2.5-2B GR1 Action-Conditioned World Model
14
+
15
+ Action-conditioned video prediction model fine-tuned from [nvidia/Cosmos-Predict2.5-2B](https://huggingface.co/nvidia/Cosmos-Predict2.5-2B) on GR1 dual-arm robot teleoperation data.
16
+
17
+ Given an initial observation frame and a sequence of robot actions, this model predicts future video frames that depict the robot executing those actions.
18
+
19
+ ## Model Details
20
+
21
+ - **Base model:** Cosmos-Predict2.5-2B (video2world pre-trained)
22
+ - **Fine-tuning data:** GR1 dual-arm robot teleoperation (LeRobot format)
23
+ - **Action dimension:** 29 (left_arm:7 + left_hand:6 + right_arm:7 + right_hand:6 + waist:3)
24
+ - **Video frames:** 13 frames per chunk (1 conditional + 12 predicted)
25
+ - **Temporal compression:** 4x (13 image frames = 4 latent frames)
26
+ - **Resolution:** 480x832
27
+ - **Checkpoint format:** EMA weights in bf16 (`model_ema_bf16.pt`, ~4GB)
28
+
29
+ ## Training Configuration
30
+
31
+ | Parameter | Value |
32
+ |-----------|-------|
33
+ | GPUs | 4x H200 (140GB) |
34
+ | Batch size | 4 per GPU (global=16) |
35
+ | Learning rate | 8e-5 |
36
+ | Max iterations | 4,000 |
37
+ | Save interval | 2,000 |
38
+ | Optimizer | AdamW (cosine schedule) |
39
+ | Precision | bf16 |
40
+ | Episodes per task | 3 (uniform sampling) |
41
+ | VAE | Wan2.1 |
42
+ | Text encoder | Cosmos-Reason1-7B |
43
+ | Tokenizer | Qwen2.5-VL-7B-Instruct |
44
+
45
+ ## Inference
46
+
47
+ ### Prerequisites
48
+
49
+ This model requires the [Cosmos-Predict2.5](https://github.com/NVIDIA/Cosmos-Predict2.5) codebase and the following dependencies:
50
+
51
+ - Wan2.1 VAE: `Wan2.1_VAE.pth`
52
+ - Text encoder: Cosmos-Reason1-7B
53
+ - Tokenizer: Qwen2.5-VL-7B-Instruct
54
+
55
+ ### Quick Start
56
+
57
+ ```bash
58
+ python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
59
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes_release_oss \
60
+ --ckpt_path=/path/to/model_ema_bf16.pt \
61
+ --input_video_root=/path/to/eval_data \
62
+ --save_root=/path/to/output \
63
+ --resolution 480,832 \
64
+ --guidance 0 \
65
+ --chunk_size 12 \
66
+ --fps_downsample_ratio 2 \
67
+ --save_fps 10 \
68
+ --vae_path /path/to/Wan2.1_VAE.pth \
69
+ --text_encoder_path /path/to/Cosmos-Reason1-7B \
70
+ --qwen_path /path/to/Qwen2.5-VL-7B-Instruct
71
+ ```
72
+
73
+ ### Input Data Format
74
+
75
+ The inference script expects a directory containing paired files:
76
+ - `episode_XXXXXX.mp4` - Input video (first frame used as conditional)
77
+ - `episode_XXXXXX_actions.npy` - Action array of shape `(T, 29)` in numpy format
78
+
79
+ ### Eval Data Preparation
80
+
81
+ Use `scripts/prepare_gr1_eval_data.py` to extract eval episodes from a LeRobot dataset:
82
+
83
+ ```bash
84
+ python scripts/prepare_gr1_eval_data.py \
85
+ --dataset-path /path/to/GR1_robot \
86
+ --output-dir /path/to/eval_data \
87
+ --num-episodes 100
88
+ ```
89
+
90
+ ## Examples
91
+
92
+ The `examples/` directory contains sample predictions alongside ground truth videos:
93
+
94
+ | Episode | Prediction | Ground Truth |
95
+ |---------|-----------|--------------|
96
+ | 000000 | [episode_000000.mp4](examples/episode_000000.mp4) | [episode_000000.mp4](examples/gt/episode_000000.mp4) |
97
+ | 000008 | [episode_000008.mp4](examples/episode_000008.mp4) | [episode_000008.mp4](examples/gt/episode_000008.mp4) |
98
+ | 000087 | [episode_000087.mp4](examples/episode_000087.mp4) | [episode_000087.mp4](examples/gt/episode_000087.mp4) |
99
+
100
+ ## File Structure
101
+
102
+ ```
103
+ .
104
+ ├── model_ema_bf16.pt # Model weights (EMA, bf16)
105
+ ├── inference/
106
+ │ ├── inference_gr00t.py # Main inference script
107
+ │ └── inference_pipeline.py # Inference pipeline (ActionVideo2WorldInference)
108
+ ├── scripts/
109
+ │ ├── eval_gr1_robot.sh # One-command eval script
110
+ │ ├── prepare_gr1_eval_data.py # Eval data preparation
111
+ │ └── convert_distcp_to_pt.py # DCP -> PT checkpoint converter
112
+ └── examples/
113
+ ├── episode_000000.mp4 # Predicted videos
114
+ ├── episode_000008.mp4
115
+ ├── episode_000087.mp4
116
+ └── gt/ # Ground truth videos
117
+ ├── episode_000000.mp4
118
+ ├── episode_000008.mp4
119
+ └── episode_000087.mp4
120
+ ```
121
+
122
+ ## Limitations
123
+
124
+ - Trained only on GR1 dual-arm robot data; may not generalize to other embodiments
125
+ - Prediction quality degrades for long-horizon generation (many chunks)
126
+ - Episodes with fewer than 12 actions cannot be processed (chunk_size=12)
127
+ - No classifier-free guidance (guidance=0) works best for this model
128
+
129
+ ## License
130
+
131
+ Apache-2.0 (same as base Cosmos-Predict2.5)
132
+
133
+ ## Acknowledgements
134
+
135
+ - Base model: [NVIDIA Cosmos-Predict2.5](https://github.com/NVIDIA/Cosmos-Predict2.5)
136
+ - Training data: GR1 teleoperation dataset (LeRobot format)
137
+ - VAE: [Wan2.1](https://huggingface.co/Wan-AI/Wan2.1-T2V-14B)
examples/episode_000000.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:a5f14c2d6e41c7eeae80f354d5b094ab7eccbf32620bd45225959f7ab05ff277
3
+ size 199034
examples/episode_000008.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:6c1b85ee2929a2616e7f2cd45bb3667b70dc4e4382270c4164d3c1074f121439
3
+ size 394188
examples/episode_000087.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:1da2555b494710a119c08760aebbf744e4b281874e01a215a66d19590e44e2d1
3
+ size 623808
examples/gt/episode_000000.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:bd87b160860d6fdd47846eb9839af86d43684c59e640be88d1df1e120a200066
3
+ size 530414
examples/gt/episode_000008.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:9ac15a966e56f983a29037b57efc5c8f5ad5ec2a026e5e4d07f7b3471aad407c
3
+ size 540443
examples/gt/episode_000087.mp4 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ec40150f03290095fca08842f89ae4cd18c6b7651053bdfa24f6a83517285aef
3
+ size 1202359
inference/inference_gr00t.py ADDED
@@ -0,0 +1,389 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+ """
17
+
18
+
19
+ # ---------------------------------- benchmark ----------------------------------
20
+
21
+
22
+ CUDA_VISIBLE_DEVICES=0 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
23
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame \
24
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame/checkpoints/iter_000014000 \
25
+ --input_video_root results/gr00t_gr1/gt \
26
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame-14k\
27
+ --resolution 480,832 --guidance 0 --chunk_size 12
28
+
29
+ CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
30
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame \
31
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame/checkpoints/iter_000020000 \
32
+ --input_video_root results/gr00t_gr1/gt \
33
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame-20k\
34
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 80 --end 100
35
+
36
+ CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
37
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame \
38
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame/checkpoints/iter_000028000 \
39
+ --input_video_root results/gr00t_gr1/gt \
40
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame-28k\
41
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
42
+
43
+ CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
44
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
45
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000004000 \
46
+ --input_video_root results/gr00t_gr1/gt \
47
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-4k\
48
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 80 --end 100
49
+
50
+ CUDA_VISIBLE_DEVICES=0 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
51
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full \
52
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full/checkpoints/iter_000004000 \
53
+ --input_video_root results/gr00t_gr1/gt \
54
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full-6k\
55
+ --resolution 480,832 --guidance 0 --chunk_size 48 --start 80 --end 100
56
+
57
+ CUDA_VISIBLE_DEVICES=0 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
58
+ --experiment=cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full \
59
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full/checkpoints/iter_000004000 \
60
+ --input_video_root results/gr00t_gr1/gt \
61
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full-4k\
62
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
63
+
64
+ CUDA_VISIBLE_DEVICES=1 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
65
+ --experiment=cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full \
66
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full/checkpoints/iter_000008000 \
67
+ --input_video_root results/gr00t_gr1/gt \
68
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_conditioned_posttrained_rl_merged_action_gr00t_gr1_customized_13frame_full-8k\
69
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
70
+
71
+ CUDA_VISIBLE_DEVICES=6 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
72
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full \
73
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full/checkpoints/iter_000010000 \
74
+ --input_video_root results/gr00t_gr1/gt \
75
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_49frame_full-10k\
76
+ --resolution 480,832 --guidance 0 --chunk_size 48 --start 90 --end 100
77
+
78
+ CUDA_VISIBLE_DEVICES=6 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
79
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_73frame_full \
80
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_73frame_full/checkpoints/iter_000006000 \
81
+ --input_video_root results/gr00t_gr1/gt \
82
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_73frame_full-6k\
83
+ --resolution 480,832 --guidance 0 --chunk_size 72 --start 70 --end 80
84
+
85
+ CUDA_VISIBLE_DEVICES=5 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
86
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
87
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000008000 \
88
+ --input_video_root results/gr00t_gr1/gt \
89
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-8k\
90
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 75 --end 100
91
+
92
+
93
+ CUDA_VISIBLE_DEVICES=6 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
94
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
95
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000010000 \
96
+ --input_video_root results/gr00t_gr1/gt \
97
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-10k\
98
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 90 --end 100
99
+
100
+ CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
101
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
102
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000014000 \
103
+ --input_video_root results/gr00t_gr1/gt \
104
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-14k\
105
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 90 --end 100
106
+
107
+
108
+ CUDA_VISIBLE_DEVICES=7 PYTHONPATH=. python cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
109
+ --experiment=cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes \
110
+ --ckpt_path s3://bucket/cosmos_predict2_action_conditioned/action_conditional/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes/checkpoints/iter_000016000 \
111
+ --input_video_root results/gr00t_gr1/gt \
112
+ --save_root results/gr00t_gr1/cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes-16k\
113
+ --resolution 480,832 --guidance 0 --chunk_size 12 --start 0 --end 100
114
+ """
115
+
116
+ import argparse
117
+ import os
118
+ from glob import glob
119
+
120
+ import mediapy
121
+ import numpy as np
122
+ import torch
123
+ from loguru import logger
124
+
125
+ from cosmos_predict2._src.imaginaire.utils import distributed
126
+ from cosmos_predict2._src.predict2.action.inference.inference_pipeline import (
127
+ _DEFAULT_NEGATIVE_PROMPT,
128
+ ActionVideo2WorldInference,
129
+ )
130
+
131
+ _IMAGE_EXTENSIONS = [".png", ".jpg", ".jpeg", "webp"]
132
+ _VIDEO_EXTENSIONS = [".mp4"]
133
+
134
+ _ACTION_SCALER = 20.0
135
+
136
+
137
+ def parse_arguments() -> argparse.Namespace:
138
+ """Parses command-line arguments for the Video2World inference script."""
139
+ parser = argparse.ArgumentParser(description="Image2World/Video2World inference script")
140
+ parser.add_argument("--experiment", type=str, required=True, help="Experiment config")
141
+ parser.add_argument("--chunk_size", type=int, default=12, help="Chunk size for action conditioning")
142
+ parser.add_argument(
143
+ "--num_chunks", type=int, default=12, help="Number of chunks to generate (-1 for all available chunks)"
144
+ )
145
+ parser.add_argument("--guidance", type=int, default=7, help="Guidance value")
146
+ parser.add_argument("--seed", type=int, default=1, help="Guidance value")
147
+ parser.add_argument(
148
+ "--ckpt_path",
149
+ type=str,
150
+ default="",
151
+ help="Path to the checkpoint. If not provided, will use the one specify in the config",
152
+ )
153
+ parser.add_argument("--s3_cred", type=str, default="credentials/s3_checkpoint.secret")
154
+ parser.add_argument(
155
+ "--resolution",
156
+ type=str,
157
+ default="none",
158
+ help="Resolution of the video (H,W). Be default it will use model trained resolution. 9:16",
159
+ )
160
+ parser.add_argument("--input_video_root", type=str, default="bridge/annotation/test_100", help="Action root")
161
+ parser.add_argument("--save_root", type=str, default="results/image2world", help="Save root")
162
+
163
+ # for pi dataset
164
+ parser.add_argument("--camera_id", type=str, default="base", help="Camera id")
165
+ parser.add_argument("--start", type=int, default=0)
166
+ parser.add_argument("--end", type=int, default=100)
167
+ parser.add_argument("--fps_downsample_ratio", type=int, default=1)
168
+ parser.add_argument("--gripper_scale", type=float, default=1.0)
169
+ parser.add_argument("--gripper_key", type=str, default="continuous_gripper_state", help="Gripper key")
170
+ parser.add_argument("--state_key", type=str, default="state", help="State key")
171
+
172
+ parser.add_argument("--reverse", action="store_true", help="Reverse the video")
173
+ parser.add_argument("--single_chunk", action="store_true", help="Single chunk")
174
+ parser.add_argument("--start_frame_idx", type=int, default=0, help="Start frame index")
175
+ parser.add_argument("--save_fps", type=int, default=10, help="Save fps")
176
+
177
+ parser.add_argument(
178
+ "--negative_prompt",
179
+ type=str,
180
+ default=_DEFAULT_NEGATIVE_PROMPT,
181
+ help="Custom negative prompt for classifier-free guidance. If not specified, uses default embeddings from S3.",
182
+ )
183
+ parser.add_argument(
184
+ "--num_latent_conditional_frames",
185
+ type=int,
186
+ default=1,
187
+ help="Number of latent conditional frames (0, 1 or 2). For images, both values work by duplicating frames. For videos, uses the first N frames.",
188
+ )
189
+ # Context parallel arguments
190
+ parser.add_argument(
191
+ "--context_parallel_size",
192
+ type=int,
193
+ default=1,
194
+ help="Context parallel size (number of GPUs to split context over). Set to 8 for 8 GPUs",
195
+ )
196
+ # Local model path overrides (avoid downloading from HuggingFace/S3)
197
+ parser.add_argument("--vae_path", type=str, default="", help="Local path to Wan2.1 VAE .pth file")
198
+ parser.add_argument("--text_encoder_path", type=str, default="", help="Local path to Cosmos-Reason1-7B directory")
199
+ parser.add_argument("--qwen_path", type=str, default="", help="Local path to Qwen2.5-VL-7B-Instruct directory")
200
+ parser.add_argument("--experiment_opts", type=str, nargs="*", default=[], help="Additional Hydra override strings")
201
+ return parser.parse_args()
202
+
203
+
204
+ def get_action_sequence_from_states(
205
+ data,
206
+ fps_downsample_ratio=1,
207
+ use_quat=False,
208
+ state_key="state",
209
+ gripper_scale=1.0,
210
+ gripper_key="continuous_gripper_state",
211
+ ):
212
+ """
213
+ Get the action sequence from the states.
214
+ """
215
+
216
+ actions = np.array(data["action"])[::fps_downsample_ratio][:-1]
217
+ return actions
218
+
219
+
220
+ def get_video_id(img_path: str):
221
+ """Extract video ID from image path by removing directory and extension."""
222
+ return img_path.split("/")[-1].split(".")[0]
223
+
224
+
225
+ def main():
226
+ torch.enable_grad(False) # Disable gradient calculations for inference
227
+ args = parse_arguments()
228
+
229
+ # Validate num_latent_conditional_frames at the very beginning
230
+ if args.num_latent_conditional_frames not in [0, 1, 2]:
231
+ raise ValueError(
232
+ f"num_latent_conditional_frames must be 0, 1 or 2, but got {args.num_latent_conditional_frames}"
233
+ )
234
+
235
+ # Determine supported extensions based on num_latent_conditional_frames
236
+ if args.num_latent_conditional_frames > 1:
237
+ supported_extensions = _VIDEO_EXTENSIONS
238
+ # Check if input folder contains any videos
239
+ has_videos = False
240
+ for file_name in os.listdir(args.input_root):
241
+ file_ext = os.path.splitext(file_name)[1].lower()
242
+ if file_ext in _VIDEO_EXTENSIONS:
243
+ has_videos = True
244
+ break
245
+
246
+ if not has_videos:
247
+ raise ValueError(
248
+ f"num_latent_conditional_frames={args.num_latent_conditional_frames} > 1 requires video inputs, "
249
+ f"but no videos found in {args.input_root}. Found extensions: "
250
+ f"{set(os.path.splitext(f)[1].lower() for f in os.listdir(args.input_root) if os.path.splitext(f)[1])}"
251
+ )
252
+
253
+ logger.info(f"Using video-only mode with {args.num_latent_conditional_frames} conditional frames")
254
+ elif args.num_latent_conditional_frames == 1:
255
+ supported_extensions = _IMAGE_EXTENSIONS + _VIDEO_EXTENSIONS
256
+ logger.info(f"Using image+video mode with {args.num_latent_conditional_frames} conditional frame")
257
+
258
+ # Initialize the inference handler with context parallel support
259
+ # Build experiment_opts from local path overrides
260
+ experiment_opts = list(args.experiment_opts)
261
+ if args.vae_path:
262
+ experiment_opts.append(f"++model.config.tokenizer.vae_pth={args.vae_path}")
263
+ logger.info(f"Using local VAE: {args.vae_path}")
264
+ if args.text_encoder_path:
265
+ experiment_opts.append(f"++model.config.text_encoder_config.ckpt_path={args.text_encoder_path}")
266
+ logger.info(f"Using local text encoder: {args.text_encoder_path}")
267
+ if args.qwen_path:
268
+ experiment_opts.append(f"++model.config.text_encoder_config.model_config.tokenizer.tokenizer_type={args.qwen_path}")
269
+ logger.info(f"Using local Qwen tokenizer: {args.qwen_path}")
270
+
271
+ video2world_cli = ActionVideo2WorldInference(
272
+ args.experiment,
273
+ args.ckpt_path,
274
+ args.s3_cred,
275
+ context_parallel_size=args.context_parallel_size,
276
+ experiment_opts=experiment_opts if experiment_opts else None,
277
+ )
278
+
279
+ mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
280
+ logger.info(f"GPU memory usage after model dcp.load: {mem_bytes / (1024**3):.2f} GB")
281
+
282
+ # get input video and annotation path
283
+ input_video_path = os.path.join(args.input_video_root)
284
+
285
+ # Only process files on rank 0 if using distributed processing
286
+ rank0 = True
287
+ if args.context_parallel_size > 1:
288
+ rank0 = distributed.get_rank() == 0
289
+
290
+ # pdb.set_trace()
291
+ video_list = glob(os.path.join(input_video_path, "*.mp4"))
292
+ input_json_list = [video_path.replace(".mp4", "_actions.npy") for video_path in video_list]
293
+
294
+ # Ensure save directory exists
295
+ os.makedirs(args.save_root, exist_ok=True)
296
+
297
+ # Process each file in the input directory
298
+ for annotation_path, video_path in zip(input_json_list[args.start : args.end], video_list[args.start : args.end]):
299
+ actions = np.load(annotation_path)
300
+
301
+ # Convert camera_id to integer if it's a string and can be converted to an integer
302
+
303
+ actions = actions[: len(actions)]
304
+ video_array = mediapy.read_video(video_path)
305
+
306
+ # Resize video_array with arg.resolution if specified
307
+ if args.resolution != "none":
308
+ try:
309
+ h, w = map(int, args.resolution.split(","))
310
+ video_array = np.stack([mediapy.resize_image(frame, (h, w)) for frame in video_array], axis=0)
311
+ except Exception as e:
312
+ logger.warning(f"Failed to resize video to {args.resolution}: {e}")
313
+
314
+ img_array = video_array[args.start_frame_idx]
315
+ # img_name = annotation_path.split("/")[-1].split(".")[0]
316
+ img_name = video_path.split("/")[-1].split(".")[0]
317
+
318
+ frames = [img_array]
319
+ chunk_video = []
320
+ video_array = video_array[:: args.fps_downsample_ratio]
321
+
322
+ video_name = f"{args.save_root}/{img_name.replace('.jpg', '.mp4')}"
323
+ chunk_video_name = f"{args.save_root}/{img_name + '.mp4'}"
324
+ logger.info(f"Saving video to {video_name}")
325
+ if os.path.exists(chunk_video_name):
326
+ logger.info(f"Video already exists: {chunk_video_name}")
327
+ continue
328
+
329
+ # Calculate the maximum number of chunks to generate
330
+ max_chunks = len(actions) // args.chunk_size
331
+ if args.num_chunks > 0:
332
+ max_chunks = min(max_chunks, args.num_chunks)
333
+
334
+ logger.info(f"Generating {max_chunks} chunks (chunk_size={args.chunk_size}, total_actions={len(actions)})")
335
+
336
+ chunk_count = 0
337
+ for i in range(args.start_frame_idx, len(actions), args.chunk_size):
338
+ if actions[i : i + args.chunk_size].shape[0] != args.chunk_size:
339
+ break
340
+
341
+ # Check if we've reached the desired number of chunks
342
+ if args.num_chunks > 0 and chunk_count >= args.num_chunks:
343
+ logger.info(f"Reached target number of chunks ({args.num_chunks}), stopping generation")
344
+ break
345
+
346
+ logger.info(f"Generating chunk {chunk_count + 1}/{max_chunks}")
347
+ next_img_array, video_clamped = video2world_cli.step_inference(
348
+ img_array=img_array,
349
+ action=actions[i : i + args.chunk_size],
350
+ guidance=args.guidance,
351
+ seed=i,
352
+ )
353
+ frames.append(next_img_array)
354
+ img_array = next_img_array
355
+ chunk_video.append(video_clamped)
356
+ chunk_count += 1
357
+
358
+ if args.single_chunk:
359
+ break
360
+
361
+ if not chunk_video:
362
+ logger.warning(f"Skipping {img_name}: not enough actions for a full chunk ({len(actions)} < {args.chunk_size})")
363
+ continue
364
+
365
+ chunk_list = [chunk_video[0]] + [chunk_video[i][: args.chunk_size] for i in range(1, len(chunk_video))]
366
+ chunk_video = np.concatenate(chunk_list, axis=0)
367
+ if args.single_chunk:
368
+ chunk_video_name = f"{args.save_root}/{img_name + '_single_chunk.mp4'}"
369
+ else:
370
+ # chunk_video_name = f"{args.save_root}/{img_name + '_chunk.mp4'}"
371
+ chunk_video_name = f"{args.save_root}/{img_name + '.mp4'}"
372
+ mediapy.write_video(chunk_video_name, chunk_video, fps=args.save_fps)
373
+
374
+ # concat_video = np.concatenate([chunk_video, video_array[: chunk_video.shape[0]]], axis=2)
375
+ # concat_video_name = f"{args.save_root}/{img_name + '_concat.mp4'}"
376
+ # mediapy.write_video(concat_video_name, concat_video, fps=args.save_fps)
377
+
378
+ logger.info(f"Saved video to {chunk_video_name}")
379
+
380
+ # Synchronize all processes before cleanup
381
+ if args.context_parallel_size > 1:
382
+ torch.distributed.barrier()
383
+
384
+ # Clean up distributed resources
385
+ video2world_cli.cleanup()
386
+
387
+
388
+ if __name__ == "__main__":
389
+ main()
inference/inference_pipeline.py ADDED
@@ -0,0 +1,436 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+
16
+
17
+ import numpy as np
18
+ import torch
19
+ import torch.distributed as dist
20
+ import torchvision
21
+ from loguru import logger
22
+ from megatron.core import parallel_state
23
+
24
+ from cosmos_predict2._src.imaginaire.utils import distributed
25
+ from cosmos_predict2._src.interactive.utils.model_loader import (
26
+ load_model_from_checkpoint as load_distilled_model_from_checkpoint,
27
+ )
28
+ from cosmos_predict2._src.predict2.inference.get_t5_emb import get_text_embedding
29
+ from cosmos_predict2._src.predict2.utils.model_loader import load_model_from_checkpoint
30
+
31
+ _DEFAULT_NEGATIVE_PROMPT = "The video captures a series of frames showing ugly scenes, static with no motion, motion blur, over-saturation, shaky footage, low resolution, grainy texture, pixelated images, poorly lit areas, underexposed and overexposed scenes, poor color balance, washed out colors, choppy sequences, jerky movements, low frame rate, artifacting, color banding, unnatural transitions, outdated special effects, fake elements, unconvincing visuals, poorly edited content, jump cuts, visual noise, and flickering. Overall, the video is of poor quality."
32
+
33
+
34
+ class ActionVideo2WorldInference:
35
+ """
36
+ Handles the Video2World inference process, including model loading, data preparation,
37
+ and video generation from an image/video and text prompt. Now supports context parallelism
38
+ and distilled model inference.
39
+ """
40
+
41
+ def __init__(
42
+ self,
43
+ experiment_name: str,
44
+ ckpt_path: str,
45
+ s3_credential_path: str,
46
+ context_parallel_size: int = 1,
47
+ distilled: bool = False,
48
+ num_steps: int = 4,
49
+ experiment_opts: list[str] | None = None,
50
+ ):
51
+ """
52
+ Initializes the Video2WorldInference class.
53
+
54
+ Loads the diffusion model and its configuration based on the provided
55
+ experiment name and checkpoint path. Sets up distributed processing if needed.
56
+
57
+ Args:
58
+ experiment_name (str): Name of the experiment configuration.
59
+ ckpt_path (str): Path to the model checkpoint (local or S3).
60
+ s3_credential_path (str): Path to S3 credentials file (if loading from S3).
61
+ context_parallel_size (int): Number of GPUs for context parallelism.
62
+ distilled (bool): Whether to load a distilled model (DMD2).
63
+ num_steps (int): Number of diffusion steps for inference (default 4 for distilled models).
64
+ experiment_opts (list[str] | None): Hydra-style override strings (e.g. "++model.config.tokenizer.vae_pth=/path").
65
+ """
66
+ self.experiment_name = experiment_name
67
+ self.ckpt_path = ckpt_path
68
+ self.s3_credential_path = s3_credential_path
69
+ self.context_parallel_size = context_parallel_size
70
+ self.process_group = None
71
+ self.distilled = distilled
72
+ self.num_steps = num_steps
73
+ self._experiment_opts = experiment_opts
74
+
75
+ # Initialize distributed processing if context parallel size > 1
76
+ if self.context_parallel_size > 1:
77
+ self._init_distributed()
78
+
79
+ # Choose the appropriate config file and loader based on whether we're loading a distilled model
80
+ if self.distilled:
81
+ config_file = "cosmos_predict2/_src/interactive/configs/registry_predict2p5.py"
82
+ logger.info(f"Loading distilled model with config: {config_file}")
83
+ # Use the cosmos3 loader for distilled models (DMD2)
84
+ model, config = load_distilled_model_from_checkpoint(
85
+ experiment_name=self.experiment_name,
86
+ s3_checkpoint_dir=self.ckpt_path,
87
+ config_file=config_file,
88
+ load_ema_to_reg=True,
89
+ )
90
+ else:
91
+ config_file = "cosmos_predict2/_src/predict2/action/configs/action_conditioned/config.py"
92
+ # Load the model and config using predict2 loader
93
+ model, config = load_model_from_checkpoint(
94
+ experiment_name=self.experiment_name,
95
+ s3_checkpoint_dir=self.ckpt_path,
96
+ config_file=config_file,
97
+ load_ema_to_reg=True,
98
+ experiment_opts=getattr(self, "_experiment_opts", None),
99
+ )
100
+
101
+ # For distilled models, set net_fake_score to None (not needed for inference)
102
+ if self.distilled and hasattr(model, "net_fake_score"):
103
+ logger.info("Setting net_fake_score to None for distilled model inference")
104
+ model.net_fake_score = None
105
+
106
+ # Enable context parallel on the model if using context parallelism
107
+ if self.context_parallel_size > 1:
108
+ model.net.enable_context_parallel(self.process_group)
109
+
110
+ self.model = model
111
+ self.config = config
112
+ self.batch_size = 1
113
+ self.neg_t5_embeddings = None
114
+
115
+ def _init_distributed(self):
116
+ """Initialize distributed processing for context parallelism."""
117
+
118
+ # Initialize distributed environment
119
+ distributed.init()
120
+
121
+ # Initialize model parallel states
122
+ parallel_state.initialize_model_parallel(
123
+ context_parallel_size=self.context_parallel_size,
124
+ )
125
+
126
+ # Get the process group for context parallel
127
+ self.process_group = parallel_state.get_context_parallel_group()
128
+
129
+ logger.info(f"Initialized context parallel with size {self.context_parallel_size}")
130
+ logger.info(f"Current rank: {distributed.get_rank()}, World size: {distributed.get_world_size()}")
131
+
132
+ def _get_data_batch_input(
133
+ self,
134
+ video: torch.Tensor,
135
+ prompt: str,
136
+ num_conditional_frames: int = 1,
137
+ negative_prompt: str = _DEFAULT_NEGATIVE_PROMPT,
138
+ use_neg_prompt: bool = True,
139
+ ):
140
+ """
141
+ Prepares the input data batch for the diffusion model.
142
+
143
+ Constructs a dictionary containing the video tensor, text embeddings,
144
+ and other necessary metadata required by the model's forward pass.
145
+ Optionally includes negative text embeddings.
146
+
147
+ Args:
148
+ video (torch.Tensor): The input video tensor (B, C, T, H, W).
149
+ prompt (str): The text prompt for conditioning.
150
+ num_conditional_frames (int): Number of conditional frames to use.
151
+ negative_prompt (str, optional): Custom negative prompt.
152
+ use_neg_prompt (bool, optional): Whether to include negative prompt embeddings. Defaults to True.
153
+
154
+ Returns:
155
+ dict: A dictionary containing the prepared data batch, moved to the correct device and dtype.
156
+ """
157
+ B, C, T, H, W = video.shape
158
+
159
+ data_batch = {
160
+ "dataset_name": "video_data",
161
+ "video": video,
162
+ "fps": torch.randint(16, 32, (self.batch_size,)).float(), # Random FPS (might be used by model)
163
+ "padding_mask": torch.zeros(self.batch_size, 1, H, W), # Padding mask (assumed no padding here)
164
+ "num_conditional_frames": num_conditional_frames, # Specify number of conditional frames
165
+ }
166
+
167
+ if use_neg_prompt:
168
+ assert negative_prompt is not None, "Negative prompt is required when use_neg_prompt is True"
169
+
170
+ # Compute text embeddings
171
+ if self.model.text_encoder is not None:
172
+ data_batch["ai_caption"] = [prompt]
173
+ data_batch["t5_text_embeddings"] = self.model.text_encoder.compute_text_embeddings_online(
174
+ data_batch={"ai_caption": [prompt], "images": None},
175
+ input_caption_key="ai_caption",
176
+ )
177
+ if use_neg_prompt:
178
+ data_batch["neg_t5_text_embeddings"] = self.model.text_encoder.compute_text_embeddings_online(
179
+ data_batch={"ai_caption": [negative_prompt], "images": None},
180
+ input_caption_key="ai_caption",
181
+ )
182
+ else:
183
+ data_batch["t5_text_embeddings"] = get_text_embedding(prompt)
184
+ if use_neg_prompt:
185
+ data_batch["neg_t5_text_embeddings"] = get_text_embedding(negative_prompt)
186
+
187
+ # Move tensors to GPU and convert to bfloat16 if they are floating point
188
+ for k, v in data_batch.items():
189
+ if isinstance(v, torch.Tensor) and torch.is_floating_point(data_batch[k]):
190
+ data_batch[k] = v.cuda().to(dtype=torch.bfloat16)
191
+
192
+ return data_batch
193
+
194
+ def step_inference_with_latents(
195
+ self,
196
+ img_array: np.ndarray,
197
+ action: np.ndarray = None,
198
+ guidance: int = 3,
199
+ seed: int = 1,
200
+ num_latent_conditional_frames: int = 1,
201
+ query_steps: list[int] = None,
202
+ ):
203
+ """
204
+ Runs a single inference step to generate the next video frame and the full video given an input image and action.
205
+ Returns intermediate latents for analysis.
206
+
207
+ Note: For distilled models, this method falls back to standard inference without latent collection
208
+ as distilled models use a different sampling process.
209
+ """
210
+
211
+ num_video_frames = action.shape[0] + 1
212
+
213
+ img_tensor = torchvision.transforms.functional.to_tensor(img_array).unsqueeze(0)
214
+ vid_input = torch.cat([img_tensor, torch.zeros_like(img_tensor).repeat(num_video_frames - 1, 1, 1, 1)], dim=0)
215
+ vid_input = (vid_input * 255.0).to(torch.uint8) # Convert to uint8 range if needed (might depend on model)
216
+ vid_input = vid_input.unsqueeze(0).permute(0, 2, 1, 3, 4) # Add batch dim B=1 and permute
217
+
218
+ # Prepare the data batch with text embeddings
219
+ data_batch = self._get_data_batch_input(
220
+ vid_input,
221
+ prompt="",
222
+ num_conditional_frames=num_latent_conditional_frames,
223
+ negative_prompt="",
224
+ use_neg_prompt=False,
225
+ )
226
+
227
+ data_batch["action"] = torch.from_numpy(action).cuda().to(dtype=torch.bfloat16)[None, ...]
228
+
229
+ mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
230
+ logger.info(f"GPU memory usage after getting data_batch: {mem_bytes / (1024**3):.2f} GB")
231
+
232
+ # Generate latent samples using the diffusion model
233
+ if self.distilled:
234
+ # Distilled model inference: use generate_samples_from_batch (no latent collection)
235
+ logger.info(f"Running distilled inference with {self.num_steps} steps (latent collection not supported)")
236
+ sample = self.model.generate_samples_from_batch(
237
+ data_batch,
238
+ n_sample=1,
239
+ seed=seed,
240
+ num_steps=self.num_steps,
241
+ )
242
+ latents_to_save = None # Distilled models don't support latent collection
243
+ else:
244
+ # Teacher model inference with latent collection
245
+ sample, latents_to_save = self.model.generate_samples_with_latents_from_batch(
246
+ data_batch,
247
+ n_sample=1, # Generate one sample
248
+ guidance=guidance,
249
+ seed=seed, # Fixed seed for reproducibility
250
+ is_negative_prompt=True, # Use classifier-free guidance
251
+ query_steps=query_steps,
252
+ )
253
+
254
+ # Decode the latent sample into a video tensor
255
+ video = self.model.decode(sample)
256
+
257
+ video_normalized = (video - (-1)) / (1 - (-1))
258
+ video_clamped = (torch.clamp(video_normalized[0], 0, 1) * 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
259
+ next_frame = torch.clamp(video_normalized[0, :, -1, :, :], 0, 1)
260
+ next_frame = (next_frame * 255).to(torch.uint8).permute(1, 2, 0).cpu().numpy()
261
+ return next_frame, video_clamped, latents_to_save
262
+
263
+ def step_inference(
264
+ self,
265
+ img_array: np.ndarray,
266
+ action: np.ndarray = None,
267
+ guidance: int = 3,
268
+ seed: int = 1,
269
+ num_latent_conditional_frames: int = 1,
270
+ ):
271
+ """
272
+ Runs a single inference step to generate the next video frame and the full video given an input image and action.
273
+
274
+ Args:
275
+ img_array (np.ndarray): Input image as a numpy array (H, W, C), typically the first frame.
276
+ action (np.ndarray, optional): Action vector to condition the model. Should be shape (action_dim,) or (chunk_size, action_dim).
277
+ guidance (int, optional): Guidance scale for classifier-free guidance. Default is 3.
278
+ seed (int, optional): Random seed for reproducibility. Default is 1.
279
+ num_latent_conditional_frames (int, optional): Number of conditional frames to use for the model. Default is 1.
280
+
281
+ Returns:
282
+ next_frame (np.ndarray): The next predicted frame as a numpy array (H, W, C), uint8.
283
+ video_clamped (np.ndarray): The generated video as a numpy array (T, H, W, C), uint8.
284
+ """
285
+ num_video_frames = action.shape[0] + 1
286
+
287
+ img_tensor = torchvision.transforms.functional.to_tensor(img_array).unsqueeze(0) # (1, H, W, C)
288
+ vid_input = torch.cat([img_tensor, torch.zeros_like(img_tensor).repeat(num_video_frames - 1, 1, 1, 1)], dim=0)
289
+ vid_input = (vid_input * 255.0).to(torch.uint8) # Convert to uint8 range if needed (might depend on model)
290
+ vid_input = vid_input.unsqueeze(0).permute(0, 2, 1, 3, 4) # Add batch dim B=1 and permute
291
+
292
+ # Prepare the data batch with text embeddings
293
+ data_batch = self._get_data_batch_input(
294
+ vid_input,
295
+ prompt="",
296
+ num_conditional_frames=num_latent_conditional_frames,
297
+ negative_prompt="",
298
+ use_neg_prompt=False,
299
+ )
300
+
301
+ data_batch["action"] = torch.from_numpy(action).cuda().to(dtype=torch.bfloat16)[None, ...]
302
+
303
+ mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
304
+ logger.info(f"GPU memory usage after getting data_batch: {mem_bytes / (1024**3):.2f} GB")
305
+
306
+ # Generate latent samples using the diffusion model
307
+ # For distilled models, use num_steps; for teacher models, use guidance with more steps
308
+ if self.distilled:
309
+ # Distilled model inference: use fewer steps, no guidance needed
310
+ logger.info(f"Running distilled inference with {self.num_steps} steps")
311
+ sample = self.model.generate_samples_from_batch(
312
+ data_batch,
313
+ n_sample=1,
314
+ seed=seed,
315
+ num_steps=self.num_steps,
316
+ )
317
+ else:
318
+ # Teacher model inference: use guidance with standard sampling
319
+ sample = self.model.generate_samples_from_batch(
320
+ data_batch,
321
+ n_sample=1, # Generate one sample
322
+ guidance=guidance,
323
+ seed=seed, # Fixed seed for reproducibility
324
+ is_negative_prompt=True, # Use classifier-free guidance
325
+ )
326
+
327
+ # Decode the latent sample into a video tensor
328
+ video = self.model.decode(sample)
329
+
330
+ video_normalized = (video - (-1)) / (1 - (-1))
331
+ video_clamped = (torch.clamp(video_normalized[0], 0, 1) * 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
332
+ next_frame = torch.clamp(video_normalized[0, :, -1, :, :], 0, 1)
333
+ next_frame = (next_frame * 255).to(torch.uint8).permute(1, 2, 0).cpu().numpy()
334
+ return next_frame, video_clamped
335
+
336
+ def step_inference_multi_frame(
337
+ self,
338
+ video_array: np.ndarray,
339
+ action: np.ndarray = None,
340
+ guidance: int = 3,
341
+ seed: int = 1,
342
+ num_latent_conditional_frames: int = 2,
343
+ ):
344
+ """
345
+ Runs a single inference step to generate the next video frame and the full video given an input image and action.
346
+
347
+ Args:
348
+ video_array (np.ndarray): Input video as a numpy array (T, H, W, C).
349
+ action (np.ndarray, optional): Action vector to condition the model. Should be shape (action_dim,) or (chunk_size, action_dim).
350
+
351
+ guidance (int, optional): Guidance scale for classifier-free guidance. Default is 3.
352
+ seed (int, optional): Random seed for reproducibility. Default is 1.
353
+ num_latent_conditional_frames (int, optional): Number of conditional frames to use for the model. Default is 1.
354
+
355
+ Returns:
356
+ next_frame (np.ndarray): The next predicted frame as a numpy array (H, W, C), uint8.
357
+ video_clamped (np.ndarray): The generated video as a numpy array (T, H, W, C), uint8.
358
+ """
359
+ num_video_frames = action.shape[0] + 1 + (num_latent_conditional_frames - 1) * 4
360
+ num_cond_image_frames = (num_latent_conditional_frames - 1) * 4 + 1
361
+
362
+ assert num_cond_image_frames == video_array.shape[0], (
363
+ "Number of conditional frames is not equal to the number of frames in the video"
364
+ )
365
+ assert action.shape[0] == num_video_frames - num_cond_image_frames, (
366
+ "Number of action frames is not equal to the number of frames in the video"
367
+ )
368
+
369
+ video_tensor = torch.stack(
370
+ [torchvision.transforms.functional.to_tensor(v) for v in video_array]
371
+ ) # (T, C, H, W)
372
+ vid_input = torch.cat(
373
+ [
374
+ video_tensor,
375
+ torch.zeros_like(video_tensor[0][None, ...]).repeat(num_video_frames - num_cond_image_frames, 1, 1, 1),
376
+ ],
377
+ dim=0,
378
+ )
379
+ vid_input = (vid_input * 255.0).to(torch.uint8) # Convert to uint8 range if needed (might depend on model)
380
+ vid_input = vid_input.unsqueeze(0).permute(0, 2, 1, 3, 4) # Add batch dim B=1 and permute
381
+
382
+ # Prepare the data batch with text embeddings
383
+ data_batch = self._get_data_batch_input(
384
+ vid_input,
385
+ prompt="",
386
+ num_conditional_frames=num_latent_conditional_frames,
387
+ negative_prompt="",
388
+ use_neg_prompt=False,
389
+ )
390
+
391
+ zero_action = np.zeros(
392
+ (4 * (num_latent_conditional_frames - 1), action.shape[1])
393
+ ) # (4 * (num_latent_conditional_frames-1), action_dim)
394
+ action_padded = np.concatenate([zero_action, action], axis=0)
395
+
396
+ data_batch["action"] = torch.from_numpy(action_padded).cuda().to(dtype=torch.bfloat16)[None, ...]
397
+
398
+ mem_bytes = torch.cuda.memory_allocated(device=torch.device("cuda" if torch.cuda.is_available() else "cpu"))
399
+ logger.info(f"GPU memory usage after getting data_batch: {mem_bytes / (1024**3):.2f} GB")
400
+
401
+ # Generate latent samples using the diffusion model
402
+ # For distilled models, use num_steps; for teacher models, use guidance with more steps
403
+ if self.distilled:
404
+ # Distilled model inference: use fewer steps, no guidance needed
405
+ logger.info(f"Running distilled inference with {self.num_steps} steps")
406
+ sample = self.model.generate_samples_from_batch(
407
+ data_batch,
408
+ n_sample=1,
409
+ seed=seed,
410
+ num_steps=self.num_steps,
411
+ )
412
+ else:
413
+ # Teacher model inference: use guidance with standard sampling
414
+ sample = self.model.generate_samples_from_batch(
415
+ data_batch,
416
+ n_sample=1, # Generate one sample
417
+ guidance=guidance,
418
+ seed=seed, # Fixed seed for reproducibility
419
+ is_negative_prompt=True, # Use classifier-free guidance
420
+ )
421
+
422
+ # Decode the latent sample into a video tensor
423
+ video = self.model.decode(sample)
424
+
425
+ video_normalized = (video - (-1)) / (1 - (-1))
426
+ video_clamped = (torch.clamp(video_normalized[0], 0, 1) * 255).to(torch.uint8).permute(1, 2, 3, 0).cpu().numpy()
427
+ next_frame = torch.clamp(video_normalized[0, :, -1, :, :], 0, 1)
428
+ next_frame = (next_frame * 255).to(torch.uint8).permute(1, 2, 0).cpu().numpy()
429
+ return next_frame, video_clamped
430
+
431
+ def cleanup(self):
432
+ """Clean up distributed resources."""
433
+ if self.context_parallel_size > 1:
434
+ if parallel_state.is_initialized():
435
+ parallel_state.destroy_model_parallel()
436
+ dist.destroy_process_group()
model_ema_bf16.pt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:94d4a1491a58ae392e4ef7059589f1295503063c9f5407091e2e66544f598e0a
3
+ size 4256645510
scripts/convert_distcp_to_pt.py ADDED
@@ -0,0 +1,134 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env -S uv run --script
2
+ # SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3
+ # SPDX-License-Identifier: Apache-2.0
4
+ #
5
+ # Licensed under the Apache License, Version 2.0 (the "License");
6
+ # you may not use this file except in compliance with the License.
7
+ # You may obtain a copy of the License at
8
+ #
9
+ # http://www.apache.org/licenses/LICENSE-2.0
10
+ #
11
+ # Unless required by applicable law or agreed to in writing, software
12
+ # distributed under the License is distributed on an "AS IS" BASIS,
13
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ # See the License for the specific language governing permissions and
15
+ # limitations under the License.
16
+
17
+ # /// script
18
+ # requires-python = ">=3.10"
19
+ # dependencies = [
20
+ # "numpy",
21
+ # "s5cmd",
22
+ # "torch",
23
+ # "tyro",
24
+ # ]
25
+ # [tool.uv.sources]
26
+ # torch = [{ index = "pytorch" }]
27
+ # [[tool.uv.index]]
28
+ # name = "pytorch"
29
+ # url = "https://download.pytorch.org/whl/cpu"
30
+ # explicit = true
31
+ # ///
32
+
33
+ """Download distributed checkpoint from S3 and convert to pytorch checkpoint.
34
+
35
+ Usage:
36
+
37
+ ```python
38
+ ./scripts/convert_distcp_to_pt.py "s3://bucket/cosmos_predict2_multiview/cosmos2_mv/buttercup_predict2p5_2b_mv_7views_res720p_fps30_t8_from16kfps10mv_jointalpamayov2mads720pmulticaps29frames-0/checkpoints/iter_000028000" "checkpoints/buttercup_predict2p5_2b_mv_7views_res720p_fps30_t8_from16kfps10mv_jointalpamayov2mads720pmulticaps29frames-0_iter_000028000"
39
+ ```
40
+ """
41
+
42
+ import shlex
43
+ import subprocess
44
+ from dataclasses import dataclass
45
+ from pathlib import Path
46
+ from typing import Any
47
+
48
+ import torch
49
+ import tyro
50
+ from torch.distributed.checkpoint.format_utils import dcp_to_torch_save
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class Args:
55
+ input_dir: tyro.conf.Positional[str]
56
+ """S3 URI of the checkpoint or path to the distcp directory."""
57
+ output_dir: tyro.conf.Positional[Path]
58
+ """Output directory to save the converted checkpoints."""
59
+
60
+ ema: bool = True
61
+ """Export EMA weights."""
62
+
63
+ s3_args: str | None = None
64
+ """Additional arguments to pass to s5cmd."""
65
+
66
+
67
+ def main():
68
+ args = tyro.cli(Args, description=__doc__)
69
+
70
+ pt_path = args.output_dir / "model.pt"
71
+ pt_path.unlink(missing_ok=True)
72
+ pt_ema_fp32_path = args.output_dir / "model_ema_fp32.pt"
73
+ pt_ema_fp32_path.unlink(missing_ok=True)
74
+ pt_ema_bf16_path = args.output_dir / "model_ema_bf16.pt"
75
+ pt_ema_bf16_path.unlink(missing_ok=True)
76
+
77
+ if args.input_dir.startswith("s3://"):
78
+ input_s3 = args.input_dir.rstrip("/")
79
+ input_s3 = input_s3.removesuffix("/model")
80
+ distcp_dir = args.output_dir / "model"
81
+ print(f"Downloading distcp to {distcp_dir}...")
82
+ # Create the directory if it doesn't exist
83
+ distcp_dir.mkdir(parents=True, exist_ok=True)
84
+ # Use sync only if directory exists and has files, otherwise use cp
85
+ cmd = ["s5cmd"]
86
+ if args.s3_args:
87
+ cmd.extend(shlex.split(args.s3_args))
88
+ if distcp_dir.exists() and any(distcp_dir.iterdir()):
89
+ cmd.extend(["sync", "--exit-on-error"])
90
+ else:
91
+ cmd.extend(["cp", "--show-progress"])
92
+ cmd.extend(
93
+ [
94
+ f"{input_s3}/model/*",
95
+ f"{distcp_dir}",
96
+ ]
97
+ )
98
+ print(shlex.join(cmd))
99
+ subprocess.run(cmd, check=True)
100
+ print(f"Downloaded distcp to '{distcp_dir}'")
101
+ else:
102
+ distcp_dir = Path(args.input_dir)
103
+
104
+ # Convert distributed checkpoint to torch single checkpoint
105
+ dcp_to_torch_save(distcp_dir, pt_path)
106
+ print(f"Converted '{distcp_dir}' to '{pt_path}'")
107
+
108
+ if not args.ema:
109
+ return
110
+
111
+ # Drop Reg keys and save EMA weights only in fp32 precision
112
+ state_dict: dict[str, Any] = torch.load(pt_path, map_location="cpu", weights_only=False)
113
+ state_dict_ema_fp32: dict[str, Any] = {}
114
+ for key, value in state_dict.items():
115
+ if key.startswith("net_ema."):
116
+ key = key.replace("net_ema.", "net.")
117
+ state_dict_ema_fp32[key] = value
118
+ if not state_dict_ema_fp32:
119
+ raise ValueError("Model doesn't contain EMA weights")
120
+ torch.save(state_dict_ema_fp32, pt_ema_fp32_path)
121
+ print(f"Saved EMA fp32 weights from '{pt_path}' to '{pt_ema_fp32_path}'")
122
+
123
+ # Save EMA weights only in bf16 precision
124
+ state_dict_ema_bf16: dict[str, Any] = {}
125
+ for key, value in state_dict_ema_fp32.items():
126
+ if isinstance(value, torch.Tensor) and value.dtype == torch.float32:
127
+ value = value.bfloat16()
128
+ state_dict_ema_bf16[key] = value
129
+ torch.save(state_dict_ema_bf16, pt_ema_bf16_path)
130
+ print(f"fp32 -> bf16: '{pt_ema_fp32_path}' to '{pt_ema_bf16_path}'")
131
+
132
+
133
+ if __name__ == "__main__":
134
+ main()
scripts/eval_gr1_robot.sh ADDED
@@ -0,0 +1,245 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+ # =============================================================================
3
+ # Cosmos-Predict2.5 Action-Conditioned Evaluation on GR1 Robot Data
4
+ # =============================================================================
5
+ #
6
+ # 一键评估:指定模型路径(DCP 目录或 _ema_bf16.pt),自动完成转换 + 推理。
7
+ #
8
+ # 使用方式 (在 GPU 机器上):
9
+ # bash scripts/eval_gr1_robot.sh /path/to/checkpoint
10
+ # bash scripts/eval_gr1_robot.sh /path/to/model_ema_bf16.pt
11
+ # bash scripts/eval_gr1_robot.sh /path/to/iter_000002000 # DCP 目录
12
+ # bash scripts/eval_gr1_robot.sh /path/to/iter_000002000 --debug
13
+ # bash scripts/eval_gr1_robot.sh /path/to/model_ema_bf16.pt --num-episodes 50
14
+ #
15
+ # 选项:
16
+ # --num-episodes N 评估 episode 数量 (default: 100)
17
+ # --output-dir PATH 输出目录
18
+ # --guidance N CFG guidance 值 (default: 0)
19
+ # --num-gpus N 使用 GPU 数量 (default: 1)
20
+ # --experiment NAME Hydra 实验名 (default: 自动推断)
21
+ # --debug 调试模式 (5 episodes, 1 GPU)
22
+ # -h, --help 显示帮助
23
+ # =============================================================================
24
+
25
+ set -euo pipefail
26
+
27
+ # ---------------------------------------------------------------------------
28
+ # 默认配置
29
+ # ---------------------------------------------------------------------------
30
+ NUM_EPISODES=100
31
+ GUIDANCE=0
32
+ OUTPUT_ROOT="/inspire/ssd/project/security-defense-and-attack/25015/world_model/output"
33
+ DATASET_PATH="/inspire/ssd/project/security-defense-and-attack/25015/world_model/data/PhysicalAI-Robotics-GR00T-Teleop-GR1/GR1_robot"
34
+ COSMOS_ROOT="/inspire/ssd/project/security-defense-and-attack/25015/cosmos-predict2.5"
35
+ NUM_GPUS=1
36
+ DEBUG=0
37
+ EXPERIMENT_NAME="cosmos_predict2p5_2B_action_conditioned_gr00t_gr1_customized_13frame_full_16nodes_release_oss"
38
+ CHECKPOINT_INPUT=""
39
+
40
+ # ---------------------------------------------------------------------------
41
+ # 解析参数 (第一个位置参数为 checkpoint 路径)
42
+ # ---------------------------------------------------------------------------
43
+ if [ $# -eq 0 ]; then
44
+ echo "用法: bash scripts/eval_gr1_robot.sh <checkpoint_path> [选项]"
45
+ echo ""
46
+ echo "checkpoint_path 可以是:"
47
+ echo " - DCP 目录 (如 .../checkpoints/iter_000002000)"
48
+ echo " - _ema_bf16.pt 文件 (如 .../iter_000002000/model_ema_bf16.pt)"
49
+ echo ""
50
+ sed -n '3,18p' "$0"
51
+ exit 1
52
+ fi
53
+
54
+ CHECKPOINT_INPUT="$1"
55
+ shift
56
+
57
+ while [[ $# -gt 0 ]]; do
58
+ case $1 in
59
+ --num-episodes) NUM_EPISODES="$2"; shift 2 ;;
60
+ --output-dir) OUTPUT_ROOT="$2"; shift 2 ;;
61
+ --guidance) GUIDANCE="$2"; shift 2 ;;
62
+ --num-gpus) NUM_GPUS="$2"; shift 2 ;;
63
+ --experiment) EXPERIMENT_NAME="$2"; shift 2 ;;
64
+ --debug) DEBUG=1; shift ;;
65
+ -h|--help)
66
+ sed -n '3,18p' "$0"
67
+ exit 0
68
+ ;;
69
+ *) echo "Unknown option: $1"; exit 1 ;;
70
+ esac
71
+ done
72
+
73
+ # 调试模式覆盖
74
+ if [ "$DEBUG" -eq 1 ]; then
75
+ NUM_EPISODES=5
76
+ NUM_GPUS=1
77
+ echo "=== DEBUG MODE ==="
78
+ fi
79
+
80
+ # ---------------------------------------------------------------------------
81
+ # Step 1: 确定 _ema_bf16.pt 路径 (自动转换 DCP → PT)
82
+ # ---------------------------------------------------------------------------
83
+ if [ ! -e "$CHECKPOINT_INPUT" ]; then
84
+ echo "ERROR: 路径不存在: ${CHECKPOINT_INPUT}"
85
+ exit 1
86
+ fi
87
+
88
+ if [ -f "$CHECKPOINT_INPUT" ]; then
89
+ # 已经是 .pt 文件,直接使用
90
+ CHECKPOINT_PATH="$CHECKPOINT_INPUT"
91
+ echo "使用已有 PT checkpoint: ${CHECKPOINT_PATH}"
92
+ else
93
+ # 是目录,需要找到或转换 _ema_bf16.pt
94
+ CKPT_DIR="$CHECKPOINT_INPUT"
95
+
96
+ # 检查是否已有 _ema_bf16.pt
97
+ if [ -f "${CKPT_DIR}/model_ema_bf16.pt" ]; then
98
+ CHECKPOINT_PATH="${CKPT_DIR}/model_ema_bf16.pt"
99
+ echo "找到已有 PT checkpoint: ${CHECKPOINT_PATH}"
100
+ elif [ -d "${CKPT_DIR}/model" ]; then
101
+ # DCP 格式,需要转换
102
+ echo "DCP checkpoint,开始转换..."
103
+ echo " 输入: ${CKPT_DIR}/model/"
104
+ echo " 输出: ${CKPT_DIR}/model_ema_bf16.pt"
105
+ echo ""
106
+
107
+ cd "${COSMOS_ROOT}"
108
+ python scripts/convert_distcp_to_pt.py \
109
+ "${CKPT_DIR}/model" \
110
+ "${CKPT_DIR}"
111
+
112
+ if [ -f "${CKPT_DIR}/model_ema_bf16.pt" ]; then
113
+ CHECKPOINT_PATH="${CKPT_DIR}/model_ema_bf16.pt"
114
+ echo ""
115
+ echo "转换成功: ${CHECKPOINT_PATH}"
116
+ else
117
+ echo "ERROR: DCP → PT 转换失败"
118
+ exit 1
119
+ fi
120
+ else
121
+ echo "ERROR: 目录中既没有 model_ema_bf16.pt 也没有 model/ 子目录"
122
+ echo " 内容: $(ls "$CKPT_DIR")"
123
+ exit 1
124
+ fi
125
+ fi
126
+
127
+ # ---------------------------------------------------------------------------
128
+ # Step 2: 准备评估数据
129
+ # ---------------------------------------------------------------------------
130
+ EVAL_DATA_DIR="${OUTPUT_ROOT}/eval/gr1_eval_data"
131
+
132
+ if [ ! -d "${EVAL_DATA_DIR}" ] || [ -z "$(ls -A "${EVAL_DATA_DIR}" 2>/dev/null)" ]; then
133
+ echo ""
134
+ echo "准备评估数据..."
135
+ cd "${COSMOS_ROOT}"
136
+ python scripts/prepare_gr1_eval_data.py \
137
+ --dataset-path "${DATASET_PATH}" \
138
+ --output-dir "${EVAL_DATA_DIR}" \
139
+ --num-episodes "${NUM_EPISODES}"
140
+ else
141
+ echo "评估数据已存在: ${EVAL_DATA_DIR}"
142
+ fi
143
+
144
+ # ---------------------------------------------------------------------------
145
+ # Step 3: 设置环境变量
146
+ # ---------------------------------------------------------------------------
147
+ export IMAGINAIRE_OUTPUT_ROOT="${OUTPUT_ROOT}"
148
+ export HF_HOME="${HF_HOME:-${OUTPUT_ROOT}/hf_cache}"
149
+
150
+ # 设置 HF 离线模式,防止尝试下载
151
+ export HF_HUB_OFFLINE=1
152
+ export TRANSFORMERS_OFFLINE=1
153
+
154
+ # ---------------------------------------------------------------------------
155
+ # Step 4: 运行推理
156
+ # ---------------------------------------------------------------------------
157
+ cd "${COSMOS_ROOT}"
158
+
159
+ PREDICT_DIR="${OUTPUT_ROOT}/eval/gr1_predicted"
160
+ mkdir -p "${PREDICT_DIR}"
161
+
162
+ # 日志文件
163
+ LOG_DIR="${OUTPUT_ROOT}/logs"
164
+ mkdir -p "${LOG_DIR}"
165
+ LOG_FILE="${LOG_DIR}/eval_$(date +%Y%m%d_%H%M%S).log"
166
+
167
+ echo ""
168
+ echo "==================================================================="
169
+ echo "Cosmos-Predict2.5 Action-Conditioned Evaluation (GR1 Robot)"
170
+ echo "==================================================================="
171
+ echo " Checkpoint: ${CHECKPOINT_PATH}"
172
+ echo " Experiment: ${EXPERIMENT_NAME}"
173
+ echo " 评估数据: ${EVAL_DATA_DIR}"
174
+ echo " 预测输出: ${PREDICT_DIR}"
175
+ echo " GPU 数量: ${NUM_GPUS}"
176
+ echo " Episode 数: ${NUM_EPISODES}"
177
+ echo " Guidance: ${GUIDANCE}"
178
+ echo " 日志: ${LOG_FILE}"
179
+ echo "==================================================================="
180
+
181
+ # 统计输入文件数
182
+ NUM_MP4=$(ls "${EVAL_DATA_DIR}"/*.mp4 2>/dev/null | wc -l)
183
+ echo " 输入视频数: ${NUM_MP4}"
184
+ echo ""
185
+
186
+ # 设置 CUDA 可见设备
187
+ if [ "$NUM_GPUS" -eq 1 ]; then
188
+ CUDA_DEVICES="0"
189
+ else
190
+ CUDA_DEVICES=$(seq -s, 0 $((NUM_GPUS - 1)))
191
+ fi
192
+
193
+ # ---------------------------------------------------------------------------
194
+ # 本地模型路径 (与训练脚本一致,避免从 HF/S3 下载)
195
+ # ---------------------------------------------------------------------------
196
+ VAE_PATH="/inspire/ssd/project/security-defense-and-attack/25015/world_model/Wan2.1-VAE/Wan2.1_VAE.pth"
197
+ REASON1_PATH="/inspire/ssd/project/security-defense-and-attack/25015/world_model/Cosmos-Reason1-7B"
198
+ QWEN_PATH="/inspire/ssd/project/security-defense-and-attack/25015/models/Qwen2.5-VL-7B-Instruct"
199
+
200
+ # 构建推理命令的本地路径参数
201
+ LOCAL_PATH_ARGS=""
202
+ if [ -f "$VAE_PATH" ]; then
203
+ LOCAL_PATH_ARGS="${LOCAL_PATH_ARGS} --vae_path ${VAE_PATH}"
204
+ echo " 使用本地 VAE: ${VAE_PATH}"
205
+ else
206
+ echo " WARNING: 本地 VAE 不存在: ${VAE_PATH}"
207
+ fi
208
+ if [ -d "$REASON1_PATH" ]; then
209
+ LOCAL_PATH_ARGS="${LOCAL_PATH_ARGS} --text_encoder_path ${REASON1_PATH}"
210
+ echo " 使用本地 Cosmos-Reason1-7B: ${REASON1_PATH}"
211
+ else
212
+ echo " WARNING: 本地 Cosmos-Reason1-7B 不存在: ${REASON1_PATH}"
213
+ fi
214
+ if [ -d "$QWEN_PATH" ]; then
215
+ LOCAL_PATH_ARGS="${LOCAL_PATH_ARGS} --qwen_path ${QWEN_PATH}"
216
+ echo " 使用本地 Qwen2.5-VL-7B-Instruct: ${QWEN_PATH}"
217
+ else
218
+ echo " WARNING: 本地 Qwen2.5-VL-7B-Instruct 不存在: ${QWEN_PATH}"
219
+ fi
220
+
221
+ echo ""
222
+ echo "启动推理..."
223
+
224
+ CUDA_VISIBLE_DEVICES=${CUDA_DEVICES} PYTHONPATH=. python \
225
+ cosmos_predict2/_src/predict2/action/inference/inference_gr00t.py \
226
+ --experiment="${EXPERIMENT_NAME}" \
227
+ --ckpt_path="${CHECKPOINT_PATH}" \
228
+ --input_video_root="${EVAL_DATA_DIR}" \
229
+ --save_root="${PREDICT_DIR}" \
230
+ --resolution 480,832 \
231
+ --guidance ${GUIDANCE} \
232
+ --chunk_size 12 \
233
+ --start 0 \
234
+ --end ${NUM_EPISODES} \
235
+ --fps_downsample_ratio 2 \
236
+ --save_fps 10 \
237
+ ${LOCAL_PATH_ARGS} \
238
+ 2>&1 | tee "${LOG_FILE}"
239
+
240
+ echo ""
241
+ echo "==================================================================="
242
+ echo "评估完成!"
243
+ echo " 预测视频: ${PREDICT_DIR}"
244
+ echo " 日志: ${LOG_FILE}"
245
+ echo "==================================================================="
scripts/prepare_gr1_eval_data.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Prepare GR1 robot evaluation data for Cosmos-Predict2.5 inference.
4
+
5
+ Extracts episodes from the LeRobot dataset and creates paired
6
+ episode_XXXXXX.mp4 + episode_XXXXXX_actions.npy
7
+ files in the format expected by inference_gr00t.py.
8
+
9
+ Usage:
10
+ python scripts/prepare_gr1_eval_data.py
11
+ python scripts/prepare_gr1_eval_data.py --num-episodes 50 --start-episode 100
12
+ python scripts/prepare_gr1_eval_data.py --output-dir /path/to/output
13
+ """
14
+
15
+ import argparse
16
+ import json
17
+ import os
18
+
19
+ import numpy as np
20
+ import pyarrow.parquet as pq
21
+
22
+
23
+ def main():
24
+ parser = argparse.ArgumentParser(description="Prepare GR1 eval data for inference")
25
+ parser.add_argument(
26
+ "--dataset-path",
27
+ default="/inspire/ssd/project/security-defense-and-attack/25015/world_model/data/PhysicalAI-Robotics-GR00T-Teleop-GR1/GR1_robot",
28
+ )
29
+ parser.add_argument("--output-dir", default=None, help="Output directory (default: {dataset_path}/../eval/gr1_eval_data)")
30
+ parser.add_argument("--num-episodes", type=int, default=100, help="Number of episodes to extract")
31
+ parser.add_argument("--start-episode", type=int, default=0, help="First episode index")
32
+ parser.add_argument("--fps-downsample-ratio", type=int, default=2, help="Downsample ratio (20fps -> 10fps)")
33
+ parser.add_argument("--action-dim", type=int, default=29, help="Action dimension (29 for GR1 selected keys)")
34
+ args = parser.parse_args()
35
+
36
+ dataset_path = args.dataset_path
37
+ output_dir = args.output_dir or os.path.join(os.path.dirname(dataset_path), "eval", "gr1_eval_data")
38
+ os.makedirs(output_dir, exist_ok=True)
39
+
40
+ # GR1 action key selection: left_arm(0:7), left_hand(7:13), right_arm(22:29), right_hand(29:35), waist(41:44)
41
+ action_indices = list(range(0, 7)) + list(range(7, 13)) + list(range(22, 29)) + list(range(29, 35)) + list(range(41, 44))
42
+
43
+ # Read episodes metadata
44
+ episodes_path = os.path.join(dataset_path, "meta", "episodes.jsonl")
45
+ episodes = []
46
+ with open(episodes_path) as f:
47
+ for line in f:
48
+ if line.strip():
49
+ episodes.append(json.loads(line))
50
+
51
+ total_episodes = len(episodes)
52
+ end_episode = min(args.start_episode + args.num_episodes, total_episodes)
53
+ selected = episodes[args.start_episode : end_episode]
54
+
55
+ print(f"Dataset: {dataset_path}")
56
+ print(f"Total episodes: {total_episodes}")
57
+ print(f"Selected: episodes {args.start_episode} - {end_episode - 1} ({len(selected)} episodes)")
58
+ print(f"Output: {output_dir}")
59
+ print(f"Action dim: {args.action_dim} (from 44-dim raw, indices {action_indices})")
60
+ print(f"FPS downsample ratio: {args.fps_downsample_ratio}")
61
+ print()
62
+
63
+ # Read info.json for chunk size
64
+ with open(os.path.join(dataset_path, "meta", "info.json")) as f:
65
+ info = json.load(f)
66
+ chunk_size = info.get("chunks_size", 1000) # episodes per chunk
67
+
68
+ skipped = 0
69
+ for i, ep in enumerate(selected):
70
+ ep_idx = ep["episode_index"]
71
+ chunk_idx = ep_idx // chunk_size
72
+
73
+ # Video path
74
+ video_key = "observation.images.ego_view_freq20"
75
+ video_src = os.path.join(
76
+ dataset_path,
77
+ "videos",
78
+ f"chunk-{chunk_idx:03d}",
79
+ video_key,
80
+ f"episode_{ep_idx:06d}.mp4",
81
+ )
82
+
83
+ # Parquet path
84
+ parquet_path = os.path.join(
85
+ dataset_path,
86
+ "data",
87
+ f"chunk-{chunk_idx:03d}",
88
+ f"episode_{ep_idx:06d}.parquet",
89
+ )
90
+
91
+ if not os.path.exists(video_src):
92
+ print(f" [SKIP] ep {ep_idx}: video not found: {video_src}")
93
+ skipped += 1
94
+ continue
95
+ if not os.path.exists(parquet_path):
96
+ print(f" [SKIP] ep {ep_idx}: parquet not found: {parquet_path}")
97
+ skipped += 1
98
+ continue
99
+
100
+ # Symlink video
101
+ video_dst = os.path.join(output_dir, f"episode_{ep_idx:06d}.mp4")
102
+ if os.path.islink(video_dst) or os.path.exists(video_dst):
103
+ os.remove(video_dst)
104
+ os.symlink(os.path.abspath(video_src), video_dst)
105
+
106
+ # Extract actions from parquet
107
+ table = pq.read_table(parquet_path)
108
+ actions_raw = np.array(table.column("action").to_pylist()) # (T, 44)
109
+
110
+ # Select 29 dims and downsample
111
+ actions_selected = actions_raw[:, action_indices] # (T, 29)
112
+ actions_downsampled = actions_selected[:: args.fps_downsample_ratio][:-1] # drop last, matching inference_gr00t.py
113
+
114
+ # Save NPY
115
+ npy_path = os.path.join(output_dir, f"episode_{ep_idx:06d}_actions.npy")
116
+ np.save(npy_path, actions_downsampled)
117
+
118
+ if (i + 1) % 20 == 0 or i == 0:
119
+ print(f" [{i + 1}/{len(selected)}] ep {ep_idx}: {actions_downsampled.shape[0]} steps, {actions_downsampled.shape[1]} dims")
120
+
121
+ print()
122
+ print(f"Done. {len(selected) - skipped} episodes prepared, {skipped} skipped.")
123
+ print(f"Output directory: {output_dir}")
124
+
125
+
126
+ if __name__ == "__main__":
127
+ main()