Skip to content

Octo: ggml inference engine (diffusion + L1/proprio), LIBERO client, and open-loop evaluation - #28

Open
DuyBaoDOCer wants to merge 34 commits into
VinRobotics:mainfrom
DuyBaoDOCer:feat/octo-openloop-eval
Open

DuyBaoDOCer wants to merge 34 commits into
VinRobotics:mainfrom
DuyBaoDOCer:feat/octo-openloop-eval

Conversation

@DuyBaoDOCer

Copy link
Copy Markdown

What

Adds a complete Octo inference engine in C++/ggml (src/models/octo.cpp) covering the full forward pipeline from image + language observation to predicted action chunk, plus an open-loop evaluation harness that measures parity against the PyTorch model.

Core C++ engine (src/models/octo.{h,cpp}):

  • SmallStem16 vision tokenizer (primary 256x256 + wrist 128x128 camera streams) as a ggml graph
  • Native T5-base text encoder with SentencePiece/unigram tokenizer (vocab baked into GGUF)
  • Language projection + repeat_task_tokens, block transformer with block-wise attention mask
  • Diffusion action head with golden noise replay for deterministic parity testing
  • L1 action head + proprio tokenizer (head_type=l1) for fine-tuned/ALOHA checkpoints
  • Resident weight-loading path: weights + embedding table loaded once; inference dispatched to model backend (CUDA when available, CPU otherwise)
  • window_size and action-buffer geometry (horizon × dim) read from GGUF metadata
  • Stats (ms_vision / ms_inference / ms_total) populated in predict()

GGUF converter (scripts/convert_octo_to_gguf.py):

  • Supports the published octo-small-1.5 diffusion checkpoint and fine-tuned PyTorch L1/proprio checkpoints (--ckpt-format pytorch --ckpt … --step …)

LIBERO client and server wiring:

  • VlaCppClient extended with wrist zero-fill fallback and data-driven unnorm_dataset key resolution
  • OctoL1Client subclass sends raw proprio via req.state (teacher-forced)
  • LIBERO libero_object unnorm + gripper-channel output

Open-loop evaluation harness (eval/client/run_open_loop_octo.py):

  • Teacher-forced runner over a real RLDS episode
  • Reports MAE / MSE / RMSE + gripper accuracy in both normalized and original action units
  • Saves GT-vs-prediction plot and NumPy trace for auditing

Test suite (tests/, tests/py/):

  • Full golden-trace parity vs OctoPt oracle (diffusion, 20/20 cases)
  • Statistical action-distribution parity (N=512 samples)
  • Preprocessing parity (rotate180 + resize 256)
  • LIBERO window=1 parity and window=2 regression
  • Stagewise CPU parity for the L1+proprio path (T0–T5)
  • Resident-path parity wired into ctest
  • Open-loop output compared against OctoPt golden trace

Bug fixes:

  • Handle flat (single-dataset) octo.dataset_statistics shape
  • Bake window_size=1 for fine-tuned PyTorch checkpoints
  • Fix SentencePiece/protobuf ABI conflict causing vla-server abort on startup
  • Derive open-loop plot title from checkpoint path (was hardcoded jitter2525)

Docs (docs/octo_open_loop_vla_cpp_en.md):

  • Open-loop evaluation report for the jitter-adapted Octo checkpoint (step 4500) on aloha_carrot_easy_rlds ep0

Why

Provides a self-contained, PyTorch-free inference path for Octo — suitable for embedded/edge deployment (vla-server over ZMQ, no Python runtime on robot). The evaluation harness confirms the C++ port is numerically indistinguishable from the original model before any deployment use. Fine-tuned L1/proprio checkpoints (ALOHA task suite) are first-class: they require a different action head and proprio tokenizer not present in the upstream diffusion checkpoint.

Verified

  • Builds clean under -Wall -Wextra (first-party code)
  • ctest passes
  • Numeric output unchanged (vla_predict_check diff), or the change is meant to move it and a LIBERO sweep is below

Open-loop parity — vla.cpp C++ engine vs OctoPt, step-4500 checkpoint, aloha_carrot_easy_rlds ep0 (149 steps, 19 inference calls):

Metric vla.cpp PyTorch |diff|
original MAE 0.008952 0.008951 4.4e-7
original MSE 0.000212 0.000212 2.0e-8
original RMSE 0.014549 0.014549 6.8e-7
gripper accuracy 99.33% 99.33% 0.0
normalized MAE 0.032435 0.032432 2.7e-6

All metrics agree to within ~1e-5. Golden-trace parity: 20/20 cases pass (diffusion).
Stagewise L1+proprio parity: all T0–T5 stages pass on CPU. Full ctest: 25/25.

Archs and backends tested:

  • x86-64 · CUDA 12.6 · RTX 4050 Laptop (sm_89)
  • x86-64 · CUDA 12.8 · A100 (sm_80, Colab)

Convert octo-small-1.5 PyTorch state_dict to F32 GGUF and load in vla.cpp.

Ref: TIP-001 (M0). Parity lane: CPU-F32.
Add primary/wrist image tokenizers in ggml and VLA_OCTO_DUMP + verify_octo_parity.py.

Ref: TIP-002 (M1). Parity lane: CPU-F32, tol<=1e-3.
Replace hand-rolled CPU arithmetic with ggml_conv_2d/group_norm/relu/mul_mat; cross-check vs golden and prior oracle; remove hand-rolled path.

Ref: TIP-002b (M1). Parity lane: CPU-F32, tol<=1e-3.
Inject saved T5 embedding, project 768->384 + pos, replicate task tokens per timestep.

Ref: TIP-003 (M2). Parity lane: CPU-F32, tol<=1e-3.
Assemble multimodal tokens, build block-wise additive mask, run 12 pre-norm
encoder blocks + final norm, split output groups.

Ref: TIP-004 (M3). Parity lane: CPU-F32, tol<=2e-3.
Score network (Fourier + cond MLP + MLPResNet) as ggml graph; DDPM 20-step

reverse with injected golden noise; unnormalize via dataset statistics.

Ref: TIP-005 (M4). Parity lane: CPU-F32, tol<=1e-3.
Export T5 encoder weights to octo.t5.*; implement encoder-only T5 (relative
position bias, T5LayerNorm, ReLU FFN, no query scaling) as ggml graph.
Ref: TIP-006 (M5). Parity lane: CPU-F32, tol<=2e-3.
T5 unigram tokenization (text -> input_ids) and vla-cli --model octo end-to-end
(image + instruction -> action).
Ref: TIP-007 (M6/M7).
Wire all tier1 + bridge_debug into ctest; verify unnormalized action against
dataset statistics; REQ coverage matrix.
Ref: TIP-008 (M8).
*.gguf was already ignored; add *.gguf.tensor_map.json so the converter's
sidecar report doesn't show up as untracked.
Free-sample N actions on the same observation each side; compare per-dim
mean/std/KS + noise ~ N(0,1) sanity.
Ref: TIP-009.
Read window_size from checkpoint config into GGUF meta; keep rail-berkeley default.

Ref: TIP-C1. Checkpoint: cyrusneary octo-small-1.5 libero step60000, window_size=1.
Thread window_size (GGUF meta) through obs/mask/transformer/diffusion; seq=16+window*337.

Ref: TIP-W1. Runs window=1 (cyrusneary) + window=2 bridge regression. Parity lane CPU-F32.
Wire LIBERO golden + wrist/bt.* exclude-list; add permanent final-action-slice unit test.
Ref: TIP-HARNESS. Parity lane CPU-F32, tol<=1e-3; per-group+final_action, exclude bt.*/obs_wrist.
Ref: TIP-P. tol chặt rotate+resize (synthetic_zero max_abs=0); JPEG soft (<=25 uint8); segment-1, libero_object.
…-fill fallback

Un-normalize dataset key (octo.cpp) is now data-driven (VLA_OCTO_UNNORM_DATASET env
var / --unnorm-dataset / single-key auto-detect / bridge_dataset fallback) instead of
hardcoded "bridge_dataset"; bridge checkpoint stays backward-compatible with no config
change. Observation-side wrist image/pad-mask now zero-fill (all-invalid) when absent,
matching scripts/patch_libero_golden_for_harness.py's placeholder exactly, so that
script is no longer required for the LIBERO golden harness. OctoModelArch::predict()
implemented (was M0 stub): builds the same window=1 cold-start observation as
octo_predict_from_images from server Inputs (primary required, wrist optional -> same
zero-fill fallback), runs the full pipeline, and returns the un-normalized action
(server-side, reusing the golden-verified unnormalize_action -- Octo's
dataset_statistics is embedded in the multi-hundred-MB checkpoint GGUF, not a small
sibling file a client can cheaply hold, unlike gr00t's --stats-json).

Ref: TIP-CLIENT. Checkpoint cyrusneary window=1, suite libero_object; bridge regression
15/15 + LIBERO parity 6/6 kept passing (22/22 total ctest) after every change in this
commit, plus a new standalone predict()-only smoke test (both windows, both camera
configs) since vla-server itself cannot start in this environment (pre-existing,
repo-wide protobuf 3.21/3.14 ABI mismatch, confirmed unrelated to Octo -- see report).
Add OctoPipelineAdapter (adapters.py): rotate180+resize256 preprocessing (TIP-P) for the
primary camera only, matching cyrusneary/octo-finetuned-libero's own single-camera
finetune exactly (its finetune_config.json image_obs_keys never included wrist);
vla-server's zero-fill+mask-invalid fallback (previous commit) reproduces the wrist slot
this checkpoint actually trained on. parse_action inverts+binarizes the gripper dim
(-1=open/+1=close for LIBERO env.step, reusing the same formula already used by
Evo1PipelineAdapter/Gr00tPipelineAdapter) -- no other un-normalization needed
client-side since octo.cpp:predict() already returns world-unit actions.

Add "octo" to ARCH_PRESETS (vla_cpp_client.py, image_size=256, tokenizer=t5-base,
max_length=16, no proprio) and a dedicated _predict_chunk_octo: real t5-base
tokenization (matching the checkpoint's own text_processor recipe exactly), sends
RGB_U8-encoded images (Octo's own C++ pipeline does the uint8->float normalization,
unlike archs that expect pre-normalized F32_RGB_01), and sends a real attention_mask
(Octo's T5 encoder needs genuine padding info, unlike archs that derive their own).
Wire "octo" into run_sim_client_direct.py's adapter dispatch.

Ref: TIP-CLIENT. Verified with a stub-ZMQ-server smoke test speaking the exact
PredictRequest/PredictResponse wire protocol vla-server uses (real vla-server itself
cannot start in this environment -- pre-existing, repo-wide protobuf ABI mismatch,
unrelated to Octo): 1 image sent (RGB_U8, 256x256), 16 lang_tokens + 16 attention_mask,
gripper correctly binarized to LIBERO's -1/+1 convention.
…-server ABI abort

SPM_PROTOBUF_PROVIDER=package removes vendored protobuf-lite 3.14 colliding with system 3.21.12.

Ref: TIP-INFRA-PROTOBUF. vla-server launches; ctest 22/22 kept, Octo tokenizer byte-exact.
…fer)

Consume a dedicated CPU residency buffer (weight_buf_cpu) in predict() instead of
re-reading GGUF per call; keep embedding table resident. CPU compute unchanged.
m->weight_buf (CUDA) left ready for TIP-B GPU wiring.
Ref: TIP-BUILD-OCTO-GPU-A. Parity bit-exact 22/22, tol unchanged; branch feat/octo-gpu.
Thread m->backend into the 5 stage graphs; consume CUDA-resident m->weight_buf;
remove TIP-A CPU-only residency buffer. Embedding gathers on backend.

Ref: TIP-BUILD-OCTO-GPU-B. GPU compute; parity drift measured (see report); branch feat/octo-gpu.
Time vision (obs tokenizer) + inference (lang/T5/transformer/diffusion) + total;
store in model stats so --timing-detail reports real numbers instead of 0.0.
Ref: TIP-BUILD-OCTO-STATS. Baseline-level (no prefill/denoise); branch feat/octo-gpu.
Register octo_parity_*_resident variants exercising the resident (m->backend)
dump path against the same golden, so ctest auto-verifies resident==golden.
Ref: TIP-ND1-A. Gate before ND-1 unify; branch feat/octo-gpu.
Drop the 5 original disk-read/cpu_init stage functions plus the original
pipeline/dump/diffusion functions; route CLI/dump/free-sample through the
resident set. Diffusion takes a noise_source param (replay/random) with
room for client in.noise later. No math change.
Ref: TIP-ND1-B. Parity 20/20 bit-exact unchanged; branch feat/octo-gpu.
…onverter

Add --ckpt-format {auto,jax,pytorch}; load PyTorch checkpoints via
OctoModelPt.load_pretrained. Read action.horizon/dim/head_type from the
checkpoint config instead of hardcoding; map the 5 L1-head groups + mean_proj
and proprio tensors (config-gated so libero JAX is unaffected).
Ref: TIP-03. jitter-adapted step2525 (L1, horizon=20); INV-3 libero JAX convert unchanged.
Prefer finetune effective_window_size over the pretrain top-level window_size
(2) so the GGUF matches how the checkpoint is deployed and how the golden ran
(window=1). Libero JAX keeps window=1 via the existing finetune_config tier.
Ref: TIP-03. Fixes GGUF octo.window_size 2->1; INV-3 unchanged.
…_type

Replace hardcoded const 28 (=4*7) with action_horizon*action_dim; relax the
horizon==4 assert and read octo.action.head_type; load L1-head + proprio tensors.

Ref: TIP-04. Additive: missing/diffusion head_type keeps the libero path; INV-1 octo_parity 22/22 unchanged.
…d_type=l1)

Add MAPHead->mean_proj->tanh*5 L1 head and a continuous proprio tokenizer,
gated on head_type=l1; diffusion/primary-only paths untouched.
Ref: TIP-05. INV-1/2/5 intact; numeric parity deferred to TIP-06.
Detect flat stats ({action,proprio,...}) vs nested and resolve correctly so
unnorm/proprio-stats work for jitter2525; nested (libero/bridge) unchanged.
Ref: TIP-05V. Found on real GGUF; octo_parity 22/22.
New ctest compares C++ T0-T5 activations vs OctoPt golden on ep0 t=0/8/16
(proprio tokens, readout, MAPHead, mean, unnorm); registered in suite.
Ref: TIP-06. head_type=l1 path (grep call-site verified); octo_parity 22/22 unchanged.
Stride exec_horizon=8 over RLDS ep0; MAE/MSE/RMSE/gripper/per-dim/normalized + plot.
Ref: TIP-07. Reuses VlaCppClient; existing runners/client untouched.
Direct npz array diffs + recomputed metrics + overlay plot vs TIP-02 local golden.
Ref: TIP-08.
Mirror octo_open_loop_two_models_en.md for the vla.cpp engine result;
vla.cpp-vs-published table + GT-vs-pred plot under docs/assets/open_loop.
Ref: TIP-09. jitter-2525 train; matches published ~1e-5.
Re-run eval with the retrained (raw-ep0) step-4500 checkpoint; numbers + plot
only, structure unchanged.
Ref: TIP-11. Matches golden ~1e-5.
… jitter2525)

Title now reflects the running checkpoint (step 4500, raw ep0); regenerated the
committed docs plot. Numbers unchanged.
Ref: TIP-12.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant