Skip to content

dsv41 metal: price each round's draft asks, so a stream gets no pass when no draft pays (stacked on #1720, #2952) - #2986

Closed
gilby wants to merge 22 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/09-priced-plain
Closed

gilby wants to merge 22 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/09-priced-plain

Conversation

@gilby

@gilby gilby commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

DeepSeek-V4.1 on Metal decided whether to draft by counting streams: rounds of three or more decoding streams ran plain. Below that, the drafter's pass for the next round was launched before that round's depths were known, so the depth rule could only choose how many drafts to verify, never to skip the pass, and a stream whose drafts were not landing still paid a pass every round. With this change the lanes price each round's asks before the pass: from each stream's own running draft confidences, calibrated, it picks the asks and depths with the most expected tokens per ms, including no ask for a stream and no pass at all for the round. A round with no ask does the work of a --no-drafts round. Drafting is on at every stream count; the count switch defaults to off and TF_DSV41_DRAFT_PLAIN_STREAMS still works when set.

How it decides: a stream's prediction is the mean of the confidences its own passes reported, by draft position, calibrated with the shared per-position calibration. The planner scans the largest chain gains over all streams against the measured cost of the rows plus the pass. A fresh stream asks. A stream that went plain comes back through a probe: when the modelled time of rounds with a plain stream reaches a hundred passes' worth, every stale stream asks together in one pass, so probing costs at most 1% of plain-round time. After the pass the same scan prices the depths with the pass paid, and zero is allowed.

Scope: nothing under zig/src/core changes. The setting (TF_DSV41_DRAFT_PRICED) defaults on only in the Metal lanes; CUDA keeps it off unless set. With it off the choices are the old ones: a test hashes 1,800 depth choices (joint modes 0 to 2, five confidences, 1 to 4 streams, two cost tables) against a hash taken on the base commit.

Checks on this head

  • Head commit: c5d5687f6
  • Lean checker, python3 tools/zig/lean_check.py, on the base and on this head: 345 and 345, 0 new. tools/pr_check.py: 0 errors.
  • Tests run, with the command and pass, fail and skip counts (M5 Max, synthetic weights): test-dsv41-draft 160/160 (17 new); test-dsv41-serve-host 14 passed, 5 skipped; test-shared-lane-regressions-host 360 passed, 1 skipped; test-dsv41-metal-all 43/43, with a new device test of four streams with the setting on (replies equal serial; 63 rounds priced, 6 with a pass, 57 without, 2 probes). zig build test: 12,649 passed, 107 skipped, 1 failed (dashboard_test, which fails the same way on main).
  • New tests, each red with the old default: weak drafts at 3 and 4 streams run plain rounds with no pass and no ingest and never price below plain; strong drafts at 3 and 4 streams draft and beat plain; one confident stream beside three unconfident ones drafts alone; a stream that went plain drafts again after a probe; replies equal one-token decode in every case, greedy and sampled, together and staggered. A property test over 3,000 random cases (tables with and without a step at row 17) finds no unforced plan priced below all-plain and matches brute force up to three streams.
  • CI: not run yet (draft).
  • Not run, and why: nothing on the release weights yet; the receipt below follows on both chips.

Receipt

  • Main against branch: to run on an M3 Ultra 512 GB and an M5 Ultra 512 GB with deepseek-ai/DeepSeek-V4.1-Flash-1017, fresh servers, --prompt-cache-gib 0: tools/bench_concurrent.py --levels 1,2,3,4 --alone --serial --temperatures 0,1.0 --tokens 256 --reps 3 and tools/bench_openai.py, arms --no-drafts, this default, this with TF_DSV41_DRAFT_PRICED=0 (the old behaviour). The bar: the default within 2% of --no-drafts in every cell, no slower than the old behaviour anywhere, and ahead of plain on code at 3 and 4 streams.
  • What the rule's arithmetic predicts, with an M3 Ultra's measured row costs and a 12 ms pass, default over --no-drafts: at confidence 0.5 the old rule gave 0.88 and 0.90 at one and two streams and this gives 1.00; at 0.9 with three and four streams the count switch gave 1.00 and this gives 1.18 and 1.08. These are the model's numbers, not measurements.
  • Boxes it reaches: Apple Silicon Macs serving DeepSeek-V4.1 natively.
  • New switch? Default, accepted values, profile row: TF_DSV41_DRAFT_PRICED (0, 1, off, on, false, true), default on in the Metal lanes and off elsewhere, with its rule row in platform/environment.zig and its docs/environment.md row; a malformed value stops a Metal start by name. No docs/profiles.md row: that file lists the CUDA profile registry's settings and none of the depth-policy switches. The pass is priced per ask from the table's timed ingest, and a stream catching up after plain rounds is charged a second one. TF_DSV41_DRAFT_PLAIN_STREAMS now defaults to 0 on Metal when it is on.
  • Environment: this head on dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's a43ad7b98 and DeepSeek-V4.1 on Metal: opt-in draft cost table timed at start, default pricing unchanged (stacked on #1720) #2952, Zig 0.17.0, Xcode 27.2 Metal toolchain, M5 Max for the tests above.
  • Paths and platforms the change reaches, which I ran and which I could not: Metal only. Ran the synthetic-weight device suites on an M5 Max. Not run: M3, M5 Ultra, the release weights.
  • Exactness: pricing only; verification is exact, and every new test compares replies with one-token decode. bench_concurrent --alone --serial on the release weights to follow.
  • Decode speed before and after: to follow.
  • Prompt speed before and after: n/a, the prompt path is not touched.
  • Licence of any new weights or drafter: n/a.

Checklist

  • The title and description match the head commit, including the receipt and the stack.
  • 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 in the diff or its commits. No AI attribution lines.
  • 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 (synthetic weights; release weights to follow).
  • No precision traded for speed.
  • Decode and prompt processing are no slower than main at one stream and the widest admitted (receipt to follow).

gilby added 22 commits November 11, 2026 22:56
…over Metal

target.zig is the lanes' verify target: a slot's prompt, its windows of rows for several slots in one launch,
keeps and rewinds, and kept prompt states (the prompt cache) whose resumed rows the reply reports as cached_tokens.
boot.zig loads a release checkpoint folder and entry.zig serves it as tensorfold-dsv41 (dsv41-metal-serve).
Tests serve the tiny model through LaneHost: concurrent equals solo equals serial, drafted equals draft:false,
cache hits equal fresh runs, seeds and cancels.
…for the next

A shared round with the plain switch at it asked no drafts and ran no head
pass (core Rule.plain_streams, threaded from the family's resolved setting:
0 on CUDA, so its rounds are unchanged), and the family's speculation no
longer launches a pass for a round that wide (armSpec/speculate plainRound).
The depth rule priced Metal rounds with the CUDA-shaped defaults. Metal now
times dry windows of 1 to 16 rows, the second launch from row 17 and a DSpark
pass with its ingest at load, on a time budget, through the lane core's
calibration path. TF_DSV41_CALIB keeps its meaning; measure is the Metal
default, and TF_DSV41_CALIB_BUDGET_MS bounds the repeats. The prompt cache is
not fed and every slot is left as new.
…t table

The first measurement took 12.7 s and priced the pass at 62 ms. It now times
rows 1, 2, 4, 8 and 16, the 4 + 4 window, rows 17 and 32 and the pass once
each after an untimed window and pass, repeats the passes only while the
budget lasts, and interpolates the rows between. The startup line reports the
ingest share of the pass and the timed runs.
…when no draft pays

Drafting stays on at every stream count. Before a round's pass, the lanes price the next round from each stream's
running confidences (calibrated, the last passes blended): the asks and drafts with the most expected tokens a ms,
with no ask and no pass among the choices. A round where no draft pays runs no pass and no drafter ingest, as
--no-drafts would; a stream asks again when its prediction pays, when it is new, or at a probe bounded to 1% of the
plain rounds' modelled time. After the pass the depths come from the same price, k = 0 included.

TF_DSV41_DRAFT_PRICED selects it; the Metal lanes turn it on, and with it the count switch defaults off
(TF_DSV41_DRAFT_PLAIN_STREAMS still sets it). Off, every choice is the earlier rule's, checked against a hash of
its choices taken before the setting existed. Pricing only: tokens never change.
… table by device and weights

The Metal measurement now checks every TF_DSV41_CALIB* setting before it
times anything, so a malformed value stops the start naming the setting, its
text and the accepted forms; only a measurement that fails falls back to the
defaults. The settings have rows in platform/environment.zig and
docs/environment.md. A stored table carries the device and checkpoint
identity, so another chip or other weights never read it. dropWindow's
comment says it is for calibration, with wipe after.
…very window a lone stream can make

The measurement prefilled the calibration text into each slot twice and ran
thirty one-row windows to grow a continuation, which cost about nine seconds
on the release model outside the timed runs. It now prefills one short prompt
in each slot it needs once, takes the window tokens from the text itself,
and clears only the slots it used. Rows 1 to 6 are timed rather than
interpolated, since a lone stream's drafts make exactly those windows.
…efuse a malformed value at a Metal start

Both settings have rows in platform/environment.zig and docs/environment.md, so a Metal
start stops naming the setting, its text and the accepted forms; on CUDA the depth
reader refuses them when the engine opens, as before. The ledger's shared-drafts cell
for DeepSeek-V4.1 points at ashhart#2986.
The start-up measurement already timed the drafter's ingest inside its one-ask pass;
the table now keeps it (stored with the table, read back to the microsecond), and the
priced rule adds it for each further ask and once more for a stream that catches up
after plain rounds. A table without it prices every pass flat, as before.
@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Closing this PR as a failed attempt. The priced draft rounds missed their bar on an M5 Ultra twice, with the version pushed here and with a reworked version that stayed local. The stream-count switch (TF_DSV41_DRAFT_PLAIN_STREAMS, plain rounds at 3 or more streams) therefore stays as the named stopgap in #1720, with its removal condition unchanged.

The bar set in advance: the default within 2% of --no-drafts in every cell, and ahead where drafts pay.

What was tried. The pushed version (head c5d5687f6) priced each round's draft asks before the pass: from each stream's running confidences it chose the asks and drafts with the most expected tokens a millisecond, with no ask and no pass among the choices, so a round where no draft pays runs no pass and no drafter ingest, as --no-drafts would; the pass was priced with the timed ingest for each further ask. TF_DSV41_DRAFT_PRICED selected it, and the Metal lanes turned it on with the count switch off by default.

The reworked version (local only, 52a622812 merged with the cost-table updates of #2952 as 1e5f35bbc; not pushed and not going to be) priced the asks by what recent passes were worth at the going rate: each slot kept the expected tokens and the rows' milliseconds of its passes' verified drafts, the ask set was the one worth most at the going rate once its pass was paid, a lone stream's depth stayed the earlier rule's, and a shared round had to clear the plain rate by 10%. The start-up measurement also timed plain rounds of 2 to 4 one-row slots and passes over 2 and 4 asks to price shared rounds.

Both priced from the cost table timed at start, which is itself not adopted as the default: on an M5 Ultra it made three chat cells 2 to 4% slower and it is an opt-in in #2952.

Result of the reworked version, M5 Ultra, 512 GB, deepseek-ai/DeepSeek-V4.1-Flash-1017, bench_concurrent and bench_openai at 256 tokens, greedy and sampled, token hashes equal in every cell. A is base a43ad7b98 with --no-drafts, "priced" is the tree at its default, "earlier rule" is the same tree with TF_DSV41_DRAFT_PRICED=0 (the plain switch at 3 streams). Run order A, priced, earlier rule, earlier rule, priced, A; the last two columns are mean over mean of the two runs of each arm. Priced against --no-drafts is under 0.98 in five cells: chat greedy 3 streams 0.915, code sampled 4 streams 0.946, chat greedy 4 streams 0.963, code greedy 4 streams 0.978 and chat sampled 4 streams 0.979. Priced against the earlier rule is under 0.98 in 12 of the 20 cells, nine of them at 1 and 2 streams (0.913 to 0.949).

1 and 2 streams, bench_concurrent (tok/s):

streams T load base --no-drafts 1 priced 1 earlier rule 1 earlier rule 2 priced 2 base --no-drafts 2 priced / --no-drafts priced / earlier rule
1 1 chat 45.9 53.1 56.9 57.0 53.8 45.9 1.164 0.939
2 1 chat 66.5 66.9 71.8 71.8 66.7 66.6 1.004 0.930
1 1 code 45.9 75.1 77.8 78.1 72.9 45.9 1.612 0.949
2 1 code 67.0 66.4 67.0 66.9 67.0 67.1 0.995 0.996
1 0 chat 46.3 55.0 59.6 59.5 55.8 46.3 1.197 0.930
2 0 chat 73.3 73.3 77.1 77.6 72.8 73.4 0.996 0.944
1 0 code 46.4 67.1 72.9 71.9 68.0 46.3 1.457 0.933
2 0 code 74.1 91.3 93.4 90.8 93.0 74.1 1.244 1.001

3 and 4 streams, bench_concurrent (tok/s):

streams T load base --no-drafts 1 priced 1 earlier rule 1 earlier rule 2 priced 2 base --no-drafts 2 priced / --no-drafts priced / earlier rule
3 1 chat 79.0 78.7 78.9 78.8 77.9 78.9 0.992 0.993
4 1 chat 90.9 89.4 90.8 90.8 88.5 90.9 0.979 0.980
3 1 code 79.2 78.1 78.1 78.6 79.2 79.1 0.994 1.004
4 1 code 90.5 85.6 84.7 85.2 85.6 90.5 0.946 1.008
3 0 chat 99.5 93.1 99.2 99.3 88.9 99.4 0.915 0.917
4 0 chat 119.3 114.8 119.1 119.1 114.7 119.1 0.963 0.963
3 0 code 101.3 99.7 101.0 101.0 98.9 101.1 0.981 0.983
4 0 code 122.1 119.3 121.8 121.8 119.5 122.0 0.978 0.980

bench_openai (decode tok/s, median of 3):

prompt T base --no-drafts 1 priced 1 earlier rule 1 earlier rule 2 priced 2 base --no-drafts 2 priced / --no-drafts priced / earlier rule
fibonacci-raw 1 44.8 65.0 66.7 65.9 67.4 44.9 1.476 0.998
fibonacci-raw 0 46.4 65.8 72.8 71.9 66.3 46.4 1.425 0.913
gpu-chat-no-think 1 44.5 51.1 54.5 54.8 50.4 44.6 1.140 0.929
gpu-chat-no-think 0 46.3 55.6 59.5 58.6 56.1 46.3 1.207 0.947

At 1 stream the priced rounds are 14 to 61% ahead of --no-drafts, where drafts pay, but 5 to 9% behind the earlier rule in nine of the twelve 1 and 2 stream cells (0.913 to 0.949), level in the other three (code sampled 2 streams 0.996, code greedy 2 streams 1.001, bench_openai fibonacci-raw sampled 0.998). At 3 and 4 streams it is behind --no-drafts in five cells, by up to 8.5%.

The pushed version, from an earlier series on the same machine and method (head c5d5687f6, 256 tokens, same A, priced, earlier rule, earlier rule, priced, A order), headline cells; all 20 cells are in tables-recomputed.txt:

streams T load base --no-drafts 1 priced 1 earlier rule 1 earlier rule 2 priced 2 base --no-drafts 2 priced / --no-drafts priced / earlier rule
4 1 chat 90.9 87.6 90.8 90.8 87.5 91.0 0.963 0.964
1 1 code 45.9 72.9 77.8 77.6 72.9 46.0 1.587 0.938
3 1 code 79.2 77.5 78.7 78.7 76.9 79.2 0.975 0.981
4 1 code 90.5 84.4 85.1 84.8 83.5 90.5 0.928 0.988
1 0 chat 46.2 49.9 58.2 58.2 54.4 46.3 1.128 0.896
3 0 chat 99.3 87.6 99.3 99.2 93.5 99.3 0.912 0.912
4 0 chat 119.1 113.4 119.0 118.9 108.9 119.1 0.933 0.934
4 0 code 121.9 115.4 121.8 121.8 119.8 122.0 0.964 0.966

Against --no-drafts it was under 0.98 in six cells (chat greedy 3 streams 0.912, chat greedy 4 streams 0.933, code sampled 4 streams 0.928, chat sampled 4 streams 0.963, code greedy 4 streams 0.964, code sampled 3 streams 0.975); against the earlier rule it was under 0.98 in 10 of the 16 bench_concurrent cells and in all four bench_openai cells (0.879 to 0.950), the worst being bench_openai chat greedy at 0.879 and bench_concurrent chat greedy 1 stream at 0.896.

So the bar is not met at 3 and 4 streams in either version, and both versions lose to the current rule at 1 and 2 streams. The cells were not changed after seeing the result.

Method as run: each cell is a fresh server, arms in the order A, priced, earlier rule, earlier rule, priced, A, a thermal-pressure check (powermetrics) at the start and end of every cell, and a cell that was not Nominal at an end discarded and re-run after a pause. In the reworked series the 3 and 4 stream bench_concurrent cells of earlier rule 2 (once) and priced 2 (twice) were discarded and re-run; run4-discards.log also lists three discards of an earlier attempt at this series that was abandoned and redone as the series reported here. One kept cell, the bench_openai base run 1 of the reworked series, started with a macOS background process (mediaanalysisd) at about 100% CPU, at Nominal pressure. Controls (base, --no-drafts, 1 stream, greedy) read 46.6 to 46.9 tok/s against a gate of at least 42. The pushed version's series has no discards. M3 Ultra: not run.

Ledger row (docs/optimization-ledger.md, DeepSeek-V4.1 shared-drafts cell), failed: "priced rounds (each round's asks priced, no pass when no draft pays): failed, two versions. M5 Ultra: 3 and 4 streams 0.915 to 0.979 of --no-drafts in five cells (worst chat greedy 3 streams 0.915), 0.913 to 0.949 of the current rule in nine of twelve 1 and 2 stream cells; tokens equal; not adopted; the plain-rounds switch at 3 streams stays as the stopgap".

Raw JSON, server logs and driver scripts: https://gist.github.com/gilby/9dc31f647b945d8522b255f1c88348aa

@gilby gilby closed this Nov 12, 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.

1 participant