Skip to content

Commit 9bde58d

Browse files
[python] Speed up LeRobot episode metadata initialization (#10754)
1 parent d32b5c9 commit 9bde58d

2 files changed

Lines changed: 148 additions & 9 deletions

File tree

‎paimon-python/pypaimon/multimodal/lerobot/dataset.py‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1032,9 +1032,30 @@ def _metadata_member(metadata, name, default=None):
10321032
return default if value is None else value
10331033

10341034

1035-
def _episode_row(episodes, ordinal):
1036-
return episodes.iloc[ordinal] if hasattr(episodes, "iloc") \
1037-
else episodes[ordinal]
1035+
def _episode_rows(episodes, columns):
1036+
if hasattr(episodes, "select_columns"):
1037+
dataset_format = getattr(episodes, "format", None)
1038+
if isinstance(dataset_format, Mapping) and (
1039+
dataset_format.get("type") == "custom"
1040+
or dataset_format.get("columns") != episodes.column_names):
1041+
# Custom transforms can read or create columns.
1042+
for ordinal in range(len(episodes)):
1043+
yield episodes[ordinal]
1044+
return
1045+
columns = [name for name in columns if name in episodes.column_names]
1046+
if columns:
1047+
# Avoid formatting wide statistics columns for every episode.
1048+
projected = episodes.select_columns(columns)
1049+
for batch in projected.iter(batch_size=1024):
1050+
for values in zip(*(batch[name] for name in columns)):
1051+
yield dict(zip(columns, values))
1052+
else:
1053+
for _ in range(len(episodes)):
1054+
yield {}
1055+
else:
1056+
for ordinal in range(len(episodes)):
1057+
yield episodes.iloc[ordinal] if hasattr(episodes, "iloc") \
1058+
else episodes[ordinal]
10381059

10391060

10401061
def _episode_ranges(metadata, total_frames, total_episodes):
@@ -1047,8 +1068,8 @@ def _episode_ranges(metadata, total_frames, total_episodes):
10471068
% (len(episodes), total_episodes))
10481069
ranges = []
10491070
expected = 0
1050-
for ordinal in range(total_episodes):
1051-
row = _episode_row(episodes, ordinal)
1071+
columns = ("episode_index", "dataset_from_index", "dataset_to_index", "length")
1072+
for ordinal, row in enumerate(_episode_rows(episodes, columns)):
10521073
try:
10531074
index = operator.index(row["episode_index"])
10541075
begin = operator.index(row["dataset_from_index"])
@@ -1097,7 +1118,7 @@ def _build_frame_validation_context(
10971118
metadata, episode_ranges, fps, tasks, subtasks, timestamp_type):
10981119
task_names = _index_names(tasks, "task_index")
10991120
subtask_names = _index_names(subtasks, "subtask_index")
1100-
episode_tasks = _episode_tasks(metadata, len(episode_ranges)) \
1121+
episode_tasks = _episode_tasks(metadata) \
11011122
if episode_ranges is not None else None
11021123
return {
11031124
"episode_ranges": episode_ranges,
@@ -1145,13 +1166,12 @@ def _index_names(values, index_field):
11451166
return result
11461167

11471168

1148-
def _episode_tasks(metadata, total_episodes):
1169+
def _episode_tasks(metadata):
11491170
episodes = _metadata_member(metadata, "episodes")
11501171
if episodes is None:
11511172
return None
11521173
result = []
1153-
for ordinal in range(total_episodes):
1154-
row = _episode_row(episodes, ordinal)
1174+
for row in _episode_rows(episodes, ("tasks",)):
11551175
tasks = row.get("tasks") if hasattr(row, "get") else None
11561176
if tasks is None:
11571177
result.append(None)

‎paimon-python/pypaimon/tests/multimodal_lerobot_test.py‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
_decode_video_frames,
5555
_decode_video_rows,
5656
_decode_video_windows,
57+
_episode_ranges,
58+
_episode_tasks,
5759
_image_tensor,
5860
_index_names,
5961
_open_video_decoder,
@@ -165,6 +167,81 @@ def _configure_uint8_transform_worker(unused_worker_id):
165167

166168
class LeRobotValidationTest(unittest.TestCase):
167169

170+
def test_episode_validation_projects_batched_hf_metadata(self):
171+
try:
172+
from datasets import Dataset
173+
except ImportError:
174+
self.skipTest("datasets is not installed")
175+
from pypaimon.multimodal.lerobot.dataset import (
176+
_episode_ranges, _episode_tasks,
177+
)
178+
179+
rows = [{
180+
"episode_index": index,
181+
"dataset_from_index": index * 2,
182+
"dataset_to_index": index * 2 + 2,
183+
"length": 2,
184+
"tasks": ["place", "pick"],
185+
"stats/camera/mean": [0.5] * 32,
186+
} for index in range(2050)]
187+
episodes = Dataset.from_list(rows)
188+
expected = [(index * 2, index * 2 + 2) for index in range(2050)]
189+
with patch.object(
190+
Dataset, "__getitem__",
191+
side_effect=AssertionError("Do not format individual rows")):
192+
self.assertEqual(expected, _episode_ranges(
193+
{"episodes": episodes}, 4100, 2050))
194+
self.assertEqual((("pick", "place"),) * 2050,
195+
_episode_tasks({"episodes": episodes}))
196+
self.assertIn("stats/camera/mean", episodes.column_names)
197+
self.assertEqual(rows[0], episodes[0])
198+
self.assertEqual((None,) * 2050, _episode_tasks({
199+
"episodes": episodes.remove_columns("tasks"),
200+
}))
201+
formatted = episodes.select(range(2)).with_format(
202+
"numpy",
203+
columns=["episode_index", "dataset_from_index",
204+
"dataset_to_index", "length"],
205+
)
206+
self.assertNotIn("tasks", formatted[0])
207+
self.assertEqual([(0, 2), (2, 4)], _episode_ranges(
208+
{"episodes": formatted}, 4, 2))
209+
self.assertEqual((None, None), _episode_tasks(
210+
{"episodes": formatted}))
211+
visible = formatted.with_format(
212+
"numpy", columns=formatted.format["columns"],
213+
output_all_columns=True)
214+
self.assertEqual((("pick", "place"),) * 2, _episode_tasks(
215+
{"episodes": visible}))
216+
# Preserve a selected Dataset's logical row order and range validation.
217+
with self.assertRaisesRegex(ValueError, "episode_index=1"):
218+
_episode_ranges({"episodes": episodes.select([1, 0])}, 4, 2)
219+
with self.assertRaisesRegex(ValueError, "integer controls"):
220+
_episode_ranges({"episodes": episodes.remove_columns("length")},
221+
4100, 2050)
222+
223+
def test_episode_validation_preserves_metadata_representations(self):
224+
from pypaimon.multimodal.lerobot.dataset import (
225+
_episode_ranges, _episode_tasks,
226+
)
227+
import pandas as pd
228+
229+
rows = [{
230+
"episode_index": 0, "dataset_from_index": 0,
231+
"dataset_to_index": 2, "length": 2, "tasks": "pick",
232+
}, {
233+
"episode_index": 1, "dataset_from_index": 2,
234+
"dataset_to_index": 3, "length": 1, "tasks": None,
235+
}]
236+
for episodes in (rows, pd.DataFrame(rows)):
237+
with self.subTest(type=type(episodes).__name__):
238+
metadata = {"episodes": episodes}
239+
self.assertEqual([(0, 2), (2, 3)],
240+
_episode_ranges(metadata, 3, 2))
241+
self.assertEqual((("pick",), None), _episode_tasks(metadata))
242+
with self.assertRaisesRegex(ValueError, "expected 4"):
243+
_episode_ranges(metadata, 4, 2)
244+
168245
def test_episode_metadata_pickle_stays_small_and_usable(self):
169246
try:
170247
from datasets import Dataset
@@ -931,6 +1008,35 @@ def test_torchcodec_batch_matches_single_frame_decoding(self):
9311008
finally:
9321009
collator.close()
9331010

1011+
def test_custom_episode_transform_keeps_cross_column_inputs(self):
1012+
try:
1013+
from datasets import Dataset
1014+
except ImportError as error:
1015+
self.skipTest(str(error))
1016+
1017+
episodes = Dataset.from_dict({
1018+
"episode_index": [0, 1],
1019+
"dataset_from_index": [0, 1],
1020+
"dataset_to_index": [1, 3],
1021+
"length": [0, 0],
1022+
"stats": [1, 2],
1023+
})
1024+
1025+
def transform(batch):
1026+
return dict(batch, length=batch["stats"], tasks=[
1027+
["pick" if index == 0 else "place"]
1028+
for index in batch["episode_index"]
1029+
])
1030+
1031+
for source in (episodes, episodes.add_column(
1032+
"tasks", [["old"], ["old"]])):
1033+
with self.subTest(physical_tasks="tasks" in source.column_names):
1034+
metadata = {"episodes": source.with_transform(transform)}
1035+
self.assertEqual([(0, 1), (1, 3)],
1036+
_episode_ranges(metadata, 3, 2))
1037+
self.assertEqual((("pick",), ("place",)),
1038+
_episode_tasks(metadata))
1039+
9341040
def test_dataset_requires_supported_python(self):
9351041
with patch(
9361042
"pypaimon.multimodal.lerobot.dataset.sys.version_info",
@@ -1038,6 +1144,19 @@ def close(self):
10381144
self.assertEqual([1.0], dataset.meta.stats["action"]["mean"].tolist())
10391145
self.assertEqual(0, dataset.meta.get_task_index("pick"))
10401146
self.assertEqual("pick", sample["task"])
1147+
try:
1148+
from datasets import Dataset
1149+
except ImportError:
1150+
pass
1151+
else:
1152+
formatted_metadata = dict(metadata)
1153+
formatted_metadata["episodes"] = Dataset.from_list(
1154+
metadata["episodes"]).with_format(
1155+
"numpy", columns=["episode_index", "dataset_from_index",
1156+
"dataset_to_index", "length"])
1157+
formatted_reader = Reader(formatted_metadata)
1158+
self.assertEqual("pick", formatted_reader[1]["task"])
1159+
formatted_reader.close()
10411160
torch.testing.assert_close(
10421161
sample["observation.state"], torch.tensor([1.0, 2.0]))
10431162
torch.testing.assert_close(

0 commit comments

Comments
 (0)