Equimo 0.5.0a1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- equimo/__init__.py +1 -0
- equimo/conversion/__init__.py +1 -0
- equimo/conversion/utils.py +230 -0
- equimo/experimental/__init__.py +1 -0
- equimo/experimental/text.py +324 -0
- equimo/io.py +313 -0
- equimo/layers/__init__.py +0 -0
- equimo/layers/activation.py +33 -0
- equimo/layers/attention.py +2216 -0
- equimo/layers/convolution.py +2317 -0
- equimo/layers/downsample.py +209 -0
- equimo/layers/dropout.py +128 -0
- equimo/layers/ffn.py +449 -0
- equimo/layers/generic.py +70 -0
- equimo/layers/mamba.py +169 -0
- equimo/layers/norm.py +174 -0
- equimo/layers/patch.py +398 -0
- equimo/layers/posemb.py +753 -0
- equimo/layers/sharing.py +160 -0
- equimo/layers/squeeze_excite.py +143 -0
- equimo/layers/wavelet.py +141 -0
- equimo/models/__init__.py +12 -0
- equimo/models/attnet.py +243 -0
- equimo/models/emamodel.py +29 -0
- equimo/models/fastervit.py +498 -0
- equimo/models/lowformer.py +387 -0
- equimo/models/mlla.py +173 -0
- equimo/models/partialformer.py +411 -0
- equimo/models/reduceformer.py +376 -0
- equimo/models/shvit.py +266 -0
- equimo/models/vit.py +482 -0
- equimo/models/vssd.py +196 -0
- equimo/ops/scan.py +246 -0
- equimo/utils.py +283 -0
- equimo-0.5.0a1.dist-info/METADATA +277 -0
- equimo-0.5.0a1.dist-info/RECORD +39 -0
- equimo-0.5.0a1.dist-info/WHEEL +5 -0
- equimo-0.5.0a1.dist-info/licenses/LICENSE.md +21 -0
- equimo-0.5.0a1.dist-info/top_level.txt +1 -0
equimo/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "0.5.0-alpha.1"
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Any, Dict, Literal, Optional, Tuple
|
|
3
|
+
|
|
4
|
+
import equinox as eqx
|
|
5
|
+
import jax
|
|
6
|
+
import jax.numpy as jnp
|
|
7
|
+
from jax.tree_util import GetAttrKey, SequenceKey
|
|
8
|
+
from loguru import logger
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def stringify_name(path: Tuple) -> str:
|
|
12
|
+
stringified = []
|
|
13
|
+
for p in path:
|
|
14
|
+
if isinstance(p, GetAttrKey):
|
|
15
|
+
stringified.append(p.name)
|
|
16
|
+
if isinstance(p, SequenceKey):
|
|
17
|
+
stringified.append(str(p.idx))
|
|
18
|
+
return ".".join(stringified)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def expand_torch_tensor(tensor, pos: str, n: int):
|
|
22
|
+
padding = [None] * n
|
|
23
|
+
match pos:
|
|
24
|
+
case "before":
|
|
25
|
+
return tensor[*padding, ...]
|
|
26
|
+
case "after":
|
|
27
|
+
return tensor[..., *padding]
|
|
28
|
+
case _:
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Invalid `pos`, expected one of [`before`, `after`], got: {pos}"
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def convert_params_from_torch(
|
|
35
|
+
jax_model: eqx.Module,
|
|
36
|
+
replace_cfg: Dict[str, str],
|
|
37
|
+
expand_cfg: Dict[str, list],
|
|
38
|
+
squeeze_cfg: Dict[str, int | None],
|
|
39
|
+
torch_whitelist: list[str],
|
|
40
|
+
jax_whitelist: list[str],
|
|
41
|
+
strict: bool = True,
|
|
42
|
+
source: Literal["torchhub", "timm", "custom"] = "torchhub",
|
|
43
|
+
torch_hub_cfg: Optional[dict] = None,
|
|
44
|
+
torch_model=None,
|
|
45
|
+
timm_cfg: Optional[list] = None,
|
|
46
|
+
return_torch: bool = False,
|
|
47
|
+
):
|
|
48
|
+
"""
|
|
49
|
+
Load weights from a torch hub model into an Equinox module.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
jax_model (eqx.Module): A preexisting Jax model corresponding to the checkpoint to download.
|
|
53
|
+
torch_hub_cfg (dict): Arguments passed to `torch.hub.load()`.
|
|
54
|
+
replace_cfg (Dict[str, str]): Rename parameters from key to value.
|
|
55
|
+
expand_cfg (Dict[str, list]): Config to reshape params, see `expand_torch_tensor`
|
|
56
|
+
sqeeze_cfg (Dict[str, int|None]): Config to squeeze tensors, opposite of expand.
|
|
57
|
+
torch_whitelist (Set[str]): Parameters to exclude from format conversion.
|
|
58
|
+
jax_whitelist (Set[str]): Parameters to exclude from format conversion.
|
|
59
|
+
strict (bool): Whether to crash on missing parameters one of the models.
|
|
60
|
+
source (str): Torch Hub or timm.
|
|
61
|
+
torch_hub_cfg (dict): args to pass to `torch.hub.load`.
|
|
62
|
+
torch_model [torch.nn.Module]: Custom torch model
|
|
63
|
+
timm_cfg (Optional[list]): args to pass to `timm.create_model`.
|
|
64
|
+
return_torch (bool): Return both jax and torch models.
|
|
65
|
+
"""
|
|
66
|
+
try:
|
|
67
|
+
import timm
|
|
68
|
+
import torch
|
|
69
|
+
except:
|
|
70
|
+
raise ImportError("`torch` not available")
|
|
71
|
+
|
|
72
|
+
# Load the pytorch model
|
|
73
|
+
match source:
|
|
74
|
+
case "custom":
|
|
75
|
+
if torch_model is None:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
"The `custom` source is selected but `torch_model` is None."
|
|
78
|
+
)
|
|
79
|
+
case "torchhub":
|
|
80
|
+
if torch_hub_cfg is None:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
"The `torchhub` source is selected but `torch_hub_cfg` is None."
|
|
83
|
+
)
|
|
84
|
+
torch_model = torch.hub.load(**torch_hub_cfg)
|
|
85
|
+
case "timm":
|
|
86
|
+
if timm_cfg is None:
|
|
87
|
+
raise ValueError(
|
|
88
|
+
"The `timm` source is selected but `timm_cfg` is None."
|
|
89
|
+
)
|
|
90
|
+
torch_model = timm.create_model(*timm_cfg)
|
|
91
|
+
|
|
92
|
+
torch_params = dict(torch_model.named_parameters())
|
|
93
|
+
|
|
94
|
+
# Extract the parameters from the defined Jax model
|
|
95
|
+
jax_params = eqx.filter(jax_model, eqx.is_array)
|
|
96
|
+
# _, jax_params, _ = nnx.split(jax_model, nnx.Param, ...)
|
|
97
|
+
jax_params_flat, jax_param_pytree = jax.tree_util.tree_flatten_with_path(jax_params)
|
|
98
|
+
|
|
99
|
+
torch_params_flat = []
|
|
100
|
+
for path, param in jax_params_flat:
|
|
101
|
+
# Match the parameters' path of pytorch
|
|
102
|
+
param_path = stringify_name(path)
|
|
103
|
+
param_path = re.sub(r"\.scale|.kernel", ".weight", param_path)
|
|
104
|
+
|
|
105
|
+
for old, new in replace_cfg.items():
|
|
106
|
+
param_path = param_path.replace(old, new)
|
|
107
|
+
|
|
108
|
+
shape = param.shape
|
|
109
|
+
|
|
110
|
+
if param_path not in torch_params:
|
|
111
|
+
_msg = f"{param_path} ({shape}) not found in PyTorch model."
|
|
112
|
+
if strict and param_path not in jax_whitelist:
|
|
113
|
+
logger.error(_msg)
|
|
114
|
+
raise AttributeError(_msg)
|
|
115
|
+
|
|
116
|
+
if param_path in jax_whitelist:
|
|
117
|
+
p = param
|
|
118
|
+
logger.warning(
|
|
119
|
+
f"{_msg} Appending original parameters to flat param list because of `jax_whitelist`."
|
|
120
|
+
)
|
|
121
|
+
else:
|
|
122
|
+
p = None
|
|
123
|
+
logger.warning(f"{_msg} Appending `None` to flat param list.")
|
|
124
|
+
|
|
125
|
+
torch_params_flat.append(p)
|
|
126
|
+
continue
|
|
127
|
+
|
|
128
|
+
logger.info(f"Converting {param_path}...")
|
|
129
|
+
torch_param = torch_params[param_path]
|
|
130
|
+
|
|
131
|
+
if param_path in expand_cfg:
|
|
132
|
+
torch_param = expand_torch_tensor(torch_param, *expand_cfg[param_path])
|
|
133
|
+
if param_path in squeeze_cfg:
|
|
134
|
+
torch_param = torch.squeeze(torch_param, dim=squeeze_cfg[param_path])
|
|
135
|
+
|
|
136
|
+
if shape != torch_param.shape:
|
|
137
|
+
_msg = f"`{param_path}`: expected shape ({shape}) does not match its pytorch implementation ({torch_param.shape})."
|
|
138
|
+
logger.error(_msg)
|
|
139
|
+
raise ValueError(_msg)
|
|
140
|
+
|
|
141
|
+
torch_params_flat.append(jnp.asarray(torch_param.detach().numpy()))
|
|
142
|
+
_ = torch_params.pop(param_path)
|
|
143
|
+
|
|
144
|
+
loaded_params = jax.tree_util.tree_unflatten(jax_param_pytree, torch_params_flat)
|
|
145
|
+
|
|
146
|
+
for path, param in torch_params.items():
|
|
147
|
+
logger.warning(
|
|
148
|
+
f"PyTorch parameters `{path}` ({param.shape}) were not converted."
|
|
149
|
+
)
|
|
150
|
+
if strict and path not in torch_whitelist:
|
|
151
|
+
_msg = f"The PyTorch model contains parameters ({path}) that do not have a Jax counterpart."
|
|
152
|
+
logger.error(_msg)
|
|
153
|
+
raise AttributeError(_msg)
|
|
154
|
+
|
|
155
|
+
if return_torch:
|
|
156
|
+
return loaded_params, torch_model
|
|
157
|
+
return loaded_params
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def convert_torch_to_equinox(
|
|
161
|
+
jax_model: eqx.Module,
|
|
162
|
+
replace_cfg: dict = {},
|
|
163
|
+
expand_cfg: dict = {},
|
|
164
|
+
squeeze_cfg: dict = {},
|
|
165
|
+
torch_whitelist: list[str] = [],
|
|
166
|
+
jax_whitelist: list[str] = [],
|
|
167
|
+
strict: bool = True,
|
|
168
|
+
source: Literal["torchhub", "timm"] = "torchhub",
|
|
169
|
+
torch_hub_cfg: Optional[dict] = None,
|
|
170
|
+
torch_model=None,
|
|
171
|
+
timm_cfg: Optional[list] = None,
|
|
172
|
+
return_torch: bool = False,
|
|
173
|
+
) -> eqx.Module | Tuple[eqx.Module, Any]:
|
|
174
|
+
"""
|
|
175
|
+
Convert a PyTorch model from torch.hub to Equinox format.
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
jax_model: The Equinox model
|
|
179
|
+
replace_cfg: Dict of parameter name replacements
|
|
180
|
+
expand_cfg: Dict of dimensions to expand
|
|
181
|
+
squeeze_cfg: Dict of dimensions to squeeze
|
|
182
|
+
torch_whitelist: List of parameters allowed to be in PT model but not in Jax
|
|
183
|
+
jax_whitelist: List of parameters allowed to be in Jax model but not in PT
|
|
184
|
+
strict: Wether to raise an issue if not all weights are converted
|
|
185
|
+
source (str): Torch Hub or timm.
|
|
186
|
+
torch_hub_cfg (dict): torch.hub.load config
|
|
187
|
+
torch_model [torch.nn.Module]: Custom torch model
|
|
188
|
+
timm_cfg (Optional[list]): args to pass to `timm.create_model`.
|
|
189
|
+
return_torch (bool): Return both jax and torch models.
|
|
190
|
+
|
|
191
|
+
Returns:
|
|
192
|
+
eqx.Module: Converted Equinox model in inference mode
|
|
193
|
+
"""
|
|
194
|
+
dynamic, static = eqx.partition(jax_model, eqx.is_array)
|
|
195
|
+
if return_torch:
|
|
196
|
+
converted_params, torch_model = convert_params_from_torch(
|
|
197
|
+
dynamic,
|
|
198
|
+
replace_cfg,
|
|
199
|
+
expand_cfg,
|
|
200
|
+
squeeze_cfg,
|
|
201
|
+
torch_whitelist,
|
|
202
|
+
jax_whitelist,
|
|
203
|
+
strict,
|
|
204
|
+
source,
|
|
205
|
+
torch_hub_cfg,
|
|
206
|
+
torch_model,
|
|
207
|
+
timm_cfg,
|
|
208
|
+
return_torch,
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
return eqx.nn.inference_mode(
|
|
212
|
+
eqx.combine(converted_params, static), value=True
|
|
213
|
+
), torch_model.eval()
|
|
214
|
+
else:
|
|
215
|
+
converted_params = convert_params_from_torch(
|
|
216
|
+
dynamic,
|
|
217
|
+
replace_cfg,
|
|
218
|
+
expand_cfg,
|
|
219
|
+
squeeze_cfg,
|
|
220
|
+
torch_whitelist,
|
|
221
|
+
jax_whitelist,
|
|
222
|
+
strict,
|
|
223
|
+
source,
|
|
224
|
+
torch_hub_cfg,
|
|
225
|
+
torch_model,
|
|
226
|
+
timm_cfg,
|
|
227
|
+
return_torch,
|
|
228
|
+
)
|
|
229
|
+
|
|
230
|
+
return eqx.nn.inference_mode(eqx.combine(converted_params, static), value=True)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Callable, Optional, Sequence, Tuple
|
|
3
|
+
from urllib.parse import urlparse
|
|
4
|
+
|
|
5
|
+
import equinox as eqx
|
|
6
|
+
import jax
|
|
7
|
+
import jax.numpy as jnp
|
|
8
|
+
import jax.random as jr
|
|
9
|
+
import requests
|
|
10
|
+
from jaxtyping import Array, Float, Int, PRNGKeyArray
|
|
11
|
+
|
|
12
|
+
from equimo.layers.activation import get_act
|
|
13
|
+
from equimo.layers.attention import AttentionBlock
|
|
14
|
+
|
|
15
|
+
try:
|
|
16
|
+
import tensorflow as tf
|
|
17
|
+
import tensorflow_text
|
|
18
|
+
except ImportError:
|
|
19
|
+
print(
|
|
20
|
+
"`tensorflow` and `tensorflow_text` need to be installed to use tokenizers. Install equimo with the `text` group such as `uv add equimo[text]`"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
DEFAULT_TOKENIZER_REPOSITORY = (
|
|
24
|
+
"https://huggingface.co/poiretclement/equimo/resolve/main/models/tokenizers"
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class Tokenizer(object):
|
|
29
|
+
"""A simple tokenizer based on TensorFlow's SentencepieceTokenizer.
|
|
30
|
+
|
|
31
|
+
This class provides a wrapper around TensorFlow's SentencepieceTokenizer,
|
|
32
|
+
handling model loading from a path (which can be a URL or local file) and
|
|
33
|
+
tokenizing input text with padding. It supports downloading remote models and
|
|
34
|
+
caching them locally.
|
|
35
|
+
|
|
36
|
+
Attributes:
|
|
37
|
+
tokenizer: The underlying TensorFlow SentencepieceTokenizer instance.
|
|
38
|
+
|
|
39
|
+
Based on: https://github.com/google-deepmind/tips/blob/72820c1841f973c9543d9c95c5ff2262ec621955/pytorch/text_encoder.py#L27
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(
|
|
43
|
+
self,
|
|
44
|
+
identifier: Optional[str] = None,
|
|
45
|
+
url: Optional[str] = None,
|
|
46
|
+
path: Optional[str] = None,
|
|
47
|
+
repository: str = DEFAULT_TOKENIZER_REPOSITORY,
|
|
48
|
+
):
|
|
49
|
+
"""Initializes the tokenizer."""
|
|
50
|
+
if not identifier and not path and not url:
|
|
51
|
+
raise ValueError(
|
|
52
|
+
"At least one of identifier, path, or url should be defined."
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
if identifier:
|
|
56
|
+
url = f"{repository}/{identifier}.model"
|
|
57
|
+
if url:
|
|
58
|
+
model_file_path = self.download(url)
|
|
59
|
+
else:
|
|
60
|
+
# User passes a local file
|
|
61
|
+
assert path
|
|
62
|
+
path: Path = Path(path)
|
|
63
|
+
if not path.expanduser().exists():
|
|
64
|
+
raise FileNotFoundError(f"Tokenizer file not found: {path}")
|
|
65
|
+
model_file_path = path
|
|
66
|
+
|
|
67
|
+
with open(model_file_path, "rb") as f:
|
|
68
|
+
model = f.read()
|
|
69
|
+
self.tokenizer = tensorflow_text.SentencepieceTokenizer(
|
|
70
|
+
model=model, add_eos=False, add_bos=False
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
def download(self, url: str) -> Path:
|
|
74
|
+
assert url.startswith("http://") or url.startswith("https://")
|
|
75
|
+
|
|
76
|
+
parsed_url = urlparse(url)
|
|
77
|
+
fname = Path(parsed_url.path).name
|
|
78
|
+
cache_dir = Path("~/.cache/equimo/tokenizers").expanduser()
|
|
79
|
+
cache_dir.mkdir(parents=True, exist_ok=True)
|
|
80
|
+
model_file_path = cache_dir / fname
|
|
81
|
+
|
|
82
|
+
if not model_file_path.exists():
|
|
83
|
+
response = requests.get(url)
|
|
84
|
+
response.raise_for_status()
|
|
85
|
+
with open(model_file_path, "wb") as f:
|
|
86
|
+
f.write(response.content)
|
|
87
|
+
|
|
88
|
+
return model_file_path
|
|
89
|
+
|
|
90
|
+
def tokenize(self, input_text, max_len=64):
|
|
91
|
+
tokens = self.tokenizer.tokenize(tf.strings.lower(input_text)).to_tensor()
|
|
92
|
+
curr_len = tokens.shape[1]
|
|
93
|
+
is_padding = tf.zeros((tokens.shape[0], max_len))
|
|
94
|
+
if curr_len > max_len:
|
|
95
|
+
tokens = tokens[:, :max_len]
|
|
96
|
+
else:
|
|
97
|
+
padding_len = max_len - curr_len
|
|
98
|
+
tokens = tf.pad(tokens, [[0, 0], [0, padding_len]], constant_values=0)
|
|
99
|
+
is_padding = tf.cast(tokens == 0, tf.int32)
|
|
100
|
+
return tokens.numpy(), is_padding.numpy()
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def global_avg_pooling(
|
|
104
|
+
inputs: Float[Array, "..."],
|
|
105
|
+
compatible_paddings: Int[Array, "..."],
|
|
106
|
+
pooling_dims: Sequence[int],
|
|
107
|
+
epsilon: float = 1e-8,
|
|
108
|
+
):
|
|
109
|
+
"""
|
|
110
|
+
Applies global average pooling to inputs over specified dimensions, handling padding.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
inputs: Input array of shape [...].
|
|
114
|
+
compatible_paddings: Padding mask, same shape as inputs, where >0 indicates padded.
|
|
115
|
+
pooling_dims: Sequence of int, dimensions to pool over.
|
|
116
|
+
epsilon: Small float to prevent division by zero, default 1e-8.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Array with pooling_dims reduced, averaged over valid elements.
|
|
120
|
+
"""
|
|
121
|
+
valid_mask = 1.0 - compatible_paddings
|
|
122
|
+
masked_inputs = inputs * valid_mask
|
|
123
|
+
inputs_sum = jnp.sum(masked_inputs, axis=pooling_dims)
|
|
124
|
+
valid_count = jnp.sum(valid_mask, axis=pooling_dims)
|
|
125
|
+
outputs = inputs_sum / (valid_count + epsilon)
|
|
126
|
+
return outputs
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
class Transformer(eqx.Module):
|
|
130
|
+
"""A transformer model composed of a stack of attention blocks.
|
|
131
|
+
|
|
132
|
+
This class implements a transformer encoder by stacking multiple `AttentionBlock`
|
|
133
|
+
instances, suitable for processing sequential data such as text embeddings.
|
|
134
|
+
|
|
135
|
+
Attributes:
|
|
136
|
+
blocks: A list of `AttentionBlock` instances forming the transformer stack.
|
|
137
|
+
"""
|
|
138
|
+
|
|
139
|
+
blocks: Tuple[AttentionBlock, ...]
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
dim: int,
|
|
144
|
+
depth: int,
|
|
145
|
+
num_heads: int,
|
|
146
|
+
mlp_ratio: float,
|
|
147
|
+
*,
|
|
148
|
+
key: PRNGKeyArray,
|
|
149
|
+
act_layer: Callable | str = jax.nn.gelu,
|
|
150
|
+
):
|
|
151
|
+
"""Initializes the Transformer with a stack of attention blocks.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
dim: The dimension of the input and output embeddings.
|
|
155
|
+
depth: The number of attention blocks in the transformer stack.
|
|
156
|
+
num_heads: The number of attention heads in each block.
|
|
157
|
+
mlp_ratio: The ratio of the MLP hidden dimension to the embedding dimension.
|
|
158
|
+
key: A PRNG key for initializing the attention blocks.
|
|
159
|
+
act_layer: The activation function for the MLP layers. Can be a callable or a
|
|
160
|
+
string name of an activation function (default: jax.nn.gelu).
|
|
161
|
+
"""
|
|
162
|
+
|
|
163
|
+
keys = jr.split(key, depth)
|
|
164
|
+
|
|
165
|
+
act_layer = get_act(act_layer)
|
|
166
|
+
|
|
167
|
+
self.blocks = tuple(
|
|
168
|
+
AttentionBlock(
|
|
169
|
+
dim=dim,
|
|
170
|
+
num_heads=num_heads,
|
|
171
|
+
mlp_ratio=mlp_ratio,
|
|
172
|
+
act_layer=act_layer,
|
|
173
|
+
key=keys[i],
|
|
174
|
+
)
|
|
175
|
+
for i in range(depth)
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
def __call__(
|
|
179
|
+
self,
|
|
180
|
+
x: Float[Array, "seqlen dim"],
|
|
181
|
+
key: PRNGKeyArray,
|
|
182
|
+
inference: Optional[bool] = None,
|
|
183
|
+
mask: Optional[Float[Array, ""]] = None,
|
|
184
|
+
) -> Float[Array, "seqlen dim"]:
|
|
185
|
+
for block in self.blocks:
|
|
186
|
+
x = block(x, mask=mask, inference=inference, key=key)
|
|
187
|
+
return x
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
class TextEncoder(eqx.Module):
|
|
191
|
+
"""A text encoder model that processes token sequences into fixed-size embeddings.
|
|
192
|
+
|
|
193
|
+
This class combines token embedding, positional embedding, a transformer encoder,
|
|
194
|
+
and final layer normalization, followed by global average pooling to produce a
|
|
195
|
+
single embedding vector per sequence.
|
|
196
|
+
|
|
197
|
+
Attributes:
|
|
198
|
+
token_embedding: An `eqx.nn.Embedding` layer for converting token IDs to embeddings.
|
|
199
|
+
transformer: A `Transformer` instance for processing the embedded sequences.
|
|
200
|
+
ln_final: An `eqx.nn.LayerNorm` layer for normalizing the transformer output.
|
|
201
|
+
dim: The dimension of the embeddings (static field).
|
|
202
|
+
scale_sqrt_depth: Boolean indicating whether to scale embeddings by sqrt(dim) (static field).
|
|
203
|
+
temperature: A temperature parameter, currently unused (static field, default: -1).
|
|
204
|
+
While unused, this parameter may be useful at inference time.
|
|
205
|
+
"""
|
|
206
|
+
|
|
207
|
+
token_embedding: eqx.nn.Embedding
|
|
208
|
+
transformer: Transformer
|
|
209
|
+
ln_final: eqx.nn.LayerNorm
|
|
210
|
+
|
|
211
|
+
dim: int = eqx.field(static=True)
|
|
212
|
+
scale_sqrt_depth: bool = eqx.field(static=True)
|
|
213
|
+
temperature: float = eqx.field(static=True, default=-1)
|
|
214
|
+
|
|
215
|
+
def __init__(
|
|
216
|
+
self,
|
|
217
|
+
dim: int,
|
|
218
|
+
mlp_ratio: float,
|
|
219
|
+
depth: int,
|
|
220
|
+
num_heads: int,
|
|
221
|
+
vocab_size: int,
|
|
222
|
+
*,
|
|
223
|
+
key: PRNGKeyArray,
|
|
224
|
+
scale_sqrt_depth: bool = True,
|
|
225
|
+
act_layer: Callable | str = jax.nn.gelu,
|
|
226
|
+
temperature: float = -1.0,
|
|
227
|
+
):
|
|
228
|
+
key_emb, key_trans = jr.split(key, 2)
|
|
229
|
+
|
|
230
|
+
self.dim = dim
|
|
231
|
+
self.scale_sqrt_depth = scale_sqrt_depth
|
|
232
|
+
self.temperature = temperature
|
|
233
|
+
|
|
234
|
+
self.token_embedding = eqx.nn.Embedding(
|
|
235
|
+
num_embeddings=vocab_size,
|
|
236
|
+
embedding_size=dim,
|
|
237
|
+
key=key_emb,
|
|
238
|
+
)
|
|
239
|
+
|
|
240
|
+
self.transformer = Transformer(
|
|
241
|
+
dim=dim,
|
|
242
|
+
depth=depth,
|
|
243
|
+
num_heads=num_heads,
|
|
244
|
+
mlp_ratio=mlp_ratio,
|
|
245
|
+
act_layer=act_layer,
|
|
246
|
+
key=key_trans,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
self.ln_final = eqx.nn.LayerNorm(dim)
|
|
250
|
+
|
|
251
|
+
def posemb(
|
|
252
|
+
self,
|
|
253
|
+
min_timescale: int = 1,
|
|
254
|
+
max_timescale: int = 10000,
|
|
255
|
+
seq_len: Optional[int] = None,
|
|
256
|
+
position: Optional[jnp.ndarray] = None,
|
|
257
|
+
) -> jnp.ndarray:
|
|
258
|
+
"""
|
|
259
|
+
Generates a tensor of sinusoids with different frequencies for positional embeddings.
|
|
260
|
+
|
|
261
|
+
Args:
|
|
262
|
+
embedding_size: Dimension of the embedding to be generated.
|
|
263
|
+
min_timescale: Start of the geometric index, determining the periodicity of the signal.
|
|
264
|
+
max_timescale: End of the geometric index, determining the frequency of the signal.
|
|
265
|
+
seq_len: Optional sequence length if position is not provided.
|
|
266
|
+
position: Optional 1D array of positions for each token in the sequence.
|
|
267
|
+
|
|
268
|
+
Returns:
|
|
269
|
+
Positional embeddings of shape [seq_len, embedding_size] with dtype float32.
|
|
270
|
+
|
|
271
|
+
Raises:
|
|
272
|
+
ValueError: If neither position nor seq_len is provided, or if position is not a 1D array.
|
|
273
|
+
"""
|
|
274
|
+
if position is None:
|
|
275
|
+
if seq_len is None:
|
|
276
|
+
raise ValueError("If position is None, seq_len must be provided.")
|
|
277
|
+
position = jnp.arange(seq_len, dtype=jnp.float32)
|
|
278
|
+
elif position.ndim != 1:
|
|
279
|
+
raise ValueError("position must be a 1D array.")
|
|
280
|
+
|
|
281
|
+
num_timescales = self.dim // 2
|
|
282
|
+
log_timescale_increment = jnp.log(max_timescale / min_timescale) / jnp.maximum(
|
|
283
|
+
num_timescales - 1, 1
|
|
284
|
+
)
|
|
285
|
+
|
|
286
|
+
inv_timescales = min_timescale * jnp.exp(
|
|
287
|
+
jnp.arange(num_timescales, dtype=jnp.float32) * -log_timescale_increment
|
|
288
|
+
)
|
|
289
|
+
|
|
290
|
+
scaled_time = position[:, None] * inv_timescales[None, :]
|
|
291
|
+
|
|
292
|
+
signal = jnp.concatenate([jnp.sin(scaled_time), jnp.cos(scaled_time)], axis=1)
|
|
293
|
+
|
|
294
|
+
# Pad with a zero column if embedding_size is odd
|
|
295
|
+
if self.dim % 2 == 1:
|
|
296
|
+
signal = jnp.pad(
|
|
297
|
+
signal, ((0, 0), (0, 1)), mode="constant", constant_values=0
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
return signal
|
|
301
|
+
|
|
302
|
+
def __call__(
|
|
303
|
+
self,
|
|
304
|
+
ids: Int[Array, "seqlen"],
|
|
305
|
+
paddings: Float[Array, "seqlen"],
|
|
306
|
+
key: PRNGKeyArray,
|
|
307
|
+
inference: Optional[bool] = None,
|
|
308
|
+
) -> Float[Array, "dim"]:
|
|
309
|
+
"""Applies TextEncoder module."""
|
|
310
|
+
seq_len = ids.shape[0]
|
|
311
|
+
mask = (paddings == 0).astype(jnp.float32)
|
|
312
|
+
|
|
313
|
+
x = jax.vmap(self.token_embedding)(ids)
|
|
314
|
+
if self.scale_sqrt_depth:
|
|
315
|
+
x = x * (self.dim**0.5)
|
|
316
|
+
|
|
317
|
+
x = x + self.posemb(seq_len=seq_len)
|
|
318
|
+
x = self.transformer(x, mask=mask[:, None], inference=inference, key=key)
|
|
319
|
+
x = jax.vmap(self.ln_final)(x)
|
|
320
|
+
x = global_avg_pooling(
|
|
321
|
+
x, compatible_paddings=paddings[:, None], pooling_dims=[0]
|
|
322
|
+
)
|
|
323
|
+
|
|
324
|
+
return x
|