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.
- {equimo-0.4.3/src/Equimo.egg-info → equimo-0.4.5}/PKG-INFO +1 -1
- {equimo-0.4.3 → equimo-0.4.5}/pyproject.toml +1 -1
- {equimo-0.4.3 → equimo-0.4.5/src/Equimo.egg-info}/PKG-INFO +1 -1
- equimo-0.4.5/src/equimo/__init__.py +1 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/convolution.py +748 -159
- equimo-0.4.3/src/equimo/__init__.py +0 -1
- {equimo-0.4.3 → equimo-0.4.5}/LICENSE.md +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/README.md +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/setup.cfg +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/SOURCES.txt +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/dependency_links.txt +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/requires.txt +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/Equimo.egg-info/top_level.txt +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/conversion/__init__.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/conversion/utils.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/experimental/__init__.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/experimental/text.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/io.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/__init__.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/activation.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/attention.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/downsample.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/dropout.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/ffn.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/generic.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/mamba.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/norm.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/patch.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/posemb.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/sharing.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/layers/squeeze_excite.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/__init__.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/emamodel.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/fastervit.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/lowformer.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/mlla.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/partialformer.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/reduceformer.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/shvit.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/vit.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/models/vssd.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/ops/scan.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/src/equimo/utils.py +0 -0
- {equimo-0.4.3 → equimo-0.4.5}/tests/test_models.py +0 -0
|
@@ -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
|
-
"""
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1090
|
-
|
|
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=
|
|
1150
|
+
stride=1, # MOD
|
|
1117
1151
|
padding=dw_size // 2,
|
|
1118
|
-
groups=
|
|
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
|
-
|
|
1139
|
-
|
|
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=
|
|
1194
|
+
stride=1,
|
|
1162
1195
|
padding=dw_size // 2,
|
|
1163
|
-
groups=
|
|
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
|
-
|
|
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
|
-
|
|
1215
|
-
|
|
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
|
-
|
|
1264
|
+
self.gate_scale = 1.0
|
|
1219
1265
|
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
]
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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
|
-
|
|
1246
|
-
|
|
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(
|
|
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
|
-
|
|
1271
|
-
|
|
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
|
-
|
|
1277
|
-
|
|
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
|
-
|
|
1280
|
-
module: An instance of GenericGhostModule with trained weights.
|
|
1326
|
+
inference: bool
|
|
1281
1327
|
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
#
|
|
1287
|
-
|
|
1288
|
-
|
|
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
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
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
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
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
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
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
|
-
|
|
1314
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
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
|
-
#
|
|
1334
|
-
|
|
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
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
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
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
lambda
|
|
1352
|
-
|
|
1353
|
-
|
|
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
|
-
|
|
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(
|
|
1666
|
+
def update_ghostnet(
|
|
1667
|
+
model: eqx.Module | Union["GenericGhostModule", "GhostBottleneck"],
|
|
1668
|
+
) -> eqx.Module | Union["GenericGhostModule", "GhostBottleneck"]:
|
|
1363
1669
|
"""
|
|
1364
|
-
|
|
1365
|
-
|
|
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(
|
|
1369
|
-
if
|
|
1370
|
-
return
|
|
1371
|
-
|
|
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
|
-
|
|
1374
|
-
return jax.tree_util.tree_map(_update_leaf, model, is_leaf=
|
|
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(
|
|
1685
|
+
def finalize_ghostnet(
|
|
1686
|
+
model: eqx.Module | Union["GenericGhostModule", "GhostBottleneck"],
|
|
1687
|
+
) -> eqx.Module | Union["GenericGhostModule", "GhostBottleneck"]:
|
|
1378
1688
|
"""
|
|
1379
|
-
|
|
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
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
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
|
-
|
|
1387
|
-
if not isinstance(module, GenericGhostModule):
|
|
1388
|
-
return module
|
|
1872
|
+
residual: bool = eqx.field(static=True)
|
|
1389
1873
|
|
|
1390
|
-
|
|
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
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
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
|
-
|
|
1399
|
-
|
|
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
|
-
|
|
1991
|
+
if self.residual:
|
|
1992
|
+
out = self.drop_path(x, out, inference=inference, key=key_droppath)
|
|
1403
1993
|
|
|
1404
|
-
|
|
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
|
|
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
|
|
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
|
|
File without changes
|
|
File without changes
|