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 |
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.