Kimi-K2.6 · popcorn · l12_default_gpt
correct 15_GQAKVHeadExpansionAttention.py
model
Kimi-K2.6
turns / max
8 / 10
tool calls
8
tokens (total)
0
wall clock
—
truncated
no
backend
cuda fp32
variant
popcorn
runtime
0.02 μs
ref runtime
100.00 μs
speedup
6289.31x
started
2026-05-02T16:46:32Z
timingkernelbench.timing
kernel runtime
0.02 μs
ref runtime
100.00 μs
speedup
6289.31x
kernel mean / median / std
0.02 / — / 2.73e-04 μs
ref mean / median / std
100.00 / — / 1.19 μs
numerical precisioneval_kernel_against_ref
max abs error
8.34e-07
mean abs error
3.22e-08
max rel error
0.13
mean rel error
1.46e-06
gpu memoryextended_metrics.measure_memory
kernel peak
33.39 MB
ref peak
33.71 MB
kernel / ref
0.99x
kernel launches / fusiontorch.profiler
kernel launches
2
ref launches
48603
fusion ratio (ref / kernel)
24301.50x
total cuda time
63 μs
SOL (speed-of-light)nsight_hardware_counters
SOL score (0–1)
0.140
dram util %
0.6%
compute util %
14.0%
bottleneck
compute-bound
dominant pipe
FP32
arithmetic intensity
—
ridge point
20.00
roofline / occupancynsight_hardware_counters
dram bandwidth
19.49 GB/s
dram util %
0.6%
fp32 tflops
—
fp32 util %
—
fp16 tflops
—
occupancy %
14.6%
bottleneck
compute-bound
dominant pipe %
14.0%
arithmetic intensity
—
ridge point
20.00
L1 hit %
—
L2 hit %
84.9%
ld sectors / req
4.00
st sectors / req
—
regs / thread
40
smem / block
6144 B
block size
256
peak bw
3350.00 GB/s
peak fp32 tflops
67.00
peak fp16 tflops
1979.00
warp stalls (top 5)nsight
long_scoreboard
235.6%
short_scoreboard
129.2%
mio_throttle
9.3%
flagssubmit_kernel guard
excessive speedup
flagged
reference PyTorch source (the prompt the agent sees)
import math

import torch
import torch.nn as nn


class Model(nn.Module):
    """
    Causal grouped-query attention with KV-head expansion.

    Query heads outnumber KV heads. Keys and values are expanded by repeating
    each KV head across its assigned query-head group before causal attention.
    """

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

    def forward(
        self,
        q: torch.Tensor,
        k: torch.Tensor,
        v: torch.Tensor,
    ) -> torch.Tensor:
        batch_size, seq_len, num_q_heads, head_dim = q.shape
        num_kv_heads = k.shape[2]
        group_size = num_q_heads // num_kv_heads

        expanded_k = k.repeat_interleave(group_size, dim=2)
        expanded_v = v.repeat_interleave(group_size, dim=2)

        out = torch.zeros_like(q)
        scale = 1.0 / math.sqrt(head_dim)

        for b in range(batch_size):
            for h in range(num_q_heads):
                for t in range(seq_len):
                    q_t = q[b, t, h]
                    k_hist = expanded_k[b, : t + 1, h]
                    v_hist = expanded_v[b, : t + 1, h]
                    scores = (k_hist * q_t.unsqueeze(0)).sum(dim=-1) * scale
                    attn = torch.softmax(scores, dim=0)
                    out[b, t, h] = (attn.unsqueeze(-1) * v_hist).sum(dim=0)

        return out


batch_size = 2
seq_len = 80
num_q_heads = 8
num_kv_heads = 2
head_dim = 32


def get_inputs():
    p = popcorn_pri
    q = torch.randn(p.jitter_int(batch_size), p.jitter_int(seq_len), p.jitter_int(num_q_heads), p.jitter_int(head_dim, align=8), dtype=torch.float32)
    k = torch.randn(p.jitter_int(batch_size), p.jitter_int(seq_len), p.jitter_int(num_kv_heads), p.jitter_int(head_dim, align=8), dtype=torch.float32)
    v = torch.randn(p.jitter_int(batch_size), p.jitter_int(seq_len), p.jitter_int(num_kv_heads), p.jitter_int(head_dim, align=8), dtype=torch.float32)
    return [q, k, v]


def get_init_inputs():
    return []
turn 0 · 1 tool call(s) · 42.8s turn · 42.8s LLM · 15,037 chars (click to expand)
turn 042.8s turn · 42.8s LLM · 0.0s tools
reasoning
Let me analyze the reference implementation. It computes causal grouped-query attention with KV-head expansion. The forward method: 1. Takes q, k, v tensors 2. Expands k and v by repeating each KV head across its group 3. Computes causal attention per batch, per query head, per timestep 4. Uses a for-loop in Python (very slow) The shapes are: - q: [batch_size, seq_len, num_q_heads, head_dim] - k, v: [batch_size, seq_len, num_kv_heads, head_dim] - group_size = num_q_heads // num_kv_heads Constraints from the problem: - batch_size = 2 - seq_len = 80 - num_q_heads = 8 - num_kv_heads = 2 - head_dim = 32 - So group_size = 4 This means k and v have heads [0, 1], and query heads [0,1,2,3] map to kv head 0, [4,5,6,7] map to kv head 1. The naive implementation uses nested Python loops and processes each token sequentially. A custom CUDA kernel can parallelize across batch, query heads, and time steps. Since seq_len is only 80 and head_dim is 32, this is a relatively small problem. The causal mask means each position t only attends to positions 0..t. A good approach: 1. Fuse the whole computation into one kernel 2. Parallelize across (batch, num_q_heads, seq_len) - one thread block per (batch, head), with threads processing different t 3. Or even one thread per (batch, head, t) but need to handle reduction and softmax. 4. For small seq_len (80), we can compute everything in registers/shared memory. Given that group_size = 4, num_kv_heads = 2, num_q_heads = 8, batch = 2, seq_len = 80, head_dim = 32. For each (b, h_q, t): - Determine kv_head = h_q / group_size - Load q[b, t, h_q, :] (32 floats) - For i=0..t: compute score[i] = dot(q, k[b, i, kv_head, :]) * scale - Find max score, compute softmax denominator and attention weights - Compute out = sum_i attn[i] * v[b, i, kv_head, :] Since seq_len is only 80, each thread can handle one (b, h_q, t). But that would be 2*8*80 = 1280 threads, which is fine. However, t ranges up to 80, and the inner loop length varies with t. Wait - actually if we parallelize over (b, h_q, t), then each thread does a sequential loop over i=0..t. The longest loop is 80 iterations, with each iteration doing a dot product of length 32, followed by softmax and weighted sum over i=0..t. 80 * (32 mul-adds + load/store) per thread. With 1280 threads total, this might be okay. But we can do better. Alternative: parallelize over (b, kv_head, t) or (b, h_q) and have threads cooperate within a block to handle all t for a given head. Actually, since seq_len is 80, and head_dim is 32, let's think about memory bandwidth vs compute. Sequence length is small so a kernel with one thread per (b, h, t) is probably fine and easiest to implement correctly. But we might want to use shared memory or register tiling for better performance. Wait, there's another consideration: if we do one thread per (b, h, t), each thread loads q once (32 floats), then loops over t+1 k's and v's. Since t is the query position and i is the key position. For t=79, we load 80 k vectors and 80 v vectors. Total memory per thread: 32 + 80*32 + 80*32 ≈ 5000 floats ≈ 20KB per thread. That's way too much for registers. It will spill to local memory or just use global memory loads. But since seq_len is only 80 and head_dim is 32, we can load the entire k and v for a (b, kv_head) into shared memory! For one block handling one (b, kv_head): - k[seq_len, head_dim] = 80 * 32 = 2560 floats = 10KB - v[seq_len, head_dim] = 80 * 32 = 2560 floats = 10KB Total 20KB per block, well within shared memory limits (usually 48KB or more per SM). Then each thread in the block can handle one or more (h_q, t) positions where h_q maps to this kv_head. For a given (b, kv_head): - There are group_size query heads = 4 - seq_len = 80 time steps - Total work units = 4 * 80 = 320 We can launch a block with, say, 320 threads, or fewer threads each handling multiple positions. Let's design: - Grid: (batch_size, num_kv_heads) blocks - Block: 256 or 320 threads - Each block loads its k [seq_len, head_dim] and v [seq_len, head_dim] into shared memory - Threads are mapped to (query_head_in_group, t) - group_size = 4, seq_len = 80, so 320 positions. Block size 320 would map exactly. - But 320 threads might be a bit unusual; 256 is also possible with some threads handling multiple. Actually, blockDim.x = 128, and each thread handles 320/128 ≈ 2.5, we could have some handle 2, some 3. Or just use blockDim.x = 320? That's allowed I think (max is usually 1024). But warps are 32 threads, so 320 = 10 warps, which is fine. For each (h_in_group, t) processed by a thread: 1. Load q[b, t, kv_head*group_size + h_in_group, :] from global memory into registers 2. Compute scores[i] for i=0..t: dot(q, k_shared[i, :]) 3. Softmax over scores[0..t] 4. Compute output = sum_i attn[i] * v_shared[i, :] For step 2, since head_dim=32, we can manually unroll the dot product or just use a loop. With 32 elements, a loop is fine. Wait - if we precompute all the scores in registers, we need an array of 80 floats. That's okay, 80 floats = 320 bytes in registers per thread. But actually we only need to do online softmax so we don't need to store all scores. Online softmax algorithm: - max_val = -inf - sum_val = 0 For i=0..t: s = dot(q, k[i]) * scale new_max = max(max_val, s) sum_val = sum_val * exp(max_val - new_max) + exp(s - new_max) max_val = new_max Then to compute output: - We could do two passes: first pass for max and sum, second pass to accumulate output. - Or we can store scores in registers and do it in two passes. Given seq_len=80, storing 80 floats per thread is fine. But registers might get spilled if we have many threads. 320 threads * 80 floats = 25600 floats total per block. Actually no - register allocation is per thread. Each thread needs ~80 floats for scores + a few temporaries. 80 * 4 bytes = 320 bytes per thread. With 320 threads that's... wait registers are per-thread. Each SM has a register file of 64K or 256K 32-bit registers. 320 threads * (say) 100 registers = 32K registers. That's fine. But storing the entire score array and doing two passes requires two loops. Alternatively, online softmax with a running accumulator of output is trickier because the output depends on the softmax weights which depend on the max. We can do a first pass to compute softmax weights and store them, then second pass to compute output. Actually, simpler: store scores in a local array of size seq_len (80). Then compute max, compute sum, compute softmax, compute weighted sum of v. Wait, if we do 2 passes over the sequence, it's 2 * 80 = 160 iterations of dot product. With head_dim=32, each dot product is 32 FMAs. Total per thread: 160 * 32 = 5120 FMAs, plus loads. That's still tiny. Let's just do two passes. Or actually, let's compute all scores in one loop and store them, then compute max and sum in the same loop, then do another loop for the weighted sum. Better: loop to compute scores and store, loop to compute max, loop to compute softmax and accumulate output? That's 3 loops. Or store scores, then compute max and sum in one go, then accumulate output. Sequence length is small. Let's keep it simple: 1. For i=0..t: compute score[i] = dot(q, k[i]) * scale, store in local array 2. Find max_score among score[0..t] 3. Compute exp(score[i] - max_score) and sum them up 4. For i=0..t: accumulate (exp(score[i] - max_score) / sum) * v[i] to output 4 passes over 0..t. For t up to 79, 4*80 = 320 iterations. Each iteration is either a dot product of length 32 (step 1) or a vector add of length 32 (step 4). Step 2 and 3 are cheap. Actually step 4: for each i, we need to load the softmax weight (a scalar) and multiply-add the vector v[i] (length 32). That's 32 FMAs per i. Step 1 is also 32 FMAs per i. Total FMAs: 2 * (t+1) * head_dim. Max: 2 * 80 * 32 = 5120 FMAs. At peak 20 TFLOPS, this is 0.25 microseconds of compute. Memory bandwidth is the bottleneck. Let's check memory: For one (b, h, t), we read: - q: head_dim = 32 floats - k[:t+1]: (t+1)*head_dim floats - v[:t+1]: (t+1)*head_dim floats Total: ~2*(t+1)*head_dim + head_dim floats. Average t is 40, so ~2*40*32 = 2560 floats per thread. With 1280 threads, total reads from k and v are shared across threads within the same (b, kv_head) - wait, if threads are for different t but same (b, h), they read different k[i] but same k[i] is read by all t. If we use one thread per (b, h, t), memory reads are not well coalesced and there's redundancy. Every thread for the same (b, h) reads k[0..t] so k[0] is read by all t threads. That's redundant but cached. Given the small size, L2 cache might help. A better approach: one block per (b, h), with threads cooperating to compute all t outputs. For example: - Block handles one (b, h) - Threads load q for all t? No, q is [seq_len, head_dim] = 80*32 = 2560 floats = 10KB. - Threads load k and v for the kv_head [seq_len, head_dim] each into shared memory = 20KB total. - Then each thread handles one or multiple t values. This is good because k and v are in shared memory, but q is still in global memory. We can also load q into shared memory or just read from global memory since each q[t] is only read by the thread handling that t. Actually mapping: - Grid: (batch_size, num_q_heads) blocks - Block: say 128 threads - Each block handles all t for one (b, h) - Shared memory: K[seq_len][head_dim], V[seq_len][head_dim] - Threads load K and V cooperatively: each thread loads some elements. - Then each thread is assigned some t indices. For 128 threads and 80 t, some threads handle 1 t. - Wait, 128 threads for 80 positions means some threads are idle or we can have each thread handle ceil(80/128) = 1 position with 48 threads idle... Actually we can just have block size = 64 or 32 and each thread handles multiple. Let's think: with block size = 32 (one warp), each thread handles 80/32 ≈ 2-3 positions. But then we lose parallelism within the block for loading shared memory. Block size 256 might be better for occupancy but we only have 2*8=16 blocks total. 16 blocks of 256 threads = 4096 threads. If GPU has 80+ SMs, many SMs will be idle. But that's fine for correctness. However, for performance on small problems, we want to saturate the GPU. 16 blocks might not be enough if each block takes long. With blockDim=256, we'd have 16 blocks. 16 blocks can run on 16 SMs at most. If GPU has 80+ SMs, 64 are idle. But it might still be much faster than CPU. Alternatively, we can process multiple (b, h) per block? Or have smaller blocks so more blocks exist? Actually the number of blocks is fixed at 16. We can't increase it beyond batch*heads = 16. Unless we split the seq_len dimension into multiple blocks. Splitting seq_len: we can have blocks for (b, h, chunk_of_seq). If chunk size is 16, then we have 16 * 5 = 80 blocks. Or chunk size 10 gives 80/10=8 chunks, 16*8=128 blocks. More blocks might help occupancy but might increase overhead. For such a small seq_len, though, I think a thread per (b, h, t) is the most natural. Let me reconsider. With one thread per (b, h, t): - Total threads: 2 * 8 * 80 = 1280 threads. - We can map them as a 3D grid or 1D grid of 128 threads per block -> 10 blocks. Wait, if we use 1D grid: total threads = 1280, with block size 128, that's 10 blocks. 10 blocks! Even worse for SM utilization. But with block size 256, that's 5 blocks. Actually we can use block size 128 and have 10 blocks. But we can increase parallelism by using 2D blocks: blockDim.x = seq_len (80), blockDim.y = group_size? Hmm. Actually, for grouped query attention, multiple query heads share the same kv head. So we could have a block process (b, kv_head), and the threads in the block process all query heads in the group and all time steps. Let's analyze: For (b, kv_head): - group_size query heads - seq_len time steps - Total (h, t) pairs: group_size * seq_len = 4 * 80 = 320 We could set up a 2D block of, say, (32, 10) = 320 threads. Then grid is (batch_size, num_kv_heads) = (2, 2) = 4 blocks total. That's only 4 blocks. Not great. Instead, if we make the block 1D of 320 threads, still 4 blocks. If we make block smaller, say 64 threads, then each block handles 5 positions per thread, still 4 blocks. We can't change the number of blocks from 4 for this mapping. What if we process (b, h) with block having threads that process steps sequentially or in parallel? (b, h) gives 16 pairs. If block size is 128, 16 blocks total. If we further split seq_len into chunks... Actually for very small workloads, the overhead of kernel launch and memory reads dominates. The PyTorch reference is pure Python loops, so any CUDA kernel will be orders of magnitude faster. But the evaluation probably compares to a faster PyTorch reference too (maybe torch.compile or using standard scaled_dot_product_attention). Wait, the problem says "faster than the reference". The reference is the PyTorch model given. But usually, they implement a PyTorch reference internally that might be optimized. Looking at typical evaluations, the reference is usually the Python loop version run on CPU or single-threaded CUDA? Actually, let me re-read. "Run faster than the PyTorch reference." The reference provided is a naive Python loop. But in evaluation, they likely run this exact naive PyTorch code (maybe on GPU since tensors are on GPU). This naive code is extremely slow because it loops in Python. So even a basic CUDA kernel will be much faster. However, I should still write a well-optimized kernel to maximize speedup. Let me look at the GPU specs first. This will help decide the strategy.
call → get_gpu_specs
callget_gpu_specs {}
result ← get_gpu_specs (ok)
okget_gpu_specs GPU specs for NVIDIA H100 80GB HBM3: total memory (runtime): 79.1 GB spec entry: H100 GPU Architecture: Hopper GPU Memory: 80GB Memory Bandwidth: 3.35 TB/s FP64 TFLOPS: 34 FP64 Tensor Core TFLOPS: 67 FP32 TFLOPS: 67 TF32 Tensor Core TFLOPS: 989 with sparsity BFLOAT16 Tensore Core TFLOPS: 1979 with sparsity FP16 Tensor Core TFLOPS: 1979 with sparsity FP8 Tensor Core TFLOPS: 3958 with sparsity INT8 Tensor Core TOPS: 3958 with sparsity Register File Size: 64K 32-bit registers per SM Maximum number of registers per thread: 255 Maximum number of thread blocks per SM: 32 Shared memory capacity per SM: 228 KB Maximum shared memory per thread block: 227 KB
turn 1 · 1 tool call(s) · 480.9s turn · 480.9s LLM · 86,278 chars (click to expand)
turn 1480.9s turn · 480.9s LLM · 0.0s tools
reasoning
Let me analyze this problem. This is a causal grouped-query attention (GQA) with KV-head expansion. The reference implementation is extremely inefficient with triple nested Python loops. Key dimensions from the reference: - batch_size = 2 - seq_len = 80 - num_q_heads = 8 - num_kv_heads = 2 - head_dim = 32 - group_size = 8 / 2 = 4 So we need to: 1. Expand k and v by repeat_interleave(group_size, dim=2) 2. Compute causal attention for each query head Let me think about how to write an efficient CUDA kernel for this. First, let me think about an efficient PyTorch implementation to compare against: ```python def forward(q, k, v): B, T, H, D = q.shape num_kv_heads = k.shape[2] group_size = H // num_kv_heads # Expand k and v: [B, T, H_kv, D] -> [B, T, H, D] k = k.repeat_interleave(group_size, dim=2) v = v.repeat_interleave(group_size, dim=2) scale = 1.0 / math.sqrt(D) # Compute attention scores: [B, H, T, T] scores = torch.einsum('bthd,bshd->bhts', q, k) * scale # Apply causal mask mask = torch.triu(torch.ones(T, T, device=q.device), diagonal=1).bool() scores = scores.masked_fill(mask, float('-inf')) # Softmax attn = torch.softmax(scores, dim=-1) # Output: [B, H, T, D] out = torch.einsum('bhts,bshd->bthd', attn, v) return out ``` Wait, but the reference does a per-head attention, and the causal mask means each position t only attends to positions 0..t. Let me check the reference more carefully: ```python for b in range(batch_size): for h in range(num_q_heads): for t in range(seq_len): q_t = q[b, t, h] k_hist = expanded_k[b, : t + 1, h] v_hist = expanded_v[b, : t + 1, h] scores = (k_hist * q_t.unsqueeze(0)).sum(dim=-1) * scale attn = torch.softmax(scores, dim=0) out[b, t, h] = (attn.unsqueeze(-1) * v_hist).sum(dim=0) ``` The issue with using `einsum` + `masked_fill` + `softmax` is that the softmax over the full T dimension with `-inf` mask is numerically equivalent to softmax over the causal part (if `-inf` gives 0 after softmax and doesn't cause NaN in the output). Let me verify: For position t, we softmax over scores[0..T-1]. If scores[t+1..T-1] = -inf, then exp(-inf) = 0, and softmax over 0..t is equivalent to full softmax with -inf mask. The denominator would be sum_{i=0}^t exp(score_i) + sum_{i=t+1}^{T-1} 0 = sum_{i=0}^t exp(score_i), which is correct. Also, the output would be sum_{i=0}^t attn_i * v_i + sum_{i=t+1}^{T-1} 0 * v_i, which is also correct. So yes, this is equivalent as long as there are no numerical issues from -inf. However, the reference implementation also doesn't need all those Python loops. PyTorch's softmax handles -inf correctly. But writing a custom CUDA kernel might be faster for this small size. With B=2, T=80, H=8, D=32, the problem is relatively small. Let me think about a custom CUDA kernel approach. I could use a fused kernel that computes the causal attention per query token. For a single query position t, I need to: 1. Compute q_t · k_s for s = 0..t 2. Scale by 1/sqrt(D) 3. Softmax over the results 4. Compute weighted sum of v_s Since the sequence length is only 80 and head_dim is 32, I can consider different parallelization strategies: Strategy 1: One thread per (batch, head, time) computing the full causal attention for that query token. With 2*8*80 = 1280 threads, this would use enough parallelism. Each thread does a reduction over D for up to 80 keys (for t=79), which is manageable. Strategy 2: Use shared memory and warp-level reductions for more efficiency. Given the small sizes (T=80, D=32), let me think about what tiling/batching strategy works best. For a single (b, h, t) position: - We need to compute dot products of q_t with k_0...k_t - D=32, so we can load q_t into registers (32 floats) - Then iterate over s from 0 to t, computing dot(q_t, k_s) - Apply scale, softmax, then compute weighted sum of v_s If I use one warp (32 threads) per (b, h, t), I could compute the dot product in parallel across D dimensions, then warp-reduce to get the score. But since there's a sequential dependency in softmax (need max then exp then sum), this might be complex. Simpler approach: one thread per (b, h, t). Since we have 1280 such threads and each does up to ~80*32 ≈ 2560 FLOPs, total FLOPs ≈ 1280 * 2560 ≈ 3.3M, which is tiny. The overhead of thread management will matter less than memory bandwidth and arithmetic. Wait, actually the PyTorch reference with the Python loop is going to be extremely slow - on the order of seconds. A custom CUDA kernel should easily beat it. Even a simple PyTorch einsum implementation would beat it by a huge margin. Let me just write a reasonably optimized custom kernel. Let me think about how to write the CUDA kernel: ```cpp __global__ void gqa_causal_attention_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int batch_size, int seq_len, int num_q_heads, int num_kv_heads, int head_dim, int group_size, float scale ) { int b = blockIdx.x; int h = blockIdx.y; int t = threadIdx.x + blockIdx.z * blockDim.x; if (b >= batch_size || h >= num_q_heads || t >= seq_len) return; int kv_head = h / group_size; // Pointers to q_t, and the start of k/v for this batch and kv head const float* q_ptr = q + ((b * seq_len + t) * num_q_heads + h) * head_dim; // Compute scores for s = 0..t float scores[80]; // seq_len max float max_score = -INFINITY; for (int s = 0; s <= t; s++) { const float* k_ptr = k + ((b * seq_len + s) * num_kv_heads + kv_head) * head_dim; float dot = 0.0f; for (int d = 0; d < head_dim; d++) { dot += q_ptr[d] * k_ptr[d]; } scores[s] = dot * scale; if (scores[s] > max_score) max_score = scores[s]; } // Softmax float sum_exp = 0.0f; for (int s = 0; s <= t; s++) { scores[s] = expf(scores[s] - max_score); sum_exp += scores[s]; } // Normalize for (int s = 0; s <= t; s++) { scores[s] /= sum_exp; } // Compute output float out_vec[32]; // head_dim for (int d = 0; d < head_dim; d++) { out_vec[d] = 0.0f; } for (int s = 0; s <= t; s++) { const float* v_ptr = v + ((b * seq_len + s) * num_kv_heads + kv_head) * head_dim; float attn = scores[s]; for (int d = 0; d < head_dim; d++) { out_vec[d] += attn * v_ptr[d]; } } // Write back float* out_ptr = out + ((b * seq_len + t) * num_q_heads + h) * head_dim; for (int d = 0; d < head_dim; d++) { out_ptr[d] = out_vec[d]; } } ``` But wait, dynamic arrays `scores[80]` and `out_vec[32]` on the stack might cause issues. I should use fixed-size arrays or registers. Actually for small sizes like this, automatic arrays in registers should be fine. Let me make them explicit or use shared memory... no, with the small sizes and 1280 threads, registers should suffice. Actually, a nicer approach might be to use shared memory for tiles. But given the small problem size, let's see if the simple kernel works first. Wait, I need to be more careful about grid/block configuration. Let me use: - gridDim.x = batch_size - gridDim.y = num_q_heads - gridDim.z = (seq_len + block_size - 1) / block_size - blockDim.x = block_size (e.g., 128) But with seq_len = 80, a single block could handle all seq positions if block_dim.x >= 80. Let me use blockDim.x = 128 and gridDim.z = 1. Actually, let me simplify: blockDim.x = seq_len = 80, but that's not always a good block size. Using 128 is more standard. Let me also consider using shared memory for the query vectors, since each query is reused across seq_len positions. Actually no, with grid organized as (batch, head, block), each thread handles one time position, and the query vector for that time position is only used once. Hmm, actually let me reconsider the parallelization. Since seq_len is only 80, we could do one block per (batch, head) with 80 threads, and each thread processes one time position. This gives us 2*8 = 16 blocks total, which is very few. Better to have more blocks. Alternative: one block per (batch, head) and threads cooperate on each time position? Or we can just use many blocks. Actually, let me think about what the real reference will be. The problem says "faster than the PyTorch reference" - but which reference? The triple-nested Python loop, or an optimized PyTorch implementation? The description says "Replace the forward computation of the PyTorch model below with a custom CUDA kernel. Your `ModelNew` class must... Run faster than the PyTorch reference." I think they compare against the reference implementation shown (`Model` with the Python loops). That would be extremely slow. But let me verify - the evaluation probably uses the `Model` class as reference for correctness, and runtime comparison might be against a more optimized PyTorch implementation. Actually, it says "Run faster than the PyTorch reference" which typically means the reference implementation shown. But to be safe, let me write an efficient kernel. A simple einsum-based PyTorch implementation would already be extremely fast. Let me just write a reasonable CUDA kernel. One issue with the simple kernel: using arrays on the stack. In CUDA, local arrays might be stored in local memory (which is slow) if there are too many registers. For seq_len=80 and head_dim=32, a scores array of 80 floats is fine, and out_vec of 32 floats is fine too. Combined with other variables, a thread might use ~120 floats = 480 bytes of local memory. With 128 threads per block, that's ~60KB per block, which should fit in registers if we're careful, but might spill to local memory. To avoid local memory, I can compute the output incrementally with streaming: after computing each score, store it, do softmax in two passes, then do the weighted sum in a third pass. I still need to store the scores between softmax and weighted sum. Alternative: process in tiles. For each tile of k/v positions, compute scores, do online softmax, accumulate output partial sums. But with T=80, D=32, tiling seems like overkill. Let me think about another approach: use shared memory for the scores and output contributions. Actually, let me look at this differently. An even simpler approach: since this is a small problem, what if I just use the PyTorch built-in operations in a more efficient way? But the task says "with a custom CUDA kernel." Let me try the simple kernel approach first. Actually, to make it register-friendly, I can process everything in a single pass with online softmax algorithm: ```cpp float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc[32] = {0}; for (int s = 0; s <= t; s++) { // Compute score float score = 0; for (int d = 0; d < head_dim; d++) { score += q[d] * k[s, d]; } score *= scale; // Online softmax update if (score > max_score) { float diff = expf(max_score - score); // = expf(old_max - new_max) sum_exp = sum_exp * diff + 1.0f; for (int d = 0; d < head_dim; d++) { out_acc[d] *= diff; out_acc[d] += v[s, d]; } max_score = score; } else { float w = expf(score - max_score); sum_exp += w; for (int d = 0; d < head_dim; d++) { out_acc[d] += w * v[s, d]; } } } // Normalize float inv_sum = 1.0f / sum_exp; for (int d = 0; d < head_dim; d++) { out[d] = out_acc[d] * inv_sum; } ``` This is beautiful! With online softmax, I don't need to store all scores. I only need `max_score`, `sum_exp`, and `out_acc[32]`. The `out_acc[32]` might still go to local memory, but 32 floats = 128 bytes per thread. For 128 threads that's 16KB, which is very reasonable and might stay in registers or at worst local memory on a fast path. Wait, does this give the correct result? Let me verify with online softmax. Standard softmax: attn_s = exp(score_s - max_score) / sum_t exp(score_t - max_score) Online algorithm: Initialize max_0 = -inf, sum_0 = 0, out_0 = 0 For each s: - If score_s > max_{s-1}: - diff = exp(max_{s-1} - score_s) - sum_s = sum_{s-1} * diff + 1 - out_s = out_{s-1} * diff + v_s - max_s = score_s - Else: - w = exp(score_s - max_{s-1}) - sum_s = sum_{s-1} + w - out_s = out_{s-1} + w * v_s After processing all s, normalize: out = out_S / sum_S Let's verify by induction. Let true_max_s = max(score_1, ..., score_s). We want to show that at step s: out_s = sum_{i=1}^s exp(score_i - true_max_s) * v_i sum_s = sum_{i=1}^s exp(score_i - true_max_s) Base case: s=0, out_0 = 0, sum_0 = 0, true_max_0 = -inf. OK. Inductive step: assume true for s-1. Case 1: score_s > true_max_{s-1} = true_max_s Then diff = exp(true_max_{s-1} - score_s) sum_s = sum_{s-1} * diff + 1 = sum_{i=1}^{s-1} exp(score_i - true_max_{s-1}) * exp(true_max_{s-1} - score_s) + 1 = sum_{i=1}^{s-1} exp(score_i - score_s) + 1 = sum_{i=1}^{s-1} exp(score_i - true_max_s) + exp(score_s - true_max_s) [since true_max_s = score_s] = sum_{i=1}^s exp(score_i - true_max_s) ✓ out_s = out_{s-1} * diff + v_s = sum_{i=1}^{s-1} exp(score_i - true_max_{s-1}) * v_i * exp(true_max_{s-1} - score_s) + v_s = sum_{i=1}^{s-1} exp(score_i - score_s) * v_i + v_s = sum_{i=1}^{s-1} exp(score_i - true_max_s) * v_i + exp(score_s - true_max_s) * v_s = sum_{i=1}^s exp(score_i - true_max_s) * v_i ✓ Case 2: score_s <= true_max_{s-1} = true_max_s Then w = exp(score_s - true_max_{s-1}) sum_s = sum_{s-1} + w = sum_{i=1}^{s-1} exp(score_i - true_max_{s-1}) + exp(score_s - true_max_{s-1}) = sum_{i=1}^{s-1} exp(score_i - true_max_s) + exp(score_s - true_max_s) = sum_{i=1}^s exp(score_i - true_max_s) ✓ out_s = out_{s-1} + w * v_s = sum_{i=1}^{s-1} exp(score_i - true_max_s) * v_i + exp(score_s - true_max_s) * v_s = sum_{i=1}^s exp(score_i - true_max_s) * v_i ✓ After all S steps, out_S = sum_{i=1}^S exp(score_i - true_max_S) * v_i Final output = out_S / sum_S = sum_{i=1}^S exp(score_i - true_max_S) * v_i / sum_{i=1}^S exp(score_i - true_max_S) = sum_{i=1}^S softmax(score_i) * v_i ✓ Great! The online softmax approach is correct. Now, how does this handle the causal masking? In our case, we only iterate s from 0 to t, so it's naturally causal. Now, for the head_dim=32 loop, I can unroll it significantly or use pragma unroll. Given that head_dim = 32, group_size = 4, seq_len = 80, the total work per thread for t=79 is: - 80 dot products of 32 elements each = 2560 multiplies and adds - 80 online softmax updates with potential 32-element vector scaling For threads with small t, less work. But since the data is small and memory-bound for the most part, one thread per (b, h, t) should be fine. Let me write the kernel. I'll use a block size that allows good occupancy. ```cpp template<int HD> __global__ void gqa_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int B, int T, int H_q, int H_kv, float scale ) { int b = blockIdx.x; int h = blockIdx.y; int tid = threadIdx.x; int t = blockIdx.z * blockDim.x + tid; if (b >= B || h >= H_q || t >= T) return; int group_size = H_q / H_kv; int kv_h = h / group_size; // Load q_t float q_vec[HD]; const float* q_ptr = q + ((b * T + t) * H_q + h) * HD; #pragma unroll for (int d = 0; d < HD; d++) { q_vec[d] = q_ptr[d]; } float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc[HD] = {0.0f}; for (int s = 0; s <= t; s++) { const float* k_ptr = k + ((b * T + s) * H_kv + kv_h) * HD; float score = 0.0f; #pragma unroll for (int d = 0; d < HD; d++) { score += q_vec[d] * k_ptr[d]; } score *= scale; const float* v_ptr = v + ((b * T + s) * H_kv + kv_h) * HD; if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; #pragma unroll for (int d = 0; d < HD; d++) { out_acc[d] = out_acc[d] * diff + v_ptr[d]; } max_score = score; } else { float w = expf(score - max_score); sum_exp += w; #pragma unroll for (int d = 0; d < HD; d++) { out_acc[d] += w * v_ptr[d]; } } } float inv_sum = 1.0f / sum_exp; float* out_ptr = out + ((b * T + t) * H_q + h) * HD; #pragma unroll for (int d = 0; d < HD; d++) { out_ptr[d] = out_acc[d] * inv_sum; } } ``` However, templates with compile-time HD are problematic with `load_inline` since we'd need to instantiate for HD=32. Actually, we can just pass head_dim as a regular parameter and use `pragma unroll` with a constant. But CUDA might not unroll loops when the upper bound is not known at compile time unless we use a literal or constexpr. Since head_dim = 32 is fixed in this problem, I could just hardcode it or use a template. But to make it more general, I'll use a loop with pragma unroll and since the function is only called with 32, it might be fine. Or better yet, I can define HD as a constexpr inside the kernel since it's passed as a template or a macro... Actually with load_inline, I need to write it as a complete CUDA source. Let me use a template parameter for head_dim since it's cleaner and allows unrolling. For `load_inline`, I'll include the template instantiation. Actually, with `load_inline`, the kernel is compiled on-the-fly, so I can just use `constexpr int head_dim = 32` in the kernel or use a macro. But since the kernel sees the actual head_dim as a runtime parameter, CUDA might not be able to unroll perfectly. A better approach: `#define HEAD_DIM 32` or use a template. Let me use a template: ```cpp template <int HD> __global__ void gqa_kernel(...) { ... } // In the host wrapper: switch (head_dim) { case 32: gqa_kernel<32><<<...>>>(...); break; default: // generic version } ``` But for a generic version, CUDA won't unroll well. Since the problem specs say head_dim = 32, let me just template on HD and instantiate for HD=32. Actually, looking at this more carefully, with the online approach the inner loops are: 1. Compute score: HD multiplies and adds 2. Update output: HD multiplies and adds (in the best case) or HD multiplies, HD adds, HD multiplies (in the new max case) For HD=32, this is very small. Each thread computes at most 80 such iterations, so the total work per thread is tiny. Wait, actually let me think about whether 1 thread per (b, h, t) is enough parallelism. We have B=2, H=8, T=80, so 1280 threads. On an H100 with 132 SMs, this is ~10 threads per SM, which is very low. We need more parallelism within each block. Alternative: have each block handle one (b, h) pair, and threads within the block process different s values (keys) for a given t, or different d values (dimensions). Actually, with 128 threads per block, and processing one t per thread, we'd have blocks of size 128, but T=80, so we'd only use 80 threads per block for gridDim.z=1. Better to increase T parallelism by having smaller blocks, e.g., blockDim.x=16, then gridDim.z=5, giving 5 blocks per (b, h). Total blocks = 2*8*5 = 80. That's still low. Or blockDim.x=8, gridDim.z=10, total blocks = 160. Or blockDim.x=4, gridDim.z=20, total blocks = 320. Or blockDim.x=1, gridDim.z=80, total blocks = 1280. This is exactly one thread per (b, h, t). Hmm, but with only 1280 threads and 132 SMs, we can't saturate the GPU. However, since each thread does significant work (especially for large t), and the problem is memory-bound with small data, it might still be fine. Actually, looking at this from a different angle - the reference triple-nested python loop is going to be incredibly slow. Even a naive CUDA kernel with thousands of threads will beat it by orders of magnitude. Let me not overthink this. Wait, but I should check - the evaluation says "Run faster than the PyTorch reference." If they compare against the python loop, it'll be very easy. But maybe they have an optimized reference? Let me re-read the problem statement... "Replace the forward computation of the PyTorch model below with a custom CUDA kernel." "Run faster than the PyTorch reference." In Kaggle-style competitions, the "reference" is typically the slow PyTorch implementation shown. But to be safe, I should write a reasonably optimized kernel. Let me consider another approach: using shared memory to load q vectors and process multiple t positions per block, or using warp-level primitives to reduce the dot products. For a warp of 32 threads, I could have each warp process one (b, h, t) position, with 32 threads cooperating to compute the dot product and subsequent reductions. But for HD=32, one warp per (b, h, t) means only 1280 warps = 40 warps per SM, which is actually decent since H100 can run up to 64 warps per SM. With a warp per (b, h, t): - Thread d in the warp loads q[d] and broadcasts it - Thread d loads k[s][d] for each s - We need to compute dot product across d, which requires warp reduction - Then one thread (or all threads) compute the online softmax - Then for each s, thread d computes w * v[s][d] and accumulates This might actually be slower due to warp synchronization overhead for such small HD. Let me just try the simple approach first and see if it compiles and is correct. If it's too slow, I can optimize. Actually, another idea: process multiple (b, h, t) elements per block using shared memory for k and v. For a given (b, kv_h), load all k and v for that batch and kv head into shared memory (80 * 32 floats = 2560 floats = 10KB). Then all threads in the block can access them. Each thread processes one (h, t) where h maps to that kv_h. For 1 block per (b, kv_h): - 1 shared memory allocation of 20KB for k and v - Up to group_size * T threads in the block, but group_size=4, T=80, so 320 threads. We can use up to 1024 threads. - Each thread does online softmax for one (h, t) With 2 batches * 2 kv_heads = 4 blocks total, this is too few blocks. Alternative: 1 block per (b, h), giving 16 blocks. Each block has T=80 threads. Load k and v for the corresponding kv_h into shared memory. Each thread processes one t. With 80 threads per block: - blockDim.x = 80 - gridDim.x = batch_size = 2 - gridDim.y = H_q = 8 - Total blocks = 16 16 blocks for 132 SMs is very low, but the workload per thread is significant. For t=79, each thread does 80 iterations of dot(32), softmax, and accumulate(32). Hmm, 16 blocks of 80 threads = 1280 threads. Same as before. I think the best approach is to just make sure the kernel is well-written and memory-access optimized. Let me proceed with the online softmax kernel. One concern: with blockDim=128 and T=80, some threads in each block will be idle. Maybe blockDim=32 or 64 is better. Actually let me use blockDim.x = 32 (one warp) and have each block process 32 time positions. With T=80, this means gridDim.z = ceil(80/32) = 3 blocks per (b, h), giving 2*8*3 = 48 blocks total. With 32 threads per block, each thread handles one t. Better yet, blockDim.x = 16, gridDim.z = 5, giving 80 blocks total. Or blockDim.x = 8, gridDim.z = 10, giving 160 blocks. Let me try blockDim.x = 32, gridDim.z = 3. The advantage is that 32 threads per block means one warp, so no inter-warp synchronization. Actually, let me just use a 2D grid: - blockIdx.x for batch (size B=2) - blockIdx.y for head (size H_q=8) - threadIdx.x for time (size T=80) So 1 block per (b, h), 80 threads per block. Total 16 blocks. But 80 is not a power of 2... that's fine. Wait, can I use blockDim.x = 80? The max block size is 1024, so yes. But some GPUs prefer block sizes that are multiples of 32 for warp efficiency. 80 = 2.5 warps, which means the last warp is half-utilized. But that's fine. Let me try 80 threads per block: ```cpp dim3 grid(batch_size, num_q_heads); dim3 block(seq_len); gqa_kernel<32><<<grid, block>>>(...); ``` This gives 16 blocks of 80 threads each. 1280 total threads. For an H100, occupancy won't be great, but the problem is small enough that this might be fine. Alternatively, I can process multiple (b, h) pairs per block. But that complicates indexing. Let me just test the simplest approach first. Actually, I want to also consider another optimization. Since k and v are accessed in sequence order (s=0 to t), and all threads in a block access the same k_s and v_s at the same time when they reach iteration s, we can preload k_s and v_s into shared memory before the loop. For a block handling (b, h), all threads access k[b, s, kv_h, :] and v[b, s, kv_h, :] in iteration s. If we preload these into shared memory at the start of each iteration s, all threads can read from shared memory instead of global memory. This is especially helpful when many threads read the same data. For 80 threads per block, in iteration s: - Threads with t >= s need k_s and v_s - Threads with t < s skip this iteration Actually wait, each thread t only iterates up to t. So in iteration s, only threads with t >= s are active. Thread 0 only does iteration 0. Thread 79 does all iterations 0..79. So there's not much benefit from shared memory for k/v since: (1) not all threads access the same s at the same time, (2) the accesses are sequential but with a staggered pattern. Hmm, actually for a fixed s, all threads t >= s access the same k_s. If we preload k_s and v_s into shared memory before iteration s, then all active threads can read it. But the cost of preloading might not be worth it for such small data. Let me think about this more carefully. Each k_s is 32 floats = 128 bytes. For 80 iterations, total k+v reads = 80 * 128 * 2 = 20KB per head. With 16 blocks, total k+v = 320KB. This is tiny and easily fits in cache. So global memory access is not the bottleneck. Actually, the real bottleneck might be instruction throughput and register pressure. The kernel does a lot of floating point operations per thread. Let me estimate FLOPs and memory: - For t=79: 80 dot products of size 32 = 2560 FMAs for scores - 80 online softmax iterations - 80 weighted accumulations of size 32 = 2560 FMAs for output - Total per thread for t=79: ~5120 FMAs + 80 expfs For 1280 threads, total FLOPs ≈ 1280 * 5120 ≈ 6.5M FMAs = 13M FLOPs. Plus ~100K expfs. This is tiny. The kernel will likely be limited by launch overhead or occupancy, not by compute or memory bandwidth. Given that the PyTorch reference with Python loops will be incredibly slow (probably > 1 second), and our kernel should run in < 1ms, we should be fine. Let me code this up and try to compile. One more thing: I should be careful about array initialization in CUDA. `float out_acc[HD] = {0.0f};` where HD is a template parameter should work, or I can use a loop. Let me write the complete kernel code now. Actually, I realize I need to consider one more thing: the q, k, v tensors are passed in as contiguous tensors, and I need to make sure my indexing matches the shape. q: [B, T, H_q, D] k: [B, T, H_kv, D] v: [B, T, H_kv, D] out: [B, T, H_q, D] For q at (b, t, h, d): idx = ((b * T + t) * H_q + h) * D + d For k at (b, s, kv_h, d): idx = ((b * T + s) * H_kv + kv_h) * D + d So my indexing is correct. Let me now write the complete Python file with the kernel. I'll also need to handle the case where head_dim is not known at compile time. Since it's always 32 in this problem, I'll use a template with HD=32. Actually, I realize the reference might use at different head_dim values in hidden tests. Let me make it generic but use template specialization for common sizes. For `load_inline`, the CUDA source is a string. Let me write the complete code. ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline cuda_source = """ #include <torch/extension.h> #include <cuda_runtime.h> #include <math.h> template <int HD> __global__ void gqa_causal_attention_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int B, int T, int H_q, int H_kv, float scale ) { int b = blockIdx.x; int h = blockIdx.y; int t = threadIdx.x + blockIdx.z * blockDim.x; if (b >= B || h >= H_q || t >= T) return; int group_size = H_q / H_kv; int kv_h = h / group_size; const float* q_ptr = q + ((b * T + t) * H_q + h) * HD; float q_vec[HD]; #pragma unroll for (int d = 0; d < HD; ++d) { q_vec[d] = q_ptr[d]; } float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc[HD]; #pragma unroll for (int d = 0; d < HD; ++d) { out_acc[d] = 0.0f; } for (int s = 0; s <= t; ++s) { const float* k_ptr = k + ((b * T + s) * H_kv + kv_h) * HD; float score = 0.0f; #pragma unroll for (int d = 0; d < HD; ++d) { score += q_vec[d] * k_ptr[d]; } score *= scale; const float* v_ptr = v + ((b * T + s) * H_kv + kv_h) * HD; if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; #pragma unroll for (int d = 0; d < HD; ++d) { out_acc[d] = out_acc[d] * diff + v_ptr[d]; } max_score = score; } else { float w = expf(score - max_score); sum_exp += w; #pragma unroll for (int d = 0; d < HD; ++d) { out_acc[d] += w * v_ptr[d]; } } } float inv_sum = 1.0f / sum_exp; float* out_ptr = out + ((b * T + t) * H_q + h) * HD; #pragma unroll for (int d = 0; d < HD; ++d) { out_ptr[d] = out_acc[d] * inv_sum; } } torch::Tensor gqa_causal_attention_forward( torch::Tensor q, torch::Tensor k, torch::Tensor v ) { int B = q.size(0); int T = q.size(1); int H_q = q.size(2); int HD = q.size(3); int H_kv = k.size(2); auto out = torch::empty_like(q); float scale = 1.0f / sqrtf(static_cast<float>(HD)); if (HD == 32) { int threads = 128; int blocks_z = (T + threads - 1) / threads; dim3 grid(B, H_q, blocks_z); dim3 block(threads); gqa_causal_attention_kernel<32><<<grid, block>>>( q.data_ptr<float>(), k.data_ptr<float>(), v.data_ptr<float>(), out.data_ptr<float>(), B, T, H_q, H_kv, scale ); } else { TORCH_CHECK(false, "Unsupported head_dim: ", HD); } return out; } """ cpp_source = """ torch::Tensor gqa_causal_attention_forward( torch::Tensor q, torch::Tensor k, torch::Tensor v ); """ ext = load_inline( name="gqa_cuda", cpp_sources=cpp_source, cuda_sources=cuda_source, functions=["gqa_causal_attention_forward"], extra_cuda_cflags=["-O3", "--use_fast_math"], verbose=True, ) class ModelNew(nn.Module): def __init__(self): super().__init__() def forward(self, q, k, v): return ext.gqa_causal_attention_forward(q, k, v) ``` Wait, there's an issue. When using `load_inline`, the `cpp_sources` should only contain declarations, not implementations. And I shouldn't include `torch/extension.h` in the cuda source? Actually, `load_inline` handles this. Let me check the format. Actually, looking at examples of `load_inline`: - `cpp_sources`: C++ wrapper declarations (the auto-generated pybind will call these) - `cuda_sources`: CUDA implementation The functions in `functions=[...]` are the C++ functions that will be exposed to Python. The declarations in `cpp_sources` tell the compiler what signatures to expect, and the definitions in `cuda_sources` (or `cpp_sources`) provide the implementations. But wait, since my `gqa_causal_attention_forward` is defined in `cuda_sources`, it will be compiled with nvcc and linked. The declaration in `cpp_sources` tells the C++ compiler about it. This should work. Actually, I think the `cpp_sources` is where C++ code goes, and `cuda_sources` is where CUDA code goes. The `functions` list names the functions that should be bound. So I should define the function in `cuda_sources` and declare it in `cpp_sources`. Wait, looking more carefully at `torch.utils.cpp_extension.load_inline`: the auto-generated pybind module looks at `functions` list and creates bindings for them by calling the C++ functions. The `cpp_sources` provides the C++ declarations that pybind uses, and the actual implementations can be in `cuda_sources`. Actually, I think it's even simpler: the `cpp_sources` and `cuda_sources` are just concatenated together (or compiled separately and linked). The `functions` list tells auto-generated pybind which functions to expose. So I can define the function in either source file. Let me simplify: put the CUDA kernel in `cuda_sources`, and also put the host wrapper in `cuda_sources`. Then `cpp_sources` can be empty or just the includes. Actually, the function needs a C++ declaration visible to the pybind code, which is in `cpp_sources`. So I need the declaration in `cpp_sources`. Hmm wait. Actually let me reconsider. I've used `load_inline` before. Here's the pattern: ```python load_inline( name="my_ext", cpp_sources=""" #include <torch/extension.h> torch::Tensor my_func(torch::Tensor x); """, cuda_sources=""" #include <torch/extension.h> torch::Tensor my_func(torch::Tensor x) { // implementation } """, functions=["my_func"], ) ``` The auto-generated pybind will call `my_func`. But since `my_func` is declared in `cpp_sources` and defined in `cuda_sources`, it should link correctly. Actually, I wonder if I even need the declaration. Let me check - `load_inline` probably compiles both sources and links them. If I put the function definition in `cuda_sources` only, and list it in `functions`, will it work? The auto-generated binding code needs to see the declaration... Actually, I think `cpp_sources` is where the pybind module code goes, and if I put my function definitions there, they need to be compilable as C++. But the CUDA kernel can only be in `cuda_sources`. Standard approach: ```python load_inline( name="ext", cpp_sources=""" #include <torch/extension.h> torch::Tensor forward(torch::Tensor q, torch::Tensor k, torch::Tensor v); """, cuda_sources="""... actual CUDA code with implementation...""", functions=["forward"], ) ``` Yes, the declaration is needed in `cpp_sources`. But actually, I realize I might be overthinking this. Let me just try it. Wait, actually there's a potential issue. The `cpp_sources` might be compiled with the C++ compiler, while `cuda_sources` is compiled with nvcc. So the declaration in `cpp_sources` and definition in `cuda_sources` should work when linked. Let me write the code and try to compile. I'll include necessary headers in `cuda_sources`. Wait, I need to double check: should I include `<torch/extension.h>` in `cuda_sources`? Yes, for `torch::Tensor` type. And `<cuda_runtime.h>` for CUDA types. Let me also think about whether I should use `restrict` keywords. In CUDA, `__restrict__` is supported and helps with alias analysis. Also, I should use `--use_fast_math` for faster `expf` at the cost of slightly lower precision. The tolerance is 1e-4 for fp32, which should be fine with fast math. Let me also think about whether the online softmax with `expf` has numerical issues. The difference `max_score - score` or `score - max_score` could be large. But `expf` handles down to about -87 before underflowing to 0 for float32. In our case, scores are dot products of normalized vectors (approximately), scaled by 1/sqrt(32) ≈ 0.176. So scores are O(1), and differences are O(1). `expf(-10) ≈ 4.5e-5` is fine. So numerical issues are unlikely. One more thing: should I worry about `sum_exp` being 0? Only if there are no iterations, but `t` ranges from 0 to T-1, so `s=0` to `s=t` always has at least 1 iteration. And `sum_exp` starts at 0, then becomes at least 1.0f in the first iteration. So no division by zero. But wait! What if `t` could be negative? No, t starts at 0. Good. Actually wait, in my grid setup with blockDim=128 and blocks_z=ceil(80/128)=1, so t ranges from 0 to 127, but I have `if (t >= T) return;`, so threads 80-127 return immediately. That's fine. But there might be a better block size. With 128 threads and only 80 being active, 62 threads are wasted. Let me use blockDim=32: - blocks_z = ceil(80/32) = 3 - Total blocks = 2 * 8 * 3 = 48 - Block 0 handles t=0..31, block 1 handles t=32..63, block 2 handles t=64..79 - All threads active, no waste Or blockDim=16: - blocks_z = 5 - Total blocks = 80 Or blockDim=8: - blocks_z = 10 - Total blocks = 160 I think blockDim=32 is good. 48 blocks total, 32 threads each, one warp per block. This means no __syncthreads needed within the block. With 48 blocks and 132 SMs, we can't saturate the GPU, but again, the problem is tiny. Actually, let me increase parallelism by processing multiple heads or batches per block. But that complicates things. Alternatively, I could have each thread process multiple t positions. For example, with blockDim=32 and each thread processing 3 time positions (covering T=80 with 27 threads + some padding). But this doesn't help with parallelism, it just increases work per thread. Wait, what if I change the mapping: one thread per (b, t), and each thread loops over all h? Then: - blockDim.x = 32 for t - blockDim.y = 2 for b - Total threads per block = 64 - Grid: ceil(T/32) = 3 blocks in x, 1 block in y - But we'd need an outer loop over h Hmm, that's not better either. What about: one thread per (t) within a block, and the block loops over (b, h)? But then threads in a block process different t values, and we need to ensure they can run independently, which they can. But with 80 threads per block processing all t values, and the block iterating over 2*8=16 (b,h) pairs: - 16 blocks of 80 threads each - Each block processes 16 (b,h) pairs, one thread per t - But we'd need to load k and v for each (b, kv_h) pair into shared memory. - Actually, with 16 iterations, each thread does 16 times the work. This gives the same total thread count but potentially better memory reuse. But 16 blocks of 80 threads is still small. Let me try a different approach: use a single block with many threads, or use fewer time positions per thread but more threads by processing multiple (b,h) in parallel. Actually, let me focus on correctness first, then optimize. Let me code it up with blockDim=32 and see. Actually, I want to check something. Let me use blockDim=16: - Blocks in z: 5 - Total blocks: 2*8*5 = 80 - Threads per block: 16 = half warp Or blockDim=8: - Blocks in z: 10 - Total blocks: 160 - Threads per block: 8 = quarter warp With 160 blocks, we have more blocks to distribute across SMs. But 8 threads per block means 20 warps total, which might have launch overhead. I think 32 threads per block (1 warp) is a sweet spot. 48 blocks total. Let me see if I can increase this. What if I do: - blockDim.x = 32 (time positions) - blockDim.y = 2 (query heads) - Then gridDim.x = batch_size = 2 - gridDim.y = H_q / 2 = 4 - gridDim.z = ceil(T / 32) = 3 Total blocks = 2*4*3 = 24 Total threads per block = 32*2 = 64 Total threads = 24*64 = 1536 Hmm, this doesn't really increase parallelism. What about using 3D threads: - blockDim.x = 16 (time) - blockDim.y = 4 (heads in group) - Then for each kv_head group - This gets complicated with indexing. Let me just go with blockDim=32 and see. If the kernel is not fast enough, I'll optimize. Wait, one more idea. I can process different batches/heads in different SMs by just using a flat 1D grid. Map: ```cpp int global_tid = blockIdx.x * blockDim.x + threadIdx.x; int total_threads = gridDim.x * blockDim.x; int total_elements = B * H_q * T; for (int idx = global_tid; idx < total_elements; idx += total_threads) { int b = idx / (H_q * T); int rem = idx % (H_q * T); int h = rem / T; int t = rem % T; // process (b, h, t) } ``` This is persistent kernel style! It naturally balances load across all SMs regardless of the grid size. If I choose total_threads to be large (like 4096 or 8192), I can get good occupancy. For example, with 256 threads per block and 32 blocks = 8192 threads: ```cpp dim3 block(256); dim3 grid(32); // or more ``` Each thread processes multiple (b, h, t) elements based on the stride pattern. With 8192 threads and 1280 elements, each thread processes exactly 1 element and most threads skip. That's wasteful. Better: use fewer total threads, like 1280. But I can't guarantee exact divisibility. With 1024 threads (32 blocks of 32 threads): - 1280 elements / 1024 threads = each thread processes 1 or 2 elements - 256 threads process 2 elements, 768 threads process 1 element This could work! But I need to be careful about TLB effects and memory coalescing. Actually, for memory coalescing, it's better if threads in a block access contiguous memory. The output layout is [B, T, H_q, D]. So for a fixed (b, h), time t varies along the second dimension. If threads in a block process consecutive t values, they access q with stride H_q*D in the T dimension. But for a fixed t, different h values access q with stride D. Actually, for the q tensor [B, T, H_q, D]: - Access pattern for (b, t, h, d): offset = ((b*T + t)*H_q + h)*D + d - For fixed b, h, varying t: offset difference = H_q*D - For fixed b, t, varying h: offset difference = D With H_q=8, D=32, H_q*D = 256 floats = 1024 bytes. So consecutive t values are 1024 bytes apart. Memory coalescing for threads accessing different t values with the same d would still coalesce well on modern GPUs (H100 has 128-byte cache lines). For k and v tensors [B, T, H_kv, D]: - H_kv=2, D=32, H_kv*D = 64 floats = 256 bytes. - When threads with the same kv_h but different t access different k_s... wait, in my kernel, each thread accesses k_s sequentially for s=0..t. Different threads access different s values, so they don't really coalesce anyway. Actually, let me reconsider. In the inner loop, all threads are at iteration s. Thread t accesses k_s and v_s only if t >= s. But threads with different kv_h access different k_s, so there's no coalescing across threads with different h. For threads with the same (b, h), different t values: at iteration s, threads t >= s all access the same k_s (same b, s, kv_h). On an H100, this will hit the L1 cache since all threads in the block read the same address (broadcast). So it's actually very efficient! But in my current grid layout where each block handles different (b, h) pairs, threads in the same block have the same (b, h) but different t. So yes, they all access the same k_s and v_s at iteration s. This is a broadcast read, which is very cache-friendly. The issue is just having enough blocks to keep all SMs busy. With 16 blocks (if I do 1 block per (b, h) with 80 threads), I only have 16 concurrent blocks. With 48 blocks (blockDim=32), I have 48. Still not enough for 132 SMs. Let me think about this differently. For a kernel with 48 blocks, each block takes some time. An H100 can execute multiple blocks per SM concurrently. If each block uses few enough resources (registers, shared memory), multiple blocks can run on the same SM. For example, if each block uses ~32 registers per thread and no shared memory, then each block needs 32*32 = 1024 registers. An H100 SM has 64K = 65536 registers. So each SM can run up to 64 blocks concurrently (limited by registers). But the maximum concurrent blocks per SM is 32. So we can have up to 32 blocks per SM. With 48 blocks total and 132 SMs, each SM would get at most 1 block, and many SMs would be idle. That's not great for utilization. To improve this, I need either: 1. More blocks 2. Each thread to do more work (but then blocks take longer, which doesn't help latency) Actually, for small total work like this, the kernel will be very fast even with low utilization. The total execution time might be dominated by kernel launch overhead, not by execution time. What if I just use a 1D grid and have threads stride through elements? Let me do: - 256 threads per block - 128 blocks (or more) in 1D grid - Total threads = 32768 - But only 1280 elements, so most threads do nothing This is even worse. How about: - 32 threads per block - 128 blocks in 1D grid = 4096 threads - Again most threads idle No, the key is to make each thread process multiple elements. With 128 blocks of 32 threads = 4096 threads, and 1280 elements: - Each thread processes floor(1280/4096) = 0 elements on average. Bad. With 16 blocks of 32 threads = 512 threads: - Each thread processes 1280/512 = 2.5 elements, so some process 2, some 3. For the persistent kernel approach, I'd use: ```cpp int idx = blockIdx.x * blockDim.x + threadIdx.x; int stride = gridDim.x * blockDim.x; for (int linear = idx; linear < B * H_q * T; linear += stride) { int b = linear / (H_q * T); int rem = linear % (H_q * T); int h = rem / T; int t = rem % T; // process (b, h, t) } ``` With 256 blocks of 16 threads = 4096 threads, and 1280 elements: - Threads 0..1279 each process 1 element - Threads 1280..4095 do nothing - Total 256 blocks But I can configure it more tightly: 40 blocks of 32 threads = 1280 threads exactly. - thread 0 processes element 0 - thread 1 processes element 1 - ... - thread 1279 processes element 1279 This is perfect! 40 blocks of 32 threads. But the issue is coalescing and divergence. The linear index maps to (b, h, t) as: linear = b * (H_q * T) + h * T + t So consecutive linear indices have consecutive t, which means they access consecutive positions in q along the T dimension. The stride is H_q * D = 8 * 32 = 256 floats. For q accesses at different t but same d: offset difference = 256 floats = 1024 bytes. With 32 threads, offsets are t*1024, (t+1)*1024, etc. These are 1024 bytes apart. For an H100, memory accesses are coalesced if threads in a warp access consecutive 128-byte regions. Here they're 1024 bytes apart, so each warp issues 32 separate 128-byte reads (for 4 floats each = 16 bytes per thread). Actually, each thread reads 32 floats = 128 bytes. So each thread reads one 128-byte segment, and they're spaced 1024 bytes apart. This is strided access, not fully coalesced. Wait, let me reconsider. If thread i accesses q at (b, t+i, h, 0..31), it reads 32 floats from offset ((b*T + t+i)*H_q + h)*D. With D=32, all 32 floats are contiguous. But consecutive threads read 1024 bytes apart. A warp of 32 threads reads 32 cache lines of 128 bytes each. This is extremely uncoalesced! If I instead have threads in a warp process consecutive h values (with same t): linear = b * (H_q * T) + h * T + t For consecutive linear indices with same (b, t), h changes: offset difference = T * D = 80 * 32 = 2560 floats = 10240 bytes. Even worse. Or if they process consecutive b values: offset difference = H_q * T * D = 8 * 80 * 32 = 20480 floats = 81920 bytes. Terrible. So any 1D linearization has bad memory access patterns unless we carefully map threads. Better mapping: block handles a (b, h) pair, threads in the block handle consecutive t values. - In a warp, threads access q at (b, t, h, d) with consecutive t. - Each thread reads all d=0..31, which is contiguous. - But consecutive threads read t that are 1 apart, so their starting addresses are H_q*D = 256 floats = 1024 bytes apart. For the dot product loop, each thread reads q_vec[d] from its own location, and k_ptr[d] from the shared k_s location (broadcast). The k reads coalesce perfectly. The q reads don't coalesce, but they're in registers/local storage and only read once. For the output write, each thread writes to (b, t, h, d) with consecutive t. Again, 1024 bytes apart. Not great. How can I improve memory coalescing? Map threads to consecutive d values for the same (b, t, h)? - Thread 0 reads q[b,t,h,0..31]? No, then each thread only reads 1 float, and we need warp reductions. - Actually, this is the warp-level reduction approach I considered earlier. With warp-level reduction for dot products: - 32 threads in a warp process d=0..31 for the same (b, t, h) - Thread d reads q[b,t,h,d] and k[b,s,h_kv,d] - They compute q[d] * k[d] in parallel - Then warp-reduce to get score - Thread 0 (or all threads) do the softmax logic - Then for output, thread d accumulates attn * v[d] This has: - Consecutive threads read consecutive d values: perfect coalescing for q, k, v - All threads in a warp work on the same (b, t, h), so natural synchronization Number of warps needed: B * H_q * T = 1280 warps. Number of warps per block: say 4 warps = 128 threads. Number of blocks: 1280 / 4 = 320 blocks. With 320 blocks, we have much better distribution across SMs! 320 / 132 ≈ 2.4 blocks per SM on average. And coalescing is great! For q: threads 0..31 in a warp read q[b,t,h,0..31], which are 4 bytes apart, perfectly coalesced into 1-2 cache lines. Same for k and v. Output writes are also perfectly coalesced. But the challenge is: how do the 32 threads in a warp cooperate on the online softmax? In the online softmax per (b, t, h): For each s from 0 to t: 1. All 32 threads read q[d] (but q[d] is the same for all s, so we can load it once) 2. All 32 threads read k[s][d] 3. Each thread computes partial = q[d] * k[s][d] 4. Warp-reduce to get score = sum(partial) over d 5. All threads need score for the online softmax update 6. Thread 0 (or all threads) compute the new max, diff, etc. 7. All 32 threads read v[s][d] 8. Each thread updates its out_acc[d] But wait, the online softmax state (max_score, sum_exp) is scalar. All threads in the warp need it. The out_acc is per-dimension, so each thread d maintains out_acc[d]. Let me write this out: ```cpp // Warp-level reduction __inline__ __device__ float warp_reduce_sum(float val) { for (int offset = 16; offset > 0; offset /= 2) val += __shfl_down_sync(0xffffffff, val, offset); return val; } __global__ void gqa_kernel(...) { int warp_id = (blockIdx.x * blockDim.x + threadIdx.x) / 32; int lane = threadIdx.x % 32; int total_warps = gridDim.x * blockDim.x / 32; for (int w = warp_id; w < B * H_q * T; w += total_warps) { int b = w / (H_q * T); int rem = w % (H_q * T); int h = rem / T; int t = rem % T; int kv_h = h / group_size; // Load q float q_val = q[((b * T + t) * H_q + h) * HD + lane]; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; for (int s = 0; s <= t; s++) { float k_val = k[((b * T + s) * H_kv + kv_h) * HD + lane]; float partial = q_val * k_val; float score = warp_reduce_sum(partial) * scale; float v_val = v[((b * T + s) * H_kv + kv_h) * HD + lane]; // Get score to all threads float score_broadcast = __shfl_sync(0xffffffff, score, 0); // We need to be careful here. Each thread has its own out_acc for its lane. // The online softmax update is: // if (score > max_score) { // diff = exp(max_score - score) // out_acc = out_acc * diff + v_val // max_score = score // sum_exp = sum_exp * diff + 1 // } else { // w = exp(score - max_score) // out_acc = out_acc + w * v_val // sum_exp += w // } // But each thread only has its own max_score and sum_exp? // Actually max_score and sum_exp are the SAME for all lanes! // So we only need one copy per warp. // Wait, but how do we share max_score and sum_exp across threads? // Since all threads in a warp work on the same (b,t,h), they all need // the same max_score and sum_exp. The broadcasts ensure this. // However, each thread computes its own out_acc[d]. That's fine. } // Normalize and write back out[((b * T + t) * H_q + h) * HD + lane] = out_acc / sum_exp; } } ``` This is more complex but potentially much faster due to: 1. Perfect memory coalescing 2. More blocks (320 blocks of 128 threads, or 128 blocks of 320 threads... wait) Let me think about block config. I want enough warps to cover all elements. Option A: 128 threads per block (4 warps). Need 1280/4 = 320 blocks. Option B: 256 threads per block (8 warps). Need 1280/8 = 160 blocks. Option C: 64 threads per block (2 warps). Need 1280/2 = 640 blocks. With 640 blocks of 64 threads = 640 blocks of 2 warps each. Total 640 blocks. This is great for occupancy. But 64 threads = 2 warps, which means the block has 2 warps and they diverge if processing different (b,t,h). Actually, each warp processes on (b,t,h). With 2 warps per block, the two warps work independently. But they share L1 cache, so k and v loads for the same (b, kv_h) but different t might benefit from cache reuse. If the two warps have the same (b, h) but different t, they load the same k[s] and v[s] values. This is good. Wait, but with the persistent loop, warps in a block might process completely different (b, h, t), so they might not share cache benefits. Actually, for simple mapping: one block handles a range of (b, h, t). Let me use a simpler grid. ```cpp // Map warps directly to (b, h, t) int num_warps_x = blockDim.x / 32; int warp_in_block = threadIdx.x / 32; int global_warp = (blockIdx.x * num_warps_x + warp_in_block); // Total elements int total = B * H_q * T; // Each warp handles one element if (global_warp < total) { int w = global_warp; int b = w / (H_q * T); int rem = w % (H_q * T); int h = rem / T; int t = rem % T; // ... } ``` With blockDim.x = 128 (4 warps): - Need 320 blocks to handle 1280 warps. - Grid could be 320 blocks in 1D, or 20x16, or whatever. With 320 blocks and 132 SMs, we have ~2.4 blocks per SM. Much better than 16 or 48 blocks. But wait, can we do even better? What if we make the grid larger than 320 blocks and use the persistent loop? No, we only have 1280 elements. For a persistent loop with larger grid: - 640 blocks of 64 threads (2 warps each) = 1280 warps total - Each warp handles exactly 1 element. Perfect. 640 blocks of 64 threads. SM can handle up to 2048 threads / 64 = 32 blocks concurrently per SM. With 640 blocks and 132 SMs, each SM gets ~4.8 blocks on average. Good utilization! And with 64 threads per block (2 warps), each block handles 2 elements. Perfect. Actually, I realize 64 threads per block means 2 warps. But blockDim.x should ideally be a multiple of 32. 64 is fine. 32 is also fine but gives fewer blocks. With blockDim.x = 32 and gridDim.x = 1280/32 = 40 blocks? No, that gives 40 blocks total, each with 1 warp, each handling 1 element. Only 40 blocks! That's worse. For non-persistent mapping: - blockDim.x = 32 - One element per warp - Total warps = 1280 - If each block has 1 warp, we need 1280 blocks. But max blocks per SM is 32, so total max active blocks = 132 * 32 = 4224. 1280 blocks is fine. - But gridDim can be at most 2^31-1 in 1D, so 1280 blocks is fine. Wait, but 1280 blocks of 32 threads = 1280 * 32 = 40960 threads. With 1280 elements, each warp handles exactly 1 element. No persistent loop needed. But 1280 blocks might have significant launch overhead. Kernel launch overhead is often ~5-10 microseconds. With 1280 blocks, it might be higher. But for a fast kernel, 5-10 us overhead is < 1%. Actually, a better approach: blockDim.x = 128 (4 warps), gridDim.x = 320 blocks. Same total threads, fewer blocks, less launch overhead. Or blockDim.x = 256 (8 warps), gridDim.x = 160 blocks. Even fewer blocks. Let me go with blockDim.x = 128, gridDim.x = 320. This is 320 blocks, which is moderate. But wait, there's a subtlety with __shfl_down_sync. It operates within a warp. With blockDim.x = 128, there are 4 warps. The warp_reduce_sum function needs to use the correct mask, but since all threads in a warp participate, mask = 0xffffffff is fine. However, __shfl_down_sync requires all threads in the mask to be active (not diverged). In our kernel, the for loop for s=0..t has divergence based on t. But this divergence is per-thread, not per-warp? Wait, all threads in a warp work on the same (b, h, t), so they all have the same t! So they all execute the same number of loop iterations. No divergence within a warp! This is beautiful. Similarly, all threads in a warp have the same b and h. So they access the same kv_h. All memory accesses within a warp are perfectly aligned and coalesced. I think the warp-per-element approach is the way to go for this problem. Let me finalize the kernel: ```cpp #include <torch/extension.h> #include <cuda_runtime.h> #include <math.h> __inline__ __device__ float warp_reduce_sum(float val) { #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { val += __shfl_down_sync(0xffffffff, val, offset); } return val; } template <int HD> __global__ void gqa_causal_attention_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int B, int T, int H_q, int H_kv, float scale ) { int num_warps_per_block = blockDim.x / 32; int warp_id = threadIdx.x / 32; int lane = threadIdx.x % 32; int global_warp = blockIdx.x * num_warps_per_block + warp_id; int total_warps = gridDim.x * num_warps_per_block; int total_elements = B * H_q * T; for (int w = global_warp; w < total_elements; w += total_warps) { int b = w / (H_q * T); int rem = w % (H_q * T); int h = rem / T; int t = rem % T; int group_size = H_q / H_kv; int kv_h = h / group_size; const float* q_ptr = q + ((b * T + t) * H_q + h) * HD; float q_val = q_ptr[lane]; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; for (int s = 0; s <= t; s++) { const float* k_ptr = k + ((b * T + s) * H_kv + kv_h) * HD; float k_val = k_ptr[lane]; float score_partial = q_val * k_val; float score = warp_reduce_sum(score_partial) * scale; const float* v_ptr = v + ((b * T + s) * H_kv + kv_h) * HD; float v_val = v_ptr[lane]; // All threads in warp need score // Since warp_reduce_sum broadcasts to lane 0, we need to broadcast to all score = __shfl_sync(0xffffffff, score, 0); if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; out_acc = out_acc * diff + v_val; max_score = score; } else { float w = expf(score - max_score); sum_exp += w; out_acc += w * v_val; } } out_acc /= sum_exp; float* out_ptr = out + ((b * T + t) * H_q + h) * HD; out_ptr[lane] = out_acc; } } ``` Wait, I used `__shfl_sync(0xffffffff, score, 0)` to broadcast score from lane 0 to all lanes. But `warp_reduce_sum` already returns the sum in lane 0. So I need to broadcast it. Actually, `__shfl_sync(0xffffffff, score, 0)` returns lane 0's value to all lanes. Good. But wait, does `warp_reduce_sum` return the correct value in all lanes, or only lane 0? With `__shfl_down_sync`, it shifts values down and adds. After the full reduction, lane 0 has the sum. Other lanes have partial sums. So yes, I need the broadcast. I'll keep it. Now, about `max_score` and `sum_exp`: each thread in the warp has its own copy. After the broadcast of `score`, all threads compute the same `max_score`, `sum_exp`, and `out_acc` logic. So no divergence. But since each thread maintains its own `max_score` and `sum_exp`, and all threads do the same computation, they're always in sync. I don't need to __syncthreads within the warp. Actually, there's a minor optimization: I can have only lane 0 maintain `max_score` and `sum_exp`, then broadcast them when needed. But since all threads do the same logic and there's no divergence (because all threads have the same score and make the same conditional choice), it doesn't matter. The compiler should optimize this. But wait - the `if (score > max_score)` branch: does this introduce warp divergence? No, because `score` is the same in all threads (after broadcast) and `max_score` is also the same in all threads (initialized the same and updated identically). So all threads in the warp take the same branch. No divergence! This is really clean. One thing: `warp_reduce_sum` can also be written using `__reduce_add_sync` on newer GPUs (compute capability 8.0+). H100 is sm_90, so it supports `__reduce_add_sync`. This is a single instruction! Much faster than the shuffle loop. ```cpp __inline__ __device__ float warp_reduce_sum(float val) { return __reduce_add_sync(0xffffffff, val); } ``` This is a great optimization for H100. Let me use it. Similarly, for broadcasting, I could use `__shfl_sync(0xffffffff, score, 0)`, which is also a single instruction. Wait, but is `__reduce_add_sync` available? It was introduced in CUDA 11.7 or so for sm_80+. H100 is sm_90, so yes. Let me check if there's a compatibility concern... `torch.utils.cpp_extension` should compile with the default CUDA version. But to be safe, I can use the `__shfl_down_sync` approach, which works everywhere. Or I can use a compile-time check. Actually, since the target is H100 (sm_90), I can safely use `__reduce_add_sync` if the CUDA version supports it (CUDA 11.8+ definitely does). But I'm not sure what CUDA version is installed. Let me use the portable shuffle-based reduction to be safe. Wait, actually the user said the GPU is H100, and the `get_gpu_specs` confirmed it. The CUDA version for H100 would be at least 12.0. But `__reduce_add_sync` requires CC 8.0+. H100 is CC 9.0, so it should work. But I'm not 100% sure about the CUDA compiler version. Let me use the shuffle approach for maximum compatibility. Actually, I can use a macro to check: ```cpp #if __CUDA_ARCH__ >= 800 return __reduce_add_sync(0xffffffff, val); #else // shuffle fallback #endif ``` But `__CUDA_ARCH__` is only defined in device code, so I can use it. Let me do that. Actually, I just realized I should also think about whether the kernel uses too many registers. With the warp-level approach: - q_val, k_val, v_val, score_partial, score, diff, w, max_score, sum_exp, out_acc = ~10 floats - Plus loop counters and indices - Total ~20-30 registers per thread - For 128 threads per block: ~2560-3840 registers per block - H100 SM has 64K registers, so each SM can run up to ~16-25 blocks concurrently - With 320 blocks and 132 SMs: ~2.4 blocks per SM. Good. With 64 threads per block (2 warps): - ~1280-1920 registers per block - Up to ~33 blocks per SM - 640 blocks / 132 SMs ≈ 4.8 blocks per SM. Better! So 64 threads per block might give better occupancy. Let me use that. Block config: - blockDim.x = 64 - num_warps_per_block = 2 - gridDim.x = (total_elements + num_warps_per_block - 1) / num_warps_per_block = (1280 + 1) / 2 = 640 blocks Wait, 640 blocks for 1280 elements with 2 warps per block means exactly 1 element per warp = exactly enough. With a persistent loop: ```cpp for (int w = global_warp; w < total_elements; w += total_warps) { ``` Since total_warps = 640 * 2 = 1280 = total_elements, each warp handles exactly 1 element. No persistent iteration needed. But the loop handles edge cases nicely. Actually, I should just use direct mapping without the loop when possible, but the loop is cheap. Let me reconsider: with blockDim=64 and gridDim=640, we launch 640 blocks. That seems like a lot, but it's well within GPU limits. Kernel launch overhead for 640 blocks might be slightly higher than for fewer blocks, but still very small. Alternatively, blockDim=128, gridDim=320. 320 blocks. This reduces launch overhead a bit. Or blockDim=256, gridDim=160. 160 blocks. I think any of these is fine. Let me go with blockDim=128 (4 warps) and gridDim=320. The advantage is fewer blocks and less launch overhead. Wait, actually with blockDim=256 (8 warps), gridDim=160. 160 blocks is quite few for 132 SMs. Most SMs get 1 block, some get 2. We can increase gridDim to have some oversubscription. For example, gridDim=320 with blockDim=128. Or even better: use a persistent kernel with a much larger grid, like gridDim=1024, blockDim=128. Total warps = 1024 * 4 = 4096. Each warp now handles 1 element, with 4096 - 1280 = 2816 warps doing nothing. That's wasteful. What if we have gridDim=256, blockDim=128? Total warps = 1024. Some warps do 2 elements. 1280 / 1024 = 1.25. So 256 warps do 2 elements, 768 warps do 1 element. Hmm, but the persistent loop means we might have cache locality benefits if a warp processes multiple elements with the same (b, h) but different t. With linear mapping, w and w+1024 are far apart. Let me stick with the tight grid: exactly enough warps for all elements. blockDim=128, gridDim=320. Total warps=1280. Each warp handles exactly 1 element. Or, to have a bit more blocks: blockDim=64, gridDim=640. Total warps=1280. Each warp handles exactly 1 element. 640 blocks vs 320 blocks. 640 blocks might have slightly higher launch overhead but potentially more parallelism since some SMs can run multiple blocks. Actually, with 64 threads per block (1/4 of max threads per block), each SM can run up to 4 blocks instead of 2. So 640 blocks means 4.8 blocks per SM on average, vs 320 blocks means 2.4 blocks per SM. With 4.8 blocks per SM, closer to the limit of 32 concurrent blocks, the scheduler has more flexibility. I'll go with blockDim=64, gridDim=640. Actually, I want to double check: can H100 run 640 blocks concurrently? The limit is 2^31-1 blocks per grid dimension, and max 32 blocks per SM. With 132 SMs, max concurrent blocks = 132 * 32 = 4224. So 640 is well within limits. Let me write the final kernel. One more thing: I need to make sure to include `<cstdint>` or not? `int` is fine. But I might want to check if there are any include issues with `load_inline`. Actually, I realize I should be a bit careful about `load_inline`. The auto-generated pybind code will be in `cpp_sources`, so I need to make sure my function declaration is in `cpp_sources`. The CUDA code with the kernel definition goes in `cuda_sources`. Let me finalize: ```python import math import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline CUDA_SOURCE = r""" #include <torch/extension.h> #include <cuda_runtime.h> #include <math.h> template <int HD> __global__ void gqa_causal_attention_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int B, int T, int H_q, int H_kv, float scale ) { int num_warps_per_block = blockDim.x / 32; int warp_id = threadIdx.x / 32; int lane = threadIdx.x % 32; int global_warp = blockIdx.x * num_warps_per_block + warp_id; int total_warps = gridDim.x * num_warps_per_block; int total_elements = B * H_q * T; for (int w = global_warp; w < total_elements; w += total_warps) { int b = w / (H_q * T); int rem = w % (H_q * T); int h = rem / T; int t = rem % T; int group_size = H_q / H_kv; int kv_h = h / group_size; const float* q_ptr = q + ((b * T + t) * H_q + h) * HD; float q_val = q_ptr[lane]; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; for (int s = 0; s <= t; ++s) { const float* k_ptr = k + ((b * T + s) * H_kv + kv_h) * HD; float k_val = k_ptr[lane]; float partial = q_val * k_val; // Warp reduction #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { partial += __shfl_down_sync(0xffffffff, partial, offset); } float score = partial * scale; const float* v_ptr = v + ((b * T + s) * H_kv + kv_h) * HD; float v_val = v_ptr[lane]; // Broadcast score from lane 0 to all lanes score = __shfl_sync(0xffffffff, score, 0); if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; out_acc = out_acc * diff + v_val; max_score = score; } else { float w = expf(score - max_score); sum_exp += w; out_acc += w * v_val; } } out_acc /= sum_exp; float* out_ptr = out + ((b * T + t) * H_q + h) * HD; out_ptr[lane] = out_acc; } } torch::Tensor gqa_causal_attention_forward( torch::Tensor q, torch::Tensor k, torch::Tensor v ) { int B = q.size(0); int T = q.size(1); int H_q = q.size(2); int HD = q.size(3); int H_kv = k.size(2); auto out = torch::empty_like(q); float scale = 1.0f / sqrtf(static_cast<float>(HD)); int total_elements = B * H_q * T; int threads_per_block = 64; int num_warps_per_block = threads_per_block / 32; int blocks = (total_elements + num_warps_per_block - 1) / num_warps_per_block; if (HD == 32) { gqa_causal_attention_kernel<32><<<blocks, threads_per_block>>>( q.data_ptr<float>(), k.data_ptr<float>(), v.data_ptr<float>(), out.data_ptr<float>(), B, T, H_q, H_kv, scale ); } else { TORCH_CHECK(false, "Unsupported head_dim: ", HD); } return out; } """ CPP_SOURCE = r""" #include <torch/extension.h> torch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v); """ gqa_cuda = load_inline( name="gqa_cuda_kernel", cpp_sources=CPP_SOURCE, cuda_sources=CUDA_SOURCE, functions=["gqa_causal_attention_forward"], extra_cuda_cflags=["-O3", "--use_fast_math"], verbose=False, ) class ModelNew(nn.Module): def __init__(self): super().__init__() def forward(self, q, k, v): return gqa_cuda.gqa_causal_attention_forward(q, k, v) ``` Wait, there's a potential issue with `load_inline`. The auto-generated pybind module in `cpp_sources` might not compile correctly if I don't handle the includes properly. Let me make sure `cpp_sources` has the right declaration. Also, I used `r"""` raw strings to avoid escaping issues. Good. One concern: `__shfl_down_sync` and `__shfl_sync` require compute capability >= 3.0, which is fine. But do I need to set architecture flags? `load_inline` should handle this automatically based on the detected GPU. But to be safe, I can add `extra_cuda_cflags` with the arch. Actually, `torch.utils.cpp_extension.load_inline` automatically detects the GPU arch and adds `-gencode arch=compute_XX,code=sm_XX`. So I don't need to worry. Another concern: the kernel uses `__restrict__` on pointers. This is fine in CUDA. Let me also think about numerical precision. The reference uses `torch.softmax` which does: 1. subtract max 2. exp 3. divide by sum My online algorithm does the same thing mathematically. The order of operations is slightly different due to the online updates, but the result should be identical within floating point precision. However, when `max_score` is updated, the `diff = exp(max_score - score)` where `max_score` is the OLD max and `score` is the NEW max. So `max_score - score < 0`, and `diff = exp(negative) < 1`. For the old values, we multiply by `diff`. This is numerically stable. For the `else` branch, `w = exp(score - max_score)` where `max_score` is the current max. So `score - max_score <= 0`, and `w <= 1`. Also numerically stable. The potential issue is when `max_score` is updated multiple times. The algorithm is provably correct, and the final result matches exact softmax. The only differences would be due to floating point rounding. With fp32, the tolerance is 1e-4. Let me check if my kernel passes this. Wait, there's one more thing. The reference computes: ```python scores = (k_hist * q_t.unsqueeze(0)).sum(dim=-1) * scale attn = torch.softmax(scores, dim=0) out[b, t, h] = (attn.unsqueeze(-1) * v_hist).sum(dim=0) ``` For t=0, scores has length 1. softmax over 1 element gives 1.0. Output is v_0. In my kernel, for t=0: - s=0: score = dot(q_0, k_0) * scale - max_score = score, sum_exp = 1.0 - out_acc = v_0 - After loop: out_acc / sum_exp = v_0 Correct. One more thing: I need to make sure `total_elements` is computed correctly. B=2, H_q=8, T=80: total_elements = 2*8*80 = 1280. threads_per_block=64, num_warps_per_block=2. blocks = (1280 + 2 - 1) / 2 = 640. Total warps = 640 * 2 = 1280. Each warp handles exactly 1 element. I think this is solid. Let me compile and test. Actually, one optimization: I should preload q_val before the loop, which I already do. k_val and v_val are loaded in each iteration. Since each warp accesses the same k_s and v_s for all 32 lanes (consecutive d values), this is perfectly coalesced. And since different warps in the same block might access the same (b, s, kv_h) if they process the same (b, h) but different t, there's L1 cache reuse. Wait, with 2 warps per block and 640 blocks, what are the chances that two warps in the same block access the same (b, s, kv_h)? Our mapping: global_warp = blockIdx.x * 2 + warp_id. Elements are mapped linearly: w = 0, 1, 2, ..., 1279. For block 0: warps 0 and 1. These map to w=0 and w=1. w=0: b=0, h=0, t=0 w=1: b=0, h=0, t=1 Both have b=0, h=0, so kv_h=0. Yes! They access the same (b, s, kv_h) sequences. In iteration s, warp 0 accesses k[0, s, 0, :] and warp 1 accesses k[0, s, 0, :]. Since they're in the same block, L1 cache should help. But actually, the two warps might not be in sync. Warp 0 processes t=0 (1 iteration), warp 1 processes t=1 (2 iterations). They will diverge. In fact, warps in the same block can run independently. Warp 0 finishes quickly (1 iteration), while warp 1 takes 2 iterations. For larger differences (t=0 vs t=79), the divergence is huge. But since warps are independent scheduling units, this is fine. The SM scheduler can run warp 0 to completion, then warp 1, or interleave them. Actually, since warps don't synchronize, they won't share the L1 cache for k_s at the same time. But that's not a big issue. For better cache sharing, I could map warps so that warps in the same block have similar t values or the same (b, h). In our linear mapping, consecutive warps in a block have consecutive w values, which means same (b, h) and consecutive t. So warps in block 0 process t=0,1; block 1 processes t=2,3; etc. This means warps in the same block have similar numbers of iterations (difference of 1), which is good. And they access the same (b, h, kv_h), so same k and v sequences. Some cache benefit, but not huge. Wait, actually w = b * H_q * T + h * T + t. Block 0 contains w=0,1 -> (b=0,h=0,t=0) and (b=0,h=0,t=1) Block 1 contains w=2,3 -> (b=0,h=0,t=2) and (b=0,h=0,t=3) ... Block 39 contains w=78,79 -> (b=0,h=0,t=78) and (b=0,h=0,t=79) Block 40 contains w=80,81 -> (b=0,h=1,t=0) and (b=0,h=1,t=1) So consecutive blocks all have the same (b=0, h=0) for blocks 0-39. These will all access the same k and v sequences! But they're different blocks, so they run on different SMs or sequentially on the same SM. There's no shared L2 cache benefit across SMs? Actually there is - L2 cache is shared. So if block 0 loads k[0,0,0,:] and block 1 also needs it, it might be in L2. But block 1 might run on a different SM, so it accesses L2, which is slower than L1 but faster than global memory. This is all fine. The data is tiny (320KB total), so it should mostly fit in L2 cache anyway. OK, I think my kernel is good. Let me write it and try to compile. One more tiny optimization: I can avoid the `__shfl_sync` for broadcasting `score` if I instead make sure `warp_reduce_sum` returns the same value in all lanes. But `__shfl_down_sync` only returns the result in lane 0. So I need the broadcast. Wait, actually there's a trick: if the warp reduction uses `__shfl_xor_sync` instead, the result ends up in all lanes. ```cpp #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { partial += __shfl_xor_sync(0xffffffff, partial, offset); } ``` With XOR shuffle: - offset=16: lane i gets value from lane i^16 - After this, lanes 0..15 have sum of pairs (0,16), (1,17), etc. - offset=8: lane i gets from i^8 - Continues until offset=1 - After full reduction, ALL lanes have the total sum! This is slightly better because it avoids the extra `__shfl_sync` broadcast. Let me use XOR shuffle instead. Actually, I need to double check. With `__shfl_xor_sync`: - After offset=16: lane 0 has val[0]+val[16], lane 1 has val[1]+val[17], etc. - After offset=8: lane 0 has (val[0]+val[16]) + (val[8]+val[24]) - After offset=4, 2, 1: lane 0 has sum of all 32 lanes. - And the pattern is symmetric, so lane 1 also ends up with sum of all. Yes! So `__shfl_xor_sync` reduction puts the result in all lanes. Perfect. I'll use this. Final `warp_reduce_sum`: ```cpp __inline__ __device__ float warp_reduce_sum(float val) { #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { val += __shfl_xor_sync(0xffffffff, val, offset); } return val; } ``` This saves one `__shfl_sync` instruction per iteration. OK, let me write the complete code now and compile it. One minor thing: I should add `#include <cuda.h>` or just use `<cuda_runtime.h>`. The shuffle and expf functions are declared in `<cuda_runtime.h>` or built-in. Actually, `__shfl_xor_sync` is a built-in CUDA intrinsic, no special include needed beyond `<cuda_runtime.h>`. Let me also think about whether I need `float` or `__half` or something. The problem says fp32 precision, and inputs are float32. Wait, the reference says `torch.softmax` with fp32. So my kernel should use float. Let me write the complete Python module now. Also, I should think about `load_inline` parameters: - `extra_cuda_cflags=["-O3", "--use_fast_math"]`: good for optimization. - `verbose=False`: to avoid clutter. Actually, when I submit, verbose output might not show. Let me use verbose=False. One potential issue: `load_inline` compiles at module load time. If there's a compilation error, I'll see it. Let me make sure the code is correct. Wait, I need to be careful about `TORCH_CHECK`. I need to include `<torch/extension.h>` in the CUDA source. But `TORCH_CHECK` is defined there. Another thing: in the host wrapper function, I need to make sure to return the tensor, and catch any CUDA errors. Actually, `<<<...>>>` launches are asynchronous. Returning the tensor immediately is fine - PyTorch will synchronize before using the tensor. One more thing: I should handle the default stream. PyTorch uses the default CUDA stream, so the kernel runs there. All good. Let me finalize and compile.
call → compile_kernel
callcompile_kernel { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n__inline__ __device__ float warp_reduce_sum(float val) {\n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n val += __shfl_xor_sync(0xffffffff, val, offset);\n }\n return val;\n}\n\ntemplate <int HD>\n__global__ void gqa_causal_attention_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv,\n float scale\n) {\n int num_warps_per_block = blockDim.x / 32;\n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int global_warp = blockIdx.x * num_warps_per_block + warp_id;\n int total_warps = gridDim.x * num_warps_per_block;\n int total_elements = B * H_q * T;\n\n for (int w = global_warp; w < total_elements; w += total_warps) {\n int b = w / (H_q * T);\n int rem = w % (H_q * T);\n int h = rem / T;\n int t = rem % T;\n\n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n\n const float* q_ptr = q + ((b * T + t) * H_q + h) * HD;\n float q_val = q_ptr[lane];\n\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc = 0.0f;\n\n for (int s = 0; s <= t; ++s) {\n const float* k_ptr = k + ((b * T + s) * H_kv + kv_h) * HD;\n float k_val = k_ptr[lane];\n\n float partial = q_val * k_val;\n float score = warp_reduce_sum(partial) * scale;\n\n const float* v_ptr = v + ((b * T + s) * H_kv + kv_h) * HD;\n float v_val = v_ptr[lane];\n\n if (score > max_score) {\n float diff = expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n out_acc = out_acc * diff + v_val;\n max_score = score;\n } else {\n float w = expf(score - max_score);\n sum_exp += w;\n out_acc += w * v_val;\n }\n }\n\n out_acc /= sum_exp;\n float* out_ptr = out + ((b * T + t) * H_q + h) * HD;\n out_ptr[lane] = out_acc;\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(\n torch::Tensor q,\n torch::Tensor k,\n torch::Tensor v\n) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n\n auto out = torch::empty_like(q);\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n\n int total_elements = B * H_q * T;\n int threads_per_block = 64;\n int num_warps_per_block = threads_per_block / 32;\n int blocks = (total_elements + num_warps_per_block - 1) / num_warps_per_block;\n\n if (HD == 32) {\n gqa_causal_attention_kernel<32><<<blocks, threads_per_block>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv, scale\n );\n } else {\n TORCH_CHECK(false, \"Unsupported head_dim: \", HD);\n }\n\n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_kernel\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 26.7s turn · 6.7s LLM · 0.0s tools
reasoning
Good, it compiled. Now let me run the correctness check.
call → run_correctness
callrun_correctness { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n__inline__ __device__ float warp_reduce_sum(float val) {\n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n val += __shfl_xor_sync(0xffffffff, val, offset);\n }\n return val;\n}\n\ntemplate <int HD>\n__global__ void gqa_causal_attention_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv,\n float scale\n) {\n int num_warps_per_block = blockDim.x / 32;\n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int global_warp = blockIdx.x * num_warps_per_block + warp_id;\n int total_warps = gridDim.x * num_warps_per_block;\n int total_elements = B * H_q * T;\n\n for (int w = global_warp; w < total_elements; w += total_warps) {\n int b = w / (H_q * T);\n int rem = w % (H_q * T);\n int h = rem / T;\n int t = rem % T;\n\n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n\n const float* q_ptr = q + ((b * T + t) * H_q + h) * HD;\n float q_val = q_ptr[lane];\n\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc = 0.0f;\n\n for (int s = 0; s <= t; ++s) {\n const float* k_ptr = k + ((b * T + s) * H_kv + kv_h) * HD;\n float k_val = k_ptr[lane];\n\n float partial = q_val * k_val;\n float score = warp_reduce_sum(partial) * scale;\n\n const float* v_ptr = v + ((b * T + s) * H_kv + kv_h) * HD;\n float v_val = v_ptr[lane];\n\n if (score > max_score) {\n float diff = expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n out_acc = out_acc * diff + v_val;\n max_score = score;\n } else {\n float w = expf(score - max_score);\n sum_exp += w;\n out_acc += w * v_val;\n }\n }\n\n out_acc /= sum_exp;\n float* out_ptr = out + ((b * T + t) * H_q + h) * HD;\n out_ptr[lane] = out_acc;\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(\n torch::Tensor q,\n torch::Tensor k,\n torch::Tensor v\n) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n\n auto out = torch::empty_like(q);\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n\n int total_elements = B * H_q * T;\n int threads_per_block = 64;\n int num_warps_per_block = threads_per_block / 32;\n int blocks = (total_elements + num_warps_per_block - 1) / num_warps_per_block;\n\n if (HD == 32) {\n gqa_causal_attention_kernel<32><<<blocks, threads_per_block>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv, scale\n );\n } else {\n TORCH_CHECK(false, \"Unsupported head_dim: \", HD);\n }\n\n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_kernel\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← run_correctness (ok)
okrun_correctness run_correctness PASSED: (5 / 5) trials all matched the reference. Numerical precision: max_abs_err=8.34e-07 mean_abs_err=3.22e-08 max_rel_err=1.26e-01
turn 3 · 1 tool call(s) · 48.0s turn · 48.0s LLM · 18,968 chars (click to expand)
turn 348.0s turn · 48.0s LLM · 0.0s tools
reasoning
The kernel passes correctness. Let me try a more optimized version to improve speed further. Key optimization ideas: 1. Preload Q into registers for each warp to avoid repeated global loads 2. Use more warps per block to better utilize SM resources 3. Consider using shared memory for sequential loading of K and V vectors 4. Process multiple query heads per warp if head_dim is small For head_dim=32, each warp can process one query vector at a time. With 2 warps per block, each block processes 2 query vectors independently. One issue: the current kernel processes each query position with a single warp, re-reading the same K/V history from global memory. Since head_dim=32 is small (128 bytes), and K/V are read in order, the accesses should be coalesced within each warp but each warp accesses different K/V elements sequentially. Let me try a different approach: use shared memory to load a tile of K/V vectors, and then all warps in the block can compute on this shared tile. This could improve memory reuse. For example: - Organize blocks by (b, h, tile_of_t) - Each block handles one (b, h) pair for a range of query positions t - Load K/V into shared memory in a tile and reuse across threads Actually, a more standard CUDA GQA kernel design: Use 1 block per (b, h, t) — too many blocks but each block is small (1 warp). Or use 1 block per (b, h) that computes all t positions using sequential processing with shared memory for K/V. Let me look at what configurations might give best occupancy on H100. For H100 with 132 SMs: - If I use 1 warp per block -> very small block, low occupancy - If I use 1024 threads per block (32 warps) -> 33 blocks per SM max, good occupancy Let me try: 1 block per (b, kv_h) that computes all queries in that group: - Block has 256 threads = 8 warps - Each warp handles one query position t (or multiple t's) - Total (b, kv_h) blocks = 2 * 2 = 4 blocks — not enough! Better: 1 block per (b, h, chunk_of_t) where chunk is e.g., 1 or 4 t positions. For chunk=1: 2*8*80 = 1280 blocks. 128 threads each (4 warps) -> 10 blocks per SM. That's reasonable. Let me redesign the kernel to use shared memory for K/V and process chunks: One warp computes one t position. We load chunks of K/V into SMEM. Let me use a simpler but more aggressive design: tightly pack warps and use vectorized loads for K/V. Alternative idea: the reference is very slow (O(B*H_q*T^2*D) with Python loops). Even a basic CUDA kernel is much faster. But I want to maximize speedup. Let me check the theoretical numbers: - B=2, H_q=8, T=80, D=32 - Total FLOPs = B*H_q*T*(T+1)/2 * D (dot products) * 2 (for V weighted sum) = 2*8*3240*32*2 ≈ 3.3M FLOPs - Memory reads: Q=2*80*8*32*4=~164KB, K=2*80*2*32*4=~41KB, V=~41KB - This is strongly memory bandwidth bound for large T but with T=80 and repeated accesses, the K/V memory traffic dominates. With the naive kernel, each warp reads K/V independently. For K: - Each (b, h, t) reads (t+1) K vectors - Total K reads = B*H_q * sum(t=0 to 79, t+1)*D = 2*8*3240*32 = 1.66M floats = 6.6MB - But K original size is only 0.04MB — we're reading K~160x times more than needed! This is very cache-friendly on GPU since the same K vectors are accessed by many warps concurrently. On H100, with 80KB L1 cache per SM and the entire K dataset being ~80KB, it should mostly hit in cache. Actually K is sized: B*T*H_kv*D = 2*80*2*32 = 10240 floats = 40KB. This easily fits in L1 cache! So the naive kernel might actually be reasonably fast due to L1 cache hits. But let me still try to optimize further with shared memory for K and V, which eliminates cache contention. For a block that handles a chunk of t values for one (b, h), we can load all needed K/V into shared memory. Wait, actually the K/V needed for a single (b, h) up to t=T is 80 vectors = 80*32*4 = ~10KB per (b,h). With shared memory limit of 227KB, we could fit multiple (b,h) pairs. But block-level parallelism exists across (b, h, t). Let me stick with 1 warp per t and just optimize the memory access pattern. One optimization: ensure all threads in a warp access the same K/V address (broadcast), avoiding bank conflicts. Currently each thread in the warp accesses a different element of k_ptr[lane]. Access pattern: - Thread 0 reads k[0], thread 1 reads k[1], ... - This is a strided global load with stride 1. Since float4 loads would be coalesced but we're doing individual float loads... Actually for coalesced memory access: threads 0-31 accessing consecutive addresses in memory. Is that what we have? Memory layout: [B][T][H][D] Address of k[b,t,h,d] = base + ((b*T + t)*H + h)*D + d With D=32 and H=2, for a given (b,t,h), the elements d=0..31 are contiguous. When warp lane accesses d=lane_idx, addresses are lane_idx apart → perfect coalescing! Great, so K and V loads are coalesced. But Q loads are also coalesced. The main issue is that different warps access different (b,t,h) combinations, leading to arbitrary memory access patterns. But since the working set (K+V) is ~80KB and L1 is ~130KB per SM, it should be mostly cached. Let me try increasing parallelism: use more threads per block (256) and have each warp process one t position, but have 8 warps per block process 8 consecutive t positions for the same (b, h). This way, all 8 warps access the same K[b, 0:t+1, h] and V[b, 0:t+1, h] simultaneously, improving cache/L1 sharing. Let me implement this: group warps within a block by (b, h) and have each block process 8 consecutive t's. Actually, with T=80 and H_q=8, we have B*H_q*(T/8) = 2*8*10 = 20 blocks if each block processes 8 t's. That's too few blocks for 132 SMs! So instead, we need to keep enough parallelism. If each warp processes one (b, h, t), and we have multiple warps per block, but spread them across different (b, h, t)'s rather than grouping by (b, h). Let me just try increasing the total number of warps and using a smaller number of blocks to amortize block overhead. For example: 128 threads = 4 warps per block, blocks = 320 (gridDim.x = 320) Total warps = 1280, which matches total_elements (1280). So one warp per element. This gives 320 blocks, which is more than 132 SMs. Good utilization. But actually, with all the computation being light and memory-bound, more blocks doesn't necessarily help if they're all reading from the same cache. Let me try another optimization: prefetching Q and keeping it in registers, but also using a different approach where we process multiple t positions in sequence within a thread to increase instruction-level parallelism and register pressure. Actually, I think the most impactful optimization would be to: 1. Use shared memory to store a tile of K/V 2. Have each block load a tile of K/V vectors from global memory to shared memory once 3. All warps in the block compute on this shared tile For T=80, we can compute a block that processes T_SIZE t positions. Each t needs K[0:t+1], so the total K needed by a block processing t positions from T_START to T_START+T_SIZE-1 is K[0:T_START+T_SIZE]. If T_SIZE=16, max K vectors needed = 80. In shared memory: 80*32*sizeof(float) = ~10KB per (b, h). But again, if we dedicate a block to one (b, h) and process all T=80 positions within it, we'd need: - Load all 80 K and 80 V vectors = 20KB total into SMEM - Then compute all 80 t positions within the block - 80 t positions with 32 threads each (1 warp per t) = 80 warps needed That's too many warps for one block. We can process 4 t positions per block (4 warps, 128 threads), each needing up to K[0:T_START+4]. For sequential blocks processing (b, h, chunk), each block loads its needed K/V prefix. Actually this is getting complex. Let me just submit the current kernel — it already passes correctness and is definitely much faster than the reference with Python loops. But wait, I should check if there are any other low-hanging fruit: 1. Vectorization: use float4 or float2 loads/stores 2. Reduce register pressure For head_dim=32, we could load 8 float4 vectors. But float4 requires aligned memory, and D=32 is divisible by 4, so it works. Let me try a quick optimization using float4 loads: Actually, I'll just stick with the current kernel. It uses 1 warp per (b,h,t) with lane per dimension, which is already efficient. The main thing is that compared to the Python reference, even a naive CUDA kernel is 1000x+ faster. Wait, I should make sure the kernel uses enough parallelism. With total_elements=1280 and each warp handling one element, and using 2 warps per block (64 threads), we get 640 blocks. That seems fine. With 132 SMs on H100, each SM gets ~5 blocks. 640 blocks is reasonable launch configuration. But with H100, we can handle more blocks or use larger blocks. Let me increase to 4 warps per block (128 threads) to get 320 blocks. 320 blocks / 132 SMs = ~2.4 blocks/SM, which is still good. Actually fewer blocks might be better to reduce launch overhead but still keep GPU busy. 128 threads/block and 320 blocks is fine. Actually there's another concern: the correctness test might be checking atomic correctness but timing could be affected by cache thrashing if too many blocks access the same K/V. But T=80 is tiny so cache should handle it. Hmm, actually I realize there's an issue with my kernel: I don't call cudaDeviceSynchronize() in the host function, and I don't check for errors. But PyTorch should handle synchronization through the returned tensor. Let me also consider using `__ldg` (load through read-only cache / texture cache) for K and V, which could help since K and V are read-only and reused. And for even more speedup, we can unroll the inner loop (but T=80, can't fully unroll). I think the kernel is good enough. Let me verify it once more and if it passes, submit it. Actually wait, let me re-check my kernel. I see a potential bug: in the streaming softmax update, when score > max_score, I do: ```c++ out_acc = out_acc * diff + v_val; ``` But v_val is from the NEW position, and when score > max_score, the weight for the new position should be 1.0 (since we normalized by max_score). But the formula: out_new = (out_old * sum_old * exp(max_old - max_new) + v_new * 1.0) / (sum_old * exp(max_old - max_new) + 1.0) But in my code I track sum_exp as the unnormalized sum. Let me trace through: Initially: max_score = -inf, sum_exp = 0, out_acc = 0 First iteration: score = s0 - score > max_score (-inf) - diff = exp(-inf - s0) = 0 [Wait, max_score is -inf, so exp(-inf - s0)... is this undefined?] Actually exp(-inf) = 0, so diff = 0. - sum_exp = sum_exp * diff + 1.0 = 0 + 1 = 1.0 - out_acc = out_acc * diff + v_val = 0 + v0 = v0 - max_score = s0 Next: score = s1 Case 1: s1 > s0 - diff = exp(s0 - s1) - sum_exp = 1.0 * diff + 1.0 = diff + 1 - out_acc = v0 * diff + v1 - max_score = s1 The weighted average would be: (v0 * exp(s0 - s1) + v1) / (exp(s0 - s1) + 1) = (v0 * exp(s0 - s1) + v1 * exp(0)) / (exp(s0 - s1) + exp(0)) = sum(v_i * exp(s_i - s1)) / sum(exp(s_i - s1)) Correct! Case 2: s1 <= s0 - w = exp(s1 - s0) - sum_exp = 1.0 + w - out_acc = v0 + w * v1 = (v0 * exp(0) + v1 * exp(s1 - s0)) / (exp(0) + exp(s1 - s0)) = (v0 * exp(s0 - s0) + v1 * exp(s1 - s0)) / (exp(s0 - s0) + exp(s1 - s0)) = sum(v_i * exp(s_i - s0)) / sum(exp(s_i - s0)) Correct! So the algorithm is correct. The first iteration where max_score = -inf and diff = exp(-inf - s0): exp(-inf - s0) = exp(-inf) = 0 (since adding a finite number to -inf still gives -inf). Wait, CUDA exp(-inf) = 0? Yes, exp(-inf) approaches 0. But is there a risk of NaN? Let's check: diff = expf(max_score - score) where max_score = -INFINITY and score is finite. max_score - score = -inf expf(-inf) = 0 sum_exp = 0 * 0 + 1 = 1 out_acc = 0 * 0 + v = v No NaN, should work. OK, the kernel is correct. Let me try to make it slightly faster by adding `__ldg` and float4 loads. Actually, let me try a version with float2 or float4 loads: For head_dim=32, float4 loads would process 4 floats per thread, so 32 threads load 128 floats = 4 head vectors per instruction. But we only need one head vector (32 floats). With 32 threads and float4, each thread loads one float4 (4 floats), covering the entire 32 floats in one coalesced transaction. Let me implement float4 loads: For this, memory must be 16-byte aligned. PyTorch tensors are typically 256-byte aligned, so accessing the start of any row should be 16-byte aligned. Let me write a version with float4 loads to reduce memory instructions. Actually, for simplicity and correctness, I'll just submit the current version. The task says to be faster than the reference, and a CUDA kernel replacing Python loops will easily be orders of magnitude faster. But I can try one small optimization: use 256 threads per block for better SM utilization. Let me change to 256 threads (8 warps), which means 160 blocks. That's more than 132 SMs, so we get good occupancy. Actually wait, with 8 warps per block and 160 blocks total, we might get block-level scheduling issues. But 160 blocks for 132 SMs means ~1.2 blocks per SM, which is acceptable. Let me go with 256 threads per block:
call → compile_kernel
callcompile_kernel { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n// Use float4 for vectorized loads\nstruct Align4 { float4 a; };\n\ntemplate <int HD>\n__global__ void gqa_causal_attention_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv,\n float scale\n) {\n int num_warps_per_block = blockDim.x / 32;\n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int global_warp = blockIdx.x * num_warps_per_block + warp_id;\n int total_warps = gridDim.x * num_warps_per_block;\n int total_elements = B * H_q * T;\n int vec_per_head = HD / 4;\n\n for (int w = global_warp; w < total_elements; w += total_warps) {\n int b = w / (H_q * T);\n int rem = w % (H_q * T);\n int h = rem / T;\n int t = rem % T;\n\n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n\n const float4* q_vec = reinterpret_cast<const float4*>(\n q + ((b * T + t) * H_q + h) * HD);\n float q_val[4];\n float4 q4 = q_vec[lane % vec_per_head];\n q_val[0] = q4.x; q_val[1] = q4.y; q_val[2] = q4.z; q_val[3] = q4.w;\n\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc[4] = {0.0f, 0.0f, 0.0f, 0.0f};\n\n for (int s = 0; s <= t; ++s) {\n const float4* k_vec = reinterpret_cast<const float4*>(\n k + ((b * T + s) * H_kv + kv_h) * HD);\n float4 k4 = k_vec[lane % vec_per_head];\n float k_val[4] = {k4.x, k4.y, k4.z, k4.w};\n\n float partial = 0.0f;\n #pragma unroll\n for (int d = 0; d < 4; ++d) {\n partial += q_val[d] * k_val[d];\n }\n\n // warp reduce sum\n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n partial += __shfl_xor_sync(0xffffffff, partial, offset);\n }\n float score = partial * scale;\n\n const float4* v_vec = reinterpret_cast<const float4*>(\n v + ((b * T + s) * H_kv + kv_h) * HD);\n float4 v4 = v_vec[lane % vec_per_head];\n float v_val[4] = {v4.x, v4.y, v4.z, v4.w};\n\n if (score > max_score) {\n float diff = __expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n #pragma unroll\n for (int d = 0; d < 4; ++d) {\n out_acc[d] = out_acc[d] * diff + v_val[d];\n }\n max_score = score;\n } else {\n float w = __expf(score - max_score);\n sum_exp += w;\n #pragma unroll\n for (int d = 0; d < 4; ++d) {\n out_acc[d] += w * v_val[d];\n }\n }\n }\n\n #pragma unroll\n for (int d = 0; d < 4; ++d) {\n out_acc[d] /= sum_exp;\n }\n\n float4* out_vec = reinterpret_cast<float4*>(\n out + ((b * T + t) * H_q + h) * HD);\n out_vec[lane % vec_per_head] = make_float4(out_acc[0], out_acc[1], out_acc[2], out_acc[3]);\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(\n torch::Tensor q,\n torch::Tensor k,\n torch::Tensor v\n) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n\n auto out = torch::empty_like(q);\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n\n int total_elements = B * H_q * T;\n int threads_per_block = 256;\n int num_warps_per_block = threads_per_block / 32;\n int blocks = (total_elements + num_warps_per_block - 1) / num_warps_per_block;\n\n TORCH_CHECK(HD == 32, \"Unsupported head_dim: \", HD);\n\n gqa_causal_attention_kernel<32><<<blocks, threads_per_block>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv, scale\n );\n\n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_kernel_v2\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 4 · 1 tool call(s) · 259.0s turn · 259.0s LLM · 88,028 chars (click to expand)
turn 4259.0s turn · 259.0s LLM · 0.0s tools
reasoning
The kernel compiles with shared memory. Now I need to verify that the shared memory indexing is correct. Let me trace this carefully. With seq_len=80, threads=256, blockDim.x=256: - num_warps = 8, 2 iters per warp For warp 0 (src=0): v_smem[0:128] = v[0:128] = v for all 16 heads across positions 0..7 v_smem[128:256] = v[128:256] = v for all 16 heads across positions 8..15 For warp 1 (src=1): v_smem[0:128] = v[256:384] = v for all 16 heads across positions 16..23 v_smem[128:256] = v[384:512] = v for all 16 heads across positions 24..31 So each t/32=0 group of heads is handled as: For t_pos < 16: load src=0 into smem[0:128], iterate s=0..t_pos within it For t_pos >= 16: also load src=1 into smem[128:256] and iterate s values that fall within 16..t_pos Wait, that's wrong! Each t_pos in a group of heads loads from the SAME src indices but v_smem will only contain the data for one t range at a time. Actually, for t_pos=0, the inner loop s=0..0 uses: src=0, offset=0 in smem[0:128], which corresponds to v[b,0,kv_h,:]\n shared_arr[(0*16 + kv_h)*2 + 0] = *(const float2*)&v[((b*80+0)*2+kv_h)*32+0] This seems correct. Let me verify offset computation for t_pos=10 (first half): src=0, offset=10 shared_arr[((10*16 + kv_h)*2 + 0) % 256] = shared_arr[(160 + kv_h)*2 % 256] = shared_arr[( (160+kv_h)%128 )*2] v_data_idx for src=0, offset=10: = ((10*16 + kv_h)*2 + 0) % 128 = (160+kv_h)*2 % 128 = (160%128 + kv_h)*2 = (32+kv_h)*2 Wait, 160 % 128 = 32, so (32+kv_h)*2. But loaded data: shared_load_idx = ((10*16 + kv_h)*2 + 0) % 128 = (160+kv_h)*2 % 128 v_data_idx = ((0*8*16 + kv_h)*2 + 0) when tid=0 = (kv_h*2) % 128 for t_pos=10, s=10 Wait, that's wrong! For t_pos=10, when s=10 in the first data load: shared_load_idx = ((10*16 + kv_h)*2 + 0) % 128 = (160 + kv_h)*2 % 128 = (32+kv_h)*2 v_data_idx = ((0*8*16 + kv_h)*2 + 0) = kv_h*2 when tid=0 For the first data load (src=0, addresses for t=0,1,...,7): tid=0: v_data_idx=0, stores to shared[0], which is v[b,0,kv_h,0..1] ... When t_pos=10, s=10: shared_load_idx = ((10*16 + kv_h)*2 + 0) % 128 = (32+kv_h)*2 But in the FIRST data load, we only loaded src=0 which contained t=0..7 shared[(32+kv_h)*2] was loaded by which tid? In load src=0: tid = tid_in_warp + 0*32 can be 0..31 For tid=0: v_data_idx=0, stores to shared[0] For tid to store to shared[(32+kv_h)*2] = shared[64] when kv_h=0: v_data_idx = 64 => (t_loaded*16 + 0)*2 = 64 => t_loaded*16 = 32 => t_loaded=2 tid = ((2*16 + 0)*2 + 0) / 2 = (32/2) = 16? No wait, tid is justtid_in_warp + src*32 where src=0. tid_in_warp = v_data_idx / 2 = 32, but max tid_in_warp is 31. So no tid loads to shared[64] in the first load! OH NO I SEE THE BUG! The shared memory stores use `((offset*16 + kv_h)*2 + j) % 128` as the index, which is NOT necessarily loaded by any thread when `src=0`. The first src=0 load only covers offsets 0..7 (for the first 8 positions), but for t_pos=10, the offset=10 data is in a later load (src=1, offsets 8..15). Wait, I think I was getting confused. Let me trace more carefully. For a fixed thread (say lane_id=0 in warp 0), it handles: b=0, h=0, t=0 For t=0, the inner loop s=0: shared_load_idx = ((0*16 + 0)*2 + 0) % 128 = 0 For src such that src*32 <= 0 < (src+1)*32: src=0 offset = 0 - 0*32 = 0 In iteration src=0, data loaded: v_data_idx = ((0*8*16 + 0)*2 + 0) = 0 (by tid_in_warp=0) shared[0] = v[b,0,0,0..1] So load is correct! s=0 maps to shared[0] and the data loaded by tid=0. For thread with lane_id=0, b=0, h=0, t=10: In inner loop s=10: shared_load_idx = ((10*16 + 0)*2 + 0) % 128 = 64 src such that src*32 <= 64 < (src+1)*32 => src=2 offset = 64 - 2*32 = 0 Wait, that's wrong! src=2 means the 3rd data load, but each src load only handles 8 positions. src=2 should handle offsets 16..23 for the 3rd batch of positions. But t=10 is in positions 8..15, which should be src=1. Aha! The problem is that `shared_arr[(offset*16 + kv_h)*2 + j]` collapses the 2D offset and kv_h into a 1D index using modulo 128, but we're treating `shared_load_idx` as a flat index in shared memory. The reduction modulo 128 breaks the ability to uniquely identify which src the data came from! Let me reconsider. `shared_load_idx` is in range [0, 127] (after mod 128). We want to find src such that the data at this shared memory location was loaded during iteration src. But the actual layout is: src=0: shared[0..127] gets v for offsets 0..7 src=1: shared[0..127] gets v for offsets 8..15 src=2: shared[0..127] gets v for offsets 16..23 ... So shared[64] loaded in src=1 corresponds to offset=8 + (64/2 % 8) = 8 + 0 = 8, kv_h=0 i.e., v[b,8,0,0..1] But we wanted v[b,10,0,0..1]! The issue is that my shared memory banking strategy uses a modulo-128 wrapping, but each data load overwrites the previous one. When we go to load `shared_load_idx=64` for s=10, we HOPE that the data at shared[64] corresponds to s=10, but it actually corresponds to whatever was last loaded there, which depends on the data loading pattern. Since we load in src order, for a given t_pos, we load src=0,1,2,... until we cover all s<=t_pos. So shared[64] gets overwritten by each src load where some thread stores to shared[64]. Which threads store to shared[64]? For src=0: tid_in_warp=32 => no thread (max 31) For src=1: offset = s - 1*32 = s - 32. We need ((offset*16 + kv_h)*2 + j) % 128 = 64. offset*16*2 + kv_h*2 + j = 64 or 192. Since offset in 0..7 for src=1 (covers s=32..39, but wait...). Actually, I think I mixed up the meaning. Let me re-express. For a fixed t_pos and src iteration, the `offset` is `s - src*32` for a specific s. But in the data loading loop, for src=0, we load offsets 0..7 (8 positions), not offset = tid_in_warp. Wait, in my code: ``` int offset = t_pos - idx_start; int shared_store_idx = ((offset * H_kv + kv_h) * 2 + j) % 128; ``` Here `offset` depends on `t_pos`, and `t_pos` varies per thread (tid_in_warp). So for src=0, the 32 threads in a warp each load a DIFFERENT offset from range 0..7. For lane_id=0, t_pos=0 => offset=0, stores to shared[0] For lane_id=1, t_pos=1 => offset=1, stores to shared[32] Wait, shared_store_idx = ((1*16 + 0)*2 + 0) % 128 = 32. For lane_id=2, t_pos=2 => offset=2, stores to shared[64] So for src=0, lane_id=2 stores to shared[64] the data for t_pos=2, s=2. Later, for lane_id=0 with t_pos=10: s=10 => shared_load_idx = 64 We check: for src=0, we need src*32 <= 64 < (src+1)*32 => src=2 But wait, src=0 loaded to shared[64] data for t_pos=2 (offset=2), and that data corresponded to t_pos=2 in v. However, for t_pos=10, s=10, we NEED data for position 10 in v, not position 2! The issue is that during data loading, each thread loads data for ITS OWN t_pos, so lane_id=2 loaded v_data_idx corresponding to t_pos=2 (which is position 2 in v). But when lane_id=0 later accesses shared[64], it gets data for position 2, not position 10. This confirms my shared memory approach is fundamentally broken for this problem because different threads load data for different t positions, and they can't share the loaded data with each other in a straightforward way. I need to either: 1. Give up on shared memory for v and just load directly from global memory 2. Restructure so that each data load loads data for ALL t positions needed by the warp, not just each thread's own Option 2 is more complex: in each data load iteration, we'd need to load v for ALL `s` values that at least one thread in the warp needs. For 8 threads over a span of t_pos values, this could be up to 8 different s values per iteration. Actually, for a warp where threads handle t_pos = 8*src + 0, 8*src + 1, ..., 8*src + 7 (for 8 threads): In iteration src, we load v for s = 8*src + 0, ..., 8*src + 7. For thread with t_pos < 8*src, it doesn't need any data from this iteration. For thread with t_pos = 8*src + i, it needs s=0..8*src+i, so it needs data for all s in 0..8*src+i. So we'd need to load v for ALL s values from 0 to 8*src+7 (all s up to max t_pos in warp). That's too much. For option 1, let me just load v directly from global memory for each s. Since we're also loading k directly, the main cost is the q load (once) and k/v loads (once per s). This is O(T) loads per head, same as the naive approach, but at least we're processing multiple heads in parallel. Actually, with 256 threads handling B*H_q*T elements, and each thread having a different (b,h,t), the memory access pattern for k and v is quite scattered. But with 8 heads processed per warp, and 2 warps per block, we get some locality. Given the complexity, let me go back to a simpler approach that I know works, and try to optimize it. The original v1 approach worked correctly! Let me re-examine it: - 64 threads per block (2 warps) - Each warp handles one (b,h,t) element - For each s, each thread loads its own k and v and computes dot product The issue was likely performance. Let me try to improve it: - Increase to 256 threads (8 warps) - OR process each position with fewer resources but more parallelism Actually, let me reconsider the original v1. It used 64 threads, 2 warps, each handling one (b,h,t). With T=80, H_q=8, B=2, that's 1280 elements. With 2 warps per block, we'd need many blocks to saturate the GPU. With total warps = blocks * 2, if blocks=20, total warps=40. Each warp handles 1280/40 = 32 elements, so about 32 iterations per warp. That seems fine for latency hiding. But the real question is: why was v1 potentially slow? Let me check if there were any obvious issues: - Each warp loads k and v for its own (b, kv_h, s), so memory access is coalesced within a warp (all threads load the same address for k and v, which is broadcast, efficient). - q is loaded once per warp, also broadcast. - The dot product uses warp shuffle, which is fast. - Each s involves 2 global loads + 1 exp. The main bottleneck is likely the sequential loop over s for each thread. With T=80, 80 iterations. But each iteration has memory divergence (different warps load different addresses). Actually, for warps in the same block, they might access different b,h values, so completely different memory addresses. This means the L1 cache won't help much. Let me try organizing by (b, h) instead, using all threads in a warp to process one t cooperatively? No, with head_dim=32, that's fewer threads than warp size. What if we use one thread block to process all t values for one (b,h), with 32 threads (one warp) processing the 32 head dimensions for a SINGLE t, but launching multiple warps/blocks? But then we'd need B*H_q*seq_len warps, which is 2*8*80=1280 warps. With 164 SMs, that's about 7 warps/SM, plenty of occupancy. Actually, let me just try the v1 approach with larger block size and more blocks. Maybe the issue was insufficient parallelism with only 64 threads. Let me also consider: with T=80, we process 80 timesteps. Each timestep does O(1) work. To speed this up, maybe I can process multiple s values per iteration using vectorized loads or unrolling. Or, I can use a different strategy: compute all scores for one warp first, store them in shared memory, then compute softmax and weighted sum. But with T=80 and 32 threads, each thread could handle 2-3 scores. Let me try yet another approach: for each (b,h), launch 1 block with multiple warps. Each warp handles a range of t values. Within each warp, process one t at a time. With blockSize=256 (8 warps), process 8 t values simultaneously in a block for a fixed (b,h). Each warp handles one t. This way, shared memory can be used to cache q and intermediate results. Let me try implementing this properly. For a fixed (b,h): - Read q for all t into shared memory? q is [T, HD] = 80*32 = 2560 floats = 10KB. Easy to cache. - Read k for all s into shared memory? k is [T, HD] = 80*32 = 2560 floats = 10KB for one kv head. - Read v similarly. Total ~30KB, fits in shared memory! Then each warp can compute attention for one t using cached q, k, v. This eliminates redundant global memory loads! But we have B*H_q = 2*8 = 16 such (b,h) pairs. For 164 SMs, each can handle ~10 thread blocks. Let me implement this approach. For each (b,h) pair, launch one block. Block structure: - dim3(32, 8) = 256 threads: 8 warps, 32 threads each - Each warp processes one t value - 8 warps handle 8 consecutive t values Shared memory: - q_smem: 8*32 = 256 floats (for 8 t positions being processed) - Actually, we need ALL t positions for q? No, each warp only needs its own q_t. But if we want to load q in parallel, we can load 8 t positions together. Wait, a better structure: - Launch B*H_q blocks, each block handles one (b,h) with multiple warps cooperating. - Block has 32 threads (1 warp), but we can't have a block with just 1 warp on modern GPUs efficiently. - Better: block has 256 threads, each block handles one (b,h) but with 8 warps doing something useful. Since each (b,h) needs T=80 outputs, and each warp handles one t, we need 80 warps per (b,h), which is too many for one block. Alternative: each block handles 8 consecutive t values for one (b,h), using 8 warps. Launch ceil(80/8)=10 blocks per (b,h). Total blocks = 16*10 = 160. That's good for 164 SMs! Let me implement this: - grid: (16, 10) or flattened to 160 blocks - Each block: 256 threads (8 warps of 32) - Each block handles one (b,h) and t_range [block_t_start, block_t_start+7] - Each warp in the block handles one t in the range. Shared memory per block: - Load q for the 8 t positions: 8*32 floats - Load k for ALL s positions (0..t_max where t_max is max t in block): at most 80*32 floats - Load v for ALL s positions: at most 80*32 floats - Total: 8*32 + 80*32 + 80*32 = 256 + 2560 + 2560 = 5376 floats = 21.5 KB Actually k and v per (b, kv_h) need to be loaded. For H_kv=2, each block handles one h, so one kv_h. Loading 80*32 floats = ~10KB each. With q for 8 positions = 1KB. Total ~21KB per block. Well under H100's 227KB limit. Steps: 1. Compute block_id -> (b, h, t_block) b = block_id / (H_q * num_t_blocks) ? Actually: num_t_blocks = (T + 7) / 8 = 10 block_idx -> b = block_id / (H_q * num_t_blocks), rem = block_id % (H_q * num_t_blocks) h = rem / num_t_blocks, t_block = rem % num_t_blocks t_start = t_block * 8, t_end = min(t_start + 8, T) 2. Each warp (warp_id) handles t = t_start + warp_id (if < t_end) 3. Separate phases: a. Cooperative load of k and v into smem b. Cooperative load of q for this block's t positions into smem c. Each warp computes its t's attention using cached data d. Write output Wait, but k and v depend on kv_h = h / group_size. So for a fixed h, kv_h is fixed. Let me implement this carefully. Actually, having 10 blocks per (b,h) with each block handling 8 t values seems like a lot of blocks (160). On H100 with 164 SMs, we'd get near-perfect occupancy. But 160 blocks each with 256 threads = 40960 threads total. That's not a lot. Let me calculate: H100 can run ~2048 threads per SM * 164 = ~335K threads concurrently. We're using only ~41K. So there's room for more. But maybe it's fine since each thread does a lot of work (80 iterations). Hmm, actually the issue is that each warp doesn't do enough parallel work per s iteration. It's sequential with 80 iterations. But with 8 warps in a block, we get 8x parallelism for t values. With 160 blocks, we get 160*8 = 1280 parallel t positions being processed (B*H_q*T total = 1280). So theoretically we process all t positions in parallel, each taking 80 iterations. This might actually be quite fast! Let me implement it. Wait, loading k and v for ALL 80 positions into shared memory for each block is redundant. 160 blocks each loading 80*32 floats of k and v. But k and v for a (b,kv_h) pair are loaded 80*H_q/H_kv = 80*4 = 320 times! That's a lot of redundant loads. Actually, each (b,kv_h) pair is accessed by H_q=8 different h values. And each h value is handled by 10 blocks. So each (b,kv_h) k/v element is loaded 8*10=80 times. With B=2, H_kv=2, total k/v elements = 2*80*2*32 = 10240 each. Total redundant loads = 10240*80 = 819200 floats. That's a lot. But if k and v sit in L2 cache, maybe it's not too bad. Each load is cached. Let me just implement it and see. I'll also try a hybrid: use the v1 style but with shared memory for caching q, and rely on L2 cache for k and v. This avoids redundant smem loads. Actually, for v1, all k/v loads go through L2. With 1280 warps each doing 80 loads, that's 102400 loads of 32 floats each = 3.2M floats. Each float is 4 bytes, so ~13MB of reads. For k and v together, ~26MB. But since they're cached in L2, actual DRAM traffic is much less. With L2 cache of 50MB on H100, these ~26MB of reads might fit, but not for repeated accesses. Let me try implementing the shared-memory version for k and v where each block loads its needed k and v once. Even with redundancy across blocks, at least within a block each warp reuses the same shared k/v. I'll code this up. For simplicity, I'll load k_smem and v_smem for all s from 0 to t_end for each block. Reconsidering: with T=80, each block handling t_start..t_end (<=8 positions), but needing k/v for s=0..t_end-1 (up to 80 positions). The max k/v size needed per block is 80*32 = 2560 floats = 10KB. For 2 arrays (k,v), 20KB. Plus q for 8 positions (1KB). Total ~21KB. 160 blocks * 21KB = 3.36MB total shared memory at peak, but only 164 blocks active at once, so 164*21KB = ~3.4MB. H100 has 228KB per SM * 164 = 37MB shared memory total. So plenty of room. But loading k and v for s=0..t_end-1 into shared memory requires thread cooperation. With 256 threads and 2560 floats to load, each thread loads 10 floats. That's manageable. Actually, let me think about whether this is even worth it. The v1 approach handles each (b,h,t) independently. Each thread loads k and v as needed. The total work is small enough that maybe v1 with proper configuration is already fast enough? Let me just submit v1 with enough parallelism (256 threads, many blocks) and see how it performs. Maybe the reference PyTorch code is actually quite slow due to Python loops, so even a basic CUDA kernel is 100x faster. Let me create a clean v1-style kernel with 256 threads per block and enough blocks to saturate the GPU. I'll also use __expf for speed. Let me also try reducing register pressure and use -maxrregcount. Let me compile and test performance. I'll use the original v1 approach but with more threads and simple FMA. Actually I realize I haven't measured the performance of v1 at all. Let me just run correctness on v1 first to see if it passes: Something I just realized about v2 (shared memory): for correctness to work, during data loading for each src, I need to load v for ALL s values that ANY thread in the warp needs. But with t_pos varying by tid_in_warp, the span of s values needed is very wide. Actually wait, I made a mistake. In the v2 approach, each thread in a warp processes a DIFFERENT (b,h,t), and for a fixed t_block (src), the t positions vary from t_block*32 + tid_in_warp_start to t_block*32 + tid_in_warp_start + 7 (for 8 threads). So when tid_in_warp_start=0, t positions are 0,1,2,3,4,5,6,7 in warp 0 (if handling 8 t values). But my v2 code had each warp handling a different t range, not different t within the same warp. Let me re-read: ``` int tid_in_warp = threadIdx.x % 32; int tid = warp_id * 32 + tid_in_warp; int t_pos = tid + src * 32; ``` So within a warp, t_pos varies from warp_id*32 to warp_id*32 + 31. For warp 0, t_pos = 0..31. For warp 1, t_pos = 32..63. Wait, warp 0 has t_pos = tid_in_warp (0..31). So with 8 threads per warp, we'd have t_pos = 0,1,2,3,4,5,6,7 (assuming num_active_threads=8). Hmm, the code says 8 threads but threadIdx.x goes 0..255, so 32 threads per warp. But we only use the first 8 threads for actual work. So within warp 0, t_pos = 0,1,2,3,4,5,6,7 (only first 8 threads have valid work). For tid_in_warp >= 8, t_pos is >= 8 but < seq_len? Actually the condition `if (t_pos < seq_len)` might be true for many threads. With seq_len=80 and warp 0, t_pos = 0..31 are all < 80. So in one warp, t ranges from 0..31, which means s ranges from 0..t, up to 0..79. In the data load loop, for src=0, we load offsets 0..7. For src=1, offsets 8..15, etc. For thread with t_pos=31, it needs s=0..31, so 4 data load iterations (src=0,1,2,3). The data loaded in each src is for offsets 0..7, 8..15, 16..23, 24..31. BUT, shared memory is reused! In src=0, shared[0..127] contains v for offsets 0..7. In src=1, it OVERWRITES with v for offsets 8..15. So by the time we've loaded src=3, shared[0..127] contains v for offsets 24..31, and data for earlier offsets is GONE. For threads with smaller t_pos, they process s=0..t_pos FIRST, before moving to higher s. Wait, the inner s loop is `for (int s = 0; s <= t_pos; ++s)`. For t_pos=5, it processes s=0,1,2,3,4,5. After each batch of 32 s values, we load the next batch. For s=0..5, we need src=0 only. So it loads src=0, processes s=0..5, then moves to s=6 which needs src=0 (still within 0..7). By the time it finishes s=5, it exits the loop because s<=t_pos means s goes 0,1,2,3,4,5 and stops. Actually, for the shared memory version to work, after loading src=0, we need to process ALL s values that map to src=0 for ALL threads BEFORE loading src=1 (which overwrites). For t_pos=5: s values in src=0 are s=0..5 (since 0..7 is the range, but s only goes to 5). For t_pos=10: s values in src=0 are s=0..7, then it needs more. For t_pos=30: s values in src=0 are s=0..7, then src=1: s=8..15, src=2: s=16..23, src=3: s=24..30. The problem is: after loading src=0, we need to process it for ALL threads before loading src=1. But threads with t_pos<0 don't need any s. Threads with t_pos=0 only need s=0. Threads with t_pos=1 need s=0..1. So the pattern should be: For each src: Load data for offsets [src*32, min((src+1)*32, seq_len)) into shared memory For each thread: If it needs s in this range (s <= t_pos AND s >= src*32 AND s < min((src+1)*32, seq_len)): Process those s values But my original code processes in s order per thread, not in src order for all threads. This is why it's wrong - when a thread with small t_pos finishes its s loop, a thread with large t_pos might still be processing earlier s values, but the shared data has already been overwritten. To fix this, the structure should be: ``` for each src_batch: load k,v for src_batch into smem (cooperatively) __syncthreads() for s in [src_start, min(src_end, t_pos+1)): // use loaded smem __syncthreads() ``` And this needs to be done per thread. Since threads in a block handle different (b,h,t), we need syncthreads to stay in sync. But threads with different t_pos will process different numbers of s values within each batch. Actually, this can work! We just need to structure it as: ``` for (int src = 0; src < num_src_batches; ++src) { int src_start = src * 32; int src_end = min(src_start + 32, seq_len); // Load k and v for this batch into smem // Each thread loads some portion // ... __syncthreads(); // Each thread processes s in [src_start, min(src_end, t_pos+1)) for (int s = src_start; s < min(src_end, t_pos + 1); ++s) { // read k_smem[s - src_start] and v_smem[s - src_start] // compute score, update softmax state // ... } __syncthreads(); } ``` The key is that after loading a batch, ALL threads (in the block) must finish processing that batch before ANY thread loads the next batch. `__syncthreads()` ensures this. Now the question is: can we organize threads so that they synchronize efficiently? For 256 threads per block = 8 warps, all processing different (b,h,t): - After loading src batch 0, warp 0 might process s=0..7 for t_pos=31, while warp 1 processes s=0..7 for its own t_pos, etc. - If some warps have t_pos < 7, they process fewer s values and wait at the next __syncthreads(). - This should work fine! Let me implement this properly. I'll have each block handle 8 different (b,h) pairs or 8 different t values for the same (b,h). Actually, to maximize divergence tolerance, let me have each block handle 8 contiguous t values for the same (b,h). That way, all warps in the block process the same b,h with t values 0..7 or 8..15 etc. They all need the SAME k,v data for their s values, which means each batch of k,v is equally useful to all warps. Block layout: - 1 block per (b, h, t_block) where t_block = t/8 - 8 warps per block, warp i handles t = t_block*8 + i - 32 threads per warp, each handling 1 head_dim element But head_dim=32, so 32 threads is exactly 1 warp. 8 warps handle 8 t values. Wait, I already thought of this! Let me implement it properly now. Shared memory per block: - q_smem[8][32]: q for 8 t positions - k_smem[32][32]: k for 32 s positions (one batch) Wait, for a batch src, we might need up to 32 s positions. But with 256 threads, we can load 32*32 = 1024 floats at a time. k for one batch is 32 positions * 32 dims = 1024 floats, which is exactly 4KB. For v, another 4KB. q for 8 positions is 1KB. Total 9KB per block. Plenty. But actually, for t_block=9 (t=72..79), threads need s up to 79. With batch size 32, we need ceil(80/32)=3 batches (0..31, 32..63, 64..79 = 16 positions). Let's use batch size 16 instead of 32 to reduce shared memory. 16*32=512 floats = 2KB per array. Total 4KB for k+v per batch. Or batch size 8: 8*32=256 floats = 1KB per array, total 2KB. Since T=80, we'd need 10 batches. With more batches, more __syncthreads() overhead. Let me use batch size 16 for 5 batches. Actually, since we have 256 threads, loading 16*32 floats requires only 2 threads per float, very efficient. Let me code this properly. Block: 256 threads, but only 8 warps doing actual work (one per t value). The other threads? We only need 8 warps * 32 threads = 256, so all threads are used. Hmm wait, 8 warps * 32 threads = 256. But each warp handles one t and all 32 dims. So 8 warps = 8 t values. Perfect. Actually, can I use fewer threads per warp? No, for head_dim=32, we need 32 threads per t value for parallel dot product. With warp shuffles, 32 threads = 1 warp. So: 1 block = 8 warps = 8 t values. We need ceil(80/8) = 10 blocks per (b,h). With B=2, H_q=8, total blocks = 2*8*10 = 160. Each block handles a specific (b, h, t_block) where t_block goes 0..9 and each block covers 8 t positions starting at t_block*8. But wait, for small t (e.g., t=0), we only need s=0 (1 position). For t=7, we need s=0..7 (8 positions). For t=79, we need s=0..79 (80 positions). So blocks near t_block=0 do very little work, while blocks near t_block=9 do lots of work. This imbalance isn't ideal for occupancy. Actually, each block runs for as long as its slowest warp takes. The last warp in block 0 (t=7) needs 8 iterations. In block 9 (t=71..79), the last warp (t=79) needs 80 iterations. So block 9 takes 10x longer than block 0. But since we have many blocks, some SMs will run block 0 and immediately pick up another block, so overall throughput should be decent. Total work: sum over all (b,h,t) of (t+1) iterations. = B*H_q * T*(T+1)/2 = 2*8*80*81/2 = 51840 iterations. This is the same regardless of block structure. Let me write the kernel properly: ```cuda __global__ void kernel(const float* q, const float* k, const float* v, float* out, int B, int T, int H_q, int H_kv) { const int HD = 32; const int WARPS_PER_BLOCK = 8; const int BATCH_SIZE = 16; // positions per k/v batch int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK; // 10 int block_idx = blockIdx.x; int b = block_idx / (H_q * num_t_blocks); int rem = block_idx % (H_q * num_t_blocks); int h = rem / num_t_blocks; int t_block = rem % num_t_blocks; int t_start = t_block * WARPS_PER_BLOCK; int t_end = min(t_start + WARPS_PER_BLOCK, T); if (t_start >= T) return; int warp_id = threadIdx.x / 32; int lane = threadIdx.x % 32; int t = t_start + warp_id; if (t >= T) return; // Some warps might be idle in the last block // Shared memory // q_smem[8][32]: q for 8 t positions // k_smem[16][32]: k for one batch of 16 s positions // v_smem[16][32]: v for one batch of 16 s positions extern __shared__ float smem[]; float* q_smem = smem; // 8*32 = 256 floats float* k_smem = q_smem + WARPS_PER_BLOCK * HD; // 16*32 = 512 floats float* v_smem = k_smem + BATCH_SIZE * HD; // 512 floats // Load q for this block's t positions // Each warp loads its own q_t, one element per thread if (warp_id < (t_end - t_start)) { int q_idx = ((b * T + (t_start + warp_id)) * H_q + h) * HD + lane; q_smem[warp_id * HD + lane] = q[q_idx]; } __syncthreads(); float q_val = q_smem[warp_id * HD + lane]; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; int num_batches = (t + 1 + BATCH_SIZE - 1) / BATCH_SIZE; for (int batch = 0; batch < num_batches; ++batch) { int s_start = batch * BATCH_SIZE; int s_end = min(s_start + BATCH_SIZE, t + 1); int batch_len = s_end - s_start; // Load k and v for this batch // Total floats to load: batch_len * HD // Threads available: blockDim.x = 256 // Each thread loads: ceil(batch_len * HD / 256) for (int i = threadIdx.x; i < batch_len * HD; i += blockDim.x) { int s_local = i / HD; int d = i % HD; int s = s_start + s_local; int kv_h = h / (H_q / H_kv); int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d; int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d; k_smem[s_local * HD + d] = k[k_idx]; v_smem[s_local * HD + d] = v[v_idx]; } __syncthreads(); for (int s = s_start; s < s_end; ++s) { int s_local = s - s_start; float k_val = k_smem[s_local * HD + lane]; float partial = q_val * k_val; // warp reduce to get dot product #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { partial += __shfl_xor_sync(0xffffffff, partial, offset); } float score = partial * scale; float v_val = v_smem[s_local * HD + lane]; // Online softmax and weighted sum if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; out_acc = out_acc * diff + v_val; max_score = score; } else { float w = expf(score - max_score); sum_exp += w; out_acc += w * v_val; } } __syncthreads(); } out_acc /= sum_exp; int out_idx = ((b * T + t) * H_q + h) * HD + lane; out[out_idx] = out_acc; } ``` Shared memory size = 8*32 + 16*32 + 16*32 = 256 + 512 + 512 = 1280 floats = 5KB. This looks correct! Let me trace through for t=7, warp 7: - num_batches = (7+1+15)/16 = 1 - batch=0: s_start=0, s_end=8, batch_len=8 - Load k/v for s=0..7 into k_smem[0..7][:] - Process s=0..7, reading from k_smem[s_local][:] correctly - Write output for t=7 For t=79, warp 7 in block 9: - t_block=9, t_start=72, t_end=80 - warp 7 handles t=79 - num_batches = (79+1+15)/16 = 5 - Each batch loads 16 positions, but last batch loads only up to s=79 This looks correct! But wait, I need to think about occupancy. Block size is 256 threads. Shared memory is 5KB. On H100, max shared per block is 227KB, so 5KB is tiny. Each SM can handle multiple blocks. Max 32 blocks per SM, limited by 2048 threads / 256 = 8 blocks per SM. With 164 SMs, total concurrent blocks = 164 * 8 = 1312, but we only need 160. So all blocks run concurrently. Each block handles at most 8 t values. Block 9 handles t=72..79. Warp 7 (t=79) needs 80 s iterations, which is the maximum. Assuming each iteration takes ~10 cycles, that's ~800 cycles. With 5 batches, there are 5 sync points. At 800MHz, ~1us per warp. With 8 warps in parallel within each SM running blocks, we get good throughput. Total runtime estimate: 160 blocks * 1us / (164 SMs * 8 blocks/SM) ~ 0.12 us. This is very fast! But wait, the first few batches are smaller. Most of the time is in the last few blocks. Blocks 0..89 handle t up to 79, but only a few blocks actually have t=79. Actually, only block 9 (for each b,h) has warps with high t values. B*H_q = 16, so 16 blocks have maximum t=79. Each of these takes ~5 * (16 iterations+sync) ~ 80+ iterations. The other blocks take less time. So maybe ~16 blocks dominate the runtime. Total "warp-iterations" = 51840 as calculated. With 164*8=1312 concurrent warps, theoretical minimum time = 51840/1312 ~ 40 iterations. But due to synchronization and divergence, actual is higher. I think this should be very fast. Let me code it up! Hmm, but I want to be extra careful about correctness. Let me re-verify the batch loading loop. ``` for (int i = threadIdx.x; i < batch_len * HD; i += blockDim.x) { int s_local = i / HD; int d = i % HD; int s = s_start + s_local; ... } ``` For batch_len=16, HD=32: total floats = 512. With blockDim.x=256, each thread loads 2 floats (or 1 float if i >= 512 for some threads). The loads are coalesced. Actually, for k and v, we're loading from different base addresses. If I load both k and v in the same loop body, the memory access pattern alternates between k and v, which might hurt coalescing. Let me split into two loops or use vectorized loads. Actually, for simplicity and because batch_len is small, let me load k and v in the same loop but ensure the addresses are computed correctly. Since each thread loads its own unique (s,d) pair, and consecutive threads load consecutive elements in d for the same s, this should be coalesced if s and d are arranged properly. Wait, the indexing is: for i=0: s_local=0, d=0. i=1: s_local=0, d=1. ... i=31: s_local=0, d=31. i=32: s_local=1, d=0. So consecutive threads load consecutive d values for the same s. In the original arrays, elements for consecutive d values are also consecutive. So this IS coalesced! For writing to shared memory: k_smem[s_local * HD + d] means consecutive d values for the same s_local are consecutive in smem. So consecutive threads write to consecutive smem addresses. Perfect! But wait, k layout is [B, T, H_kv, HD]. For fixed b and kv_h, k[b,:,kv_h,:] has stride T*H_kv*HD between consecutive s values. But within one s, k[b,s,kv_h,d] and k[b,s,kv_h,d+1] are consecutive. So for thread i and i+1 loading d and d+1 of the same s, they read consecutive memory. This IS coalesced. However, I'm loading k and v in alternating fashion inside the same thread: ``` k_smem[...] = k[k_idx]; v_smem[...] = v[v_idx]; ``` This means each thread does two loads: one from k and one from v. The k and v addresses are far apart in memory. But since all threads are doing the same pattern, it should be fine - the k loads are coalesced among all threads, and the v loads are also coalesced. Let me code this up and test. Actually, I realize I should move the kv_h computation outside the loop since it's constant per block. Let me also try to use more warps per block. With 256 threads = 8 warps, and each block handling one (b,h), we process 8 t values per block. Could we process more? With 16 warps per block = 512 threads, we'd handle 16 t values per block. But we'd need 512 threads per block, which is still within limits. With T=80, we'd need ceil(80/16)=5 blocks per (b,h), total 16*5=80 blocks. Fewer blocks might hurt occupancy. Actually, more t values per block means more shared memory for q_smem: 16*32 = 512 floats vs 256. And the work distribution is the same. Let me stick with 8. One more optimization: instead of having each block handle one (b,h), we could have each block handle multiple (b,h) pairs, using warps for different pairs. This increases block workload but might not improve much. Let me stick with the simpler design and see how it performs. I need to be careful about: 1. `__syncthreads()` placement - must be outside divergent code 2. Warp divergence - some warps might be idle if t_end < 8 3. Correct shared memory size computation For the last block (t_block=9), t_start=72, t_end=80, so 8 t values. All 8 warps are active. For earlier blocks, all 8 warps are also active since t_end = min(80, t_start+8) > t_start. Good, no idle warps. Let me write the complete kernel. ```python import torch import torch.nn as nn from torch.utils.cpp_extension import load_inline CUDA_SOURCE = r''' #include <torch/extension.h> #include <cuda_runtime.h> #include <math.h> __global__ void gqa_causal_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int B, int T, int H_q, int H_kv ) { const int HD = 32; const int WARPS_PER_BLOCK = 8; const int BATCH_SIZE = 16; const int HD_VEC = HD / 4; // for float4 int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK; int block_idx = blockIdx.x; int b = block_idx / (H_q * num_t_blocks); int rem = block_idx % (H_q * num_t_blocks); int h = rem / num_t_blocks; int t_block = rem % num_t_blocks; int t_start = t_block * WARPS_PER_BLOCK; int t_end = min(t_start + WARPS_PER_BLOCK, T); if (t_start >= T) return; int warp_id = threadIdx.x / 32; int lane = threadIdx.x % 32; int t = t_start + warp_id; // Some warps might be idle if t >= T bool valid = (t < T); int group_size = H_q / H_kv; int kv_h = h / group_size; // Shared memory extern __shared__ float smem[]; float* q_smem = smem; float* k_smem = q_smem + WARPS_PER_BLOCK * HD; float* v_smem = k_smem + BATCH_SIZE * HD; // Load q for this block's t range // Each thread in valid warp loads one q element if (valid && lane < HD) { int q_idx = ((b * T + t) * H_q + h) * HD + lane; q_smem[warp_id * HD + lane] = q[q_idx]; } __syncthreads(); float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; int s_max = valid ? t : -1; int num_batches = (s_max + 1 + BATCH_SIZE - 1) / BATCH_SIZE; num_batches = max(num_batches, 0); for (int batch = 0; batch < num_batches; ++batch) { int s_start = batch * BATCH_SIZE; int s_end = min(s_start + BATCH_SIZE, s_max + 1); int batch_len = s_end - s_start; // Cooperative load of k and v into shared memory // Each thread loads zero or more (s,d) pairs for (int i = threadIdx.x; i < batch_len * HD; i += blockDim.x) { int s_local = i / HD; int d = i % HD; int s = s_start + s_local; int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d; int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d; k_smem[s_local * HD + d] = k[k_idx]; v_smem[s_local * HD + d] = v[v_idx]; } __syncthreads(); // Each valid warp processes this batch if (valid) { for (int s = s_start; s < s_end; ++s) { int s_local = s - s_start; float k_val = k_smem[s_local * HD + lane]; float partial = q_val * k_val; // Warp reduce to get dot product #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { partial += __shfl_xor_sync(0xffffffff, partial, offset); } float score = partial * (1.0f / sqrtf(static_cast<float>(HD))); float v_val = v_smem[s_local * HD + lane]; // Online softmax if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; out_acc = out_acc * diff + v_val; max_score = score; } else { float w = expf(score - max_score); sum_exp += w; out_acc += w * v_val; } } } __syncthreads(); } if (valid) { out_acc /= sum_exp; int out_idx = ((b * T + t) * H_q + h) * HD + lane; out[out_idx] = out_acc; } } torch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) { int B = q.size(0), T = q.size(1), H_q = q.size(2), HD = q.size(3); int H_kv = k.size(2); auto out = torch::empty_like(q); const int WARPS_PER_BLOCK = 8; int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK; int num_blocks = B * H_q * num_t_blocks; const int BATCH_SIZE = 16; int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD * 2) * sizeof(float); gqa_causal_kernel<<<num_blocks, 256, smem_size>>>( q.data_ptr<float>(), k.data_ptr<float>(), v.data_ptr<float>(), out.data_ptr<float>(), B, T, H_q, H_kv ); return out; } ''' # load_inline... ``` Hmm, wait. I realized there's an issue with threads being inactive but still participating in __syncthreads(). In CUDA, __syncthreads() requires all threads in the block to reach it. If some threads are "valid=false" and skip the loop, they still need to reach the sync point. But my code has `if (valid)` around the inner computation loop, but NOT around `__syncthreads()`. Let me check: ``` for (int batch = 0; batch < num_batches; ++batch) { // load k,v for (...) { ... } // ALL threads participate __syncthreads(); // ALL threads reach here if (valid) { for (int s = ...) { ... } } __syncthreads(); // ALL threads need to reach here too } ``` But `num_batches` is computed differently for valid and invalid threads! For invalid threads (t >= T), `s_max = -1`, so `num_batches = 0`. Invalid threads won't enter the loop at all, but valid threads WILL enter it. This means invalid threads skip `__syncthreads()` while valid threads hit it. DEADLOCK! I need to have ALL threads compute the same `num_batches`. Since `s_max` varies by warp (each warp has a different t), I need a different approach. Solution: have all threads participate in the maximum number of batches. For a block handling t_start to t_end, the maximum t is t_end-1. So max_batches = ((t_end-1) + 1 + BATCH_SIZE - 1) / BATCH_SIZE = (t_end + BATCH_SIZE - 1) / BATCH_SIZE. Or, even simpler: have ALL warps process ALL batches, but skip the s values that exceed their t. ```cuda int max_t = t_end - 1; int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE; for (int batch = 0; batch < num_batches; ++batch) { // load k, v for this batch // ... __syncthreads(); int s_start = batch * BATCH_SIZE; int s_end = min(s_start + BATCH_SIZE, valid ? t + 1 : 0); for (int s = s_start; s < s_end; ++s) { // process } __syncthreads(); } ``` But for invalid threads, `s_end = 0`, so their inner loop is empty. They still reach both `__syncthreads()` calls. However, there's still an issue with the first `__syncthreads()` inside the `if (valid)` block for q loading. Let me restructure: ```cuda // Load q for ALL warps, but only write if within range if (valid && lane < HD) { q_smem[warp_id * HD + lane] = q[...]; } // No sync needed if all warps write to distinct locations // But we need to ensure all q_smem writes are visible before reads __syncthreads(); float q_val = q_smem[warp_id * HD + lane]; // safe for all, even invalid warps ``` For invalid warps, q_val might contain garbage, but we won't use it since s_end=0 for their loop. Actually, I can simplify: ALL threads in the block should participate in ALL operations, with conditional execution only inside the inner s loop. Let me restructure the code to avoid divergence around __syncthreads(): ```cuda // Compute common values for all threads in block int max_t_in_block = t_end - 1; int num_batches = (max_t_in_block + 1 + BATCH_SIZE - 1) / BATCH_SIZE; // Load q for all warps (even invalid ones write, but we don't care about invalid) int q_t_for_load = t_start + warp_id; // might be >= T // Only load if q_t_for_load < T AND lane < HD if (q_t_for_load < T && lane < HD) { q_smem[warp_id * HD + lane] = q[...]; } __syncthreads(); float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; for (int batch = 0; batch < num_batches; ++batch) { int s_start = batch * BATCH_SIZE; int s_end = min(s_start + BATCH_SIZE, max_t_in_block + 1); int batch_len = s_end - s_start; // Load k,v for this batch for (int i = threadIdx.x; i < batch_len * HD; i += blockDim.x) { int s_local = i / HD; int d = i % HD; int s = s_start + s_local; // Load from global int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d; int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d; k_smem[s_local * HD + d] = k[k_idx]; v_smem[s_local * HD + d] = v[v_idx]; } __syncthreads(); // Each warp processes applicable s values int my_s_end = valid ? min(s_end, t + 1) : s_start; // empty loop for invalid for (int s = s_start; s < my_s_end; ++s) { int s_local = s - s_start; float k_val = k_smem[s_local * HD + lane]; float dot = q_val * k_val; #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { dot += __shfl_xor_sync(0xffffffff, dot, offset); } float score = dot * scale; float v_val = v_smem[s_local * HD + lane]; // online softmax ... } __syncthreads(); } if (valid) { out_acc /= sum_exp; out[...] = out_acc; } ``` This looks correct and safe. All threads in the block execute the same loops and reach the same __syncthreads() calls. But wait, there's still an issue. For q loading: ```cuda int q_t_for_load = t_start + warp_id; if (q_t_for_load < T && lane < HD) { q_smem[warp_id * HD + lane] = q[...]; } ``` For warp_id >= (t_end - t_start), q_t_for_load >= T, so they don't write to q_smem. But they still read from q_smem later: ```cuda float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f; ``` For invalid warps, valid=false, so q_val=0.0f. No issue. But for valid warps, what if there's a __syncthreads() between the write and read? I already have one. Good. Let me also think about the last block. For t_block=9, t_start=72, t_end=80. warp_id=0..7 all have valid t. warp_id=8 would have t=80 >= T=80, so invalid. But wait, we only have 8 warps per block (threadIdx.x/32 with blockDim.x=256). warp_id ranges from 0..7. All 8 warps are valid for t_block=9 since t ranges 72..79. Actually, wait. ceil(80/8) = 10 blocks. t_block=9: t_start=72, t_end=min(80,80)=80. So 8 t values: 72,73,74,75,76,77,78,79. All < 80. Good. For t_block=9, max_t_in_block = 79. num_batches = (79+1+15)/16 = 5. For earlier blocks, say t_block=0: t_start=0, t_end=8. max_t_in_block = 7. num_batches = (7+1+15)/16 = 1. So block 0 does 1 batch, block 9 does 5 batches. This causes warp divergence in the outer loop since some blocks iterate more than others. But different blocks run on different SMs, so there's no intra-block divergence. Each block is uniform. This looks good! Let me implement and test. One optimization: instead of loading k and v together in one loop, I can load only k first, compute scores for the batch, and then load v. But that requires more shared memory for scores and more complex logic. Let me keep it simple. Another idea: use float4 for memory loads. With HD=32, each (s) has 8 float4 elements. Loading one float4 per thread. For batch_len=16, total float4 loads = 16*8=128. With 256 threads, 128 threads each load one float4 for k and one for v. Very efficient! Let me implement with float4: ```cuda float4* k_smem_vec = reinterpret_cast<float4*>(k_smem); float4* v_smem_vec = reinterpret_cast<float4*>(v_smem); for (int i = threadIdx.x; i < batch_len * HD_VEC; i += blockDim.x) { int s_local = i / HD_VEC; int d_vec = i % HD_VEC; int s = s_start + s_local; const float4* k_ptr = reinterpret_cast<const float4*>(k + ((b * T + s) * H_kv + kv_h) * HD); const float4* v_ptr = reinterpret_cast<const float4*>(v + ((b * T + s) * H_kv + kv_h) * HD); k_smem_vec[s_local * HD_VEC + d_vec] = k_ptr[d_vec]; v_smem_vec[s_local * HD_VEC + d_vec] = v_ptr[d_vec]; } ``` And then in the computation loop: ```cuda for (int s = s_start; s < my_s_end; ++s) { int s_local = s - s_start; float4 k4 = k_smem_vec[s_local * HD_VEC + (lane / 4)]; // WRONG! lane maps differently } ``` Hmm, with float4, each thread handles 4 consecutive d values. With HD=32 and 32 threads, thread i handles d = i*4, (i*4+1), (i*4+2), (i*4+3). Let me keep it simple with float and just use scalar loads. The memory bandwidth should still be fine for this small problem. Actually, with batch_len up to 16, each batch load is 16*32=512 floats for k and 512 for v. At 4 bytes each, 4KB per batch. Over 5 batches, 20KB of reads per block. For 160 blocks at once (if they all run), that's 3.2MB from L2. This should be very fast. Let me just go with the scalar version for simplicity and correctness. Wait, one more thing: the warps within a block might execute the inner s loop at different speeds. After all warps in a block finish the inner loop, they hit __syncthreads() and proceed to the next batch. But within the inner loop, all warps in a block execute independently (no sync). The __syncthreads() after the inner loop ensures all writes to shared memory are done before the next batch loads. But since each warp only READS from shared memory in the inner loop (doesn't write), there's no data hazard. The __syncthreads() after the inner loop is mainly to ensure the next batch loads don't start before all reads are done (though CPU-style memory ordering might make this unnecessary on modern GPUs, it's safer to include it). Actually, on modern GPUs, within a block, memory is coherent. We don't strictly need the second __syncthreads() if k_smem is only read in the inner loop and written only in the load loop. But to be safe and ensure the next batch's loads don't corrupt data still being read, I'll keep it. Let me now write the final code. Wait, I just realized: in the load loop, `batch_len` varies by batch. For the last batch in a block, batch_len might be smaller than BATCH_SIZE. But the smem arrays are sized for BATCH_SIZE. When we write k_smem[s_local * HD + d] for s_local < batch_len, it's fine. For s_local >= batch_len, we don't write. Good. But what about reading? In the inner loop, `s < my_s_end` and `my_s_end <= s_end = s_start + batch_len`. So `s_local = s - s_start < batch_len`. Good. Now let me also think about the case where a block's max_t_in_block is small. For block 0, max_t_in_block = 7, num_batches = 1. The single batch has batch_len = 8. We load 8 positions into k_smem and v_smem. All 8 warps process s=0..7 (for their respective t values). This works. But for warp with t=0 (in block 0), my_s_end = min(8, 0+1) = 1. So it only processes s=0. The k_smem contains positions 0..7, but only s=0 is used. That's fine, some loaded data goes unused. Let me now write the full Python code for the kernel. Actually, I want to double-check one thing. When I do: ```cuda float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f; ``` All 32 threads in a warp read the same warp_id. For warp 0, all threads read q_smem[0*32 + 0..31]. For warp 1, q_smem[1*32 + 0..31], etc. This is fine because each warp handles its own t. No bank conflicts. One more optimization: instead of the inner `for (int s = s_start; s < my_s_end; ++s)` being scalar, I could process 2 or 4 s values at a time using vectorized loads. But this complicates the online softmax. Let me skip it. Let me also consider using a different warp organization. Currently, each warp handles one t with all 32 threads computing the dot product in parallel. What if each thread computes the full sequence for one t, processing all dimensions sequentially? Then we'd have 32 threads per warp handling 32 different t values, but each thread would need to do HD=32 multiplies per s, which is more work per thread. Scalar per thread approach: - Each thread handles one (b,h,t,dim_idx) and computes the full attention - For each s, load k_smem[s][dim_idx] and v_smem[s][dim_idx] - But the dot product requires summing across dims, which is now a cross-thread operation... No, each thread can't compute the full dot product alone unless it processes all dims sequentially. With 32 threads in a warp, one approach is: - Have each warp handle 32 t values (one per thread) - For each s: have all 32 threads load k for their respective dims? No, each thread has a FIXED dim, not fixed t. Wait, if each thread handles a DIFFERENT (b,h,t) but SAME dim: - Thread 0: (b,h,0,d=0) - Thread 1: (b,h,1,d=0) -- different t! - For s: each thread needs k[b,s,kv_h,d=0]. Same k for all threads! Broadcast. - Score computation: each thread computes q[b,t,h,0] * k[b,s,kv_h,0]. This is only partial. - Need to sum across d=0..31. With 32 threads handling different t, how do we sum across dims? We'd need another warp or cross-warp reduction. This gets complicated. The v1 approach where each warp handles one (b,h,t) with all threads across dims is much cleaner. OK, I'm confident in the shared memory batching approach. Let me write and test it. ```python CUDA_SOURCE = r''' #include <torch/extension.h> #include <cuda_runtime.h> #include <math.h> __global__ void gqa_causal_kernel( const float* __restrict__ q, const float* __restrict__ k, const float* __restrict__ v, float* __restrict__ out, int B, int T, int H_q, int H_kv ) { const int HD = 32; const int WARPS_PER_BLOCK = 8; const int BATCH_SIZE = 16; const int THREADS_PER_BLOCK = 256; int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK; int block_idx = blockIdx.x; int b = block_idx / (H_q * num_t_blocks); int rem = block_idx % (H_q * num_t_blocks); int h = rem / num_t_blocks; int t_block = rem % num_t_blocks; int t_start = t_block * WARPS_PER_BLOCK; int t_end = min(t_start + WARPS_PER_BLOCK, T); int max_t = t_end - 1; if (t_start >= T) return; int warp_id = threadIdx.x / 32; int lane = threadIdx.x % 32; int t = t_start + warp_id; bool valid = (t < T); int group_size = H_q / H_kv; int kv_h = h / group_size; float scale = 1.0f / sqrtf(static_cast<float>(HD)); // Shared memory: q for WARPS_PER_BLOCK positions, k/v for one batch extern __shared__ float smem[]; float* q_smem = smem; // [WARPS_PER_BLOCK][HD] float* k_smem = q_smem + WARPS_PER_BLOCK * HD; // [BATCH_SIZE][HD] float* v_smem = k_smem + BATCH_SIZE * HD; // [BATCH_SIZE][HD] // Load q for all valid t positions in this block int q_t_load = t_start + warp_id; if (q_t_load < T && lane < HD) { int q_idx = ((b * T + q_t_load) * H_q + h) * HD + lane; q_smem[warp_id * HD + lane] = q[q_idx]; } __syncthreads(); float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f; float max_score = -INFINITY; float sum_exp = 0.0f; float out_acc = 0.0f; int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE; for (int batch = 0; batch < num_batches; ++batch) { int s_start = batch * BATCH_SIZE; int s_end = min(s_start + BATCH_SIZE, max_t + 1); int batch_len = s_end - s_start; // Cooperative load of k and v for (int i = threadIdx.x; i < batch_len * HD; i += THREADS_PER_BLOCK) { int s_local = i / HD; int d = i % HD; int s = s_start + s_local; int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d; int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d; k_smem[s_local * HD + d] = k[k_idx]; v_smem[s_local * HD + d] = v[v_idx]; } __syncthreads(); // Each valid warp processes its needed s values from this batch int my_s_end = valid ? min(s_end, t + 1) : s_start; // Unroll small loops for common cases // But since batch_size=16 and t varies, just use a regular loop #pragma unroll 1 for (int s = s_start; s < my_s_end; ++s) { int s_local = s - s_start; float k_val = k_smem[s_local * HD + lane]; float dot = q_val * k_val; // Warp shuffle reduction across 32 lanes #pragma unroll for (int offset = 16; offset > 0; offset /= 2) { dot += __shfl_xor_sync(0xffffffff, dot, offset); } float score = dot * scale; float v_val = v_smem[s_local * HD + lane]; // v_smem is written by the load above and read here // With __syncthreads() between load and compute, this is safe // Online softmax if (score > max_score) { float diff = expf(max_score - score); sum_exp = sum_exp * diff + 1.0f; out_acc = out_acc * diff + v_val; max_score = score; } else { float w = expf(score - max_score); sum_exp += w; out_acc += w * v_val; } } __syncthreads(); } if (valid) { out_acc /= sum_exp; int out_idx = ((b * T + t) * H_q + h) * HD + lane; out[out_idx] = out_acc; } } torch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) { int B = q.size(0); int T = q.size(1); int H_q = q.size(2); int HD = q.size(3); int H_kv = k.size(2); auto out = torch::empty_like(q); const int WARPS_PER_BLOCK = 8; const int BATCH_SIZE = 16; int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK; int num_blocks = B * H_q * num_t_blocks; int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD + BATCH_SIZE * HD) * sizeof(float); gqa_causal_kernel<<<num_blocks, 256, smem_size>>>( q.data_ptr<float>(), k.data_ptr<float>(), v.data_ptr<float>(), out.data_ptr<float>(), B, T, H_q, H_kv ); return out; } ''' ``` This should work! Let me compile and test. I'll also add __launch_bounds__ to help the compiler. Actually, I want to be extra careful about the `if (valid)` inside the batch loop. For invalid threads, `my_s_end = s_start`, so the inner for loop `for (int s = s_start; s < my_s_end; ++s)` is empty. This means invalid threads skip the inner loop but still reach the __syncthreads() at the end of the batch loop. The first __syncthreads() after the load is reached by all threads because it's outside any condition. Good! Wait, let me re-examine. The structure is: ``` for batch: // load loop: all threads participate __syncthreads(); // all threads reach // compute: some threads have empty loop for (s = ...; s < my_s_end; ++s) { ... } __syncthreads(); // all threads MUST reach ``` Since invalid threads have my_s_end = s_start, they don't execute the inner loop body. They go straight from `for (int s = ...)` (which evaluates to false immediately) to the __syncthreads(). Good. But wait, I put the `if (valid)` in the compute section's loop bounds, not as an outer `if`. The loop itself is not inside `if (valid)`. The variable `my_s_end` is computed conditionally, but the loop is executed by all threads. Since `my_s_end = s_start` for invalid threads, the loop body never runs. But do invalid threads still execute the loop header and condition check? Yes. And after the loop, they reach __syncthreads(). Good. One subtlety: `valid` is a `bool` computed per thread. All 32 threads in a warp might not have the same `valid` value (only warp_id determines this). For warp_id=0, all 32 threads have valid=true (assuming t_start < T). For warp_id=7, also valid=true for t_start+7 < T. So within each warp, all threads have the same valid value. This means no warp-level divergence on `valid`. Great, let me compile and test. I'll also consider whether to use `constexpr` for constants to help with compiler optimizations. Actually, one more thing: I should check if `num_t_blocks` calculation is correct. `num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK` For T=80, WARPS_PER_BLOCK=8: num_t_blocks = (80+7)/8 = 87/8 = 10. Good. Block idx mapping: `b = block_idx / (H_q * num_t_blocks)` For block_idx in [0, H_q*num_t_blocks - 1] = [0, 79]: b=0 For block_idx in [80, 159]: b=1. Good. `rem = block_idx % 80` For block_idx=0: rem=0, h=0/10=0, t_block=0%10=0 For block_idx=79: rem=79, h=79/10=7, t_block=79%10=9 For block_idx=80: rem=0, h=0, t_block=0 Good, this maps correctly. Let me write the actual code now. I'll also add some comments. Actually, let me think about one more thing: is it beneficial to process multiple (b,h) pairs in one block instead of just one? If each block handles only one (b,h), then different (b,h) pairs are in different blocks, which is good for parallelism. With 160 blocks, we have enough work. Processing multiple (b,h) in one block would reduce block count and might hurt occupancy. With 160 blocks and 164 SMs, we get roughly 1 block per SM. But each block handles 8 t values (warps). On H100, each SM can handle up to 32 blocks. So having 1 block per SM means we're not fully utilizing block-level parallelism within an SM. But each block has 8 warps, and each SM can handle up to 64 warps. So 8 warps * 1 block = 8 warps per SM, with room for 56 more warps from other blocks. Actually, blocks from different SMs can't share. If we have 160 blocks and 164 SMs, 4 SMs are idle. But the 160 active SMs each run 1 block with 8 warps. Total active warps = 1280. H100 has 164*64 = ~10496 warp slots. So occupancy is only ~12%. That's actually quite low! To increase occupancy, we could either: 1. Have each block handle more warps (process more t values or multiple (b,h) pairs) 2. Use smaller thread blocks but launch more blocks per SM Option 1: process 2 (b,h) pairs per block. Block handles 16 warps = 512 threads. Two different (b,h) pairs, each with 8 t values. Shared memory doubles to ~10KB. Still fine. Total blocks = 80. With 80 blocks on 164 SMs, occupancy is even worse. Option 2: use 32 threads per block (1 warp), process 1 t value per block. Total blocks = B*H_q*T = 1280. With 1280 blocks on 164 SMs, ~7.8 blocks per SM. Each SM handles 7-8 blocks * 1 warp = 7-8 warps, still not great. Actually, what matters is total concurrent warps, not blocks per se. If each block has 8 warps and we have 160 blocks, total warps = 1280. With 164 SMs, warps per SM = 1280/164 ≈ 7.8. H100 can hold 64 warps per SM, so theoretical occupancy is 7.8/64 ≈ 12%. This seems low. But the actual bottleneck might be instruction latency and memory latency, not occupancy. With 7.8 warps per SM, we have some ability to hide latency. But to improve occupancy, we could process multiple (b,h,t) triples per warp. With each warp handling 1 t value, and T=80, if we could pack more work into each warp... Actually, with head_dim=32, one warp handles all 32 dimensions for one t. Could one warp handle 2 t values using 16 threads each? Then we'd need cross-half-warp reductions. That's possible but more complex. Or, instead of having 32 threads all work on the same (b,h,t) but different dims, we could have 4 threads work on the same (b,h,t,d) but with 8 different (b,h,t) values, processing dims sequentially. Actually, here's an idea: each thread processes one (b,h,t) but loops over all 32 dimensions for each s. - Thread 0: (b,h,t=0), loops d=0..31 for each s - Thread 1: (b,h,t=1), loops d=0..31 for each s - ... But we have 32 threads per warp, each handling a different t. For the dot product, each thread computes its own dot product across all 32 dims (sequentially). No warp shuffle needed! Then the score is computed entirely within one thread. This trades warp parallelism for more independent work per thread. With 32 threads handling 32 different t values, we get 32 parallel attention computations per warp, each with 32 sequential dim iterations. This could be great for occupancy because each block can handle many more elements. For example, a block of 256 threads could handle 256 different t values (spread across multiple b,h pairs). But wait, for the (b,h) pairs, T=80. If one warp handles 32 t values, but there are only 80 t values and 8 h values, we'd have 80*8=640 t values per b. With 256 threads, we handle 256 t values per block. We'd need 3 blocks per b. Or maybe organize as: each block handles a range of t values across all (b,h). Actually, with this approach, each thread is completely independent! No need for warp shuffles or shared memory coherency within a warp. This means we can have many more concurrent threads and blocks. Let me explore this approach. Per-thread approach: - Each thread handles one (b,h,t) and all dims - For each s=0..t: load k and v for s, loop over d=0..31 to compute dot product - Since dims are processed sequentially, each thread needs 32 loads and 32 FMAs per s But we already load both k and v. If we process dims sequentially, we load k[b,s,kv_h,:] and v[b,s,kv_h,:] for each s. Each of these is 32 floats. For t=80, that's 80*32 = 2560 floats of k and 2560 of v per thread. That's a lot of memory traffic per thread. With the warp-parallel approach, each thread only loads 1 float of k and 1 float of v per s. For t=80, 80 floats each. Much less! So the warp-parallel approach is better for memory bandwidth. Let me stick with it. To improve occupancy with the warp-parallel approach, I need more warps running concurrently. Instead of having each block handle 8 warps for one (b,h), I can have each block handle fewer warps but more blocks. If each block handles 1 warp = 32 threads, then for B*H_q = 16 (b,h) pairs with 80 t values each, total warps = 1280. Total blocks = 1280. With 164 SMs, 1280/164 ≈ 7.8 blocks per SM. Each block has 1 warp, so 7.8 warps per SM. Same occupancy! What if each block handles 4 warps = 128 threads? Total blocks = 320. Blocks per SM = 320/164 ≈ 1.95. Warps per SM = 1.95 * 4 ≈ 7.8. Same! The occupancy is fundamentally limited by the total number of independent work units (1280). Each work unit is one (b,h,t), and each warp handles one work unit. With 1280 warps and 164 SMs, we get ~7.8 warps per SM. To increase occupancy, we'd need to process multiple (b,h,t) per warp, increasing total warps. But each warp only has 32 threads, and each (b,h,t) ideally uses all 32 threads for dim parallelism. To handle 2 (b,h,t) per warp, we'd need to split the 32 threads into 2 groups of 16, each handling one (b,h,t). With 16 threads per (b,h,t), we process 16 dims per thread sequentially. For each s: - Thread 0: process dims 0..15 of (b,h,t0) - Thread 16: process dims 0..15 of (b,h,t1) Dot product per group requires a half-warp shuffle. This reduces memory efficiency (each thread loads 16 floats per s instead of 1), but doubles total warps to 2560, giving ~15.6 warps per SM. Actually, maybe I can make this work. Let me think about it more carefully. Actually wait, for occupancy, maybe 7.8 warps per SM is enough? On H100, each warp has ~10-20 instruction slots. With 7.8 warps, we can hide some latency. But more importantly, each warp does a LOT of work (80 iterations with memory loads). Since warps within an SM can interleave, warp 0 might stall on memory while warp 1 executes. With 7.8 warps per SM, and each warp taking maybe 1000 cycles, we get decent utilization. Let me not overthink this and just implement a clean kernel. Also, I realized my v1 kernel already exists and was correct. Maybe it was fast enough? Let me go back to the original v1 but with larger block size and more sensible thread configuration. Original v1: 64 threads per block, each warp processes one (b,h,t). Total elements = 1280. With blocks = 64, we'd have 64/2 = 32 blocks? No, 64 threads = 2 warps, so 2 elements per block. We'd need 640 blocks. That's plenty! But in v1, each thread independently computes for its own element. Since the mapping is by thread, not warp, threads within a warp might be working on completely different b,h values, but they all load k and v the same way (independent loads). This is fine. Let me re-examine the v1 kernel more carefully. It used `threadIdx.x % 32` as lane and computed global warp index. With 64 threads per block (2 warps), each warp processes different elements but in lockstep. The shuffle works correctly within each warp. The issue with v1 was likely that it only used 64 threads per block with not enough blocks to saturate the GPU. Wait, total_elements = 1280. With num_warps_per_block = 2, blocks = 640. Total warps = 1280. Same as before! OK so v1 already had the right total warps, but maybe the issue was something else. Let me just try v1 with 256 threads per block (8 warps) and blocks = 160. Total warps = 1280. Same total warps! Hmm, v1 with ANY block size should give the same total number of warps since total_elements / elements_per_warp = 1280 / 1 = 1280 warps. And each warp processes one element. The performance might be limited by memory bandwidth or instruction throughput, not occupancy. Each warp does ~80 iterations, each with 2 global loads, several shuffles, and math. With limited L1 cache capacity, k and v for different (b,h) might thrash. With v3 (shared memory batched), we reduce global memory loads by loading k and v once per batch and reusing within a block. For a block of 8 warps handling 8 t values, each batch of 16 k/v positions is loaded once and read by up to 8 warps * min(16, t_max) times. If t_max is large (like 79), the last batch (16 positions) is read by all 8 warps. That's 8 reads per load. Good cache reuse! For earlier batches, some warps might only read a subset. For example, batch 0 (positions 0..15): warp with t=72 reads all 16, warp with t=0 reads only 1. But at least we loaded them once instead of each warp loading independently. So v3 should have much better memory efficiency. Let me implement it and test performance. OK, I'll code up v3 now. After extensive analysis, I'm pretty confident in the approach. One final concern: when using `num_batches` based on `max_t`, some warps in a block might not need any work from a batch. For example, in block 0 (t=0..7), batch 0 has s=0..7. Warp with t=0 only needs s=0. Warp with t=7 needs s=0..7. The batch is loaded once and all warps read from it. Good. But if num_batches=1 for block 0, we don't load more data than needed. The batch_len is min(16, 8) = 8. For block 1 (t=8..15): max_t=15, num_batches = 1. batch_len = 16. But the block only needs s=0..15, so all 16 positions are used. Perfect. For block 5 (t=40..47): max_t=47, num_batches = (47+1+15)/16 = 3. Batch 0: s=0..15, batch 1: s=16..31, batch 2: s=32..47. All fully utilized by the last warp (t=47). This is very efficient memory-wise! One more thing: for t_block = num_t_blocks - 1, max_t might not be t_start + WARPS_PER_BLOCK - 1 if T is not a multiple of WARPS_PER_BLOCK. For T=80 and WARPS_PER_BLOCK=8, it is exact. But let me handle the general case. Let me code it up! Hmm, actually I just realized. The v3 approach has a lot of shared memory reads (for k and v) and the inner loop processes one s at a time. But since k_smem and v_smem are in shared memory, the reads are very fast (a few cycles). The main cost is the sequential nature of the s loop. To speed up, I could unroll the s loop. But with variable `my_s_end`, unrolling is tricky. I could process 2 s values at a time within a batch. Actually, for batches where all warps in a block have `my_s_end >= s_end` (i.e., all t values in the block are >= s_end - 1), we know the loop runs exactly `batch_len` times. For a block handling t=8..15 and batch 0 (s=0..7), all warps need all 8 s values? No, warp with t=8 needs s=0..8, so for batch 0 it needs s=0..7. Warp with t=15 needs s=0..15, so for batch 0 it needs s=0..7. Yes! All warps in block 1 need all 8 s values from batch 0. In general, for a block handling t_start..t_end-1, batch with s=0..15: all warps need all these s values if t_start >= 15. If t_start < 15, some warps might only need a subset. So for later blocks (higher t_start), the inner loop is fully utilized by all warps. For earlier blocks, there's some waste. This is fine. Overall, the approach should still be much faster than independent loads. Let me finalize and submit. Actually, I'll also try a slightly different version: instead of having each block handle 8 t values for one (b,h), I can have each block handle WARPS_PER_BLOCK t values for the SAME (b,h) but arrange that consecutive t values are handled by consecutive warps. This helps with the `my_s_end` uniformity: consecutive t values in a block mean consecutive warps have similar amounts of work. Wait, that's what I already have! Warp 0 handles t_start, warp 1 handles t_start+1, etc. So warps in a block handle consecutive t values. Their `my_s_end` values differ by 1, creating some divergence but not a lot. For example, block 9 (t=72..79): batch 0 has s=0..15. Warp 7 (t=79) needs all 16. Warp 0 (t=72) needs all 16 too. Actually, for batch 0, s=0..15, and all t in 72..79 need all of these. So all warps execute the full loop for batch 0. For batch 4 (s=64..79 in a block with max_t=79): s_start=64, s_end=80, batch_len=16. Warp 7 (t=79) needs s=64..79 = full batch. Warp 0 (t=72) needs s=64..72 = 9 values. So there's a difference. But the difference is at most 8 out of 16, so about 50% divergence at worst. Since all warps reach the __syncthreads() together, the block runs at the speed of the slowest warp. But different blocks run in parallel on different SMs, so it's fine. OK, let me write the code now. Let me refine the kernel one more time: One more optimization: since all threads in a block need the same `kv_h`, I can compute it once and store it in a shared variable or just recompute it. With h being constant per block, all threads compute the same kv_h. Also, `scale` and `num_batches` are the same for all threads in a block. The compiler might optimize these automatically, but I can declare them without `__shared__` since they're uniform. For `b`, `h`, and `t_block`, they should ideally be in registers since they're uniform across the block. Let me write the code.
call → compile_kernel
callcompile_kernel { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n__global__ void gqa_causal_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv\n) {\n const int HD = 32;\n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n const int THREADS_PER_BLOCK = 256;\n \n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n \n int block_idx = blockIdx.x;\n int b = block_idx / (H_q * num_t_blocks);\n int rem = block_idx % (H_q * num_t_blocks);\n int h = rem / num_t_blocks;\n int t_block = rem % num_t_blocks;\n \n int t_start = t_block * WARPS_PER_BLOCK;\n int t_end = min(t_start + WARPS_PER_BLOCK, T);\n int max_t = t_end - 1;\n \n if (t_start >= T) return;\n \n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int t = t_start + warp_id;\n bool valid = (t < T);\n \n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n \n // Shared memory: q for WARPS_PER_BLOCK positions, k/v for one batch\n extern __shared__ float smem[];\n float* q_smem = smem; // [WARPS_PER_BLOCK][HD]\n float* k_smem = q_smem + WARPS_PER_BLOCK * HD; // [BATCH_SIZE][HD]\n float* v_smem = k_smem + BATCH_SIZE * HD; // [BATCH_SIZE][HD]\n \n // Load q for all valid t positions in this block\n int q_t_load = t_start + warp_id;\n if (q_t_load < T && lane < HD) {\n int q_idx = ((b * T + q_t_load) * H_q + h) * HD + lane;\n q_smem[warp_id * HD + lane] = q[q_idx];\n }\n __syncthreads();\n \n float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f;\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc = 0.0f;\n \n int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE;\n \n for (int batch = 0; batch < num_batches; ++batch) {\n int s_start = batch * BATCH_SIZE;\n int s_end = min(s_start + BATCH_SIZE, max_t + 1);\n int batch_len = s_end - s_start;\n \n // Cooperative load of k and v\n for (int i = threadIdx.x; i < batch_len * HD; i += THREADS_PER_BLOCK) {\n int s_local = i / HD;\n int d = i % HD;\n int s = s_start + s_local;\n \n int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n k_smem[s_local * HD + d] = k[k_idx];\n v_smem[s_local * HD + d] = v[v_idx];\n }\n __syncthreads();\n \n // Each valid warp processes its needed s values from this batch\n int my_s_end = valid ? min(s_end, t + 1) : s_start;\n \n for (int s = s_start; s < my_s_end; ++s) {\n int s_local = s - s_start;\n float k_val = k_smem[s_local * HD + lane];\n float dot = q_val * k_val;\n \n // Warp shuffle reduction across 32 lanes\n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n dot += __shfl_xor_sync(0xffffffff, dot, offset);\n }\n float score = dot * scale;\n \n float v_val = v_smem[s_local * HD + lane];\n \n // Online softmax\n if (score > max_score) {\n float diff = expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n out_acc = out_acc * diff + v_val;\n max_score = score;\n } else {\n float w = expf(score - max_score);\n sum_exp += w;\n out_acc += w * v_val;\n }\n }\n __syncthreads();\n }\n \n if (valid) {\n out_acc /= sum_exp;\n int out_idx = ((b * T + t) * H_q + h) * HD + lane;\n out[out_idx] = out_acc;\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n \n auto out = torch::empty_like(q);\n \n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n int num_blocks = B * H_q * num_t_blocks;\n \n int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD + BATCH_SIZE * HD) * sizeof(float);\n \n gqa_causal_kernel<<<num_blocks, 256, smem_size>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv\n );\n \n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_v3\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← compile_kernel (ok)
okcompile_kernel compile_kernel PASSED: kernel compiled without errors.
turn 5 · 1 tool call(s) · 10.6s turn · 10.6s LLM · 7,057 chars (click to expand)
turn 510.6s turn · 10.6s LLM · 0.0s tools
reasoning
Hmm, two version both compiled. Let me test the correctness of the v3 first. Actually, I had multiple kernels and the naming might be cached. Let me try a different name to avoid caching. Actually wait - the first version's correctness test passed with max error 8.34e-07. Let me just try the block-based v3 first to see if the error amounts and speed are good. If there are issues I'll fix them. Let me also think - in v3, the problem is that I'm configuring num_t_blocks wrong. With num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK and T=80, WARPS_PER_BLOCK=8, num_t_blocks=10. But my block dimension is 256 threads / 32 = 8 warps, so I'm processing 8 positions per block, which is 10 blocks per head. Then num_blocks = B * H_q * num_t_blocks = 2 * 8 * 10 = 160 blocks. That's fine. Wait, but kernel scheduling and occupancy: Each block processes at most 8 positions (one per warp) and T=80 max. With 160 blocks and 132 SMs, it's fine. Let me test v3.
call → run_correctness
callrun_correctness { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n__global__ void gqa_causal_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv\n) {\n const int HD = 32;\n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n const int THREADS_PER_BLOCK = 256;\n \n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n \n int block_idx = blockIdx.x;\n int b = block_idx / (H_q * num_t_blocks);\n int rem = block_idx % (H_q * num_t_blocks);\n int h = rem / num_t_blocks;\n int t_block = rem % num_t_blocks;\n \n int t_start = t_block * WARPS_PER_BLOCK;\n int t_end = min(t_start + WARPS_PER_BLOCK, T);\n int max_t = t_end - 1;\n \n if (t_start >= T) return;\n \n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int t = t_start + warp_id;\n bool valid = (t < T);\n \n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n \n extern __shared__ float smem[];\n float* q_smem = smem;\n float* k_smem = q_smem + WARPS_PER_BLOCK * HD;\n float* v_smem = k_smem + BATCH_SIZE * HD;\n \n int q_t_load = t_start + warp_id;\n if (q_t_load < T && lane < HD) {\n int q_idx = ((b * T + q_t_load) * H_q + h) * HD + lane;\n q_smem[warp_id * HD + lane] = q[q_idx];\n }\n __syncthreads();\n \n float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f;\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc = 0.0f;\n \n int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE;\n \n for (int batch = 0; batch < num_batches; ++batch) {\n int s_start = batch * BATCH_SIZE;\n int s_end = min(s_start + BATCH_SIZE, max_t + 1);\n int batch_len = s_end - s_start;\n \n for (int i = threadIdx.x; i < batch_len * HD; i += THREADS_PER_BLOCK) {\n int s_local = i / HD;\n int d = i % HD;\n int s = s_start + s_local;\n \n int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n k_smem[s_local * HD + d] = k[k_idx];\n v_smem[s_local * HD + d] = v[v_idx];\n }\n __syncthreads();\n \n int my_s_end = valid ? min(s_end, t + 1) : s_start;\n \n for (int s = s_start; s < my_s_end; ++s) {\n int s_local = s - s_start;\n float k_val = k_smem[s_local * HD + lane];\n float dot = q_val * k_val;\n \n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n dot += __shfl_xor_sync(0xffffffff, dot, offset);\n }\n float score = dot * scale;\n \n float v_val = v_smem[s_local * HD + lane];\n \n if (score > max_score) {\n float diff = expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n out_acc = out_acc * diff + v_val;\n max_score = score;\n } else {\n float w = expf(score - max_score);\n sum_exp += w;\n out_acc += w * v_val;\n }\n }\n __syncthreads();\n }\n \n if (valid) {\n out_acc /= sum_exp;\n int out_idx = ((b * T + t) * H_q + h) * HD + lane;\n out[out_idx] = out_acc;\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n \n auto out = torch::empty_like(q);\n \n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n int num_blocks = B * H_q * num_t_blocks;\n \n int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD + BATCH_SIZE * HD) * sizeof(float);\n \n gqa_causal_kernel<<<num_blocks, 256, smem_size>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv\n );\n \n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_v3\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← run_correctness (ok)
okrun_correctness run_correctness PASSED: (5 / 5) trials all matched the reference. Numerical precision: max_abs_err=8.34e-07 mean_abs_err=3.22e-08 max_rel_err=1.26e-01
turn 6 · 1 tool call(s) · 18.3s turn · 18.3s LLM · 8,368 chars (click to expand)
turn 618.3s turn · 18.3s LLM · 0.0s tools
reasoning
The v3 kernel is correct. Now let me calculate expected performance. For B=2, T=80, H_q=8, H_kv=2, HD=32: The reference does naive Pythonic computation with triple nested loops. But PyTorch will fuse operations. Let me think about what PyTorch actually does: 1. expand k,v: repeat_interleave(group_size=4, dim=2) - k: [2, 80, 2, 32] → [2, 80, 8, 32] This is materialized, so memory cost increases. 2. For each position t, extract expanded_k[b, :t+1, h, :] and dot with q_t But wait, this is done with Python loops over b, h, t and then k_hist * q_t, sum(dim=-1). Actually, the PyTorch reference is doing: ```python for b in range(batch_size): for h in range(num_q_heads): for t in range(seq_len): q_t = q[b, t, h] k_hist = expanded_k[b, : t + 1, h] v_hist = expanded_v[b, : t + 1, h] scores = (k_hist * q_t.unsqueeze(0)).sum(dim=-1) * scale attn = torch.softmax(scores, dim=0) out[b, t, h] = (attn.unsqueeze(-1) * v_hist).sum(dim=0) ``` This is executed on CPU with Python overhead and small tensor ops on GPU. Most of the time is from: 1. Data transfer overhead from many small GPU ops 2. Using repeat_interleave (materialization) 3. Python loop overhead 4. Kernel launch overhead for each small op My custom kernel: 1. No materialization of expanded k/v 2. Single fused kernel launch 3. Each warp handles one (b, h, t) independently 4. Shared memory for q in the block 5. Batched loading of k/v via shared memory For the special case, with 16 blocks (2 * 8 * 1 = 16 since T=80 and WARPS_PER_BLOCK=8, so num_t_blocks=1). This means we have 16 blocks. But an H100 has 132 SMs. Underutilized! Let me check: total blocks = B * H_q * num_t_blocks = 2 * 8 * ceil(80/8) = 2 * 8 * 10 = 160 blocks. 160 blocks with 256 threads each. At 132 SMs, average occupancy is ~1.2 blocks per SM. That's reasonable for such a small problem. The WARPS_PER_BLOCK=8 approach is much better pedagogically and in practice. Let me verify with a timing test. Actually, the evaluation will run timing after my submit_kernel. Let me now submit the kernel. But before that, let me make sure there are no issues with static check. Actually, let me try a small optimization: use __expf instead of expf in the CUDA kernel (both should compile to the same because of --use_fast_math, but __expf explicitly uses the fast math intrinsic).
call → static_check
callstatic_check { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n__global__ void gqa_causal_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv\n) {\n const int HD = 32;\n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n const int THREADS_PER_BLOCK = 256;\n \n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n \n int block_idx = blockIdx.x;\n int b = block_idx / (H_q * num_t_blocks);\n int rem = block_idx % (H_q * num_t_blocks);\n int h = rem / num_t_blocks;\n int t_block = rem % num_t_blocks;\n \n int t_start = t_block * WARPS_PER_BLOCK;\n int t_end = min(t_start + WARPS_PER_BLOCK, T);\n int max_t = t_end - 1;\n \n if (t_start >= T) return;\n \n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int t = t_start + warp_id;\n bool valid = (t < T);\n \n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n \n extern __shared__ float smem[];\n float* q_smem = smem;\n float* k_smem = q_smem + WARPS_PER_BLOCK * HD;\n float* v_smem = k_smem + BATCH_SIZE * HD;\n \n int q_t_load = t_start + warp_id;\n if (q_t_load < T && lane < HD) {\n int q_idx = ((b * T + q_t_load) * H_q + h) * HD + lane;\n q_smem[warp_id * HD + lane] = q[q_idx];\n }\n __syncthreads();\n \n float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f;\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc = 0.0f;\n \n int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE;\n \n for (int batch = 0; batch < num_batches; ++batch) {\n int s_start = batch * BATCH_SIZE;\n int s_end = min(s_start + BATCH_SIZE, max_t + 1);\n int batch_len = s_end - s_start;\n \n for (int i = threadIdx.x; i < batch_len * HD; i += THREADS_PER_BLOCK) {\n int s_local = i / HD;\n int d = i % HD;\n int s = s_start + s_local;\n \n int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n k_smem[s_local * HD + d] = k[k_idx];\n v_smem[s_local * HD + d] = v[v_idx];\n }\n __syncthreads();\n \n int my_s_end = valid ? min(s_end, t + 1) : s_start;\n \n for (int s = s_start; s < my_s_end; ++s) {\n int s_local = s - s_start;\n float k_val = k_smem[s_local * HD + lane];\n float dot = q_val * k_val;\n \n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n dot += __shfl_xor_sync(0xffffffff, dot, offset);\n }\n float score = dot * scale;\n \n float v_val = v_smem[s_local * HD + lane];\n \n if (score > max_score) {\n float diff = expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n out_acc = out_acc * diff + v_val;\n max_score = score;\n } else {\n float w = expf(score - max_score);\n sum_exp += w;\n out_acc += w * v_val;\n }\n }\n __syncthreads();\n }\n \n if (valid) {\n out_acc /= sum_exp;\n int out_idx = ((b * T + t) * H_q + h) * HD + lane;\n out[out_idx] = out_acc;\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n \n auto out = torch::empty_like(q);\n \n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n int num_blocks = B * H_q * num_t_blocks;\n \n int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD + BATCH_SIZE * HD) * sizeof(float);\n \n gqa_causal_kernel<<<num_blocks, 256, smem_size>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv\n );\n \n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_v3\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← static_check (ok)
okstatic_check static_check PASSED: no violations or warnings detected.
turn 7 · FINAL · 1 tool call(s) · 27.1s turn · 27.1s LLM · 11,922 chars (click to expand)
turn 727.1s turn · 27.1s LLM · 0.0s tools · FINAL
reasoning
The kernel compiles, passes correctness, and passes static checks. Let me review the approach and see if there's anything I should optimize further before submitting. Current kernel design: - Each block handles (B=1, H_q=1, T block=8) → 8 warps per block - Each warp handles one query timestep t - 32 threads (lanes) × 1 element each = 32 dims (head_dim=32) - Shared memory: q for 8 t's + k for 16 steps + v for 16 steps - Iterates over s in batches of 16, loading to shared mem This should be reasonably fast. The sequential nature of causal attention limits parallelism along the s dimension, but grouping t values per block and using shared memory for KV should be efficient. Let me think about potential improvements: 1. Currently only 256 threads per block with 32 SMs - only 32 blocks for (B=2, H_q=8, t_blocks=10) = 160 blocks total. This gives enough occupancy. 2. The __shfl_xor reduction should be fast. 3. The shared memory reuse for KV is working. Actually, let me reconsider. For T=80, each t needs to attend to 0..t-1 timesteps: - For small t: very little work, lots of shared memory load for just 1-2 values - For large t: lots of work The bottleneck might be that for blocks with small t values, we're doing too much wasted work loading KV batches. But there's not much we can do about the causal structure. One concern: for small t values (like t=0, 1, ...), we load 16 KV steps but only use a few. For t=0, we use 0 iterations. For t=1, we use 1 step. But our batching still loads 16 steps at a time. Actually this should be fine for T=80. Let me just submit it and see. Wait, actually I want to double check: is there a potential issue with sync behavior across warps? Our kernel: 1. Loads q to smem with a subset of threads 2. __syncthreads 3. Each warp (0..7) may be valid or invalid 4. For each batch: cooperative load of KV, syncthreads, then each valid warp processes some values, syncthreads 5. Non-valid warps skip the inner loop but all threads participate in syncthreads This looks correct. Invalid warps just don't do the reduction/write. The syncthreads are placed correctly for cooperative loading. One issue: `int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;` → for T=80, num_t_blocks = (80+7)/8 = 10. Total blocks = 2 * 8 * 10 = 160. Each block has 256 threads. Total launched = 160 * 256 = 40960 threads. On H100 with 132 SMs, this is well-distributed. Let me consider: should I increase to 512 threads per block or more WARPS_PER_BLOCK? With 8 warps per block handling 8 values of t, this is already good. Actually, one thought: for the second implementation approach I was considering (tile-based with each warp handling 8 t × 8 s), that might have much better memory reuse because the same KV values are used across multiple t values. But I wasn't able to get the shared memory right. Let me try one more time with careful indexing. But I've had issues with correctness before. Let me just stick with what works. The current kernel should be quite a bit faster than the reference already. Let me submit. Actually wait, let me re-examine if there's a subtle bug I might have missed. Let me trace through for t=0: - b=0, h=0, t_block=0, warp_id=0, t=0 - valid=true - q_smem[0*32+lane] is loaded - max_score = -inf, sum_exp = 0, out_acc = 0 - max_t = 7, num_batches = 1 - batch 0: s_start=0, s_end=8, batch_len=8 - Loads k and v for s=0..7 - my_s_end = min(8, 1) = 1 - Loop: s=0 only s=0: - k_val = k_smem[0*32 + lane] - dot = q_val * k_val - warp reduce - score = reduced_dot * scale v_val = v_smem[0*32 + lane] score > max_score (-inf) is true - diff = exp(-inf - score) = exp(-inf) = 0 - sum_exp = 0 * 0 + 1 = 1 - out_acc = 0 * 0 + v_val = v_val - max_score = score After loop: out_acc = out_acc / sum_exp = v_val (which is correct) output = v_val Great, this is correct! And it's equivalent to: exp(0)/sum_exp * v where sum_exp=exp(0)=1. Correct! OK let me submit. Actually, one more thing: should I try increasing occupancy? Currently using 256 threads per block which is fine. Let me submit. Wait, should I make the same batch loading work more efficiently by loading float4? Let me think. Actually for HD=32, loading 16*32 = 512 floats with 256 threads means each thread loads 2 floats in 2 iterations. This is fine but not optimal. Using float4 would make it 512/4 = 128 float4 loads, each thread does 0.5 float4 (1.00 float in 1 iteration?)... actually with 256 threads, each would do 2 loads of a pair. For simplicity and correctness, let me stick with float loads. The overhead is minimal. Actually, I realize one more thing: should I use `__ldg` for loads from global memory? Or just rely on L1 cache. For sequential loading patterns like k/v, the L1 cache should work well. But `__ldg` might help. Let me try adding it quickly. On the other hand, the main bottleneck is likely the sequential reduction per t, so memory might not be the critical path. But let me try a version with float4 and __ldg to see if it compiles and is faster. Actually, I think I should just submit the working version. It's already significantly faster than the reference due to parallelization and shared memory. Now, one more check: look at `constexpr int NUM_WARPS = 4;` etc. My latest version uses `WARPS_PER_BLOCK = 8`. Let me make sure this is fixed to 8 (it is). And BATCH_SIZE=16. Shared memory = (8*32 + 16*32 + 16*32)*4 = (256 + 512 + 512)*4 = 5120 bytes. Very small. Good. I'll submit now.
call → submit_kernel
callsubmit_kernel { "kernel_code": "import math\nimport torch\nimport torch.nn as nn\nfrom torch.utils.cpp_extension import load_inline\n\nCUDA_SOURCE = r\"\"\"\n#include <torch/extension.h>\n#include <cuda_runtime.h>\n#include <math.h>\n\n__global__ void gqa_causal_kernel(\n const float* __restrict__ q,\n const float* __restrict__ k,\n const float* __restrict__ v,\n float* __restrict__ out,\n int B, int T, int H_q, int H_kv\n) {\n const int HD = 32;\n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n const int THREADS_PER_BLOCK = 256;\n \n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n \n int block_idx = blockIdx.x;\n int b = block_idx / (H_q * num_t_blocks);\n int rem = block_idx % (H_q * num_t_blocks);\n int h = rem / num_t_blocks;\n int t_block = rem % num_t_blocks;\n \n int t_start = t_block * WARPS_PER_BLOCK;\n int t_end = min(t_start + WARPS_PER_BLOCK, T);\n int max_t = t_end - 1;\n \n if (t_start >= T) return;\n \n int warp_id = threadIdx.x / 32;\n int lane = threadIdx.x % 32;\n int t = t_start + warp_id;\n bool valid = (t < T);\n \n int group_size = H_q / H_kv;\n int kv_h = h / group_size;\n float scale = 1.0f / sqrtf(static_cast<float>(HD));\n \n extern __shared__ float smem[];\n float* q_smem = smem;\n float* k_smem = q_smem + WARPS_PER_BLOCK * HD;\n float* v_smem = k_smem + BATCH_SIZE * HD;\n \n int q_t_load = t_start + warp_id;\n if (q_t_load < T && lane < HD) {\n int q_idx = ((b * T + q_t_load) * H_q + h) * HD + lane;\n q_smem[warp_id * HD + lane] = q[q_idx];\n }\n __syncthreads();\n \n float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f;\n float max_score = -INFINITY;\n float sum_exp = 0.0f;\n float out_acc = 0.0f;\n \n int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE;\n \n for (int batch = 0; batch < num_batches; ++batch) {\n int s_start = batch * BATCH_SIZE;\n int s_end = min(s_start + BATCH_SIZE, max_t + 1);\n int batch_len = s_end - s_start;\n \n for (int i = threadIdx.x; i < batch_len * HD; i += THREADS_PER_BLOCK) {\n int s_local = i / HD;\n int d = i % HD;\n int s = s_start + s_local;\n \n int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d;\n k_smem[s_local * HD + d] = k[k_idx];\n v_smem[s_local * HD + d] = v[v_idx];\n }\n __syncthreads();\n \n int my_s_end = valid ? min(s_end, t + 1) : s_start;\n \n for (int s = s_start; s < my_s_end; ++s) {\n int s_local = s - s_start;\n float k_val = k_smem[s_local * HD + lane];\n float dot = q_val * k_val;\n \n #pragma unroll\n for (int offset = 16; offset > 0; offset /= 2) {\n dot += __shfl_xor_sync(0xffffffff, dot, offset);\n }\n float score = dot * scale;\n \n float v_val = v_smem[s_local * HD + lane];\n \n if (score > max_score) {\n float diff = expf(max_score - score);\n sum_exp = sum_exp * diff + 1.0f;\n out_acc = out_acc * diff + v_val;\n max_score = score;\n } else {\n float w = expf(score - max_score);\n sum_exp += w;\n out_acc += w * v_val;\n }\n }\n __syncthreads();\n }\n \n if (valid) {\n out_acc /= sum_exp;\n int out_idx = ((b * T + t) * H_q + h) * HD + lane;\n out[out_idx] = out_acc;\n }\n}\n\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) {\n int B = q.size(0);\n int T = q.size(1);\n int H_q = q.size(2);\n int HD = q.size(3);\n int H_kv = k.size(2);\n \n auto out = torch::empty_like(q);\n \n const int WARPS_PER_BLOCK = 8;\n const int BATCH_SIZE = 16;\n int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;\n int num_blocks = B * H_q * num_t_blocks;\n \n int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD + BATCH_SIZE * HD) * sizeof(float);\n \n gqa_causal_kernel<<<num_blocks, 256, smem_size>>>(\n q.data_ptr<float>(),\n k.data_ptr<float>(),\n v.data_ptr<float>(),\n out.data_ptr<float>(),\n B, T, H_q, H_kv\n );\n \n return out;\n}\n\"\"\"\n\nCPP_SOURCE = r\"\"\"\n#include <torch/extension.h>\ntorch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);\n\"\"\"\n\ngqa_cuda = load_inline(\n name=\"gqa_cuda_v3\",\n cpp_sources=CPP_SOURCE,\n cuda_sources=CUDA_SOURCE,\n functions=[\"gqa_causal_attention_forward\"],\n extra_cuda_cflags=[\"-O3\", \"--use_fast_math\"],\n verbose=False,\n)\n\nclass ModelNew(nn.Module):\n def __init__(self):\n super().__init__()\n\n def forward(self, q, k, v):\n return gqa_cuda.gqa_causal_attention_forward(q, k, v)\n" }
result ← submit_kernel (ok)
oksubmit_kernel submit_kernel PASSED: (5 / 5) correctness trials all passed. Kernel runtime: 0.02 μs Runtime stats: mean=0.02μs median=?μs std=0.00μs Numerical precision: max_abs_err=8.34e-07 mean_abs_err=3.22e-08 max_rel_err=1.26e-01 Memory: 33.39 MB peak (ref: 33.71 MB, ratio: 0.9904x) Kernel launches: 2 (ref: 48603, fusion ratio: 24301.5) SOL score: 0.1396 Flagged for excessive speedup — this submission may have been rejected by automated review.
final submitted kernel (166 lines, 4,941 chars) — full source, untruncated
import math
import torch
import torch.nn as nn
from torch.utils.cpp_extension import load_inline

CUDA_SOURCE = r"""
#include <torch/extension.h>
#include <cuda_runtime.h>
#include <math.h>

__global__ void gqa_causal_kernel(
    const float* __restrict__ q,
    const float* __restrict__ k,
    const float* __restrict__ v,
    float* __restrict__ out,
    int B, int T, int H_q, int H_kv
) {
    const int HD = 32;
    const int WARPS_PER_BLOCK = 8;
    const int BATCH_SIZE = 16;
    const int THREADS_PER_BLOCK = 256;
    
    int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;
    
    int block_idx = blockIdx.x;
    int b = block_idx / (H_q * num_t_blocks);
    int rem = block_idx % (H_q * num_t_blocks);
    int h = rem / num_t_blocks;
    int t_block = rem % num_t_blocks;
    
    int t_start = t_block * WARPS_PER_BLOCK;
    int t_end = min(t_start + WARPS_PER_BLOCK, T);
    int max_t = t_end - 1;
    
    if (t_start >= T) return;
    
    int warp_id = threadIdx.x / 32;
    int lane = threadIdx.x % 32;
    int t = t_start + warp_id;
    bool valid = (t < T);
    
    int group_size = H_q / H_kv;
    int kv_h = h / group_size;
    float scale = 1.0f / sqrtf(static_cast<float>(HD));
    
    extern __shared__ float smem[];
    float* q_smem = smem;
    float* k_smem = q_smem + WARPS_PER_BLOCK * HD;
    float* v_smem = k_smem + BATCH_SIZE * HD;
    
    int q_t_load = t_start + warp_id;
    if (q_t_load < T && lane < HD) {
        int q_idx = ((b * T + q_t_load) * H_q + h) * HD + lane;
        q_smem[warp_id * HD + lane] = q[q_idx];
    }
    __syncthreads();
    
    float q_val = valid ? q_smem[warp_id * HD + lane] : 0.0f;
    float max_score = -INFINITY;
    float sum_exp = 0.0f;
    float out_acc = 0.0f;
    
    int num_batches = (max_t + 1 + BATCH_SIZE - 1) / BATCH_SIZE;
    
    for (int batch = 0; batch < num_batches; ++batch) {
        int s_start = batch * BATCH_SIZE;
        int s_end = min(s_start + BATCH_SIZE, max_t + 1);
        int batch_len = s_end - s_start;
        
        for (int i = threadIdx.x; i < batch_len * HD; i += THREADS_PER_BLOCK) {
            int s_local = i / HD;
            int d = i % HD;
            int s = s_start + s_local;
            
            int k_idx = ((b * T + s) * H_kv + kv_h) * HD + d;
            int v_idx = ((b * T + s) * H_kv + kv_h) * HD + d;
            k_smem[s_local * HD + d] = k[k_idx];
            v_smem[s_local * HD + d] = v[v_idx];
        }
        __syncthreads();
        
        int my_s_end = valid ? min(s_end, t + 1) : s_start;
        
        for (int s = s_start; s < my_s_end; ++s) {
            int s_local = s - s_start;
            float k_val = k_smem[s_local * HD + lane];
            float dot = q_val * k_val;
            
            #pragma unroll
            for (int offset = 16; offset > 0; offset /= 2) {
                dot += __shfl_xor_sync(0xffffffff, dot, offset);
            }
            float score = dot * scale;
            
            float v_val = v_smem[s_local * HD + lane];
            
            if (score > max_score) {
                float diff = expf(max_score - score);
                sum_exp = sum_exp * diff + 1.0f;
                out_acc = out_acc * diff + v_val;
                max_score = score;
            } else {
                float w = expf(score - max_score);
                sum_exp += w;
                out_acc += w * v_val;
            }
        }
        __syncthreads();
    }
    
    if (valid) {
        out_acc /= sum_exp;
        int out_idx = ((b * T + t) * H_q + h) * HD + lane;
        out[out_idx] = out_acc;
    }
}

torch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v) {
    int B = q.size(0);
    int T = q.size(1);
    int H_q = q.size(2);
    int HD = q.size(3);
    int H_kv = k.size(2);
    
    auto out = torch::empty_like(q);
    
    const int WARPS_PER_BLOCK = 8;
    const int BATCH_SIZE = 16;
    int num_t_blocks = (T + WARPS_PER_BLOCK - 1) / WARPS_PER_BLOCK;
    int num_blocks = B * H_q * num_t_blocks;
    
    int smem_size = (WARPS_PER_BLOCK * HD + BATCH_SIZE * HD + BATCH_SIZE * HD) * sizeof(float);
    
    gqa_causal_kernel<<<num_blocks, 256, smem_size>>>(
        q.data_ptr<float>(),
        k.data_ptr<float>(),
        v.data_ptr<float>(),
        out.data_ptr<float>(),
        B, T, H_q, H_kv
    );
    
    return out;
}
"""

CPP_SOURCE = r"""
#include <torch/extension.h>
torch::Tensor gqa_causal_attention_forward(torch::Tensor q, torch::Tensor k, torch::Tensor v);
"""

gqa_cuda = load_inline(
    name="gqa_cuda_v3",
    cpp_sources=CPP_SOURCE,
    cuda_sources=CUDA_SOURCE,
    functions=["gqa_causal_attention_forward"],
    extra_cuda_cflags=["-O3", "--use_fast_math"],
    verbose=False,
)

class ModelNew(nn.Module):
    def __init__(self):
        super().__init__()

    def forward(self, q, k, v):
        return gqa_cuda.gqa_causal_attention_forward(q, k, v)