Skip to content

Qwen3.8-27B support for oQ in Zig engine - #756

Merged
ashhart merged 16 commits into
ashhart:mainfrom
grantoverton:oqxe/27b
Oct 14, 2026
Merged

ashhart merged 16 commits into
ashhart:mainfrom
grantoverton:oqxe/27b

Conversation

@grantoverton

@grantoverton grantoverton commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Rebased onto main c36b536 (1.0.9) on 2026-10-13: two conflicts in model.zig with 1.0.9's Neural Engine share, merged by hand (the share still loads only for a uniform MLP); every commit passes the gate again. One commit added at the end: the M5 fix @sethforprivacy reported below (a run a tensor-unit tile doesn't divide takes the row kernels on every chip), with a test. On the rebased series 27B oQ2 serves exactly on the M3 (concurrent equals solo, the 25 cancellation steps pass with the same reference hashes).

Part of #716; stacks on #755. This branch carries #755's six commits until that one merges, so please review the last ten commits here (from "The 27B resolves each loaded module's affine width" on). The Qwen3.8-27B admits only 4-bit/group-64 on main (UnqualifiedQwenFormat). With this it loads, drafts and serves TensorFold/Qwen3.8-27B-oQ4 @fb60862 (4/5/6-bit) and TensorFold/Qwen3.8-27B-oQ2 @086dcb0 (2/3/5/6/8-bit), and any checkpoint whose loaded modules are 2, 3, 4, 5, 6 or 8 bits in groups of 32, 64 or 128. The uniform 4-bit/group-64 checkpoint keeps its packed bytes, pipeline keys, kernels and launches.

What this changes

Nine commits on the shared change:

  1. The 27B resolves each loaded module's affine width and admits it against the kernels this chip runs (qwen27/widths.zig, through core/quant). Only the modules the engine loads are resolved, so MTP and vision overrides never refuse a checkpoint. A width or group the projection kernels cannot run refuses the load, naming the module, layer and width.
  2. The 27B loads mixed-width oQ checkpoints: each projection carries its width (projection.zig, gpu_weights.zig on main's staged load, quant_gpu.zig). Every member is staged and read at its own width (k * bits / 32 words, k / group metadata). Kernels by width: before tensor units simd_qmm for 4/64 matrices and the row kernel for every other width; on tensor units the non-reg lane kernel. The pipeline key carries bits, group and stride. The embedding takes the checkpoint's width (oQ2's is 8-bit). The Neural Engine prompt share, which decodes 4-bit codes only, is created only when every MLP matrix is 4/64. Synthetic graphs load at uniform 4/64.
  3. The DFlash2 drafter borrows the target embedding and head at their checkpoint widths. The draft vocabulary copies whole rows (or 32-row tiles) of k * bits / 8 bytes and k / group metadata, and the borrowed embedding is validated at its resolved width. The draft-head check covers 2-, 4- and 8-bit heads (oQ2's head is 2-bit).
  4. A mixed 27B stack runs every member as its own product, as the reference does. 0.6.6 stacks fused members only when all share one width and group; otherwise each member is its own call at its own K split (lane_fuse.py:88-91, row_matmul.py:110-117). Members of one width beside a member of another no longer share a product.
  5. A mixed 27B stack writes each member into the fused rows at a stride before tensor units: one row-kernel launch per member over all rows, through core's output row stride, in place of one launch a row.
  6. tf-qwen27-widths: the family's own products for every 27B projection class at every oQ width against float64, without tensor units.
  7. tf-qwen27-widths gathers the embedding at 8 and 3 bits against float64.
  8. tf-qwen27-run --seams N (test program only): with --compare-chunk C it also compares Session.fingerprint() (every GDN layer's recurrent and conv state, the visible KV rows of every attention layer, the last row's bf16 logits) after the decoded reply in both arms, and for every N-th prompt row prefills the prompt up to that row at --chunk and at C and compares the fingerprints byte for byte. Any difference fails the run. So the seam receipts below can be reproduced.
  9. The 27B keeps timed window costs per checkpoint widths. The draft-window costs are cached per build, chip and model shape, and every 27B checkpoint has one shape, so whichever checkpoint a build served first wrote the costs every later one read. A mixed-width checkpoint now adds a tag of its widths to the shape; a uniform 4-bit g64 checkpoint keeps its shape string, so its key and costs are unchanged. Host test: the uniform checkpoint's key is main's string, and oQ4 and oQ2 get distinct nonzero tags.

Exactness

TensorFold/Qwen3.8-27B-oQ4 @fb60862, TensorFold/Qwen3.8-27B-oQ2 @086dcb0 and the uniform Vontra/Qwen3.8-27B-MLX-4bit @70ae7fa, each with z-lab/Qwen3.8-27B-DFlash2 (model.safetensors sha256 67fc76d68dc5a941…), greedy. Every check below ran on this branch's tip (4191469d, a fresh build: tensorfold-native sha256 d640b9501b70d250…, tf-qwen27-run bedbe3942f0e7b74…).

  • Before: main refuses oQ4 and oQ2 with UnqualifiedQwenFormat.
  • Drafted, resumed, concurrent (--parallel 2 --context 65536): on oQ4, oQ2 and the uniform 4-bit each, drafted equals "draft": false 9/9 and resumed equals fresh 2/2 (a 26,130-token prompt resumed at 26,123 cached tokens, same token hash); bench_concurrent.py --levels 1,2 --alone --serial: every cell equal, 0 unequal, 0 failed. At --parallel 4 --context 65536 --prompt-cache-gib 0, bench_concurrent.py --levels 1,4 --alone --serial: every alone cell (3 at one stream, 12 at four) and serial cell equal, 0 failed, on all three checkpoints; on the uniform checkpoint the code and chat token hashes (2b28aa2108de, cbdd31cd4736) equal main's in every run.
  • Seams (tf-qwen27-run --seams 256, commit 8): the exact 2,048-token public prefill array, chunk 128 against 16 and against 37, 64 decoded tokens. On oQ4, oQ2 and the uniform 4-bit, the state/KV/logits fingerprint is byte-equal at every 256th prompt row (7 seams, each prompt ending there), at the prompt end and after the decoded reply, for both chunk pairs.
  • Synthetic: tf-qwen27-synthetic, tf-qwen27-lanes-check (lane core equals plain), tf-qwen27-dflash-synthetic, tf-qwen27-served-check and tf-qwen27-dflash-served-check pass at every commit.
  • Rows. Before tensor units every non-4/64 product is the row kernel, which gives a row the same bits in any window; tf-qwen27-widths checks every class's rows in calls of 1, 2, 3, 8, 16, 17 and 128 rows, so prompt chunks and decode rows are the same arithmetic.
  • Cancellation, reset and partial acceptance (--parallel 4, on oQ4, oQ2 and the uniform 4-bit): a fresh "draft": false probe (each bench prompt, greedy, 128 tokens) gives the reference hash and the drafted probe equals it, with partial acceptance seen (accepted < drafted) on both prompts; then after a stream dropped mid-decode, a long prompt dropped mid-prompt, a stream dropped beside a running probe (whose hash is checked too) and a request past the window (refused, the reset path), the drafted and plain probes equal the fresh hashes. 25 steps, pass, on each checkpoint (hashes oQ4 c201c615aba6 / d8c31284ff02, oQ2 2ec414650f51 / 70d57ef6b44f, uniform f5c9409cfe40 / 73e9fd21b09e, as main).
  • Precision. Activations stay bf16; every product keeps fp32 group sums. No existing sum on the uniform path changes.

Accuracy against float64, per projection class

tf-qwen27-widths on the M3 Ultra (no tensor units): the family's own products (quant_gpu.Kernels.quant on projection.prepare's matrices: the row kernel, or simd_qmm at 4/64, as a chip without tensor units picks them) on seeded synthetic MLX affine weights at each class's real shape, at 2, 3, 4, 5, 6 and 8 bits, group 64, which covers every width oQ4 and oQ2 use. The reference is core.quant.mlx.reference in float64; every column up to 1,024 columns, else 512 (the first and last 128 and 256 strided). Prompt is one 128-row call, decode one row a call, on identical input rows.

Worst column per class over the six widths, as a fraction of the fp32-sum bound (the width it occurs at), and the worst column's |err| over its largest |y|:

Class Shape Prompt Decode Worst |err| / max|y| Scales 1% off, least
GDN in_proj_qkv [10240, 5120] 0.4757 (q5) 0.4757 (q5) 3.79e-3 7.2×
GDN in_proj_z [6144, 5120] 0.4774 (q8) 0.4774 (q8) 3.83e-3 7.4×
GDN in_proj_b [48, 5120] 0.4673 (q2) 0.4673 (q2) 3.72e-3 5.2×
GDN in_proj_a [48, 5120] 0.4647 (q8) 0.4647 (q8) 3.71e-3 6.4×
GDN out_proj [5120, 6144] 0.4754 (q6) 0.4754 (q6) 3.86e-3 5.8×
attention q_proj [12288, 5120] 0.4782 (q5) 0.4782 (q5) 3.78e-3 7.6×
attention k_proj [1024, 5120] 0.4770 (q4) 0.4770 (q4) 3.77e-3 8.3×
attention v_proj [1024, 5120] 0.4801 (q4) 0.4801 (q4) 3.82e-3 7.6×
attention o_proj [5120, 6144] 0.4747 (q4) 0.4747 (q4) 3.67e-3 6.3×
MLP gate_up [34816, 5120] 0.4715 (q3) 0.4715 (q3) 3.81e-3 6.9×
MLP down_proj [5120, 17408] 0.4394 (q8) 0.4394 (q8) 3.88e-3 2.1×
lm_head [248320, 5120] 0.4808 (q6) 0.4808 (q6) 3.88e-3 6.9×

10,575,872 values within 0.482 of the bound (bf16 output rounding dominates), 1,515,200,224 bit-equal pairs. Prompt and decode agree because the row kernel's bits don't depend on the call's row count.

  • Mixed stacks [z4 | b5 a5], [z4 b4 | a5], [z2 b2 | a3] and [k5 | v6], built as the loader builds them: every row's bits equal in every call; each member's columns within the bound and equal, bit for bit, to its own product and to the member loaded as a matrix of its own, except a 4-bit z (below).
  • Embedding (q27_embed) at [248320, 5120], group 64, 64 tokens including both ends of the vocabulary and a repeat: every gathered value equals the float64 reference rounded once to bf16, 0 of 327,680 differ at 8 bits and at 3 bits; one-row calls equal one 64-row call. Scales 1% off: 160,115 and 142,528 values differ.

Performance

Mac Studio, M3 Ultra (80-core GPU), 256 GB, macOS 26.4.1 (25E253), Zig 0.17.0. Fresh builds from clean detached worktrees, zig build native tf-qwen27-run -Dcpu=apple_m1; tensorfold-native sha256 6a42d48dc1b08a1d… (main 0efbd447), d640b9501b70d250… (this branch, 4191469d). DFlash2 on. Clients are main's tools/ at 0efbd447 on Python 3.12.14; one model process on the machine, the file cache of other checkpoints evicted before each load, every run behind one GPU lock. Every receipt (manifests with binary hashes, decode.json, concurrent.json, prefill records with the exact token arrays and request bodies, /memory snapshots, time -l, load and cleanup timestamps, the fidelity and seam records) is in https://gist.github.com/grantoverton/dbbc25f85cecacca369b9b7c447294ac (index in 00-README.md; the host suite summary is test-summary.txt).

Decode and concurrency. serve <checkpoint> --drafter <DFlash2> --parallel 4 --context 65536 --prompt-cache-gib 0; bench_openai.py … --tokens 256 --reps 3 --temperatures 0 (medians; fibonacci-raw, code, and gpu-chat-no-think, prose, greedy) and bench_concurrent.py … --tokens 256 --reps 3 --temperatures 0 --levels 1,4 --alone --serial (aggregate tok/s). The uniform rows alternate branch, main, main, branch.

Run Weights bench_openai code chat concurrent code ×1 code ×4 chat ×1 chat ×4 Peak footprint
uniform 4-bit, this branch 1 ~15 GB 93.25 68.34 93.2 111.9 68.3 94.6 38.16 GB
uniform 4-bit, main 1 same 94.27 68.26 93.0 111.9 68.2 95.1 38.16 GB
uniform 4-bit, main 2 same 94.19 68.27 93.2 111.9 68.3 95.1 38.16 GB
uniform 4-bit, this branch 2 same 93.40 68.34 93.1 112.0 68.3 94.8 38.16 GB
oQ4, this branch 16 GB 85.45 51.62 83.3 95.6 51.7 75.4 38.71 GB
oQ2, this branch 11 GB 60.76 41.34 61.3 73.4 41.4 59.8 33.23 GB
uniform 4-bit, mlx_lm ~15 GB 37.32 37.20 37.2 36.6 37.1 36.5 16.16 GB
oQ4, mlx_lm 16 GB 35.39 35.33 35.2 34.7 35.3 34.6 16.73 GB
oQ2, mlx_lm 11 GB 71.52 47.80 71.3 51.1 47.8 46.7 11.41 GB

The uniform path is unchanged: the branch is 1.0% below main on bench_openai code (93.33 against 94.23) and equal on chat (68.34 against 68.27), and every bench_concurrent cell is within 0.6%, with identical token hashes. The two oQ checkpoints decode below the uniform 4-bit checkpoint on this M3, oQ4 at 0.92× (code) and 0.76× (chat), oQ2 at 0.65× and 0.60×, although both read fewer bytes a token: every non-4/64 product runs on the generic row kernel, and a mixed stack is one launch per member. mlx_lm 0.32.0 (no drafts; it serves this model's streams one after another) is slower than this branch on the uniform checkpoint and on oQ4, and faster on oQ2: 71.52 / 47.80 tok/s against 60.76 / 41.34. This PR is about loading and serving these checkpoints exactly; a follow-up PR series will work on their speed.

The oQ checkpoints have no before-run: main refuses them (UnqualifiedQwenFormat). So their speed is reported as served decode (above, beside mlx_lm), and the before/after receipts (decode, concurrency and cold prefill) are on the uniform checkpoint, which both binaries load.

Commit 9's effect, on the same oQ2 checkpoint, prompts and tokens (hashes 1396ae566f52 / af21ef189db8 in both): an earlier build of this branch without it, which had served the uniform checkpoint first, drafted oQ2 on the uniform checkpoint's window costs (wide windows nearly free there) and decoded 44.42 / 30.48 tok/s, drafting 475 to 477 rows in 38 rounds for the 256-token code reply; with oQ2's own costs, 246 to 264 rows in 54 to 60 rounds, and 60.76 / 41.34 tok/s.

Cold prefill (uniform 4-bit; main and this branch). Exact token arrays: upstream's public prefill paragraph ("A lighthouse keeper writes in the log as the boats cross the bay.", tools/zig/qwen27_gate.py) repeated and tokenized once without special tokens by the checkpoint's own tokenizer.json, each array the first N ids (every array decodes and re-encodes to itself). POST /v1/completions with the ids as prompt, max_tokens 1, temperature 0, ignore_eos; server --parallel 1 --context 262144 --prompt-cache-gib 0, one request at a time, a fresh KV every measurement (every record has cached_tokens 0 and prompt_tokens equal to N). 2k to 64k: one 2,048-token warm-up, then 3 rounds over the sizes, wall-clock medians. 128k: once per binary, in a server of its own (main's record is q27-u4.pfl-main-f1.prefill-128k.json: that server was stopped during a later request, so its prefill.json was not written). The arrays, the exact request bodies (with sha256), every request's timings and cached-token counts and the server's /memory after each are in the gist.

Tokens main tok/s (s) this branch tok/s (s) mlx_lm tok/s (s)
2,048 286.4 (7.15) 282.1 (7.26) 397.2 (5.16)
8,192 258.8 (31.66) 258.0 (31.76) 390.6 (20.97)
32,768 183.0 (179.04) 183.4 (178.70) 349.1 (93.86)
65,536 131.8 (497.15) 131.7 (497.56) 300.1 (218.36)
131,072, once 82.9 (1,580.52) 83.5 (1,570.42) not run (2k to 64k only)

Native-window prefill (262,143 tokens) not run: about 1.6 h per run on this machine; happy to add it if wanted.

Main's three reps of each size are within 0.1% of each other. The branch's spread more: 2k 286.8, 282.1 and 268.5; 8k 259.2, 258.0 and 247.0; 32k 183.4, 183.4 and 181.0; 64k 132.0, 131.7 and 130.7, the third round low at every size. Its 2k median is 1.5% under main's; from 8k up the medians are within 0.3%. Nothing in this branch changes a uniform kernel or launch, and its decode and concurrency on the uniform checkpoint match main's (above). Peak physical footprint after the 64k prompts: 40.17 GB (main) and 40.18 GB (branch); after the branch's 128k prompt, 40.19 GB. mlx_lm (2k to 64k only) takes text prompts: the same arrays decoded, each record checking prompt_tokens equals N and cached_tokens is 0; it prefills faster than the native engine at every size measured, 1.39× at 2k and 2.28× at 64k.

Memory

Weights as stored: oQ4 16 GB, oQ2 11 GB, uniform 4-bit about 15 GB. At --parallel 4 --context 65536 --prompt-cache-gib 0 with DFlash2: physical footprint after load 23.7 to 24.8 GB (uniform), 25.4 GB (oQ4), 19.9 GB (oQ2); lifetime peak physical footprint after the bench and cancel steps 38.16 GB (uniform, main and this branch alike), 38.71 GB (oQ4), 33.23 GB (oQ2) (/memory and time -l agree). The qwen27 Metal backend reports no device allocations in /memory. RAM class: M3 Ultra, 256 GB. Load and cleanup boundaries are in each run's events.tsv; prefill memory at the 262,144-token context is in the prefill receipts.

Fidelity

Teacher-forced against mlx_lm 0.32.0 on the same checkpoint. mlx_lm renders upstream's four public teacher prompts (tools/zig/qwen27_gate.py: copy, arithmetic, story, code; chat template, thinking off), prefills each in one pass and decodes 256 greedy tokens with EOS ignored, keeping its top-20 logits at every step; that continuation is the fixed history. This branch's tf-qwen27-run --run --teacher --force-length --chunk 128 --compare-chunk 16 then feeds every history token and records its top two at all 1,024 positions (prompt state byte-equal between chunk 128 and 16 on every prompt). Every top-1 difference is listed below with both engines' top two and margins (logits in bf16). Upstream's qwen27_teacher_judge.py reads the same files; its tie policy accepts a difference when the two engines name the same two contenders and both margins are within one bf16 spacing at that logit (0.0625 between 8 and 16, 0.125 between 16 and 32).

Checkpoint Positions Top-1 equal Differences Beyond the tie policy Judge
uniform 4-bit 1,024 1,018 6 0 pass
oQ4 1,024 1,018 6 0 pass
oQ2 1,024 1,013 11 7 fail

On the uniform checkpoint main's binary gives the same 6 differences at the same positions.

Every difference (mlx_lm pick and runner-up, logits, margin; this branch's the same):

Checkpoint Prompt Position mlx_lm top 2 logits margin native top 2 logits margin Verdict
uniform story 68 5133, 1216 22.875 / 22.75 0.125 1216, 5133 22.75 / 22.75 0 near-tie
uniform story 100 29262, 13071 19.875 / 19.75 0.125 13071, 29262 19.875 / 19.875 0 near-tie
uniform story 112 279, 780 22.25 / 22.25 0 780, 279 22.25 / 22.125 0.125 near-tie
uniform story 254 321, 310 21.125 / 21.0 0.125 310, 321 21.0 / 21.0 0 near-tie
uniform code 21 71483, 13 27.0 / 26.875 0.125 13, 71483 26.875 / 26.875 0 near-tie
uniform code 49 262, 1031 24.75 / 24.75 0 1031, 262 24.875 / 24.75 0.125 near-tie
oQ4 story 68 1216, 5133 22.875 / 22.875 0 5133, 1216 22.875 / 22.75 0.125 near-tie
oQ4 story 95 3242, 383 21.875 / 21.75 0.125 383, 3242 21.875 / 21.875 0 near-tie
oQ4 story 114 41928, 88851 20.625 / 20.625 0 88851, 41928 20.625 / 20.5 0.125 near-tie
oQ4 story 142 81550, 279 21.25 / 21.125 0.125 279, 81550 21.125 / 21.125 0 near-tie
oQ4 story 204 1216, 561 21.0 / 20.875 0.125 561, 1216 21.0 / 21.0 0 near-tie
oQ4 story 205 28359, 12854 17.25 / 17.125 0.125 12854, 28359 17.25 / 17.25 0 near-tie
oQ2 copy 29 198, 271 16.125 / 16.125 0 271, 198 14.0625 / 13.6875 0.375 beyond
oQ2 copy 31 312, 91 11.4375 / 11.375 0.0625 91, 3480 11.3125 / 11.3125 0 beyond
oQ2 copy 33 28104, 15 15.0625 / 14.3125 0.75 15, 14813 15.3125 / 14.3125 1.0 beyond
oQ2 arithmetic 110 16239, 64700 19.625 / 19.625 0 64700, 16239 19.5 / 19.375 0.125 near-tie
oQ2 arithmetic 203 170286, 1049 17.25 / 17.0 0.25 1049, 170286 17.125 / 17.125 0 beyond
oQ2 arithmetic 222 12576, 53031 19.25 / 19.25 0 53031, 12576 19.25 / 19.125 0.125 near-tie
oQ2 story 42 2526, 1925 14.625 / 14.5625 0.0625 1925, 2526 14.5625 / 14.5625 0 near-tie
oQ2 story 56 16312, 13 16.625 / 16.5 0.125 13, 16312 16.75 / 16.5 0.25 beyond
oQ2 story 64 13, 16312 15.6875 / 15.6875 0 16312, 13 16.0 / 15.75 0.25 beyond
oQ2 story 65 561, 2532 15.9375 / 15.4375 0.5 2532, 561 15.625 / 15.375 0.25 beyond
oQ2 code 35 5732, 864 19.25 / 19.125 0.125 864, 5732 19.25 / 19.25 0 near-tie

On oQ2, 7 of the 11 differences fall outside the tie policy: six have a margin above one bf16 spacing in at least one engine (up to 0.75 and 1.0 at copy position 33), and at copy position 31 the engines name different runner-ups. So upstream's judge fails on oQ2. Where the two engines agree on the top token, their top-1 logits differ by about as much on every checkpoint (largest: uniform 3.375, oQ4 3.0, oQ2 3.125), so oQ2 does not show a larger arithmetic gap. It has more positions whose margins are small enough to flip, and at 2 bits the two engines' different products (MLX's 2-bit quantized_matmul against this branch's row kernel with fp32 group sums) move some of them past one spacing.

Host suite and lean check

  • zig build test test-golden -Dcpu=apple_m1 at every commit, last at the tip 4191469d: 158 of 160 steps; at the float64 commit 1566/1574 tests passed, 8 skipped; goldens 309/319 equal, the 10 declared differences, 0 unexpected. The one failing step is main's own: core.checkpoint_metal's "a split checkpoint keeps later tensors in the file, packs the rest, and stages the same bytes" logs an error (get calls std.log.err before returning the error.DeferredTensor the test expects, and the runner counts logged errors). It fails the same way on main 0efbd447: 158/160 steps, 1505/1513 tests passed, 8 skipped.
  • New host tests: admission accepts the oQ formats and refuses unsupported widths and groups by name; width resolution under the language_model. prefix; the per-member run plan; lane tile packing at 4/6/8 × 32/64/128; 2-, 4- and 8-bit draft heads; the window-cost key (the uniform checkpoint keeps main's shape string, oQ4 and oQ2 get distinct tags). The failure they fix is main's: it refuses oQ4 and oQ2 with UnqualifiedQwenFormat.
  • python3 tools/zig/lean_check.py: 536 problems against main's 539, no new findings; the 27B's files add none. The inherited findings left in touched files are the shared change's (affine_mm.metal, affine_mm.zig, row_projection.metal, row_projection.zig; listed there). Every touched file is under 600 lines (widths.zig 300, gpu_weights.zig 247, quant_gpu.zig 278, native/qwen27_lanes.zig 580, tests/qwen27/widths.zig 496, tests/qwen27/run.zig 214).

Platforms

  • Run: Metal on a Mac Studio M3 Ultra, 256 GB (80-core GPU, no tensor units), macOS 26.4.1, Xcode 26.6, Zig 0.17.0.
  • Also run, thanks to @sethforprivacy on an M5 Ultra (tensor units): the synthetic checks PASS, and the report that oQ4 and oQ2 failed to load there came with the fix, now its own commit with credit; with it both serve exactly on M5.
  • And to @mrv777 on an M1 Max: tf-qwen27-widths, tf-qwen27-lanes-check and tf-qwen27-dflash-synthetic PASS.
  • Not run: M1, M2 and M4 (no hardware; they take the same row kernels as the M3); CUDA (no CUDA code changes).
  • Not run: M5. There every non-4/64 product, and every member of a mixed stack, takes the lane kernel at its width, and none of that has run. tf-qwen27-widths refuses a chip with tensor units, since it doesn't check the lane kernels. Commands:
zig build test-core-lane native -Dcpu=apple_m1
zig build tf-qwen27-lanes-check tf-qwen27-dflash-synthetic -Dcpu=apple_m1
./zig-out/native/bin/tensorfold-native serve <oQ4 or oQ2 dir> --drafter <Qwen3.8-27B-DFlash2 dir> --name qwen27 --port 8080 --parallel 2 --context 65536
python3 tools/bench_concurrent.py http://127.0.0.1:8080 qwen27 --tokens 256 --reps 3 --temperatures 0 --levels 1,2 --alone --serial --output concurrent.json

then, greedy, each prompt as sent and with "draft": false (same tokens), and a long prompt sent twice (cached 0, then cached). I have no M5; @sethforprivacy has since run these (above).

Reproduction settings

tensorfold-native serve <checkpoint> --drafter <Qwen3.8-27B-DFlash2>, greedy, at --parallel 2 --context 65536 for the exactness checks, --parallel 4 --context 65536 --prompt-cache-gib 0 for the standard clients and cancellation, and --parallel 1 --context 262144 --prompt-cache-gib 0 for cold prefill. One model process on the machine at a time, every load and check program behind one GPU lock, and the file cache of other checkpoints evicted before each load. The machine's iogpu.wired_limit_mb is 240000, which I set as its owner; the 27B checkpoints load far below it, and nothing else was raised.

Where the arithmetic differs from 0.6.6

Bit equality with 0.6.6's kernels is not the bar (#716); the float64 bound per product, row invariance and the exactness contract are.

  • Before M5, 0.6.6 runs simd_qmm at 4 bits, simd_qmm_bits at 5, 6 and 8 bits in groups of 64 or 128, and affine_rows otherwise (row_matmul.py). This branch runs simd_qmm for 4/64 matrices and core's row kernel, with fp32 group sums, for every other width.
  • A 4/64 member inside a mixed stack (oQ4's z in eight layers) runs on the 4-bit row kernel, since simd_qmm has no output stride; 0.6.6 runs it on simd_qmm. Its bits differ from the same z as a uniform layer's simd_qmm product in 51 and 72 of 128 rows, by up to 3.1e-2; both are within the float64 bound. An output stride for simd_qmm would remove the difference; that belongs with the speed follow-ups.
  • The Neural Engine prompt share declines a checkpoint whose MLP matrices are not all 4/64, since it decodes 4-bit codes only; those prompts run on the GPU.
  • On M5 the lane kernel's staging (nibbles for 2 and 3 bits, bytes for 5, 6 and 8) is 0.6.6's, through the shared change; unrun.

The open question in #716

The 27B follows 0.6.6's 27B rule: once a fused group's members differ in width, every member is its own product. So no product spans non-adjacent members, and each member writes its own contiguous columns through core's output row stride. The per-member column map #716 asks about isn't needed here (Flash Next is the family that uses one).

Checklist

  • Every token still goes through the lane rounds. No serial path, and nothing that needs drafts off.
  • Drafted output equals "draft": false, a resumed prompt equals a fresh one, and concurrent equals solo (oQ4, oQ2 and the uniform 4-bit, served, at this branch's tip).
  • No precision traded for speed. The new widths keep fp32 group sums; the one bit difference (a 4-bit member of a mixed stack) is described above.
  • Prompt processing is no slower than the last release. On the uniform checkpoint, cold prefill from 8k up is within 0.3% of main 0efbd447; the 2k median is 1.5% under, with the branch's third round low at every size (above). The oQ checkpoints have no before-run (main refuses them).
  • The description says which platforms I ran, Metal M1 to M5 and CUDA, and which I could not.
  • New tests fail before the change, pass after it, and skip cleanly without their dependency.
  • Comments and docstrings are one line. No measurements or history in the source.
  • No personal data, machine names, internal hosts or local paths. No AI attribution lines.

@sethforprivacy

Copy link
Copy Markdown
Contributor

Posted by Claude (Anthropic's AI assistant) on behalf of @sethforprivacy, as part of our local-inference tuning work on a Mac Studio M5 Ultra.

@grantoverton the M5 checks for this PR at 8c05ac9. Mac Studio M5 Ultra (80-core GPU, tensor units), 256 GB, macOS 27.0 (26A428), Xcode 27.0, Zig 0.17.0. Each branch was built clean from its PR head, zig build ... -Dcpu=apple_m1. The synthetic checks pass. Neither checkpoint loads on M5 as posted, though. Three lines fix both, and with them every live check passes.

zig build test-core-lane          PASS 196 layouts, 26656 bit-equal row/window pairs, 208 frozen fn_lane byte-equal windows, ...
tf-qwen27-lanes-check             a q4 drafter opened from its file ... drafts streams equal to plain greedy
                                  oracle branch=false: 3 rounds, 29/29 drafts landed; oracle branch=true: 7 rounds, 25/50 drafts landed
                                  lane core oracle trees: chains and non-prefix branch paths equal plain greedy tokens and state
tf-qwen27-dflash-synthetic        GPU non-prefix keep: all16 tokens and final state/KV/logits equal plain, 14 matched draft tokens
                                  draft batches1/3/7/16: no repeated pending token, all target tokens and final state equal ordinary
                                  GPU selection: ... target bytes equal ordinary; native draft synthetic: ... target state/KV/logits unchanged

What fails on M5 (serve <ckpt> --drafter z-lab/Qwen3.8-27B-DFlash2@50307d4 --name qwen27 --parallel 2 --context 65536):

  • TensorFold/Qwen3.8-27B-oQ4 @ fb60862: refused at admission with qwen27: linear_attn.in_proj_b (layer 37) is 4-bit group 64; this chip's projection kernels do not run it (InvalidShape).
    • At layer 37 only in_proj_a is 5/64 (layers 0-36 have a and b both 5-bit), so in_proj_b becomes a 48-row 4/64 run.
    • On tensor units widths.check gives every 4/64 run the reg lane layout, which needs N % 32 == 0 (lane_projection.zig:44).
  • TensorFold/Qwen3.8-27B-oQ2 @ 086dcb0: passes admission, then fails at load with BadProjectionShape. With tensor units, projection.prepare refuses any run with n % tile != 0, and a lone GDN in_proj_a or in_proj_b is 48 rows. On your M3 the same runs take prepareRows first, which is why it never showed there.

The fix: such runs take the row kernels on every chip, as they do on the M3. Mixed stacks already handle row runs (quantRuns sends a run with .raw to rowStrided), so nothing else changes. Three lines:

--- a/zig/src/families/qwen27/projection.zig
+++ b/zig/src/families/qwen27/projection.zig
-    if (!device.tensorUnits()) return prepareRows(device, allocator, owned, parts, n, k, tile, slices);
-    if (n % tile != 0) return error.BadProjectionShape;
+    if (!device.tensorUnits() or n % tile != 0) return prepareRows(device, allocator, owned, parts, n, k, tile, slices);
--- a/zig/src/families/qwen27/widths.zig
+++ b/zig/src/families/qwen27/widths.zig
-    if (chip.tensor_units) return (lane.Layout{ .n = n, .k = k, ... }).validate();
+    if (chip.tensor_units and n % 32 == 0) return (lane.Layout{ .n = n, .k = k, ... }).validate();
--- a/zig/src/families/qwen27/gpu_weights.zig
+++ b/zig/src/families/qwen27/gpu_weights.zig
-    if (p.runs.len == 0 and p.bits == 4 and p.group == 64) try jobs.append(...);
+    if (p.runs.len == 0 and p.bits == 4 and p.group == 64 and p.n % 32 == 0) try jobs.append(...);

The last line keeps the 48-row 4/64 run, now a row matrix, out of regOrder, whose n % 32 check returns InvalidShape at load. A uniform 4/64 checkpoint's fused matrices are all multiples of 32 rows, so they keep their path. I didn't have one here to confirm.

Live, with the fix, tools/bench_concurrent.py ... --tokens 256 --reps 3 --temperatures 0 --levels 1,2 --alone --serial:

Checkpoint Prompt Streams Alone check Serial ("draft": false) token_sha Aggregate tok/s
oQ4 code 1 / 2 3/3, 6/6 equal equal d06b1dd756f0 150.5 / 156.5
oQ4 chat 1 / 2 3/3, 6/6 equal equal 80d39f997290 101.2 / 112.9
oQ2 code 1 / 2 3/3, 6/6 equal equal 7930461fbcd4 163.1 / 192.1
oQ2 chat 1 / 2 3/3, 6/6 equal equal 4dff37acbe37 66.4 / 82.1
  • Long prompt sent twice (2,347 tokens on oQ4, 2,345 on oQ2): the replies are equal. The resend reports 0 cached tokens because this host keeps its states at turn ends: the next turn resumed at 2,387 of 2,409 (oQ4) and 2,472 of 2,495 (oQ2). If your "cached 0, then cached" step expects the resend itself to resume, that differs here.
  • oQ2: its hashes are the same across two separate builds of the fix.

@mrv777

mrv777 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

M1 Max check at 8c05ac94: 32 GPU cores, 32 GB RAM, macOS 26.7, Zig 0.17.0 (-Dcpu=apple_m1), no tensor units. Fresh native/check-program builds.

tf-qwen27-widths           PASS 10,575,872 values; worst err/bound 0.4820; 1,515,200,224 bit-equal pairs
tf-qwen27-lanes-check      PASS drafted/plain tokens and final state, failed-round cleanup, nonprefix tree checks
tf-qwen27-dflash-synthetic PASS GPU selection, borrowed vocabulary/head widths, native host and branch-state checks

Lanes/DFlash pass Metal API validation. GPU Shader Validation instead hits the attention-tail's instrumented threadgroup-memory limit (36,864 > 32,768 bytes).

Live oQ2 086dcb0 + DFlash2 50307d4 was attempted with q4 drafter, --parallel 2 --context 8192 --prompt-cache-gib 0. All weight SHA256s verified. macOS reached warning memory pressure during load; the guard stopped the server before readiness, at about 9.5 GiB process footprint. Live drafted/plain, cached/fresh and concurrent/solo checks remain unverified with the available RAM; no live token hashes were produced. Cleanup confirmed; system limits unchanged.

test test-golden: 1,567/1,576 passed, 8 skipped; existing GLM sparse-attention failure and checkpoint logged-error diagnostic remain. Zero unexpected golden differences. Raw logs kept local.

oQ checkpoints write per-module widths as language_model.model.layers.N..., while loaders ask for model.layers.N...
or the bare path, so an exact-key lookup missed every override and read the tensor at the global width. The table
keeps each entry under the config's own key, as before, and also indexes it under its canonical module key (no
.weight suffix, no known prefix, the rule the 27B and Flash Next families use); `width` takes the key as given, then
its module. Two spellings of one module that declare different widths refuse the table as unsupported, the error
`detect` already returns, so its callers' error sets are unchanged; the same width twice is fine.

Test that fails before: the fixture reads a 5-bit down_proj declared under the oQ prefix by the loader spelling; on
main it fails the packed shape check (read at the global width). Also: every prefix form and a trailing .weight name
one module; aliases must agree; an override in any spelling answers every spelling and stays under its own key.

Gate on main 0efbd44 (M3 Ultra, Zig 0.17.0): `zig build test test-golden -Dcpu=apple_m1` has 309/319 goldens
equal and 0 unexpected; every step passes but one that fails on main too (core.checkpoint_metal's split-checkpoint
test: `get` logs an error on the DeferredTensor path the test expects). lean_check: 539 problems (main 539), no
new findings.
… tables repeated for a smaller group

A projection of mixed-width members (the Flash Next hyper-connection down and inject pair, where 0.6.6's QWeights.of
widens) reads as one matrix at their common width, the most bits and the smallest group. Narrowing and a group that
does not divide the source group are refused. Used only where 0.6.6 widens; no caller on this commit.

Tests (widen.zig, new): every source width and every wider target dequantize to the same fp64 values, bit for bit;
a stack of experts widens row for row; the same width comes back as it is; narrowing bits, growing groups and
widths the kernels do not run are refused; the common width takes the most bits and the smallest group; fused
members keep each one's values in order.

Gate on main 0efbd44 (M3 Ultra, Zig 0.17.0): `zig build test test-golden -Dcpu=apple_m1` has 309/319 goldens
equal and 0 unexpected; every step passes but one that fails on main too (core.checkpoint_metal's split-checkpoint
test: `get` logs an error on the DeferredTensor path the test expects). lean_check: 539 problems (main 539), no
new findings.
…wider output

Chips without tensor units now decode every MLX affine width (2, 3, 4, 5, 6, 8 bits) in the row kernel: 3- and 5-bit
rows unpack eight codes from three or five bytes, low code first, as MLX packs them. The 2-, 4-, 6- and 8-bit paths
are unchanged. A 3- or 5-bit row must end on a word, as the checkpoint stores it.

callStrided launches the row kernel with an output row stride, so one member of a fused stack (one run of equal
width) writes its columns straight into the stack output. The default call, its dims and its kernels are unchanged.

tf-row-widths-check (zig/build/core_checks.zig) runs it on the GPU: each width at groups of 32, 64 and 128 against
an fp64 product from the MLX reference dequantization, within the fp32-sum bound; one row alone against the same
row in 2-, 3-, 8- and 16-row windows, relu2, indexed slots and a strided output, bit for bit; a fused stack of
48-column members at mixed widths (4-bit among them, group 64) written through the stride at windows up to 48 rows,
each member as its own call; and, given the unchanged kernel source, the widths it already served byte for byte
against it.

Tests that fail before: "shapes the row kernels serve" now validates 3 and 5 bits (main refuses them),
"3- and 5-bit rows take whole words of codes", "every width has its own pipeline slot" and "a strided call writes
rows at least a matrix wide" (no callStrided on main).

tf-row-widths-check on an M3 Ultra (no tensor units), given main's row_projection.metal: PASS 64512 values within
the fp32-sum bound (worst err/bound 0.9400), 408192 bit-equal pairs, 301056 outputs byte-equal to main's kernels
at the widths main serves (3 and 5 bits have no main kernel to compare).

Gate on main 0efbd44 (M3 Ultra, Zig 0.17.0): `zig build test test-golden -Dcpu=apple_m1` has 309/319 goldens
equal and 0 unexpected; every step passes but one that fails on main too (core.checkpoint_metal's split-checkpoint
test: `get` logs an error on the DeferredTensor path the test expects). lean_check: 536 problems (main 539), no
new findings.
…d 16-row members

Format accepts 2-, 3- and 5-bit codes beside 4, 6 and 8, in groups of 32, 64 or 128. The non-4-bit path widens 2- and
3-bit codes to nibbles for the bf16 x uint4 op and 5-, 6- and 8-bit codes to bytes for the uint8 op, the staging the
Python lane matmul uses, so each width keeps that arithmetic. 3- and 5-bit groups of 32 are an odd number of words a
lane; they are fetched a word at a time instead of in pairs, which would drop the last word.

Layout.y_stride (default N) sets the elements between output rows in every kernel (tf_lane, tf_lane_pair, tf_lane_reg),
so a member of a fused stack writes its columns of the fused rows; the output bound checks follow it. The store of
the simdgroup-matrix layout (Macs without tensor units, ashhart#679) takes the same stride and keeps its column guard.

N may be a multiple of 16 on the plain kernel: the last 32-column tile is packed with zero codes past N, its metadata
reads and stores are already guarded by N, and weightWords counts the padded tiles (sourceWords is MLX's W).
Cooperative and reg layouts still need N % 32 == 0. encodeSums checks its sums buffer by the group count, as ashhart#698
sizes the dispatch.

Without tensor units the admitted layout is unchanged: 6-bit group 32 on the plain kernel (ashhart#679); every other width
still answers TensorUnitsRequired there, now checked at 2, 3 and 5 bits too.

With the defaults the 4-, 6- and 8-bit kernels preprocess (metal -E) to the same text as main's for every group,
schedule, output, prepared-sums/reg layout and both fragment layouts tried (148 variants), so the uniform paths
keep their kernels. Host tests that fail before: 2, 3 and 5 bits validate; y_stride reaches the source and below N
is refused; a 48-row member validates (cooperative and reg refuse it); every width x group x {64, 48} rows packs
MLX codes bit for bit with zero padding. The device program covers every width, 48-row members and strided output
on tensor units; without them (M3 Ultra): PASS 14 layouts, 1904 bit-equal row/window pairs, 208 windows equal to
fn_lane's recorded bytes, 7424 CPU values (max roundoff-bound fraction 0.0144), 10 refusals, and a 48-row member
and two strided layouts at 6/32 equal the unstrided rows with the gaps untouched. fn_lane's recorded windows are
compared only on the layouts they were recorded at.

Gate on main 0efbd44 (M3 Ultra, Zig 0.17.0): `zig build test test-golden -Dcpu=apple_m1` has 309/319 goldens
equal and 0 unexpected; every step passes but one that fails on main too (core.checkpoint_metal's split-checkpoint
test: `get` logs an error on the DeferredTensor path the test expects). lean_check: 536 problems (main 539), no
new findings.
affine_mm took 4- and 8-bit weights only, so a 2-bit matrix (oQ2's routed experts) had no tiled prompt product.
At 2 bits a thread loads its 32 codes of a step as 8 bytes and writes each code into the bf16 tile as itself, with
no offset (as at 8 bits; 0..3 is exact in bf16), so the group sums are the 4- and 8-bit path's. A first try with the
4-bit "+128" offset stayed within the float64 bound, but a reference with scales 1% off no longer exceeded it at
K = 4096, so the codes enter exactly. The 4- and 8-bit sources preprocess as before.

Test that fails before: "dense and gathered, each format, against exact sums on the host" adds 2/64 and 2/32, dense
and gathered; source() refused 2 bits.

The 4- and 8-bit sources preprocess (metal -E) to main's text at both groups, both outputs and both fragment
layouts (16 variants). tf-affine-ref (next commit) holds the 2-bit products to float64.

Gate on main 0efbd44 (M3 Ultra, Zig 0.17.0): `zig build test test-golden -Dcpu=apple_m1` has 309/319 goldens
equal and 0 unexpected; every step passes but one that fails on main too (core.checkpoint_metal's split-checkpoint
test: `get` logs an error on the DeferredTensor path the test expects). lean_check: 536 problems (main 539), no
new findings.
…y row's bits in every window

The device side of ashhart#633's quant-ref for the shared prompt products, item 5 of ashhart#716. tf-affine-ref takes no
arguments and needs no model: seeded synthetic codes, scales, biases and inputs, packed as MLX affine. The engine
module now names core/affine_mm so a check program can reach it.

- affine_mm at 2, 4 and 8 bits, groups 32 and 64, bf16 and fp32 out, dense and as a one-expert gather, at
  [192, 1024] and [256, 4096].
- simd_qmm (4-bit, group 64) as tiles and as the scalar twin at 1 and 2 rows, at [40, 512], [256, 4096] and
  [6400, 256] (K chunks of 32, 16 and 8).

The reference dequantizes the same codes in float64 and multiplies in float64. Each product prints its worst
column (max abs error, and that error over the column's largest |y|) and its largest error as a fraction of the
bound: (group + K / group + 8) fp32 roundings of the terms' magnitudes as the kernels sum them (4-bit affine_mm
codes plus 128), plus one bf16 ulp for bf16 outputs. A reference with its scales 1% off must exceed the bound.

Then every row of a 129-row call must equal, byte for byte, the same row computed in windows of 1, 2, 3, 8, 16,
17, 33, 64 and 65 rows (every full window over the 129 rows, so each row is seen at every position), which
crosses the 16-row fragments, the 32- and 64-row tiles and the gather's switch between them. The twin must equal
the tiles wherever twinExact says it does, since only then do callers take it.

Receipts, M3 Ultra (no tensor units: nax.h's simdgroup-matrix layout), on main 0efbd44:

- every product within 0.4935 of its bound; every row's bits equal at every window, both kernels, every format;
  the twin equals the tiles at 1 and 2 rows on all three shapes, as twinExact says.
- worst columns, bf16 out (dense and gather equal), at [192, 1024] and [256, 4096]: affine_mm q2 g32 2.64e-2 (rel
  3.23e-3) and 6.22e-2 (3.78e-3), q2 g64 2.87e-2 (3.51e-3) and 3.12e-2 (2.47e-3), q4 g32 2.45e-2 (2.65e-3) and
  3.12e-2 (3.01e-3), q4 g64 1.56e-2 (2.89e-3) and 3.12e-2 (2.53e-3), q8 g32 0.496 (3.41e-3) and 0.968 (3.63e-3),
  q8 g64 0.453 (2.87e-3) and 0.956 (3.38e-3); simd_qmm 1.19e-2 (2.57e-3), 3.13e-2 (2.87e-3) and 1.54e-2 (3.74e-3).
  All under 2^-8 of the column's largest |y|; 0.20 to 0.49 of the bound.
- fp32 out: at most 0.0081 of the bound (q2 1.5e-6 to 7.7e-6, rel 1.9e-7 to 6.0e-7).
- scales 1% off: 2.3 to 29 times the bound for bf16 outputs, 2.7 to 391 for fp32, 44 to 273 for simd_qmm.

Not run: an M5, where affine_mm takes the tensor units; `zig build tf-affine-ref && ./zig-out/bin/tf-affine-ref`.

The fp32 bound is the worst case for any summation order, so a changed order still passes; the measured errors
sit two orders below it, and the bit checks are what pin the order.

Gate on main 0efbd44 (M3 Ultra, Zig 0.17.0): `zig build test test-golden -Dcpu=apple_m1` has 309/319 goldens
equal and 0 unexpected; every step passes but one that fails on main too (core.checkpoint_metal's split-checkpoint
test: `get` logs an error on the DeferredTensor path the test expects). lean_check: 536 problems (main 539), no
new findings.
…nst the kernels this chip runs

Only the modules the engine loads are resolved, so draft-head and vision overrides never refuse a checkpoint. A width or group the projection kernels cannot run yet refuses naming the module, layer and width; fused members of different widths are admitted as runs of one width.
… width, mixed fused groups run per width

Uniform 4-bit g64 keeps its bytes, kernels and launches. Other widths take the row kernel before tensor units and the non-reg lane kernel on them; fused members of different widths become runs, each its own product into the fused rows, one row a launch until the core output stride lands. The embedding takes the checkpoint's width. The Neural Engine share declines unless every MLP matrix is 4-bit g64, and the DFlash2 draft head refuses a non-4-bit embedding or head for now. Synthetic graphs load at uniform 4-bit g64.
…eckpoint widths

The draft vocabulary copies whole rows (or 32-row tiles) of k * bits / 8 bytes and k / group metadata, and the borrowed embedding is validated at its resolved width. The draft-head check covers 2-, 4- and 8-bit heads.
…nce does

The reference stacks fused members only when all share one width and group; otherwise each member is its own call at its own K split. Members of one width beside a member of another no longer share a product.
…before tensor units

Each member is still its own product, now one row-kernel launch over all rows writing its columns at the stack width, in place of one launch a row. The row kernel gives a row the same bits in any window, so every row count still matches.
…ainst float64 without tensor units

tf-qwen27-widths runs the family's own products (quant_gpu.Kernels.quant on projection.prepare's matrices: the row kernels, or simd_qmm at 4/64, as a chip without tensor units picks them) on synthetic MLX affine weights at the shape of every projection class (GDN qkv, z, b, a and out; attention q, k, v and o; MLP gate_up and down; lm_head) at 2, 3, 4, 5, 6 and 8 bits, group 64, which covers every width oQ4 and oQ2 use:
- a prompt call (128 rows) and decoded rows (one row a call) against core.quant.mlx.reference in float64, the worst column within the fp32-sum bound; scales 1% off exceed the bound in every class and width;
- every row's bits equal in calls of 1, 2, 3, 8, 16, 17 and 128 rows;
- the mixed stacks [z4 | b5 a5], [z4 b4 | a5], [z2 b2 | a3] and [k5 | v6], built as the loader builds them: every row's bits equal in every call, each member's columns within the float64 bound and equal bit for bit to its own product (the row kernel at its width, unstrided), and to the member loaded as a matrix of its own, except a 4-bit z, which alone takes simd_qmm (no output stride).
The engine module exports core.quant for the reference. The program refuses a chip with tensor units, whose lane kernels it does not check.

M3 Ultra (no tensor units): PASS 10,575,872 values within 0.482 of the float64 bound (bf16 output rounding; worst-column |err| / max|y| 2.5e-3 to 3.9e-3), scales 1% off at least 2.1 times the bound in every class; 1,515,200,224 bit-equal pairs; 31 s. A 4-bit z inside a mixed stack differs from the same z as a uniform layer's simd_qmm product in 51 and 72 of 128 rows, by up to 3.1e-2; both are within the bound.

zig build test test-golden -Dcpu=apple_m1: 158/160 steps, 1566/1574 tests (8 skipped), goldens 309/319 with 0 unexpected differences; the one failure is main's core.checkpoint_metal split-checkpoint test. tools/zig/lean_check.py: 536 findings, 0 new against main.
…oat64

tf-qwen27-widths now also runs the family's embedding gather (glue_gpu.Kernels.embedding, q27_embed) on a synthetic MLX affine table at the 27B's shape [248320, 5120], group 64, at 8 bits (oQ2's embedding) and 3 bits, for 64 tokens including both ends of the vocabulary and a repeat:
- every gathered value equals core.quant.mlx.reference in float64 rounded once to bf16, bit for bit;
- one-row calls give the same bits as one 64-row call;
- the reference with scales 1% off differs in a large share of values, so the comparison sees a wrong decode.

M3 Ultra (no tensor units): 8-bit 0 of 327,680 values differ, 3-bit 0 of 327,680; one-row calls equal; scales 1% off differ in 160,115 and 142,528 values. The projection checks are unchanged (PASS 10,575,872 values within 0.482 of the bound).

zig build test test-golden -Dcpu=apple_m1: 158/160 steps, goldens 309/319 with 0 unexpected differences; the one failure is main's core.checkpoint_metal split-checkpoint test. tools/zig/lean_check.py: 536 findings, 0 new.
…pt chunk seams and after decode

--compare-chunk compared Session.fingerprint() (every GDN layer's recurrent and conv state, the visible KV rows of
every attention layer, the last row's bf16 logits) at the prompt end only, and decode by tokens. With --compare-chunk
C it now also compares the fingerprint after the decoded reply in both arms (the prefill to decode seam and every
decode step's state), failing with DecodeStateOrLogitsDiffer. --seams N adds, for every N-th prompt row, a prefill of
the prompt up to that row at --chunk and at C, fingerprints compared byte for byte; the receipt lists each seam and
seams_byte_equal, and any difference fails with SeamStateOrLogitsDiffer. Test program only; nothing in the engine.

M3 Ultra (no tensor units), the exact 2048-token public prefill array, chunk 128 against 16 and against 37, seams
every 256 rows, 64 decoded tokens: every seam, the prompt end and the decoded state byte-equal on the uniform 4-bit
checkpoint and on oQ4 and oQ2.
…kpoint never drafts on another's costs

Window costs are kept per build, chip and model shape, and every 27B checkpoint has one shape: whichever checkpoint a
build served first wrote the table every later one read. An oQ2 server then chose its draft depth from the uniform
4-bit costs (wide windows nearly free) and drafted deeper than its row kernels pay for: same tokens, 28% more drafted
rows, 15% slower code decode. A mixed-width checkpoint now adds a tag of its widths to the shape; a uniform 4-bit g64
checkpoint keeps its shape string, so its key and costs are unchanged.
…he row kernels, on every chip

With tensor units, projection.prepare refused any run with n % tile != 0 (BadProjectionShape) and widths.check gave
every 4/64 run the reg lane layout, which needs n % 32 == 0. A lone 48-row GDN in_proj_a or in_proj_b is such a run:
TensorFold/Qwen3.8-27B-oQ4's layer 37 (in_proj_a at 5/64, in_proj_b a 48-row 4/64 run) was refused at admission and
-oQ2 failed at load on M5. Those runs now take the row kernels, as they do before tensor units; mixed stacks already
send a row run to rowStrided. A 4/64 run off the 32-row lanes stays out of regOrder. Before tensor units nothing
changes (those runs already took prepareRows), and a uniform 4/64 checkpoint's matrices are multiples of 32 rows.

Test: widths.zig: a 48-row 4/64 run admits on tensor units through the row kernels, a 64-row one through the lanes
(fails before: the lane layout refused n % 32 != 0).

Reported with the fix by Seth For Privacy (@sethforprivacy) on an M5 Ultra (ashhart#756), with oQ4 and oQ2 served exactly there.
@grantoverton

Copy link
Copy Markdown
Contributor Author

@sethforprivacy thank you, that was exactly it. Your three lines are now their own commit on this PR (a2420e7, credited to you), with a host test that a lone 48-row 4/64 run admits on tensor units through the row kernels. It changes nothing before tensor units: those runs already took the row kernels there. The series is also rebased onto 1.0.9.

@ashhart
ashhart merged commit 398ab93 into ashhart:main Oct 14, 2026
4 checks passed
ThinkOffApp pushed a commit to ThinkOffApp/TensorFold that referenced this pull request Oct 14, 2026
ThinkOffApp pushed a commit to ThinkOffApp/TensorFold that referenced this pull request Oct 14, 2026
The contributor's nine commits landed above, two of them carried onto the candidate's Neural Engine fit check. Our two commits after them load projection runs that are not whole 32-row tiles on the row kernels on every chip (the fix posted on the PR from an M5 Ultra run) and read module widths through core/quant's keys. This keeps the PR's head in the history with no change to the tree.
ThinkOffApp pushed a commit to ThinkOffApp/TensorFold that referenced this pull request Oct 14, 2026
The contributor's four commits landed above with the same code; the quant export one of them adds was already in from ashhart#756. This keeps the PR's head in the history with no change to the tree.
ThinkOffApp pushed a commit to ThinkOffApp/TensorFold that referenced this pull request Oct 14, 2026
…units takes the row kernels (ashhart#756)

From ashhart#756's a2420e7, whose code change is 52932f4 here.

Co-authored-by: Grant Overton <65690763+grantoverton@users.noreply.github.com>
@ashhart

ashhart commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Thank you, @grantoverton, and @sethforprivacy. Qwen3.8-27B on a Mac serves mixed-width checkpoints such as TensorFold/Qwen3.8-27B-oQ4 and -oQ2. A uniform 4-bit checkpoint keeps 1.0.9's kernels, launches and window costs.

Your commits are in 1.0.10 under your name (27230d23f, de20cc01c, 1857a1121, 62b711764, aacc2b429, 53b348513, d5f37bae5, 1fc1c70b9, f5829e435). The CHANGELOG line is 9d03b6a7a. Your head a2420e759 is merged at 398ab9333 with no change to the tree. The checks below ran at 8c05ac943. Carried onto 1.0.9's Neural Engine load: the share still needs a uniform MLP (uniformMlp()), and it still checks that the share fits under the load limit. Both stay.

Commits of ours follow yours. 52932f43a, co-authored by @sethforprivacy, loads a run that is not a whole number of 32-row tiles on the row kernels on every chip. That is oQ4's lone 48-row in_proj_b, and oQ2's lone 48-row in_proj_a and in_proj_b. 8c0c3f52b reads module widths through core/quant's keys. Same prefixes, same keys. 71c7c3ae2, co-authored by you, is the widths test from a2420e759: a uniform 4/64 run of 48 or 64 rows on tensor units takes the row kernels.

The oQ checkpoints rest on your runs on an M3 Ultra and @sethforprivacy's on an M5 Ultra at 8c05ac943, with that fix. Each has drafted replies equal to "draft": false, resumed prompts equal to fresh ones, and concurrent equal to solo. On an M3 Ultra the uniform 4-bit 27B with DFlash2, served by 1.0.9 and by this change in A B B A order, gave the same tokens in every case (17 drafted against "draft": false, 12 multi-turn replies also against a cache-off server, 8 concurrent cells), cold prefill from 2k to 32k tokens within 0.02%, and plain and prose decode within 0.12%. Drafted code ran 4.5% slower at the same tokens with three more rounds a reply on each build's own timed window costs, and 0.35% faster with the two builds' tables swapped.

@ashhart ashhart mentioned this pull request Oct 14, 2026
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.

4 participants