correct
9_AssociativeScan.py
started
2026-05-05T00:33:38Z
timingkernelbench.timing
kernel mean / median / std
0.26 / — / 7.99e-04 μs
ref mean / median / std
79.00 / — / 5.38 μs
numerical precisioneval_kernel_against_ref
gpu memoryextended_metrics.measure_memory
kernel launches / fusiontorch.profiler
fusion ratio (ref / kernel)
12291.50x
SOL (speed-of-light)nsight_hardware_counters
roofline / occupancynsight_hardware_counters
dram bandwidth
1555.13 GB/s
warp stalls (top 5)nsight
reference PyTorch source (the prompt the agent sees)
import torch
import torch.nn as nn
class Model(nn.Module):
"""
Performs a parallel associative scan (linear recurrence) over a 1D sequence.
Given coefficient a and input b at each timestep, computes:
h_0 = b_0
h_t = a_t * h_{t-1} + b_t for t > 0
This is the core parallelizable primitive underlying Mamba's selective scan and
other SSM architectures. Sequential in naive PyTorch but can be parallelized
via the associative scan algorithm in O(L log L) span.
Args:
d_model (int): Number of independent recurrence channels.
"""
def __init__(self, d_model: int):
super(Model, self).__init__()
self.d_model = d_model
def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
B, L, D = a.shape
h = torch.zeros(B, D, device=a.device, dtype=a.dtype)
outputs = []
for t in range(L):
h = a[:, t] * h + b[:, t]
outputs.append(h)
return torch.stack(outputs, dim=1)
batch_size = 32
d_model = 256
seq_len = 4096
def get_inputs():
a = torch.sigmoid(torch.randn(popcorn_pri.jitter_int(batch_size), popcorn_pri.jitter_int(seq_len), d_model))
b = torch.randn(popcorn_pri.jitter_int(batch_size), popcorn_pri.jitter_int(seq_len), d_model)
return [a, b]
def get_init_inputs():
return [d_model]turn 0 · FINAL · 1 tool call(s) · 88.6s turn · 88.6s LLM · 6,684 chars (click to expand)
turn 088.6s turn · 88.6s LLM · 0.0s tools · FINAL
reasoning
**Estimating performance and execution**
I'm estimating the performance of operations in this sequence. It seems like a direct calculation shows 49k operations for a certain configuration compared to 33 million. The time for these operations could potentially be around 0.3ms, but the usage might peak around 1ms. PyTorch's performance could exceed 100ms, but I want to consider methods like using two kernels with prefix block chunks without needing to complicate things. I'll also think about how function calls like PyTorch's multiplication and addition might handle rounding differently.
**Exploring rounding options**
To ensure accurate calculations, I might disable fmad or opt for separate rounding functions like __fmul_rn and __fadd_rn. For instance, using __fadd_rn(__fmul_rn(av, h), bv) would align with the expected behavior of performing a rounded multiply followed by a rounded add. It’s important that this method matches the floating-point precision in PyTorch, which seems like it can achieve the exact results I’m aiming for. I want to double-check this approach!
**Considering loop dependency issues**
I’m grappling with the loop dependency here, but I see that my memory load addresses are known. The compiler might struggle with overlapping due to the FMA operation, loading the next instruction only after the store. Actually, it's possible to increase blocks by splitting the data into smaller chunks, which could allow for one warp block for each 32 elements of data. That might help with the performance!
**Considering block handling**
I’m realizing that a block can handle two batches for the same d tile! So, lanes 0-15 process batch b0, while lanes 16-31 deal with batch b1. It's interesting that all lanes are active, and warp memory has two 64B contiguous chunks that might be separated by L*D. I’ll need to double-check this separation and see how it affects performance and transactions. There’s a lot going on here!
**Compiling with CUDA**
I need to consider the use of a torch extension, which is fine. If the auto-binding function is defined in the CUDA source, the linker should see it. I should ensure the function is non-static for this to work. It might be a good idea to compile with the default architecture as well. Alright, I think I’m ready to submit the tool with the code now!
call → submit_kernel
callsubmit_kernel
{
"kernel_code": "import torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\n_cpp_src = r\"\"\"\ntorch::Tensor recurrence_forward(torch::Tensor a, torch::Tensor b);\n\"\"\"\n\n_cuda_src = r\"\"\"\n#include <torch/extension.h>\n#include <ATen/cuda/CUDAContext.h>\n#include <c10/cuda/CUDAGuard.h>\n\n__global__ __launch_bounds__(32, 16)\nvoid recurrence_kernel(const float* __restrict__ a,\n const float* __restrict__ b,\n float* __restrict__ out,\n int B, int L, int D) {\n // One warp processes one contiguous tile of 32 channels for one batch item.\n // This preserves fully coalesced memory accesses for the [B,L,D] layout while\n // each lane performs the dependent recurrence for its channel.\n int lane = threadIdx.x; // blockDim.x == 32\n int tiles_per_batch = (D + 31) >> 5;\n int tile = blockIdx.x;\n int batch = tile / tiles_per_batch;\n int d0 = (tile - batch * tiles_per_batch) << 5;\n int d = d0 + lane;\n if (batch >= B || d >= D) return;\n\n int idx = (batch * L * D) + d;\n float h = 0.0f;\n\n int t = 0;\n // small manual unroll; L is 4096 in the benchmark\n for (; t + 3 < L; t += 4) {\n float av0 = a[idx];\n float bv0 = b[idx];\n h = __fadd_rn(__fmul_rn(av0, h), bv0);\n out[idx] = h;\n idx += D;\n\n float av1 = a[idx];\n float bv1 = b[idx];\n h = __fadd_rn(__fmul_rn(av1, h), bv1);\n out[idx] = h;\n idx += D;\n\n float av2 = a[idx];\n float bv2 = b[idx];\n h = __fadd_rn(__fmul_rn(av2, h), bv2);\n out[idx] = h;\n idx += D;\n\n float av3 = a[idx];\n float bv3 = b[idx];\n h = __fadd_rn(__fmul_rn(av3, h), bv3);\n out[idx] = h;\n idx += D;\n }\n for (; t < L; ++t) {\n float av = a[idx];\n float bv = b[idx];\n h = __fadd_rn(__fmul_rn(av, h), bv);\n out[idx] = h;\n idx += D;\n }\n}\n\ntorch::Tensor recurrence_forward(torch::Tensor a, torch::Tensor b) {\n const int B = (int)a.size(0);\n const int L = (int)a.size(1);\n const int D = (int)a.size(2);\n auto out = torch::empty_like(a);\n c10::cuda::CUDAGuard device_guard(a.device());\n int tiles_per_batch = (D + 31) >> 5;\n dim3 grid(B * tiles_per_batch);\n dim3 block(32);\n cudaStream_t stream = at::cuda::getCurrentCUDAStream();\n recurrence_kernel<<<grid, block, 0, stream>>>(\n a.data_ptr<float>(), b.data_ptr<float>(), out.data_ptr<float>(), B, L, D);\n return out;\n}\n\"\"\"\n\n_ext = load_inline(\n name=\"recurrence_scan_warp_ext\",\n cpp_sources=_cpp_src,\n cuda_sources=_cuda_src,\n functions=[\"recurrence_forward\"],\n extra_cflags=[\"-O3\"],\n extra_cuda_cflags=[\"-O3\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self, d_model: int):\n super(ModelNew, self).__init__()\n self.d_model = d_model\n\n def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:\n return _ext.recurrence_forward(a, b)\n"
}
result ← submit_kernel (ok)
oksubmit_kernel
submit_kernel PASSED: (5 / 5) correctness trials all passed.
Kernel runtime: 0.26 μs
Runtime stats: mean=0.26μs median=?μs std=0.00μs
Numerical precision: max_abs_err=0.00e+00 mean_abs_err=0.00e+00 max_rel_err=0.00e+00
Memory: 417.0 MB peak (ref: 545.0 MB, ratio: 0.7651x)
Kernel launches: 2 (ref: 24583, fusion ratio: 12291.5)
SOL score: 0.4642
Flagged for excessive speedup — this submission may have been rejected by automated review.
final submitted kernel (102 lines, 2,996 chars) — full source, untruncated
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline
_cpp_src = r"""
torch::Tensor recurrence_forward(torch::Tensor a, torch::Tensor b);
"""
_cuda_src = r"""
#include <torch/extension.h>
#include <ATen/cuda/CUDAContext.h>
#include <c10/cuda/CUDAGuard.h>
__global__ __launch_bounds__(32, 16)
void recurrence_kernel(const float* __restrict__ a,
const float* __restrict__ b,
float* __restrict__ out,
int B, int L, int D) {
// One warp processes one contiguous tile of 32 channels for one batch item.
// This preserves fully coalesced memory accesses for the [B,L,D] layout while
// each lane performs the dependent recurrence for its channel.
int lane = threadIdx.x; // blockDim.x == 32
int tiles_per_batch = (D + 31) >> 5;
int tile = blockIdx.x;
int batch = tile / tiles_per_batch;
int d0 = (tile - batch * tiles_per_batch) << 5;
int d = d0 + lane;
if (batch >= B || d >= D) return;
int idx = (batch * L * D) + d;
float h = 0.0f;
int t = 0;
// small manual unroll; L is 4096 in the benchmark
for (; t + 3 < L; t += 4) {
float av0 = a[idx];
float bv0 = b[idx];
h = __fadd_rn(__fmul_rn(av0, h), bv0);
out[idx] = h;
idx += D;
float av1 = a[idx];
float bv1 = b[idx];
h = __fadd_rn(__fmul_rn(av1, h), bv1);
out[idx] = h;
idx += D;
float av2 = a[idx];
float bv2 = b[idx];
h = __fadd_rn(__fmul_rn(av2, h), bv2);
out[idx] = h;
idx += D;
float av3 = a[idx];
float bv3 = b[idx];
h = __fadd_rn(__fmul_rn(av3, h), bv3);
out[idx] = h;
idx += D;
}
for (; t < L; ++t) {
float av = a[idx];
float bv = b[idx];
h = __fadd_rn(__fmul_rn(av, h), bv);
out[idx] = h;
idx += D;
}
}
torch::Tensor recurrence_forward(torch::Tensor a, torch::Tensor b) {
const int B = (int)a.size(0);
const int L = (int)a.size(1);
const int D = (int)a.size(2);
auto out = torch::empty_like(a);
c10::cuda::CUDAGuard device_guard(a.device());
int tiles_per_batch = (D + 31) >> 5;
dim3 grid(B * tiles_per_batch);
dim3 block(32);
cudaStream_t stream = at::cuda::getCurrentCUDAStream();
recurrence_kernel<<<grid, block, 0, stream>>>(
a.data_ptr<float>(), b.data_ptr<float>(), out.data_ptr<float>(), B, L, D);
return out;
}
"""
_ext = load_inline(
name="recurrence_scan_warp_ext",
cpp_sources=_cpp_src,
cuda_sources=_cuda_src,
functions=["recurrence_forward"],
extra_cflags=["-O3"],
extra_cuda_cflags=["-O3"],
verbose=False,
)
class ModelNew(nn.Module):
def __init__(self, d_model: int):
super(ModelNew, self).__init__()
self.d_model = d_model
def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
return _ext.recurrence_forward(a, b)