Equimo 0.4.3__tar.gz → 0.4.5__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 (44) hide show
  1. {equimo-0.4.3/src/Equimo.egg-info → equimo-0.4.5}/PKG-INFO +1 -1
  2. {equimo-0.4.3 → equimo-0.4.5}/pyproject.toml +1 -1
  3. {equimo-0.4.3 → equimo-0.4.5/src/Equimo.egg-info}/PKG-INFO +1 -1
  4. equimo-0.4.5/src/equimo/__init__.py +1 -0
  5. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/convolution.py +748 -159
  6. equimo-0.4.3/src/equimo/__init__.py +0 -1
  7. {equimo-0.4.3 → equimo-0.4.5}/LICENSE.md +0 -0
  8. {equimo-0.4.3 → equimo-0.4.5}/README.md +0 -0
  9. {equimo-0.4.3 → equimo-0.4.5}/setup.cfg +0 -0
  10. {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/SOURCES.txt +0 -0
  11. {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/dependency_links.txt +0 -0
  12. {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/requires.txt +0 -0
  13. {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/top_level.txt +0 -0
  14. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/conversion/__init__.py +0 -0
  15. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/conversion/utils.py +0 -0
  16. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/experimental/__init__.py +0 -0
  17. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/experimental/text.py +0 -0
  18. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/io.py +0 -0
  19. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/__init__.py +0 -0
  20. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/activation.py +0 -0
  21. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/attention.py +0 -0
  22. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/downsample.py +0 -0
  23. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/dropout.py +0 -0
  24. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/ffn.py +0 -0
  25. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/generic.py +0 -0
  26. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/mamba.py +0 -0
  27. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/norm.py +0 -0
  28. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/patch.py +0 -0
  29. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/posemb.py +0 -0
  30. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/sharing.py +0 -0
  31. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/squeeze_excite.py +0 -0
  32. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/__init__.py +0 -0
  33. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/emamodel.py +0 -0
  34. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/fastervit.py +0 -0
  35. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/lowformer.py +0 -0
  36. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/mlla.py +0 -0
  37. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/partialformer.py +0 -0
  38. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/reduceformer.py +0 -0
  39. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/shvit.py +0 -0
  40. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/vit.py +0 -0
  41. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/vssd.py +0 -0
  42. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/ops/scan.py +0 -0
  43. {equimo-0.4.3 → equimo-0.4.5}/src/equimo/utils.py +0 -0
  44. {equimo-0.4.3 → equimo-0.4.5}/tests/test_models.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Equimo
3
- Version: 0.4.3
3
+ Version: 0.4.5
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.3"
3
+ version = "0.4.5"
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.3
3
+ Version: 0.4.5
4
4
  Summary: Implementation of popular vision models in Jax
5
5
  Classifier: Development Status :: 4 - Beta
6
6
  Classifier: Intended Audience :: Developers
@@ -0,0 +1 @@
1
+ __version__ = "0.4.5"
@@ -1,6 +1,6 @@
1
1
  import math
2
2
  import operator
3
- from typing import Callable, Literal, Optional, Sequence, Tuple
3
+ from typing import Callable, Literal, Optional, Sequence, Tuple, Union
4
4
 
5
5
  import equinox as eqx
6
6
  import jax
@@ -11,6 +11,7 @@ from jaxtyping import Array, Float, PRNGKeyArray
11
11
 
12
12
  from equimo.layers.dropout import DropPathAdd
13
13
  from equimo.layers.norm import LayerScale
14
+ from equimo.layers.squeeze_excite import SEModule
14
15
  from equimo.utils import make_divisible, nearest_power_of_2_divisor
15
16
 
16
17
 
@@ -1046,31 +1047,50 @@ class UIB(eqx.Module):
1046
1047
 
1047
1048
 
1048
1049
  class GenericGhostModule(eqx.Module):
1049
- """Modded module for the GhostNet v3 model.
1050
+ """GhostNet v3-like module with GroupNorm and training-time branch fusion.
1050
1051
 
1052
+ - Uses shared GroupNorm after linear summations (keeps GN's batch-size robustness).
1053
+ - Preserves the linear pre-norm structure of the reference:
1054
+ primary: [skip (identity) + scale(1x1) + Σ conv_k] → GN → act
1055
+ cheap: [skip (identity, when ratio=2) + scale(1x1 depthwise) + Σ depthwise conv_k] → GN → act
1056
+ - Provides test-time fusion into single Conv2d layers (still followed by GN+act).
1051
1057
 
1052
- It differs from the original implementation because it shares the same norm
1053
- ayers accross multiple parallel branches to allow using other norm layers
1054
- while still fusing convolutions at test-time.
1058
+ Input/Output convention: (C, H, W).
1055
1059
  """
1056
1060
 
1061
+ # Static configuration
1057
1062
  mode: Literal["original", "shortcut"] = eqx.field(static=True)
1058
1063
  out_channels: int = eqx.field(static=True)
1059
1064
  num_conv_branches: int = eqx.field(static=True)
1065
+ kernel_size: int = eqx.field(static=True)
1066
+ dw_size: int = eqx.field(static=True)
1067
+ stride: int = eqx.field(static=True)
1068
+ primary_has_skip: bool = eqx.field(static=True)
1069
+ primary_has_scale: bool = eqx.field(static=True)
1070
+ cheap_has_skip: bool = eqx.field(static=True)
1071
+ cheap_has_scale: bool = eqx.field(static=True)
1072
+
1073
+ # Runtime flags
1060
1074
  inference: bool
1061
1075
 
1076
+ # Inference
1062
1077
  primary_conv: eqx.nn.Conv2d
1063
1078
  cheap_operation: eqx.nn.Conv2d
1064
1079
 
1080
+ # Training
1065
1081
  primary_rpr_conv: list[eqx.nn.Conv2d]
1082
+ primary_rpr_scale: eqx.nn.Conv2d | eqx.nn.Identity
1066
1083
  primary_shared_norm: eqx.nn.GroupNorm
1067
1084
  primary_activation: Callable
1068
- cheap_rpr_scale: eqx.nn.Conv2d | eqx.nn.Identity
1069
- cheap_rpr_skip: eqx.nn.Conv2d | eqx.nn.Identity
1085
+
1070
1086
  cheap_rpr_conv: list[eqx.nn.Conv2d]
1087
+ cheap_rpr_scale: eqx.nn.Conv2d | eqx.nn.Identity
1071
1088
  cheap_shared_norm: eqx.nn.GroupNorm
1072
1089
  cheap_activation: Callable
1090
+
1073
1091
  short_conv: eqx.nn.Identity | eqx.nn.Sequential
1092
+ pool2: eqx.nn.AvgPool2d
1093
+ gate_scale: float = eqx.field(static=True)
1074
1094
 
1075
1095
  def __init__(
1076
1096
  self,
@@ -1086,8 +1106,9 @@ class GenericGhostModule(eqx.Module):
1086
1106
  mode: Literal["original", "shortcut"] = "original",
1087
1107
  key: PRNGKeyArray,
1088
1108
  ):
1089
- key_primary, key_cheap, key_crs, key_skip, key_s1, key_s2, key_s3 = jr.split(
1090
- key, 7
1109
+ # Key management
1110
+ key_primary, key_cheap, key_pscale, key_cscale, key_s1, key_s2, key_s3 = (
1111
+ jr.split(key, 7)
1091
1112
  )
1092
1113
  key_ps = jr.split(key_primary, num_conv_branches)
1093
1114
  key_cs = jr.split(key_cheap, num_conv_branches)
@@ -1095,10 +1116,22 @@ class GenericGhostModule(eqx.Module):
1095
1116
  init_channels = math.ceil(out_channels / ratio)
1096
1117
  new_channels = init_channels * (ratio - 1)
1097
1118
 
1119
+ primary_has_skip = (in_channels == init_channels) and (stride == 1)
1120
+ primary_has_scale = kernel_size > 1
1121
+ cheap_has_skip = init_channels == new_channels
1122
+ cheap_has_scale = dw_size > 1
1123
+
1098
1124
  self.inference = False
1099
1125
  self.mode = mode
1100
1126
  self.num_conv_branches = num_conv_branches
1101
1127
  self.out_channels = out_channels
1128
+ self.kernel_size = kernel_size # MOD
1129
+ self.dw_size = dw_size # MOD
1130
+ self.stride = stride # MOD
1131
+ self.primary_has_skip = primary_has_skip # MOD
1132
+ self.primary_has_scale = primary_has_scale # MOD
1133
+ self.cheap_has_skip = cheap_has_skip # MOD
1134
+ self.cheap_has_scale = cheap_has_scale # MOD
1102
1135
 
1103
1136
  # Those are actually placeholders, updated at each epoch, only used at inference time
1104
1137
  self.primary_conv = eqx.nn.Conv2d(
@@ -1107,20 +1140,22 @@ class GenericGhostModule(eqx.Module):
1107
1140
  kernel_size=kernel_size,
1108
1141
  stride=stride,
1109
1142
  padding=kernel_size // 2,
1143
+ use_bias=False,
1110
1144
  key=key_primary,
1111
1145
  )
1112
1146
  self.cheap_operation = eqx.nn.Conv2d(
1113
1147
  in_channels=init_channels,
1114
1148
  out_channels=new_channels,
1115
1149
  kernel_size=dw_size,
1116
- stride=stride,
1150
+ stride=1, # MOD
1117
1151
  padding=dw_size // 2,
1118
- groups=1,
1152
+ groups=init_channels,
1153
+ use_bias=False,
1119
1154
  key=key_cheap,
1120
1155
  )
1121
1156
 
1157
+ # Primary training branches
1122
1158
  init_num_groups = nearest_power_of_2_divisor(init_channels, 32)
1123
- # TODO: test with some dropout?
1124
1159
  self.primary_rpr_conv = [
1125
1160
  eqx.nn.Conv2d(
1126
1161
  in_channels=in_channels,
@@ -1128,44 +1163,56 @@ class GenericGhostModule(eqx.Module):
1128
1163
  kernel_size=kernel_size,
1129
1164
  stride=stride,
1130
1165
  padding=kernel_size // 2,
1166
+ use_bias=False,
1131
1167
  key=key_ps[i],
1132
1168
  )
1133
1169
  for i in range(num_conv_branches)
1134
1170
  ]
1171
+ self.primary_rpr_scale = (
1172
+ eqx.nn.Conv2d(
1173
+ in_channels=in_channels,
1174
+ out_channels=init_channels,
1175
+ kernel_size=1,
1176
+ stride=1,
1177
+ padding=0,
1178
+ use_bias=False,
1179
+ key=key_pscale,
1180
+ )
1181
+ if primary_has_scale
1182
+ else eqx.nn.Identity()
1183
+ )
1135
1184
  self.primary_shared_norm = eqx.nn.GroupNorm(init_num_groups, init_channels)
1136
1185
  self.primary_activation = act_layer
1137
1186
 
1138
- self.cheap_rpr_scale = eqx.nn.Conv2d(
1139
- in_channels=init_channels,
1140
- out_channels=new_channels,
1141
- kernel_size=1,
1142
- stride=1,
1143
- padding=0,
1144
- groups=1,
1145
- key=key_crs,
1146
- )
1147
- self.cheap_rpr_skip = eqx.nn.Conv2d(
1148
- init_channels,
1149
- new_channels,
1150
- kernel_size=1,
1151
- stride=stride,
1152
- padding=0,
1153
- groups=1,
1154
- key=key_skip,
1155
- )
1187
+ # Cheap training branches (depthwise)
1188
+ newchannels_num_groups = nearest_power_of_2_divisor(new_channels, 32)
1156
1189
  self.cheap_rpr_conv = [
1157
1190
  eqx.nn.Conv2d(
1158
1191
  in_channels=init_channels,
1159
1192
  out_channels=new_channels,
1160
1193
  kernel_size=dw_size,
1161
- stride=stride,
1194
+ stride=1,
1162
1195
  padding=dw_size // 2,
1163
- groups=1,
1196
+ groups=init_channels,
1197
+ use_bias=False,
1164
1198
  key=key_cs[i],
1165
1199
  )
1166
1200
  for i in range(self.num_conv_branches)
1167
1201
  ]
1168
- newchannels_num_groups = nearest_power_of_2_divisor(new_channels, 32)
1202
+ self.cheap_rpr_scale = (
1203
+ eqx.nn.Conv2d(
1204
+ in_channels=init_channels,
1205
+ out_channels=new_channels,
1206
+ kernel_size=1,
1207
+ stride=1,
1208
+ padding=0,
1209
+ groups=init_channels,
1210
+ use_bias=False,
1211
+ key=key_cscale,
1212
+ )
1213
+ if cheap_has_scale
1214
+ else eqx.nn.Identity()
1215
+ )
1169
1216
  self.cheap_shared_norm = eqx.nn.GroupNorm(newchannels_num_groups, new_channels)
1170
1217
  self.cheap_activation = act_layer
1171
1218
 
@@ -1211,29 +1258,39 @@ class GenericGhostModule(eqx.Module):
1211
1258
  else eqx.nn.Identity()
1212
1259
  )
1213
1260
 
1214
- def training_features(self, x):
1215
- x1 = jax.tree_util.tree_reduce(
1216
- operator.add, [conv(x) for conv in self.primary_rpr_conv]
1261
+ self.pool2 = eqx.nn.AvgPool2d(
1262
+ kernel_size=2, stride=2, padding=0, use_ceil=False
1217
1263
  )
1218
- x1 = self.primary_activation(self.primary_shared_norm(x1))
1264
+ self.gate_scale = 1.0
1219
1265
 
1220
- cheap_branches = [self.cheap_rpr_skip(x1), self.cheap_rpr_scale(x1)] + [
1221
- conv(x1) for conv in self.cheap_rpr_conv
1222
- ]
1223
-
1224
- x2 = jax.tree_util.tree_reduce(operator.add, cheap_branches)
1225
- x2 = self.cheap_activation(self.cheap_shared_norm(x2))
1266
+ def training_features(self, x):
1267
+ # Primary path pre-norm linear sum
1268
+ terms = []
1269
+ if self.primary_has_skip:
1270
+ terms.append(x) # identity
1271
+ if not isinstance(self.primary_rpr_scale, eqx.nn.Identity):
1272
+ terms.append(self.primary_rpr_scale(x))
1273
+ terms.extend(conv(x) for conv in self.primary_rpr_conv)
1274
+ x1_sum = jax.tree_util.tree_reduce(operator.add, terms)
1275
+ x1 = self.primary_activation(self.primary_shared_norm(x1_sum))
1276
+
1277
+ # Cheap path pre-norm linear sum
1278
+ cheap_terms = []
1279
+ if self.cheap_has_skip:
1280
+ cheap_terms.append(x1) # identity
1281
+ if not isinstance(self.cheap_rpr_scale, eqx.nn.Identity):
1282
+ cheap_terms.append(self.cheap_rpr_scale(x1))
1283
+ cheap_terms.extend(conv(x1) for conv in self.cheap_rpr_conv)
1284
+ x2_sum = jax.tree_util.tree_reduce(operator.add, cheap_terms)
1285
+ x2 = self.cheap_activation(self.cheap_shared_norm(x2_sum))
1226
1286
 
1227
1287
  out = jnp.concatenate([x1, x2], axis=0)
1228
-
1229
1288
  return out
1230
1289
 
1231
1290
  def inference_features(self, x):
1232
1291
  x1 = self.primary_activation(self.primary_shared_norm(self.primary_conv(x)))
1233
1292
  x2 = self.cheap_activation(self.cheap_shared_norm(self.cheap_operation(x1)))
1234
-
1235
1293
  out = jnp.concatenate([x1, x2], axis=0)
1236
-
1237
1294
  return out
1238
1295
 
1239
1296
  def __call__(
@@ -1242,24 +1299,14 @@ class GenericGhostModule(eqx.Module):
1242
1299
  key: PRNGKeyArray,
1243
1300
  inference: Optional[bool] = None,
1244
1301
  ):
1245
- # key_p, key_c = jr.split(key, 2)
1246
- # key_ps = jr.split(key_p, self.num_conv_branches)
1247
- # key_cs = jr.split(key_c, self.num_conv_branches)
1248
-
1249
- if self.inference:
1250
- out = self.inference_features(x)
1251
- else:
1252
- out = self.training_features(x)
1302
+ use_inference = self.inference if inference is None else inference
1303
+ out = self.inference_features(x) if use_inference else self.training_features(x)
1253
1304
 
1254
1305
  if self.mode == "shortcut":
1255
- res = self.short_conv(reduce(x, "c (h 2) (w 2) -> c h w", reduction="mean"))
1306
+ res = self.short_conv(self.pool2(x))
1256
1307
  gating_signal = jax.image.resize(
1257
- image=jax.nn.sigmoid(res),
1258
- shape=(
1259
- res.shape[0],
1260
- out.shape[1],
1261
- out.shape[2],
1262
- ), # (channels, height, width)
1308
+ image=jax.nn.sigmoid(res / self.gate_scale),
1309
+ shape=(res.shape[0], out.shape[1], out.shape[2]),
1263
1310
  method="nearest",
1264
1311
  )
1265
1312
  out = out[: self.out_channels, :, :] * gating_signal
@@ -1267,139 +1314,681 @@ class GenericGhostModule(eqx.Module):
1267
1314
  return out
1268
1315
 
1269
1316
 
1270
- def update_fused_ghostmodule(
1271
- module: GenericGhostModule,
1272
- ) -> GenericGhostModule:
1273
- """
1274
- Fuses the weights of parallel training branches into single inference convolutions.
1317
+ class GhostBottleneck(eqx.Module):
1318
+ """Ghost bottleneck with optional SE and re-parameterizable depthwise stage."""
1275
1319
 
1276
- This function should be called after training and before switching the model
1277
- to inference mode. It operates on a single GenericGhostModule.
1320
+ # Static config
1321
+ stride: int = eqx.field(static=True)
1322
+ dw_kernel_size: int = eqx.field(static=True)
1323
+ use_shortcut_mode_in_ghost1: bool = eqx.field(static=True)
1324
+ allow_identity_residual: bool = eqx.field(static=True)
1278
1325
 
1279
- Args:
1280
- module: An instance of GenericGhostModule with trained weights.
1326
+ inference: bool
1281
1327
 
1282
- Returns:
1283
- A new GenericGhostModule with the `primary_conv` and `cheap_operation`
1284
- weights updated for inference.
1285
- """
1286
- # Aggregate weights
1287
- primary_weights = jax.tree_util.tree_reduce(
1288
- operator.add, [conv.weight for conv in module.primary_rpr_conv]
1289
- )
1328
+ ghost1: "GenericGhostModule"
1329
+ ghost2: "GenericGhostModule"
1330
+
1331
+ dw_conv: eqx.nn.Conv2d | eqx.nn.Identity
1332
+ dw_rpr_conv: list[eqx.nn.Conv2d] # depthwise conv branches (no bias)
1333
+ dw_rpr_scale: eqx.nn.Conv2d | eqx.nn.Identity # optional 1x1 depthwise (no bias)
1334
+ dw_shared_norm: eqx.nn.GroupNorm | eqx.nn.Identity
1335
+
1336
+ se: eqx.Module | eqx.nn.Identity
1337
+
1338
+ shortcut: eqx.nn.Sequential | eqx.nn.Identity
1290
1339
 
1291
- # Aggregate biases (if they exist)
1292
- primary_biases = None
1293
- if module.primary_rpr_conv[0].bias is not None:
1294
- primary_biases = jax.tree_util.tree_reduce(
1295
- operator.add, [conv.bias for conv in module.primary_rpr_conv]
1340
+ def __init__(
1341
+ self,
1342
+ in_channels: int,
1343
+ mid_channels: int,
1344
+ out_channels: int,
1345
+ *,
1346
+ dw_kernel_size: int = 3,
1347
+ stride: int = 1,
1348
+ act_layer: Callable = jax.nn.relu,
1349
+ se_ratio: float = 0.0,
1350
+ layer_id: Optional[int] = None,
1351
+ use_shortcut_mode_in_ghost1: bool = True,
1352
+ allow_identity_residual: bool = True,
1353
+ key: PRNGKeyArray,
1354
+ ):
1355
+ self.stride = stride
1356
+ self.dw_kernel_size = dw_kernel_size
1357
+ self.inference = False
1358
+ self.allow_identity_residual = allow_identity_residual
1359
+
1360
+ self.use_shortcut_mode_in_ghost1 = use_shortcut_mode_in_ghost1
1361
+
1362
+ k_g1, k_g2, k_dw_main, k_dw_scale, k_sc1, k_sc2 = jr.split(key, 6)
1363
+ k_dw_list = jr.split(k_dw_main, 3)
1364
+
1365
+ # ghost1 (expansion)
1366
+ self.ghost1 = GenericGhostModule(
1367
+ in_channels=in_channels,
1368
+ out_channels=mid_channels,
1369
+ kernel_size=1,
1370
+ ratio=2,
1371
+ dw_size=dw_kernel_size,
1372
+ stride=1 if self.use_shortcut_mode_in_ghost1 else 1,
1373
+ act_layer=act_layer,
1374
+ num_conv_branches=3,
1375
+ mode="shortcut" if self.use_shortcut_mode_in_ghost1 else "original",
1376
+ key=k_g1,
1296
1377
  )
1297
1378
 
1298
- # Create the new fused primary convolution layer
1299
- fused_primary_conv = eqx.tree_at(
1300
- lambda conv: (conv.weight, conv.bias),
1301
- module.primary_conv,
1302
- (primary_weights, primary_biases),
1303
- )
1379
+ # Depthwise stage (only if stride > 1)
1380
+ if stride > 1:
1381
+ # Training-time branches (depthwise, no bias); no activation; shared GN after sum
1382
+ self.dw_rpr_conv = [
1383
+ eqx.nn.Conv2d(
1384
+ in_channels=mid_channels,
1385
+ out_channels=mid_channels,
1386
+ kernel_size=dw_kernel_size,
1387
+ stride=stride,
1388
+ padding=(dw_kernel_size - 1) // 2,
1389
+ groups=mid_channels,
1390
+ use_bias=False,
1391
+ key=k_dw_list[i],
1392
+ )
1393
+ for i in range(3)
1394
+ ]
1395
+ # Optional scale branch (1x1, depthwise, stride=stride)
1396
+ self.dw_rpr_scale = (
1397
+ eqx.nn.Conv2d(
1398
+ in_channels=mid_channels,
1399
+ out_channels=mid_channels,
1400
+ kernel_size=1,
1401
+ stride=stride,
1402
+ padding=0,
1403
+ groups=mid_channels,
1404
+ use_bias=False,
1405
+ key=k_dw_scale,
1406
+ )
1407
+ if dw_kernel_size > 1
1408
+ else eqx.nn.Identity()
1409
+ )
1410
+ # Shared GroupNorm
1411
+ num_groups_dw = nearest_power_of_2_divisor(mid_channels, 32)
1412
+ self.dw_shared_norm = eqx.nn.GroupNorm(num_groups_dw, mid_channels)
1304
1413
 
1305
- # The cheap operation fuses the skip, scale, and depthwise-like conv branches.
1306
- target_kernel_size = module.cheap_rpr_conv[0].weight.shape[-2:]
1414
+ # Inference fused depthwise conv (weights filled by update)
1415
+ self.dw_conv = eqx.nn.Conv2d(
1416
+ in_channels=mid_channels,
1417
+ out_channels=mid_channels,
1418
+ kernel_size=dw_kernel_size,
1419
+ stride=stride,
1420
+ padding=(dw_kernel_size - 1) // 2,
1421
+ groups=mid_channels,
1422
+ use_bias=False,
1423
+ key=k_dw_main,
1424
+ )
1425
+ else:
1426
+ # No depthwise stage when stride == 1
1427
+ self.dw_rpr_conv = []
1428
+ self.dw_rpr_scale = eqx.nn.Identity()
1429
+ self.dw_shared_norm = eqx.nn.Identity()
1430
+ self.dw_conv = eqx.nn.Identity()
1431
+
1432
+ # SE
1433
+ if se_ratio is not None and se_ratio > 0.0:
1434
+ # Use provided SEModule with rd_ratio=se_ratio
1435
+ self.se = SEModule(dim=mid_channels, rd_ratio=se_ratio, key=k_sc1)
1436
+ else:
1437
+ self.se = eqx.nn.Identity()
1307
1438
 
1308
- def pad_kernel_to_target(kernel: Array, target_size: tuple[int, int]) -> Array:
1309
- current_size = kernel.shape[-2:]
1310
- if current_size == target_size:
1311
- return kernel
1439
+ # ghost2 (projection)
1440
+ self.ghost2 = GenericGhostModule(
1441
+ in_channels=mid_channels,
1442
+ out_channels=out_channels,
1443
+ kernel_size=1,
1444
+ ratio=2,
1445
+ dw_size=dw_kernel_size,
1446
+ stride=1,
1447
+ act_layer=act_layer,
1448
+ num_conv_branches=3,
1449
+ mode="original",
1450
+ key=k_g2,
1451
+ )
1452
+
1453
+ # Shortcut
1454
+ if (in_channels == out_channels) and (stride == 1):
1455
+ self.shortcut = eqx.nn.Identity()
1456
+ else:
1457
+ num_groups_sc_in = nearest_power_of_2_divisor(in_channels, 32)
1458
+ num_groups_sc_out = nearest_power_of_2_divisor(out_channels, 32)
1459
+ self.shortcut = eqx.nn.Sequential(
1460
+ [
1461
+ eqx.nn.Conv2d(
1462
+ in_channels=in_channels,
1463
+ out_channels=in_channels,
1464
+ kernel_size=dw_kernel_size,
1465
+ stride=stride,
1466
+ padding=(dw_kernel_size - 1) // 2,
1467
+ groups=in_channels,
1468
+ use_bias=False,
1469
+ key=k_sc1,
1470
+ ),
1471
+ eqx.nn.GroupNorm(num_groups_sc_in, in_channels),
1472
+ eqx.nn.Conv2d(
1473
+ in_channels=in_channels,
1474
+ out_channels=out_channels,
1475
+ kernel_size=1,
1476
+ stride=1,
1477
+ padding=0,
1478
+ use_bias=False,
1479
+ key=k_sc2,
1480
+ ),
1481
+ eqx.nn.GroupNorm(num_groups_sc_out, out_channels),
1482
+ ]
1483
+ )
1312
1484
 
1313
- pad_h = (target_size[0] - current_size[0]) // 2
1314
- pad_w = (target_size[1] - current_size[1]) // 2
1485
+ def __call__(
1486
+ self,
1487
+ x: Float[Array, "channels height width"],
1488
+ *,
1489
+ key: PRNGKeyArray,
1490
+ inference: Optional[bool] = None,
1491
+ ) -> Float[Array, "channels height width"]:
1492
+ use_inference = self.inference if inference is None else inference
1493
+ k_g1, k_g2 = jr.split(key, 2)
1315
1494
 
1316
- # JAX padding format: ((before, after), (before, after), ...)
1317
- padding_config = [
1318
- (0, 0), # out_channels
1319
- (0, 0), # in_channels
1320
- (pad_h, pad_h), # height
1321
- (pad_w, pad_w), # width
1322
- ]
1323
- return jnp.pad(kernel, padding_config, "constant", constant_values=0)
1495
+ residual = x
1324
1496
 
1325
- # Pad the 1x1 kernels from the scale and skip branches.
1326
- padded_scale_weight = pad_kernel_to_target(
1327
- module.cheap_rpr_scale.weight, target_kernel_size
1328
- )
1329
- padded_skip_weight = pad_kernel_to_target(
1330
- module.cheap_rpr_skip.weight, target_kernel_size
1497
+ x = self.ghost1(x, key=k_g1, inference=use_inference)
1498
+
1499
+ if self.stride > 1:
1500
+ if use_inference:
1501
+ x = self.dw_shared_norm(self.dw_conv(x))
1502
+ else:
1503
+ terms = []
1504
+ if not isinstance(self.dw_rpr_scale, eqx.nn.Identity):
1505
+ terms.append(self.dw_rpr_scale(x))
1506
+ terms.extend(conv(x) for conv in self.dw_rpr_conv)
1507
+ x = self.dw_shared_norm(jax.tree_util.tree_reduce(operator.add, terms))
1508
+
1509
+ x = self.se(x)
1510
+
1511
+ x = self.ghost2(x, key=k_g2, inference=use_inference)
1512
+
1513
+ if not isinstance(self.shortcut, eqx.nn.Identity):
1514
+ x += self.shortcut(residual)
1515
+ elif self.allow_identity_residual:
1516
+ x += residual
1517
+
1518
+ return x
1519
+
1520
+
1521
+ def _pad_kernel_to_target(kernel: Array, target_size: tuple[int, int]) -> Array:
1522
+ kh, kw = kernel.shape[-2], kernel.shape[-1]
1523
+ th, tw = target_size
1524
+ if (kh, kw) == (th, tw):
1525
+ return kernel
1526
+ pad_h = (th - kh) // 2
1527
+ pad_w = (tw - kw) // 2
1528
+ padding_config = [
1529
+ (0, 0), # out_channels
1530
+ (0, 0), # in_channels_per_group
1531
+ (pad_h, pad_h), # height
1532
+ (pad_w, pad_w), # width
1533
+ ]
1534
+ return jnp.pad(kernel, padding_config, mode="constant", constant_values=0)
1535
+
1536
+
1537
+ def _make_identity_kernel_standard(out_ch: int, in_ch: int, kh: int, kw: int) -> Array:
1538
+ """Identity kernel for standard conv (groups=1). Only valid if out_ch == in_ch."""
1539
+ assert out_ch == in_ch, "Standard identity requires out_ch == in_ch."
1540
+ k = jnp.zeros((out_ch, in_ch, kh, kw))
1541
+ cy, cx = kh // 2, kw // 2
1542
+ idx = jnp.arange(out_ch)
1543
+ k = k.at[idx, idx, cy, cx].set(1.0)
1544
+ return k
1545
+
1546
+
1547
+ def _make_identity_kernel_depthwise(out_ch: int, in_ch: int, kh: int, kw: int) -> Array:
1548
+ """Identity kernel for depthwise conv with channel multiplier m = out_ch // in_ch."""
1549
+ assert out_ch % in_ch == 0, "Depthwise identity requires out_ch multiple of in_ch."
1550
+ m = out_ch // in_ch # channel multiplier
1551
+ k = jnp.zeros((out_ch, 1, kh, kw))
1552
+ cy, cx = kh // 2, kw // 2
1553
+ # For identity skip, we only use this when m==1 (ratio=2). Keep generic for clarity.
1554
+ for c in range(in_ch):
1555
+ for r in range(m):
1556
+ k = k.at[c * m + r, 0, cy, cx].set(1.0)
1557
+ return k
1558
+
1559
+
1560
+ def _update_ghostmodule(module: GenericGhostModule) -> GenericGhostModule:
1561
+ """
1562
+ Fuses the training branches into the inference convolutions (weights only).
1563
+ Keeps GroupNorms; no bias is fused (bias is redundant with GN's affine).
1564
+ """
1565
+ if not isinstance(module, GenericGhostModule):
1566
+ return module
1567
+
1568
+ # Fuse primary
1569
+ primary_w = jax.tree_util.tree_reduce(
1570
+ operator.add, [conv.weight for conv in module.primary_rpr_conv]
1331
1571
  )
1572
+ if not isinstance(module.primary_rpr_scale, eqx.nn.Identity):
1573
+ target_k = module.primary_conv.weight.shape[-2:]
1574
+ scale_w = _pad_kernel_to_target(module.primary_rpr_scale.weight, target_k)
1575
+ primary_w = primary_w + scale_w
1576
+ if module.primary_has_skip:
1577
+ kh, kw = module.primary_conv.weight.shape[-2:]
1578
+ id_w = _make_identity_kernel_standard(
1579
+ module.primary_conv.out_channels, module.primary_conv.in_channels, kh, kw
1580
+ )
1581
+ primary_w = primary_w + id_w
1582
+ fused_primary = eqx.tree_at(lambda c: c.weight, module.primary_conv, primary_w)
1332
1583
 
1333
- # Sum the main cheap convolution weights.
1334
- cheap_conv_weights = jax.tree_util.tree_reduce(
1584
+ # Fuse cheap (depthwise)
1585
+ cheap_w = jax.tree_util.tree_reduce(
1335
1586
  operator.add, [conv.weight for conv in module.cheap_rpr_conv]
1336
1587
  )
1588
+ if not isinstance(module.cheap_rpr_scale, eqx.nn.Identity):
1589
+ target_k = module.cheap_operation.weight.shape[-2:]
1590
+ scale_w = _pad_kernel_to_target(module.cheap_rpr_scale.weight, target_k)
1591
+ cheap_w = cheap_w + scale_w
1592
+ if module.cheap_has_skip:
1593
+ init_ch = module.primary_conv.out_channels
1594
+ new_ch = module.cheap_operation.out_channels
1595
+ kh, kw = module.cheap_operation.weight.shape[-2:]
1596
+ if new_ch == init_ch:
1597
+ id_w = _make_identity_kernel_depthwise(new_ch, init_ch, kh, kw)
1598
+ cheap_w = cheap_w + id_w
1599
+ fused_cheap = eqx.tree_at(lambda c: c.weight, module.cheap_operation, cheap_w)
1600
+
1601
+ # Install fused convs
1602
+ module = eqx.tree_at(lambda m: m.primary_conv, module, fused_primary)
1603
+ module = eqx.tree_at(lambda m: m.cheap_operation, module, fused_cheap)
1604
+
1605
+ return module
1337
1606
 
1338
- # Sum all weights: main convs + padded scale + padded skip.
1339
- fused_cheap_weights = cheap_conv_weights + padded_scale_weight + padded_skip_weight
1340
1607
 
1341
- fused_cheap_biases = None
1342
- if module.cheap_rpr_conv[0].bias is not None:
1343
- cheap_conv_biases = jax.tree_util.tree_reduce(
1344
- operator.add, [conv.bias for conv in module.cheap_rpr_conv]
1608
+ def _finalize_ghostmodule(module: "GenericGhostModule") -> "GenericGhostModule":
1609
+ """Finalize a GenericGhostModule for inference."""
1610
+ if not isinstance(module, GenericGhostModule):
1611
+ return module
1612
+ fused = _update_ghostmodule(module)
1613
+ # Drop training-only branches and switch to inference
1614
+ fused = eqx.tree_at(lambda m: m.primary_rpr_conv, fused, list())
1615
+ fused = eqx.tree_at(lambda m: m.cheap_rpr_conv, fused, list())
1616
+ fused = eqx.tree_at(lambda m: m.primary_rpr_scale, fused, eqx.nn.Identity())
1617
+ fused = eqx.tree_at(lambda m: m.cheap_rpr_scale, fused, eqx.nn.Identity())
1618
+ fused = eqx.tree_at(lambda m: m.inference, fused, True)
1619
+ return fused
1620
+
1621
+
1622
+ def _update_ghostbottleneck(module: "GhostBottleneck") -> "GhostBottleneck":
1623
+ """Fuse depthwise stage and nested GhostModules for a GhostBottleneck."""
1624
+ if not isinstance(module, GhostBottleneck):
1625
+ return module
1626
+
1627
+ # Fuse depthwise stage (if present)
1628
+ if module.stride > 1 and len(module.dw_rpr_conv) > 0:
1629
+ dw_w = jax.tree_util.tree_reduce(
1630
+ operator.add, [conv.weight for conv in module.dw_rpr_conv]
1345
1631
  )
1346
- scale_bias = module.cheap_rpr_scale.bias
1347
- skip_bias = module.cheap_rpr_skip.bias
1348
- fused_cheap_biases = cheap_conv_biases + scale_bias + skip_bias
1349
-
1350
- fused_cheap_operation = eqx.tree_at(
1351
- lambda conv: (conv.weight, conv.bias),
1352
- module.cheap_operation,
1353
- (fused_cheap_weights, fused_cheap_biases),
1354
- )
1632
+ if not isinstance(module.dw_rpr_scale, eqx.nn.Identity):
1633
+ target_k = module.dw_conv.weight.shape[-2:]
1634
+ scale_w = _pad_kernel_to_target(module.dw_rpr_scale.weight, target_k)
1635
+ dw_w = dw_w + scale_w
1636
+ fused_dw = eqx.tree_at(lambda c: c.weight, module.dw_conv, dw_w)
1637
+ module = eqx.tree_at(lambda m: m.dw_conv, module, fused_dw)
1638
+
1639
+ # Update nested GhostModules
1640
+ module = eqx.tree_at(lambda m: m.ghost1, module, _update_ghostmodule(module.ghost1))
1641
+ module = eqx.tree_at(lambda m: m.ghost2, module, _update_ghostmodule(module.ghost2))
1642
+ return module
1355
1643
 
1356
- module = eqx.tree_at(lambda m: m.primary_conv, module, fused_primary_conv)
1357
- module = eqx.tree_at(lambda m: m.cheap_operation, module, fused_cheap_operation)
1358
1644
 
1359
- return module
1645
+ def _finalize_ghostbottleneck(module: "GhostBottleneck") -> "GhostBottleneck":
1646
+ """Finalize a GhostBottleneck for inference."""
1647
+ if not isinstance(module, GhostBottleneck):
1648
+ return module
1649
+
1650
+ fused = _update_ghostbottleneck(module)
1651
+
1652
+ # Drop depthwise training branches
1653
+ if fused.stride > 1:
1654
+ fused = eqx.tree_at(lambda m: m.dw_rpr_conv, fused, list())
1655
+ fused = eqx.tree_at(lambda m: m.dw_rpr_scale, fused, eqx.nn.Identity())
1656
+
1657
+ # Finalize nested GhostModules
1658
+ fused = eqx.tree_at(lambda m: m.ghost1, fused, _finalize_ghostmodule(fused.ghost1))
1659
+ fused = eqx.tree_at(lambda m: m.ghost2, fused, _finalize_ghostmodule(fused.ghost2))
1660
+
1661
+ # Switch to inference
1662
+ fused = eqx.tree_at(lambda m: m.inference, fused, True)
1663
+ return fused
1360
1664
 
1361
1665
 
1362
- def update_ghostnet(model: eqx.Module) -> eqx.Module:
1666
+ def update_ghostnet(
1667
+ model: eqx.Module | Union["GenericGhostModule", "GhostBottleneck"],
1668
+ ) -> eqx.Module | Union["GenericGhostModule", "GhostBottleneck"]:
1363
1669
  """
1364
- Update the inference layers of a GhostNet-like model.
1365
- Useful for intermediary evals.
1670
+ Recursively fuse training branches for both GenericGhostModule and GhostBottleneck.
1671
+ Keeps GroupNorm layers; no bias fusion.
1366
1672
  """
1367
1673
 
1368
- def _update_leaf(module: GenericGhostModule) -> GenericGhostModule:
1369
- if not isinstance(module, GenericGhostModule):
1370
- return module
1371
- return update_fused_ghostmodule(module)
1674
+ def _update_leaf(m):
1675
+ if isinstance(m, GenericGhostModule):
1676
+ return _update_ghostmodule(m)
1677
+ if isinstance(m, GhostBottleneck):
1678
+ return _update_ghostbottleneck(m)
1679
+ return m
1372
1680
 
1373
- is_g_module = lambda m: isinstance(m, GenericGhostModule)
1374
- return jax.tree_util.tree_map(_update_leaf, model, is_leaf=is_g_module)
1681
+ is_leaf = lambda m: isinstance(m, (GenericGhostModule, GhostBottleneck))
1682
+ return jax.tree_util.tree_map(_update_leaf, model, is_leaf=is_leaf)
1375
1683
 
1376
1684
 
1377
- def finalize_ghostnet(model: eqx.Module) -> eqx.Module:
1685
+ def finalize_ghostnet(
1686
+ model: eqx.Module | Union["GenericGhostModule", "GhostBottleneck"],
1687
+ ) -> eqx.Module | Union["GenericGhostModule", "GhostBottleneck"]:
1378
1688
  """
1379
- Finalizes a trained GhostNet-like model for inference.
1689
+ Recursively finalize both GenericGhostModule and GhostBottleneck for inference:
1690
+ - Fuse training branches.
1691
+ - Remove training-only branches.
1692
+ - Switch to inference path.
1693
+ """
1694
+
1695
+ def _finalize_leaf(m):
1696
+ if isinstance(m, GenericGhostModule):
1697
+ return _finalize_ghostmodule(m)
1698
+ if isinstance(m, GhostBottleneck):
1699
+ return _finalize_ghostbottleneck(m)
1700
+ return m
1701
+
1702
+ is_leaf = lambda m: isinstance(m, (GenericGhostModule, GhostBottleneck))
1703
+ return jax.tree_util.tree_map(_finalize_leaf, model, is_leaf=is_leaf)
1704
+
1705
+
1706
+ class PartialConv2d(eqx.Module):
1707
+ """Partial 2D convolution on the channel dimension.
1708
+
1709
+ This layer applies a standard 2D convolution only to the first `C // n_dim`
1710
+ input channels and leaves the remaining channels untouched (identity). It
1711
+ follows the "partial convolution" idea used to increase throughput by
1712
+ reducing compute on a subset of channels while preserving overall tensor
1713
+ shape, as explored in [1].
1714
+
1715
+ References:
1716
+ [1]. Chen et al., "Run, Don't Walk: Chasing Higher FLOPS for Faster Neural
1717
+ Networks" [arXiv:2303.03667](https://arxiv.org/abs/2303.03667).
1718
+
1719
+ Implementation details:
1720
+ - Let `C` be `in_channels` and `c = C // n_dim`. Only the first `c`
1721
+ channels are convolved with a `Conv2d(c, c, ...)`. The remaining
1722
+ `C - c` channels are forwarded unchanged.
1723
+ - The forward pass uses a functional "update-slice" pattern
1724
+ (`x.at[:c, ...].set(y1)`), which compiles to an efficient
1725
+ `dynamic_update_slice` under XLA. With JIT buffer donation, this can be
1726
+ performed in-place by the compiler.
1727
+ - Spatial dimensions must be preserved by the convolution so that the
1728
+ updated slice matches the input slice shape (e.g., use `stride=1` and
1729
+ `padding="SAME"`). If spatial dimensions change, the slice update will
1730
+ fail with a shape error.
1731
+
1732
+ Attributes
1733
+ - dim: Number of channels to be convolved, computed as `in_channels // n_dim`.
1734
+ This is treated as a static field for compilation stability.
1735
+ - conv: The underlying `eqx.nn.Conv2d(c, c, ...)` applied to the first `c`
1736
+ channels.
1737
+
1738
+ Notes
1739
+ - FLOPs reduction is approximately `c / C = 1 / n_dim` relative to a full
1740
+ convolution with the same kernel.
1741
+ """
1742
+
1743
+ dim: int = eqx.field(static=True)
1744
+
1745
+ conv: eqx.nn.Conv2d
1746
+
1747
+ def __init__(
1748
+ self,
1749
+ in_channels: int,
1750
+ n_dim: int,
1751
+ *,
1752
+ key: PRNGKeyArray,
1753
+ kernel_size: int = 3,
1754
+ padding: str | int = "SAME",
1755
+ use_bias: bool = False,
1756
+ **kwargs,
1757
+ ):
1758
+ """
1759
+ Initialize a PartialConv2d layer.
1760
+
1761
+ Parameters
1762
+ - in_channels: Total number of input channels `C`.
1763
+ - n_dim: Divisor used to determine the number of convolved channels.
1764
+ The layer will convolve `C // n_dim` channels and leave the
1765
+ remaining channels untouched. Must be > 0, and
1766
+ `C // n_dim` must be >= 1 for a meaningful layer.
1767
+ - key: PRNG key used to initialize the underlying convolution weights.
1768
+ - kernel_size: Convolution kernel size (passed to `eqx.nn.Conv2d`).
1769
+ - padding: Convolution padding (passed to `eqx.nn.Conv2d`). Use
1770
+ `"SAME"` to preserve spatial dimensions with `stride=1`.
1771
+ Integer or other forms supported by `eqx.nn.Conv2d` are also
1772
+ accepted, but must preserve H and W for the slice update.
1773
+ - use_bias: Whether to include a bias term in the underlying convolution.
1774
+ - **kwargs: Forwarded to `eqx.nn.Conv2d` (e.g., `dilation`, `groups`).
1775
+ """
1776
+ self.dim = in_channels // n_dim
1777
+ assert self.dim >= 1, "in_channels // n_dim must be >= 1"
1778
+ assert self.dim <= in_channels, (
1779
+ "Computed convolved channels exceed total channels"
1780
+ )
1781
+
1782
+ if isinstance(padding, str):
1783
+ assert padding.upper() == "SAME", (
1784
+ 'When padding is a string, it must be "SAME"'
1785
+ )
1786
+ else:
1787
+ # If padding is numeric, ensure it preserves H, W for stride=1
1788
+ # For Conv2d with dilation d and kernel k, effective kernel is k_eff = (k - 1) * d + 1.
1789
+ # Output H' = H + 2*pad - k_eff + 1; preserving H requires k_eff odd and pad = k_eff // 2.
1790
+ dilation = kwargs.get("dilation", 1)
1791
+ if isinstance(dilation, (tuple, list)):
1792
+ # Require isotropic dilation for simplicity
1793
+ assert len(dilation) == 2 and dilation[0] == dilation[1], (
1794
+ "dilation must be an int or an equal pair"
1795
+ )
1796
+ dilation = dilation[0]
1797
+ assert isinstance(dilation, int) and dilation >= 1, (
1798
+ "dilation must be a positive int"
1799
+ )
1800
+ assert isinstance(kernel_size, int) and kernel_size >= 1, (
1801
+ "kernel_size must be a positive int"
1802
+ )
1803
+
1804
+ k_eff = (kernel_size - 1) * dilation + 1
1805
+ assert isinstance(padding, int) and padding >= 0, (
1806
+ "padding must be a non-negative int"
1807
+ )
1808
+ assert k_eff % 2 == 1 and padding == k_eff // 2, (
1809
+ "Integer padding must preserve spatial size: require effective kernel odd and "
1810
+ f"padding == ((kernel_size-1)*dilation+1)//2; got k_eff={k_eff}, padding={padding}"
1811
+ )
1812
+
1813
+ self.conv = eqx.nn.Conv2d(
1814
+ in_channels=self.dim,
1815
+ out_channels=self.dim,
1816
+ kernel_size=kernel_size,
1817
+ stride=1,
1818
+ padding=padding,
1819
+ use_bias=use_bias,
1820
+ key=key,
1821
+ **kwargs,
1822
+ )
1823
+
1824
+ def __call__(
1825
+ self,
1826
+ x: Float[Array, "channels height width"],
1827
+ *args,
1828
+ **kwargs,
1829
+ ) -> Float[Array, "channels height width"]:
1830
+ c = self.dim
1831
+ x1 = x[:c, :, :]
1832
+ y1 = self.conv(x1)
1833
+
1834
+ return x.at[:c, :, :].set(y1)
1380
1835
 
1381
- This function recursively traverses the model and for each GenericGhostModule:
1382
- 1. Fuses the parallel convolutional branches.
1383
- 2. Replaces the training-only branches with Identity layers.
1836
+
1837
+ class FasterNetBlock(eqx.Module):
1838
+ """
1839
+ FasterNet-style residual block with Partial Convolution-based spatial mixing
1840
+ and pointwise MLP, adapted to Equinox/JAX.
1841
+
1842
+ Structure
1843
+ - Spatial mixing: `PartialConv2d` applies a 3×3 convolution to the first
1844
+ `C // n_dim` channels and leaves the remaining channels unchanged. This
1845
+ reduces compute while keeping the tensor shape intact. See [1].
1846
+ - Channel MLP: two pointwise (1*1) convolutions expand and then project
1847
+ channels (`C -> mlp_ratio*C -> C`), with normalization and activation
1848
+ in between.
1849
+ - Regularization: includes dropout after the MLP and optional stochastic
1850
+ depth (DropPath) on the residual branch.
1851
+ - Residual: optional residual connection `y = x + DropPath(MLP(SpatialMix(x)))`.
1852
+
1853
+ Shape invariants
1854
+ - Input: `[channels, height, width]`
1855
+ - Output: `[channels, height, width]` (same spatial and channel dimensions)
1856
+
1857
+ References
1858
+ - [1] Chen et al., "Run, Don't Walk: Chasing Higher FLOPS for Faster Neural
1859
+ Networks" [arXiv:2303.03667](https://arxiv.org/abs/2303.03667).
1860
+
1861
+ Attributes
1862
+ - residual: Whether to use a residual connection with stochastic depth.
1863
+ - spatial_mixing: `PartialConv2d` performing partial 3×3 spatial mixing.
1864
+ - pw_conv1: Pointwise convolution expanding channels to `mlp_ratio * C`.
1865
+ - pw_conv2: Pointwise convolution projecting channels back to `C`.
1866
+ - norm: Normalization layer applied on the expanded channels.
1867
+ - act: Activation applied after normalization (defaults to identity if none).
1868
+ - dropout: Dropout applied after the MLP.
1869
+ - drop_path: Stochastic depth module for residual addition.
1384
1870
  """
1385
1871
 
1386
- def _finalize_leaf(module: GenericGhostModule) -> GenericGhostModule:
1387
- if not isinstance(module, GenericGhostModule):
1388
- return module
1872
+ residual: bool = eqx.field(static=True)
1389
1873
 
1390
- fused_module = update_fused_ghostmodule(module)
1874
+ spatial_mixing: eqx.nn.Conv2d
1875
+ pw_conv1: eqx.nn.Conv2d
1876
+ pw_conv2: eqx.nn.Conv2d
1877
+ norm: eqx.Module
1878
+ act: eqx.Module
1879
+ dropout: eqx.nn.Dropout
1880
+ drop_path: DropPathAdd
1391
1881
 
1392
- # identity_list = [eqx.nn.Identity()] * len(module.primary_rpr_conv)
1393
- final_module = eqx.tree_at(lambda m: m.primary_rpr_conv, fused_module, list())
1394
- final_module = eqx.tree_at(lambda m: m.cheap_rpr_conv, final_module, list())
1395
- final_module = eqx.tree_at(
1396
- lambda m: m.cheap_rpr_scale, final_module, eqx.nn.Identity()
1882
+ def __init__(
1883
+ self,
1884
+ in_channels: int,
1885
+ *,
1886
+ key: PRNGKeyArray,
1887
+ n_dim: int = 4,
1888
+ mlp_ratio: int = 3,
1889
+ kernel_size: int = 3,
1890
+ padding: str | int = "SAME",
1891
+ norm_layer: eqx.Module | None = eqx.nn.GroupNorm,
1892
+ norm_max_group: int = 32,
1893
+ act_layer: Callable | None = jax.nn.relu,
1894
+ dropout: float = 0.0,
1895
+ drop_path: float = 0.0,
1896
+ norm_kwargs: dict = {},
1897
+ residual: bool = True,
1898
+ **kwargs,
1899
+ ):
1900
+ """
1901
+ Initialize a FasterNetBlock.
1902
+
1903
+ Parameters
1904
+ - in_channels: Number of input/output channels `C`.
1905
+ - key: PRNG key used to initialize submodules. Internally split for
1906
+ spatial mixing and pointwise convolutions.
1907
+ - n_dim: Divisor determining the fraction of channels convolved by
1908
+ `PartialConv2d`; the convolved channels are `C // n_dim`.
1909
+ - mlp_ratio: Expansion ratio for the MLP (1×1 convs): hidden size is
1910
+ `mlp_ratio * C`.
1911
+ - kernel_size: Kernel size for the spatial mixing convolution (passed
1912
+ to `PartialConv2d`).
1913
+ - padding: Padding for the spatial mixing convolution. Use `"SAME"`
1914
+ to preserve spatial dimensions.
1915
+ - norm_layer: Normalization constructor applied on the expanded
1916
+ channels. If `eqx.nn.GroupNorm`, the number of groups is chosen as
1917
+ the largest power-of-two divisor of `hidden_channels` not exceeding
1918
+ `norm_max_group`. If `None`, uses identity.
1919
+ - norm_max_group: Maximum group count when using `GroupNorm`.
1920
+ - act_layer: Callable used to construct an activation function. If
1921
+ `None`, uses identity. Passed to `eqx.nn.Lambda`.
1922
+ - dropout: Dropout probability applied after the MLP branch.
1923
+ - drop_path: Stochastic depth probability on the residual branch.
1924
+ - norm_kwargs: Extra keyword arguments forwarded to the normalization
1925
+ layer constructor.
1926
+ - residual: Whether to add the residual connection (with DropPath).
1927
+ - **kwargs: Reserved for future extensions; forwarded where applicable.
1928
+
1929
+ Notes
1930
+ - The block preserves `[H, W]`. Ensure `PartialConv2d` is configured
1931
+ to preserve spatial dimensions (e.g., `"SAME"` padding).
1932
+ - When `norm_layer` is not `GroupNorm`, it should accept the channel
1933
+ count as its first argument.
1934
+ - The same input/output channel count `C` is used throughout the block.
1935
+ """
1936
+
1937
+ key_sm, key_pw1, key_pw2 = jr.split(key, 3)
1938
+ self.residual = residual
1939
+
1940
+ self.spatial_mixing = PartialConv2d(
1941
+ in_channels=in_channels, n_dim=n_dim, key=key_sm
1397
1942
  )
1398
- final_module = eqx.tree_at(
1399
- lambda m: m.cheap_rpr_skip, final_module, eqx.nn.Identity()
1943
+
1944
+ hidden_channels = mlp_ratio * in_channels
1945
+ self.pw_conv1 = eqx.nn.Conv2d(
1946
+ in_channels,
1947
+ hidden_channels,
1948
+ kernel_size=1,
1949
+ stride=1,
1950
+ padding="SAME",
1951
+ use_bias=False,
1952
+ key=key_pw1,
1953
+ )
1954
+ self.pw_conv2 = eqx.nn.Conv2d(
1955
+ hidden_channels,
1956
+ in_channels,
1957
+ kernel_size=1,
1958
+ stride=1,
1959
+ padding="SAME",
1960
+ use_bias=False,
1961
+ key=key_pw2,
1962
+ )
1963
+
1964
+ if norm_layer is not None:
1965
+ if norm_layer == eqx.nn.GroupNorm:
1966
+ num_groups = nearest_power_of_2_divisor(hidden_channels, norm_max_group)
1967
+ self.norm = eqx.nn.GroupNorm(num_groups, hidden_channels, **norm_kwargs)
1968
+ else:
1969
+ self.norm = norm_layer(hidden_channels, **norm_kwargs)
1970
+ else:
1971
+ self.norm = eqx.nn.Identity()
1972
+
1973
+ self.dropout = eqx.nn.Dropout(dropout)
1974
+ self.drop_path = DropPathAdd(drop_path)
1975
+ self.act = eqx.nn.Lambda(act_layer) if act_layer else eqx.nn.Identity()
1976
+
1977
+ def __call__(
1978
+ self,
1979
+ x: Float[Array, "channels height width"],
1980
+ key: PRNGKeyArray,
1981
+ inference: Optional[bool] = None,
1982
+ ) -> Float[Array, "channels height width"]:
1983
+ key_dropout, key_droppath = jr.split(key, 2)
1984
+ x1 = self.spatial_mixing(x)
1985
+ out = self.dropout(
1986
+ self.pw_conv2(self.act(self.norm(self.pw_conv1(x1)))),
1987
+ inference=inference,
1988
+ key=key_dropout,
1400
1989
  )
1401
1990
 
1402
- return final_module
1991
+ if self.residual:
1992
+ out = self.drop_path(x, out, inference=inference, key=key_droppath)
1403
1993
 
1404
- is_g_module = lambda m: isinstance(m, GenericGhostModule)
1405
- return jax.tree_util.tree_map(_finalize_leaf, model, is_leaf=is_g_module)
1994
+ return out
@@ -1 +0,0 @@
1
- __version__ = "0.4.3"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes