XiangpengYang commited on
Commit
b2cec57
·
1 Parent(s): 3287fba

build: add OpenPI runtime for UR Space

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. README.md +1 -1
  2. openpi_runtime/openpi/__init__.py +0 -0
  3. openpi_runtime/openpi/conftest.py +17 -0
  4. openpi_runtime/openpi/models/__init__.py +0 -0
  5. openpi_runtime/openpi/models/gemma.py +459 -0
  6. openpi_runtime/openpi/models/gemma_fast.py +437 -0
  7. openpi_runtime/openpi/models/lora.py +148 -0
  8. openpi_runtime/openpi/models/model.py +332 -0
  9. openpi_runtime/openpi/models/pi0.py +279 -0
  10. openpi_runtime/openpi/models/pi0_config.py +117 -0
  11. openpi_runtime/openpi/models/pi0_fast.py +313 -0
  12. openpi_runtime/openpi/models/siglip.py +373 -0
  13. openpi_runtime/openpi/models/tokenizer.py +371 -0
  14. openpi_runtime/openpi/models/utils/fsq_tokenizer.py +472 -0
  15. openpi_runtime/openpi/models/vit.py +307 -0
  16. openpi_runtime/openpi/models_pytorch/gemma_pytorch.py +280 -0
  17. openpi_runtime/openpi/models_pytorch/pi0_pytorch.py +462 -0
  18. openpi_runtime/openpi/models_pytorch/preprocessing_pytorch.py +173 -0
  19. openpi_runtime/openpi/models_pytorch/transformers_replace/models/gemma/configuration_gemma.py +173 -0
  20. openpi_runtime/openpi/models_pytorch/transformers_replace/models/gemma/modeling_gemma.py +862 -0
  21. openpi_runtime/openpi/models_pytorch/transformers_replace/models/paligemma/modeling_paligemma.py +622 -0
  22. openpi_runtime/openpi/models_pytorch/transformers_replace/models/siglip/check.py +4 -0
  23. openpi_runtime/openpi/models_pytorch/transformers_replace/models/siglip/modeling_siglip.py +1237 -0
  24. openpi_runtime/openpi/policies/aloha_policy.py +202 -0
  25. openpi_runtime/openpi/policies/droid_policy.py +81 -0
  26. openpi_runtime/openpi/policies/libero_policy.py +100 -0
  27. openpi_runtime/openpi/policies/policy.py +135 -0
  28. openpi_runtime/openpi/policies/policy_config.py +94 -0
  29. openpi_runtime/openpi/policies/ur_policy.py +56 -0
  30. openpi_runtime/openpi/py.typed +0 -0
  31. openpi_runtime/openpi/serving/websocket_policy_server.py +90 -0
  32. openpi_runtime/openpi/shared/__init__.py +0 -0
  33. openpi_runtime/openpi/shared/array_typing.py +89 -0
  34. openpi_runtime/openpi/shared/download.py +216 -0
  35. openpi_runtime/openpi/shared/image_tools.py +126 -0
  36. openpi_runtime/openpi/shared/nnx_utils.py +69 -0
  37. openpi_runtime/openpi/shared/normalize.py +146 -0
  38. openpi_runtime/openpi/training/checkpoints.py +159 -0
  39. openpi_runtime/openpi/training/config.py +1070 -0
  40. openpi_runtime/openpi/training/data_loader.py +540 -0
  41. openpi_runtime/openpi/training/droid_rlds_dataset.py +248 -0
  42. openpi_runtime/openpi/training/misc/polaris_config.py +225 -0
  43. openpi_runtime/openpi/training/misc/roboarena_config.py +116 -0
  44. openpi_runtime/openpi/training/optimizer.py +109 -0
  45. openpi_runtime/openpi/training/sharding.py +102 -0
  46. openpi_runtime/openpi/training/utils.py +38 -0
  47. openpi_runtime/openpi/training/weight_loaders.py +104 -0
  48. openpi_runtime/openpi/transforms.py +460 -0
  49. openpi_runtime/openpi_client/__init__.py +1 -0
  50. openpi_runtime/openpi_client/action_chunk_broker.py +50 -0
README.md CHANGED
@@ -5,7 +5,7 @@ colorFrom: red
5
  colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
- python_version: '3.12'
9
  app_file: app.py
10
  pinned: false
11
  ---
 
5
  colorTo: yellow
6
  sdk: gradio
7
  sdk_version: 6.20.0
8
+ python_version: '3.11'
9
  app_file: app.py
10
  pinned: false
11
  ---
openpi_runtime/openpi/__init__.py ADDED
File without changes
openpi_runtime/openpi/conftest.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+
3
+ import pynvml
4
+ import pytest
5
+
6
+
7
+ def set_jax_cpu_backend_if_no_gpu() -> None:
8
+ try:
9
+ pynvml.nvmlInit()
10
+ pynvml.nvmlShutdown()
11
+ except pynvml.NVMLError:
12
+ # No GPU found.
13
+ os.environ["JAX_PLATFORMS"] = "cpu"
14
+
15
+
16
+ def pytest_configure(config: pytest.Config) -> None:
17
+ set_jax_cpu_backend_if_no_gpu()
openpi_runtime/openpi/models/__init__.py ADDED
File without changes
openpi_runtime/openpi/models/gemma.py ADDED
@@ -0,0 +1,459 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """Gemma adaptation for Pi, taken from big_vision.
16
+
17
+ We follow this einsum axis naming convention:
18
+ B: batch
19
+ T: query length
20
+ S: k/v length
21
+ N: num query heads
22
+ K: num k/v heads
23
+ G: num query heads per k/v head
24
+ H: head dim
25
+ D: d_model ("features")
26
+ """
27
+
28
+ from collections.abc import Sequence
29
+ import dataclasses
30
+ from typing import Literal, TypeAlias
31
+
32
+ import einops
33
+ import flax.linen as nn
34
+ import jax
35
+ import jax.numpy as jnp
36
+
37
+ import openpi.models.lora as lora
38
+ import openpi.shared.array_typing as at
39
+ import openpi.training.sharding as sharding
40
+
41
+ PALIGEMMA_VOCAB_SIZE = 257_152
42
+
43
+
44
+ @dataclasses.dataclass
45
+ class Config:
46
+ width: int
47
+ depth: int
48
+ mlp_dim: int
49
+ num_heads: int
50
+ num_kv_heads: int
51
+ head_dim: int
52
+ lora_configs: dict[str, lora.LoRAConfig] = dataclasses.field(default_factory=dict)
53
+
54
+
55
+ Variant = Literal["dummy", "gemma_300m", "gemma_300m_lora", "gemma_2b", "gemma_2b_lora"]
56
+
57
+
58
+ def get_config(variant: Variant) -> Config:
59
+ """Returns config for specified gemma variant."""
60
+ if variant == "dummy":
61
+ return Config(
62
+ width=64,
63
+ depth=4,
64
+ mlp_dim=128,
65
+ num_heads=8,
66
+ num_kv_heads=1,
67
+ head_dim=16,
68
+ )
69
+ if variant == "gemma_300m":
70
+ # 311M params
71
+ return Config(
72
+ width=1024,
73
+ depth=18,
74
+ mlp_dim=4096,
75
+ num_heads=8,
76
+ num_kv_heads=1,
77
+ head_dim=256,
78
+ )
79
+ if variant == "gemma_2b":
80
+ return Config(
81
+ width=2048,
82
+ depth=18,
83
+ mlp_dim=16_384,
84
+ num_heads=8,
85
+ num_kv_heads=1,
86
+ head_dim=256,
87
+ )
88
+ if variant == "gemma_2b_lora":
89
+ return Config(
90
+ width=2048,
91
+ depth=18,
92
+ mlp_dim=16_384,
93
+ num_heads=8,
94
+ num_kv_heads=1,
95
+ head_dim=256,
96
+ lora_configs={"attn": lora.LoRAConfig(rank=16, alpha=16.0), "ffn": lora.LoRAConfig(rank=16, alpha=16.0)},
97
+ )
98
+ if variant == "gemma_300m_lora":
99
+ # 311M params
100
+ return Config(
101
+ width=1024,
102
+ depth=18,
103
+ mlp_dim=4096,
104
+ num_heads=8,
105
+ num_kv_heads=1,
106
+ head_dim=256,
107
+ lora_configs={"attn": lora.LoRAConfig(rank=32, alpha=32.0), "ffn": lora.LoRAConfig(rank=32, alpha=32.0)},
108
+ )
109
+ raise ValueError(f"Unknown variant: {variant}")
110
+
111
+
112
+ @at.typecheck
113
+ class RMSNorm(nn.Module):
114
+ @nn.compact
115
+ def __call__(self, x, cond):
116
+ dtype = x.dtype # original dtype, could be half-precision
117
+ var = jnp.mean(jnp.square(x.astype(jnp.float32)), axis=-1, keepdims=True) # compute variance in float32
118
+ normed_inputs = jnp.asarray(x * jnp.reciprocal(jnp.sqrt(var + 1e-06))) # compute normalization in float32
119
+ if cond is None:
120
+ # regular RMSNorm
121
+ scale = self.param("scale", nn.initializers.zeros_init(), (x.shape[-1]))
122
+ normed_inputs = normed_inputs * (
123
+ 1 + scale
124
+ ) # scale by learned parameter in float32 (matches Flax implementation)
125
+ return normed_inputs.astype(dtype), None # return in original dtype
126
+
127
+ # adaptive RMSNorm
128
+ modulation = nn.Dense(x.shape[-1] * 3, kernel_init=nn.initializers.zeros, dtype=dtype)(cond)
129
+ scale, shift, gate = jnp.split(modulation[:, None, :], 3, axis=-1)
130
+ normed_inputs = normed_inputs * (1 + scale) + shift # scale and shift in float32
131
+ return normed_inputs.astype(dtype), gate
132
+
133
+
134
+ @at.typecheck
135
+ class Embedder(nn.Module):
136
+ """Embedder module."""
137
+
138
+ vocab_size: int
139
+ embed_dim: int
140
+
141
+ def setup(self):
142
+ self.input_embedding_table = self.param(
143
+ "input_embedding",
144
+ nn.initializers.normal(),
145
+ (self.vocab_size, self.embed_dim),
146
+ )
147
+
148
+ def encode(self, x):
149
+ x = self.input_embedding_table[(x,)]
150
+ x *= jnp.sqrt(self.embed_dim).astype(x.dtype)
151
+ return x
152
+
153
+ def decode(self, x):
154
+ return jnp.dot(x, self.input_embedding_table.T)
155
+
156
+
157
+ @at.typecheck
158
+ class Attention(nn.Module):
159
+ """Attention module."""
160
+
161
+ configs: Sequence[Config]
162
+
163
+ @nn.compact
164
+ def __call__(self, xs, positions, attn_mask, kv_cache):
165
+ # all experts must share the same head dim, num heads, and num kv heads for self-attention to work
166
+ assert all(config.head_dim == self.configs[0].head_dim for config in self.configs)
167
+ assert all(config.num_heads == self.configs[0].num_heads for config in self.configs)
168
+ assert all(config.num_kv_heads == self.configs[0].num_kv_heads for config in self.configs)
169
+
170
+ dtype = next(x.dtype for x in xs if x is not None) # original dtype, could be half-precision
171
+
172
+ qkvs = []
173
+ for i, (x, config) in enumerate(zip(xs, self.configs, strict=True)):
174
+ if x is None:
175
+ continue
176
+ if config.num_kv_heads == config.num_heads:
177
+ qkv_einsum = lora.Einsum(
178
+ shape=(3, config.num_heads, config.width, config.head_dim),
179
+ name=_name("qkv_einsum", i),
180
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0, 1)),
181
+ lora_config=config.lora_configs.get("attn"),
182
+ )
183
+ qkvs.append(qkv_einsum("BSD,3KDH->3BSKH", x))
184
+ else:
185
+ q_einsum = lora.Einsum(
186
+ shape=(config.num_heads, config.width, config.head_dim),
187
+ name=_name("q_einsum", i),
188
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
189
+ lora_config=config.lora_configs.get("attn"),
190
+ )
191
+ q = q_einsum("BTD,NDH->BTNH", x)
192
+ kv_einsum = lora.Einsum(
193
+ shape=(2, config.num_kv_heads, config.width, config.head_dim),
194
+ name=_name("kv_einsum", i),
195
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0, 1)),
196
+ lora_config=config.lora_configs.get("attn"),
197
+ )
198
+ k, v = kv_einsum("BSD,2KDH->2BSKH", x)
199
+ qkvs.append((q, k, v))
200
+
201
+ q, k, v = (jnp.concatenate(y, axis=1) for y in zip(*qkvs, strict=True))
202
+
203
+ q = _apply_rope(q, positions=positions)
204
+ q *= self.configs[0].head_dim ** -0.5
205
+
206
+ k = _apply_rope(k, positions=positions)
207
+
208
+ # should still be half-precision here (if input was half-precision)
209
+ assert q.dtype == k.dtype == v.dtype == dtype
210
+
211
+ if kv_cache is not None:
212
+ cache_k, cache_v = kv_cache
213
+ k = jnp.concatenate([cache_k, k], axis=1)
214
+ v = jnp.concatenate([cache_v, v], axis=1)
215
+
216
+ q = einops.rearrange(q, "B T (K G) H -> B T K G H", K=self.configs[0].num_kv_heads)
217
+ logits = jnp.einsum("BTKGH,BSKH->BKGTS", q, k, preferred_element_type=jnp.float32)
218
+
219
+ if attn_mask.shape != (q.shape[0], 1, q.shape[1], k.shape[1]):
220
+ raise ValueError(
221
+ f"Attention mask with shape {attn_mask.shape} but shapes for q and k are: {q.shape} and {k.shape}"
222
+ )
223
+
224
+ # big_neg = jnp.finfo(logits.dtype).min
225
+ big_neg = -2.3819763e38 # See gemma/modules.py
226
+ masked_logits = jnp.where(attn_mask[:, :, None, :, :], logits, big_neg)
227
+
228
+ probs = jax.nn.softmax(masked_logits, axis=-1).astype(dtype)
229
+
230
+ encoded = jnp.einsum("BKGTS,BSKH->BTKGH", probs, v)
231
+ encoded = einops.rearrange(encoded, "B T K G H -> B T (K G) H")
232
+
233
+ out = []
234
+ start = 0
235
+ for i, (x, config) in enumerate(zip(xs, self.configs, strict=True)):
236
+ if x is not None:
237
+ end = start + x.shape[1]
238
+ out_einsum = lora.Einsum(
239
+ shape=(config.num_heads, config.head_dim, config.width),
240
+ name=_name("attn_vec_einsum", i),
241
+ init_fn=nn.initializers.lecun_normal(in_axis=(-3, -2), out_axis=-1),
242
+ lora_config=config.lora_configs.get("attn"),
243
+ )
244
+ out.append(out_einsum("BTNH,NHD->BTD", encoded[:, start:end]))
245
+ start = end
246
+ else:
247
+ out.append(None)
248
+
249
+ return out, (k, v)
250
+
251
+
252
+ @at.typecheck
253
+ class FeedForward(nn.Module):
254
+ """Feed forward module."""
255
+
256
+ features: int
257
+ hidden_dim: int
258
+
259
+ @nn.compact
260
+ def __call__(self, x):
261
+ dtype = x.dtype # original dtype, could be half-precision
262
+ w_gating = self.param(
263
+ "gating_einsum",
264
+ nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
265
+ (2, self.features, self.hidden_dim),
266
+ ).astype(dtype)
267
+ ff_gate = jnp.dot(x, w_gating[0])
268
+ gate_value = nn.gelu(ff_gate)
269
+
270
+ ff1 = jnp.dot(x, w_gating[1])
271
+ activations = gate_value * ff1
272
+
273
+ w_linear = self.param(
274
+ "linear",
275
+ nn.initializers.lecun_normal(in_axis=-2, out_axis=-1),
276
+ (self.hidden_dim, self.features),
277
+ ).astype(dtype)
278
+ outputs = jnp.dot(activations, w_linear)
279
+ assert outputs.dtype == dtype
280
+ return outputs
281
+
282
+
283
+ @at.typecheck
284
+ class Block(nn.Module):
285
+ """Transformer block."""
286
+
287
+ configs: tuple[Config, ...]
288
+
289
+ dropout: float = 0.0
290
+ dropout_bdims: tuple[int, ...] = ()
291
+
292
+ @nn.compact
293
+ def __call__(self, xs, kv_cache, positions, attn_mask, adarms_cond, deterministic=True): # noqa: FBT002
294
+ xs = sharding.activation_sharding_constraint(xs)
295
+ drop = nn.Dropout(self.dropout, self.dropout_bdims) if self.dropout else lambda x, _: x
296
+
297
+ attn = Attention(configs=self.configs, name="attn")
298
+
299
+ pre_attn = []
300
+ gates = []
301
+ for i, x in enumerate(xs):
302
+ if x is not None:
303
+ x, gate = RMSNorm(name=_name("pre_attention_norm", i))(x, adarms_cond[i]) # noqa: PLW2901
304
+ pre_attn.append(x)
305
+ gates.append(gate if x is not None else None)
306
+
307
+ pre_attn = sharding.activation_sharding_constraint(pre_attn)
308
+ post_attn, kv_cache = attn(pre_attn, positions, attn_mask, kv_cache)
309
+ post_attn = jax.tree.map(lambda x: drop(x, deterministic), post_attn)
310
+ post_attn = sharding.activation_sharding_constraint(post_attn)
311
+ xs = [_gated_residual(x, y, gate) for x, y, gate in zip(xs, post_attn, gates, strict=True)]
312
+ xs = sharding.activation_sharding_constraint(xs)
313
+
314
+ out = []
315
+ gates = []
316
+ for i, (x, config) in enumerate(zip(xs, self.configs, strict=True)):
317
+ if x is not None:
318
+ x, gate = RMSNorm(name=_name("pre_ffw_norm", i))(x, adarms_cond[i]) # noqa: PLW2901
319
+ x = lora.FeedForward( # noqa: PLW2901
320
+ features=config.width,
321
+ hidden_dim=config.mlp_dim,
322
+ name=_name("mlp", i),
323
+ lora_config=config.lora_configs.get("ffn"),
324
+ )(x)
325
+ out.append(x)
326
+ gates.append(gate if x is not None else None)
327
+
328
+ out = sharding.activation_sharding_constraint(out)
329
+ out = jax.tree.map(lambda x: drop(x, deterministic), out)
330
+ xs = [_gated_residual(x, y, gate) for x, y, gate in zip(xs, out, gates, strict=True)]
331
+ xs = sharding.activation_sharding_constraint(xs)
332
+
333
+ return xs, kv_cache
334
+
335
+
336
+ KVCache: TypeAlias = tuple[at.Float[at.Array, "l b _t _k _h"], at.Float[at.Array, "l b _t _v _h"]]
337
+
338
+
339
+ @at.typecheck
340
+ class Module(nn.Module):
341
+ """Transformer model, supporting a mixture of different weights for different tokens."""
342
+
343
+ configs: Sequence[Config] # list of configs, one for each expert
344
+ embed_dtype: str
345
+
346
+ dropout: float = 0.0
347
+ dropout_bdims: tuple[int, ...] = () # Every float is dropped independently.
348
+ adarms: bool = False
349
+
350
+ def setup(self):
351
+ # all experts must have the same depth
352
+ assert all(config.depth == self.configs[0].depth for config in self.configs)
353
+
354
+ self.embedder = Embedder(
355
+ vocab_size=PALIGEMMA_VOCAB_SIZE,
356
+ embed_dim=self.configs[0].width, # embedder for first expert only
357
+ name="embedder",
358
+ )
359
+ block_cls = nn.remat(
360
+ Block,
361
+ prevent_cse=False,
362
+ static_argnums=(5,), # 0=self, 6=deterministic
363
+ policy=jax.checkpoint_policies.nothing_saveable,
364
+ )
365
+ self.layers = nn.scan(
366
+ block_cls,
367
+ variable_axes={"params": 0},
368
+ split_rngs={"params": True, "dropout": True},
369
+ in_axes=(
370
+ 0,
371
+ nn.broadcast,
372
+ nn.broadcast,
373
+ nn.broadcast,
374
+ nn.broadcast,
375
+ ), # 0=kv_cache, 1=positions, 2=mask, 3=adarms_cond, 4=deterministic
376
+ length=self.configs[0].depth,
377
+ )(
378
+ configs=self.configs,
379
+ dropout=self.dropout,
380
+ dropout_bdims=self.dropout_bdims,
381
+ )
382
+ self.final_norms = [RMSNorm(name=_name("final_norm", i)) for i in range(len(self.configs))]
383
+
384
+ @at.typecheck
385
+ def embed(self, tokens: at.Int[at.Array, "b t"]) -> at.Float[at.Array, "b t d"]:
386
+ return self.embedder.encode(tokens).astype(self.embed_dtype)
387
+
388
+ @at.typecheck
389
+ def __call__(
390
+ self,
391
+ # list of token arrays, one for each expert, or None if that expert should not be run
392
+ embedded: Sequence[at.Float[at.Array, "b _t _d"] | None],
393
+ positions: at.Int[at.Array, "b t"],
394
+ mask: at.Bool[at.Array, "b t s"],
395
+ adarms_cond: Sequence[at.Float[at.Array, "b _d"] | None] | None = None,
396
+ *,
397
+ kv_cache: KVCache | None = None,
398
+ deterministic: bool = True,
399
+ ) -> tuple[Sequence[at.Float[at.Array, "b _t _d"] | None], KVCache]:
400
+ embedded = jax.tree.map(lambda e: e.astype(self.embed_dtype), embedded)
401
+ mask = jnp.asarray(mask)[:, None, :, :]
402
+ if adarms_cond is None:
403
+ adarms_cond = [None] * len(self.configs)
404
+
405
+ embedded, kv_cache = self.layers(embedded, kv_cache, positions, mask, adarms_cond, deterministic)
406
+
407
+ assert all(e.dtype == jnp.dtype(self.embed_dtype) for e in embedded if e is not None)
408
+
409
+ return [
410
+ f(e, a)[0] if e is not None else e for f, e, a in zip(self.final_norms, embedded, adarms_cond, strict=True)
411
+ ], kv_cache
412
+
413
+ def init(self, use_adarms: Sequence[bool]):
414
+ """Convenience method for initializing all parameters, necessary due to the quirks of linen."""
415
+ self.embed(jnp.zeros((1, 1), dtype=jnp.int32))
416
+ self(
417
+ [jnp.zeros((1, 1, c.width)) for c in self.configs],
418
+ jnp.zeros((1, len(self.configs)), dtype=jnp.int32),
419
+ jnp.zeros((1, len(self.configs), len(self.configs)), dtype=bool),
420
+ adarms_cond=[jnp.zeros((1, c.width)) if u else None for u, c in zip(use_adarms, self.configs, strict=True)],
421
+ )
422
+
423
+
424
+ def _apply_rope(x, *, positions, max_wavelength=10_000):
425
+ """Applies RoPE positions [B, L] to x [B, L, H, D]."""
426
+ freq_exponents = (2.0 / x.shape[-1]) * jnp.arange(x.shape[-1] // 2, dtype=jnp.float32)
427
+ timescale = max_wavelength**freq_exponents
428
+ radians = positions[..., None] / timescale[None, None, :]
429
+ radians = radians[..., None, :]
430
+ assert radians.dtype == jnp.float32
431
+ # radians.shape = [...,L,1,d=D/2]
432
+ sin, cos = jnp.sin(radians), jnp.cos(radians)
433
+ x1, x2 = jnp.split(x, 2, axis=-1)
434
+ res = jnp.concatenate([x1 * cos - x2 * sin, x2 * cos + x1 * sin], axis=-1)
435
+ assert res.dtype == jnp.float32
436
+ # The original bigvision impl allows RoPE to upcast to float32. It is then immediately downcast again to the cache
437
+ # dtype when in inference mode (but not in training mode). I don't think any of this was intentional. Based on the
438
+ # original DeepMind impl, as well as the widely-used transformers impl, it is ok to always downcast back to bfloat16
439
+ # here.
440
+ return res.astype(x.dtype)
441
+
442
+
443
+ def _name(name, i):
444
+ # we name layers like this because we want the first expert's weights to have no suffix (e.g., "attn"), so that they
445
+ # can be loaded seamlessly from the existing PaliGemma checkpoint. subsequent experts will have a suffix (e.g.,
446
+ # "attn_1") and their weights will be initialized from scratch. in practice, we only use two experts -- PaliGemma,
447
+ # and the action expert.
448
+ if i == 0:
449
+ return name
450
+ return f"{name}_{i}"
451
+
452
+
453
+ def _gated_residual(x, y, gate):
454
+ assert (x is None) == (y is None)
455
+ if x is None:
456
+ return None
457
+ if gate is None:
458
+ return x + y
459
+ return x + y * gate
openpi_runtime/openpi/models/gemma_fast.py ADDED
@@ -0,0 +1,437 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """
16
+ Gemma model implementation from big_vision/models/ppp/gemma.py (with small modifications for NNX compatibility)
17
+ Used for FAST autoregressive policies.
18
+ """
19
+
20
+ import dataclasses
21
+ from typing import Literal, TypeAlias
22
+
23
+ import einops
24
+ import flax.linen as nn
25
+ import jax
26
+ import jax.numpy as jnp
27
+ import ml_collections
28
+
29
+ import openpi.models.lora as lora
30
+ import openpi.shared.array_typing as at
31
+
32
+ Variant = Literal["gemma_2b", "gemma_2b_lora"]
33
+
34
+
35
+ def get_config(variant):
36
+ """Returns config for specified gemma variant."""
37
+ if variant == "gemma_2b":
38
+ return ml_collections.ConfigDict(
39
+ {
40
+ "variant": variant,
41
+ "width": 2048,
42
+ "depth": 18,
43
+ "mlp_dim": 16_384,
44
+ "num_heads": 8,
45
+ "num_kv_heads": 1,
46
+ "head_dim": 256,
47
+ "norm_eps": 1e-6,
48
+ "vocab_size": 257_152,
49
+ "scan": True,
50
+ "remat_policy": "nothing_saveable",
51
+ }
52
+ )
53
+ if variant == "gemma_2b_lora":
54
+ return ml_collections.ConfigDict(
55
+ {
56
+ "variant": variant,
57
+ "width": 2048,
58
+ "depth": 18,
59
+ "mlp_dim": 16_384,
60
+ "num_heads": 8,
61
+ "num_kv_heads": 1,
62
+ "head_dim": 256,
63
+ "norm_eps": 1e-6,
64
+ "vocab_size": 257_152,
65
+ "scan": True,
66
+ "remat_policy": "nothing_saveable",
67
+ "lora_configs": {
68
+ "attn": lora.LoRAConfig(rank=16, alpha=16.0),
69
+ "ffn": lora.LoRAConfig(rank=16, alpha=16.0),
70
+ },
71
+ }
72
+ )
73
+ raise ValueError(f"Unknown variant: {variant}")
74
+
75
+
76
+ @at.typecheck
77
+ class Einsum(nn.Module):
78
+ shape: tuple[int, ...]
79
+
80
+ @nn.compact
81
+ def __call__(self, eqn, x):
82
+ dtype = x.dtype # original dtype, could be half-precision
83
+ w = self.param("w", nn.initializers.zeros_init(), self.shape).astype(dtype)
84
+ return jnp.einsum(eqn, x, w)
85
+
86
+
87
+ @at.typecheck
88
+ class RMSNorm(nn.Module):
89
+ @nn.compact
90
+ def __call__(self, x):
91
+ dtype = x.dtype # original dtype, could be half-precision
92
+ scale = self.param("scale", nn.initializers.zeros_init(), (x.shape[-1]))
93
+ var = jnp.mean(jnp.square(x.astype(jnp.float32)), axis=-1, keepdims=True) # compute variance in float32
94
+ normed_inputs = jnp.asarray(x * jnp.reciprocal(jnp.sqrt(var + 1e-06))) # compute normalization in float32
95
+ normed_inputs = normed_inputs * (
96
+ 1 + scale
97
+ ) # scale by learned parameter in float32 (matches Flax implementation)
98
+ return normed_inputs.astype(dtype) # return in original dtype
99
+
100
+
101
+ @at.typecheck
102
+ class Embedder(nn.Module):
103
+ """Embedder module."""
104
+
105
+ vocab_size: int
106
+ embed_dim: int
107
+
108
+ def setup(self):
109
+ self.input_embedding_table = self.param(
110
+ "input_embedding",
111
+ nn.initializers.zeros_init(),
112
+ (self.vocab_size, self.embed_dim),
113
+ )
114
+
115
+ def encode(self, x):
116
+ x = self.input_embedding_table[(x,)]
117
+ x *= jnp.sqrt(self.embed_dim).astype(x.dtype)
118
+ return x
119
+
120
+ def decode(self, x):
121
+ return jnp.dot(x, self.input_embedding_table.T)
122
+
123
+
124
+ @at.typecheck
125
+ class Attention(nn.Module):
126
+ """Attention module."""
127
+
128
+ num_heads: int
129
+ num_kv_heads: int
130
+ features: int
131
+ head_dim: int
132
+
133
+ cache_dtype: str | None = None
134
+
135
+ lora_config: lora.LoRAConfig | None = None
136
+
137
+ def setup(self):
138
+ if self.num_kv_heads == self.num_heads:
139
+ self.qkv_einsum = lora.Einsum(
140
+ shape=(3, self.num_heads, self.features, self.head_dim),
141
+ name="qkv_einsum",
142
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0, 1)),
143
+ lora_config=self.lora_config,
144
+ )
145
+ else:
146
+ self.q_einsum = lora.Einsum(
147
+ shape=(self.num_heads, self.features, self.head_dim),
148
+ name="q_einsum",
149
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
150
+ lora_config=self.lora_config,
151
+ )
152
+ self.kv_einsum = lora.Einsum(
153
+ shape=(2, self.num_kv_heads, self.features, self.head_dim),
154
+ name="kv_einsum",
155
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0, 1)),
156
+ lora_config=self.lora_config,
157
+ )
158
+ self.attn_vec_einsum = lora.Einsum(
159
+ shape=(self.num_heads, self.head_dim, self.features),
160
+ name="attn_vec_einsum",
161
+ init_fn=nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
162
+ lora_config=self.lora_config,
163
+ )
164
+
165
+ def _init_cache(self, k, v, cache_size):
166
+ """Initialize KV cache"""
167
+ prefill_len = k.shape[1]
168
+ pad_width = ((0, 0), (0, cache_size - prefill_len), (0, 0), (0, 0))
169
+ cache_dtype = self.cache_dtype or k.dtype
170
+ k_cache = jnp.pad(k.astype(cache_dtype), pad_width)
171
+ v_cache = jnp.pad(v.astype(cache_dtype), pad_width)
172
+ idx = jnp.zeros((k.shape[0],), dtype=jnp.int32) + prefill_len
173
+ return idx, k_cache, v_cache
174
+
175
+ def _update_cache(self, k, v, idx, k_cache, v_cache):
176
+ """Update KV cache with new values"""
177
+ assert k.shape[1] == 1, "Only support kv-cache updates of length 1"
178
+ indices = (0, idx[0], 0, 0)
179
+ cache_dtype = self.cache_dtype or k.dtype
180
+ k_new = jax.lax.dynamic_update_slice(k_cache, k.astype(cache_dtype), indices)
181
+ v_new = jax.lax.dynamic_update_slice(v_cache, v.astype(cache_dtype), indices)
182
+ idx_new = idx + 1
183
+ return idx_new, k_new, v_new
184
+
185
+ @nn.compact
186
+ def __call__(self, x, positions, attn_mask, kv_cache, decode, deterministic=True): # noqa: FBT002
187
+ dtype = x.dtype # original dtype, could be half-precision
188
+ if self.num_kv_heads == self.num_heads:
189
+ q, k, v = self.qkv_einsum("BSD,3KDH->3BSKH", x)
190
+ else:
191
+ q = self.q_einsum("BTD,NDH->BTNH", x)
192
+ k, v = self.kv_einsum("BSD,2KDH->2BSKH", x)
193
+
194
+ q = _apply_rope(q, positions=positions) # promotes to float32
195
+ q *= self.head_dim**-0.5
196
+
197
+ k = _apply_rope(k, positions=positions) # promotes to float32
198
+
199
+ if kv_cache is None:
200
+ idx, k_cache, v_cache = self._init_cache(k, v, attn_mask.shape[-1])
201
+ else:
202
+ idx, k_cache, v_cache = kv_cache
203
+ idx, k_cache, v_cache = self._update_cache(k, v, idx, k_cache, v_cache)
204
+
205
+ k, v = k_cache, v_cache
206
+ kv_cache = (idx, k_cache, v_cache)
207
+
208
+ q = einops.rearrange(q, "B T (K G) H -> B T K G H", K=self.num_kv_heads)
209
+ logits = jnp.einsum("BTKGH,BSKH->BKGTS", q, k, preferred_element_type=jnp.float32)
210
+
211
+ if attn_mask.shape != (q.shape[0], 1, q.shape[1], k.shape[1]):
212
+ raise ValueError(
213
+ f"Attention mask with shape {attn_mask.shape} but shapes for q and k are: {q.shape} and {k.shape}"
214
+ )
215
+
216
+ # big_neg = jnp.finfo(logits.dtype).min
217
+ big_neg = -2.3819763e38 # See gemma/modules.py
218
+ masked_logits = jnp.where(attn_mask[:, :, None, :, :], logits, big_neg)
219
+
220
+ probs = jax.nn.softmax(masked_logits, axis=-1).astype(dtype)
221
+
222
+ encoded = jnp.einsum("BKGTS,BSKH->BTKGH", probs, v)
223
+ encoded = einops.rearrange(encoded, "B T K G H -> B T (K G) H")
224
+ return self.attn_vec_einsum("BTNH,NHD->BTD", encoded), kv_cache
225
+
226
+
227
+ @at.typecheck
228
+ class Block(nn.Module):
229
+ """Transformer block."""
230
+
231
+ num_heads: int
232
+ num_kv_heads: int
233
+ embed_dim: int
234
+ head_dim: int
235
+ hidden_dim: int
236
+
237
+ dropout: float = 0.0
238
+ dropout_bdims: tuple[int, ...] = ()
239
+ cache_dtype: str | None = None
240
+ lora_configs: ml_collections.ConfigDict = dataclasses.field(default_factory=ml_collections.ConfigDict)
241
+
242
+ def setup(self):
243
+ self.pre_attention_norm = RMSNorm()
244
+ self.attn = Attention(
245
+ num_heads=self.num_heads,
246
+ num_kv_heads=self.num_kv_heads,
247
+ features=self.embed_dim,
248
+ head_dim=self.head_dim,
249
+ cache_dtype=self.cache_dtype,
250
+ lora_config=self.lora_configs.get("attn"),
251
+ )
252
+ self.pre_ffw_norm = RMSNorm()
253
+ self.mlp = lora.FeedForward(
254
+ features=self.embed_dim, hidden_dim=self.hidden_dim, name="mlp", lora_config=self.lora_configs.get("ffn")
255
+ )
256
+ if self.dropout:
257
+ self.drop = nn.Dropout(self.dropout, self.dropout_bdims)
258
+ else:
259
+ self.drop = lambda x, _: x
260
+
261
+ def __call__(self, x, kv_cache, positions, attn_mask, decode, deterministic=True): # noqa: FBT002
262
+ x = nn.with_logical_constraint(x, ("act_batch", "act_len", "act_emb"))
263
+ inputs_normalized = self.pre_attention_norm(x)
264
+ attn_output, kv_cache = self.attn(inputs_normalized, positions, attn_mask, kv_cache, decode, deterministic)
265
+ attn_output = self.drop(attn_output, deterministic)
266
+ attn_output += x
267
+ residual = attn_output
268
+ attn_output = self.pre_ffw_norm(attn_output)
269
+ outputs = self.mlp(attn_output)
270
+ outputs = self.drop(outputs, deterministic)
271
+ outputs = residual + outputs
272
+ return outputs, kv_cache
273
+
274
+
275
+ KVCache: TypeAlias = tuple[at.Int[at.Array, " b"], at.Float[at.Array, "b _t _k _h"], at.Float[at.Array, "b _t _v _h"]]
276
+
277
+
278
+ @at.typecheck
279
+ class Module(nn.Module):
280
+ """gemma model."""
281
+
282
+ variant: str
283
+
284
+ width: int
285
+ depth: int
286
+ mlp_dim: int
287
+ num_heads: int
288
+ num_kv_heads: int
289
+ head_dim: int
290
+ norm_eps: float
291
+ vocab_size: int
292
+ embed_dtype: str
293
+
294
+ dropout: float = 0.0
295
+ dropout_bdims: tuple[int, ...] = () # Every float is dropped independently.
296
+ cache_dtype: str | None = None
297
+
298
+ scan: bool = False
299
+ remat_policy: str = "none"
300
+ lora_configs: ml_collections.ConfigDict = dataclasses.field(default_factory=ml_collections.ConfigDict)
301
+
302
+ @nn.compact
303
+ def __call__(
304
+ self,
305
+ tokens=None,
306
+ embedded_prefix=None,
307
+ embed_only=False, # noqa: FBT002
308
+ pre_logits=None,
309
+ positions=None,
310
+ mask=None,
311
+ decode=False, # noqa: FBT002
312
+ kv_cache=None,
313
+ deterministic=True, # noqa: FBT002
314
+ return_prelogits=False, # noqa: FBT002
315
+ ):
316
+ """Embed only, or complete forward pass.
317
+
318
+ Args:
319
+ tokens: Embedded, then and appended to `embedded_prefix`. Can be None.
320
+ embedded_prefix: Optional prefix that is already embedded.
321
+ embed_only: Whether to compute embeddings only.
322
+ pre_logits: If present computes logits from pre_logits and returns.
323
+ positions: Optional `[B, T]` allows to specify the absolute position of
324
+ the tokens.
325
+ mask: Optional attention mask `[B, T, S]`.
326
+ decode: Whether to use kv-cache. Caller must pass masks and positions.
327
+ deterministic: Forwarded to all dropout layers.
328
+ return_prelogits: Whether to return the pre-logits.
329
+
330
+ Returns:
331
+ If `embed_only=False`, then `(logits, out)` will be returned.
332
+ If `embed_only=True`, then the embeddings will be returned.
333
+ If `return_prelogits=True`, then the pre-logits will be returned.
334
+ """
335
+ out = {}
336
+
337
+ embedder = Embedder(vocab_size=self.vocab_size, embed_dim=self.width, name="embedder")
338
+
339
+ if pre_logits is not None:
340
+ x = out["pre_logits"] = pre_logits
341
+ logits = out["logits"] = embedder.decode(x)
342
+ return logits, out
343
+
344
+ x = []
345
+ if embedded_prefix is not None:
346
+ x.append(embedded_prefix)
347
+ if tokens is not None:
348
+ x.append(embedder.encode(tokens))
349
+
350
+ x = jnp.concatenate(x, axis=-2)
351
+ x = x.astype(self.embed_dtype)
352
+ batch_size, seq_len, width = x.shape
353
+
354
+ if embed_only:
355
+ return x
356
+
357
+ if decode:
358
+ assert positions is not None and mask is not None, ( # noqa: PT018
359
+ "Must explicitly pass positions and mask for decoding."
360
+ )
361
+
362
+ if positions is None:
363
+ positions = jnp.arange(seq_len).astype(jnp.int32)[None, :]
364
+ assert positions.shape[1] == x.shape[1], (positions.shape, x.shape)
365
+
366
+ if mask is None:
367
+ mask = nn.attention.make_causal_mask(jnp.ones([batch_size, seq_len]))
368
+ if mask.ndim == 3:
369
+ mask = mask[:, None, :, :]
370
+ cache_size = max(seq_len, mask.shape[-1])
371
+ assert mask.shape == (batch_size, 1, seq_len, cache_size), mask.shape
372
+
373
+ if self.remat_policy == "none":
374
+ block_cls = Block
375
+ else:
376
+ block_cls = nn.remat(
377
+ Block,
378
+ prevent_cse=not self.scan,
379
+ static_argnums=(5, 6), # 0=self, 5=decode, 6=deterministic
380
+ policy=getattr(jax.checkpoint_policies, self.remat_policy),
381
+ )
382
+
383
+ block_kw = {
384
+ "num_heads": self.num_heads,
385
+ "head_dim": self.head_dim,
386
+ "num_kv_heads": self.num_kv_heads,
387
+ "embed_dim": width,
388
+ "hidden_dim": self.mlp_dim,
389
+ "dropout": self.dropout,
390
+ "dropout_bdims": self.dropout_bdims,
391
+ "cache_dtype": self.cache_dtype,
392
+ "lora_configs": self.lora_configs,
393
+ }
394
+ layers = self.scope.push("layers")
395
+ blocks = [
396
+ nn.scan(
397
+ block_cls,
398
+ variable_axes={"params": 0},
399
+ split_rngs={"params": True, "dropout": True},
400
+ in_axes=(0, nn.broadcast, nn.broadcast, nn.broadcast, nn.broadcast), # 0=kv_cache, 1=positions, 2=mask
401
+ length=self.depth,
402
+ )(parent=layers, **block_kw)
403
+ ]
404
+ for block in blocks:
405
+ x, kv_cache = block(x, kv_cache, positions, mask, decode, deterministic)
406
+
407
+ assert x.dtype == jnp.dtype(self.embed_dtype) # Sanity check.
408
+ out["encoded"] = x
409
+
410
+ x = RMSNorm(name="final_norm")(x)
411
+ out["pre_logits"] = x
412
+ if return_prelogits:
413
+ return x, kv_cache, out
414
+
415
+ x = embedder.decode(x)
416
+ out["logits"] = x
417
+
418
+ return x, kv_cache, out
419
+
420
+ def init(self):
421
+ """Convenience method for initializing all parameters, necessary due to the quirks of linen."""
422
+ self(jnp.zeros((1, 1), dtype=jnp.int32))
423
+
424
+
425
+ def _apply_rope(x, *, positions, max_wavelength=10_000):
426
+ """Applies RoPE positions [B, L] to x [B, L, H, D]."""
427
+ freq_exponents = (2.0 / x.shape[-1]) * jnp.arange(x.shape[-1] // 2, dtype=jnp.float32)
428
+ timescale = max_wavelength**freq_exponents
429
+ radians = positions[..., None] / timescale[None, None, :]
430
+ radians = radians[..., None, :]
431
+ assert radians.dtype == jnp.float32
432
+ # radians.shape = [...,L,1,d=D/2]
433
+ sin, cos = jnp.sin(radians), jnp.cos(radians)
434
+ x1, x2 = jnp.split(x, 2, axis=-1)
435
+ res = jnp.concatenate([x1 * cos - x2 * sin, x2 * cos + x1 * sin], axis=-1)
436
+ assert res.dtype == jnp.float32
437
+ return res
openpi_runtime/openpi/models/lora.py ADDED
@@ -0,0 +1,148 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ import re
3
+
4
+ import flax.linen as nn
5
+ import flax.struct as struct
6
+ import jax.numpy as jnp
7
+
8
+ import openpi.shared.array_typing as at
9
+
10
+
11
+ @struct.dataclass
12
+ class LoRAConfig:
13
+ """Configuration for LoRA."""
14
+
15
+ # LoRA rank.
16
+ rank: int
17
+ # LoRA scaling factor.
18
+ alpha: float = 1.0
19
+ # Initialization function for LoRA parameters.
20
+ init_fn: nn.initializers.Initializer = nn.initializers.normal(stddev=0.01)
21
+ # Enable rank-stabilized LoRA: https://arxiv.org/pdf/2312.03732
22
+ rslora: bool = False
23
+ # Axes in the weight to apply LoRA to. Should typically be the last two axes.
24
+ axes: tuple[int, int] = (-2, -1)
25
+ # Axis label which is used by LoRA in einsum equations. Must not be present in the original equation.
26
+ label: str = "L"
27
+
28
+ @property
29
+ def scaling_value(self) -> float:
30
+ return self.alpha / math.sqrt(self.rank) if self.rslora else self.alpha / self.rank
31
+
32
+
33
+ class Einsum(nn.Module):
34
+ """Einsum with LoRA support. Can be used as a drop-in replacement for the Gemma Einsum."""
35
+
36
+ # Shape of the weight.
37
+ shape: tuple[int, ...]
38
+ # Initialization function for the weight.
39
+ init_fn: nn.initializers.Initializer = nn.initializers.zeros
40
+ # If not None, apply LoRA to the weight.
41
+ lora_config: LoRAConfig | None = None
42
+
43
+ def setup(self):
44
+ self.w = self.param("w", self.init_fn, self.shape)
45
+
46
+ if config := self.lora_config:
47
+ # Setup LoRA parameters.
48
+ shape_a, shape_b = list(self.shape), list(self.shape)
49
+ shape_a[config.axes[1]] = config.rank
50
+ shape_b[config.axes[0]] = config.rank
51
+ self.w_a = self.param("lora_a", config.init_fn, shape_a)
52
+ self.w_b = self.param("lora_b", config.init_fn, shape_b)
53
+
54
+ @nn.compact
55
+ def __call__(self, eqn: str, x):
56
+ dtype = x.dtype # original dtype, could be half-precision
57
+ result = jnp.einsum(eqn, x, self.w.astype(dtype))
58
+
59
+ if config := self.lora_config:
60
+ eqn_a, eqn_b = self._make_lora_eqns(eqn)
61
+ lora = jnp.einsum(eqn_a, x, self.w_a.astype(dtype))
62
+ lora = jnp.einsum(eqn_b, lora, self.w_b.astype(dtype))
63
+ result = result + lora * config.scaling_value
64
+
65
+ return result
66
+
67
+ def _make_lora_eqns(self, eqn: str) -> tuple[str, str]:
68
+ if "L" in eqn:
69
+ raise ValueError(f"L already in eqn: {eqn}")
70
+ if not (m := re.match("(.*),(.*)->(.*)", eqn)):
71
+ raise ValueError(f"Unsupported einsum eqn: {eqn}")
72
+ lhs, rhs, out = m.groups()
73
+
74
+ assert self.lora_config is not None
75
+ a_label, b_label = (rhs[x] for x in self.lora_config.axes)
76
+ label = self.lora_config.label
77
+
78
+ a_rhs = rhs.replace(b_label, label)
79
+ a_out = out.replace(b_label, label)
80
+ eqn_a = f"{lhs},{a_rhs}->{a_out}"
81
+
82
+ b_rhs = rhs.replace(a_label, label)
83
+ eqn_b = f"{a_out},{b_rhs}->{out}"
84
+
85
+ return eqn_a, eqn_b
86
+
87
+
88
+ class FeedForward(nn.Module):
89
+ """Feed forward module."""
90
+
91
+ features: int
92
+ hidden_dim: int
93
+ # If not None, apply LoRA to the weight.
94
+ lora_config: LoRAConfig | None = None
95
+
96
+ def setup(self):
97
+ self.w_gating = self.param(
98
+ "gating_einsum",
99
+ nn.initializers.lecun_normal(in_axis=-2, out_axis=-1, batch_axis=(0,)),
100
+ (2, self.features, self.hidden_dim),
101
+ )
102
+ self.w_linear = self.param(
103
+ "linear",
104
+ nn.initializers.lecun_normal(in_axis=-2, out_axis=-1),
105
+ (self.hidden_dim, self.features),
106
+ )
107
+ self.w_gating_lora = None
108
+ self.w_linear_lora = None
109
+ if self.lora_config:
110
+ # Setup LoRA parameters.
111
+ # TODO: follow up with a simplified init_fn api.
112
+ self.w_gating_lora = (
113
+ self.param("gating_einsum_lora_a", self.lora_config.init_fn, (2, self.features, self.lora_config.rank)),
114
+ self.param(
115
+ "gating_einsum_lora_b", self.lora_config.init_fn, (2, self.lora_config.rank, self.hidden_dim)
116
+ ),
117
+ )
118
+ self.w_linear_lora = (
119
+ self.param("linear_lora_a", self.lora_config.init_fn, (self.hidden_dim, self.lora_config.rank)),
120
+ self.param("linear_lora_b", self.lora_config.init_fn, (self.lora_config.rank, self.features)),
121
+ )
122
+
123
+ @nn.compact
124
+ def __call__(self, x):
125
+ dtype = x.dtype # original dtype, could be half-precision
126
+ ff_gate = self._dot(
127
+ x,
128
+ self.w_gating[0],
129
+ None if self.w_gating_lora is None else (self.w_gating_lora[0][0], self.w_gating_lora[1][0]),
130
+ )
131
+ gate_value = nn.gelu(ff_gate)
132
+
133
+ ff1 = self._dot(
134
+ x,
135
+ self.w_gating[1],
136
+ None if self.w_gating_lora is None else (self.w_gating_lora[0][1], self.w_gating_lora[1][1]),
137
+ )
138
+ activations = gate_value * ff1
139
+
140
+ outputs = self._dot(activations, self.w_linear, self.w_linear_lora)
141
+ assert outputs.dtype == dtype
142
+ return outputs
143
+
144
+ def _dot(self, x: at.Array, w: at.Array, lora_weights: tuple[at.Array, at.Array] | None) -> at.Array:
145
+ base = jnp.dot(x, w.astype(x.dtype))
146
+ if lora_weights is None:
147
+ return base
148
+ return base + jnp.dot(jnp.dot(x, lora_weights[0].astype(x.dtype)), lora_weights[1].astype(x.dtype))
openpi_runtime/openpi/models/model.py ADDED
@@ -0,0 +1,332 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import abc
2
+ from collections.abc import Sequence
3
+ import dataclasses
4
+ import enum
5
+ import logging
6
+ import pathlib
7
+ from typing import Generic, TypeVar
8
+
9
+ import augmax
10
+ from flax import nnx
11
+ from flax import struct
12
+ from flax import traverse_util
13
+ import jax
14
+ import jax.numpy as jnp
15
+ import numpy as np
16
+ import orbax.checkpoint as ocp
17
+ import safetensors
18
+ import torch
19
+
20
+ from openpi.models_pytorch import pi0_pytorch
21
+ from openpi.shared import image_tools
22
+ import openpi.shared.array_typing as at
23
+
24
+ logger = logging.getLogger("openpi")
25
+
26
+ # Type variable for array types (JAX arrays, PyTorch tensors, or numpy arrays)
27
+ ArrayT = TypeVar("ArrayT", bound=jax.Array | torch.Tensor | np.ndarray)
28
+
29
+
30
+ class ModelType(enum.Enum):
31
+ """Supported model types."""
32
+
33
+ PI0 = "pi0"
34
+ PI0_FAST = "pi0_fast"
35
+ PI05 = "pi05"
36
+
37
+
38
+ # The model always expects these images
39
+ IMAGE_KEYS = (
40
+ "base_0_rgb",
41
+ "left_wrist_0_rgb",
42
+ "right_wrist_0_rgb",
43
+ )
44
+
45
+
46
+ # This may need change if we release a small model.
47
+ IMAGE_RESOLUTION = (224, 224)
48
+
49
+
50
+ # Data format
51
+ #
52
+ # Data transforms produce the model input as a nested dictionary which is later converted
53
+ # into `Obesrvation` and `Actions` objects. See below.
54
+ #
55
+ # In the dictory form, this data should look like:
56
+ # {
57
+ # # Observation data.
58
+ # "image": {
59
+ # "base_0_rgb": (float32|uint8)[*b, h, w, 3], # RGB image in [-1, 1] or [0, 255]
60
+ # ... # Additional camera views
61
+ # },
62
+ # "image_mask": {
63
+ # "base_0_rgb": bool[*b], # True if image is valid
64
+ # ... # Masks for additional views
65
+ # },
66
+ # "state": float32[*b, s], # Low-dimensional robot state
67
+ # "tokenized_prompt": int32[*b, l], # Optional, tokenized language prompt
68
+ # "tokenized_prompt_mask": bool[*b, l], # Optional, mask for tokenized prompt
69
+ # "token_ar_mask": int32[*b, l], # Optional, autoregressive mask for FAST model
70
+ # "token_loss_mask": bool[*b, l], # Optional, loss mask for FAST model
71
+ #
72
+ # # Actions data.
73
+ # "actions": float32[*b ah ad]
74
+ # }
75
+ # where:
76
+ # *b = batch dimensions
77
+ # h,w = image height/width
78
+ # s = state dimension
79
+ # l = sequence length
80
+ #
81
+ @at.typecheck
82
+ @struct.dataclass
83
+ class Observation(Generic[ArrayT]):
84
+ """Holds observations, i.e., inputs to the model.
85
+
86
+ See `Observation.from_dict` to see the expected dictionary form. This is the format
87
+ that should be produced by the data transforms.
88
+ """
89
+
90
+ # Images, in [-1, 1] float32.
91
+ images: dict[str, at.Float[ArrayT, "*b h w c"]]
92
+ # Image masks, with same keys as images.
93
+ image_masks: dict[str, at.Bool[ArrayT, "*b"]]
94
+ # Low-dimensional robot state.
95
+ state: at.Float[ArrayT, "*b s"]
96
+
97
+ # Tokenized prompt.
98
+ tokenized_prompt: at.Int[ArrayT, "*b l"] | None = None
99
+ # Tokenized prompt mask.
100
+ tokenized_prompt_mask: at.Bool[ArrayT, "*b l"] | None = None
101
+
102
+ # pi0-fast model specific fields.
103
+
104
+ # Token auto-regressive mask (for FAST autoregressive model).
105
+ token_ar_mask: at.Int[ArrayT, "*b l"] | None = None
106
+ # Token loss mask (for FAST autoregressive model).
107
+ token_loss_mask: at.Bool[ArrayT, "*b l"] | None = None
108
+
109
+ @classmethod
110
+ def from_dict(cls, data: at.PyTree[ArrayT]) -> "Observation[ArrayT]":
111
+ """This method defines the mapping between unstructured data (i.e., nested dict) to the structured Observation format."""
112
+ # Ensure that tokenized_prompt and tokenized_prompt_mask are provided together.
113
+ if ("tokenized_prompt" in data) != ("tokenized_prompt_mask" in data):
114
+ raise ValueError("tokenized_prompt and tokenized_prompt_mask must be provided together.")
115
+ # If images are uint8, convert them to [-1, 1] float32.
116
+ for key in data["image"]:
117
+ if data["image"][key].dtype == np.uint8:
118
+ data["image"][key] = data["image"][key].astype(np.float32) / 255.0 * 2.0 - 1.0
119
+ elif hasattr(data["image"][key], "dtype") and data["image"][key].dtype == torch.uint8:
120
+ data["image"][key] = data["image"][key].to(torch.float32).permute(0, 3, 1, 2) / 255.0 * 2.0 - 1.0
121
+ return cls(
122
+ images=data["image"],
123
+ image_masks=data["image_mask"],
124
+ state=data["state"],
125
+ tokenized_prompt=data.get("tokenized_prompt"),
126
+ tokenized_prompt_mask=data.get("tokenized_prompt_mask"),
127
+ token_ar_mask=data.get("token_ar_mask"),
128
+ token_loss_mask=data.get("token_loss_mask"),
129
+ )
130
+
131
+ def to_dict(self) -> at.PyTree[ArrayT]:
132
+ """Convert the Observation to a nested dict."""
133
+ result = dataclasses.asdict(self)
134
+ result["image"] = result.pop("images")
135
+ result["image_mask"] = result.pop("image_masks")
136
+ return result
137
+
138
+
139
+ # Defines the format of the actions. This field is included as "actions" inside the dictionary
140
+ # produced by the data transforms.
141
+ Actions = at.Float[ArrayT, "*b ah ad"]
142
+
143
+
144
+ def preprocess_observation(
145
+ rng: at.KeyArrayLike | None,
146
+ observation: Observation,
147
+ *,
148
+ train: bool = False,
149
+ image_keys: Sequence[str] = IMAGE_KEYS,
150
+ image_resolution: tuple[int, int] = IMAGE_RESOLUTION,
151
+ ) -> Observation:
152
+ """Preprocess the observations by performing image augmentations (if train=True), resizing (if necessary), and
153
+ filling in a default image mask (if necessary).
154
+ """
155
+
156
+ if not set(image_keys).issubset(observation.images):
157
+ raise ValueError(f"images dict missing keys: expected {image_keys}, got {list(observation.images)}")
158
+
159
+ batch_shape = observation.state.shape[:-1]
160
+
161
+ out_images = {}
162
+ for key in image_keys:
163
+ image = observation.images[key]
164
+ if image.shape[1:3] != image_resolution:
165
+ logger.info(f"Resizing image {key} from {image.shape[1:3]} to {image_resolution}")
166
+ image = image_tools.resize_with_pad(image, *image_resolution)
167
+
168
+ if train:
169
+ # Convert from [-1, 1] to [0, 1] for augmax.
170
+ image = image / 2.0 + 0.5
171
+
172
+ transforms = []
173
+ if "wrist" not in key:
174
+ height, width = image.shape[1:3]
175
+ transforms += [
176
+ augmax.RandomCrop(int(width * 0.95), int(height * 0.95)),
177
+ augmax.Resize(width, height),
178
+ augmax.Rotate((-5, 5)),
179
+ ]
180
+ transforms += [
181
+ augmax.ColorJitter(brightness=0.3, contrast=0.4, saturation=0.5),
182
+ ]
183
+ sub_rngs = jax.random.split(rng, image.shape[0])
184
+ image = jax.vmap(augmax.Chain(*transforms))(sub_rngs, image)
185
+
186
+ # Back to [-1, 1].
187
+ image = image * 2.0 - 1.0
188
+
189
+ out_images[key] = image
190
+
191
+ # obtain mask
192
+ out_masks = {}
193
+ for key in out_images:
194
+ if key not in observation.image_masks:
195
+ # do not mask by default
196
+ out_masks[key] = jnp.ones(batch_shape, dtype=jnp.bool)
197
+ else:
198
+ out_masks[key] = jnp.asarray(observation.image_masks[key])
199
+
200
+ return Observation(
201
+ images=out_images,
202
+ image_masks=out_masks,
203
+ state=observation.state,
204
+ tokenized_prompt=observation.tokenized_prompt,
205
+ tokenized_prompt_mask=observation.tokenized_prompt_mask,
206
+ token_ar_mask=observation.token_ar_mask,
207
+ token_loss_mask=observation.token_loss_mask,
208
+ )
209
+
210
+
211
+ @dataclasses.dataclass(frozen=True)
212
+ class BaseModelConfig(abc.ABC):
213
+ """Configuration shared by all models. Specific models should inherit from this class, and implement the `create`
214
+ method to create the corresponding model.
215
+ """
216
+
217
+ # Action space dimension.
218
+ action_dim: int
219
+ # Action sequence length.
220
+ action_horizon: int
221
+ # Tokenized prompt maximum length.
222
+ max_token_len: int
223
+
224
+ @property
225
+ @abc.abstractmethod
226
+ def model_type(self) -> ModelType:
227
+ """The model type."""
228
+
229
+ @abc.abstractmethod
230
+ def create(self, rng: at.KeyArrayLike) -> "BaseModel":
231
+ """Create a new model, initializing parameters."""
232
+
233
+ def load(self, params: at.Params, *, remove_extra_params: bool = True) -> "BaseModel":
234
+ """Create a model with the given parameters."""
235
+ model = nnx.eval_shape(self.create, jax.random.key(0))
236
+ graphdef, state = nnx.split(model)
237
+ if remove_extra_params:
238
+ params = ocp.transform_utils.intersect_trees(state.to_pure_dict(), params)
239
+ at.check_pytree_equality(expected=state.to_pure_dict(), got=params, check_shapes=True, check_dtypes=False)
240
+ state.replace_by_pure_dict(params)
241
+ return nnx.merge(graphdef, state)
242
+
243
+ def load_pytorch(self, train_config, weight_path: str):
244
+ logger.info(f"train_config: {train_config}")
245
+ model = pi0_pytorch.PI0Pytorch(config=train_config.model)
246
+ safetensors.torch.load_model(model, weight_path)
247
+ return model
248
+
249
+ @abc.abstractmethod
250
+ def inputs_spec(self, *, batch_size: int = 1) -> tuple[Observation, Actions]:
251
+ """Returns the input specification for the model. Values are jax.ShapeDtypeStruct."""
252
+
253
+ def fake_obs(self, batch_size: int = 1) -> Observation:
254
+ observation_spec, _ = self.inputs_spec(batch_size=batch_size)
255
+ return jax.tree.map(lambda x: jnp.ones(x.shape, x.dtype), observation_spec)
256
+
257
+ def fake_act(self, batch_size: int = 1) -> Actions:
258
+ _, action_spec = self.inputs_spec(batch_size=batch_size)
259
+ return jax.tree.map(lambda x: jnp.ones(x.shape, x.dtype), action_spec)
260
+
261
+
262
+ @dataclasses.dataclass
263
+ class BaseModel(nnx.Module, abc.ABC):
264
+ """Base class for all model implementations. Specific models should inherit from this class. They should call
265
+ super().__init__() to initialize the shared attributes (action_dim, action_horizon, and max_token_len).
266
+ """
267
+
268
+ action_dim: int
269
+ action_horizon: int
270
+ max_token_len: int
271
+
272
+ @abc.abstractmethod
273
+ def compute_loss(
274
+ self,
275
+ rng: at.KeyArrayLike,
276
+ observation: Observation,
277
+ actions: Actions,
278
+ *,
279
+ train: bool = False,
280
+ ) -> at.Float[at.Array, "*b ah"]: ...
281
+
282
+ @abc.abstractmethod
283
+ def sample_actions(self, rng: at.KeyArrayLike, observation: Observation, **kwargs) -> Actions: ...
284
+
285
+
286
+ def restore_params(
287
+ params_path: pathlib.Path | str,
288
+ *,
289
+ restore_type: type[np.ndarray] | type[jax.Array] = jax.Array,
290
+ dtype: jnp.dtype | None = None,
291
+ sharding: jax.sharding.Sharding | None = None,
292
+ ) -> at.Params:
293
+ """Restores unstructured params PyTree from a checkpoint.
294
+
295
+ This works with checkpoints saved with `save_state` during openpi training (see `training/checkpoints.py`) as
296
+ well as pre-trained checkpoints released for openpi.
297
+
298
+ Args:
299
+ params_path: The local path to the checkpoint directory.
300
+ restore_type: The type to restore the params as. Can be set to `np.ndarray` to load the params as a numpy array.
301
+ dtype: The dtype to restore all params as. If not provided, will use the original dtype from the checkpoint.
302
+ sharding: The sharding to use for the params. If not provided, the params will be replicated across all devices.
303
+
304
+ Returns:
305
+ The restored params.
306
+ """
307
+ params_path = pathlib.Path(params_path).resolve() if not str(params_path).startswith("gs://") else params_path
308
+
309
+ if restore_type is jax.Array and sharding is None:
310
+ mesh = jax.sharding.Mesh(jax.devices(), ("x",))
311
+ sharding = jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
312
+
313
+ with ocp.PyTreeCheckpointer() as ckptr:
314
+ metadata = ckptr.metadata(params_path)
315
+ item = {"params": metadata["params"]}
316
+
317
+ params = ckptr.restore(
318
+ params_path,
319
+ ocp.args.PyTreeRestore(
320
+ item=item,
321
+ restore_args=jax.tree.map(
322
+ lambda _: ocp.ArrayRestoreArgs(sharding=sharding, restore_type=restore_type, dtype=dtype), item
323
+ ),
324
+ ),
325
+ )["params"]
326
+
327
+ # If the params were saved with `save_state` during openpi training, every key path will end with "value", which is
328
+ # added by `nnx.State`. We remove the "value" suffix here and always return what NNX calls a "pure dict".
329
+ flat_params = traverse_util.flatten_dict(params)
330
+ if all(kp[-1] == "value" for kp in flat_params):
331
+ flat_params = {kp[:-1]: v for kp, v in flat_params.items()}
332
+ return traverse_util.unflatten_dict(flat_params)
openpi_runtime/openpi/models/pi0.py ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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.siglip as _siglip
14
+ from openpi.shared import array_typing as at
15
+
16
+ logger = logging.getLogger("openpi")
17
+
18
+
19
+ def make_attn_mask(input_mask, mask_ar):
20
+ """Adapted from big_vision.
21
+
22
+ Tokens can attend to valid inputs tokens which have a cumulative mask_ar
23
+ smaller or equal to theirs. This way `mask_ar` bool[?B, N] can be used to
24
+ setup several types of attention, for example:
25
+
26
+ [[1 1 1 1 1 1]]: pure causal attention.
27
+
28
+ [[0 0 0 1 1 1]]: prefix-lm attention. The first 3 tokens can attend between
29
+ themselves and the last 3 tokens have a causal attention. The first
30
+ entry could also be a 1 without changing behaviour.
31
+
32
+ [[1 0 1 0 1 0 0 1 0 0]]: causal attention between 4 blocks. Tokens of a
33
+ block can attend all previous blocks and all tokens on the same block.
34
+
35
+ Args:
36
+ input_mask: bool[B, N] true if its part of the input, false if padding.
37
+ mask_ar: bool[?B, N] mask that's true where previous tokens cannot depend on
38
+ it and false where it shares the same attention mask as the previous token.
39
+ """
40
+ mask_ar = jnp.broadcast_to(mask_ar, input_mask.shape)
41
+ cumsum = jnp.cumsum(mask_ar, axis=1)
42
+ attn_mask = cumsum[:, None, :] <= cumsum[:, :, None]
43
+ valid_mask = input_mask[:, None, :] * input_mask[:, :, None]
44
+ return jnp.logical_and(attn_mask, valid_mask)
45
+
46
+
47
+ @at.typecheck
48
+ def posemb_sincos(
49
+ pos: at.Real[at.Array, " b"], embedding_dim: int, min_period: float, max_period: float
50
+ ) -> at.Float[at.Array, "b {embedding_dim}"]:
51
+ """Computes sine-cosine positional embedding vectors for scalar positions."""
52
+ if embedding_dim % 2 != 0:
53
+ raise ValueError(f"embedding_dim ({embedding_dim}) must be divisible by 2")
54
+
55
+ fraction = jnp.linspace(0.0, 1.0, embedding_dim // 2)
56
+ period = min_period * (max_period / min_period) ** fraction
57
+ sinusoid_input = jnp.einsum(
58
+ "i,j->ij",
59
+ pos,
60
+ 1.0 / period * 2 * jnp.pi,
61
+ precision=jax.lax.Precision.HIGHEST,
62
+ )
63
+ return jnp.concatenate([jnp.sin(sinusoid_input), jnp.cos(sinusoid_input)], axis=-1)
64
+
65
+
66
+ class Pi0(_model.BaseModel):
67
+ def __init__(self, config: pi0_config.Pi0Config, rngs: nnx.Rngs):
68
+ super().__init__(config.action_dim, config.action_horizon, config.max_token_len)
69
+ self.pi05 = config.pi05
70
+ paligemma_config = _gemma.get_config(config.paligemma_variant)
71
+ action_expert_config = _gemma.get_config(config.action_expert_variant)
72
+ # TODO: rewrite gemma in NNX. For now, use bridge.
73
+ llm = nnx_bridge.ToNNX(
74
+ _gemma.Module(
75
+ configs=[paligemma_config, action_expert_config],
76
+ embed_dtype=config.dtype,
77
+ adarms=config.pi05,
78
+ )
79
+ )
80
+ llm.lazy_init(rngs=rngs, method="init", use_adarms=[False, True] if config.pi05 else [False, False])
81
+ img = nnx_bridge.ToNNX(
82
+ _siglip.Module(
83
+ num_classes=paligemma_config.width,
84
+ variant="So400m/14",
85
+ pool_type="none",
86
+ scan=True,
87
+ dtype_mm=config.dtype,
88
+ )
89
+ )
90
+ img.lazy_init(next(iter(config.fake_obs().images.values())), train=False, rngs=rngs)
91
+ self.PaliGemma = nnx.Dict(llm=llm, img=img)
92
+ self.action_in_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)
93
+ if config.pi05:
94
+ self.time_mlp_in = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
95
+ self.time_mlp_out = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
96
+ else:
97
+ self.state_proj = nnx.Linear(config.action_dim, action_expert_config.width, rngs=rngs)
98
+ self.action_time_mlp_in = nnx.Linear(2 * action_expert_config.width, action_expert_config.width, rngs=rngs)
99
+ self.action_time_mlp_out = nnx.Linear(action_expert_config.width, action_expert_config.width, rngs=rngs)
100
+ self.action_out_proj = nnx.Linear(action_expert_config.width, config.action_dim, rngs=rngs)
101
+
102
+ # This attribute gets automatically set by model.train() and model.eval().
103
+ self.deterministic = True
104
+
105
+ @at.typecheck
106
+ def embed_prefix(
107
+ self, obs: _model.Observation
108
+ ) -> tuple[at.Float[at.Array, "b s emb"], at.Bool[at.Array, "b s"], at.Bool[at.Array, " s"]]:
109
+ input_mask = []
110
+ ar_mask = []
111
+ tokens = []
112
+ # embed images
113
+ for name in obs.images:
114
+ image_tokens, _ = self.PaliGemma.img(obs.images[name], train=False)
115
+
116
+ tokens.append(image_tokens)
117
+ input_mask.append(
118
+ einops.repeat(
119
+ obs.image_masks[name],
120
+ "b -> b s",
121
+ s=image_tokens.shape[1],
122
+ )
123
+ )
124
+ # image tokens attend to each other
125
+ ar_mask += [False] * image_tokens.shape[1]
126
+
127
+ # add language (aka tokenized inputs)
128
+ if obs.tokenized_prompt is not None:
129
+ tokenized_inputs = self.PaliGemma.llm(obs.tokenized_prompt, method="embed")
130
+ tokens.append(tokenized_inputs)
131
+ input_mask.append(obs.tokenized_prompt_mask)
132
+ # full attention between image and language inputs
133
+ ar_mask += [False] * tokenized_inputs.shape[1]
134
+ tokens = jnp.concatenate(tokens, axis=1)
135
+ input_mask = jnp.concatenate(input_mask, axis=1)
136
+ ar_mask = jnp.array(ar_mask)
137
+ return tokens, input_mask, ar_mask
138
+
139
+ @at.typecheck
140
+ def embed_suffix(
141
+ self, obs: _model.Observation, noisy_actions: _model.Actions, timestep: at.Float[at.Array, " b"]
142
+ ) -> tuple[
143
+ at.Float[at.Array, "b s emb"],
144
+ at.Bool[at.Array, "b s"],
145
+ at.Bool[at.Array, " s"],
146
+ at.Float[at.Array, "b emb"] | None,
147
+ ]:
148
+ input_mask = []
149
+ ar_mask = []
150
+ tokens = []
151
+ if not self.pi05:
152
+ # add a single state token
153
+ state_token = self.state_proj(obs.state)[:, None, :]
154
+ tokens.append(state_token)
155
+ input_mask.append(jnp.ones((obs.state.shape[0], 1), dtype=jnp.bool_))
156
+ # image/language inputs do not attend to state or actions
157
+ ar_mask += [True]
158
+
159
+ action_tokens = self.action_in_proj(noisy_actions)
160
+ # embed timestep using sine-cosine positional encoding with sensitivity in the range [0, 1]
161
+ time_emb = posemb_sincos(timestep, self.action_in_proj.out_features, min_period=4e-3, max_period=4.0)
162
+ if self.pi05:
163
+ # time MLP (for adaRMS)
164
+ time_emb = self.time_mlp_in(time_emb)
165
+ time_emb = nnx.swish(time_emb)
166
+ time_emb = self.time_mlp_out(time_emb)
167
+ time_emb = nnx.swish(time_emb)
168
+ action_expert_tokens = action_tokens
169
+ adarms_cond = time_emb
170
+ else:
171
+ # mix timestep + action information using an MLP (no adaRMS)
172
+ time_tokens = einops.repeat(time_emb, "b emb -> b s emb", s=self.action_horizon)
173
+ action_time_tokens = jnp.concatenate([action_tokens, time_tokens], axis=-1)
174
+ action_time_tokens = self.action_time_mlp_in(action_time_tokens)
175
+ action_time_tokens = nnx.swish(action_time_tokens)
176
+ action_time_tokens = self.action_time_mlp_out(action_time_tokens)
177
+ action_expert_tokens = action_time_tokens
178
+ adarms_cond = None
179
+ tokens.append(action_expert_tokens)
180
+ input_mask.append(jnp.ones(action_expert_tokens.shape[:2], dtype=jnp.bool_))
181
+ # image/language/state inputs do not attend to action tokens
182
+ ar_mask += [True] + ([False] * (self.action_horizon - 1))
183
+ tokens = jnp.concatenate(tokens, axis=1)
184
+ input_mask = jnp.concatenate(input_mask, axis=1)
185
+ ar_mask = jnp.array(ar_mask)
186
+ return tokens, input_mask, ar_mask, adarms_cond
187
+
188
+ @override
189
+ def compute_loss(
190
+ self, rng: at.KeyArrayLike, observation: _model.Observation, actions: _model.Actions, *, train: bool = False
191
+ ) -> at.Float[at.Array, "*b ah"]:
192
+ preprocess_rng, noise_rng, time_rng = jax.random.split(rng, 3)
193
+ observation = _model.preprocess_observation(preprocess_rng, observation, train=train)
194
+
195
+ batch_shape = actions.shape[:-2]
196
+ noise = jax.random.normal(noise_rng, actions.shape)
197
+ time = jax.random.beta(time_rng, 1.5, 1, batch_shape) * 0.999 + 0.001
198
+ time_expanded = time[..., None, None]
199
+ x_t = time_expanded * noise + (1 - time_expanded) * actions
200
+ u_t = noise - actions
201
+
202
+ # one big forward pass of prefix + suffix at once
203
+ prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)
204
+ suffix_tokens, suffix_mask, suffix_ar_mask, adarms_cond = self.embed_suffix(observation, x_t, time)
205
+ input_mask = jnp.concatenate([prefix_mask, suffix_mask], axis=1)
206
+ ar_mask = jnp.concatenate([prefix_ar_mask, suffix_ar_mask], axis=0)
207
+ attn_mask = make_attn_mask(input_mask, ar_mask)
208
+ positions = jnp.cumsum(input_mask, axis=1) - 1
209
+ (prefix_out, suffix_out), _ = self.PaliGemma.llm(
210
+ [prefix_tokens, suffix_tokens], mask=attn_mask, positions=positions, adarms_cond=[None, adarms_cond]
211
+ )
212
+ v_t = self.action_out_proj(suffix_out[:, -self.action_horizon :])
213
+
214
+ return jnp.mean(jnp.square(v_t - u_t), axis=-1)
215
+
216
+ @override
217
+ def sample_actions(
218
+ self,
219
+ rng: at.KeyArrayLike,
220
+ observation: _model.Observation,
221
+ *,
222
+ num_steps: int | at.Int[at.Array, ""] = 10,
223
+ noise: at.Float[at.Array, "b ah ad"] | None = None,
224
+ ) -> _model.Actions:
225
+ observation = _model.preprocess_observation(None, observation, train=False)
226
+ # note that we use the convention more common in diffusion literature, where t=1 is noise and t=0 is the target
227
+ # distribution. yes, this is the opposite of the pi0 paper, and I'm sorry.
228
+ dt = -1.0 / num_steps
229
+ batch_size = observation.state.shape[0]
230
+ if noise is None:
231
+ noise = jax.random.normal(rng, (batch_size, self.action_horizon, self.action_dim))
232
+
233
+ # first fill KV cache with a forward pass of the prefix
234
+ prefix_tokens, prefix_mask, prefix_ar_mask = self.embed_prefix(observation)
235
+ prefix_attn_mask = make_attn_mask(prefix_mask, prefix_ar_mask)
236
+ positions = jnp.cumsum(prefix_mask, axis=1) - 1
237
+ _, kv_cache = self.PaliGemma.llm([prefix_tokens, None], mask=prefix_attn_mask, positions=positions)
238
+
239
+ def step(carry):
240
+ x_t, time = carry
241
+ suffix_tokens, suffix_mask, suffix_ar_mask, adarms_cond = self.embed_suffix(
242
+ observation, x_t, jnp.broadcast_to(time, batch_size)
243
+ )
244
+ # `suffix_attn_mask` is shape (b, suffix_len, suffix_len) indicating how the suffix tokens can attend to each
245
+ # other
246
+ suffix_attn_mask = make_attn_mask(suffix_mask, suffix_ar_mask)
247
+ # `prefix_attn_mask` is shape (b, suffix_len, prefix_len) indicating how the suffix tokens can attend to the
248
+ # prefix tokens
249
+ prefix_attn_mask = einops.repeat(prefix_mask, "b p -> b s p", s=suffix_tokens.shape[1])
250
+ # `combined_mask` is shape (b, suffix_len, prefix_len + suffix_len) indicating how the suffix tokens (which
251
+ # generate the queries) can attend to the full prefix + suffix sequence (which generates the keys and values)
252
+ full_attn_mask = jnp.concatenate([prefix_attn_mask, suffix_attn_mask], axis=-1)
253
+ assert full_attn_mask.shape == (
254
+ batch_size,
255
+ suffix_tokens.shape[1],
256
+ prefix_tokens.shape[1] + suffix_tokens.shape[1],
257
+ )
258
+ # `positions` is shape (b, suffix_len) indicating the positions of the suffix tokens
259
+ positions = jnp.sum(prefix_mask, axis=-1)[:, None] + jnp.cumsum(suffix_mask, axis=-1) - 1
260
+
261
+ (prefix_out, suffix_out), _ = self.PaliGemma.llm(
262
+ [None, suffix_tokens],
263
+ mask=full_attn_mask,
264
+ positions=positions,
265
+ kv_cache=kv_cache,
266
+ adarms_cond=[None, adarms_cond],
267
+ )
268
+ assert prefix_out is None
269
+ v_t = self.action_out_proj(suffix_out[:, -self.action_horizon :])
270
+
271
+ return x_t + dt * v_t, time + dt
272
+
273
+ def cond(carry):
274
+ x_t, time = carry
275
+ # robust to floating-point error
276
+ return time >= -dt / 2
277
+
278
+ x_0, _ = jax.lax.while_loop(cond, step, (noise, 1.0))
279
+ return x_0
openpi_runtime/openpi/models/pi0_config.py ADDED
@@ -0,0 +1,117 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+ from typing import TYPE_CHECKING
3
+
4
+ import flax.nnx as nnx
5
+ import jax
6
+ import jax.numpy as jnp
7
+ from typing_extensions import override
8
+
9
+ from openpi.models import model as _model
10
+ import openpi.models.gemma as _gemma
11
+ from openpi.shared import array_typing as at
12
+ import openpi.shared.nnx_utils as nnx_utils
13
+
14
+ if TYPE_CHECKING:
15
+ from openpi.models.pi0 import Pi0
16
+
17
+
18
+ @dataclasses.dataclass(frozen=True)
19
+ class Pi0Config(_model.BaseModelConfig):
20
+ dtype: str = "bfloat16"
21
+ paligemma_variant: _gemma.Variant = "gemma_2b"
22
+ action_expert_variant: _gemma.Variant = "gemma_300m"
23
+
24
+ # Set the model specific defaults.
25
+ action_dim: int = 32
26
+ action_horizon: int = 50
27
+ max_token_len: int = None # type: ignore
28
+ # Pi05 has two differences from Pi0:
29
+ # - the state input is part of the discrete language tokens rather than a continuous input that is part of the suffix
30
+ # - the action expert uses adaRMSNorm to inject the flow matching timestep
31
+ pi05: bool = False
32
+ # This config option is not used directly by the model, but it is read by the ModelTransformFactory.
33
+ discrete_state_input: bool = None # type: ignore
34
+
35
+ pytorch_compile_mode: str | None = "max-autotune"
36
+
37
+ def __post_init__(self):
38
+ if self.max_token_len is None:
39
+ object.__setattr__(self, "max_token_len", 200 if self.pi05 else 48)
40
+ if self.discrete_state_input is None:
41
+ object.__setattr__(self, "discrete_state_input", self.pi05)
42
+ if self.pytorch_compile_mode is not None:
43
+ assert self.pytorch_compile_mode in [
44
+ "default",
45
+ "reduce-overhead",
46
+ "max-autotune",
47
+ "max-autotune-no-cudagraphs",
48
+ ]
49
+
50
+ @property
51
+ @override
52
+ def model_type(self) -> _model.ModelType:
53
+ if self.pi05:
54
+ return _model.ModelType.PI05
55
+ return _model.ModelType.PI0
56
+
57
+ @override
58
+ def create(self, rng: at.KeyArrayLike) -> "Pi0":
59
+ from openpi.models.pi0 import Pi0
60
+
61
+ return Pi0(self, rngs=nnx.Rngs(rng))
62
+
63
+ @override
64
+ def inputs_spec(self, *, batch_size: int = 1) -> tuple[_model.Observation, _model.Actions]:
65
+ image_spec = jax.ShapeDtypeStruct([batch_size, *_model.IMAGE_RESOLUTION, 3], jnp.float32)
66
+ image_mask_spec = jax.ShapeDtypeStruct([batch_size], jnp.bool_)
67
+
68
+ with at.disable_typechecking():
69
+ observation_spec = _model.Observation(
70
+ images={
71
+ "base_0_rgb": image_spec,
72
+ "left_wrist_0_rgb": image_spec,
73
+ "right_wrist_0_rgb": image_spec,
74
+ },
75
+ image_masks={
76
+ "base_0_rgb": image_mask_spec,
77
+ "left_wrist_0_rgb": image_mask_spec,
78
+ "right_wrist_0_rgb": image_mask_spec,
79
+ },
80
+ state=jax.ShapeDtypeStruct([batch_size, self.action_dim], jnp.float32),
81
+ tokenized_prompt=jax.ShapeDtypeStruct([batch_size, self.max_token_len], jnp.int32),
82
+ tokenized_prompt_mask=jax.ShapeDtypeStruct([batch_size, self.max_token_len], bool),
83
+ )
84
+ action_spec = jax.ShapeDtypeStruct([batch_size, self.action_horizon, self.action_dim], jnp.float32)
85
+
86
+ return observation_spec, action_spec
87
+
88
+ def get_freeze_filter(self) -> nnx.filterlib.Filter:
89
+ """Returns the freeze filter based on the model config."""
90
+ filters = []
91
+ has_lora = False
92
+ gemma_params_filter = nnx_utils.PathRegex(".*llm.*")
93
+ action_expert_params_filter = nnx_utils.PathRegex(".*llm.*_1.*")
94
+ if "lora" in self.paligemma_variant:
95
+ filters.append(
96
+ gemma_params_filter,
97
+ )
98
+ if "lora" not in self.action_expert_variant:
99
+ # If only freeze gemma params, exclude action expert params.
100
+ filters.append(
101
+ nnx.Not(action_expert_params_filter),
102
+ )
103
+ has_lora = True
104
+ elif "lora" in self.action_expert_variant:
105
+ filters.append(
106
+ action_expert_params_filter,
107
+ )
108
+ has_lora = True
109
+
110
+ if has_lora:
111
+ # If any lora is used, exclude all lora params.
112
+ filters.append(
113
+ nnx.Not(nnx_utils.PathRegex(".*lora.*")),
114
+ )
115
+ if not filters:
116
+ return nnx.Nothing
117
+ return nnx.All(*filters)
openpi_runtime/openpi/models/pi0_fast.py ADDED
@@ -0,0 +1,313 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+ import logging
3
+ from typing import Any
4
+
5
+ import einops
6
+ import flax.nnx as nnx
7
+ import flax.nnx.bridge as nnx_bridge
8
+ import jax
9
+ import jax.numpy as jnp
10
+ from typing_extensions import override
11
+
12
+ from openpi.models import model as _model
13
+ import openpi.models.gemma_fast as _gemma
14
+ import openpi.models.siglip as _siglip
15
+ from openpi.shared import array_typing as at
16
+ import openpi.shared.nnx_utils as nnx_utils
17
+
18
+ logger = logging.getLogger("openpi")
19
+
20
+ PALIGEMMA_EOS_TOKEN = 1
21
+
22
+
23
+ def make_attn_mask(input_mask, mask_ar):
24
+ """Adapted from big_vision.
25
+
26
+ Tokens can attend to valid inputs tokens which have a cumulative mask_ar
27
+ smaller or equal to theirs. This way `mask_ar` bool[?B, N] can be used to
28
+ setup several types of attention, for example:
29
+
30
+ [[1 1 1 1 1 1]]: pure causal attention.
31
+
32
+ [[0 0 0 1 1 1]]: prefix-lm attention. The first 3 tokens can attend between
33
+ themselves and the last 3 tokens have a causal attention. The first
34
+ entry could also be a 1 without changing behaviour.
35
+
36
+ [[1 0 1 0 1 0 0 1 0 0]]: causal attention between 4 blocks. Tokens of a
37
+ block can attend all previous blocks and all tokens on the same block.
38
+
39
+ Args:
40
+ input_mask: bool[B, N] true if its part of the input, false if padding.
41
+ mask_ar: bool[?B, N] mask that's true where previous tokens cannot depend on
42
+ it and false where it shares the same attention mask as the previous token.
43
+ """
44
+ mask_ar = jnp.broadcast_to(mask_ar, input_mask.shape)
45
+ cumsum = jnp.cumsum(mask_ar, axis=1)
46
+ attn_mask = cumsum[:, None, :] <= cumsum[:, :, None]
47
+ valid_mask = input_mask[:, None, :] * input_mask[:, :, None]
48
+ return jnp.logical_and(attn_mask, valid_mask)
49
+
50
+
51
+ @jax.vmap
52
+ def left_to_right_align(x, input_mask, attn_mask):
53
+ """Converts input from left-align to right-aligned."""
54
+ # Due to vmap, this is operating in a single example (not batch level).
55
+ assert x.ndim == 2
56
+ assert input_mask.ndim == 1
57
+ assert attn_mask.ndim == 2
58
+ assert x.shape[0] == input_mask.shape[0]
59
+ assert attn_mask.shape[0] == attn_mask.shape[1], attn_mask.shape
60
+ seqlen = jnp.max(input_mask * jnp.arange(input_mask.shape[0])) + 1
61
+ x = jnp.roll(x, -seqlen, axis=0)
62
+ input_mask = jnp.roll(input_mask, -seqlen, axis=0)
63
+ attn_mask = jnp.roll(attn_mask, -seqlen, axis=(0, 1))
64
+ return x, input_mask, attn_mask
65
+
66
+
67
+ def put_along_last_axis(arr, indices, values):
68
+ """Like np.put_along_axis(..., axis=-1), since jax is missing it."""
69
+ assert arr.ndim == indices.ndim == values.ndim, (arr.ndim, indices.ndim, values.ndim)
70
+ onehot = jax.nn.one_hot(indices, arr.shape[-1], dtype=values.dtype)
71
+ put_mask = jnp.einsum("...i,...in->...n", jnp.ones(values.shape, jnp.int32), onehot)
72
+ put_values = jnp.einsum("...i,...in->...n", values, onehot)
73
+ return jnp.where(put_mask, put_values, arr)
74
+
75
+
76
+ @dataclasses.dataclass(frozen=True)
77
+ class Pi0FASTConfig(_model.BaseModelConfig):
78
+ dtype: str = "bfloat16"
79
+ paligemma_variant: _gemma.Variant = "gemma_2b"
80
+
81
+ # Set the model specific defaults.
82
+ action_dim: int = 32
83
+ action_horizon: int = 32
84
+ max_token_len: int = 250
85
+
86
+ # Tokenizer for the fast model.
87
+ fast_model_tokenizer: Any | None = None
88
+ # Keyword arguments for the fast model tokenizer.
89
+ fast_model_tokenizer_kwargs: dict[str, Any] | None = None
90
+
91
+ @property
92
+ @override
93
+ def model_type(self) -> _model.ModelType:
94
+ return _model.ModelType.PI0_FAST
95
+
96
+ @override
97
+ def create(self, rng: at.KeyArrayLike) -> "Pi0FAST":
98
+ return Pi0FAST(self, rngs=nnx.Rngs(rng))
99
+
100
+ @override
101
+ def inputs_spec(self, *, batch_size: int = 1) -> tuple[_model.Observation, _model.Actions]:
102
+ image_spec = jax.ShapeDtypeStruct([batch_size, *_model.IMAGE_RESOLUTION, 3], jnp.float32)
103
+ image_mask_spec = jax.ShapeDtypeStruct([batch_size], jnp.bool_)
104
+
105
+ with at.disable_typechecking():
106
+ observation_spec = _model.Observation(
107
+ images={
108
+ "base_0_rgb": image_spec,
109
+ "base_1_rgb": image_spec,
110
+ "wrist_0_rgb": image_spec,
111
+ },
112
+ image_masks={
113
+ "base_0_rgb": image_mask_spec,
114
+ "base_1_rgb": image_mask_spec,
115
+ "wrist_0_rgb": image_mask_spec,
116
+ },
117
+ state=jax.ShapeDtypeStruct([batch_size, self.action_dim], jnp.float32),
118
+ tokenized_prompt=jax.ShapeDtypeStruct([batch_size, self.max_token_len], jnp.int32),
119
+ tokenized_prompt_mask=jax.ShapeDtypeStruct([batch_size, self.max_token_len], bool),
120
+ token_ar_mask=jax.ShapeDtypeStruct([batch_size, self.max_token_len], jnp.int32),
121
+ token_loss_mask=jax.ShapeDtypeStruct([batch_size, self.max_token_len], jnp.bool_),
122
+ )
123
+ action_spec = jax.ShapeDtypeStruct([batch_size, self.action_horizon, self.action_dim], jnp.float32)
124
+
125
+ return observation_spec, action_spec
126
+
127
+ def get_freeze_filter(self) -> nnx.filterlib.Filter:
128
+ """Returns the freeze filter based on the model config."""
129
+ if "lora" in self.paligemma_variant:
130
+ return nnx.All(nnx_utils.PathRegex(".*llm.*"), nnx.Not(nnx_utils.PathRegex(".*lora.*")))
131
+ return nnx.Nothing
132
+
133
+
134
+ class Pi0FAST(_model.BaseModel):
135
+ def __init__(self, config: Pi0FASTConfig, rngs: nnx.Rngs):
136
+ super().__init__(config.action_dim, config.action_horizon, config.max_token_len)
137
+ paligemma_config = _gemma.get_config(config.paligemma_variant)
138
+ # TODO: rewrite gemma in NNX. For now, use bridge.
139
+ llm = nnx_bridge.ToNNX(
140
+ _gemma.Module(
141
+ **paligemma_config,
142
+ embed_dtype=config.dtype,
143
+ cache_dtype=config.dtype,
144
+ )
145
+ )
146
+ llm.lazy_init(rngs=rngs, method="init")
147
+ img = nnx_bridge.ToNNX(
148
+ _siglip.Module(
149
+ num_classes=paligemma_config.width,
150
+ variant="So400m/14",
151
+ pool_type="none",
152
+ scan=True,
153
+ dtype_mm=config.dtype,
154
+ )
155
+ )
156
+ img.lazy_init(next(iter(config.fake_obs().images.values())), train=False, rngs=rngs)
157
+ self.PaliGemma = nnx.Dict(llm=llm, img=img)
158
+
159
+ @at.typecheck
160
+ def embed_inputs(
161
+ self, obs: _model.Observation
162
+ ) -> tuple[at.Float[at.Array, "b s emb"], at.Bool[at.Array, "b s"], at.Int[at.Array, "b s"]]:
163
+ input_mask = []
164
+ ar_mask = []
165
+ token_embeddings = []
166
+ # embed images
167
+ for name in obs.images:
168
+ image_token_embeddings, _ = self.PaliGemma.img(obs.images[name], train=False)
169
+
170
+ token_embeddings.append(image_token_embeddings)
171
+ input_mask.append(
172
+ einops.repeat(
173
+ obs.image_masks[name],
174
+ "b -> b s",
175
+ s=image_token_embeddings.shape[1],
176
+ )
177
+ )
178
+ # image tokens attend to each other --> AR mask = 0
179
+ ar_mask.append(0 * input_mask[-1])
180
+
181
+ # add tokenized inputs
182
+ assert obs.tokenized_prompt is not None, "Tokenized prompt is required"
183
+ assert obs.tokenized_prompt_mask is not None, "Tokenized prompt mask is required"
184
+ assert obs.token_ar_mask is not None, "Token auto-regressive mask is required"
185
+ tokenized_inputs_embeddings = self.PaliGemma.llm(obs.tokenized_prompt, embed_only=True)
186
+ token_embeddings.append(tokenized_inputs_embeddings)
187
+ input_mask.append(obs.tokenized_prompt_mask)
188
+ ar_mask.append(obs.token_ar_mask)
189
+
190
+ # return embeddings, input mask, and ar mask
191
+ return (
192
+ jnp.concatenate(token_embeddings, axis=1),
193
+ jnp.concatenate(input_mask, axis=1),
194
+ jnp.concatenate(ar_mask, axis=1),
195
+ )
196
+
197
+ @override
198
+ def compute_loss(
199
+ self, rng: at.KeyArrayLike, observation: _model.Observation, actions: _model.Actions, *, train: bool = False
200
+ ) -> at.Float[at.Array, "*b ah"]:
201
+ observation = _model.preprocess_observation(
202
+ rng, observation, train=train, image_keys=list(observation.images.keys())
203
+ )
204
+
205
+ # Compute inputs: one big forward pass of prefix + suffix at once
206
+ input_token_embeddings, input_mask, ar_mask = self.embed_inputs(observation)
207
+ attn_mask = make_attn_mask(input_mask, ar_mask)
208
+
209
+ # Compute one-hot targets: we predict *next* token, so shift the input tokens by one.
210
+ targets = jax.nn.one_hot(
211
+ observation.tokenized_prompt[:, 1:],
212
+ self.PaliGemma.llm.module.vocab_size,
213
+ )
214
+
215
+ # Each input predicts *next* token, so we don't input the last token.
216
+ pre_logits, _, _ = self.PaliGemma.llm(
217
+ embedded_prefix=input_token_embeddings[:, :-1],
218
+ mask=attn_mask[:, :-1, :-1],
219
+ return_prelogits=True,
220
+ )
221
+
222
+ # Only decode logits for the target tokens to save memory
223
+ # (decoding matmul is large because it is a seq_len x vocab_size dense layer).
224
+ logits, _ = self.PaliGemma.llm(
225
+ pre_logits=pre_logits[:, -targets.shape[1] :],
226
+ )
227
+ logp = jax.nn.log_softmax(logits, axis=-1)
228
+
229
+ # Compute CE loss on token targets
230
+ assert observation.token_loss_mask is not None, "Token loss mask is required"
231
+ loss_mask = observation.token_loss_mask[:, 1:]
232
+ token_pplx = jnp.sum(targets * logp, axis=-1)
233
+ return -jnp.sum(token_pplx * loss_mask, axis=-1) / jnp.clip(jnp.sum(loss_mask, -1), 1)
234
+
235
+ @override
236
+ def sample_actions(
237
+ self,
238
+ rng: at.KeyArrayLike,
239
+ observation: _model.Observation,
240
+ *,
241
+ max_decoding_steps: int | at.Int[at.Array, ""] = 256,
242
+ temperature: float = 0.0,
243
+ ) -> _model.Actions:
244
+ # TODO: this is a hack to get the image keys.
245
+ observation = _model.preprocess_observation(
246
+ None, observation, train=False, image_keys=list(observation.images.keys())
247
+ )
248
+
249
+ # embed inputs
250
+ prefix_token_embeddings, prefix_mask, prefix_ar_mask = self.embed_inputs(observation)
251
+ prefix_attn_mask = make_attn_mask(prefix_mask, prefix_ar_mask)
252
+
253
+ # left to right align all input token sequences
254
+ prefix_token_embeddings, prefix_mask, prefix_attn_mask = left_to_right_align(
255
+ prefix_token_embeddings, prefix_mask, prefix_attn_mask
256
+ )
257
+ prefill_size = prefix_token_embeddings.shape[1]
258
+ prefill_len = jnp.sum(prefix_mask, axis=-1)
259
+ prefix_start = prefill_size - prefill_len
260
+
261
+ # first fill KV cache with a forward pass of the prefix
262
+ # pad attention mask to set the size of the KV cache (prefill_size + max_decoding_steps)
263
+ prefix_attn_mask = jnp.pad(prefix_attn_mask, ((0, 0), (0, 0), (0, max_decoding_steps)))
264
+ prefix_positions = jnp.cumsum(prefix_mask, axis=-1) - 1
265
+ prefix_logits, kv_cache, _ = self.PaliGemma.llm(
266
+ embedded_prefix=prefix_token_embeddings, mask=prefix_attn_mask, positions=prefix_positions, decode=True
267
+ )
268
+
269
+ # prepare decoding -- final logit decodes the first token
270
+ last_logit = prefix_logits[:, -1:]
271
+ output_tokens = jnp.zeros((last_logit.shape[0], max_decoding_steps))
272
+
273
+ def step(carry):
274
+ rng, last_logit, output_tokens, cache, _, step = carry
275
+
276
+ # Sample token from last logit
277
+ # Split RNG for this step
278
+ rng, rng_step = jax.random.split(rng)
279
+ token = jax.lax.cond(
280
+ temperature > 0.0,
281
+ lambda _: jax.random.categorical(rng_step, last_logit / temperature, axis=-1),
282
+ lambda _: jnp.argmax(last_logit, axis=-1),
283
+ operand=None,
284
+ )
285
+ output_tokens = put_along_last_axis(output_tokens, jnp.broadcast_to(step, (token.shape[0], 1)), token)
286
+
287
+ # Check for early stopping --> stop if all batch elements have EOS token
288
+ has_eos = jnp.any(token == PALIGEMMA_EOS_TOKEN, axis=-1)
289
+ all_eos = jnp.all(has_eos)
290
+
291
+ # Decode one step
292
+ token_embedding = self.PaliGemma.llm(token, embed_only=True)
293
+ positions = prefill_len[:, None] + step + 1
294
+ mask = jnp.logical_and(
295
+ jnp.arange(prefill_size + max_decoding_steps)[None, None, :] >= prefix_start[:, None, None],
296
+ jnp.arange(prefill_size + max_decoding_steps)[None, None, :]
297
+ < (jnp.broadcast_to(prefill_size + step + 1, (prefix_start.shape[0], 1, 1))),
298
+ )
299
+ last_logit, kv_cache, _ = self.PaliGemma.llm(
300
+ embedded_prefix=token_embedding, mask=mask, positions=positions, decode=True, kv_cache=cache
301
+ )
302
+
303
+ return rng, last_logit, output_tokens, kv_cache, all_eos, step + 1
304
+
305
+ def cond(carry):
306
+ _, _, _, _, all_eos, step = carry
307
+ return (~all_eos) & (step < max_decoding_steps)
308
+
309
+ # Use lax.while_loop so we can jit the full decoding loop.
310
+ _, _, output_tokens, _, _, _ = jax.lax.while_loop(
311
+ cond, step, (rng, last_logit, output_tokens, kv_cache, False, 0)
312
+ )
313
+ return output_tokens
openpi_runtime/openpi/models/siglip.py ADDED
@@ -0,0 +1,373 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Big Vision Authors.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ """A refactored and simplified ViT adoptation for Pi, taken from big_vision."""
16
+
17
+ from collections.abc import Sequence
18
+
19
+ import flax.linen as nn
20
+ import jax
21
+ import jax.numpy as jnp
22
+ import numpy as np
23
+
24
+ import openpi.training.sharding as sharding
25
+
26
+
27
+ def posemb_sincos_2d(h, w, width, temperature=10_000.0, dtype=jnp.float32):
28
+ """Follows the MoCo v3 logic."""
29
+ y, x = jnp.mgrid[:h, :w]
30
+
31
+ assert width % 4 == 0, "Width must be mult of 4 for sincos posemb"
32
+ omega = jnp.arange(width // 4) / (width // 4 - 1)
33
+ omega = 1.0 / (temperature**omega)
34
+ y = jnp.einsum("m,d->md", y.flatten(), omega)
35
+ x = jnp.einsum("m,d->md", x.flatten(), omega)
36
+ pe = jnp.concatenate([jnp.sin(x), jnp.cos(x), jnp.sin(y), jnp.cos(y)], axis=1)
37
+ return jnp.asarray(pe, dtype)[None, :, :]
38
+
39
+
40
+ def get_posemb(self, typ, seqshape, width, name, dtype=jnp.float32):
41
+ if typ == "learn":
42
+ return self.param(
43
+ name,
44
+ nn.initializers.normal(stddev=1 / np.sqrt(width)),
45
+ (1, np.prod(seqshape), width),
46
+ dtype,
47
+ )
48
+ if typ == "sincos2d":
49
+ return posemb_sincos_2d(*seqshape, width, dtype=dtype)
50
+ raise ValueError(f"Unknown posemb type: {typ}")
51
+
52
+
53
+ class MlpBlock(nn.Module):
54
+ """Transformer MLP / feed-forward block."""
55
+
56
+ mlp_dim: int | None = None # Defaults to 4x input dim
57
+ dropout: float = 0.0
58
+ dtype_mm: str = "float32"
59
+
60
+ @nn.compact
61
+ def __call__(self, x, deterministic=True): # noqa: FBT002
62
+ """Applies Transformer MlpBlock module."""
63
+ inits = {
64
+ "kernel_init": nn.initializers.xavier_uniform(),
65
+ "bias_init": nn.initializers.normal(stddev=1e-6),
66
+ }
67
+
68
+ _, _, d = x.shape # n,l,d
69
+ x = nn.Dense(self.mlp_dim or 4 * d, dtype=self.dtype_mm, **inits)(x)
70
+ x = nn.gelu(x)
71
+ x = nn.Dropout(rate=self.dropout)(x, deterministic)
72
+ return nn.Dense(d, dtype=self.dtype_mm, **inits)(x)
73
+
74
+
75
+ class Encoder1DBlock(nn.Module):
76
+ """Single transformer encoder block (MHSA + MLP)."""
77
+
78
+ mlp_dim: int | None = None # Defaults to 4x input dim
79
+ num_heads: int = 12
80
+ dropout: float = 0.0
81
+ dtype_mm: str = "float32"
82
+
83
+ @nn.compact
84
+ def __call__(self, x, deterministic=True): # noqa: FBT002
85
+ out = {}
86
+ x = sharding.activation_sharding_constraint(x)
87
+ y = nn.LayerNorm(dtype=self.dtype_mm)(x)
88
+ y = out["sa"] = nn.MultiHeadDotProductAttention(
89
+ num_heads=self.num_heads,
90
+ kernel_init=nn.initializers.xavier_uniform(),
91
+ deterministic=deterministic,
92
+ dtype=self.dtype_mm,
93
+ )(y, y)
94
+ y = sharding.activation_sharding_constraint(y)
95
+ y = nn.Dropout(rate=self.dropout)(y, deterministic)
96
+ x = out["+sa"] = x + y
97
+
98
+ y = nn.LayerNorm(dtype=self.dtype_mm)(x)
99
+ y = out["mlp"] = MlpBlock(
100
+ mlp_dim=self.mlp_dim,
101
+ dropout=self.dropout,
102
+ dtype_mm=self.dtype_mm,
103
+ )(y, deterministic)
104
+ y = sharding.activation_sharding_constraint(y)
105
+ y = nn.Dropout(rate=self.dropout)(y, deterministic)
106
+ x = out["+mlp"] = x + y
107
+ x = sharding.activation_sharding_constraint(x)
108
+ return x, out
109
+
110
+
111
+ class Encoder(nn.Module):
112
+ """Transformer Model Encoder for sequence to sequence translation."""
113
+
114
+ depth: int
115
+ mlp_dim: int | None = None # Defaults to 4x input dim
116
+ num_heads: int = 12
117
+ dropout: float = 0.0
118
+ scan: bool = False
119
+ remat_policy: str = "nothing_saveable"
120
+ dtype_mm: str = "float32"
121
+
122
+ @nn.compact
123
+ def __call__(self, x, deterministic=True): # noqa: FBT002
124
+ out = {}
125
+
126
+ if self.scan:
127
+ block = nn.remat(
128
+ Encoder1DBlock,
129
+ prevent_cse=False,
130
+ static_argnums=(2,), # 0=self, 2=deterministic
131
+ policy=getattr(jax.checkpoint_policies, self.remat_policy, None),
132
+ )
133
+ x, scan_out = nn.scan(
134
+ block,
135
+ variable_axes={"params": 0},
136
+ split_rngs={"params": True, "dropout": True},
137
+ in_axes=nn.broadcast,
138
+ length=self.depth,
139
+ )(
140
+ name="encoderblock",
141
+ dtype_mm=self.dtype_mm,
142
+ mlp_dim=self.mlp_dim,
143
+ num_heads=self.num_heads,
144
+ dropout=self.dropout,
145
+ )(x, deterministic)
146
+ for lyr in range(self.depth):
147
+ out[f"block{lyr:02d}"] = jax.tree.map(lambda o, lyr=lyr: o[lyr], scan_out)
148
+ else:
149
+ # Input Encoder
150
+ for lyr in range(self.depth):
151
+ block_cur = Encoder1DBlock(
152
+ name=f"encoderblock_{lyr}",
153
+ dtype_mm=self.dtype_mm,
154
+ mlp_dim=self.mlp_dim,
155
+ num_heads=self.num_heads,
156
+ dropout=self.dropout,
157
+ )
158
+ x, out[f"block{lyr:02d}"] = block_cur(x, deterministic)
159
+ out["pre_ln"] = x # Alias for last block, but without the number in it.
160
+
161
+ return nn.LayerNorm(name="encoder_norm", dtype=self.dtype_mm)(x), out
162
+
163
+
164
+ class MAPHead(nn.Module):
165
+ """Multihead Attention Pooling."""
166
+
167
+ mlp_dim: int | None = None # Defaults to 4x input dim
168
+ num_heads: int = 12
169
+ dtype_mm: str = "float32"
170
+
171
+ @nn.compact
172
+ def __call__(self, x):
173
+ n, _, d = x.shape # n,l,d
174
+ probe = self.param("probe", nn.initializers.xavier_uniform(), (1, 1, d), x.dtype)
175
+ probe = jnp.tile(probe, [n, 1, 1])
176
+
177
+ x = nn.MultiHeadDotProductAttention(
178
+ num_heads=self.num_heads,
179
+ dtype=self.dtype_mm,
180
+ kernel_init=nn.initializers.xavier_uniform(),
181
+ )(probe, x)
182
+
183
+ y = nn.LayerNorm(dtype=self.dtype_mm)(x)
184
+ x = x + MlpBlock(mlp_dim=self.mlp_dim, dtype=self.dtype_mm)(y)
185
+ return x[:, 0]
186
+
187
+
188
+ class _Module(nn.Module):
189
+ """ViT model."""
190
+
191
+ num_classes: int | None = None
192
+ patch_size: Sequence[int] = (16, 16)
193
+ width: int = 768
194
+ depth: int = 12
195
+ mlp_dim: int | None = None # Defaults to 4x input dim
196
+ num_heads: int = 12
197
+ posemb: str = "learn" # Can also be "sincos2d"
198
+ rep_size: int | bool = False
199
+ dropout: float = 0.0
200
+ pool_type: str = "gap" # Can also be "map" or "tok"
201
+ head_zeroinit: bool = True
202
+ scan: bool = False
203
+ # or "dots_with_no_batch_dims_saveable" for more speed (memory costly)
204
+ remat_policy: str = "nothing_saveable"
205
+ dtype_mm: str = "float32"
206
+
207
+ @nn.compact
208
+ def __call__(self, image, *, train=False):
209
+ out = {}
210
+
211
+ # Kevin edit: do patch extraction and posemb in float32,
212
+ # because I feel like it's a bit safer.
213
+ image = jnp.asarray(image, jnp.float32)
214
+
215
+ # Patch extraction
216
+ x = out["stem"] = nn.Conv(
217
+ self.width,
218
+ self.patch_size,
219
+ strides=self.patch_size,
220
+ padding="VALID",
221
+ name="embedding",
222
+ dtype=jnp.float32,
223
+ )(image)
224
+
225
+ n, h, w, c = x.shape
226
+ x = jnp.reshape(x, [n, h * w, c])
227
+
228
+ # Add posemb before adding extra token.
229
+ x = out["with_posemb"] = x + get_posemb(self, self.posemb, (h, w), c, "pos_embedding", jnp.float32)
230
+
231
+ if self.pool_type == "tok":
232
+ cls = self.param("cls", nn.initializers.zeros, (1, 1, c), x.dtype)
233
+ x = jnp.concatenate([jnp.tile(cls, [n, 1, 1]), x], axis=1)
234
+
235
+ n, _, c = x.shape # n,l,d
236
+ x = nn.Dropout(rate=self.dropout)(x, not train)
237
+
238
+ # Kevin edit: now cast back to dtype_mm (potentially half precision)
239
+ x = x.astype(self.dtype_mm)
240
+
241
+ x, out["encoder"] = Encoder(
242
+ depth=self.depth,
243
+ mlp_dim=self.mlp_dim,
244
+ num_heads=self.num_heads,
245
+ dropout=self.dropout,
246
+ scan=self.scan,
247
+ remat_policy=self.remat_policy,
248
+ dtype_mm=self.dtype_mm,
249
+ name="Transformer",
250
+ )(x, deterministic=not train)
251
+ encoded = out["encoded"] = x
252
+
253
+ if self.pool_type == "map":
254
+ x = out["head_input"] = MAPHead(
255
+ num_heads=self.num_heads,
256
+ mlp_dim=self.mlp_dim,
257
+ dtype=self.dtype_mm,
258
+ )(x)
259
+ elif self.pool_type == "gap":
260
+ x = out["head_input"] = jnp.mean(x, axis=1)
261
+ elif self.pool_type == "0":
262
+ x = out["head_input"] = x[:, 0]
263
+ elif self.pool_type == "tok":
264
+ x = out["head_input"] = x[:, 0]
265
+ encoded = encoded[:, 1:]
266
+ elif self.pool_type == "none":
267
+ pass
268
+ else:
269
+ raise ValueError(f"Unknown pool type: '{self.pool_type}'")
270
+
271
+ x_2d = jnp.reshape(encoded, [n, h, w, -1])
272
+
273
+ if self.rep_size:
274
+ rep_size = self.width if self.rep_size is True else self.rep_size
275
+ hid = nn.Dense(rep_size, dtype=self.dtype_mm, name="pre_logits")
276
+ # NOTE: In the past we did not include tanh in pre_logits.
277
+ # For few-shot, it should not matter much, as it whitens anyways.
278
+ x_2d = nn.tanh(hid(x_2d))
279
+ x = nn.tanh(hid(x))
280
+
281
+ out["pre_logits_2d"] = x_2d
282
+ out["pre_logits"] = x
283
+
284
+ if self.num_classes:
285
+ kw = {"kernel_init": nn.initializers.zeros} if self.head_zeroinit else {}
286
+ head = nn.Dense(self.num_classes, dtype=self.dtype_mm, name="head", **kw)
287
+ x_2d = out["logits_2d"] = head(x_2d)
288
+ x = out["logits"] = head(x)
289
+
290
+ return x, out
291
+
292
+
293
+ def Module(num_classes=None, *, variant=None, **kw): # pylint: disable=invalid-name # noqa: N802
294
+ """Factory function, because linen really don't like what I'm doing!"""
295
+ return _Module(num_classes, **{**decode_variant(variant), **kw})
296
+
297
+
298
+ def decode_variant(variant):
299
+ """Converts a string like "B" or "B/32" into a params dict."""
300
+ if variant is None:
301
+ return {}
302
+
303
+ v, patch = variant, {}
304
+ if "/" in variant:
305
+ v, patch = variant.split("/")
306
+ patch = {"patch_size": (int(patch), int(patch))}
307
+
308
+ return {
309
+ # pylint:disable=line-too-long
310
+ # Reference: Table 2 of https://arxiv.org/abs/2106.04560.
311
+ "width": {
312
+ "mu": 32,
313
+ "Ti": 192,
314
+ "S": 384,
315
+ "M": 512,
316
+ "B": 768,
317
+ "L": 1024,
318
+ "So400m": 1152,
319
+ "H": 1280,
320
+ "g": 1408,
321
+ "g-opt": 1536,
322
+ "G": 1664,
323
+ "G-opt": 1536,
324
+ "e": 1792,
325
+ }[v],
326
+ "depth": {
327
+ "mu": 1,
328
+ "Ti": 12,
329
+ "S": 12,
330
+ "M": 12,
331
+ "B": 12,
332
+ "L": 24,
333
+ "So400m": 27,
334
+ "H": 32,
335
+ "g": 40,
336
+ "g-opt": 40,
337
+ "G": 48,
338
+ "G-opt": 48,
339
+ "e": 56,
340
+ }[v],
341
+ "mlp_dim": {
342
+ "mu": 128,
343
+ "Ti": 768,
344
+ "S": 1536,
345
+ "M": 2048,
346
+ "B": 3072,
347
+ "L": 4096,
348
+ "So400m": 4304,
349
+ "H": 5120,
350
+ "g": 6144,
351
+ "g-opt": 6144,
352
+ "G": 8192,
353
+ "G-opt": 8192,
354
+ "e": 15360,
355
+ }[v],
356
+ "num_heads": {
357
+ "mu": 2,
358
+ "Ti": 3,
359
+ "S": 6,
360
+ "M": 8,
361
+ "B": 12,
362
+ "L": 16,
363
+ "So400m": 16,
364
+ "H": 16,
365
+ "g": 16,
366
+ "g-opt": 16,
367
+ "G": 16,
368
+ "G-opt": 16,
369
+ "e": 16,
370
+ }[v],
371
+ # pylint:enable=line-too-long
372
+ **patch,
373
+ }
openpi_runtime/openpi/models/tokenizer.py ADDED
@@ -0,0 +1,371 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+
4
+ import jax
5
+ import numpy as np
6
+ import orbax.checkpoint as ocp
7
+ import sentencepiece
8
+ from transformers import AutoProcessor
9
+
10
+ import openpi.models.utils.fsq_tokenizer as fsq_tokenizer
11
+ import openpi.shared.download as download
12
+
13
+
14
+ class PaligemmaTokenizer:
15
+ def __init__(self, max_len: int = 48):
16
+ self._max_len = max_len
17
+
18
+ path = download.maybe_download("gs://big_vision/paligemma_tokenizer.model", gs={"token": "anon"})
19
+ with path.open("rb") as f:
20
+ self._tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read())
21
+
22
+ def tokenize(self, prompt: str, state: np.ndarray | None = None) -> tuple[np.ndarray, np.ndarray]:
23
+ cleaned_text = prompt.strip().replace("_", " ").replace("\n", " ")
24
+ if state is not None:
25
+ # This is the Pi05 format, where the state is part of the discrete language input.
26
+ discretized_state = np.digitize(state, bins=np.linspace(-1, 1, 256 + 1)[:-1]) - 1
27
+ state_str = " ".join(map(str, discretized_state))
28
+ full_prompt = f"Task: {cleaned_text}, State: {state_str};\nAction: "
29
+ tokens = self._tokenizer.encode(full_prompt, add_bos=True)
30
+ else:
31
+ # This is the Pi0 format, where the state is part of the continuous action expert input.
32
+ # tokenize "\n" separately as the "start of answer" token
33
+ tokens = self._tokenizer.encode(cleaned_text, add_bos=True) + self._tokenizer.encode("\n")
34
+ tokens_len = len(tokens)
35
+ if tokens_len < self._max_len:
36
+ padding = [False] * (self._max_len - tokens_len)
37
+ mask = [True] * tokens_len + padding
38
+ tokens = tokens + padding
39
+ else:
40
+ if len(tokens) > self._max_len:
41
+ logging.warning(
42
+ f"Token length ({len(tokens)}) exceeds max length ({self._max_len}), truncating. "
43
+ "Consider increasing the `max_token_len` in your model config if this happens frequently."
44
+ )
45
+ tokens = tokens[: self._max_len]
46
+ mask = [True] * self._max_len
47
+
48
+ return np.asarray(tokens), np.asarray(mask)
49
+
50
+
51
+ class FASTTokenizer:
52
+ def __init__(self, max_len: int = 256, fast_tokenizer_path: str = "physical-intelligence/fast"):
53
+ self._max_len = max_len
54
+
55
+ # Download base PaliGemma tokenizer
56
+ path = download.maybe_download("gs://big_vision/paligemma_tokenizer.model", gs={"token": "anon"})
57
+ with path.open("rb") as f:
58
+ self._paligemma_tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read())
59
+
60
+ # Instantiate FAST tokenizer
61
+ self._fast_tokenizer = AutoProcessor.from_pretrained(fast_tokenizer_path, trust_remote_code=True)
62
+ self._fast_skip_tokens = 128 # Skip last 128 tokens in PaliGemma vocab since they are special tokens
63
+
64
+ def tokenize(
65
+ self, prompt: str, state: np.ndarray, actions: np.ndarray | None
66
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
67
+ cleaned_text = prompt.lower().strip().replace("_", " ")
68
+
69
+ # Convention: state gets discretized into 256 discrete bins (assumed range after normalization: [-1, 1])
70
+ discretized_state = np.digitize(state, bins=np.linspace(-1, 1, 256 + 1)[:-1]) - 1
71
+
72
+ # Convention: prefix includes prompt and string-representation of state, followed by ';'
73
+ state_str = " ".join(map(str, discretized_state))
74
+ prefix = f"Task: {cleaned_text}, State: {state_str};\n"
75
+ prefix_tokens = self._paligemma_tokenizer.encode(prefix, add_bos=True)
76
+
77
+ if actions is not None:
78
+ # Tokenize actions with FAST tokenizer --> map to last tokens in PaliGemma vocab
79
+ action_tokens = self._fast_tokenizer(actions[None])[0]
80
+ action_tokens_in_pg = self._act_tokens_to_paligemma_tokens(action_tokens)
81
+
82
+ # Convention: postfix contains 'Action:' followed by FAST tokens, followed by '|'
83
+ postfix_tokens = (
84
+ self._paligemma_tokenizer.encode("Action: ")
85
+ + action_tokens_in_pg.tolist()
86
+ + self._paligemma_tokenizer.encode("|", add_eos=True)
87
+ )
88
+ else:
89
+ postfix_tokens = []
90
+
91
+ # Create output token sequence & masks
92
+ # AR mask is 0 on prefix (bidirectional attention) and 1 on postfix (causal attention to all previous tokens)
93
+ tokens = prefix_tokens + postfix_tokens
94
+ token_mask = [True] * len(tokens)
95
+ ar_mask = [0] * len(prefix_tokens) + [1] * len(postfix_tokens)
96
+ loss_mask = [False] * len(prefix_tokens) + [True] * len(postfix_tokens) # Loss on postfix only
97
+
98
+ # Pad tokens to max length
99
+ tokens_len = len(tokens)
100
+ if tokens_len < self._max_len:
101
+ padding = [False] * (self._max_len - tokens_len)
102
+ tokens = tokens + padding
103
+ token_mask = token_mask + padding
104
+ ar_mask = ar_mask + padding
105
+ loss_mask = loss_mask + padding
106
+ else:
107
+ if len(tokens) > self._max_len:
108
+ logging.warning(
109
+ f"Token length ({len(tokens)}) exceeds max length ({self._max_len}), truncating. "
110
+ "Consider increasing the `max_token_len` in your model config if this happens frequently."
111
+ )
112
+ tokens = tokens[: self._max_len]
113
+ token_mask = token_mask[: self._max_len]
114
+ ar_mask = ar_mask[: self._max_len]
115
+ loss_mask = loss_mask[: self._max_len]
116
+
117
+ return np.asarray(tokens), np.asarray(token_mask), np.asarray(ar_mask), np.asarray(loss_mask)
118
+
119
+ def extract_actions(self, tokens: np.ndarray, action_horizon: int, action_dim: int) -> np.ndarray:
120
+ # Decode predicted output tokens
121
+ decoded_tokens = self._paligemma_tokenizer.decode(tokens.tolist())
122
+
123
+ # Extract actions from FAST model outputs
124
+ if "Action: " not in decoded_tokens:
125
+ return np.zeros((action_horizon, action_dim), dtype=np.float32)
126
+
127
+ # Extract actions from decoded tokens
128
+ raw_action_tokens = np.array(
129
+ self._paligemma_tokenizer.encode(decoded_tokens.split("Action: ")[1].split("|")[0].strip())
130
+ )
131
+ action_tokens = self._act_tokens_to_paligemma_tokens(raw_action_tokens)
132
+ return self._fast_tokenizer.decode(
133
+ [action_tokens.tolist()], time_horizon=action_horizon, action_dim=action_dim
134
+ )[0]
135
+
136
+ def _act_tokens_to_paligemma_tokens(self, tokens: np.ndarray | list[int]) -> np.ndarray:
137
+ if isinstance(tokens, list):
138
+ tokens = np.array(tokens)
139
+ return self._paligemma_tokenizer.vocab_size() - 1 - self._fast_skip_tokens - tokens
140
+
141
+
142
+ ###########################################################################
143
+ ## The tokenizers below are used for RoboArena baseline implementations. ##
144
+ ## They are *not* used for pi0-style models. ##
145
+ ###########################################################################
146
+
147
+
148
+ class BinningTokenizer:
149
+ """
150
+ Standard RT-2 / OpenVLA style binning tokenizer.
151
+ """
152
+
153
+ def __init__(self, max_len: int = 256, n_bins: int = 256):
154
+ self._max_len = max_len
155
+ self._n_bins = n_bins
156
+
157
+ # Download base PaliGemma tokenizer
158
+ path = download.maybe_download("gs://big_vision/paligemma_tokenizer.model", gs={"token": "anon"})
159
+ with path.open("rb") as f:
160
+ self._paligemma_tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read())
161
+
162
+ self._fast_skip_tokens = 128 # Skip last 128 tokens in PaliGemma vocab since they are special tokens
163
+
164
+ def tokenize(
165
+ self, prompt: str, state: np.ndarray, actions: np.ndarray | None
166
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
167
+ """Tokenize a prompt and state into a sequence of tokens.
168
+
169
+ Args:
170
+ prompt: The text prompt to tokenize.
171
+ state: The state array to discretize and tokenize.
172
+ actions: Must be None. Action encoding is not currently supported.
173
+
174
+ Returns:
175
+ A tuple of (tokens, token_mask, ar_mask, targets).
176
+
177
+ Raises:
178
+ NotImplementedError: If actions is not None.
179
+ """
180
+ cleaned_text = prompt.lower().strip().replace("_", " ")
181
+
182
+ # Convention: state gets discretized into 256 discrete bins (assumed range after normalization: [-1, 1])
183
+ discretized_state = np.digitize(state, bins=np.linspace(-1, 1, 256 + 1)[:-1]) - 1
184
+
185
+ # Convention: prefix includes prompt and string-representation of state, followed by ';'
186
+ state_str = " ".join(map(str, discretized_state))
187
+ prefix = f"Task: {cleaned_text}, State: {state_str};\n"
188
+ prefix_tokens = self._paligemma_tokenizer.encode(prefix, add_bos=True)
189
+
190
+ if actions is not None:
191
+ raise NotImplementedError("BinningTokenizer does not support encoding actions atm (only for inference use)")
192
+ postfix_tokens = []
193
+
194
+ # Create output token sequence & masks
195
+ # AR mask is 0 on prefix (bidirectional attention) and 1 on postfix (causal attention to all previous tokens)
196
+ tokens = prefix_tokens + postfix_tokens
197
+ token_mask = [True] * len(tokens)
198
+ ar_mask = [0] * len(prefix_tokens) + [1] * len(postfix_tokens)
199
+ loss_mask = [False] * len(prefix_tokens) + [True] * len(postfix_tokens) # Loss on postfix only
200
+
201
+ # Pad tokens to max length
202
+ tokens_len = len(tokens)
203
+ if tokens_len < self._max_len:
204
+ padding = [False] * (self._max_len - tokens_len)
205
+ tokens = tokens + padding
206
+ token_mask = token_mask + padding
207
+ ar_mask = ar_mask + padding
208
+ loss_mask = loss_mask + padding
209
+ else:
210
+ if len(tokens) > self._max_len:
211
+ logging.warning(
212
+ f"Token length ({len(tokens)}) exceeds max length ({self._max_len}), truncating. "
213
+ "Consider increasing the `max_token_len` in your model config if this happens frequently."
214
+ )
215
+ tokens = tokens[: self._max_len]
216
+ token_mask = token_mask[: self._max_len]
217
+ ar_mask = ar_mask[: self._max_len]
218
+ loss_mask = loss_mask[: self._max_len]
219
+
220
+ return np.asarray(tokens), np.asarray(token_mask), np.asarray(ar_mask), np.asarray(loss_mask)
221
+
222
+ def extract_actions(self, tokens: np.ndarray, action_horizon: int, action_dim: int) -> np.ndarray:
223
+ # Decode predicted output tokens
224
+ decoded_tokens = self._paligemma_tokenizer.decode(tokens.tolist())
225
+
226
+ # Extract actions from FAST model outputs
227
+ if "Action: " not in decoded_tokens:
228
+ return np.zeros((action_horizon, action_dim), dtype=np.float32)
229
+
230
+ # Extract actions from decoded tokens
231
+ raw_action_tokens = np.array(
232
+ self._paligemma_tokenizer.encode(decoded_tokens.split("Action: ")[1].split("|")[0].strip())
233
+ )
234
+ action_tokens = self._act_tokens_to_paligemma_tokens(raw_action_tokens)
235
+ if len(action_tokens) < action_horizon * action_dim:
236
+ return np.zeros([action_horizon, action_dim], dtype=np.float32)
237
+ action_tokens = action_tokens[: (action_horizon * action_dim)].reshape([action_horizon, action_dim])
238
+ return action_tokens / self._n_bins * 2 - 1
239
+
240
+ def _act_tokens_to_paligemma_tokens(self, tokens: np.ndarray | list[int]) -> np.ndarray:
241
+ if isinstance(tokens, list):
242
+ tokens = np.array(tokens)
243
+ return self._paligemma_tokenizer.vocab_size() - 1 - self._fast_skip_tokens - tokens
244
+
245
+
246
+ class FSQTokenizer:
247
+ """
248
+ FSQ tokenizer from the FAST paper baselines.
249
+ """
250
+
251
+ def __init__(self, max_len: int = 256, fsq_tokenizer_path: str | None = None):
252
+ self._max_len = max_len
253
+
254
+ assert fsq_tokenizer_path is not None, "fsq_tokenizer_path must be provided"
255
+ # Download tokenizer
256
+ path = download.maybe_download(fsq_tokenizer_path)
257
+ tok_path = os.path.join(path, os.listdir(path)[0])
258
+
259
+ # Split step from path
260
+ step = int(tok_path.split("/")[-1])
261
+ base_path = tok_path.rsplit("/", 1)[0]
262
+
263
+ mgr = ocp.CheckpointManager(
264
+ base_path,
265
+ item_handlers={
266
+ "params": ocp.StandardCheckpointHandler(),
267
+ "opt_state": ocp.StandardCheckpointHandler(),
268
+ "config": ocp.JsonCheckpointHandler(),
269
+ },
270
+ options=ocp.CheckpointManagerOptions(max_to_keep=1),
271
+ )
272
+
273
+ try:
274
+ restored = mgr.restore(
275
+ step, args=ocp.args.Composite(config=ocp.args.JsonRestore(), params=ocp.args.StandardRestore())
276
+ )
277
+ config = restored["config"]
278
+ self._params = restored["params"]
279
+ self._fsq_tokenizer = fsq_tokenizer.FsqAttentionTokenizer(**config)
280
+ except Exception as e:
281
+ raise RuntimeError(
282
+ f"Failed to load FSQ tokenizer checkpoint from {fsq_tokenizer_path}. Error: {e!s}"
283
+ ) from e
284
+
285
+ # Compile tokenize and detokenize functions
286
+ self._tokenize_fn = jax.jit(
287
+ lambda params, x: self._fsq_tokenizer.apply({"params": params}, x, method=self._fsq_tokenizer.tokenize)
288
+ )
289
+ self._detokenize_fn = jax.jit(
290
+ lambda params, x: self._fsq_tokenizer.apply({"params": params}, x, method=self._fsq_tokenizer.detokenize)
291
+ )
292
+
293
+ # Download base PaliGemma tokenizer
294
+ path = download.maybe_download("gs://big_vision/paligemma_tokenizer.model", gs={"token": "anon"})
295
+ with path.open("rb") as f:
296
+ self._paligemma_tokenizer = sentencepiece.SentencePieceProcessor(model_proto=f.read())
297
+
298
+ self._fast_skip_tokens = 128 # Skip last 128 tokens in PaliGemma vocab since they are special tokens
299
+
300
+ def tokenize(
301
+ self, prompt: str, state: np.ndarray, actions: np.ndarray | None
302
+ ) -> tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]:
303
+ cleaned_text = prompt.lower().strip().replace("_", " ")
304
+
305
+ # Convention: state gets discretized into 256 discrete bins (assumed range after normalization: [-1, 1])
306
+ discretized_state = np.digitize(state, bins=np.linspace(-1, 1, 256 + 1)[:-1]) - 1
307
+
308
+ # Convention: prefix includes prompt and string-representation of state, followed by ';'
309
+ state_str = " ".join(map(str, discretized_state))
310
+ prefix = f"Task: {cleaned_text}, State: {state_str};\n"
311
+ prefix_tokens = self._paligemma_tokenizer.encode(prefix, add_bos=True)
312
+
313
+ if actions is not None:
314
+ raise NotImplementedError("FSQTokenizer does not support encoding actions atm (only for inference use)")
315
+ postfix_tokens = []
316
+
317
+ # Create output token sequence & masks
318
+ # AR mask is 0 on prefix (bidirectional attention) and 1 on postfix (causal attention to all previous tokens)
319
+ tokens = prefix_tokens + postfix_tokens
320
+ token_mask = [True] * len(tokens)
321
+ ar_mask = [0] * len(prefix_tokens) + [1] * len(postfix_tokens)
322
+ loss_mask = [False] * len(prefix_tokens) + [True] * len(postfix_tokens) # Loss on postfix only
323
+
324
+ # Pad tokens to max length
325
+ tokens_len = len(tokens)
326
+ if tokens_len < self._max_len:
327
+ padding = [False] * (self._max_len - tokens_len)
328
+ tokens = tokens + padding
329
+ token_mask = token_mask + padding
330
+ ar_mask = ar_mask + padding
331
+ loss_mask = loss_mask + padding
332
+ else:
333
+ if len(tokens) > self._max_len:
334
+ logging.warning(
335
+ f"Token length ({len(tokens)}) exceeds max length ({self._max_len}), truncating. "
336
+ "Consider increasing the `max_token_len` in your model config if this happens frequently."
337
+ )
338
+ tokens = tokens[: self._max_len]
339
+ token_mask = token_mask[: self._max_len]
340
+ ar_mask = ar_mask[: self._max_len]
341
+ loss_mask = loss_mask[: self._max_len]
342
+
343
+ return np.asarray(tokens), np.asarray(token_mask), np.asarray(ar_mask), np.asarray(loss_mask)
344
+
345
+ def extract_actions(self, tokens: np.ndarray, action_horizon: int, action_dim: int) -> np.ndarray:
346
+ # Decode predicted output tokens
347
+ decoded_tokens = self._paligemma_tokenizer.decode(tokens.tolist())
348
+
349
+ # Extract actions from FAST model outputs
350
+ if "Action: " not in decoded_tokens:
351
+ return np.zeros((action_horizon, action_dim), dtype=np.float32)
352
+
353
+ # Extract actions from decoded tokens
354
+ raw_action_tokens = np.array(
355
+ self._paligemma_tokenizer.encode(decoded_tokens.split("Action: ")[1].split("|")[0].strip())
356
+ )
357
+ action_tokens = self._act_tokens_to_paligemma_tokens(raw_action_tokens)
358
+ try:
359
+ # Move computation to CPU and compile on-demand
360
+ device = jax.devices("cpu")[0]
361
+ with jax.default_device(device):
362
+ detok_act = self._detokenize_fn(self._params, action_tokens[None, ...])[0]
363
+ return detok_act[: action_horizon * action_dim].reshape([action_horizon, action_dim])
364
+ except Exception as e:
365
+ logging.warning(f"Error decoding FSQ: {e}")
366
+ return np.zeros((action_horizon, action_dim))
367
+
368
+ def _act_tokens_to_paligemma_tokens(self, tokens: np.ndarray | list[int]) -> np.ndarray:
369
+ if isinstance(tokens, list):
370
+ tokens = np.array(tokens)
371
+ return self._paligemma_tokenizer.vocab_size() - 1 - self._fast_skip_tokens - tokens
openpi_runtime/openpi/models/utils/fsq_tokenizer.py ADDED
@@ -0,0 +1,472 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import math
2
+ from typing import Any, Literal
3
+
4
+ import chex
5
+ from einops import einops
6
+ from flax import linen as nn
7
+ from flax.linen.module import Module
8
+ from flax.linen.module import compact
9
+ from flax.struct import dataclass
10
+ from flax.typing import Array
11
+ import jax
12
+ import jax.numpy as jnp
13
+
14
+
15
+ class FsqCodebook(nn.Module):
16
+ input_dim: int
17
+ target_codebook_size: int
18
+ codebook_type: Literal["fsq", "lfq"]
19
+
20
+ _bins_per_dim: tuple[int] | None = None
21
+
22
+ @property
23
+ def bins_per_dim(self) -> tuple[int]:
24
+ if self._bins_per_dim is not None:
25
+ return self._bins_per_dim
26
+
27
+ if self.codebook_type == "fsq":
28
+ return self._get_bins_fsq(self.target_codebook_size)
29
+ elif self.codebook_type == "lfq": # noqa: RET505
30
+ return self._get_bins_lfq(self.target_codebook_size)
31
+ elif self.codebook_type == "custom":
32
+ return self._get_bins_custom(self.target_codebook_size)
33
+ else:
34
+ raise ValueError(f"Codebook type {self.codebook_type} not supported.")
35
+
36
+ @property
37
+ def place_values(self) -> jnp.ndarray:
38
+ place_values = [1]
39
+ for b in self.bins_per_dim[:-1]:
40
+ place_values.append(place_values[-1] * b)
41
+ return jnp.array(place_values)
42
+
43
+ @staticmethod
44
+ def _get_bins_fsq(target_codebook_size: int) -> tuple[int]:
45
+ """
46
+ Get bins per dimension based on codebook size, from the original FSQ paper.
47
+ """
48
+ if target_codebook_size == 2**8:
49
+ return (8, 6, 5)
50
+ elif target_codebook_size == 2**10: # noqa: RET505
51
+ return (8, 5, 5, 5)
52
+ elif target_codebook_size == 2**12:
53
+ return (7, 5, 5, 5, 5)
54
+ elif target_codebook_size == 2**14:
55
+ return (8, 8, 8, 6, 5)
56
+ elif target_codebook_size == 2**16:
57
+ return (8, 8, 8, 5, 5, 5)
58
+ else:
59
+ raise ValueError(f"Codebook size {target_codebook_size} not supported.")
60
+
61
+ @staticmethod
62
+ def _get_bins_custom(target_codebook_size: int) -> tuple[int]:
63
+ if target_codebook_size == 2**8:
64
+ return (16, 16)
65
+ elif target_codebook_size == 2**10: # noqa: RET505
66
+ return (32, 32)
67
+ elif target_codebook_size == 2**12:
68
+ return (64, 64)
69
+ elif target_codebook_size == 2**14:
70
+ return (128, 128)
71
+ elif target_codebook_size == 2**16:
72
+ return (256, 256)
73
+ return None
74
+
75
+ @staticmethod
76
+ def _get_bins_lfq(target_codebook_size: int) -> tuple[int]:
77
+ """
78
+ Get bins per dimension according to the Lookup-Free Quantization paper (2 bins per dimension)
79
+ """
80
+ assert target_codebook_size & (target_codebook_size - 1) == 0, "Codebook size should be a power of two for LFQ"
81
+
82
+ return (2,) * int(math.log2(target_codebook_size))
83
+
84
+ def setup(self):
85
+ self.proj_down = nn.Dense(len(self.bins_per_dim))
86
+ self.proj_up = nn.Dense(self.input_dim)
87
+
88
+ def __call__(self, inputs: jnp.ndarray) -> tuple[jnp.ndarray, jnp.ndarray]:
89
+ tokens, z = self.encode(inputs)
90
+ output = self.decode(tokens, z_grad=z)
91
+ return tokens, output
92
+
93
+ def encode(self, inputs: jnp.ndarray) -> tuple[jnp.ndarray, jnp.ndarray]:
94
+ bases = jnp.array(self.bins_per_dim)
95
+
96
+ x = self.proj_down(inputs)
97
+ z = jnp.tanh(x)
98
+
99
+ # Quantize
100
+ digits = jnp.round((z + 1) * (bases - 1) / 2).astype(jnp.int32)
101
+ tokens = self.undigitize(digits)
102
+
103
+ return tokens, z
104
+
105
+ def decode(self, tokens: jnp.ndarray, z_grad: jax.Array | None = None) -> jnp.ndarray:
106
+ bases = jnp.array(self.bins_per_dim)
107
+ digits = self.digitize(tokens)
108
+
109
+ z_q = digits / (bases - 1) * 2 - 1
110
+
111
+ if z_grad is not None:
112
+ chex.assert_equal_shape([z_q, z_grad])
113
+ z_q = jax.lax.stop_gradient(z_q - z_grad) + z_grad
114
+
115
+ return self.proj_up(z_q)
116
+
117
+ def undigitize(self, digits: jnp.ndarray) -> jnp.ndarray:
118
+ return jnp.sum(digits * jnp.array(self.place_values), axis=-1)
119
+
120
+ def digitize(self, tokens: jnp.ndarray) -> jnp.ndarray:
121
+ return (tokens[..., None] // jnp.array(self.place_values)) % jnp.array(self.bins_per_dim)
122
+
123
+ @property
124
+ def vocab_size(self) -> int:
125
+ return math.prod(self.bins_per_dim)
126
+
127
+
128
+ class ResNetDownBlock(nn.Module):
129
+ stride: int = 1
130
+ n_filters: int = 64
131
+ dropout_rate: float = 0.0
132
+ group_size: int = 32
133
+
134
+ @nn.compact
135
+ def __call__(self, x: jnp.ndarray, *, train: bool = True) -> jnp.ndarray:
136
+ skip = x
137
+
138
+ if self.stride > 1 or x.shape[-1] != self.n_filters:
139
+ skip = nn.Conv(self.n_filters, (self.stride,), (self.stride,), "SAME")(skip)
140
+
141
+ x = nn.Conv(self.n_filters, (3,), (self.stride,), "SAME")(x)
142
+ x = nn.GroupNorm(num_groups=self.n_filters // self.group_size)(x)
143
+ x = nn.Dropout(self.dropout_rate)(x, deterministic=not train)
144
+ x = nn.relu(x)
145
+ x = nn.Conv(self.n_filters, (3,), (1,), "SAME")(x)
146
+
147
+ return skip + x
148
+
149
+
150
+ class ResNetUpBlock(nn.Module):
151
+ stride: int = 1
152
+ n_filters: int = 64
153
+ dropout_rate: float = 0.0
154
+ group_size: int = 32
155
+
156
+ @nn.compact
157
+ def __call__(self, x: jnp.ndarray, *, train: bool = True) -> jnp.ndarray:
158
+ skip = x
159
+
160
+ if self.stride > 1:
161
+ skip = nn.ConvTranspose(self.n_filters, (self.stride,), (self.stride,), "SAME")(skip)
162
+
163
+ x = nn.ConvTranspose(self.n_filters, (3,), (self.stride,), "SAME")(x)
164
+ x = nn.GroupNorm(num_groups=self.n_filters // self.group_size)(x)
165
+ x = nn.Dropout(self.dropout_rate)(x, deterministic=not train)
166
+ x = nn.relu(x)
167
+ x = nn.ConvTranspose(self.n_filters, (3,), (1,), "SAME")(x)
168
+
169
+ return skip + x
170
+
171
+
172
+ @dataclass
173
+ class LfqCodebookOutput:
174
+ tokens: jnp.ndarray
175
+ z: jnp.ndarray
176
+ z_q: jnp.ndarray
177
+ token_log_probs: jnp.ndarray
178
+ commit_loss: jnp.ndarray
179
+
180
+
181
+ class LookupFreeQuantization(nn.Module):
182
+ num_dims: int
183
+ latent_dim: int
184
+
185
+ def setup(self):
186
+ self.codebook = jnp.array([-1, 1])
187
+ self.activation = nn.tanh
188
+
189
+ self.project_down = nn.Dense(self.num_dims)
190
+ self.project_up = nn.Dense(self.latent_dim)
191
+
192
+ def encode(self, z: jnp.ndarray) -> jnp.ndarray:
193
+ z = self.project_down(z)
194
+ token_squared_distances = jnp.square(z[..., None] - self.codebook)
195
+ token_bits = jnp.argmin(token_squared_distances, axis=-1)
196
+ return jnp.sum(token_bits * (2 ** jnp.arange(self.num_dims)), axis=-1)
197
+
198
+ def decode(self, tokens: jnp.ndarray) -> jnp.ndarray:
199
+ token_bits = (tokens[..., None] & (2 ** jnp.arange(self.num_dims))).astype(jnp.int32)
200
+ return self.project_up(self.codebook[token_bits])
201
+
202
+ def loss(self, x: jnp.ndarray) -> LfqCodebookOutput:
203
+ z = self.project_down(x)
204
+ z = self.activation(z)
205
+
206
+ token_squared_distances = jnp.square(z[..., None] - self.codebook)
207
+ tokens = jnp.argmin(token_squared_distances, axis=-1)
208
+
209
+ token_bit_log_probs = -token_squared_distances
210
+ # Compute token log probs for tokens 0..2^num_dims-1 by summing corresponding log-probs
211
+ token_bit_expansions = jnp.bitwise_and(
212
+ jnp.arange(2**self.num_dims)[None, :], 2 ** jnp.arange(self.num_dims)[:, None]
213
+ ).astype(jnp.int32)
214
+ token_log_probs = (
215
+ token_bit_log_probs[..., 0] @ (1 - token_bit_expansions)
216
+ + token_bit_log_probs[..., 1] @ token_bit_expansions
217
+ ) # (batch_size, num_tokens, 2 ** num_dims)
218
+ token_log_probs = jax.lax.stop_gradient(jax.nn.log_softmax(token_log_probs, axis=-1))
219
+ chex.assert_shape(token_log_probs, (*x.shape[:-1], 2**self.num_dims))
220
+
221
+ z_q = self.codebook[tokens]
222
+ commit_loss = jnp.square(z - z_q).mean()
223
+ z_q = jax.lax.stop_gradient(z_q - z) + z
224
+
225
+ z_q = self.project_up(z_q)
226
+ z = self.project_up(z)
227
+
228
+ tokens = jnp.sum(tokens * (len(self.codebook) ** jnp.arange(self.num_dims)), axis=-1)
229
+ return LfqCodebookOutput(
230
+ tokens=tokens,
231
+ z=z,
232
+ z_q=z_q,
233
+ token_log_probs=jnp.zeros(()),
234
+ commit_loss=commit_loss,
235
+ )
236
+
237
+
238
+ def make_block_causal_attention_matrix(q: jnp.ndarray, k: jnp.ndarray, bs_q: int, bs_k: int) -> jnp.ndarray:
239
+ return nn.make_attention_mask(q, k, pairwise_fn=lambda x, y: jnp.greater_equal(x // bs_k, y // bs_q))
240
+
241
+
242
+ class GeGLU(Module):
243
+ """Gated Linear Unit with GELU (GeGLU) activation function.
244
+ GeGLU is a Flax layer that combines a linear transformation with a GELU
245
+ activation function in a gating mechanism. It is often used in Transformer models
246
+ to provide non-linear capabilities while preserving a strong linear component.
247
+
248
+ Attributes:
249
+ features: the number of output features (default: None).
250
+ """
251
+
252
+ output_dim: int = -1
253
+
254
+ @compact
255
+ def __call__(self, inputs: Array) -> Array:
256
+ """Applies the GeGLU activation to the inputs.
257
+ Args:
258
+ inputs: the nd-array to apply the GeGLU activation function to.
259
+ Returns:
260
+ The transformed input.
261
+ """
262
+ output_dim = inputs.shape[-1] if self.output_dim == -1 else self.output_dim
263
+
264
+ x = nn.Dense(output_dim * 2)(inputs)
265
+ x, gate = x[..., :output_dim], x[..., output_dim:]
266
+ return x * nn.gelu(gate)
267
+
268
+
269
+ class CrossAttentionLayer(nn.Module):
270
+ dropout_rate: float = 0.0
271
+ num_heads: int = None
272
+ causal: bool = False
273
+ mlp_ratio: float = 4.0
274
+
275
+ @nn.compact
276
+ def __call__(
277
+ self,
278
+ x: jnp.ndarray,
279
+ y: jnp.ndarray,
280
+ *,
281
+ mask_self: jnp.ndarray | None = None,
282
+ mask_cross: jnp.ndarray | None = None,
283
+ train: bool = True,
284
+ ) -> jnp.ndarray:
285
+ d_embed = x.shape[-1]
286
+ seq_len_q = x.shape[-2]
287
+ seq_len_k = y.shape[-2]
288
+
289
+ if self.causal:
290
+ # One block size will be 1
291
+ bs_q = max(seq_len_q // seq_len_k, 1)
292
+ bs_k = max(seq_len_k // seq_len_q, 1)
293
+
294
+ mask_self = nn.make_causal_mask(x[..., 0])
295
+ mask_cross = make_block_causal_attention_matrix(x[..., 0], y[..., 0], bs_q, bs_k)
296
+
297
+ # Self-attention block
298
+ skip = x
299
+ x = nn.LayerNorm()(x)
300
+ x = nn.MultiHeadDotProductAttention(
301
+ num_heads=self.num_heads or d_embed // 64,
302
+ dropout_rate=self.dropout_rate,
303
+ deterministic=not train,
304
+ )(x, x, x, mask=mask_self)
305
+ x = skip + x
306
+
307
+ # Cross-attention block
308
+ skip = x
309
+ x = nn.LayerNorm()(x)
310
+ x = nn.MultiHeadDotProductAttention(
311
+ num_heads=self.num_heads or d_embed // 64,
312
+ dropout_rate=self.dropout_rate,
313
+ deterministic=not train,
314
+ )(x, y, y, mask=mask_cross)
315
+ x = skip + x
316
+
317
+ # MLP block
318
+ skip = x
319
+ x = nn.LayerNorm()(x)
320
+ x = nn.Dense(int(d_embed * self.mlp_ratio))(x)
321
+ x = nn.Dropout(self.dropout_rate)(x, deterministic=not train)
322
+ x = GeGLU()(x)
323
+ x = nn.Dense(d_embed)(x)
324
+ return skip + x
325
+
326
+
327
+ def sinusoidal_pe_init(_, shape: tuple[int, int]) -> jnp.ndarray:
328
+ seq_len, d_embed = shape
329
+
330
+ position = jnp.arange(0, seq_len, 1)
331
+ div_term = jnp.exp(jnp.arange(0, d_embed, 2) * -(jnp.log(10000.0) / d_embed))
332
+ return jnp.concatenate(
333
+ [
334
+ jnp.sin(position[:, jnp.newaxis] * div_term),
335
+ jnp.cos(position[:, jnp.newaxis] * div_term),
336
+ ],
337
+ axis=-1,
338
+ )
339
+
340
+
341
+ class TokenizerEncoderDecoder(nn.Module):
342
+ num_tokens: int
343
+ num_cross_tokens: int
344
+ num_layers: int
345
+ causal: bool
346
+
347
+ mlp_ratio: float = 4.0
348
+ use_state_conditioning: bool = False
349
+
350
+ @nn.compact
351
+ def __call__(
352
+ self,
353
+ y: jnp.ndarray,
354
+ *,
355
+ train: bool = True,
356
+ state_conditioning: jnp.ndarray | None = None,
357
+ mask: jnp.ndarray | None = None,
358
+ ) -> jnp.ndarray:
359
+ x = self.param("q_embed", sinusoidal_pe_init, (self.num_tokens, y.shape[-1]))
360
+ x = jax.numpy.broadcast_to(x, y.shape[:-2] + x.shape[-2:])
361
+
362
+ if mask is not None:
363
+ # mask is (batch_dims..., num_cross_tokens)
364
+ chex.assert_equal_shape([y[..., 0], mask])
365
+ attn_mask = einops.repeat(mask, "... kv -> ... 1 q kv", q=self.num_tokens)
366
+ else:
367
+ attn_mask = jnp.ones((*y.shape[:-2], 1, self.num_tokens, self.num_cross_tokens))
368
+
369
+ if self.use_state_conditioning:
370
+ assert state_conditioning is not None, "State conditioning is required for this model."
371
+ state_embed = nn.Dense(y.shape[-1], name="state_proj")(state_conditioning)[..., None, :]
372
+ y = jnp.concatenate([y, state_embed], axis=-2)
373
+ attn_mask = jnp.concatenate([attn_mask, jnp.ones_like(attn_mask[..., 0:1])], axis=-1)
374
+
375
+ y = y + self.param("y_pos_enc", sinusoidal_pe_init, y.shape[-2:])
376
+
377
+ for _ in range(self.num_layers):
378
+ x = CrossAttentionLayer(causal=self.causal, mlp_ratio=self.mlp_ratio)(
379
+ x, y, train=train, mask_self=None, mask_cross=attn_mask
380
+ )
381
+
382
+ return x
383
+
384
+
385
+ class FsqAttentionTokenizer(nn.Module):
386
+ embed_dim: int
387
+ data_dim: int
388
+ data_horizon: int
389
+ num_tokens: int
390
+ num_layers: int
391
+ target_codebook_size: int
392
+ causal: bool = False
393
+ mlp_ratio: float = 2.0
394
+
395
+ bound: float | None = None
396
+
397
+ use_state_conditioning: bool = False
398
+
399
+ @property
400
+ def vocab_size(self) -> int:
401
+ return math.prod(FsqCodebook._get_bins_fsq(self.target_codebook_size)) # noqa: SLF001
402
+
403
+ def setup(self):
404
+ self.proj = nn.Dense(self.embed_dim)
405
+ self.encoder = TokenizerEncoderDecoder(
406
+ num_tokens=self.num_tokens,
407
+ num_cross_tokens=self.data_horizon,
408
+ num_layers=self.num_layers,
409
+ causal=self.causal,
410
+ use_state_conditioning=self.use_state_conditioning,
411
+ mlp_ratio=self.mlp_ratio,
412
+ )
413
+ self.codebook = FsqCodebook(
414
+ input_dim=self.embed_dim,
415
+ target_codebook_size=self.target_codebook_size,
416
+ codebook_type="custom",
417
+ )
418
+ self.decoder = TokenizerEncoderDecoder(
419
+ num_tokens=self.data_horizon,
420
+ num_cross_tokens=self.num_tokens,
421
+ num_layers=self.num_layers,
422
+ causal=self.causal,
423
+ use_state_conditioning=self.use_state_conditioning,
424
+ mlp_ratio=self.mlp_ratio,
425
+ )
426
+
427
+ self.proj_mean = nn.Dense(self.data_dim)
428
+ self.out_scale = self.param("out_scale", lambda _: jnp.full((), 1.0))
429
+
430
+ def tokenize(
431
+ self, action: jnp.ndarray, *, obs: jnp.ndarray | None = None, train: bool = False
432
+ ) -> tuple[jnp.ndarray, jnp.ndarray]:
433
+ if self.bound is not None:
434
+ action = jnp.clip(action, -self.bound, self.bound)
435
+
436
+ x = self.proj(action)
437
+ x = self.encoder(x, train=train, state_conditioning=obs)
438
+
439
+ return self.codebook.encode(x)
440
+
441
+ def detokenize(self, tokens: jnp.ndarray, *, obs: jnp.ndarray | None = None) -> jnp.ndarray:
442
+ x = self.decoder(self.codebook.decode(tokens), state_conditioning=obs)
443
+ mean = self.proj_mean(x)
444
+ return mean * self.out_scale
445
+
446
+ def loss(
447
+ self, action: jnp.ndarray, *, obs: jnp.ndarray | None = None, train: bool = True
448
+ ) -> tuple[jnp.ndarray, dict[str, jnp.ndarray]]:
449
+ # Encode
450
+ x = self.proj(action)
451
+ z = self.encoder(x, train=train, state_conditioning=obs)
452
+
453
+ # Quantize
454
+ tokens, z = self.codebook(z)
455
+
456
+ # Decode
457
+ x = self.decoder(z, train=train, state_conditioning=obs)
458
+ mean = self.proj_mean(x) * self.out_scale
459
+
460
+ mse = jnp.mean(jnp.square(action - mean))
461
+ mae = jnp.mean(jnp.abs(action - mean))
462
+
463
+ return mse, {
464
+ "mse": mse,
465
+ "mae": mae,
466
+ }
467
+
468
+ def __call__(self, *args: Any, **kwargs: Any) -> tuple[jnp.ndarray, dict[str, jnp.ndarray]]:
469
+ """
470
+ Dummy for .init
471
+ """
472
+ return self.loss(*args, **kwargs)
openpi_runtime/openpi/models/vit.py ADDED
@@ -0,0 +1,307 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Copyright 2024 Google LLC.
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+ """ViT implementation adapted from https://github.com/google-research/vision_transformer/blob/main/vit_jax/models_vit.py."""
15
+
16
+ from collections.abc import Callable
17
+ from typing import Any
18
+
19
+ import flax.linen as nn
20
+ import jax
21
+ import jax.numpy as jnp
22
+
23
+ from openpi.models import resnet as models_resnet
24
+
25
+ Array = Any
26
+ PRNGKey = Any
27
+ Shape = tuple[int]
28
+ Dtype = Any
29
+
30
+
31
+ class IdentityLayer(nn.Module):
32
+ """Identity layer, convenient for giving a name to an array."""
33
+
34
+ @nn.compact
35
+ def __call__(self, x):
36
+ return x
37
+
38
+
39
+ class AddPositionEmbs(nn.Module):
40
+ """Adds learned positional embeddings to the inputs.
41
+
42
+ Attributes:
43
+ posemb_init: positional embedding initializer.
44
+ """
45
+
46
+ posemb_init: Callable[[PRNGKey, Shape, Dtype], Array]
47
+ param_dtype: Dtype = jnp.float32
48
+
49
+ @nn.compact
50
+ def __call__(self, inputs):
51
+ """Applies the AddPositionEmbs module.
52
+
53
+ Args:
54
+ inputs: Inputs to the layer.
55
+
56
+ Returns:
57
+ Output tensor with shape `(bs, timesteps, in_dim)`.
58
+ """
59
+ # inputs.shape is (batch_size, seq_len, emb_dim).
60
+ assert inputs.ndim == 3, f"Number of dimensions should be 3, but it is: {inputs.ndim}"
61
+ pos_emb_shape = (1, inputs.shape[1], inputs.shape[2])
62
+ pe = self.param("pos_embedding", self.posemb_init, pos_emb_shape, self.param_dtype)
63
+ return inputs + pe
64
+
65
+
66
+ class MlpBlock(nn.Module):
67
+ """Transformer MLP / feed-forward block."""
68
+
69
+ mlp_dim: int
70
+ dtype: Dtype = jnp.float32
71
+ param_dtype: Dtype = jnp.float32
72
+ out_dim: int | None = None
73
+ dropout_rate: float = 0.1
74
+ kernel_init: Callable[[PRNGKey, Shape, Dtype], Array] = nn.initializers.xavier_uniform()
75
+ bias_init: Callable[[PRNGKey, Shape, Dtype], Array] = nn.initializers.normal(stddev=1e-6)
76
+
77
+ @nn.compact
78
+ def __call__(self, inputs, *, deterministic):
79
+ """Applies Transformer MlpBlock module."""
80
+ actual_out_dim = inputs.shape[-1] if self.out_dim is None else self.out_dim
81
+ x = nn.Dense(
82
+ features=self.mlp_dim,
83
+ dtype=self.dtype,
84
+ param_dtype=self.param_dtype,
85
+ kernel_init=self.kernel_init,
86
+ bias_init=self.bias_init,
87
+ )( # pytype: disable=wrong-arg-types
88
+ inputs
89
+ )
90
+ x = nn.gelu(x)
91
+ x = nn.Dropout(rate=self.dropout_rate)(x, deterministic=deterministic)
92
+ output = nn.Dense(
93
+ features=actual_out_dim,
94
+ dtype=self.dtype,
95
+ param_dtype=self.param_dtype,
96
+ kernel_init=self.kernel_init,
97
+ bias_init=self.bias_init,
98
+ )( # pytype: disable=wrong-arg-types
99
+ x
100
+ )
101
+ return nn.Dropout(rate=self.dropout_rate)(output, deterministic=deterministic)
102
+
103
+
104
+ class Encoder1DBlock(nn.Module):
105
+ """Transformer encoder layer.
106
+
107
+ Attributes:
108
+ inputs: input data.
109
+ mlp_dim: dimension of the mlp on top of attention block.
110
+ dtype: the dtype of the computation (default: float32).
111
+ dropout_rate: dropout rate.
112
+ attention_dropout_rate: dropout for attention heads.
113
+ deterministic: bool, deterministic or not (to apply dropout).
114
+ num_heads: Number of heads in nn.MultiHeadDotProductAttention
115
+ """
116
+
117
+ mlp_dim: int
118
+ num_heads: int
119
+ dtype: Dtype = jnp.float32
120
+ dropout_rate: float = 0.1
121
+ attention_dropout_rate: float = 0.1
122
+
123
+ @nn.compact
124
+ def __call__(self, inputs, deterministic):
125
+ """Applies Encoder1DBlock module.
126
+
127
+ Args:
128
+ inputs: Inputs to the layer.
129
+ deterministic: Dropout will not be applied when set to true.
130
+
131
+ Returns:
132
+ output after transformer encoder block.
133
+ """
134
+
135
+ # Attention block.
136
+ assert inputs.ndim == 3, f"Expected (batch, seq, hidden) got {inputs.shape}"
137
+ x = nn.LayerNorm(dtype=self.dtype)(inputs)
138
+ x = nn.MultiHeadDotProductAttention(
139
+ dtype=self.dtype,
140
+ kernel_init=nn.initializers.xavier_uniform(),
141
+ broadcast_dropout=False,
142
+ deterministic=deterministic,
143
+ dropout_rate=self.attention_dropout_rate,
144
+ num_heads=self.num_heads,
145
+ # why isn't this true by default???
146
+ force_fp32_for_softmax=True,
147
+ )(x, x)
148
+ x = nn.Dropout(rate=self.dropout_rate)(x, deterministic=deterministic)
149
+ x = x + inputs
150
+
151
+ # MLP block.
152
+ y = nn.LayerNorm(dtype=self.dtype)(x)
153
+ y = MlpBlock(mlp_dim=self.mlp_dim, dtype=self.dtype, dropout_rate=self.dropout_rate)(
154
+ y, deterministic=deterministic
155
+ )
156
+
157
+ return x + y, None
158
+
159
+
160
+ class Encoder(nn.Module):
161
+ """Transformer Model Encoder for sequence to sequence translation.
162
+
163
+ Attributes:
164
+ num_layers: number of layers
165
+ mlp_dim: dimension of the mlp on top of attention block
166
+ num_heads: Number of heads in nn.MultiHeadDotProductAttention
167
+ dropout_rate: dropout rate.
168
+ attention_dropout_rate: dropout rate in self attention.
169
+ """
170
+
171
+ dtype: jax.typing.DTypeLike
172
+ num_layers: int
173
+ mlp_dim: int
174
+ num_heads: int
175
+ dropout_rate: float = 0.1
176
+ attention_dropout_rate: float = 0.1
177
+ add_position_embedding: bool = True
178
+
179
+ @nn.compact
180
+ def __call__(self, x, *, train):
181
+ """Applies Transformer model on the inputs.
182
+
183
+ Args:
184
+ x: Inputs to the layer.
185
+ train: Set to `True` when training.
186
+
187
+ Returns:
188
+ output of a transformer encoder.
189
+ """
190
+ assert x.ndim == 3 # (batch, len, emb)
191
+
192
+ if self.add_position_embedding:
193
+ x = AddPositionEmbs(
194
+ posemb_init=nn.initializers.normal(stddev=0.02), # from BERT.
195
+ name="posembed_input",
196
+ )(x)
197
+ x = nn.Dropout(rate=self.dropout_rate)(x, deterministic=not train)
198
+
199
+ x = x.astype(self.dtype)
200
+ # Input Encoder
201
+ block = nn.remat(Encoder1DBlock, prevent_cse=False, static_argnums=(2,))
202
+ x, _ = nn.scan(
203
+ block,
204
+ variable_axes={"params": 0},
205
+ split_rngs={"params": True, "dropout": True},
206
+ in_axes=nn.broadcast,
207
+ length=self.num_layers,
208
+ )(
209
+ name="encoderblock",
210
+ mlp_dim=self.mlp_dim,
211
+ dropout_rate=self.dropout_rate,
212
+ attention_dropout_rate=self.attention_dropout_rate,
213
+ dtype=self.dtype,
214
+ num_heads=self.num_heads,
215
+ )(x, not train)
216
+ return nn.LayerNorm(name="encoder_norm", dtype=self.dtype)(x)
217
+
218
+
219
+ class VisionTransformer(nn.Module):
220
+ """VisionTransformer."""
221
+
222
+ dtype: jax.typing.DTypeLike
223
+ num_classes: int
224
+ patches: Any
225
+ transformer: Any
226
+ hidden_size: int
227
+ resnet: Any | None = None
228
+ representation_size: int | None = None
229
+ classifier: str = "token"
230
+ head_bias_init: float = 0.0
231
+ encoder: type[nn.Module] = Encoder
232
+ model_name: str | None = None
233
+
234
+ @nn.compact
235
+ def __call__(self, inputs, *, train):
236
+ x = inputs
237
+ # (Possibly partial) ResNet root.
238
+ if self.resnet is not None:
239
+ width = int(64 * self.resnet.width_factor)
240
+
241
+ # Root block.
242
+ x = models_resnet.StdConv(
243
+ features=width, kernel_size=(7, 7), strides=(2, 2), use_bias=False, name="conv_root"
244
+ )(x)
245
+ x = nn.GroupNorm(name="gn_root")(x)
246
+ x = nn.relu(x)
247
+ x = nn.max_pool(x, window_shape=(3, 3), strides=(2, 2), padding="SAME")
248
+
249
+ # ResNet stages.
250
+ if self.resnet.num_layers:
251
+ x = models_resnet.ResNetStage(
252
+ block_size=self.resnet.num_layers[0], nout=width, first_stride=(1, 1), name="block1"
253
+ )(x)
254
+ for i, block_size in enumerate(self.resnet.num_layers[1:], 1):
255
+ x = models_resnet.ResNetStage(
256
+ block_size=block_size, nout=width * 2**i, first_stride=(2, 2), name=f"block{i + 1}"
257
+ )(x)
258
+
259
+ n, h, w, c = x.shape
260
+
261
+ # We can merge s2d+emb into a single conv; it's the same.
262
+ x = nn.Conv(
263
+ features=self.hidden_size,
264
+ kernel_size=self.patches.size,
265
+ strides=self.patches.size,
266
+ padding="VALID",
267
+ name="embedding",
268
+ )(x)
269
+
270
+ # Here, x is a grid of embeddings.
271
+
272
+ # (Possibly partial) Transformer.
273
+ if self.transformer is not None:
274
+ n, h, w, c = x.shape
275
+ x = jnp.reshape(x, [n, h * w, c])
276
+
277
+ # If we want to add a class token, add it here.
278
+ if self.classifier in ["token", "token_unpooled"]:
279
+ cls = self.param("cls", nn.initializers.zeros, (1, 1, c))
280
+ cls = jnp.tile(cls, [n, 1, 1])
281
+ x = jnp.concatenate([cls, x], axis=1)
282
+
283
+ x = self.encoder(name="Transformer", **self.transformer, dtype=self.dtype)(x, train=train)
284
+
285
+ if self.classifier == "token":
286
+ x = x[:, 0]
287
+ elif self.classifier == "gap":
288
+ x = jnp.mean(x, axis=list(range(1, x.ndim - 1))) # (1,) or (1,2)
289
+ elif self.classifier in ["unpooled", "token_unpooled"]:
290
+ pass
291
+ else:
292
+ raise ValueError(f"Invalid classifier={self.classifier}")
293
+
294
+ if self.representation_size is not None:
295
+ x = nn.Dense(features=self.representation_size, name="pre_logits")(x)
296
+ x = nn.tanh(x)
297
+ else:
298
+ x = IdentityLayer(name="pre_logits")(x)
299
+
300
+ if self.num_classes:
301
+ x = nn.Dense(
302
+ features=self.num_classes,
303
+ name="head",
304
+ kernel_init=nn.initializers.zeros,
305
+ bias_init=nn.initializers.constant(self.head_bias_init),
306
+ )(x)
307
+ return x
openpi_runtime/openpi/models_pytorch/gemma_pytorch.py ADDED
@@ -0,0 +1,280 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Literal
2
+
3
+ import torch
4
+ from torch import nn
5
+ from transformers import GemmaForCausalLM
6
+ from transformers import PaliGemmaForConditionalGeneration
7
+ from transformers.models.auto import CONFIG_MAPPING
8
+ from transformers.models.gemma import modeling_gemma
9
+
10
+
11
+ class PaliGemmaWithExpertModel(nn.Module):
12
+ def __init__(
13
+ self,
14
+ vlm_config,
15
+ action_expert_config,
16
+ use_adarms=None,
17
+ precision: Literal["bfloat16", "float32"] = "bfloat16",
18
+ ):
19
+ if use_adarms is None:
20
+ use_adarms = [False, False]
21
+ super().__init__()
22
+
23
+ vlm_config_hf = CONFIG_MAPPING["paligemma"]()
24
+ vlm_config_hf._vocab_size = 257152 # noqa: SLF001
25
+ vlm_config_hf.image_token_index = 257152
26
+ vlm_config_hf.text_config.hidden_size = vlm_config.width
27
+ vlm_config_hf.text_config.intermediate_size = vlm_config.mlp_dim
28
+ vlm_config_hf.text_config.num_attention_heads = vlm_config.num_heads
29
+ vlm_config_hf.text_config.head_dim = vlm_config.head_dim
30
+ vlm_config_hf.text_config.num_hidden_layers = vlm_config.depth
31
+ vlm_config_hf.text_config.num_key_value_heads = vlm_config.num_kv_heads
32
+ vlm_config_hf.text_config.hidden_activation = "gelu_pytorch_tanh"
33
+ vlm_config_hf.text_config.torch_dtype = "float32"
34
+ vlm_config_hf.text_config.vocab_size = 257152
35
+ vlm_config_hf.text_config.use_adarms = use_adarms[0]
36
+ vlm_config_hf.text_config.adarms_cond_dim = vlm_config.width if use_adarms[0] else None
37
+ vlm_config_hf.vision_config.intermediate_size = 4304
38
+ vlm_config_hf.vision_config.projection_dim = 2048
39
+ vlm_config_hf.vision_config.projector_hidden_act = "gelu_fast"
40
+ vlm_config_hf.vision_config.torch_dtype = "float32"
41
+
42
+ action_expert_config_hf = CONFIG_MAPPING["gemma"](
43
+ head_dim=action_expert_config.head_dim,
44
+ hidden_size=action_expert_config.width,
45
+ intermediate_size=action_expert_config.mlp_dim,
46
+ num_attention_heads=action_expert_config.num_heads,
47
+ num_hidden_layers=action_expert_config.depth,
48
+ num_key_value_heads=action_expert_config.num_kv_heads,
49
+ vocab_size=257152,
50
+ hidden_activation="gelu_pytorch_tanh",
51
+ torch_dtype="float32",
52
+ use_adarms=use_adarms[1],
53
+ adarms_cond_dim=action_expert_config.width if use_adarms[1] else None,
54
+ )
55
+
56
+ self.paligemma = PaliGemmaForConditionalGeneration(config=vlm_config_hf)
57
+ self.gemma_expert = GemmaForCausalLM(config=action_expert_config_hf)
58
+ self.gemma_expert.model.embed_tokens = None
59
+
60
+ self.to_bfloat16_for_selected_params(precision)
61
+
62
+ def to_bfloat16_for_selected_params(self, precision: Literal["bfloat16", "float32"] = "bfloat16"):
63
+ if precision == "bfloat16":
64
+ self.to(dtype=torch.bfloat16)
65
+ elif precision == "float32":
66
+ self.to(dtype=torch.float32)
67
+ return
68
+ else:
69
+ raise ValueError(f"Invalid precision: {precision}")
70
+
71
+ params_to_keep_float32 = [
72
+ "vision_tower.vision_model.embeddings.patch_embedding.weight",
73
+ "vision_tower.vision_model.embeddings.patch_embedding.bias",
74
+ "vision_tower.vision_model.embeddings.position_embedding.weight",
75
+ "input_layernorm",
76
+ "post_attention_layernorm",
77
+ "model.norm",
78
+ ]
79
+
80
+ for name, param in self.named_parameters():
81
+ if any(selector in name for selector in params_to_keep_float32):
82
+ param.data = param.data.to(dtype=torch.float32)
83
+
84
+ def embed_image(self, image: torch.Tensor):
85
+ return self.paligemma.model.get_image_features(image)
86
+
87
+ def embed_language_tokens(self, tokens: torch.Tensor):
88
+ return self.paligemma.language_model.embed_tokens(tokens)
89
+
90
+ def forward(
91
+ self,
92
+ attention_mask: torch.Tensor | None = None,
93
+ position_ids: torch.LongTensor | None = None,
94
+ past_key_values: list[torch.FloatTensor] | None = None,
95
+ inputs_embeds: list[torch.FloatTensor] | None = None,
96
+ use_cache: bool | None = None,
97
+ adarms_cond: list[torch.Tensor] | None = None,
98
+ ):
99
+ if adarms_cond is None:
100
+ adarms_cond = [None, None]
101
+ if inputs_embeds[1] is None:
102
+ prefix_output = self.paligemma.language_model.forward(
103
+ inputs_embeds=inputs_embeds[0],
104
+ attention_mask=attention_mask,
105
+ position_ids=position_ids,
106
+ past_key_values=past_key_values,
107
+ use_cache=use_cache,
108
+ adarms_cond=adarms_cond[0] if adarms_cond is not None else None,
109
+ )
110
+ prefix_past_key_values = prefix_output.past_key_values
111
+ prefix_output = prefix_output.last_hidden_state
112
+ suffix_output = None
113
+ elif inputs_embeds[0] is None:
114
+ suffix_output = self.gemma_expert.model.forward(
115
+ inputs_embeds=inputs_embeds[1],
116
+ attention_mask=attention_mask,
117
+ position_ids=position_ids,
118
+ past_key_values=past_key_values,
119
+ use_cache=use_cache,
120
+ adarms_cond=adarms_cond[1] if adarms_cond is not None else None,
121
+ )
122
+ suffix_output = suffix_output.last_hidden_state
123
+ prefix_output = None
124
+ prefix_past_key_values = None
125
+ else:
126
+ models = [self.paligemma.language_model, self.gemma_expert.model]
127
+ num_layers = self.paligemma.config.text_config.num_hidden_layers
128
+
129
+ # Check if gradient checkpointing is enabled for any of the models
130
+ use_gradient_checkpointing = (
131
+ hasattr(self.gemma_expert.model, "gradient_checkpointing")
132
+ and self.gemma_expert.model.gradient_checkpointing
133
+ and self.training
134
+ ) or (hasattr(self, "gradient_checkpointing") and self.gradient_checkpointing and self.training)
135
+
136
+ # Force enable gradient checkpointing if we're in training mode and the model supports it
137
+ if self.training and hasattr(self.gemma_expert.model, "gradient_checkpointing"):
138
+ if not self.gemma_expert.model.gradient_checkpointing:
139
+ print("Forcing gradient checkpointing to be enabled for Gemma expert model")
140
+ self.gemma_expert.model.gradient_checkpointing = True
141
+ use_gradient_checkpointing = True
142
+
143
+ # Debug gradient checkpointing status
144
+ if hasattr(self, "_debug_gc_printed") and not self._debug_gc_printed:
145
+ print(f"Gemma expert model gradient checkpointing: {use_gradient_checkpointing}")
146
+ print(f"Model training mode: {self.training}")
147
+ print(
148
+ f"Gemma expert model has gradient_checkpointing attr: {hasattr(self.gemma_expert.model, 'gradient_checkpointing')}"
149
+ )
150
+ if hasattr(self.gemma_expert.model, "gradient_checkpointing"):
151
+ print(
152
+ f"Gemma expert model gradient_checkpointing value: {self.gemma_expert.model.gradient_checkpointing}"
153
+ )
154
+ self._debug_gc_printed = True
155
+
156
+ # Define the complete layer computation function for gradient checkpointing
157
+ def compute_layer_complete(layer_idx, inputs_embeds, attention_mask, position_ids, adarms_cond):
158
+ models = [self.paligemma.language_model, self.gemma_expert.model]
159
+
160
+ query_states = []
161
+ key_states = []
162
+ value_states = []
163
+ gates = []
164
+ for i, hidden_states in enumerate(inputs_embeds):
165
+ layer = models[i].layers[layer_idx]
166
+ hidden_states, gate = layer.input_layernorm(hidden_states, cond=adarms_cond[i]) # noqa: PLW2901
167
+ gates.append(gate)
168
+
169
+ input_shape = hidden_states.shape[:-1]
170
+ hidden_shape = (*input_shape, -1, layer.self_attn.head_dim)
171
+ query_state = layer.self_attn.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
172
+ key_state = layer.self_attn.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
173
+ value_state = layer.self_attn.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
174
+
175
+ query_states.append(query_state)
176
+ key_states.append(key_state)
177
+ value_states.append(value_state)
178
+
179
+ # Concatenate and process attention
180
+ query_states = torch.cat(query_states, dim=2)
181
+ key_states = torch.cat(key_states, dim=2)
182
+ value_states = torch.cat(value_states, dim=2)
183
+
184
+ dummy_tensor = torch.zeros(
185
+ query_states.shape[0],
186
+ query_states.shape[2],
187
+ query_states.shape[-1],
188
+ device=query_states.device,
189
+ dtype=query_states.dtype,
190
+ )
191
+ cos, sin = self.paligemma.model.language_model.rotary_emb(dummy_tensor, position_ids)
192
+ query_states, key_states = modeling_gemma.apply_rotary_pos_emb(
193
+ query_states, key_states, cos, sin, unsqueeze_dim=1
194
+ )
195
+
196
+ batch_size = query_states.shape[0]
197
+ scaling = self.paligemma.language_model.layers[layer_idx].self_attn.scaling
198
+
199
+ # Attention computation
200
+ att_output, _ = modeling_gemma.eager_attention_forward(
201
+ self.paligemma.language_model.layers[layer_idx].self_attn,
202
+ query_states,
203
+ key_states,
204
+ value_states,
205
+ attention_mask,
206
+ scaling,
207
+ )
208
+ # Get head_dim from the current layer, not from the model
209
+ head_dim = self.paligemma.language_model.layers[layer_idx].self_attn.head_dim
210
+ att_output = att_output.reshape(batch_size, -1, 1 * 8 * head_dim)
211
+
212
+ # Process layer outputs
213
+ outputs_embeds = []
214
+ start_pos = 0
215
+ for i, hidden_states in enumerate(inputs_embeds):
216
+ layer = models[i].layers[layer_idx]
217
+ end_pos = start_pos + hidden_states.shape[1]
218
+
219
+ if att_output.dtype != layer.self_attn.o_proj.weight.dtype:
220
+ att_output = att_output.to(layer.self_attn.o_proj.weight.dtype)
221
+ out_emb = layer.self_attn.o_proj(att_output[:, start_pos:end_pos])
222
+
223
+ # first residual
224
+ out_emb = modeling_gemma._gated_residual(hidden_states, out_emb, gates[i]) # noqa: SLF001
225
+ after_first_residual = out_emb.clone()
226
+ out_emb, gate = layer.post_attention_layernorm(out_emb, cond=adarms_cond[i])
227
+ # Convert to bfloat16 if the next layer (mlp) uses bfloat16
228
+ if layer.mlp.up_proj.weight.dtype == torch.bfloat16:
229
+ out_emb = out_emb.to(dtype=torch.bfloat16)
230
+
231
+ out_emb = layer.mlp(out_emb)
232
+ # second residual
233
+ out_emb = modeling_gemma._gated_residual(after_first_residual, out_emb, gate) # noqa: SLF001
234
+ outputs_embeds.append(out_emb)
235
+ start_pos = end_pos
236
+
237
+ return outputs_embeds
238
+
239
+ # Process all layers with gradient checkpointing if enabled
240
+ for layer_idx in range(num_layers):
241
+ if use_gradient_checkpointing:
242
+ inputs_embeds = torch.utils.checkpoint.checkpoint(
243
+ compute_layer_complete,
244
+ layer_idx,
245
+ inputs_embeds,
246
+ attention_mask,
247
+ position_ids,
248
+ adarms_cond,
249
+ use_reentrant=False,
250
+ preserve_rng_state=False,
251
+ )
252
+ else:
253
+ inputs_embeds = compute_layer_complete(
254
+ layer_idx, inputs_embeds, attention_mask, position_ids, adarms_cond
255
+ )
256
+
257
+ # Old code removed - now using compute_layer_complete function above
258
+
259
+ # final norm
260
+ # Define final norm computation function for gradient checkpointing
261
+ def compute_final_norms(inputs_embeds, adarms_cond):
262
+ outputs_embeds = []
263
+ for i, hidden_states in enumerate(inputs_embeds):
264
+ out_emb, _ = models[i].norm(hidden_states, cond=adarms_cond[i])
265
+ outputs_embeds.append(out_emb)
266
+ return outputs_embeds
267
+
268
+ # Apply gradient checkpointing to final norm if enabled
269
+ if use_gradient_checkpointing:
270
+ outputs_embeds = torch.utils.checkpoint.checkpoint(
271
+ compute_final_norms, inputs_embeds, adarms_cond, use_reentrant=False, preserve_rng_state=False
272
+ )
273
+ else:
274
+ outputs_embeds = compute_final_norms(inputs_embeds, adarms_cond)
275
+
276
+ prefix_output = outputs_embeds[0]
277
+ suffix_output = outputs_embeds[1]
278
+ prefix_past_key_values = None
279
+
280
+ return [prefix_output, suffix_output], prefix_past_key_values
openpi_runtime/openpi/models_pytorch/pi0_pytorch.py ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import math
3
+
4
+ import torch
5
+ from torch import Tensor
6
+ from torch import nn
7
+ import torch.nn.functional as F # noqa: N812
8
+
9
+ import openpi.models.gemma as _gemma
10
+ from openpi.models_pytorch.gemma_pytorch import PaliGemmaWithExpertModel
11
+ import openpi.models_pytorch.preprocessing_pytorch as _preprocessing
12
+
13
+
14
+ def get_safe_dtype(target_dtype, device_type):
15
+ """Get a safe dtype for the given device type."""
16
+ if device_type == "cpu":
17
+ # CPU doesn't support bfloat16, use float32 instead
18
+ if target_dtype == torch.bfloat16:
19
+ return torch.float32
20
+ if target_dtype == torch.float64:
21
+ return torch.float64
22
+ return target_dtype
23
+
24
+
25
+ def create_sinusoidal_pos_embedding(
26
+ time: torch.tensor, dimension: int, min_period: float, max_period: float, device="cpu"
27
+ ) -> Tensor:
28
+ """Computes sine-cosine positional embedding vectors for scalar positions."""
29
+ if dimension % 2 != 0:
30
+ raise ValueError(f"dimension ({dimension}) must be divisible by 2")
31
+
32
+ if time.ndim != 1:
33
+ raise ValueError("The time tensor is expected to be of shape `(batch_size, )`.")
34
+
35
+ dtype = get_safe_dtype(torch.float64, device.type)
36
+ fraction = torch.linspace(0.0, 1.0, dimension // 2, dtype=dtype, device=device)
37
+ period = min_period * (max_period / min_period) ** fraction
38
+
39
+ # Compute the outer product
40
+ scaling_factor = 1.0 / period * 2 * math.pi
41
+ sin_input = scaling_factor[None, :] * time[:, None]
42
+ return torch.cat([torch.sin(sin_input), torch.cos(sin_input)], dim=1)
43
+
44
+
45
+ def sample_beta(alpha, beta, bsize, device):
46
+ alpha_t = torch.as_tensor(alpha, dtype=torch.float32, device=device)
47
+ beta_t = torch.as_tensor(beta, dtype=torch.float32, device=device)
48
+ dist = torch.distributions.Beta(alpha_t, beta_t)
49
+ return dist.sample((bsize,))
50
+
51
+
52
+ def make_att_2d_masks(pad_masks, att_masks):
53
+ """Copied from big_vision.
54
+
55
+ Tokens can attend to valid inputs tokens which have a cumulative mask_ar
56
+ smaller or equal to theirs. This way `mask_ar` int[B, N] can be used to
57
+ setup several types of attention, for example:
58
+
59
+ [[1 1 1 1 1 1]]: pure causal attention.
60
+
61
+ [[0 0 0 1 1 1]]: prefix-lm attention. The first 3 tokens can attend between
62
+ themselves and the last 3 tokens have a causal attention. The first
63
+ entry could also be a 1 without changing behaviour.
64
+
65
+ [[1 0 1 0 1 0 0 1 0 0]]: causal attention between 4 blocks. Tokens of a
66
+ block can attend all previous blocks and all tokens on the same block.
67
+
68
+ Args:
69
+ input_mask: bool[B, N] true if its part of the input, false if padding.
70
+ mask_ar: int32[B, N] mask that's 1 where previous tokens cannot depend on
71
+ it and 0 where it shares the same attention mask as the previous token.
72
+ """
73
+ if att_masks.ndim != 2:
74
+ raise ValueError(att_masks.ndim)
75
+ if pad_masks.ndim != 2:
76
+ raise ValueError(pad_masks.ndim)
77
+
78
+ cumsum = torch.cumsum(att_masks, dim=1)
79
+ att_2d_masks = cumsum[:, None, :] <= cumsum[:, :, None]
80
+ pad_2d_masks = pad_masks[:, None, :] * pad_masks[:, :, None]
81
+ return att_2d_masks & pad_2d_masks
82
+
83
+
84
+ class PI0Pytorch(nn.Module):
85
+ def __init__(self, config):
86
+ super().__init__()
87
+ self.config = config
88
+ self.pi05 = config.pi05
89
+
90
+ paligemma_config = _gemma.get_config(config.paligemma_variant)
91
+ action_expert_config = _gemma.get_config(config.action_expert_variant)
92
+
93
+ self.paligemma_with_expert = PaliGemmaWithExpertModel(
94
+ paligemma_config,
95
+ action_expert_config,
96
+ use_adarms=[False, True] if self.pi05 else [False, False],
97
+ precision=config.dtype,
98
+ )
99
+
100
+ self.action_in_proj = nn.Linear(config.action_dim, action_expert_config.width)
101
+ self.action_out_proj = nn.Linear(action_expert_config.width, config.action_dim)
102
+
103
+ if self.pi05:
104
+ self.time_mlp_in = nn.Linear(action_expert_config.width, action_expert_config.width)
105
+ self.time_mlp_out = nn.Linear(action_expert_config.width, action_expert_config.width)
106
+ else:
107
+ self.state_proj = nn.Linear(config.action_dim, action_expert_config.width)
108
+ self.action_time_mlp_in = nn.Linear(2 * action_expert_config.width, action_expert_config.width)
109
+ self.action_time_mlp_out = nn.Linear(action_expert_config.width, action_expert_config.width)
110
+
111
+ torch.set_float32_matmul_precision("high")
112
+ if config.pytorch_compile_mode is not None:
113
+ self.sample_actions = torch.compile(self.sample_actions, mode=config.pytorch_compile_mode)
114
+
115
+ # Initialize gradient checkpointing flag
116
+ self.gradient_checkpointing_enabled = False
117
+
118
+ msg = "transformers_replace is not installed correctly. Please install it with `uv pip install transformers==4.53.2` and `cp -r ./src/openpi/models_pytorch/transformers_replace/* .venv/lib/python3.11/site-packages/transformers/`."
119
+ try:
120
+ from transformers.models.siglip import check
121
+
122
+ if not check.check_whether_transformers_replace_is_installed_correctly():
123
+ raise ValueError(msg)
124
+ except ImportError:
125
+ raise ValueError(msg) from None
126
+
127
+ def gradient_checkpointing_enable(self):
128
+ """Enable gradient checkpointing for memory optimization."""
129
+ self.gradient_checkpointing_enabled = True
130
+ self.paligemma_with_expert.paligemma.language_model.gradient_checkpointing = True
131
+ self.paligemma_with_expert.paligemma.vision_tower.gradient_checkpointing = True
132
+ self.paligemma_with_expert.gemma_expert.model.gradient_checkpointing = True
133
+
134
+ logging.info("Enabled gradient checkpointing for PI0Pytorch model")
135
+
136
+ def gradient_checkpointing_disable(self):
137
+ """Disable gradient checkpointing."""
138
+ self.gradient_checkpointing_enabled = False
139
+ self.paligemma_with_expert.paligemma.language_model.gradient_checkpointing = False
140
+ self.paligemma_with_expert.paligemma.vision_tower.gradient_checkpointing = False
141
+ self.paligemma_with_expert.gemma_expert.model.gradient_checkpointing = False
142
+
143
+ logging.info("Disabled gradient checkpointing for PI0Pytorch model")
144
+
145
+ def is_gradient_checkpointing_enabled(self):
146
+ """Check if gradient checkpointing is enabled."""
147
+ return self.gradient_checkpointing_enabled
148
+
149
+ def _apply_checkpoint(self, func, *args, **kwargs):
150
+ """Helper method to apply gradient checkpointing if enabled."""
151
+ if self.gradient_checkpointing_enabled and self.training:
152
+ return torch.utils.checkpoint.checkpoint(
153
+ func, *args, use_reentrant=False, preserve_rng_state=False, **kwargs
154
+ )
155
+ return func(*args, **kwargs)
156
+
157
+ def _prepare_attention_masks_4d(self, att_2d_masks):
158
+ """Helper method to prepare 4D attention masks for transformer."""
159
+ att_2d_masks_4d = att_2d_masks[:, None, :, :]
160
+ return torch.where(att_2d_masks_4d, 0.0, -2.3819763e38)
161
+
162
+ def _preprocess_observation(self, observation, *, train=True):
163
+ """Helper method to preprocess observation."""
164
+ observation = _preprocessing.preprocess_observation_pytorch(observation, train=train)
165
+ return (
166
+ list(observation.images.values()),
167
+ list(observation.image_masks.values()),
168
+ observation.tokenized_prompt,
169
+ observation.tokenized_prompt_mask,
170
+ observation.state,
171
+ )
172
+
173
+ def sample_noise(self, shape, device):
174
+ return torch.normal(
175
+ mean=0.0,
176
+ std=1.0,
177
+ size=shape,
178
+ dtype=torch.float32,
179
+ device=device,
180
+ )
181
+
182
+ def sample_time(self, bsize, device):
183
+ time_beta = sample_beta(1.5, 1.0, bsize, device)
184
+ time = time_beta * 0.999 + 0.001
185
+ return time.to(dtype=torch.float32, device=device)
186
+
187
+ def embed_prefix(
188
+ self, images, img_masks, lang_tokens, lang_masks
189
+ ) -> tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
190
+ """Embed images with SigLIP and language tokens with embedding layer to prepare
191
+ for PaliGemma transformer processing.
192
+ """
193
+ embs = []
194
+ pad_masks = []
195
+ att_masks = []
196
+
197
+ # Process images
198
+ for img, img_mask in zip(images, img_masks, strict=True):
199
+
200
+ def image_embed_func(img):
201
+ return self.paligemma_with_expert.embed_image(img)
202
+
203
+ img_emb = self._apply_checkpoint(image_embed_func, img)
204
+
205
+ bsize, num_img_embs = img_emb.shape[:2]
206
+
207
+ embs.append(img_emb)
208
+ pad_masks.append(img_mask[:, None].expand(bsize, num_img_embs))
209
+
210
+ # Create attention masks so that image tokens attend to each other
211
+ att_masks += [0] * num_img_embs
212
+
213
+ # Process language tokens
214
+ def lang_embed_func(lang_tokens):
215
+ lang_emb = self.paligemma_with_expert.embed_language_tokens(lang_tokens)
216
+ lang_emb_dim = lang_emb.shape[-1]
217
+ return lang_emb * math.sqrt(lang_emb_dim)
218
+
219
+ lang_emb = self._apply_checkpoint(lang_embed_func, lang_tokens)
220
+
221
+ embs.append(lang_emb)
222
+ pad_masks.append(lang_masks)
223
+
224
+ # full attention between image and language inputs
225
+ num_lang_embs = lang_emb.shape[1]
226
+ att_masks += [0] * num_lang_embs
227
+
228
+ embs = torch.cat(embs, dim=1)
229
+ pad_masks = torch.cat(pad_masks, dim=1)
230
+ att_masks = torch.tensor(att_masks, dtype=torch.bool, device=pad_masks.device)
231
+
232
+ # Get batch size from the first dimension of the concatenated tensors
233
+ bsize = pad_masks.shape[0]
234
+ att_masks = att_masks[None, :].expand(bsize, len(att_masks))
235
+
236
+ return embs, pad_masks, att_masks
237
+
238
+ def embed_suffix(self, state, noisy_actions, timestep):
239
+ """Embed state, noisy_actions, timestep to prepare for Expert Gemma processing."""
240
+ embs = []
241
+ pad_masks = []
242
+ att_masks = []
243
+
244
+ if not self.pi05:
245
+ if self.state_proj.weight.dtype == torch.float32:
246
+ state = state.to(torch.float32)
247
+
248
+ # Embed state
249
+ def state_proj_func(state):
250
+ return self.state_proj(state)
251
+
252
+ state_emb = self._apply_checkpoint(state_proj_func, state)
253
+
254
+ embs.append(state_emb[:, None, :])
255
+ bsize = state_emb.shape[0]
256
+ device = state_emb.device
257
+
258
+ state_mask = torch.ones(bsize, 1, dtype=torch.bool, device=device)
259
+ pad_masks.append(state_mask)
260
+
261
+ # Set attention masks so that image and language inputs do not attend to state or actions
262
+ att_masks += [1]
263
+
264
+ # Embed timestep using sine-cosine positional encoding with sensitivity in the range [0, 1]
265
+ time_emb = create_sinusoidal_pos_embedding(
266
+ timestep, self.action_in_proj.out_features, min_period=4e-3, max_period=4.0, device=timestep.device
267
+ )
268
+ time_emb = time_emb.type(dtype=timestep.dtype)
269
+
270
+ # Fuse timestep + action information using an MLP
271
+ def action_proj_func(noisy_actions):
272
+ return self.action_in_proj(noisy_actions)
273
+
274
+ action_emb = self._apply_checkpoint(action_proj_func, noisy_actions)
275
+
276
+ if not self.pi05:
277
+ time_emb = time_emb[:, None, :].expand_as(action_emb)
278
+ action_time_emb = torch.cat([action_emb, time_emb], dim=2)
279
+
280
+ # Apply MLP layers
281
+ def mlp_func(action_time_emb):
282
+ x = self.action_time_mlp_in(action_time_emb)
283
+ x = F.silu(x) # swish == silu
284
+ return self.action_time_mlp_out(x)
285
+
286
+ action_time_emb = self._apply_checkpoint(mlp_func, action_time_emb)
287
+ adarms_cond = None
288
+ else:
289
+ # time MLP (for adaRMS)
290
+ def time_mlp_func(time_emb):
291
+ x = self.time_mlp_in(time_emb)
292
+ x = F.silu(x) # swish == silu
293
+ x = self.time_mlp_out(x)
294
+ return F.silu(x)
295
+
296
+ time_emb = self._apply_checkpoint(time_mlp_func, time_emb)
297
+ action_time_emb = action_emb
298
+ adarms_cond = time_emb
299
+
300
+ # Add to input tokens
301
+ embs.append(action_time_emb)
302
+
303
+ bsize, action_time_dim = action_time_emb.shape[:2]
304
+ action_time_mask = torch.ones(bsize, action_time_dim, dtype=torch.bool, device=timestep.device)
305
+ pad_masks.append(action_time_mask)
306
+
307
+ # Set attention masks so that image, language and state inputs do not attend to action tokens
308
+ att_masks += [1] + ([0] * (self.config.action_horizon - 1))
309
+
310
+ embs = torch.cat(embs, dim=1)
311
+ pad_masks = torch.cat(pad_masks, dim=1)
312
+ att_masks = torch.tensor(att_masks, dtype=embs.dtype, device=embs.device)
313
+ att_masks = att_masks[None, :].expand(bsize, len(att_masks))
314
+
315
+ return embs, pad_masks, att_masks, adarms_cond
316
+
317
+ def forward(self, observation, actions, noise=None, time=None) -> Tensor:
318
+ """Do a full training forward pass and compute the loss (batch_size x num_steps x num_motors)"""
319
+ images, img_masks, lang_tokens, lang_masks, state = self._preprocess_observation(observation, train=True)
320
+
321
+ if noise is None:
322
+ noise = self.sample_noise(actions.shape, actions.device)
323
+
324
+ if time is None:
325
+ time = self.sample_time(actions.shape[0], actions.device)
326
+
327
+ time_expanded = time[:, None, None]
328
+ x_t = time_expanded * noise + (1 - time_expanded) * actions
329
+ u_t = noise - actions
330
+
331
+ prefix_embs, prefix_pad_masks, prefix_att_masks = self.embed_prefix(images, img_masks, lang_tokens, lang_masks)
332
+ suffix_embs, suffix_pad_masks, suffix_att_masks, adarms_cond = self.embed_suffix(state, x_t, time)
333
+ if (
334
+ self.paligemma_with_expert.paligemma.language_model.layers[0].self_attn.q_proj.weight.dtype
335
+ == torch.bfloat16
336
+ ):
337
+ suffix_embs = suffix_embs.to(dtype=torch.bfloat16)
338
+ prefix_embs = prefix_embs.to(dtype=torch.bfloat16)
339
+
340
+ pad_masks = torch.cat([prefix_pad_masks, suffix_pad_masks], dim=1)
341
+ att_masks = torch.cat([prefix_att_masks, suffix_att_masks], dim=1)
342
+
343
+ att_2d_masks = make_att_2d_masks(pad_masks, att_masks)
344
+ position_ids = torch.cumsum(pad_masks, dim=1) - 1
345
+
346
+ # Prepare attention masks
347
+ att_2d_masks_4d = self._prepare_attention_masks_4d(att_2d_masks)
348
+
349
+ # Apply gradient checkpointing if enabled
350
+ def forward_func(prefix_embs, suffix_embs, att_2d_masks_4d, position_ids, adarms_cond):
351
+ (_, suffix_out), _ = self.paligemma_with_expert.forward(
352
+ attention_mask=att_2d_masks_4d,
353
+ position_ids=position_ids,
354
+ past_key_values=None,
355
+ inputs_embeds=[prefix_embs, suffix_embs],
356
+ use_cache=False,
357
+ adarms_cond=[None, adarms_cond],
358
+ )
359
+ return suffix_out
360
+
361
+ suffix_out = self._apply_checkpoint(
362
+ forward_func, prefix_embs, suffix_embs, att_2d_masks_4d, position_ids, adarms_cond
363
+ )
364
+
365
+ suffix_out = suffix_out[:, -self.config.action_horizon :]
366
+ suffix_out = suffix_out.to(dtype=torch.float32)
367
+
368
+ # Apply gradient checkpointing to final action projection if enabled
369
+ def action_out_proj_func(suffix_out):
370
+ return self.action_out_proj(suffix_out)
371
+
372
+ v_t = self._apply_checkpoint(action_out_proj_func, suffix_out)
373
+
374
+ return F.mse_loss(u_t, v_t, reduction="none")
375
+
376
+ @torch.no_grad()
377
+ def sample_actions(self, device, observation, noise=None, num_steps=10) -> Tensor:
378
+ """Do a full inference forward and compute the action (batch_size x num_steps x num_motors)"""
379
+ bsize = observation.state.shape[0]
380
+ if noise is None:
381
+ actions_shape = (bsize, self.config.action_horizon, self.config.action_dim)
382
+ noise = self.sample_noise(actions_shape, device)
383
+
384
+ images, img_masks, lang_tokens, lang_masks, state = self._preprocess_observation(observation, train=False)
385
+
386
+ prefix_embs, prefix_pad_masks, prefix_att_masks = self.embed_prefix(images, img_masks, lang_tokens, lang_masks)
387
+ prefix_att_2d_masks = make_att_2d_masks(prefix_pad_masks, prefix_att_masks)
388
+ prefix_position_ids = torch.cumsum(prefix_pad_masks, dim=1) - 1
389
+
390
+ # Compute image and language key value cache
391
+ prefix_att_2d_masks_4d = self._prepare_attention_masks_4d(prefix_att_2d_masks)
392
+ self.paligemma_with_expert.paligemma.language_model.config._attn_implementation = "eager" # noqa: SLF001
393
+
394
+ _, past_key_values = self.paligemma_with_expert.forward(
395
+ attention_mask=prefix_att_2d_masks_4d,
396
+ position_ids=prefix_position_ids,
397
+ past_key_values=None,
398
+ inputs_embeds=[prefix_embs, None],
399
+ use_cache=True,
400
+ )
401
+
402
+ dt = -1.0 / num_steps
403
+ dt = torch.tensor(dt, dtype=torch.float32, device=device)
404
+
405
+ x_t = noise
406
+ time = torch.tensor(1.0, dtype=torch.float32, device=device)
407
+ while time >= -dt / 2:
408
+ expanded_time = time.expand(bsize)
409
+ v_t = self.denoise_step(
410
+ state,
411
+ prefix_pad_masks,
412
+ past_key_values,
413
+ x_t,
414
+ expanded_time,
415
+ )
416
+
417
+ # Euler step - use new tensor assignment instead of in-place operation
418
+ x_t = x_t + dt * v_t
419
+ time += dt
420
+ return x_t
421
+
422
+ def denoise_step(
423
+ self,
424
+ state,
425
+ prefix_pad_masks,
426
+ past_key_values,
427
+ x_t,
428
+ timestep,
429
+ ):
430
+ """Apply one denoising step of the noise `x_t` at a given timestep."""
431
+ suffix_embs, suffix_pad_masks, suffix_att_masks, adarms_cond = self.embed_suffix(state, x_t, timestep)
432
+
433
+ suffix_len = suffix_pad_masks.shape[1]
434
+ batch_size = prefix_pad_masks.shape[0]
435
+ prefix_len = prefix_pad_masks.shape[1]
436
+
437
+ prefix_pad_2d_masks = prefix_pad_masks[:, None, :].expand(batch_size, suffix_len, prefix_len)
438
+
439
+ suffix_att_2d_masks = make_att_2d_masks(suffix_pad_masks, suffix_att_masks)
440
+
441
+ full_att_2d_masks = torch.cat([prefix_pad_2d_masks, suffix_att_2d_masks], dim=2)
442
+
443
+ prefix_offsets = torch.sum(prefix_pad_masks, dim=-1)[:, None]
444
+ position_ids = prefix_offsets + torch.cumsum(suffix_pad_masks, dim=1) - 1
445
+
446
+ # Prepare attention masks
447
+ full_att_2d_masks_4d = self._prepare_attention_masks_4d(full_att_2d_masks)
448
+ self.paligemma_with_expert.gemma_expert.model.config._attn_implementation = "eager" # noqa: SLF001
449
+
450
+ outputs_embeds, _ = self.paligemma_with_expert.forward(
451
+ attention_mask=full_att_2d_masks_4d,
452
+ position_ids=position_ids,
453
+ past_key_values=past_key_values,
454
+ inputs_embeds=[None, suffix_embs],
455
+ use_cache=False,
456
+ adarms_cond=[None, adarms_cond],
457
+ )
458
+
459
+ suffix_out = outputs_embeds[1]
460
+ suffix_out = suffix_out[:, -self.config.action_horizon :]
461
+ suffix_out = suffix_out.to(dtype=torch.float32)
462
+ return self.action_out_proj(suffix_out)
openpi_runtime/openpi/models_pytorch/preprocessing_pytorch.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Sequence
2
+ import logging
3
+
4
+ import torch
5
+
6
+ from openpi.shared import image_tools
7
+
8
+ logger = logging.getLogger("openpi")
9
+
10
+ # Constants moved from model.py
11
+ IMAGE_KEYS = (
12
+ "base_0_rgb",
13
+ "left_wrist_0_rgb",
14
+ "right_wrist_0_rgb",
15
+ )
16
+
17
+ IMAGE_RESOLUTION = (224, 224)
18
+
19
+
20
+ def preprocess_observation_pytorch(
21
+ observation,
22
+ *,
23
+ train: bool = False,
24
+ image_keys: Sequence[str] = IMAGE_KEYS,
25
+ image_resolution: tuple[int, int] = IMAGE_RESOLUTION,
26
+ ):
27
+ """Torch.compile-compatible version of preprocess_observation_pytorch with simplified type annotations.
28
+
29
+ This function avoids complex type annotations that can cause torch.compile issues.
30
+ """
31
+ if not set(image_keys).issubset(observation.images):
32
+ raise ValueError(f"images dict missing keys: expected {image_keys}, got {list(observation.images)}")
33
+
34
+ batch_shape = observation.state.shape[:-1]
35
+
36
+ out_images = {}
37
+ for key in image_keys:
38
+ image = observation.images[key]
39
+
40
+ # TODO: This is a hack to handle both [B, C, H, W] and [B, H, W, C] formats
41
+ # Handle both [B, C, H, W] and [B, H, W, C] formats
42
+ is_channels_first = image.shape[1] == 3 # Check if channels are in dimension 1
43
+
44
+ if is_channels_first:
45
+ # Convert [B, C, H, W] to [B, H, W, C] for processing
46
+ image = image.permute(0, 2, 3, 1)
47
+
48
+ if image.shape[1:3] != image_resolution:
49
+ logger.info(f"Resizing image {key} from {image.shape[1:3]} to {image_resolution}")
50
+ image = image_tools.resize_with_pad_torch(image, *image_resolution)
51
+
52
+ if train:
53
+ # Convert from [-1, 1] to [0, 1] for PyTorch augmentations
54
+ image = image / 2.0 + 0.5
55
+
56
+ # Apply PyTorch-based augmentations
57
+ if "wrist" not in key:
58
+ # Geometric augmentations for non-wrist cameras
59
+ height, width = image.shape[1:3]
60
+
61
+ # Random crop and resize
62
+ crop_height = int(height * 0.95)
63
+ crop_width = int(width * 0.95)
64
+
65
+ # Random crop
66
+ max_h = height - crop_height
67
+ max_w = width - crop_width
68
+ if max_h > 0 and max_w > 0:
69
+ # Use tensor operations instead of .item() for torch.compile compatibility
70
+ start_h = torch.randint(0, max_h + 1, (1,), device=image.device)
71
+ start_w = torch.randint(0, max_w + 1, (1,), device=image.device)
72
+ image = image[:, start_h : start_h + crop_height, start_w : start_w + crop_width, :]
73
+
74
+ # Resize back to original size
75
+ image = torch.nn.functional.interpolate(
76
+ image.permute(0, 3, 1, 2), # [b, h, w, c] -> [b, c, h, w]
77
+ size=(height, width),
78
+ mode="bilinear",
79
+ align_corners=False,
80
+ ).permute(0, 2, 3, 1) # [b, c, h, w] -> [b, h, w, c]
81
+
82
+ # Random rotation (small angles)
83
+ # Use tensor operations instead of .item() for torch.compile compatibility
84
+ angle = torch.rand(1, device=image.device) * 10 - 5 # Random angle between -5 and 5 degrees
85
+ if torch.abs(angle) > 0.1: # Only rotate if angle is significant
86
+ # Convert to radians
87
+ angle_rad = angle * torch.pi / 180.0
88
+
89
+ # Create rotation matrix
90
+ cos_a = torch.cos(angle_rad)
91
+ sin_a = torch.sin(angle_rad)
92
+
93
+ # Apply rotation using grid_sample
94
+ grid_x = torch.linspace(-1, 1, width, device=image.device)
95
+ grid_y = torch.linspace(-1, 1, height, device=image.device)
96
+
97
+ # Create meshgrid
98
+ grid_y, grid_x = torch.meshgrid(grid_y, grid_x, indexing="ij")
99
+
100
+ # Expand to batch dimension
101
+ grid_x = grid_x.unsqueeze(0).expand(image.shape[0], -1, -1)
102
+ grid_y = grid_y.unsqueeze(0).expand(image.shape[0], -1, -1)
103
+
104
+ # Apply rotation transformation
105
+ grid_x_rot = grid_x * cos_a - grid_y * sin_a
106
+ grid_y_rot = grid_x * sin_a + grid_y * cos_a
107
+
108
+ # Stack and reshape for grid_sample
109
+ grid = torch.stack([grid_x_rot, grid_y_rot], dim=-1)
110
+
111
+ image = torch.nn.functional.grid_sample(
112
+ image.permute(0, 3, 1, 2), # [b, h, w, c] -> [b, c, h, w]
113
+ grid,
114
+ mode="bilinear",
115
+ padding_mode="zeros",
116
+ align_corners=False,
117
+ ).permute(0, 2, 3, 1) # [b, c, h, w] -> [b, h, w, c]
118
+
119
+ # Color augmentations for all cameras
120
+ # Random brightness
121
+ # Use tensor operations instead of .item() for torch.compile compatibility
122
+ brightness_factor = 0.7 + torch.rand(1, device=image.device) * 0.6 # Random factor between 0.7 and 1.3
123
+ image = image * brightness_factor
124
+
125
+ # Random contrast
126
+ # Use tensor operations instead of .item() for torch.compile compatibility
127
+ contrast_factor = 0.6 + torch.rand(1, device=image.device) * 0.8 # Random factor between 0.6 and 1.4
128
+ mean = image.mean(dim=[1, 2, 3], keepdim=True)
129
+ image = (image - mean) * contrast_factor + mean
130
+
131
+ # Random saturation (convert to HSV, modify S, convert back)
132
+ # For simplicity, we'll just apply a random scaling to the color channels
133
+ # Use tensor operations instead of .item() for torch.compile compatibility
134
+ saturation_factor = 0.5 + torch.rand(1, device=image.device) * 1.0 # Random factor between 0.5 and 1.5
135
+ gray = image.mean(dim=-1, keepdim=True)
136
+ image = gray + (image - gray) * saturation_factor
137
+
138
+ # Clamp values to [0, 1]
139
+ image = torch.clamp(image, 0, 1)
140
+
141
+ # Back to [-1, 1]
142
+ image = image * 2.0 - 1.0
143
+
144
+ # Convert back to [B, C, H, W] format if it was originally channels-first
145
+ if is_channels_first:
146
+ image = image.permute(0, 3, 1, 2) # [B, H, W, C] -> [B, C, H, W]
147
+
148
+ out_images[key] = image
149
+
150
+ # obtain mask
151
+ out_masks = {}
152
+ for key in out_images:
153
+ if key not in observation.image_masks:
154
+ # do not mask by default
155
+ out_masks[key] = torch.ones(batch_shape, dtype=torch.bool, device=observation.state.device)
156
+ else:
157
+ out_masks[key] = observation.image_masks[key]
158
+
159
+ # Create a simple object with the required attributes instead of using the complex Observation class
160
+ class SimpleProcessedObservation:
161
+ def __init__(self, **kwargs):
162
+ for key, value in kwargs.items():
163
+ setattr(self, key, value)
164
+
165
+ return SimpleProcessedObservation(
166
+ images=out_images,
167
+ image_masks=out_masks,
168
+ state=observation.state,
169
+ tokenized_prompt=observation.tokenized_prompt,
170
+ tokenized_prompt_mask=observation.tokenized_prompt_mask,
171
+ token_ar_mask=observation.token_ar_mask,
172
+ token_loss_mask=observation.token_loss_mask,
173
+ )
openpi_runtime/openpi/models_pytorch/transformers_replace/models/gemma/configuration_gemma.py ADDED
@@ -0,0 +1,173 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
2
+ # This file was automatically generated from src/transformers/models/gemma/modular_gemma.py.
3
+ # Do NOT edit this file manually as any edits will be overwritten by the generation of
4
+ # the file from the modular. If any change should be done, please apply the change to the
5
+ # modular_gemma.py file directly. One of our CI enforces this.
6
+ # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
7
+ # coding=utf-8
8
+ # Copyright 2024 Google Inc. HuggingFace Inc. team. All rights reserved.
9
+ #
10
+ #
11
+ # Licensed under the Apache License, Version 2.0 (the "License");
12
+ # you may not use this file except in compliance with the License.
13
+ # You may obtain a copy of the License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the License is distributed on an "AS IS" BASIS,
19
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ # See the License for the specific language governing permissions and
21
+ # limitations under the License.
22
+ from typing import Optional
23
+ from ...configuration_utils import PretrainedConfig
24
+
25
+
26
+ class GemmaConfig(PretrainedConfig):
27
+ r"""
28
+ This is the configuration class to store the configuration of a [`GemmaModel`]. It is used to instantiate an Gemma
29
+ model according to the specified arguments, defining the model architecture. Instantiating a configuration with the
30
+ defaults will yield a similar configuration to that of the Gemma-7B.
31
+ e.g. [google/gemma-7b](https://huggingface.co/google/gemma-7b)
32
+ Configuration objects inherit from [`PretrainedConfig`] and can be used to control the model outputs. Read the
33
+ documentation from [`PretrainedConfig`] for more information.
34
+ Args:
35
+ vocab_size (`int`, *optional*, defaults to 256000):
36
+ Vocabulary size of the Gemma model. Defines the number of different tokens that can be represented by the
37
+ `inputs_ids` passed when calling [`GemmaModel`]
38
+ hidden_size (`int`, *optional*, defaults to 3072):
39
+ Dimension of the hidden representations.
40
+ intermediate_size (`int`, *optional*, defaults to 24576):
41
+ Dimension of the MLP representations.
42
+ num_hidden_layers (`int`, *optional*, defaults to 28):
43
+ Number of hidden layers in the Transformer decoder.
44
+ num_attention_heads (`int`, *optional*, defaults to 16):
45
+ Number of attention heads for each attention layer in the Transformer decoder.
46
+ num_key_value_heads (`int`, *optional*, defaults to 16):
47
+ This is the number of key_value heads that should be used to implement Grouped Query Attention. If
48
+ `num_key_value_heads=num_attention_heads`, the model will use Multi Head Attention (MHA), if
49
+ `num_key_value_heads=1` the model will use Multi Query Attention (MQA) otherwise GQA is used. When
50
+ converting a multi-head checkpoint to a GQA checkpoint, each group key and value head should be constructed
51
+ by meanpooling all the original heads within that group. For more details, check out [this
52
+ paper](https://huggingface.co/papers/2305.13245). If it is not specified, will default to
53
+ `num_attention_heads`.
54
+ head_dim (`int`, *optional*, defaults to 256):
55
+ The attention head dimension.
56
+ hidden_act (`str` or `function`, *optional*, defaults to `"gelu_pytorch_tanh"`):
57
+ The legacy activation function. It is overwritten by the `hidden_activation`.
58
+ hidden_activation (`str` or `function`, *optional*):
59
+ The non-linear activation function (function or string) in the decoder. Will default to `"gelu_pytorch_tanh"`
60
+ if not specified. `"gelu_pytorch_tanh"` uses an approximation of the `"gelu"` activation function.
61
+ max_position_embeddings (`int`, *optional*, defaults to 8192):
62
+ The maximum sequence length that this model might ever be used with.
63
+ initializer_range (`float`, *optional*, defaults to 0.02):
64
+ The standard deviation of the truncated_normal_initializer for initializing all weight matrices.
65
+ rms_norm_eps (`float`, *optional*, defaults to 1e-06):
66
+ The epsilon used by the rms normalization layers.
67
+ use_cache (`bool`, *optional*, defaults to `True`):
68
+ Whether or not the model should return the last key/values attentions (not used by all models). Only
69
+ relevant if `config.is_decoder=True`.
70
+ pad_token_id (`int`, *optional*, defaults to 0):
71
+ Padding token id.
72
+ eos_token_id (`int`, *optional*, defaults to 1):
73
+ End of stream token id.
74
+ bos_token_id (`int`, *optional*, defaults to 2):
75
+ Beginning of stream token id.
76
+ tie_word_embeddings (`bool`, *optional*, defaults to `True`):
77
+ Whether to tie weight embeddings
78
+ rope_theta (`float`, *optional*, defaults to 10000.0):
79
+ The base period of the RoPE embeddings.
80
+ attention_bias (`bool`, defaults to `False`, *optional*, defaults to `False`):
81
+ Whether to use a bias in the query, key, value and output projection layers during self-attention.
82
+ attention_dropout (`float`, *optional*, defaults to 0.0):
83
+ The dropout ratio for the attention probabilities.
84
+ use_adarms (`bool`, *optional*, defaults to `False`):
85
+ Whether to use ADARMS.
86
+ adarms_cond_dim (`int`, *optional*, defaults to `None`):
87
+ The dimension of the ADARMS condition.
88
+ ```python
89
+ >>> from transformers import GemmaModel, GemmaConfig
90
+ >>> # Initializing a Gemma gemma-7b style configuration
91
+ >>> configuration = GemmaConfig()
92
+ >>> # Initializing a model from the gemma-7b style configuration
93
+ >>> model = GemmaModel(configuration)
94
+ >>> # Accessing the model configuration
95
+ >>> configuration = model.config
96
+ ```"""
97
+
98
+ model_type = "gemma"
99
+ keys_to_ignore_at_inference = ["past_key_values"]
100
+ base_model_tp_plan = {
101
+ "layers.*.self_attn.q_proj": "colwise",
102
+ "layers.*.self_attn.k_proj": "colwise",
103
+ "layers.*.self_attn.v_proj": "colwise",
104
+ "layers.*.self_attn.o_proj": "rowwise",
105
+ "layers.*.mlp.gate_proj": "colwise",
106
+ "layers.*.mlp.up_proj": "colwise",
107
+ "layers.*.mlp.down_proj": "rowwise",
108
+ }
109
+ base_model_pp_plan = {
110
+ "embed_tokens": (["input_ids"], ["inputs_embeds"]),
111
+ "layers": (["hidden_states", "attention_mask"], ["hidden_states"]),
112
+ "norm": (["hidden_states"], ["hidden_states"]),
113
+ }
114
+
115
+ def __init__(
116
+ self,
117
+ vocab_size=256000,
118
+ hidden_size=3072,
119
+ intermediate_size=24576,
120
+ num_hidden_layers=28,
121
+ num_attention_heads=16,
122
+ num_key_value_heads=16,
123
+ head_dim=256,
124
+ hidden_act="gelu_pytorch_tanh",
125
+ hidden_activation=None,
126
+ max_position_embeddings=8192,
127
+ initializer_range=0.02,
128
+ rms_norm_eps=1e-6,
129
+ use_cache=True,
130
+ pad_token_id=0,
131
+ eos_token_id=1,
132
+ bos_token_id=2,
133
+ tie_word_embeddings=True,
134
+ rope_theta=10000.0,
135
+ attention_bias=False,
136
+ attention_dropout=0.0,
137
+ use_adarms: bool = False,
138
+ adarms_cond_dim: Optional[int] = None,
139
+ **kwargs,
140
+ ):
141
+ self.vocab_size = vocab_size
142
+ self.max_position_embeddings = max_position_embeddings
143
+ self.hidden_size = hidden_size
144
+ self.intermediate_size = intermediate_size
145
+ self.num_hidden_layers = num_hidden_layers
146
+ self.num_attention_heads = num_attention_heads
147
+ self.head_dim = head_dim
148
+ self.num_key_value_heads = num_key_value_heads
149
+ self.hidden_act = hidden_act
150
+ self.hidden_activation = hidden_activation
151
+ self.initializer_range = initializer_range
152
+ self.rms_norm_eps = rms_norm_eps
153
+ self.use_cache = use_cache
154
+ self.rope_theta = rope_theta
155
+ self.attention_bias = attention_bias
156
+ self.attention_dropout = attention_dropout
157
+ self.use_adarms = use_adarms
158
+ self.adarms_cond_dim = adarms_cond_dim
159
+
160
+ # Set default for adarms_cond_dim if use_adarms is True
161
+ if self.use_adarms and self.adarms_cond_dim is None:
162
+ self.adarms_cond_dim = self.hidden_size
163
+
164
+ super().__init__(
165
+ pad_token_id=pad_token_id,
166
+ bos_token_id=bos_token_id,
167
+ eos_token_id=eos_token_id,
168
+ tie_word_embeddings=tie_word_embeddings,
169
+ **kwargs,
170
+ )
171
+
172
+
173
+ __all__ = ["GemmaConfig"]
openpi_runtime/openpi/models_pytorch/transformers_replace/models/gemma/modeling_gemma.py ADDED
@@ -0,0 +1,862 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
2
+ # This file was automatically generated from src/transformers/models/gemma/modular_gemma.py.
3
+ # Do NOT edit this file manually as any edits will be overwritten by the generation of
4
+ # the file from the modular. If any change should be done, please apply the change to the
5
+ # modular_gemma.py file directly. One of our CI enforces this.
6
+ # 🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨🚨
7
+ # coding=utf-8
8
+ # Copyright 2024 Google Inc. HuggingFace Inc. team. All rights reserved.
9
+ #
10
+ #
11
+ # Licensed under the Apache License, Version 2.0 (the "License");
12
+ # you may not use this file except in compliance with the License.
13
+ # You may obtain a copy of the License at
14
+ #
15
+ # http://www.apache.org/licenses/LICENSE-2.0
16
+ #
17
+ # Unless required by applicable law or agreed to in writing, software
18
+ # distributed under the License is distributed on an "AS IS" BASIS,
19
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
20
+ # See the License for the specific language governing permissions and
21
+ # limitations under the License.
22
+ from typing import Callable, Optional, Union
23
+
24
+ import torch
25
+ from torch import nn
26
+
27
+ from ...activations import ACT2FN
28
+ from ...cache_utils import Cache, DynamicCache
29
+ from ...generation import GenerationMixin
30
+ from ...masking_utils import create_causal_mask
31
+ from ...modeling_flash_attention_utils import FlashAttentionKwargs
32
+ from ...modeling_layers import GradientCheckpointingLayer
33
+ from ...modeling_outputs import (
34
+ BaseModelOutputWithPast,
35
+ CausalLMOutputWithPast,
36
+ SequenceClassifierOutputWithPast,
37
+ TokenClassifierOutput,
38
+ )
39
+ from ...modeling_rope_utils import ROPE_INIT_FUNCTIONS, dynamic_rope_update
40
+ from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
41
+ from ...processing_utils import Unpack
42
+ from ...utils import LossKwargs, auto_docstring, can_return_tuple, logging
43
+ from .configuration_gemma import GemmaConfig
44
+
45
+
46
+ logger = logging.get_logger(__name__)
47
+
48
+
49
+ class GemmaRMSNorm(nn.Module):
50
+ def __init__(self, dim: int, eps: float = 1e-6, cond_dim: Optional[int] = None):
51
+ super().__init__()
52
+ self.eps = eps
53
+ self.dim = dim
54
+ self.cond_dim = cond_dim
55
+
56
+ # Dense layer for adaptive normalization (if cond_dim is provided)
57
+ if cond_dim is not None:
58
+ #self.dense = nn.Linear(cond_dim, dim * 3, bias=True, dtype=torch.bfloat16)
59
+ self.dense = nn.Linear(cond_dim, dim * 3, bias=True)
60
+ # Initialize with zeros (matches source implementation)
61
+ nn.init.zeros_(self.dense.weight)
62
+ else:
63
+ self.weight = nn.Parameter(torch.zeros(dim, dtype=torch.bfloat16))
64
+ self.dense = None
65
+
66
+ def _norm(self, x):
67
+ # Compute variance in float32 (like the source implementation)
68
+ var = torch.mean(torch.square(x.float()), dim=-1, keepdim=True)
69
+ # Compute normalization in float32
70
+ normed_inputs = x * torch.rsqrt(var + self.eps)
71
+ return normed_inputs
72
+
73
+ def forward(self, x, cond=None):
74
+ dtype = x.dtype # original dtype, could be half-precision
75
+ normed_inputs = self._norm(x)
76
+
77
+ if cond is None or self.dense is None:
78
+ # regular RMSNorm
79
+ # scale by learned parameter in float32 (matches source implementation)
80
+ normed_inputs = normed_inputs * (1.0 + self.weight.float())
81
+ return normed_inputs.to(dtype), None # return in original dtype with None gate
82
+
83
+ # adaptive RMSNorm (if cond is provided and dense layer exists)
84
+ if cond.shape[-1] != self.cond_dim:
85
+ raise ValueError(f"Expected cond dimension {self.cond_dim}, got {cond.shape[-1]}")
86
+
87
+ #self.dense.to(dtype=torch.bfloat16).to(dtype=torch.float32)
88
+ modulation = self.dense(cond)
89
+ # Reshape modulation to broadcast properly: [batch, 1, features] for [batch, seq, features]
90
+ if len(x.shape) == 3: # [batch, seq, features]
91
+ modulation = modulation.unsqueeze(1)
92
+
93
+ scale, shift, gate = torch.chunk(modulation, 3, dim=-1)
94
+
95
+ # Apply adaptive normalization: use model weight dtype to ensure compatibility
96
+ # model_dtype = self.dense.weight.dtype # Use the model's dtype (bfloat16)
97
+ # scale = scale.to(model_dtype)
98
+ # shift = shift.to(model_dtype)
99
+ # gate = gate.to(model_dtype)
100
+ # normed_inputs = normed_inputs.to(model_dtype) # Convert normed_inputs to model dtype
101
+
102
+ normed_inputs = normed_inputs * (1 + scale.to(torch.float32)) + shift.to(torch.float32)
103
+
104
+ return normed_inputs.to(dtype), gate.to(dtype)
105
+
106
+ def extra_repr(self):
107
+ repr_str = f"{tuple(self.weight.shape)}, eps={self.eps}"
108
+ if self.dense is not None:
109
+ repr_str += f", adaptive=True, cond_dim={self.cond_dim}"
110
+ return repr_str
111
+
112
+
113
+ class GemmaMLP(nn.Module):
114
+ def __init__(self, config):
115
+ super().__init__()
116
+ self.config = config
117
+ self.hidden_size = config.hidden_size
118
+ self.intermediate_size = config.intermediate_size
119
+ self.gate_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
120
+ self.up_proj = nn.Linear(self.hidden_size, self.intermediate_size, bias=False)
121
+ self.down_proj = nn.Linear(self.intermediate_size, self.hidden_size, bias=False)
122
+ self.act_fn = ACT2FN[config.hidden_act]
123
+
124
+ def forward(self, x):
125
+ down_proj = self.down_proj(self.act_fn(self.gate_proj(x)) * self.up_proj(x))
126
+ return down_proj
127
+
128
+
129
+ class GemmaRotaryEmbedding(nn.Module):
130
+ def __init__(self, config: GemmaConfig, device=None):
131
+ super().__init__()
132
+ # BC: "rope_type" was originally "type"
133
+ if hasattr(config, "rope_scaling") and config.rope_scaling is not None:
134
+ self.rope_type = config.rope_scaling.get("rope_type", config.rope_scaling.get("type"))
135
+ else:
136
+ self.rope_type = "default"
137
+ self.max_seq_len_cached = config.max_position_embeddings
138
+ self.original_max_seq_len = config.max_position_embeddings
139
+
140
+ self.config = config
141
+ self.rope_init_fn = ROPE_INIT_FUNCTIONS[self.rope_type]
142
+
143
+ inv_freq, self.attention_scaling = self.rope_init_fn(self.config, device)
144
+ self.register_buffer("inv_freq", inv_freq, persistent=False)
145
+ self.original_inv_freq = self.inv_freq
146
+
147
+ @torch.no_grad()
148
+ @dynamic_rope_update # power user: used with advanced RoPE types (e.g. dynamic rope)
149
+ def forward(self, x, position_ids):
150
+ inv_freq_expanded = self.inv_freq[None, :, None].float().expand(position_ids.shape[0], -1, 1).to(x.device)
151
+ position_ids_expanded = position_ids[:, None, :].float()
152
+
153
+ device_type = x.device.type if isinstance(x.device.type, str) and x.device.type != "mps" else "cpu"
154
+ with torch.autocast(device_type=device_type, enabled=False): # Force float32
155
+ freqs = (inv_freq_expanded.float() @ position_ids_expanded.float()).transpose(1, 2)
156
+ emb = torch.cat((freqs, freqs), dim=-1)
157
+ cos = emb.cos() * self.attention_scaling
158
+ sin = emb.sin() * self.attention_scaling
159
+
160
+ return cos.to(dtype=x.dtype), sin.to(dtype=x.dtype)
161
+
162
+
163
+ def rotate_half(x):
164
+ """Rotates half the hidden dims of the input."""
165
+ x1 = x[..., : x.shape[-1] // 2]
166
+ x2 = x[..., x.shape[-1] // 2 :]
167
+ return torch.cat((-x2, x1), dim=-1)
168
+
169
+
170
+ def apply_rotary_pos_emb(q, k, cos, sin, position_ids=None, unsqueeze_dim=1):
171
+ """Applies Rotary Position Embedding to the query and key tensors.
172
+
173
+ Args:
174
+ q (`torch.Tensor`): The query tensor.
175
+ k (`torch.Tensor`): The key tensor.
176
+ cos (`torch.Tensor`): The cosine part of the rotary embedding.
177
+ sin (`torch.Tensor`): The sine part of the rotary embedding.
178
+ position_ids (`torch.Tensor`, *optional*):
179
+ Deprecated and unused.
180
+ unsqueeze_dim (`int`, *optional*, defaults to 1):
181
+ The 'unsqueeze_dim' argument specifies the dimension along which to unsqueeze cos[position_ids] and
182
+ sin[position_ids] so that they can be properly broadcasted to the dimensions of q and k. For example, note
183
+ that cos[position_ids] and sin[position_ids] have the shape [batch_size, seq_len, head_dim]. Then, if q and
184
+ k have the shape [batch_size, heads, seq_len, head_dim], then setting unsqueeze_dim=1 makes
185
+ cos[position_ids] and sin[position_ids] broadcastable to the shapes of q and k. Similarly, if q and k have
186
+ the shape [batch_size, seq_len, heads, head_dim], then set unsqueeze_dim=2.
187
+ Returns:
188
+ `tuple(torch.Tensor)` comprising of the query and key tensors rotated using the Rotary Position Embedding.
189
+ """
190
+ cos = cos.unsqueeze(unsqueeze_dim)
191
+ sin = sin.unsqueeze(unsqueeze_dim)
192
+ q_embed = (q * cos) + (rotate_half(q) * sin)
193
+ k_embed = (k * cos) + (rotate_half(k) * sin)
194
+ return q_embed, k_embed
195
+
196
+
197
+ def repeat_kv(hidden_states: torch.Tensor, n_rep: int) -> torch.Tensor:
198
+ """
199
+ This is the equivalent of torch.repeat_interleave(x, dim=1, repeats=n_rep). The hidden states go from (batch,
200
+ num_key_value_heads, seqlen, head_dim) to (batch, num_attention_heads, seqlen, head_dim)
201
+ """
202
+ batch, num_key_value_heads, slen, head_dim = hidden_states.shape
203
+ if n_rep == 1:
204
+ return hidden_states
205
+ hidden_states = hidden_states[:, :, None, :, :].expand(batch, num_key_value_heads, n_rep, slen, head_dim)
206
+ return hidden_states.reshape(batch, num_key_value_heads * n_rep, slen, head_dim)
207
+
208
+
209
+ def _gated_residual(x, y, gate):
210
+ """
211
+ Applies gated residual connection with optional gate parameter.
212
+
213
+ Args:
214
+ x: Input tensor (residual)
215
+ y: Output tensor to be added
216
+ gate: Optional gate tensor to modulate the addition
217
+
218
+ Returns:
219
+ x + y if gate is None, otherwise x + y * gate
220
+ """
221
+ if x is None and y is None:
222
+ return None
223
+ if x is None or y is None:
224
+ return x if x is not None else y
225
+ if gate is None:
226
+ return x + y
227
+ return x + y * gate
228
+
229
+
230
+ def eager_attention_forward(
231
+ module: nn.Module,
232
+ query: torch.Tensor,
233
+ key: torch.Tensor,
234
+ value: torch.Tensor,
235
+ attention_mask: Optional[torch.Tensor],
236
+ scaling: float,
237
+ dropout: float = 0.0,
238
+ **kwargs,
239
+ ):
240
+ key_states = repeat_kv(key, module.num_key_value_groups)
241
+ value_states = repeat_kv(value, module.num_key_value_groups)
242
+
243
+ attn_weights = torch.matmul(query, key_states.transpose(2, 3)) * scaling
244
+ if attention_mask is not None:
245
+ causal_mask = attention_mask[:, :, :, : key_states.shape[-2]]
246
+ attn_weights = attn_weights + causal_mask
247
+
248
+ attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
249
+ attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
250
+ attn_output = torch.matmul(attn_weights, value_states)
251
+ attn_output = attn_output.transpose(1, 2).contiguous()
252
+
253
+ return attn_output, attn_weights
254
+
255
+
256
+ class GemmaAttention(nn.Module):
257
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
258
+
259
+ def __init__(self, config: GemmaConfig, layer_idx: int):
260
+ super().__init__()
261
+ self.config = config
262
+ self.layer_idx = layer_idx
263
+ self.head_dim = getattr(config, "head_dim", config.hidden_size // config.num_attention_heads)
264
+ self.num_key_value_groups = config.num_attention_heads // config.num_key_value_heads
265
+ self.scaling = self.head_dim**-0.5
266
+ self.attention_dropout = config.attention_dropout
267
+ self.is_causal = True
268
+
269
+ self.q_proj = nn.Linear(
270
+ config.hidden_size, config.num_attention_heads * self.head_dim, bias=config.attention_bias
271
+ )
272
+ self.k_proj = nn.Linear(
273
+ config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
274
+ )
275
+ self.v_proj = nn.Linear(
276
+ config.hidden_size, config.num_key_value_heads * self.head_dim, bias=config.attention_bias
277
+ )
278
+ self.o_proj = nn.Linear(
279
+ config.num_attention_heads * self.head_dim, config.hidden_size, bias=config.attention_bias
280
+ )
281
+
282
+ def forward(
283
+ self,
284
+ hidden_states: torch.Tensor,
285
+ position_embeddings: tuple[torch.Tensor, torch.Tensor],
286
+ attention_mask: Optional[torch.Tensor],
287
+ past_key_value: Optional[Cache] = None,
288
+ cache_position: Optional[torch.LongTensor] = None,
289
+ use_cache: bool = False,
290
+ **kwargs: Unpack[FlashAttentionKwargs],
291
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor], Optional[tuple[torch.Tensor]]]:
292
+ input_shape = hidden_states.shape[:-1]
293
+ hidden_shape = (*input_shape, -1, self.head_dim)
294
+
295
+ query_states = self.q_proj(hidden_states).view(hidden_shape).transpose(1, 2)
296
+ key_states = self.k_proj(hidden_states).view(hidden_shape).transpose(1, 2)
297
+ value_states = self.v_proj(hidden_states).view(hidden_shape).transpose(1, 2)
298
+
299
+ cos, sin = position_embeddings
300
+ query_states, key_states = apply_rotary_pos_emb(query_states, key_states, cos, sin)
301
+
302
+ # Use cache if provided
303
+ if past_key_value is not None:
304
+ if use_cache:
305
+ # sin and cos are specific to RoPE models; cache_position needed for the static cache
306
+ cache_kwargs = {"sin": sin, "cos": cos, "cache_position": cache_position}
307
+ key_states, value_states = past_key_value.update(key_states, value_states, self.layer_idx, cache_kwargs)
308
+ else:
309
+ key_states = torch.cat([past_key_value[self.layer_idx][0], key_states], dim=2)
310
+ value_states = torch.cat([past_key_value[self.layer_idx][1], value_states], dim=2)
311
+
312
+ attention_interface: Callable = eager_attention_forward
313
+ if self.config._attn_implementation != "eager":
314
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
315
+
316
+ attn_output, attn_weights = attention_interface(
317
+ self,
318
+ query_states,
319
+ key_states,
320
+ value_states,
321
+ attention_mask,
322
+ dropout=0.0 if not self.training else self.attention_dropout,
323
+ scaling=self.scaling,
324
+ **kwargs,
325
+ )
326
+
327
+ attn_output = attn_output.reshape(*input_shape, -1).contiguous()
328
+ attn_output = self.o_proj(attn_output)
329
+ return attn_output, attn_weights
330
+
331
+
332
+ class GemmaDecoderLayer(GradientCheckpointingLayer):
333
+ def __init__(self, config: GemmaConfig, layer_idx: int):
334
+ super().__init__()
335
+ self.hidden_size = config.hidden_size
336
+
337
+ self.self_attn = GemmaAttention(config=config, layer_idx=layer_idx)
338
+
339
+ self.mlp = GemmaMLP(config)
340
+ cond_dim = getattr(config, 'adarms_cond_dim', None) if getattr(config, 'use_adarms', False) else None
341
+ self.input_layernorm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps, cond_dim=cond_dim)
342
+ self.post_attention_layernorm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps, cond_dim=cond_dim)
343
+
344
+ def forward(
345
+ self,
346
+ hidden_states: torch.Tensor,
347
+ attention_mask: Optional[torch.Tensor] = None,
348
+ position_ids: Optional[torch.LongTensor] = None,
349
+ past_key_value: Optional[Cache] = None,
350
+ output_attentions: Optional[bool] = False,
351
+ use_cache: Optional[bool] = False,
352
+ cache_position: Optional[torch.LongTensor] = None,
353
+ position_embeddings: Optional[tuple[torch.Tensor, torch.Tensor]] = None, # necessary, but kept here for BC
354
+ adarms_cond: Optional[torch.Tensor] = None,
355
+ **kwargs: Unpack[FlashAttentionKwargs],
356
+ ) -> tuple[torch.FloatTensor, Optional[tuple[torch.FloatTensor, torch.FloatTensor]]]:
357
+ residual = hidden_states
358
+ hidden_states, gate = self.input_layernorm(hidden_states, adarms_cond)
359
+
360
+ # Self Attention
361
+ hidden_states, self_attn_weights = self.self_attn(
362
+ hidden_states=hidden_states,
363
+ attention_mask=attention_mask,
364
+ position_ids=position_ids,
365
+ past_key_value=past_key_value,
366
+ output_attentions=output_attentions,
367
+ use_cache=use_cache,
368
+ cache_position=cache_position,
369
+ position_embeddings=position_embeddings,
370
+ **kwargs,
371
+ )
372
+ hidden_states = _gated_residual(residual, hidden_states, gate)
373
+
374
+ # Fully Connected
375
+ residual = hidden_states
376
+ hidden_states, gate = self.post_attention_layernorm(hidden_states, adarms_cond)
377
+ hidden_states = self.mlp(hidden_states)
378
+ hidden_states = _gated_residual(residual, hidden_states, gate)
379
+
380
+ outputs = (hidden_states,)
381
+ if output_attentions:
382
+ outputs += (self_attn_weights,)
383
+
384
+ return outputs
385
+
386
+
387
+ @auto_docstring
388
+ class GemmaPreTrainedModel(PreTrainedModel):
389
+ config_class = GemmaConfig
390
+ base_model_prefix = "model"
391
+ supports_gradient_checkpointing = True
392
+ _no_split_modules = ["GemmaDecoderLayer"]
393
+ _skip_keys_device_placement = ["past_key_values"]
394
+ _supports_flash_attn_3 = True
395
+ _supports_flash_attn_2 = True
396
+ _supports_sdpa = True
397
+ _supports_flex_attn = True
398
+ _supports_cache_class = True
399
+ _supports_quantized_cache = True
400
+ _supports_static_cache = True
401
+ _supports_attention_backend = True
402
+
403
+ def _init_weights(self, module):
404
+ std = self.config.initializer_range
405
+ if isinstance(module, nn.Linear):
406
+ module.weight.data.normal_(mean=0.0, std=std)
407
+ if module.bias is not None:
408
+ module.bias.data.zero_()
409
+ elif isinstance(module, nn.Embedding):
410
+ module.weight.data.normal_(mean=0.0, std=std)
411
+ if module.padding_idx is not None:
412
+ module.weight.data[module.padding_idx].zero_()
413
+ elif isinstance(module, GemmaRMSNorm):
414
+ if hasattr(module, 'weight'):
415
+ module.weight.data.fill_(1.0)
416
+
417
+
418
+ @auto_docstring
419
+ class GemmaModel(GemmaPreTrainedModel):
420
+ def __init__(self, config: GemmaConfig):
421
+ super().__init__(config)
422
+ self.padding_idx = config.pad_token_id
423
+ self.vocab_size = config.vocab_size
424
+
425
+ self.embed_tokens = nn.Embedding(config.vocab_size, config.hidden_size, self.padding_idx)
426
+ self.layers = nn.ModuleList(
427
+ [GemmaDecoderLayer(config, layer_idx) for layer_idx in range(config.num_hidden_layers)]
428
+ )
429
+
430
+ cond_dim = getattr(config, 'adarms_cond_dim', None) if getattr(config, 'use_adarms', False) else None
431
+ self.norm = GemmaRMSNorm(config.hidden_size, eps=config.rms_norm_eps, cond_dim=cond_dim)
432
+ self.rotary_emb = GemmaRotaryEmbedding(config=config)
433
+ self.gradient_checkpointing = False
434
+
435
+ # Initialize weights and apply final processing
436
+ self.post_init()
437
+
438
+ def get_input_embeddings(self):
439
+ return self.embed_tokens
440
+
441
+ def set_input_embeddings(self, value):
442
+ self.embed_tokens = value
443
+
444
+ @can_return_tuple
445
+ @auto_docstring
446
+ def forward(
447
+ self,
448
+ input_ids: Optional[torch.LongTensor] = None,
449
+ attention_mask: Optional[torch.Tensor] = None,
450
+ position_ids: Optional[torch.LongTensor] = None,
451
+ past_key_values: Optional[Cache] = None,
452
+ inputs_embeds: Optional[torch.FloatTensor] = None,
453
+ use_cache: Optional[bool] = None,
454
+ output_attentions: Optional[bool] = None,
455
+ output_hidden_states: Optional[bool] = None,
456
+ cache_position: Optional[torch.LongTensor] = None,
457
+ adarms_cond: Optional[torch.Tensor] = None,
458
+ **kwargs: Unpack[FlashAttentionKwargs],
459
+ ) -> BaseModelOutputWithPast:
460
+ """
461
+ adarms_cond (`torch.Tensor` of shape `(batch_size, cond_dim)`, *optional*):
462
+ Condition for ADARMS.
463
+ """
464
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
465
+ output_hidden_states = (
466
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
467
+ )
468
+ use_cache = use_cache if use_cache is not None else self.config.use_cache
469
+
470
+ if (input_ids is None) ^ (inputs_embeds is not None):
471
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
472
+
473
+ if self.gradient_checkpointing and self.training and use_cache:
474
+ logger.warning_once(
475
+ "`use_cache=True` is incompatible with gradient checkpointing. Setting `use_cache=False`."
476
+ )
477
+ use_cache = False
478
+
479
+ if inputs_embeds is None:
480
+ inputs_embeds = self.embed_tokens(input_ids)
481
+
482
+ if use_cache and past_key_values is None:
483
+ past_key_values = DynamicCache()
484
+
485
+ if cache_position is None:
486
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
487
+ cache_position = torch.arange(
488
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
489
+ )
490
+
491
+ if position_ids is None:
492
+ position_ids = cache_position.unsqueeze(0)
493
+
494
+ causal_mask = create_causal_mask(
495
+ config=self.config,
496
+ input_embeds=inputs_embeds,
497
+ attention_mask=attention_mask,
498
+ cache_position=cache_position,
499
+ past_key_values=past_key_values,
500
+ position_ids=position_ids,
501
+ )
502
+
503
+ # embed positions
504
+ hidden_states = inputs_embeds
505
+ # Convert to bfloat16 if the first layer uses bfloat16
506
+ if len(self.layers) > 0 and self.layers[0].self_attn.q_proj.weight.dtype == torch.bfloat16:
507
+ hidden_states = hidden_states.to(torch.bfloat16)
508
+
509
+ # create position embeddings to be shared across the decoder layers
510
+ position_embeddings = self.rotary_emb(hidden_states, position_ids)
511
+
512
+ # normalized
513
+ # Gemma downcasts the below to float16, causing sqrt(3072)=55.4256 to become 55.5
514
+ # See https://github.com/huggingface/transformers/pull/29402
515
+ normalizer = torch.tensor(self.config.hidden_size**0.5, dtype=hidden_states.dtype)
516
+ #hidden_states = hidden_states * normalizer
517
+
518
+ # decoder layers
519
+ all_hidden_states = () if output_hidden_states else None
520
+ all_self_attns = () if output_attentions else None
521
+
522
+ for decoder_layer in self.layers[: self.config.num_hidden_layers]:
523
+ if output_hidden_states:
524
+ all_hidden_states += (hidden_states,)
525
+
526
+ layer_outputs = decoder_layer(
527
+ hidden_states,
528
+ attention_mask=causal_mask,
529
+ position_ids=position_ids,
530
+ past_key_value=past_key_values,
531
+ output_attentions=output_attentions,
532
+ use_cache=use_cache,
533
+ cache_position=cache_position,
534
+ position_embeddings=position_embeddings,
535
+ adarms_cond=adarms_cond,
536
+ **kwargs,
537
+ )
538
+
539
+ hidden_states = layer_outputs[0]
540
+
541
+ if output_attentions:
542
+ all_self_attns += (layer_outputs[1],)
543
+
544
+ hidden_states, _ = self.norm(hidden_states, adarms_cond)
545
+
546
+ # add hidden states from the last decoder layer
547
+ if output_hidden_states:
548
+ all_hidden_states += (hidden_states,)
549
+
550
+ return BaseModelOutputWithPast(
551
+ last_hidden_state=hidden_states,
552
+ past_key_values=past_key_values if use_cache else None,
553
+ hidden_states=all_hidden_states,
554
+ attentions=all_self_attns,
555
+ )
556
+
557
+
558
+ class KwargsForCausalLM(FlashAttentionKwargs, LossKwargs): ...
559
+
560
+
561
+ @auto_docstring
562
+ class GemmaForCausalLM(GemmaPreTrainedModel, GenerationMixin):
563
+ _tied_weights_keys = ["lm_head.weight"]
564
+ _tp_plan = {"lm_head": "colwise_rep"}
565
+ _pp_plan = {"lm_head": (["hidden_states"], ["logits"])}
566
+
567
+ def __init__(self, config):
568
+ super().__init__(config)
569
+ self.model = GemmaModel(config)
570
+ self.vocab_size = config.vocab_size
571
+ self.lm_head = nn.Linear(config.hidden_size, config.vocab_size, bias=False)
572
+
573
+ # Initialize weights and apply final processing
574
+ self.post_init()
575
+
576
+ def get_input_embeddings(self):
577
+ return self.model.embed_tokens
578
+
579
+ def set_input_embeddings(self, value):
580
+ self.model.embed_tokens = value
581
+
582
+ def get_output_embeddings(self):
583
+ return self.lm_head
584
+
585
+ def set_output_embeddings(self, new_embeddings):
586
+ self.lm_head = new_embeddings
587
+
588
+ def set_decoder(self, decoder):
589
+ self.model = decoder
590
+
591
+ def get_decoder(self):
592
+ return self.model
593
+
594
+ @can_return_tuple
595
+ @auto_docstring
596
+ def forward(
597
+ self,
598
+ input_ids: Optional[torch.LongTensor] = None,
599
+ attention_mask: Optional[torch.Tensor] = None,
600
+ position_ids: Optional[torch.LongTensor] = None,
601
+ past_key_values: Optional[Cache] = None,
602
+ inputs_embeds: Optional[torch.FloatTensor] = None,
603
+ labels: Optional[torch.LongTensor] = None,
604
+ use_cache: Optional[bool] = None,
605
+ output_attentions: Optional[bool] = None,
606
+ output_hidden_states: Optional[bool] = None,
607
+ cache_position: Optional[torch.LongTensor] = None,
608
+ logits_to_keep: Union[int, torch.Tensor] = 0,
609
+ adarms_cond: Optional[torch.Tensor] = None,
610
+ **kwargs: Unpack[KwargsForCausalLM],
611
+ ) -> CausalLMOutputWithPast:
612
+ r"""
613
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
614
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
615
+ config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
616
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.vocab_size]`.
617
+
618
+ adarms_cond (`torch.Tensor` of shape `(batch_size, cond_dim)`, *optional*):
619
+ Condition for ADARMS.
620
+
621
+ Example:
622
+
623
+ ```python
624
+ >>> from transformers import AutoTokenizer, GemmaForCausalLM
625
+
626
+ >>> model = GemmaForCausalLM.from_pretrained("google/gemma-7b")
627
+ >>> tokenizer = AutoTokenizer.from_pretrained("google/gemma-7b")
628
+
629
+ >>> prompt = "What is your favorite condiment?"
630
+ >>> inputs = tokenizer(prompt, return_tensors="pt")
631
+
632
+ >>> # Generate
633
+ >>> generate_ids = model.generate(inputs.input_ids, max_length=30)
634
+ >>> tokenizer.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
635
+ "What is your favorite condiment?"
636
+ ```"""
637
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
638
+ output_hidden_states = (
639
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
640
+ )
641
+
642
+ # decoder outputs consists of (dec_features, layer_state, dec_hidden, dec_attn)
643
+ outputs: BaseModelOutputWithPast = self.model(
644
+ input_ids=input_ids,
645
+ attention_mask=attention_mask,
646
+ position_ids=position_ids,
647
+ past_key_values=past_key_values,
648
+ inputs_embeds=inputs_embeds,
649
+ use_cache=use_cache,
650
+ output_attentions=output_attentions,
651
+ output_hidden_states=output_hidden_states,
652
+ cache_position=cache_position,
653
+ adarms_cond=adarms_cond,
654
+ **kwargs,
655
+ )
656
+
657
+ hidden_states = outputs.last_hidden_state
658
+ # Only compute necessary logits, and do not upcast them to float if we are not computing the loss
659
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
660
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
661
+
662
+ loss = None
663
+ if labels is not None:
664
+ loss = self.loss_function(logits=logits, labels=labels, vocab_size=self.config.vocab_size, **kwargs)
665
+
666
+ return CausalLMOutputWithPast(
667
+ loss=loss,
668
+ logits=logits,
669
+ past_key_values=outputs.past_key_values,
670
+ hidden_states=outputs.hidden_states,
671
+ attentions=outputs.attentions,
672
+ )
673
+
674
+
675
+ @auto_docstring(
676
+ custom_intro="""
677
+ The Gemma Model transformer with a sequence classification head on top (linear layer).
678
+
679
+ [`GemmaForSequenceClassification`] uses the last token in order to do the classification, as other causal models
680
+ (e.g. GPT-2) do.
681
+
682
+ Since it does classification on the last token, it requires to know the position of the last token. If a
683
+ `pad_token_id` is defined in the configuration, it finds the last token that is not a padding token in each row. If
684
+ no `pad_token_id` is defined, it simply takes the last value in each row of the batch. Since it cannot guess the
685
+ padding tokens when `inputs_embeds` are passed instead of `input_ids`, it does the same (take the last value in
686
+ each row of the batch).
687
+ """
688
+ )
689
+ class GemmaForSequenceClassification(GemmaPreTrainedModel):
690
+ def __init__(self, config):
691
+ super().__init__(config)
692
+ self.num_labels = config.num_labels
693
+ self.model = GemmaModel(config)
694
+ self.score = nn.Linear(config.hidden_size, self.num_labels, bias=False)
695
+
696
+ # Initialize weights and apply final processing
697
+ self.post_init()
698
+
699
+ def get_input_embeddings(self):
700
+ return self.model.embed_tokens
701
+
702
+ def set_input_embeddings(self, value):
703
+ self.model.embed_tokens = value
704
+
705
+ @can_return_tuple
706
+ @auto_docstring
707
+ def forward(
708
+ self,
709
+ input_ids: Optional[torch.LongTensor] = None,
710
+ attention_mask: Optional[torch.Tensor] = None,
711
+ position_ids: Optional[torch.LongTensor] = None,
712
+ past_key_values: Optional[Cache] = None,
713
+ inputs_embeds: Optional[torch.FloatTensor] = None,
714
+ labels: Optional[torch.LongTensor] = None,
715
+ use_cache: Optional[bool] = None,
716
+ output_attentions: Optional[bool] = None,
717
+ output_hidden_states: Optional[bool] = None,
718
+ adarms_cond: Optional[torch.Tensor] = None,
719
+ ) -> SequenceClassifierOutputWithPast:
720
+ r"""
721
+ labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*):
722
+ Labels for computing the sequence classification/regression loss. Indices should be in `[0, ...,
723
+ config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If
724
+ `config.num_labels > 1` a classification loss is computed (Cross-Entropy).
725
+
726
+ adarms_cond (`torch.Tensor` of shape `(batch_size, cond_dim)`, *optional*):
727
+ Condition for ADARMS.
728
+ """
729
+
730
+ transformer_outputs: BaseModelOutputWithPast = self.model(
731
+ input_ids,
732
+ attention_mask=attention_mask,
733
+ position_ids=position_ids,
734
+ past_key_values=past_key_values,
735
+ inputs_embeds=inputs_embeds,
736
+ use_cache=use_cache,
737
+ output_attentions=output_attentions,
738
+ output_hidden_states=output_hidden_states,
739
+ adarms_cond=adarms_cond,
740
+ )
741
+ hidden_states = transformer_outputs.last_hidden_state
742
+ logits = self.score(hidden_states)
743
+
744
+ if input_ids is not None:
745
+ batch_size = input_ids.shape[0]
746
+ else:
747
+ batch_size = inputs_embeds.shape[0]
748
+
749
+ if self.config.pad_token_id is None and batch_size != 1:
750
+ raise ValueError("Cannot handle batch sizes > 1 if no padding token is defined.")
751
+ if self.config.pad_token_id is None:
752
+ last_non_pad_token = -1
753
+ elif input_ids is not None:
754
+ # To handle both left- and right- padding, we take the rightmost token that is not equal to pad_token_id
755
+ non_pad_mask = (input_ids != self.config.pad_token_id).to(logits.device, torch.int32)
756
+ token_indices = torch.arange(input_ids.shape[-1], device=logits.device, dtype=torch.int32)
757
+ last_non_pad_token = (token_indices * non_pad_mask).argmax(-1)
758
+ else:
759
+ last_non_pad_token = -1
760
+ logger.warning_once(
761
+ f"{self.__class__.__name__} will not detect padding tokens in `inputs_embeds`. Results may be "
762
+ "unexpected if using padding tokens in conjunction with `inputs_embeds.`"
763
+ )
764
+
765
+ pooled_logits = logits[torch.arange(batch_size, device=logits.device), last_non_pad_token]
766
+
767
+ loss = None
768
+ if labels is not None:
769
+ loss = self.loss_function(logits=logits, labels=labels, pooled_logits=pooled_logits, config=self.config)
770
+
771
+ return SequenceClassifierOutputWithPast(
772
+ loss=loss,
773
+ logits=pooled_logits,
774
+ past_key_values=transformer_outputs.past_key_values,
775
+ hidden_states=transformer_outputs.hidden_states,
776
+ attentions=transformer_outputs.attentions,
777
+ )
778
+
779
+
780
+ @auto_docstring
781
+ class GemmaForTokenClassification(GemmaPreTrainedModel):
782
+ def __init__(self, config):
783
+ super().__init__(config)
784
+ self.num_labels = config.num_labels
785
+ self.model = GemmaModel(config)
786
+ if getattr(config, "classifier_dropout", None) is not None:
787
+ classifier_dropout = config.classifier_dropout
788
+ elif getattr(config, "hidden_dropout", None) is not None:
789
+ classifier_dropout = config.hidden_dropout
790
+ else:
791
+ classifier_dropout = 0.1
792
+ self.dropout = nn.Dropout(classifier_dropout)
793
+ self.score = nn.Linear(config.hidden_size, config.num_labels)
794
+
795
+ # Initialize weights and apply final processing
796
+ self.post_init()
797
+
798
+ def get_input_embeddings(self):
799
+ return self.model.embed_tokens
800
+
801
+ def set_input_embeddings(self, value):
802
+ self.model.embed_tokens = value
803
+
804
+ @can_return_tuple
805
+ @auto_docstring
806
+ def forward(
807
+ self,
808
+ input_ids: Optional[torch.LongTensor] = None,
809
+ attention_mask: Optional[torch.Tensor] = None,
810
+ position_ids: Optional[torch.LongTensor] = None,
811
+ past_key_values: Optional[Cache] = None,
812
+ inputs_embeds: Optional[torch.FloatTensor] = None,
813
+ labels: Optional[torch.LongTensor] = None,
814
+ use_cache: Optional[bool] = None,
815
+ output_attentions: Optional[bool] = None,
816
+ output_hidden_states: Optional[bool] = None,
817
+ adarms_cond: Optional[torch.Tensor] = None,
818
+ ) -> TokenClassifierOutput:
819
+ r"""
820
+ labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*):
821
+ Labels for computing the sequence classification/regression loss. Indices should be in `[0, ...,
822
+ config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If
823
+ `config.num_labels > 1` a classification loss is computed (Cross-Entropy).
824
+
825
+ adarms_cond (`torch.Tensor` of shape `(batch_size, cond_dim)`, *optional*):
826
+ Condition for ADARMS.
827
+ """
828
+
829
+ outputs: BaseModelOutputWithPast = self.model(
830
+ input_ids,
831
+ attention_mask=attention_mask,
832
+ position_ids=position_ids,
833
+ past_key_values=past_key_values,
834
+ inputs_embeds=inputs_embeds,
835
+ use_cache=use_cache,
836
+ output_attentions=output_attentions,
837
+ output_hidden_states=output_hidden_states,
838
+ adarms_cond=adarms_cond,
839
+ )
840
+ sequence_output = outputs.last_hidden_state
841
+ sequence_output = self.dropout(sequence_output)
842
+ logits = self.score(sequence_output)
843
+
844
+ loss = None
845
+ if labels is not None:
846
+ loss = self.loss_function(logits, labels, self.config)
847
+
848
+ return TokenClassifierOutput(
849
+ loss=loss,
850
+ logits=logits,
851
+ hidden_states=outputs.hidden_states,
852
+ attentions=outputs.attentions,
853
+ )
854
+
855
+
856
+ __all__ = [
857
+ "GemmaModel",
858
+ "GemmaForCausalLM",
859
+ "GemmaForSequenceClassification",
860
+ "GemmaForTokenClassification",
861
+ "GemmaPreTrainedModel",
862
+ ]
openpi_runtime/openpi/models_pytorch/transformers_replace/models/paligemma/modeling_paligemma.py ADDED
@@ -0,0 +1,622 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2024 the HuggingFace Inc. team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """PyTorch PaliGemmamodel."""
16
+
17
+ from dataclasses import dataclass
18
+ from typing import Optional, Union
19
+
20
+ import torch
21
+ import torch.utils.checkpoint
22
+ from torch import nn
23
+
24
+ from ...cache_utils import Cache, HybridCache, StaticCache
25
+ from ...generation import GenerationMixin
26
+ from ...modeling_flash_attention_utils import FlashAttentionKwargs
27
+ from ...modeling_outputs import BaseModelOutputWithPast
28
+ from ...modeling_utils import PreTrainedModel
29
+ from ...processing_utils import Unpack
30
+ from ...utils import LossKwargs, ModelOutput, auto_docstring, can_return_tuple, is_torchdynamo_compiling, logging
31
+ from ..auto import AutoModel
32
+ from .configuration_paligemma import PaliGemmaConfig
33
+
34
+
35
+ logger = logging.get_logger(__name__)
36
+
37
+
38
+ @dataclass
39
+ @auto_docstring(
40
+ custom_intro="""
41
+ Base class for Paligemma outputs, with hidden states and attentions.
42
+ """
43
+ )
44
+ class PaligemmaModelOutputWithPast(BaseModelOutputWithPast):
45
+ r"""
46
+ past_key_values (`tuple(tuple(torch.FloatTensor))`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`):
47
+ Tuple of `tuple(torch.FloatTensor)` of length `config.n_layers`, with each tuple having 2 tensors of shape
48
+ `(batch_size, num_heads, sequence_length, embed_size_per_head)`)
49
+
50
+ Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see
51
+ `past_key_values` input) to speed up sequential decoding.
52
+ image_hidden_states (`torch.FloatTensor`, *optional*):
53
+ A `torch.FloatTensor` of size `(batch_size, num_images, sequence_length, hidden_size)`.
54
+ image_hidden_states of the model produced by the vision encoder and after projecting the last hidden state.
55
+ """
56
+
57
+ image_hidden_states: Optional[torch.FloatTensor] = None
58
+
59
+
60
+ @dataclass
61
+ @auto_docstring(
62
+ custom_intro="""
63
+ Base class for PaliGemma causal language model (or autoregressive) outputs.
64
+ """
65
+ )
66
+ class PaliGemmaCausalLMOutputWithPast(ModelOutput):
67
+ r"""
68
+ loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `labels` is provided):
69
+ Language modeling loss (for next-token prediction).
70
+ logits (`torch.FloatTensor` of shape `(batch_size, sequence_length, config.text_config.vocab_size)`):
71
+ Prediction scores of the language modeling head (scores for each vocabulary token before SoftMax).
72
+ past_key_values (`tuple(tuple(torch.FloatTensor))`, *optional*, returned when `use_cache=True` is passed or when `config.use_cache=True`):
73
+ Tuple of `tuple(torch.FloatTensor)` of length `config.n_layers`, with each tuple having 2 tensors of shape
74
+ `(batch_size, num_heads, sequence_length, embed_size_per_head)`)
75
+
76
+ Contains pre-computed hidden-states (key and values in the self-attention blocks) that can be used (see
77
+ `past_key_values` input) to speed up sequential decoding.
78
+ image_hidden_states (`torch.FloatTensor`, *optional*):
79
+ A `torch.FloatTensor` of size `(batch_size, num_images, sequence_length, hidden_size)`.
80
+ image_hidden_states of the model produced by the vision encoder after projecting last hidden state.
81
+ """
82
+
83
+ loss: Optional[torch.FloatTensor] = None
84
+ logits: Optional[torch.FloatTensor] = None
85
+ past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None
86
+ hidden_states: Optional[tuple[torch.FloatTensor]] = None
87
+ attentions: Optional[tuple[torch.FloatTensor]] = None
88
+ image_hidden_states: Optional[torch.FloatTensor] = None
89
+
90
+
91
+ class PaliGemmaMultiModalProjector(nn.Module):
92
+ def __init__(self, config: PaliGemmaConfig):
93
+ super().__init__()
94
+ self.linear = nn.Linear(config.vision_config.hidden_size, config.vision_config.projection_dim, bias=True)
95
+
96
+ def forward(self, image_features):
97
+ hidden_states = self.linear(image_features)
98
+
99
+ return hidden_states
100
+
101
+
102
+ @auto_docstring
103
+ class PaliGemmaPreTrainedModel(PreTrainedModel):
104
+ config_class = PaliGemmaConfig
105
+ base_model_prefix = ""
106
+ supports_gradient_checkpointing = True
107
+ _no_split_modules = ["PaliGemmaMultiModalProjector"]
108
+ _skip_keys_device_placement = "past_key_values"
109
+ _supports_cache_class = True
110
+ _supports_quantized_cache = True
111
+ _supports_static_cache = True
112
+ _supports_flash_attn_2 = True
113
+ _supports_sdpa = True
114
+ _supports_flex_attn = True
115
+ _supports_attention_backend = True
116
+
117
+ def _init_weights(self, module):
118
+ # important: this ported version of PaliGemmaisn't meant for training from scratch - only
119
+ # inference and fine-tuning
120
+ std = getattr(self.config, "initializer_range", self.config.get_text_config().initializer_range)
121
+
122
+ if isinstance(module, nn.Linear):
123
+ module.weight.data.normal_(mean=0.0, std=std)
124
+ if module.bias is not None:
125
+ module.bias.data.zero_()
126
+
127
+
128
+ @auto_docstring(
129
+ custom_intro="""
130
+ The Base Paligemma model which consists of a vision backbone and a language model withou language modeling head.,
131
+ """
132
+ )
133
+ class PaliGemmaModel(PaliGemmaPreTrainedModel):
134
+ _checkpoint_conversion_mapping = {"language_model.model": "language_model"}
135
+ # we are filtering the logits/labels so we shouldn't divide the loss based on num_items_in_batch
136
+ accepts_loss_kwargs = False
137
+
138
+ def __init__(self, config: PaliGemmaConfig):
139
+ super().__init__(config)
140
+ self.vision_tower = AutoModel.from_config(config=config.vision_config)
141
+ self.multi_modal_projector = PaliGemmaMultiModalProjector(config)
142
+ self.vocab_size = config.text_config.vocab_size
143
+
144
+ language_model = AutoModel.from_config(config=config.text_config)
145
+ self.language_model = language_model
146
+
147
+ self.pad_token_id = self.config.pad_token_id if self.config.pad_token_id is not None else -1
148
+ self.post_init()
149
+
150
+ # Copied from transformers.models.llava.modeling_llava.LlavaModel.get_input_embeddings with Llava->PaliGemma
151
+ def get_input_embeddings(self):
152
+ return self.language_model.get_input_embeddings()
153
+
154
+ # Copied from transformers.models.llava.modeling_llava.LlavaModel.set_input_embeddings with Llava->PaliGemma
155
+ def set_input_embeddings(self, value):
156
+ self.language_model.set_input_embeddings(value)
157
+
158
+ def set_decoder(self, decoder):
159
+ self.language_model = decoder
160
+
161
+ def get_decoder(self):
162
+ return self.language_model
163
+
164
+ def _update_causal_mask(
165
+ self,
166
+ attention_mask,
167
+ token_type_ids=None,
168
+ past_key_values=None,
169
+ cache_position=None,
170
+ input_tensor=None,
171
+ is_training: Optional[bool] = None,
172
+ ):
173
+ if self.config.text_config._attn_implementation == "flash_attention_2":
174
+ if attention_mask is not None and 0.0 in attention_mask:
175
+ return attention_mask
176
+ return None
177
+ is_training = is_training if is_training is not None else self.training
178
+ using_static_cache = isinstance(past_key_values, StaticCache)
179
+ min_dtype = torch.finfo(self.dtype).min
180
+ if input_tensor is None:
181
+ input_tensor = attention_mask
182
+
183
+ inputs_lead_dim, sequence_length = input_tensor.shape[:2]
184
+ if using_static_cache:
185
+ target_length = past_key_values.get_max_cache_shape()
186
+ elif isinstance(past_key_values, HybridCache):
187
+ target_length = past_key_values.get_max_cache_shape()
188
+ else:
189
+ target_length = (
190
+ attention_mask.shape[-1]
191
+ if isinstance(attention_mask, torch.Tensor)
192
+ else cache_position[0] + sequence_length + 1
193
+ )
194
+
195
+ if attention_mask is not None and attention_mask.dim() == 4:
196
+ # In this case we assume that the mask comes already in inverted form and requires no inversion or slicing.
197
+ return attention_mask
198
+
199
+ causal_mask = torch.full(
200
+ (sequence_length, target_length), fill_value=min_dtype, dtype=self.dtype, device=cache_position.device
201
+ )
202
+ # Causal diagonal mask only if training, otherwise attend to the whole prefix. Training-specific attn for prefix is handled below
203
+ if sequence_length != 1:
204
+ if is_training:
205
+ causal_mask = torch.triu(causal_mask, diagonal=1)
206
+ else:
207
+ causal_mask[:, :sequence_length] = 0.0
208
+
209
+ causal_mask *= torch.arange(target_length, device=cache_position.device) > cache_position.reshape(-1, 1)
210
+ causal_mask = causal_mask[None, None, :, :].expand(inputs_lead_dim, 1, -1, -1)
211
+ if attention_mask is not None:
212
+ causal_mask = causal_mask.clone() # copy to contiguous memory for in-place edit
213
+ mask_length = attention_mask.shape[-1]
214
+
215
+ # First unmask prefix tokens during training
216
+ if is_training:
217
+ if token_type_ids is None:
218
+ raise ValueError("Token type ids must be provided during training")
219
+ causal_mask[:, :, :, :mask_length] = causal_mask[:, :, :, :mask_length].masked_fill(
220
+ token_type_ids[:, None, None, :].to(causal_mask.device) == 0, 0
221
+ )
222
+
223
+ # Then apply padding mask (will mask pad tokens)
224
+ padding_mask = causal_mask[:, :, :, :mask_length] + attention_mask[:, None, None, :].to(causal_mask.device)
225
+ padding_mask = padding_mask == 0
226
+ causal_mask[:, :, :, :mask_length] = causal_mask[:, :, :, :mask_length].masked_fill(
227
+ padding_mask, min_dtype
228
+ )
229
+
230
+ return causal_mask
231
+
232
+ def get_image_features(self, pixel_values: torch.FloatTensor):
233
+ """
234
+ Obtains image last hidden states from the vision tower and apply multimodal projection.
235
+
236
+ Args:
237
+ pixel_values (`torch.FloatTensor]` of shape `(batch_size, channels, height, width)`)
238
+ The tensors corresponding to the input images.
239
+ Returns:
240
+ image_features (`torch.Tensor`): Image feature tensor of shape `(num_images, image_length, embed_dim)`).
241
+ """
242
+ image_outputs = self.vision_tower(pixel_values)
243
+ selected_image_feature = image_outputs.last_hidden_state
244
+ image_features = self.multi_modal_projector(selected_image_feature)
245
+ return image_features
246
+
247
+ @can_return_tuple
248
+ @auto_docstring
249
+ def forward(
250
+ self,
251
+ input_ids: torch.LongTensor = None,
252
+ pixel_values: torch.FloatTensor = None,
253
+ attention_mask: Optional[torch.Tensor] = None,
254
+ position_ids: Optional[torch.LongTensor] = None,
255
+ past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None,
256
+ token_type_ids: Optional[torch.LongTensor] = None,
257
+ cache_position: Optional[torch.LongTensor] = None,
258
+ inputs_embeds: Optional[torch.FloatTensor] = None,
259
+ labels: Optional[torch.LongTensor] = None,
260
+ use_cache: Optional[bool] = None,
261
+ output_attentions: Optional[bool] = None,
262
+ output_hidden_states: Optional[bool] = None,
263
+ return_dict: Optional[bool] = None,
264
+ **kwargs: Unpack[FlashAttentionKwargs],
265
+ ) -> Union[tuple, PaligemmaModelOutputWithPast]:
266
+ r"""
267
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
268
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
269
+ config.text_config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
270
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.text_config.vocab_size]`.
271
+
272
+ Example:
273
+
274
+ ```python
275
+ >>> from PIL import Image
276
+ >>> import requests
277
+ >>> from transformers import AutoProcessor, PaliGemmaForConditionalGeneration
278
+
279
+ >>> model = PaliGemmaForConditionalGeneration.from_pretrained("google/paligemma2-3b-mix-224")
280
+ >>> processor = AutoProcessor.from_pretrained("google/paligemma2-3b-mix-224")
281
+
282
+ >>> prompt = "Where is the cat standing?"
283
+ >>> url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
284
+ >>> image = Image.open(requests.get(url, stream=True).raw)
285
+
286
+ >>> inputs = processor(images=image, text=prompt, return_tensors="pt")
287
+
288
+ >>> # Generate
289
+ >>> generate_ids = model.generate(**inputs,)
290
+ >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
291
+ "Where is the cat standing?\nsnow"
292
+ ```"""
293
+
294
+ if (input_ids is None) ^ (inputs_embeds is not None):
295
+ raise ValueError("You must specify exactly one of input_ids or inputs_embeds")
296
+
297
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
298
+ output_hidden_states = (
299
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
300
+ )
301
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
302
+
303
+ is_training = token_type_ids is not None and labels is not None
304
+
305
+ # Replace image id woth PAD if the image token if OOV, to avoid index-errors
306
+ if input_ids is not None and self.config.image_token_id >= self.vocab_size:
307
+ special_image_mask = input_ids == self.config.image_token_id
308
+ llm_input_ids = input_ids.clone()
309
+ llm_input_ids[special_image_mask] = 0
310
+ else:
311
+ llm_input_ids = input_ids
312
+
313
+ if inputs_embeds is None:
314
+ inputs_embeds = self.get_input_embeddings()(llm_input_ids)
315
+
316
+ if cache_position is None:
317
+ past_seen_tokens = past_key_values.get_seq_length() if past_key_values is not None else 0
318
+ cache_position = torch.arange(
319
+ past_seen_tokens, past_seen_tokens + inputs_embeds.shape[1], device=inputs_embeds.device
320
+ )
321
+
322
+ if position_ids is None:
323
+ position_ids = cache_position.unsqueeze(0) + 1 # Paligemma positions are 1-indexed
324
+
325
+ # Merge text and images
326
+ if pixel_values is not None:
327
+ image_features = self.get_image_features(pixel_values)
328
+
329
+ if input_ids is None:
330
+ special_image_mask = inputs_embeds == self.get_input_embeddings()(
331
+ torch.tensor(self.config.image_token_id, dtype=torch.long, device=inputs_embeds.device)
332
+ )
333
+ else:
334
+ special_image_mask = (input_ids == self.config.image_token_id).unsqueeze(-1)
335
+ special_image_mask = special_image_mask.expand_as(inputs_embeds).to(inputs_embeds.device)
336
+
337
+ if not is_torchdynamo_compiling() and inputs_embeds[special_image_mask].numel() != image_features.numel():
338
+ image_tokens_in_text = (special_image_mask).sum(dim=1).sum(dim=0)[0]
339
+ raise ValueError(
340
+ f"Number of images does not match number of special image tokens in the input text. "
341
+ f"Got {image_tokens_in_text} image tokens in the text but {image_features.shape[0] * image_features.shape[1]} "
342
+ "tokens from image embeddings."
343
+ )
344
+ image_features = image_features.to(inputs_embeds.device, inputs_embeds.dtype)
345
+ inputs_embeds = inputs_embeds.masked_scatter(special_image_mask, image_features)
346
+
347
+ causal_mask = self._update_causal_mask(
348
+ attention_mask, token_type_ids, past_key_values, cache_position, inputs_embeds, is_training
349
+ )
350
+ outputs = self.language_model(
351
+ attention_mask=causal_mask,
352
+ position_ids=position_ids,
353
+ past_key_values=past_key_values,
354
+ inputs_embeds=inputs_embeds,
355
+ use_cache=use_cache,
356
+ output_attentions=output_attentions,
357
+ output_hidden_states=output_hidden_states,
358
+ return_dict=True,
359
+ cache_position=cache_position,
360
+ **kwargs,
361
+ )
362
+
363
+ return PaligemmaModelOutputWithPast(
364
+ last_hidden_state=outputs.last_hidden_state,
365
+ past_key_values=outputs.past_key_values,
366
+ hidden_states=outputs.hidden_states,
367
+ attentions=outputs.attentions,
368
+ image_hidden_states=image_features if pixel_values is not None else None,
369
+ )
370
+
371
+
372
+ class KwargsForCausalLM(FlashAttentionKwargs, LossKwargs): ...
373
+
374
+
375
+ @auto_docstring(
376
+ custom_intro="""
377
+ The Base Paligemma model which consists of a vision backbone and a language model without language modeling head.,
378
+ """
379
+ )
380
+ class PaliGemmaForConditionalGeneration(PaliGemmaPreTrainedModel, GenerationMixin):
381
+ _checkpoint_conversion_mapping = {
382
+ "^language_model.model": "model.language_model",
383
+ "^vision_tower": "model.vision_tower",
384
+ "^multi_modal_projector": "model.multi_modal_projector",
385
+ "^language_model.lm_head": "lm_head",
386
+ }
387
+ _tied_weights_keys = ["lm_head.weight"]
388
+
389
+ def __init__(self, config: PaliGemmaConfig):
390
+ super().__init__(config)
391
+ self.model = PaliGemmaModel(config)
392
+ self.lm_head = nn.Linear(config.text_config.hidden_size, config.text_config.vocab_size, bias=False)
393
+ self.post_init()
394
+
395
+ def get_input_embeddings(self):
396
+ return self.model.get_input_embeddings()
397
+
398
+ def set_input_embeddings(self, value):
399
+ self.model.set_input_embeddings(value)
400
+
401
+ def get_output_embeddings(self):
402
+ return self.lm_head
403
+
404
+ def set_output_embeddings(self, new_embeddings):
405
+ self.lm_head = new_embeddings
406
+
407
+ def set_decoder(self, decoder):
408
+ self.model.set_decoder(decoder)
409
+
410
+ def get_decoder(self):
411
+ return self.model.get_decoder()
412
+
413
+ def get_image_features(self, pixel_values):
414
+ return self.model.get_image_features(pixel_values)
415
+
416
+ # Make modules available throught conditional class for BC
417
+ @property
418
+ def language_model(self):
419
+ return self.model.language_model
420
+
421
+ @property
422
+ def vision_tower(self):
423
+ return self.model.vision_tower
424
+
425
+ @property
426
+ def multi_modal_projector(self):
427
+ return self.model.multi_modal_projector
428
+
429
+ @can_return_tuple
430
+ @auto_docstring
431
+ def forward(
432
+ self,
433
+ input_ids: torch.LongTensor = None,
434
+ pixel_values: torch.FloatTensor = None,
435
+ attention_mask: Optional[torch.Tensor] = None,
436
+ position_ids: Optional[torch.LongTensor] = None,
437
+ past_key_values: Optional[Union[list[torch.FloatTensor], Cache]] = None,
438
+ token_type_ids: Optional[torch.LongTensor] = None,
439
+ cache_position: Optional[torch.LongTensor] = None,
440
+ inputs_embeds: Optional[torch.FloatTensor] = None,
441
+ labels: Optional[torch.LongTensor] = None,
442
+ use_cache: Optional[bool] = None,
443
+ output_attentions: Optional[bool] = None,
444
+ output_hidden_states: Optional[bool] = None,
445
+ return_dict: Optional[bool] = None,
446
+ logits_to_keep: Union[int, torch.Tensor] = 0,
447
+ **kwargs: Unpack[KwargsForCausalLM],
448
+ ) -> Union[tuple, PaliGemmaCausalLMOutputWithPast]:
449
+ r"""
450
+ labels (`torch.LongTensor` of shape `(batch_size, sequence_length)`, *optional*):
451
+ Labels for computing the masked language modeling loss. Indices should either be in `[0, ...,
452
+ config.text_config.vocab_size]` or -100 (see `input_ids` docstring). Tokens with indices set to `-100` are ignored
453
+ (masked), the loss is only computed for the tokens with labels in `[0, ..., config.text_config.vocab_size]`.
454
+
455
+ Example:
456
+
457
+ ```python
458
+ >>> from PIL import Image
459
+ >>> import requests
460
+ >>> from transformers import AutoProcessor, PaliGemmaForConditionalGeneration
461
+
462
+ >>> model = PaliGemmaForConditionalGeneration.from_pretrained("google/paligemma2-3b-mix-224")
463
+ >>> processor = AutoProcessor.from_pretrained("google/paligemma2-3b-mix-224")
464
+
465
+ >>> prompt = "Where is the cat standing?"
466
+ >>> url = "https://huggingface.co/datasets/huggingface/documentation-images/resolve/main/pipeline-cat-chonk.jpeg"
467
+ >>> image = Image.open(requests.get(url, stream=True).raw)
468
+
469
+ >>> inputs = processor(images=image, text=prompt, return_tensors="pt")
470
+
471
+ >>> # Generate
472
+ >>> generate_ids = model.generate(**inputs,)
473
+ >>> processor.batch_decode(generate_ids, skip_special_tokens=True, clean_up_tokenization_spaces=False)[0]
474
+ "Where is the cat standing?\nsnow"
475
+ ```"""
476
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
477
+ output_hidden_states = (
478
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
479
+ )
480
+ return_dict = return_dict if return_dict is not None else self.config.use_return_dict
481
+
482
+ outputs = self.model(
483
+ input_ids=input_ids,
484
+ pixel_values=pixel_values,
485
+ token_type_ids=token_type_ids,
486
+ attention_mask=attention_mask,
487
+ position_ids=position_ids,
488
+ past_key_values=past_key_values,
489
+ inputs_embeds=inputs_embeds,
490
+ use_cache=use_cache,
491
+ labels=labels,
492
+ output_attentions=output_attentions,
493
+ output_hidden_states=output_hidden_states,
494
+ return_dict=True,
495
+ cache_position=cache_position,
496
+ **kwargs,
497
+ )
498
+
499
+ hidden_states = outputs[0]
500
+ # Only compute necessary logits, and do not upcast them to float if we are not computing the loss
501
+ slice_indices = slice(-logits_to_keep, None) if isinstance(logits_to_keep, int) else logits_to_keep
502
+ logits = self.lm_head(hidden_states[:, slice_indices, :])
503
+
504
+ loss = None
505
+ if labels is not None:
506
+ loss = self.loss_function(
507
+ logits=logits, labels=labels, vocab_size=self.config.text_config.vocab_size, **kwargs
508
+ )
509
+
510
+ return PaliGemmaCausalLMOutputWithPast(
511
+ loss=loss,
512
+ logits=logits,
513
+ past_key_values=outputs.past_key_values,
514
+ hidden_states=outputs.hidden_states,
515
+ attentions=outputs.attentions,
516
+ image_hidden_states=outputs.image_hidden_states,
517
+ )
518
+
519
+ def prepare_inputs_for_generation(
520
+ self,
521
+ input_ids,
522
+ past_key_values=None,
523
+ inputs_embeds=None,
524
+ cache_position=None,
525
+ position_ids=None,
526
+ pixel_values=None,
527
+ attention_mask=None,
528
+ token_type_ids=None,
529
+ use_cache=True,
530
+ logits_to_keep=None,
531
+ labels=None,
532
+ **kwargs,
533
+ ):
534
+ # Overwritten -- custom `position_ids` and `pixel_values` handling
535
+ model_inputs = super().prepare_inputs_for_generation(
536
+ input_ids,
537
+ past_key_values=past_key_values,
538
+ inputs_embeds=inputs_embeds,
539
+ attention_mask=attention_mask,
540
+ position_ids=position_ids,
541
+ cache_position=cache_position,
542
+ use_cache=use_cache,
543
+ logits_to_keep=logits_to_keep,
544
+ token_type_ids=token_type_ids,
545
+ **kwargs,
546
+ )
547
+
548
+ # position_ids in Paligemma are 1-indexed
549
+ if model_inputs.get("position_ids") is not None:
550
+ model_inputs["position_ids"] += 1
551
+ # If we're in cached decoding stage, pixel values should be None because input ids do not contain special image token anymore
552
+ # Otherwise we need pixel values to be passed to model. NOTE: use_cache=False needs pixel_values always
553
+ if cache_position[0] == 0:
554
+ model_inputs["pixel_values"] = pixel_values
555
+ is_training = token_type_ids is not None and labels is not None
556
+ if cache_position[0] == 0 and isinstance(past_key_values, HybridCache):
557
+ input_tensor = inputs_embeds if inputs_embeds is not None else input_ids
558
+ causal_mask = self.model._update_causal_mask(
559
+ attention_mask, token_type_ids, past_key_values, cache_position, input_tensor, is_training
560
+ )
561
+ model_inputs["attention_mask"] = causal_mask
562
+
563
+ return model_inputs
564
+
565
+ @staticmethod
566
+ # Copied from transformers.models.gptj.modeling_gptj.GPTJModel._prepare_4d_causal_attention_mask_with_cache_position
567
+ def _prepare_4d_causal_attention_mask_with_cache_position(
568
+ attention_mask: torch.Tensor,
569
+ sequence_length: int,
570
+ target_length: int,
571
+ dtype: torch.dtype,
572
+ cache_position: torch.Tensor,
573
+ batch_size: int,
574
+ **kwargs,
575
+ ):
576
+ """
577
+ Creates a causal 4D mask of shape `(batch_size, 1, query_length, key_value_length)` from a 2D mask of shape
578
+ `(batch_size, key_value_length)`, or if the input `attention_mask` is already 4D, do nothing.
579
+
580
+ Args:
581
+ attention_mask (`torch.Tensor`):
582
+ A 2D attention mask of shape `(batch_size, key_value_length)` or a 4D attention mask of shape
583
+ `(batch_size, 1, query_length, key_value_length)`.
584
+ sequence_length (`int`):
585
+ The sequence length being processed.
586
+ target_length (`int`):
587
+ The target length: when generating with static cache, the mask should be as long as the static cache,
588
+ to account for the 0 padding, the part of the cache that is not filled yet.
589
+ dtype (`torch.dtype`):
590
+ The dtype to use for the 4D attention mask.
591
+ cache_position (`torch.Tensor`):
592
+ Indices depicting the position of the input sequence tokens in the sequence.
593
+ batch_size (`torch.Tensor`):
594
+ Batch size.
595
+ """
596
+ if attention_mask is not None and attention_mask.dim() == 4:
597
+ # In this case we assume that the mask comes already in inverted form and requires no inversion or slicing.
598
+ causal_mask = attention_mask
599
+ else:
600
+ min_dtype = torch.finfo(dtype).min
601
+ causal_mask = torch.full(
602
+ (sequence_length, target_length), fill_value=min_dtype, dtype=dtype, device=cache_position.device
603
+ )
604
+ if sequence_length != 1:
605
+ causal_mask = torch.triu(causal_mask, diagonal=1)
606
+ causal_mask *= torch.arange(target_length, device=cache_position.device) > cache_position.reshape(-1, 1)
607
+ causal_mask = causal_mask[None, None, :, :].expand(batch_size, 1, -1, -1)
608
+ if attention_mask is not None:
609
+ causal_mask = causal_mask.clone() # copy to contiguous memory for in-place edit
610
+ mask_length = attention_mask.shape[-1]
611
+ padding_mask = causal_mask[:, :, :, :mask_length] + attention_mask[:, None, None, :].to(
612
+ causal_mask.device
613
+ )
614
+ padding_mask = padding_mask == 0
615
+ causal_mask[:, :, :, :mask_length] = causal_mask[:, :, :, :mask_length].masked_fill(
616
+ padding_mask, min_dtype
617
+ )
618
+
619
+ return causal_mask
620
+
621
+
622
+ __all__ = ["PaliGemmaForConditionalGeneration", "PaliGemmaPreTrainedModel", "PaliGemmaModel"]
openpi_runtime/openpi/models_pytorch/transformers_replace/models/siglip/check.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import transformers
2
+
3
+ def check_whether_transformers_replace_is_installed_correctly():
4
+ return transformers.__version__ == "4.53.2"
openpi_runtime/openpi/models_pytorch/transformers_replace/models/siglip/modeling_siglip.py ADDED
@@ -0,0 +1,1237 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # coding=utf-8
2
+ # Copyright 2024 Google AI and The HuggingFace Team. All rights reserved.
3
+ #
4
+ # Licensed under the Apache License, Version 2.0 (the "License");
5
+ # you may not use this file except in compliance with the License.
6
+ # You may obtain a copy of the License at
7
+ #
8
+ # http://www.apache.org/licenses/LICENSE-2.0
9
+ #
10
+ # Unless required by applicable law or agreed to in writing, software
11
+ # distributed under the License is distributed on an "AS IS" BASIS,
12
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
+ # See the License for the specific language governing permissions and
14
+ # limitations under the License.
15
+ """PyTorch Siglip model."""
16
+
17
+ import math
18
+ import warnings
19
+ from dataclasses import dataclass
20
+ from typing import Any, Callable, Optional, Union
21
+
22
+ import numpy as np
23
+ import torch
24
+ import torch.utils.checkpoint
25
+ from torch import nn
26
+ from torch.nn import BCEWithLogitsLoss, CrossEntropyLoss, MSELoss
27
+ from torch.nn.init import _calculate_fan_in_and_fan_out
28
+
29
+ from ...activations import ACT2FN
30
+ from ...modeling_attn_mask_utils import _prepare_4d_attention_mask
31
+ from ...modeling_layers import GradientCheckpointingLayer
32
+ from ...modeling_outputs import BaseModelOutput, BaseModelOutputWithPooling, ImageClassifierOutput
33
+ from ...modeling_utils import ALL_ATTENTION_FUNCTIONS, PreTrainedModel
34
+ from ...utils import ModelOutput, auto_docstring, can_return_tuple, logging, torch_int
35
+ from .configuration_siglip import SiglipConfig, SiglipTextConfig, SiglipVisionConfig
36
+
37
+
38
+ logger = logging.get_logger(__name__)
39
+
40
+
41
+ def _trunc_normal_(tensor, mean, std, a, b):
42
+ # Cut & paste from PyTorch official master until it's in a few official releases - RW
43
+ # Method based on https://people.sc.fsu.edu/~jburkardt/presentations/truncated_normal.pdf
44
+ def norm_cdf(x):
45
+ # Computes standard normal cumulative distribution function
46
+ return (1.0 + math.erf(x / math.sqrt(2.0))) / 2.0
47
+
48
+ if (mean < a - 2 * std) or (mean > b + 2 * std):
49
+ warnings.warn(
50
+ "mean is more than 2 std from [a, b] in nn.init.trunc_normal_. "
51
+ "The distribution of values may be incorrect.",
52
+ stacklevel=2,
53
+ )
54
+
55
+ # Values are generated by using a truncated uniform distribution and
56
+ # then using the inverse CDF for the normal distribution.
57
+ # Get upper and lower cdf values
58
+ l = norm_cdf((a - mean) / std)
59
+ u = norm_cdf((b - mean) / std)
60
+
61
+ # Uniformly fill tensor with values from [l, u], then translate to
62
+ # [2l-1, 2u-1].
63
+ tensor.uniform_(2 * l - 1, 2 * u - 1)
64
+
65
+ # Use inverse cdf transform for normal distribution to get truncated
66
+ # standard normal
67
+ tensor.erfinv_()
68
+
69
+ # Transform to proper mean, std
70
+ tensor.mul_(std * math.sqrt(2.0))
71
+ tensor.add_(mean)
72
+
73
+ # Clamp to ensure it's in the proper range
74
+ tensor.clamp_(min=a, max=b)
75
+
76
+
77
+ def trunc_normal_tf_(
78
+ tensor: torch.Tensor, mean: float = 0.0, std: float = 1.0, a: float = -2.0, b: float = 2.0
79
+ ) -> torch.Tensor:
80
+ """Fills the input Tensor with values drawn from a truncated
81
+ normal distribution. The values are effectively drawn from the
82
+ normal distribution :math:`\\mathcal{N}(\text{mean}, \text{std}^2)`
83
+ with values outside :math:`[a, b]` redrawn until they are within
84
+ the bounds. The method used for generating the random values works
85
+ best when :math:`a \\leq \text{mean} \\leq b`.
86
+
87
+ NOTE: this 'tf' variant behaves closer to Tensorflow / JAX impl where the
88
+ bounds [a, b] are applied when sampling the normal distribution with mean=0, std=1.0
89
+ and the result is subsequently scaled and shifted by the mean and std args.
90
+
91
+ Args:
92
+ tensor: an n-dimensional `torch.Tensor`
93
+ mean: the mean of the normal distribution
94
+ std: the standard deviation of the normal distribution
95
+ a: the minimum cutoff value
96
+ b: the maximum cutoff value
97
+ """
98
+ with torch.no_grad():
99
+ _trunc_normal_(tensor, 0, 1.0, a, b)
100
+ tensor.mul_(std).add_(mean)
101
+
102
+
103
+ def variance_scaling_(tensor, scale=1.0, mode="fan_in", distribution="normal"):
104
+ fan_in, fan_out = _calculate_fan_in_and_fan_out(tensor)
105
+ if mode == "fan_in":
106
+ denom = fan_in
107
+ elif mode == "fan_out":
108
+ denom = fan_out
109
+ elif mode == "fan_avg":
110
+ denom = (fan_in + fan_out) / 2
111
+
112
+ variance = scale / denom
113
+
114
+ if distribution == "truncated_normal":
115
+ # constant is stddev of standard normal truncated to (-2, 2)
116
+ trunc_normal_tf_(tensor, std=math.sqrt(variance) / 0.87962566103423978)
117
+ elif distribution == "normal":
118
+ with torch.no_grad():
119
+ tensor.normal_(std=math.sqrt(variance))
120
+ elif distribution == "uniform":
121
+ bound = math.sqrt(3 * variance)
122
+ with torch.no_grad():
123
+ tensor.uniform_(-bound, bound)
124
+ else:
125
+ raise ValueError(f"invalid distribution {distribution}")
126
+
127
+
128
+ def lecun_normal_(tensor):
129
+ variance_scaling_(tensor, mode="fan_in", distribution="truncated_normal")
130
+
131
+
132
+ def default_flax_embed_init(tensor):
133
+ variance_scaling_(tensor, mode="fan_in", distribution="normal")
134
+
135
+
136
+ @dataclass
137
+ @auto_docstring(
138
+ custom_intro="""
139
+ Base class for vision model's outputs that also contains image embeddings of the pooling of the last hidden states.
140
+ """
141
+ )
142
+ # Copied from transformers.models.clip.modeling_clip.CLIPVisionModelOutput with CLIP->Siglip
143
+ class SiglipVisionModelOutput(ModelOutput):
144
+ r"""
145
+ image_embeds (`torch.FloatTensor` of shape `(batch_size, output_dim)` *optional* returned when model is initialized with `with_projection=True`):
146
+ The image embeddings obtained by applying the projection layer to the pooler_output.
147
+ """
148
+
149
+ image_embeds: Optional[torch.FloatTensor] = None
150
+ last_hidden_state: Optional[torch.FloatTensor] = None
151
+ hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None
152
+ attentions: Optional[tuple[torch.FloatTensor, ...]] = None
153
+
154
+
155
+ @dataclass
156
+ @auto_docstring(
157
+ custom_intro="""
158
+ Base class for text model's outputs that also contains a pooling of the last hidden states.
159
+ """
160
+ )
161
+ # Copied from transformers.models.clip.modeling_clip.CLIPTextModelOutput with CLIP->Siglip
162
+ class SiglipTextModelOutput(ModelOutput):
163
+ r"""
164
+ text_embeds (`torch.FloatTensor` of shape `(batch_size, output_dim)` *optional* returned when model is initialized with `with_projection=True`):
165
+ The text embeddings obtained by applying the projection layer to the pooler_output.
166
+ """
167
+
168
+ text_embeds: Optional[torch.FloatTensor] = None
169
+ last_hidden_state: Optional[torch.FloatTensor] = None
170
+ hidden_states: Optional[tuple[torch.FloatTensor, ...]] = None
171
+ attentions: Optional[tuple[torch.FloatTensor, ...]] = None
172
+
173
+
174
+ @dataclass
175
+ @auto_docstring
176
+ # Copied from transformers.models.clip.modeling_clip.CLIPOutput with CLIP->Siglip
177
+ class SiglipOutput(ModelOutput):
178
+ r"""
179
+ loss (`torch.FloatTensor` of shape `(1,)`, *optional*, returned when `return_loss` is `True`):
180
+ Contrastive loss for image-text similarity.
181
+ logits_per_image (`torch.FloatTensor` of shape `(image_batch_size, text_batch_size)`):
182
+ The scaled dot product scores between `image_embeds` and `text_embeds`. This represents the image-text
183
+ similarity scores.
184
+ logits_per_text (`torch.FloatTensor` of shape `(text_batch_size, image_batch_size)`):
185
+ The scaled dot product scores between `text_embeds` and `image_embeds`. This represents the text-image
186
+ similarity scores.
187
+ text_embeds (`torch.FloatTensor` of shape `(batch_size, output_dim`):
188
+ The text embeddings obtained by applying the projection layer to the pooled output of [`SiglipTextModel`].
189
+ image_embeds (`torch.FloatTensor` of shape `(batch_size, output_dim`):
190
+ The image embeddings obtained by applying the projection layer to the pooled output of [`SiglipVisionModel`].
191
+ text_model_output (`BaseModelOutputWithPooling`):
192
+ The output of the [`SiglipTextModel`].
193
+ vision_model_output (`BaseModelOutputWithPooling`):
194
+ The output of the [`SiglipVisionModel`].
195
+ """
196
+
197
+ loss: Optional[torch.FloatTensor] = None
198
+ logits_per_image: Optional[torch.FloatTensor] = None
199
+ logits_per_text: Optional[torch.FloatTensor] = None
200
+ text_embeds: Optional[torch.FloatTensor] = None
201
+ image_embeds: Optional[torch.FloatTensor] = None
202
+ text_model_output: BaseModelOutputWithPooling = None
203
+ vision_model_output: BaseModelOutputWithPooling = None
204
+
205
+ def to_tuple(self) -> tuple[Any]:
206
+ return tuple(
207
+ self[k] if k not in ["text_model_output", "vision_model_output"] else getattr(self, k).to_tuple()
208
+ for k in self.keys()
209
+ )
210
+
211
+
212
+ class SiglipVisionEmbeddings(nn.Module):
213
+ def __init__(self, config: SiglipVisionConfig):
214
+ super().__init__()
215
+ self.config = config
216
+ self.embed_dim = config.hidden_size
217
+ self.image_size = config.image_size
218
+ self.patch_size = config.patch_size
219
+
220
+ self.patch_embedding = nn.Conv2d(
221
+ in_channels=config.num_channels,
222
+ out_channels=self.embed_dim,
223
+ kernel_size=self.patch_size,
224
+ stride=self.patch_size,
225
+ padding="valid",
226
+ )
227
+
228
+ self.num_patches = (self.image_size // self.patch_size) ** 2
229
+ self.num_positions = self.num_patches
230
+ self.position_embedding = nn.Embedding(self.num_positions, self.embed_dim)
231
+ self.register_buffer("position_ids", torch.arange(self.num_positions).expand((1, -1)), persistent=False)
232
+
233
+ def interpolate_pos_encoding(self, embeddings: torch.Tensor, height: int, width: int) -> torch.Tensor:
234
+ """
235
+ This method allows to interpolate the pre-trained position encodings, to be able to use the model on higher resolution
236
+ images. This method is also adapted to support torch.jit tracing and no class embeddings.
237
+
238
+ Adapted from:
239
+ - https://github.com/facebookresearch/dino/blob/de9ee3df6cf39fac952ab558447af1fa1365362a/vision_transformer.py#L174-L194, and
240
+ - https://github.com/facebookresearch/dinov2/blob/e1277af2ba9496fbadf7aec6eba56e8d882d1e35/dinov2/models/vision_transformer.py#L179-L211
241
+ """
242
+
243
+ num_patches = embeddings.shape[1]
244
+ num_positions = self.position_embedding.weight.shape[0]
245
+
246
+ # always interpolate when tracing to ensure the exported model works for dynamic input shapes
247
+ if not torch.jit.is_tracing() and num_patches == num_positions and height == width:
248
+ return self.position_embedding(self.position_ids)
249
+
250
+ patch_pos_embed = self.position_embedding.weight.unsqueeze(0)
251
+
252
+ dim = embeddings.shape[-1]
253
+
254
+ new_height = height // self.patch_size
255
+ new_width = width // self.patch_size
256
+
257
+ sqrt_num_positions = torch_int(num_positions**0.5)
258
+ patch_pos_embed = patch_pos_embed.reshape(1, sqrt_num_positions, sqrt_num_positions, dim)
259
+ patch_pos_embed = patch_pos_embed.permute(0, 3, 1, 2)
260
+
261
+ patch_pos_embed = nn.functional.interpolate(
262
+ patch_pos_embed,
263
+ size=(new_height, new_width),
264
+ mode="bicubic",
265
+ align_corners=False,
266
+ )
267
+
268
+ patch_pos_embed = patch_pos_embed.permute(0, 2, 3, 1).view(1, -1, dim)
269
+ return patch_pos_embed
270
+
271
+ def forward(self, pixel_values: torch.FloatTensor, interpolate_pos_encoding=False) -> torch.Tensor:
272
+ _, _, height, width = pixel_values.shape
273
+ target_dtype = self.patch_embedding.weight.dtype
274
+ patch_embeds = self.patch_embedding(pixel_values.to(dtype=target_dtype)) # shape = [*, width, grid, grid]
275
+ embeddings = patch_embeds.flatten(2).transpose(1, 2)
276
+
277
+ if interpolate_pos_encoding:
278
+ embeddings = embeddings + self.interpolate_pos_encoding(embeddings, height, width)
279
+ else:
280
+ embeddings = embeddings + self.position_embedding(self.position_ids)
281
+ return embeddings
282
+
283
+
284
+ # Copied from transformers.models.clip.modeling_clip.CLIPTextEmbeddings with CLIP->Siglip
285
+ class SiglipTextEmbeddings(nn.Module):
286
+ def __init__(self, config: SiglipTextConfig):
287
+ super().__init__()
288
+ embed_dim = config.hidden_size
289
+
290
+ self.token_embedding = nn.Embedding(config.vocab_size, embed_dim)
291
+ self.position_embedding = nn.Embedding(config.max_position_embeddings, embed_dim)
292
+
293
+ # position_ids (1, len position emb) is contiguous in memory and exported when serialized
294
+ self.register_buffer(
295
+ "position_ids", torch.arange(config.max_position_embeddings).expand((1, -1)), persistent=False
296
+ )
297
+
298
+ def forward(
299
+ self,
300
+ input_ids: Optional[torch.LongTensor] = None,
301
+ position_ids: Optional[torch.LongTensor] = None,
302
+ inputs_embeds: Optional[torch.FloatTensor] = None,
303
+ ) -> torch.Tensor:
304
+ seq_length = input_ids.shape[-1] if input_ids is not None else inputs_embeds.shape[-2]
305
+ max_position_embedding = self.position_embedding.weight.shape[0]
306
+
307
+ if seq_length > max_position_embedding:
308
+ raise ValueError(
309
+ f"Sequence length must be less than max_position_embeddings (got `sequence length`: "
310
+ f"{seq_length} and max_position_embeddings: {max_position_embedding}"
311
+ )
312
+
313
+ if position_ids is None:
314
+ position_ids = self.position_ids[:, :seq_length]
315
+
316
+ if inputs_embeds is None:
317
+ inputs_embeds = self.token_embedding(input_ids)
318
+
319
+ position_embeddings = self.position_embedding(position_ids)
320
+ embeddings = inputs_embeds + position_embeddings
321
+
322
+ return embeddings
323
+
324
+
325
+ def eager_attention_forward(
326
+ module: nn.Module,
327
+ query: torch.Tensor,
328
+ key: torch.Tensor,
329
+ value: torch.Tensor,
330
+ attention_mask: Optional[torch.Tensor],
331
+ scaling: float,
332
+ dropout: float = 0.0,
333
+ **kwargs,
334
+ ):
335
+ attn_weights = torch.matmul(query, key.transpose(-1, -2)) * scaling
336
+ if attention_mask is not None:
337
+ attn_weights = attn_weights + attention_mask
338
+
339
+ attn_weights = nn.functional.softmax(attn_weights, dim=-1, dtype=torch.float32).to(query.dtype)
340
+ attn_weights = nn.functional.dropout(attn_weights, p=dropout, training=module.training)
341
+
342
+ attn_output = torch.matmul(attn_weights, value)
343
+ attn_output = attn_output.transpose(1, 2).contiguous()
344
+
345
+ return attn_output, attn_weights
346
+
347
+
348
+ class SiglipAttention(nn.Module):
349
+ """Multi-headed attention from 'Attention Is All You Need' paper"""
350
+
351
+ def __init__(self, config):
352
+ super().__init__()
353
+ self.config = config
354
+ self.embed_dim = config.hidden_size
355
+ self.num_heads = config.num_attention_heads
356
+ self.head_dim = self.embed_dim // self.num_heads
357
+ if self.head_dim * self.num_heads != self.embed_dim:
358
+ raise ValueError(
359
+ f"embed_dim must be divisible by num_heads (got `embed_dim`: {self.embed_dim} and `num_heads`:"
360
+ f" {self.num_heads})."
361
+ )
362
+ self.scale = self.head_dim**-0.5
363
+ self.dropout = config.attention_dropout
364
+ self.is_causal = False
365
+
366
+ self.k_proj = nn.Linear(self.embed_dim, self.embed_dim)
367
+ self.v_proj = nn.Linear(self.embed_dim, self.embed_dim)
368
+ self.q_proj = nn.Linear(self.embed_dim, self.embed_dim)
369
+ self.out_proj = nn.Linear(self.embed_dim, self.embed_dim)
370
+
371
+ def forward(
372
+ self,
373
+ hidden_states: torch.Tensor,
374
+ attention_mask: Optional[torch.Tensor] = None,
375
+ output_attentions: Optional[bool] = False,
376
+ ) -> tuple[torch.Tensor, Optional[torch.Tensor]]:
377
+ """Input shape: Batch x Time x Channel"""
378
+
379
+ batch_size, seq_length, embed_dim = hidden_states.shape
380
+
381
+ queries = self.q_proj(hidden_states)
382
+ keys = self.k_proj(hidden_states)
383
+ values = self.v_proj(hidden_states)
384
+
385
+ queries = queries.view(batch_size, seq_length, self.num_heads, self.head_dim).transpose(1, 2)
386
+ keys = keys.view(batch_size, seq_length, self.num_heads, self.head_dim).transpose(1, 2)
387
+ values = values.view(batch_size, seq_length, self.num_heads, self.head_dim).transpose(1, 2)
388
+
389
+ attention_interface: Callable = eager_attention_forward
390
+ if self.config._attn_implementation != "eager":
391
+ if self.config._attn_implementation == "sdpa" and output_attentions:
392
+ logger.warning_once(
393
+ "`torch.nn.functional.scaled_dot_product_attention` does not support `output_attentions=True`. Falling back to "
394
+ 'eager attention. This warning can be removed using the argument `attn_implementation="eager"` when loading the model.'
395
+ )
396
+ else:
397
+ attention_interface = ALL_ATTENTION_FUNCTIONS[self.config._attn_implementation]
398
+
399
+ attn_output, attn_weights = attention_interface(
400
+ self,
401
+ queries,
402
+ keys,
403
+ values,
404
+ attention_mask,
405
+ is_causal=self.is_causal,
406
+ scaling=self.scale,
407
+ dropout=0.0 if not self.training else self.dropout,
408
+ )
409
+
410
+ attn_output = attn_output.reshape(batch_size, seq_length, embed_dim).contiguous()
411
+ attn_output = self.out_proj(attn_output)
412
+
413
+ if not output_attentions:
414
+ attn_weights = None
415
+
416
+ return attn_output, attn_weights
417
+
418
+
419
+ # Copied from transformers.models.clip.modeling_clip.CLIPMLP with CLIP->Siglip
420
+ class SiglipMLP(nn.Module):
421
+ def __init__(self, config):
422
+ super().__init__()
423
+ self.config = config
424
+ self.activation_fn = ACT2FN[config.hidden_act]
425
+ self.fc1 = nn.Linear(config.hidden_size, config.intermediate_size)
426
+ self.fc2 = nn.Linear(config.intermediate_size, config.hidden_size)
427
+
428
+ def forward(self, hidden_states: torch.Tensor) -> torch.Tensor:
429
+ hidden_states = self.fc1(hidden_states)
430
+ hidden_states = self.activation_fn(hidden_states)
431
+ hidden_states = self.fc2(hidden_states)
432
+ return hidden_states
433
+
434
+
435
+ class SiglipEncoderLayer(GradientCheckpointingLayer):
436
+ def __init__(self, config: Union[SiglipVisionConfig, SiglipTextConfig]):
437
+ super().__init__()
438
+ self.embed_dim = config.hidden_size
439
+ self.layer_norm1 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
440
+ self.self_attn = SiglipAttention(config)
441
+ self.layer_norm2 = nn.LayerNorm(self.embed_dim, eps=config.layer_norm_eps)
442
+ self.mlp = SiglipMLP(config)
443
+
444
+ def forward(
445
+ self,
446
+ hidden_states: torch.Tensor,
447
+ attention_mask: torch.Tensor,
448
+ output_attentions: Optional[bool] = False,
449
+ ) -> tuple[torch.FloatTensor]:
450
+ """
451
+ Args:
452
+ hidden_states (`torch.FloatTensor`):
453
+ Input to the layer of shape `(batch, seq_len, embed_dim)`.
454
+ attention_mask (`torch.FloatTensor`):
455
+ Attention mask of shape `(batch, 1, q_len, k_v_seq_len)` where padding elements are indicated by very large negative values.
456
+ output_attentions (`bool`, *optional*, defaults to `False`):
457
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under
458
+ returned tensors for more detail.
459
+ """
460
+ residual = hidden_states
461
+
462
+ hidden_states = self.layer_norm1(hidden_states)
463
+ hidden_states, attn_weights = self.self_attn(
464
+ hidden_states=hidden_states,
465
+ attention_mask=attention_mask,
466
+ output_attentions=output_attentions,
467
+ )
468
+ hidden_states = residual + hidden_states
469
+
470
+ residual = hidden_states
471
+ hidden_states = self.layer_norm2(hidden_states)
472
+ hidden_states = self.mlp(hidden_states)
473
+ hidden_states = residual + hidden_states
474
+
475
+ outputs = (hidden_states,)
476
+
477
+ if output_attentions:
478
+ outputs += (attn_weights,)
479
+
480
+ return outputs
481
+
482
+
483
+ @auto_docstring
484
+ class SiglipPreTrainedModel(PreTrainedModel):
485
+ config_class = SiglipConfig
486
+ base_model_prefix = "siglip"
487
+ supports_gradient_checkpointing = True
488
+
489
+ _no_split_modules = [
490
+ "SiglipTextEmbeddings",
491
+ "SiglipEncoderLayer",
492
+ "SiglipVisionEmbeddings",
493
+ "SiglipEncoderLayer",
494
+ "SiglipMultiheadAttentionPoolingHead",
495
+ ]
496
+ _supports_flash_attn_2 = True
497
+ _supports_sdpa = True
498
+ _supports_flex_attn = True
499
+ _supports_attention_backend = True
500
+
501
+ def _init_weights(self, module):
502
+ """Initialize the weights"""
503
+ if isinstance(module, SiglipVisionEmbeddings):
504
+ width = (
505
+ self.config.vision_config.hidden_size
506
+ if isinstance(self.config, SiglipConfig)
507
+ else self.config.hidden_size
508
+ )
509
+ nn.init.normal_(module.position_embedding.weight, std=1 / np.sqrt(width))
510
+ elif isinstance(module, nn.Embedding):
511
+ default_flax_embed_init(module.weight)
512
+ elif isinstance(module, SiglipAttention):
513
+ nn.init.xavier_uniform_(module.q_proj.weight)
514
+ nn.init.xavier_uniform_(module.k_proj.weight)
515
+ nn.init.xavier_uniform_(module.v_proj.weight)
516
+ nn.init.xavier_uniform_(module.out_proj.weight)
517
+ nn.init.zeros_(module.q_proj.bias)
518
+ nn.init.zeros_(module.k_proj.bias)
519
+ nn.init.zeros_(module.v_proj.bias)
520
+ nn.init.zeros_(module.out_proj.bias)
521
+ elif isinstance(module, SiglipMLP):
522
+ nn.init.xavier_uniform_(module.fc1.weight)
523
+ nn.init.xavier_uniform_(module.fc2.weight)
524
+ nn.init.normal_(module.fc1.bias, std=1e-6)
525
+ nn.init.normal_(module.fc2.bias, std=1e-6)
526
+ elif isinstance(module, SiglipMultiheadAttentionPoolingHead):
527
+ nn.init.xavier_uniform_(module.probe.data)
528
+ nn.init.xavier_uniform_(module.attention.in_proj_weight.data)
529
+ nn.init.zeros_(module.attention.in_proj_bias.data)
530
+ elif isinstance(module, SiglipModel):
531
+ logit_scale_init = torch.log(torch.tensor(1.0))
532
+ module.logit_scale.data.fill_(logit_scale_init)
533
+ module.logit_bias.data.zero_()
534
+ elif isinstance(module, SiglipForImageClassification):
535
+ nn.init.normal_(
536
+ module.classifier.weight,
537
+ std=self.config.vision_config.hidden_size**-0.5 * self.config.initializer_factor,
538
+ )
539
+ elif isinstance(module, (nn.Linear, nn.Conv2d)):
540
+ lecun_normal_(module.weight)
541
+ if module.bias is not None:
542
+ nn.init.zeros_(module.bias)
543
+ elif isinstance(module, nn.LayerNorm):
544
+ module.bias.data.zero_()
545
+ module.weight.data.fill_(1.0)
546
+
547
+
548
+ # Copied from transformers.models.altclip.modeling_altclip.AltCLIPEncoder with AltCLIP->Siglip
549
+ class SiglipEncoder(nn.Module):
550
+ """
551
+ Transformer encoder consisting of `config.num_hidden_layers` self attention layers. Each layer is a
552
+ [`SiglipEncoderLayer`].
553
+
554
+ Args:
555
+ config: SiglipConfig
556
+ """
557
+
558
+ def __init__(self, config: SiglipConfig):
559
+ super().__init__()
560
+ self.config = config
561
+ self.layers = nn.ModuleList([SiglipEncoderLayer(config) for _ in range(config.num_hidden_layers)])
562
+ self.gradient_checkpointing = False
563
+
564
+ # Ignore copy
565
+ @can_return_tuple
566
+ def forward(
567
+ self,
568
+ inputs_embeds,
569
+ attention_mask: Optional[torch.Tensor] = None,
570
+ output_attentions: Optional[bool] = None,
571
+ output_hidden_states: Optional[bool] = None,
572
+ ) -> BaseModelOutput:
573
+ r"""
574
+ Args:
575
+ inputs_embeds (`torch.FloatTensor` of shape `(batch_size, sequence_length, hidden_size)`):
576
+ Optionally, instead of passing `input_ids` you can choose to directly pass an embedded representation.
577
+ This is useful if you want more control over how to convert `input_ids` indices into associated vectors
578
+ than the model's internal embedding lookup matrix.
579
+ attention_mask (`torch.Tensor` of shape `(batch_size, sequence_length)`, *optional*):
580
+ Mask to avoid performing attention on padding token indices. Mask values selected in `[0, 1]`:
581
+
582
+ - 1 for tokens that are **not masked**,
583
+ - 0 for tokens that are **masked**.
584
+
585
+ [What are attention masks?](../glossary#attention-mask)
586
+ output_attentions (`bool`, *optional*):
587
+ Whether or not to return the attentions tensors of all attention layers. See `attentions` under
588
+ returned tensors for more detail.
589
+ output_hidden_states (`bool`, *optional*):
590
+ Whether or not to return the hidden states of all layers. See `hidden_states` under returned tensors
591
+ for more detail.
592
+ return_dict (`bool`, *optional*):
593
+ Whether or not to return a [`~utils.ModelOutput`] instead of a plain tuple.
594
+ """
595
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
596
+ output_hidden_states = (
597
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
598
+ )
599
+
600
+ encoder_states = () if output_hidden_states else None
601
+ all_attentions = () if output_attentions else None
602
+
603
+ hidden_states = inputs_embeds
604
+ for encoder_layer in self.layers:
605
+ if output_hidden_states:
606
+ encoder_states = encoder_states + (hidden_states,)
607
+
608
+ layer_outputs = encoder_layer(
609
+ hidden_states,
610
+ attention_mask,
611
+ output_attentions=output_attentions,
612
+ )
613
+
614
+ hidden_states = layer_outputs[0]
615
+
616
+ if output_attentions:
617
+ all_attentions = all_attentions + (layer_outputs[1],)
618
+
619
+ if output_hidden_states:
620
+ encoder_states = encoder_states + (hidden_states,)
621
+
622
+ return BaseModelOutput(
623
+ last_hidden_state=hidden_states,
624
+ hidden_states=encoder_states,
625
+ attentions=all_attentions,
626
+ )
627
+
628
+
629
+ class SiglipTextTransformer(nn.Module):
630
+ def __init__(self, config: SiglipTextConfig):
631
+ super().__init__()
632
+ self.config = config
633
+ embed_dim = config.hidden_size
634
+ self.embeddings = SiglipTextEmbeddings(config)
635
+ self.encoder = SiglipEncoder(config)
636
+ self.final_layer_norm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
637
+
638
+ self.head = nn.Linear(embed_dim, config.projection_size)
639
+ self._use_flash_attention_2 = config._attn_implementation == "flash_attention_2"
640
+
641
+ @can_return_tuple
642
+ @auto_docstring
643
+ def forward(
644
+ self,
645
+ input_ids: Optional[torch.Tensor] = None,
646
+ attention_mask: Optional[torch.Tensor] = None,
647
+ position_ids: Optional[torch.Tensor] = None,
648
+ output_attentions: Optional[bool] = None,
649
+ output_hidden_states: Optional[bool] = None,
650
+ ) -> BaseModelOutputWithPooling:
651
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
652
+ output_hidden_states = (
653
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
654
+ )
655
+
656
+ if input_ids is None:
657
+ raise ValueError("You have to specify input_ids")
658
+
659
+ input_shape = input_ids.size()
660
+ input_ids = input_ids.view(-1, input_shape[-1])
661
+
662
+ hidden_states = self.embeddings(input_ids=input_ids, position_ids=position_ids)
663
+
664
+ # note: SigLIP's text model does not use a causal mask, unlike the original CLIP model.
665
+ # expand attention_mask
666
+ if attention_mask is not None and not self._use_flash_attention_2:
667
+ # [batch_size, seq_len] -> [batch_size, 1, tgt_seq_len, src_seq_len]
668
+ attention_mask = _prepare_4d_attention_mask(attention_mask, hidden_states.dtype)
669
+
670
+ encoder_outputs: BaseModelOutput = self.encoder(
671
+ inputs_embeds=hidden_states,
672
+ attention_mask=attention_mask,
673
+ output_attentions=output_attentions,
674
+ output_hidden_states=output_hidden_states,
675
+ )
676
+
677
+ last_hidden_state = encoder_outputs.last_hidden_state
678
+ last_hidden_state = self.final_layer_norm(last_hidden_state)
679
+
680
+ # Assuming "sticky" EOS tokenization, last token is always EOS.
681
+ pooled_output = last_hidden_state[:, -1, :]
682
+ pooled_output = self.head(pooled_output)
683
+
684
+ return BaseModelOutputWithPooling(
685
+ last_hidden_state=last_hidden_state,
686
+ pooler_output=pooled_output,
687
+ hidden_states=encoder_outputs.hidden_states,
688
+ attentions=encoder_outputs.attentions,
689
+ )
690
+
691
+
692
+ @auto_docstring(
693
+ custom_intro="""
694
+ The text model from SigLIP without any head or projection on top.
695
+ """
696
+ )
697
+ class SiglipTextModel(SiglipPreTrainedModel):
698
+ config_class = SiglipTextConfig
699
+
700
+ def __init__(self, config: SiglipTextConfig):
701
+ super().__init__(config)
702
+ self.text_model = SiglipTextTransformer(config)
703
+ # Initialize weights and apply final processing
704
+ self.post_init()
705
+
706
+ def get_input_embeddings(self) -> nn.Module:
707
+ return self.text_model.embeddings.token_embedding
708
+
709
+ def set_input_embeddings(self, value):
710
+ self.text_model.embeddings.token_embedding = value
711
+
712
+ @can_return_tuple
713
+ @auto_docstring
714
+ def forward(
715
+ self,
716
+ input_ids: Optional[torch.Tensor] = None,
717
+ attention_mask: Optional[torch.Tensor] = None,
718
+ position_ids: Optional[torch.Tensor] = None,
719
+ output_attentions: Optional[bool] = None,
720
+ output_hidden_states: Optional[bool] = None,
721
+ ) -> BaseModelOutputWithPooling:
722
+ r"""
723
+ Examples:
724
+
725
+ ```python
726
+ >>> from transformers import AutoTokenizer, SiglipTextModel
727
+
728
+ >>> model = SiglipTextModel.from_pretrained("google/siglip-base-patch16-224")
729
+ >>> tokenizer = AutoTokenizer.from_pretrained("google/siglip-base-patch16-224")
730
+
731
+ >>> # important: make sure to set padding="max_length" as that's how the model was trained
732
+ >>> inputs = tokenizer(["a photo of a cat", "a photo of a dog"], padding="max_length", return_tensors="pt")
733
+
734
+ >>> outputs = model(**inputs)
735
+ >>> last_hidden_state = outputs.last_hidden_state
736
+ >>> pooled_output = outputs.pooler_output # pooled (EOS token) states
737
+ ```"""
738
+
739
+ return self.text_model(
740
+ input_ids=input_ids,
741
+ attention_mask=attention_mask,
742
+ position_ids=position_ids,
743
+ output_attentions=output_attentions,
744
+ output_hidden_states=output_hidden_states,
745
+ )
746
+
747
+
748
+ class SiglipVisionTransformer(nn.Module):
749
+ def __init__(self, config: SiglipVisionConfig):
750
+ super().__init__()
751
+ self.config = config
752
+ embed_dim = config.hidden_size
753
+
754
+ self.embeddings = SiglipVisionEmbeddings(config)
755
+ self.encoder = SiglipEncoder(config)
756
+ self.post_layernorm = nn.LayerNorm(embed_dim, eps=config.layer_norm_eps)
757
+ self.use_head = True if not hasattr(config, "vision_use_head") else config.vision_use_head
758
+ if self.use_head:
759
+ self.head = SiglipMultiheadAttentionPoolingHead(config)
760
+
761
+ @can_return_tuple
762
+ @auto_docstring
763
+ def forward(
764
+ self,
765
+ pixel_values,
766
+ output_attentions: Optional[bool] = None,
767
+ output_hidden_states: Optional[bool] = None,
768
+ interpolate_pos_encoding: Optional[bool] = False,
769
+ ) -> BaseModelOutputWithPooling:
770
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
771
+ output_hidden_states = (
772
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
773
+ )
774
+
775
+ hidden_states = self.embeddings(pixel_values, interpolate_pos_encoding=interpolate_pos_encoding)
776
+ # Convert to bfloat16 if the encoder uses bfloat16
777
+ if len(self.encoder.layers) > 0 and self.encoder.layers[0].self_attn.q_proj.weight.dtype == torch.bfloat16:
778
+ hidden_states = hidden_states.to(torch.bfloat16)
779
+
780
+ encoder_outputs: BaseModelOutput = self.encoder(
781
+ inputs_embeds=hidden_states,
782
+ output_attentions=output_attentions,
783
+ output_hidden_states=output_hidden_states,
784
+ )
785
+
786
+ last_hidden_state = encoder_outputs.last_hidden_state
787
+ last_hidden_state = self.post_layernorm(last_hidden_state)
788
+
789
+ pooler_output = self.head(last_hidden_state) if self.use_head else None
790
+
791
+ return BaseModelOutputWithPooling(
792
+ last_hidden_state=last_hidden_state,
793
+ pooler_output=pooler_output,
794
+ hidden_states=encoder_outputs.hidden_states,
795
+ attentions=encoder_outputs.attentions,
796
+ )
797
+
798
+
799
+ class SiglipMultiheadAttentionPoolingHead(nn.Module):
800
+ """Multihead Attention Pooling."""
801
+
802
+ def __init__(self, config: SiglipVisionConfig):
803
+ super().__init__()
804
+
805
+ self.probe = nn.Parameter(torch.randn(1, 1, config.hidden_size))
806
+ self.attention = torch.nn.MultiheadAttention(config.hidden_size, config.num_attention_heads, batch_first=True)
807
+ self.layernorm = nn.LayerNorm(config.hidden_size, eps=config.layer_norm_eps)
808
+ self.mlp = SiglipMLP(config)
809
+
810
+ def forward(self, hidden_state):
811
+ batch_size = hidden_state.shape[0]
812
+ probe = self.probe.repeat(batch_size, 1, 1)
813
+
814
+ hidden_state = self.attention(probe, hidden_state, hidden_state)[0]
815
+
816
+ residual = hidden_state
817
+ hidden_state = self.layernorm(hidden_state)
818
+ hidden_state = residual + self.mlp(hidden_state)
819
+
820
+ return hidden_state[:, 0]
821
+
822
+
823
+ @auto_docstring(
824
+ custom_intro="""
825
+ The vision model from SigLIP without any head or projection on top.
826
+ """
827
+ )
828
+ class SiglipVisionModel(SiglipPreTrainedModel):
829
+ config_class = SiglipVisionConfig
830
+ main_input_name = "pixel_values"
831
+
832
+ def __init__(self, config: SiglipVisionConfig):
833
+ super().__init__(config)
834
+
835
+ self.vision_model = SiglipVisionTransformer(config)
836
+
837
+ # Initialize weights and apply final processing
838
+ self.post_init()
839
+
840
+ def get_input_embeddings(self) -> nn.Module:
841
+ return self.vision_model.embeddings.patch_embedding
842
+
843
+ @can_return_tuple
844
+ @auto_docstring
845
+ def forward(
846
+ self,
847
+ pixel_values,
848
+ output_attentions: Optional[bool] = None,
849
+ output_hidden_states: Optional[bool] = None,
850
+ interpolate_pos_encoding: bool = False,
851
+ ) -> BaseModelOutputWithPooling:
852
+ r"""
853
+ Examples:
854
+
855
+ ```python
856
+ >>> from PIL import Image
857
+ >>> import requests
858
+ >>> from transformers import AutoProcessor, SiglipVisionModel
859
+
860
+ >>> model = SiglipVisionModel.from_pretrained("google/siglip-base-patch16-224")
861
+ >>> processor = AutoProcessor.from_pretrained("google/siglip-base-patch16-224")
862
+
863
+ >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg"
864
+ >>> image = Image.open(requests.get(url, stream=True).raw)
865
+
866
+ >>> inputs = processor(images=image, return_tensors="pt")
867
+
868
+ >>> outputs = model(**inputs)
869
+ >>> last_hidden_state = outputs.last_hidden_state
870
+ >>> pooled_output = outputs.pooler_output # pooled features
871
+ ```"""
872
+
873
+ return self.vision_model(
874
+ pixel_values=pixel_values,
875
+ output_attentions=output_attentions,
876
+ output_hidden_states=output_hidden_states,
877
+ interpolate_pos_encoding=interpolate_pos_encoding,
878
+ )
879
+
880
+
881
+ @auto_docstring
882
+ class SiglipModel(SiglipPreTrainedModel):
883
+ config_class = SiglipConfig
884
+
885
+ def __init__(self, config: SiglipConfig):
886
+ super().__init__(config)
887
+
888
+ if not isinstance(config.text_config, SiglipTextConfig):
889
+ raise TypeError(
890
+ "config.text_config is expected to be of type SiglipTextConfig but is of type"
891
+ f" {type(config.text_config)}."
892
+ )
893
+
894
+ if not isinstance(config.vision_config, SiglipVisionConfig):
895
+ raise TypeError(
896
+ "config.vision_config is expected to be of type SiglipVisionConfig but is of type"
897
+ f" {type(config.vision_config)}."
898
+ )
899
+
900
+ text_config = config.text_config
901
+ vision_config = config.vision_config
902
+
903
+ # First, initialize the text and vision models with proper attention implementation
904
+ text_model = SiglipTextModel._from_config(text_config)
905
+ vision_model = SiglipVisionModel._from_config(vision_config)
906
+
907
+ # Second, get the text and vision submodules (for backward compatibility)
908
+ self.text_model = text_model.text_model
909
+ self.vision_model = vision_model.vision_model
910
+
911
+ self.logit_scale = nn.Parameter(torch.randn(1))
912
+ self.logit_bias = nn.Parameter(torch.randn(1))
913
+
914
+ # Initialize weights and apply final processing
915
+ self.post_init()
916
+
917
+ @auto_docstring
918
+ def get_text_features(
919
+ self,
920
+ input_ids: Optional[torch.Tensor] = None,
921
+ attention_mask: Optional[torch.Tensor] = None,
922
+ position_ids: Optional[torch.Tensor] = None,
923
+ output_attentions: Optional[bool] = None,
924
+ output_hidden_states: Optional[bool] = None,
925
+ ) -> torch.FloatTensor:
926
+ r"""
927
+ Returns:
928
+ text_features (`torch.FloatTensor` of shape `(batch_size, output_dim`): The text embeddings obtained by
929
+ applying the projection layer to the pooled output of [`SiglipTextModel`].
930
+
931
+ Examples:
932
+
933
+ ```python
934
+ >>> from transformers import AutoTokenizer, AutoModel
935
+ >>> import torch
936
+
937
+ >>> model = AutoModel.from_pretrained("google/siglip-base-patch16-224")
938
+ >>> tokenizer = AutoTokenizer.from_pretrained("google/siglip-base-patch16-224")
939
+
940
+ >>> # important: make sure to set padding="max_length" as that's how the model was trained
941
+ >>> inputs = tokenizer(["a photo of a cat", "a photo of a dog"], padding="max_length", return_tensors="pt")
942
+ >>> with torch.no_grad():
943
+ ... text_features = model.get_text_features(**inputs)
944
+ ```"""
945
+ # Use SigLIP model's config for some fields (if specified) instead of those of vision & text components.
946
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
947
+ output_hidden_states = (
948
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
949
+ )
950
+
951
+ text_outputs: BaseModelOutputWithPooling = self.text_model(
952
+ input_ids=input_ids,
953
+ attention_mask=attention_mask,
954
+ position_ids=position_ids,
955
+ output_attentions=output_attentions,
956
+ output_hidden_states=output_hidden_states,
957
+ )
958
+
959
+ pooled_output = text_outputs.pooler_output
960
+
961
+ return pooled_output
962
+
963
+ @auto_docstring
964
+ def get_image_features(
965
+ self,
966
+ pixel_values: Optional[torch.FloatTensor] = None,
967
+ output_attentions: Optional[bool] = None,
968
+ output_hidden_states: Optional[bool] = None,
969
+ interpolate_pos_encoding: bool = False,
970
+ ) -> torch.FloatTensor:
971
+ r"""
972
+ Returns:
973
+ image_features (`torch.FloatTensor` of shape `(batch_size, output_dim`): The image embeddings obtained by
974
+ applying the projection layer to the pooled output of [`SiglipVisionModel`].
975
+
976
+ Examples:
977
+
978
+ ```python
979
+ >>> from PIL import Image
980
+ >>> import requests
981
+ >>> from transformers import AutoProcessor, AutoModel
982
+ >>> import torch
983
+
984
+ >>> model = AutoModel.from_pretrained("google/siglip-base-patch16-224")
985
+ >>> processor = AutoProcessor.from_pretrained("google/siglip-base-patch16-224")
986
+
987
+ >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg"
988
+ >>> image = Image.open(requests.get(url, stream=True).raw)
989
+
990
+ >>> inputs = processor(images=image, return_tensors="pt")
991
+
992
+ >>> with torch.no_grad():
993
+ ... image_features = model.get_image_features(**inputs)
994
+ ```"""
995
+ # Use SiglipModel's config for some fields (if specified) instead of those of vision & text components.
996
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
997
+ output_hidden_states = (
998
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
999
+ )
1000
+
1001
+ vision_outputs: BaseModelOutputWithPooling = self.vision_model(
1002
+ pixel_values=pixel_values,
1003
+ output_attentions=output_attentions,
1004
+ output_hidden_states=output_hidden_states,
1005
+ interpolate_pos_encoding=interpolate_pos_encoding,
1006
+ )
1007
+
1008
+ pooled_output = vision_outputs.pooler_output
1009
+
1010
+ return pooled_output
1011
+
1012
+ @can_return_tuple
1013
+ @auto_docstring
1014
+ def forward(
1015
+ self,
1016
+ input_ids: Optional[torch.LongTensor] = None,
1017
+ pixel_values: Optional[torch.FloatTensor] = None,
1018
+ attention_mask: Optional[torch.Tensor] = None,
1019
+ position_ids: Optional[torch.LongTensor] = None,
1020
+ return_loss: Optional[bool] = None,
1021
+ output_attentions: Optional[bool] = None,
1022
+ output_hidden_states: Optional[bool] = None,
1023
+ interpolate_pos_encoding: bool = False,
1024
+ ) -> SiglipOutput:
1025
+ r"""
1026
+ return_loss (`bool`, *optional*):
1027
+ Whether or not to return the contrastive loss.
1028
+
1029
+ Examples:
1030
+
1031
+ ```python
1032
+ >>> from PIL import Image
1033
+ >>> import requests
1034
+ >>> from transformers import AutoProcessor, AutoModel
1035
+ >>> import torch
1036
+
1037
+ >>> model = AutoModel.from_pretrained("google/siglip-base-patch16-224")
1038
+ >>> processor = AutoProcessor.from_pretrained("google/siglip-base-patch16-224")
1039
+
1040
+ >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg"
1041
+ >>> image = Image.open(requests.get(url, stream=True).raw)
1042
+
1043
+ >>> texts = ["a photo of 2 cats", "a photo of 2 dogs"]
1044
+ >>> # important: we pass `padding=max_length` since the model was trained with this
1045
+ >>> inputs = processor(text=texts, images=image, padding="max_length", return_tensors="pt")
1046
+
1047
+ >>> with torch.no_grad():
1048
+ ... outputs = model(**inputs)
1049
+
1050
+ >>> logits_per_image = outputs.logits_per_image
1051
+ >>> probs = torch.sigmoid(logits_per_image) # these are the probabilities
1052
+ >>> print(f"{probs[0][0]:.1%} that image 0 is '{texts[0]}'")
1053
+ 31.9% that image 0 is 'a photo of 2 cats'
1054
+ ```"""
1055
+ # Use SigLIP model's config for some fields (if specified) instead of those of vision & text components.
1056
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
1057
+ output_hidden_states = (
1058
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
1059
+ )
1060
+
1061
+ vision_outputs: BaseModelOutputWithPooling = self.vision_model(
1062
+ pixel_values=pixel_values,
1063
+ output_attentions=output_attentions,
1064
+ output_hidden_states=output_hidden_states,
1065
+ interpolate_pos_encoding=interpolate_pos_encoding,
1066
+ )
1067
+
1068
+ text_outputs: BaseModelOutputWithPooling = self.text_model(
1069
+ input_ids=input_ids,
1070
+ attention_mask=attention_mask,
1071
+ position_ids=position_ids,
1072
+ output_attentions=output_attentions,
1073
+ output_hidden_states=output_hidden_states,
1074
+ )
1075
+
1076
+ image_embeds = vision_outputs.pooler_output
1077
+ text_embeds = text_outputs.pooler_output
1078
+
1079
+ # normalized features
1080
+ image_embeds = image_embeds / image_embeds.norm(p=2, dim=-1, keepdim=True)
1081
+ text_embeds = text_embeds / text_embeds.norm(p=2, dim=-1, keepdim=True)
1082
+
1083
+ # cosine similarity as logits
1084
+ logits_per_text = torch.matmul(text_embeds, image_embeds.t().to(text_embeds.device))
1085
+
1086
+ logit_scale, logit_bias = self.logit_scale.to(text_embeds.device), self.logit_bias.to(text_embeds.device)
1087
+ logits_per_text = logits_per_text * logit_scale.exp() + logit_bias
1088
+
1089
+ logits_per_image = logits_per_text.t()
1090
+
1091
+ loss = None
1092
+ if return_loss:
1093
+ # Adapted from https://github.com/google-research/big_vision/blob/01edb81a4716f93a48be43b3a4af14e29cdb3a7f/big_vision/trainers/proj/image_text/siglip.py#L287
1094
+ eye = torch.eye(logits_per_text.size(0), device=logits_per_text.device)
1095
+ m1_diag1 = -torch.ones_like(logits_per_text) + 2 * eye
1096
+ loglik = torch.nn.functional.logsigmoid(m1_diag1 * logits_per_text)
1097
+ nll = -torch.sum(loglik, dim=-1)
1098
+ loss = nll.mean()
1099
+
1100
+ return SiglipOutput(
1101
+ loss=loss,
1102
+ logits_per_image=logits_per_image,
1103
+ logits_per_text=logits_per_text,
1104
+ text_embeds=text_embeds,
1105
+ image_embeds=image_embeds,
1106
+ text_model_output=text_outputs,
1107
+ vision_model_output=vision_outputs,
1108
+ )
1109
+
1110
+
1111
+ @auto_docstring(
1112
+ custom_intro="""
1113
+ SigLIP vision encoder with an image classification head on top (a linear layer on top of the pooled final hidden states of
1114
+ the patch tokens) e.g. for ImageNet.
1115
+ """
1116
+ )
1117
+ class SiglipForImageClassification(SiglipPreTrainedModel):
1118
+ main_input_name = "pixel_values"
1119
+
1120
+ def __init__(self, config: SiglipConfig) -> None:
1121
+ super().__init__(config)
1122
+
1123
+ self.num_labels = config.num_labels
1124
+
1125
+ # Create the vision model with proper attention
1126
+ # and take only vision_model submodule (for backward compatibility)
1127
+ vision_model = SiglipVisionModel._from_config(config.vision_config)
1128
+ self.vision_model = vision_model.vision_model
1129
+
1130
+ # Classifier head
1131
+ self.classifier = (
1132
+ nn.Linear(config.vision_config.hidden_size, config.num_labels) if config.num_labels > 0 else nn.Identity()
1133
+ )
1134
+
1135
+ # Initialize weights and apply final processing
1136
+ self.post_init()
1137
+
1138
+ @can_return_tuple
1139
+ @auto_docstring
1140
+ def forward(
1141
+ self,
1142
+ pixel_values: Optional[torch.Tensor] = None,
1143
+ labels: Optional[torch.Tensor] = None,
1144
+ output_attentions: Optional[bool] = None,
1145
+ output_hidden_states: Optional[bool] = None,
1146
+ interpolate_pos_encoding: bool = False,
1147
+ ) -> ImageClassifierOutput:
1148
+ r"""
1149
+ labels (`torch.LongTensor` of shape `(batch_size,)`, *optional*):
1150
+ Labels for computing the image classification/regression loss. Indices should be in `[0, ...,
1151
+ config.num_labels - 1]`. If `config.num_labels == 1` a regression loss is computed (Mean-Square loss), If
1152
+ `config.num_labels > 1` a classification loss is computed (Cross-Entropy).
1153
+
1154
+ Examples:
1155
+
1156
+ ```python
1157
+ >>> from transformers import AutoImageProcessor, SiglipForImageClassification
1158
+ >>> import torch
1159
+ >>> from PIL import Image
1160
+ >>> import requests
1161
+
1162
+ >>> torch.manual_seed(3) # doctest: +IGNORE_RESULT
1163
+ >>> url = "http://images.cocodataset.org/val2017/000000039769.jpg"
1164
+ >>> image = Image.open(requests.get(url, stream=True).raw)
1165
+
1166
+ >>> # note: we are loading a `SiglipModel` from the hub here,
1167
+ >>> # so the head will be randomly initialized, hence the predictions will be random if seed is not set above.
1168
+ >>> image_processor = AutoImageProcessor.from_pretrained("google/siglip-base-patch16-224")
1169
+ >>> model = SiglipForImageClassification.from_pretrained("google/siglip-base-patch16-224")
1170
+
1171
+ >>> inputs = image_processor(images=image, return_tensors="pt")
1172
+ >>> outputs = model(**inputs)
1173
+ >>> logits = outputs.logits
1174
+ >>> # model predicts one of the two classes
1175
+ >>> predicted_class_idx = logits.argmax(-1).item()
1176
+ >>> print("Predicted class:", model.config.id2label[predicted_class_idx])
1177
+ Predicted class: LABEL_1
1178
+ ```"""
1179
+ output_attentions = output_attentions if output_attentions is not None else self.config.output_attentions
1180
+ output_hidden_states = (
1181
+ output_hidden_states if output_hidden_states is not None else self.config.output_hidden_states
1182
+ )
1183
+
1184
+ outputs: BaseModelOutputWithPooling = self.vision_model(
1185
+ pixel_values,
1186
+ output_attentions=output_attentions,
1187
+ output_hidden_states=output_hidden_states,
1188
+ interpolate_pos_encoding=interpolate_pos_encoding,
1189
+ )
1190
+
1191
+ sequence_output = outputs.last_hidden_state
1192
+
1193
+ # average pool the patch tokens
1194
+ sequence_output = torch.mean(sequence_output, dim=1)
1195
+ # apply classifier
1196
+ logits = self.classifier(sequence_output)
1197
+
1198
+ loss = None
1199
+ if labels is not None:
1200
+ # move labels to correct device to enable model parallelism
1201
+ labels = labels.to(logits.device)
1202
+ if self.config.problem_type is None:
1203
+ if self.num_labels == 1:
1204
+ self.config.problem_type = "regression"
1205
+ elif self.num_labels > 1 and (labels.dtype == torch.long or labels.dtype == torch.int):
1206
+ self.config.problem_type = "single_label_classification"
1207
+ else:
1208
+ self.config.problem_type = "multi_label_classification"
1209
+
1210
+ if self.config.problem_type == "regression":
1211
+ loss_fct = MSELoss()
1212
+ if self.num_labels == 1:
1213
+ loss = loss_fct(logits.squeeze(), labels.squeeze())
1214
+ else:
1215
+ loss = loss_fct(logits, labels)
1216
+ elif self.config.problem_type == "single_label_classification":
1217
+ loss_fct = CrossEntropyLoss()
1218
+ loss = loss_fct(logits.view(-1, self.num_labels), labels.view(-1))
1219
+ elif self.config.problem_type == "multi_label_classification":
1220
+ loss_fct = BCEWithLogitsLoss()
1221
+ loss = loss_fct(logits, labels)
1222
+
1223
+ return ImageClassifierOutput(
1224
+ loss=loss,
1225
+ logits=logits,
1226
+ hidden_states=outputs.hidden_states,
1227
+ attentions=outputs.attentions,
1228
+ )
1229
+
1230
+
1231
+ __all__ = [
1232
+ "SiglipModel",
1233
+ "SiglipPreTrainedModel",
1234
+ "SiglipTextModel",
1235
+ "SiglipVisionModel",
1236
+ "SiglipForImageClassification",
1237
+ ]
openpi_runtime/openpi/policies/aloha_policy.py ADDED
@@ -0,0 +1,202 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+ from typing import ClassVar
3
+
4
+ import einops
5
+ import numpy as np
6
+
7
+ from openpi import transforms
8
+
9
+
10
+ def make_aloha_example() -> dict:
11
+ """Creates a random input example for the Aloha policy."""
12
+ return {
13
+ "state": np.ones((14,)),
14
+ "images": {
15
+ "cam_high": np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),
16
+ "cam_low": np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),
17
+ "cam_left_wrist": np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),
18
+ "cam_right_wrist": np.random.randint(256, size=(3, 224, 224), dtype=np.uint8),
19
+ },
20
+ "prompt": "do something",
21
+ }
22
+
23
+
24
+ @dataclasses.dataclass(frozen=True)
25
+ class AlohaInputs(transforms.DataTransformFn):
26
+ """Inputs for the Aloha policy.
27
+
28
+ Expected inputs:
29
+ - images: dict[name, img] where img is [channel, height, width]. name must be in EXPECTED_CAMERAS.
30
+ - state: [14]
31
+ - actions: [action_horizon, 14]
32
+ """
33
+
34
+ # If true, this will convert the joint and gripper values from the standard Aloha space to
35
+ # the space used by the pi internal runtime which was used to train the base model.
36
+ adapt_to_pi: bool = True
37
+
38
+ # The expected cameras names. All input cameras must be in this set. Missing cameras will be
39
+ # replaced with black images and the corresponding `image_mask` will be set to False.
40
+ EXPECTED_CAMERAS: ClassVar[tuple[str, ...]] = ("cam_high", "cam_low", "cam_left_wrist", "cam_right_wrist")
41
+
42
+ def __call__(self, data: dict) -> dict:
43
+ data = _decode_aloha(data, adapt_to_pi=self.adapt_to_pi)
44
+
45
+ in_images = data["images"]
46
+ if set(in_images) - set(self.EXPECTED_CAMERAS):
47
+ raise ValueError(f"Expected images to contain {self.EXPECTED_CAMERAS}, got {tuple(in_images)}")
48
+
49
+ # Assume that base image always exists.
50
+ base_image = in_images["cam_high"]
51
+
52
+ images = {
53
+ "base_0_rgb": base_image,
54
+ }
55
+ image_masks = {
56
+ "base_0_rgb": np.True_,
57
+ }
58
+
59
+ # Add the extra images.
60
+ extra_image_names = {
61
+ "left_wrist_0_rgb": "cam_left_wrist",
62
+ "right_wrist_0_rgb": "cam_right_wrist",
63
+ }
64
+ for dest, source in extra_image_names.items():
65
+ if source in in_images:
66
+ images[dest] = in_images[source]
67
+ image_masks[dest] = np.True_
68
+ else:
69
+ images[dest] = np.zeros_like(base_image)
70
+ image_masks[dest] = np.False_
71
+
72
+ inputs = {
73
+ "image": images,
74
+ "image_mask": image_masks,
75
+ "state": data["state"],
76
+ }
77
+
78
+ # Actions are only available during training.
79
+ if "actions" in data:
80
+ actions = np.asarray(data["actions"])
81
+ actions = _encode_actions_inv(actions, adapt_to_pi=self.adapt_to_pi)
82
+ inputs["actions"] = actions
83
+
84
+ if "prompt" in data:
85
+ inputs["prompt"] = data["prompt"]
86
+
87
+ return inputs
88
+
89
+
90
+ @dataclasses.dataclass(frozen=True)
91
+ class AlohaOutputs(transforms.DataTransformFn):
92
+ """Outputs for the Aloha policy."""
93
+
94
+ # If true, this will convert the joint and gripper values from the standard Aloha space to
95
+ # the space used by the pi internal runtime which was used to train the base model.
96
+ adapt_to_pi: bool = True
97
+
98
+ def __call__(self, data: dict) -> dict:
99
+ # Only return the first 14 dims.
100
+ actions = np.asarray(data["actions"][:, :14])
101
+ return {"actions": _encode_actions(actions, adapt_to_pi=self.adapt_to_pi)}
102
+
103
+
104
+ def _joint_flip_mask() -> np.ndarray:
105
+ """Used to convert between aloha and pi joint angles."""
106
+ return np.array([1, -1, -1, 1, 1, 1, 1, 1, -1, -1, 1, 1, 1, 1])
107
+
108
+
109
+ def _normalize(x, min_val, max_val):
110
+ return (x - min_val) / (max_val - min_val)
111
+
112
+
113
+ def _unnormalize(x, min_val, max_val):
114
+ return x * (max_val - min_val) + min_val
115
+
116
+
117
+ def _gripper_to_angular(value):
118
+ # Aloha transforms the gripper positions into a linear space. The following code
119
+ # reverses this transformation to be consistent with pi0 which is pretrained in
120
+ # angular space.
121
+ #
122
+ # These values are coming from the Aloha code:
123
+ # PUPPET_GRIPPER_POSITION_OPEN, PUPPET_GRIPPER_POSITION_CLOSED
124
+ value = _unnormalize(value, min_val=0.01844, max_val=0.05800)
125
+
126
+ # This is the inverse of the angular to linear transformation inside the Interbotix code.
127
+ def linear_to_radian(linear_position, arm_length, horn_radius):
128
+ value = (horn_radius**2 + linear_position**2 - arm_length**2) / (2 * horn_radius * linear_position)
129
+ return np.arcsin(np.clip(value, -1.0, 1.0))
130
+
131
+ # The constants are taken from the Interbotix code.
132
+ value = linear_to_radian(value, arm_length=0.036, horn_radius=0.022)
133
+
134
+ # pi0 gripper data is normalized (0, 1) between encoder counts (2405, 3110).
135
+ # There are 4096 total encoder counts and aloha uses a zero of 2048.
136
+ # Converting this to radians means that the normalized inputs are between (0.5476, 1.6296)
137
+ return _normalize(value, min_val=0.5476, max_val=1.6296)
138
+
139
+
140
+ def _gripper_from_angular(value):
141
+ # Convert from the gripper position used by pi0 to the gripper position that is used by Aloha.
142
+ # Note that the units are still angular but the range is different.
143
+
144
+ # We do not scale the output since the trossen model predictions are already in radians.
145
+ # See the comment in _gripper_to_angular for a derivation of the constant
146
+ value = value + 0.5476
147
+
148
+ # These values are coming from the Aloha code:
149
+ # PUPPET_GRIPPER_JOINT_OPEN, PUPPET_GRIPPER_JOINT_CLOSE
150
+ return _normalize(value, min_val=-0.6213, max_val=1.4910)
151
+
152
+
153
+ def _gripper_from_angular_inv(value):
154
+ # Directly inverts the gripper_from_angular function.
155
+ value = _unnormalize(value, min_val=-0.6213, max_val=1.4910)
156
+ return value - 0.5476
157
+
158
+
159
+ def _decode_aloha(data: dict, *, adapt_to_pi: bool = False) -> dict:
160
+ # state is [left_arm_joint_angles, left_arm_gripper, right_arm_joint_angles, right_arm_gripper]
161
+ # dim sizes: [6, 1, 6, 1]
162
+ state = np.asarray(data["state"])
163
+ state = _decode_state(state, adapt_to_pi=adapt_to_pi)
164
+
165
+ def convert_image(img):
166
+ img = np.asarray(img)
167
+ # Convert to uint8 if using float images.
168
+ if np.issubdtype(img.dtype, np.floating):
169
+ img = (255 * img).astype(np.uint8)
170
+ # Convert from [channel, height, width] to [height, width, channel].
171
+ return einops.rearrange(img, "c h w -> h w c")
172
+
173
+ images = data["images"]
174
+ images_dict = {name: convert_image(img) for name, img in images.items()}
175
+
176
+ data["images"] = images_dict
177
+ data["state"] = state
178
+ return data
179
+
180
+
181
+ def _decode_state(state: np.ndarray, *, adapt_to_pi: bool = False) -> np.ndarray:
182
+ if adapt_to_pi:
183
+ # Flip the joints.
184
+ state = _joint_flip_mask() * state
185
+ # Reverse the gripper transformation that is being applied by the Aloha runtime.
186
+ state[[6, 13]] = _gripper_to_angular(state[[6, 13]])
187
+ return state
188
+
189
+
190
+ def _encode_actions(actions: np.ndarray, *, adapt_to_pi: bool = False) -> np.ndarray:
191
+ if adapt_to_pi:
192
+ # Flip the joints.
193
+ actions = _joint_flip_mask() * actions
194
+ actions[:, [6, 13]] = _gripper_from_angular(actions[:, [6, 13]])
195
+ return actions
196
+
197
+
198
+ def _encode_actions_inv(actions: np.ndarray, *, adapt_to_pi: bool = False) -> np.ndarray:
199
+ if adapt_to_pi:
200
+ actions = _joint_flip_mask() * actions
201
+ actions[:, [6, 13]] = _gripper_from_angular_inv(actions[:, [6, 13]])
202
+ return actions
openpi_runtime/openpi/policies/droid_policy.py ADDED
@@ -0,0 +1,81 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+
3
+ import einops
4
+ import numpy as np
5
+
6
+ from openpi import transforms
7
+ from openpi.models import model as _model
8
+
9
+
10
+ def make_droid_example() -> dict:
11
+ """Creates a random input example for the Droid policy."""
12
+ return {
13
+ "observation/exterior_image_1_left": np.random.randint(256, size=(224, 224, 3), dtype=np.uint8),
14
+ "observation/wrist_image_left": np.random.randint(256, size=(224, 224, 3), dtype=np.uint8),
15
+ "observation/joint_position": np.random.rand(7),
16
+ "observation/gripper_position": np.random.rand(1),
17
+ "prompt": "do something",
18
+ }
19
+
20
+
21
+ def _parse_image(image) -> np.ndarray:
22
+ image = np.asarray(image)
23
+ if np.issubdtype(image.dtype, np.floating):
24
+ image = (255 * image).astype(np.uint8)
25
+ if image.shape[0] == 3:
26
+ image = einops.rearrange(image, "c h w -> h w c")
27
+ return image
28
+
29
+
30
+ @dataclasses.dataclass(frozen=True)
31
+ class DroidInputs(transforms.DataTransformFn):
32
+ # Determines which model will be used.
33
+ model_type: _model.ModelType
34
+
35
+ def __call__(self, data: dict) -> dict:
36
+ gripper_pos = np.asarray(data["observation/gripper_position"])
37
+ if gripper_pos.ndim == 0:
38
+ # Ensure gripper position is a 1D array, not a scalar, so we can concatenate with joint positions
39
+ gripper_pos = gripper_pos[np.newaxis]
40
+ state = np.concatenate([data["observation/joint_position"], gripper_pos])
41
+
42
+ # Possibly need to parse images to uint8 (H,W,C) since LeRobot automatically
43
+ # stores as float32 (C,H,W), gets skipped for policy inference
44
+ base_image = _parse_image(data["observation/exterior_image_1_left"])
45
+ wrist_image = _parse_image(data["observation/wrist_image_left"])
46
+
47
+ match self.model_type:
48
+ case _model.ModelType.PI0 | _model.ModelType.PI05:
49
+ names = ("base_0_rgb", "left_wrist_0_rgb", "right_wrist_0_rgb")
50
+ images = (base_image, wrist_image, np.zeros_like(base_image))
51
+ image_masks = (np.True_, np.True_, np.False_)
52
+ case _model.ModelType.PI0_FAST:
53
+ names = ("base_0_rgb", "base_1_rgb", "wrist_0_rgb")
54
+ # We don't mask out padding images for FAST models.
55
+ images = (base_image, np.zeros_like(base_image), wrist_image)
56
+ image_masks = (np.True_, np.True_, np.True_)
57
+ case _:
58
+ raise ValueError(f"Unsupported model type: {self.model_type}")
59
+
60
+ inputs = {
61
+ "state": state,
62
+ "image": dict(zip(names, images, strict=True)),
63
+ "image_mask": dict(zip(names, image_masks, strict=True)),
64
+ }
65
+
66
+ if "actions" in data:
67
+ inputs["actions"] = np.asarray(data["actions"])
68
+
69
+ if "prompt" in data:
70
+ if isinstance(data["prompt"], bytes):
71
+ data["prompt"] = data["prompt"].decode("utf-8")
72
+ inputs["prompt"] = data["prompt"]
73
+
74
+ return inputs
75
+
76
+
77
+ @dataclasses.dataclass(frozen=True)
78
+ class DroidOutputs(transforms.DataTransformFn):
79
+ def __call__(self, data: dict) -> dict:
80
+ # Only return the first 8 dims.
81
+ return {"actions": np.asarray(data["actions"][..., :8])}
openpi_runtime/openpi/policies/libero_policy.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+
3
+ import einops
4
+ import numpy as np
5
+
6
+ from openpi import transforms
7
+ from openpi.models import model as _model
8
+
9
+
10
+ def make_libero_example() -> dict:
11
+ """Creates a random input example for the Libero policy."""
12
+ return {
13
+ "observation/state": np.random.rand(8),
14
+ "observation/image": np.random.randint(256, size=(224, 224, 3), dtype=np.uint8),
15
+ "observation/wrist_image": np.random.randint(256, size=(224, 224, 3), dtype=np.uint8),
16
+ "prompt": "do something",
17
+ }
18
+
19
+
20
+ def _parse_image(image) -> np.ndarray:
21
+ image = np.asarray(image)
22
+ if np.issubdtype(image.dtype, np.floating):
23
+ image = (255 * image).astype(np.uint8)
24
+ if image.shape[0] == 3:
25
+ image = einops.rearrange(image, "c h w -> h w c")
26
+ return image
27
+
28
+
29
+ @dataclasses.dataclass(frozen=True)
30
+ class LiberoInputs(transforms.DataTransformFn):
31
+ """
32
+ This class is used to convert inputs to the model to the expected format. It is used for both training and inference.
33
+
34
+ For your own dataset, you can copy this class and modify the keys based on the comments below to pipe
35
+ the correct elements of your dataset into the model.
36
+ """
37
+
38
+ # Determines which model will be used.
39
+ # Do not change this for your own dataset.
40
+ model_type: _model.ModelType
41
+
42
+ def __call__(self, data: dict) -> dict:
43
+ # Possibly need to parse images to uint8 (H,W,C) since LeRobot automatically
44
+ # stores as float32 (C,H,W), gets skipped for policy inference.
45
+ # Keep this for your own dataset, but if your dataset stores the images
46
+ # in a different key than "observation/image" or "observation/wrist_image",
47
+ # you should change it below.
48
+ # Pi0 models support three image inputs at the moment: one third-person view,
49
+ # and two wrist views (left and right). If your dataset does not have a particular type
50
+ # of image, e.g. wrist images, you can comment it out here and replace it with zeros like we do for the
51
+ # right wrist image below.
52
+ base_image = _parse_image(data["observation/image"])
53
+ wrist_image = _parse_image(data["observation/wrist_image"])
54
+
55
+ # Create inputs dict. Do not change the keys in the dict below.
56
+ inputs = {
57
+ "state": data["observation/state"],
58
+ "image": {
59
+ "base_0_rgb": base_image,
60
+ "left_wrist_0_rgb": wrist_image,
61
+ # Pad any non-existent images with zero-arrays of the appropriate shape.
62
+ "right_wrist_0_rgb": np.zeros_like(base_image),
63
+ },
64
+ "image_mask": {
65
+ "base_0_rgb": np.True_,
66
+ "left_wrist_0_rgb": np.True_,
67
+ # We only mask padding images for pi0 model, not pi0-FAST. Do not change this for your own dataset.
68
+ "right_wrist_0_rgb": np.True_ if self.model_type == _model.ModelType.PI0_FAST else np.False_,
69
+ },
70
+ }
71
+
72
+ # Pad actions to the model action dimension. Keep this for your own dataset.
73
+ # Actions are only available during training.
74
+ if "actions" in data:
75
+ inputs["actions"] = data["actions"]
76
+
77
+ # Pass the prompt (aka language instruction) to the model.
78
+ # Keep this for your own dataset (but modify the key if the instruction is not
79
+ # stored in "prompt"; the output dict always needs to have the key "prompt").
80
+ if "prompt" in data:
81
+ inputs["prompt"] = data["prompt"]
82
+
83
+ return inputs
84
+
85
+
86
+ @dataclasses.dataclass(frozen=True)
87
+ class LiberoOutputs(transforms.DataTransformFn):
88
+ """
89
+ This class is used to convert outputs from the model back the the dataset specific format. It is
90
+ used for inference only.
91
+
92
+ For your own dataset, you can copy this class and modify the action dimension based on the comments below.
93
+ """
94
+
95
+ def __call__(self, data: dict) -> dict:
96
+ # Only return the first N actions -- since we padded actions above to fit the model action
97
+ # dimension, we need to now parse out the correct number of actions in the return dict.
98
+ # For Libero, we only return the first 7 actions (since the rest is padding).
99
+ # For your own dataset, replace `7` with the action dimension of your dataset.
100
+ return {"actions": np.asarray(data["actions"][..., :7])}
openpi_runtime/openpi/policies/policy.py ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Sequence
2
+ import logging
3
+ import pathlib
4
+ import time
5
+ from typing import Any, TypeAlias
6
+
7
+ import flax
8
+ import flax.traverse_util
9
+ import jax
10
+ import jax.numpy as jnp
11
+ import numpy as np
12
+ from openpi_client import base_policy as _base_policy
13
+ import torch
14
+ from typing_extensions import override
15
+
16
+ from openpi import transforms as _transforms
17
+ from openpi.models import model as _model
18
+ from openpi.shared import array_typing as at
19
+ from openpi.shared import nnx_utils
20
+
21
+ BasePolicy: TypeAlias = _base_policy.BasePolicy
22
+
23
+
24
+ class Policy(BasePolicy):
25
+ def __init__(
26
+ self,
27
+ model: _model.BaseModel,
28
+ *,
29
+ rng: at.KeyArrayLike | None = None,
30
+ transforms: Sequence[_transforms.DataTransformFn] = (),
31
+ output_transforms: Sequence[_transforms.DataTransformFn] = (),
32
+ sample_kwargs: dict[str, Any] | None = None,
33
+ metadata: dict[str, Any] | None = None,
34
+ pytorch_device: str = "cpu",
35
+ is_pytorch: bool = False,
36
+ ):
37
+ """Initialize the Policy.
38
+
39
+ Args:
40
+ model: The model to use for action sampling.
41
+ rng: Random number generator key for JAX models. Ignored for PyTorch models.
42
+ transforms: Input data transformations to apply before inference.
43
+ output_transforms: Output data transformations to apply after inference.
44
+ sample_kwargs: Additional keyword arguments to pass to model.sample_actions.
45
+ metadata: Additional metadata to store with the policy.
46
+ pytorch_device: Device to use for PyTorch models (e.g., "cpu", "cuda:0").
47
+ Only relevant when is_pytorch=True.
48
+ is_pytorch: Whether the model is a PyTorch model. If False, assumes JAX model.
49
+ """
50
+ self._model = model
51
+ self._input_transform = _transforms.compose(transforms)
52
+ self._output_transform = _transforms.compose(output_transforms)
53
+ self._sample_kwargs = sample_kwargs or {}
54
+ self._metadata = metadata or {}
55
+ self._is_pytorch_model = is_pytorch
56
+ self._pytorch_device = pytorch_device
57
+
58
+ if self._is_pytorch_model:
59
+ self._model = self._model.to(pytorch_device)
60
+ self._model.eval()
61
+ self._sample_actions = model.sample_actions
62
+ else:
63
+ # JAX model setup
64
+ self._sample_actions = nnx_utils.module_jit(model.sample_actions)
65
+ self._rng = rng or jax.random.key(0)
66
+
67
+ @override
68
+ def infer(self, obs: dict, *, noise: np.ndarray | None = None) -> dict: # type: ignore[misc]
69
+ # Make a copy since transformations may modify the inputs in place.
70
+ inputs = jax.tree.map(lambda x: x, obs)
71
+ inputs = self._input_transform(inputs)
72
+ if not self._is_pytorch_model:
73
+ # Make a batch and convert to jax.Array.
74
+ inputs = jax.tree.map(lambda x: jnp.asarray(x)[np.newaxis, ...], inputs)
75
+ self._rng, sample_rng_or_pytorch_device = jax.random.split(self._rng)
76
+ else:
77
+ # Convert inputs to PyTorch tensors and move to correct device
78
+ inputs = jax.tree.map(lambda x: torch.from_numpy(np.array(x)).to(self._pytorch_device)[None, ...], inputs)
79
+ sample_rng_or_pytorch_device = self._pytorch_device
80
+
81
+ # Prepare kwargs for sample_actions
82
+ sample_kwargs = dict(self._sample_kwargs)
83
+ if noise is not None:
84
+ noise = torch.from_numpy(noise).to(self._pytorch_device) if self._is_pytorch_model else jnp.asarray(noise)
85
+
86
+ if noise.ndim == 2: # If noise is (action_horizon, action_dim), add batch dimension
87
+ noise = noise[None, ...] # Make it (1, action_horizon, action_dim)
88
+ sample_kwargs["noise"] = noise
89
+
90
+ observation = _model.Observation.from_dict(inputs)
91
+ start_time = time.monotonic()
92
+ outputs = {
93
+ "state": inputs["state"],
94
+ "actions": self._sample_actions(sample_rng_or_pytorch_device, observation, **sample_kwargs),
95
+ }
96
+ model_time = time.monotonic() - start_time
97
+ if self._is_pytorch_model:
98
+ outputs = jax.tree.map(lambda x: np.asarray(x[0, ...].detach().cpu()), outputs)
99
+ else:
100
+ outputs = jax.tree.map(lambda x: np.asarray(x[0, ...]), outputs)
101
+
102
+ outputs = self._output_transform(outputs)
103
+ outputs["policy_timing"] = {
104
+ "infer_ms": model_time * 1000,
105
+ }
106
+ return outputs
107
+
108
+ @property
109
+ def metadata(self) -> dict[str, Any]:
110
+ return self._metadata
111
+
112
+
113
+ class PolicyRecorder(_base_policy.BasePolicy):
114
+ """Records the policy's behavior to disk."""
115
+
116
+ def __init__(self, policy: _base_policy.BasePolicy, record_dir: str):
117
+ self._policy = policy
118
+
119
+ logging.info(f"Dumping policy records to: {record_dir}")
120
+ self._record_dir = pathlib.Path(record_dir)
121
+ self._record_dir.mkdir(parents=True, exist_ok=True)
122
+ self._record_step = 0
123
+
124
+ @override
125
+ def infer(self, obs: dict) -> dict: # type: ignore[misc]
126
+ results = self._policy.infer(obs)
127
+
128
+ data = {"inputs": obs, "outputs": results}
129
+ data = flax.traverse_util.flatten_dict(data, sep="/")
130
+
131
+ output_path = self._record_dir / f"step_{self._record_step}"
132
+ self._record_step += 1
133
+
134
+ np.save(output_path, np.asarray(data))
135
+ return results
openpi_runtime/openpi/policies/policy_config.py ADDED
@@ -0,0 +1,94 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ import os
3
+ import pathlib
4
+ from typing import Any
5
+
6
+ import jax.numpy as jnp
7
+
8
+ import openpi.models.model as _model
9
+ import openpi.policies.policy as _policy
10
+ import openpi.shared.download as download
11
+ from openpi.training import checkpoints as _checkpoints
12
+ from openpi.training import config as _config
13
+ import openpi.transforms as transforms
14
+
15
+
16
+ def create_trained_policy(
17
+ train_config: _config.TrainConfig,
18
+ checkpoint_dir: pathlib.Path | str,
19
+ *,
20
+ repack_transforms: transforms.Group | None = None,
21
+ sample_kwargs: dict[str, Any] | None = None,
22
+ default_prompt: str | None = None,
23
+ norm_stats: dict[str, transforms.NormStats] | None = None,
24
+ pytorch_device: str | None = None,
25
+ ) -> _policy.Policy:
26
+ """Create a policy from a trained checkpoint.
27
+
28
+ Args:
29
+ train_config: The training config to use to create the model.
30
+ checkpoint_dir: The directory to load the model from.
31
+ repack_transforms: Optional transforms that will be applied before any other transforms.
32
+ sample_kwargs: The kwargs to pass to the `sample_actions` method. If not provided, the default
33
+ kwargs will be used.
34
+ default_prompt: The default prompt to use for the policy. Will inject the prompt into the input
35
+ data if it doesn't already exist.
36
+ norm_stats: The norm stats to use for the policy. If not provided, the norm stats will be loaded
37
+ from the checkpoint directory.
38
+ pytorch_device: Device to use for PyTorch models (e.g., "cpu", "cuda", "cuda:0").
39
+ If None and is_pytorch=True, will use "cuda" if available, otherwise "cpu".
40
+
41
+ Note:
42
+ The function automatically detects whether the model is PyTorch-based by checking for the
43
+ presence of "model.safensors" in the checkpoint directory.
44
+ """
45
+ repack_transforms = repack_transforms or transforms.Group()
46
+ checkpoint_dir = download.maybe_download(str(checkpoint_dir))
47
+
48
+ # Check if this is a PyTorch model by looking for model.safetensors
49
+ weight_path = os.path.join(checkpoint_dir, "model.safetensors")
50
+ is_pytorch = os.path.exists(weight_path)
51
+
52
+ logging.info("Loading model...")
53
+ if is_pytorch:
54
+ model = train_config.model.load_pytorch(train_config, weight_path)
55
+ model.paligemma_with_expert.to_bfloat16_for_selected_params("bfloat16")
56
+ else:
57
+ model = train_config.model.load(_model.restore_params(checkpoint_dir / "params", dtype=jnp.bfloat16))
58
+ data_config = train_config.data.create(train_config.assets_dirs, train_config.model)
59
+ if norm_stats is None:
60
+ # We are loading the norm stats from the checkpoint instead of the config assets dir to make sure
61
+ # that the policy is using the same normalization stats as the original training process.
62
+ if data_config.asset_id is None:
63
+ raise ValueError("Asset id is required to load norm stats.")
64
+ norm_stats = _checkpoints.load_norm_stats(checkpoint_dir / "assets", data_config.asset_id)
65
+
66
+ # Determine the device to use for PyTorch models
67
+ if is_pytorch and pytorch_device is None:
68
+ try:
69
+ import torch
70
+
71
+ pytorch_device = "cuda" if torch.cuda.is_available() else "cpu"
72
+ except ImportError:
73
+ pytorch_device = "cpu"
74
+
75
+ return _policy.Policy(
76
+ model,
77
+ transforms=[
78
+ *repack_transforms.inputs,
79
+ transforms.InjectDefaultPrompt(default_prompt),
80
+ *data_config.data_transforms.inputs,
81
+ transforms.Normalize(norm_stats, use_quantiles=data_config.use_quantile_norm),
82
+ *data_config.model_transforms.inputs,
83
+ ],
84
+ output_transforms=[
85
+ *data_config.model_transforms.outputs,
86
+ transforms.Unnormalize(norm_stats, use_quantiles=data_config.use_quantile_norm),
87
+ *data_config.data_transforms.outputs,
88
+ *repack_transforms.outputs,
89
+ ],
90
+ sample_kwargs=sample_kwargs,
91
+ metadata=train_config.policy_metadata,
92
+ is_pytorch=is_pytorch,
93
+ pytorch_device=pytorch_device if is_pytorch else None,
94
+ )
openpi_runtime/openpi/policies/ur_policy.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+
3
+ import einops
4
+ import numpy as np
5
+
6
+ from openpi import transforms
7
+ from openpi.models import model as _model
8
+
9
+
10
+ def _parse_image(image: np.ndarray) -> np.ndarray:
11
+ image = np.asarray(image)
12
+ if np.issubdtype(image.dtype, np.floating):
13
+ image = (255 * image).astype(np.uint8)
14
+ if image.shape[0] == 3:
15
+ image = einops.rearrange(image, "c h w -> h w c")
16
+ return image
17
+
18
+
19
+ @dataclasses.dataclass(frozen=True)
20
+ class URInputs(transforms.DataTransformFn):
21
+ """Map UR TCP observations and delta actions to OpenPI model inputs."""
22
+
23
+ model_type: _model.ModelType
24
+
25
+ def __call__(self, data: dict) -> dict:
26
+ base_image = _parse_image(data["observation/image"])
27
+ wrist_image = _parse_image(data["observation/wrist_image"])
28
+
29
+ inputs = {
30
+ "state": np.asarray(data["observation/state"]),
31
+ "image": {
32
+ "base_0_rgb": base_image,
33
+ "left_wrist_0_rgb": wrist_image,
34
+ "right_wrist_0_rgb": np.zeros_like(base_image),
35
+ },
36
+ "image_mask": {
37
+ "base_0_rgb": np.True_,
38
+ "left_wrist_0_rgb": np.True_,
39
+ "right_wrist_0_rgb": np.True_
40
+ if self.model_type == _model.ModelType.PI0_FAST
41
+ else np.False_,
42
+ },
43
+ }
44
+ if "actions" in data:
45
+ inputs["actions"] = np.asarray(data["actions"])
46
+ if "prompt" in data:
47
+ inputs["prompt"] = data["prompt"]
48
+ return inputs
49
+
50
+
51
+ @dataclasses.dataclass(frozen=True)
52
+ class UROutputs(transforms.DataTransformFn):
53
+ """Return the six TCP delta dimensions and absolute gripper command."""
54
+
55
+ def __call__(self, data: dict) -> dict:
56
+ return {"actions": np.asarray(data["actions"])[..., :7]}
openpi_runtime/openpi/py.typed ADDED
File without changes
openpi_runtime/openpi/serving/websocket_policy_server.py ADDED
@@ -0,0 +1,90 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import http
3
+ import logging
4
+ import time
5
+ import traceback
6
+
7
+ from openpi_client import base_policy as _base_policy
8
+ from openpi_client import msgpack_numpy
9
+ import websockets.asyncio.server as _server
10
+ import websockets.frames
11
+
12
+ logger = logging.getLogger(__name__)
13
+
14
+
15
+ class WebsocketPolicyServer:
16
+ """Serves a policy using the websocket protocol. See websocket_client_policy.py for a client implementation.
17
+
18
+ Currently only implements the `load` and `infer` methods.
19
+ """
20
+
21
+ def __init__(
22
+ self,
23
+ policy: _base_policy.BasePolicy,
24
+ host: str = "0.0.0.0",
25
+ port: int | None = None,
26
+ metadata: dict | None = None,
27
+ ) -> None:
28
+ self._policy = policy
29
+ self._host = host
30
+ self._port = port
31
+ self._metadata = metadata or {}
32
+ logging.getLogger("websockets.server").setLevel(logging.INFO)
33
+
34
+ def serve_forever(self) -> None:
35
+ asyncio.run(self.run())
36
+
37
+ async def run(self):
38
+ async with _server.serve(
39
+ self._handler,
40
+ self._host,
41
+ self._port,
42
+ compression=None,
43
+ max_size=None,
44
+ process_request=_health_check,
45
+ ) as server:
46
+ await server.serve_forever()
47
+
48
+ async def _handler(self, websocket: _server.ServerConnection):
49
+ logger.info(f"Connection from {websocket.remote_address} opened")
50
+ packer = msgpack_numpy.Packer()
51
+
52
+ await websocket.send(packer.pack(self._metadata))
53
+
54
+ prev_total_time = None
55
+ while True:
56
+ try:
57
+ start_time = time.monotonic()
58
+ obs = msgpack_numpy.unpackb(await websocket.recv())
59
+
60
+ infer_time = time.monotonic()
61
+ action = self._policy.infer(obs)
62
+ infer_time = time.monotonic() - infer_time
63
+
64
+ action["server_timing"] = {
65
+ "infer_ms": infer_time * 1000,
66
+ }
67
+ if prev_total_time is not None:
68
+ # We can only record the last total time since we also want to include the send time.
69
+ action["server_timing"]["prev_total_ms"] = prev_total_time * 1000
70
+
71
+ await websocket.send(packer.pack(action))
72
+ prev_total_time = time.monotonic() - start_time
73
+
74
+ except websockets.ConnectionClosed:
75
+ logger.info(f"Connection from {websocket.remote_address} closed")
76
+ break
77
+ except Exception:
78
+ await websocket.send(traceback.format_exc())
79
+ await websocket.close(
80
+ code=websockets.frames.CloseCode.INTERNAL_ERROR,
81
+ reason="Internal server error. Traceback included in previous frame.",
82
+ )
83
+ raise
84
+
85
+
86
+ def _health_check(connection: _server.ServerConnection, request: _server.Request) -> _server.Response | None:
87
+ if request.path == "/healthz":
88
+ return connection.respond(http.HTTPStatus.OK, "OK\n")
89
+ # Continue with the normal request handling.
90
+ return None
openpi_runtime/openpi/shared/__init__.py ADDED
File without changes
openpi_runtime/openpi/shared/array_typing.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import functools as ft
3
+ import inspect
4
+ from typing import TypeAlias, TypeVar, cast
5
+
6
+ import beartype
7
+ import jax
8
+ import jax._src.tree_util as private_tree_util
9
+ import jax.core
10
+ from jaxtyping import ArrayLike
11
+ from jaxtyping import Bool # noqa: F401
12
+ from jaxtyping import DTypeLike # noqa: F401
13
+ from jaxtyping import Float
14
+ from jaxtyping import Int # noqa: F401
15
+ from jaxtyping import Key # noqa: F401
16
+ from jaxtyping import Num # noqa: F401
17
+ from jaxtyping import PyTree
18
+ from jaxtyping import Real # noqa: F401
19
+ from jaxtyping import UInt8 # noqa: F401
20
+ from jaxtyping import config
21
+ from jaxtyping import jaxtyped
22
+ import jaxtyping._decorator
23
+ import torch
24
+
25
+ # patch jaxtyping to handle https://github.com/patrick-kidger/jaxtyping/issues/277.
26
+ # the problem is that custom PyTree nodes are sometimes initialized with arbitrary types (e.g., `jax.ShapeDtypeStruct`,
27
+ # `jax.Sharding`, or even <object>) due to JAX tracing operations. this patch skips typechecking when the stack trace
28
+ # contains `jax._src.tree_util`, which should only be the case during tree unflattening.
29
+ _original_check_dataclass_annotations = jaxtyping._decorator._check_dataclass_annotations # noqa: SLF001
30
+ # Redefine Array to include both JAX arrays and PyTorch tensors
31
+ Array = jax.Array | torch.Tensor
32
+
33
+
34
+ def _check_dataclass_annotations(self, typechecker):
35
+ if not any(
36
+ frame.frame.f_globals.get("__name__") in {"jax._src.tree_util", "flax.nnx.transforms.compilation"}
37
+ for frame in inspect.stack()
38
+ ):
39
+ return _original_check_dataclass_annotations(self, typechecker)
40
+ return None
41
+
42
+
43
+ jaxtyping._decorator._check_dataclass_annotations = _check_dataclass_annotations # noqa: SLF001
44
+
45
+ KeyArrayLike: TypeAlias = jax.typing.ArrayLike
46
+ Params: TypeAlias = PyTree[Float[ArrayLike, "..."]]
47
+
48
+ T = TypeVar("T")
49
+
50
+
51
+ # runtime type-checking decorator
52
+ def typecheck(t: T) -> T:
53
+ return cast(T, ft.partial(jaxtyped, typechecker=beartype.beartype)(t))
54
+
55
+
56
+ @contextlib.contextmanager
57
+ def disable_typechecking():
58
+ initial = config.jaxtyping_disable
59
+ config.update("jaxtyping_disable", True) # noqa: FBT003
60
+ yield
61
+ config.update("jaxtyping_disable", initial)
62
+
63
+
64
+ def check_pytree_equality(*, expected: PyTree, got: PyTree, check_shapes: bool = False, check_dtypes: bool = False):
65
+ """Checks that two PyTrees have the same structure and optionally checks shapes and dtypes. Creates a much nicer
66
+ error message than if `jax.tree.map` is naively used on PyTrees with different structures.
67
+ """
68
+
69
+ if errors := list(private_tree_util.equality_errors(expected, got)):
70
+ raise ValueError(
71
+ "PyTrees have different structure:\n"
72
+ + (
73
+ "\n".join(
74
+ f" - at keypath '{jax.tree_util.keystr(path)}': expected {thing1}, got {thing2}, so {explanation}.\n"
75
+ for path, thing1, thing2, explanation in errors
76
+ )
77
+ )
78
+ )
79
+
80
+ if check_shapes or check_dtypes:
81
+
82
+ def check(kp, x, y):
83
+ if check_shapes and x.shape != y.shape:
84
+ raise ValueError(f"Shape mismatch at {jax.tree_util.keystr(kp)}: expected {x.shape}, got {y.shape}")
85
+
86
+ if check_dtypes and x.dtype != y.dtype:
87
+ raise ValueError(f"Dtype mismatch at {jax.tree_util.keystr(kp)}: expected {x.dtype}, got {y.dtype}")
88
+
89
+ jax.tree_util.tree_map_with_path(check, expected, got)
openpi_runtime/openpi/shared/download.py ADDED
@@ -0,0 +1,216 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import concurrent.futures
2
+ import datetime
3
+ import logging
4
+ import os
5
+ import pathlib
6
+ import re
7
+ import shutil
8
+ import stat
9
+ import subprocess
10
+ import time
11
+ import urllib.parse
12
+
13
+ import filelock
14
+ import fsspec
15
+ import fsspec.generic
16
+ import tqdm_loggable.auto as tqdm
17
+
18
+ # Environment variable to control cache directory path, ~/.cache/openpi will be used by default.
19
+ _OPENPI_DATA_HOME = "OPENPI_DATA_HOME"
20
+ DEFAULT_CACHE_DIR = "~/.cache/openpi"
21
+
22
+ logger = logging.getLogger(__name__)
23
+
24
+
25
+ def get_cache_dir() -> pathlib.Path:
26
+ cache_dir = pathlib.Path(os.getenv(_OPENPI_DATA_HOME, DEFAULT_CACHE_DIR)).expanduser().resolve()
27
+ cache_dir.mkdir(parents=True, exist_ok=True)
28
+ _set_folder_permission(cache_dir)
29
+ return cache_dir
30
+
31
+
32
+ def maybe_download(url: str, *, force_download: bool = False, **kwargs) -> pathlib.Path:
33
+ """Download a file or directory from a remote filesystem to the local cache, and return the local path.
34
+
35
+ If the local file already exists, it will be returned directly.
36
+
37
+ It is safe to call this function concurrently from multiple processes.
38
+ See `get_cache_dir` for more details on the cache directory.
39
+
40
+ Args:
41
+ url: URL to the file to download.
42
+ force_download: If True, the file will be downloaded even if it already exists in the cache.
43
+ **kwargs: Additional arguments to pass to fsspec.
44
+
45
+ Returns:
46
+ Local path to the downloaded file or directory. That path is guaranteed to exist and is absolute.
47
+ """
48
+ # Don't use fsspec to parse the url to avoid unnecessary connection to the remote filesystem.
49
+ parsed = urllib.parse.urlparse(url)
50
+
51
+ # Short circuit if this is a local path.
52
+ if parsed.scheme == "":
53
+ path = pathlib.Path(url)
54
+ if not path.exists():
55
+ raise FileNotFoundError(f"File not found at {url}")
56
+ return path.resolve()
57
+
58
+ cache_dir = get_cache_dir()
59
+
60
+ local_path = cache_dir / parsed.netloc / parsed.path.strip("/")
61
+ local_path = local_path.resolve()
62
+
63
+ # Check if the cache should be invalidated.
64
+ invalidate_cache = False
65
+ if local_path.exists():
66
+ if force_download or _should_invalidate_cache(cache_dir, local_path):
67
+ invalidate_cache = True
68
+ else:
69
+ return local_path
70
+
71
+ try:
72
+ lock_path = local_path.with_suffix(".lock")
73
+ with filelock.FileLock(lock_path):
74
+ # Ensure consistent permissions for the lock file.
75
+ _ensure_permissions(lock_path)
76
+ # First, remove the existing cache if it is expired.
77
+ if invalidate_cache:
78
+ logger.info(f"Removing expired cached entry: {local_path}")
79
+ if local_path.is_dir():
80
+ shutil.rmtree(local_path)
81
+ else:
82
+ local_path.unlink()
83
+
84
+ if not local_path.exists():
85
+ # Download the data to a local cache.
86
+ logger.info(f"Downloading {url} to {local_path}")
87
+ scratch_path = local_path.with_suffix(".partial")
88
+ # Route openpi-assets through gsutil to avoid gcsfs auth issues with this bucket.
89
+ # All other gs:// URLs (e.g. big_vision) continue to use gcsfs as normal.
90
+ if parsed.scheme == "gs" and parsed.netloc == "openpi-assets":
91
+ _download_gsutil(url, scratch_path, **kwargs)
92
+ else:
93
+ _download_fsspec(url, scratch_path, **kwargs)
94
+
95
+ shutil.move(scratch_path, local_path)
96
+ _ensure_permissions(local_path)
97
+
98
+ except PermissionError as e:
99
+ msg = (
100
+ f"Local file permission error was encountered while downloading {url}. "
101
+ f"Please try again after removing the cached data using: `rm -rf {local_path}*`"
102
+ )
103
+ raise PermissionError(msg) from e
104
+
105
+ return local_path
106
+
107
+
108
+ def _download_gsutil(url: str, local_path: pathlib.Path, **kwargs) -> None:
109
+ """Download a file or directory from GCS using gsutil if available, otherwise fall back to gcsfs."""
110
+ if shutil.which("gsutil") is None:
111
+ logger.warning(
112
+ "gsutil not found, falling back to gcsfs. This may fail if GCP credentials are not configured correctly."
113
+ )
114
+ _download_fsspec(url, local_path, **kwargs)
115
+ return
116
+ local_path.mkdir(parents=True, exist_ok=True)
117
+ subprocess.run(
118
+ ["gsutil", "-m", "cp", "-r", f"{url}/*", str(local_path)],
119
+ check=True,
120
+ )
121
+
122
+
123
+ def _download_fsspec(url: str, local_path: pathlib.Path, **kwargs) -> None:
124
+ """Download a file from a remote filesystem to the local cache, and return the local path."""
125
+ fs, _ = fsspec.core.url_to_fs(url, **kwargs)
126
+ info = fs.info(url)
127
+ # Folders are represented by 0-byte objects with a trailing forward slash.
128
+ if is_dir := (info["type"] == "directory" or (info["size"] == 0 and info["name"].endswith("/"))):
129
+ total_size = fs.du(url)
130
+ else:
131
+ total_size = info["size"]
132
+ with tqdm.tqdm(total=total_size, unit="iB", unit_scale=True, unit_divisor=1024) as pbar:
133
+ executor = concurrent.futures.ThreadPoolExecutor(max_workers=1)
134
+ future = executor.submit(fs.get, url, local_path, recursive=is_dir)
135
+ while not future.done():
136
+ current_size = sum(f.stat().st_size for f in [*local_path.rglob("*"), local_path] if f.is_file())
137
+ pbar.update(current_size - pbar.n)
138
+ time.sleep(1)
139
+ pbar.update(total_size - pbar.n)
140
+
141
+
142
+ def _set_permission(path: pathlib.Path, target_permission: int):
143
+ """chmod requires executable permission to be set, so we skip if the permission is already match with the target."""
144
+ if path.stat().st_mode & target_permission == target_permission:
145
+ logger.debug(f"Skipping {path} because it already has correct permissions")
146
+ return
147
+ path.chmod(target_permission)
148
+ logger.debug(f"Set {path} to {target_permission}")
149
+
150
+
151
+ def _set_folder_permission(folder_path: pathlib.Path) -> None:
152
+ """Set folder permission to be read, write and searchable."""
153
+ _set_permission(folder_path, stat.S_IRWXU | stat.S_IRWXG | stat.S_IRWXO)
154
+
155
+
156
+ def _ensure_permissions(path: pathlib.Path) -> None:
157
+ """Since we are sharing cache directory with containerized runtime as well as training script, we need to
158
+ ensure that the cache directory has the correct permissions.
159
+ """
160
+
161
+ def _setup_folder_permission_between_cache_dir_and_path(path: pathlib.Path) -> None:
162
+ cache_dir = get_cache_dir()
163
+ relative_path = path.relative_to(cache_dir)
164
+ moving_path = cache_dir
165
+ for part in relative_path.parts:
166
+ _set_folder_permission(moving_path / part)
167
+ moving_path = moving_path / part
168
+
169
+ def _set_file_permission(file_path: pathlib.Path) -> None:
170
+ """Set all files to be read & writable, if it is a script, keep it as a script."""
171
+ file_rw = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IWGRP | stat.S_IROTH | stat.S_IWOTH
172
+ if file_path.stat().st_mode & 0o100:
173
+ _set_permission(file_path, file_rw | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
174
+ else:
175
+ _set_permission(file_path, file_rw)
176
+
177
+ _setup_folder_permission_between_cache_dir_and_path(path)
178
+ for root, dirs, files in os.walk(str(path)):
179
+ root_path = pathlib.Path(root)
180
+ for file in files:
181
+ file_path = root_path / file
182
+ _set_file_permission(file_path)
183
+
184
+ for dir in dirs:
185
+ dir_path = root_path / dir
186
+ _set_folder_permission(dir_path)
187
+
188
+
189
+ def _get_mtime(year: int, month: int, day: int) -> float:
190
+ """Get the mtime of a given date at midnight UTC."""
191
+ date = datetime.datetime(year, month, day, tzinfo=datetime.UTC)
192
+ return time.mktime(date.timetuple())
193
+
194
+
195
+ # Map of relative paths, defined as regular expressions, to expiration timestamps (mtime format).
196
+ # Partial matching will be used from top to bottom and the first match will be chosen.
197
+ # Cached entries will be retained only if they are newer than the expiration timestamp.
198
+ _INVALIDATE_CACHE_DIRS: dict[re.Pattern, float] = {
199
+ re.compile("openpi-assets/checkpoints/pi0_aloha_pen_uncap"): _get_mtime(2025, 2, 17),
200
+ re.compile("openpi-assets/checkpoints/pi0_libero"): _get_mtime(2025, 2, 6),
201
+ re.compile("openpi-assets/checkpoints/"): _get_mtime(2025, 2, 3),
202
+ }
203
+
204
+
205
+ def _should_invalidate_cache(cache_dir: pathlib.Path, local_path: pathlib.Path) -> bool:
206
+ """Invalidate the cache if it is expired. Return True if the cache was invalidated."""
207
+
208
+ assert local_path.exists(), f"File not found at {local_path}"
209
+
210
+ relative_path = str(local_path.relative_to(cache_dir))
211
+ for pattern, expire_time in _INVALIDATE_CACHE_DIRS.items():
212
+ if pattern.match(relative_path):
213
+ # Remove if not newer than the expiration timestamp.
214
+ return local_path.stat().st_mtime <= expire_time
215
+
216
+ return False
openpi_runtime/openpi/shared/image_tools.py ADDED
@@ -0,0 +1,126 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import functools
2
+
3
+ import jax
4
+ import jax.numpy as jnp
5
+ import torch
6
+ import torch.nn.functional as F # noqa: N812
7
+
8
+ import openpi.shared.array_typing as at
9
+
10
+
11
+ @functools.partial(jax.jit, static_argnums=(1, 2, 3))
12
+ @at.typecheck
13
+ def resize_with_pad(
14
+ images: at.UInt8[at.Array, "*b h w c"] | at.Float[at.Array, "*b h w c"],
15
+ height: int,
16
+ width: int,
17
+ method: jax.image.ResizeMethod = jax.image.ResizeMethod.LINEAR,
18
+ ) -> at.UInt8[at.Array, "*b {height} {width} c"] | at.Float[at.Array, "*b {height} {width} c"]:
19
+ """Replicates tf.image.resize_with_pad. Resizes an image to a target height and width without distortion
20
+ by padding with black. If the image is float32, it must be in the range [-1, 1].
21
+ """
22
+ has_batch_dim = images.ndim == 4
23
+ if not has_batch_dim:
24
+ images = images[None] # type: ignore
25
+ cur_height, cur_width = images.shape[1:3]
26
+ ratio = max(cur_width / width, cur_height / height)
27
+ resized_height = int(cur_height / ratio)
28
+ resized_width = int(cur_width / ratio)
29
+ resized_images = jax.image.resize(
30
+ images, (images.shape[0], resized_height, resized_width, images.shape[3]), method=method
31
+ )
32
+ if images.dtype == jnp.uint8:
33
+ # round from float back to uint8
34
+ resized_images = jnp.round(resized_images).clip(0, 255).astype(jnp.uint8)
35
+ elif images.dtype == jnp.float32:
36
+ resized_images = resized_images.clip(-1.0, 1.0)
37
+ else:
38
+ raise ValueError(f"Unsupported image dtype: {images.dtype}")
39
+
40
+ pad_h0, remainder_h = divmod(height - resized_height, 2)
41
+ pad_h1 = pad_h0 + remainder_h
42
+ pad_w0, remainder_w = divmod(width - resized_width, 2)
43
+ pad_w1 = pad_w0 + remainder_w
44
+ padded_images = jnp.pad(
45
+ resized_images,
46
+ ((0, 0), (pad_h0, pad_h1), (pad_w0, pad_w1), (0, 0)),
47
+ constant_values=0 if images.dtype == jnp.uint8 else -1.0,
48
+ )
49
+
50
+ if not has_batch_dim:
51
+ padded_images = padded_images[0]
52
+ return padded_images
53
+
54
+
55
+ def resize_with_pad_torch(
56
+ images: torch.Tensor,
57
+ height: int,
58
+ width: int,
59
+ mode: str = "bilinear",
60
+ ) -> torch.Tensor:
61
+ """PyTorch version of resize_with_pad. Resizes an image to a target height and width without distortion
62
+ by padding with black. If the image is float32, it must be in the range [-1, 1].
63
+
64
+ Args:
65
+ images: Tensor of shape [*b, h, w, c] or [*b, c, h, w]
66
+ height: Target height
67
+ width: Target width
68
+ mode: Interpolation mode ('bilinear', 'nearest', etc.)
69
+
70
+ Returns:
71
+ Resized and padded tensor with same shape format as input
72
+ """
73
+ # Check if input is in channels-last format [*b, h, w, c] or channels-first [*b, c, h, w]
74
+ if images.shape[-1] <= 4: # Assume channels-last format
75
+ channels_last = True
76
+ # Convert to channels-first for torch operations
77
+ if images.dim() == 3:
78
+ images = images.unsqueeze(0) # Add batch dimension
79
+ images = images.permute(0, 3, 1, 2) # [b, h, w, c] -> [b, c, h, w]
80
+ else:
81
+ channels_last = False
82
+ if images.dim() == 3:
83
+ images = images.unsqueeze(0) # Add batch dimension
84
+
85
+ batch_size, channels, cur_height, cur_width = images.shape
86
+
87
+ # Calculate resize ratio
88
+ ratio = max(cur_width / width, cur_height / height)
89
+ resized_height = int(cur_height / ratio)
90
+ resized_width = int(cur_width / ratio)
91
+
92
+ # Resize
93
+ resized_images = F.interpolate(
94
+ images, size=(resized_height, resized_width), mode=mode, align_corners=False if mode == "bilinear" else None
95
+ )
96
+
97
+ # Handle dtype-specific clipping
98
+ if images.dtype == torch.uint8:
99
+ resized_images = torch.round(resized_images).clamp(0, 255).to(torch.uint8)
100
+ elif images.dtype == torch.float32:
101
+ resized_images = resized_images.clamp(-1.0, 1.0)
102
+ else:
103
+ raise ValueError(f"Unsupported image dtype: {images.dtype}")
104
+
105
+ # Calculate padding
106
+ pad_h0, remainder_h = divmod(height - resized_height, 2)
107
+ pad_h1 = pad_h0 + remainder_h
108
+ pad_w0, remainder_w = divmod(width - resized_width, 2)
109
+ pad_w1 = pad_w0 + remainder_w
110
+
111
+ # Pad
112
+ constant_value = 0 if images.dtype == torch.uint8 else -1.0
113
+ padded_images = F.pad(
114
+ resized_images,
115
+ (pad_w0, pad_w1, pad_h0, pad_h1), # left, right, top, bottom
116
+ mode="constant",
117
+ value=constant_value,
118
+ )
119
+
120
+ # Convert back to original format if needed
121
+ if channels_last:
122
+ padded_images = padded_images.permute(0, 2, 3, 1) # [b, c, h, w] -> [b, h, w, c]
123
+ if batch_size == 1 and images.shape[0] == 1:
124
+ padded_images = padded_images.squeeze(0) # Remove batch dimension if it was added
125
+
126
+ return padded_images
openpi_runtime/openpi/shared/nnx_utils.py ADDED
@@ -0,0 +1,69 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Callable
2
+ import dataclasses
3
+ import functools
4
+ import inspect
5
+ import re
6
+ from typing import Any, ParamSpec, TypeVar
7
+
8
+ import flax.nnx as nnx
9
+ import jax
10
+
11
+ P = ParamSpec("P")
12
+ R = TypeVar("R")
13
+
14
+
15
+ def module_jit(meth: Callable[P, R], *jit_args, **jit_kwargs) -> Callable[P, R]:
16
+ """A higher-order function to JIT-compile `nnx.Module` methods, freezing the module's state in the process.
17
+
18
+ Why not `nnx.jit`? For some reason, naively applying `nnx.jit` to `nnx.Module` methods, bound or unbound, uses much
19
+ more memory than necessary. I'm guessing it has something to do with the fact that it must keep track of module
20
+ mutations. Also, `nnx.jit` has some inherent overhead compared to a standard `jax.jit`, since every call must
21
+ traverse the NNX module graph. See https://github.com/google/flax/discussions/4224 for details.
22
+
23
+ `module_jit` is an alternative that avoids these issues by freezing the module's state. The function returned by
24
+ `module_jit` acts exactly like the original method, except that the state of the module is frozen to whatever it was
25
+ when `module_jit` was called. Mutations to the module within `meth` are still allowed, but they will be discarded
26
+ after the method call completes.
27
+ """
28
+ if not (inspect.ismethod(meth) and isinstance(meth.__self__, nnx.Module)):
29
+ raise ValueError("module_jit must only be used on bound methods of nnx.Modules.")
30
+
31
+ graphdef, state = nnx.split(meth.__self__)
32
+
33
+ def fun(state: nnx.State, *args: P.args, **kwargs: P.kwargs) -> R:
34
+ module = nnx.merge(graphdef, state)
35
+ return meth.__func__(module, *args, **kwargs)
36
+
37
+ jitted_fn = jax.jit(fun, *jit_args, **jit_kwargs)
38
+
39
+ @functools.wraps(meth)
40
+ def wrapper(*args: P.args, **kwargs: P.kwargs) -> R:
41
+ return jitted_fn(state, *args, **kwargs)
42
+
43
+ return wrapper
44
+
45
+
46
+ @dataclasses.dataclass(frozen=True)
47
+ class PathRegex:
48
+ """NNX Filter that matches paths using a regex.
49
+
50
+ By default, paths are joined with a `/` separator. This can be overridden by setting the `sep` argument.
51
+ """
52
+
53
+ pattern: str | re.Pattern
54
+ sep: str = "/"
55
+
56
+ def __post_init__(self):
57
+ if not isinstance(self.pattern, re.Pattern):
58
+ object.__setattr__(self, "pattern", re.compile(self.pattern))
59
+
60
+ def __call__(self, path: nnx.filterlib.PathParts, x: Any) -> bool:
61
+ joined_path = self.sep.join(str(x) for x in path)
62
+ assert isinstance(self.pattern, re.Pattern)
63
+ return self.pattern.fullmatch(joined_path) is not None
64
+
65
+
66
+ def state_map(state: nnx.State, filter: nnx.filterlib.Filter, fn: Callable[[Any], Any]) -> nnx.State:
67
+ """Apply a function to the leaves of the state that match the filter."""
68
+ filtered_keys = set(state.filter(filter).flat_state())
69
+ return state.map(lambda k, v: fn(v) if k in filtered_keys else v)
openpi_runtime/openpi/shared/normalize.py ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import pathlib
3
+
4
+ import numpy as np
5
+ import numpydantic
6
+ import pydantic
7
+
8
+
9
+ @pydantic.dataclasses.dataclass
10
+ class NormStats:
11
+ mean: numpydantic.NDArray
12
+ std: numpydantic.NDArray
13
+ q01: numpydantic.NDArray | None = None # 1st quantile
14
+ q99: numpydantic.NDArray | None = None # 99th quantile
15
+
16
+
17
+ class RunningStats:
18
+ """Compute running statistics of a batch of vectors."""
19
+
20
+ def __init__(self):
21
+ self._count = 0
22
+ self._mean = None
23
+ self._mean_of_squares = None
24
+ self._min = None
25
+ self._max = None
26
+ self._histograms = None
27
+ self._bin_edges = None
28
+ self._num_quantile_bins = 5000 # for computing quantiles on the fly
29
+
30
+ def update(self, batch: np.ndarray) -> None:
31
+ """
32
+ Update the running statistics with a batch of vectors.
33
+
34
+ Args:
35
+ vectors (np.ndarray): An array where all dimensions except the last are batch dimensions.
36
+ """
37
+ batch = batch.reshape(-1, batch.shape[-1])
38
+ num_elements, vector_length = batch.shape
39
+ if self._count == 0:
40
+ self._mean = np.mean(batch, axis=0)
41
+ self._mean_of_squares = np.mean(batch**2, axis=0)
42
+ self._min = np.min(batch, axis=0)
43
+ self._max = np.max(batch, axis=0)
44
+ self._histograms = [np.zeros(self._num_quantile_bins) for _ in range(vector_length)]
45
+ self._bin_edges = [
46
+ np.linspace(self._min[i] - 1e-10, self._max[i] + 1e-10, self._num_quantile_bins + 1)
47
+ for i in range(vector_length)
48
+ ]
49
+ else:
50
+ if vector_length != self._mean.size:
51
+ raise ValueError("The length of new vectors does not match the initialized vector length.")
52
+ new_max = np.max(batch, axis=0)
53
+ new_min = np.min(batch, axis=0)
54
+ max_changed = np.any(new_max > self._max)
55
+ min_changed = np.any(new_min < self._min)
56
+ self._max = np.maximum(self._max, new_max)
57
+ self._min = np.minimum(self._min, new_min)
58
+
59
+ if max_changed or min_changed:
60
+ self._adjust_histograms()
61
+
62
+ self._count += num_elements
63
+
64
+ batch_mean = np.mean(batch, axis=0)
65
+ batch_mean_of_squares = np.mean(batch**2, axis=0)
66
+
67
+ # Update running mean and mean of squares.
68
+ self._mean += (batch_mean - self._mean) * (num_elements / self._count)
69
+ self._mean_of_squares += (batch_mean_of_squares - self._mean_of_squares) * (num_elements / self._count)
70
+
71
+ self._update_histograms(batch)
72
+
73
+ def get_statistics(self) -> NormStats:
74
+ """
75
+ Compute and return the statistics of the vectors processed so far.
76
+
77
+ Returns:
78
+ dict: A dictionary containing the computed statistics.
79
+ """
80
+ if self._count < 2:
81
+ raise ValueError("Cannot compute statistics for less than 2 vectors.")
82
+
83
+ variance = self._mean_of_squares - self._mean**2
84
+ stddev = np.sqrt(np.maximum(0, variance))
85
+ q01, q99 = self._compute_quantiles([0.01, 0.99])
86
+ return NormStats(mean=self._mean, std=stddev, q01=q01, q99=q99)
87
+
88
+ def _adjust_histograms(self):
89
+ """Adjust histograms when min or max changes."""
90
+ for i in range(len(self._histograms)):
91
+ old_edges = self._bin_edges[i]
92
+ new_edges = np.linspace(self._min[i], self._max[i], self._num_quantile_bins + 1)
93
+
94
+ # Redistribute the existing histogram counts to the new bins
95
+ new_hist, _ = np.histogram(old_edges[:-1], bins=new_edges, weights=self._histograms[i])
96
+
97
+ self._histograms[i] = new_hist
98
+ self._bin_edges[i] = new_edges
99
+
100
+ def _update_histograms(self, batch: np.ndarray) -> None:
101
+ """Update histograms with new vectors."""
102
+ for i in range(batch.shape[1]):
103
+ hist, _ = np.histogram(batch[:, i], bins=self._bin_edges[i])
104
+ self._histograms[i] += hist
105
+
106
+ def _compute_quantiles(self, quantiles):
107
+ """Compute quantiles based on histograms."""
108
+ results = []
109
+ for q in quantiles:
110
+ target_count = q * self._count
111
+ q_values = []
112
+ for hist, edges in zip(self._histograms, self._bin_edges, strict=True):
113
+ cumsum = np.cumsum(hist)
114
+ idx = np.searchsorted(cumsum, target_count)
115
+ q_values.append(edges[idx])
116
+ results.append(np.array(q_values))
117
+ return results
118
+
119
+
120
+ class _NormStatsDict(pydantic.BaseModel):
121
+ norm_stats: dict[str, NormStats]
122
+
123
+
124
+ def serialize_json(norm_stats: dict[str, NormStats]) -> str:
125
+ """Serialize the running statistics to a JSON string."""
126
+ return _NormStatsDict(norm_stats=norm_stats).model_dump_json(indent=2)
127
+
128
+
129
+ def deserialize_json(data: str) -> dict[str, NormStats]:
130
+ """Deserialize the running statistics from a JSON string."""
131
+ return _NormStatsDict(**json.loads(data)).norm_stats
132
+
133
+
134
+ def save(directory: pathlib.Path | str, norm_stats: dict[str, NormStats]) -> None:
135
+ """Save the normalization stats to a directory."""
136
+ path = pathlib.Path(directory) / "norm_stats.json"
137
+ path.parent.mkdir(parents=True, exist_ok=True)
138
+ path.write_text(serialize_json(norm_stats))
139
+
140
+
141
+ def load(directory: pathlib.Path | str) -> dict[str, NormStats]:
142
+ """Load the normalization stats from a directory."""
143
+ path = pathlib.Path(directory) / "norm_stats.json"
144
+ if not path.exists():
145
+ raise FileNotFoundError(f"Norm stats file not found at: {path}")
146
+ return deserialize_json(path.read_text())
openpi_runtime/openpi/training/checkpoints.py ADDED
@@ -0,0 +1,159 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ import concurrent.futures as futures
5
+ import dataclasses
6
+ import logging
7
+ from typing import Protocol
8
+
9
+ from etils import epath
10
+ import jax
11
+ import orbax.checkpoint as ocp
12
+ import orbax.checkpoint.future as future
13
+
14
+ from openpi.shared import array_typing as at
15
+ import openpi.shared.normalize as _normalize
16
+ import openpi.training.data_loader as _data_loader
17
+ import openpi.training.utils as training_utils
18
+
19
+
20
+ def initialize_checkpoint_dir(
21
+ checkpoint_dir: epath.Path | str, *, keep_period: int | None, overwrite: bool, resume: bool
22
+ ) -> tuple[ocp.CheckpointManager, bool]:
23
+ checkpoint_dir = epath.Path(checkpoint_dir).resolve()
24
+ resuming = False
25
+ if checkpoint_dir.exists():
26
+ if overwrite:
27
+ checkpoint_dir.rmtree()
28
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
29
+ logging.info(f"Wiped checkpoint directory {checkpoint_dir}")
30
+ elif resume:
31
+ resuming = True
32
+ else:
33
+ raise FileExistsError(
34
+ f"Checkpoint directory {checkpoint_dir} already exists. Use --overwrite or --resume "
35
+ "to indicate how to handle it."
36
+ )
37
+
38
+ checkpoint_dir.mkdir(parents=True, exist_ok=True)
39
+
40
+ mngr = ocp.CheckpointManager(
41
+ checkpoint_dir,
42
+ item_handlers={
43
+ "assets": CallbackHandler(),
44
+ "train_state": ocp.PyTreeCheckpointHandler(),
45
+ "params": ocp.PyTreeCheckpointHandler(),
46
+ },
47
+ options=ocp.CheckpointManagerOptions(
48
+ max_to_keep=1,
49
+ keep_period=keep_period,
50
+ create=False,
51
+ async_options=ocp.AsyncOptions(timeout_secs=7200),
52
+ ),
53
+ )
54
+
55
+ # Special case: the checkpoint directory exists and the user requests to resume training, but the training run did
56
+ # not get to the first checkpoint saved. In this case, we don't actually want the train script to try and restore a
57
+ # checkpoint, since it will fail.
58
+ if resuming and tuple(mngr.all_steps()) in [(), (0,)]:
59
+ logging.info("Checkpoint directory exists, but does not contain any checkpoints. Aborting resume.")
60
+ resuming = False
61
+
62
+ return mngr, resuming
63
+
64
+
65
+ def save_state(
66
+ checkpoint_manager: ocp.CheckpointManager,
67
+ state: training_utils.TrainState,
68
+ data_loader: _data_loader.DataLoader,
69
+ step: int,
70
+ ):
71
+ def save_assets(directory: epath.Path):
72
+ # Save the normalization stats.
73
+ data_config = data_loader.data_config()
74
+ norm_stats = data_config.norm_stats
75
+ if norm_stats is not None and data_config.asset_id is not None:
76
+ _normalize.save(directory / data_config.asset_id, norm_stats)
77
+
78
+ # Split params that can be used for inference into a separate item.
79
+ with at.disable_typechecking():
80
+ train_state, params = _split_params(state)
81
+ items = {
82
+ "assets": save_assets,
83
+ "train_state": train_state,
84
+ "params": {"params": params},
85
+ }
86
+ checkpoint_manager.save(step, items)
87
+
88
+
89
+ def restore_state(
90
+ checkpoint_manager: ocp.CheckpointManager,
91
+ state: training_utils.TrainState,
92
+ data_loader: _data_loader.DataLoader,
93
+ step: int | None = None,
94
+ ) -> training_utils.TrainState:
95
+ del data_loader
96
+
97
+ with at.disable_typechecking():
98
+ # Split params that can be used for inference into a separate item.
99
+ train_state, params = _split_params(state)
100
+ restored = checkpoint_manager.restore(
101
+ step,
102
+ items={
103
+ "train_state": train_state,
104
+ "params": {"params": params},
105
+ },
106
+ )
107
+ return _merge_params(restored["train_state"], restored["params"])
108
+
109
+
110
+ def load_norm_stats(assets_dir: epath.Path | str, asset_id: str) -> dict[str, _normalize.NormStats] | None:
111
+ norm_stats_dir = epath.Path(assets_dir) / asset_id
112
+ norm_stats = _normalize.load(norm_stats_dir)
113
+ logging.info(f"Loaded norm stats from {norm_stats_dir}")
114
+ return norm_stats
115
+
116
+
117
+ class Callback(Protocol):
118
+ def __call__(self, directory: epath.Path) -> None: ...
119
+
120
+
121
+ class CallbackHandler(ocp.AsyncCheckpointHandler):
122
+ """A CheckpointHandler for calling an arbitrary function asynchronously. Only for saving, not for restoring."""
123
+
124
+ def save(self, directory: epath.Path, args: CallbackSave):
125
+ if jax.process_index() == 0:
126
+ args.callback(directory)
127
+
128
+ async def async_save(self, directory: epath.Path, args: CallbackSave) -> list[futures.Future]:
129
+ return [future.CommitFutureAwaitingContractedSignals(asyncio.to_thread(self.save, directory, args))]
130
+
131
+ def restore(self, *args, **kwargs):
132
+ raise NotImplementedError("CallbackHandler does not support restore")
133
+
134
+
135
+ @ocp.args.register_with_handler(CallbackHandler, for_save=True)
136
+ @dataclasses.dataclass
137
+ class CallbackSave(ocp.args.CheckpointArgs):
138
+ callback: Callback
139
+
140
+
141
+ @ocp.args.register_with_handler(CallbackHandler, for_restore=True)
142
+ class CallbackRestore(ocp.args.CheckpointArgs): ...
143
+
144
+
145
+ def _split_params(state: training_utils.TrainState) -> tuple[training_utils.TrainState, at.Params]:
146
+ if state.ema_params is not None:
147
+ params = state.ema_params
148
+ train_state = dataclasses.replace(state, ema_params=None)
149
+ else:
150
+ params = state.params
151
+ train_state = dataclasses.replace(state, params={})
152
+ return train_state, params
153
+
154
+
155
+ def _merge_params(train_state: training_utils.TrainState, params: dict[str, at.Params]) -> training_utils.TrainState:
156
+ # Revert the logic inside `_split_params`. Assumes that existence of `params` means that EMA params were used during the split.
157
+ if train_state.params:
158
+ return dataclasses.replace(train_state, ema_params=params["params"])
159
+ return dataclasses.replace(train_state, params=params["params"])
openpi_runtime/openpi/training/config.py ADDED
@@ -0,0 +1,1070 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """See _CONFIGS for the list of available configs."""
2
+
3
+ import abc
4
+ from collections.abc import Sequence
5
+ import dataclasses
6
+ import difflib
7
+ import logging
8
+ import pathlib
9
+ from typing import Any, Literal, Protocol, TypeAlias
10
+
11
+ import etils.epath as epath
12
+ import flax.nnx as nnx
13
+ from typing_extensions import override
14
+ import tyro
15
+
16
+ import openpi.models.model as _model
17
+ import openpi.models.pi0_config as pi0_config
18
+ import openpi.models.pi0_fast as pi0_fast
19
+ import openpi.models.tokenizer as _tokenizer
20
+ import openpi.policies.aloha_policy as aloha_policy
21
+ import openpi.policies.droid_policy as droid_policy
22
+ import openpi.policies.libero_policy as libero_policy
23
+ import openpi.policies.ur_policy as ur_policy
24
+ import openpi.shared.download as _download
25
+ import openpi.shared.normalize as _normalize
26
+ import openpi.training.droid_rlds_dataset as droid_rlds_dataset
27
+ import openpi.training.misc.polaris_config as polaris_config
28
+ import openpi.training.misc.roboarena_config as roboarena_config
29
+ import openpi.training.optimizer as _optimizer
30
+ import openpi.training.weight_loaders as weight_loaders
31
+ import openpi.transforms as _transforms
32
+
33
+ ModelType: TypeAlias = _model.ModelType
34
+ # Work around a tyro issue with using nnx.filterlib.Filter directly.
35
+ Filter: TypeAlias = nnx.filterlib.Filter
36
+
37
+
38
+ @dataclasses.dataclass(frozen=True)
39
+ class AssetsConfig:
40
+ """Determines the location of assets (e.g., norm stats) that will be used to set up the data pipeline.
41
+
42
+ These assets will be replicated inside the checkpoint under the `assets/asset_id` directory.
43
+
44
+ This can be used to load assets from a different checkpoint (e.g., base model checkpoint) or some other
45
+ centralized location. For example, to load the norm stats for the Trossen robot from the base model checkpoint
46
+ during fine-tuning, use:
47
+
48
+ ```
49
+ AssetsConfig(
50
+ assets_dir="gs://openpi-assets/checkpoints/pi0_base/assets",
51
+ asset_id="trossen",
52
+ )
53
+ ```
54
+ """
55
+
56
+ # Assets directory. If not provided, the config assets_dirs will be used. This is useful to load assets from
57
+ # a different checkpoint (e.g., base model checkpoint) or some other centralized location.
58
+ assets_dir: str | None = None
59
+
60
+ # Asset id. If not provided, the repo id will be used. This allows users to reference assets that describe
61
+ # different robot platforms.
62
+ asset_id: str | None = None
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True)
66
+ class DataConfig:
67
+ # LeRobot repo id. If None, fake data will be created.
68
+ repo_id: str | None = None
69
+ # Directory within the assets directory containing the data assets.
70
+ asset_id: str | None = None
71
+ # Contains precomputed normalization stats. If None, normalization will not be performed.
72
+ norm_stats: dict[str, _transforms.NormStats] | None = None
73
+
74
+ # Used to adopt the inputs from a dataset specific format to a common format
75
+ # which is expected by the data transforms.
76
+ repack_transforms: _transforms.Group = dataclasses.field(default_factory=_transforms.Group)
77
+ # Data transforms, typically include robot specific transformations. Will be applied
78
+ # before the data is normalized. See `model.Observation` and `model.Actions` to learn about the
79
+ # normalized data.
80
+ data_transforms: _transforms.Group = dataclasses.field(default_factory=_transforms.Group)
81
+ # Model specific transforms. Will be applied after the data is normalized.
82
+ model_transforms: _transforms.Group = dataclasses.field(default_factory=_transforms.Group)
83
+ # If true, will use quantile normalization. Otherwise, normal z-score normalization will be used.
84
+ use_quantile_norm: bool = False
85
+
86
+ # Names of keys that will be used by the data loader to generate the action sequence. The length of the
87
+ # sequence is defined by the `action_horizon` field in the model config. This should be adjusted if your
88
+ # LeRobot dataset is using different keys to represent the action.
89
+ action_sequence_keys: Sequence[str] = ("actions",)
90
+
91
+ # If true, will use the LeRobot dataset task to define the prompt.
92
+ prompt_from_task: bool = False
93
+
94
+ # Only used for RLDS data loader (ie currently only used for DROID).
95
+ rlds_data_dir: str | None = None
96
+ # Action space for DROID dataset.
97
+ action_space: droid_rlds_dataset.DroidActionSpace | None = None
98
+ # List of datasets to sample from: name, version, weight, and optionally filter_dict_path
99
+ datasets: Sequence[droid_rlds_dataset.RLDSDataset] = ()
100
+
101
+
102
+ class GroupFactory(Protocol):
103
+ def __call__(self, model_config: _model.BaseModelConfig) -> _transforms.Group:
104
+ """Create a group."""
105
+
106
+
107
+ @dataclasses.dataclass(frozen=True)
108
+ class ModelTransformFactory(GroupFactory):
109
+ """Creates model transforms for standard pi0 models."""
110
+
111
+ # If provided, will determine the default prompt that be used by the model.
112
+ default_prompt: str | None = None
113
+
114
+ def __call__(self, model_config: _model.BaseModelConfig) -> _transforms.Group:
115
+ match model_config.model_type:
116
+ case _model.ModelType.PI0:
117
+ return _transforms.Group(
118
+ inputs=[
119
+ _transforms.InjectDefaultPrompt(self.default_prompt),
120
+ _transforms.ResizeImages(224, 224),
121
+ _transforms.TokenizePrompt(
122
+ _tokenizer.PaligemmaTokenizer(model_config.max_token_len),
123
+ ),
124
+ _transforms.PadStatesAndActions(model_config.action_dim),
125
+ ],
126
+ )
127
+ case _model.ModelType.PI05:
128
+ assert isinstance(model_config, pi0_config.Pi0Config)
129
+ return _transforms.Group(
130
+ inputs=[
131
+ _transforms.InjectDefaultPrompt(self.default_prompt),
132
+ _transforms.ResizeImages(224, 224),
133
+ _transforms.TokenizePrompt(
134
+ _tokenizer.PaligemmaTokenizer(model_config.max_token_len),
135
+ discrete_state_input=model_config.discrete_state_input,
136
+ ),
137
+ _transforms.PadStatesAndActions(model_config.action_dim),
138
+ ],
139
+ )
140
+ case _model.ModelType.PI0_FAST:
141
+ tokenizer_cls = (
142
+ _tokenizer.FASTTokenizer
143
+ if model_config.fast_model_tokenizer is None
144
+ else model_config.fast_model_tokenizer
145
+ )
146
+ tokenizer_kwargs = (
147
+ {} if model_config.fast_model_tokenizer_kwargs is None else model_config.fast_model_tokenizer_kwargs
148
+ )
149
+ return _transforms.Group(
150
+ inputs=[
151
+ _transforms.InjectDefaultPrompt(self.default_prompt),
152
+ _transforms.ResizeImages(224, 224),
153
+ _transforms.TokenizeFASTInputs(
154
+ tokenizer_cls(model_config.max_token_len, **tokenizer_kwargs),
155
+ ),
156
+ ],
157
+ outputs=[
158
+ _transforms.ExtractFASTActions(
159
+ tokenizer_cls(model_config.max_token_len, **tokenizer_kwargs),
160
+ action_horizon=model_config.action_horizon,
161
+ action_dim=model_config.action_dim,
162
+ )
163
+ ],
164
+ )
165
+
166
+
167
+ @dataclasses.dataclass(frozen=True)
168
+ class DataConfigFactory(abc.ABC):
169
+ # The LeRobot repo id.
170
+ repo_id: str = tyro.MISSING
171
+ # Determines how the assets will be loaded.
172
+ assets: AssetsConfig = dataclasses.field(default_factory=AssetsConfig)
173
+ # Base config that will be updated by the factory.
174
+ base_config: tyro.conf.Suppress[DataConfig | None] = None
175
+
176
+ @abc.abstractmethod
177
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
178
+ """Create a data config."""
179
+
180
+ def create_base_config(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
181
+ repo_id = self.repo_id if self.repo_id is not tyro.MISSING else None
182
+ asset_id = self.assets.asset_id or repo_id
183
+ return dataclasses.replace(
184
+ self.base_config or DataConfig(),
185
+ repo_id=repo_id,
186
+ asset_id=asset_id,
187
+ norm_stats=self._load_norm_stats(epath.Path(self.assets.assets_dir or assets_dirs), asset_id),
188
+ use_quantile_norm=model_config.model_type != ModelType.PI0,
189
+ )
190
+
191
+ def _load_norm_stats(self, assets_dir: epath.Path, asset_id: str | None) -> dict[str, _transforms.NormStats] | None:
192
+ if asset_id is None:
193
+ return None
194
+ try:
195
+ data_assets_dir = str(assets_dir / asset_id)
196
+ norm_stats = _normalize.load(_download.maybe_download(data_assets_dir))
197
+ logging.info(f"Loaded norm stats from {data_assets_dir}")
198
+ return norm_stats
199
+ except FileNotFoundError:
200
+ logging.info(f"Norm stats not found in {data_assets_dir}, skipping.")
201
+ return None
202
+
203
+
204
+ @dataclasses.dataclass(frozen=True)
205
+ class FakeDataConfig(DataConfigFactory):
206
+ repo_id: str = "fake"
207
+
208
+ @override
209
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
210
+ return DataConfig(repo_id=self.repo_id)
211
+
212
+
213
+ @dataclasses.dataclass(frozen=True)
214
+ class SimpleDataConfig(DataConfigFactory):
215
+ # Factory for the data transforms.
216
+ data_transforms: tyro.conf.Suppress[GroupFactory] = dataclasses.field(default_factory=GroupFactory)
217
+ # Factory for the model transforms.
218
+ model_transforms: tyro.conf.Suppress[GroupFactory] = dataclasses.field(default_factory=ModelTransformFactory)
219
+
220
+ @override
221
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
222
+ return dataclasses.replace(
223
+ self.create_base_config(assets_dirs, model_config),
224
+ data_transforms=self.data_transforms(model_config),
225
+ model_transforms=self.model_transforms(model_config),
226
+ )
227
+
228
+
229
+ @dataclasses.dataclass(frozen=True)
230
+ class LeRobotAlohaDataConfig(DataConfigFactory):
231
+ # If true, will convert joint dimensions to deltas with respect to the current state before passing to the model.
232
+ # Gripper dimensions will remain in absolute values.
233
+ use_delta_joint_actions: bool = True
234
+ # If provided, will be injected into the input data if the "prompt" key is not present.
235
+ default_prompt: str | None = None
236
+ # If true, this will convert the joint and gripper values from the standard Aloha space to
237
+ # the space used by the pi internal runtime which was used to train the base model. People who
238
+ # use standard Aloha data should set this to true.
239
+ adapt_to_pi: bool = True
240
+
241
+ # Repack transforms.
242
+ repack_transforms: tyro.conf.Suppress[_transforms.Group] = dataclasses.field(
243
+ default=_transforms.Group(
244
+ inputs=[
245
+ _transforms.RepackTransform(
246
+ {
247
+ "images": {"cam_high": "observation.images.top"},
248
+ "state": "observation.state",
249
+ "actions": "action",
250
+ }
251
+ )
252
+ ]
253
+ )
254
+ )
255
+ # Action keys that will be used to read the action sequence from the dataset.
256
+ action_sequence_keys: Sequence[str] = ("action",)
257
+
258
+ @override
259
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
260
+ data_transforms = _transforms.Group(
261
+ inputs=[aloha_policy.AlohaInputs(adapt_to_pi=self.adapt_to_pi)],
262
+ outputs=[aloha_policy.AlohaOutputs(adapt_to_pi=self.adapt_to_pi)],
263
+ )
264
+ if self.use_delta_joint_actions:
265
+ delta_action_mask = _transforms.make_bool_mask(6, -1, 6, -1)
266
+ data_transforms = data_transforms.push(
267
+ inputs=[_transforms.DeltaActions(delta_action_mask)],
268
+ outputs=[_transforms.AbsoluteActions(delta_action_mask)],
269
+ )
270
+
271
+ model_transforms = ModelTransformFactory(default_prompt=self.default_prompt)(model_config)
272
+
273
+ return dataclasses.replace(
274
+ self.create_base_config(assets_dirs, model_config),
275
+ repack_transforms=self.repack_transforms,
276
+ data_transforms=data_transforms,
277
+ model_transforms=model_transforms,
278
+ action_sequence_keys=self.action_sequence_keys,
279
+ )
280
+
281
+
282
+ @dataclasses.dataclass(frozen=True)
283
+ class LeRobotLiberoDataConfig(DataConfigFactory):
284
+ """
285
+ This config is used to configure transforms that are applied at various parts of the data pipeline.
286
+ For your own dataset, you can copy this class and modify the transforms to match your dataset based on the
287
+ comments below.
288
+ """
289
+
290
+ extra_delta_transform: bool = False
291
+
292
+ @override
293
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
294
+ # The repack transform is *only* applied to the data coming from the dataset,
295
+ # and *not* during inference. We can use it to make inputs from the dataset look
296
+ # as close as possible to those coming from the inference environment (e.g. match the keys).
297
+ # Below, we match the keys in the dataset (which we defined in the data conversion script) to
298
+ # the keys we use in our inference pipeline (defined in the inference script for libero).
299
+ # For your own dataset, first figure out what keys your environment passes to the policy server
300
+ # and then modify the mappings below so your dataset's keys get matched to those target keys.
301
+ # The repack transform simply remaps key names here.
302
+ repack_transform = _transforms.Group(
303
+ inputs=[
304
+ _transforms.RepackTransform(
305
+ {
306
+ "observation/image": "image",
307
+ "observation/wrist_image": "wrist_image",
308
+ "observation/state": "state",
309
+ "actions": "actions",
310
+ "prompt": "prompt",
311
+ }
312
+ )
313
+ ]
314
+ )
315
+
316
+ # The data transforms are applied to the data coming from the dataset *and* during inference.
317
+ # Below, we define the transforms for data going into the model (``inputs``) and the transforms
318
+ # for data coming out of the model (``outputs``) (the latter is only used during inference).
319
+ # We defined these transforms in `libero_policy.py`. You can check the detailed comments there for
320
+ # how to modify the transforms to match your dataset. Once you created your own transforms, you can
321
+ # replace the transforms below with your own.
322
+ data_transforms = _transforms.Group(
323
+ inputs=[libero_policy.LiberoInputs(model_type=model_config.model_type)],
324
+ outputs=[libero_policy.LiberoOutputs()],
325
+ )
326
+
327
+ # One additional data transform: pi0 models are trained on delta actions (relative to the first
328
+ # state in each action chunk). IF your data has ``absolute`` actions (e.g. target joint angles)
329
+ # you can uncomment the following line to convert the actions to delta actions. The only exception
330
+ # is for the gripper actions which are always absolute.
331
+ # In the example below, we would apply the delta conversion to the first 6 actions (joints) and
332
+ # leave the 7th action (gripper) unchanged, i.e. absolute.
333
+ # In Libero, the raw actions in the dataset are already delta actions, so we *do not* need to
334
+ # apply a separate delta conversion (that's why it's commented out). Choose whether to apply this
335
+ # transform based on whether your dataset uses ``absolute`` or ``delta`` actions out of the box.
336
+
337
+ # LIBERO already represents actions as deltas, but we have some old Pi0 checkpoints that are trained with this
338
+ # extra delta transform.
339
+ if self.extra_delta_transform:
340
+ delta_action_mask = _transforms.make_bool_mask(6, -1)
341
+ data_transforms = data_transforms.push(
342
+ inputs=[_transforms.DeltaActions(delta_action_mask)],
343
+ outputs=[_transforms.AbsoluteActions(delta_action_mask)],
344
+ )
345
+
346
+ # Model transforms include things like tokenizing the prompt and action targets
347
+ # You do not need to change anything here for your own dataset.
348
+ model_transforms = ModelTransformFactory()(model_config)
349
+
350
+ # We return all data transforms for training and inference. No need to change anything here.
351
+ return dataclasses.replace(
352
+ self.create_base_config(assets_dirs, model_config),
353
+ repack_transforms=repack_transform,
354
+ data_transforms=data_transforms,
355
+ model_transforms=model_transforms,
356
+ )
357
+
358
+
359
+ @dataclasses.dataclass(frozen=True)
360
+ class LeRobotURDataConfig(DataConfigFactory):
361
+ """Data pipeline for keyboard-collected UR TCP delta trajectories."""
362
+
363
+ @override
364
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
365
+ repack_transform = _transforms.Group(
366
+ inputs=[
367
+ _transforms.RepackTransform(
368
+ {
369
+ "observation/image": "video.image_0",
370
+ "observation/wrist_image": "video.wrist",
371
+ "observation/state": "observation.state",
372
+ "actions": "action",
373
+ "prompt": "prompt",
374
+ }
375
+ )
376
+ ]
377
+ )
378
+ data_transforms = _transforms.Group(
379
+ inputs=[ur_policy.URInputs(model_type=model_config.model_type)],
380
+ outputs=[ur_policy.UROutputs()],
381
+ )
382
+ model_transforms = ModelTransformFactory()(model_config)
383
+
384
+ return dataclasses.replace(
385
+ self.create_base_config(assets_dirs, model_config),
386
+ repack_transforms=repack_transform,
387
+ data_transforms=data_transforms,
388
+ model_transforms=model_transforms,
389
+ action_sequence_keys=("action",),
390
+ )
391
+
392
+
393
+ @dataclasses.dataclass(frozen=True)
394
+ class RLDSDroidDataConfig(DataConfigFactory):
395
+ """
396
+ Config for training on DROID, using RLDS data format (for efficient training on larger datasets).
397
+ """
398
+
399
+ rlds_data_dir: str | None = None
400
+ action_space: droid_rlds_dataset.DroidActionSpace | None = None
401
+
402
+ # Filtering options. Can pass a path to a dictionary that maps episodes to timestep ranges
403
+ # to tuples denoting ranges of time steps to keep (start, end). Episodes are uniquely identified with
404
+ # f"{recording_folderpath}--{file_path}", both of which are present in the RLDS episode metadata.
405
+
406
+ # List of datasets to sample from: name, version, weight, and optionally filter_dict_path
407
+ datasets: Sequence[droid_rlds_dataset.RLDSDataset] = (
408
+ droid_rlds_dataset.RLDSDataset(
409
+ name="droid",
410
+ version="1.0.1",
411
+ weight=1.0,
412
+ filter_dict_path="gs://openpi-assets/droid/droid_sample_ranges_v1_0_1.json",
413
+ ),
414
+ )
415
+
416
+ @override
417
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
418
+ repack_transform = _transforms.Group(
419
+ inputs=[
420
+ _transforms.RepackTransform(
421
+ {
422
+ "observation/exterior_image_1_left": "observation/image",
423
+ "observation/wrist_image_left": "observation/wrist_image",
424
+ "observation/joint_position": "observation/joint_position",
425
+ "observation/gripper_position": "observation/gripper_position",
426
+ "actions": "actions",
427
+ "prompt": "prompt",
428
+ }
429
+ )
430
+ ]
431
+ )
432
+
433
+ data_transforms = _transforms.Group(
434
+ inputs=[droid_policy.DroidInputs(model_type=model_config.model_type)],
435
+ outputs=[droid_policy.DroidOutputs()],
436
+ )
437
+
438
+ if self.action_space == droid_rlds_dataset.DroidActionSpace.JOINT_POSITION:
439
+ # Data loader returns absolute joint position actions -- convert to delta actions for training.
440
+ delta_action_mask = _transforms.make_bool_mask(7, -1)
441
+ data_transforms = data_transforms.push(
442
+ inputs=[_transforms.DeltaActions(delta_action_mask)],
443
+ outputs=[_transforms.AbsoluteActions(delta_action_mask)],
444
+ )
445
+
446
+ model_transforms = ModelTransformFactory()(model_config)
447
+
448
+ assert self.rlds_data_dir is not None, "Need to set rlds data dir for RLDS data loader."
449
+
450
+ return dataclasses.replace(
451
+ self.create_base_config(assets_dirs, model_config),
452
+ repack_transforms=repack_transform,
453
+ data_transforms=data_transforms,
454
+ model_transforms=model_transforms,
455
+ rlds_data_dir=self.rlds_data_dir,
456
+ action_space=self.action_space,
457
+ datasets=self.datasets,
458
+ )
459
+
460
+
461
+ @dataclasses.dataclass(frozen=True)
462
+ class LeRobotDROIDDataConfig(DataConfigFactory):
463
+ """
464
+ Example data config for custom DROID dataset in LeRobot format.
465
+ To convert your custom DROID dataset (<10s of hours) to LeRobot format, see examples/droid/convert_droid_data_to_lerobot.py
466
+ """
467
+
468
+ @override
469
+ def create(self, assets_dirs: pathlib.Path, model_config: _model.BaseModelConfig) -> DataConfig:
470
+ repack_transform = _transforms.Group(
471
+ inputs=[
472
+ _transforms.RepackTransform(
473
+ {
474
+ "observation/exterior_image_1_left": "exterior_image_1_left",
475
+ "observation/exterior_image_2_left": "exterior_image_2_left",
476
+ "observation/wrist_image_left": "wrist_image_left",
477
+ "observation/joint_position": "joint_position",
478
+ "observation/gripper_position": "gripper_position",
479
+ "actions": "actions",
480
+ "prompt": "prompt",
481
+ }
482
+ )
483
+ ]
484
+ )
485
+ # We assume joint *velocity* actions, so we should *not* apply an additional delta transform.
486
+ data_transforms = _transforms.Group(
487
+ inputs=[droid_policy.DroidInputs(model_type=model_config.model_type)],
488
+ outputs=[droid_policy.DroidOutputs()],
489
+ )
490
+ model_transforms = ModelTransformFactory()(model_config)
491
+
492
+ return dataclasses.replace(
493
+ self.create_base_config(assets_dirs, model_config),
494
+ repack_transforms=repack_transform,
495
+ data_transforms=data_transforms,
496
+ model_transforms=model_transforms,
497
+ )
498
+
499
+
500
+ @dataclasses.dataclass(frozen=True)
501
+ class TrainConfig:
502
+ # Name of the config. Must be unique. Will be used to reference this config.
503
+ name: tyro.conf.Suppress[str]
504
+ # Project name.
505
+ project_name: str = "openpi"
506
+ # Experiment name. Will be used to name the metadata and checkpoint directories.
507
+ exp_name: str = tyro.MISSING
508
+
509
+ # Defines the model config. Some attributes (action_dim, action_horizon, and max_token_len) are shared by all models
510
+ # -- see BaseModelConfig. Specific model implementations (e.g., Pi0Config) inherit from BaseModelConfig and may
511
+ # define additional attributes.
512
+ model: _model.BaseModelConfig = dataclasses.field(default_factory=pi0_config.Pi0Config)
513
+
514
+ # A weight loader can optionally load (possibly partial) weights from disk after the model is initialized.
515
+ weight_loader: weight_loaders.WeightLoader = dataclasses.field(default_factory=weight_loaders.NoOpWeightLoader)
516
+
517
+ # Optional path to a PyTorch checkpoint to load weights from.
518
+ pytorch_weight_path: str | None = None
519
+
520
+ # Precision for PyTorch training.
521
+ pytorch_training_precision: Literal["bfloat16", "float32"] = "bfloat16"
522
+
523
+ lr_schedule: _optimizer.LRScheduleConfig = dataclasses.field(default_factory=_optimizer.CosineDecaySchedule)
524
+ optimizer: _optimizer.OptimizerConfig = dataclasses.field(default_factory=_optimizer.AdamW)
525
+ ema_decay: float | None = 0.99
526
+
527
+ # Specifies which weights should be frozen.
528
+ freeze_filter: tyro.conf.Suppress[Filter] = dataclasses.field(default_factory=nnx.Nothing)
529
+
530
+ # Determines the data to be trained on.
531
+ data: DataConfigFactory = dataclasses.field(default_factory=FakeDataConfig)
532
+
533
+ # Base directory for config assets (e.g., norm stats).
534
+ assets_base_dir: str = "./assets"
535
+ # Base directory for checkpoints.
536
+ checkpoint_base_dir: str = "./checkpoints"
537
+
538
+ # Random seed that will be used by random generators during training.
539
+ seed: int = 42
540
+ # Global batch size.
541
+ batch_size: int = 32
542
+ # Number of workers to use for the data loader. Increasing this number will speed up data loading but
543
+ # will increase memory and CPU usage.
544
+ num_workers: int = 2
545
+ # Number of train steps (batches) to run.
546
+ num_train_steps: int = 30_000
547
+
548
+ # How often (in steps) to log training metrics.
549
+ log_interval: int = 100
550
+ # How often (in steps) to save checkpoints.
551
+ save_interval: int = 1000
552
+ # If set, any existing checkpoints matching step % keep_period == 0 will not be deleted.
553
+ keep_period: int | None = 5000
554
+
555
+ # If true, will overwrite the checkpoint directory if it already exists.
556
+ overwrite: bool = False
557
+ # If true, will resume training from the last checkpoint.
558
+ resume: bool = False
559
+
560
+ # If true, will enable wandb logging.
561
+ wandb_enabled: bool = True
562
+
563
+ # Used to pass metadata to the policy server.
564
+ policy_metadata: dict[str, Any] | None = None
565
+
566
+ # If the value is greater than 1, FSDP will be enabled and shard across number of specified devices; overall
567
+ # device memory will be reduced but training could potentially be slower.
568
+ # eg. if total device is 4 and fsdp devices is 2; then the model will shard to 2 devices and run
569
+ # data parallel between 2 groups of devices.
570
+ fsdp_devices: int = 1
571
+
572
+ @property
573
+ def assets_dirs(self) -> pathlib.Path:
574
+ """Get the assets directory for this config."""
575
+ return (pathlib.Path(self.assets_base_dir) / self.name).resolve()
576
+
577
+ @property
578
+ def checkpoint_dir(self) -> pathlib.Path:
579
+ """Get the checkpoint directory for this config."""
580
+ if not self.exp_name:
581
+ raise ValueError("--exp_name must be set")
582
+ return (pathlib.Path(self.checkpoint_base_dir) / self.name / self.exp_name).resolve()
583
+
584
+ @property
585
+ def trainable_filter(self) -> nnx.filterlib.Filter:
586
+ """Get the filter for the trainable parameters."""
587
+ return nnx.All(nnx.Param, nnx.Not(self.freeze_filter))
588
+
589
+ def __post_init__(self) -> None:
590
+ if self.resume and self.overwrite:
591
+ raise ValueError("Cannot resume and overwrite at the same time.")
592
+
593
+
594
+ # Use `get_config` if you need to get a config by name in your code.
595
+ _CONFIGS = [
596
+ #
597
+ # Inference Aloha configs.
598
+ #
599
+ TrainConfig(
600
+ name="pi0_aloha",
601
+ model=pi0_config.Pi0Config(),
602
+ data=LeRobotAlohaDataConfig(
603
+ assets=AssetsConfig(asset_id="trossen"),
604
+ ),
605
+ policy_metadata={"reset_pose": [0, -1.5, 1.5, 0, 0, 0]},
606
+ ),
607
+ TrainConfig(
608
+ name="pi05_aloha",
609
+ model=pi0_config.Pi0Config(pi05=True),
610
+ data=LeRobotAlohaDataConfig(
611
+ assets=AssetsConfig(asset_id="trossen"),
612
+ ),
613
+ policy_metadata={"reset_pose": [0, -1.5, 1.5, 0, 0, 0]},
614
+ ),
615
+ TrainConfig(
616
+ name="pi0_aloha_towel",
617
+ model=pi0_config.Pi0Config(),
618
+ data=LeRobotAlohaDataConfig(
619
+ assets=AssetsConfig(asset_id="trossen"),
620
+ default_prompt="fold the towel",
621
+ ),
622
+ policy_metadata={"reset_pose": [0, -1.5, 1.5, 0, 0, 0]},
623
+ ),
624
+ TrainConfig(
625
+ name="pi0_aloha_tupperware",
626
+ model=pi0_config.Pi0Config(),
627
+ data=LeRobotAlohaDataConfig(
628
+ assets=AssetsConfig(asset_id="trossen"),
629
+ default_prompt="open the tupperware and put the food on the plate",
630
+ ),
631
+ policy_metadata={"reset_pose": [0, -1.5, 1.5, 0, 0, 0]},
632
+ ),
633
+ #
634
+ # Inference DROID configs.
635
+ #
636
+ TrainConfig(
637
+ name="pi0_droid",
638
+ model=pi0_config.Pi0Config(action_horizon=10),
639
+ data=SimpleDataConfig(
640
+ assets=AssetsConfig(asset_id="droid"),
641
+ data_transforms=lambda model: _transforms.Group(
642
+ inputs=[droid_policy.DroidInputs(model_type=ModelType.PI0)],
643
+ outputs=[droid_policy.DroidOutputs()],
644
+ ),
645
+ base_config=DataConfig(
646
+ prompt_from_task=True,
647
+ ),
648
+ ),
649
+ ),
650
+ TrainConfig(
651
+ name="pi0_fast_droid",
652
+ model=pi0_fast.Pi0FASTConfig(action_dim=8, action_horizon=10),
653
+ data=SimpleDataConfig(
654
+ assets=AssetsConfig(asset_id="droid"),
655
+ data_transforms=lambda model: _transforms.Group(
656
+ inputs=[droid_policy.DroidInputs(model_type=ModelType.PI0_FAST)],
657
+ outputs=[droid_policy.DroidOutputs()],
658
+ ),
659
+ base_config=DataConfig(
660
+ prompt_from_task=True,
661
+ ),
662
+ ),
663
+ ),
664
+ TrainConfig(
665
+ name="pi05_droid",
666
+ model=pi0_config.Pi0Config(action_horizon=15, pi05=True),
667
+ data=SimpleDataConfig(
668
+ assets=AssetsConfig(asset_id="droid"),
669
+ data_transforms=lambda model: _transforms.Group(
670
+ inputs=[droid_policy.DroidInputs(model_type=ModelType.PI05)],
671
+ outputs=[droid_policy.DroidOutputs()],
672
+ ),
673
+ base_config=DataConfig(
674
+ prompt_from_task=True,
675
+ ),
676
+ ),
677
+ ),
678
+ #
679
+ # Fine-tuning Libero configs.
680
+ #
681
+ # These train configs define the hyperparameters for fine-tuning the base model on your own dataset.
682
+ # They are used to define key elements like the dataset you are training on, the base checkpoint you
683
+ # are using, and other hyperparameters like how many training steps to run or what learning rate to use.
684
+ # For your own dataset, you can copy this class and modify the dataset name, and data transforms based on
685
+ # the comments below.
686
+ TrainConfig(
687
+ # Change the name to reflect your model and dataset.
688
+ name="pi0_libero",
689
+ # Here you define the model config -- In this example we use pi0 as the model
690
+ # architecture and perform *full* finetuning. in the examples below we show how to modify
691
+ # this to perform *low-memory* (LORA) finetuning and use pi0-FAST as an alternative architecture.
692
+ model=pi0_config.Pi0Config(),
693
+ # Here you define the dataset you are training on. In this example we use the Libero
694
+ # dataset. For your own dataset, you can change the repo_id to point to your dataset.
695
+ # Also modify the DataConfig to use the new config you made for your dataset above.
696
+ data=LeRobotLiberoDataConfig(
697
+ repo_id="physical-intelligence/libero",
698
+ base_config=DataConfig(
699
+ # This flag determines whether we load the prompt (i.e. the task instruction) from the
700
+ # ``task`` field in the LeRobot dataset. If set to True, the prompt will show up in
701
+ # a field called ``prompt`` in the input dict. The recommended setting is True.
702
+ prompt_from_task=True,
703
+ ),
704
+ extra_delta_transform=True,
705
+ ),
706
+ # Here you define which pre-trained checkpoint you want to load to initialize the model.
707
+ # This should match the model config you chose above -- i.e. in this case we use the pi0 base model.
708
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_base/params"),
709
+ # Below you can define other hyperparameters like the learning rate, number of training steps, etc.
710
+ # Check the base TrainConfig class for a full list of available hyperparameters.
711
+ num_train_steps=30_000,
712
+ ),
713
+ TrainConfig(
714
+ name="pi0_libero_low_mem_finetune",
715
+ # Here is an example of loading a pi0 model for LoRA fine-tuning.
716
+ model=pi0_config.Pi0Config(paligemma_variant="gemma_2b_lora", action_expert_variant="gemma_300m_lora"),
717
+ data=LeRobotLiberoDataConfig(
718
+ repo_id="physical-intelligence/libero",
719
+ base_config=DataConfig(prompt_from_task=True),
720
+ extra_delta_transform=True,
721
+ ),
722
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_base/params"),
723
+ num_train_steps=30_000,
724
+ # The freeze filter defines which parameters should be frozen during training.
725
+ # We have a convenience function in the model config that returns the default freeze filter
726
+ # for the given model config for LoRA finetuning. Just make sure it matches the model config
727
+ # you chose above.
728
+ freeze_filter=pi0_config.Pi0Config(
729
+ paligemma_variant="gemma_2b_lora", action_expert_variant="gemma_300m_lora"
730
+ ).get_freeze_filter(),
731
+ # Turn off EMA for LoRA finetuning.
732
+ ema_decay=None,
733
+ ),
734
+ TrainConfig(
735
+ name="pi0_fast_libero",
736
+ # Here is an example of loading a pi0-FAST model for full finetuning.
737
+ # Modify action_dim and action_horizon to match your dataset (action horizon is equal to
738
+ # the desired action chunk length).
739
+ # The max_token_len is the maximum number of (non-image) tokens the model can handle.
740
+ # This includes the tokenized prompt, proprioceptive state, and (FAST-tokenized) action tokens.
741
+ # Choosing this value too small may chop off tokens at the end of your sequence (the code will throw
742
+ # a warning), while choosing it too large will waste memory (since we pad each batch element to the
743
+ # max_token_len). A good rule of thumb is to use approx 180 for single-arm robots, and approx 250 for
744
+ # two-arm robots. Generally, err on the lower side here first, and potentially increase the value if
745
+ # you see many warnings being thrown during training.
746
+ model=pi0_fast.Pi0FASTConfig(action_dim=7, action_horizon=10, max_token_len=180),
747
+ data=LeRobotLiberoDataConfig(
748
+ repo_id="physical-intelligence/libero",
749
+ base_config=DataConfig(prompt_from_task=True),
750
+ extra_delta_transform=True,
751
+ ),
752
+ # Note that we load the pi0-FAST base model checkpoint here.
753
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_fast_base/params"),
754
+ num_train_steps=30_000,
755
+ ),
756
+ TrainConfig(
757
+ name="pi0_fast_libero_low_mem_finetune",
758
+ # Here is an example of loading a pi0-FAST model for LoRA finetuning.
759
+ # For setting action_dim, action_horizon, and max_token_len, see the comments above.
760
+ model=pi0_fast.Pi0FASTConfig(
761
+ action_dim=7, action_horizon=10, max_token_len=180, paligemma_variant="gemma_2b_lora"
762
+ ),
763
+ data=LeRobotLiberoDataConfig(
764
+ repo_id="physical-intelligence/libero",
765
+ base_config=DataConfig(prompt_from_task=True),
766
+ extra_delta_transform=True,
767
+ ),
768
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_fast_base/params"),
769
+ num_train_steps=30_000,
770
+ # Again, make sure to match the model config above when extracting the freeze filter
771
+ # that specifies which parameters should be frozen during LoRA finetuning.
772
+ freeze_filter=pi0_fast.Pi0FASTConfig(
773
+ action_dim=7, action_horizon=10, max_token_len=180, paligemma_variant="gemma_2b_lora"
774
+ ).get_freeze_filter(),
775
+ # Turn off EMA for LoRA finetuning.
776
+ ema_decay=None,
777
+ ),
778
+ TrainConfig(
779
+ name="pi05_libero",
780
+ model=pi0_config.Pi0Config(pi05=True, action_horizon=10, discrete_state_input=False),
781
+ data=LeRobotLiberoDataConfig(
782
+ repo_id="physical-intelligence/libero",
783
+ base_config=DataConfig(prompt_from_task=True),
784
+ extra_delta_transform=False,
785
+ ),
786
+ batch_size=256,
787
+ lr_schedule=_optimizer.CosineDecaySchedule(
788
+ warmup_steps=10_000,
789
+ peak_lr=5e-5,
790
+ decay_steps=1_000_000,
791
+ decay_lr=5e-5,
792
+ ),
793
+ optimizer=_optimizer.AdamW(clip_gradient_norm=1.0),
794
+ ema_decay=0.999,
795
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"),
796
+ pytorch_weight_path="/path/to/your/pytorch_weight_path",
797
+ num_train_steps=30_000,
798
+ ),
799
+ TrainConfig(
800
+ name="pi05_ur_demo_state",
801
+ model=pi0_config.Pi0Config(
802
+ pi05=True,
803
+ action_horizon=10,
804
+ discrete_state_input=True,
805
+ ),
806
+ data=LeRobotURDataConfig(
807
+ repo_id="ur_demo",
808
+ base_config=DataConfig(prompt_from_task=True),
809
+ ),
810
+ batch_size=256,
811
+ lr_schedule=_optimizer.CosineDecaySchedule(
812
+ warmup_steps=10_000,
813
+ peak_lr=5e-5,
814
+ decay_steps=1_000_000,
815
+ decay_lr=5e-5,
816
+ ),
817
+ optimizer=_optimizer.AdamW(clip_gradient_norm=1.0),
818
+ ema_decay=0.999,
819
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"),
820
+ num_train_steps=30_000,
821
+ ),
822
+ TrainConfig(
823
+ name="pi05_ur_demo_no_state",
824
+ model=pi0_config.Pi0Config(
825
+ pi05=True,
826
+ action_horizon=10,
827
+ discrete_state_input=False,
828
+ ),
829
+ data=LeRobotURDataConfig(
830
+ repo_id="ur_demo",
831
+ base_config=DataConfig(prompt_from_task=True),
832
+ ),
833
+ batch_size=256,
834
+ lr_schedule=_optimizer.CosineDecaySchedule(
835
+ warmup_steps=10_000,
836
+ peak_lr=5e-5,
837
+ decay_steps=1_000_000,
838
+ decay_lr=5e-5,
839
+ ),
840
+ optimizer=_optimizer.AdamW(clip_gradient_norm=1.0),
841
+ ema_decay=0.999,
842
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"),
843
+ num_train_steps=30_000,
844
+ ),
845
+ #
846
+ # Fine-tuning Aloha configs.
847
+ #
848
+ # This is a test config that is used to illustate how train on a custom LeRobot dataset.
849
+ # For instructions on how to convert and train on your own Aloha dataset see examples/aloha_real/README.md
850
+ TrainConfig(
851
+ name="pi0_aloha_pen_uncap",
852
+ model=pi0_config.Pi0Config(),
853
+ data=LeRobotAlohaDataConfig(
854
+ repo_id="physical-intelligence/aloha_pen_uncap_diverse",
855
+ assets=AssetsConfig(
856
+ assets_dir="gs://openpi-assets/checkpoints/pi0_base/assets",
857
+ asset_id="trossen",
858
+ ),
859
+ default_prompt="uncap the pen",
860
+ repack_transforms=_transforms.Group(
861
+ inputs=[
862
+ _transforms.RepackTransform(
863
+ {
864
+ "images": {
865
+ "cam_high": "observation.images.cam_high",
866
+ "cam_left_wrist": "observation.images.cam_left_wrist",
867
+ "cam_right_wrist": "observation.images.cam_right_wrist",
868
+ },
869
+ "state": "observation.state",
870
+ "actions": "action",
871
+ }
872
+ )
873
+ ]
874
+ ),
875
+ ),
876
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_base/params"),
877
+ num_train_steps=20_000,
878
+ ),
879
+ TrainConfig(
880
+ name="pi05_aloha_pen_uncap",
881
+ model=pi0_config.Pi0Config(pi05=True),
882
+ data=LeRobotAlohaDataConfig(
883
+ repo_id="physical-intelligence/aloha_pen_uncap_diverse",
884
+ assets=AssetsConfig(
885
+ assets_dir="gs://openpi-assets/checkpoints/pi05_base/assets",
886
+ asset_id="trossen",
887
+ ),
888
+ default_prompt="uncap the pen",
889
+ repack_transforms=_transforms.Group(
890
+ inputs=[
891
+ _transforms.RepackTransform(
892
+ {
893
+ "images": {
894
+ "cam_high": "observation.images.cam_high",
895
+ "cam_left_wrist": "observation.images.cam_left_wrist",
896
+ "cam_right_wrist": "observation.images.cam_right_wrist",
897
+ },
898
+ "state": "observation.state",
899
+ "actions": "action",
900
+ }
901
+ )
902
+ ]
903
+ ),
904
+ ),
905
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"),
906
+ num_train_steps=20_000,
907
+ batch_size=64,
908
+ ),
909
+ #
910
+ # Fine-tuning DROID configs.
911
+ #
912
+ TrainConfig(
913
+ # This config is for fine-tuning pi0-FAST-base on the *full* DROID dataset.
914
+ # We use RLDS data loading to make training on this large dataset tractable.
915
+ # For fine-tuning on your own DROID dataset, see below.
916
+ name="pi0_fast_full_droid_finetune",
917
+ model=pi0_fast.Pi0FASTConfig(
918
+ action_dim=8,
919
+ action_horizon=16,
920
+ max_token_len=180,
921
+ ),
922
+ data=RLDSDroidDataConfig(
923
+ repo_id="droid",
924
+ # Set this to the path to your DROID RLDS dataset (the parent directory of the `droid` directory).
925
+ rlds_data_dir="<path_to_droid_rlds_dataset>",
926
+ action_space=droid_rlds_dataset.DroidActionSpace.JOINT_POSITION,
927
+ ),
928
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_fast_base/params"),
929
+ lr_schedule=_optimizer.CosineDecaySchedule(
930
+ warmup_steps=1_000,
931
+ peak_lr=5e-5,
932
+ decay_steps=1_000_000,
933
+ decay_lr=5e-5,
934
+ ),
935
+ num_train_steps=100_000, # 100k steps should be sufficient, takes ~2 days on 8x H100s
936
+ batch_size=256,
937
+ log_interval=100,
938
+ save_interval=5000,
939
+ keep_period=20_000,
940
+ num_workers=0, # Important: RLDS DataLoader requires num_workers=0, handles multi-processing internally
941
+ ),
942
+ TrainConfig(
943
+ # This config is for fine-tuning pi05 on the *full* DROID dataset.
944
+ # We use RLDS data loading to make training on this large dataset tractable.
945
+ # For fine-tuning on your own DROID dataset, see below.
946
+ name="pi05_full_droid_finetune",
947
+ model=pi0_config.Pi0Config(
948
+ pi05=True,
949
+ action_dim=32,
950
+ action_horizon=16,
951
+ ),
952
+ data=RLDSDroidDataConfig(
953
+ repo_id="droid",
954
+ # Set this to the path to your DROID RLDS dataset (the parent directory of the `droid` directory).
955
+ rlds_data_dir="/mnt/pi-data/kevin",
956
+ action_space=droid_rlds_dataset.DroidActionSpace.JOINT_POSITION,
957
+ assets=AssetsConfig(
958
+ assets_dir="gs://openpi-assets/checkpoints/pi05_base/assets/",
959
+ asset_id="droid",
960
+ ),
961
+ ),
962
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_base/params"),
963
+ lr_schedule=_optimizer.CosineDecaySchedule(
964
+ warmup_steps=1_000,
965
+ peak_lr=5e-5,
966
+ decay_steps=1_000_000,
967
+ decay_lr=5e-5,
968
+ ),
969
+ num_train_steps=100_000,
970
+ batch_size=256,
971
+ log_interval=100,
972
+ save_interval=5000,
973
+ keep_period=10_000,
974
+ num_workers=0, # Important: RLDS DataLoader requires num_workers=0, handles multi-processing internally
975
+ ),
976
+ TrainConfig(
977
+ # This config is for fine-tuning pi05-DROID on a custom (smaller) DROID dataset.
978
+ # Here, we use LeRobot data format (like for all other fine-tuning examples)
979
+ # To convert your custom DROID dataset (<10s of hours) to LeRobot format, see examples/droid/convert_droid_data_to_lerobot.py
980
+ name="pi05_droid_finetune",
981
+ model=pi0_config.Pi0Config(
982
+ pi05=True,
983
+ action_dim=32, # pi05 is trained with 32-dim actions
984
+ action_horizon=16,
985
+ ),
986
+ data=LeRobotDROIDDataConfig(
987
+ # Replace with your custom DROID LeRobot dataset repo id.
988
+ repo_id="your_hf_username/my_droid_dataset",
989
+ base_config=DataConfig(prompt_from_task=True),
990
+ assets=AssetsConfig(
991
+ # Important: reuse the original DROID norm stats during fine-tuning!
992
+ assets_dir="gs://openpi-assets/checkpoints/pi05_droid/assets",
993
+ asset_id="droid",
994
+ ),
995
+ ),
996
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi05_droid/params"),
997
+ num_train_steps=20_000,
998
+ batch_size=32,
999
+ ),
1000
+ #
1001
+ # ALOHA Sim configs. This config is used to demonstrate how to train on a simple simulated environment.
1002
+ #
1003
+ TrainConfig(
1004
+ name="pi0_aloha_sim",
1005
+ model=pi0_config.Pi0Config(),
1006
+ data=LeRobotAlohaDataConfig(
1007
+ repo_id="lerobot/aloha_sim_transfer_cube_human",
1008
+ default_prompt="Transfer cube",
1009
+ use_delta_joint_actions=False,
1010
+ ),
1011
+ weight_loader=weight_loaders.CheckpointWeightLoader("gs://openpi-assets/checkpoints/pi0_base/params"),
1012
+ num_train_steps=20_000,
1013
+ ),
1014
+ #
1015
+ # Debugging configs.
1016
+ #
1017
+ TrainConfig(
1018
+ name="debug",
1019
+ data=FakeDataConfig(),
1020
+ batch_size=2,
1021
+ model=pi0_config.Pi0Config(paligemma_variant="dummy", action_expert_variant="dummy"),
1022
+ save_interval=100,
1023
+ overwrite=True,
1024
+ exp_name="debug",
1025
+ num_train_steps=10,
1026
+ wandb_enabled=False,
1027
+ ),
1028
+ TrainConfig(
1029
+ name="debug_restore",
1030
+ data=FakeDataConfig(),
1031
+ batch_size=2,
1032
+ model=pi0_config.Pi0Config(paligemma_variant="dummy", action_expert_variant="dummy"),
1033
+ weight_loader=weight_loaders.CheckpointWeightLoader("./checkpoints/debug/debug/9/params"),
1034
+ overwrite=True,
1035
+ exp_name="debug",
1036
+ num_train_steps=10,
1037
+ wandb_enabled=False,
1038
+ ),
1039
+ TrainConfig(
1040
+ name="debug_pi05",
1041
+ model=pi0_config.Pi0Config(pi05=True, paligemma_variant="dummy", action_expert_variant="dummy"),
1042
+ data=FakeDataConfig(),
1043
+ batch_size=2,
1044
+ num_train_steps=10,
1045
+ overwrite=True,
1046
+ exp_name="debug_pi05",
1047
+ wandb_enabled=False,
1048
+ ),
1049
+ # RoboArena & PolaRiS configs.
1050
+ *roboarena_config.get_roboarena_configs(),
1051
+ *polaris_config.get_polaris_configs(),
1052
+ ]
1053
+
1054
+ if len({config.name for config in _CONFIGS}) != len(_CONFIGS):
1055
+ raise ValueError("Config names must be unique.")
1056
+ _CONFIGS_DICT = {config.name: config for config in _CONFIGS}
1057
+
1058
+
1059
+ def cli() -> TrainConfig:
1060
+ return tyro.extras.overridable_config_cli({k: (k, v) for k, v in _CONFIGS_DICT.items()})
1061
+
1062
+
1063
+ def get_config(config_name: str) -> TrainConfig:
1064
+ """Get a config by name."""
1065
+ if config_name not in _CONFIGS_DICT:
1066
+ closest = difflib.get_close_matches(config_name, _CONFIGS_DICT.keys(), n=1, cutoff=0.0)
1067
+ closest_str = f" Did you mean '{closest[0]}'? " if closest else ""
1068
+ raise ValueError(f"Config '{config_name}' not found.{closest_str}")
1069
+
1070
+ return _CONFIGS_DICT[config_name]
openpi_runtime/openpi/training/data_loader.py ADDED
@@ -0,0 +1,540 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Iterator, Sequence
2
+ import logging
3
+ import multiprocessing
4
+ import os
5
+ import typing
6
+ from typing import Literal, Protocol, SupportsIndex, TypeVar
7
+
8
+ import jax
9
+ import jax.numpy as jnp
10
+ import lerobot.common.datasets.lerobot_dataset as lerobot_dataset
11
+ import numpy as np
12
+ import torch
13
+
14
+ import openpi.models.model as _model
15
+ import openpi.training.config as _config
16
+ from openpi.training.droid_rlds_dataset import DroidRldsDataset
17
+ import openpi.transforms as _transforms
18
+
19
+ T_co = TypeVar("T_co", covariant=True)
20
+
21
+
22
+ class Dataset(Protocol[T_co]):
23
+ """Interface for a dataset with random access."""
24
+
25
+ def __getitem__(self, index: SupportsIndex) -> T_co:
26
+ raise NotImplementedError("Subclasses of Dataset should implement __getitem__.")
27
+
28
+ def __len__(self) -> int:
29
+ raise NotImplementedError("Subclasses of Dataset should implement __len__.")
30
+
31
+
32
+ class IterableDataset(Protocol[T_co]):
33
+ """Interface for an iterable dataset."""
34
+
35
+ def __iter__(self) -> Iterator[T_co]:
36
+ raise NotImplementedError("Subclasses of IterableDataset should implement __iter__.")
37
+
38
+ def __len__(self) -> int:
39
+ raise NotImplementedError("Subclasses of Dataset should implement __len__.")
40
+
41
+
42
+ class DataLoader(Protocol[T_co]):
43
+ """Interface for a data loader."""
44
+
45
+ def data_config(self) -> _config.DataConfig:
46
+ """Get the data config for this data loader."""
47
+ raise NotImplementedError("Subclasses of DataLoader should implement data_config.")
48
+
49
+ def __iter__(self) -> Iterator[T_co]:
50
+ raise NotImplementedError("Subclasses of DataLoader should implement __iter__.")
51
+
52
+
53
+ class TransformedDataset(Dataset[T_co]):
54
+ def __init__(self, dataset: Dataset, transforms: Sequence[_transforms.DataTransformFn]):
55
+ self._dataset = dataset
56
+ self._transform = _transforms.compose(transforms)
57
+
58
+ def __getitem__(self, index: SupportsIndex) -> T_co:
59
+ return self._transform(self._dataset[index])
60
+
61
+ def __len__(self) -> int:
62
+ return len(self._dataset)
63
+
64
+
65
+ class IterableTransformedDataset(IterableDataset[T_co]):
66
+ def __init__(
67
+ self,
68
+ dataset: IterableDataset,
69
+ transforms: Sequence[_transforms.DataTransformFn],
70
+ *,
71
+ is_batched: bool = False,
72
+ ):
73
+ self._dataset = dataset
74
+ self._transform = _transforms.compose(transforms)
75
+ self._is_batched = is_batched
76
+
77
+ def __iter__(self):
78
+ for sample in self._dataset:
79
+ if self._is_batched:
80
+ # Transforms are designed to be applied to individual samples. So we need to split the batch into
81
+ # individual samples and apply the transform to each sample individually.
82
+ batch_size = next(v.shape[0] for v in sample.values())
83
+
84
+ # Split batch into individual samples using tree_map
85
+ individual_samples = [jax.tree.map(lambda x: x[i], sample) for i in range(batch_size)] # noqa: B023
86
+
87
+ # Transform each sample
88
+ transformed = [self._transform(s) for s in individual_samples]
89
+
90
+ # Recombine batch with tree_map
91
+ yield jax.tree.map(lambda *x: np.stack(x, axis=0), *transformed)
92
+ else:
93
+ yield self._transform(sample)
94
+
95
+ def __len__(self) -> int:
96
+ return len(self._dataset)
97
+
98
+
99
+ class FakeDataset(Dataset):
100
+ def __init__(self, model_config: _model.BaseModelConfig, num_samples: int):
101
+ self._num_samples = num_samples
102
+ self._observation_spec, self._action_spec = model_config.inputs_spec()
103
+
104
+ def __getitem__(self, index: SupportsIndex) -> dict:
105
+ rng = jax.random.key(index.__index__())
106
+
107
+ def make_from_spec(spec: jax.ShapeDtypeStruct):
108
+ nonlocal rng
109
+ rng, data_rng = jax.random.split(rng)
110
+ # Remove the batch dimension.
111
+ shape = spec.shape[1:]
112
+ if spec.dtype == jnp.float32:
113
+ return jax.random.uniform(data_rng, shape=shape, minval=-1.0, maxval=1.0)
114
+ if spec.dtype == jnp.int32:
115
+ return jax.random.randint(data_rng, shape=shape, minval=0, maxval=2048)
116
+ return jnp.zeros(shape=shape, dtype=spec.dtype)
117
+
118
+ observation = jax.tree.map(make_from_spec, self._observation_spec)
119
+ action = jax.tree.map(make_from_spec, self._action_spec)
120
+
121
+ return {
122
+ **observation.to_dict(),
123
+ "actions": action,
124
+ }
125
+
126
+ def __len__(self) -> int:
127
+ return self._num_samples
128
+
129
+
130
+ def create_torch_dataset(
131
+ data_config: _config.DataConfig, action_horizon: int, model_config: _model.BaseModelConfig
132
+ ) -> Dataset:
133
+ """Create a dataset for training."""
134
+ repo_id = data_config.repo_id
135
+ if repo_id is None:
136
+ raise ValueError("Repo ID is not set. Cannot create dataset.")
137
+ if repo_id == "fake":
138
+ return FakeDataset(model_config, num_samples=1024)
139
+
140
+ dataset_meta = lerobot_dataset.LeRobotDatasetMetadata(repo_id)
141
+ dataset = lerobot_dataset.LeRobotDataset(
142
+ data_config.repo_id,
143
+ delta_timestamps={
144
+ key: [t / dataset_meta.fps for t in range(action_horizon)] for key in data_config.action_sequence_keys
145
+ },
146
+ )
147
+
148
+ if data_config.prompt_from_task:
149
+ dataset = TransformedDataset(dataset, [_transforms.PromptFromLeRobotTask(dataset_meta.tasks)])
150
+
151
+ return dataset
152
+
153
+
154
+ def create_rlds_dataset(
155
+ data_config: _config.DataConfig,
156
+ action_horizon: int,
157
+ batch_size: int,
158
+ *,
159
+ shuffle: bool = False,
160
+ ) -> Dataset:
161
+ # At the moment, we only support DROID for RLDS datasets.
162
+ return DroidRldsDataset(
163
+ data_dir=data_config.rlds_data_dir,
164
+ batch_size=batch_size,
165
+ shuffle=shuffle,
166
+ action_chunk_size=action_horizon,
167
+ action_space=data_config.action_space,
168
+ datasets=data_config.datasets,
169
+ )
170
+
171
+
172
+ def transform_dataset(dataset: Dataset, data_config: _config.DataConfig, *, skip_norm_stats: bool = False) -> Dataset:
173
+ """Transform the dataset by applying the data transforms."""
174
+ norm_stats = {}
175
+ if data_config.repo_id != "fake" and not skip_norm_stats:
176
+ if data_config.norm_stats is None:
177
+ raise ValueError(
178
+ "Normalization stats not found. "
179
+ "Make sure to run `scripts/compute_norm_stats.py --config-name=<your-config>`."
180
+ )
181
+ norm_stats = data_config.norm_stats
182
+
183
+ return TransformedDataset(
184
+ dataset,
185
+ [
186
+ *data_config.repack_transforms.inputs,
187
+ *data_config.data_transforms.inputs,
188
+ _transforms.Normalize(norm_stats, use_quantiles=data_config.use_quantile_norm),
189
+ *data_config.model_transforms.inputs,
190
+ ],
191
+ )
192
+
193
+
194
+ def transform_iterable_dataset(
195
+ dataset: IterableDataset,
196
+ data_config: _config.DataConfig,
197
+ *,
198
+ skip_norm_stats: bool = False,
199
+ is_batched: bool = False,
200
+ ) -> IterableDataset:
201
+ """Transform the dataset by applying the data transforms."""
202
+ norm_stats = {}
203
+ if data_config.repo_id != "fake" and not skip_norm_stats:
204
+ if data_config.norm_stats is None:
205
+ raise ValueError(
206
+ "Normalization stats not found. "
207
+ "Make sure to run `scripts/compute_norm_stats.py --config-name=<your-config>`."
208
+ )
209
+ norm_stats = data_config.norm_stats
210
+
211
+ return IterableTransformedDataset(
212
+ dataset,
213
+ [
214
+ *data_config.repack_transforms.inputs,
215
+ *data_config.data_transforms.inputs,
216
+ _transforms.Normalize(norm_stats, use_quantiles=data_config.use_quantile_norm),
217
+ *data_config.model_transforms.inputs,
218
+ ],
219
+ is_batched=is_batched,
220
+ )
221
+
222
+
223
+ def create_data_loader(
224
+ config: _config.TrainConfig,
225
+ *,
226
+ sharding: jax.sharding.Sharding | None = None,
227
+ shuffle: bool = False,
228
+ num_batches: int | None = None,
229
+ skip_norm_stats: bool = False,
230
+ framework: Literal["jax", "pytorch"] = "jax",
231
+ ) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
232
+ """Create a data loader for training.
233
+
234
+ Args:
235
+ config: The training configuration.
236
+ sharding: The sharding to use for the data loader (JAX only).
237
+ shuffle: Whether to shuffle the data.
238
+ num_batches: Determines the number of batches to return.
239
+ skip_norm_stats: Whether to skip data normalization.
240
+ framework: The framework to use ("jax" or "pytorch").
241
+ """
242
+ data_config = config.data.create(config.assets_dirs, config.model)
243
+ logging.info(f"data_config: {data_config}")
244
+
245
+ if data_config.rlds_data_dir is not None:
246
+ return create_rlds_data_loader(
247
+ data_config,
248
+ action_horizon=config.model.action_horizon,
249
+ batch_size=config.batch_size,
250
+ sharding=sharding,
251
+ shuffle=shuffle,
252
+ num_batches=num_batches,
253
+ skip_norm_stats=skip_norm_stats,
254
+ framework=framework,
255
+ )
256
+ return create_torch_data_loader(
257
+ data_config,
258
+ model_config=config.model,
259
+ action_horizon=config.model.action_horizon,
260
+ batch_size=config.batch_size,
261
+ sharding=sharding,
262
+ shuffle=shuffle,
263
+ num_batches=num_batches,
264
+ num_workers=config.num_workers,
265
+ seed=config.seed,
266
+ skip_norm_stats=skip_norm_stats,
267
+ framework=framework,
268
+ )
269
+
270
+
271
+ def create_torch_data_loader(
272
+ data_config: _config.DataConfig,
273
+ model_config: _model.BaseModelConfig,
274
+ action_horizon: int,
275
+ batch_size: int,
276
+ *,
277
+ sharding: jax.sharding.Sharding | None = None,
278
+ skip_norm_stats: bool = False,
279
+ shuffle: bool = False,
280
+ num_batches: int | None = None,
281
+ num_workers: int = 0,
282
+ seed: int = 0,
283
+ framework: str = "jax",
284
+ ) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
285
+ """Create a data loader for training.
286
+
287
+ Args:
288
+ data_config: The data configuration.
289
+ action_horizon: The action horizon.
290
+ batch_size: The batch size.
291
+ sharding: The sharding to use for the data loader. If None, the data loader will
292
+ use a single device sharding.
293
+ skip_norm_stats: Whether to skip data normalization.
294
+ shuffle: Whether to shuffle the data.
295
+ num_batches: Determines the number of batches to return. If the number exceeds the
296
+ number of batches in the dataset, the data loader will loop over the dataset.
297
+ If not provided, will iterate over the dataset indefinitely.
298
+ num_workers: The number of worker processes to use. If zero, the data loader will
299
+ execute in the main process.
300
+ seed: The seed to use for shuffling the data.
301
+ """
302
+ dataset = create_torch_dataset(data_config, action_horizon, model_config)
303
+ dataset = transform_dataset(dataset, data_config, skip_norm_stats=skip_norm_stats)
304
+
305
+ # Use TorchDataLoader for both frameworks
306
+ # For PyTorch DDP, create DistributedSampler and divide batch size by world size
307
+ # For JAX, divide by process count
308
+ sampler = None
309
+ if framework == "pytorch":
310
+ if torch.distributed.is_initialized():
311
+ sampler = torch.utils.data.distributed.DistributedSampler(
312
+ dataset,
313
+ num_replicas=torch.distributed.get_world_size(),
314
+ rank=torch.distributed.get_rank(),
315
+ shuffle=shuffle,
316
+ drop_last=True,
317
+ )
318
+ local_batch_size = batch_size // torch.distributed.get_world_size()
319
+ else:
320
+ local_batch_size = batch_size
321
+ else:
322
+ local_batch_size = batch_size // jax.process_count()
323
+
324
+ logging.info(f"local_batch_size: {local_batch_size}")
325
+ data_loader = TorchDataLoader(
326
+ dataset,
327
+ local_batch_size=local_batch_size,
328
+ sharding=None if framework == "pytorch" else sharding,
329
+ shuffle=(sampler is None and shuffle), # Don't shuffle if using sampler
330
+ sampler=sampler,
331
+ num_batches=num_batches,
332
+ num_workers=num_workers,
333
+ seed=seed,
334
+ framework=framework,
335
+ )
336
+
337
+ return DataLoaderImpl(data_config, data_loader)
338
+
339
+
340
+ def create_rlds_data_loader(
341
+ data_config: _config.DataConfig,
342
+ action_horizon: int,
343
+ batch_size: int,
344
+ *,
345
+ sharding: jax.sharding.Sharding | None = None,
346
+ skip_norm_stats: bool = False,
347
+ shuffle: bool = False,
348
+ num_batches: int | None = None,
349
+ framework: str = "jax",
350
+ ) -> DataLoader[tuple[_model.Observation, _model.Actions]]:
351
+ """Create an RLDS data loader for training.
352
+
353
+ Note: This data loader requires some extra dependencies -- see examples/droid/README_train.md
354
+
355
+ Args:
356
+ data_config: The data configuration.
357
+ action_horizon: The action horizon.
358
+ batch_size: The batch size.
359
+ sharding: The sharding to use for the data loader. If None, the data loader will
360
+ use a single device sharding.
361
+ skip_norm_stats: Whether to skip data normalization.
362
+ shuffle: Whether to shuffle the data.
363
+ num_batches: Determines the number of batches to return. If the number exceeds the
364
+ number of batches in the dataset, the data loader will loop over the dataset.
365
+ If not provided, will iterate over the dataset indefinitely.
366
+ """
367
+ if framework == "pytorch":
368
+ raise NotImplementedError("PyTorch RLDS data loader is not supported yet")
369
+ dataset = create_rlds_dataset(data_config, action_horizon, batch_size, shuffle=shuffle)
370
+ dataset = transform_iterable_dataset(dataset, data_config, skip_norm_stats=skip_norm_stats, is_batched=True)
371
+
372
+ data_loader = RLDSDataLoader(
373
+ dataset,
374
+ sharding=sharding,
375
+ num_batches=num_batches,
376
+ )
377
+
378
+ return DataLoaderImpl(data_config, data_loader)
379
+
380
+
381
+ class TorchDataLoader:
382
+ """Torch data loader implementation."""
383
+
384
+ def __init__(
385
+ self,
386
+ dataset,
387
+ local_batch_size: int,
388
+ *,
389
+ sharding: jax.sharding.Sharding | None = None,
390
+ shuffle: bool = False,
391
+ sampler: torch.utils.data.Sampler | None = None,
392
+ num_batches: int | None = None,
393
+ num_workers: int = 0,
394
+ seed: int = 0,
395
+ framework: str = "jax",
396
+ ):
397
+ """Create a PyTorch data loader.
398
+
399
+ Args:
400
+ dataset: The dataset to load.
401
+ local_batch_size: The local batch size for each process.
402
+ sharding: The sharding to use for the data loader.
403
+ shuffle: Whether to shuffle the data.
404
+ num_batches: If provided, determines the number of returned batches. If the
405
+ number is larger than the number of batches in the dataset, the data loader
406
+ will loop over the dataset. If not provided, will iterate over the dataset
407
+ indefinitely.
408
+ num_workers: The number of worker processes to use. If zero, the data loader will
409
+ execute in the main process.
410
+ seed: The seed to use for shuffling the data.
411
+ """
412
+ if jax.process_count() > 1:
413
+ raise NotImplementedError("Data loading with multiple processes is not supported.")
414
+
415
+ if len(dataset) < local_batch_size:
416
+ raise ValueError(f"Local batch size ({local_batch_size}) is larger than the dataset size ({len(dataset)}).")
417
+
418
+ # Store sharding - None for PyTorch, JAX sharding for JAX
419
+ self._sharding = sharding
420
+ if sharding is None and framework == "jax":
421
+ # Use data parallel sharding by default for JAX only.
422
+ self._sharding = jax.sharding.NamedSharding(
423
+ jax.sharding.Mesh(jax.devices(), ("B",)),
424
+ jax.sharding.PartitionSpec("B"),
425
+ )
426
+ self._num_batches = num_batches
427
+
428
+ mp_context = None
429
+ if num_workers > 0:
430
+ mp_context = multiprocessing.get_context("spawn")
431
+
432
+ generator = torch.Generator()
433
+ generator.manual_seed(seed)
434
+ self._data_loader = torch.utils.data.DataLoader(
435
+ typing.cast(torch.utils.data.Dataset, dataset),
436
+ batch_size=local_batch_size,
437
+ shuffle=(sampler is None and shuffle), # Don't shuffle if using sampler
438
+ sampler=sampler,
439
+ num_workers=num_workers,
440
+ multiprocessing_context=mp_context,
441
+ persistent_workers=num_workers > 0,
442
+ collate_fn=_collate_fn,
443
+ worker_init_fn=_worker_init_fn,
444
+ drop_last=True,
445
+ generator=generator,
446
+ )
447
+
448
+ @property
449
+ def torch_loader(self) -> torch.utils.data.DataLoader:
450
+ return self._data_loader
451
+
452
+ def __iter__(self):
453
+ num_items = 0
454
+ while True:
455
+ data_iter = iter(self._data_loader)
456
+ while True:
457
+ if self._num_batches is not None and num_items >= self._num_batches:
458
+ return
459
+ try:
460
+ batch = next(data_iter)
461
+ except StopIteration:
462
+ break # We've exhausted the dataset. Create a new iterator and start over.
463
+ num_items += 1
464
+ # For JAX, convert to sharded arrays; for PyTorch, return torch tensors
465
+ if self._sharding is not None:
466
+ yield jax.tree.map(lambda x: jax.make_array_from_process_local_data(self._sharding, x), batch)
467
+ else:
468
+ yield jax.tree.map(torch.as_tensor, batch)
469
+
470
+
471
+ def _collate_fn(items):
472
+ """Collate the batch elements into batched numpy arrays."""
473
+ # Make sure to convert to numpy arrays before stacking since some of the incoming elements
474
+ # may be JAX arrays.
475
+ return jax.tree.map(lambda *xs: np.stack([np.asarray(x) for x in xs], axis=0), *items)
476
+
477
+
478
+ def _worker_init_fn(worker_id: int) -> None:
479
+ """Tell JAX inside the worker process not to preallocate the GPU memory."""
480
+ # NOTE: This is called after jax is imported inside the worker process. This
481
+ # means that this approach will not work for selecting the backend.
482
+ os.environ["XLA_PYTHON_CLIENT_PREALLOCATE"] = "false"
483
+ os.environ["XLA_PYTHON_CLIENT_ALLOCATOR"] = "platform"
484
+
485
+
486
+ class RLDSDataLoader:
487
+ """Shallow wrapper around the DROID data loader to make it compatible with openpi.
488
+
489
+ All batching already happens in the DROID dataset, so we don't need to do anything here.
490
+ """
491
+
492
+ def __init__(
493
+ self,
494
+ dataset: DroidRldsDataset,
495
+ *,
496
+ sharding: jax.sharding.Sharding | None = None,
497
+ num_batches: int | None = None,
498
+ ):
499
+ self._dataset = dataset
500
+ self._num_batches = num_batches
501
+
502
+ if jax.process_count() > 1:
503
+ raise NotImplementedError("Data loading with multiple processes is not supported.")
504
+
505
+ if sharding is None:
506
+ # Use data parallel sharding by default.
507
+ sharding = jax.sharding.NamedSharding(
508
+ jax.sharding.Mesh(jax.devices(), ("B",)),
509
+ jax.sharding.PartitionSpec("B"),
510
+ )
511
+
512
+ self._sharding = sharding
513
+ self._num_batches = num_batches
514
+
515
+ def __iter__(self):
516
+ num_items = 0
517
+ while True:
518
+ data_iter = iter(self._dataset)
519
+ while True:
520
+ if self._num_batches is not None and num_items >= self._num_batches:
521
+ return
522
+ try:
523
+ batch = next(data_iter)
524
+ except StopIteration:
525
+ break # We've exhausted the dataset. Create a new iterator and start over.
526
+ num_items += 1
527
+ yield jax.tree.map(lambda x: jax.make_array_from_process_local_data(self._sharding, x), batch)
528
+
529
+
530
+ class DataLoaderImpl(DataLoader):
531
+ def __init__(self, data_config: _config.DataConfig, data_loader: TorchDataLoader | RLDSDataLoader):
532
+ self._data_config = data_config
533
+ self._data_loader = data_loader
534
+
535
+ def data_config(self) -> _config.DataConfig:
536
+ return self._data_config
537
+
538
+ def __iter__(self):
539
+ for batch in self._data_loader:
540
+ yield _model.Observation.from_dict(batch), batch["actions"]
openpi_runtime/openpi/training/droid_rlds_dataset.py ADDED
@@ -0,0 +1,248 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RLDS-based data loader for DROID.
3
+ While openpi typically uses LeRobot's data loader, it is not currently scalable enough for larger datasets like DROID.
4
+ Thus, we provide a data loader example here that uses the RLDS data format.
5
+ The data loader also applies a few DROID-specific data filters / transformations.
6
+ """
7
+
8
+ from collections.abc import Sequence
9
+ import dataclasses
10
+ from enum import Enum
11
+ from enum import auto
12
+ import json
13
+ import logging
14
+ from pathlib import Path
15
+
16
+ import tqdm
17
+
18
+ import openpi.shared.download as download
19
+
20
+
21
+ class DroidActionSpace(Enum):
22
+ """Action space for DROID dataset."""
23
+
24
+ JOINT_POSITION = auto()
25
+ JOINT_VELOCITY = auto()
26
+
27
+
28
+ @dataclasses.dataclass
29
+ class RLDSDataset:
30
+ name: str
31
+ version: str
32
+ weight: float
33
+ filter_dict_path: str | None = None
34
+
35
+
36
+ class DroidRldsDataset:
37
+ def __init__(
38
+ self,
39
+ data_dir: str,
40
+ batch_size: int,
41
+ datasets: Sequence[RLDSDataset],
42
+ *, # Force keyword-only arguments
43
+ shuffle: bool = True,
44
+ action_chunk_size: int = 16,
45
+ # We default to joint position actions, since they allow policy evaluation in simulation.
46
+ action_space: DroidActionSpace = DroidActionSpace.JOINT_POSITION,
47
+ max_loaded_steps_per_episode: int = 100,
48
+ # Reduce this if you are running out of memory, but careful -- below ~100k shuffling is not sufficiently random.
49
+ shuffle_buffer_size: int = 250_000,
50
+ num_parallel_reads: int = -1, # -1 == tf.data.AUTOTUNE -- hack to not import tf at top level
51
+ num_parallel_calls: int = -1, # -1 == tf.data.AUTOTUNE -- hack to not import tf at top level
52
+ ):
53
+ # Import tensorflow here to not make it mandatory in case RLDS data loader is not used.
54
+ import dlimp as dl
55
+ import tensorflow as tf
56
+ import tensorflow_datasets as tfds
57
+
58
+ # Configure Tensorflow with *no GPU devices* (to prevent clobber with PyTorch / JAX)
59
+ tf.config.set_visible_devices([], "GPU")
60
+
61
+ # Ensure dataset weights sum to 1.0
62
+ assert sum(dataset.weight for dataset in datasets) == 1.0, "Dataset weights must sum to 1.0"
63
+
64
+ def prepare_single_dataset(dataset_cfg: RLDSDataset):
65
+ # ds_name, version = dataset_name.split(":")
66
+ ds_name, version = dataset_cfg.name, dataset_cfg.version
67
+ builder = tfds.builder(ds_name, data_dir=data_dir, version=version)
68
+ dataset = dl.DLataset.from_rlds(
69
+ builder, split="train", shuffle=shuffle, num_parallel_reads=num_parallel_reads
70
+ )
71
+
72
+ # Filter out any unsuccessful trajectories -- we use the file name to check this
73
+ dataset = dataset.filter(
74
+ lambda traj: tf.strings.regex_full_match(
75
+ traj["traj_metadata"]["episode_metadata"]["file_path"][0], ".*success.*"
76
+ )
77
+ )
78
+
79
+ # Repeat dataset so we never run out of data.
80
+ dataset = dataset.repeat()
81
+
82
+ # Load the filter dictionary if provided.
83
+ # The filter dictionary is a JSON file that maps episode keys to ranges of frames to sample
84
+ # (e.g.,
85
+ # {
86
+ # "<episode key>": [[0, 100], [200, 300]]
87
+ # }
88
+ # means keep frames 0-99 and 200-299).
89
+
90
+ filter_dict_path = dataset_cfg.filter_dict_path
91
+ if filter_dict_path is not None:
92
+ cached_filter_dict_path = download.maybe_download(filter_dict_path)
93
+ with Path(cached_filter_dict_path).open("r") as f:
94
+ filter_dict = json.load(f)
95
+ logging.info(f"Using filter dictionary with {len(filter_dict)} episodes")
96
+
97
+ keys_tensor = []
98
+ values_tensor = []
99
+
100
+ for episode_key, ranges in tqdm.tqdm(filter_dict.items(), desc="Creating idle filter hash table..."):
101
+ for start, end in ranges:
102
+ for t in range(start, end):
103
+ frame_key = f"{episode_key}--{t}"
104
+ keys_tensor.append(frame_key)
105
+ values_tensor.append(True)
106
+ self.filter_table = tf.lookup.StaticHashTable(
107
+ tf.lookup.KeyValueTensorInitializer(keys_tensor, values_tensor), default_value=False
108
+ )
109
+ logging.info("Filter hash table initialized")
110
+ else:
111
+ self.filter_table = tf.lookup.StaticHashTable(
112
+ tf.lookup.KeyValueTensorInitializer([""], [True]), default_value=True
113
+ )
114
+
115
+ def restructure(traj):
116
+ """Reformat observation and action keys, sample language instruction."""
117
+ # Important: we use joint *position* action space -- easier to simulate!
118
+ actions = tf.concat(
119
+ (
120
+ (
121
+ traj["action_dict"]["joint_position"]
122
+ if action_space == DroidActionSpace.JOINT_POSITION
123
+ else traj["action_dict"]["joint_velocity"]
124
+ ),
125
+ traj["action_dict"]["gripper_position"],
126
+ ),
127
+ axis=-1,
128
+ )
129
+ # Randomly samples one of the two exterior images in DROID during training (we only train with one at a time).
130
+ # Note: the "left" refers to the left camera in the stereo pair, we only train on the left camera.
131
+ exterior_img = tf.cond(
132
+ tf.random.uniform(shape=[]) > 0.5,
133
+ lambda: traj["observation"]["exterior_image_1_left"],
134
+ lambda: traj["observation"]["exterior_image_2_left"],
135
+ )
136
+ wrist_img = traj["observation"]["wrist_image_left"]
137
+ # Randomly sample one of the three language instructions
138
+ instruction = tf.random.shuffle(
139
+ [traj["language_instruction"], traj["language_instruction_2"], traj["language_instruction_3"]]
140
+ )[0]
141
+
142
+ traj_len = tf.shape(traj["action"])[0]
143
+ indices = tf.as_string(tf.range(traj_len))
144
+
145
+ # Data filtering:
146
+ # Compute a uniquely-identifying step ID by concatenating the recording folderpath, file path,
147
+ # and each step's time step index. This will index into the filter hash table, and if it returns true,
148
+ # then the frame passes the filter.
149
+ step_id = (
150
+ traj["traj_metadata"]["episode_metadata"]["recording_folderpath"]
151
+ + "--"
152
+ + traj["traj_metadata"]["episode_metadata"]["file_path"]
153
+ + "--"
154
+ + indices
155
+ )
156
+ passes_filter = self.filter_table.lookup(step_id)
157
+
158
+ return {
159
+ "actions": actions,
160
+ "observation": {
161
+ "image": exterior_img,
162
+ "wrist_image": wrist_img,
163
+ "joint_position": traj["observation"]["joint_position"],
164
+ "gripper_position": traj["observation"]["gripper_position"],
165
+ },
166
+ "prompt": instruction,
167
+ "step_id": step_id,
168
+ "passes_filter": passes_filter,
169
+ }
170
+
171
+ dataset = dataset.traj_map(restructure, num_parallel_calls)
172
+
173
+ def chunk_actions(traj):
174
+ """Splits episode into action chunks."""
175
+ traj_len = tf.shape(traj["actions"])[0]
176
+
177
+ # For each step in the trajectory, construct indices for the next n actions
178
+ action_chunk_indices = tf.broadcast_to(
179
+ tf.range(action_chunk_size)[None],
180
+ [traj_len, action_chunk_size],
181
+ ) + tf.broadcast_to(
182
+ tf.range(traj_len)[:, None],
183
+ [traj_len, action_chunk_size],
184
+ )
185
+
186
+ # Cap to length of the sequence --> final chunks will repeat the last action
187
+ # This makes sense, since we are using absolute joint + gripper position actions
188
+ action_chunk_indices = tf.minimum(action_chunk_indices, traj_len - 1)
189
+
190
+ # Gather the actions for each chunk
191
+ traj["actions"] = tf.gather(traj["actions"], action_chunk_indices)
192
+ return traj
193
+
194
+ dataset = dataset.traj_map(chunk_actions, num_parallel_calls)
195
+
196
+ # Flatten: map from trajectory dataset to dataset of individual action chunks
197
+ dataset = dataset.flatten(num_parallel_calls=num_parallel_calls)
198
+
199
+ # Filter data that doesn't pass the filter
200
+ def filter_from_dict(frame):
201
+ return frame["passes_filter"]
202
+
203
+ dataset = dataset.filter(filter_from_dict)
204
+
205
+ # Remove "passes_filter" key from output
206
+ def remove_passes_filter(frame):
207
+ frame.pop("passes_filter")
208
+ return frame
209
+
210
+ dataset = dataset.map(remove_passes_filter)
211
+
212
+ # Decode images: RLDS saves encoded images, only decode now for efficiency
213
+ def decode_images(traj):
214
+ traj["observation"]["image"] = tf.io.decode_image(
215
+ traj["observation"]["image"], expand_animations=False, dtype=tf.uint8
216
+ )
217
+ traj["observation"]["wrist_image"] = tf.io.decode_image(
218
+ traj["observation"]["wrist_image"], expand_animations=False, dtype=tf.uint8
219
+ )
220
+ return traj
221
+
222
+ return dataset.frame_map(decode_images, num_parallel_calls)
223
+
224
+ logging.info(f"Preparing {len(datasets)} datasets...")
225
+ logging.info("-" * 50)
226
+ for dataset in datasets:
227
+ logging.info(f" {dataset.name}:{dataset.version} with weight {dataset.weight:.2f}")
228
+ logging.info("-" * 50)
229
+ all_datasets = [prepare_single_dataset(dataset) for dataset in datasets]
230
+ weights = [dataset.weight for dataset in datasets]
231
+
232
+ final_dataset = dl.DLataset.sample_from_datasets(all_datasets, weights=weights)
233
+ final_dataset = final_dataset.shuffle(shuffle_buffer_size)
234
+ final_dataset = final_dataset.batch(batch_size)
235
+ # Note =>> Seems to reduce memory usage without affecting speed?
236
+ final_dataset = final_dataset.with_ram_budget(1)
237
+
238
+ self.dataset = final_dataset
239
+ self.batch_size = batch_size
240
+ self.shuffle = shuffle
241
+
242
+ def __iter__(self):
243
+ yield from self.dataset.as_numpy_iterator()
244
+
245
+ def __len__(self):
246
+ # This is the approximate number of samples in DROID after filtering.
247
+ # Easier to hardcode than to iterate through the dataset and compute it.
248
+ return 20_000_000
openpi_runtime/openpi/training/misc/polaris_config.py ADDED
@@ -0,0 +1,225 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """PolaRiS baseline policy configs."""
2
+
3
+ from typing import TypeAlias
4
+
5
+ import openpi.models.model as _model
6
+ import openpi.models.pi0_config as pi0_config
7
+ import openpi.models.pi0_fast as pi0_fast
8
+ import openpi.models.tokenizer as _tokenizer
9
+ import openpi.policies.droid_policy as droid_policy
10
+ import openpi.training.droid_rlds_dataset as droid_rlds_dataset
11
+ import openpi.training.optimizer as _optimizer
12
+ import openpi.training.weight_loaders as weight_loaders
13
+ import openpi.transforms as _transforms
14
+
15
+ ModelType: TypeAlias = _model.ModelType
16
+
17
+
18
+ def get_polaris_configs():
19
+ # Import here to avoid circular imports.
20
+ from openpi.training.config import AssetsConfig
21
+ from openpi.training.config import RLDSDroidDataConfig
22
+ from openpi.training.config import SimpleDataConfig
23
+ from openpi.training.config import TrainConfig
24
+
25
+ return [
26
+ #
27
+ # PolaRiS DROID jointpos policies
28
+ #
29
+ TrainConfig(
30
+ name="pi05_droid_jointpos_polaris",
31
+ model=pi0_config.Pi0Config(action_horizon=15, pi05=True),
32
+ data=RLDSDroidDataConfig(
33
+ assets=AssetsConfig(
34
+ assets_dir="gs://openpi-assets/checkpoints/polaris/pi05_droid_jointpos_polaris/assets",
35
+ asset_id="droid",
36
+ ),
37
+ datasets=(
38
+ droid_rlds_dataset.RLDSDataset(
39
+ name="droid",
40
+ version="1.0.1",
41
+ weight=0.9,
42
+ filter_dict_path="gs://openpi-assets/droid/droid_sample_ranges_v1_0_1.json",
43
+ ),
44
+ droid_rlds_dataset.RLDSDataset(
45
+ name="polaris_droid_cotrain_dataset",
46
+ version="1.0.0",
47
+ weight=0.1,
48
+ filter_dict_path="gs://openpi-assets/droid/polaris_droid_cotrain_dataset_sample_ranges_v1_0_0.json",
49
+ ),
50
+ ),
51
+ rlds_data_dir="<path_to_droid_rlds_dataset>",
52
+ action_space=droid_rlds_dataset.DroidActionSpace.JOINT_POSITION,
53
+ ),
54
+ weight_loader=weight_loaders.CheckpointWeightLoader(
55
+ "gs://openpi-assets/checkpoints/polaris/pi05_droid_jointpos_polaris/params"
56
+ ),
57
+ lr_schedule=_optimizer.CosineDecaySchedule(
58
+ warmup_steps=1_000,
59
+ peak_lr=5e-5,
60
+ decay_steps=1_000_000,
61
+ decay_lr=5e-5,
62
+ ),
63
+ num_train_steps=1_000,
64
+ batch_size=128,
65
+ log_interval=100,
66
+ save_interval=1000,
67
+ keep_period=1000,
68
+ num_workers=0, # Important: RLDS DataLoader requires num_workers=0, handles multi-processing internally
69
+ ),
70
+ TrainConfig(
71
+ name="pi0_fast_droid_jointpos_polaris",
72
+ model=pi0_fast.Pi0FASTConfig(
73
+ action_dim=8,
74
+ action_horizon=10,
75
+ max_token_len=180,
76
+ ),
77
+ data=RLDSDroidDataConfig(
78
+ assets=AssetsConfig(
79
+ assets_dir="gs://openpi-assets/checkpoints/polaris/pi0_fast_droid_jointpos_polaris/assets",
80
+ asset_id="droid",
81
+ ),
82
+ datasets=(
83
+ droid_rlds_dataset.RLDSDataset(
84
+ name="droid",
85
+ version="1.0.1",
86
+ weight=0.9,
87
+ filter_dict_path="gs://openpi-assets/droid/droid_sample_ranges_v1_0_1.json",
88
+ ),
89
+ droid_rlds_dataset.RLDSDataset(
90
+ name="polaris_droid_cotrain_dataset",
91
+ version="1.0.0",
92
+ weight=0.1,
93
+ filter_dict_path="gs://openpi-assets/droid/polaris_droid_cotrain_dataset_sample_ranges_v1_0_0.json",
94
+ ),
95
+ ),
96
+ rlds_data_dir="<path_to_droid_rlds_dataset>",
97
+ action_space=droid_rlds_dataset.DroidActionSpace.JOINT_POSITION,
98
+ ),
99
+ weight_loader=weight_loaders.CheckpointWeightLoader(
100
+ "gs://openpi-assets/checkpoints/polaris/pi0_fast_droid_jointpos_polaris/params"
101
+ ),
102
+ lr_schedule=_optimizer.CosineDecaySchedule(
103
+ warmup_steps=1_000,
104
+ peak_lr=5e-5,
105
+ decay_steps=1_000_000,
106
+ decay_lr=5e-5,
107
+ ),
108
+ num_train_steps=1_000,
109
+ batch_size=128,
110
+ log_interval=100,
111
+ save_interval=1000,
112
+ keep_period=1000,
113
+ num_workers=0, # Important: RLDS DataLoader requires num_workers=0, handles multi-processing internally
114
+ ),
115
+ TrainConfig(
116
+ name="pi0_droid_jointpos_polaris",
117
+ model=pi0_config.Pi0Config(
118
+ # action_dim=8, # leave as 32 default...
119
+ action_horizon=10,
120
+ max_token_len=100,
121
+ ),
122
+ data=RLDSDroidDataConfig(
123
+ assets=AssetsConfig(
124
+ assets_dir="gs://openpi-assets/checkpoints/polaris/pi0_droid_jointpos_polaris/assets",
125
+ asset_id="droid",
126
+ ),
127
+ datasets=(
128
+ droid_rlds_dataset.RLDSDataset(
129
+ name="droid",
130
+ version="1.0.1",
131
+ weight=0.9,
132
+ filter_dict_path="gs://openpi-assets/droid/droid_sample_ranges_v1_0_1.json",
133
+ ),
134
+ droid_rlds_dataset.RLDSDataset(
135
+ name="polaris_droid_cotrain_dataset",
136
+ version="1.0.0",
137
+ weight=0.1,
138
+ filter_dict_path="gs://openpi-assets/droid/polaris_droid_cotrain_dataset_sample_ranges_v1_0_0.json",
139
+ ),
140
+ ),
141
+ rlds_data_dir="<path_to_droid_rlds_dataset>",
142
+ action_space=droid_rlds_dataset.DroidActionSpace.JOINT_POSITION,
143
+ ),
144
+ weight_loader=weight_loaders.CheckpointWeightLoader(
145
+ "gs://openpi-assets/checkpoints/polaris/pi0_droid_jointpos_polaris/params"
146
+ ),
147
+ lr_schedule=_optimizer.CosineDecaySchedule(
148
+ warmup_steps=1_000,
149
+ peak_lr=5e-5,
150
+ decay_steps=1_000_000,
151
+ decay_lr=5e-5,
152
+ ),
153
+ num_train_steps=1_000,
154
+ batch_size=128,
155
+ log_interval=100,
156
+ save_interval=1000,
157
+ keep_period=1000,
158
+ num_workers=0, # Important: RLDS DataLoader requires num_workers=0, handles multi-processing internally
159
+ ),
160
+ TrainConfig(
161
+ name="pi0_droid_jointpos_100k_polaris",
162
+ model=pi0_config.Pi0Config(
163
+ # action_dim=8, # leave as 32 default...
164
+ action_horizon=10,
165
+ max_token_len=100,
166
+ ),
167
+ data=RLDSDroidDataConfig(
168
+ assets=AssetsConfig(
169
+ assets_dir="gs://openpi-assets/checkpoints/polaris/pi0_droid_jointpos_100k_polaris/assets",
170
+ asset_id="droid",
171
+ ),
172
+ datasets=(
173
+ droid_rlds_dataset.RLDSDataset(
174
+ name="droid",
175
+ version="1.0.1",
176
+ weight=0.9,
177
+ filter_dict_path="gs://openpi-assets/droid/droid_sample_ranges_v1_0_1.json",
178
+ ),
179
+ droid_rlds_dataset.RLDSDataset(
180
+ name="polaris_droid_cotrain_dataset",
181
+ version="1.0.0",
182
+ weight=0.1,
183
+ filter_dict_path="gs://openpi-assets/droid/polaris_droid_cotrain_dataset_sample_ranges_v1_0_0.json",
184
+ ),
185
+ ),
186
+ rlds_data_dir="<path_to_droid_rlds_dataset>",
187
+ action_space=droid_rlds_dataset.DroidActionSpace.JOINT_POSITION,
188
+ ),
189
+ weight_loader=weight_loaders.CheckpointWeightLoader(
190
+ "gs://openpi-assets/checkpoints/polaris/pi0_droid_jointpos_100k_polaris/params"
191
+ ),
192
+ lr_schedule=_optimizer.CosineDecaySchedule(
193
+ warmup_steps=1_000,
194
+ peak_lr=5e-5,
195
+ decay_steps=1_000_000,
196
+ decay_lr=5e-5,
197
+ ),
198
+ num_train_steps=1_000,
199
+ batch_size=128,
200
+ log_interval=100,
201
+ save_interval=1000,
202
+ keep_period=1000,
203
+ num_workers=0, # Important: RLDS DataLoader requires num_workers=0, handles multi-processing internally
204
+ ),
205
+ # openpi doesn't support finetuning of binning policies, so this is an inference-only config
206
+ TrainConfig(
207
+ name="paligemma_binning_droid_jointpos",
208
+ model=pi0_fast.Pi0FASTConfig(
209
+ action_dim=8,
210
+ action_horizon=15,
211
+ max_token_len=600,
212
+ fast_model_tokenizer=_tokenizer.BinningTokenizer,
213
+ ),
214
+ data=SimpleDataConfig(
215
+ assets=AssetsConfig(asset_id="droid"),
216
+ data_transforms=lambda model: _transforms.Group(
217
+ inputs=[droid_policy.DroidInputs(model_type=ModelType.PI0_FAST)],
218
+ outputs=[
219
+ _transforms.AbsoluteActions(_transforms.make_bool_mask(7, -1)),
220
+ droid_policy.DroidOutputs(),
221
+ ],
222
+ ),
223
+ ),
224
+ ),
225
+ ]
openpi_runtime/openpi/training/misc/roboarena_config.py ADDED
@@ -0,0 +1,116 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """RoboArena baseline policy configs."""
2
+
3
+ from typing import TypeAlias
4
+
5
+ import openpi.models.model as _model
6
+ import openpi.models.pi0_config as pi0_config
7
+ import openpi.models.pi0_fast as pi0_fast
8
+ import openpi.models.tokenizer as _tokenizer
9
+ import openpi.policies.droid_policy as droid_policy
10
+ import openpi.transforms as _transforms
11
+
12
+ ModelType: TypeAlias = _model.ModelType
13
+
14
+
15
+ def get_roboarena_configs():
16
+ # Import here to avoid circular imports.
17
+ from openpi.training.config import AssetsConfig
18
+ from openpi.training.config import DataConfig
19
+ from openpi.training.config import SimpleDataConfig
20
+ from openpi.training.config import TrainConfig
21
+
22
+ return [
23
+ #
24
+ # RoboArena DROID baseline inference configs.
25
+ #
26
+ TrainConfig(
27
+ # Trained from PaliGemma, using RT-2 / OpenVLA style binning tokenizer.
28
+ name="paligemma_binning_droid",
29
+ model=pi0_fast.Pi0FASTConfig(
30
+ action_dim=8,
31
+ action_horizon=15,
32
+ max_token_len=400,
33
+ fast_model_tokenizer=_tokenizer.BinningTokenizer,
34
+ ),
35
+ data=SimpleDataConfig(
36
+ assets=AssetsConfig(asset_id="droid"),
37
+ data_transforms=lambda model: _transforms.Group(
38
+ inputs=[droid_policy.DroidInputs(action_dim=model.action_dim, model_type=ModelType.PI0_FAST)],
39
+ outputs=[droid_policy.DroidOutputs()],
40
+ ),
41
+ base_config=DataConfig(
42
+ prompt_from_task=True,
43
+ ),
44
+ ),
45
+ ),
46
+ TrainConfig(
47
+ # Trained from PaliGemma, using FAST tokenizer (using universal FAST+ tokenizer).
48
+ name="paligemma_fast_droid",
49
+ model=pi0_fast.Pi0FASTConfig(action_dim=8, action_horizon=15),
50
+ data=SimpleDataConfig(
51
+ assets=AssetsConfig(asset_id="droid"),
52
+ data_transforms=lambda model: _transforms.Group(
53
+ inputs=[droid_policy.DroidInputs(action_dim=model.action_dim, model_type=ModelType.PI0_FAST)],
54
+ outputs=[droid_policy.DroidOutputs()],
55
+ ),
56
+ base_config=DataConfig(
57
+ prompt_from_task=True,
58
+ ),
59
+ ),
60
+ ),
61
+ TrainConfig(
62
+ # Trained from PaliGemma, using FAST tokenizer (tokenizer trained on DROID dataset).
63
+ name="paligemma_fast_specialist_droid",
64
+ model=pi0_fast.Pi0FASTConfig(
65
+ action_dim=8,
66
+ action_horizon=15,
67
+ fast_model_tokenizer=_tokenizer.FASTTokenizer,
68
+ fast_model_tokenizer_kwargs={"fast_tokenizer_path": "KarlP/fast_droid_specialist"},
69
+ ),
70
+ data=SimpleDataConfig(
71
+ assets=AssetsConfig(asset_id="droid"),
72
+ data_transforms=lambda model: _transforms.Group(
73
+ inputs=[droid_policy.DroidInputs(action_dim=model.action_dim, model_type=ModelType.PI0_FAST)],
74
+ outputs=[droid_policy.DroidOutputs()],
75
+ ),
76
+ base_config=DataConfig(
77
+ prompt_from_task=True,
78
+ ),
79
+ ),
80
+ ),
81
+ TrainConfig(
82
+ # Trained from PaliGemma, using FSQ tokenizer.
83
+ name="paligemma_vq_droid",
84
+ model=pi0_fast.Pi0FASTConfig(
85
+ action_dim=8,
86
+ action_horizon=15,
87
+ fast_model_tokenizer=_tokenizer.FSQTokenizer,
88
+ fast_model_tokenizer_kwargs={"fsq_tokenizer_path": "gs://openpi-assets/tokenizers/droid_fsq_tokenizer"},
89
+ ),
90
+ data=SimpleDataConfig(
91
+ assets=AssetsConfig(asset_id="droid"),
92
+ data_transforms=lambda model: _transforms.Group(
93
+ inputs=[droid_policy.DroidInputs(action_dim=model.action_dim, model_type=ModelType.PI0_FAST)],
94
+ outputs=[droid_policy.DroidOutputs()],
95
+ ),
96
+ base_config=DataConfig(
97
+ prompt_from_task=True,
98
+ ),
99
+ ),
100
+ ),
101
+ TrainConfig(
102
+ # pi0-style diffusion / flow VLA, trained on DROID from PaliGemma.
103
+ name="paligemma_diffusion_droid",
104
+ model=pi0_config.Pi0Config(action_horizon=10, action_dim=8),
105
+ data=SimpleDataConfig(
106
+ assets=AssetsConfig(asset_id="droid"),
107
+ data_transforms=lambda model: _transforms.Group(
108
+ inputs=[droid_policy.DroidInputs(action_dim=model.action_dim)],
109
+ outputs=[droid_policy.DroidOutputs()],
110
+ ),
111
+ base_config=DataConfig(
112
+ prompt_from_task=True,
113
+ ),
114
+ ),
115
+ ),
116
+ ]
openpi_runtime/openpi/training/optimizer.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+ from typing import Protocol, runtime_checkable
3
+
4
+ import jax.numpy as jnp
5
+ import optax
6
+
7
+ import openpi.shared.array_typing as at
8
+
9
+
10
+ @runtime_checkable
11
+ class LRScheduleConfig(Protocol):
12
+ def create(self) -> optax.Schedule: ...
13
+
14
+
15
+ @dataclasses.dataclass(frozen=True)
16
+ class CosineDecaySchedule(LRScheduleConfig):
17
+ """Cosine decay schedule with warmup."""
18
+
19
+ warmup_steps: int = 1_000
20
+ peak_lr: float = 2.5e-5
21
+ decay_steps: int = 30_000
22
+ decay_lr: float = 2.5e-6
23
+
24
+ def create(self) -> optax.Schedule:
25
+ return optax.warmup_cosine_decay_schedule(
26
+ init_value=self.peak_lr / (self.warmup_steps + 1),
27
+ peak_value=self.peak_lr,
28
+ warmup_steps=self.warmup_steps,
29
+ decay_steps=self.decay_steps,
30
+ end_value=self.decay_lr,
31
+ )
32
+
33
+
34
+ @dataclasses.dataclass(frozen=True)
35
+ class RsqrtDecaySchedule(LRScheduleConfig):
36
+ """Inverse square root decay schedule with warmup."""
37
+
38
+ warmup_steps: int = 1_000
39
+ peak_lr: float = 5e-5
40
+ timescale: float = 10_000
41
+
42
+ def create(self) -> optax.Schedule:
43
+ return optax.join_schedules(
44
+ [
45
+ optax.linear_schedule(
46
+ init_value=self.peak_lr / (self.warmup_steps + 1),
47
+ end_value=self.peak_lr,
48
+ transition_steps=self.warmup_steps,
49
+ ),
50
+ lambda step: self.peak_lr / jnp.sqrt((self.timescale + step) / self.timescale),
51
+ ],
52
+ [self.warmup_steps],
53
+ )
54
+
55
+
56
+ @runtime_checkable
57
+ class OptimizerConfig(Protocol):
58
+ def create(
59
+ self,
60
+ lr: optax.ScalarOrSchedule,
61
+ weight_decay_mask: at.PyTree | None = None,
62
+ ) -> optax.GradientTransformation: ...
63
+
64
+
65
+ @dataclasses.dataclass(frozen=True)
66
+ class AdamW(OptimizerConfig):
67
+ """AdamW optimizer."""
68
+
69
+ b1: float = 0.9
70
+ b2: float = 0.95
71
+ eps: float = 1e-8
72
+ # Changing this to 0 can cause out-of-memory errors for some reason, so we set it to a negligible value.
73
+ weight_decay: float = 1e-10
74
+ clip_gradient_norm: float = 1.0
75
+
76
+ def create(
77
+ self,
78
+ lr: optax.ScalarOrSchedule,
79
+ weight_decay_mask: at.PyTree | None = None,
80
+ ) -> optax.GradientTransformation:
81
+ tx = optax.adamw(
82
+ lr, b1=self.b1, b2=self.b2, eps=self.eps, weight_decay=self.weight_decay, mask=weight_decay_mask
83
+ )
84
+
85
+ return optax.chain(optax.clip_by_global_norm(self.clip_gradient_norm), tx)
86
+
87
+
88
+ @dataclasses.dataclass(frozen=True)
89
+ class SGD(OptimizerConfig):
90
+ """SGD optimizer."""
91
+
92
+ lr: float = 5e-5
93
+ momentum: float = 0.9
94
+ nesterov: bool = False
95
+
96
+ def create(
97
+ self,
98
+ lr: optax.ScalarOrSchedule,
99
+ weight_decay_mask: at.PyTree | None = None,
100
+ ) -> optax.GradientTransformation:
101
+ assert weight_decay_mask is None, "Weight decay is not supported for SGD"
102
+ return optax.sgd(lr, momentum=self.momentum, nesterov=self.nesterov)
103
+
104
+
105
+ def create_optimizer(
106
+ optimizer: OptimizerConfig, lr_schedule: LRScheduleConfig, weight_decay_mask: at.PyTree | None = None
107
+ ) -> optax.GradientTransformation:
108
+ lr = lr_schedule.create()
109
+ return optimizer.create(lr, weight_decay_mask=weight_decay_mask)
openpi_runtime/openpi/training/sharding.py ADDED
@@ -0,0 +1,102 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import contextlib
2
+ import logging
3
+
4
+ import jax
5
+ import numpy as np
6
+
7
+ BATCH_AXIS = "batch"
8
+ FSDP_AXIS = "fsdp"
9
+ # In FSDP, we shard the data across both the batch and FSDP axes.
10
+ DATA_AXIS = (BATCH_AXIS, FSDP_AXIS)
11
+
12
+
13
+ class _MeshState:
14
+ active_mesh: jax.sharding.Mesh | None = None
15
+
16
+
17
+ def make_mesh(num_fsdp_devices: int) -> jax.sharding.Mesh:
18
+ if jax.device_count() % num_fsdp_devices != 0:
19
+ raise ValueError(
20
+ f"Number of devices {jax.device_count()} must be divisible by the number of FSDP devices {num_fsdp_devices}."
21
+ )
22
+ mesh_shape = (jax.device_count() // num_fsdp_devices, num_fsdp_devices)
23
+ return jax.make_mesh(mesh_shape, (BATCH_AXIS, FSDP_AXIS))
24
+
25
+
26
+ @contextlib.contextmanager
27
+ def set_mesh(mesh: jax.sharding.Mesh):
28
+ """Plumbing the mesh deep into the module tree is extremely cumbersome; until the JAX team lands a better API, a
29
+ custom context manager like this one is the recommended way to maintain a reference to a global mesh. This is only used
30
+ in `activation_sharding_constraint` below."""
31
+ if _MeshState.active_mesh is not None:
32
+ raise ValueError("Cannot nest set_mesh context managers.")
33
+ _MeshState.active_mesh = mesh
34
+ try:
35
+ yield
36
+ finally:
37
+ _MeshState.active_mesh = None
38
+
39
+
40
+ def activation_sharding_constraint(pytree):
41
+ if _MeshState.active_mesh is None:
42
+ return pytree
43
+ return jax.lax.with_sharding_constraint(
44
+ pytree, jax.sharding.NamedSharding(_MeshState.active_mesh, jax.sharding.PartitionSpec(DATA_AXIS))
45
+ )
46
+
47
+
48
+ def fsdp_sharding(
49
+ pytree,
50
+ mesh: jax.sharding.Mesh,
51
+ *,
52
+ min_size_mbytes: int = 4, # 4 MiB
53
+ log: bool = False,
54
+ ):
55
+ """Apply FSDP sharding to a pytree of arrays based on the mesh shape.
56
+
57
+ Args:
58
+ pytree: A pytree to be apply sharding specified by the mesh, note that only array types (eg. contains .shape attr)
59
+ will be considered for sharding.
60
+ mesh: The mesh being used for applying sharding on to pytree.
61
+ min_size_mbytes: The minimum size of the array in MiB to be considered for sharding, any array smaller than this
62
+ will be replicated.
63
+ log: If true, will log the sharding decisions for arrays that are being considered for sharding.
64
+
65
+ Returns:
66
+ The sharded pytree.
67
+ """
68
+ min_size_bytes = min_size_mbytes * 2**20
69
+
70
+ def _shard_arr(kp, array: jax.ShapeDtypeStruct):
71
+ # if fsdp is not actually going to be used, replicate everything to avoid extraneous logging
72
+ if mesh.shape[FSDP_AXIS] == 1:
73
+ return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
74
+ # replicate scalar and vector arrays
75
+ if not hasattr(array, "shape"):
76
+ return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
77
+ if len(array.shape) < 2:
78
+ return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
79
+ # replicate small arrays
80
+ if (arr_size := np.prod(array.shape) * np.dtype(array.dtype).itemsize) < min_size_bytes:
81
+ return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
82
+
83
+ # shard matrices and larger tensors along the largest axis that is divisible by the fsdp dimension
84
+ axes = np.argsort(array.shape)[::-1]
85
+ spec = [None] * len(axes)
86
+ for i in axes:
87
+ if array.shape[i] % mesh.shape[FSDP_AXIS] == 0:
88
+ if log:
89
+ logging.info(
90
+ f"Sharding {jax.tree_util.keystr(kp)} of shape {array.shape} ({arr_size / 2**20:.2f} MiB) along axis {i}"
91
+ )
92
+ spec[i] = FSDP_AXIS
93
+ return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec(*spec))
94
+
95
+ # replicate if no valid sharding was found
96
+ if log:
97
+ logging.warning(
98
+ f"Could not find a valid sharding for {jax.tree_util.keystr(kp)} of shape {array.shape} with mesh of shape {mesh.shape}"
99
+ )
100
+ return jax.sharding.NamedSharding(mesh, jax.sharding.PartitionSpec())
101
+
102
+ return jax.tree_util.tree_map_with_path(_shard_arr, pytree)
openpi_runtime/openpi/training/utils.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Callable
2
+ from typing import Any
3
+
4
+ from flax import nnx
5
+ from flax import struct
6
+ import jax
7
+ import optax
8
+
9
+ from openpi.models import model as _model
10
+ from openpi.shared import array_typing as at
11
+
12
+
13
+ @at.typecheck
14
+ @struct.dataclass
15
+ class TrainState:
16
+ step: at.Int[at.ArrayLike, ""]
17
+ params: nnx.State
18
+ model_def: nnx.GraphDef[_model.BaseModel]
19
+ opt_state: optax.OptState
20
+ tx: optax.GradientTransformation = struct.field(pytree_node=False)
21
+
22
+ ema_decay: float | None = struct.field(pytree_node=False)
23
+ ema_params: nnx.State | None = None
24
+
25
+
26
+ @at.typecheck
27
+ def tree_to_info(tree: at.PyTree, interp_func: Callable[[Any], str] = str) -> str:
28
+ """Converts a PyTree into a human-readable string for logging. Optionally, `interp_func` can be provided to convert
29
+ the leaf values to more meaningful strings.
30
+ """
31
+ tree, _ = jax.tree_util.tree_flatten_with_path(tree)
32
+ return "\n".join(f"{jax.tree_util.keystr(path)}: {interp_func(value)}" for path, value in tree)
33
+
34
+
35
+ @at.typecheck
36
+ def array_tree_to_info(tree: at.PyTree) -> str:
37
+ """Converts a PyTree of arrays into a human-readable string for logging."""
38
+ return tree_to_info(tree, lambda x: f"{x.shape}@{x.dtype}")
openpi_runtime/openpi/training/weight_loaders.py ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import dataclasses
2
+ import logging
3
+ import re
4
+ from typing import Protocol, runtime_checkable
5
+
6
+ import flax.traverse_util
7
+ import numpy as np
8
+
9
+ import openpi.models.model as _model
10
+ import openpi.shared.array_typing as at
11
+ import openpi.shared.download as download
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ @runtime_checkable
17
+ class WeightLoader(Protocol):
18
+ def load(self, params: at.Params) -> at.Params:
19
+ """Loads the model weights.
20
+
21
+ Args:
22
+ params: Parameters of the model. This is a nested structure of array-like objects that
23
+ represent the model's parameters.
24
+
25
+ Returns:
26
+ Loaded parameters. The structure must be identical to `params`. If returning a subset of
27
+ the parameters the loader must merge the loaded parameters with `params`.
28
+ """
29
+
30
+
31
+ @dataclasses.dataclass(frozen=True)
32
+ class NoOpWeightLoader(WeightLoader):
33
+ def load(self, params: at.Params) -> at.Params:
34
+ return params
35
+
36
+
37
+ @dataclasses.dataclass(frozen=True)
38
+ class CheckpointWeightLoader(WeightLoader):
39
+ """Loads an entire set of weights from a checkpoint.
40
+
41
+ Compatible with:
42
+ trained checkpoints:
43
+ example: "./checkpoints/<config>/<exp>/<step>/params"
44
+ released checkpoints:
45
+ example: "gs://openpi-assets/checkpoints/<model>/params"
46
+ """
47
+
48
+ params_path: str
49
+
50
+ def load(self, params: at.Params) -> at.Params:
51
+ # We are loading np.ndarray and relying on the training code to properly convert and shard the params.
52
+ loaded_params = _model.restore_params(download.maybe_download(self.params_path), restore_type=np.ndarray)
53
+ # Add all missing LoRA weights.
54
+ return _merge_params(loaded_params, params, missing_regex=".*lora.*")
55
+
56
+
57
+ @dataclasses.dataclass(frozen=True)
58
+ class PaliGemmaWeightLoader(WeightLoader):
59
+ """Loads weights from the official PaliGemma checkpoint.
60
+
61
+ This will overwrite existing weights with similar names while keeping all extra weights intact.
62
+ This allows us to support the action expert which is used by the Pi0 model.
63
+ """
64
+
65
+ def load(self, params: at.Params) -> at.Params:
66
+ path = download.maybe_download(
67
+ "gs://vertex-model-garden-paligemma-us/paligemma/pt_224.npz", gs={"token": "anon"}
68
+ )
69
+ with path.open("rb") as f:
70
+ flat_params = dict(np.load(f, allow_pickle=False))
71
+ loaded_params = {"PaliGemma": flax.traverse_util.unflatten_dict(flat_params, sep="/")["params"]}
72
+ # Add all missing weights.
73
+ return _merge_params(loaded_params, params, missing_regex=".*")
74
+
75
+
76
+ def _merge_params(loaded_params: at.Params, params: at.Params, *, missing_regex: str) -> at.Params:
77
+ """Merges the loaded parameters with the reference parameters.
78
+
79
+ Args:
80
+ loaded_params: The parameters to merge.
81
+ params: The reference parameters.
82
+ missing_regex: A regex pattern for all missing keys that should be merged from the reference parameters.
83
+
84
+ Returns:
85
+ A new dictionary with the merged parameters.
86
+ """
87
+ flat_ref = flax.traverse_util.flatten_dict(params, sep="/")
88
+ flat_loaded = flax.traverse_util.flatten_dict(loaded_params, sep="/")
89
+
90
+ # First, take all weights that are a subset of the reference weights.
91
+ result = {}
92
+ for k, v in flat_loaded.items():
93
+ if k in flat_ref:
94
+ result[k] = v.astype(flat_ref[k].dtype) if v.dtype != flat_ref[k].dtype else v
95
+
96
+ flat_loaded.clear()
97
+
98
+ # Then, merge any missing weights as defined by the missing regex.
99
+ pattern = re.compile(missing_regex)
100
+ for k in {k for k in flat_ref if pattern.fullmatch(k)}:
101
+ if k not in result:
102
+ result[k] = flat_ref[k]
103
+
104
+ return flax.traverse_util.unflatten_dict(result, sep="/")
openpi_runtime/openpi/transforms.py ADDED
@@ -0,0 +1,460 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import Callable, Mapping, Sequence
2
+ import dataclasses
3
+ import re
4
+ from typing import Protocol, TypeAlias, TypeVar, runtime_checkable
5
+
6
+ import flax.traverse_util as traverse_util
7
+ import jax
8
+ import numpy as np
9
+ from openpi_client import image_tools
10
+
11
+ from openpi.models import tokenizer as _tokenizer
12
+ from openpi.shared import array_typing as at
13
+ from openpi.shared import normalize as _normalize
14
+
15
+ DataDict: TypeAlias = at.PyTree
16
+ NormStats: TypeAlias = _normalize.NormStats
17
+
18
+
19
+ T = TypeVar("T")
20
+ S = TypeVar("S")
21
+
22
+
23
+ @runtime_checkable
24
+ class DataTransformFn(Protocol):
25
+ def __call__(self, data: DataDict) -> DataDict:
26
+ """Apply transformation to the data.
27
+
28
+ Args:
29
+ data: The data to apply the transform to. This is a possibly nested dictionary that contains
30
+ unbatched data elements. Each leaf is expected to be a numpy array. Using JAX arrays is allowed
31
+ but not recommended since it may result in extra GPU memory usage inside data loader worker
32
+ processes.
33
+
34
+ Returns:
35
+ The transformed data. Could be the input `data` that was modified in place, or a new data structure.
36
+ """
37
+
38
+
39
+ @dataclasses.dataclass(frozen=True)
40
+ class Group:
41
+ """A group of transforms."""
42
+
43
+ # Transforms that are applied to the model input data.
44
+ inputs: Sequence[DataTransformFn] = ()
45
+
46
+ # Transforms that are applied to the model output data.
47
+ outputs: Sequence[DataTransformFn] = ()
48
+
49
+ def push(self, *, inputs: Sequence[DataTransformFn] = (), outputs: Sequence[DataTransformFn] = ()) -> "Group":
50
+ """Append transforms to the group and return a new group.
51
+
52
+ Args:
53
+ inputs: Appended to the *end* of the current input transforms.
54
+ outputs: Appended to the *beginning* of the current output transforms.
55
+
56
+ Returns:
57
+ A new group with the appended transforms.
58
+ """
59
+ return Group(inputs=(*self.inputs, *inputs), outputs=(*outputs, *self.outputs))
60
+
61
+
62
+ @dataclasses.dataclass(frozen=True)
63
+ class CompositeTransform(DataTransformFn):
64
+ """A composite transform that applies a sequence of transforms in order."""
65
+
66
+ transforms: Sequence[DataTransformFn]
67
+
68
+ def __call__(self, data: DataDict) -> DataDict:
69
+ for transform in self.transforms:
70
+ data = transform(data)
71
+ return data
72
+
73
+
74
+ def compose(transforms: Sequence[DataTransformFn]) -> DataTransformFn:
75
+ """Compose a sequence of transforms into a single transform."""
76
+ return CompositeTransform(transforms)
77
+
78
+
79
+ @dataclasses.dataclass(frozen=True)
80
+ class RepackTransform(DataTransformFn):
81
+ """Repacks an input dictionary into a new dictionary.
82
+
83
+ Repacking is defined using a dictionary where the keys are the new keys and the values
84
+ are the flattened paths to the old keys. We use '/' as the separator during flattening.
85
+
86
+ Example:
87
+ {
88
+ "images": {
89
+ "cam_high": "observation.images.top",
90
+ "cam_low": "observation.images.bottom",
91
+ },
92
+ "state": "observation.state",
93
+ "actions": "action",
94
+ }
95
+ """
96
+
97
+ structure: at.PyTree[str]
98
+
99
+ def __call__(self, data: DataDict) -> DataDict:
100
+ flat_item = flatten_dict(data)
101
+ return jax.tree.map(lambda k: flat_item[k], self.structure)
102
+
103
+
104
+ @dataclasses.dataclass(frozen=True)
105
+ class InjectDefaultPrompt(DataTransformFn):
106
+ prompt: str | None
107
+
108
+ def __call__(self, data: DataDict) -> DataDict:
109
+ if self.prompt is not None and "prompt" not in data:
110
+ data["prompt"] = np.asarray(self.prompt)
111
+ return data
112
+
113
+
114
+ @dataclasses.dataclass(frozen=True)
115
+ class Normalize(DataTransformFn):
116
+ norm_stats: at.PyTree[NormStats] | None
117
+ # If true, will use quantile normalization. Otherwise, normal z-score normalization will be used.
118
+ use_quantiles: bool = False
119
+ # If true, will raise an error if any of the keys in the norm stats are not present in the data.
120
+ strict: bool = False
121
+
122
+ def __post_init__(self):
123
+ if self.norm_stats is not None and self.use_quantiles:
124
+ _assert_quantile_stats(self.norm_stats)
125
+
126
+ def __call__(self, data: DataDict) -> DataDict:
127
+ if self.norm_stats is None:
128
+ return data
129
+
130
+ return apply_tree(
131
+ data,
132
+ self.norm_stats,
133
+ self._normalize_quantile if self.use_quantiles else self._normalize,
134
+ strict=self.strict,
135
+ )
136
+
137
+ def _normalize(self, x, stats: NormStats):
138
+ mean, std = stats.mean[..., : x.shape[-1]], stats.std[..., : x.shape[-1]]
139
+ return (x - mean) / (std + 1e-6)
140
+
141
+ def _normalize_quantile(self, x, stats: NormStats):
142
+ assert stats.q01 is not None
143
+ assert stats.q99 is not None
144
+ q01, q99 = stats.q01[..., : x.shape[-1]], stats.q99[..., : x.shape[-1]]
145
+ return (x - q01) / (q99 - q01 + 1e-6) * 2.0 - 1.0
146
+
147
+
148
+ @dataclasses.dataclass(frozen=True)
149
+ class Unnormalize(DataTransformFn):
150
+ norm_stats: at.PyTree[NormStats] | None
151
+ # If true, will use quantile normalization. Otherwise, normal z-score normalization will be used.
152
+ use_quantiles: bool = False
153
+
154
+ def __post_init__(self):
155
+ if self.norm_stats is not None and self.use_quantiles:
156
+ _assert_quantile_stats(self.norm_stats)
157
+
158
+ def __call__(self, data: DataDict) -> DataDict:
159
+ if self.norm_stats is None:
160
+ return data
161
+
162
+ # Make sure that all the keys in the norm stats are present in the data.
163
+ return apply_tree(
164
+ data,
165
+ self.norm_stats,
166
+ self._unnormalize_quantile if self.use_quantiles else self._unnormalize,
167
+ strict=True,
168
+ )
169
+
170
+ def _unnormalize(self, x, stats: NormStats):
171
+ mean = pad_to_dim(stats.mean, x.shape[-1], axis=-1, value=0.0)
172
+ std = pad_to_dim(stats.std, x.shape[-1], axis=-1, value=1.0)
173
+ return x * (std + 1e-6) + mean
174
+
175
+ def _unnormalize_quantile(self, x, stats: NormStats):
176
+ assert stats.q01 is not None
177
+ assert stats.q99 is not None
178
+ q01, q99 = stats.q01, stats.q99
179
+ if (dim := q01.shape[-1]) < x.shape[-1]:
180
+ return np.concatenate([(x[..., :dim] + 1.0) / 2.0 * (q99 - q01 + 1e-6) + q01, x[..., dim:]], axis=-1)
181
+ return (x + 1.0) / 2.0 * (q99 - q01 + 1e-6) + q01
182
+
183
+
184
+ @dataclasses.dataclass(frozen=True)
185
+ class ResizeImages(DataTransformFn):
186
+ height: int
187
+ width: int
188
+
189
+ def __call__(self, data: DataDict) -> DataDict:
190
+ data["image"] = {k: image_tools.resize_with_pad(v, self.height, self.width) for k, v in data["image"].items()}
191
+ return data
192
+
193
+
194
+ @dataclasses.dataclass(frozen=True)
195
+ class SubsampleActions(DataTransformFn):
196
+ stride: int
197
+
198
+ def __call__(self, data: DataDict) -> DataDict:
199
+ data["actions"] = data["actions"][:: self.stride]
200
+ return data
201
+
202
+
203
+ @dataclasses.dataclass(frozen=True)
204
+ class DeltaActions(DataTransformFn):
205
+ """Repacks absolute actions into delta action space."""
206
+
207
+ # Boolean mask for the action dimensions to be repacked into delta action space. Length
208
+ # can be smaller than the actual number of dimensions. If None, this transform is a no-op.
209
+ # See `make_bool_mask` for more details.
210
+ mask: Sequence[bool] | None
211
+
212
+ def __call__(self, data: DataDict) -> DataDict:
213
+ if "actions" not in data or self.mask is None:
214
+ return data
215
+
216
+ state, actions = data["state"], data["actions"]
217
+ mask = np.asarray(self.mask)
218
+ dims = mask.shape[-1]
219
+ actions[..., :dims] -= np.expand_dims(np.where(mask, state[..., :dims], 0), axis=-2)
220
+ data["actions"] = actions
221
+
222
+ return data
223
+
224
+
225
+ @dataclasses.dataclass(frozen=True)
226
+ class AbsoluteActions(DataTransformFn):
227
+ """Repacks delta actions into absolute action space."""
228
+
229
+ # Boolean mask for the action dimensions to be repacked into absolute action space. Length
230
+ # can be smaller than the actual number of dimensions. If None, this transform is a no-op.
231
+ # See `make_bool_mask` for more details.
232
+ mask: Sequence[bool] | None
233
+
234
+ def __call__(self, data: DataDict) -> DataDict:
235
+ if "actions" not in data or self.mask is None:
236
+ return data
237
+
238
+ state, actions = data["state"], data["actions"]
239
+ mask = np.asarray(self.mask)
240
+ dims = mask.shape[-1]
241
+ actions[..., :dims] += np.expand_dims(np.where(mask, state[..., :dims], 0), axis=-2)
242
+ data["actions"] = actions
243
+
244
+ return data
245
+
246
+
247
+ @dataclasses.dataclass(frozen=True)
248
+ class TokenizePrompt(DataTransformFn):
249
+ tokenizer: _tokenizer.PaligemmaTokenizer
250
+ discrete_state_input: bool = False
251
+
252
+ def __call__(self, data: DataDict) -> DataDict:
253
+ if (prompt := data.pop("prompt", None)) is None:
254
+ raise ValueError("Prompt is required")
255
+
256
+ if self.discrete_state_input:
257
+ if (state := data.get("state", None)) is None:
258
+ raise ValueError("State is required.")
259
+ else:
260
+ state = None
261
+
262
+ if not isinstance(prompt, str):
263
+ prompt = prompt.item()
264
+
265
+ tokens, token_masks = self.tokenizer.tokenize(prompt, state)
266
+ return {**data, "tokenized_prompt": tokens, "tokenized_prompt_mask": token_masks}
267
+
268
+
269
+ @dataclasses.dataclass(frozen=True)
270
+ class TokenizeFASTInputs(DataTransformFn):
271
+ tokenizer: _tokenizer.FASTTokenizer
272
+
273
+ def __call__(self, data: DataDict) -> DataDict:
274
+ if (prompt := data.pop("prompt", None)) is None:
275
+ raise ValueError("Prompt is required")
276
+
277
+ if not isinstance(prompt, str):
278
+ prompt = prompt.item()
279
+
280
+ state, actions = data["state"], data.get("actions")
281
+ tokens, token_mask, ar_mask, loss_mask = self.tokenizer.tokenize(prompt, state, actions)
282
+ return {
283
+ **data,
284
+ "tokenized_prompt": tokens,
285
+ "tokenized_prompt_mask": token_mask,
286
+ "token_ar_mask": ar_mask,
287
+ "token_loss_mask": loss_mask,
288
+ }
289
+
290
+
291
+ @dataclasses.dataclass(frozen=True)
292
+ class ExtractFASTActions(DataTransformFn):
293
+ tokenizer: _tokenizer.FASTTokenizer
294
+ action_horizon: int
295
+ action_dim: int
296
+
297
+ def __call__(self, data: DataDict) -> DataDict:
298
+ if "actions" not in data:
299
+ return data
300
+ # Model outputs are saved in "actions", but for FAST models they represent tokens.
301
+ tokens = data.pop("actions")
302
+ actions = self.tokenizer.extract_actions(tokens.astype(np.int32), self.action_horizon, self.action_dim)
303
+ return {
304
+ **data,
305
+ "actions": actions,
306
+ }
307
+
308
+
309
+ @dataclasses.dataclass(frozen=True)
310
+ class PromptFromLeRobotTask(DataTransformFn):
311
+ """Extracts a prompt from the current LeRobot dataset task."""
312
+
313
+ # Contains the LeRobot dataset tasks (dataset.meta.tasks).
314
+ tasks: dict[int, str]
315
+
316
+ def __call__(self, data: DataDict) -> DataDict:
317
+ if "task_index" not in data:
318
+ raise ValueError('Cannot extract prompt without "task_index"')
319
+
320
+ task_index = int(data["task_index"])
321
+ if (prompt := self.tasks.get(task_index)) is None:
322
+ raise ValueError(f"{task_index=} not found in task mapping: {self.tasks}")
323
+
324
+ return {**data, "prompt": prompt}
325
+
326
+
327
+ @dataclasses.dataclass(frozen=True)
328
+ class PadStatesAndActions(DataTransformFn):
329
+ """Zero-pads states and actions to the model action dimension."""
330
+
331
+ model_action_dim: int
332
+
333
+ def __call__(self, data: DataDict) -> DataDict:
334
+ data["state"] = pad_to_dim(data["state"], self.model_action_dim, axis=-1)
335
+ if "actions" in data:
336
+ data["actions"] = pad_to_dim(data["actions"], self.model_action_dim, axis=-1)
337
+ return data
338
+
339
+
340
+ def flatten_dict(tree: at.PyTree) -> dict:
341
+ """Flatten a nested dictionary. Uses '/' as the separator."""
342
+ return traverse_util.flatten_dict(tree, sep="/")
343
+
344
+
345
+ def unflatten_dict(tree: dict) -> at.PyTree:
346
+ """Unflatten a flattened dictionary. Assumes that '/' was used as a separator."""
347
+ return traverse_util.unflatten_dict(tree, sep="/")
348
+
349
+
350
+ def transform_dict(patterns: Mapping[str, str | None], tree: at.PyTree) -> at.PyTree:
351
+ """Transform the structure of a nested dictionary using a set of patterns.
352
+
353
+ The transformation is defined using the `patterns` dictionary. The keys are the
354
+ input keys that should be matched and the values are the new names inside the output
355
+ dictionary. If the value is None, the input key is removed.
356
+
357
+ Both keys and values should represent flattened paths using '/' as the separator.
358
+ Keys can be regular expressions and values can include backreferences to the
359
+ matched groups (see `re.sub` for more details). Note that the regular expression
360
+ must match the entire key.
361
+
362
+ The order inside the `patterns` dictionary is important. Only the first pattern that
363
+ matches the input key will be used.
364
+
365
+ See unit tests for more examples.
366
+
367
+ Args:
368
+ patterns: A mapping from old keys to new keys.
369
+ tree: The nested dictionary to transform.
370
+
371
+ Returns:
372
+ The transformed nested dictionary.
373
+ """
374
+ data = flatten_dict(tree)
375
+
376
+ # Compile the patterns.
377
+ compiled = {re.compile(k): v for k, v in patterns.items()}
378
+
379
+ output = {}
380
+ for k in data:
381
+ for pattern, repl in compiled.items():
382
+ if pattern.fullmatch(k):
383
+ new_k = pattern.sub(repl, k, count=1) if repl is not None else None
384
+ break
385
+ else:
386
+ # Use the original key if no match is found.
387
+ new_k = k
388
+
389
+ if new_k is not None:
390
+ if new_k in output:
391
+ raise ValueError(f"Key '{new_k}' already exists in output")
392
+ output[new_k] = data[k]
393
+
394
+ # Validate the output structure to make sure that it can be unflattened.
395
+ names = sorted(output)
396
+ for i in range(len(names) - 1):
397
+ name, next_name = names[i : i + 2]
398
+ if next_name.startswith(name + "/"):
399
+ raise ValueError(f"Leaf '{name}' aliases a node of '{next_name}'")
400
+
401
+ return unflatten_dict(output)
402
+
403
+
404
+ def apply_tree(
405
+ tree: at.PyTree[T], selector: at.PyTree[S], fn: Callable[[T, S], T], *, strict: bool = False
406
+ ) -> at.PyTree[T]:
407
+ tree = flatten_dict(tree)
408
+ selector = flatten_dict(selector)
409
+
410
+ def transform(k: str, v: T) -> T:
411
+ if k in selector:
412
+ return fn(v, selector[k])
413
+ return v
414
+
415
+ if strict:
416
+ for k in selector:
417
+ if k not in tree:
418
+ raise ValueError(f"Selector key {k} not found in tree")
419
+
420
+ return unflatten_dict({k: transform(k, v) for k, v in tree.items()})
421
+
422
+
423
+ def pad_to_dim(x: np.ndarray, target_dim: int, axis: int = -1, value: float = 0.0) -> np.ndarray:
424
+ """Pad an array to the target dimension with zeros along the specified axis."""
425
+ current_dim = x.shape[axis]
426
+ if current_dim < target_dim:
427
+ pad_width = [(0, 0)] * len(x.shape)
428
+ pad_width[axis] = (0, target_dim - current_dim)
429
+ return np.pad(x, pad_width, constant_values=value)
430
+ return x
431
+
432
+
433
+ def make_bool_mask(*dims: int) -> tuple[bool, ...]:
434
+ """Make a boolean mask for the given dimensions.
435
+
436
+ Example:
437
+ make_bool_mask(2, -2, 2) == (True, True, False, False, True, True)
438
+ make_bool_mask(2, 0, 2) == (True, True, True, True)
439
+
440
+ Args:
441
+ dims: The dimensions to make the mask for.
442
+
443
+ Returns:
444
+ A tuple of booleans.
445
+ """
446
+ result = []
447
+ for dim in dims:
448
+ if dim > 0:
449
+ result.extend([True] * (dim))
450
+ else:
451
+ result.extend([False] * (-dim))
452
+ return tuple(result)
453
+
454
+
455
+ def _assert_quantile_stats(norm_stats: at.PyTree[NormStats]) -> None:
456
+ for k, v in flatten_dict(norm_stats).items():
457
+ if v.q01 is None or v.q99 is None:
458
+ raise ValueError(
459
+ f"quantile stats must be provided if use_quantile_norm is True. Key {k} is missing q01 or q99."
460
+ )
openpi_runtime/openpi_client/__init__.py ADDED
@@ -0,0 +1 @@
 
 
1
+ __version__ = "0.1.0"
openpi_runtime/openpi_client/action_chunk_broker.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import Dict
2
+
3
+ import numpy as np
4
+ import tree
5
+ from typing_extensions import override
6
+
7
+ from openpi_client import base_policy as _base_policy
8
+
9
+
10
+ class ActionChunkBroker(_base_policy.BasePolicy):
11
+ """Wraps a policy to return action chunks one-at-a-time.
12
+
13
+ Assumes that the first dimension of all action fields is the chunk size.
14
+
15
+ A new inference call to the inner policy is only made when the current
16
+ list of chunks is exhausted.
17
+ """
18
+
19
+ def __init__(self, policy: _base_policy.BasePolicy, action_horizon: int):
20
+ self._policy = policy
21
+ self._action_horizon = action_horizon
22
+ self._cur_step: int = 0
23
+
24
+ self._last_results: Dict[str, np.ndarray] | None = None
25
+
26
+ @override
27
+ def infer(self, obs: Dict) -> Dict: # noqa: UP006
28
+ if self._last_results is None:
29
+ self._last_results = self._policy.infer(obs)
30
+ self._cur_step = 0
31
+
32
+ def slicer(x):
33
+ if isinstance(x, np.ndarray):
34
+ return x[self._cur_step, ...]
35
+ else:
36
+ return x
37
+
38
+ results = tree.map_structure(slicer, self._last_results)
39
+ self._cur_step += 1
40
+
41
+ if self._cur_step >= self._action_horizon:
42
+ self._last_results = None
43
+
44
+ return results
45
+
46
+ @override
47
+ def reset(self) -> None:
48
+ self._policy.reset()
49
+ self._last_results = None
50
+ self._cur_step = 0