reasoning
Let me understand the task:
1. The reference implementation takes an input tensor of shape (R, B, H) = (4, 12, 32)
2. It "virtually all-gathers" by stacking R tensors of shape (B, H) into (B, R*H) = (B, 128)
3. Then applies a linear layer to get output of shape (B, K) = (12, 56)
The forward function:
- Takes x of shape (R, B, H)
- Splits into R parts along dim 0
- Concatenates along dim -1 to get (B, R*H)
- Applies linear projection
So essentially it's doing:
1. Reshape/transpose from (R, B, H) to (B, R*H)
2. Linear projection
The key insight is that `torch.cat([x[i] for i in range(R)], dim=-1)` where x[i] has shape (B, H) creates a tensor of shape (B, R*H). This is equivalent to reshaping/transposing x.
If x has shape (R, B, H), then:
- x.permute(1, 0, 2) gives shape (B, R, H)
- x.permute(1, 0, 2).reshape(B, R*H) gives shape (B, R*H)
So we can fuse:
1. The transpose/reshape operation
2. The linear operation
This is essentially a fused kernel that does:
- Input: (R, B, H)
- Transpose to (B, R, H)
- Linear to (B, K)
The linear operation is: y = x @ W.T + b, where W has shape (K, R*H) and b has shape (K)
We can write a custom CUDA kernel that:
1. Reads input from (R, B, H) layout
2. Computes the linear operation directly without materializing the intermediate (B, R*H) tensor
3. Leverages coalesced memory access by having each thread handle one output element, computing the dot product by iterating over all R*H input values in the transposed order, then adding the bias
For performance, I'll have each thread compute one output element (b, k), load the weight matrix efficiently, and perform the computation with proper memory access patterns. The weight matrix is (K, R*H), bias is (K), and input is (R, B, H). For output element y[b, k], we compute y[b, k] = sum over r,h of x[r, b, h] * W[k, r*H + h] + bias[k], which requires careful indexing to maintain coalescing.
Let me reconsider the memory access pattern. If threads are organized with x as threadIdx.x and y as blockIdx.x * blockDim.x + threadIdx.x, where x maps to the K dimension and y to the B dimension, I can have each thread compute one output element (b, k). The thread stride becomes 1 for k and B for b. This allows the kernel to iterate over the input tensor efficiently while loading weights with coalesced access.
For each input element at position (r, b, h), I need to compute its contribution across all k outputs. The weight access pattern is W[k, r*H + h] for each k. However, I'm realizing the input tensor shape might be different than I initially assumed—let me verify the dimensions are actually (R, B, H) and not something else.
Given that shape, the thread indexing should use `idx = blockIdx.x * blockDim.x + threadIdx.x` and `idy = blockIdx.y * blockDim.y + threadIdx.y` to map threads to output elements, then verify each thread stays within bounds before computing its partial sum over the R and H dimensions.
For each thread computing one output element, I need to iterate through all input elements, accumulating the dot product. The weight matrix is stored as (R*H, K) in column-major format, so accessing W_col[r*H + h, k] retrieves the correct weight for input[r, b, h] multiplied by output k. This requires loading the bias separately and iterating through the full input dimension.
With a fused kernel, each thread handles a single (b, k) output pair, loading weights from global memory as needed. The alternative is breaking this into two separate operations: first a transpose/reshape step that converts (R, B, H) to (B, R*H), then a standard linear layer. This modular approach trades some efficiency for clarity, since the transpose can leverage optimized library functions.
The transpose kernel itself could use a tiled approach for better memory access patterns, but cuBLAS provides geam which handles transposition efficiently. Since PyTorch's contiguous view operations don't actually move data when the reshape dimensions align with the original tensor's layout, there might be an opportunity to fuse the transpose directly into the linear operation without intermediate materialization.
However, this won't work here because the memory layout requires reordering—x is stored as (R, B, H) with elements from the same R adjacent, but we need them from the same H adjacent. A CUDA kernel would need to handle this explicitly. For simplicity, using cuBLAS for the transposition via a contiguous operation followed by the linear layer could work well, though writing a fused kernel that performs both steps in one pass would likely be more efficient by avoiding intermediate memory writes.
The fused approach would compute output[b, k] by iterating over r and h to directly access input[r, b, h] and weight[k, r*H + h], adding bias[k] at the end. To make this practical, I'd want to tile the computation and leverage shared memory for better performance.
With tile dimensions of TILE_K and TILE_B, each thread block handles a TILE_B × TILE_K output tile. The shared memory would hold TILE_B × H input tiles and TILE_K × H weight tiles, allowing threads to compute partial products and accumulate results across tiles before writing the final output.
Since weights total 7168 elements and input is 1536 elements, both fit comfortably in shared memory. Rather than trying to optimize memory reuse across the H dimension, I can load the input and weights directly into shared memory once per thread block, then have each thread iterate through the H dimension to compute its output element. This eliminates redundant global memory accesses and keeps the working set in fast memory throughout the computation.
For better cache utilization with the weights, I'll structure the thread blocks so consecutive threads access consecutive weight elements. With a block size of 16×16, I can map threads so that threadIdx.x corresponds to the K dimension and threadIdx.y to the B dimension. This arrangement means threads in a warp (consecutive threadIdx.x values) read consecutive K indices, giving coalesced memory access when loading weights from global memory into shared memory.
The kernel setup uses blockIdx.x and blockIdx.y to map blocks to output tiles, with each thread computing one output element. I need to verify the weight matrix ordering—PyTorch stores Linear weights as (out_features, in_features), so the output at position [b, k] is the dot product of input[b, :] with weights[k, :] plus the bias.
For the input tensor x with shape (R, B, H), I'll treat it as a flattened array where index [r, b, h] maps to position r*B*H + b*H + h, and similarly for weights with shape (K, R*H).
I can leverage PyTorch's CUBLAS bindings directly. A transpose and reshape operation on x followed by a CUBLAS GEMM call should handle this efficiently. I need to complete the partial thought about the memory layout issue and then summarize the approach.
The reshape alone won't reorder elements correctly since x is stored as (R, B, H) but needs (B, R, H) layout. I should either use permute().contiguous() to properly reorder, or fuse the transpose directly into the GEMM operation using the appropriate transa parameter. PyTorch's addmm function can handle the fused operation, so I'll explore using that with the properly transposed input to avoid the intermediate memory copy.
For a custom CUDA kernel, I need to determine the optimal thread and block configuration. With output dimensions (B, K) = (12, 56), I'm considering whether to have each thread compute one output element, use 1D blocks across the K dimension with 64-128 threads per block, or structure blocks to handle the B dimension efficiently.
Given the small batch size of 12, a 1D block layout with 64 threads works well—I can assign one thread per K index, with blockIdx.y handling the batch dimension. This gives me 12 blocks of 64 threads each, where each thread computes the dot product across the R*H input dimension for its assigned (batch, feature) pair.
Each thread computes output[b, k] = sum over r,h of input[r, b, h] * weight[k, r*H + h]. To improve efficiency, I should leverage shared memory for the bias vector since all threads in a block access the same bias values repeatedly, and optimize weight access patterns to maximize cache utilization.
With K=56, I can have each thread handle one or more k indices. Setting blockDim.x to 64 threads covers all 56 needed indices—thread 0 computes k=0, thread 1 computes k=1, and so on. Within each thread, I iterate across r and h to compute the dot product, accumulating partial sums before adding the corresponding bias term.
For better memory performance, I'll load weights into shared memory with each thread pulling one weight element, then reusing those weights across the batch dimension. I'll also tile the H dimension to reduce register pressure, though I should start with a simpler version and optimize incrementally. I can leverage PyTorch's optimized addmm function to compute the matrix multiplication with the bias in a single call. After transposing and reshaping the input tensor to get it into the right format, addmm will handle the linear transformation efficiently on GPU.
The challenge is that transposing creates an intermediate tensor. A fused CUDA kernel could eliminate this intermediate by computing the transpose and matrix multiplication simultaneously, reading directly from the original tensor layout. In such a kernel, each output element (b, k) would be computed by accessing elements from the original tensor at position (r, b, h) multiplied by the corresponding weight (k, r*H + h), accumulating across all r and h values before adding bias is added.
For implementation, I'm considering a tiled approach using shared memory. With dimensions (B, K, R, H) = (12, 56, 4, 32), I can structure blocks to have threads cooperate on loading input tiles and weight tiles, then compute partial sums that get accumulated. Setting BLOCK_SIZE to 16 with TILE_H of 32 threads allows efficient shared memory usage while keeping each thread responsible for computing one output element.
In the kernel, each thread processes one output location by iterating through tiles of the H dimension, loading input and weight data cooperatively into shared memory buffers, then computing dot products across the R and tiled H dimensions. The bias gets added at the end after all tiles are processed.
For the weight loading, threads with local index less than TILE_H fetch weight elements from global memory into shared storage. The same pattern applies to input loading, where threads cooperatively load the current tile of input data.
However, there's a synchronization issue—after the second `__syncthreads()` call, threads that already loaded input data might load weight data while other threads are still using the previous weight values in shared memory. I need to rethink the cooperative loading strategy to ensure both input and weight tiles are fully loaded before any thread begins computation.
A better approach uses separate shared memory buffers for weights and inputs. Each thread loads one weight element and one input element, then `__syncthreads()` ensures both buffers are populated before the dot product computation begins. This way, all threads can safely access both shared memory arrays during the computation phase without race conditions. = 1.0f;
}
// Load input[r, b, h] for our b
// Each thread loads for one h
if (s_h < H) {
s_input[s_h] = input[b_idx * H + s_h];
}
__syncthreads();
// Compute partial dot product
for (int h = 0; h < H; h++) {
sum += s_input[h] * s_weight[k_idx * R * H + r * H + h];
}
}
```
I need to reconsider the layout here. The weight tensor is shaped (K, R*H), so when I access s_weight, I'm using a shared memory buffer that's been loaded from global memory. The indexing should account for how the weights are actually organized in shared memory versus the original tensor layout.
Let me step back and think through the memory access patterns more carefully. I want to leverage shared memory effectively for the transpose-and-gemm operation, but I'm overcomplicating the indexing scheme. A cleaner approach would be to focus on straightforward weight loading without the shared memory abstraction for now, then optimize if needed.
In the kernel, each thread handles one (b, k) pair. For each r and h combination, I load the input element at [r, b, h], fetch the corresponding weight at [k, r*H + h], accumulate their product into a running sum, then add the bias term for that output index. The bias gets loaded separately from the bias tensor.
I'm reconsidering the thread indexing to avoid bank conflicts. If I thread across K within a block and batch across blocks, each warp handles consecutive k values, which gives coalesced reads on the bias vector. The input tensor reads would be strided by B threads per (r, b, h) element, creating potential inefficiency, but since threads in a warp access the same r, b, h elements with different k values, the weights become the bottleneck—each thread reads different weight elements scattered across the (K, R*H) tensor, leading to poor memory access patterns.
Switching to thread across B and batch across K inverts this: consecutive threads now read consecutive input elements from (r, b, h) with varying b values, achieving coalesced input access, while weights become duplicated across threads with poor locality, though caching may help given the weight tensor fits in L2 cache.
For the final implementation, I'll use standard PyTorch operations—transpose and reshape to (B, R*H), then a linear layer—since the JIT compiler already fuses these effectively. For a custom CUDA approach, cuBLAS provides the most reliable path: transpose to contiguous format, then perform matrix multiplication.
For a hand-written kernel, I'd load weights and bias into shared memory, then have each thread compute one output element by iterating through the input. The thread mapping uses blockIdx.x for the K dimension and blockIdx.y for the batch dimension, with bounds checking and direct weight access from global memory.
For each (r, h) pair, I compute the dot product by accumulating input[r, b_idx, h] * weight[k_idx, r*H + h]. After the loop completes, I add the bias term and store the result. This approach has straightforward memory access patterns and should perform reasonably well.
Now I need to build the complete Python wrapper around this kernel.
call → submit_kernel
callsubmit_kernel
{
"kernel_code": "\"\"\"\nFused virtual all-gather + Linear kernel.\nOptimized CUDA implementation that fuses the transpose with the GEMM operation.\n\"\"\"\n\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Compile CUDA kernel at module load time\ncuda_module = load_inline(\n name='fused_allgather_linear',\n cpp_sources='',\n cuda_sources=r'''\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n\ntemplate <int TILE_SIZE>\n__global__ void fused_allgather_linear_kernel(\n const float* __restrict__ input, // (R, B, H)\n const float* __restrict__ weight, // (K, R*H)\n const float* __restrict__ bias, // (K,)\n float* __restrict__ output, // (B, K)\n int B, int K, int R, int H) {\n \n // Block maps to one B element, threads cover K dimension\n int b_idx = blockIdx.y;\n int k_idx = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (b_idx >= B || k_idx >= K) return;\n \n float sum = 0.0f;\n \n // Iterate over r and h\n for (int r = 0; r < R; r++) {\n // Load input[r, b, :] into shared memory for cache efficiency\n for (int h = threadIdx.x; h < H; h += blockDim.x) {\n // Prefetch hint for input\n }\n __syncthreads();\n \n for (int h = 0; h < H; h++) {\n // input[r, b, h]\n float in_val = input[r * B * H + b_idx * H + h];\n // weight[k, r*H + h]\n float w_val = weight[k_idx * R * H + r * H + h];\n sum += in_val * w_val;\n }\n }\n \n // Add bias\n sum += bias[k_idx];\n \n output[b_idx * K + k_idx] = sum;\n}\n\n// Optimized version with weight caching via shared memory\n__global__ void fused_allgather_linear_cached_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int B, int K, int R, int H) {\n \n int b_idx = blockIdx.y;\n int k_idx = blockIdx.x * blockDim.x + threadIdx.x;\n int t_idx = threadIdx.x;\n \n if (b_idx >= B || k_idx >= K) return;\n \n // Shared memory for bias (small, K elements max)\n extern __shared__ float shared_mem[];\n float* s_bias = shared_mem;\n \n // Cooperatively load bias into shared memory\n for (int i = t_idx; i < K; i += blockDim.x) {\n s_bias[i] = bias[i];\n }\n __syncthreads();\n \n float sum = 0.0f;\n \n // Process each r and h\n // Input access pattern: for fixed (r, b), vary h -> coalesced\n // Weight access pattern: for fixed k, vary (r, h) -> coalesced within thread\n \n for (int r = 0; r < R; r++) {\n for (int h = 0; h < H; h++) {\n float in_val = input[r * B * H + b_idx * H + h];\n float w_val = weight[k_idx * R * H + r * H + h];\n sum += in_val * w_val;\n }\n }\n \n // Add bias from shared memory\n sum += s_bias[k_idx];\n \n output[b_idx * K + k_idx] = sum;\n}\n\n// Vectorized version using float4 for better memory throughput\n__global__ void fused_allgather_linear_vec4_kernel(\n const float* __restrict__ input,\n const float* __restrict__ weight,\n const float* __restrict__ bias,\n float* __restrict__ output,\n int B, int K, int R, int H) {\n \n int b_idx = blockIdx.y;\n int k_idx = blockIdx.x * blockDim.x + threadIdx.x;\n \n if (b_idx >= B || k_idx >= K) return;\n \n float sum = 0.0f;\n \n // Process in chunks of 4 when possible\n int H_vec4 = (H / 4) * 4;\n \n for (int r = 0; r < R; r++) {\n // Vectorized loads\n const float4* input_vec = reinterpret_cast<const float4*>(\n input + r * B * H + b_idx * H);\n const float4* weight_vec = reinterpret_cast<const float4*>(\n weight + k_idx * R * H + r * H);\n \n int h_vec = 0;\n for (; h_vec < H_vec4 / 4; h_vec++) {\n float4 in_vec = input_vec[h_vec];\n float4 w_vec = weight_vec[h_vec];\n sum += in_vec.x * w_vec.x + in_vec.y * w_vec.y + \n in_vec.z * w_vec.z + in_vec.w * w_vec.w;\n }\n \n // Handle remaining elements\n for (int h = h_vec * 4; h < H; h++) {\n sum += input[r * B * H + b_idx * H + h] * \n weight[k_idx * R * H + r * H + h];\n }\n }\n \n sum += bias[k_idx];\n output[b_idx * K + k_idx] = sum;\n}\n\ntorch::Tensor fused_allgather_linear_cuda(\n torch::Tensor input, // (R, B, H)\n torch::Tensor weight, // (K, R*H)\n torch::Tensor bias, // (K,)\n int R, int B, int H, int K) {\n \n auto output = torch::empty({B, K}, input.options());\n \n // Use vectorized kernel for better memory throughput\n int threads_per_block = 64;\n int blocks_k = (K + threads_per_block - 1) / threads_per_block;\n \n dim3 grid(blocks_k, B);\n dim3 block(threads_per_block);\n \n // Choose kernel based on H alignment\n if (H % 4 == 0) {\n fused_allgather_linear_vec4_kernel<<<grid, block>>>(\n input.data_ptr<float>(),\n weight.data_ptr<float>(),\n bias.data_ptr<float>(),\n output.data_ptr<float>(),\n B, K, R, H);\n } else {\n // Allocate shared memory for bias\n int shared_mem_size = K * sizeof(float);\n fused_allgather_linear_cached_kernel<<<grid, block, shared_mem_size>>>(\n input.data_ptr<float>(),\n weight.data_ptr<float>(),\n bias.data_ptr<float>(),\n output.data_ptr<float>(),\n B, K, R, H);\n }\n \n return output;\n}\n''',\n functions=['fused_allgather_linear_cuda'],\n extra_cuda_cflags=['-O3', '--use_fast_math'],\n verbose=False\n)\n\nR = 4\nB = 12\nH = 32\nK = 56\n\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n self.proj = nn.Linear(R * H, K, bias=True)\n # Ensure weights are contiguous for optimal CUDA access\n self.proj.weight.data = self.proj.weight.data.contiguous()\n if self.proj.bias is not None:\n self.proj.bias.data = self.proj.bias.data.contiguous()\n\n def forward(self, x: torch.Tensor) -> torch.Tensor:\n r, b, h = x.shape\n assert r == R and h == H\n \n # Use custom fused kernel\n return cuda_module.fused_allgather_linear_cuda(\n x.contiguous(),\n self.proj.weight,\n self.proj.bias,\n R, B, H, K\n )"
}