Skip to content

[Performance]: Vulkan (#418) vs ROCm/HIP on RDNA4 (RX 9070 XT) — CORRECTED: Vulkan is 19–24% faster (original comparison was confounded) #523

Description

@noobdev-ph

Important

CORRECTED — the conclusion below is wrong. My original comparison was confounded: COLI_CUDA=1 tripped the g_draft guard and disabled MTP on the HIP arms while the Vulkan arms drafted at 2.00 tokens/forward, so the two backends were not running the same decode configuration.

With speculation controlled on both, Vulkan is 19–24% faster than ROCm/HIP, not 32–35% slower. @steve-m's ~35% faster than ROCm on RDNA4 note is directionally correct and my challenge to it is withdrawn.

See the corrected 80-run results. The ReBAR finding and the correctness results below still stand; the performance comparison does not.


Independent measurements of the Vulkan backend from #418 on RDNA4, since @JustVugg mentioned not having a Vulkan GPU to verify against. Filing as an issue rather than a PR comment so the numbers are citable on their own and the review thread stays focused.

Summary up front: the backend works correctly on RDNA4 with no ROCm installed, and produces exact output on a real fmt=4/gs=64 container. That's the headline and it's a good one — this is the only path that gets the engine onto a lot of hardware.

Two things I ran into that I can't explain on my own, and would value your read on: it needs Resizable BAR (and fails silently without it), and on this particular box it comes out 23–35% slower than ROCm/HIP at matched VRAM — which runs opposite to the ~35% faster than ROCm on RDNA4 note in c/Makefile:260. I'm assuming I've got something configured differently rather than that the note is wrong, so I've documented my full setup below and would happily re-run anything.

Commit

550df5c (branch of #418, rebased onto dce7012 / v1.1.0). One binary built with both backends — they are independent in the Makefile — so every arm below is the same executable and only environment variables differ.

Hardware and storage

  • CPU: AMD Ryzen 7 5700G (8C/16T, Zen 3)
  • RAM: 64 GB DDR4
  • GPU: AMD Radeon RX 9070 XT (gfx1201, RDNA4), 16 GB VRAM, Resizable BAR enabled (16 GB host-visible)
  • Storage: NVMe SSD, link capped at Gen3 x4 (~3.5 GB/s) — the 5700G's PCIe limitation, not the drive's
  • Model: GLM-5.2 744B, fmt=4 grouped int4, gs=64, full container on the NVMe

This box is storage-bound. All backends here are gated by expert streaming rather than compute, so absolute throughput is low and these ratios may not transfer to a Gen4/Gen5 machine.

Software environment

  • Linux 7.0.14-arch1-1
  • Mesa 26.1.3 / RADV, Vulkan 1.4.348
  • ROCm 6.x (hipcc, HIP_ARCH=gfx1201)
  • glslc from shaderc

Build and benchmark commands

make -C c colibri HIP=1 HIP_ARCH=gfx1201 VK=1

# Vulkan arm (320 or 640)
env COLI_VULKAN=1 COLI_VK_EXPERTS=320 COLI_VK_SHADERS=$PWD/c/shaders/qmatmul.spv \
    SNAP=model_gs64 NGEN=40 TOPP=0.7 TEMP=0 \
    PROMPT='[gMASK]<sop><|user|>Explain in two sentences why the sky is blue.<|assistant|><think></think>' \
    ./c/colibri 320

# HIP arm (6 or 12 GB)
env HIP_VISIBLE_DEVICES=0 COLI_CUDA=1 COLI_GPU=0 CUDA_EXPERT_GB=6 CUDA_RELEASE_HOST=1 \
    SNAP=model_gs64 NGEN=40 TOPP=0.7 TEMP=0 \
    PROMPT='[gMASK]<sop><|user|>Explain in two sentences why the sky is blue.<|assistant|><think></think>' \
    ./c/colibri 320

Methodology

  • 50 runs total, 10 per arm.
  • Arms interleaved (VK6, HIP6, VK12, HIP12, repeat) so page-cache warmth and thermal drift affect all arms equally.
  • .coli_usage frozen: the same snapshot is restored before every run, so the "top-N of history" hot-expert ranking is identical in all 50 runs. Without this the learning cache drifts and silently changes what each arm holds.
  • Fresh process per run.
  • Warm-up policy: the frozen snapshot carries prior chat history; no run in the series is cold-start, and every run sees the identical history.
  • Machine quiesced: llama-server stopped (it auto-restarts as a user service and holds ~15 GB VRAM), Firefox killed, and a MemAvailable >= 48 GB gate before each run — the engine sizes its RAM tier from MemAvailable at startup, so an unsettled machine silently produces a smaller tier.
  • Throughput computed from decode_s, not the printed tok/s. The printed value rounds to 2 decimals; at these speeds that turns a true +2.35% into an apparent +4.2%.

Results

1. Resizable BAR is required, and its absence fails silently

pick_memtype() requires HOST_VISIBLE, and the tier wants HOST_VISIBLE|DEVICE_LOCAL. With ReBAR off, that type only exists in a 256 MB window, so the allocation falls back to plain host-visible system RAM — while still reporting success.

ReBAR off (256 MB BAR) ReBAR on (16 GB BAR)
tok/s 0.11 0.24
expert hit rate 22.3% (vk 5.7%) 38.8% (vk 13.2%)
GPU VRAM in use 82 MB 6,724 MB
expert tier upload 2.8–3.0 s 1.9 s

Same binary, same config; only the BIOS setting changed. In the ReBAR-off case the log reported 320 hot experts resident (6.04 GB VRAM) while the card sat at 82 MB and every access crossed PCIe — slower than the pure CPU path, with nothing in the output indicating the degraded mode. It took a sysfs check to identify.

Suggestion: compare the heap size in VkPhysicalDeviceMemoryProperties against the size of the host-visible+device-local type at init and warn, e.g.

[VK] warning: only 256 MB of 16304 MB VRAM is host-visible (Resizable BAR
     appears disabled) — expert tier will fall back to system RAM and be slow

(This comparison is n=1 per side — toggling ReBAR needs a BIOS change and reboot — but the 2.2× gap is far outside the ±2% run-to-run spread measured everywhere else.)

2. VRAM-matched Vulkan vs ROCm/HIP

10 runs per arm, medians, no overlap between arms:

arm tok/s hit rate resident experts tier size VRAM peak
Vulkan COLI_VK_EXPERTS=320 0.2414 38.8% 320 6.04 GB 6,724 MB
HIP CUDA_EXPERT_GB=6 0.3189 57.3% 282 5.99 GB 6,215 MB
Vulkan COLI_VK_EXPERTS=640 0.2475 39.6% 640 12.08 GB 13,124 MB
HIP CUDA_EXPERT_GB=12 0.3331 59.9% 565 12.00 GB 12,160 MB

HIP +32.1% at ~6 GB, +34.6% at ~12 GB. Note Vulkan holds more resident experts in both pairs (320 vs 282, 640 vs 565) and still loses, so this is not a residency-budget artifact.

Note on buckets: Vulkan reports its VRAM tier as a separate vk bucket; HIP with CUDA_RELEASE_HOST=1 folds its VRAM prefix into pin (npin += prefix_est). Total hit rate and tok/s are comparable across backends; the per-bucket split is not.

3. Control: CUDA_RELEASE_HOST does not explain the gap

CUDA_RELEASE_HOST defaults to (g_cuda_ndev>1), i.e. 0 on a single-GPU box, so I tested whether an untuned ROCm baseline would account for the discrepancy. It does not — the default still builds the full 565-expert tier:

~12 GB budget tok/s hit rate resident
Vulkan (640) 0.2475 39.6% 640
HIP, CUDA_RELEASE_HOST unset (default 0) 0.3051 54.7% 565
HIP, CUDA_RELEASE_HOST=1 0.3331 59.9% 565

HIP leads by +23% even untuned; the flag adds a further ~9%.

4. experts loaded/token stays flat with the tier resident — is this expected?

Identical in every run of every arm:

arm experts loaded/token per-layer (baseline topk=8)
Vulkan 320 677.0 9.03
Vulkan 640 674.5 8.99
HIP (all three configs) 330.1 4.40

Vulkan is still streaming ~9 experts per layer per token with 320–640 experts resident in VRAM — roughly the topk=8 baseline, i.e. the tier does not appear to be preventing the host-side load. HIP halves it, consistently, in every configuration including the default.

This is also why doubling the Vulkan tier changes so little (see below): if the tier were short-circuiting the load path as described in #418 ("no RAM slot, no disk read, no prefetch"), this number should fall roughly in proportion to residency.

I don't know the backend internals well enough to say whether this is a genuine gap or specific to this setup — this box's RAM pin tier is auto-capped to 10 experts (cap lowered 320->10 (projected peak 50.7 GB)), so it leans unusually hard on streaming.

5. COLI_VK_EXPERTS 320 → 640: +2.4% for 2× VRAM

Separate 20-run interleaved A/B (same controls):

320 640 delta
tok/s 0.2417 0.2474 +2.35%
hit rate 39.0% 39.9% +0.9 pp
vk 13.2% 19.9% +6.7 pp
pin 9.1% 2.3% −6.8 pp
VRAM peak 6,724 MB 13,124 MB 1.95×
tier upload 1.9 s 3.7 s +1.8 s

Real and cleanly separated (all 10 doubled runs beat all 10 default runs), but small. The vk gain (+6.7 pp) is almost exactly offset by the pin loss (−6.8 pp): the extra 320 experts largely re-serve experts already held in RAM rather than converting cold reads. The 320 default looks well-chosen.

Correctness

50/50 runs produced clean, on-topic output, byte-identical across every configuration and both backends:

"Sunlight is composed of all colors, but it's scattered in all directions by gases and particles in Earth's atmosphere. Blue light is scattered more than other colors because it travels as shorter, smaller waves…"

This is a genuine fmt=4 / gs=64 conversion of the full 744B checkpoint, so it also exercises the #298 per-group scale semantics on Vulkan against a real quantized container — no garbage, no drift.

Minor

COLI_VK_SHADERS unset falls back to "shaders/qmatmul.spv" (colibri.c:6955), relative to CWD, so it only resolves when launched from c/. Resolving relative to the binary's directory — or a docs mention — would smooth the first run. It also expects the full path to the .spv file, not the shader directory.

What would you change about this setup?

Genuinely asking — I'd rather fix the experiment than argue about the result, and a lot of these knobs I chose by reading the source rather than from experience with the engine. Specific things I'm unsure about:

  • Did I pick a fair ROCm baseline? I used CUDA_EXPERT_GB + CUDA_RELEASE_HOST=1 as the closest analogue to Vulkan's resident tier, and also ran the untuned default as a control. If there's a more representative HIP configuration, I'll re-run against it.
  • Is 40 tokens too short? Startup effects could be over-weighted. A longer generation (512/1024 tokens) is easy to run if that's more meaningful.
  • Should COLI_VK_DENSE / COLI_VK_ATTN be part of the comparison? I only exercised the expert tier, so this may be measuring the least favourable slice of the backend.
  • Is freezing .coli_usage the right control? It makes runs reproducible, but if the tier is meant to co-adapt with a live learning cache, freezing it may understate Vulkan specifically.
  • Would a different container help? This is fmt=4/gs=64; happy to test fmt=2 or another quantization.

And the open one: what hardware and configuration produced the ~35% faster than ROCm on RDNA4 for int4 experts measurement in c/Makefile:260? A different RDNA4 SKU, container format, generation length, or the dense/attention tiers enabled would all be plausible explanations, and knowing the intended setup would let me reproduce it directly rather than guess. The card and the full 744B checkpoint are set up here, so re-running is cheap — just say what would be most useful.

Caveats

  • One machine, storage-bound (Gen3-capped NVMe on a 5700G). Every backend here is gated by expert streaming, not compute.
  • Only the expert tier was exercised; COLI_VK_DENSE and COLI_VK_ATTN were not enabled.
  • The ReBAR comparison is n=1 per side (BIOS toggle + reboot); everything else is a 10-run median.
  • None of this affects the portability case for Vulkan backend: expert tier + dense + MLA attention on any Vulkan 1.2 GPU (successor to #84) #418, which works as advertised: correct output on RDNA4 with no vendor stack installed.

Activity

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions