FW-GLM-5-1 · popcorn · pop_l123_st_gpt
incorrect 22_SE3InvariantLinear.py
model
FW-GLM-5-1
turns / max
1 / 1
tool calls
1
tokens (total)
4933
wall clock
856.0s
truncated
no
backend
cuda fp32
variant
popcorn
runtime
—
ref runtime
1.46 μs
speedup
—
started
2026-05-11T09:51:47Z
correctness issue: Output mismatch
timingkernelbench.timing
kernel runtime
—
ref runtime
1.46 μs
speedup
—
kernel mean / median / std
—
ref mean / median / std
1.46 / — / 8.08 μs
roofline / occupancyheuristic
dram bandwidth
—
dram util %
—
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
—
bottleneck
—
dominant pipe %
—
arithmetic intensity
—
ridge point
—
L1 hit %
—
L2 hit %
—
ld sectors / req
—
st sectors / req
—
regs / thread
—
smem / block
—
block size
—
peak bw
—
peak fp32 tflops
—
peak fp16 tflops
—
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn


class Model(nn.Module):
    """
    SE(3)-invariant linear layer.  Maps node features that are augmented
    with 3-D coordinate information through a linear transform that
    respects rotational and translational invariance.  The layer operates
    on (scalar_features, vector_features) pairs where scalar features
    transform trivially and vector features transform as 3-D vectors.
    Used in equivariant GNNs (EGNN, PaiNN, TFN).
    """

    def __init__(self, scalar_in, scalar_out, vector_in, vector_out):
        super().__init__()
        self.scalar_linear = nn.Linear(scalar_in + vector_in, scalar_out)
        self.vector_weight = nn.Parameter(torch.randn(vector_out, vector_in))
        nn.init.xavier_uniform_(self.vector_weight)
        self.vector_gate = nn.Linear(scalar_in + vector_in, vector_out)

    def forward(
        self, scalar_feat: torch.Tensor, vector_feat: torch.Tensor
    ) -> tuple:
        """
        Args:
            scalar_feat: (B, N, scalar_in) – invariant node features
            vector_feat: (B, N, vector_in, 3) – equivariant vector features
        Returns:
            (scalar_out, vector_out):
                scalar_out: (B, N, scalar_out)
                vector_out: (B, N, vector_out, 3)
        """
        # Invariant scalars from vector norms
        vec_norm = vector_feat.norm(dim=-1)  # (B, N, vector_in)
        combined = torch.cat([scalar_feat, vec_norm], dim=-1)

        s_out = self.scalar_linear(combined)

        # Equivariant vector transform: linear combination of input vectors
        v_out = torch.einsum("oi,bnid->bnod", self.vector_weight, vector_feat)
        # Gating by scalar-derived signal
        gate = torch.sigmoid(self.vector_gate(combined)).unsqueeze(-1)  # (B, N, vector_out, 1)
        v_out = gate * v_out

        return s_out, v_out


scalar_in = 64
scalar_out = 64
vector_in = 16
vector_out = 16
num_nodes = 128
batch_size = 8


def get_inputs():
    p = popcorn_pri
    bs = p.jitter_int(batch_size)
    nn = p.jitter_int(num_nodes)
    s = torch.randn(bs, nn, scalar_in)
    v = torch.randn(bs, nn, vector_in, 3)
    return [s, v]


def get_init_inputs():
    return [scalar_in, scalar_out, vector_in, vector_out]
turn 0 · FINAL · 1 tool call(s) · 856.0s turn · 39.2s LLM · tokens in=1,363 · out=3,570 · total=4,933 · 17,121 chars (click to expand)
turn 0856.0s turn · 39.2s LLM · 816.7s tools · tokens in=1,363 · out=3,570 · total=4,933 · FINAL
reasoning
Let me analyze the reference implementation carefully: 1. The model takes scalar features (B, N, scalar_in) and vector features (B, N, vector_in, 3) 2. It computes: - vec_norm = vector_feat.norm(dim=-1) -> (B, N, vector_in) - combined = concat(scalar_feat, vec_norm) -> (B, N, scalar_in + vector_in) - s_out = scalar_linear(combined) -> (B, N, scalar_out) - v_out = einsum("oi,bnid->bnod", vector_weight, vector_feat) -> (B, N, vector_out, 3) - gate = sigmoid(vector_gate(combined)).unsqueeze(-1) -> (B, N, vector_out, 1) - v_out = gate * v_out -> (B, N, vector_out, 3) For the dimensions: - scalar_in = 64, scalar_out = 64 - vector_in = 16, vector_out = 16 - num_nodes = 128 - batch_size = 8 The key operations are: 1. Vector norm computation (L2 norm along last dimension of size 3) 2. Concatenation 3. Linear layers (matrix multiplication + bias) 4. Einsum for vector transformation 5. Sigmoid gating 6. Element-wise multiplication Let me write an optimized CUDA kernel for this. I'll need to: 1. Fuse the norm computation with concatenation 2. Use cuBLAS for the linear layers (or write custom kernels) 3. Fuse the vector transformation, sigmoid, and gating Actually, let me think about this more carefully. The main computations are: 1. `scalar_linear(combined)` - this is a matmul of shape (B*N, scalar_in+vector_in) @ (scalar_in+vector_in, scalar_out) 2. `vector_weight @ vector_feat` - einsum "oi,bnid->bnod" which is essentially (vector_out, vector_in) @ (B, N, vector_in, 3) 3. `sigmoid(vector_gate(combined))` - another matmul I can fuse the norm computation with the linear operations, and combine the einsum with the sigmoid gating. The most expensive parts are the matrix multiplications for the scalar and vector linear layers, plus the einsum operation. I'll design custom kernels that handle the norm calculation inline and optimize the tensor contractions for the specific shapes we're working with. d_3 = sqrtf(vec_x*vec_x + vec_y*vec_y + vec_z*vec_z); } // Apply scalar linear transformation // combined = [scalar_feat, vec_norm] // s_out = scalar_linear(combined) // combined shape: (B, N, scalar_in + vector_in) // scalar_linear: Linear(scalar_in + vector_in, scalar_out) // This is a matmul: (B*N, scalar_in+vector_in) @ (scalar_in+vector_in, scalar_out) // Plus bias // We can use cuBLAS for this, or write a custom kernel // For the vector output: // v_out = einsum("oi,bnid->bnod", vector_weight, vector_feat) // v_out[b,n,o,d] = sum_i vector_weight[o,i] * vector_feat[b,n,i,d] I'm computing the L2 norm of each vector across the 3D components, then applying a linear transformation to the combined scalar and norm features. The vector output gets transformed through an einsum operation that contracts the input vectors with a weight matrix. For optimization, I should leverage cuBLAS for the large matrix multiplications and write a custom kernel for the vector transformation. The gating mechanism applies a sigmoid activation to the vector gate output, then multiplies it element-wise with the transformed vectors. I could fuse the einsum and gating into a single kernel to reduce memory overhead. d to compute norms separately, or handle that specially. A cleaner strategy emerges: write a fused kernel for the vector transformation step (einsum + sigmoid gating), since that's where we can combine operations effectively. For the linear layers and norm computation, standard PyTorch operations suffice. This approach minimizes memory transfers while keeping the implementation straightforward. Let me examine the workload dimensions to gauge where optimization matters most. With a batch size of 8 and num_nodes of 128, the scalar linear layer processes 1024 samples through a 64-to-64 transformation, and the vector linear layer handles the same number of samples. The vector_weight tensor is compact at 16×16, and the einsum operation transforms 8×128×16×3 data. The vector norm computation is lightweight—just 8×128×16 dot products of 3-element vectors. The einsum stands out as the most computationally intensive part since it's a large tensor contraction that could benefit from a fused kernel approach. I'm considering whether a custom kernel merging norm computation with linear operations would actually improve performance, given that cuBLAS handles matrix multiplications efficiently already. A more promising optimization might be combining the einsum operation with the sigmoid gating mechanism into a single kernel to reduce memory transfers. The forward pass would first compute vector norms, concatenate them with scalar features, then run the linear transformations. The custom kernel would handle the einsum computation, apply sigmoid gating, and fuse these steps together rather than computing them sequentially with intermediate memory writes. (B, N, vector_out, 3) scalar_out = self.scalar_linear(combined) vector_out = self.vec_transform_cuda(combined, vector_feat, self.vector_weight, self.vector_gate.weight, self.vector_gate.bias) return scalar_out, vector_out For the CUDA kernel, I'm setting up dimensions where each thread processes one output element. With batches of 8, 128 nodes per batch, and 16 output vectors per node across 3 dimensions, I need to map threads to handle the (batch, node, out_idx, d) indexing scheme efficiently. The kernel computes two main operations: first a gated transformation using the vector gate weights and sigmoid activation, then a weighted combination of input vector features. Each thread calculates its position in the output tensor, applies the gate to combine input scalars with vector norms, and then performs the matrix multiplication to produce the final output vector. After the gated values are computed, the kernel performs a reduction over the input vector dimension to accumulate weighted contributions from each input vector, storing the final result in the output tensor. The launch configuration uses a grid size based on the total number of output elements divided by 4, with each thread handling 4 elements to maximize throughput. scalar_out) For the linear layers, I'm implementing a single fused kernel that handles both scalar transformations. Each thread computes one output element by iterating through the input dimension, accumulating the weighted sum of inputs plus the bias term. This eliminates the separate cuBLAS calls and reduces kernel launch overhead. In the initialization, I set up both the scalar linear layer and a separate vector gate layer, storing the vector weights as a learnable parameter. The forward pass then concatenates the scalar features with the vector norms, applies the fused scalar transformation, and processes the vector features through a custom CUDA kernel that combines the einsum operation with sigmoid gating for the output. I should reconsider the overall structure to ensure the CUDA integration works smoothly with the module loading mechanism. Let me design a cleaner version that properly separates the compilation setup from the model definition. The initialization creates the scalar and vector linear layers, then compiles a custom CUDA kernel using `load_inline` to handle the fused einsum and gating operations. The compiled module gets stored as an attribute for later use in the forward pass. In the forward pass, vector norms are computed and concatenated with scalar features. The scalar output goes through its linear transformation, while the vector output is processed through the custom CUDA kernel that applies the transformation and gating in a single fused operation. The CUDA kernel itself launches with threads organized to handle the output dimensions, where each thread processes one element of the final output tensor. The kernel calculates gate values by iterating through the input features and applying weights, then uses these gates to modulate the vector transformation. After computing the sigmoid-activated gate, the kernel performs a second accumulation over the vector input dimension, multiplying input vector features by weight factors and the computed gate to produce the final output value. The kernel is then invoked through a C++ wrapper that extracts dimensions from the input tensors and launches the grid with a specified block size. I need to reconsider the approach here. Using `torch.nn.functional.linear` would be more efficient than manual computation since it leverages cuBLAS optimizations for the matrix multiplications. The vector transformation with gating can still be handled by the custom kernel, but I should leverage PyTorch's built-in linear operations where possible for better performance.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n# Compile CUDA kernel at module load time\ncuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <cmath>\n\n// Fused kernel: einsum + sigmoid gating\n// v_out[b,n,o,d] = sigmoid(sum_k gate_w[o,k] * combined[b,n,k] + gate_b[o]) * sum_i weight[o,i] * v_in[b,n,i,d]\n__global__ void vec_transform_kernel(\n const float* __restrict__ combined,\n const float* __restrict__ vector_weight,\n const float* __restrict__ gate_weight,\n const float* __restrict__ gate_bias,\n const float* __restrict__ vector_in,\n float* __restrict__ vector_out,\n int batch_size, int num_nodes, int vector_in_dim, int vector_out_dim, int combined_dim\n) {\n // Each thread handles one (b, n, o, d) output element\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int total = batch_size * num_nodes * vector_out_dim * 3;\n \n if (idx < total) {\n int d = idx % 3;\n int o = (idx / 3) % vector_out_dim;\n int n = (idx / (3 * vector_out_dim)) % num_nodes;\n int b = idx / (3 * vector_out_dim * num_nodes);\n \n int bn_idx = b * num_nodes + n;\n \n // Compute gate value: sigmoid(sum_k gate_w[o,k] * combined[b,n,k] + gate_b[o])\n float gate_val = gate_bias[o];\n for (int k = 0; k < combined_dim; k++) {\n gate_val += gate_weight[o * combined_dim + k] * combined[bn_idx * combined_dim + k];\n }\n gate_val = 1.0f / (1.0f + expf(-gate_val)); // sigmoid\n \n // Compute weighted vector sum: sum_i weight[o,i] * v_in[b,n,i,d]\n float v_val = 0.0f;\n for (int i = 0; i < vector_in_dim; i++) {\n int v_in_idx = ((b * num_nodes + n) * vector_in_dim + i) * 3 + d;\n v_val += vector_weight[o * vector_in_dim + i] * vector_in[v_in_idx];\n }\n \n // Apply gate\n vector_out[idx] = gate_val * v_val;\n }\n}\n\n// Vectorized version: process 4 elements per thread (o*d grouped)\n__global__ void vec_transform_kernel_vec4(\n const float* __restrict__ combined,\n const float* __restrict__ vector_weight,\n const float* __restrict__ gate_weight,\n const float* __restrict__ gate_bias,\n const float* __restrict__ vector_in,\n float* __restrict__ vector_out,\n int batch_size, int num_nodes, int vector_in_dim, int vector_out_dim, int combined_dim\n) {\n int idx = blockIdx.x * blockDim.x + threadIdx.x;\n int bn = batch_size * num_nodes;\n int total = bn * vector_out_dim; // Each thread handles 3 elements (one o for all d)\n \n if (idx < total) {\n int o = idx % vector_out_dim;\n int bn_idx = idx / vector_out_dim;\n \n // Compute gate once for all 3 dimensions\n float gate_val = gate_bias[o];\n const float* combined_ptr = combined + bn_idx * combined_dim;\n const float* gate_w_ptr = gate_weight + o * combined_dim;\n \n #pragma unroll 8\n for (int k = 0; k < combined_dim; k++) {\n gate_val += gate_w_ptr[k] * combined_ptr[k];\n }\n gate_val = 1.0f / (1.0f + expf(-gate_val));\n \n // Compute weighted vector for each of 3 dimensions\n const float* weight_ptr = vector_weight + o * vector_in_dim;\n \n for (int d = 0; d < 3; d++) {\n float v_val = 0.0f;\n #pragma unroll 4\n for (int i = 0; i < vector_in_dim; i++) {\n int v_in_idx = (bn_idx * vector_in_dim + i) * 3 + d;\n v_val += weight_ptr[i] * vector_in[v_in_idx];\n }\n vector_out[(bn_idx * vector_out_dim + o) * 3 + d] = gate_val * v_val;\n }\n }\n}\n\ntorch::Tensor vec_transform_cuda(\n torch::Tensor combined,\n torch::Tensor vector_weight,\n torch::Tensor gate_weight,\n torch::Tensor gate_bias,\n torch::Tensor vector_in\n) {\n int batch_size = combined.size(0);\n int num_nodes = combined.size(1);\n int combined_dim = combined.size(2);\n int vector_in_dim = vector_in.size(2);\n int vector_out_dim = vector_weight.size(0);\n \n auto vector_out = torch::empty({batch_size, num_nodes, vector_out_dim, 3}, combined.options());\n \n int total = batch_size * num_nodes * vector_out_dim;\n int threads = 256;\n int blocks = (total + threads - 1) / threads;\n \n vec_transform_kernel_vec4<<<blocks, threads>>>(\n combined.data_ptr<float>(),\n vector_weight.data_ptr<float>(),\n gate_weight.data_ptr<float>(),\n gate_bias.data_ptr<float>(),\n vector_in.data_ptr<float>(),\n vector_out.data_ptr<float>(),\n batch_size, num_nodes, vector_in_dim, vector_out_dim, combined_dim\n );\n \n return vector_out;\n}\n\"\"\"\n\ncpp_source = \"\"\"\ntorch::Tensor vec_transform_cuda(\n torch::Tensor combined,\n torch::Tensor vector_weight,\n torch::Tensor gate_weight,\n torch::Tensor gate_bias,\n torch::Tensor vector_in\n);\n\"\"\"\n\nvec_module = load_inline(\n name=\"vec_transform_module\",\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=[\"vec_transform_cuda\"],\n verbose=False,\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"]\n)\n\n\nclass ModelNew(nn.Module):\n \"\"\"\n SE(3)-invariant linear layer with optimized CUDA kernel for vector transformation.\n \"\"\"\n\n def __init__(self, scalar_in, scalar_out, vector_in, vector_out):\n super().__init__()\n self.scalar_linear = nn.Linear(scalar_in + vector_in, scalar_out)\n self.vector_weight = nn.Parameter(torch.empty(vector_out, vector_in))\n nn.init.xavier_uniform_(self.vector_weight)\n self.vector_gate = nn.Linear(scalar_in + vector_in, vector_out)\n \n self.vector_in = vector_in\n self.vector_out = vector_out\n\n def forward(\n self, scalar_feat: torch.Tensor, vector_feat: torch.Tensor\n ) -> tuple:\n \"\"\"\n Args:\n scalar_feat: (B, N, scalar_in) - invariant node features\n vector_feat: (B, N, vector_in, 3) - equivariant vector features\n Returns:\n (scalar_out, vector_out):\n scalar_out: (B, N, scalar_out)\n vector_out: (B, N, vector_out, 3)\n \"\"\"\n # Invariant scalars from vector norms\n vec_norm = vector_feat.norm(dim=-1) # (B, N, vector_in)\n combined = torch.cat([scalar_feat, vec_norm], dim=-1) # (B, N, scalar_in + vector_in)\n\n # Scalar output via optimized linear\n s_out = self.scalar_linear(combined)\n\n # Vector output via fused CUDA kernel (einsum + sigmoid gating)\n v_out = vec_module.vec_transform_cuda(\n combined,\n self.vector_weight,\n self.vector_gate.weight,\n self.vector_gate.bias,\n vector_feat\n )\n\n return s_out, v_out" }
result ← submit_kernel (fail)
failsubmit_kernel submit_kernel FAILED: correctness check did not pass ((0 / 5) trials). correctness_issue: Output mismatch max_difference: ['4.385889', '4.068792', '3.486669', '3.991396', '4.190471'] avg_difference: ['0.563917', '0.566549', '0.559676', '0.562504', '0.558501']
final submitted kernel (194 lines, 6,794 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

# Compile CUDA kernel at module load time
cuda_source = """
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <cmath>

// Fused kernel: einsum + sigmoid gating
// v_out[b,n,o,d] = sigmoid(sum_k gate_w[o,k] * combined[b,n,k] + gate_b[o]) * sum_i weight[o,i] * v_in[b,n,i,d]
__global__ void vec_transform_kernel(
    const float* __restrict__ combined,
    const float* __restrict__ vector_weight,
    const float* __restrict__ gate_weight,
    const float* __restrict__ gate_bias,
    const float* __restrict__ vector_in,
    float* __restrict__ vector_out,
    int batch_size, int num_nodes, int vector_in_dim, int vector_out_dim, int combined_dim
) {
    // Each thread handles one (b, n, o, d) output element
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int total = batch_size * num_nodes * vector_out_dim * 3;
    
    if (idx < total) {
        int d = idx % 3;
        int o = (idx / 3) % vector_out_dim;
        int n = (idx / (3 * vector_out_dim)) % num_nodes;
        int b = idx / (3 * vector_out_dim * num_nodes);
        
        int bn_idx = b * num_nodes + n;
        
        // Compute gate value: sigmoid(sum_k gate_w[o,k] * combined[b,n,k] + gate_b[o])
        float gate_val = gate_bias[o];
        for (int k = 0; k < combined_dim; k++) {
            gate_val += gate_weight[o * combined_dim + k] * combined[bn_idx * combined_dim + k];
        }
        gate_val = 1.0f / (1.0f + expf(-gate_val));  // sigmoid
        
        // Compute weighted vector sum: sum_i weight[o,i] * v_in[b,n,i,d]
        float v_val = 0.0f;
        for (int i = 0; i < vector_in_dim; i++) {
            int v_in_idx = ((b * num_nodes + n) * vector_in_dim + i) * 3 + d;
            v_val += vector_weight[o * vector_in_dim + i] * vector_in[v_in_idx];
        }
        
        // Apply gate
        vector_out[idx] = gate_val * v_val;
    }
}

// Vectorized version: process 4 elements per thread (o*d grouped)
__global__ void vec_transform_kernel_vec4(
    const float* __restrict__ combined,
    const float* __restrict__ vector_weight,
    const float* __restrict__ gate_weight,
    const float* __restrict__ gate_bias,
    const float* __restrict__ vector_in,
    float* __restrict__ vector_out,
    int batch_size, int num_nodes, int vector_in_dim, int vector_out_dim, int combined_dim
) {
    int idx = blockIdx.x * blockDim.x + threadIdx.x;
    int bn = batch_size * num_nodes;
    int total = bn * vector_out_dim;  // Each thread handles 3 elements (one o for all d)
    
    if (idx < total) {
        int o = idx % vector_out_dim;
        int bn_idx = idx / vector_out_dim;
        
        // Compute gate once for all 3 dimensions
        float gate_val = gate_bias[o];
        const float* combined_ptr = combined + bn_idx * combined_dim;
        const float* gate_w_ptr = gate_weight + o * combined_dim;
        
        #pragma unroll 8
        for (int k = 0; k < combined_dim; k++) {
            gate_val += gate_w_ptr[k] * combined_ptr[k];
        }
        gate_val = 1.0f / (1.0f + expf(-gate_val));
        
        // Compute weighted vector for each of 3 dimensions
        const float* weight_ptr = vector_weight + o * vector_in_dim;
        
        for (int d = 0; d < 3; d++) {
            float v_val = 0.0f;
            #pragma unroll 4
            for (int i = 0; i < vector_in_dim; i++) {
                int v_in_idx = (bn_idx * vector_in_dim + i) * 3 + d;
                v_val += weight_ptr[i] * vector_in[v_in_idx];
            }
            vector_out[(bn_idx * vector_out_dim + o) * 3 + d] = gate_val * v_val;
        }
    }
}

torch::Tensor vec_transform_cuda(
    torch::Tensor combined,
    torch::Tensor vector_weight,
    torch::Tensor gate_weight,
    torch::Tensor gate_bias,
    torch::Tensor vector_in
) {
    int batch_size = combined.size(0);
    int num_nodes = combined.size(1);
    int combined_dim = combined.size(2);
    int vector_in_dim = vector_in.size(2);
    int vector_out_dim = vector_weight.size(0);
    
    auto vector_out = torch::empty({batch_size, num_nodes, vector_out_dim, 3}, combined.options());
    
    int total = batch_size * num_nodes * vector_out_dim;
    int threads = 256;
    int blocks = (total + threads - 1) / threads;
    
    vec_transform_kernel_vec4<<<blocks, threads>>>(
        combined.data_ptr<float>(),
        vector_weight.data_ptr<float>(),
        gate_weight.data_ptr<float>(),
        gate_bias.data_ptr<float>(),
        vector_in.data_ptr<float>(),
        vector_out.data_ptr<float>(),
        batch_size, num_nodes, vector_in_dim, vector_out_dim, combined_dim
    );
    
    return vector_out;
}
"""

cpp_source = """
torch::Tensor vec_transform_cuda(
    torch::Tensor combined,
    torch::Tensor vector_weight,
    torch::Tensor gate_weight,
    torch::Tensor gate_bias,
    torch::Tensor vector_in
);
"""

vec_module = load_inline(
    name="vec_transform_module",
    cpp_sources=cpp_source,
    cuda_sources=cuda_source,
    functions=["vec_transform_cuda"],
    verbose=False,
    extra_cuda_cflags=["-O3", "--use_fast_math"]
)


class ModelNew(nn.Module):
    """
    SE(3)-invariant linear layer with optimized CUDA kernel for vector transformation.
    """

    def __init__(self, scalar_in, scalar_out, vector_in, vector_out):
        super().__init__()
        self.scalar_linear = nn.Linear(scalar_in + vector_in, scalar_out)
        self.vector_weight = nn.Parameter(torch.empty(vector_out, vector_in))
        nn.init.xavier_uniform_(self.vector_weight)
        self.vector_gate = nn.Linear(scalar_in + vector_in, vector_out)
        
        self.vector_in = vector_in
        self.vector_out = vector_out

    def forward(
        self, scalar_feat: torch.Tensor, vector_feat: torch.Tensor
    ) -> tuple:
        """
        Args:
            scalar_feat: (B, N, scalar_in) - invariant node features
            vector_feat: (B, N, vector_in, 3) - equivariant vector features
        Returns:
            (scalar_out, vector_out):
                scalar_out: (B, N, scalar_out)
                vector_out: (B, N, vector_out, 3)
        """
        # Invariant scalars from vector norms
        vec_norm = vector_feat.norm(dim=-1)  # (B, N, vector_in)
        combined = torch.cat([scalar_feat, vec_norm], dim=-1)  # (B, N, scalar_in + vector_in)

        # Scalar output via optimized linear
        s_out = self.scalar_linear(combined)

        # Vector output via fused CUDA kernel (einsum + sigmoid gating)
        v_out = vec_module.vec_transform_cuda(
            combined,
            self.vector_weight,
            self.vector_gate.weight,
            self.vector_gate.bias,
            vector_feat
        )

        return s_out, v_out