Upload dreamzero source code (no model checkpoints)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +3 -0
- .gitignore +246 -0
- COPYRIGHT +4 -0
- LICENSE +21 -0
- README.md +194 -0
- assets/dataset_analysis.png +3 -0
- assets/dataset_overview.png +3 -0
- assets/hardware_setup.png +3 -0
- assets/trex_camera_calib.json +51 -0
- docs/DATASET_TO_GEAR_AND_TRAIN.md +471 -0
- docs/DROID_CONVERSION.md +62 -0
- docs/TREX_TRACK_FORCE_MODEL.md +301 -0
- docs/WAN22_BACKBONE.md +167 -0
- eval_utils/policy_client.py +93 -0
- eval_utils/policy_server.py +130 -0
- eval_utils/run_sim_eval.py +219 -0
- eval_utils/serve_dreamzero_wan22.py +391 -0
- groot/__init__.py +0 -0
- groot/control/__init__.py +0 -0
- groot/control/tensorrt_utils.py +852 -0
- groot/vla/__init__.py +0 -0
- groot/vla/common/__init__.py +1 -0
- groot/vla/common/utils/__init__.py +3 -0
- groot/vla/common/utils/data_structure/__init__.py +2 -0
- groot/vla/common/utils/data_structure/shape_utils.py +283 -0
- groot/vla/common/utils/data_structure/tree_utils.py +219 -0
- groot/vla/common/utils/io/__init__.py +6 -0
- groot/vla/common/utils/io/config_utils.py +260 -0
- groot/vla/common/utils/io/file_utils.py +707 -0
- groot/vla/common/utils/io/hdf5_utils.py +84 -0
- groot/vla/common/utils/io/json_utils.py +270 -0
- groot/vla/common/utils/io/print_utils.py +362 -0
- groot/vla/common/utils/io/termcolor.py +186 -0
- groot/vla/common/utils/misc/__init__.py +5 -0
- groot/vla/common/utils/misc/array_tensor_utils.py +372 -0
- groot/vla/common/utils/misc/functional_utils.py +635 -0
- groot/vla/common/utils/misc/image_utils.py +225 -0
- groot/vla/common/utils/misc/misc_utils.py +261 -0
- groot/vla/common/utils/misc/torch_utils.py +748 -0
- groot/vla/common/utils/misc/video_utils.py +487 -0
- groot/vla/configs/conf.yaml +151 -0
- groot/vla/configs/data/dreamzero/agibot_relative.yaml +54 -0
- groot/vla/configs/data/dreamzero/base_48_wan_fine_aug_relative.yaml +464 -0
- groot/vla/configs/data/dreamzero/droid_relative.yaml +49 -0
- groot/vla/configs/data/dreamzero/droid_relative_wan22.yaml +51 -0
- groot/vla/configs/data/dreamzero/trex_relative_wan22.yaml +54 -0
- groot/vla/configs/data/dreamzero/trex_track_force_wan22.yaml +139 -0
- groot/vla/configs/data/dreamzero/yam_relative.yaml +52 -0
- groot/vla/configs/deepspeed/zero2.json +28 -0
- 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
|
assets/dataset_overview.png
ADDED
|
Git LFS Details
|
assets/hardware_setup.png
ADDED
|
Git LFS Details
|
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 |
+
}
|