gpt-5.5-priority · original · l123_single_gpt
incorrect 40_GRUHidden.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
—
ref runtime
35.70 μs
speedup
—
started
2026-05-05T00:01:57Z
correctness issue: Output mismatch
timingkernelbench.timing
kernel runtime
—
ref runtime
35.70 μs
speedup
—
kernel mean / median / std
—
ref mean / median / std
35.70 / — / 1.79 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
0.139
dram util %
0.3%
compute util %
13.8%
bottleneck
compute-bound
dominant pipe
FP32
arithmetic intensity
—
ridge point
20.00
roofline / occupancynsight_hardware_counters
dram bandwidth
11.82 GB/s
dram util %
0.4%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
73.7%
bottleneck
compute-bound
dominant pipe %
13.9%
arithmetic intensity
—
ridge point
20.00
L1 hit %
—
L2 hit %
1195.9%
ld sectors / req
15.02
st sectors / req
—
regs / thread
336
smem / block
15360 B
block size
1536
peak bw
3350.00 GB/s
peak fp32 tflops
67.00
peak fp16 tflops
1979.00
warp stalls (top 5)nsight
long_scoreboard
10951.1%
short_scoreboard
1103.4%
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn
import torch.nn.functional as F

class Model(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=3, bias=True, batch_first=False):
        """
        :param input_size: The number of expected features in the input x
        :param hidden_size: The number of features in the hidden state h
        :param num_layers: Number of recurrent layers (default: 1)
        :param bias: If False, then the layer does not use bias weights b_ih and b_hh (default: True)
        :param batch_first: If True, then the input and output tensors are provided as (batch, seq, feature) (default: False)
        """
        super(Model, self).__init__()
        
        self.gru = nn.GRU(input_size, hidden_size, num_layers, bias, batch_first, dropout=0, bidirectional=False)
    
    def forward(self, x,h0):
        """
        :param x: The input tensor, shape (seq_len, batch_size, input_size) if batch_first=False, otherwise (batch_size, seq_len, input_size)
        :param h_0: The initial hidden state for the input sequence, shape (num_layers * num_directions, batch_size, hidden_size) (default: None)
        :return: output, h_n
            - output: The output features (h_t) from the last layer of the GRU, for each t, shape (seq_len, batch_size, num_directions * hidden_size) if batch_first=False, otherwise (batch_size, seq_len, num_directions * hidden_size)
            - h_n: The hidden state for t = seq_len, shape (num_layers * num_directions, batch_size, hidden_size)
        """
        output, h_n = self.gru(x, h0)
        return h_n

# Test code
batch_size = 10
seq_len = 512
input_size = 128
hidden_size = 256
num_layers = 6

def get_inputs():
    return [torch.rand(seq_len, batch_size, input_size),torch.rand((num_layers, batch_size, hidden_size))]

def get_init_inputs():
    return [input_size, hidden_size, num_layers]
turn 0 · FINAL · 1 tool call(s) · 106.5s turn · 106.5s LLM · 14,335 chars (click to expand)
turn 0106.5s turn · 106.5s LLM · 0.0s tools · FINAL
reasoning
**Implementing GRU in Python** I need to create a complete Python implementation of a GRU that returns just the hidden state \(h_n\). I'll check if the input sizes are fixed and see if I need to copy the weights using `nn.GRU`. ModelNew should load state_dict, and I’ll ensure parameters are compatible. I’ll also implement custom CUDA, targeting GRU with specific parameters. It’s key to compute all layers sequentially. The GRU equations from PyTorch offer a foundation for my work. **Computing GRU Layers Efficiently** I need to compute for 6 layers with a configuration of T=512, B=10, and H=256, focusing on matrix-vector operations for a small batch. It seems like using cuDNN might optimize performance, but a custom naive implementation could be slower. I could explore using a C++ extension for ATen ops, but maybe I should stick to custom CUDA kernels. The GRU compute can be substantial, requiring careful handling of outputs since each layer needs the full sequence to feed into the next. It sounds tricky, but achievable! **Analyzing computational structure** I’m thinking about how to manage separate threads for each gate in a hidden layer. I have 96 active threads, and I'm concerned about memory weights and coalescing. If each thread processes its own gate, the memory access might not be optimal since the addresses are far apart, leading to poor coalescing. Maybe rearranging loops could help? I could also consider using a matrix-vector computation with tiling, which would allow a block of threads to process outputs efficiently. **Considering model implementation** I’m thinking about needing intermediate sequence buffers for layer 0 input, shaped (T, B, I) with batch_first set to false. I should handle the batch_first argument, especially since the tests default to false. Maybe I can accept it while implementing both options? The model constructor has a batch_first setting; perhaps the tests just focus on getting the initialization without batch_first. I’ll make sure to store this for clarity in my implementation. **Clarifying index management** I’m thinking about whether to maintain the current index. I believe I can use a local integer, cur, set to 0. After computing, I'll write to 1-cur and apply a barrier, then switch cur to 1-cur. Once in the loop, hbuf[cur] will finalize, and I’ll copy h_final. There's no need for a barrier after that copy, but I should remember to output the sequence write at each t with h_new.
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#include <ATen/cuda/CUDAContext.h>\n#include <vector>\n\nvoid launch_gru_layer(const at::Tensor& inp,\n at::Tensor& out,\n const at::Tensor& hinit,\n at::Tensor& hfinal,\n const at::Tensor& wihT,\n const at::Tensor& whhT,\n const at::Tensor& bih,\n const at::Tensor& bhh,\n at::Tensor& hbuf,\n at::Tensor& syncbuf,\n int T, int B, int I, int H,\n bool input_batch_first,\n cudaStream_t stream);\n\ntorch::Tensor gru_forward(torch::Tensor x,\n torch::Tensor h0,\n std::vector<torch::Tensor> wihT,\n std::vector<torch::Tensor> whhT,\n std::vector<torch::Tensor> bih,\n std::vector<torch::Tensor> bhh,\n bool batch_first) {\n TORCH_CHECK(x.is_cuda() && h0.is_cuda(), \"inputs must be CUDA tensors\");\n TORCH_CHECK(x.scalar_type() == at::kFloat, \"only fp32 is supported\");\n const int64_t L64 = h0.size(0);\n const int L = (int)L64;\n const int B = (int)h0.size(1);\n const int H = (int)h0.size(2);\n const int T = batch_first ? (int)x.size(1) : (int)x.size(0);\n\n auto hn = at::empty_like(h0);\n at::Tensor cur = x;\n std::vector<at::Tensor> keep;\n keep.reserve(L);\n cudaStream_t stream = at::cuda::getCurrentCUDAStream();\n\n for (int l = 0; l < L; ++l) {\n int I = (int)wihT[l].size(0);\n auto out = at::empty({T, B, H}, x.options());\n auto hbuf = at::empty({2, B, H}, x.options());\n auto syncbuf = at::zeros({2}, x.options().dtype(at::kInt));\n at::Tensor hinit_l = h0.select(0, l);\n at::Tensor hfinal_l = hn.select(0, l);\n launch_gru_layer(cur, out, hinit_l, hfinal_l,\n wihT[l], whhT[l], bih[l], bhh[l],\n hbuf, syncbuf, T, B, I, H,\n (l == 0) && batch_first, stream);\n keep.push_back(out);\n cur = keep.back();\n }\n return hn;\n}\n'''\n\n_cuda_src = r'''\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n\n#define CHUNK_H 32\n\n__device__ __forceinline__ float sigmoid_f(float x) {\n return 1.0f / (1.0f + expf(-x));\n}\n\n__device__ __forceinline__ void grid_barrier(int total_blocks, int* counter, int* sense, int target) {\n __syncthreads();\n if (threadIdx.x == 0) {\n __threadfence();\n int old = atomicAdd(counter, 1);\n if (old == total_blocks - 1) {\n *counter = 0;\n __threadfence();\n atomicExch(sense, target);\n } else {\n while (atomicAdd(sense, 0) != target) { }\n }\n }\n __syncthreads();\n}\n\n__global__ void gru_layer_kernel(const float* __restrict__ inp,\n float* __restrict__ out,\n const float* __restrict__ hinit,\n float* __restrict__ hfinal,\n const float* __restrict__ wihT,\n const float* __restrict__ whhT,\n const float* __restrict__ bih,\n const float* __restrict__ bhh,\n float* __restrict__ hbuf,\n int* __restrict__ syncbuf,\n int T, int B, int I, int H,\n int chunks, int input_batch_first) {\n __shared__ float sr[CHUNK_H];\n __shared__ float sz[CHUNK_H];\n __shared__ float sni[CHUNK_H];\n __shared__ float snh[CHUNK_H];\n\n const int b = blockIdx.x / chunks;\n const int c = blockIdx.x - b * chunks;\n const int hstart = c * CHUNK_H;\n const int tid = threadIdx.x;\n const int total_blocks = gridDim.x;\n int sense_val = 0;\n\n // initialize recurrent state buffer 0 from h0\n if (tid < CHUNK_H) {\n int h = hstart + tid;\n if (h < H) {\n hbuf[b * H + h] = hinit[b * H + h];\n }\n }\n sense_val = 1 - sense_val;\n grid_barrier(total_blocks, syncbuf, syncbuf + 1, sense_val);\n\n int cur = 0;\n for (int t = 0; t < T; ++t) {\n if (tid < 3 * CHUNK_H) {\n const int gate = tid / CHUNK_H;\n const int off = tid - gate * CHUNK_H;\n const int h = hstart + off;\n if (h < H) {\n float si = 0.0f;\n float sh = 0.0f;\n const int row = gate * H + h;\n\n // input contribution, weights are cached as transposed: [I, 3H]\n for (int k = 0; k < I; ++k) {\n float xv;\n if (input_batch_first) {\n xv = inp[(b * T + t) * I + k];\n } else {\n xv = inp[(t * B + b) * I + k];\n }\n si = fmaf(xv, wihT[k * (3 * H) + row], si);\n }\n // recurrent contribution from previous hidden buffer\n const float* hp = hbuf + cur * (B * H) + b * H;\n for (int k = 0; k < H; ++k) {\n sh = fmaf(hp[k], whhT[k * (3 * H) + row], sh);\n }\n\n if (gate == 0) {\n sr[off] = sigmoid_f(si + sh + bih[row] + bhh[row]);\n } else if (gate == 1) {\n sz[off] = sigmoid_f(si + sh + bih[row] + bhh[row]);\n } else {\n sni[off] = si + bih[row];\n snh[off] = sh + bhh[row];\n }\n }\n }\n __syncthreads();\n\n if (tid < CHUNK_H) {\n int h = hstart + tid;\n if (h < H) {\n float oldh = hbuf[cur * (B * H) + b * H + h];\n float n = tanhf(sni[tid] + sr[tid] * snh[tid]);\n float z = sz[tid];\n float newh = fmaf(z, oldh, (1.0f - z) * n);\n hbuf[(1 - cur) * (B * H) + b * H + h] = newh;\n out[(t * B + b) * H + h] = newh;\n }\n }\n\n sense_val = 1 - sense_val;\n grid_barrier(total_blocks, syncbuf, syncbuf + 1, sense_val);\n cur = 1 - cur;\n }\n\n if (tid < CHUNK_H) {\n int h = hstart + tid;\n if (h < H) {\n hfinal[b * H + h] = hbuf[cur * (B * H) + b * H + h];\n }\n }\n}\n\nvoid launch_gru_layer(const at::Tensor& inp,\n at::Tensor& out,\n const at::Tensor& hinit,\n at::Tensor& hfinal,\n const at::Tensor& wihT,\n const at::Tensor& whhT,\n const at::Tensor& bih,\n const at::Tensor& bhh,\n at::Tensor& hbuf,\n at::Tensor& syncbuf,\n int T, int B, int I, int H,\n bool input_batch_first,\n cudaStream_t stream) {\n int chunks = (H + CHUNK_H - 1) / CHUNK_H;\n dim3 grid(B * chunks);\n dim3 block(128);\n gru_layer_kernel<<<grid, block, 0, stream>>>(\n inp.data_ptr<float>(), out.data_ptr<float>(),\n hinit.data_ptr<float>(), hfinal.data_ptr<float>(),\n wihT.data_ptr<float>(), whhT.data_ptr<float>(),\n bih.data_ptr<float>(), bhh.data_ptr<float>(),\n hbuf.data_ptr<float>(), syncbuf.data_ptr<int>(),\n T, B, I, H, chunks, input_batch_first ? 1 : 0);\n}\n'''\n\n_ext = load_inline(\n name='gru_return_hn_fused_ext',\n cpp_sources=_cpp_src,\n cuda_sources=_cuda_src,\n functions=['gru_forward'],\n extra_cflags=['-O3'],\n extra_cuda_cflags=['-O3'],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self, input_size, hidden_size, num_layers=3, bias=True, batch_first=False):\n super(ModelNew, self).__init__()\n self.gru = nn.GRU(input_size, hidden_size, num_layers, bias, batch_first, dropout=0, bidirectional=False)\n self.input_size = input_size\n self.hidden_size = hidden_size\n self.num_layers = num_layers\n self.bias = bias\n self.batch_first = batch_first\n self._cache_versions = None\n self._cache_device = None\n self._wihT = None\n self._whhT = None\n self._bih = None\n self._bhh = None\n\n def _refresh_cache(self, device):\n params = []\n for l in range(self.num_layers):\n params.append(getattr(self.gru, f'weight_ih_l{l}'))\n params.append(getattr(self.gru, f'weight_hh_l{l}'))\n if self.bias:\n params.append(getattr(self.gru, f'bias_ih_l{l}'))\n params.append(getattr(self.gru, f'bias_hh_l{l}'))\n versions = tuple(p._version for p in params)\n if (self._cache_versions == versions and self._cache_device == device and\n self._wihT is not None):\n return\n self._wihT = []\n self._whhT = []\n self._bih = []\n self._bhh = []\n for l in range(self.num_layers):\n wih = getattr(self.gru, f'weight_ih_l{l}').detach()\n whh = getattr(self.gru, f'weight_hh_l{l}').detach()\n self._wihT.append(wih.t().contiguous())\n self._whhT.append(whh.t().contiguous())\n if self.bias:\n self._bih.append(getattr(self.gru, f'bias_ih_l{l}').detach().contiguous())\n self._bhh.append(getattr(self.gru, f'bias_hh_l{l}').detach().contiguous())\n else:\n self._bih.append(torch.zeros(3 * self.hidden_size, device=device, dtype=torch.float32))\n self._bhh.append(torch.zeros(3 * self.hidden_size, device=device, dtype=torch.float32))\n self._cache_versions = versions\n self._cache_device = device\n\n def forward(self, x, h0):\n self._refresh_cache(x.device)\n return _ext.gru_forward(x.contiguous(), h0.contiguous(), self._wihT, self._whhT, self._bih, self._bhh, self.batch_first)\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.000173', '0.000170', '0.000190', '0.000169', '0.000168'] avg_difference: ['0.000022', '0.000022', '0.000022', '0.000022', '0.000022']
final submitted kernel (273 lines, 10,143 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>
#include <ATen/cuda/CUDAContext.h>
#include <vector>

void launch_gru_layer(const at::Tensor& inp,
                      at::Tensor& out,
                      const at::Tensor& hinit,
                      at::Tensor& hfinal,
                      const at::Tensor& wihT,
                      const at::Tensor& whhT,
                      const at::Tensor& bih,
                      const at::Tensor& bhh,
                      at::Tensor& hbuf,
                      at::Tensor& syncbuf,
                      int T, int B, int I, int H,
                      bool input_batch_first,
                      cudaStream_t stream);

torch::Tensor gru_forward(torch::Tensor x,
                          torch::Tensor h0,
                          std::vector<torch::Tensor> wihT,
                          std::vector<torch::Tensor> whhT,
                          std::vector<torch::Tensor> bih,
                          std::vector<torch::Tensor> bhh,
                          bool batch_first) {
    TORCH_CHECK(x.is_cuda() && h0.is_cuda(), "inputs must be CUDA tensors");
    TORCH_CHECK(x.scalar_type() == at::kFloat, "only fp32 is supported");
    const int64_t L64 = h0.size(0);
    const int L = (int)L64;
    const int B = (int)h0.size(1);
    const int H = (int)h0.size(2);
    const int T = batch_first ? (int)x.size(1) : (int)x.size(0);

    auto hn = at::empty_like(h0);
    at::Tensor cur = x;
    std::vector<at::Tensor> keep;
    keep.reserve(L);
    cudaStream_t stream = at::cuda::getCurrentCUDAStream();

    for (int l = 0; l < L; ++l) {
        int I = (int)wihT[l].size(0);
        auto out = at::empty({T, B, H}, x.options());
        auto hbuf = at::empty({2, B, H}, x.options());
        auto syncbuf = at::zeros({2}, x.options().dtype(at::kInt));
        at::Tensor hinit_l = h0.select(0, l);
        at::Tensor hfinal_l = hn.select(0, l);
        launch_gru_layer(cur, out, hinit_l, hfinal_l,
                         wihT[l], whhT[l], bih[l], bhh[l],
                         hbuf, syncbuf, T, B, I, H,
                         (l == 0) && batch_first, stream);
        keep.push_back(out);
        cur = keep.back();
    }
    return hn;
}
'''

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

#define CHUNK_H 32

__device__ __forceinline__ float sigmoid_f(float x) {
    return 1.0f / (1.0f + expf(-x));
}

__device__ __forceinline__ void grid_barrier(int total_blocks, int* counter, int* sense, int target) {
    __syncthreads();
    if (threadIdx.x == 0) {
        __threadfence();
        int old = atomicAdd(counter, 1);
        if (old == total_blocks - 1) {
            *counter = 0;
            __threadfence();
            atomicExch(sense, target);
        } else {
            while (atomicAdd(sense, 0) != target) { }
        }
    }
    __syncthreads();
}

__global__ void gru_layer_kernel(const float* __restrict__ inp,
                                 float* __restrict__ out,
                                 const float* __restrict__ hinit,
                                 float* __restrict__ hfinal,
                                 const float* __restrict__ wihT,
                                 const float* __restrict__ whhT,
                                 const float* __restrict__ bih,
                                 const float* __restrict__ bhh,
                                 float* __restrict__ hbuf,
                                 int* __restrict__ syncbuf,
                                 int T, int B, int I, int H,
                                 int chunks, int input_batch_first) {
    __shared__ float sr[CHUNK_H];
    __shared__ float sz[CHUNK_H];
    __shared__ float sni[CHUNK_H];
    __shared__ float snh[CHUNK_H];

    const int b = blockIdx.x / chunks;
    const int c = blockIdx.x - b * chunks;
    const int hstart = c * CHUNK_H;
    const int tid = threadIdx.x;
    const int total_blocks = gridDim.x;
    int sense_val = 0;

    // initialize recurrent state buffer 0 from h0
    if (tid < CHUNK_H) {
        int h = hstart + tid;
        if (h < H) {
            hbuf[b * H + h] = hinit[b * H + h];
        }
    }
    sense_val = 1 - sense_val;
    grid_barrier(total_blocks, syncbuf, syncbuf + 1, sense_val);

    int cur = 0;
    for (int t = 0; t < T; ++t) {
        if (tid < 3 * CHUNK_H) {
            const int gate = tid / CHUNK_H;
            const int off  = tid - gate * CHUNK_H;
            const int h = hstart + off;
            if (h < H) {
                float si = 0.0f;
                float sh = 0.0f;
                const int row = gate * H + h;

                // input contribution, weights are cached as transposed: [I, 3H]
                for (int k = 0; k < I; ++k) {
                    float xv;
                    if (input_batch_first) {
                        xv = inp[(b * T + t) * I + k];
                    } else {
                        xv = inp[(t * B + b) * I + k];
                    }
                    si = fmaf(xv, wihT[k * (3 * H) + row], si);
                }
                // recurrent contribution from previous hidden buffer
                const float* hp = hbuf + cur * (B * H) + b * H;
                for (int k = 0; k < H; ++k) {
                    sh = fmaf(hp[k], whhT[k * (3 * H) + row], sh);
                }

                if (gate == 0) {
                    sr[off] = sigmoid_f(si + sh + bih[row] + bhh[row]);
                } else if (gate == 1) {
                    sz[off] = sigmoid_f(si + sh + bih[row] + bhh[row]);
                } else {
                    sni[off] = si + bih[row];
                    snh[off] = sh + bhh[row];
                }
            }
        }
        __syncthreads();

        if (tid < CHUNK_H) {
            int h = hstart + tid;
            if (h < H) {
                float oldh = hbuf[cur * (B * H) + b * H + h];
                float n = tanhf(sni[tid] + sr[tid] * snh[tid]);
                float z = sz[tid];
                float newh = fmaf(z, oldh, (1.0f - z) * n);
                hbuf[(1 - cur) * (B * H) + b * H + h] = newh;
                out[(t * B + b) * H + h] = newh;
            }
        }

        sense_val = 1 - sense_val;
        grid_barrier(total_blocks, syncbuf, syncbuf + 1, sense_val);
        cur = 1 - cur;
    }

    if (tid < CHUNK_H) {
        int h = hstart + tid;
        if (h < H) {
            hfinal[b * H + h] = hbuf[cur * (B * H) + b * H + h];
        }
    }
}

void launch_gru_layer(const at::Tensor& inp,
                      at::Tensor& out,
                      const at::Tensor& hinit,
                      at::Tensor& hfinal,
                      const at::Tensor& wihT,
                      const at::Tensor& whhT,
                      const at::Tensor& bih,
                      const at::Tensor& bhh,
                      at::Tensor& hbuf,
                      at::Tensor& syncbuf,
                      int T, int B, int I, int H,
                      bool input_batch_first,
                      cudaStream_t stream) {
    int chunks = (H + CHUNK_H - 1) / CHUNK_H;
    dim3 grid(B * chunks);
    dim3 block(128);
    gru_layer_kernel<<<grid, block, 0, stream>>>(
        inp.data_ptr<float>(), out.data_ptr<float>(),
        hinit.data_ptr<float>(), hfinal.data_ptr<float>(),
        wihT.data_ptr<float>(), whhT.data_ptr<float>(),
        bih.data_ptr<float>(), bhh.data_ptr<float>(),
        hbuf.data_ptr<float>(), syncbuf.data_ptr<int>(),
        T, B, I, H, chunks, input_batch_first ? 1 : 0);
}
'''

_ext = load_inline(
    name='gru_return_hn_fused_ext',
    cpp_sources=_cpp_src,
    cuda_sources=_cuda_src,
    functions=['gru_forward'],
    extra_cflags=['-O3'],
    extra_cuda_cflags=['-O3'],
    verbose=False,
)

class ModelNew(nn.Module):
    def __init__(self, input_size, hidden_size, num_layers=3, bias=True, batch_first=False):
        super(ModelNew, self).__init__()
        self.gru = nn.GRU(input_size, hidden_size, num_layers, bias, batch_first, dropout=0, bidirectional=False)
        self.input_size = input_size
        self.hidden_size = hidden_size
        self.num_layers = num_layers
        self.bias = bias
        self.batch_first = batch_first
        self._cache_versions = None
        self._cache_device = None
        self._wihT = None
        self._whhT = None
        self._bih = None
        self._bhh = None

    def _refresh_cache(self, device):
        params = []
        for l in range(self.num_layers):
            params.append(getattr(self.gru, f'weight_ih_l{l}'))
            params.append(getattr(self.gru, f'weight_hh_l{l}'))
            if self.bias:
                params.append(getattr(self.gru, f'bias_ih_l{l}'))
                params.append(getattr(self.gru, f'bias_hh_l{l}'))
        versions = tuple(p._version for p in params)
        if (self._cache_versions == versions and self._cache_device == device and
            self._wihT is not None):
            return
        self._wihT = []
        self._whhT = []
        self._bih = []
        self._bhh = []
        for l in range(self.num_layers):
            wih = getattr(self.gru, f'weight_ih_l{l}').detach()
            whh = getattr(self.gru, f'weight_hh_l{l}').detach()
            self._wihT.append(wih.t().contiguous())
            self._whhT.append(whh.t().contiguous())
            if self.bias:
                self._bih.append(getattr(self.gru, f'bias_ih_l{l}').detach().contiguous())
                self._bhh.append(getattr(self.gru, f'bias_hh_l{l}').detach().contiguous())
            else:
                self._bih.append(torch.zeros(3 * self.hidden_size, device=device, dtype=torch.float32))
                self._bhh.append(torch.zeros(3 * self.hidden_size, device=device, dtype=torch.float32))
        self._cache_versions = versions
        self._cache_device = device

    def forward(self, x, h0):
        self._refresh_cache(x.device)
        return _ext.gru_forward(x.contiguous(), h0.contiguous(), self._wihT, self._whhT, self._bih, self._bhh, self.batch_first)