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.