Fix GR00T N1.7 RTC action decoding

This commit is contained in:
Andrew Wrenn
2026-06-03 13:43:13 -07:00
committed by Andy Wrenn
parent 60e1474cf6
commit bed3747804
3 changed files with 259 additions and 12 deletions
+123 -1
View File
@@ -334,13 +334,15 @@ class _DummyGrootModel(nn.Module):
self.config = SimpleNamespace(compute_dtype="float32")
self.compute_dtype = "float32"
self.forward_inputs = None
self.get_action_options = None
def forward(self, inputs):
self.forward_inputs = dict(inputs)
return {"loss": self.weight + 1.0}
def get_action(self, inputs):
def get_action(self, inputs, options=None):
self.forward_inputs = dict(inputs)
self.get_action_options = options
batch_size = inputs["state"].shape[0]
return {"action_pred": torch.zeros(batch_size, 40, 132, device=self.weight.device)}
@@ -427,6 +429,35 @@ def test_groot_predict_action_chunk_accepts_rtc_kwargs():
signature.bind(object(), {}, inference_delay=2, prev_chunk_left_over=None)
def test_groot_predict_action_chunk_forwards_n1_7_rtc_prefix(monkeypatch):
from lerobot.policies.groot.groot_n1_7 import GR00TN17
dummy_model = _DummyGrootModel()
monkeypatch.setattr(GR00TN17, "from_pretrained", classmethod(lambda cls, **kwargs: dummy_model))
config = _groot_config(GROOT_N1_7)
policy = GrootPolicy(config)
policy.config.rtc_config = SimpleNamespace(execution_horizon=6)
prev_chunk = torch.arange(8 * 7, dtype=torch.float32).view(8, 7)
actions = policy.predict_action_chunk(
{"state": torch.zeros(1, 1, 132)},
inference_delay=3,
prev_chunk_left_over=prev_chunk,
)
assert actions.shape == (1, 40, 7)
assert dummy_model.get_action_options == {
"action_horizon": 8,
"rtc_overlap_steps": 6,
"rtc_frozen_steps": 3,
"rtc_ramp_rate": 6.0,
}
assert dummy_model.forward_inputs["action"].shape == (1, 8, 132)
torch.testing.assert_close(dummy_model.forward_inputs["action"][0, :, :7], prev_chunk)
torch.testing.assert_close(dummy_model.forward_inputs["action"][0, :, 7:], torch.zeros(8, 125))
def test_groot_from_pretrained_rejects_mismatched_caller_config(tmp_path):
model_path = tmp_path / "GR00T-N1.7-local"
model_path.mkdir()
@@ -593,6 +624,27 @@ def test_groot_n1_7_saved_processors_round_trip_checkpoint_specific_fields(tmp_p
assert decode_actions.raw_stats["action"]["gripper"]["q99"] == [115.0]
def test_converted_raw_n1_7_processors_load_without_legacy_action_unpack_override(tmp_path):
model_path = tmp_path / "libero_spatial"
_write_raw_n1_7_libero_checkpoint(model_path)
config = _raw_n1_7_libero_config(model_path)
preprocessor, postprocessor = make_pre_post_processors(config, pretrained_path=str(model_path))
save_dir = tmp_path / "converted_pretrained_model"
config.save_pretrained(save_dir)
preprocessor.save_pretrained(save_dir)
postprocessor.save_pretrained(save_dir)
loaded_preprocessor, loaded_postprocessor = make_pre_post_processors(
config,
pretrained_path=str(save_dir),
preprocessor_overrides={"rename_observations_processor": {"rename_map": {}}},
)
assert any(isinstance(step, GrootN17PackInputsStep) for step in loaded_preprocessor.steps)
assert any(isinstance(step, GrootN17ActionDecodeStep) for step in loaded_postprocessor.steps)
def test_groot_n1_7_pack_inputs_rejects_state_dim_above_core_max():
step = GrootN17PackInputsStep(
max_state_dim=2,
@@ -941,6 +993,76 @@ def test_groot_n1_7_action_decode_applies_named_libero_transform_from_modality_k
torch.testing.assert_close(output[TransitionKey.ACTION], expected)
def test_groot_n1_7_action_decode_truncates_to_valid_horizon_for_relative_stats():
arm_min = [[-1.0] * 5 for _ in range(16)]
arm_max = [[1.0] * 5 for _ in range(16)]
raw_stats = {
"state": {
"single_arm": _stats([0.0] * 5),
"gripper": _stats([0.0]),
},
"action": {
"single_arm": _stats([0.0] * 5),
"gripper": {
"min": [0.0],
"max": [10.0],
"mean": [5.0],
"std": [1.0],
"q01": [0.0],
"q99": [10.0],
},
},
"relative_action": {
"single_arm": {
"min": arm_min,
"max": arm_max,
"mean": [[0.0] * 5 for _ in range(16)],
"std": [[1.0] * 5 for _ in range(16)],
"q01": arm_min,
"q99": arm_max,
},
},
}
modality_config = {
"state": {
"modality_keys": ["single_arm", "gripper"],
},
"action": {
"delta_indices": list(range(16)),
"modality_keys": ["single_arm", "gripper"],
"action_configs": [
{"rep": "RELATIVE", "type": "NON_EEF", "format": "DEFAULT", "state_key": None},
{"rep": "ABSOLUTE", "type": "NON_EEF", "format": "DEFAULT", "state_key": None},
],
},
}
pack_step = GrootN17PackInputsStep(
raw_stats=raw_stats,
modality_config=modality_config,
normalize_min_max=False,
)
pack_step(
{
TransitionKey.OBSERVATION: {OBS_STATE: torch.zeros(1, 6)},
TransitionKey.COMPLEMENTARY_DATA: {},
}
)
decode_step = GrootN17ActionDecodeStep(
env_action_dim=6,
raw_stats=raw_stats,
modality_config=modality_config,
use_relative_action=True,
pack_step=pack_step,
)
output = decode_step({TransitionKey.ACTION: torch.zeros(1, 40, 6)})
decoded = output[TransitionKey.ACTION]
assert decoded.shape == (1, 16, 6)
torch.testing.assert_close(decoded[..., :5], torch.zeros(1, 16, 5))
torch.testing.assert_close(decoded[..., 5], torch.full((1, 16), 5.0))
def test_groot_n1_7_action_decode_requires_gripper_key_for_libero_transform():
step = GrootN17ActionDecodeStep(
env_action_dim=1,