qwen36: Vulkan expert tier, and staged device-local uploads for cards without Resizable BAR - #1338
crichalchemist wants to merge 15 commits into
Conversation
Prepares the backend for a second thread uploading weights while the decode thread submits expert groups (qwen36 tier). No behaviour change on one thread. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Picks a DEVICE_LOCAL-only memory type and a non-BAR staging type, creates the upload command buffer/fence, and reports the mode. Uploads still use the mapped path; the next commit switches them. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Scratches, KV mirror and readbacks keep their memory types. New device-local blocks are filled once with vkCmdFillBuffer before use: without it, results computed from a fresh block differ slightly and non-deterministically; with it (any fill value — 0x00 and 0xFF measured identical) all 50 harness cases are byte-identical in both modes on RX 580 (gfx803). The fill value being irrelevant means the mechanism is the first GPU-side touch of the allocation, not a read of filler bytes; the underlying cause is unresolved and reported as a finding. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Same lookup colibri.c and kimi_k3.c each carry privately; the qwen36 tier uses this one. The two existing copies are left for a later cleanup. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
No behaviour change on CUDA: the placement logic now calls ten be_* operations and a Vulkan implementation slots in next. Header guard admits COLI_VULKAN. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Routed experts are served from VRAM through coli_vk_expert_group_issue/take. Budget via VK_EXPERT_GB (auto = device budget minus 1 GB). No runtime LFRU swaps: the Vulkan arena never reclaims a freed slice, so residency is decided at warmstart (HEAT_FILE order). test_qwen36_tier_vk checks GPU-vs-CPU output and the fill-once rule; it skips without VK=1 or a device. Also fixes a pre-existing race in the shared uploader: qt_fill_wait() checked G.qn (the ring-buffer depth, freed at dequeue time) instead of tracking real upload completion, so it could return before the last queued expert's be_upload() had actually finished. Harmless on fast synchronous CUDA copies, but on staged Vulkan uploads (submit + fence wait) the window was wide enough to fail deterministically -- confirmed by tracing be_upload() completions against the test's residency check, and by COLI_VK_STAGED=0 (no staging submit) passing cleanly with the unmodified logic. Added a separate G.inflight counter, incremented on enqueue and decremented only when a queued item's upload actually completes, and switched qt_fill_wait() to wait on it instead of G.qn. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
The banner names CUDA or Vulkan, and a build without the tier says so once when COLI_VULKAN is set instead of ignoring it (refs JustVugg#894). Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
…loads Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
…ing cleanup, docs - qwen36_tier: header identity and qt_fill_wait contract state completion, not dequeue; G.ybuf allocated only on the single-device (Vulkan) build; qt_take logs once and clears the count when a take fails. - backend_vulkan: stage scratch handles nulled on free/failure (no double free at shutdown on OOM); COLI_VK_STAGED accepts exactly 0/1, else auto; small-BAR hint only when =0 was set; COLI_VK_SPIN_US read once at init. - docs: CUDA-only semantics labelled in the tier doc; COLI_VK_DEV noted; Polaris validation moved out of "Limits"; ENVIRONMENT row for COLI_VK_STAGED; CI tier gate uses pipefail and explains the skip policy. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
Since JustVugg#1334 the tier promotes int8 experts, and on a Vulkan build they are served through coli_vk_tensor_ensure(fmt 1) — a path neither JustVugg#1334 nor the Vulkan tier exercised on its own. Run it on a real device with the same three checks the int4 gate runs. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N
a5872f5 to
5e4ffda
Compare
|
Rebased onto current What the merge had to absorb, all inside the backend shim this branch introduces: the fp8 streaming mode, the resident lm_head and DeltaNet projections, automatic placement, and the cudaMalloc-granularity accounting. The shim gained three entry points ( The one test change: since d0a382d an expert is charged at allocator granularity, so Verified locally: the CPU build and the eleven other qwen36 test binaries; and, new since the first push, (Edited: an earlier version of this comment said "Apple GPU"; the device is the AMD Radeon Pro 580.) Ready for review whenever someone has the time. |
Brings the Vulkan expert tier onto the tier as it stands after JustVugg#1344, JustVugg#1360, the fp8 streaming mode (979025c), the resident dense trunk (868852a, 85c90c4), automatic placement (ff13134) and the cudaMalloc- granularity accounting (d0a382d, 40ff645). Resolution, all inside the backend shim that this branch introduced: - be_fp8_set_lut: CUDA publishes the e4m3 table; Vulkan returns 0, so qt_init_fp8 lands on the CPU path with the existing message. - be_trunk_upload / be_trunk_matmul: the resident lm_head and DeltaNet projections go through the shim. CUDA maps them to tensor_upload(fmt=1) and coli_cuda_matmul; Vulkan refuses (one stderr line) and the pieces stay on the CPU, because backend_vulkan has no matmul over an already-uploaded tensor yet. - The init sequence keeps upstream's affinity widening around be_init and the single-device short-circuit around the COLI_GPUS/COLI_GPU parsing; the budget reads QT_BUDGET_ENV and be_mem_info. - ybuf (the Vulkan take target) is allocated next to the per-device replica buffer, whose sizing is upstream's (JustVugg#1339). - docs/qwen36-cuda-tier.md is restored as upstream has it (it grew the placement calibration meanwhile); docs/qwen36-tier.md now covers only what differs on Vulkan and points there for the mechanics. Verified on macOS 13 x86_64 (2017 iMac, i7-7700K): the CPU build, the seven fake-CUDA tier tests and the four other qwen36 tests build and pass; qwen36 and the two Vulkan tier tests build with VK=1 against MoltenVK on the Radeon Pro 580, and the tier initialises on that GPU (the test budget is adjusted in the next commit).
b0503af to
2bebf00
Compare
…tprint Since d0a382d the tier charges an expert at the device allocator's granularity (six allocations, 8 KiB minimum each), not by payload. At the test geometry that is 49152 bytes per expert in both int4 and int8 mode, so the hand-computed 21474/26843-byte budgets admitted nothing and the test failed with resident=0. One budget for both modes now, sized to admit two experts and not three, with the arithmetic in the comment. Same adjustment upstream made to its own tier tests in 823caa8. Passes on MoltenVK over a Radeon Pro 580 (macOS 13 x86_64) in both modes; maxrel 6.8e-6 / 4.6e-6.
|
Token-exact end to end on real Vulkan hardware, not only the unit tests. Setup: the
One thing worth knowing for anyone repeating this: on the stock 16-token reference, The two |
test_qwen36_tier_vk proves the numerics against a real Vulkan device and skips everywhere else -- which is every CI runner but the Lavapipe job. The shim's control flow had no gate that runs in make check or under the sanitizers on Linux, macOS and Windows. tests/qwen36_fake_vulkan.h is the Vulkan twin of qwen36_fake_cuda.h: it defines every coli_vk_* symbol the tier links against and records what it receives, with knobs for the budget extension, the issue result and the take result. tests/test_qwen36_tier_vk_fake.c drives the shim through it: single device (COLI_GPUS ignored), VK_EXPERT_GB budget and the 4 GB fallback without VK_EXT_memory_budget, int4-gs64 reaching the backend as fmt 4 / grp 64 at half a byte per element, fill-once (a far hotter non-resident never evicts a resident, 64 ticks, no swap, no free), issue/take through the tier's ybuf with the weighted accumulation checked lane by lane, a failed take leaving the output alone and closing the group, and the two refusals -- fp8 streaming and the resident trunk -- landing on the CPU path without touching the backend. Two mutants confirm the assertions bite: a take that always fails trips the accumulation check; QT_SWAPS=1 on the Vulkan shim trips seven. Clean under ASan+UBSan. -UCOLI_CUDA on the rule because the shim prefers CUDA when both are defined.
|
Pushed 241f7f3: the Vulkan shim now has a gate that runs without a device. It is collected by Also note #1390, which carries the |
Summary
Two things, one PR because the second is what makes the first measurable on most older discrete cards.
Qwen3.6 Vulkan expert tier.
make qwen36 VK=1builds the existingqwen36_tier.cagainst the shared Vulkan backend. The placement logic (warmstart, heat file, upload queue, hit/miss accounting) is unchanged; a compile-time shim of twelve one-line wrappers selects CUDA or Vulkan (#if COLI_CUDA / #elif COLI_VULKAN, CUDA wins if both). On Vulkan the tier is single-device and fills once at warmstart: the Vulkan weight arena never reclaims a freed slice, so runtime LFRU swaps would leak one expert of VRAM each. Heat still accumulates and saves, so the second run starts hot. Budget isVK_EXPERT_GB(auto = device budget minus 1 GB). The engine names its backend in the banner and, on a build without the tier, says so once whenCOLI_VULKANis set instead of silently ignoring it (refs #894).Staged device-local uploads in
backend_vulkan.c. Without Resizable BAR the HOST_VISIBLE|DEVICE_LOCAL type is a ~256 MB window and everything past it silently spills to system RAM, so the tier reports resident experts that every access fetches over PCIe. When the host-visible slice is under a quarter of VRAM (orCOLI_VK_STAGED=1), resident weights now go to a DEVICE_LOCAL-only arena through a host staging buffer andvkCmdCopyBuffer. Scratches, the KV mirror and readbacks keep their memory types. Queue submits and arena allocation are mutex-protected so the tier's upload thread is safe next to the decode thread.Validation
Hardware: AMD Radeon RX 580 8 GB (Polaris, gfx803, 256 MB host-visible BAR), Mesa 25.2.8 RADV, i7-7700K, 62 GB RAM, Ubuntu 24.04. First validation of the Vulkan backend on Polaris.
-DVK_TEST), mapped vs staged: 50/50maxrelvalues identical; staged batched int4 matmuls ~4× faster on this card (0.17 vs 0.66 ms/matmul).tests/test_qwen36_tier_vk(part ofmake check; skips withoutVK=1or a device): GPU output vs CPU int4 reference maxrel 6.7e-06, misses return no mask bit, residency frozen after warmstart.HEAT_FILEon runs 2–3 and the mapped arm, GPU clocks not pinned (no root):COLI_VK_STAGED=0)Runs 2 and 3 are token-identical; CPU vs GPU output is token-identical on this prompt. Measured on the tree rebased onto current
dev(tier commit bb16ab3); an earlier measurement of the same code before the rebase read 7.5 / 4.2 tok/s for the warm and mapped arms, so treat the absolute numbers as ±15 % (clocks unpinned) and the CPU→Vulkan and mapped→staged ratios as the result.VK=1build (done manually: there is noqwen36-tiny-checktarget).make checkgreen;make qwen36,make qwen36 VK=1,make colibri VK=1build with 0 warnings from the touched files. CI's Vulkan job now also buildsqwen36 VK=1and runs the tier gate on Lavapipe.Compatibility
ldd qwen36links no libvulkan withoutVK=1).VK=1is opt-in.qt_fill_waitcould return before the last upload finished while the engine frees the RAM int8 copies right after. Deterministic on the slower staged path (5/5), latent on CUDA. Now an in-flight completion counter;qt_fill_waitreturns strictly later.vkCmdFillBufferon creation. The fill value is irrelevant (0x00 and 0xFF measured identical), so it is the first GPU-side touch of the allocation, not a read of filler bytes. The fill is in; the cause is open (no GPU validation layers on my box). Repro: skip the fill and run the harness twice withCOLI_VK_STAGED=1.qt_issuestridesG.is_xbydi*8*Dwhile it is sized32*D, so a two-GPU CUDA run with a wide top-k can overrun;qt_shutdownnever signalscv_take, so a thread parked inqt_fill_waitwould not seeth_stop.dev(f58a267). fix(qwen36): the VRAM tier now promotes int8 experts instead of reserving for nothing #1334's int8 expert promotion is kept verbatim and its uploads go through the shim as fmt 1 on both backends; the int8-on-Vulkan path, which neither side had alone, gets its own gate (test_qwen36_tier_vk_int8, runs on Lavapipe in CI). The whole validation above was re-run on the rebased tree.docs/qwen36-cuda-tier.md→docs/qwen36-tier.md(link updated),docs/vulkan.md,docs/ENVIRONMENT.md(COLI_VK_STAGED,VK_EXPERT_GB), CHANGELOG.🤖 Generated with Claude Code
https://claude.ai/code/session_019BacNGNxAJ1M57UdYE2M3N