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"]