Skip to content

ggml-cuda: enable MMA FlashAttention for head dim 256 on AMD RDNA - #26419

Open
srgtuszy wants to merge 6 commits into
ggml-org:masterfrom
srgtuszy:opt/rdna-wmma-dkq256
Open

srgtuszy wants to merge 6 commits into
ggml-org:masterfrom
srgtuszy:opt/rdna-wmma-dkq256

Conversation

@srgtuszy

@srgtuszy srgtuszy commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Overview

Fixes #26220

After the removal of rocWMMA Flash Attention kernel path, the replacement had slower prompt processing at deep context on RDNA 4. The regression was caused by WMMA tensor cores not being used at all for head dim 256.

This PR widens flash_attn_ext_f16 device guard to unblock MMA at head dim 256. Without it, the tile kernel is used instead which works, but slows down with more context. The config table also needed a precision fix for DKQ=256. nbatch_combine was too small, causing multi-pass FP16 accumulation errors in test-backend-ops.

Additionally, for DKQ>128 on WMMA K/V is loaded directly from VRAM instead of going through LDS. Without it, MMA is actually slower than the tile kernel.

Additional information

Below are benchmark numbers to confirm no regressions and the speedup. It's worth to mention that, while prompt processing is faster than current master(a7a6d0d), it's still about 12-20% slower the old rocWMMA kernel depending on context size. There's definitely room for further improvement.

GPU: AMD Radeon AI PRO R9700 (gfx1201, RDNA4), ROCm 7.2.4
Baseline: master a7a6d0d, 3 runs per data point

test-backend-ops -o FLASH_ATTN_EXT: 2920/2920 passed

Qwen3.6-35B-A3B Q4_K_M (head dim 256)

Benchmark Baseline (t/s) PR (t/s) Delta
pp4096 @ d0 2580 +/- 14 2592 +/- 12 +0.4%
pp4096 @ d16384 1679 +/- 3 1907 +/- 8 +13.6%
pp4096 @ d65536 832 +/- 0.4 990 +/- 0.6 +19.1%
pp4096 @ d126976 511 +/- 0.02 623 +/- 0.4 +21.8%
tg64 @ d0 87.1 +/- 1.7 86.9 +/- 1.7 -0.2%
tg64 @ d16384 80.9 +/- 1.4 81.0 +/- 1.4 +0.1%
tg64 @ d65536 69.0 +/- 0.9 69.0 +/- 0.8 0.0%
tg64 @ d126976 58.6 +/- 0.7 58.5 +/- 0.6 -0.3%

Why LDS bypass is needed (Qwen3.6-35B-A3B, MMA enabled but without LDS bypass)

Benchmark Baseline/tile (t/s) MMA without LDS bypass (t/s) Delta
pp4096 @ d0 2584 2536 -1.9%
pp4096 @ d16384 1680 1719 +2.3%
pp4096 @ d65536 831 761 -8.4%
pp4096 @ d126976 510 458 -10.3%

Llama-3.1-8B-Instruct Q4_K_M (head dim 128, regression check)

Benchmark Baseline (t/s) PR (t/s) Delta
pp4096 @ d0 3817 +/- 3 3830 +/- 2 +0.3%
pp4096 @ d16384 2068 +/- 5 2077 +/- 3 +0.4%
pp4096 @ d65536 917 +/- 0.3 917 +/- 0.3 0.0%
pp4096 @ d126976 539 +/- 0.6 539 +/- 0.6 0.0%
tg64 @ d0 99.3 +/- 0.4 99.6 +/- 0.5 +0.4%
tg64 @ d16384 74.8 +/- 0.1 75.0 +/- 0.2 +0.3%
tg64 @ d65536 42.7 +/- 0.04 42.7 +/- 0.06 0.0%
tg64 @ d126976 27.8 +/- 0.02 27.8 +/- 0.02 0.0%

Requirements

  • I have read and agree with the contributing guidelines
  • AI usage disclosure: YES, used antigravity harness with Gemini/Opus to write the code, ran the benchmarks to confirm the numbers and tested the changes locally using llama server with the same model used to run the benchmarks.

@srgtuszy
srgtuszy requested a review from a team as a code owner August 1, 2026 22:43
@github-actions github-actions Bot added ggml changes relating to the ggml tensor library for machine learning CUDA Related to the CUDA backend labels Aug 1, 2026
@williamtwomey

Copy link
Copy Markdown

Tested on 2 x R9700 (gfx1201), ROCm 7.2.1, kernel 6.17, Qwen3.5-122B-A10B IQ3_XXS (head dim 256, GQA 32/2), -fa 1 -ctk q8_0 -ctv q8_0 -ngl 99 -sm layer -ts 0.9/1.1. PR head d273e0b. Baselines are prior runs of the same harness: b10154 is post-removal (tile kernel), b10182 is master with fa72aeccb reverse-applied to restore rocWMMA.

Speedup reproduces. pp512 tok/s, -r 3:

build @d0 @d16384 @d65536
b10154 (tile) 1074.4 684.7 344.9
b10182 (rocWMMA restored) 1088.8 896.1 571.0
this PR 1097.0 801.8 404.2

pp4096 tok/s, -r 2:

build @d0 @d16384 @d65536
b10154 (tile) 1699.8 1153.5 565.1
b10182 (rocWMMA restored) 1743.0 1488.8 964.7
this PR 1731.2 1347.1 662.4

Against b10154 that is +2.1/+17.1/+17.2% at pp512 and +1.8/+16.8/+17.2% at pp4096. tg128 is flat (48.75/42.79/29.71 vs 48.69/42.58/29.73), expected since batch-1 decode does not clear the new > 64 gate.

rocprofv3 --kernel-trace at d65536, 1560 attention calls in each, confirming the MMA path is actually live and not just exporting symbols:

build kernel total
b10043 (old rocWMMA) flash_attn_ext_f16<256, 16, 4, 64, float, false> 25.4s
b10154 flash_attn_tile<256, 256, 4, 8, false> 65.2s
this PR flash_attn_ext_f16<256, 256, 8, 8, false, false> 49.8s

Gap to rocWMMA is wider than 12-20% here

Against b10182: +0.7/-10.5/-29.2% at pp512, -0.7/-9.5/-31.3% at pp4096. As a fraction of the rocWMMA deficit recovered, 55% at 16k and 26% at 65k for pp512, 58% and 24% for pp4096.

I assumed pp512 was penalising the MMA kernel, but pp4096 gives the same answer, so it is not batch size. What is left against your setup is q8_0 KV vs f16, and IQ3_XXS 122B vs Q4_K_M 35B. If the quantised KV path is the cause that is worth knowing, since kv-quant at long context is common on 32GB cards.

Not an argument against merging. +17% at depth with no tg regression is a clear win over the status quo.

One correctness failure at head dim 192

test-backend-ops -o FLASH_ATTN_EXT passes 2920/2920 on device 0, matching your result. On one run device 1 returned 2918/2920:

FLASH_ATTN_EXT(hsk=192,hsv=128,nh=4,nr23=[16,1],kv=512,nb=75,mask=1,sinks=1,
  max_bias=0.000000,logit_softcap=0.000000,prec=f32,type_K=f16,type_V=f16,
  permute=[0,1,2,3]): FAIL   ERR = 0.001280 > 0.000500
  permute=[0,2,1,3]): FAIL   ERR = 0.013859 > 0.000500

Head dim 192, not 256. Widening the host gate to <= 256 also routes 192 into the MMA path, where master sent it to the tile kernel.

The reproduction evidence is thin and I would rather say so than dress it up: 1 failure in 3 full two-device runs on a build carrying a local allocator patch of mine, and 0 failures in 4 runs on a clean build of this PR. At a 1-in-3 rate, 4 clean runs happen about 20% of the time, so that A/B settles nothing in either direction.

What made me look is source-level and does not depend on my build. On the DKQ > 128 path with nstages <= 1, K and V no longer transit LDS but tile_mask still does. Per iteration of flash_attn_ext_f16_iter:

  1. load_mask writes tile_mask
  2. the __syncthreads() this PR adds, which covers the read-after-write
  3. KQ, no barrier
  4. softmax reads tile_mask (lines 731 and 797)
  5. VKQ, no barrier
  6. end of iteration, __syncthreads() at line 1063 now skipped by if (DKQ <= 128)
  7. next iteration load_mask writes tile_mask again

Nothing sits between 4 and 7. Both barriers that used to cover that window (698 and 1063) are now behind DKQ <= 128. If the write-after-read there is genuinely safe for some reason I am missing, then the failure is more likely mine and I will chase it on my side.

Happy to test patches for either. Both builds are deployed side by side here.

@IMbackK IMbackK self-assigned this Aug 2, 2026
srgtuszy added a commit to srgtuszy/llama.cpp that referenced this pull request Aug 2, 2026
When DKQ > 128 on AMD WMMA, K/V loads bypass LDS so the end-of-loop
__syncthreads() barriers were skipped entirely. However, tile_mask still
transits LDS and is read during softmax. Without a barrier at the end of
the VKQ section, the next iteration's load_mask can overwrite tile_mask
while a slow warp is still reading it from the current iteration.

Fix: keep the __syncthreads() at the end of the V loop when the mask is
active (ncols2 > 1 || mask_h), even when DKQ > 128.

Reported-by: williamtwomey (PR ggml-org#26419 review)
Tested: 5 consecutive runs of test-backend-ops -o FLASH_ATTN_EXT on
gfx1201, 2920/2920 passed each time (14600/14600 total, 0 failures).
@srgtuszy
srgtuszy requested a review from ggerganov as a code owner August 2, 2026 21:11
@github-actions github-actions Bot added the testing Everything test related label Aug 2, 2026
@srgtuszy

srgtuszy commented Aug 2, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for testing @williamtwomey! You're right, when DKQ > 128, the LDS bypass skips the end-of-loop __syncthreads() entirely, but tile_mask still lives in LDS. Fixed the race condition in d76c004 and verified with 5 consecutive runs of test-backend-ops -o FLASH_ATTN_EXT.

torhve pushed a commit to torhve/llama.cpp that referenced this pull request Aug 17, 2026
When DKQ > 128 on AMD WMMA, K/V loads bypass LDS so the end-of-loop
__syncthreads() barriers were skipped entirely. However, tile_mask still
transits LDS and is read during softmax. Without a barrier at the end of
the VKQ section, the next iteration's load_mask can overwrite tile_mask
while a slow warp is still reading it from the current iteration.

Fix: keep the __syncthreads() at the end of the V loop when the mask is
active (ncols2 > 1 || mask_h), even when DKQ > 128.

Reported-by: williamtwomey (PR ggml-org#26419 review)
Tested: 5 consecutive runs of test-backend-ops -o FLASH_ATTN_EXT on
gfx1201, 2920/2920 passed each time (14600/14600 total, 0 failures).
@toboriak

Copy link
Copy Markdown

Another data point, maybe it is useful: Validation on gfx1201 (Radeon AI PRO R9700): +62% prefill at 100k depth.

Setup

  • Radeon AI PRO R9700 (Navi 48, gfx1201, RDNA4), single device (-sm none), ROCm 10.1.0a20260822 (TheRock nightly)
  • Qwen3.8-27B (head_dim 256), Q6_K, fully GPU-resident, --ctx-size 262144, -fa on -ctk q8_0 -ctv q8_0, -b/-ub 1024, --parallel 1
  • Base: b10604 (bf0a29cc). This PR's three commits were cherry-picked onto that exact base, so the patch is the only variable between the two binaries.
  • One 100,002-token prompt, identical token sequence in both runs, unique prefix so nothing hits the prompt cache.

Results

b10604 + this PR
prefill @ 100,002 tok (aggregate) 362.38 t/s 587.77 t/s +62.2%
wall time for that prefill (tokens ÷ the rate above) 276.0 s 170.1 s −106 s
token generation 16.77 t/s 16.77 t/s unchanged
VRAM: model / KV / compute 19625 / 8853 / 1696 MiB identical

Decomposing per-token prefill cost as a + b·d (constant term + attention-over-KV), fitted from the per-ubatch print_timing lines at ~5k and ~90–97k depth:

a (ms/tok) b (ns/tok per token of depth)
b10604 1.123 31.5
+ this PR 1.136 10.9

It reads as a pure depth-term change: a is unchanged within fit noise while b drops 2.9×. The two-parameter model reproduces measured total prefill time to ~1% at every depth I sampled. At ~97k depth the marginal rate roughly doubles (240 t/s measured → ~457 t/s from the fit).

Extrapolated to this model's full 262,144-token context: ~23 min → ~11 min which is a big win for me. For now, I measured throughput, not correctness. I have not run perplexity or diffed outputs against the unpatched build, so please don't read this as validation of the kernel's numerics.

Quick question about DKQ > 256: The guard here lands at DKQ <= 256. Models using MLA report a larger head dim e.g. DeepSeek-V4-Flash on my box report n_embd_head_k = n_embd_head_v = 512 which means they sit outside the widened guard and get none of this. Is extending toward 512 feasible in the same shape? I have both a 256-dim and a 512-dim model resident on my hardware and am happy to test a patch if that would be useful.

@Stoney49th

This comment was marked as outdated.

@Stoney49th

Stoney49th commented Aug 26, 2026 •

Copy link
Copy Markdown

with 2x R9700, powercapped at 230W each, with the docker config and model config below, actual long context tests with 500 tokens minimum, i get the following figures. I did not re-measure PP between different MTP configs since there is no effect of n-max during PP. RCCL + Graphs enabled. The Master baseline is eab8ee4. For the master baseline WMMA_FATTN was enabled, and removed for the PR test

Compose and Model Config

  llama-server-rocm:
    build:
      context: .
      target: full
      dockerfile: .devops/rocm.Dockerfile
      args:
        - ROCM_DOCKER_ARCH='gfx1201'
    container_name: llamacpp-rocm
    ipc: host
    networks:
      - ai_stack
    ports:
      - "11437:8080"    
    environment:
      - GGML_CUDA_P2P=true
    volumes:
      - /mnt/workdrive/llamacpp/models:/models
      - /mnt/workdrive/llamacppbuild/llama.cpp/modelfiles/model_rocm.ini:/app/models.ini:ro
      - /mnt/workdrive/llamacpp/huggingface:/root/.cache/huggingface
    group_add:
      - video
    devices:
      - "/dev/kfd:/dev/kfd"
      - "/dev/dri:/dev/dri"
    entrypoint: ["./llama-server"]
    command: >
     --host 0.0.0.0
      --port 8080
      --models-dir /models
      --models-preset /app/models.ini
      --models-max 1
      --sleep-idle-seconds 6000
      --metrics
      --load-mode none
      --offline
      --log-verbosity 4
      --sse-ping-interval 15
      --slot-save-path /tmp
    restart: unless-stopped
[qwen3-8-27b]
cache-ram              = 14336
ctx-checkpoints        = 4
checkpoint-min-step    = 8192
main-gpu               = 0
parallel               = 3
batch-size             = 4096
ubatch-size            = 512
kv-unified             = false
hf                     = unsloth/Qwen3.8-27B-GGUF:UD-Q4_K_XL
ctx-size               = 491520
temp                   = 1.0
top-p                  = 0.95
top-k                  = 20
min-p                  = 0.0
presence-penalty       = 0.0
repeat-penalty         = 1.0
cache-type-k           = q8_0
cache-type-v           = q8_0
flash-attn             = true
split-mode             = tensor
#tensor-split           = 50,50
jinja                  = true
reasoning-preserve     = true
reasoning-effort       = medium
reasoning-budget       = 20000
image-min-tokens       = 1024
no-mmproj-offload      = true
spec-type              = none
spec-draft-n-max       = 1
spec-draft-p-min       = 0.75

Details

Prompt processing (PP) -- tokens/s, independent of parallel level

mtp=none

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60000 875.4 847.7 -3.2%
75000 776.9 750.5 -3.4%
90000 704.3 680.0 -3.5%
105000 646.0 621.8 -3.7%
120000 596.8 574.1 -3.8%

mtp=1

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60000 806.9 772.5 -4.3%
75000 727.7 700.4 -3.8%
90000 661.9 638.2 -3.6%
105000 606.1 584.3 -3.6%
120000 560.1 540.0 -3.6%

mtp=2

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60000 806.9 772.5 -4.3%
75000 727.7 700.4 -3.8%
90000 661.9 638.2 -3.6%
105000 606.1 584.3 -3.6%
120000 560.1 540.0 -3.6%

mtp=3

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60000 806.9 772.5 -4.3%
75000 727.7 700.4 -3.8%
90000 661.9 638.2 -3.6%
105000 606.1 584.3 -3.6%
120000 560.1 540.0 -3.6%

mtp=4

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60000 806.9 772.5 -4.3%
75000 727.7 700.4 -3.8%
90000 661.9 638.2 -3.6%
105000 606.1 584.3 -3.6%
120000 560.1 540.0 -3.6%

Decode (TG) -- combined tokens/s across concurrently-decoding slots

mtp=none parallel=1

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 29.36 29.36 +0.0%
75010 27.94 27.94 +0.0%
90010 26.42 26.42 +0.0%
105010 25.16 25.16 +0.0%
120010 23.88 23.88 +0.0%

mtp=none parallel=2

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 43.18 43.18 +0.0%
75010 40.00 40.00 +0.0%
90010 37.12 37.12 +0.0%
105010 34.76 34.76 +0.0%
120010 32.54 32.54 +0.0%

mtp=none parallel=3

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 51.21 51.21 +0.0%
75010 46.86 46.86 +0.0%
90010 43.10 43.10 +0.0%
105010 40.05 40.05 +0.0%
120010 37.17 37.17 +0.0%

mtp=1 parallel=1

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 35.31 35.31 +0.0%
75010 - 33.49 -
90010 36.30 43.39 +19.5%
105010 41.01 41.30 +0.7%
120010 31.68 39.72 +25.4%

mtp=1 parallel=2

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 48.09 25.08 -47.8%
75010 56.97 43.56 -23.5%
90010 41.71 38.64 -7.4%
105010 52.01 54.06 +3.9%
120010 50.55 39.95 -21.0%

mtp=1 parallel=3

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 34.75 70.30 +102.3%
75010 31.45 65.22 +107.4%
90010 56.27 56.77 +0.9%
105010 52.84 57.39 +8.6%
120010 40.66 47.30 +16.3%

mtp=2 parallel=1

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 59.22 38.09 -35.7%
75010 32.64 46.94 +43.8%
90010 51.94 28.93 -44.3%
105010 50.39 42.92 -14.8%
120010 47.96 35.29 -26.4%

mtp=2 parallel=2

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 56.92 27.78 -51.2%
75010 39.70 29.46 -25.8%
90010 32.17 30.50 -5.2%
105010 48.77 56.74 +16.3%
120010 39.84 53.49 +34.3%

mtp=2 parallel=3

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 63.23 98.79 +56.2%
75010 87.79 23.45 -73.3%
90010 33.53 62.80 +87.3%
105010 54.97 33.54 -39.0%
120010 47.82 29.05 -39.3%

mtp=3 parallel=1

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 69.06 35.97 -47.9%
75010 26.23 32.76 +24.9%
90010 37.17 30.98 -16.7%
105010 59.83 39.97 -33.2%
120010 56.41 49.21 -12.8%

mtp=3 parallel=2

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 51.69 30.31 -41.4%
75010 23.87 39.18 +64.1%
90010 33.16 32.22 -2.8%
105010 51.59 67.13 +30.1%
120010 64.29 37.00 -42.4%

mtp=3 parallel=3

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 59.14 122.50 +107.1%
75010 42.58 104.79 +146.1%
90010 41.10 101.82 +147.7%
105010 59.13 40.55 -31.4%
120010 43.33 30.25 -30.2%

mtp=4 parallel=1

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 71.78 32.24 -55.1%
75010 24.33 24.64 +1.3%
90010 23.05 31.85 +38.2%
105010 57.98 49.09 -15.3%
120010 27.60 43.43 +57.4%

mtp=4 parallel=2

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 49.23 38.83 -21.1%
75010 43.15 52.81 +22.4%
90010 33.20 42.23 +27.2%
105010 40.80 83.70 +105.1%
120010 68.99 55.06 -20.2%

mtp=4 parallel=3

context length A: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN Removed B: PR26419-testing, gfx1201, GGML_HIP_ROCWMMA_FATTN=ON, Code Master % diff
60010 48.76 124.02 +154.3%
75010 45.15 54.74 +21.2%
90010 55.69 93.42 +67.8%
105010 57.72 82.77 +43.4%
120010 47.18 29.74 -37.0%

Images

decode-speed-vs-context__PR26419-testing__gfx1201__GGML_HIP_ROCWMMA_FATTN_ON__Code_Master_ decode-speed-vs-context__PR26419-testing__gfx1201__GGML_HIP_ROCWMMA_FATTN_Removed_

TL;DR, not sure if these results are conclusive, but for me it seems like the the current path with WMMA_FATTN is better in TG, but the PR has ~ +3.5% in PP.

@justinappler

Copy link
Copy Markdown

I've been keeping an eye out for PRs/commits that might impact RDNA 3.5 and specifically gfx1151 (Strix Halo). Benched this PR vs master and it looks like this regresses pp @ depth. Might be worth limiting the scope of this to RDNA4 on the DKQ <= 256 arm. tg/s was essentially flat, no impact. Data below:

Environment
GPU AMD Radeon 8060S Graphics, gfx1151 (RDNA3.5), 104 GiB unified
ROCm 7.14.0
Model Qwen3.6-35B-A3B UD-Q4_K_XL, head dim 256
Commits master c841aeeb8, PR head d76c00469 cherry-picked to master
llama-bench -m Qwen3.6-35B-A3B-UD-Q4_K_XL.gguf \
  -ctk f16 -ctv f16 -fa 1 \
  -b 4096 -ub 2048 -ngl 999 -lm none \
  -p 512 -n 128 -r 3 \
  -d 0,2048,8192,16384,32768,65536

pp512 (t/s)

depth master master + PR delta
0 1057.15 1054.27 −0.3%
2,048 978.35 978.04 −0.0%
8,192 870.26 887.36 +2.0%
16,384 747.84 710.48 −5.0%
32,768 576.22 517.48 −10.2%
65,536 390.27 335.20 −14.1%

Each arm run 3+ times, alternating master / PR between runs; figures are means. tg128 was flat at every depth, within 0.2% (39.33 t/s on both arms at d=65,536).

justinappler added a commit to justinappler/llama.cpp-strix-halo that referenced this pull request Aug 29, 2026
Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Aug 31, 2026
Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Sep 3, 2026
When DKQ > 128 on AMD WMMA, K/V loads bypass LDS so the end-of-loop
__syncthreads() barriers were skipped entirely. However, tile_mask still
transits LDS and is read during softmax. Without a barrier at the end of
the VKQ section, the next iteration's load_mask can overwrite tile_mask
while a slow warp is still reading it from the current iteration.

Fix: keep the __syncthreads() at the end of the V loop when the mask is
active (ncols2 > 1 || mask_h), even when DKQ > 128.

Reported-by: williamtwomey (PR ggml-org#26419 review)
Tested: 5 consecutive runs of test-backend-ops -o FLASH_ATTN_EXT on
gfx1201, 2920/2920 passed each time (14600/14600 total, 0 failures).
@srgtuszy
srgtuszy force-pushed the opt/rdna-wmma-dkq256 branch from d76c004 to 975671d Compare September 7, 2026 20:54
@srgtuszy

srgtuszy commented Sep 7, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for looking into the 3.5 @justinappler! I've rebased onto master and scoped the DKQ <= 256 MMA dispatch to RDNA4 exclusively. Could you verify if this restores master tile behavior on your gfx1151?

Here's the latest benchmark results on a single Radeon AI Pro R9700:

Qwen3.6-35B-A3B Q4_K_M (head dim 256, GQA):

Context Depth Master (Tile) PR (MMA) Speedup
d0 2580 t/s 2686.64 ± 15.10 t/s +4.1%
d16384 1679 t/s 1935.42 ± 5.02 t/s +15.3%
d65536 832 t/s 966.37 ± 0.69 t/s +16.1%
d126976 511 t/s 603.64 ± 0.26 t/s +18.2%

@srgtuszy
srgtuszy requested review from IMbackK and am17an September 7, 2026 21:13
@SimSonic

Copy link
Copy Markdown

Hi!

My setup: Windows 11, 64 Gb RAM, Ryzen 5700G (all monitors on iGPU), RX 9070 XT fully available for inference.
Trying to make max perf for Qwen 3.8 27B IQ4_XS. Benchmarking on 50k prefill + 90k context window, two scenarios (lyric + agentic/opencode).

I independently arrived at the same change (enabling MMA for DKQ=256) and ran into a correctness problem that the PR does not cover. Look:

Enabled MMA for 256, got the speedup and garbage output:

Removed two guards (Q->ne[0] <= 128 in fattn.cu:650 and DKQ > 128 in fattn-mma-f16.cuh). Speed improved immediately:

Config pp, t/s (50k) TTFT1
master, tile kernel 655.9 76.2 s
+ MMA@256 856 (+30.7%) 58.4 s

But the output turned to garbage. On ~2k prompts garbage appeared in about half of the runs, on 50k in 100% of runs. Example response head: isk af afPOSE afolar... instead of meaningful text. Generation speed stayed normal, no errors in the log.

In fattn-mma-f16.cuh, the ncols2 > 1 branch (GQA, non-sparse), the upstream KV loop:

} else {
    constexpr bool oob_check = false;
    for (; kb0 < kb0_stop-1; ++kb0) {
        constexpr bool last_iter = false;
        constexpr int  k_VKQ_sup = nbatch_fa;
        flash_attn_ext_f16_iter
            <..., last_iter, oob_check, ...>(..., kb0, k_VKQ_sup);
    }
    constexpr bool last_iter = true;
    constexpr int  k_VKQ_sup = nbatch_fa;   // <- full batch, not the remainder
    flash_attn_ext_f16_iter
        <..., last_iter, oob_check, ...>(..., kb0, k_VKQ_sup);
}

The causal chain:

  1. oob_check = false means the K/V/mask loaders do not clamp indices and do not zero-fill anything past the last valid row.
  2. On the last partial KV block (n_kv not a multiple of nbatch_fa) the reads go past n_kv into KV buffer memory that holds tokens from earlier requests.
  3. The plan was to rely on the mask: "no need to bounds-check, masked-out K/V contribute nothing anyway". But the mask for the partial tile is also loaded without a bounds check, and its rows past n_kv are uninitialized. Instead of -inf, random garbage lands there, and garbage K/V take part in the softmax.
  4. Hence the probabilistic behavior: while zeros or lucky mask bytes sit past n_kv, the answer looks fine. On long context (from ~46.5k in my runs) and under real server traffic (the KV buffer is reused across requests) the chance of a clean pass drops to zero. That is where I caught it, on a live llama-server.

Fix:

         constexpr bool last_iter = true;
-        constexpr int  k_VKQ_sup = nbatch_fa;
+        // multi-stage cp.async loads cannot zero-fill, oob_check is not supported there
+        constexpr bool oob_check_last = nstages == 0;
+        const     int  k_VKQ_sup = ne11 - kb0*nbatch_fa;
         flash_attn_ext_f16_iter
-            <..., last_iter, oob_check, ...>
+            <..., last_iter, oob_check_last, ...>
  • k_VKQ_sup = ne11 - kb0*nbatch_fa: the last iteration gets the true number of remaining rows instead of the full batch.
  • oob_check_last: the check is enabled only on the last iteration. Full blocks are guaranteed in bounds, so the hot loop stays untouched.
  • The nstages == 0 gate: only plain loads can zero-fill. At nstages >= 1 the code uses cp.async, which cannot zero-fill (see the preload block above, it already has constexpr bool oob_check = false for the same reason). NVIDIA multi-stage behavior is unchanged.

I've tested it:

  • 20/20 coherent runs on the scenarios that failed before the fix (~50% at 2k, 100% at 50k).
  • Context ladder 8k-88k, including the old break boundary (46484/47305), clean.
  • PPL on a code corpus: 1.0127 (fix) vs 1.0118 (tile kernel), difference within noise.

Caveat: test-backend-ops -o FLASH_ATTN_EXT passes without the fix, for the same reason as point 3 above: test tensors are freshly allocated, reads past n_kv get zeros, and a zero mask row behaves like a valid one. The bug needs a dirty KV buffer, that is multi-turn server traffic or a dedicated test that fills the memory beyond the boundary with garbage.

I also checked reachability on clean master: took Llama-3.2-3B (head_dim 128, GQA 24/8, exactly the path MMA serves today), ran a cold 50k prompt, a short request after the long one, and a delta turn. Both builds (master and master+fix) give identical correct answers. So on current master at DKQ<=128 it does not reproduce, and the exact case this PR enables (DKQ=256) is the one I hit.

On top of the fix I swept the RDNA (256,256,64) config:

-    GGML_CUDA_FATTN_MMA_CONFIG_CASE(256, 256, 64, 128, 2,  64, 128, 128, 64, 1, true);
+    GGML_CUDA_FATTN_MMA_CONFIG_CASE(256, 256, 64, 128, 2,  32,  64, 128, 64, 1, true);

The fa curve: 128 -> crash (LDS exceeds 64KB), 96 -> -11.6%, 64 -> baseline, 32 -> +7.9% (pp32768, llama-bench), 32 is the floor (assert on multiple of 32). Mechanism: KQ_C splits into smaller tiles (4 -> 2), lower register pressure and finer stream-k granularity, the effect shows at deep context.

So my final numbers (A/B interleaved 3+3 sessions)

Config pp, t/s TTFT1 delta turn gen
master b10897 (tile) 655.9 76.2 s 428 ms 27.34
+ MMA@256 + oob fix 856 58.4 s 370 ms 27.3
+ fa=32 996.4 50.2 s 351 ms 27.29

Gen did not move at any step (batch=1 goes through the vec kernel).

What this means for the PR

  1. The LDS bypass in this PR reads K/V via ldmatrix straight from VRAM without bounds checks on the partial tile. Worth running a multi-turn server scenario with KV reuse and 40k+ prompts before merging.
  2. My fix is small and local, and it works without the LDS bypass. I can send it as a separate commit onto this branch if that is convenient.
  3. Question: does fa=32 stack with the LDS bypass? My fa=32 runs used the LDS-staging config. I can test the combination if useful.

This is not AI written comment but I used it to translate/format some parts into English =)

@JohannesGaessler JohannesGaessler left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This PR should definitely not be merged like this. It simply adds too much technical debt for what it does. I am also quite skeptical that an implementation loading directly from VRAM is actually the optimal solution. For RDNA in particular I realized when reviewing another PR that the 16 byte padding is conflicting with the way I implemented FP16 accumulation. In terms of scheduling, first I need to work out the details of #28536 , then I need to think about how to integrate AMD.

justinappler added a commit to justinappler/llama.cpp-strix-halo that referenced this pull request Sep 26, 2026
justinappler added a commit to justinappler/llama.cpp-strix-halo that referenced this pull request Sep 26, 2026
Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Sep 28, 2026
Aristo94 added a commit to Aristo94/EngramHalo.cpp that referenced this pull request Sep 30, 2026

This branch has not been deployed

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

Labels

CUDA Related to the CUDA backend ggml changes relating to the ggml tensor library for machine learning testing Everything test related

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Native MMA FA kernel regresses prompt processing up to 2x at depth on RDNA4 (gfx1201) after rocWMMA removal

9 participants