Spaces:
Sleeping
Sleeping
Commit ·
b2cec57
1
Parent(s): 3287fba
build: add OpenPI runtime for UR Space
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- README.md +1 -1
- openpi_runtime/openpi/__init__.py +0 -0
- openpi_runtime/openpi/conftest.py +17 -0
- openpi_runtime/openpi/models/__init__.py +0 -0
- openpi_runtime/openpi/models/gemma.py +459 -0
- openpi_runtime/openpi/models/gemma_fast.py +437 -0
- openpi_runtime/openpi/models/lora.py +148 -0
- openpi_runtime/openpi/models/model.py +332 -0
- openpi_runtime/openpi/models/pi0.py +279 -0
- openpi_runtime/openpi/models/pi0_config.py +117 -0
- openpi_runtime/openpi/models/pi0_fast.py +313 -0
- openpi_runtime/openpi/models/siglip.py +373 -0
- openpi_runtime/openpi/models/tokenizer.py +371 -0
- openpi_runtime/openpi/models/utils/fsq_tokenizer.py +472 -0
- openpi_runtime/openpi/models/vit.py +307 -0
- openpi_runtime/openpi/models_pytorch/gemma_pytorch.py +280 -0
- openpi_runtime/openpi/models_pytorch/pi0_pytorch.py +462 -0
- openpi_runtime/openpi/models_pytorch/preprocessing_pytorch.py +173 -0
- openpi_runtime/openpi/models_pytorch/transformers_replace/models/gemma/configuration_gemma.py +173 -0
- openpi_runtime/openpi/models_pytorch/transformers_replace/models/gemma/modeling_gemma.py +862 -0
- openpi_runtime/openpi/models_pytorch/transformers_replace/models/paligemma/modeling_paligemma.py +622 -0
- openpi_runtime/openpi/models_pytorch/transformers_replace/models/siglip/check.py +4 -0
- openpi_runtime/openpi/models_pytorch/transformers_replace/models/siglip/modeling_siglip.py +1237 -0
- openpi_runtime/openpi/policies/aloha_policy.py +202 -0
- openpi_runtime/openpi/policies/droid_policy.py +81 -0
- openpi_runtime/openpi/policies/libero_policy.py +100 -0
- openpi_runtime/openpi/policies/policy.py +135 -0
- openpi_runtime/openpi/policies/policy_config.py +94 -0
- openpi_runtime/openpi/policies/ur_policy.py +56 -0
- openpi_runtime/openpi/py.typed +0 -0
- openpi_runtime/openpi/serving/websocket_policy_server.py +90 -0
- openpi_runtime/openpi/shared/__init__.py +0 -0
- openpi_runtime/openpi/shared/array_typing.py +89 -0
- openpi_runtime/openpi/shared/download.py +216 -0
- openpi_runtime/openpi/shared/image_tools.py +126 -0
- openpi_runtime/openpi/shared/nnx_utils.py +69 -0
- openpi_runtime/openpi/shared/normalize.py +146 -0
- openpi_runtime/openpi/training/checkpoints.py +159 -0
- openpi_runtime/openpi/training/config.py +1070 -0
- openpi_runtime/openpi/training/data_loader.py +540 -0
- openpi_runtime/openpi/training/droid_rlds_dataset.py +248 -0
- openpi_runtime/openpi/training/misc/polaris_config.py +225 -0
- openpi_runtime/openpi/training/misc/roboarena_config.py +116 -0
- openpi_runtime/openpi/training/optimizer.py +109 -0
- openpi_runtime/openpi/training/sharding.py +102 -0
- openpi_runtime/openpi/training/utils.py +38 -0
- openpi_runtime/openpi/training/weight_loaders.py +104 -0
- openpi_runtime/openpi/transforms.py +460 -0
- openpi_runtime/openpi_client/__init__.py +1 -0
- 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.
|
| 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
|