The head of the GR00T N1.7 on SO-100 training guide on AY-Robots with its spec strip showing GPU tier, dataset format and trainer defaults
deploymentinferencecheckpointslerobotgr00tso-100

From Checkpoint to Running Arm: The Last Mile

AY-Robots ResearchAugust 23, 202624 min read

A finished checkpoint does not move a servo. The real file paths, the config keys that must match, the three deployment stacks and the errors that eat a day.

Training finishes. The loss curve looks reasonable, a directory full of safetensors appears, and nothing at all happens to the robot. The last mile, taking a finished checkpoint and getting it to drive a real SO-100, is where most of a day disappears, and it is almost never the model's fault. It is a camera key spelled differently, an embodiment tag that does not exist, a backbone the checkpoint still needs to download, or an execution horizon longer than the chunk the model predicts.

This page walks that mile for the three stacks that ship today: lerobot's lerobot-rollout CLI, NVIDIA's Isaac-GR00T policy server, and openpi's websocket server. Real paths, real flags, real error text. Every command, file name and number below was read out of the upstream repositories and model cards on 24 August 2026, and where a stack changed recently the version is named. If you have not trained anything yet, train your first policy first and come back with a checkpoint in hand.

What you need to know

  • A checkpoint is weights plus a config plus normalization statistics. Copy only the weights and the arm still moves, just to the wrong place.
  • The most common last-mile failure is a key mismatch: the camera name your dataset used is not the camera name the policy expects.
  • A GR00T fine-tune saves its modality config into the checkpoint and loads it back automatically at inference. lerobot policies carry feature names in config.json, and --rename_map bridges the difference.
  • Run open-loop evaluation against a training episode before you power the arm. If the predicted curve does not track ground truth there, hardware will not rescue it.
  • lerobot-rollout --strategy.type=base is the shortest path from checkpoint to moving arm. Every other strategy is that same loop plus recording or human takeover.
  • Latency decides the architecture. NVIDIA measures GR00T N1.7 at 85.8 ms per inference on an H100 in PyTorch eager and 27.9 ms with the full TensorRT pipeline, at four denoising steps with one camera.
  • On AY-Robots the control loop runs 20 ms per action step for ACT and 485 ms for Pi0.5. Public-internet round trips on top of that turn a working policy into a hesitant one.

What training actually left you

Three trainers, three different trees on disk. None of them is a single file, and the difference between them is the reason a checkpoint that works on the training pod does nothing on the robot machine. The table below is what each stack writes and what reads it back.

StackWhere the checkpoint landsFiles that matterWhat loads it
lerobot (ACT, SmolVLA, Pi0.5, GR00T N1.7 via the groot policy type)outputs/train/<job_name>/checkpoints/<step>/pretrained_model/config.json, model.safetensors, train_config.json, policy_preprocessor.json and policy_postprocessor.json with their normalizer and unnormalizer safetensorslerobot-rollout --policy.path=<dir or Hub id>
Isaac-GR00T gr00t/experiment/launch_finetune.py<--output-dir>/checkpoint-<step>/config.json, embodiment_id.json, model-0000N-of-0000M.safetensors, model.safetensors.index.json, processor_config.json, statistics.jsongr00t/eval/run_gr00t_server.py --model-path=<dir>
openpi (JAX Pi0 / Pi0.5)checkpoints/<config>/<exp_name>/<step>/params and train_state, plus an assets directory holding the normalization statisticsscripts/serve_policy.py policy:checkpoint --policy.config= --policy.dir=

The base GR00T N1.7 repository on Hugging Face shows the shape clearly. This is the complete file list of nvidia/GR00T-N1.7-3B as the Hugging Face model API returned it on 24 August 2026, license and readme files included, because knowing which entries are noise is half the point.

text
.gitattributes
EXPLAINABILITY.md
LICENSE
PRIVACY.md
README.md
SAFETY_and_SECURITY.md
SUCCESS
config.json
embodiment_id.json
experiment_cfg/conf.yaml
experiment_cfg/config.yaml
experiment_cfg/dataset_statistics.json
experiment_cfg/final_model_config.json
experiment_cfg/final_processor_config.json
experiment_cfg/initial_actions.npz
latest
model-00001-of-00002.safetensors
model-00002-of-00002.safetensors
model-architecture.png
model.safetensors.index.json
processor_config.json
scheduler.pt
statistics.json
trainer_state.json
training_args.bin
wandb_config.json
zero_to_fp32.py
Complete file listing of the nvidia/GR00T-N1.7-3B repository, read from the Hugging Face model API on 24 August 2026.

Which of those does a fine-tune of your own actually need on the robot machine? NVIDIA's deployment guide answers it by example: when it pulls a checkpoint down to run inference, it fetches config.json, embodiment_id.json, the model-*.safetensors shards, model.safetensors.index.json, processor_config.json and statistics.json. That is the set to copy. trainer_state.json, scheduler.pt, training_args.bin and wandb_config.json are training bookkeeping. statistics.json is the one nobody misses until it is missing: it carries the normalization statistics that turn raw network output back into joint angles.

Your checkpoint is not self-contained

GR00T N1.7 uses nvidia/Cosmos-Reason2-2B (a Qwen3-VL architecture) as its vision-language backbone, and the Isaac-GR00T README states plainly that every GR00T checkpoint, the base nvidia/GR00T-N1.7-3B included, loads that gated repository on first use. On a fresh pod without a token the load fails with GatedRepoError or a 401. Request access on the model page and authenticate with hf auth login (or export HF_TOKEN) before you go anywhere near the arm. The same applies to lerobot's --policy.base_model_path=nvidia/GR00T-N1.7-3B.

The five things that have to match

A policy is a function from a named dictionary of observations to a named dictionary of actions. Deployment is the job of making the robot produce exactly the dictionary the checkpoint was trained on. Five things decide whether it does.

What must matchWhere it is definedWhat you see when it does not
Camera keysGR00T: meta/modality.json, where each video key carries an original_key such as observation.images.front. lerobot: the input features in the policy config.jsonThe policy runs and looks almost sane, but ignores the view that mattered. The gripper is usually the first casualty.
State and action keysGR00T: the index ranges in modality.json plus the modality config registered under NEW_EMBODIMENT. lerobot: dataset feature namesPredicted curve is flat or constant. NVIDIA lists this exact symptom as a modality key mismatch rather than a model problem.
Embodiment tag--embodiment-tag at fine-tune time. For NEW_EMBODIMENT the modality config is saved into the checkpoint and loaded automatically at inferenceAn error listing every known tag. Posttrain tags pointed at the base model fail outright, and a tag from a different robot fails on the state keys.
Execution horizon against predicted chunk lengthGR00T: --execution-horizon at rollout, which must stay at or below action_horizon in the model config (40 for the base N1.7 checkpoint). lerobot RTC: --inference.rtc.execution_horizon, which is the number of steps blended with the previous chunk, not the same quantity despite the same nameGR00T refuses an execution horizon above the predicted chunk length. Too long and the arm commits to stale plans, too short and you pay for inference more often.
Normalization statisticsGR00T: statistics.json. lerobot: policy_preprocessor.json and policy_postprocessor.json plus their safetensors. openpi: the assets directoryEnormous or NaN error, and an arm that drives straight into a joint limit.
The numbered run steps on the AY-Robots GR00T N1.7 on SO-100 training guide, showing dataset selection, hyperparameters and checkpoint output
The step list on /train/groot-n1-7-on-so-100. The deployment questions on this page start where that list ends: the checkpoint exists, now what reads it.

Camera keys deserve their own paragraph because the fix is unintuitive. lerobot ships a rename map, a JSON dictionary passed on the command line that renames observation keys inside the preprocessor pipeline so the policy always sees the names baked into its config. The convention is always source key first, policy key second. It is supported for Pi0, Pi0.5, Pi0Fast, SmolVLA and XVLA, and it renames observations only: action keys are untouched.

The trap that eats a day

A rename map written backwards does not announce itself. lerobot/pi0fast-libero expects observation.images.base_0_rgb and observation.images.left_wrist_0_rgb; your recording probably says observation.images.front and observation.images.wrist. Write --rename_map='{"observation.images.front": "observation.images.base_0_rgb"}', not the reverse. Only listed keys are renamed and everything else passes through unchanged, so a mapping whose source key does not exist in your data simply renames nothing, the policy never receives the view it expects, and the gripper never closes. If your dataset has fewer cameras than the policy expects, set --policy.empty_cameras=N, which adds masked placeholder image features instead of making you feed the model black frames.

Route A: lerobot-rollout, one command from checkpoint to motion

In current lerobot, lerobot-rollout is the single CLI for deploying a trained policy on a real robot, and the imitation-learning guide now points at it rather than at the recording command. It covers ACT, SmolVLA, Pi0.5 and GR00T N1.7 through one interface. Two things you choose: a strategy, which is the shape of the control loop, and an inference backend, which is how chunks are produced.

  1. 1
    Install the deployment extras on the robot machine

    The training environment and the robot environment are not the same install. You need the serial driver for the servos and, if you want the live view, the visualization extra. This is the exact line the GR00T policy page uses before its hardware rollout.

    bash
    pip install "lerobot[feetech,viz]"
  2. 2
    Find the camera indices, do not guess them

    USB camera indices move between reboots and between machines. Enumerate them, then write the index into the rollout command explicitly.

    bash
    lerobot-find-cameras opencv
  3. 3
    Run the base strategy for a fixed duration

    No recording, no dataset, no Hub push. This is the smallest thing that proves the checkpoint drives the arm. Keep a hand near the power switch for the first run.

    bash
    lerobot-rollout \
      --strategy.type=base \
      --policy.path=${HF_USER}/my_policy \
      --robot.type=so100_follower \
      --robot.port=/dev/ttyACM0 \
      --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30}}" \
      --task="Put lego brick into the box" \
      --duration=60 \
      --display_data=true
  4. 4
    Read the cadence report instead of guessing

    Every strategy reports what its control loop actually achieved against the --fps target, and so do lerobot-record, lerobot-teleoperate and lerobot-replay, because they pace through the same timer. The breakdown names observe, process_obs, infer, send and record. A high infer share points at the policy or the device, a high observe share points at the cameras, and pacing headroom near zero means the loop is saturated and the next slow tick will cost a frame.

    text
    Cadence summary - whole run, 2 episodes - target 30 Hz x 2 (16.7 ms tick slot, 33.3 ms cycle budget)
      effective cadence: 29.84 Hz policy / 59.67 Hz commands over 40.2 s measured
      cycles over the 33.3 ms work budget: 12/1197 (1.0%) - work mean 18.7 ms, worst 48.1 ms
      loop-body steps (share of measured work):
        observe      mean   3.13 ms -  31.7% of work
        process_obs  mean   0.50 ms -   5.1% of work
        infer        mean   5.18 ms -  52.4% of work
        send         mean   0.40 ms -   4.1% of work
        record       mean   0.67 ms -   6.8% of work
      pacing headroom: 7.4 ms slept per tick on average
--strategy.typeWhat the loop doesUse it when
baseAutonomous execution, nothing recordedFirst run, demos, latency measurement
episodicEpisode-oriented recording that mirrors lerobot-record, with an optional teleoperator driving during the reset phaseBuilding an evaluation set with clean episode boundaries
sentryContinuous autonomous recording with periodic Hub upload; policy state persists across episode boundaries and the robot does not reset between themLong unattended runs you want on the Hub
highlightRuns continuously with a ring buffer of the last N seconds (default 30); press s to flush the buffer and save the interesting windowCatching rare failures without recording hours of nothing
daggerAlternates policy execution with human takeover through a teleoperator; intervention frames are tagged intervention=TrueFixing a policy that drifts, without recording from scratch

The second choice is the backend. The default is synchronous: one policy call per control tick, and the loop blocks until the action comes back. That is fine for ACT at 20 ms per step and painful for a 3 B parameter VLA. Real-Time Chunking runs a background thread that produces the next action chunk while the current one is still being consumed, and guides the new chunk so its opening steps blend with the motion already under way. The technique comes from the Real-Time Execution of Action Chunking Flow Policies paper (Black, Galliker and Levine, June 2025) and lerobot exposes it as a backend flag.

bash
export MODEL_ID=your_trained_model_on_huggingface

lerobot-rollout \
  --strategy.type=base \
  --inference.type=rtc \
  --inference.rtc.execution_horizon=8 \
  --inference.queue_threshold=0 \
  --policy.path=$MODEL_ID \
  --policy.base_model_path=nvidia/GR00T-N1.7-3B \
  --policy.n_action_steps=8 \
  --robot.type=so101_follower \
  --robot.port=/dev/ttyACM0 \
  --robot.id=follower_robot \
  --robot.cameras="{ front: {type: opencv, index_or_path: 0, width: 640, height: 480, fps: 30, fourcc: \"MJPG\"}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30, fourcc: \"MJPG\"} }" \
  --task="place the vial in the rack" \
  --duration=60 \
  --device=cuda \
  --display_data=true
The GR00T N1.7 hardware rollout from the lerobot GR00T page. --policy.base_model_path is still required: the fine-tune does not carry the backbone. The queue threshold defaults to 30, and this page tells you to keep it at or below 5 for stable inference.
--policy.path and --policy.pretrained_path are not interchangeable

--policy.path loads weights and the checkpoint's config.json, so feature names, n_action_steps and every other stored setting come along. --policy.pretrained_path loads weights only, takes feature names from your dataset, and resets stored settings to their defaults; it also requires --policy.type, which --policy.path forbids. The lerobot Pi0.5 page spells out the consequence: a checkpoint trained with n_action_steps=10 and empty_cameras=1 silently falls back to 50 and 0 when it is loaded the second way.

A GR00T N1.5 checkpoint will not load in current lerobot

LeRobot removed GR00T N1.5 support: current releases handle N1.7 only, and N1.5 checkpoints and configs are rejected with a migration note. To keep running an existing N1.5 checkpoint, pin pip install 'lerobot==0.5.1'; to move forward, retrain against nvidia/GR00T-N1.7-3B. Both GR00T N1.5 and GR00T N1.7 are still trainable on AY-Robots, so check which one produced the checkpoint in your hand before you debug the loader.

Route B: the Isaac-GR00T policy server and a thin client

NVIDIA's own stack splits the problem in two. The policy runs inside a server process on the GPU; the robot side is a small client that speaks ZeroMQ with msgpack serialization and carries none of the model dependencies. Upstream gives two reasons for the split, and both are practical: the inference GPU does not have to be the machine holding the serial port, and the robot environment does not have to satisfy the model's dependencies. Isaac-GR00T ships a worked SO-100 example, which is the closest thing upstream has to this article.

  1. 1
    Check the checkpoint open-loop before touching hardware

    Predict on an episode the model was trained on and compare against ground truth. The script logs Average MSE and Average MAE across trajectories as unnormalized action error and saves a ground-truth-versus-predicted plot. NVIDIA deliberately publishes no target MSE, because the number does not transfer between datasets: what you read is plot overlap on a training trajectory first, then whether the error falls across successive checkpoints. Pass --save-plot-path explicitly, since the SO-100 guide says plots land in /tmp/open_loop_eval/traj_<id>.jpeg by default while the parameter table lists no default at all.

    bash
    uv run python gr00t/eval/open_loop_eval.py \
      --dataset-path examples/SO100/finish_sandwich_lerobot/izuluaga/finish_sandwich \
      --embodiment-tag NEW_EMBODIMENT \
      --model-path /tmp/so100_finetune/checkpoint-10000 \
      --traj-ids 0 \
      --execution-horizon 16 \
      --steps 400 \
      --modality-keys single_arm gripper \
      --save-plot-path /tmp/open_loop_eval
  2. 2
    Start the policy server on the GPU machine

    Bind to 0.0.0.0 only if the robot machine is elsewhere and the link is trusted; 127.0.0.1 plus an SSH tunnel is the safer default. The server prints its embodiment tag, model path, device and listening address before it accepts anything, which is the fastest way to catch a wrong checkpoint path.

    bash
    uv run python gr00t/eval/run_gr00t_server.py \
      --model-path /tmp/so100_finetune/checkpoint-10000 \
      --embodiment-tag NEW_EMBODIMENT \
      --device cuda:0 \
      --host 0.0.0.0 \
      --port 5555
  3. 3
    Run the SO-100 client in its own environment

    The client environment is created separately under gr00t/eval/real_robot/SO100 with its own uv sync, then the repo root is installed into it with --no-deps, precisely so the robot side does not pull in flash-attn, onnx and TensorRT. The keys in --robot.cameras have to be the video keys from modality.json (front and wrist in the SO-100 example), and the language instruction has to be the instruction the episodes were labelled with.

    bash
    cd "$GR00T_REPO/gr00t/eval/real_robot/SO100"
    uv sync
    uv pip install --no-deps -e ../../../../
    
    uv run --no-sync python eval_so100.py \
      --robot.type=so101_follower \
      --robot.port=/dev/ttyACM2 \
      --robot.id=orange_follower \
      --robot.cameras="{ front: {type: opencv, index_or_path: 6, width: 640, height: 480, fps: 30}, wrist: {type: opencv, index_or_path: 2, width: 640, height: 480, fps: 30}}" \
      --policy_host=localhost \
      --policy_port=5555 \
      --lang_instruction="finish the ham cheese olives sandwich"
Debug the loop with no model at all

Start the server with --dataset-path instead of --model-path and it replays recorded actions from that dataset instead of running the network. That is ReplayPolicy, and upstream recommends it as the first step when integrating a new environment: if the arm cannot replay a recorded episode cleanly, no policy will fix it. Give it the same --execution-horizon your environment executes, because that is how many dataset steps it advances per call. Note the client timeout too: timeout_ms defaults to 15000 to absorb cold-start model loading on the first call, and after that a timeout raises zmq.error.Again, which your control loop has to answer by retrying or halting.

Route C: openpi over a websocket

If you fine-tuned Pi0 or Pi0.5 with Physical Intelligence's own openpi rather than through lerobot, the deployment surface is a websocket server on port 8000 and a client package with deliberately minimal dependencies, installed into the robot environment from packages/openpi-client. The observation dictionary is flat and prefixed, the client resizes images itself to keep bandwidth down, and the server does the normalization. Note the shape of the return value: it is a whole action chunk, and upstream expects you to execute several steps of it open-loop before asking again.

python
from openpi_client import image_tools
from openpi_client import websocket_client_policy

# localhost and 8000 are the defaults
client = websocket_client_policy.WebsocketClientPolicy(host="localhost", port=8000)

observation = {
    "observation/image": image_tools.convert_to_uint8(
        image_tools.resize_with_pad(img, 224, 224)
    ),
    "observation/wrist_image": image_tools.convert_to_uint8(
        image_tools.resize_with_pad(wrist_img, 224, 224)
    ),
    "observation/state": state,
    "prompt": task_instruction,
}

# shape (action_horizon, action_dim)
action_chunk = client.infer(observation)["actions"]
The openpi remote inference client, from docs/remote_inference.md. 224 is the typical resize for pre-trained Pi0 models, and state can be passed unnormalized because the server normalizes.

The matching server command is uv run scripts/serve_policy.py policy:checkpoint --policy.config=<config name> --policy.dir=<checkpoint dir>, where the checkpoint directory looks like checkpoints/pi05_libero/my_experiment/20000. The config name is not decorative: it selects the input and output transforms that were used during training, the same classes that map your robot's observations into the model and its actions back out, so a checkpoint served under the wrong config produces well-formed nonsense. This is the openpi equivalent of GR00T's embodiment tag.

The errors in between

Collected from the upstream troubleshooting tables and from what actually goes wrong in the first hour. Read the first column, then check the third before you change anything about the model.

Message or symptomWhat it actually isFirst thing to check
GatedRepoError, or 401 Client Error on model loadThe gated Cosmos-Reason2-2B backbone that every GR00T checkpoint pulls on first useAccess granted on the model page, and a token present on the machine that loads the model
Address already in use when the policy server startsPort 5555, the default, is held by an earlier server you did not killPass --port, or find the stale process
zmq.error.Again on the clientThe server did not answer inside timeout_ms, default 15000Cold start on the first call is normal; repeated timeouts mean the GPU is too slow or the link is too long
An error listing all known embodiment tagsA typo, or a posttrain tag pointed at the base modelTags are case-insensitive, but posttrain tags require a fine-tuned checkpoint and the tag has to match the dataset
Predicted action curve is flat or constantmodality.json keys or --modality-config-path do not map your action keysThe modality config, not the learning rate
MSE extremely large, or NaN loss during trainingAction and state normalizationmeta/stats in the dataset, and whether the action ranges are physically sane
RuntimeError: Could not load libtorchcodec ... We support versions 4, 5, 6 and 7FFmpeg 8, which ships on Ubuntu 25.10 and newer, against the pinned torchcodec 0.8.0Install an FFmpeg below 8 and put its libraries on LD_LIBRARY_PATH
Tracks traj 0 well, fails on held-out episodesData scarcity, not a deployment bugEpisode count. NVIDIA calls this out explicitly for their five-episode demo set
The AY-Robots /fix index listing failure modes such as arm not detected, policy freezes mid-motion and gripper does not close, each linking to a dedicated page
The /fix index. Each entry is one failure mode with the checks in order, which is faster than re-deriving them at 2 a.m.

Several of these have a dedicated page: camera not detected, arm not detected, policy freezes mid-motion, loss falls but the policy does nothing and policy only works in one setup. The last one is worth reading before you conclude the deployment is broken, because a policy that works only under the exact lighting it was recorded in is a data problem wearing a deployment costume.

How fast it runs once it runs

Deployment is not finished when the arm moves; it is finished when the arm moves at a usable rate. NVIDIA publishes measured end-to-end timings for GR00T N1.7 at four denoising steps with one camera, comparing PyTorch eager against torch.compile against the full TensorRT pipeline. These are the model's own numbers on the GPU, data processing included, before any camera capture or serial cost. They are not AY-Robots figures and they are not a benchmark reproduced here.

DevicePyTorch eagertorch.compileTensorRT full pipelineRate with TensorRT
H100 80 GB HBM385.8 ms48.6 ms27.9 ms35.9 Hz
RTX Pro 6000 Blackwell78.4 ms50.7 ms27.9 ms35.9 Hz
L40128.3 ms69.0 ms38.4 ms26.0 Hz
DGX Spark126.4 ms108.8 ms98.6 ms10.1 Hz
AGX Thor112.8 ms102.3 ms80.4 ms12.4 Hz
Jetson Orin354.0 ms227.1 ms150.9 ms6.6 Hz

Two things stand out. The end-to-end speedup from TensorRT is real but bounded, from 1.28x on DGX Spark to 3.34x on the L40 in NVIDIA's own table, and it is largest on the cards that were already fastest. And the edge devices sit an order of magnitude behind the datacenter cards, which is the honest reason most SO-100 setups keep a GPU on the desk rather than on the robot.

The AY-Robots /policies comparison table listing the five trainable policies with parameter count, GPU tier, inference latency per action step and minimum episode count
The /policies table. Its latency column is the platform's own per-action-step figure, from 20 ms for ACT to 485 ms for Pi0.5, which is a different measurement from NVIDIA's per-inference benchmark above.
TensorRT engines are not portable

The engine build takes roughly 2 to 5 minutes depending on the GPU, and the engines are specific to the GPU architecture they were built on, so they must be rebuilt for a different card. The --batch-size value is baked in as a static dimension: engines built for one batch size cannot run at another, and changing it means re-running export and build. Budget for this if your inference host is a rented spot instance whose GPU model changes between runs.

Running inference on the robot machine instead of a remote GPU
Advantages
  • No network in the control loop, so the only latency is the model plus the cameras.
  • Nothing to bill by the hour once the card is paid for, and it works when the internet does not, which matters in a workshop more than it sounds.
  • One machine to configure. The single most common self-hosted failure, two machines with different ideas about the camera keys, cannot happen.
  • Predictable timing. A Jetson Orin at 150.9 ms with the TensorRT pipeline is slow, but a control loop prefers slow and steady to fast and variable.
Trade-offs
  • Edge devices are an order of magnitude off the datacenter cards in NVIDIA's own table: 150.9 ms on Jetson Orin against 27.9 ms on an H100.
  • The robot machine has to carry the full model stack, flash-attn and TensorRT included, instead of a thin ZeroMQ or websocket client.
  • You buy a card instead of renting one for an afternoon, and TensorRT engines have to be rebuilt whenever the card changes.
  • One local GPU serves one arm, where a server can serve several, as long as none of them has to be fast.

Doing it yourself vs doing it on AY-Robots

The manual path above is complete and it works. What it costs you is environment management: two Python environments, a gated backbone, a serial port, camera indices that renumber on reboot, and a GPU that has to be alive at the moment you want to test. Here is the same goal, both ways.

  1. Pull the checkpoint from wherever the trainer wrote it, with the config and the statistics file, not just the weights.
  2. Build an inference environment: CUDA, uv, an FFmpeg below 8 for torchcodec, and the model stack with its submodules.
  3. Request access to the gated backbone and put a Hugging Face token on that machine.
  4. Verify open-loop against a training episode and record the MSE and MAE as your baseline before anything touches hardware.
  5. Build a second, smaller environment on the robot machine for the client and the serial driver.
  6. Enumerate cameras, map their keys to what the checkpoint expects, and write the language instruction the episodes were labelled with.
  7. Start the server, tunnel if it is remote, then run 60 seconds with a hand on the power switch.
  8. Read the cadence report, and shut the GPU down when you walk away.
Two environments, one mistake

The most common self-hosted failure is not exotic: the robot client and the policy server end up with different ideas about the camera keys, because they were configured on different days on different machines. Write both configurations into one file and read it from both sides.

The AY-Robots CLI page showing install and run commands for driving the same training and inference operations from a terminal
The /cli page. Deployment from a terminal matters when the thing you are debugging is itself a terminal on the robot machine.

Where none of this helps

Worth saying plainly, because the failure is silent and gets blamed on the model. Inference latency on this platform runs from 20 ms per action step for ACT to 485 ms for Pi0.5, with GR00T N1.7 at 152 ms and SmolVLA at 245 ms. Those are the numbers before you add anything. A public-internet round trip added to a 485 ms step does not make the policy worse at the task; it makes the arm arrive late, over and over, which looks identical to a bad policy.

  • Remote inference is viable for slow pick-and-place. It is not viable for fast reactive motion, and no amount of chunking hides a link that is genuinely far away.
  • Nothing here rescues a dataset problem. A policy that only works under the lighting it was recorded in is a recording issue, and recording is where it gets fixed.
  • A LeRobot v3.0 dataset crashes the GR00T loader; it has to be converted down to v2.1 first, which is a training-side problem that surfaces as deployment-side confusion. The dataset format is worth checking before you blame the checkpoint.
  • GR00T N1.5, GR00T N1.7 and Pi0.5 are cloud-only on this platform. SmolVLA and ACT also run locally, which makes them the reasonable first target if you want the whole loop on your desk.
  • GR00T's fine-tuning entry point exposes no seed, so two GR00T runs on identical data are not bit-for-bit identical. Do not chase a deployment difference that is really run-to-run variance.

The failure modes, with the checks in order

Arm not detected, gripper does not close, policy freezes mid-motion, loss falls but nothing moves. Each one is a page with the diagnostic sequence rather than a list of guesses.

Open the fix index

Frequently asked questions

My checkpoint loads and the arm moves, but it moves to the wrong place. What is wrong?

Almost always normalization or key mapping, not the weights. Check that the statistics travelled with the weights: GR00T keeps them in statistics.json, lerobot in policy_preprocessor.json and policy_postprocessor.json plus their normalizer and unnormalizer safetensors, openpi in the assets directory next to params. If those are present, check the camera keys next. A policy that gets the wrong image for a view still produces confident, smooth, wrong motion.

Do I have to run open-loop evaluation before I run on hardware?

No, but it is the cheapest test you have. Predicting on an episode that was in the training set and comparing to ground truth separates a broken checkpoint from a broken robot setup in about a minute. NVIDIA's guidance is to read the plot overlap first and the error trend across checkpoints second, and deliberately not to chase a fixed target MSE, because that number does not transfer between datasets.

What should I set the execution horizon to?

In Isaac-GR00T it has to be at or below the model's predicted chunk length; the base GR00T N1.7 checkpoint predicts 40 steps, and fine-tuned checkpoints can differ. Isaac-GR00T uses 16 as the default for open-loop evaluation and calls 8 common for real-time deployment where the plan is refreshed often. Longer means fewer inference calls and more commitment to a stale plan, shorter means the opposite. Watch out for the name collision: lerobot's --inference.rtc.execution_horizon is how many steps get blended with the previous chunk, not how many are executed before replanning.

Can I run the policy server on a rented GPU and the robot at home?

Yes, and all three stacks are built for it: Isaac-GR00T uses a ZeroMQ server-client pair, openpi a websocket, and lerobot ships an asynchronous inference stack with its own policy server and robot client. Whether it is usable depends on the task. Adding a public-internet round trip to a step budget that is already 152 ms for GR00T N1.7 or 485 ms for Pi0.5 is fine for slow pick-and-place and not fine for anything reactive. Measure the round trip before you design around it.

Why does my GR00T checkpoint still want to download a model at inference time?

Because the fine-tune does not contain the vision-language backbone. Every GR00T N1.7 checkpoint loads nvidia/Cosmos-Reason2-2B on first use, and that repository is gated. On a machine with no token you get GatedRepoError or a 401, which reads like a checkpoint problem and is not one.

What does it cost to rent a GPU for this?

The platform publishes figures for training runs, not for inference pods: on the A100 80 GB or H100 tier a run is 3 to 6 hours at 1.20 to 2.00 USD per hour, about 4 to 12 USD; on the RTX 4090 or 24 GB tier it is 2 to 5 hours at 0.30 to 0.60 USD per hour, about 1 to 3 USD. What the platform does say about inference pods is that they carry an idle watchdog and destroy themselves after an idle period, which is the part that keeps a forgotten session from quietly running all weekend.

The short version

Getting a checkpoint to move an arm is four questions in order. Did the whole checkpoint arrive, config and statistics included? Does the observation dictionary the robot produces use the names the checkpoint expects? Does the checkpoint predict sensible actions on an episode it has already seen? And does the loop close fast enough for the task? Only the last one is about hardware. The first three are file paths and key names, which is why this is a tedious problem rather than a hard one.

If you want the surrounding context, the complete SO-100 guide covers assembly through training, collecting high-quality VLA training data covers the recording side that decides whether deployment can work at all, and the VLA overview covers why these models expect a language instruction in the first place. For the models themselves, the policy comparison has the parameter counts and latencies side by side, and the GR00T N1.7 on SO-100 guide is the training run that produces the checkpoint this page starts from.

Ready for high-quality robotics data?

AY-Robots connects your robots to skilled operators worldwide.

Get Started