perf: replace standard attention with unified flash attention#45
perf: replace standard attention with unified flash attention#45Skyrion9 wants to merge 2 commits into
Conversation
Replace the multi-step standard attention pipeline with a single ggml_flash_attn_ext call that handles all token counts uniformly. Causal masking: - For n_tokens > 1, build an F16 causal mask of shape [kv_len, n_tokens, 1, 1] on the CPU where mask[j][i] is ..-INFINITY if i > n_past_ + j and 0.0f otherwise, then upload it via ggml_backend_tensor_set before graph compute. Attention computation: - Remove the entire standard attention block: concat of past KV, repeat_interleave_heads for GQA, mul_mat for KQ scores, ..ggml_scale, ggml_diag_mask_inf, ggml_soft_max, and mul_mat for KQV. - Replaced with a single ggml_flash_attn_ext call using Q permuted to [head_dim, n_tokens, n_head, 1] and K/V cache ..views of shape [head_dim, kv_len, n_head_kv]. - Flatten the flash attention output to [q_size, n_tokens] for the wo projection via ggml_cpy. KV cache layout: - Swap dimensions 1 and 2 in init_kv_cache to match the view shape expected by ggml_flash_attn_ext. - Update token_off_k/token_off_v from nb[2] to nb[1] to reflect the new sequence-dimension stride. - Update k_slot/v_slot view shapes from [head_dim, n_head_kv, n_tokens] to [head_dim, n_tokens, n_head_kv]. - Add ggml_permute + ggml_cont on k and v before writing to the cache to transpose from the QKV projection output shape ..[head_dim, n_head_kv, n_tokens] to the cache layout [head_dim, n_tokens, n_head_kv]. - As a byproduct this fixes segfaults on Vulkan backend on Linux RADV (RDNA2) when trying to prefill_fast. Previously we had to use prefill ..with chunks instead to workaround segfaults, like due to ggml_soft_max limitations on ggml vulkan backend for this particular setup. - Significant performance gains due to FA's O(N) memory bandwidth cost as opposed to the standard O(N^2).
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthrough
ChangesFlash-attention integration
Estimated code review effort: 4 (Complex) | ~45 minutes Sequence Diagram(s)sequenceDiagram
participant eval_cached
participant KVCache
participant ggml_flash_attn_ext
participant Backend
eval_cached->>KVCache: write current K/V into cache views
eval_cached->>ggml_flash_attn_ext: pass permuted Q/K/V and fa_mask
ggml_flash_attn_ext-->>eval_cached: return attention output
eval_cached->>Backend: upload fa_mask before graph execution
Possibly related PRs
🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
🧹 Nitpick comments (1)
src/s2_model.cpp (1)
1046-1049: 🚀 Performance & Scalability | 🔵 Trivial | ⚡ Quick winPrefer reshape over
ggml_cpyfor the flash-attn output to avoid an extra dispatch.
attn_fais already contiguous in[head_dim, n_head, n_tokens, 1]layout (output ofggml_flash_attn_ext), so merging the first two dims viaggml_reshape_2d/ggml_cont_2davoids allocating a new tensor and performing a full copy. This directly furthers the PR's stated goal of reducing shader dispatches per layer.♻️ Proposed refactor
- // Output is [head_dim, n_head, n_tokens, 1], flatten to [q_size, n_tokens] for wo - ggml_tensor * attn_cur = ggml_cpy(ctx0, attn_fa, - ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, q_size, n_tokens)); + // Output is [head_dim, n_head, n_tokens, 1], flatten to [q_size, n_tokens] for wo + ggml_tensor * attn_cur = ggml_reshape_2d(ctx0, attn_fa, q_size, n_tokens);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/s2_model.cpp` around lines 1046 - 1049, Replace the ggml_cpy call constructing attn_cur with a reshape-based operation, using ggml_reshape_2d (or ggml_cont_2d if required by layout) to merge attn_fa’s head_dim and n_head dimensions into q_size while preserving n_tokens. Avoid allocating and copying the flash-attention output, and keep the resulting [q_size, n_tokens] shape for the subsequent wo path.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@src/s2_model.cpp`:
- Around line 1046-1049: Replace the ggml_cpy call constructing attn_cur with a
reshape-based operation, using ggml_reshape_2d (or ggml_cont_2d if required by
layout) to merge attn_fa’s head_dim and n_head dimensions into q_size while
preserving n_tokens. Avoid allocating and copying the flash-attention output,
and keep the resulting [q_size, n_tokens] shape for the subsequent wo path.
… an extra dispatch.
Replaces the standard (naive) attention implementation in
SlowARModel::eval_cachedwith a single unifiedggml_flash_attn_extcall for all token counts.The previous attention path used a multi-step standard attention pipeline:
repeat_interleave_headsto expand KV heads for GQAmul_matto compute QK scoresggml_scale+ggml_diag_mask_inf+ggml_soft_maxmul_matto compute KQV outputThis required 8+ separate GPU shader dispatches per layer and materialized the full$N \times N$ attention matrix in VRAM, creating an $O(N^2)$ memory bandwidth bottleneck.
Changes:
KV cache layout (
init_kv_cache):[head_dim, n_head_kv, max_seq_len, n_layer]to[head_dim, max_seq_len, n_head_kv, n_layer], placing the sequence dimension atne[1]to match the view shape expected byggml_flash_attn_ext.nb[2]tonb[1]accordingly.ggml_permute+ggml_contto transpose from the QKV projection output shape to the cache layout.Causal mask generation (
eval_cached):n_tokens > 1: an F16 causal mask of shape[kv_len, n_tokens, 1, 1]is built on the CPU and uploaded to the backend before graph compute. Entry[j][i]is-INFINITYifi > n_past_ + j,0.0fotherwise.n_tokens == 1:fa_maskremainsnullptr. This is not a separate code path — the sameggml_flash_attn_extcall executes regardless of token count.Attention computation (
eval_cached):ggml_flash_attn_extcall.[head_dim, n_tokens, n_head, 1]; K/V cache views are[head_dim, kv_len, n_head_kv].[q_size, n_tokens]for thewoprojection.As a "sideffect" #38 is no longer needed for Linux Radv (Vulkan) on RDNA2 - RX 6700 to function without segfaulting when using prefill_fast, likely due to ggml_soft_max limitations on ggml vulkan backend for this particular setup.
Impact:
Benchmarks:
Can be compared as the improvement over #44 as those graphs implemented the deprecated implementation of FA #40 which wasn't uniform in its approach. You can see that we're running ~20% faster across Total RTF if you compare the R1/R2 results. As before, the non-R1/R2 runs are upstream using prefill instead of prefill_fast as fast_prefill was unusable on my system before this PR.
Summary by CodeRabbit
Summary by CodeRabbit