7 Commits
Author SHA1 Message Date
ViperEkura 8f89c82d55 chore: bump version to 1.3.9 2026-07-12 21:04:20 +08:00
ViperEkura 21871197d7 refactor: use actual q_len from input, remove dead num_splits init 2026-07-12 19:48:17 +08:00
ViperEkura 4c35d36146 fix: auto-assign free port in spawn_parallel_fn to avoid EADDRINUSE 2026-07-12 19:21:56 +08:00
ViperEkura 9aca62c26c perf: remove per-element sentinel checks in softmax + fix stale comments
- Replace 4*NC8 per-element -FLT_MAX comparisons with 2 row-level pn guards; masked entries naturally underflow via expf(-FLT_MAX - nm) ≈ 0; pn only guards all-masked-row edge where nm == -FLT_MAX (exp(0)=1 not 0); ~1-3% speedup on prefill (verified via standalone CUDA bench); correctness verified: prefill 4/4, decode 3/3, paged decode 13/13
- Fix stale comments: remove false 'pre-scale Q' claim, correct occupancy numbers, remove phantom sQ from smem description
2026-07-12 15:45:08 +08:00
ViperEkura b5cdea98ad refactor: remove ineffective __launch_bounds__ from prefill kernel
- Remove MIN_BLOCKS template param and __launch_bounds__ attribute
- Profiling shows smem (not registers) is the occupancy bottleneck for D>=64, making the hint a no-op
- D=64 sees 2-4% speedup, D=128 unchanged (smem-capped at 1 block/SM)
- Update comment blocks in kernel header and both dispatch sites
- Verified correctness via standalone CUDA test (max_err ~1e-4)
2026-07-12 15:00:53 +08:00
ViperEkura 69fecaf387 perf: double-buffer KV pipeline and Q direct-to-register in decode
- Double-buffered KV (STAGES=2) for D<=128: next tile cp.async overlaps current tile MMA compute, hiding global load latency
- Q loaded directly from global into mma A-operand registers, removing sQ staging and prologue syncwarp
- Predicated cp.async unifies full and partial tile paths, eliminating scalar fallback branch
- STAGES=1 fallback for D=256 (double-buffer would exceed smem budget)
- Applied to both contiguous and paged decode MMA kernels
- ~1.27x average speedup on L20 (sm_89), zero precision loss
2026-07-12 14:14:54 +08:00
ViperEkura fd6d25ad86 refactor: extract bench_kernel and dispatch_by_head_dim into test_utils 2026-07-12 00:05:04 +08:00
15 changed files with 399 additions and 256 deletions
+71
View File
@@ -0,0 +1,71 @@
name: Release
on:
push:
tags:
- "v*"
jobs:
build-pure:
name: Build pure-Python wheel
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Build wheel (no CUDA)
run: |
pip wheel . --no-deps -w dist/
- uses: actions/upload-artifact@v4
with:
name: pure-wheel
path: dist/*.whl
build-cuda-linux:
name: Build CUDA wheel (Linux)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install torch (CUDA 12.8)
run: |
pip install torch --index-url https://download.pytorch.org/whl/cu128
- name: Setup CUDA
uses: Jimver/cuda-toolkit@v0.2.35
with:
cuda: "12.8.0"
- name: Build wheel (with CUDA kernels)
run: |
CSRC_KERNELS=true pip wheel . --no-deps --no-build-isolation -w dist/
- uses: actions/upload-artifact@v4
with:
name: cuda-wheel-linux
path: dist/*.whl
release:
name: Attach wheels to release
needs: [build-pure, build-cuda-linux]
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/download-artifact@v4
with:
pattern: "*-wheel"
merge-multiple: true
- name: Create release & upload assets
uses: softprops/action-gh-release@v2
with:
files: ./*.whl
tag_name: ${{ github.ref_name }}
generate_release_notes: true
+1 -1
View File
@@ -1,4 +1,4 @@
__version__ = "1.3.8" __version__ = "1.3.9"
__author__ = "ViperEkura" __author__ = "ViperEkura"
from astrai.config import ( from astrai.config import (
+11 -2
View File
@@ -1,14 +1,21 @@
import os import os
import socket
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from contextlib import contextmanager from contextlib import contextmanager
from functools import wraps from functools import wraps
from typing import Callable from typing import Callable, Optional
import torch import torch
import torch.distributed as dist import torch.distributed as dist
import torch.multiprocessing as mp import torch.multiprocessing as mp
def find_free_port() -> str:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(("", 0))
return str(s.getsockname()[1])
def get_current_device(): def get_current_device():
return os.environ["LOCAL_DEVICE"] return os.environ["LOCAL_DEVICE"]
@@ -217,11 +224,13 @@ def spawn_parallel_fn(
world_size: int, world_size: int,
backend: str = "nccl", backend: str = "nccl",
master_addr: str = "localhost", master_addr: str = "localhost",
master_port: str = "29500", master_port: Optional[str] = None,
device_type: str = "cuda", device_type: str = "cuda",
start_method: str = "spawn", start_method: str = "spawn",
**kwargs, **kwargs,
): ):
if master_port is None:
master_port = find_free_port()
launcher = _detect_launcher() launcher = _detect_launcher()
if launcher in ("torchelastic", "torchrun", "external"): if launcher in ("torchelastic", "torchrun", "external"):
strategy = TorchrunStrategy( strategy = TorchrunStrategy(
+1 -1
View File
@@ -20,7 +20,7 @@ def _arch_flags() -> list[str]:
_kernels_dir = Path("csrc/kernels") _kernels_dir = Path("csrc/kernels")
REGISTRY: dict[str, dict] = {} REGISTRY: dict[str, dict] = {}
CXX_FLAGS = ["-O3", "-march=native", "-funroll-loops"] CXX_FLAGS = ["-O3", "-funroll-loops"]
NVCC_FLAGS = [ NVCC_FLAGS = [
"-O3", "-O3",
"--expt-relaxed-constexpr", "--expt-relaxed-constexpr",
+6 -4
View File
@@ -30,9 +30,12 @@ static void launch_scalar_decode(AttentionParams<bf16>& p) {
} }
#ifndef ASTRAI_NO_MMA #ifndef ASTRAI_NO_MMA
// MMA head-packing requires G <= 16 (sQ has BR=16 rows). sm_80+ tensor-core // MMA head-packing requires G <= 16 (BR=16 rows). sm_80+ tensor-core
// + cp.async wins even at G=1 (decode is memory-bound, not compute-bound). // + cp.async wins even at G=1 (decode is memory-bound, not compute-bound).
template <int HEAD_DIM, int BC> // STAGES=2 (double-buffer) for D<=128 (smem 16 KB); STAGES=1 for D=256
// (double-buffer would be 32 KB, near the 48 KB static cap — keep single
// to preserve occupancy).
template <int HEAD_DIM, int BC, int STAGES = (HEAD_DIM <= 128) ? 2 : 1>
static void launch_mma_decode(AttentionParams<bf16>& p) { static void launch_mma_decode(AttentionParams<bf16>& p) {
int tiles_total = (p.kv_len + BC - 1) / BC; int tiles_total = (p.kv_len + BC - 1) / BC;
p.num_splits = decode_num_splits(p.batch * p.kv_head, tiles_total); p.num_splits = decode_num_splits(p.batch * p.kv_head, tiles_total);
@@ -43,8 +46,7 @@ static void launch_mma_decode(AttentionParams<bf16>& p) {
p.o_part = o_part.data_ptr<float>(); p.o_part = o_part.data_ptr<float>();
p.ml_part = ml_part.data_ptr<float>(); p.ml_part = ml_part.data_ptr<float>();
attn_decode_split_kv_mma_kernel<HEAD_DIM, BC> attn_decode_split_kv_mma_kernel<HEAD_DIM, BC, STAGES><<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p);
<<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p);
attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p); attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p);
} }
#endif #endif
+80 -52
View File
@@ -27,9 +27,18 @@ using bf16 = __nv_bfloat16;
// Optimizations: // Optimizations:
// - cp.async global→shared for K/V (bypasses registers, cuts instruction count) // - cp.async global→shared for K/V (bypasses registers, cuts instruction count)
// - XOR swizzle (swiz_col): LD=HEAD_DIM, zero waste, no bank conflicts // - XOR swizzle (swiz_col): LD=HEAD_DIM, zero waste, no bank conflicts
// - pre-scaled Q: Q scaled during load, softmax skips per-tile multiply // - Q loaded directly from global into mma A-operand registers (no sQ staging,
// - single-buffer: keeps smem small for high occupancy // no prologue syncwarp) — frees shared memory for double-buffering
template <int HEAD_DIM, int BC> // - Double-buffered KV (STAGES=2): next tile's cp.async overlaps current
// tile's MMA compute — hides global load latency / boosts bandwidth
// utilization for small-batch (low-occupancy) decode
// - Predicated cp.async (cp_async_16_pred) for full AND partial tiles on one
// uniform path — eliminates the scalar fallback branch
//
// Smem footprint (BC=32): STAGES=2 → 2*(sK+sV) = 2*2*32*HEAD_DIM*2 bytes.
// D=128: 16 KB (fits 48 KB static cap). D=256: 32 KB (also fits).
// STAGES=1 fallback (4/8 KB) for smem-constrained configs.
template <int HEAD_DIM, int BC, int STAGES = 2>
__global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) { __global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) {
constexpr int BR = 16; constexpr int BR = 16;
constexpr int KD = HEAD_DIM / 16; constexpr int KD = HEAD_DIM / 16;
@@ -38,6 +47,8 @@ __global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) {
constexpr int DN8 = HEAD_DIM / 8; constexpr int DN8 = HEAD_DIM / 8;
constexpr int LD = HEAD_DIM; constexpr int LD = HEAD_DIM;
constexpr int SWIZ_MASK = (HEAD_DIM >= 64) ? 7 : (HEAD_DIM / 8 - 1); constexpr int SWIZ_MASK = (HEAD_DIM >= 64) ? 7 : (HEAD_DIM / 8 - 1);
constexpr int VEC = 8;
constexpr int TOTAL = BC * HEAD_DIM;
const int lane = threadIdx.x; const int lane = threadIdx.x;
const int gid = lane >> 2; const int gid = lane >> 2;
@@ -49,27 +60,31 @@ __global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) {
const int G = p.q_head / p.kv_head; const int G = p.q_head / p.kv_head;
const int q_head0 = kv_head * G; const int q_head0 = kv_head * G;
__shared__ __align__(16) bf16 sK[BC * HEAD_DIM]; // Double-buffered shared memory for K/V (no sQ needed — Q goes direct
__shared__ __align__(16) bf16 sV[BC * HEAD_DIM]; // from global to registers).
__shared__ __align__(16) bf16 sQ[BR * HEAD_DIM]; __shared__ __align__(16) bf16 sK[STAGES * BC * LD];
__shared__ __align__(16) bf16 sV[STAGES * BC * LD];
for (int i = lane; i < BR * HEAD_DIM; i += 32) {
int r = i / HEAD_DIM, d = i % HEAD_DIM;
bf16 val = __float2bfloat16(0.0f);
if (r < G) {
int qh = q_head0 + r;
val = p.q[(batch * p.q_head + qh) * HEAD_DIM + d];
}
sQ[r * LD + swiz_col(d, r, SWIZ_MASK)] = val;
}
__syncwarp();
// ---- Load Q directly from global into mma A-operand registers ----
// Same layout as prefill: frag[0]/[2] = row gid, frag[1]/[3] = row gid+8
// cols kt*16 + tid4*2 + {0,1} / +{8,9}. pau[0]=cols c,c+1; pau[4]=c+8,c+9.
const int q_base = (batch * p.q_head + q_head0) * HEAD_DIM;
const int qra = gid;
const int qrb = gid + 8;
const bool va = qra < G, vb = qrb < G;
unsigned Qa[KD][4]; unsigned Qa[KD][4];
int qrow_l = (lane & 7) + (lane & 8);
int qcol_l = (lane & 16) ? 8 : 0;
#pragma unroll #pragma unroll
for (int kt = 0; kt < KD; kt++) for (int kt = 0; kt < KD; kt++) {
ldmatrix_x4(Qa[kt], &sQ[qrow_l * LD + swiz_col(kt * 16 + qcol_l, qrow_l, SWIZ_MASK)]); int c = kt * 16 + tid4 * 2;
const unsigned* pau = reinterpret_cast<const unsigned*>(
&p.q[q_base + qra * HEAD_DIM + c]);
const unsigned* pbu = reinterpret_cast<const unsigned*>(
&p.q[q_base + qrb * HEAD_DIM + c]);
Qa[kt][0] = va ? pau[0] : 0u;
Qa[kt][1] = vb ? pbu[0] : 0u;
Qa[kt][2] = va ? pau[4] : 0u;
Qa[kt][3] = vb ? pbu[4] : 0u;
}
float Oacc[DN8][4]; float Oacc[DN8][4];
#pragma unroll #pragma unroll
@@ -85,39 +100,48 @@ __global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) {
const int ti_end = min(tiles_total, ti_begin + tiles_per_split); const int ti_end = min(tiles_total, ti_begin + tiles_per_split);
const int has_mask = p.use_mask && p.mask; const int has_mask = p.use_mask && p.mask;
// ---- Load tile lambda: predicated cp.async, unified full/partial ----
auto load_tile = [&](int ti, int buf) {
int kv0 = ti * BC;
bf16* dK = sK + buf * BC * LD;
bf16* dV = sV + buf * BC * LD;
#pragma unroll
for (int i = lane * VEC; i < TOTAL; i += 32 * VEC) {
int r = i / HEAD_DIM, d = i % HEAD_DIM;
int kc = kv0 + r;
bool valid = kc < p.kv_len;
int off = r * LD + swiz_col(d, r, SWIZ_MASK);
cp_async_16_pred(&dK[off], &p.k[kv_base + kc * HEAD_DIM + d], valid);
cp_async_16_pred(&dV[off], &p.v[kv_base + kc * HEAD_DIM + d], valid);
}
cp_async_commit();
};
// ---- Prologue: issue first tile load ----
if (ti_begin < ti_end) {
load_tile(ti_begin, 0);
}
for (int ti = ti_begin; ti < ti_end; ti++) { for (int ti = ti_begin; ti < ti_end; ti++) {
constexpr int BUF_MASK = (STAGES > 1) ? (STAGES - 1) : 0;
int buf = (ti - ti_begin) & BUF_MASK;
// Wait for current tile, then issue next tile's prefetch (overlaps
// with this tile's compute). Single syncwarp covers both hazards.
// When STAGES==1, no prefetch — load happens at end of prior iter.
cp_async_wait_group<0>();
__syncwarp();
if constexpr (STAGES > 1) {
if (ti + 1 < ti_end)
load_tile(ti + 1, (ti + 1 - ti_begin) & BUF_MASK);
}
const bf16* bK = sK + buf * BC * LD;
const bf16* bV = sV + buf * BC * LD;
int kv0 = ti * BC; int kv0 = ti * BC;
bool full_tile = (kv0 + BC <= p.kv_len);
if (full_tile) {
constexpr int VEC = 8;
int total = BC * HEAD_DIM;
#pragma unroll
for (int i = lane * VEC; i < total; i += 32 * VEC) {
int r = i / HEAD_DIM, d = i % HEAD_DIM;
int kc = kv0 + r;
cp_async_16(&sK[r * LD + swiz_col(d, r, SWIZ_MASK)],
&p.k[kv_base + kc * HEAD_DIM + d]);
cp_async_16(&sV[r * LD + swiz_col(d, r, SWIZ_MASK)],
&p.v[kv_base + kc * HEAD_DIM + d]);
}
cp_async_commit();
cp_async_wait_all();
} else {
for (int i = lane; i < BC * HEAD_DIM; i += 32) {
int r = i / HEAD_DIM, d = i % HEAD_DIM;
int kc = kv0 + r;
bf16 z = __float2bfloat16(0.0f);
sK[r * LD + swiz_col(d, r, SWIZ_MASK)] =
(kc < p.kv_len) ? p.k[kv_base + kc * HEAD_DIM + d] : z;
sV[r * LD + swiz_col(d, r, SWIZ_MASK)] =
(kc < p.kv_len) ? p.v[kv_base + kc * HEAD_DIM + d] : z;
}
}
__syncwarp();
float Sacc[NC8][4]; float Sacc[NC8][4];
mma_compute_scores<KD, NC8>(Qa, sK, LD, SWIZ_MASK, lane, Sacc); mma_compute_scores<KD, NC8>(Qa, bK, LD, SWIZ_MASK, lane, Sacc);
#pragma unroll #pragma unroll
for (int n8 = 0; n8 < NC8; n8++) for (int n8 = 0; n8 < NC8; n8++)
@@ -129,8 +153,13 @@ __global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) {
mask_base, p.mask, has_mask, mask_base, p.mask, has_mask,
Sacc, Oacc, m0, m1, l0, l1, lane); Sacc, Oacc, m0, m1, l0, l1, lane);
mma_pv_accumulate<DN8, KT2>(Sacc, sV, LD, SWIZ_MASK, lane, Oacc); mma_pv_accumulate<DN8, KT2>(Sacc, bV, LD, SWIZ_MASK, lane, Oacc);
__syncwarp(); __syncwarp();
if constexpr (STAGES == 1) {
if (ti + 1 < ti_end)
load_tile(ti + 1, 0);
}
} }
// ---- write UN-normalised partials for this split ---- // ---- write UN-normalised partials for this split ----
@@ -169,4 +198,3 @@ __global__ void attn_decode_split_kv_mma_kernel(AttentionParams<bf16> p) {
} }
} }
} }
+26 -11
View File
@@ -114,7 +114,8 @@ __device__ __forceinline__ void cp_async_wait_group() {
// between the two kernels; only the per-row causal/mask bounds differ. // between the two kernels; only the per-row causal/mask bounds differ.
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// S = Q @ K^T (Qa pre-loaded and pre-scaled by the caller). // S = Q @ K^T (Qa pre-loaded by the caller; scale applied post-mma in the
// caller to avoid bf16 precision loss).
// LD and SWIZ_MASK are constexpr in the calling kernel — passing them as // LD and SWIZ_MASK are constexpr in the calling kernel — passing them as
// runtime ints lets the compiler fold them while keeping the signature clean. // runtime ints lets the compiler fold them while keeping the signature clean.
template <int KD, int NC8> template <int KD, int NC8>
@@ -138,10 +139,11 @@ __device__ inline void mma_compute_scores(
} }
} }
// Online softmax + Oacc rescale for one K/V tile. maxc0/maxc1 are the per-row // Online softmax + Oacc rescale for one K/V tile.
// KV column bounds prefill passes per-query-row causal limits while decode // maxc0/maxc1: per-row KV column bounds (prefill: per-query-row causal limits;
// passes the same value for both rows (q_len==1). Sacc is consumed in place // decode: same value for both rows since q_len==1).
// (replaced by P = exp(S - nm) for the subsequent P@V step). // Reads Sacc (Q@K^T scores), applies causal/mask, computes P = exp(S - nm),
// rescales Oacc by exp(m_old - nm), and updates m/l — all in place.
template <int NC8, int DN8> template <int NC8, int DN8>
__device__ inline void mma_softmax_tile( __device__ inline void mma_softmax_tile(
int kv0, int kv0,
@@ -157,6 +159,8 @@ __device__ inline void mma_softmax_tile(
{ {
int tid4 = lane & 3; int tid4 = lane & 3;
// Mask out-of-bounds / masked columns: set -FLT_MAX so expf → 0 downstream
// without per-element sentinel checks. Compute tile-local row maxima.
float rmax0 = -FLT_MAX, rmax1 = -FLT_MAX; float rmax0 = -FLT_MAX, rmax1 = -FLT_MAX;
#pragma unroll #pragma unroll
for (int n8 = 0; n8 < NC8; n8++) { for (int n8 = 0; n8 < NC8; n8++) {
@@ -175,22 +179,33 @@ __device__ inline void mma_softmax_tile(
rmax0 = fmaxf(rmax0, fmaxf(s0, s1)); rmax0 = fmaxf(rmax0, fmaxf(s0, s1));
rmax1 = fmaxf(rmax1, fmaxf(s2, s3)); rmax1 = fmaxf(rmax1, fmaxf(s2, s3));
} }
// Warp-reduce row maxima across the 4-lane thread group (xor 1, xor 2).
rmax0 = fmaxf(rmax0, __shfl_xor_sync(0xFFFFFFFF, rmax0, 1)); rmax0 = fmaxf(rmax0, __shfl_xor_sync(0xFFFFFFFF, rmax0, 1));
rmax0 = fmaxf(rmax0, __shfl_xor_sync(0xFFFFFFFF, rmax0, 2)); rmax0 = fmaxf(rmax0, __shfl_xor_sync(0xFFFFFFFF, rmax0, 2));
rmax1 = fmaxf(rmax1, __shfl_xor_sync(0xFFFFFFFF, rmax1, 1)); rmax1 = fmaxf(rmax1, __shfl_xor_sync(0xFFFFFFFF, rmax1, 1));
rmax1 = fmaxf(rmax1, __shfl_xor_sync(0xFFFFFFFF, rmax1, 2)); rmax1 = fmaxf(rmax1, __shfl_xor_sync(0xFFFFFFFF, rmax1, 2));
// nm = max(running max m, tile-local max rmax) — updated running maximum.
float nm0 = fmaxf(m0, rmax0), nm1 = fmaxf(m1, rmax1); float nm0 = fmaxf(m0, rmax0), nm1 = fmaxf(m1, rmax1);
float corr0 = (nm0 == -FLT_MAX) ? 1.0f : __expf(m0 - nm0); // corr rescales Oacc and l by exp(m_old - nm). When all-masked (m == nm ==
float corr1 = (nm1 == -FLT_MAX) ? 1.0f : __expf(m1 - nm1); // -FLT_MAX), exp(0) = 1 — correct, no guard needed.
float corr0 = __expf(m0 - nm0);
float corr1 = __expf(m1 - nm1);
// pn guards only the all-masked-row edge: if nm == -FLT_MAX, exp(S - nm)
// gives 1 not 0 for masked entries. Two scalar masks replace 4*NC8
// per-element comparisons.
float pn0 = (nm0 == -FLT_MAX) ? 0.0f : 1.0f;
float pn1 = (nm1 == -FLT_MAX) ? 0.0f : 1.0f;
// P = exp(S - nm) for each element. Masked entries (Sacc = -FLT_MAX) give
// exp(-inf) ≈ 0 naturally; pn zero-fills the all-masked-row edge.
float rsum0 = 0.0f, rsum1 = 0.0f; float rsum0 = 0.0f, rsum1 = 0.0f;
#pragma unroll #pragma unroll
for (int n8 = 0; n8 < NC8; n8++) { for (int n8 = 0; n8 < NC8; n8++) {
float p0 = (Sacc[n8][0] == -FLT_MAX) ? 0.0f : __expf(Sacc[n8][0] - nm0); float p0 = pn0 * __expf(Sacc[n8][0] - nm0);
float p1 = (Sacc[n8][1] == -FLT_MAX) ? 0.0f : __expf(Sacc[n8][1] - nm0); float p1 = pn0 * __expf(Sacc[n8][1] - nm0);
float p2 = (Sacc[n8][2] == -FLT_MAX) ? 0.0f : __expf(Sacc[n8][2] - nm1); float p2 = pn1 * __expf(Sacc[n8][2] - nm1);
float p3 = (Sacc[n8][3] == -FLT_MAX) ? 0.0f : __expf(Sacc[n8][3] - nm1); float p3 = pn1 * __expf(Sacc[n8][3] - nm1);
Sacc[n8][0] = p0; Sacc[n8][1] = p1; Sacc[n8][0] = p0; Sacc[n8][1] = p1;
Sacc[n8][2] = p2; Sacc[n8][3] = p3; Sacc[n8][2] = p2; Sacc[n8][3] = p3;
rsum0 += p0 + p1; rsum0 += p0 + p1;
+6 -9
View File
@@ -25,15 +25,14 @@ static void launch_paged_scalar_decode(PagedAttentionParams<bf16>& p) {
p.ml_part = ml_part.data_ptr<float>(); p.ml_part = ml_part.data_ptr<float>();
size_t smem = PDC_CHUNK * p.head_dim * sizeof(bf16); size_t smem = PDC_CHUNK * p.head_dim * sizeof(bf16);
paged_attn_decode_split_kv_kernel<<< dim3 grid = dim3(p.batch * p.kv_head, 1, p.num_splits);
dim3(p.batch * p.kv_head, 1, p.num_splits), dim3 block = dim3(32, group_size);
dim3(32, group_size), paged_attn_decode_split_kv_kernel<<<grid, block, smem>>>(p);
smem>>>(p);
paged_attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p); paged_attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p);
} }
#ifndef ASTRAI_NO_MMA #ifndef ASTRAI_NO_MMA
template <int HEAD_DIM, int BC> template <int HEAD_DIM, int BC, int STAGES = (HEAD_DIM <= 128) ? 2 : 1>
static void launch_paged_mma_decode(PagedAttentionParams<bf16>& p) { static void launch_paged_mma_decode(PagedAttentionParams<bf16>& p) {
int tiles_total = (p.kv_len + BC - 1) / BC; int tiles_total = (p.kv_len + BC - 1) / BC;
p.num_splits = paged_decode_num_splits(p.batch * p.kv_head, tiles_total); p.num_splits = paged_decode_num_splits(p.batch * p.kv_head, tiles_total);
@@ -44,8 +43,7 @@ static void launch_paged_mma_decode(PagedAttentionParams<bf16>& p) {
p.o_part = o_part.data_ptr<float>(); p.o_part = o_part.data_ptr<float>();
p.ml_part = ml_part.data_ptr<float>(); p.ml_part = ml_part.data_ptr<float>();
paged_attn_decode_split_kv_mma_kernel<HEAD_DIM, BC> paged_attn_decode_split_kv_mma_kernel<HEAD_DIM, BC, STAGES><<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p);
<<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p);
paged_attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p); paged_attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p);
} }
#endif #endif
@@ -104,13 +102,12 @@ torch::Tensor attn_paged_decode(
p.batch = batch; p.batch = batch;
p.q_head = q_head; p.q_head = q_head;
p.kv_head = kv_head; p.kv_head = kv_head;
p.q_len = 1; p.q_len = static_cast<int>(q.size(2));
p.kv_len = static_cast<int>(kv_len); p.kv_len = static_cast<int>(kv_len);
p.head_dim = head_dim; p.head_dim = head_dim;
p.use_mask = (mask.has_value() && mask.value().defined()) ? 1 : 0; p.use_mask = (mask.has_value() && mask.value().defined()) ? 1 : 0;
p.is_causal = is_causal ? 1 : 0; p.is_causal = is_causal ? 1 : 0;
p.causal_offset = static_cast<int>(causal_offset); p.causal_offset = static_cast<int>(causal_offset);
p.num_splits = 1;
p.scale = scale_val; p.scale = scale_val;
p.page_size = static_cast<int>(page_size); p.page_size = static_cast<int>(page_size);
p.max_pages = max_pages; p.max_pages = max_pages;
+74 -66
View File
@@ -11,7 +11,12 @@ using bf16 = __nv_bfloat16;
// directly from the page pool through a page table, eliminating the gather // directly from the page pool through a page table, eliminating the gather
// copy. Each tile (BC=32) fits within a single page (page_size >= 32), so // copy. Each tile (BC=32) fits within a single page (page_size >= 32), so
// the page-table lookup happens once per tile for cp.async. // the page-table lookup happens once per tile for cp.async.
template <int HEAD_DIM, int BC> //
// Optimizations mirror attn_decode_split_kv_mma_kernel:
// - Q loaded directly from global into mma A-operand registers (no sQ)
// - Double-buffered KV (STAGES=2) for D<=128, single-buffer for D=256
// - Predicated cp.async for unified full/partial tile path
template <int HEAD_DIM, int BC, int STAGES = (HEAD_DIM <= 128) ? 2 : 1>
__global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16> p) { __global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16> p) {
constexpr int BR = 16; constexpr int BR = 16;
constexpr int KD = HEAD_DIM / 16; constexpr int KD = HEAD_DIM / 16;
@@ -20,6 +25,8 @@ __global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16>
constexpr int DN8 = HEAD_DIM / 8; constexpr int DN8 = HEAD_DIM / 8;
constexpr int LD = HEAD_DIM; constexpr int LD = HEAD_DIM;
constexpr int SWIZ_MASK = (HEAD_DIM >= 64) ? 7 : (HEAD_DIM / 8 - 1); constexpr int SWIZ_MASK = (HEAD_DIM >= 64) ? 7 : (HEAD_DIM / 8 - 1);
constexpr int VEC = 8;
constexpr int TOTAL = BC * HEAD_DIM;
const int lane = threadIdx.x; const int lane = threadIdx.x;
const int gid = lane >> 2; const int gid = lane >> 2;
@@ -31,31 +38,30 @@ __global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16>
const int G = p.q_head / p.kv_head; const int G = p.q_head / p.kv_head;
const int q_head0 = kv_head_idx * G; const int q_head0 = kv_head_idx * G;
__shared__ __align__(16) bf16 sK[BC * HEAD_DIM]; __shared__ __align__(16) bf16 sK[STAGES * BC * LD];
__shared__ __align__(16) bf16 sV[BC * HEAD_DIM]; __shared__ __align__(16) bf16 sV[STAGES * BC * LD];
__shared__ __align__(16) bf16 sQ[BR * HEAD_DIM];
// ---- load Q into registers via ldmatrix ----
for (int i = lane; i < BR * HEAD_DIM; i += 32) {
int r = i / HEAD_DIM, d = i % HEAD_DIM;
bf16 val = __float2bfloat16(0.0f);
if (r < G) {
int qh = q_head0 + r;
val = p.q[(batch * p.q_head + qh) * HEAD_DIM + d];
}
sQ[r * LD + swiz_col(d, r, SWIZ_MASK)] = val;
}
__syncwarp();
// ---- Load Q directly from global into mma A-operand registers ----
const int q_base = (batch * p.q_head + q_head0) * HEAD_DIM;
const int qra = gid;
const int qrb = gid + 8;
const bool va = qra < G, vb = qrb < G;
unsigned Qa[KD][4]; unsigned Qa[KD][4];
int qrow_l = (lane & 7) + (lane & 8); #pragma unroll
int qcol_l = (lane & 16) ? 8 : 0; for (int kt = 0; kt < KD; kt++) {
#pragma unroll int c = kt * 16 + tid4 * 2;
for (int kt = 0; kt < KD; kt++) const unsigned* pau = reinterpret_cast<const unsigned*>(
ldmatrix_x4(Qa[kt], &sQ[qrow_l * LD + swiz_col(kt * 16 + qcol_l, qrow_l, SWIZ_MASK)]); &p.q[q_base + qra * HEAD_DIM + c]);
const unsigned* pbu = reinterpret_cast<const unsigned*>(
&p.q[q_base + qrb * HEAD_DIM + c]);
Qa[kt][0] = va ? pau[0] : 0u;
Qa[kt][1] = vb ? pbu[0] : 0u;
Qa[kt][2] = va ? pau[4] : 0u;
Qa[kt][3] = vb ? pbu[4] : 0u;
}
float Oacc[DN8][4]; float Oacc[DN8][4];
#pragma unroll #pragma unroll
for (int j = 0; j < DN8; j++) for (int j = 0; j < DN8; j++)
Oacc[j][0] = Oacc[j][1] = Oacc[j][2] = Oacc[j][3] = 0.0f; Oacc[j][0] = Oacc[j][1] = Oacc[j][2] = Oacc[j][3] = 0.0f;
float m0 = -FLT_MAX, m1 = -FLT_MAX, l0 = 0.0f, l1 = 0.0f; float m0 = -FLT_MAX, m1 = -FLT_MAX, l0 = 0.0f, l1 = 0.0f;
@@ -72,57 +78,54 @@ __global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16>
const int64_t pos_stride = (int64_t)p.kv_head * HEAD_DIM; const int64_t pos_stride = (int64_t)p.kv_head * HEAD_DIM;
const int64_t head_off = (int64_t)kv_head_idx * HEAD_DIM; const int64_t head_off = (int64_t)kv_head_idx * HEAD_DIM;
for (int ti = ti_begin; ti < ti_end; ti++) { // ---- Load tile lambda: predicated cp.async, paged addressing ----
auto load_tile = [&](int ti, int buf) {
int kv0 = ti * BC; int kv0 = ti * BC;
bf16* dK = sK + buf * BC * LD;
// phys_page is constant for the whole tile (BC <= page_size). bf16* dV = sV + buf * BC * LD;
int logical_page = kv0 / p.page_size; int logical_page = kv0 / p.page_size;
int phys_page = p.page_table[batch * p.max_pages + logical_page]; int phys_page = p.page_table[batch * p.max_pages + logical_page];
bool page_valid = (phys_page >= 0); bool page_valid = (phys_page >= 0);
#pragma unroll
bool full_tile = page_valid && (kv0 + BC <= p.kv_len); for (int i = lane * VEC; i < TOTAL; i += 32 * VEC) {
if (full_tile) { int r = i / HEAD_DIM, d = i % HEAD_DIM;
constexpr int VEC = 8; int kc = kv0 + r;
int total = BC * HEAD_DIM; bool valid = (kc < p.kv_len) && page_valid;
#pragma unroll int page_off = kc % p.page_size;
for (int i = lane * VEC; i < total; i += 32 * VEC) { int64_t gmem_base = (int64_t)phys_page * page_stride
int r = i / HEAD_DIM, d = i % HEAD_DIM; + (int64_t)page_off * pos_stride
int kc = kv0 + r; + head_off;
int page_off = kc % p.page_size; int off = r * LD + swiz_col(d, r, SWIZ_MASK);
int64_t gmem_base = (int64_t)phys_page * page_stride cp_async_16_pred(&dK[off], &p.k_cache[gmem_base + d], valid);
+ (int64_t)page_off * pos_stride cp_async_16_pred(&dV[off], &p.v_cache[gmem_base + d], valid);
+ head_off;
cp_async_16(&sK[r * LD + swiz_col(d, r, SWIZ_MASK)],
&p.k_cache[gmem_base + d]);
cp_async_16(&sV[r * LD + swiz_col(d, r, SWIZ_MASK)],
&p.v_cache[gmem_base + d]);
}
cp_async_commit();
cp_async_wait_all();
} else {
for (int i = lane; i < BC * HEAD_DIM; i += 32) {
int r = i / HEAD_DIM, d = i % HEAD_DIM;
int kc = kv0 + r;
bf16 z = __float2bfloat16(0.0f);
if (kc < p.kv_len && page_valid) {
int page_off = kc % p.page_size;
int64_t gmem_base = (int64_t)phys_page * page_stride
+ (int64_t)page_off * pos_stride
+ head_off;
sK[r * LD + swiz_col(d, r, SWIZ_MASK)] = p.k_cache[gmem_base + d];
sV[r * LD + swiz_col(d, r, SWIZ_MASK)] = p.v_cache[gmem_base + d];
} else {
sK[r * LD + swiz_col(d, r, SWIZ_MASK)] = z;
sV[r * LD + swiz_col(d, r, SWIZ_MASK)] = z;
}
}
} }
cp_async_commit();
};
// ---- Prologue: issue first tile load ----
if (ti_begin < ti_end) {
load_tile(ti_begin, 0);
}
for (int ti = ti_begin; ti < ti_end; ti++) {
constexpr int BUF_MASK = (STAGES > 1) ? (STAGES - 1) : 0;
int buf = (ti - ti_begin) & BUF_MASK;
cp_async_wait_group<0>();
__syncwarp(); __syncwarp();
if constexpr (STAGES > 1) {
if (ti + 1 < ti_end)
load_tile(ti + 1, (ti + 1 - ti_begin) & BUF_MASK);
}
const bf16* bK = sK + buf * BC * LD;
const bf16* bV = sV + buf * BC * LD;
int kv0 = ti * BC;
float Sacc[NC8][4]; float Sacc[NC8][4];
mma_compute_scores<KD, NC8>(Qa, sK, LD, SWIZ_MASK, lane, Sacc); mma_compute_scores<KD, NC8>(Qa, bK, LD, SWIZ_MASK, lane, Sacc);
#pragma unroll #pragma unroll
for (int n8 = 0; n8 < NC8; n8++) for (int n8 = 0; n8 < NC8; n8++)
Sacc[n8][0] *= p.scale, Sacc[n8][1] *= p.scale, Sacc[n8][0] *= p.scale, Sacc[n8][1] *= p.scale,
Sacc[n8][2] *= p.scale, Sacc[n8][3] *= p.scale; Sacc[n8][2] *= p.scale, Sacc[n8][3] *= p.scale;
@@ -132,8 +135,13 @@ __global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16>
mask_base, p.mask, has_mask, mask_base, p.mask, has_mask,
Sacc, Oacc, m0, m1, l0, l1, lane); Sacc, Oacc, m0, m1, l0, l1, lane);
mma_pv_accumulate<DN8, KT2>(Sacc, sV, LD, SWIZ_MASK, lane, Oacc); mma_pv_accumulate<DN8, KT2>(Sacc, bV, LD, SWIZ_MASK, lane, Oacc);
__syncwarp(); __syncwarp();
if constexpr (STAGES == 1) {
if (ti + 1 < ti_end)
load_tile(ti + 1, 0);
}
} }
// ---- write UN-normalised partials for this split ---- // ---- write UN-normalised partials for this split ----
@@ -141,7 +149,7 @@ __global__ void paged_attn_decode_split_kv_mma_kernel(PagedAttentionParams<bf16>
size_t bh = (size_t)batch * p.q_head + h; size_t bh = (size_t)batch * p.q_head + h;
return bh * p.num_splits + split; return bh * p.num_splits + split;
}; };
#pragma unroll #pragma unroll
for (int dn8 = 0; dn8 < DN8; dn8++) { for (int dn8 = 0; dn8 < DN8; dn8++) {
int d = dn8 * 8 + 2 * tid4; int d = dn8 * 8 + 2 * tid4;
int r0 = gid, r1 = gid + 8; int r0 = gid, r1 = gid + 8;
+6 -8
View File
@@ -13,17 +13,15 @@ static void dispatch_prefill(AttentionParams<bf16>& p) {
// loop overhead over more tensor-core work (this kernel is latency-bound, // loop overhead over more tensor-core work (this kernel is latency-bound,
// not compute/bandwidth-bound), so BC=32 wins ~6-8% over BC=16 for // not compute/bandwidth-bound), so BC=32 wins ~6-8% over BC=16 for
// D<=128. D=256 stays at 16: BC=32 double-buffered would need 64KB smem, // D<=128. D=256 stays at 16: BC=32 double-buffered would need 64KB smem,
// over the 48KB static cap. Both keep 3 blocks/SM (2 for D=256). // over the 48KB static cap.
constexpr int BC = (HEAD_DIM <= 128) ? 32 : 16; constexpr int BC = (HEAD_DIM <= 128) ? 32 : 16;
// Register-hint MIN_BLOCKS tuned per HEAD_DIM's (BC=32) smem+register
// footprint: the largest blocks/SM that avoids register spills.
constexpr int MIN_BLOCKS = (HEAD_DIM <= 32) ? 6 : (HEAD_DIM <= 64) ? 4
: (HEAD_DIM <= 128) ? 3 : 2;
dim3 grid((p.q_len + BR * WARPS - 1) / (BR * WARPS), p.q_head, p.batch); dim3 grid((p.q_len + BR * WARPS - 1) / (BR * WARPS), p.q_head, p.batch);
dim3 block(WARPS * 32, 1, 1); dim3 block(WARPS * 32, 1, 1);
// Static shared memory — no dynamic smem or cudaFuncSetAttribute needed. // Static shared memory — double-buffered K/V only (no sQ: Q goes direct
// sK[BC*LD] + sV[BC*LD] + sQ[BR*LD], all sized by template params. // to registers). 2*BC*LD bf16 each for sK and sV → 4*BC*HEAD_DIM*2 bytes.
attn_prefill_split_q_mma_kernel<HEAD_DIM, WARPS, BC, MIN_BLOCKS><<<grid, block>>>(p); // Occupancy is smem-capped: D=64→3 blocks/SM (16KB), D=128→1 (32KB),
// D=256→1 (32KB, BC=16).
attn_prefill_split_q_mma_kernel<HEAD_DIM, WARPS, BC><<<grid, block>>>(p);
#else #else
constexpr int G = 8, ROWS = 32, P_BC = 32; constexpr int G = 8, ROWS = 32, P_BC = 32;
dim3 grid((p.q_len + ROWS - 1) / ROWS, p.q_head, p.batch); dim3 grid((p.q_len + ROWS - 1) / ROWS, p.q_head, p.batch);
+5 -10
View File
@@ -16,12 +16,7 @@ using bf16 = __nv_bfloat16;
// allocation. The mma fragment layout is used directly: the S accumulator // allocation. The mma fragment layout is used directly: the S accumulator
// (f32) maps element-for-element onto the P matrix_a (bf16) operand, so // (f32) maps element-for-element onto the P matrix_a (bf16) operand, so
// softmax needs no shuffle repack; row reductions fold across the 4-lane // softmax needs no shuffle repack; row reductions fold across the 4-lane
// thread group. Templated on <HEAD_DIM, WARPS, BC, MIN_BLOCKS> with BC a // thread group. Templated on <HEAD_DIM, WARPS, BC> with BC a multiple of 16.
// multiple of 16.
//
// Occupancy: __launch_bounds__ forces the compiler to fit MIN_BLOCKS blocks/SM,
// spilling to local memory as needed. MIN_BLOCKS is tuned per HEAD_DIM to the
// double-buffered smem footprint (2*BC*LD for each of K/V).
// //
// Software pipeline: K/V are double-buffered and loaded via cp.async one tile // Software pipeline: K/V are double-buffered and loaded via cp.async one tile
// ahead, so the next tile streams from global memory while the current tile's // ahead, so the next tile streams from global memory while the current tile's
@@ -35,14 +30,14 @@ using bf16 = __nv_bfloat16;
// register pressure), so fewer, larger tiles beat many tiny ones. // register pressure), so fewer, larger tiles beat many tiny ones.
// //
// Optimizations: load Q fragments directly from global in mma A-operand layout // Optimizations: load Q fragments directly from global in mma A-operand layout
// (no sQ staging, no prologue barriers); pre-scale Q by attention scale during Q load; packed bf16x2 output stores; // (no sQ staging, no prologue barriers); post-multiply scale in float after
// S=Q@K^T to avoid bf16 precision loss; packed bf16x2 output stores;
// causal tile skipping (block-level prefetch bound + warp-level compute skip); // causal tile skipping (block-level prefetch bound + warp-level compute skip);
// XOR swizzle (swiz_col) → eliminates ldmatrix bank conflicts without LD // XOR swizzle (swiz_col) → eliminates ldmatrix bank conflicts without LD
// padding (LD=HEAD_DIM). // padding (LD=HEAD_DIM).
template <int HEAD_DIM, int WARPS, int BC, int MIN_BLOCKS> template <int HEAD_DIM, int WARPS, int BC>
__global__ __launch_bounds__(WARPS * 32, MIN_BLOCKS) __global__ void attn_prefill_split_q_mma_kernel(AttentionParams<bf16> p) {
void attn_prefill_split_q_mma_kernel(AttentionParams<bf16> p) {
constexpr int BR = 16; constexpr int BR = 16;
constexpr int KD = HEAD_DIM / 16; // Q/K k-tiles constexpr int KD = HEAD_DIM / 16; // Q/K k-tiles
constexpr int NC8 = BC / 8; // S n-tiles (N=8 each) constexpr int NC8 = BC / 8; // S n-tiles (N=8 each)
+33 -59
View File
@@ -27,14 +27,14 @@ static bool decode_use_mma(const AttentionParams<bf16>& p) {
return !p.use_mask && G > 1 && G <= 16; return !p.use_mask && G > 1 && G <= 16;
} }
template <int HEAD_DIM, int BC> template <int HEAD_DIM, int BC, int STAGES = (HEAD_DIM <= 128) ? 2 : 1>
static void launch_mma_decode(AttentionParams<bf16>& p, DecodeScratch& sc) { static void launch_mma_decode(AttentionParams<bf16>& p, DecodeScratch& sc) {
int tiles_total = (p.kv_len + BC - 1) / BC; int tiles_total = (p.kv_len + BC - 1) / BC;
p.num_splits = compute_num_splits(p.batch * p.kv_head, tiles_total); p.num_splits = compute_num_splits(p.batch * p.kv_head, tiles_total);
p.o_part = sc.o_part; p.o_part = sc.o_part;
p.ml_part = sc.ml_part; p.ml_part = sc.ml_part;
attn_decode_split_kv_mma_kernel<HEAD_DIM, BC> attn_decode_split_kv_mma_kernel<HEAD_DIM, BC, STAGES>
<<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p); <<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p);
attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p); attn_decode_combine_kernel<<<p.batch * p.q_head, p.head_dim>>>(p);
} }
@@ -61,90 +61,64 @@ static void dispatch_decode_t(AttentionParams<bf16>& p, DecodeScratch& sc) {
} }
static void dispatch_decode(AttentionParams<bf16>& p, DecodeScratch& sc) { static void dispatch_decode(AttentionParams<bf16>& p, DecodeScratch& sc) {
switch (p.head_dim) { dispatch_by_head_dim(p.head_dim, [&]<int D>() { dispatch_decode_t<D>(p, sc); });
case 32: dispatch_decode_t<32>(p, sc); break;
case 64: dispatch_decode_t<64>(p, sc); break;
case 128: dispatch_decode_t<128>(p, sc); break;
case 256: dispatch_decode_t<256>(p, sc); break;
default: printf("bench: unsupported D=%d\n", p.head_dim);
}
} }
// Warmed-up, CUDA-event timed sweep over the production decode MMA path. // Warmed-up, CUDA-event timed sweep over the production decode MMA path.
// Decode (q_len==1) is memory-bound: the two matmuls are GEMV-shaped, so we
// report both effective K/V read bandwidth and the (small) attention FLOP/s.
// FLOP/s = 2 matmuls (q@K^T, P@V), each 2*B*Hq*kv*D flops.
// Bytes = K + V read = 2 * B*Hk*kv*D * sizeof(bf16).
static void bench() { static void bench() {
const int cfgs[][5] = { const int cfgs[][5] = {
{1, 32, 4, 512, 128}, // B,Hq,Hk,seq,D {1, 32, 4, 512, 128}, // B, Hq, Hk, kv_len, D
{1, 32, 4, 1024, 128}, {1, 32, 4, 1024, 128},
{1, 32, 4, 2048, 128}, {1, 32, 4, 2048, 128},
{1, 32, 4, 4096, 128}, {1, 32, 4, 4096, 128},
{16, 32, 4, 2048, 128}, {16, 32, 4, 2048, 128},
{32, 32, 4, 1024, 128}, {32, 32, 4, 1024, 128},
}; };
int n = sizeof(cfgs)/sizeof(cfgs[0]);
const int WARMUP = 10, ITERS = 100; const int WARMUP = 10, ITERS = 100;
printf("\n===== DECODE BENCH (warmup=%d iters=%d) =====\n", WARMUP, ITERS); printf("\n===== DECODE BENCH (warmup=%d iters=%d) =====\n", WARMUP, ITERS);
printf("%-46s | %10s | %10s | %10s\n", print_bench_header();
"config", "latency", "bandwidth", "throughput");
printf("---------------------------------------------------------------"
"----------------------------\n");
for (int ci = 0; ci < n; ci++) { for (int ci = 0; ci < 6; ci++) {
int B=cfgs[ci][0], Hq=cfgs[ci][1], Hk=cfgs[ci][2]; int B = cfgs[ci][0], Hq = cfgs[ci][1], Hk = cfgs[ci][2];
int sl=cfgs[ci][3], D=cfgs[ci][4]; int sl = cfgs[ci][3], D = cfgs[ci][4];
size_t nQ=(size_t)B*Hq*D, nKV=(size_t)B*Hk*sl*D; size_t nQ = (size_t)B * Hq * D;
size_t nKV = (size_t)B * Hk * sl * D;
bf16 *dQ,*dK,*dV,*dO,*tmp; bf16 *dQ, *dK, *dV, *dO;
cudaMalloc(&dQ,nQ*2); cudaMalloc(&dK,nKV*2); cudaMalloc(&dQ, nQ*2); cudaMalloc(&dK, nKV*2);
cudaMalloc(&dV,nKV*2); cudaMalloc(&dO,nQ*2); cudaMalloc(&dV, nKV*2); cudaMalloc(&dO, nQ*2);
size_t big = nQ>nKV?nQ:nKV; tmp=new bf16[big]; size_t big = nQ > nKV ? nQ : nKV; bf16* tmp = new bf16[big];
for (size_t i=0;i<nQ;i++) tmp[i]=f2bf(randf()); for (size_t i = 0; i < nQ; i++) tmp[i] = f2bf(randf());
cudaMemcpy(dQ,tmp,nQ*2,cudaMemcpyHostToDevice); cudaMemcpy(dQ, tmp, nQ*2, cudaMemcpyHostToDevice);
for (size_t i=0;i<nKV;i++) tmp[i]=f2bf(randf()); for (size_t i = 0; i < nKV; i++) tmp[i] = f2bf(randf());
cudaMemcpy(dK,tmp,nKV*2,cudaMemcpyHostToDevice); cudaMemcpy(dK, tmp, nKV*2, cudaMemcpyHostToDevice);
for (size_t i=0;i<nKV;i++) tmp[i]=f2bf(randf()); for (size_t i = 0; i < nKV; i++) tmp[i] = f2bf(randf());
cudaMemcpy(dV,tmp,nKV*2,cudaMemcpyHostToDevice); cudaMemcpy(dV, tmp, nKV*2, cudaMemcpyHostToDevice);
delete[] tmp;
AttentionParams<bf16> p; AttentionParams<bf16> p;
p.batch=B; p.q_head=Hq; p.kv_head=Hk; p.q_len=1; p.kv_len=sl; p.head_dim=D; p.batch = B; p.q_head = Hq; p.kv_head = Hk; p.q_len = 1; p.kv_len = sl;
p.use_mask=0; p.is_causal=0; p.causal_offset=0; p.head_dim = D; p.use_mask = 0; p.is_causal = 0; p.causal_offset = 0;
p.scale=1.0f/sqrtf((float)D); p.scale = 1.0f / sqrtf((float)D);
p.q=dQ; p.k=dK; p.v=dV; p.mask=nullptr; p.o=dO; p.q = dQ; p.k = dK; p.v = dV; p.mask = nullptr; p.o = dO;
DecodeScratch sc; DecodeScratch sc;
cudaMalloc(&sc.o_part, (size_t)B*Hq*32*D*sizeof(float)); cudaMalloc(&sc.o_part, (size_t)B*Hq*32*D*sizeof(float));
cudaMalloc(&sc.ml_part, (size_t)B*Hq*32*2*sizeof(float)); cudaMalloc(&sc.ml_part, (size_t)B*Hq*32*2*sizeof(float));
for (int i=0;i<WARMUP;i++) dispatch_decode(p, sc); auto launch = [&]() { dispatch_decode(p, sc); };
cudaDeviceSynchronize(); double flops = 4.0 * B * Hq * (double)sl * D;
cudaError_t err=cudaGetLastError(); double bytes = 2.0 * (2.0 * nKV * sizeof(bf16));
if (err!=cudaSuccess){printf("CUDA err: %s\n",cudaGetErrorString(err));return;} BenchResult r = bench_kernel(launch, WARMUP, ITERS, flops, bytes);
cudaEvent_t s,e; cudaEventCreate(&s); cudaEventCreate(&e);
cudaEventRecord(s);
for (int i=0;i<ITERS;i++) dispatch_decode(p, sc);
cudaEventRecord(e); cudaEventSynchronize(e);
float ms=0; cudaEventElapsedTime(&ms,s,e); ms/=ITERS;
double flops = 4.0*B*Hq*(double)sl*D;
double tflops = flops/(ms*1e-3)/1e12;
// HBM traffic: K + V read (B*Hk*sl*D each), bf16; Q/O negligible.
double bytes = 2.0 * (2.0*nKV);
double gbps = bytes/(ms*1e-3)/1e9;
char cfg[64]; char cfg[64];
snprintf(cfg, sizeof(cfg), snprintf(cfg, sizeof(cfg),
"B=%2d Hq=%2d Hk=%d q=%4d kv=%4d D=%3d causal=%d", "B=%2d Hq=%2d Hk=%d q=%4d kv=%4d D=%3d causal=%d",
B,Hq,Hk,1,sl,D,0); B, Hq, Hk, 1, sl, D, 0);
printf("%-46s | %7.4f ms | %7.1f GB/s | %6.2f TFLOP/s\n", print_bench_row(cfg, r);
cfg, ms, gbps, tflops);
cudaFree(dQ);cudaFree(dK);cudaFree(dV);cudaFree(dO); cudaFree(dQ); cudaFree(dK); cudaFree(dV); cudaFree(dO);
cudaFree(sc.o_part);cudaFree(sc.ml_part); cudaFree(sc.o_part); cudaFree(sc.ml_part);
delete[]tmp; cudaEventDestroy(s); cudaEventDestroy(e);
} }
} }
+14 -30
View File
@@ -42,9 +42,10 @@ static void launch_paged_decode(PagedAttentionParams<bf16, float>& p) {
int G_check = p.q_head / p.kv_head; int G_check = p.q_head / p.kv_head;
bool use_mma = !p.use_mask && G_check >= 1 && G_check <= 16 && p.page_size >= 32; bool use_mma = !p.use_mask && G_check >= 1 && G_check <= 16 && p.page_size >= 32;
if (use_mma) { if (use_mma) {
constexpr int STAGES = (HEAD_DIM <= 128) ? 2 : 1;
int tiles_total = (p.kv_len + 32 - 1) / 32; int tiles_total = (p.kv_len + 32 - 1) / 32;
p.num_splits = compute_num_splits(p.batch * p.kv_head, tiles_total); p.num_splits = compute_num_splits(p.batch * p.kv_head, tiles_total);
paged_attn_decode_split_kv_mma_kernel<HEAD_DIM, 32> paged_attn_decode_split_kv_mma_kernel<HEAD_DIM, 32, STAGES>
<<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p); <<<dim3(p.kv_head, p.batch, p.num_splits), 32>>>(p);
} else } else
#endif #endif
@@ -221,17 +222,16 @@ static const TestCase TESTS[] = {
}; };
static int dispatch_test(const TestCase& tc) { static int dispatch_test(const TestCase& tc) {
switch (tc.head_dim) { bool matched = false;
case 32: return run_test<32>(tc.B, tc.Hq, tc.Hkv, tc.kv_len, tc.page_size, tc.seed); int r = 0;
case 64: return run_test<64>(tc.B, tc.Hq, tc.Hkv, tc.kv_len, tc.page_size, tc.seed); dispatch_by_head_dim(tc.head_dim, [&]<int D>() {
case 128: return run_test<128>(tc.B, tc.Hq, tc.Hkv, tc.kv_len, tc.page_size, tc.seed); matched = true;
case 256: return run_test<256>(tc.B, tc.Hq, tc.Hkv, tc.kv_len, tc.page_size, tc.seed); r = run_test<D>(tc.B, tc.Hq, tc.Hkv, tc.kv_len, tc.page_size, tc.seed);
default: return 1; });
} return matched ? r : 1;
} }
// Warmed-up, CUDA-event timed sweep over paged decode configs. // Warmed-up, CUDA-event timed sweep over paged decode configs.
// Reports per-call latency and effective K/V read bandwidth.
// Bytes = K + V read through page table (B*Hk*kv*D each), bf16. // Bytes = K + V read through page table (B*Hk*kv*D each), bf16.
template <int HEAD_DIM> template <int HEAD_DIM>
static void bench_config(int B, int Hq, int Hkv, int kv_len, int page_size) { static void bench_config(int B, int Hq, int Hkv, int kv_len, int page_size) {
@@ -281,43 +281,27 @@ static void bench_config(int B, int Hq, int Hkv, int kv_len, int page_size) {
pa.o_part = d_op; pa.ml_part = d_ml; pa.o_part = d_op; pa.ml_part = d_ml;
const int WARMUP = 10, ITERS = 100; const int WARMUP = 10, ITERS = 100;
for (int i = 0; i < WARMUP; i++) launch_paged_decode<HEAD_DIM>(pa); auto launch = [&]() { launch_paged_decode<HEAD_DIM>(pa); };
cudaDeviceSynchronize();
CUDA_CHECK(cudaGetLastError());
cudaEvent_t s, e;
cudaEventCreate(&s); cudaEventCreate(&e);
cudaEventRecord(s);
for (int i = 0; i < ITERS; i++) launch_paged_decode<HEAD_DIM>(pa);
cudaEventRecord(e); cudaEventSynchronize(e);
float ms = 0; cudaEventElapsedTime(&ms, s, e); ms /= ITERS;
double flops = 4.0 * B * Hq * (double)kv_len * HEAD_DIM; double flops = 4.0 * B * Hq * (double)kv_len * HEAD_DIM;
double tflops = flops / (ms * 1e-3) / 1e12;
size_t nKV = (size_t)B * Hkv * kv_len * HEAD_DIM; size_t nKV = (size_t)B * Hkv * kv_len * HEAD_DIM;
double bytes = 2.0 * (2.0 * nKV); double bytes = 2.0 * (2.0 * nKV * sizeof(bf16));
double gbps = bytes / (ms * 1e-3) / 1e9; BenchResult r = bench_kernel(launch, WARMUP, ITERS, flops, bytes);
char cfg[64]; char cfg[64];
snprintf(cfg, sizeof(cfg), snprintf(cfg, sizeof(cfg),
"B=%2d Hq=%2d Hk=%d q=%4d kv=%4d D=%3d page=%3d", "B=%2d Hq=%2d Hk=%d q=%4d kv=%4d D=%3d page=%3d",
B, Hq, Hkv, 1, kv_len, HEAD_DIM, page_size); B, Hq, Hkv, 1, kv_len, HEAD_DIM, page_size);
printf("%-46s | %7.4f ms | %7.1f GB/s | %6.2f TFLOP/s\n", print_bench_row(cfg, r);
cfg, ms, gbps, tflops);
free(tmp); free(tmp);
cudaFree(d_q); cudaFree(d_o); cudaFree(d_q); cudaFree(d_o);
cudaFree(d_k_pool); cudaFree(d_v_pool); cudaFree(d_pt); cudaFree(d_k_pool); cudaFree(d_v_pool); cudaFree(d_pt);
cudaFree(d_op); cudaFree(d_ml); cudaFree(d_op); cudaFree(d_ml);
cudaEventDestroy(s); cudaEventDestroy(e);
} }
static void bench() { static void bench() {
printf("\n===== PAGED DECODE BENCH =====\n"); printf("\n===== PAGED DECODE BENCH =====\n");
printf("%-46s | %10s | %10s | %10s\n", print_bench_header();
"config", "latency", "bandwidth", "throughput");
printf("---------------------------------------------------------------"
"----------------------------\n");
bench_config<128>(1, 32, 4, 512, 128); bench_config<128>(1, 32, 4, 512, 128);
bench_config<128>(1, 32, 4, 1024, 128); bench_config<128>(1, 32, 4, 1024, 128);
bench_config<128>(1, 32, 4, 2048, 128); bench_config<128>(1, 32, 4, 2048, 128);
+1 -3
View File
@@ -18,11 +18,9 @@ static void launch_prefill(AttentionParams<bf16>& p) {
#ifndef ASTRAI_NO_MMA #ifndef ASTRAI_NO_MMA
constexpr int WARPS = 4, BR = 16; constexpr int WARPS = 4, BR = 16;
constexpr int BC = (HEAD_DIM <= 128) ? 32 : 16; constexpr int BC = (HEAD_DIM <= 128) ? 32 : 16;
constexpr int MIN_BLOCKS = (HEAD_DIM <= 32) ? 6 : (HEAD_DIM <= 64) ? 4
: (HEAD_DIM <= 128) ? 3 : 2;
dim3 grid((p.q_len + BR * WARPS - 1) / (BR * WARPS), p.q_head, p.batch); dim3 grid((p.q_len + BR * WARPS - 1) / (BR * WARPS), p.q_head, p.batch);
dim3 block(WARPS * 32, 1, 1); dim3 block(WARPS * 32, 1, 1);
attn_prefill_split_q_mma_kernel<HEAD_DIM, WARPS, BC, MIN_BLOCKS><<<grid, block>>>(p); attn_prefill_split_q_mma_kernel<HEAD_DIM, WARPS, BC><<<grid, block>>>(p);
#else #else
constexpr int G = 8, ROWS = 32, P_BC = 32; constexpr int G = 8, ROWS = 32, P_BC = 32;
dim3 grid((p.q_len + ROWS - 1) / ROWS, p.q_head, p.batch); dim3 grid((p.q_len + ROWS - 1) / ROWS, p.q_head, p.batch);
+64
View File
@@ -37,6 +37,70 @@ inline int compute_num_splits(int base_blocks, int tiles_total) {
} \ } \
} while (0) } while (0)
struct BenchResult {
float ms;
double gbps;
double tflops;
};
template <typename Fn>
BenchResult bench_kernel(Fn launch, int warmup, int iters,
double flops, double bytes) {
for (int i = 0; i < warmup; i++) launch();
cudaDeviceSynchronize();
cudaError_t err = cudaGetLastError();
if (err != cudaSuccess) {
printf("CUDA error before bench: %s\n", cudaGetErrorString(err));
return {0, 0, 0};
}
cudaEvent_t s, e;
cudaEventCreate(&s); cudaEventCreate(&e);
cudaEventRecord(s);
for (int i = 0; i < iters; i++) launch();
cudaEventRecord(e); cudaEventSynchronize(e);
float ms = 0; cudaEventElapsedTime(&ms, s, e); ms /= iters;
cudaEventDestroy(s); cudaEventDestroy(e);
return {ms, bytes / (ms * 1e-3) / 1e9, flops / (ms * 1e-3) / 1e12};
}
inline void print_bench_header() {
printf("%-46s | %10s | %10s | %10s\n",
"config", "latency", "bandwidth", "throughput");
printf("---------------------------------------------------------------"
"----------------------------\n");
}
inline void print_bench_row(const char* cfg, const BenchResult& r) {
printf("%-46s | %7.4f ms | %7.1f GB/s | %6.2f TFLOP/s\n",
cfg, r.ms, r.gbps, r.tflops);
}
template <int... Ds>
struct _HeadSwitch;
template <int D>
struct _HeadSwitch<D> {
template <typename Fn>
static void call(int hd, Fn&& fn) { if (hd == D) fn.template operator()<D>(); }
};
template <int D, int... Rest>
struct _HeadSwitch<D, Rest...> {
template <typename Fn>
static void call(int hd, Fn&& fn) {
if (hd == D) fn.template operator()<D>();
else _HeadSwitch<Rest...>::call(hd, fn);
}
};
// Default set: 32, 64, 128, 256
template <typename Fn>
void dispatch_by_head_dim(int head_dim, Fn&& fn) {
_HeadSwitch<32, 64, 128, 256>::call(head_dim, fn);
}
// Generic CPU reference for multi-query / grouped-query attention. // Generic CPU reference for multi-query / grouped-query attention.
// Tensor shapes (all float*): // Tensor shapes (all float*):
// Q : [B, Hq, q_len, D] // Q : [B, Hq, q_len, D]