FW-GLM-5-1 · popcorn · pop_l123_all_gpt
correct 6_TriangularAttention.py
model
FW-GLM-5-1
turns / max
15 / 15
tool calls
15
tokens (total)
465017
wall clock
1254.2s
truncated
no
backend
cuda fp32
variant
popcorn
runtime
0.30 μs
ref runtime
0.48 μs
speedup
1.60x
started
2026-05-11T19:45:29Z
timingkernelbench.timing
kernel runtime
0.30 μs
ref runtime
0.48 μs
speedup
1.60x
kernel mean / median / std
0.30 / — / 0.09 μs
ref mean / median / std
0.48 / — / 0.14 μ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
38.23 MB
ref peak
38.20 MB
kernel / ref
1.00x
kernel launches / fusiontorch.profiler
kernel launches
106
ref launches
142
fusion ratio (ref / kernel)
1.34x
total cuda time
1937 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
0.557
dram util %
55.7%
compute util %
6.5%
bottleneck
memory-bound
dominant pipe
tensor-core
arithmetic intensity
—
ridge point
295.22
roofline / occupancynsight_hardware_counters
dram bandwidth
1866.18 GB/s
dram util %
55.7%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
230.7%
bottleneck
memory-bound
dominant pipe %
6.5%
arithmetic intensity
—
ridge point
295.22
L1 hit %
—
L2 hit %
1272.3%
ld sectors / req
120.49
st sectors / req
—
regs / thread
785
smem / block
62848 B
block size
1488
peak bw
3350.00 GB/s
peak fp32 tflops
67.00
peak fp16 tflops
1979.00
warp stalls (top 5)nsight
long_scoreboard
11021.5%
short_scoreboard
1797.8%
mio_throttle
153.6%
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn
import torch.nn.functional as F
import math


class Model(nn.Module):
    """
    Triangular self-attention over pair representations, as used in
    AlphaFold2's Evoformer stack.  Starting-node variant: for each
    row i of the pair matrix, standard multi-head self-attention is
    performed across the column dimension j, with an additive pair
    bias.  This lets positions (i,j) and (i,k) exchange information
    through their shared starting node i.
    """

    def __init__(self, pair_dim, num_heads, dropout=0.0):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = pair_dim // num_heads
        assert pair_dim % num_heads == 0

        self.layer_norm = nn.LayerNorm(pair_dim)
        self.query = nn.Linear(pair_dim, pair_dim, bias=False)
        self.key = nn.Linear(pair_dim, pair_dim, bias=False)
        self.value = nn.Linear(pair_dim, pair_dim, bias=False)
        self.gate = nn.Linear(pair_dim, pair_dim)
        self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)
        self.out_proj = nn.Linear(pair_dim, pair_dim)
        self.dropout = nn.Dropout(dropout)

    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)
        Returns:
            updated pair representation (B, N, N, pair_dim)
        """
        B, N, _, D = pair_repr.shape
        h = self.num_heads
        d = self.head_dim

        x = self.layer_norm(pair_repr)
        g = torch.sigmoid(self.gate(x))

        # Pair bias from the pair representation itself
        bias = self.bias_proj(x).permute(0, 3, 1, 2)  # (B, h, N, N)

        # Reshape to (B*N, N, D) — attention across columns for each row
        x_flat = x.reshape(B * N, N, D)
        q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2)  # (B*N, h, N, d)
        k = self.key(x_flat).view(B * N, N, h, d).transpose(1, 2)
        v = self.value(x_flat).view(B * N, N, h, d).transpose(1, 2)

        attn = (q @ k.transpose(-1, -2)) / math.sqrt(d)  # (B*N, h, N, N)
        attn = attn.view(B, N, h, N, N)

        # Add pair bias: bias (B, h, N, N) → (B, 1, h, N, N) broadcast across rows
        attn = attn + bias.unsqueeze(1)

        # Apply mask: pair_mask (B, N, N) → mask over the key dimension
        mask = pair_mask.unsqueeze(1).unsqueeze(2)  # (B, 1, 1, N, N) – last dim is keys
        attn = attn.masked_fill(mask == 0, float("-inf"))
        attn = attn.view(B * N, h, N, N)
        attn = F.softmax(attn, dim=-1)
        attn = self.dropout(attn)

        out = (attn @ v).transpose(1, 2).reshape(B, N, N, D)
        out = g * out
        return self.out_proj(out)


pair_dim = 64
num_heads = 4
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, num_heads]
turn 013.5s turn · 7.5s LLM · 5.9s tools · tokens in=2,685 · out=543 · total=3,228
reasoning
Let me analyze this task. I need to implement a custom CUDA kernel for the "Triangular self-attention over pair representations" from AlphaFold2's Evoformer. Let me understand the forward computation: 1. Input: `pair_repr` (B, N, N, D), `pair_mask` (B, N, N) 2. LayerNorm on pair_repr 3. Compute gate: g = sigmoid(gate(x)) 4. Compute bias: bias_proj(x) -> (B, h, N, N) 5. Reshape x to (B*N, N, D) 6. Compute Q, K, V projections 7. Compute attention scores: Q @ K.T / sqrt(d) -> (B*N, h, N, N) 8. Reshape attention to (B, N, h, N, N) 9. Add bias (broadcasting) 10. Apply mask 11. Softmax 12. Dropout 13. Compute output: attn @ V -> (B, N, N, D) 14. Apply gate: g * out 15. Output projection This is a complex operation. Let me think about how to optimize this with CUDA. Key operations: 1. LayerNorm - could use a CUDA kernel 2. Linear projections (query, key, value, gate, bias_proj, out_proj) - matrix multiplications 3. Sigmoid for gate 4. Attention computation (Q @ K.T) 5. Softmax 6. Attn @ V 7. Element-wise multiplication for gating The main challenge is that this involves multiple operations, and some are hard to fuse efficiently. Let me think about what can be fused: Option 1: Write a single fused kernel for everything - This would be very complex and might have low parallelism for certain operations - Memory intensive due to intermediate results Option 2: Fuse specific operations and keep others as PyTorch ops - LayerNorm + projections might be fusable - Attention + softmax could be fused - The gating mechanism requires careful synchronization I see multiple challenges with kernel design. Memory access patterns and synchronization will be critical. Fusing operations could reduce memory bandwidth, but introduces complexity in implementation. The forward pass seems to involve nuanced reshaping strategies that impact computational efficiency. Batched matrix multiplications across rows suggest potential optimization opportunities, especially considering the unique tensor layout constraints. Flash Attention emerges as a promising optimization technique, particularly given the manageable sequence length of N=32. The implementation appears to require careful attention to tensor transformations and projection strategies. I'll investigate GPU specifications to understand potential performance improvements.
call → get_gpu_specs
callget_gpu_specs {}
result ← get_gpu_specs (ok)
okget_gpu_specs GPU specs for NVIDIA H100 80GB HBM3: total memory (runtime): 79.1 GB spec entry: H100 GPU Architecture: Hopper GPU Memory: 80GB Memory Bandwidth: 3.35 TB/s FP64 TFLOPS: 34 FP64 Tensor Core TFLOPS: 67 FP32 TFLOPS: 67 TF32 Tensor Core TFLOPS: 989 with sparsity BFLOAT16 Tensore Core TFLOPS: 1979 with sparsity FP16 Tensor Core TFLOPS: 1979 with sparsity FP8 Tensor Core TFLOPS: 3958 with sparsity INT8 Tensor Core TOPS: 3958 with sparsity Register File Size: 64K 32-bit registers per SM Maximum number of registers per thread: 255 Maximum number of thread blocks per SM: 32 Shared memory capacity per SM: 228 KB Maximum shared memory per thread block: 227 KB
turn 1 · 1 tool call(s) · 111.4s turn · 34.8s LLM · tokens in=2,933 · out=3,762 · total=6,695 · 15,446 chars (click to expand)
turn 1111.4s turn · 34.8s LLM · 76.7s tools · tokens in=2,933 · out=3,762 · total=6,695
reasoning
Now let me write a custom CUDA kernel for this triangular self-attention. The key operations are: 1. LayerNorm 2. Query, Key, Value projections (linear layers) 3. Gate projection (linear + sigmoid) 4. Bias projection (linear) 5. Attention computation (Q @ K^T) 6. Add bias 7. Apply mask and softmax 8. Attention @ V 9. Gate multiplication 10. Output projection Let me write a fused kernel that handles the attention computation efficiently. I'll need to carefully manage memory access patterns and use shared memory for the attention matrix. Let me start with a basic implementation and then optimize it.
assistantLet me write a custom CUDA kernel for this triangular self-attention operation. I'll start by writing a complete implementation that fuses the key operations.
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\n# Define CUDA source for triangular self-attention\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\nconstexpr int BLOCK_SIZE = 16;\nconstexpr int TILE_SIZE = 16;\n\n__device__ __forceinline__ float warp_reduce_max(float val) {\n for (int offset = 16; offset > 0; offset >>= 1) {\n val = fmaxf(val, __shfl_xor_sync(0xffffffff, val, offset));\n }\n return val;\n}\n\n__device__ __forceinline__ float warp_reduce_sum(float val) {\n for (int offset = 16; offset > 0; offset >>= 1) {\n val += __shfl_xor_sync(0xffffffff, val, offset);\n }\n return val;\n}\n\n// LayerNorm kernel\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int B, int N, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = B * N;\n if (idx >= total) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Fused projection + reshape kernel\n__global__ void fused_proj_kernel(\n const float* __restrict__ input,\n float* __restrict__ query,\n float* __restrict__ key,\n float* __restrict__ value,\n float* __restrict__ gate,\n float* __restrict__ bias,\n const float* __restrict__ w_q,\n const float* __restrict__ w_k,\n const float* __restrict__ w_v,\n const float* __restrict__ w_g,\n const float* __restrict__ b_g,\n const float* __restrict__ w_bias,\n int B, int N, int D, int num_heads, int head_dim\n) {\n int bn_idx = blockIdx.x;\n int d_idx = threadIdx.x;\n \n int total_bn = B * N;\n if (bn_idx >= total_bn) return;\n \n int b = bn_idx / N;\n int n = bn_idx % N;\n \n const float* in = input + bn_idx * D;\n \n // Each thread handles one output dimension\n if (d_idx < D) {\n // Query projection\n float q_val = 0.0f;\n for (int i = 0; i < D; i++) {\n q_val += in[i] * w_q[d_idx * D + i];\n }\n int h = d_idx / head_dim;\n int hd = d_idx % head_dim;\n query[(bn_idx * num_heads + h) * N * head_dim + n * head_dim + hd] = q_val;\n \n // Key projection\n float k_val = 0.0f;\n for (int i = 0; i < D; i++) {\n k_val += in[i] * w_k[d_idx * D + i];\n }\n key[(bn_idx * num_heads + h) * N * head_dim + n * head_dim + hd] = k_val;\n \n // Value projection\n float v_val = 0.0f;\n for (int i = 0; i < D; i++) {\n v_val += in[i] * w_v[d_idx * D + i];\n }\n value[(bn_idx * num_heads + h) * N * head_dim + n * head_dim + hd] = v_val;\n \n // Gate projection\n float g_val = b_g[d_idx];\n for (int i = 0; i < D; i++) {\n g_val += in[i] * w_g[d_idx * D + i];\n }\n gate[bn_idx * D + d_idx] = 1.0f / (1.0f + expf(-g_val));\n }\n \n // Bias projection (only num_heads dimensions)\n if (d_idx < num_heads) {\n float bias_val = 0.0f;\n for (int i = 0; i < D; i++) {\n bias_val += in[i] * w_bias[d_idx * D + i];\n }\n bias[(b * num_heads + d_idx) * N * N + n * N + n] = bias_val;\n }\n}\n\n// Attention kernel\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const int* __restrict__ mask,\n float* __restrict__ attn_out,\n float scale,\n int B, int N, int num_heads, int head_dim\n) {\n int b = blockIdx.x / N;\n int row = blockIdx.x % N;\n int h = blockIdx.y;\n \n if (b >= B || row >= N || h >= num_heads) return;\n \n int bn_idx = b * N + row;\n int q_offset = (bn_idx * num_heads + h) * N * head_dim;\n \n extern __shared__ float shared_mem[];\n float* k_cache = shared_mem; // [N, head_dim]\n float* v_cache = &shared_mem[N * head_dim]; // [N, head_dim]\n float* attn_scores = &shared_mem[2 * N * head_dim]; // [N]\n \n int tid = threadIdx.x;\n \n // Load Q for this row (one thread per head_dim element)\n float q_local[64]; // assuming head_dim <= 64\n for (int d = tid; d < head_dim; d += blockDim.x) {\n q_local[d] = query[q_offset + row * head_dim + d];\n }\n __syncthreads();\n \n // Compute attention scores for each key position\n float max_score = -INFINITY;\n \n for (int k_pos = 0; k_pos < N; k_pos++) {\n // Load K and V for key position k_pos\n for (int d = tid; d < head_dim; d += blockDim.x) {\n int k_offset = (bn_idx * num_heads + h) * N * head_dim + k_pos * head_dim + d;\n k_cache[k_pos * head_dim + d] = key[k_offset];\n v_cache[k_pos * head_dim + d] = value[k_offset];\n }\n __syncthreads();\n \n // Compute dot product Q[row] . K[k_pos]\n if (tid == 0) {\n float score = 0.0f;\n for (int d = 0; d < head_dim; d++) {\n score += q_local[d] * k_cache[k_pos * head_dim + d];\n }\n score *= scale;\n \n // Add bias\n score += bias[(b * num_heads + h) * N * N + row * N + k_pos];\n \n // Apply mask\n if (mask[b * N * N + row * N + k_pos] == 0) {\n score = -INFINITY;\n }\n \n attn_scores[k_pos] = score;\n max_score = fmaxf(max_score, score);\n }\n __syncthreads();\n }\n \n // Softmax\n if (tid == 0) {\n float sum_exp = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float exp_val = expf(attn_scores[k_pos] - max_score);\n attn_scores[k_pos] = exp_val;\n sum_exp += exp_val;\n }\n float inv_sum = 1.0f / sum_exp;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n attn_scores[k_pos] *= inv_sum;\n }\n }\n __syncthreads();\n \n // Compute weighted sum of V\n float out_local[64];\n for (int d = tid; d < head_dim; d += blockDim.x) {\n float val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n val += attn_scores[k_pos] * v_cache[k_pos * head_dim + d];\n }\n out_local[d - tid] = val;\n }\n __syncthreads();\n \n // Store output\n for (int d = tid; d < head_dim; d += blockDim.x) {\n int out_offset = (b * N + row) * N * num_heads * head_dim + row * num_heads * head_dim + h * head_dim + d;\n attn_out[out_offset] = out_local[d - tid];\n }\n}\n\n// Output projection kernel\n__global__ void output_proj_kernel(\n const float* __restrict__ attn_out,\n const float* __restrict__ gate,\n const float* __restrict__ w_out,\n const float* __restrict__ b_out,\n float* __restrict__ output,\n int B, int N, int D, int num_heads, int head_dim\n) {\n int bn_idx = blockIdx.x;\n int d_idx = threadIdx.x;\n \n if (bn_idx >= B * N || d_idx >= D) return;\n \n int b = bn_idx / N;\n int n = bn_idx % N;\n \n // Read gated attention output\n float g = gate[bn_idx * D + d_idx];\n \n // Reshape and apply gate\n int h = d_idx / head_dim;\n int hd = d_idx % head_dim;\n float val = attn_out[bn_idx * N * D + n * D + d_idx] * g;\n \n // Output projection\n float out_val = b_out[d_idx];\n for (int i = 0; i < D; i++) {\n out_val += val * w_out[d_idx * D + i];\n }\n \n output[bn_idx * D + d_idx] = out_val;\n}\n\n// Host functions\nstd::vector<torch::Tensor> triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n \n auto options = pair_repr.options();\n \n // Allocate output tensors\n auto output = torch::empty({B, N, N, D}, options);\n auto normed = torch::empty({B * N, N, D}, options);\n auto query = torch::empty({B * N, num_heads, N, head_dim}, options);\n auto key = torch::empty({B * N, num_heads, N, head_dim}, options);\n auto value = torch::empty({B * N, num_heads, N, head_dim}, options);\n auto gate = torch::empty({B, N, N, D}, options);\n auto bias = torch::empty({B, num_heads, N, N}, options);\n \n int threads = 256;\n int blocks = (B * N + threads - 1) / threads;\n \n // LayerNorm\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n normed.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n B, N, D, 1e-5f\n );\n \n // Fused projections\n fused_proj_kernel<<<B * N, D>>>(\n normed.data_ptr<float>(),\n query.data_ptr<float>(),\n key.data_ptr<float>(),\n value.data_ptr<float>(),\n gate.data_ptr<float>(),\n bias.data_ptr<float>(),\n w_query.data_ptr<float>(),\n w_key.data_ptr<float>(),\n w_value.data_ptr<float>(),\n w_gate.data_ptr<float>(),\n b_gate.data_ptr<float>(),\n w_bias.data_ptr<float>(),\n B, N, D, num_heads, head_dim\n );\n \n // Attention\n size_t shared_mem_size = 2 * N * head_dim * sizeof(float) + N * sizeof(float);\n dim3 grid(B * N, num_heads);\n attention_kernel<<<grid, head_dim, shared_mem_size>>>(\n query.data_ptr<float>(),\n key.data_ptr<float>(),\n value.data_ptr<float>(),\n bias.data_ptr<float>(),\n pair_mask.to(torch::kInt).data_ptr<int>(),\n output.data_ptr<float>(),\n 1.0f / sqrtf((float)head_dim),\n B, N, num_heads, head_dim\n );\n \n return {output, gate};\n}\n\"\"\"\n\ncpp_source = \"\"\"\nstd::vector<torch::Tensor> triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\n# Load the CUDA extension\ntriangular_attn = load_inline(\n name='triangular_attention',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Triangular self-attention with custom CUDA kernel\n \"\"\"\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n # Ensure contiguous and correct dtype\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous()\n\n # Call custom CUDA kernel\n outputs = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n result = outputs[0]\n gate = outputs[1]\n \n return result" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 2 · 1 tool call(s) · 28.2s turn · 28.2s LLM · tokens in=6,572 · out=3,607 · total=10,179 · 14,868 chars (click to expand)
turn 228.2s turn · 28.2s LLM · 0.0s tools · tokens in=6,572 · out=3,607 · total=10,179
reasoning
The kernel compiled, now let me check correctness.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\n# Define CUDA source for triangular self-attention\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\nconstexpr int BLOCK_SIZE = 16;\nconstexpr int TILE_SIZE = 16;\n\n__device__ __forceinline__ float warp_reduce_max(float val) {\n for (int offset = 16; offset > 0; offset >>= 1) {\n val = fmaxf(val, __shfl_xor_sync(0xffffffff, val, offset));\n }\n return val;\n}\n\n__device__ __forceinline__ float warp_reduce_sum(float val) {\n for (int offset = 16; offset > 0; offset >>= 1) {\n val += __shfl_xor_sync(0xffffffff, val, offset);\n }\n return val;\n}\n\n// LayerNorm kernel\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int B, int N, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = B * N;\n if (idx >= total) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Fused projection + reshape kernel\n__global__ void fused_proj_kernel(\n const float* __restrict__ input,\n float* __restrict__ query,\n float* __restrict__ key,\n float* __restrict__ value,\n float* __restrict__ gate,\n float* __restrict__ bias,\n const float* __restrict__ w_q,\n const float* __restrict__ w_k,\n const float* __restrict__ w_v,\n const float* __restrict__ w_g,\n const float* __restrict__ b_g,\n const float* __restrict__ w_bias,\n int B, int N, int D, int num_heads, int head_dim\n) {\n int bn_idx = blockIdx.x;\n int d_idx = threadIdx.x;\n \n int total_bn = B * N;\n if (bn_idx >= total_bn) return;\n \n int b = bn_idx / N;\n int n = bn_idx % N;\n \n const float* in = input + bn_idx * D;\n \n // Each thread handles one output dimension\n if (d_idx < D) {\n // Query projection\n float q_val = 0.0f;\n for (int i = 0; i < D; i++) {\n q_val += in[i] * w_q[d_idx * D + i];\n }\n int h = d_idx / head_dim;\n int hd = d_idx % head_dim;\n query[(bn_idx * num_heads + h) * N * head_dim + n * head_dim + hd] = q_val;\n \n // Key projection\n float k_val = 0.0f;\n for (int i = 0; i < D; i++) {\n k_val += in[i] * w_k[d_idx * D + i];\n }\n key[(bn_idx * num_heads + h) * N * head_dim + n * head_dim + hd] = k_val;\n \n // Value projection\n float v_val = 0.0f;\n for (int i = 0; i < D; i++) {\n v_val += in[i] * w_v[d_idx * D + i];\n }\n value[(bn_idx * num_heads + h) * N * head_dim + n * head_dim + hd] = v_val;\n \n // Gate projection\n float g_val = b_g[d_idx];\n for (int i = 0; i < D; i++) {\n g_val += in[i] * w_g[d_idx * D + i];\n }\n gate[bn_idx * D + d_idx] = 1.0f / (1.0f + expf(-g_val));\n }\n \n // Bias projection (only num_heads dimensions)\n if (d_idx < num_heads) {\n float bias_val = 0.0f;\n for (int i = 0; i < D; i++) {\n bias_val += in[i] * w_bias[d_idx * D + i];\n }\n bias[(b * num_heads + d_idx) * N * N + n * N + n] = bias_val;\n }\n}\n\n// Attention kernel\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const int* __restrict__ mask,\n float* __restrict__ attn_out,\n float scale,\n int B, int N, int num_heads, int head_dim\n) {\n int b = blockIdx.x / N;\n int row = blockIdx.x % N;\n int h = blockIdx.y;\n \n if (b >= B || row >= N || h >= num_heads) return;\n \n int bn_idx = b * N + row;\n int q_offset = (bn_idx * num_heads + h) * N * head_dim;\n \n extern __shared__ float shared_mem[];\n float* k_cache = shared_mem; // [N, head_dim]\n float* v_cache = &shared_mem[N * head_dim]; // [N, head_dim]\n float* attn_scores = &shared_mem[2 * N * head_dim]; // [N]\n \n int tid = threadIdx.x;\n \n // Load Q for this row (one thread per head_dim element)\n float q_local[64]; // assuming head_dim <= 64\n for (int d = tid; d < head_dim; d += blockDim.x) {\n q_local[d] = query[q_offset + row * head_dim + d];\n }\n __syncthreads();\n \n // Compute attention scores for each key position\n float max_score = -INFINITY;\n \n for (int k_pos = 0; k_pos < N; k_pos++) {\n // Load K and V for key position k_pos\n for (int d = tid; d < head_dim; d += blockDim.x) {\n int k_offset = (bn_idx * num_heads + h) * N * head_dim + k_pos * head_dim + d;\n k_cache[k_pos * head_dim + d] = key[k_offset];\n v_cache[k_pos * head_dim + d] = value[k_offset];\n }\n __syncthreads();\n \n // Compute dot product Q[row] . K[k_pos]\n if (tid == 0) {\n float score = 0.0f;\n for (int d = 0; d < head_dim; d++) {\n score += q_local[d] * k_cache[k_pos * head_dim + d];\n }\n score *= scale;\n \n // Add bias\n score += bias[(b * num_heads + h) * N * N + row * N + k_pos];\n \n // Apply mask\n if (mask[b * N * N + row * N + k_pos] == 0) {\n score = -INFINITY;\n }\n \n attn_scores[k_pos] = score;\n max_score = fmaxf(max_score, score);\n }\n __syncthreads();\n }\n \n // Softmax\n if (tid == 0) {\n float sum_exp = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float exp_val = expf(attn_scores[k_pos] - max_score);\n attn_scores[k_pos] = exp_val;\n sum_exp += exp_val;\n }\n float inv_sum = 1.0f / sum_exp;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n attn_scores[k_pos] *= inv_sum;\n }\n }\n __syncthreads();\n \n // Compute weighted sum of V\n float out_local[64];\n for (int d = tid; d < head_dim; d += blockDim.x) {\n float val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n val += attn_scores[k_pos] * v_cache[k_pos * head_dim + d];\n }\n out_local[d - tid] = val;\n }\n __syncthreads();\n \n // Store output\n for (int d = tid; d < head_dim; d += blockDim.x) {\n int out_offset = (b * N + row) * N * num_heads * head_dim + row * num_heads * head_dim + h * head_dim + d;\n attn_out[out_offset] = out_local[d - tid];\n }\n}\n\n// Output projection kernel\n__global__ void output_proj_kernel(\n const float* __restrict__ attn_out,\n const float* __restrict__ gate,\n const float* __restrict__ w_out,\n const float* __restrict__ b_out,\n float* __restrict__ output,\n int B, int N, int D, int num_heads, int head_dim\n) {\n int bn_idx = blockIdx.x;\n int d_idx = threadIdx.x;\n \n if (bn_idx >= B * N || d_idx >= D) return;\n \n int b = bn_idx / N;\n int n = bn_idx % N;\n \n // Read gated attention output\n float g = gate[bn_idx * D + d_idx];\n \n // Reshape and apply gate\n int h = d_idx / head_dim;\n int hd = d_idx % head_dim;\n float val = attn_out[bn_idx * N * D + n * D + d_idx] * g;\n \n // Output projection\n float out_val = b_out[d_idx];\n for (int i = 0; i < D; i++) {\n out_val += val * w_out[d_idx * D + i];\n }\n \n output[bn_idx * D + d_idx] = out_val;\n}\n\n// Host functions\nstd::vector<torch::Tensor> triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n \n auto options = pair_repr.options();\n \n // Allocate output tensors\n auto output = torch::empty({B, N, N, D}, options);\n auto normed = torch::empty({B * N, N, D}, options);\n auto query = torch::empty({B * N, num_heads, N, head_dim}, options);\n auto key = torch::empty({B * N, num_heads, N, head_dim}, options);\n auto value = torch::empty({B * N, num_heads, N, head_dim}, options);\n auto gate = torch::empty({B, N, N, D}, options);\n auto bias = torch::empty({B, num_heads, N, N}, options);\n \n int threads = 256;\n int blocks = (B * N + threads - 1) / threads;\n \n // LayerNorm\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n normed.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n B, N, D, 1e-5f\n );\n \n // Fused projections\n fused_proj_kernel<<<B * N, D>>>(\n normed.data_ptr<float>(),\n query.data_ptr<float>(),\n key.data_ptr<float>(),\n value.data_ptr<float>(),\n gate.data_ptr<float>(),\n bias.data_ptr<float>(),\n w_query.data_ptr<float>(),\n w_key.data_ptr<float>(),\n w_value.data_ptr<float>(),\n w_gate.data_ptr<float>(),\n b_gate.data_ptr<float>(),\n w_bias.data_ptr<float>(),\n B, N, D, num_heads, head_dim\n );\n \n // Attention\n size_t shared_mem_size = 2 * N * head_dim * sizeof(float) + N * sizeof(float);\n dim3 grid(B * N, num_heads);\n attention_kernel<<<grid, head_dim, shared_mem_size>>>(\n query.data_ptr<float>(),\n key.data_ptr<float>(),\n value.data_ptr<float>(),\n bias.data_ptr<float>(),\n pair_mask.to(torch::kInt).data_ptr<int>(),\n output.data_ptr<float>(),\n 1.0f / sqrtf((float)head_dim),\n B, N, num_heads, head_dim\n );\n \n return {output, gate};\n}\n\"\"\"\n\ncpp_source = \"\"\"\nstd::vector<torch::Tensor> triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\n# Load the CUDA extension\ntriangular_attn = load_inline(\n name='triangular_attention',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Triangular self-attention with custom CUDA kernel\n \"\"\"\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n # Ensure contiguous and correct dtype\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous()\n\n # Call custom CUDA kernel\n outputs = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n result = outputs[0]\n gate = outputs[1]\n \n return result" }
result ← run_correctness (fail)
failrun_correctness run_correctness FAILED: (0 / 3) trials did not all match. correctness_issue: Output mismatch max_difference: ['0.555439', '1.033334', '4.551388'] avg_difference: ['0.088538', '0.492076', '0.781945']
turn 3 · 1 tool call(s) · 129.0s turn · 54.1s LLM · tokens in=10,250 · out=6,025 · total=16,275 · 22,093 chars (click to expand)
turn 3129.0s turn · 54.1s LLM · 74.9s tools · tokens in=10,250 · out=6,025 · total=16,275
reasoning
The kernel is not producing correct results. There are several issues I need to fix: 1. The data layout/reshaping is complex - I need to carefully trace through the original PyTorch code and match the exact reshaping logic. 2. In the original: - `x` has shape `(B, N, N, D)` - `x_flat = x.reshape(B * N, N, D)` - this flattens the first two dimensions of `x`, so we get `(B*N, N, D)` where each element corresponds to `(b, row)` for all columns - The query, key, value projections are applied to `x_flat` which is shape `(B*N, N, D)` - Then they're reshaped to `(B*N, h, N, d)` and transposed to get `(B*N, h, N, d)` Wait, let me trace through more carefully: Original code: ```python B, N, _, D = pair_repr.shape # (B, N, N, D) h = self.num_heads d = self.head_dim x = self.layer_norm(pair_repr) # (B, N, N, D) g = torch.sigmoid(self.gate(x)) # (B, N, N, D) # Pair bias from the pair representation itself bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) # Reshape to (B*N, N, D) — attention across columns for each row x_flat = x.reshape(B * N, N, D) q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2) # (B*N, h, N, d) k = self.key(x_flat).view(B * N, N, h, d).transpose(1, 2) v = self.value(x_flat).view(B * N, N, h, d).transpose(1, 2) attn = (q @ k.transpose(-1, -2)) / math.sqrt(d) # (B*N, h, N, N) attn = attn.view(B, N, h, N, N) # Add pair bias: bias (B, h, N, N) → (B, 1, h, N, N) broadcast across rows attn = attn + bias.unsqueeze(1) ``` Wait, let me trace this more carefully. The reshape is: - `x` is `(B, N, N, D)` - `x_flat = x.reshape(B * N, N, D)` This is flattening the first two dimensions. So if we think of `x` as having indices `(b, i, j, d)`, then after reshape, it becomes `(b*N + i, j, d)` or is it `(b*N + j, i, d)`? Actually, in PyTorch, `.reshape(B * N, N, D)` on a tensor of shape `(B, N, N, D)` will: - Flatten the first two dimensions as `(B*N, N, D)` - This means the memory layout is preserved, so we're treating it as `(B*N rows, N cols, D features)` But wait, the original `x` is `(B, N, N, D)` - this is a pair representation. So `(b, i, j, d)` represents the pair `(i, j)` in batch `b`. When we reshape to `(B*N, N, D)`: - The resulting tensor has `B*N` "rows" - Each "row" is of dimension `N` with `D` features - Which pair does row index `r = b*N + i` correspond to? I think the reshape is: row `r = b*N + i` contains all pairs `(i, j)` for `j = 0, ..., N-1` in batch `b`. Let me verify: in PyTorch, reshape preserves the linear memory order. So element `(b, i, j, d)` at position `b*N*N*D + i*N*D + j*D + d` in the original tensor will be at position `r*N*D + j*D + d` where `r = b*N + i` in the reshaped tensor. So indeed, row `r = b*N + i` contains pairs `(i, 0), (i, 1), ..., (i, N-1)` for batch `b`. This makes sense: for each "starting node" `i` (across all batches), we do self-attention across the column dimension `j`. Now let me trace through my kernel: In my `fused_proj_kernel`, I have: - `bn_idx = blockIdx.x` ranges from `0` to `B*N - 1` - `b = bn_idx / N`, `n = bn_idx % N` - The input is read from `input + bn_idx * D` But wait, the input should be `(B*N, N, D)` shaped, so the element at index `(bn_idx, j, d)` should be at offset `bn_idx * N * D + j * D + d`. Actually no, I defined `normed` as `torch::empty({B * N, N, D}, options);` and in the layernorm kernel, I'm writing to it with the assumption that each `(b, i)` pair gets one row of `D` elements... but that's not right. Wait, let me re-examine my layernorm kernel: ```cpp __global__ void layernorm_kernel( const float* __restrict__ input, float* __restrict__ output, ... int B, int N, int D, float eps ) { int idx = blockIdx.x * blockDim.x + threadIdx.x; int total = B * N; if (idx >= total) return; const float* in = input + idx * D; float* out = output + idx * D; ... } ``` This treats `input` and `output` as 2D tensors of shape `(B*N, D)`. But: - `input` has shape `(B, N, N, D)` in the original code - `output` should have shape `(B*N, N, D)` which is what I want So my layernorm is wrong. I need to iterate over the middle `N` dimension as well. Let me think about this differently. The original code: 1. `x = self.layer_norm(pair_repr)` where `pair_repr` is `(B, N, N, D)` 2. This applies LayerNorm to each `(b, i, j, :)` slice independently So LayerNorm normalizes across the `D` dimension for each position `(b, i, j)`. Then: 3. `x_flat = x.reshape(B * N, N, D)` - this reshapes to `(B*N, N, D)` 4. Apply linear projections to `x_flat` So I need to: 1. Apply LayerNorm to `pair_repr` of shape `(B, N, N, D)` to get `x` of shape `(B, N, N, D)` 2. Reshape `x` to `(B*N, N, D)` 3. Apply projections Let me rewrite this more carefully. I'll use a simpler approach that just matches the PyTorch operations step by step. Actually, let me simplify the whole thing. Instead of trying to fuse everything into one kernel, let me write separate kernels for each major operation and make sure they're correct first, then I can fuse them. The main operations are: 1. LayerNorm 2. Linear projections (query, key, value, gate, bias) 3. Attention computation 4. Output projection Let me write a simpler, more straightforward implementation.
assistantLet me fix the implementation. I need to carefully trace through the tensor shapes and ensure the operations match the reference:
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int B, int N, int D, float eps\n) {\n // Each thread block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Parallel reduction for mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Parallel reduction for variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n// input: (M, in_features), weight: (out_features, in_features), bias: (out_features)\n// output: (M, out_features)\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Sigmoid + multiply kernel\n__global__ void sigmoid_mul_kernel(\n const float* __restrict__ gate_input,\n const float* __restrict__ attn_out,\n float* __restrict__ gated_out,\n int total, int D\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total) return;\n \n for (int d = 0; d < D; d++) {\n float g = 1.0f / (1.0f + expf(-gate_input[idx * D + d]));\n gated_out[idx * D + d] = g * attn_out[idx * D + d];\n }\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\ntemplate <int TILE_SIZE = 16>\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x indexes over B*N rows and h heads\n // blockIdx.y indexes over N output positions\n int bn = blockIdx.x / h;\n int head = blockIdx.x % h;\n int q_pos = blockIdx.y; // query position\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int i = bn % N; // row index in pair representation\n \n extern __shared__ float smem[];\n float* k_tile = smem; // [TILE_SIZE, d]\n float* v_tile = &smem[TILE_SIZE * d]; // [TILE_SIZE, d]\n float* scores = &smem[2 * TILE_SIZE * d]; // [N]\n \n int tid = threadIdx.x;\n \n // Load Q for this query position\n // Q layout: (B*N, h, N, d) -> index: bn * h * N * d + head * N * d + q_pos * d + tid\n float q_local[64];\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_local[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n \n // Compute attention scores in tiles\n float max_score = -INFINITY;\n int score_idx = 0;\n \n for (int k_tile_start = 0; k_tile_start < N; k_tile_start += TILE_SIZE) {\n int k_tile_end = min(k_tile_start + TILE_SIZE, N);\n int tile_size = k_tile_end - k_tile_start;\n \n // Load K and V tiles into shared memory\n for (int k_off = tid; k_off < tile_size * d; k_off += blockDim.x) {\n int k_pos_local = k_off / d;\n int dd = k_off % d;\n int k_pos = k_tile_start + k_pos_local;\n // K layout: (B*N, h, N, d)\n k_tile[k_off] = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n v_tile[k_off] = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n }\n __syncthreads();\n \n // Compute scores for this tile\n if (tid < tile_size) {\n int k_pos = k_tile_start + tid;\n float score = 0.0f;\n for (int dd = 0; dd < d; dd++) {\n score += q_local[dd] * k_tile[tid * d + dd];\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n score += bias[b * h * N * N + head * N * N + i * N + k_pos];\n \n // Apply mask: mask is (B, N, N)\n if (mask[b * N * N + i * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n if (score > max_score) max_score = score;\n }\n __syncthreads();\n }\n \n // Softmax\n // Find max (reduction)\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n // Load V for k_pos (already in v_tile if we process carefully)\n // For now, load directly from global memory\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n // Store output: (B*N, h, N, d)\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Final output: reshape (B*N, h, N, d) -> (B, N, N, D), apply gate, project\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_sigmoid, // (B, N, N, D)\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i; // bn index\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for position j\n // attn_out layout: (B*N, h, N, d)\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Apply gate\n val *= gate_sigmoid[idx * D + out_dim];\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int threads = 256;\n int blocks = (B * N * N + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n B, N, D, 1e-5f\n );\n \n // Step 2: Reshape x to (B*N, N, D)\n auto x_flat = x.view({BN, N, D});\n \n // Step 3: Compute Q, K, V\n // x_flat is (BN, N, D), reshape to (BN*N, D) for linear\n auto x_2d = x_flat.view({BN * N, D});\n \n auto query = torch::empty({BN * N, D}, options);\n auto key = torch::empty({BN * N, D}, options);\n auto value = torch::empty({BN * N, D}, options);\n auto gate_proj = torch::empty({BN * N, D}, options);\n auto bias_proj = torch::empty({B * N * N, h}, options); // (B*N*N, h)\n \n dim3 grid_qk((D + threads - 1) / threads, BN * N);\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), BN * N, D, D, false\n );\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), BN * N, D, D, false\n );\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), BN * N, D, D, false\n );\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), BN * N, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, B * N * N);\n linear_kernel<<<grid_bias, threads>>>(\n x_2d.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), B * N * N, D, h, false\n );\n \n // Reshape Q, K, V to (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // Reshape bias_proj to (B, h, N, N)\n // bias_proj is (B*N*N, h) = (B, N, N, h) after reshape\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 4: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: K tile, V tile, scores\n // We'll use a simplified kernel that loads all K, V for now\n size_t smem_size = 2 * 16 * d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN * h, N);\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 5: Compute sigmoid gate\n auto gate_sigmoid = torch::sigmoid(gate_proj.view({B, N, N, D}));\n \n // Step 6: Output projection\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (B * N * N + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_sigmoid.data_ptr<float>(),\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\n# Load the CUDA extension\ntriangular_attn = load_inline(\n name='triangular_attention_v2',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Triangular self-attention with custom CUDA kernel\n \"\"\"\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n # Ensure contiguous and correct dtype\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n # Call custom CUDA kernel\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 4 · 1 tool call(s) · 34.7s turn · 34.7s LLM · tokens in=14,609 · out=4,331 · total=18,940 · 16,813 chars (click to expand)
turn 434.7s turn · 34.7s LLM · 0.0s tools · tokens in=14,609 · out=4,331 · total=18,940
reasoning
Now let me check correctness again.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int B, int N, int D, float eps\n) {\n // Each thread block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Parallel reduction for mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Parallel reduction for variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n// input: (M, in_features), weight: (out_features, in_features), bias: (out_features)\n// output: (M, out_features)\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Sigmoid + multiply kernel\n__global__ void sigmoid_mul_kernel(\n const float* __restrict__ gate_input,\n const float* __restrict__ attn_out,\n float* __restrict__ gated_out,\n int total, int D\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total) return;\n \n for (int d = 0; d < D; d++) {\n float g = 1.0f / (1.0f + expf(-gate_input[idx * D + d]));\n gated_out[idx * D + d] = g * attn_out[idx * D + d];\n }\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\ntemplate <int TILE_SIZE = 16>\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x indexes over B*N rows and h heads\n // blockIdx.y indexes over N output positions\n int bn = blockIdx.x / h;\n int head = blockIdx.x % h;\n int q_pos = blockIdx.y; // query position\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int i = bn % N; // row index in pair representation\n \n extern __shared__ float smem[];\n float* k_tile = smem; // [TILE_SIZE, d]\n float* v_tile = &smem[TILE_SIZE * d]; // [TILE_SIZE, d]\n float* scores = &smem[2 * TILE_SIZE * d]; // [N]\n \n int tid = threadIdx.x;\n \n // Load Q for this query position\n // Q layout: (B*N, h, N, d) -> index: bn * h * N * d + head * N * d + q_pos * d + tid\n float q_local[64];\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_local[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n \n // Compute attention scores in tiles\n float max_score = -INFINITY;\n int score_idx = 0;\n \n for (int k_tile_start = 0; k_tile_start < N; k_tile_start += TILE_SIZE) {\n int k_tile_end = min(k_tile_start + TILE_SIZE, N);\n int tile_size = k_tile_end - k_tile_start;\n \n // Load K and V tiles into shared memory\n for (int k_off = tid; k_off < tile_size * d; k_off += blockDim.x) {\n int k_pos_local = k_off / d;\n int dd = k_off % d;\n int k_pos = k_tile_start + k_pos_local;\n // K layout: (B*N, h, N, d)\n k_tile[k_off] = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n v_tile[k_off] = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n }\n __syncthreads();\n \n // Compute scores for this tile\n if (tid < tile_size) {\n int k_pos = k_tile_start + tid;\n float score = 0.0f;\n for (int dd = 0; dd < d; dd++) {\n score += q_local[dd] * k_tile[tid * d + dd];\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n score += bias[b * h * N * N + head * N * N + i * N + k_pos];\n \n // Apply mask: mask is (B, N, N)\n if (mask[b * N * N + i * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n if (score > max_score) max_score = score;\n }\n __syncthreads();\n }\n \n // Softmax\n // Find max (reduction)\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n // Load V for k_pos (already in v_tile if we process carefully)\n // For now, load directly from global memory\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n // Store output: (B*N, h, N, d)\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Final output: reshape (B*N, h, N, d) -> (B, N, N, D), apply gate, project\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_sigmoid, // (B, N, N, D)\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i; // bn index\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for position j\n // attn_out layout: (B*N, h, N, d)\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Apply gate\n val *= gate_sigmoid[idx * D + out_dim];\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int threads = 256;\n int blocks = (B * N * N + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n B, N, D, 1e-5f\n );\n \n // Step 2: Reshape x to (B*N, N, D)\n auto x_flat = x.view({BN, N, D});\n \n // Step 3: Compute Q, K, V\n // x_flat is (BN, N, D), reshape to (BN*N, D) for linear\n auto x_2d = x_flat.view({BN * N, D});\n \n auto query = torch::empty({BN * N, D}, options);\n auto key = torch::empty({BN * N, D}, options);\n auto value = torch::empty({BN * N, D}, options);\n auto gate_proj = torch::empty({BN * N, D}, options);\n auto bias_proj = torch::empty({B * N * N, h}, options); // (B*N*N, h)\n \n dim3 grid_qk((D + threads - 1) / threads, BN * N);\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), BN * N, D, D, false\n );\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), BN * N, D, D, false\n );\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), BN * N, D, D, false\n );\n linear_kernel<<<grid_qk, threads>>>(\n x_2d.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), BN * N, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, B * N * N);\n linear_kernel<<<grid_bias, threads>>>(\n x_2d.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), B * N * N, D, h, false\n );\n \n // Reshape Q, K, V to (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // Reshape bias_proj to (B, h, N, N)\n // bias_proj is (B*N*N, h) = (B, N, N, h) after reshape\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 4: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: K tile, V tile, scores\n // We'll use a simplified kernel that loads all K, V for now\n size_t smem_size = 2 * 16 * d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN * h, N);\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 5: Compute sigmoid gate\n auto gate_sigmoid = torch::sigmoid(gate_proj.view({B, N, N, D}));\n \n // Step 6: Output projection\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (B * N * N + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_sigmoid.data_ptr<float>(),\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\n# Load the CUDA extension\ntriangular_attn = load_inline(\n name='triangular_attention_v2',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n Triangular self-attention with custom CUDA kernel\n \"\"\"\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n # Ensure contiguous and correct dtype\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n # Call custom CUDA kernel\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← run_correctness (fail)
failrun_correctness run_correctness FAILED: (0 / 3) trials did not all match. correctness_issue: Output mismatch max_difference: ['0.267639', '0.548102', '2.574322'] avg_difference: ['0.072836', '0.088762', '0.462230']
turn 5 · 1 tool call(s) · 159.8s turn · 84.2s LLM · tokens in=19,012 · out=9,536 · total=28,548 · 32,535 chars (click to expand)
turn 5159.8s turn · 84.2s LLM · 75.6s tools · tokens in=19,012 · out=9,536 · total=28,548
reasoning
The output still doesn't match. Let me think about this more carefully. Looking at the reference code again: 1. `x = self.layer_norm(pair_repr)` - LayerNorm over (B, N, N, D), last dim is normalized 2. `g = torch.sigmoid(self.gate(x))` - sigmoid of gate(x), stored in `g` 3. `bias = self.bias_proj(x).permute(0, 3, 1, 2)` - bias from x, permuted to (B, h, N, N) 4. `x_flat = x.reshape(B * N, N, D)` - reshape to (B*N, N, D) - this is treating the first N as the row dimension and second N as the column dimension within each "flat batch" 5. Then Q, K, V are computed from x_flat 6. Q, K, V are reshaped: `.view(B * N, N, h, d).transpose(1, 2)` - this gives (B*N, h, N, d) 7. Attention: `attn = (q @ k.transpose(-1, -2)) / math.sqrt(d)` - gives (B*N, h, N, N) 8. Reshape to (B, N, h, N, N) and add bias 9. Apply mask over last two dimensions (key dimension) 10. Reshape back to (B*N, h, N, N), softmax over last dim, dropout 11. Output: `(attn @ v).transpose(1, 2).reshape(B, N, N, D)` - gives (B, N, N, D) 12. Gate: `out = g * out` 13. Final: `self.out_proj(out)` Wait, I see an issue in my implementation. The reference computes `attn @ v` where: - `attn` is (B*N, h, N, N) - `v` is (B*N, h, N, d) So `attn @ v` should be (B*N, h, N, d). Then `.transpose(1, 2)` gives (B*N, N, h, d). Then `.reshape(B, N, N, D)` - how does this work? - (B*N, N, h, d) -> (B, N, N, h, d) -> then D = h * d So the reshape is: (B*N, N, h, d) with the understanding that the first B*N contains b*N*n elements, so we view it as (B, N, N, h, d) then flatten to (B, N, N, D). But wait, that doesn't make sense. Let me think again. `attn` is (B*N, h, N, N). After softmax and dropout, it's still (B*N, h, N, N). `v` is (B*N, h, N, d). `attn @ v` where the last two dimensions are the matrix dimensions: - (N, N) @ (N, d) = (N, d) - So the result is (B*N, h, N, d) Then `.transpose(1, 2)` gives (B*N, N, h, d). Then `.reshape(B, N, N, D)`: - The total elements are B*N*N*h*d = B*N*N*D - So we reshape (B*N, N, h, d) to (B, N, N, D) - But the first dimension is B*N, not B! Wait, I see. The view is: (B*N, N, h, d). If we do .reshape(B, N, N, D): - B*N elements become B groups of N, so we get (B, N, N, h, d)? - No wait, reshape just reinterprets the shape. (B*N, N, h, d) -> (B, N, N, h*d) = (B, N, N, D) Actually I think this works because: - (B*N, N, h, d) has B*N*N*h*d = B*N*N*D elements - Reshaping to (B, N, N, D) has B*N*N*D elements - The order is preserved So position (bn, row, h, d_idx) in (B*N, N, h, d) maps to: - b = bn / N - bn_local = bn % N (row in the original B*N batch) - So position (b, bn_local, row, h*d + d_idx) in (B, N, N, D) Hmm, this is getting confusing. Let me trace through more carefully. Actually, looking at the code again: ```python x_flat = x.reshape(B * N, N, D) # (B*N, N, D) - each "batch" has N rows with D features q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2) # (B*N, h, N, d) ``` So `x_flat` has shape (B*N, N, D). The first N from the original pair_repr is collapsed into the batch dimension. The second N becomes the sequence dimension within each "flat batch". After computing Q, K, V from x_flat: - `self.query(x_flat)` gives (B*N, N, D) - `.view(B * N, N, h, d)` reshapes to (B*N, N, h, d) - `.transpose(1, 2)` gives (B*N, h, N, d) So in this layout: - The first dimension is B*N "flat batches" - The second dimension is h heads - The third dimension is N positions (this was the column dimension in original pair_repr) - The fourth dimension is d head features Now attention: - `q @ k.transpose(-1, -2)` = (B*N, h, N, d) @ (B*N, h, d, N) = (B*N, h, N, N) - This gives attention scores for each (bn, head, query_pos, key_pos) Then: ```python attn = attn.view(B, N, h, N, N) attn = attn + bias.unsqueeze(1) # bias is (B, h, N, N), becomes (B, 1, h, N, N) ``` The reshape from (B*N, h, N, N) to (B, N, h, N, N): - The first dimension B*N becomes (B, N) - This makes the first N the row index in original pair_repr - The second N remains the key positions So the bias is added to all N query positions, but the bias values depend on the original row index (which is now dimension 1 in the reshaped attn). Now the mask: ```python mask = pair_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, N, N) attn = attn.masked_fill(mask == 0, float("-inf")) ``` The mask is (B, 1, 1, N, N) and broadcasts to (B, N, h, N, N). This masks the key positions. Then reshape back and softmax: ```python attn = attn.view(B * N, h, N, N) attn = F.softmax(attn, dim=-1) # softmax over key dimension ``` Then output: ```python out = (attn @ v).transpose(1, 2).reshape(B, N, N, D) ``` - `attn @ v` = (B*N, h, N, N) @ (B*N, h, N, d) = (B*N, h, N, d) - `.transpose(1, 2)` = (B*N, N, h, d) - `.reshape(B, N, N, D)` = reshape to (B, N, N, D) The reshape from (B*N, N, h, d) to (B, N, N, D): - B*N becomes (B, N) - first N is row index - N stays as column index - h, d become D = h*d So the output for position (b, i, j) in the original pair representation: - The bn index is b*N + i - The attention output for row i, column j is: attn[bn, :, j, :] which is (h, d) - After reshape, this becomes output[b, i, j, :] which is D = h*d Then gate and output projection: ```python out = g * out # g is (B, N, N, D) return self.out_proj(out) ``` OK so I think my understanding is correct. Let me check my kernel implementation. In my attention kernel: - `bn = blockIdx.x / h` where `blockIdx.x` ranges from 0 to BN*h-1 - `head = blockIdx.x % h` - `q_pos = blockIdx.y` where `blockIdx.y` ranges from 0 to N-1 So for each (bn, head, q_pos), I compute the attention scores for all key positions. The query for (bn, head, q_pos, d) is: `query[bn * h * N * d + head * N * d + q_pos * d + dd]` This is correct for (B*N, h, N, d) layout. The key for (bn, head, k_pos, d) is: `key[bn * h * N * d + head * N * d + k_pos * d + dd]` This is also correct. The bias for (b, head, i, k_pos) where i = bn % N: `bias[b * h * N * N + head * N * N + i * N + k_pos]` This is correct for (B, h, N, N) layout. The mask for (b, i, k_pos): `mask[b * N * N + i * N + k_pos]` This is correct for (B, N, N) layout. The output for (bn, head, q_pos, d): `output[bn * h * N * d + head * N * d + q_pos * d + dd]` This is correct for (B*N, h, N, d) layout. Now in the output kernel: - `idx = blockIdx.x` ranges from 0 to B*N*N-1 - `b = idx / (N * N)` - `ij = idx % (N * N)` - `i = ij / N` - `j = ij % N` So `i` is the row index and `j` is the column index in the original pair representation. The attention output for (bn=b*N+i, head, j, d) is: `attn_out[bn * h * N * d + head * N * d + j * d + dd]` Hmm wait, there's an issue here. Let me think about this more carefully. In the reference: - `attn @ v` computes the weighted sum of values for each query position - The result is (B*N, h, N, d) where the N dimension is the query position In my kernel: - I'm computing attention for each (bn, head, q_pos) - The output is stored at (bn, head, q_pos, d) So after the attention kernel, the output for (bn, head, q_pos, d) is: `output[bn * h * N * d + head * N * d + q_pos * d + d_idx]` Now in the output kernel, I need to read the attention output for (bn=b*N+i, head, j, d_idx). But wait, `j` here is the column position in the original pair representation. And `q_pos` in the attention kernel was the column position as well. So for (b, i, j) in the original pair repr: - bn = b*N + i - The attention output for column j should be at (bn, head, j, d_idx) So I should read: `attn_out[(b*N + i) * h * N * d + head * N * d + j * d + d_idx]` Let me check my output kernel: - `bn = b * N + i` (correct) - `head = out_dim / d` - `dd = out_dim % d` - `val = attn_out[bn * h * N * d + head * N * d + j * d + dd]` This looks correct! But wait, there's another issue. The reference reshapes: `out = (attn @ v).transpose(1, 2).reshape(B, N, N, D)` So the layout is: - `attn @ v` is (B*N, h, N, d) - `.transpose(1, 2)` is (B*N, N, h, d) - `.reshape(B, N, N, D)` is (B, N, N, D) After transpose and reshape: - Position (bn, pos, head, d_idx) in (B*N, N, h, d) maps to position (b, i, pos, head*d+d_idx) in (B, N, N, D) Where: - b = bn / N - i = bn % N So for original position (b, i, j) in pair_repr: - bn = b*N + i - The value for column j is from position (bn, j, :, :) in (B*N, N, h, d) - Which is (bn, j, h_idx, d_idx) for head h_idx So after reshape to (B, N, N, D): - Position (b, i, j, D_idx) where D_idx = h_idx * d + d_idx This is exactly what I have in my output kernel! So why is the output wrong? Let me check the other operations. Actually, I think there might be an issue with the linear projections. Let me check the weight layout. In PyTorch, `nn.Linear(in_features, out_features)` has weight of shape `(out_features, in_features)`. And `output = input @ weight.T + bias`. So `x @ w.T` where x is (M, in_features) and w is (out_features, in_features) gives (M, out_features). In my linear kernel: ```cpp float sum = has_bias ? bias[out_f] : 0.0f; for (int in_f = 0; in_f < in_features; in_f++) { sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f]; } output[m * out_features + out_f] = sum; ``` This computes `sum = input[m, :] @ weight[out_f, :]`, which is correct for weight.T @ input.T... wait no. Let me think again. For a single output element: - output[m, out_f] = sum over in_f of input[m, in_f] * weight[out_f, in_f] - This is input[m, :] dot weight[out_f, :] - Which is the same as (input[m, :] @ weight.T) = input[m, :] @ (weight.T)[out_f, :] - Where weight.T is (in_features, out_features) So output[m, :] = input[m, :] @ weight.T This is correct! Let me check the LayerNorm next. The reference uses `nn.LayerNorm(pair_dim)` which normalizes over the last dimension. My kernel: ```cpp // Each thread block handles one (b, i, j) position int idx = blockIdx.x; int total = B * N * N; if (idx >= total) return; const float* in = input + idx * D; float* out = output + idx * D; // Compute mean float mean = 0.0f; for (int d = 0; d < D; d++) { mean += in[d]; } mean /= D; // Compute variance float var = 0.0f; for (int d = 0; d < D; d++) { float diff = in[d] - mean; var += diff * diff; } var /= D; float inv_std = rsqrtf(var + eps); // Normalize and apply weight/bias for (int d = 0; d < D; d++) { out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d]; } ``` This looks correct. It computes the mean and variance over D elements, then normalizes. Let me check the bias projection. In the reference: ```python bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) ``` Where `self.bias_proj` is `nn.Linear(pair_dim, num_heads, bias=False)`. So `bias_proj(x)` gives (B, N, N, h), and then permute gives (B, h, N, N). In my kernel: ```cpp auto bias_proj = torch::empty({B * N * N, h}, options); dim3 grid_bias((h + threads - 1) / threads, B * N * N); linear_kernel<<<grid_bias, threads>>>( x_2d.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr, bias_proj.data_ptr<float>(), B * N * N, D, h, false ); auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous(); ``` So `bias_proj` is (B*N*N, h), then reshape to (B, N, N, h), then permute to (B, h, N, N). This should be correct! Hmm, but wait. In the reference, `x` is (B, N, N, D). Then `self.bias_proj(x)` applies a linear layer to the last dimension, giving (B, N, N, h). But in my code, I'm using `x_2d` which is `x_flat.view({BN * N, D})`. Wait, that's wrong! Let me trace through my code: 1. `x` is (B, N, N, D) after LayerNorm 2. `x_flat = x.view({BN, N, D})` - this is (B*N, N, D), treating the first N as batch and second N as sequence 3. `x_2d = x_flat.view({BN * N, D})` - this is (B*N*N, D), flattening all but the last dim So `x_2d` contains the same data as `x.reshape(-1, D)`, which should be equivalent to `x.view(B, N, N, D).reshape(-1, D)`. Then `bias_proj` is computed from `x_2d`, giving (B*N*N, h). This is equivalent to `x.reshape(-1, D) @ w_bias.T`. And then reshaping to (B, N, N, h) and permuting to (B, h, N, N) should give the same result as the reference. Actually, let me check the permutation. In the reference: ```python bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) ``` So the order changes from (B, N, N, h) to (B, h, N, N). In my code: ```cpp auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous(); ``` This does the same: (B, N, N, h) -> (B, h, N, N). So this is correct. Let me check the attention computation again. The issue might be there. Actually, I think I see a problem. In my attention kernel, I'm loading all K and V values from global memory in each iteration: ```cpp for (int dd = tid; dd < d; dd += blockDim.x) { float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd]; out_val += scores[k_pos] * v_val; } ``` This is correct, but the issue might be in how I'm handling the softmax. Let me look at that part. Actually, I think the issue might be that in my attention kernel, I'm using `tid` which is `threadIdx.x`, and `blockDim.x` is `d` (head_dim = 16). But then when I store the output, I'm using: ```cpp for (int dd = tid; dd < d; dd += blockDim.x) { ... output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val; } ``` But `out_val` is a scalar, and I'm computing it for `dd = tid`. So I should have `out_val` computed for each `dd`. Let me check my code again. ```cpp // Compute weighted sum of V for (int dd = tid; dd < d; dd += blockDim.x) { float out_val = 0.0f; for (int k_pos = 0; k_pos < N; k_pos++) { float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd]; out_val += scores[k_pos] * v_val; } // Store output: (B*N, h, N, d) output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val; } ``` This looks correct! Each thread with `tid` computes the output for dimension `dd = tid`. But wait, there's an issue. The `scores` array is in shared memory, and I'm only computing the scores if `tid < tile_size`. But the softmax reduction assumes all threads participate. Let me check this part. ```cpp // Compute exp and sum __shared__ float ssum; float local_sum = 0.0f; for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) { float exp_val = expf(scores[k_pos] - smax); scores[k_pos] = exp_val; local_sum += exp_val; } // Sum reduction for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) { local_sum += __shfl_down_sync(0xffffffff, local_sum, offset); } if (tid == 0) ssum = local_sum; ``` The issue is that `scores` is computed only by threads where `tid < tile_size` in each iteration, but later all threads (with tid from 0 to d-1) try to access `scores`. This should be fine because `scores` is shared memory and all threads can access it. Actually, I think the issue is that the computation of scores happens inside the loop where only `tid < tile_size` threads participate: ```cpp // Compute scores for this tile if (tid < tile_size) { int k_pos = k_tile_start + tid; float score = 0.0f; for (int dd = 0; dd < d; dd++) { score += q_local[dd] * k_tile[tid * d + dd]; } ... scores[k_pos] = score; ... } ``` But `q_local` is computed by all threads: ```cpp for (int dd = tid; dd < d; dd += blockDim.x) { q_local[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd]; } ``` So `q_local` is a per-thread array of size d. But `q_local` is declared as `float q_local[64]` which is way too big for registers. Let me reconsider. Actually, since `q_local` is a local array, it's stored in registers (or local memory if it doesn't fit). With `d = 16`, it should fit in registers. But the issue is that in the score computation loop: ```cpp for (int dd = 0; dd < d; dd++) { score += q_local[dd] * k_tile[tid * d + dd]; } ``` This accesses `q_local[dd]` for all dd from 0 to d-1. But `q_local` is computed by a single thread for `dd = tid`. So `q_local[dd]` for dd != tid might be garbage! Ah, I see the problem now. Each thread only computes `q_local[tid]`, but then when computing the score, I iterate over all dd. This is wrong! I need to either: 1. Have each thread compute q_local fully 2. Or use shared memory for Q Let me fix this by having each thread load the entire Q vector: Actually, since the Q vector is small (d = 16), I can have each thread load all of Q: ```cpp // Load Q for this query position - all threads load the same Q float q_local[64]; for (int dd = 0; dd < d; dd++) { q_local[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd]; } ``` But this means each thread loads the same data from global memory, which is inefficient. Alternatively, I can use shared memory for Q: ```cpp extern __shared__ float smem[]; float* q_shared = smem; // [d] float* k_tile = &smem[d]; // [TILE_SIZE, d] float* v_tile = &smem[d + TILE_SIZE * d]; // [TILE_SIZE, d] float* scores = &smem[d + 2 * TILE_SIZE * d]; // [N] ``` Then load Q cooperatively: ```cpp for (int dd = tid; dd < d; dd += blockDim.x) { q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd]; } __syncthreads(); ``` And in the score computation: ```cpp float score = 0.0f; for (int dd = 0; dd < d; dd++) { score += q_shared[dd] * k_tile[tid * d + dd]; } ``` Let me rewrite the kernel with this fix.
assistantLet me trace through the reference more carefully and fix the issues:
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor - each position normalized independently\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x: index over B*N rows\n // blockIdx.y: index over h heads \n // blockIdx.z: index over N query positions\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N; // row index in pair representation\n int tid = threadIdx.x;\n \n // Shared memory layout\n extern __shared__ float smem[];\n float* q_shared = smem; // [d] - Q for this query position\n float* scores = &smem[d]; // [N] - attention scores\n \n // Cooperatively load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores for all key positions\n // Each thread handles a subset of key positions\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n // Load K for this key position and compute dot product\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n score += bias[b * h * N * N + head * N * N + row * N + k_pos];\n \n // Apply mask: mask is (B, N, N)\n if (mask[b * N * N + row * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Find max score for numerical stability\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp(score - max) and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V for each output dimension\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for column j\n // attn_out layout: (B*N, h, N, d) -> (bn, head, j, dd)\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Compute sigmoid gate and apply\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + out_dim]));\n val *= g;\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Reshape x for linear projections\n // x is (B, N, N, D), flatten to (B*N*N, D)\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n // Q, K, V: (B*N*N, D) -> (B*N, N, h, d) -> (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // bias: (B*N*N, h) -> (B, N, N, h) -> (B, h, N, N)\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: Q (d) + scores (N)\n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N); // B*N blocks, h heads, N query positions\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(), // pass pre-sigmoid gate values\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v3',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 6 · 1 tool call(s) · 29.7s turn · 29.6s LLM · tokens in=22,767 · out=3,736 · total=26,503 · 15,068 chars (click to expand)
turn 629.7s turn · 29.6s LLM · 0.0s tools · tokens in=22,767 · out=3,736 · total=26,503
reasoning
Now let me check correctness.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor - each position normalized independently\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x: index over B*N rows\n // blockIdx.y: index over h heads \n // blockIdx.z: index over N query positions\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N; // row index in pair representation\n int tid = threadIdx.x;\n \n // Shared memory layout\n extern __shared__ float smem[];\n float* q_shared = smem; // [d] - Q for this query position\n float* scores = &smem[d]; // [N] - attention scores\n \n // Cooperatively load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores for all key positions\n // Each thread handles a subset of key positions\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n // Load K for this key position and compute dot product\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n score += bias[b * h * N * N + head * N * N + row * N + k_pos];\n \n // Apply mask: mask is (B, N, N)\n if (mask[b * N * N + row * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Find max score for numerical stability\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp(score - max) and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V for each output dimension\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for column j\n // attn_out layout: (B*N, h, N, d) -> (bn, head, j, dd)\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Compute sigmoid gate and apply\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + out_dim]));\n val *= g;\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Reshape x for linear projections\n // x is (B, N, N, D), flatten to (B*N*N, D)\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n // Q, K, V: (B*N*N, D) -> (B*N, N, h, d) -> (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // bias: (B*N*N, h) -> (B, N, N, h) -> (B, h, N, N)\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: Q (d) + scores (N)\n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N); // B*N blocks, h heads, N query positions\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(), // pass pre-sigmoid gate values\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v3',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← run_correctness (fail)
failrun_correctness run_correctness FAILED: (0 / 3) trials did not all match. correctness_issue: Output mismatch max_difference: ['2.667119', '2.781310', '1.098691'] avg_difference: ['0.467300', '0.467228', '0.500993']
turn 7 · 1 tool call(s) · 142.4s turn · 64.5s LLM · tokens in=26,573 · out=7,375 · total=33,948 · 25,399 chars (click to expand)
turn 7142.4s turn · 64.5s LLM · 77.9s tools · tokens in=26,573 · out=7,375 · total=33,948
reasoning
Let me trace through the reference implementation more carefully: 1. `x = self.layer_norm(pair_repr)` - LayerNorm on the last dimension 2. `g = torch.sigmoid(self.gate(x))` - Gate (sigmoid of linear projection) 3. `bias = self.bias_proj(x).permute(0, 3, 1, 2)` - Pair bias, shape (B, h, N, N) 4. `x_flat = x.reshape(B * N, N, D)` - Reshape to (B*N, N, D) 5. `q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2)` - Query, shape (B*N, h, N, d) 6. Same for k and v 7. `attn = (q @ k.transpose(-1, -2)) / math.sqrt(d)` - Attention scores, shape (B*N, h, N, N) 8. `attn = attn.view(B, N, h, N, N)` - Reshape attention 9. `attn = attn + bias.unsqueeze(1)` - Add bias 10. Apply mask and softmax 11. `out = (attn @ v).transpose(1, 2).reshape(B, N, N, D)` - Attention output 12. `out = g * out` - Apply gate 13. `return self.out_proj(out)` - Output projection The key insight is that the attention is computed for each row i of the pair matrix: - For each row i, we have Q, K, V computed from that row - The Q is (h, N, d) - it queries all columns in row i - The K is also (h, N, d) - keys from all columns in row i - The attention matrix is (h, N, N) - attention over columns for row i Wait, I need to look more carefully. In the reference: - `x_flat = x.reshape(B * N, N, D)` reshapes x from (B, N, N, D) to (B*N, N, D) - This means x_flat[b*N + i] = x[b, i, :, :] - the entire row i So when we compute Q: - `q = self.query(x_flat)` is (B*N, N, D) - `q = q.view(B * N, N, h, d).transpose(1, 2)` is (B*N, h, N, d) - For bn = b*N + i, the Q is for row i And the attention: - `attn = q @ k.transpose(-1, -2)` is (B*N, h, N, N) - For bn = b*N + i, the attention matrix is over columns j, k for row i The bias: - `bias = self.bias_proj(x).permute(0, 3, 1, 2)` is (B, h, N, N) - This is the pair bias computed from the normalized input x - When added to attention: `attn.view(B, N, h, N, N) + bias.unsqueeze(1)` - The attention at (b, i, head, j, k) gets bias(b, head, j, k) Wait, that's not right. Let me check again. `attn = attn.view(B, N, h, N, N)` - (B, N, h, N, N) `bias = self.bias_proj(x).permute(0, 3, 1, 2)` - (B, h, N, N) `attn = attn + bias.unsqueeze(1)` - (B, h, N, N) broadcast to (B, 1, h, N, N) to (B, N, h, N, N) So the bias at (b, head, j, k) is added to attention at (b, i, head, j, k) for all i. In my kernel: - I'm using bias[b * h * N * N + head * N * N + row * N + k_pos] - But the bias should be at position (b, head, ?, ?) - Looking at bias_reshaped: bias_proj is (B*N*N, h) -> reshape to (B, N, N, h) -> permute to (B, h, N, N) Wait, there's a problem. The bias_proj is computed from each position in x. Let me trace this more carefully. In the reference: ```python bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) ``` Here, `x` is (B, N, N, D), and `self.bias_proj` is a linear layer (D -> h). So `bias_proj(x)` is (B, N, N, h). After permute(0, 3, 1, 2), it becomes (B, h, N, N). In my kernel: ```cpp linear_kernel<<<grid_bias, threads>>>( x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr, bias_proj.data_ptr<float>(), total_positions, D, h, false ); ``` Where x_flat is (B*N*N, D) and bias_proj becomes (B*N*N, h). Then I reshape: ```cpp auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous(); ``` So bias_reshaped should be (B, h, N, N), which matches the reference. Now in the attention kernel, I access: ```cpp score += bias[b * h * N * N + head * N * N + row * N + k_pos]; ``` Where row is the row index in the pair representation (derived from bn = b * N + row). But wait, the bias is (B, h, N, N), and I'm using `row` for the first N dimension and `k_pos` for the second. But in the reference, when adding bias: ```python attn = attn.view(B, N, h, N, N) # (B, i, h, j, k) attn = attn + bias.unsqueeze(1) # bias is (B, h, N, N), unsqueeze to (B, 1, h, N, N) ``` So the attention at (b, i, head, j, k) gets bias at (b, head, j, k). The bias doesn't depend on i! In my kernel, for bn = b * N + i (row i), and q_pos = j (querying column j), and k_pos (attending to column k_pos), I should use: ```cpp score += bias[b * h * N * N + head * N * N + j * N + k_pos]; ``` Not `row` but `q_pos` (or actually the column being queried). Wait, let me re-read the reference again... ```python # Reshape to (B*N, N, D) — attention across columns for each row x_flat = x.reshape(B * N, N, D) q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2) # (B*N, h, N, d) k = self.key(x_flat).view(B * N, N, h, d).transpose(1, 2) v = self.value(x_flat).view(B * N, N, h, d).transpose(1, 2) attn = (q @ k.transpose(-1, -2)) / math.sqrt(d) # (B*N, h, N, N) attn = attn.view(B, N, h, N, N) # Add pair bias: bias (B, h, N, N) → (B, 1, h, N, N) broadcast across rows attn = attn + bias.unsqueeze(1) ``` So the attention matrix is (B*N, h, N, N). After view, it's (B, N, h, N, N). The first N is the row index i. The bias is (B, h, N, N), and after unsqueeze(1), it's (B, 1, h, N, N), which broadcasts to (B, N, h, N, N). So attn[b, i, head, j, k] = q[b*N+i, head, j] @ k[b*N+i, head, k] + bias[b, head, j, k] Wait, that's still confusing. Let me think about this differently. The x_flat is (B*N, N, D). For bn = b*N + i, x_flat[bn] contains x[b, i, :, :], i.e., all columns j for row i. Then q = self.query(x_flat) is (B*N, N, D). After view and transpose, q is (B*N, h, N, d). So q[bn, head, j, :] is the query vector for position (i, j) in the pair matrix, for head `head`. Similarly, k[bn, head, k, :] is the key vector for position (i, k) in the pair matrix. The attention score at[bn, head, j, k] = q[bn, head, j, :] @ k[bn, head, k, :] / sqrt(d). After view: attn[b, i, head, j, k] = at[b*N + i, head, j, k]. The bias is (B, h, N, N). bias[b, head, j, k] is computed from x[b, j, k, :]. So attn[b, i, head, j, k] += bias[b, head, j, k]. In my kernel: - bn = b * N + row (where row = i) - head = blockIdx.y - q_pos = j (the query position) - k_pos is the key position So I should use: bias[b * h * N * N + head * N * N + q_pos * N + k_pos] But I'm using: bias[b * h * N * N + head * N * N + row * N + k_pos] That's wrong! I should use q_pos, not row! Let me also check the output reshaping. In the reference: ```python out = (attn @ v).transpose(1, 2).reshape(B, N, N, D) ``` Here: - attn is (B, N, h, N, N) after softmax - attn @ v is... wait, this is done after `attn = attn.view(B * N, h, N, N)`. Let me trace more carefully: ```python attn = attn.view(B * N, h, N, N) # after softmax out = (attn @ v).transpose(1, 2).reshape(B, N, N, D) ``` - attn is (B*N, h, N, N) - v is (B*N, h, N, d) - attn @ v is (B*N, h, N, d) - transpose(1, 2) is (B*N, N, h, d) - reshape(B, N, N, D) is (B, N, N, D) So out[b, i, j, d_idx] comes from attn_output[b*N + i, j, head, dd] where head = d_idx / d and dd = d_idx % d. In my output kernel: ```cpp float val = attn_out[bn * h * N * d + head * N * d + j * d + dd]; ``` But wait, in my attention kernel, I'm storing output at: ```cpp output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val; ``` Where q_pos is the query position. In the reference, for bn = b*N + i and q_pos = j, the output is for position (i, j) in the pair matrix. But in my output kernel, I'm reading: ```cpp float val = attn_out[bn * h * N * d + head * N * d + j * d + dd]; ``` Where j is the column index in the output position (i, j). This should match q_pos in the attention kernel! Actually wait, let me re-check the reshape in the reference: ```python out = (attn @ v).transpose(1, 2).reshape(B, N, N, D) ``` Before transpose, attn @ v is (B*N, h, N, d). After transpose(1, 2), it's (B*N, N, h, d). After reshape, it's (B, N, N, D). So out[b, i, j, d_idx]: - From reshape: b*N*N + i*N*N + j*D + d_idx - Before reshape: (b*N + i)*N*h*d + j*h*d + head*d + dd Wait, let me be more careful. After transpose, the shape is (B*N, N, h, d). Element at position (bn, j, head, dd) goes to position bn*N*h*d + j*h*d + head*d + dd. After reshape to (B, N, N, D): - Element (bn, j, head, dd) maps to (b, i, j, head*d + dd) where bn = b*N + i. So out[b, i, j, d_idx] = attn_output_before_reshape[b*N + i, j, head, dd]. But attn_output_before_reshape comes from attn @ v before transpose: - attn @ v is (B*N, h, N, d) - Element (bn, head, j, dd) is at bn*h*N*d + head*N*d + j*d + dd After transpose(1, 2): - Element (bn, j, head, dd) comes from (bn, head, j, dd) So the final mapping is: out[b, i, j, d_idx] = (attn @ v)[b*N + i, head, j, dd] And (attn @ v)[bn, head, j, dd] = sum_k(attn[bn, head, j, k] * v[bn, head, k, dd]) So for output position (b, i, j), the value is computed from attention over row i, with j being the query position. In my kernel: - I'm storing output at: output[bn * h * N * d + head * N * d + q_pos * d + dd] - This is correct if q_pos = j. In my output kernel, I'm reading: ```cpp float val = attn_out[bn * h * N * d + head * N * d + j * d + dd]; ``` Where j is the column index from the output position (i, j). This should be correct. But wait, I see another issue. In the reshape, after `(attn @ v).transpose(1, 2)`, the tensor is (B*N, N, h, d). Then after reshape(B, N, N, D), it should be treating the contiguous layout differently. Actually, let me check if my understanding of reshape is correct. In PyTorch: - `.reshape(B, N, N, D)` on a tensor of shape (B*N, N, h, d) with D = h*d - The contiguous memory layout matters After transpose(1, 2), if the tensor is not contiguous, the reshape might give different results. Let me check if `.contiguous()` is needed. In the reference: ```python out = (attn @ v).transpose(1, 2).reshape(B, N, N, D) ``` Transpose does not make the tensor contiguous, so the data might be transposed in memory. Reshape on a non-contiguous tensor will first make it contiguous and then reshape. Actually, I think PyTorch's reshape will handle this correctly by viewing the memory in the right order. But for CUDA, I need to be careful. Let me simplify and check the attention computation itself. The main issue I identified is the bias indexing. Let me fix that first.
assistantI see the issue - the bias indexing is wrong. The bias should use the query position `q_pos`, not the row index. Let me fix this:
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x: index over B*N rows\n // blockIdx.y: index over h heads \n // blockIdx.z: index over N query positions\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N; // row index in pair representation (i)\n int tid = threadIdx.x;\n \n // Shared memory layout\n extern __shared__ float smem[];\n float* q_shared = smem; // [d] - Q for this query position\n float* scores = &smem[d]; // [N] - attention scores\n \n // Cooperatively load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores for all key positions\n // Each thread handles a subset of key positions\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n // Load K for this key position and compute dot product\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n // attn[b, i, head, j, k] += bias[b, head, j, k]\n // Here q_pos = j, k_pos = k\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n \n // Apply mask: mask is (B, N, N) at (b, i, k_pos)\n // mask[b, row, k_pos] = mask[b * N * N + row * N + k_pos]\n if (mask[b * N * N + row * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Find max score for numerical stability\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp(score - max) and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V for each output dimension\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for column j\n // attn_out layout: (B*N, h, N, d)\n // We need attn_out[bn, head, j, dd]\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Compute sigmoid gate and apply\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + out_dim]));\n val *= g;\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Reshape x for linear projections\n // x is (B, N, N, D), flatten to (B*N*N, D)\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n // Q, K, V: (B*N*N, D) -> (B*N, N, h, d) -> (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // bias: (B*N*N, h) -> (B, N, N, h) -> (B, h, N, N)\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: Q (d) + scores (N)\n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N); // B*N blocks, h heads, N query positions\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(), // pass pre-sigmoid gate values\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v4',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 8 · 1 tool call(s) · 30.8s turn · 30.8s LLM · tokens in=30,420 · out=3,809 · total=34,229 · 15,233 chars (click to expand)
turn 830.8s turn · 30.8s LLM · 0.0s tools · tokens in=30,420 · out=3,809 · total=34,229
reasoning
Let me test correctness again.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x: index over B*N rows\n // blockIdx.y: index over h heads \n // blockIdx.z: index over N query positions\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N; // row index in pair representation (i)\n int tid = threadIdx.x;\n \n // Shared memory layout\n extern __shared__ float smem[];\n float* q_shared = smem; // [d] - Q for this query position\n float* scores = &smem[d]; // [N] - attention scores\n \n // Cooperatively load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores for all key positions\n // Each thread handles a subset of key positions\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n // Load K for this key position and compute dot product\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n // attn[b, i, head, j, k] += bias[b, head, j, k]\n // Here q_pos = j, k_pos = k\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n \n // Apply mask: mask is (B, N, N) at (b, i, k_pos)\n // mask[b, row, k_pos] = mask[b * N * N + row * N + k_pos]\n if (mask[b * N * N + row * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Find max score for numerical stability\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp(score - max) and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V for each output dimension\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for column j\n // attn_out layout: (B*N, h, N, d)\n // We need attn_out[bn, head, j, dd]\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Compute sigmoid gate and apply\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + out_dim]));\n val *= g;\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Reshape x for linear projections\n // x is (B, N, N, D), flatten to (B*N*N, D)\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n // Q, K, V: (B*N*N, D) -> (B*N, N, h, d) -> (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // bias: (B*N*N, h) -> (B, N, N, h) -> (B, h, N, N)\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: Q (d) + scores (N)\n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N); // B*N blocks, h heads, N query positions\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(), // pass pre-sigmoid gate values\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v4',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← run_correctness (fail)
failrun_correctness run_correctness FAILED: (0 / 3) trials did not all match. correctness_issue: Output mismatch max_difference: ['2.667119', '2.781310', '1.098691'] avg_difference: ['0.467305', '0.467236', '0.500998']
turn 9 · 1 tool call(s) · 183.1s turn · 65.6s LLM · tokens in=34,300 · out=7,350 · total=41,650 · 24,648 chars (click to expand)
turn 9183.1s turn · 65.6s LLM · 117.5s tools · tokens in=34,300 · out=7,350 · total=41,650
reasoning
The issue is still the same. Let me look more carefully at the shapes in the reference: Reference: 1. `x = self.layer_norm(pair_repr)` → (B, N, N, D) 2. `g = torch.sigmoid(self.gate(x))` → (B, N, N, D) 3. `bias = self.bias_proj(x).permute(0, 3, 1, 2)` → (B, h, N, N) 4. `x_flat = x.reshape(B * N, N, D)` → **(B*N, N, D)** - this is the key! 5. `q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2)` → **(B*N, h, N, d)** Ah! The reshape is different! In the reference: - `x_flat = x.reshape(B * N, N, D)` means we flatten (B, N, N, D) to (B*N, N, D) - This means the first N in the shape is the row index, and the second N is the column index Let me trace through this more carefully: - x has shape (B, N, N, D) = (B, i, j, D) - x.reshape(B*N, N, D) = (B*i, j, D) = (B*N, j, D) - So `bn = b * N + i` is the batch-row index - The attention is across columns j for each row i So for query at position q_pos (column j): - q = self.query(x_flat) projects x_flat to get (B*N, N, D) - q.view(B*N, N, h, d) = (bn, j, h, d) - q.transpose(1, 2) = (bn, h, j, d) So the query for (bn, h, q_pos) is the query for row `bn = b*N + i`, column `q_pos = j`. Now for the bias: - bias = self.bias_proj(x).permute(0, 3, 1, 2) - self.bias_proj(x) projects x from (B, N, N, D) to (B, N, N, h) - permute(0, 3, 1, 2) = (B, h, N, N) But wait, in the attention: - attn = attn.view(B, N, h, N, N) → attn[b, i, h, j, k] - bias.unsqueeze(1) → bias[b, 1, h, N, N] = bias[b, 1, h, j, k] broadcast - attn = attn + bias.unsqueeze(1) → attn[b, i, h, j, k] + bias[b, 1, h, j, k] - So bias[b, h, j, k] is added to all rows i for the same column j and key k But wait, that doesn't match what I have. The bias has shape (B, h, N, N) where the two N dimensions are for what? Looking at: ```python bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) ``` x has shape (B, N, N, D). After bias_proj (which goes from D to h), we get (B, N, N, h). Then permute(0, 3, 1, 2) gives (B, h, N, N). So bias[b, h, i, j] = bias_proj(x[b, i, j, :])[h] In the attention: - attn = attn.view(B, N, h, N, N) → attn[b, i, h, j, k] - bias.unsqueeze(1) → (B, 1, h, N, N) - So bias[b, :, h, j, k] is the same for all i, and it's bias[b, h, j, k] So the bias is: - bias[b, h, j, k] = bias_proj(x[b, j, k, :])[h] Wait, but that means the bias at (j, k) uses x at (j, k), not at (i, something). Actually looking at it again: - bias = self.bias_proj(x) has shape (B, N, N, h) = (B, i, j, h) using the indices from x - After permute: (B, h, N, N) = (B, h, i, j) - But the attention uses bias at (b, h, j, k) where j is the query position and k is the key position This is confusing. Let me look at the attention add again: ```python attn = attn.view(B, N, h, N, N) # (B, i, h, j, k) # Add pair bias: bias (B, h, N, N) → (B, 1, h, N, N) broadcast across rows attn = attn + bias.unsqueeze(1) # bias[b, :, h, j, k] = bias[b, h, j, k] ``` So the bias added is bias[b, h, j, k]. With bias having shape (B, h, N, N), this is the element at position (j, k) in the last two dimensions. But bias = bias_proj(x).permute(0, 3, 1, 2), where x has shape (B, i, j, D). - bias_proj(x) has shape (B, i, j, h) - permute(0, 3, 1, 2) has shape (B, h, i, j) So bias[b, h, i, j] = bias_proj(x[b, i, j, :])[h] But we're adding bias[b, h, j, k] to attn[b, i, h, j, k]. This means the bias index is (j, k) which corresponds to x[b, j, k, :]. OK so the bias uses the query position j as its "row" and the key position k as its "column". This makes sense - it's the pair representation at (j, k). But wait, I'm computing the bias from x at all positions. Let me check my bias reshaping: - I have bias_proj with shape (B*N*N, h) from flattening x to (B*N*N, D) - Then I reshape to (B, N, N, h) and permute to (B, h, N, N) So bias_reshaped[b, h, i, j] should be bias_proj for position (b, i, j). In my kernel, I access: ```cpp score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos]; ``` This is bias[b, head, q_pos, k_pos], which should be bias_proj(x[b, q_pos, k_pos, :])[head]. But wait, x is the layer norm output with shape (B, N, N, D). I'm computing bias from x at position (b, i, j) which should give bias_proj(x[b, i, j, :]). But the attention uses bias at position (b, h, q_pos, k_pos), which would be bias_proj(x[b, q_pos, k_pos, :]). So the issue is: in my implementation, I'm computing bias for each position (i, j), but I should be accessing it based on the query position q_pos and key position k_pos. Actually wait, let me check again. In my code: - I compute bias_proj for each position in x: bias_proj[b * N * N + i * N + j, head] = bias_proj(x[b, i, j, :])[head] - Then I reshape and permute: bias_reshaped[b, head, i, j] = bias_proj[b * N * N + i * N + j, head] So bias_reshaped[b, head, i, j] = bias_proj(x[b, i, j, :])[head]. In the attention kernel, I access: ```cpp score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos]; ``` This is bias[b, head, q_pos, k_pos] = bias_proj(x[b, q_pos, k_pos, :])[head]. This looks correct! But wait, let me check the reference again: In reference: - bias = self.bias_proj(x).permute(0, 3, 1, 2) → (B, h, N, N) - bias[b, h, i, j] = bias_proj(x[b, i, j, :])[h] But wait, in the reference, x still has its original dimensions (B, N, N, D). So bias_proj(x) = bias_proj(x[b, i, j, :]) for each (i, j). And then in the attention: - attn = attn.view(B, N, h, N, N) → attn[b, i, h, j, k] - attn = attn + bias.unsqueeze(1) → bias[b, h, j, k] So the bias is bias[b, h, j, k] = bias_proj(x[b, j, k, :])[h]. Hmm, but my bias_reshaped[b, head, q_pos, k_pos] = bias_proj(x[b, q_pos, k_pos, :])[head]. This should be correct since q_pos is j and k_pos is k. Let me check another thing - the mask. In reference: ```python # Apply mask: pair_mask (B, N, N) → mask over the key dimension mask = pair_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, N, N) – last dim is keys attn = attn.masked_fill(mask == 0, float("-inf")) ``` So mask is (B, 1, 1, N, N) which broadcasts to (B, N, h, N, N). The mask at position [b, :, :, j, k] is mask[b, 0, 0, j, k] = pair_mask[b, j, k]. But wait, that doesn't seem right. Let me trace through: - pair_mask has shape (B, N, N) = (B, i, j) - mask = pair_mask.unsqueeze(1).unsqueeze(2) → (B, 1, 1, N, N) - attn has shape (B, N, h, N, N) = (B, i, h, j, k) - The mask broadcasts to (B, N, h, N, N), so mask[b, i, h, j, k] = mask[b, 0, 0, j, k] - But mask[b, 0, 0, j, k] = pair_mask[b, j, k]?? That doesn't match the dimensions. Actually wait, let me re-read: ```python mask = pair_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, N, N) – last dim is keys ``` pair_mask has shape (B, N, N). unsqueeze(1) gives (B, 1, N, N). unsqueeze(2) gives (B, 1, 1, N, N). So mask[b, 0, 0, i, j] = pair_mask[b, i, j]. In the attention: - attn has shape (B, N, h, N, N) = (B, i, h, j, k) where j is query and k is key - mask broadcasts to (B, N, h, N, N), so mask[b, i, h, j, k] = mask[b, 0, 0, j, k] - But wait, mask[b, 0, 0, j, k] should be pair_mask[b, j, k] Hmm, but the comment says "last dim is keys". So the mask at (b, i, h, j, k) uses pair_mask at (b, ?, ?). Actually, the broadcasting works like this: - mask has shape (B, 1, 1, N, N) - attn has shape (B, N, h, N, N) - For element attn[b, i, h, j, k], the corresponding mask element is mask[b, 0, 0, j, k] - But mask[b, 0, 0, j, k] = pair_mask[b, j, k]... no wait, pair_mask has shape (B, N, N) Let me trace this more carefully: - pair_mask has shape (B, N, N) - pair_mask.unsqueeze(1) has shape (B, 1, N, N) - pair_mask.unsqueeze(1).unsqueeze(2) has shape (B, 1, 1, N, N) So the original indices (b, i, j) in pair_mask become (b, 0, 0, i, j) in mask. Now for attn with shape (B, N, h, N, N) = (B, i, h, j, k): - The mask broadcasts from (B, 1, 1, N, N) to (B, N, h, N, N) - mask[b, i, h, j, k] = mask[b, 0, 0, j, k] = pair_mask[b, j, k] But wait, that means the mask uses pair_mask[b, j, k] where j is the query position and k is the key position. But in my implementation, I use mask[b, row, k_pos] where row = i. Hmm, but the reference uses: ```python mask = pair_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, N, N) ``` So mask[b, 0, 0, :, :] = pair_mask[b, :, :]. The indices in mask are (b, :, :, dim3, dim4) = (b, :, :, i, j) from the original pair_mask. So mask[b, 0, 0, i, j] = pair_mask[b, i, j]. When broadcast to attn with shape (B, N, h, N, N) = (B, i, h, j, k): - The third and fourth dimensions of mask (both N) correspond to j and k in attn - So mask[b, 0, 0, j, k] is applied to attn[b, i, h, j, k] This means the mask at position (b, i, h, j, k) uses pair_mask[b, j, k]. But in my kernel, I use: ```cpp if (mask[b * N * N + row * N + k_pos] == 0.0f) { ``` This is mask[b, row, k_pos] = pair_mask[b, row, k_pos], which uses the row index (i) instead of the query position (j). I need to fix this to use: ```cpp if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) { ``` Let me also double-check the bias indexing. The bias at (b, h, j, k) should be added to all rows i. So the bias is: - bias[b, h, j, k] = bias_proj(x[b, j, k, :])[h] And in my kernel, I should access: ```cpp score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos]; ``` This looks correct. But the mask is wrong. Let me fix it.
assistantLet me trace through the reference more carefully. I see the issue - the mask indexing uses the query position, not the row:
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x: index over B*N rows\n // blockIdx.y: index over h heads \n // blockIdx.z: index over N query positions\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N; // row index in pair representation (i)\n int tid = threadIdx.x;\n \n // Shared memory layout\n extern __shared__ float smem[];\n float* q_shared = smem; // [d] - Q for this query position\n float* scores = &smem[d]; // [N] - attention scores\n \n // Cooperatively load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores for all key positions\n // Each thread handles a subset of key positions\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n // Load K for this key position and compute dot product\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n // Reference: attn[b, i, h, j, k] += bias[b, h, j, k]\n // So bias index uses q_pos (j) and k_pos (k)\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n \n // Apply mask: mask is (B, N, N)\n // Reference: mask broadcasts from (B, 1, 1, N, N) to attn (B, N, h, N, N)\n // mask[b, i, h, j, k] = mask[b, 0, 0, j, k] = pair_mask[b, j, k]\n // So mask index uses q_pos (j) and k_pos (k)\n if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Find max score for numerical stability\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp(score - max) and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V for each output dimension\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for column j\n // attn_out layout: (B*N, h, N, d)\n // We need attn_out[bn, head, j, dd]\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Compute sigmoid gate and apply\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + out_dim]));\n val *= g;\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Reshape x for linear projections\n // x is (B, N, N, D), flatten to (B*N*N, D)\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n // Q, K, V: (B*N*N, D) -> (B*N, N, h, d) -> (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // bias: (B*N*N, h) -> (B, N, N, h) -> (B, h, N, N)\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: Q (d) + scores (N)\n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N); // B*N blocks, h heads, N query positions\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(), // pass pre-sigmoid gate values\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v5',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 10 · 1 tool call(s) · 72.2s turn · 32.0s LLM · tokens in=38,200 · out=3,876 · total=42,076 · 15,425 chars (click to expand)
turn 1072.2s turn · 32.0s LLM · 40.2s tools · tokens in=38,200 · out=3,876 · total=42,076
reasoning
Let me run correctness again to check if I fixed the mask indexing.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n // blockIdx.x: index over B*N rows\n // blockIdx.y: index over h heads \n // blockIdx.z: index over N query positions\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N; // row index in pair representation (i)\n int tid = threadIdx.x;\n \n // Shared memory layout\n extern __shared__ float smem[];\n float* q_shared = smem; // [d] - Q for this query position\n float* scores = &smem[d]; // [N] - attention scores\n \n // Cooperatively load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores for all key positions\n // Each thread handles a subset of key positions\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n // Load K for this key position and compute dot product\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n \n // Add bias: bias is (B, h, N, N)\n // Reference: attn[b, i, h, j, k] += bias[b, h, j, k]\n // So bias index uses q_pos (j) and k_pos (k)\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n \n // Apply mask: mask is (B, N, N)\n // Reference: mask broadcasts from (B, 1, 1, N, N) to attn (B, N, h, N, N)\n // mask[b, i, h, j, k] = mask[b, 0, 0, j, k] = pair_mask[b, j, k]\n // So mask index uses q_pos (j) and k_pos (k)\n if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n \n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Find max score for numerical stability\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n // Compute exp(score - max) and sum\n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n // Sum reduction across threads\n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n // Normalize scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V for each output dimension\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n // Each block handles one (b, i, j) position\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Read attention output for column j\n // attn_out layout: (B*N, h, N, d)\n // We need attn_out[bn, head, j, dd]\n int head = out_dim / d;\n int dd = out_dim % d;\n float val = attn_out[bn * h * N * d + head * N * d + j * d + dd];\n \n // Compute sigmoid gate and apply\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + out_dim]));\n val *= g;\n \n // Output projection\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n out_val += val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Reshape x for linear projections\n // x is (B, N, N, D), flatten to (B*N*N, D)\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n // Q, K, V: (B*N*N, D) -> (B*N, N, h, d) -> (B*N, h, N, d)\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n \n // bias: (B*N*N, h) -> (B, N, N, h) -> (B, h, N, N)\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n // Shared memory: Q (d) + scores (N)\n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N); // B*N blocks, h heads, N query positions\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(), // pass pre-sigmoid gate values\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v5',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← run_correctness (fail)
failrun_correctness run_correctness FAILED: (0 / 3) trials did not all match. correctness_issue: Output mismatch max_difference: ['2.667119', '2.781310', '1.098691'] avg_difference: ['0.467305', '0.467236', '0.500998']
turn 11 · 1 tool call(s) · 156.7s turn · 72.2s LLM · tokens in=42,139 · out=7,830 · total=49,969 · 27,723 chars (click to expand)
turn 11156.7s turn · 72.2s LLM · 84.5s tools · tokens in=42,139 · out=7,830 · total=49,969
reasoning
Looking at the reference code more carefully: ```python # Reshape to (B*N, N, D) — attention across columns for each row x_flat = x.reshape(B * N, N, D) q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2) # (B*N, h, N, d) ``` So Q, K, V are computed on x_flat which is (B*N, N, D), and then reshaped to (B*N, h, N, d). But wait, the query projection is done on x_flat, which has shape (B*N, N, D). So each position (b, i, j) contributes to Q[b*N+i, j, :], and the query weight transforms the D features. The issue is that I'm computing Q, K, V for all positions (B*N*N, D) first, then reshaping. But the reference computes Q, K, V on x_flat of shape (B*N, N, D). Let me trace through more carefully: - x has shape (B, N, N, D) - x_flat = x.reshape(B * N, N, D) - this is the normalized x - q = self.query(x_flat) - this applies the linear projection to the last dimension - q has shape (B*N, N, D) - q.view(B * N, N, h, d).transpose(1, 2) gives shape (B*N, h, N, d) So the query for position (b, i, j) is at index (b*N + i, h_idx, j, d_idx). But in my implementation, I'm computing Q for all positions first: - query has shape (B*N*N, D) - q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}) - This gives shape (B*N, h, N, d) But this is wrong! The view should be (B, N, N, h, d) first, then somehow reshape to (B*N, h, N, d). Actually, let me think about this differently: - x_flat has shape (B*N, N, D) - The query projects x_flat to get (B*N, N, D) - Then view gives (B*N, N, h, d) - Then transpose gives (B*N, h, N, d) So for each batch and row index (b, i), we have N queries (one for each j), and each query attends to N keys. In my implementation: - I compute query on x_flat (B*N*N, D) - But the reference computes query on x.reshape(B*N, N, D) The difference is: - My way: query[b*N*N + i*N + j, :] = linear(x[b, i, j, :]) - Reference way: query[b*N + i, j, :] = linear(x[b, i, j, :]) These are mathematically the same! The linear projection is element-wise on the last dimension. So: - My query has shape (B*N*N, D) - Reference query has shape (B*N, N, D) But the values should be the same, just reshaped differently. Let me check the reshape: - My query.view({BN, N, h, d}) - this takes the first BN*N*h*d = B*N*N*D elements and reshapes - But BN*N = B*N*N, so this is the same as (B*N, N, h, d), and then permute gives (B*N, h, N, d) Wait, but BN = B*N, so BN*N = B*N*N. So my view is (B*N, N, h, d), which is the same as the reference! Hmm, but there's a subtle issue. Let me check if my indexing is correct for the bias. In the reference: - bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) - bias is computed on x which has shape (B, N, N, D) - bias_proj output has shape (B, N, N, h) - After permute, bias has shape (B, h, N, N) So bias[b, h_idx, i, j] is the bias value for position (i, j) in batch b for head h_idx. In the attention: - attn = attn.view(B, N, h, N, N) # from (B*N, h, N, N) - attn = attn + bias.unsqueeze(1) # bias.unsqueeze(1) is (B, 1, h, N, N) - So attn[b, i, h_idx, j, k] += bias[b, h_idx, j, k] So the bias is added based on the query position j and key position k, not the row i. Let me trace through the mask: - mask = pair_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, N, N) - attn = attn.masked_fill(mask == 0, float("-inf")) - So mask[b, 0, 0, j, k] is used for attn[b, i, h_idx, j, k] - pair_mask[b, j, k] determines the mask So the mask is also based on query position j and key position k. Let me re-check my kernel. In the attention kernel: - q_pos is the query position (j in my notation) - k_pos is the key position (k in my notation) - For bias: I use bias[b * h * N * N + head * N * N + q_pos * N + k_pos] - This is bias[b, head, q_pos, k_pos] which is correct! Wait, let me check the reshape again. In the host code: - bias_proj is (total_positions, h) where total_positions = B*N*N - bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}) - This gives (B, h, N, N) So bias_reshaped[b, h_idx, i, j] = bias_proj[b*N*N + i*N + j, h_idx] But wait, this is the bias for position (i, j), not (j, k). Let me think... Actually, the confusion is in the notation. In the reference: - bias has shape (B, h, N, N) - bias[b, h_idx, row, col] where row and col are indices When added to attn: - attn has shape (B, N, h, N, N) - attn[b, i, h_idx, j, k] += bias[b, h_idx, j, k] - So the bias uses j and k as indices, where j is the query position and k is the key position In my bias_reshaped: - bias_reshaped[b, h_idx, i, j] = bias_proj[b*N*N + i*N + j, h_idx] - But I need bias[b, h_idx, q_pos, k_pos] for the attention score at position (q_pos, k_pos) The issue is that bias_proj[b*N*N + i*N + j, h_idx] corresponds to the bias for the position (i, j) in the original x tensor. But in the attention, we're adding bias based on the query and key positions. Actually wait, let me re-read the reference: ```python bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) ``` Here x has shape (B, N, N, D), and bias_proj(x) has shape (B, N, N, h). So: - bias_proj(x)[b, i, j, h_idx] is the bias for position (i, j) in batch b for head h_idx Then permute(0, 3, 1, 2) gives: - bias[b, h_idx, i, j] = bias_proj(x)[b, i, j, h_idx] And in the attention: - attn = attn.view(B, N, h, N, N) # attn shape: (B, N, h, N, N) - attn = attn + bias.unsqueeze(1) # bias.unsqueeze(1) shape: (B, 1, h, N, N) The broadcasting adds bias[b, :, h_idx, j, k] to attn[b, i, h_idx, j, k]. So bias[b, h_idx, j, k] is added, which means the bias is indexed by j (query position) and k (key position). But in my kernel, I'm computing bias_proj for each position (i, j) in the original x tensor. So: - bias_proj[b*N*N + i*N + j, h_idx] = bias for position (i, j) And I reshape to (B, N, N, h) then permute to (B, h, N, N): - bias_reshaped[b, h_idx, i, j] = bias_proj[b*N*N + i*N + j, h_idx] But this is the bias for position (i, j)! The reference's bias is for position (j, k) in the attention. The issue is that the bias_proj is applied to x, which has shape (B, N, N, D), so the bias for each position (i, j) is computed from x[b, i, j, :]. But then this bias is used for the attention scores where: - j is the query position - k is the key position So bias[b, h_idx, j, k] should be the bias computed from x[b, j, k, :], not x[b, i, j, :]! Let me re-read the reference again... Actually, I think I'm confusing myself. Let me be more careful: In the reference: - x has shape (B, N, N, D) - bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N) - So bias_proj(x)[b, i, j, :] has shape h, and bias[b, h_idx, i, j] = bias_proj(x)[b, i, j, h_idx] The bias tensor has indices (b, h_idx, i, j), where i and j are indices from 0 to N-1. Then in attention: - attn = attn.view(B, N, h, N, N) # (B, N, h, N, N) - attn[b, i, h_idx, j, k] is the attention score - attn = attn + bias.unsqueeze(1) # bias.unsqueeze(1) has shape (B, 1, h, N, N) - bias.unsqueeze(1)[b, 0, h_idx, j, k] = bias[b, h_idx, j, k] - This broadcasts to all i, so attn[b, i, h_idx, j, k] += bias[b, h_idx, j, k] So the bias is indeed indexed by j (query position) and k (key position), not i (row index). In my implementation: - bias_proj is computed from x_flat which is (B*N*N, D) - bias_proj[b*N*N + i*N + j, h_idx] is the bias for position (i, j) - bias_reshaped[b, h_idx, i, j] = bias_proj[b*N*N + i*N + j, h_idx] When I index bias_reshaped[b, h_idx, q_pos, k_pos], I'm getting the bias for position (q_pos, k_pos), which is what the reference expects! So my indexing should be correct. Let me check the mask again. In the reference: - pair_mask has shape (B, N, N) - mask = pair_mask.unsqueeze(1).unsqueeze(2) # (B, 1, 1, N, N) - mask[b, 0, 0, j, k] = pair_mask[b, j, k] And attn is masked: - attn[b, i, h_idx, j, k] is masked by mask[b, 0, 0, j, k] = pair_mask[b, j, k] So the mask uses j and k as indices. In my kernel: - I use mask[b * N * N + q_pos * N + k_pos] - This is mask[b, q_pos, k_pos] = pair_mask[b, q_pos, k_pos] This should be correct! Let me think about what else could be wrong... Actually, I think the issue might be in how I'm reading the attention output in the output kernel. In the reference: - out = (attn @ v).transpose(1, 2).reshape(B, N, N, D) - attn has shape (B*N, h, N, N) after softmax - v has shape (B*N, h, N, d) - attn @ v has shape (B*N, h, N, d) - transpose(1, 2) gives (B*N, N, h, d) - reshape(B, N, N, D) gives (B, N, N, D) So the output before gating is: - out[b, i, j, h_idx * d + d_idx] = (attn @ v)[b*N + i, j, h_idx, d_idx] - But (attn @ v)[b*N + i, h_idx, j, d_idx] = sum over k of attn[b*N + i, h_idx, j, k] * v[b*N + i, h_idx, k, d_idx] - After transpose: out_before_gate[b*N + i, j, h_idx, d_idx] = (attn @ v)[b*N + i, h_idx, j, d_idx] So the output for position (b, i, j) is the result of the attention at query position j for the row (b, i). In my output kernel: - I'm reading attn_out[bn * h * N * d + head * N * d + j * d + dd] - This is attn_out[bn, head, j, dd] - bn = b * N + i So I'm reading the attention output for query position j, which is correct! Let me check the gate: - g = torch.sigmoid(self.gate(x)) where x is the normalized representation - g has shape (B, N, N, D) - out = g * out_before_gate So g[b, i, j, :] is applied to out_before_gate[b, i, j, :]. In my output kernel: - gate_proj has shape (B*N*N, D) which is the pre-sigmoid gate values - gate_proj[idx * D + out_dim] where idx = b*N*N + i*N + j - This is gate_proj[b*N*N + i*N + j, out_dim] - I apply sigmoid and multiply with the attention output This should be correct! Hmm, let me check the output projection: - out_proj has shape (D, D) for weight and (D,) for bias - output = self.out_proj(g * out_before_gate) - output[b, i, j, :] = out_proj.weight @ (g * out_before_gate)[b, i, j, :] + out_proj.bias In my output kernel: - I compute val = g * attn_out_value - Then out_val = b_out[out_dim] + sum over in_f of val * w_out[out_dim * D + in_f] Wait, this is wrong! The output projection should be: - output[batch, out_dim] = sum over in_f of input[batch, in_f] * weight[out_dim, in_f] + bias[out_dim] So for each output dimension, I sum over all input dimensions. Let me check my kernel: ```cpp float out_val = b_out[out_dim]; for (int in_f = 0; in_f < D; in_f++) { out_val += val * w_out[out_dim * D + in_f]; } ``` This is summing val * weight[out_dim, in_f] over in_f. But val should depend on in_f! The issue is that I'm using the same `val` for all in_f, but val should be different for each in_f. The input to the output projection is the gated attention output, which has D dimensions. For each output dimension, I need to sum over all D input dimensions. But in my kernel, each thread handles one output dimension (out_dim). So I should: 1. For each output dimension, I need to read all D input values 2. Compute the gated values for all D input dimensions 3. Then do the matrix multiplication But currently, I'm reading only one attention output value (for a specific head and d_idx) and using that for all in_f. Let me fix this. I need to restructure the kernel so that each thread block handles one output position, and all threads cooperatively compute the output projection. Actually, the issue is even more fundamental. Let me trace through the dimensions again. The attention output attn_out has shape (B*N, h, N, d) = (B*N, h, N, d). For position (b, i, j), we need: - attn_out[b*N + i, h_idx, j, d_idx] for all h_idx and d_idx The output before gating should have shape (B, N, N, D). - out_before_gate[b, i, j, D_idx] where D_idx = h_idx * d + d_idx So I need to gather the attention output from all heads and all d values for position (b, i, j). In my current kernel, I'm iterating over out_dim which goes from 0 to D-1, and for each out_dim: - head = out_dim / d - dd = out_dim % d - val = attn_out[bn * h * N * d + head * N * d + j * d + dd] So val is attn_out[bn, head, j, dd]. This should be correct for gathering the attention output. But then I apply the gate and do the output projection. The gate is applied to each dimension independently, so: - gated_val[out_dim] = sigmoid(gate_proj[idx * D + out_dim]) * attn_out[bn, head, j, dd] Then the output projection: - output[idx * D + out_dim] = sum over in_f of gated_val[in_f] * weight[out_dim, in_f] + bias[out_dim] But I'm computing this wrong! I should be summing over in_f, but I'm computing: - out_val = bias[out_dim] + val * sum over in_f of weight[out_dim, in_f] This is wrong because val doesn't depend on in_f. I need to restructure the kernel to: 1. Each thread block handles one output position (b, i, j) 2. Each thread handles one output dimension 3. For each output dimension, sum over all input dimensions 4. But each input dimension comes from a different thread! This requires shared memory to store all the gated values, then a parallel reduction. Actually, let me simplify by having each thread iterate over all input dimensions: ```cpp float out_val = b_out[out_dim]; for (int in_f = 0; in_f < D; in_f++) { // Read gated value for input dimension in_f int in_head = in_f / d; int in_dd = in_f % d; float in_val = attn_out[bn * h * N * d + in_head * N * d + j * d + in_dd]; float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + in_f])); in_val *= g; out_val += in_val * w_out[out_dim * D + in_f]; } output[idx * D + out_dim] = out_val; ``` This would work but is inefficient because each thread reads all D input dimensions. Let me fix this.
assistantLet me trace through the dimensions more carefully. The issue is in the output kernel - I'm not correctly computing the output projection:
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int tid = threadIdx.x;\n \n extern __shared__ float smem[];\n float* q_shared = smem;\n float* scores = &smem[d];\n \n // Load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Softmax\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Output projection: output = (gated_attention_output) @ w_out.T + b_out\n // For each output dimension, we need to sum over all input dimensions\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n // Read attention output for input dimension in_f\n int in_head = in_f / d;\n int in_dd = in_f % d;\n float attn_val = attn_out[bn * h * N * d + in_head * N * d + j * d + in_dd];\n \n // Apply sigmoid gate\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + in_f]));\n float gated_val = g * attn_val;\n \n // Accumulate for output projection\n out_val += gated_val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Flatten for linear projections\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N);\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(),\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v6',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 12 · 1 tool call(s) · 28.1s turn · 28.1s LLM · tokens in=45,649 · out=3,460 · total=49,109 · 14,202 chars (click to expand)
turn 1228.1s turn · 28.1s LLM · 0.0s tools · tokens in=45,649 · out=3,460 · total=49,109
reasoning
I need to test the correctness of my latest kernel before submitting. Let me run the correctness check.
call → run_correctness
callrun_correctness { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// LayerNorm kernel for (B, N, N, D) tensor\n__global__ void layernorm_kernel(\n const float* __restrict__ input,\n float* __restrict__ output,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n int total_positions, int D, float eps\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n if (idx >= total_positions) return;\n \n const float* in = input + idx * D;\n float* out = output + idx * D;\n \n // Compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += in[d];\n }\n mean /= D;\n \n // Compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = in[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n float inv_std = rsqrtf(var + eps);\n \n // Normalize and apply weight/bias\n for (int d = 0; d < D; d++) {\n out[d] = (in[d] - mean) * inv_std * weight[d] + bias[d];\n }\n}\n\n// Linear projection kernel: output = input @ weight.T + bias\n__global__ void linear_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int M, int in_features, int out_features, bool has_bias\n) {\n int m = blockIdx.y;\n int out_f = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (m >= M || out_f >= out_features) return;\n \n float sum = has_bias ? bias[out_f] : 0.0f;\n for (int in_f = 0; in_f < in_features; in_f++) {\n sum += input[m * in_features + in_f] * weight[out_f * in_features + in_f];\n }\n output[m * out_features + out_f] = sum;\n}\n\n// Attention computation kernel\n// query, key, value: (B*N, h, N, d)\n// bias: (B, h, N, N)\n// mask: (B, N, N)\n// output: (B*N, h, N, d)\n__global__ void attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int tid = threadIdx.x;\n \n extern __shared__ float smem[];\n float* q_shared = smem;\n float* scores = &smem[d];\n \n // Load Q for this query position\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute attention scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n for (int dd = 0; dd < d; dd++) {\n float k_val = key[bn * h * N * d + head * N * d + k_pos * d + dd];\n score += q_shared[dd] * k_val;\n }\n score *= scale;\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Softmax\n __shared__ float smax;\n if (tid == 0) {\n smax = -INFINITY;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n if (scores[k_pos] > smax) smax = scores[k_pos];\n }\n }\n __syncthreads();\n \n __shared__ float ssum;\n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum of V\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n float v_val = value[bn * h * N * d + head * N * d + k_pos * d + dd];\n out_val += scores[k_pos] * v_val;\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\n// Output kernel: reshape, apply gate, output projection\n__global__ void output_kernel(\n const float* __restrict__ attn_out, // (B*N, h, N, d)\n const float* __restrict__ gate_proj, // (B, N, N, D) - before sigmoid\n const float* __restrict__ w_out, // (D, D)\n const float* __restrict__ b_out, // (D)\n float* __restrict__ output, // (B, N, N, D)\n int B, int N, int h, int d, int D\n) {\n int idx = blockIdx.x;\n int total = B * N * N;\n if (idx >= total) return;\n \n int b = idx / (N * N);\n int ij = idx % (N * N);\n int i = ij / N;\n int j = ij % N;\n \n int bn = b * N + i;\n int out_dim = threadIdx.x;\n \n if (out_dim >= D) return;\n \n // Output projection: output = (gated_attention_output) @ w_out.T + b_out\n // For each output dimension, we need to sum over all input dimensions\n float out_val = b_out[out_dim];\n for (int in_f = 0; in_f < D; in_f++) {\n // Read attention output for input dimension in_f\n int in_head = in_f / d;\n int in_dd = in_f % d;\n float attn_val = attn_out[bn * h * N * d + in_head * N * d + j * d + in_dd];\n \n // Apply sigmoid gate\n float g = 1.0f / (1.0f + expf(-gate_proj[idx * D + in_f]));\n float gated_val = g * attn_val;\n \n // Accumulate for output projection\n out_val += gated_val * w_out[out_dim * D + in_f];\n }\n \n output[idx * D + out_dim] = out_val;\n}\n\n// Host function\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n) {\n int B = pair_repr.size(0);\n int N = pair_repr.size(1);\n int D = pair_repr.size(3);\n int h = num_heads;\n int d = head_dim;\n int BN = B * N;\n \n auto options = pair_repr.options();\n int threads = 256;\n \n // Step 1: LayerNorm\n auto x = torch::empty({B, N, N, D}, options);\n int total_positions = B * N * N;\n int blocks = (total_positions + threads - 1) / threads;\n layernorm_kernel<<<blocks, threads>>>(\n pair_repr.data_ptr<float>(),\n x.data_ptr<float>(),\n ln_weight.data_ptr<float>(),\n ln_bias.data_ptr<float>(),\n total_positions, D, 1e-5f\n );\n \n // Step 2: Flatten for linear projections\n auto x_flat = x.view({total_positions, D});\n \n // Step 3: Compute all linear projections\n auto query = torch::empty({total_positions, D}, options);\n auto key = torch::empty({total_positions, D}, options);\n auto value = torch::empty({total_positions, D}, options);\n auto gate_proj = torch::empty({total_positions, D}, options);\n auto bias_proj = torch::empty({total_positions, h}, options);\n \n dim3 grid_linear((D + threads - 1) / threads, total_positions);\n \n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_query.data_ptr<float>(), nullptr,\n query.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_key.data_ptr<float>(), nullptr,\n key.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_value.data_ptr<float>(), nullptr,\n value.data_ptr<float>(), total_positions, D, D, false\n );\n linear_kernel<<<grid_linear, threads>>>(\n x_flat.data_ptr<float>(), w_gate.data_ptr<float>(), b_gate.data_ptr<float>(),\n gate_proj.data_ptr<float>(), total_positions, D, D, true\n );\n \n dim3 grid_bias((h + threads - 1) / threads, total_positions);\n linear_kernel<<<grid_bias, threads>>>(\n x_flat.data_ptr<float>(), w_bias.data_ptr<float>(), nullptr,\n bias_proj.data_ptr<float>(), total_positions, D, h, false\n );\n \n // Step 4: Reshape for attention\n auto q_reshaped = query.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto k_reshaped = key.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto v_reshaped = value.view({BN, N, h, d}).permute({0, 2, 1, 3}).contiguous();\n auto bias_reshaped = bias_proj.view({B, N, N, h}).permute({0, 3, 1, 2}).contiguous();\n \n // Step 5: Compute attention\n auto attn_out = torch::empty({BN, h, N, d}, options);\n float scale = 1.0f / sqrtf((float)d);\n \n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid_attn(BN, h, N);\n attention_kernel<<<grid_attn, d, smem_size>>>(\n q_reshaped.data_ptr<float>(),\n k_reshaped.data_ptr<float>(),\n v_reshaped.data_ptr<float>(),\n bias_reshaped.data_ptr<float>(),\n pair_mask.data_ptr<float>(),\n attn_out.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n // Step 6: Output projection with gate\n auto output = torch::empty({B, N, N, D}, options);\n blocks = (total_positions + threads - 1) / threads;\n output_kernel<<<blocks, threads>>>(\n attn_out.data_ptr<float>(),\n gate_proj.data_ptr<float>(),\n w_out.data_ptr<float>(),\n b_out.data_ptr<float>(),\n output.data_ptr<float>(),\n B, N, h, d, D\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor triangular_attention_cuda(\n torch::Tensor pair_repr,\n torch::Tensor pair_mask,\n torch::Tensor ln_weight,\n torch::Tensor ln_bias,\n torch::Tensor w_query,\n torch::Tensor w_key,\n torch::Tensor w_value,\n torch::Tensor w_gate,\n torch::Tensor b_gate,\n torch::Tensor w_bias,\n torch::Tensor w_out,\n torch::Tensor b_out,\n int num_heads,\n int head_dim\n);\n\"\"\"\n\ntriangular_attn = load_inline(\n name='triangular_attention_v6',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['triangular_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n self.pair_dim = pair_dim\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n pair_repr = pair_repr.contiguous().float()\n pair_mask = pair_mask.contiguous().float()\n\n output = triangular_attn.triangular_attention_cuda(\n pair_repr,\n pair_mask,\n self.layer_norm.weight.contiguous(),\n self.layer_norm.bias.contiguous(),\n self.query.weight.contiguous(),\n self.key.weight.contiguous(),\n self.value.weight.contiguous(),\n self.gate.weight.contiguous(),\n self.gate.bias.contiguous(),\n self.bias_proj.weight.contiguous(),\n self.out_proj.weight.contiguous(),\n self.out_proj.bias.contiguous(),\n self.num_heads,\n self.head_dim\n )\n \n return output" }
result ← run_correctness (fail)
failrun_correctness run_correctness FAILED: (0 / 3) trials did not all match. correctness_issue: Output mismatch max_difference: ['2.667119', '2.781310', '1.098691'] avg_difference: ['0.467166', '0.467085', '0.500847']
turn 13 · 1 tool call(s) · 95.0s turn · 16.6s LLM · tokens in=49,198 · out=1,770 · total=50,968 · 7,506 chars (click to expand)
turn 1395.0s turn · 16.6s LLM · 78.4s tools · tokens in=49,198 · out=1,770 · total=50,968
reasoning
I'm having persistent issues with correctness. The problem seems to be in the attention/output computation. Let me try a simpler approach - use PyTorch for the bulk of the operations and only implement custom kernels for the most performance-critical parts. This way I can ensure correctness while still potentially getting speedups. Let me try a hybrid approach where I use PyTorch operations but try to optimize them.
assistantLet me try a simpler approach using PyTorch operations but with fused CUDA kernels only for the attention computation:
call → compile_kernel
callcompile_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// Fused attention kernel for triangular self-attention\n// Computes: softmax((Q @ K.T) * scale + bias) @ V\n__global__ void fused_attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N;\n int tid = threadIdx.x;\n \n extern __shared__ float smem[];\n float* q_shared = smem;\n float* scores = &smem[d];\n \n // Load Q\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n for (int dd = 0; dd < d; dd++) {\n score += q_shared[dd] * key[bn * h * N * d + head * N * d + k_pos * d + dd];\n }\n score *= scale;\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Softmax\n __shared__ float smax, ssum;\n if (tid == 0) {\n smax = -INFINITY;\n for (int i = 0; i < N; i++) {\n if (scores[i] > smax) smax = scores[i];\n }\n }\n __syncthreads();\n \n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n out_val += scores[k_pos] * value[bn * h * N * d + head * N * d + k_pos * d + dd];\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\ntorch::Tensor fused_attention_cuda(\n torch::Tensor query,\n torch::Tensor key,\n torch::Tensor value,\n torch::Tensor bias,\n torch::Tensor mask,\n int B, int N, int h, int d\n) {\n int BN = B * N;\n auto output = torch::empty({BN, h, N, d}, query.options());\n float scale = 1.0f / sqrtf((float)d);\n \n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid(BN, h, N);\n fused_attention_kernel<<<grid, d, smem_size>>>(\n query.data_ptr<float>(),\n key.data_ptr<float>(),\n value.data_ptr<float>(),\n bias.data_ptr<float>(),\n mask.data_ptr<float>(),\n output.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor fused_attention_cuda(\n torch::Tensor query,\n torch::Tensor key,\n torch::Tensor value,\n torch::Tensor bias,\n torch::Tensor mask,\n int B, int N, int h, int d\n);\n\"\"\"\n\nfused_attn = load_inline(\n name='fused_attention',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['fused_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n # LayerNorm\n x = self.layer_norm(pair_repr)\n g = torch.sigmoid(self.gate(x))\n\n # Bias projection\n bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N)\n\n # Reshape and project\n x_flat = x.reshape(B * N, N, D)\n q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2)\n k = self.key(x_flat).view(B * N, N, h, d).transpose(1, 2)\n v = self.value(x_flat).view(B * N, N, h, d).transpose(1, 2)\n\n # Fused attention via custom CUDA kernel\n attn_out = fused_attn.fused_attention_cuda(\n q.contiguous().float(),\n k.contiguous().float(),\n v.contiguous().float(),\n bias.contiguous().float(),\n pair_mask.contiguous().float(),\n B, N, h, d\n )\n\n # Reshape and gate\n out = attn_out.transpose(1, 2).reshape(B, N, N, D)\n out = g * out\n \n return self.out_proj(out)" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 14 · FINAL · 1 tool call(s) · 39.7s turn · 15.6s LLM · tokens in=50,989 · out=1,711 · total=52,700 · 7,396 chars (click to expand)
turn 1439.7s turn · 15.6s LLM · 24.1s tools · tokens in=50,989 · out=1,711 · total=52,700 · FINAL
reasoning
This is my last turn. I need to submit my kernel. Let me submit the one that uses a fused attention kernel but relies on PyTorch for other operations. This should be correct and faster than the reference.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nimport torch.nn.functional as F\nimport math\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <c10/cuda/CUDAException.h>\n\n// Fused attention kernel for triangular self-attention\n// Computes: softmax((Q @ K.T) * scale + bias) @ V\n__global__ void fused_attention_kernel(\n const float* __restrict__ query,\n const float* __restrict__ key,\n const float* __restrict__ value,\n const float* __restrict__ bias,\n const float* __restrict__ mask,\n float* __restrict__ output,\n float scale,\n int B, int N, int h, int d, int BN\n) {\n int bn = blockIdx.x;\n int head = blockIdx.y;\n int q_pos = blockIdx.z;\n \n if (bn >= BN || head >= h || q_pos >= N) return;\n \n int b = bn / N;\n int row = bn % N;\n int tid = threadIdx.x;\n \n extern __shared__ float smem[];\n float* q_shared = smem;\n float* scores = &smem[d];\n \n // Load Q\n for (int dd = tid; dd < d; dd += blockDim.x) {\n q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];\n }\n __syncthreads();\n \n // Compute scores\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float score = 0.0f;\n for (int dd = 0; dd < d; dd++) {\n score += q_shared[dd] * key[bn * h * N * d + head * N * d + k_pos * d + dd];\n }\n score *= scale;\n score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];\n if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {\n score = -INFINITY;\n }\n scores[k_pos] = score;\n }\n __syncthreads();\n \n // Softmax\n __shared__ float smax, ssum;\n if (tid == 0) {\n smax = -INFINITY;\n for (int i = 0; i < N; i++) {\n if (scores[i] > smax) smax = scores[i];\n }\n }\n __syncthreads();\n \n float local_sum = 0.0f;\n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n float exp_val = expf(scores[k_pos] - smax);\n scores[k_pos] = exp_val;\n local_sum += exp_val;\n }\n \n for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n if (tid == 0) ssum = local_sum;\n __syncthreads();\n \n for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {\n scores[k_pos] /= ssum;\n }\n __syncthreads();\n \n // Compute weighted sum\n for (int dd = tid; dd < d; dd += blockDim.x) {\n float out_val = 0.0f;\n for (int k_pos = 0; k_pos < N; k_pos++) {\n out_val += scores[k_pos] * value[bn * h * N * d + head * N * d + k_pos * d + dd];\n }\n output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;\n }\n}\n\ntorch::Tensor fused_attention_cuda(\n torch::Tensor query,\n torch::Tensor key,\n torch::Tensor value,\n torch::Tensor bias,\n torch::Tensor mask,\n int B, int N, int h, int d\n) {\n int BN = B * N;\n auto output = torch::empty({BN, h, N, d}, query.options());\n float scale = 1.0f / sqrtf((float)d);\n \n size_t smem_size = d * sizeof(float) + N * sizeof(float);\n dim3 grid(BN, h, N);\n fused_attention_kernel<<<grid, d, smem_size>>>(\n query.data_ptr<float>(),\n key.data_ptr<float>(),\n value.data_ptr<float>(),\n bias.data_ptr<float>(),\n mask.data_ptr<float>(),\n output.data_ptr<float>(),\n scale,\n B, N, h, d, BN\n );\n \n return output;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor fused_attention_cuda(\n torch::Tensor query,\n torch::Tensor key,\n torch::Tensor value,\n torch::Tensor bias,\n torch::Tensor mask,\n int B, int N, int h, int d\n);\n\"\"\"\n\nfused_attn = load_inline(\n name='fused_attention',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['fused_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = pair_dim // num_heads\n assert pair_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(pair_dim)\n self.query = nn.Linear(pair_dim, pair_dim, bias=False)\n self.key = nn.Linear(pair_dim, pair_dim, bias=False)\n self.value = nn.Linear(pair_dim, pair_dim, bias=False)\n self.gate = nn.Linear(pair_dim, pair_dim)\n self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(pair_dim, pair_dim)\n self.dropout = nn.Dropout(dropout)\n\n def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:\n B, N, _, D = pair_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n # LayerNorm\n x = self.layer_norm(pair_repr)\n g = torch.sigmoid(self.gate(x))\n\n # Bias projection\n bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N)\n\n # Reshape and project\n x_flat = x.reshape(B * N, N, D)\n q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2)\n k = self.key(x_flat).view(B * N, N, h, d).transpose(1, 2)\n v = self.value(x_flat).view(B * N, N, h, d).transpose(1, 2)\n\n # Fused attention via custom CUDA kernel\n attn_out = fused_attn.fused_attention_cuda(\n q.contiguous().float(),\n k.contiguous().float(),\n v.contiguous().float(),\n bias.contiguous().float(),\n pair_mask.contiguous().float(),\n B, N, h, d\n )\n\n # Reshape and gate\n out = attn_out.transpose(1, 2).reshape(B, N, N, D)\n out = g * out\n \n return self.out_proj(out)" }
result ← submit_kernel (ok)
oksubmit_kernel submit_kernel PASSED: (3 / 3) correctness trials all passed. Kernel runtime: 0.30 μs Runtime stats: mean=0.30μs median=?μs std=0.09μs Numerical precision: max_abs_err=0.00e+00 mean_abs_err=0.00e+00 max_rel_err=0.00e+00 Memory: 38.23 MB peak (ref: 38.2 MB, ratio: 1.0008x) Kernel launches: 106 (ref: 142, fusion ratio: 1.3396) SOL score: 0.5571
final submitted kernel (196 lines, 5,755 chars) — full source, untruncated
import torch
import torch.nn as nn
import torch.nn.functional as F
import math
from torch.utils.cpp_extension import load_inline

cuda_source = """
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <c10/cuda/CUDAException.h>

// Fused attention kernel for triangular self-attention
// Computes: softmax((Q @ K.T) * scale + bias) @ V
__global__ void fused_attention_kernel(
    const float* __restrict__ query,
    const float* __restrict__ key,
    const float* __restrict__ value,
    const float* __restrict__ bias,
    const float* __restrict__ mask,
    float* __restrict__ output,
    float scale,
    int B, int N, int h, int d, int BN
) {
    int bn = blockIdx.x;
    int head = blockIdx.y;
    int q_pos = blockIdx.z;
    
    if (bn >= BN || head >= h || q_pos >= N) return;
    
    int b = bn / N;
    int row = bn % N;
    int tid = threadIdx.x;
    
    extern __shared__ float smem[];
    float* q_shared = smem;
    float* scores = &smem[d];
    
    // Load Q
    for (int dd = tid; dd < d; dd += blockDim.x) {
        q_shared[dd] = query[bn * h * N * d + head * N * d + q_pos * d + dd];
    }
    __syncthreads();
    
    // Compute scores
    for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {
        float score = 0.0f;
        for (int dd = 0; dd < d; dd++) {
            score += q_shared[dd] * key[bn * h * N * d + head * N * d + k_pos * d + dd];
        }
        score *= scale;
        score += bias[b * h * N * N + head * N * N + q_pos * N + k_pos];
        if (mask[b * N * N + q_pos * N + k_pos] == 0.0f) {
            score = -INFINITY;
        }
        scores[k_pos] = score;
    }
    __syncthreads();
    
    // Softmax
    __shared__ float smax, ssum;
    if (tid == 0) {
        smax = -INFINITY;
        for (int i = 0; i < N; i++) {
            if (scores[i] > smax) smax = scores[i];
        }
    }
    __syncthreads();
    
    float local_sum = 0.0f;
    for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {
        float exp_val = expf(scores[k_pos] - smax);
        scores[k_pos] = exp_val;
        local_sum += exp_val;
    }
    
    for (int offset = blockDim.x / 2; offset > 0; offset >>= 1) {
        local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);
    }
    if (tid == 0) ssum = local_sum;
    __syncthreads();
    
    for (int k_pos = tid; k_pos < N; k_pos += blockDim.x) {
        scores[k_pos] /= ssum;
    }
    __syncthreads();
    
    // Compute weighted sum
    for (int dd = tid; dd < d; dd += blockDim.x) {
        float out_val = 0.0f;
        for (int k_pos = 0; k_pos < N; k_pos++) {
            out_val += scores[k_pos] * value[bn * h * N * d + head * N * d + k_pos * d + dd];
        }
        output[bn * h * N * d + head * N * d + q_pos * d + dd] = out_val;
    }
}

torch::Tensor fused_attention_cuda(
    torch::Tensor query,
    torch::Tensor key,
    torch::Tensor value,
    torch::Tensor bias,
    torch::Tensor mask,
    int B, int N, int h, int d
) {
    int BN = B * N;
    auto output = torch::empty({BN, h, N, d}, query.options());
    float scale = 1.0f / sqrtf((float)d);
    
    size_t smem_size = d * sizeof(float) + N * sizeof(float);
    dim3 grid(BN, h, N);
    fused_attention_kernel<<<grid, d, smem_size>>>(
        query.data_ptr<float>(),
        key.data_ptr<float>(),
        value.data_ptr<float>(),
        bias.data_ptr<float>(),
        mask.data_ptr<float>(),
        output.data_ptr<float>(),
        scale,
        B, N, h, d, BN
    );
    
    return output;
}
"""

cpp_source = """
torch::Tensor fused_attention_cuda(
    torch::Tensor query,
    torch::Tensor key,
    torch::Tensor value,
    torch::Tensor bias,
    torch::Tensor mask,
    int B, int N, int h, int d
);
"""

fused_attn = load_inline(
    name='fused_attention',
    cpp_sources=cpp_source,
    cuda_sources=cuda_source,
    functions=['fused_attention_cuda'],
    extra_cuda_cflags=['-O3'],
    verbose=False
)


class ModelNew(nn.Module):
    def __init__(self, pair_dim, num_heads, dropout=0.0):
        super().__init__()
        self.num_heads = num_heads
        self.head_dim = pair_dim // num_heads
        assert pair_dim % num_heads == 0

        self.layer_norm = nn.LayerNorm(pair_dim)
        self.query = nn.Linear(pair_dim, pair_dim, bias=False)
        self.key = nn.Linear(pair_dim, pair_dim, bias=False)
        self.value = nn.Linear(pair_dim, pair_dim, bias=False)
        self.gate = nn.Linear(pair_dim, pair_dim)
        self.bias_proj = nn.Linear(pair_dim, num_heads, bias=False)
        self.out_proj = nn.Linear(pair_dim, pair_dim)
        self.dropout = nn.Dropout(dropout)

    def forward(self, pair_repr: torch.Tensor, pair_mask: torch.Tensor) -> torch.Tensor:
        B, N, _, D = pair_repr.shape
        h = self.num_heads
        d = self.head_dim

        # LayerNorm
        x = self.layer_norm(pair_repr)
        g = torch.sigmoid(self.gate(x))

        # Bias projection
        bias = self.bias_proj(x).permute(0, 3, 1, 2)  # (B, h, N, N)

        # Reshape and project
        x_flat = x.reshape(B * N, N, D)
        q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2)
        k = self.key(x_flat).view(B * N, N, h, d).transpose(1, 2)
        v = self.value(x_flat).view(B * N, N, h, d).transpose(1, 2)

        # Fused attention via custom CUDA kernel
        attn_out = fused_attn.fused_attention_cuda(
            q.contiguous().float(),
            k.contiguous().float(),
            v.contiguous().float(),
            bias.contiguous().float(),
            pair_mask.contiguous().float(),
            B, N, h, d
        )

        # Reshape and gate
        out = attn_out.transpose(1, 2).reshape(B, N, N, D)
        out = g * out
        
        return self.out_proj(out)