gpt-5.5-priority · original · l123_single_gpt
correct 10_ResNet101.py
model
gpt-5.5-priority
turns / max
1 / 1
tool calls
1
tokens (total)
0
wall clock
—
truncated
no
backend
cuda fp32
variant
original
runtime
8.60 μs
ref runtime
9.51 μs
speedup
1.11x
started
2026-05-05T02:26:42Z
timingkernelbench.timing
kernel runtime
8.60 μs
ref runtime
9.51 μs
speedup
1.11x
kernel mean / median / std
8.60 / — / 0.42 μs
ref mean / median / std
9.51 / — / 1.14 μs
numerical precisioneval_kernel_against_ref
max abs error
5.96e-07
mean abs error
8.77e-08
max rel error
0.00
mean rel error
1.16e-06
gpu memoryextended_metrics.measure_memory
kernel peak
486.73 MB
ref peak
486.73 MB
kernel / ref
1.00x
kernel launches / fusiontorch.profiler
kernel launches
1699
ref launches
1804
fusion ratio (ref / kernel)
1.06x
total cuda time
73971 μs
energyNVML (pynvml)
kernel mJ / run
1209.18
ref mJ / run
1692.78
ratio (ref / kernel)
1.40x
avg power
164.48 W
ref avg power
214.83 W
SOL (speed-of-light)heuristic_from_speedup
SOL score (0–1)
0.111
dram util %
—
compute util %
—
bottleneck
unknown
dominant pipe
—
arithmetic intensity
—
ridge point
—
roofline / occupancyheuristic
dram bandwidth
—
dram util %
—
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
—
bottleneck
—
dominant pipe %
—
arithmetic intensity
—
ridge point
—
L1 hit %
—
L2 hit %
—
ld sectors / req
—
st sectors / req
—
regs / thread
—
smem / block
—
block size
—
peak bw
—
peak fp32 tflops
—
peak fp16 tflops
—
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn
import torch.nn.functional as F

class Bottleneck(nn.Module):
    expansion = 4

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        """
        :param in_channels: Number of input channels
        :param out_channels: Number of output channels
        :param stride: Stride for the first convolutional layer
        :param downsample: Downsample layer for the shortcut connection
        """
        super(Bottleneck, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        """
        :param x: Input tensor, shape (batch_size, in_channels, height, width)
        :return: Output tensor, shape (batch_size, out_channels * expansion, height, width)
        """
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        out += identity
        out = self.relu(out)

        return out

class Model(nn.Module):
    def __init__(self, layers, num_classes=1000):
        """
        :param block: Type of block to use (BasicBlock or Bottleneck)
        :param layers: List of integers specifying the number of blocks in each layer
        :param num_classes: Number of output classes
        """
        super(Model, self).__init__()
        self.in_channels = 64

        self.conv1 = nn.Conv2d(3, self.in_channels, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channels)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        block = Bottleneck

        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)

    def _make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        if stride != 1 or self.in_channels != out_channels * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * block.expansion),
            )

        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        self.in_channels = out_channels * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.in_channels, out_channels))

        return nn.Sequential(*layers)

    def forward(self, x):
        """
        :param x: Input tensor, shape (batch_size, 3, height, width)
        :return: Output tensor, shape (batch_size, num_classes)
        """
        x = self.conv1(x)
        x = self.bn1(x)
        x = self.relu(x)
        x = self.maxpool(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = self.avgpool(x)
        x = torch.flatten(x, 1)
        x = self.fc(x)

        return x

# Test code
batch_size = 10
height = 224
width = 224
layers = [3, 4, 23, 3]
num_classes = 1000

def get_inputs():
    return [torch.rand(batch_size, 3, height, width)]

def get_init_inputs():
    return [layers, num_classes]
turn 0 · FINAL · 1 tool call(s) · 84.7s turn · 84.7s LLM · 11,992 chars (click to expand)
turn 084.7s turn · 84.7s LLM · 0.0s tools · FINAL
reasoning
**Evaluating optimization strategies** I’m considering that fusing the direct sum and weight per spatial does many more operations, which isn't ideal. Maybe using a custom average pooling would be better? I know PyTorch's adaptive average pooling is fast, though there could be some overhead. Full connectivity with GEMM is quick as well. I’m wondering if I can optimize the final layer with a custom average pooling or a fully connected layer, but I need to be exact with the data. **Evaluating tensor storage** I'm looking into whether stored `channels_last` tensors are contiguous despite the `is_contiguous` flag being false. It seems like adding elementwise linear storage could work if they have the same physical layout. I'm considering if both the identity and output tensors maintain the same shape—likely they do with `channels_last`. I also need to keep in mind that for the first block of a layer, the downsample identity output should match in size with `channels_last`. **Looking at tensor operations** I need to include "torch/extension.h" in my code to work with tensors. Also, I should declare the function `torch::Tensor add_relu_inplace(torch::Tensor a, torch::Tensor b);`. It seems that the definition of this function will be found in the `cuda_sources`. I want to make sure I have everything set up correctly to avoid any issues when running the code. Let's make sure to clearly define my function to avoid confusion later! **Considering implementation options** I'm evaluating if I can skip using a scale of 0.999995 in my calculations, but there might be differences that accumulate. I should implement matching as a next step. I'm thinking about whether to include channels_last, but that might impact correctness. If speed is the goal, then failure in accuracy is not acceptable. I need to make a clear choice, and it might be safer to avoid channels_last this time. **Evaluating final shape and performance** I'm considering whether the final height and width won't be 7. If that's the case, I'll have one thread per N*C, but if H*W is large, that could be an issue. I need to list out functions in threes and check if there's any risk that the custom average differs by more than 1e-4. The inertia seems manageable, and after final checks, I note that I can use torch linear for the final fully connected layer. Now, it’s time to submit everything!
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n_cpp_src = r'''\n#include <torch/extension.h>\n\ntorch::Tensor add_relu_inplace(torch::Tensor a, torch::Tensor b);\ntorch::Tensor relu_maxpool2d_3x3s2p1(torch::Tensor x);\ntorch::Tensor global_avgpool_flat(torch::Tensor x);\n'''\n\n_cuda_src = r'''\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <cfloat>\n\n__global__ void add_relu_kernel(float* __restrict__ a, const float* __restrict__ b, long long n) {\n long long idx = (long long)blockIdx.x * blockDim.x + threadIdx.x;\n long long stride = (long long)blockDim.x * gridDim.x;\n for (long long i = idx; i < n; i += stride) {\n float v = a[i] + b[i];\n a[i] = v > 0.0f ? v : 0.0f;\n }\n}\n\ntorch::Tensor add_relu_inplace(torch::Tensor a, torch::Tensor b) {\n TORCH_CHECK(a.is_cuda() && b.is_cuda(), \"add_relu_inplace: CUDA tensors expected\");\n TORCH_CHECK(a.scalar_type() == torch::kFloat && b.scalar_type() == torch::kFloat, \"add_relu_inplace: float32 only\");\n TORCH_CHECK(a.numel() == b.numel(), \"add_relu_inplace: size mismatch\");\n const long long n = a.numel();\n if (n == 0) return a;\n const int threads = 256;\n int blocks = (int)((n + threads - 1) / threads);\n if (blocks > 65535) blocks = 65535;\n add_relu_kernel<<<blocks, threads>>>(a.data_ptr<float>(), b.data_ptr<float>(), n);\n return a;\n}\n\n__global__ void relu_maxpool_kernel(const float* __restrict__ x, float* __restrict__ y,\n int N, int C, int H, int W, int OH, int OW) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = N * C * OH * OW;\n int stride = blockDim.x * gridDim.x;\n for (int t = idx; t < total; t += stride) {\n int ow = t % OW;\n int q = t / OW;\n int oh = q % OH;\n q /= OH;\n int c = q % C;\n int n = q / C;\n int hs = oh * 2 - 1;\n int ws = ow * 2 - 1;\n float m = -FLT_MAX;\n #pragma unroll\n for (int kh = 0; kh < 3; ++kh) {\n int ih = hs + kh;\n if ((unsigned)ih < (unsigned)H) {\n #pragma unroll\n for (int kw = 0; kw < 3; ++kw) {\n int iw = ws + kw;\n if ((unsigned)iw < (unsigned)W) {\n float v = x[((n * C + c) * H + ih) * W + iw];\n m = v > m ? v : m;\n }\n }\n }\n }\n y[t] = m > 0.0f ? m : 0.0f;\n }\n}\n\ntorch::Tensor relu_maxpool2d_3x3s2p1(torch::Tensor x) {\n TORCH_CHECK(x.is_cuda(), \"relu_maxpool2d_3x3s2p1: CUDA tensor expected\");\n TORCH_CHECK(x.scalar_type() == torch::kFloat, \"relu_maxpool2d_3x3s2p1: float32 only\");\n TORCH_CHECK(x.dim() == 4, \"relu_maxpool2d_3x3s2p1: 4D tensor expected\");\n x = x.contiguous();\n int N = (int)x.size(0), C = (int)x.size(1), H = (int)x.size(2), W = (int)x.size(3);\n int OH = (H + 1) / 2;\n int OW = (W + 1) / 2;\n auto y = torch::empty({N, C, OH, OW}, x.options());\n int total = N * C * OH * OW;\n if (total == 0) return y;\n const int threads = 256;\n int blocks = (total + threads - 1) / threads;\n if (blocks > 65535) blocks = 65535;\n relu_maxpool_kernel<<<blocks, threads>>>(x.data_ptr<float>(), y.data_ptr<float>(), N, C, H, W, OH, OW);\n return y;\n}\n\n__global__ void global_avgpool_kernel(const float* __restrict__ x, float* __restrict__ y,\n int N, int C, int H, int W) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = N * C;\n int HW = H * W;\n int stride = blockDim.x * gridDim.x;\n for (int t = idx; t < total; t += stride) {\n int c = t % C;\n int n = t / C;\n const float* base = x + ((n * C + c) * H * W);\n float s = 0.0f;\n for (int i = 0; i < HW; ++i) s += base[i];\n y[t] = s / (float)HW;\n }\n}\n\ntorch::Tensor global_avgpool_flat(torch::Tensor x) {\n TORCH_CHECK(x.is_cuda(), \"global_avgpool_flat: CUDA tensor expected\");\n TORCH_CHECK(x.scalar_type() == torch::kFloat, \"global_avgpool_flat: float32 only\");\n TORCH_CHECK(x.dim() == 4, \"global_avgpool_flat: 4D tensor expected\");\n x = x.contiguous();\n int N = (int)x.size(0), C = (int)x.size(1), H = (int)x.size(2), W = (int)x.size(3);\n auto y = torch::empty({N, C}, x.options());\n int total = N * C;\n if (total == 0) return y;\n const int threads = 256;\n int blocks = (total + threads - 1) / threads;\n if (blocks > 65535) blocks = 65535;\n global_avgpool_kernel<<<blocks, threads>>>(x.data_ptr<float>(), y.data_ptr<float>(), N, C, H, W);\n return y;\n}\n'''\n\n_ext = load_inline(\n name='resnet_bottleneck_fused_kernels_v1',\n cpp_sources=_cpp_src,\n cuda_sources=_cuda_src,\n functions=['add_relu_inplace', 'relu_maxpool2d_3x3s2p1', 'global_avgpool_flat'],\n extra_cflags=['-O3'],\n extra_cuda_cflags=['-O3'],\n verbose=False,\n)\n\n\nclass _BottleneckNew(nn.Module):\n expansion = 4\n\n def __init__(self, in_channels, out_channels, stride=1, downsample=None):\n super(_BottleneckNew, self).__init__()\n self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)\n self.bn1 = nn.BatchNorm2d(out_channels)\n self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)\n self.bn2 = nn.BatchNorm2d(out_channels)\n self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, bias=False)\n self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)\n self.relu = nn.ReLU(inplace=True)\n self.downsample = downsample\n self.stride = stride\n\n def forward(self, x):\n identity = x\n\n out = self.conv1(x)\n out = self.bn1(out)\n out = self.relu(out)\n\n out = self.conv2(out)\n out = self.bn2(out)\n out = self.relu(out)\n\n out = self.conv3(out)\n out = self.bn3(out)\n\n if self.downsample is not None:\n identity = self.downsample(x)\n\n return _ext.add_relu_inplace(out, identity)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, layers, num_classes=1000):\n super(ModelNew, self).__init__()\n self.in_channels = 64\n\n self.conv1 = nn.Conv2d(3, self.in_channels, kernel_size=7, stride=2, padding=3, bias=False)\n self.bn1 = nn.BatchNorm2d(self.in_channels)\n self.relu = nn.ReLU(inplace=True)\n self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n\n block = _BottleneckNew\n self.layer1 = self._make_layer(block, 64, layers[0])\n self.layer2 = self._make_layer(block, 128, layers[1], stride=2)\n self.layer3 = self._make_layer(block, 256, layers[2], stride=2)\n self.layer4 = self._make_layer(block, 512, layers[3], stride=2)\n\n self.avgpool = nn.AdaptiveAvgPool2d((1, 1))\n self.fc = nn.Linear(512 * block.expansion, num_classes)\n\n def _make_layer(self, block, out_channels, blocks, stride=1):\n downsample = None\n if stride != 1 or self.in_channels != out_channels * block.expansion:\n downsample = nn.Sequential(\n nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False),\n nn.BatchNorm2d(out_channels * block.expansion),\n )\n\n layers = []\n layers.append(block(self.in_channels, out_channels, stride, downsample))\n self.in_channels = out_channels * block.expansion\n for _ in range(1, blocks):\n layers.append(block(self.in_channels, out_channels))\n\n return nn.Sequential(*layers)\n\n def forward(self, x):\n x = self.conv1(x)\n x = self.bn1(x)\n x = _ext.relu_maxpool2d_3x3s2p1(x)\n\n x = self.layer1(x)\n x = self.layer2(x)\n x = self.layer3(x)\n x = self.layer4(x)\n\n x = _ext.global_avgpool_flat(x)\n x = self.fc(x)\n return x\n" }
result ← submit_kernel (ok)
oksubmit_kernel submit_kernel PASSED: (5 / 5) correctness trials all passed. Kernel runtime: 8.60 μs Runtime stats: mean=8.60μs median=?μs std=0.42μs Numerical precision: max_abs_err=5.96e-07 mean_abs_err=8.77e-08 max_rel_err=2.23e-03 Memory: 486.73 MB peak (ref: 486.73 MB, ratio: 1.0x) Kernel launches: 1699 (ref: 1804, fusion ratio: 1.0618) Energy: 1209.18 mJ/run (ref: 1692.78 mJ, ratio: 1.3999x) SOL score: 0.1106
final submitted kernel (218 lines, 7,936 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

_cpp_src = r'''
#include <torch/extension.h>

torch::Tensor add_relu_inplace(torch::Tensor a, torch::Tensor b);
torch::Tensor relu_maxpool2d_3x3s2p1(torch::Tensor x);
torch::Tensor global_avgpool_flat(torch::Tensor x);
'''

_cuda_src = r'''
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <cfloat>

__global__ void add_relu_kernel(float* __restrict__ a, const float* __restrict__ b, long long n) {
    long long idx = (long long)blockIdx.x * blockDim.x + threadIdx.x;
    long long stride = (long long)blockDim.x * gridDim.x;
    for (long long i = idx; i < n; i += stride) {
        float v = a[i] + b[i];
        a[i] = v > 0.0f ? v : 0.0f;
    }
}

torch::Tensor add_relu_inplace(torch::Tensor a, torch::Tensor b) {
    TORCH_CHECK(a.is_cuda() && b.is_cuda(), "add_relu_inplace: CUDA tensors expected");
    TORCH_CHECK(a.scalar_type() == torch::kFloat && b.scalar_type() == torch::kFloat, "add_relu_inplace: float32 only");
    TORCH_CHECK(a.numel() == b.numel(), "add_relu_inplace: size mismatch");
    const long long n = a.numel();
    if (n == 0) return a;
    const int threads = 256;
    int blocks = (int)((n + threads - 1) / threads);
    if (blocks > 65535) blocks = 65535;
    add_relu_kernel<<<blocks, threads>>>(a.data_ptr<float>(), b.data_ptr<float>(), n);
    return a;
}

__global__ void relu_maxpool_kernel(const float* __restrict__ x, float* __restrict__ y,
                                    int N, int C, int H, int W, int OH, int OW) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int total = N * C * OH * OW;
    int stride = blockDim.x * gridDim.x;
    for (int t = idx; t < total; t += stride) {
        int ow = t % OW;
        int q = t / OW;
        int oh = q % OH;
        q /= OH;
        int c = q % C;
        int n = q / C;
        int hs = oh * 2 - 1;
        int ws = ow * 2 - 1;
        float m = -FLT_MAX;
        #pragma unroll
        for (int kh = 0; kh < 3; ++kh) {
            int ih = hs + kh;
            if ((unsigned)ih < (unsigned)H) {
                #pragma unroll
                for (int kw = 0; kw < 3; ++kw) {
                    int iw = ws + kw;
                    if ((unsigned)iw < (unsigned)W) {
                        float v = x[((n * C + c) * H + ih) * W + iw];
                        m = v > m ? v : m;
                    }
                }
            }
        }
        y[t] = m > 0.0f ? m : 0.0f;
    }
}

torch::Tensor relu_maxpool2d_3x3s2p1(torch::Tensor x) {
    TORCH_CHECK(x.is_cuda(), "relu_maxpool2d_3x3s2p1: CUDA tensor expected");
    TORCH_CHECK(x.scalar_type() == torch::kFloat, "relu_maxpool2d_3x3s2p1: float32 only");
    TORCH_CHECK(x.dim() == 4, "relu_maxpool2d_3x3s2p1: 4D tensor expected");
    x = x.contiguous();
    int N = (int)x.size(0), C = (int)x.size(1), H = (int)x.size(2), W = (int)x.size(3);
    int OH = (H + 1) / 2;
    int OW = (W + 1) / 2;
    auto y = torch::empty({N, C, OH, OW}, x.options());
    int total = N * C * OH * OW;
    if (total == 0) return y;
    const int threads = 256;
    int blocks = (total + threads - 1) / threads;
    if (blocks > 65535) blocks = 65535;
    relu_maxpool_kernel<<<blocks, threads>>>(x.data_ptr<float>(), y.data_ptr<float>(), N, C, H, W, OH, OW);
    return y;
}

__global__ void global_avgpool_kernel(const float* __restrict__ x, float* __restrict__ y,
                                      int N, int C, int H, int W) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int total = N * C;
    int HW = H * W;
    int stride = blockDim.x * gridDim.x;
    for (int t = idx; t < total; t += stride) {
        int c = t % C;
        int n = t / C;
        const float* base = x + ((n * C + c) * H * W);
        float s = 0.0f;
        for (int i = 0; i < HW; ++i) s += base[i];
        y[t] = s / (float)HW;
    }
}

torch::Tensor global_avgpool_flat(torch::Tensor x) {
    TORCH_CHECK(x.is_cuda(), "global_avgpool_flat: CUDA tensor expected");
    TORCH_CHECK(x.scalar_type() == torch::kFloat, "global_avgpool_flat: float32 only");
    TORCH_CHECK(x.dim() == 4, "global_avgpool_flat: 4D tensor expected");
    x = x.contiguous();
    int N = (int)x.size(0), C = (int)x.size(1), H = (int)x.size(2), W = (int)x.size(3);
    auto y = torch::empty({N, C}, x.options());
    int total = N * C;
    if (total == 0) return y;
    const int threads = 256;
    int blocks = (total + threads - 1) / threads;
    if (blocks > 65535) blocks = 65535;
    global_avgpool_kernel<<<blocks, threads>>>(x.data_ptr<float>(), y.data_ptr<float>(), N, C, H, W);
    return y;
}
'''

_ext = load_inline(
    name='resnet_bottleneck_fused_kernels_v1',
    cpp_sources=_cpp_src,
    cuda_sources=_cuda_src,
    functions=['add_relu_inplace', 'relu_maxpool2d_3x3s2p1', 'global_avgpool_flat'],
    extra_cflags=['-O3'],
    extra_cuda_cflags=['-O3'],
    verbose=False,
)


class _BottleneckNew(nn.Module):
    expansion = 4

    def __init__(self, in_channels, out_channels, stride=1, downsample=None):
        super(_BottleneckNew, self).__init__()
        self.conv1 = nn.Conv2d(in_channels, out_channels, kernel_size=1, bias=False)
        self.bn1 = nn.BatchNorm2d(out_channels)
        self.conv2 = nn.Conv2d(out_channels, out_channels, kernel_size=3, stride=stride, padding=1, bias=False)
        self.bn2 = nn.BatchNorm2d(out_channels)
        self.conv3 = nn.Conv2d(out_channels, out_channels * self.expansion, kernel_size=1, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels * self.expansion)
        self.relu = nn.ReLU(inplace=True)
        self.downsample = downsample
        self.stride = stride

    def forward(self, x):
        identity = x

        out = self.conv1(x)
        out = self.bn1(out)
        out = self.relu(out)

        out = self.conv2(out)
        out = self.bn2(out)
        out = self.relu(out)

        out = self.conv3(out)
        out = self.bn3(out)

        if self.downsample is not None:
            identity = self.downsample(x)

        return _ext.add_relu_inplace(out, identity)


class ModelNew(nn.Module):
    def __init__(self, layers, num_classes=1000):
        super(ModelNew, self).__init__()
        self.in_channels = 64

        self.conv1 = nn.Conv2d(3, self.in_channels, kernel_size=7, stride=2, padding=3, bias=False)
        self.bn1 = nn.BatchNorm2d(self.in_channels)
        self.relu = nn.ReLU(inplace=True)
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)

        block = _BottleneckNew
        self.layer1 = self._make_layer(block, 64, layers[0])
        self.layer2 = self._make_layer(block, 128, layers[1], stride=2)
        self.layer3 = self._make_layer(block, 256, layers[2], stride=2)
        self.layer4 = self._make_layer(block, 512, layers[3], stride=2)

        self.avgpool = nn.AdaptiveAvgPool2d((1, 1))
        self.fc = nn.Linear(512 * block.expansion, num_classes)

    def _make_layer(self, block, out_channels, blocks, stride=1):
        downsample = None
        if stride != 1 or self.in_channels != out_channels * block.expansion:
            downsample = nn.Sequential(
                nn.Conv2d(self.in_channels, out_channels * block.expansion, kernel_size=1, stride=stride, bias=False),
                nn.BatchNorm2d(out_channels * block.expansion),
            )

        layers = []
        layers.append(block(self.in_channels, out_channels, stride, downsample))
        self.in_channels = out_channels * block.expansion
        for _ in range(1, blocks):
            layers.append(block(self.in_channels, out_channels))

        return nn.Sequential(*layers)

    def forward(self, x):
        x = self.conv1(x)
        x = self.bn1(x)
        x = _ext.relu_maxpool2d_3x3s2p1(x)

        x = self.layer1(x)
        x = self.layer2(x)
        x = self.layer3(x)
        x = self.layer4(x)

        x = _ext.global_avgpool_flat(x)
        x = self.fc(x)
        return x