gpt-5.5-priority · popcorn · l123_single_gpt
incorrect 35_FusedConv1dGroupNorm.py
model
gpt-5.5-priority
turns / max
1 / 1
tool calls
1
tokens (total)
0
wall clock
—
truncated
no
backend
cuda fp32
variant
popcorn
runtime
—
ref runtime
0.08 μs
speedup
—
started
2026-05-05T03:52:30Z
correctness issue: Output mismatch
timingkernelbench.timing
kernel runtime
—
ref runtime
0.08 μs
speedup
—
kernel mean / median / std
—
ref mean / median / std
0.08 / — / 0.01 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
0.255
dram util %
20.0%
compute util %
25.5%
bottleneck
compute-bound
dominant pipe
FP32
arithmetic intensity
—
ridge point
20.00
roofline / occupancynsight_hardware_counters
dram bandwidth
670.72 GB/s
dram util %
20.0%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
142.3%
bottleneck
compute-bound
dominant pipe %
25.5%
arithmetic intensity
—
ridge point
20.00
L1 hit %
—
L2 hit %
153.7%
ld sectors / req
5.16
st sectors / req
—
regs / thread
61
smem / block
4480 B
block size
512
peak bw
3350.00 GB/s
peak fp32 tflops
67.00
peak fp16 tflops
1979.00
warp stalls (top 5)nsight
long_scoreboard
1866.7%
short_scoreboard
308.5%
mio_throttle
101.9%
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn

# Default ModelNew is PyTorch-only. run_and_check: use check_kernel=False with backend=cuda
# until the candidate file contains a real CUDA kernel (see kernel_static_checker).

class Model(nn.Module):
    """
    Fused 1D convolution + GroupNorm for temporal sequences (N, C, T).

    Typical in time-series heads, audio / motor trajectories, and sequence models
    that use strided or padded Conv1d followed by per-channel normalization without
    collapsing the time dimension (GroupNorm over `out_channels` on (N, C_out, T')).

    Reference fusion target: `Conv1d` → `GroupNorm` (no activation), matching a
    common stem or residual branch in temporal backbones.
    """

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        num_groups: int,
        kernel_size: int = 3,
        stride: int = 1,
        padding: int = 1,
        dilation: int = 1,
        eps: float = 1e-5,
        affine: bool = True,
    ):
        super().__init__()
        assert out_channels % num_groups == 0
        self.conv = nn.Conv1d(
            in_channels,
            out_channels,
            kernel_size,
            stride=stride,
            padding=padding,
            dilation=dilation,
            bias=False,
        )
        self.norm = nn.GroupNorm(
            num_groups=num_groups,
            num_channels=out_channels,
            eps=eps,
            affine=affine,
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        """
        Args:
            x: (N, C_in, T) temporal feature map
        Returns:
            (N, C_out, T') with T' determined by conv stride / padding / kernel
        """
        return self.norm(self.conv(x))


class ModelNew(nn.Module):
    """
    KernelBench candidate entry point. Replace with a fused CUDA/Triton kernel;
    default delegates to `Model` for harness smoke tests.
    """

    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        num_groups: int,
        kernel_size: int = 3,
        stride: int = 1,
        padding: int = 1,
        dilation: int = 1,
        eps: float = 1e-5,
        affine: bool = True,
    ):
        super().__init__()
        self._impl = Model(
            in_channels,
            out_channels,
            num_groups,
            kernel_size=kernel_size,
            stride=stride,
            padding=padding,
            dilation=dilation,
            eps=eps,
            affine=affine,
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        return self._impl(x)


batch_size = 16
in_channels = 128
out_channels = 256
num_groups = 32
seq_len = 1024
kernel_size = 3
stride = 1
padding = 1


def get_inputs():
    p = popcorn_pri
    x = torch.randn(p.jitter_int(batch_size), in_channels, p.jitter_int(seq_len))
    return [x]


def get_init_inputs():
    return [
        in_channels,
        out_channels,
        num_groups,
        kernel_size,
        stride,
        padding,
    ]
turn 0 · FINAL · 1 tool call(s) · 59.9s turn · 59.9s LLM · 11,884 chars (click to expand)
turn 059.9s turn · 59.9s LLM · 0.0s tools · FINAL
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 fused_conv_gn(torch::Tensor x,\n torch::Tensor weight,\n torch::Tensor gamma,\n torch::Tensor beta,\n int64_t num_groups,\n double eps,\n int64_t stride,\n int64_t padding,\n int64_t dilation);\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// Specialized fast path for the benchmark's Conv1d parameters:\n// kernel=3, stride=1, padding=1, dilation=1, fp32, contiguous NCT layout.\n// One block computes a 16 (time) x 16 (output-channel) tile for one batch item.\n__global__ void conv1d_k3s1p1_tile_kernel(const float* __restrict__ x,\n const float* __restrict__ w,\n float* __restrict__ y,\n int N, int Cin, int Cout, int T, int Tout) {\n const int tx = threadIdx.x; // 0..15 time within tile\n const int ty = threadIdx.y; // 0..15 output channel within tile\n const int tid = ty * 16 + tx;\n const int t0 = blockIdx.x * 16;\n const int co = blockIdx.y * 16 + ty;\n const int n = blockIdx.z;\n const int t = t0 + tx;\n\n __shared__ float sx[18]; // x[n, ci, t0-1 : t0+16]\n __shared__ float sw[16][3]; // w[co_tile, ci, k]\n\n float acc = 0.0f;\n for (int ci = 0; ci < Cin; ++ci) {\n if (tid < 18) {\n const int tt = t0 + tid - 1;\n float v = 0.0f;\n if ((unsigned)tt < (unsigned)T) {\n v = x[(n * Cin + ci) * T + tt];\n }\n sx[tid] = v;\n }\n if (tx < 3) {\n const int c = co;\n if (c < Cout) {\n sw[ty][tx] = w[(c * Cin + ci) * 3 + tx];\n }\n }\n __syncthreads();\n if (co < Cout && t < Tout) {\n acc = fmaf(sw[ty][0], sx[tx], acc);\n acc = fmaf(sw[ty][1], sx[tx + 1], acc);\n acc = fmaf(sw[ty][2], sx[tx + 2], acc);\n }\n __syncthreads();\n }\n if (co < Cout && t < Tout) {\n y[(n * Cout + co) * Tout + t] = acc;\n }\n}\n\n// Generic direct Conv1d fallback for unusual constructor arguments. It is mainly\n// for correctness outside the benchmark shape; the specialized path above is used\n// for the provided case.\n__global__ void conv1d_generic_kernel(const float* __restrict__ x,\n const float* __restrict__ w,\n float* __restrict__ y,\n int N, int Cin, int Cout, int T, int K,\n int Tout, int stride, int padding, int dilation) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = N * Cout * Tout;\n for (; idx < total; idx += blockDim.x * gridDim.x) {\n int t = idx % Tout;\n int co = (idx / Tout) % Cout;\n int n = idx / (Tout * Cout);\n float acc = 0.0f;\n int base = t * stride - padding;\n for (int ci = 0; ci < Cin; ++ci) {\n #pragma unroll\n for (int k = 0; k < 8; ++k) {\n if (k >= K) break;\n int ti = base + k * dilation;\n if ((unsigned)ti < (unsigned)T) {\n acc = fmaf(w[(co * Cin + ci) * K + k], x[(n * Cin + ci) * T + ti], acc);\n }\n }\n for (int k = 8; k < K; ++k) {\n int ti = base + k * dilation;\n if ((unsigned)ti < (unsigned)T) {\n acc = fmaf(w[(co * Cin + ci) * K + k], x[(n * Cin + ci) * T + ti], acc);\n }\n }\n }\n y[idx] = acc;\n }\n}\n\n// One block per (sample, group). Reduces over C_per_group * T, then writes the\n// normalized and affine-transformed output.\n__global__ void groupnorm_forward_kernel(const float* __restrict__ inp,\n const float* __restrict__ gamma,\n const float* __restrict__ beta,\n float* __restrict__ out,\n int N, int C, int T, int G,\n float eps, bool affine) {\n extern __shared__ float smem[];\n float* ssum = smem;\n float* ssq = smem + blockDim.x;\n const int tid = threadIdx.x;\n const int ng = blockIdx.x;\n const int n = ng / G;\n const int g = ng - n * G;\n const int cpg = C / G;\n const int elems = cpg * T;\n const int cbase = g * cpg;\n\n float sum = 0.0f;\n float sumsq = 0.0f;\n for (int i = tid; i < elems; i += blockDim.x) {\n int c = cbase + i / T;\n int t = i - (i / T) * T;\n float v = inp[(n * C + c) * T + t];\n sum += v;\n sumsq = fmaf(v, v, sumsq);\n }\n ssum[tid] = sum;\n ssq[tid] = sumsq;\n __syncthreads();\n\n for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1) {\n if (tid < offset) {\n ssum[tid] += ssum[tid + offset];\n ssq[tid] += ssq[tid + offset];\n }\n __syncthreads();\n }\n\n const float mean = ssum[0] / (float)elems;\n float var = ssq[0] / (float)elems - mean * mean;\n var = var < 0.0f ? 0.0f : var;\n const float rstd = rsqrtf(var + eps);\n\n for (int i = tid; i < elems; i += blockDim.x) {\n int c_local = i / T;\n int c = cbase + c_local;\n int t = i - c_local * T;\n float v = inp[(n * C + c) * T + t];\n float z = (v - mean) * rstd;\n if (affine) z = z * gamma[c] + beta[c];\n out[(n * C + c) * T + t] = z;\n }\n}\n\ntorch::Tensor fused_conv_gn(torch::Tensor x,\n torch::Tensor weight,\n torch::Tensor gamma,\n torch::Tensor beta,\n int64_t num_groups,\n double eps,\n int64_t stride,\n int64_t padding,\n int64_t dilation) {\n TORCH_CHECK(x.is_cuda(), \"x must be CUDA\");\n TORCH_CHECK(weight.is_cuda(), \"weight must be CUDA\");\n TORCH_CHECK(x.scalar_type() == torch::kFloat32, \"only fp32 is supported\");\n TORCH_CHECK(weight.scalar_type() == torch::kFloat32, \"only fp32 weights are supported\");\n\n x = x.contiguous();\n weight = weight.contiguous();\n if (gamma.defined()) gamma = gamma.contiguous();\n if (beta.defined()) beta = beta.contiguous();\n\n const int N = (int)x.size(0);\n const int Cin = (int)x.size(1);\n const int T = (int)x.size(2);\n const int Cout = (int)weight.size(0);\n const int K = (int)weight.size(2);\n const int Tout = (int)((T + 2 * padding - dilation * (K - 1) - 1) / stride + 1);\n\n auto conv = torch::empty({N, Cout, Tout}, x.options());\n auto out = torch::empty({N, Cout, Tout}, x.options());\n cudaStream_t stream = at::cuda::getCurrentCUDAStream();\n\n if (K == 3 && stride == 1 && padding == 1 && dilation == 1) {\n dim3 block(16, 16, 1);\n dim3 grid((Tout + 15) / 16, (Cout + 15) / 16, N);\n conv1d_k3s1p1_tile_kernel<<<grid, block, 0, stream>>>(\n x.data_ptr<float>(), weight.data_ptr<float>(), conv.data_ptr<float>(),\n N, Cin, Cout, T, Tout);\n } else {\n int total = N * Cout * Tout;\n int threads = 256;\n int blocks = (total + threads - 1) / threads;\n if (blocks > 4096) blocks = 4096;\n conv1d_generic_kernel<<<blocks, threads, 0, stream>>>(\n x.data_ptr<float>(), weight.data_ptr<float>(), conv.data_ptr<float>(),\n N, Cin, Cout, T, K, Tout, (int)stride, (int)padding, (int)dilation);\n }\n\n const bool affine = gamma.defined() && gamma.numel() == Cout && beta.defined() && beta.numel() == Cout;\n const float* gptr = affine ? gamma.data_ptr<float>() : nullptr;\n const float* bptr = affine ? beta.data_ptr<float>() : nullptr;\n int threads = 256;\n size_t shmem = 2 * threads * sizeof(float);\n groupnorm_forward_kernel<<<N * (int)num_groups, threads, shmem, stream>>>(\n conv.data_ptr<float>(), gptr, bptr, out.data_ptr<float>(),\n N, Cout, Tout, (int)num_groups, (float)eps, affine);\n\n return out;\n}\n'''\n\n_ext = load_inline(\n name=\"fused_conv1d_groupnorm_ext_v1\",\n cpp_sources=_cpp_src,\n cuda_sources=_cuda_src,\n functions=[\"fused_conv_gn\"],\n extra_cflags=[\"-O3\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(\n self,\n in_channels: int,\n out_channels: int,\n num_groups: int,\n kernel_size: int = 3,\n stride: int = 1,\n padding: int = 1,\n dilation: int = 1,\n eps: float = 1e-5,\n affine: bool = True,\n ):\n super().__init__()\n assert out_channels % num_groups == 0\n self.conv = nn.Conv1d(\n in_channels,\n out_channels,\n kernel_size,\n stride=stride,\n padding=padding,\n dilation=dilation,\n bias=False,\n )\n self.norm = nn.GroupNorm(\n num_groups=num_groups,\n num_channels=out_channels,\n eps=eps,\n affine=affine,\n )\n self.num_groups = num_groups\n self.eps = eps\n self.stride = stride\n self.padding = padding\n self.dilation = dilation\n self.affine = affine\n\n def forward(self, x: torch.Tensor) -> torch.Tensor:\n if self.affine:\n gamma = self.norm.weight\n beta = self.norm.bias\n else:\n gamma = torch.empty(0, device=x.device, dtype=x.dtype)\n beta = torch.empty(0, device=x.device, dtype=x.dtype)\n return _ext.fused_conv_gn(\n x,\n self.conv.weight,\n gamma,\n beta,\n self.num_groups,\n float(self.eps),\n int(self.stride),\n int(self.padding),\n int(self.dilation),\n )\n" }
result ← submit_kernel (fail)
failsubmit_kernel submit_kernel FAILED: correctness check did not pass ((0 / 5) trials). correctness_issue: Output mismatch max_difference: ['0.001633', '0.001641', '0.001589', '0.001566', '0.001546'] avg_difference: ['0.000233', '0.000233', '0.000233', '0.000233', '0.000233']
final submitted kernel (289 lines, 10,267 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 fused_conv_gn(torch::Tensor x,
                            torch::Tensor weight,
                            torch::Tensor gamma,
                            torch::Tensor beta,
                            int64_t num_groups,
                            double eps,
                            int64_t stride,
                            int64_t padding,
                            int64_t dilation);
'''

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

// Specialized fast path for the benchmark's Conv1d parameters:
// kernel=3, stride=1, padding=1, dilation=1, fp32, contiguous NCT layout.
// One block computes a 16 (time) x 16 (output-channel) tile for one batch item.
__global__ void conv1d_k3s1p1_tile_kernel(const float* __restrict__ x,
                                           const float* __restrict__ w,
                                           float* __restrict__ y,
                                           int N, int Cin, int Cout, int T, int Tout) {
    const int tx = threadIdx.x;          // 0..15 time within tile
    const int ty = threadIdx.y;          // 0..15 output channel within tile
    const int tid = ty * 16 + tx;
    const int t0 = blockIdx.x * 16;
    const int co = blockIdx.y * 16 + ty;
    const int n = blockIdx.z;
    const int t = t0 + tx;

    __shared__ float sx[18];             // x[n, ci, t0-1 : t0+16]
    __shared__ float sw[16][3];          // w[co_tile, ci, k]

    float acc = 0.0f;
    for (int ci = 0; ci < Cin; ++ci) {
        if (tid < 18) {
            const int tt = t0 + tid - 1;
            float v = 0.0f;
            if ((unsigned)tt < (unsigned)T) {
                v = x[(n * Cin + ci) * T + tt];
            }
            sx[tid] = v;
        }
        if (tx < 3) {
            const int c = co;
            if (c < Cout) {
                sw[ty][tx] = w[(c * Cin + ci) * 3 + tx];
            }
        }
        __syncthreads();
        if (co < Cout && t < Tout) {
            acc = fmaf(sw[ty][0], sx[tx], acc);
            acc = fmaf(sw[ty][1], sx[tx + 1], acc);
            acc = fmaf(sw[ty][2], sx[tx + 2], acc);
        }
        __syncthreads();
    }
    if (co < Cout && t < Tout) {
        y[(n * Cout + co) * Tout + t] = acc;
    }
}

// Generic direct Conv1d fallback for unusual constructor arguments.  It is mainly
// for correctness outside the benchmark shape; the specialized path above is used
// for the provided case.
__global__ void conv1d_generic_kernel(const float* __restrict__ x,
                                      const float* __restrict__ w,
                                      float* __restrict__ y,
                                      int N, int Cin, int Cout, int T, int K,
                                      int Tout, int stride, int padding, int dilation) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int total = N * Cout * Tout;
    for (; idx < total; idx += blockDim.x * gridDim.x) {
        int t = idx % Tout;
        int co = (idx / Tout) % Cout;
        int n = idx / (Tout * Cout);
        float acc = 0.0f;
        int base = t * stride - padding;
        for (int ci = 0; ci < Cin; ++ci) {
            #pragma unroll
            for (int k = 0; k < 8; ++k) {
                if (k >= K) break;
                int ti = base + k * dilation;
                if ((unsigned)ti < (unsigned)T) {
                    acc = fmaf(w[(co * Cin + ci) * K + k], x[(n * Cin + ci) * T + ti], acc);
                }
            }
            for (int k = 8; k < K; ++k) {
                int ti = base + k * dilation;
                if ((unsigned)ti < (unsigned)T) {
                    acc = fmaf(w[(co * Cin + ci) * K + k], x[(n * Cin + ci) * T + ti], acc);
                }
            }
        }
        y[idx] = acc;
    }
}

// One block per (sample, group).  Reduces over C_per_group * T, then writes the
// normalized and affine-transformed output.
__global__ void groupnorm_forward_kernel(const float* __restrict__ inp,
                                         const float* __restrict__ gamma,
                                         const float* __restrict__ beta,
                                         float* __restrict__ out,
                                         int N, int C, int T, int G,
                                         float eps, bool affine) {
    extern __shared__ float smem[];
    float* ssum = smem;
    float* ssq = smem + blockDim.x;
    const int tid = threadIdx.x;
    const int ng = blockIdx.x;
    const int n = ng / G;
    const int g = ng - n * G;
    const int cpg = C / G;
    const int elems = cpg * T;
    const int cbase = g * cpg;

    float sum = 0.0f;
    float sumsq = 0.0f;
    for (int i = tid; i < elems; i += blockDim.x) {
        int c = cbase + i / T;
        int t = i - (i / T) * T;
        float v = inp[(n * C + c) * T + t];
        sum += v;
        sumsq = fmaf(v, v, sumsq);
    }
    ssum[tid] = sum;
    ssq[tid] = sumsq;
    __syncthreads();

    for (int offset = blockDim.x >> 1; offset > 0; offset >>= 1) {
        if (tid < offset) {
            ssum[tid] += ssum[tid + offset];
            ssq[tid] += ssq[tid + offset];
        }
        __syncthreads();
    }

    const float mean = ssum[0] / (float)elems;
    float var = ssq[0] / (float)elems - mean * mean;
    var = var < 0.0f ? 0.0f : var;
    const float rstd = rsqrtf(var + eps);

    for (int i = tid; i < elems; i += blockDim.x) {
        int c_local = i / T;
        int c = cbase + c_local;
        int t = i - c_local * T;
        float v = inp[(n * C + c) * T + t];
        float z = (v - mean) * rstd;
        if (affine) z = z * gamma[c] + beta[c];
        out[(n * C + c) * T + t] = z;
    }
}

torch::Tensor fused_conv_gn(torch::Tensor x,
                            torch::Tensor weight,
                            torch::Tensor gamma,
                            torch::Tensor beta,
                            int64_t num_groups,
                            double eps,
                            int64_t stride,
                            int64_t padding,
                            int64_t dilation) {
    TORCH_CHECK(x.is_cuda(), "x must be CUDA");
    TORCH_CHECK(weight.is_cuda(), "weight must be CUDA");
    TORCH_CHECK(x.scalar_type() == torch::kFloat32, "only fp32 is supported");
    TORCH_CHECK(weight.scalar_type() == torch::kFloat32, "only fp32 weights are supported");

    x = x.contiguous();
    weight = weight.contiguous();
    if (gamma.defined()) gamma = gamma.contiguous();
    if (beta.defined()) beta = beta.contiguous();

    const int N = (int)x.size(0);
    const int Cin = (int)x.size(1);
    const int T = (int)x.size(2);
    const int Cout = (int)weight.size(0);
    const int K = (int)weight.size(2);
    const int Tout = (int)((T + 2 * padding - dilation * (K - 1) - 1) / stride + 1);

    auto conv = torch::empty({N, Cout, Tout}, x.options());
    auto out = torch::empty({N, Cout, Tout}, x.options());
    cudaStream_t stream = at::cuda::getCurrentCUDAStream();

    if (K == 3 && stride == 1 && padding == 1 && dilation == 1) {
        dim3 block(16, 16, 1);
        dim3 grid((Tout + 15) / 16, (Cout + 15) / 16, N);
        conv1d_k3s1p1_tile_kernel<<<grid, block, 0, stream>>>(
            x.data_ptr<float>(), weight.data_ptr<float>(), conv.data_ptr<float>(),
            N, Cin, Cout, T, Tout);
    } else {
        int total = N * Cout * Tout;
        int threads = 256;
        int blocks = (total + threads - 1) / threads;
        if (blocks > 4096) blocks = 4096;
        conv1d_generic_kernel<<<blocks, threads, 0, stream>>>(
            x.data_ptr<float>(), weight.data_ptr<float>(), conv.data_ptr<float>(),
            N, Cin, Cout, T, K, Tout, (int)stride, (int)padding, (int)dilation);
    }

    const bool affine = gamma.defined() && gamma.numel() == Cout && beta.defined() && beta.numel() == Cout;
    const float* gptr = affine ? gamma.data_ptr<float>() : nullptr;
    const float* bptr = affine ? beta.data_ptr<float>() : nullptr;
    int threads = 256;
    size_t shmem = 2 * threads * sizeof(float);
    groupnorm_forward_kernel<<<N * (int)num_groups, threads, shmem, stream>>>(
        conv.data_ptr<float>(), gptr, bptr, out.data_ptr<float>(),
        N, Cout, Tout, (int)num_groups, (float)eps, affine);

    return out;
}
'''

_ext = load_inline(
    name="fused_conv1d_groupnorm_ext_v1",
    cpp_sources=_cpp_src,
    cuda_sources=_cuda_src,
    functions=["fused_conv_gn"],
    extra_cflags=["-O3"],
    extra_cuda_cflags=["-O3", "--use_fast_math"],
    verbose=False,
)


class ModelNew(nn.Module):
    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        num_groups: int,
        kernel_size: int = 3,
        stride: int = 1,
        padding: int = 1,
        dilation: int = 1,
        eps: float = 1e-5,
        affine: bool = True,
    ):
        super().__init__()
        assert out_channels % num_groups == 0
        self.conv = nn.Conv1d(
            in_channels,
            out_channels,
            kernel_size,
            stride=stride,
            padding=padding,
            dilation=dilation,
            bias=False,
        )
        self.norm = nn.GroupNorm(
            num_groups=num_groups,
            num_channels=out_channels,
            eps=eps,
            affine=affine,
        )
        self.num_groups = num_groups
        self.eps = eps
        self.stride = stride
        self.padding = padding
        self.dilation = dilation
        self.affine = affine

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        if self.affine:
            gamma = self.norm.weight
            beta = self.norm.bias
        else:
            gamma = torch.empty(0, device=x.device, dtype=x.dtype)
            beta = torch.empty(0, device=x.device, dtype=x.dtype)
        return _ext.fused_conv_gn(
            x,
            self.conv.weight,
            gamma,
            beta,
            self.num_groups,
            float(self.eps),
            int(self.stride),
            int(self.padding),
            int(self.dilation),
        )