Skip to content

Commit c69ce50

Browse files
authored
feat(timm_res2net): add timm Res2Net image-classification family (1231)
## Implementation Adds `families/timm_res2net/`. Res2Net replaces the single 3x3 of a ResNet bottleneck with a chain of narrower 3x3 convolutions. The 1x1 output is split into `scale` equal chunks; each chunk after the first is added to the running result before its own convolution, and the last chunk bypasses the chain. The chunks are concatenated before the final 1x1, so one block sees several receptive field sizes at once. At the head of a stage every rung starts from its own chunk, because the spatial size has just changed. The layout is recovered from the checkpoint rather than tabulated: | Property | Recovered from | | --- | --- | | Stage depths | the `layer<stage>.<block>` keys | | Scale | how many `convs` entries a block has, plus one for the chunk that skips | | Chunk width and groups | those convolutions' own weight shapes | | Stem shape | whether `conv1` is one tensor or a sequence | | Shortcut form | whether `downsample` starts at index 0 or 1 | ### fp32 builds now switch off TF32 TensorRT runs fp32 convolutions in TF32 by default, keeping ten mantissa bits instead of twenty-four. Measured against timm across all nine checkpoints, that costs roughly three orders of magnitude of accuracy, and on `res2net50_26w_8s` it changes the predicted class: | Checkpoint | TF32 on | TF32 off | | --- | --- | --- | | res2net50_26w_8s | corr 0.99931, top-1 **446 vs 490** | corr 1.00000000, top-1 match |
1 parent 1f7fc41 commit c69ce50

21 files changed

Lines changed: 1657 additions & 0 deletions

apps/benchmark/performance/release.yaml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ excluded_profiles:
4848
Functional and timm reference-parity qualification is present for every
4949
published DPN width, but the release-performance workload and receipt
5050
were collected only for dpn68b.
51+
- model: res2net50d-in1k
52+
reason: &res2net_performance_exclusion >-
53+
Functional and timm reference-parity qualification is present for every
54+
published Res2Net checkpoint, but the release-performance workload and
55+
receipt were collected only for res2net50_26w_4s.
56+
- model: res2next50-in1k
57+
reason: *res2net_performance_exclusion
5158

5259
entries:
5360
- id: albert.encode
@@ -1157,6 +1164,19 @@ entries:
11571164
reference_backend: hf_transformers
11581165
timing_scope: task-model-call-wall
11591166
input_preparation_included: false
1167+
- id: timm_res2net.classify
1168+
family: timm_res2net
1169+
operation: classify
1170+
model: res2net50-26w-4s-in1k
1171+
workload:
1172+
testcase: res2net50-26w-4s-in1k
1173+
baseline:
1174+
runner: task-reference
1175+
adapter: hf-transformers-vision
1176+
mode: hf-eager
1177+
reference_backend: hf_transformers
1178+
timing_scope: task-model-call-wall
1179+
input_preparation_included: false
11601180
- id: timm_repvgg.classify
11611181
family: timm_repvgg
11621182
operation: classify

families/timm_res2net/__init__.py

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 Res2Net image-classification family."""
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Exact NumPy safetensors access for timm Res2Net checkpoints."""
5+
6+
from __future__ import annotations
7+
8+
import json
9+
from dataclasses import dataclass
10+
from pathlib import Path, PurePosixPath
11+
from typing import Any
12+
13+
import numpy as np
14+
from safetensors import safe_open
15+
16+
17+
@dataclass(frozen=True)
18+
class Checkpoint:
19+
readers: tuple[Any, ...]
20+
tensor_map: dict[str, Any]
21+
22+
@classmethod
23+
def open(cls, model_dir: Path) -> "Checkpoint":
24+
single = model_dir / "model.safetensors"
25+
if single.is_file():
26+
reader = safe_open(str(single), framework="numpy")
27+
return cls((reader,), {str(name): reader for name in reader.keys()})
28+
29+
index_path = model_dir / "model.safetensors.index.json"
30+
if not index_path.is_file():
31+
raise FileNotFoundError(f"Res2Net checkpoint has no model safetensors: {model_dir}")
32+
payload = json.loads(index_path.read_text(encoding="utf-8"))
33+
weight_map = payload.get("weight_map") if isinstance(payload, dict) else None
34+
if not isinstance(weight_map, dict) or not weight_map:
35+
raise ValueError("Res2Net safetensors index has no weight_map")
36+
if not all(
37+
isinstance(name, str) and name and isinstance(shard, str) and shard
38+
for name, shard in weight_map.items()
39+
):
40+
raise ValueError("Res2Net safetensors index contains invalid entries")
41+
shard_names = sorted(set(weight_map.values()))
42+
for name in shard_names:
43+
path = PurePosixPath(name)
44+
if path.is_absolute() or len(path.parts) != 1 or path.name != name:
45+
raise ValueError("Res2Net safetensors shard names must be direct relative files")
46+
readers = {
47+
name: safe_open(str(model_dir / name), framework="numpy") for name in shard_names
48+
}
49+
return cls(
50+
tuple(readers[name] for name in shard_names),
51+
{str(tensor): readers[str(shard)] for tensor, shard in weight_map.items()},
52+
)
53+
54+
@property
55+
def names(self) -> frozenset[str]:
56+
return frozenset(self.tensor_map)
57+
58+
def tensor(self, name: str) -> np.ndarray:
59+
reader = self.tensor_map.get(name)
60+
if reader is None:
61+
raise KeyError(f"Res2Net checkpoint tensor not found: {name}")
62+
return np.asarray(reader.get_tensor(name), dtype=np.float32)

families/timm_res2net/graph.py

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Small TensorRT graph vocabulary owned by timm Res2Net."""
5+
6+
from __future__ import annotations
7+
8+
import numpy as np
9+
import tensorrt as trt
10+
11+
12+
def convolution(
13+
network,
14+
tensor,
15+
weight: np.ndarray,
16+
bias: np.ndarray,
17+
*,
18+
stride: int = 1,
19+
padding: int = 0,
20+
groups: int = 1,
21+
dtype: np.dtype,
22+
):
23+
layer = network.add_convolution_nd(
24+
tensor,
25+
num_output_maps=int(weight.shape[0]),
26+
kernel_shape=(int(weight.shape[2]), int(weight.shape[3])),
27+
kernel=trt.Weights(np.ascontiguousarray(weight, dtype=dtype)),
28+
bias=trt.Weights(np.ascontiguousarray(bias, dtype=dtype)),
29+
)
30+
if layer is None:
31+
raise RuntimeError("TensorRT rejected a Res2Net convolution")
32+
layer.stride_nd = (stride, stride)
33+
layer.padding_nd = (padding, padding)
34+
layer.num_groups = groups
35+
return layer.get_output(0)
36+
37+
38+
def relu(network, tensor):
39+
layer = network.add_activation(tensor, trt.ActivationType.RELU)
40+
if layer is None:
41+
raise RuntimeError("TensorRT rejected a Res2Net ReLU")
42+
return layer.get_output(0)
43+
44+
45+
def add(network, left, right):
46+
layer = network.add_elementwise(left, right, trt.ElementWiseOperation.SUM)
47+
if layer is None:
48+
raise RuntimeError("TensorRT rejected a Res2Net residual add")
49+
return layer.get_output(0)
50+
51+
52+
def max_pool(network, tensor, *, kernel: int, stride: int, padding: int):
53+
layer = network.add_pooling_nd(tensor, trt.PoolingType.MAX, (kernel, kernel))
54+
if layer is None:
55+
raise RuntimeError("TensorRT rejected the Res2Net stem pooling")
56+
layer.stride_nd = (stride, stride)
57+
layer.padding_nd = (padding, padding)
58+
return layer.get_output(0)
59+
60+
61+
def global_average_pool(network, tensor, height: int, width: int):
62+
layer = network.add_pooling_nd(tensor, trt.PoolingType.AVERAGE, (height, width))
63+
if layer is None:
64+
raise RuntimeError("TensorRT rejected Res2Net global average pooling")
65+
layer.stride_nd = (1, 1)
66+
return layer.get_output(0)
67+
68+
69+
def classifier(
70+
network,
71+
tensor,
72+
weight: np.ndarray,
73+
bias: np.ndarray,
74+
*,
75+
dtype: np.dtype,
76+
):
77+
flattened = network.add_shuffle(tensor)
78+
if flattened is None:
79+
raise RuntimeError("TensorRT rejected the Res2Net classifier reshape")
80+
flattened.reshape_dims = (1, int(weight.shape[1]))
81+
matrix = np.ascontiguousarray(weight.T, dtype=dtype)
82+
matrix_layer = network.add_constant(matrix.shape, trt.Weights(matrix))
83+
if matrix_layer is None:
84+
raise RuntimeError("TensorRT rejected the Res2Net classifier weights")
85+
product = network.add_matrix_multiply(
86+
flattened.get_output(0),
87+
trt.MatrixOperation.NONE,
88+
matrix_layer.get_output(0),
89+
trt.MatrixOperation.NONE,
90+
)
91+
if product is None:
92+
raise RuntimeError("TensorRT rejected the Res2Net classifier matmul")
93+
values = np.ascontiguousarray(bias.reshape(1, -1), dtype=dtype)
94+
bias_layer = network.add_constant(values.shape, trt.Weights(values))
95+
if bias_layer is None:
96+
raise RuntimeError("TensorRT rejected the Res2Net classifier bias")
97+
output = network.add_elementwise(
98+
product.get_output(0),
99+
bias_layer.get_output(0),
100+
trt.ElementWiseOperation.SUM,
101+
)
102+
if output is None:
103+
raise RuntimeError("TensorRT rejected the Res2Net classifier output")
104+
return output.get_output(0)
105+
106+
107+
def average_pool(
108+
network, tensor, *, kernel: int, stride: int, padding: int, count_include_pad: bool
109+
):
110+
"""Average pooling.
111+
112+
TensorRT excludes padded cells from the divisor by default and PyTorch
113+
includes them, so the caller states which convention the checkpoint was
114+
trained with rather than inheriting either default.
115+
"""
116+
layer = network.add_pooling_nd(tensor, trt.PoolingType.AVERAGE, (kernel, kernel))
117+
if layer is None:
118+
raise RuntimeError("TensorRT rejected a Res2Net average pooling")
119+
layer.stride_nd = (stride, stride)
120+
layer.padding_nd = (padding, padding)
121+
layer.average_count_excludes_padding = not count_include_pad
122+
return layer.get_output(0)
123+
124+
125+
def channel_slice(network, tensor, start: int, count: int):
126+
shape = [int(value) for value in tensor.shape]
127+
starts, sizes = [0] * len(shape), list(shape)
128+
starts[1], sizes[1] = start, count
129+
layer = network.add_slice(tensor, trt.Dims(starts), trt.Dims(sizes), trt.Dims([1] * len(shape)))
130+
if layer is None:
131+
raise RuntimeError("TensorRT rejected a Res2Net channel slice")
132+
return layer.get_output(0)
133+
134+
135+
def concatenate(network, tensors):
136+
layer = network.add_concatenation(list(tensors))
137+
if layer is None:
138+
raise RuntimeError("TensorRT rejected a Res2Net concatenation")
139+
layer.axis = 1
140+
return layer.get_output(0)

0 commit comments

Comments
 (0)