FW-GLM-5-1 · popcorn · pop_l123_st_gpt
compile_fail 37_CSRFusedAttentionValue.py
model
FW-GLM-5-1
turns / max
1 / 1
tool calls
1
tokens (total)
11391
wall clock
1423.3s
truncated
yes
backend
cuda fp32
variant
popcorn
runtime
—
ref runtime
-1.00 μs
speedup
—
started
2026-05-11T09:35:33Z
compile error: Error building extension 'sparse_attention_ext': [1/3] /opt/conda/bin/x86_64-conda-linux-gnu-c++ -MMD -MF main.o.d -DTORCH_EXTENSION_NAME=sparse_attention_ext -DTORCH_API_INCLUDE_EXTENSION_H -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include/torch/csrc/api/include -isystem /usr/local/cuda/include -isystem /opt/conda/include/python3.10 -fPIC -std=c++17 -c /scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_37_cache/k_2ceabf90c67c/sparse_attenti
timingkernelbench.timing
kernel runtime
—
ref runtime
—
speedup
—
kernel mean / median / std
—
ref mean / median / std
—
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn


class Model(nn.Module):
    """
    Fused sparse attention-value aggregation in CSR format.

    For each destination row, compute a softmax over edge logits and then use
    the normalized weights to aggregate source node values. This is a common
    fused graph-attention pattern.
    """

    def __init__(self):
        super().__init__()

    def forward(
        self,
        row_ptr: torch.Tensor,
        col_idx: torch.Tensor,
        edge_scores: torch.Tensor,
        node_value: torch.Tensor,
    ) -> torch.Tensor:
        num_nodes = row_ptr.numel() - 1
        feat_dim = node_value.shape[1]
        out = torch.zeros(num_nodes, feat_dim, dtype=node_value.dtype, device=node_value.device)

        for dst in range(num_nodes):
            start = int(row_ptr[dst].item())
            end = int(row_ptr[dst + 1].item())
            if end > start:
                weights = torch.softmax(edge_scores[start:end], dim=0)
                src = col_idx[start:end].long()
                out[dst] = (node_value[src] * weights.unsqueeze(-1)).sum(dim=0)

        return out


num_nodes = 384
avg_degree = 18
feat_dim = 64


def get_inputs():
    return list(
        popcorn_pri.make_csr_fused_attention_value_graph(
            num_nodes, avg_degree, feat_dim
        )
    )



def get_init_inputs():
    return []
turn 0 · FINAL · 1 tool call(s) · 1423.3s turn · 99.8s LLM · tokens in=1,262 · out=10,129 · total=11,391 · 23,258 chars (click to expand)
turn 01423.3s turn · 99.8s LLM · 1323.4s tools · tokens in=1,262 · out=10,129 · total=11,391 · FINAL · reasoning clipped
reasoning
reasoning (truncated 21059 chars): Let me analyze this task: 1. The model computes a fused sparse attention-value aggregation in CSR format 2. For each destination node, it: - Gets the edge scores for edges coming into that node - Computes softmax over those edge scores - Uses the softmax weights to aggregate source node values 3. The inputs are: - `row_ptr`: CSR row pointers (num_nodes + 1 elements) - `col_idx`: Column indices (source nodes for each edge) - `edge_scores`: Score for each edge - `node_value`: Value for each node (num_nodes x feat_dim) 4. Output: aggregated values for each destination node (num_nodes x feat_dim) The reference implementation is a sequential Python loop. I need to write a CUDA kernel that: 1. Parallelizes over destination nodes 2. Computes softmax for each node's edges 3. Aggregates source node values using the softmax weights Key challenges: - Different nodes have different degrees (number of edges) - Need to compute softmax which requires finding max and sum - Need to handle the aggregation across feature dimensions Let me design the kernel: Option 1: One thread per destination node - Each thread handles all edges for one destination - Compute softmax by iterating through edges twice (once for max, once for sum/exp) - Then aggregate values - This could work but might be slow for high-degree nodes Option 2: One warp/block per destination node - Parallel reduction within a warp/block for softmax - More complex but could be faster for high-degree nodes Given avg_degree = 18, Option 1 should work well since each thread handles ~18 edges on average. Let me write the kernel: ```cpp __global__ void sparse_attention_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 * blockDim.x + threadIdx.x; if (dst >= num_nodes) return; int start = row_ptr[dst]; int end = row_ptr[dst + 1]; if (end == start) { // No edges, output remains zero return; } // Find max for numerical stability float max_score = edge_scores[start]; for (int i = start + 1; i < end; i++) { max_score = fmaxf(max_score, edge_scores[i]); } // Compute sum of exp float sum_exp = 0.0f; for (int i = start; i < end; i++) { sum_exp += expf(edge_scores[i] - max_score); } // Aggregate with softmax weights for (int f = 0; f < feat_dim; f++) { float val = 0.0f; for (int i = start; i < end; i++) { float weight = expf(edge_scores[i] - max_score) / sum_exp; int src = col_idx[i]; val += node_value[src * feat_dim + f] * weight; } out[dst * feat_dim + f] = val; } } ``` Wait, I'm computing exp twice for each edge - once for sum_exp and once for the weight. Let me optimize by precomputing or using shared memory. Actually, for avg_degree = 18, precomputing in registers or a small local array might be better. Let me think about this more carefully. The inner loop over features iterates feat_dim times, and for each feature, we iterate over edges. This means we're computing the same softmax weights feat_dim times. Better approach: compute softmax weights once and store them in local memory (registers if small enough), then use them for aggregation. ```cpp __global__ void sparse_attention_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 * blockDim.x + threadIdx.x; if (dst >= num_nodes) return; int start = row_ptr[dst]; int end = row_ptr[dst + 1]; int degree = end - start; if (degree == 0) { return; } // Local arrays for weights and source indices // Max degree could be high, but we need to handle this // For safety, let's use a loop-based approach // Find max for numerical stability float max_score = edge_scores[start]; for (int i = start + 1; i < end; i++) { max_score = fmaxf(max_score, edge_scores[i]); } // Compute softmax weights and sum float sum_exp = 0.0f; // Store weights in local memory - but degree is variable // We'll do two passes: first compute sum, then aggregate // First pass: compute sum for (int i = start; i < end; i++) { sum_exp += expf(edge_scores[i] - max_score); } // Second pass: aggregate for (int f = 0; f < feat_dim; f++) { float val = 0.0f; for (int i = start; i < end; i++) { float weight = expf(edge_scores[i] - max_score) / sum_exp; int src = col_idx[i]; val += node_value[src * feat_dim + f] * weight; } out[dst * feat_dim + f] = val; } } ``` This still computes exp twice. Let me try a different approach where I process the aggregation differently - maybe iterating over edges in the outer loop and features in the inner loop. Actually, I realize that for each edge, I need to add its contribution to the output. The contribution is weight * node_value[src]. So: ```cpp // First compute softmax weights float inv_sum_exp = 1.0f / sum_exp; // Initialize output for (int f = 0; f < feat_dim; f++) { out[dst * feat_dim + f] = 0.0f; } // Aggregate for (int i = start; i < end; i++) { float weight = expf(edge_scores[i] - max_score) * inv_sum_exp; int src = col_idx[i]; for (int f = 0; f < feat_dim; f++) { out[dst * feat_dim + f] += node_value[src * feat_dim + f] * weight; } } ``` This is better! Now exp is computed once per edge. But we're accessing node_value in a potentially uncoalesced way because src is random. Let me think about memory access patterns: - edge_scores[start:end] - contiguous, good - col_idx[start:end] - contiguous, good - node_value[src * feat_dim + f] for varying src - random access, unavoidable - out[dst * feat_dim + f] - contiguous for fixed dst, good The random access to node_value is inherent to sparse graph operations. Let me also consider using warp-level primitives or shared memory for reduction within a block. But given avg_degree = 18 and feat_dim = 64, and 384 nodes, the simple one-thread-per-node approach should work. Actually, let me think about the parallelism more. We have 384 nodes. With 256 threads per block, we need 2 blocks. Each thread handles one node. This should give good occupancy. Let me also consider if we can use vectorized loads for node_value. Since feat_dim = 64, we could potentially load 4 floats at a time using float4. Let me refine the kernel: ```cpp __global__ void sparse_attention_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 * blockDim.x + threadIdx.x; if (dst >= num_nodes) return; int start = row_ptr[dst]; int end = row_ptr[dst + 1]; int degree = end - start; if (degree == 0) { // Zero initialize output (though it should already be zero) for (int f = 0; f < feat_dim; f++) { out[dst * feat_dim + f] = 0.0f; } return; } // Find max for numerical stability float max_score = edge_scores[start]; for (int i = start + 1; i < end; i++) { max_score = fmaxf(max_score, edge_scores[i]); } // Compute sum of exp float sum_exp = 0.0f; for (int i = start; i < end; i++) { sum_exp += expf(edge_scores[i] - max_score); } float inv_sum_exp = 1.0f / sum_exp; // Initialize output to zero for (int f = 0; f < feat_dim; f++) { ...[ omitted 21059 characters ]... ghts) Total: degree expf calls. So the local array approach saves degree expf calls, which for avg_degree=18 and 384 nodes is 6912 expf calls saved. Is this significant? Let me think... expf is a transcendental function that takes ~32 cycles on modern GPUs. 6912 * 32 = 221184 cycles saved. At 1GHz clock, that's 0.22ms saved. Given the total runtime is likely in the tens of microseconds, this could be significant. However, the local array approach might cause register spilling or increased register pressure, which could reduce occupancy. Let me try both approaches and see which is faster. For the final submission, I'll go with the simpler two-pass approach first, then try the local array if needed. Actually, let me just use the two-pass approach for now. It's simpler and less likely to cause register spilling issues. The performance difference might not be huge. Let me finalize the code: ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = """ __global__ void sparse_attention_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 * blockDim.x + threadIdx.x; if (dst >= num_nodes) return; int start = row_ptr[dst]; int end = row_ptr[dst + 1]; if (start == end) { return; } // Find max for numerical stability float max_score = edge_scores[start]; for (int i = start + 1; i < end; i++) { max_score = fmaxf(max_score, edge_scores[i]); } // Compute sum of exp float sum_exp = 0.0f; for (int i = start; i < end; i++) { sum_exp += expf(edge_scores[i] - max_score); } float inv_sum_exp = 1.0f / sum_exp; // Get pointer to output row float* dst_out = out + dst * feat_dim; // Initialize output to zero for (int f = 0; f < feat_dim; f++) { dst_out[f] = 0.0f; } // Aggregate for (int i = start; i < end; i++) { float weight = expf(edge_scores[i] - max_score) * inv_sum_exp; int src = col_idx[i]; const float* src_val = node_value + src * feat_dim; for (int f = 0; f < feat_dim; f++) { dst_out[f] += src_val[f] * weight; } } } torch::Tensor sparse_attention_cuda( torch::Tensor row_ptr, torch::Tensor col_idx, torch::Tensor edge_scores, torch::Tensor node_value ) { int num_nodes = row_ptr.numel() - 1; int feat_dim = node_value.size(1); auto out = torch::zeros({num_nodes, feat_dim}, node_value.options()); int threads = 256; int blocks = (num_nodes + threads - 1) / threads; sparse_attention_kernel<<<blocks, threads>>>( row_ptr.data_ptr<int>(), col_idx.data_ptr<int>(), edge_scores.data_ptr<float>(), node_value.data_ptr<float>(), out.data_ptr<float>(), num_nodes, feat_dim ); return out; } """ module = load_inline( name='sparse_attention_ext', cpp_sources='', cuda_sources=cuda_source, functions=['sparse_attention_cuda'], extra_cuda_cflags=['-O3', '--use_fast_math'], verbose=False ) class ModelNew(nn.Module): def __init__(self): super().__init__() self.op = module.sparse_attention_cuda def forward(self, row_ptr, col_idx, edge_scores, node_value): return self.op(row_ptr, col_idx, edge_scores, node_value) ``` Wait, I should double-check if the tensors need to be contiguous. Let me add .contiguous() to be safe: ```python def forward(self, row_ptr, col_idx, edge_scores, node_value): return self.op( row_ptr.contiguous(), col_idx.contiguous(), edge_scores.contiguous(), node_value.contiguous() ) ``` Actually, this might add overhead if the tensors are already contiguous. Let me check if they're contiguous and only call .contiguous() if needed. But for simplicity, let me just add .contiguous() since the overhead should be minimal (just a check and a view, no copy if already contiguous). Actually, in PyTorch, .contiguous() returns a view if the tensor is already contiguous, so there's no copy overhead. I'll add it for safety. Let me also consider adding .cuda() check and ensure the tensors are on the right device. But the reference implementation doesn't do this, so I assume the inputs are already on the correct device. Alright, I think the implementation is ready. Let me submit it. One more thought: I'm using `node_value.options()` for the output tensor, which preserves dtype and device. This should be correct. Let me also ensure the CUDA kernel has the correct signature for the data types. The inputs are: - row_ptr: int32 - col_idx: int32 - edge_scores: float32 - node_value: float32 And my kernel uses: - const int* row_ptr: matches int32 - const int* col_idx: matches int32 - const float* edge_scores: matches float32 - const float* node_value: matches float32 All correct. Final code: ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = """ __global__ void sparse_attention_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 * blockDim.x + threadIdx.x; if (dst >= num_nodes) return; int start = row_ptr[dst]; int end = row_ptr[dst + 1]; if (start == end) { return; } // Find max for numerical stability float max_score = edge_scores[start]; for (int i = start + 1; i < end; i++) { max_score = fmaxf(max_score, edge_scores[i]); } // Compute sum of exp float sum_exp = 0.0f; for (int i = start; i < end; i++) { sum_exp += expf(edge_scores[i] - max_score); } float inv_sum_exp = 1.0f / sum_exp; // Get pointer to output row float* dst_out = out + dst * feat_dim; // Initialize output to zero for (int f = 0; f < feat_dim; f++) { dst_out[f] = 0.0f; } // Aggregate for (int i = start; i < end; i++) { float weight = expf(edge_scores[i] - max_score) * inv_sum_exp; int src = col_idx[i]; const float* src_val = node_value + src * feat_dim; for (int f = 0; f < feat_dim; f++) { dst_out[f] += src_val[f] * weight; } } } torch::Tensor sparse_attention_cuda( torch::Tensor row_ptr, torch::Tensor col_idx, torch::Tensor edge_scores, torch::Tensor node_value ) { int num_nodes = row_ptr.numel() - 1; int feat_dim = node_value.size(1); auto out = torch::zeros({num_nodes, feat_dim}, node_value.options()); int threads = 256; int blocks = (num_nodes + threads - 1) / threads; sparse_attention_kernel<<<blocks, threads>>>( row_ptr.data_ptr<int>(), col_idx.data_ptr<int>(), edge_scores.data_ptr<float>(), node_value.data_ptr<float>(), out.data_ptr<float>(), num_nodes, feat_dim ); return out; } """ module = load_inline( name='sparse_attention_ext', cpp_sources='', cuda_sources=cuda_source, functions=['sparse_attention_cuda'], extra_cuda_cflags=['-O3', '--use_fast_math'], verbose=False ) class ModelNew(nn.Module): def __init__(self): super().__init__() self.op = module.sparse_attention_cuda def forward(self, row_ptr, col_idx, edge_scores, node_value): return self.op( row_ptr.contiguous(), col_idx.contiguous(), edge_scores.contiguous(), node_value.contiguous() ) ``` I think this is ready for submission. Let me submit it now.
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_attention_kernel(\n const int* __restrict__ row_ptr,\n const int* __restrict__ col_idx,\n const float* __restrict__ edge_scores,\n const float* __restrict__ node_value,\n float* __restrict__ out,\n int num_nodes,\n int feat_dim\n) {\n int dst = blockIdx.x * blockDim.x + threadIdx.x;\n if (dst >= num_nodes) return;\n \n int start = row_ptr[dst];\n int end = row_ptr[dst + 1];\n \n if (start == end) {\n return;\n }\n \n // Find max for numerical stability\n float max_score = edge_scores[start];\n for (int i = start + 1; i < end; i++) {\n max_score = fmaxf(max_score, edge_scores[i]);\n }\n \n // Compute sum of exp\n float sum_exp = 0.0f;\n for (int i = start; i < end; i++) {\n sum_exp += expf(edge_scores[i] - max_score);\n }\n \n float inv_sum_exp = 1.0f / sum_exp;\n \n // Get pointer to output row\n float* dst_out = out + dst * feat_dim;\n \n // Initialize output to zero\n for (int f = 0; f < feat_dim; f++) {\n dst_out[f] = 0.0f;\n }\n \n // Aggregate\n for (int i = start; i < end; i++) {\n float weight = expf(edge_scores[i] - max_score) * inv_sum_exp;\n int src = col_idx[i];\n const float* src_val = node_value + src * feat_dim;\n for (int f = 0; f < feat_dim; f++) {\n dst_out[f] += src_val[f] * weight;\n }\n }\n}\n\ntorch::Tensor sparse_attention_cuda(\n torch::Tensor row_ptr,\n torch::Tensor col_idx,\n torch::Tensor edge_scores,\n torch::Tensor node_value\n) {\n int num_nodes = row_ptr.numel() - 1;\n int feat_dim = node_value.size(1);\n \n auto out = torch::zeros({num_nodes, feat_dim}, node_value.options());\n \n int threads = 256;\n int blocks = (num_nodes + threads - 1) / threads;\n \n sparse_attention_kernel<<<blocks, threads>>>(\n row_ptr.data_ptr<int>(),\n col_idx.data_ptr<int>(),\n edge_scores.data_ptr<float>(),\n node_value.data_ptr<float>(),\n out.data_ptr<float>(),\n num_nodes,\n feat_dim\n );\n \n return out;\n}\n\"\"\"\n\nmodule = load_inline(\n name='sparse_attention_ext',\n cpp_sources='',\n cuda_sources=cuda_source,\n functions=['sparse_attention_cuda'],\n extra_cuda_cflags=['-O3', '--use_fast_math'],\n verbose=False\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n self.op = module.sparse_attention_cuda\n \n def forward(self, row_ptr, col_idx, edge_scores, node_value):\n return self.op(\n row_ptr.contiguous(),\n col_idx.contiguous(),\n edge_scores.contiguous(),\n node_value.contiguous()\n )" }
result ← submit_kernel (fail)
failsubmit_kernel submit_kernel FAILED: kernel did not compile. Error building extension 'sparse_attention_ext': [1/3] /opt/conda/bin/x86_64-conda-linux-gnu-c++ -MMD -MF main.o.d -DTORCH_EXTENSION_NAME=sparse_attention_ext -DTORCH_API_INCLUDE_EXTENSION_H -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include/torch/csrc/api/include -isystem /usr/local/cuda/include -isystem /opt/conda/include/python3.10 -fPIC -std=c++17 -c /scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_37_cache/k_2ceabf90c67c/sparse_attention_ext/main.cpp -o main.o FAILED: [code=1] main.o /opt/conda/bin/x86_64-conda-linux-gnu-c++ -MMD -MF main.o.d -DTORCH_EXTENSION_NAME=sparse_attention_ext -DTORCH_API_INCLUDE_EXTENSION_H -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include/torch/csrc/api/include -isystem /usr/local/cuda/include -isystem /opt/conda/include/python3.10 -fPIC -std=c++17 -c /scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_37_cache/k_2ceabf90c67c/sparse_attention_ext/main.cpp -o main.o /scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_37_cache/k_2ceabf90c67c/sparse_attention_ext/main.cpp: In function 'void pybind11_init_sparse_attention_ext(pybind11::module_&)': /scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_37_cache/k_2ceabf90c67c/sparse_attention_ext/main.cpp:4:60: error: 'sparse_attention_cuda' was not declared in this scope 4 | m.def("sparse_attention_cuda", torch::wrap_pybind_function(sparse_attention_cuda), "sparse_attention_cuda"); | ^~~~~~~~~~~~~~~~~~~~~ [2/3] /usr/local/cuda/bin/nvcc -MD -MF cuda.cuda.o.d -ccbin /opt/conda/bin/x86_64-conda-linux-gnu-cc -DTORCH_EXTENSION_NAME=sparse_attention_ext -DTORCH_API_INCLUDE_EXTENSION_H -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include -isystem /scratch/abaner/KernelBench/.venv/lib/python3.10/site-packages/torch/include/torch/csrc/api/include -isystem /usr/local/cuda/include -isystem /opt/conda/include/python3.10 -D__CUDA_NO_HALF_OPERATORS__ -D__CUDA_NO_HALF_CONVERSIONS__ -D__CUDA_NO_BFLOAT16_CONVERSIONS__ -D__CUDA_NO_HALF2_OPERATORS__ --expt-relaxed-constexpr -gencode=arch=compute_90,code=compute_90 -gencode=arch=compute_90,code=sm_90 --compiler-options '-fPIC' -O3 --use_fast_math -std=c++17 -c /scratch/abaner/KernelBench/runs/pop_l123_st_gpt/popcorn/FW-GLM-5-1/level_2_problem_37_cache/k_2ceabf90c67c/sparse_attention_ext/cuda.cu -o cuda.cuda.o nvcc warning : incompatible redefinition for option 'compiler-bindir', the last value of this option was used ninja: build stopped: subcommand failed.
final submitted kernel (106 lines, 2,781 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

cuda_source = """
__global__ void sparse_attention_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 * blockDim.x + threadIdx.x;
    if (dst >= num_nodes) return;
    
    int start = row_ptr[dst];
    int end = row_ptr[dst + 1];
    
    if (start == end) {
        return;
    }
    
    // Find max for numerical stability
    float max_score = edge_scores[start];
    for (int i = start + 1; i < end; i++) {
        max_score = fmaxf(max_score, edge_scores[i]);
    }
    
    // Compute sum of exp
    float sum_exp = 0.0f;
    for (int i = start; i < end; i++) {
        sum_exp += expf(edge_scores[i] - max_score);
    }
    
    float inv_sum_exp = 1.0f / sum_exp;
    
    // Get pointer to output row
    float* dst_out = out + dst * feat_dim;
    
    // Initialize output to zero
    for (int f = 0; f < feat_dim; f++) {
        dst_out[f] = 0.0f;
    }
    
    // Aggregate
    for (int i = start; i < end; i++) {
        float weight = expf(edge_scores[i] - max_score) * inv_sum_exp;
        int src = col_idx[i];
        const float* src_val = node_value + src * feat_dim;
        for (int f = 0; f < feat_dim; f++) {
            dst_out[f] += src_val[f] * weight;
        }
    }
}

torch::Tensor sparse_attention_cuda(
    torch::Tensor row_ptr,
    torch::Tensor col_idx,
    torch::Tensor edge_scores,
    torch::Tensor node_value
) {
    int num_nodes = row_ptr.numel() - 1;
    int feat_dim = node_value.size(1);
    
    auto out = torch::zeros({num_nodes, feat_dim}, node_value.options());
    
    int threads = 256;
    int blocks = (num_nodes + threads - 1) / threads;
    
    sparse_attention_kernel<<<blocks, threads>>>(
        row_ptr.data_ptr<int>(),
        col_idx.data_ptr<int>(),
        edge_scores.data_ptr<float>(),
        node_value.data_ptr<float>(),
        out.data_ptr<float>(),
        num_nodes,
        feat_dim
    );
    
    return out;
}
"""

module = load_inline(
    name='sparse_attention_ext',
    cpp_sources='',
    cuda_sources=cuda_source,
    functions=['sparse_attention_cuda'],
    extra_cuda_cflags=['-O3', '--use_fast_math'],
    verbose=False
)

class ModelNew(nn.Module):
    def __init__(self):
        super().__init__()
        self.op = module.sparse_attention_cuda
    
    def forward(self, row_ptr, col_idx, edge_scores, node_value):
        return self.op(
            row_ptr.contiguous(),
            col_idx.contiguous(),
            edge_scores.contiguous(),
            node_value.contiguous()
        )