FW-GLM-5-1 · popcorn · pop_l123_st_gpt
incorrect 30_SelectiveScan_Mamba.py
model
FW-GLM-5-1
turns / max
1 / 1
tool calls
1
tokens (total)
9897
wall clock
610.0s
truncated
yes
backend
cuda fp32
variant
popcorn
runtime
—
ref runtime
324.00 μs
speedup
—
started
2026-05-11T09:15:07Z
correctness issue:
timingkernelbench.timing
kernel runtime
—
ref runtime
324.00 μs
speedup
—
kernel mean / median / std
—
ref mean / median / std
324.00 / — / 871.00 μ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):
    """
    Selective scan (Mamba's S6 layer): projects input to derive input-dependent SSM parameters
    (delta, B, C), discretizes a learned state matrix A, runs the linear recurrence, and
    applies a skip connection via D. The fusion opportunity is keeping all intermediates
    (discretized parameters, hidden states) in SRAM rather than materializing them to global memory.

    Args:
        d_model (int): Input feature dimension.
        d_state (int): Hidden state dimension per channel (N in Mamba, typically 16).
    """
    def __init__(self, d_model: int, d_state: int):
        super(Model, self).__init__()
        self.d_model = d_model
        self.d_state = d_state
        self.A_log = nn.Parameter(torch.randn(d_model, d_state))
        self.proj_B = nn.Linear(d_model, d_state, bias=False)
        self.proj_C = nn.Linear(d_model, d_state, bias=False)
        self.proj_delta = nn.Linear(d_model, d_model, bias=True)
        self.D = nn.Parameter(torch.ones(d_model))

    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, L, D = x.shape
        N = self.d_state

        delta = torch.nn.functional.softplus(self.proj_delta(x))
        B_t = self.proj_B(x)
        C_t = self.proj_C(x)

        A = -torch.exp(self.A_log)
        dA = torch.exp(delta.unsqueeze(-1) * A.unsqueeze(0).unsqueeze(0))
        dB = delta.unsqueeze(-1) * B_t.unsqueeze(2)
        x_db = x.unsqueeze(-1) * dB

        h = torch.zeros(B, D, N, device=x.device, dtype=x.dtype)
        outputs = []
        for t in range(L):
            h = dA[:, t] * h + x_db[:, t]
            y_t = (h * C_t[:, t].unsqueeze(1)).sum(dim=-1)
            outputs.append(y_t)

        y = torch.stack(outputs, dim=1)
        y = y + x * self.D.unsqueeze(0).unsqueeze(0)
        return y

batch_size = 16
d_model = 256
d_state = 16
seq_len = 2048

def get_inputs():
    return [torch.randn(popcorn_pri.jitter_int(batch_size), popcorn_pri.jitter_int(seq_len), d_model)]

def get_init_inputs():
    return [d_model, d_state]
turn 0 · FINAL · 1 tool call(s) · 610.0s turn · 97.3s LLM · tokens in=1,349 · out=8,548 · total=9,897 · 23,958 chars (click to expand)
turn 0610.0s turn · 97.3s LLM · 512.7s tools · tokens in=1,349 · out=8,548 · total=9,897 · FINAL · reasoning clipped
reasoning
reasoning (truncated 14672 chars): This is a complex task involving implementing Mamba's S6 (Selective State Space) layer as a custom CUDA kernel. Let me analyze the reference implementation carefully: 1. The model has: - `A_log`: learned state matrix parameters (d_model x d_state) - `proj_B`: linear projection for B (d_model -> d_state) - `proj_C`: linear projection for C (d_model -> d_state) - `proj_delta`: linear projection for delta (d_model -> d_model) with bias - `D`: skip connection parameter (d_model) 2. Forward pass: - Compute delta = softplus(proj_delta(x)) - shape (B, L, D) - Compute B_t = proj_B(x) - shape (B, L, N) - Compute C_t = proj_C(x) - shape (B, L, N) - Compute A = -exp(A_log) - shape (D, N) - Compute dA = exp(delta * A) - shape (B, L, D, N) - Compute dB = delta * B_t (with broadcasting) - shape (B, L, D, N) - Compute x_db = x * dB - shape (B, L, D, N) - Run the recurrence: h = dA[:, t] * h + x_db[:, t] - Compute output at each step: y_t = sum(h * C_t[:, t], dim=-1) - Final output: y + x * D The main computational challenge is the sequential scan operation. Each timestep depends on the previous hidden state, so this is inherently sequential. However, we can still optimize by: 1. Fusing the projections 2. Keeping intermediate values in registers/SRAM 3. Using parallel scan algorithms where possible For a CUDA implementation, I'll need to: 1. Fuse all operations into kernels where possible 2. Keep the recurrence computation efficient by processing multiple batch elements and channels in parallel I'll structure this with fused projection kernels that compute delta, B_t, and C_t together, followed by a scan kernel that handles the recurrent computation across timesteps. The key is launching sufficient thread blocks to saturate the GPU while keeping the recurrence data local to each thread. For the scan itself, I need to account for how the state matrix A evolves: at each step, the state updates as h_new = exp(delta * A) * h + x * delta * B, with the output y computed by summing h * C across the state dimension. Since each batch element and channel operates independently, I can parallelize across those dimensions, but the L timesteps must remain sequential unless I implement a parallel scan algorithm. Given the high parallelism available (4096 elements for B=16, D=256), a straightforward sequential scan per thread should work well. The core kernel computes delta and dB matrices for each step, then applies the recurrence h = exp(delta * A) * h + x * delta * B, where the state dimension N=16 determines the inner loop iterations. Actually, I need to reconsider the tensor dimensions. Looking at the projections: delta is (B, L, D), B_t is (B, L, N), C_t is (B, L, N), and A is (D, N). The delta unsqueeze creates (B, L, D, 1), which broadcasts against A's (D, N) when expanded to (1, 1, D, N), giving dA as (B, L, D, N). The key insight is that each channel d has its own A[d, :] vector of size N, and the hidden state h maintains separate values across all batch, channel, and state dimensions. The recurrence formula becomes h[b, d, :] = dA[b, t, d, :] * h[b, d, :] + x_db[b, t, d, :], which updates the state at each timestep. The output y[b, t, d] then results from summing the element-wise product of the state and C_t across the N dimension. For parallelization, I can assign each thread to handle one (b, d) pair independently, processing all L timesteps sequentially while maintaining private state vectors. This maps directly to a kernel structure where threads process their assigned channel across time. ```cpp __global__ void selective_scan_kernel( const float* x, // (B, L, D) const float* delta, // (B, L, D) const float* B_t, // (B, L, N) const float* C_t, // (B, L, N) const float* A, // (D, N), A is already -exp(A_log) const float* D_param, // (D,) float* y, // (B, L, D) int B, int L, int D, int N ) { int b = blockIdx.x / D; int d = blockIdx.x % D; int tid = b * D + d; if (b >= B || d >= D) return; extern __shared__ float shared_mem[]; float* h = shared_mem; // size N float* A_local = &shared_mem[N]; // size N // Load A for this channel for (int n = 0; n < N; n++) { A_local[n] = A[d * N + n]; h[n] = 0.0f; } for (int t = 0; t < L; t++) { float delta_val = delta[b * L * D + t * D + d]; float x_val = x[b * L * D + t * D + d]; // Load B_t and C_t for this (b, t) // B_t and C_t are shared across D for a given (b, t) // B_t[b, t, n] = B_t[(b * L + t) * N + n] // C_t[b, t, n] = C_t[(b * L + t) * N + n] float y_sum = 0.0f; for (int n = 0; n < N; n++) { float B_val = B_t[(b * L + t) * N + n]; float dB_val = delta_val * B_val; float x_db_val = x_val * dB_val; float dA_val = expf(delta_val * A_local[n]); h[n] = dA_val * h[n] + x_db_val; float C_val = C_t[(b * L + t) * N + n]; y_sum += h[n] * C_val; } y[b * L * D + t * D + d] = y_sum + x_val * D_param[d]; } } ``` The kernel processes each timestep sequentially, computing delta and the discretized parameters on-the-fly, then applying the recurrence relation while accumulating the output. The shared memory stores the state vector h and precomputed A values to avoid repeated global memory accesses. For the memory layout, delta, x, and output y all use (B, L, D) indexing with stride D across the feature dimension, while B_t and C_t use (B, L, N) indexing with stride N. This means each thread loads its corresponding delta and x values directly, then loops through all N state dimensions to compute the updates. I need to reconsider the parallelization strategy. With 4096 total elements across batch and feature dimensions, using one thread per (b, d) pair could work if each thread handles all L timesteps and N state dimensions. This would require substantial register space for the state vector—256 bytes per thread for float16, which might be manageable with proper optimization. The main computational bottleneck involves loading the delta and A values to compute the state updates, along with accessing the B and C matrices for the discretization step. I should examine whether fusing the projection operations into a single kernel could reduce memory overhead and improve efficiency. A[blockIdx.y * N + threadIdx.x];` Within the loop over timesteps, I compute delta and x values at each step, then load B_t into shared memory cooperatively across the warp. For each element in the sequence, I retrieve delta and x from global memory, then have all threads in the warp participate in loading the B_t values for that timestep into shared memory. Once B_t is loaded, I compute deltaA and deltaB_u values for each element, then perform the core update computation where h accumulates the state transition. After processing all elements in the sequence, I store the final output by multiplying h with C and adding the skip connection, writing the result to global memory. The kernel structure involves loading the A matrix into registers for the current channel, then iterating through each timestep to update the hidden state using the input, delta, B, and C matrices. I'm computing the delta scaling factors and applying the state update formula before storing the transformed output back to the output tensor. int stride = blockDim.x * gridDim.x; int total = B * D; for (int idx = tid; idx < total; idx += stride) { int b = idx / D; int d = idx % D; // Load A for this channel float A_local[16]; // N = 16 for (int n = 0; n < N; n++) { A_local[n] = A[d * N + n]; } float h[16] = {0.0f}; // Hidden state for (int t = 0; t < L; t++) { ...[ omitted 14672 characters ]... e, with each thread handling its own sequence. N = 16, so the inner loop is manageable. Each thread does L * N = 32K iterations with 2 loads, 1 exp, and 3 FLOPs per iteration. That's roughly 64K loads, 32K exp calls, and 96K FLOPs per thread, which should fit reasonably in the arithmetic intensity budget. The memory access pattern needs checking though—the indexing into x, delta, and y should coalesce across threads, while B_t and C_t access patterns depend on how N relates to the thread indexing. Looking more carefully at the B_t access: since threadIdx.x increments across a warp and consecutive threads map to different (b, d) pairs, they'll hit different b*L*N + t*N + n locations. This means poor coalescing within a warp unless threads happen to access adjacent memory locations, which they won't given the strided pattern across the batch and model dimensions. However, examining the actual layout reveals that threads in a warp with consecutive indices access consecutive addresses in the D dimension for x, delta, and y arrays—those tensors are (B, L, D) and threads vary d while keeping b and t fixed, so threadIdx.x maps directly to adjacent D positions. This actually provides good coalescing for those tensors. For B_t and C_t though, the issue is more nuanced. Since A has shape (D, N) and threads vary d, consecutive threads access different A rows but the same N column. When multiple threads load the same A[d, n] value across different d indices, each thread does a separate load. However, the compiler or hardware might optimize this with caching or coalescing when threads access consecutive d values for the same n. The real solution is using shared memory to load A once and share it across threads, reducing redundant memory traffic. For a production implementation, I should focus on three key optimizations: using __restrict__ pointers and const qualifiers, leveraging vectorized loads where the data layout supports it, and precomputing exp(A_log) to avoid recalculating it. For the softplus activation on delta, I'm computing it via the log1p-exp trick for numerical stability: log(1 + exp(x)), which handles the edge cases properly. The kernel structure follows the reference implementation's sequential scan pattern. I load the precomputed A matrix into registers, maintain a hidden state vector, then for each timestep compute the discretized delta and B values, update the hidden state, and accumulate the output contribution from each state dimension. After processing all timesteps, I store the final output by combining the accumulated sum with the skip connection scaled by D_param, then apply softplus to the delta values. The host function orchestrates the kernel launch by determining grid and block dimensions based on the total number of elements. I load the CUDA extension module and call the compiled kernel, passing all tensors and their dimensions. The forward pass computes the core selective scan operation by first obtaining the delta values through softplus activation, then projecting the input to derive the B and C matrices, and computing the diagonal A matrix from the log-space parameter. After preparing these components, I invoke the CUDA kernel with the input tensor and all computed matrices to produce the output. The kernel's indexing scheme relies on contiguous memory layouts where x, delta, and y are stored as [B, L, D] tensors with element t*D+d at offset b*L*D + t*D + d, while B_t and C_t follow [B, L, N] ordering. The shared indices between B_t and C_t work correctly since both use the same formula. For A, each channel d accesses its N-element slice at offset d*N, and D_param retrieves individual channel values. The thread structure assigns each thread to a specific (b, d) pair, then processes all L timesteps sequentially, iterating through each of the N state elements within the inner loop. Regarding numerical precision, the implementation uses expf and standard floating-point operations that should align with PyTorch's FP32 behavior. For memory efficiency, B and C tensors become the dominant storage overhead—the combined size is roughly 1MB for the given dimensions. The approach avoids creating intermediate dA and x_db tensors, maintaining the same memory footprint as the reference implementation. Now I need to verify the kernel registration. Since I'm declaring selective_scan_cuda in the C++ source and defining it in CUDA, both get compiled together through load_inline, so the linking should resolve correctly. The functions parameter only needs to list the C++ function name that Python will invoke directly. For the kernel itself, I should reconsider whether to inline all operations. A fully fused version would compute projections on-the-fly within shared memory, but that requires loading weight matrices and computing matrix products directly in the kernel, which adds significant complexity. A practical middle ground is keeping separate projection kernels for B_t, C_t, and delta while fusing the scan computation with softplus. This approach balances code clarity with reasonable performance—projections happen once upfront, then the scan kernel handles the recurrence without materializing intermediate results to global memory. However, examining the reference implementation reveals that x_db is only needed within the loop itself, so there's potential to eliminate even this intermediate tensor by computing it on-demand during the scan. Looking at the actual computation flow: A is precomputed from A_log, then dA, dB, and x_db are all derived from delta and the projections. The key insight is that x_db can be computed element-by-element within the scan loop rather than stored as a full tensor. The recurrence h = dA * h + x_db at each timestep only needs the current x_db value, so I can compute x_val * dB[n] on the fly for each state dimension n, eliminating a significant memory allocation. h[b, d, n] *= dA[b, t, d, n] h[b, d, n] += x[b, t, d] * delta[b, t, d] * B_t[b, t, n] y[b, t, d] = sum_n(h[b, d, n] * C_t[b, t, n]) After the loop completes, I add the skip connection: y = y + x * D. This matches my kernel implementation. For the exp(A_log) computation, A_log is a learnable parameter that gets transformed via A = -exp(A_log) during the forward pass. Since A_log doesn't require gradients in my custom kernel, I could precompute A once after initialization. However, A_log remains a Parameter that gets updated during training, so I need to recompute A = -exp(A_log) on each forward pass to capture any changes from optimization steps. I could register a forward hook to cache this computation, but for simplicity I'll just compute it directly in the forward method. Let me finalize the implementation. The model class stores the core parameters: A_log for the state matrix, projection layers for B, C, and delta, and the D skip connection parameter. The CUDA kernel gets cached after the first forward pass. In the forward method, I apply softplus to the delta projection, compute the B and C projections from the input, then transform A_log into the negative exponential form needed for the scan operation. After preparing the output tensor, I invoke the CUDA kernel through the extension module, passing all the computed tensors along with the dimensional parameters. The kernel launch configuration uses 256 threads per block with enough blocks to cover all batch and dimension elements. Inside the kernel itself, each thread processes a single batch-dimension pair across all timesteps, loading the corresponding A values into local registers and iterating through the sequence to compute the selective scan. For each timestep, I retrieve the delta and input values, then accumulate contributions across all N elements by computing the decay factor, updating the hidden state, and summing the weighted output. The final result combines this accumulated sum with the skip connection scaled by the D parameter before writing to global memory.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nclass ModelNew(nn.Module):\n \"\"\"\n Optimized Mamba S6 (Selective Scan) implementation using custom CUDA kernels.\n \n This implementation fuses the scan operation to keep intermediate values in registers,\n avoiding materializing dA, dB, and x_db to global memory.\n \"\"\"\n \n def __init__(self, d_model: int, d_state: int):\n super(ModelNew, self).__init__()\n self.d_model = d_model\n self.d_state = d_state\n self.A_log = nn.Parameter(torch.randn(d_model, d_state))\n self.proj_B = nn.Linear(d_model, d_state, bias=False)\n self.proj_C = nn.Linear(d_model, d_state, bias=False)\n self.proj_delta = nn.Linear(d_model, d_model, bias=True)\n self.D = nn.Parameter(torch.ones(d_model))\n \n # Cache the compiled kernel\n self._cuda_module = None\n \n def _get_cuda_module(self):\n \"\"\"Lazy-load and cache the CUDA kernel module.\"\"\"\n if self._cuda_module is not None:\n return self._cuda_module\n \n cuda_source = \"\"\"\n#include <torch/extension.h>\n#include <cuda.h>\n#include <cuda_runtime.h>\n#include <cmath>\n\n// Optimized selective scan kernel\n// Each thread processes one (batch, channel) pair across all timesteps\n// This maximizes data reuse and keeps h in registers\n__global__ void selective_scan_kernel(\n const float* __restrict__ x, // (B, L, D)\n const float* __restrict__ delta, // (B, L, D)\n const float* __restrict__ B_t, // (B, L, N)\n const float* __restrict__ C_t, // (B, L, N)\n const float* __restrict__ A, // (D, N)\n const float* __restrict__ D_param, // (D,)\n float* __restrict__ y, // (B, L, D)\n int B, int L, int D, int N) {\n \n int tid = blockIdx.x * blockDim.x + threadIdx.x;\n int stride = blockDim.x * gridDim.x;\n int total = B * D;\n \n // Process each (b, d) pair\n for (int idx = tid; idx < total; idx += stride) {\n int b = idx / D;\n int d = idx % D;\n \n // Load A for this channel into registers (kept hot throughout scan)\n // A shape: (D, N), we need A[d, :]\n float A_local[16]; // Max N = 16\n #pragma unroll\n for (int n = 0; n < N; n++) {\n A_local[n] = A[d * N + n];\n }\n \n // Hidden state h (D, N) -> we only need h[d, :] = (N,)\n // Kept in registers for the entire scan\n float h[16] = {0.0f};\n \n // Sequential scan over timesteps\n for (int t = 0; t < L; t++) {\n // Linear index into x, delta, y tensors (shape B, L, D)\n // Layout: [b, t, d] -> b*L*D + t*D + d\n int x_idx = b * L * D + t * D + d;\n \n float delta_val = delta[x_idx];\n float x_val = x[x_idx];\n \n // Accumulate output for this timestep\n // y[b, t, d] = sum_n(h[n] * C_t[b, t, n])\n float y_sum = 0.0f;\n \n // Process all N state dimensions\n // B_t, C_t shape: (B, L, N)\n // Layout: [b, t, n] -> (b*L + t)*N + n\n int base_idx = (b * L + t) * N;\n \n #pragma unroll\n for (int n = 0; n < N; n++) {\n int btn_idx = base_idx + n;\n \n // Compute dB = delta * B_t on the fly\n float dB_val = delta_val * B_t[btn_idx];\n \n // Compute x_db = x * dB on the fly\n float x_db_val = x_val * dB_val;\n \n // Compute dA = exp(delta * A) on the fly\n // A already contains -exp(A_log), so A_local[n] is negative\n float dA_val = expf(delta_val * A_local[n]);\n \n // Update hidden state: h = dA * h + x_db\n h[n] = dA_val * h[n] + x_db_val;\n \n // Accumulate output contribution\n y_sum += h[n] * C_t[btn_idx];\n }\n \n // Write output with skip connection\n // y = y + x * D\n y[x_idx] = y_sum + x_val * D_param[d];\n }\n }\n}\n\nvoid selective_scan_cuda(\n at::Tensor x,\n at::Tensor delta,\n at::Tensor B_t,\n at::Tensor C_t,\n at::Tensor A,\n at::Tensor D_param,\n at::Tensor y,\n int64_t B, int64_t L, int64_t D, int64_t N) {\n \n int64_t total = B * D;\n int threads = 256;\n int blocks = (total + threads - 1) / threads;\n \n // Limit blocks to avoid launch errors\n int max_blocks = 65535;\n blocks = min(blocks, max_blocks);\n \n selective_scan_kernel<<<blocks, threads>>>(\n x.data_ptr<float>(),\n delta.data_ptr<float>(),\n B_t.data_ptr<float>(),\n C_t.data_ptr<float>(),\n A.data_ptr<float>(),\n D_param.data_ptr<float>(),\n y.data_ptr<float>(),\n (int)B, (int)L, (int)D, (int)N\n );\n \n AT_CUDA_CHECK(cudaGetLastError());\n}\n\"\"\"\n \n cpp_source = \"\"\"\nvoid selective_scan_cuda(\n at::Tensor x,\n at::Tensor delta,\n at::Tensor B_t,\n at::Tensor C_t,\n at::Tensor A,\n at::Tensor D_param,\n at::Tensor y,\n int64_t B, int64_t L, int64_t D, int64_t N);\n\"\"\"\n \n self._cuda_module = load_inline(\n name='selective_scan_ext',\n cpp_sources=cpp_source,\n cuda_sources=cuda_source,\n functions=['selective_scan_cuda'],\n extra_cuda_cflags=['-O3', '-use_fast_math'],\n verbose=False\n )\n return self._cuda_module\n \n def forward(self, x: torch.Tensor) -> torch.Tensor:\n B, L, D = x.shape\n N = self.d_state\n \n # Compute projections (same as reference)\n delta = torch.nn.functional.softplus(self.proj_delta(x))\n B_t = self.proj_B(x)\n C_t = self.proj_C(x)\n \n # Compute A = -exp(A_log)\n A = -torch.exp(self.A_log)\n \n # Allocate output tensor\n y = torch.empty(B, L, D, device=x.device, dtype=x.dtype)\n \n # Get and call the CUDA kernel\n cuda_module = self._get_cuda_module()\n cuda_module.selective_scan_cuda(x, delta, B_t, C_t, A, self.D, y, B, L, D, N)\n \n return y" }
result ← submit_kernel (fail)
failsubmit_kernel submit_kernel FAILED: correctness check did not pass (? trials). runtime_error: Error building extension 'selective_scan_ext': [1/3] /opt/conda/bin/x86_64-conda-linux-gnu-c++ -MMD -MF main.o.d -DTORCH_EXTENSION_NAME=selective_scan_ext -DTORCH_API_INCLUDE_EXTENSION_H -isystem /...
final submitted kernel (191 lines, 6,275 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

class ModelNew(nn.Module):
    """
    Optimized Mamba S6 (Selective Scan) implementation using custom CUDA kernels.
    
    This implementation fuses the scan operation to keep intermediate values in registers,
    avoiding materializing dA, dB, and x_db to global memory.
    """
    
    def __init__(self, d_model: int, d_state: int):
        super(ModelNew, self).__init__()
        self.d_model = d_model
        self.d_state = d_state
        self.A_log = nn.Parameter(torch.randn(d_model, d_state))
        self.proj_B = nn.Linear(d_model, d_state, bias=False)
        self.proj_C = nn.Linear(d_model, d_state, bias=False)
        self.proj_delta = nn.Linear(d_model, d_model, bias=True)
        self.D = nn.Parameter(torch.ones(d_model))
        
        # Cache the compiled kernel
        self._cuda_module = None
    
    def _get_cuda_module(self):
        """Lazy-load and cache the CUDA kernel module."""
        if self._cuda_module is not None:
            return self._cuda_module
        
        cuda_source = """
#include <torch/extension.h>
#include <cuda.h>
#include <cuda_runtime.h>
#include <cmath>

// Optimized selective scan kernel
// Each thread processes one (batch, channel) pair across all timesteps
// This maximizes data reuse and keeps h in registers
__global__ void selective_scan_kernel(
    const float* __restrict__ x,        // (B, L, D)
    const float* __restrict__ delta,    // (B, L, D)
    const float* __restrict__ B_t,      // (B, L, N)
    const float* __restrict__ C_t,      // (B, L, N)
    const float* __restrict__ A,        // (D, N)
    const float* __restrict__ D_param,  // (D,)
    float* __restrict__ y,              // (B, L, D)
    int B, int L, int D, int N) {
    
    int tid = blockIdx.x * blockDim.x + threadIdx.x;
    int stride = blockDim.x * gridDim.x;
    int total = B * D;
    
    // Process each (b, d) pair
    for (int idx = tid; idx < total; idx += stride) {
        int b = idx / D;
        int d = idx % D;
        
        // Load A for this channel into registers (kept hot throughout scan)
        // A shape: (D, N), we need A[d, :]
        float A_local[16];  // Max N = 16
        #pragma unroll
        for (int n = 0; n < N; n++) {
            A_local[n] = A[d * N + n];
        }
        
        // Hidden state h (D, N) -> we only need h[d, :] = (N,)
        // Kept in registers for the entire scan
        float h[16] = {0.0f};
        
        // Sequential scan over timesteps
        for (int t = 0; t < L; t++) {
            // Linear index into x, delta, y tensors (shape B, L, D)
            // Layout: [b, t, d] -> b*L*D + t*D + d
            int x_idx = b * L * D + t * D + d;
            
            float delta_val = delta[x_idx];
            float x_val = x[x_idx];
            
            // Accumulate output for this timestep
            // y[b, t, d] = sum_n(h[n] * C_t[b, t, n])
            float y_sum = 0.0f;
            
            // Process all N state dimensions
            // B_t, C_t shape: (B, L, N)
            // Layout: [b, t, n] -> (b*L + t)*N + n
            int base_idx = (b * L + t) * N;
            
            #pragma unroll
            for (int n = 0; n < N; n++) {
                int btn_idx = base_idx + n;
                
                // Compute dB = delta * B_t on the fly
                float dB_val = delta_val * B_t[btn_idx];
                
                // Compute x_db = x * dB on the fly
                float x_db_val = x_val * dB_val;
                
                // Compute dA = exp(delta * A) on the fly
                // A already contains -exp(A_log), so A_local[n] is negative
                float dA_val = expf(delta_val * A_local[n]);
                
                // Update hidden state: h = dA * h + x_db
                h[n] = dA_val * h[n] + x_db_val;
                
                // Accumulate output contribution
                y_sum += h[n] * C_t[btn_idx];
            }
            
            // Write output with skip connection
            // y = y + x * D
            y[x_idx] = y_sum + x_val * D_param[d];
        }
    }
}

void selective_scan_cuda(
    at::Tensor x,
    at::Tensor delta,
    at::Tensor B_t,
    at::Tensor C_t,
    at::Tensor A,
    at::Tensor D_param,
    at::Tensor y,
    int64_t B, int64_t L, int64_t D, int64_t N) {
    
    int64_t total = B * D;
    int threads = 256;
    int blocks = (total + threads - 1) / threads;
    
    // Limit blocks to avoid launch errors
    int max_blocks = 65535;
    blocks = min(blocks, max_blocks);
    
    selective_scan_kernel<<<blocks, threads>>>(
        x.data_ptr<float>(),
        delta.data_ptr<float>(),
        B_t.data_ptr<float>(),
        C_t.data_ptr<float>(),
        A.data_ptr<float>(),
        D_param.data_ptr<float>(),
        y.data_ptr<float>(),
        (int)B, (int)L, (int)D, (int)N
    );
    
    AT_CUDA_CHECK(cudaGetLastError());
}
"""
        
        cpp_source = """
void selective_scan_cuda(
    at::Tensor x,
    at::Tensor delta,
    at::Tensor B_t,
    at::Tensor C_t,
    at::Tensor A,
    at::Tensor D_param,
    at::Tensor y,
    int64_t B, int64_t L, int64_t D, int64_t N);
"""
        
        self._cuda_module = load_inline(
            name='selective_scan_ext',
            cpp_sources=cpp_source,
            cuda_sources=cuda_source,
            functions=['selective_scan_cuda'],
            extra_cuda_cflags=['-O3', '-use_fast_math'],
            verbose=False
        )
        return self._cuda_module
    
    def forward(self, x: torch.Tensor) -> torch.Tensor:
        B, L, D = x.shape
        N = self.d_state
        
        # Compute projections (same as reference)
        delta = torch.nn.functional.softplus(self.proj_delta(x))
        B_t = self.proj_B(x)
        C_t = self.proj_C(x)
        
        # Compute A = -exp(A_log)
        A = -torch.exp(self.A_log)
        
        # Allocate output tensor
        y = torch.empty(B, L, D, device=x.device, dtype=x.dtype)
        
        # Get and call the CUDA kernel
        cuda_module = self._get_cuda_module()
        cuda_module.selective_scan_cuda(x, delta, B_t, C_t, A, self.D, y, B, L, D, N)
        
        return y