FW-GLM-5-1 · popcorn · pop_l123_st_gpt
error 10_GFVLA_FusedPairwiseNodeAggregate.py
model
FW-GLM-5-1
turns / max
1 / 1
tool calls
1
tokens (total)
8906
wall clock
282.0s
truncated
yes
backend
cuda fp32
variant
popcorn
runtime
—
ref runtime
0.00 μs
speedup
—
started
2026-05-11T08:53:47Z
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn
import torch.nn.functional as F

# Default ModelNew is PyTorch-only. run_and_check: use check_kernel=False with backend=cuda
# until the candidate file contains a real CUDA kernel (see kernel_static_checker).


class Model(nn.Module):
    """
    GF-VLA style **fused pairwise → node** scene-graph readout.

    For every ordered pair (i, j), fuse ``[node_i || node_j || pair_feat_ij]`` with
    an MLP, mask invalid graph edges, then **max-pool over outgoing neighbors j**
    to produce an updated per-object signal (object-centric scene graph refresh).

    Complements ``7_GFVLA_FusedSceneGraphUpdate`` (attention) with an MLP + reduce
    fusion pattern common for relation classifiers and affordance heads.
    """

    def __init__(self, dim: int, pair_dim: int, hidden: int):
        super().__init__()
        self.dim = dim
        self.pair_dim = pair_dim
        self.fuse = nn.Sequential(
            nn.Linear(2 * dim + pair_dim, hidden),
            nn.SiLU(),
            nn.Linear(hidden, dim),
        )
        self.out_norm = nn.LayerNorm(dim)

    def forward(
        self,
        nodes: torch.Tensor,
        pair_feats: torch.Tensor,
        edge_mask: torch.Tensor,
    ) -> torch.Tensor:
        """
        Args:
            nodes: (B, N, D)
            pair_feats: (B, N, N, P)
            edge_mask: (B, N, N) float {0,1}
        Returns:
            (B, N, D) residual ``nodes + LN( max_j fuse(...) )``
        """
        B, N, D = nodes.shape
        assert pair_feats.shape == (B, N, N, self.pair_dim)

        hi = nodes.unsqueeze(2).expand(-1, -1, N, -1)
        hj = nodes.unsqueeze(1).expand(-1, N, -1, -1)
        fused = self.fuse(torch.cat([hi, hj, pair_feats], dim=-1))

        neg = torch.finfo(fused.dtype).min
        fused = fused.masked_fill(edge_mask.unsqueeze(-1) < 0.5, neg)
        pooled, _ = fused.max(dim=2)
        return nodes + self.out_norm(pooled)


class ModelNew(nn.Module):
    """KernelBench candidate entry point; default delegates to ``Model``."""

    def __init__(self, dim: int, pair_dim: int, hidden: int):
        super().__init__()
        self._impl = Model(dim, pair_dim, hidden)

    def forward(
        self,
        nodes: torch.Tensor,
        pair_feats: torch.Tensor,
        edge_mask: torch.Tensor,
    ) -> torch.Tensor:
        return self._impl(nodes, pair_feats, edge_mask)


batch_size = 4
num_objects = 24
dim = 96
pair_dim = 16
hidden = 192


def get_inputs():
    p = popcorn_pri
    nodes = torch.randn(p.jitter_int(batch_size), p.jitter_int(num_objects), dim)
    pair_feats = torch.randn(p.jitter_int(batch_size), p.jitter_int(num_objects), p.jitter_int(num_objects), pair_dim)
    edge_mask = torch.ones(p.jitter_int(batch_size), p.jitter_int(num_objects), p.jitter_int(num_objects))
    return [nodes, pair_feats, edge_mask]


def get_init_inputs():
    return [dim, pair_dim, hidden]
turn 0 · 1 tool call(s) · 282.0s turn · 99.0s LLM · tokens in=1,531 · out=7,375 · total=8,906 · 24,909 chars (click to expand)
turn 0282.0s turn · 99.0s LLM · 183.0s tools · tokens in=1,531 · out=7,375 · total=8,906 · reasoning clipped
reasoning
reasoning (truncated 7939 chars): Let me analyze this task carefully: 1. The model is a scene-graph readout that: - Takes nodes (B, N, D), pair_feats (B, N, N, P), and edge_mask (B, N, N) - For each ordered pair (i, j), fuses [node_i || node_j || pair_feat_ij] - Passes through an MLP: Linear -> SiLU -> Linear - Masks invalid edges - Max-pools over j dimension - Applies LayerNorm and adds residual 2. The operations are: - Expand nodes to create hi and hj - Concatenate [hi, hj, pair_feats] - Two linear layers with SiLU activation in between - Masking with a large negative value - Max pooling over dimension 2 - LayerNorm - Residual addition 3. For a custom CUDA kernel, I need to: - Fuse the MLP computation (concatenation + linear + SiLU + linear) - Handle the masking - Perform max pooling - Apply LayerNorm - Add residual Let me write a kernel that fuses these operations. The key insight is that: - The MLP can be computed per-pair: each (i,j) pair has its own fused output - The max-pool is over j for each i - LayerNorm needs mean and variance computed over the entire D dimension For the MLP: - First layer: (2*D + P) -> hidden - SiLU activation - Second layer: hidden -> D Let me think about how to optimize this: - B=4, N=24, D=96, P=16, hidden=192 - Total pairs per batch: N*N = 576 - Total pairs: B * N * N = 2304 The computation is: 1. For each pair (b, i, j), compute: fuse([nodes[b,i], nodes[b,j], pair_feats[b,i,j]]) 2. This becomes a batched matrix multiplication where I reshape the concatenated features and weight matrices, then apply element-wise SiLU activation, followed by another linear transformation to produce the output dimension. 3. After computing the fused representations, I mask them, apply max pooling over the j dimension, add layer normalization, and include a residual connection. For the CUDA kernel, I'm considering how to parallelize the workload across thread blocks. Each block could handle a single batch element and source node, processing all destination nodes within that block. This approach leverages the independence of each (b,i) pair's computation across different j values, making it well-suited for GPU parallelization. Alternatively, I could have each block handle one batch element entirely, distributing the work across threads for the (i,j) pairs, or assign each thread block to a specific (b,i,j) triple for maximum parallelism. Given the dimensions (B=4, N=24), a block per (b,i) seems like a reasonable starting point that allows threads within the block to collaborate on the computations. For each (b,i,j) coordinate, I need to concatenate the input features: nodes[b,i], nodes[b,j], and pair_feats[b,i,j], forming a vector of length 2*D + P. This concatenated vector then passes through two linear transformations with SiLU activation in between, followed by masking, max pooling across j, layer normalization, and a residual connection. The kernel should be written in Python using cpp_extension.load_inline for compilation. += weight2[d_out * hidden + d_in] * hidden_vec[d_in]; } output[d_out] = bias2[d_out] + sum; } } For each (b, i) block, I'm computing the hidden representation for all j indices. Each thread handles one j value, concatenating the input features (node_i, node_j, and pair features), then applying the first linear transformation with SiLU activation. The kernel loops through the hidden dimension to compute intermediate values before applying the second linear layer. _idx, d] = hidden_vec[d]; } } // Max pool __shared__ float max_vals[MAX_N][D]; __shared__ float local_vals[D]; for (int d = 0; d < D; d++) { max_vals[threadIdx.x][d] = -1e10f; } if (valid) { for (int d = 0; d < D; d++) { max_vals[threadIdx.x][d] = output[d]; } } __syncthreads(); // Block-level max reduce for (int stride = blockDim.x / 2; stride > 0; stride /= 2) { if (threadIdx.x < stride) { for I'm storing the hidden vectors in shared memory so all threads can access them, then initializing the max values array with very negative numbers. Each thread writes its output to the max_vals array if it has valid data. After synchronizing, I perform a parallel reduction to find the maximum values across all threads in the block. stride >>= 1) { if (tid < stride) { for (int d = 0; d < D; d++) { if (sdata[tid + stride][d] > sdata[tid][d]) { sdata[tid][d] = sdata[tid + stride][d]; } } } __syncthreads(); } if (tid == 0) { // LayerNorm and write result for (int d = 0; d < D; d++) { float val = sdata[0][d]; // LayerNorm computation... float out_val = val + nodes[b_idx * N * D + i_idx * D + d]; output[b_idx * N * D + i_idx * D + d] = out_val; } } } ``` The shared memory constraint becomes an issue—storing MAX_N * D floats for N=24 and D=96 requires 9216 bytes just for the reduction, and the kernel needs additional space for intermediate computations. For LayerNorm, I need to compute the mean and variance across the D dimension, then apply the normalization formula: (x - mean) / sqrt(var + eps) * gamma + beta, which means each block needs to reduce across all D elements to get the statistics before normalizing. values across the D dimension float sum = 0.0f; for (int d = 0; d < D; d++) { sum += pooled[d]; } float mean = sum / D; float var_sum = 0.0f; for (int d = 0; d < D; d++) { float diff = pooled[d] - mean; var_sum += diff * diff; } float var = var_sum / D; float inv_std = rsqrtf(var + 1e-5f); // LayerNorm: (x - mean) / std * gamma + beta for (int d = 0; d < D; d++) { float normed = (pooled[d] - mean) * inv_std; normed = normed * gamma[d] + beta[d]; output[b_idx * N * D + i_idx * D + d] = nodes[b_idx * N * D + i_idx * D + d] + normed; } } } ``` Now I need to reconsider the shared memory constraints. With 24 threads per block and D=96 dimensions, each thread requires roughly 3*96 floats for its computation, which pushes against the 48KB limit. The matmul operations alone consume significant space, and I haven't yet accounted for the pooling, statistics, and normalization arrays. Let me recalculate more carefully—each thread needs input concatenation space (208 floats), hidden state storage, and output buffers, plus the reduction arrays for the block-level operations. Memory usage drops to ~11-12 KB, comfortably within the 48 KB limit. The kernel processes each block handling a single (b, i) pair across all j values. The fused_pairwise_kernel takes nodes, pair_feats, edge_mask, and the two linear layer weights with their biases, plus LayerNorm parameters. For each j index from 0 to N-1, I concatenate the input features by combining node embeddings from the two indices with their corresponding pair features, then store the result in shared memory. After synchronizing threads, I apply the first linear transformation followed by a SiLU activation, computing the hidden representation for each dimension. Then I perform a second linear transformation to produce the final output for each dimension, storing results in shared output arrays while also tracking the maximum values encountered across valid positions based on the edge mask. Once all threads complete this computation, I synchronize and perform a parallel reduction to find the maximum values across threads. Threads in the lower half compare their values with corresponding threads in the upper half, updating maxima where larger values are found, then synchronize again for the next iteration. When the reduction completes, thread 0 computes layer normalization ...[ omitted 7939 characters ]... pooling step. This scales to N*N threads per (b, i) pair with better occupancy. But that creates 576 threads per pair, which seems excessive. Let me try launching blocks where each handles a single (b, i) pair with all j values processed in parallel. Actually, a cleaner structure: launch B*N blocks total (one per batch element and node index), with N threads per block handling the j dimension. Each thread computes its portion of the MLP, then threads cooperate via shared memory for max pooling and layer normalization. Since N=24 threads per block is low, I could increase utilization by having each thread process multiple j values instead, or restructure the block assignment entirely. Let me start with a straightforward implementation and iterate from there. I need to complete the max pooling reduction across all threads, then implement LayerNorm and store the results. After the parallel reduction finishes finding the maximum values along the j dimension, thread 0 computes layer normalization by calculating the mean and variance across the D dimension, then applies the normalization with learnable gamma and beta parameters before writing the final output. , d] = sum; } // Max reduce over threads // Thread 0 collects from all threads __syncthreads(); if (tid == 0) { float pooled[D]; for (int d = 0; d < D; d++) { float max_val = -1e10f; for (int j = 0; j < N; j++) { float val = output[j][d]; if (val > max_val) max_val = val; } pooled[d] = max_val; } // LayerNorm float mean = 0.0f; for (int d = 0; d < D; d++) { mean += pooled[d]; } mean /= D; float var = 0.0f; for (int d = 0; d < D; d++) { float diff = pooled[d] - mean; var += diff * diff; } var /= D; float inv_std = rsqrtf(var + 1e-5f); // Output with residual for (int d = 0; d < D; d++) { float normed = (pooled[d] - mean) * inv_std; normed = normed * gamma[d] + beta[d]; out[b_idx * N * D + i_idx * D + d] = nodes[b_idx * N * D + i_idx * D + d] + normed; } } } extern "C" __global__ void fused_pairwise_kernel(...); ``` I cannot complete this thought as the next thinking appears to be incomplete or cut off mid-function definition. The shared memory allocation scales with N and D, so for N=24 and D=96 we get about 9.2KB per block, which stays well under the 48KB limit. But we're duplicating w1 and w2 in shared memory across all blocks unnecessarily—those should remain in global memory instead. Let me refactor to use registers for thread-local data and keep weights in global memory, which improves memory efficiency. For each (b, i, j) coordinate, we load the input features and apply the first linear transformation with a SiLU activation, then the second linear layer. If the edge is valid, we store the output and perform a max reduction across threads using parallel reduction in shared memory, halving the stride each iteration. After synchronization, thread 0 computes layer normalization by first calculating the mean across all dimensions, then the variance, and finally normalizing each dimension with the computed statistics. The normalized output is then scaled by gamma, shifted by beta, and combined with the residual connection before being stored. However, using a large negative value like -1e10f for masking could introduce numerical instability, so I should consider using -INFINITY or a sentinel value instead. The key difference from my implementation is that PyTorch performs the max operation directly on the masked tensor rather than handling it separately—I need to ensure the kernel stores the masked values before reduction. Let me implement this properly with the correct header inclusions and kernel structure. float out_val = out_ptr[b_idx * N * D + i_idx * D + d]; float max_val = -FLT_MAX; for (int j = 0; j < N; j++) { float val = out_ptr[b_idx * N * N * D + i_idx * N * D + j * D + d]; if (val > max_val) max_val = val; } pooled[d] = max_val; } // LayerNorm float mean = 0.0f; for (int d = 0; d < D; d++) { mean += pooled[d]; } mean /= D; float var = 0.0f; for (int d = 0; d < D; d++) { float diff = pooled[d] - mean; var += diff * diff; } var /= D; float std = sqrtf(var + eps); for (int d = 0; d < D; d++) { out_ptr[b_idx * N * D + i_idx * D + d] = (pooled[d] - mean) / std; } } } } ``` For each valid j, I compute the MLP output across all D dimensions by loading the node and pair features, applying linear transformations with SiLU activation between them. Then thread 0 performs a max pooling over the N dimension for each D, followed by layer normalization on the pooled result. I need to think through the memory layout more carefully here. The output array should be organized as [N, D] for each block, meaning index [j, d] maps to j*D + d. When flattening this into a 1D array of size N*D, the same indexing applies. The shared memory allocation needs to account for this N*D requirement. I should also verify that the block and grid dimensions align with the kernel launch configuration—specifically, the grid should have dimensions [B, N] to match the problem structure. Since block sizes must be powers of 2, I'll set blockDim.x to 32 (the next power above N=24) and have threads beyond N-1 exit early. The kernel itself then needs to handle this indexing constraint by checking if the thread index exceeds N before proceeding. For valid threads, I load the input features by concatenating node_i, node_j, and pair features into shared memory, then apply the first linear transformation followed by a SiLU activation function. Next, I compute the second linear layer to produce the output. If the edge is valid according to the mask, I store the result in shared memory; otherwise I initialize it to negative infinity. After synchronizing threads, I perform a parallel reduction to find the maximum value across all threads, processing the reduction in steps of 32. Once the reduction completes, I compute the layer normalization by first calculating the mean across all dimensions, then determining the variance from the squared differences from that mean. With the variance computed, I apply the inverse standard deviation (with epsilon for numerical stability) to normalize each value, then scale by gamma and shift by beta parameters. Finally, I add the residual connection from the original nodes and store the result back to the output tensor. The kernel function takes the input nodes, pair features, edge mask, layer weights and biases, layer norm parameters, and output pointer along with dimension parameters B, N, D, P, and hidden size to process the full batch. The forward pass allocates an empty output tensor matching the input shape, then launches the kernel with a grid of B*N blocks and 32 threads per block. Shared memory is dynamically allocated based on the sequence length and feature dimension. After computation, the result is returned. However, there's an issue with shared memory addressing. When multiple thread blocks run on the same stream processor, they each need their own shared memory segment. The static shared memory declaration actually allocates one instance per thread block, so when indexing s_output[tid * D + d], each block maintains its own separate shared memory space. With N=24 and D=96, the required shared memory per block is 9216 bytes, which should fit within typical hardware limits.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\ncuda_sources = r'''\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <cfloat>\n\ntemplate<int D, int P, int HIDDEN>\n__global__ void fused_pairwise_kernel(\n const float* __restrict__ nodes,\n const float* __restrict__ pair_feats,\n const float* __restrict__ edge_mask,\n const float* __restrict__ w1,\n const float* __restrict__ b1,\n const float* __restrict__ w2,\n const float* __restrict__ b2,\n const float* __restrict__ gamma,\n const float* __restrict__ beta,\n float* __restrict__ out,\n int B, int N\n) {\n extern __shared__ float s_output[];\n \n int b_idx = blockIdx.x;\n int i_idx = blockIdx.y;\n int tid = threadIdx.x;\n int valid_j = (tid < N);\n \n // Load nodes[b,i] once per block\n __shared__ float node_i[96];\n if (tid < D) {\n node_i[tid] = nodes[b_idx * N * D + i_idx * D + tid];\n }\n __syncthreads();\n \n // Each thread handles one j\n if (valid_j) {\n int j = tid;\n float node_j[96];\n float pair_vec[16];\n \n // Load node_j\n for (int d = 0; d < D; d++) {\n node_j[d] = nodes[b_idx * N * D + j * D + d];\n }\n // Load pair features\n for (int p = 0; p < P; p++) {\n pair_vec[p] = pair_feats[b_idx * N * N * P + i_idx * N * P + j * P + p];\n }\n \n // Linear 1: (2D + P) -> HIDDEN\n float hidden_vec[192];\n for (int h = 0; h < HIDDEN; h++) {\n float sum = b1[h];\n // nodes_i contribution\n for (int d = 0; d < D; d++) {\n sum += w1[h * (2*D + P) + d] * node_i[d];\n }\n // nodes_j contribution\n for (int d = 0; d < D; d++) {\n sum += w1[h * (2*D + P) + D + d] * node_j[d];\n }\n // pair contribution\n for (int p = 0; p < P; p++) {\n sum += w1[h * (2*D + P) + 2*D + p] * pair_vec[p];\n }\n hidden_vec[h] = sum;\n }\n \n // SiLU activation\n for (int h = 0; h < HIDDEN; h++) {\n float x = hidden_vec[h];\n hidden_vec[h] = x * (1.0f / (1.0f + expf(-x)));\n }\n \n // Linear 2: HIDDEN -> D\n float output[96];\n for (int d = 0; d < D; d++) {\n float sum = b2[d];\n for (int h = 0; h < HIDDEN; h++) {\n sum += w2[d * HIDDEN + h] * hidden_vec[h];\n }\n output[d] = sum;\n }\n \n // Check edge mask\n float mask_val = edge_mask[b_idx * N * N + i_idx * N + j];\n bool valid = (mask_val > 0.5f);\n \n // Mask and store to shared memory\n for (int d = 0; d < D; d++) {\n s_output[tid * D + d] = valid ? output[d] : (-FLT_MAX);\n }\n } else if (tid < 32) {\n // Extra threads initialize shared memory\n for (int d = 0; d < D; d++) {\n s_output[tid * D + d] = -FLT_MAX;\n }\n }\n __syncthreads();\n \n // Parallel max reduction over j dimension\n for (int stride = 16; stride > 0; stride >>= 1) {\n if (tid < stride && tid + stride < N) {\n for (int d = 0; d < D; d++) {\n float v1 = s_output[tid * D + d];\n float v2 = s_output[(tid + stride) * D + d];\n if (v2 > v1) {\n s_output[tid * D + d] = v2;\n }\n }\n }\n __syncthreads();\n }\n \n // Thread 0: LayerNorm + residual + output\n if (tid == 0) {\n // Pooled result is in s_output[0, :]\n float pooled[96];\n for (int d = 0; d < D; d++) {\n pooled[d] = s_output[d];\n }\n \n // LayerNorm: compute mean\n float mean = 0.0f;\n for (int d = 0; d < D; d++) {\n mean += pooled[d];\n }\n mean /= D;\n \n // LayerNorm: compute variance\n float var = 0.0f;\n for (int d = 0; d < D; d++) {\n float diff = pooled[d] - mean;\n var += diff * diff;\n }\n var /= D;\n \n // LayerNorm: normalize and apply affine transform\n float inv_std = rsqrtf(var + 1e-5f);\n for (int d = 0; d < D; d++) {\n float normed = (pooled[d] - mean) * inv_std;\n normed = normed * gamma[d] + beta[d];\n // Residual connection\n out[b_idx * N * D + i_idx * D + d] = node_i[d] + normed;\n }\n }\n}\n\ntorch::Tensor fused_pairwise_cuda(\n torch::Tensor nodes,\n torch::Tensor pair_feats,\n torch::Tensor edge_mask,\n torch::Tensor w1,\n torch::Tensor b1,\n torch::Tensor w2,\n torch::Tensor b2,\n torch::Tensor gamma,\n torch::Tensor beta\n) {\n TORCH_CHECK(nodes.scalar_type() == torch::kFloat32, \"nodes must be float32\");\n int B = nodes.size(0);\n int N = nodes.size(1);\n int D = nodes.size(2);\n int P = pair_feats.size(3);\n int HIDDEN = w1.size(0);\n \n auto out = torch::empty_like(nodes);\n \n dim3 grid(B, N);\n int threads = 32;\n int shared_mem = N * D * sizeof(float);\n \n fused_pairwise_kernel<96, 16, 192><<<grid, threads, shared_mem>>>(\n nodes.data_ptr<float>(),\n pair_feats.data_ptr<float>(),\n edge_mask.data_ptr<float>(),\n w1.data_ptr<float>(),\n b1.data_ptr<float>(),\n w2.data_ptr<float>(),\n b2.data_ptr<float>(),\n gamma.data_ptr<float>(),\n beta.data_ptr<float>(),\n out.data_ptr<float>(),\n B, N\n );\n \n return out;\n}\n'''\n\ncpp_sources = '''\ntorch::Tensor fused_pairwise_cuda(\n torch::Tensor nodes,\n torch::Tensor pair_feats,\n torch::Tensor edge_mask,\n torch::Tensor w1,\n torch::Tensor b1,\n torch::Tensor w2,\n torch::Tensor b2,\n torch::Tensor gamma,\n torch::Tensor beta\n);\n'''\n\n# Compile the module at import time\n_fused_module = load_inline(\n name='fused_pairwise_ext',\n cpp_sources=cpp_sources,\n cuda_sources=cuda_sources,\n functions=['fused_pairwise_cuda'],\n extra_cuda_cflags=['-O3', '--use_fast_math'],\n verbose=False\n)\n\n\nclass ModelNew(nn.Module):\n def __init__(self, dim: int, pair_dim: int, hidden: int):\n super().__init__()\n self.dim = dim\n self.pair_dim = pair_dim\n self.hidden = hidden\n \n # Define the same layers as Model\n self.fc1 = nn.Linear(2 * dim + pair_dim, hidden)\n self.fc2 = nn.Linear(hidden, dim)\n self.out_norm = nn.LayerNorm(dim)\n \n def forward(\n self,\n nodes: torch.Tensor,\n pair_feats: torch.Tensor,\n edge_mask: torch.Tensor,\n ) -> torch.Tensor:\n return _fused_module.fused_pairwise_cuda(\n nodes,\n pair_feats,\n edge_mask,\n self.fc1.weight,\n self.fc1.bias,\n self.fc2.weight,\n self.fc2.bias,\n self.out_norm.weight,\n self.out_norm.bias\n )" }
result ← submit_kernel (fail)
failsubmit_kernel submit_kernel FAILED: AcceleratorError: CUDA error: an illegal memory access was encountered Search for `cudaErrorIllegalAddress' in https://docs.nvidia.com/cuda/cuda-runtime-api/group__CUDART__TYPES.html for more information. CUDA kernel errors might be asynchronously reported at some other API call, so the stacktrace below might be incorrect. For debugging consider passing CUDA_LAUNCH_BLOCKING=1 Compile with `TORCH_USE_CUDA_DSA` to enable device-side assertions.
final submitted kernel (247 lines, 6,963 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

cuda_sources = r'''
#include <cuda.h>
#include <cuda_runtime.h>
#include <cfloat>

template<int D, int P, int HIDDEN>
__global__ void fused_pairwise_kernel(
    const float* __restrict__ nodes,
    const float* __restrict__ pair_feats,
    const float* __restrict__ edge_mask,
    const float* __restrict__ w1,
    const float* __restrict__ b1,
    const float* __restrict__ w2,
    const float* __restrict__ b2,
    const float* __restrict__ gamma,
    const float* __restrict__ beta,
    float* __restrict__ out,
    int B, int N
) {
    extern __shared__ float s_output[];
    
    int b_idx = blockIdx.x;
    int i_idx = blockIdx.y;
    int tid = threadIdx.x;
    int valid_j = (tid < N);
    
    // Load nodes[b,i] once per block
    __shared__ float node_i[96];
    if (tid < D) {
        node_i[tid] = nodes[b_idx * N * D + i_idx * D + tid];
    }
    __syncthreads();
    
    // Each thread handles one j
    if (valid_j) {
        int j = tid;
        float node_j[96];
        float pair_vec[16];
        
        // Load node_j
        for (int d = 0; d < D; d++) {
            node_j[d] = nodes[b_idx * N * D + j * D + d];
        }
        // Load pair features
        for (int p = 0; p < P; p++) {
            pair_vec[p] = pair_feats[b_idx * N * N * P + i_idx * N * P + j * P + p];
        }
        
        // Linear 1: (2D + P) -> HIDDEN
        float hidden_vec[192];
        for (int h = 0; h < HIDDEN; h++) {
            float sum = b1[h];
            // nodes_i contribution
            for (int d = 0; d < D; d++) {
                sum += w1[h * (2*D + P) + d] * node_i[d];
            }
            // nodes_j contribution
            for (int d = 0; d < D; d++) {
                sum += w1[h * (2*D + P) + D + d] * node_j[d];
            }
            // pair contribution
            for (int p = 0; p < P; p++) {
                sum += w1[h * (2*D + P) + 2*D + p] * pair_vec[p];
            }
            hidden_vec[h] = sum;
        }
        
        // SiLU activation
        for (int h = 0; h < HIDDEN; h++) {
            float x = hidden_vec[h];
            hidden_vec[h] = x * (1.0f / (1.0f + expf(-x)));
        }
        
        // Linear 2: HIDDEN -> D
        float output[96];
        for (int d = 0; d < D; d++) {
            float sum = b2[d];
            for (int h = 0; h < HIDDEN; h++) {
                sum += w2[d * HIDDEN + h] * hidden_vec[h];
            }
            output[d] = sum;
        }
        
        // Check edge mask
        float mask_val = edge_mask[b_idx * N * N + i_idx * N + j];
        bool valid = (mask_val > 0.5f);
        
        // Mask and store to shared memory
        for (int d = 0; d < D; d++) {
            s_output[tid * D + d] = valid ? output[d] : (-FLT_MAX);
        }
    } else if (tid < 32) {
        // Extra threads initialize shared memory
        for (int d = 0; d < D; d++) {
            s_output[tid * D + d] = -FLT_MAX;
        }
    }
    __syncthreads();
    
    // Parallel max reduction over j dimension
    for (int stride = 16; stride > 0; stride >>= 1) {
        if (tid < stride && tid + stride < N) {
            for (int d = 0; d < D; d++) {
                float v1 = s_output[tid * D + d];
                float v2 = s_output[(tid + stride) * D + d];
                if (v2 > v1) {
                    s_output[tid * D + d] = v2;
                }
            }
        }
        __syncthreads();
    }
    
    // Thread 0: LayerNorm + residual + output
    if (tid == 0) {
        // Pooled result is in s_output[0, :]
        float pooled[96];
        for (int d = 0; d < D; d++) {
            pooled[d] = s_output[d];
        }
        
        // LayerNorm: compute mean
        float mean = 0.0f;
        for (int d = 0; d < D; d++) {
            mean += pooled[d];
        }
        mean /= D;
        
        // LayerNorm: compute variance
        float var = 0.0f;
        for (int d = 0; d < D; d++) {
            float diff = pooled[d] - mean;
            var += diff * diff;
        }
        var /= D;
        
        // LayerNorm: normalize and apply affine transform
        float inv_std = rsqrtf(var + 1e-5f);
        for (int d = 0; d < D; d++) {
            float normed = (pooled[d] - mean) * inv_std;
            normed = normed * gamma[d] + beta[d];
            // Residual connection
            out[b_idx * N * D + i_idx * D + d] = node_i[d] + normed;
        }
    }
}

torch::Tensor fused_pairwise_cuda(
    torch::Tensor nodes,
    torch::Tensor pair_feats,
    torch::Tensor edge_mask,
    torch::Tensor w1,
    torch::Tensor b1,
    torch::Tensor w2,
    torch::Tensor b2,
    torch::Tensor gamma,
    torch::Tensor beta
) {
    TORCH_CHECK(nodes.scalar_type() == torch::kFloat32, "nodes must be float32");
    int B = nodes.size(0);
    int N = nodes.size(1);
    int D = nodes.size(2);
    int P = pair_feats.size(3);
    int HIDDEN = w1.size(0);
    
    auto out = torch::empty_like(nodes);
    
    dim3 grid(B, N);
    int threads = 32;
    int shared_mem = N * D * sizeof(float);
    
    fused_pairwise_kernel<96, 16, 192><<<grid, threads, shared_mem>>>(
        nodes.data_ptr<float>(),
        pair_feats.data_ptr<float>(),
        edge_mask.data_ptr<float>(),
        w1.data_ptr<float>(),
        b1.data_ptr<float>(),
        w2.data_ptr<float>(),
        b2.data_ptr<float>(),
        gamma.data_ptr<float>(),
        beta.data_ptr<float>(),
        out.data_ptr<float>(),
        B, N
    );
    
    return out;
}
'''

cpp_sources = '''
torch::Tensor fused_pairwise_cuda(
    torch::Tensor nodes,
    torch::Tensor pair_feats,
    torch::Tensor edge_mask,
    torch::Tensor w1,
    torch::Tensor b1,
    torch::Tensor w2,
    torch::Tensor b2,
    torch::Tensor gamma,
    torch::Tensor beta
);
'''

# Compile the module at import time
_fused_module = load_inline(
    name='fused_pairwise_ext',
    cpp_sources=cpp_sources,
    cuda_sources=cuda_sources,
    functions=['fused_pairwise_cuda'],
    extra_cuda_cflags=['-O3', '--use_fast_math'],
    verbose=False
)


class ModelNew(nn.Module):
    def __init__(self, dim: int, pair_dim: int, hidden: int):
        super().__init__()
        self.dim = dim
        self.pair_dim = pair_dim
        self.hidden = hidden
        
        # Define the same layers as Model
        self.fc1 = nn.Linear(2 * dim + pair_dim, hidden)
        self.fc2 = nn.Linear(hidden, dim)
        self.out_norm = nn.LayerNorm(dim)
    
    def forward(
        self,
        nodes: torch.Tensor,
        pair_feats: torch.Tensor,
        edge_mask: torch.Tensor,
    ) -> torch.Tensor:
        return _fused_module.fused_pairwise_cuda(
            nodes,
            pair_feats,
            edge_mask,
            self.fc1.weight,
            self.fc1.bias,
            self.fc2.weight,
            self.fc2.bias,
            self.out_norm.weight,
            self.out_norm.bias
        )