Foundry/cuda_kernels/attention.cu

2674 lines
121 KiB
Plaintext

/* SPDX-License-Identifier: AGPL-3.0-or-later */
/* Copyright (C) 2026 David (Corey) Herrera */
/**
* @file attention.cu
* @brief On-die causal GQA flash attention for Foundry (fp32 and Q4_0 KV cache).
*
* Written from a behaviour specification (docs/cleanroom/f_cua_spec.md); no ggml or llama.cpp source was consulted.
* Scope of that rewrite: the Q4_0 KV-cache format defines, fc_kvq4_row_bytes,
* fc_kvq4_quant_block, fc_kvq4_dequant_elem, fc_kvq4_dequant_lane_chunk and
* fc_attn_flash_prefill_q4_kvs; the rest of the file is unchanged original work.
*
* Target: sm_37 (K80, Kepler)
* Compile: nvcc -ptx -arch=sm_37 attention.cu -o attention.ptx
*
* Replaces the HOST-side causal GQA softmax in the qwen35moe batched
* prefill (the O(N^2) 2-core bottleneck: 87.5% of an 8k prefill) with a
* flash-attention-style kernel: tiled QK^T -> ONLINE softmax (running
* max + running sum, the N x N score matrix is never materialized) ->
* PV accumulate, entirely on the die against a GPU-resident KV cache.
*
* Numerics: same math as the host reference (qwen35moe_forward_cuda.c
* host softmax) but reduced in tile order with running-max rescaling, so
* results drift ~1e-6 relative vs the host's global-max single pass —
* NOT bit-exact by design (gate: argmax-exact + logits within ~1e-3).
*
* Sliding-window attention (QWEN35_ATTN_WINDOW, StreamingLLM-style): each
* flash kernel takes attn_sink/attn_win. When attn_win > 0 the query at
* absolute position cur_pos attends ONLY the key set
* [0, attn_sink) U [cur_pos - attn_win + 1, cur_pos]
* (attention sinks + recent window); tiles that intersect neither are
* skipped BEFORE any staging/sync, turning prefill O(N^2) -> O(N*W). The
* online-softmax (m, l, acc) simply never sees the excluded keys, so the
* result is a correct softmax over the windowed set. When attn_win <= 0
* every tile is processed — byte-identical to full attention (the OFF /
* A-baseline path). A query always sees its own position (cur_pos is in
* the recent window for attn_win >= 1); an overlapping sink/window tile is
* processed once (the range predicate is an OR).
*
* PTX symbols emitted (for embedding into ../cuda/fc_cuda_ptx.h):
* FC_PTX_ATTENTION -> kernels fc_attn_qz_split, fc_attn_kv_append,
* fc_attn_flash_prefill (+ _q4 variants),
* fc_attn_flash_decode(_q4) + _combine,
* the *_dev device-position variants for
* CUDA-graph capture (C1), and the *_slots
* multi-user decode variants (L7)
*
* Kepler notes:
* - Pure fp32 FMA (no tensor cores); expf matches the host libm family.
* - 48 KB shared per block: the flash kernel's working set (q group +
* one padded K tile + softmax state) is 26 720 B for kv_groups=8/
* head_dim=256, so 4 blocks per SMX (114 688 B/SMX; shared -- not
* registers -- is what caps residency here). V is streamed through
* __ldg (read-only cache), never staged in shared.
* - __shfl_down_sync with width 16 for the dot-product lane reduction
* (sm_30+; the mask is ignored pre-Volta).
*
* @status None
* @issues None
* @todo Split into prefill and decode files (see the TODO in the
* NARROW-STAGING comment for the SASS-identity gate it needs).
*/
/* Flash-kernel tiling: keys staged per tile, lanes cooperating on one
* q.k dot, and the register-accumulator cap on GQA group size. */
#define FC_ATTN_TILE_KEYS 16
#define FC_ATTN_DOT_LANES 16
#define FC_ATTN_MAX_GROUPS 8
/* K-tile row stride, in floats. The tile is read as k_s[ky*stride + lane +
* i*16] with ky = tid/16 and lane = tid%16, so the two half-warps of a warp
* carry IDENTICAL lanes and ADJACENT ky. At the shipped head_dim = 256 an
* unpadded row stride is exactly 8 x 32 banks, which puts both half-warps on
* the same banks at different addresses: a 2-way conflict on every k-operand
* LDS. Padding the row by half a bank cycle breaks it for any head_dim that
* is a multiple of 16. Costs TILE_KEYS*16 floats (1 KB) of dynamic shared and
* does not change occupancy at the shipped shape (26 720 B x 4 blocks of
* 114 688 B/SMX). MUST match fc_attn_flash_shared_bytes() in cuda/fc_cuda.c. */
#define FC_ATTN_K_PAD 16
#define FC_ATTN_K_STRIDE(hd) ((hd) + FC_ATTN_K_PAD)
/* ============================================================
* 4-bit KV cache (Q4_0 blocks)
*
* A K or V row of head_dim floats is stored as head_dim/32 blocks of 18 bytes:
* bytes 0-1 scale d, binary16 bit pattern, little-endian
* bytes 2-17 16 code bytes; byte 2+j holds element j in its low nibble and
* element j+16 in its high nibble
* A code c in 0..15 decodes to (c - 8) * d. The host (Pantheon pn_kvq4_*)
* reads and writes the same bytes, so the layout is an interface.
* ============================================================ */
#include <cuda_fp16.h>
#define FC_KVQ4_BLK 32
#define FC_KVQ4_BLK_BYTES 18
/* fp16 sink + recent-window exemption: the attention sink (first `sink`
* positions) and the last `win` tokens are kept at fp16 in small side buffers
* instead of q4, so greedy argmax stops flipping on near-ties. The q4 body
* still holds every position; recent rows are read from the fp16 window ring
* while recent and from q4 once they age out (no rolling). Buffers are sized to
* these maxima (must match host PN_KVQ4_WMAX / PN_KVQ4_SINKMAX).
*
* THE RING LAPS AT FC_KVQ4_WMAX, NOT AT `win`. The reader's window is only
* `win` wide, but a BATCHED prefill call appends ALL n rows before ANY query
* row reads, so a ring lapped at `win` lets row t's own window keys be
* overwritten by rows t+1..n-1 of the same call — row t then attends to K/V
* from positions AFTER it, a causality violation (see fc_attn_flash_prefill_q4
* for the guard and Pantheon results/q4_win_ring_hazard.md for the history).
* Lapping at WMAX instead keeps live every key a batch of up to
* (WMAX - win) rows can still need, and costs nothing: the buffers were
* always WMAX rows per head — only `win` of them were ever being used. */
#define FC_KVQ4_WMAX 256
#define FC_KVQ4_SINKMAX 8
/* Predicate: key position s read from the fp16 shadow (sink or recent window)
* for a query at cur_pos. Matches host pn_kvq4_is_fp16 exactly. */
__device__ __forceinline__ int fc_kvq4_is_fp16(int s, int cur_pos, int sink,
int win) {
return (s < sink) || (win > 0 && s > cur_pos - win);
}
/* Bytes in one Q4_0 row of head_dim elements (host mirror: FC_ATTN_KVQ4_ROW_BYTES). */
__device__ __forceinline__ int fc_kvq4_row_bytes(int head_dim) {
return (head_dim / FC_KVQ4_BLK) * FC_KVQ4_BLK_BYTES;
}
/* Max head_dim the append kernels stage in shared memory (CUDA's block-size
* ceiling; every dispatcher launches block = head_dim threads). */
#define FC_KVQ4_MAX_HEAD_DIM 1024
/* Quantize one 32-element block x32 (shared memory, already synced) into the
* 18 bytes at blk_ptr. Called by all 32 lanes of a warp with the same block;
* lane 0 writes the scale, lanes 0..15 one code byte each, so every byte of the
* block is written exactly once.
*
* Each lane folds the block maximum itself instead of reducing across the warp:
* on sm_37 a shuffle from an inactive source lane returns 0, which corrupted the
* high nibbles in a cooperative version. The serial fold also matches the host
* quantizer's order bit for bit. The scale is stored negative (d = amax / -8).
* The order convert -> +8 -> clamp is part of the output for out-of-range
* products (NaN scale, reciprocal overflow); keep it. */
__device__ __forceinline__ void fc_kvq4_quant_block(const float *x32,
unsigned char *blk_ptr,
int lane) {
float amax = 0.0f;
for (int j = 0; j < FC_KVQ4_BLK; j++)
amax = fmaxf(amax, fabsf(x32[j]));
const float d = amax / -8.0f;
const float id = (d != 0.0f) ? 1.0f / d : 0.0f;
if (lane == 0) {
/* One 16-bit store. Every block starts at an even offset (18-byte
* stride), so the scale is 2-byte aligned. Do not split it into two
* byte stores: ptxas 11.4 (sm_37) compiles st.u8 of the f16 register
* as a value conversion (F2I.U32.F16.TRUNC) and writes 0 as the low
* byte of every (negative) scale. */
*(__half *)blk_ptr = __float2half(d);
}
if (lane < FC_KVQ4_BLK / 2) {
int q_lo = (int)rintf(x32[lane] * id) + 8;
int q_hi = (int)rintf(x32[lane + FC_KVQ4_BLK / 2] * id) + 8;
if (q_lo < 0)
q_lo = 0;
else if (q_lo > 15)
q_lo = 15;
if (q_hi < 0)
q_hi = 0;
else if (q_hi > 15)
q_hi = 15;
blk_ptr[2 + lane] = (unsigned char)(q_lo | (q_hi << 4));
}
}
/* Decode element e of a Q4_0 row. The scale is read as two byte loads. */
__device__ __forceinline__ float fc_kvq4_dequant_elem(const unsigned char *row, int e) {
const unsigned char *blk = row + (e / FC_KVQ4_BLK) * FC_KVQ4_BLK_BYTES;
const unsigned short d_bits = (unsigned short)(blk[0] | (blk[1] << 8));
const float d = __half2float(__ushort_as_half(d_bits));
const int local = e & (FC_KVQ4_BLK - 1);
const int half_blk = FC_KVQ4_BLK / 2;
const int code_off = local < half_blk ? local : local - half_blk;
const int shift = local < half_blk ? 0 : 4;
const int code = (blk[2 + code_off] >> shift) & 0xF;
return (float)(code - 8) * d;
}
/* Sliding-window tile predicate: is the key tile [j0, j0+tk) attended by the
* query at absolute position cur_pos? True when attn_win <= 0 (full attention),
* OR the tile intersects the sink region [0, attn_sink), OR (attn_win > 0) it
* intersects the recent window [cur_pos - attn_win + 1, cur_pos]. The test is
* uniform across the block (depends only on j0/tk/cur_pos), so branching on it
* before a __syncthreads() is safe. */
__device__ __forceinline__ int fc_attn_tile_in_window(int j0, int tk,
int cur_pos,
int attn_sink,
int attn_win) {
if (attn_win <= 0)
return 1;
if (j0 < attn_sink)
return 1;
int win_lo = cur_pos - attn_win + 1;
if (win_lo < 0)
win_lo = 0;
/* intersects [win_lo, cur_pos]: tile end > win_lo AND tile start <= cur_pos */
return (j0 + tk > win_lo) && (j0 <= cur_pos);
}
extern "C" {
/*
* Split the joint Q+gate projection into contiguous q and z tensors.
*
* qg: [n][n_head][2*head_dim] fp32 — per head: [q head_dim | z head_dim]
* q, z: [n][n_head][head_dim] fp32 (written)
* head_dim: per-head dimension
* total: n * n_head * head_dim (elements of each output)
*
* Replaces the host-side split loop (a_qg D2H + memcpy + a_q H2D).
* 1D launch, one thread per output element.
*/
__global__ void fc_attn_qz_split(const float *qg, float *q, float *z,
int head_dim, int total) {
int i = blockIdx.x * blockDim.x + threadIdx.x;
if (i >= total) return;
int dd = i % head_dim;
int th = i / head_dim; /* flattened (position, head) */
size_t src = (size_t)th * 2 * head_dim + dd;
q[i] = qg[src];
z[i] = qg[src + head_dim];
}
/*
* Append n roped K/V rows to the GPU-resident ring KV cache.
*
* k_new, v_new: [n][n_head_kv][head_dim] fp32 (post QK-norm + RoPE)
* k_cache, v_cache: [n_head_kv][max_seq][head_dim] fp32 (written) — same
* layout as the host cache so host<->GPU syncs are
* contiguous per-head row ranges
* pos0: global position of row 0 (row t lands at
* (pos0 + t) % max_seq, the host ring rule)
*
* Grid (n, n_head_kv), block head_dim threads.
*/
__global__ void fc_attn_kv_append(const float *k_new, const float *v_new,
float *k_cache, float *v_cache,
int n_head_kv, int head_dim, int max_seq,
int pos0) {
int t = blockIdx.x;
int h = blockIdx.y;
int dd = threadIdx.x;
if (dd >= head_dim) return;
int cache_pos = (pos0 + t) % max_seq;
size_t src = ((size_t)t * n_head_kv + h) * head_dim + dd;
size_t dst = ((size_t)h * max_seq + cache_pos) * head_dim + dd;
k_cache[dst] = k_new[src];
v_cache[dst] = v_new[src];
}
/*
* Causal GQA flash attention over the GPU KV cache (prefill mode).
*
* q: [n][n_head][head_dim] fp32 (post QK-norm + RoPE);
* n_head = gridDim.y * kv_groups, group g of kv head h is
* global head h*kv_groups + g (the host GQA map qh/kv_groups)
* k_cache: [n_head_kv][max_seq][head_dim] fp32 (rows [0, pos0+n) live)
* v_cache: same layout
* out: [n][n_head][head_dim] fp32 (written; softmax(qK^T*scale)V)
* kv_groups: q heads per kv head (<= FC_ATTN_MAX_GROUPS)
* pos0: global position of query row 0 — query t attends cache rows
* [0, pos0+t] (causal); caller guarantees pos0+n <= max_seq
* (no ring wrap inside the window)
* scale: 1/sqrt(head_dim)
*
* Grid (n, n_head_kv), block head_dim threads (multiple of 16), dynamic
* shared = (kv_groups*head_dim + TILE*FC_ATTN_K_STRIDE(head_dim) +
* 2*kv_groups*TILE + 3*kv_groups) floats.
*
* Per K tile: the block's kv_groups q heads score the tile's keys with
* 16-lane cooperative dots (each KV row is read ONCE per tile and reused
* across the whole q group — the GQA lever), one thread per head folds
* the tile into the running (max, sum) state, then every thread
* accumulates its output dim across the tile's V rows with the standard
* online-softmax rescale. Fully-masked tiles are never visited (the key
* loop stops at the query's causal bound).
*/
__global__ void fc_attn_flash_prefill(const float *q, const float *k_cache,
const float *v_cache, float *out,
int kv_groups, int head_dim,
int max_seq, int pos0, int attn_sink,
int attn_win, float scale) {
const int t = blockIdx.x; /* query row in the batch */
const int kvh = blockIdx.y; /* kv head */
const int tid = threadIdx.x; /* == output dim owned */
const int G = kv_groups;
const int seq_len = pos0 + t + 1; /* causal window */
const int cur_pos = seq_len - 1; /* this query's absolute position */
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ float sh[];
float *q_s = sh; /* [G][head_dim] */
float *k_s = q_s + (size_t)G * head_dim; /* [TILE][kstride] */
float *score_s = k_s + (size_t)FC_ATTN_TILE_KEYS * kstride; /* [G][TILE] */
float *p_s = score_s + G * FC_ATTN_TILE_KEYS; /* [TILE][G] */
float *m_s = p_s + G * FC_ATTN_TILE_KEYS; /* [G] running max */
float *l_s = m_s + G; /* [G] running sum */
float *corr_s = l_s + G; /* [G] tile rescale */
/* Stage this block's q group; init the online-softmax state. */
const size_t n_head = (size_t)gridDim.y * G;
const float *q_base = q + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000); /* -inf */
l_s[tid] = 0.0f;
}
__syncthreads();
/* Compile-time width, so ptxas keeps acc[] in REGISTERS. Every loop that
* indexes acc[] is unrolled to FC_ATTN_MAX_GROUPS with an `if (g < G)`
* guard; a loop bounded by the RUNTIME kv_groups cannot be unrolled, the
* array lands in LOCAL memory, and the PV inner loop then pays an
* LDL.128/STL.128 round trip per key. ptxas reports that as a 32-byte
* "stack frame" and prints "0 bytes spill stores" right beside it --
* a never-promoted local array is not counted as a spill. */
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const float *k_head = k_cache + (size_t)kvh * max_seq * head_dim;
const float *v_head = v_cache + (size_t)kvh * max_seq * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES; /* key-in-tile this thread scores */
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES; /* dims per lane */
for (int j0 = 0; j0 < seq_len; j0 += FC_ATTN_TILE_KEYS) {
int tk = seq_len - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
/* Sliding window: skip tiles outside sink U recent-window before any
* staging/sync (uniform across the block — no __syncthreads() split). */
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
/* Stage the K tile (coalesced; read once, reused by all G heads).
* Key-outer so the padded row stride costs no address arithmetic. */
for (int key = 0; key < tk; key++)
for (int e = tid; e < head_dim; e += blockDim.x)
k_s[(size_t)key * kstride + e] =
k_head[(size_t)(j0 + key) * head_dim + e];
__syncthreads();
/* QK^T: 16 lanes cooperate on each (head, key) dot.
*
* i-outer / g-inner: each thread reads its chunk of the staged K
* row ONCE per tile into a register and fans it across the whole GQA
* group, instead of re-reading k_s once per group (G=8 -> 1 LDS per
* element). BIT-EXACT: every part[g] still accumulates over e in the
* same ascending order the g-outer form used, so this is pure operand
* motion. part[] is indexed only by the unrolled compile-time g, for
* the same reason acc[] is -- a runtime-bounded loop would push it to
* LOCAL memory and cost an LDL/STL round trip per element. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const float *kv = k_s + (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kve = kv[e];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
/* G is uniform across the block, so the guarded unroll keeps the
* shuffle warp-uniform exactly as the runtime-bounded loop did. */
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
/* Online-softmax fold, one thread per head: new running max, the
* rescale factor for the accumulators, this tile's probabilities. */
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj; /* [TILE][G]: PV reads a run */
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
/* PV accumulate: thread tid owns out dim tid; V rows stream through
* the read-only cache (coalesced across the block). */
for (int j = 0; j < tk; j++) {
float vv = __ldg(&v_head[(size_t)(j0 + j) * head_dim + tid]);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads(); /* next tile overwrites k_s/score_s/p_s */
}
float *out_base = out + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
out_base[(size_t)g * head_dim + tid] = acc[g] / l_s[g];
}
/*
* Append n roped K/V rows to the GPU-resident Q4 ring KV cache (quantizing
* each row to Q4_0 as it lands). Same ring rule as fc_attn_kv_append.
*
* k_new, v_new: [n][n_head_kv][head_dim] fp32 (post QK-norm + RoPE)
* k_cache, v_cache: [n_head_kv][max_seq][row_bytes] Q4_0 bytes (written)
* pos0: global position of row 0 (caller guarantees pos0+n<=max_seq
* so cache_pos == pos0+t, no ring wrap in the window)
*
* Grid (n, n_head_kv), block head_dim threads (a multiple of 32; each 32-lane
* warp owns one Q4_0 block of the row).
*/
__global__ void fc_attn_kv_append_q4(const float *k_new, const float *v_new,
unsigned char *k_cache,
unsigned char *v_cache,
__half *k_sink, __half *v_sink,
__half *k_win, __half *v_win,
int n_head_kv, int head_dim, int max_seq,
int pos0, int sink, int win) {
int t = blockIdx.x;
int h = blockIdx.y;
int tid = threadIdx.x;
if (tid >= head_dim) return;
int cache_pos = pos0 + t;
int row_bytes = fc_kvq4_row_bytes(head_dim);
size_t src = ((size_t)t * n_head_kv + h) * head_dim + tid;
size_t dst_row = ((size_t)h * max_seq + cache_pos) * row_bytes;
int blk = tid / FC_KVQ4_BLK;
int lane = tid & (FC_KVQ4_BLK - 1);
/* Stage the row in shared memory so the (shuffle-free, Kepler-safe)
* block quantizer can read all 32 elements of its block. */
__shared__ float s_k[FC_KVQ4_MAX_HEAD_DIM];
__shared__ float s_v[FC_KVQ4_MAX_HEAD_DIM];
float kf = k_new[src];
float vf = v_new[src];
s_k[tid] = kf;
s_v[tid] = vf;
__syncthreads();
fc_kvq4_quant_block(s_k + (size_t)blk * FC_KVQ4_BLK,
k_cache + dst_row + (size_t)blk * FC_KVQ4_BLK_BYTES,
lane);
fc_kvq4_quant_block(s_v + (size_t)blk * FC_KVQ4_BLK,
v_cache + dst_row + (size_t)blk * FC_KVQ4_BLK_BYTES,
lane);
/* Mirror the row into the fp16 sink / recent-window shadow (element tid). */
if (win > 0) {
size_t wi = ((size_t)h * FC_KVQ4_WMAX + (cache_pos % FC_KVQ4_WMAX)) * head_dim + tid;
k_win[wi] = __float2half(kf);
v_win[wi] = __float2half(vf);
}
if (cache_pos < sink) {
size_t si = ((size_t)h * FC_KVQ4_SINKMAX + cache_pos) * head_dim + tid;
k_sink[si] = __float2half(kf);
v_sink[si] = __float2half(vf);
}
}
/*
* Causal GQA flash attention over the Q4 GPU KV cache (prefill mode). Identical
* online-softmax math and tiling to fc_attn_flash_prefill; the only difference
* is K/V are read from the packed Q4_0 cache and dequantized in-kernel (the K
* tile into shared, V per element on read) so no fp32 KV is ever materialized.
*
* k_cache/v_cache: [n_head_kv][max_seq][row_bytes] Q4_0 bytes.
* Grid/shared/other args as fc_attn_flash_prefill.
*/
__global__ void fc_attn_flash_prefill_q4(const float *q,
const unsigned char *k_cache,
const unsigned char *v_cache,
const __half *k_sink,
const __half *v_sink,
const __half *k_win,
const __half *v_win,
float *out, int kv_groups,
int head_dim, int max_seq, int pos0,
int sink, int win, int attn_sink,
int attn_win, float scale) {
const int t = blockIdx.x;
const int kvh = blockIdx.y;
const int tid = threadIdx.x;
const int G = kv_groups;
const int seq_len = pos0 + t + 1;
const int cur_pos = seq_len - 1; /* this query row's absolute position */
const int row_bytes = fc_kvq4_row_bytes(head_dim);
/* Liveness floor for the fp16 window ring. Every row of this call was
* appended BEFORE any row reads, so slot (p % WMAX) holds position p only
* while p + WMAX > last_pos, where last_pos is the highest position this
* call appended (row gridDim.x-1). Keys below that floor were lapped by a
* LATER row of the same batch; reading them would attend to a future
* token. They are still exact in the q4 body, so fall back to it. With the
* host capping a batch at (WMAX - win) rows this never fires; it is the
* backstop that keeps an oversized batch merely lossier, never acausal. */
const int win_live_lo = (cur_pos - t + (int)gridDim.x - 1) - FC_KVQ4_WMAX;
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ float sh[];
float *q_s = sh;
float *k_s = q_s + (size_t)G * head_dim;
float *score_s = k_s + (size_t)FC_ATTN_TILE_KEYS * kstride;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const size_t n_head = (size_t)gridDim.y * G;
const float *q_base = q + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
/* Compile-time width, so ptxas keeps acc[] in REGISTERS. Every loop that
* indexes acc[] is unrolled to FC_ATTN_MAX_GROUPS with an `if (g < G)`
* guard; a loop bounded by the RUNTIME kv_groups cannot be unrolled, the
* array lands in LOCAL memory, and the PV inner loop then pays an
* LDL.128/STL.128 round trip per key. ptxas reports that as a 32-byte
* "stack frame" and prints "0 bytes spill stores" right beside it --
* a never-promoted local array is not counted as a spill. */
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const unsigned char *k_head = k_cache + (size_t)kvh * max_seq * row_bytes;
const unsigned char *v_head = v_cache + (size_t)kvh * max_seq * row_bytes;
const __half *k_win_head = k_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *v_win_head = v_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *k_sink_head = k_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const __half *v_sink_head = v_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES;
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = 0; j0 < seq_len; j0 += FC_ATTN_TILE_KEYS) {
int tk = seq_len - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
/* Sliding window: skip tiles outside sink U recent-window (attn_sink/
* attn_win are the ATTENDED set, orthogonal to sink/win which pick the
* fp16-shadow vs q4-dequant read path). Uniform across the block. */
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
/* Stage the K tile (read once, reused by all G heads): fp16 shadow for
* sink/recent keys, in-kernel q4 dequant for the aged-out middle. */
for (int key = 0; key < tk; key++) {
int kp = j0 + key;
float *krow = k_s + (size_t)key * kstride;
if (kp < sink) {
const __half *s = k_sink_head + (size_t)kp * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = __half2float(s[e]);
} else if (win > 0 && kp > cur_pos - win && kp > win_live_lo) {
const __half *s = k_win_head +
(size_t)(kp % FC_KVQ4_WMAX) * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = __half2float(s[e]);
} else {
const unsigned char *s = k_head + (size_t)kp * row_bytes;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = fc_kvq4_dequant_elem(s, e);
}
}
__syncthreads();
/* QK^T, i-outer / g-inner: each thread reads its chunk of the staged K
* row ONCE per tile into a register and fans it across the whole GQA
* group, instead of re-reading k_s once per group (G=8 -> 1 LDS per
* element). BIT-EXACT: every part[g] still accumulates over e in the
* same ascending order the g-outer form used, so this is pure operand
* motion. part[] is indexed only by the unrolled compile-time g, for
* the same reason acc[] is -- a runtime-bounded loop would push it to
* LOCAL memory and cost an LDL/STL round trip per element. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const float *kv = k_s + (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kve = kv[e];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
/* G is uniform across the block, so the guarded unroll keeps the
* shuffle warp-uniform exactly as the runtime-bounded loop did. */
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj; /* [TILE][G]: PV reads a run */
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
/* PV accumulate: thread tid owns out dim tid; V rows dequantized on
* read (element tid of each V row). */
for (int j = 0; j < tk; j++) {
int vp = j0 + j;
float vv;
if (vp < sink)
vv = __half2float(v_sink_head[(size_t)vp * head_dim + tid]);
else if (win > 0 && vp > cur_pos - win && vp > win_live_lo)
vv = __half2float(v_win_head[(size_t)(vp % FC_KVQ4_WMAX) * head_dim + tid]);
else
vv = fc_kvq4_dequant_elem(v_head + (size_t)vp * row_bytes, tid);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
float *out_base = out + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
out_base[(size_t)g * head_dim + tid] = acc[g] / l_s[g];
}
/* ============================================================
* REGISTER-K prefill variant (FC_ATTN_KTILE_REG) — BIT-EXACT, DEFAULT OFF
*
* Same tiling, same online softmax, same q4 KV cache and the same accumulation
* ORDER as fc_attn_flash_prefill_q4. One thing changes: the staged K tile is
* DELETED and K lives in per-thread registers instead.
*
* Why that is free. In the QK dot, thread (ky, lane) reads exactly the
* head_dim/16 elements k[ky][lane + 16*i] of key ky and nothing else — the
* shared tile is written by one thread and read back by that same thread. It
* exists only because the STAGING loop assigns elements element-outer
* (`for e = tid; e < head_dim; e += blockDim.x`) while the CONSUMING loop reads
* them lane-strided. Re-assigning the staging to match the consumer removes the
* STS/LDS round trip and the tile with it:
*
* shared 26 720 -> 9 312 B residency 4 -> 7 blocks/SMX
* registers 72 (unchanged) stack frame 0, no LDL/STL
*
* Measured on the standalone microbenchmark (cuda_kernels/tests/exp_attn_qtile.cu
* arm `xq_q1_kr`, driven by cuda/tests/bench_attn_qtile.c): 1.51x on the kernel
* at the shipped shape, depth-flat 1 k -> 32 k, at both 666 and 875 MHz, and
* BIT-EXACT — 0 of 524 288 output floats differ from fc_attn_flash_prefill_q4.
* The factor decomposes as 1.194x structural (the round trip) x 1.269x from the
* residency gain. Q-tiling the same body was REFUTED there (Q_TILE=2 costs
* shared and comes out at 0.886x); this variant is Q_TILE=1 only.
*
* TWO THINGS ARE LOAD-BEARING AND MUST NOT BE "SIMPLIFIED":
*
* 1. The HOISTED dequant (fc_kvq4_dequant_lane_chunk) is the enabler, not a
* detail. Calling fc_kvq4_dequant_elem once per element instead costs
* 105 REGISTERS, which caps residency back at 4 blocks/SMX and collapses
* the measured gain from 1.51x to 1.068x (microbench arm `xq_q1_krn`).
* 2. The online-softmax rescale stays FUSED in the PV loop at j == 0, exactly
* where fc_attn_flash_prefill_q4 puts it, because ptxas contracts
* `acc *= corr; acc += p0*v0` into a single `acc = fma(acc, corr, p0*v0)` —
* one rounding instead of two. Hoisting it out of the j loop is
* algebraically identical and moved 148 099 of 524 288 output floats by up
* to 4.0e-7 relative (1-ulp class), measured. Keep it at j == 0.
*
* CONTRACT (enforced host-side in fc_attn_flash_prefill_q4_ctx):
* - head_dim <= FC_ATTN_KREG_MAX_CHUNK * FC_ATTN_DOT_LANES (= 256), so the
* kreg[] array is bounded by a COMPILE-TIME constant. An array bounded by
* the runtime `chunk` is never promoted to registers: it lands in local
* memory and ptxas prints "0 bytes spill" beside a nonzero stack frame.
* - FC_ATTN_DOT_LANES == 16 (the hoisted dequant's nibble mapping).
* - attn_win <= 0 (full attention). The tile predicate here is the shipped
* one and a windowed prefill should behave identically, but that is
* UNMEASURED, so the host declines attn_win > 0 and uses the shipped
* kernel.
* ============================================================ */
#if FC_ATTN_DOT_LANES != 16
#error "fc_kvq4_dequant_lane_chunk assumes FC_ATTN_DOT_LANES == 16"
#endif
/* Compile-time bound on head_dim / FC_ATTN_DOT_LANES; 16 covers head_dim 256. */
#define FC_ATTN_KREG_MAX_CHUNK 16
/* Decode the elements a QK-dot lane owns, lane + 16*i for i < chunk, into
* kreg[i]. Element lane + 16*i sits in block i/2 at code byte 2 + lane: the low
* nibble for even i, the high nibble for odd i. So one scale and one code byte
* per block serve two kreg entries. kreg[chunk..] is left untouched.
*
* Unrolled over the compile-time bound so every kreg index is a constant; a
* runtime index would demote kreg to local memory. Requires lane < 16 and
* chunk <= FC_ATTN_KREG_MAX_CHUNK. Bit-equal to fc_kvq4_dequant_elem. */
__device__ __forceinline__ void fc_kvq4_dequant_lane_chunk(
const unsigned char *row, int lane, int chunk, float *kreg) {
#pragma unroll
for (int b = 0; b < FC_ATTN_KREG_MAX_CHUNK / 2; b++) {
if (2 * b < chunk) {
const unsigned char *bp = row + (size_t)b * FC_KVQ4_BLK_BYTES;
const unsigned short d_bits = (unsigned short)(bp[0] | (bp[1] << 8));
const float d = __half2float(__ushort_as_half(d_bits));
const unsigned int codes = bp[2 + lane];
kreg[2 * b] = (float)((int)(codes & 0xF) - 8) * d;
if (2 * b + 1 < chunk)
kreg[2 * b + 1] = (float)((int)((codes >> 4) & 0xF) - 8) * d;
}
}
}
/*
* Causal GQA flash attention over the Q4 GPU KV cache (prefill mode), with the
* K tile held in REGISTERS instead of shared memory. Bit-exact against
* fc_attn_flash_prefill_q4; see the block comment above for the contract, the
* measured factor and the two things that must not be changed.
*
* Args, grid and block are identical to fc_attn_flash_prefill_q4 — only the
* dynamic-shared request differs (fc_attn_flash_kreg_shared_bytes in
* cuda/fc_cuda.c).
*/
__global__ void fc_attn_flash_prefill_q4_kr(const float *q,
const unsigned char *k_cache,
const unsigned char *v_cache,
const __half *k_sink,
const __half *v_sink,
const __half *k_win,
const __half *v_win,
float *out, int kv_groups,
int head_dim, int max_seq, int pos0,
int sink, int win, int attn_sink,
int attn_win, float scale) {
const int t = blockIdx.x;
const int kvh = blockIdx.y;
const int tid = threadIdx.x;
const int G = kv_groups;
const int seq_len = pos0 + t + 1;
const int cur_pos = seq_len - 1;
const int row_bytes = fc_kvq4_row_bytes(head_dim);
const int win_live_lo = (cur_pos - t + (int)gridDim.x - 1) - FC_KVQ4_WMAX;
/* No K tile: q group + scores + probs + m/l/corr only. MUST match
* fc_attn_flash_kreg_shared_bytes() in cuda/fc_cuda.c. */
extern __shared__ float shk[];
float *q_s = shk;
float *score_s = q_s + (size_t)G * head_dim;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const size_t n_head = (size_t)gridDim.y * G;
const float *q_base = q + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const unsigned char *k_head = k_cache + (size_t)kvh * max_seq * row_bytes;
const unsigned char *v_head = v_cache + (size_t)kvh * max_seq * row_bytes;
const __half *k_win_head = k_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *v_win_head = v_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *k_sink_head = k_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const __half *v_sink_head = v_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES; /* key-in-tile this thread scores */
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = 0; j0 < seq_len; j0 += FC_ATTN_TILE_KEYS) {
int tk = seq_len - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
/* Load key ky's own 16 elements straight into registers — the same read
* path fc_attn_flash_prefill_q4's staging loop uses (fp16 shadow for
* sink/recent keys, q4 dequant for the aged-out middle), just assigned
* to the thread that consumes it. No shared write, so no sync. */
float kreg[FC_ATTN_KREG_MAX_CHUNK];
if (ky < tk) {
const int kp = j0 + ky;
if (kp < sink) {
const __half *s = k_sink_head + (size_t)kp * head_dim;
#pragma unroll
for (int i = 0; i < FC_ATTN_KREG_MAX_CHUNK; i++)
if (i < chunk)
kreg[i] = __half2float(s[lane + i * FC_ATTN_DOT_LANES]);
} else if (win > 0 && kp > cur_pos - win && kp > win_live_lo) {
const __half *s = k_win_head +
(size_t)(kp % FC_KVQ4_WMAX) * head_dim;
#pragma unroll
for (int i = 0; i < FC_ATTN_KREG_MAX_CHUNK; i++)
if (i < chunk)
kreg[i] = __half2float(s[lane + i * FC_ATTN_DOT_LANES]);
} else {
fc_kvq4_dequant_lane_chunk(k_head + (size_t)kp * row_bytes,
lane, chunk, kreg);
}
}
/* QK^T, i-outer / g-inner. Same accumulation order over e as
* fc_attn_flash_prefill_q4 — the K operand simply comes from a register
* instead of from k_s. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
#pragma unroll
for (int i = 0; i < FC_ATTN_KREG_MAX_CHUNK; i++) {
if (i < chunk) {
const int e = lane + i * FC_ATTN_DOT_LANES;
const float kve = kreg[i];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj;
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
/* PV accumulate — byte-for-byte the shipped loop, INCLUDING the rescale
* fused at j == 0 (see point 2 of the block comment). */
for (int j = 0; j < tk; j++) {
int vp = j0 + j;
float vv;
if (vp < sink)
vv = __half2float(v_sink_head[(size_t)vp * head_dim + tid]);
else if (win > 0 && vp > cur_pos - win && vp > win_live_lo)
vv = __half2float(v_win_head[(size_t)(vp % FC_KVQ4_WMAX) * head_dim + tid]);
else
vv = fc_kvq4_dequant_elem(v_head + (size_t)vp * row_bytes, tid);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
float *out_base = out + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
out_base[(size_t)g * head_dim + tid] = acc[g] / l_s[g];
}
/* ============================================================
* SHARED-V prefill variant (FC_ATTN_VSTAGE) — BIT-EXACT, DEFAULT OFF
*
* fc_attn_flash_prefill_q4_kr with one thing changed: the key tile's V rows are
* staged into shared memory AS RAW Q4 BYTES once per tile, and the per-key
* dequant reads shared instead of global.
*
* WHY, and it is the mirror image of the register-K finding. Register-K deleted
* the shared K tile because its reuse was 1:1 — one writer, and the only reader
* was the same thread. V's reuse is 32:1 and it had NO tile: a V row is
* row_bytes (144 at the shipped shape), and the block issued 256 threads x 3
* byte loads = 768 load instructions to read it, because all 32 threads of a
* Q4_0 block load the same 2-byte fp16 scale and each nibble byte serves 2
* threads. The staging pattern (none) did not match the consuming pattern
* (block-wide). So the same question that said "delete the tile" for K says
* "add one" for V.
*
* MEASURED, die 6 pinned 666 MHz, noise floor 0.301 % over 10 processes, three
* interleaved reps per arm, all bit-EXACT on 524 288 outputs:
*
* VIND (the address-arithmetic half alone, V still in global) 1.0418x
* THIS (VIND + the shared V tile) 1.1497x
* the shared-V half on its own 1.0959x
*
* DEPTH-FLAT: 1.1453 / 1.1455 / 1.1479 at 8 192 / 14 080 / 32 000 tokens.
*
* It is FREE in residency. Shared goes 9 312 -> 11 616 B and 7 blocks/SMX is
* unchanged because 7 is the REGISTER cap at 72 registers x 256 threads
* (floor(131072/(72*256)) = 7; 80 registers would give 6). Registers stay at
* 72 — COUNTED, and it was this lever's whole risk. Staging is placed in the
* SOFTMAX SHADOW: the online softmax is `if (tid < G)`, so 248 of 256 threads
* are idle there, and the copy is covered by the __syncthreads() that already
* separates the softmax from the PV loop. No barrier is added.
*
* WHY IT PAYS MORE THAN ITS INSTRUCTION COUNT: the per-key 64-bit global address
* chain (29 of the PV loop's 97 executed instructions, 15 of them loop-invariant
* and recomputed all 16 times) collapses to 3 instructions of 32-bit shared
* addressing, and the three LD.E.U8 become ONE LDS.U16 plus one LDS.U8 — in
* shared ptxas CAN prove the 2-byte alignment of the fp16 scale that it cannot
* prove for v_head + vp*row_bytes with a runtime row_bytes, so the PRMT that
* reassembled it disappears. But the counted instruction cut is only 15.7 % and
* the measured gain is 15.0 %, i.e. instruction reduction converts at ~27 % on
* this kernel: the rest is LATENCY. The old form exposed 16 serial global round
* trips per warp per tile at 3 loads in flight; this one exposes 3 at tile entry
* and reads shared thereafter.
*
* THREE THINGS ARE LOAD-BEARING AND MUST NOT BE "SIMPLIFIED":
*
* 1. Everything fc_attn_flash_prefill_q4_kr's own block comment says is still
* load-bearing here, because this kernel is that kernel: the HOISTED K
* dequant keeps registers at 72, and the online-softmax rescale stays FUSED
* in the PV loop at j == 0 where ptxas contracts it to
* acc = fma(acc, corr, p0*v0). Hoisting it is algebraically identical and
* moves 148 099 of 524 288 outputs by up to 4.0e-7 relative.
* 2. The three staging rounds are written OUT rather than looped under a
* #pragma unroll. A rolled runtime-bounded copy is the exact pathology the
* matvec staging gate priced at 1.426x for 1 -> 8 loads in flight, and a
* dropped unroll would compile it silently — nvcc emitted "this pragma must
* immediately precede a statement" for the looped version.
* 3. The dequant is INLINE, not a call to fc_kvq4_dequant_elem. Passing a
* shared pointer into a function taking a generic pointer lets ptxas fall
* back to generic LD instead of LDS. Inline, the SASS is LDS.U16 + LDS.U8 —
* verified in cuobjdump, which is the only reason the latency win exists.
*
* CONTRACT (enforced host-side in fc_attn_flash_prefill_q4_ctx):
* - everything fc_attn_flash_prefill_q4_kr requires, plus
* - row_bytes % 4 == 0, so the tile copies as 32-bit words. True for every
* head_dim that is a multiple of 64; head_dim is pinned to exactly 256 by
* the kr contract, giving row_bytes 144.
* - the staged tile must fit: shared <= 48 KB, checked by the host.
* ============================================================ */
/* Words in the staged V tile at the contracted shape, and the number of
* blockDim-strided rounds needed to copy it. head_dim is pinned to
* FC_ATTN_TILE_KEYS * FC_ATTN_DOT_LANES by the kr contract, and a block has
* head_dim threads, so both are compile-time constants. */
#define FC_ATTN_VS_ROW_BYTES ((FC_ATTN_TILE_KEYS * FC_ATTN_DOT_LANES / \
FC_KVQ4_BLK) * FC_KVQ4_BLK_BYTES)
#define FC_ATTN_VS_TILE_WORDS ((FC_ATTN_TILE_KEYS * FC_ATTN_VS_ROW_BYTES) / 4)
#define FC_ATTN_VS_THREADS (FC_ATTN_TILE_KEYS * FC_ATTN_DOT_LANES)
#define FC_ATTN_VS_ROUNDS ((FC_ATTN_VS_TILE_WORDS + FC_ATTN_VS_THREADS - 1) \
/ FC_ATTN_VS_THREADS)
#if FC_ATTN_VS_ROUNDS != 3
#error "fc_attn_flash_prefill_q4_kvs stages the V tile in exactly 3 rounds"
#endif
#if (FC_ATTN_TILE_KEYS * FC_ATTN_VS_ROW_BYTES) % 4 != 0
#error "the staged V tile must be a whole number of 32-bit words"
#endif
/* Falsification hook for the in-model digest, default OFF. See the use site. */
#ifndef FC_ATTN_VSTAGE_POISON
#define FC_ATTN_VSTAGE_POISON 0
#endif
/*
* Causal GQA flash attention over the Q4 GPU KV cache (prefill mode):
* fc_attn_flash_prefill_q4_kr with each key tile's V rows staged in shared
* memory as raw Q4 bytes and decoded from there. Bit-exact against
* fc_attn_flash_prefill_q4 and _kr; see the block comment above for why and for
* the contract.
*
* Args, grid and block are identical to fc_attn_flash_prefill_q4; dynamic
* shared is fc_attn_flash_kvs_shared_bytes in cuda/fc_cuda.c.
*/
__global__ void fc_attn_flash_prefill_q4_kvs(const float *q,
const unsigned char *k_cache,
const unsigned char *v_cache,
const __half *k_sink,
const __half *v_sink,
const __half *k_win,
const __half *v_win,
float *out, int kv_groups,
int head_dim, int max_seq, int pos0,
int sink, int win, int attn_sink,
int attn_win, float scale) {
const int t = blockIdx.x;
const int kvh = blockIdx.y;
const int tid = threadIdx.x;
const int G = kv_groups;
const int seq_len = pos0 + t + 1;
const int cur_pos = seq_len - 1;
const int row_bytes = fc_kvq4_row_bytes(head_dim);
/* Ring slots below the batch's highest appended position - WMAX are lapped. */
const int win_live_lo = (cur_pos - t + (int)gridDim.x - 1) - FC_KVQ4_WMAX;
/* kr's layout plus the staged V tile after corr_s. MUST match
* fc_attn_flash_kvs_shared_bytes() in cuda/fc_cuda.c. */
extern __shared__ float shv[];
float *q_s = shv;
float *score_s = q_s + (size_t)G * head_dim;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
unsigned int *v_sh = (unsigned int *)(corr_s + G);
const size_t n_head = (size_t)gridDim.y * G;
const float *q_base = q + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const unsigned char *k_head = k_cache + (size_t)kvh * max_seq * row_bytes;
const unsigned char *v_head = v_cache + (size_t)kvh * max_seq * row_bytes;
const __half *k_win_head = k_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *v_win_head = v_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *k_sink_head = k_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const __half *v_sink_head = v_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES; /* key-in-tile this thread scores */
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
/* Where element tid sits inside a staged V row; fixed per thread. */
const int vlocal = tid & (FC_KVQ4_BLK - 1);
const int vhalf = FC_KVQ4_BLK / 2;
const int vbyte = 2 + (vlocal < vhalf ? vlocal : vlocal - vhalf);
const int vshift = vlocal < vhalf ? 0 : 4;
const int vqoff = (tid / FC_KVQ4_BLK) * FC_KVQ4_BLK_BYTES;
for (int j0 = 0; j0 < seq_len; j0 += FC_ATTN_TILE_KEYS) {
int tk = seq_len - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
/* K for key ky straight into registers, as in _kr. */
float kreg[FC_ATTN_KREG_MAX_CHUNK];
if (ky < tk) {
const int kp = j0 + ky;
if (kp < sink) {
const __half *s = k_sink_head + (size_t)kp * head_dim;
#pragma unroll
for (int i = 0; i < FC_ATTN_KREG_MAX_CHUNK; i++)
if (i < chunk)
kreg[i] = __half2float(s[lane + i * FC_ATTN_DOT_LANES]);
} else if (win > 0 && kp > cur_pos - win && kp > win_live_lo) {
const __half *s = k_win_head +
(size_t)(kp % FC_KVQ4_WMAX) * head_dim;
#pragma unroll
for (int i = 0; i < FC_ATTN_KREG_MAX_CHUNK; i++)
if (i < chunk)
kreg[i] = __half2float(s[lane + i * FC_ATTN_DOT_LANES]);
} else {
fc_kvq4_dequant_lane_chunk(k_head + (size_t)kp * row_bytes,
lane, chunk, kreg);
}
}
/* QK^T, i-outer / g-inner; every thread joins the shuffles. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
#pragma unroll
for (int i = 0; i < FC_ATTN_KREG_MAX_CHUNK; i++) {
if (i < chunk) {
const int e = lane + i * FC_ATTN_DOT_LANES;
const float kve = kreg[i];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj;
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
/* Stage the tile's V rows as words while the softmax threads work; the
* barrier below covers both. Three rounds written out so all three
* loads are in flight before the first store. Every load is guarded: a
* partial last tile is shorter than one round and must not read past
* the end of v_cache. */
{
const unsigned int *vsrc =
(const unsigned int *)(v_head + (size_t)j0 * row_bytes);
const int vwords = (tk * row_bytes) >> 2;
const int w0_idx = tid;
const int w1_idx = tid + FC_ATTN_VS_THREADS;
const int w2_idx = tid + 2 * FC_ATTN_VS_THREADS;
const unsigned int w0 = w0_idx < vwords ? vsrc[w0_idx] : 0u;
const unsigned int w1 = w1_idx < vwords ? vsrc[w1_idx] : 0u;
const unsigned int w2 = w2_idx < vwords ? vsrc[w2_idx] : 0u;
if (w0_idx < vwords)
v_sh[w0_idx] = w0;
if (w1_idx < vwords)
v_sh[w1_idx] = w1;
if (w2_idx < vwords)
v_sh[w2_idx] = w2;
}
__syncthreads();
/* PV accumulate with the rescale fused at j == 0, as in _kr. */
for (int j = 0; j < tk; j++) {
int vp = j0 + j;
float vv;
if (vp < sink) {
vv = __half2float(v_sink_head[(size_t)vp * head_dim + tid]);
} else if (win > 0 && vp > cur_pos - win && vp > win_live_lo) {
vv = __half2float(v_win_head[(size_t)(vp % FC_KVQ4_WMAX) * head_dim + tid]);
} else {
/* Inline on purpose: through a generic-pointer helper ptxas
* loses the shared address space and emits generic loads. */
const unsigned char *bp =
(const unsigned char *)v_sh + j * row_bytes + vqoff;
const unsigned short d_bits = (unsigned short)(bp[0] | (bp[1] << 8));
const float d = __half2float(__ushort_as_half(d_bits));
const int code = (bp[vbyte] >> vshift) & 0xF;
vv = (float)(code - 8) * d;
#if FC_ATTN_VSTAGE_POISON
vv = __fmul_rn(vv, 1.00000011920928955078125f);
#endif
}
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
float *out_base = out + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
out_base[(size_t)g * head_dim + tid] = acc[g] / l_s[g];
}
/* ============================================================
* NARROW-STAGING prefill variants (FC_ATTN_KTILE_FP16) — LOSSY, DEFAULT OFF
*
* Same tiling, same online softmax and the same q4 KV cache as
* fc_attn_flash_prefill_q4. ONE thing changes now:
*
* 1. The i-outer QK dot with eight REGISTER partials — which used to be a
* difference here — is on the DEFAULT path since the lossless landing, so
* all six shipped flash kernels carry it. It is what makes (2) affordable
* (a narrow tile costs one widening per K LOAD, and this cuts loads 8x),
* but it is no longer part of what this flag turns on.
* 2. The staged K tile (and, at level 2, the staged q group) is __half
* instead of float. That is the whole point: the K tile is 17 408 B of
* the 26 720 B footprint, and residency on this kernel is capped by
* SHARED MEMORY, never by registers (72 regs would allow 7 blocks/SMX).
* 26 720 -> 18 016 B lifts residency 4 -> 6 blocks/SMX; also narrowing q
* gives 13 920 B and 7 blocks/SMX.
*
* NUMERICS — this is NOT bit-exact, which is why it is behind a flag:
* - sink and recent-window keys are ALREADY fp16 in memory (k_sink/k_win);
* staging them as __half is bit-exact for those keys and today's widening
* to float is pure waste.
* - aged-out keys are q4-sourced (fp16 scale x a 4-bit level), so rounding
* them to fp16 costs <= 2^-11 relative — orders below the q4 error the
* cache already carries.
* - level 2 additionally rounds the q group, a live activation. Strictly
* lossier and gated separately.
* Measured max relative output error at the shipped shape: 1.3e-5 (level 1),
* 1.7e-5 (level 2). See Pantheon results/prefill_attention_ktile_fp16.md.
*
* TODO: attention.cu is past the 1 000-line guidance and now carries THREE
* prefill families (exact, narrow-staged, register-K) plus the whole decode
* family. Splitting it into a prefill file and a decode file is still the right
* move, and it needs a SASS-identity check as its gate: extracting code out of a
* flash body is NOT free here even behind __forceinline__. Pulling the
* register-K path's three-way K load into one helper was tried and measured to
* CHANGE that kernel's SASS (same 72 registers, same 0-byte stack frame,
* different instruction stream), so it was reverted rather than shipped on top
* of a measurement it would have invalidated. Same reason the register-K body is
* a copy of fc_attn_flash_prefill_q4's rather than a shared template.
* See Pantheon results/prefill_attention_register_ktile.md §8 gap 14.
* ============================================================ */
/* K-tile row stride in ELEMENTS is FC_ATTN_K_STRIDE(head_dim) for both the
* float and the __half tile: two __halfs inside one 32-bit word are
* conflict-free on sm_37 (the sub-word rule), and the +16-element pad still
* puts the two half-warps of a warp on disjoint bank ranges.
*
* The shared body is a TEMPLATE, which cannot carry C linkage, so the file's
* enclosing `extern "C"` block closes here and reopens for the two entry
* points — whose PTX names must stay unmangled, since cuModuleGetFunction
* looks them up by string. */
} /* extern "C" */
template <bool Q_FP16>
__device__ __forceinline__ void fc_attn_flash_prefill_q4_narrow(
const float *q, const unsigned char *k_cache,
const unsigned char *v_cache, const __half *k_sink,
const __half *v_sink, const __half *k_win, const __half *v_win,
float *out, int kv_groups, int head_dim, int max_seq, int pos0,
int sink, int win, int attn_sink, int attn_win, float scale)
{
const int t = blockIdx.x;
const int kvh = blockIdx.y;
const int tid = threadIdx.x;
const int G = kv_groups;
const int seq_len = pos0 + t + 1;
const int cur_pos = seq_len - 1;
const int row_bytes = fc_kvq4_row_bytes(head_dim);
const int win_live_lo = (cur_pos - t + (int)gridDim.x - 1) - FC_KVQ4_WMAX;
/* Byte-addressed shared layout so the fp32 softmax state stays 4-byte
* aligned whichever tiles are narrowed. MUST match
* fc_attn_flash_narrow_shared_bytes() in cuda/fc_cuda.c. */
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ char shb[];
const int q_bytes = (int)(G * head_dim *
(Q_FP16 ? sizeof(__half) : sizeof(float)));
const int k_bytes = (int)(FC_ATTN_TILE_KEYS * kstride * sizeof(__half));
float *q_f = (float *)shb;
__half *q_h = (__half *)shb;
__half *k_h = (__half *)(shb + q_bytes);
float *score_s = (float *)(shb + q_bytes + k_bytes);
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const size_t n_head = (size_t)gridDim.y * G;
const float *q_base = q + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
for (int g = 0; g < G; g++) {
float qv = q_base[(size_t)g * head_dim + tid];
if (Q_FP16) q_h[(size_t)g * head_dim + tid] = __float2half(qv);
else q_f[(size_t)g * head_dim + tid] = qv;
}
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const unsigned char *k_head = k_cache + (size_t)kvh * max_seq * row_bytes;
const unsigned char *v_head = v_cache + (size_t)kvh * max_seq * row_bytes;
const __half *k_win_head = k_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *v_win_head = v_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *k_sink_head = k_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const __half *v_sink_head = v_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES;
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = 0; j0 < seq_len; j0 += FC_ATTN_TILE_KEYS) {
int tk = seq_len - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
/* Stage the K tile as __half. The fp16 shadow rows copy across with no
* conversion at all (they were fp16 to begin with); only the q4 body
* rounds. */
for (int key = 0; key < tk; key++) {
int kp = j0 + key;
const size_t base = (size_t)key * kstride;
if (kp < sink) {
const __half *s = k_sink_head + (size_t)kp * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
k_h[base + e] = s[e];
} else if (win > 0 && kp > cur_pos - win && kp > win_live_lo) {
const __half *s = k_win_head +
(size_t)(kp % FC_KVQ4_WMAX) * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
k_h[base + e] = s[e];
} else {
const unsigned char *s = k_head + (size_t)kp * row_bytes;
for (int e = tid; e < head_dim; e += blockDim.x)
k_h[base + e] = __float2half(fc_kvq4_dequant_elem(s, e));
}
}
__syncthreads();
/* QK^T, i-outer: one K load (and one widening) per element per tile,
* eight register partials across the GQA group. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const size_t kb = (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kv = __half2float(k_h[kb + e]);
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G) {
size_t qi = (size_t)g * head_dim + e;
float qv = Q_FP16 ? __half2float(q_h[qi]) : q_f[qi];
part[g] += qv * kv;
}
}
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj;
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
/* PV accumulate — unchanged from fc_attn_flash_prefill_q4: V streams
* through __ldg and is never staged, so it is untouched by all this. */
for (int j = 0; j < tk; j++) {
int vp = j0 + j;
float vv;
if (vp < sink)
vv = __half2float(v_sink_head[(size_t)vp * head_dim + tid]);
else if (win > 0 && vp > cur_pos - win && vp > win_live_lo)
vv = __half2float(v_win_head[(size_t)(vp % FC_KVQ4_WMAX) * head_dim + tid]);
else
vv = fc_kvq4_dequant_elem(v_head + (size_t)vp * row_bytes, tid);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
float *out_base = out + ((size_t)t * n_head + (size_t)kvh * G) * head_dim;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
out_base[(size_t)g * head_dim + tid] = acc[g] / l_s[g];
}
extern "C" {
/* FC_ATTN_KTILE_FP16=1: __half K tile, fp32 q group. 18 016 B at the shipped
* shape -> 6 blocks/SMX. */
__global__ void fc_attn_flash_prefill_q4_kh(
const float *q, const unsigned char *k_cache,
const unsigned char *v_cache, const __half *k_sink,
const __half *v_sink, const __half *k_win, const __half *v_win,
float *out, int kv_groups, int head_dim, int max_seq, int pos0,
int sink, int win, int attn_sink, int attn_win, float scale)
{
fc_attn_flash_prefill_q4_narrow<false>(
q, k_cache, v_cache, k_sink, v_sink, k_win, v_win, out, kv_groups,
head_dim, max_seq, pos0, sink, win, attn_sink, attn_win, scale);
}
/* FC_ATTN_KTILE_FP16=2: __half K tile AND __half q group. 13 920 B ->
* 7 blocks/SMX. Strictly lossier than level 1 — q is an activation. */
__global__ void fc_attn_flash_prefill_q4_kqh(
const float *q, const unsigned char *k_cache,
const unsigned char *v_cache, const __half *k_sink,
const __half *v_sink, const __half *k_win, const __half *v_win,
float *out, int kv_groups, int head_dim, int max_seq, int pos0,
int sink, int win, int attn_sink, int attn_win, float scale)
{
fc_attn_flash_prefill_q4_narrow<true>(
q, k_cache, v_cache, k_sink, v_sink, k_win, v_win, out, kv_groups,
head_dim, max_seq, pos0, sink, win, attn_sink, attn_win, scale);
}
/* ============================================================
* Decode-mode flash attention (QWEN35_FLASH_DECODE)
*
* A single query (the new token at absolute position seq_len-1) attends the
* full cached context [0, seq_len). Reusing fc_attn_flash_prefill with npos=1
* launches only n_head_kv blocks (=2 for this model) — the K80's ~13 SMX sit
* idle. So the KV sequence is SPLIT across blocks: grid (n_splits, n_head_kv),
* each block runs the same tiled online-softmax over its key slice for the
* whole GQA group and writes a PARTIAL {running max m, running sum l,
* UNnormalized accumulator acc} to scratch. fc_attn_flash_decode_combine then
* merges the n_splits partials per head with the standard max-rescale, which
* is algebraically identical to a single-pass softmax (only ~1e-6 tile drift,
* same class as prefill; gate: argmax-exact vs the host decode path).
*
* Partial scratch layout, indexed by GLOBAL head qh = kvh*G + g:
* m_out[qh*n_splits + split], l_out[qh*n_splits + split]
* acc_out[(qh*n_splits + split)*head_dim + dim]
* ============================================================ */
/* Split plan from the context length: ~kps keys per split, capped at `cap`
* splits, split_len rounded up to the tile width. MUST stay formula-identical
* to the host plan in qwen35moe_forward_cuda.c gpu_attn_block_decode_flash —
* the *_dev kernels below recompute the plan on-device (from a device-resident
* position) so a captured CUDA graph replays correctly at every position, and
* the recomputed plan must equal the host plan for the same seq_len or the
* graphed and direct paths stop being bit-identical. */
__device__ __forceinline__ int fc_attn_dec_plan(int seq_len, int kps, int cap,
int *split_len_out) {
int n_splits = (seq_len + kps - 1) / kps;
if (n_splits < 1)
n_splits = 1;
if (n_splits > cap)
n_splits = cap;
int split_len = (seq_len + n_splits - 1) / n_splits;
split_len = (split_len + 15) & ~15;
*split_len_out = split_len;
return (seq_len + split_len - 1) / split_len;
}
/* Merge the n_splits per-head partials into the final attention output.
* One block per head; thread tid owns output dim tid. Shared verbatim by the
* host-arg and device-pos combine kernels. */
__device__ __forceinline__ void fc_attn_flash_decode_combine_body(
const float *m_in, const float *l_in, const float *acc_in, float *out,
int head_dim, int n_splits) {
const int head = blockIdx.x;
const int tid = threadIdx.x;
if (tid >= head_dim) return;
const float *m_h = m_in + (size_t)head * n_splits;
const float *l_h = l_in + (size_t)head * n_splits;
const float *acc_h = acc_in + (size_t)head * n_splits * head_dim;
float M = -__int_as_float(0x7f800000);
for (int s = 0; s < n_splits; s++)
M = fmaxf(M, m_h[s]);
float o = 0.0f, L = 0.0f;
for (int s = 0; s < n_splits; s++) {
float w = expf(m_h[s] - M); /* empty split: m=-inf -> w=0 */
L += l_h[s] * w;
o += acc_h[(size_t)s * head_dim + tid] * w;
}
out[(size_t)head * head_dim + tid] = o / L;
}
/* Merge the n_splits per-head partials into the final attention output.
* Grid (n_head), block head_dim threads (thread tid owns output dim tid). */
__global__ void fc_attn_flash_decode_combine(const float *m_in,
const float *l_in,
const float *acc_in, float *out,
int head_dim, int n_splits) {
fc_attn_flash_decode_combine_body(m_in, l_in, acc_in, out, head_dim,
n_splits);
}
/* Device-position combine (CUDA-graph capture): seq_len = pos[0] + 1 is read
* from device memory and the split plan recomputed with fc_attn_dec_plan, so
* the launch itself is position-invariant. Bit-exact to
* fc_attn_flash_decode_combine at the same position. */
__global__ void fc_attn_flash_decode_combine_dev(const float *m_in,
const float *l_in,
const float *acc_in,
float *out, int head_dim,
const int *pos, int kps,
int splits_cap) {
int seq_len = pos[0] + 1;
int split_len;
int n_splits = fc_attn_dec_plan(seq_len, kps, splits_cap, &split_len);
fc_attn_flash_decode_combine_body(m_in, l_in, acc_in, out, head_dim,
n_splits);
}
/*
* Decode split-attention over the fp32 GPU KV cache. Same online-softmax math
* and tiling as fc_attn_flash_prefill, but the query is the single decode
* position and the key loop is restricted to this block's split slice
* [split*split_len, min(seq_len, (split+1)*split_len)). Writes the split's
* partial {m, l, acc} for every head in the group (see the section header).
*
* q: [n_head][head_dim] fp32 (post QK-norm + RoPE), single position.
* Grid (n_splits, n_head_kv), block head_dim threads (multiple of 16),
* dynamic shared identical to fc_attn_flash_prefill.
*/
__device__ __forceinline__ void fc_attn_flash_decode_body(
const float *q, const float *k_cache, const float *v_cache,
float *m_out, float *l_out, float *acc_out, int kv_groups,
int head_dim, int max_seq, int seq_len, int n_splits, int split_len,
int attn_sink, int attn_win, float scale) {
const int split = blockIdx.x;
const int kvh = blockIdx.y;
const int tid = threadIdx.x;
const int G = kv_groups;
const int cur_pos = seq_len - 1; /* decode query's absolute position */
int k_lo = split * split_len;
int k_hi = k_lo + split_len;
if (k_hi > seq_len) k_hi = seq_len;
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ float sh[];
float *q_s = sh;
float *k_s = q_s + (size_t)G * head_dim;
float *score_s = k_s + (size_t)FC_ATTN_TILE_KEYS * kstride;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const float *q_base = q + (size_t)kvh * G * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
/* Compile-time width, so ptxas keeps acc[] in REGISTERS. Every loop that
* indexes acc[] is unrolled to FC_ATTN_MAX_GROUPS with an `if (g < G)`
* guard; a loop bounded by the RUNTIME kv_groups cannot be unrolled, the
* array lands in LOCAL memory, and the PV inner loop then pays an
* LDL.128/STL.128 round trip per key. ptxas reports that as a 32-byte
* "stack frame" and prints "0 bytes spill stores" right beside it --
* a never-promoted local array is not counted as a spill. */
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const float *k_head = k_cache + (size_t)kvh * max_seq * head_dim;
const float *v_head = v_cache + (size_t)kvh * max_seq * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES;
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = k_lo; j0 < k_hi; j0 += FC_ATTN_TILE_KEYS) {
int tk = k_hi - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
/* Sliding window: skip tiles outside sink U recent-window. */
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
for (int key = 0; key < tk; key++)
for (int e = tid; e < head_dim; e += blockDim.x)
k_s[(size_t)key * kstride + e] =
k_head[(size_t)(j0 + key) * head_dim + e];
__syncthreads();
/* QK^T, i-outer / g-inner: each thread reads its chunk of the staged K
* row ONCE per tile into a register and fans it across the whole GQA
* group, instead of re-reading k_s once per group (G=8 -> 1 LDS per
* element). BIT-EXACT: every part[g] still accumulates over e in the
* same ascending order the g-outer form used, so this is pure operand
* motion. part[] is indexed only by the unrolled compile-time g, for
* the same reason acc[] is -- a runtime-bounded loop would push it to
* LOCAL memory and cost an LDL/STL round trip per element. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const float *kv = k_s + (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kve = kv[e];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
/* G is uniform across the block, so the guarded unroll keeps the
* shuffle warp-uniform exactly as the runtime-bounded loop did. */
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj; /* [TILE][G]: PV reads a run */
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
for (int j = 0; j < tk; j++) {
float vv = __ldg(&v_head[(size_t)(j0 + j) * head_dim + tid]);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
size_t po = ((size_t)kvh * G + g) * n_splits + split;
acc_out[po * head_dim + tid] = acc[g];
if (tid == 0) {
m_out[po] = m_s[g];
l_out[po] = l_s[g];
}
}
}
}
__global__ void fc_attn_flash_decode(const float *q, const float *k_cache,
const float *v_cache, float *m_out,
float *l_out, float *acc_out,
int kv_groups, int head_dim, int max_seq,
int seq_len, int n_splits, int split_len,
int attn_sink, int attn_win, float scale) {
fc_attn_flash_decode_body(q, k_cache, v_cache, m_out, l_out, acc_out,
kv_groups, head_dim, max_seq, seq_len, n_splits,
split_len, attn_sink, attn_win, scale);
}
/*
* Device-position decode split-attention (CUDA-graph capture): the query's
* position is read from device memory (pos[0], seq_len = pos[0] + 1) and the
* split plan recomputed on-device, so every launch argument and the grid are
* position-invariant — capture once, replay every token. Launched with a
* FIXED grid (splits_cap, n_head_kv); blocks past the live plan exit before
* touching scratch (the partial layout is strided by the LIVE n_splits, same
* as the host-arg kernel, so combine sees an identical layout). Bit-exact to
* fc_attn_flash_decode at the same position. Caller guarantees no ring wrap
* (pos + 1 <= max_seq).
*/
__global__ void fc_attn_flash_decode_dev(const float *q, const float *k_cache,
const float *v_cache, float *m_out,
float *l_out, float *acc_out,
int kv_groups, int head_dim,
int max_seq, const int *pos, int kps,
int splits_cap, int attn_sink,
int attn_win, float scale) {
int seq_len = pos[0] + 1;
int split_len;
int n_splits = fc_attn_dec_plan(seq_len, kps, splits_cap, &split_len);
if ((int)blockIdx.x >= n_splits)
return;
fc_attn_flash_decode_body(q, k_cache, v_cache, m_out, l_out, acc_out,
kv_groups, head_dim, max_seq, seq_len, n_splits,
split_len, attn_sink, attn_win, scale);
}
/*
* Decode split-attention over the Q4 GPU KV cache. Identical to
* fc_attn_flash_decode but K/V are read from the packed Q4_0 cache (with the
* fp16 sink / recent-window shadow) and dequantized in-kernel, matching
* fc_attn_flash_prefill_q4. Writes the same partial layout; the combine
* kernel is shared.
*/
__device__ __forceinline__ void fc_attn_flash_decode_q4_body(
const float *q, const unsigned char *k_cache,
const unsigned char *v_cache, const __half *k_sink,
const __half *v_sink, const __half *k_win, const __half *v_win,
float *m_out, float *l_out, float *acc_out, int kv_groups,
int head_dim, int max_seq, int seq_len, int n_splits, int split_len,
int sink, int win, int attn_sink, int attn_win, float scale) {
const int split = blockIdx.x;
const int kvh = blockIdx.y;
const int tid = threadIdx.x;
const int G = kv_groups;
const int cur_pos = seq_len - 1;
const int row_bytes = fc_kvq4_row_bytes(head_dim);
int k_lo = split * split_len;
int k_hi = k_lo + split_len;
if (k_hi > seq_len) k_hi = seq_len;
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ float sh[];
float *q_s = sh;
float *k_s = q_s + (size_t)G * head_dim;
float *score_s = k_s + (size_t)FC_ATTN_TILE_KEYS * kstride;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const float *q_base = q + (size_t)kvh * G * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
/* Compile-time width, so ptxas keeps acc[] in REGISTERS. Every loop that
* indexes acc[] is unrolled to FC_ATTN_MAX_GROUPS with an `if (g < G)`
* guard; a loop bounded by the RUNTIME kv_groups cannot be unrolled, the
* array lands in LOCAL memory, and the PV inner loop then pays an
* LDL.128/STL.128 round trip per key. ptxas reports that as a 32-byte
* "stack frame" and prints "0 bytes spill stores" right beside it --
* a never-promoted local array is not counted as a spill. */
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const unsigned char *k_head = k_cache + (size_t)kvh * max_seq * row_bytes;
const unsigned char *v_head = v_cache + (size_t)kvh * max_seq * row_bytes;
const __half *k_win_head = k_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *v_win_head = v_win + (size_t)kvh * FC_KVQ4_WMAX * head_dim;
const __half *k_sink_head = k_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const __half *v_sink_head = v_sink + (size_t)kvh * FC_KVQ4_SINKMAX * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES;
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = k_lo; j0 < k_hi; j0 += FC_ATTN_TILE_KEYS) {
int tk = k_hi - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
/* Sliding window: skip tiles outside sink U recent-window. */
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
for (int key = 0; key < tk; key++) {
int kp = j0 + key;
float *krow = k_s + (size_t)key * kstride;
if (kp < sink) {
const __half *s = k_sink_head + (size_t)kp * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = __half2float(s[e]);
} else if (win > 0 && kp > cur_pos - win) {
const __half *s = k_win_head +
(size_t)(kp % FC_KVQ4_WMAX) * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = __half2float(s[e]);
} else {
const unsigned char *s = k_head + (size_t)kp * row_bytes;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = fc_kvq4_dequant_elem(s, e);
}
}
__syncthreads();
/* QK^T, i-outer / g-inner: each thread reads its chunk of the staged K
* row ONCE per tile into a register and fans it across the whole GQA
* group, instead of re-reading k_s once per group (G=8 -> 1 LDS per
* element). BIT-EXACT: every part[g] still accumulates over e in the
* same ascending order the g-outer form used, so this is pure operand
* motion. part[] is indexed only by the unrolled compile-time g, for
* the same reason acc[] is -- a runtime-bounded loop would push it to
* LOCAL memory and cost an LDL/STL round trip per element. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const float *kv = k_s + (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kve = kv[e];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
/* G is uniform across the block, so the guarded unroll keeps the
* shuffle warp-uniform exactly as the runtime-bounded loop did. */
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj; /* [TILE][G]: PV reads a run */
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
for (int j = 0; j < tk; j++) {
int vp = j0 + j;
float vv;
if (vp < sink)
vv = __half2float(v_sink_head[(size_t)vp * head_dim + tid]);
else if (win > 0 && vp > cur_pos - win)
vv = __half2float(v_win_head[(size_t)(vp % FC_KVQ4_WMAX) * head_dim + tid]);
else
vv = fc_kvq4_dequant_elem(v_head + (size_t)vp * row_bytes, tid);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
size_t po = ((size_t)kvh * G + g) * n_splits + split;
acc_out[po * head_dim + tid] = acc[g];
if (tid == 0) {
m_out[po] = m_s[g];
l_out[po] = l_s[g];
}
}
}
}
__global__ void fc_attn_flash_decode_q4(const float *q,
const unsigned char *k_cache,
const unsigned char *v_cache,
const __half *k_sink,
const __half *v_sink,
const __half *k_win,
const __half *v_win, float *m_out,
float *l_out, float *acc_out,
int kv_groups, int head_dim,
int max_seq, int seq_len, int n_splits,
int split_len, int sink, int win,
int attn_sink, int attn_win,
float scale) {
fc_attn_flash_decode_q4_body(q, k_cache, v_cache, k_sink, v_sink, k_win,
v_win, m_out, l_out, acc_out, kv_groups,
head_dim, max_seq, seq_len, n_splits,
split_len, sink, win, attn_sink, attn_win,
scale);
}
/* Device-position Q4 decode split-attention (CUDA-graph capture). Same
* contract as fc_attn_flash_decode_dev; bit-exact to fc_attn_flash_decode_q4
* at the same position. */
__global__ void fc_attn_flash_decode_q4_dev(const float *q,
const unsigned char *k_cache,
const unsigned char *v_cache,
const __half *k_sink,
const __half *v_sink,
const __half *k_win,
const __half *v_win, float *m_out,
float *l_out, float *acc_out,
int kv_groups, int head_dim,
int max_seq, const int *pos,
int kps, int splits_cap, int sink,
int win, int attn_sink,
int attn_win, float scale) {
int seq_len = pos[0] + 1;
int split_len;
int n_splits = fc_attn_dec_plan(seq_len, kps, splits_cap, &split_len);
if ((int)blockIdx.x >= n_splits)
return;
fc_attn_flash_decode_q4_body(q, k_cache, v_cache, k_sink, v_sink, k_win,
v_win, m_out, l_out, acc_out, kv_groups,
head_dim, max_seq, seq_len, n_splits,
split_len, sink, win, attn_sink, attn_win,
scale);
}
/*
* Device-position single-row KV append (CUDA-graph capture): the decode
* token's ring slot is computed from device-resident pos[0] instead of a host
* scalar, so the recorded launch replays correctly at every position. Grid
* (1, n_head_kv) — decode appends exactly one row. Bit-exact to
* fc_attn_kv_append with pos0 = pos[0], npos = 1.
*/
__global__ void fc_attn_kv_append_dev(const float *k_new, const float *v_new,
float *k_cache, float *v_cache,
int n_head_kv, int head_dim, int max_seq,
const int *pos) {
int t = blockIdx.x;
int h = blockIdx.y;
int dd = threadIdx.x;
if (dd >= head_dim) return;
int cache_pos = (pos[0] + t) % max_seq;
size_t src = ((size_t)t * n_head_kv + h) * head_dim + dd;
size_t dst = ((size_t)h * max_seq + cache_pos) * head_dim + dd;
k_cache[dst] = k_new[src];
v_cache[dst] = v_new[src];
}
/* ============================================================
* Multi-user decode attention (QWEN35_FLASH_SLOTS)
*
* The batched-decode "slots" path: K independent users, each decoding ONE
* token at its OWN position against its OWN KV ring. The slot kernels below
* move the whole per-step attention on-die — replacing the host per-user
* softmax loop and its 2 syncs + PCIe round-trips per layer (the K=8
* steady-state ceiling, multi-tenant-speed-opt.md §3.2).
*
* KV layout: one ring PER SLOT, each slice byte-identical to the single-user
* ring — [slot][n_head_kv][max_seq][row] (row = head_dim fp32 or Q4_0
* bytes + per-slot fp16 sink/window shadows).
*
* Split plan: every user recomputes its OWN split plan from its position
* (fc_attn_dec_plan(seq_len_u), the exact single-user formula), so each
* user's per-split (m, l, acc) partials are bit-identical to the
* single-token decode kernel at the same position. The launch is uniform
* (grid.x = the max plan across users; partial stride = gridDim.x); blocks
* past a user's own plan write EMPTY partials (m=-inf, l=0, acc=0), which
* the combine's max-rescale weights to exactly zero — merging them adds
* only +0.0f terms, so the merged output is bit-identical to combining the
* live splits alone. Combine reuses fc_attn_flash_decode_combine with
* gridDim.x = nslots * n_head (partials/out are (slot, head)-major).
* ============================================================ */
/*
* Append each slot's single roped K/V row to its own fp32 ring at its own
* position (pos[slot], device int32 array — the RoPE position buffer).
*
* k_new, v_new: [nslots][n_head_kv][head_dim] fp32
* k_cache, v_cache: [nslots][n_head_kv][max_seq][head_dim] fp32 (written)
*
* Grid (nslots, n_head_kv), block head_dim threads. Row lands at
* pos[slot] % max_seq (the host ring rule).
*/
__global__ void fc_attn_kv_append_slots(const float *k_new, const float *v_new,
float *k_cache, float *v_cache,
int n_head_kv, int head_dim,
int max_seq, const int *pos) {
int u = blockIdx.x; /* slot */
int h = blockIdx.y;
int dd = threadIdx.x;
if (dd >= head_dim) return;
int cache_pos = pos[u] % max_seq;
size_t src = ((size_t)u * n_head_kv + h) * head_dim + dd;
size_t dst = (((size_t)u * n_head_kv + h) * max_seq + cache_pos) *
head_dim + dd;
k_cache[dst] = k_new[src];
v_cache[dst] = v_new[src];
}
/* Per-slot Q4 ring append (quantizing in-kernel) + per-slot fp16 sink /
* recent-window shadow mirror. Same contract as fc_attn_kv_append_q4 per
* slot; caller guarantees no ring wrap (pos[u] + 1 <= max_seq).
*
* k_cache/v_cache: [nslots][n_head_kv][max_seq][row_bytes] Q4_0 bytes
* k_sink/v_sink: [nslots][n_head_kv][FC_KVQ4_SINKMAX][head_dim] fp16
* k_win/v_win: [nslots][n_head_kv][FC_KVQ4_WMAX][head_dim] fp16
*
* Grid (nslots, n_head_kv), block head_dim threads (multiple of 32). */
__global__ void fc_attn_kv_append_slots_q4(const float *k_new,
const float *v_new,
unsigned char *k_cache,
unsigned char *v_cache,
__half *k_sink, __half *v_sink,
__half *k_win, __half *v_win,
int n_head_kv, int head_dim,
int max_seq, const int *pos,
int sink, int win) {
int u = blockIdx.x; /* slot */
int h = blockIdx.y;
int tid = threadIdx.x;
if (tid >= head_dim) return;
int cache_pos = pos[u];
int row_bytes = fc_kvq4_row_bytes(head_dim);
size_t src = ((size_t)u * n_head_kv + h) * head_dim + tid;
size_t dst_row = (((size_t)u * n_head_kv + h) * max_seq + cache_pos) *
row_bytes;
int blk = tid / FC_KVQ4_BLK;
int lane = tid & (FC_KVQ4_BLK - 1);
/* Shared staging for the shuffle-free block quantizer (Kepler-safe). */
__shared__ float s_k[FC_KVQ4_MAX_HEAD_DIM];
__shared__ float s_v[FC_KVQ4_MAX_HEAD_DIM];
float kf = k_new[src];
float vf = v_new[src];
s_k[tid] = kf;
s_v[tid] = vf;
__syncthreads();
fc_kvq4_quant_block(s_k + (size_t)blk * FC_KVQ4_BLK,
k_cache + dst_row + (size_t)blk * FC_KVQ4_BLK_BYTES,
lane);
fc_kvq4_quant_block(s_v + (size_t)blk * FC_KVQ4_BLK,
v_cache + dst_row + (size_t)blk * FC_KVQ4_BLK_BYTES,
lane);
/* Mirror the row into this slot's fp16 sink / recent-window shadow. */
if (win > 0) {
size_t wi = (((size_t)u * n_head_kv + h) * FC_KVQ4_WMAX +
(cache_pos % FC_KVQ4_WMAX)) * head_dim + tid;
k_win[wi] = __float2half(kf);
v_win[wi] = __float2half(vf);
}
if (cache_pos < sink) {
size_t si = (((size_t)u * n_head_kv + h) * FC_KVQ4_SINKMAX +
cache_pos) * head_dim + tid;
k_sink[si] = __float2half(kf);
v_sink[si] = __float2half(vf);
}
}
/*
* Multi-user decode split-attention over the fp32 per-slot KV rings. Each
* block runs one (split, kv-head, slot) slice of one user's decode attention
* with the user's OWN split plan recomputed on-device from pos[slot] — the
* per-split math is bit-identical to fc_attn_flash_decode at that position.
*
* q: [nslots][n_head][head_dim] fp32 (post QK-norm + RoPE), one row/slot.
* Partials indexed by GLOBAL head gh = slot*n_head + kvh*G + g, strided by
* the LAUNCH width gridDim.x:
* m_out[gh*gridDim.x + split], l_out[...], acc_out[(...)*head_dim + dim]
* Off-plan blocks (split >= this user's n_splits) write EMPTY partials.
*
* Grid (launch_splits, n_head_kv, nslots), block head_dim threads, dynamic
* shared identical to fc_attn_flash_prefill. Caller guarantees no ring wrap
* (pos[u] + 1 <= max_seq for every slot).
*/
__global__ void fc_attn_flash_decode_slots(const float *q,
const float *k_cache,
const float *v_cache, float *m_out,
float *l_out, float *acc_out,
int kv_groups, int head_dim,
int max_seq, const int *pos,
int kps, int splits_cap,
int attn_sink, int attn_win,
float scale) {
const int split = blockIdx.x;
const int kvh = blockIdx.y;
const int slot = blockIdx.z;
const int tid = threadIdx.x;
const int G = kv_groups;
const int n_head_kv = gridDim.y;
const int stride = gridDim.x; /* uniform partial stride */
const int seq_len = pos[slot] + 1;
const int cur_pos = seq_len - 1;
/* This user's own split plan (the single-user formula). */
int split_len;
int n_splits = fc_attn_dec_plan(seq_len, kps, splits_cap, &split_len);
/* Partial base: global head (slot, kvh, g=0), strided by the launch. */
const size_t head0 = ((size_t)slot * n_head_kv + kvh) * G;
if (split >= n_splits) {
/* Off-plan for this (shorter-context) user: EMPTY partial. The
* combine weights m=-inf splits to exactly 0 (see section header). */
for (int g = 0; g < G; g++) {
size_t po = (head0 + g) * stride + split;
acc_out[po * head_dim + tid] = 0.0f;
if (tid == 0) {
m_out[po] = -__int_as_float(0x7f800000);
l_out[po] = 0.0f;
}
}
return;
}
int k_lo = split * split_len;
int k_hi = k_lo + split_len;
if (k_hi > seq_len) k_hi = seq_len;
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ float sh[];
float *q_s = sh;
float *k_s = q_s + (size_t)G * head_dim;
float *score_s = k_s + (size_t)FC_ATTN_TILE_KEYS * kstride;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const float *q_base = q + head0 * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
/* Compile-time width, so ptxas keeps acc[] in REGISTERS. Every loop that
* indexes acc[] is unrolled to FC_ATTN_MAX_GROUPS with an `if (g < G)`
* guard; a loop bounded by the RUNTIME kv_groups cannot be unrolled, the
* array lands in LOCAL memory, and the PV inner loop then pays an
* LDL.128/STL.128 round trip per key. ptxas reports that as a 32-byte
* "stack frame" and prints "0 bytes spill stores" right beside it --
* a never-promoted local array is not counted as a spill. */
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const float *k_head = k_cache +
((size_t)slot * n_head_kv + kvh) * max_seq * head_dim;
const float *v_head = v_cache +
((size_t)slot * n_head_kv + kvh) * max_seq * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES;
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = k_lo; j0 < k_hi; j0 += FC_ATTN_TILE_KEYS) {
int tk = k_hi - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
/* Sliding window: skip tiles outside sink U recent-window. */
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
for (int key = 0; key < tk; key++)
for (int e = tid; e < head_dim; e += blockDim.x)
k_s[(size_t)key * kstride + e] =
k_head[(size_t)(j0 + key) * head_dim + e];
__syncthreads();
/* QK^T, i-outer / g-inner: each thread reads its chunk of the staged K
* row ONCE per tile into a register and fans it across the whole GQA
* group, instead of re-reading k_s once per group (G=8 -> 1 LDS per
* element). BIT-EXACT: every part[g] still accumulates over e in the
* same ascending order the g-outer form used, so this is pure operand
* motion. part[] is indexed only by the unrolled compile-time g, for
* the same reason acc[] is -- a runtime-bounded loop would push it to
* LOCAL memory and cost an LDL/STL round trip per element. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const float *kv = k_s + (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kve = kv[e];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
/* G is uniform across the block, so the guarded unroll keeps the
* shuffle warp-uniform exactly as the runtime-bounded loop did. */
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj; /* [TILE][G]: PV reads a run */
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
for (int j = 0; j < tk; j++) {
float vv = __ldg(&v_head[(size_t)(j0 + j) * head_dim + tid]);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
size_t po = (head0 + g) * stride + split;
acc_out[po * head_dim + tid] = acc[g];
if (tid == 0) {
m_out[po] = m_s[g];
l_out[po] = l_s[g];
}
}
}
}
/*
* Multi-user decode split-attention over the per-slot Q4 KV rings (with the
* per-slot fp16 sink / recent-window shadows). Identical plan/partial scheme
* to fc_attn_flash_decode_slots; K/V dequantized in-kernel like
* fc_attn_flash_decode_q4.
*/
__global__ void fc_attn_flash_decode_slots_q4(const float *q,
const unsigned char *k_cache,
const unsigned char *v_cache,
const __half *k_sink,
const __half *v_sink,
const __half *k_win,
const __half *v_win,
float *m_out, float *l_out,
float *acc_out, int kv_groups,
int head_dim, int max_seq,
const int *pos, int kps,
int splits_cap, int sink,
int win, int attn_sink,
int attn_win, float scale) {
const int split = blockIdx.x;
const int kvh = blockIdx.y;
const int slot = blockIdx.z;
const int tid = threadIdx.x;
const int G = kv_groups;
const int n_head_kv = gridDim.y;
const int stride = gridDim.x;
const int seq_len = pos[slot] + 1;
const int cur_pos = seq_len - 1;
const int row_bytes = fc_kvq4_row_bytes(head_dim);
int split_len;
int n_splits = fc_attn_dec_plan(seq_len, kps, splits_cap, &split_len);
const size_t head0 = ((size_t)slot * n_head_kv + kvh) * G;
if (split >= n_splits) {
for (int g = 0; g < G; g++) {
size_t po = (head0 + g) * stride + split;
acc_out[po * head_dim + tid] = 0.0f;
if (tid == 0) {
m_out[po] = -__int_as_float(0x7f800000);
l_out[po] = 0.0f;
}
}
return;
}
int k_lo = split * split_len;
int k_hi = k_lo + split_len;
if (k_hi > seq_len) k_hi = seq_len;
const int kstride = FC_ATTN_K_STRIDE(head_dim);
extern __shared__ float sh[];
float *q_s = sh;
float *k_s = q_s + (size_t)G * head_dim;
float *score_s = k_s + (size_t)FC_ATTN_TILE_KEYS * kstride;
float *p_s = score_s + G * FC_ATTN_TILE_KEYS;
float *m_s = p_s + G * FC_ATTN_TILE_KEYS;
float *l_s = m_s + G;
float *corr_s = l_s + G;
const float *q_base = q + head0 * head_dim;
for (int g = 0; g < G; g++)
q_s[(size_t)g * head_dim + tid] = q_base[(size_t)g * head_dim + tid];
if (tid < G) {
m_s[tid] = -__int_as_float(0x7f800000);
l_s[tid] = 0.0f;
}
__syncthreads();
/* Compile-time width, so ptxas keeps acc[] in REGISTERS. Every loop that
* indexes acc[] is unrolled to FC_ATTN_MAX_GROUPS with an `if (g < G)`
* guard; a loop bounded by the RUNTIME kv_groups cannot be unrolled, the
* array lands in LOCAL memory, and the PV inner loop then pays an
* LDL.128/STL.128 round trip per key. ptxas reports that as a 32-byte
* "stack frame" and prints "0 bytes spill stores" right beside it --
* a never-promoted local array is not counted as a spill. */
float acc[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
acc[g] = 0.0f;
const unsigned char *k_head = k_cache +
((size_t)slot * n_head_kv + kvh) * max_seq * row_bytes;
const unsigned char *v_head = v_cache +
((size_t)slot * n_head_kv + kvh) * max_seq * row_bytes;
const __half *k_win_head = k_win +
((size_t)slot * n_head_kv + kvh) * FC_KVQ4_WMAX * head_dim;
const __half *v_win_head = v_win +
((size_t)slot * n_head_kv + kvh) * FC_KVQ4_WMAX * head_dim;
const __half *k_sink_head = k_sink +
((size_t)slot * n_head_kv + kvh) * FC_KVQ4_SINKMAX * head_dim;
const __half *v_sink_head = v_sink +
((size_t)slot * n_head_kv + kvh) * FC_KVQ4_SINKMAX * head_dim;
const int ky = tid / FC_ATTN_DOT_LANES;
const int lane = tid % FC_ATTN_DOT_LANES;
const int chunk = head_dim / FC_ATTN_DOT_LANES;
for (int j0 = k_lo; j0 < k_hi; j0 += FC_ATTN_TILE_KEYS) {
int tk = k_hi - j0;
if (tk > FC_ATTN_TILE_KEYS) tk = FC_ATTN_TILE_KEYS;
/* Sliding window: skip tiles outside sink U recent-window. */
if (!fc_attn_tile_in_window(j0, tk, cur_pos, attn_sink, attn_win))
continue;
for (int key = 0; key < tk; key++) {
int kp = j0 + key;
float *krow = k_s + (size_t)key * kstride;
if (kp < sink) {
const __half *s = k_sink_head + (size_t)kp * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = __half2float(s[e]);
} else if (win > 0 && kp > cur_pos - win) {
const __half *s = k_win_head +
(size_t)(kp % FC_KVQ4_WMAX) * head_dim;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = __half2float(s[e]);
} else {
const unsigned char *s = k_head + (size_t)kp * row_bytes;
for (int e = tid; e < head_dim; e += blockDim.x)
krow[e] = fc_kvq4_dequant_elem(s, e);
}
}
__syncthreads();
/* QK^T, i-outer / g-inner: each thread reads its chunk of the staged K
* row ONCE per tile into a register and fans it across the whole GQA
* group, instead of re-reading k_s once per group (G=8 -> 1 LDS per
* element). BIT-EXACT: every part[g] still accumulates over e in the
* same ascending order the g-outer form used, so this is pure operand
* motion. part[] is indexed only by the unrolled compile-time g, for
* the same reason acc[] is -- a runtime-bounded loop would push it to
* LOCAL memory and cost an LDL/STL round trip per element. */
{
float part[FC_ATTN_MAX_GROUPS];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
part[g] = 0.0f;
if (ky < tk) {
const float *kv = k_s + (size_t)ky * kstride;
for (int i = 0; i < chunk; i++) {
int e = lane + i * FC_ATTN_DOT_LANES;
float kve = kv[e];
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++)
if (g < G)
part[g] += q_s[(size_t)g * head_dim + e] * kve;
}
}
/* G is uniform across the block, so the guarded unroll keeps the
* shuffle warp-uniform exactly as the runtime-bounded loop did. */
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
float p = part[g];
for (int off = FC_ATTN_DOT_LANES / 2; off > 0; off >>= 1)
p += __shfl_down_sync(0xffffffffu, p, (unsigned)off,
FC_ATTN_DOT_LANES);
if (lane == 0 && ky < tk)
score_s[g * FC_ATTN_TILE_KEYS + ky] = p * scale;
}
}
}
__syncthreads();
if (tid < G) {
const float *sc = score_s + tid * FC_ATTN_TILE_KEYS;
float m_new = m_s[tid];
for (int j = 0; j < tk; j++)
m_new = fmaxf(m_new, sc[j]);
float corr = expf(m_s[tid] - m_new);
float sum = 0.0f;
for (int j = 0; j < tk; j++) {
float pj = expf(sc[j] - m_new);
p_s[(size_t)j * G + tid] = pj; /* [TILE][G]: PV reads a run */
sum += pj;
}
l_s[tid] = l_s[tid] * corr + sum;
m_s[tid] = m_new;
corr_s[tid] = corr;
}
__syncthreads();
for (int j = 0; j < tk; j++) {
int vp = j0 + j;
float vv;
if (vp < sink)
vv = __half2float(v_sink_head[(size_t)vp * head_dim + tid]);
else if (win > 0 && vp > cur_pos - win)
vv = __half2float(v_win_head[(size_t)(vp % FC_KVQ4_WMAX) * head_dim + tid]);
else
vv = fc_kvq4_dequant_elem(v_head + (size_t)vp * row_bytes, tid);
const float *pj_s = p_s + (size_t)j * G;
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
if (j == 0)
acc[g] *= corr_s[g];
acc[g] += pj_s[g] * vv;
}
}
}
__syncthreads();
}
#pragma unroll
for (int g = 0; g < FC_ATTN_MAX_GROUPS; g++) {
if (g < G) {
size_t po = (head0 + g) * stride + split;
acc_out[po * head_dim + tid] = acc[g];
if (tid == 0) {
m_out[po] = m_s[g];
l_out[po] = l_s[g];
}
}
}
}
/* Device-position single-row Q4 KV append (CUDA-graph capture). Bit-exact to
* fc_attn_kv_append_q4 with pos0 = pos[0], npos = 1 (caller guarantees no
* ring wrap, matching the host-arg kernel's contract). */
__global__ void fc_attn_kv_append_q4_dev(const float *k_new,
const float *v_new,
unsigned char *k_cache,
unsigned char *v_cache,
__half *k_sink, __half *v_sink,
__half *k_win, __half *v_win,
int n_head_kv, int head_dim,
int max_seq, const int *pos, int sink,
int win) {
int t = blockIdx.x;
int h = blockIdx.y;
int tid = threadIdx.x;
if (tid >= head_dim) return;
int cache_pos = pos[0] + t;
int row_bytes = fc_kvq4_row_bytes(head_dim);
size_t src = ((size_t)t * n_head_kv + h) * head_dim + tid;
size_t dst_row = ((size_t)h * max_seq + cache_pos) * row_bytes;
int blk = tid / FC_KVQ4_BLK;
int lane = tid & (FC_KVQ4_BLK - 1);
/* Shared staging for the shuffle-free block quantizer (Kepler-safe). */
__shared__ float s_k[FC_KVQ4_MAX_HEAD_DIM];
__shared__ float s_v[FC_KVQ4_MAX_HEAD_DIM];
float kf = k_new[src];
float vf = v_new[src];
s_k[tid] = kf;
s_v[tid] = vf;
__syncthreads();
fc_kvq4_quant_block(s_k + (size_t)blk * FC_KVQ4_BLK,
k_cache + dst_row + (size_t)blk * FC_KVQ4_BLK_BYTES,
lane);
fc_kvq4_quant_block(s_v + (size_t)blk * FC_KVQ4_BLK,
v_cache + dst_row + (size_t)blk * FC_KVQ4_BLK_BYTES,
lane);
/* Mirror the row into the fp16 sink / recent-window shadow (element tid). */
if (win > 0) {
size_t wi = ((size_t)h * FC_KVQ4_WMAX + (cache_pos % FC_KVQ4_WMAX)) * head_dim + tid;
k_win[wi] = __float2half(kf);
v_win[wi] = __float2half(vf);
}
if (cache_pos < sink) {
size_t si = ((size_t)h * FC_KVQ4_SINKMAX + cache_pos) * head_dim + tid;
k_sink[si] = __float2half(kf);
v_sink[si] = __float2half(vf);
}
}
} /* extern "C" */