gpt-5.5-priority · original · l123_single_gpt
correct 26_ShuffleNet.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.38 μs
ref runtime
8.75 μs
speedup
1.04x
started
2026-05-05T02:37:54Z
timingkernelbench.timing
kernel runtime
8.38 μs
ref runtime
8.75 μs
speedup
1.04x
kernel mean / median / std
8.38 / — / 0.20 μs
ref mean / median / std
8.75 / — / 1.55 μs
numerical precisioneval_kernel_against_ref
max abs error
0.00
mean abs error
0.00
max rel error
0.00
mean rel error
0.00
gpu memoryextended_metrics.measure_memory
kernel peak
495.76 MB
ref peak
495.76 MB
kernel / ref
1.00x
kernel launches / fusiontorch.profiler
kernel launches
791
ref launches
882
fusion ratio (ref / kernel)
1.11x
total cuda time
36906 μs
energyNVML (pynvml)
kernel mJ / run
966.00
ref mJ / run
1207.96
ratio (ref / kernel)
1.25x
avg power
105.88 W
ref avg power
140.04 W
SOL (speed-of-light)heuristic_from_speedup
SOL score (0–1)
0.104
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 ShuffleNetUnit(nn.Module):
    def __init__(self, in_channels, out_channels, groups=3):
        """
        ShuffleNet unit implementation.

        :param in_channels: Number of input channels.
        :param out_channels: Number of output channels.
        :param groups: Number of groups for group convolution.
        """
        super(ShuffleNetUnit, self).__init__()
        
        # Ensure the output channels are divisible by groups
        assert out_channels % 4 == 0
        mid_channels = out_channels // 4
        
        # First 1x1 group convolution
        self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, padding=0, groups=groups, bias=False)
        self.bn1 = nn.BatchNorm2d(mid_channels)
        
        # Depthwise 3x3 convolution
        self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=1, padding=1, groups=mid_channels, bias=False)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        
        # Second 1x1 group convolution
        self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1, padding=0, groups=groups, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels)
        
        # Shuffle operation
        self.shuffle = ChannelShuffle(groups)
        
        # Shortcut connection if input and output channels are the same
        if in_channels == out_channels:
            self.shortcut = nn.Sequential()
        else:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(out_channels)
            )
    
    def forward(self, x):
        """
        Forward pass for ShuffleNet unit.

        :param x: Input tensor, shape (batch_size, in_channels, height, width)
        :return: Output tensor, shape (batch_size, out_channels, height, width)
        """
        out = F.relu(self.bn1(self.conv1(x)))
        out = self.bn2(self.conv2(out))
        out = self.shuffle(out)
        out = F.relu(self.bn3(self.conv3(out)))
        
        out += self.shortcut(x)
        return out

class ChannelShuffle(nn.Module):
    def __init__(self, groups):
        """
        Channel shuffle operation.

        :param groups: Number of groups for shuffling.
        """
        super(ChannelShuffle, self).__init__()
        self.groups = groups
    
    def forward(self, x):
        """
        Forward pass for channel shuffle.

        :param x: Input tensor, shape (batch_size, channels, height, width)
        :return: Output tensor, shape (batch_size, channels, height, width)
        """
        batch_size, channels, height, width = x.size()
        channels_per_group = channels // self.groups
        
        # Reshape
        x = x.view(batch_size, self.groups, channels_per_group, height, width)
        
        # Transpose
        x = x.transpose(1, 2).contiguous()
        
        # Flatten
        x = x.view(batch_size, -1, height, width)
        
        return x

class Model(nn.Module):
    def __init__(self, num_classes=1000, groups=3, stages_repeats=[3, 7, 3], stages_out_channels=[24, 240, 480, 960]):
        """
        ShuffleNet architecture.

        :param num_classes: Number of output classes.
        :param groups: Number of groups for group convolution.
        :param stages_repeats: List of ints specifying the number of repeats for each stage.
        :param stages_out_channels: List of ints specifying the output channels for each stage.
        """
        super(Model, self).__init__()
        
        self.conv1 = nn.Conv2d(3, stages_out_channels[0], kernel_size=3, stride=2, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(stages_out_channels[0])
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        
        self.stage2 = self._make_stage(stages_out_channels[0], stages_out_channels[1], stages_repeats[0], groups)
        self.stage3 = self._make_stage(stages_out_channels[1], stages_out_channels[2], stages_repeats[1], groups)
        self.stage4 = self._make_stage(stages_out_channels[2], stages_out_channels[3], stages_repeats[2], groups)
        
        self.conv5 = nn.Conv2d(stages_out_channels[3], 1024, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn5 = nn.BatchNorm2d(1024)
        
        self.fc = nn.Linear(1024, num_classes)
    
    def _make_stage(self, in_channels, out_channels, repeats, groups):
        """
        Helper function to create a stage of ShuffleNet units.

        :param in_channels: Number of input channels.
        :param out_channels: Number of output channels.
        :param repeats: Number of ShuffleNet units in the stage.
        :param groups: Number of groups for group convolution.
        :return: nn.Sequential containing the stage.
        """
        layers = []
        layers.append(ShuffleNetUnit(in_channels, out_channels, groups))
        for _ in range(1, repeats):
            layers.append(ShuffleNetUnit(out_channels, out_channels, groups))
        return nn.Sequential(*layers)
    
    def forward(self, x):
        """
        Forward pass for ShuffleNet.

        :param x: Input tensor, shape (batch_size, 3, height, width)
        :return: Output tensor, shape (batch_size, num_classes)
        """
        x = F.relu(self.bn1(self.conv1(x)))
        x = self.maxpool(x)
        
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        
        x = F.relu(self.bn5(self.conv5(x)))
        x = F.adaptive_avg_pool2d(x, (1, 1))
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        
        return x

# Test code
batch_size = 10
input_channels = 3
height = 224
width = 224
num_classes = 1000

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

def get_init_inputs():
    return [num_classes]
turn 0 · FINAL · 1 tool call(s) · 81.7s turn · 81.7s LLM · 12,622 chars (click to expand)
turn 081.7s turn · 81.7s LLM · 0.0s tools · FINAL
reasoning
**Implementing channel shuffle** I need to implement an extension with two functions. The first function, `channel_shuffle`, takes two inputs: `x` and `groups`. The input has a shape of N, C, H, W, and we calculate `cpg` as C divided by groups. For the output channel, I’m referencing the operation `x.view` to reshape it, then using `transpose` and `flatten`. I’m also trying to determine how to calculate the output channel, `c_out`, with the formula `cp*g + gidx`. Let's clarify those details! **Evaluating stage units and performance** I'm noticing a potential issue with the stage units. We define ChannelShuffleFast, but it seems the state_dict keys match, which is confusing. The reference has self.shuffle with no parameters, and I think I need to include the helper function _make_stage. I’m also considering whether I could reduce Python overhead by using manual forward loops. Additionally, there's a question of whether a custom shuffle kernel is slower due to launch overhead compared to transpose contiguous. **Evaluating instantiation methods** I'm considering whether I should instantiate separately for comparison. It seems like in tasks, there's usually a copying of reference weights to new instances. If I don't do this, I might not be able to match randomly due to the same seed. If I instantiate after referencing, the RNG gets consumed, making it different. I also wonder if I need to set maxpool to ceil? It might be a good idea to default to false. Maybe using nn.ReLU, or F.relu could work better. Time to submit!
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nfrom torch.utils.cpp_extension import load_inline\n\n_cpp_src = r'''\n#include <torch/extension.h>\n\ntorch::Tensor channel_shuffle(torch::Tensor x, int64_t groups);\ntorch::Tensor dw_bn_shuffle(torch::Tensor x, torch::Tensor weight, torch::Tensor gamma,\n torch::Tensor beta, torch::Tensor mean, torch::Tensor var,\n double eps, int64_t groups);\n'''\n\n_cuda_src = r'''\n#include <torch/extension.h>\n#include <ATen/cuda/CUDAContext.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n\n__global__ void channel_shuffle_kernel(const float* __restrict__ x,\n float* __restrict__ y,\n int total, int C, int HW, int groups) {\n int cpg = C / groups;\n int stride = blockDim.x * gridDim.x;\n for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < total; idx += stride) {\n int widx = idx % HW;\n int t = idx / HW;\n int co = t % C;\n int n = t / C;\n int cp = co / groups;\n int g = co - cp * groups;\n int ci = g * cpg + cp;\n y[(n * C + co) * HW + widx] = x[(n * C + ci) * HW + widx];\n }\n}\n\n__global__ void dw_bn_shuffle_kernel(const float* __restrict__ x,\n const float* __restrict__ w,\n const float* __restrict__ gamma,\n const float* __restrict__ beta,\n const float* __restrict__ mean,\n const float* __restrict__ var,\n float* __restrict__ y,\n int total, int N, int C, int H, int Wd,\n int groups, float eps) {\n int HW = H * Wd;\n int cpg = C / groups;\n int stride = blockDim.x * gridDim.x;\n for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < total; idx += stride) {\n int pix = idx % HW;\n int h = pix / Wd;\n int col = pix - h * Wd;\n int t = idx / HW;\n int co = t % C;\n int n = t / C;\n int cp = co / groups;\n int g = co - cp * groups;\n int ci = g * cpg + cp;\n\n const float* xbase = x + (n * C + ci) * HW;\n const float* wbase = w + ci * 9;\n float acc = 0.0f;\n\n int ih = h - 1;\n if (ih >= 0) {\n int row = ih * Wd;\n int iw = col - 1;\n if (iw >= 0) acc += xbase[row + iw] * wbase[0];\n acc += xbase[row + col] * wbase[1];\n iw = col + 1;\n if (iw < Wd) acc += xbase[row + iw] * wbase[2];\n }\n ih = h;\n {\n int row = ih * Wd;\n int iw = col - 1;\n if (iw >= 0) acc += xbase[row + iw] * wbase[3];\n acc += xbase[row + col] * wbase[4];\n iw = col + 1;\n if (iw < Wd) acc += xbase[row + iw] * wbase[5];\n }\n ih = h + 1;\n if (ih < H) {\n int row = ih * Wd;\n int iw = col - 1;\n if (iw >= 0) acc += xbase[row + iw] * wbase[6];\n acc += xbase[row + col] * wbase[7];\n iw = col + 1;\n if (iw < Wd) acc += xbase[row + iw] * wbase[8];\n }\n\n float out = (acc - mean[ci]) * (gamma[ci] * rsqrtf(var[ci] + eps)) + beta[ci];\n y[(n * C + co) * HW + pix] = out;\n }\n}\n\ntorch::Tensor channel_shuffle(torch::Tensor x, int64_t groups) {\n TORCH_CHECK(x.is_cuda(), \"x must be CUDA\");\n TORCH_CHECK(x.scalar_type() == torch::kFloat32, \"only float32 supported\");\n TORCH_CHECK(x.dim() == 4, \"x must be NCHW\");\n auto y = torch::empty_like(x);\n int N = (int)x.size(0);\n int C = (int)x.size(1);\n int H = (int)x.size(2);\n int Wd = (int)x.size(3);\n int total = N * C * H * Wd;\n int threads = 256;\n int blocks = (total + threads - 1) / threads;\n if (blocks > 4096) blocks = 4096;\n channel_shuffle_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(\n x.data_ptr<float>(), y.data_ptr<float>(), total, C, H * Wd, (int)groups);\n return y;\n}\n\ntorch::Tensor dw_bn_shuffle(torch::Tensor x, torch::Tensor weight, torch::Tensor gamma,\n torch::Tensor beta, torch::Tensor mean, torch::Tensor var,\n double eps, int64_t groups) {\n TORCH_CHECK(x.is_cuda(), \"x must be CUDA\");\n TORCH_CHECK(x.scalar_type() == torch::kFloat32, \"only float32 supported\");\n auto y = torch::empty_like(x);\n int N = (int)x.size(0);\n int C = (int)x.size(1);\n int H = (int)x.size(2);\n int Wd = (int)x.size(3);\n int total = N * C * H * Wd;\n int threads = 256;\n int blocks = (total + threads - 1) / threads;\n if (blocks > 4096) blocks = 4096;\n dw_bn_shuffle_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(\n x.data_ptr<float>(), weight.data_ptr<float>(), gamma.data_ptr<float>(), beta.data_ptr<float>(),\n mean.data_ptr<float>(), var.data_ptr<float>(), y.data_ptr<float>(), total, N, C, H, Wd,\n (int)groups, (float)eps);\n return y;\n}\n'''\n\n_ext = load_inline(\n name=\"shufflenet_fused_ext_v1\",\n cpp_sources=_cpp_src,\n cuda_sources=_cuda_src,\n functions=[\"channel_shuffle\", \"dw_bn_shuffle\"],\n extra_cflags=[\"-O3\"],\n extra_cuda_cflags=[\"-O3\"],\n verbose=False,\n)\n\n\nclass ChannelShuffle(nn.Module):\n def __init__(self, groups):\n super(ChannelShuffle, self).__init__()\n self.groups = groups\n\n def forward(self, x):\n if x.is_cuda and x.dtype == torch.float32 and x.is_contiguous():\n return _ext.channel_shuffle(x, self.groups)\n batch_size, channels, height, width = x.size()\n channels_per_group = channels // self.groups\n x = x.view(batch_size, self.groups, channels_per_group, height, width)\n x = x.transpose(1, 2).contiguous()\n x = x.view(batch_size, -1, height, width)\n return x\n\n\nclass ShuffleNetUnit(nn.Module):\n def __init__(self, in_channels, out_channels, groups=3):\n super(ShuffleNetUnit, self).__init__()\n assert out_channels % 4 == 0\n mid_channels = out_channels // 4\n self.groups = groups\n self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, padding=0, groups=groups, bias=False)\n self.bn1 = nn.BatchNorm2d(mid_channels)\n self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=1, padding=1, groups=mid_channels, bias=False)\n self.bn2 = nn.BatchNorm2d(mid_channels)\n self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1, padding=0, groups=groups, bias=False)\n self.bn3 = nn.BatchNorm2d(out_channels)\n self.shuffle = ChannelShuffle(groups)\n if in_channels == out_channels:\n self.shortcut = nn.Sequential()\n else:\n self.shortcut = nn.Sequential(\n nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),\n nn.BatchNorm2d(out_channels)\n )\n\n def forward(self, x):\n out = F.relu(self.bn1(self.conv1(x)))\n # In inference mode, fuse depthwise 3x3 + BatchNorm + channel shuffle into one CUDA kernel.\n # In training mode, keep PyTorch BatchNorm semantics exactly (batch statistics and running updates).\n if (not self.training) and out.is_cuda and out.dtype == torch.float32 and out.is_contiguous():\n out = _ext.dw_bn_shuffle(out, self.conv2.weight, self.bn2.weight, self.bn2.bias,\n self.bn2.running_mean, self.bn2.running_var, self.bn2.eps, self.groups)\n else:\n out = self.bn2(self.conv2(out))\n out = self.shuffle(out)\n out = F.relu(self.bn3(self.conv3(out)))\n out = out + self.shortcut(x)\n return out\n\n\nclass ModelNew(nn.Module):\n def __init__(self, num_classes=1000, groups=3, stages_repeats=[3, 7, 3], stages_out_channels=[24, 240, 480, 960]):\n super(ModelNew, self).__init__()\n self.conv1 = nn.Conv2d(3, stages_out_channels[0], kernel_size=3, stride=2, padding=1, bias=False)\n self.bn1 = nn.BatchNorm2d(stages_out_channels[0])\n self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)\n self.stage2 = self._make_stage(stages_out_channels[0], stages_out_channels[1], stages_repeats[0], groups)\n self.stage3 = self._make_stage(stages_out_channels[1], stages_out_channels[2], stages_repeats[1], groups)\n self.stage4 = self._make_stage(stages_out_channels[2], stages_out_channels[3], stages_repeats[2], groups)\n self.conv5 = nn.Conv2d(stages_out_channels[3], 1024, kernel_size=1, stride=1, padding=0, bias=False)\n self.bn5 = nn.BatchNorm2d(1024)\n self.fc = nn.Linear(1024, num_classes)\n\n def _make_stage(self, in_channels, out_channels, repeats, groups):\n layers = []\n layers.append(ShuffleNetUnit(in_channels, out_channels, groups))\n for _ in range(1, repeats):\n layers.append(ShuffleNetUnit(out_channels, out_channels, groups))\n return nn.Sequential(*layers)\n\n def forward(self, x):\n x = F.relu(self.bn1(self.conv1(x)))\n x = self.maxpool(x)\n x = self.stage2(x)\n x = self.stage3(x)\n x = self.stage4(x)\n x = F.relu(self.bn5(self.conv5(x)))\n x = F.adaptive_avg_pool2d(x, (1, 1))\n x = x.view(x.size(0), -1)\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.38 μs Runtime stats: mean=8.38μs median=?μs std=0.20μs Numerical precision: max_abs_err=0.00e+00 mean_abs_err=0.00e+00 max_rel_err=0.00e+00 Memory: 495.76 MB peak (ref: 495.76 MB, ratio: 1.0x) Kernel launches: 791 (ref: 882, fusion ratio: 1.115) Energy: 966.0 mJ/run (ref: 1207.96 mJ, ratio: 1.2505x) SOL score: 0.1044
final submitted kernel (232 lines, 9,500 chars) — full source, untruncated
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.cpp_extension import load_inline

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

torch::Tensor channel_shuffle(torch::Tensor x, int64_t groups);
torch::Tensor dw_bn_shuffle(torch::Tensor x, torch::Tensor weight, torch::Tensor gamma,
                            torch::Tensor beta, torch::Tensor mean, torch::Tensor var,
                            double eps, int64_t groups);
'''

_cuda_src = r'''
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include <cuda.h>
#include <cuda_runtime.h>

__global__ void channel_shuffle_kernel(const float* __restrict__ x,
                                       float* __restrict__ y,
                                       int total, int C, int HW, int groups) {
    int cpg = C / groups;
    int stride = blockDim.x * gridDim.x;
    for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < total; idx += stride) {
        int widx = idx % HW;
        int t = idx / HW;
        int co = t % C;
        int n = t / C;
        int cp = co / groups;
        int g  = co - cp * groups;
        int ci = g * cpg + cp;
        y[(n * C + co) * HW + widx] = x[(n * C + ci) * HW + widx];
    }
}

__global__ void dw_bn_shuffle_kernel(const float* __restrict__ x,
                                     const float* __restrict__ w,
                                     const float* __restrict__ gamma,
                                     const float* __restrict__ beta,
                                     const float* __restrict__ mean,
                                     const float* __restrict__ var,
                                     float* __restrict__ y,
                                     int total, int N, int C, int H, int Wd,
                                     int groups, float eps) {
    int HW = H * Wd;
    int cpg = C / groups;
    int stride = blockDim.x * gridDim.x;
    for (int idx = blockIdx.x * blockDim.x + threadIdx.x; idx < total; idx += stride) {
        int pix = idx % HW;
        int h = pix / Wd;
        int col = pix - h * Wd;
        int t = idx / HW;
        int co = t % C;
        int n = t / C;
        int cp = co / groups;
        int g = co - cp * groups;
        int ci = g * cpg + cp;

        const float* xbase = x + (n * C + ci) * HW;
        const float* wbase = w + ci * 9;
        float acc = 0.0f;

        int ih = h - 1;
        if (ih >= 0) {
            int row = ih * Wd;
            int iw = col - 1;
            if (iw >= 0) acc += xbase[row + iw] * wbase[0];
            acc += xbase[row + col] * wbase[1];
            iw = col + 1;
            if (iw < Wd) acc += xbase[row + iw] * wbase[2];
        }
        ih = h;
        {
            int row = ih * Wd;
            int iw = col - 1;
            if (iw >= 0) acc += xbase[row + iw] * wbase[3];
            acc += xbase[row + col] * wbase[4];
            iw = col + 1;
            if (iw < Wd) acc += xbase[row + iw] * wbase[5];
        }
        ih = h + 1;
        if (ih < H) {
            int row = ih * Wd;
            int iw = col - 1;
            if (iw >= 0) acc += xbase[row + iw] * wbase[6];
            acc += xbase[row + col] * wbase[7];
            iw = col + 1;
            if (iw < Wd) acc += xbase[row + iw] * wbase[8];
        }

        float out = (acc - mean[ci]) * (gamma[ci] * rsqrtf(var[ci] + eps)) + beta[ci];
        y[(n * C + co) * HW + pix] = out;
    }
}

torch::Tensor channel_shuffle(torch::Tensor x, int64_t groups) {
    TORCH_CHECK(x.is_cuda(), "x must be CUDA");
    TORCH_CHECK(x.scalar_type() == torch::kFloat32, "only float32 supported");
    TORCH_CHECK(x.dim() == 4, "x must be NCHW");
    auto y = torch::empty_like(x);
    int N = (int)x.size(0);
    int C = (int)x.size(1);
    int H = (int)x.size(2);
    int Wd = (int)x.size(3);
    int total = N * C * H * Wd;
    int threads = 256;
    int blocks = (total + threads - 1) / threads;
    if (blocks > 4096) blocks = 4096;
    channel_shuffle_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(
        x.data_ptr<float>(), y.data_ptr<float>(), total, C, H * Wd, (int)groups);
    return y;
}

torch::Tensor dw_bn_shuffle(torch::Tensor x, torch::Tensor weight, torch::Tensor gamma,
                            torch::Tensor beta, torch::Tensor mean, torch::Tensor var,
                            double eps, int64_t groups) {
    TORCH_CHECK(x.is_cuda(), "x must be CUDA");
    TORCH_CHECK(x.scalar_type() == torch::kFloat32, "only float32 supported");
    auto y = torch::empty_like(x);
    int N = (int)x.size(0);
    int C = (int)x.size(1);
    int H = (int)x.size(2);
    int Wd = (int)x.size(3);
    int total = N * C * H * Wd;
    int threads = 256;
    int blocks = (total + threads - 1) / threads;
    if (blocks > 4096) blocks = 4096;
    dw_bn_shuffle_kernel<<<blocks, threads, 0, at::cuda::getCurrentCUDAStream()>>>(
        x.data_ptr<float>(), weight.data_ptr<float>(), gamma.data_ptr<float>(), beta.data_ptr<float>(),
        mean.data_ptr<float>(), var.data_ptr<float>(), y.data_ptr<float>(), total, N, C, H, Wd,
        (int)groups, (float)eps);
    return y;
}
'''

_ext = load_inline(
    name="shufflenet_fused_ext_v1",
    cpp_sources=_cpp_src,
    cuda_sources=_cuda_src,
    functions=["channel_shuffle", "dw_bn_shuffle"],
    extra_cflags=["-O3"],
    extra_cuda_cflags=["-O3"],
    verbose=False,
)


class ChannelShuffle(nn.Module):
    def __init__(self, groups):
        super(ChannelShuffle, self).__init__()
        self.groups = groups

    def forward(self, x):
        if x.is_cuda and x.dtype == torch.float32 and x.is_contiguous():
            return _ext.channel_shuffle(x, self.groups)
        batch_size, channels, height, width = x.size()
        channels_per_group = channels // self.groups
        x = x.view(batch_size, self.groups, channels_per_group, height, width)
        x = x.transpose(1, 2).contiguous()
        x = x.view(batch_size, -1, height, width)
        return x


class ShuffleNetUnit(nn.Module):
    def __init__(self, in_channels, out_channels, groups=3):
        super(ShuffleNetUnit, self).__init__()
        assert out_channels % 4 == 0
        mid_channels = out_channels // 4
        self.groups = groups
        self.conv1 = nn.Conv2d(in_channels, mid_channels, kernel_size=1, stride=1, padding=0, groups=groups, bias=False)
        self.bn1 = nn.BatchNorm2d(mid_channels)
        self.conv2 = nn.Conv2d(mid_channels, mid_channels, kernel_size=3, stride=1, padding=1, groups=mid_channels, bias=False)
        self.bn2 = nn.BatchNorm2d(mid_channels)
        self.conv3 = nn.Conv2d(mid_channels, out_channels, kernel_size=1, stride=1, padding=0, groups=groups, bias=False)
        self.bn3 = nn.BatchNorm2d(out_channels)
        self.shuffle = ChannelShuffle(groups)
        if in_channels == out_channels:
            self.shortcut = nn.Sequential()
        else:
            self.shortcut = nn.Sequential(
                nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=1, padding=0, bias=False),
                nn.BatchNorm2d(out_channels)
            )

    def forward(self, x):
        out = F.relu(self.bn1(self.conv1(x)))
        # In inference mode, fuse depthwise 3x3 + BatchNorm + channel shuffle into one CUDA kernel.
        # In training mode, keep PyTorch BatchNorm semantics exactly (batch statistics and running updates).
        if (not self.training) and out.is_cuda and out.dtype == torch.float32 and out.is_contiguous():
            out = _ext.dw_bn_shuffle(out, self.conv2.weight, self.bn2.weight, self.bn2.bias,
                                     self.bn2.running_mean, self.bn2.running_var, self.bn2.eps, self.groups)
        else:
            out = self.bn2(self.conv2(out))
            out = self.shuffle(out)
        out = F.relu(self.bn3(self.conv3(out)))
        out = out + self.shortcut(x)
        return out


class ModelNew(nn.Module):
    def __init__(self, num_classes=1000, groups=3, stages_repeats=[3, 7, 3], stages_out_channels=[24, 240, 480, 960]):
        super(ModelNew, self).__init__()
        self.conv1 = nn.Conv2d(3, stages_out_channels[0], kernel_size=3, stride=2, padding=1, bias=False)
        self.bn1 = nn.BatchNorm2d(stages_out_channels[0])
        self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=1)
        self.stage2 = self._make_stage(stages_out_channels[0], stages_out_channels[1], stages_repeats[0], groups)
        self.stage3 = self._make_stage(stages_out_channels[1], stages_out_channels[2], stages_repeats[1], groups)
        self.stage4 = self._make_stage(stages_out_channels[2], stages_out_channels[3], stages_repeats[2], groups)
        self.conv5 = nn.Conv2d(stages_out_channels[3], 1024, kernel_size=1, stride=1, padding=0, bias=False)
        self.bn5 = nn.BatchNorm2d(1024)
        self.fc = nn.Linear(1024, num_classes)

    def _make_stage(self, in_channels, out_channels, repeats, groups):
        layers = []
        layers.append(ShuffleNetUnit(in_channels, out_channels, groups))
        for _ in range(1, repeats):
            layers.append(ShuffleNetUnit(out_channels, out_channels, groups))
        return nn.Sequential(*layers)

    def forward(self, x):
        x = F.relu(self.bn1(self.conv1(x)))
        x = self.maxpool(x)
        x = self.stage2(x)
        x = self.stage3(x)
        x = self.stage4(x)
        x = F.relu(self.bn5(self.conv5(x)))
        x = F.adaptive_avg_pool2d(x, (1, 1))
        x = x.view(x.size(0), -1)
        x = self.fc(x)
        return x