twanghcmut's picture
|
download
raw
14.6 kB
# Graph-grounded support tracking for frozen VLA policies
A vision-language-action policy that is never retrained **drifts** off the demonstrated manifold when
the world is perturbed.
This repo tracks that drift with one mechanism: a graph built from the training demonstrations, plus
a small GNN that maps a window of joint states to a distribution over graph nodes, filtered over time
into a belief. The policy is frozen — no finetuning, no LoRA, no adapter. The same artifacts drive
StableVLA and GR00T-N1.7 without touching either.
> **Naming.** The Python package is `onf` ("Oriented Neural Field"). That name is historical.
> Nothing in the current method is a neural field. Read `onf` as the package name and nothing else.
> The one component that is still a field — `onf_head.npz`, a 136k-parameter MLP scoring
> distance-to-manifold — is called the **cleanliness field** everywhere in these docs.
```
demo HDF5 ──► g_nodes.npz ──► g_head.npz ──► g_track.npz
(raw) + g_edges.npz (the GNN) (transition kernel + basin geometry)
│ │ │
└────────┬──────────┘────────────────────┘
SENTINEL (belief filter)
```
Once per policy action chunk (8 env steps) the sentinel steps its belief over the graph and reads a
target off it: the projection of the current configuration onto the certified basin around the
tracked node, published as a `SEG_K`-step reference segment.
**The reading is applied by mixing it into the policy's own actions.** The reference segment becomes
an end-effector tracking chunk in the policy's action units, and that chunk is blended into the
`[8, 7]` chunk the policy just produced, at exactly one point — the refill line in
`evals/libero_plus/harness/model2libero_interface.py`. There is no second controller: the policy
keeps executing its own chunk. The gripper column is never blended. Modes in
`evals/common/modes.sh`: `base`, `blend` (alpha 0, the plumbing check), the fixed-alpha probes
(`blend_a05`, `blend_a015_bounded`, `blend_bounded`, …) and the learned-weight arms
(`blend_scaled015`, `blend_full`). `blend_full` is the current recipe.
The joint-space PD servo, the WHEN e-process fire rule and the conformal monitors that preceded this
were **deleted in commit `e942260`** — they existed only to support a second controller. Their
results are kept as history in [`docs/results.md`](docs/results.md) §1–§4 and are not reproducible
on this tree.
## Where to go
| you want to | read | time |
|---|---|---|
| understand the idea | [`docs/concepts.md`](docs/concepts.md) | 20 min |
| set it up from scratch | [`docs/SETUP.md`](docs/SETUP.md) | once |
| look up a term | [`docs/GLOSSARY.md`](docs/GLOSSARY.md) | — |
| run it | [`docs/RUNBOOK.md`](docs/RUNBOOK.md) | — |
| modify the code | [`docs/technical/`](docs/technical/), in chapter order | 2 h |
| edit the code | [`docs/code_style.md`](docs/code_style.md) — the cleanup rule and the numeric invariants | — |
| check the numbers | [`docs/results.md`](docs/results.md) — every measured cell; the table below is the headline excerpt | — |
## Results
Full tables, per-axis breakdown, run-to-run spread and McNemar tests:
[`docs/results.md`](docs/results.md). The current mechanism in full, and two rows of the deleted one:
**Current — action-chunk blend, full stack (`blend_full`), StableVLA, `long`, 2026-08-12**
(`results.md` §6). Learned per-row per-block weight rescaled by `SN_BLEND_SCALE=0.27`, row-0
saturation bound, behavioural task lane, base-frame tracking chunk. Paired McNemar against the
`lp_libero_10_*` base on every cell:
| axis | n | base | `blend_full` | Δ | p |
|---|---|---|---|---|---|
| Robot_Initial_States | 393 | 170 = 43.3% | 246 = 62.6% | **+19.3** | 4.3e−15 |
| Camera_Viewpoints | 419 | 230 = 54.9% | 278 = 66.3% | **+11.5** | 9.7e−07 |
| Objects_Layout | 312 | 247 = 79.2% | 275 = 88.1% | **+9.0** | 2.3e−04 |
| Sensor_Noise | 449 | 336 = 74.8% | 375 = 83.5% | **+8.7** | 2.6e−06 |
| Language_Instructions | 382 | 299 = 78.3% | 304 = 79.6% | +1.3 | 0.72 ns |
| Background_Textures | 289 | 243 = 84.1% | 245 = 84.8% | +0.7 | 0.90 ns |
| Light_Conditions | 273 | 243 = 89.0% | 244 = 89.4% | +0.4 | 1.00 ns |
| **total** | **2517** | **1768 = 70.2%** | **1967 = 78.1%** | **+7.9** | |
The same mechanism measured **−12.6 pp** on the same seven axes twelve hours earlier, at a fixed
alpha with an unbounded row 0, a task lane that resolved to nothing on the axis it was for, and a
world-frame chunk (`results.md` §5, **superseded**). Four fixes, each measured separately, are the
whole difference; `results.md` §6a names them.
**Historical — the deleted entry/sentinel arms, 2026-08-06/07** (`results.md` §1–§4), kept as the
record of what the graph reading was worth under a joint-space controller:
| cell | StableVLA | GR00T-N1.7 |
|---|---|---|
| t=0 `entry`, `long` × Robot_Initial_States (n=393) | 378/393 = 96.2% | 359/393 = 91.3% |
| t>0 `sentinel`, `long` × Objects_Layout (n=312) | 249/312 = 79.8% | 198/312 = 63.5% |
Read with these caveats. They are not boilerplate; each one changes how the tables should be
interpreted.
- **Four axes gain and three do not move.** Language_Instructions, Background_Textures and
Light_Conditions are all non-significant. The +7.9 pp total is carried entirely by the four
significant cells; rounding the other three up would misattribute it.
- **On Robot_Initial_States the blend is still 33.6 pp behind what it replaced.** The deleted `entry`
recipe scores 378/393 = 96.2% on that cell against `blend_full`'s 62.6% — paired, 141 episodes
broken against 9 fixed, p = 1.2e−31. A t=0 joint-space prefix is still the best answer to a t=0
joint-space perturbation, and this tree cannot run one.
- **Language_Instructions is not a win, and no Language arm is.** Ten arms have been run on that
axis and none differs significantly from base. The best is `blend_scaled015` at 311/382 = 81.4%
(p = 0.21); against `blend_full` it is −1.9 pp, CI [−5.6, +2.1], p = 0.42. Any mechanism story
about the difference between those two recipes is unsupported.
- **No single arm is best everywhere.** The superseded fixed-alpha `blend_a05` still leads on
Camera_Viewpoints (293/419 = 69.9% vs 278). Oracle unions over arms that already exist reach 98.7%
on Robot_Initial_States and 88.0% on Language — the headroom is in a per-episode selector, not in
another weight (`results.md` §6h).
- **The blend's basin gate never fired** (`no_op` on 0% of checks) in the only sweep that measured
it, so it ran on all 42–57 checks an episode, where the servo it replaced was capped at 2
interventions.
- **Only `long`, only StableVLA.** No blend arm has been run on `object`, `spatial` or `goal`, and
none on GR00T-N1.7.
- **No same-session `base` arm was run for any GR00T cell.** There is no baseline to compare against
in the same session, and cross-session GR00T comparison is invalid (next bullet).
- **GR00T-N1.7 is flow-matching and unseeded.** Measured run-to-run spread on the sentinel cell is
62.8–66.7% over seven runs of the identical config (`results.md` §2c). Any GR00T delta smaller than
~4 pp is noise. StableVLA is deterministic (0 episode churn on the entry cell across 393 episodes),
so its arms compare episode-by-episode.
- **On GR00T at Robot_Initial_States, a fixed-home baseline (94.7%) beats this method (91.3%).** The
graph-derived entry target wins on StableVLA and loses on GR00T.
## Install
Three environments; they cannot be merged (conflicting pins). Install commands and the constraints
this repo adds on top of each upstream README: [`docs/SETUP.md`](docs/SETUP.md) §2. Why they are
split the way they are: [`envs/README.md`](envs/README.md).
| runtime | env file | used for |
|---|---|---|
| `onf-core` | `envs/onf-core.yml` | the `onf` package: graph build/train, artifacts, tests. CPU is enough. |
| `stablevla` | `envs/stablevla.yml` | StableVLA policy server **and** the LIBERO-Plus sim client |
| gr00t | Isaac-GR00T's own `.venv`, path in `$GR00T_PY` | GR00T-N1.7 policy server |
```bash
conda env create -f envs/onf-core.yml && conda activate onf-core
pip install -e .
export ONF_DATA=... ONF_OUTPUTS=... ONF_RESULTS=...
```
`onf.config.Paths` resolves every path from those three variables. Nothing is hardcoded.
**Checkpoints, demonstrations and trained artifacts do not ship with this repo** — the first two are
downloads (~10 GB a suite, ~17 GB with the GR00T arm), the third is trained from the second.
[`docs/SETUP.md`](docs/SETUP.md) is six steps of copy-paste: what to clone, what to download, which
directory each piece lands in, and the three places StableVLA's and LIBERO's own instructions have to
be overridden.
On the author's box `data/`, `outputs/`, `results/` and `external/` are symlinks into a shared
working tree.
## Hardware and wall-clock
| step | hardware | time (`long`, 500 demos) |
|---|---|---|
| cleanliness field | CPU | ~10 s |
| graph build | CPU | minutes |
| head training | 1 GPU | ~1 h |
| tracker fit | 1 GPU | ~30 min |
| smoke benchmark (`--rung R1`, 35 episodes) | 2 GPUs | ~15 min |
| full suite × axis benchmark | 2 GPUs | ~1 h per 400-episode cell |
Estimates, not a benchmark. The build figures come from the timed end-to-end run recorded in
[`docs/RUNBOOK.md`](docs/RUNBOOK.md). The per-cell figure is one measured `long` ×
Robot_Initial_States run on a single GPU (393 episodes, 8 clients, ~60 min).
`OMP_NUM_THREADS=4` is required for every command. Without it the training loop oversubscribes
threads and runs an order of magnitude slower.
## Quickstart
Assumes [`docs/SETUP.md`](docs/SETUP.md) is done — clones, environments, weights, demonstrations and
artifacts all in place.
```bash
export PY=python
export OMP_NUM_THREADS=4 PYTHONPATH=src
export SUITE=long # object | spatial | goal | long
export GPU=0 # adjust to a free device
# are this suite's artifacts built?
$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')})"
# benchmark. Dry-run first.
$PY scripts/run_sr.py --check-env
$PY scripts/run_sr.py --rung R1 --gpus $GPU --dry-run
$PY scripts/run_sr.py --rung R1 --gpus $GPU
# score. The tag prefix names the policy: run_sr.py writes stablevla_*, run_gr00t.py writes gr00t_*.
$PY evals/libero_plus/score.py --suites $SUITE --methods base:stablevla_base
```
[`docs/RUNBOOK.md`](docs/RUNBOOK.md) is the same list with outputs, timings and failure modes.
## Repo layout
```
src/onf/
device.py the single torch.device resolver
config.py Paths + GraphConfig / SentinelConfig, all env-driven
fk.py forward kinematics helpers
graph/
core/ the graph itself. Imports nothing else in onf.graph. Mostly numpy-only.
schema.py every constant and the on-disk artifact layout
nodes.py NodeTable: raw frames -> nodes
edges.py EdgeSet: the 12 relations
binning.py phase -> bin index; the one bucketing everything agrees on
geometry.py distances, graph_hash, cleanliness
corpus.py graph measurement (no torch)
net/ pure nn.Module. Takes tensors, knows no paths, knows no |V|.
run/ executes inside a rollout. Imports neither train/ nor build/.
retrieve.py memoryless inference
track.py belief-filter inference
kernel.py params.py readout.py
train/ build-time: loop.py + data / loss / metrics / types / drift
build/ build-time artifact producers, top of the dependency graph
cli.py python -m onf.graph build | train
sentinel/ the runtime observer: per-check belief step + basin readout
blend/ the correction: reference segment -> action-unit chunk -> mix into the policy's
ee_track.py (SO(3), ActionScale, EeTrack, blend), plan.py, alpha.py (the learned
weight), target.py + kinematics.py (build-time only)
field/ the cleanliness field: build.py -> train.py -> field.py
evals/
common/modes.sh the ONLY definition of the run modes
libero_plus/ vendored sim client, scorer, McNemar
gr00t/run_gr00t.py GR00T-N1.7 benchmark driver
stablevla/ StableVLA policy server
scripts/
run_sr.py StableVLA benchmark driver (ladder, scheduling, ledger)
build_sentinel_artifacts.py fits g_track.npz
fit_action_scale.py fits action_scale.json — metres/radians per action unit
cache_policy_actions.py caches the frozen policy's own chunks (a_pi_raw.npz) for alpha training
trace_query.py walks ONE query through the whole pipeline, printing shapes
capture_golden.py regenerates the bit-parity golden file
```
The five `graph/` layers import downwards only: `core ← net ← run, train`, with `build` on top.
`tests/test_layering.py` asserts that and the absence of import cycles. `tests/test_imports.py`
asserts the numpy-only modules stay numpy-only.
## Reproducibility
```bash
OMP_NUM_THREADS=4 PYTHONPATH=src pytest tests/ -q
```
Three tests are contracts, not coverage:
- `tests/test_parity.py` — replays 64 recorded t=0 retrievals and 64 t>0 tracker steps against the
real trained artifacts and asserts exact float equality (`float.hex()`, not `approx`) against
`tests/golden/parity.json`. The golden file was captured with **no cleanliness field loaded**, so
the parity net covers the graph head and the belief filter only, not the field path.
- `tests/test_layering.py` — the import rules above.
- `tests/test_imports.py` — which modules must stay torch-free.
Every artifact carries a `graph_hash` stamp and is refused on mismatch. Every result directory carries
a `run.json` tying its number to a git commit and a specific artifact set. See
[`docs/technical/05-artifacts.md`](docs/technical/05-artifacts.md) for why the hash check is the most
important check in the subsystem.
## License and provenance
*TODO: no LICENSE file exists. Two vendored trees ship without an upstream license
(`UPSTREAM.md` flags them). Resolve before any distribution.*
Third-party code, commits and licenses: [`UPSTREAM.md`](UPSTREAM.md).

Xet Storage Details

Size:
14.6 kB
·
Xet hash:
7ca8b9924f3422c75c94302b59f2a025c545ade20ba6b20ee2ebcacb2615ffb4

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