Equimo 0.4.4__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.4/src/Equimo.egg-info → equimo-0.4.5}/PKG-INFO +1 -1
  2. {equimo-0.4.4 → equimo-0.4.5}/pyproject.toml +1 -1
  3. {equimo-0.4.4 → 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.4 → equimo-0.4.5}/src/equimo/layers/convolution.py +463 -165
  6. equimo-0.4.4/src/equimo/__init__.py +0 -1
  7. {equimo-0.4.4 → equimo-0.4.5}/LICENSE.md +0 -0
  8. {equimo-0.4.4 → equimo-0.4.5}/README.md +0 -0
  9. {equimo-0.4.4 → equimo-0.4.5}/setup.cfg +0 -0
  10. {equimo-0.4.4 → equimo-0.4.5}/src/Equimo.egg-info/SOURCES.txt +0 -0
  11. {equimo-0.4.4 → equimo-0.4.5}/src/Equimo.egg-info/dependency_links.txt +0 -0
  12. {equimo-0.4.4 → equimo-0.4.5}/src/Equimo.egg-info/requires.txt +0 -0
  13. {equimo-0.4.4 → equimo-0.4.5}/src/Equimo.egg-info/top_level.txt +0 -0
  14. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/conversion/__init__.py +0 -0
  15. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/conversion/utils.py +0 -0
  16. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/experimental/__init__.py +0 -0
  17. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/experimental/text.py +0 -0
  18. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/io.py +0 -0
  19. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/__init__.py +0 -0
  20. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/activation.py +0 -0
  21. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/attention.py +0 -0
  22. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/downsample.py +0 -0
  23. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/dropout.py +0 -0
  24. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/ffn.py +0 -0
  25. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/generic.py +0 -0
  26. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/mamba.py +0 -0
  27. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/norm.py +0 -0
  28. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/patch.py +0 -0
  29. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/posemb.py +0 -0
  30. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/sharing.py +0 -0
  31. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/layers/squeeze_excite.py +0 -0
  32. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/__init__.py +0 -0
  33. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/emamodel.py +0 -0
  34. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/fastervit.py +0 -0
  35. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/lowformer.py +0 -0
  36. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/mlla.py +0 -0
  37. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/partialformer.py +0 -0
  38. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/reduceformer.py +0 -0
  39. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/shvit.py +0 -0
  40. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/vit.py +0 -0
  41. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/models/vssd.py +0 -0
  42. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/ops/scan.py +0 -0
  43. {equimo-0.4.4 → equimo-0.4.5}/src/equimo/utils.py +0 -0
  44. {equimo-0.4.4 → 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.4
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.4"
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.4
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))
1219
-
1220
- cheap_branches = [self.cheap_rpr_skip(x1), self.cheap_rpr_scale(x1)] + [
1221
- conv(x1) for conv in self.cheap_rpr_conv
1222
- ]
1264
+ self.gate_scale = 1.0
1223
1265
 
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,142 +1314,393 @@ 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
1339
+
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
1290
1361
 
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]
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)
1413
+
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()
1438
+
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
+ )
1304
1484
 
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:]
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)
1307
1494
 
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
1495
+ residual = x
1312
1496
 
1313
- pad_h = (target_size[0] - current_size[0]) // 2
1314
- pad_w = (target_size[1] - current_size[1]) // 2
1497
+ x = self.ghost1(x, key=k_g1, inference=use_inference)
1315
1498
 
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)
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
1324
1567
 
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
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
1360
1649
 
1650
+ fused = _update_ghostbottleneck(module)
1361
1651
 
1362
- def update_ghostnet(model: eqx.Module) -> eqx.Module:
1363
- """
1364
- Update the inference layers of a GhostNet-like model.
1365
- Useful for intermediary evals.
1366
- """
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())
1367
1656
 
1368
- def _update_leaf(module: GenericGhostModule) -> GenericGhostModule:
1369
- if not isinstance(module, GenericGhostModule):
1370
- return module
1371
- return update_fused_ghostmodule(module)
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))
1372
1660
 
1373
- is_g_module = lambda m: isinstance(m, GenericGhostModule)
1374
- return jax.tree_util.tree_map(_update_leaf, model, is_leaf=is_g_module)
1661
+ # Switch to inference
1662
+ fused = eqx.tree_at(lambda m: m.inference, fused, True)
1663
+ return fused
1375
1664
 
1376
1665
 
1377
- def finalize_ghostnet(model: eqx.Module) -> eqx.Module:
1666
+ def update_ghostnet(
1667
+ model: eqx.Module | Union["GenericGhostModule", "GhostBottleneck"],
1668
+ ) -> eqx.Module | Union["GenericGhostModule", "GhostBottleneck"]:
1378
1669
  """
1379
- Finalizes a trained GhostNet-like model for inference.
1380
-
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.
1670
+ Recursively fuse training branches for both GenericGhostModule and GhostBottleneck.
1671
+ Keeps GroupNorm layers; no bias fusion.
1384
1672
  """
1385
1673
 
1386
- def _finalize_leaf(module: GenericGhostModule) -> GenericGhostModule:
1387
- if not isinstance(module, GenericGhostModule):
1388
- return 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
1389
1680
 
1390
- fused_module = update_fused_ghostmodule(module)
1681
+ is_leaf = lambda m: isinstance(m, (GenericGhostModule, GhostBottleneck))
1682
+ return jax.tree_util.tree_map(_update_leaf, model, is_leaf=is_leaf)
1391
1683
 
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()
1397
- )
1398
- final_module = eqx.tree_at(
1399
- lambda m: m.cheap_rpr_skip, final_module, eqx.nn.Identity()
1400
- )
1401
1684
 
1402
- return final_module
1685
+ def finalize_ghostnet(
1686
+ model: eqx.Module | Union["GenericGhostModule", "GhostBottleneck"],
1687
+ ) -> eqx.Module | Union["GenericGhostModule", "GhostBottleneck"]:
1688
+ """
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
1403
1701
 
1404
- is_g_module = lambda m: isinstance(m, GenericGhostModule)
1405
- return jax.tree_util.tree_map(_finalize_leaf, model, is_leaf=is_g_module)
1702
+ is_leaf = lambda m: isinstance(m, (GenericGhostModule, GhostBottleneck))
1703
+ return jax.tree_util.tree_map(_finalize_leaf, model, is_leaf=is_leaf)
1406
1704
 
1407
1705
 
1408
1706
  class PartialConv2d(eqx.Module):
@@ -1592,7 +1890,7 @@ class FasterNetBlock(eqx.Module):
1592
1890
  padding: str | int = "SAME",
1593
1891
  norm_layer: eqx.Module | None = eqx.nn.GroupNorm,
1594
1892
  norm_max_group: int = 32,
1595
- act_layer: Callable | None = None,
1893
+ act_layer: Callable | None = jax.nn.relu,
1596
1894
  dropout: float = 0.0,
1597
1895
  drop_path: float = 0.0,
1598
1896
  norm_kwargs: dict = {},
@@ -1 +0,0 @@
1
- __version__ = "0.4.4"
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