Repository navigation
GGUF-on-NPU: all six architectures answer on --device npu (qwen3moe, qwen35moe, GLM fixes), tokenizer and serve fixes - #209
Conversation
The converter's 'k_b to the MLA math grid' transpose (FLM_Q4NX_Converter 62594563f) is inverted. Measured directly with the converter's own _pack_2d_float on GLM's blk.0.attn_k_b.weight: the dequantised per-head slice is [512][192] (kv_lora x nope) and packing it AS-IS gives the 12 tiles/head the forward reads (load_heads3 with kb_rows_ = qk_nope_ = 192, cols = kv_lora_ = 512), while transposing it to [192][512] gives 16 tiles/head. The forward then reads past the slice and '1bit serve --device npu' segfaults on the first request. A repack via scripts/repack_gguf.py therefore emits k_b [20][16][5120] while the Q4NX the GLM capture was verified against (/tmp/glm_v3) has [20][12][5120]. The engine's repack now runs the converter from a patched copy with that gate disabled, the same way it already forces Q4_1, and prints why. Verified: the patched repack writes k_b [20][12][5120] (v_b unchanged at [20][16][5120]).
…grid) The MLA/MoE path in this re-synced forward was missing two canonical npu-infer fixes, so `1bit serve -m GLM-4.7-Flash-Q4_K_M.gguf --device npu` decoded " jp" instead of " Paris": 1. mla_expert() transposed every routed expert. Its tile decode already reproduces the GGUF's flat order, which IS the logical [N][K] weight, so the transpose made every routed expert ~10-20x wrong and flipped the top-k selection (canonical f336dfc72). Now a plain std::copy. 2. v_b was indexed in-major (wv[k*vh+n]); the Q4NX's logical index is out-major (h*256+n)*512+k (canonical 3d1b252ec). Now wv[n*kv_lora+k]. Verified on the NPU: /v1/completions "The capital of France is" -> " Paris" (4m4s). Q4NX is byte-identical to the verified glm_npu2 dir; the two bugs were the only functional deltas from canonical.
…-device npu) Qwen3-Coder-30B-A3B (qwen3moe) is the first NON-MLA MoE arch: the FFN is a 128-expert top-8 softmax router with no shared expert and no router bias. Four changes make `1bit serve -m <gguf> --device npu` answer " Paris": 1. forward: parse the MoE params (n_expert/top_k/expert_im/dense_im/ leading_dense/gating/scale) INDEPENDENTLY of mla_, so a non-MLA MoE arch gets its expert geometry (they were read only inside `if (mla_)`). 2. forward: load the MoE tensors (bf16 router + pooled gate/up/down experts) in the standard path when n_expert_>0, instead of the dense gate/up/down; the runner bias and shared expert stay optional (moe_mlp already handles both). 3. forward: dispatch moe_mlp in the standard path for n_expert_>0 (deepseek2 still reaches moe_mlp from the MLA branch, which continues first). 4. forward: `dense_weights_cached` must NOT skip load_layer for a MoE arch -- moe_router_ is a HOST vector reloaded per layer, so skipping it left every layer using the last-loaded layer's router on tokens >= 1 (pos 0 was fine, which is why it looked plausible but decoded "."). scripts/repack_gguf.py: write the MoE config keys for qwen3moe/qwen35moe -- the per-EXPERT intermediate (768) must replace the dense feed_forward_length (5472) so the G/U/D designs ARE the expert MLP, plus n_expert/top_k/softmax gating. Verified: anchor -> " Paris" (3m54s); experts/q_proj/o_proj/router all decode corr >= 0.9989 vs the GGUF; GLM (deepseek2) still answers " Paris" (regression).
The device exposes one NPU hardware context; a second concurrent '1bit serve --device npu' dies inside DRM_IOCTL_AMDXDNA_CREATE_HWCTX (EINVAL) and exits 1. npu_engine_universal has always held this flock; the serve route did not, so siblings could not serialise (and my qwen3moe run hit exactly this when a leftover GLM serve held the context).
Qwen3.6-35B-A3B is a hybrid: 30 GDN (gated delta-net linear attention) layers interleaved with 10 full-attention layers, each FFN a 256-expert top-8 softmax router plus a gated shared expert. Extends the qwen3moe work: - forward: load_gdn_layer now takes the MoE branch (router + pooled experts + the optional shared expert) when n_expert_ > 0, instead of the dense gate/up/down; the GDN layers' FFN was already dispatched by the common MLP section (it sees n_expert_ > 0 and calls moe_mlp). - forward: the shared expert (share_gate/up/down_exps_proj) and its [H] gate (share_router) load in both MoE branches; moe_mlp applies sigmoid(x . g) to the shared expert output (qwen35moe), and leaves it unscaled when absent (deepseek2 has no such gate). - repack: write the GDN geometry (linear_num_key_heads/key_head_dim/ value_head_dim/num_value_heads/conv_kernel_dim from the GGUF ssm.* fields), attn_output_gate (the q_proj emits q|gate) and the per-layer layer_types list the forward dispatches on. Kernels generated for the six new geometries (QKV K2048 N9216 for the gated full-attn, G/U K2048 N512, D K512 N2048, QKV K2048 N8192 (GDN qkv), Z K2048 N4096). Q4NX repacked (21 GB) + the GDN config keys verified.
The stamp records converter_path/commit; --check-stamp re-resolves the converter from the dir's config. It used model_type, which for qwen35moe is the HF name 'qwen3_5_moe' (no q4nx/models/qwen3_5_moe.py), so it fell back to the canonical checkout while the dir was built by the iso-build one -> always 'stale' -> the serve discarded and re-repacked the 21 GB dir on every start (and twice hit ENOSPC doing it). config.json now carries "arch" (the GGUF architecture) and the check prefers it.
…er.json
Two defects on the SentencePiece path (MiniCPM4-8B, the only SentencePiece model of
the four blocked targets), both found by testing against oracle ids rather than by
reading the code.
1. Every SPM decode gained a spurious leading space. decode() mapped every metaspace
to ' ', including the one spm() synthesises as SentencePiece's add_dummy_prefix
marker, so 'The capital of France is' -> ' The capital of France is' and
'Paris' -> ' Paris'. Strip exactly that one leading space: stripping one rather
than all also preserves a genuinely space-leading input, since ' Paris' encodes to
the double metaspace [1345, 3837, 1360] and must decode back to ' Paris'.
2. A correctly-typed tokenizer.json could not be loaded at all. model.at("merges")
throws [json.exception.out_of_range.403] when the key is absent, and a
SentencePiece/Unigram tokenizer.json has no such key -- the throw aborted the
constructor before the SPM detection, so the unigram path was unreachable and
every SPM model fell back to raw bytes. Also accept the Unigram list-form vocab
([[token, score], ...], id = position) as well as the BPE object form. Verified
with a negative control: the old expression throws on a Unigram file, the new one
returns an empty merge set.
Test: tests/data/minicpm4_tokenizer_golden.json, whose ids come from llama.cpp's own
tokenizer (llama-server /tokenize on MiniCPM4-8B-Q4_K_M.gguf), not from this
tokenizer. Results: MiniCPM4 as repacked 5/5 (was 0/3), MiniCPM4 as a proper Unigram
file 5/5 (previously unloadable), Qwen3 BPE golden 14/14 unchanged.
- tools/registry_check.py: an npu .gguf row resolves under --models and reads its arch from the GGUF header (the GGUF-on-NPU route repacks it itself), instead of being treated as a Q4NX model directory. - registry/check_models.tsv: add the six npu GGUF rows (qwen2, qwen3moe, qwen35moe, deepseek2/glm, minicpm, llama). - registry/checked.json: refresh the Qwen3-0.6B npu-directory entry to the current HEAD. - tests/anchor_rerun.sh: harness to re-run the anchor completion across a set of served models and capture the results.
The GGUF-on-NPU route serves six arches, each verified by a captured `--device npu` run (goal muhz0s39-8c9l9o). Record them so the registry reflects what the NPU actually carries — text decoders only: - qwen2 (Qwen2.5-7B), qwen3moe (Qwen3-Coder-30B), qwen3_5_moe (Qwen3.6-35B), deepseek2 (GLM-4.7-Flash), minicpm (MiniCPM4-8B), llama (MiniCPM5-1B). Regenerates registry/architectures.json (npu count 1 -> 7).
…ane routing - run_lm_head picks the widest design with K=H (G/U geometry, N=18944) instead of the first (QKV): 9 head launches per token instead of 33, ~24 fewer synchronous NPU launches (each ~131 ms). Captured qwen2: 0.018 -> 0.020 tok/s, anchor ' Paris' preserved; the five other arches re-verified (evidence/gguf-npu-six/after-speed/). - npu_gemm gains an env-gated NPU_PROFILE hook: ~173 dispatches/token, each ~183 ms (52 pack+quant, 2.3 quant+launch, 131 wait, 0.1 readback). - app/serve.cpp: opt-in forward->lane routing (ONEBIT_NPU_LANE_DIR) for models with a matching fused-lane kernel set; off by default because the repack data packing differs from the lane kernels' (lane ' in the city of' vs forward ' Paris'). Goal mulmuy25-vnl62c.
97a43e2 strips the first metaspace (SentencePiece's add_dummy_prefix marker) from every decode. Both servers decode one generated token at a time (app/forward_serve.cpp, app/unified.cpp), so every word-initial token lost its space: MiniCPM4 streamed "...-TheUnitedStates". The marker belongs to a whole text's first token only; in a stream the space after the prompt or the previous word is real. decode(ids) keeps stripping it (whole texts, the tokenizer goldens); decode(id), the stream's form, keeps it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Docs7 for 1bit-monster/engine
Commit |
PR Reviewer Guide 🔍(Review updated until commit 7dfccad)Here are some key observations to aid the review process:
|
flock works on a read-only descriptor, so the lock file no longer needs to be world-writable, and the path no longer comes from the environment. O_NOFOLLOW refuses a symlink planted at the /tmp path. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Persistent review updated to latest commit 7dfccad |
|
Post-merge note: |
…UF on the NPU, 1BP; blog post (#214) README gets a Direction note (HRX with Loom kernels plus the NPU, as geramyL proposed; Vulkan stays the default until the RFC #213 gates are met) and current status lines: six GGUF architectures answer on the NPU (0.006-0.17 tok/s), DwarfStar reads 1BP, and Laya classifies conversations with its scorer on HRX at 15-16 ms. PORTING's NPU and Laya rows gain #179/#181/#209 and #187/#206/#208. The blog post covers the same. Co-authored-by: bong-water-water-bong <bong-water-water-bong@1bit.gg> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This PR lands the pi agents' GGUF-on-NPU work from strixhalo (branch
npu-glm-kb-fixin~/1bit-engine-gguf-npu; 9 commits were local only), rebased onto main, plus one fix of mine. The agents' commits keep their own authorship.Agents' commits (goals muhz0s39 / mulmuy25):
a58c976,d22ef1a: GLM-4.7-Flash (deepseek2/MLA). The repack neutralises the inverted k_b transpose, and the forward re-syncs two MLA fixes: no expert transpose, and the v_b index. Each is checked element by element against the GGUF (correlation 0.9992/0.9969 against ~0 for the old indexing).6484552,9ad4fdf: Qwen3 MoE (Qwen3-Coder-30B-A3B) and qwen35moe (Qwen3.6-35B-A3B, GDN + MoE) on the per-op forward. MoE geometry is now parsed independently of MLA, and the shared expert gets its sigmoid gate.e5b9f38: the NPU serve takes/tmp/1bit-npu-device.lock, like the ELF runner, so two serves no longer fail onCREATE_HWCTX.669c105: the repack records the GGUF arch for the provenance stamp check.97a43e2: tokenizer. Accepts a Unigramtokenizer.json(array vocab, no merges) and strips SentencePiece's dummy-prefix space.4784b06,b6b5864: registry rows for the six GGUF-on-NPU arches, plustests/anchor_rerun.sh.1f800d2: the LM head uses the widest K=H design, which fewer launches and also fixes a SIGFPE on MLA. Adds anNPU_PROFILE=1dispatch profiler. Adds opt-in forward->lane routing (ONEBIT_NPU_LANE_DIR, off by default).My commit:
97a43e2stripped the leading space from every decode. Both servers decode one streamed token at a time, so every word-initial SentencePiece token lost its space: the agents' benchmark shows MiniCPM4 streaming "…-TheUnitedStates".decode(ids)still strips it for whole texts;decode(id), the streaming form, keeps it.Evidence (captured by the agents on strixhalo,
~/evidence/gguf-npu-six/benchmark-2026-09-28.md):1bit serve -m <gguf> --device npuanswers " Paris" on all six arches (llama MiniCPM5-1B, qwen2 Qwen2.5-7B, qwen3moe Qwen3-Coder-30B-A3B, deepseek2 GLM-4.7-Flash, minicpm MiniCPM4-8B, qwen35moe Qwen3.6-35B-A3B). Speed is 0.006–0.17 tok/s, because it is still the per-op forward (~173 synchronous dispatches per token). The fused per-layer path is the agents' next goal.Verified here: the rebase onto main is clean. ctest passes 12/12. The MiniCPM4 tokenizer golden (llama.cpp oracle ids) passes 5/5 against the repacked
tokenizer.json.Review notes, not blocking:
load_layerandload_gdn_layer.leading_dense_block_countnow defaults to 0 when missing. GLM's config carries it, so GLM is unaffected.ONEBIT_NPU_LANE_DIRleaves annpusymlink in the repack cache dir. The GGUF path doesn't read that dir afterwards.🤖 Generated with Claude Code