Skip to content

Commit 2ec6d37

Browse files
hyzhou-nvclaude
andcommitted
feat(action): add RoboCasa mobile-manipulation action-policy support
Adds RoboCasa to the action-policy path: dataset loader, dataset factory, Nano post-training config, embodiment registration, closed-loop evaluator, and the LeRobot v2.1 -> v3.0 dataset converter. RoboCasa is a mobile-manipulation benchmark, so unlike DROID and LIBERO the base is not fixed. The loader exposes a 15-D raw action contract that passes the native base command through unchanged: [base_motion(4), control_mode(1), eef_pos(3), eef_rot6d(6), gripper(1)] Observations are the agentview_left | eye_in_hand composite at full resolution, with EEF proprioception prepended as a clean condition token. data/generator/action/datasets/ RoboCasaLeRobotDataset and its factory configs/.../action_policy_robocasa_nano.py simulation/robocasa/ closed-loop evaluator, next to LIBERO's scripts/convert_robocasa_to_lerobot_v30.sh The action server gains support for the `use_state=True` contract the recipe trains with: the current end-effector pose is prepended to the action sequence as a clean conditioning frame, making it `chunk_length + 1` long. Without it a `use_state` checkpoint was served an all-zero `chunk_length` action tensor and the request's `"state"` field was ignored. Measured on target/atomic at iter 16000, 18 tasks x 20 rollouts, same weights either way: 226/360 without the state token against 287/360 with it. Requests that omit "state" are unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1 parent c14617c commit 2ec6d37

11 files changed

Lines changed: 2886 additions & 0 deletions

File tree

cosmos_framework/configs/base/config.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,7 @@ def make_config() -> Config:
9797
import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_droid_nano # noqa: F401
9898
import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_libero_all_nano # noqa: F401
9999
import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_libero_nano # noqa: F401
100+
import cosmos_framework.configs.base.experiment.action.posttrain_config.action_policy_robocasa_nano # noqa: F401
100101
import cosmos_framework.configs.base.experiment.action.posttrain_config.action_fd_droid_posttrain # noqa: F401
101102
import cosmos_framework.configs.base.experiment.sft.vision_sft_nano # noqa: F401
102103
import cosmos_framework.configs.base.experiment.sft.vision_sft_super # noqa: F401
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: OpenMDW-1.1
3+
4+
"""``action_policy_robocasa_nano`` — RoboCasa mobile-base action policy.
5+
6+
Trains Cosmos3-Nano into a mobile-manipulation policy on the 18 RoboCasa ``target/atomic``
7+
tasks. The action contract is 15-dimensional and carries RoboCasa's native base command
8+
unchanged (``base_encoding="raw"``)::
9+
10+
[base_motion(4), control_mode(1), eef_pos(3), eef_rot6d(6), gripper(1)]
11+
12+
``base_motion`` is the command sent to the base controller, so the closed-loop client writes it
13+
straight back (``env[7:11] = action[0:4]``) and a recorded demonstration replays exactly. It is
14+
already normalised to [-1, 1], i.e. the same scale as the arm block, which matters because
15+
``action_normalization=None``: the state token is absolute, so delta statistics do not apply and
16+
the raw channel scales are what the loss sees.
17+
18+
Observation is ``camera_set="left_wrist"`` — ``agentview_left`` and ``eye_in_hand`` concatenated
19+
horizontally at their native 256x256 each — plus end-effector proprioception prepended as a
20+
clean condition token (``use_state=True``). Action chunk length 32 at 20 fps, with
21+
``tokenizer.encode_exact_durations=[33]`` pinned to the matching 33-frame observation window
22+
(chunk + 1).
23+
24+
Closed-loop evaluation must use the same contract::
25+
26+
ACTION_HORIZON=32 CAMERA_SET=left_wrist USE_STATE=1
27+
USE_BASE_ACTION=1 BASE_ENCODING=raw RAW_ACTION_DIM=15
28+
"""
29+
30+
import copy
31+
32+
from hydra.core.config_store import ConfigStore
33+
34+
from cosmos_framework.configs.base.experiment.sft.models.nano_model_config import NANO_MODEL_CONFIG
35+
from cosmos_framework.data.generator.action.datasets.action_sft_dataset import get_action_robocasa_sft_dataset
36+
from cosmos_framework.data.generator.action.datasets.robocasa_lerobot_dataset import DEFAULT_ALL_ATOMIC_TASKS
37+
from cosmos_framework.data.generator.joint_dataloader import (
38+
PackingDataLoader,
39+
RankPartitionedDataLoader,
40+
)
41+
from cosmos_framework.utils.lazy_config import LazyCall as L
42+
from cosmos_framework.utils.lazy_config import LazyDict
43+
44+
cs = ConfigStore.instance()
45+
46+
47+
def _lw_state_model_config() -> dict:
48+
cfg = copy.deepcopy(NANO_MODEL_CONFIG) # action_gen=True, max_action_dim=64
49+
cfg["max_num_tokens_after_packing"] = 74000
50+
cfg["activation_checkpointing"]["mode"] = "selective"
51+
cfg["diffusion_expert_config"]["load_weights_from_pretrained"] = False
52+
cfg["rectified_flow_training_config"]["loss_scale"] = 10.0
53+
cfg["rectified_flow_training_config"]["image_loss_scale"] = None
54+
# chunk_length=32 -> 33 observation frames; pin the VAE encode duration to match
55+
# (mirrors action_policy_droid_nano, which also trains with a 32-step chunk).
56+
cfg["tokenizer"]["encode_exact_durations"] = [33]
57+
return cfg
58+
59+
60+
action_policy_robocasa_nano = LazyDict(
61+
dict(
62+
defaults=[
63+
{"override /model": "mot_fsdp"},
64+
{"override /data_train": None},
65+
{"override /data_val": None},
66+
{"override /optimizer": "fusedadamw"},
67+
{"override /scheduler": "lambdalinear"},
68+
{"override /checkpoint": "s3"},
69+
{"override /callbacks": ["basic", "optimization", "job_monitor"]},
70+
{"override /ema": "power"},
71+
{"override /tokenizer": "wan2pt2_tokenizer"},
72+
{"override /sound_tokenizer": None},
73+
{"override /vlm_config": None},
74+
{"override /ckpt_type": "dcp"},
75+
"_self_",
76+
],
77+
job=dict(
78+
project="cosmos3",
79+
group="action_sft",
80+
name="action_policy_robocasa_nano",
81+
wandb_mode="disabled",
82+
),
83+
model=dict(config=_lw_state_model_config()),
84+
optimizer=dict(
85+
betas=[0.9, 0.99],
86+
eps=1.0e-08,
87+
fused=True,
88+
keys_to_select=[
89+
"moe_gen", "time_embedder", "vae2llm", "llm2vae",
90+
"action2llm", "llm2action", "action_modality_embed",
91+
],
92+
lr=5.0e-05,
93+
lr_multipliers={"action2llm": 5.0, "llm2action": 5.0, "action_modality_embed": 5.0},
94+
optimizer_type="FusedAdam",
95+
weight_decay=0.05,
96+
),
97+
scheduler=dict(
98+
lr_scheduler_type="LambdaLinear",
99+
cycle_lengths=[100],
100+
f_max=[1.0], f_min=[0.0], f_start=[1.0e-06],
101+
verbosity_interval=0, warm_up_steps=[0],
102+
),
103+
trainer=dict(
104+
distributed_parallelism="fsdp",
105+
grad_accum_iter=1, logging_iter=1, max_iter=100, max_val_iter=None,
106+
run_validation=False, run_validation_on_start=False,
107+
save_zero_checkpoint=False, seed=42, timeout_period=999999999,
108+
validation_iter=100,
109+
compile_config=dict(recompile_limit=8, use_duck_shape=False),
110+
cudnn=dict(benchmark=True, deterministic=False),
111+
ddp=dict(broadcast_buffers=True, find_unused_parameters=False, static_graph=True),
112+
grad_scaler_args=dict(enabled=False),
113+
callbacks=dict(
114+
dataloader_speed=dict(every_n=100, save_s3=False, step_size=1),
115+
device_monitor=dict(every_n=200, log_memory_detail=True, save_s3=False, step_size=1, upload_every_n_mul=5),
116+
grad_clip=dict(clip_norm=1.0, force_finite=True),
117+
heart_beat=dict(every_n=200, save_s3=False, step_size=1, update_interval_in_minute=20),
118+
iter_speed=dict(every_n=1, hit_thres=50, save_s3=False, save_s3_every_log_n=500),
119+
low_precision=dict(update_iter=1),
120+
manual_gc=dict(every_n=5, gc_level=1, warm_up=1),
121+
param_count=dict(save_s3=False),
122+
skip_nan_step=dict(max_consecutive_nan=100),
123+
training_stats=dict(log_freq=100),
124+
),
125+
),
126+
checkpoint=dict(
127+
broadcast_via_filesystem=False,
128+
dcp_async_mode_enabled=False,
129+
enable_gcs_patch_in_boto3=True,
130+
keys_not_to_resume=[],
131+
keys_to_skip_loading=[
132+
"net_ema.", "action2llm", "llm2action", "action_modality_embed", "action_pos_embed",
133+
],
134+
load_ema_to_reg=False,
135+
load_path="???",
136+
load_training_state=False,
137+
only_load_scheduler_state=False,
138+
save_iter=100,
139+
strict_resume=False,
140+
verbose=True,
141+
hf_export=dict(enabled=False, export_every_n=1, hf_repo_id=None,
142+
upload_to_object_store=dict(bucket="", credentials="", enabled=False)),
143+
jit=dict(device="cuda", dtype="bfloat16", enabled=False, input_shape=None, strict=True),
144+
load_from_object_store=dict(bucket="", credentials="", enabled=False),
145+
save_to_object_store=dict(bucket="", credentials="", enabled=False),
146+
),
147+
dataloader_train=L(PackingDataLoader)(
148+
audio_sample_rate=48000,
149+
dataset_name="action_robocasa",
150+
max_samples_per_batch=128,
151+
max_sequence_length=None,
152+
patch_spatial=2,
153+
sound_latent_fps=0,
154+
tokenizer_spatial_compression_factor=16,
155+
tokenizer_temporal_compression_factor=4,
156+
dataloader=L(RankPartitionedDataLoader)(
157+
batch_size=1, in_order=False, num_workers=4,
158+
persistent_workers=True, pin_memory=True, prefetch_factor=4, sampler=None,
159+
datasets=dict(
160+
robocasa=dict(
161+
ratio=1,
162+
dataset=L(get_action_robocasa_sft_dataset)(
163+
root="${oc.env:ROBOCASA_ROOT}",
164+
task_names=DEFAULT_ALL_ATOMIC_TASKS, # all 18 atomic tasks (NavigateKitchen included)
165+
use_base_action=True,
166+
base_encoding="raw", # 15D: native base_motion(4) + control_mode + arm
167+
fps=20,
168+
chunk_length=32, # 32-step action chunk (DROID recipe uses 32); eval must use ACTION_HORIZON=32
169+
mode="wam",
170+
viewpoint="concat_view",
171+
camera_set="left_wrist", # agentview_left + wrist, full-res 256x512
172+
use_state=True, # EEF proprioception -> prepended clean condition token
173+
action_normalization=None, # absolute state token: delta-stats do not apply
174+
split="train",
175+
split_val_ratio=0.01,
176+
iterable_shuffle=True,
177+
episode_shuffle_seed=42,
178+
resolution=None, # keep 256x512 horizontal concat un-squished
179+
max_action_dim="${model.config.max_action_dim}",
180+
cfg_dropout_rate=0.1,
181+
format_prompt_as_json=True,
182+
tokenizer_config="${model.config.vlm_config.tokenizer}",
183+
),
184+
),
185+
),
186+
),
187+
),
188+
dataloader_val=None,
189+
upload_reproducible_setup=False,
190+
),
191+
flags={"allow_objects": True},
192+
)
193+
194+
195+
for _item in [action_policy_robocasa_nano]:
196+
_name = [k for k, v in globals().items() if v is _item][0]
197+
cs.store(group="experiment", package="_global_", name=_name, node=_item)

cosmos_framework/data/generator/action/datasets/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from cosmos_framework.data.generator.action.datasets.droid_lerobot_dataset import DROIDLeRobotDataset
1818
from cosmos_framework.data.generator.action.datasets.fractal_lerobot_dataset import FractalLeRobotDataset
1919
from cosmos_framework.data.generator.action.datasets.human_hand_pose_lerobot_dataset import HumanHandPoseLeRobotDataset
20+
from cosmos_framework.data.generator.action.datasets.robocasa_lerobot_dataset import RoboCasaLeRobotDataset
2021
from cosmos_framework.data.generator.action.datasets.libero_lerobot_dataset import LIBEROLeRobotDataset
2122
from cosmos_framework.data.generator.action.datasets.robomind_franka_dataset import RoboMINDFrankaDataset
2223
from cosmos_framework.data.generator.action.datasets.robomind_ur_dataset import RoboMINDURDataset
@@ -30,6 +31,7 @@
3031
"DROIDMergedLeRobotDataset",
3132
"FractalLeRobotDataset",
3233
"HumanHandPoseLeRobotDataset",
34+
"RoboCasaLeRobotDataset",
3335
"LIBEROLeRobotDataset",
3436
"RoboMINDFrankaDataset",
3537
"RoboMINDURDataset",

cosmos_framework/data/generator/action/datasets/action_sft_dataset.py

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121

2222
from cosmos_framework.data.generator.action.datasets.droid_merged_lerobot_dataset import DROIDMergedLeRobotDataset
2323
from cosmos_framework.data.generator.action.datasets.droid_lerobot_dataset import DROIDLeRobotDataset
24+
from cosmos_framework.data.generator.action.datasets.robocasa_lerobot_dataset import (
25+
DEFAULT_ALL_ATOMIC_TASKS,
26+
RoboCasaLeRobotDataset,
27+
)
2428
from cosmos_framework.data.generator.action.datasets.libero_lerobot_dataset import LIBEROLeRobotDataset
2529
from cosmos_framework.data.generator.action.utils.transforms import ActionTransformPipeline
2630

@@ -211,6 +215,89 @@ def get_action_droid_merged_lerobot_sft_dataset(
211215
return sft
212216

213217

218+
def get_action_robocasa_sft_dataset(
219+
*,
220+
root: str,
221+
fps: float = 20.0,
222+
chunk_length: int = 16,
223+
mode: str = "wam",
224+
viewpoint: str = "concat_view",
225+
camera_set: str = "wrist_lr",
226+
task_names: tuple[str, ...] | list[str] = DEFAULT_ALL_ATOMIC_TASKS,
227+
use_state: bool = False,
228+
use_base_action: bool = False,
229+
base_encoding: str = "ego",
230+
action_normalization: str | None = None,
231+
split: str = "train",
232+
split_val_ratio: float = 0.01,
233+
split_seed: int = 42,
234+
resolution: str | int = "256",
235+
max_action_dim: int = 64,
236+
tokenizer_config: dict | None = None,
237+
cfg_dropout_rate: float = 0.1,
238+
append_viewpoint_info: bool = True,
239+
append_duration_fps_timestamps: bool = True,
240+
append_resolution_info: bool = True,
241+
append_idle_frames: bool = True,
242+
format_prompt_as_json: bool = False,
243+
iterable_shuffle: bool = False,
244+
episode_shuffle_seed: int = 42,
245+
) -> Dataset:
246+
"""Build the RoboCasa fixed-base action-policy SFT dataset.
247+
248+
Feeds ``RoboCasaLeRobotDataset`` (10D ``[pos, rot6d, gripper]`` end-effector
249+
deltas, concat_view wrist + two third-person cams) through
250+
``ActionTransformPipeline``. ``root`` is the RoboCasa atomic dir
251+
(``.../target/atomic``); each task in ``task_names`` is discovered as
252+
``<root>/<task>/*/lerobot`` and registered as a separate LeRobot shard.
253+
Defaults to ``action_normalization=None`` (like DROID); pass ``quantile_rot``
254+
with a bundled stats file to normalize.
255+
256+
``use_base_action=True`` widens the contract so the mobile base is representable
257+
(required for the full 18-task ``DEFAULT_ALL_ATOMIC_TASKS`` set, which includes
258+
NavigateKitchen); leave it False for the 10D fixed-base recipe. ``base_encoding``
259+
then selects the base representation:
260+
261+
* ``"ego"`` (default, 20D) --
262+
``[base_pos(3), base_rot6d(6), control_mode(1), eef_pos(3), eef_rot6d(6), gripper(1)]``
263+
* ``"raw"`` (15D) --
264+
``[base_motion(4), control_mode(1), eef_pos(3), eef_rot6d(6), gripper(1)]``
265+
266+
``"raw"`` regresses RoboCasa's native normalised velocity command, so replay is an
267+
identity round-trip and the base channels share the arm's scale; ``"ego"`` is retained
268+
as the default so existing configs and checkpoints reproduce unchanged.
269+
"""
270+
dataset: Dataset = RoboCasaLeRobotDataset(
271+
root=root,
272+
fps=fps,
273+
chunk_length=chunk_length,
274+
mode=mode,
275+
viewpoint=viewpoint,
276+
camera_set=camera_set,
277+
task_names=task_names,
278+
use_state=use_state,
279+
use_base_action=use_base_action,
280+
base_encoding=base_encoding,
281+
action_normalization=action_normalization,
282+
split=split,
283+
split_val_ratio=split_val_ratio,
284+
split_seed=split_seed,
285+
)
286+
transform = ActionTransformPipeline(
287+
tokenizer_config=tokenizer_config,
288+
cfg_dropout_rate=cfg_dropout_rate,
289+
max_action_dim=max_action_dim,
290+
append_viewpoint_info=append_viewpoint_info,
291+
append_duration_fps_timestamps=append_duration_fps_timestamps,
292+
append_resolution_info=append_resolution_info,
293+
append_idle_frames=append_idle_frames,
294+
format_prompt_as_json=format_prompt_as_json,
295+
)
296+
sft = ActionSFTDataset(dataset, transform, resolution)
297+
if iterable_shuffle:
298+
return ActionIterableShuffleDataset(sft, seed=episode_shuffle_seed)
299+
return sft
300+
214301
def get_action_libero_sft_dataset(
215302
*,
216303
root: str,

0 commit comments

Comments
 (0)