Add Qwen3.6-35B-A3B engine: Vulkan MoE backend, resident expert pinning, serve GPU support + fixes - #602
Conversation
β¦perts, and serve GPU support
- New Phase-2 engine (c/qwen36.c): Gated Attention (GQA + partial RoPE)
+ Gated DeltaNet recurrent linear attention + streaming MoE (shared
expert + group-limited top-k). Targets Qwen3.6-35B-A3B (35B params,
3B active, Apache-2.0); runs on 16GB RAM via on-demand expert streaming.
- Vulkan MoE backend (c/vulkan_gemv.c + headers): int4 weights unpacked
in-shader + float GEMV, working around the AMD OpSDotKHR/int8 compiler
segfault on integrated GPUs. Auto-probes a Vulkan compute device and
silently falls back to CPU.
- Fix GPU zero-output regression:
1. vg_expert_loaded() was called with layer/expert-index arguments
swapped, uploading weights into the wrong slot (shader read zeros).
2. the int4 container (g4/u4/d4) was gated behind vg_use_int4(), so it
was never filled when experts loaded before vg_init.
Verified: GPU vs CPU MoE cosine = 1.00000 and byte-identical output.
- Resident expert pinning (COLIBRI_RESIDENT=1/2): pin the experts a prompt
actually uses into RAM (and, via the GPU g_w buffer, into VRAM) to remove
repeated disk IO. mode 2 accumulates across the decode phase.
- Serve mode (c/qwen36_serve.c): wire up the Vulkan backend, and fix two
HTTP POST parsing bugs (Expect: 100-continue was not answered; the request
buffer was NUL-truncated in place, breaking the body search). Now returns
valid OpenAI-format responses.
- Conversion toolkit (c/tools/convert_qwen36.py, make_qwen36_oracle.py,
make_qwen36_tiny.py) and design docs under docs/qwen36-*.md.
- Benchmark (c/int4_vs_int8_cold.md): on an integrated-GPU 16GB machine, a
cold-start comparison (N_NEW=256, long prompt) shows int8 CPU-only is
fastest (1.08 tok/s, 10.25GB peak) because the iGPU shares system memory
and gains no bandwidth, while int4 pays an unpack penalty. int4 stays
half the on-disk size.
|
Thanks for this β it's a substantial, well-built contribution, and we'd like to bring the Qwen3.6-35B engine in. Two things before it can land, both about shape, not quality:
Land the engine piece on |
|
|
|
Context that affects this PR: #418 is now merged to That makes it the project's Vulkan backend, and this PR carries a second one ( Concretely, the ask when you rebase: split this into the Qwen3.6 engine alone, dropping the Vulkan backend (use #418) and the standalone HTTP serve ( There's also #544 (@opxyc) adding a Qwen3-30B-A3B engine β same family, different file. Worth the two of you agreeing on one Qwen engine before either lands; @steve-m and @ZacharyZcR did exactly that on Kimi last week and it worked well. |
|
Hi @minne100, first of all: thank you for #602 β the qwen36 engine, the converter and the Your expectation was right β dedicated GPUs do show the opposite result, but
A few findings from our runs that may be useful for your Vulkan path:
To be clear, we see the two GPU paths as complementary: your Vulkan/iGPU |
|
Closing in favor of #712 (engine slice, with co-authorship) β thanks @kreuzzelg for picking up the dedicated-GPU benchmarking I left open in the original test plan. Local follow-ups I've applied (happy to fold into #712 or a separate Vulkan PR):
A Vulkan MoE GEMV backend for integrated/AMD GPUs (this implementation) is complementary to #418 (steve-m, CUDA/NVIDIA-oriented). I'm opening a separate draft PR on dev for it. Happy to coordinate so we don't end up maintaining two backends if you'd rather unify. |
Summary
This PR adds a complete, self-contained engine for Qwen3.6-35B-A3B (35B params / 3B active, Apache-2.0) to colibri, plus a set of fixes and a benchmark. The goal is to run this large MoE model on a 16 GB machine by streaming experts from disk on demand.
What's included
c/qwen36.c) β Gated Attention (GQA + partial RoPE,rope_dim=64) blended with a pure-C Gated DeltaNet recurrent linear-attention path (per-layerDN_rec/DN_convstate), followed by a streaming MoE (shared expert + group-limited top-k). Hybrid layers use Gated Attention ati%4==3(10/40 layers), the rest are DeltaNet.c/vulkan_gemv.c+ headers) β int4 weights are unpacked inside the shader and multiplied as float GEMV. This is a deliberate workaround for the AMD 780M driverOpSDotKHR/int8 compiler segfault (0x800184): we only useOpCapability Shaderand never touch int8 types. The backend auto-probes a Vulkan compute device and silently falls back to CPU when none is present (dynamicvulkan-1load, zero-dependency binary).vulkan_gemv.c:vg_expert_ensure β vg_expert_loadedhad itslayer/expert-idarguments swapped (loaded(layer, eid, li)was called as(layer, li, eid)), so weights were uploaded to the wrong slot and the shader read zeros. The self-test missed this becauseeid==liby coincidence.load_expert_merged's int4 slice was gated behindvg_use_int4(), so the int4 container (g4/u4/d4) was never filled when an expert loaded beforevg_init. Now the int4 container is always populated.Verified: GPU vs CPU MoE cosine = 1.00000 at layer 0 / token 0, and byte-identical generated text vs CPU-only.
COLIBRI_RESIDENT=1|2) β pin the experts a prompt actually routes to into RAM (and, via the GPUg_wbuffer, into VRAM) so they are not repeatedly evicted and re-streamed from disk.=1pins once after prefill;=2keeps collecting and pinning incrementally across the decode phase. Per-layer pin budget =capto avoid LRU deadlock.c/qwen36_serve.c) β integrates the Vulkan backend (enabled automatically perg_gpu_backend) and fixes two HTTP POST parsing bugs:Expect: 100-continuewas never answered, so clients that wait for100never sent the body.handle_connNUL-truncated the request buffer in place at the first space (*sp1=0), which destroyed the body and madestrstr(req,"\r\n\r\n")fail β400. Now method/path are copied into local arrays and the full buffer is searched.Verified end-to-end:
/v1/chat/completionsreturns valid OpenAI-format JSON ("The capital of France is Paris.").c/tools/convert_qwen36.pydefault true-int4,make_qwen36_oracle.pyattention-only,make_qwen36_tiny.pyweight-free smoke) and design docs (docs/qwen36-*.md).c/int4_vs_int8_cold.md): cold-start comparison on an integrated-GPU 16 GB laptop (AMD 780M, no dedicated VRAM), long prompt,N_NEW=256.Benchmark (cold start, long prompt, default GPU unless noted)
Takeaway: on this iGPU machine, int8 CPU-only is the fastest and smallest β the integrated GPU shares system memory and gains no bandwidth, while int4 pays a CPU-side unpack-to-int8 penalty before upload. int4's only hard advantage is half the on-disk size (19.7 GB vs 34.6 GB). A dedicated GPU with the experts resident in VRAM is expected to show the opposite result; that path is implemented (
COLIBRI_RESIDENT) but not yet measured on such hardware.Test plan
convert_qwen36.py --selftest) passesCOLIBRI_RESIDENT=2prefill pin hit-rate ~39%, text correct, <16 GBNotes / limitations
modles/) and the Vulkan SDK headers (c/vulkan/) are intentionally git-ignored; the generated*.spv.hheaders are committed.