GCC Code Coverage Report


Directory: ./
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 100.0% 5 / 0 / 5
Functions: 100.0% 1 / 0 / 1
Branches: -% 0 / 0 / 0

include/brotensor/ops/flash_attention.h
Line Branch Exec Source
1 #pragma once
2
3 // brotensor ops/flash_attention.h — Flash-attention family: tiled/windowed/varlen/qkvo/decode + KV cache.
4
5 #include "../tensor.h"
6 #include <cstdint>
7
8 namespace brotensor {
9
10
11 // Flash-attention-style fused attention (FP16, inference-only). Q, K, V are
12 // already projected. Tiled online softmax over Lk (no Lk-long materialisation);
13 // FP32 accumulation.
14 // Q: (Lq,D); K, V: (Lk,D) — FP16.
15 // d_mask: optional length-Lk FP32 mask (1 valid / 0 invalid); may be null.
16 // num_heads divides D.
17 // causal: if true, key k attends to query q only when k <= q (requires
18 // Lq == Lk); combines multiplicatively with d_mask.
19 // O: (Lq,D) FP16, resized as needed.
20 void flash_attention_forward(const Tensor& Q,
21 const Tensor& K,
22 const Tensor& V,
23 const float* d_mask,
24 int num_heads,
25 bool causal,
26 Tensor& O);
27
28
29 // GQA generalisation of flash_attention_forward: grouped-query self-attention
30 // over pre-projected Q/K/V, causal OR bidirectional. Q carries num_q_heads,
31 // K/V carry num_kv_heads (num_kv_heads must divide num_q_heads; equal == plain
32 // MHA). causal == false is full bidirectional attention over all Lk keys — the
33 // encoder prefill a bidirectional-ified decoder needs (LLM2Vec). causal == true
34 // reproduces flash_attention_forward's causal mask with GQA (requires Lq == Lk).
35 // Tiled online softmax, FP16/BF16/FP32, FP32 accumulation; shares the windowed
36 // kernel's GQA + key-mask machinery.
37 // Q: (Lq, num_q_heads*head_dim); K, V: (Lk, num_kv_heads*head_dim).
38 // d_mask: optional length-Lk FP32 key mask (1 valid / 0 invalid); may be null.
39 // num_kv_heads divides num_q_heads; head_dim = Q.cols/num_q_heads must equal
40 // K.cols/num_kv_heads. Lk >= Lq; causal additionally requires Lq == Lk.
41 // O: (Lq, num_q_heads*head_dim), same dtype as Q, resized as needed.
42 void flash_attention_gqa_forward(const Tensor& Q,
43 const Tensor& K,
44 const Tensor& V,
45 const float* d_mask,
46 int num_q_heads,
47 int num_kv_heads,
48 bool causal,
49 Tensor& O);
50
51
52 // Sliding-window causal self-attention (FP32, inference-only) — the local
53 // attention of streaming neural codecs (e.g. Qwen3-TTS / Mimi) and the
54 // autoregressive decode step. Q, K, V already projected, (L, num_heads*head_dim).
55 // Always causal. The Lq queries occupy the last Lq positions of a length-Lk
56 // causal sequence (q_offset = Lk - Lq): query row r is at absolute position
57 // r + q_offset and attends keys [max(0, pos-window+1), pos]. window <= 0 means
58 // unbounded causal — identical to flash_attention_forward with causal=true.
59 // - Lq == Lk: self-attention (prefill / codec sliding window).
60 // - Lq < Lk: incremental decode of an Lq-token block over a K/V cache;
61 // Lq == 1 with window <= 0 attends every cached key (full cache attention),
62 // replacing a varlen call with no cu_seqlens upload. Requires Lk >= Lq.
63 // d_mask: optional length-Lk FP32 key mask (1 valid / 0 invalid), combined
64 // multiplicatively with the window; may be null.
65 // num_heads divides D. O: (Lq, num_heads*head_dim), resized as needed.
66 void flash_attention_windowed_forward(const Tensor& Q,
67 const Tensor& K,
68 const Tensor& V,
69 const float* d_mask,
70 int num_heads,
71 int window,
72 Tensor& O);
73
74
75 // Packed variable-length multi-head attention, forward only (Qwen3-VL window
76 // attention). All sequences in a batch live contiguously in one packed tensor;
77 // per-sequence boundaries come from `cu_seqlens_*` INT32 prefix-sum buffers of
78 // length `batch_size + 1`. Sequence b covers Q rows
79 // [cu_seqlens_q[b], cu_seqlens_q[b+1]) and attends to K/V rows
80 // [cu_seqlens_k[b], cu_seqlens_k[b+1]); no cross-sequence attention. Mirrors
81 // flash_attn_varlen_func semantics.
82 // Q: (total_tokens_q, num_heads * head_dim) — FP16/BF16/FP32 (GPU) / FP32 (CPU).
83 // K, V: (total_tokens_k, num_heads * head_dim) — same packing, num_heads
84 // matches Q (no GQA — that's a future cleanup).
85 // cu_seqlens_q, cu_seqlens_k: DEVICE pointers (CUDA/Metal device, raw host
86 // pointers on CPU), length batch_size + 1, INT32 prefix sums. Same
87 // convention as `const float* d_mask` elsewhere — caller owns the
88 // buffer; the op does not allocate or copy.
89 // max_seqlen_q, max_seqlen_k: bound the longest sequence's length — used by
90 // the GPU kernel for block sizing; the CPU impl ignores them.
91 // causal: if true, key k in sequence b attends to query q only when
92 // (k - cu_seqlens_k[b]) <= (q - cu_seqlens_q[b]); only meaningful when
93 // the per-sequence Q and K lengths match.
94 // O: (total_tokens_q, num_heads * head_dim) — overwritten, resized + dtype-set.
95 void flash_attention_varlen_forward(const Tensor& Q,
96 const Tensor& K,
97 const Tensor& V,
98 const int32_t* cu_seqlens_q,
99 const int32_t* cu_seqlens_k,
100 int batch_size,
101 int max_seqlen_q,
102 int max_seqlen_k,
103 int num_heads,
104 int head_dim,
105 bool causal,
106 Tensor& O);
107
108
109 // Backward of flash_attention_varlen_forward — packed variable-length attention
110 // over pre-projected Q/K/V (no projection weights, no biases — projections are
111 // handled by the caller's linear layer; bias gradients belong to that layer).
112 // Recompute-based: consumes no forward caches, re-runs the per-sequence
113 // softmax then reverses it. Matches flash_attention_backward's per-sequence
114 // math; only the cu_seqlens scatter/gather and per-sequence causal differ.
115 // Q: (total_tokens_q, num_heads*head_dim) — FP16/BF16/FP32 (GPU) / FP32 (CPU).
116 // K, V: (total_tokens_k, num_heads*head_dim) — same dtype, same packing.
117 // O: (total_tokens_q, ...) forward output — currently unused (kept for API
118 // symmetry with flash_attention_backward).
119 // dO: (total_tokens_q, ...) upstream gradient, same dtype as Q/K/V.
120 // cu_seqlens_q, cu_seqlens_k: DEVICE pointers on GPU, host pointers on CPU,
121 // length batch_size + 1, INT32 prefix sums. Same convention as the
122 // forward.
123 // max_seqlen_q, max_seqlen_k: bound the longest per-sequence length — used
124 // by the GPU kernel for block/scratch sizing; the CPU impl ignores them.
125 // causal: per-sequence; only meaningful when per-sequence Lq == Lk.
126 // dQ: (total_tokens_q, ...); dK, dV: (total_tokens_k, ...) — OVERWRITTEN
127 // (resized + dtype-set), matching flash_attention_backward's contract.
128 // Out-of-range positions (causal-excluded, or in a fully empty K sequence)
129 // contribute nothing to any gradient. No cross-sequence attention.
130 void flash_attention_varlen_backward(const Tensor& Q,
131 const Tensor& K,
132 const Tensor& V,
133 const Tensor& O,
134 const Tensor& dO,
135 const int32_t* cu_seqlens_q,
136 const int32_t* cu_seqlens_k,
137 int batch_size,
138 int max_seqlen_q,
139 int max_seqlen_k,
140 int num_heads,
141 int head_dim,
142 bool causal,
143 Tensor& dQ,
144 Tensor& dK,
145 Tensor& dV);
146
147
148 // Flash-attention with QKV and output projections fused at the boundary.
149 // Projects X->Q, Ctx->K,V (or X->Q,K,V when Ctx is null), runs the tiled core,
150 // then projects with Wo. FP16 throughout.
151 // X: (Lq,D) FP16, query source.
152 // Ctx: (Lk,D_ctx) FP16 or null. Null => self-attention (Ctx<-X). D_ctx may
153 // differ from D (e.g. SD1.5 cross-attention).
154 // Wq, Wo: (D,D); Wk, Wv: (D,D_ctx) — FP16.
155 // bq, bk, bv, bo: optional (D,1) FP16 biases; null to skip.
156 // d_mask: optional length-Lk FP32 mask. num_heads divides D.
157 // causal: see flash_attention_forward (typically with Ctx == null).
158 // O: (Lq,D) FP16, resized as needed.
159 void flash_attention_qkvo_forward(const Tensor& X,
160 const Tensor* Ctx,
161 const Tensor& Wq, const Tensor* bq,
162 const Tensor& Wk, const Tensor* bk,
163 const Tensor& Wv, const Tensor* bv,
164 const Tensor& Wo, const Tensor* bo,
165 const float* d_mask,
166 int num_heads,
167 bool causal,
168 Tensor& O);
169
170
171 // Backward of flash_attention_qkvo_forward. Recompute-style: consumes no
172 // forward caches — re-runs the attention math from the inputs, then reverses
173 // it. FP16 storage, FP32 accumulation. All shape / dtype / Ctx-null /
174 // rectangular-Wk-Wv / causal / optional-bias rules match the forward; pass the
175 // same values.
176 // dO: (Lq,D) FP16 upstream.
177 // dX: (Lq,D) FP16 overwritten. For self-attention (Ctx null) dX absorbs
178 // the K/V-projection gradients too: dX = dQ.Wq + dK.Wk + dV.Wv.
179 // dCtx: (Lk,D_ctx) FP16 overwritten; must be null iff Ctx is null. For
180 // cross-attention dCtx = dK.Wk + dV.Wv.
181 // dWq, dWo: (D,D); dWk, dWv: (D,D_ctx) — FP16, accumulated (caller zeros).
182 // dbq, dbk, dbv, dbo: (D,1) FP16, accumulated iff the matching forward bias
183 // was non-null; pass null otherwise (the null/non-null symmetry must
184 // be exact — a mismatch is rejected).
185 // Causal- and mask-excluded positions contribute nothing to any gradient.
186 void flash_attention_qkvo_backward(
187 const Tensor& X, const Tensor* Ctx,
188 const Tensor& Wq, const Tensor* bq,
189 const Tensor& Wk, const Tensor* bk,
190 const Tensor& Wv, const Tensor* bv,
191 const Tensor& Wo, const Tensor* bo,
192 const float* d_mask,
193 int num_heads,
194 bool causal,
195 const Tensor& dO,
196 Tensor& dX, Tensor* dCtx,
197 Tensor& dWq, Tensor* dbq,
198 Tensor& dWk, Tensor* dbk,
199 Tensor& dWv, Tensor* dbv,
200 Tensor& dWo, Tensor* dbo);
201
202
203 // Backward of flash_attention_forward — bare attention core, no projection
204 // weights (what LoRA-style adapters need; projections are wrapped externally).
205 // Recompute-based; FP16 storage, FP32 accumulation.
206 // Q: (Lq,D); K, V: (Lk,D) — pre-projected forward inputs, FP16.
207 // O: (Lq,D) forward output — currently unused (kept for API symmetry).
208 // dO: (Lq,D) FP16 upstream.
209 // d_mask: optional length-Lk FP32 mask (null for unmasked); positions with
210 // mask[k] <= 0.5 are dropped.
211 // num_heads divides D. causal: match the forward (requires Lq == Lk).
212 // dQ: (Lq,D); dK, dV: (Lk,D) — FP16, overwritten (resized + dtype-set).
213 // Causal- and mask-excluded positions contribute nothing to dQ/dK/dV.
214 void flash_attention_backward(const Tensor& Q,
215 const Tensor& K,
216 const Tensor& V,
217 const Tensor& O,
218 const Tensor& dO,
219 const float* d_mask,
220 int num_heads,
221 bool causal,
222 Tensor& dQ,
223 Tensor& dK,
224 Tensor& dV);
225
226
227 // Project a context tensor through Wk/Wv into the exact (Lk,D) FP16 K/V buffers
228 // flash_attention_forward consumes. Used to precompute cross-attention K/V once
229 // per generate() (text context is fixed across denoising steps). Numerically
230 // identical to the K/V projection stage of flash_attention_qkvo_forward.
231 // ctx: (Lk,D_ctx) FP16. Wk, Wv: (D,D_ctx) FP16. bk, bv: (D,1) FP16 or null.
232 // K_out, V_out: (Lk,D) FP16, resized as needed.
233 void flash_attention_project_kv(const Tensor& ctx,
234 const Tensor& Wk, const Tensor* bk,
235 const Tensor& Wv, const Tensor* bv,
236 Tensor& K_out,
237 Tensor& V_out);
238
239
240 // Like flash_attention_qkvo_forward but K and V are already projected by the
241 // caller (typically via flash_attention_project_kv). Projects X->Q with Wq/bq,
242 // runs the tiled core against the supplied K/V, applies Wo/bo. FP16 throughout.
243 // X: (Lq,D); K, V: (Lk,D) — FP16. Wq, Wo: (D,D) FP16; bq, bo: (D,1) FP16 or null.
244 // d_mask: optional length-Lk FP32 mask. num_heads divides D.
245 // causal: see flash_attention_forward. O: (Lq,D) FP16, resized as needed.
246 void flash_attention_q_with_kv_cached_forward(const Tensor& X,
247 const Tensor& K,
248 const Tensor& V,
249 const Tensor& Wq, const Tensor* bq,
250 const Tensor& Wo, const Tensor* bo,
251 const float* d_mask,
252 int num_heads,
253 bool causal,
254 Tensor& O);
255
256
257 // KV-cache append (FP16): copy K_new, V_new into rows [cur_len, cur_len+L_new)
258 // of K_cache, V_cache.
259 // K_new, V_new: (L_new,D) FP16. K_cache, V_cache: (L_max,D) FP16 — must be
260 // pre-sized (not resized); cur_len + L_new <= L_max.
261 void kv_cache_append(const Tensor& K_new, const Tensor& V_new,
262 int cur_len, Tensor& K_cache, Tensor& V_cache);
263
264
265 // Causal flash-attention against a partially-filled KV cache (FP16, fwd-only).
266 // Runs the tiled core against rows [0, valid_len) of the caches. Query position
267 // p_q = (valid_len - L_q) + i attends to cache positions [0, p_q].
268 // Q: (L_q, num_q_heads*head_dim) FP16 — L_q == 1 for token-by-token decode,
269 // L_q > 1 supported.
270 // K_cache, V_cache: (L_max, num_kv_heads*head_dim) FP16 — only rows
271 // [0, valid_len) are read.
272 // valid_len >= L_q. num_kv_heads must divide num_q_heads (GQA); each KV head
273 // serves num_q_heads/num_kv_heads consecutive query heads. num_kv_heads ==
274 // num_q_heads is plain MHA. head_dim = Q.cols/num_q_heads must equal
275 // K_cache.cols/num_kv_heads.
276 // O: (L_q, num_q_heads*head_dim) FP16, resized as needed.
277 // GQA is native on all three backends: the decode kernel maps query head h to KV
278 // head h/(num_q_heads/num_kv_heads), reading the n_kv-wide cache directly — no
279 // KV-head widening needed. num_kv_heads == num_q_heads is plain MHA.
280 //
281 // Gemma-2 extensions (both default to today's exact behaviour):
282 // attn_softcap > 0: tanh logit soft-capping. After the 1/sqrt(head_dim) scale
283 // and BEFORE the causal/window mask and the online-softmax max/exp, each
284 // raw score s is replaced by attn_softcap * tanh(s / attn_softcap).
285 // attn_softcap == 0 (default) disables it — bit-identical to before.
286 // window > 0: sliding-window causal masking. Query at absolute position p
287 // (p = valid_len - L_q + q) attends key j only when j <= p AND
288 // j > p - window, i.e. keys in [max(0, p-window+1), p]. window <= 0
289 // (default) is unbounded causal — bit-identical to before. Composes
290 // multiplicatively with the causal mask.
291 void flash_attention_decode(const Tensor& Q,
292 const Tensor& K_cache, const Tensor& V_cache,
293 int valid_len, int num_q_heads, int num_kv_heads,
294 Tensor& O,
295 float attn_softcap = 0.0f, int window = 0);
296
297
298 // Single-token decode attention over a FIXED-CAPACITY masked KV cache — the
299 // CUDA-graph-capturable twin of flash_attention_decode. The kernel always
300 // sees the full (L_max, ·) cache buffers, so its launch shape never changes
301 // as generation advances; validity lives in `d_mask`, a device-resident
302 // length-L_max FP32 key mask (1 valid / 0 invalid) the caller updates
303 // between graph replays. Masked keys are dropped before the dot product and
304 // softmax (their weights underflow to exact zeros), so with
305 // mask = [1]*valid_len + [0]*(L_max-valid_len) the result is bit-identical
306 // to flash_attention_decode(Q, K, V, valid_len, ...) at L_q == 1. Fully
307 // masked key tiles are skipped without touching K/V.
308 // Q: (1, num_q_heads*head_dim) — single query row only.
309 // K_cache, V_cache: (L_max, num_kv_heads*head_dim); same GQA rules as
310 // flash_attention_decode.
311 // d_mask: device pointer, length L_max, FP32; must not be null and must
312 // mark at least one key valid.
313 // O: (1, num_q_heads*head_dim), resized as needed.
314 // Gemma-2 extensions match flash_attention_decode (see above):
315 // attn_softcap > 0: tanh logit soft-capping applied to each raw score before
316 // the mask and softmax. attn_softcap == 0 (default) disables it.
317 // window > 0: sliding-window masking. The query's absolute position p is the
318 // highest valid key index (the last 1 in d_mask); a key j is kept only
319 // when it is valid AND j > p - window, i.e. j in [max(0,p-window+1), p].
320 // window <= 0 (default) keeps the full valid set — bit-identical to
321 // before. Combines multiplicatively with the validity mask.
322 void flash_attention_decode_masked(const Tensor& Q,
323 const Tensor& K_cache,
324 const Tensor& V_cache,
325 const float* d_mask,
326 int num_q_heads, int num_kv_heads,
327 Tensor& O,
328 float attn_softcap = 0.0f, int window = 0);
329
330
331 // Back-compat overload: num_kv_heads defaults to num_heads (plain MHA).
332 1 inline void flash_attention_decode(const Tensor& Q,
333 const Tensor& K_cache, const Tensor& V_cache,
334 int valid_len, int num_heads, Tensor& O,
335 float attn_softcap = 0.0f, int window = 0) {
336 2 flash_attention_decode(Q, K_cache, V_cache, valid_len,
337 1 num_heads, /*num_kv_heads=*/num_heads, O,
338 1 attn_softcap, window);
339 1 }
340
341
342 // ─── W8A16 flash-attention variants ────────────────────────────────────────
343 //
344 // Same composition as flash_attention_project_kv /
345 // flash_attention_q_with_kv_cached_forward / flash_attention_qkvo_forward, but
346 // every projection takes an INT8 weight + per-output-row FP32 scale instead of
347 // an FP16 weight. The attention core stays FP16 (activations are never
348 // quantised). Each W*_int8 is (D, in_dim) with a matching scale (D,1); biases
349 // stay FP16 (D,1), optional. Masks, causal flag, and num_heads match the FP16
350 // versions.
351 void flash_attention_project_kv_int8w_fp16(const Tensor& ctx,
352 const Tensor& Wk_int8,
353 const Tensor& sk,
354 const Tensor* bk,
355 const Tensor& Wv_int8,
356 const Tensor& sv,
357 const Tensor* bv,
358 Tensor& K_out,
359 Tensor& V_out);
360
361
362 void flash_attention_q_with_kv_cached_int8w_fp16(const Tensor& X,
363 const Tensor& K,
364 const Tensor& V,
365 const Tensor& Wq_int8,
366 const Tensor& sq,
367 const Tensor* bq,
368 const Tensor& Wo_int8,
369 const Tensor& so,
370 const Tensor* bo,
371 const float* d_mask,
372 int num_heads,
373 bool causal,
374 Tensor& O);
375
376
377 void flash_attention_qkvo_int8w_fp16(const Tensor& X,
378 const Tensor* Ctx,
379 const Tensor& Wq_int8, const Tensor& sq, const Tensor* bq,
380 const Tensor& Wk_int8, const Tensor& sk, const Tensor* bk,
381 const Tensor& Wv_int8, const Tensor& sv, const Tensor* bv,
382 const Tensor& Wo_int8, const Tensor& so, const Tensor* bo,
383 const float* d_mask,
384 int num_heads,
385 bool causal,
386 Tensor& O);
387
388 } // namespace brotensor
389