Skip to content

lance migration - #716

Open
RyanPCo wants to merge 6 commits into
ryanco/rdt-memoryfrom
ryanco/lance-migration
Open

RyanPCo wants to merge 6 commits into
ryanco/rdt-memoryfrom
ryanco/lance-migration

Conversation

@RyanPCo

@RyanPCo RyanPCo commented Oct 14, 2026 •

Copy link
Copy Markdown
Contributor

Lance tables (steps, recordings, tasks, aliases) and MP4 videos for Aria
recordings, a LanceEpisodeResolver that builds ZarrDataset-compatible
leaves, a committer (append, replace, edit, add_columns, tag, compact,
cleanup), the aria_to_lance converter, pilot benchmarks and A/B data
configs. Norm caches key on a leaf's own fingerprint so Lance leaves
reuse them; loaders over Lance use forkserver workers.

lab is the collecting lab (e.g. rl2), as in zarr and SQL; there is no
site column, so filter lambdas see lab directly. A required
end_effector column (END_EFFECTOR: hand, gripper) sits next to
embodiment, and steps are split into one table per (lab, embodiment,
end_effector) at steps/{lab}/{embodiment}/{end_effector}.lance. The
committer derives each recording's table and rejects missing or unknown
values; the resolver reads each recording from its own table, so one
resolver can span labs. recording_id(source, vendor_recording_id) hashes
the converter's fixed source name ("aria/"), unrelated to
lab.

resolve() finds recording boundaries with pc.run_end_encode instead of a
numpy object array: 8.4 s -> 3.2 s at 6,120 recordings.

Streaming from object storage: LanceStreamingDataset reads frames from
8-frame video chunk rows, and MultiDataset.getitems fetches a whole
DataLoader batch at once (one steps take plus one read_blobs per chunk
table, overlapped). format_bench compares LeRobot v3.0, zarr and Lance
on storage, startup and local/S3 throughput.

The fps=10 cases in the image history/memory tests move to 60, since only
30 and 60 will be valid episode rates.

Stacked on #689 (previously #690). Depends on pylance 13.0.0 (pyproject.toml, uv.lock).

🤖 Generated with Claude Code

https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw

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 force-pushed the ryanco/lance-migration branch from e890bcb to 136fbff Compare October 23, 2026 19:28
@RyanPCo
RyanPCo force-pushed the ryanco/lance-migration branch from 121ad91 to 49123e8 Compare October 30, 2026 18:27

@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 high, 6 medium, 10 low, 11 nit.

out = [None] * len(idxs)
groups: dict[type, list] = {}
for pos, idx in enumerate(idxs):
name, local = self.index_map[idx]

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.

high (correctness): EvenStrideDataset inherits this getitems without overriding it. Inside it, self.index_map resolves through __getattr__ to base.index_map, so a stride-space index (0..len(indices)) is read as a base-global index. For LanceStreamingDataset leaves (the only ones with fetch_planned), a strided val loader silently evaluates roughly the first len(indices) base samples instead of the strided subset. Please override it in EvenStrideDataset: def __getitems__(self, idxs): return self.base.__getitems__([self.indices[i] for i in idxs]), and add a test that compares batched and per-item output for a wrapped streaming dataset.

Comment thread egomimic/rldb/lance/schema.py Outdated


def recording_id(source: str, vendor_recording_id: str) -> str:
"""`source` is a fixed per-converter namespace ("aria", "eva"), unrelated to the `lab` column."""

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 (consistency): This docstring says the first argument is a per-converter namespace ("aria") unrelated to lab. mcap/spec.py (which says it copies this function), validate.py and SPEC.md all call it with the lab. Both use the same UUIDv5 namespace, so one vendor recording gets uuid5(ns, "aria/<id>") from aria_to_lance and uuid5(ns, "rl2/<id>") from the validator. The promised agreement test only compares the namespace constant, so it won't catch this. Please pick one meaning and use it in this function, spec.py, SPEC.md and the aria_to_lance/duplicate.py callers.

Comment thread egomimic/trainHydra.py
)
episodes = {
h: norm_cache.episode_fingerprint(getattr(ds, "episode_path", None))
h: norm_cache.leaf_fingerprint(ds)

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 switches the norm-cache key to norm_cache.leaf_fingerprint(ds), but egomimic/scripts/precompute_norm_stats.py (~line 173) still uses episode_fingerprint(getattr(ds, "episode_path", None)). For Lance leaves the two fingerprints differ, so a precompute run on a Lance config such as aria_pilot_lance writes an entry that training never hits, and training quietly recomputes. Switch precompute to leaf_fingerprint too, or better, move the episodes-dict construction into one norm_cache helper that both callers use.

embodiment=text_or_none(s.embodiment),
end_effector=m.get("end_effector"),
rig_name=text_or_none(s.rig_name),
operator=text_or_none(s.operator),

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 (data-integrity): operator and task_name (line 126) can come out of SQL as None, and nothing rejects them: commit.steps_key only checks lab/embodiment/end_effector. Recordings with missing operator or task then commit, and task filters silently drop them. Reject the delivery when either is missing. Better, enforce required metadata (lab, task_name, operator) in rldb/lance/commit so every committer gets the check.

Comment thread egomimic/rldb/lance/commit.py Outdated
def append(root: Path, deliveries: list[Delivery]) -> list[str]:
"""Commit new recordings; ones already in `recordings` are skipped, so re-running is a no-op."""
have = committed_ids(root)
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.

medium (data-integrity): append() is not idempotent after a partial failure. Steps are committed before recordings, and have only reflects recordings.lance. If the process dies after the steps append, a re-run appends the same steps rows again. The same happens if deliveries contains one recording_id twice, because todo is never deduplicated. LanceEpisodeResolver._steps_rows then raises ('not contiguous' or 'steps rows != num_frames') for the whole (lab, embodiment, end_effector) table until someone deletes the rows by hand. Suggested fix: deduplicate todo by recording_id, and write steps through _replace(steps_uri, 'recording_id IN (...)', table) so orphaned rows from an earlier failed attempt get dropped. Please also add a test that simulates a crash before the recordings commit.


- Lance: `storage_options` gains `allow_http` (plain-http endpoints), reading the same R2_* vars.
- zarr: `ZarrEpisode` opens s3:// paths through obstore, and reopens lazily in each worker rather
than pickling a live client (as logs/claude_scratch/zarr_r2_patch.py does for R2).

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 (leftover): logs/claude_scratch/zarr_r2_patch.py is not in the repo. Drop the parenthetical or describe the R2 pattern inline.

@@ -0,0 +1,35 @@
# Lance pilot baseline: the pilot's bimanual Aria zarrs, read locally. Pair with
# aria_pilot_lance.yaml (same recordings, keymap and transforms); see
# docs/lance_aria_pilot_plan.md.

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 (docs): docs/lance_aria_pilot_plan.md does not exist on this branch or later ones in the stack. Remove the reference or add the doc. tests/integration/test_lance_versions.py likewise refers to 'Phase 7 of the Lance pilot' and prints PHASE7; describe the test without the plan's phase numbering.

stream_ts = {"rgb": list(rgb_ts)}

# Same per-frame pose math as aria_to_zarr, on every RGB frame (no clean_data).
both = "both"

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 (dead-code): both = "both" only aliases a literal; pass arm="both" directly. log at line 35 is never used and can be removed.

assert _all_same(s1, _samples(t1), s1) == []
assert {h: leaf.cache_fingerprint() for h, leaf in t1.items()} == fp1
assert _row_count_ok(root, tag="t1") and _row_count_ok(root, tag="t6")
with pytest.raises(Exception):

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): pytest.raises(Exception) also passes on an unrelated TypeError or KeyError. Narrow it to the exception Lance raises for a missing version and add match= so the test checks what it means to check.

Comment thread .gitignore Outdated

scratch/
sample_data/
/fb/

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 (leftover): Nothing in the repo writes to /fb/ (every bench --out is required or defaults elsewhere), so this looks like local working state. Remove it, or have the bench scripts default to an existing ignored directory such as scratch/.

RyanPCo and others added 6 commits October 30, 2026 18:51
Lance tables (steps, recordings, tasks, aliases) and MP4 videos for Aria
recordings, a LanceEpisodeResolver that builds ZarrDataset-compatible
leaves, a committer (append, replace, edit, add_columns, tag, compact,
cleanup), the aria_to_lance converter, pilot benchmarks and A/B data
configs. Norm caches key on a leaf's own fingerprint so Lance leaves
reuse them; loaders over Lance use forkserver workers.

resolve() finds recording boundaries with pc.run_end_encode instead of a
numpy object array: 8.4 s -> 3.2 s at 6,120 recordings.

Streaming from object storage: LanceStreamingDataset reads frames from
8-frame video chunk rows, and MultiDataset.__getitems__ fetches a whole
DataLoader batch at once (one steps take plus one read_blobs per chunk
table, overlapped). format_bench compares LeRobot v3.0, zarr and Lance
on storage, startup and local/S3 throughput.

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

`lab` held the source format and `site` the collecting lab; lab now means the
collecting lab as in zarr and SQL, and site is gone, so filter lambdas see the
column directly instead of through a resolver rename. A required
`end_effector` column (END_EFFECTOR: human_hand, parallel_jaw_gripper) joins embodiment, and
steps tables are split per (lab, embodiment, end_effector) under
steps/{lab}/{embodiment}/{end_effector}.lance. The committer derives the table
from each recording and rejects missing or unknown values; the resolver reads
each recording from its own table, so one resolver can span labs. Bench and
pilot tools derive the steps path from the recordings table.

recording_id keeps hashing the converter's fixed source name ("aria"), so
existing ids and everything derived from them are unchanged.

fps=10 cases in the image history/memory tests move to 60: only 30 and 60
will be valid episode rates.

Drops aria_pilot_lance_stream.yaml: its S3 root
(lance_pilot/lance_faststart) is stale and nothing else references it.

Claude-Session: https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw
The lance tables and loader in this PR import lance; without the pin CI
cannot import them.

Claude-Session: https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw
…ithout obstore

The JPEG retry lands on a random frame. When that frame is under 3, the
history window repeats frame 0, but the expected slice was shorter. CI hit
it on every run. obstore is only used by the format bench and is not a
dependency.

Claude-Session: https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw
@RyanPCo
RyanPCo force-pushed the ryanco/lance-migration branch from 49123e8 to b7ceab0 Compare October 30, 2026 23:05
@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

  • EvenStrideDataset inherits the new getitems and maps its local indices through base.index_map: EvenStrideDataset.getitems now forwards through self.indices to base.getitems. Added a test that runs it over a streaming Lance dataset.
  • trainHydra keys the norm cache on leaf_fingerprint, but precompute_norm_stats.py still uses episode_fingerprint: precompute_norm_stats.py now uses norm_cache.leaf_fingerprint. That file is outside the PR, but the fix requires changing it.
  • leaf_fingerprint is used by trainHydra but not by precompute_norm_stats: Duplicate of the trainHydra finding; the same commit fixes it.
  • Pilot committer accepts recordings with null operator and task_name: New commit.check_required(), used by append and replace, rejects a delivery with no lab, operator or task_name. The pilot committer lists such deliveries as rejected. Tests added. Later PRs' committers (lance: S3 committer fed by a staging prefix #731, rldb/lance: steps schemas for yam_bimanual and dexmate_bimanual #749) keep the check.
  • append() is not idempotent after a partial failure: append deduplicates by recording_id. It replaces rows that an interrupted run left in steps, aliases and tasks, instead of appending them again. Added a test that simulates a crash before the recordings commit.
  • check_samples opens a hardcoded steps/aria.lance: Reads each leaf's own steps_uri at its steps_version. The import lance moved to the top of the file.
  • torchcodec is a runtime dependency but is not declared: Added torchcodec==0.4.0 (the build for torch 2.7) and a 'bench' extra with obstore and google-crc32c, then relocked. The local uv also added platform markers across uv.lock.
  • _finish copies the tail of ZarrDataset.getitem: Moved the shared tail into ZarrDataset._finalize_sample and deleted _finish. Lance intrinsics now come back as a 3x3 or 3x4 matrix. Added a test. From rldb: move MultiDataset and format-neutral pieces out of zarr/ #717 on, EpisodeLeaf._finish is that single shared tail for zarr and Lance leaves.
  • Embodiment comes from SQL with no check against the converter's embodiment: The pilot committer rejects a delivery when meta['embodiment'] differs from the SQL embodiment.
  • MANO fit runs on every frame, including untracked ones: Fits only the tracked frames and fills the rest with NaN. Each frame's fit is independent (its own beta, elementwise Adam), so tracked frames get the same results. aria_to_lance: 6.5x faster MANO fit, same output #753's batched fit keeps this.
  • tag()/compact()/cleanup() skip video_chunks tables: table_uris now includes video_chunks/*.lance, so tag, compact and cleanup cover them. Added a test. rldb/lance: stream frames from MP4 byte ranges, drop chunk tables #730 drops chunk tables entirely, so that coverage and its test go away there.
  • Eligibility covers only the action chunk; history frames can be invalid: eligible_intervals also requires head and hand validity at the frames that non-camera history reads, using the same offsets as zarr's _history_indices at the recording's fps. Tests added. rldb/lance: require_hands_in_frame and per-hand sample masks #724's require_hands_in_frame modes apply the same history check to the hands they require.
  • edit() can rewrite lab/embodiment/end_effector on recordings: edit on recordings.lance rejects a change to a STEPS_KEY column and still allows unchanged values. Test added.
  • Committed data configs and script defaults point at /coc/flash7/scratch/lance_pilot: The env var EGOVERSE_LANCE_PILOT sets the path, with the old path as the default. Configs read it via oc.env; scripts use the single constant lance_pilot.PILOT (both kept when lance_pilot: migrate pilot tables to one lab column and end_effector #741 moves them to lance_v2).
  • index() relies on table_uris ordering via a negative slice: New steps_uris() helper, used by index().
  • Conditional expression used only for its side effect: Replaced with a plain if/else.
  • _horizon crashes when key_map is None: key_map is now a required argument, and the ad-hoc or {} guard is gone.
  • worker_context special-cases leaves that _iter_leaves already handles: Simplified to a single any() over _iter_leaves, in the same commit as the EvenStrideDataset fix.
  • chunk_videos docstring says chunk tables go under the Lance root: --out-root now defaults to --lance-root.
  • to_lerobot docstring says no egomimic imports; column() annotation wrong: Docstring now says egomimic must be importable; column() is annotated as returning pa.Array.
  • s3patch docstring references a personal scratch file: Removed the parenthetical.
  • Comment points to docs/lance_aria_pilot_plan.md, which does not exist: Removed that reference and the docs/ reference in the schema.py docstring. Dropped 'Phase 7' from the test docstring.
  • Unused logger, a pointless both alias and a magic 480: Removed log and both, and the scale now comes from VIDEOS. The 1e9 fill and lr=0.05 went away with the MANO change.
  • Correctness test measures throughput and prints a PHASE7 report: Removed _take_rate, the report dict and the print. The assertions stay.
  • pytest.raises(Exception) accepts any failure: Now pytest.raises(ValueError, match='Not found'). A probe on a compute node showed Lance raises ValueError for a missing manifest and ArrowInvalid (a ValueError subclass) for a removed data file.
  • Unexplained /fb/ ignore entry: Removed; nothing in the repo writes to /fb/.

Partial

  • recording_id namespace argument contradicts the copy in mcap/spec.py: Docstring now says the namespace is whoever assigns vendor_recording_id: the converter for in-house data, the lab for MCAP. I kept the 'aria' ids, which the lab-column commit kept on purpose. mcap/spec.py and SPEC.md belong to another PR and are unchanged.

Not fixed

  • About 4.7k lines of one-off benchmark tooling ship inside the egomimic package: Design-level split or move of the bench tooling, left for the author.

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