Skip to content

Aniketh/arc - #573

Open
AnikethCheluva wants to merge 87 commits into
mainfrom
aniketh/arc
Open

Aniketh/arc#573
AnikethCheluva wants to merge 87 commits into
mainfrom
aniketh/arc

Conversation

@AnikethCheluva

Copy link
Copy Markdown
Collaborator

No description provided.

rpunamiya and others added 30 commits July 10, 2026 12:15
…pling

Port of the GR00T arc-length tokenizer (gr00t branch
rpunamiya/arc-length-tokenizer) adapted to EgoVerse conventions:

- egomimic/rldb/zarr/arc_length_tokenizer.py: chunks each arm's EEF
  trajectory into fixed translational arc-length units, resamples to M
  waypoints uniform in arc length (pos/gripper lerp, rotation SLERP via
  scipy on ypr ZYX euler instead of rot6d), and appends per-arm
  translational-velocity channels for timing reconstruction. Operates on
  the canonical (T, 14) bimanual cartesian layout -> (M, 14 + 2*vel_dim);
  per-arm independent arc length; zero tokens for stationary arms;
  >=1e8 invalid-pose sentinel handling; all four velocity modes
  (mean_scalar, mean_per_dim, per_step_scalar, per_step_per_dim);
  deploy-time detokenize back to a time-indexed chunk (mean modes);
  optional per-arm joint chunks resampled along that arm's arc length;
  duck-typed Transform adapter (numpy/scipy only, no torch import).
- egomimic/rldb/zarr/arc_length_stats.py: data-driven
  {min,max,mean,std,q01,q99} stats for the synthesized trans_vel
  channels, computed by tokenizing raw (T, 14) chunks (sentinel chunks
  skipped).
- 67 colocated unit tests covering arc-length math, SLERP correctness,
  velocity payloads per mode, per-arm independence, round-trip recovery,
  curved-path fidelity, joint resampling, stats, and the adapter.

Verified numerically equivalent (machine epsilon) to the GR00T reference
implementation on shared trajectories, including per-dim payloads,
per-dim detokenize, and joint resampling.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…isodeResolver, legacy embodiment aliases

New arc-tokenizer transform:
  * arc_length_tokenizer.py: TokenizeBimanualArcLengthCartesian +
    detokenize helper for the (M+1, 8) bimanual layout
    [Lx, Ly, Lz, L_grip, Rx, Ry, Rz, R_grip] with per-dim mean vel
    token appended at slot M.
  * eva.py / human.py: new keymap_mode=arc_tokenizer_cartesian
    (widened raw horizon so joint arc length has room to reach D)
    + get_transform_list mode with min_distance_unit and
    resampled_vector_length args. Human pipeline zero-pads grippers
    upstream of the tokenize step.

Also:
  * LocalFolderEpisodeResolver: folder-of-zarrs resolver used by the
    arc_tests_cotrain data yaml — no SQL, filters by zarr
    attrs.embodiment.
  * embodiment.py: alias legacy vendor embodiment strings
    (mecka_bimanual, aria_bimanual, ...) to human_* so pre-collapse
    caches load without a re-download.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ult)

Model head consumes (M+1, 8) arc-token layout from get_item — 15
waypoints + 1 per-dim vel token, xyz + gripper per arm.

  * hpt_cotrain_mecka_flow_shared_head_arc.yaml: base FMPolicy head
    at action_horizon=M+1, act_dim=8, act_seq=M+1, infer_ac_dims=8
    (both embodiments). Human gripper columns are zero-padded upstream.
  * hpt_cotrain_mecka_flow_shared_head_arc_D20_M15.yaml: overrides
    for the D=20cm/M=15 data config.
  * arc_tests_cotrain_arctok.yaml: data yaml using arc_tokenizer_cartesian
    keymap + transform on the arc_tests folder (290 eva + 580 human).
  * eval_arctok.py + eval_arctok.yaml: val-video eval that runs
    TokenizeBimanualArcLengthCartesian.detokenize on both prediction
    and GT before viz — reconstructs a time-parameterized (H, 14) chunk
    using the predicted vel token to set duration, then hands it to the
    canonical _viz_traj path so overlay dots project the same way as any
    non-arc cotrain val video.
  * eval_arctok_D20_M15.yaml: D=0.20, M=15 override matching the data
    config.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Interactive HTML tools for eyeballing what the arc tokenizer produces
before wiring it into a training run:

  * arc_embedding_viz.py: single-run t-SNE (or PCA/UMAP) over
    positions_delta / velocity / flatten features from a tokenized
    dataset. Front-camera thumbnail per point with green GT-action
    overlay; click a point to enlarge in a floating card.
  * arc_embedding_sweep.py: sweep over (joint_distance_cm × waypoints)
    with one tabbed t-SNE per (D, W). Widened raw window
    (raw_horizon = max(200, 2.5*max_D + 1)) so joint arc length can
    reach large D without _pad_sequences drowning the tail. Per-tab
    HDBSCAN + cluster stats sidebar. Partial-D toggle hides
    end-of-episode samples that reach <50% of D. Cross-embodiment
    KNN grid view: per HDBSCAN cluster, one medoid per embodiment
    plus the 4 nearest opposite-embodiment neighbors, rendered as a
    scrollable grid with image + overlay at higher resolution.
    TSNE/KNN switch via a top-bar button that does not touch the
    default TSNE UI.
  * arc_embedding_baseline.py: classic time-window (H frames of
    positions_delta) baseline for side-by-side comparison against
    the arc-token sweep. Uses class-default read horizons so the
    baseline chunk matches the cotrain model input exactly.

Supporting configs:
  * hydra_configs/arc_embedding*.yaml: sweep grids
    (D=[20,40,60,80] W=[5-30] default; extended D up to 280cm).
  * hydra_configs/data/{aria_eva_fold_clothes_rl2,
    aria_fold_clothes_lab, human_mecka_eva_rl2_fold_clothes,
    human_mecka_eva_rl2_folding_clothes, arc_tests_cotrain}.yaml:
    filtered / capped population variants for the sweep.
  * hydra_configs/embedder/{pca,tsne,umap}_{2d,3d}.yaml: embedder
    presets so the base arc_embedding.yaml can defaults-select
    dimensionality reducer.
  * scripts/embedding_process/arc_embedding{,_sweep,_baseline}.sbatch:
    PACE L40S/CPU-small submissions.
  * hydra_configs/hydra/launcher/submitit_pace_l40s.yaml:
    launcher variant used for the cotrain runs.
  * .gitignore: skip out/*.html + arc_*_*.html at repo root so
    generated HTMLs never sneak into the branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Switch data/arc_tests_cotrain{,_arctok}.yaml from LocalFolderEpisodeResolver
to S3EpisodeResolver so the episode set resolves from the SQL table and syncs
from R2 into ${paths.dataset_dir} on its own. The PACE folder the local
resolver depended on (/storage/project/r-dxu345-0/shared/arc_tests) does not
exist on OSMO, and LocalFolderEpisodeResolver explicitly does not sync.

Per-leaf DatasetFilter uses lab='rl2' and task='fold_clothes', split by
embodiment -> 290 eva_bimanual + 572 human_bimanual, verified against
app.episodes. The eva filter matches the one
human_mecka_eva_rl2_fold_clothes.yaml already uses.

Add osmo/arc_cotrain_l40s.yaml: 1 node x 8 L40S on groot-l40s-03, no submitit
(Lightning DDP runs the ranks in-container), per-GPU batch_size 32 -> 4,
num_workers 6 -> 4. Norm stats are computed once on 1 GPU and consumed by the
8-rank run, because trainHydra.py computes them unguarded by rank and ddp
re-executes the script per rank -- 8x redundant work plus a write race on
norm_stats.json otherwise. That phase also serializes the dataset download.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Hydra rejects `group=null` on the command line for config groups
("Config group override must be a string or a list. Got NoneType"),
which killed both runs at config composition. The delete syntax is what
the repo already uses elsewhere (~evaluator), and both instantiate_loggers
and instantiate_callbacks treat a missing group as empty.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both variants submitted under the same hardcoded `egoverse-arc-cotrain`,
so they were only distinguishable by the trailing counter in
`osmo workflow list`. Name is now a {{job_name}} template var; convention
is arc-<variant>-<hardware>.

Workflow names are immutable after submit and `osmo workflow tag` returns
403 for this profile, so already-submitted runs keep the old name.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Both 8xL40S runs died at wandb.init() with "CommError: permission denied":
logger/wandb.yaml hardcodes entity rl2-group, and the key in use only has
access to nv-gear. That happens in phase 2, so each failure burned the
~155GB dataset pull and the norm-stats phase first (~2.5h).

Add a {{wandb_entity}} override and check entity access right after
setup_secret.sh, before any of the expensive work, so a bad key fails in
seconds.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
api.viewer.teams returns plain strings, not objects, so `t.name` raised
AttributeError and the preflight killed the run it was meant to protect
(arc-tok-d20m15-8xl40s-1, 2.5min in). Handle both shapes. Verified against
a live key that both paths behave: exit 0 when the entity is reachable,
exit 1 with the accessible list when it isn't.

cpu 100 -> 80: most groot-l40s-03 nodes sit at 84/126 free, so 100 only
matched the emptiest few and cost ~6-8 min of queueing. 64 dataloader
workers still fit.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The cotrain commands carry ddp_find_unused_parameters_true, which costs a
graph traversal per step and suppresses the error that would reveal a
parameter silently not training. Reading the model did not settle whether
it is needed: per-domain heads are null (only the shared head registers),
use_domain_embedding is dead config, forward_training loops both domains
with a single backward, and crossattn_modality_dropout is plain dropout
rather than modality dropout.

Run the real model under plain ddp on 4 episodes / 2 GPUs with
TORCH_DISTRIBUTED_DEBUG=DETAIL so DDP names any parameter that receives
no gradient.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Eval mode sets cfg.logger = None (trainHydra.py:186), so the Valid/*
metrics the evaluator computes via log_dict had nowhere to go, and
trainer.validate()'s return value was discarded. Dump both it and
callback_metrics to eval_metrics.json in the run dir.

Add osmo/eval_ckpts_l40s.yaml: pulls a training run's checkpoints from
R2, then runs the matching evaluator over each one. Baseline uses
eval_hpt, arctok uses eval_arctok_D20_M15 -- the latter detokenizes
prediction and GT back to (B, 100, 14) cartesian via the predicted
velocity token, so its MSE is in the same physical units as the
baseline's and the two are directly comparable. Results are pushed to
R2 after each checkpoint so a late failure keeps earlier ones.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OSMO rejects disproportionate requests: 1 of 8 GPUs caps CPU at 15.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lightning writes "epoch_epoch=199.ckpt". The tag sanitizer kept '=', so
the sweep passed description=epoch_epoch=199 and hydra rejected it with
"mismatched input '=' expecting <EOF>" on the first checkpoint.

Also guard the eval_metrics.json lookup: under set -e a failing find in a
command substitution aborts the sweep, so one bad checkpoint took down
the whole job instead of being skipped.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Exporting AWS_DEFAULT_REGION=auto for the R2 checkpoint pull also
redirected boto3's Secrets Manager call to secretsmanager.auto.amazonaws.com,
so create_default_engine failed and S3EpisodeResolver never resolved --
every checkpoint errored. Scope the R2 creds to an r2() wrapper instead.

The job still exited 0 because the trailing summary succeeded, so OSMO
reported COMPLETED on a run that produced nothing. Assert at least one
checkpoint was captured.

Add FIRST_CKPT so the latest epoch shared by both runs is evaluated
first; remaining checkpoints then run newest-first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
trainHydra runs infer_norm_from_dataset before branching on mode, so eval
paid the full sample_frac=1.0 pass: ~1.33M samples at ~350/s, roughly 63
minutes per checkpoint (10.5h for a 10-checkpoint sweep). Nothing was
failing -- the job was just spending all its time here, which read as a
hang because no results appeared.

The training run's norm_stats.json is already synced alongside its
checkpoints, so pass it via precomputed_norm_path and assert it exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sanitizing the description was only half the problem. hydra's override
grammar rejects a second '=' anywhere in an override including the value,
and lightning names checkpoints "epoch_epoch=1599.ckpt", so
ckpt_path=<that> raised OverrideParseException immediately. Only
last.ckpt -- the sole filename without '=' -- ever got past parsing, and
that one then sat in the norm-stat recompute, which is why the sweep
looked like it was running for hours while producing nothing.

Symlink each checkpoint to an '='-free path and point ckpt_path at that.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
viz/cartesian.yaml wires annotation_key: sampled_prompt for every
embodiment and eval_hpt inherits it, but arc_tests has no annotations, so
viz_gt_preds raised KeyError('sampled_prompt') at the first validation
step. eval_arctok.yaml already documents and fixes this for the arc path;
do the same on the command line for the baseline path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
find -newer | head -1 matched any recent eval_metrics.json, so a
checkpoint that failed to write one silently inherited the previous
checkpoint's result under its own tag. In one sweep 5 of 7 published
results were duplicates of just two real evals; only the ckpt_path
embedded in each JSON revealed it.

Pin hydra.run.dir per checkpoint and read that exact file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ArcTokEvalVideo calls .contiguous() on the detokenized tensors, but the
subsequent advanced-index slices do not inherit it, and torchmetrics' MSE
uses .view() internally. pred_slice[:, -1] (the final_mse metric) is a
strided view, so it raised "view size is not compatible with input
tensor's size and stride" and took down every arc-tok eval.

Reproduced standalone: of the four metrics only final_mse fails, which is
why this survived until the detokenized path was actually exercised --
~evaluator meant it had never run.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every eval bug so far took 35-70min to surface because the 141GB dataset
pull sits in front of the code under test. debug_episodes=N caps each
resolver so the whole path -- checkpoint load, evaluator attach,
trainer.validate, eval_metrics.json, R2 push -- runs in minutes on 4
episodes. 'debug' is not in the resolver config block, so it needs a '+'
prefix; omitting that is what killed the earlier diagnostic.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
EvalVideo defaults limit_val_batches=400 and renders validation video
frames each batch (~45s/batch), so every checkpoint eval was a ~5 hour
job -- the sweeps were never hung, just doing 400 batches of video work.

It is a constructor arg that flows into override_dict, and trainHydra
applies override_dict AFTER the CLI (trainHydra.py:181), so
trainer.limit_val_batches from the command line was silently discarded.
Set it on the evaluator instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
limit_val_batches is an EvalVideo constructor arg and appears in no
evaluator yaml, so a plain override raises "Key 'limit_val_batches' is
not in struct" -- the same failure the resolver's debug key had. Add it
with a leading + so hydra appends rather than overrides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Two separate issues, both found by round-tripping a real human episode:

1. dt: the tokenizer was constructed with its 1/30 default while the human
   chunk is subsampled by actions[::stride]. At stride=3 that inflates the
   velocity channel by exactly 3.00x (measured). It cancels within
   tokenize->detokenize, so it does not affect the val overlay or the detok
   MSE, but it is what the model learns and what a deployed policy would
   command. Pass dt=stride/30.

2. stride: an arc-length model can only reconstruct on the sample grid, so a
   fixed D=0.20m arc yields ~D/(v*dt) points. stride=3 gives ~6 points at
   3.3cm spacing; stride=1 gives ~17 at 1.0cm with lower reconstruction RMSE
   (0.1645 vs 0.1920). eva is unstrided, which is why only human overlays
   looked sparse. Zero-velocity degeneration is 0.0% at both strides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sequence is M+1=26 rows; model act_seq/action_horizon set to 26 to match.
stride=1 for the same reason as the s1 variant -- an arc-length model can
only reconstruct on the sample grid.

Measured on a real human episode (unique reconstruction points / spacing):
  D=0.20 M=15 stride=3   ~8   2.56cm
  D=0.20 M=15 stride=1  ~18   1.00cm
  D=0.40 M=25 stride=1  ~34   0.93cm, 0.314m arc span
Zero-velocity degeneration 0.0% in all cases.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A time-indexed baseline chunk and an arc token cover different amounts of
motion, so per-timestep MSE compares them over different distances. On a
real human episode the baseline's 100 steps travel 1.887m while the arc
token spans 0.188m -- a 10x mismatch that alone produced the implausible
130x "arc-tok wins" on human.

Add arc_matched_resample(): resample a (T, 14) chunk to M samples spaced
uniformly in arc length over the first D metres, per arm, reusing the
tokenizer's own cumulative_arc_length/resample_by_distance so both sides
run identical code. Clamps to actual distance travelled when a chunk is
shorter than D rather than padding a held endpoint.

Wired via EvalVideo ctor (arc_match_distance/arc_match_points) so both
evaluators inherit it; ArcTokEvalVideo overrides _arc_match_source to
detokenize first. Verified on real data: 1.887m vs 0.188m becomes 0.1878m
vs 0.1874m over 15 matched points.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
So the baseline can be compared against the stride=1 arc runs without
stride being confounded with the tokenizer.

InterpolatePose subsamples by stride then interpolates to a fixed
new_chunk_length=100, so stride changes neither the output shape nor the
time window (ACTION_HORIZON=30 raw frames either way). It only changes how
many real samples back the interpolation: 10 at stride=3, 30 at stride=1.
No model config change needed -- act_seq stays 100.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
16 data configs at stride=1 plus 4 model configs (act_seq = M+1, which is
asserted by ArcTokEvalVideo._detokenize_batch).

Measured on a real human episode, detokenize can only emit ~D/(v*dt)
distinct points, so M above that stores waypoints the reconstruction
cannot express:
  D=0.10 ->   7 recon pts  (M>=25 over-parameterized)
  D=0.20 ->  18            (M=100 over-parameterized)
  D=0.40 ->  32            (M=100 marginal)
  D=0.80 -> 100            (all M fine)
Zero-velocity degeneration is 0.0% in every cell -- this is a
representation ceiling, not degenerate data. The over-parameterized cells
are kept as the negative control.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OSMO requires the resource request to be proportional to GPU count, so a
4-GPU run on an 8-GPU node may not exceed half the node (cpu<=63). The
hardcoded 8-GPU values are rejected at submit. Template them, and add
max_epochs so sweep cells can rank configs without paying the full 2000.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ElmoPA and others added 29 commits August 9, 2026 07:18
egomimic/eval/dfot/ and egomimic/eval/tf/ move to the DFoT PR stacked above.

egomimic/eval/__init__.py loses their entries. That registry is not lazy despite
its name -- it import_module()s every entry at package-import time, so
'import egomimic.eval' was pulling the whole DFoT tree in. eval/core/img_utils.py
mentions the DFoT evaluators only in docstring :mod: cross-references, not
imports, so core is unaffected.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The tests covering the preceding layers, plus the scripts worth keeping: the
CUDA kernel build, ops/, the eval entrypoint, and the sim replay tooling.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
They exercise egomimic.algo.diffusion / egomimic.eval.dfot, which move to the
DFoT PR stacked above.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
No test referenced MultiDataModuleWrapper at all, which is why the missing
pack_collate dispatch survived: test_packed_pipeline covers the *other*
pack_collate call site (norm-stat inference), so the subsystem looked tested.

Asserts the dispatch itself and that it reaches the DataLoader, for both the
train and valid loaders, and that unpacked datasets still get annotation_collate.
Uses MagicMock(spec=ZarrEpisodePackedDataset) -- the same pattern
test_packed_pipeline already uses -- so it needs no dataset on disk.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
All four groups were stale tests, not broken code. Nothing under egomimic/ is
touched by this commit.

test_training_recipe (7): PackedAlgoBase takes an assembled outer_stage now --
action_dim / action_horizon / d_model / cond_encoder / hnet moved onto
HNetOuterStage. The tests still passed them flat, so __init__ raised TypeError
for the missing positional. They now build the HNetOuterStage and pass it; what
each test asserts (lr stamping, parameter_groups, init_weights_range) is
unchanged.

test_pi (4): skipped at module level, not repaired. They describe a
PI.visualize_preds API that exists nowhere -- no branch of this repo defines it
(main included), egomimic.algo.pi.algo exposes no draw_actions to monkeypatch,
and neither EgoVerse2 nor EgoVerse-gmm-dualstream implements it, so it was never
ported into this lineage rather than dropped from it. Kept rather than deleted
so the intent stays on record. (Their first error was a stale aria_bimanual
domain, renamed to human_bimanual by the human/eva collapse; fixing that only
exposed the missing method underneath.)

test_core_defaults_byte_identical (1): the tx forward checksum was never a valid
invariant. tx's output cancels from an absolute sum of 7163 down to a signed sum
of ~1e-5, so the value moves with BLAS reduction order -- measured -1.21e-05 at
one thread vs -4.43e-05 at two, on identical weights -- while the test compared
it to a fixed ~1e-6 absolute bound. The reference simply captured one machine's
rounding noise; refreshing it would fail again elsewhere. The guard is now the
ABSOLUTE sum, stable to ~1e-8 relative across thread counts and still sensitive
to any real forward change, with the signed sum kept at a tolerance scaled to
the magnitude summed. lstm and hnet were unaffected because their sums are O(1)
and O(100) and do not cancel.

test_packed_pipeline (1): PUSHT_FOLDER pointed one directory level too high.
circle/ now holds a basic/ subfolder rather than .zarr episodes and the resolver
does not recurse, so the isdir() skip-guard passed while the resolver matched
nothing. Same drift as data/pushshapes/packed_episode/simulation/delta.yaml.
Correcting it also un-skipped three dataset-gated tests, which pass.

Suite: 13 failed / 297 passed -> 0 failed / 309 passed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
data, experiment, evaluator and callbacks groups, plus every model config the
rest of the tree actually references.

An earlier pass kept 3 model configs. Scanning hydra_configs, tests and scripts
for names that resolve against the model group finds 62 referenced, so that
prune broke two things at once:

  * all 31 experiment configs -- each carries an `override /model:`, so hydra
    fails at `Could not find 'model/<name>'` before any code runs;
  * 25 cases in tests/test_config_compose.py, covering the dfot_*, vae_* and
    bc_rnn_pushshapes_paperexact_* families.

The 59 missing configs are restored here, bringing the model group to the 62
that are reachable from the tree. Two of them (bf_rh_sdp_dual,
bf_rh_sdp_nodual) are the only configs that instantiate egomimic/pipeline, so
without them the batchflow runner shipped unreachable.

The remaining 181 per-arm variants on elmo/batchflow-core are still
deliberately excluded -- nothing in this tree references them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…amilies

The model group was 62 flat files, 9,581 lines, with exactly one config
(pi0.5_bc_eva) using defaults-inheritance. Everything else was authored by
copying a whole file and editing a few numbers.

Folders. Each model family gets its own directory and the filename drops the
prefix the folder already carries:

    hnet_cotrain_cossim_s4_trunk_200M.yaml
      -> hnet_cotrain_cossim/s4.yaml            (model=hnet_cotrain_cossim/s4)

64 configs, 10 folders, none left at the root. Every reference was rewritten:
8 in-repo files plus 26 scratch launchers outside the repo. Old -> new mapping
is saved at scratch/name_mapping_full.txt, since model names appear in the
Results Ledger and run pages as provenance.

Deduplication. Two families were literal copies differing only in numbers, and
both are now base + thin variants:

  * hnet_cotrain_cossim -- 12 configs, 270 of 304 lines identical across all of
    them (89%). The whole family is one 5-stage skeleton with a different split
    of transformer depth between the per-embodiment levels and the shared apex.
    Now 8 knobs; each variant is ~13 lines. 3,642 -> 961 lines.
  * hnet_dualstream_txar -- 3 configs, 9 knobs. txar_m16 turns out to differ
    from txar in k_agnostic/k_specific alone (3/2 -> 10/6).

Model group overall: 9,581 -> 6,414 lines.

The knobs are top-level scalars referenced by interpolation rather than
overridden in place, because outer_stage.hnet.stages is a YAML *list* and
OmegaConf replaces lists wholesale on merge -- a variant cannot override one
element of it.

Defaults entries need @_here_ (e.g. `- hnet_cotrain_cossim/base@_here_`).
Without it Hydra derives the package from the folder path and merges the base
under model.hnet_cotrain_cossim instead of the model root; the config still
composes and the tests still pass, it just silently inherits nothing.

The other seven families are NOT factored. Each has line-count differences
between members, i.e. they are structurally different models that share
boilerplate rather than copies of one template, so the same mechanical proof
does not apply. Duplication left: dfot 31%, bc_rnn 58%, vae 62%,
hnet_pushshapes 39%, hpt 34%, 2trunk 44%, bf_rh 39%.

Verified: parameterization proven lossless by reconstruction (11/11 cossim,
3/3 txar reproduce the originals byte-for-byte) before anything was written;
all 31 experiment configs compose; tests/test_config_compose.py 25 passed; and
the resolved model config of every one of the 62 pre-existing configs was
compared across the rename -- 48 identical, 14 differing only by the added
knob keys, 0 real differences.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
data/pushshapes/packed_episode/simulation/delta.yaml pointed at
Tsim_datasets2/circle, which today contains a single basic/ subdirectory and
zero .zarr episodes. LocalEpisodeResolver does not recurse (AGENTS.md:139 says
so explicitly), so the config resolved to an empty dataset and training died
with:

    No valid collection names from local filtering: filters matched no
    episodes in the local directory.

The episodes live one level down. Both the train and valid resolver paths now
point at circle/basic.

Verified: the composed config resolves to a directory containing 311 .zarr
episodes (previously 0); all 32 experiment configs still compose.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
evaluator/dfot, evaluator/tf and model/dfot move to the DFoT PR stacked above.
No experiment config selects them.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This stack added egomimic/scripts/eva_process/zarr_write_test.py while main has
the same converter at egomimic/rldb/zarr/hdf5_to_zarr.py, so merging would have
partly undone #562, which renamed it. Git recorded that as a rename, so a
delete-oriented check across the stack missed it; content-level duplicate
detection found the shared bodies (main 179 lines, is_image_array 38,
needs_transpose_to_hwc 17).

The copy here is also the older fork: it predates main's mandatory-intrinsics
change and omits the intrinsics= kwarg entirely, so it would write zarr episodes
with no camera calibration -- the exact failure that per-episode intrinsics were
introduced to prevent.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The chunk is (100, 138) = 2 x (21*3 + 6): wrist is 6-dim xyz+ypr with no
gripper slot, not the 7 I assumed, so the space is 138 not 140. Confirm
which block is which by bone-length test -- correct slicing yields
anatomically sized bones that stay rigid across the chunk; wrong slicing
does not.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Probed the live pipeline rather than assuming: actions_keypoints is
(100, 138) = 2 * (21*3 + 6). The wrist carries xyz+ypr only -- aria has no
gripper and nothing pads one on this path -- so the space is 138, not the
140 I had, and the wrist block precedes the keypoints within each hand.

Confirmed by bone-length test, which cannot be fooled: slicing as
[Lwrist6|Lkp63|Rwrist6|Rkp63] yields 39.7mm bones with 0.38mm variation
across a chunk, while keypoints-first yields 330mm "bones" with 3.7m
outliers. The velocity slot moves to the wrist block accordingly.

Re-verified end to end at D=0.45/M=30: token (31, 138), keypoint
reconstruction 0.200mm (linf), and bones survive the round trip at 39.1mm
with 0.61mm variation, so the hand stays rigid.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds mode keypoints_wristframe_ypr_arctok, which runs the existing
wrist-frame keypoint pipeline and then arc-tokenizes the (T, 138) chunk to
(M+1, 138). dt = stride/30 for the same reason as the cartesian arc path.

Two data configs on the SAME population as the cartesian runs (lab=rl2,
task=fold_clothes), stride=1, with a real 20% holdout -- the existing
aria_keypoints* configs set valid = train, so they cannot measure
generalization. The pair differs only in parameterization:
  kp_human_baseline  time-uniform, (100, 138)
  kp_human_arctok    arc-uniform,  (31, 138) at D=0.45 / M=30, linf
Identical 138-dim action space, so this isolates tokenization rather than
confounding it with action space.

No model change needed for the baseline -- hpt_bc_keypoints_base is
already single-domain with act_dim 138. The arctok variant only overrides
act_seq to M+1 = 31.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The workflow hardcoded data.{train,valid}_dataloader_params.eva_bimanual.*,
so any human-only config died with "Key 'eva_bimanual' is not in struct" --
after the dataset pull and norm stats had already run. Build the overrides
from an EMBODIMENTS list instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
hpt_bc_keypoints_base sets lr=5e-5 with CosineAnnealingLR(T_max=1400).
scheduler_interval defaults to "step", so T_max is 1400 STEPS ~ 14 epochs
at the observed 100.8 steps/epoch -- and CosineAnnealingLR keeps evaluating
cos(pi*T_cur/T_max) past T_max, so the LR climbs back up rather than
stopping. Over 600 epochs that is ~22 sawtooth cycles between 1e-5 and
5e-5. Predicted 1.44e-05 at step 12299 vs 1.8e-05 observed on wandb.

Every arc-cartesian run used constant lr 3e-4 with scheduler: null, so the
keypoint runs as launched were neither constant nor at the same LR, and
not comparable. Add const-LR variants.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
_ARM_SLOTS_BY_DIM only knew 12/14, so the keypoint action space fell
through the shape guard and returned None -- the same silent skip that hid
the human arcmatch metric earlier. Add the 138 layout using the confirmed
wrist-first ordering [L wrist6 | L kp63 | R wrist6 | R kp63].

Arc-matching runs on the WRIST pose deliberately: that is the same
trajectory the cartesian runs arc-match, so keypoint models can be
compared against them directly. Per-keypoint accuracy is a separate
metric, keypoint_mse(), reporting mean L2 over all 42 joints in metres so
it reads against the ~1.5mm reconstruction floor.

Verified: (15, 8) output on 138-dim, self-consistency exactly 0 for
identical inputs on both metrics, 12/14 unaffected, unsupported dims still
rejected, and keypoint_mse correctly skips cartesian chunks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
viz/keypoints.yaml and viz/keypoints_wrist.yaml both use a bare
front_img_1 image_key, but the batch carries observations.images.front_img_1
(confirmed by probe) -- so neither can run. Add viz/keypoints_human.yaml
with the correct key, no eva entry (these runs are single-domain), and no
annotation_key, since arc_tests has no annotations.

The eval sweep hardcoded annotation_key=null for four cartesian viz
entries; on a keypoint evaluator those keys do not exist and hydra errors.
Drive them from an ANNOT_EMBODIMENTS list instead, same fix as the
dataloader overrides.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Same bug as the training workflow, fixed there but not here: the sweep set
data.valid_dataloader_params.eva_bimanual.*, so any human-only run died
with 'Key eva_bimanual is not in struct'. Drive from EMBODIMENTS.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Third instance of the same assumption in this file. Drive it from
EMBODIMENTS like the others.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Walks a real frame pair through the computation: two hand poses with
per-joint displacement, the 21 distances as bars with L-inf/L2/L1-mean
marked, the reduction to a scalar, the rotation term, and accumulation
into tokens at D.

Uses frames 2731->2737 where the index fingertip moves 37.9mm while the
slowest joint moves 6.9mm, so the choice of norm is visible: L1-sum is
8.3x L-inf on that single step, which is what compounds into the ~22x
path inflation over an episode.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…r_training

zarr_key_to_keyname returns None for any batch key that isn't a registered action/proprio zarr key (intrinsics, episode_hash, image keys). The old 'if key is not None' guard checked the wrong variable — 'key' is a string from _batch.items() and never None, so the branch always fired and wrote every unregistered key under a single None dict slot. Later writes clobbered earlier ones, and 'intrinsics' was silently dropped.

Downstream _intrinsics_from_batch(batch, i) then returned None, so Human.viz / Eva.viz fell back to the hardcoded class INTRINSICS constant. For episodes whose per-episode K disagrees with the aria default (mecka fx=fy≈251, cy≈184 vs ARIA cy=240), this projected the GT trajectory ~55px vertically offset — the visible wrist-vs-palm misalignment reported against arc_tests mecka fold_clothes val-videos.

Fix: fall back to the original key when zarr_key_to_keyname is None so unregistered keys survive the rekey. Also fixes downstream access to episode_hash and per-episode image side-channel keys.
Layers on top of arc-length-nv-eval only what wasn't already there:

data configs (D=0.20m / M=15 hardcoded to match target convention):
- aria_train_mecka_val{,_arctok}: cross-domain (train aria fold, valid
  mecka fold_clothes)
- eva_only_fold{,_arctok}: eva-only robot-baseline runs
- mecka_folding_clothes{,_arctok}: mecka folding_clothes (note "ing"
  variant of task name — distinct from arc_tests fold_clothes)
- mecka_folding_eva_fold_cotrain{,_arctok}: cotrain across mismatched
  task names (mecka folding_clothes + eva fold_clothes)

hydra launchers (mirror target's submitit_pace_l40s.yaml convention):
- submitit_pace_a100 / _blackwell / _h100

viz + diagnostic scripts:
- egomimic/visualization/arc_tok_viz.py: detokenize+overlay helper for
  notebooks (mirrors ArcTokEvalVideo viz path)
- scripts/pixel_check.py + .sbatch: verify projected GT dots match
  val-video mp4 frame 0
- scripts/visualize_trunk_latents.py + .sbatch: t-SNE + HDBSCAN over
  HPTModel.forward_features, emits arc_embedding_sweep's tabbed HTML

small UI fix in scripts/arc_embedding_sweep.py: image-panel close
button now has type=button + inline onclick fallback.

Dropped from the pre-rebase stash because target already had them
(and more evolved): eval_arctok/eval_hpt/eval_video changes,
arc_tests_cotrain* config edits, D40_M100 model/evaluator configs.
Dropped as no-longer-wanted per user note: FMPolicyWithVelDecoder /
WithVelReadout wrappers, associated hpt.py / denoising_policy.py /
hpt_nets.py / model config _veldec / _velreadout variants. Also
dropped an accidental LocalFolderEpisodeResolver -> S3EpisodeResolver
revert and a stray _LEGACY_EMBODIMENT_ALIASES removal.
Merges origin/arc-length-nv-eval (arc-length tokenizer + arc-tok
evaluators + keypoint tokenizer + arc model configs + osmo PACE
configs) and the two rpunamiya/arc-length-tokenizer deltas (hpt
batch-keys preservation fix, PACE launchers + task splits + viz)
onto the batchflow config stack (bf/1..bf/7 config reorg).

Conflicts resolved by keeping bf's structural refactors as the
skeleton and porting arc's functionality into that layout:
- eval_video.py: keep both bf's max_videos and arc's arc_match_*
  + video_chunk_frames constructor params.
- zarr_dataset_multi.py: honor both skip_bounds_check (bf) and
  bounds_check (arc) semantics on the bounds guard.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…configs

Ports the D=40cm / M=100 arc-tok model configs and required class code
onto the bf/7-configs config layout so the in-flight training jobs can
launch from this branch:

- Restore hpt_cotrain_enc_dec_base.yaml with _target_ paths pointing at
  bf's module layout (algo.hpt.algo.HPT, models.stems.hpt_stems.*,
  models.heads.hpt_heads.MultiBlockTransformerDecoder,
  models.diffusion.denoising_nets.CrossTransformer).
- Point hpt_cotrain_mecka_flow_shared_head_arc.yaml back at
  hpt_cotrain_enc_dec_base and update its _target_ paths.
- New: hpt_cotrain_mecka_flow_shared_head_arc_D40_M100{,_veldec,_velreadout}.yaml.
- Add MLPVelocityDecoder to egomimic/models/heads/hpt_heads.py and
  FMPolicyWithVelDecoder / FMPolicyWithVelReadout to
  egomimic/models/heads/fm_policy.py.
- Fix eval_arctok.yaml viz path (cartesian → cartesian/base).
- Add ``arc_tokenizer`` config group defaults to train_zarr_cartesian.yaml
  so ``arc_tokenizer.min_distance_unit=…`` CLI overrides resolve.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- pushshapes arc-length tokenizer (egomimic/rldb/zarr/pushshapes_arc_tokenizer.py)
  and pushshapes.get_keymap_hpt_arc for reading (M+1, 2) arc-tok windows.
- HPT closed-loop inference: expand_arc_chunk_to_time so the (M+1, D) chunk is
  played back as a time-uniform buffer against the env; replan_at tracks the
  variable expanded-buffer length.
- hydra config groups for the pusht arc-tok stack:
  data/pushshapes/pusht/{circle,circle_arc}.yaml, evaluator/hpt/{pusht,pusht_arc}.yaml,
  model/pusht/*, model/pusht_arc/*.
- Restored hpt_cotrain_mecka_flow_shared_head.yaml (non-arc baseline) with
  bf/7-configs _target_ paths so mecka baselines resume from aniketh/arc.
- logger/wandb/base.yaml default project: zarr_test -> arc so arc-tok runs land
  in rl2-group/arc automatically.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adapts robot/rollout.py for arc-tokenized policies. Subclasses PolicyRollout
to reuse the loader / obs transform / safety pipeline; overrides only the
predict step to detokenize the model's (M+1, 8) arc-tok output into a
(H, 14) time-uniform chunk before handing it to the controller.
Rewrites TokenizeBimanualArcLengthCartesian to produce (M+1, 14) with full
xyz + ypr + grip per arm (was (M+1, 8) dropping rotation). Rotation is
unconditionally supervised — no opt-out. Gripper padding for human aria
data preserved as an existing transform option.

- arc_length_tokenizer.py: ARC_TOK_PER_ARM_DIM=7, ARC_TOK_BIMANUAL_DIM=14;
  SLERP for waypoint ypr resample; vel row extended to 14 dims with
  per-axis mean angular velocity in ypr slots.
- Model configs (arc, D20_M15, D40_M25, D40_M100, M15/M25/M50/M100 +
  veldec + velreadout): act_dim 8 -> 14, infer_ac_dims 8 -> 14, veldec
  output_dim 8 -> 14.
- FMPolicyWithVelDecoder / FMPolicyWithVelReadout: act_dim default 14.
- eval_arctok.py, rollout-arc.py, visualization/arc_tok_viz.py: shape
  asserts 8 -> 14; removed zero-fill of ypr; detokenize returns 14-dim
  including model-predicted rotation.
- Data configs (arc_sweep_*, arc_tests_cotrain_arctok*, folding_clothes
  _arctok, folding_eva_fold_cotrain_arctok, eva_only_fold_arctok,
  aria_train_mecka_val_arctok): comment updates.
- Embodiment keymap/transforms: docstring updates only; routing intact.
@github-actions

Copy link
Copy Markdown

Claude Code Review

Review

Summary

This PR is titled "Aniketh/arc" but bundles a massive, unrelated collection of changes: a full H-Net algorithm implementation, packed dataloading, versioned PushShapes simulators (sim_v1/sim_v2), a "BATCHFLOW" refactor doc, ~600 lines of new AGENTS.md content, CUDA kernel install scripts, and gitignore/README churn. The diff was truncated at 80k chars, so I can only see the first portion.

Key concerns

  1. PR scope is unreviewable. The title says "arc" but there is no arc code in the visible diff — instead there are at least 5 orthogonal features (H-Net algo, packed dataloader, versioned sim, BATCHFLOW doc, CUDA install tooling). Each of these deserves its own PR with its own reviewers. Merging this as one unit makes bisecting regressions nearly impossible and hides risky changes behind unrelated ones.

  2. BATCHFLOW.md contradicts the rest of the repo. It declares a new pipeline.algo.PipelineAlgo architecture where "every module is stage(batch: dict) -> dict", says "old code is FROZEN" on another branch, and says "backward ckpt compatibility explicitly NOT required." None of the referenced modules (egomimic/pipeline/stages_io.py, stages_hnet.py, stages_flow.py, algo.py) appear in the visible diff. Either this file describes work not in this PR (delete it), or it describes future work (move to a design doc / issue), or the pipeline modules are hidden in the truncation (split PR). As-is, checking in aspirational architecture docs alongside a different architecture (egomimic/algo/hnet/algo.py) will confuse every future contributor.

  3. Tsimulation/ at repo root duplicates egomimic/scripts/pushshapes/…? The AGENTS.md references Tsimulation/pushshapes as the canonical sim, and the new Tsimulation/__init__.py sets TSIM_VERSION via env var with sim_v2 as default. Env-var-selected package behavior is a footgun for reproducibility — the docstring even flags that jobs "should still assert the version they expect." Prefer explicit imports (from Tsimulation.sim_v2 import ...) with no default aliasing, or a config field, not an env var.

  4. AGENTS.md is now 550+ lines of implementation notes. Much of this (CUDA install steps, memory baselines, smoke script inventory, bug-fix changelogs) belongs in docs/ or module docstrings, not the top-level agent instructions. AGENTS.md should stay short and normative ("do X, don't do Y"), not narrative.

  5. Convention drift I can see:

    • ZarrDemoWriter bypasses timestamp-based episode hashing (uses episode_{idx:06d} naming). Per repo conventions episodes are hashed by YYYY-MM-DD-HH-MM-SS-ffffff. Is sim data exempt? If so, document it; if not, this is a data-integrity bug.
    • embodiment="pushshapes_sim" — confirm this exact string is registered in the embodiment enum (the PR claims PUSHSHAPES_SIM = 15 was added, but I can't see that file in the visible diff).
    • No mention of SHA-256 hashing for operator field (N/A for sim, but worth an explicit note).
    • No mention of R2 upload path for sim data.
  6. Test coverage is asymmetric. The H-Net side has 86 tests according to AGENTS.md. The Tsimulation collectors and ZarrDemoWriter have only smoke tests referenced; I don't see any test that validates the sim-produced zarrs round-trip through ZarrDataset and the training pipeline. That's the integration boundary most likely to silently break.

  7. .gitignore adds out/ and arc_*.html — these are the only "arc"-related things in the visible diff, which reinforces that this PR was branched off an arc feature branch and then had unrelated work piled onto it.

Suggestions

  • Split this PR. At minimum: (a) H-Net algo + packed dataloading, (b) Tsimulation versioning, (c) BATCHFLOW/pipeline refactor, (d) AGENTS.md + docs, (e) actual arc changes. Each independently reviewable.
  • Move AGENTS.md additions to docs/hnet.md, docs/packed_dataloading.md, docs/cuda_kernels.md. Keep AGENTS.md to a few dozen lines of rules.
  • Delete BATCHFLOW.md from this PR unless the pipeline modules it describes are actually included and wired up. If they are (hidden in truncation), the diff needs to show that they replace or coexist cleanly with egomimic/algo/hnet/algo.py.
  • Replace TSIM_VERSION env-var dispatch with explicit imports at call sites, or a Hydra config field. Env-var-selected packages don't compose with SLURM job arrays or Hydra sweeps cleanly.
  • Verify PUSHSHAPES_SIM enum entry exists and matches the string used in ZarrDemoWriter ("pushshapes_sim"), and add a test that constructs a ZarrDataset on a sim-written episode.
  • Document the episode-naming departure (or fix it to use timestamp hashes) — this is a documented convention and drift here will cause pain later.
  • Confirm norm stats compatibility. The claim in AGENTS.md that sample_frac is reinterpreted as a "frame budget" in packed mode is a subtle behavior change to a shared function (MultiDataset.infer_norm_from_dataset). Existing configs using sample_frac on non-packed datasets need regression coverage to confirm

Reviewed by Claude · Review workflow

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.

2 participants