Skip to content

Commit 714f1fc

Browse files
authored
feat(timm_mobilenetv4): add the timm MobileNetV4 image-classification family (1276)
## Implementation Almost the whole layout comes out of the checkpoint. The three block kinds carry disjoint leaf names, so the kind is read rather than tabulated: | block kind | identifying leaf | | --- | --- | | universal inverted bottleneck | `pw_exp` | | edge residual | `conv_exp` | | plain convolution | `conv` | Kernel sizes, channel counts and which of the optional depthwise convolutions are present all follow from weight shapes. A block carrying anything outside the supported set - squeeze-excite or layer scale, which other MobileNetV4 variants use - is refused, so a silently partial build cannot look like a working one. Three things are not in a safetensors checkpoint and are stated explicitly. Each was measured against the reference across all four widths: | value | rule | measured over | | --- | --- | --- | | stride | 2 on the first block of stages 0-3, else 1 | 4 widths; stem 2 gives the factor of 32 the published input sizes divide by | | activation | ReLU throughout, but not after `dw_start`, `pw_proj` or an edge residual's second norm | 4 widths | | residual | plain convolutions never add their input back; the others do when stride is 1 and channels match | 6 widths, 136 blocks |
1 parent 4c162bf commit 714f1fc

24 files changed

Lines changed: 2010 additions & 0 deletions

apps/benchmark/performance/release.yaml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,15 @@ excluded_profiles:
4949
performance baseline runs Ultralytics, which cannot load a legacy YOLOv5
5050
archive: those archives pickle classes from the standalone yolov5
5151
repository, which is not a dependency here.
52+
- model: mobilenetv4-conv-small
53+
reason: &mobilenetv4_performance_exclusion >-
54+
Functional and timm reference-parity qualification is present for every
55+
claimed MobileNetV4 width, but the release-performance workload and
56+
receipt were collected only for the 050 width.
57+
- model: mobilenetv4-conv-medium
58+
reason: *mobilenetv4_performance_exclusion
59+
- model: mobilenetv4-conv-large
60+
reason: *mobilenetv4_performance_exclusion
5261
- model: dpn92-mx-in1k
5362
reason: >-
5463
Functional and timm reference-parity qualification is present for every
@@ -1376,6 +1385,19 @@ entries:
13761385
reference_backend: hf_transformers
13771386
timing_scope: task-model-call-wall
13781387
input_preparation_included: false
1388+
- id: timm_mobilenetv4.classify
1389+
family: timm_mobilenetv4
1390+
operation: classify
1391+
model: mobilenetv4-conv-small-050
1392+
workload:
1393+
testcase: mobilenetv4-conv-small-050
1394+
baseline:
1395+
runner: task-reference
1396+
adapter: hf-transformers-vision
1397+
mode: hf-eager
1398+
reference_backend: hf_transformers
1399+
timing_scope: task-model-call-wall
1400+
input_preparation_included: false
13791401
- id: timm_mobilenetv3.classify
13801402
family: timm_mobilenetv3
13811403
operation: classify
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""timm MobileNetV4 model family."""
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Read the timm MobileNetV4 fields used by this family."""
5+
6+
from __future__ import annotations
7+
8+
import json
9+
from dataclasses import dataclass
10+
from pathlib import Path
11+
12+
13+
@dataclass
14+
class ModelConfig:
15+
architecture: str
16+
raw: dict
17+
18+
@staticmethod
19+
def from_json(text: str) -> "ModelConfig":
20+
raw = json.loads(text)
21+
architecture = raw.get("architecture")
22+
if not isinstance(architecture, str) or not architecture:
23+
raise ValueError("timm MobileNetV4 config requires architecture")
24+
return ModelConfig(architecture=architecture, raw=raw)
25+
26+
@classmethod
27+
def from_dir(cls, model_dir: str | Path) -> "ModelConfig":
28+
config_path = Path(model_dir) / "config.json"
29+
if not config_path.is_file():
30+
raise FileNotFoundError(f"missing model config: {config_path}")
31+
return cls.from_json(config_path.read_text(encoding="utf-8"))
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Family-owned TensorRT model construction components."""
Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""TensorRT graph builders for timm MobileNetV4 classifiers.
5+
6+
MobileNetV4 adds three things to the plain convolutional op set: the hard
7+
activations (hard-swish and hard-sigmoid), depthwise separable convolutions,
8+
and a squeeze-and-excite gate.
9+
"""
10+
11+
from __future__ import annotations
12+
13+
import numpy as np
14+
import tensorrt as trt
15+
16+
17+
def add_conv2d(
18+
network,
19+
inp,
20+
weight: np.ndarray,
21+
bias: np.ndarray | None,
22+
out_channels: int,
23+
kernel_size: tuple[int, int],
24+
stride: tuple[int, int] = (1, 1),
25+
padding: tuple[int, int] = (0, 0),
26+
groups: int = 1,
27+
*,
28+
dtype: np.dtype,
29+
):
30+
"""2D convolution wrapper.
31+
32+
Input: [N, C_in, H, W]
33+
Weight: [C_out, C_in/groups, kH, kW]
34+
Output: [N, C_out, H', W']
35+
36+
Convolution bias is present only in the squeeze-excite and classifier heads.
37+
"""
38+
conv_w = trt.Weights(np.ascontiguousarray(weight, dtype=dtype))
39+
conv_b = trt.Weights()
40+
if bias is not None:
41+
conv_b = trt.Weights(np.ascontiguousarray(bias, dtype=dtype))
42+
43+
conv = network.add_convolution_nd(
44+
inp,
45+
num_output_maps=out_channels,
46+
kernel_shape=kernel_size,
47+
kernel=conv_w,
48+
bias=conv_b,
49+
)
50+
conv.stride_nd = stride
51+
conv.padding_nd = padding
52+
conv.num_groups = groups
53+
return conv.get_output(0)
54+
55+
56+
def add_batch_norm(
57+
network,
58+
x,
59+
gamma: np.ndarray,
60+
beta: np.ndarray,
61+
running_mean: np.ndarray,
62+
running_var: np.ndarray,
63+
eps: float,
64+
*,
65+
dtype: np.dtype,
66+
):
67+
"""Fold inference-time batch norm into a single per-channel scale+shift.
68+
69+
y = (x - mean) / sqrt(var + eps) * gamma + beta
70+
= x * scale + shift
71+
Folding avoids emitting a normalization layer whose statistics are
72+
constant at inference time.
73+
"""
74+
scale = (gamma / np.sqrt(running_var + eps)).astype(np.float32)
75+
shift = (beta - running_mean * scale).astype(np.float32)
76+
layer = network.add_scale(
77+
x,
78+
trt.ScaleMode.CHANNEL,
79+
shift=trt.Weights(np.ascontiguousarray(shift, dtype=dtype)),
80+
scale=trt.Weights(np.ascontiguousarray(scale, dtype=dtype)),
81+
)
82+
return layer.get_output(0)
83+
84+
85+
def add_relu(network, x):
86+
return network.add_activation(x, trt.ActivationType.RELU).get_output(0)
87+
88+
89+
def add_global_avg_pool(network, x, spatial: tuple[int, int]):
90+
pool = network.add_pooling_nd(x, trt.PoolingType.AVERAGE, spatial)
91+
pool.stride_nd = (1, 1)
92+
return pool.get_output(0)
93+
94+
95+
def add_sum(network, a, b):
96+
return network.add_elementwise(a, b, trt.ElementWiseOperation.SUM).get_output(0)
97+
98+
99+
def add_fc(
100+
network,
101+
x,
102+
in_features: int,
103+
out_features: int,
104+
weight: np.ndarray,
105+
bias: np.ndarray,
106+
*,
107+
dtype: np.dtype,
108+
):
109+
"""Final classifier: flatten the pooled feature map, then y = x @ W^T + b."""
110+
flat = network.add_shuffle(x)
111+
flat.reshape_dims = (1, in_features)
112+
flat_out = flat.get_output(0)
113+
114+
# timm stores fc.weight as (out, in); TensorRT wants the (in, out) operand.
115+
w = np.ascontiguousarray(weight.T, dtype=dtype)
116+
w_const = network.add_constant((in_features, out_features), trt.Weights(w)).get_output(0)
117+
mm = network.add_matrix_multiply(
118+
flat_out,
119+
trt.MatrixOperation.NONE,
120+
w_const,
121+
trt.MatrixOperation.NONE,
122+
).get_output(0)
123+
124+
b = np.ascontiguousarray(bias.reshape(1, out_features), dtype=dtype)
125+
b_const = network.add_constant((1, out_features), trt.Weights(b)).get_output(0)
126+
return network.add_elementwise(mm, b_const, trt.ElementWiseOperation.SUM).get_output(0)
127+
128+
129+
def add_hard_sigmoid(network, x, *, dtype: np.dtype):
130+
"""hard-sigmoid: clamp(x / 6 + 0.5, 0, 1), the timm/PyTorch definition."""
131+
scaled = network.add_scale(
132+
x,
133+
trt.ScaleMode.UNIFORM,
134+
shift=trt.Weights(np.array([0.5], dtype=dtype)),
135+
scale=trt.Weights(np.array([1.0 / 6.0], dtype=dtype)),
136+
).get_output(0)
137+
clipped = network.add_activation(scaled, trt.ActivationType.CLIP)
138+
clipped.alpha = 0.0
139+
clipped.beta = 1.0
140+
return clipped.get_output(0)
141+
142+
143+
def add_hard_swish(network, x, *, dtype: np.dtype):
144+
"""hard-swish: x * hard_sigmoid(x)."""
145+
gate = add_hard_sigmoid(network, x, dtype=dtype)
146+
return network.add_elementwise(x, gate, trt.ElementWiseOperation.PROD).get_output(0)
147+
148+
149+
def add_squeeze_excite(
150+
network,
151+
x,
152+
spatial: tuple[int, int],
153+
reduce_w: np.ndarray,
154+
reduce_b: np.ndarray,
155+
expand_w: np.ndarray,
156+
expand_b: np.ndarray,
157+
*,
158+
dtype: np.dtype,
159+
):
160+
"""Squeeze-and-excite gate.
161+
162+
Mean over the spatial dims, a 1x1 reduce convolution with ReLU, a 1x1
163+
expand convolution, then a hard-sigmoid gate multiplied back into x.
164+
timm applies the gate to the full-resolution tensor, so the pooled branch
165+
broadcasts over height and width.
166+
"""
167+
pooled = network.add_pooling_nd(x, trt.PoolingType.AVERAGE, spatial)
168+
pooled.stride_nd = (1, 1)
169+
squeezed = pooled.get_output(0)
170+
171+
reduced = add_conv2d(
172+
network, squeezed, reduce_w, reduce_b, int(reduce_w.shape[0]), (1, 1), dtype=dtype
173+
)
174+
reduced = add_relu(network, reduced)
175+
expanded = add_conv2d(
176+
network, reduced, expand_w, expand_b, int(expand_w.shape[0]), (1, 1), dtype=dtype
177+
)
178+
gate = add_hard_sigmoid(network, expanded, dtype=dtype)
179+
return network.add_elementwise(x, gate, trt.ElementWiseOperation.PROD).get_output(0)

0 commit comments

Comments
 (0)