VLAwithVariousSpeed / README_ablation.md
Alan0928's picture
Upload folder using huggingface_hub
08ff31f verified
|
Raw
History Blame Contribute Delete
17.4 kB

Variable-Speed Ablation Sweeps

Workflow for running ablation studies that compare different target-speed sets, different ways of integrating the speed signal into the model, and different soft-prompt capacities, against a shared LIBERO evaluation harness.

For the underlying single-pipeline workflow (build one dataset, train one model), see VARIOUS_SPEED_README.md.

1. Research questions

The default ABLATIONS table in scripts/run_ablations.py answers three research questions:

  1. Step size β€” holding the speed range fixed at [0.5, 2.0], does denser speed coverage improve generalization?
  2. Range β€” holding step size roughly fixed, how much does the speed range matter? Where does the policy break?
  3. Integration strategy β€” given a fixed training speed set, does it matter how the speed signal reaches the model: as text in the instruction prompt, as a continuous feature that modulates the action expert (adaRMSNorm), or as a learned soft-prompt inserted between the image and the language tokens?

Plus a hyperparameter sweep on the soft-prompt capacity:

  1. Soft-prompt P β€” how many learnable tokens per speed anchor are actually needed?

Speed-set sweep (text conditioning across all entries)

name speeds role
g1_baseline [1.0] no augmentation, baseline
g2_coarse [0.5, 1.0, 1.5, 2.0] wide range, coarse step (0.5)
g3a_step025 [0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0] wide range, fine step (0.25)
g4_narrow [0.75, 1.0, 1.25, 1.5] narrow range probe
g5_extreme [0.25, 0.5, 1.0, 2.0, 4.0] extreme range probe

Speed-integration sweep (fixed speeds [0.75, 1.0, 1.25, 1.5])

name speed_integration model surgery
speedint_text text none (instruction prompt only)
speedint_modulation modulation requires Pi0Config.speed_modulation=True -> MLP head + adaRMSNorm in action expert
softprompt_p8 soft_prompt K=4 Γ— P=8 learnable tokens (also serves as the P=8 arm of the P-sweep below)

Soft-prompt P-length sweep (same speeds, varies tokens per anchor)

name P
softprompt_p1 1
softprompt_p4 4
softprompt_p8 8
softprompt_p16 16
softprompt_p32 32

g4_narrow, speedint_text, speedint_modulation, and all five softprompt_p* entries share the same speed set (0.75, 1.0, 1.25, 1.5) -- the runner builds that dataset only once.

Run-time dedup

  • Build dedup: ablations with identical speeds reuse one built dataset directory.
  • Norm-stats dedup: ablations whose effective asset_id collides reuse one norm_stats.json. The five softprompt_p* entries declare shared_norm_key="softprompt_shared" because norm stats depend only on the dataset, not on the soft-prompt parameter P.
  • For the default 12-ablation table, this means 5 dataset builds, 8 norm stats computes, and 12 training runs.

Note on data-volume confound

Each ablation produces len(speeds) Γ— source_episodes training samples, so larger speed sets train on more total data. When comparing groups, prefer plotting metrics against samples seen (or epochs), not raw step count, to disentangle "more granular speeds" from "more data".

2. Calibrate eps once (data-driven cleaning thresholds)

Before the first build, profile the source dataset to pick --clean-*-eps:

uv run python scripts/profile_action_norms.py \
  --src "$SRC" \
  --output "$SRC/action_norm_profile.json"

If you want near-zero cleaning, use the printed P1 / P5 percentiles as --clean-eps overrides. The default is 0.0 (no cleaning), since LIBERO demos never fall below 1e-4 in practice. Set --clean-eps 1e-4 (or the percentile value) if you bring in a noisier dataset.

3. Two ways to drive a sweep

(a) Data-prep only (separate machine)

export SRC=/path/to/libero_data
export ROOT=/path/to/ablation_outputs

uv run python scripts/build_ablation_datasets.py \
  --src "$SRC" --out-root "$ROOT" \
  --num-workers 16 \
  --clean-eps 0.0  # 0 = no cleaning (LIBERO default); set 1e-4 for noisier data

This shells out to scripts/build_libero_speed_dataset_mp.py once per unique speed set. Each dataset lands at $ROOT/libero_speed_<speed_token>_<run_tag>/.

(b) End-to-end (build + norm-stats + train)

uv run python scripts/run_ablations.py \
  --src "$SRC" --out-root "$ROOT" \
  --train-config pi05_libero_various_speed_all \
  --base-asset-id libero_various_speed_all_pi05 \
  --exp-prefix pi05_ablation \
  --num-train-steps 30000 \
  --build-num-workers 16 --train-num-workers 8

Useful flags: --only NAME[,NAME] to run a subset, --skip-build / --skip-norm-stats / --skip-train to bypass stages, --dry-run to print commands without running them. Always do a --dry-run once before the first real run to sanity-check the generated CLI strings (especially --model.soft-prompt-speeds 0.75 1 1.25 1.5 which must be space-separated).

4. How configs are scoped per ablation

A single TrainConfig name (e.g., pi05_libero_various_speed_all) is reused across all ablations. Per-ablation differences are applied at the CLI via tyro's overridable_config_cli:

Stage Override
build --speeds <ab.speeds> and per-(speeds) --dst
compute_norm_stats --repo-id <dataset_dir> --asset-id <effective_asset_id>
train_pytorch --data.repo-id, --data.assets.asset-id, --data.speed-integration, --exp-name, --eval-speed-set, plus per-ablation extra_train_args

effective_asset_id = "<base>_<ablation.name>" unless Ablation.shared_norm_key is set, in which case it is "<base>_<shared_norm_key>". Norm stats are written to assets/<train_config_name>/<effective_asset_id>/norm_stats.json.

--eval-speed-set makes the wandb per-speed loss breakdown follow the ablation's own speeds. It only fires when use_flow_control=True (i.e. the modulation path); for text and soft_prompt paths it's harmless dead-code that nonetheless stays in sync.

5. Diagnostic outputs after each build

Three artifacts under <dataset_dir>/meta/:

  • cleaning_summary.json β€” frame counts and ratios that were near-zero and got zeroed by clean_near_zero_actions. Cross-check against the percentiles in action_norm_profile.json from Β§2.
  • replay_summary.json β€” per-target-speed mean/median/max of integrated translation/rotation L2 error vs. source, path-length ratios, padded ratio, and gripper-switch delta. Also printed to stdout at end of build.
  • speed_metrics.jsonl β€” one row per (source_episode, target_speed) with the same raw fields as above; useful for plotting.

If transl_L2 / rot_L2 medians are not at floating-point noise level (~1e-7), or if gripper_delta sum != 0, something is wrong with that build.

6. LIBERO evaluation across 8 GPUs

For each trained model, evaluate at multiple speeds β€” including out-of-distribution values β€” to measure interpolation vs. extrapolation:

training:   g3a_step025 ([0.5, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0])
evaluation: [0.4, 0.5, 0.6, 0.75, 1.0, 1.25, 1.5, 1.75, 2.0, 2.5, 3.0]

For each speed, run all four LIBERO suites on the 8-GPU server. Use scripts/eval_libero_8gpu.sh, which partitions the work to roughly equal wall-clock per GPU:

GPU suite task_ids episodes
0 libero_spatial all 500
1 libero_goal all 500
2 libero_object all 500
3 libero_10 0,1 100
4 libero_10 2,3 100
5 libero_10 4,5 100
6 libero_10 6,7 100
7 libero_10 8,9 100

libero_10 (libero_long) has the longest rollouts (max_steps=520), so splitting it 5 ways balances total wall-clock against the three short suites at max_steps ≀ 300.

Step 1: launch one policy server per GPU

for g in 0 1 2 3 4 5 6 7; do
  CUDA_VISIBLE_DEVICES=$g uv run python scripts/serve_policy.py \
    policy:checkpoint --policy.config=pi05_libero_various_speed_all \
    --policy.dir=checkpoints/<run>/<step> --port=$((8000 + g)) &
done

Step 2: launch the 8-way eval driver

SPEED=1.25 ./scripts/eval_libero_8gpu.sh

This dispatches scripts/eval_libero_speed.py 8 times in parallel (one per GPU/server), each writing per-episode results to results/libero_eval_<speed>x_<ts>/<label>_<speed>x.json and a video directory under the same root.

What gets recorded per episode

scripts/eval_libero_speed.py records:

  • success (bool, from env.step returning done=True before max_steps)
  • steps (int, policy steps actually executed, excluding the num_steps_wait warmup frames)
  • task_id, episode_idx, task_description, suite, speed

Successful rollouts terminate as soon as the env returns done; failures run to max_steps for the suite.

What gets printed per rank and globally

Each rank prints a summary line like:

[rank=0] libero_spatial speed=1.25x  success=412/500 (82.4%)  mean_steps_success=87.3 median=82.0  mean_steps_failure=220.0  mean_steps_all=110.6

After all 8 ranks finish, the driver auto-aggregates per suite (merging the 5 libero_10 shards back into one row) and globally:

--- per-suite rollup ---
  libero_spatial    success=412/500 (82.4%)  mean_steps_success=87.3   mean_steps_all=110.6
  libero_goal       success=...
  libero_object     success=...
  libero_10         success=...   (merged across ranks 3-7)

GLOBAL (speed=1.25): success=1342/1500 (89.5%)  mean_steps_success=128.5  mean_steps_all=156.8

Use the mean_steps_success vs. mean_steps_all gap as a fast read-out: when a policy fails, it tends to walk to the time limit, so mean_steps_all rises sharply while mean_steps_success stays roughly constant.

How speed reaches the policy

The eval client adds both "speed": float(speed) and "speed_label": "1p25x" (etc.) to the element dict it sends to the websocket server. Whichever integration strategy was trained, the corresponding pipeline consumes the appropriate key:

  • text β†’ data-side SpeedConditionedPrompt reads speed_label and rewrites the instruction prompt
  • modulation β†’ model-side reads observation.speed (raw scalar) and feeds it through an MLP that fuses with the timestep embedding to drive adaRMSNorm in the action expert
  • soft_prompt β†’ model-side reads observation.speed for nearest-anchor lookup over the K learnable token groups

modulation and soft_prompt share the same observation.speed field end-to-end; there is no separate flow_control channel anymore.

For OOD speeds at eval time, text benefits from token-level extrapolation, modulation is naturally continuous, but soft_prompt falls back to the nearest training anchor (e.g., speed=2.0 with anchors (0.75, 1.0, 1.25, 1.5) clamps to 1.5). This is by design and is the main reason to compare the three strategies on the same OOD test set.

7. Implementation notes

The speed-integration paths (modulation + soft-prompt) were implemented / refactored across these files (branch 0502_mp_process):

  • src/openpi/models/model.py β€” Observation.speed is the single end-to-end channel for raw target speed. The legacy flow_control field (and the LogSpeedControl data transform that produced it) was removed.
  • src/openpi/models/pi0_config.py β€” added speed_modulation: bool (replaces the old flow_control_dim: int), soft_prompt_speeds (tuple of K anchor speeds), and soft_prompt_p (tokens per anchor).
  • src/openpi/models_pytorch/pi0_pytorch.py:
    • __init__ registers two optional sub-modules:
      • speed_mod_mlp_in/out + speed_condition_mlp_in/out when speed_modulation=True. Reads raw observation.speed (shape (B, 1)), pushes through MLP, fuses with timestep embedding, sends to action expert as adarms_cond.
      • soft_prompt_tokens: nn.Parameter of shape (K, P, paligemma_width) with N(0, 0.02) init, plus a non-persistent buffer soft_prompt_anchors: tensor(K,), when soft_prompt_p > 0.
    • embed_prefix does an argmin nearest-anchor lookup, then inserts (B, P, hidden) soft-prompt tokens between vision and language with full attention.
    • embed_suffix reads speed directly (raw scalar) for the modulation path; the MLP head is wide enough to learn any monotonic transform internally so log-scaling is unnecessary.
    • _preprocess_observation, forward, sample_actions, and denoise_step plumb speed through.
  • src/openpi/models_pytorch/preprocessing_pytorch.py β€” passes speed through SimpleProcessedObservation. (Previously it silently dropped flow_control, which is why the old modulation path appeared half-baked.)
  • src/openpi/policies/libero_policy.py β€” LiberoInputs passes speed through to the observation dict.
  • src/openpi/models/pi0.py β€” JAX Pi0 mirrors the PyTorch rename (speed_modulation, speed_mod_mlp_*, speed_condition_mlp_*, reads obs.speed).
  • src/openpi/training/config.py β€” LeRobotVariousSpeedLiberoDataConfig has a single high-level speed_integration: Literal["text", "modulation", "soft_prompt"] switch (no more auto, use_flow_control, or use_speed_prompt plumbing). The pi05_libero_various_speed_all_flow_prompt/noprompt configs were replaced by pi05_libero_various_speed_all_modulation.
  • tests/test_soft_prompt_smoke.py β€” light tests for config validation and the argmin nearest-neighbor logic. The full forward-pass test is gated to manual GPU runs because PaliGemma weights are heavy.

Token budget

For PI0.5, max_token_len=200 counts only the language tokens (image and soft-prompt tokens come from a separate budget). With P=8, the soft prompt adds 8 prefix tokens; P=32 is still well under typical attention budgets. For sanity, pi0_config.py:max_token_len only constrains the tokenized prompt, not the visual or soft-prompt embeddings.

Inference at OOD speeds (soft-prompt only)

embed_prefix uses an argmin lookup, so a speed not in soft_prompt_speeds falls back to the nearest training anchor. If you want linear interpolation between two adjacent anchors instead, modify the lookup in pi0_pytorch.py:embed_prefix. (text and modulation paths do not have this limitation.)

8. Adding a new ablation

  1. Append Ablation(name, speeds, ...) to the ABLATIONS tuple in scripts/run_ablations.py. Use a short unique name -- it is embedded in dataset directory, asset_id, and exp_name.
  2. (Optional) If the new group uses very different speeds than what profile_action_norms.py was last run for, re-run it.
  3. build_ablation_datasets.py --only <new_name> to produce the dataset (or skip if it shares speeds with an existing one).
  4. run_ablations.py --only <new_name> --skip-build to compute norm-stats and train.
  5. After training, eval_libero_8gpu.sh to evaluate at the speeds you care about.

9. Files added by this workflow

# New
scripts/profile_action_norms.py            # action-norm profiler (eps calibration)
scripts/build_libero_speed_dataset_mp.py   # multi-process speed dataset builder
scripts/build_ablation_datasets.py         # data-prep stage of ablation sweep
scripts/run_ablations.py                   # end-to-end ablation runner
scripts/eval_libero_speed.py               # LIBERO eval client w/ speed + step tracking
scripts/eval_libero_8gpu.sh                # 8-GPU eval driver
tests/test_soft_prompt_smoke.py            # smoke tests for soft_prompt logic

# Updated
scripts/build_libero_speed_dataset.py      # writes cleaning_summary.json + replay_summary.json
scripts/compute_norm_stats.py              # accepts --repo-id / --asset-id overrides
scripts/train_pytorch.py                   # eval_speed_set config-driven; wandb image logging removed
src/openpi/models/model.py                 # Observation.speed field
src/openpi/models/pi0_config.py            # soft_prompt_speeds / soft_prompt_p fields
src/openpi/models_pytorch/pi0_pytorch.py   # soft_prompt model surgery
src/openpi/models_pytorch/preprocessing_pytorch.py  # bug fix: flow_control + speed pass-through
src/openpi/policies/libero_policy.py       # speed pass-through
src/openpi/training/config.py              # eval_speed_set, speed_integration fields
src/various_speed/core.py                  # cleaned_any/both metrics + ratios