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 §1–§4 and are not reproducible on this tree.

Where to go

you want to read time
understand the idea docs/concepts.md 20 min
set it up from scratch docs/SETUP.md once
look up a term docs/GLOSSARY.md
run it docs/RUNBOOK.md
modify the code docs/technical/, in chapter order 2 h
edit the code docs/code_style.md — the cleanup rule and the numeric invariants
check the numbers 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. 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 §2. Why they are split the way they are: 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
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 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. 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 is done — clones, environments, weights, demonstrations and artifacts all in place.

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 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

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 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.

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.