Repository navigation
Qwen3.8-Flash-Next support for oQ in Zig engine - #774
Conversation
|
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 Live.
|
|
M1 Max check at GPU Shader Validation reports a 45,056-byte threadgroup-memory requirement against M1's 32,768-byte limit; the complete unmodified check passes with API validation alone. Live drafted/plain, cached/fresh and concurrent/solo checks were not run: the smallest listed oQ2 checkpoint is about 71 GB, exceeding this Mac's 32 GB capacity.
|
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.
b29d032 to
a42b541
Compare
The contributor's four commits landed above with the same code, build.zig keeping both families' check steps. Our commit after them reads module widths through core/quant's keys. This keeps the PR's head in the history with no change to the tree.
|
Thank you, @grantoverton. Qwen3.8-Flash-Next on a Mac serves mixed-width checkpoints such as TensorFold/Qwen3.8-Flash-Next-MLX-oQ2-MTP and -oQ3-MTP. A uniform 6-bit group-32 checkpoint keeps its recorded kernels and launches. Its pack is rebuilt once, because the pack's identity now covers Your commits are in 1.0.10 under your name ( The oQ checkpoints rest on your runs on an M3 Ultra and @sethforprivacy's on an M5 Ultra, both at |
Rebased onto main c36b536 (1.0.9) on 2026-10-13: no conflicts in this change's code (1.0.9's
pleReffix from #783 sits beside it); the tip passes the gate again on the new base,tf-flashnext-widthsincluded.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 "Flash Next: the depth rule and copy drafts move to drafts.zig" on). Flash Next admits only 6-bit/group-32 on main, and its packs refuse group 128 and fused members of mixed widths. With this the native engine loads, drafts and serves TensorFold/Qwen3.8-Flash-Next-MLX-oQ2-MTP @0a1bf6f, -oQ3-MTP @993186e and Jundot/Qwen3.8-Flash-Next-oQ4e-mtp @2615fc0 exactly on an M3 Ultra, on GPUs with and without tensor units. The uniform 6/32 checkpoints keep their recorded kernels and launches, and their packs keep every tensor byte for byte (only the pack's identity line changes).
What this changes
Four commits on the shared change:
drafts.zig, the rounds buffers toengine_rounds.zig. A pure move, to make room underreplay.zig's andengine.zig's lengths for what follows (3321 → 3265 and 899 → 857 lines at this commit). No behaviour changes.widths.zig): each module's width throughcore/quant; disagreeing spellings and half-declared overrides refused. 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 refuses an n-gram table whose group does not divide its 160-wide rows._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 (corewiden; group 128 read as 64), with.runs/.memberstables. A run whose column count is not a multiple of 32 keeps MLX's layout and runs untiled, asdecode.py'snt = 0; the one public case is oQ4e's layer-21 GDN input (4/64, 5/128, 5/128, 6/64), a 5-bit run of 6192 columns and a 6-bit run of 48. A projection of one width keeps its single run.QWeights.of); the up projection packs at its stored width. Prompt projections of several widths keep each member's bytes in order with a.partstable. 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.mtp.*is read where the index keeps it, and the n-gram tables under either shard spelling (shard_N,shards.N). An n-gram table'sweight_scale(oQ conversions of the FP8 checkpoint keep it, as oQ4e does) must be one finite, nonzero value; the next commit applies it.config.json, so a config-only change rebuilds the pack. Pack building frees every buffer when an allocation fails.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 aslane_qmm.split_k; one launch a run, columns placed through the.membersmap (FZ_MAPin 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). WithFZ_LANE=1, 6/32 lanes take core's lane projection as before and other widths takelane_w.variants.zig): 0.6.6's any-widthqa_kernels, recorded at 6/32, launched at the tensor's bits and group. 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_w.zig,prefill/qmmw_nax.metal):qmm6_nax's tiles and fp32 sums with codes read at any width: 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).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.SpeedUpNeeds6Bit32); the recipe's--tp 2lines for other widths are the CUDA engine's. The replay harness refuses other widths (HarnessNeeds6Bit32).tf-flashnext-widths: Flash Next's products at every MLX width against float64, each row's bits in every window.On upgrade, every 6/32 user's pack is rebuilt once (the new identity); the old cache directory stays on disk.
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 a uniform 6/32 (below), greedy, MTP drafts on unless
"draft": false. Same checkpoint, binary and settings within each comparison.bench_openaiandbench_concurrenttoken hash is identical across the four runs, and the earlier exactness runs gave greedy tokens identical on 6 prompts × plain and drafted × 2 repetitions, drafted equal to plain on both, and the same first token of a 5,267- and a 12,467-token prompt; packs: all 1160 + 291 + 9 tensors identical, and each file's data section byte-identical; only the__metadata__identity differs."draft": false4/4 (MTP acceptance 55 to 59%, 2.8 to 3.8 tokens a round); resumed equals fresh (a 5.3k-token prompt at 0 and 5,262 cached tokens); the second turn resumed at 5,262 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 (RYB and RGB primaries, a Fibonacci function with doctests, "5:45 PM").weight_scaleof 1.993e-4. Concurrent equals solo (34/34); drafted equals plain and the cancellation steps below pass (MTP acceptance 47 to 59%); text sane ("Here is a short Python function to compute the Fibonacci sequence…"). Without the scale, every reply is garbage while every equality still holds, which is why fidelity is checked against a reference too.--parallel 2, on oQ2, oQ3 and oQ4e): a fresh"draft": falseprobe (each bench prompt, greedy, 128 tokens) gives the reference hash and the drafted probe equals it, with partial acceptance seen (accepted < drafted) on both prompts; then after a stream dropped mid-decode, a long prompt dropped mid-prompt, a stream dropped beside a running probe (whose hash is checked too) and a request past the window (refused, the reset path), the drafted and plain probes equal the fresh hashes. 25 steps, pass, on each checkpoint (hashes oQ217516a9b267a/9d1e003353b7, oQ3a5c773efe57b/150f131d4962, oQ4edc4869db94b0/bf13c8e44c84).tf-flashnext-seams, a test program on this branch, not in the PR): the exact 2,048-token public prefill array, in the engine's one 8,192-row chunk against chunks of 128 and of 37, comparing every DeltaNet layer's conv and recurrent state, every attention layer's and the MTP head's key, value and indexer rows, the pooled indexer blocks, the n-gram tail and history, and the last row's logits at seams 256, 512, …, 1,792, the prompt end, and after 64 one-row decode windows; the served replies equal the windows' too.FP64 per class
tf-flashnext-widthson the M3 Ultra (no tensor units): seeded synthetic codes, scales, biases and inputs packed as the engine packs them; every projection class at bits {2,3,4,5,6,8} × group {32,64,128}, decode and prompt separately on identical input rows, plus each public checkpoint's mixed stacks and oQ4e's ragged one. Each check reports its worst column against a float64 bound, and a reference with scales 1% off must exceed it.2722 checks pass, worst 0.9947 of the bound (bf16 output rounding dominates); all 262 negative controls exceed the bound, the least by 1.012× (prompt dense, K 6144, 3/64). The relative errors above 3.9e-3 are the classes whose outputs pass through a nonlinearity or mix (silu, the hyper-connection up's sigmoid). 8-bit codes are checked over their full range for the hyper-connections, experts, embedding and tables. 2- to 5-bit experts read 16 codes a lane step as 0.6.6's
lane_valuesdoes, so they are not bit-equal to a 6/32 recode of the same values; 6- and 8-bit experts are. The n-gram scale kernel equals the host's bf16 product on 4,099 values at oQ4e's scale (ple_scale.zig's test).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-nativesha25692dca1896a14c982…(this branch's tip),18adc658fa963516…(the branch before the n-gram scale, which no checkpoint without aweight_scalereaches: oQ2 gives identical hashes on both) and1c8c96881568cc5f…(main 0efbd44). Every run starts from a fresh server, with other checkpoints' file cache evicted first.Decode and concurrency.
serve <checkpoint> --name flashnext --parallel 2 --context 16384 --prompt-cache-gib 0, MTP drafts on;bench_openai.py … --tokens 256 --reps 3 --temperatures 0(medians;fibonacci-raw, code, andgpu-chat-no-think, chat) andbench_concurrent.py --levels 1,4 --alone --serial(aggregate tok/s; 4 requests share the 2 streams):bench_openaicodeThe uniform path is unchanged: every cell of the branch's runs is within 0.9% of main's (the largest, concurrent code ×1, is main's first run below the other three), 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, the standard server for Flash Next; stock mlx-vlm 0.7.6 refuses the checkpoint's MTP head parameters at its strict load. oQ4e decodes at 1.01× the uniform checkpoint's code and chat, oQ3 at 0.93× code and 1.11× chat, and oQ2 at 0.89× and 0.96×. oMLX batches four requests (151 to 154 tok/s at ×4); this engine serves two streams. Their non-6/32 products run on the generic kernels; a follow-up PR series works on that speed (#716). The oQ rows ran at the branch's tip (oQ3 before the n-gram scale, as above).
Cold prefill (uniform 6/32; 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;/v1/completionswith the ids,max_tokens 1,--parallel 1 --context 66560 --prompt-cache-gib 0, every request cold (cached_tokens0), a 2k warmup, three reps of each size, medians:--context 132096)Native-window prefill not run; happy to add it if wanted.
oQ prefill (the earlier exactness servers, cache off): not measured against the uniform checkpoint's arrays; the oQ checkpoints have no before-run.
Memory
Weights as stored: oQ2 71 GB, oQ3 93 GB, oQ4e 106 GB, the uniform 6/32 158 GB; oQ2's pack is 4.5 GB. Physical footprint (
darwin_rusage_info_v4, the server process) at--parallel 2 --context 16384after load and at the end: uniform 184.25 / 183.90 GB (peak 184.31 GB, main and this branch equal to 0.03 GB), oQ4e 131.97 / 132.23 GB (peak 132.71 GB), oQ3 118.19 / 118.46 GB (peak 118.93 GB), oQ2 96.31 / 96.63 GB (peak 97.10 GB). The uniform 6/32 peaks at 184.58 GB at--parallel 1 --context 66560and--context 132096on both.Fidelity
Teacher-forced against oMLX 0.7.1.dev1 on the same checkpoint: oMLX's own load path (its pre-load patches, with mlx-vlm 0.7.7's
qwen4_expand oMLX's compat patch, and each per-module width's runtime aliases), which applies the n-gramweight_scale. 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'stf-flashnext-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.On the uniform checkpoint main and this branch give the same 13 differences at the same positions, so that gap to oMLX is there before this change. oQ4e and oQ3 sit under it; oQ2 has more, and at 2 bits the two engines' different products (this branch's fp32 group sums against MLX's 2-bit kernels) move more small margins past one spacing. In every difference this branch's pick is in oMLX's top four (second in 51 of 60), 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 6/32 (main and this branch, identical): 13 differences, 10 beyond the tie policy
oQ4e: 10 differences, 4 beyond the tie policy
oQ3: 7 differences, 7 beyond the tie policy
oQ2: 30 differences, 23 beyond the tie policy
Host suite and lean check
zig build test test-golden -Dcpu=apple_m1at each of the four commits, from a clean detached worktree: 158 of 160 steps; goldens 309/319 equal, the 10 declared differences, 0 unexpected;nativeandtf-flashnext-run(withflashnext_bench),-reuse,-packand-thinkbuild;tf-flashnext-widthspasses 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 (getcallsstd.log.errbefore returning theerror.DeferredTensorthe test expects, and the runner counts logged errors). It fails the same way on main0efbd447.zig build test-nativeat the tip: 16/16, the n-gram scale test included.widths_test.zig;load_admission_test.zig's overrides at other widths, global groups 64/128, refusal under the tensor's name and oMLX'smtp.*;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;lane_w_test.zig,engine_widths_test.zig,prompt_w_test.zigandple_scale.zig's test (new with their files). The uniform packs' digest is pinned.python3 tools/zig/lean_check.py: 531 problems against main's 539 at every commit, no new findings. Gone: four multi-line comments inreplay.zigand one inengine.zig(the moved code's comments are one line now), and Core support for oQ in Zig engine #755's three. Inherited and left:engine.zig(863 lines here, 899 on main),replay.zig(3298, 3321 on main) andtests/flashnext_run.zig(1689, as on main) are over 600 lines on main and do not grow; their multi-line and over-long comments; three intests/flashnext_bench.zig; a multi-line comment inkernels/metal/sources.zig; and Core support for oQ in Zig engine #755's. Every new file is under 600 lines;zig fmtclean.Platforms
tf-flashnext-widthsPASS, and oQ2 serves exactly (drafted, cached and concurrent).tf-flashnext-widthsPASS.tf-flashnext-widths lane|hc|moe|promptruns one class. Then, greedy, per prompt: a request and the same with"draft": false(same tokens); a ~5k-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
tensorfold-native serve <checkpoint> --name flashnext --parallel 2, greedy; cache-off runs add--prompt-cache-gib 0. Each server builds or reuses its own pack. One model process on the machine at a time, every load and check program behind one GPU lock, and the file cache of other checkpoints evicted before each load. The machine'siogpu.wired_limit_mbis 240000, which I set as its owner; nothing else was raised.Where the arithmetic differs from 0.6.6
Bit equality with 0.6.6's kernels is not the bar (#716); the float64 bound per product, row invariance and the exactness contract are.
qmv_rows, row by row (decode.py:174-187). This branch runs the engine's own recorded lane kernel (its twin for GPUs without tensor units) at every width instead.lowbitkernel for 2- and 3-bit andcoop(64 columns) for 4-bit (lane_qmm.py:315-327). This branch stages every width as bytes there too._stacked), the exact regroup, group 128 read as 64 in lane packs, the untiled ragged runs and the hyper-connection widen are 0.6.6's.The open question in #716: the column map
#716 asks whether the lane kernel should take a per-member output offset table, or run the grouped product into scratch and copy. This branch ships the table in Flash Next's own lane kernel (
FZ_MAP,lane_w.zig), because 0.6.6 groups members that aren't adjacent (q6 k6 v8 idx6 is one 6-bit product of three members and one 8-bit product), and the table writes each member's columns in place with no copy. Core'slane_projectionis unchanged in this respect: it has only the output row stride. I'd like your answer on this before it lands; if you'd rather have scratch and a copy, or the table in core, it's a small change.Follow-ups
Checklist
"draft": false, a resumed prompt equals a fresh one, and concurrent equals solo (oQ2, oQ3 and oQ4e served; the uniform 6/32 tokens equal main's).Receipts: https://gist.github.com/grantoverton/668032ee274e8559aaf628ef05f55964