Skip to content

rollout: config-driven viz, wristframe revert, HPT annotation support#482

Open
rl2aloha wants to merge 19 commits into
mainfrom
elmo/rollout-viz-and-wristframe
Open

rollout: config-driven viz, wristframe revert, HPT annotation support#482
rl2aloha wants to merge 19 commits into
mainfrom
elmo/rollout-viz-and-wristframe

Conversation

@rl2aloha

@rl2aloha rl2aloha commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Read transform_list mode from .hydra/config.yaml instead of hardcoding
cartesian_wristframe_ypr. The rollout now matches whatever frame the
model was trained on (e.g. mode: cartesian for QWEN-HPT,
mode: cartesian_wristframe_ypr for pi05_eva_aria_obj_gen_lang_wristframe).

Add config-driven prediction visualization. build_viz_func_from_config
reads evaluator.viz_func from .hydra/config.yaml and uses hydra.utils.instantiate
to bind image_key / action_key / mode. A runtime 'v' toggle in the
intervention menu enables/disables saving viz
.png to debug/
on every inference. The live camera image is resized to 640x480
inside _save_viz so the projection matches ARIA_INTRINSICS (the
Aria camera publishes at 960x720 per configs.yaml, but the intrinsics
in INTRINSICS['base'] are calibrated for 640x480). cv2.imwrite gets
BGR conversion to avoid the inverted-color save bug.

Add wrist-frame revert. _build_revert_transform_from_config reads
evaluator.transform_lists. from config and instantiates
the revert transform that converts wrist-frame predictions back to
cam frame using the current obs ee_pose as the reference. rollout_step
applies this to preds and the GT batch BEFORE both the viz call and
the cam_frame_to_base_frame post-processing. Wrist-frame models now
execute correctly on the robot AND project to the right pixels.
Cam-frame models (revert is None) skip the revert as a no-op, so
QWEN-HPT and old PI cartesian checkpoints continue to work unchanged.

Add HPT (QWEN) annotation support. _apply_annotation_to_algo now
sets annotation_sampling_mode='first' in addition to PI's
sampling_mode='first', so --annotation-path works for both algos
without subclass-specific code paths.

Co-Authored-By: Claude Opus 4.7 noreply@anthropic.com

rl2aloha commented Jun 2, 2026

Copy link
Copy Markdown
Contributor Author

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

@ElmoPA ElmoPA changed the base branch from elmo/rollout-proprio-norm-fix to graphite-base/482 July 8, 2026 01:30
@ElmoPA ElmoPA force-pushed the elmo/rollout-viz-and-wristframe branch from 9772397 to 2f50589 Compare July 8, 2026 01:30
@ElmoPA ElmoPA force-pushed the graphite-base/482 branch from 486eb81 to 085325d Compare July 8, 2026 01:30
@ElmoPA ElmoPA changed the base branch from graphite-base/482 to elmo/intrinsic-zarr July 8, 2026 01:30
@ElmoPA ElmoPA changed the base branch from elmo/intrinsic-zarr to graphite-base/482 July 9, 2026 00:24
ElmoPA and others added 11 commits July 9, 2026 01:39
The trained pi prompt format (`Task: ...; Action: ...`) is governed by
`data.{proprio,embodiment_label,control_mode,state_num_bins}` in the
hydra config. The `.ckpt` only stores the `model:` subtree (see
`trainHydra._build_model_config_tree`), so read the flags from the
run-output snapshot at `<run_dir>/.hydra/config.yaml` instead.

- rollout.py: `build_tokenized_collate` now wired from the run snapshot
  rather than hardcoded args. `data["embodiment"]` is set to the
  integer id; previously it was the string `"eva_bimanual"`, which
  `_embodiment_name`'s `int(eid)` cast would have crashed on.
- hydra_utils.py: new `find_run_snapshot_path` / `load_run_snapshot`
  helpers for reading hydra run-output `config.yaml` without
  re-resolving runtime interpolations like
  `${data.dataset.data_schematic}`.

Debug prints kept (TODO-marked) to ease verification on the rollout
host.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Local-only branch that should NOT be merged. Carries the patches the
rollout host needs to bring up the policy:

- pyproject.toml: relax `requires-python` to `>=3.10` (ARX `arx5_interface`
  ships as a cpython-310 .so, so the rollout host can't be 3.11) and drop
  `zarr==3.1.5` from the runtime deps (training-only).
- pull_models.sh: REMOTE_PATH bumped to the EgoVerse4 logs dir.
- rollout-requirements.txt: trimmed sibling of requirements.txt with
  zarr removed and 3.10-friendly pins.
- uv.lock: 3.10-resolved lockfile (used to seed openpi's pip install
  via `uv export -c`).
- ROLLOUT_INSTALL.md: end-to-end setup guide for the rollout host
  (Python 3.10, openpi fork, ARX bindings, HF auth, checkpoint conversion).

External submodule pointer for `external/openpi` is intentionally left
uncommitted here; commit inside the submodule first if you want to
freeze the openpi rollout-fork rev.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The "Branches" section claimed external/openpi is on a fork branch with
the rollout patches "already committed" — it isn't. The submodule is
detached at upstream 981483d with the patches as uncommitted
working-tree edits, plus an untracked scripts/patch_transformers.py.

The fork has a pi-rollout-changes branch with overlapping patches, but
it diverges from this host's working tree (adds chex, loosens numpy /
opencv-python, patches pi0_pytorch.py; missing .python-version,
packages/openpi-client/pyproject.toml, uv.lock, patch_transformers.py).
Calling out the divergence so future-us doesn't merge them blindly.

Replaces the prior "checked out on a rollout branch" wording with an
explicit "openpi patches to apply on top of 981483d" checklist —
file-by-file, what to change and why — so the setup is reproducible
even without a clean fork branch.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per-task / per-object prompt files used by `rollout.py --annotation-path`
(or the `a <path>` intervention command). Each file holds a single
natural-language instruction tokenized into the `Task: ...` block of
the pi prompt.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
PI's process_batch_for_training now owns prompt sampling, embodiment/
control-mode/State splicing, and tokenization (see PI._build_prompts /
_tokenize_prompts). The collate's only job is to stack tensors and
preserve list-valued keys.

- Use annotation_collate instead of build_tokenized_collate.
- Replace _build_collate_from_checkpoint_cfg with
  _apply_annotation_to_algo: override annotation_key="annotations",
  sampling_mode="first", default_prompt=<rollout annotation> on the
  loaded PI algo so rollout-time prompts deterministically flow through.
- Drop _debug_print_proprio_norm and the post-collate sampled_prompt
  debug print -- both inspected fields the collate no longer produces.
- Stop loading the hydra run snapshot in _load_policy; the only
  consumer was the removed collate builder.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The algo-side prompt refactor (9af36e0) moved State-block discretization
from build_tokenized_collate into PI._discretize_state_for_sample. The
old path normalized proprio via data_schematic.normalize_data before
clipping to [-1,1] and binning; the new path skips that step. Raw xyz
in meters saturates the [-1,1] clip, producing meaningless bin indices
the model never saw during training (causing jerky rollouts).

Match MultiDataset.__getitem__'s behavior at the rollout boundary:
normalize the post-transform single sample via the model's norm_stats
(itself a MultiDataset) before collating and handing to
process_batch_for_training. norm_stats are the ones baked into the
checkpoint, so train/inference State bins now agree.

Also adds a one-shot debug print of the assembled prompt on the first
inference step, so the State block can be eyeballed at runtime.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Read transform_list mode from .hydra/config.yaml instead of hardcoding
cartesian_wristframe_ypr. The rollout now matches whatever frame the
model was trained on (e.g. mode: cartesian for QWEN-HPT,
mode: cartesian_wristframe_ypr for pi05_eva_aria_obj_gen_lang_wristframe).

Add config-driven prediction visualization. _build_viz_func_from_config
reads evaluator.viz_func from .hydra/config.yaml and uses hydra.utils.instantiate
to bind image_key / action_key / mode. A runtime 'v' toggle in the
intervention menu enables/disables saving viz_<step>.png to debug/
on every inference. The live camera image is resized to 640x480
inside _save_viz so the projection matches ARIA_INTRINSICS (the
Aria camera publishes at 960x720 per configs.yaml, but the intrinsics
in INTRINSICS['base'] are calibrated for 640x480). cv2.imwrite gets
BGR conversion to avoid the inverted-color save bug.

Add wrist-frame revert. _build_revert_transform_from_config reads
evaluator.transform_lists.<embodiment> from config and instantiates
the revert transform that converts wrist-frame predictions back to
cam frame using the current obs ee_pose as the reference. rollout_step
applies this to preds and the GT batch BEFORE both the viz call and
the cam_frame_to_base_frame post-processing. Wrist-frame models now
execute correctly on the robot AND project to the right pixels.
Cam-frame models (revert is None) skip the revert as a no-op, so
QWEN-HPT and old PI cartesian checkpoints continue to work unchanged.

Add HPT (QWEN) annotation support. _apply_annotation_to_algo now
sets annotation_sampling_mode='first' in addition to PI's
sampling_mode='first', so --annotation-path works for both algos
without subclass-specific code paths.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add 'm' intervention command (independent of 'v') that saves the
prediction viz at the model's input resolution — 224x224 with padding
mirroring resize_with_pad_torch — written to debug/viz_model_<step>.png.
Intrinsics are scaled-and-padded to match (base intrinsics calibrated
for 640x480 are first scaled to the live camera's resolution, then by
the resize ratio, with padding added to cx/cy) so projections still
land on the correct pixels. Pred is hidden (pred_alpha=0.0) so the
saved image shows what the model actually sees, with only the green
GT cluster overlaid.

Fix the 'annotations' KeyError that crashed both viz modes when no
--annotation-path was supplied. The viz_func partial bakes in
annotation_key from the config, so viz_gt_preds does an unconditional
batch[annotation_key] lookup. _build_viz_func_from_config now captures
that key, and both _save_viz / _save_viz_model_res inject a default
(self.annotation or empty string) into the batch before calling viz_func.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Cherry-picked file-scoped from commit 2c53ef8 ("6D pi data transform"):
- egomimic/utils/pose_utils.py: adds _ypr_to_rot6d / _rot6d_to_ypr helpers
- egomimic/rldb/zarr/action_chunk_transforms.py: adds CartesianYPRToRot6D and
  CartesianRot6DToYPR transform classes (xyz+ypr <-> xyz+rot6d per arm, 14<->20
  or 12<->18 dims; numpy/tensor passthrough)
- egomimic/rldb/embodiment/eva.py: adds 'cartesian_6d' and
  'cartesian_wristframe_6d' modes to Eva.get_transform_list, plus the two
  cam-frame revert builders the evaluator config references
  (_build_eva_cartesian_revert_6d_transform_list,
  _build_eva_cartesian_revert_6d_wristframe_transform_list).

Unblocks rollout of pi05_eva_aria_wristframe_6d checkpoints, which the rollout
branch was crashing on with 'NoneType is not iterable' (unknown mode falling
through Eva.get_transform_list) and 'Error locating target ...
_build_eva_cartesian_revert_6d_wristframe_transform_list' (missing revert
builder). Skipped the training-side changes in 2c53ef8 (pi.py, hydra configs,
action_utils, human.py, test files) — not needed for rollout and would
conflict with rollout-branch edits to pi.py.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…on format

Followup to 491496f (which only brought the data-side eva.py / transforms /
pose_utils). The 6D checkpoint outputs 20-dim per-arm actions (xyz + rot6d +
gripper) but RobotBimanualCartesianEuler.to32 still expected 14-dim, causing:
  ValueError: RobotBimanual: expected 14-dim, got 20

File-checkout from 2c53ef8 brings in:
- The full raw-rotation refactor that landed on aidan/pi-6d before 2c53ef8
  (1ddf896, 53e69ea, 59729c4) — PI05_CARTESIAN_ACTION_ENCODING_{RAW_ROT_6D,
  LEGACY, NORM_ROT_6D} constants, to32_raw_rotation/from32_raw_rotation, and
  raw/normalized rotation pipeline in PI.forward_eval / PI._unnormalize_action.
- The new to32_norm_6d / from32_norm_6d converter methods on
  RobotBimanualCartesianEuler and HumanBimanualCartesianEuler.
- The NORM_ROT_6D dispatch branches in PI.forward_eval (extract 6D, then
  unnormalize) and PI's action-encoding setup.

Safe checkout: HEAD's pi.py and action_utils.py were byte-identical to the
merge-base with 2c53ef8, so file-checkout doesn't overwrite any rollout-branch
edits to these files. Purely EgoVerse-side; no openpi changes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
rl2aloha and others added 7 commits July 9, 2026 01:39
Got rolled back by an earlier gt sync. The 20GB pi05 checkpoint OOM-killed
the rollout between the patch-save and the main ModelWrapper.load_from_checkpoint:
both held the full checkpoint simultaneously (~40GB peak on a host with less).

Two mitigations:
- If <ckpt>.patched already exists on disk, return its path immediately
  and skip both torch.load and torch.save. The first run still pays the
  double-load cost; every subsequent run is single-load.
- del + gc.collect() between _patch_checkpoint_paths and the main load so
  the patching checkpoint is freed before the load load runs even on
  first-launch.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Subtask-prediction PI models train with annotation_key='annotations_high'
(the user-supplied high-level goal) and predict the low-level subtask
under 'annotations'. The old rollout code hardcoded:
  - _apply_annotation_to_algo: model.annotation_key = 'annotations'
  - process_obs_for_transform_list: data['annotations'] = [text]
which would override the trained key and clobber the slot reserved for
the model's own predictions.

Now:
  - _apply_annotation_to_algo no longer overrides annotation_key — the
    config-loaded value (e.g. 'annotations_high') is preserved. Other
    deterministic-inference knobs (sampling_mode / annotation_sampling_mode /
    default_prompt) still get set.
  - process_obs_for_transform_list reads model.annotation_key dynamically
    and injects the user prompt under that key. Falls back to 'annotations'
    for legacy models that don't have annotation_key.

Unblocks rollout of sort_pp_subtask_wf6d checkpoints.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Both were dead-code — the solver was instantiated in main() and never
used, and _get_model_xml_path only existed to feed it. The same auto-
merge that left a stale CameraTransforms(...) call also kept these.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
PI hierarchical models (subtask_prediction=true) decode a subtask from
the high-level prompt and return it in forward_eval under
<embodiment>_subtask_pred. The decoded text is otherwise discarded by
the rollout — surface it as

  [rollout][subtask] step=30 predicted='Pick the screwdriver with the right hand.'

so the user can verify the hierarchical planning at runtime. Falls back
silently for models without subtask prediction (key not in fwd_out).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The model already re-decodes the subtask every query_frequency steps via
forward_eval -> sample_actions; there's no caching, so each call runs the
autoregressive subtask decode fresh. But there's no way to trigger a
replan between query boundaries (e.g. right after loading a new
annotation via 'a <path>').

Add 'p' to the intervention menu. Sets policy._force_replan = True; the
next rollout_step checks the flag alongside the i % query_frequency
gate, runs forward_eval, then clears the flag. Works the same for plain
PI / HPT (just resamples actions) and PI hierarchical (also redecodes
the subtask, surfaces it via [rollout][subtask]).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
The intrinsic-zarr collapse renumbered EVA from 6/7/8 to 4/5/6 (after
adding HUMAN_RIGHT_ARM/LEFT_ARM/BIMANUAL as 1/2/3). rollout.py's
EMBODIMENT_MAP still hardcoded the old 8/7/6, so:

  data['embodiment'] = self.embodiment_id  # = 8

ended up in the viz batch, then viz_gt_preds did:

  embodiment_id = batch['embodiment'][0].item()   # 8
  get_embodiment(8).lower()                       # None.lower() => AttributeError

(Action prediction kept working — the model receives the batch keyed by
the *name* 'eva_bimanual' and resolves the id internally via
get_embodiment_id, which correctly returned 6.)

Source the IDs directly from EMBODIMENT.<member>.value so the map can
never drift from the enum again.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@ElmoPA ElmoPA force-pushed the elmo/rollout-viz-and-wristframe branch from 2f50589 to 0c73a6c Compare July 9, 2026 01:39
@graphite-app graphite-app Bot changed the base branch from graphite-base/482 to main July 9, 2026 01:39
The intrinsic-zarr base used it; the replayed 'algo-side prompt assembly'
commit removed all usages (annotation_collate now), and the conflict
resolution kept the base's import line.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYmG3nhxt7LYiaaSJPsKEV
@ElmoPA ElmoPA force-pushed the elmo/rollout-viz-and-wristframe branch from 0c73a6c to 794d6d3 Compare July 9, 2026 01:39
@github-actions

github-actions Bot commented Jul 9, 2026

Copy link
Copy Markdown

Claude Code Review

Review of PR #482

Summary

This PR bundles three logically distinct changes: (1) config-driven rollout viz + wristframe revert (per the PR title), (2) HPT annotation support, and (3) a large hierarchical subtask-prediction feature (PI0Subtask, new configs, embodiment keymap changes, 6D rotation transforms, proprio normalization in collate). The scope mismatch between title/description and diff is a serious concern.

Key concerns

1. Scope creep — PR description does not match the diff

The description covers only the rollout changes, but the diff adds:

  • PI0SubtaskPytorch model (~370 lines)
  • New pi0.5_subtask / sort_pi_subtask configs
  • New 6D rotation transforms (CartesianYPRToRot6D, CartesianRot6DToYPR)
  • New action_encoding field on PI with three modes
  • Changes to Embodiment.get_keymap signature (adds high_annotation_key)
  • Changes to ZarrDataset._load_annotations signature and cache structure (dict-keyed)
  • Non-trivial changes to build_tokenized_collate (now requires data_schematic, hard-fails without stats)
  • New ROLLOUT_INSTALL.md documenting hand-editing an external submodule

This should be split into at least 3 PRs. The rollout fix is small and safe; the subtask work is a large research feature that needs its own review. Landing them together makes bisecting regressions painful.

2. Breaking API change without callers audited

ZarrDataset._annotations changed from list[dict] | None to dict[str, list[dict]] | None. Any external code (or __setstate__, checkpoint restores, tests) touching self._annotations directly will silently misbehave. Also _annotation_text_for_frame now returns list[str] instead of str (per the docstring/signature) — this is a return-type change; all callers need to be verified.

3. build_tokenized_collate hard-fails on missing schematic/stats

The new code raises ValueError if data_schematic is None or norm_stats is None when proprio=True. This is correct behavior (silent skip would corrupt bins), but existing callers that construct the collate without passing data_schematic will now crash. Please confirm all call sites in pl_data_utils.py / trainHydra.py / any tests are updated, and that ordering (infer_norm_from_dataset before first batch) is guaranteed under DDP with multiple workers.

4. Rollout viz resize assumption

_save_viz resizes the live camera to 640×480 to match ARIA_INTRINSICS. This is Aria-specific — will it silently mis-project for Eva / Mecka rollouts that have different intrinsics? The revert transform is embodiment-aware (evaluator.transform_lists.<embodiment>), but the intrinsics used for viz appear hardcoded. Please confirm or gate on embodiment.

5. Subtask decoding correctness

In sample_actions, next_tok is a [B] tensor and zeros_col[:, 0] is used as the "inactive" branch of torch.where. Verify dtype/device match — this looks fine but is worth a unit test. More importantly: when the high-only prefix arrives right-padded and tokens are inserted at insert_idx = cur_mask.sum(dim=1), this writes inside the original padding region only if the prefix was left-packed and padding is on the right. That's the assumption in _tokenize_highonly (ids[i, :len(pre)] = ...), so it holds — but please add a comment or assertion because the correctness of return_subtask gathering depends on it.

6. Non-idempotent openpi submodule setup

ROLLOUT_INSTALL.md documents hand-editing files in external/openpi at a detached HEAD with uncommitted changes and an untracked script. This is not reproducible and will cause silent drift. Please push a pi-rollout-host branch to the fork and reference a commit SHA, as the doc itself suggests.

7. No tests

Zero tests added for: 6D↔ypr round-trip (CartesianYPRToRot6D / CartesianRot6DToYPR), subtask CE loss shape, hierarchical decoding, _load_annotations filtering by level, or the wristframe revert transform. For a change this large touching training loss computation and data layout, at minimum:

  • Round-trip test for the 6D transforms
  • Snapshot test that pi0.5_cotrain_eva_aria_6d produces identical batch tensors (modulo action rep) to the ypr config
  • Unit test for _annotation_text_for_frame with level="high" / "low" split

8. Norm stats regression risk

action_encoding=cartesian_normalized_rot6d uses norm stats computed over the 6D representation. If any existing checkpoint's norm stats were computed over ypr, loading it under a 6D config will silently produce garbage actions. Is there a version/compatibility check on the norm_stats file, or at least a config-level assertion that transform_list mode and action_encoding are consistent?

Suggestions

  1. Split the PR. Land the rollout fix + HPT annotation support as a small PR first (matches the description, low risk). Land subtask + 6D as a separate feature PR.
  2. Add a config-level validator: action_encoding=cartesian_normalized_rot6d ⇒ transform mode must be cartesian_6d or cartesian_wristframe_6d. Fail fast in PI.__init__.
  3. Add a norm_stats fingerprint (embodiment × ac_key × action_encoding) and check it at load time.
  4. Push a stable openpi fork branch and reference a SHA in ROLLOUT_INSTALL.md.
  5. Grep for

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.

3 participants