Skip to content

llama: published MLX affine quants (8-bit and 4-bit) read in place, beside the bf16 default (builds on #1415) - #1416

Open
BobClawblaw wants to merge 4 commits into
ashhart:mainfrom
BobClawblaw:llama-affine-pr
Open

BobClawblaw wants to merge 4 commits into
ashhart:mainfrom
BobClawblaw:llama-affine-pr

Conversation

@BobClawblaw

@BobClawblaw BobClawblaw commented Oct 26, 2026 •

Copy link
Copy Markdown
Contributor

What

The llama family serves published MLX affine quants (8-bit and 4-bit, groups of 64) of a Llama release in place, as mlx_lm writes them, beside its bf16 default (builds on #1415). Nothing is converted, by us or at load. Checked on two community quants of Nanbeige4.3-33B, pinned. They are lossy; their cost against the release is measured below, and the bf16 release stays the recipe's default.

How

  • weights.zig: Linear = union{bf16, affine}. Each of q, k, v, o, gate, up and down is read as its own {weight (u32 words), scales, biases} triple, and the embedding and head as mlx_lm quantizes them. Shapes, dtypes and byte extents are checked at load. config.zig admits a uniform quantization table (affine, groups of 64, 4 or 8 bits) and refuses anything else by name.
  • Decode windows run the core's row kernels (row_projection.zig), and prompt chunks the core's tensor-unit affine matmul (affine_mm.zig), as the other affine families do.
  • The embedding's rows come from a new family kernel, ll_embed_affine (zig/kernels/metal/llama/glue.metal), which computes MLX's dequantize bf16(bf16(scale * q) + bias).
  • Kernel contract: ll_embed_affine has gates 1-4 at 4 and 8 bits (zig/tests/contract/metal_llama.zig) and a limits entry. affine_mm at 8 bits in groups of 64, a shape the published 8-bit quants use, is registered in metal_main.zig and the registry. tf-kernel-contract --only llama: SUMMARY 23 kernels: 23 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems; affine_mm q8 g64 passes gates 1-4.
  • capabilities --json lists bf16, mlx-q4g64 and mlx-q8g64 for llama.
  • The sums scratch is allocated only for quantized checkpoints, so the bf16 path is as in llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415.
hf download pratikgx/Nanbeige4.3-33B-MLX-4bit --revision 883c8bda53b2062ff7e37f178ccf503c79674797 --local-dir nanbeige4.3-33b-4bit
hf download fcmeyer/Nanbeige4.3-33B-MLX-8bit --revision 5b206655a9974094df4258cc2381765ee6329af2 --local-dir nanbeige4.3-33b-8bit

Both state Apache-2.0, like the release.

Cost against the release (M5 Ultra; the HF reference and harness plans of #1415)

The reference is HF's bf16 run of Nanbeige/Nanbeige4.3-33B @ c1093c501cc6460427c0ac634db7223d0450c0ee over 1,500 teacher-forced positions. MLX is MLX 0.32.4 with mlx_lm 0.32.2 running the same quant files.

bf16 8-bit 4-bit
engine vs HF: mean KL, top-1 0.0015, 99.1% 0.00234, 98.7% 0.0849, 91.1%
MLX vs HF: mean KL, top-1 0.00272, 98.7% 0.0851, 90.7%
engine vs MLX: mean KL, top-1 0.00255, 98.7% 0.00166, 98.5%
greedy chats equal to MLX's (64 tokens, of 6) 2 3
greedy chats equal to HF's (64 tokens, of 6) 3 4 0 (all part within 16 tokens)
decode, 1 stream / 2 streams together 16.4 / 28-32 tok/s 27.9-28.4 / 49.2-51.5 46.0-46.7 / 76.5-79.3
cold prompt 2.1k / 8.3k / 33k / 61k tokens 1,259 / 1,313 / 1,074 / 866 tok/s 1,268 / 1,253 / 1,022 / 832 1,369 / 1,348 / 1,080 / 872
weights 61.7 GiB 32.77 17.35

The engine is as close to the release as MLX is on each quant. 16- and 7-row windows and one-row steps agree bit for bit on both. Every harness serving check passes on both: structured 18/18 (drafted == plain 12/12), tools 5/5, sampling 9/9, turns resumed 5,493 of 5,649 tokens, soak 60/60, concurrent 12/12, streaming, multi-turn, stop, thinking, max_tokens.

Receipt

  • Environment: TensorFold at this branch (main 15a0b2a plus these commits), Zig 0.17.0, one Apple M5 Ultra (256 GB unified, macOS 27, tensor units). Nanbeige/Nanbeige4.3-33B @ c1093c501cc6460427c0ac634db7223d0450c0ee, pratikgx/Nanbeige4.3-33B-MLX-4bit @ 883c8bda53b2062ff7e37f178ccf503c79674797 and fcmeyer/Nanbeige4.3-33B-MLX-8bit @ 5b206655a9974094df4258cc2381765ee6329af2, each served with --context 131072 --parallel 2 --prompt-cache-gib 8 and LLAMA_LOAD_LIMIT_GB=150, with an idle server of another model loaded beside it.
  • Exactness, tools/bench_concurrent.py --levels 1,2 --tokens 128 --temperatures 0,0.7 --alone --serial on each quant: every reply equals its request alone and with "draft": false (alone 12/12, serial 12/12, failed 0, on each).
  • Decode speed, tools/bench_openai.py --tokens 256 --reps 3, bf16 → 8-bit → 4-bit:
    • greedy: 16.4 → 28.1 → 46.2 tok/s (chat) and 18.4 → 28.1 → 46.3 (raw prompt)
    • sampled at 0.7: 16.2-16.3 → 27.8 → 45.4
    • two streams, bench_concurrent, greedy: 31.3-34.4 → 49.8-50.4 → 76.4-76.8
  • Prompt speed, tools/bench_cache.py --require-cold, a 9,000-token prompt cold: 7.18 s (8-bit) and 6.64 s (4-bit). Repeated raw-token prompts in bench_cache.py report cached_tokens 0: the family's prompt cache resumes chat turns, and the harness's turns check resumes 5,493 of 5,649 tokens in 0.24-0.28 s against 4.15-4.49 s cold. The cold first request is what the receipt asks for.
  • bf16 A/B, llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415's head against this one on the release: the 1,500- and 64-token logits are bit-identical, a greedy chat is identical, and the 64-row bench step is 62.0 / 62.3 / 61.6 ms against 62.4 / 61.7 / 61.5 ms.
  • Tests run, on these commits over main a1e6d34: zig build test, test-kernel-contract, test-golden, fuzz-cortheon, dev-tools, test-server-security, test-pull-security all exit 0; zig fmt --check is clean; the lean check finds 0 new problems (428 on main and on the head); tf-kernel-contract --only llama on the M5 Ultra: SUMMARY 23 kernels: 23 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems. Platforms: Metal on the M5 only; M1-M4 Macs, CUDA and HIP not run (no machine here; the family is Metal-only).
  • Pushed head: main then took MOSS-Image (moss_image: MOSS-Image-Base's transformer on the Metal runtime (new family, denoiser) #1457), DFlash2 and MTP fixes for other families, a GLM CUDA log message and an opt-in prompt-cache rule for CUDA families (off by default): none of it in this family's files or the paths it serves through. Rebased onto main 15a0b2a for this push, the stack's top head (llama: a smaller Llama drafting for the served one (--drafter), replies unchanged (builds on #1415, #1416) #1417's, which carries these commits) reran the build, tf-kernel-contract --only llama, test-kernel-contract, zig fmt --check and the bf16, quant and drafted bit-identity checks, and all pass.
  • Not run: the harness quality suite; CUDA, HIP, M1-M4.

@ashhart

ashhart commented Oct 27, 2026

Copy link
Copy Markdown
Owner

Nothing converts at load, which is right. But when quantization is set, weights.load requires fused qkv_proj and gate_up_proj tensors. A real MLX affine checkpoint (mlx_lm's split q/k/v and gate/up) is therefore refused with a missing-tensor error, and only llama_convert.py output loads. We'd be shipping our own lossy derived weights (4-bit: KL 0.082, top-1 91.1%) rather than serving a user's MLX checkpoint as published. Please read MLX's own split layout in place, and leave conversion to the user's tools.

Also: capabilities --json still advertises only bf16. The bf16 path gains a sums scratch buffer, so its "bit-identical" claim needs the device A/B.

A new `llama` family for the native Metal engine: dense Llama-architecture
checkpoints (heads of 128, default RoPE, no biases, an untied head) in bf16,
read in place from the shards with nothing converted. It keeps HF
modeling_llama's rounding points (a bf16 residual, LlamaRMSNorm, RoPE with
bf16 cos and sin, SiLU), runs prompt chunks on gemm_nax's tensor-unit GEMM and
decode windows on a simdgroup-a-row matvec, decode attention on sdpa_vector
lanes at absolute positions, and sampled draws on tf_sample_full. tf-llama-run
checks greedy tokens, every position's logits, and that windows and one-row
steps agree bit for bit.

Measured on an M5 Ultra against HF's bf16 run of the release: 1,500 positions
teacher-forced at mean KL 0.0015 and top-1 99.1%; decode 16.4 tokens/s a
stream and 28-32 with two; cold prompts 1.3k tokens/s at 8k tokens and 563 at
131k. docs/recipes/nanbeige-4.3-33b-metal.md has the rest.
The family's Metal kernels through gates 1-5 (zig/tests/contract/metal_llama*.zig, the roll call and sources in
contracts.zig): the glue (embedding, the norms at HF's rounding points, the residual add, RoPE with bf16 cos and sin,
the SiLU gate, the decode attention's partials and their merge, the argmax draw by its stated NaN rule), the six
row-projection forms, the four GEMM forms and the prompt attention at both served geometries, each with its float64
reference, served shapes and tiles, launched as forward.zig launches it; gate 5 reads the pipelines on this GPU.

tf-kernel-contract --only llama on the M5 Ultra: 21 kernels, 21 pass gates 1-4, 0 fail, 0 known findings, 0 limit
problems.
…out tensor units by name; the teacher-forced misses and the licence in the doc

tf-llama-run takes exact's window width as an argument and loses its
environment switches (LLAMA_LAYERS, LLAMA_WINDOW).
…eside the bf16 default

The family reads an MLX affine checkpoint as mlx_lm writes it: each projection
its own {u32 words, bf16 scales, bf16 biases} triple in groups of 64 at 4 or 8
bits, the embedding and head quantized, the norms bf16, and config.json's
uniform quantization table. Nothing is converted, by us or at load. Decode
windows run the core's row kernels, prompt chunks its tensor-unit affine
matmul; the embedding's rows come from a new family kernel, ll_embed_affine,
MLX's dequantize bf16(bf16(scale * q) + bias), registered with the contract.
affine_mm's 8-bit groups-of-64 form is registered too. capabilities lists
bf16, mlx-q4g64 and mlx-q8g64. The sums scratch is allocated only for
quantized checkpoints, so the bf16 path is as before.

Nanbeige4.3-33B on an M5 Ultra, two published quants pinned
(pratikgx/Nanbeige4.3-33B-MLX-4bit, fcmeyer/Nanbeige4.3-33B-MLX-8bit),
against HF's bf16 run of the release over 1,500 positions: 8-bit mean KL
0.00234, top-1 98.7% (MLX on the same files: 0.00272); 4-bit 0.0849, 91.1%
(MLX: 0.0851). Windows and one-row steps agree bit for bit on both; the
release's logits are bit-identical to before. The recipe doc has the tables;
the quants are lossy and the release stays the default.
@BobClawblaw

Copy link
Copy Markdown
Contributor Author

Thanks, agreed. Pushed 0500646 (one commit on #1415's head f92916d, which is rebased onto main 15a0b2a):

MLX's own layout, read in place. weights.zig now reads mlx_lm's split checkpoint as written: each of q, k, v, o, gate, up and down is its own {weight (u32 words), scales, biases} triple (Linear = union{bf16, affine}), and the embedding and head are quantized as mlx_lm leaves them. Decode windows use the core's row kernels (row_projection.zig) and prompt chunks its affine matmul (affine_mm.zig), as the other affine families do. The embedding's rows come from a new family kernel, ll_embed_affine in zig/kernels/metal/llama/glue.metal, which computes MLX's dequantize (bf16(bf16(scale * q) + bias)). tools/zig/llama_convert.py and the fused qkv/gate_up paths are gone. Nothing is converted, by us or at load.

Checked on two published community quants, pinned: pratikgx/Nanbeige4.3-33B-MLX-4bit @ 883c8bda53b2062ff7e37f178ccf503c79674797 and fcmeyer/Nanbeige4.3-33B-MLX-8bit @ 5b206655a9974094df4258cc2381765ee6329af2 (both Apache-2.0, like the release). Against the same HF bf16 reference as #1415 (1,500 teacher-forced positions), and against MLX 0.32.4 with mlx_lm 0.32.2 running the same files:

8-bit 4-bit
engine vs HF: mean KL, top-1 0.00234, 98.7% 0.0849, 91.1%
MLX vs HF: mean KL, top-1 0.00272, 98.7% 0.0851, 90.7%
engine vs MLX: mean KL, top-1 0.00255, 98.7% 0.00166, 98.5%

The engine is as close to the release as MLX is on each quant. 16- and 7-row windows and one-row steps agree bit for bit on both. Every harness serving check passes on both: structured 18/18, tools 5/5, sampling 9/9, turns, soak 60/60, concurrent 12/12, streaming, multi-turn, stop, thinking, max_tokens.

Contract. ll_embed_affine is registered (gates 1-4 in zig/tests/contract/metal_llama.zig for 4 and 8 bits, a limits entry in metal_llama_all.zig): SUMMARY 23 kernels: 23 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems. The 8-bit published quants run affine_mm at 8 bits in groups of 64, a shape it had not been registered at, so that is added to metal_main.zig and the registry too: it passes gates 1-4.

Capabilities. capabilities --json lists bf16, mlx-q4g64 and mlx-q8g64.

bf16 A/B. The sums scratch is now allocated only for quantized checkpoints, so the bf16 path allocates what it did in #1415. On the M5 Ultra, #1415's head against this one on the release: the 1,500-position logits are bit-identical, the greedy chat is identical, and the bench step is 62.0 / 62.3 / 61.6 ms against 62.4 / 61.7 / 61.5 ms (three runs each, 64-token windows).

The PR body is rewritten for the new scope (published quants, no conversion). On these commits over main a1e6d34: zig build test, test-kernel-contract, test-golden, fuzz-cortheon, dev-tools, test-server-security, test-pull-security all exit 0; zig fmt --check is clean; the lean check finds 0 new problems (428 on main and on the head); tf-kernel-contract --only llama on the M5 Ultra: SUMMARY 23 kernels: 23 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems. After the rebase, both quants' 1,500-position logits and a greedy chat are bit-identical to the measured build. Main then took MOSS-Image (#1457), DFlash2 and MTP fixes for other families, a GLM CUDA log message and an opt-in prompt-cache rule for CUDA families (off by default): none of it in this family's files or the paths it serves through. Rebased onto main 15a0b2a for this push, the stack's top head (#1417's, which carries these commits) reran the build, tf-kernel-contract --only llama, test-kernel-contract, zig fmt --check and the bf16, quant and drafted bit-identity checks, and all pass.

@BobClawblaw BobClawblaw changed the title llama: MLX affine 8-bit and 4-bit conversions of a release, served beside the bf16 default (builds on #1415) llama: published MLX affine quants (8-bit and 4-bit) read in place, beside the bf16 default (builds on #1415) Oct 28, 2026
@ashhart

ashhart commented Oct 28, 2026

Copy link
Copy Markdown
Owner

Thanks @BobClawblaw. The published split MLX layout is now read in place (zig/src/families/llama/weights.zig:70, :117, :123), nothing is converted at load (:91), and the formats are advertised (zig/src/native/metal.zig:54).

Two asks:

  1. The quants against their own references. The 4-bit and 8-bit checkpoints agree with their quantised references at 98.7% and 98.5% of teacher-forced positions, and in two and three of six greedy chats (docs/recipes/nanbeige-4.3-33b-metal.md:154-155). Please list every differing next-token choice for both, with the contenders' margins, as llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415's table does, and make the comparison fail when a difference isn't a tie. A difference that isn't a tie needs its cause found, as with llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415's row 1370.
  2. A clean receipt with one model process: the bf16 path against llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415's, then concurrent against solo and resumed against fresh for one quant.

#1415's second point (the exactness mode always exits 0) applies to this branch too.

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