reasoning
reasoning (truncated 23416 chars): Let me analyze this task. I need to implement a custom CUDA kernel for sparse message passing with CSR adjacency. This is essentially a sparse matrix-vector multiplication where we aggregate weighted source-node features.
The reference implementation:
1. For each destination node, it finds the range of edges [start, end) from the CSR row_ptr
2. Gets source nodes from col_idx
3. Multiplies node features by edge weights
4. Sums the results
This is essentially: output = A^T * (W * X) where A is the adjacency matrix in CSR format, W is diagonal edge weights, and X is node features.
For a CUDA implementation, I should:
1. Assign one thread per destination node (or one warp/block for better performance)
2. Each thread iterates over its edges and accumulates the weighted features
Let me design the kernel:
Option 1: One thread per destination node
- Simple but may have load imbalance
- Each thread processes all edges for one node
Option 2: One warp per destination node
- Better for memory coalescing if multiple threads help load features
Option 3: One block per destination node (overkill for avg_degree=20)
Given avg_degree=20 and feat_dim=96, I think one thread per node might work well, but let me consider the memory access pattern.
Actually, for better performance, I should consider:
- Using shared memory for frequently accessed node features (but each edge may access different source nodes)
- Coalesced memory access
Let me start with a simple approach: one thread per destination node, and see if it works.
```cpp
__global__ void sparse_agg_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_weight,
const float* __restrict__ node_feat,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x * blockDim.x + threadIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
for (int f = 0; f < feat_dim; f++) {
float sum = 0.0f;
for (int e = start; e < end; e++) {
int src = col_idx[e];
sum += edge_weight[e] * node_feat[src * feat_dim + f];
}
out[dst * feat_dim + f] = sum;
}
}
```
This might have poor memory access patterns because:
- edge_weight is accessed sequentially (good)
- col_idx is accessed sequentially (good)
- node_feat is accessed with stride feat_dim for each feature dimension (poor coalescing within a thread, but across threads accessing different destination nodes, it's random)
Let me try a different approach where we iterate over edges first then features:
```cpp
__global__ void sparse_agg_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_weight,
const float* __restrict__ node_feat,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x * blockDim.x + threadIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
// Initialize output to zero
for (int f = 0; f < feat_dim; f++) {
out[dst * feat_dim + f] = 0.0f;
}
// Iterate over edges
for (int e = start; e < end; e++) {
int src = col_idx[e];
float w = edge_weight[e];
for (int f = 0; f < feat_dim; f++) {
out[dst * feat_dim + f] += w * node_feat[src * feat_dim + f];
}
}
}
```
This is better because we load the source node features more efficiently - once per edge rather than once per feature per edge.
Let me think about optimizations:
1. Use vectorized loads/stores for feat_dim features
2. Use shared memory for output accumulation
3. Consider using warp-level primitives
Actually, for feat_dim=96, we could use float4 loads to load 4 floats at a time.
Let me also consider using a warp per destination node if the degree is high, but with avg_degree=20, one thread per node should be fine.
Let me write a complete solution:
```python
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
# Define the CUDA kernel
cuda_source = """
__global__ void sparse_agg_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_weight,
const float* __restrict__ node_feat,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x * blockDim.x + threadIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
// Initialize output to zero
for (int f = 0; f < feat_dim; f++) {
out[dst * feat_dim + f] = 0.0f;
}
// Iterate over edges
for (int e = start; e < end; e++) {
int src = col_idx[e];
float w = edge_weight[e];
for (int f = 0; f < feat_dim; f++) {
out[dst * feat_dim + f] += w * node_feat[src * feat_dim + f];
}
}
}
torch::Tensor sparse_agg_cuda(
torch::Tensor row_ptr,
torch::Tensor col_idx,
torch::Tensor edge_weight,
torch::Tensor node_feat
) {
int num_nodes = row_ptr.numel() - 1;
int feat_dim = node_feat.size(1);
auto out = torch::zeros({num_nodes, feat_dim}, node_feat.options());
int threads = 256;
int blocks = (num_nodes + threads - 1) / threads;
sparse_agg_kernel<<<blocks, threads>>>(
row_ptr.data_ptr<int>(),
col_idx.data_ptr<int>(),
edge_weight.data_ptr<float>(),
node_feat.data_ptr<float>(),
out.data_ptr<float>(),
num_nodes,
feat_dim
);
return out;
}
"""
cpp_source = """
torch::Tensor sparse_agg_cuda(
torch::Tensor row_ptr,
torch::Tensor col_idx,
torch::Tensor edge_weight,
torch::Tensor node_feat
);
"""
# Load the extension
sparse_agg_ext = load_inline(
name='sparse_agg',
cpp_sources=cpp_source,
cuda_sources=cuda_source,
functions=['sparse_agg_cuda'],
extra_cuda_cflags=['-O3'],
)
class ModelNew(nn.Module):
def __init__(self):
super().__init__()
def forward(
self,
row_ptr: torch.Tensor,
col_idx: torch.Tensor,
edge_weight: torch.Tensor,
node_feat: torch.Tensor,
) -> torch.Tensor:
return sparse_agg_ext.sparse_agg_cuda(row_ptr, col_idx, edge_weight, node_feat)
```
Let me think about potential issues:
1. The input tensors might be on CPU - need to check device
2. The input might have different dtypes - need to handle
Actually, looking at the reference, the tensors are created with specific types and devices, so we should be fine.
Let me also consider optimizations:
1. Vectorized memory access using float4
2. Using __ldg() for read-only data
3. Loop unrolling
Let me try with some optimizations:
```cpp
__global__ void sparse_agg_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_weight,
const float* __restrict__ node_feat,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x * blockDim.x + threadIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
// Use shared memory for accumulation
extern __shared__ float shared_out[];
float* my_out = shared_out + threadIdx.x * feat_dim;
// Initialize to zero
for (int f = 0; f < feat_dim; f++) {
my_out[f] = 0.0f;
}
// Iterate over edges
for (int e = start; e < end; e++) {
int src = __ldg(&col_idx[e]);
float w = __ldg(&edge_weight[e]);
const float* src_feat = node_feat + src * feat_dim;
for (int f = 0; f < feat_dim; f++) {
my_out[f] += w * __ldg(&src_feat[f]);
}
}
// Write to global memory
for (int f = 0; f < feat_dim; f++) {
out[dst * feat_dim + f] = my_out[f];
}
}
```
Wait, using shared memory this way requires d
...[ omitted 23416 characters ]...
Tensor col_idx,
torch::Tensor edge_weight,
torch::Tensor node_feat
);
"""
sparse_agg_ext = load_inline(
name='sparse_agg_ext',
cpp_sources=cpp_source,
cuda_sources=cuda_source,
functions=['sparse_agg_cuda'],
extra_cuda_cflags=['-O3'],
)
class ModelNew(nn.Module):
def __init__(self):
super().__init__()
def forward(
self,
row_ptr: torch.Tensor,
col_idx: torch.Tensor,
edge_weight: torch.Tensor,
node_feat: torch.Tensor,
) -> torch.Tensor:
return sparse_agg_ext.sparse_agg_cuda(row_ptr, col_idx, edge_weight, node_feat)
```
This should work, but might not be the fastest. Let me add some optimizations:
1. Use __ldg for read-only node_feat to utilize the read-only cache
2. Consider using a constant memory for feat_dim (or template parameter)
Actually, let me try using the constant cache for node_feat. Hmm, that won't work because node_feat is too large.
Let me think about this differently. The bottleneck is likely the random memory access to node_feat. Each edge accesses a random source node's features.
One optimization is to use shared memory to cache node features, but with num_nodes=768 and feat_dim=96, we'd need 295KB which is too much for shared memory.
Another approach is to prefetch. We can use __ldg() to prefetch the source node features while computing.
Actually, let me just try a straightforward implementation with __ldg and see if it's faster:
```cpp
__global__ void sparse_agg_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_weight,
const float* __restrict__ node_feat,
float* __restrict__ out,
const int num_nodes,
const int feat_dim
) {
const int dst = blockIdx.x * blockDim.x + threadIdx.x;
if (dst >= num_nodes) return;
const int start = row_ptr[dst];
const int end = row_ptr[dst + 1];
float* out_row = out + dst * feat_dim;
// Zero initialize output
for (int f = 0; f < feat_dim; f++) {
out_row[f] = 0.0f;
}
// Process each incoming edge
for (int e = start; e < end; e++) {
const int src = col_idx[e];
const float w = edge_weight[e];
for (int f = 0; f < feat_dim; f++) {
out_row[f] += w * __ldg(&node_feat[src * feat_dim + f]);
}
}
}
```
Actually, I realize I should check what happens with the restrict keyword on the output pointer. Since I'm modifying out_row in place, the restrict keyword should still be fine as long as I don't alias.
Let me also consider using cudaMemcpyAsync or other optimizations, but for this simple problem, the basic kernel should be sufficient.
Let me finalize the solution. I'll use a combination of techniques:
1. Simple one-thread-per-node approach
2. __ldg for read-only node_feat access
3. Proper restrict qualifiers
Wait, I just realized that with 768 nodes and 256 threads per block, we have 3 blocks. That's very low occupancy on modern GPUs. Let me consider using fewer threads per block to get more blocks:
With 64 threads per block: 768/64 = 12 blocks
With 128 threads per block: 768/128 = 6 blocks
With 256 threads per block: 768/256 = 3 blocks
For better latency hiding, more blocks are better. Let me use 64 or 128 threads per block.
But wait, 768 nodes with 256 threads gives 3 blocks. With 128 threads, we get 6 blocks. With 64 threads, we get 12 blocks. All are quite low.
The issue is that the problem size (768 nodes) is small. We might not get good GPU utilization regardless of the block size.
Let me stick with 256 threads for now and see how it performs. The kernel should still be faster than PyTorch because:
1. No Python overhead
2. No tensor indexing overhead
3. Better memory access patterns (within each thread)
Actually, let me think about this more. The reference implementation does:
```python
for dst in range(num_nodes):
start = int(row_ptr[dst].item())
end = int(row_ptr[dst + 1].item())
if end > start:
src = col_idx[start:end].long()
weights = edge_weight[start:end].unsqueeze(-1)
out[dst] = (node_feat[src] * weights).sum(dim=0)
```
This creates many small tensor operations which is very inefficient on GPU. The CUDA kernel should be much faster just by avoiding this overhead.
Let me finalize the solution:
```python
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
cuda_source = """
__global__ void sparse_agg_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_weight,
const float* __restrict__ node_feat,
float* __restrict__ out,
const int num_nodes,
const int feat_dim
) {
const int dst = blockIdx.x * blockDim.x + threadIdx.x;
if (dst >= num_nodes) return;
const int start = row_ptr[dst];
const int end = row_ptr[dst + 1];
float* out_row = out + dst * feat_dim;
// Zero initialize output
for (int f = 0; f < feat_dim; f++) {
out_row[f] = 0.0f;
}
// Process each incoming edge
for (int e = start; e < end; e++) {
const int src = col_idx[e];
const float w = edge_weight[e];
const float* src_feat = node_feat + src * feat_dim;
for (int f = 0; f < feat_dim; f++) {
out_row[f] += w * src_feat[f];
}
}
}
torch::Tensor sparse_agg_cuda(
torch::Tensor row_ptr,
torch::Tensor col_idx,
torch::Tensor edge_weight,
torch::Tensor node_feat
) {
const int num_nodes = row_ptr.numel() - 1;
const int feat_dim = node_feat.size(1);
auto out = torch::zeros({num_nodes, feat_dim}, node_feat.options());
const int threads = 256;
const int blocks = (num_nodes + threads - 1) / threads;
sparse_agg_kernel<<<blocks, threads>>>(
row_ptr.data_ptr<int>(),
col_idx.data_ptr<int>(),
edge_weight.data_ptr<float>(),
node_feat.data_ptr<float>(),
out.data_ptr<float>(),
num_nodes,
feat_dim
);
return out;
}
"""
cpp_source = """
torch::Tensor sparse_agg_cuda(
torch::Tensor row_ptr,
torch::Tensor col_idx,
torch::Tensor edge_weight,
torch::Tensor node_feat
);
"""
sparse_agg_ext = load_inline(
name='sparse_agg_ext',
cpp_sources=cpp_source,
cuda_sources=cuda_source,
functions=['sparse_agg_cuda'],
extra_cuda_cflags=['-O3'],
)
class ModelNew(nn.Module):
def __init__(self):
super().__init__()
def forward(
self,
row_ptr: torch.Tensor,
col_idx: torch.Tensor,
edge_weight: torch.Tensor,
node_feat: torch.Tensor,
) -> torch.Tensor:
return sparse_agg_ext.sparse_agg_cuda(row_ptr, col_idx, edge_weight, node_feat)
```
This should work. Let me double-check the code for any issues:
1. The kernel uses int* for row_ptr and col_idx which matches the int32 dtype in the reference
2. The kernel uses float* for edge_weight and node_feat which matches the float32 dtype in the reference
3. The output is initialized to zero, matching the reference
One thing I want to verify: the reference uses `col_idx[start:end].long()` to convert to int64 for indexing. In my kernel, I use int32 directly. Since num_nodes=768, int32 is sufficient.
Let me also make sure the device and dtype handling is correct:
- All input tensors should be on the same device (GPU)
- All tensors should have matching dtypes
The kernel assumes this, which should be fine for the benchmark.
I think this solution is ready. Let me submit it.
Wait, I should add some safety checks and ensure the code compiles correctly. Let me also make sure the `.data_ptr<int>()` is correct for int32 tensors.
Actually, I realized I should use `row_ptr.data_ptr<int32_t>()` or verify that `int` on the CUDA side matches `torch.int32`. On most systems, `int` is 32-bit, so this should be fine.
Let me finalize and submit:
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__global__ void sparse_agg_kernel(\n const int* __restrict__ row_ptr,\n const int* __restrict__ col_idx,\n const float* __restrict__ edge_weight,\n const float* __restrict__ node_feat,\n float* __restrict__ out,\n const int num_nodes,\n const int feat_dim\n) {\n const int dst = blockIdx.x * blockDim.x + threadIdx.x;\n if (dst >= num_nodes) return;\n \n const int start = row_ptr[dst];\n const int end = row_ptr[dst + 1];\n \n float* out_row = out + dst * feat_dim;\n \n // Zero initialize output\n for (int f = 0; f < feat_dim; f++) {\n out_row[f] = 0.0f;\n }\n \n // Process each incoming edge\n for (int e = start; e < end; e++) {\n const int src = col_idx[e];\n const float w = edge_weight[e];\n const float* src_feat = node_feat + src * feat_dim;\n \n #pragma unroll 8\n for (int f = 0; f < feat_dim; f++) {\n out_row[f] += w * src_feat[f];\n }\n }\n}\n\ntorch::Tensor sparse_agg_cuda(\n torch::Tensor row_ptr,\n torch::Tensor col_idx,\n torch::Tensor edge_weight,\n torch::Tensor node_feat\n) {\n const int num_nodes = row_ptr.numel() - 1;\n const int feat_dim = node_feat.size(1);\n \n auto out = torch::zeros({num_nodes, feat_dim}, node_feat.options());\n \n const int threads = 256;\n const int blocks = (num_nodes + threads - 1) / threads;\n \n sparse_agg_kernel<<<blocks, threads>>>(\n row_ptr.data_ptr<int>(),\n col_idx.data_ptr<int>(),\n edge_weight.data_ptr<float>(),\n node_feat.data_ptr<float>(),\n out.data_ptr<float>(),\n num_nodes,\n feat_dim\n );\n \n return out;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor sparse_agg_cuda(\n torch::Tensor row_ptr,\n torch::Tensor col_idx,\n torch::Tensor edge_weight,\n torch::Tensor node_feat\n);\n\"\"\"\n\nsparse_agg_ext = load_inline(\n name='sparse_agg_ext',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['sparse_agg_cuda'],\n extra_cuda_cflags=['-O3'],\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n \n def forward(\n self,\n row_ptr: torch.Tensor,\n col_idx: torch.Tensor,\n edge_weight: torch.Tensor,\n node_feat: torch.Tensor,\n ) -> torch.Tensor:\n return sparse_agg_ext.sparse_agg_cuda(row_ptr, col_idx, edge_weight, node_feat)"
}