SPIRAL on rlt12 stage 3: stage-1 (round 1) beta sweep 5 / 15 / 70 / 100 on π₁'s own data (57 teleop + 44 DAgger RLT_1, RM1 fix12), 1,500 steps
SPIRAL round 1 as Algorithm 1 stage 2b defines it (π₁'s own fine-tuning data, labelled by RM1),
repeated at four values of beta_bc, one run per beta, everything else fixed. A residual actor and a
5-critic ensemble on top of a FROZEN π₀.₅ + RLT policy; actor and critics both start fresh.
|
repo / step |
| base policy (required, not included) |
Nadeem-Shoukath115113/pi05-so101-rlt12-lehome-s3-daggerrlt1-15fps-b32-10k, step 9999 (vla_params + rlt_params) |
| this repo |
beta5/, beta15/, beta70/, beta100/, each holding step_00001499 (final); action_cholesky.npy (shared) |
| related |
r1b (same data, beta 30, 5,000 steps, MC 0.5, target smoothing on): Nadeem-Shoukath115113/spiral-so101-rlt12-lehome-r1b-teleop57-daggerrlt1-rm1fix12-b30-mc05-bs32-5k |
Not robot-evaluated; the offline critic probes have not been run.
The four checkpoints
All four are identical except beta_bc (checked in each rl_config.json).
| folder |
beta_bc |
W&B (spiral-rlt12-stage1-beta) |
residual size at step 100 → 300 → 1400 |
residual × beta at 1400 |
Q(actor) − Q(base) at 1400 |
beta5/step_00001499 |
5 |
up4f3x0r |
0.0213 → 0.0086 → 0.0069 |
0.034 |
0.00048 |
beta15/step_00001499 |
15 |
ndwoxn6x |
0.0113 → 0.0038 → 0.0024 |
0.036 |
0.00015 |
beta70/step_00001499 |
70 |
vx76htg2 |
0.0074 → 0.0021 → 0.00088 |
0.062 |
0.00003 |
beta100/step_00001499 |
100 |
wjsxqfh2 |
0.0070 → 0.0020 → 0.00077 |
0.077 |
0.00002 |
Caveat for beta 70 and 100: with a fresh actor the residual starts large (random init) and has
to shrink to the value set by the critic. At beta 5 and 15 it settles (residual × beta ≈ 0.035, the
critic's slope on this data), but at beta 70 and 100 it was still falling at step 1,499: the
critic-set value would be about 0.0005 and 0.00035. Part of their residual is therefore left over
from initialisation, not the critic's direction. Both are still tiny corrections.
The critics of the four runs are the same to 3 decimals (at step 1400: critic loss 0.0017,
Q min / mean / max ≈ 0.48 / 0.851 / 0.979, best-of-4 selection gain 0.0005).
Data
local/r1b_teleop57_daggerrlt1_rm1fix12: 101 episodes / 37,813 frames, 15 fps; the same data r1b
used and π₁ (the base) was fine-tuned on:
| episodes |
source |
driven by |
| 0–56 |
57 teleop demos (so101_tshirt_clean57) |
human, all successes |
| 57–100 |
dagger_session_RLT_1 (44 eps, 85 takeovers, 4 with no takeover) |
stage-2 policy + human takeovers, all end in success |
Same episodes and states as π₁'s training set local/teleop57_daggerrlt1_v21 (checked frame by
frame), minus the last frame of each episode, which the labeller drops.
Labels: RM1 (sarm2_gripper58), fix12:
opensarm/tools/label_for_spiral.py --stride 15 --smoother-mode none --write-progress --task "Fold the
long sleeved T-shirt on the table" --rm-instruction "Fold the T-shirt on the table".
Final progress 0.90–0.97 in every episode; MC target 0.871 ± 0.056; 4.2% of per-step rewards are
negative. There are no failed endings in this data, so the critic's target is nearly constant.
Training
|
|
| code |
branch lehome-noise-norm @ f79f191, plus uncommitted additions: preset residual_rl_pi05_so101_rlt12_r1b_rm1fix12; logged metric actor/q_gain_select_best_of_k |
| init |
actor fresh, 5 critics + targets fresh |
| features |
RLT z_rl (2048) + state (12) |
| action dim / horizon |
12 (all deltas, grippers included) / 20 |
| beta (BC regulariser) |
5 / 15 / 70 / 100, one separate run each |
| critic target |
0.45 MC + 0.55 TD, gamma 0.9995 |
| MC return |
progress[last frame] × 0.9995^steps_remaining |
| target smoothing |
off (sigma 0) |
| kappa / critics / tau / n_step |
4 / 5 / 0.005 / 3 |
| actor / critic |
3×1024 / 3×1024, residual output scale 0.1 |
| lr actor / critic |
1e-4 / 1e-4, AdamW weight decay 1e-4 |
| batch / steps / seed |
32 / 1,500 / 42 (≈ 1.27 passes over the data) |
| correlated noise |
ON: action_cholesky.npy (pre-shrunk, 240×240, identical to rounds 1–4) |
| W&B |
spiral-rlt12-stage1-beta: up4f3x0r (5), ndwoxn6x (15), vx76htg2 (70), wjsxqfh2 (100) |
Training numbers, beta 15 run (from the log)
| step |
critic loss |
Q min / mean / max |
MC / TD target mean |
Q(actor) − Q(base) |
best-of-4 selection gain |
residual size |
| 100 |
0.155 |
−0.23 / 0.19 / 0.66 |
0.871 / −0.362 |
0.0009 |
0.0014 |
0.0113 |
| 300 |
0.0020 |
0.44 / 0.58 / 0.74 |
0.871 / 0.342 |
0.0002 |
0.0006 |
0.0038 |
| 1000 |
0.0014 |
0.46 / 0.82 / 0.96 |
0.872 / 0.786 |
0.0001 |
0.0005 |
0.0024 |
| 1400 |
0.0017 |
0.48 / 0.85 / 0.98 |
0.872 / 0.834 |
0.0002 |
0.0005 |
0.0024 |
Suggested robot order (the openspiral author's procedure): base (dry_run=True) → beta 100 → 70 →
(r1b, beta 30) → 15 → 5; stop once a beta is clearly worse than the base.
Serving
Identical to rounds 1b, 2 and 4; only the checkpoint path changes.
1. Get everything
hf download Nadeem-Shoukath115113/pi05-so101-rlt12-lehome-s3-daggerrlt1-15fps-b32-10k --local-dir ckpt/base
hf download Nadeem-Shoukath115113/spiral-so101-rlt12-stage1-beta-teleop57-dagger44-rm1fix12-fresh-1500 --local-dir ckpt/spiral_s1beta
You need code from branch lehome-noise-norm (commit f79f191 or later): the lehome normalisation
and the 12-dim model. A code-only copy is in the HF bucket Nadeem-Shoukath115113/test, folder
spiral_rlt12_serve/ (hf sync hf://buckets/Nadeem-Shoukath115113/test/spiral_rlt12_serve ./spiral_rlt12_serve).
Run from the code root. The loader falls back to
assets/rlt12_s3_dagger_rlt1/local/teleop57_daggerrlt1_v21/norm_stats.json relative to that root.
That file is included in the bucket copy and is byte-identical to the base checkpoint's
9999/assets/local/teleop57_daggerrlt1_v21/norm_stats.json.
2. Load
scripts/serve_policy.py has no entry point for this policy. Build it with the loader and wrap it
in the websocket server:
import os
from openpi.policies.residual_rl_rlt_policy import load_residual_rl_rlt_policy
A = os.path.abspath # ALL paths must be absolute -- orbax rejects relative ones
policy = load_residual_rl_rlt_policy(
pi0_config_name="residual_rl_pi05_so101_rlt12_demos57", # same model/transforms; see note
rl_checkpoint_dir=A("ckpt/spiral_s1beta/beta15/step_00001499"), # or beta5 / beta70 / beta100
rlt_checkpoint_dir=A("ckpt/base"), # latest step inside must be 9999
pi0_checkpoint=A("ckpt/base/9999/vla_params"),
best_of_k=1, # or 4, see below
noise_cholesky_path=A("ckpt/spiral_s1beta/action_cholesky.npy"),
dry_run=False, # True = base actions only (no residual)
)
On the config name: the round-1 preset residual_rl_pi05_so101_rlt12_demos57 is in the committed
code, and its model, normalisation and assets are identical to this run's preset. The two differ
only in the training repo_id, which serving does not use. With the uncommitted preset available,
residual_rl_pi05_so101_rlt12_r1b_rm1fix12 works too.
3. Choose K and the noise
| setting |
what it does |
best_of_k=1 + Cholesky |
one sample, no critic: what the official openspiral repo serves. Needs the K=1 fix below. |
best_of_k=4 + Cholesky |
best-of-4 ranked by the critic, as described in the paper. Works as committed. Expect almost no effect: in rounds 2–4 the critic's scores for the 4 candidates differed by ~1% of its spread across states (these checkpoints: best-of-4 selection gain 0.0005 in training, against a Q spread of about 0.5 across states). |
noise_cholesky_path=None |
plain N(0, I) noise. Mismatched for this checkpoint; use only as an ablation. |
dry_run=True |
outputs base actions only and logs the residual it would have added. Use with K=1 for a clean "no residual" baseline. At K>1 the critic still picks the candidate. |
K=1 fix. At commit f79f191, _FusedRLTInferenceModule.infer_actions
(src/openpi/policies/residual_rl_rlt_policy.py, about line 122) draws plain noise and ignores the
Cholesky. Only the best-of-K path uses eps = L @ z. For K=1:
- give infer_actions a noise_cholesky=None argument and draw
eps = einsum("ij,...j->...i", L, z).reshape(B, 20, 12) when it is set;
- pass noise_cholesky= at warmup and in infer() for both K paths.
The same eps must go to pi0_model.sample_actions(..., noise=eps) and, flattened to 240, to the
actor.
4. Observation sent by the robot
The input transforms skip the repack step, so send the post-repack keys:
images.right_front <- overhead camera (observation.images.left_top)
images.left_wrist <- observation.images.left_left_wrist
images.right_wrist <- observation.images.right_right_wrist
state <- 12 joint values, robot units
prompt <- "Fold the long sleeved T-shirt on the table" (exactly)
5. What must match training (it does, if you use the loader above)
- Actions: all 12 dims are deltas from the state, then z-scored per chunk step with
per_timestamp_mean/std (20 × 12). The residual is added in that normalised delta space, then:
Unnormalize (per step) → AbsoluteActions → BiSO101Outputs. A plain global mean/std normaliser
gives wrong actions with no error.
- State: min/max mapped to [-1, 1]. π₀.₅ and the actor see the same normalised state.
- z_rl: RLT encoder 2048 → 2048, 768 image tokens only, 2 layers, 8 heads, mlp ratio 4.0.
π₀.₅ runs in bfloat16.
- Noise:
eps = L @ z, z ~ N(0, I_240), L = this repo's action_cholesky.npy. Never use the
raw Cholesky stored in norm_stats.json. π₀.₅ uses 10 flow steps.
- Residual scale: the actor already multiplies its output by 0.1. Add it as is.