gpt-5.5-priority · popcorn · l123_single_gpt
incorrect 14_TriangularMultiplicativeUpdateOutgoing.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.49 μs
speedup
—
started
2026-05-05T00:33:11Z
correctness issue: Output mismatch
timingkernelbench.timing
kernel runtime
—
ref runtime
0.49 μs
speedup
—
kernel mean / median / std
—
ref mean / median / std
0.49 / — / 1.04 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
0.155
dram util %
3.7%
compute util %
15.5%
bottleneck
compute-bound
dominant pipe
FP32
arithmetic intensity
—
ridge point
20.00
roofline / occupancynsight_hardware_counters
dram bandwidth
123.36 GB/s
dram util %
3.7%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
113.7%
bottleneck
compute-bound
dominant pipe %
15.5%
arithmetic intensity
—
ridge point
20.00
L1 hit %
—
L2 hit %
252.7%
ld sectors / req
54.10
st sectors / req
—
regs / thread
101
smem / block
4992 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
mio_throttle
12717.9%
short_scoreboard
7080.2%
long_scoreboard
4962.0%
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):
    """
    Triangular multiplicative update – outgoing edges.
    From AlphaFold2 Evoformer: updates pair (i,j) by aggregating over the
    shared index k via element-wise products of projected pair values along
    edges (i,k) and (j,k).
    """

    def __init__(self, pair_dim, hidden_dim):
        super().__init__()
        self.layer_norm_in = nn.LayerNorm(pair_dim)
        self.proj_a = nn.Linear(pair_dim, hidden_dim)
        self.gate_a = nn.Linear(pair_dim, hidden_dim)
        self.proj_b = nn.Linear(pair_dim, hidden_dim)
        self.gate_b = nn.Linear(pair_dim, hidden_dim)
        self.layer_norm_out = nn.LayerNorm(hidden_dim)
        self.out_proj = nn.Linear(hidden_dim, pair_dim)
        self.out_gate = nn.Linear(pair_dim, pair_dim)

    def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:
        """
        Args:
            pair_repr: (B, N, N, pair_dim)
            pair_mask: (B, N, N, 1)
        Returns:
            updated pair representation (B, N, N, pair_dim)
        """
        mask = pair_mask.unsqueeze(-1)
        x = self.layer_norm_in(pair_repr)

        a = self.proj_a(x) * torch.sigmoid(self.gate_a(x)) * mask  # (B, N, N, H)
        b = self.proj_b(x) * torch.sigmoid(self.gate_b(x)) * mask

        # Outgoing: aggregate over k with a[i,k] * b[j,k]
        out = torch.einsum("bikh,bjkh->bijh", a, b)

        out = self.layer_norm_out(out)
        out = self.out_proj(out)
        out = torch.sigmoid(self.out_gate(pair_repr)) * out
        return out


pair_dim = 64
hidden_dim = 32
seq_len = 32
batch_size = 2


def get_inputs():
    p = popcorn_pri
    pair_repr = torch.randn(p.jitter_int(batch_size), p.jitter_int(seq_len), p.jitter_int(seq_len), pair_dim)
    pair_mask = torch.ones(p.jitter_int(batch_size), p.jitter_int(seq_len), p.jitter_int(seq_len))
    return [pair_repr, pair_mask]


def get_init_inputs():
    return [pair_dim, hidden_dim]
turn 0 · FINAL · 1 tool call(s) · 55.1s turn · 55.1s LLM · 10,308 chars (click to expand)
turn 055.1s turn · 55.1s 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 triangular_forward(\n torch::Tensor pair_repr, torch::Tensor pair_mask,\n torch::Tensor ln_in_w, torch::Tensor ln_in_b,\n torch::Tensor proj_a_w, torch::Tensor proj_a_b,\n torch::Tensor gate_a_w, torch::Tensor gate_a_b,\n torch::Tensor proj_b_w, torch::Tensor proj_b_b,\n torch::Tensor gate_b_w, torch::Tensor gate_b_b,\n torch::Tensor ln_out_w, torch::Tensor ln_out_b,\n torch::Tensor out_proj_w, torch::Tensor out_proj_b,\n torch::Tensor out_gate_w, torch::Tensor out_gate_b);\n'''\n\n_cuda_src = r'''\n#include <torch/extension.h>\n#include <ATen/cuda/CUDAContext.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\nstatic __device__ __forceinline__ float sigmoidf_dev(float x) {\n return 1.0f / (1.0f + expf(-x));\n}\n\n// One block per (B,N,N) row. Computes input LayerNorm and all a/b projections.\n__global__ void compute_ab_kernel(\n const float* __restrict__ x0, const float* __restrict__ mask,\n const float* __restrict__ ln_w, const float* __restrict__ ln_b,\n const float* __restrict__ pa_w, const float* __restrict__ pa_b,\n const float* __restrict__ ga_w, const float* __restrict__ ga_b,\n const float* __restrict__ pb_w, const float* __restrict__ pb_b,\n const float* __restrict__ gb_w, const float* __restrict__ gb_b,\n float* __restrict__ a, float* __restrict__ b,\n int rows, int C, int H)\n{\n int row = blockIdx.x;\n int tid = threadIdx.x;\n __shared__ float red[128];\n __shared__ float xn[64];\n __shared__ float vals[128];\n\n float v = 0.0f;\n if (tid < C) v = x0[row * C + tid];\n red[tid] = (tid < C) ? v : 0.0f;\n __syncthreads();\n for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }\n float mean = red[0] * (1.0f / (float)C);\n\n float d = 0.0f;\n if (tid < C) { float t = v - mean; d = t * t; }\n red[tid] = (tid < C) ? d : 0.0f;\n __syncthreads();\n for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }\n float rstd = rsqrtf(red[0] * (1.0f / (float)C) + 1.0e-5f);\n\n if (tid < C) xn[tid] = (v - mean) * rstd * ln_w[tid] + ln_b[tid];\n __syncthreads();\n\n int h = tid & 31;\n int which = tid >> 5;\n if (h < H) {\n const float* w;\n const float* bias;\n if (which == 0) { w = pa_w; bias = pa_b; }\n else if (which == 1) { w = ga_w; bias = ga_b; }\n else if (which == 2) { w = pb_w; bias = pb_b; }\n else { w = gb_w; bias = gb_b; }\n float sum = bias[h];\n const float* wh = w + h * C;\n #pragma unroll\n for (int c = 0; c < 64; ++c) sum += wh[c] * xn[c];\n vals[tid] = sum;\n }\n __syncthreads();\n\n if (tid < H) {\n float m = mask[row];\n float av = vals[tid] * sigmoidf_dev(vals[32 + tid]) * m;\n float bv = vals[64 + tid] * sigmoidf_dev(vals[96 + tid]) * m;\n a[row * H + tid] = av;\n b[row * H + tid] = bv;\n }\n}\n\n// One thread per (b,i,j,h); sum over k.\n__global__ void einsum_kernel(const float* __restrict__ a, const float* __restrict__ b,\n float* __restrict__ out, int B, int N, int H) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = B * N * N * H;\n if (idx >= total) return;\n int h = idx % H;\n int t = idx / H;\n int j = t % N;\n t /= N;\n int i = t % N;\n int bb = t / N;\n float sum = 0.0f;\n int base_a = ((bb * N + i) * N) * H + h;\n int base_b = ((bb * N + j) * N) * H + h;\n #pragma unroll\n for (int k = 0; k < 32; ++k) {\n sum += a[base_a + k * H] * b[base_b + k * H];\n }\n out[idx] = sum;\n}\n\n// One block per output row. LayerNorm over H, out projection, out gate, multiply.\n__global__ void final_kernel(\n const float* __restrict__ mid, const float* __restrict__ pair_repr,\n const float* __restrict__ ln_w, const float* __restrict__ ln_b,\n const float* __restrict__ op_w, const float* __restrict__ op_b,\n const float* __restrict__ og_w, const float* __restrict__ og_b,\n float* __restrict__ out, int rows, int C, int H)\n{\n int row = blockIdx.x;\n int tid = threadIdx.x;\n __shared__ float red[128];\n __shared__ float yn[32];\n\n float v = 0.0f;\n if (tid < H) v = mid[row * H + tid];\n red[tid] = (tid < H) ? v : 0.0f;\n __syncthreads();\n for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }\n float mean = red[0] * (1.0f / (float)H);\n\n float d = 0.0f;\n if (tid < H) { float t = v - mean; d = t * t; }\n red[tid] = (tid < H) ? d : 0.0f;\n __syncthreads();\n for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }\n float rstd = rsqrtf(red[0] * (1.0f / (float)H) + 1.0e-5f);\n if (tid < H) yn[tid] = (v - mean) * rstd * ln_w[tid] + ln_b[tid];\n __syncthreads();\n\n if (tid < C) {\n int c = tid;\n float pv = op_b[c];\n const float* ow = op_w + c * H;\n #pragma unroll\n for (int h = 0; h < 32; ++h) pv += ow[h] * yn[h];\n\n float gv = og_b[c];\n const float* gw = og_w + c * C;\n const float* xr = pair_repr + row * C;\n #pragma unroll\n for (int k = 0; k < 64; ++k) gv += gw[k] * xr[k];\n out[row * C + c] = sigmoidf_dev(gv) * pv;\n }\n}\n\ntorch::Tensor triangular_forward(\n torch::Tensor pair_repr, torch::Tensor pair_mask,\n torch::Tensor ln_in_w, torch::Tensor ln_in_b,\n torch::Tensor proj_a_w, torch::Tensor proj_a_b,\n torch::Tensor gate_a_w, torch::Tensor gate_a_b,\n torch::Tensor proj_b_w, torch::Tensor proj_b_b,\n torch::Tensor gate_b_w, torch::Tensor gate_b_b,\n torch::Tensor ln_out_w, torch::Tensor ln_out_b,\n torch::Tensor out_proj_w, torch::Tensor out_proj_b,\n torch::Tensor out_gate_w, torch::Tensor out_gate_b)\n{\n const int B = pair_repr.size(0);\n const int N = pair_repr.size(1);\n const int C = pair_repr.size(3);\n const int H = proj_a_w.size(0);\n const int rows = B * N * N;\n auto optsH = pair_repr.options();\n auto a = torch::empty({B, N, N, H}, optsH);\n auto b = torch::empty({B, N, N, H}, optsH);\n auto mid = torch::empty({B, N, N, H}, optsH);\n auto out = torch::empty_like(pair_repr);\n cudaStream_t stream = at::cuda::getCurrentCUDAStream();\n\n compute_ab_kernel<<<rows, 128, 0, stream>>>(\n pair_repr.data_ptr<float>(), pair_mask.data_ptr<float>(),\n ln_in_w.data_ptr<float>(), ln_in_b.data_ptr<float>(),\n proj_a_w.data_ptr<float>(), proj_a_b.data_ptr<float>(),\n gate_a_w.data_ptr<float>(), gate_a_b.data_ptr<float>(),\n proj_b_w.data_ptr<float>(), proj_b_b.data_ptr<float>(),\n gate_b_w.data_ptr<float>(), gate_b_b.data_ptr<float>(),\n a.data_ptr<float>(), b.data_ptr<float>(), rows, C, H);\n\n int total = B * N * N * H;\n einsum_kernel<<<(total + 255) / 256, 256, 0, stream>>>(a.data_ptr<float>(), b.data_ptr<float>(), mid.data_ptr<float>(), B, N, H);\n\n final_kernel<<<rows, 128, 0, stream>>>(\n mid.data_ptr<float>(), pair_repr.data_ptr<float>(),\n ln_out_w.data_ptr<float>(), ln_out_b.data_ptr<float>(),\n out_proj_w.data_ptr<float>(), out_proj_b.data_ptr<float>(),\n out_gate_w.data_ptr<float>(), out_gate_b.data_ptr<float>(),\n out.data_ptr<float>(), rows, C, H);\n return out;\n}\n'''\n\n_ext = load_inline(\n name='tri_mul_outgoing_ext_v1',\n cpp_sources=_cpp_src,\n cuda_sources=_cuda_src,\n functions=['triangular_forward'],\n extra_cflags=['-O3'],\n extra_cuda_cflags=['-O3'],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, hidden_dim):\n super().__init__()\n self.layer_norm_in = nn.LayerNorm(pair_dim)\n self.proj_a = nn.Linear(pair_dim, hidden_dim)\n self.gate_a = nn.Linear(pair_dim, hidden_dim)\n self.proj_b = nn.Linear(pair_dim, hidden_dim)\n self.gate_b = nn.Linear(pair_dim, hidden_dim)\n self.layer_norm_out = nn.LayerNorm(hidden_dim)\n self.out_proj = nn.Linear(hidden_dim, pair_dim)\n self.out_gate = nn.Linear(pair_dim, pair_dim)\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n return _ext.triangular_forward(\n pair_repr, pair_mask,\n self.layer_norm_in.weight, self.layer_norm_in.bias,\n self.proj_a.weight, self.proj_a.bias,\n self.gate_a.weight, self.gate_a.bias,\n self.proj_b.weight, self.proj_b.bias,\n self.gate_b.weight, self.gate_b.bias,\n self.layer_norm_out.weight, self.layer_norm_out.bias,\n self.out_proj.weight, self.out_proj.bias,\n self.out_gate.weight, self.out_gate.bias,\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.117905', '0.103265', '0.109587', '0.113593', '0.083498'] avg_difference: ['0.003129', '0.003052', '0.003265', '0.003430', '0.002669']
final submitted kernel (236 lines, 8,751 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 triangular_forward(
    torch::Tensor pair_repr, torch::Tensor pair_mask,
    torch::Tensor ln_in_w, torch::Tensor ln_in_b,
    torch::Tensor proj_a_w, torch::Tensor proj_a_b,
    torch::Tensor gate_a_w, torch::Tensor gate_a_b,
    torch::Tensor proj_b_w, torch::Tensor proj_b_b,
    torch::Tensor gate_b_w, torch::Tensor gate_b_b,
    torch::Tensor ln_out_w, torch::Tensor ln_out_b,
    torch::Tensor out_proj_w, torch::Tensor out_proj_b,
    torch::Tensor out_gate_w, torch::Tensor out_gate_b);
'''

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

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

// One block per (B,N,N) row. Computes input LayerNorm and all a/b projections.
__global__ void compute_ab_kernel(
    const float* __restrict__ x0, const float* __restrict__ mask,
    const float* __restrict__ ln_w, const float* __restrict__ ln_b,
    const float* __restrict__ pa_w, const float* __restrict__ pa_b,
    const float* __restrict__ ga_w, const float* __restrict__ ga_b,
    const float* __restrict__ pb_w, const float* __restrict__ pb_b,
    const float* __restrict__ gb_w, const float* __restrict__ gb_b,
    float* __restrict__ a, float* __restrict__ b,
    int rows, int C, int H)
{
    int row = blockIdx.x;
    int tid = threadIdx.x;
    __shared__ float red[128];
    __shared__ float xn[64];
    __shared__ float vals[128];

    float v = 0.0f;
    if (tid < C) v = x0[row * C + tid];
    red[tid] = (tid < C) ? v : 0.0f;
    __syncthreads();
    for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }
    float mean = red[0] * (1.0f / (float)C);

    float d = 0.0f;
    if (tid < C) { float t = v - mean; d = t * t; }
    red[tid] = (tid < C) ? d : 0.0f;
    __syncthreads();
    for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }
    float rstd = rsqrtf(red[0] * (1.0f / (float)C) + 1.0e-5f);

    if (tid < C) xn[tid] = (v - mean) * rstd * ln_w[tid] + ln_b[tid];
    __syncthreads();

    int h = tid & 31;
    int which = tid >> 5;
    if (h < H) {
        const float* w;
        const float* bias;
        if (which == 0) { w = pa_w; bias = pa_b; }
        else if (which == 1) { w = ga_w; bias = ga_b; }
        else if (which == 2) { w = pb_w; bias = pb_b; }
        else { w = gb_w; bias = gb_b; }
        float sum = bias[h];
        const float* wh = w + h * C;
        #pragma unroll
        for (int c = 0; c < 64; ++c) sum += wh[c] * xn[c];
        vals[tid] = sum;
    }
    __syncthreads();

    if (tid < H) {
        float m = mask[row];
        float av = vals[tid] * sigmoidf_dev(vals[32 + tid]) * m;
        float bv = vals[64 + tid] * sigmoidf_dev(vals[96 + tid]) * m;
        a[row * H + tid] = av;
        b[row * H + tid] = bv;
    }
}

// One thread per (b,i,j,h); sum over k.
__global__ void einsum_kernel(const float* __restrict__ a, const float* __restrict__ b,
                              float* __restrict__ out, int B, int N, int H) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int total = B * N * N * H;
    if (idx >= total) return;
    int h = idx % H;
    int t = idx / H;
    int j = t % N;
    t /= N;
    int i = t % N;
    int bb = t / N;
    float sum = 0.0f;
    int base_a = ((bb * N + i) * N) * H + h;
    int base_b = ((bb * N + j) * N) * H + h;
    #pragma unroll
    for (int k = 0; k < 32; ++k) {
        sum += a[base_a + k * H] * b[base_b + k * H];
    }
    out[idx] = sum;
}

// One block per output row. LayerNorm over H, out projection, out gate, multiply.
__global__ void final_kernel(
    const float* __restrict__ mid, const float* __restrict__ pair_repr,
    const float* __restrict__ ln_w, const float* __restrict__ ln_b,
    const float* __restrict__ op_w, const float* __restrict__ op_b,
    const float* __restrict__ og_w, const float* __restrict__ og_b,
    float* __restrict__ out, int rows, int C, int H)
{
    int row = blockIdx.x;
    int tid = threadIdx.x;
    __shared__ float red[128];
    __shared__ float yn[32];

    float v = 0.0f;
    if (tid < H) v = mid[row * H + tid];
    red[tid] = (tid < H) ? v : 0.0f;
    __syncthreads();
    for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }
    float mean = red[0] * (1.0f / (float)H);

    float d = 0.0f;
    if (tid < H) { float t = v - mean; d = t * t; }
    red[tid] = (tid < H) ? d : 0.0f;
    __syncthreads();
    for (int s = 64; s > 0; s >>= 1) { if (tid < s) red[tid] += red[tid+s]; __syncthreads(); }
    float rstd = rsqrtf(red[0] * (1.0f / (float)H) + 1.0e-5f);
    if (tid < H) yn[tid] = (v - mean) * rstd * ln_w[tid] + ln_b[tid];
    __syncthreads();

    if (tid < C) {
        int c = tid;
        float pv = op_b[c];
        const float* ow = op_w + c * H;
        #pragma unroll
        for (int h = 0; h < 32; ++h) pv += ow[h] * yn[h];

        float gv = og_b[c];
        const float* gw = og_w + c * C;
        const float* xr = pair_repr + row * C;
        #pragma unroll
        for (int k = 0; k < 64; ++k) gv += gw[k] * xr[k];
        out[row * C + c] = sigmoidf_dev(gv) * pv;
    }
}

torch::Tensor triangular_forward(
    torch::Tensor pair_repr, torch::Tensor pair_mask,
    torch::Tensor ln_in_w, torch::Tensor ln_in_b,
    torch::Tensor proj_a_w, torch::Tensor proj_a_b,
    torch::Tensor gate_a_w, torch::Tensor gate_a_b,
    torch::Tensor proj_b_w, torch::Tensor proj_b_b,
    torch::Tensor gate_b_w, torch::Tensor gate_b_b,
    torch::Tensor ln_out_w, torch::Tensor ln_out_b,
    torch::Tensor out_proj_w, torch::Tensor out_proj_b,
    torch::Tensor out_gate_w, torch::Tensor out_gate_b)
{
    const int B = pair_repr.size(0);
    const int N = pair_repr.size(1);
    const int C = pair_repr.size(3);
    const int H = proj_a_w.size(0);
    const int rows = B * N * N;
    auto optsH = pair_repr.options();
    auto a = torch::empty({B, N, N, H}, optsH);
    auto b = torch::empty({B, N, N, H}, optsH);
    auto mid = torch::empty({B, N, N, H}, optsH);
    auto out = torch::empty_like(pair_repr);
    cudaStream_t stream = at::cuda::getCurrentCUDAStream();

    compute_ab_kernel<<<rows, 128, 0, stream>>>(
        pair_repr.data_ptr<float>(), pair_mask.data_ptr<float>(),
        ln_in_w.data_ptr<float>(), ln_in_b.data_ptr<float>(),
        proj_a_w.data_ptr<float>(), proj_a_b.data_ptr<float>(),
        gate_a_w.data_ptr<float>(), gate_a_b.data_ptr<float>(),
        proj_b_w.data_ptr<float>(), proj_b_b.data_ptr<float>(),
        gate_b_w.data_ptr<float>(), gate_b_b.data_ptr<float>(),
        a.data_ptr<float>(), b.data_ptr<float>(), rows, C, H);

    int total = B * N * N * H;
    einsum_kernel<<<(total + 255) / 256, 256, 0, stream>>>(a.data_ptr<float>(), b.data_ptr<float>(), mid.data_ptr<float>(), B, N, H);

    final_kernel<<<rows, 128, 0, stream>>>(
        mid.data_ptr<float>(), pair_repr.data_ptr<float>(),
        ln_out_w.data_ptr<float>(), ln_out_b.data_ptr<float>(),
        out_proj_w.data_ptr<float>(), out_proj_b.data_ptr<float>(),
        out_gate_w.data_ptr<float>(), out_gate_b.data_ptr<float>(),
        out.data_ptr<float>(), rows, C, H);
    return out;
}
'''

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

class ModelNew(nn.Module):
    def __init__(self, pair_dim, hidden_dim):
        super().__init__()
        self.layer_norm_in = nn.LayerNorm(pair_dim)
        self.proj_a = nn.Linear(pair_dim, hidden_dim)
        self.gate_a = nn.Linear(pair_dim, hidden_dim)
        self.proj_b = nn.Linear(pair_dim, hidden_dim)
        self.gate_b = nn.Linear(pair_dim, hidden_dim)
        self.layer_norm_out = nn.LayerNorm(hidden_dim)
        self.out_proj = nn.Linear(hidden_dim, pair_dim)
        self.out_gate = nn.Linear(pair_dim, pair_dim)

    def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:
        return _ext.triangular_forward(
            pair_repr, pair_mask,
            self.layer_norm_in.weight, self.layer_norm_in.bias,
            self.proj_a.weight, self.proj_a.bias,
            self.gate_a.weight, self.gate_a.bias,
            self.proj_b.weight, self.proj_b.bias,
            self.gate_b.weight, self.gate_b.bias,
            self.layer_norm_out.weight, self.layer_norm_out.bias,
            self.out_proj.weight, self.out_proj.bias,
            self.out_gate.weight, self.out_gate.bias,
        )