reasoning
The correctness check failed. There are some differences between my output and the reference. Let me think about what could be wrong.
Looking at my kernel:
1. I'm computing QK^T correctly with scaling
2. I'm adding bias correctly
3. I'm applying mask correctly
Wait, I think I see an issue. In the reference, after computing `attn = (q @ k.transpose(-1, -2))`, it's reshaped and then bias is added. But the bias has shape (B, h, N, N) and is unsqueezed to (B, 1, h, N, N). This means bias is broadcast across the row dimension (the second "N" in BN, h, N, N).
Let me look at this more carefully:
- attn has shape (B*N, h, N, N) initially
- Then it's reshaped to (B, N, h, N, N)
- bias has shape (B, h, N, N), unsqueezed to (B, 1, h, N, N)
- So bias[i, :, j, :, :] (shape 1, h, N, N) is added to attn[i, :, j, :, :]
This means for each (batch, row_idx), the bias is the same for all queries in that row. The bias[b, h_idx, :, :] should be the full bias matrix for that batch and head.
But in my kernel, I'm loading bias as:
```cpp
const scalar_t* bias_row = bias + (batch_idx * h + head_idx) * N * N + row_idx * N;
```
This is loading bias[b, h, row_idx, :] which would be a row of the bias. But wait, in the reference:
- attn after reshape is (B, N, h, N, N)
- bias unsqueezed is (B, 1, h, N, N)
- When added: attn[i, j, h, k, l] + bias[i, 0, h, k, l]
- So bias is indexed as [batch, 0, head, :, :] but with the third dimension being the key dimension...
Wait, I'm confused about the dimensions. Let me trace through more carefully:
Reference:
```python
attn = (q @ k.transpose(-1, -2)) / math.sqrt(d) # (B*N, h, N, N)
attn = attn.view(B, N, h, N, N)
# attn[b, row, head, :, :] is the attention matrix for that specific row
bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N)
# bias[b, head, i, j] is the pair bias for position (i,j)
# Add pair bias: bias (B, h, N, N) → (B, 1, h, N, N) broadcast across rows
attn = attn + bias.unsqueeze(1)
```
So the attention and bias addition is:
- attn[b, row, head, q_pos, k_pos] + bias[b, 0, head, q_pos, k_pos]
- Wait, this doesn't look right either.
Actually, `bias.unsqueeze(1)` gives shape (B, 1, h, N, N). When broadcasting with attn (B, N, h, N, N):
- The broadcast happens along dimension 1
- So bias is the same for all row values in attn
So for attn[b, row, head, q_pos, k_pos], the added bias is bias[b, 0, head, q_pos, k_pos].
But wait, in my kernel, I'm computing attention for a single row (row_idx). And I'm using:
```cpp
const scalar_t* bias_row = bias + (batch_idx * h + head_idx) * N * N + row_idx * N;
```
This is indexing bias[b, head, row_idx, :], which gives me a row of the bias matrix. But I should be adding bias[b, head, q_pos, k_pos] for each (q_pos, k_pos) pair.
Oh I see the issue now! The bias is (B, h, N, N), and when added to attn (B, N, h, N, N) after unsqueeze, the broadcasting works as follows:
- attn indices: [b, row_idx, h, q_pos, k_pos]
- bias indices after unsqueeze: [b, 0, h, q_pos, k_pos] -> broadcasts to [b, any, h, q_pos, k_pos]
So for any row_idx, the bias added is bias[b, head, q_pos, k_pos].
But in my kernel, I'm reading bias[b, head, row_idx, k_pos]. This is wrong!
In my kernel:
- For query position q_idx and key position key_pos, I should add bias[batch, head, q_idx, key_pos]
But I'm adding bias[batch, head, row_idx, key_pos] which is wrong!
Wait, but looking at the reference again, after the reshape to (B, N, h, N, N):
- attn[b, row, head, q_idx, k_idx]
- When I compute attention, I'm computing for a specific row (row_idx), and within that row, I have query positions from 0 to N-1.
Hmm, actually let me look at what the attention is computing:
- q has shape (B*N, h, N, d) after processing
- This is reshaped from x.reshape(B * N, N, D)
- So q[batch*N + row_idx, head, q_pos, :] is the query for row `row_idx`, query position `q_pos`
Wait, but if we're doing "attention across columns for each row", then what does q_pos represent?
Looking at the reference:
```python
x_flat = x.reshape(B * N, N, D) # (B*N, N, D)
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), where the first dimension is batch*N. This means x_flat[batch*N + row_idx, :, :] is one row of the original pair representation.
When we compute q @ k.transpose(-1, -2), we get (B*N, h, N, N), which means for each (batch_row, head), we get an N×N attention matrix.
After viewing as (B, N, h, N, N), the dimensions are:
- attn[b, row_idx, head, q_pos, k_pos]
And since both q_pos and k_pos range over N, the attention is between all positions in that row.
But wait, if q = self.query(x_flat), and x_flat is (B*N, N, D), then:
- x_flat[batch_row, pos, :] is the feature at position `pos` in the row
- q[batch_row, head, pos, :] is the query for position `pos`
So the attention matrix attn[batch_row, head, q_pos, k_pos] is the attention from position q_pos to position k_pos within that row.
Now for bias:
- bias[b, head, q_pos, k_pos] is added
- After unsqueeze: bias[b, 0, head, q_pos, k_pos]
- When added to attn[b, row, head, q_pos, k_pos], it broadcasts along the row dimension
So for my kernel, for query position q_idx and key position key_pos, I should add:
bias[batch_idx, head_idx, q_idx, key_pos]
But I'm currently reading:
bias[batch_idx, head_idx, row_idx, key_pos]
This is the bug! Let me fix it.
Actually wait, let me re-read my kernel. I'm computing attention for a specific row_idx, and then for each q_idx within that row. The bias should be bias[batch_idx, head_idx, q_idx, key_pos].
Let me fix this:
```cpp
// In the loop for computing attention scores
float score = float(bias[(batch_idx * h + head_idx) * N * N + q_idx * N + key_pos]);
```
Actually, I realize there might be another issue. The bias has shape (B, h, N, N), but in the reference:
```python
bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N)
```
So bias[b, head, i, j] where i and j are the row and column indices in the original pair representation.
When we do:
```python
attn = attn + bias.unsqueeze(1)
```
Where attn is (B, N, h, N, N), the bias gets broadcast. So:
attn[b, row, h, q_idx, k_idx] + bias[b, 0, h, q_idx, k_idx]
Hmm, but wait. The pair representation x has shape (B, N, N, D). When we reshape to (B*N, N, D), we're treating each row of the pair representation as a separate sequence.
So the attention within row `row_idx` of batch `b` is computed using:
- x[b, row_idx, :, :] as the sequence
- This is reshaped to x_flat[b*N + row_idx, :, :]
- Q, K, V are computed from this
The attention matrix then has dimensions [q_pos, k_pos], both ranging over the N positions in the row.
Now for the bias:
- bias[b, head, i, j] comes from the pair representation
- When added to attn[b, row, head, q_pos, k_pos], we use bias[b, 0, head, q_pos, k_pos]
- But this means the bias depends on (q_pos, k_pos), not (row, k_pos) or (row, q_pos)!
So the bias being added is the same for all rows! The bias[b, head, q_pos, k_pos] is added to every attn[b, :, head, q_pos, k_pos].
Wait, that doesn't seem right either. Let me check the reference implementation more carefully:
```python
bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N)
# x has shape (B, N, N, D)
# bias_proj(x) has shape (B, N, N, h)
# permute(0, 3, 1, 2) gives (B, h, N, N)
```
So bias[b, head, i, j] is the pair bias at position (i, j) in the pair representation.
Now when we add bias to attention:
```python
attn = attn.view(B, N, h, N, N) # (B, N, h, N, N)
attn = attn + bias.unsqueeze(1) # bias.unsqueeze(1) is (B, 1, h, N, N)
```
The broadcasting rules say:
- attn has shape (B, N, h, N, N)
- bias has shape (B, 1, h, N, N)
- The result has shape (B, N, h, N, N) where bias is broadcast along dimension 1
So for attn[b, row, head, q_pos, k_pos], the added bias is bias[b, 0, head, q_pos, k_pos].
This means:
- For all rows (across the row dimension), the same bias matrix is added
- The bias[b, head, q_pos, k_pos] is added to the attention weight from q_pos to k_pos
So in my kernel, for query position q_idx and key position key_pos:
bias_idx = (batch_idx * h + head_idx) * N * N + q_idx * N + key_pos
But I'm currently using:
bias_idx = (batch_idx * h + head_idx) * N * N + row_idx * N + key_pos
So the fix is to use q_idx instead of row_idx for the bias indexing!
Also, I notice another issue. In the reference, the dropout is applied after softmax:
```python
attn = F.softmax(attn, dim=-1)
attn = self.dropout(attn)
```
In my kernel, I'm not applying dropout. But since dropout=0.0 by default, it should be fine. Let me check if dropout is used...
Looking at the init: `self.dropout = nn.Dropout(dropout)` where `dropout=0.0` by default.
But we need to handle the case where dropout > 0. However, since the reference uses dropout after softmax, and I'm implementing a fused kernel without dropout, I should ensure that dropout=0 is the case in testing, or I need to handle it differently.
For now, let me assume dropout=0 and fix the bias issue.
Actually, I just realized there might be another issue with my indexing. Let me trace through the reference more carefully:
In the reference:
- attn after `attn = (q @ k.transpose(-1, -2))` has shape (B*N, h, N, N)
- Then `attn = attn.view(B, N, h, N, N)`
- The view reinterprets the memory
So the memory layout is:
- attn_flat[batch*N*h*N*N + row*N*h*N*N + head*N*N + q*N + k]
Wait, let me think about this more carefully. q has shape (B*N, h, N, d). When we compute `q @ k.transpose(-1, -2)`, we get (B*N, h, N, N).
In C-contiguous memory:
- attn[batch_row, head, q, k] is at position batch_row*h*N*N + head*N*N + q*N + k
After view(B, N, h, N, N):
- The memory is reinterpreted
- attn_new[b, row, head, q, k] accesses the same memory
- b*N*h*N*N + row*h*N*N + head*N*N + q*N + k
But originally the memory was laid out as:
- attn[b*N + row, head, q, k] = batch_row*h*N*N + head*N*N + q*N + k
So we need:
- b*N*h*N*N + row*h*N*N = (b*N + row)*h*N*N
- This gives b*N*h*N*N + row*h*N*N = b*N*h*N*N + row*h*N*N ✓
So the memory layout is consistent.
Now for the bias:
- bias[b, head, i, j] is at position b*h*N*N + head*N*N + i*N + j
When we add bias.unsqueeze(1) to attn.view(B, N, h, N, N):
- attn[b, row, head, q, k] + bias[b, 0, head, q, k] (broadcast)
- So bias is indexed as bias[b, head, q, k]
So in my kernel, I should use:
bias[(batch_idx * h + head_idx) * N * N + q_idx * N + key_pos]
Not:
bias[(batch_idx * h + head_idx) * N * N + row_idx * N + key_pos]
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\n# Define CUDA kernels\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n\n// Fused attention kernel - each block handles one (batch_row, head)\ntemplate <typename scalar_t>\n__global__ void fused_attention_kernel(\n const scalar_t* __restrict__ query, // (BN, h, N, d) - contiguous\n const scalar_t* __restrict__ key, // (BN, h, N, d)\n const scalar_t* __restrict__ value, // (BN, h, N, d)\n const scalar_t* __restrict__ bias, // (B, h, N, N)\n const bool* __restrict__ mask, // (B, N, N)\n scalar_t* __restrict__ output, // (BN, N, h, d)\n const int B, const int N, const int h, const int d, const int BN,\n const float scale\n) {\n // Block index determines which (batch_row, head) we handle\n const int batch_row_idx = blockIdx.x / h;\n const int head_idx = blockIdx.x % h;\n const int batch_idx = batch_row_idx / N;\n \n const int tid = threadIdx.x;\n const int blockDimX = blockDim.x;\n \n // Shared memory layout:\n // - K tile: [blockDimX, d]\n // - V tile: [blockDimX, d]\n // - attention scores: [N] (shared across threads for each query)\n extern __shared__ char smem[];\n scalar_t* k_tile = reinterpret_cast<scalar_t*>(smem);\n scalar_t* v_tile = k_tile + blockDimX * d;\n float* attn_scores = reinterpret_cast<float*>(v_tile + blockDimX * d);\n \n // Base pointers for Q, K, V\n const scalar_t* q_base = query + (batch_row_idx * h + head_idx) * N * d;\n const scalar_t* k_base = key + (batch_row_idx * h + head_idx) * N * d;\n const scalar_t* v_base = value + (batch_row_idx * h + head_idx) * N * d;\n // Bias is indexed as bias[b, head, q_pos, k_pos]\n const scalar_t* bias_base = bias + batch_idx * h * N * N + head_idx * N * N;\n // Mask is indexed as mask[b, i, j] = mask[b, row_idx, k_pos] for our case\n const int row_idx = batch_row_idx % N;\n const bool* mask_row = mask + batch_idx * N * N + row_idx * N;\n \n // Each thread handles multiple query positions\n for (int q_idx = tid; q_idx < N; q_idx += blockDimX) {\n // Load query vector into registers\n float q_reg[64]; // d <= 64\n const scalar_t* q_ptr = q_base + q_idx * d;\n for (int i = 0; i < d; i++) {\n q_reg[i] = float(q_ptr[i]);\n }\n \n // Output accumulator\n float out_acc[64];\n for (int i = 0; i < d; i++) out_acc[i] = 0.0f;\n \n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n \n // First pass: compute scores and find max\n for (int kv_tile = 0; kv_tile < N; kv_tile += blockDimX) {\n const int tile_start = kv_tile;\n const int tile_end = min(kv_tile + blockDimX, N);\n const int tile_size = tile_end - tile_start;\n \n // Cooperatively load K tile\n if (tid < tile_size) {\n const scalar_t* k_ptr = k_base + (tile_start + tid) * d;\n for (int i = 0; i < d; i++) {\n k_tile[tid * d + i] = k_ptr[i];\n }\n }\n __syncthreads();\n \n // Compute QK^T for this query against all keys in tile\n if (q_idx < N) {\n for (int kv = 0; kv < tile_size; kv++) {\n const int key_pos = tile_start + kv;\n float score = 0.0f;\n for (int i = 0; i < d; i++) {\n score += q_reg[i] * float(k_tile[kv * d + i]);\n }\n score *= scale;\n // Add bias: bias[b, head, q_idx, key_pos]\n score += float(bias_base[q_idx * N + key_pos]);\n \n // Apply mask: mask[b, row_idx, key_pos]\n if (!mask_row[key_pos]) {\n score = -INFINITY;\n }\n \n attn_scores[key_pos] = score;\n max_score = max(max_score, score);\n }\n }\n __syncthreads();\n }\n \n // Compute exp(scores - max) and sum\n if (q_idx < N) {\n for (int i = 0; i < N; i++) {\n float score = attn_scores[i];\n if (score > -INFINITY / 2) {\n float exp_val = expf(score - max_score);\n attn_scores[i] = exp_val;\n sum_exp += exp_val;\n } else {\n attn_scores[i] = 0.0f;\n }\n }\n \n // Normalize\n float inv_sum = 1.0f / (sum_exp + 1e-8f);\n for (int i = 0; i < N; i++) {\n attn_scores[i] *= inv_sum;\n }\n }\n \n // Second pass: compute weighted sum of values\n for (int kv_tile = 0; kv_tile < N; kv_tile += blockDimX) {\n const int tile_start = kv_tile;\n const int tile_end = min(kv_tile + blockDimX, N);\n const int tile_size = tile_end - tile_start;\n \n // Load V tile\n if (tid < tile_size) {\n const scalar_t* v_ptr = v_base + (tile_start + tid) * d;\n for (int i = 0; i < d; i++) {\n v_tile[tid * d + i] = v_ptr[i];\n }\n }\n __syncthreads();\n \n if (q_idx < N) {\n for (int kv = 0; kv < tile_size; kv++) {\n const int key_pos = tile_start + kv;\n float attn_w = attn_scores[key_pos];\n if (attn_w > 0) {\n for (int i = 0; i < d; i++) {\n out_acc[i] += attn_w * float(v_tile[kv * d + i]);\n }\n }\n }\n }\n __syncthreads();\n }\n \n // Write output\n if (q_idx < N) {\n scalar_t* out_ptr = output + (batch_row_idx * N + q_idx) * h * d + head_idx * d;\n for (int i = 0; i < d; i++) {\n out_ptr[i] = scalar_t(out_acc[i]);\n }\n }\n }\n}\n\n// Host function\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 const int BN = B * N;\n const float scale = 1.0f / sqrtf((float)d);\n \n // Output: (BN, N, h, d)\n auto output = torch::empty({BN, N, h, d}, query.options());\n \n const int num_blocks = BN * h;\n const int threads_per_block = 256;\n const int shared_mem_size = threads_per_block * d * 2 * sizeof(float) + N * sizeof(float);\n \n AT_DISPATCH_FLOATING_TYPES(query.scalar_type(), \"fused_attention_kernel\", ([&] {\n fused_attention_kernel<scalar_t><<<num_blocks, threads_per_block, shared_mem_size>>>(\n query.data_ptr<scalar_t>(),\n key.data_ptr<scalar_t>(),\n value.data_ptr<scalar_t>(),\n bias.data_ptr<scalar_t>(),\n mask.data_ptr<bool>(),\n output.data_ptr<scalar_t>(),\n B, N, h, d, BN, scale\n );\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# Compile extension\nfused_attention_module = load_inline(\n name='fused_attention_v2',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['fused_attention_cuda'],\n extra_cuda_cflags=['-O3'],\n verbose=True\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 x = self.layer_norm(pair_repr)\n g = torch.sigmoid(self.gate(x))\n\n # Pair bias from the pair representation itself\n bias = self.bias_proj(x).permute(0, 3, 1, 2) # (B, h, N, N)\n\n # Reshape to (B*N, N, D) \u2014 attention across columns for each row\n x_flat = x.reshape(B * N, N, D)\n q = self.query(x_flat).view(B * N, N, h, d).transpose(1, 2) # (B*N, h, N, d)\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 # Use custom fused attention kernel\n q = q.contiguous()\n k = k.contiguous()\n v = v.contiguous()\n bias = bias.contiguous()\n mask_bool = (pair_mask == 1).contiguous()\n \n out = fused_attention_module.fused_attention_cuda(q, k, v, bias, mask_bool, B, N, h, d)\n out = out.reshape(B, N, N, D)\n \n out = g * out\n return self.out_proj(out)\n\n\npair_dim = 64\nnum_heads = 4\nseq_len = 32\nbatch_size = 2\n\n\ndef get_inputs():\n pair_repr = torch.randn(batch_size, seq_len, seq_len, pair_dim)\n pair_mask = torch.ones(batch_size, seq_len, seq_len)\n return [pair_repr, pair_mask]\n\n\ndef get_init_inputs():\n return [pair_dim, num_heads]"
}