| """Real-Time Chunking (RTC) for MolmoAct2 — inference-time, no retraining. |
| |
| RTC (arXiv 2506.07339) removes the discontinuity you get when a freshly generated |
| action chunk replaces the one currently executing. It treats the overlap as an |
| INPAINTING problem during flow sampling: the actions that will inevitably execute |
| while we're computing are pinned to the previous chunk, a middle band is softly |
| guided toward it, and the tail beyond the old chunk is generated freely. |
| |
| idx < d "frozen" weight 1 (executes during the inference delay) |
| d <= idx < s "guided" weight 1->0 (EXP schedule) |
| idx >= s "free" weight 0 |
| |
| Guidance is NOT an overwrite. Following the paper (and LeRobot's reference |
| implementation) it is a pseudoinverse-guidance (PiGDM) correction injected into the |
| velocity field at EVERY denoising step, via a vector-Jacobian product: |
| |
| x1 = x_t + (1 - tau) * v # predicted clean sample |
| err = (prev_chunk - x1) * W # weighted target error |
| corr = VJP(x1, x_t, err) # d x1/d x_t ^T @ err |
| v_rtc = v + w(tau) * corr |
| w(tau) = min(beta, ((1-tau)^2 + tau^2) / (tau * (1-tau))) |
| |
| SIGN NOTE (the highest-risk part of this port): LeRobot integrates time 1->0 with a |
| velocity pointing toward NOISE and writes `v - w*corr`. MolmoAct2 integrates 0->1 |
| with a velocity pointing toward DATA (`trajectory = trajectory + dt * velocity`), so |
| the correction ADDS here. Both end up moving the trajectory by +corr; only the |
| velocity convention differs. `selftest_guidance_direction()` asserts this |
| empirically rather than trusting the derivation. |
| |
| COSTS (measured/reported, worth knowing before enabling): |
| * needs autograd -> the model's @torch.no_grad() must be lifted, and the |
| CUDA-graph fast path must be disabled (you cannot backprop a captured graph). |
| * ~20% extra latency on top of that. We are already NETWORK-bound, so RTC is |
| only worth enabling once the round-trip is short. |
| * requires d <= s <= H - d. With H=30, s=10: d <= 10. At 10fps that's ~1.0s of |
| tolerable delay; at 15fps only ~0.66s. |
| """ |
|
|
| import math |
| from typing import Optional |
|
|
| import torch |
|
|
|
|
| def prefix_weights( |
| delay: int, execution_horizon: int, total: int, schedule: str = "exp" |
| ) -> torch.Tensor: |
| """Per-timestep guidance weights (port of LeRobot's get_prefix_weights). |
| |
| `delay` (d) actions are pinned at 1.0; weights decay to 0 by `execution_horizon` |
| (s); everything at/after s is free (0.0).""" |
| start = min(delay, execution_horizon) |
| end = execution_horizon |
| if schedule == "zeros": |
| w = torch.zeros(total) |
| w[:start] = 1.0 |
| return w |
| if schedule == "ones": |
| w = torch.ones(total) |
| w[end:] = 0.0 |
| return w |
|
|
| |
| skip = max(total - end, 0) |
| steps = total - skip - start |
| lin = torch.linspace(1, 0, steps + 2)[1:-1] if (end > start and steps > 0) else torch.tensor([]) |
| if schedule == "exp": |
| |
| lin = lin * torch.expm1(lin).div(math.e - 1) |
| if total - end > 0: |
| lin = torch.cat([lin, torch.zeros(total - end)]) |
| if min(start, total) > 0: |
| lin = torch.cat([torch.ones(min(start, total)), lin]) |
| return lin |
|
|
|
|
| def guidance_weight(tau: float, max_weight: float) -> float: |
| """w(tau) = min(beta, ((1-tau)^2 + tau^2) / (tau*(1-tau))), clamped at both ends.""" |
| one_minus = 1.0 - tau |
| if tau <= 0.0 or one_minus <= 0.0: |
| return float(max_weight) |
| w = ((one_minus ** 2) + (tau ** 2)) / (tau * one_minus) |
| return float(min(w, max_weight)) |
|
|
|
|
| def feasible(delay: int, horizon: int, execution_horizon: int) -> bool: |
| """RTC needs d <= s <= H - d. Past that the frozen prefix and the free tail |
| overlap and the guidance is not well defined — better to skip RTC for that |
| request than to emit a silently wrong chunk.""" |
| return 0 <= delay <= execution_horizon <= horizon - delay |
|
|
|
|
| def pick_execution_horizon(delay: int, horizon: int) -> Optional[int]: |
| """Smallest feasible s that leaves the guided band some room, or None if the |
| delay is too large for this horizon (d > H/2).""" |
| s = min(max(delay + 4, delay), horizon - delay) |
| return s if feasible(delay, horizon, s) else None |
|
|
|
|
| class RTCState: |
| """Per-session guidance target, kept in the model's NORMALIZED action space. |
| |
| We cache the previous chunk as the raw flow output rather than asking the client |
| to send actions back: the flow operates on normalized actions, so a robot-scale |
| chunk from the client would have to be re-normalized (and our client also applies |
| a joint calibration). Caching the model's own output sidesteps both.""" |
|
|
| def __init__(self) -> None: |
| self.prev: Optional[torch.Tensor] = None |
| self.enabled = False |
| self.consumed = 0 |
| self.delay = 0 |
| self.execution_horizon = 10 |
| self.max_guidance_weight = 10.0 |
| self.schedule = "exp" |
| self.applied = 0 |
|
|
| def target(self, like: torch.Tensor) -> Optional[torch.Tensor]: |
| """Previous chunk aligned to the new chunk's timeline: drop the `consumed` |
| actions that already executed, then zero-pad to the new chunk's shape.""" |
| if not self.enabled or self.prev is None: |
| return None |
| left = self.prev[:, self.consumed:, :] |
| if left.shape[1] == 0: |
| return None |
| out = torch.zeros_like(like) |
| n = min(left.shape[1], out.shape[1]) |
| a = min(left.shape[2], out.shape[2]) |
| out[:, :n, :a] = left[:, :n, :a].to(out.device, out.dtype) |
| return out |
|
|
|
|
| def _flow_owner(model): |
| """Find the object that actually owns the flow loop. |
| |
| `_run_action_flow_loop`, `_require_action_expert` and `_mask_action_dim_tensor` |
| are all methods of MolmoAct2Model, while what you load from the Hub is a |
| MolmoAct2ForConditionalGeneration that HOLDS one as `.model`. Patching the |
| outer wrapper raises AttributeError at load time and takes the whole server |
| down, so resolve the owner instead of assuming it.""" |
| for cand in (model, getattr(model, "model", None), getattr(model, "base_model", None)): |
| if cand is not None and hasattr(cand, "_run_action_flow_loop"): |
| return cand |
| return None |
|
|
|
|
| def install_rtc(model, state: RTCState): |
| """Monkeypatch the flow loop to apply RTC guidance. |
| |
| Returns the original bound method, or None if the loop could not be located — |
| NEVER raises. A diagnostic feature must not be able to stop the model from |
| loading; without the patch the server simply serves un-guided chunks. |
| |
| The patched loop is a no-op (bit-identical to upstream) whenever `state` has no |
| target, so leaving it installed costs nothing when RTC is off.""" |
| owner = _flow_owner(model) |
| if owner is None: |
| return None |
| original = owner._run_action_flow_loop |
|
|
| def guided_loop(inputs, steps: int) -> torch.Tensor: |
| trajectory = inputs.trajectory |
| target = state.target(trajectory) |
| if target is None: |
| |
| |
| |
| out = original(inputs, steps) |
| state.prev = out.detach() |
| return out |
|
|
| action_expert = owner._require_action_expert() |
| dt = 1.0 / steps |
| pad = inputs.action_dim_is_pad |
| mask_enabled = owner.config.mask_action_dim_padding |
| W = prefix_weights(state.delay, state.execution_horizon, |
| trajectory.shape[1], state.schedule) |
| W = W.to(trajectory.device, trajectory.dtype).view(1, -1, 1) |
|
|
| for idx in range(steps): |
| tau = idx / steps |
| x_t = trajectory.detach().requires_grad_(True) |
| with torch.enable_grad(): |
| velocity = action_expert.forward_with_context( |
| x_t, |
| inputs.modulations[idx].conditioning, |
| context=inputs.context, |
| modulation=inputs.modulations[idx], |
| ) |
| velocity = owner._mask_action_dim_tensor( |
| velocity, action_dim_is_pad=pad, enabled=mask_enabled |
| ) |
| x1 = x_t + (1.0 - tau) * velocity |
| err = ((target - x1) * W).detach() |
| corr = torch.autograd.grad(x1, x_t, err, retain_graph=False)[0] |
|
|
| w = guidance_weight(tau, state.max_guidance_weight) |
| |
| velocity = (velocity + w * corr).detach() |
| trajectory = owner._mask_action_dim_tensor( |
| trajectory.detach() + dt * velocity, action_dim_is_pad=pad, enabled=mask_enabled |
| ) |
| state.applied += 1 |
| state.prev = trajectory.detach() |
| return trajectory |
|
|
| owner._run_action_flow_loop = guided_loop |
| return original |
|
|
|
|
| |
| def selftest_guidance_direction() -> dict: |
| """Assert the SIGN empirically on a toy linear flow, with no model involved. |
| |
| Toy: velocity = (goal - x). Euler-integrating it drives x -> goal. With RTC |
| guidance toward `prev`, the FROZEN prefix must end up closer to `prev` than the |
| unguided run does, and the free tail must be left alone.""" |
| H, A, steps = 30, 4, 10 |
| goal = torch.zeros(1, H, A) |
| prev = torch.ones(1, H, A) * 5.0 |
| W = prefix_weights(4, 10, H, "exp").view(1, -1, 1) |
|
|
| def run(guided: bool) -> torch.Tensor: |
| x = torch.full((1, H, A), -5.0) |
| for idx in range(steps): |
| tau = idx / steps |
| xt = x.detach().requires_grad_(True) |
| with torch.enable_grad(): |
| v = goal - xt |
| x1 = xt + (1.0 - tau) * v |
| if guided: |
| err = ((prev - x1) * W).detach() |
| corr = torch.autograd.grad(x1, xt, err, retain_graph=False)[0] |
| v = v + guidance_weight(tau, 10.0) * corr |
| x = (x.detach() + (1.0 / steps) * v.detach()) |
| return x |
|
|
| plain, rtc = run(False), run(True) |
| d_plain = (plain[0, 0] - prev[0, 0]).abs().mean().item() |
| d_rtc = (rtc[0, 0] - prev[0, 0]).abs().mean().item() |
| tail_shift = (rtc[0, -1] - plain[0, -1]).abs().mean().item() |
| return { |
| "prefix_dist_unguided": round(d_plain, 4), |
| "prefix_dist_rtc": round(d_rtc, 4), |
| "prefix_pulled_toward_prev": d_rtc < d_plain, |
| "free_tail_unchanged": tail_shift < 1e-5, |
| } |
|
|
|
|
| if __name__ == "__main__": |
| w = prefix_weights(4, 10, 30, "exp") |
| print("weights[:12]:", [round(float(x), 3) for x in w[:12]]) |
| print("frozen prefix all 1.0 :", bool((w[:4] == 1.0).all())) |
| print("free tail all 0.0 :", bool((w[10:] == 0.0).all())) |
| print("monotonic in guided band:", bool((w[4:10].diff() <= 0).all())) |
| print("w(tau) mid/edges :", [round(guidance_weight(t, 10.0), 3) for t in (0.0, 0.1, 0.5, 0.9, 1.0)]) |
| print("direction selftest :", selftest_guidance_direction()) |
|
|