isaac_ros_unitree_g1_gr00t#
Source code available on GitHub.
Overview#
Deploy launch for running a fine-tuned GR00T N policy on the Unitree G1.
The user-facing entry point is unitree_g1_gr00t_agile.launch.py in this
package. It wraps the generic unitree_g1_inference_graph.launch.py from
unitree_g1_bringup with the gr00t_n17_apple_to_plate controller
group preselected (unless overridden), exposes gr00t_leapp_yaml_path to
point at any LEAPP export without copying into ROS package data trees,
and adds two GR00T-specific pieces:
A diffusion-policy initial-noise publisher that seeds GR00T’s diffusion sampler.
On real hardware, the RealSense D435 composable node loaded into the inference pipeline container so frames stay in-process with
InputBuilderand Triton.
For a narrative walk-through of deploying a fine-tuned policy, see Deploy the Fine-Tuned Policy.
Tutorial: Deploy a Fine-Tuned GR00T Policy#
This tutorial walks through deploying a LEAPP-exported fine-tuned GR00T policy on the Unitree G1, both in MuJoCo simulation and on real hardware.
Prerequisites#
For real-hardware deployment on Jetson AGX Thor, complete the host-level
setup in Real-Hardware Prerequisite Details before you start the Isaac
ROS environment with isaac-ros activate. The setup enables CUDA MPS
(Multi-Process Service), mounts the MPS directories into the container, and
adds --ulimit rtprio=99 for the 200 Hz controller. MuJoCo simulation
does not require these steps.
Set Up Development Environment#
Set up your development environment by following the instructions in getting started.
(Optional) Install dependencies for any sensors you want to use by following the sensor-specific guides.
Note
We strongly recommend installing all sensor dependencies before starting any quickstarts. Some sensor dependencies require restarting the development environment during installation, which will interrupt the quickstart process.
Build isaac_ros_unitree_g1_gr00t#
If your container was built with the gr00t_workflow Docker layer, you can skip step 1
Install and build
isaac_ros_unitree_g1_gr00t:Activate the Isaac ROS environment:
isaac-ros activateInstall the prebuilt Isaac ROS Debian package.
ros-jazzy-isaac-ros-unitree-g1-gr00tpulls in the deploy stack (isaac-ros-deploy,unitree-g1-bringup, etc.) transitively:sudo apt-get update
sudo apt-get install -y ros-jazzy-isaac-ros-unitree-g1-gr00t
Install the ROS build-farm dependencies —
ros-jazzy-mujoco-ros2-controlfor simulation,ros-jazzy-realsense2-cameraandros-jazzy-realsense2-camera-msgs(same version, install together) for real hardware:sudo apt-get install -y \ ros-jazzy-mujoco-ros2-control \ ros-jazzy-realsense2-camera \ ros-jazzy-realsense2-camera-msgs source /opt/ros/jazzy/setup.bash
Install Git LFS:
sudo apt-get install -y git-lfs && git lfs install
Clone this repository under
${ISAAC_ROS_WS}/src:cd ${ISAAC_ROS_WS}/src && \ git clone -b release-4.5 https://github.com/NVIDIA-ISAAC-ROS/isaac_ros_physical_ai.git isaac_ros_physical_ai
Activate the Isaac ROS environment:
isaac-ros activateUse
rosdepto install the package’s dependencies:sudo apt-get update
rosdep update && rosdep install --from-paths ${ISAAC_ROS_WS}/src/isaac_ros_physical_ai/isaac_ros_unitree_g1_gr00t --ignore-src -y
Build the package from source:
cd ${ISAAC_ROS_WS}/ && \ colcon build --symlink-install --packages-up-to isaac_ros_unitree_g1_gr00t --base-paths ${ISAAC_ROS_WS}/src/isaac_ros_physical_ai/isaac_ros_unitree_g1_gr00t
Source the ROS workspace:
Note
Make sure to repeat this step in every terminal created inside the Isaac ROS environment. Because this package was built from source, the enclosing workspace must be sourced for ROS to be able to find the package’s contents.
source install/setup.bash
Inside the Isaac ROS environment, download and install the bundled GR00T Unitree G1 policy assets:
ros2 run isaac_ros_gr00t_unitree_g1_install install_gr00t_unitree_g1.sh --eula
This stores the model files and LEAPP YAML under
${ISAAC_ROS_WS}/isaac_ros_assets/models/gr00t_unitree_g1/. The default launch path uses these installed assets when present. Run this command in the same container/workspace used for launch so the asset paths and file permissions match the runtime environment.
Launch#
ros2 launch isaac_ros_unitree_g1_gr00t unitree_g1_gr00t_agile.launch.py \
hardware_type:=mujoco
We have separate blend ratios for the lower and upper body. The lower body blend ratio controls the AGILE locomotion policy and defaults to 1.0 in sim such that the robot will start balancing immediately.
The upper body blend ratio controls the GR00T policy and defaults to 0.0.
You can enable the upper body by setting the blend ratio to 1.0.
ros2 param set /safety_controller_upper_body blend_ratio 1.0
We have separate blend ratios for the lower and upper body. The lower body
blend ratio controls the AGILE locomotion policy, the upper body blend ratio
controls the GR00T policy. Both default to 0.0. This allows us to
individually enable the policies.
Warning
Before operating on real hardware:
Clear the work area of persons and hazards.
Both safety controllers default to
blend_ratio=0.0on real hardware — do not raise either until you are ready.Have the disable commands ready in your shell history (see Disable below).
Position the robot at the table. Stand G1 directly in front of the workspace with the table in reach, and rest the hands flat on top of the table before enabling any controller. Keep the hands above the tabletop while you enable the lower-body controller first and then the upper-body controller — if they’re below the table at any point, they catch on the edge.
Run the network setup as described on isaac_ros_unitree_g1_teleop_bringup if you have not already.
Launch the deploy stack:
ros2 launch isaac_ros_unitree_g1_gr00t unitree_g1_gr00t_agile.launch.py \ hardware_type:=real \ network_interface:=<your_interface>
Bring up balance — enable AGILE legs first:
ros2 param set /safety_controller_lower_body blend_ratio 1.0
Confirm the robot is stably balancing before proceeding. This is a good time to verify the camera, see Verify the Camera View below.
Enable the upper-body policy once balance is stable and the work area is clear:
ros2 param set /safety_controller_upper_body blend_ratio 1.0
Disable arms while keeping legs balancing (recommended abort):
ros2 param set /safety_controller_upper_body blend_ratio 0.0
To disable everything (full stop, robot will fall without external support):
ros2 param set /safety_controller_lower_body blend_ratio 0.0
Tip
Keep both disable commands in your shell history so you can execute them quickly if something goes wrong.
Verify the Camera View#
The deploy launch publishes /realsense_d435_rgb/color/image_raw
itself (via the RealSense composable node), so verify the camera
after launching while both safety controllers are still at
blend_ratio=0.0 (the robot is idle). In a separate container
terminal:
rviz2
Add → By topic → /realsense_d435_rgb/color/image_raw/Image
→ OK. Confirm the live frame shows the apple, plate, and table
within reach. Adjust the camera mount or scene as needed before
ramping up the safety controllers.
Expected Results#
The bundled apple-to-plate GR00T N1.7 policy is provided as a reference for the end-to-end fine-tuning and deployment workflow, not as a production-grade manipulation policy. On the reference Unitree G1 setup it completes the apple-to-plate task with a success rate of approximately 60%–70%. To obtain a higher success rate, fine-tune a policy on your own data and deploy it as described in Use Your Own Policy.
Note
After completing the apple-to-plate task, the G1 may move its hands into an
unusual backward position rather than returning to a clean neutral posture,
and the upper-body safety controller must be set back to blend_ratio=0.0
before the next run. This is a known limitation of the current bundled GR00T
N1.7 policy and does not affect task completion.
Use Your Own Policy#
The launch commands above use a pre-trained GR00T policy that is fine-tuned for the apple-to-plate task by default. To deploy your own exported LEAPP bundle, place the export in a directory that is mounted into the container. Keep the LEAPP YAML and ONNX files in the same directory.
Make the bundle world-readable inside the runtime environment. Some exports
ship *.onnx.data as mode 600, which causes ONNX Runtime permission
errors:
chmod -R a+rX /path/to/your/leapp_export_dir
Then append gr00t_leapp_yaml_path to the launch command to override the
default policy:
gr00t_leapp_yaml_path:=/path/to/leapp_export/your_policy.yaml
For full staging details — copying LEAPP bundles, permissions, semantics of
gr00t_leapp_yaml_path, and advanced controller_groups.yaml overrides —
see Stage Your Own Policy below.
Stage Your Own Policy#
To deploy your LEAPP export instead of the bundled apple-to-plate policy,
prefer gr00t_leapp_yaml_path so you never copy ONNX weights into ROS
install trees or edit controller_groups.yaml purely to change the YAML
location.
Recommended: pass the YAML path
Place your export anywhere the container can read — for example under
<ISAAC_ROS_WS>/policies/on the deploy machine, which is bind-mounted into the container by default. Copy the bundle from your workstation, replacing<ISAAC_ROS_WS>with the path to your Isaac ROS workspace on the deploy machine:scp -r /path/to/exported_bundle <deploy-user>@<deploy-host>:<ISAAC_ROS_WS>/policies/
Note
Keep the whole bundle together — every
.onnxplus its matching.onnx.dataweight file must sit next to the YAML, or Triton fails to load.Make bundles world-readable inside the runtime environment (exports often ship
*.onnx.dataas mode600, which causes ONNX Runtime permission errors):chmod -R a+rX /path/to/your/leapp_export_dir
Launch with
gr00t_leapp_yaml_pathset to the YAML file itself (.yamlsuffix),~expands to the invoking user’s home dir:ros2 launch isaac_ros_unitree_g1_gr00t unitree_g1_gr00t_agile.launch.py \ hardware_type:=mujoco \ gr00t_leapp_yaml_path:=/path/to/leapp_export/your_policy.yaml \ publish_rate:=5.0
The MuJoCo scene has no manipulation objects — the pipeline loads but has nothing coherent to manipulate.
ros2 launch isaac_ros_unitree_g1_gr00t unitree_g1_gr00t_agile.launch.py \ hardware_type:=real \ network_interface:=<your_interface> \ gr00t_leapp_yaml_path:=/path/to/leapp_export/your_policy.yaml \ publish_rate:=5.0
Replace
<your_interface>with the actual interface name (e.g.eno1). Don’t paste literal angle brackets — bash treats<oddly.
Semantics:
Leaving
gr00t_leapp_yaml_pathempty (default) uses the YAML from the installed GR00T asset directory when present, otherwise the packagedata/YAML fromcontroller_groups.yamlfor whicheverinitial_controller_groupis active.The argument drives the deploy inference graph (
InputBuilder, Triton,OutputBuilder) plus the diffusion initial_noise publisher. When the selected group combines AGILE legs with GR00T (split blend +agile_configpresent), ROS 2 Control’sinference_controllercontinues loading the AGILE ONNX YAML; GR00T comes only from the graph. For*_no_agilegroups where the controller group pointsinference_controllerat GR00T,gr00t_leapp_yaml_pathreplaces that ROS 2 YAML as well — keep YAML and ONNX co-located either way.
publish_rate ticks InputBuilder/Triton inputs (Hz); default 5.0 for
these launches to leave headroom beside AGILE. Raise toward 10.0 when the GPU
budget allows fresher observations; lower if the inference graph overruns next to
balanced walking.
If you need non-default source_to_topic mappings or a forked controller
list, maintain a workspace overlay of unitree_g1_bringup and change
controller_groups.yaml there, then set initial_controller_group at launch.
The policy graph still points at gr00t_leapp_yaml_path when set (or at the
built-in config: path in that entry).
Real-Hardware Prerequisite Details#
Real-hardware deployment requires two host-level prerequisites on the Jetson AGX Thor.
CUDA MPS (Multi-Process Service)#
When GR00T runs alongside the real-time AGILE balancing policy (200 Hz),
both compete for GPU resources. Without isolation, the heavy GR00T
inference starves AGILE, causing controller overruns and the robot
falling. CUDA MPS partitions GPU threads between the processes;
CUDA_MPS_ACTIVE_THREAD_PERCENTAGE=20 is set automatically by the
launch file for the AGILE controller.
One-time setup (on the Thor host, outside the container):
Install a systemd unit so MPS starts on boot:
sudo tee /etc/systemd/system/nvidia-cuda-mps.service << 'EOF'
[Unit]
Description=NVIDIA CUDA MPS Daemon
After=nvidia-persistenced.service
Requires=nvidia-persistenced.service
[Service]
Type=forking
Environment=CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
Environment=CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log
ExecStartPre=/bin/mkdir -p /tmp/nvidia-mps /tmp/nvidia-log
ExecStart=/usr/bin/nvidia-cuda-mps-control -d
ExecStop=/bin/bash -c "echo quit | /usr/bin/nvidia-cuda-mps-control"
[Install]
WantedBy=multi-user.target
EOF
sudo systemctl daemon-reload
sudo systemctl enable --now nvidia-cuda-mps.service
Mount the MPS socket into the Isaac ROS dev container by adding the
following lines to ~/.isaac_ros_dev-dockerargs on the Thor host:
-v /tmp/nvidia-mps:/tmp/nvidia-mps
-v /tmp/nvidia-log:/tmp/nvidia-log
Then recreate the container (isaac-ros activate).
Real-Time Scheduling for ros2_control#
The controller manager needs FIFO scheduling priority to hit 200 Hz without
jitter. The container’s rtprio ulimit defaults to 0, which blocks
this. Add the following to ~/.isaac_ros_dev-dockerargs on the Thor
host:
--ulimit rtprio=99
Then recreate the container. Inside the container, confirm:
ulimit -r
The output should be 99.
API#
Usage#
ros2 launch isaac_ros_unitree_g1_gr00t unitree_g1_gr00t_agile.launch.py
Launch Arguments#
Launch Argument |
Type |
Default |
Description |
|---|---|---|---|
|
|
|
Absolute path to the LEAPP export YAML (filename ending in
|
|
|
|
Overrides |
|
|
|
If |
|
|
|
Hardware platform. Options: |
|
|
|
Enable the MuJoCo viewer GUI. Only used when
|
|
|
|
Absolute path to the MuJoCo scene XML. Only used when
|
|
|
|
Start Foxglove Studio bridge for visualization. |
|
|
|
Rate at which |
|
|
|
Publish commanded joint positions as a ghost robot. |
|
|
|
Network interface for G1 communication. Only used when
|
Troubleshooting#
Verify MPS Is Reachable From the Container#
From inside the Isaac ROS dev container:
ls /tmp/nvidia-mps/
You should see control, control_lock, and related files. If the
directory is empty, the mounts in ~/.isaac_ros_dev-dockerargs were not
applied, or the MPS daemon is not running on the host.
On the host:
echo get_server_list | nvidia-cuda-mps-control
A running daemon responds with a server-list line. If the command fails, start MPS manually:
export CUDA_MPS_PIPE_DIRECTORY=/tmp/nvidia-mps
export CUDA_MPS_LOG_DIRECTORY=/tmp/nvidia-log
mkdir -p $CUDA_MPS_PIPE_DIRECTORY $CUDA_MPS_LOG_DIRECTORY
sudo -E nvidia-cuda-mps-control -d
Controller Overruns / Robot Falls#
If the AGILE controller reports overruns or the robot fails to maintain balance under GR00T load on real hardware, verify:
The CUDA MPS daemon is running on the host and its socket is mounted into the container (see above).
ulimit -rinside the container prints99, not0.