Skip to content

rldb: move MultiDataset and format-neutral pieces out of zarr/ - #717

Draft
RyanPCo wants to merge 4 commits into
ryanco/lance-migrationfrom
ryanco/rldb-multi
Draft

RyanPCo wants to merge 4 commits into
ryanco/lance-migrationfrom
ryanco/rldb-multi

Conversation

@RyanPCo

@RyanPCo RyanPCo commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor

rldb: move MultiDataset and format-neutral pieces out of zarr/

MultiDataset, EvenStrideDataset, pinned_episode_subset, worker_context and
_IndexMap move to egomimic/rldb/multi.py; PinError and _ensure_dataset_filter
to rldb/filters.py. zarr_dataset_multi re-exports them so the Hydra
_target_s (MultiDataset._from_resolver) and older imports keep resolving.

Also moved, since both the zarr and Lance paths use them:

  • zarr/action_chunk_transforms.py -> rldb/transforms.py
  • zarr/norm_cache.py, zarr/episode_norm_samples.py -> rldb/norm/
  • zarr/utils.py (set_global_seed) -> utils/seed.py

MultiDataset._from_resolver no longer type-checks for LocalEpisodeResolver:
it forwards sync_from_s3 only when set, so a resolver that cannot sync now
raises instead of silently ignoring it. The tutorials that passed it to an
S3EpisodeResolver (a no-op) drop it.

norm_cache hashes both rldb.multi and zarr_dataset_multi as reader modules.
code_hash covers module source, so every existing norm cache misses once.

Co-Authored-By: Claude Opus 5.5 (1M context) [email protected]
Claude-Session: https://claude.ai/code/session_01KY4JqVTaiGrzTaprPEzwZo

rldb: EpisodeLeaf base for zarr and Lance leaves

LanceRecordingDataset subclassed ZarrDataset only for its keymap windowing,
and carried a hand copy of the end of ZarrDataset.getitem that had already
drifted (no multi-camera or 3x4 K handling). Both now subclass
egomimic/rldb/leaf.EpisodeLeaf, which owns:

  • the windowing, padding and mask helpers (_read_windows, _history_indices,
    _action_pad_mask, ...), with _chunk_end_idx still the per-subclass hook;
  • _finish: masks, transforms, tensors, embodiment, fps, intrinsics and
    episode_hash for every leaf. A flat 9-value K (how Lance stores it) is now
    reshaped instead of becoming the NaN sentinel;
  • the batch contract (plan_sample / fetch_planned / assemble_sample) and a
    default cache_fingerprint.

MultiDataset.getitems batches leaves with batched_fetch set rather than
any leaf with a fetch_planned attribute, which every leaf now has.
LanceStreamingDataset and format_bench's FastZarrDataset set it.

test_leaf_equivalence reads one recording as a Lance leaf and as a zarr
episode written from the same rows and checks every sample matches.

Co-Authored-By: Claude Opus 5.5 (1M context) [email protected]
Claude-Session: https://claude.ai/code/session_01KY4JqVTaiGrzTaprPEzwZo

rldb/lance: annotations table with review, commit and loader support

annotations.lance is one global table keyed by a content-addressed
annotation_id, with half-open [t_start_ns, t_end_ns) intervals on the
recording clock and in-place grading columns. apply_annotations adds and
deletes in one commit; the release tag covers it like every other table.
The loader optionally returns all covering texts per frame, with opt-in
action clamping and annotated-only sampling. task_instance_id is now
deterministic, and the pilot commit's validation is reusable as
load_deliveries.

The format bench writes its spans to annotations.lance too, since the tasks
annotations column and schema.ANNOTATION are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) [email protected]
Claude-Session: https://claude.ai/code/session_01AeFJsNDQrLsyrDyJ2xgbrW

aria_process: Ray pipeline from raw Aria recordings to Lance tables

Converts and chunks recordings as Ray tasks and commits them in batches
from the driver; reruns skip committed recordings.

Co-Authored-By: Claude Opus 5.5 (1M context) [email protected]
Claude-Session: https://claude.ai/code/session_01AeFJsNDQrLsyrDyJ2xgbrW

https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw

@RyanPCo RyanPCo mentioned this pull request Oct 19, 2026

RyanPCo commented Oct 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@RyanPCo RyanPCo left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated adversarial review (read-only; findings were checked by a second skeptical pass before posting): 1 medium, 7 low, 5 nit.

errs.append(f"t_start_ns {t0} is not before t_end_ns {t1}")
if r["recording_id"] not in tasks:
errs.append(f"recording {r['recording_id']} has no tasks")
elif not any(s <= t0 and t1 <= e for s, e in tasks[r["recording_id"]]):

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium (correctness): This containment check rejects any annotation that covers a recording's last frame. Annotations are half-open, so such a span ends at ts[-1] + period. But aria_to_lance writes the task's t_end_ns as int(ts[-1]) (inclusive), and lance_pilot/commit.py copies that into tasks.lance. migrate-zarr produces exactly these end-of-clip spans, so review flags them and commit refuses the whole file. Please pick one convention: either make the task/alias end half-open in aria_to_lance (ts[-1] + period), or allow t1 <= e + period here. Also make tests/fixtures/lance_tables.py match what aria_to_lance actually writes.

Comment thread egomimic/rldb/multi.py Outdated
expected = None

# Only LocalEpisodeResolver can sync; other resolvers reject the kwarg.
extra = {"sync_from_s3": True} if sync_from_s3 else {}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low (backward-compat): This now passes sync_from_s3=True to every resolver. The old code passed it only to LocalEpisodeResolver. S3EpisodeResolver.resolve, the annotation-cutoff resolver and LanceEpisodeResolver.resolve don't accept the kwarg, so _from_resolver(S3EpisodeResolver(...), sync_from_s3=True) (as used in zarr_data_viz.ipynb) raises TypeError. Either drop the kwarg for resolvers that don't take it, or remove sync_from_s3 entirely and update the notebook. Also fix the comment: LocalEpisodeResolver is the one that ignores the flag.

def append_chunks(uri: Path, deliveries: list, camera: str, k: int) -> None:
"""Chunk rows for deliveries not yet in the chunk table, each recording's rows contiguous."""
have = chunked_ids(uri)
todo = [d for d in deliveries if d.recording_id not in have]

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low (data-integrity): Because chunk rows are written before commit.append, a crash between the two leaves chunk rows for an uncommitted recording. A rerun with --reconvert re-encodes the MP4, but this filter keeps the old chunks and also skips the num_frames check. The streaming reader then decodes stale chunks, or fails at train time if the frame count changed. Under --reconvert, delete the existing chunk rows for uncommitted recordings first. Alternatively, check the existing rows' total frames against the delivery and fail on a mismatch.

from egomimic.rldb.filters import DatasetFilter, PinError, _ensure_dataset_filter
from egomimic.rldb.leaf import EpisodeLeaf

# Re-exported: Hydra `_target_`s (MultiDataset._from_resolver) and older imports

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low (naming): MultiDataset now lives in egomimic.rldb.multi, but all 16 data configs, aria_pilot_lance.yaml included, still target egomimic.rldb.zarr.zarr_dataset_multi.MultiDataset._from_resolver, so a Lance-only run imports the zarr module just to reach it. Point at least the Lance config, and ideally all MultiDataset targets, at egomimic.rldb.multi.MultiDataset._from_resolver. Then trim this re-export block to what external callers still need, and drop _IndexMap and SEED.

Comment thread egomimic/rldb/lance/commit.py Outdated
def edit(root: Path, table: str, rows: pa.Table, on: str) -> None:
"""Update columns of existing rows keyed by `on` (label and metadata fixes); every key must exist."""
if table not in CATALOG:
if table not in CATALOG and table != "annotations.lance":

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

low (correctness): edit() now accepts annotations.lance, but the error still says edit only applies to {list(CATALOG)}. Build an editable set that includes annotations (reuse annotations_uri(root).name rather than the literal) and list that set in the message. A merge_insert edit to text or t_*_ns on annotations.lance would also leave annotation_id out of sync with the row's content. Either restrict edits there to the grading columns or say in the docstring that content changes must go through apply_annotations.

Comment thread egomimic/rldb/lance/commit.py Outdated
def edit(root: Path, table: str, rows: pa.Table, on: str) -> None:
"""Update columns of existing rows keyed by `on` (label and metadata fixes); every key must exist."""
if table not in CATALOG:
if table not in CATALOG and table != "annotations.lance":

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (data-integrity): edit now accepts annotations.lance, but annotation_id is a uuid5 of the content columns. Editing text or the timestamps here would leave a stale id, which breaks later deletes and the resubmit dedupe. Consider allowing only the grading columns (quality_score, graded_by, rubric_version) for this table. The error message also still says edit applies only to CATALOG; please update it.

Comment thread egomimic/rldb/filters.py Outdated
if isinstance(filters, DatasetFilter):
return filters
raise TypeError(
"filters must be a DatasetFilter or None in the zarr resolver path. "

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (naming): Lance now calls this helper too (lance/dataset.py resolve), so the message 'in the zarr resolver path' names the wrong backend there. Drop that phrase. Since two modules import it, consider making it public (ensure_dataset_filter).

import pyarrow as pa
import pyarrow.parquet as pq

from egomimic.rldb.lance.commit import _exists, annotations_uri

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (naming): _exists is now used outside commit.py, and the new aria_to_lance_ray.py repeats the same (uri / "_versions").exists() check at lines 83 and 115. Make it public (e.g. commit.table_exists) and use it in both places.


have = commit.committed_ids(args.lance)
vids = [v for v in discover(args.raw_dir) if recording_id("aria", v) not in have]
delivered = {v for v in vids if (args.deliveries / v / "meta.json").exists()} - (

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (simplification): Simpler and equivalent: delivered = set() if args.reconvert else {v for v in vids if (args.deliveries / v / "meta.json").exists()}.

Comment thread tests/unit/test_lance_commit.py Outdated
recording_id=rid,
t_start_ns=0,
t_end_ns=n,
t_end_ns=n * 33_333_333,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit (test-quality): Use PERIOD_NS from fixtures.lance_tables (already imported from there) instead of the literal 33_333_333 here and in the alias below.

RyanPCo and others added 4 commits October 30, 2026 18:51
MultiDataset, EvenStrideDataset, pinned_episode_subset, worker_context and
_IndexMap move to egomimic/rldb/multi.py; PinError and _ensure_dataset_filter
to rldb/filters.py. zarr_dataset_multi re-exports them so the Hydra
`_target_`s (MultiDataset._from_resolver) and older imports keep resolving.

Also moved, since both the zarr and Lance paths use them:
- zarr/action_chunk_transforms.py -> rldb/transforms.py
- zarr/norm_cache.py, zarr/episode_norm_samples.py -> rldb/norm/
- zarr/utils.py (set_global_seed) -> utils/seed.py

MultiDataset._from_resolver no longer type-checks for LocalEpisodeResolver:
it forwards sync_from_s3 only when set, so a resolver that cannot sync now
raises instead of silently ignoring it. The tutorials that passed it to an
S3EpisodeResolver (a no-op) drop it.

norm_cache hashes both rldb.multi and zarr_dataset_multi as reader modules.
code_hash covers module source, so every existing norm cache misses once.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KY4JqVTaiGrzTaprPEzwZo
LanceRecordingDataset subclassed ZarrDataset only for its keymap windowing,
and carried a hand copy of the end of ZarrDataset.__getitem__ that had already
drifted (no multi-camera or 3x4 K handling). Both now subclass
egomimic/rldb/leaf.EpisodeLeaf, which owns:

- the windowing, padding and mask helpers (_read_windows, _history_indices,
  _action_pad_mask, ...), with _chunk_end_idx still the per-subclass hook;
- _finish: masks, transforms, tensors, embodiment, fps, intrinsics and
  episode_hash for every leaf. A flat 9-value K (how Lance stores it) is now
  reshaped instead of becoming the NaN sentinel;
- the batch contract (plan_sample / fetch_planned / assemble_sample) and a
  default cache_fingerprint.

MultiDataset.__getitems__ batches leaves with batched_fetch set rather than
any leaf with a fetch_planned attribute, which every leaf now has.
LanceStreamingDataset and format_bench's FastZarrDataset set it.

test_leaf_equivalence reads one recording as a Lance leaf and as a zarr
episode written from the same rows and checks every sample matches.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01KY4JqVTaiGrzTaprPEzwZo
annotations.lance is one global table keyed by a content-addressed
annotation_id, with half-open [t_start_ns, t_end_ns) intervals on the
recording clock and in-place grading columns. apply_annotations adds and
deletes in one commit; the release tag covers it like every other table.
The loader optionally returns all covering texts per frame, with opt-in
action clamping and annotated-only sampling. task_instance_id is now
deterministic, and the pilot commit's validation is reusable as
load_deliveries.

The format bench writes its spans to annotations.lance too, since the tasks
annotations column and schema.ANNOTATION are gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AeFJsNDQrLsyrDyJ2xgbrW
Converts and chunks recordings as Ray tasks and commits them in batches
from the driver; reruns skip committed recordings.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Claude-Session: https://claude.ai/code/session_01AeFJsNDQrLsyrDyJ2xgbrW
@RyanPCo

RyanPCo commented Oct 30, 2026

Copy link
Copy Markdown
Contributor Author

Review findings: status after the fixup pass. The fixes are squashed into this PR's commits, and the whole stack was force-pushed.

Fixed

  • Task-containment check rejects annotations covering a recording's last frame (inclusive task t_end_ns): Already fixed later in the stack: aria_to_lance: half-open task and alias end #727 (f329ae7) makes aria_to_lance write a half-open t_end_ns, and the fixture already uses half-open ends. No new commit.
  • sync_from_s3=True forwarded to every resolver -> TypeError: Passed only when resolve() has a sync_from_s3 parameter (checked with inspect.signature). Corrected the comment and added a test in test_pins.
  • edit() on annotations.lance can change content without recomputing annotation_id: On annotations.lance, edit only accepts annotation_id plus the grading columns and raises otherwise. Added a test.
  • append_chunks keeps stale chunk rows under --reconvert: append_chunks(redo=args.reconvert) deletes a delivery's existing chunk rows before appending. Added a unit test that fakes Ray. rldb/lance: stream frames from MP4 byte ranges, drop chunk tables #730 later deletes the chunk path, and this code and its test go with it.
  • Re-export shim keeps Hydra configs pointing at zarr module: All 15 data configs now target egomimic.rldb.multi.MultiDataset._from_resolver (these configs are outside the PR). The shim drops _IndexMap and SEED, and test_index_map imports from rldb.multi. The public re-exports stay for older imports.
  • edit() error message still lists only CATALOG: Added ANNOTATIONS_TABLE and EDITABLE constants. annotations_uri uses the constant, and the error lists EDITABLE.
  • append_chunks duplicates chunk_videos row builder: Added chunk_videos.chunk_table (with the coverage check) and CHUNK_ROWS_PER_FILE, used by both append_chunks and chunk_videos.main (chunk_videos.py is outside the PR). Removed again in rldb/lance: stream frames from MP4 byte ranges, drop chunk tables #730 along with the chunk tables.
  • Convoluted delivered set expression: Now: set() if --reconvert, otherwise the delivered set.
  • --approver required but never recorded: apply_annotations and _replace take a commit message. cmd_commit stores the file, approver and date on the Lance commit. Added a test that reads it back with read_transaction. The message is kept after lance: annotation importer for zarr and the Scale bucket, tagged commits #733/lance: annotations go through the committer; provenance in changes #734 move commits into the committer, which passes the staged file and approver.
  • Hardcoded 33_333_333 instead of PERIOD_NS: Uses PERIOD_NS from fixtures.lance_tables.

Partial

  • _ensure_dataset_filter private cross-module API; message says zarr path: Fixed the error message. Did not rename the function: the rename touches call sites that rldb/lance: one leaf per recording, sampled over its task segments #722 rewrites, so it would cause autosquash conflicts.
  • Private commit._exists used across modules; aria_to_lance_ray re-implements it: aria_to_lance_ray now uses commit._exists instead of its inline copies. Did not rename to table_exists: _exists predates this stack.

Not fixed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant