Isaac GR00T N1.7

Results at a Glance

  • A first test of the latest NVIDIA Isaac GR00T N1.7 VLA model on the Trossen Stationary AI did not go smoothly! The robot learned the policy but it shook so violently it looked like it might destroy itself. EMA low-pass filtering, a longer action chunk, and some action clipping got it smoothed out.
  • After training on a transfer-cube dataset with only red cubes, a GR00T model was able to generalize the task to other cube colors.
  • GR00T was also able to learn the high dexterity bead-on-a-string task using the same smoothing and action chunk parameters used for the transfer cube task.
  • GR00T was able to include our robot's 4th (low) camera, but surprisingly this led to causal confusion.

Very First Experiment

Fig 1. Isaac GR00T N1.7 model rollout.
Note: dataset only contains red cubes!

We trained an Isaac GR00T N1.7 model on our red cube dataset for 30K training steps. As shown in Fig 1, the model successfully learned to transfer the cube, and it also generalized to other cube colors. This is our best result. Initially, the robot shook violently, but after some trial and error, we were able to smooth out the robot using a combination of EMA low-pass filtering, an increase in the action chunk length to 32, and robot action clipping. See the next section for implementation details. Note that our ACT models also required action clipping to smooth out their trajectories, while our openpi models did not seem to need any smoothing or clipping.

  • EMA low-pass filtering: At run time, actions at are filtered using a't = α*at + (1-α)*a't-1, where a' is the filtered action that is sent to the robot. After some trial and error, the best value was α=0.25.
  • Action chunk size: The best model was trained with the action chunk size set to 32 in the config file, trossen_ai_config_h32.py. Also, the open_loop_horizon option was set to 32 when running the robot using main_gr00t_trossen.py. The longer action chunks seem to be less noisy and also more reliable.
  • Action clipping: The Trossen robot parameter, max_relative_target, was set to 0.1 which keeps the maximum change in any action — joint value — below 0.1 radians. At this value, clipping happens only once in a while, but when it does, it prevents sudden large movements.
  • Real-time chunking: Introduced by Physical Intelligence, Real-Time Action Chunking (RTC) is designed to interpolate successive action chunks to create a smooth, consistent trajectory. GR00T has the RTC machinery, but to use it requires some mods to gr00t/policy/gr00t_policy.py and additions to examples/trossen_ai/main_gr00t_trossen.py. We tried RTC to solve the robot shaking problem but it did not help. We believe this is because intra-chunk noise was a bigger problem than inter-chunk discontinuities.
Back to top

High Dexterity Experiments

Fig 2. Isaac GR00T N1.7 model rollout.
Yes, GR00T can place a bead on a string, but still working on reliability.

We trained an Isaac GR00T N1.7 model on our 50 episode bead-on-a-string dataset for 30K training steps. The resulting model did not succeed at placing the bead on the string, although it did get close. We then performed two iterations of policy improvement using human interventions, adding 25 episodes per iteration. At each iteration, the model was trained for an additional 20K steps. As shown in Fig 2, the model successfully learned to place a bead on a string, although the success rate is only about 20% and is not yet reliably repeatable. This is therefore a preliminary result, but we are excited to see that GR00T is capable of performing this high dexterity task and of doing so smoothly!

  • Parameters: We found that exactly the same EMA smoothing, action clipping, and chunk size parameters that worked best for the cube transfer, above, worked best for bead-on-a-string too!
  • Causal confusion: Our model, even after policy improvement, gave us results which were extremely inconsistent. Sometimes, the robot would even get stuck in a micro-loop of states very close to each other, effectively a freeze. After eliminating all other causes it turned out that the model was using the low camera to rely on the horizontal position of the string as it hangs vertically while it is held by the left gripper before bead placement. In the original dataset this was a good signal, but in testing the string was not always vertical and this confused the model. This is a classic case of causal confusion (see paper) which arises because GR00T, unlike pi0.5, is able to include the low camera (at the center bottom of the Fig 2 video) as input. One would expect this additional sensory data to improve model performance, but what it does is add a strongly correlated and powerful signal -- the horizontal position of the string -- which is present in our original and augmented datasets. However, during testing the string was sometimes pulled off to the side and did not hang vertically. This messed up the horizontal position signal. So although this signal is correlated with actions in the training data, it is not actually a causal driver of the task. We hope to solve this problem by further augmenting the dataset with more human intervention episodes, but this time purposely adding natural variations in string position.
Back to top

Implementation Details

The code here uses our fork of Isaac GR00T N1.7. At the moment, the latest code is in the develop branch. The commands below are not an exhaustive set of instructions. You first need to install our develop branch or install the original repository directly from GR00T, and then cut and paste our mods in if you want to use them. We did not add any dependencies not in the original GR00T repository.

  • Config file: To train a GR00T model on a lerobot dataset for the Trossen Stationary AI robot, it is necessary to add a configuration file which tells GR00T what robot data formats to expect. The one we used in our training example is trossen_ai_config_h32.py:
  • # SPDX-FileCopyrightText: Copyright (c) 2026 ANRedlich. All rights reserved. # SPDX-License-Identifier: Apache-2.0 # # Modality config for the Trossen Stationary AI bimanual robot. # # Robot details: # - 2 arms x 7 joints each (joint_6 is the gripper on each arm) # - 14-dim concatenated state and action vectors # - 4 cameras: cam_high, cam_low, cam_left_wrist, cam_right_wrist # # Layout of the 14-dim state/action vectors (per meta/info.json column names): # indices 0-5 : left_joint_0 .. left_joint_5 (left_arm) # index 6 : left_joint_6 (left_gripper) # indices 7-12 : right_joint_0 .. right_joint_5 (right_arm) # index 13 : right_joint_6 (right_gripper) from gr00t.configs.data.embodiment_configs import register_modality_config from gr00t.data.embodiment_tags import EmbodimentTag from gr00t.data.types import ( ActionConfig, ActionFormat, ActionRepresentation, ActionType, ModalityConfig, ) trossen_ai_config_h32 = { # 4 cameras: high (third-person), low (third-person), left wrist, right wrist. # delta_indices=[0] = current frame only (no temporal history). "video": ModalityConfig( delta_indices=[0], modality_keys=["cam_high", "cam_low", "cam_left_wrist", "cam_right_wrist"], ), # State: split the 14-dim qpos into 4 named blocks for per-block normalization. # Splitting separates the gripper (0-0.044 m linear stroke) from the arm joints # (radians) so each gets its own normalization range. "state": ModalityConfig( delta_indices=[0], modality_keys=["left_arm", "left_gripper", "right_arm", "right_gripper"], ), # Action: 32-step (was 16) prediction horizon. Same key structure as state. # ABSOLUTE matches the openpi training convention used previously for this robot. "action": ModalityConfig( #delta_indices=list(range(0, 16)), delta_indices=list(range(0, 32)), modality_keys=["left_arm", "left_gripper", "right_arm", "right_gripper"], action_configs=[ # left_arm — 6 joint angles ActionConfig( rep=ActionRepresentation.ABSOLUTE, type=ActionType.NON_EEF, format=ActionFormat.DEFAULT, ), # left_gripper — finger displacement (meters) ActionConfig( rep=ActionRepresentation.ABSOLUTE, type=ActionType.NON_EEF, format=ActionFormat.DEFAULT, ), # right_arm ActionConfig( rep=ActionRepresentation.ABSOLUTE, type=ActionType.NON_EEF, format=ActionFormat.DEFAULT, ), # right_gripper ActionConfig( rep=ActionRepresentation.ABSOLUTE, type=ActionType.NON_EEF, format=ActionFormat.DEFAULT, ), ], ), # Language: task description from tasks.jsonl, indexed by task_index in parquet. "language": ModalityConfig( delta_indices=[0], modality_keys=["annotation.human.task_description"], ), } register_modality_config(trossen_ai_config_h32, embodiment_tag=EmbodimentTag.NEW_EMBODIMENT)
  • Modality file: It is necessary to add a modality.json file inside the meta directory of each lerobot dataset, as in our transfer-cube dataset. Here is our modality.json file:
  • { "state": { "left_arm": { "start": 0, "end": 6 }, "left_gripper": { "start": 6, "end": 7 }, "right_arm": { "start": 7, "end": 13 }, "right_gripper": { "start": 13, "end": 14 } }, "action": { "left_arm": { "start": 0, "end": 6 }, "left_gripper": { "start": 6, "end": 7 }, "right_arm": { "start": 7, "end": 13 }, "right_gripper": { "start": 13, "end": 14 } }, "video": { "cam_high": { "original_key": "observation.images.cam_high" }, "cam_low": { "original_key": "observation.images.cam_low" }, "cam_left_wrist": { "original_key": "observation.images.cam_left_wrist" }, "cam_right_wrist": { "original_key": "observation.images.cam_right_wrist" } }, "annotation": { "human.task_description": { "original_key": "task_index" } } }
  • Dataset download: The GR00T training script expects a local filesystem path rather than a HuggingFace repo ID for --dataset-path. Therefore, we used huggingface-cli download to move our dataset to demo_data/ before training.
  • Training: Here is the command line we used for training our transfer-cube dataset with action chunk size 32:
  • uv run --no-sync python gr00t/experiment/launch_finetune.py \ --base-model-path nvidia/GR00T-N1.7-3B \ --dataset-path demo_data/trossen_transfer_40mm_cube_02 \ --embodiment-tag NEW_EMBODIMENT \ --modality-config-path examples/trossen_ai/trossen_ai_config_h32.py \ --num-gpus 1 \ --output-dir ./outputs/trossen_full_h32 \ --max-steps 30000 \ --save-steps 10000 \ --save-total-limit 10 \ --global-batch-size 32 \ --learning-rate 1e-4 \ --episode-sampling-rate 1.0 \ --dataloader-num-workers 4 \
  • Running the policy: To serve the policy:
  • uv run --no-sync python gr00t/eval/run_gr00t_server.py \ --model-path outputs/trossen_full_h32 \ --embodiment-tag NEW_EMBODIMENT \ --device cuda:0 \ --host 127.0.0.1 \ --port 5555
  • Running the robot: The following script uses the learned model to control the robot. It sends robot and camera states to the policy server which sends back the next action to take:
  • python examples/trossen_ai/main_gr00t_trossen.py \ --mode autonomous \ --task_prompt "transfer the cube" \ --max_relative_target 0.1 \ --open_loop_horizon 32 \ --action_chunk_size 32 \ --max_steps 1000 \ --action_smooth_alpha 0.25 \ --diagnostics
  • RTC: Although RTC did not smooth out the robot for the transfer-cube example, it might be useful in the future. Here are the RTC options that we believe should work best. We also reduced open_loop_horizon to 24 because the last 8 steps out of 32 are overlap steps which should not be run. Otherwise, those actions would be either repeated or partly repeated by the first 8 steps of the next chunk:
  • --open_loop_horizon 24 \ --use_rtc \ --rtc_overlap_steps 8 \ --rtc_frozen_steps 2 \ --rtc_ramp_rate 5.0 \
    Back to top