Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions frontend/src/components/training/ConfigurationTab.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -38,6 +39,7 @@ const ConfigurationTab: React.FC<ConfigurationTabProps> = ({
datasets={datasets}
datasetsLoading={datasetsLoading}
/>
<GrootCard config={config} updateConfig={updateConfig} />
<AdvancedCard config={config} updateConfig={updateConfig} />
</div>
);
Expand Down
1 change: 1 addition & 0 deletions frontend/src/components/training/config/EssentialsCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ const EssentialsCard: React.FC<EssentialsCardProps> = ({ config, updateConfig, d
<SelectItem value="diffusion">Diffusion Policy</SelectItem>
<SelectItem value="pi0">PI0</SelectItem>
<SelectItem value="smolvla">SmolVLA</SelectItem>
<SelectItem value="groot">GR00T N1.7</SelectItem>
<SelectItem value="tdmpc">TD-MPC</SelectItem>
<SelectItem value="vqbet">VQ-BeT</SelectItem>
<SelectItem value="pi0_fast">PI0 Fast</SelectItem>
Expand Down
178 changes: 178 additions & 0 deletions frontend/src/components/training/config/GrootCard.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
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';
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<ConfigComponentProps> = ({ 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(() => {
Comment thread
EAOZONE marked this conversation as resolved.
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);
}
});
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isGroot]);

if (!isGroot) return null;

const excludeJoints = (config.policy_relative_exclude_joints ?? []).join(', ');

return (
<Card className="bg-slate-800/50 border-slate-700 rounded-xl">
<CardHeader>
<CardTitle className="text-white">GR00T N1.7</CardTitle>
</CardHeader>
<CardContent className="space-y-6">
<div>
<Label htmlFor="policy_base_model_path" className="text-slate-300">
Base Model Path
</Label>
<Input
id="policy_base_model_path"
value={config.policy_base_model_path ?? ''}
onChange={(e) =>
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"
/>
<p className="text-xs text-slate-500 mt-1">
HuggingFace repo of the pretrained GR00T backbone to fine-tune.
</p>
</div>

<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<Label htmlFor="policy_embodiment_tag" className="text-slate-300">
Embodiment Tag
</Label>
<Input
id="policy_embodiment_tag"
value={config.policy_embodiment_tag ?? ''}
onChange={(e) =>
updateConfig('policy_embodiment_tag', e.target.value || undefined)
}
placeholder="new_embodiment"
className="bg-slate-900 border-slate-600 text-white rounded-lg"
/>
</div>

<div>
<Label htmlFor="policy_relative_exclude_joints" className="text-slate-300">
Relative Exclude Joints
</Label>
<Input
id="policy_relative_exclude_joints"
value={excludeJoints}
onChange={(e) =>
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"
/>
<p className="text-xs text-slate-500 mt-1">
Comma-separated joints kept absolute when relative actions are on.
</p>
</div>

<div>
<Label htmlFor="policy_chunk_size" className="text-slate-300">
Chunk Size
</Label>
<NumberInput
id="policy_chunk_size"
value={config.policy_chunk_size}
onChange={(v) => updateConfig('policy_chunk_size', v)}
className="bg-slate-900 border-slate-600 text-white rounded-lg"
/>
</div>

<div>
<Label htmlFor="policy_n_action_steps" className="text-slate-300">
Action Steps
</Label>
<NumberInput
id="policy_n_action_steps"
value={config.policy_n_action_steps}
onChange={(v) => updateConfig('policy_n_action_steps', v)}
className="bg-slate-900 border-slate-600 text-white rounded-lg"
/>
</div>
</div>

<div className="space-y-3">
<div className="flex items-center space-x-3">
<Switch
id="policy_use_relative_actions"
checked={config.policy_use_relative_actions ?? false}
onCheckedChange={(checked) =>
updateConfig('policy_use_relative_actions', checked)
}
className="data-[state=checked]:bg-green-500"
/>
<Label htmlFor="policy_use_relative_actions" className="text-slate-300">
Use Relative Actions
</Label>
</div>

<div className="flex items-center space-x-3">
<Switch
id="policy_use_bf16"
checked={config.policy_use_bf16 ?? false}
onCheckedChange={(checked) => updateConfig('policy_use_bf16', checked)}
className="data-[state=checked]:bg-green-500"
/>
<Label htmlFor="policy_use_bf16" className="text-slate-300">
Use bfloat16
</Label>
</div>

<div className="flex items-center space-x-3">
<Switch
id="dataset_image_transforms_enable"
checked={config.dataset_image_transforms_enable ?? false}
onCheckedChange={(checked) =>
updateConfig('dataset_image_transforms_enable', checked)
}
className="data-[state=checked]:bg-green-500"
/>
<Label htmlFor="dataset_image_transforms_enable" className="text-slate-300">
Enable Image Augmentations
</Label>
</div>
</div>
</CardContent>
</Card>
);
};

export default GrootCard;
10 changes: 10 additions & 0 deletions frontend/src/components/training/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,16 @@ export interface TrainingConfig {

// Advanced configuration
use_policy_training_preset: boolean;

// GR00T-specific configuration (only used when policy_type === "groot").
dataset_image_transforms_enable?: boolean;
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 {
Expand Down
10 changes: 10 additions & 0 deletions frontend/src/lib/jobsApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,16 @@ 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;
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 };
}
Expand Down
14 changes: 14 additions & 0 deletions frontend/src/pages/Training.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,20 @@ 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,
...(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,
}
: {}),
};
}

Expand Down
54 changes: 54 additions & 0 deletions lelab/rollout.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from __future__ import annotations

import contextlib
import json
import logging
import os
import re
Expand Down Expand Up @@ -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=16")

# 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
Expand Down Expand Up @@ -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


Expand Down Expand Up @@ -286,6 +339,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)}")
Expand Down
Loading