Skip to content

Qwen3-Coder-30B-A3B (qwen3_moe) on the qwen36 engine - #1828

Merged
JustVugg merged 4 commits into
devfrom
feat/qwen3-moe
Oct 2, 2026
Merged

JustVugg merged 4 commits into
devfrom
feat/qwen3-moe

Conversation

@JustVugg

@JustVugg JustVugg commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Qwen/Qwen3-Coder-30B-A3B-Instruct (Apache-2.0, qwen3_moe) runs on the qwen36 engine: text, tool calls and streaming, through coli chat, coli serve and coli web. Its REAP prunes (same architecture, 103 experts) resolve to the same family under their own name.

What it took

Qwen3 MoE differs from the Qwen3.5/3.6 layers this engine was written for in four places: no DeltaNet (every layer attention), no attention output gate, no shared expert, and plain RMSNorm weights with rotary over the whole head.

  • Engine (qwen36.c): shared_inter 0 is accepted for a routed model, and the shared expert is skipped at load and in both compute paths. Attention multiplied a missing gate as sigmoid(0) = 0.5 on every layer; without a gate the output now passes through. Qwen3.5/3.6/3.8 always carry the gate, so their numerics are unchanged: the 35B, 2.4T and 27B-dense tiny oracles are 16/16 before and after.
  • Converter: for model_type: qwen3_moe it writes all-attention layer_types, shared_inter: 0, partial_rotary_factor: 1.0, zero_centered_norms: false, and M-RoPE sections that cover the rotary half (one position per token, so ordinary RoPE). It also reads num_local_experts, the name transformers 5 writes.
  • Registry: qwen3_moe maps to the qwen36 family, the variants are named by geometry (Qwen3-Coder-30B-A3B, qwen3-coder-30b-a3b-colibri; Qwen3-Coder-REAP-25B-A3B), and the planner no longer requires DeltaNet keys when every layer is attention.
  • Gateway: Qwen3-Coder's template is detected from the container (detect_chat_flavor) and rendered by render_chat_qwen3_coder: its XML tool block, its default system line, tool responses sharing one user turn, no thinking. enable_thinking is forced off for it (otherwise a request asking for it would file the whole answer as reasoning, as [Bug]: OLMoE streaming responses drop generated content #984 did on OLMoE). The Qwen parser reads its calls, and now reads a declared boolean written as True/False as a boolean, which is how the model writes it (seen on the real model; Qwen's own parser does the same).

Correctness

  • Tiny oracle in CI: make_qwen36_tiny.py --geometry qwen3-coder-30b builds the model with transformers' Qwen3MoeForCausalLM (RMSNorm weights drawn around 1, so a zero-centered reading cannot pass); converted, the engine matches it 16/16 at caches of 1 and 8. Before the gate fix it matched 1/16, with each layer's attention output exactly half of transformers'.
  • Template: tests/test_qwen3_coder_chat_template.py renders the release's chat_template.jinja (fixture, sha256 pinned) with jinja2 the way transformers does and compares byte for byte: single turns, system, history, tools with extra schema keys, an agent loop with parallel calls and tool results, a tool result last, and the open turn.
  • Real checkpoint against bf16: a 325-token code question and answer, teacher forced, every position's logits against a reference that reads the bf16 release one layer at a time in f32 (that reference matches transformers to 4.8e-7 on the tiny model):
experts dense trunk top-1 = bf16 top-5 overlap mean |Δ log p|
int4 gs64 int8 (default) 96.9% 91.1% 0.176
int4 gs64 f32 96.0% 91.7% 0.134
int8 int8 95.1% 95.2% 0.098
int8 f32 98.1% 97.8% 0.022

Measured

Ryzen 7 PRO 8700GE, 64 GB, NVMe RAID, nothing else running; CLI from a cold cache, 128 tokens, dense trunk int8:

container experts cached per layer decode resident
int4 gs64 (19 GB) 128 8.5 and 9.6 tok/s (two runs) 15.2 GB
int8 (30 GB) 128 6.4 tok/s 25.1 GB
int4 gs64 32 5.1 tok/s 6.5 GB
int8 32 3.8 tok/s 9.6 GB

coli chat with --cap 128 and a warm cache streams a 577-token answer at 13 tok/s. Conversion takes under a minute per container.

On the real model, every front end

  • Gateway: a code answer, enable_thinking: true answered in content (not reasoning), a tool call run_tests({"path": "/srv/app", "verbose": true, "workers": 4}) with both types restored, and streaming.
  • coli chat (attached) and coli web:

Qwen3-Coder-30B-A3B in coli web

One thing to know, not changed here: like the rest of this engine, coli serve/coli chat size the expert cache from --cap, not from --ram (docs/qwen36.md); without --cap the engine starts at the routing floor (8 experts per layer) and runs at about 2.7 tok/s. coli plan already computes the right cap (128 here); passing it through for this family is the launcher change discussed in #1823.

docs/qwen36.md documents the checkpoint, the commands, the quality and the speed tables.

JustVugg added 4 commits October 2, 2026 01:59
Qwen3-Coder-30B-A3B is Qwen3MoeForCausalLM: every layer attention, no
shared expert and no attention output gate. The config check now accepts
shared_inter 0 for a routed model and the loader and both shared-expert
paths skip it. Attention multiplied a missing gate as sigmoid(0) = 0.5 on
every layer; without a gate the output now passes as it is (Qwen3.5/3.6/3.8
always carry one, so their output is unchanged). The gate scratch is not
allocated when there is none.
For model_type qwen3_moe the converter writes what the checkpoint is: all
layers full attention, shared_inter 0, partial_rotary_factor 1.0, plain
RMSNorm weights (zero_centered_norms false) and M-RoPE sections that cover
the whole rotary half, which for one position per token is ordinary RoPE.
It also reads num_local_experts, the name transformers 5 writes.

make_qwen36_tiny.py gains the qwen3-coder-30b geometry, built with
transformers' Qwen3MoeForCausalLM and RMSNorm weights drawn around 1 so a
zero-centered reading cannot pass. CI converts it and holds the engine to
transformers token for token at caches of 1 and 8.
The qwen36 family takes model_type qwen3_moe, names the 30B and the REAP 25B
by geometry (qwen3-coder-30b-a3b-colibri), and plans it without the DeltaNet
keys it does not have. The gateway recognises Qwen3-Coder's template and
renders it byte for byte (its XML tool block, no thinking; tested against the
release's chat_template.jinja), parses its calls with the Qwen parser, and
keeps enable_thinking off so a request asking for it does not file the answer
as reasoning. A declared boolean written as True/False, the way the template
prints past values, is read as a boolean, as Qwen's own parser does. The
tests that used qwen3_moe as an unknown model_type now use mixtral.
@JustVugg
JustVugg merged commit dc8426c into dev Oct 2, 2026
31 checks passed
JustVugg added a commit that referenced this pull request Oct 2, 2026
- qwen36-metal-test links qwen36.c, which now calls the Vulkan backend, so
  under VK=1 it needs $(VK_OBJ). The binary gets a rule named after it so
  the object can be its prerequisite, as test_makefile_vk_obj requires.
- test_registry_engine_agreement no longer counts VK_OBJ as an accelerator:
  in olmoe, deepseek_v41, mimo and qwenimage, Vulkan only places resident
  matrices, opt-in, with no VRAM tier for the planner to size, which is what
  supports_accelerator promises. They stay CPU-only to the planner.
- convert_fp8_to_int4's family guard gains qwen3_moe (#1828 added it to the
  registry only). The test is skipped in CI, so only a local run showed it.
pavolbauer pushed a commit to pavolbauer/colibri that referenced this pull request Oct 2, 2026
The README the converter writes and its closing line said Qwen3.6-35B-A3B and
Gated DeltaNet for every container, so a Qwen3-Coder container (JustVugg#1828) shipped
a README about a different model. A qwen3_moe container now gets its own
README (attention layers, no DeltaNet, no shared expert, the expert format),
and the closing line counts the DeltaNet layers instead of naming them.
ggeorgovassilis pushed a commit to ggeorgovassilis/colibri that referenced this pull request Oct 18, 2026
Qwen3-30B-A3B and Qwen3-Coder as llama.cpp writes them: every layer
attention, no DeltaNet, no shared expert, no output gate on q, full
rotary, the norm after attention named ffn_norm. The engine already runs
the architecture from safetensors (JustVugg#1828); the GGUF config now takes its
numbers, and the importer writes a Qwen3MoeForCausalLM config.

Checked: the tiny Qwen3-Coder fixture (make_qwen36_tiny.py --geometry
qwen3-coder-30b) converted to an F32 GGUF by llama.cpp's own converter
gives the transformers reference's 16 of 16 greedy tokens, on the exact
and on the integer path; unsloth's Qwen3-Coder-30B-A3B-Instruct Q4_K_M
passes GGUF_CHECK=1 from its header (579 tensors, 18.55 GB).
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.

1 participant