Equimo 0.4.7__tar.gz → 0.5.0a2__tar.gz

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.
Files changed (47) hide show
  1. {equimo-0.4.7/src/Equimo.egg-info → equimo-0.5.0a2}/PKG-INFO +1 -1
  2. {equimo-0.4.7 → equimo-0.5.0a2}/pyproject.toml +1 -1
  3. {equimo-0.4.7 → equimo-0.5.0a2/src/Equimo.egg-info}/PKG-INFO +1 -1
  4. {equimo-0.4.7 → equimo-0.5.0a2}/src/Equimo.egg-info/SOURCES.txt +1 -0
  5. equimo-0.5.0a2/src/equimo/__init__.py +1 -0
  6. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/attention.py +9 -9
  7. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/convolution.py +315 -17
  8. equimo-0.5.0a2/src/equimo/layers/downsample.py +209 -0
  9. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/norm.py +31 -17
  10. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/patch.py +18 -21
  11. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/wavelet.py +4 -1
  12. equimo-0.5.0a2/src/equimo/models/attnet.py +243 -0
  13. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/fastervit.py +8 -4
  14. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/shvit.py +2 -117
  15. equimo-0.4.7/src/equimo/__init__.py +0 -1
  16. equimo-0.4.7/src/equimo/layers/downsample.py +0 -58
  17. {equimo-0.4.7 → equimo-0.5.0a2}/LICENSE.md +0 -0
  18. {equimo-0.4.7 → equimo-0.5.0a2}/README.md +0 -0
  19. {equimo-0.4.7 → equimo-0.5.0a2}/setup.cfg +0 -0
  20. {equimo-0.4.7 → equimo-0.5.0a2}/src/Equimo.egg-info/dependency_links.txt +0 -0
  21. {equimo-0.4.7 → equimo-0.5.0a2}/src/Equimo.egg-info/requires.txt +0 -0
  22. {equimo-0.4.7 → equimo-0.5.0a2}/src/Equimo.egg-info/top_level.txt +0 -0
  23. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/conversion/__init__.py +0 -0
  24. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/conversion/utils.py +0 -0
  25. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/experimental/__init__.py +0 -0
  26. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/experimental/text.py +0 -0
  27. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/io.py +0 -0
  28. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/__init__.py +0 -0
  29. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/activation.py +0 -0
  30. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/dropout.py +0 -0
  31. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/ffn.py +0 -0
  32. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/generic.py +0 -0
  33. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/mamba.py +0 -0
  34. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/posemb.py +0 -0
  35. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/sharing.py +0 -0
  36. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/layers/squeeze_excite.py +0 -0
  37. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/__init__.py +0 -0
  38. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/emamodel.py +0 -0
  39. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/lowformer.py +0 -0
  40. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/mlla.py +0 -0
  41. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/partialformer.py +0 -0
  42. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/reduceformer.py +0 -0
  43. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/vit.py +0 -0
  44. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/models/vssd.py +0 -0
  45. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/ops/scan.py +0 -0
  46. {equimo-0.4.7 → equimo-0.5.0a2}/src/equimo/utils.py +0 -0
  47. {equimo-0.4.7 → equimo-0.5.0a2}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Equimo
3
- Version: 0.4.7
3
+ Version: 0.5.0a2
4
4
  Summary: Implementation of popular vision models in Jax
5
5
  Classifier: Development Status :: 4 - Beta
6
6
  Classifier: Intended Audience :: Developers
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "Equimo"
3
- version = "0.4.7"
3
+ version = "0.5.0-alpha.2"
4
4
  description = "Implementation of popular vision models in Jax"
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Equimo
3
- Version: 0.4.7
3
+ Version: 0.5.0a2
4
4
  Summary: Implementation of popular vision models in Jax
5
5
  Classifier: Development Status :: 4 - Beta
6
6
  Classifier: Intended Audience :: Developers
@@ -29,6 +29,7 @@ src/equimo/layers/sharing.py
29
29
  src/equimo/layers/squeeze_excite.py
30
30
  src/equimo/layers/wavelet.py
31
31
  src/equimo/models/__init__.py
32
+ src/equimo/models/attnet.py
32
33
  src/equimo/models/emamodel.py
33
34
  src/equimo/models/fastervit.py
34
35
  src/equimo/models/lowformer.py
@@ -0,0 +1 @@
1
+ __version__ = "0.5.0-alpha.2"
@@ -356,8 +356,8 @@ class AttentionBlock(eqx.Module):
356
356
  self.norm = norm_layer(dim, eps=eps)
357
357
 
358
358
  if init_values:
359
- self.ls1 = LayerScale(dim, init_values=init_values)
360
- self.ls2 = LayerScale(dim, init_values=init_values)
359
+ self.ls1 = LayerScale(dim, axis=1, init_values=init_values)
360
+ self.ls2 = LayerScale(dim, axis=1, init_values=init_values)
361
361
  else:
362
362
  self.ls1 = self.ls2 = eqx.nn.Identity()
363
363
 
@@ -538,8 +538,8 @@ class HATBlock(eqx.Module):
538
538
  self.norm2 = norm_layer(dim, eps=eps)
539
539
 
540
540
  if init_values:
541
- self.ls1 = LayerScale(dim, init_values=init_values)
542
- self.ls2 = LayerScale(dim, init_values=init_values)
541
+ self.ls1 = LayerScale(dim, axis=1, init_values=init_values)
542
+ self.ls2 = LayerScale(dim, axis=1, init_values=init_values)
543
543
  else:
544
544
  self.ls1 = self.ls2 = eqx.nn.Identity()
545
545
 
@@ -611,17 +611,17 @@ class HATBlock(eqx.Module):
611
611
  dim, rank=2, seq_len=cr_tokens_total, key=key_hatposemb
612
612
  )
613
613
  self.hat_ls1 = (
614
- LayerScale(dim, init_values=init_values)
614
+ LayerScale(dim, axis=1, init_values=init_values)
615
615
  if init_values
616
616
  else eqx.nn.Identity()
617
617
  )
618
618
  self.hat_ls2 = (
619
- LayerScale(dim, init_values=init_values)
619
+ LayerScale(dim, axis=1, init_values=init_values)
620
620
  if init_values
621
621
  else eqx.nn.Identity()
622
622
  )
623
623
  self.hat_ls3 = (
624
- LayerScale(dim, init_values=init_values)
624
+ LayerScale(dim, axis=1, init_values=init_values)
625
625
  if init_values
626
626
  else eqx.nn.Identity()
627
627
  )
@@ -1425,8 +1425,8 @@ class PartialFormerBlock(eqx.Module):
1425
1425
  self.norm2 = norm_layer(dim, eps=eps)
1426
1426
 
1427
1427
  if init_values:
1428
- self.ls1 = LayerScale(dim, init_values=init_values)
1429
- self.ls2 = LayerScale(dim, init_values=init_values)
1428
+ self.ls1 = LayerScale(dim, axis=1, init_values=init_values)
1429
+ self.ls2 = LayerScale(dim, axis=1, init_values=init_values)
1430
1430
  else:
1431
1431
  self.ls1 = self.ls2 = eqx.nn.Identity()
1432
1432
 
@@ -39,7 +39,7 @@ class ConvBlock(eqx.Module):
39
39
  norm2: eqx.Module
40
40
  drop_path1: DropPathAdd
41
41
  act: Callable
42
- ls1: LayerScale | None
42
+ ls1: LayerScale | eqx.nn.Identity
43
43
 
44
44
  def __init__(
45
45
  self,
@@ -50,7 +50,7 @@ class ConvBlock(eqx.Module):
50
50
  kernel_size: int = 3,
51
51
  stride: int = 1,
52
52
  padding: int = 1,
53
- act_layer: Callable = jax.nn.gelu,
53
+ act_layer: Callable | None = jax.nn.gelu,
54
54
  norm_max_group: int = 32,
55
55
  drop_path: float = 0.0,
56
56
  init_values: float | None = None,
@@ -87,7 +87,7 @@ class ConvBlock(eqx.Module):
87
87
  key=key_conv1,
88
88
  )
89
89
  self.norm1 = eqx.nn.GroupNorm(num_groups1, hidden_dim)
90
- self.act = act_layer
90
+ self.act = act_layer if act_layer is not None else eqx.nn.Identity()
91
91
  self.conv2 = eqx.nn.Conv(
92
92
  num_spatial_dims=2,
93
93
  in_channels=hidden_dim,
@@ -103,18 +103,11 @@ class ConvBlock(eqx.Module):
103
103
  dpr = drop_path[0] if isinstance(drop_path, list) else float(drop_path)
104
104
  self.drop_path1 = DropPathAdd(dpr)
105
105
 
106
- self.ls1 = LayerScale(dim, init_values=init_values) if init_values else None
107
-
108
- def permute(
109
- self, x: Float[Array, "channels height width"]
110
- ) -> Float[Array, "height width channels"]:
111
- return rearrange(x, "c h w -> h w c")
112
-
113
- def depermute(
114
- self,
115
- x: Float[Array, "height width channels"],
116
- ) -> Float[Array, "channels height width"]:
117
- return rearrange(x, "h w c -> c h w")
106
+ self.ls1 = (
107
+ LayerScale(dim, init_values=init_values)
108
+ if init_values
109
+ else eqx.nn.Identity()
110
+ )
118
111
 
119
112
  def __call__(
120
113
  self,
@@ -124,8 +117,7 @@ class ConvBlock(eqx.Module):
124
117
  ) -> Float[Array, "channels height width"]:
125
118
  x2 = self.act(self.norm1(self.conv1(x)))
126
119
  x2 = self.norm2(self.conv2(x2))
127
- if self.ls1 is not None:
128
- x2 = self.depermute(jax.vmap(jax.vmap(self.ls1))(self.permute(x2)))
120
+ x2 = self.ls1(x2)
129
121
 
130
122
  return self.drop_path1(x, x2, inference=inference, key=key)
131
123
 
@@ -2017,3 +2009,309 @@ class FasterNetBlock(eqx.Module):
2017
2009
  out = self.drop_path(x, out, inference=inference, key=key_droppath)
2018
2010
 
2019
2011
  return out
2012
+
2013
+
2014
+ class GLUConv(eqx.Module):
2015
+ conv1: eqx.nn.Conv2d
2016
+ conv2: eqx.nn.Conv2d
2017
+ dwconv: eqx.nn.Conv2d | eqx.nn.Identity
2018
+ norm: eqx.nn.GroupNorm | eqx.nn.Identity
2019
+ act: Callable
2020
+ dr1: eqx.nn.Dropout
2021
+ dr2: eqx.nn.Dropout
2022
+
2023
+ def __init__(
2024
+ self,
2025
+ in_channels: int,
2026
+ hidden_channels: int,
2027
+ *,
2028
+ glu_norm: bool = True,
2029
+ glu_dwconv: bool = False,
2030
+ act_layer: Callable = jax.nn.gelu,
2031
+ dropout: float = 0.0,
2032
+ key: PRNGKeyArray,
2033
+ ):
2034
+ key_conv1, key_conv2, key_dwconv = jr.split(key, 3)
2035
+
2036
+ self.conv1 = eqx.nn.Conv2d(
2037
+ in_channels=in_channels,
2038
+ out_channels=hidden_channels * 2,
2039
+ kernel_size=1,
2040
+ stride=1,
2041
+ padding=0,
2042
+ use_bias=True,
2043
+ key=key_conv1,
2044
+ )
2045
+ self.conv2 = eqx.nn.Conv2d(
2046
+ in_channels=hidden_channels,
2047
+ out_channels=in_channels,
2048
+ kernel_size=1,
2049
+ stride=1,
2050
+ padding=0,
2051
+ use_bias=True,
2052
+ key=key_conv2,
2053
+ )
2054
+ self.dwconv = (
2055
+ eqx.nn.Conv2d(
2056
+ in_channels=hidden_channels,
2057
+ out_channels=hidden_channels,
2058
+ kernel_size=3,
2059
+ stride=1,
2060
+ padding=1,
2061
+ groups=hidden_channels,
2062
+ use_bias=True,
2063
+ key=key_dwconv,
2064
+ )
2065
+ if glu_dwconv
2066
+ else eqx.nn.Identity()
2067
+ )
2068
+
2069
+ num_groups = nearest_power_of_2_divisor(in_channels, 32)
2070
+ self.norm = (
2071
+ eqx.nn.GroupNorm(num_groups, hidden_channels)
2072
+ if glu_norm
2073
+ else eqx.nn.Identity()
2074
+ )
2075
+
2076
+ self.act = act_layer
2077
+ self.dr1 = eqx.nn.Dropout(dropout)
2078
+ self.dr2 = eqx.nn.Dropout(dropout)
2079
+
2080
+ def __call__(
2081
+ self,
2082
+ x: Float[Array, "channels height width"],
2083
+ key: PRNGKeyArray,
2084
+ inference: Optional[bool] = None,
2085
+ ) -> Float[Array, "channels height width"]:
2086
+ key_dr1, key_dr2 = jr.split(key, 2)
2087
+
2088
+ x, v = jnp.split(self.conv1(x), 2)
2089
+
2090
+ x = self.norm(self.act(self.dwconv(x)) * v)
2091
+ x = self.dr1(x, inference=inference, key=key_dr1)
2092
+ x = self.conv2(x)
2093
+ x = self.dr2(x, inference=inference, key=key_dr2)
2094
+
2095
+ return x
2096
+
2097
+
2098
+ class ATConv(eqx.Module):
2099
+ """
2100
+ Attentive Convolution (ATConv) layer that generates per-sample, per-channel 3×3 dynamic kernels
2101
+ and applies them as a depthwise grouped 2D convolution with linear-time cost, capturing adaptive
2102
+ routing and lateral inhibition principles identified as key advantages of self-attention over static
2103
+ convolutions[1].
2104
+
2105
+ The operator first projects features, synthesizes a per-channel HWIO kernel from pooled spatial
2106
+ summaries, subtracts a learnable fraction of its channel-wise mean to induce inhibition, then
2107
+ performs a depthwise convolution followed by a 1×1 projection, matching the reference design
2108
+ that unifies attention-like expressivity with convolutional efficiency[1].
2109
+
2110
+ Referencea:
2111
+ [1]. Yu, et al., Attentive Convolution: Unifying the Expressivity of Self-Attention with Convolutional
2112
+ Efficiency. 2025. arXiv:2510.20092
2113
+ """
2114
+
2115
+ kernel_size: int = eqx.field(static=True)
2116
+ padding: int = eqx.field(static=True)
2117
+
2118
+ x_proj: eqx.nn.Conv2d
2119
+ proj: eqx.nn.Conv2d
2120
+ kernel_proj: eqx.nn.Conv2d
2121
+ kernel_act: Callable
2122
+ kernel_gen: eqx.nn.Linear
2123
+ pool: eqx.nn.AdaptiveAvgPool1d
2124
+ difference_control: jax.Array
2125
+
2126
+ def __init__(
2127
+ self,
2128
+ in_channels: int,
2129
+ *,
2130
+ kernel_size: int = 3,
2131
+ act_layer: Callable = jax.nn.gelu,
2132
+ use_bias: bool = True,
2133
+ key: PRNGKeyArray,
2134
+ ):
2135
+ key_xproj, key_proj, key_kp, key_kg = jr.split(key, 4)
2136
+
2137
+ self.kernel_size = kernel_size
2138
+ k2 = kernel_size**2
2139
+ self.padding = kernel_size // 2
2140
+
2141
+ self.x_proj = eqx.nn.Conv2d(
2142
+ in_channels,
2143
+ in_channels,
2144
+ kernel_size=1,
2145
+ stride=1,
2146
+ padding=0,
2147
+ use_bias=use_bias,
2148
+ key=key_xproj,
2149
+ )
2150
+ self.proj = eqx.nn.Conv2d(
2151
+ in_channels,
2152
+ in_channels,
2153
+ kernel_size=1,
2154
+ stride=1,
2155
+ padding=0,
2156
+ use_bias=use_bias,
2157
+ key=key_proj,
2158
+ )
2159
+ self.kernel_proj = eqx.nn.Conv2d(
2160
+ in_channels=in_channels,
2161
+ out_channels=in_channels,
2162
+ kernel_size=1,
2163
+ stride=1,
2164
+ padding=0,
2165
+ use_bias=use_bias,
2166
+ key=key_kp,
2167
+ )
2168
+ self.kernel_act = act_layer
2169
+ self.kernel_gen = eqx.nn.Linear(k2, k2, use_bias=use_bias, key=key_kg)
2170
+ self.pool = eqx.nn.AdaptiveAvgPool1d(target_shape=k2)
2171
+ self.difference_control = jnp.zeros((1, 1, 1, in_channels))
2172
+
2173
+ def _generate_kernels(
2174
+ self, x: Float[Array, "channels height width"]
2175
+ ) -> Float[Array, "k k 1 channels"]:
2176
+ kernels = self.kernel_proj(x)
2177
+ kernels = self.pool(rearrange(kernels, "c h w -> c (h w)"))
2178
+ kernels = self.kernel_act(kernels)
2179
+ kernels = jax.vmap(self.kernel_gen)(kernels)
2180
+ kernels = rearrange(
2181
+ kernels, "c (k1 k2) -> k1 k2 1 c", k1=self.kernel_size, k2=self.kernel_size
2182
+ )
2183
+
2184
+ return kernels
2185
+
2186
+ def _apply_kernel_difference(
2187
+ self, kernels: Float[Array, "channels k k"]
2188
+ ) -> Float[Array, "channels k k"]:
2189
+ mean_kernels = kernels.mean(axis=(0, 1, 2), keepdims=True)
2190
+ factor = jax.nn.sigmoid(self.difference_control)
2191
+
2192
+ return kernels - factor * mean_kernels
2193
+
2194
+ def __call__(
2195
+ self,
2196
+ x: Float[Array, "channels height width"],
2197
+ key: PRNGKeyArray,
2198
+ inference: Optional[bool] = None,
2199
+ ) -> Float[Array, "channels height width"]:
2200
+ kernels = self._generate_kernels(x)
2201
+ kernels = self._apply_kernel_difference(kernels)
2202
+
2203
+ x = self.x_proj(x)
2204
+ C, H, W = x.shape
2205
+ x = jax.lax.conv_general_dilated(
2206
+ x[None, ...], # (N, C, H, W)
2207
+ kernels, # (H, W, I=1, O=C)
2208
+ window_strides=(1, 1),
2209
+ padding=((self.padding, self.padding), (self.padding, self.padding)),
2210
+ lhs_dilation=None,
2211
+ rhs_dilation=None,
2212
+ dimension_numbers=("NCHW", "HWIO", "NCHW"),
2213
+ feature_group_count=C, # depthwise
2214
+ )[0]
2215
+
2216
+ x = self.proj(x)
2217
+
2218
+ return x
2219
+
2220
+
2221
+ class ATConvBlock(eqx.Module):
2222
+ token_mixer: ATConv
2223
+ channel_mixer: GLUConv
2224
+ ls1: LayerScale | eqx.nn.Identity
2225
+ ls2: LayerScale | eqx.nn.Identity
2226
+ norm1: eqx.nn.LayerNorm
2227
+ norm2: eqx.nn.LayerNorm
2228
+ drop_path1: DropPathAdd
2229
+ drop_path2: DropPathAdd
2230
+
2231
+ def __init__(
2232
+ self,
2233
+ in_channels: int,
2234
+ *,
2235
+ kernel_size: int = 3,
2236
+ exp_rate: float = 4.0,
2237
+ act_layer: Callable = jax.nn.gelu,
2238
+ glu_norm: bool = True,
2239
+ glu_dwconv: bool = False,
2240
+ use_bias: bool = True,
2241
+ dropout: float = 0.0,
2242
+ drop_path: float | list[float] = 0.0,
2243
+ use_layer_scale: bool = True,
2244
+ key: PRNGKeyArray,
2245
+ ):
2246
+ key_tm, key_cm = jr.split(key, 2)
2247
+
2248
+ hidden_features = int(in_channels * exp_rate)
2249
+ # NOTE: slightly different from the original paper
2250
+ glu_hidden_features = 32 * round(int(2 * hidden_features / 3) / 32)
2251
+
2252
+ self.token_mixer = ATConv(
2253
+ in_channels,
2254
+ kernel_size=kernel_size,
2255
+ act_layer=act_layer,
2256
+ use_bias=use_bias,
2257
+ key=key_tm,
2258
+ )
2259
+ self.channel_mixer = GLUConv(
2260
+ in_channels,
2261
+ hidden_channels=glu_hidden_features,
2262
+ glu_norm=glu_norm,
2263
+ glu_dwconv=glu_dwconv,
2264
+ act_layer=act_layer,
2265
+ dropout=dropout,
2266
+ key=key_cm,
2267
+ )
2268
+
2269
+ self.ls1 = LayerScale(in_channels) if use_layer_scale else eqx.nn.Identity()
2270
+ self.ls2 = LayerScale(in_channels) if use_layer_scale else eqx.nn.Identity()
2271
+
2272
+ self.norm1 = eqx.nn.LayerNorm(in_channels)
2273
+ self.norm2 = eqx.nn.LayerNorm(in_channels)
2274
+
2275
+ if isinstance(drop_path, list):
2276
+ if len(drop_path) != 2:
2277
+ raise AssertionError(
2278
+ f"`drop_path` needs to have 2 elements, got {len(drop_path)} ({drop_path})."
2279
+ )
2280
+ dr1, dr2 = drop_path
2281
+ dr1 = float(dr1)
2282
+ dr2 = float(dr2)
2283
+ else:
2284
+ dr1 = dr2 = float(drop_path)
2285
+
2286
+ self.drop_path1 = DropPathAdd(dr1)
2287
+ self.drop_path2 = DropPathAdd(dr2)
2288
+
2289
+ def __call__(
2290
+ self,
2291
+ x: Float[Array, "channels height width"],
2292
+ key: PRNGKeyArray,
2293
+ inference: Optional[bool] = None,
2294
+ ) -> Float[Array, "channels height width"]:
2295
+ key_tm, key_cm, key_dr1, key_dr2 = jr.split(key, 4)
2296
+
2297
+ norm1 = jax.vmap(
2298
+ jax.vmap(self.norm1, in_axes=1, out_axes=1), in_axes=1, out_axes=1
2299
+ )
2300
+ norm2 = jax.vmap(
2301
+ jax.vmap(self.norm2, in_axes=1, out_axes=1), in_axes=1, out_axes=1
2302
+ )
2303
+
2304
+ x = self.drop_path1(
2305
+ x,
2306
+ self.ls1(self.token_mixer(norm1(x), inference=inference, key=key_tm)),
2307
+ inference=inference,
2308
+ key=key_dr1,
2309
+ )
2310
+ x = self.drop_path2(
2311
+ x,
2312
+ self.ls2(self.channel_mixer(norm2(x), inference=inference, key=key_cm)),
2313
+ inference=inference,
2314
+ key=key_dr2,
2315
+ )
2316
+
2317
+ return x
@@ -0,0 +1,209 @@
1
+ from typing import Callable, Literal, Optional
2
+
3
+ import equinox as eqx
4
+ import jax
5
+ import jax.random as jr
6
+ from jaxtyping import Array, Float, PRNGKeyArray
7
+
8
+ from equimo.layers.convolution import ConvBlock, SingleConvBlock
9
+ from equimo.layers.generic import Residual
10
+ from equimo.layers.patch import SEPatchMerging
11
+ from equimo.utils import nearest_power_of_2_divisor
12
+
13
+
14
+ class ConvNormDownsampler(eqx.Module):
15
+ """A module that performs spatial downsampling using strided convolution.
16
+
17
+ This module reduces spatial dimensions (height and width) by a factor of 2
18
+ while optionally increasing the channel dimension. Uses a 3x3 strided
19
+ convolution for downsampling.
20
+
21
+ Attributes:
22
+ reduction: Convolutional layer that performs the downsampling
23
+ """
24
+
25
+ downsampler: eqx.nn.Sequential
26
+
27
+ def __init__(
28
+ self,
29
+ in_channels: int,
30
+ *,
31
+ out_channels: int | None = None,
32
+ act_layer: Callable = jax.nn.gelu,
33
+ use_bias: bool = False,
34
+ use_norm: bool = True,
35
+ mode: Literal["double", "simple"] = "simple",
36
+ key: PRNGKeyArray,
37
+ ):
38
+ out_channels = out_channels if out_channels else 2 * in_channels
39
+
40
+ match mode:
41
+ case "simple":
42
+ self.downsampler = eqx.nn.Sequential(
43
+ [
44
+ eqx.nn.Conv2d(
45
+ in_channels=in_channels,
46
+ out_channels=out_channels,
47
+ kernel_size=3,
48
+ stride=2,
49
+ padding=1,
50
+ use_bias=False,
51
+ key=key,
52
+ ),
53
+ eqx.nn.GroupNorm(
54
+ nearest_power_of_2_divisor(out_channels, 32), out_channels
55
+ )
56
+ if use_norm
57
+ else eqx.nn.Identity(),
58
+ ]
59
+ )
60
+ case "double":
61
+ self.downsampler = eqx.nn.Sequential(
62
+ [
63
+ eqx.nn.Conv2d(
64
+ in_channels=in_channels,
65
+ out_channels=(_d := out_channels // 2),
66
+ kernel_size=3,
67
+ stride=2,
68
+ padding=1,
69
+ use_bias=use_bias,
70
+ key=jr.fold_in(key, 0),
71
+ ),
72
+ eqx.nn.GroupNorm(nearest_power_of_2_divisor(_d, 32), _d)
73
+ if use_norm
74
+ else eqx.nn.Identity(),
75
+ eqx.nn.Lambda(act_layer),
76
+ eqx.nn.Conv2d(
77
+ in_channels=_d,
78
+ out_channels=out_channels,
79
+ kernel_size=3,
80
+ stride=2,
81
+ padding=1,
82
+ use_bias=use_bias,
83
+ key=jr.fold_in(key, 1),
84
+ ),
85
+ eqx.nn.GroupNorm(
86
+ nearest_power_of_2_divisor(out_channels, 32), out_channels
87
+ )
88
+ if use_norm
89
+ else eqx.nn.Identity(),
90
+ ]
91
+ )
92
+
93
+ def __call__(self, x, *args, **kwargs):
94
+ return self.downsampler(x)
95
+
96
+
97
+ class PWSEDownsampler(eqx.Module):
98
+ """Downsampling module for spatial feature reduction.
99
+
100
+ Combines convolution blocks and patch merging to reduce spatial dimensions
101
+ while increasing feature channels. Uses residual connections and alternates
102
+ between depthwise and pointwise convolutions.
103
+
104
+ Attributes:
105
+ conv1: First depthwise convolution with residual
106
+ conv2: First pointwise convolution block
107
+ conv3: Second depthwise convolution with residual
108
+ conv4: Second pointwise convolution block
109
+ patch_merging: Squeeze-and-excitation patch merging layer
110
+ """
111
+
112
+ conv1: eqx.Module
113
+ conv2: eqx.Module
114
+ conv3: eqx.Module
115
+ conv4: eqx.Module
116
+ patch_merging: eqx.Module
117
+
118
+ def __init__(
119
+ self,
120
+ in_channels: int,
121
+ out_channels: int,
122
+ *,
123
+ key: PRNGKeyArray,
124
+ drop_path: float = 0.0,
125
+ **kwargs,
126
+ ):
127
+ key_conv1, key_conv2, key_conv3, key_conv4, key_pm = jr.split(key, 5)
128
+ self.conv1 = Residual(
129
+ SingleConvBlock(
130
+ in_channels,
131
+ in_channels,
132
+ act_layer=None,
133
+ kernel_size=3,
134
+ stride=1,
135
+ padding=1,
136
+ groups=in_channels,
137
+ key=key_conv1,
138
+ ),
139
+ drop_path=drop_path,
140
+ )
141
+ self.conv2 = ConvBlock(
142
+ in_channels,
143
+ hidden_in_channels=in_channels * 2,
144
+ act_layer=None,
145
+ drop_path=drop_path,
146
+ kernel_size=1,
147
+ stride=1,
148
+ padding=0,
149
+ key=key_conv2,
150
+ )
151
+ self.patch_merging = SEPatchMerging(
152
+ in_channels=in_channels,
153
+ out_channels=out_channels,
154
+ key=key_pm,
155
+ )
156
+ self.conv3 = Residual(
157
+ SingleConvBlock(
158
+ out_channels,
159
+ out_channels,
160
+ act_layer=None,
161
+ kernel_size=3,
162
+ stride=1,
163
+ padding=1,
164
+ groups=out_channels,
165
+ key=key_conv3,
166
+ ),
167
+ drop_path=drop_path,
168
+ )
169
+ self.conv4 = ConvBlock(
170
+ out_channels,
171
+ hidden_in_channels=out_channels * 2,
172
+ act_layer=None,
173
+ drop_path=drop_path,
174
+ kernel_size=1,
175
+ stride=1,
176
+ padding=0,
177
+ key=key_conv4,
178
+ )
179
+
180
+ def __call__(
181
+ self,
182
+ x: Float[Array, "seqlen dim"],
183
+ key: PRNGKeyArray,
184
+ inference: Optional[bool] = None,
185
+ ) -> Float[Array, "seqlen dim"]:
186
+ """Apply downsampling to input features.
187
+
188
+ Args:
189
+ x: Input feature tensor
190
+ inference: Whether to enable dropout during inference
191
+ key: PRNG key for random operations
192
+
193
+ Returns:
194
+ Downsampled feature tensor with increased channels
195
+ """
196
+ key_conv1, key_conv2, key_conv3, key_conv4 = jr.split(key, 4)
197
+ x = self.conv2(
198
+ self.conv1(x, inference=inference, key=key_conv1),
199
+ inference=inference,
200
+ key=key_conv2,
201
+ )
202
+ x = self.patch_merging(x)
203
+ x = self.conv4(
204
+ self.conv3(x, inference=inference, key=key_conv3),
205
+ inference=inference,
206
+ key=key_conv4,
207
+ )
208
+
209
+ return x