The Physical AI Sprint
Get warmed up for Actuate the best way possible — by building. Join Nebius, NVIDIA, Antioch, and Toloka for a day of workshops and building at the intersection of AI and the physical world. Start with technical workshops on the tools and challenge space, then form a team — or work solo — and build a working prototype.
What to expect
We'll begin with technical workshops designed to help you get familiar with the tools, infrastructure, and challenge space. Then, you'll form a team — or work independently — and spend the rest of the day building a working prototype.
- Robotics workshops
- A focused physical AI challenge
- Access to tools, infrastructure, and technical support
- Project demos and judging
- Prizes for the strongest submissions
01The challenge
The perception–reasoning–action loop
Modern AI excels at generating text and pixels. Physical AI requires closing the loop between the digital mind and a physical embodiment, integrating three pillars:
Interpret camera and sensor data into a live understanding of a scene.
Ingesting multi-modal sensor data — RGB-D video, LiDAR, IMU readings, tactile feedback, spatial audio — into a real-time, dynamic understanding of the physical surroundings.
Turn goals into multi-step behavior and adapt when the world changes.
Leveraging foundation models, spatial computing, and decision-making frameworks to analyze constraints, anticipate outcomes, plan multi-step tasks, and adapt to unexpected real-world changes.
Translate plans into robot motion that actually works.
Translating high-level plans into precise physical outputs: robotic manipulation, autonomous navigation, drone flight dynamics, or interactive IoT actuator responses.
Your goal: design and demonstrate a Physical AI task in simulation, in the real world, or across both. You have several robot modalities, the Antioch simulation platform for building and testing, and a single day. The brief is deliberately open-ended: we provide the robots, the world, and the building blocks, and you decide what the system does and how it thinks. In short, build some cool stuff.
Three project directions
All are equally viable.
- Sim only. Build and demonstrate entirely on the Antioch platform. No hardware queue, unlimited resets, and every robot modality is available.
- Hardware only. Build directly on the physical robots at the hardware stations.
- Sim and real. Show a trajectory between the two: prototype or train in sim and run it on the real robot (sim-to-real), or capture real demonstrations and bring them into sim (real-to-sim).
02Format
100 to 125 participants, hacking in teams of 4 or 5.
03Schedule
Monday, August 17, 2026 · Subject to change
04What you get
The Antioch platform
Antioch is a cloud simulation platform for robotics. You write ordinary Python against the NVIDIA Isaac stack (Isaac Sim and Isaac Lab are both available) on your own laptop, and Antioch dispatches it to GPU machines on Nebius. Nothing heavy runs locally: no GPU, no Docker, no Isaac install, and macOS and Linux both work.
You can develop interactively by building simulations and streaming them into the browser. You can also define parameterized simulation scenarios and run variants at scale headlessly. Scenario runs are recorded and replayable, with metrics, logs, live viewport streams, and 3D recordings you can open in the browser.
Your team will receive a link to sign up for the Antioch Console. Each team gets its own development environment where teammates share machines and scenario runs and can see each other's work as it lands. Signup links will be distributed over email once teams are formed.
Robot hardware
LeRobot SO-101 pairs
Leader/follower arms. Setup documentation for Linux and Windows; for macOS, use Guide 1.
Hardware stations are shared between teams and staffed by the host teams.
Sim-ready assets
- Robots. A calibrated SO-101 digital twin, plus the Unitree lineup in the Isaac asset library (Go2, G1, and others; Guide 4 lists what is on the shelf).
- Environments. Ready-made scenes to stage your task in.
- Objects. Containers, fixtures, and props, so composing a scene takes minutes rather than hours of importing and tuning your own meshes.
Physical assets for SO-101
- Blocks of various size, shape, and color
- Vials and vial racks
Frontier models: Kimi K3 on Nebius Token Factory
Nebius Token Factory serves open-weight frontier models over an OpenAI-compatible API. Kimi K3 is Moonshot AI's open-weight flagship — a mixture-of-experts model with a 1M-token context window, built for agentic work. New accounts get $25 in Token Factory credits plus $25 in Tavily credits for live web search.
In your robot's reasoning layer
This is the pillar that is easiest to underbuild. Any OpenAI client works — point it at Token Factory and use it for natural-language tasking, multi-step planning, or deciding what to do when a grasp fails.
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.tokenfactory.nebius.com/v1/",
api_key=os.environ["NEBIUS_API_KEY"],
)
plan = client.chat.completions.create(
model="moonshotai/Kimi-K3",
messages=[
{"role": "system", "content": "Turn goals into SO-101 pick-and-place steps."},
{"role": "user", "content": "Put the green cube in the tray, then reset."},
],
)
print(plan.choices[0].message.content)In your coding agent
kimirelay routes Claude Code, Codex CLI, OpenCode, or Pi Code through Token Factory for a single run, without editing your real agent config — useful when you are building fast and would rather not spend your own subscription limits on it.
curl -fsSL https://kimirelay.com/install.sh | sh
kimirelay configure # your Nebius key, plus an optional Tavily key for web search
klaude # or kodex, openkode, kpi| Command | Launches |
|---|---|
klaude | Claude Code |
kodex | Codex CLI |
openkode | OpenCode |
kpi | Pi Code |
--model before the harnesskimirelay <harness>, so a flag placed after the harness name is passed through to the agent and silently dropped — you get the default model instead of the one you asked for. kimirelay --model moonshotai/Kimi-K2.6 codex exec "…" works; kodex --model … does not.moonshotai/Kimi-K2.7-Code (coding, 262K context) and moonshotai/Kimi-K2.6 (vision + reasoning) are the fallbacks. Keys: tokenfactory.nebius.com. Relay docs: kimirelay.com.Support
Engineers from the host companies are on hand all day for platform, hardware, and training questions.
05Getting started guides
The four guides at the back of this handbook each take you from zero to a working proof of concept with the hardware or with Antioch.
Sim teleoperation
Drive the Antioch sim with a real SO-101 leader arm and record scored demonstration episodes.
Policy training
Train an ACT or Diffusion policy on sim data, evaluate it closed-loop, and run it on a real arm.
Unitree robots on Antioch
Spawn a quadruped or humanoid, give it a controller, and verify it on physics rather than video.
Some directions to calibrate ambition
- A vial pick-and-place cell that survives a shuffled scene
- A new skill taught by teleop and distilled into a policy
- Person-following with a quadruped
- Pouring with the G1
- A multi-robot scene
- Natural-language tasking
- Deliberate failure recovery
06Judging
Judging runs science-fair style first: judges from the hosting companies circulate and view projects at team stations, so have a tight walkthrough and a live or recorded demo ready. The top 6 teams then demo to the full group, and winners are announced after.
Projects are judged on four categories, unweighted.
How hard the problem is and how much of the loop you take on.
Does it work end-to-end when demonstrated.
Originality in the task, approach, or demo.
How well the system is put together: clean boundaries between perception, reasoning, and action, and sound engineering choices.
07Ground rules
- Unless you are confident you can pull it off quickly, avoid fine-tuning pre-trained policies or VLA models. Training a small imitation policy from scratch on your own demonstrations fits comfortably in the day (Guide 3 shows the path); wrangling a large pre-trained model usually does not.
- Make it awesome.
Prizes
Prizes for the strongest submissions, awarded after the top 6 teams demo to the full group. Judging criteria are in section 06.
What to bring
Bring your laptop, charger, and anything else you rely on for a full day of building. Required setup instructions and recommended tools are shared with registered participants before the event — the four guides in this handbook cover the setup you can do in advance.
Who should attend
This event is designed for:
- Robotics engineers
- Machine learning engineers
- Computer vision engineers
- Research engineers and applied scientists
- Data and infrastructure engineers working with robotics or physical AI
- Technical founders and builders exploring embodied intelligence
You don't need to arrive with a team. We'll help participants connect and form teams before and at the event — post yours or find one with room in the team directory.
Important notes
- Space is limited, and all attendees must apply and be approved to attend.
- This is a hands-on technical event. Participants should be comfortable writing code and building independently or as part of a small team.
- Please register individually, even if you plan to attend with a team.
Guide 1LeRobot on macOS: first-time setup for the SO-101
Go from a Mac with nothing installed to a real SO-101 arm following your hand. The published LeRobot instructions assume Windows or Ubuntu, so the port names, permissions, and GPU advice in them do not match a Mac. This guide is the macOS version, with every difference called out where you hit it.
Step 0 · What you need
Hardware on the desk
- An SO-101 follower arm, which is the one that moves under command.
- An SO-101 leader arm if you want to teleoperate. The leader has a handle and no gripper servo horn.
- One data-capable USB-C cable per arm, plus the arm power supplies. A charge-only cable shows no serial port at all and wastes twenty minutes.
Software: a terminal, and either miniconda or uv (docs.astral.sh/uv). You do not need a GPU, Docker, or ROS.
Step 1 · Create the environment
Either tool works. The conda route pulls in ffmpeg, which LeRobot uses for video encoding during dataset recording:
conda create -n lerobot python=3.10 ffmpeg -c conda-forge -y
conda activate lerobot
pip install lerobot opencv-pythonWith uv instead:
uv venv ~/lerobot-env --python 3.10
source ~/lerobot-env/bin/activate
uv pip install lerobot opencv-pythonAdd opencv-python in either case, because anything that touches a camera needs it and the error you get without it is unhelpful.
python -c "import lerobot, torch; print(lerobot.__version__, torch.__version__)"
python -c "import torch; print('MPS available:', torch.backends.mps.is_available())"You should see a lerobot version and MPS available: True. Plain pip install torch already gives you Apple Silicon acceleration. There is no CUDA wheel for a Mac, so ignore every tutorial that tells you to install one.
Step 2 · Find the serial ports
Plug in one arm, then list the ports:
ls /dev/tty.usbmodem*macOS names serial ports /dev/tty.usbmodemXXXXXXXX. Linux tutorials say /dev/ttyACM0 and the vendor manual says COM22 or COM24; neither exists here, so substitute your own port name into every command you copy. LeRobot also ships an interactive finder that walks you through unplugging a cable:
lerobot-find-portTwo more macOS specifics. You do not need udev rules or sudo chmod on the port, because it is usable as soon as it appears. USB hubs also renumber ports across reboots and replugs, so check the name at the start of each session rather than trusting one you wrote down yesterday.
ls /dev/tty.usbmodem* prints one port per plugged-in arm. If it prints nothing, swap the cable before you debug anything else.
Step 3 · Tell the two arms apart
Both arms produce identical port names, so you have to identify them. Two reliable methods:
- The unplug test. List the ports with both arms connected, unplug one, list again, and note which name disappeared.
- The passive wiggle test. This project ships a script that reads joints for 15 seconds and commands no motion.
cd ~/my-sim
python replay/check_so101.py --port /dev/tty.usbmodemXXXXXXXXMove each joint gently by hand while it prints. The sanity bar comes from the script's own docstring: at rest the numbers should be repeatable, none should sit near plus or minus 180, and moving one joint must move only its own number, in a consistent direction.
You know which port is the leader and which is the follower. Save them so later commands are copy-paste:
export LEADER=/dev/tty.usbmodemAAAAAAAA
export FOLLOWER=/dev/tty.usbmodemBBBBBBBBconnect() enables torque, so the arm stiffens when the script starts. That is normal, and the script still sends no motion.Step 4 · Assign servo IDs (DIY kits only)
Pre-assembled arms arrive with servo IDs set, so skip this step. If you built the arm from a kit, each servo needs its ID written one at a time, with only that servo connected:
lerobot-setup-motors --robot.type=so101_follower --robot.port=$FOLLOWERFollow the prompts and the vendor manual's wiring order. Connecting several unconfigured servos at once writes the same ID to more than one of them, which is confusing to undo.
Step 5 · Calibrate each arm
Calibration teaches LeRobot each joint's real range. Do it once per arm, in the calibration start pose from the manual:
lerobot-calibrate --robot.type=so101_follower \
--robot.port=$FOLLOWER --robot.id=my_follower_arm
lerobot-calibrate --teleop.type=so101_leader \
--teleop.port=$LEADER --teleop.id=my_leader_armThe --id you choose here is the --id every later command must use, because profiles are keyed by it. They are written to ~/.cache/huggingface/lerobot/calibration/.
Both profiles exist:
ls ~/.cache/huggingface/lerobot/calibration/Back that directory up now. Recalibrating in the middle of a demo costs you the demo.
Step 6 · First motion: teleoperate
With both arms calibrated, the leader drives the follower:
lerobot-teleoperate \
--robot.type=so101_follower --robot.port=$FOLLOWER --robot.id=my_follower_arm \
--teleop.type=so101_leader --teleop.port=$LEADER --teleop.id=my_leader_armKeep one hand near the power switch for the first run. Move the leader slowly and watch for a joint that travels the wrong way, which points at a calibration or ID mismatch rather than a hardware fault.
The follower mirrors the leader across all six joints, including the gripper, with no joint racing to its limit. Flags vary slightly between lerobot releases, so run lerobot-teleoperate --help if one is rejected.
Step 7 · Record a dataset
Recording captures synchronized camera frames, joint states, and commands in the LeRobot dataset format, which is what the training scripts consume. Check your cameras first:
lerobot-find-cameras opencvThen record, adding your camera and dataset flags from lerobot-record --help:
lerobot-record \
--robot.type=so101_follower --robot.port=$FOLLOWER --robot.id=my_follower_arm \
--teleop.type=so101_leader --teleop.port=$LEADER --teleop.id=my_leader_arm \
--dataset.repo_id=<your-name>/so101-pick-place \
--dataset.single_task="Put the green cube in the tray."Episodes appear under ~/.cache/huggingface/lerobot/<your-repo-id>/ with video files and parquet data. From there, lerobot-train fits a policy; Guide 3 covers the training and evaluation path, including a way to generate episodes in simulation instead of by hand.
macOS differences at a glance
| Every other tutorial says | On a Mac |
|---|---|
/dev/ttyACM0 or COM22 | /dev/tty.usbmodemXXXXXXXX |
sudo chmod 666 /dev/ttyACM0 and udev rules | Nothing to do; the port works when it appears |
| Install a CUDA build of torch | pip install torch gives MPS; no CUDA wheel exists |
| Cameras work once plugged in | Grant terminal camera access in Privacy and Security |
| Port names are stable | Hubs renumber ports on reboot and replug |
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| ls /dev/tty.usbmodem* prints nothing | Charge-only cable, or the plug is not seated. Swap to a data cable. |
| Both arms show the same kind of port name | Expected. Identify them by the unplug test or check_so101.py (Step 3). |
| A calibration or teleop command cannot find the profile | The --id does not match the one used at calibration. Check ~/.cache/huggingface/lerobot/calibration/. |
| One joint moves the wrong direction | Calibration or servo ID mismatch on that joint. Recalibrate that arm; check IDs on a DIY kit. |
| Camera capture returns black frames | macOS camera permission for your terminal (Step 7). |
| The arm stiffens as soon as a script connects | Expected: connect() enables torque. Passive scripts still send no motion. |
| Port worked yesterday, not today | The hub renumbered it. Re-run ls /dev/tty.usbmodem* and update $LEADER and $FOLLOWER. |
| A script refuses to run over a version or units stamp | Do not bypass it. Re-stamp in the environment you are running from. |
House rules for real hardware
These are enforced by the scripts in replay/, and they exist because each one was learned the expensive way.
- Pin one driver per data file. Every trajectory and policy records the driver, lerobot version, and calibration id it was made with, and the loader validates all three before anything moves. A mismatch stops the run loudly instead of falling back.
- Treat units as data. The units dictionary travels with the file and is asserted against the robot config at both ends.
- Never cut torque while the arm holds a commanded pose, because the arm drops. Ramp back to the rest pose read at connect time, then disconnect.
- Run the real
--dry-runfirst, every time the code or the trajectory changed. It exercises the whole path with sends suppressed. Note that--dry-lift-mmis a rehearsal that does move the arm. - Keep a hand near the power switch on first runs.
Scripts referenced here: replay/check_so101.py, replay/replay_on_so101.py, replay/run_act_on_so101.py. Sim teleop and cloud setup: Guide 2.
Guide 2Sim teleoperation: drive the Antioch sim with a real SO-101 leader arm
Hold a real leader arm, watch a physics-simulated SO-101 mirror your hand on a cloud GPU, and record demonstration episodes that are scored the moment you stop recording. Nothing runs locally except the arm driver, so your Mac needs no GPU, no Docker, and no Isaac install.
Step 0 · One-time setup
You need three things before the fun part.
- The project at
~/my-sim, with its own environment. - A separate environment that talks to the arm over USB. Keep it separate from the project environment, because mixing the two causes the most confusing errors in this whole pipeline.
- A calibrated leader arm. If yours has never been calibrated, follow Guide 1 through Step 5 and come back. The follower arm stays in its box for this guide, because the sim replaces it.
cd ~/my-sim && uv sync && .venv/bin/antioch auth loginconda create -n lerobot python=3.10 ffmpeg -c conda-forge -y
conda activate lerobot
pip install lerobot opencv-python~/my-sim/.venv/bin/antioch from inside ~/my-sim. A bare antioch on your PATH is a different binary that cannot see the project, and the CLI refuses to run outside a directory holding antioch.yaml.Step 1 · Two terminals
This pipeline needs two terminals at once, and it helps to know which is which before you start.
The sim session
Run from ~/my-sim with the project environment. It claims the cloud machine and keeps the simulation alive.
The bridge
Run in your (lerobot) environment. It reads the arm and streams joints into the machine that Terminal A claimed.
Step 2 · Find the leader port
Plug in the leader arm by itself, then capture the port in one command:
export LEADER=$(ls /dev/tty.usbmodem* | head -1) && echo $LEADERIt echoes something like /dev/tty.usbmodem58FA0836661. If it prints nothing, the cable is charge-only or unseated. If you have both arms plugged in, this picks one arbitrarily, so unplug the follower or identify the ports as described in Guide 1, Step 3.
Step 3 · Warm a machine and start the session
In Terminal A. The first command claims a cloud machine and warms it, which matters because a freshly claimed machine deadlocks on its first render and a teleop session started cold dies a few minutes in with no useful error:
cd ~/my-sim && .venv/bin/antioch scenario run --scenario so101_render_probe --timeout 300Then start a 30 minute session on that same machine. Leave off --machine entirely and the session uses the machine you just warmed:
.venv/bin/antioch scenario run --scenario so101_live_teleop \
--set max_seconds=1800 --verbose --timeout 1900The output prints [teleop] live for 1800s; waiting for /tmp/leader_stream.jsonl. The simulation is up and idling. Leave this terminal alone for the rest of the session.
Step 4 · Open the livestream
In a third shell, or Terminal B before you connect the arm, open the machine's viewport straight into your browser:
cd ~/my-sim && .venv/bin/antioch machine status | awk '/url/{print $2}' | xargs openA browser tab shows the arm, a green cube, and a blue tray. The stream starts when a viewer connects, so leave the tab open. Every new session on a new machine has a new URL, so re-run this command rather than reusing an old tab.
Step 5 · Connect the arm
In Terminal B, in your (lerobot) environment. One command, no machine id to look up, because the wrapper resolves the assigned machine itself:
~/my-sim/replay/teleop_pipe.sh $LEADERIt prints connecting to antioch-… and then # streaming at 30.0 Hz. Within about a second the sim arm snaps to your leader's pose. Move the leader and the sim follows.
Step 6 · Record episodes
The collection rhythm:
r → new cube position
s → recording starts
pick the cube up with the leader, place it in the tray
s → episode saved and auto-scored
r → againReset: the cube teleports to a random spot on the table
Start recording, and press again to stop and save
Stop the bridge; the sim session keeps running
After each stop, Terminal A prints episode N closed: success/fail, NNN frames. Success means the cube was lifted and ended inside the tray, judged by the same gates the automated test suites use, so it is a real score rather than a self-report.
Aim for quality over quantity: smooth approach, clean grasp, deliberate place. Ten to fifteen good episodes per session is a solid haul. Episodes upload as they close, so a session that dies loses at most the episode in progress.
Step 7 · Download your data
From ~/my-sim, once you are done. This resolves the newest run id for you:
.venv/bin/antioch scenario download $(.venv/bin/antioch scenario list --limit 1 \
| grep -oE '[0-9a-f]{32}' | head -1)You get epNNN_success.mp4, the robot's wrist camera view at 640×480 and 10 Hz, paired with epNNN_success.npz holding joint states and your leader commands aligned at 10 Hz. That pair is exactly what the training pipeline ingests, so Guide 3 picks up from here.
.npz are your commanded leader targets rather than the achieved joint positions. Train on it as it is, because a policy trained on achieved positions cannot hold a load.Why the pipe uses machine exec
This is the one detail worth understanding, because getting it wrong fails silently.
antioch machine exec forwards stdin into the container where the scenario runs. antioch machine ssh lands you in a separate ephemeral filesystem, so a /tmp/leader_stream.jsonl written there is invisible to the scenario.
The sim arm then never moves, with no error printed anywhere. The wrapper script uses machine exec for exactly this reason, which is why the recommended path is to run the wrapper rather than assemble the pipe by hand.
Troubleshooting
pkill inside the container, because that can wedge the exec channel and take the bridge down with it. Let sessions expire on max_seconds, or use antioch machine reset and then warm the machine again.| Symptom | Cause and fix |
|---|---|
| BrokenPipeError from the bridge | The pipe went through machine ssh instead of machine exec. Use teleop_pipe.sh. |
| no machine assigned from the wrapper | Terminal A is not running. Start the session first, because the wrapper only connects and never claims. |
| Assigning machine… waiting spinner | A stale machine id made the CLI claim a new machine. Stop it and let the wrapper resolve the id itself. |
| No antioch.yaml found | You ran an antioch command outside ~/my-sim. cd ~/my-sim first. |
| Session hangs at start and dies a few minutes in | The machine was cold. Run so101_render_probe on it, then relaunch the session. |
| Stream is fine but the sim arm ignores you | Check Terminal A for bridge header ok and a rising frame count. If neither appears, restart the bridge. |
| Pressed s and nothing recorded | The keypress went to the browser. Click Terminal B, then watch for the # record ON echo. |
| Stream URL is dead | The platform rotated machines, so the id and URL both changed. Restart from Step 3. |
| ls /dev/tty.usbmodem* shows nothing | Charge-only cable or a loose plug. Use a data-capable USB-C cable. |
Scripts referenced here: replay/teleop_pipe.sh, replay/leader_bridge.py, src/so101_live_teleop.py. Arm setup: Guide 1. Training on this data: Guide 3.
Guide 3Train your first SO-101 policy: ACT/Diffusion on sim data
Generate demonstration episodes in simulation, train an imitation policy on them, check it closed-loop in sim, and run it on a real arm. You need the my-sim project with Antioch access, plus a Python 3.10 env with lerobot, torch, and opencv-python. Your laptop needs no GPU and no Isaac install: simulation runs on cloud GPUs, and training runs locally on Apple Silicon (MPS).
Step 1 · Generate episodes
From the project root, run the scripted-expert suite. It replays a verified pick-and-place over randomized cube positions and sizes:
.venv/bin/antioch suite run wrist_cam_enabled --queue
.venv/bin/antioch scenario list --limit 5 # find the run ids
.venv/bin/antioch scenario download <run-id>About 30 episodes appear in roughly 4 minutes of suite time. Each episode is a pair of files: epNNN_success.npz with joint states and actions at 10 Hz, and epNNN_success.mp4 with the 640×480 wrist camera view.
If you would rather record human demonstrations, drive the sim arm with a real leader arm instead (Guide 2). The output format is the same; plan for 10 to 15 episodes per session.
actions array must hold the commanded joint targets rather than the measured positions. A policy trained on measured positions learns to command whatever pose the arm already has, so it stops producing force the moment it meets resistance and freezes under load. Both routes already record commands; keep it that way if you modify them.Step 2 · Build the dataset
Convert the episode pairs into a LeRobot dataset with exactly these fields:
| Field | Value |
|---|---|
observation.images.wrist | 640×480 RGB, 10 Hz |
observation.state | 6 joint positions, radians |
action | 6 next-step joint targets, radians |
task | string "Put the green cube in the tray." |
About 30 episodes of 240 steps each load without errors. That is enough for a first policy. Add more data only after eval shows you need it.
Step 3 · Train
Train ACT with the LeRobot default config (chunk 100), batch 8, inside your lerobot env:
lerobot-train --policy.type=act --dataset.repo_id=<your-dataset> \
--steps 15000 --batch_size 8 --save_freq 5000Set the budget to 15k steps rather than the 100k in the LeRobot tutorial. The tutorial assumes large, messy, human-collected datasets; 30 consistent scripted episodes converge much earlier. For Diffusion Policy, swap in --policy.type=diffusion and expect a longer wall clock (about 90 M parameters against ACT's 52 M).
Training runs at about 4 steps/s on MPS and finishes 15k steps in roughly an hour. The smoothed loss should end below about 20% of its value at step 500, with no NaN. Keep all three checkpoints; Step 4 decides which one is best.
Step 4 · Evaluate closed-loop in sim
A low training loss tells you the policy can imitate the dataset. It does not tell you the policy can do the task. To find out, export the checkpoint as TorchScript (trace it on torch 2.10; the remote's 2.11 loads it) and run the eval scenario. The policy takes the scripted expert's place at 10 Hz: it receives the wrist image and the measured joints, and it outputs joint targets. It receives nothing else.
The run scores against the same physics gates as the expert suite: the cube must end inside the tray within the tick budget.
| Eval set | Episodes | Pass bar |
|---|---|---|
| Seen positions (training bearings) | 10 | TBD |
| Held-out positions (midpoints) | 9 | TBD |
| Held-out sizes (47.5 / 52.5 mm) | 6 | reported |
A passing checkpoint clears both bars. If the seen set passes and held-out fails, add data diversity. If both fail, check the commanded-actions rule from Step 1 before you touch anything else.
Step 5 · Run on the real arm
The runner is replay/run_act_on_so101.py:
# stamp the policy with driver + lerobot version + calibration id
python replay/run_act_on_so101.py --stamp --id my_follower_arm# the full path with motion suppressed
python replay/run_act_on_so101.py --port /dev/tty.usbmodemXXX \
--id my_follower_arm --camera 0 --dry-runpython replay/run_act_on_so101.py --port /dev/tty.usbmodemXXX \
--id my_follower_arm --camera 0--dry-run prints every command it would send while moving nothing. The live run performs the task. The script also enforces the safety rules: it validates the stamped driver, lerobot version, and calibration id at load, asserts units at both ends, and ramps torque back to rest before it disconnects. Leave those checks alone.
Troubleshooting
| Symptom | Fix |
|---|---|
| Policy grasps, carries, then freezes mid-task | It was trained on measured positions. Rebuild the dataset from commanded targets (Step 1). |
| Identical failures across all checkpoints | The first camera frame was black. Warm-render before the first decision, and reuse the previous frame when a capture returns nothing. |
| First render run on a fresh cloud machine hangs | The machine is cold. Warm it with antioch scenario run --scenario so101_render_probe (about 25 s), then rerun. |
| Loss looks great, eval fails | This is expected sometimes. The loss curve does not predict closed-loop success; only Step 4 does. |
| Training crawls or crashes on Mac | Use plain pip install torch for MPS. There is no CUDA wheel for Mac, and you do not need one. |
| Real arm ignores the policy file | The version stamp does not match. Re-stamp (--stamp) in the env you will run with, using the calibration id you calibrated with. |
Code: replay/run_act_on_so101.py, replay/leader_bridge.py, src/so101_bench_tasks.py. Benchmark and results: sim2real_benchmark.md.
Guide 4Running Unitree robots on Antioch: Isaac Sim vs Isaac Lab
Spawn a Unitree quadruped or humanoid, give it a controller, and prove it works. The first decision is which engine you run on, because Isaac Sim and Isaac Lab expose completely different Python entry points for the same robot. This guide covers both, then walks the path from a stock asset to a verified walking robot.
Step 1 · Choose your engine
One project runs one engine, selected in three places that must agree: the antioch-sim extra in pyproject.toml, the image coordinate or Dockerfile in antioch.yaml, and the FROM line of that Dockerfile. Changing engines means editing all three and rebuilding the image.
The two engines are not two flavors of the same API. They are different libraries:
| Isaac Sim 6.0.1 (isaac-601-ga) | Isaac Lab 3.0 (isaac-lab-30b2) | |
|---|---|---|
| Simulation context | isaacsim.core.api.World | isaaclab.sim.SimulationContext(SimulationCfg(...)) |
antioch.world() | works | raises UnsupportedEngineError |
| Robot object | isaacsim.core.api.robots.Robot plus add_reference_to_stage | isaaclab.assets.Articulation(ArticulationCfg(...)) |
| Prebuilt robot configs | none; you point at a USD | isaaclab_assets.robots.unitree |
| Gains | authored in the USD DriveAPI, or set_gains at runtime | declared in the cfg's actuator groups |
| Commands | controller.apply_action(ArticulationAction(...)) | set_joint_position_target then write_data_to_sim |
| Camera | isaacsim.sensors.camera.Camera | isaaclab.sensors.Camera |
| Physics manager | the PhysX default works | needs isaaclab_ovphysx, see the gotchas below |
Choose Isaac Sim when you want a robot standing, driving, or running a pretrained policy, which is what this guide's verified runs use. Choose Isaac Lab when you want its ArticulationCfg library or its managers for training.
The same 15 lines on each engine
from isaacsim.core.api import World
from isaacsim.core.api.robots import Robot
from isaacsim.core.utils.stage import add_reference_to_stage
from isaacsim.core.utils.types import ArticulationAction
world = World(physics_dt=1/200, rendering_dt=1/200, stage_units_in_meters=1.0)
world.scene.add_ground_plane(z_position=0.0)
add_reference_to_stage(usd_path=usd, prim_path="/World/robot")
robot = world.scene.add(Robot(prim_path="/World/robot", name="robot",
position=np.array([0.0, 0.0, 0.42])))
world.reset()
controller = robot.get_articulation_controller()
controller.set_gains(kps=kp, kds=kd)
controller.apply_action(ArticulationAction(joint_positions=targets))
world.step(render=True)import isaaclab.sim as sim_utils
from isaaclab.actuators import ImplicitActuatorCfg
from isaaclab.assets import Articulation, ArticulationCfg
from isaaclab.sim import SimulationCfg, SimulationContext
from isaaclab_ovphysx.physics import OvPhysxCfg
sim = SimulationContext(SimulationCfg(dt=1/200, device="cuda:0",
physics=OvPhysxCfg()))
robot = Articulation(ArticulationCfg(
prim_path="/World/robot",
spawn=sim_utils.UsdFileCfg(usd_path=usd),
init_state=ArticulationCfg.InitialStateCfg(pos=(0.0, 0.0, 0.42)),
actuators={"all": ImplicitActuatorCfg(joint_names_expr=[".*"],
stiffness=25.0, damping=0.5)}))
sim.reset()
robot.set_joint_position_target(targets)
robot.write_data_to_sim()
sim.step(render=True)
robot.update(1/200)Step 2 · Check what already exists
Before building anything, find out whether your robot is already on the shelf. The Isaac 6.0 asset library carries these Unitree models:
A1 B2 Dex3 Dex5 G1 G1_23dof Go1 Go2 H1 H2 Z1 aliengo laikagoIsaac Lab additionally ships ArticulationCfg objects with tuned gains: UNITREE_A1_CFG, UNITREE_GO1_CFG, UNITREE_GO2_CFG, H1_CFG, G1_CFG, G1_MINIMAL_CFG, and G1_29DOF_CFG. Those configs live in isaaclab_assets, which is importable on the Lab engine only.
isaaclab_assets, so they need Step 4.To check for yourself rather than trusting a list, src/unitree_go2w_probe.py lists the library and the config module in one boot:
.venv/bin/antioch scenario run --scenario unitree_go2w_probe --verboseThe run prints the Unitree folder listing and whether the config module imported. On isaac-601-ga expect No module named 'isaaclab_assets', which is normal and tells you the gains must come from the USD or from your own table.
Step 3 · Spawn a stock robot
For a robot the library already has, point at its USD and let the engine spawn it. On Isaac Sim, resolve the asset root and reference the file:
from isaacsim.storage.native import get_assets_root_path
usd = f"{get_assets_root_path()}/Isaac/Robots/Unitree/Go2/go2.usd"On Isaac Lab, use the shipped config instead, which carries both the USD path and the tuned actuator groups:
from isaaclab_assets.robots.unitree import UNITREE_GO2_CFG
robot = Articulation(UNITREE_GO2_CFG.replace(prim_path="/World/robot"))The robot appears at its spawn height and holds a pose without folding. If it collapses, the drives carry placeholder gains, so continue to the bake in Step 4.
Step 4 · Build a robot the shelf does not have
This is the Go2W path, and it applies to any Unitree model missing from the library. Unitree publishes official URDFs in unitreerobotics/unitree_ros under robots/<model>_description, with real masses, limits, and geometry. The pipeline is fetch, patch, import, bake, and it lives in src/unitree_go2w_bake.py:
.venv/bin/antioch scenario run --scenario unitree_go2w_bake --verboseFour things about the importer on this engine, all measured rather than documented:
- Importer 3.11.2 has no Kit commands. The documented
URDFParseAndImportFilebelongs to an older generation. UseURDFImporter(URDFImporterConfig(...)).import_urdf()fromisaacsim.asset.importer.urdf. - Its
usd_pathis a directory, not a filename. Output lands at<usd_path>/<robot>/<robot>.usdawith payload files beside it, and the call returns the file it wrote. - It converts primitive collisions and silently drops mesh collisions. A Go2W imported as-is has no wheel colliders at all and rests on its belly, so author convex hulls on the visual meshes yourself, per mesh rather than on the parent Xform.
- Patch
package://mesh references to relative paths before importing.
The bake then writes gains into the USD DriveAPI so the file stands on its own. Two conversions matter: USD angular drives store stiffness and damping per degree while robot gain tables are per radian, so divide by 57.2958, and wheel joints take velocity drives with stiffness zero rather than position drives.
The run reports the joint count it baked, the wheel colliders it authored, and a stand plus drive result. Verified reference: run 93cd6ff55fdd4742b0411d0f2514e803 stands at 0.335 m and drives 0.90 m in 2.5 s with a wheel radius of 0.086 m.
Step 5 · Give it a controller
No pretrained locomotion policy ships with either engine for a wheeled quadruped, and Unitree's own RL repositories cover Go2, H1, and G1 rather than the W variants. The one public source of deployable checkpoints is fan-ziqi/rl_sar, which also deploys them to real robots over unitree_sdk2. It carries policies for go2, go2w, b2, b2w, g1, a1, lite3, tita, and others.
Read the contract from each policy's config.yaml rather than guessing it. The two we run differ in a way that matters:
| Go2W | G1 | |
|---|---|---|
| Joints | 16 — 12 legs and 4 wheels | 29 |
| Observation size | 57 | 96 |
| Legs | position targets, default + scale × action | position targets, all 29 |
| Wheels | velocity targets, stiffness 0 | none |
| Gains | legs kp 20 / kd 0.5, wheels kp 0 / kd 0.5 | per joint, kp 20 to 200 |
| Joint order | leg triplets FR, FL, RR, RL then wheels | base_joint_names[joint_mapping[i]] |
| Wheel positions in obs | zeroed, because the joints are continuous | not applicable |
Both are velocity-commanded at the operator level, meaning you send a body twist of forward speed, lateral speed, and yaw rate at 50 Hz on a 200 Hz sim. Set the policy's own gains on the articulation controller at runtime, which is what the real deployment does.
Verified reference runs, both on isaac-601-ga: b52b679e94db4b96aca4599b71b923a2 tracks a 0.5 m/s Go2W command at 96%, and 9dbf0a1627cc4a639f2127e1e8c090ba walks the G1 at 85% of a 0.4 m/s command with torso tilt under 5 degrees.
Step 6 · Gate it on physics, not on video
A robot that looks fine in a clip can be sliding on frictionless wheels or holding a pose the drives cannot actually produce. Every scenario here asserts measured quantities, and the useful gates for legged robots are base height above a floor, tilt off vertical, joint tracking error, and real displacement over ground for a commanded velocity.
Allow a grace window at the start of each segment. A dropped-in robot rocks once while the controller catches it, and on the Go2W that transient reaches about 27 degrees of tilt before settling under 4 degrees. Judging the caught state rather than the transient is the difference between a meaningful gate and a flaky one.
Engine gotchas
| Symptom | Cause and fix |
|---|---|
| Every video films a frozen robot while physics clearly moves | The Fabric scene delegate severs USD from PhysX. Boot with --/app/useFabricSceneDelegate=0, --/rtx/hydra/readTransformsFromFabricInRenderDelegate=0, --/physics/fabricEnabled=0, and call SimulationManager.enable_fabric(False) after world.reset(), because a GPU boot turns it back on inside World. |
| On Isaac Lab, commands appear to be ignored | The Lab beta's default physics manager drops command writes. Pass physics=OvPhysxCfg() from isaaclab_ovphysx. |
| No module named 'isaaclab_assets' | You are on the Isaac Sim engine. Read the gain values from the upstream config source and author them yourself, or switch engines. |
| import isaacsim.sensors fails on Isaac Lab | That Kit extension is not enabled by default there. Use isaaclab.sensors.Camera, which needs omni.replicator.core and the carb flag /isaaclab/cameras_enabled. |
| First render scenario on a fresh machine hangs | The machine is cold. Warm it with a light render scenario first, roughly 25 seconds. |
| A camera returns black frames | The near clip plane is at its default. Set it explicitly, for example 0.05 m, and check that the subject fits the vertical field of view at your working distance. |
| The robot falls through the floor | Mesh collisions were dropped at import. Author convex hulls on the visual meshes of every collider-less link, including feet. |
Verified runs
| Scenario | Run | Result |
|---|---|---|
unitree_go2w_probe | 2a277fa189fe44cc959a1f2dbab3671f | No Go2W in the library or in isaaclab_assets |
unitree_go2w_bake | 93cd6ff55fdd4742b0411d0f2514e803 | Stands 0.335 m, drives 0.90 m |
unitree_go2w_policy_eval | b52b679e94db4b96aca4599b71b923a2 | 96% velocity tracking |
unitree_go2w_follow | 8ff230388c444b77b7fe0c75e4260a48 | Camera-driven person following |
unitree_g1_walk | 9dbf0a1627cc4a639f2127e1e8c090ba | 85% velocity tracking |
Scenarios referenced here: src/unitree_go2w_probe.py, src/unitree_go2w_bake.py, src/unitree_go2w_policy.py, src/unitree_go2w_follow.py, src/unitree_g1_policy.py, and src/unitree_stand.py for the Isaac Lab bake pattern.