Equimo 0.1.0__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.
@@ -0,0 +1,7 @@
1
+ from .emamodel import EmaModel
2
+ from .fastervit import FasterViT
3
+ from .mlla import Mlla
4
+ from .partialformer import PartialFormer
5
+ from .shvit import SHViT
6
+ from .vit import VisionTransformer
7
+ from .vssd import Vssd
@@ -0,0 +1,99 @@
1
+ # Draws inspiration from Timm's ModelEmaV2
2
+ # <https://github.com/huggingface/pytorch-image-models/blob/main/timm/utils/model_ema.py#L83>
3
+
4
+ from copy import deepcopy
5
+
6
+ import equinox as eqx
7
+ import jax
8
+
9
+
10
+ class EmaModel:
11
+ """
12
+ Exponential Moving Average (EMA) model wrapper.
13
+
14
+ This class implements an EMA model, which maintains a moving average of the weights
15
+ of another model. This can be useful for model smoothing and improving generalization.
16
+
17
+ Example usage can be Sharpness-Aware Training for Free[1].
18
+
19
+ Attributes:
20
+ model (nnx.Module): The underlying model to apply EMA to.
21
+ decay (float): The decay rate for the moving average. Default is 0.9999.
22
+
23
+ References:
24
+ [1] Du, et al. Sharpness-Aware Training for Free (2022). https://arxiv.org/abs/2205.14083
25
+ """
26
+
27
+ model: eqx.Module
28
+ decay: float
29
+
30
+ def __init__(self, model: eqx.Module, decay: float = 0.9999):
31
+ self.model = deepcopy(model)
32
+ self.decay = decay
33
+ self.ema_params = None
34
+
35
+ def __call__(self, *args, **kwargs):
36
+ """
37
+ Call the underlying model.
38
+
39
+ This method allows the EMA model to be used as a drop-in replacement for the original model.
40
+
41
+ Args:
42
+ *args: Positional arguments to pass to the underlying model.
43
+ **kwargs: Keyword arguments to pass to the underlying model.
44
+
45
+ Returns:
46
+ The output of the underlying model.
47
+ """
48
+ return self.model(*args, **kwargs)
49
+
50
+ def initialize_ema(self, params):
51
+ """
52
+ Initialize the EMA parameters.
53
+
54
+ This method creates a copy of the initial model parameters to start the EMA process.
55
+
56
+ Args:
57
+ params: The initial parameters of the model.
58
+ """
59
+ self.ema_params = jax.tree.map(lambda x: x.copy(), params)
60
+
61
+ def update(self, params):
62
+ """
63
+ Update the EMA parameters.
64
+
65
+ This method updates the EMA parameters based on the current model parameters.
66
+
67
+ Args:
68
+ params: The current parameters of the model.
69
+ """
70
+ if self.ema_params is None:
71
+ self.initialize_ema(params)
72
+ else:
73
+ self.ema_params = jax.tree.map(
74
+ lambda ema, new: self.decay * ema + (1.0 - self.decay) * new,
75
+ self.ema_params,
76
+ params,
77
+ )
78
+
79
+ self.model = eqx.apply_updates(self.model, self.ema_params)
80
+
81
+ def set(self, params):
82
+ """
83
+ Set the EMA parameters directly.
84
+
85
+ This method allows for manual setting of the EMA parameters.
86
+
87
+ Args:
88
+ params: The parameters to set as the new EMA parameters.
89
+ """
90
+ self.ema_params = jax.tree.map(lambda x: x.copy(), params)
91
+
92
+ def get_ema_params(self):
93
+ """
94
+ Get the current EMA parameters.
95
+
96
+ Returns:
97
+ The current EMA parameters.
98
+ """
99
+ return self.ema_params
@@ -0,0 +1,488 @@
1
+ from typing import Callable, List, Literal, Optional
2
+
3
+ import equinox as eqx
4
+ import jax
5
+ import jax.numpy as jnp
6
+ import jax.random as jr
7
+ from einops import rearrange
8
+ from jaxtyping import Array, Float, PRNGKeyArray
9
+
10
+ from equimo.layers.attention import HATBlock, WindowedAttention
11
+ from equimo.layers.convolution import ConvBlock
12
+ from equimo.layers.downsample import Downsampler
13
+ from equimo.layers.ffn import Mlp
14
+ from equimo.layers.patch import ConvPatchEmbed
15
+ from equimo.layers.sharing import LayerSharing
16
+ from equimo.utils import pool_sd, to_list
17
+
18
+
19
+ class LayerSharingWithCT(LayerSharing):
20
+ """Layer sharing implementation that handles carrier tokens (CT).
21
+
22
+ Extends LayerSharing to support passing and updating carrier tokens through
23
+ the network layers while maintaining layer sharing functionality.
24
+ """
25
+
26
+ def __call__(
27
+ self,
28
+ x: Array,
29
+ ct: Array,
30
+ # *args,
31
+ enable_dropout: bool,
32
+ key: PRNGKeyArray,
33
+ **kwargs,
34
+ ):
35
+ """Apply layer sharing with carrier token support.
36
+
37
+ Args:
38
+ x: Input tensor
39
+ ct: carrier token tensor
40
+ enable_dropout: Whether to enable dropout during inference
41
+ key: PRNG key for random operations
42
+
43
+ Returns:
44
+ Tuple of (processed tensor, updated carrier token)
45
+ """
46
+ if self.repeat == 1:
47
+ return self.f(
48
+ x,
49
+ ct,
50
+ # *args,
51
+ enable_dropout=enable_dropout,
52
+ key=key,
53
+ **kwargs,
54
+ )
55
+
56
+ keys = jr.split(key, self.repeat)
57
+ reshape = len(x.shape) == 3
58
+
59
+ for i in range(self.repeat):
60
+ if reshape:
61
+ _, h, w = x.shape
62
+ lora_x = rearrange(x, "c h w -> (h w) c")
63
+ else:
64
+ lora_x = x
65
+ lora_output = self.dropouts[i](
66
+ jax.vmap(self.loras[i])(lora_x),
67
+ inference=not enable_dropout,
68
+ key=keys[i],
69
+ )
70
+ if reshape:
71
+ lora_output = rearrange(
72
+ lora_output,
73
+ "(h w) c -> c h w",
74
+ h=h,
75
+ w=w,
76
+ )
77
+
78
+ x, ct = self.f(
79
+ x,
80
+ ct,
81
+ enable_dropout=enable_dropout,
82
+ key=key,
83
+ **kwargs,
84
+ )
85
+
86
+ x += lora_output
87
+
88
+ return x, ct
89
+
90
+
91
+ class TokenInitializer(eqx.Module):
92
+ """Initializes and processes carrier tokens for the network.
93
+
94
+ Creates carrier tokens by applying positional embeddings and pooling
95
+ to the input features. Used to generate global tokens for attention.
96
+ """
97
+
98
+ ct_size: int = eqx.field(static=True)
99
+ stride: int = eqx.field(static=True)
100
+ kernel: int = eqx.field(static=True)
101
+
102
+ pos_embed: eqx.nn.Conv
103
+ pool: eqx.nn.AvgPool2d
104
+
105
+ def __init__(
106
+ self,
107
+ dim: int,
108
+ input_resolution: int,
109
+ window_size: int,
110
+ *,
111
+ key: PRNGKeyArray,
112
+ ct_size: int = 1,
113
+ **kwargs,
114
+ ):
115
+ self.ct_size = ct_size
116
+ output_size = int(ct_size * input_resolution / window_size)
117
+ self.stride = int(input_resolution / output_size)
118
+ self.kernel = int(input_resolution - (output_size - 1) * self.stride)
119
+
120
+ self.pos_embed = eqx.nn.Conv(
121
+ num_spatial_dims=2,
122
+ in_channels=dim,
123
+ out_channels=dim,
124
+ kernel_size=3,
125
+ stride=1,
126
+ padding=1,
127
+ groups=dim,
128
+ key=key,
129
+ )
130
+ self.pool = eqx.nn.AvgPool2d(kernel_size=self.kernel, stride=self.stride)
131
+
132
+ def __call__(
133
+ self,
134
+ x: Float[Array, "channels height weight"],
135
+ ) -> Float[Array, "new_channels seqlen"]:
136
+ x = self.pos_embed(x)
137
+ x = self.pool(x)
138
+
139
+ ct = rearrange(
140
+ x, "c (h h1) (w w1) -> (h w h1 w1) c", h1=self.ct_size, w1=self.ct_size
141
+ )
142
+
143
+ return ct
144
+
145
+
146
+ class BlockChunk(eqx.Module):
147
+ """A chunk of processing blocks with optional downsampling and global tokenization.
148
+
149
+ Handles a sequence of processing blocks (either ConvBlock or HATBlock),
150
+ with support for window partitioning, global tokenization, and downsampling.
151
+ """
152
+
153
+ reshape: bool = eqx.field(static=True)
154
+ downsampler_contains_dropout: bool = eqx.field(static=True)
155
+ is_hat: bool = eqx.field(static=True)
156
+ ct_size: int = eqx.field(static=True)
157
+ window_size: bool = eqx.field(static=True)
158
+ do_gt: bool = eqx.field(static=True)
159
+
160
+ blocks: List[eqx.Module]
161
+ downsample: eqx.Module
162
+ global_tokenizer: Optional[TokenInitializer]
163
+
164
+ def __init__(
165
+ self,
166
+ input_resolution: int,
167
+ window_size: int,
168
+ depth: int,
169
+ *,
170
+ key: PRNGKeyArray,
171
+ block: eqx.Module = HATBlock,
172
+ repeat: int = 1,
173
+ downsampler: eqx.Module = Downsampler,
174
+ downsampler_contains_dropout: bool = False,
175
+ only_local: bool = False,
176
+ hierarchy: bool = True,
177
+ ct_size: int = 1,
178
+ **kwargs,
179
+ ):
180
+ key_ds, key_gt, *block_subkeys = jr.split(key, depth + 2)
181
+ if not isinstance(downsampler, eqx.nn.Identity):
182
+ if kwargs.get("dim") is None:
183
+ raise ValueError(
184
+ "Using a downsampler requires passing a `dim` argument."
185
+ )
186
+
187
+ # self.reshape = block is not ConvBlock
188
+ self.reshape = True # TODO
189
+ self.downsampler_contains_dropout = downsampler_contains_dropout
190
+ self.is_hat = block is HATBlock
191
+ self.ct_size = ct_size
192
+ self.window_size = window_size
193
+
194
+ keys_to_spread = [
195
+ k for k, v in kwargs.items() if isinstance(v, list) and len(v) == depth
196
+ ]
197
+
198
+ self.blocks = []
199
+ for i in range(depth):
200
+ config = kwargs | {k: kwargs[k][i] for k in keys_to_spread}
201
+
202
+ if self.is_hat:
203
+ config = config | {
204
+ "sr_ratio": (
205
+ input_resolution // window_size if not only_local else 1
206
+ ),
207
+ "window_size": window_size,
208
+ "last": i == depth - 1,
209
+ "ct_size": self.ct_size,
210
+ }
211
+
212
+ wrapper = LayerSharingWithCT if self.is_hat else LayerSharing
213
+ self.blocks.append(
214
+ wrapper(
215
+ dim=kwargs.get("dim"),
216
+ f=block(**config, key=block_subkeys[i]),
217
+ repeat=repeat,
218
+ key=block_subkeys[i],
219
+ ),
220
+ )
221
+
222
+ self.downsample = downsampler(dim=kwargs.get("dim"), key=key_ds)
223
+
224
+ if (
225
+ len(self.blocks)
226
+ and not only_local
227
+ and input_resolution // window_size > 1
228
+ and hierarchy
229
+ and self.is_hat
230
+ ):
231
+ self.do_gt = True
232
+ self.global_tokenizer = TokenInitializer(
233
+ kwargs.get("dim"),
234
+ input_resolution,
235
+ window_size,
236
+ ct_size=self.ct_size,
237
+ key=key_gt,
238
+ )
239
+ else:
240
+ self.do_gt = False
241
+ self.global_tokenizer = eqx.nn.Identity()
242
+
243
+ def window_partition(
244
+ self,
245
+ x: Float[Array, "channels height width"],
246
+ window_size: int,
247
+ ) -> Float[Array, "patches window_size channels"]:
248
+ """Partition input tensor into windows.
249
+
250
+ Args:
251
+ x: Input tensor of shape (channels, height, width)
252
+ window_size: Size of each window
253
+
254
+ Returns:
255
+ Tensor partitioned into windows of specified size
256
+ """
257
+ return rearrange(
258
+ x,
259
+ "c (h h1) (w w1) -> (h w) (h1 w1) c",
260
+ h1=window_size,
261
+ w1=window_size,
262
+ )
263
+
264
+ def window_reverse(
265
+ self,
266
+ x: Float[Array, "patches window_size channels"],
267
+ window_size: int,
268
+ h: int,
269
+ w: int,
270
+ ) -> Float[Array, "channels height width"]:
271
+ """Reverse window partitioning to original tensor shape.
272
+
273
+ Args:
274
+ x: Window partitioned tensor
275
+ window_size: Size of each window
276
+ h: Original height
277
+ w: Original width
278
+
279
+ Returns:
280
+ Tensor restored to original spatial dimensions
281
+ """
282
+ return rearrange(
283
+ x,
284
+ "(h w) (h1 w1) c -> c (h h1) (w w1)",
285
+ h=h // window_size,
286
+ w=w // window_size,
287
+ h1=window_size,
288
+ w1=window_size,
289
+ )
290
+
291
+ def __call__(
292
+ self,
293
+ x: Float[Array, "channels height width"],
294
+ enable_dropout: bool,
295
+ key: PRNGKeyArray,
296
+ ) -> Float[Array, "..."]:
297
+ keys = jr.split(key, len(self.blocks))
298
+ ct = self.global_tokenizer(x) if self.do_gt else None
299
+ c, h, w = x.shape
300
+
301
+ if self.is_hat:
302
+ x = self.window_partition(x, self.window_size)
303
+ for blk, key_block in zip(self.blocks, keys):
304
+ x, ct = jax.vmap(
305
+ blk,
306
+ in_axes=(0, None, None, None),
307
+ out_axes=(0, None),
308
+ )(x, ct, enable_dropout, key_block)
309
+ x = self.window_reverse(x, self.window_size, h, w)
310
+ else:
311
+ for blk, key_block in zip(self.blocks, keys):
312
+ x = blk(x, enable_dropout=enable_dropout, key=key_block)
313
+
314
+ if self.downsampler_contains_dropout:
315
+ x = self.downsample(x, enable_dropout, key)
316
+ else:
317
+ x = self.downsample(x)
318
+
319
+ return x
320
+
321
+
322
+ class FasterViT(eqx.Module):
323
+ """FasterViT: Fast Vision Transformers with Hierarchical Attention[1]
324
+
325
+ Implements a vision transformer architecture that combines convolution and
326
+ hierarchical attention mechanisms for efficient image processing. Features
327
+ carrier tokens, window attention, and progressive feature resolution reduction.
328
+
329
+ References:
330
+ [1]: Hatamizadeh, et al., 2023. https://arxiv.org/abs/2306.06189
331
+ """
332
+
333
+ patch_embed: ConvPatchEmbed
334
+ blocks: List[eqx.Module]
335
+ norm: eqx.Module
336
+ head: eqx.Module
337
+
338
+ dim: int = eqx.field(static=True)
339
+ global_pool: str = eqx.field(static=True)
340
+
341
+ def __init__(
342
+ self,
343
+ img_size: int,
344
+ in_channels: int,
345
+ dim: int,
346
+ in_dim: int,
347
+ num_heads: int | List[int],
348
+ hat: bool | List[bool],
349
+ depths: List[int],
350
+ window_size: int | List[int],
351
+ ct_size: int,
352
+ *,
353
+ key: PRNGKeyArray,
354
+ drop_path_rate: float = 0.0,
355
+ drop_path_uniform: bool = False,
356
+ block: eqx.Module = HATBlock,
357
+ repeat: int = 1,
358
+ mlp_ratio: float = 4.0,
359
+ qkv_bias: bool = True,
360
+ proj_bias: bool = True,
361
+ qk_norm: bool = False,
362
+ attn_drop: float = 0.0,
363
+ proj_drop: float = 0.0,
364
+ act_layer: Callable = jax.nn.gelu,
365
+ attn_layer: eqx.Module = WindowedAttention,
366
+ ffn_layer: eqx.Module = Mlp,
367
+ ffn_bias: bool = True,
368
+ norm_layer: eqx.Module = eqx.nn.LayerNorm,
369
+ init_values: float | None = 1e-5,
370
+ ls_convblock: bool = False,
371
+ do_propagation: bool = False,
372
+ global_pool: Literal["", "token", "avg", "avgmax", "max"] = "avg",
373
+ num_classes: int = 1000,
374
+ interpolate_antialias: bool = False,
375
+ **kwargs,
376
+ ):
377
+ depth = sum(depths)
378
+ key_patchemb, key_posemb, key_cls, key_reg, key_head, *block_subkeys = jr.split(
379
+ key, 5 + len(depths)
380
+ )
381
+ self.dim = dim
382
+ self.global_pool = global_pool
383
+
384
+ self.patch_embed = ConvPatchEmbed(
385
+ in_channels,
386
+ in_dim,
387
+ dim,
388
+ key=key_patchemb,
389
+ )
390
+
391
+ if drop_path_uniform:
392
+ dpr = [drop_path_rate] * depth
393
+ else:
394
+ dpr = list(jnp.linspace(0.0, drop_path_rate, depth))
395
+
396
+ n_chunks = len(depths)
397
+ num_heads = to_list(num_heads, n_chunks)
398
+ hat = to_list(hat, n_chunks)
399
+ attn_layer = to_list(attn_layer, n_chunks)
400
+ window_size = to_list(window_size, n_chunks)
401
+ self.blocks = [
402
+ BlockChunk(
403
+ block=ConvBlock if i < 2 else HATBlock,
404
+ repeat=repeat,
405
+ dim=int(dim * 2**i),
406
+ depth=depths[i],
407
+ input_resolution=int(2 ** (-2 - i) * img_size),
408
+ window_size=window_size[i],
409
+ ct_size=ct_size,
410
+ num_heads=num_heads[i],
411
+ mlp_ratio=mlp_ratio,
412
+ drop_path=dpr[sum(depths[:i]) : sum(depths[: i + 1])],
413
+ qkv_bias=qkv_bias,
414
+ proj_bias=proj_bias,
415
+ qk_norm=qk_norm,
416
+ attn_drop=attn_drop,
417
+ proj_drop=proj_drop,
418
+ act_layer=act_layer,
419
+ attn_layer=attn_layer[i],
420
+ ffn_layer=ffn_layer,
421
+ ffn_bias=ffn_bias,
422
+ norm_layer=norm_layer,
423
+ init_values=None if i < 2 and not ls_convblock else init_values,
424
+ downsampler=Downsampler if i < len(depths) - 1 else eqx.nn.Identity,
425
+ only_local=not hat[i],
426
+ do_propagation=do_propagation,
427
+ key=block_subkeys[i],
428
+ )
429
+ for i, depth in enumerate(depths)
430
+ ]
431
+
432
+ num_features = int(dim * 2 ** (len(depths) - 1))
433
+ self.norm = norm_layer(num_features)
434
+ self.head = eqx.nn.Linear(num_features, num_classes, key=key_head)
435
+
436
+ def features(
437
+ self,
438
+ x: Float[Array, "channels height width"],
439
+ enable_dropout: bool,
440
+ key: PRNGKeyArray,
441
+ ) -> Float[Array, "seqlen dim"]:
442
+ """Extract features from input image.
443
+
444
+ Args:
445
+ x: Input image tensor
446
+ enable_dropout: Whether to enable dropout during inference
447
+ key: PRNG key for random operations
448
+
449
+ Returns:
450
+ Processed feature tensor
451
+ """
452
+ key_posdrop, *block_subkeys = jr.split(key, len(self.blocks) + 1)
453
+ x = self.patch_embed(x)
454
+
455
+ for blk, key_block in zip(self.blocks, block_subkeys):
456
+ x = blk(x, enable_dropout=enable_dropout, key=key_block)
457
+
458
+ return x
459
+
460
+ def __call__(
461
+ self,
462
+ x: Float[Array, "channels height width"],
463
+ enable_dropout: bool,
464
+ key: PRNGKeyArray,
465
+ ) -> Float[Array, "num_classes"]:
466
+ """Process input image through the full network.
467
+
468
+ Args:
469
+ x: Input image tensor
470
+ enable_dropout: Whether to enable dropout during inference
471
+ key: PRNG key for random operations
472
+
473
+ Returns:
474
+ Classification logits for each class
475
+ """
476
+ x = self.features(x, enable_dropout, key)
477
+ x = rearrange(x, "c h w -> (h w) c")
478
+ x = jax.vmap(self.norm)(x)
479
+ x = pool_sd(
480
+ x,
481
+ num_prefix_tokens=0,
482
+ pool_type=self.global_pool,
483
+ reduce_include_prefix=False,
484
+ )
485
+
486
+ x = self.head(x)
487
+
488
+ return x