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.c β€” openai_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.

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