File size: 10,673 Bytes
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 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 | """RTC adapter for openpi's PyTorch pi0.5 (``openpi.models_pytorch.pi0_pytorch.PI0Pytorch``).
Bring your own pi0.5 *base* checkpoint (loaded through openpi's
``create_trained_policy``); this module supplies the differentiable velocity
field RTC needs and wires it to :func:`rtc.sampler.rtc_guided_sample`.
Why this works without inlining anything
----------------------------------------
openpi's sampler ``PI0Pytorch.sample_actions`` is ``@torch.no_grad`` *and*
``torch.compile``d, so RTC cannot route its autograd through it. But the single
step it calls, ``PI0Pytorch.denoise_step(state, prefix_pad_masks,
past_key_values, x_t, timestep)``, is **not** decorated ``@torch.no_grad`` -- so
we can call it directly and gradients flow back to ``x_t``. (Contrast the vlash
adapter, whose ``denoise_step`` *is* no-grad and had to be inlined.)
We therefore:
1. run the prefix (VLM) once under ``torch.no_grad`` to build the KV cache
(``embed_prefix`` -> ``paligemma_with_expert.forward(use_cache=True)``),
exactly like ``scripts/sanity_check_ut_mse.py`` in openpi;
2. return ``velocity_fn(x, tau)`` that runs only ``denoise_step`` (the action
expert / suffix) against that cache, with grad enabled.
Time / velocity convention
--------------------------
openpi integrates ``t=1`` noise -> ``t=0`` data and its expert regresses
``v_pi = noise - data`` (see ``PI0Pytorch.sample_actions``: ``dt<0``, loop from
``time=1``). RTC's core (:mod:`rtc.sampler`) integrates the *opposite* way
(``tau=0`` noise -> ``tau=1`` data). The two are reconciled by the identity used
throughout this package::
t = 1 - tau
v_rtc(x, tau) = - v_pi(x, t = 1 - tau)
which is what :func:`build_pi0_velocity_field` returns.
"""
from __future__ import annotations
import torch
from .masking import PrefixAttentionSchedule
from .sampler import VelocityField, rtc_guided_sample
def build_observation(policy, obs_dict: dict):
"""Turn a raw obs dict into a batched, on-device openpi ``Observation``.
Replicates the input preamble of ``openpi.policies.policy.Policy.infer`` for
the PyTorch path: apply the policy's input transforms (normalization,
tokenization, image resize keys), convert to torch tensors, add a batch
dim, and build the ``Observation`` dataclass.
Args:
policy: an openpi ``Policy`` created by ``create_trained_policy`` with a
PyTorch model (``policy._model`` is a ``PI0Pytorch``).
obs_dict: the same raw dict you would pass to ``policy.infer`` -- e.g.
``{"observation/image": HWC uint8, "observation/state": (D,) float32,
"prompt": str, ...}`` (exact keys depend on the checkpoint's data
config / input transforms).
Returns:
An ``openpi.models.model.Observation`` on ``policy._pytorch_device`` with
a leading batch dim of 1.
"""
import jax
import numpy as np
from openpi.models import model as _model
device = policy._pytorch_device
inputs = jax.tree.map(lambda x: x, obs_dict) # copy (transforms mutate in place)
inputs = policy._input_transform(inputs)
inputs = jax.tree.map(
lambda x: torch.from_numpy(np.array(x)).to(device)[None, ...], inputs
)
return _model.Observation.from_dict(inputs)
def build_pi0_velocity_field(
policy, observation
) -> tuple[VelocityField, int, int, torch.device, dict]:
"""Build an RTC-convention velocity field for one observation.
Mirrors :func:`rtc.vlash_pi05.build_vlash_pi05_velocity_field`, using openpi's
``PI0Pytorch`` seams: preprocess -> ``embed_prefix`` -> prefill KV cache
(no-grad) -> ``velocity_fn(x, tau)`` that calls ``denoise_step`` (grads flow).
Args:
policy: openpi ``Policy`` (PyTorch) whose ``_model`` is a ``PI0Pytorch``.
observation: a batched, on-device ``Observation`` (see
:func:`build_observation`).
Returns:
``(velocity_fn, horizon, action_dim, device, decode_ctx)``. ``horizon`` is
the model action horizon ``H`` and ``action_dim`` the (padded, usually 32)
model action dim ``A``. ``decode_ctx`` carries what
:func:`decode_actions` needs to unnormalize a model-space chunk back to
robot space.
"""
from openpi.models_pytorch.pi0_pytorch import make_att_2d_masks
model = policy._model
model.eval()
device = policy._pytorch_device
horizon = int(model.config.action_horizon)
action_dim = int(model.config.action_dim)
# ---- Prefill: cache the prefix (VLM) KV once, no grad (it is a constant) ----
with torch.no_grad():
(
images,
img_masks,
lang_tokens,
lang_masks,
state,
sam2_tokens,
) = model._preprocess_observation(observation, train=False)
prefix_embs, prefix_pad_masks, prefix_att_masks = model.embed_prefix(
images, img_masks, lang_tokens, lang_masks, sam2_tokens=sam2_tokens
)
prefix_att_2d_masks = make_att_2d_masks(prefix_pad_masks, prefix_att_masks)
prefix_position_ids = torch.cumsum(prefix_pad_masks, dim=1) - 1
prefix_att_2d_masks_4d = model._prepare_attention_masks_4d(prefix_att_2d_masks)
# openpi requires eager attention for the KV-cache path.
model.paligemma_with_expert.paligemma.language_model.config._attn_implementation = "eager"
_, past_key_values = model.paligemma_with_expert.forward(
attention_mask=prefix_att_2d_masks_4d,
position_ids=prefix_position_ids,
past_key_values=None,
inputs_embeds=[prefix_embs, None],
use_cache=True,
)
bsize = int(prefix_pad_masks.shape[0])
def velocity_fn(x: torch.Tensor, tau: float) -> torch.Tensor:
# RTC tau -> openpi t convention, and v_rtc = -v_pi (see module docstring).
t = 1.0 - tau
timestep = torch.full((bsize,), t, dtype=torch.float32, device=x.device)
v_pi = model.denoise_step(state, prefix_pad_masks, past_key_values, x, timestep)
return -v_pi
# state is the normalized model-space state; the output transform may need it
# (e.g. to convert delta actions to absolute) when decoding.
decode_ctx = {"state": state}
return velocity_fn, horizon, action_dim, device, decode_ctx
def align_previous_chunk(prev_chunk: torch.Tensor, execution_horizon: int) -> torch.Tensor:
"""Shift the currently-executing chunk into the new chunk's frame.
Drops the first ``s`` rows (already executed) and right-pads with zeros.
Padding rows get soft-mask weight 0, so their value is irrelevant. Same as
:func:`rtc.vlash_pi05.align_previous_chunk`.
"""
if prev_chunk.ndim != 3:
raise ValueError(f"prev_chunk must be (B, H, A), got {tuple(prev_chunk.shape)}")
b, h, a = prev_chunk.shape
s = execution_horizon
remaining = prev_chunk[:, s:, :]
pad = torch.zeros(
(b, h - remaining.shape[1], a), dtype=prev_chunk.dtype, device=prev_chunk.device
)
return torch.cat([remaining, pad], dim=1)
def sample_initial_chunk(
policy,
observation,
*,
num_steps: int = 5,
noise: torch.Tensor | None = None,
) -> tuple[torch.Tensor, dict]:
"""Plain (unguided) flow sample for the FIRST chunk ``A_init``.
RTC's controller needs an initial chunk in the *same model space* as the RTC
output (normalized, ``action_dim``-wide), so it must come from the same
velocity field -- not from ``policy.infer`` (which is decoded to robot space).
This runs :func:`rtc.sampler.sample_flow` with RTC's velocity field.
Returns ``(chunk, decode_ctx)`` where ``chunk`` is ``(B, H, A)`` in model
space; pass it (and later RTC outputs) through :func:`decode_actions` per step.
"""
from .sampler import sample_flow
velocity_fn, horizon, action_dim, device, decode_ctx = build_pi0_velocity_field(
policy, observation
)
if noise is None:
noise = policy._model.sample_noise((1, horizon, action_dim), device)
chunk = sample_flow(velocity_fn, noise=noise, num_steps=num_steps)
return chunk, decode_ctx
def rtc_sample_actions(
policy,
observation,
prev_chunk: torch.Tensor,
inference_delay: int,
execution_horizon: int,
*,
num_steps: int = 5,
max_guidance_weight: float = 5.0,
prefix_attention_schedule: PrefixAttentionSchedule = "exp",
noise: torch.Tensor | None = None,
already_aligned: bool = False,
) -> tuple[torch.Tensor, dict]:
"""RTC guided inpainting for openpi pi0.5.
Same surface as :func:`rtc.vlash_pi05.rtc_sample_actions_vlash`. ``observation``
is a batched, on-device ``Observation`` (see :func:`build_observation`);
``prev_chunk`` is the currently-executing chunk in model space ``(B, H, A)``.
Returns ``(new_chunk, decode_ctx)``. ``new_chunk`` is in model space; run it
through :func:`decode_actions` to get robot-space actions.
"""
velocity_fn, horizon, action_dim, device, decode_ctx = build_pi0_velocity_field(
policy, observation
)
y = prev_chunk if already_aligned else align_previous_chunk(prev_chunk, execution_horizon)
if y.shape[1] != horizon:
raise ValueError(f"prev_chunk horizon {y.shape[1]} != model action_horizon {horizon}")
if noise is None:
noise = policy._model.sample_noise((y.shape[0], horizon, action_dim), device)
noise = noise.to(dtype=y.dtype)
new_chunk = rtc_guided_sample(
velocity_fn,
prev_chunk=y,
inference_delay=inference_delay,
execution_horizon=execution_horizon,
noise=noise,
num_steps=num_steps,
max_guidance_weight=max_guidance_weight,
prefix_attention_schedule=prefix_attention_schedule,
)
return new_chunk, decode_ctx
def decode_actions(policy, chunk: torch.Tensor, decode_ctx: dict):
"""Unnormalize a model-space chunk back to robot space.
Mirrors the output path of ``Policy.infer``: run the policy's output
transforms on ``{"state", "actions"}``. ``chunk`` is ``(B, H, A)`` or
``(H, A)`` in model space; a single batch element is decoded.
Returns a numpy array of robot-space actions ``(H, robot_action_dim)``.
"""
import numpy as np
arr = chunk.detach().to(torch.float32).cpu().numpy()
if arr.ndim == 3:
arr = arr[0]
state = decode_ctx.get("state")
if state is not None:
state = np.asarray(state.detach().to(torch.float32).cpu().numpy())
if state.ndim == 2:
state = state[0]
outputs = {"state": state, "actions": arr}
return policy._output_transform(outputs)["actions"]
|