Skip to content

Qwen3.8-Flash-Next faster kernels in Zig engine - #788

Merged
ashhart merged 13 commits into
ashhart:mainfrom
grantoverton:oqxe/speed-flashnext
Oct 14, 2026
Merged

ashhart merged 13 commits into
ashhart:mainfrom
grantoverton:oqxe/speed-flashnext

Conversation

@grantoverton

@grantoverton grantoverton commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Rebased onto main c36b536 (1.0.9) and #774's rebased tip on 2026-10-13: no conflicts in this change's code; the tip passes the gate again. The receipts below are the pre-rebase runs.

Part of #716; stacks on #774 (which stacks on #755). This branch carries their ten commits until they merge, so please review the last three commits here (from "Flash Next decode runs dense projections and hyper-connections on per-row kernels" on). Without tensor units, Flash Next's decode runs every dense projection and hyper-connection on the lane kernels, whose -lanes twin reads a one-row window's weights at 240-640 GB/s on an M3 Ultra. As 0.6.6 does before M5, this runs them one input row a simdgroup (with a matrix-unit twin and, for 6- and 8-bit products at 2-4 rows, four rows a simdgroup, all three giving a row the same bits), and picks the draft depth by measured round cost. On an M3 Ultra the uniform 6/32 checkpoint decodes 1.06× (code) and 1.19× (chat) faster, oQ2 1.02× and 1.14×, oQ3 1.04× and 1.11×, oQ4e 1.06× and 1.21×; prefill is unchanged. GPUs with tensor units keep every kernel and launch they run today.

This changes the uniform 6/32 checkpoint's tokens on GPUs without tensor units (M1 to M4): its dense products' fp32 sums run in another order, within the same float64 bound, and fidelity against oMLX is below. With FZ_M3_LANES set, decode keeps the lane kernels and their tokens.

What this changes

  1. Flash Next decode runs dense projections and hyper-connections on per-row kernels without tensor units. fn_rows.metal: q4_qmv_rows at 4/32 and qa_qmv_rows at any MLX width, one input row a simdgroup with MLX's qmv_fast inner loop and fp32 sums, and for other widths than 4/32 a matrix-unit twin from 4 rows that keeps those sums (qa_qmv_rows_mma over qa_row_block_sums). fn_rows_hc.metal: hyper-connections each row in its own threadgroups, a 4/32 pair's 8-row tiles from 8 rows. rows_load.zig loads every dense lane's members in MLX layout at their own widths beside the lanes (TP and the 6-bit fast paths keep the lanes), checks each twin and tile against the per-row loop at load and keeps the loop where one differs. Model.lane and the hyper-connection calls take the rows when they are loaded and there is no TP.
  2. Four rows a simdgroup for 6- and 8-bit products at 2-4 rows, the same bits. qa_qmv_rows4 cuts each code once for four input rows, each row keeping qa_qmv_rows' lanes, fma chain and fp32 sums in order. rows_w picks the kernel by width and row count from rows-time: 6 and 8 bits take it at 2-4 rows and the twin past them; 4/32 keeps the loop; the other widths the loop to 3 rows and the twin from 4, where the four-row kernel measured slower.
  3. Draft depth by measured round cost without tensor units (drafts_cost.zig, 0.6.5's DraftDepth for one stream). On the row kernels a round's time grows with its window, so the lane-tuned depth rule drafts too deep. CostDepth keeps each depth's landing rate and each window width's measured round time and picks the depth with the most expected tokens a millisecond: head rounds up to 3 drafts, copied rounds up to 15. TP and tensor-unit GPUs keep the existing rule. The depth changes which rows share a window, never a row's bits.

Exactness

TensorFold/Qwen3.8-Flash-Next-MLX-oQ2-MTP @0a1bf6f, TensorFold/Qwen3.8-Flash-Next-MLX-oQ3-MTP @993186e, Jundot/Qwen3.8-Flash-Next-oQ4e-mtp @2615fc0 and #774's uniform 6/32, greedy, MTP drafts on unless "draft": false; serve <checkpoint> --name flashnext --parallel 2 --context 16384 --prompt-cache-gib 0. Before is #774's tip b29d032; after is a19b2cb1, this branch's tip 2759e70 but for one doc comment joined onto one line.

  • Row invariance. A row's bits depend on its own inputs alone in every window, whichever of the three kernels runs it, so drafted equals plain, resumed equals fresh and concurrent equals solo as before.
  • Uniform 6/32 and oQ2, after: concurrent equals solo (bench_concurrent's alone and serial checks at 1 and 4 requests, both prompts: 34/34 on each, both runs); cancellation, reset and partial acceptance, 25 steps, pass (uniform acd6c2129c4f / d2ed982f7f4b, oQ2 0fb078e5ec87 / e7bf591da8fa). oQ3 and oQ4e, after: concurrent equals solo (34/34 each) and the 25 cancellation steps pass (oQ3 a5c773efe57b / fcd1fba4ccc0, oQ4e dc4869db94b0 / 96c25ce8bedb).
  • The tokens change where a product's sum order changes: the uniform 6/32 code prompt cabc8bb121c8 → af87d66e6194, chat 7c3f61c8db2c → b294a0e6e95e; oQ2 556545c4fd22 → e6305acf5ab2, 64b3971d709b → 2710d23bcad7; oQ3 and oQ4e keep their code tokens (02a5b69658f0, 581df06bae38) and change chat (cdf91657573d → d59e34de846e, 33924006fc9d → 03acda6865af).
  • Rows and float64. tf-flashnext-widths at the tip: PASS, 4,486 checks, worst 0.9947 of the float64 bound. The rows class (1,038 checks, worst 0.9844): every width 2/3/4/5/6/8 in groups of 32/64/128 on four shapes and the decode shapes at the public checkpoints' widths, against float64 with a 1%-off-scales control; every row's bits equal in windows of 1, 2, 3, 4, 5, 8, 9, 16, 17 and 33 rows against a 64-row call, so the loop, the four-row kernel and the twin are checked against one another at every width; the checkpoints' mixed stacks as the engine attaches them, each member's columns equal its own product's in every window. rows-hc: hyper-connections at every oQ width, rows 1-16, against float64 and the recorded 6/32 kernels' bits on the same values.
  • Precision. Activations stay bf16; every product keeps fp32 group sums.

Performance

Mac Studio, M3 Ultra (80-core GPU), 256 GB, macOS 26.4.1 (25E253), Zig 0.17.0. Fresh builds, zig build native -Dcpu=apple_m1; tensorfold-native sha256 92dca1896a14… (before) and 0c8a03935857… (after). Every run starts from a fresh server, with other checkpoints' file cache evicted first; before and after alternate (before, after, after, before).

Decode and concurrency. 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 bench_openai code chat concurrent code ×1 code ×4 chat ×1 chat ×4 Peak footprint
uniform 6/32, before 1 111.76 89.05 111.5 103.9 88.9 83.7 184.28 GB
uniform 6/32, after 1 116.48 106.50 117.2 109.4 106.4 99.6 185.24 GB
uniform 6/32, after 2 119.42 105.97 116.2 109.5 106.3 99.8 185.06 GB
uniform 6/32, before 2 111.20 88.89 111.1 103.6 88.7 83.5 184.35 GB
oQ2, before 1 99.95 85.95 99.8 93.5 85.6 80.8 96.81 GB
oQ2, after 1 101.95 97.57 101.6 96.3 97.5 91.3 97.70 GB
oQ2, after 2 101.63 97.45 101.9 96.4 97.7 91.1 97.45 GB
oQ2, before 2 99.87 85.84 99.7 93.4 85.9 80.7 96.81 GB
oQ3, before (#774's receipt) 104.40 99.10 104.2 97.3 99.1 92.5 118.93 GB
oQ3, after 108.15 110.28 108.1 100.2 110.1 102.5 119.55 GB
oQ4e, before (#774's receipt) 112.92 90.17 112.9 105.0 90.1 84.6 132.71 GB
oQ4e, after 120.06 109.38 119.9 111.7 109.4 101.7 133.36 GB

Against before: uniform 6/32 code 1.06× and chat 1.19×; oQ2 1.02× and 1.14×; oQ3 1.04× and 1.11×; oQ4e 1.06× and 1.21×; the concurrent cells move the same way. The chat prompt gains most: its windows are short (head drafts, 1-4 rows), where the row kernels are fastest. The code prompt copies long runs, whose wide windows the lanes ran almost for free; with the depth picked by cost it still gains. oQ4e now decodes code at 1.02× and chat at 1.03× of the uniform checkpoint, oQ3 at 0.92× and 1.04×. The oQ3 before-run is #774's receipt on the branch before the n-gram scale, which oQ3 (no weight_scale) never reaches; oQ4e's is #774's on its tip.

Cold prefill (uniform 6/32; before and after). 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; /v1/completions with the ids, max_tokens 1, --parallel 1 --context 34816 --prompt-cache-gib 0, every request cold (cached_tokens 0), a 2k warmup, one rep a size. The prompt kernels are unchanged:

Tokens before tok/s (s) after tok/s (s)
2,048 1292.7 (1.58) 1289.4 (1.59)
8,192 1382.3 (5.93) 1384.3 (5.92)
32,768 1464.9 (22.37) 1466.3 (22.35)

Per launch (tf-flashnext-widths rows-time, synthetic weights at decode shapes over at least 1 GB of copies, µs a launch, median of three command buffers; lane is #774's path, after the kernel rows_w picks; the receipt's run also timed a 4/32 four-row kernel, dropped as slower than the loop):

Shape, width Rows Lane After Lane / after
[16480, 2560] 6/32 1 99.0 51.3 1.93×
2 99.5 60.0 1.66×
4 100.2 61.3 1.63×
16 108.0 180.1 0.60×
[248320, 2560] 8/64 1 1064.1 876.8 1.21×
4 1082.0 883.5 1.22×
16 1219.2 2316.0 0.53×
[13952, 2560] 3/64 1 81.7 30.4 2.69×
4 82.1 69.7 1.18×
16 88.8 129.8 0.68×
[10240, 2560] 4/32 1 67.7 24.9 2.72×
4 67.7 33.5 2.02×
16 72.9 116.8 0.62×

The lane kernels' time is nearly flat in the row count and the row kernels' is not, which is why commit 3 picks the depth by measured cost: one-row and short windows gain, and the widest copied windows are drafted less often.

Memory

The row kernels read each dense projection in MLX layout beside the lane pack: +0.8 GB after load on the uniform 6/32 (184.25 → 185.02 GB physical footprint) and +0.9 GB on oQ2 (96.31 → 97.25 GB), oQ3 (118.19 → 119.13 GB) and oQ4e (131.97 → 132.91 GB). With FZ_M3_LANES set, none of it is loaded.

Fidelity

Teacher-forced against oMLX 0.7.1.dev1 on the same checkpoints and the same fixed histories as #774 (oMLX's reference reran here and is byte-identical to #774's on all four): upstream's four public teacher prompts, 1,024 positions, top two at every position from tf-flashnext-teacher (a test program, not in the PR), which prefills each prompt through the prompt chunks and feeds every history token in a one-row window, so each position runs on the row kernels here.

Checkpoint #774 differences (beyond the tie policy) This PR
uniform 6/32 13 (10) 12 (9)
oQ4e 10 (4) 8 (5)
oQ3 7 (7) 12 (11)
oQ2 30 (23) 29 (25)

Two checkpoints move closer to oMLX, oQ2 is level and oQ3 has five more differences: a different fp32 sum order moves which near ties fall either way, In the 61 differences this branch's pick is oMLX's second token 54 times, its third 6 times and its fourth once (oQ4e's code position 142, as in #774).

Every difference (oMLX's pick and margin; this branch's pick and margin; its rank in oMLX's top 20)
Checkpoint Prompt Position oMLX pick margin this branch's pick margin rank Verdict
uniform 6/32 copy 14 248045 0.375 846 1.625 2 beyond tie policy
uniform 6/32 copy 15 248046 0.5625 248045 0.5 2 beyond tie policy
uniform 6/32 copy 19 198 0.875 248046 0.75 2 beyond tie policy
uniform 6/32 arithmetic 59 56491 0.125 27567 0.125 2 near tie
uniform 6/32 arithmetic 205 846 0.875 248045 2.5 2 beyond tie policy
uniform 6/32 arithmetic 206 248046 1.625 198 0.125 2 beyond tie policy
uniform 6/32 arithmetic 215 2064 1.0 760 0.125 2 beyond tie policy
uniform 6/32 story 38 8831 0.125 6316 0.125 2 near tie
uniform 6/32 story 139 17415 0.25 21678 0.125 3 beyond tie policy
uniform 6/32 story 254 68342 0.125 41423 0.0 2 near tie
uniform 6/32 code 140 248046 0.25 198 0.875 3 beyond tie policy
uniform 6/32 code 156 71093 0.125 760 0.375 2 beyond tie policy
oQ4e arithmetic 131 198 0.0 318 0.25 2 beyond tie policy
oQ4e story 47 10184 0.125 1801 0.125 2 near tie
oQ4e story 171 16002 0.125 12701 0.125 2 near tie
oQ4e story 212 324 0.0 70676 0.125 2 near tie
oQ4e code 142 248045 0.0625 1787 0.4375 4 beyond tie policy
oQ4e code 173 2493 0.125 557 0.25 2 beyond tie policy
oQ4e code 209 248046 0.25 198 1.125 2 beyond tie policy
oQ4e code 243 883 0.25 0 0.0 2 beyond tie policy
oQ3 copy 15 248046 0.5 198 2.0 2 beyond tie policy
oQ3 arithmetic 37 220 0.0 279 0.25 2 beyond tie policy
oQ3 arithmetic 202 248045 1.0 846 1.25 2 beyond tie policy
oQ3 arithmetic 228 248046 0.0625 248045 0.75 2 beyond tie policy
oQ3 arithmetic 239 248046 0.625 248045 0.125 2 beyond tie policy
oQ3 story 51 321 0.25 22186 0.125 2 beyond tie policy
oQ3 story 114 14484 0.0 29952 0.125 2 near tie
oQ3 story 207 14656 0.0 34909 0.375 2 beyond tie policy
oQ3 code 33 5447 0.25 561 0.0 2 beyond tie policy
oQ3 code 139 248045 0.625 248046 0.625 2 beyond tie policy
oQ3 code 142 1787 0.375 248045 1.3125 3 beyond tie policy
oQ3 code 143 248046 0.3125 198 0.0625 2 beyond tie policy
oQ2 copy 17 74455 0.5 248046 0.125 3 beyond tie policy
oQ2 copy 42 198 4.375 248046 0.25 2 beyond tie policy
oQ2 copy 70 74455 0.875 846 0.375 2 beyond tie policy
oQ2 copy 84 369 0.125 7701 0.625 3 beyond tie policy
oQ2 copy 95 846 0.875 74455 1.375 2 beyond tie policy
oQ2 copy 96 198 1.5 248046 0.75 2 beyond tie policy
oQ2 copy 106 760 0.125 40 0.25 2 beyond tie policy
oQ2 copy 148 846 0.25 74455 1.375 2 beyond tie policy
oQ2 arithmetic 62 393 0.25 279 0.0 2 beyond tie policy
oQ2 arithmetic 78 220 0.125 18 0.0 2 near tie
oQ2 arithmetic 208 60439 0.25 283 0.0 2 beyond tie policy
oQ2 arithmetic 237 25273 0.125 283 0.0 2 near tie
oQ2 story 4 15218 0.0 22725 0.0 2 beyond tie policy
oQ2 story 136 725 0.25 28552 0.625 2 beyond tie policy
oQ2 story 157 59035 0.25 9101 0.125 2 beyond tie policy
oQ2 story 170 3184 0.5 13087 0.125 2 beyond tie policy
oQ2 story 180 2064 0.125 1465 0.25 2 beyond tie policy
oQ2 story 183 5983 0.8125 2682 0.0625 2 beyond tie policy
oQ2 story 205 1274 0.0 11840 0.3125 2 beyond tie policy
oQ2 story 211 16002 0.0 41217 0.5 2 beyond tie policy
oQ2 story 226 18473 0.3125 279 0.0625 2 beyond tie policy
oQ2 story 228 264 0.0 34830 0.125 2 near tie
oQ2 story 233 1248 0.125 41423 0.125 2 beyond tie policy
oQ2 story 238 36 0.25 1465 0.125 2 beyond tie policy
oQ2 story 244 1049 0.125 561 0.125 2 near tie
oQ2 code 36 220 0.0 321 0.375 2 beyond tie policy
oQ2 code 198 248045 1.375 248046 1.875 2 beyond tie policy
oQ2 code 202 248044 0.0 248046 3.125 3 beyond tie policy
oQ2 code 204 248045 0.875 248046 0.875 2 beyond tie policy

Host suite and lean check

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.
  • Not run: M1, M2 and M4 (no hardware; they take the same row kernels as the M3); CUDA (no CUDA code changes).
  • M5 (tensor units): unchanged; rows_load.on is false there, so no row kernel is compiled or loaded and the depth rule is the existing one.
zig build tf-flashnext-widths native -Dcpu=apple_m1
./zig-out/bin/tf-flashnext-widths rows
./zig-out/bin/tf-flashnext-widths rows-hc
./zig-out/native/bin/tensorfold-native serve <checkpoint dir> --name flashnext --context 16384 --parallel 2

then, greedy: a request and the same with "draft": false (same tokens), and two requests at once against each alone.

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 (served on the tip, every checkpoint above).
  • No precision traded for speed. If the bits change, the description says which sums change and why: the dense and hyper-connection products' fp32 sum order without tensor units (above), within the same float64 bound.
  • Prompt processing is no slower than the last release: the prompt kernels are unchanged (above).
  • 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/09035b07768c87e8dd6112e1452e19d8

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.
…unds buffers to engine_rounds.zig

Moved, to make room under replay.zig's and engine.zig's lengths for the widths that follow: DepthRule,
its test and copyDrafts (re-exported from replay.zig, the test reached from replay's test block), and the body of
Engine.rounds (Engine keeps a one-line rounds, so the load test that reads the source still finds `try e.rounds();`;
RING is now pub for it). Each moved doc comment is one line now; no behaviour changes.

replay.zig 3321 -> 3265 lines, engine.zig 899 -> 857.

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); native and tf-flashnext-run (with flashnext_bench),
-reuse, -pack and -think build. lean_check: 531 problems (main 539); the only findings not on main are the two
long files' shorter line counts.
…ine still runs 6-bit group 32 only

Admission took one global 6-bit/group-32 width, and the packs refused group 128 and fused members of mixed widths.

- Widths (widths.zig): each module's width through core/quant (module keys under any prefix; disagreeing spellings
  and half-declared overrides refused; `false` and `{}` mark plain modules). Admission reads MLX affine 2, 3, 4, 5, 6
  or 8 bits in groups of 32, 64 or 128 for every text and MTP matrix, refuses the first other one under its
  checkpoint tensor name, and an n-gram table whose group does not divide its 160-wide rows.
- Lane packs follow 0.6.6's _stacked: a fused projection's members grouped by bits in first-seen order, adjacent or
  not, each group one run at its smallest group, regrouped exactly (core/quant widen; group 128 read as 64), with
  .runs/.members tables. A run whose columns are not a multiple of 32 keeps MLX's layout untiled (decode.py's nt 0;
  oQ4e's layer-21 GDN input). A projection of one width keeps its single run.
- Hyper-connections: a down and inject pair of two widths widens exactly to the wider (QWeights.of); the up
  projection packs at its stored width. Prompt (pack_mlx) projections of several widths keep each member's bytes in
  order with a .parts table. Experts, embedding and n-gram tables, which the engine reads straight from the
  checkpoint, are shape-checked at their declared widths when the pack is built.
- oMLX's layout: an MTP head kept at top-level mtp.* is read where the index keeps it; an n-gram table's weight_scale
  (oQ conversions of the FP8 checkpoint keep its scale, as oQ4e does) must be one finite, nonzero value, else the pack
  is refused by name; the next commit applies it.
- Pack format 3, whose identity also hashes config.json, so a config-only change rebuilds the pack; packs under the
  format-less identity are rebuilt, never reused. Pack building frees every buffer when an allocation fails.

The uniform 6/32 packs keep every tensor byte for byte (a digest pinned before the change). The engine refuses any
other width at load until the next commit gives the device its kernels.

Tests that fail before: widths_test.zig; load_admission_test.zig's overrides at other widths, global groups 64/128,
refusal under the tensor's name, oMLX's mtp.*; pack_lane_test.zig (runs by bits, a pack per run, ragged runs
untiled at oQ4e's real rows 10240/6144/48/48, allocation failures free everything); pack_hc_test.zig (oQ4e's
4/64 down with a 5/64 inject as one 5/64 matrix; the up projection at its stored width; mixed prompt members);
pack_widen_test.zig; pack_test.zig's half-declared and contradicting raw tensors; pack_source_test.zig's config-only
change; pack_cache_test.zig's format-less identities.

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); native and tf-flashnext-run (with flashnext_bench),
-reuse, -pack and -think build. lean_check: 531 problems (main 539); the only findings not on main are the two
long files' shorter line counts.
…nits, in decode and in prompt

- Lanes (decode, the MTP head, lane rounds; lane_w.zig): the recorded lane kernel's text compiled per (N, K, bits,
  group): codes staged as bytes, group sums over the group, K slices as lane_qmm.split_k; one launch a run, columns
  placed through the .members map (FZ_MAP in the kernel's store), untiled runs as MLX lays them out. A 6/32 lane keeps
  its recorded kernels and launches; a lane at other widths holds no 6/32 pair, so a 6-bit kernel can't read one of
  its runs (LaneNot6Bit32).
- Hyper-connections, experts, embedding and n-gram tables (variants.zig): 0.6.6's any-width qa_ kernels, recorded
  at 6/32, launched at the tensor's BITS/GS (WB/WG, SWB/SWG for the experts). The 6-bit fast paths (fz_xgu/fz_xdown,
  fz_dense, fz_hc_up) run only on 6/32 tensors; an expert whose gate and up differ in width, and n-gram shards of
  different widths, are refused (one kernel reads both).
- Prompt products (prompt_w.zig, prefill/qmmw_nax.metal): qmm6_nax's tiles and fp32 sums with codes read at WB bits
  in groups of WG: dense, split-K (the hyper-connection down), sorted-expert gathers and the fused gate/up; a fused
  projection of several widths runs one product a member into its columns. Every prompt weight is registered with
  its width at load, and a launch on an unregistered one fails the pass by name (PromptWeightWidthUnknown).
- Fail closed: every width kernel compiles at load (engine_warm.zig), so a compile failure refuses the load, not a
  request. Width tables are read on the host, never through device buffers. Speed-up mode (two Macs) splits the 6/32
  kernels only and refuses any other checkpoint by name (SpeedUpNeeds6Bit32). The replay harness refuses other
  widths (HarnessNeeds6Bit32). The config and index are read once at load.
- The n-gram table's weight_scale (ple_scale.zig): oQ conversions of the FP8 checkpoint store the unscaled rows and
  oMLX's runtime multiplies each looked-up row by the scale (bf16 row times bf16 scale, one rounding). One in-place
  kernel after the lookup, in decode and in prompt, compiled at load only when the scale is not 1.

The uniform 6/32 checkpoint keeps every recorded kernel and launch.

Tests that fail before (none of these files exist on the previous commit): lane_w_test.zig (split_k gives every
recorded kernel's SK; every recorded lane text takes any width, a row stride and a column map and keeps its
arithmetic; runs land at their members' columns; width tables read on the host; a lane at other widths gives a
6-bit kernel nothing to read), engine_widths_test.zig (oQ2's experts, embedding and n-gram tables at their own
widths; the uniform checkpoint keeps every recorded kernel; mixed gate/up and n-gram shards refused; the 6/32-only
harness refuses others), prompt_w_test.zig, ple_scale.zig's scale against the bf16 product on the host.

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); native and tf-flashnext-run (with flashnext_bench),
-reuse, -pack and -think build. lean_check: 531 problems (main 539); the only findings not on main are the two
long files' shorter line counts.
…loat64, each row's bits in every window

No model files: seeded synthetic codes, scales, biases and inputs, packed as the engine packs them. Every projection
class at bits {2,3,4,5,6,8} x group {32,64,128}, decode and prompt separately, plus each public checkpoint's mixed
stacks and oQ4e's ragged one: lanes at K slices 1, 2, 4 and 8; hyper-connections (down partials and up); experts
(gate/up through silu, down); the embedding and the n-gram tables (8-bit codes over their full range); prompt dense,
split-K, expert gathers and the fused gate/up. Each check reports its worst column against a float64 bound, and a
reference with scales 1% off must exceed it (negative control). Every row's bits must be equal alone and in every
window (1-16 rows in decode, 1-65 in prompt), and the generic texts at 6/32 must give the recorded kernels' bits.
`tf-flashnext-widths lane|hc|moe|prompt` runs one class.

tf-flashnext-widths (M3 Ultra, no tensor units): PASS 2722 checks, worst column 0.9947 of its float64 bound (bf16
output rounding); all 262 negative controls exceed the bound, the least by 1.012x; every row's bits equal alone
and in every window. The core lane check without tensor units passes as on the core commits.

Qwen3.8-Flash-Next-MLX-oQ2-MTP @0a1bf6f on an M3 Ultra (its cached pack; loads in 21.1 s from a cold file cache,
9.0 s warm): drafted == `"draft": false` 4/4 (MTP acceptance 55-59%; plain 45.0-54.1 tok/s, drafted 84.3-91.2);
resumed == fresh (a 5.3k-token prompt at 0 and 5262 cached tokens; the second turn resumed at 5262 == a server with
`--prompt-cache-gib 0`); concurrent == solo, 2 streams; greedy text sane (RYB and RGB primaries, a doctested
function, "5:45 PM").

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); native and tf-flashnext-run (with flashnext_bench),
-reuse, -pack and -think build. lean_check: 531 problems (main 539); the only findings not on main are the two
long files' shorter line counts.
…-row kernels without tensor units

Without tensor units Flash Next's decode took the lane kernels at every window width: their -lanes twin reads a
1-row window's weights at 240-640 GB/s on the M3 Ultra. As 0.6.6 does before M5 (rows.py), decode now runs each dense
projection one input row a simdgroup (fn_rows.metal: q4_qmv_rows at 4/32, qa_qmv_rows at any MLX width, with MLX's
qmv_fast inner loop and fp32 sums), and for other widths than 4/32 from 4 rows a matrix-unit twin that keeps those
sums (qa_qmv_rows_mma over qa_row_block_sums). Hyper-connections take fn_rows_hc.metal the same way, each row its own
threadgroups, a 4/32 pair's 8-row tiles from 8 rows. rows_load.zig loads every dense lane's members in MLX layout at
their own widths beside the lanes (TP and the 6-bit fast paths keep the lanes), checks each twin and tile against the
per-row loop on 8 and 12 rows at load and keeps the loop where one differs. Model.lane and the hyper-connection
calls take the rows when they are loaded and there is no TP. With FZ_M3_LANES set, decode keeps the lanes.

A row's bits depend on its own inputs alone at every window width, so drafted equals plain, resumed equals fresh and
concurrent equals solo as before. They differ from the lane kernels' bits, on oQ checkpoints and the uniform 6/32:
the fp32 sums run in another order, within the same float64 bound.

Tests: tf-flashnext-widths rows (dense) and rows-hc: every width 2/3/4/5/6/8 in groups of 32/64/128 on four shapes
and the decode shapes at the public checkpoints' widths, against float64 with a 1%-off-scales control, every row's
bits equal in windows of 1, 2, 3, 4, 5, 8, 9, 16, 17 and 33 rows against a 64-row call; the checkpoints' mixed
stacks as the engine attaches them, each member's columns equal its own product's in every window; hyper-connections
at every oQ width, rows 1-16, against float64 and the recorded 6/32 kernels' bits on the same values. rows-time prints
the lane and row kernels' GPU time at decode shapes, 1 to 16 rows.
…t products at 2-4 rows, the same bits

At 2-4 rows each simdgroup of qa_qmv_rows cut every code from its row again for its own input row, and the MMA twin
from 4 rows ran 8-row tiles, so a 6/32 product at 4 rows took 1.8x its 1-row time and an 8/64 one 1.3x. qa_qmv_rows4
runs four input rows a simdgroup: each code is cut once and each row keeps qa_qmv_rows' lanes, fma chain and fp32
sums in the same order, so every row's bits are qa_qmv_rows' bits. rows_w picks the kernel by width and row count
(all three give a row the same bits): 6 and 8 bits take the four-row kernel at 2-4 rows and the twin past them; 4/32
keeps the loop; the other widths keep the loop to 3 rows and the twin from 4, where the four-row kernel measured
slower. The load check runs the four-row kernel beside the twin; a member stops taking whichever differs from the loop
(the twin gives way to the four-row kernel, that one to the loop).

Tests: tf-flashnext-widths rows: every window now crosses the four-row kernel at 2-4 rows against the 64-row call's
twin, every width and decode shape. rows-time prints the loop, four-row and twin times apart.
…r units

On the row kernels a round's time grows with its window (a 16-row copied window costs about twice a 1-row one, where
the lane kernels' cost is nearly flat), so the depth rule tuned for the lanes drafts too deep. As 0.6.5 does for one
stream before M5 (DraftDepth), drafts_cost.CostDepth keeps each depth's landing rate and each window width's measured
round time (the GPU gap between consecutive read rounds) and picks the depth with the most expected tokens a
millisecond: head rounds up to 3 drafts, measuring each depth once first and probing one deeper or shallower every
8 rounds; copied rounds up to 15 drafts at their landing rate. TP and tensor-unit GPUs keep the existing rule.

The depth changes which rows share a window, never a row's bits: drafted output still equals "draft": false.

Tests: drafts_cost.zig's unit tests: unmeasured depths first, then the best rate of tokens a millisecond; landing
rates move toward kept drafts; costs interpolate between measured widths and extend past the widest by rows.
@ashhart
ashhart merged commit 25c3731 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 three commits landed above with the same code. Our commit after them wraps the lines past 120 columns. 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. On Macs without tensor units, Qwen3.8-Flash-Next's dense projections and hyper-connections run one input row a simdgroup, with a matrix-unit twin from 4 rows, and the draft depth is chosen by measured round cost. This changes the uniform 6-bit checkpoint's tokens on those Macs. Its dense products' fp32 sums run in another order, inside the same float64 bound, and FZ_M3_LANES=1 keeps the lane kernels and their tokens. Tensor units keep every kernel and launch.

Your commits are in 1.0.10 under your name (2f79e5a91, 1b8b78233, d2dc17b51). The CHANGELOG line is 355a68904. Your head b1a51eee6 is merged at 25c3731c6 with no change to the tree. The checks below ran at 2759e70f9.

On your M3 Ultra, at 2759e70f9, the uniform 6-bit checkpoint decodes 1.06x (code) and 1.19x (chat) faster, oQ2 1.02x and 1.14x, prefill unchanged, with drafted equal to "draft": false and concurrent equal to solo. tf-flashnext-widths, its rows and rows-hc classes included, passes on an M3 Ultra, 4,486 checks within 0.9947 of the float64 bound.

@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.

2 participants