File size: 7,061 Bytes
aba31a6 bdca518 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 | ---
license: mit
library_name: rtc-pi05
tags:
- robotics
- pi0
- pi05
- flow-matching
- real-time-chunking
- openpi
---
# rtc-pi05 — Real-Time Chunking for openpi pi0.5
Standalone, inference-time [**Real-Time Chunking (RTC)**](https://arxiv.org/abs/2506.07339)
for [openpi](https://github.com/Physical-Intelligence/openpi) pi0.5 flow policies.
> Black, Galliker, Levine. *Real-Time Execution of Action Chunking Flow Policies.*
> NeurIPS 2025. arXiv:2506.07339.
**You bring your own pi0.5 base checkpoint** (loaded through openpi). RTC only
replaces the inference path: while the current action chunk executes, a
background thread generates the *next* chunk by **guided inpainting** — freezing
the actions already committed to the robot and softly agreeing with the
overlapping ones — so consecutive chunks stay consistent and execution never
stalls waiting on inference.
This repo is the RTC algorithm plus a thin openpi adapter and a no-robot demo.
It contains **no robot / camera / task-specific code**.
## What's inside
```
rtc/
masking.py soft prefix masks + PiGDM guidance weight (backend-agnostic, numpy)
controller.py Algorithm 1: async execution + background inference thread
sampler.py guided-inpainting denoising loop (PyTorch, torch.autograd.grad)
pi0.py adapter for openpi PI0Pytorch pi0.5 (torch + openpi, imported lazily)
examples/
pi05_realtime.py load a pi0.5 ckpt -> run the async RTC controller (synthetic obs)
tests/
test_masking.py property tests for the soft masks
```
`import rtc` pulls in **neither torch nor openpi** — the sampler and adapter are
lazy, so the controller/masking core is usable in a plain-numpy process.
## Install
Requires Python ≥ 3.11 and a working [openpi](https://github.com/Physical-Intelligence/openpi)
install (it provides `PI0Pytorch` and the checkpoint/normalization machinery; it
is **not** on PyPI, so install it from source into the same environment).
```bash
# in the environment that already has openpi + torch:
pip install -e ".[torch,dev]"
pytest # runs the masking property tests
```
## Quickstart (no robot)
```bash
python examples/pi05_realtime.py \
--config pi05_libero \ # your openpi TrainConfig name
--checkpoint /path/to/pi05_ckpt_dir \ # dir with model.safetensors + assets/
--device cuda --steps 60 --hz 30
```
It loads the policy, samples an initial chunk, then simulates a 30 Hz control
loop while the background thread regenerates chunks RTC-style, printing the
observed inference delay `d` and confirming the real-time constraint `d ≤ H − s`
holds. Pass `--obs-npz` to feed a recorded observation instead of the synthetic
one (its keys must match your checkpoint's input transform).
## Using it in your own inference loop
```python
import torch
from openpi.training import config as _config
from openpi.policies import policy_config as _policy_config
from rtc.controller import RealTimeChunkingController
from rtc.pi0 import build_observation, sample_initial_chunk, rtc_sample_actions, decode_actions
policy = _policy_config.create_trained_policy(
_config.get_config("pi05_libero"), "/path/to/ckpt", pytorch_device="cuda")
H = int(policy._model.config.action_horizon)
# 1. seed the first chunk (model space) from the same velocity field RTC uses
obs0 = build_observation(policy, raw_obs_dict)
init_chunk, _ = sample_initial_chunk(policy, obs0, num_steps=5)
# 2. the controller calls this in a background thread
def inference_fn(obs, prev_chunk, inference_delay, execution_horizon):
new_b, _ = rtc_sample_actions(
policy, obs, prev_chunk.unsqueeze(0),
inference_delay=inference_delay, execution_horizon=execution_horizon,
num_steps=5, max_guidance_weight=5.0, prefix_attention_schedule="exp")
return new_b[0]
ctrl = RealTimeChunkingController(inference_fn, horizon=H, initial_chunk=init_chunk[0],
smin=6, initial_delay=4, delay_buffer_size=10)
ctrl.start()
# 3. every control period dt:
action = ctrl.get_action(build_observation(policy, latest_raw_obs)) # (A,) model space, returns immediately
robot_action = decode_actions(policy, action[None, None], {"state": None}) # -> robot space
```
Chunks flow in the model's **normalized** action space (width `action_dim`,
usually padded to 32). Decode to robot space with `rtc.pi0.decode_actions`, which
runs your policy's output transforms (unnormalize / delta→absolute).
## RTC hyperparameters
| flag / arg | default | paper | meaning |
|---------------------|:-------:|:-----:|------------------------------------------------------|
| `hz` | 30 | 1/Δt | control rate |
| `smin` | 6 | s_min | min execution horizon; `s = max(d, s_min)` |
| `initial_delay` | 4 | d_init| delay-buffer seed (control steps) |
| `delay_buffer` | 10 | b | recent-delay buffer size (delay estimate = its max) |
| `num_steps` | 5 | n | denoising steps |
| `beta` | 5.0 | β | guidance-weight clip (App. A.2) |
| `schedule` | exp | — | soft mask; `zeros` = naive hard-masking baseline |
**Real-time constraint:** `d ≤ s ≤ H − d`. With pi0.5's action horizon `H`, if a
guided inference takes longer than `H − s` control steps the current chunk is
exhausted and `get_action` raises. If that happens: lower `hz`, raise `smin`, or
lower `num_steps`. Watch the printed observed delay `d` — it must stay `≤ H − s`.
Compare RTC against the naive async baseline with `--schedule zeros` (hard
masking), and against synchronous inference (plain `policy.infer`).
## How the adapter works (conventions)
openpi integrates `t=1` noise → `t=0` data and its action expert regresses
`v_pi = noise − data`. RTC's core integrates the other way (`tau=0` noise →
`tau=1` data). The adapter reconciles them with
```
t = 1 − tau , v_rtc(x, tau) = − v_pi(x, t = 1 − tau)
```
Crucially, openpi's `PI0Pytorch.denoise_step` is **not** `@torch.no_grad`
(only the compiled `sample_actions` wrapper is), so RTC calls `denoise_step`
directly and autograd flows back to the noisy chunk `x` — no model surgery
needed. The VLM prefix KV cache is built once per observation under `no_grad`;
each denoising step reuses it. See `rtc/pi0.py` and `rtc/sampler.py` for details.
## Status / caveats
- The `examples/pi05_realtime.py` obs keys are libero-style defaults; adapt them
(or use `--obs-npz`) to your checkpoint's input transform.
- The paper's real-world π0.5 used a larger horizon (`s_min=25, b=10`); with a
smaller `H` the defaults here are what fit `d ≤ H − s`. Tune on your setup.
- RTC's benefit shows up under inference latency / perturbation; on clean,
fast-inference setups it is roughly neutral vs. synchronous.
|