twanghcmut's picture
|
download
raw
16.8 kB
# Runbook — every command, in order
Typed top to bottom, this file takes a fresh checkout to a scored benchmark for one suite. Every
command below was run on this box; the CLI flags are copied from `--help`, not guessed.
The README's Quickstart is the same sequence with the commands only. This file adds what each step
writes, how long it takes, and how it fails.
It assumes setup is already done — third-party trees under `external/`, the three runtimes installed,
checkpoints and demonstration HDF5 on disk. [`SETUP.md`](SETUP.md) is that, step by step, including
which directory each download has to land in. On this box everything is already in place.
Two rules that apply to **every** command here:
- `OMP_NUM_THREADS=4` — without it the training loop oversubscribes threads and runs an order of
magnitude slower.
- `PYTHONPATH=src` — the `onf` package is not installed into the eval envs.
Shorthand used below:
```bash
cd /path/to/your/checkout # NOT hardcoded: sibling worktrees of this repo
export ONF=$(pwd) # exist on this box, and they are different branches
export PY=/home/quang/miniconda3/envs/stablevla/bin/python
export OMP_NUM_THREADS=4 PYTHONPATH=src
```
### Just want to run a LIBERO benchmark? Skip to §5.
§1–§4 build artifacts, and on this box they are **already built for all four suites**
(`g_nodes`/`g_edges`/`g_head`/`g_track` under `outputs/<suite>/latest/artifacts/`). Check with:
```bash
$PY -c "from onf.config import default_paths as d; import os; \
print({s: sorted(os.listdir(d().graph(s))) for s in ('object','spatial','goal','long')})"
```
If that prints four artifact sets, go straight to **§5 Benchmark**. Read §1–§4 only when you need to
rebuild a graph or retrain a head. The three commands worth running first, in this order:
```bash
$PY scripts/run_sr.py --check-env # GPUs, load, LIBERO-Plus episode counts
$PY scripts/run_sr.py --rung R1 --gpus 2,3 --dry-run # what would run, and for how long
$PY scripts/run_sr.py --rung R1 --gpus 2,3 # 35 episodes, ~15 min
```
---
Pick one suite and keep it fixed through §1–§4. Valid values: `object`, `spatial`, `goal`, `long`
(`long` is LIBERO-10; the mapping to the benchmark's own suite names lives in `configs/suites.yaml`).
```bash
export SUITE=goal
```
---
## 0. Inputs
Clones, environments, weights and demonstrations: [`SETUP.md`](SETUP.md). Point the repo at its trees
with:
```bash
export ONF_DATA=/path/to/data # demo hdf5 + per-suite fwm/ artifacts
export ONF_OUTPUTS=/path/to/outputs # graph artifacts land here
export ONF_RESULTS=/path/to/results # rollout mp4s land here
```
`onf.config.Paths` resolves everything from these three; nothing else is hardcoded.
| Input | Where | Note |
|---|---|---|
| demo HDF5 | `data/` (`Paths.hdf5(_hdf5_name(SUITE))`) | [`SETUP.md`](SETUP.md) step 4 |
| policy checkpoint | `stablevla/hf_weights/<suite>/`, `external/Isaac-GR00T/ckpts_n17_libero/<...>` | paths in `configs/suites.yaml` |
| LIBERO-Plus | `external/LIBERO-plus` | [`SETUP.md`](SETUP.md) step 1 |
On the author's box `data/`, `outputs/`, `results/` and `external/` are **symlinks into trees shared
with sibling git worktrees** — a fresh clone has real directories and none of this applies. Where they
are shared, another branch rebuilding an artifact will change it underneath you. `tests/golden/parity.json`
records a sha256 of each artifact for exactly this reason — if the parity test reports `ARTIFACT DRIFT`,
someone else rebuilt a file, it is not a regression in your code.
---
## 1. Field artifacts — `q_flow.npz` → `onf_head.npz`
The retrieval head pools a query window by *cleanliness*, and cleanliness is read off a trained
distance-to-manifold field. That field is needed both to train the graph head and at deploy time, so
it comes first.
The two directories the builders need are resolved by `onf.config.Paths`, but the HDF5 one goes
through a suite -> dataset-name mapping (`goal` -> `libero_goal`, `long` -> `libero_10`, ...), so
resolve both once and reuse them:
```bash
eval "$($PY -c "
from onf.config import default_paths
from onf.graph.build.from_demos import _hdf5_name
p = default_paths()
print(f'HDF5={p.hdf5(_hdf5_name(\"$SUITE\"))}')
print(f'FWM={p.fwm(\"$SUITE\")}')")"
echo "$HDF5 -> $FWM"
# demo velocity cloud -> $FWM/q_flow.npz, q_home.npz
$PY -m onf.field.build qflow "$HDF5" "$FWM"
# q_flow.npz -> $FWM/onf_head.npz (the field; ~10 s on CPU for `long`)
$PY -m onf.field.train "$FWM"
```
Or both in one step, which also runs the field training:
```bash
$PY -m onf.field.build suite --suite "$SUITE"
```
Skip this section if `<fwm_dir>/onf_head.npz` already exists. On this box all four suites already
have one (`data/fwm/onf_head.npz` for `object`, `data/fwm/<suite>/onf_head.npz` for the rest).
If it is missing, nothing breaks loudly — `cleanliness()` warns once and falls back to uniform window
weights. That is a different (worse) query encoder, so train and deploy must agree: never train with
a field and deploy without one.
---
## 2. Build the demonstration graph
```bash
$PY -m onf.graph build --suite "$SUITE"
```
Writes `g_nodes.npz` + `g_edges.npz` into `outputs/$SUITE/<run>/artifacts/` and **repoints
`outputs/$SUITE/latest` at that run**.
> **Read this before building `long`.** `outputs/` is a symlink into a tree shared with sibling
> worktrees, and `tests/golden/parity.json` pins the sha256 of `outputs/long/latest/artifacts/*`. So
> repointing `latest` does not just affect you: every checkout's `tests/test_parity.py` starts
> reporting `ARTIFACT DRIFT`, and any eval reading `latest` silently switches to your new graph.
> Build somewhere else unless you specifically mean to replace the shipped artifacts:
>
> ```bash
> export ONF_OUTPUTS=/tmp/my_build # `Paths` resolves everything from this; nothing else changes
> ```
>
> Steps 2–4 then read and write only under `$ONF_OUTPUTS`. Step 1 is separate: it writes into
> `Paths.fwm($SUITE)`, i.e. the shared `data/` tree, so pass an explicit output directory there
> (the recipe in §1 already takes one as an argument).
Useful flags: `--limit-demos 8 --limit-tasks 2` for a fast smoke build, `--dry-run` to print the
resolved config and exit without touching `outputs/`, `--coarsen N` to change raw frames per node.
> **On this box, `outputs/object/latest` is a real directory, not a symlink** — a stale build from an
> earlier run. `Paths.graph("object")` therefore resolves to it and a new `build` cannot repoint
> `latest`. It contains a valid `g_nodes`/`g_edges` pair, so training into it works; just be aware
> that "latest" is frozen for that one suite.
## 3. Train the retrieval head
```bash
$PY -m onf.graph train --suite "$SUITE" --device cuda:3
```
Writes `g_head.npz` next to the graph it trained on. ~1 hour per suite on one GPU. `--epochs 1` for a
smoke run; `--dry-run` prints the resolved config and the planned stages without writing anything.
This is the only learned component on the retrieval path. The blend weight below is the second and
last one in the repo, and it is trained against this head frozen.
## 4. Fit the sentinel artifacts
```bash
ART=$($PY -c "from onf.config import default_paths; print(default_paths().graph('$SUITE'))")
$PY scripts/build_sentinel_artifacts.py "$ART" --suite "$SUITE" --device cuda:3
```
Fits the transition kernel (`pi`, `beta`, `leak`) and the basin geometry into `g_track.npz`, plus a
`sentinel_artifacts.json` provenance sidecar. ~30 min per suite.
This is what the sentinel belief filter needs. A missing `g_track.npz` is a hard error at launch, not
a silent fallback.
### The blend's own artifacts
`base` needs nothing below. Every `blend*` mode needs the first of them; only `blend_learned` needs
all three. Design: [`technical/03-action-chunk-blend.md`](technical/03-action-chunk-blend.md).
```bash
# metres/radians per action unit, fitted off the demos -> <graph dir>/action_scale.json
$PY scripts/fit_action_scale.py "$SUITE"
```
Seconds, CPU, no policy needed. It **refuses to write** a fit whose position or rotation block scores
R² < 0.8 — that gate catches a rotation delta subtracted instead of composed, which is the mistake
that silently corrupts the orientation channel. A blend mode with no `action_scale.json` raises at
launch and names this command; there is no default calibration.
```bash
# the frozen policy's own action chunks over the whole corpus -> <graph dir>/a_pi_raw.npz
$PY scripts/cache_policy_actions.py "$SUITE"
# train the blend weight against a FROZEN retriever -> <graph dir>/g_alpha.npz
$PY -m onf.graph train --suite "$SUITE" --objective chunk --device "cuda:$GPU"
```
`cache_policy_actions.py` talks to a running policy server (`--host` / `--port`, default port 10093)
and is resumable per demo; `scripts/run_cache_policy_actions.sh` brings the server up around it.
`--objective chunk` optimises only the weight head — it loads `g_head.npz`, freezes it, and writes
`g_alpha.npz` beside it without rewriting the head it froze.
### Build everything from scratch, in one block
Verified end to end on 2026-08-07 with a smoke-sized graph (`--limit-demos 8 --limit-tasks 2`,
CPU, `long`). Timings are from that run; a full-suite build is the per-step estimates above.
```bash
export SUITE=long
export SCRATCH=/tmp/onf_scratch # nothing here touches the shared outputs/ tree
export ONF_OUTPUTS=$SCRATCH/outputs
mkdir -p "$SCRATCH/fwm"
HDF5=$($PY -c "from onf.config import default_paths as d
from onf.graph.build.from_demos import _hdf5_name
print(d().hdf5(_hdf5_name('$SUITE')))")
$PY -m onf.field.build qflow "$HDF5" "$SCRATCH/fwm" # 1s -> q_flow.npz, q_home.npz
$PY -m onf.field.train "$SCRATCH/fwm" # 8s -> onf_head.npz
$PY -m onf.graph build --suite "$SUITE" --limit-demos 8 --limit-tasks 2 # 12s -> g_nodes, g_edges
$PY -m onf.graph train --suite "$SUITE" --epochs 1 --device cpu # 86s -> g_head.npz
ART=$($PY -c "from onf.config import default_paths; print(default_paths().graph('$SUITE'))")
$PY scripts/build_sentinel_artifacts.py "$ART" --suite "$SUITE" --device cpu # 126s -> g_track.npz
```
Drop the two `--limit-*` flags and `--epochs 1`, and use `--device cuda:N`, for a real build.
**Verify what you built** — loads the artifacts and runs one step of each regime:
```bash
$PY - <<'EOF'
import numpy as np
from onf.config import GraphConfig, default_paths
from onf.graph.run.track import GraphTracker
tr = GraphTracker.load(default_paths().graph("long"), cfg=GraphConfig(hist=8, device="cpu"), device="cpu")
n = tr.retriever.nodes
print(f"V={len(n)} demos={n.n_demos} tasks={n.n_tasks} beta={tr.kernel.beta:.3f} leak={tr.leak:.3f}")
q = n.q_raw[:8].astype(np.float64); tr.reset()
print("step:", tr.step(q).phase_hat, "| where:", tr.where_target(q[-1]).depth)
EOF
```
A build that produces all four `.npz` files but cannot be loaded here is the failure worth catching
early; `g_head.npz` and `g_track.npz` both carry a `graph_hash` stamp and are refused against a graph
they were not fit on.
### All three steps for the three suites that only have a graph
```bash
GPU=3 # sequential, ~1.5 h per suite
for SUITE in goal spatial object; do
$PY -m onf.graph build --suite "$SUITE"
$PY -m onf.graph train --suite "$SUITE" --device "cuda:$GPU"
ART=$($PY -c "from onf.config import default_paths; print(default_paths().graph('$SUITE'))")
$PY scripts/build_sentinel_artifacts.py "$ART" --suite "$SUITE" --device "cuda:$GPU"
done
```
---
## 5. Benchmark
Modes are defined in one place (`evals/common/modes.sh`):
| mode | what it does |
|---|---|
| `base` | frozen policy, no intervention |
| `blend` | the action-chunk blend at alpha 0.0 — bit-identical to `base`, the plumbing check |
| `blend_a05` | the blend at a fixed alpha 0.5, ADVANCE=0 head, no bound. The arm [`results.md`](results.md) §5 measures — **superseded**, kept because it is still the best arm on Camera_Viewpoints |
| `blend_a015` | the same at 0.15 — intervention magnitude only |
| `blend_bounded` | fixed alpha 0.3 with the row-0 saturation bound at 1.0 |
| `blend_a015_bounded` | the same at 0.15 — the best fixed weight measured |
| `blend_scaled015` | the trained per-row, per-block weight from `g_alpha.npz`, rescaled by `SN_BLEND_SCALE` |
| `blend_full` | **the current recipe.** Learned weight at scale 0.27 off the base-frame head, plus the behavioural task lane. [`results.md`](results.md) §6 |
`evals/common/modes.sh` holds a dozen further probe arms; the table above is the ladder that leads to
the current recipe.
Every `blend*` mode needs `action_scale.json` and every learned-weight mode needs `g_alpha.npz`; both
are §4's "blend's own artifacts". Both are missing-file **errors** at launch, never silent fallbacks.
`configs/sr_ladder.yaml`'s `axis_default_mode` picks a mode per axis; both drivers read that table, so
it cannot drift between them. It is `base` everywhere today — the blend arms are run by passing
`--mode` explicitly, because at a fixed alpha the blend is net negative across the seven axes and
nothing has earned the default.
Result and log names share one grammar — the result tag and the log directory name are the same
string, so a number can always be traced back to its logs:
```
results/plus_{bench}/{axis}/{policy}_{mode}_{suite}_{axis}/ # rollout mp4s + run.json
logs/{utc_timestamp}_{policy}_{mode}_{suite}_{axis}/ # server.log + client_shard*.log
```
### StableVLA
```bash
# always look at the schedule first
$PY scripts/run_sr.py --rung R4 --gpus 2,3 --dry-run
# smoke: three small instance-filtered cells
$PY scripts/run_sr.py --rung R1 --gpus 2,3
# the full 4 suites x 7 axes grid
$PY scripts/run_sr.py --rung R4 --gpus 2,3
# a subset of a rung, keeping each cell's own episode filter / expected count / gate
$PY scripts/run_sr.py --rung R1 --cells long:Robot_Initial_States,long:Sensor_Noise --gpus 2,3
```
Other flags: `--resume` (skip cells already complete in the ledger, purge and rerun partial ones),
`--mode <mode>` (override the per-axis default), `--score` / `--ledger` (report on existing results
without launching anything), `--check-env` (GPU/load/LIBERO-Plus preflight only).
### GR00T-N1.7
```bash
$PY evals/gr00t/run_gr00t.py --suites "$SUITE" --gpu 2 --num-clients 6 --dry-run
$PY evals/gr00t/run_gr00t.py --suites "$SUITE" --gpu 2 --num-clients 6
```
One suite per GPU (`--gpu` takes a single index). Add `--axes "Sensor Noise"` to restrict axes, or
`--mode base` for a raw-GR00T baseline. It refuses to write into a result directory that already
holds rollouts unless `--resume`/`--force`: re-running a cell in place would leave both the success
and the failure mp4 of a flipped episode and inflate `n`.
### Score
```bash
# success-rate grid; --methods takes name:tag_prefix pairs
$PY evals/libero_plus/score.py --suites "$SUITE" --methods base:stablevla_base
# paired McNemar between two arms of one suite x axis
$PY evals/libero_plus/mcnemar.py --suite 10 --axis Sensor_Noise \
--tags stablevla_base_long_Sensor_Noise stablevla_other_long_Sensor_Noise
```
McNemar is only meaningful for a **deterministic** policy. StableVLA is deterministic (0 episode
churn), so its arms compare exactly, episode by episode. GR00T-N1.7 is flow-matching and stochastic
(~11% episode churn, roughly ±2.4 pp): compare its arms only **within one session**, against a
baseline measured alongside them, and read the sign rather than the digits.
---
## 6. Reproducibility
```bash
$PY -m pytest tests/ -q
$PY -m pytest tests/test_parity.py -q # must be "2 passed"
```
`tests/test_parity.py` is the golden bit-parity net: it replays 64 recorded t=0 retrievals and 64 t>0
tracker steps against the real trained artifacts and asserts every float is **exactly** equal (via
`float.hex()`, not `approx`) to `tests/golden/parity.json`. It skips cleanly when the artifacts are
absent, so a fresh clone never fails it.
To re-record it — only after you have confirmed the artifacts on disk are the ones you mean to ship:
```bash
$PY scripts/capture_golden.py
```
Every result directory also carries a `run.json` recording policy, mode, suite, axis, expected episode
count, graph dir, `graph_hash`, head mtime, git commit and the full mode env.
---
## 7. GPU etiquette on this box
GPUs 0 and 1 are in use by other work. Use `--gpus 2,3` (StableVLA) or `--gpu 2` / `--gpu 3` (GR00T).
`run_sr.py` also refuses to launch a large grid when the 1-minute load average is above its preflight
threshold; `--check-env` prints that report on its own.

Xet Storage Details

Size:
16.8 kB
·
Xet hash:
dde7ce4673ab0d3acb1a453278f29428a2fd4540f4a73b1343e9c6ded4540906

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.