This is an automated email from the ASF dual-hosted git repository.
JingsongLi pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/paimon.git
The following commit(s) were added to refs/heads/master by this push:
new 9bde58d0d9 [python] Speed up LeRobot episode metadata initialization
(#10754)
9bde58d0d9 is described below
commit 9bde58d0d9f1067b19630896e6f5b14da98bc3a8
Author: XiaoHongbo <[email protected]>
AuthorDate: Sun Oct 25 22:11:28 2026 +0800
[python] Speed up LeRobot episode metadata initialization (#10754)
---
.../pypaimon/multimodal/lerobot/dataset.py | 38 +++++--
.../pypaimon/tests/multimodal_lerobot_test.py | 119 +++++++++++++++++++++
2 files changed, 148 insertions(+), 9 deletions(-)
diff --git a/paimon-python/pypaimon/multimodal/lerobot/dataset.py
b/paimon-python/pypaimon/multimodal/lerobot/dataset.py
index 7b0892314a..e1f5512360 100644
--- a/paimon-python/pypaimon/multimodal/lerobot/dataset.py
+++ b/paimon-python/pypaimon/multimodal/lerobot/dataset.py
@@ -1032,9 +1032,30 @@ def _metadata_member(metadata, name, default=None):
return default if value is None else value
-def _episode_row(episodes, ordinal):
- return episodes.iloc[ordinal] if hasattr(episodes, "iloc") \
- else episodes[ordinal]
+def _episode_rows(episodes, columns):
+ if hasattr(episodes, "select_columns"):
+ dataset_format = getattr(episodes, "format", None)
+ if isinstance(dataset_format, Mapping) and (
+ dataset_format.get("type") == "custom"
+ or dataset_format.get("columns") != episodes.column_names):
+ # Custom transforms can read or create columns.
+ for ordinal in range(len(episodes)):
+ yield episodes[ordinal]
+ return
+ columns = [name for name in columns if name in episodes.column_names]
+ if columns:
+ # Avoid formatting wide statistics columns for every episode.
+ projected = episodes.select_columns(columns)
+ for batch in projected.iter(batch_size=1024):
+ for values in zip(*(batch[name] for name in columns)):
+ yield dict(zip(columns, values))
+ else:
+ for _ in range(len(episodes)):
+ yield {}
+ else:
+ for ordinal in range(len(episodes)):
+ yield episodes.iloc[ordinal] if hasattr(episodes, "iloc") \
+ else episodes[ordinal]
def _episode_ranges(metadata, total_frames, total_episodes):
@@ -1047,8 +1068,8 @@ def _episode_ranges(metadata, total_frames,
total_episodes):
% (len(episodes), total_episodes))
ranges = []
expected = 0
- for ordinal in range(total_episodes):
- row = _episode_row(episodes, ordinal)
+ columns = ("episode_index", "dataset_from_index", "dataset_to_index",
"length")
+ for ordinal, row in enumerate(_episode_rows(episodes, columns)):
try:
index = operator.index(row["episode_index"])
begin = operator.index(row["dataset_from_index"])
@@ -1097,7 +1118,7 @@ def _build_frame_validation_context(
metadata, episode_ranges, fps, tasks, subtasks, timestamp_type):
task_names = _index_names(tasks, "task_index")
subtask_names = _index_names(subtasks, "subtask_index")
- episode_tasks = _episode_tasks(metadata, len(episode_ranges)) \
+ episode_tasks = _episode_tasks(metadata) \
if episode_ranges is not None else None
return {
"episode_ranges": episode_ranges,
@@ -1145,13 +1166,12 @@ def _index_names(values, index_field):
return result
-def _episode_tasks(metadata, total_episodes):
+def _episode_tasks(metadata):
episodes = _metadata_member(metadata, "episodes")
if episodes is None:
return None
result = []
- for ordinal in range(total_episodes):
- row = _episode_row(episodes, ordinal)
+ for row in _episode_rows(episodes, ("tasks",)):
tasks = row.get("tasks") if hasattr(row, "get") else None
if tasks is None:
result.append(None)
diff --git a/paimon-python/pypaimon/tests/multimodal_lerobot_test.py
b/paimon-python/pypaimon/tests/multimodal_lerobot_test.py
index 597e683d25..56966d5503 100644
--- a/paimon-python/pypaimon/tests/multimodal_lerobot_test.py
+++ b/paimon-python/pypaimon/tests/multimodal_lerobot_test.py
@@ -54,6 +54,8 @@ from pypaimon.multimodal.lerobot.dataset import (
_decode_video_frames,
_decode_video_rows,
_decode_video_windows,
+ _episode_ranges,
+ _episode_tasks,
_image_tensor,
_index_names,
_open_video_decoder,
@@ -165,6 +167,81 @@ def _configure_uint8_transform_worker(unused_worker_id):
class LeRobotValidationTest(unittest.TestCase):
+ def test_episode_validation_projects_batched_hf_metadata(self):
+ try:
+ from datasets import Dataset
+ except ImportError:
+ self.skipTest("datasets is not installed")
+ from pypaimon.multimodal.lerobot.dataset import (
+ _episode_ranges, _episode_tasks,
+ )
+
+ rows = [{
+ "episode_index": index,
+ "dataset_from_index": index * 2,
+ "dataset_to_index": index * 2 + 2,
+ "length": 2,
+ "tasks": ["place", "pick"],
+ "stats/camera/mean": [0.5] * 32,
+ } for index in range(2050)]
+ episodes = Dataset.from_list(rows)
+ expected = [(index * 2, index * 2 + 2) for index in range(2050)]
+ with patch.object(
+ Dataset, "__getitem__",
+ side_effect=AssertionError("Do not format individual rows")):
+ self.assertEqual(expected, _episode_ranges(
+ {"episodes": episodes}, 4100, 2050))
+ self.assertEqual((("pick", "place"),) * 2050,
+ _episode_tasks({"episodes": episodes}))
+ self.assertIn("stats/camera/mean", episodes.column_names)
+ self.assertEqual(rows[0], episodes[0])
+ self.assertEqual((None,) * 2050, _episode_tasks({
+ "episodes": episodes.remove_columns("tasks"),
+ }))
+ formatted = episodes.select(range(2)).with_format(
+ "numpy",
+ columns=["episode_index", "dataset_from_index",
+ "dataset_to_index", "length"],
+ )
+ self.assertNotIn("tasks", formatted[0])
+ self.assertEqual([(0, 2), (2, 4)], _episode_ranges(
+ {"episodes": formatted}, 4, 2))
+ self.assertEqual((None, None), _episode_tasks(
+ {"episodes": formatted}))
+ visible = formatted.with_format(
+ "numpy", columns=formatted.format["columns"],
+ output_all_columns=True)
+ self.assertEqual((("pick", "place"),) * 2, _episode_tasks(
+ {"episodes": visible}))
+ # Preserve a selected Dataset's logical row order and range validation.
+ with self.assertRaisesRegex(ValueError, "episode_index=1"):
+ _episode_ranges({"episodes": episodes.select([1, 0])}, 4, 2)
+ with self.assertRaisesRegex(ValueError, "integer controls"):
+ _episode_ranges({"episodes": episodes.remove_columns("length")},
+ 4100, 2050)
+
+ def test_episode_validation_preserves_metadata_representations(self):
+ from pypaimon.multimodal.lerobot.dataset import (
+ _episode_ranges, _episode_tasks,
+ )
+ import pandas as pd
+
+ rows = [{
+ "episode_index": 0, "dataset_from_index": 0,
+ "dataset_to_index": 2, "length": 2, "tasks": "pick",
+ }, {
+ "episode_index": 1, "dataset_from_index": 2,
+ "dataset_to_index": 3, "length": 1, "tasks": None,
+ }]
+ for episodes in (rows, pd.DataFrame(rows)):
+ with self.subTest(type=type(episodes).__name__):
+ metadata = {"episodes": episodes}
+ self.assertEqual([(0, 2), (2, 3)],
+ _episode_ranges(metadata, 3, 2))
+ self.assertEqual((("pick",), None), _episode_tasks(metadata))
+ with self.assertRaisesRegex(ValueError, "expected 4"):
+ _episode_ranges(metadata, 4, 2)
+
def test_episode_metadata_pickle_stays_small_and_usable(self):
try:
from datasets import Dataset
@@ -931,6 +1008,35 @@ class LeRobotValidationTest(unittest.TestCase):
finally:
collator.close()
+ def test_custom_episode_transform_keeps_cross_column_inputs(self):
+ try:
+ from datasets import Dataset
+ except ImportError as error:
+ self.skipTest(str(error))
+
+ episodes = Dataset.from_dict({
+ "episode_index": [0, 1],
+ "dataset_from_index": [0, 1],
+ "dataset_to_index": [1, 3],
+ "length": [0, 0],
+ "stats": [1, 2],
+ })
+
+ def transform(batch):
+ return dict(batch, length=batch["stats"], tasks=[
+ ["pick" if index == 0 else "place"]
+ for index in batch["episode_index"]
+ ])
+
+ for source in (episodes, episodes.add_column(
+ "tasks", [["old"], ["old"]])):
+ with self.subTest(physical_tasks="tasks" in source.column_names):
+ metadata = {"episodes": source.with_transform(transform)}
+ self.assertEqual([(0, 1), (1, 3)],
+ _episode_ranges(metadata, 3, 2))
+ self.assertEqual((("pick",), ("place",)),
+ _episode_tasks(metadata))
+
def test_dataset_requires_supported_python(self):
with patch(
"pypaimon.multimodal.lerobot.dataset.sys.version_info",
@@ -1038,6 +1144,19 @@ class LeRobotValidationTest(unittest.TestCase):
self.assertEqual([1.0], dataset.meta.stats["action"]["mean"].tolist())
self.assertEqual(0, dataset.meta.get_task_index("pick"))
self.assertEqual("pick", sample["task"])
+ try:
+ from datasets import Dataset
+ except ImportError:
+ pass
+ else:
+ formatted_metadata = dict(metadata)
+ formatted_metadata["episodes"] = Dataset.from_list(
+ metadata["episodes"]).with_format(
+ "numpy", columns=["episode_index", "dataset_from_index",
+ "dataset_to_index", "length"])
+ formatted_reader = Reader(formatted_metadata)
+ self.assertEqual("pick", formatted_reader[1]["task"])
+ formatted_reader.close()
torch.testing.assert_close(
sample["observation.state"], torch.tensor([1.0, 2.0]))
torch.testing.assert_close(