Skip to content

Commit 18ab314

Browse files
ZhangYi1999claude
andcommitted
feat: add policy test script, recording launcher, and dataset fix utility
- test_lerobot_policy.py: standalone script to load any LeRobot policy checkpoint and run timed inference benchmark; supports all policy types (GR00T, ACT, Diffusion, etc.) with graceful processor fallback - bash_scripts/start_recording.sh: convenience launcher for leader-follower recording session - dataset_conversions/fix_task_index.py: utility to reset task_index to 0 in parquet files for single-task datasets Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 10055ff commit 18ab314

3 files changed

Lines changed: 215 additions & 0 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
python ../../crisp_gym/scripts/record_lerobot_format_leader_follower.py \
2+
--repo-id=continuallearning/real_2_put_moka_pot \
3+
--tasks="put the moka pot on the stove" \
4+
# --resume \
5+
--debug-level="DEBUG"
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
"""Standalone test: load any LeRobot policy from a checkpoint path and run inference.
2+
3+
Usage:
4+
python crisp_gym/scripts/test_lerobot_policy.py --policy-path /path/to/checkpoint
5+
python crisp_gym/scripts/test_lerobot_policy.py --policy-path continuallearning/groot_fft_10000steps_ga4_real_0_put_bowl
6+
"""
7+
8+
import argparse
9+
import json
10+
import logging
11+
import os
12+
import time
13+
14+
import torch
15+
from lerobot.configs.train import TrainPipelineConfig
16+
from lerobot.configs.types import FeatureType
17+
from lerobot.policies.factory import get_policy_class, make_pre_post_processors
18+
19+
logger = logging.getLogger(__name__)
20+
21+
22+
def make_dummy_obs(policy, device: str) -> dict[str, torch.Tensor]:
23+
"""Build a random observation batch (size 1) matching policy.config.input_features.
24+
25+
Images: float32 in [0, 1] (FeatureType.VISUAL)
26+
State/env/etc: standard normal float32
27+
28+
Note: tensors have explicit batch dim (1, ...) so AddBatchDimensionProcessorStep
29+
(which only adds dim to 1D/3D tensors) leaves them unchanged — no double-batching.
30+
"""
31+
obs = {}
32+
for key, feature in policy.config.input_features.items():
33+
shape = (1, *feature.shape)
34+
if feature.type is FeatureType.VISUAL:
35+
obs[key] = torch.rand(shape, dtype=torch.float32, device=device)
36+
else:
37+
obs[key] = torch.randn(shape, dtype=torch.float32, device=device)
38+
return obs
39+
40+
41+
def load_processors(policy, policy_path: str):
42+
"""Load pre/post processors: try from checkpoint first, then fresh, then None.
43+
44+
GR00T REQUIRES processors — its select_action filters batch to only eagle_*/state/*
45+
keys; without preprocessing, observation.* keys are silently dropped → crash.
46+
"""
47+
# Attempt 1: load saved processors from checkpoint (includes dataset stats for normalization)
48+
try:
49+
pre, post = make_pre_post_processors(
50+
policy_cfg=policy.config,
51+
pretrained_path=policy_path,
52+
)
53+
print(" status: loaded from checkpoint (with saved stats)")
54+
return pre, post
55+
except Exception as e1:
56+
print(f" WARN: Could not load from checkpoint: {e1}")
57+
58+
# Attempt 2: create fresh processors (no normalization stats — values may be out of range)
59+
try:
60+
pre, post = make_pre_post_processors(policy_cfg=policy.config)
61+
print(" status: created fresh (no dataset stats — normalization skipped)")
62+
return pre, post
63+
except Exception as e2:
64+
print(f" WARN: Could not create fresh processors: {e2}")
65+
print(" status: unavailable — inference will proceed without preprocessing")
66+
print(" NOTE: GR00T policies WILL fail without processors!")
67+
return None, None
68+
69+
70+
def main():
71+
parser = argparse.ArgumentParser(description="Test LeRobot policy loading and inference.")
72+
parser.add_argument("--policy-path", required=True, help="Path or HF Hub repo ID of LeRobot checkpoint.")
73+
parser.add_argument("--device", default="cuda" if torch.cuda.is_available() else "cpu")
74+
parser.add_argument("--n-warmup", type=int, default=5, help="Warmup iterations.")
75+
parser.add_argument("--n-bench", type=int, default=10, help="Benchmark iterations.")
76+
args = parser.parse_args()
77+
78+
print(f"\n{'='*60}")
79+
print("LeRobot Policy Integration Test")
80+
print(f"{'='*60}")
81+
print(f" policy_path : {args.policy_path}")
82+
print(f" device : {args.device}")
83+
84+
# ── Step 1: Load train config ──────────────────────────────────
85+
print("\n[1/4] Loading TrainPipelineConfig...")
86+
try:
87+
train_cfg = TrainPipelineConfig.from_pretrained(args.policy_path)
88+
policy_type = train_cfg.policy.type
89+
print(f" policy type : {policy_type}")
90+
print(f" dataset : {train_cfg.dataset.repo_id}")
91+
except Exception as e:
92+
print(f" WARN: Could not load TrainPipelineConfig: {e}")
93+
print(" Falling back: reading policy type from config.json...")
94+
if os.path.isdir(args.policy_path):
95+
config_path = os.path.join(args.policy_path, "config.json")
96+
with open(config_path) as f:
97+
cfg_json = json.load(f)
98+
else:
99+
from huggingface_hub import hf_hub_download
100+
config_file = hf_hub_download(args.policy_path, "config.json")
101+
with open(config_file) as f:
102+
cfg_json = json.load(f)
103+
policy_type = cfg_json.get("type") or cfg_json.get("policy_type")
104+
if not policy_type:
105+
raise RuntimeError("Cannot determine policy type from checkpoint.")
106+
print(f" policy type : {policy_type}")
107+
108+
# ── Step 2: Load policy ────────────────────────────────────────
109+
print("\n[2/4] Loading policy weights...")
110+
policy_cls = get_policy_class(policy_type)
111+
policy = policy_cls.from_pretrained(args.policy_path)
112+
policy.to(args.device).eval()
113+
policy.reset()
114+
115+
print(f" class : {policy.__class__.__name__}")
116+
for key, ft in policy.config.input_features.items():
117+
print(f" {key:40s} type={ft.type.value:8s} shape={ft.shape}")
118+
119+
# ── Step 3: Pre/post processors ───────────────────────────────
120+
print("\n[3/4] Loading processors...")
121+
pre, post = load_processors(policy, args.policy_path)
122+
use_processors = pre is not None
123+
124+
# ── Step 4: Dummy obs + inference ─────────────────────────────
125+
print(f"\n[4/4] Running inference ({args.n_warmup} warmup + {args.n_bench} bench)...")
126+
dummy_obs = make_dummy_obs(policy, args.device)
127+
if use_processors:
128+
dummy_obs = pre(dummy_obs)
129+
130+
with torch.inference_mode():
131+
for _ in range(args.n_warmup):
132+
policy.reset()
133+
_ = policy.select_action(dummy_obs)
134+
if args.device == "cuda":
135+
torch.cuda.synchronize()
136+
137+
times = []
138+
for _ in range(args.n_bench):
139+
policy.reset()
140+
t0 = time.perf_counter()
141+
action = policy.select_action(dummy_obs)
142+
if args.device == "cuda":
143+
torch.cuda.synchronize()
144+
times.append(time.perf_counter() - t0)
145+
146+
if use_processors:
147+
action = post(action)
148+
149+
avg_ms = sum(times) / len(times) * 1000
150+
print(f"\n{'='*60}")
151+
print("Results")
152+
print(f"{'='*60}")
153+
print(f" action shape : {tuple(action.shape)}")
154+
print(f" avg latency : {avg_ms:.2f} ms")
155+
print(f" min / max : {min(times)*1000:.2f} / {max(times)*1000:.2f} ms")
156+
print("\nPASS: policy loaded and inference completed successfully.")
157+
158+
159+
if __name__ == "__main__":
160+
logging.basicConfig(level=logging.INFO)
161+
main()
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Fix task_index in parquet files: set all task_index values to 0."""
2+
3+
import argparse
4+
from pathlib import Path
5+
6+
import pyarrow.parquet as pq
7+
import pyarrow as pa
8+
9+
10+
def fix_task_index(data_dir: str):
11+
data_path = Path(data_dir)
12+
parquet_files = sorted(data_path.glob("*.parquet"))
13+
14+
if not parquet_files:
15+
print(f"No parquet files found in {data_dir}")
16+
return
17+
18+
fixed = 0
19+
for fpath in parquet_files:
20+
table = pq.read_table(fpath)
21+
if "task_index" not in table.column_names:
22+
continue
23+
24+
col = table.column("task_index")
25+
# Check if any value != 0
26+
if col.to_pylist() == [0] * len(col):
27+
continue
28+
29+
# Replace task_index column with all zeros
30+
idx = table.column_names.index("task_index")
31+
new_col = pa.array([0] * len(table), type=col.type)
32+
table = table.set_column(idx, "task_index", new_col)
33+
pq.write_table(table, fpath)
34+
fixed += 1
35+
print(f"Fixed: {fpath.name}")
36+
37+
print(f"\nDone. Fixed {fixed}/{len(parquet_files)} files.")
38+
39+
40+
if __name__ == "__main__":
41+
parser = argparse.ArgumentParser(description="Fix task_index in parquet files")
42+
parser.add_argument(
43+
"--data-dir",
44+
type=str,
45+
default=str(Path.home() / ".cache/huggingface/lerobot/continuallearning/real_1_stack_bowls/data/chunk-000"),
46+
help="Path to the directory containing parquet files",
47+
)
48+
args = parser.parse_args()
49+
fix_task_index(args.data_dir)

0 commit comments

Comments
 (0)