HIP: fix turbo KV decode crash under graph capture; batch-aware VEC/TILE FA routing#176
Conversation
…ILE FA routing Route small-batch (decode) quantized-KV flash attention through the graph-safe VEC kernel and let large prefill batches fall through to the fast TILE/MMA kernel. Make the f16 dequant temp allocation capture-aware: allocate from the ggml pool while a stream is capturing (no cudaMalloc/cudaFree/cudaStreamSynchronize), keep raw alloc for large eager prefill so the multi-GB buffer is released immediately (gfx1201 has no VMM, the legacy pool would retain it). Fixes 'FLASH_ATTN_EXT failed: operation not permitted when stream is capturing' with GGML_HIP_GRAPHS=ON and turbo KV types on RDNA4. Tested on gfx1201 (Radeon AI PRO R9700, Windows, HIP SDK 7.1): pp2048 735 t/s (vs 188 t/s without graphs), tg128 22.9 t/s, no decode crash. Possibly related: TheTom#12.
|
Note for triage: #156 touches the same region (it removes hip_f16_alloc entirely by extending VEC D=512 coverage for quantized KV). The two approaches solve different problems and will conflict textually: removing the f16-dequant path forces quantized-KV prefill onto the sequential VEC kernel (the ~188 t/s path on gfx1201), while this PR keeps the fast TILE/MMA prefill (~735 t/s measured) and makes its temp allocation capture-aware instead. If #156 lands first I am happy to rebase; the capture-awareness here would still be needed for any remaining f16-dequant fallback. |
|
Thanks @KaiFelixBennett, the design reads right: the capture path adopts what the stock CUDA build already does unconditionally (pool alloc for K_f16/V_f16), so HIP-under-capture is upstream-proven behavior rather than something new, and the ne[1] <= 8 threshold covers decode plus MTP spec-decode while letting eager prefill keep the raw alloc/free that protects VRAM on gfx1201. One question before merging: the warmup-safety argument assumes the first eval at any new size runs eagerly before capture begins. Did you verify a fresh-context, first-token decode with GGML_HIP_GRAPHS=ON (not just steady-state)? If ggml ever captures the first eval at a new size, a cold pool miss would cudaMalloc during capture and reproduce the original crash on a different path. Minor, can be a follow-up: when decode cannot use VEC (head_dim 192 or a K stride mismatch), the captured TILE path pool-allocates the full dequant buffer and the legacy pool retains it permanently, which is the exact OOM the old raw-alloc comment defended against. Worth a sentence in the comment noting those configs trade memory for graph compatibility. |
Address review on TheTom#176: document that head_dim==192 / K-stride-mismatch configs fall through to the TILE/MMA path under capture and pool-alloc the full f16 dequant buffer, which the legacy pool retains permanently -- a VRAM tradeoff, not a crash. VEC-eligible head dims (Gemma) never hit this. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
@TheTom Verified both ways — mechanism and a cold-start run. ggml never captures the first eval at a new size. For VEC-eligible head dims (Gemma) the pool path isn't even in the captured graph — decode routes to the VEC kernel, which doesn't call Cold-start, instrumented (Gemma-4-31B, q8_0-K/turbo4-V,
Minor point: pushed a commit adding a sentence to the |
Address review on TheTom#176: document that head_dim==192 / K-stride-mismatch configs fall through to the TILE/MMA path under capture and pool-alloc the full f16 dequant buffer, which the legacy pool retains permanently -- a VRAM tradeoff, not a crash. VEC-eligible head dims (Gemma) never hit this.
f04d012 to
9b1536f
Compare
|
Both answered exactly as hoped. The warmup-gating trace is the key point: capture only fires on an eval whose sizes match the immediately preceding eager eval, so the pool is always warmed at the right size before any cudaMalloc-free capture, and a size change resets back to eager. That closes the cold-start window I was worried about, and your instrumented run (zero capture failures, captured decode reused after warmup) confirms it on real hardware. The added comment on the head_dim 192 / K-stride fallthrough is exactly right too: VRAM tradeoff, not a crash. Thanks for the thorough verification and the clean writeup. Merging. |
…ILE FA routing (TheTom#176) * HIP: fix turbo KV decode crash under graph capture; batch-aware VEC/TILE FA routing Route small-batch (decode) quantized-KV flash attention through the graph-safe VEC kernel and let large prefill batches fall through to the fast TILE/MMA kernel. Make the f16 dequant temp allocation capture-aware: allocate from the ggml pool while a stream is capturing (no cudaMalloc/cudaFree/cudaStreamSynchronize), keep raw alloc for large eager prefill so the multi-GB buffer is released immediately (gfx1201 has no VMM, the legacy pool would retain it). Fixes 'FLASH_ATTN_EXT failed: operation not permitted when stream is capturing' with GGML_HIP_GRAPHS=ON and turbo KV types on RDNA4. Tested on gfx1201 (Radeon AI PRO R9700, Windows, HIP SDK 7.1): pp2048 735 t/s (vs 188 t/s without graphs), tg128 22.9 t/s, no decode crash. Possibly related: TheTom#12. * fattn (HIP): note pool-retention tradeoff for non-VEC captured decode Address review on TheTom#176: document that head_dim==192 / K-stride-mismatch configs fall through to the TILE/MMA path under capture and pool-alloc the full f16 dequant buffer, which the legacy pool retains permanently -- a VRAM tradeoff, not a crash. VEC-eligible head dims (Gemma) never hit this. --------- Co-authored-by: KaiAtAdesso <KaiAtAdesso@users.noreply.github.com>
Full upstream catch-up (merge, not rebase: all fork commits + hashes + authors retained, merge keeps feature/turboquant-kv-cache as first parent). 45 files conflicted; resolved preserving all TurboQuant+, MTP, and contributor work. Verified on M5 Max (Metal): build green; turbo4/turbo3 KV track f16, turbo2 coherent. Preserved (verified in tree): - CUDA TurboQuant (signalnine/Gabe Ortiz): turbo2/3/4, TQ4_1S/TQ3_1S, WHT, sparse-V, InnerQ, MLA fixes, D=640 MMA FA. - HIP: variadic shfl macros, VEC-force, TheTom#176 graph-capture decode fix. - Metal: TQ3-rotated mul_mm + turbo_wht pipeline. - KV cache: auto-asymmetric turbo-K upgrade, default-off per-side attn-rotation policy, turbo head padding, n_layer_kv, +3 rotation overhead. - MTP: gemma4-assistant masked-embd, draft-MTP server multimodal, ctx_other. - Server: get_slot_by_cache_key unioned with upstream get_slot_by_cmpl_id. - Vulkan turbo3 KV-cache pipelines. Key resolutions (see TURBOQUANT_UPSTREAM_MERGE.md): - kv-cache ctor: union upstream v_cells_impl/shared-cells + fork auto-asymmetric turbo + n_layer_kv. - attn-rotation: fork default-off + per-side overrides, plus upstream's DeepSeek-V3.2 indexer force (guarded). - fattn-common.cuh: upstream f16_extra refactor (graph-capture-safe, supersedes the HIP pool workaround). - Took upstream for build/UI refactor, model evolution (n_layer_all rename, gated_delta_net, nextn/MTP), vocab/normalizer additions. Build fixes (auto-merge dual-additions / API drift): - GGML_OP_COUNT static_assert 97 -> 98 (TURBO_WHT). - duplicate n_outputs_max (llama.h, common.h, default-params init). - missing n_embd decl in output_reserve. - duplicate get_suppress_tokens, PROJECTOR_TYPE_GEMMA4UA, clip_graph_gemma4uv. - server get_available_slot signature reconciled with get_slot_by_cmpl_id. DEFERRED (tracked, not lost): Vulkan turbo3 flash-attention re-port — upstream's FA stack evolved past the fork's; took upstream, re-port + validate on the AMD RDNA4 box. Recovery commits in TURBOQUANT_UPSTREAM_MERGE.md. TODO: AMD RDNA4 Vulkan build + turbo3 FA re-port; 5090 CUDA build + turbo tests; M3 GGUF Config-I (this + upstream ggml-org#24523 M3 support).
With
GGML_HIP_GRAPHS=ON, any turbo KV cache type crashes on the first decode step on RDNA4:Two coupled causes:
launch_fattn's f16 dequant temp buffers (K_f16/V_f16) use rawcudaMalloc/cudaFree/cudaStreamSynchronize, which are illegal during graph capture.The raw alloc exists for a good reason on gfx1201 (no VMM): the legacy pool would retain the multi-GB dequant buffer permanently and negate the quantized-KV VRAM savings. So the fix has to be capture-aware rather than always-pool.
Changes
fattn.cu: only force VEC for small batches (Q->ne[1] <= 8). Decode stays on the graph-safe VEC path (inline dequant, no temp buffer); large prefill batches fall through to the TILE/MMA kernel.fattn-common.cuh: checkcudaStreamIsCapturing; allocateK_f16/V_f16from the ggml pool while capturing (pool alloc/free make no CUDA calls), keep raw alloc + immediate free for large eager prefill. This is safe because ggml resets graph warmup whenever tensor sizes change, so the pool buffer is warmed at the right size before any capture.Results
gfx1201 (Radeon AI PRO R9700, 32 GB), Windows 11, HIP SDK 7.1, Gemma-4-31B-it Q4_K_M:
Long context with
-b 2048 -ub 512: turbo3/turbo3 tg128 @ d131072 = 9.38 ± 0.93 t/s; the full 256K context loads in ~22.9 GB. Quality checked with KLD vs an f16 baseline and needle-in-a-haystack (9/9 for q8_0/turbo4 and turbo3/turbo3). Full methodology, raw data and a one-command repro build for gfx1201: https://github.com/KaiFelixBennett/gemma4-turboquant-rdna4Possibly related: #12 (turbo3 crash on an RX 9070 XT — same gfx1201 family, closed as stale).
Originally developed and benchmarked at 7d9715f; rebased onto 73eb521 (the fattn files were untouched in between) and built warning-free there. Can re-run any of the benchmarks on this hardware if useful.