reasoning
The issue is still the output layout. Let me trace through the reference implementation more carefully:
Reference:
1. q, k, v, g: permute(0, 1, 3, 2, 4) -> (B, S, h, N, d) ✓
2. attn = einsum("bshid,bshjd->bshij", q, k) -> (B, S, h, N, N)
3. bias: permute(0, 3, 1, 2) -> (B, h, N, N), then unsqueeze(1) -> (B, 1, h, N, N)
4. attn = attn + bias.unsqueeze(1) -> broadcasts along S dimension
5. mask: view(B, S, 1, 1, N).expand_as(attn) -> (B, S, h, N, N)
6. softmax over last dim (N)
7. out = einsum("bshij,bshjd->bshid", attn, v) -> (B, S, h, N, d)
8. out = g * out (same layout)
9. out.permute(0, 1, 3, 2, 4).reshape(B, S, N, D) -> (B, S, N, h, d) then flattened to (B, S, N, D)
So my output layout should be (B, S, h, N, d) to match g, but I'm outputting (B, S, N, h, d). Let me fix that.
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# C++ source with function declaration\ncpp_source = \"\"\"\ntorch::Tensor fused_attention_cuda(\n torch::Tensor q, torch::Tensor k, torch::Tensor v, torch::Tensor g,\n torch::Tensor bias, c10::optional<torch::Tensor> msa_mask,\n int64_t B, int64_t S, int64_t N, int64_t h, int64_t d, double scale);\n\"\"\"\n\n# CUDA source with kernel implementation\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n\ntemplate <typename scalar_t, int BLOCK_SIZE>\n__global__ void fused_attention_kernel(\n const scalar_t* __restrict__ q,\n const scalar_t* __restrict__ k,\n const scalar_t* __restrict__ v,\n const scalar_t* __restrict__ g,\n const scalar_t* __restrict__ bias,\n const bool* __restrict__ mask,\n scalar_t* __restrict__ out,\n int B, int S, int N, int h, int d,\n float scale) {\n \n // Each block handles one (batch, seq, head, query_pos)\n // Tensor layout: (B, S, h, N, d)\n int batch_idx = blockIdx.x / (S * h);\n int s_idx = (blockIdx.x / h) % S;\n int head_idx = blockIdx.x % h;\n int query_pos = blockIdx.y;\n \n if (batch_idx >= B || s_idx >= S || head_idx >= h || query_pos >= N) return;\n \n int tid = threadIdx.x;\n \n // Base offset for this (batch, seq, head)\n // Index: b * S * h * N * d + s * h * N * d + head * N * d + n * d + i\n int base_offset = (batch_idx * S + s_idx) * h * N * d + head_idx * N * d;\n int q_offset = base_offset + query_pos * d; // Position for Q\n \n // Shared memory for Q (all threads need access to full Q vector)\n __shared__ float Q_shared[64]; // max d=64\n \n // Load Q into shared memory\n for (int i = tid; i < d; i += BLOCK_SIZE) {\n Q_shared[i] = float(q[q_offset + i]);\n }\n __syncthreads();\n \n // Shared memory for reduction\n __shared__ float max_score_shared;\n __shared__ float sum_exp_shared;\n __shared__ float out_shared[64]; // max d\n \n if (tid == 0) {\n max_score_shared = -INFINITY;\n sum_exp_shared = 0.0f;\n }\n for (int i = tid; i < d; i += BLOCK_SIZE) {\n out_shared[i] = 0.0f;\n }\n __syncthreads();\n \n // First pass: find max score\n float local_max = -INFINITY;\n for (int kv_pos = tid; kv_pos < N; kv_pos += BLOCK_SIZE) {\n int mask_idx = batch_idx * S * N + s_idx * N + kv_pos;\n if (mask != nullptr && !mask[mask_idx]) continue;\n \n int k_offset = base_offset + kv_pos * d; // K position\n \n float score = 0.0f;\n for (int i = 0; i < d; i++) {\n score += Q_shared[i] * float(k[k_offset + i]);\n }\n score *= scale;\n \n // Bias: (B, h, N, N) -> bias[b * h * N * N + head * N * N + q * N + kv]\n int bias_idx = batch_idx * h * N * N + head_idx * N * N + query_pos * N + kv_pos;\n score += float(bias[bias_idx]);\n \n local_max = max(local_max, score);\n }\n \n // Reduce max within warp\n for (int offset = 16; offset > 0; offset >>= 1) {\n local_max = max(local_max, __shfl_down_sync(0xffffffff, local_max, offset));\n }\n \n if ((tid & 31) == 0) {\n atomicMax(reinterpret_cast<int*>(&max_score_shared), __float_as_int(local_max));\n }\n __syncthreads();\n float global_max = max_score_shared;\n \n // Second pass: compute exp and accumulate\n float local_sum = 0.0f;\n float local_out[64];\n for (int i = 0; i < d; i++) local_out[i] = 0.0f;\n \n for (int kv_pos = tid; kv_pos < N; kv_pos += BLOCK_SIZE) {\n int mask_idx = batch_idx * S * N + s_idx * N + kv_pos;\n bool valid = (mask == nullptr) || mask[mask_idx];\n \n if (valid) {\n int k_offset = base_offset + kv_pos * d;\n \n float score = 0.0f;\n for (int i = 0; i < d; i++) {\n score += Q_shared[i] * float(k[k_offset + i]);\n }\n score *= scale;\n \n int bias_idx = batch_idx * h * N * N + head_idx * N * N + query_pos * N + kv_pos;\n score += float(bias[bias_idx]);\n \n float exp_val = expf(score - global_max);\n local_sum += exp_val;\n \n int v_offset = base_offset + kv_pos * d; // V position\n for (int i = 0; i < d; i++) {\n local_out[i] += exp_val * float(v[v_offset + i]);\n }\n }\n }\n \n // Reduce sum within warp\n for (int offset = 16; offset > 0; offset >>= 1) {\n local_sum += __shfl_down_sync(0xffffffff, local_sum, offset);\n }\n \n if ((tid & 31) == 0) {\n atomicAdd(&sum_exp_shared, local_sum);\n }\n \n // Reduce out within warp (each thread handles subset of d)\n for (int i = 0; i < d; i++) {\n float val = local_out[i];\n for (int offset = 16; offset > 0; offset >>= 1) {\n val += __shfl_down_sync(0xffffffff, val, offset);\n }\n if ((tid & 31) == 0) {\n atomicAdd(&out_shared[i], val);\n }\n }\n __syncthreads();\n \n // Write output with gate\n // Output layout: (B, S, h, N, d) - same as Q, K, V, G\n int out_offset = base_offset + query_pos * d;\n \n float inv_sum = 1.0f / (sum_exp_shared + 1e-8f);\n for (int i = tid; i < d; i += BLOCK_SIZE) {\n float norm_out = out_shared[i] * inv_sum;\n float gate_val = float(g[out_offset + i]);\n out[out_offset + i] = scalar_t(gate_val * norm_out);\n }\n}\n\ntorch::Tensor fused_attention_cuda(\n torch::Tensor q, torch::Tensor k, torch::Tensor v, torch::Tensor g,\n torch::Tensor bias, c10::optional<torch::Tensor> msa_mask,\n int64_t B, int64_t S, int64_t N, int64_t h, int64_t d, double scale) {\n \n auto options = q.options();\n // Output is (B, S, h, N, d) - same layout as input tensors\n auto out = torch::empty({B, S, int(h), int(N), int(d)}, options);\n \n const int threads = 128;\n \n dim3 grid(B * S * h, N);\n dim3 block(threads);\n \n bool* mask_ptr = msa_mask.has_value() ? msa_mask.value().data_ptr<bool>() : nullptr;\n \n AT_DISPATCH_FLOATING_TYPES(q.scalar_type(), \"fused_attention_kernel\", ([&] {\n fused_attention_kernel<scalar_t, threads><<<grid, block>>>(\n q.data_ptr<scalar_t>(),\n k.data_ptr<scalar_t>(),\n k.data_ptr<scalar_t>(),\n g.data_ptr<scalar_t>(),\n bias.data_ptr<scalar_t>(),\n mask_ptr,\n out.data_ptr<scalar_t>(),\n B, S, N, h, d,\n static_cast<float>(scale)\n );\n }));\n \n return out;\n}\n\"\"\"\n\n# Load the CUDA extension\nfused_ops = load_inline(\n name='fused_ops_v5',\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\nclass ModelNew(nn.Module):\n def __init__(self, msa_dim, pair_dim, num_heads, dropout=0.0):\n super().__init__()\n self.num_heads = num_heads\n self.head_dim = msa_dim // num_heads\n assert msa_dim % num_heads == 0\n\n self.layer_norm = nn.LayerNorm(msa_dim)\n self.query = nn.Linear(msa_dim, msa_dim, bias=False)\n self.key = nn.Linear(msa_dim, msa_dim, bias=False)\n self.value = nn.Linear(msa_dim, msa_dim, bias=False)\n self.gate = nn.Linear(msa_dim, msa_dim)\n self.pair_bias = nn.Linear(pair_dim, num_heads, bias=False)\n self.out_proj = nn.Linear(msa_dim, msa_dim)\n self.dropout = nn.Dropout(dropout)\n self.dropout_p = dropout\n\n def forward(\n self,\n msa_repr: torch.Tensor,\n pair_repr: torch.Tensor,\n msa_mask: torch.Tensor,\n ) -> torch.Tensor:\n B, S, N, D = msa_repr.shape\n h = self.num_heads\n d = self.head_dim\n\n x = self.layer_norm(msa_repr)\n q = self.query(x).view(B, S, N, h, d).permute(0, 1, 3, 2, 4) # (B, S, h, N, d)\n k = self.key(x).view(B, S, N, h, d).permute(0, 1, 3, 2, 4)\n v = self.value(x).view(B, S, N, h, d).permute(0, 1, 3, 2, 4)\n g = torch.sigmoid(self.gate(x)).view(B, S, N, h, d).permute(0, 1, 3, 2, 4)\n\n # Pair bias: shared across MSA rows\n bias = self.pair_bias(pair_repr).permute(0, 3, 1, 2) # (B, h, N, N)\n\n scale = 1.0 / math.sqrt(d)\n \n # Call fused attention kernel\n out = fused_ops.fused_attention_cuda(\n q.contiguous(), k.contiguous(), v.contiguous(), g.contiguous(),\n bias.contiguous(), msa_mask.bool(), B, S, N, h, d, scale\n )\n \n # out is (B, S, h, N, d), need to permute to (B, S, N, h, d) then reshape\n out = out.permute(0, 1, 3, 2, 4).reshape(B, S, N, D)\n return self.out_proj(out)"
}