[python] Persist LeRobot dataset metadata in Paimon - #9529
Conversation
dd6ac5b to
445d4a9
Compare
445d4a9 to
286235b
Compare
|
I think the four-table split and the READY-last publication point are the right direction. My remaining concern is that the current schema combines two different version models without defining which one is authoritative for row membership. Today every component row carries Because the frame table is append-only, S2 is cumulative. Reading S2 or its tag alone returns both V1 and V2. The actual V2 view still requires That model is internally consistent only if every version is a complete materialization. Adding C to an existing dataset must write A, B, and C again. Writing only C under V2 would make the V2 filter return only the delta, unless the reader implements parent-version replay and INSERT/UPDATE/DELETE overlay semantics. The current implementation does not implement such a delta model. I suggest choosing one explicit model before establishing this as the persisted contract:
I prefer the snapshot-state model because it matches Paimon snapshot/time-travel semantics and LeRobot recording behavior, which is primarily append-by-episode. It also avoids A possible manifest is: The publication flow can then be: The public import result should contain at least The important point is not that UUIDs and snapshots cannot coexist. They can, but they need distinct roles: the UUID should identify the cross-table release, while exactly one mechanism must define which component rows belong to that release. |
|
I suggest using version_id directly as the Paimon tag name for all component tables. The component tables keep their native LeRobot V3 schemas and do not need dataset_id or version_id columns: Each published version creates the same tag on all three tables: The versions table acts as the version manifest: Version id is a bigint. Readers first resolve a READY row from versions, then read all three component tables using scan.tag-name = version_id. Snapshot IDs do not need to be stored in versions, because each table’s tag already resolves the version to its corresponding snapshot. |
| ) | ||
| print(snapshot_id) | ||
| print(result.version_id) | ||
| print(result.frames_snapshot_id) |
There was a problem hiding this comment.
Just version_id is enough?
|
|
||
| ```python | ||
| snapshot_id = conn.load_from_lerobot( | ||
| version_id = conn.load_from_lerobot( |
There was a problem hiding this comment.
How to use this version in PyToch Dataloader?
There was a problem hiding this comment.
How to use this version in PyToch Dataloader?
Like this dataset = PaimonLeRobotDataset( conn, "catalog.db.robot_data", version_id=version_id, )?
There was a problem hiding this comment.
I think it is OK, if none you should find latest.
| or not isinstance(timestamp, numbers.Real) | ||
| or not math.isclose( | ||
| float(timestamp), frame_index / fps, | ||
| rel_tol=0.0, abs_tol=1e-4)): |
There was a problem hiding this comment.
LeRobot stores timestamp as float32, so this fixed absolute tolerance can reject valid long episodes as the float32 ULP grows. For example, at 30 FPS, frame_index = 61,441 is stored as 2048.033447265625, while frame_index / fps is 2048.0333333333333; the error is about 1.14e-4, so this check fails for otherwise valid LeRobot data. Please compare against an expected value quantized to the declared Arrow dtype, or use a dtype/ULP-aware tolerance.
|
I suggest two additional schema-alignment changes:
|
…contained-import # Conflicts: # paimon-python/pypaimon/tests/multimodal_lerobot_test.py
| raise ValueError( | ||
| "LeRobot metadata table %s must be append-only." % identifier) | ||
| actual = _target_schema(table) | ||
| if not actual.equals(schema, check_metadata=False): |
There was a problem hiding this comment.
Ignoring schema metadata here hides a round-trip incompatibility. LeRobot writes tasks.parquet and subtasks.parquet from Pandas DataFrames, with the task or subtask text encoded as a named Pandas index. Schema.from_pyarrow_schema does not persist the top-level pandas schema metadata, so scanning or exporting this Paimon table and calling to_pandas() produces a RangeIndex instead of restoring that text index; LeRobot lookups such as tasks.iloc[task_idx].name then return the wrong value. Please either persist and reattach the component Arrow schema metadata, or explicitly reconstruct the index from the physical text column in the Paimon LeRobot reader and cover that round trip in a test.
| )) | ||
| return pa.schema(fields) | ||
| return pa.schema([ | ||
| _feature_field(name, feature) |
There was a problem hiding this comment.
This derives the frame schema from arbitrary info.json descriptors but never enforces the mandatory LeRobot V3 control schema. LeRobot V3 defines timestamp as scalar float32 and frame_index, episode_index, index, and task_index as scalar int64; subtask_index should likewise be scalar int64 when present. Today a source declaring timestamp as float64 or frame_index as int32 is accepted and published as a non-native frame schema because the row validator only checks values. Please validate these required feature descriptors against the V3 defaults before creating the table.
| def _validated_episode_tables(metadata): | ||
| rows = [] | ||
| for table in _source_episode_tables(metadata): | ||
| rows.extend(table.select(_EPISODE_CONTROL_COLUMNS).to_pylist()) |
There was a problem hiding this comment.
Although each Arrow shard is released before the next one is read, this still materializes every Episode control row as Python dictionaries in rows, and _episode_rows then builds a second full Python list retained in metadata. The FileIO path has already materialized another locator list in _RemoteLeRobotDataset, so peak heap remains O(total_episodes) and can reach multiple GB at the million-Episode scale that V3 targets. Please validate ordered Episode rows incrementally and keep the boundaries in Arrow, mmap, or compact arrays, or consume them directly during frame import, instead of retaining duplicate to_pylist() structures.
|
I’m not sure whether we should clean up the table group automatically when an import fails. I prefer leaving it in place and letting the user drop it explicitly. Could you let me know your suggestion? @JingsongLi @YannByron |
+1 to keep it. |
|
Two suggestions on the publication contract:
|
|
+1 |
Depends on #9589 for reconciled commit callback delivery.
Summary
This establishes the persisted LeRobot contract that #9498 can consume after rebasing.
Tests