Repository navigation
llama: a smaller Llama drafting for the served one (--drafter), replies unchanged (builds on #1415) - #1417
llama: a smaller Llama drafting for the served one (--drafter), replies unchanged (builds on #1415)#1417BobClawblaw wants to merge 14 commits into
Conversation
|
Drafted equals plain per the receipts, and there's no lane-core change or new kernel. That's good. Two things:
|
f7c7562 to
5d4a3af
Compare
|
Thanks. Pushed 5d4a3af (one commit, on #1416's new head):
Shared rounds. Both of your options, in turn:
One correction to the old receipt: the 87-117 tok/s single-stream figures from Also: the 3B is admitted as a drafter only ( On these commits over main a1e6d34: |
|
Thanks @BobClawblaw. The drafter's default now reads the published 4-bit layout, or bf16 with an explicit width of 0 ( Three asks:
|
5d4a3af to
b652456
Compare
|
Thanks. Pushed b652456 (one commit on #1416's new head): 1. One memory budget. The backend's admission now runs on one ledger ( 2. Shared rounds are priced, and drafting stays on.
3. Warm-up failure. The harness's quick plan on the drafted The doc's drafter section is rewritten for the budget and the priced rounds. CI on these commits over main 03d08f1 (the pushed heads are rebased onto d0c9dc9, which adds only a CI workflow time limit): on #1417's head, which carries all three, |
|
Thanks @BobClawblaw. On One ordering problem is left. The checker and float64 requests on #1415 and #1416 also apply to this stack. |
b652456 to
50fe245
Compare
|
Thanks, that ordering was wrong. Pushed 50fe245. This PR now builds on #1415 alone: we've withdrawn #1416 (the quants; the reasons are there), so the drafter is served from its bf16 release as published. Admission before allocation.
The host tests drive the same function with a fake store, drafter slot and allocation recorder that tracks the most the ledger and the kept states hold at once:
The drafter without #1416.
CI on these commits over main ea01ae1: |
|
Thanks @BobClawblaw. The admission order now works: the stream's peak is checked and the kept prompt states are trimmed before anything is allocated, a protected restore entry stays or is dropped safely, and the recorder tests cover the 8 GiB shape and a failed One thing left, from our first review. With the quantized drafter withdrawn, A small one while you're there: in Also, if you can, please author future commits with your id-prefixed noreply address ( Once the default is sorted, we'll run the device checks on our side and land this right after #1415. |
|
Thanks @ashhart. Done in c3ee3db:
Checked on the M5 Ultra on this head, with the served command carrying no
|
c3ee3db to
80bf371
Compare
80bf371 to
09cb746
Compare
|
Rebased onto main e5b50c8 with #1415, which it builds on; the head is now 09cb746. This branch's own conflict was the Checks on the M5 Ultra, run on the same commits rebased onto main fdd8b6d, one main earlier, against the previous head (80bf371):
|
09cb746 to
08379d5
Compare
|
Rebased onto main 6f9dfa2; the head is now 08379d5, carrying #1415's seven commits plus this PR's five. One change beyond the rebase, in the last commit: main's new serving line names a server's draft sources (3dfd41f). With Checks on the M5 Ultra, on this head over main 6f9dfa2:
On the same commits one main earlier (d377fcc):
|
|
On the drafter: sampled at 0.7 it's 35.5 and 29.9 tok/s against about 39.7 without, and level on ordinary greedy chats. Our rule is that |
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).
…hecks a planted row is reported
… near-tie, a float64 reference, the doc's misses and a one-process receipt
…ty, on either side, before comparing
…s rounding points (--bf16-points)
… as planted metal_llama_controls.zig edits the source each kernel is built from in one place, as the loader builds it, and the set registers the kernel with that edit beside the real one: the embed reads its own row, the norm divides by the threads, the add subtracts, the add-norm drops its residual write, the SiLU swaps gate and up, the rope rotates every row at the window's first position, the argmax never picks a NaN, the decode never rescales, the merge drops each block's scale, the row projection drops the columns past the last whole 256, gemm_00 reads its weights untransposed and the prompt attention lets a row see the next key. An edit that does not match the source exactly once errors.
…rafts verified by the model's windows Nanbeige4.1-3B shares Nanbeige4.3-33B's tokenizer, so the family admits it as a drafter only (config.zig's qualified list has a role; serving the 3B on its own is refused by name). drafter.zig is the lane host's priced copy proposer: one cache a stream synced to the context by longest common prefix (made and grown in 4,096-row steps), greedy steps chained on the GPU through token slots, and a depth from measured rates (the stream's acceptance, a chained step's time, a line fitted to the round's time by its window's rows). While more than one stream holds a drafter cache it proposes nothing, so shared rounds decode as undrafted. The model's window keeps the drafts its own draws agree with, so replies keep its bits. --drafter-bits 4 (the default) serves an MLX 4-bit drafter as published (mlx_lm's layout, read as the served model's quants are), 0 its bf16 release. Model.loadOn puts the drafter on the served model's queue and residency set; budget.longestWith counts its caches. M5 Ultra, the bf16 33B with mlx-community/Nanbeige4.1-3B-4bit: drafted greedy tokens equal plain greedy tokens on nine prompts in tf-llama-run's new draft mode (the bf16 drafter on two of them too), 23.2-46.4 tok/s against 16.0-16.3; served with --parallel 1, one stream 99.5-116.8 tok/s on the harness's ruler (16.4 undrafted) and every quick-plan check passes. With --parallel 2 two streams decode together at the undrafted rate less the lane core's copy drafts (code 31.5 against 34.4), so the recipe doc recommends the drafter with --parallel 1.
…ound priced and drafted, a failed warm-up frees the drafter
… leaves before allocating, tested with a recorder
…after-bits 4 and quantized drafters refused by name
… not read for Llama, as for Gemma 4
The Llama drafter has one width, its bf16 release as published, so the shared --drafter-bits default (4, Qwen's
DFlash width) no longer refuses `--drafter <dir>` at startup. A test under test-native shows the default options
reach the drafter. The drafter model is held by the errdefer before its refusal message is built, so a failed
allocation there frees it. The doc's drafted command drops --drafter-bits 0. The serving line names the
drafter among the draft sources ("Llama drafter (<dir>) + copies"), set where it is registered as the copy proposer.
…er where it pays, a plain round otherwise With --drafter the lane host hands a request the Llama drafter alone, so the copy proposals it would otherwise make were lost, and a stream alone always drafted at least one token, so a drafter that lands little (sampled replies) cost time. Each request now keeps the host's copy proposer (lanes.SuffixLookup at the host's min_match) and asks it first, behind the lane core's own gate (two tokens, enter_match ids matched); its rounds are judged as copies. pricing.depth prices a plain round beside the drafting depths and returns 0 when it lands more a millisecond: a plain round is priced from the same timed line of drafted rounds. A new request starts from the recent requests' acceptance; while it stands aside a round drafts after 64, then twice as many each time (up to 1,024), so its acceptance stays measured at little cost.
08379d5 to
33a44db
Compare
|
Thanks, reproduced: on the previous head, sampled replies on Nanbeige4.3-33B-INT4 with the 3B drafter ran at about 30 tok/s against 46.6 without. The new commit (
Rebased onto main 7b65d60; the head is now 33a44db. Speed, on an M5 Ultra: one stream, 16k context, thinking off, 384 tokens a reply, decode tok/s after the first token, the mean of two passes, each from a fresh server. Measured on the same commits over main 6f9dfa2. The rebases since change only the refusal line beside main's new Nanbeige4.3-33B-INT4 with the bf16 Nanbeige4.1-3B drafter:
Nanbeige4.3-33B bf16 with the same drafter, where drafting pays:
Two limits, so you can judge them:
Checked on the pushed head: |
|
Thanks @BobClawblaw, commit 14 reads like exactly what we asked for. Could you post |
|
Here are Natural stops (no
The drafter is behind in 0 of the 24 window pairs; the closest is a tie at 19.8 (0.7, 2 streams). Per-window spread at 0.7 with 1 stream: 14.3-44.5 against 12.9-37.9. Without a drafter the server still drafts from the request's own copies, which is why the raw-completion prompts reach 60 tok/s greedy on both sides.
These overstate the drafter. Replies are unchanged. Every reply equals its request run alone (72/72) and On this head, |
|
Thanks @BobClawblaw, this answers the 1 Nov ask cell for cell. On head 33a44db, natural stops, medians over the six windows:
None of the 24 window pairs is slower with the drafter (the closest is a tie at 19.8). Replies match with and without it, and without One question, not blocking: the 1 Nov INT4 tables show the first request on a new server 2.5% slower with the drafter (45.6 against 46.8). This PR refuses quantized Llama configs and drafters ( Next we run the same matrix on our own M5, alternating the two servers. Then we gate it. |
|
Thanks @ashhart. The 1 Nov INT4 numbers came from #1782's branch: its INT4 commits on top of this PR's commits, commit 14's stand-aside included, over main 6f9dfa2. This PR alone refuses quantized configs, so INT4 runs only on #1782's stack. Both branches were then rebased onto 7b65d60 (#1782's head became 97c6e12); the rebase changed only the refusal line beside main's new The cold first request can no longer cost anything. Since 70817a8, #1782 refuses |
What
--drafterfor thellamafamily: a smaller Llama on the same tokenizer proposes a stream's next tokens, and the served model verifies them in its windows, so replies keep the model's bits (builds on #1415). It was brought up with Nanbeige/Nanbeige4.1-3B drafting for Nanbeige4.3-33B, both served from their bf16 releases as published. The two share the tokenizer exactly (166,100 ids, merges, added tokens, bos and eos); only theirvocab_sizes pad differently. The 3B is admitted as a drafter only: serving it on its own is refused by name.--drafter-bits 4and quantized drafter checkpoints are refused by name too: the drafter is the bf16 release, nothing prepared at load.How
zig/src/families/llama/drafter.zigis the lane host's priced copy proposer (LaneHost.proposers). A round asks it for up to 15 tokens after the stream's context. The window verifies them row by row and keeps the prefix the model's own draws agree with, as it keeps any copy draft. There is no lane-core change and no new kernel: the drafter runs the family's own forward on the served model's queue (Model.loadOn) and in its residency set.budget.Ledger). A stream's admission (budget.admitStream) checks its peak first (its caches, its drafter cache's growth and the old state's copy), kept drafter caches shedding first; trims the kept prompt states to what that peak leaves, freeing a restore entry that can't stay beside it; and only then allocates. A freed drafter cache keeps its tokens for the next request sharing its prefix, counted, and gives way first.pricing.depthShared), nothing only where drafting costs the round more than it lands.Drafter.startowns the model from the call and frees the drafter if its warm-up fails; the host frees the drafter before the backend whose ledger it gives back to.tf-llama-run <model> <tokens> draft <drafter>,<n>decodes greedily through the drafter's windows and prints the tokens with the rounds and drafts kept.Measured (M5 Ultra, the bf16 33B and the bf16 3B, 28 Oct 2026, one model process on the Mac)
Identity. On eight chat prompts (128 tokens, thinking on and off, English and Chinese) and after a 1,500-token prompt (256 tokens), the drafted greedy tokens equal the plain greedy tokens exactly: 21.1-40.7 tok/s drafted against 15.8-16.1 plain, 1.73-4.54 tokens landing a round, a chained bf16 step 8.5-8.9 ms. The recipe doc has each prompt.
Two streams. On fresh servers (
--context 32768 --parallel 2 --prompt-cache-gib 8), eight new prompts sent two at a time (greedy, 256 tokens, thinking off) decode together at 30.1-40.9 tok/s (36.1 on average) with--drafter, against 26.6-30.1 (29.0) without; every reply's token ids are equal. Streams joining, leaving, queued, refused (HTTP 400) and cancelled each return the ids of their plain solo run, a reply alone after a cancel is unchanged, and shared rounds keep drafting (199 drafts offered, 125 kept).Receipt
--drafterNanbeige/Nanbeige4.1-3B @ 9c4555c37921c0982af8fffeee1e0a6e6b953c4f (--drafter-bits 0),LLAMA_LOAD_LIMIT_GB=150.zig build testandtest-kernel-contractexit 0 on llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415's head; on llama: a smaller Llama drafting for the served one (--drafter), replies unchanged (builds on #1415) #1417's head, which carries llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415,zig build test,test-kernel-contract,test-golden,fuzz-cortheon,dev-tools,test-server-securityandtest-pull-securityall exit 0;zig fmt --checkis clean, the lean check finds 0 new problems (426 on main and on each head),llama_teacher_judge_test.pypasses, andtf-kernel-contract --only llamaon the M5 Ultra givesSUMMARY 21 kernels: 21 pass gates 1-4, 0 fail, 0 known findings; 0 controls: 0 red as planted, 0 missed; 0 limit problems. Rebased onto main 01e25d3 for this push (one conflict, indocs/kernel-contract.md: main's row-split seam paragraph and this family's, both kept; the family's own files are unchanged): on llama: a smaller Llama drafting for the served one (--drafter), replies unchanged (builds on #1415) #1417's headzig build test,test-kernel-contract,test-golden,fuzz-cortheon,dev-tools,check-release-pathsand the security tests exit 0, fmt is clean and lean finds 0 new; on llama: Nanbeige4.3-33B on Metal, the bf16 release served as published, every kernel in the contract #1415's headzig build testandtest-kernel-contractexit 0. Five greedy chats (1,102 tokens) are byte-identical between the previous head and this one, both plain and with--drafter.Checks
zig build testcovers the depth rule and its shared-round pricing, the cost fit, the acceptance history, the drafter role, the roll call, the one budget (two drafted 240,000-token requests that each fit a 100 GiB budget alone are refused together), admission's order with an allocation recorder (kept prompt states trimmed before anything is made, a failed growth giving every byte back) and a failed warm-up freeing the drafter. Without--drafter, the served path is #1415's: the 1,500-position logits are bit-identical to its judged run.