correct
10_ResNet101.py
started
2026-05-05T02:26:42Z
timingkernelbench.timing
kernel mean / median / std
8.60 / — / 0.42 μs
ref mean / median / std
9.51 / — / 1.14 μs
numerical precisioneval_kernel_against_ref
gpu memoryextended_metrics.measure_memory
kernel launches / fusiontorch.profiler
fusion ratio (ref / kernel)
1.06x
energyNVML (pynvml)
ratio (ref / kernel)
1.40x
SOL (speed-of-light)heuristic_from_speedup
roofline / occupancyheuristic
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