reasoning
reasoning (truncated 1706 chars): Let me analyze this problem carefully.
The model performs an outer-product mean operation:
1. Layer normalization on MSA representation
2. Two linear projections (proj_a and proj_b)
3. Element-wise masking
4. Einsum outer product: `bsih,bsjk->bijhk`
5. Division by sum of mask (mean over S dimension)
6. Reshape and final linear projection
The key computation is the einsum which computes outer products. Let me think about how to optimize this.
Given:
- B = 2 (batch size)
- S = 8 (MSA depth)
- N = 32 (sequence length)
- msa_dim = 64
- hidden_dim = H = 32
- pair_dim = 64
The output shape is (B, N, N, pair_dim) = (2, 32, 32, 64)
The einsum computes: for each (i,j), compute sum over S of a[:,:,i,:] * b[:,:,j,:]^T
This is essentially computing an outer product matrix for each position pair (i,j), averaged over S.
Let me think about the memory access patterns:
- a: (B, S, N, H) = (2, 8, 32, 32)
- b: (B, S, N, H) = (2, 8, 32, 32)
- outer: (B, N, N, H, H) = (2, 32, 32, 32, 32)
The output size is 2 * 32 * 32 * 32 * 32 = 2,097,152 elements for outer, and 2 * 32 * 32 * 64 = 131,072 for the final output.
For the CUDA kernel, I need to:
1. Implement layer norm
2. Implement the two linear projections
3. Do the masking
4. Compute the outer product mean
5. Apply the final projection
I could fuse operations together for efficiency—combining layer norm with projections, or projections with the outer product computation. The einsum is the most computationally intensive part, so that's where optimization matters most. Since the output is large (2M elements), I need to think about thread parallelism and memory coalescing carefully.
The computation breaks down into a weighted sum over S, and I can exploit shared memory to avoid redundant loads. The operations themselves are straightforward: the outer products are simply multiplications, the normalization happens once per (i,j) pair, and the projections are just matrix multiplications.
For the CUDA implementation, I'll start by projecting the input through both layers and computing the outer product mean. A fused kernel handling the projections and outer product together should be efficient, while keeping the layer norm and final projection as PyTorch operations initially.
The key optimization lies in the outer product computation itself. I need to iterate over batch and position pairs, accumulating the outer product of a and b vectors across the sequence dimension, then divide by the mask sum. Each thread block can handle one (b, i, j) triplet, computing the H×H outer product with H×H threads per block, loading from global memory as needed.
For efficiency, I should tile the computation so threads in a block cooperatively process the outer product. With blockDim.y and blockDim.z covering the H×H output dimensions, each thread computes a single element by iterating through S, accumulating the products of corresponding elements from a and b, then normalizing by the sequence length.
I can leverage shared memory by having threads cooperatively load the a and b vectors from the current s index, then compute the outer product cooperatively before moving to the next s. This amortizes the memory access cost across all threads in the block.
For this problem size with N=32 and H=32, I'll allocate one thread block per (b, i, j) combination, giving 2048 total blocks with 1024 threads each organized as a 32×32 grid. Each thread computes one output element by iterating through S and accumulating contributions to its assigned position in the result matrix.
When loading from a and b into shared memory, I need to handle cases where threads might access out-of-bounds indices. The shared memory arrays are limited to 32 elements, so I'll need to carefully bound the loads based on blockDim rather than assuming full coverage.
For the accumulation loop iterating through S, each thread multiplies its corresponding elements from the shared memory buffers and accumulates the result across all valid iterations.
Once accumulation completes, I normalize by the mask sum, then reshape the outer product into a flat vector and apply the output projection layer. The reference reshapes from (B, N, N, H, H) to (B, N, N, H*H) before the final linear transformation.
For the output projection, each output position (b, i, j, k) requires computing a dot product across the flattened H×H dimension. This is a standard matrix multiplication with shape (B*N*N, H*H) x (H*H, pair_dim), which I could handle with PyTorch's matmul for efficiency. The outer product itself seems like the primary optimization target—it's computing all pairwise products across S positions, which has significant memory and compute demands.
Looking at the shapes involved: a and b are both (2, 8, 32, 32), accumulating into an output of (2, 32, 32, 32, 32). This is manageable on GPU, so I should focus on an efficient kernel design.
I'll structure the computation to parallelize over batch, N1, and N2 dimensions while using thread blocks for the H dimensions. Each thread handles one element of the H×H output matrix. Within a thread block, I can leverage shared memory to cooperatively load data and reduce memory traffic. A fused kernel combining the outer product with the output projection would further improve efficiency. = 1;
}
__syncthreads();
int h1 = threadIdx.y;
int h2 = threadIdx.x;
float sum = 0.0f;
for (int s = 0; s < S; s++) {
sum += s_a[s] * s_b[s];
}
// Divide by mask sum
sum /= mask_sum;
// Output projection
// Each thread computes one element of the output projection
// We need to cooperate to compute the full projection
// Actually, let's write output to shared memory first
// Then do the projection cooperatively
}
For the output projection, I need to compute matmul(outer_flat, out_proj_weight) + out_proj_bias, where outer_flat is (B, N, N, H*H) and out_proj_weight is (H*H, pair_dim). After the outer product, I have H×H values in registers. I could fuse this into the kernel by computing the projection incrementally.
For each s iteration, I accumulate the outer product a[s] * b[s] into registers. Instead of storing the full outer product first, I can compute the projection as I go: for each output element k, I compute sum over (h1, h2) of outer[h1, h2] * out_proj_weight[h1*H + h2, k]. This incremental approach avoids materializing the full intermediate tensor.
The challenge is that each thread has only one (h1, h2) element, so I need cooperation across threads. I can store partial products in shared memory—one value per thread representing outer[h1, h2] * weight[h1*H + h2, k]—then reduce across threads to get the final output for each k. However, with H×H threads and pair_dim outputs, I'd need to compute pair_dim separate reductions, which becomes expensive.
A simpler strategy: each thread block handles one (b, i, j, k) combination. Threads across the block cooperatively iterate through the H*H elements of the outer product, accumulate their contributions, then write the single result. This means launching B*N*N*pair_dim thread blocks total, each with H*H threads.
For the implementation, each thread reads the weight element at its assigned (h1, h2) position for the current k, multiplies it with the corresponding outer product value, and accumulates. After the loop completes, a parallel reduction sums all thread contributions to get the final output for that (b, i, j, k) tuple.
However, this requires reloading a and b values repeatedly across different k iterations, which is inefficient. I should reconsider the structure.
An alternative approach uses two kernel phases: first compute the outer product mean and store it, then perform the projection separately. Or I could restructure so each thread block handles one (b, i, j) pair with H×H threads, where each thread computes its outer product element and then threads cooperate to project all k outputs simultaneously using s
...[ omitted 1706 characters ]...
t and position pair independently, computing the masked outer product and accumulating the sum over the sequence dimension before dividing by the mask count.
Each thread block handles one (b, i, j) combination, with threads organized in a 2D grid covering the H×H output space. For each thread, I iterate through the sequence dimension S, loading the corresponding a and b values, multiplying by the mask, and accumulating the outer product contribution. After the loop completes, I normalize by dividing through the mask sum and write the result to the output tensor.
I should reconsider the thread indexing—using threadIdx.x for h2 and threadIdx.y for h1 aligns with how H×H thread blocks are typically organized, but I need to verify the exact indexing matches the output tensor layout.
In PyTorch's row-major storage, the last dimension varies fastest. For outer[b, i, j, h1, h2], adjacent h2 values are contiguous in memory, so mapping threadIdx.x to h2 gives coalesced memory access when threads write to the output tensor.
For the weight tensor in the output projection, which has shape (pair_dim, H*H), I need to check whether the layout is transposed. nn.Linear stores weights as (out_features, in_features), so weight[k, h] means element at row k, column h, where h indexes the flattened H×H dimension.
Since I'm only implementing the outer product kernel initially, I'll keep the Python integration straightforward:
```python
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
cuda_source = """
...kernel code...
"""
module = load_inline(
name='outer_product_cuda',
cpp_sources='',
cuda_sources=cuda_source,
functions=['outer_product_mean_cuda'],
extra_cuda_cflags=['-O3'],
)
class ModelNew(nn.Module):
def __init__(self, msa_dim, pair_dim, hidden_dim=32):
super().__init__()
self.layer_norm = nn.LayerNorm(msa_dim)
self.proj_a = nn.Linear(msa_dim, hidden_dim)
self.proj_b = nn.Linear(msa_dim, hidden_dim)
self.out_proj = nn.Linear(hidden_dim * hidden_dim, pair_dim)
def forward(self, msa_repr: torch.Tensor, msa_mask: torch.Tensor) -> torch.Tensor:
x = self.layer_norm(msa_repr)
a = self.proj_a(x)
b = self.proj_b(x)
mask = msa_mask.unsqueeze(-1)
```
I'm applying the mask to both projections, then calling the custom CUDA kernel to compute the outer product mean efficiently. The kernel handles the einsum operation and normalization. After reshaping the result from (B, N, N, H, H) to (B, N, N, H*H), I pass it through the output projection layer. The module loads the CUDA source inline for compilation. = 1;
}
__syncthreads();
int h1 = threadIdx.y;
int h2 = threadIdx.x;
float sum = 0.0f;
for (int s = 0; s < S; s++) {
sum += s_a[s] * s_b[s];
}
// Normalize by mask sum
sum /= mask_sum;
// Write output
outer_out[b * N * N * H * H + i * N * H * H + j * H * H + h1 * H + h2] = sum;
}
"""
module = load_inline(
name='outer_product_cuda',
cpp_sources='',
cuda_sources=cuda_source,
functions=['outer_product_mean_cuda'],
extra_cuda_cflags=['-O3'],
)
Each thread accumulates the outer product across the sequence dimension using shared memory for the masked values, then normalizes by the mask sum before writing the result to global memory. The kernel launches with a grid of (B * N * N) blocks and H * H threads per block to parallelize across all batch, sequence, and head dimensions.
The ModelNew class wraps this operation, taking the MSA representation and mask as input. It applies layer normalization and projects through two separate linear layers, then uses the custom CUDA kernel to compute the outer product mean before reshaping and projecting to the final output dimension. I'm completing the output projection and returning the result. The CUDA kernel needs to handle the tensor dimensions correctly for the outer product computation across the batch and sequence dimensions.
For the linear layer weight access, I need to account for PyTorch's memory layout where weights are stored as (out_features, in_features). The weight tensor shape is (pair_dim, H*H), and I should verify how this maps to the actual computation when reshaping the outer product from (B, N, N, H, H) to (B, N, N, H*H).
Rather than optimizing the output projection immediately, I'll start with a straightforward CUDA kernel for the outer product mean operation to validate correctness before adding further optimizations.
For the shared memory loading, I need to handle cases where S might exceed the number of threads available. I'll implement a loop that iterates through S in chunks of H, where each thread loads one element if its index is within bounds. This ensures all values get loaded cooperatively by the thread block regardless of the relationship between S and H.
After loading the a and b tensors into shared memory with masking applied, each thread computes one element of the outer product by iterating over the S dimension and accumulating the product of corresponding elements. The result is then normalized by the mask sum before being written to the output tensor. The kernel launches with a grid dimension covering all batch and sequence positions, and a block dimension of H×H threads to parallelize the computation across the hidden dimension.
For a production implementation, I'd want to optimize the index calculations and consider fusing the output projection, but this version should establish correctness first. I need to verify the tensor indexing in the kernel matches the Python implementation and ensure the dimensions align properly—particularly checking that the storage ordering and strides work correctly for the 5D output tensor.
Looking at the kernel launch, I'm computing `num_blocks` as the product of B, N, and N, which gives the total number of positions. For the `outer_out` tensor, I need to make sure the shape is (B, N, N, H, H) and that the flattened indexing in the kernel matches how PyTorch arranges this data in memory.
The index formula `b * N * N * H * H + i * N * H * H + j * H * H + h1 * H + h2` breaks down the stride correctly: each batch index strides by the full spatial and hidden dimensions, each row strides by the column and hidden dimensions, and each hidden dimension index strides by its size. This should work with contiguous tensors, which I get by calling `contiguous()` on the inputs before passing them to the kernel. The intermediate tensors a and b from the linear projections should already be contiguous in memory layout. I don't think there's a fundamental issue with the current approach.
For optimization, rather than using a 2D block configuration, I could switch to a 1D block arrangement where each thread computes multiple elements of the H×H output matrix. This typically improves occupancy when block sizes are limited. The outer product mean itself could also benefit from a fused kernel implementation that combines the computation more directly.
A more direct kernel approach would have each thread compute one output element at index (b, i, j, h1, h2), with blockIdx determining the batch and position indices, and threadIdx determining the hidden dimension indices. This maps directly to the output tensor layout and avoids the complexity of cooperative loading across threads. The stride-based indexing for a and b tensors uses N*H for position indexing and H for hidden dimension indexing, which aligns with how PyTorch stores contiguous tensors. For the output tensor with shape (B, N, N, H, H), each index increments by its corresponding stride: batch by N*N*H*H, first N by N*H*H, second N by H*H, and hidden dimensions by H and 1 respectively.
I should add a clamp on the mask_sum to handle potential division by zero. Once I verify the kernel works correctly, I can explore performance optimizations like shared memory tiling or weight caching.
call → submit_kernel
callsubmit_kernel
{
"kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_source = \"\"\"\n#include <cuda_runtime.h>\n\ntemplate <typename scalar_t>\n__global__ void outer_product_mean_kernel(\n const scalar_t* __restrict__ a,\n const scalar_t* __restrict__ b,\n const float* __restrict__ mask,\n scalar_t* __restrict__ outer_out,\n int B, int S, int N, int H) {\n \n // blockIdx.x encodes (b * N + i) * N + j\n int b = blockIdx.x / (N * N);\n int remainder = blockIdx.x % (N * N);\n int i = remainder / N;\n int j = remainder % N;\n \n int h1 = threadIdx.y;\n int h2 = threadIdx.x;\n \n // Strides for a and b tensors (shape: B, S, N, H)\n // a[b, s, i, h1], b[b, s, j, h2]\n int stride_b_a = S * N * H;\n int stride_s_a = N * H;\n int stride_n_a = H;\n \n // Compute mask sum for position (b, i) and (b, j)\n float mask_sum_i = 0.0f;\n float mask_sum_j = 0.0f;\n for (int s = 0; s < S; s++) {\n mask_sum_i += mask[b * S * N + s * N + i];\n mask_sum_j += mask[b * S * N + s * N + j];\n }\n float mask_sum = mask_sum_i * mask_sum_j;\n mask_sum = max(mask_sum, 1.0f); // Avoid division by zero\n \n // Compute outer product: sum over s of a[b,s,i,h1] * b[b,s,j,h2] * mask_i * mask_j\n scalar_t sum = scalar_t(0);\n for (int s = 0; s < S; s++) {\n float m_i = mask[b * S * N + s * N + i];\n float m_j = mask[b * S * N + s * N + j];\n scalar_t a_val = a[b * stride_b_a + s * stride_s_a + i * stride_n_a + h1];\n scalar_t b_val = b[b * stride_b_a + s * stride_s_a + j * stride_n_a + h2];\n sum += a_val * b_val * m_i * m_j;\n }\n sum /= mask_sum;\n \n // Write output (shape: B, N, N, H, H)\n outer_out[b * N * N * H * H + i * N * H * H + j * H * H + h1 * H + h2] = sum;\n}\n\ntorch::Tensor outer_product_mean_cuda(torch::Tensor a, torch::Tensor b, torch::Tensor mask) {\n int B = a.size(0);\n int S = a.size(1);\n int N = a.size(2);\n int H = a.size(3);\n \n auto outer_out = torch::empty({B, N, N, H, H}, a.options());\n \n dim3 threadsPerBlock(H, H);\n int numBlocks = B * N * N;\n \n AT_DISPATCH_FLOATING_TYPES(a.type(), \"outer_product_mean\", ([&] {\n outer_product_mean_kernel<scalar_t><<<numBlocks, threadsPerBlock>>>(\n a.data_ptr<scalar_t>(),\n b.data_ptr<scalar_t>(),\n mask.data_ptr<float>(),\n outer_out.data_ptr<scalar_t>(),\n B, S, N, H);\n }));\n \n return outer_out;\n}\n\"\"\"\n\nmodule = load_inline(\n name='outer_product_mean_module',\n cpp_sources='',\n cuda_sources=cuda_source,\n functions=['outer_product_mean_cuda'],\n extra_cuda_cflags=['-O3', '--use_fast_math'],\n verbose=False,\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, msa_dim, pair_dim, hidden_dim=32):\n super().__init__()\n self.layer_norm = nn.LayerNorm(msa_dim)\n self.proj_a = nn.Linear(msa_dim, hidden_dim)\n self.proj_b = nn.Linear(msa_dim, hidden_dim)\n self.out_proj = nn.Linear(hidden_dim * hidden_dim, pair_dim)\n\n def forward(self, msa_repr: torch.Tensor, msa_mask: torch.Tensor) -> torch.Tensor:\n x = self.layer_norm(msa_repr)\n a = self.proj_a(x)\n b = self.proj_b(x)\n \n # Use custom CUDA kernel for outer product mean\n outer = module.outer_product_mean_cuda(a, b, msa_mask)\n \n B, N1, N2, H1, H2 = outer.shape\n outer = outer.reshape(B, N1, N2, H1 * H2)\n return self.out_proj(outer)"
}