```python
import torch
import torch.nn as nn
import torch.nn.functional as F
from torch.utils.cpp_extension import load_inline


_cpp_source = r"""
#include <torch/extension.h>
#include <vector>

std::vector<torch::Tensor> prepare_transposed_conv(
    torch::Tensor x, torch::Tensor weight, int64_t groups);
"""

_cuda_source = r"""
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include <c10/cuda/CUDAGuard.h>
#include <c10/cuda/CUDAException.h>
#include <cuda_runtime.h>
#include <vector>

// Transpose each batch's C x HW matrix into HW x C.
__global__ void nchw_to_channels_last(
    const float* __restrict__ input,
    float* __restrict__ output,
    int channels,
    int64_t spatial)
{
    __shared__ float tile[32][33];

    const int tx = threadIdx.x;
    const int ty = threadIdx.y;
    const int64_t s = int64_t(blockIdx.x) * 32 + tx;
    const int c = int(blockIdx.y) * 32 + ty;
    const int64_t base = int64_t(blockIdx.z) * channels * spatial;

    #pragma unroll
    for (int j = 0; j < 32; j += 8) {
        if (s < spatial && c + j < channels) {
            tile[ty + j][tx] =
                input[base + int64_t(c + j) * spatial + s];
        }
    }
    __syncthreads();

    const int out_c = int(blockIdx.y) * 32 + tx;
    const int64_t out_s = int64_t(blockIdx.x) * 32 + ty;

    #pragma unroll
    for (int j = 0; j < 32; j += 8) {
        if (out_c < channels && out_s + j < spatial) {
            output[base + (out_s + j) * channels + out_c] =
                tile[tx][ty + j];
        }
    }
}

// Convert ConvTranspose2d's IOHW weights into spatially reversed
// Conv2d weights, stored in channels-last layout.
__global__ void convert_weights(
    const float* __restrict__ input,
    float* __restrict__ output,
    int ci_per_group,
    int co_per_group,
    int kh,
    int kw,
    int64_t count)
{
    const int64_t index =
        int64_t(blockIdx.x) * blockDim.x + threadIdx.x;
    if (index >= count) return;

    int64_t q = index;
    const int ic = q % ci_per_group;
    q /= ci_per_group;
    const int s = q % kw;
    q /= kw;
    const int r = q % kh;
    const int oc = q / kh;

    const int group = oc / co_per_group;
    const int local_oc = oc % co_per_group;
    const int global_ic = group * ci_per_group + ic;

    const int64_t source =
        ((int64_t(global_ic) * co_per_group + local_oc) * kh
         + (kh - 1 - r)) * kw + (kw - 1 - s);

    output[index] = input[source];
}

std::vector<torch::Tensor> prepare_transposed_conv(
    torch::Tensor x, torch::Tensor weight, int64_t groups)
{
    TORCH_CHECK(x.is_cuda() && weight.is_cuda(),
                "Expected CUDA tensors");
    TORCH_CHECK(x.device() == weight.device(),
                "Input and weight must share a device");
    TORCH_CHECK(x.scalar_type() == torch::kFloat32 &&
                weight.scalar_type() == torch::kFloat32,
                "Expected float32 tensors");
    TORCH_CHECK(x.dim() == 4 && weight.dim() == 4,
                "Expected four-dimensional tensors");
    TORCH_CHECK(weight.is_contiguous(),
                "Weight must be contiguous");
    TORCH_CHECK(groups > 0 && weight.size(0) % groups == 0,
                "Invalid groups");
    TORCH_CHECK(x.size(1) == weight.size(0),
                "Input channel count does not match weight");

    const c10::cuda::CUDAGuard device_guard(x.device());
    const auto stream = at::cuda::getCurrentCUDAStream();

    const int64_t n = x.size(0);
    const int64_t ci = x.size(1);
    const int64_t h = x.size(2);
    const int64_t w = x.size(3);
    const int64_t kh = weight.size(2);
    const int64_t kw = weight.size(3);
    const int64_t ci_per_group = ci / groups;
    const int64_t co_per_group = weight.size(1);
    const int64_t co = co_per_group * groups;

    torch::Tensor packed_x;
    if (x.is_contiguous(at::MemoryFormat::ChannelsLast)) {
        packed_x = x;
    } else {
        TORCH_CHECK(x.is_contiguous(), "Input must be contiguous");
        packed_x = torch::empty({n, h, w, ci}, x.options())
                       .permute({0, 3, 1, 2});

        if (x.numel() > 0) {
            const dim3 block(32, 8);
            const dim3 grid(
                (h * w + 31) / 32,
                (ci + 31) / 32,
                n);

            nchw_to_channels_last<<<grid, block, 0, stream>>>(
                x.data_ptr<float>(),
                packed_x.data_ptr<float>(),
                static_cast<int>(ci),
                h * w);
            C10_CUDA_KERNEL_LAUNCH_CHECK();
        }
    }

    auto packed_weight =
        torch::empty({co, kh, kw, ci_per_group}, weight.options())
            .permute({0, 3, 1, 2});

    const int64_t count = weight.numel();
    if (count > 0) {
        convert_weights<<<(count + 255) / 256, 256, 0, stream>>>(
            weight.data_ptr<float>(),
            packed_weight.data_ptr<float>(),
            static_cast<int>(ci_per_group),
            static_cast<int>(co_per_group),
            static_cast<int>(kh),
            static_cast<int>(kw),
            count);
        C10_CUDA_KERNEL_LAUNCH_CHECK();
    }

    return {packed_x, packed_weight};
}
"""

_extension = load_inline(
    name="asymmetric_transpose_conv_layout_v1",
    cpp_sources=_cpp_source,
    cuda_sources=_cuda_source,
    functions=["prepare_transposed_conv"],
    extra_cflags=["-O3"],
    extra_cuda_cflags=["-O3"],
    verbose=False,
)


class _PrepareTransposedConv(torch.autograd.Function):
    @staticmethod
    def forward(ctx, x, weight, groups):
        ctx.groups = groups
        ctx.ci_per_group = weight.shape[0] // groups
        ctx.co_per_group = weight.shape[1]
        packed_x, packed_weight = _extension.prepare_transposed_conv(
            x, weight, groups
        )
        return packed_x, packed_weight

    @staticmethod
    def backward(ctx, grad_x, grad_weight):
        if grad_weight is not None:
            kh, kw = grad_weight.shape[-2:]
            grad_weight = (
                grad_weight.reshape(
                    ctx.groups,
                    ctx.co_per_group,
                    ctx.ci_per_group,
                    kh,
                    kw,
                )
                .transpose(1, 2)
                .flip((-2, -1))
                .reshape(
                    ctx.groups * ctx.ci_per_group,
                    ctx.co_per_group,
                    kh,
                    kw,
                )
            )
        return grad_x, grad_weight, None


class ModelNew(nn.Module):
    def __init__(
        self,
        in_channels: int,
        out_channels: int,
        kernel_size: tuple,
        stride: tuple = (1, 1),
        padding: tuple = (0, 0),
        output_padding: tuple = (0, 0),
        dilation: tuple = (1, 1),
        groups: int = 1,
        bias: bool = False,
    ):
        super().__init__()
        self.conv_transpose2d = nn.ConvTranspose2d(
            in_channels,
            out_channels,
            kernel_size,
            stride=stride,
            padding=padding,
            output_padding=output_padding,
            dilation=dilation,
            groups=groups,
            bias=bias,
        )

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        conv = self.conv_transpose2d
        forward_padding = tuple(
            d * (k - 1) - p
            for d, k, p in zip(
                conv.dilation, conv.kernel_size, conv.padding
            )
        )

        eligible = (
            x.is_cuda
            and x.ndim == 4
            and x.dtype == torch.float32
            and conv.weight.dtype == torch.float32
            and conv.stride == (1, 1)
            and conv.output_padding == (0, 0)
            and min(forward_padding) >= 0
            and 0 < x.shape[0] <= 65535
            and x.shape[2] > 0
            and x.shape[3] > 0
            and (x.shape[1] + 31) // 32 <= 65535
        )
        if not eligible:
            return conv(x)

        if not (
            x.is_contiguous()
            or x.is_contiguous(memory_format=torch.channels_last)
        ):
            x = x.contiguous()

        packed_x, packed_weight = _PrepareTransposedConv.apply(
            x, conv.weight.contiguous(), conv.groups
        )

        # At unit stride, transposed convolution equals forward
        # convolution with reversed, channel-transposed weights.
        return F.conv2d(
            packed_x,
            packed_weight,
            conv.bias,
            stride=1,
            padding=forward_padding,
            dilation=conv.dilation,
            groups=conv.groups,
        )
```