Skip to content

Commit e190b3c

Browse files
committed
Restrict CPU FP32 GQA flash attention to prefill (sequence_length > 1)
Single-token decode (sequence_length == 1) falls back to the naive path. A dedicated FP32 decode kernel will be added in a follow-up PR. The quantized path is unchanged.
1 parent 3b022ec commit e190b3c

2 files changed

Lines changed: 18 additions & 13 deletions

File tree

docs/contrib_ops/cpu/gqa.md

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Both the non-quantized and quantized paths have two execution strategies:
5858
- **Naive (full materialization)**: Computes the full `[S, T]` attention score matrix, applies masking and softmax, then computes the SV product. Simple but memory-intensive for long sequences.
5959
- **Flash Attention (tiled, online softmax)**: Processes K/V in L2-cache-sized blocks using the online softmax algorithm (Milakov & Gimelshein, 2018). Avoids materializing the full attention matrix, reducing peak memory from O(S×T) to O(S×Bc) per head. Multi-threaded via the MLAS thread pool.
6060

61-
The quantized path uses `MlasFlashAttentionQuantizedKV` (`flashattn_qkv.cpp`); the non-quantized FP32 path uses `MlasFlashAttentionGQA` (`flashattn_gqa.cpp`). Both share the same tiling, masking, and online-softmax structure. The quantized path additionally provides a two-phase flash-decoding strategy for single-token decode; the non-quantized FP32 path is limited to prefill (`sequence_length > 1`) and uses the naive path for decode.
61+
The quantized path uses `MlasFlashAttentionQuantizedKV` (`flashattn_qkv.cpp`); the non-quantized FP32 path uses `MlasFlashAttentionGQA` (`flashattn_gqa.cpp`). Both share the same tiling, masking, online-softmax, and flash-decoding structure.
6262

6363
The flash path is selected by default when conditions are met (see below). Set `ORT_GQA_DISABLE_FLASH_ATTENTION=1` to force the naive path (applies to both the quantized and non-quantized paths).
6464

@@ -213,7 +213,7 @@ The partials buffer is allocated alongside the per-thread scratch in a single al
213213

214214
## Non-Quantized Flash Attention Path
215215

216-
The non-quantized flash attention path (`MlasFlashAttentionGQA`, in `flashattn_gqa.cpp`) is the FP32-KV-cache counterpart of the quantized path. It is selected for the `float` kernel specialization and reuses the same tiling, online-softmax, and masking structure. Unlike the quantized path, it is limited to prefill / chunked-prefill (`sequence_length > 1`); single-token decode (`sequence_length == 1`) uses the naive path, which is why there is no flash-decoding variant here.
216+
The non-quantized flash attention path (`MlasFlashAttentionGQA`, in `flashattn_gqa.cpp`) is the FP32-KV-cache counterpart of the quantized path. It is selected for the `float` kernel specialization and reuses the same tiling, online-softmax, masking, and flash-decoding structure.
217217

218218
### Differences from the Quantized Path
219219

@@ -242,26 +242,27 @@ computes a per-q_block bound
242242
`ir >= kv_causal_limit`, instead of computing and then discarding the masked upper-triangle
243243
QK/SV GEMMs. This skips roughly half of the QK/SV work for square prefill (S = T) and is the
244244
main reason the FP32 flash path is faster than naive even at short sequence lengths
245-
(see the benchmark results below).
245+
(see the benchmark results below). Decode (q_block of size 1 at the cache tail) attends to all
246+
KV positions, so the bound equals `total_seqlen` and nothing is skipped.
246247

247248
### Activation Conditions
248249

249250
The non-quantized flash path is selected when ALL of the following hold:
250251

251252
- The kernel specialization is `float` (FP16 uses the naive path)
252253
- `ORT_GQA_DISABLE_FLASH_ATTENTION` environment variable is not set (or set to `0`)
253-
- `sequence_length > 1` (prefill / chunked-prefill; single-token decode uses the naive path)
254+
- `total_sequence_length > 1`
254255
- No softcap
255256
- No smooth softmax
256257
- No head sink
257258
- No output QK capture
258259
- `present_key` and `present_value` are provided
259260

260-
Attention bias, causal masking, local window attention, GQA head grouping (`num_heads != kv_num_heads`), ragged per-batch sequence lengths, and shared past/present buffers are all supported, mirroring the quantized flash path. When any condition is not met, the kernel falls back to the naive full-materialization path.
261+
Attention bias, causal masking, local window attention, GQA head grouping (`num_heads != kv_num_heads`), ragged per-batch sequence lengths, and shared past/present buffers are all supported for prefill, mirroring the quantized flash path. The non-quantized flash path is currently selected for prefill only (`sequence_length > 1`); single-token decode falls back to the naive full-materialization path (a dedicated decode kernel is added in a follow-up change). When any supported condition is not met, the kernel also falls back to the naive path.
261262

262-
### Block Sizes and Threading
263+
### Block Sizes, Threading, and Flash Decoding
263264

264-
Block-size selection (`kv_block_size`, `q_block_size`), `(batch, head, q_block)` task partitioning, and the per-thread working buffer layout (`l`, `m`, `scores`, `temp_output`) are identical to the quantized path described above. The only difference is that the per-thread `temp_output` tile is accumulated directly by the SV SGEMM rather than via a fused dequantization. Because this path is prefill-only, it does not include the quantized path's two-phase flash-decoding strategy for single-token decode.
265+
Block-size selection (`kv_block_size`, `q_block_size`), `(batch, head, q_block)` task partitioning, and the per-thread working buffer layout (`l`, `m`, `scores`, `temp_output`) for prefill are identical to the quantized path described above. The only difference is that the per-thread `temp_output` tile is accumulated directly by the SV SGEMM rather than via a fused dequantization. The two-phase flash-decoding strategy for single-token decode is gated off for the non-quantized path in this PR (decode falls back to naive); it is enabled together with the dedicated decode kernel in a follow-up change.
265266

266267
## MLAS Dispatch Paths
267268

@@ -513,13 +514,13 @@ offsets the intrinsic per-KV-block online-softmax overhead (running max/exp/outp
513514
The same advantage holds single-threaded (1.4\u20131.8x at threads=1), confirming the gain is
514515
algorithmic rather than purely from threading.
515516

516-
#### Decode (S = 1, token generation)
517+
#### Latency — Decode (S = 1, token generation)
517518

518-
Single-token decode (`sequence_length == 1`) is **not** handled by the FP32 flash path; it falls
519-
back to the naive path. Decode produces only a `[1, total_sequence_length]` score row per head,
520-
so there is nothing to tile away, and the extra online-softmax bookkeeping made the flash kernel
521-
slower and noisier in practice. Restricting the flash path to prefill (`sequence_length > 1`) keeps
522-
the consistent prefill win without regressing decode.
519+
Single-token decode (`sequence_length == 1`) currently falls back to the naive path for the
520+
non-quantized FP32 cache: the flash path is gated on `sequence_length > 1` (prefill only),
521+
because routing the tiny `1 × T × H` decode work through the tiled SGEMM kernel pays
522+
per-block GEMM setup overhead with no tiling reuse benefit. A dedicated FP32 decode kernel
523+
is added in a follow-up change.
523524
## Current CPU Limitations
524525

525526
The current CPU GroupQueryAttention implementation has a few important limitations:

onnxruntime/contrib_ops/cpu/bert/group_query_attention.cc

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,10 @@ Status GroupQueryAttention<T>::Compute(OpKernelContext* context) const {
348348
// kernel to avoid materializing the full attention score matrix. Falls back to the
349349
// naive path when an unsupported feature is requested (softcap, smooth softmax,
350350
// head sink, or QK output).
351+
//
352+
// The flash path is currently used for prefill only (sequence_length > 1). Single-token
353+
// decode (sequence_length == 1) falls back to the naive path; a dedicated decode kernel
354+
// is added in a follow-up change.
351355
if constexpr (std::is_same_v<T, float>) {
352356
// Restrict the flash path to prefill / chunked-prefill (query length > 1). Single-token
353357
// decode (sequence_length == 1) has no flash benefit: the naive score matrix is only

0 commit comments

Comments
 (0)