Skip to content

GLM-5.3-Flash support for oQ in Zig engine - #773

Merged
ashhart merged 10 commits into
ashhart:mainfrom
grantoverton:oqxe/glm
Oct 14, 2026
Merged

ashhart merged 10 commits into
ashhart:mainfrom
grantoverton:oqxe/glm

Conversation

@grantoverton

@grantoverton grantoverton commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Rebased onto main c36b536 (1.0.9) on 2026-10-13: no conflicts in this change's code; every commit passes the gate again on the new base.

Part of #716; stacks on #755. This branch carries #755's six commits until that one merges, so please review the last four commits here (from "GLM-5.3-Flash reads each module's MLX affine width and mlx-lm's layout" on). GLM-5.3-Flash types every projection as 4-bit/group-64 on main (weights.zig's Q4), so an oQ checkpoint cannot even be planned. With this, TensorFold/GLM-5.3-Flash-MLX-oQ2-MTP @dc55627 and TensorFold/GLM-5.3-Flash-MLX-oQ4-MTP @9872546 load, draft and serve exactly on an M3 Ultra. Nothing here waits for an M5: GLM has no tensor-unit-only kernel (its *_nax kernels take the simdgroup-matrix fragments without tensor units), and every width now has a product before tensor units. The uniform 4/64 checkpoint keeps its load plan (a digest pinned before the change), its kernels and its launches.

What this changes

Four commits on the shared change:

  1. GLM-5.3-Flash reads each module's MLX affine width and mlx-lm's layout; a width-carrying matrix replaces Q4.
    • Widths (quant.zig): each module's width from config.json through core/quant, then GLM's own spellings (the MTP block is layer 45; forget_gate folds into the KDA gate). Aliases that disagree, a missing table and other modes are refused; a tensor stored at another width than declared is refused by name, never read at a guessed width.
    • Affine{bits, group} in place of Q4 (affine.zig): stored shapes, byte spans and column cuts follow each tensor's width. A fused matrix whose members differ in format plans one run a member at its output rows, as 0.6.6's QSplit (glm5_next/linear.py); a one-format stack stays one matrix. The load plan counts layer and expert bytes at those widths.
    • mlx-lm's layout (names.zig), which the public oQ checkpoints use: the MTP block, hyper-connections, stacked experts, the absorbed embed_q/unembed_out pair and a bf16 eh_proj. A fused conv1d.weight is cut into q/k/v; fp32 hyper-connection mixes and router narrow to bf16 only when exact, else the load is refused naming the tensor, shard, offset and shape.
    • A real load still refuses any matrix the 4/64 kernels cannot read until the next commit; TP2 and expert parallel refuse any width but 4/64.
  2. GLM-5.3-Flash runs every MLX width on GPUs without tensor units, split stacks a product a member. glm_rows.metal (wide.zig) runs over core row_projection's fp32 group sums, 2/3/4/5/6/8 bits in groups of 32/64/128, every row its own sums:
    • dense, with input and output row strides and per-head batching: each split-stack member at its own output rows (oQ4's KDA input, q and v at 5 bits and k, f_a, g_a, b at 8, is six products), and mlx-lm's embed_q and unembed_out a head at a time;
    • by expert id (decode's routed experts, gate and up fused with SwiGLU, using glm_glue's helpers rather than a copy) and by sorted pairs (the prompt's routed experts), then the existing combine;
    • bf16 (the MTP head's unquantized eh_proj), and the embedding at 6 and 8 bits.
    • KDA f_b and g_b at 4/64 keep the generated kernel's fused dots; at any other width they are row products first, and one instance of the generated kda_rows reads them in place of its dots (0.6.6's unfused shape). They borrow the dense MLP scratch, so a config whose KDA width outgrows it is refused.
    • A shared expert not at 4/64 runs its own products (no shared-in-routed launch); routed experts not at 4/64 run by id and by sorted pair. Prompt chunks: a one-width 8/64 matrix runs on core affine_mm, split stacks and other widths on the row kernels.
    • The new pipelines load only when a checkpoint has a matrix the 4/64 kernels cannot read, their source hash joins the kernels' identity, and a failed compile releases every pipeline built before it. Refusals at load, by name: a member whose rows are not a multiple of 4, routed gate and up at two widths, any width but 4/64 with TP2 or expert parallel.
  3. tf-glm-rows-check: GLM's products at every MLX width against float64, and each row's bits in every launch.
  4. GLM-5.3-Flash prompt chunks gather 2-bit routed experts on core affine_mm. oQ2's 2/64 routed experts spent 81% of a 2048-row chunk's GPU time on the sorted row kernels; they now take gatherGlu/gatherTo on affine_mm's 2/64 pipelines, as 4/64 experts do. Decode is unchanged.

Exactness

TensorFold/GLM-5.3-Flash-MLX-oQ2-MTP @dc55627, TensorFold/GLM-5.3-Flash-MLX-oQ4-MTP @9872546, and the uniform 4/64 (below), greedy, MTP drafts on unless "draft": false. Same checkpoint, binary and settings within each comparison.

  • Before: main refuses oQ2 and oQ4 at load (every projection is Q4).
  • Uniform 4/64, main against this branch. The published TensorFold/GLM-5.3-Flash-MLX-4bit-MTP @76add2a had not finished downloading, so I built the same checkpoint from the public BF16: every 2-D language-model weight but the router quantized to MLX affine 4/64 (round to nearest), everything else copied as stored, written with the published shard headers. All 43 shard headers, the index and the side files are byte-equal to the published ones, so every name, dtype, shape and offset is the published one. Main and this branch alternated twice (main, branch, branch, main): both plan 180.58 GB; every bench_openai and bench_concurrent token hash is identical across the four runs, and the earlier exactness runs gave all 21 contract hashes identical (four prompts plain and drafted, resumed equals fresh at 0 and 2,425 cached tokens and on a cache-off server, the second turn resumed equals a cache-off server, concurrent equals solo with 2 streams).
  • oQ2 @dc55627 (plan 109.01 GB: 2-bit/64 ×126, 4-bit/64 ×3, 8-bit/64 ×290; loads in 20 to 24 s): drafted equals "draft": false 4/4 at 160 tokens (MTP acceptance 74 to 88%); resumed equals fresh (a 2,427-token prompt at 0 and 2,425 cached tokens); the second turn resumed at 2,425 equals a server with --prompt-cache-gib 0; concurrent equals solo (bench_concurrent's alone and serial checks at 1 and at 4 requests over 2 streams, both prompts: 34/34 equal); greedy text sane ("3:15 PM + 2:30 = 5:45 PM").
  • oQ4 @9872546 (plan 184.66 GB: 4-bit/64 ×129, 5-bit ×36, 6-bit ×17, 8-bit ×250; loads in 36 s on a fresh server): drafted equals plain 4/4 (MTP acceptance 72 to 82%); resumed equals fresh at 0 and 2,425 cached tokens and on a cache-off server; the second turn equals a cache-off server; concurrent equals solo (34/34, as for oQ2); greedy text sane ("210 / 84 = 2.5 hours … 5:45 PM"). oQ4 is the checkpoint with mixed stacks: layer 6's KDA input (5/8/5/8/8/8) and the split MLA x_proj/qr_proj of layer 7 and the MTP layer.
  • Cancellation, reset and partial acceptance (--parallel 2, on oQ2 and oQ4): 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 one prompt (oQ2) and both (oQ4); 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 oQ2 89faa950b278 / fd4c707155c9, oQ4 7152f0a8505e / 0821b2106b69).
  • Seams (tf-glm-seams, a test program on this branch, not in the PR): the exact 2,048-token public prefill array, in the engine's one 16,384-row chunk against chunks of 128 and of 37, comparing every KDA layer's recurrent and conv state, the MLA rows and the last row's logits at seams 256, 512, …, 1,792, the prompt end, and after 64 one-row decode windows.
    • Against 128: byte-equal at every seam, the prompt end and after decode, on oQ2, oQ4 and the uniform 4/64.
    • Against 37: equal at seams 256 to 1,536; the first difference is KDA layer 0's state at 1,792, where the 37-row chunks leave a final chunk of 16 rows, which GLM sends through its 1-16-row window path (forward.zig) rather than the prompt path (prompt.zig). Main does the same on the uniform 4/64: I ran the same test program on main 0efbd44 (the program only, no engine change) and it gives the same result, part for part. This is Proposal: restore single-Mac GLM-5.3-Flash on the M3 Ultra #743's item 3 (prompt chunks and 16-row windows give different bits); the 256-token token streams of prompt chunks and of GLM_PROMPT=0 windows also differ on main and here, with identical hashes on both.
  • Rows. Every new product gives a row the same bits alone and in 2-, 3- and 16-row windows; strided equals contiguous; per head equals each head dense; by id equals dense; GLU by id equals glm_swiglu(dense gate | up); sorted equals by id (experts of 8, 5, 2 and 1 pairs); the row kernels equal core row_projection; a split stack in one call equals each member alone (oQ4's KDA input 5/8/5/8/8/8 and x_proj 6/6/5/5, at the decode pitch and the 64-padded prompt pitch); 2/64 experts gathered on affine_mm equal the dense product. 1,976,704 bit-equal pairs.
  • KDA state. The separate f_b/g_b products followed by kda_rows equal the generated 4/64 kda_rows byte for byte in the output, the recurrent state and the conv window, on every 4-bit code with random per-group power-of-two scales and nonzero biases.
  • Precision. Activations stay bf16; every row product keeps fp32 group sums; an fp32 mix or router is narrowed only when bf16 holds it exactly.

FP64 per class

tf-glm-rows-check on the M3 Ultra (no tensor units), seeded synthetic MLX affine weights. The bound is the fp32 sums' depth (k / 8 + 64 additions) plus one bf16 rounding; every column of two rows is compared against float64, decode rows and the prompt's product separately on identical input rows. A reference with scales 1% off must exceed the bound in every class and width.

Every bits {2,3,4,5,6,8} × group {32,64,128} on three shapes ([132, 384], [64, 2688], [40, 1536]), worst error over the bound:

Bits g32 g64 g128
2 0.966 0.938 0.954
3 0.961 0.946 0.949
4 0.905 0.960 0.959
5 0.970 0.919 0.911
6 0.966 0.950 0.943
8 0.948 0.933 0.948

Every GLM projection class at the public oQ checkpoints' widths, at its real shape:

Per class: decode and prompt worst column against the float64 bound
Class Shape Width Decode Prompt (kernel) Worst |err| / max|y| Scales 1% off caught
KDA in q/k/v [8192, 4096] 5 0.7736 0.7708 (rows) 2.51e-3 5161 / 8192 cols
KDA in q/k/v [8192, 4096] 8 0.7975 0.7680 (affine_mm) 2.25e-3 6173 / 8192
KDA in f_a/g_a [128, 4096] 5 0.6051 0.6746 (rows) 1.87e-3 92 / 128
KDA in f_a/g_a [128, 4096] 8 0.6497 0.6497 (affine_mm) 2.28e-3 121 / 128
KDA in b [64, 4096] 5 0.5518 0.5795 (rows) 2.51e-3 42 / 64
KDA in b [64, 4096] 8 0.7464 0.7464 (affine_mm) 2.64e-3 59 / 64
KDA f_b/g_b [8192, 128] 5 0.9749 0.9749 (rows) 2.69e-3 7979 / 8192
KDA f_b/g_b [8192, 128] 8 0.9839 0.9839 (affine_mm) 2.88e-3 8148 / 8192
KDA o_proj [4096, 8192] 6 0.5442 0.5797 (rows) 2.60e-3 1398 / 4096
KDA o_proj [4096, 8192] 8 0.6302 0.5729 (affine_mm) 3.06e-3 1454 / 4096
MLA q_a [1536, 4096] 6 0.8060 0.8060 (rows) 2.42e-3 1345 / 1536
MLA q_a [1536, 4096] 8 0.7999 0.7989 (affine_mm) 2.45e-3 999 / 1536
MLA kv_a [512, 4096] 6 0.8079 0.7055 (rows) 2.58e-3 344 / 512
MLA kv_a [512, 4096] 8 0.6910 0.6812 (affine_mm) 2.78e-3 385 / 512
indexer wk [128, 4096] 5 0.7333 0.7308 (rows) 2.27e-3 87 / 128
indexer wk [128, 4096] 8 0.6774 0.6448 (affine_mm) 2.55e-3 88 / 128
indexer weights_proj [32, 4096] 5 0.5731 0.5731 (rows) 1.66e-3 17 / 32
indexer weights_proj [32, 4096] 8 0.6400 0.6283 (affine_mm) 2.86e-3 19 / 32
MLA x_proj, one width [2208, 4096] 8 0.7148 0.7258 (affine_mm) 2.69e-3 1672 / 2208
MLA q_b [16384, 1536] 6 0.9316 0.9330 (rows) 2.94e-3 15738 / 16384
MLA q_b [16384, 1536] 8 0.9236 0.9294 (affine_mm) 2.95e-3 14998 / 16384
indexer wq_b [4096, 1536] 5 0.9327 0.9327 (rows) 2.87e-3 4040 / 4096
indexer wq_b [4096, 1536] 8 0.9417 0.9470 (affine_mm) 2.87e-3 4072 / 4096
embed_q, a head [512, 256] 5 0.9541 0.9541 (rows) 2.01e-3 510 / 512
embed_q, a head [512, 256] 8 0.9792 0.9792 (affine_mm) 2.26e-3 512 / 512
unembed_out, a head [256, 512] 5 0.9089 0.9089 (rows) 1.79e-3 254 / 256
unembed_out, a head [256, 512] 8 0.8975 0.9584 (affine_mm) 2.10e-3 251 / 256
MLA o_proj [4096, 16384] 6 0.3982 0.3982 (rows) 2.11e-3 675 / 4096
MLA o_proj [4096, 16384] 8 0.3910 0.3953 (affine_mm) 2.30e-3 61 / 4096
dense gate/up [12288, 4096] 5 0.8050 0.8052 (rows) 1.93e-3 8298 / 12288
dense gate/up [12288, 4096] 8 0.8926 0.8112 (affine_mm) 3.05e-3 8450 / 12288
dense down [4096, 12288] 6 0.5292 0.5292 (rows) 2.32e-3 282 / 4096
dense down [4096, 12288] 8 0.5244 0.5321 (affine_mm) 2.31e-3 1065 / 4096
shared gate/up [2048, 4096] 8 0.8038 0.7141 (affine_mm) 2.72e-3 1443 / 2048
shared down [4096, 2048] 8 0.8938 0.9065 (affine_mm) 2.59e-3 3669 / 4096
routed gate/up [2048, 4096] 2 0.7837 0.7821 (affine_mm) 3.18e-3 677 / 2048
routed gate/up [2048, 4096] 4 0.7551 0.8092 (rows) 3.55e-3 1260 / 2048
routed down [4096, 2048] 2 0.8564 0.8350 (affine_mm) 2.49e-3 1967 / 4096
routed down [4096, 2048] 4 0.8902 0.8526 (rows) 3.41e-3 3095 / 4096
lm_head [154880, 4096] 8 0.8393 0.8397 (affine_mm) 3.28e-3 124888 / 154880

1,328,072 values within the bound, worst 0.9839 (KDA f_b/g_b at 8 bits: bf16 output rounding); worst-column |err| over the column's largest |y| 1.7e-3 to 3.6e-3 in every class. The 6- and 8-bit embedding gathers are within two bf16 roundings of float64, and a neighbouring group's bias is caught. Core tf-affine-ref at 2 bits (groups 32 and 64, dense and gathered) is in #755.

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 -Dcpu=apple_m1; tensorfold-native sha256 d508376cf1fdc1f3… (this branch) and 1c8c96881568… (main 0efbd44). Every run starts from a fresh server, with other checkpoints' file cache evicted first.

Decode and concurrency. serve <checkpoint> --name glm --parallel 2 --context 16384 --prompt-cache-gib 0, MTP drafts on; bench_openai.py … --tokens 256 --reps 3 --temperatures 0 (medians; fibonacci-raw, code, and gpu-chat-no-think, chat) and bench_concurrent.py --levels 1,4 --alone --serial (aggregate tok/s; 4 requests share the 2 streams):

Run Weights (plan) bench_openai code chat concurrent code ×1 code ×4 chat ×1 chat ×4 Peak footprint
uniform 4/64, main 1 180.58 GB 102.97 61.38 102.9 107.3 60.9 82.5 185.33 GB
uniform 4/64, this branch 1 same 102.55 63.78 102.4 107.1 63.9 82.2 185.34 GB
uniform 4/64, this branch 2 same 102.57 63.86 102.4 107.2 61.3 83.6 185.34 GB
uniform 4/64, main 2 same 102.48 60.06 100.8 107.1 60.6 82.5 185.32 GB
oQ4, this branch 184.66 GB 62.76 55.34 62.7 83.8 54.4 72.7 189.43 GB
oQ2, this branch 109.01 GB 101.49 54.52 101.4 91.6 54.0 56.4 113.78 GB
uniform 4/64, oMLX 0.7.1.dev1 same 55.21 55.01 55.0 74.9 54.8 73.7 189.57 GB
uniform 4/64, mlx-vlm 0.7.6 same no /v1/completions (chat only) 34.3 73.6 183.56 GB

The uniform path is unchanged: the branch's two runs average 0.2% below main's on bench_openai code (102.56 against 102.73) and 5.1% above on chat (63.82 against 60.72; the chat prompt's single reps range from 60.0 to 65.5 across the four runs, and main's reach 63.2), no bench_concurrent cell is more than 0.2% below main's average, and every token hash is identical. The oQ checkpoints have no before-run: main refuses them at load. So their speed is reported as served decode beside oMLX and mlx-vlm, the standard servers for GLM-5.3-Flash (an mlx-vlm model type). Against this branch's uniform runs, oQ4 decodes code at 0.61× and chat at 0.87×, while oQ2's code is level (0.99×; MTP acceptance is high on that prompt) and its chat is 0.85×. Their non-4/64 products run on the generic row kernels, and a split stack is one launch per member; a follow-up PR series works on that speed (#716).

Cold prefill (uniform 4/64; 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 (tokenizer.json sha256 19e77364…); /v1/completions with the ids, max_tokens 1, --parallel 1 --context 66560 --prompt-cache-gib 0, every request cold (cached_tokens 0), a 2k warmup, three reps of each size, medians:

Tokens main tok/s (s) this branch tok/s (s)
2,048 511.8 (4.00) 512.0 (4.00)
8,192 518.3 (15.80) 518.3 (15.80)
32,768 519.8 (63.04) 519.8 (63.04)
65,536 510.3 (128.43) 515.6 (127.10)
131,072, once (--context 132096) 507.6 (258.23) 507.5 (258.27)

Native-window prefill not run; happy to add it if wanted.

oQ prefill (a 2,427-token prompt, cache off, the earlier exactness servers): oQ2 475 to 477 tok/s and oQ4 380, against the uniform 4/64's 506.7 to 507.7 (0.94× and 0.75×). Before the last commit oQ2 prefilled the same prompt at 163 tok/s.

Memory

Load plans as logged: uniform 4/64 180.58 GB, oQ4 184.66 GB, oQ2 109.01 GB in buffers. Physical footprint (darwin_rusage_info_v4, the server process) at --parallel 2 --context 16384 after load and at the end: uniform 185.29 / 184.92 GB (peak 185.34 GB, main and this branch equal to 0.02 GB), oQ4 189.38 / 189.01 GB (peak 189.43 GB), oQ2 113.70 / 113.33 GB (peak 113.78 GB). At --parallel 1 --context 66560, uniform peaks at 186.06 GB (main) and 186.03 GB (branch); at --context 132096, 187.33 GB on both. During the full oQ4 load free memory never went under 44.9 GB.

Fidelity

Teacher-forced against oMLX 0.7.1.dev1 on the same checkpoint: oMLX's own GLM-5.3 load path (its vendored glm5_next from mlx-vlm PR #2030 on mlx-vlm 0.7.7, with each per-module width's runtime aliases), which keeps every projection at its own width and drops the MTP tensors. Stock mlx-vlm 0.7.6 fuses q/k/v into one Linear, so it cannot load oQ4 (layer 6 stores q/k/v at 5 and 8 bits). oMLX renders upstream's four public teacher prompts (copy, arithmetic, story, code; chat template, thinking off), prefills each in one pass and decodes 256 greedy tokens with EOS ignored, no drafts, keeping its top-20 logits at every step; that continuation is the fixed history. This branch's tf-glm-teacher (a test program, not in the PR) then feeds every history token and records its top two at all 1,024 positions. Logits are bf16.

Checkpoint Positions Top-1 equal Differences Beyond the tie policy
uniform 4/64, main 1,024 996 28 26
uniform 4/64, this branch 1,024 996 28 (the same positions and tokens) 26
oQ4 1,024 1,005 19 12
oQ2 1,024 995 29 22

On the uniform checkpoint main and this branch give the same 28 differences at the same positions, so the gap to oMLX is TensorFold's GLM against oMLX's, and it is there before this change. The oQ checkpoints sit at or under it: oQ4 has fewer differences than the uniform checkpoint, oQ2 one more with fewer beyond the tie policy. In every difference this branch's pick is oMLX's second (or third) token, never a token oMLX ranks lower, and no position has a pick below another token's logit.

Every difference (oMLX's pick and its margin over the runner-up; this branch's pick and its margin; this branch's pick's rank in oMLX's top 20):

uniform 4/64 (main and this branch, identical): 28 differences, 26 beyond the tie policy
Prompt Position oMLX pick oMLX margin this branch's pick its margin its rank in oMLX Verdict
copy 33 2396 1.625 12951 0.375 2 beyond tie policy
copy 77 36380 0.125 88713 0.5 2 beyond tie policy
copy 185 6801 0.125 702 0.0 2 near tie
copy 202 481 0.0 1959 0.125 2 near tie
copy 233 785 0.25 16 0.5625 2 beyond tie policy
copy 235 374 0.0 6801 1.0 2 beyond tie policy
arithmetic 17 10056 0.0 101140 0.25 2 beyond tie policy
arithmetic 89 3523 0.75 20586 0.125 2 beyond tie policy
arithmetic 106 220 0.5 121577 0.25 2 beyond tie policy
arithmetic 217 22 0.375 101140 0.375 2 beyond tie policy
arithmetic 230 553 0.375 220 0.875 2 beyond tie policy
story 53 13222 0.25 3637 0.0 2 beyond tie policy
story 93 271 0.5 4751 0.25 3 beyond tie policy
story 94 8372 0.375 10056 0.25 2 beyond tie policy
story 101 1943 0.25 11411 0.125 2 beyond tie policy
story 142 8787 0.5 6876 0.25 2 beyond tie policy
story 154 198 0.25 11 0.875 2 beyond tie policy
story 186 198 0.0 271 0.125 3 beyond tie policy
story 194 11 0.0 429 0.125 2 beyond tie policy
story 213 28083 0.125 35872 0.25 2 beyond tie policy
story 232 16485 0.375 911 0.125 2 beyond tie policy
story 247 315 0.625 518 1.0 2 beyond tie policy
code 17 911 0.125 15504 0.25 2 beyond tie policy
code 132 315 0.5 25 0.125 2 beyond tie policy
code 138 11 0.0 13 0.25 2 beyond tie policy
code 191 785 0.0 7039 0.875 2 beyond tie policy
code 204 6771 0.75 2055 0.125 2 beyond tie policy
code 250 284 0.0 366 1.0 2 beyond tie policy
oQ4: 19 differences, 12 beyond the tie policy
Prompt Position oMLX pick oMLX margin this branch's pick its margin its rank in oMLX Verdict
copy 25 91806 0.25 311 0.5 2 beyond tie policy
copy 27 3383 0.125 1681 0.125 2 near tie
copy 34 11411 0.25 4185 0.0 2 beyond tie policy
copy 70 1196 0.125 3974 0.125 2 near tie
copy 77 330 0.625 25 0.125 2 beyond tie policy
copy 87 1189 0.125 2217 0.125 2 near tie
copy 121 594 0.0 2484 0.625 2 beyond tie policy
copy 192 358 0.0 1077 0.25 2 beyond tie policy
copy 212 6771 0.25 576 0.125 2 beyond tie policy
copy 239 279 0.125 1077 0.25 2 beyond tie policy
arithmetic 26 271 0.0 284 0.375 2 beyond tie policy
arithmetic 45 24692 0.125 284 0.0 2 near tie
arithmetic 131 2195 0.125 13385 0.125 2 near tie
story 108 11411 0.375 1943 0.5 2 beyond tie policy
story 112 264 0.125 1943 0.125 2 near tie
story 141 12531 1.625 3267 0.0 3 beyond tie policy
code 20 323 0.0 382 0.125 2 near tie
code 63 323 1.875 434 0.125 2 beyond tie policy
code 81 2604 0.125 61711 0.875 2 beyond tie policy
oQ2: 29 differences, 22 beyond the tie policy
Prompt Position oMLX pick oMLX margin this branch's pick its margin its rank in oMLX Verdict
copy 26 22739 0.125 2550 0.375 2 beyond tie policy
copy 29 1467 0.125 96063 0.25 2 beyond tie policy
copy 52 1449 0.375 678 0.25 2 beyond tie policy
copy 93 25 0.0 330 0.125 2 near tie
copy 109 11 0.0 7600 0.125 2 near tie
copy 116 330 0.125 374 0.125 3 beyond tie policy
copy 163 481 0.25 1959 0.125 2 beyond tie policy
copy 170 11646 0.125 4734 0.0 2 near tie
copy 206 9450 0.125 4792 0.125 2 near tie
copy 210 279 0.25 432 0.5 2 beyond tie policy
copy 213 13 0.125 807 0.5 2 beyond tie policy
arithmetic 62 51185 0.25 11760 0.0 2 beyond tie policy
arithmetic 135 28 0.25 101140 0.125 2 beyond tie policy
arithmetic 217 25 0.125 1948 0.375 2 beyond tie policy
story 34 2805 0.0 3364 0.25 2 beyond tie policy
story 48 1959 0.125 438 0.25 2 beyond tie policy
story 77 6243 0.125 53060 0.25 2 beyond tie policy
story 88 14012 0.875 3100 0.0 2 beyond tie policy
story 111 5510 0.125 17934 0.125 2 near tie
story 161 91571 0.625 85354 0.0 2 beyond tie policy
story 226 28521 0.125 2805 0.125 2 near tie
story 247 4401 0.125 264 0.0 2 near tie
code 66 1251 0.625 432 0.0 2 beyond tie policy
code 109 16432 0.125 5338 0.75 2 beyond tie policy
code 128 11 2.125 13 1.0 2 beyond tie policy
code 129 384 0.125 537 0.5 2 beyond tie policy
code 156 279 0.25 6821 0.125 3 beyond tie policy
code 166 320 0.25 448 0.625 2 beyond tie policy
code 168 369 0.375 32151 0.125 2 beyond tie policy

Host suite and lean check

  • zig build test test-golden -Dcpu=apple_m1 at each of the four commits, from a clean detached worktree: 158 of 160 steps; goldens 309/319 equal, the 10 declared differences, 0 unexpected; tf-glm-rows-check passes from the commit that adds it on. 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.
  • Tests that fail before (no width reading and no mlx-lm names on main): weights_test.zig's oQ-shaped fake checkpoints in both layouts find every tensor at its own width; overrides under either prefix; a tensor stored at another width, two spellings or the MTP block and layer 45 at two widths, an odd member and a routed gate/up pair at two widths are refused by name; oQ4's KDA input and MLA x_proj plan as one run a member; mlx-lm's conv1d and fp32 mixes plan as the original layout's tensors; 4/64 alone takes the generated kernels and any other f_b/g_b width the separate products; a failed compile releases every pipeline built before it (GPU); prompt chunks route routed experts to affine_mm at 4/64 and 2/64 only. The uniform plan's digest is unchanged.
  • python3 tools/zig/lean_check.py: 533 problems against main's 539 at every commit, no new findings. Gone: three over-long comments in weights.zig and Core support for oQ in Zig engine #755's three. Inherited findings left in files this branch touches: over-long comments in checks.zig (5), engine.zig (6), forward.zig (8), kernels.zig (1), load_plan.zig (2), moe.zig (2), mtp.zig (2), prompt.zig (11), weights.zig (1), a multi-line comment in kernels/metal/sources.zig, and Core support for oQ in Zig engine #755's. Every touched file is at or under 600 lines; zig fmt clean.

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): tf-glm-rows-check and tf-affine-ref PASS, and oQ2 serves exactly (drafted, cached and concurrent).
  • And to @mrv777 on an M1 Max: tf-affine-ref PASS; tf-glm-rows-check needs GLM: fix KDA and router thread limits on M1 #708's KDA thread limit on M1 and passes with it.
  • Not run: M1, M2 and M4 (no hardware; the M3 runs the same non-tensor-unit kernels); CUDA (no CUDA code changes). The 256 GB class is the one I measured; oQ2 (109 GB) is the only one of these that fits a smaller Mac.
  • Not run: M5. The same paths run there, but affine_mm (8/64 and 2/64 prompts) compiles its tensor-unit variant, and the row kernels' one- and two-row instances have shown row invariance only under the M3's compiler. Commands:
zig build tf-glm-rows-check tf-affine-ref native -Dcpu=apple_m1
./zig-out/bin/tf-glm-rows-check
./zig-out/bin/tf-affine-ref
./zig-out/native/bin/tensorfold-native serve <oQ2 or oQ4 dir> --name glm --port 8080 --context 16384 --parallel 2

then, greedy, per prompt: a request and the same with "draft": false (same tokens); a ~2.4k-token prompt sent twice (cached 0, then cached) and a second turn compared with a server started with --prompt-cache-gib 0; two requests at once compared with each alone. I have no M5; @sethforprivacy has since run these (above).

Reproduction settings

  • GLM_LOAD_LIMIT_GB=208, which I set as the machine's owner. The default limit is 70% of RAM, 179.2 GB here, and the plan is 184.7 GB for oQ4 and 180.6 GB even for the official uniform 4-bit, so neither loads on a 256 GB Mac without it (oQ2 at 109 GB does). The limit only admits the load and changes no arithmetic. The engine warns that past 70% a load can stall macOS until its watchdog restarts the Mac: run nothing else large beside it.
  • The machine's iogpu.wired_limit_mb is 240000, also set by me.
  • One model process on the machine at a time, every load and check program behind one GPU lock, and other checkpoints' file cache evicted before each load: the loader's paced reads wait for free memory, so another model's cached pages slow a big load (the first oQ4 load took 141 s that way, 36 s after eviction).

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.

  • Row products keep fp32 group sums at every width; 0.6.6's qmv_rows_b rounds group sums to bf16 at 5 and 6 bits.
  • 2-bit routed experts: the row kernels in decode and affine_mm in prompts, with fp32 sums, not MLX's 2-bit quantized_matmul. One-width 8/64 prompt products run on affine_mm.
  • Split-stack prompts run each member on the row kernels, not PM.matmul.
  • An fp32 mix or router that bf16 cannot hold exactly is refused (InexactBf16), where 0.6.6's bf16_if_exact keeps it in fp32.
  • TP2 and expert parallel refuse any width but 4/64 (one Mac only here).

The open question in #716

GLM follows 0.6.6's QSplit: once a fused matrix's members differ in format, every member is its own product at its own output rows, members that share a width included. Nothing is grouped across members, so the per-member column map #716 asks about isn't needed here (Flash Next is the family that uses one).

Follow-ups

  • Proposal: restore single-Mac GLM-5.3-Flash on the M3 Ultra #743's item 3 (above): prompt chunks and 16-row windows give different bits, on main and here; a final chunk of 16 rows or fewer takes the window path.
  • Prompt chunks with embed_q run the decode's per-head launch serially over the chunk's rows.
  • oQ2's prompt: a 2048-row chunk now spends 42% in the routed experts (81% before the last commit), and about a quarter each in KDA and MLA.
  • oQ decode and prefill speed on Macs without tensor units (above): a separate PR series.

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/64, full models, served).
  • No precision traded for speed. Every new product keeps fp32 group sums; the differences from 0.6.6 are listed above.
  • Prompt processing is no slower than the last release: the uniform 4/64 prefills 2k to 128k at main's rate (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.

Receipts: https://gist.github.com/grantoverton/57663bbdd9b1ebcd4d5524e310923403

@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 9777aeb all pass, the live ones on TensorFold/GLM-5.3-Flash-MLX-oQ2-MTP @ dc55627. 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.

tf-glm-rows-check    GPU Apple M5 Ultra, tensor units: true
                     PASS 1328072 values within the fp32-sum bound (worst err/bound 0.9839), 1976704 bit-equal pairs
tf-affine-ref        PASS every product within 0.4935 of its float64 bound, every row's bits equal at every window

Live. serve <oQ2> --name glm --context 16384 --parallel 2; it loaded in 12.5 s (109.0 GB of weights). Greedy throughout.

  • tools/bench_concurrent.py ... --tokens 256 --reps 3 --temperatures 0 --levels 1,2 --alone --serial:
Prompt Streams Alone check Serial ("draft": false) token_sha Aggregate tok/s
code 1 3/3 equal equal 4792b220bd0f 157.6
code 2 6/6 equal equal 4792b220bd0f 148.1
chat 1 3/3 equal equal 23142643f543 81.3
chat 2 6/6 equal equal 23142643f543 105.4
  • Cached against fresh: a 2,114-token prompt (its own nonce, then the public lighthouse paragraph repeated), sent cold (0 cached) and again (2,111 cached). The replies are equal (cec9fad29164).
  • Second turn against --prompt-cache-gib 0: resumed at 2,111 of 2,155 tokens, it equals a server started without a prompt cache (d16c599649a9).

@mrv777

mrv777 commented Oct 13, 2026

Copy link
Copy Markdown
Contributor

M1 Max check at 9777aeb2: 32 GPU cores, 32 GB RAM, macOS 26.7, Zig 0.17.0 (-Dcpu=apple_m1), no tensor units. Metal API validation enabled.

tf-affine-ref    PASS every product within 0.4935 of its float64 bound; every row's bits equal at every window
tf-glm-rows-check FAIL unmodified head: KDA dispatches 1,024 threads, pipeline maxTotalThreadsPerThreadgroup is 512

This is the M1 KDA issue covered by #708 (which depends on #704). In a separate local copy, adding its max_total_threads_per_threadgroup(1024) reservation to both generated KDA kernels makes the entire check pass:

tf-glm-rows-check PASS 1,328,072 values; worst err/bound 0.9839; 1,976,704 bit-equal pairs

That PASS is for the compatibility experiment, not the unmodified PR. The original snapshots were unchanged. Live drafted/plain, cached/fresh and concurrent/solo checks were not run: the roughly 115 GB GLM oQ2 checkpoint exceeds this Mac's 32 GB capacity.

test test-golden: 1,573/1,582 passed, 8 skipped; the existing GLM sparse-attention failure and checkpoint logged-error diagnostic remain. Goldens have zero unexpected 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.
…t; a width-carrying matrix replaces Q4

Every GLM projection was typed 4-bit/group-64 (weights.zig's Q4), so an oQ checkpoint could not even be planned.

- Widths (quant.zig): each module's MLX affine width from config.json through core/quant (keys.canonical, then
  GLM's own spellings: the MTP block is layer 45, forget_gate folds into the KDA gate). Aliases that disagree,
  a missing table and other modes are refused; a tensor stored at another width than declared is refused by name,
  never read at a guessed width.
- Affine{bits, group} in place of Q4 (affine.zig): stored shapes, byte spans and column cuts follow each tensor's
  width; a fused matrix whose members differ in format plans one run a member at its output rows, as 0.6.6's QSplit
  calls each part (glm5_next/linear.py); a one-format stack stays one matrix. The load plan's layer and expert bytes
  are counted at those widths.
- mlx-lm's layout (names.zig), which the public oQ checkpoints use: the MTP block, hyper-connections, stacked
  experts, the absorbed embed_q/unembed_out pair and a bf16 eh_proj; a fused conv1d.weight is cut into q/k/v; fp32
  hyper-connection mixes and router narrow to bf16 only when exact (else refused naming fix, shard, offset, shape).
- The dry-run plan prints its matrices by width. A real load still refuses any matrix the generated 4/64 kernels
  cannot read (the next commit gives the device every width); TP2 and expert parallel refuse any width but 4/64.

The uniform 4/64 checkpoint plans the same matrices, offsets and bytes as the 4-bit-only loader (a digest pinned
before the change).

Tests that fail before (no width reading, no mlx-lm names on main): weights_test.zig, oQ-shaped fake checkpoints in
both layouts, each tensor found at its own width; overrides under either prefix; a tensor stored at another width,
two spellings or the MTP block and layer 45 at two widths, an odd member and a routed gate/up pair at two widths
refused by name; oQ4's KDA input and MLA x_proj planned as one run a member; mlx-lm's conv1d and fp32 mixes planned
as the original layout's tensors. quant.zig, affine.zig, names.zig and weights_fix.zig have their own unit tests.

Gate on main 0efbd44 + the core commits (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 logs an error on the path it expects). lean_check: 533 problems (main 539), no new findings.
…t stacks a product a member

GLM has no tensor-unit-only kernel (its *_nax kernels take the simdgroup-matrix fragments without tensor units), so
the pre-M5 path only lacked products at other widths. glm_rows.metal (wide.zig) runs them over core row_projection's
fp32 group sums, 2/3/4/5/6/8 bits in groups of 32/64/128, every row its own sums (one row alone == the same row in
any window):

- dense with input and output row strides and per-head batching: each split-stack member at its own output rows
  (oQ4's KDA input q, v at 5 bits and k, f_a, g_a, b at 8 is six products), mlx-lm's embed_q and unembed_out a head;
- by expert id (decode's routed experts, gate and up fused with SwiGLU, glm_glue's sigmoid_fast and bfloat overloads
  compiled in, not copied), and by sorted pairs (prompt's routed experts); then the existing combine;
- bf16 (the MTP head's unquantized eh_proj); the embedding at 6 and 8 bits.

KDA f_b and g_b at 4/64 keep the generated kernel's fused dots; at any other width they are row products first and
one instance of the generated kda_rows reads them in place of its dots (0.6.6's unfused shape); the products borrow
the dense MLP scratch, so state refuses a config whose KDA width outgrows it. A shared expert not at 4/64 runs its own
products (no shared-in-routed launch); routed experts not at 4/64 run by id and by sorted pair. Prompt chunks: a
one-width 8/64 matrix runs on core affine_mm, split stacks and other widths on the row kernels.

The new pipelines load only when a checkpoint has a matrix the 4/64 kernels cannot read, and their source hash joins
the kernels' identity; a failed compile releases every pipeline built before it. The uniform 4/64 checkpoint compiles
and launches what it did. The previous commit's refusals stay: a member whose rows are not a multiple of 4 (the row
kernels read four rows), routed gate and up at two widths, any width but 4/64 with TP2 or expert parallel.

Tests that fail before (no wide.zig on the previous commit): 4/64 of one width alone takes the generated kernels;
f_b and g_b at any other width (8/64 too) take the products made apart; a failed compile releases every pipeline
built before it and a load counts each one (GPU); glm_rows's SwiGLU compiles glm_glue's helpers, no copy of its own.
The previous commit's load-time refusal of every other width is gone.

Gate on main 0efbd44 + the core commits (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 logs an error on the path it expects). lean_check: 533 problems (main 539), no new findings.
…float64, each row bits in every launch

No model files: seeded synthetic codes, scales, biases and inputs, packed as MLX affine.

- Every bits {2,3,4,5,6,8} x group {32,64,128} on three shapes, and every GLM projection class at the public oQ
  checkpoints' widths at real shapes (KDA in, f_b/g_b, KDA o, MLA x/qr/o, embed_q/unembed_out, dense, shared,
  routed, lm_head), decode rows and the prompt's product separately, against float64 on every column of two rows.
  The bound is the fp32 sums' depth (k / 8 + 64 additions) plus a bf16 rounding; each class prints its worst column
  (max |err| and its share of max |y|). A reference with scales 1% off must exceed the bound in every class and width.
- Bit-equal pairs: one row alone == the same row in 2-, 3- and 16-row windows; strided == contiguous; per head ==
  each head dense; by id == dense; GLU by id == glm_swiglu(dense gate | up); sorted == by id (experts of 8, 5, 2 and 1
  pairs); the row kernels == core row_projection; a split stack in one call == each member alone (oQ4's KDA input
  5/8/5/8/8/8 and x_proj 6/6/5/5, at the decode pitch and the 64-padded prompt pitch); experts [2048, 4096] and
  [4096, 2048] at 2 and 4 bits by pick and sorted == dense.
- KDA: the separate f_b/g_b products then kda_rows == the generated 4/64 kda_rows byte for byte (output, state, conv
  window) on every 4-bit code, random per-group power-of-two scales and nonzero biases.
- The 6- and 8-bit embedding against float64 within two bf16 roundings; a neighbouring group's bias is caught.

The engine module exports core/quant (`tensorfold.quant`) for the check's MLX reader.

tf-glm-rows-check (M3 Ultra, no tensor units): PASS 1,328,072 values within the fp32-sum bound of float64 (worst
0.9839 of it, bf16 output rounding; worst-column |err| / max|y| 1.7e-3 to 3.6e-3 a class), 1,780,096 bit-equal
pairs; scales 1% off exceed the bound in every class and width.

Gate on main 0efbd44 + the core commits (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 logs an error on the path it expects). lean_check: 533 problems (main 539), no new findings.
…e_mm

oQ2's routed experts (2-bit, group 64) ran a prompt chunk on the sorted row kernels: 81% of a 2048-row chunk's GPU
time. With core affine_mm reading 2-bit codes, wide.routed(gate, up, down) picks affine_mm's 4/64 pipelines, its
2/64 ones (wide.Pipes.mm2) or the sorted row kernels, and 2/64 experts take gatherGlu/gatherTo as 4/64 ones do;
other widths stay on the row kernels. Decode is unchanged.

Test that fails before: "prompt chunks gather routed experts on affine_mm at 4/64 and 2/64 only; other widths take
the sorted row products" (no wide.routed). tf-glm-rows-check adds gatherCheck: 2/64 experts gathered on affine_mm
== the dense product (GLU == glm_swiglu(dense gate | up), down == dense) at expert shapes, and the routed q2 prompt
classes now run on affine_mm against float64.

tf-glm-rows-check (M3 Ultra, no tensor units): PASS 1,328,072 values within the fp32-sum bound of float64 (worst
0.9839 of it, bf16 output rounding; worst-column |err| / max|y| 1.7e-3 to 3.6e-3 a class), 1,976,704 bit-equal
pairs; scales 1% off exceed the bound in every class and width.

TensorFold/GLM-5.3-Flash-MLX-oQ2-MTP @dc55627 on an M3 Ultra (plan 109.01 GB: 2-bit/64 x126, 4-bit/64 x3, 8-bit/64
x290; loads in 21-24 s): drafted == `"draft": false` 4/4 at 160 tokens (MTP acceptance 74-88%; plain 39.5-40.1
tok/s, drafted 50.9-54.4); resumed == fresh (a 2427-token prompt at 0 and 2425 cached tokens; the second turn
resumed at 2425 == a server with `--prompt-cache-gib 0`); concurrent == solo, 2 streams. Cache off: prefill 2427
tokens 5.09-5.11 s (475-477 tok/s; 163 tok/s with the experts on the sorted row kernels), decode 256 tokens
40.8-41.0 tok/s.

Gate on main 0efbd44 + the core commits (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 logs an error on the path it expects). lean_check: 533 problems (main 539), no new findings.
@ashhart
ashhart merged commit ad83576 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 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.
@ashhart

ashhart commented Oct 14, 2026

Copy link
Copy Markdown
Owner

Thank you, @grantoverton. GLM-5.3-Flash on a Mac serves mixed-width checkpoints such as TensorFold/GLM-5.3-Flash-MLX-oQ2-MTP. A uniform 4-bit checkpoint compiles none of the new pipelines. Two Macs and expert parallel still take 4-bit group-64 checkpoints only, and refuse others by name at load.

Your commits are in 1.0.10 under your name (c1679de1a, ea1e42d5b, 71961a484, 06c96038d). The CHANGELOG line is f1deedead. Your head 5fe7e7890 is merged at ad83576f3 with no change to the tree. The checks below ran at 9777aeb2d. 50a267cdf makes the pipeline-release test skip when there is no Metal device, as the tree's other device tests do.

The oQ checkpoints rest on your runs on an M3 Ultra and @sethforprivacy's on an M5 Ultra, both at 9777aeb2d (drafted equal to "draft": false, resumed equal to fresh, concurrent equal to solo). @mrv777 ran the width checks on an M1 Max. Thank you both. No uniform GLM-5.3-Flash checkpoint fits one 256 GB Mac within the 70% load limit, so its uniform path was checked by kernel text and the row checks, and was not served.

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