From ff2828fba427fcc1632153534699a3f504f80fe1 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Jul 2026 09:09:49 -0500 Subject: [PATCH 1/6] feat(training): add GR00T policy support --- lelab/train.py | 40 ++++++++++++++++++++++++++++++++++++++++ lelab/utils/system.py | 3 +++ 2 files changed, 43 insertions(+) diff --git a/lelab/train.py b/lelab/train.py index bfdc0226..871f3491 100644 --- a/lelab/train.py +++ b/lelab/train.py @@ -18,6 +18,7 @@ lives in app/jobs.py. """ +import json import re from typing import TYPE_CHECKING @@ -35,6 +36,7 @@ class TrainingRequest(BaseModel): dataset_revision: str | None = None dataset_root: str | None = None dataset_episodes: list[int] | None = None + dataset_image_transforms_enable: bool = False # Policy configuration policy_type: str = "act" @@ -49,6 +51,7 @@ class TrainingRequest(BaseModel): log_freq: int = 250 save_freq: int = 1000 env_eval_freq: int = 0 + eval_steps: int = 0 save_checkpoint: bool = True # Output configuration @@ -79,6 +82,17 @@ class TrainingRequest(BaseModel): policy_push_to_hub: bool = False policy_repo_id: str | None = None + # GR00T-specific policy options (only emitted when policy_type == "groot"; + # these flags don't exist on other policy configs, so draccus would reject + # them). All optional — unset fields fall back to the groot config defaults. + policy_base_model_path: str | None = None + policy_embodiment_tag: str | None = None + policy_chunk_size: int | None = None + policy_n_action_steps: int | None = None + policy_use_relative_actions: bool | None = None + policy_relative_exclude_joints: list[str] | None = None + policy_use_bf16: bool | None = None + # Optimizer optimizer_type: str | None = "adam" optimizer_lr: float | None = None @@ -118,6 +132,8 @@ def build_training_command( cmd.extend(["--dataset.root", request.dataset_root]) if request.dataset_episodes: cmd.extend(["--dataset.episodes"] + [str(ep) for ep in request.dataset_episodes]) + if request.dataset_image_transforms_enable: + cmd.extend(["--dataset.image_transforms.enable", "true"]) # Policy cmd.extend(["--policy.type", request.policy_type]) @@ -143,10 +159,34 @@ def build_training_command( if request.policy_push_to_hub and request.policy_repo_id: cmd.extend(["--policy.repo_id", request.policy_repo_id]) + # GR00T-specific policy flags. These options only exist on the groot config, + # so emit them only for --policy.type=groot; sending them to another policy + # would make draccus abort the run. Each is skipped when unset so the groot + # config's own defaults apply. + if request.policy_type == "groot": + if request.policy_base_model_path: + cmd.extend(["--policy.base_model_path", request.policy_base_model_path]) + if request.policy_embodiment_tag: + cmd.extend(["--policy.embodiment_tag", request.policy_embodiment_tag]) + if request.policy_chunk_size is not None: + cmd.extend(["--policy.chunk_size", str(request.policy_chunk_size)]) + if request.policy_n_action_steps is not None: + cmd.extend(["--policy.n_action_steps", str(request.policy_n_action_steps)]) + if request.policy_use_relative_actions is not None: + cmd.extend( + ["--policy.use_relative_actions", "true" if request.policy_use_relative_actions else "false"] + ) + if request.policy_relative_exclude_joints is not None: + # draccus parses list values from a single JSON token, e.g. '["gripper"]'. + cmd.extend(["--policy.relative_exclude_joints", json.dumps(request.policy_relative_exclude_joints)]) + if request.policy_use_bf16 is not None: + cmd.extend(["--policy.use_bf16", "true" if request.policy_use_bf16 else "false"]) + # Logging / checkpointing cmd.extend(["--log_freq", str(request.log_freq)]) cmd.extend(["--save_freq", str(request.save_freq)]) cmd.extend(["--env_eval_freq", str(request.env_eval_freq)]) + cmd.extend(["--eval_steps", str(request.eval_steps)]) cmd.extend(["--save_checkpoint", "true" if request.save_checkpoint else "false"]) # Output. On HF Cloud the pod, not this host, runs the trainer: an absolute host diff --git a/lelab/utils/system.py b/lelab/utils/system.py index 128a0431..a4029303 100644 --- a/lelab/utils/system.py +++ b/lelab/utils/system.py @@ -200,6 +200,9 @@ def handle_install_wandb_extra_status() -> dict[str, Any]: "pi0": ("transformers", "lerobot[pi]"), "pi0_fast": ("transformers", "lerobot[pi]"), "diffusion": ("diffusers", "lerobot[diffusion]"), + # groot pulls in peft (LoRA), diffusers, timm, dm-tree and decord; peft is a + # reliable, cross-platform stand-in for "the lerobot[groot] extra is present". + "groot": ("peft", "lerobot[groot]"), } # One install manager per install target (lerobot[smolvla] / lerobot[pi] / …), From 0c04137cf1e412e7fb5cb71f7fd4107ae0ed8356 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Jul 2026 09:09:49 -0500 Subject: [PATCH 2/6] feat(frontend): add GR00T training configuration --- .../components/training/ConfigurationTab.tsx | 2 + .../training/config/EssentialsCard.tsx | 1 + .../components/training/config/GrootCard.tsx | 176 ++++++++++++++++++ frontend/src/components/training/types.ts | 11 ++ frontend/src/lib/jobsApi.ts | 11 ++ frontend/src/pages/Training.tsx | 15 ++ 6 files changed, 216 insertions(+) create mode 100644 frontend/src/components/training/config/GrootCard.tsx diff --git a/frontend/src/components/training/ConfigurationTab.tsx b/frontend/src/components/training/ConfigurationTab.tsx index 8106bb79..caa0ab8c 100644 --- a/frontend/src/components/training/ConfigurationTab.tsx +++ b/frontend/src/components/training/ConfigurationTab.tsx @@ -1,5 +1,6 @@ import React from 'react'; import EssentialsCard from './config/EssentialsCard'; +import GrootCard from './config/GrootCard'; import AdvancedCard from './config/AdvancedCard'; import TargetCard from './config/TargetCard'; import { ConfigComponentProps } from './types'; @@ -38,6 +39,7 @@ const ConfigurationTab: React.FC = ({ datasets={datasets} datasetsLoading={datasetsLoading} /> + ); diff --git a/frontend/src/components/training/config/EssentialsCard.tsx b/frontend/src/components/training/config/EssentialsCard.tsx index b99397ee..fac6b77d 100644 --- a/frontend/src/components/training/config/EssentialsCard.tsx +++ b/frontend/src/components/training/config/EssentialsCard.tsx @@ -92,6 +92,7 @@ const EssentialsCard: React.FC = ({ config, updateConfig, d Diffusion Policy PI0 SmolVLA + GR00T N1.7 TD-MPC VQ-BeT PI0 Fast diff --git a/frontend/src/components/training/config/GrootCard.tsx b/frontend/src/components/training/config/GrootCard.tsx new file mode 100644 index 00000000..c02fbe9f --- /dev/null +++ b/frontend/src/components/training/config/GrootCard.tsx @@ -0,0 +1,176 @@ +import React, { useEffect } from 'react'; +import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; +import { Input } from '@/components/ui/input'; +import { NumberInput } from '@/components/ui/number-input'; +import { Label } from '@/components/ui/label'; +import { Switch } from '@/components/ui/switch'; +import { ConfigComponentProps } from '../types'; + +// Sensible GR00T N1.7 defaults, seeded the first time the user picks the policy +// so the form matches the canonical fine-tuning command out of the box. +const GROOT_DEFAULTS = { + policy_base_model_path: 'nvidia/GR00T-N1.7-3B', + policy_embodiment_tag: 'new_embodiment', + policy_chunk_size: 16, + policy_n_action_steps: 16, + policy_use_relative_actions: true, + policy_relative_exclude_joints: ['gripper'], + policy_use_bf16: true, + dataset_image_transforms_enable: true, +} as const; + +const GrootCard: React.FC = ({ config, updateConfig }) => { + const isGroot = config.policy_type === 'groot'; + + // Seed defaults once when groot is selected. Each key is only written when + // still undefined, so the effect can't loop and never clobbers user edits. + useEffect(() => { + if (!isGroot) return; + (Object.keys(GROOT_DEFAULTS) as (keyof typeof GROOT_DEFAULTS)[]).forEach((key) => { + if (config[key] === undefined) { + updateConfig(key, GROOT_DEFAULTS[key] as never); + } + }); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isGroot]); + + if (!isGroot) return null; + + const excludeJoints = (config.policy_relative_exclude_joints ?? []).join(', '); + + return ( + + + GR00T N1.7 + + +
+ + + updateConfig('policy_base_model_path', e.target.value || undefined) + } + placeholder="nvidia/GR00T-N1.7-3B" + className="bg-slate-900 border-slate-600 text-white rounded-lg" + /> +

+ HuggingFace repo of the pretrained GR00T backbone to fine-tune. +

+
+ +
+
+ + + updateConfig('policy_embodiment_tag', e.target.value || undefined) + } + placeholder="new_embodiment" + className="bg-slate-900 border-slate-600 text-white rounded-lg" + /> +
+ +
+ + + updateConfig( + 'policy_relative_exclude_joints', + e.target.value + .split(',') + .map((s) => s.trim()) + .filter(Boolean), + ) + } + placeholder="gripper" + className="bg-slate-900 border-slate-600 text-white rounded-lg" + /> +

+ Comma-separated joints kept absolute when relative actions are on. +

+
+ +
+ + updateConfig('policy_chunk_size', v)} + className="bg-slate-900 border-slate-600 text-white rounded-lg" + /> +
+ +
+ + updateConfig('policy_n_action_steps', v)} + className="bg-slate-900 border-slate-600 text-white rounded-lg" + /> +
+
+ +
+
+ + updateConfig('policy_use_relative_actions', checked) + } + className="data-[state=checked]:bg-green-500" + /> + +
+ +
+ updateConfig('policy_use_bf16', checked)} + className="data-[state=checked]:bg-green-500" + /> + +
+ +
+ + updateConfig('dataset_image_transforms_enable', checked) + } + className="data-[state=checked]:bg-green-500" + /> + +
+
+
+
+ ); +}; + +export default GrootCard; diff --git a/frontend/src/components/training/types.ts b/frontend/src/components/training/types.ts index d0ee5f44..d459a7ef 100644 --- a/frontend/src/components/training/types.ts +++ b/frontend/src/components/training/types.ts @@ -41,6 +41,17 @@ export interface TrainingConfig { // Advanced configuration use_policy_training_preset: boolean; + + // GR00T-specific configuration (only used when policy_type === "groot"). + dataset_image_transforms_enable?: boolean; + eval_steps?: number; + policy_base_model_path?: string; + policy_embodiment_tag?: string; + policy_chunk_size?: number; + policy_n_action_steps?: number; + policy_use_relative_actions?: boolean; + policy_relative_exclude_joints?: string[]; + policy_use_bf16?: boolean; } export interface TrainingStatus { diff --git a/frontend/src/lib/jobsApi.ts b/frontend/src/lib/jobsApi.ts index 088b727b..fa9a262f 100644 --- a/frontend/src/lib/jobsApi.ts +++ b/frontend/src/lib/jobsApi.ts @@ -49,6 +49,17 @@ export interface TrainingRequest { optimizer_weight_decay?: number; optimizer_grad_clip_norm?: number; use_policy_training_preset: boolean; + // GR00T-specific (only sent when policy_type === "groot"); backend ignores + // the policy_* ones for other policies. + dataset_image_transforms_enable?: boolean; + eval_steps?: number; + policy_base_model_path?: string; + policy_embodiment_tag?: string; + policy_chunk_size?: number; + policy_n_action_steps?: number; + policy_use_relative_actions?: boolean; + policy_relative_exclude_joints?: string[]; + policy_use_bf16?: boolean; // Optional target for runner dispatch; omitted ⇒ local. target?: { runner: "local" | "hf_cloud"; flavor?: string }; } diff --git a/frontend/src/pages/Training.tsx b/frontend/src/pages/Training.tsx index a361d042..a74b2ab4 100644 --- a/frontend/src/pages/Training.tsx +++ b/frontend/src/pages/Training.tsx @@ -93,6 +93,21 @@ function configToRequest(c: TrainingConfig): TrainingRequest { optimizer_weight_decay: c.optimizer_weight_decay, optimizer_grad_clip_norm: c.optimizer_grad_clip_norm, use_policy_training_preset: c.use_policy_training_preset, + // GR00T-specific. Only forward the policy_* fields for groot so other + // policies never receive flags their config doesn't define. + dataset_image_transforms_enable: c.dataset_image_transforms_enable, + eval_steps: c.eval_steps, + ...(c.policy_type === "groot" + ? { + policy_base_model_path: c.policy_base_model_path, + policy_embodiment_tag: c.policy_embodiment_tag, + policy_chunk_size: c.policy_chunk_size, + policy_n_action_steps: c.policy_n_action_steps, + policy_use_relative_actions: c.policy_use_relative_actions, + policy_relative_exclude_joints: c.policy_relative_exclude_joints, + policy_use_bf16: c.policy_use_bf16, + } + : {}), }; } From f93dc2e3108959badd70a35beee32949e8bd4d06 Mon Sep 17 00:00:00 2001 From: Ben Date: Sun, 12 Jul 2026 09:09:49 -0500 Subject: [PATCH 3/6] fix(rollout): use RTC inference for relative actions --- lelab/rollout.py | 54 +++++++++++++++++++++++++++++++++++++++++++ tests/test_rollout.py | 31 +++++++++++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/lelab/rollout.py b/lelab/rollout.py index 0b34f31e..0f02ff9c 100644 --- a/lelab/rollout.py +++ b/lelab/rollout.py @@ -24,6 +24,7 @@ from __future__ import annotations import contextlib +import json import logging import os import re @@ -139,6 +140,53 @@ def _resolve_policy_path(policy_ref: str) -> str: raise ValueError(f"Unrecognised policy ref: {policy_ref!r}") +def _read_policy_config(policy_path: str) -> dict[str, Any]: + """Load pretrained_model/config.json if present.""" + config_path = Path(policy_path) / "config.json" + if not config_path.is_file(): + return {} + try: + with open(config_path) as fh: + data = json.load(fh) + return data if isinstance(data, dict) else {} + except Exception as exc: + logger.warning("Failed to read policy config at %s: %s", config_path, exc) + return {} + + +def _rollout_inference_args(policy_path: str) -> list[str]: + """Return extra lerobot-rollout flags for policies that reject sync inference. + + GR00T and other relative-action policies decode action chunks against the + observation state at inference time. The default sync backend calls + ``select_action`` per tick and can re-decode cached relative actions + against newer states, so lerobot requires the RTC/chunked rollout path + instead. + + GR00T also needs tuned RTC queue settings: the lerobot default + ``queue_threshold=30`` makes the background thread replan while a 16-step + chunk is still playing, which replaces the queue mid-motion and feels + like one action then a full recalculation.""" + cfg = _read_policy_config(policy_path) + policy_type = cfg.get("type") + needs_rtc = bool(cfg.get("use_relative_actions")) or policy_type == "groot" + if not needs_rtc: + return [] + + args = ["--inference.type=rtc"] + + n_action_steps = cfg.get("n_action_steps") + if isinstance(n_action_steps, int) and n_action_steps > 0: + args.append(f"--inference.rtc.execution_horizon={n_action_steps}") + elif policy_type == "groot": + args.append("--inference.rtc.execution_horizon=8") + + # Wait until the current chunk is consumed before replanning. GROOT docs + # recommend keeping this at 0 (never > 5) for stable real-robot rollout. + args.append("--inference.queue_threshold=0") + return args + + def _format_cameras_arg(cameras: dict[str, dict[str, Any]]) -> str: """Convert {name: {type, camera_index, width, height, fps}} into lerobot's CLI dict syntax. The frontend key `camera_index` is @@ -211,6 +259,11 @@ def _friendly_hint(error_text: str | None) -> str | None: return "A camera doesn't support the configured resolution — open camera settings and click Auto." if "permission" in low and ("port" in low or "com" in low): return "Couldn't open the serial port — close anything else using it, or run `lelab --stop`." + if "relative-action" in low or "relative chunk actions" in low: + return ( + "This policy was trained with relative actions and needs RTC (chunked) inference. " + "Update lelab and try again — recent versions enable that automatically." + ) return None @@ -291,6 +344,7 @@ def handle_start_inference(request: InferenceRequest) -> dict[str, Any]: f"--robot.id={follower_id}", f"--task={request.task}", f"--duration={request.duration_s}", + *_rollout_inference_args(policy_path), ] if request.cameras: cmd.append(f"--robot.cameras={_format_cameras_arg(request.cameras)}") diff --git a/tests/test_rollout.py b/tests/test_rollout.py index 5e445b70..0b41fd34 100644 --- a/tests/test_rollout.py +++ b/tests/test_rollout.py @@ -21,6 +21,8 @@ from __future__ import annotations +import json + import pytest @@ -312,11 +314,40 @@ def test_classify_outcome_ok_warns_and_fails() -> None: assert _classify_outcome(1, True, "DeviceNotConnectedError: follower is not connected") == "failed" +def test_rollout_inference_args_uses_rtc_for_relative_action_policies(tmp_path) -> None: + from lelab.rollout import _rollout_inference_args + + policy_dir = tmp_path / "pretrained_model" + policy_dir.mkdir() + (policy_dir / "config.json").write_text( + json.dumps({"type": "groot", "use_relative_actions": True, "n_action_steps": 16}), + encoding="utf-8", + ) + assert _rollout_inference_args(str(policy_dir)) == [ + "--inference.type=rtc", + "--inference.rtc.execution_horizon=16", + "--inference.queue_threshold=0", + ] + + +def test_rollout_inference_args_omits_rtc_for_absolute_policies(tmp_path) -> None: + from lelab.rollout import _rollout_inference_args + + policy_dir = tmp_path / "pretrained_model" + policy_dir.mkdir() + (policy_dir / "config.json").write_text( + json.dumps({"type": "act", "use_relative_actions": False}), + encoding="utf-8", + ) + assert _rollout_inference_args(str(policy_dir)) == [] + + def test_friendly_hint_maps_common_failures() -> None: from lelab.rollout import _friendly_hint assert "gripper" in (_friendly_hint("Motor overload detected") or "").lower() assert "connect" in (_friendly_hint("Failed to connect to the follower") or "").lower() + assert "rtc" in (_friendly_hint("GrootPolicy.select_action does not support relative-action policies") or "").lower() assert _friendly_hint("some unrecognised traceback") is None assert _friendly_hint(None) is None From cc8bb865412ba4ef7f568e47346fd6eadcdc0ae0 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 16 Jul 2026 08:55:07 -0500 Subject: [PATCH 4/6] Address GR00T PR review feedback --- frontend/src/components/training/types.ts | 1 - frontend/src/lib/jobsApi.ts | 1 - frontend/src/pages/Training.tsx | 1 - lelab/rollout.py | 2 +- lelab/train.py | 2 - tests/test_rollout.py | 16 ++++++++ tests/test_train.py | 47 +++++++++++++++++++++++ 7 files changed, 64 insertions(+), 6 deletions(-) diff --git a/frontend/src/components/training/types.ts b/frontend/src/components/training/types.ts index d459a7ef..cd4882dd 100644 --- a/frontend/src/components/training/types.ts +++ b/frontend/src/components/training/types.ts @@ -44,7 +44,6 @@ export interface TrainingConfig { // GR00T-specific configuration (only used when policy_type === "groot"). dataset_image_transforms_enable?: boolean; - eval_steps?: number; policy_base_model_path?: string; policy_embodiment_tag?: string; policy_chunk_size?: number; diff --git a/frontend/src/lib/jobsApi.ts b/frontend/src/lib/jobsApi.ts index fa9a262f..8cd82f70 100644 --- a/frontend/src/lib/jobsApi.ts +++ b/frontend/src/lib/jobsApi.ts @@ -52,7 +52,6 @@ export interface TrainingRequest { // GR00T-specific (only sent when policy_type === "groot"); backend ignores // the policy_* ones for other policies. dataset_image_transforms_enable?: boolean; - eval_steps?: number; policy_base_model_path?: string; policy_embodiment_tag?: string; policy_chunk_size?: number; diff --git a/frontend/src/pages/Training.tsx b/frontend/src/pages/Training.tsx index a74b2ab4..ca15ae0f 100644 --- a/frontend/src/pages/Training.tsx +++ b/frontend/src/pages/Training.tsx @@ -96,7 +96,6 @@ function configToRequest(c: TrainingConfig): TrainingRequest { // GR00T-specific. Only forward the policy_* fields for groot so other // policies never receive flags their config doesn't define. dataset_image_transforms_enable: c.dataset_image_transforms_enable, - eval_steps: c.eval_steps, ...(c.policy_type === "groot" ? { policy_base_model_path: c.policy_base_model_path, diff --git a/lelab/rollout.py b/lelab/rollout.py index 0f02ff9c..9e0243c5 100644 --- a/lelab/rollout.py +++ b/lelab/rollout.py @@ -179,7 +179,7 @@ def _rollout_inference_args(policy_path: str) -> list[str]: if isinstance(n_action_steps, int) and n_action_steps > 0: args.append(f"--inference.rtc.execution_horizon={n_action_steps}") elif policy_type == "groot": - args.append("--inference.rtc.execution_horizon=8") + args.append("--inference.rtc.execution_horizon=16") # Wait until the current chunk is consumed before replanning. GROOT docs # recommend keeping this at 0 (never > 5) for stable real-robot rollout. diff --git a/lelab/train.py b/lelab/train.py index 871f3491..257edeaf 100644 --- a/lelab/train.py +++ b/lelab/train.py @@ -51,7 +51,6 @@ class TrainingRequest(BaseModel): log_freq: int = 250 save_freq: int = 1000 env_eval_freq: int = 0 - eval_steps: int = 0 save_checkpoint: bool = True # Output configuration @@ -186,7 +185,6 @@ def build_training_command( cmd.extend(["--log_freq", str(request.log_freq)]) cmd.extend(["--save_freq", str(request.save_freq)]) cmd.extend(["--env_eval_freq", str(request.env_eval_freq)]) - cmd.extend(["--eval_steps", str(request.eval_steps)]) cmd.extend(["--save_checkpoint", "true" if request.save_checkpoint else "false"]) # Output. On HF Cloud the pod, not this host, runs the trainer: an absolute host diff --git a/tests/test_rollout.py b/tests/test_rollout.py index 0b41fd34..2cd0bc2a 100644 --- a/tests/test_rollout.py +++ b/tests/test_rollout.py @@ -342,6 +342,22 @@ def test_rollout_inference_args_omits_rtc_for_absolute_policies(tmp_path) -> Non assert _rollout_inference_args(str(policy_dir)) == [] +def test_rollout_inference_args_uses_groot_chunk_default_when_missing(tmp_path) -> None: + from lelab.rollout import _rollout_inference_args + + policy_dir = tmp_path / "pretrained_model" + policy_dir.mkdir() + (policy_dir / "config.json").write_text( + json.dumps({"type": "groot", "use_relative_actions": True}), + encoding="utf-8", + ) + assert _rollout_inference_args(str(policy_dir)) == [ + "--inference.type=rtc", + "--inference.rtc.execution_horizon=16", + "--inference.queue_threshold=0", + ] + + def test_friendly_hint_maps_common_failures() -> None: from lelab.rollout import _friendly_hint diff --git a/tests/test_train.py b/tests/test_train.py index 58fd0d15..be8a6537 100644 --- a/tests/test_train.py +++ b/tests/test_train.py @@ -175,3 +175,50 @@ def test_local_target_keeps_push_to_hub() -> None: assert _arg_value(cmd, "--policy.push_to_hub") == "true" assert _arg_value(cmd, "--policy.repo_id") == "me/x" assert "--job.target" not in cmd + + +def test_groot_policy_flags_are_emitted_only_for_groot() -> None: + from lelab.train import TrainingRequest, build_training_command + + req = TrainingRequest( + dataset_repo_id="x", + policy_type="groot", + policy_base_model_path="nvidia/GR00T-N1.7-3B", + policy_embodiment_tag="new_embodiment", + policy_chunk_size=16, + policy_n_action_steps=16, + policy_use_relative_actions=True, + policy_relative_exclude_joints=["gripper"], + policy_use_bf16=True, + ) + cmd = build_training_command(req, "/tmp/out") + + assert _arg_value(cmd, "--policy.base_model_path") == "nvidia/GR00T-N1.7-3B" + assert _arg_value(cmd, "--policy.embodiment_tag") == "new_embodiment" + assert _arg_value(cmd, "--policy.chunk_size") == "16" + assert _arg_value(cmd, "--policy.n_action_steps") == "16" + assert _arg_value(cmd, "--policy.use_relative_actions") == "true" + assert _arg_value(cmd, "--policy.relative_exclude_joints") == '["gripper"]' + assert _arg_value(cmd, "--policy.use_bf16") == "true" + + non_groot = build_training_command( + TrainingRequest( + dataset_repo_id="x", + policy_type="act", + policy_base_model_path="nvidia/GR00T-N1.7-3B", + policy_embodiment_tag="new_embodiment", + policy_chunk_size=16, + policy_n_action_steps=16, + policy_use_relative_actions=True, + policy_relative_exclude_joints=["gripper"], + policy_use_bf16=True, + ), + "/tmp/out", + ) + assert "--policy.base_model_path" not in non_groot + assert "--policy.embodiment_tag" not in non_groot + assert "--policy.chunk_size" not in non_groot + assert "--policy.n_action_steps" not in non_groot + assert "--policy.use_relative_actions" not in non_groot + assert "--policy.relative_exclude_joints" not in non_groot + assert "--policy.use_bf16" not in non_groot From b2cf6af9850f9289f10bec82875fe2997c91fbc7 Mon Sep 17 00:00:00 2001 From: Ben Date: Thu, 16 Jul 2026 08:55:07 -0500 Subject: [PATCH 5/6] Address GR00T PR review feedback --- frontend/src/components/training/config/GrootCard.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/components/training/config/GrootCard.tsx b/frontend/src/components/training/config/GrootCard.tsx index c02fbe9f..ea820f0e 100644 --- a/frontend/src/components/training/config/GrootCard.tsx +++ b/frontend/src/components/training/config/GrootCard.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useRef } from 'react'; import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'; import { Input } from '@/components/ui/input'; import { NumberInput } from '@/components/ui/number-input'; @@ -21,11 +21,13 @@ const GROOT_DEFAULTS = { const GrootCard: React.FC = ({ config, updateConfig }) => { const isGroot = config.policy_type === 'groot'; + const seededDefaults = useRef(false); // Seed defaults once when groot is selected. Each key is only written when // still undefined, so the effect can't loop and never clobbers user edits. useEffect(() => { - if (!isGroot) return; + if (!isGroot || seededDefaults.current) return; + seededDefaults.current = true; (Object.keys(GROOT_DEFAULTS) as (keyof typeof GROOT_DEFAULTS)[]).forEach((key) => { if (config[key] === undefined) { updateConfig(key, GROOT_DEFAULTS[key] as never); From 51cdc9123e868d6941d19b82a69006a6530f3776 Mon Sep 17 00:00:00 2001 From: Nicolas Rabault Date: Mon, 3 Aug 2026 15:58:14 +0200 Subject: [PATCH 6/6] style: wrap two lines over the ruff line limit --- lelab/train.py | 4 +++- tests/test_rollout.py | 7 ++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/lelab/train.py b/lelab/train.py index 257edeaf..637e304d 100644 --- a/lelab/train.py +++ b/lelab/train.py @@ -177,7 +177,9 @@ def build_training_command( ) if request.policy_relative_exclude_joints is not None: # draccus parses list values from a single JSON token, e.g. '["gripper"]'. - cmd.extend(["--policy.relative_exclude_joints", json.dumps(request.policy_relative_exclude_joints)]) + cmd.extend( + ["--policy.relative_exclude_joints", json.dumps(request.policy_relative_exclude_joints)] + ) if request.policy_use_bf16 is not None: cmd.extend(["--policy.use_bf16", "true" if request.policy_use_bf16 else "false"]) diff --git a/tests/test_rollout.py b/tests/test_rollout.py index 2cd0bc2a..746ba90f 100644 --- a/tests/test_rollout.py +++ b/tests/test_rollout.py @@ -363,7 +363,12 @@ def test_friendly_hint_maps_common_failures() -> None: assert "gripper" in (_friendly_hint("Motor overload detected") or "").lower() assert "connect" in (_friendly_hint("Failed to connect to the follower") or "").lower() - assert "rtc" in (_friendly_hint("GrootPolicy.select_action does not support relative-action policies") or "").lower() + assert ( + "rtc" + in ( + _friendly_hint("GrootPolicy.select_action does not support relative-action policies") or "" + ).lower() + ) assert _friendly_hint("some unrecognised traceback") is None assert _friendly_hint(None) is None