Skip to content

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

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

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.

@JustVugg

Copy link
Copy Markdown
Owner

dev has moved a fair amount (v1.3.0: Kimi K3 and Inkling engines, the ragged-attention grouped-scale fix, CUDA_RELEASE_HOST default, the Inkling expert-cache fix). This PR now conflicts — could you rebase onto latest dev? Nothing is wrong with the change itself; CI was green where it ran. Ping me once it's clean and I'll review/merge.

@JustVugg

Copy link
Copy Markdown
Owner

Context that affects this PR: #418 is now merged to dev@steve-m's Vulkan backend (expert tier + dense + MLA attention, any Vulkan 1.2 driver, opt-in via VK=1/COLI_VULKAN=1).

That makes it the project's Vulkan backend, and this PR carries a second one (vulkan_gemv.c, vulkan_core.h). Two Vulkan implementations in one repo is the kind of duplication we'd rather not carry — and you'd inherit steve-m's shader work and the field reports coming from #523/#510 for free.

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 (qwen36_serve.c, test_http_parse.copenai_server.py already serves every engine, and Kimi/Inkling both route through it). That takes the PR from +15,515 to something reviewable, and the engine is the part only you have written.

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.

@kreuzzelg

Copy link
Copy Markdown

Hi @minne100,

first of all: thank you for #602 — the qwen36 engine, the converter and the
tiny/oracle validation setup are excellent groundwork. We had a machine with
dedicated GPUs (RTX 3070 8 GB + Quadro RTX 4000 8 GB) and picked up exactly
the item you left open in your test plan: "measure on a dedicated-GPU
machine"
.

Your expectation was right — dedicated GPUs do show the opposite result, but
only once the experts are truly resident in VRAM. Following the
maintainers' guidance (split, target dev, engine first), we submitted:

A few findings from our runs that may be useful for your Vulkan path:

  1. The gpu_probe constants in qwen36.c (VK_QUEUE_COMPUTE_BIT as 0x20,
    stride 32) prevent the probe from ever enabling the GPU on NVIDIA/Linux
    (0x20 is VIDEO_DECODE; the struct is 24 bytes).
  2. With large caches (>4 GB weight pool) the 32-bit slot arithmetic in
    vulkan_gemv.c wraps and aliases slots (layers 32–39 ⇄ 0–7), and the
    precompiled GEMV shader itself cannot address past 4 GB — we hit both at
    cache=256. Fixes are on our branch if useful.
  3. Your published i4/i8 containers just need two small files to run out of
    the box: tokenizer.json and a flattened config.json — the updated
    converter in feat(qwen36): Qwen3.6-35B-A3B engine (CPU): hybrid Gated Attention + Gated DeltaNet + streaming MoE #712 bundles both; a self-contained container is here:
    https://huggingface.co/Kreuzzelg/qwen36-35b-a3b-colibri-i4

To be clear, we see the two GPU paths as complementary: your Vulkan/iGPU
approach is the right answer for shared-memory laptops and AMD (no CUDA),
the CUDA tier for dedicated NVIDIA cards. Happy to coordinate however works
best for you — and thanks again for the foundation.

@minne100 minne100 closed this Jul 31, 2026
@minne100

Copy link
Copy Markdown
Author

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):

  1. gpu_probe bug: VK_QUEUE_COMPUTE_BIT was 0x20 (VIDEO_DECODE_BIT), so the GPU was never probed on NVIDIA/Linux. Fixed to 0x00000002u in qwen36.c.
  2. vulkan_gemv.c 32-bit slot wrap: slot byte offsets are uint32_t, so any weights pool >= 4GB wraps/aliases (layers 32-39 -> 0-7 at cache>=256). Added a >4GB guard that disables the Vulkan backend and falls back to the CPU MoE path instead of silently corrupting output. 64-bit shader offsets (buffer_device_address) are a follow-up.
  3. Published i4/i8 containers now include tokenizer.json (was missing) — uploaded to both HF repos, so they run out of the box.

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.

kreuzzelg added a commit to kreuzzelg/colibri that referenced this pull request Aug 3, 2026
…n + Gated DeltaNet + streaming MoE

Self-contained engine for Qwen3.6-35B-A3B (35B total / 3B active, 256 experts
per layer), split out of PR JustVugg#602 per maintainer guidance (engine first, GPU
backends as follow-ups; no shared GLM/Inkling files touched):

- c/qwen36.c: 40-layer hybrid forward (10x Gated Attention with partial RoPE
  + per-head q/k RMSNorm; 30x Gated DeltaNet recurrent linear attention with
  carried conv ring + per-head state), streaming MoE with per-layer LRU expert
  cache, shared expert, router lookahead prefetch (PILOT), resident-expert
  pinning (COLIBRI_RESIDENT), per-phase timers (COLI_TIMERS=1).
- CPU performance work: optional per-row int8 quantization of the large dense
  matrices (COLI_DENSE_I8, on by default, f32 originals freed), hand-written
  AVX2/FMA int8 GEMV, DeltaNet recurrence parallelized per value head.
  Decode on a 12-core Zen2 + int4 container: ~2.5 tok/s CPU-only
  (vs 0.35 tok/s scalar baseline).
- c/tools/convert_qwen36.py: HF -> container converter (true int4 packing,
  per-row scales, --selftest). Containers now run out of the box: config.json
  is written flat (VL checkpoints nest dims under text_config) and
  tokenizer.json is bundled.
- tools/make_qwen36_tiny.py + make_qwen36_oracle.py: weight-free tiny model
  and oracle for engine validation without the 70 GB checkpoint.
- docs/qwen36-phase01.md, qwen36-phase02.md: design notes.

Numerics: logits cosine vs f32 reference 0.9992 with COLI_DENSE_I8=1,
bit-identical with COLI_DENSE_I8=0.

Co-authored-by: minne100 <xufenglu@hotmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

model-support Supporto a nuovi modelli needs-rebase Confligge, serve rebase dell'autore vulkan Backend Vulkan/AMD

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants