# 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: 4. **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`: ```bash 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) ```bash 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__/`. ### (b) End-to-end (build + norm-stats + train) ```bash 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 ` and per-(speeds) `--dst` | | `compute_norm_stats` | `--repo-id ` `--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 = "_"` unless `Ablation.shared_norm_key` is set, in which case it is `"_"`. Norm stats are written to `assets///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 `/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: ```text 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 ```bash 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// --port=$((8000 + g)) & done ``` ### Step 2: launch the 8-way eval driver ```bash 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_x_/