Skip to content

norm stats: cache sampled rows per episode so splits share them - #687

Draft
RyanPCo wants to merge 2 commits into
ryanco/mecka-bad-egomotionfrom
ryanco/norm-stats-per-episode
Draft

RyanPCo wants to merge 2 commits into
ryanco/mecka-bad-egomotionfrom
ryanco/norm-stats-per-episode

Conversation

@RyanPCo

@RyanPCo RyanPCo commented Sep 24, 2026 •

Copy link
Copy Markdown
Contributor

Norm stats were cached per split, so a new episode set (20% -> full, an operator hold-out, newly collected data) resampled every episode. Each episode's sampled proprio/action rows are now cached on their own under <cache_dir>/episodes/, keyed by the resolver recipe and reader code but not by the split, and a split's stats are computed from the union of its episodes' rows. Frames sit on a power-of-two stride with a per-episode phase, so a file cached at stride s serves every coarser stride.

Adds norm_stats.max_samples (now part of the split key; KEY_VERSION 3).

Also probes one episode per keymap instead of every episode, and filters SQL pins without per-row pandas.

Tests: tests/unit/test_episode_norm_samples.py, test_norm_cache.py, test_populate_probe.py.


Folded in (to keep the stack under Graphite's 50-branch cap):

🤖 Generated with Claude Code

https://claude.ai/code/session_01HjCQBqQCr2igFEMR7yUovw

RyanPCo commented Sep 24, 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.

@aidang3019
aidang3019 force-pushed the ryanco/mecka-bad-egomotion branch from 56731c5 to c39e112 Compare September 30, 2026 01:07
@aidang3019
aidang3019 force-pushed the ryanco/norm-stats-per-episode branch from dedae99 to 1c8e707 Compare September 30, 2026 01:07
@RyanPCo
RyanPCo force-pushed the ryanco/norm-stats-per-episode branch from 1c8e707 to e51b3b4 Compare October 12, 2026 20:26
@RyanPCo
RyanPCo force-pushed the ryanco/mecka-bad-egomotion branch 2 times, most recently from c5fb24d to add5691 Compare October 12, 2026 21:50
@RyanPCo
RyanPCo force-pushed the ryanco/norm-stats-per-episode branch from e51b3b4 to 3488d12 Compare October 12, 2026 21:50
@RyanPCo
RyanPCo force-pushed the ryanco/norm-stats-per-episode branch from 3488d12 to ece3cd2 Compare October 12, 2026 23:19
@RyanPCo
RyanPCo force-pushed the ryanco/mecka-bad-egomotion branch from add5691 to d4b33aa Compare October 12, 2026 23:19
@RyanPCo RyanPCo mentioned this pull request Oct 19, 2026
@RyanPCo
RyanPCo force-pushed the ryanco/mecka-bad-egomotion branch from d4b33aa to 98d8d22 Compare October 19, 2026 19:53

@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): 3 low, 2 nit.

drawn uniformly from ``leaves`` (episode hash -> ZarrDataset). Episodes
missing from ``root`` at the needed density are sampled and cached first."""
root = Path(root)
fingerprints = {

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 (duplication): collect() recomputes the episode fingerprints that trainHydra just computed for the split cache key, a scandir plus a stat per array per episode, done twice on a split-cache miss. Pass trainHydra's dict through infer_norm_from_dataset(..., episode_fingerprints=...) into collect(). Also handle a None fingerprint (episode_fingerprint returns None on an OSError), which currently crashes _file/find with a TypeError on fingerprint[:16]. Sample such an episode without caching it.

if pending[h] == 0:
_write(
_file(root, h, fingerprints[h], stride),
idx_buf.pop(h, []),

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): A frame that fails in _Frames.__getitem__ becomes None, but the episode's npz is still written once pending[h] hits 0. A transient failure is therefore cached permanently as a partial or empty sample, keyed only by recipe, fingerprint and stride. Skip the cache write (keep the rows in memory) when any frame of the episode failed, or record the failure count and treat such a file as a miss in find. Relatedly, _write swallows OSError, but collect then np.loads files[h] unconditionally and crashes. Fall back to the in-memory rows there.

missing from ``root`` at the needed density are sampled and cached first."""
root = Path(root)
fingerprints = {
h: norm_cache.episode_fingerprint(getattr(ds, "episode_path", None))

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 (error-handling): norm_cache.episode_fingerprint can return None (scandir failure, no metadata files), and find/_file then evaluate fingerprint[:16] on it and raise TypeError at startup. The old cache path only hashed the value, so it tolerated None. Treat a None-fingerprint episode as uncacheable: sample it without writing, or fall back to the whole-dataset loader.

return {str(k): z[f"k{i}"][rows] for i, k in enumerate(z["keys"])}


def _to_numpy(x):

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 (duplication): This local _to_numpy duplicates egomimic.utils.type_utils._to_numpy. Import that one instead.

generator=torch.Generator().manual_seed(seed),
worker_init_fn=die_with_parent,
)
if episode_cache is not None and not all(

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): The double negation is hard to read. An equivalent form is if episode_cache is not None and any(isinstance(ds, MultiDataset) or getattr(ds, "episode_path", None) is None for ds in dataset.datasets.values()):.

RyanPCo and others added 2 commits October 30, 2026 18:51
Norm stats were cached per split, so a new episode set (20% -> full, an
operator hold-out, newly collected data) resampled every episode. Each
episode's sampled proprio/action rows are now cached on their own under
<cache_dir>/episodes/, keyed by the resolver recipe and reader code but not by
the split, and a split's stats are computed from the union of its episodes'
rows. Frames sit on a power-of-two stride with a per-episode phase, so a file
cached at stride s serves every coarser stride.

Adds norm_stats.max_samples (now part of the split key; KEY_VERSION 3).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0118RhoJmzcWC75B3vtc3zkF
…andas

Startup on the 41.6k-episode full-mecka split spent ~20 min before training:
populate_from_datasets read a full sample from every episode to learn the
post-transform keys, though a resolver hands every episode the same key_map
and transform objects. It now probes one episode per (embodiment, key_map,
transform) group, trying up to 8 before falling back to the raw key_map.

Resolving pinned filters was ~50 s each: a row-wise df.apply(row.to_dict())
over the ~458k-row episode table plus a per-pin .loc in _check_pins. Pinned
filters now narrow the table to their hashes first, rows are filtered as
records, and _check_pins builds one dict of pinned rows. Same paths on the real
table; 41.6k pins 2.5 s, val pins ~0 s, unpinned lab filter 32 s -> 13 s.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_0118RhoJmzcWC75B3vtc3zkF
@RyanPCo
RyanPCo force-pushed the ryanco/mecka-bad-egomotion branch from 0a74649 to bd2cb3e Compare October 30, 2026 23:05
@RyanPCo
RyanPCo force-pushed the ryanco/norm-stats-per-episode branch from b28a7ff to 36f7cf1 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

  • Episode fingerprints computed twice; None fingerprint crashes: collect(fingerprints=) and infer_norm_from_dataset(episode_fingerprints=) take trainHydra's dict; an episode with a None fingerprint is used from memory and not cached; tests added
  • Re-implements type_utils._to_numpy: imports type_utils._to_numpy
  • Double-negative all(not ...) guard: rewritten as any(isinstance(MultiDataset) or episode_path is None)
  • Transient sample failures cached permanently: an episode with any failed sample (or a failed write) is used from memory and not written to the cache; test added; no rank-0 broadcast added
  • None episode fingerprint crashes collect: fixed with the item above: None-fingerprint episodes are sampled but not cached

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