RDNA4 Llama Experiments — Squeezing Every Token/s from the R9700 #21043
Replies: 18 comments 29 replies
|
@JohnTDI-cpu thanks again, do you mind sharing huggingface links to the two models you used |
|
All my stats seem to perform better overall. Also i'm running this on a custom LACT R9700 Profile Differences I can tell which may have mattered are Models (GGUF)
System Configuration
Test ConfigurationEnvironment
|
| Flag | Value |
|---|---|
-t / threads |
1 |
-ngl |
99 |
-fa |
1 (flash attention on) |
-p |
128,512,2048,8192 (prefill sizes) |
-n |
128,512,2048 (decode / generation lengths) |
-r |
3 (repetitions) |
Not set: VK_ICD_FILENAMES
Configs (columns in results)
| Column | Binary / batching |
|---|---|
| Stock RADV | Default llama-bench batching: -b 2048, -ub 512 (no extra flags). ggml-vulkan.cpp: rm_kq = 2 (upstream default). |
| RADV+ub2048 | Same binary as stock; add -ub 2048 and -b 16384. |
| RADV+rm_kq1+ub2048 | Rebuild with uint32_t rm_kq = 1 in ggml/src/ggml-vulkan/ggml-vulkan.cpp (line that defaults to 2); same flags as RADV+ub2048. |
Backend is Vulkan / RADV via the cmake build (GGML_VULKAN=ON).
Detailed results: Qwen3.5-35B-A3B (MoE)
Decode
| Context | Stock RADV | RADV+ub2048 | RADV+rm_kq1+ub2048 |
|---|---|---|---|
| tg128 | 153.4 | 153.1 | 155.6 |
| tg512 | 152.3 | 151.7 | 151.8 |
| tg2048 | 149.7 | 150.4 | 152.3 |
Prefill
| Prompt | Stock RADV | RADV+ub2048 | RADV+rm_kq1+ub2048 |
|---|---|---|---|
| pp128 | 1839 | 1814 | 1742 |
| pp512 | 3314 | 3265 | 3257 |
| pp2048 | 3272 | 3964 | 3946 |
| pp8192 | 3131 | 3846 | 3832 |
Detailed results: Qwen3.5-27B (Dense)
Decode
| Context | Stock RADV | RADV+ub2048 | RADV+rm_kq1+ub2048 |
|---|---|---|---|
| tg128 | 32.25 | 32.17 | 32.06 |
| tg512 | 32.27 | 32.21 | 32.06 |
| tg2048 | 32.09 | 32.09 | 31.89 |
Prefill
| Prompt | Stock RADV | RADV+ub2048 | RADV+rm_kq1+ub2048 |
|---|---|---|---|
| pp128 | 841 | 821 | 830 |
| pp512 | 942 | 914 | 921 |
| pp2048 | 933 | 923 | 930 |
| pp8192 | 883 | 880 | 890 |
build: 48cda24c1 (8555)
Condensed comparison (three RADV configs)
All values are t/s from the detailed tables above. RADV+ub2048 and RADV+rm_kq1+ub2048 use absolute t/s with Δ vs Stock RADV in parentheses (percent, rounded).
| Model | Test | Stock RADV | RADV+ub2048 | RADV+rm_kq1+ub2048 |
|---|---|---|---|---|
| MoE 35B | tg128 | 153.4 | 153.1 (−0.2%) | 155.6 (+1.4%) |
| MoE 35B | pp512 | 3314 | 3265 (−1.5%) | 3257 (−1.7%) |
| MoE 35B | pp2048 | 3272 | 3964 (+21.2%) | 3946 (+20.6%) |
| MoE 35B | pp8192 | 3131 | 3846 (+22.8%) | 3832 (+22.4%) |
| Dense 27B | tg128 | 32.25 | 32.17 (−0.2%) | 32.06 (−0.6%) |
| Dense 27B | pp512 | 942 | 914 (−3.0%) | 921 (−2.2%) |
| Dense 27B | pp2048 | 933 | 923 (−1.1%) | 930 (−0.3%) |
| Dense 27B | pp8192 | 883 | 880 (−0.3%) | 890 (+0.8%) |
|
Here's something interesting, for those folk with dual R9700s - messing with the power profiles can make a huge difference to TG. The results are...interesting, to say the least. Before: After: So a 14% increase in TG, but a significant decrease in PP. I don't have the tables to hand right now, but the change for Qwen3-30B-A3B-Q4_K_M was also interesting; 159t/s TG on a single card, 152t/s TG on two. I'm not entirely sure how to explain that. |
|
anyone tried and tested the new qwen 3.6 27B and 35B optimisations like MTP , Turboquant and Dflash ? |
AMD Radeon AI PRO R9700 (RDNA4, gfx1201) Benchmark & Optimization ResultsAMD Radeon AI PRO R9700 (RDNA4, gfx1201) 测试结果与优化经验分享Hardware / 硬件配置:
Optimizations confirmed on RDNA4 / RDNA4上验证有效的优化项All recommendations from this thread apply equally to gfx1201.
Results — Vulkan + MTP (spec-draft-n-max=3, parallel=1)测试结果 — Vulkan + MTP(spec-draft-n-max=3,parallel=1)
Key finding / 关键发现MTP provides a consistent ~2× speedup on RDNA4, compensating significantly for the narrower memory bus. R9700's tg/bandwidth ratio is slightly better than 7900XTX (69% vs 60%), suggesting RDNA4 is marginally more compute-efficient per GB/s. Why R9700 is slower than 7900XTX for decode / 为什么R9700解码比7900XTX慢: |
|
My preliminary results testing MTP out , the best performance boost are actually for single GPU R9700 27B Q4 , and the one I actually use daily is dual GPU 35B Q8, gets a modest 6-7% boost at the expense of PP and TTFT do you guys have similar results , any tips on making 35B get as much boost as 27B does ? MTP BenchmarksSingle R9700 + Qwen3.6-27B-UD-Q4_K_XL + KV Q4_0Prompt Processing (PP)
TTFT (seconds)
Text Generation (TG)
TG Acceptance Rate
Dual R9700 + Qwen3.6-35B-A3B-UD-Q8_K_XL + KV F16Prompt Processing (PP)
TTFT (seconds)
Text Generation (TG)
TG Acceptance Rate
|
|
After a few days of testing, I would like to share some of my thoughts. My system ubuntu 26.04,and I upgraded kernel to 7.0.0-15。 I tested vulkan and rocm, I think rocm is actully better when run Qwen3.6-27b (dense) docker pull ghcr.io/ggml-org/llama.cpp:full-rocm --->it is b9209 when I tested I run "amd-smi version" in container and check ROCm verion, and get: At first, the r9700 is optimized specifically for data types such as INT4, INT8, and FP8. Therefore—whether for model precision or KV cache precision—you'd better use Q4 or Q8. Do not use Q6, nor the mixed-precision UD-Q4_K_XL; otherwise, performance will suffer significantly (I have already tested a lot), particularly when using the ROCm driver. I used unsloth/Qwen3.6-27B-MTP-GGUF Qwen3.6-27B-Q8_0.gguf,I believe if you use Qwen3.6-27B-Q4_K_M.gguf will get better speed and context window. Fortunately, 32GB of VRAM is just enough to accommodate a Q8 model combined with a q4_0 KV cache. However, you cannot set the parameters to Now, see my test.(Qwen3.6-27B-Q8_0.gguf + q4_0 kv cache) Request Concurrency=1 Request Concurrency=1 Request Concurrency=1 Request Concurrency=1 The above is a single-request test for the Q8 model, which I consider sufficiently good. I'm too lazy to test Q4. Below are the results of two concurrent tests: Request Concurrency=2 thread1: thread2: As shown above, under concurrent conditions, the mtp for the second thread is extremely poor. I am not sure whether this is a bug. As pp continues to increase, tg deteriorates significantly, so I did not proceed with further testing. Below is my Docker Compose configuration: param refer to: for kv cache type: as mentioned before, the speed of q4_0 is far better than q4_1, iq4_nl, q5_0, q5_1. "-sm row" option will not impact speed obviously, "-sm layer" is default. before reproducing, you should config your machine: Step 1: Enable Resizable BAR in BIOS Step 2: Lock GPU to Highest Performance Level (maybe you don't need this step, see below "two key findings") Method 1: Using rocm-smi (Recommend) Method 2: Directly via sysfs Persistence: Step 3: Enable ASPM Method 1: Via Kernel Boot Parameters (Recommend) Method 2: Using a Script Verification after Reboot: Step 4: Disable ECC for RX 9700 Series GPUs (I'm not sure if this has any impact, but I did it anyway.) Add the following parameter: Verification after Reboot: Finally, my /etc/default/grub contains: then I run update-grub and verify that the kernel parameters appear correctly in /boot/grub/grub.cfg under the menu entries. Two key findings! (I only tested rocm) First: Second: The latter is more power-efficient (idle power consumption typically hovers around 20W in Shown below is the
|
|
The OP deserves a special gold star. These 2 optimizations boosted performance for Qwen3 122B from 15 t/s to 48 t/s with 40 t/s achieved with just #1. And it worked even with RPC using 2 nodes. (1) -ub 2048 | MoE 35B | RADV | +29% prefill pp2048 | -ub 2048 -b 16384 I saw significant performance improvements with other MOE models like Qwen 3 Next 80. I have not yet seen any boost with dense models but I just started testing. Node 1: Node 2: Tyvm for this post! |
|
anyone got any udpates on qwen 3.6 27b perhaps |
|
@zedbytes Of the optimizations listed below, I can report that the latest mesa build did increase prefill by roughly the numbers shown below for both dense (Qwen 3.6 27b) and moe (Qwen 3.5). Likewise for me, I ran the llama-bench to optimize b and ub for your setup. All tested with llama.cpp release b10068. The only boost I did not see is from setting pcie_aspm to performance as shown. Possibly this is due to my motherboard/bios settings or the fact I have dual R9700 one each on a PCIe Gen4 x8 slot.
|
This comment was marked as off-topic.
This comment was marked as off-topic.
A
|
-ub |
prefill pp512 | decode tg128 |
|---|---|---|
| 64 | 457.7 | 25.4 |
| 65 | 16.2 | - |
| 68 | - | 2.09 |
| 128 | 17.1 | 2.09 |
| 256 | 19.3 | 2.08 |
| 288 | 653.3 | 25.3 |
| 512 | 774.4 | 25.3 |
| 2048 | 773.9 | 25.4 |
28x on prefill between 64 and 65. 40x between 65 and 288. 12x on decode.
Control on the same card, same sweep, gpt-oss-20b (non-hybrid MoE): 1323 /
1957 / 2803 / 3674 t/s at -ub 64/128/256/512. Clean monotonic scaling, no
dead band. So this is specific to the hybrid architecture, not the driver, not
the card, not the tunnel.
Why it matters even if you never touch -ub
llama-bench sets n_batch = n_ubatch = n_gen for a tg test when n_gen is
smaller than the default. So a plain llama-bench -n 128 puts you in the dead
band without asking:
| test | implied n_ubatch |
reported |
|---|---|---|
| tg64 | 64 | 25.5 t/s |
| tg96 | 96 | 2.08 t/s |
| tg128 | 128 | 2.09 t/s |
| tg256 | 256 | 2.09 t/s |
| tg512 | 512 | 25.3 t/s |
Generating 128 tokens takes 60.7 s; generating 512 takes 20.2 s. Fewer tokens,
three times the wall clock.
So any tg96/tg128/tg256 number posted for a Qwen3.5/3.6 hybrid is roughly 12x
too low, and comparisons against a tg512 or against a non-hybrid model are
meaningless. My first pass at these benchmarks was wrong the same way, which is how I found it.
It also reframes the -ub 2048 advice in the opening post: it is not only worth
+29% prefill, it keeps you out of a 40x hole. And -ub 128 or -ub 256, which
are otherwise perfectly natural choices, are the worst thing you can pick.
What it is not
- Not amortization or warmup. A fixed per-test cost cannot make 512 tokens
finish in less absolute time than 128, and llama-bench hoists every cache
clear and context setup outside its timed region. - Not a stall: GPU busy is 95-99% in both regimes, so the slow path is doing
real GPU work. - Not paging or VRAM spill: zero disk reads in both, both resident (15.9 vs
16.4 GiB), recurrent-state buffer on device (149.62 MiB,Vulkan0) in both. - Not a graph fallback: identical graph node count (3655) and the fused
GatedDeltaNet path reports enabled in both.
Pointer for whoever wants to fix it
GGML_VK_PERF_LOGGER=1 attributes it to RMS_NORM, whose per-row cost is flat
at 0.17 us up to 64 rows and flat at ~26 us above, a 157x step exactly at the
RDNA wave size of 64:
| RMS_NORM(5120, N) | us/call | us/row |
|---|---|---|
| 32 | 10.0 | 0.31 |
| 64 | 10.7 | 0.17 |
| 65 | 1710.5 | 26.3 |
| 128 | 3352.7 | 26.2 |
| 256 | 6839.4 | 26.7 |
I am flagging that as a lead, not a root cause: those absolute per-op numbers
do not reconcile with -ub 2048 being fast end to end, so something about the
attribution is incomplete. The end-to-end table is the solid part.
Repro, no special build:
llama-bench -m Qwen3.6-27B-Q4_K_M.gguf -ngl 99 -fa on -p 512 -n 0 -b 16384 -ub 64
llama-bench -m Qwen3.6-27B-Q4_K_M.gguf -ngl 99 -fa on -p 512 -n 0 -b 16384 -ub 65
Happy to open a separate issue with the full data if that is more useful than a
discussion comment.
Smaller items
DPM high is 8-9% slower for prefill, and here is why. Following up on the
report that forcing power_dpm_force_performance_level=high raised decode but
lowered prefill: the sclk DPM table on this card is only three entries deep.
$ cat /sys/class/drm/card1/device/pp_dpm_sclk
S: 0Mhz *
1: 500Mhz
2: 2350Mhz
high pins level 2 = 2350 MHz, but under auto the card boosts to
3348 MHz, above the whole table. So "high" does not mean maximum, it means
fixed, and it disables opportunistic boost. Prefill is clock-bound and loses
8-9%; decode is bandwidth-bound and does not care. It does cut power from
139 W to 94 W, so it is a real efficiency lever, just not a performance one.
ASPM did nothing on my topology. pcie_aspm.policy=performance moved
prefill -0.8% (noise) on the tunnelled card, against +10.8% reported on
PCIe 5.0 x16 and no reproduction on Gen4 x8. Three topologies, one positive
result, so I would not treat it as general.
Check your build before publishing hybrid numbers. Same card, same model,
same flags: a 2026-07-06 build prefills at 17 t/s where b10107 does 799 t/s.
The hybrid path was slow before and is fixed now.
eGPU users: current_link_speed lies. My GPU reports 32.0 GT/s x16,
which is the link to the bridge inside the dock. Every hop upstream is
2.5 GT/s x1-x4, and a warm 16 GiB model load measures under 1 GB/s end to end.
If you are on Thunderbolt or USB4, measure, do not read sysfs.
One hazard worth knowing. Loading a 22.4 GB MoE across the tunnel
reproducibly timed out the SDMA ring (ring sdma0 timeout ->
Ring sdma0 reset failed -> GPU reset begin -> MES stops answering). The
driver's own recovery wedged, VRAM stayed claimed, every hwmon sensor went
unreadable, and the host's irq/N-pciehp thread got stuck so the slot could
not re-enumerate. A dock re-plug did not clear it; only a reboot did. The
17 GB dense model never triggered it.
|
BIOS -> PCIe ASPM -> Disable and it will sit in perf mode all the time.... otherwise change it through sysfs. Don't forget to disable Global C-State too. So to get better performance you need: I have checked that rm_kq = 1 and it didn't bring any advantages with single and dual RDNA3 gpus. |
2× R9700 / gfx1201 result: +12.47% tg128 across 3 launches, matched PPLTested the HIP direct-P2P AllReduce experiment on 2× Radeon AI PRO R9700 ( Environment
The unpatched and patched binaries were built independently from the same upstream commit with identical CMake settings. llama-benchEquivalent command shape: HIP_VISIBLE_DEVICES=0,1 llama-bench \
-m Qwen2.5-7B-Instruct-Q6_K.gguf \
-p 512 -n 128 -b 2048 -ub 512 \
-sm tensor -fa 1 -r 5
tg128 launch aggregates:
Each launch contains five internal llama-bench samples, giving 15 internal samples per build. The launch-aggregate tg128 ranges do not overlap: the patched minimum ( The unpatched runs logged the HIP internal-AllReduce fallback to the meta-backend butterfly. The patched runs did not log that fallback. WikiText-2 perplexityCommand shape: HIP_VISIBLE_DEVICES=0,1 llama-perplexity \
-m Qwen2.5-7B-Instruct-Q6_K.gguf \
-f wiki.test.raw \
-sm tensor -fa on -c 512 -b 2048 -ub 1 \
-ngl 99 -t 12 --fit off --chunks 16
The unpatched and patched tensor runs produced identical cumulative PPL values at every one of the 16 chunks. Patched tensor versus patched non-split differed by 0.0040 PPL (about 0.053%). Dataset: Salesforce WikiText-2 raw test split pinned at revision The performance figures above are descriptive results from three independent launches per build, not a formal statistical claim. |
Follow-up: 2× R9700 / Huihui Qwen3.6-27B — +9.37% tg128 with HIP direct-P2PFollow-up to my earlier 2× R9700 result: the first benchmark used Qwen2.5-7B Q6_K. That result remains a valid matched comparison, but it was not the model used in my intended deployment. I repeated the comparison with the directly relevant Huihui Qwen3.6-27B Q6_K model. Environment
The unpatched and patched binaries were built independently from the same upstream commit with identical CMake settings. llama-benchEquivalent command shape: HIP_VISIBLE_DEVICES=0,1 llama-bench \
-m Huihui-Qwen3.6-27B-abliterated-ggml-model-Q6_K.gguf \
-p 512 -n 128 -b 2048 -ub 512 \
-sm tensor -fa 1 -r 5Three independent launches were run per build. Each launch is llama-bench's aggregate over five internal samples, for 15 internal samples per build.
Launch aggregates:
The tg128 launch ranges do not overlap: the patched minimum ( This GGUF contains an embedded MTP layer, but llama-bench does not exercise server-side MTP speculative decoding. These numbers isolate the direct-P2P change on the correct 27B model. I verified separately that the patched llama-server can load this model in tensor mode and perform MTP-assisted inference, but that server smoke test is not included in the throughput comparison above. Queue IDs for the recorded Llama Lab evidence:
These are descriptive results from three independent launches per build, not a formal statistical claim. |
Correction to my
|
-ub |
256 MB BAR | 32 GB BAR |
|---|---|---|
| 192 | 326.71 | 617.08 |
| 224 | 18.94 | 637.45 |
| 256 | 19.54 | 761.55 |
| 288 | 400.22 | 693.10 |
| 512 | 891.84 | 893.65 |
-ub 256 goes from 19.5 to 761 t/s — 39×. The "dead band" simply doesn't exist once the aperture is whole.
How the BAR gets lost
The kernel assigns the card a correct 32 GB BAR early in boot. Then the thunderbolt driver loads, drops the link, and pciehp re-adds the device — and the re-add sizes the bridge window to current demand:
[0.418219] pci 0000:02:00.0: bridge window [mem 0x7800000000-0x800fffffff 64bit pref]: assigned ← 32 GB
[0.418245] pci 0000:05:00.0: BAR 0 [mem 0x7800000000-0x7fffffffff 64bit pref]: assigned ← 32 GB
[2.150456] ACPI: bus type thunderbolt registered
[2.165482] pcieport 0000:00:01.1: pciehp: Slot(0): Link Down
[2.165489] pcieport 0000:00:01.1: pciehp: Slot(0): Card not present
...
[5.380555] amdgpu 0000:05:00.0: Not enough PCI address space for a large BAR.
[5.380576] amdgpu 0000:05:00.0: [drm] Detected VRAM RAM=32624M, BAR=256M
Window ends up 258 M — exactly 256M + 2M, i.e. fitted to the BAR that had already fallen back.
Diagnosing it from op timings
If you suspect this, the per-op profile is unambiguous. GGML_VK_PERF_LOGGER=1, in-band vs out-of-band (total µs):
| op | -ub 256 |
-ub 288 |
ratio |
|---|---|---|---|
| ADD | 4,605,550 | 16,141 | 285× |
| MUL | 2,672,607 | 10,737 | 249× |
| GLU | 6,464,740 | 26,730 | 242× |
| RMS_NORM_MUL | 5,167,378 | 30,405 | 170× |
| MUL_MAT | 23,095,087 | 1,369,132 | 17× |
Everything slows down, and the small elementwise ops worst — matmul is the least affected. That's fixed per-dispatch cost dominating, i.e. memory starvation. A broken shader would show one op exploding. This is also why my original "RMS_NORM at wave boundaries" guess was wrong: RMS_NORM runs in every model, and dense models were always fine.
The fix, and a second bug on the way to it
modprobe.blacklist=thunderbolt amdgpu.rebar=0
modprobe.blacklist=thunderbolt stops the takeover, so the original 32 GB allocation survives (the tunnel is already enumerated by firmware; the card still works). Only do this if you have no other USB4 devices — it's a diagnostic, not a general fix.
amdgpu.rebar=0 is needed because of a separate bug: with the BAR already large, amdgpu's ReBAR renegotiation wedges init —
amdgpu 0000:05:00.0: hw_init of IP block <gfx_v12_0> failed -110
amdgpu 0000:05:00.0: amdgpu_device_ip_init failed
amdgpu 0000:05:00.0: Fatal error during GPU init
It does not disable the large BAR — the BAR stays 32 GB (Detected VRAM RAM=32624M, BAR=32768M); it just stops amdgpu resizing what it already has. I isolated this by holding thunderbolt blacklisted and toggling only rebar.
Above 4G Decoding and Re-Size BAR Support were already enabled in my BIOS throughout, and pci=realloc,hpmmioprefsize=32G makes no difference here (02:00.0 reports HotPlug-, so it never applies).
Two other things worth knowing
In-band runs can hang the GPU outright, not just run slow — ring comp_1.0.1 timeout → Ring reset failed, needing a power cycle. Twice for me.
The 22.4 GB MoE load failure I reported is the same root cause. With a 256 MB aperture everything moves via SDMA, which is exactly the engine that timed out. It's not a size limit, it's the window.
Kernel-side reports (the window discard, and the ReBAR renegotiation) are going to linux-usb/linux-pci and AMD's tracker respectively — nothing here needs changing in llama.cpp.
tl;dr for anyone on an eGPU: check lspci -vv | grep -A2 "Resizable BAR". If current size is 256 MB on a 32 GB card, that's your cliff, and it isn't RDNA4's fault.
|
Following this RDNA4 work, I isolated a separate Q6_K prefill optimization on HIP; results and patch are here #27082 |
|
• Sorry for my long absence from this discussion. I recently returned to testing with the newly released Qwen3.8-27B and ran a new optimization sweep on a single Radeon AI PRO R9700. Qwen3.8-27B Q8_0 + MTP — single R9700Environment
One important note: compiling the Vulkan shaders with the old system glslc 2023.8 caused a severe performance regression. Rebuilding with Vulkan SDK 1.4.357.1 restored the expected performance. BaselineMy initial configuration used: ctx-size=65536 This produced approximately: 59.2 tok/s Best configurationexport VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/radeon_icd.json llama-server This creates three 16,384-token slots. Reducing the per-slot context appears to be particularly beneficial for this hybrid Qwen architecture. ResultsTwo independent 30-request runs, including a full server restart and VRAM cleanup: Run # Average TG# Median TG# Range # Valid outputs Run 1 65.2 tok/s 65.2 tok/s 53.5–79.3 30/30 Cold restart 64.7 tok/s 64.5 tok/s 53.0–78.6 30/30 Combined average: 64.95 tok/s This is approximately a 9.7% improvement over the 59.2 tok/s baseline. A shorter 10-request run reached 68.0 tok/s, but I consider the two longer runs more representative. The large range is expected because MTP performance depends heavily on generated content. Structured text and code generally achieve higher acceptance and throughput than less predictable text. What mattered
I also tested forced/disabled MMVQ, different submit sizes, CPU thread counts, KV formats, and an experimental larger DMMV workgroup for AMD. None improved the final result; the larger workgroup patch was slower and was reverted. So, for this model on a single desktop-connected R9700, the repeatable result is currently approximately 65 tok/s with Q8_0 and MTP, with individual workloads ranging from roughly 53 to 79 tok/s. |

Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
50+ experiments over several days to find every optimization that matters for llama.cpp Vulkan on RDNA4. All benchmarks were run and verified manually on real hardware. Claude (Anthropic) assisted throughout — helping analyze results, suggest hypotheses for unexpected findings (like the PCIe ASPM discovery), and structure this document. Full results below.
System Configuration
dc8d14c58(build 8554)cmake -DGGML_VULKAN=ON -DGGML_NATIVE=ON -DCMAKE_BUILD_TYPE=ReleaseDriver identification
RADV reports:
AMD Radeon AI PRO R9700 (RADV GFX1201) (radv)AMDVLK reports:
AMD Radeon AI PRO R9700 (AMD open-source driver)All benchmarks use explicit
VK_ICD_FILENAMESto guarantee driver selection.Models Tested
Results: Qwen3.5-35B-A3B (MoE, 35B total, ~3.5B active)
Decode
FA ON, 3 reps, values in tokens/s.
Prefill
FA ON, 3 reps, values in tokens/s.
Results: Qwen3.5-27B (Dense, 27B)
Decode
Prefill
RADV vs AMDVLK
RADV wins overall. AMDVLK has a moderate decode advantage on MoE (+3.7%), but RADV's prefill is dramatically faster, especially on dense models where AMDVLK is nearly 4× slower.
Optimization Impact (RADV)
-ub 2048rm_kq=1rm_kq=1code changeOne line in
ggml/src/ggml-vulkan/ggml-vulkan.cpp:AMDVLK + rm_kq=1 (surprise finding)
rm_kq=1has a large effect on AMDVLK dense decode (+13%), much more than RADV (+1%). This suggests AMDVLK's LLPC compiler benefits more from reduced register pressure on RDNA4.Quality & VRAM Verification
Qwen3.5-35B-A3B — WikiText-2 Perplexity
PPL and VRAM identical across all configurations. No quality or memory impact from any optimization.
Reproduction
Exhaustive Flag Testing
Qwen3.5-35B-A3B (MoE) — Decode tg128, rm_kq=1 active
RADV experiments
gfx queue has zero effect on RADV 35B MoE decode. Disable fusion catastrophically hurts.
AMDVLK experiments
gfx queue gives +4.7% on AMDVLK 35B MoE. No other flag breaks through 164 t/s.
Qwen3.5-27B (Dense) — Decode tg128, rm_kq=1 active
RADV experiments
Nothing moves RADV 27B decode. 29.3 t/s = hard BW ceiling (15.58 GiB × 29.3 = 456 GB/s = 71% of 640 GB/s).
AMDVLK experiments
AMDVLK + rm_kq=1 without gfx = best dense decode (32.73 t/s, +13% over stock rm_kq=2!)
gfx queue HURTS dense AMDVLK by -8% — opposite of MoE where it helps +4.7%.
rm_kq impact across all configs
rm_kq=1has the largest impact on AMDVLK dense decode (+13%). This suggests AMDVLK's LLPC compiler benefits significantly from reduced VGPR pressure on RDNA4 wave32 architecture. RADV's ACO compiler handles register allocation differently, gaining less from the same change.Best Achievable Performance
35B MoE
27B Dense
Dense decode improved by +10.8% on RADV and +14.5% on AMDVLK (vs stock rm_kq=2 + ASPM default) from combined
rm_kq=1+ PCIe ASPM performance mode.Key findings
rm_kq=1is the single most impactful code change: +1% RADV, +2% AMDVLK MoE, +13% AMDVLK dense.PCIe ASPM Discovery
Setting PCIe ASPM to performance mode eliminates L1 exit latency:
ASPM L1 power saving adds latency to every PCIe transaction. Dense models suffer most because they read the entire model (~15.6 GB) every token with many small transactions. MoE models batch work more efficiently, hiding PCIe latency.
This is a system-level optimization — no code change, no driver change. Persists until reboot. To make permanent: add
pcie_aspm.policy=performanceto kernel boot parameters.Known Issues
GGML_VK_DISABLE_COOPMAT=1) improves AMDVLK dense prefill by +17% (207→243) — suggests AMDVLK's cooperative matrix codegen is suboptimal for dense models. RADV's coopmat works correctly.Exhaustive Experiment Log (50+ combinations tested)
Parameters with REAL impact
echo performance > /sys/module/pcie_aspm/parameters/policy-ub 2048 -b 16384GGML_VK_ALLOW_GRAPHICS_QUEUE=1GGML_VK_DISABLE_COOPMAT=1Parameters with ZERO impact (all tested, all confirmed ±0.3%)
RADV flags: gfx queue (on RADV), RADV_DEBUG=nocompute, RADV_PERFTEST=sam/bolist/localbos/dmashaders/nircache/hic/nogttspill, RADV_PROFILE_PSTATE
llama.cpp env vars: GGML_VK_DISABLE(F16/BF16/COOPMAT2/INTEGER_DOT_PRODUCT/ASYNC/GRAPH_OPTIMIZE), GGML_VK_FORCE_MMVQ, GGML_VK_DISABLE_MMVQ, GGML_VK_DMMV_LARGE, GGML_VK_ENABLE_MEMORY_PRIORITY, GGML_VK_DISABLE_HOST_VISIBLE_VIDMEM, GGML_VK_FORCE_MAX_ALLOCATION_SIZE, GGML_VK_FORCE_MAX_BUFFER_SIZE, GGML_VK_SUBALLOCATION_BLOCK_SIZE (16MB and 1GB)
llama.cpp params: -t 1/4/12 (thread count), --no-host, -nopo (no-op-offload), -dio (direct-io), -mmp 0 (no mmap), -sm row (split mode), -b 1/2/512 (batch size), --prio 2 (scheduling priority), -ctk/-ctv q8_0/q4_0 (KV cache quant)
Code changes: rm_stdq=2, rm_kq_int=2, rm_stdq_int=2, rm_kq=3/4
System tuning: hugepages (16GB), transparent hugepages=always, CPU pinning (taskset), nice -n -20, GPU power profile (COMPUTE/3D_FULL_SCREEN)
DISABLE_FUSION is catastrophic: -18.5% on MoE, -5.1% on dense. Never disable.
Bandwidth utilization analysis
Dense models reach 79-83% BW utilization with ASPM fix. MoE models are lower (56-61%) due to dispatch overhead from expert routing. The remaining 17-20% gap on dense is primarily from:
s_wait_kmcntper Q4K GEMV shader)Please share your discoveries too — I'm curious what's the max we can get out of RDNA4.
All reactions