Vulkan backend: expert tier + dense + MLA attention on any Vulkan 1.2 GPU (successor to #84) - #418
Conversation
|
Heads-up: |
|
Rebased onto current dev — the split turned out painless: git tracked |
|
pp512/tg128-style numbers, for comparability with the llama.cpp benchmark conventions (same box and config as the tables above; dual-SSD mirror active for both backends; fresh process per run; two runs each):
Notes: prefill here is expert-streaming-bound (a 512-token batch-union touches nearly every expert of the 744B model), so pp measures the whole engine + disks more than the GPU. The accounting slightly favors HIP — Vulkan uploads its dense weights during the first prefill while the CUDA/HIP tier uploads at load, before the prefill timer — and Vulkan leads regardless (+13% pp, +4–5% tg). These standard workloads also reproduce much more tightly than free-running generations (≤2% spread), so we'll use them for future comparisons. |
|
Rebase needed: #298 just merged into |
|
Rebased onto current dev ( Grouped scales in the Vulkan kernels (the #298 ask). All three kernels (dense GEMV, fused gate+up, absorb attention) now decode fmt=4 grouped int4 with one scale per Validation. The Also in the refresh, from production use on our box:
One measured caveat worth documenting: on our RX 9070/RADV, growing the resident expert tier past ~6 GB slows the other GPU paths' per-dispatch execution (~1.3 s per tier-GB over a 64-token decode; profiled to the fence wait, not the submit call, with eviction/allocation-count/free-memory all ruled out) — so the sweep-derived Numbers on the box are unchanged from the tables above (the refresh is format coverage + hygiene, not a perf change): 64-tok tg on the int3-g64 container now reaches 1.73–1.75 tok/s with the engine-side improvements that landed in dev (#421 mirror + the |
|
Thanks @steve-m — this is a big, welcome piece of work, and I want to get it in. Two things before merge, one mechanical and one that only you can provide (I don't have a Vulkan GPU to verify it myself). 1. CI is red for a mechanical reason — 9 committed test binaries. The PR added compiled binaries under 2. Because it's a new hardware backend I can't run, I'd like to read validation data before merging — this is the bar we hold every backend to. Could you post, from your Vulkan GPU:
Plan: once CI is green (step 1) and the data checks out (coherent output + tok/s in step 2), I'll review and merge. I've verified the mechanical side is the only CI blocker; the rest is your GPU's numbers. Happy to fix the committed-binary bit for you and push to your branch if you'd rather — just say the word. |
|
Both items addressed — plus one more commit that was in flight. 1. CI / committed binaries: removed from the entire history, not just the tip. All nine entered through one commit (an over-broad 2. Validation from the Vulkan GPU. Hardware: AMD Radeon RX 9070 (RADV GFX1201), Mesa 26.1.4, Vulkan 1.4.354, Linux 6.19. Model: GLM-5.2 744B, per-row int4 container, dual-SSD mirror (#421), full Vulkan stack = dense + absorb attention + 320-expert VRAM tier. Correctness. The tiny-model TF oracle fixture isn't bundled, so here is the equivalent evidence on the real model:
Performance — standard workloads (pp526 = 526-token prompt, tg128; fresh process per run, two runs each, same box/prompt/config, only
+21% prefill, +32–35% decode. Expert hit rate ~80% both (VK serves 15.2–15.5% of lookups from the VRAM tier with zero disk I/O; CPU-only covers the same hit rate from RAM alone — the delta is the GPU offload, not cache coverage). 3. New commit since the refresh: single-submit q-prep chain ( |
|
Rebased onto current dev ( Two new commits since the last push, both from profiling decode on the
Standard workloads, same box/prompt/config/convention as the tables above
Decode attention dropped 26.6 → 15.8 s per tg128 run; what remains of decode Validation on this exact branch build (RX 9070, RADV, Mesa 26.1.4): Follow-up we're preparing as a separate PR: a second-device Vulkan expert |
|
Tested this on RDNA4 (RX 9070 XT / gfx1201, RADV, Mesa 26.1.3) since @JustVugg mentioned not having a Vulkan GPU to verify against — it came up and ran correctly first try on a box it had never seen. Nice work. Two things that should be useful for the merge:
Also a small one: Full numbers, methodology and a ROCm/HIP comparison are in #523 rather than here, to keep this thread focused. There's an open question there about the |
…nse + MLA absorb, on top of main
|
Rebased onto current dev (
Also reworded the On the substantive #523 items: the |
Records the full Vulkan investigation as RESEARCH.md §7, plus updated benchmark table and resume points. Findings: - Vulkan is the fastest config on this box: 0.40 tok/s, +18.9% over HIP at ~12 GB and +23.7% at ~6 GB (10 runs/arm, no overlap between arms). - Resizable BAR is mandatory and fails silently without it: the tier lands in system RAM while logging "320 hot experts resident (6.04 GB VRAM)" with the card at 82 MB. 0.11 -> 0.24 tok/s once enabled. - MTP is an I/O amplifier: positions per emitted token = 4/(1+3a). At our 33% acceptance that is 2.0, matching measured ereq/token 330.1 -> 674.5 (2.04x). Costs ~35-40% on BOTH backends, so DRAFT=0 is the single biggest knob here. - Shrinking the CPU LRU 20x barely moves either backend: the VRAM tiers were doing the work, RAM was never masking residency. Also records the confound that made the first 50-run battery report the opposite conclusion (COLI_CUDA=1 trips the g_draft guard, so the HIP arms ran without speculation while the Vulkan arms drafted), and the misuse of experts/token, which is a router-side counter that cannot show tier behaviour. Both corrected upstream in JustVugg#523.
|
Rebased onto current dev ( One thing worth noting from it: dev picked up the fmt=6 E8/IQ3 container (#465, #501) in the meantime. The Vulkan paths are unaffected by construction — every VK entry point is gated on the Validation on this exact branch build (RX 9070, RADV, Mesa 26.1.4):
|
…A blocks Takes Qwen3.5-35B-A3B GPTQ-Int4 decode on the Arc B390 from 0.82 to 11.71 tok/s (all experts + attention + GDN on the iGPU). - fmt=6 expert-group bug: the shared expert is dense-quantized (fmt=2) but was folded into the fmt=6 routed-expert group; the fused gate_up shader decodes the whole batch with one fmt, so the mismatch rejected the ENTIRE group and dropped every routed expert to CPU. moe_token now folds the shared expert in only when its fmt matches (l->gsg.fmt == c->expert_fmt). 0.82 -> 7.09 tok/s. - shared expert rides the routed group: vk_dense quantizes it as fmt=6 when the container is fmt=6 (VkDense gained gs; quant_rows/vkd_make/mm_dense handle the grouped-asym format), removing ~80 mm_dense submits/token. 7.09 -> 7.59. - fused output->projection: coli_vk_gqa_project / coli_vk_gdn_project record the attention/GDN kernel + its o/out-projection matmul in ONE submit via a barrier; the intermediate ctx/y stays in GPU-only scratch (mirrors JustVugg#418 absorb_project). 7.59 -> 9.79. - fused whole GDN input block: gdn_conv.comp (causal conv1d + SiLU with a device- resident ring updated in-kernel) + gdn_delta_cv.comp (delta rule reading the raw conv output, q/k L2-norm folded in-shader). coli_vk_gdn_full chains gqkv/gz matmul -> conv1d -> qknorm+delta -> out-proj as 5 dispatches in one submit; only the tiny b/a->decay/beta stay on the CPU. COLI_VK_GDN_FUSE=0 keeps the staged path. 9.79 -> 11.71. All stages validated token-identical to the CPU/staged path. Microbenches: test_vk_gdn_fused.c (conv relerr 2e-8, delta y 3.6e-7). Diagnostics: qwen.c prints "[VK] routed experts: tier-served N", VK_EG_DBG=1 prints expert-group reject reasons.
|
Thank you a lot for your hard work! It's so exciting to see all the progress on local AI inference! 😃 Does this also help with iGPUs like the AI MAX 395? What's the correct/best setup for such a box? Set GPU RAM to max possible (e.g. 96 GB)? |
|
Rebased onto current dev ( That is a change of position from my last comment, where I said the VK paths were unaffected by the E8 container "by construction" because every entry point is gated on What it doesDecode in The rotation, which is what the expert tier actually needs. fmt=6 stores Scope is narrower than it might look:
If ValidationNew Errors are relative L2 on purpose — per-element relative error is the wrong metric for these long, sign-cancelling dot products, where individual outputs land near zero by cancellation and their relative error explodes on summation order alone. fmt=6 sits at or below the int4 baseline. Green on RADV GFX1201 (RX 9070) and llvmpipe, so both subgroup widths.
MeasuredGLM-5.2 (744B, E8 container converted with
+24%, generated text character-identical between the two. 440 experts resident in 6.36 GB = 14.45 MB/expert, exactly 3 × (98 B per 256 weights) — about 14% more experts per GB of VRAM than int3-g64, which is the capacity argument for the format. Two caveats so these are not read as more than they are. They come from a container whose CPU-side One thing worth flagging to maintainers
|
Every sync submit's vkWaitForFences pays a scheduler wake (~50-150 us) on signal; the engine fences ~2 submits per layer per token. Poll vkGetFenceStatus for a short budget first (default 300 us, the common decode dispatch completes in 0.5-2 ms), then fall back to the blocking wait. The spinning thread is stalled on the GPU result anyway. COLI_VK_SPIN_US=0 restores the pure blocking wait.
…RIPE) A cold ~19 MB expert read is single-thread latency-bound (~4 GB/s on one NVMe) and is the felt wait of every demand miss. Split the one coalesced O_DIRECT pread into disjoint 4K-aligned chunks, one per replica that holds the shard, read in parallel; chunk 0 stays on the hash-routed replica so first-chunk load spreads evenly. O_DIRECT only (no page cache, so no double-caching); any short stripe falls back to the whole read on the routed replica. Measured on the 3-drive box: felt wait 22.8 -> 18.6 s per tg128, decode +6% (1.66 -> 1.76 tok/s, new record) with the drives already near thermal limits.
…guard, rebased on current dev Deterministic rebase of the multi-worker pilot onto current dev (which carries the merged JustVugg#474 eviction guard). dev's only colibri.c delta since the branch base was the guard itself, in the original pilot_realload structure; this branch carries the guard re-applied into the SPMC multi-worker structure, so the two overlap — resolved by reparenting the byte-identity-verified tree onto dev. Contents: SPMC pilot ring + PILOT_WORKERS (default 1 = today's behavior), visible -(eid+2) reservations on the blocking loader, adversarial-review hardening, tests/test_pilot_ring.c (+ Makefile rule), and the JustVugg#474 LFRU guard in both pilot_realload and pilot_uring_batch. Draft base for the JustVugg#441 good-hardware A/B; not for merge until it shows a win. Byte-identical output across PILOT off / workers 1|8 / guard on|off (sha256, 9/9), make check 111/111. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ide the primary A self-contained context on a second Vulkan GPU hosts ONLY tier experts and runs ONLY the async expert-group path; attention, dense, q-prep and the KV mirror stay on device 0, and both groups fly simultaneously (slower device issued first, taken last). The dev2 tier fills with the next heat-ranked experts after dev0's budget stop (COLI_VK_EXPERTS2, COLI_VK_RESERVE2_GB). COLI_VK_DEV2=auto picks the best real GPU that is not device 0; an explicit index allows the same physical device (second logical device — pre-hardware test mode). Tensors carry their device (coli_vk_tensor_dev) so the moe() partition, free and byte accounting route correctly. Device-0 hot path untouched: the shared helpers only gained explicit device/phys parameters. Smoke-tested on RX 9070 + RX 580 (POLARIS10, chipset x4): tier fill, dual issue/take, correct output.
The Polaris/x4-slot submit path costs ~0.8ms per dev2-active block (3.2s/decode serialized, vs 0.15s dev0-only) while the 580's exec finishes long before take (take-wait 0.1s). Spawn issue2 on a pthread and join right before take2: the submit cost overlaps dev0's issue and the CPU expert share. G2 state is private to issue2/take2 and the join precedes take2, so the one-in-flight invariant is unchanged.
VK_PROF=1 now splits the dev2 issue cost (memcpy_x/desc/record/submit, [VK_PROF d2iss] every 2048 calls) and VK-BLOCK reports the issue worker's own elapsed vs the join wait. This localized the dev2 tax to vkQueueSubmit at 1.33ms/call — the runtime-suspended headless 580 parked at 300MHz/gen1 by DPM during microsecond-burst decode — not engine code. With the card kept awake: submit 48us, decode 2.07 tok/s.
… to the binary Field report JustVugg#523 (RX 9070 XT): with Resizable BAR disabled the HOST_VISIBLE|DEVICE_LOCAL combination exists only in a ~256 MB BAR window; the driver then places tier allocations in system RAM while the resident- experts log looks healthy, and the run comes out slower than CPU-only with nothing indicating why. Compare the chosen memory type's heap against the largest DEVICE_LOCAL heap at init and say so up front. Also from the same report: the COLI_VK_SHADERS fallback was CWD-relative, so the engine only started from inside c/. Resolve the default next to the binary (/proc/self/exe), accept a directory as well as the .spv path, and name what was tried in the startup error. Makefile note on the ROCm comparison now states what was measured (the VRAM-resident expert-group primitive, per-call) instead of implying end-to-end tok/s.
…hmarking notes The benchmarking section captures the two defaults that silently skew any Vulkan-vs-CUDA/HIP comparison (DRAFT auto-resolves to 0 under COLI_CUDA per JustVugg#163 but stays 3 elsewhere — the arms then run different decode loops; and DPM never ramps for microsecond-burst dispatches), plus what the experts-loaded/token stat actually counts.
The eroute readahead (last forward's routing, hinted at layer entry) was the one prefetch path without a vk_reg_served check. Tier-served experts are never demand-read, so their pages never stay warm: on a RAM-tight streaming box the hint re-fetches the same hot experts from disk each time cache pressure evicts them — pure wasted bandwidth on the box class JustVugg#523 measured on. Prefill (S>4) keeps the hint: the tier only serves decode, so those bytes are still needed.
The README stated "measured faster than ROCm on RDNA4" unqualified; a careful third party (JustVugg#523) measured the opposite end-to-end on a storage-bound box (the gap turned out to be a DRAFT/speculation confound, but the unqualified claim was the problem). Lead with the unassailable point — Vulkan is the only backend for cards ROCm dropped — and call the RDNA4 speed "competitive", pointing at the docs' benchmarking notes for the honest, config-dependent picture.
|
Rebased onto current dev (v1.3.0, post-#676) — conflict-free and green again. And since Kimi K3 is now upstream, this PR gains its Vulkan support, following the same pattern as the fmt=4/fmt=5 kernels already here:
Full-model behaviour on the 2.8T snapshot (62 GiB box, RX 9070): the tier registered 13 GPU hits within the first 8 steps of filling (~1.6× random — K3's short-term routing locality is real, despite its Quantile-Balancing-flat marginals), decode at parity with CPU while the tier is cold, with the I/O savings accruing as a resident server fills the remaining ~9.5 GB of budget. Docs updated ( |
|
Note on the comment above: the merge and my push crossed by a few seconds — the two Kimi commits (fmt=7 MXFP4 + the |
Vulkan: fmt=7 MXFP4 decode + Kimi K3 GPU tier (follow-up to #418)
Summary
An opt-in Vulkan compute backend (
make glm VK=1, runtimeCOLI_VULKAN=1) that runs the GLM decode compute path on any GPU with a Vulkan 1.2 driver — no CUDA, no ROCm. Three tiers, each independently switchable and each falling back to the CPU path on any failure:COLI_VK_EXPERTS, default 320): the top-N routed experts by.coli_usageheat are uploaded once at startup and served from VRAM with no RAM slot, no disk read, no prefetch — issued as one fused async batch per layer (gate+up+silu→down, hidden on-device) that overlaps with the CPU computing the remaining experts. Appears as a newvkbucket in the hit-rate line.COLI_VK_DENSE=1): q_a+kv_a fused into one submit, q_b, the o-projection, and the shared expert as a single fused expert-group submit.COLI_VK_ATTN=1): one dispatch per layer covering absorbed query → scores → softmax → weighted latent → value rows, fused with the o-projection so the context vector never leaves the GPU. The latent/rope KV lives in a persistent per-layer device mirror appended at ~2.3 KB/token/layer, with the same invalidation points as the CUDA KV shadow (row rewrite, rebind, resize).This is a continuation of @kryptt's #84 (the original backend skeleton, tensor-resident
coli_vk_matmul, and the int4 nibble-8 shader decode are his design — thank you!). It addresses the two things asked for there: it is rebased onto currentdev, and it ships a CPU-reference exactness harness covering every primitive.Why
libvulkan+ a Vulkan 1.2 ICD with subgroup arithmetic, andglslcat build time.Correctness
gcc -O3 -DVK_TEST backend_vulkan.c -o test_vk -lvulkan -lm && ./test_vk shaders/qmatmul.spv): CPU-reference exactness for every primitive — int4/int8 GEMV across shapes including the long-row o-projection (I=16384), fused gate+up, the full expert group (sync and async issue/take, verified bit-identical to each other), the q_a+kv_a pair, and the absorb attention core including causal S=2,kv_startwindows, int8, and long-context (T=2000) cases. Typical maxrel 1e-5…2e-3 (fp32 reduction order on 6144-long dots).#ifdef COLI_VULKAN;make glmwithoutVK=1compiles the same code as before (plus three always-false locals).make test-cfully green on the branch.Measured performance
Hardware/software: AMD RX 9070 16 GB (RDNA4/gfx1201, PCIe 4.0 x16 effective), Ryzen 9 3900X (12C/24T Zen2,
OMP_NUM_THREADS=12, threads pinned), 64 GB DDR4-3200 dual-rank, GLM-5.2 744B int4 streamed from NVMe (~19 MB/expert). Mesa/RADV 26.1.4, Vulkan 1.4 ICD; ROCm 7.2.4 + HIP for the comparison backend. Model: 78 layers, 256 experts/layer, kv_lora 512, MLA/DSA.Primitive microbenchmarks (per-call including readback, K-expert int4 MLP 6144→2048→6144):
End-to-end GLM-5.2 decode (same box, same engine settings, expert-routing top-p 0.7, drafts off, interleaved/batched A/B runs; our fork additionally carries an unrelated dual-SSD-mirror patch on the same base — active for both backends in every comparison, so the deltas are backend-attributable):
Run-to-run variance on this box is ±0.1 tok/s; the 64-token advantage (~+13%) narrows to ~+5% at long form as the attention window and expert-I/O share grow for both backends.
The two memory-type rules that made it work (documented in
docs/vulkan.md, learned the hard way):HOST_CACHED— reading write-combined ReBAR VRAM from the CPU runs at ~40 MB/s and was a hidden 5.6× per-call penalty.HOST_VISIBLE|DEVICE_LOCAL, so uploads are plain memcpys and the GPU reads at VRAM speed.Integration surface
c/backend_vulkan.c(~1.3k lines, plain C99 + libvulkan),c/backend_vulkan.h,c/shaders/qmatmul.comp,c/shaders/qmatmul_gate_up.comp,c/shaders/attention_absorb.comp,docs/vulkan.md.c/glm.c(hooks under#ifdef COLI_VULKAN),c/Makefile(VK=1block +glslcrule),docs/ENVIRONMENT.md,README.md.COLI_VULKAN,COLI_VK_SHADERS,COLI_VK_EXPERTS,COLI_VK_DENSE,COLI_VK_ATTN.One operational note worth flagging beyond this PR: the engine's OMP self-tune (
OMP_WAIT_POLICY=active+ spin) is skipped underCOLI_CUDA/COLI_METALbut not under other configurations. With 12 pinned spinning threads the async I/O pool starves (measured 28 → 5 GB/s CPU expert bandwidth, ~2.8× end-to-end).docs/vulkan.mdtells Vulkan users to setCOLI_NO_OMP_TUNE=1; extending the self-tune skip to any GPU backend (or toPIPE=1runs) may be worth a separate look.Limits / future work
S≤4; prefill keeps the CPU/batched paths (dense projections do run on VK at prefill, which is why prefill got faster).KV8=1(already implemented and validated againstkv_fp8.hon our side — the absorb shader decodes e4m3 in place, 4× less mirror traffic; this PR just cleanly falls back to CPU attention when the f32 cache is absent), cooperative-matrix prefill kernels, and a fully resident-layer pipeline.How to run