zhicao commited on
Commit
fbd9366
·
verified ·
1 Parent(s): dc4ae0d

Upload dreamzero source code (no model checkpoints)

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .gitattributes +3 -0
  2. .gitignore +246 -0
  3. COPYRIGHT +4 -0
  4. LICENSE +21 -0
  5. README.md +194 -0
  6. assets/dataset_analysis.png +3 -0
  7. assets/dataset_overview.png +3 -0
  8. assets/hardware_setup.png +3 -0
  9. assets/trex_camera_calib.json +51 -0
  10. docs/DATASET_TO_GEAR_AND_TRAIN.md +471 -0
  11. docs/DROID_CONVERSION.md +62 -0
  12. docs/TREX_TRACK_FORCE_MODEL.md +301 -0
  13. docs/WAN22_BACKBONE.md +167 -0
  14. eval_utils/policy_client.py +93 -0
  15. eval_utils/policy_server.py +130 -0
  16. eval_utils/run_sim_eval.py +219 -0
  17. eval_utils/serve_dreamzero_wan22.py +391 -0
  18. groot/__init__.py +0 -0
  19. groot/control/__init__.py +0 -0
  20. groot/control/tensorrt_utils.py +852 -0
  21. groot/vla/__init__.py +0 -0
  22. groot/vla/common/__init__.py +1 -0
  23. groot/vla/common/utils/__init__.py +3 -0
  24. groot/vla/common/utils/data_structure/__init__.py +2 -0
  25. groot/vla/common/utils/data_structure/shape_utils.py +283 -0
  26. groot/vla/common/utils/data_structure/tree_utils.py +219 -0
  27. groot/vla/common/utils/io/__init__.py +6 -0
  28. groot/vla/common/utils/io/config_utils.py +260 -0
  29. groot/vla/common/utils/io/file_utils.py +707 -0
  30. groot/vla/common/utils/io/hdf5_utils.py +84 -0
  31. groot/vla/common/utils/io/json_utils.py +270 -0
  32. groot/vla/common/utils/io/print_utils.py +362 -0
  33. groot/vla/common/utils/io/termcolor.py +186 -0
  34. groot/vla/common/utils/misc/__init__.py +5 -0
  35. groot/vla/common/utils/misc/array_tensor_utils.py +372 -0
  36. groot/vla/common/utils/misc/functional_utils.py +635 -0
  37. groot/vla/common/utils/misc/image_utils.py +225 -0
  38. groot/vla/common/utils/misc/misc_utils.py +261 -0
  39. groot/vla/common/utils/misc/torch_utils.py +748 -0
  40. groot/vla/common/utils/misc/video_utils.py +487 -0
  41. groot/vla/configs/conf.yaml +151 -0
  42. groot/vla/configs/data/dreamzero/agibot_relative.yaml +54 -0
  43. groot/vla/configs/data/dreamzero/base_48_wan_fine_aug_relative.yaml +464 -0
  44. groot/vla/configs/data/dreamzero/droid_relative.yaml +49 -0
  45. groot/vla/configs/data/dreamzero/droid_relative_wan22.yaml +51 -0
  46. groot/vla/configs/data/dreamzero/trex_relative_wan22.yaml +54 -0
  47. groot/vla/configs/data/dreamzero/trex_track_force_wan22.yaml +139 -0
  48. groot/vla/configs/data/dreamzero/yam_relative.yaml +52 -0
  49. groot/vla/configs/deepspeed/zero2.json +28 -0
  50. groot/vla/configs/deepspeed/zero2_offload.json +32 -0
.gitattributes CHANGED
@@ -33,3 +33,6 @@ 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
+ assets/dataset_analysis.png filter=lfs diff=lfs merge=lfs -text
37
+ assets/dataset_overview.png filter=lfs diff=lfs merge=lfs -text
38
+ assets/hardware_setup.png filter=lfs diff=lfs merge=lfs -text
.gitignore ADDED
@@ -0,0 +1,246 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ wandb/
6
+
7
+ # C extensions
8
+
9
+
10
+ # Distribution / packaging
11
+ .Python
12
+ build/
13
+ develop-eggs/
14
+ dist/
15
+ downloads/
16
+ eggs/
17
+ .eggs/
18
+ sdist/
19
+ var/
20
+ wheels/
21
+ share/python-wheels/
22
+ *.egg-info/
23
+ .installed.cfg
24
+ *.egg
25
+ *.swp
26
+ *.swo
27
+ gear_working_dir/
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+ cover/
54
+ /groot/control/wbc_checkpoints/
55
+ # Translations
56
+ *.mo
57
+ *.pot
58
+
59
+ # Django stuff:
60
+ *.log
61
+ local_settings.py
62
+ db.sqlite3
63
+ db.sqlite3-journal
64
+
65
+ # Flask stuff:
66
+ instance/
67
+ .webassets-cache
68
+
69
+ .vscode/
70
+ .cursor/
71
+ .cursor*
72
+ .claude/
73
+ CLAUDE.md
74
+
75
+ # Scrapy stuff:
76
+ .scrapy
77
+
78
+ # Sphinx documentation
79
+ docs/_build/
80
+
81
+ # PyBuilder
82
+ .pybuilder/
83
+ target/
84
+
85
+ # Jupyter Notebook
86
+ .ipynb_checkpoints
87
+
88
+ # IPython
89
+ profile_default/
90
+ ipython_config.py
91
+
92
+ # pyenv
93
+ # For a library or package, you might want to ignore these files since the code is
94
+ # intended to run in multiple environments; otherwise, check them in:
95
+ # .python-version
96
+
97
+ # pipenv
98
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
99
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
100
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
101
+ # install all needed dependencies.
102
+ #Pipfile.lock
103
+
104
+ # UV
105
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ #uv.lock
109
+
110
+ # poetry
111
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
112
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
113
+ # commonly ignored for libraries.
114
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
115
+ #poetry.lock
116
+
117
+ # pdm
118
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
119
+ #pdm.lock
120
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
121
+ # in version control.
122
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
123
+ .pdm.toml
124
+ .pdm-python
125
+ .pdm-build/
126
+
127
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
128
+ __pypackages__/
129
+
130
+ # Celery stuff
131
+ celerybeat-schedule
132
+ celerybeat.pid
133
+
134
+ # SageMath parsed files
135
+ *.sage.py
136
+
137
+ # Spyder project settings
138
+ .spyderproject
139
+ .spyproject
140
+
141
+ # Rope project settings
142
+ .ropeproject
143
+
144
+ # mkdocs documentation
145
+ /site
146
+
147
+ # mypy
148
+ .mypy_cache/
149
+ .dmypy.json
150
+ dmypy.json
151
+
152
+ # Pyre type checker
153
+ .pyre/
154
+
155
+ # pytype static type analyzer
156
+ .pytype/
157
+
158
+ # Cython debug symbols
159
+ cython_debug/
160
+ # IDE
161
+ .idea/
162
+ .vscode/
163
+
164
+ # log
165
+ outputs/
166
+ logs/
167
+ *logs_rl*
168
+ !external_dependencies/OpenHomie/HomieRL/legged_gym/logs/exported/policies/*.onnx
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
175
+
176
+ outputs/
177
+ osmo/
178
+ source/
179
+ groot/rl/data/
180
+
181
+ .DS_Store
182
+ sim_dependencies/
183
+ .venv/*
184
+
185
+
186
+ # Mujoco
187
+ MUJOCO_LOG.TXT
188
+ playground/
189
+
190
+ *.code-workspace
191
+ batch_*.sh
192
+ logs_*/
193
+ runs/
194
+ out/
195
+ recordings/
196
+ pyrightconfig.json
197
+ *.npz
198
+ *.nbize.py
199
+
200
+ # Git worktrees
201
+ /wt
202
+
203
+ # Gear working directory
204
+ gear_working_dir/
205
+
206
+ episode_data/
207
+
208
+ # third party packages
209
+ third_parties/
210
+
211
+ # redis related
212
+ *.rdb
213
+ download_model.py
214
+ .dockerignore
215
+
216
+ # Sysid files
217
+ plots/
218
+ sysid_data/
219
+ isaac_data/
220
+
221
+ # External dependencies
222
+ external_dependencies/ws_lidar_slam_ros2/build
223
+ external_dependencies/ws_lidar_slam_ros2/install
224
+ external_dependencies/ws_lidar_slam_ros2/log
225
+ external_dependencies/ws_slam/
226
+ external_dependencies/Livox-SDK2/
227
+ external_dependencies/mfm
228
+ # external_dependencies/genmo
229
+ external_dependencies/whole_body_tracking
230
+ inputs/
231
+ outputs/
232
+
233
+ groot/dexmg/grootrobosuite/docs/images/
234
+ external_dependencies/mfm/
235
+ dump.rdb
236
+
237
+ # Open loop eval plots
238
+ offline_open_loop_eval_plots/
239
+
240
+ # yam eval data
241
+ groot/control/envs/yam/data/eval/*
242
+ /models
243
+ /data
244
+
245
+ checkpoints/
246
+ video_pred_output*/
COPYRIGHT ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Copyright (c) 2025 NVIDIA Corporation. All rights reserved.
2
+
3
+ Licensed under the Apache License, Version 2.0.
4
+ See LICENSE for the full license text.
LICENSE ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The Regents of the University of California
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ task_categories:
6
+ - robotics
7
+ tags:
8
+ - LeRobot
9
+ - robotics
10
+ - manipulation
11
+ - tactile
12
+ - bimanual
13
+ - dexterous-manipulation
14
+ pretty_name: T-Rex Dataset
15
+ size_categories:
16
+ - 1M<n<10M
17
+ configs:
18
+ - config_name: frames
19
+ data_files:
20
+ - split: train
21
+ path: data/chunk-*/file-*.parquet
22
+ default: true
23
+ - config_name: episodes
24
+ data_files:
25
+ - split: train
26
+ path: episodes_preview.parquet
27
+ ---
28
+
29
+ # T-Rex Dataset
30
+
31
+ A large-scale, tactile-reactive bimanual manipulation dataset, collected via teleoperation on a
32
+ Dexmate Vega-1 robot with two Sharpa Wave dexterous hands. Stored as a
33
+ [LeRobotDataset v3.0](https://github.com/huggingface/lerobot).
34
+
35
+ [🌐 Project Page](https://tactile-rex.github.io/) · [✍️ Paper (arXiv)](https://arxiv.org/abs/2606.17055) · [💻 Code (T-Rex)](https://github.com/ZhuoyangLiu2005/T-Rex) · [🚀 Dataset Quickstart](https://github.com/ZhuoyangLiu2005/T-Rex/tree/main/dataset_quickstart) · [📓 Colab notebook](https://colab.research.google.com/github/ZhuoyangLiu2005/T-Rex/blob/main/dataset_quickstart/quickstart.ipynb)
36
+
37
+ <p align="center">
38
+ <img src="assets/dataset_overview.png" width="100%">
39
+ <br>
40
+ <em>One episode from each of 20 motor primitives (head-camera view, cropped to the workspace), each with a different object.</em>
41
+ </p>
42
+
43
+ <p align="center">
44
+ <img src="assets/hardware_setup.png" width="100%">
45
+ <br>
46
+ <em>Teleoperation setup: Manus gloves + VIVE trackers drive the bimanual Dexmate Vega-1 with two Sharpa Wave hands; observations come from a head-mounted ZED X Mini camera and two wide-view ZED X One S wrist cameras.</em>
47
+ </p>
48
+
49
+ ## At a glance
50
+
51
+ | | |
52
+ |---|---|
53
+ | Robot | Dexmate Vega-1 dual-arm (7 actuated joints per arm) + 2× Sharpa Wave dexterous hands (5 fingertip tactile sensors each) |
54
+ | Modalities | 3 RGB cameras · 10 raw tactile + 10 deformation tactile videos · 6-axis fingertip wrenches · joint states/targets |
55
+ | Episodes | 5,464 |
56
+ | Frames | 5,473,459 (~50 hours @ 30 fps) |
57
+ | Tasks | 5,370 language-annotated trajectories · 22 motor primitives · 207 objects |
58
+ | Format | LeRobotDataset v3.0 (`codebase_version: v3.0`) |
59
+
60
+ ## Composition
61
+
62
+ <p align="center">
63
+ <img src="assets/dataset_analysis.png" width="100%">
64
+ <br>
65
+ <em>Object categories, episodes per motor primitive, and per-object episode counts.</em>
66
+ </p>
67
+
68
+ ## Collection setup
69
+
70
+ The full teleoperation/hardware stack used to collect the dataset is open-sourced in
71
+ [`hardware_code/`](https://github.com/ZhuoyangLiu2005/T-Rex/tree/main/hardware_code) in the T-Rex repo.
72
+
73
+ - **Robot.** The Dexmate Vega-1 is a dual-arm mobile robot with 7 actuated joints per arm, here
74
+ equipped with two Sharpa Wave dexterous hands. During collection the wheels, torso, and head
75
+ joints are fixed; only the 14 arm joints and the two hands are actuated.
76
+ - **Cameras.** A head-mounted ZED X Mini stereo camera (its left monocular RGB stream is recorded)
77
+ plus two wide-view ZED X One S monocular RGB cameras mounted on the wrists, posed so the head
78
+ camera covers the full reachable workspace while the wrist cameras keep the fingers visible
79
+ without significant palm occlusion. All three streams are recorded at 640×360.
80
+ - **Tactile.** Each hand carries five fingertip tactile sensors. Per sensor, the dataset stores the
81
+ raw sensor image and the estimated deformation map (both as video), and the estimated 6-axis net
82
+ wrench (`observation.tactile_force`).
83
+ - **Teleoperation.** Manus gloves capture fingertip positions relative to the hand base, retargeted
84
+ to the Sharpa Wave hands with the manufacturer's differential-inverse-kinematics package
85
+ (Pinocchio + CasADi). Two VIVE trackers provide SE(3) wrist poses, converted to arm joint
86
+ commands via differential inverse kinematics ([Pink](https://github.com/stephane-caron/pink)),
87
+ low-pass filtered, and tracked by the manufacturer's low-level cascade PID controller. A 30 Hz
88
+ high-level thread records observations and joint-space targets while asynchronously updating a
89
+ 300 Hz low-level control thread — the dataset's 30 fps matches the high-level loop, and `action`
90
+ holds its 30 Hz joint-space targets.
91
+
92
+ ## Per-frame features
93
+
94
+ | feature | shape | description |
95
+ |---|---|---|
96
+ | `observation.state` | `(58,)` | joint **positions**, laid out `[L_arm 7 \| L_hand 22 \| R_arm 7 \| R_hand 22]` |
97
+ | `action` | `(58,)` | **target** joint positions (same layout) |
98
+ | `observation.tactile_force` | `(60,)` | per-fingertip 6-axis wrench: `(left, right) × (thumb…pinky) × (Fx, Fy, Fz, Mx, My, Mz)` |
99
+ | `observation.images.{head_left, left_wrist, right_wrist}` | `360×640×3` | scene + wrist RGB cameras |
100
+ | `observation.images.tactile_{left,right}_raw_{finger}` | `240×320` (grayscale) | raw tactile sensor images (10 = 2 hands × 5 fingers) |
101
+ | `observation.images.tactile_{left,right}_deform_{finger}` | `240×240` (grayscale) | tactile deformation fields (10) |
102
+
103
+ Finger order is `thumb, index, middle, ring, pinky`. Full per-dimension joint names are in
104
+ `meta/info.json` (`features[*].names`).
105
+
106
+ ## Per-episode metadata
107
+
108
+ `meta/episodes/*.parquet` carries language and task labels per episode, in addition to the standard
109
+ LeRobot fields (`episode_index`, `tasks`, `length`, per-feature stats, video pointers):
110
+
111
+ | field | description |
112
+ |---|---|
113
+ | `caption` | human-verified natural-language instruction (5,370 unique) |
114
+ | `motor_primitive` | one of 22 primitives (`reach`, `lift_and_place`, …) |
115
+ | `object` | canonical object name (207 unique) |
116
+ | `target` | canonical target/receptacle (only set for `lift_and_place`; null otherwise) |
117
+
118
+ ## Tactile video encoding (read before decoding)
119
+
120
+ The raw and deformation tactile videos are stored **losslessly** (`libx264 -qp 0`) because their
121
+ pixel values are physically meaningful (raw sensor images and deformation maps). They are grayscale —
122
+ the signal lives entirely in the **luma (Y) plane** — and use **full-range** `yuvj420p`, so values
123
+ span the full `0–255` with no range conversion. Decode the luma plane to recover the original `uint8`
124
+ images exactly, e.g. `frame.to_ndarray(format="gray")` in PyAV.
125
+
126
+ > **No web thumbnails for tactile.** Lossless H.264 forces the *High 4:4:4 Predictive* profile,
127
+ > which most browsers and the Hugging Face preview cannot decode — so the tactile videos do not show
128
+ > thumbnails on the dataset page. This is expected; decode them locally (ffmpeg / PyAV / torchcodec).
129
+ > The RGB cameras use standard `yuv420p` (limited range, BT.709) and preview normally.
130
+
131
+ ## Usage
132
+
133
+ ### With LeRobot
134
+
135
+ ```python
136
+ from lerobot.datasets.lerobot_dataset import LeRobotDataset
137
+
138
+ ds = LeRobotDataset("zekaiwang/trex_dataset")
139
+ frame = ds[0] # dict of tensors: observation.state, action, observation.tactile_force, images...
140
+ ```
141
+
142
+ ### Stream individual episodes (no full download)
143
+
144
+ The [**T-Rex Quick Start**](https://github.com/ZhuoyangLiu2005/T-Rex/tree/main/dataset_quickstart)
145
+ companion repo browses, inspects, and replays single episodes without downloading the full dataset,
146
+ and includes a notebook you can
147
+ [open directly in Colab](https://colab.research.google.com/github/ZhuoyangLiu2005/T-Rex/blob/main/dataset_quickstart/quickstart.ipynb).
148
+
149
+ ## Dataset viewer
150
+
151
+ The Hugging Face table viewer is manually configured (the `configs` block above) with two views:
152
+
153
+ - **`frames`** (default) — the raw per-frame `observation.state`, `action`, and
154
+ `observation.tactile_force` arrays (plus index columns). This is the actual data, and what
155
+ `datasets.load_dataset("zekaiwang/trex_dataset")` returns by default.
156
+ - **`episodes`** — one row per episode with its language `caption`, `motor_primitive`, `object`, and
157
+ `target`: a readable table of contents. Switch to it with the config dropdown, or load it with
158
+ `datasets.load_dataset("zekaiwang/trex_dataset", "episodes")`.
159
+
160
+ Videos are stored as separate `.mp4` files referenced by timestamp pointers, so they are not shown in
161
+ the table viewer — load them via LeRobot or the quick-start tools. For interactive video playback, use
162
+ the LeRobot dataset visualizer.
163
+
164
+ ## Layout
165
+
166
+ ```
167
+ data/chunk-000/file-*.parquet per-frame state / action / tactile_force (+ index columns)
168
+ videos/<key>/chunk-*/file-*.mp4 23 video streams (3 RGB + 20 tactile)
169
+ episodes_preview.parquet curated per-episode labels for the web viewer (see "Dataset viewer")
170
+ meta/info.json features, shapes, fps, codebase_version
171
+ meta/episodes/*.parquet per-episode metadata + stats + video pointers
172
+ meta/tasks.parquet task (caption) table
173
+ meta/stats.json global feature statistics
174
+ ```
175
+
176
+ ## Citation
177
+
178
+ If you find the T-Rex Dataset useful, please cite:
179
+
180
+ ```bibtex
181
+ @misc{trex2026,
182
+ title={T-Rex: Tactile-Reactive Dexterous Manipulation},
183
+ author={Dantong Niu and Zhuoyang Liu and Zekai Wang and Boning Shao and Zhao-Heng Yin and Anirudh Pai and Yuvan Sharma and Stefano Saravalle and Ruijie Zheng and Jing Wang and Ryan Punamiya and Mengda Xu and Yuqi Xie and Yunfan Jiang and Letian Fu and Konstantinos Kallidromitis and Matteo Gioia and Junyi Zhang and Jiaxin Ge and Haiwen Feng and Fabio Galasso and Wei Zhan and David M. Chan and Yutong Bai and Roei Herzig and Jiahui Lei and Fei-Fei Li and Ken Goldberg and Jitendra Malik and Pieter Abbeel and Yuke Zhu and Danfei Xu and Jim Fan and Trevor Darrell},
184
+ year={2026},
185
+ eprint={2606.17055},
186
+ archivePrefix={arXiv},
187
+ primaryClass={cs.RO},
188
+ url={https://arxiv.org/abs/2606.17055},
189
+ }
190
+ ```
191
+
192
+ ## License
193
+
194
+ Released under the **MIT License** © 2026 The Regents of the University of California. See `LICENSE`.
assets/dataset_analysis.png ADDED

Git LFS Details

  • SHA256: 725cd8b39919bd84df67a0e487d18995910ee328aafe3eed10f3b058c8933c27
  • Pointer size: 131 Bytes
  • Size of remote file: 361 kB
assets/dataset_overview.png ADDED

Git LFS Details

  • SHA256: 9b4f28523cbdad67c4b7779ecbfb3394c9f21db9ac6b42e9579a077b27bbdd06
  • Pointer size: 132 Bytes
  • Size of remote file: 3.81 MB
assets/hardware_setup.png ADDED

Git LFS Details

  • SHA256: 06579da8a21d7a860c70a5bebc3350e3fac225f3e358f23ec8e2be0f3da4b79a
  • Pointer size: 132 Bytes
  • Size of remote file: 1.32 MB
assets/trex_camera_calib.json ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "native_hw": [360, 640],
3
+ "video_hw": [180, 320],
4
+ "notes": "Default T-Rex camera calibration. Head uses URDF zed_left_camera FK; wrist uses L_ee/R_ee + T_ee_to_cam. Tune wrist mounts if projections look misaligned.",
5
+ "views": {
6
+ "head_left": {
7
+ "camera_model": "zed_x_mini_left",
8
+ "extrinsic_source": "fk_zed_left_camera",
9
+ "K_native": [
10
+ [349.5075, 0.0, 313.375],
11
+ [0.0, 349.5075, 180.47075],
12
+ [0.0, 0.0, 1.0]
13
+ ],
14
+ "dist": [-0.173733, 0.0272837, -6.10446e-05, 0.0, 0.0]
15
+ },
16
+ "left_wrist": {
17
+ "camera_model": "zed_one",
18
+ "extrinsic_source": "fk_L_ee_times_T_ee_to_cam",
19
+ "ee_frame": "L_ee",
20
+ "T_ee_to_cam": [
21
+ [0.0, 0.0, 1.0, 0.08],
22
+ [-1.0, 0.0, 0.0, 0.0],
23
+ [0.0, -1.0, 0.0, 0.02],
24
+ [0.0, 0.0, 0.0, 1.0]
25
+ ],
26
+ "K_native": [
27
+ [350.0, 0.0, 320.0],
28
+ [0.0, 350.0, 180.0],
29
+ [0.0, 0.0, 1.0]
30
+ ],
31
+ "dist": [0.0, 0.0, 0.0, 0.0, 0.0]
32
+ },
33
+ "right_wrist": {
34
+ "camera_model": "zed_one",
35
+ "extrinsic_source": "fk_R_ee_times_T_ee_to_cam",
36
+ "ee_frame": "R_ee",
37
+ "T_ee_to_cam": [
38
+ [0.0, 0.0, 1.0, 0.08],
39
+ [-1.0, 0.0, 0.0, 0.0],
40
+ [0.0, -1.0, 0.0, 0.02],
41
+ [0.0, 0.0, 0.0, 1.0]
42
+ ],
43
+ "K_native": [
44
+ [350.0, 0.0, 320.0],
45
+ [0.0, 350.0, 180.0],
46
+ [0.0, 0.0, 1.0]
47
+ ],
48
+ "dist": [0.0, 0.0, 0.0, 0.0, 0.0]
49
+ }
50
+ }
51
+ }
docs/DATASET_TO_GEAR_AND_TRAIN.md ADDED
@@ -0,0 +1,471 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Adding a New Embodiment to DreamZero
2
+
3
+ How to take a LeRobot v2 dataset for a new robot, convert it to GEAR format, define its modality config, and train a DreamZero policy.
4
+
5
+ Throughout this guide, replace `<EMBODIMENT>` with your robot's name (e.g. `myrobot`, `franka`, `aloha`).
6
+
7
+ ---
8
+
9
+ ## Overview
10
+
11
+ ```
12
+ Step 1 Convert LeRobot v2 dataset → GEAR metadata
13
+ Step 2 Register the embodiment tag
14
+ Step 3 Add modality config + transforms to base YAML
15
+ Step 4 Create a dataset YAML
16
+ Step 5 Create a training script
17
+ Step 6 Train
18
+ ```
19
+
20
+ ---
21
+
22
+ ## Step 1: Convert Dataset to GEAR Format
23
+
24
+ The converter reads a LeRobot v2 dataset and generates the metadata files DreamZero needs. It does **not** modify your parquet files or videos — it only writes to `meta/`.
25
+
26
+ ### Expected input structure
27
+
28
+ ```
29
+ your_dataset/
30
+ ├── data/
31
+ │ └── chunk-000/
32
+ │ ├── episode_000000.parquet
33
+ │ └── ...
34
+ ├── videos/
35
+ │ └── chunk-000/
36
+ │ ├── observation.images.cam0/
37
+ │ │ ├── episode_000000.mp4
38
+ │ │ └── ...
39
+ │ └── observation.images.cam1/
40
+ │ └── ...
41
+ └── meta/
42
+ └── info.json # must contain: features, total_episodes, fps
43
+ ```
44
+
45
+ ### Run the converter
46
+
47
+ ```bash
48
+ python scripts/data/convert_lerobot_to_gear.py \
49
+ --dataset-path /path/to/your_dataset \
50
+ --embodiment-tag <EMBODIMENT> \
51
+ --state-keys '{"joint_pos": [0, 6], "gripper_pos": [6, 7]}' \
52
+ --action-keys '{"joint_pos": [0, 6], "gripper_pos": [6, 7]}' \
53
+ --relative-action-keys joint_pos gripper_pos \
54
+ --task-key annotation.task
55
+ ```
56
+
57
+ `--state-keys` and `--action-keys` tell the converter how to split a packed vector column into named sub-keys. The JSON maps sub-key name → `[start_index, end_index]`. Omit these flags to let the converter auto-detect.
58
+
59
+ ### Arguments
60
+
61
+ | Argument | Default | Description |
62
+ |---|---|---|
63
+ | `--dataset-path` | *(required)* | Path to the LeRobot v2 dataset |
64
+ | `--output-path` | *(in-place)* | Write to a different directory instead of in-place |
65
+ | `--embodiment-tag` | `xdof` | Tag for `meta/embodiment.json`; must match the key you use in Step 3 |
66
+ | `--state-keys` | *(auto)* | JSON: sub-key name → `[start, end]` index range |
67
+ | `--action-keys` | *(auto)* | JSON: sub-key name → `[start, end]` index range |
68
+ | `--relative-action-keys` | *(none)* | Sub-key names to compute relative action stats for |
69
+ | `--task-key` | *(auto)* | Column name for language/task annotations |
70
+ | `--fps` | *(from info.json)* | Override dataset FPS |
71
+ | `--action-horizon` | `24` | Horizon for relative stats computation |
72
+ | `--force` | `false` | Overwrite existing metadata files |
73
+
74
+ ### Generated files
75
+
76
+ The converter creates these under `meta/`:
77
+
78
+ | File | Contents |
79
+ |---|---|
80
+ | `modality.json` | Maps state, action, video, and annotation keys with index ranges and dtypes |
81
+ | `embodiment.json` | `{"embodiment_tag": "<EMBODIMENT>"}` |
82
+ | `stats.json` | Per-feature statistics (mean, std, min, max, q01, q99) |
83
+ | `relative_stats_dreamzero.json` | Relative action statistics (action − reference state) |
84
+ | `tasks.jsonl` | Unique task descriptions |
85
+ | `episodes.jsonl` | Per-episode metadata (index, tasks, length) |
86
+
87
+ ---
88
+
89
+ ## Step 2: Register the Embodiment Tag
90
+
91
+ 1. Add to the enum in `groot/vla/data/schema/embodiment_tags.py`:
92
+
93
+ ```python
94
+ class EmbodimentTag(str, Enum):
95
+ ...
96
+ MY_ROBOT = "<EMBODIMENT>"
97
+ ```
98
+
99
+ 2. Add to `VALID_EMBODIMENT_TAGS` in `scripts/data/convert_lerobot_to_gear.py` (if you want the converter to accept the tag without `--force`):
100
+
101
+ ```python
102
+ VALID_EMBODIMENT_TAGS = [
103
+ ...,
104
+ "<EMBODIMENT>",
105
+ ]
106
+ ```
107
+
108
+ ---
109
+
110
+ ## Step 3: Add Modality Config and Transforms
111
+
112
+ Edit `groot/vla/configs/data/dreamzero/base_48_wan_fine_aug_relative.yaml`.
113
+
114
+ ### 3a. Understanding modality
115
+
116
+ **Modality** connects your dataset columns to the training pipeline. The `modality.json` from Step 1 contains entries like:
117
+
118
+ ```json
119
+ {
120
+ "state": {
121
+ "joint_pos": {"original_key": "observation.state", "start": 0, "end": 6},
122
+ "gripper_pos": {"original_key": "observation.state", "start": 6, "end": 7}
123
+ },
124
+ "action": {
125
+ "joint_pos": {"original_key": "action", "start": 0, "end": 6},
126
+ "gripper_pos": {"original_key": "action", "start": 6, "end": 7}
127
+ },
128
+ "video": {
129
+ "cam0": {"original_key": "observation.images.cam0"}
130
+ },
131
+ "annotation": {
132
+ "task": {"original_key": "annotation.task"}
133
+ }
134
+ }
135
+ ```
136
+
137
+ The YAML config must reference **exactly these key names** with type prefixes:
138
+
139
+ | Modality | YAML key format | Example |
140
+ |---|---|---|
141
+ | State | `state.<name>` | `state.joint_pos` |
142
+ | Action | `action.<name>` | `action.joint_pos` |
143
+ | Video | `video.<name>` | `video.cam0` |
144
+ | Language | `annotation.<name>` | `annotation.task` |
145
+
146
+ If the YAML keys don't match `modality.json`, training will fail with missing-key errors.
147
+
148
+ ### 3b. Add `modality_config_<EMBODIMENT>`
149
+
150
+ ```yaml
151
+ modality_config_<EMBODIMENT>:
152
+ video:
153
+ _target_: groot.vla.data.dataset.ModalityConfig
154
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
155
+ eval_delta_indices: [0]
156
+ modality_keys: # one entry per camera, matching modality.json
157
+ - video.cam0
158
+ - video.cam1
159
+ - video.cam2
160
+ state:
161
+ _target_: groot.vla.data.dataset.ModalityConfig
162
+ delta_indices: [0]
163
+ modality_keys: # matching modality.json state keys
164
+ - state.joint_pos
165
+ - state.gripper_pos
166
+ action:
167
+ _target_: groot.vla.data.dataset.ModalityConfig
168
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
169
+ modality_keys: # matching modality.json action keys
170
+ - action.joint_pos
171
+ - action.gripper_pos
172
+ language:
173
+ _target_: groot.vla.data.dataset.ModalityConfig
174
+ delta_indices: [0]
175
+ modality_keys:
176
+ - annotation.task
177
+ ```
178
+
179
+ **`delta_indices` explained:**
180
+
181
+ - **Video** — frame offsets to sample (25 entries = 25 frames from the trajectory).
182
+ - **State / Language** — `[0]` = current timestep only.
183
+ - **Action** — future offsets (24 entries = 24-step action chunk).
184
+
185
+ Adjust these to match your `num_frames` and `action_horizon` training settings.
186
+
187
+ ### 3c. Add `transform_<EMBODIMENT>`
188
+
189
+ ```yaml
190
+ transform_<EMBODIMENT>:
191
+ _target_: groot.vla.data.transform.ComposedModalityTransform
192
+ transforms:
193
+ # Video
194
+ - <<: *totensor_cfg
195
+ apply_to: ${modality_config_<EMBODIMENT>.video.modality_keys}
196
+ - <<: *crop_cfg
197
+ apply_to: ${modality_config_<EMBODIMENT>.video.modality_keys}
198
+ - <<: *resize_cfg
199
+ apply_to: ${modality_config_<EMBODIMENT>.video.modality_keys}
200
+ - <<: *color_jitter_cfg
201
+ apply_to: ${modality_config_<EMBODIMENT>.video.modality_keys}
202
+ - <<: *to_numpy_cfg
203
+ apply_to: ${modality_config_<EMBODIMENT>.video.modality_keys}
204
+
205
+ # State
206
+ - _target_: groot.vla.data.transform.StateActionToTensor
207
+ apply_to: ${modality_config_<EMBODIMENT>.state.modality_keys}
208
+ - _target_: groot.vla.data.transform.StateActionTransform
209
+ apply_to: ${modality_config_<EMBODIMENT>.state.modality_keys}
210
+ normalization_modes:
211
+ state.joint_pos: q99 # every state key needs a normalization mode
212
+ state.gripper_pos: q99
213
+
214
+ # Action
215
+ - _target_: groot.vla.data.transform.StateActionToTensor
216
+ apply_to: ${modality_config_<EMBODIMENT>.action.modality_keys}
217
+ - _target_: groot.vla.data.transform.StateActionTransform
218
+ apply_to: ${modality_config_<EMBODIMENT>.action.modality_keys}
219
+ normalization_modes:
220
+ action.joint_pos: q99 # every action key needs a normalization mode
221
+ action.gripper_pos: q99
222
+
223
+ # Concat
224
+ - _target_: groot.vla.data.transform.ConcatTransform
225
+ video_concat_order: ${modality_config_<EMBODIMENT>.video.modality_keys}
226
+ state_concat_order: ${modality_config_<EMBODIMENT>.state.modality_keys}
227
+ action_concat_order: ${modality_config_<EMBODIMENT>.action.modality_keys}
228
+
229
+ # Model-specific (required, don't change)
230
+ - ${model_specific_transform}
231
+ ```
232
+
233
+ Every state and action key **must** appear in `normalization_modes`. The strategy is typically `q99`.
234
+
235
+ ### 3d. Register in the global maps
236
+
237
+ Add your embodiment to each of these four maps (at the bottom of the base YAML):
238
+
239
+ ```yaml
240
+ modality_configs:
241
+ ...
242
+ <EMBODIMENT>: ${modality_config_<EMBODIMENT>}
243
+
244
+ transforms:
245
+ ...
246
+ <EMBODIMENT>: ${transform_<EMBODIMENT>}
247
+
248
+ metadata_versions:
249
+ ...
250
+ <EMBODIMENT>: '0221'
251
+
252
+ fps:
253
+ ...
254
+ <EMBODIMENT>: 30 # set to your dataset's FPS
255
+ ```
256
+
257
+ ---
258
+
259
+ ## Step 4: Create a Dataset YAML
260
+
261
+ Create `groot/vla/configs/data/dreamzero/<EMBODIMENT>_relative.yaml`:
262
+
263
+ ```yaml
264
+ # @package _global_
265
+
266
+ defaults:
267
+ - dreamzero/base_48_wan_fine_aug_relative
268
+ - _self_
269
+
270
+ max_state_dim: 64
271
+ use_global_metadata: false
272
+ relative_action: true
273
+ relative_action_per_horizon: false
274
+ relative_action_keys:
275
+ - joint_pos # sub-key names (without state./action. prefix)
276
+ - gripper_pos # that should use relative actions
277
+ max_chunk_size: 5
278
+ dataset_shard_sampling_rate: 0.1
279
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
280
+ single_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotSubLangSingleActionChunkDatasetDROID
281
+
282
+ <EMBODIMENT>_data_root: ??? # set via CLI or env var
283
+
284
+ train_dataset:
285
+ _target_: ${mixture_dataset_cls}
286
+ _convert_: object
287
+ mixture_spec:
288
+ - dataset_path:
289
+ <EMBODIMENT>: # must match key in modality_configs/transforms
290
+ - ${<EMBODIMENT>_data_root}
291
+ dataset_weight: 1.0
292
+ distribute_weights: true
293
+
294
+ dataset_class: ${single_dataset_cls}
295
+ all_modality_configs: ${modality_configs}
296
+ all_transforms: ${transforms}
297
+ metadata_versions: ${metadata_versions}
298
+ fps: ${fps}
299
+ dataset_kwargs:
300
+ video_backend: decord
301
+ use_global_metadata: ${use_global_metadata}
302
+ max_chunk_size: ${max_chunk_size}
303
+ relative_action: ${relative_action}
304
+ relative_action_keys: ${relative_action_keys}
305
+ relative_action_per_horizon: ${relative_action_per_horizon}
306
+ mixture_kwargs:
307
+ training: true
308
+ balance_dataset_weights: false
309
+ seed: 42
310
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
311
+ ```
312
+
313
+ The critical things to get right:
314
+
315
+ - `<EMBODIMENT>` in `mixture_spec.dataset_path` must match the key in `modality_configs` and `transforms`.
316
+ - `relative_action_keys` lists the sub-key names (without `state.`/`action.` prefix) that exist in **both** state and action modalities.
317
+
318
+ ---
319
+
320
+ ## Step 5: Create a Training Script
321
+
322
+ Create `scripts/train/<EMBODIMENT>_training.sh`:
323
+
324
+ ```bash
325
+ #!/bin/bash
326
+ export HYDRA_FULL_ERROR=1
327
+
328
+ # ============ CONFIGURATION ============
329
+ DATA_ROOT=${DATA_ROOT:?"Set DATA_ROOT to your GEAR-converted dataset"}
330
+ OUTPUT_DIR=${OUTPUT_DIR:-"./checkpoints/dreamzero_<EMBODIMENT>_lora"}
331
+
332
+ if [ -z "${NUM_GPUS:-}" ]; then
333
+ NUM_GPUS=$(nvidia-smi -L 2>/dev/null | wc -l)
334
+ fi
335
+ NUM_GPUS=${NUM_GPUS:-8}
336
+
337
+ WAN_CKPT_DIR=${WAN_CKPT_DIR:-"./checkpoints/Wan2.1-I2V-14B-480P"}
338
+ TOKENIZER_DIR=${TOKENIZER_DIR:-"./checkpoints/umt5-xxl"}
339
+ # =======================================
340
+
341
+ # Auto-download weights if missing
342
+ if [ ! -d "$WAN_CKPT_DIR" ] || [ -z "$(ls -A "$WAN_CKPT_DIR" 2>/dev/null)" ]; then
343
+ huggingface-cli download Wan-AI/Wan2.1-I2V-14B-480P --local-dir "$WAN_CKPT_DIR"
344
+ fi
345
+ if [ ! -d "$TOKENIZER_DIR" ] || [ -z "$(ls -A "$TOKENIZER_DIR" 2>/dev/null)" ]; then
346
+ huggingface-cli download google/umt5-xxl --local-dir "$TOKENIZER_DIR"
347
+ fi
348
+
349
+ if [ ! -d "$DATA_ROOT" ]; then
350
+ echo "ERROR: Dataset not found at $DATA_ROOT"
351
+ exit 1
352
+ fi
353
+ if [ ! -f "$DATA_ROOT/meta/embodiment.json" ]; then
354
+ echo "ERROR: meta/embodiment.json missing — run convert_lerobot_to_gear.py first"
355
+ exit 1
356
+ fi
357
+
358
+ torchrun --nproc_per_node $NUM_GPUS --standalone \
359
+ groot/vla/experiment/experiment.py \
360
+ report_to=wandb \
361
+ data=dreamzero/<EMBODIMENT>_relative \
362
+ wandb_project=dreamzero \
363
+ train_architecture=lora \
364
+ num_frames=33 \
365
+ action_horizon=24 \
366
+ num_views=3 \
367
+ model=dreamzero/vla \
368
+ model/dreamzero/action_head=wan_flow_matching_action_tf \
369
+ model/dreamzero/transform=dreamzero_cotrain \
370
+ num_frame_per_block=2 \
371
+ num_action_per_block=24 \
372
+ num_state_per_block=1 \
373
+ seed=42 \
374
+ training_args.learning_rate=1e-5 \
375
+ training_args.deepspeed="groot/vla/configs/deepspeed/zero2.json" \
376
+ save_steps=10000 \
377
+ training_args.warmup_ratio=0.05 \
378
+ output_dir=$OUTPUT_DIR \
379
+ per_device_train_batch_size=4 \
380
+ max_steps=100000 \
381
+ weight_decay=1e-5 \
382
+ save_total_limit=10 \
383
+ upload_checkpoints=false \
384
+ bf16=true \
385
+ tf32=true \
386
+ eval_bf16=true \
387
+ dataloader_pin_memory=false \
388
+ dataloader_num_workers=1 \
389
+ image_resolution_width=320 \
390
+ image_resolution_height=176 \
391
+ save_lora_only=true \
392
+ max_chunk_size=4 \
393
+ frame_seqlen=880 \
394
+ save_strategy=steps \
395
+ <EMBODIMENT>_data_root=$DATA_ROOT \
396
+ dit_version=$WAN_CKPT_DIR \
397
+ text_encoder_pretrained_path=$WAN_CKPT_DIR/models_t5_umt5-xxl-enc-bf16.pth \
398
+ image_encoder_pretrained_path=$WAN_CKPT_DIR/models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth \
399
+ vae_pretrained_path=$WAN_CKPT_DIR/Wan2.1_VAE.pth \
400
+ tokenizer_path=$TOKENIZER_DIR \
401
+ pretrained_model_path=./checkpoints/DreamZero-AgiBot \
402
+ ++action_head_cfg.config.skip_component_loading=true \
403
+ ++action_head_cfg.config.defer_lora_injection=true
404
+ ```
405
+
406
+ ### Key parameters to adjust per embodiment
407
+
408
+ | Parameter | Default | When to change |
409
+ |---|---|---|
410
+ | `num_views` | `3` | Number of cameras your robot has |
411
+ | `action_horizon` | `24` | Must match the number of action `delta_indices` |
412
+ | `num_frames` | `33` | Must be `len(video delta_indices) + num_frame_per_block * (blocks - 1)` |
413
+ | `image_resolution_width` | `320` | Match your camera resolution (or desired resize) |
414
+ | `image_resolution_height` | `176` | Match your camera resolution (or desired resize) |
415
+ | `max_steps` | `100000` | Scale with dataset size |
416
+ | `per_device_train_batch_size` | `4` | Adjust for GPU memory |
417
+
418
+ ---
419
+
420
+ ## Step 6: Train
421
+
422
+ ### Download the pretrained checkpoint
423
+
424
+ The training scripts load from a pretrained DreamZero checkpoint for LoRA fine-tuning. Download [DreamZero-AgiBot](https://huggingface.co/GEAR-Dreams/DreamZero-AgiBot) (~45GB) to `./checkpoints/DreamZero-AgiBot`:
425
+
426
+ ```bash
427
+ git clone https://huggingface.co/GEAR-Dreams/DreamZero-AgiBot ./checkpoints/DreamZero-AgiBot
428
+ ```
429
+
430
+ Or with the Hugging Face CLI:
431
+
432
+ ```bash
433
+ hf download GEAR-Dreams/DreamZero-AgiBot --repo-type model --local-dir ./checkpoints/DreamZero-AgiBot
434
+ ```
435
+
436
+ ### Launch training
437
+
438
+ ```bash
439
+ DATA_ROOT=/path/to/your_dataset bash scripts/train/<EMBODIMENT>_training.sh
440
+
441
+ # With overrides:
442
+ DATA_ROOT=/path/to/your_dataset OUTPUT_DIR=./checkpoints/run1 NUM_GPUS=4 \
443
+ bash scripts/train/<EMBODIMENT>_training.sh
444
+ ```
445
+
446
+ ---
447
+
448
+ ## Pre-Training Checklist
449
+
450
+ - [ ] `meta/embodiment.json` exists and has the correct tag
451
+ - [ ] `meta/modality.json` state/action/video/annotation keys are populated
452
+ - [ ] `meta/stats.json` and `meta/relative_stats_dreamzero.json` exist
453
+ - [ ] `meta/tasks.jsonl` and `meta/episodes.jsonl` exist
454
+ - [ ] Embodiment tag in `embodiment.json` matches the key in `modality_configs` / `transforms` / `metadata_versions` / `fps`
455
+ - [ ] YAML `modality_keys` match `modality.json` keys exactly (with `state.`/`action.`/`video.`/`annotation.` prefix)
456
+ - [ ] Every state and action key appears in `normalization_modes` in the transform block
457
+ - [ ] `relative_action_keys` are sub-key names that exist in both state and action
458
+ - [ ] Wan2.1-I2V-14B-480P and umt5-xxl weights are available
459
+ - [ ] DreamZero-AgiBot checkpoint is downloaded to `./checkpoints/DreamZero-AgiBot`
460
+
461
+ ---
462
+
463
+ ## Quick Reference: Existing Embodiments
464
+
465
+ | Embodiment | Data Config | Layout |
466
+ |---|---|---|
467
+ | `oxe_droid` | `droid_relative.yaml` | 3 cameras, joint_position + gripper_position |
468
+ | `agibot` | `agibot_relative.yaml` | 3 cameras, 6 state keys, 7 action keys |
469
+ | `yam` | `yam_relative.yaml` | 3 cameras (top/left/right), bimanual left/right joint_pos + gripper_pos |
470
+
471
+ Use these as concrete examples when building your own config.
docs/DROID_CONVERSION.md ADDED
@@ -0,0 +1,62 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Converting DROID from Scratch
2
+
3
+ If you want to reproduce the DreamZero DROID dataset conversion yourself (or modify the filtering), follow the steps below. This requires the raw DROID 1.0.1 dataset in RLDS format and the idle filter ranges JSON.
4
+
5
+ > **Most users should skip this** and simply download the preprocessed dataset:
6
+ > ```bash
7
+ > huggingface-cli download GEAR-Dreams/DreamZero-DROID-Data --repo-type dataset --local-dir ./data/droid_lerobot
8
+ > ```
9
+
10
+ ## Step 1: Install conversion dependencies
11
+
12
+ ```bash
13
+ pip install tensorflow tensorflow-datasets polars av
14
+ ```
15
+
16
+ ## Step 2: Download the raw DROID 1.0.1 dataset
17
+
18
+ This requires `gsutil` ([Google Cloud CLI](https://cloud.google.com/storage/docs/gsutil_install)). The full dataset is ~1.7TB.
19
+
20
+ ```bash
21
+ gsutil -m cp -r gs://gresearch/robotics/droid/1.0.1 ./data/droid/1.0.1
22
+ ```
23
+
24
+ > **Important:** Use version 1.0.1, not 1.0.0. Version 1.0.1 contains the complete set of language annotations (~75k episodes).
25
+
26
+ ## Step 3: Download the idle filter ranges
27
+
28
+ This JSON file maps each episode to the frame ranges that should be kept (non-idle frames). It was originally computed by [Physical Intelligence](https://github.com/Physical-Intelligence/openpi) for training pi0-DROID models.
29
+
30
+ ```bash
31
+ gsutil cp gs://openpi-assets/droid/droid_sample_ranges_v1_0_1.json ./data/keep_ranges.json
32
+ ```
33
+
34
+ ## Step 4: Run the conversion
35
+
36
+ ```bash
37
+ python scripts/data/convert_droid.py \
38
+ ./data/droid/1.0.1 \
39
+ ./data/droid_lerobot \
40
+ --keep-ranges-path ./data/keep_ranges.json \
41
+ --filter-failed \
42
+ -n 16
43
+ ```
44
+
45
+ For a quick test with a small subset:
46
+ ```bash
47
+ python scripts/data/convert_droid.py \
48
+ ./data/droid/1.0.1 \
49
+ ./data/droid_lerobot_test \
50
+ --keep-ranges-path ./data/keep_ranges.json \
51
+ --filter-failed \
52
+ --first-n 5 \
53
+ -n 4
54
+ ```
55
+
56
+ ## Script reference
57
+
58
+ See [`scripts/data/convert_droid.py`](scripts/data/convert_droid.py) for full usage:
59
+
60
+ ```
61
+ python scripts/data/convert_droid.py --help
62
+ ```
docs/TREX_TRACK_FORCE_MODEL.md ADDED
@@ -0,0 +1,301 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # T-Rex Track-Force:16-step、20 Hz 动作与 5 Hz 触觉模型
2
+
3
+ 本文描述独立的 `trex_track_force` 模型、数据契约、embedding、注意力、两阶段
4
+ flow matching、训练和在线执行。它不会修改原 DreamZero 的模型调用路径。
5
+
6
+ ## 1. 固定时序契约
7
+
8
+ - 每个 action chunk 含 **16 个动作**,动作频率为 **20 Hz**。
9
+ - 一个 chunk 覆盖 `16 / 20 = 0.8 s`;动作时间戳跨度为 `0.75 s`。
10
+ - 触觉频率为 **5 Hz**,因此每 **4 个动作步**刷新一次。
11
+ - 一个 chunk 内的触觉刷新 offset 固定为 `[0, 4, 8, 12]`。
12
+ - 每次刷新只重新去噪尚未执行的 action suffix;已经执行的前缀保持不变。
13
+ - `max_chunk_size=4` 保留 DreamZero 原有的 autoregressive block memory。
14
+ 这里的 `max_chunk_size` 是跨 block 的记忆窗口,不是单个 action chunk 的长度。
15
+ - 一个训练 sample 仍由 4 个 autoregressive blocks 组成,因此共有 64 个动作;
16
+ 只有每 block 的 action 长度从 24 改为 16。
17
+ - 三路视频保持 DreamZero 的 33 帧布局:1 帧 clean conditioning observation,
18
+ 后接每 block 8 帧、共 32 帧 10 Hz future-video targets。
19
+ - 每个 block 都有 16 帧过去 track 和 16 步 target track;target 的第 0 步就是
20
+ 当前 anchor 帧,与 OpenPI 的 track window 契约一致。
21
+
22
+ ## 2. 总体架构
23
+
24
+ ```mermaid
25
+ flowchart LR
26
+ V["三视角 RGB\nhead + left wrist + right wrist"] --> GRID["三视角拼图\n160 × 320"]
27
+ GRID --> VAE["Wan2.2 VAE38\n48-channel latent"]
28
+ GRID --> CLIP["Wan CLIP\n图像条件"]
29
+ TXT["任务文本"] --> T5["UMT5\n文本条件"]
30
+
31
+ TRK["250 点 track\npast clean + future noisy"] --> TE["TrackEncoder\n几何 + 可见性 + 身份 embedding"]
32
+ ACT["16 × 62D delta-base action\n补零到 64D"] --> AE["Action encoder"]
33
+ ST["62D 当前 EEF/hand state"] --> SE["State encoder"]
34
+
35
+ VAE --> WAN["CausalWanTrackForceModel\nWan2.2-TI2V-5B · 30 blocks"]
36
+ CLIP --> WAN
37
+ T5 --> WAN
38
+ TE --> WAN
39
+ AE --> WAN
40
+ SE --> WAN
41
+
42
+ WAN --> COARSE["τ: 1 → 0.4\n6-step coarse action"]
43
+ WAN --> TFLOW["未来 track flow"]
44
+ WAN --> VFLOW["未来 video flow"]
45
+ WAN --> MEM["τ=0.4 coarse memory"]
46
+
47
+ RAW["10 fingers × 6D force\n16-sample history"] --> VQ["Per-finger force VQ-VAE\n64 codes · 256D"]
48
+ FRESH["当前 10 × 6D force"] --> FT["Force-only Transformer\n6 layers · width 768"]
49
+ VQ --> FT
50
+ MEM --> FT
51
+ COARSE --> FT
52
+ FT --> FINE["τ: 0.4 → 0\n4-step tactile action suffix"]
53
+ ```
54
+
55
+ 粗阶段负责从视觉、状态、语言和 track 中得到动作的大尺度结构;独立的
56
+ force-only transformer 只使用力信号和粗阶段 memory 完成剩余去噪,使触觉
57
+ 负责接触后的细粒度修正。
58
+
59
+ ## 3. 62DoF 动作空间
60
+
61
+ 每只手臂占 31 维,两侧共 62 维:
62
+
63
+ - 手腕/末端位姿:`xyz + rotation-6D`,共 9 维。
64
+ - 手部关节目标:22 维。
65
+
66
+ 训练动作不是世界坐标绝对位姿。loader 以 chunk 起始状态为 reference,把每个
67
+ 目标手腕位姿转换为 **delta-base**:
68
+
69
+ - 平移在 chunk 起始手腕坐标系中表示。
70
+ - 旋转为 `R_reference^-1 × R_target`,再编码为 rotation-6D。
71
+ - 手部 22 维仍是绝对目标。
72
+
73
+ 模型内部把 62 个物理维补零为 64 维;采样的两个 padding 维在初始化和每个
74
+ Euler step 后都会重新置零。输出先按 relative-action 统计量反归一化,再用
75
+ chunk 起始 state 恢复绝对 EEF 位姿。
76
+
77
+ ## 4. Track 数据和点身份
78
+
79
+ 250 个点具有固定、可验证的顺序:
80
+
81
+ - `0:50`:头部视角左手及手臂。
82
+ - `50:100`:头部视角右手及手臂。
83
+ - `100:125`:左腕视角 5×5 背景点。
84
+ - `125:175`:左腕视角手掌点。
85
+ - `175:200`:右腕视角 5×5 背景点。
86
+ - `200:250`:右腕视角手掌点。
87
+
88
+ SAM2 只在 episode 第 0 帧根据固定 prompt 产生手/臂 mask;点从 mask 中采样,
89
+ 随后由 CoTracker 跟踪整段视频。保存字段为:
90
+
91
+ - `observation.track_xy`:`[250, 2]`,坐标归一化到 `[0, 1]`。
92
+ - `observation.track_visibility`:`[250]`。
93
+ - metadata 中同时记录 view、hand、role、point index 和各 segment 边界。
94
+
95
+ ## 5. Embedding 设计
96
+
97
+ ### 5.1 Video、语言和图像条件
98
+
99
+ - 三视角拼入 2×2 canvas 的三个有效格,再统一缩放到 `160×320`。
100
+ - Wan2.2 VAE38 生成 48-channel latent。
101
+ - `patch_size=[1,2,2]`,每 latent frame 形成 50 个 video tokens。
102
+ - video token 使用 Wan 原生 3D RoPE,编码时间、高度和宽度。
103
+ - UMT5 输出 4096D 文本条件。
104
+ - Wan CLIP 输出 1280D 图像条件,并经 `img_emb` 投影。
105
+ - CLIP/T5 cross-attention 只作用于 observation/action query,不向 track query
106
+ 泄漏视觉或语言信息。
107
+
108
+ ### 5.2 Action 和 state
109
+
110
+ - action:每个 62D 动作补零到 64D,经线性层投影到 Wan hidden dim 3072。
111
+ - 一个 block 有 16 个 action tokens;位置由 1D action RoPE 编码。
112
+ - state:62D 当前状态补到 64D,经线性层得到一个 state token。
113
+ - flow 时间 `τ` 经 sinusoidal embedding 和 MLP 后参与 Wan modulation。
114
+
115
+ ### 5.3 TrackEncoder
116
+
117
+ 每个点分别产生一个 `past token` 和一个 `future token`。时间序列输入特征为:
118
+
119
+ ```text
120
+ [x, y, visibility, Δx, Δy]
121
+ ```
122
+
123
+ 不可见时刻的坐标和 motion 先清零,随后完整的 `16×5` 时序按固定顺序展平,再经
124
+ `Linear → SiLU → Linear` 投影。不能在时间维求平均:future flow 的每个时刻含有
125
+ 独立噪声,平均会丢失“哪一个噪声属于哪一个 timestep”,使 16-step 重建不可解。
126
+
127
+ 最终每个点 token 是下列 embedding 的和,再经 LayerNorm:
128
+
129
+ ```text
130
+ trajectory
131
+ + view(head / left_wrist / right_wrist)
132
+ + hand(none / left / right)
133
+ + role(head_hand / wrist_background / wrist_hand)
134
+ + canonical point id(0...249)
135
+ + autoregressive block id
136
+ + temporal role(past / future)
137
+ ```
138
+
139
+ 左右手不只通过点序号区分,还具有显式 hand embedding。背景点使用
140
+ `hand=none` 和独立 role embedding。
141
+
142
+ 与 OpenPI 一致,track target 始终是 `[0,1]` 内的绝对归一化 XY;target window
143
+ 从当前帧开始,因此第 0 步等于 GT anchor。`TrackDecoder` 对 250 个
144
+ future-track hidden token 分别执行 `LayerNorm → Linear(16×2)`,预测 CFM
145
+ velocity `noise-clean_xy`。Euler 积分后的结果已经是绝对坐标,不做空间
146
+ `cumsum`,也不再额外加 anchor。
147
+
148
+ ### 5.4 Force-only VQ-VAE
149
+
150
+ 输入只包含 force/wrench,不包含 deformation map:
151
+
152
+ ```text
153
+ [batch, history=16, fingers=10, wrench=6]
154
+ ```
155
+
156
+ - 两只手共享同一套时序卷积 encoder/decoder。
157
+ - 每只手内部加入 5 个 finger identity embeddings。
158
+ - 输出每根手指一个 256D latent,共 10 个 tactile history tokens。
159
+ - EMA codebook 大小为 64,带 commitment loss、perplexity 统计和 dead-code
160
+ revival。
161
+ - 左右手通过 side embedding 区分,手指位置通过 finger embedding 区分。
162
+ - 训练可直接输入 raw 16-step history;推理也可输入预计算的 10 个离散 codes。
163
+ - episode 前缀缺失的历史填为归一化中性值,并用 validity mask 从 VQ 重建
164
+ loss 中排除 padding 步。
165
+
166
+ ### 5.5 Force-only Transformer
167
+
168
+ 输入 token 包括:
169
+
170
+ - 16 个 noisy action tokens。
171
+ - 10 个当前 force tokens。
172
+ - 10 个 VQ history tokens。
173
+ - 从 Wan `τ=0.4` hidden state 提取的 coarse memory tokens。
174
+
175
+ action token 叠加 action position、5Hz force slot、token type、当前 refresh
176
+ offset 和 `τ` embedding。force/history token叠加 finger position、token type 和
177
+ refresh offset。独立 transformer 为 6 层、12 heads、hidden dim 768。
178
+
179
+ ## 6. 非对称 attention 契约
180
+
181
+ 每个 autoregressive block 的 packed 顺序为:
182
+
183
+ ```text
184
+ [obs, action, state, track_past, track_future]
185
+ ```
186
+
187
+ 其可见性严格为:
188
+
189
+ - observation query 和 action query 在同一 block 内互相可见。
190
+ - observation/action query 可读取同 block 的 state、past track 和 noisy future
191
+ track,也可读取有限 AR 窗口内的历史 obs/action/track。
192
+ - state query 只读取自己的 state token,避免把多模态信息反向带给 track。
193
+ - past-track query 读取历史 block 的 track 和当前 past track,但不能读取当前
194
+ noisy future track。
195
+ - future-track query 只读取当前及历史 track。
196
+ - **任何 track query 都不能读取 observation、action、state、CLIP 或文本。**
197
+ - 任何 query 都不能读取未来 block。
198
+
199
+ 因此实现了要求的方向性:`obs/action → track` 表示 obs/action 可以把 track
200
+ 作为条件;反方向被 mask 禁止。
201
+
202
+ ## 7. 两阶段 flow matching
203
+
204
+ 使用线性插值:
205
+
206
+ ```text
207
+ x_τ = τ · noise + (1 - τ) · clean
208
+ target flow = noise - clean
209
+ x_next = x_τ + (τ_next - τ) · predicted_flow
210
+ ```
211
+
212
+ ### 粗阶段:Wan,`τ=1 → 0.4`
213
+
214
+ - 训练时 action/Wan expert 遵循原始 T-Rex,在完整 `(0,1]` 上按
215
+ `Beta(1.5,1.0)` 采样;其均值约为 `0.6`,但这不是 split timestep。
216
+ - Wan 联合预测 action flow、future-track flow 和 future-video flow。
217
+ - 推理按总计 10 个、`Δτ=-0.1` 的 Euler 网格运行前 6 步,到达 `τ=0.4`。
218
+ - 在精确的 `τ=0.4` 再执行一次 Wan,生成给触觉 transformer 使用的 detached
219
+ coarse memory。
220
+
221
+ ### 精阶段:触觉,`τ=0.4 → 0`
222
+
223
+ - 训练时 `τ_tactile = 0.4 × Beta(1.5,1.0)`,覆盖 `(0,0.4]`。
224
+ - 每个训练 chunk 对 offset `0/4/8/12` 分别提供对应的 force 与 16-sample
225
+ history。
226
+ - offset 为 `k` 时,loss 只覆盖 action `k:16`。
227
+ - 推理运行剩余 4 个 Euler updates;每次 5Hz 刷新只写入未执行 suffix。
228
+ - runtime controller 保存之前已经发出的 prefix,确保后续触觉刷新不能改写历史
229
+ command。
230
+
231
+ ## 8. 训练 loss
232
+
233
+ 总 loss 由以下部分加权求和:
234
+
235
+ - `dynamics_loss`:未来视频 latent flow MSE。
236
+ - `action_loss`:粗阶段 62 个物理动作维的 flow MSE。
237
+ - `track_loss`:可见性 mask 后的绝对 XY CFM velocity MSE。
238
+ - `force_loss`:按 refresh offset mask 后的动作 suffix flow MSE。
239
+ - `vq_loss`:仅在有效��史步计算的 force history reconstruction loss。
240
+ - `commitment_loss`:VQ commitment loss,默认系数 0.25。
241
+
242
+ 训练使用 Wan2.2-TI2V-5B 共享权重和 LoRA。LoRA 注入
243
+ `q,k,v,o,k_img,v_img,ffn.0,ffn.2`;新建的 action/state/track/force、video token
244
+ 投影和 decoder 完整训练。checkpoint 保存 LoRA、新模块参数以及 VQ EMA buffers。
245
+
246
+ ## 9. 在线执行
247
+
248
+ 1. 收集三视角 conditioning observation(在线接口也可编码多帧历史)、当前
249
+ 62D state、16 帧 past track、当前 force 和 16-sample force history。
250
+ 2. Wan 对 action、future track 和 future video 运行 6 步,从 `τ=1` 到
251
+ `τ=0.4`。
252
+ 3. offset 0 的触觉运行剩余 4 步,把完整 16-step action 去噪到 `τ=0`。
253
+ 4. 以 20 Hz 执行动作。
254
+ 5. 执行 4、8、12 步后,各接收一次新的 5Hz 触觉;每次从缓存的 coarse state
255
+ 重新去噪剩余 suffix,同时保留已执行 prefix。
256
+ 6. 输出 delta-base 动作反归一化并恢复为绝对手腕 `xyz + rotation-6D`。
257
+
258
+ ## 10. 入口
259
+
260
+ 训练:
261
+
262
+ ```bash
263
+ cd /scratch1/home/zhicao/dreamzero
264
+ bash scripts/train/trex_track_force_training_wan22.sh
265
+ ```
266
+
267
+ 训练脚本直接使用已经构建完成的 `data/trex_small`,启动前只执行 schema
268
+ 完整性校验,不会再次运行 SAM2/CoTracker。
269
+
270
+ 日志、checkpoint 与原 T-Rex 训练共用同一套 Trainer 回调。每隔
271
+ `wandb_video_reconstruction_steps` 个 global step 会分别保存两个监控视频:
272
+ 不带标记的原始重建视频位于
273
+ `OUTPUT_DIR/eval_videos/train_step_XXXXXX.mp4`,20Hz 预测 track motion
274
+ 叠加到 10Hz head/left-wrist/right-wrist 三面板后的版本位于
275
+ `OUTPUT_DIR/eval_track_videos/train_step_XXXXXX.mp4`。原始预测与目标
276
+ track 同时写入 `OUTPUT_DIR/eval_tracks/train_step_XXXXXX.npz`。两个视频
277
+ 分别记录到 W&B `eval/predicted_video` 和 `eval/predicted_track_video`;
278
+ `dynamics/action/track/force/VQ/commitment` loss 和触觉 codebook
279
+ perplexity/active-code 指标同时写入 W&B 与 `OUTPUT_DIR/loss_log.jsonl`。
280
+ 两个视频的首帧都是精确 GT conditioning frame;track target/prediction 的第 0
281
+ 步在模型链路内就是当前 GT anchor。可视化不再对整段预测做事后平移。
282
+
283
+ 训练可视化默认使用
284
+ `wandb_video_reconstruction_inference_steps=1`:先运行一次 coarse WAN
285
+ Euler update,再在 `tau=0.4` 用一次边界预测恢复 clean video/track;不会直接
286
+ 解码半噪声状态,并跳过不会改变这两个输出的 action-only 触觉精修。
287
+ 这不会改变训练或正式在线推理的 6+4 两阶段 schedule;如需更高质量的监控视频,
288
+ 可以单独提高该值,但耗时近似按 inference steps 线性增长。
289
+
290
+ 离线 NPZ 推理:
291
+
292
+ ```bash
293
+ python scripts/eval/trex_track_force_inference.py \
294
+ --checkpoint checkpoints/trex_track_force_wan22_lora/checkpoint-8000 \
295
+ --dataset-root data/trex_small \
296
+ --input sample_input.npz \
297
+ --output prediction.npz
298
+ ```
299
+
300
+ 核心实现位于 `groot/vla/model/trex_track_force/`;独立 Hydra 配置为
301
+ `model=trex_track_force/vla` 和 `data=dreamzero/trex_track_force_wan22`。
docs/WAN22_BACKBONE.md ADDED
@@ -0,0 +1,167 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Training DreamZero with Wan2.2-TI2V-5B Backbone
2
+
3
+ This guide explains how to train DreamZero on the DROID dataset using **Wan2.2-TI2V-5B** as the backbone instead of the default Wan2.1-I2V-14B.
4
+
5
+ ## Architecture Differences
6
+
7
+ | Component | Wan2.1-I2V-14B | Wan2.2-TI2V-5B |
8
+ |-----------|-----------------|----------------|
9
+ | DiT dim | 5120 | 3072 |
10
+ | DiT layers | 32 | 30 |
11
+ | DiT heads | 16 | 24 |
12
+ | FFN dim | 13824 | 14336 |
13
+ | VAE latent channels | 16 | 48 |
14
+ | VAE spatial stride | 8× | 16× |
15
+ | Model type | i2v | ti2v |
16
+
17
+ **FFN** = Feed-Forward Network: the two-layer MLP in each transformer block (Linear → GELU → Linear). FFN dim is the intermediate hidden size (e.g. 14336 for 5B).
18
+
19
+ DreamZero uses a **CausalWanModel** wrapper that extends the base Wan architecture with **action/state registers** for robot policy learning. The same `CausalWanModel` class supports both Wan2.1 and Wan2.2 backbones via configuration—no new class is required. The config switches the architecture parameters (dim, in_dim, out_dim, etc.) and uses `WanVideoVAE38` for the 48-channel Wan2.2 VAE.
20
+
21
+ **What action/state registers do:** The DiT sees a single sequence `[video_tokens | action_register]` where the action register is encoded action and state features (one chunk per block). All tokens share the same transformer (with causal masking and RoPE). The model learns to predict **video noise** (dynamics) and **action noise** (policy): the action-register slice is decoded by `action_decoder` to produce action noise predictions. So the model is conditioned on current state and (noisy) actions and learns to denoise both video and actions for closed-loop policy learning.
22
+
23
+ ## Causal masking, RoPE, and sequence layout
24
+
25
+ ### Causal masking
26
+
27
+ In attention, **causal masking** means each position can only attend to **past and current** positions (no future). So token at index `i` can see keys at indices `j ≤ i`. That keeps the model autoregressive: it never uses future video frames or future actions when predicting the current step. In CausalWanModel the masking is **blockwise**: the first frame attends to itself; each later block of video frames can attend to the first frame plus previous (and optionally current) blocks. Action and state tokens have their own causal pattern so each action chunk only sees past video and past actions/state. This matches policy learning where you condition on observed history and predict the next action chunk.
28
+
29
+ ### RoPE (Rotary Position Embeddings)
30
+
31
+ **RoPE** encodes position by rotating query and key vectors in a complex plane with position-dependent angles. Unlike adding a position vector, RoPE makes attention scores depend on the *relative* position of query and key, which generalizes better to longer sequences. In CausalWanModel:
32
+
33
+ - **Video tokens** use **3D RoPE**: separate frequency components for frame index (time), height, and width of the patch grid. So each token knows its (t, h, w) in the video.
34
+ - **Action and state tokens** use **1D RoPE**: a single position index along the sequence (frame/block index). So the model knows the temporal order of action chunks and state.
35
+
36
+ Freqs are built in `_create_freqs()` from the patch grid size (F, H, W) and concatenated with separate 1D freqs for the action register.
37
+
38
+ ### Tokens, blocks, and chunks
39
+
40
+ - **Token**: The smallest unit the transformer sees. After **patch_embedding** (stride 1×2×2 on the latent), one frame yields a 2D grid of tokens; the total per frame is **frame_seqlen** (e.g. 50 for 160×320). So one **token** = one patch (e.g. 1×2×2 in latent space).
41
+
42
+ - **Block (image block)**: A group of consecutive **frames**, not tokens. **num_frame_per_block** (e.g. 2) frames form one “image block.” So with 33 frames you get multiple blocks. **num_image_blocks** = `(num_frames - 1) // num_frame_per_block`. Blocks are used for blockwise causal attention and to align video with action/state.
43
+
44
+ - **Chunk**: In policy terms, an **action chunk** is the sequence of actions the policy outputs for one block (e.g. **num_action_per_block** = 24 actions per block). The **action register** in the DiT has one chunk per image block: for each block there are `num_action_per_block` action tokens and `num_state_per_block` state tokens. So the register length is `num_image_blocks * (num_action_per_block + num_state_per_block)`. “Chunk” and “block” are often used together: one video block corresponds to one action chunk (and one state token) in the register.
45
+
46
+ Summary: **tokens** = patch-level units (50 per frame); **blocks** = groups of frames (e.g. 2 frames per block); **chunks** = per-block action (and state) outputs that are packed into the action register.
47
+
48
+ ## Inference: blocks, chunks, and closed-loop
49
+
50
+ ### How blocks and chunks are used when predicting actions
51
+
52
+ At **inference**, the model predicts **one action chunk** per call, conditioned on **one block** of video (and current state):
53
+
54
+ 1. **Input**: A short video of the current block — e.g. `num_frame_per_block` frames (e.g. 2) — plus current **state** and (during the denoising loop) **noisy actions** for the chunk being predicted. The first time in a trajectory, the “context” is the first frame (and optionally a warm-up pass with no action to fill the KV cache).
55
+
56
+ 2. **DiT input**: The sequence is `[video_tokens for this block | action_register]`. The action register holds encoded **noisy** actions and **state** for this block only (one chunk). So the DiT sees: “this block of video + this chunk’s noisy actions and state.”
57
+
58
+ 3. **KV cache**: To keep inference causal and efficient, the model uses a **KV cache** over previous blocks. So for the *next* block, the cache already contains keys/values for earlier frames; the DiT only runs on the **new** block’s tokens plus the new action register. `current_start_frame` tells the DiT which block we’re on so RoPE and cache indexing are correct.
59
+
60
+ 4. **Output**: The DiT predicts **video noise** and **action noise**. The action noise is decoded by `action_decoder` into a prediction for the **current chunk**. The scheduler then updates the noisy action toward clean; after `num_inference_steps` denoising steps you get **one clean action chunk** (e.g. 24 actions).
61
+
62
+ So: **one block of frames** (and state) in → **one action chunk** out. Blocks and chunks are aligned: one image block ↔ one action chunk in the register.
63
+
64
+ ### How DreamZero does closed-loop inference
65
+
66
+ Closed-loop execution reuses the same block/chunk logic in a loop:
67
+
68
+ 1. **Observe**: Robot has current observation (e.g. image history + state). The policy is called with this observation (e.g. via `lazy_joint_video_action` or `lazy_joint_video_action_causal`).
69
+
70
+ 2. **Predict**: The action head runs the diffusion loop for the **current block**: it encodes the observed frames to latent, runs the DiT (with KV cache and `current_start_frame`) for each denoising step, and returns one denoised **action chunk** (e.g. 24 actions).
71
+
72
+ 3. **Execute**: The robot **executes** that chunk (e.g. 24 steps at 5 Hz → ~4.8 s). No new model call during execution.
73
+
74
+ 4. **Repeat**: After execution, new observation is available. The policy is called again with the new video (e.g. last N frames). If the task/language is unchanged, `current_start_frame` is incremented by `num_frame_per_block` and the KV cache is reused; the DiT only processes the **new** block and predicts the **next** action chunk. If the task or language changes (or the cache is full), the cache and `current_start_frame` are reset.
75
+
76
+ So closed-loop = **repeated “one block in → one chunk out”** with KV cache across steps so the model never re-processes past frames.
77
+
78
+ ### What changes when you swap the backbone to 5B
79
+
80
+ The **inference algorithm and API stay the same** for 14B vs 5B:
81
+
82
+ - Same **block/chunk layout**: `num_frame_per_block`, `num_action_per_block`, `num_state_per_block` (and thus one block → one chunk) are defined by config and data; they do not depend on which backbone (14B vs 5B) you use.
83
+ - Same **closed-loop flow**: `lazy_joint_video_action`, KV cache, `current_start_frame`, and the denoising loop are in the **action head** and are shared. The policy still calls the same methods (`get_action`, `lazy_joint_video_action`, etc.).
84
+ - Same **backbone role**: The backbone only produces conditioning (e.g. text embeddings). The action head owns the DiT, VAE, and action/state encoders. So “swapping to 5B” means swapping the **action head config** (and checkpoints) to the Wan22 5B DiT + VAE38 + 160×320; the high-level inference path (backbone → action_head → one chunk) is unchanged.
85
+
86
+ What **does** change with 5B:
87
+
88
+ - **DiT size and layout**: 5B uses a smaller DiT (dim 3072, 30 layers, 24 heads), **frame_seqlen = 50** (for 160×320), and **no** first-frame latent concat (`concat_first_frame_latent=False`). First frame is conditioned via **CLIP** in the context, not as extra channel in the latent.
89
+ - **VAE and resolution**: 5B uses **WanVideoVAE38** (48 channels, 16× spatial) and **160×320** video. So latent is 10×20; tokens per frame = 50.
90
+ - **Conditioning**: 5B uses CLIP image embedding for the first frame in the context; 14B can concatenate the first-frame latent to the DiT input. The action head handles this inside the same `_forward_inference` / `_forward_blocks`; no change to the external inference API.
91
+
92
+ So: **blocks and chunks** are used the same way for predicting actions at inference; **closed-loop** is the same loop of “observe → predict one chunk → execute → repeat” with KV cache; **swapping to 5B** keeps that flow and only changes the internal model (DiT/VAE) and resolution/conditioning.
93
+
94
+ ## Prerequisites
95
+
96
+ 1. **Wan2.2-TI2V-5B** weights:
97
+ ```bash
98
+ huggingface-cli download Wan-AI/Wan2.2-TI2V-5B --local-dir ./checkpoints/Wan2.2-TI2V-5B
99
+ ```
100
+ Or clone from [Wan2.2 GitHub](https://github.com/Wan-Video/Wan2.2) and follow their download instructions.
101
+
102
+ 2. **Image encoder (CLIP)**: Wan2.2-TI2V-5B does not include the CLIP image encoder. Use the one from Wan2.1:
103
+ ```bash
104
+ huggingface-cli download Wan-AI/Wan2.1-I2V-14B-480P --local-dir ./checkpoints/Wan2.1-I2V-14B-480P
105
+ ```
106
+ Only `models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth` is needed.
107
+
108
+ 3. **DROID dataset** in LeRobot format:
109
+ ```bash
110
+ huggingface-cli download GEAR-Dreams/DreamZero-DROID-Data --repo-type dataset --local-dir ./data/droid_lerobot
111
+ ```
112
+
113
+ ## Quick Start
114
+
115
+ ```bash
116
+ # Set paths (optional - defaults shown)
117
+ export WAN22_CKPT_DIR=./checkpoints/Wan2.2-TI2V-5B
118
+ export IMAGE_ENCODER_DIR=./checkpoints/Wan2.1-I2V-14B-480P # for CLIP only
119
+ export DROID_DATA_ROOT=./data/droid_lerobot
120
+
121
+ # Run training
122
+ bash scripts/train/droid_training_wan22.sh
123
+ ```
124
+
125
+ ## Configuration Details
126
+
127
+ The Wan2.2 config (`wan_flow_matching_action_tf_wan22.yaml`) overrides:
128
+
129
+ - **model/dreamzero/action_head**: `wan_flow_matching_action_tf_wan22`
130
+ - **diffusion_model_cfg**: Wan2.2 architecture (dim=3072, in_dim=48, out_dim=48, etc.)
131
+ - **vae_cfg**: `WanVideoVAE38` (48-channel Wan2.2 VAE)
132
+ - **frame_seqlen**: 50 (patch output per frame)
133
+ - **target_video_height / target_video_width**: 160 and 320 so latent spatial size is **even** (10×20 after VAE38 16×), avoiding a dynamics-loss crop. Previously 176×320 gave latent 11×20 (odd height); we use **160×320** (H×W) so both latent dimensions are even after the DiT’s stride-(1,2,2) patch embedding.
134
+
135
+ For other resolutions, `frame_seqlen` must match patch output per frame; use H and W divisible by 32 for even latent:
136
+ - 160×320 (H×W): latent 10×20 → 50
137
+ - 176×320: latent 11×20 → 50 (odd H; loss uses crop)
138
+ - 640×352: 220
139
+
140
+ ## Using with Custom Training Scripts
141
+
142
+ To use Wan2.2 in your own training script, add:
143
+
144
+ ```bash
145
+ model/dreamzero/action_head=wan_flow_matching_action_tf_wan22 \
146
+ dit_version=$WAN22_CKPT_DIR \
147
+ text_encoder_pretrained_path=$WAN22_CKPT_DIR/models_t5_umt5-xxl-enc-bf16.pth \
148
+ image_encoder_pretrained_path=$IMAGE_ENCODER_DIR/models_clip_open-clip-xlm-roberta-large-vit-huge-14.pth \
149
+ vae_pretrained_path=$WAN22_CKPT_DIR/Wan2.2_VAE.pth
150
+ ```
151
+ (Do not pass `frame_seqlen`; the Wan22 config uses 50.)
152
+
153
+ ## File Layout
154
+
155
+ ```
156
+ dreamzero/
157
+ ├── groot/vla/configs/model/dreamzero/action_head/
158
+ │ ├── wan_flow_matching_action_tf.yaml # Wan2.1 (default)
159
+ │ └── wan_flow_matching_action_tf_wan22.yaml # Wan2.2-TI2V-5B
160
+ ├── scripts/train/
161
+ │ ├── droid_training.sh # Wan2.1 backbone
162
+ │ └── droid_training_wan22.sh # Wan2.2 backbone
163
+ └── docs/
164
+ └── WAN22_BACKBONE.md # This file
165
+ ```
166
+
167
+ The action head (`wan_flow_matching_action_tf.py`) automatically detects Wan2.2 vs Wan2.1 based on `in_dim` (48 vs 16) and `vae.z_dim` (48 vs 16), and loads the correct checkpoint files from the appropriate HuggingFace repos when local paths are not found.
eval_utils/policy_client.py ADDED
@@ -0,0 +1,93 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Client for communicating with a policy server.
2
+
3
+ Adapted from https://github.com/robo-arena/roboarena/
4
+
5
+ """
6
+
7
+ import logging
8
+ import time
9
+ from typing import Dict, Tuple
10
+
11
+ import websockets.sync.client
12
+ from typing_extensions import override
13
+
14
+ from openpi_client.base_policy import BasePolicy
15
+ from openpi_client import msgpack_numpy
16
+
17
+ # The websockets library by default sends a ping every 20 seconds and
18
+ # expects a pong response within 20 seconds. However, the sever may not
19
+ # send a pong response immediately if it is busy processing a request.
20
+ # Increase the ping interval and timeout so that the client can wait
21
+ # for a longer time before closing the connection.
22
+ PING_INTERVAL_SECS = 60
23
+ PING_TIMEOUT_SECS = 600
24
+
25
+ class WebsocketClientPolicy(BasePolicy):
26
+ """Implements the Policy interface by communicating with a server over websocket.
27
+
28
+ See WebsocketPolicyServer for a corresponding server implementation.
29
+ """
30
+
31
+ def __init__(self, host: str = "0.0.0.0", port: int = 8000) -> None:
32
+ self._uri = f"ws://{host}:{port}"
33
+ self._packer = msgpack_numpy.Packer()
34
+ self._ws, self._server_metadata = self._wait_for_server()
35
+
36
+ def get_server_metadata(self) -> Dict:
37
+ return self._server_metadata
38
+
39
+ def _wait_for_server(self) -> Tuple[websockets.sync.client.ClientConnection, Dict]:
40
+ logging.info(f"Waiting for server at {self._uri}...")
41
+ try:
42
+ conn = websockets.sync.client.connect(
43
+ self._uri,
44
+ compression=None,
45
+ max_size=None,
46
+ ping_interval=PING_INTERVAL_SECS,
47
+ ping_timeout=PING_TIMEOUT_SECS,
48
+ )
49
+ metadata = msgpack_numpy.unpackb(conn.recv())
50
+ return conn, metadata
51
+ except:
52
+ logging.info("Connection to server with ws:// failed. Trying wss:// ...")
53
+
54
+ self._uri = "wss://" + self._uri.split("//")[1]
55
+ conn = websockets.sync.client.connect(
56
+ self._uri,
57
+ compression=None,
58
+ max_size=None,
59
+ ping_interval=PING_INTERVAL_SECS,
60
+ ping_timeout=PING_TIMEOUT_SECS,
61
+ )
62
+ metadata = msgpack_numpy.unpackb(conn.recv())
63
+ return conn, metadata
64
+
65
+ @override
66
+ def infer(self, obs: Dict) -> Dict: # noqa: UP006
67
+ # Notify server that we're calling the infer endpoint (as opposed to the reset endpoint)
68
+ obs["endpoint"] = "infer"
69
+
70
+ data = self._packer.pack(obs)
71
+ self._ws.send(data)
72
+ response = self._ws.recv()
73
+ if isinstance(response, str):
74
+ # we're expecting bytes; if the server sends a string, it's an error.
75
+ raise RuntimeError(f"Error in inference server:\n{response}")
76
+ return msgpack_numpy.unpackb(response)
77
+
78
+ @override
79
+ def reset(self, reset_info: Dict) -> None:
80
+ # Notify server that we're calling the reset endpoint (as opposed to the infer endpoint)
81
+ reset_info["endpoint"] = "reset"
82
+
83
+ data = self._packer.pack(reset_info)
84
+ self._ws.send(data)
85
+ response = self._ws.recv()
86
+ return response
87
+
88
+ if __name__ == "__main__":
89
+ logging.basicConfig(level=logging.INFO)
90
+ client = WebsocketClientPolicy()
91
+ actions = client.infer({})
92
+ print(f"Actions received: {actions}")
93
+ client.reset({})
eval_utils/policy_server.py ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Server for serving a policy over websockets.
2
+
3
+ Adapted from https://github.com/robo-arena/roboarena/
4
+
5
+ """
6
+
7
+
8
+ import asyncio
9
+ import dataclasses
10
+ import logging
11
+ import traceback
12
+
13
+ from openpi_client.base_policy import BasePolicy
14
+ from openpi_client import msgpack_numpy
15
+ import websockets.asyncio.server
16
+ import websockets.frames
17
+
18
+
19
+ @dataclasses.dataclass
20
+ class PolicyServerConfig:
21
+ # Resolution that images get resized to client-side, None means no resizing.
22
+ # It's beneficial to resize images to the desired resolution client-side for faster communication.
23
+ image_resolution: tuple[int, int] | None = (224, 224)
24
+ # Whether or not wrist camera image(s) should be sent.
25
+ needs_wrist_camera: bool = True
26
+ # Number of external cameras to send.
27
+ n_external_cameras: int = 1 # can be in [0, 1, 2]
28
+ # Whether or not stereo camera image(s) should be sent.
29
+ needs_stereo_camera: bool = False
30
+ # Whether or not the unique eval session id should be sent (e.g. for policies that want to keep track of history).
31
+ needs_session_id: bool = False
32
+ # Which action space to use.
33
+ action_space: str = "joint_position" # can be in ["joint_position", "joint_velocity", "cartesian_position", "cartesian_velocity"]
34
+
35
+
36
+ class WebsocketPolicyServer:
37
+ """
38
+ Serves a policy using the websocket protocol.
39
+
40
+ Interface:
41
+ Observation:
42
+ - observation/wrist_image_left: (H, W, 3) if needs_wrist_camera is True
43
+ - observation/wrist_image_right: (H, W, 3) if needs_wrist_camera is True and needs_stereo_camera is True
44
+ - observation/exterior_image_{i}_left: (H, W, 3) if n_external_cameras >= 1
45
+ - observation/exterior_image_{i}_right: (H, W, 3) if needs_stereo_camera is True
46
+ - session_id: (1,) if needs_session_id is True
47
+ - observation/joint_position: (7,)
48
+ - observation/cartesian_position: (6,)
49
+ - observation/gripper_position: (1,)
50
+ - prompt: str, the natural language task instruction for the policy
51
+
52
+ Action:
53
+ - action: (N, 8,) or (N, 7,): either 7 movement actions (for joint action spaces) or 6 (for cartesian) plus one dimension for gripper position
54
+ --> all N actions will get executed on the robot before the server is queried again
55
+
56
+ """
57
+
58
+ def __init__(
59
+ self,
60
+ policy: BasePolicy,
61
+ server_config: PolicyServerConfig,
62
+ host: str = "0.0.0.0",
63
+ port: int = 8000,
64
+ ) -> None:
65
+ self._policy = policy
66
+ self._server_config = server_config
67
+ self._host = host
68
+ self._port = port
69
+ logging.getLogger("websockets.server").setLevel(logging.INFO)
70
+
71
+ def serve_forever(self) -> None:
72
+ asyncio.run(self.run())
73
+
74
+ async def run(self):
75
+ async with websockets.asyncio.server.serve(
76
+ self._handler,
77
+ self._host,
78
+ self._port,
79
+ compression=None,
80
+ max_size=None,
81
+ ) as server:
82
+ await server.serve_forever()
83
+
84
+ async def _handler(self, websocket: websockets.asyncio.server.ServerConnection):
85
+ logging.info(f"Connection from {websocket.remote_address} opened")
86
+ packer = msgpack_numpy.Packer()
87
+
88
+ # Send server config to client to configure what gets sent to server.
89
+ await websocket.send(packer.pack(dataclasses.asdict(self._server_config)))
90
+
91
+ while True:
92
+ try:
93
+ obs = msgpack_numpy.unpackb(await websocket.recv())
94
+
95
+ endpoint = obs["endpoint"]
96
+ del obs["endpoint"]
97
+ if endpoint == "reset":
98
+ self._policy.reset(obs)
99
+ to_return = "reset successful"
100
+ else:
101
+ action = self._policy.infer(obs)
102
+ to_return = packer.pack(action)
103
+ await websocket.send(to_return)
104
+ except websockets.ConnectionClosed:
105
+ logging.info(f"Connection from {websocket.remote_address} closed")
106
+ break
107
+ except Exception:
108
+ await websocket.send(traceback.format_exc())
109
+ await websocket.close(
110
+ code=websockets.frames.CloseCode.INTERNAL_ERROR,
111
+ reason="Internal server error. Traceback included in previous frame.",
112
+ )
113
+ raise
114
+
115
+
116
+ if __name__ == "__main__":
117
+ import numpy as np
118
+
119
+ class DummyPolicy(BasePolicy):
120
+ def infer(self, obs):
121
+ return np.zeros((1, 8), dtype=np.float32)
122
+
123
+ def reset(self, reset_info):
124
+ pass
125
+
126
+ logging.basicConfig(level=logging.INFO)
127
+ policy = DummyPolicy()
128
+ server = WebsocketPolicyServer(policy, PolicyServerConfig())
129
+ server.serve_forever()
130
+
eval_utils/run_sim_eval.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Example script for running 10 rollouts of a DROID policy on the example environment.
3
+
4
+ Usage:
5
+
6
+ First, make sure you download the simulation assets and unpack them into the root directory of this package.
7
+
8
+ Then, in a separate terminal, launch the policy server on localhost:8000
9
+ -- make sure to set XLA_PYTHON_CLIENT_MEM_FRACTION to avoid JAX hogging all the GPU memory.
10
+
11
+ For example, to launch a pi0-FAST-DROID policy (with joint position control),
12
+ run the command below in a separate terminal from the openpi "karl/droid_policies" branch:
13
+
14
+ XLA_PYTHON_CLIENT_MEM_FRACTION=0.5 uv run scripts/serve_policy.py policy:checkpoint --policy.config=pi0_fast_droid_jointpos --policy.dir=s3://openpi-assets-simeval/pi0_fast_droid_jointpos
15
+
16
+ Finally, run the evaluation script:
17
+
18
+ python run_eval.py --episodes 10 --headless
19
+ """
20
+
21
+ import uuid
22
+
23
+ import tyro
24
+ import argparse
25
+ import gymnasium as gym
26
+ import torch
27
+ import cv2
28
+ import mediapy
29
+ import numpy as np
30
+ from datetime import datetime
31
+ from pathlib import Path
32
+ from PIL import Image
33
+ from tqdm import tqdm
34
+
35
+ from openpi_client import image_tools
36
+ from sim_evals.inference.abstract_client import InferenceClient
37
+ from policy_client import WebsocketClientPolicy
38
+
39
+
40
+ class DreamZeroJointPosClient(InferenceClient):
41
+ def __init__(self,
42
+ remote_host:str = "localhost",
43
+ remote_port:int = 6000,
44
+ open_loop_horizon:int = 8,
45
+ ) -> None:
46
+ self.client = WebsocketClientPolicy(remote_host, remote_port)
47
+ self.open_loop_horizon = open_loop_horizon
48
+ self.actions_from_chunk_completed = 0
49
+ self.pred_action_chunk = None
50
+ self.session_id = str(uuid.uuid4())
51
+
52
+ def visualize(self, request: dict):
53
+ """
54
+ Return the camera views how the model sees it
55
+ """
56
+ curr_obs = self._extract_observation(request)
57
+ right_img = image_tools.resize_with_pad(curr_obs["right_image"], 224, 224)
58
+ wrist_img = image_tools.resize_with_pad(curr_obs["wrist_image"], 224, 224)
59
+ left_img = image_tools.resize_with_pad(curr_obs["left_image"], 224, 224)
60
+ combined = np.concatenate([right_img, wrist_img, left_img], axis=1)
61
+ return combined
62
+
63
+ def reset(self):
64
+ self.actions_from_chunk_completed = 0
65
+ self.pred_action_chunk = None
66
+ self.session_id = str(uuid.uuid4())
67
+
68
+ def infer(self, obs: dict, instruction: str) -> dict:
69
+ """
70
+ Infer the next action from the policy in a server-client setup
71
+ """
72
+ curr_obs = self._extract_observation(obs)
73
+ if (
74
+ self.actions_from_chunk_completed == 0
75
+ or self.actions_from_chunk_completed >= self.open_loop_horizon
76
+ ):
77
+ self.actions_from_chunk_completed = 0
78
+ request_data = {
79
+ "observation/exterior_image_0_left": image_tools.resize_with_pad(curr_obs["right_image"], 180, 320),
80
+ "observation/exterior_image_1_left": image_tools.resize_with_pad(curr_obs["left_image"], 180, 320),
81
+ "observation/wrist_image_left": image_tools.resize_with_pad(curr_obs["wrist_image"], 180, 320),
82
+ "observation/joint_position": curr_obs["joint_position"].astype(np.float64),
83
+ "observation/cartesian_position": np.zeros((6,), dtype=np.float64), # dummy cartesian position
84
+ "observation/gripper_position": curr_obs["gripper_position"].astype(np.float64),
85
+ "prompt": instruction,
86
+ "session_id": self.session_id,
87
+ }
88
+ for k, v in request_data.items():
89
+ print(f"{k}: {v.shape if not isinstance(v, str) else v}")
90
+
91
+ result = self.client.infer(request_data)
92
+ actions = result["actions"] if isinstance(result, dict) else result
93
+ assert len(actions.shape) == 2, f"Expected 2D array, got shape {actions.shape}"
94
+ assert actions.shape[-1] == 8, f"Expected 8 action dimensions (7 joints + 1 gripper), got {actions.shape[-1]}"
95
+ self.pred_action_chunk = actions
96
+
97
+
98
+ action = self.pred_action_chunk[self.actions_from_chunk_completed]
99
+ self.actions_from_chunk_completed += 1
100
+
101
+ # binarize gripper action
102
+ if action[-1].item() > 0.5:
103
+ action = np.concatenate([action[:-1], np.ones((1,))])
104
+ else:
105
+ action = np.concatenate([action[:-1], np.zeros((1,))])
106
+
107
+ img1 = image_tools.resize_with_pad(curr_obs["right_image"], 224, 224)
108
+ img2 = image_tools.resize_with_pad(curr_obs["wrist_image"], 224, 224)
109
+ img3 = image_tools.resize_with_pad(curr_obs["left_image"], 224, 224)
110
+ both = np.concatenate([img1, img2, img3], axis=1)
111
+
112
+ return {"action": action, "viz": both}
113
+
114
+ def _extract_observation(self, obs_dict, *, save_to_disk=False):
115
+ # Assign images
116
+ right_image = obs_dict["policy"]["external_cam"][0].clone().detach().cpu().numpy()
117
+ left_image = obs_dict["policy"]["external_cam_2"][0].clone().detach().cpu().numpy()
118
+ wrist_image = obs_dict["policy"]["wrist_cam"][0].clone().detach().cpu().numpy()
119
+
120
+ # Capture proprioceptive state
121
+ robot_state = obs_dict["policy"]
122
+ joint_position = robot_state["arm_joint_pos"].clone().detach().cpu().numpy()
123
+ gripper_position = robot_state["gripper_pos"].clone().detach().cpu().numpy()
124
+
125
+ if save_to_disk:
126
+ combined_image = np.concatenate([right_image, wrist_image], axis=1)
127
+ combined_image = Image.fromarray(combined_image)
128
+ combined_image.save("robot_camera_views.png")
129
+
130
+ return {
131
+ "right_image": right_image,
132
+ "left_image": left_image,
133
+ "wrist_image": wrist_image,
134
+ "joint_position": joint_position,
135
+ "gripper_position": gripper_position,
136
+ }
137
+
138
+
139
+
140
+
141
+ def main(
142
+ episodes: int = 10,
143
+ scene: int = 1,
144
+ headless: bool = True,
145
+ host: str = "localhost",
146
+ port: int = 6000,
147
+ ):
148
+ # launch omniverse app with arguments (inside function to prevent overriding tyro)
149
+ from isaaclab.app import AppLauncher
150
+ parser = argparse.ArgumentParser(description="Tutorial on creating an empty stage.")
151
+ AppLauncher.add_app_launcher_args(parser)
152
+ args_cli, _ = parser.parse_known_args()
153
+ args_cli.enable_cameras = True
154
+ args_cli.headless = headless
155
+ app_launcher = AppLauncher(args_cli)
156
+ simulation_app = app_launcher.app
157
+
158
+ # All IsaacLab dependent modules should be imported after the app is launched
159
+ import sim_evals.environments # noqa: F401
160
+ from isaaclab_tasks.utils import parse_env_cfg
161
+
162
+
163
+ # Initialize the env
164
+ env_cfg = parse_env_cfg(
165
+ "DROID",
166
+ device=args_cli.device,
167
+ num_envs=1,
168
+ use_fabric=True,
169
+ )
170
+ instruction = None
171
+ match scene:
172
+ case 1:
173
+ instruction = "put the cube in the bowl"
174
+ case 2:
175
+ instruction = "pick up the can and put it in the mug"
176
+ case 3:
177
+ instruction = "put the banana in the bin"
178
+ case _:
179
+ raise ValueError(f"Scene {scene} not supported")
180
+
181
+ env_cfg.set_scene(scene)
182
+ env = gym.make("DROID", cfg=env_cfg)
183
+
184
+ obs, _ = env.reset()
185
+ obs, _ = env.reset() # need second render cycle to get correctly loaded materials
186
+ client = DreamZeroJointPosClient(remote_host=host, remote_port=port)
187
+
188
+
189
+ video_dir = Path("runs") / datetime.now().strftime("%Y-%m-%d") / datetime.now().strftime("%H-%M-%S")
190
+ video_dir.mkdir(parents=True, exist_ok=True)
191
+ video = []
192
+ ep = 0
193
+ max_steps = env.env.max_episode_length
194
+ with torch.no_grad():
195
+ for ep in range(episodes):
196
+ for _ in tqdm(range(max_steps), desc=f"Episode {ep+1}/{episodes}"):
197
+ ret = client.infer(obs, instruction)
198
+ if not headless:
199
+ cv2.imshow("Right Camera", cv2.cvtColor(ret["viz"], cv2.COLOR_RGB2BGR))
200
+ cv2.waitKey(1)
201
+ video.append(ret["viz"])
202
+ action = torch.tensor(ret["action"])[None]
203
+ obs, _, term, trunc, _ = env.step(action)
204
+ if term or trunc:
205
+ break
206
+
207
+ client.reset()
208
+ mediapy.write_video(
209
+ video_dir / f"episode_{ep}.mp4",
210
+ video,
211
+ fps=15,
212
+ )
213
+ video = []
214
+
215
+ env.close()
216
+ simulation_app.close()
217
+
218
+ if __name__ == "__main__":
219
+ args = tyro.cli(main)
eval_utils/serve_dreamzero_wan22.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Serve the DreamZero 5B implementation (Wan2.2-TI2V-5B) over the websocket policy server.
3
+
4
+ This is the 5B model: Wan2.2 diffusion backbone, 48-channel VAE38, frame_seqlen=50 (160×320
5
+ latent 10×20). Inference is causal with KV caching: first request in a session uses 1 frame
6
+ and warms the cache; subsequent requests use FRAMES_PER_CHUNK=4 frames and append to the cache.
7
+ On session_id change (or explicit reset), buffers and action_head.current_start_frame are cleared.
8
+
9
+ The checkpoint at model_path should be DreamZero with Wan22 5B (model/dreamzero/action_head=
10
+ wan_flow_matching_action_tf_wan22, data droid_relative_wan22 → 160×320). GrootSimPolicy loads
11
+ that checkpoint and runs inference; it is the correct policy class for DreamZero.
12
+
13
+ Usage (single GPU):
14
+
15
+ torchrun --nproc_per_node=1 eval_utils/serve_dreamzero_wan22.py --model_path ./checkpoints/dreamzero_droid_wan22_smoke --port 8000
16
+
17
+ # Or single process:
18
+ python eval_utils/serve_dreamzero_wan22.py --model_path ./checkpoints/dreamzero_droid_wan22_smoke --port 8000
19
+
20
+ Client: send observations per PolicyServerConfig (policy_server.py). Video is resized to the
21
+ checkpoint's expected resolution (e.g. 180×320) so the eval transform accepts it; the 5B action
22
+ head resizes to 160×320 internally. Override with --image_height/--image_width if needed.
23
+ Response is an action chunk (N, 8). Use session_id for episode boundaries.
24
+ """
25
+
26
+ import datetime
27
+ import logging
28
+ import os
29
+ import sys
30
+
31
+ import imageio
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ import cv2
36
+ import numpy as np
37
+ import torch
38
+ import torch.distributed as dist
39
+ from torch.distributed.device_mesh import init_device_mesh
40
+ import tyro
41
+
42
+ # Avoid FailOnRecompileLimitHit when serving: the flow scheduler's torch.compile'd
43
+ # multistep_uni_p_bh_update recompiles under varying shapes/inputs (e.g. batch size,
44
+ # step_index, order). Increase limits so the server doesn't hit the default cap.
45
+ _dynamo = torch._dynamo.config
46
+ if hasattr(_dynamo, "cache_size_limit"):
47
+ _dynamo.cache_size_limit = 1000
48
+ if hasattr(_dynamo, "recompile_limit"):
49
+ _dynamo.recompile_limit = 800
50
+ if hasattr(_dynamo, "accumulated_cache_size_limit"):
51
+ _dynamo.accumulated_cache_size_limit = 1000
52
+ if hasattr(_dynamo, "accumulated_recompile_limit"):
53
+ _dynamo.accumulated_recompile_limit = 2000
54
+ from pathlib import Path
55
+ from tianshou.data import Batch
56
+
57
+ # Add repo root for imports
58
+ REPO_ROOT = Path(__file__).resolve().parents[1]
59
+ if str(REPO_ROOT) not in sys.path:
60
+ sys.path.insert(0, str(REPO_ROOT))
61
+
62
+ from openpi_client.base_policy import BasePolicy
63
+
64
+ from eval_utils.policy_server import WebsocketPolicyServer, PolicyServerConfig
65
+ from groot.vla.model.n1_5.sim_policy import GrootSimPolicy
66
+ from groot.vla.data.schema import EmbodimentTag
67
+ from groot.vla.data.transform import ComposedModalityTransform
68
+
69
+
70
+ # DreamZero Wan 5B is trained with 160×320 (droid_relative_wan22). Fallback if we cannot read from policy.
71
+ DEFAULT_IMAGE_HEIGHT = 160
72
+ DEFAULT_IMAGE_WIDTH = 320
73
+ FRAMES_PER_CHUNK = 4 # matches 5B num_frame_per_block for causal chunked inference
74
+
75
+
76
+ def _get_expected_video_resolution(policy: GrootSimPolicy) -> tuple[int, int]:
77
+ """Get (height, width) the policy's eval_transform expects for video (from checkpoint
78
+ metadata). Resolution in metadata is (width, height); we return (height, width) for resize.
79
+ DreamZero Wan 5B (droid_relative_wan22) uses 160×320; other configs may use e.g. 180×320.
80
+ """
81
+ eval_transform = getattr(policy, "eval_transform", None)
82
+ if eval_transform is None:
83
+ return (DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH)
84
+ if not isinstance(eval_transform, ComposedModalityTransform):
85
+ return (DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH)
86
+ for t in eval_transform.transforms:
87
+ if hasattr(t, "original_resolutions") and getattr(t, "original_resolutions", None):
88
+ res = t.original_resolutions
89
+ if res:
90
+ # original_resolutions values are (width, height)
91
+ w, h = next(iter(res.values()))
92
+ return (int(h), int(w))
93
+ return (DEFAULT_IMAGE_HEIGHT, DEFAULT_IMAGE_WIDTH)
94
+
95
+
96
+ def _resize_frames_to_resolution(frames: np.ndarray, target_h: int, target_w: int) -> np.ndarray:
97
+ """Resize video frames to (target_h, target_w). Accepts (H,W,C) or (T,H,W,C)."""
98
+ if frames.ndim == 3:
99
+ if (frames.shape[0], frames.shape[1]) != (target_h, target_w):
100
+ frames = cv2.resize(frames, (target_w, target_h), interpolation=cv2.INTER_LINEAR)
101
+ return frames
102
+ out = np.stack(
103
+ [cv2.resize(f, (target_w, target_h), interpolation=cv2.INTER_LINEAR) for f in frames],
104
+ axis=0,
105
+ )
106
+ return out
107
+
108
+
109
+ def _maybe_init_distributed():
110
+ """Initialize process group for single-GPU or multi-GPU. Required by GrootSimPolicy."""
111
+ if dist.is_initialized():
112
+ return
113
+ os.environ.setdefault("MASTER_ADDR", "localhost")
114
+ os.environ.setdefault("MASTER_PORT", "29500")
115
+ dist.init_process_group(backend="nccl", rank=0, world_size=1)
116
+ torch.cuda.set_device(0)
117
+
118
+
119
+ # Modality key mappings: client observation keys -> model input keys per embodiment.
120
+ # Client sends: observation/exterior_image_0_left, exterior_image_1_left, wrist_image_left.
121
+ VIDEO_KEY_MAPPING = {
122
+ "oxe_droid": {
123
+ "observation/exterior_image_0_left": "video.exterior_image_1_left",
124
+ "observation/exterior_image_1_left": "video.exterior_image_2_left",
125
+ "observation/wrist_image_left": "video.wrist_image_left",
126
+ },
127
+ }
128
+ STATE_KEY_MAPPING = {
129
+ "oxe_droid": ("state.joint_position", "state.gripper_position"),
130
+ }
131
+ LANGUAGE_KEY_MAPPING = {
132
+ "oxe_droid": "annotation.language.action_text",
133
+ }
134
+
135
+
136
+ class DreamZeroWan225BPolicy(BasePolicy):
137
+ """
138
+ Wraps GrootSimPolicy for the DreamZero 5B implementation (Wan2.2-TI2V-5B).
139
+
140
+ Converts roboarena observation/action format to DROID/Batch. Video is resized to the
141
+ resolution expected by the policy's eval_transform (from checkpoint metadata) so
142
+ VideoToTensor validation passes. The 5B action head then resizes to 160×320 internally.
143
+ First call in a session uses 1 frame; later calls use 4 frames (FRAMES_PER_CHUNK).
144
+ Session reset clears frame buffers and action_head.current_start_frame.
145
+ """
146
+
147
+ def __init__(
148
+ self,
149
+ groot_policy: GrootSimPolicy,
150
+ image_height: int,
151
+ image_width: int,
152
+ embodiment_tag: str = "oxe_droid",
153
+ save_video_pred: bool = False,
154
+ video_output_dir: str = "./video_pred_output",
155
+ ):
156
+ super().__init__()
157
+ self._policy = groot_policy
158
+ self._image_height = image_height
159
+ self._image_width = image_width
160
+ self._embodiment_tag = (
161
+ embodiment_tag if embodiment_tag in VIDEO_KEY_MAPPING else "oxe_droid"
162
+ )
163
+ video_keys = list(VIDEO_KEY_MAPPING[self._embodiment_tag].values())
164
+ self._frame_buffers = {k: [] for k in video_keys}
165
+ self._is_first_call = True
166
+ self._current_session_id = None
167
+ self._save_video_pred = save_video_pred
168
+ self._video_output_dir = video_output_dir
169
+ self._video_pred_latents: list[torch.Tensor] = []
170
+ self._current_prompt: str = ""
171
+
172
+ def _convert_observation(self, obs: dict) -> dict:
173
+ """Convert roboarena observation format to model Batch format.
174
+ Incoming frames are resized to the policy's expected (height, width) so
175
+ eval_transform's VideoToTensor check passes.
176
+ """
177
+ image_key_mapping = VIDEO_KEY_MAPPING[self._embodiment_tag]
178
+ for roboarena_key, model_key in image_key_mapping.items():
179
+ if roboarena_key in obs:
180
+ data = obs[roboarena_key]
181
+ if isinstance(data, np.ndarray):
182
+ data = _resize_frames_to_resolution(
183
+ data, self._image_height, self._image_width
184
+ )
185
+ if data.ndim == 4:
186
+ self._frame_buffers[model_key].extend(list(data))
187
+ else:
188
+ self._frame_buffers[model_key].append(data)
189
+
190
+ num_frames = 1 if self._is_first_call else FRAMES_PER_CHUNK
191
+ converted = {}
192
+ for model_key, buffer in self._frame_buffers.items():
193
+ if len(buffer) > 0:
194
+ if len(buffer) >= num_frames:
195
+ frames_to_use = buffer[-num_frames:]
196
+ else:
197
+ frames_to_use = buffer.copy()
198
+ while len(frames_to_use) < num_frames:
199
+ frames_to_use.insert(0, buffer[0])
200
+ video = np.stack(frames_to_use, axis=0)
201
+ converted[model_key] = video
202
+
203
+ state_joint_key, state_gripper_key = STATE_KEY_MAPPING[self._embodiment_tag]
204
+ if "observation/joint_position" in obs:
205
+ joint_pos = np.asarray(obs["observation/joint_position"])
206
+ if joint_pos.ndim == 1:
207
+ joint_pos = joint_pos.reshape(1, -1)
208
+ converted[state_joint_key] = joint_pos.astype(np.float64)
209
+ else:
210
+ converted[state_joint_key] = np.zeros((1, 7), dtype=np.float64)
211
+
212
+ if "observation/gripper_position" in obs:
213
+ gripper_pos = np.asarray(obs["observation/gripper_position"])
214
+ if gripper_pos.ndim == 1:
215
+ gripper_pos = gripper_pos.reshape(1, -1)
216
+ converted[state_gripper_key] = gripper_pos.astype(np.float64)
217
+ else:
218
+ converted[state_gripper_key] = np.zeros((1,1), dtype=np.float64)
219
+
220
+ text_prompt = obs.get("prompt", "")
221
+ logger.info("Text prompt: %s", text_prompt)
222
+ if text_prompt:
223
+ self._current_prompt = text_prompt
224
+ lang_key = LANGUAGE_KEY_MAPPING[self._embodiment_tag]
225
+ converted[lang_key] = text_prompt
226
+ return converted
227
+
228
+ def _convert_action(self, action_dict: dict) -> np.ndarray:
229
+ """Convert model action dict to (N, 8) array (7 joint + 1 gripper)."""
230
+ joint_action = None
231
+ gripper_action = None
232
+ for key, value in action_dict.items():
233
+ if ("joint_position" in key or "joint_pos" in key) and "gripper" not in key:
234
+ joint_action = value
235
+ elif "gripper_position" in key or "gripper" in key:
236
+ gripper_action = value
237
+ if joint_action is None:
238
+ return np.zeros((1, 8), dtype=np.float32)
239
+ if isinstance(joint_action, torch.Tensor):
240
+ joint_action = joint_action.cpu().numpy()
241
+ if joint_action.ndim == 1:
242
+ joint_action = joint_action.reshape(1, -1)
243
+ N = joint_action.shape[0]
244
+ if gripper_action is not None:
245
+ if isinstance(gripper_action, torch.Tensor):
246
+ gripper_action = gripper_action.cpu().numpy()
247
+ if gripper_action.ndim == 1:
248
+ gripper_action = gripper_action.reshape(-1, 1)
249
+ if gripper_action.shape[-1] > 1:
250
+ gripper_action = gripper_action[..., :1]
251
+ else:
252
+ gripper_action = np.zeros((N, 1), dtype=np.float32)
253
+ return np.concatenate([joint_action, gripper_action], axis=-1).astype(np.float32)
254
+
255
+ def infer(self, obs: dict) -> np.ndarray:
256
+ session_id = obs.get("session_id")
257
+ if session_id is not None and session_id != self._current_session_id:
258
+ if self._current_session_id is not None:
259
+ self.reset({})
260
+ self._current_session_id = session_id
261
+
262
+ converted_obs = self._convert_observation(obs)
263
+ batch = Batch(obs=converted_obs)
264
+ with torch.no_grad():
265
+ result_batch, video_pred = self._policy.lazy_joint_forward_causal(batch)
266
+ if self._save_video_pred and video_pred is not None:
267
+ self._video_pred_latents.append(video_pred.detach())
268
+ action_dict = {}
269
+ action_chunk_dict = result_batch.act
270
+ for k in dir(action_chunk_dict):
271
+ if k.startswith("action."):
272
+ action_dict[k] = getattr(action_chunk_dict, k)
273
+ action = self._convert_action(action_dict)
274
+ if self._is_first_call:
275
+ self._is_first_call = False
276
+ return action
277
+
278
+ def _save_predicted_video(self) -> None:
279
+ """Decode accumulated video prediction latents through the VAE and save as mp4."""
280
+ if not self._video_pred_latents:
281
+ return
282
+ try:
283
+ from einops import rearrange
284
+
285
+ action_head = self._policy.trained_model.action_head
286
+ latents = torch.cat(self._video_pred_latents, dim=2)
287
+ with torch.no_grad():
288
+ frames = action_head.vae.decode(
289
+ latents,
290
+ tiled=action_head.tiled,
291
+ tile_size=(action_head.tile_size_height, action_head.tile_size_width),
292
+ tile_stride=(action_head.tile_stride_height, action_head.tile_stride_width),
293
+ )
294
+ frames = rearrange(frames, "B C T H W -> B T H W C")[0]
295
+ frames = ((frames.float() + 1) * 127.5).clip(0, 255).cpu().numpy().astype(np.uint8)
296
+
297
+ os.makedirs(self._video_output_dir, exist_ok=True)
298
+ timestamp = datetime.datetime.now().strftime("%m_%d_%H_%M_%S")
299
+ n_latent_frames = latents.shape[2]
300
+ existing = [f for f in os.listdir(self._video_output_dir) if f.endswith(".mp4")]
301
+ safe_prompt = self._current_prompt.replace(" ", "_")
302
+ safe_prompt = "".join(c for c in safe_prompt if c.isalnum() or c in "_-.")
303
+ if len(safe_prompt) > 80:
304
+ safe_prompt = safe_prompt[:80]
305
+ if not safe_prompt:
306
+ safe_prompt = "no_prompt"
307
+ output_path = os.path.join(
308
+ self._video_output_dir,
309
+ f"{len(existing):06}_{safe_prompt}_{timestamp}.mp4",
310
+ )
311
+ imageio.mimsave(output_path, list(frames), fps=5, codec="libx264")
312
+ logger.info("Saved video prediction (%d frames) to %s", len(frames), output_path)
313
+ except Exception as e:
314
+ logger.warning("Failed to save video prediction: %s", e)
315
+
316
+ def reset(self, reset_info: dict) -> None:
317
+ if self._save_video_pred:
318
+ self._save_predicted_video()
319
+ self._video_pred_latents.clear()
320
+ self._current_prompt = ""
321
+ for key in self._frame_buffers:
322
+ self._frame_buffers[key] = []
323
+ self._is_first_call = True
324
+ self._current_session_id = None
325
+ if hasattr(self._policy.trained_model, "action_head") and hasattr(
326
+ self._policy.trained_model.action_head, "current_start_frame"
327
+ ):
328
+ self._policy.trained_model.action_head.current_start_frame = 0
329
+
330
+
331
+ def main(
332
+ model_path: str = "./checkpoints/dreamzero_droid_wan22_smoke",
333
+ embodiment_tag: str = "oxe_droid",
334
+ tokenizer_path: str | None = None,
335
+ port: int = 8000,
336
+ host: str = "0.0.0.0",
337
+ image_height: int | None = None,
338
+ image_width: int | None = None,
339
+ save_video_pred: bool = False,
340
+ video_output_dir: str = "./video_pred_output",
341
+ ) -> None:
342
+ logging.basicConfig(level=logging.INFO, force=True)
343
+
344
+ _maybe_init_distributed()
345
+ device_mesh = init_device_mesh("cuda", mesh_shape=(1,), mesh_dim_names=("ip",))
346
+
347
+ logger.info("Loading DreamZero Wan22 policy from %s (embodiment=%s)", model_path, embodiment_tag)
348
+ checkpoint_name = os.path.basename(model_path.rstrip("/"))
349
+ video_output_dir = os.path.join(video_output_dir, checkpoint_name)
350
+ policy = GrootSimPolicy(
351
+ embodiment_tag=EmbodimentTag(embodiment_tag),
352
+ model_path=model_path,
353
+ tokenizer_path_override=tokenizer_path,
354
+ device="cuda" if torch.cuda.is_available() else "cpu",
355
+ device_mesh=device_mesh,
356
+ )
357
+ if image_height is not None and image_width is not None:
358
+ h, w = image_height, image_width
359
+ logger.info("Using CLI video resolution: %dx%d", h, w)
360
+ else:
361
+ h, w = _get_expected_video_resolution(policy)
362
+ logger.info("Using checkpoint video resolution: %dx%d (HxW)", h, w)
363
+ wrapper = DreamZeroWan225BPolicy(
364
+ groot_policy=policy,
365
+ image_height=h,
366
+ image_width=w,
367
+ embodiment_tag=embodiment_tag,
368
+ save_video_pred=save_video_pred,
369
+ video_output_dir=video_output_dir,
370
+ )
371
+
372
+ server_config = PolicyServerConfig(
373
+ image_resolution=(h, w),
374
+ needs_wrist_camera=True,
375
+ n_external_cameras=2,
376
+ needs_stereo_camera=False,
377
+ needs_session_id=True,
378
+ action_space="joint_position",
379
+ )
380
+ logger.info("Starting WebsocketPolicyServer on %s:%d (DreamZero 5B, %dx%d)", host, port, h, w)
381
+ server = WebsocketPolicyServer(
382
+ policy=wrapper,
383
+ server_config=server_config,
384
+ host=host,
385
+ port=port,
386
+ )
387
+ server.serve_forever()
388
+
389
+
390
+ if __name__ == "__main__":
391
+ tyro.cli(main)
groot/__init__.py ADDED
File without changes
groot/control/__init__.py ADDED
File without changes
groot/control/tensorrt_utils.py ADDED
@@ -0,0 +1,852 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import torch
2
+ import os
3
+ import subprocess
4
+ import tensorrt as trt
5
+ import sys
6
+ import atexit
7
+ import ctypes
8
+ import modelopt.torch.quantization as mtq
9
+ from typing import Dict, List, Tuple
10
+ import shutil
11
+
12
+ import numpy as np
13
+ import torch
14
+
15
+
16
+ FP8_DEFAULT_CONFIG = {
17
+ "quant_cfg": {
18
+ "*weight_quantizer": {"num_bits": (4, 3), "axis": None},
19
+ "*input_quantizer": {"num_bits": (4, 3), "axis": None},
20
+ "*output_quantizer": {"enable": False},
21
+ "*[qkv]_bmm_quantizer": {"num_bits": (4, 3), "axis": None},
22
+ "*softmax_quantizer": {
23
+ "num_bits": (4, 3),
24
+ "axis": None,
25
+ },
26
+ "default": {"enable": False},
27
+ },
28
+ "algorithm": "max",
29
+ }
30
+
31
+ NVFP4_DEFAULT_CONFIG = {
32
+ "quant_cfg": {
33
+ "*weight_quantizer": {
34
+ "num_bits": (2, 1),
35
+ "block_sizes": {-1: 16, "type": "dynamic", "scale_bits": (4, 3)},
36
+ "axis": None,
37
+ "enable": True,
38
+ },
39
+ "*input_quantizer": {
40
+ "num_bits": (2, 1),
41
+ "block_sizes": {-1: 16, "type": "dynamic", "scale_bits": (4, 3)},
42
+ "axis": None,
43
+ "enable": True,
44
+ },
45
+ "*output_quantizer": {"enable": False},
46
+ "*[qkv]_bmm_quantizer": {"num_bits": (4, 3), "axis": None},
47
+ "*softmax_quantizer": {
48
+ "num_bits": (4, 3),
49
+ "axis": None,
50
+ },
51
+ "default": {"enable": False},
52
+ },
53
+ "algorithm": "max",
54
+ }
55
+
56
+
57
+
58
+ def wan_quantize(
59
+ policy,
60
+ quantization_config,
61
+ model_type,
62
+ forward_loop,
63
+ ):
64
+ """Quantize the VLA model using ModelOpt - simplified to use calc_mse_for_single_trajectory."""
65
+
66
+ # Configure quantization - disable problematic layers
67
+ if "quant_cfg" in quantization_config:
68
+ quantization_config["quant_cfg"]["*patch_embedding*"] = {"enable": False}
69
+ # if model_type == "14B" or model_type == "ar_14B":
70
+ # # Workaround: until we understand the issue https://nvbugspro.nvidia.com/bug/5612316
71
+ # quantization_config["quant_cfg"]["*.self_attn.o.*"] = {"enable": False}
72
+ # quantization_config["quant_cfg"]["*.cross_attn.o.*"] = {"enable": False}
73
+
74
+ policy.trained_model.action_head.model = mtq.quantize(
75
+ policy.trained_model.action_head.model, quantization_config, forward_loop=forward_loop
76
+ )
77
+ mtq.print_quant_summary(policy.trained_model.action_head.model)
78
+
79
+ return
80
+
81
+
82
+ def wan_trt_quantize_and_load_engine(
83
+ policy,
84
+ cfg,
85
+ onnx_path,
86
+ engine_path,
87
+ model_type,
88
+ forward_loop,
89
+ ):
90
+ if (
91
+ os.path.exists(os.path.dirname(engine_path))
92
+ and cfg.inference_mode == "trt_build"
93
+ ):
94
+ shutil.rmtree(os.path.dirname(engine_path))
95
+
96
+ quantization_config = None
97
+ if cfg.quantize_dtype == "fp8":
98
+ quantization_config = FP8_DEFAULT_CONFIG.copy()
99
+ elif cfg.quantize_dtype == "nvfp4":
100
+ quantization_config = NVFP4_DEFAULT_CONFIG.copy()
101
+ else:
102
+ print(f"Quantization type {cfg.quantize_dtype} not supported. Skipping quantization.")
103
+
104
+ if quantization_config is not None and cfg.inference_mode == "trt_build":
105
+ #policy.trained_model.action_head.model.to(torch.float16)
106
+ wan_quantize(
107
+ policy,
108
+ quantization_config,
109
+ model_type=model_type,
110
+ forward_loop=forward_loop,
111
+ )
112
+
113
+ if cfg.inference_mode == "trt_build":
114
+ policy.trained_model.action_head.model.to(torch.float16)
115
+
116
+ print("Export model:", policy.trained_model.action_head.model)
117
+
118
+ test_inputs = create_wan_test_inputs(policy, device="cuda", model_type=model_type)
119
+ min_shape = None
120
+ max_shape = None
121
+ opt_shape = None
122
+
123
+ if model_type == "ar_14B":
124
+
125
+ policy.trained_model.action_head.model.forward = policy.trained_model.action_head.model._forward_inference_trt
126
+ dynamic_axes = {
127
+ "kv_cache_packed": {3: "kv_cache_len"},
128
+ }
129
+ min_shape = "kv_cache_packed:40x2x1x880x40x128"
130
+ max_shape = "kv_cache_packed:40x2x1x8800x40x128"
131
+ opt_shape = "kv_cache_packed:40x2x1x7920x40x128"
132
+ elif model_type == "ar_14B_droid":
133
+ policy.trained_model.action_head.model.forward = policy.trained_model.action_head.model._forward_inference_trt
134
+ dynamic_axes = {
135
+ "kv_cache_packed": {3: "kv_cache_len"},
136
+ }
137
+ min_shape = "kv_cache_packed:40x2x1x880x40x128"
138
+ max_shape = "kv_cache_packed:40x2x1x8800x40x128"
139
+ opt_shape = "kv_cache_packed:40x2x1x7920x40x128"
140
+ elif model_type == "ar_5B_n6":
141
+ policy.trained_model.action_head.model.forward = policy.trained_model.action_head.model._forward_inference_trt
142
+ dynamic_axes = {
143
+ "kv_cache_packed": {3: "kv_cache_len"},
144
+ }
145
+ min_shape = "kv_cache_packed:30x2x1x220x24x128"
146
+ max_shape = "kv_cache_packed:30x2x1x3080x24x128"
147
+ opt_shape = "kv_cache_packed:30x2x1x2860x24x128"
148
+ else:
149
+ dynamic_axes = None
150
+
151
+ if cfg.quantize_dtype == "nvfp4":
152
+ export_to_onnx_fp4(policy.trained_model.action_head.model, test_inputs, onnx_path, dynamic_axes=dynamic_axes)
153
+ else:
154
+ export_to_onnx(
155
+ policy.trained_model.action_head.model,
156
+ test_inputs,
157
+ onnx_path,
158
+ model_type=model_type,
159
+ quantization_mode=cfg.quantize_dtype,
160
+ dynamic_axes=dynamic_axes,
161
+ )
162
+
163
+ build_tensorrt_engine(onnx_path, engine_path, min_shape, max_shape, opt_shape)
164
+
165
+ trt_wan_model = load_tensorrt_engine(engine_path, model_type=model_type)
166
+ policy.trained_model.action_head.model = trt_wan_model
167
+
168
+ def export_to_onnx_fp4(model, test_inputs, onnx_save_path, dynamic_axes=None):
169
+ from modelopt.torch._deploy.utils.torch_onnx import OnnxBytes
170
+ from modelopt.torch._deploy.utils.torch_onnx import get_onnx_bytes_and_metadata
171
+
172
+ print("exporting to onnx fp4")
173
+ try:
174
+ onnx_bytes, _ = get_onnx_bytes_and_metadata(model=model, dummy_input=test_inputs, dynamic_axes=dynamic_axes)
175
+ onnx_model = OnnxBytes.from_bytes(onnx_bytes)
176
+ except Exception as e:
177
+ print(f"Error exporting model to ONNX: {e}")
178
+ return
179
+ save_dir = os.path.dirname(os.path.abspath(onnx_save_path))
180
+ os.makedirs(save_dir, exist_ok=True)
181
+ for filename, file_bytes in onnx_model.onnx_model.items():
182
+ file_path = os.path.join(save_dir, filename)
183
+ with open(file_path, "wb") as f:
184
+ f.write(file_bytes)
185
+ print(f"exported onnx to {file_path}")
186
+
187
+
188
+ def export_to_onnx(
189
+ pytorch_model,
190
+ test_inputs,
191
+ onnx_path="tensorrt/wan_model.onnx",
192
+ model_type="5B",
193
+ quantization_mode="fp8",
194
+ dynamic_axes=None,
195
+ ):
196
+ #
197
+ if model_type == "5B":
198
+ return export_to_onnx_5B(pytorch_model, test_inputs, onnx_path, dynamic_axes)
199
+ elif model_type == "14B":
200
+ return export_to_onnx_14B(pytorch_model, test_inputs, onnx_path, dynamic_axes)
201
+ elif model_type == "ar_14B" or model_type == "ar_14B_droid":
202
+ return export_to_onnx_ar_14B(pytorch_model, test_inputs, onnx_path, dynamic_axes)
203
+ else:
204
+ raise ValueError(f"Model type {model_type} not supported")
205
+
206
+
207
+ def export_to_onnx_ar_14B(pytorch_model, test_inputs, onnx_path="tensorrt/wan_model.onnx", dynamic_axes=None):
208
+ """Export PyTorch model to ONNX"""
209
+ print("Exporting AR 14B model to ONNX...", onnx_path)
210
+
211
+ # Create directory if it doesn't exist
212
+ os.makedirs(os.path.dirname(onnx_path), exist_ok=True)
213
+ pytorch_model.eval()
214
+ pytorch_model.to(torch.float16)
215
+
216
+ input_names = [
217
+ "x",
218
+ "timestep",
219
+ "context",
220
+ "kv_cache_packed",
221
+ "y",
222
+ "clip_feature",
223
+ "action",
224
+ "timestep_action",
225
+ "state",
226
+ ]
227
+ output_names = ["video_noise_pred", "action_noise_pred"]
228
+
229
+ try:
230
+ with torch.no_grad():
231
+ torch.onnx.export(
232
+ pytorch_model,
233
+ test_inputs,
234
+ onnx_path,
235
+ export_params=True,
236
+ opset_version=20,
237
+ do_constant_folding=True,
238
+ input_names=input_names,
239
+ output_names=output_names,
240
+ dynamic_axes=dynamic_axes,
241
+ )
242
+ print(f" ONNX model exported to: {onnx_path}")
243
+ return onnx_path
244
+
245
+ except Exception as e:
246
+ import traceback
247
+ print(f" ERROR: ONNX export failed. Exception type: {type(e)}")
248
+ print("Traceback:")
249
+ traceback.print_exc()
250
+ return None
251
+
252
+
253
+
254
+ def export_to_onnx_5B(pytorch_model, test_inputs, onnx_path="tensorrt/wan_model.onnx"):
255
+ """Export PyTorch model to ONNX"""
256
+ print("Exporting model to ONNX...")
257
+
258
+ # Create directory if it doesn't exist
259
+ os.makedirs(os.path.dirname(onnx_path), exist_ok=True)
260
+ pytorch_model.eval()
261
+ pytorch_model.to(torch.float16)
262
+
263
+ x, action, timestep, context, state, embodiment_id = test_inputs
264
+
265
+ # Define input names for better ONNX graph
266
+ input_names = ["x", "action", "timestep", "context", "state", "embodiment_id"]
267
+ output_names = ["video_noise_pred", "action_noise_pred"]
268
+
269
+ try:
270
+ with torch.no_grad():
271
+ torch.onnx.export(
272
+ pytorch_model,
273
+ (x, action, timestep, context, state, embodiment_id),
274
+ onnx_path,
275
+ export_params=True,
276
+ opset_version=20,
277
+ do_constant_folding=True,
278
+ input_names=input_names,
279
+ output_names=output_names,
280
+ )
281
+ print(f" ONNX model exported to: {onnx_path}")
282
+ return onnx_path
283
+
284
+ except Exception as e:
285
+ import traceback
286
+ print(f" ERROR: ONNX export failed. Exception type: {type(e)}")
287
+ print("Traceback:")
288
+ traceback.print_exc()
289
+ return None
290
+
291
+
292
+ def export_to_onnx_14B(pytorch_model, test_inputs, onnx_path="tensorrt/wan_model.onnx"):
293
+ """Export PyTorch model to ONNX"""
294
+ print("Exporting model to ONNX...")
295
+
296
+ # Create directory if it doesn't exist
297
+ os.makedirs(os.path.dirname(onnx_path), exist_ok=True)
298
+ pytorch_model.eval()
299
+ pytorch_model.to(torch.float16)
300
+
301
+ x, action, timestep, context, state, embodiment_id, clip_feature, y = test_inputs
302
+
303
+ # Define input names for better ONNX graph
304
+ input_names = [
305
+ "x",
306
+ "action",
307
+ "timestep",
308
+ "context",
309
+ "state",
310
+ "embodiment_id",
311
+ "clip_feature",
312
+ "y",
313
+ ]
314
+ output_names = ["video_noise_pred", "action_noise_pred"]
315
+
316
+ try:
317
+ with torch.no_grad():
318
+ torch.onnx.export(
319
+ pytorch_model,
320
+ (x, action, timestep, context, state, embodiment_id, clip_feature, y),
321
+ onnx_path,
322
+ export_params=True,
323
+ opset_version=20,
324
+ do_constant_folding=True,
325
+ input_names=input_names,
326
+ output_names=output_names,
327
+ )
328
+ print(f" ONNX model exported to: {onnx_path}")
329
+ return onnx_path
330
+
331
+ except Exception as e:
332
+ import traceback
333
+ print(f" ERROR: ONNX export failed. Exception type: {type(e)}")
334
+ print("Traceback:")
335
+ traceback.print_exc()
336
+ return None
337
+
338
+
339
+ def build_tensorrt_engine(onnx_path, engine_path="tensorrt/wan_model.trt", min_shape=None, max_shape=None, opt_shape=None):
340
+ """Build TensorRT engine from ONNX using trtexec"""
341
+ print("Building TensorRT engine with trtexec...")
342
+
343
+ if not os.path.exists(onnx_path):
344
+ print(f" ERROR: ONNX file not found: {onnx_path}")
345
+ return None
346
+
347
+ # Create directory if it doesn't exist
348
+ os.makedirs(os.path.dirname(engine_path), exist_ok=True)
349
+
350
+ # Build engine using trtexec (much faster than torch_tensorrt)
351
+ trtexec_bin = shutil.which("trtexec") or "/opt/tensorrt/bin/trtexec"
352
+ cmd = [
353
+ trtexec_bin,
354
+ f"--onnx={onnx_path}",
355
+ f"--saveEngine={engine_path}",
356
+ "--fp8",
357
+ "--fp16",
358
+ "--bf16",
359
+ "--separateProfileRun",
360
+ "--profilingVerbosity=detailed",
361
+ "--memPoolSize=workspace:65536",
362
+ "--dumpProfile",
363
+ "--dumpLayerInfo",
364
+ "--useCudaGraph",
365
+ "--verbose",
366
+ ]
367
+
368
+ if min_shape is not None:
369
+ cmd.append(f"--minShapes={min_shape}")
370
+ if max_shape is not None:
371
+ cmd.append(f"--maxShapes={max_shape}")
372
+ if opt_shape is not None:
373
+ cmd.append(f"--optShapes={opt_shape}")
374
+
375
+ # Create log file for trtexec output
376
+ log_file = engine_path.replace(".trt", "_build.log")
377
+
378
+ try:
379
+ print(f" Running: {' '.join(cmd)}")
380
+ print(f" Logging output to: {log_file}")
381
+
382
+ with open(log_file, "w") as f:
383
+ result = subprocess.run(cmd, stdout=f, stderr=subprocess.STDOUT, text=True, timeout=600)
384
+
385
+ if result.returncode == 0:
386
+ print(f" TensorRT engine built successfully: {engine_path}")
387
+ print(f" Build log saved to: {log_file}")
388
+ return engine_path
389
+ else:
390
+ print(f" ERROR: trtexec failed with return code {result.returncode}")
391
+ print(f" Check build log for details: {log_file}")
392
+ # Print last few lines of log file for immediate feedback
393
+ try:
394
+ with open(log_file, "r") as f:
395
+ lines = f.readlines()
396
+ if lines:
397
+ print(" Last few lines from build log:")
398
+ for line in lines[-10:]: # Show last 10 lines
399
+ print(f" {line.rstrip()}")
400
+ except:
401
+ pass
402
+ return None
403
+
404
+ except subprocess.TimeoutExpired:
405
+ print(" ERROR: trtexec timed out after 5 minutes")
406
+ print(f" Partial build log saved to: {log_file}")
407
+ return None
408
+ except Exception as e:
409
+ print(f" ERROR: Failed to run trtexec: {e}")
410
+ return None
411
+
412
+
413
+ def torch_type(trt_type):
414
+ mapping = {
415
+ trt.float32: torch.float32, # Added missing FLOAT mapping
416
+ trt.float16: torch.float16,
417
+ trt.bfloat16: torch.bfloat16,
418
+ trt.int8: torch.int8,
419
+ trt.int32: torch.int32,
420
+ trt.bool: torch.bool,
421
+ trt.uint8: torch.uint8,
422
+ trt.int64: torch.int64,
423
+ }
424
+ if trt_type in mapping:
425
+ return mapping[trt_type]
426
+
427
+ raise TypeError(
428
+ f"Could not resolve TensorRT datatype to an equivalent torch datatype. {trt_type}"
429
+ )
430
+
431
+
432
+ class Engine(object):
433
+ def __init__(self, file, plugins=[]):
434
+ super().__init__()
435
+
436
+ self.logger = trt.Logger(trt.Logger.ERROR)
437
+ trt.init_libnvinfer_plugins(self.logger, "")
438
+
439
+ self.plugins = [ctypes.CDLL(plugin, ctypes.RTLD_GLOBAL) for plugin in plugins]
440
+ self.file = file
441
+ self.load(file)
442
+
443
+ def destroy(self):
444
+ del self.execution_context
445
+ del self.handle
446
+
447
+ atexit.register(destroy, self)
448
+ self.print()
449
+
450
+ def print(self):
451
+
452
+ print("============= TRT Engine Detail =============")
453
+ print(f"Engine file: {self.file}")
454
+ print(f"Inputs: {len(self.in_meta)}")
455
+ for ib, item in enumerate(self.in_meta):
456
+ tensor_name, shape, dtype = item[:3]
457
+ print(f" {ib}. {tensor_name}: {'x'.join(map(str, shape))} [{dtype}]")
458
+
459
+ print(f"Outputs: {len(self.out_meta)}")
460
+ for ib, item in enumerate(self.out_meta):
461
+ tensor_name, shape, dtype = item[:3]
462
+ print(f" {ib}. {tensor_name}: {'x'.join(map(str, shape))} [{dtype}]")
463
+ print("=============================================")
464
+
465
+ def load(self, file):
466
+ runtime = trt.Runtime(self.logger)
467
+
468
+ with open(file, "rb") as f:
469
+ self.handle = runtime.deserialize_cuda_engine(f.read())
470
+ assert (
471
+ self.handle is not None
472
+ ), f"Failed to deserialize the cuda engine from file: {file}"
473
+
474
+ self.execution_context = self.handle.create_execution_context()
475
+ self.meta, self.in_meta, self.out_meta = [], [], []
476
+ for tensor_name in self.handle:
477
+ shape = self.handle.get_tensor_shape(tensor_name)
478
+ print(f"Tensor name: {tensor_name}, shape: {shape}")
479
+ dtype = torch_type(self.handle.get_tensor_dtype(tensor_name))
480
+ if self.handle.get_tensor_mode(tensor_name) == trt.TensorIOMode.INPUT:
481
+ self.in_meta.append([tensor_name, shape, dtype])
482
+ else:
483
+ self.out_meta.append([tensor_name, shape, dtype])
484
+
485
+ def __call__(self, *args, **inputs):
486
+ return self.forward(*args, **inputs)
487
+
488
+ def set_runtime_tensor_shape(self, name, shape):
489
+ self.execution_context.set_input_shape(name, shape)
490
+
491
+ def forward(self, *args, **kwargs):
492
+ return_list = kwargs.pop("return_list", False)
493
+ reference_tensors = []
494
+ stream = torch.cuda.current_stream()
495
+ for iarg, x in enumerate(args):
496
+ name, shape, dtype = self.in_meta[iarg]
497
+ runtime_shape = self.execution_context.get_tensor_shape(name)
498
+ assert isinstance(x, torch.Tensor), f"Unsupported tensor type: {type(x)}"
499
+ assert runtime_shape == x.shape, f"Invalid input shape: {runtime_shape} != {x.shape}"
500
+ assert (
501
+ dtype == x.dtype
502
+ ), f"Invalid tensor dtype, excepted dtype is {dtype}, but got {x.dtype}"
503
+ assert x.is_cuda, f"Invalid tensor device, excepted device is cuda, but got {x.device}"
504
+ x = x.cuda().contiguous()
505
+ self.execution_context.set_tensor_address(name, x.data_ptr())
506
+ reference_tensors.append(x)
507
+
508
+ for name, shape, dtype in self.in_meta:
509
+ if name not in kwargs:
510
+ continue
511
+
512
+ runtime_shape = self.execution_context.get_tensor_shape(name)
513
+ x = kwargs[name]
514
+ assert isinstance(x, torch.Tensor), f"Unsupported tensor[{name}] type: {type(x)}"
515
+ assert (
516
+ runtime_shape == x.shape
517
+ ), f"Invalid input[{name}] shape: {x.shape}, but the expected shape is: {runtime_shape}"
518
+ assert (
519
+ dtype == x.dtype
520
+ ), f"Invalid tensor[{name}] dtype, expected dtype is {dtype}, but got {x.dtype}"
521
+ assert (
522
+ x.is_cuda
523
+ ), f"Invalid tensor[{name}] device, expected device is cuda, but got {x.device}"
524
+ x = x.cuda().contiguous()
525
+ self.execution_context.set_tensor_address(name, x.data_ptr())
526
+ reference_tensors.append(x)
527
+
528
+ for item in self.out_meta:
529
+ name = item[0]
530
+ runtime_shape = self.execution_context.get_tensor_shape(name)
531
+ output_tensor = torch.zeros(
532
+ *runtime_shape, dtype=item[2], device=reference_tensors[0].device
533
+ )
534
+ self.execution_context.set_tensor_address(name, output_tensor.data_ptr())
535
+ reference_tensors.append(output_tensor)
536
+
537
+ self.execution_context.execute_async_v3(stream.cuda_stream)
538
+ stream.synchronize()
539
+ assert len(reference_tensors) == len(self.in_meta) + len(
540
+ self.out_meta
541
+ ), f"Invalid input tensors. The expected I/O tensors are {len(self.in_meta) + len(self.out_meta)}, but got {len(reference_tensors)}"
542
+
543
+ if return_list:
544
+ return [
545
+ reference_tensors[len(self.in_meta) + i] for i, item in enumerate(self.out_meta)
546
+ ]
547
+ else:
548
+ return {
549
+ item[0]: reference_tensors[len(self.in_meta) + i]
550
+ for i, item in enumerate(self.out_meta)
551
+ }
552
+
553
+
554
+ class WanTrtModel5B(torch.nn.Module):
555
+ def __init__(self, eng_path: str):
556
+ super().__init__()
557
+ self.engine = Engine(eng_path)
558
+
559
+ def forward(
560
+ self,
561
+ x: torch.Tensor,
562
+ action: torch.Tensor,
563
+ timestep: torch.Tensor,
564
+ context: torch.Tensor,
565
+ state: torch.Tensor,
566
+ embodiment_id: torch.Tensor,
567
+ ):
568
+
569
+ self.engine.set_runtime_tensor_shape("x", x.shape)
570
+ self.engine.set_runtime_tensor_shape("action", action.shape)
571
+ self.engine.set_runtime_tensor_shape("context", context.shape)
572
+ self.engine.set_runtime_tensor_shape("state", state.shape)
573
+
574
+ output = self.engine(
575
+ x=x.to(torch.float16),
576
+ action=action.to(torch.float16),
577
+ timestep=timestep.to(torch.float16),
578
+ context=context.to(torch.float16),
579
+ state=state.to(torch.float16),
580
+ embodiment_id=embodiment_id.to(torch.int32),
581
+ )
582
+ if "out.0" in output: # for nvfp4 model export through modelopt
583
+ return output["out.0"].to(torch.bfloat16).contiguous(), output["out.1"].to(torch.bfloat16).contiguous()
584
+ else:
585
+ return output["video_noise_pred"].to(torch.bfloat16).contiguous(), output["action_noise_pred"].to(torch.bfloat16).contiguous()
586
+
587
+
588
+ class WanTrtModel14B(torch.nn.Module):
589
+ def __init__(self, eng_path: str):
590
+ super().__init__()
591
+ self.engine = Engine(eng_path)
592
+
593
+ def forward(
594
+ self,
595
+ x: torch.Tensor,
596
+ action: torch.Tensor,
597
+ timestep: torch.Tensor,
598
+ context: torch.Tensor,
599
+ state: torch.Tensor,
600
+ embodiment_id: torch.Tensor,
601
+ clip_feature: torch.Tensor,
602
+ y: torch.Tensor,
603
+ ):
604
+
605
+ self.engine.set_runtime_tensor_shape("x", x.shape)
606
+ self.engine.set_runtime_tensor_shape("action", action.shape)
607
+ self.engine.set_runtime_tensor_shape("context", context.shape)
608
+ self.engine.set_runtime_tensor_shape("state", state.shape)
609
+ self.engine.set_runtime_tensor_shape("clip_feature", clip_feature.shape)
610
+ self.engine.set_runtime_tensor_shape("y", y.shape)
611
+
612
+ output = self.engine(
613
+ x=x.to(torch.float16),
614
+ action=action.to(torch.float16),
615
+ timestep=timestep.to(torch.float16),
616
+ context=context.to(torch.float16),
617
+ state=state.to(torch.float16),
618
+ embodiment_id=embodiment_id.to(torch.int32),
619
+ clip_feature=clip_feature.to(torch.float16),
620
+ y=y.to(torch.float16),
621
+ )
622
+ if "out.0" in output: # for nvfp4 model export through modelopt
623
+ return output["out.0"].to(torch.bfloat16).contiguous(), output["out.1"].to(torch.bfloat16).contiguous()
624
+ else:
625
+ return output["video_noise_pred"].to(torch.bfloat16).contiguous(), output["action_noise_pred"].to(torch.bfloat16).contiguous()
626
+
627
+
628
+ class WanTrtModelAr5B(torch.nn.Module):
629
+ """TRT wrapper for ar_5B_n6 model type - uses kv_cache but no clip_feature."""
630
+ def __init__(self, eng_path: str):
631
+ super().__init__()
632
+ self.engine = Engine(eng_path)
633
+
634
+ def forward(
635
+ self,
636
+ x,
637
+ timestep,
638
+ context,
639
+ kv_cache: list[torch.Tensor],
640
+ y=None,
641
+ action=None,
642
+ timestep_action=None,
643
+ state=None,
644
+ ):
645
+
646
+ kv_cache_packed = torch.stack(kv_cache, dim=0)
647
+
648
+ self.engine.set_runtime_tensor_shape("x", x.shape)
649
+ self.engine.set_runtime_tensor_shape("timestep", timestep.shape)
650
+ self.engine.set_runtime_tensor_shape("context", context.shape)
651
+ self.engine.set_runtime_tensor_shape("kv_cache_packed", kv_cache_packed.shape)
652
+ # self.engine.set_runtime_tensor_shape("y", y.shape)
653
+ self.engine.set_runtime_tensor_shape("action", action.shape)
654
+ self.engine.set_runtime_tensor_shape("timestep_action", timestep_action.shape)
655
+ self.engine.set_runtime_tensor_shape("state", state.shape)
656
+
657
+
658
+ output = self.engine(
659
+ x.to(torch.float16),
660
+ timestep.to(torch.float16),
661
+ context.to(torch.float16),
662
+ kv_cache_packed.to(torch.float16),
663
+ # y.to(torch.float16),
664
+ action.to(torch.float16),
665
+ timestep_action.to(torch.float16),
666
+ state.to(torch.float16),
667
+ )
668
+
669
+ if "out.0" in output: # for nvfp4 model export through modelopt
670
+ return output["out.0"].to(torch.bfloat16).contiguous(), output["out.1"].to(torch.bfloat16).contiguous()
671
+ else:
672
+ return output["video_noise_pred"].to(torch.bfloat16).contiguous(), output["action_noise_pred"].to(torch.bfloat16).contiguous()
673
+
674
+
675
+ class WanTrtModelAr14B(torch.nn.Module):
676
+ def __init__(self, eng_path: str):
677
+ super().__init__()
678
+ self.engine = Engine(eng_path)
679
+
680
+ def forward(
681
+ self,
682
+ x,
683
+ timestep,
684
+ context,
685
+ kv_cache: list[torch.Tensor],
686
+ y=None,
687
+ clip_feature=None,
688
+ action=None,
689
+ timestep_action=None,
690
+ state=None,
691
+ ):
692
+
693
+ kv_cache_packed = torch.stack(kv_cache, dim=0)
694
+
695
+ self.engine.set_runtime_tensor_shape("x", x.shape)
696
+ self.engine.set_runtime_tensor_shape("timestep", timestep.shape)
697
+ self.engine.set_runtime_tensor_shape("context", context.shape)
698
+ self.engine.set_runtime_tensor_shape("kv_cache_packed", kv_cache_packed.shape)
699
+ self.engine.set_runtime_tensor_shape("y", y.shape)
700
+ self.engine.set_runtime_tensor_shape("clip_feature", clip_feature.shape)
701
+ self.engine.set_runtime_tensor_shape("action", action.shape)
702
+ self.engine.set_runtime_tensor_shape("timestep_action", timestep_action.shape)
703
+ self.engine.set_runtime_tensor_shape("state", state.shape)
704
+
705
+
706
+ output = self.engine(
707
+ x.to(torch.float16),
708
+ timestep.to(torch.float16),
709
+ context.to(torch.float16),
710
+ kv_cache_packed.to(torch.float16),
711
+ y.to(torch.float16),
712
+ clip_feature.to(torch.float16),
713
+ action.to(torch.float16),
714
+ timestep_action.to(torch.float16),
715
+ state.to(torch.float16),
716
+ )
717
+
718
+ if "out.0" in output: # for nvfp4 model export through modelopt
719
+ return output["out.0"].to(torch.bfloat16).contiguous(), output["out.1"].to(torch.bfloat16).contiguous()
720
+ else:
721
+ return output["video_noise_pred"].to(torch.bfloat16).contiguous(), output["action_noise_pred"].to(torch.bfloat16).contiguous()
722
+
723
+ def load_tensorrt_engine(engine_path="tensorrt/wan_model.trt", model_type="5B"):
724
+ """Load TensorRT engine"""
725
+ if model_type == "5B":
726
+ trt_inference = WanTrtModel5B(engine_path)
727
+ elif model_type == "ar_5B_n6" or model_type == "ar_5B":
728
+ trt_inference = WanTrtModelAr5B(engine_path)
729
+ elif model_type == "14B":
730
+ trt_inference = WanTrtModel14B(engine_path)
731
+ elif model_type == "ar_14B" or model_type == "ar_14B_droid":
732
+ trt_inference = WanTrtModelAr14B(engine_path)
733
+ else:
734
+ raise ValueError(f"Model type {model_type} not supported")
735
+ return trt_inference
736
+
737
+
738
+ def create_wan_test_inputs(policy, device="cuda", model_type="5B"):
739
+ # Get dtype from model parameters
740
+ dtype = torch.float16
741
+
742
+ # Use hardcoded dimensions from the original working version of the script
743
+ if model_type == "5B":
744
+ x = torch.randn(1, 48, 13, 22, 40, dtype=dtype, device=device)
745
+ action = torch.randn(1, 48, 32, dtype=dtype, device=device)
746
+ timestep = torch.randn(1, dtype=dtype, device=device)
747
+ context = torch.randn(1, 512, 4096, dtype=dtype, device=device)
748
+ state = torch.randn(1, 1, 64, dtype=dtype, device=device)
749
+ embodiment_id = torch.zeros(1, dtype=torch.int32, device=device)
750
+ timestep_action = torch.randn(1, 48, dtype=dtype, device=device)
751
+ seq_len = torch.tensor(440, dtype=torch.int32, device=device)
752
+ return x, action, timestep, context, state, embodiment_id, timestep_action, seq_len
753
+ elif model_type == "ar_5B_n6":
754
+ # ar_5B_n6 uses _forward_inference_trt which requires kv_cache_packed
755
+ # Shape from dynamic_axes: kv_cache_packed:30x2x1x220x24x128
756
+ # Note: 5B model doesn't use clip_feature (unlike 14B), but still needs y
757
+ x = torch.randn(1, 48, 2, 22, 40, dtype=dtype, device=device)
758
+ timestep = torch.randn(1, 2, dtype=dtype, device=device)
759
+ context = torch.randn(1, 512, 4096, dtype=dtype, device=device)
760
+ # y = torch.randn(1, 52, 2, 22, 40, dtype=dtype, device=device) # y is required by _forward_inference_trt
761
+ action = torch.randn(1, 48, 32, dtype=dtype, device=device)
762
+ timestep_action = torch.randn(1, 48, dtype=dtype, device=device)
763
+ state = torch.randn(1, 1, 64, dtype=dtype, device=device)
764
+
765
+ num_heads = 24
766
+ head_dim = 128
767
+ num_layers = 30
768
+ B = 1
769
+
770
+ kv_cache = []
771
+ for _ in range(num_layers):
772
+ kv_cache.append(
773
+ torch.zeros([2, B, 13*220, num_heads, head_dim], dtype=dtype, device=device)
774
+ )
775
+
776
+ kv_cache_packed = torch.stack(kv_cache, dim=0)
777
+ # Return order matches _forward_inference_trt signature: x, timestep, context, kv_cache_packed, y, action, timestep_action, state
778
+ return (x, timestep, context, kv_cache_packed, action, timestep_action, state)
779
+ elif model_type == "14B":
780
+ x = torch.randn(1, 16, 13, 44, 80, dtype=dtype, device=device)
781
+ action = torch.randn(1, 48, 32, dtype=dtype, device=device)
782
+ timestep = torch.randn(1, dtype=dtype, device=device)
783
+ context = torch.randn(1, 512, 4096, dtype=dtype, device=device)
784
+ state = torch.randn(1, 1, 64, dtype=dtype, device=device)
785
+ embodiment_id = torch.zeros(1, dtype=torch.int32, device=device)
786
+ clip_feature = torch.randn(1, 257, 1280, dtype=dtype, device=device)
787
+ y = torch.randn(1, 20, 13, 44, 80, dtype=dtype, device=device)
788
+ return x, action, timestep, context, state, embodiment_id, clip_feature, y
789
+ elif model_type == "ar_14B":
790
+ clip_feature = torch.randn(1, 257, 1280, dtype=dtype, device=device)
791
+ y = torch.randn(1, 20, 2, 44, 80, dtype=dtype, device=device)
792
+ timestep_action = torch.randn(1, 48, dtype=dtype, device=device)
793
+ x = torch.randn(1, 16, 2, 44, 80, dtype=dtype, device=device)
794
+ timestep = torch.randn(1, 2, dtype=dtype, device=device)
795
+ context = torch.randn(1, 512, 4096, dtype=dtype, device=device)
796
+ seq_len = torch.tensor(1760, dtype=torch.int32, device=device)
797
+ action = torch.randn(1, 48, 32, dtype=dtype, device=device)
798
+ state = torch.randn(1, 1, 64, dtype=dtype, device=device)
799
+ embodiment_id = torch.zeros(1, dtype=torch.int32, device=device)
800
+
801
+ num_heads = 40
802
+ head_dim = 5120 // num_heads
803
+ num_layers = 40
804
+ B = 1
805
+
806
+ kv_cache = []
807
+ for _ in range(num_layers):
808
+ kv_cache.append(
809
+ torch.zeros([2, B, 9*880, num_heads, head_dim], dtype=dtype, device=device)
810
+ )
811
+
812
+ crossattn_k_cache = []
813
+ for _ in range(num_layers):
814
+ crossattn_k_cache.append(
815
+ torch.zeros([2, B, 9*880, num_heads, head_dim], dtype=dtype, device=device)
816
+ )
817
+ kv_cache_packed = torch.stack(kv_cache, dim=0)
818
+ crossattn_packed = torch.stack(crossattn_k_cache, dim=0)
819
+ return (x, timestep, context, kv_cache_packed, y, clip_feature, action, timestep_action, state)
820
+ elif model_type == "ar_14B_droid":
821
+ clip_feature = torch.randn(1, 257, 1280, dtype=dtype, device=device)
822
+ y = torch.randn(1, 20, 2, 44, 80, dtype=dtype, device=device)
823
+ timestep_action = torch.randn(1, 24, dtype=dtype, device=device)
824
+ x = torch.randn(1, 16, 2, 44, 80, dtype=dtype, device=device)
825
+ timestep = torch.randn(1, 2, dtype=dtype, device=device)
826
+ context = torch.randn(1, 512, 4096, dtype=dtype, device=device)
827
+ seq_len = torch.tensor(1760, dtype=torch.int32, device=device)
828
+ action = torch.randn(1, 24, 32, dtype=dtype, device=device)
829
+ state = torch.randn(1, 1, 64, dtype=dtype, device=device)
830
+ embodiment_id = torch.zeros(1, dtype=torch.int32, device=device)
831
+
832
+ num_heads = 40
833
+ head_dim = 5120 // num_heads
834
+ num_layers = 40
835
+ B = 1
836
+
837
+ kv_cache = []
838
+ for _ in range(num_layers):
839
+ kv_cache.append(
840
+ torch.zeros([2, B, 9*880, num_heads, head_dim], dtype=dtype, device=device)
841
+ )
842
+
843
+ crossattn_k_cache = []
844
+ for _ in range(num_layers):
845
+ crossattn_k_cache.append(
846
+ torch.zeros([2, B, 9*880, num_heads, head_dim], dtype=dtype, device=device)
847
+ )
848
+ kv_cache_packed = torch.stack(kv_cache, dim=0)
849
+ crossattn_packed = torch.stack(crossattn_k_cache, dim=0)
850
+ return (x, timestep, context, kv_cache_packed, y, clip_feature, action, timestep_action, state)
851
+
852
+
groot/vla/__init__.py ADDED
File without changes
groot/vla/common/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+
groot/vla/common/utils/__init__.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from .data_structure import * # noqa: F403
2
+ from .io import * # noqa: F403
3
+ from .misc import * # noqa: F403
groot/vla/common/utils/data_structure/__init__.py ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ from .shape_utils import * # noqa: F403
2
+ from .tree_utils import * # noqa: F403
groot/vla/common/utils/data_structure/shape_utils.py ADDED
@@ -0,0 +1,283 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Shape inference methods
3
+ """
4
+
5
+ from functools import partial
6
+ import math
7
+ from typing import List, Tuple, Union
8
+ import warnings
9
+
10
+ import numpy as np
11
+ import torch
12
+
13
+ # fmt: off
14
+ __all__ = [
15
+ "shape_convnd",
16
+ "shape_conv1d", "shape_conv2d", "shape_conv3d",
17
+ "shape_transpose_convnd",
18
+ "shape_transpose_conv1d", "shape_transpose_conv2d", "shape_transpose_conv3d",
19
+ "shape_poolnd",
20
+ "shape_maxpool1d", "shape_maxpool2d", "shape_maxpool3d",
21
+ "shape_avgpool1d", "shape_avgpool2d", "shape_avgpool3d",
22
+ "shape_slice",
23
+ "check_shape"
24
+ ]
25
+ # fmt: on
26
+
27
+
28
+ def _get_shape(x):
29
+ "single object"
30
+ if isinstance(x, np.ndarray):
31
+ return tuple(x.shape)
32
+ else:
33
+ return tuple(x.size())
34
+
35
+
36
+ def _expands(dim, *xs):
37
+ "repeat vars like kernel and stride to match dim"
38
+
39
+ def _expand(x):
40
+ if isinstance(x, int):
41
+ return (x,) * dim
42
+ else:
43
+ assert len(x) == dim
44
+ return x
45
+
46
+ return map(lambda x: _expand(x), xs)
47
+
48
+
49
+ _HELPER_TENSOR = torch.zeros((1,))
50
+
51
+
52
+ def shape_slice(input_shape, slice):
53
+ """
54
+ Credit to Adam Paszke for the trick. Shape inference without instantiating
55
+ an actual tensor.
56
+ The key is that `.expand()` does not actually allocate memory
57
+ Still needs to allocate a one-element HELPER_TENSOR.
58
+ """
59
+ shape = _HELPER_TENSOR.expand(*input_shape)[slice]
60
+ if hasattr(shape, "size"):
61
+ return tuple(shape.size())
62
+ return (1,)
63
+
64
+
65
+ class ShapeSlice:
66
+ """
67
+ shape_slice inference with easy []-operator
68
+ """
69
+
70
+ def __init__(self, input_shape):
71
+ self.input_shape = input_shape
72
+
73
+ def __getitem__(self, slice):
74
+ return shape_slice(self.input_shape, slice)
75
+
76
+
77
+ def check_shape(
78
+ value: Union[Tuple, List, torch.Tensor, np.ndarray],
79
+ expected: Union[Tuple, List, torch.Tensor, np.ndarray],
80
+ err_msg="",
81
+ mode="raise",
82
+ ):
83
+ """
84
+ Args:
85
+ value: np array or torch Tensor
86
+ expected:
87
+ - list[int], tuple[int]: if any value is None, will match any dim
88
+ - np array or torch Tensor: must have the same dimensions
89
+ mode:
90
+ - "raise": raise ValueError, shape mismatch
91
+ - "return": returns True if shape matches, otherwise False
92
+ - "warning": warnings.warn
93
+ """
94
+ assert mode in ["raise", "return", "warning"]
95
+ if torch.is_tensor(value):
96
+ actual_shape = value.size()
97
+ elif hasattr(value, "shape"):
98
+ actual_shape = value.shape
99
+ else:
100
+ assert isinstance(value, (list, tuple))
101
+ actual_shape = value
102
+ assert all(
103
+ isinstance(s, int) for s in actual_shape
104
+ ), f"actual shape: {actual_shape} is not a list of ints"
105
+
106
+ if torch.is_tensor(expected):
107
+ expected_shape = expected.size()
108
+ elif hasattr(expected, "shape"):
109
+ expected_shape = expected.shape
110
+ else:
111
+ assert isinstance(expected, (list, tuple))
112
+ expected_shape = expected
113
+
114
+ err_msg = f" for {err_msg}" if err_msg else ""
115
+
116
+ if len(actual_shape) != len(expected_shape):
117
+ err_msg = (
118
+ f"Dimension mismatch{err_msg}: actual shape {actual_shape} "
119
+ f"!= expected shape {expected_shape}."
120
+ )
121
+ if mode == "raise":
122
+ raise ValueError(err_msg)
123
+ elif mode == "warning":
124
+ warnings.warn(err_msg)
125
+ return False
126
+
127
+ for s_a, s_e in zip(actual_shape, expected_shape):
128
+ if s_e is not None and s_a != s_e:
129
+ err_msg = (
130
+ f"Shape mismatch{err_msg}: actual shape {actual_shape} "
131
+ f"!= expected shape {expected_shape}."
132
+ )
133
+ if mode == "raise":
134
+ raise ValueError(err_msg)
135
+ elif mode == "warning":
136
+ warnings.warn(err_msg)
137
+ return False
138
+ return True
139
+
140
+
141
+ def shape_convnd(
142
+ dim,
143
+ input_shape,
144
+ out_channels,
145
+ kernel_size,
146
+ stride=1,
147
+ padding=0,
148
+ dilation=1,
149
+ has_batch=False,
150
+ ):
151
+ """
152
+ http://pytorch.org/docs/nn.html#conv1d
153
+ http://pytorch.org/docs/nn.html#conv2d
154
+ http://pytorch.org/docs/nn.html#conv3d
155
+
156
+ Args:
157
+ dim: supports 1D to 3D
158
+ input_shape:
159
+ - 1D: [channel, length]
160
+ - 2D: [channel, height, width]
161
+ - 3D: [channel, depth, height, width]
162
+ has_batch: whether the first dim is batch size or not
163
+ """
164
+ if has_batch:
165
+ assert (
166
+ len(input_shape) == dim + 2
167
+ ), "input shape with batch should be {}-dimensional".format(dim + 2)
168
+ else:
169
+ assert (
170
+ len(input_shape) == dim + 1
171
+ ), "input shape without batch should be {}-dimensional".format(dim + 1)
172
+ if stride is None:
173
+ # for pooling convention in PyTorch
174
+ stride = kernel_size
175
+ kernel_size, stride, padding, dilation = _expands(dim, kernel_size, stride, padding, dilation)
176
+ if has_batch:
177
+ batch = input_shape[0]
178
+ input_shape = input_shape[1:]
179
+ else:
180
+ batch = None
181
+ _, *img = input_shape
182
+ new_img_shape = [
183
+ math.floor(
184
+ (img[i] + 2 * padding[i] - dilation[i] * (kernel_size[i] - 1) - 1) // stride[i] + 1
185
+ )
186
+ for i in range(dim)
187
+ ]
188
+ return ((batch,) if has_batch else ()) + (out_channels, *new_img_shape)
189
+
190
+
191
+ def shape_poolnd(
192
+ dim, input_shape, kernel_size, stride=None, padding=0, dilation=1, has_batch=False
193
+ ):
194
+ """
195
+ The only difference from infer_shape_convnd is that `stride` default is None
196
+ """
197
+ if has_batch:
198
+ out_channels = input_shape[1]
199
+ else:
200
+ out_channels = input_shape[0]
201
+ return shape_convnd(
202
+ dim,
203
+ input_shape,
204
+ out_channels,
205
+ kernel_size,
206
+ stride,
207
+ padding,
208
+ dilation,
209
+ has_batch,
210
+ )
211
+
212
+
213
+ def shape_transpose_convnd(
214
+ dim,
215
+ input_shape,
216
+ out_channels,
217
+ kernel_size,
218
+ stride=1,
219
+ padding=0,
220
+ output_padding=0,
221
+ dilation=1,
222
+ has_batch=False,
223
+ ):
224
+ """
225
+ http://pytorch.org/docs/nn.html#convtranspose1d
226
+ http://pytorch.org/docs/nn.html#convtranspose2d
227
+ http://pytorch.org/docs/nn.html#convtranspose3d
228
+
229
+ Args:
230
+ dim: supports 1D to 3D
231
+ input_shape:
232
+ - 1D: [channel, length]
233
+ - 2D: [channel, height, width]
234
+ - 3D: [channel, depth, height, width]
235
+ has_batch: whether the first dim is batch size or not
236
+ """
237
+ if has_batch:
238
+ assert (
239
+ len(input_shape) == dim + 2
240
+ ), "input shape with batch should be {}-dimensional".format(dim + 2)
241
+ else:
242
+ assert (
243
+ len(input_shape) == dim + 1
244
+ ), "input shape without batch should be {}-dimensional".format(dim + 1)
245
+ kernel_size, stride, padding, output_padding, dilation = _expands(
246
+ dim, kernel_size, stride, padding, output_padding, dilation
247
+ )
248
+ if has_batch:
249
+ batch = input_shape[0]
250
+ input_shape = input_shape[1:]
251
+ else:
252
+ batch = None
253
+ _, *img = input_shape
254
+ new_img_shape = [
255
+ (img[i] - 1) * stride[i] - 2 * padding[i] + kernel_size[i] + output_padding[i]
256
+ for i in range(dim)
257
+ ]
258
+ return ((batch,) if has_batch else ()) + (out_channels, *new_img_shape)
259
+
260
+
261
+ shape_conv1d = partial(shape_convnd, 1)
262
+ shape_conv2d = partial(shape_convnd, 2)
263
+ shape_conv3d = partial(shape_convnd, 3)
264
+
265
+
266
+ shape_transpose_conv1d = partial(shape_transpose_convnd, 1)
267
+ shape_transpose_conv2d = partial(shape_transpose_convnd, 2)
268
+ shape_transpose_conv3d = partial(shape_transpose_convnd, 3)
269
+
270
+
271
+ shape_maxpool1d = partial(shape_poolnd, 1)
272
+ shape_maxpool2d = partial(shape_poolnd, 2)
273
+ shape_maxpool3d = partial(shape_poolnd, 3)
274
+
275
+
276
+ """
277
+ http://pytorch.org/docs/nn.html#avgpool1d
278
+ http://pytorch.org/docs/nn.html#avgpool2d
279
+ http://pytorch.org/docs/nn.html#avgpool3d
280
+ """
281
+ shape_avgpool1d = partial(shape_maxpool1d, dilation=1)
282
+ shape_avgpool2d = partial(shape_maxpool2d, dilation=1)
283
+ shape_avgpool3d = partial(shape_maxpool3d, dilation=1)
groot/vla/common/utils/data_structure/tree_utils.py ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Utils to handle nested data structures
3
+
4
+ Install dm_tree first:
5
+ https://tree.readthedocs.io/en/latest/api.html
6
+ """
7
+
8
+ import collections
9
+ from typing import Any, Iterable, List, Tuple, TypeVar
10
+
11
+ import numpy as np
12
+
13
+ try:
14
+ import tree
15
+
16
+ except ImportError:
17
+ raise ImportError("Please install dm_tree first: `pip install dm_tree`")
18
+
19
+
20
+ def is_sequence(obj):
21
+ """
22
+ Returns:
23
+ True if the sequence is a collections.Sequence and not a string.
24
+ """
25
+ return isinstance(obj, collections.abc.Sequence) and not isinstance(obj, str)
26
+
27
+
28
+ def is_mapping(obj):
29
+ """
30
+ Returns:
31
+ True if the sequence is a collections.Mapping
32
+ """
33
+ return isinstance(obj, collections.abc.Mapping)
34
+
35
+
36
+ def tree_value_at_path(obj, paths: Tuple):
37
+ try:
38
+ for p in paths:
39
+ obj = obj[p]
40
+ return obj
41
+ except Exception as e:
42
+ raise ValueError(f"{e}\n\n-- Incorrect nested path {paths} for object: {obj}.")
43
+
44
+
45
+ def tree_assign_at_path(obj, paths: Tuple, value):
46
+ try:
47
+ for p in paths[:-1]:
48
+ obj = obj[p]
49
+ if len(paths) > 0:
50
+ obj[paths[-1]] = value
51
+ except Exception as e:
52
+ raise ValueError(f"{e}\n\n-- Incorrect nested path {paths} for object: {obj}.")
53
+
54
+
55
+ def copy_non_leaf(obj):
56
+ """
57
+ Deepcopy the nested structure, but does NOT copy the leaf values like Tensors
58
+ """
59
+ return tree.map_structure(lambda x: x, obj)
60
+
61
+
62
+ # =======================================================================
63
+ # Copyright 2018 DeepMind Technologies Limited. All rights reserved.
64
+ #
65
+ # Licensed under the Apache License, Version 2.0 (the "License");
66
+ # you may not use this file except in compliance with the License.
67
+ # You may obtain a copy of the License at
68
+ #
69
+ # http://www.apache.org/licenses/LICENSE-2.0
70
+ #
71
+ # Unless required by applicable law or agreed to in writing, software
72
+ # distributed under the License is distributed on an "AS IS" BASIS,
73
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
74
+ # See the License for the specific language governing permissions and
75
+ # limitations under the License.
76
+
77
+ # Tensor framework-agnostic utilities for manipulating nested structures.
78
+
79
+ ElementType = TypeVar("ElementType")
80
+
81
+
82
+ def fast_map_structure(func, *structure):
83
+ """Faster map_structure implementation which skips some error checking."""
84
+ flat_structure = (tree.flatten(s) for s in structure)
85
+ entries = zip(*flat_structure)
86
+ # Arbitrarily choose one of the structures of the original sequence (the last)
87
+ # to match the structure for the flattened sequence.
88
+ return tree.unflatten_as(structure[-1], [func(*x) for x in entries])
89
+
90
+
91
+ def stack_sequence_fields(sequence: Iterable[ElementType]) -> ElementType:
92
+ """Stacks a list of identically nested objects.
93
+
94
+ This takes a sequence of identically nested objects and returns a single
95
+ nested object whose ith leaf is a stacked numpy array of the corresponding
96
+ ith leaf from each element of the sequence.
97
+
98
+ For example, if `sequence` is:
99
+
100
+ ```python
101
+ [{
102
+ 'action': np.array([1.0]),
103
+ 'observation': (np.array([0.0, 1.0, 2.0]),),
104
+ 'reward': 1.0
105
+ }, {
106
+ 'action': np.array([0.5]),
107
+ 'observation': (np.array([1.0, 2.0, 3.0]),),
108
+ 'reward': 0.0
109
+ }, {
110
+ 'action': np.array([0.3]),1
111
+ 'observation': (np.array([2.0, 3.0, 4.0]),),
112
+ 'reward': 0.5
113
+ }]
114
+ ```
115
+
116
+ Then this function will return:
117
+
118
+ ```python
119
+ {
120
+ 'action': np.array([....]) # array shape = [3 x 1]
121
+ 'observation': (np.array([...]),) # array shape = [3 x 3]
122
+ 'reward': np.array([...]) # array shape = [3]
123
+ }
124
+ ```
125
+
126
+ Note that the 'observation' entry in the above example has two levels of
127
+ nesting, i.e it is a tuple of arrays.
128
+
129
+ Args:
130
+ sequence: a list of identically nested objects.
131
+
132
+ Returns:
133
+ A nested object with numpy.
134
+
135
+ Raises:
136
+ ValueError: If `sequence` is an empty sequence.
137
+ """
138
+ # Handle empty input sequences.
139
+ if not sequence:
140
+ raise ValueError("Input sequence must not be empty")
141
+
142
+ # Default to asarray when arrays don't have the same shape to be compatible
143
+ # with old behaviour.
144
+ try:
145
+ return fast_map_structure(lambda *values: np.stack(values), *sequence)
146
+ except ValueError:
147
+ return fast_map_structure(lambda *values: np.asarray(values), *sequence)
148
+
149
+
150
+ def unstack_sequence_fields(struct: ElementType, batch_size: int) -> List[ElementType]:
151
+ """Converts a struct of batched arrays to a list of structs.
152
+
153
+ This is effectively the inverse of `stack_sequence_fields`.
154
+
155
+ Args:
156
+ struct: An (arbitrarily nested) structure of arrays.
157
+ batch_size: The length of the leading dimension of each array in the struct.
158
+ This is assumed to be static and known.
159
+
160
+ Returns:
161
+ A list of structs with the same structure as `struct`, where each leaf node
162
+ is an unbatched element of the original leaf node.
163
+ """
164
+
165
+ return [tree.map_structure(lambda s, i=i: s[i], struct) for i in range(batch_size)]
166
+
167
+
168
+ def broadcast_structures(*args: Any) -> Any:
169
+ """Returns versions of the arguments that give them the same nested structure.
170
+
171
+ Any nested items in *args must have the same structure.
172
+
173
+ Any non-nested item will be replaced with a nested version that shares that
174
+ structure. The leaves will all be references to the same original non-nested
175
+ item.
176
+
177
+ If all *args are nested, or all *args are non-nested, this function will
178
+ return *args unchanged.
179
+
180
+ Example:
181
+ ```
182
+ a = ('a', 'b')
183
+ b = 'c'
184
+ tree_a, tree_b = broadcast_structure(a, b)
185
+ tree_a
186
+ > ('a', 'b')
187
+ tree_b
188
+ > ('c', 'c')
189
+ ```
190
+
191
+ Args:
192
+ *args: A Sequence of nested or non-nested items.
193
+
194
+ Returns:
195
+ `*args`, except with all items sharing the same nest structure.
196
+ """
197
+ if not args:
198
+ return
199
+
200
+ reference_tree = None
201
+ for arg in args:
202
+ if tree.is_nested(arg):
203
+ reference_tree = arg
204
+ break
205
+
206
+ if reference_tree is None:
207
+ reference_tree = args[0]
208
+
209
+ def mirror_structure(value, reference_tree):
210
+ if tree.is_nested(value):
211
+ # Use check_types=True so that the types of the trees we construct aren't
212
+ # dependent on our arbitrary choice of which nested arg to use as the
213
+ # reference_tree.
214
+ tree.assert_same_structure(value, reference_tree, check_types=True)
215
+ return value
216
+ else:
217
+ return tree.map_structure(lambda _: value, reference_tree)
218
+
219
+ return tuple(mirror_structure(arg, reference_tree) for arg in args)
groot/vla/common/utils/io/__init__.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from .config_utils import * # noqa: F403
2
+ from .file_utils import * # noqa: F403
3
+ from .hdf5_utils import * # noqa: F403
4
+ from .json_utils import * # noqa: F403
5
+ from .print_utils import * # noqa: F403
6
+ from .termcolor import * # noqa: F403
groot/vla/common/utils/io/config_utils.py ADDED
@@ -0,0 +1,260 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from copy import deepcopy
2
+ import importlib.resources
3
+ import os
4
+ import sys
5
+
6
+ import hydra
7
+ from omegaconf import DictConfig, OmegaConf
8
+ import tree
9
+
10
+ from ..misc.functional_utils import call_once, is_mapping, is_sequence, meta_decorator
11
+ from .print_utils import to_scientific_str
12
+
13
+ _CLASS_REGISTRY = {} # for instantiation
14
+
15
+
16
+ def resource_file_path(pkg_name, fname) -> str:
17
+ with importlib.resources.path(pkg_name, fname) as p:
18
+ return str(p)
19
+
20
+
21
+ def print_config(cfg: DictConfig):
22
+ print(cfg.pretty(resolve=True))
23
+
24
+
25
+ def is_hydra_initialized():
26
+ return hydra.utils.HydraConfig.initialized()
27
+
28
+
29
+ def hydra_config():
30
+ # https://github.com/facebookresearch/hydra/issues/377
31
+ # HydraConfig() is a singleton
32
+ if is_hydra_initialized():
33
+ return hydra.utils.HydraConfig().cfg.hydra
34
+ else:
35
+ return None
36
+
37
+
38
+ def hydra_override_arg_list() -> list[str]:
39
+ """
40
+ Returns:
41
+ list ["lr=0.2", "batch=64", ...]
42
+ """
43
+ if is_hydra_initialized():
44
+ return hydra_config().overrides.task
45
+ else:
46
+ return []
47
+
48
+
49
+ def hydra_override_name():
50
+ if is_hydra_initialized():
51
+ return hydra_config().job.override_dirname
52
+ else:
53
+ return ""
54
+
55
+
56
+ def hydra_original_dir(*subpaths):
57
+ return os.path.join(hydra.utils.get_original_cwd(), *subpaths)
58
+
59
+
60
+ @call_once(on_second_call="noop")
61
+ def register_omegaconf_resolvers():
62
+ import numpy as np
63
+
64
+ OmegaConf.register_new_resolver("scientific", lambda v, i=0: to_scientific_str(v, i))
65
+ OmegaConf.register_new_resolver("_optional", lambda v: f"_{v}" if v else "")
66
+ OmegaConf.register_new_resolver("optional_", lambda v: f"{v}_" if v else "")
67
+ OmegaConf.register_new_resolver("_optional_", lambda v: f"_{v}_" if v else "")
68
+ OmegaConf.register_new_resolver("__optional", lambda v: f"__{v}" if v else "")
69
+ OmegaConf.register_new_resolver("optional__", lambda v: f"{v}__" if v else "")
70
+ OmegaConf.register_new_resolver("__optional__", lambda v: f"__{v}__" if v else "")
71
+ OmegaConf.register_new_resolver("iftrue", lambda cond, v_default: cond if cond else v_default)
72
+ OmegaConf.register_new_resolver("ifelse", lambda cond, v1, v2="": v1 if cond else v2)
73
+ OmegaConf.register_new_resolver(
74
+ "ifequal", lambda query, key, v1, v2: v1 if query == key else v2
75
+ )
76
+ OmegaConf.register_new_resolver("intbool", lambda cond: 1 if cond else 0)
77
+ OmegaConf.register_new_resolver("mult", lambda *x: np.prod(x).tolist())
78
+ OmegaConf.register_new_resolver("add", lambda *x: sum(x))
79
+ OmegaConf.register_new_resolver("div", lambda x, y: x / y)
80
+ OmegaConf.register_new_resolver("intdiv", lambda x, y: x // y)
81
+
82
+ # try each key until the key exists. Useful for multiple classes that have different
83
+ # names for the same key
84
+ def _try_key(cfg, *keys):
85
+ for k in keys:
86
+ if k in cfg:
87
+ return cfg[k]
88
+ raise KeyError(f"no key in {keys} is valid")
89
+
90
+ OmegaConf.register_new_resolver("trykey", _try_key)
91
+ # replace `resnet.gn.ws` -> `resnet_gn_ws`, because omegaconf doesn't support
92
+ # keys with dots. Useful for generating run name with dots
93
+ OmegaConf.register_new_resolver("underscore_to_dots", lambda s: s.replace("_", "."))
94
+
95
+ def _no_instantiate(cfg):
96
+ cfg = deepcopy(cfg)
97
+ cfg[_NO_INSTANTIATE] = True
98
+ return cfg
99
+
100
+ OmegaConf.register_new_resolver("no_instantiate", _no_instantiate)
101
+
102
+
103
+ # ========================================================
104
+ # ================== Instantiation tools ================
105
+ # ========================================================
106
+
107
+
108
+ def register_callable(name, class_type):
109
+ if isinstance(class_type, str):
110
+ class_type, name = name, class_type
111
+ assert callable(class_type)
112
+ _CLASS_REGISTRY[name] = class_type
113
+
114
+
115
+ @meta_decorator
116
+ def register_class(cls, alias=None):
117
+ """
118
+ Decorator
119
+ """
120
+ assert callable(cls)
121
+ _CLASS_REGISTRY[cls.__name__] = cls
122
+ if alias:
123
+ assert is_sequence(alias)
124
+ for a in alias:
125
+ _CLASS_REGISTRY[str(a)] = cls
126
+ return cls
127
+
128
+
129
+ def omegaconf_to_dict(cfg, resolve: bool = True, enum_to_str: bool = False):
130
+ """
131
+ Convert arbitrary nested omegaconf objects to primitive containers
132
+
133
+ WARNING: cannot use tree lib because it gets confused on DictConfig and ListConfig
134
+ """
135
+ kw = dict(resolve=resolve, enum_to_str=enum_to_str)
136
+ if OmegaConf.is_config(cfg):
137
+ return OmegaConf.to_container(cfg, **kw)
138
+ elif is_sequence(cfg):
139
+ return type(cfg)(omegaconf_to_dict(c, **kw) for c in cfg)
140
+ elif is_mapping(cfg):
141
+ return {k: omegaconf_to_dict(c, **kw) for k, c in cfg.items()}
142
+ else:
143
+ return cfg
144
+
145
+
146
+ def omegaconf_save(cfg, *paths: str, resolve: bool = True):
147
+ """
148
+ Save omegaconf to yaml
149
+ """
150
+ from .file_utils import f_join
151
+
152
+ OmegaConf.save(cfg, f_join(*paths), resolve=resolve)
153
+
154
+
155
+ def get_class(path):
156
+ """
157
+ First try to find the class in the registry first,
158
+ if it doesn't exist, use importlib to locate it
159
+ """
160
+ if path in _CLASS_REGISTRY:
161
+ return _CLASS_REGISTRY[path]
162
+ else:
163
+ assert "." in path, (
164
+ f"Because {path} is not found in class registry, " f"it must be a full module path"
165
+ )
166
+ try:
167
+ from importlib import import_module
168
+
169
+ module_path, _, class_name = path.rpartition(".")
170
+ mod = import_module(module_path)
171
+ try:
172
+ class_type = getattr(mod, class_name)
173
+ except AttributeError:
174
+ raise ImportError("Class {} is not in module {}".format(class_name, module_path))
175
+ return class_type
176
+ except ValueError as e:
177
+ print("Error initializing class " + path, file=sys.stderr)
178
+ raise e
179
+
180
+
181
+ _DELETE_ARG = "__delete__"
182
+ _NO_INSTANTIATE = "__no_instantiate__" # return config as-is
183
+ _OMEGA_MISSING = "???"
184
+
185
+
186
+ def _get_instantiate_params(cfg, kwargs=None):
187
+ params = cfg
188
+ f_args, f_kwargs = (), {}
189
+ for k, value in params.items():
190
+ if k in ["cls", "class"]:
191
+ continue
192
+ elif k == "*args":
193
+ assert is_sequence(value), '"*args" value must be a sequence'
194
+ f_args = list(value)
195
+ continue
196
+ if value == _OMEGA_MISSING:
197
+ if kwargs and k in kwargs:
198
+ value = kwargs[k]
199
+ else:
200
+ raise ValueError(f'Missing required keyword arg "{k}" in cfg: {cfg}')
201
+ if value == _DELETE_ARG:
202
+ continue
203
+ else:
204
+ f_kwargs[k] = value
205
+ return f_args, f_kwargs
206
+
207
+
208
+ def _instantiate_single(cfg):
209
+ if is_mapping(cfg) and ("cls" in cfg or "class" in cfg):
210
+ assert bool("cls" in cfg) != bool("class" in cfg), (
211
+ "to instantiate from config, "
212
+ 'one and only one of "cls" or "class" key should be provided'
213
+ )
214
+ if _NO_INSTANTIATE in cfg:
215
+ no_instantiate = cfg.pop(_NO_INSTANTIATE)
216
+ if no_instantiate:
217
+ cfg = deepcopy(cfg)
218
+ return cfg
219
+ else:
220
+ return _instantiate_single(cfg)
221
+
222
+ cls = cfg.get("class", cfg.get("cls"))
223
+ args, kwargs = _get_instantiate_params(cfg)
224
+ try:
225
+ class_type = get_class(cls)
226
+ return class_type(*args, **kwargs)
227
+ except Exception as e:
228
+ raise RuntimeError(f"Error instantiating {cls}: {e}")
229
+ else:
230
+ return None
231
+
232
+
233
+ def instantiate(_cfg_, **kwargs):
234
+ """
235
+ Any dict with "cls" or "class" key is considered instantiable.
236
+
237
+ Any key that has the special value "__delete__"
238
+ will not be passed to the constructor
239
+
240
+ **kwargs only apply to the top level object if it's a dict, otherwise raise error
241
+ """
242
+ assert OmegaConf.is_config(_cfg_) or isinstance(_cfg_, (list, tuple)) or is_mapping(_cfg_), (
243
+ '"cfg" must be a dict, list, tuple, or OmegaConf config to be instantiated. '
244
+ f"Current its type is {type(_cfg_)}"
245
+ )
246
+
247
+ _cfg_ = omegaconf_to_dict(_cfg_, resolve=True)
248
+
249
+ if kwargs:
250
+ if is_mapping(_cfg_):
251
+ _cfg_ = _cfg_.copy()
252
+ _cfg_.update(kwargs)
253
+ _cfg_ = {k: v for k, v in _cfg_.items() if v != _DELETE_ARG}
254
+ else:
255
+ raise RuntimeError(
256
+ f"**kwargs specified, but the top-level cfg is not a dict. "
257
+ f"It has type {type(_cfg_)}"
258
+ )
259
+
260
+ return tree.traverse(_instantiate_single, _cfg_, top_down=False)
groot/vla/common/utils/io/file_utils.py ADDED
@@ -0,0 +1,707 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ File system utils.
3
+ """
4
+
5
+ import glob
6
+ import os
7
+ import pickle
8
+ import shutil
9
+ import sys
10
+ from typing import Callable, Union
11
+
12
+ from ..data_structure.tree_utils import is_sequence
13
+
14
+ __all__ = [
15
+ "create_tar",
16
+ "dump_pickle",
17
+ "dump_text",
18
+ "dump_text_lines",
19
+ "extract_tar",
20
+ "f_add_ext",
21
+ "f_append_before_ext",
22
+ "f_copy",
23
+ "f_copytree",
24
+ "f_exists",
25
+ "f_expand",
26
+ "f_ext",
27
+ "f_glob",
28
+ "f_has_ext",
29
+ "f_join",
30
+ "f_listdir",
31
+ "f_mkdir",
32
+ "f_mkdir_in_path",
33
+ "f_move",
34
+ "f_not_empty",
35
+ "f_remove",
36
+ "f_size",
37
+ "f_split_path",
38
+ "f_time",
39
+ "get_dir",
40
+ "get_file_lock",
41
+ "get_package_root",
42
+ "get_parent_dir",
43
+ "get_script_dir",
44
+ "get_script_file_name",
45
+ "get_script_self_path",
46
+ "host_id",
47
+ "host_name",
48
+ "insert_before_ext",
49
+ "is_abs_path",
50
+ "is_dir",
51
+ "is_file",
52
+ "is_relative_path",
53
+ "last_part_in_path",
54
+ "load_pickle",
55
+ "load_text",
56
+ "load_text_lines",
57
+ "md5_checksum",
58
+ "move_with_backup",
59
+ "next_available_file_name",
60
+ "owner_name",
61
+ "pickle_dump",
62
+ "pickle_load",
63
+ "read_text",
64
+ "read_text_lines",
65
+ "text_dump",
66
+ "text_load",
67
+ "timestamp_file_name",
68
+ "utf_open",
69
+ "write_text",
70
+ "write_text_lines",
71
+ ]
72
+
73
+ f_ext = os.path.splitext
74
+
75
+ f_size = os.path.getsize
76
+
77
+ is_file = os.path.isfile
78
+
79
+ is_dir = os.path.isdir
80
+
81
+ get_dir = os.path.dirname
82
+
83
+
84
+ def owner_name(filepath):
85
+ """
86
+ Returns: file owner name, unix only
87
+ """
88
+ import pwd
89
+
90
+ return pwd.getpwuid(os.stat(filepath).st_uid).pw_name
91
+
92
+
93
+ def host_name():
94
+ "Get host name, alias with ``socket.gethostname()``"
95
+ from socket import gethostname
96
+
97
+ return gethostname()
98
+
99
+
100
+ def host_id():
101
+ """
102
+ Returns: first part of hostname up to '.'
103
+ """
104
+ return host_name().split(".")[0]
105
+
106
+
107
+ def utf_open(fname, mode):
108
+ """
109
+ Wrapper for codecs.open
110
+ """
111
+ import codecs
112
+
113
+ return codecs.open(fname, mode=mode, encoding="utf-8")
114
+
115
+
116
+ def f_not_empty(*fpaths):
117
+ """
118
+ Returns:
119
+ True if and only if the file exists and file size > 0
120
+ if fpath is a dir, if and only if dir exists and has at least 1 file
121
+ """
122
+ fpath = f_join(*fpaths)
123
+ if not os.path.exists(fpath):
124
+ return False
125
+
126
+ if os.path.isdir(fpath):
127
+ return len(os.listdir(fpath)) > 0
128
+ else:
129
+ return os.path.getsize(fpath) > 0
130
+
131
+
132
+ def f_expand(fpath):
133
+ return os.path.expandvars(os.path.expanduser(fpath))
134
+
135
+
136
+ def f_exists(*fpaths):
137
+ return os.path.exists(f_join(*fpaths))
138
+
139
+
140
+ def f_join(*fpaths):
141
+ """
142
+ Join file paths and expand special symbols like `~` for home dir
143
+ """
144
+
145
+ def pack_varargs(args):
146
+ """
147
+ Pack *args or a single list arg as list
148
+
149
+ def f(*args):
150
+ arg_list = pack_varargs(args)
151
+ # arg_list is now packed as a list
152
+ """
153
+ assert isinstance(args, tuple), "please input the tuple `args` as in *args"
154
+ if len(args) == 1 and is_sequence(args[0]):
155
+ return args[0]
156
+ else:
157
+ return args
158
+
159
+ fpaths = pack_varargs(fpaths)
160
+ fpath = f_expand(os.path.join(*fpaths))
161
+ if isinstance(fpath, str):
162
+ fpath = fpath.strip()
163
+ return fpath
164
+
165
+
166
+ def f_listdir(
167
+ *fpaths,
168
+ filter_ext=None,
169
+ filter=None,
170
+ sort=True,
171
+ full_path=False,
172
+ nonexist_ok=True,
173
+ recursive=False,
174
+ ):
175
+ """
176
+ Args:
177
+ full_path: True to return full paths to the dir contents
178
+ filter: function that takes in file name and returns True to include
179
+ nonexist_ok: True to return [] if the dir is non-existent, False to raise
180
+ sort: sort the file names by alphabetical
181
+ recursive: True to use os.walk to recursively list files. Note that `filter`
182
+ will be applied to the relative path string to the root dir.
183
+ e.g. filter will take "a/data1.txt" and "a/b/data3.txt" as input, instead of
184
+ just the base file names "data1.txt" and "data3.txt".
185
+ if False, will simply call os.listdir()
186
+ """
187
+ assert not (filter_ext and filter), "filter_ext and filter are mutually exclusive"
188
+ dir_path = f_join(*fpaths)
189
+ if not os.path.exists(dir_path) and nonexist_ok:
190
+ return []
191
+ if recursive:
192
+ files = [
193
+ os.path.join(os.path.relpath(root, dir_path), file)
194
+ for root, _, files in os.walk(dir_path)
195
+ for file in files
196
+ ]
197
+ else:
198
+ files = os.listdir(dir_path)
199
+ if filter is not None:
200
+ files = [f for f in files if filter(f)]
201
+ elif filter_ext is not None:
202
+ files = [f for f in files if f.endswith(filter_ext)]
203
+ if sort:
204
+ files.sort()
205
+ if full_path:
206
+ return [os.path.join(dir_path, f) for f in files]
207
+ else:
208
+ return files
209
+
210
+
211
+ def f_mkdir(*fpaths):
212
+ """
213
+ Recursively creates all the subdirs
214
+ If exist, do nothing.
215
+ """
216
+ fpath = f_join(*fpaths)
217
+ os.makedirs(fpath, exist_ok=True)
218
+ return fpath
219
+
220
+
221
+ def f_mkdir_in_path(*fpaths):
222
+ """
223
+ fpath is a file,
224
+ recursively creates all the parent dirs that lead to the file
225
+ If exist, do nothing.
226
+ """
227
+ os.makedirs(get_dir(f_join(*fpaths)), exist_ok=True)
228
+
229
+
230
+ def last_part_in_path(fpath):
231
+ """
232
+ https://stackoverflow.com/questions/3925096/how-to-get-only-the-last-part-of-a-path-in-python
233
+ """
234
+ return os.path.basename(os.path.normpath(f_expand(fpath)))
235
+
236
+
237
+ def is_abs_path(*fpath):
238
+ return os.path.isabs(f_join(*fpath))
239
+
240
+
241
+ def is_relative_path(*fpath):
242
+ return not is_abs_path(f_join(*fpath))
243
+
244
+
245
+ def f_time(*fpath):
246
+ "File modification time"
247
+ return str(os.path.getctime(f_join(*fpath)))
248
+
249
+
250
+ def f_append_before_ext(fpath, suffix):
251
+ """
252
+ Append a suffix to file name and retain its extension
253
+ """
254
+ name, ext = f_ext(fpath)
255
+ return name + suffix + ext
256
+
257
+
258
+ def f_add_ext(fpath, ext):
259
+ """
260
+ Append an extension if not already there
261
+ Args:
262
+ ext: will add a preceding `.` if doesn't exist
263
+ """
264
+ if not ext.startswith("."):
265
+ ext = "." + ext
266
+ if fpath.endswith(ext):
267
+ return fpath
268
+ else:
269
+ return fpath + ext
270
+
271
+
272
+ def f_has_ext(fpath, ext):
273
+ "Test if file path is a text file"
274
+ _, actual_ext = f_ext(fpath)
275
+ return actual_ext == "." + ext.lstrip(".")
276
+
277
+
278
+ def f_glob(*fpath):
279
+ return glob.glob(f_join(*fpath), recursive=True)
280
+
281
+
282
+ def f_remove(*fpath, verbose=False, dry_run=False):
283
+ """
284
+ If exist, remove. Supports both dir and file. Supports glob wildcard.
285
+ """
286
+ import errno
287
+
288
+ assert isinstance(verbose, bool)
289
+ fpath = f_join(fpath)
290
+ if dry_run:
291
+ print("Dry run, delete:", fpath)
292
+ return
293
+ for f in glob.glob(fpath):
294
+ try:
295
+ shutil.rmtree(f)
296
+ except OSError as e:
297
+ if e.errno == errno.ENOTDIR:
298
+ try:
299
+ os.remove(f)
300
+ except Exception as e: # final resort safeguard
301
+ pass
302
+ if verbose:
303
+ print(f'Deleted "{fpath}"')
304
+
305
+
306
+ def f_copy(fsrc, fdst, ignore=None, include=None, exists_ok=True, verbose=False):
307
+ """
308
+ Supports both dir and file. Supports glob wildcard.
309
+ """
310
+ import errno
311
+
312
+ fsrc, fdst = f_expand(fsrc), f_expand(fdst)
313
+ for f in glob.glob(fsrc):
314
+ try:
315
+ f_copytree(f, fdst, ignore=ignore, include=include, exist_ok=exists_ok)
316
+ except OSError as e:
317
+ if e.errno == errno.ENOTDIR:
318
+ shutil.copy(f, fdst)
319
+ else:
320
+ raise
321
+ if verbose:
322
+ print(f'Copied "{fsrc}" to "{fdst}"')
323
+
324
+
325
+ def _f_copytree(
326
+ src,
327
+ dst,
328
+ symlinks=False,
329
+ ignore=None,
330
+ exist_ok=True,
331
+ copy_function=shutil.copy2,
332
+ ignore_dangling_symlinks=False,
333
+ ):
334
+ """Copied from python standard lib shutil.copytree
335
+ except that we allow exist_ok
336
+ Use f_copytree as entry
337
+ """
338
+ names = os.listdir(src)
339
+ if ignore is not None:
340
+ ignored_names = ignore(src, names)
341
+ else:
342
+ ignored_names = set()
343
+
344
+ os.makedirs(dst, exist_ok=exist_ok)
345
+ errors = []
346
+ for name in names:
347
+ if name in ignored_names:
348
+ continue
349
+ srcname = os.path.join(src, name)
350
+ dstname = os.path.join(dst, name)
351
+ try:
352
+ if os.path.islink(srcname):
353
+ linkto = os.readlink(srcname)
354
+ if symlinks:
355
+ # We can't just leave it to `copy_function` because legacy
356
+ # code with a custom `copy_function` may rely on copytree
357
+ # doing the right thing.
358
+ os.symlink(linkto, dstname)
359
+ shutil.copystat(srcname, dstname, follow_symlinks=not symlinks)
360
+ else:
361
+ # ignore dangling symlink if the flag is on
362
+ if not os.path.exists(linkto) and ignore_dangling_symlinks:
363
+ continue
364
+ # otherwise let the copy occurs. copy2 will raise an error
365
+ if os.path.isdir(srcname):
366
+ _f_copytree(srcname, dstname, symlinks, ignore, exist_ok, copy_function)
367
+ else:
368
+ copy_function(srcname, dstname)
369
+ elif os.path.isdir(srcname):
370
+ _f_copytree(srcname, dstname, symlinks, ignore, exist_ok, copy_function)
371
+ else:
372
+ # Will raise a SpecialFileError for unsupported file types
373
+ copy_function(srcname, dstname)
374
+ # catch the Error from the recursive copytree so that we can
375
+ # continue with other files
376
+ except shutil.Error as err:
377
+ errors.extend(err.args[0])
378
+ except OSError as why:
379
+ errors.append((srcname, dstname, str(why)))
380
+ try:
381
+ shutil.copystat(src, dst)
382
+ except OSError as why:
383
+ # Copying file access times may fail on Windows
384
+ if getattr(why, "winerror", None) is None:
385
+ errors.append((src, dst, str(why)))
386
+ if errors:
387
+ raise shutil.Error(errors)
388
+ return dst
389
+
390
+
391
+ def _include_patterns(*patterns):
392
+ """Factory function that can be used with copytree() ignore parameter.
393
+
394
+ Arguments define a sequence of glob-style patterns
395
+ that are used to specify what files to NOT ignore.
396
+ Creates and returns a function that determines this for each directory
397
+ in the file hierarchy rooted at the source directory when used with
398
+ shutil.copytree().
399
+ """
400
+
401
+ def _ignore_patterns(path, names):
402
+ import fnmatch
403
+
404
+ keep = set(name for pattern in patterns for name in fnmatch.filter(names, pattern))
405
+ ignore = set(
406
+ name
407
+ for name in names
408
+ if name not in keep and not os.path.isdir(os.path.join(path, name))
409
+ )
410
+ return ignore
411
+
412
+ return _ignore_patterns
413
+
414
+
415
+ def f_copytree(fsrc, fdst, symlinks=False, ignore=None, include=None, exist_ok=True):
416
+ fsrc, fdst = f_expand(fsrc), f_expand(fdst)
417
+ assert (ignore is None) or (include is None), "ignore= and include= are mutually exclusive"
418
+ if ignore:
419
+ ignore = shutil.ignore_patterns(*ignore)
420
+ elif include:
421
+ ignore = _include_patterns(*include)
422
+ _f_copytree(fsrc, fdst, ignore=ignore, symlinks=symlinks, exist_ok=exist_ok)
423
+
424
+
425
+ def f_move(fsrc, fdst):
426
+ fsrc, fdst = f_expand(fsrc), f_expand(fdst)
427
+ for f in glob.glob(fsrc):
428
+ shutil.move(f, fdst)
429
+
430
+
431
+ def f_split_path(fpath, normpath=True):
432
+ """
433
+ Splits path into a list of its component folders
434
+
435
+ Args:
436
+ normpath: call os.path.normpath to remove redundant '/' and
437
+ up-level references like ".."
438
+ """
439
+ if normpath:
440
+ fpath = os.path.normpath(fpath)
441
+ allparts = []
442
+ while 1:
443
+ parts = os.path.split(fpath)
444
+ if parts[0] == fpath: # sentinel for absolute paths
445
+ allparts.insert(0, parts[0])
446
+ break
447
+ elif parts[1] == fpath: # sentinel for relative paths
448
+ allparts.insert(0, parts[1])
449
+ break
450
+ else:
451
+ fpath = parts[0]
452
+ allparts.insert(0, parts[1])
453
+ return allparts
454
+
455
+
456
+ def get_script_dir():
457
+ """
458
+ Returns: the dir of current script
459
+ """
460
+ return os.path.dirname(os.path.realpath(sys.argv[0]))
461
+
462
+
463
+ def get_script_file_name():
464
+ """
465
+ Returns: the dir of current script
466
+ """
467
+ return os.path.basename(sys.argv[0])
468
+
469
+
470
+ def get_script_self_path():
471
+ """
472
+ Returns: the dir of current script
473
+ """
474
+ return os.path.realpath(sys.argv[0])
475
+
476
+
477
+ def get_parent_dir(location, abspath=False):
478
+ """
479
+ Args:
480
+ location: current directory or file
481
+
482
+ Returns:
483
+ parent directory absolute or relative path
484
+ """
485
+ _path = os.path.abspath if abspath else os.path.relpath
486
+ return _path(f_join(location, os.pardir))
487
+
488
+
489
+ def md5_checksum(*fpath):
490
+ """
491
+ File md5 signature
492
+ """
493
+ import hashlib
494
+
495
+ hash_md5 = hashlib.md5()
496
+ with open(f_join(*fpath), "rb") as f:
497
+ for chunk in iter(lambda: f.read(65536), b""):
498
+ hash_md5.update(chunk)
499
+ return hash_md5.hexdigest()
500
+
501
+
502
+ def create_tar(fsrc, output_tarball, include=None, ignore=None, compress_mode="gz"):
503
+ """
504
+ Args:
505
+ fsrc: source file or folder
506
+ output_tarball: output tar file name
507
+ compress_mode: ``gz``, ``bz2``, ``xz`` or ``''`` (empty for uncompressed write)
508
+ include: include pattern, will trigger copy to temp directory
509
+ ignore: ignore pattern, will trigger copy to temp directory
510
+ """
511
+ import tarfile
512
+ import tempfile
513
+
514
+ fsrc, output_tarball = f_expand(fsrc), f_expand(output_tarball)
515
+ assert compress_mode in ["gz", "bz2", "xz", ""]
516
+ src_base = os.path.basename(fsrc)
517
+
518
+ tempdir = None
519
+ if include or ignore:
520
+ tempdir = tempfile.mkdtemp()
521
+ tempdest = f_join(tempdir, src_base)
522
+ f_copy(fsrc, tempdest, include=include, ignore=ignore)
523
+ fsrc = tempdest
524
+
525
+ with tarfile.open(output_tarball, "w:" + compress_mode) as tar:
526
+ tar.add(fsrc, arcname=src_base)
527
+
528
+ if tempdir:
529
+ f_remove(tempdir)
530
+
531
+
532
+ def extract_tar(source_tarball, output_dir=".", members=None):
533
+ """
534
+ Args:
535
+ source_tarball: extract members from archive
536
+ output_dir: default to current working dir
537
+ members: must be a subset of the list returned by getmembers()
538
+ """
539
+ import tarfile
540
+
541
+ source_tarball, output_dir = f_expand(source_tarball), f_expand(output_dir)
542
+ with tarfile.open(source_tarball, "r:*") as tar:
543
+ tar.extractall(output_dir, members=members)
544
+
545
+
546
+ def move_with_backup(*fpath, suffix=".bak"):
547
+ """
548
+ Ensures that a path is not occupied. If there is a file, rename it by
549
+ adding @suffix. Resursively backs up everything.
550
+
551
+ Args:
552
+ fpath: file path to clear
553
+ suffix: Add to backed up files (default: {'.bak'})
554
+ """
555
+ fpath = str(f_join(*fpath))
556
+ if os.path.exists(fpath):
557
+ move_with_backup(fpath + suffix)
558
+ shutil.move(fpath, fpath + suffix)
559
+
560
+
561
+ def insert_before_ext(name, insert):
562
+ """
563
+ log.txt -> log.ep50.txt
564
+ """
565
+ name, ext = os.path.splitext(name)
566
+ return name + insert + ext
567
+
568
+
569
+ def timestamp_file_name(fname):
570
+ from datetime import datetime
571
+
572
+ timestr = datetime.now().strftime("_%H-%M-%S_%m-%d-%y")
573
+ return insert_before_ext(fname, timestr)
574
+
575
+
576
+ def next_available_file_name(
577
+ *fpath,
578
+ suffix_template: Union[str, Callable[[int], str]] = "_v{i+1}",
579
+ before_ext: bool = True,
580
+ ):
581
+ """
582
+ Args:
583
+ suffix_template: a format string using "i" variable or
584
+ lambda int -> str
585
+ before_ext: True to insert suffix before the extension
586
+ """
587
+
588
+ def fstring(fmt_str, **kwargs):
589
+ """
590
+ Simulate python f-string but without `f`
591
+ """
592
+ import shlex
593
+
594
+ locals().update(kwargs)
595
+ return eval("f" + shlex.quote(fmt_str))
596
+
597
+ orig_file_path = f_join(*fpath)
598
+ i = 0
599
+ fpath = orig_file_path
600
+ while os.path.exists(fpath):
601
+ if isinstance(suffix_template, str):
602
+ suffix = fstring(suffix_template, i=i)
603
+ elif callable(suffix_template):
604
+ suffix = suffix_template(i)
605
+ assert isinstance(suffix, str)
606
+ else:
607
+ raise NotImplementedError(f"Unsupported suffix template {suffix_template}")
608
+ if before_ext:
609
+ fpath = insert_before_ext(orig_file_path, suffix)
610
+ else:
611
+ fpath = orig_file_path + suffix
612
+ i += 1
613
+ return fpath
614
+
615
+
616
+ def get_file_lock(*fpath, timeout: int = 15, logging_level="critical"):
617
+ """
618
+ NFS-safe filesystem-backed lock. `pip install flufl.lock`
619
+ https://flufllock.readthedocs.io/en/stable/apiref.html
620
+
621
+ Args:
622
+ fpath: should be a path on NFS so that every process can see it
623
+ timeout: seconds
624
+ """
625
+ import logging
626
+
627
+ from flufl.lock import Lock
628
+
629
+ logging.getLogger("flufl.lock").setLevel(logging_level.upper())
630
+ return Lock(f_join(*fpath), lifetime=timeout)
631
+
632
+
633
+ def load_pickle(*fpaths):
634
+ with open(f_join(*fpaths), "rb") as fp:
635
+ return pickle.load(fp)
636
+
637
+
638
+ def dump_pickle(data, *fpaths):
639
+ with open(f_join(*fpaths), "wb") as fp:
640
+ pickle.dump(data, fp)
641
+
642
+
643
+ def load_text(*fpaths, by_lines=False):
644
+ with open(f_join(*fpaths), "r") as fp:
645
+ if by_lines:
646
+ return fp.readlines()
647
+ else:
648
+ return fp.read()
649
+
650
+
651
+ def load_text_lines(*fpaths):
652
+ return load_text(*fpaths, by_lines=True)
653
+
654
+
655
+ def dump_text(s, *fpaths):
656
+ with open(f_join(*fpaths), "w") as fp:
657
+ fp.write(s)
658
+
659
+
660
+ def dump_text_lines(lines: list[str], *fpaths, add_newline=True):
661
+ with open(f_join(*fpaths), "w") as fp:
662
+ for line in lines:
663
+ print(line, file=fp, end="\n" if add_newline else "")
664
+
665
+
666
+ def get_package_root() -> str:
667
+ import importlib.util
668
+ import inspect
669
+
670
+ # Get the current frame
671
+ current_frame = inspect.currentframe()
672
+ if current_frame is None:
673
+ raise ImportError("Cannot determine the package name from __package__")
674
+
675
+ # Get the caller module
676
+ caller_module = inspect.getmodule(current_frame.f_back)
677
+ if caller_module is None:
678
+ raise ImportError("Cannot determine the package name from __package__")
679
+
680
+ # Get the package name
681
+ package_name = caller_module.__package__
682
+ if not package_name:
683
+ raise ImportError("Cannot determine the package name from __package__")
684
+
685
+ # Get the top-level package name
686
+ top_package_name = package_name.split(".")[0]
687
+
688
+ # Find the package specification
689
+ spec = importlib.util.find_spec(top_package_name)
690
+
691
+ if spec and spec.origin:
692
+ # Get the directory containing the package's __init__.py file
693
+ package_dir = os.path.dirname(spec.origin)
694
+ return package_dir
695
+ else:
696
+ raise ImportError(f"Cannot find the package {top_package_name}")
697
+
698
+
699
+ # aliases to be consistent with other load_* and dump_*
700
+ pickle_load = load_pickle
701
+ pickle_dump = dump_pickle
702
+ text_load = load_text
703
+ read_text = load_text
704
+ read_text_lines = load_text_lines
705
+ write_text = dump_text
706
+ write_text_lines = dump_text_lines
707
+ text_dump = dump_text
groot/vla/common/utils/io/hdf5_utils.py ADDED
@@ -0,0 +1,84 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import h5py
2
+ import numpy as np
3
+ from pydantic import BaseModel
4
+
5
+
6
+ def hdf5_save(data: BaseModel | dict, group: h5py.Group) -> None:
7
+ """Recursively save Pydantic model or dict to HDF5 group."""
8
+ if isinstance(data, BaseModel):
9
+ # Convert to dict and exclude None values
10
+ data_dict = data.model_dump(mode="python", exclude_none=True)
11
+ else:
12
+ data_dict = data
13
+
14
+ for key, value in data_dict.items():
15
+ if isinstance(value, np.ndarray):
16
+ group.create_dataset(key, data=value)
17
+ elif isinstance(value, (BaseModel, dict)):
18
+ subgroup = group.create_group(key)
19
+ hdf5_save(value, subgroup)
20
+ else:
21
+ # For primitive types, convert to numpy array
22
+ try:
23
+ group.create_dataset(key, data=np.array(value))
24
+ except TypeError:
25
+ raise ValueError(f"Unsupported type: {type(value)} for key: {key}")
26
+
27
+
28
+ def hdf5_load(group: h5py.Group) -> dict:
29
+ """Recursively load HDF5 group to Pydantic model or dict."""
30
+ data_dict = {}
31
+ for key, value in group.items():
32
+ if isinstance(value, h5py.Dataset):
33
+ data_dict[key] = value[()]
34
+ elif isinstance(value, h5py.Group):
35
+ data_dict[key] = hdf5_load(value)
36
+ return data_dict
37
+
38
+
39
+ def hdf5_is_subset(this: h5py.Group, other: h5py.Group, verbose: bool = False) -> bool:
40
+ """Check if this HDF5 group is a subset of another HDF5 group."""
41
+ for key, value in this.items():
42
+ if key not in other:
43
+ if verbose:
44
+ print(f"Key {key} not in other")
45
+ return False
46
+ elif isinstance(value, h5py.Group):
47
+ if not isinstance(other[key], h5py.Group):
48
+ if verbose:
49
+ print(f"Key {key} is not a group in other")
50
+ return False
51
+ if not hdf5_is_subset(value, other[key], verbose):
52
+ if verbose:
53
+ print(f"Key {key} is not a subset of other")
54
+ return False
55
+ elif isinstance(value, h5py.Dataset):
56
+ if not isinstance(other[key], h5py.Dataset):
57
+ if verbose:
58
+ print(f"Key {key} is not a dataset in other")
59
+ return False
60
+ if not np.array_equal(value, other[key]):
61
+ if verbose:
62
+ print(f"Key {key} is not equal in other")
63
+ return False
64
+ elif isinstance(value, h5py.Datatype):
65
+ if not isinstance(other[key], h5py.Datatype):
66
+ if verbose:
67
+ print(f"Key {key} is not a datatype in other")
68
+ return False
69
+ if value != other[key]:
70
+ if verbose:
71
+ print(f"Key {key} is not equal in other")
72
+ return False
73
+ else:
74
+ # try to compare
75
+ if value != other[key]:
76
+ if verbose:
77
+ print(f"Key {key} is not equal in other")
78
+ return False
79
+ return True
80
+
81
+
82
+ def hdf5_is_equal(this: h5py.Group, other: h5py.Group, verbose: bool = False) -> bool:
83
+ """Check if this HDF5 group is equal to another HDF5 group."""
84
+ return hdf5_is_subset(this, other, verbose) and hdf5_is_subset(other, this, verbose)
groot/vla/common/utils/io/json_utils.py ADDED
@@ -0,0 +1,270 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ JSON, YAML, and python config file utilities
3
+ """
4
+
5
+ from io import StringIO
6
+ import json
7
+ import os.path as path
8
+
9
+ import yaml
10
+
11
+ from ..misc.functional_utils import make_recursive_func
12
+ from .file_utils import f_join
13
+
14
+ __all__ = [
15
+ "json_load",
16
+ "json_loads",
17
+ "jsonl_load",
18
+ "yaml_load",
19
+ "yaml_loads",
20
+ "json_dump",
21
+ "json_dumps",
22
+ "jsonl_dump",
23
+ "yaml_dump",
24
+ "yaml_dumps",
25
+ "json_or_yaml_load",
26
+ "json_or_yaml_dump",
27
+ "Jsonl",
28
+ # ---------------- Aliases -----------------
29
+ "load_json",
30
+ "loads_json",
31
+ "load_jsonl",
32
+ "load_yaml",
33
+ "loads_yaml",
34
+ "dump_json",
35
+ "dumps_json",
36
+ "dump_jsonl",
37
+ "dump_yaml",
38
+ "dumps_yaml",
39
+ "load_json_or_yaml",
40
+ "dump_json_or_yaml",
41
+ ]
42
+
43
+ from typing import Dict, List
44
+
45
+ from typing_extensions import Literal
46
+
47
+
48
+ def json_load(*file_path, **kwargs):
49
+ file_path = f_join(file_path)
50
+ with open(file_path, "r") as fp:
51
+ return json.load(fp, **kwargs)
52
+
53
+
54
+ def json_loads(string, **kwargs):
55
+ return json.loads(string, **kwargs)
56
+
57
+
58
+ def jsonl_load(*file_path, **kwargs):
59
+ file_path = f_join(file_path)
60
+ data = []
61
+ for line in open(file_path):
62
+ data.append(json.loads(line, **kwargs))
63
+ return data
64
+
65
+
66
+ @make_recursive_func
67
+ def any_to_primitive(x):
68
+ try:
69
+ import torch
70
+ except ImportError:
71
+ raise ImportError("torch is required for any_to_primitive")
72
+ import numpy as np
73
+
74
+ if isinstance(x, (np.ndarray, np.number, torch.Tensor)):
75
+ return x.tolist()
76
+ else:
77
+ return x
78
+
79
+
80
+ def json_dump(data, *file_path, convert_to_primitive=False, **kwargs):
81
+ if convert_to_primitive:
82
+ data = any_to_primitive(data)
83
+ file_path = f_join(file_path)
84
+ with open(file_path, "w") as fp:
85
+ json.dump(data, fp, **kwargs)
86
+
87
+
88
+ def json_dumps(data, convert_to_primitive=False, **kwargs):
89
+ """
90
+ Returns: string
91
+ """
92
+ if convert_to_primitive:
93
+ data = any_to_primitive(data)
94
+ return json.dumps(data, **kwargs)
95
+
96
+
97
+ def jsonl_dump(data, *file_path):
98
+ from .file_utils import is_sequence
99
+
100
+ assert is_sequence(data)
101
+ data = any_to_primitive(data)
102
+ file_path = f_join(file_path)
103
+ with open(file_path, "w") as fp:
104
+ for line in data:
105
+ print(json.dumps(line), file=fp, flush=True)
106
+
107
+
108
+ def yaml_load(*file_path, loader=yaml.safe_load, **kwargs):
109
+ file_path = f_join(file_path)
110
+ with open(file_path, "r") as fp:
111
+ return loader(fp, **kwargs)
112
+
113
+
114
+ def yaml_loads(string, *, loader=yaml.safe_load, **kwargs):
115
+ return loader(string, **kwargs)
116
+
117
+
118
+ def yaml_dump(data, *file_path, dumper=yaml.safe_dump, convert_to_primitive=False, **kwargs):
119
+ if convert_to_primitive:
120
+ data = any_to_primitive(data)
121
+ file_path = f_join(file_path)
122
+ indent = kwargs.pop("indent", 2)
123
+ default_flow_style = kwargs.pop("default_flow_style", False)
124
+ sort_keys = kwargs.pop("sort_keys", False) # preserves original dict order
125
+ with open(file_path, "w") as fp:
126
+ dumper(
127
+ data,
128
+ stream=fp,
129
+ indent=indent,
130
+ default_flow_style=default_flow_style,
131
+ sort_keys=sort_keys,
132
+ **kwargs,
133
+ )
134
+
135
+
136
+ def yaml_dumps(data, *, dumper=yaml.safe_dump, convert_to_primitive=False, **kwargs):
137
+ "Returns: string"
138
+ if convert_to_primitive:
139
+ data = any_to_primitive(data)
140
+ stream = StringIO()
141
+ indent = kwargs.pop("indent", 2)
142
+ default_flow_style = kwargs.pop("default_flow_style", False)
143
+ sort_keys = kwargs.pop("sort_keys", False) # preserves original dict order
144
+ dumper(
145
+ data,
146
+ stream,
147
+ indent=indent,
148
+ default_flow_style=default_flow_style,
149
+ sort_keys=sort_keys,
150
+ **kwargs,
151
+ )
152
+ return stream.getvalue()
153
+
154
+
155
+ # ==================== auto-recognize extension ====================
156
+ def json_or_yaml_load(*file_path, **loader_kwargs):
157
+ """
158
+ Args:
159
+ file_path: JSON or YAML loader depends on the file extension
160
+
161
+ Raises:
162
+ IOError: if extension is not ".json", ".yml", or ".yaml"
163
+ """
164
+ file_path = str(f_join(file_path))
165
+ if file_path.endswith(".json"):
166
+ return json_load(file_path, **loader_kwargs)
167
+ elif file_path.endswith(".yml") or file_path.endswith(".yaml"):
168
+ return yaml_load(file_path, **loader_kwargs)
169
+ else:
170
+ raise IOError(
171
+ f'unknown file extension: "{file_path}", '
172
+ f'loader supports only ".json", ".yml", ".yaml"'
173
+ )
174
+
175
+
176
+ def json_or_yaml_dump(data, *file_path, **dumper_kwargs):
177
+ """
178
+ Args:
179
+ file_path: JSON or YAML loader depends on the file extension
180
+
181
+ Raises:
182
+ IOError: if extension is not ".json", ".yml", or ".yaml"
183
+ """
184
+ file_path = str(f_join(file_path))
185
+ if file_path.endswith(".json"):
186
+ return json_dump(data, file_path, **dumper_kwargs)
187
+ elif file_path.endswith(".yml") or file_path.endswith(".yaml"):
188
+ return yaml_dump(data, file_path, **dumper_kwargs)
189
+ else:
190
+ raise IOError(
191
+ f'unknown file extension: "{file_path}", '
192
+ f'dumper supports only ".json", ".yml", ".yaml"'
193
+ )
194
+
195
+
196
+ # ---------------- Aliases -----------------
197
+ # add aliases where verb goes first, json_load -> load_json
198
+ load_json = json_load
199
+ load_yaml = yaml_load
200
+ load_jsonl = jsonl_load
201
+ loads_json = json_loads
202
+ loads_yaml = yaml_loads
203
+ dump_json = json_dump
204
+ dump_jsonl = jsonl_dump
205
+ dump_yaml = yaml_dump
206
+ dumps_json = json_dumps
207
+ dumps_yaml = yaml_dumps
208
+ load_json_or_yaml = json_or_yaml_load
209
+ dump_json_or_yaml = json_or_yaml_dump
210
+
211
+ # ==================== Jsonl ====================
212
+
213
+
214
+ class Jsonl:
215
+ """
216
+ Both reader and writer, as if everything's in-memory
217
+ """
218
+
219
+ def __init__(self, *file_path, mode: Literal["r", "w", "a"] = "a"):
220
+ """
221
+ Args:
222
+ mode:
223
+ - 'r': file must already exists
224
+ - 'w': overwrite the file regardless of whether it exists or not
225
+ - 'a': create a new file if doesn't exist, or append to an existing file
226
+ """
227
+ assert mode in "rwa"
228
+ self._file_path = str(f_join(file_path))
229
+ self._mode = mode
230
+ if mode == "r":
231
+ assert path.exists(self._file_path)
232
+ self._fp = None
233
+ else:
234
+ self._fp = open(self._file_path, mode)
235
+ if path.exists(self._file_path) and mode != "w":
236
+ self.data = jsonl_load(self._file_path)
237
+ else:
238
+ self.data = []
239
+
240
+ def append(self, data: Dict):
241
+ if self._mode == "r":
242
+ raise RuntimeError("Jsonl read mode cannot call append()")
243
+ self.data.append(data)
244
+ print(json_dumps(data), file=self._fp, flush=True)
245
+
246
+ def extend(self, data_list: List[Dict]):
247
+ for data in data_list:
248
+ self.append(data)
249
+
250
+ def close(self):
251
+ if self._fp is not None:
252
+ self._fp.close()
253
+
254
+ def __getitem__(self, idx):
255
+ return self.data[idx]
256
+
257
+ def __len__(self):
258
+ return len(self.data)
259
+
260
+ def __iter__(self):
261
+ return iter(self.data)
262
+
263
+ def __enter__(self):
264
+ return self
265
+
266
+ def __exit__(self, type, value, traceback):
267
+ self.close()
268
+
269
+ def __bool__(self):
270
+ return bool(self.data)
groot/vla/common/utils/io/print_utils.py ADDED
@@ -0,0 +1,362 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ import io
3
+ import logging
4
+ import os
5
+ import pprint
6
+ import shlex
7
+ import string
8
+ import sys
9
+ import textwrap
10
+ import time
11
+ import traceback
12
+ from typing import Callable, Union
13
+
14
+ import numpy as np
15
+ from typing_extensions import Literal
16
+
17
+ from ..misc.functional_utils import meta_decorator
18
+ from ..misc.misc_utils import match_patterns
19
+
20
+
21
+ def to_readable_count_str(value: int, precision: int = 2) -> str:
22
+ assert value >= 0
23
+ labels = [" ", "K", "M", "B", "T"]
24
+ num_digits = int(np.floor(np.log10(value)) + 1 if value > 0 else 1)
25
+ num_groups = int(np.ceil(num_digits / 3))
26
+ num_groups = min(num_groups, len(labels)) # don't abbreviate beyond trillions
27
+ shift = -3 * (num_groups - 1)
28
+ value = value * (10**shift)
29
+ index = num_groups - 1
30
+ rem = value - int(value)
31
+ if precision > 0 and rem > 0.01:
32
+ fmt = f"{{:.{precision}f}}"
33
+ rem_str = fmt.format(rem).lstrip("0")
34
+ else:
35
+ rem_str = ""
36
+ return f"{int(value):,d}{rem_str} {labels[index]}"
37
+
38
+
39
+ def to_scientific_str(value, precision: int = 1, capitalize: bool = False) -> str:
40
+ """
41
+ 0.0015 -> "1.5e-3"
42
+ """
43
+ if value == 0:
44
+ return "0"
45
+ return f"{value:.{precision}e}".replace("e-0", "E-" if capitalize else "e-")
46
+
47
+
48
+ def print_str(*args, **kwargs):
49
+ """
50
+ Same as print() signature but returns a string
51
+ """
52
+ sstream = io.StringIO()
53
+ kwargs.pop("file", None)
54
+ print(*args, **kwargs, file=sstream)
55
+ return sstream.getvalue()
56
+
57
+
58
+ def fstring(fmt_str, **kwargs):
59
+ """
60
+ Simulate python f-string but without `f`
61
+ """
62
+ locals().update(kwargs)
63
+ return eval("f" + shlex.quote(fmt_str))
64
+
65
+
66
+ def get_format_keys(fmt_str):
67
+ keys = []
68
+ for literal, field_name, fmt_spec, conversion in string.Formatter().parse(fmt_str):
69
+ if field_name:
70
+ keys.append(field_name)
71
+ return keys
72
+
73
+
74
+ def get_timestamp(milli_precision: int = 3):
75
+ fmt = "%y-%m-%d %H:%M:%S"
76
+ if milli_precision > 0:
77
+ fmt += ".%f"
78
+ stamp = datetime.now().strftime(fmt)
79
+ if milli_precision > 0:
80
+ stamp = stamp[:-milli_precision]
81
+ return stamp
82
+
83
+
84
+ def pretty_repr_str(obj, **kwargs):
85
+ """
86
+ Useful to produce __repr__()
87
+ """
88
+ if isinstance(obj, str):
89
+ cls_name = obj
90
+ else:
91
+ cls_name = obj.__class__.__name__
92
+ kw_strs = [k + "=" + pprint.pformat(v, indent=2, compact=True) for k, v in kwargs.items()]
93
+ new_line = len(cls_name) + sum(len(kw) for kw in kw_strs) > 84
94
+ if new_line:
95
+ kw = ",\n".join(kw_strs)
96
+ return f"{cls_name}(\n{textwrap.indent(kw, ' ')}\n)"
97
+ else:
98
+ kw = ", ".join(kw_strs)
99
+ return f"{cls_name}({kw})"
100
+
101
+
102
+ def pprint_(*objs, **kwargs):
103
+ """
104
+ Use pprint to format the objects
105
+ """
106
+ print(
107
+ *[pprint.pformat(obj, indent=2) if not isinstance(obj, str) else obj for obj in objs],
108
+ **kwargs,
109
+ )
110
+
111
+
112
+ def get_exception_info(to_str: bool = False):
113
+ """
114
+ Returns:
115
+ {'type': ExceptionType, 'value': ExceptionObject, 'trace': <traceback str>}
116
+ """
117
+ typ_, value, trace = sys.exc_info()
118
+ return {
119
+ "type": typ_.__name__ if to_str else typ_,
120
+ "value": str(value) if to_str else value,
121
+ "trace": "".join(traceback.format_exception(typ_, value, trace)),
122
+ }
123
+
124
+
125
+ class DebugPrinter:
126
+ """
127
+ Debug print, usage: dprint = DebugPrint(enabled=True)
128
+ dprint(...)
129
+ """
130
+
131
+ def __init__(self, enabled, tensor_summary: Literal["shape", "shape+dtype", "none"] = "shape"):
132
+ """
133
+ Args:
134
+ tensor_summary:
135
+ - shape: only prints shape
136
+ - shape+dtype: also prints dtype and device
137
+ - none: print full tensor
138
+ """
139
+ self.enabled = enabled
140
+ assert tensor_summary in ["shape", "shape+dtype", "none"]
141
+ self.tensor_summary = tensor_summary
142
+
143
+ def __call__(self, *args, **kwargs):
144
+ if not self.enabled:
145
+ return
146
+ args = [self._process_arg(a) for a in args]
147
+ pprint_(*args, **kwargs)
148
+
149
+ def _process_arg(self, arg):
150
+ import numpy as np
151
+ import torch
152
+
153
+ if torch.is_tensor(arg):
154
+ if self.tensor_summary == "shape":
155
+ return str(list(arg.size()))
156
+ elif self.tensor_summary == "shape+dtype":
157
+ return f"{arg.dtype}{list(arg.size())}|{arg.device}"
158
+ elif isinstance(arg, np.ndarray):
159
+ if self.tensor_summary == "shape":
160
+ return str(list(arg.shape))
161
+ elif self.tensor_summary == "shape+dtype":
162
+ return f"{arg.dtype}{list(arg.shape)}"
163
+ return arg
164
+
165
+
166
+ @meta_decorator
167
+ def watch(func, seconds: int = 5, max_times: int = 0, keep_returns: bool = False):
168
+ """
169
+ Decorator: executes a function repeated with the args and
170
+ emulate `watch -n` capability
171
+
172
+ See `gpustat` repo: https://github.com/wookayin/gpustat/pull/41/files
173
+
174
+ Args:
175
+ max_times: watch for `max_times` and then exit. If 0, never exits
176
+ keep_returns: if True, will keep the return value from the function
177
+ and return as a list at the end
178
+ """
179
+ from blessings import Terminal
180
+
181
+ def _wrapped(*args, **kwargs):
182
+ term = Terminal()
183
+ N = 0
184
+ returns = []
185
+ with term.fullscreen():
186
+ while True:
187
+ try:
188
+ with term.location(0, 0):
189
+ ret = func(*args, **kwargs)
190
+ print(term.clear_eos, end="")
191
+ if keep_returns:
192
+ returns.append(ret)
193
+ N += 1
194
+ if max_times > 0 and N >= max_times:
195
+ break
196
+ time.sleep(seconds)
197
+ except KeyboardInterrupt:
198
+ break
199
+ return returns
200
+
201
+ return _wrapped
202
+
203
+
204
+ class PrintRedirection(object):
205
+ """
206
+ Context manager: temporarily redirects stdout and stderr
207
+ """
208
+
209
+ def __init__(self, stdout=None, stderr=None):
210
+ """
211
+ Args:
212
+ stdout: if None, defaults to sys.stdout, unchanged
213
+ stderr: if None, defaults to sys.stderr, unchanged
214
+ """
215
+ if stdout is None:
216
+ stdout = sys.stdout
217
+ if stderr is None:
218
+ stderr = sys.stderr
219
+ self._stdout, self._stderr = stdout, stderr
220
+
221
+ def __enter__(self):
222
+ self._old_out, self._old_err = sys.stdout, sys.stderr
223
+ self._old_out.flush()
224
+ self._old_err.flush()
225
+ sys.stdout, sys.stderr = self._stdout, self._stderr
226
+ return self
227
+
228
+ def __exit__(self, exc_type, exc_value, traceback):
229
+ self.flush()
230
+ # restore the normal stdout and stderr
231
+ sys.stdout, sys.stderr = self._old_out, self._old_err
232
+
233
+ def flush(self):
234
+ "Manually flush the replaced stdout/stderr buffers."
235
+ self._stdout.flush()
236
+ self._stderr.flush()
237
+
238
+
239
+ class PrintToFile(PrintRedirection):
240
+ """
241
+ Print to file and save/close the handle at the end.
242
+ """
243
+
244
+ def __init__(self, out_file=None, err_file=None):
245
+ """
246
+ Args:
247
+ out_file: file path
248
+ err_file: file path. If the same as out_file, print both stdout
249
+ and stderr to one file in order.
250
+ """
251
+ self.out_file, self.err_file = out_file, err_file
252
+ if out_file:
253
+ out_file = os.path.expanduser(out_file)
254
+ self.out_file = open(out_file, "w")
255
+ if err_file:
256
+ err_file = os.path.expanduser(out_file)
257
+ if err_file == out_file: # redirect both stdout/err to one file
258
+ self.err_file = self.out_file
259
+ else:
260
+ self.err_file = open(os.path.expanduser(out_file), "w")
261
+ super().__init__(stdout=self.out_file, stderr=self.err_file)
262
+
263
+ def __exit__(self, *args):
264
+ super().__exit__(*args)
265
+ if self.out_file:
266
+ self.out_file.close()
267
+ if self.err_file:
268
+ self.err_file.close()
269
+
270
+
271
+ def PrintSuppress(no_out=True, no_err=False):
272
+ """
273
+ Args:
274
+ no_out: stdout writes to sys.devnull
275
+ no_err: stderr writes to sys.devnull
276
+ """
277
+ out_file = os.devnull if no_out else None
278
+ err_file = os.devnull if no_err else None
279
+ return PrintToFile(out_file=out_file, err_file=err_file)
280
+
281
+
282
+ class PrintString(PrintRedirection):
283
+ """
284
+ Redirect stdout and stderr to strings.
285
+ """
286
+
287
+ def __init__(self):
288
+ self.out_stream = io.StringIO()
289
+ self.err_stream = io.StringIO()
290
+ super().__init__(stdout=self.out_stream, stderr=self.err_stream)
291
+
292
+ def stdout(self):
293
+ "Returns: stdout as one string."
294
+ return self.out_stream.getvalue()
295
+
296
+ def stderr(self):
297
+ "Returns: stderr as one string."
298
+ return self.err_stream.getvalue()
299
+
300
+ def stdout_by_line(self):
301
+ "Returns: a list of stdout line by line, ignore trailing blanks"
302
+ return self.stdout().rstrip().split("\n")
303
+
304
+ def stderr_by_line(self):
305
+ "Returns: a list of stderr line by line, ignore trailing blanks"
306
+ return self.stderr().rstrip().split("\n")
307
+
308
+
309
+ # ==================== Logging filters ====================
310
+ class ExcludeLoggingFilter(logging.Filter):
311
+ """
312
+ Usage: logging.getLogger('name').addFilter(
313
+ ExcludeLoggingFilter(['info mess*age', 'Warning: *'])
314
+ )
315
+ Supports wildcard.
316
+ https://relaxdiego.com/2014/07/logging-in-python.html
317
+ """
318
+
319
+ def __init__(self, patterns):
320
+ super().__init__()
321
+ self._patterns = patterns
322
+
323
+ def filter(self, record):
324
+ if match_patterns(record.msg, include=self._patterns):
325
+ return False
326
+ else:
327
+ return True
328
+
329
+
330
+ class ReplaceStringLoggingFilter(logging.Filter):
331
+ def __init__(self, patterns, replacer: Callable):
332
+ super().__init__()
333
+ self._patterns = patterns
334
+ assert callable(replacer)
335
+ self._replacer = replacer
336
+
337
+ def filter(self, record):
338
+ if match_patterns(record.msg, include=self._patterns):
339
+ record.msg = self._replacer(record.msg)
340
+
341
+
342
+ def logging_exclude_pattern(
343
+ logger_name,
344
+ patterns: Union[str, list[str], Callable, list[Callable], None],
345
+ ):
346
+ """
347
+ Args:
348
+ patterns: see groot.vla.common.utils.misc_utils.match_patterns
349
+ """
350
+ logging.getLogger(logger_name).addFilter(ExcludeLoggingFilter(patterns))
351
+
352
+
353
+ def logging_replace_string(
354
+ logger_name,
355
+ patterns: Union[str, list[str], Callable, list[Callable], None],
356
+ replacer: Callable,
357
+ ):
358
+ """
359
+ Args:
360
+ patterns: see groot.vla.common.utils.misc_utils.match_patterns
361
+ """
362
+ logging.getLogger(logger_name).addFilter(ReplaceStringLoggingFilter(patterns, replacer))
groot/vla/common/utils/io/termcolor.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding: utf-8
2
+ # Copyright (c) 2008-2011 Volvox Development Team
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Original Author: Konstantin Lepa <konstantin.lepa@gmail.com>
23
+ # Updated by Jim Fan
24
+
25
+ """ANSII Color formatting for output in terminal."""
26
+ import io
27
+ import os
28
+ from typing import List, Optional, Union
29
+
30
+ __ALL__ = ["color_text", "cprint"]
31
+
32
+ STYLES = dict(
33
+ list(
34
+ zip(
35
+ ["bold", "dark", "", "underline", "blink", "", "reverse", "concealed"],
36
+ list(range(1, 9)),
37
+ )
38
+ )
39
+ )
40
+ del STYLES[""]
41
+
42
+
43
+ HIGHLIGHTS = dict(
44
+ list(
45
+ zip(
46
+ ["grey", "red", "green", "yellow", "blue", "magenta", "cyan", "white"],
47
+ list(range(40, 48)),
48
+ )
49
+ )
50
+ )
51
+
52
+
53
+ COLORS = dict(
54
+ list(
55
+ zip(
56
+ ["grey", "red", "green", "yellow", "blue", "magenta", "cyan", "white"],
57
+ list(range(30, 38)),
58
+ )
59
+ )
60
+ )
61
+
62
+
63
+ def _strip_bg_prefix(color):
64
+ "on_red -> red"
65
+ if color.startswith("on_"):
66
+ return color[len("on_") :]
67
+ else:
68
+ return color
69
+
70
+
71
+ RESET = "\033[0m"
72
+
73
+
74
+ def color_text(
75
+ text,
76
+ color: Optional[str] = None,
77
+ bg_color: Optional[str] = None,
78
+ styles: Optional[Union[str, List[str]]] = None,
79
+ ):
80
+ """Colorize text.
81
+
82
+ Available text colors:
83
+ red, green, yellow, blue, magenta, cyan, white.
84
+
85
+ Available text highlights:
86
+ on_red, on_green, on_yellow, on_blue, on_magenta, on_cyan, on_white.
87
+
88
+ Available attributes:
89
+ bold, dark, underline, blink, reverse, concealed.
90
+
91
+ Example:
92
+ colored('Hello, World!', 'red', 'on_grey', ['blue', 'blink'])
93
+ colored('Hello, World!', 'green')
94
+ """
95
+ if os.getenv("ANSI_COLORS_DISABLED") is None:
96
+ fmt_str = "\033[%dm%s"
97
+ if color is not None:
98
+ text = fmt_str % (COLORS[color], text)
99
+
100
+ if bg_color is not None:
101
+ bg_color = _strip_bg_prefix(bg_color)
102
+ text = fmt_str % (HIGHLIGHTS[bg_color], text)
103
+
104
+ if styles is not None:
105
+ if isinstance(styles, str):
106
+ styles = [styles]
107
+ for style in styles:
108
+ text = fmt_str % (STYLES[style], text)
109
+
110
+ text += RESET
111
+ return text
112
+
113
+
114
+ def cprint(
115
+ *args,
116
+ color: Optional[str] = None,
117
+ bg_color: Optional[str] = None,
118
+ styles: Optional[Union[str, List[str]]] = None,
119
+ **kwargs,
120
+ ):
121
+ """Print colorize text.
122
+
123
+ It accepts arguments of print function.
124
+ """
125
+ sstream = io.StringIO()
126
+ print(*args, sep=kwargs.pop("sep", None), end="", file=sstream)
127
+ text = sstream.getvalue()
128
+ print((color_text(text, color, bg_color, styles)), **kwargs)
129
+
130
+
131
+ if __name__ == "__main__":
132
+ print("Current terminal type: %s" % os.getenv("TERM"))
133
+ print("Test basic colors:")
134
+ cprint("Grey color", color="grey")
135
+ cprint("Red color", color="red")
136
+ cprint("Green color", color="green")
137
+ cprint("Yellow color", color="yellow")
138
+ cprint("Blue color", color="blue")
139
+ cprint("Magenta color", color="magenta")
140
+ cprint("Cyan color", color="cyan")
141
+ cprint("White color", color="white")
142
+ print(("-" * 78))
143
+
144
+ print("Test highlights:")
145
+ cprint("On grey color", bg_color="on_grey")
146
+ cprint("On red color", bg_color="on_red")
147
+ cprint("On green color", bg_color="on_green")
148
+ cprint("On yellow color", bg_color="on_yellow")
149
+ cprint("On blue color", bg_color="on_blue")
150
+ cprint("On magenta color", bg_color="on_magenta")
151
+ cprint("On cyan color", bg_color="on_cyan")
152
+ cprint("On white color", color="grey", bg_color="on_white")
153
+ print("-" * 78)
154
+
155
+ print("Test attributes:")
156
+ cprint("Bold grey color", color="grey", styles="bold")
157
+ cprint("Dark red color", color="red", styles=["dark"])
158
+ cprint("Underline green color", color="green", styles=["underline"])
159
+ cprint("Blink yellow color", color="yellow", styles=["blink"])
160
+ cprint("Reversed blue color", color="blue", styles=["reverse"])
161
+ cprint("Concealed Magenta color", color="magenta", styles=["concealed"])
162
+ cprint(
163
+ "Bold underline reverse cyan color",
164
+ color="cyan",
165
+ styles=["bold", "underline", "reverse"],
166
+ )
167
+ cprint(
168
+ "Dark blink concealed white color",
169
+ color="white",
170
+ styles=["dark", "blink", "concealed"],
171
+ )
172
+ print(("-" * 78))
173
+
174
+ print("Test mixing:")
175
+ cprint(
176
+ "Underline red on grey color",
177
+ color="red",
178
+ bg_color="on_grey",
179
+ styles="underline",
180
+ )
181
+ cprint(
182
+ "Reversed green on red color",
183
+ color="green",
184
+ bg_color="on_red",
185
+ styles="reverse",
186
+ )
groot/vla/common/utils/misc/__init__.py ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ from .functional_utils import * # noqa: F403
2
+ from .image_utils import * # noqa: F403
3
+ from .misc_utils import * # noqa: F403
4
+ from .torch_utils import * # noqa: F403
5
+ from .video_utils import * # noqa: F403
groot/vla/common/utils/misc/array_tensor_utils.py ADDED
@@ -0,0 +1,372 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Functions that work on nested structures of torch.Tensor or numpy array
3
+ """
4
+
5
+ from typing import Any, Dict, List, Optional, Union
6
+
7
+ import numpy as np
8
+ import torch
9
+ import tree
10
+
11
+ from ..data_structure.tree_utils import (
12
+ copy_non_leaf,
13
+ is_sequence,
14
+ tree_assign_at_path,
15
+ tree_value_at_path,
16
+ )
17
+ from .functional_utils import make_recursive_func
18
+
19
+
20
+ def is_array_tensor(obj):
21
+ return isinstance(obj, (np.ndarray, torch.Tensor))
22
+
23
+
24
+ def is_numpy(obj):
25
+ return isinstance(obj, np.ndarray)
26
+
27
+
28
+ def is_tensor(obj):
29
+ return torch.is_tensor(obj)
30
+
31
+
32
+ def any_stack(xs: List, *, dim: int = 0):
33
+ """
34
+ Works for both torch Tensor and numpy array
35
+ """
36
+
37
+ def _any_stack_helper(*xs):
38
+ x = xs[0]
39
+ if isinstance(x, np.ndarray):
40
+ return np.stack(xs, axis=dim)
41
+ elif torch.is_tensor(x):
42
+ return torch.stack(xs, dim=dim)
43
+ elif isinstance(x, float):
44
+ # special treatment for float, defaults to float32
45
+ return np.array(xs, dtype=np.float32)
46
+ else:
47
+ return np.array(xs)
48
+
49
+ return tree.map_structure(_any_stack_helper, *xs)
50
+
51
+
52
+ def any_concat(xs: List, *, dim: int = 0):
53
+ """
54
+ Works for both torch Tensor and numpy array
55
+ """
56
+
57
+ def _any_concat_helper(*xs):
58
+ x = xs[0]
59
+ if isinstance(x, np.ndarray):
60
+ return np.concatenate(xs, axis=dim)
61
+ elif torch.is_tensor(x):
62
+ return torch.cat(xs, dim=dim)
63
+ elif isinstance(x, float):
64
+ # special treatment for float, defaults to float32
65
+ return np.array(xs, dtype=np.float32)
66
+ else:
67
+ return np.array(xs)
68
+
69
+ return tree.map_structure(_any_concat_helper, *xs)
70
+
71
+
72
+ def any_chunk(x, chunks: int, *, dim: int = 0, strict: bool = True) -> List[Any]:
73
+ """
74
+ Works for both torch Tensor and numpy array
75
+
76
+ Returns:
77
+ list of chunked nested structures
78
+ """
79
+ assert chunks >= 1
80
+
81
+ x_copies = [copy_non_leaf(x) for _ in range(chunks)]
82
+
83
+ def _any_chunk_helper(path, x):
84
+ if is_array_tensor(x):
85
+ if isinstance(x, np.ndarray):
86
+ chunked_values = np.split(x, chunks, axis=dim)
87
+ else:
88
+ chunked_values = torch.chunk(x, chunks, dim=dim)
89
+
90
+ if path:
91
+ for xc, chunked in zip(x_copies, chunked_values):
92
+ tree_assign_at_path(xc, path, chunked)
93
+ else: # top-level, no nested path
94
+ for i, chunked in enumerate(chunked_values):
95
+ x_copies[i] = chunked
96
+ else:
97
+ if strict:
98
+ raise NotImplementedError(f"Cannot chunk type {type(x)}")
99
+ else:
100
+ return
101
+
102
+ tree.map_structure_with_path(_any_chunk_helper, x)
103
+ return x_copies
104
+
105
+
106
+ def chunk_seq(arr, chunks: int, check_divide=True):
107
+ """
108
+ Args:
109
+ check_divide: True to force arr must divide n
110
+ """
111
+ k, m = divmod(len(arr), chunks)
112
+ if check_divide and m != 0:
113
+ raise ValueError(f"Array len {len(arr)} does not divide chunks {chunks}")
114
+ return (arr[i * k + min(i, m) : (i + 1) * k + min(i + 1, m)] for i in range(chunks))
115
+
116
+
117
+ @make_recursive_func
118
+ def any_zeros_like(x: Union[Dict, np.ndarray, torch.Tensor, int, float, np.number]):
119
+ """Returns a zero-filled object of the same (d)type and shape as the input.
120
+
121
+ The difference between this and `np.zeros_like()` is that this works well
122
+ with `np.number`, `int`, `float`, and `jax.numpy.DeviceArray` objects without
123
+ converting them to `np.ndarray`s.
124
+
125
+ Args:
126
+ x: The object to replace with 0s.
127
+
128
+ Returns:
129
+ A zero-filed object of the same (d)type and shape as the input.
130
+ """
131
+ if isinstance(x, (int, float, np.number)):
132
+ return type(x)(0)
133
+ elif is_tensor(x):
134
+ return torch.zeros_like(x)
135
+ elif is_numpy(x):
136
+ return np.zeros_like(x)
137
+ else:
138
+ raise ValueError(
139
+ f"Input ({type(x)}) must be either a numpy array, a tensor, an int, or a float."
140
+ )
141
+
142
+
143
+ @make_recursive_func
144
+ def any_ones_like(x: Union[Dict, np.ndarray, torch.Tensor, int, float, np.number]):
145
+ """Returns a one-filled object of the same (d)type and shape as the input.
146
+ The difference between this and `np.ones_like()` is that this works well
147
+ with `np.number`, `int`, `float`, and `jax.numpy.DeviceArray` objects without
148
+ converting them to `np.ndarray`s.
149
+ Args:
150
+ x: The object to replace with 1s.
151
+ Returns:
152
+ A one-filed object of the same (d)type and shape as the input.
153
+ """
154
+ if isinstance(x, (int, float, np.number)):
155
+ return type(x)(1)
156
+ elif is_tensor(x):
157
+ return torch.ones_like(x)
158
+ elif is_numpy(x):
159
+ return np.ones_like(x)
160
+ else:
161
+ raise ValueError(
162
+ f"Input ({type(x)}) must be either a numpy array, a tensor, an int, or a float."
163
+ )
164
+
165
+
166
+ @make_recursive_func
167
+ def any_zero_(x: Union[Dict, np.ndarray, torch.Tensor]):
168
+ """
169
+ Apply in-place zero-out to a tensor, i.e. x.zero_()
170
+ """
171
+ if is_tensor(x):
172
+ x.zero_()
173
+ elif is_numpy(x):
174
+ x.fill(0)
175
+ else:
176
+ raise ValueError(f"Input ({type(x)}) must be either a numpy array or a tensor")
177
+
178
+
179
+ @make_recursive_func
180
+ def any_fill_(x: Union[Dict, np.ndarray, torch.Tensor], value):
181
+ """
182
+ Apply in-place zero-out to a tensor, i.e. x.zero_()
183
+ """
184
+ if is_tensor(x):
185
+ x.fill_(value)
186
+ elif is_numpy(x):
187
+ x.fill(value)
188
+ else:
189
+ raise ValueError(f"Input ({type(x)}) must be either a numpy array or a tensor")
190
+
191
+
192
+ def get_batch_size(x, strict: bool = False) -> int:
193
+ """
194
+ Args:
195
+ x: can be any arbitrary nested structure of np array and torch tensor
196
+ strict: True to check all batch sizes are the same
197
+ """
198
+
199
+ def _get_batch_size(x):
200
+ if isinstance(x, np.ndarray):
201
+ return x.shape[0]
202
+ elif torch.is_tensor(x):
203
+ return x.size(0)
204
+ else:
205
+ return len(x)
206
+
207
+ xs = tree.flatten(x)
208
+
209
+ if strict:
210
+ batch_sizes = [_get_batch_size(x) for x in xs]
211
+ assert all(
212
+ b == batch_sizes[0] for b in batch_sizes
213
+ ), f"batch sizes must all be the same in nested structure: {batch_sizes}"
214
+ return batch_sizes[0]
215
+ else:
216
+ return _get_batch_size(xs[0])
217
+
218
+
219
+ @make_recursive_func
220
+ def add_batch_dim(x):
221
+ if is_numpy(x):
222
+ return np.expand_dims(x, axis=0)
223
+ elif is_tensor(x):
224
+ return x.unsqueeze(0)
225
+ else:
226
+ raise NotImplementedError(f"Unsupported data structure: {type(x)}")
227
+
228
+
229
+ @make_recursive_func
230
+ def remove_batch_dim(x):
231
+ if is_numpy(x):
232
+ return np.squeeze(x, axis=0)
233
+ elif is_tensor(x):
234
+ return x.squeeze(0)
235
+ else:
236
+ raise NotImplementedError(f"Unsupported data structure: {type(x)}")
237
+
238
+
239
+ @make_recursive_func
240
+ def any_to_primitive(x):
241
+ if isinstance(x, (np.ndarray, np.number, torch.Tensor)):
242
+ return x.tolist()
243
+ else:
244
+ return x
245
+
246
+
247
+ @make_recursive_func
248
+ def any_get_shape(x):
249
+ if is_numpy(x):
250
+ return tuple(x.shape)
251
+ elif is_tensor(x):
252
+ return tuple(x.size())
253
+ else:
254
+ raise NotImplementedError(f"Unsupported data structure: {type(x)}")
255
+
256
+
257
+ @make_recursive_func
258
+ def any_mean(x, dim: Optional[int] = None, keepdim: bool = False):
259
+ if is_numpy(x):
260
+ return np.mean(x, axis=dim, keepdims=keepdim)
261
+ elif is_tensor(x):
262
+ return torch.mean(x, dim=dim, keepdim=keepdim)
263
+ else:
264
+ raise NotImplementedError(f"Unsupported data structure: {type(x)}")
265
+
266
+
267
+ @make_recursive_func
268
+ def any_variance(x, dim: Optional[int] = None, keepdim: bool = False, unbiased: bool = False):
269
+ if is_numpy(x):
270
+ return np.var(x, axis=dim, keepdims=keepdim, ddof=1 if unbiased else 0)
271
+ elif is_tensor(x):
272
+ return torch.var(x, dim=dim, keepdim=keepdim, unbiased=unbiased)
273
+ else:
274
+ raise NotImplementedError(f"Unsupported data structure: {type(x)}")
275
+
276
+
277
+ @make_recursive_func
278
+ def any_describe_str(x, shape_only=False):
279
+ """
280
+ Describe type, shape, device, data type (of np array/tensor)
281
+ Very useful for debugging
282
+ """
283
+ t = type(x)
284
+ tname = type(x).__name__
285
+ if is_numpy(x):
286
+ shape = list(x.shape)
287
+ if x.size == 1:
288
+ if shape_only:
289
+ return f"np scalar: {x.item()} {shape}"
290
+ else:
291
+ return f"np scalar: {x.item()} {shape} {x.dtype}"
292
+ else:
293
+ if shape_only:
294
+ return f"np: {shape}"
295
+ else:
296
+ return f"np: {shape} {x.dtype}"
297
+ elif is_tensor(x):
298
+ shape = list(x.size())
299
+ if x.numel() == 1:
300
+ if shape_only:
301
+ return f"torch scalar: {x.item()} {shape}"
302
+ else:
303
+ return f"torch scalar: {x.item()} {shape} {x.dtype} {x.device}"
304
+ else:
305
+ if shape_only:
306
+ return f"torch: {shape}"
307
+ else:
308
+ return f"torch: {shape} {x.dtype} {x.device}"
309
+ elif is_sequence(x):
310
+ return f"{tname}[{len(x)}]"
311
+ elif isinstance(x, str):
312
+ return x
313
+ elif x is None:
314
+ return "None"
315
+ elif np.issubdtype(t, np.number) or np.issubdtype(t, np.bool_):
316
+ return f"{tname}: {x}"
317
+ else:
318
+ return f"{tname}"
319
+
320
+
321
+ def any_describe(x, msg="", *, shape_only=False):
322
+ # from omlet.utils import yaml_dumps
323
+ from pprint import pprint
324
+
325
+ if isinstance(x, str) and msg != "":
326
+ x, msg = msg, x
327
+
328
+ if msg:
329
+ msg += ": "
330
+ print(msg, end="")
331
+ pprint(any_describe_str(x, shape_only=shape_only))
332
+
333
+
334
+ @make_recursive_func
335
+ def any_slice(x, slice):
336
+ """
337
+ Args:
338
+ slice: you can use np.s_[...] to return the slice object
339
+ """
340
+ if is_array_tensor(x):
341
+ return x[slice]
342
+ else:
343
+ return x
344
+
345
+
346
+ def any_assign(x, assign_value, slice):
347
+ """
348
+ Recursive version of x[slice] = assign_value
349
+ If structures of x and assign_value do not match, we will respect `assign_value`
350
+ E.g. x = {'a': ..., 'b': ...}, assign_value = {'a': ...}, then 'b' will not change
351
+
352
+ Use np.s_[...] to get advanced slicing
353
+ """
354
+
355
+ def _any_assign_helper(path, v):
356
+ y = tree_value_at_path(x, path)
357
+ y[slice] = v
358
+
359
+ tree.map_structure_with_path(_any_assign_helper, assign_value)
360
+
361
+
362
+ @make_recursive_func
363
+ def any_transpose_first_two_axes(x):
364
+ """
365
+ util to convert between (L, B, ...) and (B, L, ...)
366
+ """
367
+ if is_numpy(x):
368
+ return np.swapaxes(x, 0, 1)
369
+ elif is_tensor(x):
370
+ return torch.swapaxes(x, 0, 1)
371
+ else:
372
+ raise ValueError(f"Input ({type(x)}) must be either a numpy array or a tensor.")
groot/vla/common/utils/misc/functional_utils.py ADDED
@@ -0,0 +1,635 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Inspect, meta, etc.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import functools
8
+ import inspect
9
+ import pprint
10
+ import sys
11
+ import types
12
+ from typing import Any, Dict, Literal
13
+ import warnings
14
+
15
+ from ..data_structure.tree_utils import is_mapping, is_sequence
16
+
17
+
18
+ def state_dict_class(keys: list[str]):
19
+ """
20
+ Just like pytorch nn.Module
21
+ Add the following methods to the class:
22
+ state_dict() -> dict of attribute keys
23
+ load_state_dict(sdict) restore states
24
+ """
25
+
26
+ def _wrap_class(cls):
27
+ assert inspect.isclass(cls)
28
+
29
+ def state_dict(self):
30
+ return {k: getattr(self, k) for k in keys}
31
+
32
+ def load_state_dict(self, states: Dict[str, Any]):
33
+ if not set(keys).issubset(set(states.keys())):
34
+ raise ValueError(f"states does not have all the required keys: {keys}")
35
+ for k in keys:
36
+ setattr(self, k, states[k])
37
+
38
+ @property
39
+ def state_keys(self):
40
+ return keys
41
+
42
+ cls.state_dict = state_dict
43
+ cls.load_state_dict = load_state_dict
44
+ cls.state_keys = state_keys
45
+ return cls
46
+
47
+ return _wrap_class
48
+
49
+
50
+ def implements_method(object, method: str):
51
+ """
52
+ Returns:
53
+ True if object implements a method
54
+ """
55
+ return hasattr(object, method) and callable(getattr(object, method))
56
+
57
+
58
+ def assert_implements_method(object, method: str | list[str]):
59
+ if isinstance(method, str):
60
+ method = [method]
61
+ for m in method:
62
+ assert implements_method(object, m), (
63
+ f"object {object.__class__} does not " f"implement method {m}()"
64
+ )
65
+
66
+
67
+ def meta_decorator(decor):
68
+ """
69
+ a decorator, allowing the wrapped decorator to be used as:
70
+ @decorator(*args, **kwargs)
71
+ def callable()
72
+ -- or --
73
+ @decorator # without parenthesis, args and kwargs will use default
74
+ def callable()
75
+
76
+ Args:
77
+ decor: a decorator whose first argument is a callable (function or class
78
+ to be decorated), and the rest of the arguments can be omitted as default.
79
+ decor(f, ... the other arguments must have default values)
80
+
81
+ Warning:
82
+ decor can NOT be a function that receives a single, callable argument.
83
+ See stackoverflow: http://goo.gl/UEYbDB
84
+ """
85
+ import functools
86
+
87
+ def single_callable(args, kwargs):
88
+ return len(args) == 1 and len(kwargs) == 0 and callable(args[0])
89
+
90
+ @functools.wraps(decor)
91
+ def new_decor(*args, **kwargs):
92
+ if single_callable(args, kwargs):
93
+ # this is the double-decorated f.
94
+ # It should not run on a single callable.
95
+ return decor(args[0])
96
+ else:
97
+ # decorator arguments
98
+ return lambda real_f: decor(real_f, *args, **kwargs)
99
+
100
+ return new_decor
101
+
102
+
103
+ @meta_decorator
104
+ def make_recursive_func(fn, *, with_path=False):
105
+ """
106
+ Decorator that turns a function that works on a single array/tensor to working on
107
+ arbitrary nested structures.
108
+ """
109
+ import functools
110
+
111
+ import tree
112
+
113
+ @functools.wraps(fn)
114
+ def _wrapper(tensor_struct, *args, **kwargs):
115
+ if with_path:
116
+ return tree.map_structure_with_path(
117
+ lambda paths, x: fn(paths, x, *args, **kwargs), tensor_struct
118
+ )
119
+ else:
120
+ return tree.map_structure(lambda x: fn(x, *args, **kwargs), tensor_struct)
121
+
122
+ return _wrapper
123
+
124
+
125
+ @meta_decorator
126
+ def deprecated(func, msg="", action="warning", type=""):
127
+ """
128
+ Function/class decorator: designate deprecation.
129
+
130
+ Args:
131
+ msg: string message.
132
+ action: string mode
133
+ - 'warning': (default) prints `msg` to stderr
134
+ - 'noop': do nothing, just for source code annotation purposes
135
+ - 'raise': raise DeprecatedError(`msg`)
136
+ """
137
+ action = action.lower()
138
+ type = type.lower()
139
+ ALL_ACTIONS = ["warn", "warning", "noop", "raise"]
140
+ if action not in ALL_ACTIONS:
141
+ raise ValueError(f"Unknown action {action}. Choose from {ALL_ACTIONS}.")
142
+ ALL_TYPES = {
143
+ "": DeprecationWarning,
144
+ "pending": PendingDeprecationWarning,
145
+ "future": FutureWarning,
146
+ }
147
+ if type not in ALL_TYPES:
148
+ raise ValueError(f"Unknown type {type}. Choose from {ALL_TYPES.keys()}.")
149
+ if not msg:
150
+ msg = "This is a deprecated feature."
151
+
152
+ WarningExceptionCls = ALL_TYPES[type]
153
+
154
+ # only does the deprecation when being called
155
+ @functools.wraps(func)
156
+ def _deprecated(*args, **kwargs):
157
+ if action in ["warning", "warn"]:
158
+ warnings.warn(msg, WarningExceptionCls)
159
+ elif action == "raise":
160
+ raise WarningExceptionCls(msg)
161
+ return func(*args, **kwargs)
162
+
163
+ return _deprecated
164
+
165
+
166
+ @meta_decorator
167
+ def call_once(func, on_second_call: Literal["noop", "raise", "warn"] = "noop"):
168
+ """
169
+ Decorator to ensure that a function is only called once.
170
+
171
+ Args:
172
+ on_second_call (str): what happens when the function is called a second time.
173
+ """
174
+ assert on_second_call in [
175
+ "noop",
176
+ "raise",
177
+ "warn",
178
+ ], "mode must be one of 'noop', 'raise', 'warn'"
179
+
180
+ @functools.wraps(func)
181
+ def wrapper(*args, **kwargs):
182
+ if wrapper._called:
183
+ if on_second_call == "raise":
184
+ raise RuntimeError(f"{func.__name__} has already been called. Can only call once.")
185
+ elif on_second_call == "warn":
186
+ warnings.warn(f"{func.__name__} has already been called. Should only call once.")
187
+ else:
188
+ wrapper._called = True
189
+ return func(*args, **kwargs)
190
+
191
+ wrapper._called = False
192
+ return wrapper
193
+
194
+
195
+ class NoopObject:
196
+ """
197
+ Object that does nothing when called any method
198
+ """
199
+
200
+ def __init__(self, *args, **kwargs):
201
+ self.init_args = args
202
+ self.init_kwargs = kwargs
203
+
204
+ def __getattr__(self, name):
205
+ def _func(*args, **kwargs):
206
+ pass
207
+
208
+ return _func
209
+
210
+
211
+ class NoopContext:
212
+ """
213
+ Placeholder context manager that does nothing.
214
+ We could have written simply as:
215
+
216
+ @contextmanager
217
+ def noop_context(*args, **kwargs):
218
+ yield
219
+
220
+ but the returned context manager cannot be called twice, i.e.
221
+ my_noop = NoopContext()
222
+ with my_noop:
223
+ do1()
224
+ with my_noop: # trigger generator error
225
+ do2()
226
+ """
227
+
228
+ def __init__(self, *args, **kwargs):
229
+ self.args = args
230
+ self.kwargs = kwargs
231
+
232
+ def __enter__(self):
233
+ return self
234
+
235
+ def __exit__(self, exc_type, exc_val, exc_tb):
236
+ pass
237
+
238
+
239
+ def make_registry_metaclass(class_name):
240
+ """
241
+ Usage:
242
+
243
+ TrainerRegistry = make_registry_metaclass('TrainerRegistry')
244
+
245
+ class BaseTrainer(metaclass=TrainerRegistry):
246
+ pass
247
+
248
+ class MyTrainer(BaseTrainer):
249
+ pass
250
+
251
+ TrainerRegistry['MyTrainer'] -> MyTrainer class # syntax enabled by metaclass
252
+ TrainerRegistry.get_class('MyTrainer') # same as above
253
+ TrainerRegistry.registry -> full dict of {name: trainer_class}
254
+
255
+ Templated definition:
256
+ class TrainerRegistry(type):
257
+ registry = {}
258
+
259
+ def __new__(cls, name, bases, attr):
260
+ new_cls = super().__new__(cls, name, bases, attr)
261
+ TrainerRegistry.registry[name] = new_cls
262
+ return new_cls
263
+
264
+ def get_class(cls, name):
265
+ if name not in cls.registry:
266
+ raise KeyError(
267
+ f"Trainer class {name} not found in registry: "
268
+ f"{pprint.pformat(cls.registry)}"
269
+ )
270
+ return cls.registry[name]"""
271
+
272
+ def new__(cls, name, bases, attr):
273
+ """
274
+ Change the attr dict to dynamically add methods and attributes
275
+ """
276
+ new_cls = type.__new__(cls, name, bases, attr)
277
+ cls.registry[name] = new_cls
278
+ return new_cls
279
+
280
+ def get_class(cls, name):
281
+ if name not in cls.registry:
282
+ existing_cls = list(cls.registry.keys())
283
+ raise KeyError(f"{class_name} class '{name}' not found in registry: {existing_cls}")
284
+ return cls.registry[name]
285
+
286
+ def instantiate(cls_, cls, **kwargs):
287
+ Cls = cls_.get_class(cls)
288
+ return Cls(**kwargs)
289
+
290
+ class _BracketOperator(type):
291
+ def __getitem__(cls, name):
292
+ return get_class(cls, name)
293
+
294
+ return types.new_class(
295
+ class_name,
296
+ bases=(type,),
297
+ kwds={"metaclass": _BracketOperator},
298
+ exec_body=lambda ns: ns.update(
299
+ {
300
+ "registry": {},
301
+ "__new__": new__,
302
+ "get_class": classmethod(get_class),
303
+ "instantiate": classmethod(instantiate),
304
+ }
305
+ ),
306
+ )
307
+
308
+
309
+ class ClassRegistry:
310
+ """
311
+ May be a preferred way over make_registry_metaclass if your code does not support
312
+ metaclass well, e.g. pickle or Ray
313
+
314
+ Use in conjunction with `__init_subclass__` hook in your base class
315
+
316
+ class BaseClass:
317
+ registry = ClassRegistry()
318
+
319
+ def __init_subclass__(cls, **kwargs):
320
+ cls.registry.add(cls)
321
+ super().__init_subclass__(**kwargs)
322
+
323
+ print(registry)
324
+ """
325
+
326
+ def __init__(self, base_class_name: str = None):
327
+ self.registry = {}
328
+ self._base_class_name = base_class_name
329
+
330
+ def add(self, cls):
331
+ self.registry[cls.__name__] = cls
332
+
333
+ def get(self, name):
334
+ if name not in self.registry:
335
+ existing_cls = list(self.registry.keys())
336
+ base_name = self._base_class_name + " " if self._base_class_name else ""
337
+ raise KeyError(f"{base_name} subclass '{name}' not found in registry: {existing_cls}")
338
+ return self.registry[name]
339
+
340
+ def __str__(self):
341
+ return pprint.pformat(self.registry)
342
+
343
+ def __getitem__(self, name):
344
+ return self.get(name)
345
+
346
+ def instantiate(self, cls, **kwargs):
347
+ return self.get(cls)(**kwargs)
348
+
349
+
350
+ # ========================================================
351
+ # =================== Inspect utils ====================
352
+ # ========================================================
353
+
354
+
355
+ def func_parameters(func):
356
+ return inspect.signature(func).parameters
357
+
358
+
359
+ def func_has_arg(func, arg_name):
360
+ return arg_name in func_parameters(func)
361
+
362
+
363
+ def pack_varargs(args):
364
+ """
365
+ Pack *args or a single list arg as list
366
+
367
+ def f(*args):
368
+ arg_list = pack_varargs(args)
369
+ # arg_list is now packed as a list
370
+ """
371
+ assert isinstance(args, tuple), "please input the tuple `args` as in *args"
372
+ if len(args) == 1 and is_sequence(args[0]):
373
+ return args[0]
374
+ else:
375
+ return args
376
+
377
+
378
+ def enable_list_arg(func):
379
+ """
380
+ Function decorator.
381
+ If a function only accepts varargs (*args),
382
+ make it support a single list arg as well
383
+ """
384
+
385
+ @functools.wraps(func)
386
+ def wrapper(*args, **kwargs):
387
+ args = pack_varargs(args)
388
+ return func(*args, **kwargs)
389
+
390
+ return wrapper
391
+
392
+
393
+ def enable_varargs(func):
394
+ """
395
+ Function decorator.
396
+ If a function only accepts a list arg, make it support varargs as well
397
+ """
398
+
399
+ @functools.wraps(func)
400
+ def wrapper(*args, **kwargs):
401
+ args = pack_varargs(args)
402
+ return func(args, **kwargs)
403
+
404
+ return wrapper
405
+
406
+
407
+ def pack_kwargs(args, kwargs):
408
+ """
409
+ Pack **kwargs or a single dict arg as dict
410
+
411
+ def f(*args, **kwargs):
412
+ kwdict = pack_kwargs(args, kwargs)
413
+ # kwdict is now packed as a dict
414
+ """
415
+ if len(args) == 1 and is_mapping(args[0]):
416
+ assert not kwargs, "cannot have both **kwargs and a dict arg"
417
+ return args[0] # single-dict
418
+ else:
419
+ assert not args, "cannot have positional args if **kwargs exist"
420
+ return kwargs
421
+
422
+
423
+ def merge_kwargs(args, kwargs) -> Dict:
424
+ """
425
+ Merge all dicts in `args` and keywords in kwargs.
426
+
427
+ E.g. merge_kwargs({"a.b": 1, "a.c": 2}, foo=6, bar=8)
428
+ -> {"a.b": 1, "a.c": 2, "foo": 6, "bar": 8}
429
+ """
430
+ kw_all = {}
431
+ for arg in args:
432
+ assert is_mapping(arg), f"{arg} is not a dict."
433
+ kw_all.update(arg)
434
+ kw_all.update(kwargs)
435
+ return kw_all
436
+
437
+
438
+ def enable_dict_arg(func):
439
+ """
440
+ Function decorator.
441
+ If a function only accepts varargs (*args),
442
+ make it support a single list arg as well
443
+ """
444
+
445
+ @functools.wraps(func)
446
+ def wrapper(*args, **kwargs):
447
+ kwargs = pack_kwargs(args, kwargs)
448
+ return func(**kwargs)
449
+
450
+ return wrapper
451
+
452
+
453
+ def enable_kwargs(func):
454
+ """
455
+ Function decorator.
456
+ If a function only accepts a dict arg, make it support kwargs as well
457
+ """
458
+
459
+ @functools.wraps(func)
460
+ def wrapper(*args, **kwargs):
461
+ kwargs = pack_kwargs(args, kwargs)
462
+ return func(kwargs)
463
+
464
+ return wrapper
465
+
466
+
467
+ def has_keys(D, keys: list):
468
+ assert is_mapping(D)
469
+ return all(key in D for key in keys)
470
+
471
+
472
+ def assert_has_keys(D, keys: list):
473
+ assert is_mapping(D), "Input is not a dict"
474
+ for key in keys:
475
+ if key not in D:
476
+ raise KeyError(f'Required key "{key}" is missing in dict {D}')
477
+ return True
478
+
479
+
480
+ def method_decorator(decorator):
481
+ """
482
+ Decorator of decorator: transform a decorator that only works on normal
483
+ functions to a decorator that works on class methods
484
+ From Django form: https://goo.gl/XLjxKK
485
+ """
486
+
487
+ @functools.wraps(decorator)
488
+ def wrapped_decorator(method):
489
+ @functools.wraps(method)
490
+ def wrapper(self, *args, **kwargs):
491
+ def bound_func(*args2, **kwargs2):
492
+ return method(self, *args2, **kwargs2)
493
+
494
+ return decorator(bound_func)(*args, **kwargs)
495
+
496
+ return wrapper
497
+
498
+ return wrapped_decorator
499
+
500
+
501
+ def accepts_varargs(func):
502
+ """
503
+ If a function accepts *args
504
+ """
505
+ params = inspect.signature(func).parameters
506
+ return any(param.kind == inspect.Parameter.VAR_POSITIONAL for param in params.values())
507
+
508
+
509
+ def accepts_kwargs(func):
510
+ """
511
+ If a function accepts **kwargs
512
+ """
513
+ params = inspect.signature(func).parameters
514
+ return any(param.kind == inspect.Parameter.VAR_KEYWORD for param in params.values())
515
+
516
+
517
+ def is_signature_compatible(func, *args, **kwargs):
518
+ sig = inspect.signature(func)
519
+ try:
520
+ sig.bind(*args, **kwargs)
521
+ return True
522
+ except TypeError:
523
+ return False
524
+
525
+
526
+ def make_list(x):
527
+ """
528
+ Turns a singleton object to a list. If already a list, no change.
529
+ """
530
+ if is_sequence(x):
531
+ return x
532
+ else:
533
+ return [x]
534
+
535
+
536
+ def make_tuple(elem, repeats):
537
+ """
538
+ E.g. expand a singleton x into (x, x, x)
539
+ useful for things like image_size or kernal, which can be a single int/float
540
+ or a tuple of fixed size
541
+ """
542
+ if is_sequence(elem):
543
+ assert len(elem) == repeats, f"length of input must be {repeats}: {elem}"
544
+ return elem
545
+ else:
546
+ return (elem,) * repeats
547
+
548
+
549
+ def accumulate(iterable, fn=lambda x, y: x + y):
550
+ """
551
+ Return running totals
552
+ # _accumulate([1,2,3,4,5]) --> 1 3 6 10 15
553
+ # _accumulate([1,2,3,4,5], operator.mul) --> 1 2 6 24 120
554
+ """
555
+ it = iter(iterable)
556
+ try:
557
+ total = next(it)
558
+ except StopIteration:
559
+ return
560
+ yield total
561
+ for element in it:
562
+ total = fn(total, element)
563
+ yield total
564
+
565
+
566
+ class DecoratorContextManager:
567
+ """
568
+ Allow a context manager to be used as a decorator
569
+ From torch.auto_grad.grad_mode
570
+ """
571
+
572
+ def __call__(self, func):
573
+ if inspect.isgeneratorfunction(func):
574
+ return self._wrap_generator(func)
575
+
576
+ @functools.wraps(func)
577
+ def decorate_context(*args, **kwargs):
578
+ with self.__class__():
579
+ return func(*args, **kwargs)
580
+
581
+ return decorate_context
582
+
583
+ def _wrap_generator(self, func):
584
+ """Wrap each generator invocation with the context manager"""
585
+
586
+ @functools.wraps(func)
587
+ def generator_context(*args, **kwargs):
588
+ gen = func(*args, **kwargs)
589
+
590
+ # Generators are suspended and unsuspended at `yield`, hence we
591
+ # make sure the grad mode is properly set every time the execution
592
+ # flow returns into the wrapped generator and restored when it
593
+ # returns through our `yield` to our caller (see PR #49017).
594
+ cls = type(self)
595
+ try:
596
+ # Issuing `None` to a generator fires it up
597
+ with cls():
598
+ response = gen.send(None)
599
+
600
+ while True:
601
+ try:
602
+ # Forward the response to our caller and get its next request
603
+ request = yield response
604
+
605
+ except GeneratorExit:
606
+ # Inform the still active generator about its imminent closure
607
+ with cls():
608
+ gen.close()
609
+ raise
610
+
611
+ except BaseException:
612
+ # Propagate the exception thrown at us by the caller
613
+ with cls():
614
+ response = gen.throw(*sys.exc_info())
615
+
616
+ else:
617
+ # Pass the last request to the generator and get its response
618
+ with cls():
619
+ response = gen.send(request)
620
+
621
+ # We let the exceptions raised above by the generator's `.throw` or
622
+ # `.send` methods bubble up to our caller, except for StopIteration
623
+ except StopIteration as e:
624
+ # The generator informed us that it is done: take whatever its
625
+ # returned value (if any) was and indicate that we're done too
626
+ # by returning it (see docs for python's return-statement).
627
+ return e.value
628
+
629
+ return generator_context
630
+
631
+ def __enter__(self) -> None:
632
+ raise NotImplementedError
633
+
634
+ def __exit__(self, exc_type: Any, exc_value: Any, traceback: Any) -> None:
635
+ raise NotImplementedError
groot/vla/common/utils/misc/image_utils.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Visualizations
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ import os
8
+ import time
9
+ from typing import Literal
10
+ import warnings
11
+
12
+ import cv2
13
+ import imageio
14
+ import matplotlib.pyplot as plt
15
+ import numpy as np
16
+ import torch
17
+
18
+ from .array_tensor_utils import any_describe
19
+ from .misc_utils import global_once
20
+ from .torch_utils import torch_normalize
21
+
22
+
23
+ def to_image(img, channel_order="auto"):
24
+ """
25
+ Returns:
26
+ numpy image of shape [H, W, C]
27
+ in "auto" mode, we assume C == 3
28
+ """
29
+ assert channel_order in ["hwc", "chw", "auto"]
30
+ if torch.is_tensor(img):
31
+ img = img.cpu().numpy()
32
+ assert isinstance(img, np.ndarray)
33
+ if img.ndim == 4:
34
+ assert img.shape[0] == 1
35
+ img = img[0]
36
+ assert img.ndim == 3
37
+ if channel_order == "auto":
38
+ # use C==3 to detect order
39
+ if img.shape[0] == 3:
40
+ channel_order = "chw"
41
+ else:
42
+ assert img.shape[-1] == 3, "image should either have [3,H,W] or [H,W,3]"
43
+ channel_order = "hwc"
44
+ img = img.astype(np.uint8)
45
+ if channel_order == "chw":
46
+ return np.transpose(img, (1, 2, 0))
47
+ else:
48
+ return img
49
+
50
+
51
+ def imshow(img):
52
+ plt.imshow(to_image(img))
53
+
54
+
55
+ def imsave(img, path):
56
+ imageio.imsave(os.path.expanduser(path), to_image(img))
57
+
58
+
59
+ def imread(path, channel_order="chw", format="torch"):
60
+ assert channel_order in ["hwc", "chw"]
61
+ assert format in ["numpy", "torch"]
62
+ img = imageio.imread(path)
63
+ if channel_order == "chw":
64
+ img = np.transpose(img, (2, 0, 1)) # hwc -> chw
65
+ if format == "torch":
66
+ return torch.from_numpy(img)
67
+ else:
68
+ return img
69
+
70
+
71
+ class Cv2Display:
72
+ def __init__(
73
+ self,
74
+ window_name="display",
75
+ image_size=None,
76
+ channel_order="auto",
77
+ bgr2rgb=True,
78
+ step_sleep=0,
79
+ enabled=True,
80
+ ):
81
+ """
82
+ Use cv2.imshow() to pop a window, requires virtual desktop GUI
83
+
84
+ Args:
85
+ channel_order: auto, hwc, or chw
86
+ image_size: None to use the original image size, otherwise resize
87
+ step_sleep: sleep for a few seconds
88
+ """
89
+ self._window_name = window_name
90
+ if isinstance(image_size, int):
91
+ image_size = (image_size, image_size)
92
+ else:
93
+ assert image_size is None or len(image_size) == 2
94
+ self._image_size = image_size
95
+ assert channel_order in ["auto", "chw", "hwc"]
96
+ self._channel_order = channel_order
97
+ self._bgr2rgb = bgr2rgb
98
+ self._step_sleep = step_sleep
99
+ self._enabled = enabled
100
+
101
+ def _resize(self, img):
102
+ if self._image_size is None:
103
+ return img
104
+ H, W = img.shape[:2]
105
+ Ht, Wt = self._image_size # target
106
+ return cv2.resize(
107
+ img,
108
+ self._image_size,
109
+ interpolation=cv2.INTER_AREA if Ht < H else cv2.INTER_LINEAR,
110
+ )
111
+
112
+ def _reorder(self, img):
113
+ if self._channel_order == "chw":
114
+ return np.transpose(img, (1, 2, 0))
115
+ elif self._channel_order == "hwc":
116
+ return img
117
+ else:
118
+ if img.shape[0] in [1, 3]: # chw
119
+ return np.transpose(img, (1, 2, 0))
120
+ else:
121
+ return img
122
+
123
+ def __call__(self, img):
124
+ if not self._enabled:
125
+ return
126
+ import torch
127
+
128
+ # prevent segfault in IsaacGym
129
+ display_var = os.environ.get("DISPLAY", None)
130
+ if not display_var:
131
+ os.environ["DISPLAY"] = ":0.0"
132
+
133
+ if torch.is_tensor(img):
134
+ img = img.detach().cpu().numpy()
135
+
136
+ img = self._resize(self._reorder(img))
137
+ if self._bgr2rgb:
138
+ img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
139
+ time.sleep(self._step_sleep)
140
+ cv2.imshow(self._window_name, img)
141
+ cv2.waitKey(1)
142
+
143
+ if display_var is not None:
144
+ os.environ["DISPLAY"] = display_var
145
+
146
+ def close(self):
147
+ if not self._enabled:
148
+ return
149
+ cv2.destroyWindow(self._window_name)
150
+
151
+
152
+ # ---------------- Image tensor handling -----------------
153
+ def sanity_check_image_tensor(
154
+ img: torch.Tensor, on_error: Literal["raise", "warn", "ignore"] = "raise"
155
+ ):
156
+ """
157
+ Check if the input image tensor is all integers, which is wrong for any NN input.
158
+ This is a common case if the user forgets to normalize the image first
159
+ """
160
+ assert on_error in [
161
+ "raise",
162
+ "warn",
163
+ "ignore",
164
+ ], 'on_error must be "raise", "warn", or "ignore"'
165
+ if not img.dtype.is_floating_point:
166
+ msg = f"Image tensor is not floating point format, but {img.dtype}!"
167
+ if on_error == "raise":
168
+ raise ValueError(msg)
169
+ elif on_error == "warn":
170
+ warnings.warn(msg)
171
+ else:
172
+ return False
173
+ # check if all values in the image are close to an integer
174
+ if (img - torch.round(img)).abs().max() < 1e-5:
175
+ msg = (
176
+ "Input image is all close to integers, "
177
+ "are you sure you have normalized it before passing it to a NN?"
178
+ )
179
+ if on_error == "raise":
180
+ raise ValueError(msg)
181
+ elif on_error == "warn":
182
+ warnings.warn(msg)
183
+ else:
184
+ return False
185
+ return True
186
+
187
+
188
+ @torch.no_grad()
189
+ def basic_image_tensor_preprocess(
190
+ img,
191
+ mean: tuple[float, float, float] = (0.5, 0.5, 0.5),
192
+ std: tuple[float, float, float] = (0.5, 0.5, 0.5),
193
+ shape: tuple[int, int] | None = None,
194
+ ):
195
+ """
196
+ Check for resize, and divide by 255
197
+ """
198
+ import kornia
199
+
200
+ assert torch.is_tensor(img)
201
+ assert img.dim() >= 4, any_describe(img)
202
+ original_shape = list(img.size())
203
+ img = img.float()
204
+ img = img.flatten(0, img.dim() - 4)
205
+ assert img.dim() == 4
206
+
207
+ input_size = img.size()[-2:]
208
+ if global_once("groot.vla.common.utils.image_utils.basic_image_preprocess:input_size"):
209
+ assert img.max() > 2, "img should be between [0, 255] before normalize"
210
+
211
+ if shape and input_size != shape:
212
+ if global_once("groot.vla.common.utils.image_utils.basic_image_preprocess:transform"):
213
+ warnings.warn(
214
+ f'{"Down" if shape < input_size else "Up"}sampling image'
215
+ f" from original resolution {input_size}x{input_size}"
216
+ f" to {shape}x{shape}"
217
+ )
218
+ img = kornia.geometry.transform.resize(img, shape).clamp(0.0, 255.0)
219
+
220
+ B, C, H, W = img.size()
221
+ assert C % 3 == 0, "channel must divide 3"
222
+ img = img.view(B * C // 3, 3, H, W)
223
+ img = torch_normalize(img / 255.0, mean=mean, std=std)
224
+ original_shape[-2:] = H, W
225
+ return img.view(original_shape)
groot/vla/common/utils/misc/misc_utils.py ADDED
@@ -0,0 +1,261 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import codecs
2
+ from collections import Counter
3
+ import fnmatch
4
+ import hashlib
5
+ import os
6
+ import pickle
7
+ from typing import Any, Callable, Dict, List, Optional, Union
8
+
9
+ from typing_extensions import Literal
10
+
11
+
12
+ def set_os_envs(envs: Optional[Dict[str, Any]] = None):
13
+ """
14
+ Special value __delete__ or None indicates that the ENV_VAR should be removed
15
+ """
16
+ if envs is None:
17
+ envs = {}
18
+ DEL = {None, "__delete__"}
19
+ for k, v in envs.items():
20
+ if v in DEL:
21
+ os.environ.pop(k, None)
22
+ os.environ.update({k: str(v) for k, v in envs.items() if v not in DEL})
23
+
24
+
25
+ def argmax(L):
26
+ return max(zip(L, range(len(L))))[1]
27
+
28
+
29
+ def _match_patterns_helper(element, patterns):
30
+ for p in patterns:
31
+ if callable(p) and p(element):
32
+ return True
33
+ if fnmatch.fnmatch(element, p):
34
+ return True
35
+ return False
36
+
37
+
38
+ def match_patterns(
39
+ item: str,
40
+ include: Union[str, List[str], Callable, List[Callable], None] = None,
41
+ exclude: Union[str, List[str], Callable, List[Callable], None] = None,
42
+ *,
43
+ precedence: Literal["include", "exclude"] = "exclude",
44
+ ):
45
+ """
46
+ Args:
47
+ include: None to disable `include` filter and delegate to exclude
48
+ precedence: "include" or "exclude"
49
+ """
50
+ assert precedence in ["include", "exclude"]
51
+ if exclude is None:
52
+ exclude = []
53
+ if isinstance(exclude, (str, Callable)):
54
+ exclude = [exclude]
55
+ if isinstance(include, (str, Callable)):
56
+ include = [include]
57
+ if include is None:
58
+ # exclude is the sole veto vote
59
+ return not _match_patterns_helper(item, exclude)
60
+
61
+ if precedence == "include":
62
+ return _match_patterns_helper(item, include)
63
+ else:
64
+ if _match_patterns_helper(item, exclude):
65
+ return False
66
+ else:
67
+ return _match_patterns_helper(item, include)
68
+
69
+
70
+ def filter_patterns(
71
+ items: List[str],
72
+ include: Union[str, List[str], Callable, List[Callable], None] = None,
73
+ exclude: Union[str, List[str], Callable, List[Callable], None] = None,
74
+ *,
75
+ precedence: Literal["include", "exclude"] = "exclude",
76
+ ordering: Literal["original", "include"] = "original",
77
+ ):
78
+ """
79
+ Args:
80
+ ordering: affects the order of items in the returned list. Does not affect the
81
+ content of the returned list.
82
+ - "original": keep the ordering of items in the input list
83
+ - "include": order items by the order of include patterns
84
+ """
85
+ assert ordering in ["original", "include"]
86
+ if include is None or isinstance(include, str) or ordering == "original":
87
+ return [
88
+ x
89
+ for x in items
90
+ if match_patterns(x, include=include, exclude=exclude, precedence=precedence)
91
+ ]
92
+ else:
93
+ items = items.copy()
94
+ ret = []
95
+ for inc in include:
96
+ for i, item in enumerate(items):
97
+ if item is None:
98
+ continue
99
+ if match_patterns(item, include=inc, exclude=exclude, precedence=precedence):
100
+ ret.append(item)
101
+ items[i] = None
102
+ return ret
103
+
104
+
105
+ def getitem_nested(cfg, key: str):
106
+ """
107
+ Recursively get key, if key has '.' in it
108
+ """
109
+ keys = key.split(".")
110
+ for k in keys:
111
+ assert k in cfg, f'{k} in key "{key}" does not exist in config'
112
+ cfg = cfg[k]
113
+ return cfg
114
+
115
+
116
+ def setitem_nested(cfg, key: str, value):
117
+ """
118
+ Recursively get key, if key has '.' in it
119
+ """
120
+ keys = key.split(".")
121
+ for k in keys[:-1]:
122
+ assert k in cfg, f'{k} in key "{key}" does not exist in config'
123
+ cfg = cfg[k]
124
+ cfg[keys[-1]] = value
125
+
126
+
127
+ def getattr_nested(obj, key: str):
128
+ """
129
+ Recursively get attribute
130
+ """
131
+ keys = key.split(".")
132
+ for k in keys:
133
+ assert hasattr(obj, k), f'{k} in attribute "{key}" does not exist'
134
+ obj = getattr(obj, k)
135
+ return obj
136
+
137
+
138
+ def setattr_nested(obj, key: str, value):
139
+ """
140
+ Recursively set attribute
141
+ """
142
+ keys = key.split(".")
143
+ for k in keys[:-1]:
144
+ assert hasattr(obj, k), f'{k} in attribute "{key}" does not exist'
145
+ obj = getattr(obj, k)
146
+ setattr(obj, keys[-1], value)
147
+
148
+
149
+ class PeriodicEvent:
150
+ """
151
+ triggers every period
152
+ """
153
+
154
+ def __init__(self, period: int, initial_value=0):
155
+ self._period = period
156
+ assert self._period >= 1
157
+ self._last_threshold = initial_value
158
+ self._last_value = initial_value
159
+ self._trigger_counts = 0
160
+
161
+ def __call__(self, new_value=None, increment=None):
162
+ assert bool(new_value is None) != bool(increment is None), (
163
+ "you must specify one and only one of new_value or increment, " "but not both"
164
+ )
165
+ d = self._period
166
+ if new_value is None:
167
+ new_value = self._last_value + increment
168
+ assert new_value >= self._last_value, (
169
+ f"value must be monotonically increasing. "
170
+ f"Current value {new_value} < last value {self._last_value}"
171
+ )
172
+ self._last_value = new_value
173
+ if new_value - self._last_threshold >= d:
174
+ self._last_threshold += (new_value - self._last_threshold) // d * d
175
+ self._trigger_counts += 1
176
+ return True
177
+ else:
178
+ return False
179
+
180
+ @property
181
+ def trigger_counts(self):
182
+ return self._trigger_counts
183
+
184
+ @property
185
+ def current_value(self):
186
+ return self._last_value
187
+
188
+
189
+ class Once:
190
+ def __init__(self):
191
+ self._triggered = False
192
+
193
+ def __call__(self):
194
+ if not self._triggered:
195
+ self._triggered = True
196
+ return True
197
+ else:
198
+ return False
199
+
200
+ def __bool__(self):
201
+ raise RuntimeError("`Once` objects should be used by calling ()")
202
+
203
+
204
+ _GLOBAL_ONCE_SET = set()
205
+ _GLOBAL_NTIMES_COUNTER = Counter()
206
+
207
+
208
+ def global_once(name):
209
+ """
210
+ Try this to automate the name:
211
+ https://gist.github.com/techtonik/2151727#gistcomment-2333747
212
+ """
213
+ if name in _GLOBAL_ONCE_SET:
214
+ return False
215
+ else:
216
+ _GLOBAL_ONCE_SET.add(name)
217
+ return True
218
+
219
+
220
+ def global_n_times(name, n: int):
221
+ """
222
+ Triggers N times
223
+ """
224
+ assert n >= 1
225
+ if _GLOBAL_NTIMES_COUNTER[name] < n:
226
+ _GLOBAL_NTIMES_COUNTER[name] += 1
227
+ return True
228
+ else:
229
+ return False
230
+
231
+
232
+ class Every:
233
+ def __init__(self, n: int, on_first: bool = False):
234
+ assert n > 0
235
+ self._i = 0 if on_first else 1
236
+ self._n = n
237
+
238
+ def __call__(self):
239
+ return self._i % self._n == 0
240
+
241
+ def __bool__(self):
242
+ raise RuntimeError("`Every` objects should be used by calling ()")
243
+
244
+
245
+ def encode_base64(obj) -> str:
246
+ return codecs.encode(pickle.dumps(obj), "base64").decode()
247
+
248
+
249
+ def decode_base64(s: str):
250
+ return pickle.loads(codecs.decode(s.encode(), "base64"))
251
+
252
+
253
+ def safe_hash(input_tuple):
254
+ # keep 128 bits of the hash
255
+ tuple_string = repr(input_tuple).encode("utf-8")
256
+ sha256 = hashlib.sha256()
257
+ sha256.update(tuple_string)
258
+
259
+ seed = int(sha256.hexdigest(), 16)
260
+
261
+ return seed & 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
groot/vla/common/utils/misc/torch_utils.py ADDED
@@ -0,0 +1,748 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ from copy import deepcopy
4
+ import os
5
+ import random
6
+ import time
7
+ from typing import List, Optional, Tuple, Union
8
+
9
+ import numpy as np
10
+ import torch
11
+ import torch.nn as nn
12
+ import tree
13
+ from typing_extensions import Literal
14
+
15
+ from ..data_structure.tree_utils import tree_value_at_path
16
+ from ..io.file_utils import f_join
17
+ from ..io.print_utils import to_readable_count_str
18
+ from .functional_utils import assert_implements_method, implements_method
19
+
20
+
21
+ def weight_init(m):
22
+ """Custom weight init for Conv2D and Linear layers."""
23
+ if isinstance(m, nn.Linear):
24
+ nn.init.orthogonal_(m.weight.data)
25
+ if hasattr(m.bias, "data"):
26
+ m.bias.data.fill_(0.0)
27
+ elif isinstance(m, nn.Conv2d) or isinstance(m, nn.ConvTranspose2d):
28
+ gain = nn.init.calculate_gain("relu")
29
+ nn.init.orthogonal_(m.weight.data, gain)
30
+ if hasattr(m.bias, "data"):
31
+ m.bias.data.fill_(0.0)
32
+
33
+
34
+ def get_seed(
35
+ seed: Union[int, str, None],
36
+ handle_invalid_seed: Literal["none", "system", "raise"] = "none",
37
+ ) -> Optional[int]:
38
+ """
39
+ Args:
40
+ seed:
41
+ "system": use scrambled int based on system time
42
+ None or int < 0: invalid seed values, see `handle_invalid_seed`
43
+ int >= 0: returns seed
44
+ handle_invalid_seed: None or int < 0
45
+ - "none": returns None
46
+ - "system": returns scrambled int based on system time
47
+ - "raise": raise Exception
48
+ """
49
+ handle_invalid_seed = handle_invalid_seed.lower()
50
+ assert handle_invalid_seed in ["none", "system", "raise"]
51
+ if isinstance(seed, str):
52
+ assert seed in ["system"]
53
+ invalid = False
54
+ else:
55
+ assert seed is None or isinstance(seed, int)
56
+ invalid = seed is None or seed < 0
57
+
58
+ if seed == "system" or invalid and handle_invalid_seed == "system":
59
+ # https://stackoverflow.com/questions/27276135/python-random-system-time-seed
60
+ t = int(time.time() * 100000)
61
+ return (
62
+ ((t & 0xFF000000) >> 24)
63
+ + ((t & 0x00FF0000) >> 8)
64
+ + ((t & 0x0000FF00) << 8)
65
+ + ((t & 0x000000FF) << 24)
66
+ )
67
+ elif invalid:
68
+ if handle_invalid_seed == "none":
69
+ return None
70
+ elif handle_invalid_seed == "raise":
71
+ raise ValueError(
72
+ f"Invalid random seed: {seed}, " f'must be a non-negative integer or "system"'
73
+ )
74
+ else:
75
+ raise NotImplementedError
76
+ else:
77
+ return seed
78
+
79
+
80
+ def set_deterministic(flag: bool = True):
81
+ if not flag:
82
+ return
83
+
84
+ os.environ["CUBLAS_WORKSPACE_CONFIG"] = ":4096:8"
85
+ os.environ["HOROVOD_FUSION_THRESHOLD"] = "0"
86
+ import torch.backends.cudnn as cudnn
87
+
88
+ cudnn.deterministic = True
89
+ cudnn.benchmark = False
90
+ if hasattr(torch, "use_deterministic_algorithms"):
91
+ # only available in PyTorch >= 1.9
92
+ torch.use_deterministic_algorithms(True)
93
+ elif hasattr(torch, "set_deterministic"):
94
+ # only available in PyTorch >= 1.7
95
+ torch.set_deterministic(True)
96
+
97
+
98
+ def set_seed_everywhere(
99
+ seed: Optional[Union[int, str]],
100
+ deterministic=False,
101
+ set_tensorflow=False,
102
+ handle_invalid_seed: Literal["none", "system", "raise"] = "none",
103
+ ) -> Optional[int]:
104
+ """
105
+ References:
106
+ - https://github.com/NVIDIA/framework-determinism/blob/master/pytorch.md
107
+ - https://pytorch.org/docs/stable/notes/randomness.html
108
+ - CUBLAS env var:
109
+ https://docs.nvidia.com/cuda/cublas/index.html#cublasApi_reproducibility
110
+
111
+ Args:
112
+ seed: see `get_seed()`
113
+ handle_invalid_seed: see `get_seed()`
114
+ """
115
+ set_deterministic(deterministic)
116
+
117
+ seed = get_seed(seed, handle_invalid_seed=handle_invalid_seed)
118
+ if seed is None:
119
+ return None
120
+
121
+ os.environ["PYTHONHASHSEED"] = str(seed)
122
+ random.seed(seed)
123
+ np.random.seed(seed)
124
+ torch.manual_seed(seed)
125
+ if torch.cuda.is_available():
126
+ torch.cuda.manual_seed_all(seed)
127
+ if set_tensorflow:
128
+ try:
129
+ import tensorflow as tf
130
+
131
+ tf.random.set_seed(seed)
132
+ except ImportError:
133
+ pass
134
+ return seed
135
+
136
+
137
+ class eval_mode(object):
138
+ def __init__(self, *models):
139
+ self.models = models
140
+
141
+ def __enter__(self):
142
+ self.prev_states = []
143
+ for model in self.models:
144
+ self.prev_states.append(model.training)
145
+ model.train(False)
146
+
147
+ def __exit__(self, *args):
148
+ for model, state in zip(self.models, self.prev_states):
149
+ model.train(state)
150
+ return False
151
+
152
+
153
+ def get_device(x, strict: bool = False) -> int:
154
+ """
155
+ Args:
156
+ x: can be any arbitrary nested structure of np array and torch tensor
157
+ strict: True to check all batch sizes are the same
158
+ """
159
+ xs = tree.flatten(x)
160
+
161
+ def _get_device(x):
162
+ if torch.is_tensor(x):
163
+ return x.device
164
+ elif isinstance(x, nn.Module):
165
+ return get_module_device(x)
166
+ else:
167
+ return None
168
+
169
+ if strict:
170
+ devices = [_get_device(x) for x in xs]
171
+ assert all(
172
+ b == devices[0] for b in devices
173
+ ), f"devices must all be the same in nested structure: {devices}"
174
+ return devices[0]
175
+ else:
176
+ return _get_device(xs[0])
177
+
178
+
179
+ def load_torch(*fpath: str, map_location="cpu") -> dict:
180
+ """
181
+ Default maps to "cpu"
182
+ """
183
+ fpath = str(f_join(fpath))
184
+ try:
185
+ return torch.load(fpath, map_location=map_location)
186
+ except RuntimeError as e:
187
+ raise RuntimeError(f"{e}\n\n --- Error loading {fpath}")
188
+
189
+
190
+ def save_torch(D, *fpath):
191
+ """
192
+ Supports both (D, fpath) and (fpath, D) arg order, as long as one of them is a str
193
+ """
194
+ if isinstance(D, str):
195
+ assert not isinstance(fpath, str), "Either torch_save(D, fpath) " "or torch_save(fpath, D)"
196
+ fpath, D = D, fpath
197
+ torch.save(D, str(f_join(fpath)))
198
+
199
+
200
+ # Aliases for consistency with load_pickle, load_text, load_json/yaml, etc.
201
+ torch_load = load_torch
202
+ torch_save = save_torch
203
+ dump_torch = save_torch
204
+
205
+
206
+ def torch_compute_stats(x, precision: int = 2):
207
+ x = x.to(dtype=torch.float32)
208
+ return (
209
+ f"mean|std: {torch.mean(x):.{precision}f} +/- {torch.std(x):.{precision}f}, "
210
+ f"median: {torch.median(x):.{precision}f}, "
211
+ f"max: {torch.max(x):.{precision}f}, min: {torch.min(x):.{precision}f}"
212
+ )
213
+
214
+
215
+ def tensor_hash(x: torch.Tensor, mode: str = "mean"):
216
+ if isinstance(x, np.ndarray):
217
+ x = torch.from_numpy(x)
218
+ x = x.float().abs()
219
+ if mode == "sum":
220
+ x = x.sum()
221
+ elif mode == "mean":
222
+ x = x.mean()
223
+ else:
224
+ raise NotImplementedError
225
+ return float(x)
226
+
227
+
228
+ def torch_flatten_indices(indices: torch.Tensor, shape: Tuple[int]):
229
+ """
230
+ Convert M dim indices to 1D indices with the given shape
231
+
232
+ Args:
233
+ indices: BxM, batch_size x M-dimensional
234
+ """
235
+ offsets = np.array(shape) # e.g. [3, 4, 5, 6]
236
+ offsets = np.append(offsets[1:], 1) # [4, 5, 6, 1]
237
+ offsets = np.cumprod(offsets[::-1])[::-1] # [4*5*6, 5*6, 6, 1]
238
+ offsets = torch.tensor(offsets.copy(), dtype=torch.long)
239
+ assert offsets.size() == (len(shape),)
240
+ return (indices * offsets.to(device=indices.device)).sum(dim=1)
241
+
242
+
243
+ def torch_multi_index_select(x: torch.Tensor, indices: torch.Tensor):
244
+ """
245
+ Args:
246
+ x: N dim
247
+ indices: [B x M], M <= N, will select the first M-D from N-D
248
+
249
+ Returns:
250
+ (N - M + 1) dim
251
+ """
252
+ assert indices.ndim == 2
253
+ B, idx_dim = indices.size()
254
+ x_shape = x.size()
255
+ assert len(x_shape) >= idx_dim
256
+ remainder_dim = len(x_shape) - idx_dim
257
+ if remainder_dim == 0:
258
+ x = torch.flatten(x)
259
+ else:
260
+ x = x.view(-1, *x_shape[-remainder_dim:]) # flatten the first M dims
261
+ # convert indices to a 1D flattened array
262
+ indices = torch_flatten_indices(indices, x_shape[:idx_dim])
263
+ selected = x[indices]
264
+ return selected
265
+
266
+
267
+ # ========== module operations =========
268
+ def set_requires_grad(model, requires_grad):
269
+ if torch.is_tensor(model):
270
+ model.requires_grad = requires_grad
271
+ else:
272
+ for param in model.parameters():
273
+ param.requires_grad = requires_grad
274
+
275
+
276
+ def freeze_params(model):
277
+ set_requires_grad(model, False)
278
+ if not torch.is_tensor(model):
279
+ model.eval()
280
+
281
+
282
+ def unfreeze_params(model):
283
+ set_requires_grad(model, True)
284
+ if not torch.is_tensor(model):
285
+ model.train()
286
+
287
+
288
+ def clip_grad_value(model, max_value):
289
+ with torch.no_grad():
290
+ nn.utils.clip_grad_value_(model.parameters(), max_value)
291
+
292
+
293
+ def clip_grad_norm(model, max_norm, norm_type=2):
294
+ """
295
+ Returns:
296
+ Total norm of the parameters (viewed as a single vector).
297
+ """
298
+ with torch.no_grad():
299
+ return nn.utils.clip_grad_norm_(model.parameters(), max_norm=max_norm, norm_type=norm_type)
300
+
301
+
302
+ def implements_state_dict(object, requires_load_method: bool = False):
303
+ cond = implements_method(object, "state_dict")
304
+ if requires_load_method:
305
+ return cond and implements_method(object, "load_state_dict")
306
+ else:
307
+ return cond
308
+
309
+
310
+ def unwrap_ddp_model(model):
311
+ if hasattr(model, "module") and len(list(model.children())) == 1:
312
+ model = model.module
313
+ return model
314
+
315
+
316
+ class DDPMethodWrapper(nn.Module):
317
+ """
318
+ Wraps another module's method as forward(), because DDP only works on forward()
319
+ This module can be wrapped with DDP and directly called.
320
+ It will not save any extra parameters
321
+ """
322
+
323
+ def __init__(self, net: nn.Module, method_name: str):
324
+ super().__init__()
325
+ self.net = net
326
+ assert_implements_method(net, method_name)
327
+ self._method_name = method_name
328
+
329
+ def forward(self, *args, **kwargs):
330
+ return getattr(self.net, self._method_name)(*args, **kwargs)
331
+
332
+ def state_dict(self):
333
+ return {}
334
+
335
+
336
+ def to_state_dict(objects, to_cpu: bool = False, copy: bool = False, unwrap_ddp: bool = False):
337
+ """
338
+ Anything that has state_dict() method, e.g. nn.Module, Optimizer, LRScheduler, etc.
339
+
340
+ Args:
341
+ to_cpu: True to copy to CPU. The original tensors will still be on GPU.
342
+ copy: takes effect if and only if to_cpu is False
343
+ """
344
+
345
+ def _transfer(x):
346
+ if torch.is_tensor(x):
347
+ x = x.detach()
348
+ if to_cpu:
349
+ return x.cpu()
350
+ elif copy:
351
+ return x.clone()
352
+ return x
353
+
354
+ def _to_state_dict(m):
355
+ if implements_state_dict(m):
356
+ if isinstance(m, nn.Module) and unwrap_ddp:
357
+ m = unwrap_ddp_model(m)
358
+ return tree.map_structure(_transfer, m.state_dict())
359
+ else:
360
+ return _transfer(m)
361
+
362
+ return tree.map_structure(_to_state_dict, objects)
363
+
364
+
365
+ def load_state_dict(objects, states, strip_prefix=None, strict=False):
366
+ """
367
+ Args:
368
+ strict: objects and states must match exactly
369
+ strip_prefix: only match the keys that have the prefix, and strip it
370
+ """
371
+
372
+ def _load(paths, obj):
373
+ if not implements_method(obj, "load_state_dict"):
374
+ raise ValueError(f"Object {type(obj)} does not support load_state_dict() method")
375
+ try:
376
+ state = tree_value_at_path(states, paths)
377
+ except ValueError: # paths do not exist in `states` structure
378
+ if strict:
379
+ raise
380
+ else:
381
+ return
382
+ if strip_prefix:
383
+ assert isinstance(strip_prefix, str)
384
+ state = {
385
+ k[len(strip_prefix) :]: v for k, v in state.items() if k.startswith(strip_prefix)
386
+ }
387
+ if isinstance(obj, nn.Module):
388
+ return obj.load_state_dict(state, strict=strict)
389
+ else:
390
+ return obj.load_state_dict(state)
391
+
392
+ return tree.map_structure_with_path(_load, objects)
393
+
394
+
395
+ def count_parameters(model):
396
+ return sum(x.numel() for x in model.parameters())
397
+
398
+
399
+ def readable_count_parameters(model, precision: int = 2):
400
+ return to_readable_count_str(count_parameters(model), precision=precision)
401
+
402
+
403
+ def get_module_device(model):
404
+ """
405
+ Returns:
406
+ first model parameter's device
407
+ """
408
+ return next(model.parameters()).device
409
+
410
+
411
+ def maybe_transfer_module(model, device):
412
+ """
413
+ Transfer a module to another device if and only if they are on different devices.
414
+ Assumes that the module's first parameter determines the module device, i.e.
415
+ no model parallelism.
416
+
417
+ Returns:
418
+ True if module is transferred to a different device, False otherwise
419
+ """
420
+ if device is None:
421
+ return False
422
+ device = torch.device(device)
423
+ if get_module_device(model) != device:
424
+ model.to(device=device)
425
+ return True
426
+ else:
427
+ return False
428
+
429
+
430
+ def clone_model(model):
431
+ with torch.no_grad():
432
+ new_model = deepcopy(model).to(get_module_device(model))
433
+ # new_model.load_state_dict(model.state_dict())
434
+ return new_model
435
+
436
+
437
+ def update_soft_params(net, target_net, tau):
438
+ for param, target_param in zip(net.parameters(), target_net.parameters()):
439
+ target_param.data.copy_(tau * param.data + (1 - tau) * target_param.data)
440
+
441
+
442
+ def tie_weights(src, trg):
443
+ # TODO deprecate this
444
+ assert type(src) is type(trg)
445
+ trg.weight = src.weight
446
+ trg.bias = src.bias
447
+
448
+
449
+ def torch_normalize(tensor: torch.Tensor, mean, std, inplace=False):
450
+ """
451
+ Adapted from https://pytorch.org/docs/stable/_modules/torchvision/transforms/functional.html#normalize
452
+
453
+ Normalize a tensor image with mean and standard deviation.
454
+
455
+ .. note::
456
+ This transform acts out of place by default, i.e., it does not mutates the input tensor.
457
+
458
+ See :class:`~torchvision.transforms.Normalize` for more details.
459
+
460
+ Args:
461
+ tensor (Tensor): Tensor image of size (C, H, W) to be normalized.
462
+ mean (sequence): Sequence of means for each channel.
463
+ std (sequence): Sequence of standard deviations for each channel.
464
+ inplace(bool,optional): Bool to make this operation inplace.
465
+
466
+ Returns:
467
+ Tensor: Normalized Tensor image.
468
+ """
469
+ if not torch.is_tensor(tensor):
470
+ raise TypeError("tensor should be a torch tensor. Got {}.".format(type(tensor)))
471
+
472
+ if not inplace:
473
+ tensor = tensor.clone()
474
+
475
+ dtype = tensor.dtype
476
+ mean = torch.as_tensor(mean, dtype=dtype, device=tensor.device)
477
+ std = torch.as_tensor(std, dtype=dtype, device=tensor.device)
478
+ if (std == 0).any():
479
+ raise ValueError(
480
+ f"std evaluated to zero after conversion to {dtype}, leading to division by zero."
481
+ )
482
+ if mean.ndim == 1:
483
+ mean = mean[:, None, None]
484
+ if std.ndim == 1:
485
+ std = std[:, None, None]
486
+ tensor.sub_(mean).div_(std)
487
+ return tensor
488
+
489
+
490
+ def contains_rnn(net: nn.Module) -> bool:
491
+ for m in net.modules():
492
+ if isinstance(m, nn.RNNBase):
493
+ return True
494
+ return False
495
+
496
+
497
+ def multi_one_hot(x, num_classes: List[int], to_float=True):
498
+ """
499
+ Concatenates multiple one-hot matrices, useful for embedding MultiDiscrete action space
500
+
501
+ Args:
502
+ x: torch.long, [*N, D]
503
+ num_classes: list len == D, match the last dim of x
504
+
505
+ Returns:
506
+ [*N, sum(num_classes)]
507
+ """
508
+ from torch.nn.functional import one_hot
509
+
510
+ assert x.dtype == torch.long
511
+ assert x.dim() >= 2, x.size()
512
+ assert len(num_classes) == x.size(-1), f"{len(num_classes)} != {x.size(1)}"
513
+ result = torch.cat(
514
+ [one_hot(t, c) for t, c in zip(torch.unbind(x, dim=-1), num_classes)], dim=-1
515
+ )
516
+ if to_float:
517
+ return result.float()
518
+ else:
519
+ return result
520
+
521
+
522
+ def _random_derangement(n):
523
+ while True:
524
+ v = [i for i in range(n)]
525
+ for j in range(n - 1, -1, -1):
526
+ p = random.randint(0, j)
527
+ if v[p] == j:
528
+ break
529
+ else:
530
+ v[j], v[p] = v[p], v[j]
531
+ else:
532
+ if v[0] != 0:
533
+ return tuple(v)
534
+
535
+
536
+ def random_derangement(n, format: Literal["list", "numpy", "torch"] = "torch"):
537
+ """
538
+ Early refusal algorithm, described at
539
+ https://stackoverflow.com/questions/25200220/generate-a-random-derangement-of-a-list
540
+ Derangement is permuation without fixed point, useful for constructing negative
541
+ pairs in contrastive learning.
542
+ """
543
+ assert format in ["list", "numpy", "torch"]
544
+ D = _random_derangement(n)
545
+ if format == "list":
546
+ return D
547
+ elif format == "numpy":
548
+ return np.array(D, dtype=np.long)
549
+ elif format == "torch":
550
+ return torch.tensor(D, dtype=torch.long)
551
+ else:
552
+ raise NotImplementedError(f"Unknown format {format}")
553
+
554
+
555
+ def classify_accuracy(
556
+ output,
557
+ target,
558
+ topk: Union[int, List[int], Tuple[int]] = 1,
559
+ mask=None,
560
+ reduction="mean",
561
+ scale_100=False,
562
+ ):
563
+ """
564
+ Computes the accuracy over the k top predictions for the specified values of k.
565
+ Accuracy is a float between 0.0 and 1.0
566
+
567
+ Args:
568
+ topk: if int, return a single acc. If tuple, return a tuple of accs
569
+ mask: shape [batch_size,], binary mask of whether to include this sample or not
570
+ """
571
+ if isinstance(topk, int):
572
+ topk = [topk]
573
+ is_int = True
574
+ else:
575
+ is_int = False
576
+
577
+ batch_size = target.size(0)
578
+ assert output.size(0) == batch_size
579
+ if mask is not None:
580
+ assert mask.dim() == 1
581
+ assert mask.size(0) == batch_size
582
+
583
+ assert reduction in ["sum", "mean", "none"]
584
+ if reduction != "mean":
585
+ assert not scale_100, f"reduce={reduction} does not support scale_100=True"
586
+
587
+ with torch.no_grad():
588
+ maxk = max(topk)
589
+
590
+ _, pred = output.topk(maxk, 1, True, True)
591
+ pred = pred.t()
592
+ correct = pred.eq(target.view(1, -1).expand_as(pred))
593
+ if mask is not None:
594
+ correct = mask * correct
595
+
596
+ mult = 100.0 if scale_100 else 1.0
597
+ res = []
598
+ for k in topk:
599
+ correct_k = correct[:k].int().sum(dim=0)
600
+ if reduction == "mean":
601
+ if mask is not None:
602
+ # fmt: off
603
+ res.append(
604
+ float(correct_k.float().sum().mul_(mult / mask.sum().item()).item())
605
+ )
606
+ # fmt: on
607
+ else:
608
+ res.append(float(correct_k.float().sum().mul_(mult / batch_size).item()))
609
+ elif reduction == "sum":
610
+ res.append(int(correct_k.sum().item()))
611
+ elif reduction == "none":
612
+ res.append(correct_k)
613
+ else:
614
+ raise NotImplementedError(f"Unknown reduce={reduction}")
615
+
616
+ if is_int:
617
+ assert len(res) == 1, "INTERNAL"
618
+ return res[0]
619
+ else:
620
+ return res
621
+
622
+
623
+ def sequential_split_dataset(dataset: torch.utils.data.Dataset, split_portions: list[float]):
624
+ """
625
+ Split a dataset into multiple datasets, each with a different portion of the
626
+ original dataset. Uses torch.utils.data.Subset.
627
+ """
628
+ from .functional_utils import accumulate
629
+
630
+ assert len(split_portions) > 0, "split_portions must be a non-empty list"
631
+ assert all(0.0 <= p <= 1.0 for p in split_portions), f"{split_portions=}"
632
+ assert abs(sum(split_portions) - 1.0) < 1e-6, f"{sum(split_portions)=} != 1.0"
633
+ L = len(dataset)
634
+ assert L > 0, "dataset must be non-empty"
635
+ # split the list with proportions
636
+ lengths = [int(p * L) for p in split_portions]
637
+ # make sure the last split fills the full dataset
638
+ lengths[-1] += L - sum(lengths)
639
+ indices = list(range(L))
640
+
641
+ return [
642
+ torch.utils.data.Subset(dataset, indices[offset - length : offset])
643
+ for offset, length in zip(accumulate(lengths), lengths)
644
+ ]
645
+
646
+
647
+ class RunningMeanStd:
648
+ def __init__(self):
649
+ """
650
+ Calulates the running mean and std of a data stream
651
+ https://en.wikipedia.org/wiki/Algorithms_for_calculating_variance#Parallel_algorithm
652
+ """
653
+ self._mean = None
654
+ self._var = None
655
+ self._count = 0
656
+
657
+ @property
658
+ def mean(self):
659
+ return self._mean
660
+
661
+ @property
662
+ def var(self):
663
+ return self._var
664
+
665
+ @property
666
+ def std(self):
667
+ if isinstance(self._var, np.ndarray):
668
+ return np.sqrt(self._var)
669
+ else:
670
+ return self._var.sqrt()
671
+
672
+ @property
673
+ def count(self):
674
+ return self._count
675
+
676
+ def update(self, values: np.ndarray | torch.Tensor) -> None:
677
+ from .array_tensor_utils import any_mean, any_variance, get_batch_size
678
+
679
+ batch_mean = any_mean(values, dim=0)
680
+ # our running var calculation currently only supports unbiased=False
681
+ batch_var = any_variance(values, dim=0, unbiased=False)
682
+ batch_count = get_batch_size(values)
683
+ self.update_from_moments(batch_mean, batch_var, batch_count)
684
+
685
+ def update_from_moments(
686
+ self,
687
+ batch_mean: np.ndarray | torch.Tensor,
688
+ batch_var: np.ndarray | torch.Tensor,
689
+ batch_count: int,
690
+ ) -> None:
691
+ from .array_tensor_utils import any_get_shape
692
+
693
+ is_tensor = torch.is_tensor(batch_mean)
694
+ _zeros = batch_mean.new_zeros if is_tensor else np.zeros
695
+ if self._mean is None:
696
+ self._mean = _zeros(any_get_shape(batch_mean))
697
+ if self._var is None:
698
+ self._var = _zeros(any_get_shape(batch_var)) + 1.0
699
+
700
+ delta = batch_mean - self._mean
701
+ tot_count = self._count + batch_count
702
+ assert tot_count > 0, "count must be > 0"
703
+
704
+ new_mean = self._mean + delta * batch_count / tot_count
705
+ m_a = self._var * self._count
706
+ m_b = batch_var * batch_count
707
+ m_2 = m_a + m_b + delta * delta * self._count * batch_count / tot_count
708
+ new_var = m_2 / tot_count
709
+
710
+ self._mean = new_mean
711
+ self._var = new_var
712
+ self._count = tot_count
713
+
714
+
715
+ class AverageMeter:
716
+ """Computes and stores the average and current value"""
717
+
718
+ def __init__(self, name="", fmt="f"):
719
+ self._name = name
720
+ self._fmt = fmt
721
+ self.reset()
722
+
723
+ def reset(self):
724
+ self._sum = 0.0
725
+ self._count = 0.0
726
+
727
+ @torch.no_grad()
728
+ def update(self, value, n=1):
729
+ if torch.is_tensor(value):
730
+ value = value.detach()
731
+ self._sum += value * n
732
+ self._count += n
733
+
734
+ @torch.no_grad()
735
+ def compute(self):
736
+ return float(self._sum / self._count)
737
+
738
+ def __float__(self):
739
+ return self.compute()
740
+
741
+ def __str__(self):
742
+ if self._fmt:
743
+ s = f"{float(self):{self._fmt}}"
744
+ else:
745
+ s = str(float(self))
746
+ if self._name:
747
+ return f"{self._name}: {s}"
748
+ return s
groot/vla/common/utils/misc/video_utils.py ADDED
@@ -0,0 +1,487 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import subprocess
3
+
4
+ import av
5
+ import cv2
6
+ import numpy as np
7
+ import torchvision
8
+
9
+ # Import decord with graceful fallback
10
+ try:
11
+ import decord
12
+
13
+ DECORD_AVAILABLE = True
14
+ except ImportError:
15
+ DECORD_AVAILABLE = False
16
+
17
+ try:
18
+ import torchcodec
19
+
20
+ TORCHCODEC_AVAILABLE = True
21
+ except (ImportError, RuntimeError):
22
+ TORCHCODEC_AVAILABLE = False
23
+
24
+
25
+ def _get_video_info_ffmpeg(video_path: str) -> dict:
26
+ """Get video metadata using ffprobe."""
27
+ cmd = [
28
+ "ffprobe",
29
+ "-v",
30
+ "error",
31
+ "-select_streams",
32
+ "v:0",
33
+ "-show_entries",
34
+ "stream=nb_frames,duration,r_frame_rate",
35
+ "-of",
36
+ "json",
37
+ video_path,
38
+ ]
39
+
40
+ try:
41
+ output = subprocess.check_output(cmd, stderr=subprocess.STDOUT).decode("utf-8")
42
+ probe_data = json.loads(output)
43
+ stream = probe_data["streams"][0]
44
+
45
+ # Parse frame rate (comes as fraction like "15/1")
46
+ if "/" in stream["r_frame_rate"]:
47
+ num, den = map(int, stream["r_frame_rate"].split("/"))
48
+ fps = num / den
49
+ else:
50
+ fps = float(stream["r_frame_rate"])
51
+
52
+ # Get frame count and duration
53
+ nb_frames = int(stream.get("nb_frames", 0))
54
+ duration = float(stream.get("duration", 0))
55
+
56
+ # If nb_frames is not available, estimate from duration and fps
57
+ if nb_frames == 0 and duration > 0:
58
+ nb_frames = int(duration * fps)
59
+
60
+ return {
61
+ "nb_frames": nb_frames,
62
+ "fps": fps,
63
+ "duration": duration,
64
+ }
65
+ except (subprocess.CalledProcessError, json.JSONDecodeError, KeyError) as e:
66
+ raise ValueError(f"Failed to get video info for {video_path}: {e}")
67
+
68
+
69
+ def _extract_frames_ffmpeg(video_path: str, frame_indices: list[int]) -> np.ndarray:
70
+ """Extract specific frames using ffmpeg."""
71
+ frames = []
72
+
73
+ for idx in frame_indices:
74
+ # Use ffmpeg to extract a specific frame
75
+ cmd = [
76
+ "ffmpeg",
77
+ "-i",
78
+ video_path,
79
+ "-vf",
80
+ f"select=eq(n\\,{idx})",
81
+ "-vframes",
82
+ "1",
83
+ "-f",
84
+ "image2pipe",
85
+ "-pix_fmt",
86
+ "rgb24",
87
+ "-vcodec",
88
+ "rawvideo",
89
+ "-",
90
+ ]
91
+
92
+ try:
93
+ output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
94
+
95
+ # Check if output is empty (frame doesn't exist)
96
+ if len(output) == 0:
97
+ raise subprocess.CalledProcessError(1, cmd)
98
+
99
+ # Get frame dimensions by probing first
100
+ if len(frames) == 0:
101
+ info_cmd = [
102
+ "ffprobe",
103
+ "-v",
104
+ "error",
105
+ "-select_streams",
106
+ "v:0",
107
+ "-show_entries",
108
+ "stream=width,height",
109
+ "-of",
110
+ "json",
111
+ video_path,
112
+ ]
113
+ info_output = subprocess.check_output(info_cmd).decode("utf-8")
114
+ info_data = json.loads(info_output)
115
+ width = info_data["streams"][0]["width"]
116
+ height = info_data["streams"][0]["height"]
117
+
118
+ # Decode raw RGB data
119
+ frame_data = np.frombuffer(output, dtype=np.uint8)
120
+ frame = frame_data.reshape((height, width, 3))
121
+ frames.append(frame)
122
+
123
+ except subprocess.CalledProcessError:
124
+ # Frame might not exist, create a black frame
125
+ if len(frames) > 0:
126
+ frames.append(np.zeros_like(frames[0]))
127
+ else:
128
+ # Default fallback frame
129
+ frames.append(np.zeros((480, 640, 3), dtype=np.uint8))
130
+
131
+ return np.array(frames)
132
+
133
+
134
+ def _extract_frames_at_timestamps_ffmpeg(video_path: str, timestamps: list[float]) -> np.ndarray:
135
+ """Extract frames at specific timestamps using ffmpeg."""
136
+ frames = []
137
+
138
+ for timestamp in timestamps:
139
+ cmd = [
140
+ "ffmpeg",
141
+ "-ss",
142
+ str(timestamp),
143
+ "-i",
144
+ video_path,
145
+ "-vframes",
146
+ "1",
147
+ "-f",
148
+ "image2pipe",
149
+ "-pix_fmt",
150
+ "rgb24",
151
+ "-vcodec",
152
+ "rawvideo",
153
+ "-",
154
+ ]
155
+
156
+ try:
157
+ output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
158
+
159
+ # Check if output is empty (timestamp doesn't exist)
160
+ if len(output) == 0:
161
+ raise subprocess.CalledProcessError(1, cmd)
162
+
163
+ # Get frame dimensions
164
+ if len(frames) == 0:
165
+ info_cmd = [
166
+ "ffprobe",
167
+ "-v",
168
+ "error",
169
+ "-select_streams",
170
+ "v:0",
171
+ "-show_entries",
172
+ "stream=width,height",
173
+ "-of",
174
+ "json",
175
+ video_path,
176
+ ]
177
+ info_output = subprocess.check_output(info_cmd).decode("utf-8")
178
+ info_data = json.loads(info_output)
179
+ width = info_data["streams"][0]["width"]
180
+ height = info_data["streams"][0]["height"]
181
+
182
+ # Decode raw RGB data
183
+ frame_data = np.frombuffer(output, dtype=np.uint8)
184
+ frame = frame_data.reshape((height, width, 3))
185
+ frames.append(frame)
186
+
187
+ except subprocess.CalledProcessError:
188
+ # Timestamp might be out of bounds, use last frame or black frame
189
+ if len(frames) > 0:
190
+ frames.append(frames[-1])
191
+ else:
192
+ frames.append(np.zeros((480, 640, 3), dtype=np.uint8))
193
+
194
+ return np.array(frames)
195
+
196
+
197
+ def _extract_all_frames_ffmpeg(video_path: str) -> tuple[np.ndarray, np.ndarray]:
198
+ """Extract all frames and their timestamps using ffmpeg."""
199
+ # Get video info
200
+ info = _get_video_info_ffmpeg(video_path)
201
+ fps = info["fps"]
202
+
203
+ # Extract all frames
204
+ cmd = [
205
+ "ffmpeg",
206
+ "-i",
207
+ video_path,
208
+ "-f",
209
+ "image2pipe",
210
+ "-pix_fmt",
211
+ "rgb24",
212
+ "-vcodec",
213
+ "rawvideo",
214
+ "-",
215
+ ]
216
+
217
+ try:
218
+ output = subprocess.check_output(cmd, stderr=subprocess.DEVNULL)
219
+
220
+ # Get frame dimensions
221
+ info_cmd = [
222
+ "ffprobe",
223
+ "-v",
224
+ "error",
225
+ "-select_streams",
226
+ "v:0",
227
+ "-show_entries",
228
+ "stream=width,height",
229
+ "-of",
230
+ "json",
231
+ video_path,
232
+ ]
233
+ info_output = subprocess.check_output(info_cmd).decode("utf-8")
234
+ info_data = json.loads(info_output)
235
+ width = info_data["streams"][0]["width"]
236
+ height = info_data["streams"][0]["height"]
237
+
238
+ # Decode all frames
239
+ frame_data = np.frombuffer(output, dtype=np.uint8)
240
+ total_pixels = len(frame_data) // 3
241
+ actual_frames = total_pixels // (width * height)
242
+
243
+ frames = frame_data[: actual_frames * width * height * 3].reshape(
244
+ (actual_frames, height, width, 3)
245
+ )
246
+
247
+ # Generate timestamps
248
+ timestamps = np.arange(actual_frames) / fps
249
+
250
+ return frames, timestamps
251
+
252
+ except subprocess.CalledProcessError as e:
253
+ raise ValueError(f"Failed to extract frames from {video_path}: {e}")
254
+
255
+
256
+ def get_frames_by_indices(
257
+ video_path: str,
258
+ indices: list[int] | np.ndarray,
259
+ video_backend: str = "ffmpeg",
260
+ video_backend_kwargs: dict = {},
261
+ ) -> np.ndarray:
262
+ if video_backend == "decord":
263
+ if not DECORD_AVAILABLE:
264
+ raise ImportError("decord is not available. Install it with: pip install decord")
265
+ vr = decord.VideoReader(video_path, **video_backend_kwargs)
266
+ frames = vr.get_batch(indices)
267
+ return frames.asnumpy()
268
+ elif video_backend == "torchcodec":
269
+ if not TORCHCODEC_AVAILABLE:
270
+ raise ImportError("torchcodec is not available.")
271
+ decoder = torchcodec.decoders.VideoDecoder(
272
+ video_path, device="cpu", dimension_order="NHWC", num_ffmpeg_threads=0
273
+ )
274
+ return decoder.get_frames_at(indices=indices).data.numpy()
275
+ elif video_backend == "ffmpeg":
276
+ return _extract_frames_ffmpeg(video_path, list(indices))
277
+ elif video_backend == "opencv":
278
+ frames = []
279
+ cap = cv2.VideoCapture(video_path, **video_backend_kwargs)
280
+ for idx in indices:
281
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
282
+ ret, frame = cap.read()
283
+ if not ret:
284
+ raise ValueError(f"Unable to read frame at index {idx}")
285
+ frames.append(frame)
286
+ cap.release()
287
+ frames = np.array(frames)
288
+ return frames
289
+ else:
290
+ raise NotImplementedError
291
+
292
+
293
+ def get_frames_by_timestamps(
294
+ video_path: str,
295
+ timestamps: list[float] | np.ndarray,
296
+ video_backend: str = "ffmpeg",
297
+ video_backend_kwargs: dict = {},
298
+ fps: None | float = None,
299
+ ) -> np.ndarray:
300
+ """Get frames from a video at specified timestamps.
301
+
302
+ Args:
303
+ video_path (str): Path to the video file.
304
+ timestamps (list[int] | np.ndarray): Timestamps to retrieve frames for, in seconds.
305
+ video_backend (str, optional): Video backend to use. Defaults to "ffmpeg".
306
+ fps (float, optional): FPS of the video. Defaults to 30.
307
+ Returns:
308
+ np.ndarray: Frames at the specified timestamps.
309
+ """
310
+ if video_backend == "decord":
311
+ if not DECORD_AVAILABLE:
312
+ raise ImportError("decord is not available. Install it with: pip install decord")
313
+ vr = decord.VideoReader(video_path, **video_backend_kwargs)
314
+ num_frames = len(vr)
315
+ # Retrieve the timestamps for each frame in the video
316
+ frame_ts: np.ndarray = vr.get_frame_timestamp(range(num_frames))
317
+ # Map each requested timestamp to the closest frame index
318
+ # Only take the first element of the frame_ts array which corresponds to start_seconds
319
+ indices = np.abs(frame_ts[:, :1] - timestamps).argmin(axis=0)
320
+ frames = vr.get_batch(indices)
321
+ return frames.asnumpy()
322
+ elif video_backend == "torchcodec":
323
+ if not TORCHCODEC_AVAILABLE:
324
+ raise ImportError("torchcodec is not available.")
325
+ decoder = torchcodec.decoders.VideoDecoder(
326
+ video_path, device="cpu", dimension_order="NHWC", num_ffmpeg_threads=0
327
+ )
328
+
329
+ # https://docs.pytorch.org/torchcodec/stable/generated/torchcodec.decoders.VideoStreamMetadata.html#torchcodec.decoders.VideoStreamMetadata
330
+ # Temporary fix: use 30 fps as the fps of the video (agibot)
331
+ # TODO: get fps as parameter
332
+ if fps is None:
333
+ fps = decoder.metadata.average_fps
334
+ interval = 1 / fps
335
+ timestamps = np.array(timestamps).astype(np.float64)
336
+
337
+ if np.all(timestamps == 0):
338
+ timestamps = np.arange(len(timestamps)) / fps
339
+
340
+ # Get video duration range from first and last frames
341
+ # This is a robust way to get valid timestamp range without depending on specific metadata attributes
342
+ first_frame = decoder.get_frames_at(indices=[0])
343
+ last_frame = decoder.get_frames_at(indices=[len(decoder) - 1])
344
+ min_pts = float(first_frame.pts_seconds[0])
345
+ max_pts = float(last_frame.pts_seconds[0])
346
+
347
+ # Clamp timestamps to valid range to avoid RuntimeError
348
+ timestamps = np.clip(timestamps, min_pts, max_pts)
349
+
350
+ # Correct float precision issues in timestamps
351
+ # E.g. for 5fps video: [1.0, 1.20000005, 1.39999998] -> [1.0, 1.2, 1.4]
352
+ # Without this, the torchcodec will read the delayed frame (e.g. 1.39999998 -> 1.2)
353
+ # Round to nearest frame interval to prevent torchcodec from reading wrong frames
354
+ # Allow max 1% error from expected interval
355
+ if fps is None:
356
+ closest_timestamps = np.round(timestamps / interval) * interval
357
+ # Re-clamp after rounding to ensure still in valid range
358
+ closest_timestamps = np.clip(closest_timestamps, min_pts, max_pts)
359
+ timestamp_errors = np.abs(closest_timestamps - timestamps) / interval
360
+ invalid_mask = timestamp_errors >= 0.01
361
+ if np.any(invalid_mask):
362
+ invalid_indices = np.where(invalid_mask)[0]
363
+ invalid_timestamps = timestamps[invalid_indices]
364
+ raise ValueError(
365
+ f"Try to read invalid timestamps {invalid_timestamps} from video {video_path} (FPS: {fps})"
366
+ )
367
+
368
+ timestamps = closest_timestamps
369
+
370
+ return decoder.get_frames_played_at(seconds=timestamps).data.numpy()
371
+ elif video_backend == "ffmpeg":
372
+ return _extract_frames_at_timestamps_ffmpeg(video_path, list(timestamps))
373
+ elif video_backend == "opencv":
374
+ # Open the video file
375
+ cap = cv2.VideoCapture(video_path, **video_backend_kwargs)
376
+ if not cap.isOpened():
377
+ raise ValueError(f"Unable to open video file: {video_path}")
378
+ # Retrieve the total number of frames
379
+ num_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
380
+ # Calculate timestamps for each frame
381
+ fps = cap.get(cv2.CAP_PROP_FPS)
382
+ frame_ts = np.arange(num_frames) / fps
383
+ frame_ts = frame_ts[:, np.newaxis] # Reshape to (num_frames, 1) for broadcasting
384
+ # Map each requested timestamp to the closest frame index
385
+ indices = np.abs(frame_ts - timestamps).argmin(axis=0)
386
+ frames = []
387
+ for idx in indices:
388
+ cap.set(cv2.CAP_PROP_POS_FRAMES, idx)
389
+ ret, frame = cap.read()
390
+ if not ret:
391
+ raise ValueError(f"Unable to read frame at index {idx}")
392
+ frames.append(frame)
393
+ cap.release()
394
+ frames = np.array(frames)
395
+ return frames
396
+
397
+ elif video_backend == "torchvision_av":
398
+ # set backend
399
+ torchvision.set_video_backend("pyav")
400
+
401
+ # set a video stream reader
402
+ reader = torchvision.io.VideoReader(video_path, "video")
403
+
404
+ # set the first and last requested timestamps
405
+ # Note: previous timestamps are usually loaded, since we need to access the previous key frame
406
+ first_ts = timestamps[0]
407
+ last_ts = timestamps[-1]
408
+
409
+ # access closest key frame of the first requested frame
410
+ # Note: closest key frame timestamp is usally smaller than `first_ts` (e.g. key frame can be the first frame of the video)
411
+ # for details on what `seek` is doing see: https://pyav.basswood-io.com/docs/stable/api/container.html?highlight=inputcontainer#av.container.InputContainer.seek
412
+ reader.seek(first_ts, keyframes_only=True)
413
+
414
+ # Decode frames sequentially, storing the ones we need in a dictionary
415
+ # to map timestamps to frame data. This allows for easy re-ordering later.
416
+ found_frames_map = {}
417
+ tolerance = 0.001 # 1ms tolerance for timestamp matching
418
+
419
+ for frame in reader:
420
+ current_ts = frame["pts"]
421
+
422
+ # Use tolerance-based matching instead of exact match
423
+ for ts in timestamps:
424
+ if ts not in found_frames_map and abs(current_ts - ts) < tolerance:
425
+ found_frames_map[ts] = frame["data"]
426
+ break
427
+
428
+ if current_ts >= last_ts + tolerance or len(found_frames_map) == len(timestamps):
429
+ break
430
+
431
+ reader.container.close()
432
+ reader = None
433
+
434
+ # Debug: print timestamp matching results
435
+ print(f"[video_utils] Requested {len(timestamps)} timestamps: {timestamps[:4]}{'...' if len(timestamps) > 4 else ''}")
436
+ print(f"[video_utils] Found {len(found_frames_map)} frames with tolerance={tolerance}s")
437
+ if len(found_frames_map) < len(timestamps):
438
+ missing = [ts for ts in timestamps if ts not in found_frames_map]
439
+ print(f"[video_utils] WARNING: Missing timestamps: {missing[:4]}{'...' if len(missing) > 4 else ''}")
440
+
441
+ frames = np.array(list(found_frames_map.values()))
442
+ return frames.transpose(0, 2, 3, 1)
443
+
444
+ else:
445
+ raise NotImplementedError
446
+
447
+
448
+ def get_all_frames(
449
+ video_path: str,
450
+ video_backend: str = "ffmpeg",
451
+ video_backend_kwargs: dict = {},
452
+ ) -> tuple[np.ndarray, np.ndarray]:
453
+ """Get all frames from a video.
454
+
455
+ Returns:
456
+ tuple[np.ndarray, np.ndarray]: Frames and timestamps.
457
+ """
458
+ if video_backend == "decord":
459
+ if not DECORD_AVAILABLE:
460
+ raise ImportError("decord is not available. Install it with: pip install decord")
461
+ vr = decord.VideoReader(video_path, **video_backend_kwargs)
462
+ frames = vr.get_batch(range(len(vr))).asnumpy()
463
+ return frames, vr.get_frame_timestamp(range(len(vr)))[:, 0]
464
+ elif video_backend == "torchcodec":
465
+ if not TORCHCODEC_AVAILABLE:
466
+ raise ImportError("torchcodec is not available.")
467
+ decoder = torchcodec.decoders.VideoDecoder(
468
+ video_path, device="cpu", dimension_order="NHWC", num_ffmpeg_threads=0
469
+ )
470
+ frames = decoder.get_frames_at(indices=range(len(decoder)))
471
+ return frames.data.numpy(), frames.pts_seconds.numpy()
472
+ elif video_backend == "ffmpeg":
473
+ return _extract_all_frames_ffmpeg(video_path)
474
+ elif video_backend == "pyav":
475
+ container = av.open(video_path)
476
+ stream = container.streams.video[0]
477
+ assert stream.time_base is not None
478
+ frames = []
479
+ timestamps = []
480
+ for frame in container.decode(video=0):
481
+ frames.append(frame.to_ndarray(format="rgb24"))
482
+ timestamps.append(frame.pts * stream.time_base)
483
+ container.close()
484
+ return np.stack(frames), np.array(timestamps)
485
+
486
+ else:
487
+ raise NotImplementedError
groot/vla/configs/conf.yaml ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ defaults:
2
+ - _self_ # all below configs will override this conf.yaml
3
+ - model: dreamzero/vla
4
+ - data: dreamzero/droid_horizon_relative
5
+ - override hydra/hydra_logging: disabled # disable hydra logging
6
+ - override hydra/job_logging: disabled # disable hydra job logging
7
+
8
+
9
+ # === Model Arguments ===
10
+ model: ???
11
+
12
+ # === Data Arguments ===
13
+ train_dataset: ???
14
+
15
+ # ======== Trainer ========
16
+ trainer:
17
+ _target_: groot.vla.experiment.VLATrainer
18
+ _partial_: true
19
+ _recursive_: false
20
+ callbacks:
21
+ model: ??? # model
22
+ train_dataset: ??? # train_dataset
23
+ compute_dtype: ??? # dtype_from_string(model.config.model_dtype)
24
+ benchmark_time: false # whether or not to benchmark time for training
25
+ # Legacy per-step profiling (profiles every N steps)
26
+ enable_profiling: false # (legacy) enable per-step profiling in training_step
27
+ profiling_steps: 5 # (legacy) profile every N steps
28
+ # ProfCallback: window-based profiling
29
+ enable_prof_callback: false # enable ProfCallback for window-based profiling
30
+ profile_start_step: 50 # session step to start profiling
31
+ profile_warmup_steps: 1 # warmup steps for profiler
32
+ profile_active_steps: 3 # active profiling steps
33
+ profile_record_shapes: false # record tensor shapes (adds overhead)
34
+ profile_with_stack: false # record Python stack traces
35
+ profile_memory: false # record memory allocation
36
+
37
+ # === Training Arguments ===
38
+
39
+ wandb_project: ??? # needs to be specified by user
40
+ output_dir: ??? # need to be specified by user
41
+ load_from_yaml: # need to be specified by user, will override the current config
42
+ gear_credentials: null
43
+ upload_checkpoints: false
44
+ upload_every: 1000
45
+ upload_last_n_checkpoints: 5
46
+ remove_unused_columns: false
47
+ bf16: false
48
+ tf32: false
49
+ global_batch_size: null
50
+ raise_error_if_global_batch_size_not_set: false
51
+ per_device_train_batch_size: 256
52
+ per_device_eval_batch_size: 64
53
+ gradient_accumulation_steps: 1
54
+ dataloader_num_workers: 10
55
+ dataloader_pin_memory: true
56
+ dataloader_persistent_workers: true
57
+ optim: adamw_torch
58
+ learning_rate: 1e-4
59
+ adam_beta1: 0.95
60
+ adam_beta2: 0.999
61
+ adam_epsilon: 1e-8
62
+ weight_decay: 1e-6
63
+ lr_scheduler_type: cosine
64
+ warmup_ratio: 0.05
65
+ logging_steps: 10.0
66
+ num_train_epochs: 1000
67
+ max_steps: -1
68
+ save_strategy: steps
69
+ save_steps: 500
70
+ eval_strategy: "no" # there has to be a double quote; otherwise a bare `no` will be interpreted as False
71
+ save_total_limit: 8
72
+ report_to: wandb
73
+ seed: 42
74
+ do_eval: false
75
+ gradient_checkpointing: false
76
+ ddp_find_unused_parameters: false
77
+ ddp_bucket_cap_mb: 100
78
+ ray_num_workers: ???
79
+ eval_bf16: true
80
+ torch_compile_mode: null
81
+
82
+ pretrained_model_path: null
83
+ only_tune_projectors: false
84
+
85
+ save_llm: false
86
+ save_lora_only: false
87
+ save_value_model: false
88
+ save_q_model: false
89
+
90
+ download_cache: false
91
+
92
+ training_args:
93
+ _target_: transformers.TrainingArguments
94
+ output_dir: ${output_dir}
95
+ run_name: ??? # training_args.output_dir.split("/")[-1]
96
+ remove_unused_columns: ${remove_unused_columns}
97
+ deepspeed: ""
98
+ gradient_checkpointing: ${gradient_checkpointing}
99
+ bf16: ${bf16}
100
+ tf32: ${tf32}
101
+ per_device_train_batch_size: ${per_device_train_batch_size}
102
+ per_device_eval_batch_size: ${per_device_eval_batch_size}
103
+ gradient_accumulation_steps: ${gradient_accumulation_steps}
104
+ dataloader_num_workers: ${dataloader_num_workers}
105
+ dataloader_pin_memory: ${dataloader_pin_memory}
106
+ dataloader_persistent_workers: ${dataloader_persistent_workers}
107
+ optim: ${optim}
108
+ adam_beta1: ${adam_beta1}
109
+ adam_beta2: ${adam_beta2}
110
+ adam_epsilon: ${adam_epsilon}
111
+ learning_rate: ${learning_rate}
112
+ weight_decay: ${weight_decay}
113
+ warmup_ratio: ${warmup_ratio}
114
+ lr_scheduler_type: ${lr_scheduler_type}
115
+ logging_steps: ${logging_steps}
116
+ num_train_epochs: ${num_train_epochs}
117
+ max_steps: ${max_steps}
118
+ save_strategy: ${save_strategy}
119
+ save_steps: ${save_steps}
120
+ save_total_limit: ${save_total_limit}
121
+ report_to: ${report_to}
122
+ seed: ${seed}
123
+ do_eval: ${do_eval}
124
+ ddp_find_unused_parameters: ${ddp_find_unused_parameters}
125
+ ddp_bucket_cap_mb: ${ddp_bucket_cap_mb}
126
+ torch_compile_mode: ${torch_compile_mode}
127
+
128
+ # === T-Rex wandb video reconstruction (optional) ===
129
+ enable_wandb_video_reconstruction: false
130
+ wandb_video_reconstruction_steps: 100
131
+ wandb_video_reconstruction_episode: 0
132
+ wandb_video_reconstruction_num_chunks: 4
133
+ wandb_video_reconstruction_fps: 5
134
+ wandb_video_use_dataset_prompt: true
135
+ wandb_video_prompt: "perform the task"
136
+ wandb_video_overlay_tracks: false
137
+ wandb_video_save_local: true
138
+ wandb_video_save_tracks: false
139
+ wandb_video_track_trail_steps: 8
140
+ wandb_video_reconstruction_inference_steps: 1
141
+ wandb_video_start_chunk_index: 0
142
+ wandb_video_compare_tracks_on_gt: true
143
+
144
+ # === Profiling Arguments ===
145
+ profile_dir: null
146
+
147
+ # === Disable Hydra Config ===
148
+ hydra:
149
+ output_subdir: null
150
+ run:
151
+ dir: .
groot/vla/configs/data/dreamzero/agibot_relative.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ defaults:
4
+ - dreamzero/base_48_wan_fine_aug_relative
5
+ - _self_ # this file will override the base
6
+
7
+ max_state_dim: 64
8
+ use_global_metadata: false
9
+ relative_action: true
10
+ relative_action_per_horizon: false
11
+ relative_action_keys:
12
+ - left_arm_joint_position
13
+ - right_arm_joint_position
14
+ - left_effector_position
15
+ - right_effector_position
16
+ - head_position
17
+ - waist_position
18
+ max_chunk_size: 5
19
+ # Use 10% of data in shards before moving to next shard
20
+ dataset_shard_sampling_rate: 0.1
21
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
22
+ single_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotSubLangSingleActionChunkDatasetDROID
23
+
24
+ # Set your AGIbot dataset path here or override via CLI:
25
+ # agibot_data_root=/path/to/your/agibot_dataset
26
+ agibot_data_root: ???
27
+
28
+ train_dataset:
29
+ _target_: ${mixture_dataset_cls}
30
+ _convert_: object
31
+ mixture_spec:
32
+ - dataset_path:
33
+ agibot:
34
+ - ${agibot_data_root}
35
+ dataset_weight: 1.0
36
+ distribute_weights: true
37
+
38
+ dataset_class: ${single_dataset_cls}
39
+ all_modality_configs: ${modality_configs}
40
+ all_transforms: ${transforms}
41
+ metadata_versions: ${metadata_versions}
42
+ fps: ${fps}
43
+ dataset_kwargs:
44
+ video_backend: decord
45
+ use_global_metadata: ${use_global_metadata}
46
+ max_chunk_size: ${max_chunk_size}
47
+ relative_action: ${relative_action}
48
+ relative_action_keys: ${relative_action_keys}
49
+ relative_action_per_horizon: ${relative_action_per_horizon}
50
+ mixture_kwargs:
51
+ training: true
52
+ balance_dataset_weights: false
53
+ seed: 42
54
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
groot/vla/configs/data/dreamzero/base_48_wan_fine_aug_relative.yaml ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ # Assume `model_specific_transform` is defined in the model config file
4
+
5
+ ################################################################################
6
+ # Normalization Statistics
7
+ # By default, we compute the normalization statistics for the datasets actually
8
+ # used in the mixture. If you want to use the global metadata, set this to true.
9
+ ################################################################################
10
+
11
+ use_global_metadata: false
12
+
13
+ ################################################################################
14
+ # Dimension Information
15
+ ################################################################################
16
+
17
+ num_frames: 49
18
+ action_horizon: 48
19
+ state_horizon: 1
20
+
21
+ # image_resolution_width: 832
22
+ # image_resolution_height: 480
23
+
24
+ image_resolution_width: 480
25
+ image_resolution_height: 256
26
+
27
+ image_resolution_width_single_frame: 256
28
+ image_resolution_height_single_frame: 256
29
+
30
+ ################################################################################
31
+ # Anchored Video Transforms
32
+ ################################################################################
33
+ totensor_cfg: &totensor_cfg
34
+ _target_: groot.vla.data.transform.VideoToTensor
35
+ apply_to: ???
36
+
37
+ crop_cfg: &crop_cfg
38
+ _target_: groot.vla.data.transform.VideoCrop
39
+ apply_to: ???
40
+ scale: 0.95
41
+ mode: random
42
+
43
+
44
+ resize_cfg: &resize_cfg
45
+ _target_: groot.vla.data.transform.VideoResize
46
+ apply_to: ???
47
+ height: ${image_resolution_height}
48
+ width: ${image_resolution_width}
49
+ interpolation: linear
50
+
51
+ resize_cfg_single_frame: &resize_cfg_single_frame
52
+ _target_: groot.vla.data.transform.VideoResize
53
+ apply_to: ???
54
+ height: ${image_resolution_height_single_frame}
55
+ width: ${image_resolution_width_single_frame}
56
+ interpolation: linear
57
+
58
+ color_jitter_cfg: &color_jitter_cfg
59
+ _target_: groot.vla.data.transform.VideoColorJitter
60
+ apply_to: ???
61
+ brightness: 0.3
62
+ contrast: 0.4
63
+ saturation: 0.5
64
+ hue: 0.08
65
+
66
+ random_grayscale_cfg: &random_grayscale_cfg
67
+ _target_: groot.vla.data.transform.VideoRandomGrayscale
68
+ apply_to: ???
69
+ p: 0.1
70
+
71
+ random_posterize_cfg: &random_posterize_cfg
72
+ _target_: groot.vla.data.transform.VideoRandomPosterize
73
+ apply_to: ???
74
+ bits: 4
75
+ p: 0.1
76
+
77
+ normalize_cfg: &normalize_cfg
78
+ _target_: groot.vla.data.transform.VideoNormalize
79
+ apply_to: ???
80
+ mean: [0.5, 0.5, 0.5]
81
+ std: [0.5, 0.5, 0.5]
82
+
83
+
84
+ to_numpy_cfg: &to_numpy_cfg
85
+ _target_: groot.vla.data.transform.VideoToNumpy
86
+ apply_to: ???
87
+
88
+
89
+ ################################################################################
90
+ # oxe_droid (OXE Droid)
91
+ ################################################################################
92
+
93
+ # Modality Configs
94
+ modality_config_oxe_droid:
95
+ video:
96
+ _target_: groot.vla.data.dataset.ModalityConfig
97
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
98
+ eval_delta_indices: [0]
99
+ modality_keys:
100
+ - video.exterior_image_1_left
101
+ - video.exterior_image_2_left
102
+ - video.wrist_image_left
103
+ state:
104
+ _target_: groot.vla.data.dataset.ModalityConfig
105
+ delta_indices: [0]
106
+ modality_keys:
107
+ - state.joint_position
108
+ - state.gripper_position
109
+ action:
110
+ _target_: groot.vla.data.dataset.ModalityConfig
111
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
112
+ modality_keys:
113
+ - action.joint_position
114
+ - action.gripper_position
115
+ language:
116
+ _target_: groot.vla.data.dataset.ModalityConfig
117
+ delta_indices: [0]
118
+ modality_keys:
119
+ - annotation.language.language_instruction
120
+ - annotation.language.language_instruction_2
121
+ - annotation.language.language_instruction_3
122
+ lapa_action:
123
+ _target_: groot.vla.data.dataset.ModalityConfig
124
+ delta_indices: [0]
125
+ modality_keys:
126
+ - lapa_action
127
+
128
+ # Transforms
129
+ transform_oxe_droid:
130
+ _target_: groot.vla.data.transform.ComposedModalityTransform
131
+ transforms:
132
+ # Video transforms
133
+ - <<: *totensor_cfg
134
+ apply_to: ${modality_config_oxe_droid.video.modality_keys}
135
+ - <<: *crop_cfg
136
+ apply_to: ${modality_config_oxe_droid.video.modality_keys}
137
+ - <<: *resize_cfg
138
+ apply_to: ${modality_config_oxe_droid.video.modality_keys}
139
+ - <<: *color_jitter_cfg
140
+ apply_to: ${modality_config_oxe_droid.video.modality_keys}
141
+ - <<: *to_numpy_cfg
142
+ apply_to: ${modality_config_oxe_droid.video.modality_keys}
143
+
144
+ # State transforms
145
+ - _target_: groot.vla.data.transform.StateActionToTensor
146
+ apply_to: ${modality_config_oxe_droid.state.modality_keys}
147
+ - _target_: groot.vla.data.transform.StateActionTransform
148
+ apply_to: ${modality_config_oxe_droid.state.modality_keys}
149
+ normalization_modes:
150
+ state.joint_position: q99
151
+ state.gripper_position: q99
152
+
153
+ # Action transforms
154
+ - _target_: groot.vla.data.transform.StateActionToTensor
155
+ apply_to: ${modality_config_oxe_droid.action.modality_keys}
156
+ - _target_: groot.vla.data.transform.StateActionTransform
157
+ apply_to: ${modality_config_oxe_droid.action.modality_keys}
158
+ normalization_modes:
159
+ action.joint_position: q99
160
+ action.gripper_position: q99
161
+
162
+ # ConcatTransform
163
+ - _target_: groot.vla.data.transform.ConcatTransform
164
+ video_concat_order: ${modality_config_oxe_droid.video.modality_keys}
165
+ state_concat_order: ${modality_config_oxe_droid.state.modality_keys}
166
+ action_concat_order: ${modality_config_oxe_droid.action.modality_keys}
167
+
168
+ # Model-specific transform
169
+ - ${model_specific_transform}
170
+
171
+ ################################################################################
172
+ # agibot (AGIbot: state 32, action 22, 3 views)
173
+ ################################################################################
174
+
175
+ modality_config_agibot:
176
+ video:
177
+ _target_: groot.vla.data.dataset.ModalityConfig
178
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
179
+ eval_delta_indices: [-3, -2, -1, 0]
180
+ modality_keys:
181
+ - video.top_head
182
+ - video.hand_left
183
+ - video.hand_right
184
+ state:
185
+ _target_: groot.vla.data.dataset.ModalityConfig
186
+ delta_indices: [0]
187
+ modality_keys:
188
+ - state.left_arm_joint_position
189
+ - state.right_arm_joint_position
190
+ - state.left_effector_position
191
+ - state.right_effector_position
192
+ - state.head_position
193
+ - state.waist_position
194
+ action:
195
+ _target_: groot.vla.data.dataset.ModalityConfig
196
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
197
+ modality_keys:
198
+ - action.left_arm_joint_position
199
+ - action.right_arm_joint_position
200
+ - action.left_effector_position
201
+ - action.right_effector_position
202
+ - action.head_position
203
+ - action.waist_position
204
+ - action.robot_velocity
205
+ language:
206
+ _target_: groot.vla.data.dataset.ModalityConfig
207
+ delta_indices: [0]
208
+ modality_keys:
209
+ - annotation.language.action_text
210
+
211
+ transform_agibot:
212
+ _target_: groot.vla.data.transform.ComposedModalityTransform
213
+ transforms:
214
+ # Video transforms
215
+ - <<: *totensor_cfg
216
+ apply_to: ${modality_config_agibot.video.modality_keys}
217
+ - <<: *crop_cfg
218
+ apply_to: ${modality_config_agibot.video.modality_keys}
219
+ - <<: *resize_cfg
220
+ apply_to: ${modality_config_agibot.video.modality_keys}
221
+ - <<: *color_jitter_cfg
222
+ apply_to: ${modality_config_agibot.video.modality_keys}
223
+ - <<: *to_numpy_cfg
224
+ apply_to: ${modality_config_agibot.video.modality_keys}
225
+
226
+ # State transforms
227
+ - _target_: groot.vla.data.transform.StateActionToTensor
228
+ apply_to: ${modality_config_agibot.state.modality_keys}
229
+ - _target_: groot.vla.data.transform.StateActionTransform
230
+ apply_to: ${modality_config_agibot.state.modality_keys}
231
+ normalization_modes:
232
+ state.left_arm_joint_position: q99
233
+ state.right_arm_joint_position: q99
234
+ state.left_effector_position: q99
235
+ state.right_effector_position: q99
236
+ state.head_position: q99
237
+ state.waist_position: q99
238
+
239
+ # Action transforms
240
+ - _target_: groot.vla.data.transform.StateActionToTensor
241
+ apply_to: ${modality_config_agibot.action.modality_keys}
242
+ - _target_: groot.vla.data.transform.StateActionTransform
243
+ apply_to: ${modality_config_agibot.action.modality_keys}
244
+ normalization_modes:
245
+ action.left_arm_joint_position: q99
246
+ action.right_arm_joint_position: q99
247
+ action.left_effector_position: q99
248
+ action.right_effector_position: q99
249
+ action.head_position: q99
250
+ action.waist_position: q99
251
+ action.robot_velocity: q99
252
+
253
+ # ConcatTransform
254
+ - _target_: groot.vla.data.transform.ConcatTransform
255
+ video_concat_order: ${modality_config_agibot.video.modality_keys}
256
+ state_concat_order: ${modality_config_agibot.state.modality_keys}
257
+ action_concat_order: ${modality_config_agibot.action.modality_keys}
258
+
259
+ # Model-specific transform
260
+ - ${model_specific_transform}
261
+
262
+ ################################################################################
263
+ # yam (YAM: joint+gripper only from Dataset/YAM_play_data/meta/modality.json;
264
+ # state 14 dims, action 14 dims, 3 views)
265
+ ################################################################################
266
+
267
+ modality_config_yam:
268
+ video:
269
+ _target_: groot.vla.data.dataset.ModalityConfig
270
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
271
+ eval_delta_indices: [0]
272
+ modality_keys:
273
+ - video.top_camera-images-rgb
274
+ - video.left_camera-images-rgb
275
+ - video.right_camera-images-rgb
276
+ state:
277
+ _target_: groot.vla.data.dataset.ModalityConfig
278
+ delta_indices: [0]
279
+ modality_keys:
280
+ - state.left_joint_pos
281
+ - state.left_gripper_pos
282
+ - state.right_joint_pos
283
+ - state.right_gripper_pos
284
+ action:
285
+ _target_: groot.vla.data.dataset.ModalityConfig
286
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
287
+ modality_keys:
288
+ - action.left_joint_pos
289
+ - action.left_gripper_pos
290
+ - action.right_joint_pos
291
+ - action.right_gripper_pos
292
+ language:
293
+ _target_: groot.vla.data.dataset.ModalityConfig
294
+ delta_indices: [0]
295
+ modality_keys:
296
+ - annotation.task
297
+
298
+ transform_yam:
299
+ _target_: groot.vla.data.transform.ComposedModalityTransform
300
+ transforms:
301
+ # Video transforms
302
+ - <<: *totensor_cfg
303
+ apply_to: ${modality_config_yam.video.modality_keys}
304
+ - <<: *crop_cfg
305
+ apply_to: ${modality_config_yam.video.modality_keys}
306
+ - <<: *resize_cfg
307
+ apply_to: ${modality_config_yam.video.modality_keys}
308
+ - <<: *color_jitter_cfg
309
+ apply_to: ${modality_config_yam.video.modality_keys}
310
+ - <<: *to_numpy_cfg
311
+ apply_to: ${modality_config_yam.video.modality_keys}
312
+
313
+ # State transforms
314
+ - _target_: groot.vla.data.transform.StateActionToTensor
315
+ apply_to: ${modality_config_yam.state.modality_keys}
316
+ - _target_: groot.vla.data.transform.StateActionTransform
317
+ apply_to: ${modality_config_yam.state.modality_keys}
318
+ normalization_modes:
319
+ state.left_joint_pos: q99
320
+ state.left_gripper_pos: q99
321
+ state.right_joint_pos: q99
322
+ state.right_gripper_pos: q99
323
+
324
+ # Action transforms
325
+ - _target_: groot.vla.data.transform.StateActionToTensor
326
+ apply_to: ${modality_config_yam.action.modality_keys}
327
+ - _target_: groot.vla.data.transform.StateActionTransform
328
+ apply_to: ${modality_config_yam.action.modality_keys}
329
+ normalization_modes:
330
+ action.left_joint_pos: q99
331
+ action.left_gripper_pos: q99
332
+ action.right_joint_pos: q99
333
+ action.right_gripper_pos: q99
334
+
335
+ # ConcatTransform
336
+ - _target_: groot.vla.data.transform.ConcatTransform
337
+ video_concat_order: ${modality_config_yam.video.modality_keys}
338
+ state_concat_order: ${modality_config_yam.state.modality_keys}
339
+ action_concat_order: ${modality_config_yam.action.modality_keys}
340
+
341
+ # Model-specific transform
342
+ - ${model_specific_transform}
343
+
344
+
345
+ ################################################################################
346
+ # trex (T-Rex: Dexmate Vega-1 dual-arm + 2x Sharpa Wave hands;
347
+ # state 58, action 58 (7 arm + 22 hand per side), 3 views @ 30fps)
348
+ ################################################################################
349
+
350
+ modality_config_trex:
351
+ video:
352
+ _target_: groot.vla.data.dataset.ModalityConfig
353
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24]
354
+ eval_delta_indices: [0]
355
+ modality_keys:
356
+ - video.head_left
357
+ - video.left_wrist
358
+ - video.right_wrist
359
+ state:
360
+ _target_: groot.vla.data.dataset.ModalityConfig
361
+ delta_indices: [0]
362
+ modality_keys:
363
+ - state.left_arm
364
+ - state.left_hand
365
+ - state.right_arm
366
+ - state.right_hand
367
+ action:
368
+ _target_: groot.vla.data.dataset.ModalityConfig
369
+ delta_indices: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23]
370
+ modality_keys:
371
+ - action.left_arm
372
+ - action.left_hand
373
+ - action.right_arm
374
+ - action.right_hand
375
+ language:
376
+ _target_: groot.vla.data.dataset.ModalityConfig
377
+ delta_indices: [0]
378
+ modality_keys:
379
+ - annotation.task
380
+
381
+ transform_trex:
382
+ _target_: groot.vla.data.transform.ComposedModalityTransform
383
+ transforms:
384
+ # Video transforms
385
+ - <<: *totensor_cfg
386
+ apply_to: ${modality_config_trex.video.modality_keys}
387
+ - <<: *crop_cfg
388
+ apply_to: ${modality_config_trex.video.modality_keys}
389
+ - <<: *resize_cfg
390
+ apply_to: ${modality_config_trex.video.modality_keys}
391
+ - <<: *color_jitter_cfg
392
+ apply_to: ${modality_config_trex.video.modality_keys}
393
+ - <<: *to_numpy_cfg
394
+ apply_to: ${modality_config_trex.video.modality_keys}
395
+
396
+ # State transforms
397
+ - _target_: groot.vla.data.transform.StateActionToTensor
398
+ apply_to: ${modality_config_trex.state.modality_keys}
399
+ - _target_: groot.vla.data.transform.StateActionTransform
400
+ apply_to: ${modality_config_trex.state.modality_keys}
401
+ normalization_modes:
402
+ state.left_arm: q99
403
+ state.left_hand: q99
404
+ state.right_arm: q99
405
+ state.right_hand: q99
406
+
407
+ # Action transforms
408
+ - _target_: groot.vla.data.transform.StateActionToTensor
409
+ apply_to: ${modality_config_trex.action.modality_keys}
410
+ - _target_: groot.vla.data.transform.StateActionTransform
411
+ apply_to: ${modality_config_trex.action.modality_keys}
412
+ normalization_modes:
413
+ action.left_arm: q99
414
+ action.left_hand: q99
415
+ action.right_arm: q99
416
+ action.right_hand: q99
417
+
418
+ # ConcatTransform
419
+ - _target_: groot.vla.data.transform.ConcatTransform
420
+ video_concat_order: ${modality_config_trex.video.modality_keys}
421
+ state_concat_order: ${modality_config_trex.state.modality_keys}
422
+ action_concat_order: ${modality_config_trex.action.modality_keys}
423
+
424
+ # Model-specific transform
425
+ - ${model_specific_transform}
426
+
427
+
428
+ ################################################################################
429
+ # Modality Configs
430
+ ################################################################################
431
+
432
+ modality_configs:
433
+ oxe_droid: ${modality_config_oxe_droid}
434
+ agibot: ${modality_config_agibot}
435
+ yam: ${modality_config_yam}
436
+ trex: ${modality_config_trex}
437
+
438
+ ################################################################################
439
+ # Transforms
440
+ ################################################################################
441
+
442
+ transforms:
443
+ oxe_droid: ${transform_oxe_droid}
444
+ agibot: ${transform_agibot}
445
+ yam: ${transform_yam}
446
+ trex: ${transform_trex}
447
+
448
+ ################################################################################
449
+ # Metadata Versions
450
+ ################################################################################
451
+
452
+ metadata_versions:
453
+ oxe_droid: '0221'
454
+ agibot: '0221'
455
+ yam: '0221'
456
+ trex: '0221'
457
+
458
+ ################################################################################
459
+ # FPS (per embodiment, null means use dataset default)
460
+ ################################################################################
461
+
462
+ fps:
463
+ yam: 30
464
+ trex: 30
groot/vla/configs/data/dreamzero/droid_relative.yaml ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ defaults:
4
+ - dreamzero/base_48_wan_fine_aug_relative
5
+ - _self_ # this file will override the base
6
+
7
+ max_state_dim: 64
8
+ use_global_metadata: false
9
+ relative_action: true
10
+ relative_action_per_horizon: false
11
+ relative_action_keys:
12
+ - joint_position
13
+ max_chunk_size: 5
14
+ # Use 10% of data in shards before moving to next shard
15
+ dataset_shard_sampling_rate: 0.1
16
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
17
+ single_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotSubLangSingleActionChunkDatasetDROID
18
+
19
+ # Set your DROID dataset path here or override via CLI:
20
+ # droid_data_root=/path/to/your/droid_dataset
21
+ droid_data_root: ???
22
+
23
+ train_dataset:
24
+ _target_: ${mixture_dataset_cls}
25
+ _convert_: object
26
+ mixture_spec:
27
+ - dataset_path:
28
+ oxe_droid:
29
+ - ${droid_data_root}
30
+ dataset_weight: 1.0
31
+ distribute_weights: true
32
+
33
+ dataset_class: ${single_dataset_cls}
34
+ all_modality_configs: ${modality_configs}
35
+ all_transforms: ${transforms}
36
+ metadata_versions: ${metadata_versions}
37
+ fps: ${fps}
38
+ dataset_kwargs:
39
+ video_backend: decord
40
+ use_global_metadata: ${use_global_metadata}
41
+ max_chunk_size: ${max_chunk_size}
42
+ relative_action: ${relative_action}
43
+ relative_action_keys: ${relative_action_keys}
44
+ relative_action_per_horizon: ${relative_action_per_horizon}
45
+ mixture_kwargs:
46
+ training: true
47
+ balance_dataset_weights: false
48
+ seed: 42
49
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
groot/vla/configs/data/dreamzero/droid_relative_wan22.yaml ADDED
@@ -0,0 +1,51 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+ # DROID data config for Wan 5B (Wan2.2): 320x160 so latent is 20x10 (even H,W) with WanVideoVAE38 (16x), frame_seqlen=50.
3
+ # Extends base and droid settings directly to avoid Hydra nesting (dreamzero/droid_relative would double-resolve defaults).
4
+
5
+ defaults:
6
+ - dreamzero/base_48_wan_fine_aug_relative
7
+ - _self_
8
+
9
+ # Wan 5B: 160x320 (HxW) -> latent 10x20, (10//2)*(20//2)=50. Use H,W divisible by 32 so latent is even (no crop in loss).
10
+ image_resolution_width: 320
11
+ image_resolution_height: 160
12
+
13
+ max_state_dim: 64
14
+ use_global_metadata: false
15
+ relative_action: true
16
+ relative_action_per_horizon: false
17
+ relative_action_keys:
18
+ - joint_position
19
+ max_chunk_size: 5
20
+ dataset_shard_sampling_rate: 0.1
21
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
22
+ single_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotSubLangSingleActionChunkDatasetDROID
23
+ droid_data_root: ???
24
+
25
+ train_dataset:
26
+ _target_: ${mixture_dataset_cls}
27
+ _convert_: object
28
+ mixture_spec:
29
+ - dataset_path:
30
+ oxe_droid:
31
+ - ${droid_data_root}
32
+ dataset_weight: 1.0
33
+ distribute_weights: true
34
+
35
+ dataset_class: ${single_dataset_cls}
36
+ all_modality_configs: ${modality_configs}
37
+ all_transforms: ${transforms}
38
+ metadata_versions: ${metadata_versions}
39
+ fps: ${fps}
40
+ dataset_kwargs:
41
+ video_backend: decord
42
+ use_global_metadata: ${use_global_metadata}
43
+ max_chunk_size: ${max_chunk_size}
44
+ relative_action: ${relative_action}
45
+ relative_action_keys: ${relative_action_keys}
46
+ relative_action_per_horizon: ${relative_action_per_horizon}
47
+ mixture_kwargs:
48
+ training: true
49
+ balance_dataset_weights: false
50
+ seed: 42
51
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
groot/vla/configs/data/dreamzero/trex_relative_wan22.yaml ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+ # T-Rex data config for Wan2.2-TI2V-5B: 320x160 so latent is 20x10 (even H,W) with WanVideoVAE38 (16x), frame_seqlen=50.
3
+ # T-Rex: Dexmate Vega-1 dual-arm + 2x Sharpa Wave hands, state/action 58-dim, 3 RGB views @ 30fps.
4
+ # Dataset: LeRobot v2 layout converted from v3 by scripts/data/convert_trex_v3_to_v2.py + convert_lerobot_to_gear.py.
5
+
6
+ defaults:
7
+ - dreamzero/base_48_wan_fine_aug_relative
8
+ - _self_
9
+
10
+ # Wan 5B: 160x320 (HxW) -> latent 10x20, (10//2)*(20//2)=50. Use H,W divisible by 32 so latent is even (no crop in loss).
11
+ image_resolution_width: 320
12
+ image_resolution_height: 160
13
+
14
+ max_state_dim: 64
15
+ use_global_metadata: false
16
+ relative_action: true
17
+ relative_action_per_horizon: false
18
+ # Relative actions for the arms only; dexterous hands (22 dof each) stay absolute.
19
+ relative_action_keys:
20
+ - left_arm
21
+ - right_arm
22
+ max_chunk_size: 5
23
+ dataset_shard_sampling_rate: 0.1
24
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
25
+ single_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotSubLangSingleActionChunkDatasetDROID
26
+ trex_data_root: ???
27
+
28
+ train_dataset:
29
+ _target_: ${mixture_dataset_cls}
30
+ _convert_: object
31
+ mixture_spec:
32
+ - dataset_path:
33
+ trex:
34
+ - ${trex_data_root}
35
+ dataset_weight: 1.0
36
+ distribute_weights: true
37
+
38
+ dataset_class: ${single_dataset_cls}
39
+ all_modality_configs: ${modality_configs}
40
+ all_transforms: ${transforms}
41
+ metadata_versions: ${metadata_versions}
42
+ fps: ${fps}
43
+ dataset_kwargs:
44
+ video_backend: decord
45
+ use_global_metadata: ${use_global_metadata}
46
+ max_chunk_size: ${max_chunk_size}
47
+ relative_action: ${relative_action}
48
+ relative_action_keys: ${relative_action_keys}
49
+ relative_action_per_horizon: ${relative_action_per_horizon}
50
+ mixture_kwargs:
51
+ training: true
52
+ balance_dataset_weights: false
53
+ seed: 42
54
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
groot/vla/configs/data/dreamzero/trex_track_force_wan22.yaml ADDED
@@ -0,0 +1,139 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+ # T-Rex 20 Hz EEF62 + 250-point tracks + 5 Hz raw tactile history.
3
+
4
+ defaults:
5
+ - /data/dreamzero/base_48_wan_fine_aug_relative
6
+ - _self_
7
+
8
+ image_resolution_width: 320
9
+ image_resolution_height: 160
10
+ max_state_dim: 64
11
+ max_action_dim: 64
12
+ action_horizon: 16
13
+ state_horizon: 1
14
+ num_frames: 33
15
+ relative_action: true
16
+ relative_action_per_horizon: false
17
+ relative_action_keys: [eef62]
18
+ use_global_metadata: false
19
+ max_chunk_size: 4
20
+ dataset_shard_sampling_rate: 0.1
21
+ # Optional overfit controls (None = use every valid causal anchor).
22
+ max_training_anchors: null
23
+ pin_anchor_rank: null
24
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
25
+ single_dataset_cls: groot.vla.model.trex_track_force.dataset.TrexTrackForceShardedDataset
26
+ trex_data_root: ???
27
+
28
+ # The parquet stores absolute EEF targets. The dedicated loader converts each
29
+ # 16-step chunk into T-Rex delta-base actions relative to its start state.
30
+ modality_config_trex:
31
+ video:
32
+ _target_: groot.vla.data.dataset.ModalityConfig
33
+ delta_indices: [0]
34
+ eval_delta_indices: [0]
35
+ modality_keys:
36
+ - video.head_left
37
+ - video.left_wrist
38
+ - video.right_wrist
39
+ state:
40
+ _target_: groot.vla.data.dataset.ModalityConfig
41
+ delta_indices: [0]
42
+ modality_keys:
43
+ - state.eef62
44
+ action:
45
+ _target_: groot.vla.data.dataset.ModalityConfig
46
+ delta_indices: [0]
47
+ modality_keys:
48
+ - action.eef62
49
+ language:
50
+ _target_: groot.vla.data.dataset.ModalityConfig
51
+ delta_indices: [0]
52
+ modality_keys:
53
+ - annotation.task
54
+
55
+ transform_trex:
56
+ _target_: groot.vla.data.transform.ComposedModalityTransform
57
+ transforms:
58
+ - _target_: groot.vla.data.transform.VideoToTensor
59
+ apply_to: ${modality_config_trex.video.modality_keys}
60
+ - _target_: groot.vla.data.transform.VideoResize
61
+ apply_to: ${modality_config_trex.video.modality_keys}
62
+ height: ${image_resolution_height}
63
+ width: ${image_resolution_width}
64
+ interpolation: linear
65
+ - _target_: groot.vla.data.transform.VideoToNumpy
66
+ apply_to: ${modality_config_trex.video.modality_keys}
67
+ - _target_: groot.vla.data.transform.StateActionToTensor
68
+ apply_to: ${modality_config_trex.state.modality_keys}
69
+ - _target_: groot.vla.data.transform.StateActionTransform
70
+ apply_to: ${modality_config_trex.state.modality_keys}
71
+ normalization_modes:
72
+ state.eef62: q99
73
+ - _target_: groot.vla.data.transform.StateActionToTensor
74
+ apply_to: ${modality_config_trex.action.modality_keys}
75
+ - _target_: groot.vla.data.transform.StateActionTransform
76
+ apply_to: ${modality_config_trex.action.modality_keys}
77
+ normalization_modes:
78
+ action.eef62: q99
79
+ - _target_: groot.vla.data.transform.ConcatTransform
80
+ video_concat_order: ${modality_config_trex.video.modality_keys}
81
+ state_concat_order: ${modality_config_trex.state.modality_keys}
82
+ action_concat_order: ${modality_config_trex.action.modality_keys}
83
+ - ${model_specific_transform}
84
+
85
+ # Explicit model/data hand-off contract.
86
+ track_force_columns:
87
+ track_xy: observation.track_xy
88
+ track_visibility: observation.track_visibility
89
+ tactile_force_history: observation.tactile_force
90
+ track_points: 250
91
+ track_history_frames: 16
92
+ track_future_steps: 16
93
+ force_shape: [10, 6]
94
+ force_history_frames: 16
95
+ force_code_tokens: 10
96
+ force_codebook_size: 64
97
+ action_rate_hz: 20
98
+ tactile_rate_hz: 5
99
+ force_stride: 4
100
+ force_offsets: [0, 4, 8, 12]
101
+ autoregressive_blocks: 4
102
+ video_frames_per_block: 8
103
+
104
+ fps:
105
+ # This is the source mp4 rate; logical control samples use timestamp-based
106
+ # 20 Hz action/track and 5 Hz force grids inside the dedicated loader.
107
+ trex: 30
108
+
109
+ train_dataset:
110
+ _target_: ${mixture_dataset_cls}
111
+ _convert_: object
112
+ mixture_spec:
113
+ - dataset_path:
114
+ trex:
115
+ - ${trex_data_root}
116
+ dataset_weight: 1.0
117
+ distribute_weights: true
118
+ dataset_class: ${single_dataset_cls}
119
+ all_modality_configs: ${modality_configs}
120
+ all_transforms: ${transforms}
121
+ metadata_versions: ${metadata_versions}
122
+ fps: ${fps}
123
+ dataset_kwargs:
124
+ video_backend: decord
125
+ use_global_metadata: ${use_global_metadata}
126
+ max_chunk_size: ${max_chunk_size}
127
+ relative_action: ${relative_action}
128
+ relative_action_keys: ${relative_action_keys}
129
+ relative_action_per_horizon: ${relative_action_per_horizon}
130
+ action_rate_hz: 20
131
+ tactile_rate_hz: 5
132
+ video_rate_hz: 10
133
+ max_training_anchors: ${max_training_anchors}
134
+ pin_anchor_rank: ${pin_anchor_rank}
135
+ mixture_kwargs:
136
+ training: true
137
+ balance_dataset_weights: false
138
+ seed: 42
139
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
groot/vla/configs/data/dreamzero/yam_relative.yaml ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # @package _global_
2
+
3
+ defaults:
4
+ - dreamzero/base_48_wan_fine_aug_relative
5
+ - _self_ # this file will override the base
6
+
7
+ max_state_dim: 64
8
+ use_global_metadata: false
9
+ relative_action: true
10
+ relative_action_per_horizon: false
11
+ relative_action_keys:
12
+ - left_joint_pos
13
+ - left_gripper_pos
14
+ - right_joint_pos
15
+ - right_gripper_pos
16
+ max_chunk_size: 5
17
+ # Use 10% of data in shards before moving to next shard
18
+ dataset_shard_sampling_rate: 0.1
19
+ mixture_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotMixtureDataset.from_mixture_spec
20
+ single_dataset_cls: groot.vla.data.dataset.lerobot_sharded.ShardedLeRobotSubLangSingleActionChunkDatasetDROID
21
+
22
+ # Set your YAM dataset path here or override via CLI:
23
+ # yam_data_root=/path/to/your/yam_dataset
24
+ yam_data_root: ???
25
+
26
+ train_dataset:
27
+ _target_: ${mixture_dataset_cls}
28
+ _convert_: object
29
+ mixture_spec:
30
+ - dataset_path:
31
+ yam:
32
+ - ${yam_data_root}
33
+ dataset_weight: 1.0
34
+ distribute_weights: true
35
+
36
+ dataset_class: ${single_dataset_cls}
37
+ all_modality_configs: ${modality_configs}
38
+ all_transforms: ${transforms}
39
+ metadata_versions: ${metadata_versions}
40
+ fps: ${fps}
41
+ dataset_kwargs:
42
+ video_backend: decord
43
+ use_global_metadata: ${use_global_metadata}
44
+ max_chunk_size: ${max_chunk_size}
45
+ relative_action: ${relative_action}
46
+ relative_action_keys: ${relative_action_keys}
47
+ relative_action_per_horizon: ${relative_action_per_horizon}
48
+ mixture_kwargs:
49
+ training: true
50
+ balance_dataset_weights: false
51
+ seed: 42
52
+ shard_sampling_rate: ${dataset_shard_sampling_rate}
groot/vla/configs/deepspeed/zero2.json ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": {
3
+ "load_universal": false
4
+ },
5
+ "train_batch_size": "auto",
6
+ "train_micro_batch_size_per_gpu": "auto",
7
+ "gradient_accumulation_steps": "auto",
8
+ "gradient_clipping": "auto",
9
+ "zero_allow_untested_optimizer": true,
10
+ "fp16": {
11
+ "enabled": "auto",
12
+ "loss_scale": 0,
13
+ "loss_scale_window": 1000,
14
+ "initial_scale_power": 16,
15
+ "hysteresis": 2,
16
+ "min_loss_scale": 1
17
+ },
18
+ "bf16": {
19
+ "enabled": "auto"
20
+ },
21
+ "zero_optimization": {
22
+ "stage": 2,
23
+ "overlap_comm": false,
24
+ "contiguous_gradients": true,
25
+ "sub_group_size": 1e9,
26
+ "reduce_bucket_size": 1e8
27
+ }
28
+ }
groot/vla/configs/deepspeed/zero2_offload.json ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "checkpoint": {
3
+ "load_universal": false
4
+ },
5
+ "train_batch_size": "auto",
6
+ "train_micro_batch_size_per_gpu": "auto",
7
+ "gradient_accumulation_steps": "auto",
8
+ "gradient_clipping": "auto",
9
+ "zero_allow_untested_optimizer": true,
10
+ "fp16": {
11
+ "enabled": "auto",
12
+ "loss_scale": 0,
13
+ "loss_scale_window": 1000,
14
+ "initial_scale_power": 16,
15
+ "hysteresis": 2,
16
+ "min_loss_scale": 1
17
+ },
18
+ "bf16": {
19
+ "enabled": "auto"
20
+ },
21
+ "zero_optimization": {
22
+ "stage": 2,
23
+ "offload_optimizer": {
24
+ "device": "cpu",
25
+ "pin_memory": true
26
+ },
27
+ "overlap_comm": false,
28
+ "contiguous_gradients": true,
29
+ "sub_group_size": 1e9,
30
+ "reduce_bucket_size": 1e8
31
+ }
32
+ }