ember.cpp 0.1.0
C++ inference engine for Llama-family and paperkiln models (CPU, CUDA, WebAssembly)
Loading...
Searching...
No Matches
decode_attn_tiled.cuh
Go to the documentation of this file.
1// Tiled split-K decode attention: the block bodies shared by the decode step (cuda_kernels.cu) and the speculative
2// verification pass (verify_kernels.cu), so a verification row is the decode step's arithmetic by construction.
3//
4// One new query row attends to L keys of one head. The keys are cut into nchunks(L) chunks of chunk(L) consecutive
5// keys; one block of kTiledThreads threads takes one (head, chunk):
6// scores a quad of 4 lanes per key, each lane a float4 slice of head_dim (j, j + 4, ...), the 4 lane sums added
7// by a 2-step xor butterfly (the same value in all 4 lanes); 32 keys per pass of the block;
8// softmax the chunk's max and sum of e^{s - max} by warp shuffles and per-warp slots added in warp order;
9// values G = 128 / (head_dim / 4) groups of head_dim / 4 lanes; group g adds the keys g, g + G, ... in order
10// into a float4 accumulator; the G partial sums are then added in group order;
11// and writes (m, l, unnormalised sum_k p_k v_k) to its slot of the scratch. The merge (one block per head) takes
12// the chunks' maximum M, the weights w_c = e^{m_c - M}, L = sum_c w_c l_c by a fixed shuffle tree, and
13// out = (sum_c w_c acc_c) / L, adding the chunks in index order. Every reduction has a fixed order, so the result
14// is the same run to run, and it depends only on L (not on the cache's capacity or the grid): the graph decode,
15// the per-launch decode and the verification rows agree bit for bit at equal L.
16//
17// The chunking (tiled_chunk): chunk = max(kTiledMinChunk, ceil(L / kTiledMaxChunks)), nchunks = ceil(L / chunk).
18// A grid for a cache of max_len positions has tiled_grid_chunks(max_len) chunk blocks per head, an upper bound for
19// every L <= max_len; blocks past nchunks(L) return at once. The old kernel (256-key chunks for the cache's
20// capacity, experiments/decode_occupancy_2026-10-27) ran 10 blocks for pick_e1_L16 at any length.
21#ifndef EMBER_DECODE_ATTN_TILED_CUH
22#define EMBER_DECODE_ATTN_TILED_CUH
23
24#include <cuda_runtime.h>
25#include <math.h>
26
27namespace ember_tiled {
28
29constexpr int kTiledThreads = 128;
30constexpr int kTiledMinChunk = 32; // keys per chunk below kTiledMinChunk * kTiledMaxChunks keys
31constexpr int kTiledMaxChunks = 64; // chunks per head at most (the merge's weights; the scratch's chunk slots)
32constexpr int kTiledChunkMax = 2048; // keys per chunk at most (the scores in shared memory)
33constexpr int kTiledHeadDimMax = 256; // head_dim % 4 == 0 and <= this
34
36 const int c = (L + max_chunks - 1) / max_chunks;
37 return c < min_chunk ? min_chunk : c;
38}
39__host__ __device__ inline int tiled_nchunks(int L, int chunk) { return (L + chunk - 1) / chunk; }
40// Chunk blocks per head for a cache of max_len positions (>= tiled_nchunks for every L <= max_len).
41__host__ __device__ inline int tiled_grid_chunks(int max_len, int min_chunk, int max_chunks) {
42 const int n = (max_len + min_chunk - 1) / min_chunk;
43 return n < max_chunks ? n : max_chunks;
44}
45// True when the tiled kernel takes this shape for a cache of max_len positions.
46__host__ inline bool tiled_shape_ok(int heads, int head_dim, int max_len, int heads_max, int min_chunk,
47 int max_chunks) {
50}
51// Dynamic shared memory of the split block: q[head_dim] and the scores s[chunk] for the cache's largest chunk.
52__host__ inline size_t tiled_shm_bytes(int head_dim, int max_len, int min_chunk, int max_chunks) {
53 return (size_t)(head_dim + tiled_chunk(max_len, min_chunk, max_chunks)) * sizeof(float);
54}
55
56// One (head h, chunk c) block. Qh: the head's query row [head_dim]; K, V: the layer's cache [pos][kv_head][head_dim];
57// part: this block's slot [m, l, acc[head_dim]]. Every thread of the block calls it.
59 const float* __restrict__ V, int L, int c, int kvh,
60 int num_kv_heads, int head_dim, float scale, int min_chunk,
61 int max_chunks, float* __restrict__ part) {
62 const int chunk = tiled_chunk(L, min_chunk, max_chunks);
63 const int k0 = c * chunk;
64 if (k0 >= L) return; // past nchunks(L): the merge does not read this slot
65 const int k1 = min(L, k0 + chunk);
66 const int n = k1 - k0;
67 const int d4n = head_dim >> 2;
69 float4* q4 = tiled_sm4; // [head_dim / 4]
70 float* s = reinterpret_cast<float*>(q4 + d4n); // [chunk]
71 __shared__ float red[kTiledThreads / 32];
72 __shared__ float4 pacc4[kTiledThreads]; // [G][head_dim / 4]
73 const int tid = threadIdx.x, lane = tid & 31, warp = tid >> 5;
74 const float4* Q4 = reinterpret_cast<const float4*>(Qh);
75 for (int j = tid; j < d4n; j += kTiledThreads) q4[j] = Q4[j];
77 const size_t row4 = (size_t)num_kv_heads * d4n; // float4s per cache position
78 const float4* K4 = reinterpret_cast<const float4*>(K) + (size_t)kvh * d4n;
79 const float4* V4 = reinterpret_cast<const float4*>(V) + (size_t)kvh * d4n;
80 // Scores: quad (tid >> 2) takes key kb + quad; lane (tid & 3) the float4 slices ql, ql + 4, ...
81 const int quad = tid >> 2, ql = tid & 3;
82 for (int kb = k0; kb < k1; kb += kTiledThreads / 4) {
83 const int k = kb + quad;
84 float dot = 0.0f;
85 if (k < k1) {
86 const float4* kr = K4 + (size_t)k * row4;
87#pragma unroll 4
88 for (int j = ql; j < d4n; j += 4) {
89 const float4 a = q4[j];
90 const float4 b = __ldg(kr + j);
91 dot = fmaf(a.x, b.x, dot);
92 dot = fmaf(a.y, b.y, dot);
93 dot = fmaf(a.z, b.z, dot);
94 dot = fmaf(a.w, b.w, dot);
95 }
96 }
97 dot += __shfl_xor_sync(0xffffffff, dot, 1);
98 dot += __shfl_xor_sync(0xffffffff, dot, 2);
99 if (k < k1 && ql == 0) s[k - k0] = dot * scale;
100 }
102 float mx = -INFINITY;
103 for (int i = tid; i < n; i += kTiledThreads) mx = fmaxf(mx, s[i]);
104 for (int o = 16; o > 0; o >>= 1) mx = fmaxf(mx, __shfl_xor_sync(0xffffffff, mx, o));
105 if (lane == 0) red[warp] = mx;
107 mx = red[0];
108 for (int w = 1; w < kTiledThreads / 32; ++w) mx = fmaxf(mx, red[w]);
110 float sum = 0.0f;
111 for (int i = tid; i < n; i += kTiledThreads) {
112 const float e = expf(s[i] - mx);
113 s[i] = e;
114 sum += e;
115 }
116 for (int o = 16; o > 0; o >>= 1) sum += __shfl_xor_sync(0xffffffff, sum, o);
117 if (lane == 0) red[warp] = sum;
118 __syncthreads(); // also publishes s[] = e^{s - max} to the value pass
119 if (tid == 0) {
120 float l = 0.0f;
121 for (int w = 0; w < kTiledThreads / 32; ++w) l += red[w];
122 part[0] = mx;
123 part[1] = l;
124 }
125 // Values: group g = tid / d4n adds keys g, g + G, ... (G = 128 / d4n groups; threads past G * d4n idle).
126 const int G = kTiledThreads / d4n;
127 const int g = tid / d4n, j = tid - g * d4n;
128 if (g < G) {
129 float4 acc = make_float4(0.0f, 0.0f, 0.0f, 0.0f);
130 const float4* vc = V4 + (size_t)k0 * row4 + j;
131#pragma unroll 4
132 for (int i = g; i < n; i += G) {
133 const float w = s[i];
134 const float4 v = __ldg(vc + (size_t)i * row4);
135 acc.x = fmaf(w, v.x, acc.x);
136 acc.y = fmaf(w, v.y, acc.y);
137 acc.z = fmaf(w, v.z, acc.z);
138 acc.w = fmaf(w, v.w, acc.w);
139 }
140 pacc4[g * d4n + j] = acc;
141 }
143 const float* pacc = reinterpret_cast<const float*>(pacc4);
144 for (int d = tid; d < head_dim; d += kTiledThreads) {
145 float a = 0.0f;
146 for (int gg = 0; gg < G; ++gg) a += pacc[gg * head_dim + d];
147 part[2 + d] = a;
148 }
149}
150
151// The merge of one head: base = the head's chunk slots (stride head_dim + 2), out_h = the head's output row.
153 int head_dim, int min_chunk, int max_chunks) {
154 const int nc = tiled_nchunks(L, tiled_chunk(L, min_chunk, max_chunks)); // <= max_chunks <= 64
155 const int stride = head_dim + 2;
157 __shared__ float inv_s;
158 const int tid = threadIdx.x;
159 if (tid < 32) {
160 const int c0 = tid, c1 = tid + 32;
161 const float m0 = c0 < nc ? base[(size_t)c0 * stride] : -INFINITY;
162 const float m1 = c1 < nc ? base[(size_t)c1 * stride] : -INFINITY;
163 float M = fmaxf(m0, m1);
164 for (int o = 16; o > 0; o >>= 1) M = fmaxf(M, __shfl_xor_sync(0xffffffff, M, o));
165 const float w0 = c0 < nc ? expf(m0 - M) : 0.0f;
166 const float w1 = c1 < nc ? expf(m1 - M) : 0.0f;
167 w[c0] = w0;
168 w[c1] = w1;
169 // Explicit roundings: no contraction into an fma, so every kernel that inlines this adds the same values.
170 const float l0 = c0 < nc ? __fmul_rn(w0, base[(size_t)c0 * stride + 1]) : 0.0f;
171 const float l1 = c1 < nc ? __fmul_rn(w1, base[(size_t)c1 * stride + 1]) : 0.0f;
172 float l = __fadd_rn(l0, l1);
173 for (int o = 16; o > 0; o >>= 1) l += __shfl_xor_sync(0xffffffff, l, o);
174 if (tid == 0) inv_s = l > 0.0f ? 1.0f / l : 0.0f;
175 }
177 const float inv = inv_s;
178 for (int d = tid; d < head_dim; d += blockDim.x) {
179 float acc = 0.0f;
180 const float* p = base + 2 + d;
181#pragma unroll 4
182 for (int c = 0; c < nc; ++c) acc = fmaf(w[c], p[(size_t)c * stride], acc);
183 out_h[d] = acc * inv;
184 }
185}
186
187} // namespace ember_tiled
188
189#endif // EMBER_DECODE_ATTN_TILED_CUH
const float * p[kIrDepthSourcesMax]
int n
std::vector< float > v
float c
std::vector< E > a
node scale
size_t j
__host__ bool tiled_shape_ok(int heads, int head_dim, int max_len, int heads_max, int min_chunk, int max_chunks)
__host__ size_t tiled_shm_bytes(int head_dim, int max_len, int min_chunk, int max_chunks)
__host__ __device__ int tiled_nchunks(int L, int chunk)
constexpr int kTiledHeadDimMax
__device__ __forceinline__ void tiled_split_block(const float *__restrict__ Qh, const float *__restrict__ K, const float *__restrict__ V, int L, int c, int kvh, int num_kv_heads, int head_dim, float scale, int min_chunk, int max_chunks, float *__restrict__ part)
constexpr int kTiledThreads
constexpr int kTiledMaxChunks
__host__ __device__ int tiled_chunk(int L, int min_chunk, int max_chunks)
constexpr int kTiledMinChunk
constexpr int kTiledChunkMax
__device__ __forceinline__ void tiled_merge_block(const float *__restrict__ base, float *__restrict__ out_h, int L, int head_dim, int min_chunk, int max_chunks)
__host__ __device__ int tiled_grid_chunks(int max_len, int min_chunk, int max_chunks)