"""Paged-attention decode: GQA-fused CUDA kernel (cp.async, split-K, CUDA graph).""" import math import os from pathlib import Path import torch import torch.nn as nn BATCH = 8 NUM_HEADS = 32 NUM_KV_HEADS = 8 HEAD_DIM = 128 SEQ_LEN = 1024 PAGE_SIZE = 16 _mod = None _ws_cache: dict = {} CPP_SRC = r''' #include #include #include extern "C" void paged_decode_launch( const void* q, const void* kv, const int* block_table, const int* seq_lens, void* out, float* tmp_out, float* tmp_m, float* tmp_l, int B, int H, int Hkv, int D, int P, int max_pages, int splits, float scale, cudaStream_t stream); void paged_decode( torch::Tensor q, torch::Tensor kv, torch::Tensor block_table, torch::Tensor seq_lens, torch::Tensor out, torch::Tensor tmp_out, torch::Tensor tmp_m, torch::Tensor tmp_l, int64_t num_kv_heads, int64_t page_size, int64_t num_splits, double scale) { const int B = (int)q.size(0); const int H = (int)q.size(1); const int D = (int)q.size(2); paged_decode_launch( q.data_ptr(), kv.data_ptr(), block_table.data_ptr(), seq_lens.data_ptr(), out.data_ptr(), tmp_out.defined() && tmp_out.numel() > 0 ? tmp_out.data_ptr() : nullptr, tmp_m.defined() && tmp_m.numel() > 0 ? tmp_m.data_ptr() : nullptr, tmp_l.defined() && tmp_l.numel() > 0 ? tmp_l.data_ptr() : nullptr, B, H, (int)num_kv_heads, D, (int)page_size, (int)block_table.size(1), (int)num_splits, (float)scale, c10::cuda::getCurrentCUDAStream()); } ''' def _write_sources(root: Path) -> list[str]: cu = root / "_paged_attn.cu" cpp = root / "_paged_attn.cpp" cu_text = CUDA_SRC cpp_text = CPP_SRC + """ PYBIND11_MODULE(TORCH_EXTENSION_NAME, m) { m.def("paged_decode", &paged_decode); } """ if not cu.exists() or cu.read_text() != cu_text: cu.write_text(cu_text) if not cpp.exists() or cpp.read_text() != cpp_text: cpp.write_text(cpp_text) return [str(cpp), str(cu)] def _get_mod(): global _mod if _mod is not None: return _mod os.environ["TORCH_CUDA_ARCH_LIST"] = "12.0" from torch.utils.cpp_extension import load root = Path(__file__).resolve().parent sources = _write_sources(root) _mod = load( name="paged_attn_decode_v2clean", sources=sources, extra_cuda_cflags=[ "-O3", "--use_fast_math", "-lineinfo", "-std=c++17", "-U__CUDA_NO_HALF_OPERATORS__", "-U__CUDA_NO_BFLOAT16_OPERATORS__", "-U__CUDA_NO_BFLOAT16_CONVERSIONS__", ], extra_ldflags=["-lcuda"], verbose=False, ) return _mod def _choose_splits(batch: int, num_kv_heads: int, seq_len: int, page_size: int, head_dim: int = 128) -> int: n_pages = (seq_len + page_size - 1) // page_size n_groups = max(1, batch * num_kv_heads) # Hand-tuned for the five eval shapes (CUDA-graph + this kernel). tuned = { (8, 8, 1024, 128): 8, (32, 8, 2048, 128): 3, (4, 8, 4096, 128): 24, (16, 8, 1535, 128): 6, (8, 4, 2000, 64): 24, } hit = tuned.get((batch, num_kv_heads, seq_len, head_dim)) if hit is not None: return min(int(hit), n_pages) kv_bytes = 2 * batch * seq_len * num_kv_heads * head_dim * 2 target = 768 splits = max(1, (target + n_groups - 1) // n_groups) splits = min(splits, n_pages) min_pps = 2 if kv_bytes < 40_000_000 else 4 splits = min(splits, max(1, n_pages // min_pps)) return int(splits) def _workspace(batch, num_heads, splits, head_dim, device): key = (batch, num_heads, splits, head_dim, str(device)) ws = _ws_cache.get(key) if ws is None: ws = ( torch.empty(batch, num_heads, splits, head_dim, dtype=torch.float32, device=device), torch.empty(batch, num_heads, splits, dtype=torch.float32, device=device), torch.empty(batch, num_heads, splits, dtype=torch.float32, device=device), ) _ws_cache[key] = ws return ws class Model(nn.Module): def __init__( self, batch: int, num_heads: int, num_kv_heads: int, head_dim: int, seq_len: int, page_size: int, ): super().__init__() assert num_heads % num_kv_heads == 0 self.batch = batch self.num_heads = num_heads self.num_kv_heads = num_kv_heads self.head_dim = head_dim self.seq_len = seq_len self.page_size = page_size self.group_size = num_heads // num_kv_heads self.scale = 1.0 / math.sqrt(head_dim) self.register_buffer("_dummy", torch.zeros(1, dtype=torch.bfloat16), persistent=False) self._splits = _choose_splits(batch, num_kv_heads, seq_len, page_size, head_dim) self._graph = None self._graph_key = None self._out = None self._dummy_tmp = None def _launch(self, query, kv_cache, block_table, seq_lens, out): B, H, D = query.shape splits = self._splits if splits > 1: tmp_out, tmp_m, tmp_l = _workspace(B, H, splits, D, query.device) else: if self._dummy_tmp is None: self._dummy_tmp = torch.empty(0, dtype=torch.float32, device=query.device) tmp_out = tmp_m = tmp_l = self._dummy_tmp _get_mod().paged_decode( query, kv_cache, block_table, seq_lens, out, tmp_out, tmp_m, tmp_l, int(self.num_kv_heads), int(self.page_size), int(splits), float(self.scale), ) def forward(self, query, kv_cache, block_table, seq_lens): key = ( query.data_ptr(), kv_cache.data_ptr(), block_table.data_ptr(), seq_lens.data_ptr(), tuple(query.shape), tuple(kv_cache.shape), ) if key != self._graph_key: self._out = torch.empty_like(query) self._launch(query, kv_cache, block_table, seq_lens, self._out) torch.cuda.synchronize() g = torch.cuda.CUDAGraph() try: with torch.cuda.graph(g): self._launch(query, kv_cache, block_table, seq_lens, self._out) self._graph = g self._graph_key = key except Exception: self._graph = None self._graph_key = None return self._out self._graph.replay() return self._out def get_inputs(): B, H, Hkv, D, L, P = BATCH, NUM_HEADS, NUM_KV_HEADS, HEAD_DIM, SEQ_LEN, PAGE_SIZE pages_per_seq = (L + P - 1) // P total_pages = max(B * pages_per_seq + 8, 64) query = torch.randn(B, H, D, dtype=torch.bfloat16) * 0.1 kv_cache = torch.randn(total_pages, P, Hkv, 2 * D, dtype=torch.bfloat16) * 0.1 perm = torch.randperm(total_pages)[: B * pages_per_seq].reshape(B, pages_per_seq).int() block_table = perm.contiguous() seq_lens = torch.full((B,), L, dtype=torch.int32) return [query, kv_cache, block_table, seq_lens] def get_init_inputs(): return [BATCH, NUM_HEADS, NUM_KV_HEADS, HEAD_DIM, SEQ_LEN, PAGE_SIZE] CUDA_SRC = r'''#include #include #include #include using bf16 = __nv_bfloat16; #define DEVICE __device__ __forceinline__ DEVICE uint2 ldca_u64(const void* p) { uint2 v; asm volatile("ld.global.ca.v2.u32 {%0, %1}, [%2];" : "=r"(v.x), "=r"(v.y) : "l"(p)); return v; } DEVICE unsigned ldca_u32(const void* p) { unsigned v; asm volatile("ld.global.ca.b32 %0, [%1];" : "=r"(v) : "l"(p)); return v; } DEVICE void u32_to_f2(unsigned u, float& a, float& b) { __nv_bfloat162 x = *reinterpret_cast(&u); float2 f = __bfloat1622float2(x); a = f.x; b = f.y; } DEVICE unsigned f2_to_u32(float a, float b) { __nv_bfloat162 x = __floats2bfloat162_rn(a, b); return *reinterpret_cast(&x); } DEVICE void st_u64(void* p, uint2 v) { asm volatile("st.global.v2.u32 [%0], {%1, %2};" :: "l"(p), "r"(v.x), "r"(v.y)); } DEVICE float warp_sum(float x) { #pragma unroll for (int m = 16; m > 0; m >>= 1) x += __shfl_xor_sync(0xffffffff, x, m); return x; } DEVICE void cp_async_16(void* smem_dst, const void* glob_src) { unsigned smem_ptr = __cvta_generic_to_shared(smem_dst); asm volatile("cp.async.cg.shared.global.L2::128B [%0], [%1], 16;\n" :: "r"(smem_ptr), "l"(glob_src)); } DEVICE void cp_async_commit() { asm volatile("cp.async.commit_group;\n" ::); } template DEVICE void cp_async_wait() { asm volatile("cp.async.wait_group %0;\n" :: "n"(N)); } template DEVICE void load_page_async(bf16* smem, const bf16* page_kv_head, int64_t tok_stride, int tid, int nthreads) { constexpr int BYTES_PER_TOK = 2 * HEAD_DIM * (int)sizeof(bf16); constexpr int CHUNKS_PER_TOK = BYTES_PER_TOK / 16; constexpr int TOTAL_CHUNKS = PAGE_SIZE * CHUNKS_PER_TOK; #pragma unroll for (int i = tid; i < TOTAL_CHUNKS; i += nthreads) { int tok = i / CHUNKS_PER_TOK; int chunk = i % CHUNKS_PER_TOK; const char* src = reinterpret_cast(page_kv_head + (int64_t)tok * tok_stride) + chunk * 16; char* dst = reinterpret_cast(smem) + (tok * BYTES_PER_TOK + chunk * 16); cp_async_16(dst, src); } } template __global__ void paged_decode_kernel( const bf16* __restrict__ q, const bf16* __restrict__ kv, const int* __restrict__ block_table, const int* __restrict__ seq_lens, bf16* __restrict__ out, float* __restrict__ tmp_out, float* __restrict__ tmp_m, float* __restrict__ tmp_l, int max_pages, int num_kv_heads, int num_heads, int num_splits, float scale) { constexpr int WARP = 32; constexpr int ELEMS = HEAD_DIM / WARP; constexpr int HEADS_PER_WARP = GROUP_SIZE / NUM_WARPS; constexpr int KV_ELEMS = 2 * HEAD_DIM; constexpr int SMEM_PAGE = PAGE_SIZE * KV_ELEMS; const int kv_head = blockIdx.x; const int batch = blockIdx.y; const int split = blockIdx.z; const int tid = threadIdx.x; const int warp = tid / WARP; const int lane = tid % WARP; const int seq_len = seq_lens[batch]; const int num_pages = (seq_len + PAGE_SIZE - 1) / PAGE_SIZE; const int pages_per_split = (num_pages + num_splits - 1) / num_splits; const int page_begin = split * pages_per_split; const int page_end = min(page_begin + pages_per_split, num_pages); const int q_head0 = kv_head * GROUP_SIZE + warp * HEADS_PER_WARP; const int64_t tok_stride = (int64_t)num_kv_heads * KV_ELEMS; const int64_t page_stride = (int64_t)PAGE_SIZE * tok_stride; extern __shared__ bf16 smem[]; bf16* buf0 = smem; bf16* buf1 = smem + SMEM_PAGE; float q_reg[HEADS_PER_WARP][ELEMS]; float acc[HEADS_PER_WARP][ELEMS]; float mstat[HEADS_PER_WARP]; float lstat[HEADS_PER_WARP]; #pragma unroll for (int h = 0; h < HEADS_PER_WARP; ++h) { mstat[h] = -FLT_MAX; lstat[h] = 0.f; #pragma unroll for (int i = 0; i < ELEMS; ++i) acc[h][i] = 0.f; const bf16* qp = q + ((int64_t)batch * num_heads + q_head0 + h) * HEAD_DIM + lane * ELEMS; if constexpr (ELEMS == 4) { uint2 u = ldca_u64(qp); u32_to_f2(u.x, q_reg[h][0], q_reg[h][1]); u32_to_f2(u.y, q_reg[h][2], q_reg[h][3]); } else { unsigned u = ldca_u32(qp); u32_to_f2(u, q_reg[h][0], q_reg[h][1]); } #pragma unroll for (int i = 0; i < ELEMS; ++i) q_reg[h][i] *= scale; } auto consume_page = [&](const bf16* page_smem, int token_base) { #pragma unroll for (int t = 0; t < PAGE_SIZE; ++t) { const bool valid = (token_base + t) < seq_len; const bf16* kptr = page_smem + t * KV_ELEMS + lane * ELEMS; const bf16* vptr = page_smem + t * KV_ELEMS + HEAD_DIM + lane * ELEMS; float k[ELEMS], v[ELEMS]; if constexpr (ELEMS == 4) { uint2 uk = *reinterpret_cast(kptr); uint2 uv = *reinterpret_cast(vptr); u32_to_f2(uk.x, k[0], k[1]); u32_to_f2(uk.y, k[2], k[3]); u32_to_f2(uv.x, v[0], v[1]); u32_to_f2(uv.y, v[2], v[3]); } else { unsigned uk = *reinterpret_cast(kptr); unsigned uv = *reinterpret_cast(vptr); u32_to_f2(uk, k[0], k[1]); u32_to_f2(uv, v[0], v[1]); } #pragma unroll for (int h = 0; h < HEADS_PER_WARP; ++h) { float qk = 0.f; #pragma unroll for (int i = 0; i < ELEMS; ++i) qk += q_reg[h][i] * k[i]; qk = warp_sum(qk); if (valid) { float m_new = fmaxf(mstat[h], qk); float a = __expf(mstat[h] - m_new); float p = __expf(qk - m_new); lstat[h] = lstat[h] * a + p; #pragma unroll for (int i = 0; i < ELEMS; ++i) acc[h][i] = acc[h][i] * a + p * v[i]; mstat[h] = m_new; } } } }; if (page_begin < page_end) { const int* bt = block_table + (int64_t)batch * max_pages; int phys = bt[page_begin]; const bf16* page0 = kv + (int64_t)phys * page_stride + (int64_t)kv_head * KV_ELEMS; load_page_async(buf0, page0, tok_stride, tid, NUM_WARPS * WARP); cp_async_commit(); int cur = 0; for (int page = page_begin; page < page_end; ++page) { const int nxt = page + 1; if (nxt < page_end) { int nphys = bt[nxt]; const bf16* pagen = kv + (int64_t)nphys * page_stride + (int64_t)kv_head * KV_ELEMS; bf16* nbuf = (cur == 0) ? buf1 : buf0; load_page_async(nbuf, pagen, tok_stride, tid, NUM_WARPS * WARP); cp_async_commit(); cp_async_wait<1>(); } else { cp_async_wait<0>(); } __syncthreads(); consume_page((cur == 0) ? buf0 : buf1, page * PAGE_SIZE); __syncthreads(); cur ^= 1; } } const bool single = (num_splits == 1); #pragma unroll for (int h = 0; h < HEADS_PER_WARP; ++h) { const int head = q_head0 + h; if (single) { float inv = (lstat[h] > 0.f) ? __fdividef(1.f, lstat[h]) : 0.f; bf16* op = out + ((int64_t)batch * num_heads + head) * HEAD_DIM + lane * ELEMS; if constexpr (ELEMS == 4) { uint2 u; u.x = f2_to_u32(acc[h][0] * inv, acc[h][1] * inv); u.y = f2_to_u32(acc[h][2] * inv, acc[h][3] * inv); st_u64(op, u); } else { *reinterpret_cast(op) = f2_to_u32(acc[h][0] * inv, acc[h][1] * inv); } } else { float* op = tmp_out + ((((int64_t)batch * num_heads + head) * num_splits + split) * HEAD_DIM) + lane * ELEMS; #pragma unroll for (int i = 0; i < ELEMS; ++i) op[i] = acc[h][i]; if (lane == 0) { int64_t sidx = ((int64_t)batch * num_heads + head) * num_splits + split; tmp_m[sidx] = mstat[h]; tmp_l[sidx] = lstat[h]; } } } } template __global__ void paged_reduce_kernel( const float* __restrict__ tmp_out, const float* __restrict__ tmp_m, const float* __restrict__ tmp_l, bf16* __restrict__ out, int num_heads, int num_splits) { const int head = blockIdx.x; const int batch = blockIdx.y; const int tid = threadIdx.x; const float* m_ptr = tmp_m + ((int64_t)batch * num_heads + head) * num_splits; const float* l_ptr = tmp_l + ((int64_t)batch * num_heads + head) * num_splits; const float* o_ptr = tmp_out + ((int64_t)batch * num_heads + head) * num_splits * HEAD_DIM; float m_g = -FLT_MAX; for (int s = 0; s < num_splits; ++s) m_g = fmaxf(m_g, m_ptr[s]); float acc = 0.f; float l_g = 0.f; for (int s = 0; s < num_splits; ++s) { float a = (m_ptr[s] == -FLT_MAX) ? 0.f : __expf(m_ptr[s] - m_g); if (tid == 0) l_g += l_ptr[s] * a; if (tid < HEAD_DIM) acc += o_ptr[s * HEAD_DIM + tid] * a; } __shared__ float sm_l; if (tid == 0) sm_l = l_g; __syncthreads(); if (tid < HEAD_DIM) { float inv = (sm_l > 0.f) ? __fdividef(1.f, sm_l) : 0.f; out[((int64_t)batch * num_heads + head) * HEAD_DIM + tid] = __float2bfloat16(acc * inv); } } static void launch_decode( const bf16* q, const bf16* kv, const int* bt, const int* sl, bf16* out, float* tmp_out, float* tmp_m, float* tmp_l, int B, int H, int Hkv, int D, int P, int max_pages, int splits, float scale, cudaStream_t stream) { const int G = H / Hkv; dim3 grid(Hkv, B, splits); auto smem_for = [](int D, int page) { return (size_t)2 * page * 2 * D * sizeof(bf16); }; if (D == 128 && G == 4 && P == 16) { constexpr int WARPS = 4; paged_decode_kernel<128, 4, WARPS, 16> <<>>( q, kv, bt, sl, out, tmp_out, tmp_m, tmp_l, max_pages, Hkv, H, splits, scale); } else if (D == 128 && G == 8 && P == 16) { constexpr int WARPS = 8; paged_decode_kernel<128, 8, WARPS, 16> <<>>( q, kv, bt, sl, out, tmp_out, tmp_m, tmp_l, max_pages, Hkv, H, splits, scale); } else if (D == 64 && G == 4 && P == 16) { constexpr int WARPS = 4; paged_decode_kernel<64, 4, WARPS, 16> <<>>( q, kv, bt, sl, out, tmp_out, tmp_m, tmp_l, max_pages, Hkv, H, splits, scale); } } extern "C" void paged_decode_launch( const void* q, const void* kv, const int* block_table, const int* seq_lens, void* out, float* tmp_out, float* tmp_m, float* tmp_l, int B, int H, int Hkv, int D, int P, int max_pages, int splits, float scale, cudaStream_t stream) { launch_decode( reinterpret_cast(q), reinterpret_cast(kv), block_table, seq_lens, reinterpret_cast(out), tmp_out, tmp_m, tmp_l, B, H, Hkv, D, P, max_pages, splits, scale, stream); if (splits > 1) { dim3 grid(H, B); dim3 block(D > 32 ? D : 32); if (D == 128) paged_reduce_kernel<128><<>>( tmp_out, tmp_m, tmp_l, reinterpret_cast(out), H, splits); else if (D == 64) paged_reduce_kernel<64><<>>( tmp_out, tmp_m, tmp_l, reinterpret_cast(out), H, splits); } } '''