Skip to content

Add Qwen3.6-35B-A3B engine: Vulkan MoE backend, resident expert pinning, serve GPU support + fixes#602

Open
minne100 wants to merge 2 commits into
JustVugg:mainfrom
minne100:feat/qwen36-gpu-residency-serve
Open

Add Qwen3.6-35B-A3B engine: Vulkan MoE backend, resident expert pinning, serve GPU support + fixes#602
minne100 wants to merge 2 commits into
JustVugg:mainfrom
minne100:feat/qwen36-gpu-residency-serve

Conversation

@minne100

Copy link
Copy Markdown

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

  • Phase-2 engine (c/qwen36.c) — Gated Attention (GQA + partial RoPE, rope_dim=64) blended with a pure-C Gated DeltaNet recurrent linear-attention path (per-layer DN_rec/DN_conv state), followed by a streaming MoE (shared expert + group-limited top-k). Hybrid layers use Gated Attention at i%4==3 (10/40 layers), the rest are DeltaNet.
  • Vulkan MoE backend (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 driver OpSDotKHR/int8 compiler segfault (0x800184): we only use OpCapability Shader and never touch int8 types. The backend auto-probes a Vulkan compute device and silently falls back to CPU when none is present (dynamic vulkan-1 load, zero-dependency binary).
  • Bug fix: GPU produced all-zero output on the real 35B int4 model. Two root causes, both in vulkan_gemv.c:
    1. vg_expert_ensure → vg_expert_loaded had its layer/expert-id arguments 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 because eid==li by coincidence.
    2. load_expert_merged's int4 slice was gated behind vg_use_int4(), so the int4 container (g4/u4/d4) was never filled when an expert loaded before vg_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.
  • Resident expert pinning (COLIBRI_RESIDENT=1|2) — pin the experts a prompt actually routes to into RAM (and, via the GPU g_w buffer, into VRAM) so they are not repeatedly evicted and re-streamed from disk. =1 pins once after prefill; =2 keeps collecting and pinning incrementally across the decode phase. Per-layer pin budget = cap to avoid LRU deadlock.
  • Serve mode (c/qwen36_serve.c) — integrates the Vulkan backend (enabled automatically per g_gpu_backend) and fixes two HTTP POST parsing bugs:
    1. Expect: 100-continue was never answered, so clients that wait for 100 never sent the body.
    2. handle_conn NUL-truncated the request buffer in place at the first space (*sp1=0), which destroyed the body and made strstr(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/completions returns valid OpenAI-format JSON ("The capital of France is Paris.").
  • Conversion toolkit (c/tools/convert_qwen36.py default true-int4, make_qwen36_oracle.py attention-only, make_qwen36_tiny.py weight-free smoke) and design docs (docs/qwen36-*.md).
  • Benchmark (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)

Metric int4 +GPU int8 +GPU int8 CPU-only
First-token latency (TTFT) 50.18 s 28.68 s 13.83 s
Throughput 0.29 tok/s 0.48 tok/s 1.08 tok/s
Peak RSS 11.30 GB 11.27 GB 10.25 GB
256-token wall time 873 s 532 s 238 s

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

  • int4 self-test (convert_qwen36.py --selftest) passes
  • GPU vs CPU MoE cosine = 1.00000; byte-identical generation
  • COLIBRI_RESIDENT=2 prefill pin hit-rate ~39%, text correct, <16 GB
  • serve POST returns valid OpenAI JSON (curl round-trip)
  • measure on a dedicated-GPU machine (not available here)

Notes / limitations

  • NFC normalization is not implemented (the tokenizer assumes NFC input).
  • The AMD integrated-GPU path uses float GEMV (not int8 dot) by design.
  • Model weights (modles/) and the Vulkan SDK headers (c/vulkan/) are intentionally git-ignored; the generated *.spv.h headers are committed.

minne100 added 2 commits July 25, 2026 10:07
…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.
@JustVugg

Copy link
Copy Markdown
Owner

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:

  1. Retarget to dev, not main. All contributions merge into dev (main is release-only and protected). Please change the base branch to dev and rebase onto it.

  2. Split it — this is +15,515 lines in one PR. It bundles at least three separable things: (a) the Qwen3.6 engine (qwen36.c / qwen36_serve.c / converter / oracle tooling), (b) a Vulkan MoE backend, and (c) resident-expert pinning + serve GPU changes. We can't responsibly review or merge that as a single unit, and if any one piece needs iteration it blocks the whole thing. Please break it into an incremental series we can land one at a time — ideally starting with the self-contained engine (new qwen36.c that doesn't touch the shared GLM/Inkling path), which is the lowest-risk piece and can go in first. The Vulkan backend is its own big review and also overlaps Vulkan backend: expert tier + dense + MLA attention on any Vulkan 1.2 GPU (successor to #84) #418 / WIP: MiniMax-M3 support — GQA + MSA block-sparse attention, o200k tokenizer, converter (follow-up to #418) #601 — worth coordinating so we don't end up with two Vulkan backends.

Land the engine piece on dev first, and we'll take the GPU/Vulkan parts as follow-ups. Happy to review each increment quickly.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants