Visual RL for robot manipulation, built on MJLab. Two tasks (Lift-Cube, Push-T), a Trossen WidowX AI arm, and a visual-encoder stack that trains either a frozen pretrained backbone or a CNN from scratch.
The task ID is the contract. An ID such as
Mjlab-PushT-RealTable-DinoV2ViTS14-Afa6-TrossenIdentified fixes the task,
robot, scene, camera, encoder, adapter and network. Learning rate, seed and
iteration count are flags, so many runs share one ID.
uv sync # creates .venv from uv.lock
uv run vbrl-fetch-backbones # once: DINOv2 + R3M into .models/
uv run pytest -m "not sim" -q # ~1 min, no simulator neededPrefix every command with uv run, or source .venv/bin/activate once.
vbrl-list # every task ID, architecture, scene, robot
vbrl-list architectures # one sectionWatch an untrained policy in the browser — no checkpoint needed, opens Viser:
vbrl-play Mjlab-PushT-State-TrossenIdentified --agent randomTrain the state-based Push-T task (fast, no camera):
vbrl-train Mjlab-PushT-State-TrossenIdentified --agent.max-iterations 50Train a visual policy, and record a video while it runs:
vbrl-train Mjlab-PushT-RealTable-DinoV2ViTS14-Afa6-TrossenIdentified \
--env.scene.num-envs 512 --agent.max-iterations 100 --video TrueWatch a trained policy, or record it to a file:
vbrl-play <TASK_ID> --agent trained --checkpoint-file ckpts/<run>/model.pt
vbrl-play <TASK_ID> --agent trained --checkpoint-file ... --record artifacts/run.mp4vbrl-play, vbrl-evaluate, vbrl-analyze and vbrl-export-onnx load weights
from a local file or a W&B run, never both:
--checkpoint-file ckpts/lift_cube/dinov2.pt
--wandb-run-path entity/project/run_id [--wandb-checkpoint-name model_5998.pt]Every run writes a deployable ONNX beside its checkpoint automatically.
vbrl-list |
every registry, read live from its table |
vbrl-train |
train a task ID; vbrl-train <ID> --help lists all flags |
vbrl-play |
Viser viewer, or --record out.mp4 / .gif |
vbrl-evaluate |
success rates over a model × scene × seed grid, from YAML |
vbrl-analyze |
probe/PCA/occlusion on what an encoder sees, from YAML |
vbrl-deploy |
run a policy on the real arm, from YAML |
vbrl-export-onnx |
re-export a checkpoint that predates automatic export |
vbrl-fetch-backbones |
download the pretrained backbones |
vbrl-evaluate, vbrl-analyze and vbrl-deploy are driven by YAML under
configs/ — copy one and edit it.
vbrl-evaluate configs/evaluation/ood_4096.yaml
vbrl-analyze configs/analysis/lift_cube_rgb/trossen/default.yaml
vbrl-deploy configs/deployment/push_t_realtable_target_sdk50.yaml --dry-runThe hardware drivers are an extra, so a cluster or development sync never pulls them:
uv sync --extra deployOn the Jetson AGX Thor there is one more step uv cannot express — its sm_110
wheels bundle no CUDA, so the libraries need preloading and ONNX Runtime comes
from a separate index:
bash jetson/setup.sh # preload + ONNX Runtime, then verify GPU kernels runRun a policy from a manifest under configs/deployment/.
Every run homes the arm first and parks it afterwards:
vbrl-deploy configs/deployment/push_t_realtable_target_sdk50.yaml
vbrl-deploy configs/deployment/lift_cube.yaml --dry-run # no motion, sanity check
vbrl-deploy configs/deployment/lift_cube.yaml --max-steps 250
vbrl-deploy configs/deployment/lift_cube.yaml --log run.npz # per-step trace
vbrl-deploy configs/deployment/lift_cube.yaml --home # hold home and stop
vbrl-deploy configs/deployment/lift_cube.yaml --park # rest and release torqueArrow keys move the goal while it runs (--no-keyboard-goal disables). A manifest
names the ONNX graph, the arm IP, the goal and the motion limits; the policy's
observation and action contract travel inside the ONNX metadata, so deployment
never reads the task registry.
Only needed when a camera moves or the printed goal marker is repositioned:
python -m vbrl.deployment.intrinsics --save artifacts/deployment/intrinsics
python -m vbrl.deployment.camera # pick camera_exposure_us
python -m vbrl.deployment.capture_board --execute --arm-ip 192.168.1.2
python -m vbrl.deployment.recalibrate --capture artifacts/deployment/charuco
python -m vbrl.deployment.goal_pose frame.png --margin-mm 7.0recalibrate prints an MJCF <camera> block to paste into the robot XML.
goal_pose turns a photo of the two AprilTags into the goal: numbers a manifest
needs — then take the tags off, because the policy must never see them.
Training runs inside a pinned Apptainer image, because the cluster has no Python on the host. Build it once:
apptainer build --force rl.sif rl.defEach launcher takes --profile {a100,b200,testing,a100-single,b200-single} and
passes everything after -- through unchanged:
# Four GPUs; --env.scene.num-envs and --gpu-ids are set by the profile.
bash cluster/submit.sh --profile a100 -- \
Mjlab-PushT-State-TrossenIdentified --agent.max-iterations 500
# One GPU; you own --env.scene.num-envs. ~1.6x the wall clock, a quarter of the GPUs.
bash cluster/submit.sh --profile a100-single -- \
Mjlab-PushT-State-TrossenIdentified --env.scene.num-envs 4096
# One job per architecture in a variant.
bash cluster/submit_sweep.sh --variant Sim2Real --profile b200-single --dry-run
# Viewing and evaluation.
bash cluster/view.sh --profile testing -- Mjlab-PushT-State-TrossenIdentified --agent random
bash cluster/evaluate.sh --profile testing -- configs/evaluation/ood_4096.yamlRun any command inside the image directly with:
apptainer exec --nv --env "PYTHONPATH=$PWD/src" rl.sif <command>Profiles live in cluster/profiles/ — review one before
launching on another cluster.
src/vbrl/
├── scripts/ the CLIs
├── tasks/ lift_cube/, push_t/ — env cfg, mdp/, config/<robot>/
├── vision/ one registry table: 5 encoders x 7 adapters
├── scenes/ presets.py (data) → materials.py → builder.py
├── training/ VisualPPO + the runner that exports ONNX on save
├── evaluation/ rollout, report, video recording
├── analysis/ capture → features → probe/pca/occlusion/comparison
├── deployment/ the real-robot loop, camera, calibration
└── asset_zoo/ robot XML, objects, textures
Adding a task ID is one register_mjlab_task() line in
tasks/<task>/config/<robot>/__init__.py. Adding an encoder or adapter is one
row in vision/registry.py plus one in
vision/architectures.py.
uv run pytest -m "not sim" # contract tests, ~1 min
uv run pytest # adds the tests that build and step a simulator