roostField / Lab
Curriculum
Phase 05Lesson 3 of 5
80 min
Engineer the real-time loopWeeks 15–18

Real-time chunking: asynchronous execution without discontinuity

Asynchronous execution hides inference behind motion — and creates a seam where two plans disagree, mid-trajectory, at full torque. This lesson quantifies the seam, retires the averaging band-aid, and builds real-time chunking: freeze what you have committed, inpaint the rest.

After this lesson you can
  • Quantify the freeze fraction of synchronous execution and the seam discontinuity of naive asynchronous switching using measured π₀ serving latencies.
  • Show with arithmetic why temporal ensembling cannot rescue a 110 ms VLA at 50 Hz, and what it degenerates into when you try.
  • Derive the frozen-prefix inpainting step for a flow-matching policy and state exactly which actions get frozen and why.
  • Implement an executor/inference-thread architecture with a commit horizon and name the runtime knobs a latency-aware scheduler can adapt.

You have spent a career hiding latency behind work: prefetch, double-buffering, pipelining, speculative decoding. The previous lesson measured what delay and jitter cost a control loop; the obvious cure is the oldest trick in systems — generate the next action chunk while the arm executes the current one. This lesson is about the side effect. Overlap creates a seam: a moment mid-motion where the executor switches between plans that were computed from different observations, different noise draws, sometimes different strategies entirely — and do not agree. In an LLM, a seam is a token nobody notices. On a WidowX AI at 50 Hz, it is a commanded velocity step the joint actuators execute with a clack you can hear across the room.

Conceptual Foundation: State, Seam, and Conditional Generation

Before deriving the mechanics of real-time chunking, we must rigorously define the physical and probabilistic objects involved. The core tension in asynchronous execution is between the physical state of the robot, which evolves continuously and irreversibly, and the planned trajectory, which is a discrete sequence of commands generated by a stochastic model. A discontinuity arises when these two representations diverge at the moment of handover. To quantify this, we first define the kinematic variables. Let qRnq \in \mathbb{R}^n denote the vector of joint positions, q˙\dot{q} the joint velocities, and q¨\ddot{q} the joint accelerations. The jerk, q...\dddot{q}, is the time derivative of acceleration. In control engineering, smoothness is often evaluated via the Root Mean Square (RMS) jerk, defined as Jrms=1Nt=1Nq...t2J_{\text{rms}} = \sqrt{\frac{1}{N}\sum_{t=1}^N \dddot{q}_t^2}, where NN is the number of control steps. High jerk indicates rapid changes in force, which can excite mechanical resonances and cause audible noise.

The seam is the discontinuity in the commanded action sequence at the splice point. Let atolda^{old}_t be the action from the previous chunk and atnewa^{new}_t be the action from the new chunk at the same control step tt. The seam error is defined as ϵseam=atnewatold2\epsilon_{seam} = \| a^{new}_t - a^{old}_t \|_2. It is crucial to distinguish this from tracking error, which is the difference between the commanded action and the actual robot state. A seam is a planning artifact; it exists even if the robot tracks perfectly. If the seam error is δ\delta and the control period is Δt=1/fc\Delta t = 1/f_c, the resulting velocity spike is q˙spike=δ/Δt\dot{q}_{spike} = \delta / \Delta t. For a 7-DoF arm at fc=50f_c = 50 Hz, a 1-degree seam error produces a 50 deg/s velocity step, which is often perceptible as a jerk.

ϵseam=atnewatold2,q˙spike=ϵseam1/fc\epsilon_{seam} = \| a^{new}_t - a^{old}_t \|_2, \quad \dot{q}_{spike} = \frac{\epsilon_{seam}}{1/f_c}
Definition of seam error and the induced velocity spike. Note that ϵseam\epsilon_{seam} is a norm in action space, not state space.

Real-time chunking (RTC) resolves the seam by treating the problem as a conditional generation task. When a new chunk is requested, the first dd actions are already committed to execution. We define the frozen prefix AfrozenRd×nA_{frozen} \in \mathbb{R}^{d \times n} as the sequence of actions that will be executed before the new chunk arrives. The goal is no longer to generate an arbitrary chunk AA, but to sample from the conditional distribution p(Afreeonew,Afrozen)p(A_{free} \mid o_{new}, A_{frozen}), where onewo_{new} is the fresh observation and AfreeA_{free} is the remaining HdH-d actions. This is mathematically equivalent to inpainting: we fix the values of the first dd rows of the action matrix and allow the model to generate the rest, conditioned on those fixed values. The 'hard mask' ensures that the generated plan is consistent with the robot's actual trajectory up to the splice point.

StrategyMechanismEffect on SeamEffect on ReactivityComputational Cost
Naive SwitchReplace old chunk with new chunkDiscontinuous (step change)High (immediate)Low
Temporal EnsemblingWeighted average of old and new chunksSmoothed (low-pass filter)Low (dilutes correction)Low
RTC InpaintingCondition new chunk on frozen prefixContinuous (by construction)High (preserves correction)High (iterative sampling)
Comparison of Seam Mitigation Strategies

Worked Example: Quantifying the Seam and the RTC Fix

Consider a 7-DoF arm operating at fc=50f_c = 50 Hz. The inference latency is Tinf=110T_{inf} = 110 ms, which corresponds to d=50×0.110=6d = \lceil 50 \times 0.110 \rceil = 6 control steps. At t=0t=0, the arm is at q0=[0,0,0,0,0,0,0]q_0 = [0, 0, 0, 0, 0, 0, 0]. The old plan commands a linear move to qgoal=[10,0,0,0,0,0,0]q_{goal} = [10, 0, 0, 0, 0, 0, 0] over 50 steps. The action at step tt is atold=[0.2,0,0,0,0,0,0]a^{old}_t = [0.2, 0, 0, 0, 0, 0, 0] (since 10/50=0.210/50 = 0.2 degrees/step). At t=110t=110 ms (step 6), the arm has actually moved to qactual[1.2,0,0,0,0,0,0]q_{actual} \approx [1.2, 0, 0, 0, 0, 0, 0] due to tracking lag. A new observation reveals an obstacle, so the new plan must detour. The new plan commands a6new=[1.0,0.5,0,0,0,0,0]a^{new}_6 = [1.0, 0.5, 0, 0, 0, 0, 0] to initiate the detour.

ϵseam=[1.0,0.5,0,0,0,0,0][0.2,0,0,0,0,0,0]2=0.82+0.520.94\epsilon_{seam} = \| [1.0, 0.5, 0, 0, 0, 0, 0] - [0.2, 0, 0, 0, 0, 0, 0] \|_2 = \sqrt{0.8^2 + 0.5^2} \approx 0.94^\circ
Calculation of the seam error for the naive switch. The 0.94-degree discontinuity is significant for a 50 Hz control loop.

The velocity spike induced by this seam is q˙spike=0.94/0.02=47\dot{q}_{spike} = 0.94 / 0.02 = 47 deg/s. For comparison, a smooth reach might have a peak velocity of 20 deg/s. This spike is a 2.35x increase in commanded velocity, which will cause a current spike in the actuators. Now, apply RTC. The first 6 actions are frozen to the old plan's values (or the actual executed values). The new plan is generated conditioned on Afrozen=[a0old,,a5old]A_{frozen} = [a^{old}_0, \dots, a^{old}_5]. The action at step 6 is no longer a free variable; it is constrained to be consistent with the trajectory up to step 5. The model generates a smooth transition from a5olda^{old}_5 to the detour. Suppose the generated action at step 6 is a6rtc=[0.3,0.1,0,0,0,0,0]a^{rtc}_6 = [0.3, 0.1, 0, 0, 0, 0, 0]. The seam error is now ϵseam=[0.3,0.1,0,0,0,0,0][0.2,0,0,0,0,0,0]2=0.12+0.120.14\epsilon_{seam} = \| [0.3, 0.1, 0, 0, 0, 0, 0] - [0.2, 0, 0, 0, 0, 0, 0] \|_2 = \sqrt{0.1^2 + 0.1^2} \approx 0.14^\circ. The velocity spike is reduced to 0.14/0.02=70.14 / 0.02 = 7 deg/s, a 85% reduction.

Checkpoint 01

In the worked example, why does the RTC inpainting reduce the seam error from 0.94 degrees to 0.14 degrees?

The synchronous baseline: paying for inference with stillness

Start from the loop you built in the policy-serving lesson: π₀ on your RTX workstation returns chunks of H=50H = 50 actions consumed at fc=50f_c = 50 Hz — one second of motion per chunk — with request-to-chunk-in-hand latency of 110 ms at p50 and 260 ms at p99. The synchronous schedule is the one every openpi example ships with: execute the chunk, stop, capture a fresh observation, infer, resume. The arm is motionless for every inference call, and the frozen fraction of wall-clock time falls straight out of the cadence:

φ  =  TinferH/fc+Tinfer  =  0.1101.0+0.110    9.9%(p50),0.2601.260    20.6%(p99)\varphi \;=\; \frac{T_{\text{infer}}}{H/f_c + T_{\text{infer}}} \;=\; \frac{0.110}{1.0 + 0.110} \;\approx\; 9.9\% \quad\text{(p50)}, \qquad \frac{0.260}{1.260} \;\approx\; 20.6\% \quad\text{(p99)}
Freeze fraction of the synchronous schedule. Every second of motion buys a 110 ms statue impression — 260 ms on the p99 ticks that arrive, per the previous lesson, like clockwork.

Ten percent downtime sounds tolerable until you notice the schedule punishes the thing you want more of. The standard reactivity fix is to execute only part of each chunk — say the first 25 actions. Redo the arithmetic: φ=0.110/0.61018%\varphi = 0.110/0.610 \approx 18\% at p50, 0.260/0.76034%0.260/0.760 \approx 34\% at p99. Reactivity and uptime fight each other, and jitter makes the fight worse. Nor is the freeze neutral: each boundary is a stop-start transient injecting exactly the jerk Lesson 5 will measure, and freezing is not even well-defined mid-contact — a gripper holding a mug, a peg half-inserted. Meanwhile the last action of a full chunk runs on an observation Tinfer+49/fc1.09T_{\text{infer}} + 49/f_c \approx 1.09 s old: 27 cm of world motion at Phase 01's 0.25 m/s reach speed. The synchronous baseline is not wrong — it is the honest control condition for your capstone. But it pays for consistency in stillness, the one currency a dynamic task refuses.

Going asynchronous: the seam, and the averaging answer

You already derived the asynchronous fire rule: request the next chunk when the buffer holds kfire=fcTe2ep99=13k_{\text{fire}} = \lceil f_c \, T^{p99}_{\text{e2e}} \rceil = 13 actions, so it lands just before the old one exhausts. The arm never stops — but the chunk now arrives mid-motion. Alignment first: openpi's chunk index 0 corresponds to the observation instant, and by arrival d=fcTe2ed = \lceil f_c \, T_{\text{e2e}} \rceil control steps have elapsed — 6 at p50, 13 at p99. Replaying from index 0 literally rewinds time, commanding the arm back toward where it was 260 ms ago; the correct naive splice skips to index dd. Yet index alignment is nothing like sufficient, because the two chunks are answers to different questions. Three sources of disagreement stack up at the splice:

  • Correction — the new chunk saw a fresh observation and fixes the accumulated tracking error the old open-loop chunk never knew about: a genuinely useful jump, delivered as a step.
  • Multimodality — the flow-matching lesson showed both mug grasps survive training; a fresh noise draw can flow to the other mode, so the old chunk descends toward the handle while the new one has chosen the rim.
  • Sampling noise — even in a unimodal region, chunks generated from different ϵ\epsilon draws differ by the policy's own output variance.
δ  =  adnewasoldq˙spike  =  δ1/fc  =  220ms  =  100/s,q¨spike    100/s20ms  =  5000/s2\delta \;=\; \big\| a^{\text{new}}_{d} - a^{\text{old}}_{s} \big\| \qquad\Rightarrow\qquad \dot q_{\text{spike}} \;=\; \frac{\delta}{1/f_c} \;=\; \frac{2^{\circ}}{20\,\text{ms}} \;=\; 100\,^{\circ}/\text{s}, \qquad \ddot q_{\text{spike}} \;\approx\; \frac{100\,^{\circ}/\text{s}}{20\,\text{ms}} \;=\; 5000\,^{\circ}/\text{s}^2
Both actions target the same wall-clock instant, yet a modest 2-degree disagreement on one joint, executed across a single 20 ms control period, commands a velocity spike several times a smooth reach's tens of degrees per second.

On the arm this lands as a current spike through the joints' brushless actuators, any transmission slack taken up in one tick, a visible twitch of the end effector — repeated every replan, roughly every 740 ms. The robot develops a tic. And because the jump direction depends on the noise draw, it never averages out; it accumulates in Lesson 5's jerk metrics and in grasp success whenever a seam lands during the approach centimeters.

Checkpoint 02

You switch to a fresh chunk with correct index alignment — the new chunk's action for the current control step targets the same wall-clock instant as the old one. Both chunks are individually smooth. Why can the commanded trajectory still jump at the seam?

One published answer you know from the ACT lesson: temporal ensembling — query the policy every step, keep all live chunks, execute an exponentially-weighted average. It genuinely smooths seams. But check its preconditions against your stack. Per-step inference at 50 Hz means a 20 ms budget against π₀'s 110 ms p50 — a 5.5× shortfall that batching cannot close, because one robot's requests are sequential in time. At the replan cadence you can afford (~740 ms), you hold at most two live chunks, and the grand ensemble degenerates into a crossfade between exactly the two plans that disagree.

Even where ensembling is affordable, you derived its cost already: the informed-weight share after a surprise is S(τ)τ/kS(\tau) \approx \tau/k, so with k=50k = 50 the ensemble is still majority-stale 560 ms after the world changed. Averaging with old chunks is the mechanism of the smoothing and of the dilution. And mode averaging returns wearing a new hat: the mean of a handle-grasp plan and a rim-grasp plan is a trajectory toward neither. Ensembling treats the symptom — command discontinuity, in signal space — while the disease is plan disagreement, in decision space.

Real-time chunking: freeze the committed, inpaint the rest

The reframe that unlocks the clean solution comes from Physical Intelligence's real-time chunking work (opens in a new tab) (RTC), and it starts from an admission: while inference runs, the executor will execute the next dd actions of the old chunk — the buffer must not starve, and physics offers no rollback. Those actions are committed. So treat them as a constraint, not a problem: generate the new chunk conditioned on its first $d$ actions being exactly the committed ones. This is inpainting in the image-generation sense — committed actions are the known pixels, frozen in place; the model fills in the rest consistently. The seam then cannot disagree with reality by construction: the new plan begins by agreeing with what the arm actually did, and its correction unfolds as a continuation instead of a step.

Why does a flow-matching policy admit this naturally? Because sampling is not one forward pass — it is K=10K = 10 Euler steps of ODE integration, and iterative generation gives you a hook at every step. Recall the convention: the chunk rides a straight path Aτ=(1τ)ϵ+τAA^{\tau} = (1-\tau)\epsilon + \tau A from noise at τ=0\tau = 0 to data at τ=1\tau = 1. For the frozen rows the destination is known — the committed actions aˉ\bar a — so their entire path is known too. Clamp them onto it at every integration step and let the field move only the free rows:

Aτ+δ  =  Aτ+δvθ ⁣(Aτ,τ,onew),thenAiτ+δ    (1(τ+δ))ϵi  +  (τ+δ)aˉifor i<dA^{\tau+\delta} \;=\; A^{\tau} + \delta\, v_\theta\!\big(A^{\tau}, \tau, o_{\text{new}}\big), \qquad \text{then} \qquad A^{\tau+\delta}_{i} \;\leftarrow\; \big(1-(\tau+\delta)\big)\,\epsilon_i \;+\; (\tau+\delta)\,\bar a_i \quad \text{for } i < d
Replacement inpainting for a flow policy: after each Euler step, overwrite the frozen rows with their known point on the noise-to-data segment. At τ = 1 they equal the committed actions exactly; the action expert attends across the whole chunk at every step, so the free rows are generated feeling the frozen prefix the entire way.

That clamp is the pedagogical skeleton; the RTC paper hardens it in two honest ways. Plain replacement conditions only weakly — the free rows see the prefix but their velocity is never corrected for it — so RTC adds a guidance term, adapted from training-free diffusion inpainting, steering the free rows toward consistency. And it softens the boundary: actions just past index dd are not committed but will likely execute before the next replan can change them, so RTC attaches a per-index weight decaying from one to zero across the old chunk's remainder — the near future held close to the old plan, the far future left free to react. Note what inpainting does not do: it enforces consistency, not correctness. If the observation was stale, you get a beautifully continuous wrong plan — staleness math still governs; RTC just stops adding self-inflicted discontinuity on top. The paper's evaluations make the case: under injected latency, naive switching degrades sharply while RTC holds near zero-latency performance — at zero training cost, on frozen π₀-class checkpoints.

Note the schedule freedom this buys. The scheme stays real-time as long as d<Hd < H — inference must finish within one chunk duration, and your 260 ms p99 sits nearly 4× inside the 1 s budget. Because inference no longer forces a freeze, the 15% GPU duty cycle becomes spendable: replan more often, and reaction latency floors near Te2eT_{\text{e2e}} plus sensing — roughly 150–300 ms — versus 560 ms of ensemble dilution or the second-plus synchronous worst case.

Checkpoint 03

In RTC-style inpainting, which actions of the new chunk are hard-frozen to the previous chunk's values?

The other philosophy: train for the delay you will have

Inference-time inpainting is a patch applied to a policy that was never told delays exist. The December 2025 follow-up from the same group (arXiv:2512.05964 (opens in a new tab)) moves the fix into training: each training example samples a delay dd from a realistic distribution, hands the model the would-be-committed action prefix as an explicit input alongside the observation, and trains it to predict the continuation. At deployment there is no guidance machinery, no soft-mask schedule, no extra sampling cost — you pass the committed actions and the model natively generates a consistent continuation, because continuing a committed prefix is now in distribution rather than imposed on it.

You have seen this fork in your own field: post-training quantization versus quantization-aware training. Inference-time RTC is the PTQ move — works today, on frozen checkpoints you do not own the training run for, at the cost of approximation error and a guidance knob. Delay-conditioned training is the QAT move — the model learns the deployment condition, and can even learn delay-aware behavior such as hedging away from maneuvers it cannot revise in time; but it demands training access, compute, and a delay distribution fixed in advance — deploy outside it and you are extrapolating. The right experimental order is the course's: reproduce synchronous and inference-time RTC baselines first, then treat training-time conditioning as the extension. For your capstone, inference-time RTC is the natural substrate: its knobs move at runtime, exactly the degree of freedom a latency-aware scheduler needs.

StrategyRobot freezes?Seam behaviorReaction to a surpriseTraining changeMain knobs
SynchronousYes — 110–260 ms per chunkNone; motion stops insteadUp to ~1.1 s (chunk + inference)NoneFraction of chunk executed
Naive async switchNoDiscontinuous — plans disagree at the spliceReplan wait + ~260 msNoneFire threshold
Temporal ensemblingNo, but needs per-step inference: 20 ms budget vs 110 msSmooth by averaging~560 ms to 50% informed weight (k = 50)NoneDecay m, chunk overlap
RTC inpaintingNoConsistent by construction (frozen prefix + guided remainder)Replan wait + ~260 ms; floor ~150–300 msNone — inference-time onlyCommit horizon, replan rate, guidance weight
Delay-conditioned trainingNoConsistent — learned, no guidance machinerySame as RTC; model can also anticipate the delayFine-tune with sampled delays + prefix conditioningTraining delay distribution
Five execution strategies for a 50-action π₀ chunk at 50 Hz (p50 = 110 ms, p99 = 260 ms)

Building it: two threads, one buffer, a commit horizon

The runtime is two loops sharing one buffer. The control thread ticks at a hard 50 Hz and never blocks: pop an action, send it, check whether to fire. The inference thread owns everything slow: grab the freshest observation, snapshot the frozen prefix, call the policy server, deliver the chunk. The coupling discipline is one number, the commit horizon d^=fcTe2ep99=13\hat d = \lceil f_c \, T^{p99}_{\text{e2e}} \rceil = 13: at fire time, freeze the next d^\hat d buffered actions and fix the splice point d^\hat d steps out, deterministically, regardless of when the chunk actually lands. Freezing to the p99 costs a few steps of staleness on fast requests, but it makes the seam location a constant of the system instead of a random variable — and the inpainting mask must be known at generation time anyway. Two failure clauses carry over from policy serving: a chunk arriving after its splice point is dead on arrival (drop it, keep playing, re-fire — latest-wins), and a starved buffer triggers the safe-stop contract within one control period.

async_executor.py — chunk buffer with a frozen commit horizonpython
import threading
import numpy as np

class AsyncChunkExecutor:
    """Executor side of real-time chunking.

    Control thread, every 1/f_c seconds:
        action, fire = ex.get_action()
        if action is None: safe_stop()        # starvation clause
        else:              send_to_arm(action)
        if fire:
            frozen = ex.request_context()
            # inference thread: chunk = policy.infer_inpaint(obs, frozen)
            #                   ex.deliver_chunk(chunk)
    """

    def __init__(self, horizon=50, action_dim=7, commit=13, fire_at=13):
        self.H = horizon
        self.commit = commit          # ceil(f_c * T_e2e_p99): frozen prefix length
        self.fire_at = fire_at        # request when this many actions remain
        self.buf = np.zeros((0, action_dim))
        self.step = 0                 # absolute index of next action to execute
        self.pending = False
        self.splice_at = 0
        self.lock = threading.Lock()

    def get_action(self):
        with self.lock:
            remaining = len(self.buf) - self.step
            fire = (not self.pending) and remaining <= self.fire_at
            if remaining <= 0:
                return None, fire     # buffer starved: caller must safe-stop
            action = self.buf[self.step].copy()
            self.step += 1
            return action, fire

    def request_context(self):
        """Snapshot the frozen prefix; fixes the splice point deterministically."""
        with self.lock:
            self.pending = True
            self.splice_at = self.step + self.commit
            frozen = self.buf[self.step : self.splice_at].copy()
            return frozen             # inpainting target: chunk[0:commit] == frozen

    def deliver_chunk(self, chunk):
        """chunk index 0 aligns with the fire-time step; prefix was frozen."""
        with self.lock:
            self.pending = False
            if self.step > self.splice_at:
                return False          # late chunk: dead on arrival, re-fire next tick
            head = self.buf[: self.splice_at]
            tail = chunk[self.commit :]         # only the regenerated future
            self.buf = np.concatenate([head, tail], axis=0)
            return True

Look where the tunable numbers live, because they are the interface to your capstone. Commit horizon: larger never races the chunk but retains more stale actions per replan; smaller reacts faster but late chunks start hitting the dead-on-arrival clause. Fire threshold / replan rate: replanning more often buys reactivity with GPU duty cycle. Guidance weight (inside infer_inpaint): how hard the soft mask pulls the near future toward the old plan. A static, p99-derived setting is provably conservative — it wastes commit margin on the half of requests that finish in 110 ms and still loses beyond p99. The capstone thesis: drive these knobs with a forecast — predict each request's latency from signals you already log (queue state, thermals, recent p99 drift), set the commit horizon per request, and modulate replan rate by task phase, since approach wants reactivity and free-space transit does not. That is scheduling under a latency distribution: your home turf.

Studio exercise 01

Reproduce the seam, then remove it

Build a discrete-time simulation of three executors — synchronous, naive async switch, and frozen-prefix async — with no robot and no network. Use a scripted "policy": given a 7-dim joint state and a goal, return a 50-step chunk with (a) a seeded RNG flipping between two path homotopies around an obstacle with probability 0.3 per replan, (b) a correction term proportional to accumulated tracking error, and (c) per-chunk Gaussian action noise of 0.5 degrees. Model inference latency as lognormal (median 110 ms, p99 260 ms) plus injected extra delay swept from 0 to 300 ms. For each executor and delay, run 2,000 steps at 50 Hz and record max per-tick command delta at each splice, RMS commanded jerk, and total frozen time. Plot seam magnitude versus injected delay and explain the shapes in three sentences.

Need a hint?

Skip threads — in discrete time, latency is an index offset: a chunk requested at step s becomes available at step s + ceil(f_c * latency), making runs deterministic. For the frozen-prefix executor, emulate inpainting by requiring the planner's chunk to start from the frozen prefix's final state and velocity; measure the seam at index d, one step past the frozen window.

Where this goes next: the previous lesson, What delay and jitter do to a control loop, gave you the cost model; this lesson spent it, trading a freeze you could see for a seam you now know how to remove. Both knobs assume you know where Te2eT_{\text{e2e}} actually goes, request by request — that is the next lesson's job: Profiling the loop: NVTX, Nsight, and the robot twist instruments the inference path so your commit horizon becomes a measurement rather than a guess. Then Motion quality: smoothness, jerk, and recovery turns the seam metrics you just simulated into the evaluation suite that decides whether your capstone scheduler actually made the robot move better.