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/io.py
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import io
|
|
3
|
+
import tarfile
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import equinox as eqx
|
|
9
|
+
import jax
|
|
10
|
+
import jax.numpy as jnp
|
|
11
|
+
import lz4.frame
|
|
12
|
+
import requests
|
|
13
|
+
from loguru import logger
|
|
14
|
+
from semver import Version
|
|
15
|
+
|
|
16
|
+
import equimo.models as em
|
|
17
|
+
from equimo import __version__
|
|
18
|
+
|
|
19
|
+
DEFAULT_REPOSITORY_URL = (
|
|
20
|
+
"https://huggingface.co/poiretclement/equimo/resolve/main/models/default"
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def save_model(
|
|
25
|
+
path: Path,
|
|
26
|
+
model: eqx.Module,
|
|
27
|
+
model_config: dict,
|
|
28
|
+
torch_hub_cfg: list[str] | dict = {},
|
|
29
|
+
timm_cfg: list = [],
|
|
30
|
+
compression: bool = True,
|
|
31
|
+
):
|
|
32
|
+
"""Save an Equinox model with its configuration and metadata to disk.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
path (Path): Target path where the model will be saved. If compression is True
|
|
36
|
+
and path doesn't end with '.tar.lz4', it will be automatically appended.
|
|
37
|
+
model (eqx.Module): The Equinox model to be saved.
|
|
38
|
+
model_config (dict): Configuration dictionary containing model hyperparameters.
|
|
39
|
+
torch_hub_cfg (list[str]): List of torch hub configuration parameters.
|
|
40
|
+
timm_cfg (list[str]): List of timm configuration parameters.
|
|
41
|
+
compression (bool, optional): Whether to compress the saved model using LZ4.
|
|
42
|
+
Defaults to True.
|
|
43
|
+
|
|
44
|
+
The function saves:
|
|
45
|
+
- Model weights using Equinox serialization
|
|
46
|
+
- Metadata including model configuration, torch hub config, and version info
|
|
47
|
+
- If compression=True: Creates a .tar.lz4 archive containing both files
|
|
48
|
+
- If compression=False: Creates a directory containing both files
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
logger.info(f"Saving model to {path}...")
|
|
52
|
+
|
|
53
|
+
metadata = {
|
|
54
|
+
"model_config": model_config,
|
|
55
|
+
"torch_hub_cfg": torch_hub_cfg,
|
|
56
|
+
"timm": timm_cfg,
|
|
57
|
+
"jax_version": jax.__version__,
|
|
58
|
+
"equinox_version": eqx.__version__,
|
|
59
|
+
"equimo_version": __version__,
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.debug(f"Metadata: {metadata}")
|
|
63
|
+
|
|
64
|
+
if compression:
|
|
65
|
+
logger.info("Compressing...")
|
|
66
|
+
if not path.name.endswith(".tar.lz4"):
|
|
67
|
+
path = path.with_name(path.name + ".tar.lz4")
|
|
68
|
+
|
|
69
|
+
with tempfile.TemporaryDirectory() as tmp_dir:
|
|
70
|
+
tmp_path = Path(tmp_dir)
|
|
71
|
+
with open(tmp_path / "metadata.json", "w") as f:
|
|
72
|
+
json.dump(metadata, f)
|
|
73
|
+
|
|
74
|
+
# Save model weights
|
|
75
|
+
eqx.tree_serialise_leaves(tmp_path / "weights.eqx", model)
|
|
76
|
+
|
|
77
|
+
# Create compressed archive
|
|
78
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
with lz4.frame.open(path, "wb") as f_out:
|
|
80
|
+
with tarfile.open(fileobj=f_out, mode="w") as tar:
|
|
81
|
+
tar.add(tmp_path / "metadata.json", arcname="metadata.json")
|
|
82
|
+
tar.add(tmp_path / "weights.eqx", arcname="weights.eqx")
|
|
83
|
+
else:
|
|
84
|
+
path.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
with open(path / "metadata.json", "w") as f:
|
|
86
|
+
json.dump(metadata, f)
|
|
87
|
+
eqx.tree_serialise_leaves(path / "weights.eqx", model)
|
|
88
|
+
|
|
89
|
+
logger.info("Model succesfully saved.")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
@logger.catch
|
|
93
|
+
def download(identifier: str, repository: str) -> Path:
|
|
94
|
+
"""Download a model archive from a specified repository.
|
|
95
|
+
|
|
96
|
+
Args:
|
|
97
|
+
identifier (str): Unique identifier for the model to download.
|
|
98
|
+
repository (str): Base URL of the repository containing the model.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
Path: Local path to the downloaded model archive.
|
|
102
|
+
|
|
103
|
+
The function:
|
|
104
|
+
- Constructs the download URL using the repository and identifier
|
|
105
|
+
- Checks for existing cached file in ~/.cache/equimo/
|
|
106
|
+
- Downloads and saves the model if not cached
|
|
107
|
+
- Verifies the download using HTTPS
|
|
108
|
+
- Raises HTTP errors if download fails
|
|
109
|
+
"""
|
|
110
|
+
|
|
111
|
+
logger.info(f"Downloading {identifier}...")
|
|
112
|
+
|
|
113
|
+
model = identifier.split("_")[0]
|
|
114
|
+
url = f"{repository}/{model}/{identifier}.tar.lz4"
|
|
115
|
+
path = Path(f"~/.cache/equimo/{model}/{identifier}.tar.lz4").expanduser()
|
|
116
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
117
|
+
|
|
118
|
+
if path.exists():
|
|
119
|
+
logger.info("Archive already downloaded, using cached file.")
|
|
120
|
+
return path
|
|
121
|
+
|
|
122
|
+
res = requests.get(url, verify=True)
|
|
123
|
+
res.raise_for_status()
|
|
124
|
+
|
|
125
|
+
with open(path.absolute(), "wb") as f:
|
|
126
|
+
f.write(res.content)
|
|
127
|
+
|
|
128
|
+
return path
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@logger.catch
|
|
132
|
+
def load_model(
|
|
133
|
+
cls: str,
|
|
134
|
+
identifier: str | None = None,
|
|
135
|
+
path: Path | None = None,
|
|
136
|
+
repository: str = DEFAULT_REPOSITORY_URL,
|
|
137
|
+
inference_mode: bool = True,
|
|
138
|
+
**model_kwargs,
|
|
139
|
+
) -> eqx.Module:
|
|
140
|
+
"""Load an Equinox model from either a local path or remote repository.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
cls (str): Model class identifier. Must be one of: 'vit', 'mlla', 'vssd',
|
|
144
|
+
'shvit', 'fastervit', 'partialformer'.
|
|
145
|
+
identifier (str | None, optional): Remote model identifier for downloading.
|
|
146
|
+
Mutually exclusive with path. Defaults to None.
|
|
147
|
+
path (Path | None, optional): Local path to load model from.
|
|
148
|
+
Mutually exclusive with identifier. Defaults to None.
|
|
149
|
+
repository (str, optional): Base URL for model download.
|
|
150
|
+
Defaults to DEFAULT_REPOSITORY_URL.
|
|
151
|
+
inference_mode (bool): Disables dropouts if True.
|
|
152
|
+
Defaults to True.
|
|
153
|
+
model_kwargs: kwargs passed to model instanciation. Overrides metadatas.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
eqx.Module: Loaded and initialized model with weights.
|
|
157
|
+
|
|
158
|
+
Raises:
|
|
159
|
+
ValueError: If both identifier and path are None or if both are provided.
|
|
160
|
+
ValueError: If cls is not one of the supported model types.
|
|
161
|
+
|
|
162
|
+
The function:
|
|
163
|
+
- Downloads model if identifier is provided
|
|
164
|
+
- Handles both compressed (.tar.lz4) and uncompressed formats
|
|
165
|
+
- Loads model configuration and metadata
|
|
166
|
+
- Reconstructs model architecture and loads weights
|
|
167
|
+
- Supports caching of downloaded and decompressed files
|
|
168
|
+
"""
|
|
169
|
+
|
|
170
|
+
if identifier is None and path is None:
|
|
171
|
+
raise ValueError(
|
|
172
|
+
"Both `identifier` and `path` are None. Please provide one of them."
|
|
173
|
+
)
|
|
174
|
+
if identifier and path:
|
|
175
|
+
raise ValueError(
|
|
176
|
+
"Both `identifier` and `path` are defined. Please provide only one of them."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
if identifier:
|
|
180
|
+
path = download(identifier, repository)
|
|
181
|
+
|
|
182
|
+
load_path = path
|
|
183
|
+
|
|
184
|
+
logger.info(f"Loading a {cls} model...")
|
|
185
|
+
|
|
186
|
+
if path.suffixes == [".tar", ".lz4"]:
|
|
187
|
+
logger.info("Decompressing...")
|
|
188
|
+
# Handle compressed archive
|
|
189
|
+
decompressed_dir = path.with_suffix("").with_suffix("") # Remove .tar.lz4
|
|
190
|
+
|
|
191
|
+
# Check if we need to decompress
|
|
192
|
+
if not decompressed_dir.exists() or (
|
|
193
|
+
decompressed_dir.stat().st_mtime < path.stat().st_mtime
|
|
194
|
+
):
|
|
195
|
+
decompressed_dir.mkdir(parents=True, exist_ok=True)
|
|
196
|
+
with lz4.frame.open(path, "rb") as f_in:
|
|
197
|
+
with tarfile.open(fileobj=f_in, mode="r") as tar:
|
|
198
|
+
tar.extractall(decompressed_dir)
|
|
199
|
+
|
|
200
|
+
load_path = decompressed_dir
|
|
201
|
+
|
|
202
|
+
# Load metadata and model
|
|
203
|
+
with open(load_path / "metadata.json", "r") as f:
|
|
204
|
+
metadata = json.load(f)
|
|
205
|
+
|
|
206
|
+
logger.debug(f"Metadata: {metadata}")
|
|
207
|
+
|
|
208
|
+
model_eqm_version = metadata.get("equimo_version", "0.2.0")
|
|
209
|
+
if Version.parse(model_eqm_version) > Version.parse(__version__):
|
|
210
|
+
logger.warning(
|
|
211
|
+
f"The model you are importing was packaged with equimo v{model_eqm_version}, but you have equimo v{__version__}. You may face unexpected errors. Please consider updating equimo."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
# Class resolution
|
|
215
|
+
match cls:
|
|
216
|
+
case "vit":
|
|
217
|
+
model_cls = em.VisionTransformer
|
|
218
|
+
case "mlla":
|
|
219
|
+
model_cls = em.Mlla
|
|
220
|
+
case "vssd":
|
|
221
|
+
model_cls = em.Vssd
|
|
222
|
+
case "shvit":
|
|
223
|
+
model_cls = em.SHViT
|
|
224
|
+
case "fastervit":
|
|
225
|
+
model_cls = em.FasterViT
|
|
226
|
+
case "partialformer":
|
|
227
|
+
model_cls = em.PartialFormer
|
|
228
|
+
case "experimental.textencoder":
|
|
229
|
+
from equimo.experimental.text import TextEncoder
|
|
230
|
+
|
|
231
|
+
model_cls = TextEncoder
|
|
232
|
+
case _:
|
|
233
|
+
raise ValueError(f"Unknown model class: {cls}")
|
|
234
|
+
|
|
235
|
+
# Reconstruct model skeleton
|
|
236
|
+
kwargs = metadata["model_config"] | model_kwargs
|
|
237
|
+
model = model_cls(**kwargs, key=jax.random.PRNGKey(42))
|
|
238
|
+
|
|
239
|
+
# Load weights and set inference mode
|
|
240
|
+
model = eqx.tree_deserialise_leaves(load_path / "weights.eqx", model)
|
|
241
|
+
model = eqx.nn.inference_mode(model, inference_mode)
|
|
242
|
+
|
|
243
|
+
logger.info("Model loaded successfully.")
|
|
244
|
+
|
|
245
|
+
return model
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _center_crop_square(array: jnp.ndarray) -> jnp.ndarray:
|
|
249
|
+
"""Center-crop a HxW(xC) array to a square of side min(H, W).
|
|
250
|
+
|
|
251
|
+
Args:
|
|
252
|
+
array: jnp.ndarray with shape (H, W) or (H, W, C).
|
|
253
|
+
|
|
254
|
+
Returns:
|
|
255
|
+
Center-cropped array with shape (M, M) or (M, M, C) where M = min(H, W).
|
|
256
|
+
"""
|
|
257
|
+
if array.ndim < 2:
|
|
258
|
+
raise ValueError("Input array must have at least 2 dimensions (H, W[, C]).")
|
|
259
|
+
h, w = array.shape[:2]
|
|
260
|
+
if h == w:
|
|
261
|
+
return array
|
|
262
|
+
m = min(h, w)
|
|
263
|
+
top = (h - m) // 2
|
|
264
|
+
left = (w - m) // 2
|
|
265
|
+
return array[top : top + m, left : left + m, ...]
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def load_image(
|
|
269
|
+
path: str,
|
|
270
|
+
mean: Optional[list[float]] = None,
|
|
271
|
+
std: Optional[list[float]] = None,
|
|
272
|
+
size: Optional[int] = None,
|
|
273
|
+
center_crop: bool = False,
|
|
274
|
+
):
|
|
275
|
+
"""Load an image and perform minor preprocessing.
|
|
276
|
+
|
|
277
|
+
Args:
|
|
278
|
+
path (str): Path of the image.
|
|
279
|
+
mean (list, optional): Channel mean for normalization.
|
|
280
|
+
std (list, optional): Channel std for normalization.
|
|
281
|
+
size (int, optional): Size to which resize the image.
|
|
282
|
+
center_crop (bool, optional): If True, center-crop to square prior to resizing.
|
|
283
|
+
|
|
284
|
+
Returns:
|
|
285
|
+
jnp.array: The loaded image.
|
|
286
|
+
|
|
287
|
+
Raises:
|
|
288
|
+
ImportError: If PIL can't be loaded.
|
|
289
|
+
"""
|
|
290
|
+
try:
|
|
291
|
+
from PIL import Image
|
|
292
|
+
except ImportError:
|
|
293
|
+
raise ImportError("PIL is needed to be able to load images.")
|
|
294
|
+
|
|
295
|
+
with open(path, "rb") as fd:
|
|
296
|
+
image_bytes = io.BytesIO(fd.read())
|
|
297
|
+
pil_image = Image.open(image_bytes)
|
|
298
|
+
|
|
299
|
+
array = jnp.array(pil_image).astype(jnp.float32) / 255.0
|
|
300
|
+
|
|
301
|
+
if center_crop:
|
|
302
|
+
array = _center_crop_square(array)
|
|
303
|
+
|
|
304
|
+
if size is not None:
|
|
305
|
+
array = jax.image.resize(array, (size, size, 3), method="bilinear")
|
|
306
|
+
|
|
307
|
+
if mean is not None and std is not None:
|
|
308
|
+
mean = jnp.array(mean)[None, None, :]
|
|
309
|
+
std = jnp.array(std)[None, None, :]
|
|
310
|
+
|
|
311
|
+
array = (array - mean) / std
|
|
312
|
+
|
|
313
|
+
return array.transpose(2, 0, 1)
|
|
File without changes
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
from typing import Callable
|
|
2
|
+
import jax
|
|
3
|
+
from functools import partial
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def get_act(activation: str | Callable) -> Callable:
|
|
7
|
+
"""Get an activation function from its common name.
|
|
8
|
+
|
|
9
|
+
This is necessary because configs have to be stringified and stored as
|
|
10
|
+
json files to allow (de)serialization.
|
|
11
|
+
"""
|
|
12
|
+
if not isinstance(activation, str):
|
|
13
|
+
return activation
|
|
14
|
+
|
|
15
|
+
match activation:
|
|
16
|
+
case "relu":
|
|
17
|
+
return jax.nn.relu
|
|
18
|
+
case "gelu":
|
|
19
|
+
return jax.nn.gelu
|
|
20
|
+
case "exactgelu":
|
|
21
|
+
return partial(jax.nn.gelu, approximate=False)
|
|
22
|
+
case "silu":
|
|
23
|
+
return jax.nn.silu
|
|
24
|
+
case "elu":
|
|
25
|
+
return jax.nn.elu
|
|
26
|
+
case "sigmoid":
|
|
27
|
+
return jax.nn.sigmoid
|
|
28
|
+
case "hard_sigmoid":
|
|
29
|
+
return jax.nn.hard_sigmoid
|
|
30
|
+
case "softmax":
|
|
31
|
+
return jax.nn.softmax
|
|
32
|
+
case _:
|
|
33
|
+
raise ValueError(f"Got an unknown activation string: {activation}")
|