feat(rldb): detect JPEG vs H264 image encoding instead of trusting dtype - #543
feat(rldb): detect JPEG vs H264 image encoding instead of trusting dtype#543ElmoPA wants to merge 174 commits into
Conversation
Squash of: - a31d8ab4 tshape sim environment - ac272e8a Add Tsimulation viz/scripted/stats tools + physics tuning
…ding Squash of: - f261ff68 tsim training configs/embodiment - b38264ab Add pushshapes_sim HPT training: keymap, viz, eval fixes - 1e174131 Add episode-level packed dataloading
Squash of 10 commits from temp-arch-flexible: 7faf2012, 49ed0d34, 8387986e, 11a266ce, 3fe9a353, 0ea9d013, c83b6e69, 63a2e852, 7b71650a, a063c021
- test_hnet_nets.py (57): routing, chunk, dechunk, isotropic, stages (padded + packed), HNet assembly, ratio_loss, chunk_stats, STE, RMSNorm, AdaLN. - test_packed_pipeline.py (9): normalize broadcast on padded vs packed; _iter_leaves descent; multi-frame JPEG decode; end-to-end packed stats collection. - test_training_recipe.py (20): apply_optimization_params, init_weights height-scaled init, apply_lr_multiplier per-stage stamping, parameter_groups (default, with bias/norm WD=0, per-stage groups, AdamW-consumable). Plus algo wiring tests for the opt-in init_weights_range / lr_multipliers / use_parameter_groups / weight_decay kwargs. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Imports from EgoVerse7@temp-arch-flexible working tree as of 2026-05-20. Includes: - algo: input_modules.py + obs_transforms.py (new modules) - callbacks: chunker_residual_scheduler, ckpt_chunker(+dropout), random_attn_dropout - data configs: tsimulation_400ep, tsimulation_allep + tweaks to existing tsim configs - model configs: hnet_pushshapes_mamba_encdec, hnet_pushshapes_obs_ar + tweaks - eval/* edits, models/hnet_nets/* edits, schedulers, uv.lock - removes scripts/install_cuda_kernels.sh and egomimic/eval/eval_hnet_sim.py Excluded: egomimic/algo/hnet.py.bak.preinput (manual backup) and drift_eval_out_* (eval artifacts).
…screte diffusion + DDPM/DDIM samplers
… per-frame obs cond)
…l); drop 400ep config
…ens defensive fix
…ckedSimEval rename, composite eval
…r); supports growing-T AR
- algo.py: add ar_inference_step_size (sub-steps per env tick at closed-loop AR); thread cfg_scale through _inference_step_ar/_inference_step_chunk; remove unused shape var - backbone.py: force_uncond branch for CFG two-pass blending; wire attn/resid dropout into Isotropic trunk - sampling.py: _CFGBackbone wrapper for cfg_scale > 1 sampling; schedule-matrix CFG plumbing Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…val viz - model/dfot_pushshapes.yaml: scale to 67M (d_model=512, T=12, num_heads=8, d_intermediate=2048); attn dropout=0.1, resid dropout=0.1, cond_dropout_prob=0.1; cfg_scale field; causal=true - data/tsimulation_full.yaml: 750-episode circle_750 dataset, batch_size=16 - evaluator/eval_dfot_val.yaml + eval_dfot_full.yaml: cfg_scale + ar_chunk_size + ar_step_size knobs - callbacks/ckpt_attn_dropout.yaml: composed callback (checkpoints + random_attn_dropout with values [0.1, 0.5, 0.8, 0.9, 0.95, 0.97, 0.98]) - eval/eval_dfot_val.py: 96x96 -> 512x512 nearest-neighbor upscale, palette (gt=green, chunk=red, ar=yellow), world-coord pixel scaling, threaded cfg_scale Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- sbatch_train_dfot_200ep_full750_pace.sh: minor cleanup - sbatch_train_dfot_400ep_full750_pace.sh: 400ep H200 launch with scheduler.max_steps=18800 (fixes the 200ep cosine-not-decaying bug) - sbatch_train_dfot_400ep_attndrop.sh: 400ep + random_attn_dropout (the 5.6x sim_coverage win) - scripts/eval_cfg_latest.py: post-hoc DFoT eval CLI with --cfg-scale, --ar-chunk-size, --ar-step-size, --ar-inference-chunk-size, --ar-inference-step-size, --skip-val/--skip-sim - scripts/eval_fsd_latest.py: convenience wrapper for inference_mode=chunk - scripts/sbatch_fsd_eval.sh + sbatch_sim_sweep.sh: sbatch templates for closed-loop sim sweeps Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the abstract bases for the upcoming refactor: - egomimic/algo/outer_stage.py: OuterStage base. Owns an inner_stage field (the trunk). Subclasses implement encode (raw batch -> trunk-input tensor; can sample noise / record state on ctx) and decode (trunk-output -> per-modality prediction keys on batch). - egomimic/algo/loss.py: Loss base + CompositeLoss (weighted sum of terms) + MSELoss (per-modality MSE between pred_key and target_key). Loss policy becomes data — a hydra config block — rather than inheritance. No algorithm code uses these yet; HNetOuterStage / DFoTOuterStage and the algo.hnet.HNet / algo.dfot.DFoT refactors come in follow-up commits on this branch.
Foundational refactor for the upcoming DFoTOuterStage + DFoTLoss classes: - q_sample(x, t) -> dict: forward-noising step. Returns x_t, noise, alpha_t, sigma_t, logsnr, and the precond_scale * logsnr time_cond the backbone consumes. No backbone call inside. - compute_loss(v_pred, q_state) -> per-token weighted MSE: takes the dict from q_sample plus the backbones v_pred and computes the SNR-weighted epsilon-MSE. - forward(backbone, x, t, cond): kept as a back-compat wrapper that calls q_sample, runs the backbone, then compute_loss. Existing callers (DFoT.forward_training) are unaffected. Bitwise verified equivalent to the prior single-method path via the included /tmp/test_diffusion_split.py smoke (loss + x_pred match exactly, same random seed). discrete_diffusion.py is unsplit for now; current configs use continuous.
…verified) - egomimic/algo/dfot/outer_stage.py: DFoTOuterStage subclass. encode: encode obs to per-token cond, sample noise levels, run diffusion.q_sample, store q_state + external_cond on ctx, return noisy x_t. decode: write batch[pred_v] for the loss to read. forward override threads cu_seqlens/max_seqlen (packed mode) and time_cond into the backbone call. - egomimic/algo/loss.py: DFoTLoss class. Reads batch[pred_v] and ctx.q_state, calls diffusion.compute_loss (SNR-weighted eps-MSE), reduces to scalar. Bitwise verified via /tmp/test_dfot_outer_stage.py: padded-mode loss through DFoTOuterStage + DFoTLoss matches DFoT.forward_training to 0.0e+00 difference at fixed seed. Real CondEncoderModule + DFoTBackbone + ContinuousDiffusion submodules; no mocks of the math path. Algo class (DFoT.forward_training) is NOT yet wired to use these — that comes in the next commit on this branch. Inference paths (closed-loop AR sample_step, chunk plan-execute) also deferred.
Algo class: - __init__ now takes outer_stage: DFoTOuterStage and optional loss: Loss (auto-built as DFoTLoss(outer_stage.diffusion) if None). - Removes legacy cond_encoder, backbone, diffusion_type, diffusion_kwargs, cond_output_key args — they now live on the outer_stage subblock. - Adds @Property accessors for cond_encoder, backbone, diffusion, outer_stage, loss so existing inference paths (_inference_step_ar, _inference_step_chunk, _sample_chunk, forward_eval) keep working unchanged via property forwarding. - forward_training shrinks ~40 LOC -> ~20 LOC: build ctx, call outer_stage(batch, ctx), call loss(batch, ctx). No more inline diffusion math; no more cu_seqlens threading at this level. - Adds ar_inference_step_size knob. dfot_pushshapes.yaml: - New outer_stage: block wraps cond_encoder + backbone + diffusion. - Removes top-level diffusion_type / diffusion_kwargs; the diffusion module is now its own _target_ inside outer_stage. - loss: omitted (uses default DFoTLoss(outer_stage.diffusion)). End-to-end smoke (scripts/test_dfot_refactor_e2e.py) verifies the config instantiates via hydra and forward_training emits a finite scalar loss. Bitwise loss-equivalence was already shown in the prior commit (test_dfot_outer_stage.py). Old checkpoints WILL NOT load — state_dict keys moved from nets.{cond_encoder,backbone}.* to nets.outer_stage.{cond_encoder,inner_stage}.* This is intentional per the agreed clean-break refactor.
Adds scripts/test_dfot_inference.py: instantiates the refactored DFoT from dfot_pushshapes.yaml, runs inference_step in both ar and chunk modes, asserts action is (action_dim,) and finite. Verifies the @Property accessors (self.backbone, self.cond_encoder, self.diffusion) forward correctly to outer_stage submodules so the closed-loop AR and chunk-mode inference paths keep working after the refactor. Passing on compute node 8997316: [ar] action @ t=0: [0.30 0.47] [ar] action @ t=1: [0.44 1.06] [chunk] action @ t=0: [-0.78 -1.89]
…ied) - egomimic/algo/hnet_outer_stage.py: HNetOuterStage class. Inherits from OuterStage with inner_stage = HNetCore (stage tree). Owns cond_encoder, input_modules (summed per-token contributions), action_out head. Three forward paths inherited from the old HNetPolicy pattern: forward(batch, ctx) dispatcher (padded/packed), generate (offline AR), init_step_state + step (online single-tick). - egomimic/algo/loss.py: HNetLoss class. Reads batch[pred_action] + batch[actions], adds per-chunker ratio_loss_from_aux from ctx.aux. - scripts/test_hnet_outer_stage.py: equivalence smoke. Instantiates the existing hnet_pushshapes.yaml subcomponents, wraps the SAME instances in HNetPolicy and HNetOuterStage, ties their action_out heads, runs identical padded forward. Verified bitwise (max diff 0.00e+00 at fixed seed) on H200 alloc 8989249. Also smoke-tests the step inference path (shape + finite). The old HNetPolicy class is still in egomimic/algo/hnet.py and not yet removed. Algo-class refactor and yaml updates come in follow-up commits on this branch. Old checkpoints will NOT load — state_dict keys move from policy.* to outer_stage.* (or wherever the algo class places it). Per clean- break policy.
Algo class:
- HNet.__init__ now takes outer_stage: HNetOuterStage + loss: Optional[Loss]
instead of cond_encoder + hnet + action_dim + action_horizon +
d_model + action_head_type + input_modules. action_horizon read from
outer_stage.
- Loss defaults to HNetLoss() if not provided.
- self.nets is now ModuleDict({outer_stage, loss}); old keys
(self.nets[policy], self.nets[cond_encoder], ...) are exposed via
@Property forwarding to outer_stage submodules so legacy callsites
in forward_eval / _teacher_forced_packed / _ar_rollout_packed / step
inference keep working.
- forward_training builds (batch, ctx), calls outer_stage(batch, ctx)
+ loss(batch, ctx), unpacks per-term breakdown (ctx.action_loss,
ctx.ratio_loss) into the predictions dict for logging.
HNetLoss:
- Computes action MSE + ratio_loss_from_aux(ctx.aux) and stashes the
per-term split on ctx for the algo to log separately.
HNetOuterStage:
- Adds back-compat bridge methods forward_padded(actions, obs) and
forward_packed(actions, obs, cu, msl) returning (pred, aux). The
forward_eval / _teacher_forced_packed paths use these; .generate /
.step / .init_step_state already had matching signatures.
hnet_pushshapes.yaml:
- New outer_stage: block wraps cond_encoder + hnet stage tree +
input_modules + action_head_type. Top-level keeps training-recipe
knobs (init_weights_range, lr_multipliers, ...) and embodiment
wiring.
- loss: block omitted (defaults to HNetLoss()).
scripts/test_hnet_refactor_e2e.py: packed-mode forward_training smoke.
Verified passing on H200 alloc 8989249 — produces action_loss 1.51 +
ratio_loss 0.032 + chunker stats for a 2-episode packed batch
(T=12+20). Padded mode hits a pre-existing torch SDPA error
(Explicit attn_mask should not be set when is_causal=True) in
train mode — this is in the trunk code, not introduced by the
refactor (the production training uses packed mode and never hits
the padded-train path).
Old HNetPolicy class is still in algo/hnet.py for now (no longer
used by HNet algo); will be removed in a cleanup commit once all
stage-based + flat yamls are migrated.
…hema Same outer_stage block pattern as the base hnet_pushshapes.yaml, applied to the variant configs. Each yaml moves cond_encoder + hnet stage tree + (optional) input_modules + action_head_type under outer_stage; keeps training-recipe knobs + embodiment wiring at the top level. Configs migrated: - hnet_pushshapes_big.yaml (d_model 256, 21M params) - hnet_pushshapes_crossattn.yaml (cond_mode: cross_attn) - hnet_pushshapes_mamba_encdec.yaml (M8 encoder/decoder) - hnet_pushshapes_obs_ar.yaml (ObsToken input module) - hnet_pushshapes_obs_ar_large.yaml (ObsToken + d_model 256 + T8) - hnet_pushshapes_recipe.yaml (H-Net paper recipe) scripts/test_hnet_yamls_load.py: batch instantiate smoke. Verified on H200 alloc 8989249 — all 7 stage-based yamls (base + 6 variants) instantiate from hydra config and produce sensible param counts (5.5M baseline up to 42.8M obs_ar_large).
- egomimic/algo/flat_fused_outer_stage.py: FlatFusedOuterStage class. Structurally a rename of FlatFusedPolicy with OuterStage inheritance plus an OuterStage forward(batch, ctx) dispatcher delegating to the existing forward_padded / forward_packed. Legacy generate / step / init_step_state preserved verbatim. encode / decode raise NotImplementedError since the interleaved 2T-token flow does not cleanly split along encode -> trunk -> decode. - egomimic/algo/hnet.py: HNetFused is now a thin pass-through subclass of HNet, kept as a separate _target_ for the existing flat yamls. All flat-fused behavior moved into FlatFusedOuterStage; HNet.__init__ already tolerates outer_stage.inner_stage=None. - 3 flat yamls migrated to outer_stage schema: hnet_pushshapes_fused.yaml, hnet_pushshapes_fused_lowlr.yaml, hnet_pushshapes_fused_pusher.yaml. - scripts/test_hnet_yamls_load.py extended to cover all 10 H-Net configs. Verified on H200 alloc 8989249: 10/10 instantiate successfully (7 HNetOuterStage + 3 FlatFusedOuterStage). Param counts sensible. Old FlatFusedPolicy class still in algo/hnet.py for now; cleanup of unused legacy classes (HNetPolicy, FlatFusedPolicy) is a follow-up.
EMACallback: shadow of all float state (decay 0.9999) updated per train batch, saved as ema_state_dict in every ckpt, restored on resume. Eval opts in via ckpt_loading --use-ema (overlays EMA tensors; int buffers stay live). VisualCore norm_layer=group swaps backbone BatchNorm2d -> GroupNorm(C/16,C) (EMA-averaged weights + live BN stats mismatch — the reason DP uses obs_encoder_group_norm). Default batch = byte-identical. Launcher gains EXTRA env passthrough. Arm: bf_var4_ema (var4 recipe + EMA + GN, callbacks=checkpoints_ema). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…ment via ObsEncoders cu stamp; nopre+EMA arms x2 crop scopes Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…ment+coverage EMA candidate) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…ssing escape-combo cell for the split family) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…de mirror of dp_noaug) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
… history); bf_nopre_win arm (L0 w16, L1 w8, apex full) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…ral vs 4+4; toward sym_w1 all-encode profile) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…ng bug; now verified E6+T2 / E7+T1 x2 levels Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
…radNormLogger callback (per-top-module tree walk, read-only) in default callbacks Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CjuZ3zwWaCeQQigxiNuzGw
- RatioLoss now logs boundary_rate_{i}/avg_chunk_len_{i} + aggregate
boundary_rate with chunk_stats_from_aux semantics (hard mask, forced
starts count); frac_indecisive and avg_chunk_len_tok removed
- GradNormLogger deleted; per-layer grad norms come from the existing
pl_model log_per_layer_grad_norms flag, now default-on in bf_prdec_abl
…h views) + PCA wheel-zoom/drag-pan/dblclick-reset
…be, multi-epoch html rebuild - bf_chunkviz_nopre_bracket.sbatch: snap+export nopre_gmm 1499/1999/2499 (true-epoch verified) for organization-onset bracket - bf_chunkviz_reverdict.sbatch: re-probe arms condemned pre-ep2000 (var4/sig_var4/ste/regoff/cvae) at their late ckpts - bf_rebuild_orghtml.sbatch: rebuild nopre_gmm_organization.html across all 5 epochs
…en->frame step function smoothed to a heat gradient)
…n), no interpolation
…ad w/ temperature (T=2/3 arms; calibrated-clamp router REMOVED: grad=0 beyond bounds) + SDP fix2 family (pusher_hold, w=0.001, adaLN-zero denoiser, r0 ladder) + prenet sr2 trio + diffusion heads + thin ckpts + chunkviz full-precision + abslogit saturation telemetry. prev-action proprio REMOVED (action-copying risk)
…n fast weights, per-episode reset, tanh-gated ~identity at init; DualTrunkLevel ttt knob; arms fix2ada+ttt (H-Net) and txar_sdpada(+ttt) flat pair
… TTT arms NaN'd); SDPHead detach_offregime + FlowHead denoiser_arch knobs; new arms dpclone_flow/ddpm_regoff499/sdp_srfix/sr2c15/sr2opt; 250-cadence launcher
… selection
Video image storage (video_codec.py, _common.decode_video_span, zarr_writer,
zarr_dataset_multi): store frames as one mp4 per group of frames_per_chunk
instead of per-frame JPEG. Measured on real fold episodes: 44.8 -> 20.2 KB/frame
and 8.33 -> 1.68 ms/frame decode, and h264 also beats JPEG at EQUAL bytes
(+2.4..2.9 dB PSNR at 4-8 KB), so it is strictly better coding rather than a
quality-for-size trade. crf15/gop30/yuv420p; decode speed is flat across CRF, so
compressing harder would buy only disk.
Layout: chunk-indexed, NOT one blob per episode -- a span read would otherwise
pull the whole episode (~68 MB) and we read ~13 spans/episode. Frame f lives in
chunk f//fpc at offset f%fpc; chunks start on a keyframe (keyint_min=g,
sc_threshold=0) so each decodes independently. Readers dispatch on
_features[key]["dtype"] ("jpeg" vs "h264"), so existing episodes are untouched
and both formats coexist. decode_video_span returns the same (T,3,H,W) float
array as decode_jpeg_window, so pack_collate/TargetBuilder need no change;
equivalence-gated against the JPEG path on the same episode (38.6-39.1 dB,
including a chunk-boundary span and the partial tail chunk) and stride parity
matches decode_jpeg_window_strided.
annotation_processing.py: port of the role-in-key-name scheme -- role lives in
the zarr key NAME (annotations_task/annotations_subtask), entries are plain
{text,start_idx,end_idx} spans. _load_annotations is now per-key (the previous
single-slot cache aliased across keys) and an absent key degrades to [] rather
than raising.
zarr_dataset_packed: annotation_key + span_indices, so a run can read a
structural segmentation key (fold_segments) and select individual spans. Note
fold_segments deliberately sits OUTSIDE the annotations* glob -- those spans
define training windows, not language, and must not reach the batch as a text
role.
fold_span_transforms: RH_WRIST_MODE=none -> 126-dim keypoints-only (the wrist
pose is redundant with the keypoints). ypr(138)/pos(132) unchanged; default
still pos.
ZarrDataset dispatched purely on features[key]["dtype"]. That is a claim, and a
stale claim routes real mp4 payloads into simplejpeg -- which fails deep in the
decoder, far from the actual cause. Episodes converted before the codec switch,
or whose metadata was copied from a sibling, could not be loaded at all.
_classify_image_keys now uses three signals, cheapest first, escalating only on
disagreement:
1. the declared dtype;
2. the element count -- per-frame JPEG stores one element per frame, chunked
video one mp4 per frames_per_chunk, so a full-length array is JPEG and a
much shorter one is video. Compared with ">=" because writers pad past
total_frames (a 290-frame episode occupies 300 slots);
3. the magic bytes of element 0, read only to break a tie, and authoritative
when read.
Verified on 72 real episodes across 6 datasets: identical classification and
ZERO payload reads, so the common path costs nothing.
Also fixes two gaps this exposed:
* video keys with no "video" metadata block now recover frames_per_chunk by
decoding chunk 0. It is not derivable arithmetically -- 1000 frames over 4
chunks admits any fpc in (250, 333].
* ZarrDataset.__getitem__ had NO video branch, only _read_span did, so a
detected video key hit a frame-indexed read of a chunk-indexed array and
returned the wrong elements. It now mirrors _read_span, matching
decode_jpeg_single for horizon=None and _pad_sequences at the episode tail.
Mismatch warnings are deduped per (key, declared, detected) so a systematically
mislabelled dataset logs once, not once per episode.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012V58H37tmcvgDthELMd5Xk
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Claude Code ReviewReviewSummaryReplaces trust-the-dtype dispatch in Key concerns
Suggestions
Verdict: Request ChangesThe core detection logic is sound and the bug is real, but I want (1) test coverage for the mp4-declared-as-jpeg case and the Reviewed by Claude · Review workflow |
9cb385b to
95cfd61
Compare

ZarrDataset dispatched purely on features[key]["dtype"]. That is a claim, and a
stale claim routes real mp4 payloads into simplejpeg -- which fails deep in the
decoder, far from the actual cause. Episodes converted before the codec switch,
or whose metadata was copied from a sibling, could not be loaded at all.
_classify_image_keys now uses three signals, cheapest first, escalating only on
disagreement:
video one mp4 per frames_per_chunk, so a full-length array is JPEG and a
much shorter one is video. Compared with ">=" because writers pad past
total_frames (a 290-frame episode occupies 300 slots);
when read.
Verified on 72 real episodes across 6 datasets: identical classification and
ZERO payload reads, so the common path costs nothing.
Also fixes two gaps this exposed:
decoding chunk 0. It is not derivable arithmetically -- 1000 frames over 4
chunks admits any fpc in (250, 333].
detected video key hit a frame-indexed read of a chunk-indexed array and
returned the wrong elements. It now mirrors _read_span, matching
decode_jpeg_single for horizon=None and _pad_sequences at the episode tail.
Mismatch warnings are deduped per (key, declared, detected) so a systematically
mislabelled dataset logs once, not once per episode.
Co-Authored-By: Claude Opus 5 (1M context) noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_012V58H37tmcvgDthELMd5Xk