Skip to content

feat: Flash Attention and modernized KV Cache layout for Slow-AR#40

Open
Skyrion9 wants to merge 3 commits into
rodrigomatta:mainfrom
Skyrion9:FA
Open

feat: Flash Attention and modernized KV Cache layout for Slow-AR#40
Skyrion9 wants to merge 3 commits into
rodrigomatta:mainfrom
Skyrion9:FA

Conversation

@Skyrion9

@Skyrion9 Skyrion9 commented Jul 15, 2026

Copy link
Copy Markdown
  • 17% speedup on Q8 (per-frame ms)
  • Allows for longer text input before OOM. For example, 593 prefill + 1000/1024 tokens crashes on non-FA version, while FA consumes far less VRAM allowing for successful completion (10 GB RX 6700 Vulkan, 1.5 GB background usage, Archlinux)

The only downside is that prefill takes significantly longer unless you're also using #39 with which it's negligible.

Here are some numbers:
WITH #39 (Notice smaller prefill gap):
image

FA run #39 :

[Metrics] Init: tokenizer=232,325471 ms, model=20,368374 ms, codec=11200,593645 ms (Vulkan0), total=11458,379596 ms, max_rss=631,917969 MB
Loading voice profile: 69
Loaded voice profile: 69 (134 frames)
--- Pipeline Synthesize ---
Text: hello world, how are you today?
[Model] KV cache allocated on Vulkan0, size: 172.688 MB
[Generate] Prefilling 204 tokens...
[Model] Chunked prefill: 204 tokens in blocks of 64
[Generate] Generating (max 1024 tokens)...
[Generate] 50 / 1024 tokens...
[Generate] Done: 58 frames generated.
[Metrics] Generate: prefill=378.19 ms, loop=3241.61 ms, total=3622.34 ms, avg=55.8898 ms/frame
[Metrics] Synthesis: frames=58, audio_s=2,693515, ref_encode=0,000000 ms, kv_init=4,852855 ms, generate=3622,358779 ms, decode=652,069728 ms, decode_wall=652,103110 ms, decode_batches=1, decode_stride=16 frames, total=4279,762568 ms, gen_avg=62,454462 ms/frame, total_avg=73,789010 ms/frame, gen_rtf=1,344845, total_rtf=1,588914, max_rss=631,917969 MB

no FA run #39 :

[Metrics] Init: tokenizer=244,556109 ms, model=19,648296 ms,
codec=11091,179776 ms (Vulkan0), total=11357,319675 ms,
max_rss=633,492188 MB
Loading voice profile: 69
Loaded voice profile: 69 (134 frames)
--- Pipeline Synthesize ---
Text: hello world, how are you today?
[Model] KV cache allocated on Vulkan0, size: 172.688 MB
[Generate] Prefilling 204 tokens...
[Generate] Generating (max 1024 tokens)...
[Generate] 50 / 1024 tokens...
[Generate] Done: 55 frames generated.
[Metrics] Generate: prefill=274.399 ms, loop=3596.74 ms, total=3873.83 ms, avg=65.3952 ms/frame
[Metrics] Synthesis: frames=55, audio_s=2,554195, ref_encode=0,000000
ms, kv_init=7,245439 ms, generate=3873,850302 ms, decode=614,090191 ms,
decode_wall=614,120076 ms, decode_batches=1, decode_stride=16 frames,
total=4495,618254 ms, gen_avg=70,433642 ms/frame, total_avg=81,738514
ms/frame, gen_rtf=1,516662, total_rtf=1,760092, max_rss=633,492188 MB

WITHOUT #39 (bigger prefill gap which #39 remedies & longer text showcases growing gap with FA ahead) :
image

FA run:

[Metrics] Init: tokenizer=234,274231 ms, model=19,048437 ms,
codec=11234,416737 ms (Vulkan0), total=11489,671653 ms,
max_rss=631,949219 MB
Loading voice profile: 69
Loaded voice profile: 69 (134 frames)
--- Pipeline Synthesize ---
Text: hello world, how are you today? I hope you're doing good,
relatively speaking. What is doing good and who came up with it anyways?
[Model] KV cache allocated on Vulkan0, size: 175.781 MB
[Generate] Prefilling 226 tokens...
[Model] Chunked prefill: 226 tokens in blocks of 8
[Generate] Generating (max 1024 tokens)...
[Generate] 200 / 1024 tokens...
[Generate] Done: 206 frames generated.
[Metrics] Generate: prefill=963.529 ms, loop=11083.1 ms, total=12049.1 ms, avg=53.8013 ms/frame
[Metrics] Synthesis: frames=206, audio_s=9,566621, ref_encode=0,000000
ms, kv_init=8,704014 ms, generate=12049,148369 ms, decode=10677,101339
ms, decode_wall=10677,214061 ms, decode_batches=5, decode_stride=16
frames, total=22735,440749 ms, gen_avg=58,491011 ms/frame,
total_avg=110,366217 ms/frame, gen_rtf=1,259499, total_rtf=2,376538,
max_rss=631,949219 MB

no FA run:

[Metrics] Init: tokenizer=237,676438 ms, model=19,685375 ms,
codec=11381,829297 ms (Vulkan0), total=11641,658635 ms,
max_rss=631,691406 MB
Loading voice profile: 69
Loaded voice profile: 69 (134 frames)
--- Pipeline Synthesize ---
Text: hello world, how are you today? I hope you're doing good,
relatively speaking. What is doing good and who came up with it anyways?
[Model] KV cache allocated on Vulkan0, size: 175.781 MB
[Generate] Prefilling 226 tokens...
[Generate] Generating (max 1024 tokens)...
[Generate] 200 / 1024 tokens...
[Generate] Done: 220 frames generated.
[Metrics] Generate: prefill=308.885 ms, loop=15580.2 ms, total=15891.7 ms, avg=70.8192 ms/frame
[Metrics] Synthesis: frames=220, audio_s=10,216780, ref_encode=0,000000
ms, kv_init=5,445922 ms, generate=15891,673671 ms, decode=13403,478807
ms, decode_wall=13403,608681 ms, decode_batches=6, decode_stride=16
frames, total=29301,117276 ms, gen_avg=72,234880 ms/frame,
total_avg=133,186897 ms/frame, gen_rtf=1,555448, total_rtf=2,867941,
max_rss=631,691406 MB
Saved audio to: output.wav

As you can see, the gap increases between them the longer your text is. FA doesn't only allow longer text with smaller memory footprint it also speeds things up. If you TTS many short sentences consecutively you will definitely want #39 .

First graph text was

hello world, how are you today?

Second graph text was

hello world, how are you today? I hope you're doing good, relatively speaking. What is doing good and who came up with it anyways?

https://hazyresearch.stanford.edu/blog/2023-01-12-flashattention-long-sequences

Summary by CodeRabbit

Release Notes

  • Performance
    • Improved attention execution for single-token generation with a dedicated fast path.
    • Enhanced key-value cache layout and update behavior to improve attention throughput and consistency.
    • Refined handling of cached past context for more efficient attention computation.
  • Compatibility
    • Maintains existing model weight placement and hardware offload behavior.
  • Documentation
    • Updated notes clarifying the key-value cache layout used to support accelerated attention.

- 17% speedup on Q8  (per-frame ms)
- Allows for longer text input before OOM. For example, 593 prefill + 1000/1024 tokens crashes on non-FA version, while FA consumes far less VRAM allowing for successfuly completion (10 GB RX 6700 Vulkan, 1.5 GB background usage, Archlinux)
@coderabbitai

coderabbitai Bot commented Jul 15, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: b9dacead-cf47-4798-b03b-db80f39c3075

📥 Commits

Reviewing files that changed from the base of the PR and between bd3909d and e7506ba.

📒 Files selected for processing (1)
  • src/s2_model.cpp
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/s2_model.cpp

📝 Walkthrough

Walkthrough

src/s2_model.cpp reorders KV-cache tensor dimensions and refactors cached evaluation to write permuted keys and values, using flash attention for single-token evaluation and a prefill/concat path for multiple tokens.

Changes

Attention cache execution

Layer / File(s) Summary
KV-cache layout setup
src/s2_model.cpp
KV-cache allocation dimensions are reordered and annotated for flash-attention layout.
Cached attention and cache writes
src/s2_model.cpp
eval_cached updates graph boundaries, cache offsets, and cache writes, then selects ggml_flash_attn_ext for single-token evaluation or the prefill/concat-style path for multiple tokens.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant eval_cached
  participant memory_k_v
  participant ggml_flash_attn_ext
  eval_cached->>memory_k_v: compute offsets and write permuted k/v
  alt n_tokens == 1
    eval_cached->>ggml_flash_attn_ext: compute single-token attention
  else n_tokens != 1
    eval_cached->>memory_k_v: build prefill/concat attention inputs
    memory_k_v-->>eval_cached: provide attention inputs
  end
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: Flash Attention plus a revised KV cache layout for Slow-AR.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
src/s2_model.cpp (2)

1029-1033: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low value

Redundant permute in the single-token flash path.

Since this branch only runs when n_tokens == 1, attn_merged = ggml_permute(ctx0, attn_fa, 0, 2, 1, 3) swaps a size-1 axis with the head axis, which cannot change element ordering. It only adds a non-contiguous view that ggml_cpy then has to stride through. attn_fa can likely be copied directly.

♻️ Proposed simplification
-            ggml_tensor * attn_merged = ggml_permute(ctx0, attn_fa, 0, 2, 1, 3);
-            
-            // ggml_cpy safely handles reading from the permuted view into a fresh 2D tensor.
-            attn_cur = ggml_cpy(ctx0, attn_merged,
-                ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, q_size, n_tokens));
+            // attn_fa is [head_dim, n_head, n_tokens=1, 1]; with n_tokens==1 no reorder is needed.
+            attn_cur = ggml_cpy(ctx0, attn_fa,
+                ggml_new_tensor_2d(ctx0, GGML_TYPE_F32, 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 1029 - 1033, In the single-token
flash-attention path around attn_merged, remove the redundant ggml_permute call
and copy directly from attn_fa into the existing fresh 2D F32 tensor via
ggml_cpy. Preserve the current output shape and attn_cur assignment.

481-497: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Inconsistent indentation violates the 4-space rule.

The full_model_offload/get_weight_backend block (Lines 481-497) and the layer-loop opening (Line 964) mix 0/4/8/12/16-space indentation for logically equivalent nesting levels. Logic itself is unchanged (same routing rules per the AI summary), this is purely a formatting regression introduced by the reformat.

🧹 Proposed cleanup
-        const bool full_model_offload =
-            backend_gpu_ != nullptr &&
-            backend_type != BackendType::CUDA &&
-                        n_gpu_layers_ == hparams_.block_count;
-
-        auto get_weight_backend = [&](const std::string & name) -> ggml_backend_t {
-            
-            if (full_model_offload) {
-            return backend_gpu_;
-        }
-
-                if (name == "embeddings.weight" ||
-                    name == "codebook_embeddings.weight" ||
-                    name == "fast_embeddings.weight" ||
-                    name == "norm.weight") {
-                    return backend_cpu_;
-                            }
+    const bool full_model_offload =
+        backend_gpu_ != nullptr &&
+        backend_type != BackendType::CUDA &&
+        n_gpu_layers_ == hparams_.block_count;
+
+    auto get_weight_backend = [&](const std::string & name) -> ggml_backend_t {
+        if (full_model_offload) {
+            return backend_gpu_;
+        }
+
+        if (name == "embeddings.weight" ||
+            name == "codebook_embeddings.weight" ||
+            name == "fast_embeddings.weight" ||
+            name == "norm.weight") {
+            return backend_cpu_;
+        }

As per coding guidelines, "Use 4-space indentation in C++ code" for **/*.{cpp,h,hpp}.

Also applies to: 964-964

🤖 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 481 - 497, Normalize indentation to 4 spaces
throughout the full_model_offload declaration and get_weight_backend lambda,
including its conditional blocks and returns, without changing routing logic.
Apply the same 4-space indentation rule to the layer-loop opening around the
referenced loop symbol.

Source: Coding guidelines

🤖 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 1029-1033: In the single-token flash-attention path around
attn_merged, remove the redundant ggml_permute call and copy directly from
attn_fa into the existing fresh 2D F32 tensor via ggml_cpy. Preserve the current
output shape and attn_cur assignment.
- Around line 481-497: Normalize indentation to 4 spaces throughout the
full_model_offload declaration and get_weight_backend lambda, including its
conditional blocks and returns, without changing routing logic. Apply the same
4-space indentation rule to the layer-loop opening around the referenced loop
symbol.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: efe5bef9-7633-445d-9a31-6c862f88cb47

📥 Commits

Reviewing files that changed from the base of the PR and between 2c33261 and 1dec132.

📒 Files selected for processing (1)
  • src/s2_model.cpp

…depath.

- Copying attn_fa directly
into the [q_size, 1] destination is mathematically equivalent and
removes a useless node from the compute graph.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/s2_model.cpp (1)

1064-1064: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the V permutation in both attention paths. ggml_mul_mat_checked(V, KQf, ...) needs V->ne[0] to match KQf->ne[0], but ggml_permute(ctx0, v_rep, 1, 2, 0, 3) makes V start with n_head instead of n_tokens. Use 2, 0, 1, 3 here and at src/s2_model.cpp:1231.

🤖 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` at line 1064, Update the V tensor permutation in both
attention paths, at the visible V construction and its corresponding path near
the second occurrence, changing the permutation order to 2, 0, 1, 3 so V->ne[0]
matches KQf->ne[0] (n_tokens). Preserve the surrounding ggml_cont and matrix
multiplication logic.
🤖 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.

Outside diff comments:
In `@src/s2_model.cpp`:
- Line 1064: Update the V tensor permutation in both attention paths, at the
visible V construction and its corresponding path near the second occurrence,
changing the permutation order to 2, 0, 1, 3 so V->ne[0] matches KQf->ne[0]
(n_tokens). Preserve the surrounding ggml_cont and matrix multiplication logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a8a1e620-8a12-4466-a010-68b902f0e8cb

📥 Commits

Reviewing files that changed from the base of the PR and between 1dec132 and e62f190.

📒 Files selected for processing (1)
  • src/s2_model.cpp

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/s2_model.cpp (1)

1009-1027: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Make the KV-cache write a dependency of flash attention src/s2_model.cpp:1009-1027

ggml_build_forward_expand() follows data-flow, not aliasing, so ggml_flash_attn_ext() can be scheduled before the ggml_cpy() into memory_k_ / memory_v_ completes. Chain the copy outputs into the flash-attn path, or add an explicit dependency, and cover both n_past_ == 0 and n_past_ > 0.

🤖 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 1009 - 1027, Make the KV-cache writes created
by the ggml_cpy calls in the n_tokens == 1 flash-attention path explicit
dependencies of ggml_flash_attn_ext, rather than relying on memory aliasing.
Ensure both memory_k_ and memory_v_ copies complete before attention for n_past_
== 0 and n_past_ > 0, while preserving the existing cache views and attention
behavior.
🧹 Nitpick comments (1)
src/s2_model.cpp (1)

964-964: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fix the loop indentation.

Line 964 is indented by 8 spaces while the surrounding function body uses 4-space indentation. Align the for statement with the other top-level statements in eval_cached.

🤖 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` at line 964, Correct the indentation of the for loop in
eval_cached so it uses 4 spaces and aligns with other top-level statements in
the function; do not change the loop logic.

Source: Coding guidelines

🤖 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.

Outside diff comments:
In `@src/s2_model.cpp`:
- Around line 1009-1027: Make the KV-cache writes created by the ggml_cpy calls
in the n_tokens == 1 flash-attention path explicit dependencies of
ggml_flash_attn_ext, rather than relying on memory aliasing. Ensure both
memory_k_ and memory_v_ copies complete before attention for n_past_ == 0 and
n_past_ > 0, while preserving the existing cache views and attention behavior.

---

Nitpick comments:
In `@src/s2_model.cpp`:
- Line 964: Correct the indentation of the for loop in eval_cached so it uses 4
spaces and aligns with other top-level statements in the function; do not change
the loop logic.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 516efcc0-2024-44f4-ba1b-e76993d42c59

📥 Commits

Reviewing files that changed from the base of the PR and between e62f190 and bd3909d.

📒 Files selected for processing (1)
  • src/s2_model.cpp

@Skyrion9

Copy link
Copy Markdown
Author

src/s2_model.cpp (1)> 1064-1064: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Fix the V permutation in both attention paths. ggml_mul_mat_checked(V, KQf, ...) needs V->ne[0] to match KQf->ne[0], but ggml_permute(ctx0, v_rep, 1, 2, 0, 3) makes V start with n_head instead of n_tokens. Use 2, 0, 1, 3 here and at src/s2_model.cpp:1231.

🤖 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` at line 1064, Update the V tensor permutation in both
attention paths, at the visible V construction and its corresponding path near
the second occurrence, changing the permutation order to 2, 0, 1, 3 so V->ne[0]
matches KQf->ne[0] (n_tokens). Preserve the surrounding ggml_cont and matrix
multiplication logic.

AI seems to be confusing pytorch permute with ggml permute. When I tried its suggestion anyways it failed.

[Model] KV cache allocated on Vulkan0, size: 172.688 MB
[Generate] Prefilling 204 tokens...
mul_mat:kqv transposed=0 a=(32,204,128,1) b=(204,204,32,1)
/home/otto/Tools/Scripts/piper-tts-selection/s2.cpp/ggml/src/ggml.c:3262: GGML_ASSERT(ggml_can_mul_mat(a, b)) failed
[New LWP 37688]
[New LWP 37687]

This GDB supports auto-downloading debuginfo from the following URLs:
  <https://debuginfod.archlinux.org>
  <https://debuginfod.cachyos.org>
Enable debuginfod for this session? (y or [n]) [answered N; input not from terminal]
Debuginfod has been disabled.
To make this setting permanent, add 'set debuginfod enabled off' to .gdbinit.
[Thread debugging using libthread_db enabled]
Using host libthread_db library "/usr/lib/libthread_db.so.1".
0x00007fb2a36b4b42 in ?? () from /usr/lib/libc.so.6
#0  0x00007fb2a36b4b42 in ?? () from /usr/lib/libc.so.6
#1  0x00007fb2a36a7e78 in ?? () from /usr/lib/libc.so.6
#2  0x00007fb2a372f46b in wait4 () from /usr/lib/libc.so.6
#3  0x000056048177a867 in ggml_print_backtrace ()
#4  0x000056048177aaf2 in ggml_abort ()
#5  0x000056048177d897 in ggml_mul_mat ()
#6  0x00005604813e0373 in s2::SlowARModel::eval_cached(std::vector<int, std::allocator<int> > const&, int, int, s2::StepResult&) ()
#7  0x00005604813e14e9 in s2::SlowARModel::prefill(std::vector<int, std::allocator<int> > const&, int, int, s2::StepResult&) ()
#8  0x00005604813fe33a in s2::generate(s2::SlowARModel&, s2::TokenizerConfig const&, s2::PromptTensor const&, s2::GenerateParams const&) ()
#9  0x0000560481408c44 in s2::Pipeline::synthesize_prompt_codes_locked(s2::PipelineParams const&, int const*, int, std::vector<float, std::allocator<float> >&, double) ()
#10 0x0000560481407861 in s2::Pipeline::synthesize_raw(s2::PipelineParams const&, s2::AudioData&, std::vector<float, std::allocator<float> >&) ()
#11 0x0000560481406e6c in s2::Pipeline::synthesize(s2::PipelineParams const&) ()
#12 0x000056048147edc7 in main ()
[Inferior 1 (process 37686) detached]
./cmd_test.sh: line 12: 37686 Aborted                    (core dumped) ./s2 -m models/s2-pro-q8_0.gguf -t models/tokenizer.json -v 0 --threads 8 --temp 0.8 --top-p 0.95 --voice 69 --codec-context-frames 128 --codec-follow-backend -text "hello world, how are you today?" -o output.wav

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant