dennis96 commited on
Commit
9a4a86b
·
verified ·
1 Parent(s): d98b09b

Upload folder using huggingface_hub

Browse files
openpi_modification/pi0.py ADDED
@@ -0,0 +1,310 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+
3
+ import einops
4
+ import flax.nnx as nnx
5
+ import flax.nnx.bridge as nnx_bridge
6
+ import jax
7
+ import jax.numpy as jnp
8
+ from typing_extensions import override
9
+
10
+ from openpi.models import model as _model
11
+ from openpi.models import pi0_config
12
+ import openpi.models.gemma as _gemma
13
+ import openpi.models.pointnet as _pointnet
14
+ import openpi.models.siglip as _siglip
15
+ from openpi.shared import array_typing as at
16
+
17
+ logger = logging.getLogger("openpi")
18
+
19
+
20
+ def make_attn_mask(input_mask, mask_ar):
21
+ """Adapted from big_vision.
22
+
23
+ Tokens can attend to valid inputs tokens which have a cumulative mask_ar
24
+ smaller or equal to theirs. This way `mask_ar` bool[?B, N] can be used to
25
+ setup several types of attention, for example:
26
+
27
+ [[1 1 1 1 1 1]]: pure causal attention.
28
+
29
+ [[0 0 0 1 1 1]]: prefix-lm attention. The first 3 tokens can attend between
30
+ themselves and the last 3 tokens have a causal attention. The first
31
+ entry could also be a 1 without changing behaviour.
32
+
33
+ [[1 0 1 0 1 0 0 1 0 0]]: causal attention between 4 blocks. Tokens of a
34
+ block can attend all previous blocks and all tokens on the same block.
35
+
36
+ Args:
37
+ input_mask: bool[B, N] true if its part of the input, false if padding.
38
+ mask_ar: bool[?B, N] mask that's true where previous tokens cannot depend on
39
+ it and false where it shares the same attention mask as the previous token.
40
+ """
41
+ mask_ar = jnp.broadcast_to(mask_ar, input_mask.shape)
42
+ cumsum = jnp.cumsum(mask_ar, axis=1)
43
+ attn_mask = cumsum[:, None, :] <= cumsum[:, :, None]
44
+ valid_mask = input_mask[:, None, :] * input_mask[:, :, None]
45
+ return jnp.logical_and(attn_mask, valid_mask)
46
+
47
+
48
+ @at.typecheck
49
+ def posemb_sincos(
50
+ pos: at.Real[at.Array, " b"], embedding_dim: int, min_period: float, max_period: float
51
+ ) -> at.Float[at.Array, "b {embedding_dim}"]:
52
+ """Computes sine-cosine positional embedding vectors for scalar positions."""
53
+ if embedding_dim % 2 != 0:
54
+ raise ValueError(f"embedding_dim ({embedding_dim}) must be divisible by 2")
55
+
56
+ fraction = jnp.linspace(0.0, 1.0, embedding_dim // 2)
57
+ period = min_period * (max_period / min_period) ** fraction
58
+ sinusoid_input = jnp.einsum(
59
+ "i,j->ij",
60
+ pos,
61
+ 1.0 / period * 2 * jnp.pi,
62
+ precision=jax.lax.Precision.HIGHEST,
63
+ )
64
+ return jnp.concatenate([jnp.sin(sinusoid_input), jnp.cos(sinusoid_input)], axis=-1)
65
+
66
+
67
+ class Pi0(_model.BaseModel):
68
+ def __init__(self, config: pi0_config.Pi0Config, rngs: nnx.Rngs):
69
+ super().__init__(config.action_dim, config.action_horizon, config.max_token_len)
70
+ self.pi05 = config.pi05
71
+ self.pcd = config.pcd
72
+ paligemma_config = _gemma.get_config(config.paligemma_variant)
73
+ action_expert_config = _gemma.get_config(config.action_expert_variant)
74
+ # TODO: rewrite gemma in NNX. For now, use bridge.
75
+ llm = nnx_bridge.ToNNX(
76
+ _gemma.Module(
77
+ configs=[paligemma_config, action_expert_config],
78
+ embed_dtype=config.dtype,
79
+ adarms=config.pi05,
80
+ )
81
+ )
82
+ llm.lazy_init(rngs=rngs, method="init", use_adarms=[False, True] if config.pi05 else [False, False])
83
+ img = nnx_bridge.ToNNX(
84
+ _siglip.Module(
85
+ num_classes=paligemma_config.width,
86
+ variant="So400m/14",
87
+ pool_type="none",
88
+ scan=True,
89
+ dtype_mm=config.dtype,
90
+ )
91
+ )
92
+ img.lazy_init(next(iter(config.fake_obs().images.values())), train=False, rngs=rngs)
93
+ self.PaliGemma = nnx.Dict(llm=llm, img=img)
94
+ self.action_in_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)
95
+ if config.pi05:
96
+ self.time_mlp_in = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
97
+ self.time_mlp_out = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
98
+ else:
99
+ self.state_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)
100
+ self.action_time_mlp_in = nnx.Linear(2 * action_expert_config.width, action_expert_config.width, rngs=rngs)
101
+ self.action_time_mlp_out = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
102
+ self.action_out_proj = nnx.Linear(action_expert_config.width, config.action_dim, rngs=rngs)
103
+
104
+ if self.pcd:
105
+ pointnet_config = _pointnet.get_config(config.pointnet_variant)
106
+ self.pointnet = nnx_bridge.ToNNX(
107
+ _pointnet.UncoloredPointNet(
108
+ n_coordinates=pointnet_config.n_coordinates,
109
+ output_dim=pointnet_config.output_dim,
110
+ hidden_dim=pointnet_config.hidden_dim,
111
+ hidden_depth=pointnet_config.hidden_depth,
112
+ )
113
+ )
114
+ self.pointnet.lazy_init(config.fake_obs().pcd_xyz, rngs=rngs)
115
+
116
+ # This attribute gets automatically set by model.train() and model.eval().
117
+ self.deterministic = True
118
+
119
+ @at.typecheck
120
+ def embed_prefix(
121
+ self, obs: _model.Observation
122
+ ) -> tuple[at.Float[at.Array, "b s emb"], at.Bool[at.Array, "b s"], at.Bool[at.Array, " s"]]:
123
+ input_mask = []
124
+ ar_mask = []
125
+ tokens = []
126
+ # embed images
127
+ for name in obs.images:
128
+ image_tokens, _ = self.PaliGemma.img(obs.images[name], train=False)
129
+
130
+ tokens.append(image_tokens)
131
+ input_mask.append(
132
+ einops.repeat(
133
+ obs.image_masks[name],
134
+ "b -> b s",
135
+ s=image_tokens.shape[1],
136
+ )
137
+ )
138
+ # image tokens attend to each other
139
+ ar_mask += [False] * image_tokens.shape[1]
140
+
141
+ # add language (aka tokenized inputs)
142
+ if obs.tokenized_prompt is not None:
143
+ tokenized_inputs = self.PaliGemma.llm(obs.tokenized_prompt, method="embed")
144
+ tokens.append(tokenized_inputs)
145
+ input_mask.append(obs.tokenized_prompt_mask)
146
+ # full attention between image and language inputs
147
+ ar_mask += [False] * tokenized_inputs.shape[1]
148
+
149
+ # add point cloud
150
+ if self.pcd:
151
+ pcd_tokens = self.pointnet(obs.pcd_xyz) # (b s=16 2048)
152
+ tokens.append(pcd_tokens)
153
+ input_mask.append(jnp.ones(pcd_tokens.shape[:2], dtype=jnp.bool_))
154
+ ar_mask += [False] * pcd_tokens.shape[1]
155
+
156
+ tokens = jnp.concatenate(tokens, axis=1)
157
+ input_mask = jnp.concatenate(input_mask, axis=1)
158
+ ar_mask = jnp.array(ar_mask)
159
+ return tokens, input_mask, ar_mask
160
+
161
+ @at.typecheck
162
+ def embed_suffix(
163
+ self, obs: _model.Observation, noisy_actions: _model.Actions, timestep: at.Float[at.Array, " b"]
164
+ ) -> tuple[
165
+ at.Float[at.Array, "b s emb"],
166
+ at.Bool[at.Array, "b s"],
167
+ at.Bool[at.Array, " s"],
168
+ at.Float[at.Array, "b emb"] | None,
169
+ ]:
170
+ input_mask = []
171
+ ar_mask = []
172
+ tokens = []
173
+ if not self.pi05:
174
+ # add a single state token
175
+ state_token = self.state_proj(obs.state)[:, None, :]
176
+ tokens.append(state_token)
177
+ input_mask.append(jnp.ones((obs.state.shape[0], 1), dtype=jnp.bool_))
178
+ # image/language inputs do not attend to state or actions
179
+ ar_mask += [True]
180
+
181
+ action_tokens = self.action_in_proj(noisy_actions)
182
+ # embed timestep using sine-cosine positional encoding with sensitivity in the range [0, 1]
183
+ time_emb = posemb_sincos(timestep, self.action_in_proj.out_features, min_period=4e-3, max_period=4.0)
184
+ if self.pi05:
185
+ # time MLP (for adaRMS)
186
+ time_emb = self.time_mlp_in(time_emb)
187
+ time_emb = nnx.swish(time_emb)
188
+ time_emb = self.time_mlp_out(time_emb)
189
+ time_emb = nnx.swish(time_emb)
190
+ action_expert_tokens = action_tokens
191
+ adarms_cond = time_emb
192
+ else:
193
+ # mix timestep + action information using an MLP (no adaRMS)
194
+ time_tokens = einops.repeat(time_emb, "b emb -> b s emb", s=self.action_horizon)
195
+ action_time_tokens = jnp.concatenate([action_tokens, time_tokens], axis=-1)
196
+ action_time_tokens = self.action_time_mlp_in(action_time_tokens)
197
+ action_time_tokens = nnx.swish(action_time_tokens)
198
+ action_time_tokens = self.action_time_mlp_out(action_time_tokens)
199
+ action_expert_tokens = action_time_tokens
200
+ adarms_cond = None
201
+ tokens.append(action_expert_tokens)
202
+ input_mask.append(jnp.ones(action_expert_tokens.shape[:2], dtype=jnp.bool_))
203
+ # image/language/state inputs do not attend to action tokens
204
+ # ar_mask += [True] + ([False] * (self.action_horizon - 1))
205
+ tokens = jnp.concatenate(tokens, axis=1)
206
+ input_mask = jnp.concatenate(input_mask, axis=1)
207
+ ar_mask += [True] + ([False] * (input_mask.shape[1] - 1))
208
+ ar_mask = jnp.array(ar_mask)
209
+ return tokens, input_mask, ar_mask, adarms_cond
210
+
211
+ @override
212
+ def compute_loss(
213
+ self, rng: at.KeyArrayLike, observation: _model.Observation, actions: _model.Actions, *, train: bool = False
214
+ ) -> at.Float[at.Array, "*b ah"]:
215
+ preprocess_rng, noise_rng, time_rng = jax.random.split(rng, 3)
216
+ observation = _model.preprocess_observation(preprocess_rng, observation, train=train)
217
+
218
+ batch_shape = actions.shape[:-2]
219
+ noise = jax.random.normal(noise_rng, actions.shape)
220
+ time = jax.random.beta(time_rng, 1.5, 1, batch_shape) * 0.999 + 0.001
221
+ time_expanded = time[..., None, None]
222
+ x_t = time_expanded * noise + (1 - time_expanded) * actions
223
+ u_t = noise - actions
224
+
225
+ # one big forward pass of prefix + suffix at once
226
+ prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)
227
+ suffix_tokens, suffix_mask, suffix_ar_mask, adarms_cond = self.embed_suffix(observation, x_t, time)
228
+ input_mask = jnp.concatenate([prefix_mask, suffix_mask], axis=1)
229
+ ar_mask = jnp.concatenate([prefix_ar_mask, suffix_ar_mask], axis=0)
230
+ attn_mask = make_attn_mask(input_mask, ar_mask)
231
+ positions = jnp.cumsum(input_mask, axis=1) - 1
232
+ (prefix_out, suffix_out), _ = self.PaliGemma.llm(
233
+ [prefix_tokens, suffix_tokens], mask=attn_mask, positions=positions, adarms_cond=[None, adarms_cond]
234
+ )
235
+ v_t = self.action_out_proj(suffix_out[:, -self.action_horizon :])
236
+
237
+ return jnp.mean(jnp.square(v_t - u_t), axis=-1)
238
+
239
+ @override
240
+ def sample_actions(
241
+ self,
242
+ rng: at.KeyArrayLike,
243
+ observation: _model.Observation,
244
+ *,
245
+ num_steps: int | at.Int[at.Array, ""] = 10,
246
+ noise: at.Float[at.Array, "b ah ad"] | None = None,
247
+ return_prefix_z: bool = False,
248
+ ) -> _model.Actions:
249
+ observation = _model.preprocess_observation(None, observation, train=False)
250
+ # note that we use the convention more common in diffusion literature, where t=1 is noise and t=0 is the target
251
+ # distribution. yes, this is the opposite of the pi0 paper, and I'm sorry.
252
+ dt = -1.0 / num_steps
253
+ batch_size = observation.state.shape[0]
254
+ if noise is None:
255
+ noise = jax.random.normal(rng, (batch_size, self.action_horizon, self.action_dim))
256
+
257
+ # first fill KV cache with a forward pass of the prefix
258
+ prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)
259
+ prefix_attn_mask = make_attn_mask(prefix_mask, prefix_ar_mask)
260
+ positions = jnp.cumsum(prefix_mask, axis=1) - 1
261
+ (prefix_out, _), kv_cache = self.PaliGemma.llm([prefix_tokens, None], mask=prefix_attn_mask, positions=positions)
262
+
263
+ if return_prefix_z:
264
+ weights = prefix_mask.astype(jnp.float32)
265
+ prefix_z = jnp.sum(prefix_out.astype(jnp.float32) * weights[..., None], axis=1)
266
+ prefix_z = prefix_z / jnp.maximum(jnp.sum(weights, axis=1, keepdims=True), 1.0)
267
+
268
+ def step(carry):
269
+ x_t, time = carry
270
+ suffix_tokens, suffix_mask, suffix_ar_mask, adarms_cond = self.embed_suffix(
271
+ observation, x_t, jnp.broadcast_to(time, batch_size)
272
+ )
273
+ # `suffix_attn_mask` is shape (b, suffix_len, suffix_len) indicating how the suffix tokens can attend to each
274
+ # other
275
+ suffix_attn_mask = make_attn_mask(suffix_mask, suffix_ar_mask)
276
+ # `prefix_attn_mask` is shape (b, suffix_len, prefix_len) indicating how the suffix tokens can attend to the
277
+ # prefix tokens
278
+ prefix_attn_mask = einops.repeat(prefix_mask, "b p -> b s p", s=suffix_tokens.shape[1])
279
+ # `combined_mask` is shape (b, suffix_len, prefix_len + suffix_len) indicating how the suffix tokens (which
280
+ # generate the queries) can attend to the full prefix + suffix sequence (which generates the keys and values)
281
+ full_attn_mask = jnp.concatenate([prefix_attn_mask, suffix_attn_mask], axis=-1)
282
+ assert full_attn_mask.shape == (
283
+ batch_size,
284
+ suffix_tokens.shape[1],
285
+ prefix_tokens.shape[1] + suffix_tokens.shape[1],
286
+ )
287
+ # `positions` is shape (b, suffix_len) indicating the positions of the suffix tokens
288
+ positions = jnp.sum(prefix_mask, axis=-1)[:, None] + jnp.cumsum(suffix_mask, axis=-1) - 1
289
+
290
+ (prefix_out, suffix_out), _ = self.PaliGemma.llm(
291
+ [None, suffix_tokens],
292
+ mask=full_attn_mask,
293
+ positions=positions,
294
+ kv_cache=kv_cache,
295
+ adarms_cond=[None, adarms_cond],
296
+ )
297
+ assert prefix_out is None
298
+ v_t = self.action_out_proj(suffix_out[:, -self.action_horizon :])
299
+
300
+ return x_t + dt * v_t, time + dt
301
+
302
+ def cond(carry):
303
+ x_t, time = carry
304
+ # robust to floating-point error
305
+ return time >= -dt / 2
306
+
307
+ x_0, _ = jax.lax.while_loop(cond, step, (noise, 1.0))
308
+ if return_prefix_z:
309
+ return x_0, prefix_z
310
+ return x_0
openpi_modification/policy.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Sequence
2
+ import inspect
3
+ import logging
4
+ import pathlib
5
+ import time
6
+ from typing import Any, TypeAlias
7
+
8
+ import flax
9
+ import flax.traverse_util
10
+ import jax
11
+ import jax.numpy as jnp
12
+ import numpy as np
13
+ from openpi_client import base_policy as _base_policy
14
+ import torch
15
+ from typing_extensions import override
16
+
17
+ from openpi import transforms as _transforms
18
+ from openpi.models import model as _model
19
+ from openpi.shared import array_typing as at
20
+ from openpi.shared import nnx_utils
21
+
22
+ BasePolicy: TypeAlias = _base_policy.BasePolicy
23
+
24
+
25
+ class Policy(BasePolicy):
26
+ def __init__(
27
+ self,
28
+ model: _model.BaseModel,
29
+ *,
30
+ rng: at.KeyArrayLike | None = None,
31
+ transforms: Sequence[_transforms.DataTransformFn] = (),
32
+ output_transforms: Sequence[_transforms.DataTransformFn] = (),
33
+ sample_kwargs: dict[str, Any] | None = None,
34
+ metadata: dict[str, Any] | None = None,
35
+ pytorch_device: str = "cpu",
36
+ is_pytorch: bool = False,
37
+ ):
38
+ """Initialize the Policy.
39
+
40
+ Args:
41
+ model: The model to use for action sampling.
42
+ rng: Random number generator key for JAX models. Ignored for PyTorch models.
43
+ transforms: Input data transformations to apply before inference.
44
+ output_transforms: Output data transformations to apply after inference.
45
+ sample_kwargs: Additional keyword arguments to pass to model.sample_actions.
46
+ metadata: Additional metadata to store with the policy.
47
+ pytorch_device: Device to use for PyTorch models (e.g., "cpu", "cuda:0").
48
+ Only relevant when is_pytorch=True.
49
+ is_pytorch: Whether the model is a PyTorch model. If False, assumes JAX model.
50
+ """
51
+ self._model = model
52
+ self._input_transform = _transforms.compose(transforms)
53
+ self._output_transform = _transforms.compose(output_transforms)
54
+ self._sample_kwargs = sample_kwargs or {}
55
+ self._metadata = metadata or {}
56
+ self._is_pytorch_model = is_pytorch
57
+ self._pytorch_device = pytorch_device
58
+ self._supports_prefix_z = False
59
+
60
+ if self._is_pytorch_model:
61
+ self._model = self._model.to(pytorch_device)
62
+ self._model.eval()
63
+ self._sample_actions = model.sample_actions
64
+ else:
65
+ # JAX model setup
66
+ self._supports_prefix_z = "return_prefix_z" in inspect.signature(model.sample_actions).parameters
67
+ if self._supports_prefix_z:
68
+ self._sample_actions = nnx_utils.module_jit(
69
+ model.sample_actions,
70
+ static_argnames=("return_prefix_z",),
71
+ )
72
+ else:
73
+ self._sample_actions = nnx_utils.module_jit(model.sample_actions)
74
+ self._rng = rng or jax.random.key(0)
75
+
76
+ @override
77
+ def infer(self, obs: dict, *, noise: np.ndarray | None = None) -> dict: # type: ignore[misc]
78
+ # Make a copy since transformations may modify the inputs in place.
79
+ inputs = jax.tree.map(lambda x: x, obs)
80
+ inputs = self._input_transform(inputs)
81
+ if not self._is_pytorch_model:
82
+ # Make a batch and convert to jax.Array.
83
+ inputs = jax.tree.map(lambda x: jnp.asarray(x)[np.newaxis, ...], inputs)
84
+ self._rng, sample_rng_or_pytorch_device = jax.random.split(self._rng)
85
+ else:
86
+ # Convert inputs to PyTorch tensors and move to correct device
87
+ inputs = jax.tree.map(lambda x: torch.from_numpy(np.array(x)).to(self._pytorch_device)[None, ...], inputs)
88
+ sample_rng_or_pytorch_device = self._pytorch_device
89
+
90
+ # Prepare kwargs for sample_actions
91
+ sample_kwargs = dict(self._sample_kwargs)
92
+ if noise is not None:
93
+ noise = torch.from_numpy(noise).to(self._pytorch_device) if self._is_pytorch_model else jnp.asarray(noise)
94
+
95
+ if noise.ndim == 2: # If noise is (action_horizon, action_dim), add batch dimension
96
+ noise = noise[None, ...] # Make it (1, action_horizon, action_dim)
97
+ sample_kwargs["noise"] = noise
98
+ observation = _model.Observation.from_dict(inputs)
99
+ start_time = time.monotonic()
100
+ outputs = {
101
+ "state": inputs["state"],
102
+ "actions": self._sample_actions(sample_rng_or_pytorch_device, observation, **sample_kwargs),
103
+ }
104
+ model_time = time.monotonic() - start_time
105
+ if self._is_pytorch_model:
106
+ outputs = jax.tree.map(lambda x: np.asarray(x[0, ...].detach().cpu()), outputs)
107
+ else:
108
+ outputs = jax.tree.map(lambda x: np.asarray(x[0, ...]), outputs)
109
+
110
+ outputs = self._output_transform(outputs)
111
+ outputs["policy_timing"] = {
112
+ "infer_ms": model_time * 1000,
113
+ }
114
+ return outputs
115
+
116
+ def infer_with_prefix_z(self, obs: dict, *, noise: np.ndarray | None = None) -> dict:
117
+ """Run policy inference and return the COMET prefix latent used by A2C2.
118
+
119
+ This method preserves the normal output transforms for the action chunk,
120
+ and appends ``prefix_z`` after transforms so task-specific output
121
+ transforms cannot accidentally drop or reshape it.
122
+ """
123
+
124
+ if self._is_pytorch_model or not self._supports_prefix_z:
125
+ raise NotImplementedError("infer_with_prefix_z is only supported for JAX policies with return_prefix_z.")
126
+
127
+ # Make a copy since transformations may modify the inputs in place.
128
+ inputs = jax.tree.map(lambda x: x, obs)
129
+ inputs = self._input_transform(inputs)
130
+ inputs = jax.tree.map(lambda x: jnp.asarray(x)[np.newaxis, ...], inputs)
131
+ self._rng, sample_rng = jax.random.split(self._rng)
132
+
133
+ sample_kwargs = dict(self._sample_kwargs)
134
+ if noise is not None:
135
+ noise = jnp.asarray(noise)
136
+ if noise.ndim == 2:
137
+ noise = noise[None, ...]
138
+ sample_kwargs["noise"] = noise
139
+
140
+ observation = _model.Observation.from_dict(inputs)
141
+ start_time = time.monotonic()
142
+ actions, prefix_z = self._sample_actions(
143
+ sample_rng,
144
+ observation,
145
+ **sample_kwargs,
146
+ return_prefix_z=True,
147
+ )
148
+ model_time = time.monotonic() - start_time
149
+
150
+ outputs = {
151
+ "state": inputs["state"],
152
+ "actions": actions,
153
+ }
154
+ outputs = jax.tree.map(lambda x: np.asarray(x[0, ...]), outputs)
155
+ prefix_z = np.asarray(prefix_z[0, ...])
156
+
157
+ outputs = self._output_transform(outputs)
158
+ outputs["prefix_z"] = prefix_z
159
+ outputs["policy_timing"] = {
160
+ "infer_ms": model_time * 1000,
161
+ }
162
+ return outputs
163
+
164
+ @property
165
+ def metadata(self) -> dict[str, Any]:
166
+ return self._metadata
167
+
168
+
169
+ class PolicyRecorder(_base_policy.BasePolicy):
170
+ """Records the policy's behavior to disk."""
171
+
172
+ def __init__(self, policy: _base_policy.BasePolicy, record_dir: str):
173
+ self._policy = policy
174
+
175
+ logging.info(f"Dumping policy records to: {record_dir}")
176
+ self._record_dir = pathlib.Path(record_dir)
177
+ self._record_dir.mkdir(parents=True, exist_ok=True)
178
+ self._record_step = 0
179
+
180
+ @override
181
+ def infer(self, obs: dict) -> dict: # type: ignore[misc]
182
+ results = self._policy.infer(obs)
183
+
184
+ data = {"inputs": obs, "outputs": results}
185
+ data = flax.traverse_util.flatten_dict(data, sep="/")
186
+
187
+ output_path = self._record_dir / f"step_{self._record_step}"
188
+ self._record_step += 1
189
+
190
+ np.save(output_path, np.asarray(data))
191
+ return results