reasoning
The issue is that row_ptr is computed with torch.cumsum of int32, creating int32 tensor, but in the kernel we're accessing it as if it's a regular int. The problem is that `degree` is int32 and `row_ptr` is int32. Let me check - the error says `expected scalar type Int but found Float`.
Oh wait, looking at the problem more carefully - in PyTorch, the default int type is int64, not int32. When we call `row_ptr.numel() - 1`, that returns int64 scalar tensor. But when we use `.item<int>()`, that tries to convert to C++ int.
The issue is that `row_ptr` type can be either int32 or int64 depending on the inputs. Let me look more carefully...
Actually the get_inputs() function says `row_ptr = torch.zeros(num_nodes + 1, dtype=torch.int32)`. So row_ptr is indeed int32.
But wait, `auto degrees = row_ptr.slice(...) - row_ptr.slice(...)` - this might produce a different dtype. And `degrees.max().item<int>()` might fail if it's not int32.
Let me fix by making sure we read the tensors correctly. I should use int32_t or int64_t depending on the dtype.
Actually, looking at the error `expected scalar type Int but found Float`, this could be happening because `node_value` accessor is float but we're using float, and somewhere else there might be a type mismatch.
Wait, actually this error `expected scalar type Int but found Float` might be from the `.item<int>()` call when the scalar type is float. Let me think...
The `degrees` tensor should be int32 since subtraction of two int32 tensors gives int32. But maybe PyTorch promotes to int64? Or maybe `max()` returns something differently.
Let me just rewrite to properly handle types and avoid the `.item<int>()` call on possibly non-int tensors. I can read the data directly from the row_ptr tensor pointer.
Actually, the simplest approach is to just ensure we use the right type. Let me also use a simpler approach - don't compute max_degree via torch operations, just read the first and last row_ptr entry.
Let me also use a template or just int64_t and do type conversion properly.
Actually, looking at the reference implementation, it uses `.item()` which returns Python int for int32. And my code uses `.item<int>()` which should work for int32... unless there's something about how the tensor type is handled.
Let me check - the row_ptr dtype is int32, so `.item<int>()` should work... unless `degrees.max()` returns something else.
Let me just avoid `.item<int>()` and read the values directly from CPU using `.cpu().item<int>()` or just use `.item<long>()` and cast.
Actually, let me just make the kernel more robust and not depend on computing max_degree in C++. I'll pass row_ptr to the kernel and let the kernel handle variable degrees with dynamic shared memory per block.
But CUDA requires fixed shared memory per block, set at launch. So I still need max_degree. Another approach is to use a fixed shared memory per block (or zero shared memory) and use different strategies.
Actually for small graphs like num_nodes=384, avg_degree=18, feat_dim=64, the max degree would be at most ~24 or so.
Let me just read the max from row_ptr directly.
```cpp
int max_degree = 0;
if (num_nodes > 0) {
int* h_row_ptr = row_ptr.data_ptr<int>();
for (int i = 0; i < num_nodes; i++) {
int deg = h_row_ptr[i+1] - h_row_ptr[i];
if (deg > max_degree) max_degree = deg;
}
}
```
Wait, but `row_ptr` is on GPU, so accessing `row_ptr.data_ptr<int>()` host-side would crash. I'd need to copy to CPU.
Let me just use `cudaMemcpy` or `.cpu()` to get the tensor on CPU.
Actually a much simpler approach:
```cpp
auto row_ptr_cpu = row_ptr.cpu();
int max_degree = 0;
for (int i = 0; i < num_nodes; i++) {
int deg = row_ptr_cpu.data_ptr<int>()[i+1] - row_ptr_cpu.data_ptr<int>()[i];
if (deg > max_degree) max_degree = deg;
}
```
Or even simpler, use torch operations but with proper type handling:
```cpp
auto max_degree_tensor = (row_ptr[1:] - row_ptr[:-1]).max();
int max_degree = max_degree_tensor.item<int>();
```
The issue is: the error says `expected scalar type Int but found Float`. This typically happens when you call `.item<int>()` on a float tensor. So maybe `row_ptr` got converted to float somehow, or the subtraction produced float...
Actually wait - `row_ptr.slice(0, 1, num_nodes + 1)` returns a tensor, then subtracting `row_ptr.slice(0, 0, num_nodes)` - both should be int32 if row_ptr is int32. But maybe num_nodes + 1 and num_nodes get promoted to int tensors differently?
I think the safest approach is to handle everything properly. Let me compute max_degree on the host side using `.to(torch::kCPU)` and manual iteration, or just use a large fixed shared memory allocation and skip computing max_degree entirely if we can.
For the specific problem: num_nodes=384, max_degree is around 24, feat_dim=64. Shared memory for weights (24) + reduction (8 warps for 256 threads) + values (24*64=1536) = ~1568 floats ≈ 6.3 KB. This is well within shared memory.
Actually, I could also just set a large fixed shared memory and not compute max_degree. Like allocate enough for the maximum possible. But what if there are larger degrees in other inputs?
The problem specification says the inputs are generated by `get_inputs()`, so degree ranges from 1 to 24. But for robustness, I should handle arbitrary degrees.
Actually, let me just do the CPU copy approach - it's a tiny tensor (385 elements), so it's negligible cost.
Or I can be smarter and not use shared memory at all for values, just read directly from global memory after weight computation. That way I don't need max_degree.
Let me reconsider the memory access: the slow part is reading node_value. With degree=18 and feat_dim=64, each destination reads 18 source nodes × 64 features = 1152 floats. With 384 nodes, that's 442K floats total. This is small enough that caching might work.
Actually, let me try a different strategy: don't cache node values in shared memory. Instead, aggregate directly by iterating over edges and loading from global memory. This avoids the max_degree shared memory calculation.
For aggregation, each thread handles one feature dimension. For each edge, all threads load the corresponding source value and weight, multiply, and accumulate.
But wait, in this case, threads in the same warp would access different features of the same source node (good coalescing), but across edges they would access different source nodes. With feat_dim=64, that's 64 threads × 4 bytes = 256 bytes per warp per memory instruction, which is good.
Let me try this approach without shared memory for values, just using shared memory for weights and reduction.
Actually, for even better performance, I can have each thread process multiple features. Or I can use vectorized loads.
Let me try a cleaner approach that's more likely to be correct:
```cpp
__global__ void sparse_attention_csr_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_scores,
const float* __restrict__ node_value,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
int degree = end - start;
int tid = threadIdx.x;
int num_threads = blockDim.x;
int num_warps = (num_threads + 31) >> 5;
extern __shared__ float sdata[];
float* s_weights = sdata;
float* s_red = s_weights + degree;
// Load scores and find max
float local_max = -1e20f;
for (int i = tid; i < degree; i += num_threads) {
float score = edge_scores[start + i];
s_weights[i] = score;
local_max = fmaxf(local_max, score);
}
local_max = warp_reduce_max(local_max);
if ((tid & 31) == 0) s_red[tid >> 5] = local_max;
__syncthreads();
if (tid < num_warps) local_max = s_red[tid];
else local_max = -1e20f;
local_max = warp_reduce_max(local_max);
__shared__ float max_score;
if (tid == 0) max_score = local_max;
__syncthreads();
// Exp and sum
float local_sum = 0.0f;
for (int i = tid; i < degree; i += num_threads) {
float w = expf(s_weights[i] - max_score);
s_weights[i] = w;
local_sum += w;
}
local_sum = warp_reduce_sum(local_sum);
if ((tid & 31) == 0) s_red[tid >> 5] = local_sum;
__syncthreads();
if (tid < num_warps) local_sum = s_red[tid];
else local_sum = 0.0f;
local_sum = warp_reduce_sum(local_sum);
__shared__ float sum_exp;
if (tid == 0) sum_exp = local_sum;
__syncthreads();
// Normalize
for (int i = tid; i < degree; i += num_threads) {
s_weights[i] /= sum_exp;
}
__syncthreads();
// Aggregate - directly from global memory
for (int f = tid; f < feat_dim; f += num_threads) {
float val = 0.0f;
for (int e = 0; e < degree; e++) {
int src = col_idx[start + e];
val += node_value[src * feat_dim + f] * s_weights[e];
}
out[dst * feat_dim + f] = val;
}
}
```
For the shared memory size: `degree + num_warps`. Still need degree. But I can avoid needing max_degree by computing it from CPU after copying the small row_ptr tensor.
Actually, let me just avoid this complexity and compute max_degree on the host side by copying row_ptr to CPU:
```cpp
int max_degree = 0;
auto row_ptr_acc = row_ptr.accessor<int, 1>();
for (int i = 0; i < num_nodes; i++) {
max_degree = std::max(max_degree, (int)(row_ptr_acc[i+1] - row_ptr_acc[i]));
}
```
No, `accessor` only works on CPU tensors.
Let me just do:
```cpp
auto row_ptr_cpu = row_ptr.to(torch::kCPU);
int max_degree = 0;
const int* rp = row_ptr_cpu.data_ptr<int>();
for (int i = 0; i < num_nodes; i++) {
max_degree = std::max(max_degree, rp[i+1] - rp[i]);
}
```
This is a tiny CPU tensor copy and should be very fast.
Actually, the error could also be that `row_ptr` was being passed as Float somewhere. Looking again:
The error message is `expected scalar type Int but found Float`. This is a PyTorch runtime error, not a kernel error. It might be from `.item<int>()` on a float tensor, or from trying to access a tensor as int when it's float.
Let me just fix the C++ side to avoid this entirely by removing the problematic `.item<int>()` calls.
Oh I see: `auto degrees = row_ptr.slice(0, 1, num_nodes + 1) - row_ptr.slice(0, 0, num_nodes);`
Wait, `row_ptr` is int32. But `slice` takes int64 indices by default in PyTorch's C++ API. And the `.slice()` method might have issues with int32 tensors? No, that shouldn't be the case.
Actually, the issue might be simpler: in the slice operations, `num_nodes + 1` and `num_nodes` are Python ints passed to C++ as int64_t, and the slice works fine. But the result of subtraction might somehow be int64. And `.item<int>()` on int64 fails because it's `expected scalar type Int but found Long`... no, the error says Float.
Hmm, let me check if maybe `degrees.max()` returns a float for some reason? Actually no, max() of int should be int.
I wonder if the error is actually from somewhere else in my code. Let me re-read...
Oh wait! The problem might be that when `out` is created with `node_value.options()`, if node_value is float then `out` is float, which is correct.
Actually, let me think about whether the error could be from `row_ptr.data_ptr<int>()` when row_ptr is int64. But `get_inputs()` creates int32 row_ptr.
Let me just completely rewrite to avoid any potential type issues. I'll compute max_degree with a CPU copy and use templates if needed.
Actually, what if I just make the kernel work for both int32 and int64 by templating or by casting? The safest is to use `int64_t` for indices.
No wait, the row_ptr is definitely int32. Let me just trace through carefully.
Actually, I think the error might be because `.item<int>()` doesn't work for int32 tensors in some PyTorch versions? Let me check - `.item<int32_t>()` or `.item<int>()`?
Actually, in PyTorch C++ API, `.item<int>()` should work for both int32 and int64 with narrowing. But for float it fails with "expected scalar type Int but found Float".
So the tensor I'm calling `.item<int>()` on is actually float. This means `degrees.max()` or equivalent is a float tensor.
Could it be that `row_ptr.slice(0, 1, num_nodes + 1) - row_ptr.slice(0, 0, num_nodes)` does something strange? No, subtraction of int32 tensors should give int32.
Unless `row_ptr` is float? But `get_inputs()` creates it as int32.
Wait! Could there be an issue because `torch.zeros(num_nodes + 1, dtype=torch.int32)` but then `row_ptr[1:] = torch.cumsum(degree, dim=0)` where `degree` is int32 and `cumsum` of int32 might be int64? Let me think...
`torch.cumsum(torch.tensor([1,2], dtype=torch.int32), dim=0)` should return int32. So that's not it either.
What if I just use a totally different approach that avoids any host-side computation on the tensor? I'll launch the kernel without needing max_degree by not using shared memory for weights.
Actually wait - the error might not even be from my kernel! The error says `expected scalar type Int but found Float`. What if there's no C++ compilation error but when the reference code runs there's an issue? No, `run_correctness` should run the reference first.
Let me re-read my code. I had:
```python
auto degrees = row_ptr.slice(0, 1, num_nodes + 1) - row_ptr.slice(0, 0, num_nodes);
int max_degree = degrees.max().item<int>();
```
The `slice` function in C++: `Tensor slice(int64_t dim = 0, int64_t start = 0, int64_t end = 9223372036854775807, int64_t step = 1)`
Oh! I bet the problem is that `num_nodes + 1` overflows? No, num_nodes is 384.
Hold on. What if the issue is that `.slice(0, 1, num_nodes + 1)` creates an empty tensor or something unexpected? Let me check - for a tensor of size num_nodes+1 = 385, slice from 1 to 385 gives size 384. Slice from 0 to 384 gives size 384. That's correct.
What if the issue is `.item<int>()` when the tensor is on GPU? No, `.item()` should work on GPU tensors too (it copies the scalar to CPU internally).
I think the safest approach is to just compute max_degree from a CPU copy of row_ptr. And also handle the case where row_ptr might be int64.
Let me rewrite entirely with a cleaner approach.
Actually, I'll simplify: instead of computing max_degree on the host, I'll over-allocate shared memory to handle the worst case. But I don't know the worst case degree.
For this specific problem, degree is at most 24. I could hardcode a generous limit. But that won't work for general inputs.
Actually, I can abandon the approach of copying all values to shared memory. Instead:
1. Use shared memory just for weights (need degree floats per block)
2. For the aggregation, read node_value directly from global memory
This still needs max_degree for shared memory, but I can compute it from the graph structure on the host.
Let me just do the CPU copy approach - it's tiny.
Actually, let me also consider: maybe the issue is that when I compiled, something in the PyTorch ops caused the issue. Let me just trace exactly where the error comes from.
The `run_correctness FAILED` message says `expected scalar type Int but found Float`. This is likely a PyTorch C++ exception. Where would this come from?
From my C++ code, the suspicious lines are:
- `row_ptr.data_ptr<int>()` - if row_ptr is not int32
- `degrees.max().item<int>()` - if degrees is float
The first one would give something like `expected scalar type Int but found Long` if it's int64.
So it's probably the `.item<int>()` call. But why would `degrees` be float?
OH! I bet the issue is with how `torch::cumsum` or the tensor operations might have been done in the test. But wait, the `get_inputs()` creates int32 for everything...
Let me just check: what if `row_ptr` is converted to the default dtype somewhere? No, that shouldn't happen.
I think the most robust fix is to just use `int64_t` everywhere for indices and compute max_degree by copying to CPU.
Actually, let me try another approach: don't compute max_degree at all. Instead, process each row cooperatively with a fixed block size and use atomic adds for the output.
Wait, I have another idea. Let's not use shared memory. Instead:
- Each block handles one destination node
- BlockDim.x = feat_dim (64)
- Each thread computes the weighted sum for one feature across all edges
- We compute softmax in parallel using warp reductions, store normalized weights in __shared__
- Then each thread iterates over edges and accumulates
But we still need shared memory for the normalized weights. The amount is `degree * sizeof(float)`. With degree up to ~24, that's 96 bytes. For block size 64, we need at most 24 floats in shared memory. BUT CUDA requires the shared memory size to be known at kernel launch time. So we still need to know max_degree.
Unless... we set a very large shared memory size and only use part of it. Like `max_degree * sizeof(float)` where max_degree is a large enough constant. For safety, we could set it to something like 1024 or 2048. But for general graphs, degrees could be much larger.
Actually wait, for this specific problem the degrees are bounded. But for correctness on arbitrary inputs, we need to handle larger degrees.
One approach is to not use shared memory for weights at all. Instead, recompute the softmax on the fly for each feature thread. Each thread loads all edge scores, computes max, sum, and then normalized weight for each edge. This avoids shared memory entirely but is redundant work. With degree ~18, each of the 64 threads would compute softmax separately. That's 64 * 18 ≈ 1152 operations, which is fine.
But there's another problem: if each thread reads the same edge scores, they're reading from the same memory locations. The L1 cache should help here.
Actually, a better approach is:
- Have a warp (or the whole block) compute softmax once
- Store results in shared memory or have each thread keep a local copy
Since degree is small (~18), each thread can store the weights in registers! Each thread loads all edge scores into local register array, computes softmax, then uses those weights for aggregation.
With degree up to, say, 32, each thread can have 32 floats in registers. For 64 threads, that's 64 * 32 * 4 = 8KB of register usage, which should be fine.
Actually, CUDA doesn't allow variable-length arrays in registers. But we can use a fixed-size array with a reasonable max.
Or we can avoid arrays altogether and compute things in two passes:
1. First pass: all threads find max score (reduction)
2. Second pass: all threads compute sum of exp (reduction)
3. Third pass: each thread loops over edges, computes weight on the fly, and accumulates the weighted value
In step 3, each thread loads edge score `e_i` and computes `exp(e_i - max) / sum_exp` individually. This avoids storing weights.
But thread i doing this for edge j would:
- Load edge_scores[start + j]
- Load col_idx[start + j]
- Load node_value[src * feat_dim + f]
- Multiply by weight computed on the fly
The issue is that all threads access the same edge score and col_idx, but since they're in a warp, the reads will be broadcast from L1 cache (good). And each thread accesses a different feature of the same source node (coalesced).
So each thread (handling one feature) would loop over all edges. For degree=18, that's 18 iterations per thread. Each iteration:
- Read edge score (broadcast, cached)
- Read src idx (broadcast, cached)
- Read one float from node_value (coalesced)
This is actually very efficient and requires NO shared memory!
Let me write this kernel:
```cpp
__global__ void sparse_attention_csr_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_scores,
const float* __restrict__ node_value,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
int degree = end - start;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
int num_warps = (blockDim.x + 31) >> 5;
if (degree == 0) {
for (int f = tid; f < feat_dim; f += blockDim.x) {
out[dst * feat_dim + f] = 0.0f;
}
return;
}
// Step 1: find max score
float local_max = -1e20f;
for (int i = tid; i < degree; i += blockDim.x) {
local_max = fmaxf(local_max, edge_scores[start + i]);
}
local_max = warp_reduce_max(local_max);
// For multi-warp blocks, need another reduction
if (num_warps > 1) {
__shared__ float s_max[...];
if (lane == 0) s_max[warp_id] = local_max;
__syncthreads();
// ... reduce s_max
}
// No wait, let me use a simpler approach. Since degree is small,
// we can have the first warp do the softmax computation
// and broadcast the results.
}
```
Actually, with degree ~18 and block size 64 (2 warps), the first step is fine with warp reductions. But I need inter-warp reduction for max and sum.
Let me think about this more carefully. If block size is 64 = 2 warps:
- Warp 0 threads 0-31 find their local max from edges 0-17 (tid < 32 so all read at least one edge, some read none since 18 < 32)
- Warp 1 threads 32-63: some read edges (tid=32 reads edge 0, tid=33 reads edge 1, etc. up to tid=35 reading edge 3, then tid=36+ wraps around... no, for degree=18 and tid starting at 32, no edge is read since 32 > 18.
Wait, my loop is `for (int i = tid; i < degree; i += blockDim.x)`. So:
- tid=0: edges 0, 2, 4, ..., 16
- tid=1: edges 1, 3, 5, ..., 17
- ...
- tid=17: edge 17
- tid=18: none
- ...
- tid=31: none
- tid=32: none (32 >= 18)
- ...
So for degree=18 and block size=64, only the first 18 threads participate in finding local_max. Warp 0 has threads 0-17 doing work, threads 18-31 returning -inf. Warp 1 (threads 32-63) all return -inf. The warp_reduce_max handles this correctly.
Then for inter-warp reduction, I can use shared memory.
OK let me write a clean version with proper inter-warp reduction, NO shared memory for weights or values, just for the small reduction arrays.
For shared memory, I only need `num_warps` floats for max reduction and `num_warps` floats for sum reduction. That's 2 * num_warps * 4 bytes. For 256 threads (8 warps), that's 64 bytes. Very small.
```cpp
__global__ void sparse_attention_csr_kernel(...) {
int dst = blockIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
int degree = end - start;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
int num_warps = (blockDim.x + 31) >> 5;
extern __shared__ float sdata[];
float* s_max = sdata;
float* s_sum = s_max + num_warps;
// Find max
float local_max = -1e20f;
for (int i = tid; i < degree; i += blockDim.x) {
local_max = fmaxf(local_max, edge_scores[start + i]);
}
local_max = warp_reduce_max(local_max);
if (lane == 0) s_max[warp_id] = local_max;
__syncthreads();
float max_score = local_max;
if (tid < num_warps) {
max_score = s_max[tid];
} else {
max_score = -1e20f;
}
max_score = warp_reduce_max(max_score);
__shared__ float global_max; // wait, can't use __shared__ here since we already used extern
}
```
The issue is I can't mix `extern __shared__` with `__shared__` in the same kernel function. Let me just put the global max and sum in the extern array too.
```cpp
extern __shared__ float sdata[];
// Layout: [max_per_warp (num_warps), sum_per_warp (num_warps), global_max (1), global_sum (1)]
float* s_max = sdata;
float* s_sum = s_max + num_warps;
float* s_global = s_sum + num_warps;
```
After the first warp-level reduction, I store in s_max[warp_id]. After syncthreads, I read from s_max into registers for inter-warp reduction, then store the final result in s_global[0].
For sum, same process.
```cpp
// Step 1: max
float local_max = -1e20f;
for (int i = tid; i < degree; i += blockDim.x) {
local_max = fmaxf(local_max, edge_scores[start + i]);
}
local_max = warp_reduce_max(local_max);
if (lane == 0) s_max[warp_id] = local_max;
__syncthreads();
if (tid < num_warps) local_max = s_max[tid];
else local_max = -1e20f;
local_max = warp_reduce_max(local_max);
if (tid == 0) s_global[0] = local_max;
__syncthreads();
max_score = s_global[0];
// Step 2: sum of exp
float local_sum = 0.0f;
for (int i = tid; i < degree; i += blockDim.x) {
local_sum += expf(edge_scores[start + i] - max_score);
}
local_sum = warp_reduce_sum(local_sum);
if (lane == 0) s_sum[warp_id] = local_sum;
__syncthreads();
if (tid < num_warps) local_sum = s_sum[tid];
else local_sum = 0.0f;
local_sum = warp_reduce_sum(local_sum);
if (tid == 0) s_global[1] = local_sum;
__syncthreads();
float sum_exp = s_global[1];
// Step 3: aggregate
for (int f = tid; f < feat_dim; f += blockDim.x) {
float val = 0.0f;
for (int i = 0; i < degree; i++) {
int src = col_idx[start + i];
float w = expf(edge_scores[start + i] - max_score) / sum_exp;
val += node_value[src * feat_dim + f] * w;
}
out[dst * feat_dim + f] = val;
}
```
Wait, this recomputes exp for each feature thread. With feat_dim=64, degree=18, that's 64*18=1152 exp calls. Each exp is about 10-20 instructions. So ~20000 instructions. Not terrible but could be optimized.
I can optimize by having each thread compute and cache one or more weights in registers, then use them. With degree=18, each thread can store a few weights.
Actually, let me think about what fraction of time is spent in exp. For small graphs, this should be OK. But for correctness first, let me just use this approach.
Wait, there's still the problem: I need to know the row_ptr type (int32 vs int64). In the test inputs, row_ptr is int32. But to be safe, I should cast to int32 in Python before passing to the kernel, or I should use int64_t in the kernel.
Actually, looking at the test code, `row_ptr` is `torch.int32`. `col_idx` is `torch.int32`. In my kernel, I'm reading them as `const int*` which should be `int32_t`. On most systems, `int` is 32-bit, so this should match.
But if for some reason PyTorch internally represents int32 differently, there could be issues. The safest is to cast to int64 in Python or handle in the C++ wrapper.
Actually, I think the real issue from the earlier error was something else in my C++ code. Let me just rewrite the C++ wrapper more carefully.
Actually, you know what, let me check if it's possible that the earlier error was from the kernel launch shared memory computation. Maybe `shared_mem_size` became too large or something invalid. Or maybe the error was from `.item<int>()` where the scalar tensor was float because it was uninitialized or something.
Let me just completely avoid `.item<int>()` on any PyTorch tensor in C++. Instead, I'll compute everything on the Python side and pass as regular arguments, or I'll read the data from CPU.
Here's the plan for the new wrapper:
```cpp
torch::Tensor sparse_attention_csr_forward(...) {
int num_nodes = row_ptr.size(0) - 1;
int feat_dim = node_value.size(1);
auto out = torch::zeros({num_nodes, feat_dim}, node_value.options());
if (num_nodes == 0) return out;
// Ensure int32
auto row_ptr_i = row_ptr.to(torch::kInt32);
auto col_idx_i = col_idx.to(torch::kInt32);
// Copy to CPU to find max degree
auto row_ptr_cpu = row_ptr_i.cpu();
const int32_t* rp = row_ptr_cpu.data_ptr<int32_t>();
int max_degree = 0;
for (int i = 0; i < num_nodes; i++) {
max_degree = std::max(max_degree, (int)(rp[i+1] - rp[i]));
}
int block_size = 64;
if (block_size < feat_dim) block_size = 128;
if (block_size < feat_dim) block_size = 256;
int grid = num_nodes;
int num_warps = (block_size + 31) / 32;
size_t smem = (num_warps * 2 + 2) * sizeof(float);
kernel<<<grid, block_size, smem>>>(...);
return out;
}
```
Wait, but `row_ptr` might already be on GPU. Converting to int32 and then copying to CPU adds overhead. But for num_nodes=384, this is negligible.
Actually, I realize that if the original error was from the PyTorch reference implementation, not from my kernel... no, `run_correctness` ran my kernel and it failed with that error.
Hmm, let me try a simpler approach. What if the issue is that `num_nodes` is derived from `row_ptr.numel() - 1`, and `row_ptr.numel()` returns an int64, and then when I pass it to `slice(0, 0, num_nodes)` the `num_nodes` variable is C++ int which might not convert properly to int64? No, the slice parameter is int64_t, and int converts to int64_t just fine.
Let me just stop guessing and try the new version with proper type handling.
Actually, I realize: maybe I should NOT do `.to(torch::kInt32)` if the tensor is already int32. That might cause unnecessary copy. But for correctness, let me just do it.
Let me write the complete kernel now. And I think I want to use the approach where I recompute exp on the fly rather than caching weights, so I don't need max_degree for shared memory. This eliminates one source of potential issues.
But wait, if I recompute exp for each feature, the block size should be at least feat_dim=64 for good occupancy. I'll use block_size = 64.
For the shared memory, I only need space for warp reductions: `2 * num_warps + 2` floats. For 64 threads (2 warps), that's 6 floats ≈ 24 bytes. For 128 threads (4 warps), that's 10 floats ≈ 40 bytes. Total shared memory is tiny.
Let me write this now. I'll still compute max_degree to be safe for potential future variants that might need it, but the current kernel doesn't need it.
Actually, I realize I can also use the approach of caching weights in shared memory if I compute max_degree. Let me stick with the recompute-on-the-fly approach since it doesn't need max_degree and is simpler.
Hold on - let me reconsider whether the "recompute exp" approach is too slow. For each of the 384 destination nodes:
- 64 threads per block
- Each thread handles 1 feature (out of 64)
- Each thread iterates over ~18 edges
- For each edge, it loads edge score, computes exp, loads col_idx, loads node_value, multiplies, accumulates
- The exp of the edge score is computed 64 times (once per feature thread)
With 64 threads computing the same exp 64 times = 1152 exp() calls per destination node. 384 nodes = 442K exp() calls. Each exp() is maybe ~20 FLOPs, so ~9M FLOPs. The whole computation is memory bound anyway, so the extra compute might not matter.
But actually, if I use shared memory for weights, I can compute exp once per edge and reuse it. That's only 18 exp() calls per node = 7K total. Much better.
Let me stick with shared memory for weights. I just need to compute max_degree properly.
For max_degree, I can read it directly from row_ptr on the GPU without CPU copy if I just launch a small kernel or use torch operations. But the simplest is a CPU copy.
OK, I'll write the kernel with weights in shared memory, and compute max_degree via CPU copy. Let me also be careful about types.
Actually, for the specific test inputs, the maximum degree is 24 (18 + 6). Let me just set it to something safe. But I need general correctness.
Here's my updated approach:
```python
# Compute max_degree on Python side
row_ptr_cpu = row_ptr.cpu().numpy()
max_degree = int((row_ptr_cpu[1:] - row_ptr_cpu[:-1]).max())
```
Then pass max_degree to the kernel.
But in load_inline, I can't easily pass Python values to the C++ function unless they're function arguments. So I can compute it in C++ by doing a CPU copy there.
OR, I can make the Python wrapper class compute it and then call a different function, but load_inline only exposes one function. Unless I write a Python wrapper around it.
Wait, I can compute max_degree in Python and then just not use it in the kernel! I can over-allocate shared memory for the weights. For this specific problem, max_degree is small.
For safety, I can check max_degree in Python and fall back to a larger allocation or a different kernel. But the simplest is to compute in C++.
Let me just do the CPU copy in C++. For num_nodes=384, it's copying 385 ints = 1.5KB from GPU to CPU. This takes microseconds.
Actually, I could even avoid shared memory by having each block load weights into a small static shared memory array with a reasonable upper bound. For example, if I know max_degree can't exceed 1024 for typical cases:
```cpp
__shared__ float s_weights[1024];
```
This avoids the dynamic shared memory size computation. But 1024 floats = 4KB, and with 384 blocks that might use too much shared memory if they're all resident. But actually, shared memory is per-SM, and blocks don't all run simultaneously.
If the actual degree exceeds 1024, we'd have a bug. But for this problem, degree is about 24. For general correctness, I should handle arbitrary degrees.
Let me just do dynamic shared memory with max_degree computed from CPU copy.
Here's the complete solution I'll try:
```cpp
// CUDA kernel
__global__ void sparse_attention_csr_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_scores,
const float* __restrict__ node_value,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
// ... as before with shared memory for weights ...
}
// C++ wrapper
torch::Tensor sparse_attention_csr_forward(
torch::Tensor row_ptr,
torch::Tensor col_idx,
torch::Tensor edge_scores,
torch::Tensor node_value
) {
int num_nodes = row_ptr.size(0) - 1;
int feat_dim = node_value.size(1);
auto out = torch::zeros({num_nodes, feat_dim}, node_value.options());
if (num_nodes == 0) return out;
auto row_ptr_cpu = row_ptr.to(torch::kCPU).to(torch::kInt32);
const int32_t* rp = row_ptr_cpu.data_ptr<int32_t>();
int max_degree = 0;
for (int i = 0; i < num_nodes; i++) {
int deg = (int)(rp[i+1] - rp[i]);
if (deg > max_degree) max_degree = deg;
}
if (max_degree == 0) return out;
auto row_ptr_gpu = row_ptr.to(torch::kInt32).to(row_ptr.device());
auto col_idx_gpu = col_idx.to(torch::kInt32).to(col_idx.device());
int block_size = 64;
if (block_size < feat_dim) block_size = 128;
if (block_size < feat_dim) block_size = 256;
int num_warps = (block_size + 31) / 32;
size_t smem = (max_degree + num_warps + max_degree * feat_dim) * sizeof(float);
launch_kernel<<<num_nodes, block_size, smem>>>(
row_ptr_gpu.data_ptr<int32_t>(),
col_idx_gpu.data_ptr<int32_t>(),
edge_scores.data_ptr<float>(),
node_value.data_ptr<float>(),
out.data_ptr<float>(),
num_nodes, feat_dim
);
return out;
}
```
Hmm, this does extra copies: row_ptr to CPU, then back to GPU as int32. For 384 elements, this is trivial.
But wait, `to(torch::kCPU)` copies to CPU. Then `to(torch::kInt32)` converts type. Then `row_ptr.to(torch::kInt32).to(row_ptr.device())` converts back to GPU. Two extra copies for 384 ints = ~3KB total. Completely negligible.
Actually, I need to be more careful. `row_ptr.to(torch::kCPU)` is a blocking operation, which means it synchronizes the GPU. But again, for a tiny tensor, this is fine and happens once per forward pass.
Let me try this approach.
Wait, I also need to be careful about `row_ptr.size(0) - 1`. In C++ PyTorch, `size(0)` returns int64_t. So `num_nodes` would be int64_t. When passed as `int num_nodes` to the kernel, if num_nodes > 2^31, it truncates. But for our case, it's 384.
OK, let me write the complete code and try compiling + correctness.
Actually, I realize there might be a simpler interpretation of the error. What if the error `expected scalar type Int but found Float` is coming from the reference implementation, not my kernel?
No, wait. `run_correctness` runs both the reference and my kernel. If the reference failed, it would say so. The error message just says "trials did not all match" and gives the runtime_error. But maybe the runtime_error is from MY kernel, not from PyTorch operations.
If my kernel had a bug that caused a CUDA error, it might manifest in a strange way. Or if there was an illegal memory access, it might crash.
Actually, looking more carefully, the error says "runtime_error: expected scalar type Int but found Float". This is clearly a PyTorch exception, not a CUDA error. CUDA errors typically say "CUDA error: ...".
And if it's a PyTorch exception happening during my kernel launch, where could it come from?
- From the C++ wrapper code that runs before kernel launch
- From the C++ wrapper code that runs after kernel launch
The most likely culprits are:
1. `degrees.max().item<int>()` where degrees is float
2. `row_ptr.data_ptr<int>()` where row_ptr is float or int64
If `row_ptr` were float, calling `data_ptr<int>()` wouldn't compile because it's a template that the compiler resolves at compile time based on the tensor's dtype... Actually no, `data_ptr<int>()` is just a cast. It doesn't check at compile time what the actual dtype is. At runtime, PyTorch checks that the tensor's dtype matches what you requested. So `data_ptr<int>()` on a float tensor would throw `expected scalar type Int but found Float`!
That's probably it! My kernel was compiled with `row_ptr.data_ptr<int>()`, but maybe somehow row_ptr was float? Or maybe I was calling `data_ptr<int>()` on a tensor that wasn't int32.
Wait, `get_inputs()` says `row_ptr = torch.zeros(num_nodes + 1, dtype=torch.int32)`. So it's definitely int32.
Unless... the issue is with `torch.cumsum(degree, dim=0)` where `degree` is int32. Does `cumsum` preserve int32? Let me think... Yes, `torch.cumsum` preserves the input dtype.
Unless there's some issue with the assignment `row_ptr[1:] = torch.cumsum(degree, dim=0)`. This assigns an int32 tensor to an int32 tensor slice. Should be fine.
But wait, what about the degrees computation? Let me trace the reference:
```python
degree = torch.full((num_nodes,), avg_degree, dtype=torch.int32)
degree = torch.clamp(degree + ((torch.arange(num_nodes, dtype=torch.int32) % 7) - 3), min=1)
row_ptr = torch.zeros(num_nodes + 1, dtype=torch.int32)
row_ptr[1:] = torch.cumsum(degree, dim=0)
```
All int32. Then in C++, if I do `row_ptr.data_ptr<int>()`, this should work because int32 matches `int` on the platform.
Hmm, but actually `int32_t` and `int` might be different types in C++ even if they're the same size. PyTorch might use `int32_t` internally. Let me check... PyTorch uses scalar types like `at::kInt`. The `data_ptr<int>()` function works with `int` if `sizeof(int) == 4`. On most platforms, `int` is 32-bit. So this should be fine.
What if the issue is not with row_ptr but with `col_idx`? Same thing - it's int32.
OK let me try my new approach where I explicitly convert to int32 using `.to(torch::kInt32)` before calling data_ptr. This ensures correctness.
Actually, I should also check if the error could be from my Python-level `_ext.sparse_attention_csr_forward()` call somehow having an issue with argument passing. That's unlikely since load_inline handles this.
Alright, let me compile and test the new version.
Oh wait, I should also consider whether the error is from the previous correctness run, not my kernel itself. What if there's a memory corruption from the previous kernel launch that causes the subsequent trial to fail? Unlikely but possible.
Let me try a completely fresh implementation.
One more thing: I notice I used `node_value.options()` for creating `out`. If `node_value` is `torch.float32`, then `out` is `torch.float32`. Good.
But what if `node_value` is on CPU? Then `out` would be on CPU, and the kernel would write to CPU memory. That would cause issues. But the inputs are supposed to be on the same device. The test's `get_inputs()` doesn't specify device, so the tensors are on CPU by default!
Wait, let me check the test harness... If the test creates inputs and then moves them to CUDA before calling the model, then they'd be on CUDA. Let me assume the test handles device placement correctly.
Actually, let me look at the reference implementation: it doesn't do any explicit device handling. PyTorch operations automatically work on whatever device the tensors are on. My kernel needs to handle this too.
In the C++ wrapper, if `out` is created with `node_value.options()`, it uses the same device and dtype as `node_value`. So if `node_value` is on CPU, `out` is on CPU, and trying to launch a CUDA kernel on CPU tensors would fail with a different error.
Let me assume the test places tensors on GPU.
OK, let me write the kernel.
Actually, I realize I want to avoid potential issues with shared memory calculation. What if I just set a fixed shared memory size that's large enough? The theoretical max for degree could be num_nodes = 384. So weights could need 384 floats = 1.5KB. Plus num_warps (say 8) * 2 for reduction = 64 bytes. Plus values: 384 * 64 = 24576 floats = 98KB. This exceeds the 48KB default shared memory limit.
So I can't just over-allocate for all cases. I NEED max_degree to size the shared memory properly, OR I need to not load values into shared memory.
The approach of reading values from global memory on the fly is safer because it only needs shared memory for weights and reduction: `max_degree + num_warps * 2 + 2` floats. For max_degree = 384, that's 384 + 18 = 402 floats = 1.6KB, which is well within limits.
And for the actual problem with max_degree ~24, it's ~24 + 18 = 42 floats = 168 bytes.
Great, let me go with the on-the-fly global memory reads for values, and shared memory only for weights + reduction.
Wait, I can even avoid shared memory for weights if I recompute exp. But to save compute, I'll use shared memory for weights.
Here's the plan:
1. Compute max_degree by CPU copy of row_ptr
2. Launch kernel with `smem = (max_degree + num_warps * 2 + 2) * sizeof(float)`
3. Kernel: shared memory layout: [weights(max_degree), warp_max(num_warps), warp_sum(num_warps), global_max(1), global_sum(1)]
4. Compute softmax and store weights in shared memory
5. Each thread aggregates features by reading from global memory using the precomputed weights
This is memory-efficient and compute-efficient.
Let me write it out:
Actually wait, there's a subtle issue. The shared memory has to be allocated per block based on max_degree. But max_degree is the maximum over all rows. For a block handling a specific row with degree < max_degree, it only uses the first `degree` entries of the weights array. That's fine.
But there's another issue: if max_degree is very large, the shared memory might exceed the block limit. On H100 it's 227KB. For max_degree=384, shared memory for weights is 1.5KB. Total ~1.6KB. Fine.
Actually, I want to think about whether I even need to cache weights in shared memory. With block_size=64 and degree~18, I could have each warp compute the weights (since degree < warp_size) and store them in shared memory. Then all threads can read them. This works.
Or, I can compute weights in shared memory as planned.
OK let me write the code.
For the C++ code, I'll:
1. Get num_nodes and feat_dim
2. Create output
3. Copy row_ptr to CPU to compute max_degree
4. Convert inputs to proper device/dtype
5. Launch kernel
One concern: `row_ptr.to(torch::kCPU)` synchronizes the GPU. But it's tiny.
Alternatively, I can compute max_degree in a lightweight CUDA kernel. But CPU copy is simpler.
Let me just write the full implementation now.
```cpp
__forceinline__ __device__ float warp_reduce_max(float val) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1)
val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
return val;
}
__forceinline__ __device__ float warp_reduce_sum(float val) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1)
val += __shfl_down_sync(0xFFFFFFFF, val, offset);
return val;
}
__global__ void sparse_attention_csr_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_scores,
const float* __restrict__ node_value,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
int degree = end - start;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
int num_warps = (blockDim.x + 31) >> 5;
extern __shared__ float sdata[];
float* s_weights = sdata;
float* s_warp_max = s_weights + degree;
float* s_warp_sum = s_warp_max + num_warps;
float* s_global = s_warp_sum + num_warps;
// Compute max score
float local_max = -1e20f;
for (int i = tid; i < degree; i += blockDim.x) {
float score = edge_scores[start + i];
s_weights[i] = score;
local_max = fmaxf(local_max, score);
}
local_max = warp_reduce_max(local_max);
if (lane == 0) s_warp_max[warp_id] = local_max;
__syncthreads();
if (tid < num_warps) local_max = s_warp_max[tid];
else local_max = -1e20f;
local_max = warp_reduce_max(local_max);
if (tid == 0) s_global[0] = local_max;
__syncthreads();
float max_score = s_global[0];
// Compute sum of exp
float local_sum = 0.0f;
for (int i = tid; i < degree; i += blockDim.x) {
float w = expf(s_weights[i] - max_score);
s_weights[i] = w;
local_sum += w;
}
local_sum = warp_reduce_sum(local_sum);
if (lane == 0) s_warp_sum[warp_id] = local_sum;
__syncthreads();
if (tid < num_warps) local_sum = s_warp_sum[tid];
else local_sum = 0.0f;
local_sum = warp_reduce_sum(local_sum);
if (tid == 0) s_global[1] = local_sum;
__syncthreads();
float sum_exp = s_global[1];
// Normalize weights
for (int i = tid; i < degree; i += blockDim.x) {
s_weights[i] /= sum_exp;
}
__syncthreads();
// Aggregate
for (int f = tid; f < feat_dim; f += blockDim.x) {
float val = 0.0f;
for (int e = 0; e < degree; e++) {
int src = col_idx[start + e];
val += node_value[src * feat_dim + f] * s_weights[e];
}
out[dst * feat_dim + f] = val;
}
}
```
Wait, for the shared memory layout, I said `sdata` has size `max_degree + num_warps * 2 + 2`. But in the kernel, I use `s_weights + degree` where `degree` is the actual degree of this row, not `max_degree`. But the shared memory was allocated with `max_degree`. So `s_warp_max = s_weights + degree` is correct because we only use `degree` entries for weights, regardless of how much was allocated.
Actually no, `s_weights + degree` could overlap with what other threads might expect if degree varies. But wait, shared memory is per-block, and each block handles one row. So the degree is the same for all threads in a block. And the shared memory is allocated per-block. The layout `s_weights + degree` is fine.
But for correctness, I should use the actual allocated size. The warp arrays start after the weights array. If max_degree > degree, then `s_weights + degree` is within the allocated region. Good.
Wait, actually, if I allocated `max_degree + num_warps * 2 + 2` floats, but then for a specific block with degree < max_degree, I access `s_warp_max[warp_id]` where `s_warp_max = s_weights + degree`. The memory at that location was allocated. Good.
OK, this should work. Let me also think about potential bank conflicts.
When threads read `s_weights[e]` in the aggregation loop, all threads in a warp read the same weight `s_weights[e]` at the same time. This is a broadcast, which is handled efficiently by shared memory (no bank conflicts, since all threads read the same address).
When threads read `node_value[src * feat_dim + f]`, each thread reads a different feature `f` of the same source node (since threads are indexed by `f`). With feat_dim=64, threads 0-31 read features 0-31 of the same source node, threads 32-63 read features 32-63. This is perfectly coalesced if the node values are stored in row-major order (which they are in PyTorch). Good!
When threads read `edge_scores[start + i]` in the first loop, with degree=18 and block_size=64, multiple threads read the same score. This is cached and broadcast. Good.
When threads read `col_idx[start + e]`, all threads read the same value. Broadcast from global memory (cached in L1). Good.
This kernel should be quite efficient.
One small optimization: I can process multiple features per thread to reduce the loop overhead. For example, if block_size = 64 and feat_dim = 64, each thread handles exactly 1 feature. If block_size = 32, each thread handles 2 features. I'll choose block_size = 64 for this case.
But what if feat_dim is larger, say 128 or 256? With block_size=64, each thread handles 2 or 4 features. That's fine.
What about block_size < feat_dim? The loop `for (int f = tid; f < feat_dim; f += blockDim.x)` handles this.
I need to choose a reasonable block_size. For small feat_dim (64), block_size=64 gives each thread 1 feature. For larger feat_dim, block_size=128 or 256 might be better to have more parallelism.
But for degree=18 and feat_dim=64, memory bandwidth is the bottleneck. With 64 threads per block and 384 blocks, we have 384*64 = 24576 threads, which can saturate the H100's memory bandwidth.
Let me choose block_size based on feat_dim:
```cpp
int block_size = 64;
if (feat_dim > 64) block_size = 128;
if (feat_dim > 128) block_size = 256;
```
This is a simple heuristic.
One more thing: I need to make sure `num_nodes` in the kernel launch doesn't exceed the grid size limit, but 384 is tiny.
Let me also check warp synchronization. I use `__syncthreads()` after writing to shared memory and before reading from it. I also use `__shfl_down_sync` with `0xFFFFFFFF` (all threads in warp participate). This should be correct.
For the case where `degree == 0`, the kernel skips all loops and writes zeros:
```cpp
if (degree == 0) {
for (int f = tid; f < feat_dim; f += blockDim.x) {
out[dst * feat_dim + f] = 0.0f;
}
return;
}
```
Actually, I should add this to handle empty rows cleanly. In the reference code, `if (end > start)` means empty rows contribute nothing (out is already zero). But since `out` is zero-initialized, I don't strictly need this. However, adding it avoids unnecessary computation and potential issues with empty loops.
Wait, in the reference, `out` starts as zeros and only gets updated if `end > start`. So my kernel should produce the same result even without the `degree == 0` early return, since all the loops just won't execute. But adding it is cleaner.
One concern: when degree is 0, the warp_reduce_max on -1e20f... let me see, local_max starts at -1e20f for all threads, then warp_reduce_max returns -1e20f. Then exp(-1e20f - (-1e20f)) = exp(0) = 1.0f. But if degree is 0, the exp loop doesn't execute, so local_sum = 0.0f. Then we divide by 0... but the aggregation loop also doesn't execute, so no issue. However, it's safer to handle degree==0 explicitly.
Let me add the early return.
One more thing: I notice in the reference implementation, `col_idx[start:end].long()` converts to long (int64). In my kernel, I'm using `int` (int32) for col_idx. The actual values are indices from 0 to num_nodes-1 = 383, which fits in int32. So this should be fine.
But let me make sure: if `col_idx` is stored as int32 on the GPU, reading it as `const int*` is correct.
OK let me write the code and test.
Wait, I also want to think about whether I can optimize further using vectorized loads. For the node_value reads, each thread reads one feature at a time. With 64 threads, that's 64 floats = 256 bytes per warp. This is perfectly coalesced. I could use float4 to load 4 features at a time, reducing the number of memory instructions by 4x. But this requires:
- feat_dim to be a multiple of 4 (64 is)
- Unrolling the inner loop
Let me not do this initially. Let me get correctness first, then optimize.
```python
import torch
from torch.utils.cpp_extension import load_inline
cuda_source = r"""
#include <torch/extension.h>
#include <cuda_runtime.h>
__forceinline__ __device__ float warp_reduce_max(float val) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1)
val = fmaxf(val, __shfl_down_sync(0xFFFFFFFF, val, offset));
return val;
}
__forceinline__ __device__ float warp_reduce_sum(float val) {
#pragma unroll
for (int offset = 16; offset > 0; offset >>= 1)
val += __shfl_down_sync(0xFFFFFFFF, val, offset);
return val;
}
__global__ void sparse_attention_csr_kernel(
const int* __restrict__ row_ptr,
const int* __restrict__ col_idx,
const float* __restrict__ edge_scores,
const float* __restrict__ node_value,
float* __restrict__ out,
int num_nodes,
int feat_dim
) {
int dst = blockIdx.x;
if (dst >= num_nodes) return;
int start = row_ptr[dst];
int end = row_ptr[dst + 1];
int degree = end - start;
int tid = threadIdx.x;
int lane = tid & 31;
int warp_id = tid >> 5;
int num_warps = (blockDim.x + 31) >> 5;
if (degree == 0) {
for (int f = tid; f < feat_dim; f += blockDim.x) {
out[dst * feat_dim + f] = 0.0f;
}
return;
}
extern __shared__ float sdata[];
float* s_weights = sdata;
float* s_warp_max = s_weights + degree;
float* s_warp_sum = s_warp_max + num_warps;
float* s_global = s_warp_sum + num_warps;
float local_max = -1e20f;
for (int i = tid; i < degree; i += blockDim.x) {
float score = edge_scores[start + i];
s_weights[i] = score;
local_max = fmaxf(local_max, score);
}
local_max = warp_reduce_max(local_max);
if (lane == 0) s_warp_max[warp_id] = local_max;
__syncthreads();
if (tid < num_warps) local_max = s_warp_max[tid];
else local_max = -1e20f;
local_max = warp_reduce_max(local_max);
if (tid == 0) s_global[0] = local_max;
__syncthreads();
float max_score = s_global[0];
float local_sum = 0.0f;
for (int i = tid; i < degree; i += blockDim.x) {
float w = expf(s_weights[i] - max_score);
s_weights[i] = w;
local_sum += w;
}
local_sum = warp_reduce_sum(local_sum);
if (lane == 0) s_warp_sum[warp_id] = local_sum;
__syncthreads();
if (tid < num_warps) local_sum = s_warp_sum[tid];
else local_sum = 0.0f;
local_sum = warp_reduce_sum(local_sum);
if (tid == 0) s_global[1] = local_sum;
__syncthreads();
float sum_exp = s_global[1];
for (int i = tid; i < degree; i += blockDim.x) {
s_weights[i] /= sum_exp;
}
__syncthreads();
for (int f = tid; f < feat_dim; f += blockDim.x) {
float val = 0.0f;
for (int e = 0; e < degree; e++) {
int src = col_idx[start + e];
val += node_value[src * feat_dim + f] * s_weights[e];
}
out[dst * feat_dim + f] = val;
}
}
"""
cpp_source = r"""
#include <torch/extension.h>
#include <algorithm>
void launch_sparse_attention_csr_kernel(
const int* row_ptr,
const int* col_idx,
const float* edge_scores,
const float* node_value,
float* out,
int num_nodes,
int feat_dim,
int max_degree,
int block_size
);
torch::Tensor sparse_attention_csr_forward(
torch::Tensor row_ptr,
torch::Tensor col_idx,
torch::Tensor edge_scores,
torch::Tensor node_value
) {
int num_nodes = row_ptr.size(0) - 1;
int feat_dim = node_value.size(1);
auto out = torch::zeros({num_nodes, feat_dim}, node_value.options());
if (num_nodes == 0) return out;
auto row_ptr_cpu = row_ptr.to(torch::kCPU).to(torch::kInt32);
const int32_t* rp = row_ptr_cpu.data_ptr<int32_t>();
int max_degree = 0;
for (int i = 0; i < num_nodes; i++) {
int deg = static_cast<int>(rp[i+1] - rp[i]);
if (deg > max_degree) max_degree = deg;
}
if (max_degree == 0) return out;
auto row_ptr_gpu = row_ptr.to(torch::kInt32).to(row_ptr.device());
auto col_idx_gpu = col_idx.to(torch::kInt32).to(col_idx.device());
int block_size = 64;
if (block_size < feat_dim) block_size = 128;
if (block_size < feat_dim) block_size = 256;
if (block_size > 512) block_size = 512;
int num_warps = (block_size + 31) / 32;
size_t smem = (max_degree + num_warps * 2 + 2) * sizeof(float);
launch_sparse_attention_csr_kernel(
row_ptr_gpu.data_ptr<int32_t>(),
col_idx_gpu.data_ptr<int32_t>(),
edge_scores.data_ptr<float>(),
node_value.data_ptr<float>(),
out.data_ptr<float>(),
num_nodes,
feat_dim,
max_degree,
block_size
);
return out;
}
"""
Wait, I need to define launch_sparse_attention_csr_kernel in the CUDA source.
```
I will include the launch helper in the CUDA source and call it from the C++ wrapper. Now I'll write the full Python code and test.
Actually, I realize I should question whether I need `to(torch::kInt32)` conversions. If row_ptr and col_idx are already int32, the conversion is a no-op. If they're int64, the conversion copies and narrows. This is good for robustness.
But wait, `row_ptr_gpu.data_ptr<int32_t>()` - will PyTorch's data_ptr check the scalar type? Yes, it does a runtime check. If row_ptr_gpu is int32, it works. Good.
What about the original issue: `expected scalar type Int but found Float`. I was calling `.item<int>()` on a float tensor before. Now I removed that entirely, so the issue should be gone.
One more thing: `node_value.size(1)` returns int64_t in PyTorch C++ API. When passed as `int feat_dim` in the launch function, if feat_dim is very large, it could overflow. But for our case, feat_dim=64.
OK, let me write the actual Python module code and test it.
I also need to make sure `load_inline` has the right function names in the `functions` list.
Let me double-check my CUDA source includes the properly defined `launch_sparse_attention_csr_kernel`. In my previous successful compile, I had the kernel and launch function in the CUDA source, and the C++ wrapper called the launch function.
Let me write the complete code now.