add-cuda-kernel
Skill42 starsChanged 8 months agoMITView on GitHub
Rules and steps for creating a compute kernel in RL.cu project.
- Deletes or force-pushes
2 other projects use a version of this.
Adopted by
Most recently taken up by
- flashinfer-ai/flashinfertheir own version, first seen Oct 8, 2026
- drunkcoding/AgentSkillsArxivtheir own version, first seen Mar 1, 2026
Other versions2 projects use a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 2
- flashinfer-ai/flashinfer★ 6.9ksame name
- drunkcoding/AgentSkillsArxiv★ 4same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repositoryOne other file it gives its agents.
- .claude/CLAUDE.mdCLAUDE.md
The file
We split this skill into the rules it gives an agent
1 rule. About 0 are common, 1 is distinctive, and 0 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈1
Distinctive · estimate
This author's own choices. Worth a look.
0
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 16
After finsihed implementation, you should explain how the kernel works.
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
Tutorial: Adding a New Kernel to RL.cu
Goal
Implement high performance cuda kernel with target format:
- include files: include/kernels/[kernel_name].cuh
- source files: src/kernels/[kernel_name].cu
- testing: tests/test_[kernel_name].cu
- Makefile: Compile the testing implementation Same format for non-cuda function (e.g. cpp) After finsihed implementation, you should explain how the kernel works.
Example
include/kernels/attention.cuh — Flash Attention 2 Kernel
#pragma once
#include <cuda_fp16.h>
#include <cuda_runtime.h>
// ---------------------------------------------------------------------------
// Flash Attention 2 — Prefill
// ---------------------------------------------------------------------------
// Q, K, V layout: [B, S, H, D] (H = num_heads, D = head_dim)
// O layout: [B, S, H_q, D]
//
// GQA supported: H_q % H_kv == 0. kv_head = q_head * H_kv / H_q.
// Causal mask applied (token i attends only to positions <= i).
// FP16 I/O, FP32 accumulation. Currently specialised for head_dim = 128.
//
// Tile sizes: Br=16 (Q rows per block), Bc=64 (KV cols per tile).
// Shared memory per block: 2 × Bc × D × sizeof(half) = 32 KB.
// ---------------------------------------------------------------------------
void launch_flash_attention_prefill(
const half* Q,
const half* K,
const half* V,
half* O,
int B, int S, int H_q, int H_kv, int head_dim,
cudaStream_t stream = 0
);
// ---------------------------------------------------------------------------
// Paged Attention — Decode
// ---------------------------------------------------------------------------
// Single new token (S_q=1) attends to full context stored in a paged KV cache.
//
// q layout: [num_seqs, H_q, D]
// k_cache / v_cache: [num_blocks, H_kv, block_size, D]
// block_tables: [num_seqs, max_blocks_per_seq] (logical→physical)
// seq_lens: [num_seqs] — context length including the new token
//
// Specialised for head_dim=128, block_size=16.
// ---------------------------------------------------------------------------
void launch_paged_attention_decode(
const half* q,
const half* k_cache,
const half* v_cache,
half* out,
const int* block_tables,
const int* seq_lens,
int num_seqs, int H_q, int H_kv, int head_dim,
int max_blocks_per_seq, int block_size,
cudaStream_t stream = 0
);src/kernels/attention.cu — Flash Attention 2 Kernel Implementation
#include "kernels/attention.cuh"
#include <cuda_fp16.h>
#include <cuda_runtime.h>
#include <math.h>
// =============================================================================
// FA2 Prefill Kernel
// =============================================================================
//
// Grid: (ceil(S/Br), H_q, B)
// Block: (Br) — one thread per Q row in the current tile
//
// Algorithm (Flash Attention 2 with online softmax):
// Outer loop: iterate over KV tiles (K and V loaded into shared memory)
// Inner loop: each thread computes scores Q[q_row] · K[kv_pos] for all
// positions in the tile, updating (row_max, row_sum, O_acc)
// with the standard online softmax rescaling step.
//
// Shared memory layout:
// K_smem[Bc][HEAD_DIM] — current K tile, FP16, 16 KB
// V_smem[Bc][HEAD_DIM] — current V tile, FP16, 16 KB
// Total: 32 KB per block
//
// Registers per thread (FP32):
// q_reg[HEAD_DIM] — query row (loaded once)
// o_acc[HEAD_DIM] — output accumulator
// row_max, row_sum — online softmax state
// =============================================================================
template<int HEAD_DIM, int Br, int Bc>
__global__ void flash_attention_prefill_kernel(
const half* __restrict__ Q, // [B, S, H_q, D]
const half* __restrict__ K, // [B, S, H_kv, D]
const half* __restrict__ V, // [B, S, H_kv, D]
half* __restrict__ O, // [B, S, H_q, D]
int B, int S, int H_q, int H_kv, float scale
) {
const int r = threadIdx.x; // Row within Q tile (0..Br-1)
const int q_tile = blockIdx.x;
const int h_q = blockIdx.y;
const int b = blockIdx.z;
const int h_kv = h_q * H_kv / H_q; // GQA: integer division
const int q_row = q_tile * Br + r; // Global row in Q
// NOTE: do NOT return early here — all Br threads must participate in the
// cooperative K/V tile load and reach every __syncthreads(). Use a flag
// instead and skip computation/writes for out-of-bounds rows.
const bool valid_q = (q_row < S);
// Shared memory — K and V tiles, FP16
__shared__ half K_smem[Bc][HEAD_DIM];
__shared__ half V_smem[Bc][HEAD_DIM];
// Load Q row into registers (FP32, converted once) — only for valid rows.
float q_reg[HEAD_DIM];
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++) q_reg[d] = 0.0f;
if (valid_q) {
const half* q_ptr = Q + ((long)(b * S + q_row) * H_q + h_q) * HEAD_DIM;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
q_reg[d] = __half2float(q_ptr[d]);
}
// Online softmax state (per Q row, in registers)
float row_max = -INFINITY;
float row_sum = 0.0f;
float o_acc[HEAD_DIM];
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++) o_acc[d] = 0.0f;
const int num_kv_tiles = (S + Bc - 1) / Bc;
for (int kv_tile = 0; kv_tile < num_kv_tiles; kv_tile++) {
const int tile_start = kv_tile * Bc;
// ------------------------------------------------------------------
// Cooperative load: ALL Br threads fill K_smem and V_smem (Bc rows).
// Each thread is responsible for rows: r, r+Br, r+2*Br, r+3*Br.
// All threads must participate — including those with q_row >= S —
// so that every K/V row needed by valid threads is populated before
// the __syncthreads() below.
// ------------------------------------------------------------------
for (int row = r; row < Bc; row += Br) {
const int kv_row = tile_start + row;
if (kv_row < S) {
const half* k_ptr = K + ((long)(b * S + kv_row) * H_kv + h_kv) * HEAD_DIM;
const half* v_ptr = V + ((long)(b * S + kv_row) * H_kv + h_kv) * HEAD_DIM;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++) {
K_smem[row][d] = k_ptr[d];
V_smem[row][d] = v_ptr[d];
}
}
}
__syncthreads();
// ------------------------------------------------------------------
// Compute attention scores and update online softmax (valid rows only)
// ------------------------------------------------------------------
if (valid_q) {
const int tile_end = min(tile_start + Bc, S);
for (int c = 0; c < tile_end - tile_start; c++) {
const int kv_pos = tile_start + c;
// Causal mask: later positions in this row are also masked (break ok)
if (kv_pos > q_row) break;
// Dot product: Q[q_row] · K[kv_pos]
float dot = 0.0f;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
dot += q_reg[d] * __half2float(K_smem[c][d]);
dot *= scale;
// Online softmax update (numerically stable)
const float new_max = fmaxf(row_max, dot);
const float alpha = expf(row_max - new_max); // rescale old accum
const float p = expf(dot - new_max); // weight for new token
row_sum = row_sum * alpha + p;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
o_acc[d] = o_acc[d] * alpha + p * __half2float(V_smem[c][d]);
row_max = new_max;
}
}
__syncthreads();
}
// Normalise and write output (FP32 → FP16) — valid rows only
if (valid_q) {
half* o_ptr = O + ((long)(b * S + q_row) * H_q + h_q) * HEAD_DIM;
const float inv_sum = 1.0f / row_sum;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
o_ptr[d] = __float2half(o_acc[d] * inv_sum);
}
}
// =============================================================================
// Paged Attention Decode Kernel
// =============================================================================
//
// Grid: (num_seqs, H_q)
// Block: (1) — one thread per (sequence, q_head)
//
// The single query token attends to all context_len tokens stored in the
// paged KV cache. Block tables map logical KV blocks to physical locations.
//
// Algorithm: same online softmax as prefill, but without shared memory tiling
// (Q has only one row; no tiling over Q needed). Walks block_table to handle
// non-contiguous physical storage.
// =============================================================================
template<int HEAD_DIM, int BLOCK_SIZE>
__global__ void paged_attention_decode_kernel(
const half* __restrict__ q, // [num_seqs, H_q, D]
const half* __restrict__ k_cache, // [num_blocks, H_kv, BLOCK_SIZE, D]
const half* __restrict__ v_cache, // [num_blocks, H_kv, BLOCK_SIZE, D]
half* __restrict__ out, // [num_seqs, H_q, D]
const int* __restrict__ block_tables, // [num_seqs, max_blocks_per_seq]
const int* __restrict__ seq_lens, // [num_seqs]
float scale,
int max_blocks_per_seq,
int H_q, int H_kv
) {
const int seq_idx = blockIdx.x;
const int h_q = blockIdx.y;
const int h_kv = h_q * H_kv / H_q; // GQA
const int context_len = seq_lens[seq_idx];
const int num_blocks = (context_len + BLOCK_SIZE - 1) / BLOCK_SIZE;
// Load Q into registers (FP32)
float q_reg[HEAD_DIM];
{
const half* q_ptr = q + (seq_idx * H_q + h_q) * HEAD_DIM;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
q_reg[d] = __half2float(q_ptr[d]);
}
float max_score = -INFINITY;
float sum_exp = 0.0f;
float acc[HEAD_DIM];
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++) acc[d] = 0.0f;
const int* seq_block_table = block_tables + seq_idx * max_blocks_per_seq;
for (int blk = 0; blk < num_blocks; blk++) {
// Indirection: logical block index → physical block in the pool
const int physical_block = seq_block_table[blk];
const int tokens_in_block = min(BLOCK_SIZE, context_len - blk * BLOCK_SIZE);
// Base offset for this physical block + KV head
// k_cache layout: [num_blocks, H_kv, BLOCK_SIZE, D]
const long block_base =
((long)physical_block * H_kv + h_kv) * BLOCK_SIZE * HEAD_DIM;
for (int tok = 0; tok < tokens_in_block; tok++) {
const half* k_ptr = k_cache + block_base + (long)tok * HEAD_DIM;
const half* v_ptr = v_cache + block_base + (long)tok * HEAD_DIM;
float score = 0.0f;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
score += q_reg[d] * __half2float(k_ptr[d]);
score *= scale;
// Online softmax update
const float new_max = fmaxf(max_score, score);
const float alpha = expf(max_score - new_max);
const float p = expf(score - new_max);
sum_exp = sum_exp * alpha + p;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
acc[d] = acc[d] * alpha + p * __half2float(v_ptr[d]);
max_score = new_max;
}
}
// Write normalised output
half* out_ptr = out + (seq_idx * H_q + h_q) * HEAD_DIM;
const float inv = 1.0f / sum_exp;
#pragma unroll
for (int d = 0; d < HEAD_DIM; d++)
out_ptr[d] = __float2half(acc[d] * inv);
}
// =============================================================================
// Launch wrappers
// =============================================================================
void launch_flash_attention_prefill(
const half* Q,
const half* K,
const half* V,
half* O,
int B, int S, int H_q, int H_kv, int head_dim,
cudaStream_t stream
) {
constexpr int Br = 16;
constexpr int Bc = 64;
const float scale = 1.0f / sqrtf((float)head_dim);
dim3 grid((S + Br - 1) / Br, H_q, B);
dim3 block(Br);
flash_attention_prefill_kernel<128, Br, Bc><<<grid, block, 0, stream>>>(
Q, K, V, O, B, S, H_q, H_kv, scale
);
}
void launch_paged_attention_decode(
const half* q,
const half* k_cache,
const half* v_cache,
half* out,
const int* block_tables,
const int* seq_lens,
int num_seqs, int H_q, int H_kv, int head_dim,
int max_blocks_per_seq, int block_size,
cudaStream_t stream
) {
const float scale = 1.0f / sqrtf((float)head_dim);
dim3 grid(num_seqs, H_q);
dim3 block(1);
paged_attention_decode_kernel<128, 16><<<grid, block, 0, stream>>>(
q, k_cache, v_cache, out,
block_tables, seq_lens,
scale, max_blocks_per_seq, H_q, H_kv
);
}tests/test_attention.cu — Flash Attention 2 Kernel Testing
// test_attention.cu
// Validates Flash Attention 2 (prefill) and Paged Attention (decode) kernels
// against a CPU FP32 naive reference.
//
// Pass criterion (from DESIGN.md): max absolute error < 5e-3 vs naive attention.
//
// Prefill test cases:
// 1. B=1, S=16, H_q=2, H_kv=1 — small, GQA ratio 2:1
// 2. B=1, S=64, H_q=4, H_kv=2 — single KV tile (Bc=64)
// 3. B=1, S=128, H_q=16, H_kv=8 — Qwen3 head config, two KV tiles
// 4. B=2, S=96, H_q=4, H_kv=2 — batched
//
// Decode test cases:
// 5. num_seqs=1, context_len=32, H_q=2, H_kv=1
// 6. num_seqs=2, context_len=128, H_q=16, H_kv=8
#include <cstdio>
#include <cstdlib>
#include <cmath>
#include <cuda_fp16.h>
#include <cuda_runtime.h>
#include "kernels/attention.cuh"
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
#define CUDA_CHECK(call) \
do { \
cudaError_t err = (call); \
if (err != cudaSuccess) { \
fprintf(stderr, "CUDA error at %s:%d — %s\n", \
__FILE__, __LINE__, cudaGetErrorString(err)); \
exit(EXIT_FAILURE); \
} \
} while (0)
// Deterministic LCG (no srand dependency)
static unsigned int lcg_state = 42u;
static float lcg_randf() {
lcg_state = lcg_state * 1664525u + 1013904223u;
return ((float)(lcg_state >> 1) / (float)0x7fffffffu) * 2.0f - 1.0f;
}
// ---------------------------------------------------------------------------
// CPU reference: naive causal attention
// Q, K, V layout: [B, S, H, D] (H = num_heads)
// O layout: [B, S, H_q, D]
// FP32 throughout.
// ---------------------------------------------------------------------------
static void ref_attention(
float* O,
const float* Q,
const float* K,
const float* V,
int B, int S, int H_q, int H_kv, int D
) {
const float scale = 1.0f / sqrtf((float)D);
float* scores = new float[S];
for (int b = 0; b < B; b++) {
for (int h = 0; h < H_q; h++) {
const int hkv = h * H_kv / H_q;
for (int i = 0; i < S; i++) {
const float* qi = Q + ((b * S + i) * H_q + h) * D;
// Compute scores[0..i]
float max_s = -1e30f;
for (int j = 0; j <= i; j++) {
const float* kj = K + ((b * S + j) * H_kv + hkv) * D;
float dot = 0.0f;
for (int d = 0; d < D; d++) dot += qi[d] * kj[d];
scores[j] = dot * scale;
if (scores[j] > max_s) max_s = scores[j];
}
// Softmax over [0..i]
float sum_e = 0.0f;
for (int j = 0; j <= i; j++) {
scores[j] = expf(scores[j] - max_s);
sum_e += scores[j];
}
for (int j = 0; j <= i; j++) scores[j] /= sum_e;
// O[i] = softmax(S) · V[0..i]
float* oi = O + ((b * S + i) * H_q + h) * D;
for (int d = 0; d < D; d++) {
float acc = 0.0f;
for (int j = 0; j <= i; j++) {
const float* vj = V + ((b * S + j) * H_kv + hkv) * D;
acc += scores[j] * vj[d];
}
oi[d] = acc;
}
}
}
}
delete[] scores;
}
// ---------------------------------------------------------------------------
// CPU reference: non-causal attention (for decode, S_q=1)
// q layout: [num_seqs, H_q, D]
// K, V layout: [num_seqs, S_kv, H_kv, D] — contiguous KV context
// O layout: [num_seqs, H_q, D]
// ---------------------------------------------------------------------------
static void ref_decode_attention(
float* O,
const float* q,
const float* K,
const float* V,
int num_seqs, int S_kv, int H_q, int H_kv, int D
) {
const float scale = 1.0f / sqrtf((float)D);
float* scores = new float[S_kv];
for (int s = 0; s < num_seqs; s++) {
for (int h = 0; h < H_q; h++) {
const int hkv = h * H_kv / H_q;
const float* qi = q + (s * H_q + h) * D;
float max_s = -1e30f;
for (int j = 0; j < S_kv; j++) {
const float* kj = K + ((s * S_kv + j) * H_kv + hkv) * D;
float dot = 0.0f;
for (int d = 0; d < D; d++) dot += qi[d] * kj[d];
scores[j] = dot * scale;
if (scores[j] > max_s) max_s = scores[j];
}
float sum_e = 0.0f;
for (int j = 0; j < S_kv; j++) {
scores[j] = expf(scores[j] - max_s);
sum_e += scores[j];
}
for (int j = 0; j < S_kv; j++) scores[j] /= sum_e;
float* oi = O + (s * H_q + h) * D;
for (int d = 0; d < D; d++) {
float acc = 0.0f;
for (int j = 0; j < S_kv; j++) {
const float* vj = V + ((s * S_kv + j) * H_kv + hkv) * D;
acc += scores[j] * vj[d];
}
oi[d] = acc;
}
}
}
delete[] scores;
}
// ---------------------------------------------------------------------------
// Prefill test runner
// ---------------------------------------------------------------------------
static bool run_prefill_test(
const char* name, int B, int S, int H_q, int H_kv,
int D = 128, float tol = 5e-3f
) {
const long N_q = (long)B * S * H_q * D;
const long N_kv = (long)B * S * H_kv * D;
// Host buffers (FP32 for reference, FP16 for kernel)
float* h_Q_f32 = new float[N_q];
float* h_K_f32 = new float[N_kv];
float* h_V_f32 = new float[N_kv];
float* h_ref = new float[N_q];
half* h_Q = new half[N_q];
half* h_K = new half[N_kv];
half* h_V = new half[N_kv];
half* h_out = new half[N_q];
// Fill with random values, scale to prevent softmax saturation
for (long i = 0; i < N_q; i++) h_Q[i] = __float2half(lcg_randf() * 0.5f);
for (long i = 0; i < N_kv; i++) h_K[i] = __float2half(lcg_randf() * 0.5f);
for (long i = 0; i < N_kv; i++) h_V[i] = __float2half(lcg_randf() * 0.5f);
// FP32 reference uses the FP16-quantised values (round-trip)
for (long i = 0; i < N_q; i++) h_Q_f32[i] = __half2float(h_Q[i]);
for (long i = 0; i < N_kv; i++) h_K_f32[i] = __half2float(h_K[i]);
for (long i = 0; i < N_kv; i++) h_V_f32[i] = __half2float(h_V[i]);
ref_attention(h_ref, h_Q_f32, h_K_f32, h_V_f32, B, S, H_q, H_kv, D);
// Device buffers
half *d_Q, *d_K, *d_V, *d_O;
CUDA_CHECK(cudaMalloc(&d_Q, N_q * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_K, N_kv * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_V, N_kv * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_O, N_q * sizeof(half)));
CUDA_CHECK(cudaMemcpy(d_Q, h_Q, N_q * sizeof(half), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_K, h_K, N_kv * sizeof(half), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_V, h_V, N_kv * sizeof(half), cudaMemcpyHostToDevice));
launch_flash_attention_prefill(d_Q, d_K, d_V, d_O, B, S, H_q, H_kv, D);
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(h_out, d_O, N_q * sizeof(half), cudaMemcpyDeviceToHost));
// Compare
float max_err = 0.0f;
for (long i = 0; i < N_q; i++) {
float diff = fabsf(__half2float(h_out[i]) - h_ref[i]);
if (diff > max_err) max_err = diff;
}
bool passed = (max_err < tol);
printf("[%s] %-50s max_err=%.6f %s\n",
passed ? "PASS" : "FAIL", name, max_err,
passed ? "" : "<-- EXCEEDS 5e-3");
cudaFree(d_Q); cudaFree(d_K); cudaFree(d_V); cudaFree(d_O);
delete[] h_Q_f32; delete[] h_K_f32; delete[] h_V_f32; delete[] h_ref;
delete[] h_Q; delete[] h_K; delete[] h_V; delete[] h_out;
return passed;
}
// ---------------------------------------------------------------------------
// Paged attention decode test runner
// Uses a contiguous block mapping (block i → physical block i) so the paged
// result is equivalent to standard attention on contiguous K/V.
// ---------------------------------------------------------------------------
static bool run_decode_test(
const char* name,
int num_seqs, int context_len, int H_q, int H_kv,
int D = 128, int BLOCK_SIZE = 16, float tol = 5e-3f
) {
// Total KV tokens per sequence (all seqs same length for simplicity)
const long Q_total = (long)num_seqs * H_q * D;
const long O_total = Q_total;
// Host buffers (FP32 reference)
float* h_q_f32 = new float[Q_total];
float* h_K_f32 = new float[(long)num_seqs * context_len * H_kv * D];
float* h_V_f32 = new float[(long)num_seqs * context_len * H_kv * D];
float* h_ref = new float[O_total];
half* h_q = new half[Q_total];
half* h_out = new half[O_total];
for (long i = 0; i < Q_total; i++) h_q[i] = __float2half(lcg_randf() * 0.5f);
for (long i = 0; i < (long)num_seqs * context_len * H_kv * D; i++) {
h_K_f32[i] = lcg_randf() * 0.5f;
h_V_f32[i] = lcg_randf() * 0.5f;
}
for (long i = 0; i < Q_total; i++) h_q_f32[i] = __half2float(h_q[i]);
// Quantise K/V round-trip
for (long i = 0; i < (long)num_seqs * context_len * H_kv * D; i++) {
h_K_f32[i] = __half2float(__float2half(h_K_f32[i]));
h_V_f32[i] = __half2float(__float2half(h_V_f32[i]));
}
ref_decode_attention(h_ref, h_q_f32, h_K_f32, h_V_f32,
num_seqs, context_len, H_q, H_kv, D);
// Build the paged KV cache with an identity block table
// Each sequence uses context_len/BLOCK_SIZE (rounded up) blocks.
const int blocks_per_seq = (context_len + BLOCK_SIZE - 1) / BLOCK_SIZE;
const int total_phys_blocks = num_seqs * blocks_per_seq;
// k_cache layout: [num_blocks, H_kv, BLOCK_SIZE, D]
const long cache_elems = (long)total_phys_blocks * H_kv * BLOCK_SIZE * D;
half* h_k_cache = new half[cache_elems]();
half* h_v_cache = new half[cache_elems]();
// Fill paged cache from contiguous K/V
// seq s, token t, kv head h → logical_block = t/BLOCK_SIZE, offset = t%BLOCK_SIZE
// physical_block = s * blocks_per_seq + logical_block
for (int s = 0; s < num_seqs; s++) {
for (int t = 0; t < context_len; t++) {
for (int h = 0; h < H_kv; h++) {
int logical_block = t / BLOCK_SIZE;
int tok_offset = t % BLOCK_SIZE;
int phys_block = s * blocks_per_seq + logical_block;
const float* ksrc = h_K_f32 + ((s * context_len + t) * H_kv + h) * D;
const float* vsrc = h_V_f32 + ((s * context_len + t) * H_kv + h) * D;
half* kdst = h_k_cache
+ ((long)phys_block * H_kv + h) * BLOCK_SIZE * D
+ tok_offset * D;
half* vdst = h_v_cache
+ ((long)phys_block * H_kv + h) * BLOCK_SIZE * D
+ tok_offset * D;
for (int d = 0; d < D; d++) {
kdst[d] = __float2half(ksrc[d]);
vdst[d] = __float2half(vsrc[d]);
}
}
}
}
// Block tables: identity mapping (seq s, logical block b → physical block s*bps+b)
int* h_block_tables = new int[num_seqs * blocks_per_seq];
int* h_seq_lens = new int[num_seqs];
for (int s = 0; s < num_seqs; s++) {
h_seq_lens[s] = context_len;
for (int b = 0; b < blocks_per_seq; b++)
h_block_tables[s * blocks_per_seq + b] = s * blocks_per_seq + b;
}
// Device allocations
half *d_q, *d_k_cache, *d_v_cache, *d_out;
int *d_block_tables, *d_seq_lens;
CUDA_CHECK(cudaMalloc(&d_q, Q_total * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_k_cache, cache_elems * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_v_cache, cache_elems * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_out, O_total * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_block_tables, num_seqs * blocks_per_seq * sizeof(int)));
CUDA_CHECK(cudaMalloc(&d_seq_lens, num_seqs * sizeof(int)));
CUDA_CHECK(cudaMemcpy(d_q, h_q, Q_total * sizeof(half), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_k_cache, h_k_cache, cache_elems * sizeof(half), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_v_cache, h_v_cache, cache_elems * sizeof(half), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_block_tables, h_block_tables, num_seqs * blocks_per_seq * sizeof(int), cudaMemcpyHostToDevice));
CUDA_CHECK(cudaMemcpy(d_seq_lens, h_seq_lens, num_seqs * sizeof(int), cudaMemcpyHostToDevice));
launch_paged_attention_decode(
d_q, d_k_cache, d_v_cache, d_out,
d_block_tables, d_seq_lens,
num_seqs, H_q, H_kv, D,
blocks_per_seq, BLOCK_SIZE
);
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaMemcpy(h_out, d_out, O_total * sizeof(half), cudaMemcpyDeviceToHost));
float max_err = 0.0f;
for (long i = 0; i < O_total; i++) {
float diff = fabsf(__half2float(h_out[i]) - h_ref[i]);
if (diff > max_err) max_err = diff;
}
bool passed = (max_err < tol);
printf("[%s] %-50s max_err=%.6f %s\n",
passed ? "PASS" : "FAIL", name, max_err,
passed ? "" : "<-- EXCEEDS 5e-3");
cudaFree(d_q); cudaFree(d_k_cache); cudaFree(d_v_cache);
cudaFree(d_out); cudaFree(d_block_tables); cudaFree(d_seq_lens);
delete[] h_q_f32; delete[] h_K_f32; delete[] h_V_f32; delete[] h_ref;
delete[] h_q; delete[] h_out;
delete[] h_k_cache; delete[] h_v_cache;
delete[] h_block_tables; delete[] h_seq_lens;
return passed;
}
// ---------------------------------------------------------------------------
// Benchmarks
// ---------------------------------------------------------------------------
static void run_prefill_benchmark(
const char* name, int B, int S, int H_q, int H_kv,
int D = 128, int warmup = 10, int iters = 200
) {
const long N_q = (long)B * S * H_q * D;
const long N_kv = (long)B * S * H_kv * D;
half *d_Q, *d_K, *d_V, *d_O;
CUDA_CHECK(cudaMalloc(&d_Q, N_q * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_K, N_kv * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_V, N_kv * sizeof(half)));
CUDA_CHECK(cudaMalloc(&d_O, N_q * sizeof(half)));
cudaEvent_t ev0, ev1;
CUDA_CHECK(cudaEventCreate(&ev0));
CUDA_CHECK(cudaEventCreate(&ev1));
for (int i = 0; i < warmup; i++)
launch_flash_attention_prefill(d_Q, d_K, d_V, d_O, B, S, H_q, H_kv, D);
CUDA_CHECK(cudaDeviceSynchronize());
CUDA_CHECK(cudaEventRecord(ev0));
for (int i = 0; i < iters; i++)
launch_flash_attention_prefill(d_Q, d_K, d_V, d_O, B, S, H_q, H_kv, D);
CUDA_CHECK(cudaEventRecord(ev1));
CUDA_CHECK(cudaEventSynchronize(ev1));
float ms = 0.0f;
CUDA_CHECK(cudaEventElapsedTime(&ms, ev0, ev1));
const float us = ms * 1000.0f / iters;
// FLOPs: 2 * B * H_q * S^2 * D (Q@K^T and softmax(S)@V, approximately)
const double flops = 2.0 * B * H_q * (double)S * S * D * 2;
const double tflops = flops / (us * 1e-6) / 1e12;
printf("[BENCH] %-50s %7.2f us %5.2f TFLOPS\n", name, us, tflops);
CUDA_CHECK(cudaEventDestroy(ev0));
CUDA_CHECK(cudaEventDestroy(ev1));
cudaFree(d_Q); cudaFree(d_K); cudaFree(d_V); cudaFree(d_O);
}
// ---------------------------------------------------------------------------
// main
// ---------------------------------------------------------------------------
int main() {
printf("=== Flash Attention kernel tests ===\n\n");
// ── Prefill tests ────────────────────────────────────────────────────────
printf("--- Prefill (FA2) ---\n");
bool all_pass = true;
all_pass &= run_prefill_test(
"Prefill B=1 S=16 H_q=2 H_kv=1 (small GQA 2:1)",
1, 16, 2, 1);
all_pass &= run_prefill_test(
"Prefill B=1 S=64 H_q=4 H_kv=2 (single KV tile)",
1, 64, 4, 2);
all_pass &= run_prefill_test(
"Prefill B=1 S=128 H_q=16 H_kv=8 (Qwen3 heads, 2 KV tiles)",
1, 128, 16, 8);
all_pass &= run_prefill_test(
"Prefill B=2 S=96 H_q=4 H_kv=2 (batched)",
2, 96, 4, 2);
all_pass &= run_prefill_test(
"Prefill B=1 S=256 H_q=16 H_kv=8 (4 KV tiles)",
1, 256, 16, 8);
// ── Decode tests ─────────────────────────────────────────────────────────
printf("\n--- Decode (Paged Attention) ---\n");
all_pass &= run_decode_test(
"Decode num_seqs=1 ctx=32 H_q=2 H_kv=1",
1, 32, 2, 1);
all_pass &= run_decode_test(
"Decode num_seqs=1 ctx=128 H_q=16 H_kv=8 (Qwen3)",
1, 128, 16, 8);
all_pass &= run_decode_test(
"Decode num_seqs=2 ctx=64 H_q=4 H_kv=2 (batched seqs)",
2, 64, 4, 2);
all_pass &= run_decode_test(
"Decode num_seqs=4 ctx=128 H_q=16 H_kv=8 (GRPO-style batch)",
4, 128, 16, 8);
// ── Summary ──────────────────────────────────────────────────────────────
printf("\n%s\n", all_pass ? "All tests PASSED." : "Some tests FAILED.");
// ── Benchmarks ───────────────────────────────────────────────────────────
printf("\n=== Prefill benchmarks (warmup=10, iters=200) ===\n");
run_prefill_benchmark("Prefill B=1 S=128 H_q=16 H_kv=8", 1, 128, 16, 8);
run_prefill_benchmark("Prefill B=1 S=512 H_q=16 H_kv=8", 1, 512, 16, 8);
run_prefill_benchmark("Prefill B=1 S=2048 H_q=16 H_kv=8", 1, 2048, 16, 8);
return all_pass ? 0 : 1;
}# ──────────────────────────────────────────────────────────────────────────────
# Makefile — GRPO-CUDA kernel tests
#
# Quick-start:
# make tests # build + run all three kernel tests
# make test_rmsnorm # build + run rmsnorm only
# make generate_refs # generate PyTorch reference data (needs torch)
# make clean # remove build/
#
# To target a specific GPU architecture instead of auto-detect:
# make tests ARCH=sm_120 # Blackwell (RTX PRO 6000 / RTX 50xx) ← this machine
# make tests ARCH=sm_89 # Ada (RTX 40xx)
# make tests ARCH=sm_86 # Ampere (RTX 30xx)
# make tests ARCH=sm_80 # A100
# ──────────────────────────────────────────────────────────────────────────────
CUDA_HOME := /usr/local/cuda-12.8
NVCC := $(CUDA_HOME)/bin/nvcc
ARCH := sm_120
INCLUDES := -I include
NVCCFLAGS := -O2 -std=c++17 $(INCLUDES) --gpu-architecture=$(ARCH)
BUILDDIR := build
PYTHON := python3
# ── Kernel test binaries ───────────────────────────────────────────────────────
$(BUILDDIR)/test_rmsnorm: src/kernels/rmsnorm.cu tests/test_rmsnorm.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_softmax: src/kernels/softmax.cu tests/test_softmax.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_swiglu: src/kernels/swiglu.cu tests/test_swiglu.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_attention: src/kernels/attention.cu tests/test_attention.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_kv_cache: src/model/kv_cache.cu tests/test_kv_cache.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_rope: src/kernels/rope.cu tests/test_rope.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_embedding: src/kernels/embedding.cu tests/test_embedding.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR)/test_linear: src/kernels/linear.cu tests/test_linear.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@ -lcublas
QWEN3_SRCS := src/model/qwen3.cu src/model/kv_cache.cu \
src/kernels/rmsnorm.cu src/kernels/rope.cu src/kernels/attention.cu \
src/kernels/swiglu.cu src/kernels/embedding.cu src/kernels/linear.cu \
src/kernels/config.cpp src/kernels/weights.cpp
$(BUILDDIR)/test_qwen3: $(QWEN3_SRCS) tests/test_qwen3.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@ -lcublas
$(BUILDDIR)/bench_decode: $(QWEN3_SRCS) tests/bench_decode.cu | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@ -lcublas
$(BUILDDIR)/test_loading_weights: src/kernels/config.cpp src/kernels/weights.cpp tests/test_loading_weights.cpp | $(BUILDDIR)
$(NVCC) $(NVCCFLAGS) $^ -o $@
$(BUILDDIR):
mkdir -p $(BUILDDIR)
# ── Run targets ────────────────────────────────────────────────────────────────
.PHONY: test_rmsnorm test_softmax test_swiglu test_attention test_kv_cache test_rope test_embedding test_linear test_qwen3 test_loading_weights bench_decode tests generate_refs clean
test_rmsnorm: $(BUILDDIR)/test_rmsnorm
./$(BUILDDIR)/test_rmsnorm
test_softmax: $(BUILDDIR)/test_softmax
./$(BUILDDIR)/test_softmax
test_swiglu: $(BUILDDIR)/test_swiglu
./$(BUILDDIR)/test_swiglu
test_attention: $(BUILDDIR)/test_attention
./$(BUILDDIR)/test_attention
test_kv_cache: $(BUILDDIR)/test_kv_cache
./$(BUILDDIR)/test_kv_cache
test_rope: $(BUILDDIR)/test_rope
./$(BUILDDIR)/test_rope
test_embedding: $(BUILDDIR)/test_embedding
./$(BUILDDIR)/test_embedding
test_linear: $(BUILDDIR)/test_linear
./$(BUILDDIR)/test_linear
test_qwen3: $(BUILDDIR)/test_qwen3
./$(BUILDDIR)/test_qwen3
test_loading_weights: $(BUILDDIR)/test_loading_weights
./$(BUILDDIR)/test_loading_weights
bench_decode: $(BUILDDIR)/bench_decode
./$(BUILDDIR)/bench_decode
tests: test_rmsnorm test_softmax test_swiglu test_attention test_kv_cache test_rope test_embedding test_linear test_qwen3 test_loading_weights
# ── PyTorch reference generator ───────────────────────────────────────────────
generate_refs:
$(PYTHON) tests/generate_references.py --outdir tests/reference_data
# ── Cleanup ────────────────────────────────────────────────────────────────────
clean:
rm -rf $(BUILDDIR)Key points:
- This fully implementation with CUDA/C++ and cublas, do not contain torch api.
- The model will load in fp16, the kernel should optimization for fp16 computation.
- The kernel implementation should consider for LLM RL training.
References
docs/DESIGN.mdinclude/kernels/attention.cuhsrc/kernels/attention.cuinclude/kernels/rmsnorm.cuhsrc/kernels/rmsnorm.cutests/test_attention.cutests/test_rmsnorm.cu
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

