Fine-tuning, evaluation, and deployment of π0 / π0.5 models for the HSR robot. One codebase supports two runtime families:
- Local GPU machine (Docker) — reproducible training (non-Blackwell / Blackwell GB200 / HSR deploy images)
- HPC (SLURM / PBS + qsub) — distributed multi-node training
The stack is pixi + uv + Docker in layers, and local training is unified on
the Docker path. Running pixi run uv run scripts/train.py ... directly on a
local host ("pixi + uv standalone") is not supported at the moment; pixi is
invoked automatically from within the HPC launchers.
Experiment configs are YAML-first (configs/experiments/*.yaml). The legacy
tyro CONFIG_NAME path is preserved for single-node development, but the
multi-node launchers, Docker images, Blackwell overlay, and policy server are
all designed around YAML.
日本語版: See
README_ja.mdfor the Japanese comprehensive guide. PyTorch training is supported but shows worse performance than the original JAX implementation on this codebase — prefer JAX for HSR training.
- Overview & compatibility matrix
- Repository layout
- Quick start (TL;DR)
- Prerequisites
- Experiment configuration (YAML-first)
- Local environment
- Model deployment
- HPC — SLURM
- HPC — PBS (qsub)
- Cross-cutting tips
- .env / .env.local cheatsheet
- Troubleshooting
- Appendix
| Use case | Recommended script(s) | Config style | Notes |
|---|---|---|---|
| Local Docker (non-Blackwell) | BUILD-DOCKER-CONTAINER.sh train + RUN-DOCKER-CONTAINER.sh train |
YAML | CUDA 12.2 base, standard local path |
| Local Docker (Blackwell GB200) | BUILD-DOCKER-CONTAINER.sh train-gb200 + RUN-DOCKER-CONTAINER.sh train-gb200 |
YAML | Overlays cu128 torch |
| Local Docker (HSR deploy) | BUILD-DOCKER-CONTAINER.sh deploy + RUN-DOCKER-CONTAINER.sh deploy |
— | ROS Noetic included |
| Policy server image | docker build -f server/Dockerfile -t openpi-serve . |
YAML / tyro | WebSocket-based remote inference |
| HPC SLURM single-node | RUN-UV-SBATCH.sh / RUN-UV-SBATCH-8GPU.sh |
tyro (CONFIG_NAME) |
Script invokes pixi+uv internally |
| HPC SLURM multi-node | RUN-UV-SBATCH-OPENPI-MULTINODE.sh |
YAML | Assumes 8 GPU × N nodes |
| HPC PBS single-node | RUN-UV.sh |
YAML / tyro | Script invokes pixi+uv internally |
| HPC PBS multi-node (preferred) | RUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.sh |
YAML | HPCX + direct mpirun |
| HPC PBS multi-node (full-featured) | RUN-UV-QSUB-OPENPI-MULTINODE.sh |
YAML | UCX tuning, preflight |
| HPC Slurm interactive allocation | ./run_interactive_node.sh |
— | salloc wrapper; use sbatch-based launcher after shell |
| HPC Singularity single-node | BUILD_SINGULARITY.sh + RUN-SINGULARITY*.sh |
tyro / YAML | For Docker-less sites |
Running
pixi run uv run scripts/train.py ...directly on the local host is not currently supported. Always use Docker (train/train-gb200) for local training.
AiroPi/
├── README.md # English comprehensive guide
├── README_ja.md # Japanese comprehensive guide
│
├── pixi.toml, pixi.lock # System deps (ffmpeg 7 / pkg-config / compilers)
├── pyproject.toml, uv.lock # Python deps (Python 3.11)
│
├── .env.example # Experiment settings template (tracked)
├── .env.local.example # Personal settings template (tracked)
│ # Actual .env / .env.local are gitignored
│
├── BUILD-DOCKER-CONTAINER.sh # docker compose build
├── RUN-DOCKER-CONTAINER.sh # docker compose up + exec bash
├── RUN-DOCKER-COMMON.sh # Shared logic for train / train-gb200 / deploy
├── RUN-DOCKER-TRAIN.sh # In-container training helper
│
├── RUN-UV-COMMON.sh # Shared logic for pixi + uv launchers
├── RUN-UV.sh # PBS single-node (YAML / tyro)
├── RUN-UV-SBATCH.sh # SLURM single-node 1 GPU (tyro)
├── RUN-UV-SBATCH-8GPU.sh # SLURM single-node 8 GPU (tyro)
├── RUN-UV-SBATCH-OPENPI-MULTINODE.sh # SLURM multi-node (YAML)
├── RUN-UV-QSUB-OPENPI-MULTINODE.sh # PBS multi-node (UCX-tuned)
├── RUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.sh# PBS multi-node (SIMPLE, preferred)
├── RUN-UV-EVAL.sh, RUN-UV-DATA-CHECK.sh, ...
├── BUILD_SINGULARITY.sh, RUN-SINGULARITY*.sh # Singularity path
├── run_interactive_node.sh # Slurm interactive allocation (salloc)
│
├── docker/
│ ├── docker-compose.yml # Mode-agnostic compose
│ ├── Dockerfile.train # Non-Blackwell training (CUDA 12.2)
│ ├── Dockerfile.train.gb200 # Blackwell (cu122 + cu128 overlay)
│ ├── Dockerfile.deploy # HSR deploy (ROS Noetic)
│ ├── openpi.def # Singularity definition
│ └── scripts/ # init, GB200 overlay, ROS setup, …
│
├── server/
│ ├── Dockerfile # Policy server (lean runtime image)
│ ├── entrypoint.sh
│ └── serve_hsr_policy_ws.py # WebSocket policy server
│
├── scripts/
│ ├── train.py # Training entrypoint
│ ├── compute_norm_stats.py # tyro-based norm stats helper
│ ├── aggregate_stats_simple.py / aggregate_stats_fast.py
│ ├── eval_val_loss.py
│ ├── serve_policy.py # Alternate server entrypoint (reference)
│ └── openpi_utils/ # SLURM / PBS distribution glue
│
├── configs/experiments/ # Experiment YAML
│ ├── example_base.yaml # tracked template
│ ├── example_lora.yaml # tracked template
│ ├── example_multinode.yaml # tracked template
│ └── (other *.yaml are gitignored, local-only)
│
├── src/openpi/ # Main source
├── packages/openpi-client/ # Client SDK (workspace subpackage)
└── deploy/ # HSR ROS deployment assets
.gitignoretracks onlyconfigs/experiments/example_*.yaml. Any other YAML you add under that directory is treated as local-only. To share a new experiment config, copy it from anexample_*.yaml, rename it with theexample_prefix, or update.gitignore.
# On the host
export DEEP_PROJECT_NAME=mytest
export DEEP_DATASET_PATH=/path/to/datasets
./BUILD-DOCKER-CONTAINER.sh train
./RUN-DOCKER-CONTAINER.sh train
# → Docker exec'd you into the container; the rest runs inside /home/openpi:
# 1) Compute norm_stats for the YAML's dataset (required before training;
# without it, the loader raises
# "Normalization stats not found. Make sure to run scripts/compute_norm_stats.py ...")
JAX_PLATFORMS=cpu uv run python scripts/aggregate_stats_fast.py \
--episodes-stats "${DATA_DIR}/meta/episodes_stats.jsonl" \
--output-file "assets/example_base/${REPO_ID}/norm_stats.json" \
--chunk-dir "${DATA_DIR}/data/" \
--action-column "action.relative" \
--action-mode "relative"
# 2) Train (YAML mode)
uv run scripts/train.py \
--config-yaml configs/experiments/example_base.yaml \
--exp-name my_exp --overwrite- Point
DATA_DIR/REPO_IDat whatever your YAML'sdataset.data_dir/dataset.repo_idresolves to inside the container (typically under/home/datasets/...) - The output path
assets/<assets_dir>/<asset_id>/norm_stats.jsoncomes from the YAML —configs/experiments/example_base.yamlusesassets_dir: ./assets/example_baseandasset_id: <repo_id>. Your own YAML may rename both; keep theaggregate_stats_fast.py --output-filealigned. - The first
uv syncand cache setup run automatically from the container's init script — you do not need to runpixi run syncon the host.
export DEEP_PROJECT_NAME=gb200test
export DEEP_DATASET_PATH=/path/to/datasets
./BUILD-DOCKER-CONTAINER.sh train-gb200
./RUN-DOCKER-CONTAINER.sh train-gb200
# The cu128 torch overlay is applied automatically inside the container.
# norm_stats → train (same as §3.1)
JAX_PLATFORMS=cpu uv run python scripts/aggregate_stats_fast.py \
--episodes-stats "${DATA_DIR}/meta/episodes_stats.jsonl" \
--output-file "assets/example_base/${REPO_ID}/norm_stats.json" \
--chunk-dir "${DATA_DIR}/data/" \
--action-column "action.relative" \
--action-mode "relative"
uv run scripts/train.py \
--config-yaml configs/experiments/example_base.yaml \
--exp-name my_exp --overwritecp .env.example .env # experiment settings (YAML path, EXP_NAME, ...)
cp .env.local.example .env.local # personal settings (WANDB_API_KEY, CACHE_ROOT, ...)
# Edit both files
qsub RUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.shThis repo depends on LeRobot (submodule) and Git LFS. Use a recursive clone for the first time:
git clone --recurse-submodules git@github.com:airoa-org/AiroPi.git
cd AiroPi
# If already cloned:
git submodule update --init --recursiveConfiguration is split across two layers:
.env— experiment / launcher settings (YAML path,EXP_NAME, node counts, resume/overwrite, etc.). Intended to be team-shareable..env.local— personal / machine-specific (WANDB key,CACHE_ROOT,CHECKPOINT_DIR,HF_LEROBOT_HOME, etc.). Intended to be per-developer.
Both templates (.env.example, .env.local.example) are committed. The actual
.env and .env.local are in .gitignore.
cp .env.example .env
cp .env.local.example .env.local
# Edit both filesLoad order depends on the launcher family:
| Launcher family | Behavior | Notes |
|---|---|---|
Family A: everything using RUN-UV-COMMON.sh::openpi_load_env_file, plus RUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.sh |
Loads .env first, then .env.local (local wins) |
HSR_OPENPI_ENV_FILE overrides the .env path |
Family B: RUN-SINGULARITY.sh, RUN-SINGULARITY-COMPUTE-SIMPLE-NORM.sh, download_base_model.sh, download_training_weights.sh |
Loads only one of HSR_OPENPI_ENV_FILE > .env.local > .env |
Different semantics from Family A |
Family B exception: RUN-SINGULARITY-FT.sh |
Same as Family B, but override variable is AIROPI_ENV_FILE |
Inconsistent with other Singularity scripts |
Minimum variables (details in §11):
.env:OPENPI_CONFIG_YAML,EXP_NAME,OPENPI_NUM_NODES,OPENPI_GRES,OPENPI_RESUME/OPENPI_OVERWRITE.env.local:WANDB_API_KEY,CACHE_ROOT,CHECKPOINT_DIR,HF_LEROBOT_HOME
The stack is pixi + uv + Docker, layered as follows:
- Docker (standard local path): bare
uvis used inside the container. ffmpeg 7 and compilers are baked in, so pixi is not needed - HPC (SLURM / PBS): the
RUN-UV-*.shlaunchers invokepixi run uv syncandpixi run uv run ...on the login / compute nodes, so pixi and uv must be installed there - Host-side training: not currently supported. Use Docker for local training
Install pixi and uv only if you manage the HPC cluster environment yourself (or want a host-side venv for IDE / LSP reasons):
pixi run sync/pixi run sync-blackwellare still valid tasks inpixi.toml— run them if you want a local venv for source browsing. The official training path is the Docker flow in §6.
- Datasets follow the LeRobot layout (
${DATA_DIR}/<repo_id>/{data,meta,videos,...}) meta/episodes_stats.jsonlis required for norm-stat computation- Base models come from
checkpoints.base_model.<model_type>in the YAML (e.g.gs://openpi-assets/checkpoints/pi05_base/params) - For offline / multi-node setups, pre-download base models to
OPENPI_DATA_HOMEto avoid concurrent download races (see thePre-download base model weightsblock inRUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.sh).download_base_model.shanddownload_training_weights.shhelp with this.
.env Experiment / launcher settings
(OPENPI_CONFIG_YAML, EXP_NAME, node counts, resume/overwrite, preflight)
.env.local Personal / credentials
(WANDB_API_KEY, CACHE_ROOT, CHECKPOINT_DIR, HF_LEROBOT_HOME)
configs/experiments/ Experiment semantics
*.yaml (dataset / model / batch / LR / sampler / GPU count / base checkpoint)
The YAML captures the reproducible part of each run; .env* absorb machine and
user specifics.
| Section | Main fields |
|---|---|
experiment |
name, project_name, seed, wandb_enabled |
dataset |
repo_id, data_dir, assets_dir, asset_id, action_mode, prompt_from_task, fast_lerobot, lerobot_backend, video_backend, adapt_to_pi, convert_gripper, ... |
model |
type (pi0 / pi05), paligemma_variant, action_expert_variant, action_dim, action_horizon, finetune_recipe (vision_lora, ...) |
training |
batch_size, num_train_steps, pin_memory, prefetch_factor, eval_interval, save_interval, keep_period, lr_schedule (e.g. cosine_decay) |
gpu |
num_gpus, base_gpus, fsdp_devices |
scaling |
base_batch_size, scale_batch_size, scale_learning_rate, scale_train_steps, workers_per_gpu |
task_sampler |
kind (uniform etc.), alpha, ema_decay, min_prob |
weight_loader |
type (checkpoint / paligemma / noop), params_path |
checkpoints |
base_model.<type> — base checkpoint URL per model type |
The full schema lives in src/openpi/training/experiment_config.py
(ExperimentConfig.from_yaml).
Start a YAML with _base_: <filename> to merge from another YAML in the same
directory (load_yaml_with_inheritance resolves recursively).
# configs/experiments/example_lora.yaml (conceptual sketch)
_base_: example_base.yaml
experiment:
name: my_lora_run
model:
finetune_recipe:
vision_train_mode: full
vision_lora:
enabled: true
rank: 16Only three YAMLs are committed:
configs/experiments/example_base.yaml— minimal pi0.5 fine-tuneconfigs/experiments/example_lora.yaml— LoRA exampleconfigs/experiments/example_multinode.yaml— multi-node GPU / scaling example
Other YAMLs are gitignored (configs/experiments/*.yaml, negated by
!configs/experiments/example_*.yaml). When you want to share a new config,
copy from one of the templates and use an example_ prefix (or update the
gitignore rules).
When --config-yaml is not given, scripts/train.py treats the first
positional argument as a registered tyro TrainConfig name (defined in
src/openpi/training/config.py). Examples: pi05_hsr, pi0_hsr,
pi0_aloha, pi05_aloha, pi0_libero, plus many fine-tune variants.
# Inside the local Docker container:
uv run scripts/train.py pi05_hsr --exp-name my_exp --overwrite
# On HPC single-node SLURM (§8.1), the launcher uses tyro:
sbatch RUN-UV-SBATCH.shThe tyro path is handy for single-node dev, but multi-node launchers, Blackwell overlay, and the policy server assume YAML. Prefer YAML for new experiments.
Local training on a GPU machine goes through Docker only. Three modes are
available (train / train-gb200 / deploy). On the host you only need the
preparation from §4 (copy .env / .env.local) plus Docker and the NVIDIA
Container Toolkit.
Running
scripts/train.pydirectly on the host via pixi + uv is not currently supported.pixi run syncis only useful if you want a local venv for source browsing / IDE — not for training.
Builds from docker/Dockerfile.train (CUDA 12.2 / Ubuntu 22.04). In the Docker
path you do not use pixi — you use bare uv inside the container (ffmpeg 7
is already built into the image).
Prerequisites
- Docker + NVIDIA Container Toolkit (see
scripts/docker/install_docker_ubuntu22.shandscripts/docker/install_nvidia_container_toolkit.sh) - Environment variables:
DEEP_PROJECT_NAME(required): compose project name. The container name becomes${DEEP_PROJECT_NAME}_deep_1DEEP_DATASET_PATH(recommended): host path for datasets, mounted into/home/datasetsinside the containerDEEP_CACHE_ROOT_HOST(optional): host cache directory for uv / HF / tmp; defaults to./.docker_cache
Build
export DEEP_PROJECT_NAME=review
export DEEP_DATASET_PATH=/path/to/lerobot_datasets
./BUILD-DOCKER-CONTAINER.sh trainRun
./RUN-DOCKER-CONTAINER.sh train
# docker compose up -d, then docker exec into the containerOnce the container starts, docker/scripts/initialize-docker-container.sh
performs:
- Create
/home/cache/*(uv, HF, tmp, XDG) - Seed
.bashrc(append${UV_PROJECT_ENVIRONMENT}/binand source the GB200 overlay env file if present; inside the container,UV_PROJECT_ENVIRONMENTdefaults to/home/cache/venv, which maps to the host directory.docker_cache/venv— it is intentionally isolated from any host-side.venv/) - Initial
uv sync(populates the venv outside of image layers) - Apply the GB200 overlay (only in
train-gb200mode) tail -f /dev/nullto keep the container alive
Inside the container you land in /home/openpi. From there:
uv run scripts/train.py --config-yaml configs/experiments/example_base.yaml --exp-name my_exp --overwriteNorm-stats computation works the same way (uv run ...).
Volumes (docker/docker-compose.yml)
| Host | Container | Purpose |
|---|---|---|
| Repo root | /home/openpi |
Source (writable) |
./docker/scripts |
/home/docker_scripts |
init / overlay scripts |
${DEEP_CACHE_ROOT_HOST} |
/home/cache |
uv / HF / tmp |
${DEEP_DATASET_PATH} |
/home/datasets |
LeRobot datasets |
/tmp/.X11-unix |
/tmp/.X11-unix |
X11 (GUI debug) |
/etc/passwd, /etc/group (ro) |
same | User resolution |
/dev/ |
/dev/ |
GPU / USB |
Main CLI flags (uv run scripts/train.py inside the container):
| Flag | Meaning |
|---|---|
--config-yaml PATH |
YAML path (recommended, exclusive with tyro) |
--exp-name NAME |
Run name (overrides YAML experiment.name) |
--resume |
Resume from the latest checkpoint |
--overwrite |
Clear and re-create the checkpoint directory |
--checkpoint-base-dir PATH |
Override checkpoint base dir (default: CHECKPOINT_DIR) |
--assets-base-dir PATH |
Override the assets base dir |
--seed N |
Override the random seed |
Norm stats are computed inside the container:
JAX_PLATFORMS=cpu uv run python scripts/aggregate_stats_fast.py \
--episodes-stats "${DATA_DIR}/meta/episodes_stats.jsonl" \
--output-file "assets/<exp_name>/<repo_id>/norm_stats.json" \
--chunk-dir "${DATA_DIR}/data/" \
--action-column "action.relative" \
--action-mode "relative"Keep --action-mode aligned with YAML dataset.action_mode (current default:
relative).
For GB200 (cu128 + aarch64) use Dockerfile.train.gb200. The
docker/scripts/apply-gb200-overlay.sh script runs automatically inside the
container to overlay cu128 torch.
Outline
- Base image:
nvidia/cuda:12.2.2-devel-ubuntu22.04 - First-time
uv syncinstalls cu122 torch (2.7.1) and jax (0.5.3) - Then
docker/scripts/apply-gb200-overlay.shruns and:- Reinstalls
torch==2.10.0+cu128/torchvision==0.25.0+cu128/torchcodec==0.10.0+cu128from the official cu128 index - Writes
${UV_PROJECT_ENVIRONMENT}/.gb200_overlay_env.sh(adds cu128 torch libs and nvidia libs toLD_LIBRARY_PATH) - Sources that file from
.bashrcfor interactive shells only - Idempotence via
${UV_PROJECT_ENVIRONMENT}/.gb200_overlay_stamp
- Reinstalls
Usage
export DEEP_PROJECT_NAME=gb200test
export DEEP_DATASET_PATH=/path/to/datasets
./BUILD-DOCKER-CONTAINER.sh train-gb200
./RUN-DOCKER-CONTAINER.sh train-gb200
# After exec, verify cu128 is in effect:
uv run python -c "import torch; print(torch.__version__, torch.version.cuda)"
# Expected: 2.10.0+cu128 / 12.8Caveat
The overlay-driven LD_LIBRARY_PATH is only applied in interactive login
shells. To run commands via docker exec, use docker exec ... bash -lc '...' (login shell) or prefix your command with
source ${UV_PROJECT_ENVIRONMENT}/.gb200_overlay_env.sh (inside the container
this defaults to /home/cache/venv).
Dockerfile.deploy is Ubuntu 20.04 / ROS Noetic with the HSR catkin workspace
(hsr_data_msgs, hsr_data_collection, hsr_openpi_deploy).
Build inputs
export DEEP_PROJECT_NAME=hsrdeploy
export HSR_APT_USER=<apt user> # HSR vendor apt credentials
export HSR_APT_PASSWORD=<apt pass>
./BUILD-DOCKER-CONTAINER.sh deployRun
# Resolve the robot: either ROBOT_NAME or HSR_IP
export ROBOT_NAME=hsrb107 # or:
# export HSR_IP=192.168.1.2
# ROS_IP is picked interactively (when multiple interfaces are detected)
./RUN-DOCKER-CONTAINER.sh deployBehavior of RUN-DOCKER-COMMON.sh::openpi_resolve_deploy_network:
- If
HSR_IPis unset, resolveROBOT_NAMEviagetent hoststhenavahi-resolve - If
ROS_IPis unset, pick fromifconfig/ip addroutput ROS_MASTER_URIis auto-set tohttp://${HSR_IP}:11311
On deploy mode, initialize-ros-env.sh also runs (catkin setup etc.).
On-robot inference is performed inside the deploy Docker container (§6.3)
using roslaunch. A separate server/Dockerfile is provided to run a
WebSocket-based policy server as a standalone process for external evaluation
tools or remote inference clients (§7.2).
Prerequisites:
- Container built and running via §6.3; you are inside the container shell
- A trained checkpoint (step directory) is accessible from inside the
container (e.g.
/home/openpi/checkpoints/...) - The HSR is network-reachable;
ROS_MASTER_URIresolves tohttp://${HSR_IP}:11311(handled by §6.3)
Inside the container:
source /root/catkin_ws/devel/setup.bashinitialize-docker-container.sh writes this line into .bashrc, so login
shells (bash -l / exec bash) pick it up automatically.
cd /home/openpi
GIT_LFS_SKIP_SMUDGE=1 uv sync --python 3.11The container's first-boot hook (warm_uv_environment) already syncs once,
but doing it explicitly surfaces dependency drift early.
cd /root/catkin_ws
catkin build
source devel/setup.bashRe-run after changes under deploy/.
Before launching, verify:
src/openpi/training/config.py— tyro path only; check theTrainConfigyou intend to usedeploy/hsr_data_collection/hsr_data_collection/config/hsr_data_collection_config.yaml— controller selection (DualShock 3 vs 4)
Then connect a DualShock 3 / 4 controller to the HSR.
A. Launch with a tyro config name
roslaunch hsr_openpi hsr_openpi.launch \
config_name:=pi05_hsr \
checkpoint_dir:=/home/openpi/checkpoints/pi05_hsr/my_experiment/100000B. Launch with a YAML config (recommended)
roslaunch hsr_openpi hsr_openpi.launch \
config_yaml:=configs/experiments/your_run.yaml \
checkpoint_dir:=/path/to/your_run/100000Main launch arguments (deploy/hsr_openpi_deploy/launch/hsr_openpi.launch):
| Argument | Default | Purpose |
|---|---|---|
config_name |
pi0_hsr_low_mem_finetune |
Registered tyro TrainConfig name |
config_yaml |
"" |
Experiment YAML (takes precedence over config_name) |
checkpoint_dir |
(required) | Step directory to load |
update_freq |
10 |
Inference loop rate (Hz) |
adopted_action_chunks |
10 |
Number of action chunks consumed per step |
upsample, upsample_hz, upsample_method |
true, 100, spline |
Temporal action upsampling |
action_smoothing, ema_alpha, ma_window |
ema, 0.2, 5 |
Action smoothing (ema / moving_average / ...) |
smooth_gripper, smooth_base |
false, false |
Whether to smooth gripper / base |
gripper_mode |
hybrid |
Gripper control mode |
save_exec_trace |
false |
Whether to persist the execution trace |
rosservice call /hsr_openpi/update_instruction "message: 'Open the oven toaster'"Press the controller's left D-pad to start execution.
server/ at the repo root holds the assets for building a lean WebSocket
policy server as a standalone container. Use this when an external client
(evaluation tool, remote inference harness, etc.) needs to issue inference
requests over the network.
For the details (image build, server/entrypoint.sh environment variables,
checkpoint-embedded YAML auto-detection, hot reload / health check endpoints),
see server/Dockerfile, server/entrypoint.sh,
server/serve_hsr_policy_ws.py, and
src/openpi/serving/websocket_policy_server.py.
Both are tyro (CONFIG_NAME) path only. They do not support YAML mode.
Minimum .env / .env.local:
# .env (experiment)
CONFIG_NAME=pi05_hsr # a name registered in src/openpi/training/config.py
EXP_NAME=my_slurm_run
# .env.local (personal)
WANDB_API_KEY=...
DATA_DIR=/path/to/lerobot_datasets
HF_LEROBOT_HOME=/path/to/lerobot_cache
CHECKPOINT_DIR=/path/to/checkpoints
CACHE_ROOT=/path/to/cache_rootSubmit:
sbatch RUN-UV-SBATCH.sh # 1 node / 1 GPU / 24h
sbatch RUN-UV-SBATCH-8GPU.sh # 1 node / 8 GPUsInternally the script runs:
module load cuda(if Environment Modules is present)RUN-UV-COMMON.sh: load.env, then.env.localpixi run uv syncaggregate_stats_simple.pyto compute norm statspixi run uv run scripts/train.py "${CONFIG_NAME}" --exp-name=... --overwrite
YAML-mode multi-node launcher. The script re-submits itself via sbatch and
then uses script_distribute_slurm_tasks.sh to launch per-rank srun tasks.
Minimum .env / .env.local:
# .env (experiment)
OPENPI_CONFIG_YAML=configs/experiments/example_multinode.yaml
EXP_NAME=pi05_hsr_task689_run1
OPENPI_NUM_NODES=4
OPENPI_GRES=gpu:8
OPENPI_CPUS_PER_TASK=240
OPENPI_SBATCH_ARGS="--partition=<your-partition> --mem=1490000M --time=336:00:00"
OPENPI_RESUME=0
OPENPI_OVERWRITE=1
OPENPI_PREFLIGHT_ENABLE=1
# .env.local (personal)
WANDB_API_KEY=...
CACHE_ROOT=/groups/.../tmp_storage
CHECKPOINT_DIR=/groups/.../checkpoints
HF_LEROBOT_HOME=/groups/.../lerobot_cacheSubmit:
bash RUN-UV-SBATCH-OPENPI-MULTINODE.shKeep YAML gpu.num_gpus aligned with OPENPI_NUM_NODES * (GPUs per node from OPENPI_GRES);
the launcher warns when they disagree.
A salloc wrapper for getting an interactive Slurm node.
./run_interactive_node.sh # defaults: 8 GPU / 192h / 1900G mem
./run_interactive_node.sh -g 1 -t 1:00:00 # 1 GPU / 1 hour
./run_interactive_node.sh -p <your-partition> -g 8 -t 4:00:00 -c 56
./run_interactive_node.sh --cpu-only # no GPU (connector nodes)Flags:
| Flag | Description |
|---|---|
-p, --partition PART |
Slurm partition (default <your-partition>) |
-g, --gpus N |
GPUs per node (default 8) |
-c, --cpus N |
CPUs per task (default 224) |
-t, --time HH:MM:SS |
Wall time (default 192:00:00) |
-m, --mem SIZE |
Memory (default 1900G) |
--cpu-only |
Drop the GPU request (connector nodes) |
Once you have a shell, the usual way to run training is to submit a
sbatch-based launcher (§8.1 / §8.2). The launchers invoke
pixi run uv sync / pixi run uv run ... internally.
PBS + MPI (OpenMPI / HPCX) path, developed against ABCI.
YAML-only. Loads HPCX via module load and calls mpirun directly within the
PBS job.
Minimum .env / .env.local:
# .env (experiment)
OPENPI_CONFIG_YAML=configs/experiments/example_multinode.yaml
EXP_NAME=pi05_hsr_task6_4nodes_run1
OPENPI_RESUME=1 # to continue from the latest checkpoint
OPENPI_OVERWRITE=0
# .env.local (personal)
WANDB_API_KEY=...
DATA_DIR=/groups/.../lerobot_datasets
HF_LEROBOT_HOME=/groups/...
CHECKPOINT_DIR=/groups/.../${USER}/AiroPi/checkpoints
CACHE_ROOT=/groups/.../tmp_storagePBS headers at the top of the script fix select / walltime. Node count is
derived from select=N:ncpus=192:mpiprocs=8 (assumes 8 GPUs per node;
gpu.num_gpus in the YAML should be N * 8).
Submit:
qsub RUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.sh
# Or override the PBS headers:
qsub -l select=4:ncpus=192:mpiprocs=8 RUN-UV-QSUB-OPENPI-MULTINODE-SIMPLE.shA healthy run logs from each rank:
[INFO] MASTER_ADDR=node-abc
[INFO] MASTER_PORT=12345
[INFO] NUM_NODES=4
[INFO] CONFIG=configs/experiments/example_multinode.yaml
Pre-steps (base-model download, norm-stat recompute via
scripts/aggregate_stats_fast.py) are executed by a single process before
mpirun (through pixi run uv run python).
Adds UCX tuning, preflight parquet checks, variable OPENPI_MPI_TASKS_PER_NODE,
etc. Relevant extra .env variables:
OPENPI_MPI_TASKS_PER_NODE=1
OPENPI_MPI_MODULE=hpcx/2.20
OPENPI_MPI_LAUNCHER=mpirun
OPENPI_MPI_MAP_BY=ppr:1:node
OPENPI_MPI_SHARED_TMPDIR=/groups/.../tmp_storage/mpi
OPENPI_MPI_ARGS="--bind-to none"
OPENPI_MPI_ASSIGN_UCX_NET_DEVICES=1
OPENPI_MPI_NDR_COUNT=8
UCX_MAX_RNDV_RAILS=8
UCX_MAX_EAGER_RAILS=2
OPENPI_QSUB_ARGS="-V -q <your-queue> -P <your-project> -l walltime=336:00:00"
OPENPI_QSUB_SELECT="select=4:mpiprocs=1"
OPENPI_PREFLIGHT_ENABLE=1
OPENPI_PREFLIGHT_SAMPLES_PER_NODE=20
OPENPI_PREFLIGHT_RETRIES=3Submit:
bash RUN-UV-QSUB-OPENPI-MULTINODE.shFallback for HPC sites without Docker. docker/openpi.def defines the image
and BUILD_SINGULARITY.sh builds airopi.sif.
./BUILD_SINGULARITY.sh # produces airopi.sif
qsub RUN-SINGULARITY.sh # training + uv sync
qsub RUN-SINGULARITY-FT.sh # fine-tune
qsub RUN-SINGULARITY-COMPUTE-SIMPLE-NORM.sh # norm stats only- Each script supports
SIF_PATH/WORK_DIRoverrides via env / args --bind /groups/<your-project>/dataset:/groups/<your-project>/datasetis ABCI-specific; update the line for other sites (otherwisesingularity execfails)- Env file loading follows Family B semantics (see §4.2) — if
.env.localexists,.envis not read
| Script | Characteristics | Use |
|---|---|---|
scripts/aggregate_stats_fast.py |
Parallel JAX aggregation | HPC / multi-node |
scripts/aggregate_stats_simple.py |
Pure Python | Single-node, debugging |
scripts/compute_norm_stats.py |
tyro CONFIG_NAME entrypoint | tyro mode only |
Orbax writes the following per save step:
${CHECKPOINT_DIR}/<exp_name>/
└── <step>/
├── train_state/ # Optimizer state etc.
├── params/ # Inference parameters
├── assets/<asset_id>/norm_stats.json
└── experiment_config/experiment_config.yaml # Only in YAML mode
experiment_config.yaml holds the merged-with-_base_ YAML, so the policy
server (§7.2) can reconstruct the experiment semantics from a step directory
alone via its auto-detection path.
- Set
WANDB_API_KEYin.env.localto enable - Disable via YAML
experiment.wandb_enabled: false - In multi-node runs, only rank 0 logs; other ranks initialize wandb in
disabledmode
src/openpi/training/distributed.py probes the following env vars in order.
MPI-provided variables are picked up automatically.
| Canonical | MPI fallbacks |
|---|---|
RANK |
OMPI_COMM_WORLD_RANK, PMI_RANK, PMIX_RANK, MV2_COMM_WORLD_RANK |
LOCAL_RANK |
OMPI_COMM_WORLD_LOCAL_RANK, MPI_LOCALRANKID, ... |
WORLD_SIZE |
OMPI_COMM_WORLD_SIZE, ... |
LOCAL_WORLD_SIZE |
OMPI_COMM_WORLD_LOCAL_SIZE, ... |
MASTER_ADDR, MASTER_PORT |
Required explicitly |
Local training is pinned to the Docker flow (bare uv run), so you almost
never call pixi run directly.
- Inside Docker / Singularity: bare
uv run ...(ffmpeg is in the image) - Inside HPC launchers:
RUN-UV-COMMON.shinvokespixi run uv sync/pixi run uv run ...automatically. pixi and uv must be installed on the login node - Exception:
RUN-UV-QSUB-OPENPI-MULTINODE.sh(the full-featured variant) still uses bareuv runhistorically — candidate for cleanup
| Variable | Scope | Description |
|---|---|---|
OPENPI_CONFIG_YAML |
YAML path | Training YAML (relative or absolute) |
EXP_NAME |
All | Run identifier |
CONFIG_NAME |
tyro | Registered TrainConfig name (legacy) |
DATASET_NAME |
tyro | Dataset subpath (legacy) |
OPENPI_RESUME |
Training | 1 passes --resume (mutually exclusive with overwrite) |
OPENPI_OVERWRITE |
Training | 1 passes --overwrite |
OPENPI_NUM_NODES |
SLURM / PBS | Node count |
OPENPI_GRES |
SLURM | e.g. gpu:8 |
OPENPI_SBATCH_ARGS |
SLURM | Extra SBATCH options |
OPENPI_CPUS_PER_TASK |
SLURM | CPUs per task (default 240) |
OPENPI_MPI_TASKS_PER_NODE |
PBS (full) | MPI ranks per node |
OPENPI_MPI_MAP_BY |
PBS (full) | OpenMPI map specification |
OPENPI_MPI_LAUNCHER |
PBS (full) | mpirun / mpiexec |
OPENPI_MPI_MODULE |
PBS (full) | Module to load (default hpcx/2.20). The SIMPLE variant hardcodes hpcx/2.20 |
OPENPI_MPI_SHARED_TMPDIR |
PBS (full) | Shared MPI temp dir |
OPENPI_MPI_ARGS |
PBS (full) | Extra mpirun options |
OPENPI_QSUB_ARGS |
PBS (full) | Extra qsub options |
OPENPI_QSUB_SELECT |
PBS (full) | qsub -l select=... |
OPENPI_MPI_ASSIGN_UCX_NET_DEVICES |
PBS (full) | 1 assigns UCX_NET_DEVICES per rank |
OPENPI_MPI_NDR_COUNT |
PBS (full) | HCA count (default 8) |
OPENPI_AGGREGATE_STATS_ENABLE |
PBS (full) | 1 recomputes norm stats before training |
OPENPI_PREFLIGHT_ENABLE |
SLURM / PBS (full) | 1 enables parquet cross-node check |
OPENPI_PREFLIGHT_SAMPLES_PER_NODE |
SLURM / PBS (full) | Preflight sample count |
| Variable | Scope | Description |
|---|---|---|
WANDB_API_KEY |
All | W&B logging (logging is off when unset) |
WORKING_DIR |
uv | Working dir (default: script location) |
CACHE_ROOT |
Training | Base for uv / HF / tmp / XDG caches (writable from compute nodes) |
CHECKPOINT_DIR |
Training | Checkpoint root |
HF_LEROBOT_HOME |
Training | LeRobot HuggingFace cache |
DATA_DIR |
Training | LeRobot dataset root (usually inferred from YAML; override only when necessary) |
| Variable | Scope | Description |
|---|---|---|
DEEP_PROJECT_NAME |
Docker | compose project name (required) |
DEEP_DATASET_PATH |
Docker | Host dataset path |
DEEP_CACHE_ROOT_HOST |
Docker | Host cache root (default ./.docker_cache) |
DEEP_IMAGE_TAG |
Docker | Image tag (defaults to the mode name) |
DEEP_DOCKERFILE |
Docker | Dockerfile (auto-set by mode) |
OPENPI_PROJECT_PYTHON |
Docker | Default 3.11 |
OPENPI_ENABLE_ROS_INIT |
Docker | auto (default) / 1 / 0 |
OPENPI_PYTHON_OVERLAY_HOOK |
Docker (GB200) | Overlay script path (set by Dockerfile) |
HSR_APT_USER, HSR_APT_PASSWORD |
Docker deploy | HSR apt credentials (required) |
ROBOT_NAME, HSR_IP, ROS_IP, ROS_MASTER_URI |
Docker deploy | ROS connectivity |
| Variable | Scope | Description |
|---|---|---|
POLICY_CHECKPOINT_DIR |
server | Step directory for inference (required) |
POLICY_CONFIG_NAME, POLICY_CONFIG_YAML |
server | One of them (unless the checkpoint has an embedded experiment_config.yaml) |
POLICY_SERVER_HOST, POLICY_SERVER_PORT |
server | Default 0.0.0.0:8000 |
POLICY_DEFAULT_PROMPT |
server | Prompt fallback |
POLICY_RECORD_DIR |
server | Recording directory |
POLICY_PYTORCH_DEVICE |
server | cuda / cpu / ... |
| Variable | Scope | Description |
|---|---|---|
HSR_OPENPI_ENV_FILE |
Family A and most of Family B | Override for the path of .env |
AIROPI_ENV_FILE |
RUN-SINGULARITY-FT.sh only |
Same role, inconsistent naming |
- Network flakiness usually resolves on retry
- Pass
GIT_LFS_SKIP_SMUDGE=1touv syncif you don't want LeRobot LFS to pull - A root-owned
.venv/left over on the host can clash with the mounted repo. The Docker-side venv is isolated at${UV_PROJECT_ENVIRONMENT}(default/home/cache/venv= host.docker_cache/venv), so you can safely delete the host's.venv/if it was created by a previous container run
${UV_PROJECT_ENVIRONMENT}/.gb200_overlay_stampguards re-application. Changing overlay versions (OPENPI_GB200_TORCH_VERSION, ...) triggers a reinstall on the next container start. To force it, remove the stamp file and restart the container
- When
ROBOT_NAMEis a.localname,avahi-resolve -4 --name ...must succeed (apt install avahi-utils) - To force an IP, set
HSR_IP=...in.envor the shell
The only YAMLs committed under configs/experiments/ are:
example_base.yaml— minimal pi0.5 fine-tuneexample_lora.yaml— LoRA (vision / action) exampleexample_multinode.yaml— multi-node GPU / scaling example
To share a new config in-repo, copy one of the templates and rename it with
the example_ prefix (or update the .gitignore rule).
- Python 3.11.x
- jax 0.5.3 (cu12)
- torch 2.7.1 (non-Blackwell) / 2.10.0+cu128 (Blackwell overlay)
- flax 0.10.2, orbax-checkpoint 0.11.13
- transformers 4.53.2
- lerobot (pinned git rev, see
pyproject.toml) - pixi: ffmpeg ≥ 7 < 8, pkg-config, compilers, cython (conda-forge)
The following π0.5 checkpoints (HSR) are planned for public release. Each entry
corresponds to a step directory under ${CHECKPOINT_DIR}/<exp_name>/<step>/
produced by scripts/train.py (layout described in §10.2), and can be loaded
either via checkpoint_dir:=... on the deploy launcher (§7.1) or by pointing
the policy server (§7.2) at the step directory.
| Stage | Run name (step) | Tasks |
|---|---|---|
| Pre-training | pi05_hsr_75tasks_fast_multinode_8nodes_vision_lora_action_full (step 250,000) |
75 HSR tasks; π0.5 with vision LoRA + full action expert, trained on 8 nodes |
| Pre-training + post-training | pi05_hsr_exercise_ph1_0405_lora_pretrain (step 49,999) |
Table pick-and-place (1 task) |
| Pre-training + post-training | pi05_hsr_task6891011_level12_260304_finetune_68tasks_full_pi05 (step 200,000) |
Bottle relocation, two-bottle relocation, box relocation, open/close microwave, cup relocation (5 tasks) |
The released base model weights are published on Hugging Face at
airoa-org/airoa-pi05-hsr-base.
Download the checkpoint directory (the
checkpoint directory layout) and pass it as
checkpoint_dir:=...
(on-robot deployment) or
POLICY_CHECKPOINT_DIR (policy server).
The training dataset (AIRoA MoMa 5k, gated) is available at
airoa-org/airoa-moma-5k.