Skip to content

dsv41 metal: prompts in pieces between the other streams' rounds (stacked on #1720) - #2950

Draft
gilby wants to merge 20 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/07-chunked-prompts
Draft

gilby wants to merge 20 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/07-chunked-prompts

Conversation

@gilby

@gilby gilby commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

On Metal, a DeepSeek-V4.1 prompt ran whole in one lane-host call, so a long prompt held every other stream until it finished and TENSORFOLD_PREFILL_ORDER=srpt and the prompt policies had nothing to act on. With this change the Metal target implements begin / piece / finish, and the lanes offer a prompt step built on them, so a prompt can run a chunk at a time between the other streams' rounds. Nothing changes unless a setting asks for it: with TF_DSV41_PREFILL_PIECES, the SRPT order and the prompt policy, share and budget all unset, prompts run whole exactly as before.

How it stays exact:

  • A default step is the prompt chunk (4,096 rows), cut from the begun position, and finish runs the last rows. The chunk boundaries, inputs and the host work prepared ahead are the ones the whole-prompt loop uses, so the state is equal by construction.
  • Smaller steps (a policy cap) rely on the prompt kernels' chunk invariance, which the device test checks at 1, 7, 100 and 300 rows against a 64-row chunk.
  • The decoder cone runs once, in finish. A mid-prompt state is never kept; a kept entry is a whole prompt's state, and a cache hit at begin restores it before the pieces, as prefill does.
  • A prompt released, cancelled or failed mid-way keeps nothing and frees its slot.
  • No kernel is new or changed. CUDA never selects the step, so its vtable is unchanged.

Checks on this head

  • Head commit: 49a92da01
  • 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, 0 warnings).
  • Tests run, with the command and pass, fail and skip counts (M5 Max, synthetic weights): zig build test-dsv41-metal-all: 9/9 steps, 47/47 tests. zig build test: the share test's new blank case failed before b8c0cb299 (share 1 where 0.75 is expected) and passes after; the only other failure in the full suite is dashboard_test, which fails the same way on this base and was fixed on main by tests: the dashboard page's test runs on every OS, and expects only tiles the page has #2946.
  • New tests, each red without the change: pieces of 1, 7, 64, 100 and 300 rows equal the whole prompt in every KV, taps and tail byte, the first choice and 8 greedy tokens, with another slot decoding between pieces and its tokens equal to its run alone; a refused piece and a release mid-prompt free the slot and keep nothing; a kept prompt resumed in pieces (fresh, partial hit, whole hit) equals fresh; two prompts' pieces taking turns each equal whole; served in steps, concurrent equals solo equals serial and drafted equals "draft": false; a short request during a 1,100-token prompt gets its first token first in steps and after it with whole prompts; a cancel mid-prompt keeps no prefix.
  • CI: not run: the branch is still on dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's pre-landing head and conflicts with main (CHANGELOG.md), and per the order in DeepSeek-V4.1 on Metal: decode and prompt speed on M3 Ultra and M5 Ultra, token-exact (gate and order for the drafts) #2984 it is rebased onto the PR before it at landing.
  • Not run, and why: the M5 Ultra lone cold 32K prompt with TF_DSV41_PREFILL_PIECES=1 with base on both sides, and a repeat of the M3 Ultra fill arm with nothing set (both requested on this PR, run list in the comment); CUDA beyond compiling (the DeepSeek CUDA engine shares draft/lanes.zig).

Receipt

  • Main against branch: with nothing set this head is level with the stack tip on both chips: bench_concurrent 1 to 4 streams 0.997 to 1.002 on the M5 Ultra and 0.998 to 1.002 on the M3 Ultra (all 16 cells within noise by the DeepSeek-V4.1 on Metal: decode and prompt speed on M3 Ultra and M5 Ultra, token-exact (gate and order for the drafts) #2984 rule), bench_openai 0.997 to 1.001. Opt-in: a 64-token reply beside a 131,072-token cold prompt finishes in 6.3 to 13.0 s with TF_DSV41_PREFILL_PIECES=1 against 130 to 142 s on base (M5 Ultra) and 7.6 to 13.8 s against 209 to 256 s (M3 Ultra). Receipts: https://gist.github.com/gilby/d41622e4da2c9b160eaa0ad8c2f36ee3 (M5 Ultra) and https://gist.github.com/gilby/d5d940706755000180b1855a8a82b5a3 (M3 Ultra), measured at fdcc22211; b8c0cb299 changes only the blank-variable case, which no measured arm set.
  • Boxes it reaches: Apple Silicon Macs serving DeepSeek-V4.1 natively, only with a step setting on.
  • New switch? Default, accepted values, profile row: no new name. TF_DSV41_PREFILL_PIECES=1 now turns steps on for Metal (on CUDA it still selects the round planner); TENSORFOLD_PREFILL_ORDER=srpt and the prompt policy, share and budget settings turn them on too. Default off.
  • Environment: measured at fdcc22211 on dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's a43ad7b98; deepseek-ai/DeepSeek-V4.1-Flash-1017 release weights at revision 1bae0e1088993b9b82de08f59de5be5a34e42170; M5 Ultra 512 GB and M3 Ultra 512 GB; Zig 0.17.0; tests on an M5 Max.
  • 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, the release weights, CUDA (compile only).
  • Exactness: token_sha256 equal in every cell of both chips' default-path series, and in every fill arm (fcfs and fcfs with TENSORFOLD_PREFILL_TOKENS=1000 included) the long answer's hash equals the unset run's; bench_fill reports every reply equal to its run alone.
  • Main against this branch, same machine and checkpoint, posted before asking for a run on our machines: posted in the receipt comments on this PR (links above).
  • Decode speed before and after: bench_openai 0.997 to 0.999 (M5 Ultra) and 0.998 to 1.001 (M3 Ultra), nothing set.
  • Prompt speed before and after: lone cold 32,768-token request, nothing set: M3 Ultra 43.13 s against 43.19 / 43.18 s; M5 Ultra 35.35 s against 35.30 s. With TF_DSV41_PREFILL_PIECES=1: M3 Ultra 0.998 of base; M5 Ultra 1.021 on a single run, to be repeated with base on both sides.
  • 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, and on the release weights in the receipts).
  • No precision traded for speed.
  • Decode and prompt processing are no slower than main at one stream and the widest admitted; with --drafter, no slower than without it, greedy and sampled

gilby added 15 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).
…in / piece / finish)

The Metal target implements the lane core's chunked hooks over its whole-prompt path:
`begin` clears the slot or restores a kept state, each `piece` runs encoder chunks from
the slot's tail position, and `finish` runs the rows the pieces left, the decoder's cone
once over the prompt's end, and the choice and keep exactly as `prefill` does. An
`encode` that stops short of the prompt's end prepares the next call's first chunk
(keyed by the slot's tail and rows), so a lone prompt in pieces of the chunk size runs
the same chunks, inputs and host overlap as one call.

A mid-prompt state is never kept: a released, cancelled or failed prompt is abandoned
(nothing saved, the prepared chunk dropped) and the slot's next prompt starts cleared.

The lanes backend fills prompts a step at a time (Backend.prefill_step over the
pieces, draft/lane_steps.zig) only when Options.steps is set; serve_lanes turns it on
for Metal with TF_DSV41_PREFILL_PIECES=1, TENSORFOLD_PREFILL_ORDER=srpt or a LaneHost
prompt policy, share or budget. With none of them set prompts run whole at admission,
as before; the CUDA backend never sets it.

Tests: pieces of 1, 7, 64, 100 and 300 rows equal the whole prompt (every state byte,
the first choice and 8 greedy tokens) with a decode stream running between pieces; two
prompts' pieces taking turns at the same positions; refused and released mid-prompt
prefills; kept prompts resumed in pieces; served replies in steps equal serial; a short
request during a long prompt gets its first token first in steps (after it as whole
prompts); a cancel mid-prompt keeps nothing; host tests over a stepped CPU twin for the
step adapter, its failure and cancel paths and the switch.
@gilby
gilby force-pushed the dsv41-metal-opt/07-chunked-prompts branch from 3410755 to 3e49808 Compare November 11, 2026 22:48
@ashhart

ashhart commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. This reads well: with nothing set the Metal server still runs whole prompts (the steps vtable is only chosen with steps, and no round planner is attached on Metal), a default step is exactly the prompt chunk so the encoder's boundaries don't move, and the tests compare every state byte with another slot decoding between pieces. A few things before it lands:

  1. draft/lanes.zig:88 tells the lane host the steps are chunk-invariant, so under TENSORFOLD_PREFILL_POLICY=fcfs it cuts every step at 2,048 rows (or whatever TENSORFOLD_PREFILL_TOKENS says), which is not the 4,096-row chunk. The device test shows invariance on the tiny model. That cut only happens while another stream is decoding. Please add two bench_fill cells on the release weights (the long prompt with the short request sent mid-prompt): one with TENSORFOLD_PREFILL_POLICY=fcfs, one with that plus TENSORFOLD_PREFILL_TOKENS=1000, each with the long reply's token_sha256 equal to the unset run's.
  2. --decode-share does nothing on this server: metal/entry.zig never reads o.decode_share, serve_lanes.zig:147 passes none to the lane host, and stepsFromEnv only looks at the environment. So --decode-share 0.5 runs whole prompts silently, while TENSORFOLD_PREFILL_SHARE=0.5 turns steps on. Please either pass it through (and count it in stepsFromEnv) or refuse it by name at open.
  3. Please add a line for TF_DSV41_PREFILL_PIECES to docs/environment.md saying what it does on each backend. On CUDA it selects the round planner; with this PR, on Metal it turns on prompt steps.
  4. The receipt in the PR body, at the pushed head, once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 has landed and this is rebased onto it.

One pointer, not a blocker: when a step stops short of the prompt's end, encode pre-stages the next full chunk, Engram rows included (metal/prompt.zig:168). Under a capped step that stage never matches the next call (:151) and is read again at the smaller size. Your policy cells in item 1 will show whether that costs anything.

These five all sit on #1720 and touch the same few fields, so they'll land in this order, each rebased on the one before: #2951 (the profiler, to read the others' receipts), #2950 (opt-in), #2954, #2953 (on #2951, so forward.zig keeps one run), then #2952 (so its table is measured on the final kernels). Against main they only conflict in CHANGELOG.md.

  • Synthena (Ash's Agent)

@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Real-weights receipt. One M3 Ultra 512 GB, deepseek-ai/DeepSeek-V4.1-Flash-1017 release weights, --parallel 4 --context 1048576 --prompt-cache-gib 0, ReleaseFast, fresh server per arm, base is #1720 at a43ad7b98. tools/bench_fill.py with a 131,072-token prompt, a 64-token reply and a 10 s lead, cold; arms base, this with TF_DSV41_PREFILL_PIECES=1, base. The tool reports every reply exact.

arm long prompt first token (reply first / long first) short reply done (reply first / long first) reply's largest gap
base 203.1 / 211.1 s 206.3 / 204.3 s 203.2 s
this, steps on 204.9 / 216.1 s 144.0 / 156.2 s 6.5 to 7.0 s
base 204.4 / 215.6 s 207.6 / 208.8 s about 203 s

Alone, the long prompt takes 190 to 194 s and the short reply 1.8 s.

Lone cold 32,768-token prompt, tools/bench_cache.py --require-cold: base 48.64 and 47.23 s, this unset 46.45 s, this with steps on 46.98 s; token_sha 0e3f2ebade35 in all four. So the default path and a lone stepped prompt are level with base.

What it shows: steps work and are exact, the long prompt pays 1 to 5 s, and a decoding stream's freeze drops from the whole prefill to one chunk. But the short reply still takes 144 to 156 s, because the host runs one decode round after each 4,096-row piece, so the reply gets one token per piece. That is one token every 6.5 s, not fair service. The next step is to give decoders a share between pieces (several rounds a piece, or smaller pieces while a decoder is live) through the lane host's existing share setting, which this branch does not wire for DeepSeek yet. I'll add that here and rerun before marking it ready.

…pt's steps

With prompts in steps the lane host ran one decode round after each 4,096-row piece,
so a reply beside a long prompt drew one token a piece. The DeepSeek served lanes now
use LaneHost's own share: `--decode-share` reaches LaneHost (its share and, while
replies decode, its row cap on a step), else TENSORFOLD_PREFILL_SHARE, else replies
get 0.25 of wall time beside a prompt in steps. A step taken while a reply decodes owes
it a third of the step's time in rounds before the next step; with no reply decoding
nothing is owed, so a lone prompt runs as before. `--decode-share` on Metal turns
steps on. With steps off the share stays the host's default (whole prompts).

The target counts the most launches between two of a prompt's pieces (Stats).
Tests: on the device, a share of 0.75 gives a decoding reply at least 3 rounds between
two pieces (37 here) where a share of 0 gives at most 1, replies equal serial; host
tests of the same over the stepped twin and of which share applies (flag, env,
default, whole prompts).
…refused by name, docs

`serve_lanes.stepsFor` takes --decode-share: above 0 it turns prompt steps on (its
share then reaches LaneHost), outside [0, 1) it is refused, and otherwise the
environment decides as before. The Metal entry reports either refusal by name at open:
the flag's range, or TF_DSV41_PREFILL_PIECES's expected forms when its text is
malformed (blank keeps the default).

Docs: TF_DSV41_PREFILL_PIECES's row in environment.md names what it does on each
backend; api.md lists DeepSeek-V4.1 on Metal among the servers that apply
--decode-share; profiles.md notes the Metal reader; the optimization ledger's
DeepSeek cell for prompt chunk interleaving is open on Metal.

Test: --decode-share 0.5 with no environment turns steps on at that share (and the
row cap); 0 and unset keep whole prompts; 1, 1.5, -0.1 and NaN are refused; a blank
switch keeps its default and malformed text fails.
…nk at its own size

When a step stopped short of the prompt's end, `encode` prepared the next full chunk.
Under a capped step (TENSORFOLD_PREFILL_POLICY=fcfs with _TOKENS, or the row cap
beside replies with --decode-share) the next call asks for fewer rows, so that stage
never matched and the call read its inputs again. The stage now takes the rows the
next call will ask for when it repeats this one's size, which is the next full chunk
for an uncapped step (the same as before) and for the rows a finish has left.

`Prompt.stage_hits` counts calls that start on a staged chunk. Tests: on the device,
pieces of 1, 7, 64, 100 and 300 rows each start every later call on a staged chunk
(1 and 7 rows found one before this change) with state equal to the whole prompt; a
served fcfs prompt in 128- and 100-row steps beside a decoding reply starts its steps
on staged chunks (8 of 8 and 10 of 10; 1 before) and replies equal serial.
@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Head is now fdcc22211 (three commits on top of the one you read; not rebased). Your points:

  1. The policy cells on the release weights (TENSORFOLD_PREFILL_POLICY=fcfs, and with TENSORFOLD_PREFILL_TOKENS=1000, the long reply's token_sha256 against the unset run's) are queued on the M3 Ultra and I'll post them here. On the tiny model a new served test runs the real fcfs path with 128- and 100-row steps (half a chunk, and not a divisor) beside a decoding reply: replies equal serial.
  2. --decode-share is passed through: metal/entry.zig to boot.zig to serve.zig to serve_lanes.zig, into LaneHost.init, and a value above 0 turns steps on (stepsFor). Values outside 0 to under 1 are refused by name at open, as the CLI already does. A test checks that --decode-share 0.5 with no environment gives steps at that share with the 512-row cap, that 0 and unset keep whole prompts, and that 1, 1.5, -0.1 and NaN are refused. docs/api.md no longer says DeepSeek ignores the flag.
  3. docs/environment.md has the TF_DSV41_PREFILL_PIECES row: the round planner on CUDA, prompt steps on Metal, and what else turns steps on. A malformed value now fails the Metal open with the setting's forms. docs/profiles.md notes the Metal server reads it too.
  4. Understood: the receipt in the body at the pushed head once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 has landed and this is rebased onto it.

Your pointer was right, and it is fixed: a step that stops short now prepares the next call's first chunk at this step's own size (capped at the prompt's end), so a capped step starts on a prepared chunk. The uncapped path prepares the same full chunk as before. A counter of calls that start on a prepared chunk is checked at 1, 7, 64, 100 and 300 rows and in the fcfs test (8 of 8 and 10 of 10 steps; it was 1 of 8 and 1 of 10 before).

Since your review the branch also gives decoding replies a share of wall time between steps, through the lane host's own share (0.25 by default when steps are on). The first receipt showed why it was needed: one round per step. Receipt at c2e65dcde on one M3 Ultra 512 GB, release weights, tools/bench_fill.py with a 131,072-token prompt, a 64-token reply, 10 s lead, cold, fresh server per arm, --prompt-cache-gib 0; every run reports exact:

arm 64-token reply done (reply first / long first) long prompt's first token reply's largest gap
stack tip 188.6 / 212.9 s 185.5 / 219.6 s 185.6 s
steps, default share 8.3 / 11.4 s 201.3 / 208.9 s 6.6 / 6.8 s
--decode-share 0.25 7.1 / 10.4 s 202.4 / 213.8 s 1.4 / 1.6 s
steps, TENSORFOLD_PREFILL_SHARE=1 140.7 / 152.5 s 200.4 / 211.1 s 6.3 / 6.8 s
stack tip 200.3 / 201.8 s 197.1 / 208.6 s 197.2 s

Alone, the reply takes 1.8 s and the long prompt 165 to 194 s across the five servers. So the short reply goes from about 200 s to about 8 s, the long prompt's first token stays inside the spread between the two base runs, and the share-1 arm reproduces the earlier one-round-per-step behaviour. Lone cold 32,768-token request: stack tip 47.48 and 45.85 s, steps on 45.16 s, same token_sha. The M5 Ultra run follows.

The optimization ledger's cell for this family and item is updated, and the body's issue line now points at #2984.

At this head on an M5 Max: test-dsv41-metal-all and dsv41-metal-serve 47/47, the DeepSeek host steps and Linux compiles 75/75 steps, lean_check.py 345 on base and head, pr_check.py --pr 2950 0 errors and 0 warnings.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. We read the three new commits on fdcc22211, and they cover our points 2 and 3 and the pointer. --decode-share now reaches the lane host and is refused by name outside 0 to under 1 (serve_lanes.zig:77-83, metal/entry.zig:29-35). TF_DSV41_PREFILL_PIECES has its row. A capped step now stages the next call's first chunk at its own size (metal/prompt.zig:172). A stage that doesn't match is only prepared again, so it can't change state, and the stage_hits counts pin it. The share table reads well too: the short reply drops from about 200 s to about 8 s, the long prompt stays inside the base runs' spread, and TENSORFOLD_PREFILL_SHARE=1 reproduces the one-round-per-step arm.

One small fix, then what's still owed:

  1. applyShare (serve_lanes.zig:71-74) skips the 0.25 default whenever TENSORFOLD_PREFILL_SHARE is present, including when it's blank. The lane host accepts a blank value as unset (core/lane_host_env.zig:16) and reads it as the default share of 1 (core/lane_host_fill.zig:117). So with steps on and TENSORFOLD_PREFILL_SHARE=, the prompt keeps all the time, and replies are back to one round per step. Please treat blank as unset in applyShare too, and add a blank case to the test at serve_steps_test.zig:416.
  2. The two policy cells from point 1 on the release weights: TENSORFOLD_PREFILL_POLICY=fcfs, and that plus TENSORFOLD_PREFILL_TOKENS=1000, each with the long reply's token_sha256 equal to the unset run's.
  3. Please rebase onto dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's current head c97d661c4, or onto main once it lands. This branch is on a43ad7b98, and the copy-window commit since then changes core/lanes/windows.zig, which these rounds go through. As planned, dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951 lands first, then this PR.
  4. The receipt in the body at the pushed head, on both chips: the bench_fill table (base, steps at the default share, --decode-share 0.25), the lone cold 32K request, and bench_concurrent --levels 1,2,3,4 --alone --serial with nothing set, showing the default path unchanged. No CI has run yet because the branch conflicts with main (only in CHANGELOG.md). The rebase will start it.
  • Synthena (Ash's Agent)

@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

M3 Ultra, 512 GB: the series is being re-measured now under the noise rule in #2984 (one discarded first run, base on both sides, both-under-both test); it follows in this thread.

  • Head: fdcc22211
  • Base: a43ad7b98
  • Checkpoint: deepseek-ai/DeepSeek-V4.1-Flash-1017 release weights as published (pinned revision: not recorded in these runs)
  • Hardware: M5 Ultra, 512 GB; macOS 27.2.1 (26B131); Metal compiler 383.1 (printed by the rotary sweep of this run); the device-test and kernel-contract builds set TOOLCHAINS=com.apple.dt.toolchain.Metal.32023.922.4; the Xcode and Zig versions are not recorded in these logs
  • Tools: tools/bench_fill.py --corpus <text file> --long 131072 --reply 64 --lead 10; tools/bench_cache.py --prompt-file <32,768-token file> --expected-prompt-tokens 32768 --require-cold --tokens 64 --repeats 1; default path: tools/bench_concurrent.py --tokens 128 --reps 3 --temperatures 1.0,0 --levels 1,2,3,4 --alone --serial and tools/bench_openai.py --tokens 256 --reps 3
  • Server flags: --context 1048576 --parallel 4 --prompt-cache-gib 0 --vision --no-update-check, TF_EXPERIMENTAL=deepseek; one fresh server per cell
  • Arm order (M5 Ultra): fill and cold 32K: base, this unset, pieces, decode-share, fcfs, fcfs + 1000, base (cold 32K: base, this unset, pieces); default path (nothing set): base, this, this, base, then bench_openai base, this
  • TENSORFOLD_PREFILL_SHARE was unset in every arm; settings other than the named one were unset

Summary: with nothing set this head is level with base on this chip and changes no token. Default-path decode (bench_concurrent, 1 to 4 streams, T 1 and 0, chat and code): this over base, mean over mean, 0.997 to 1.002 in every cell, and 0.996 to 1.003 for the single-run ratios; bench_openai 0.997 to 0.999; token hashes equal in every cell; 0 failed or unequal comparisons. Fill, alone, long TTFT 136.83 s unset against 136.45 s and 137.16 s for the two base runs; the largest reply gap with the reply sent first is 137.5 s unset against 138.6 s and 139.7 s for base. The settings change how a running reply and a long prompt share the GPU; no arm changed a token.

Cells under 0.98 of base: none.

Default path, nothing set: bench_concurrent aggregate tok/s

Order as run: base 1, this 1, this 2, base 2. Ratios: this 1 over base 1, this 2 over base 2, and mean over mean. Token hashes equal in every cell across the four arms: yes; failed / alone / serial mismatches: 0. All 16 cells are reported.

streams T load base 1 this 1 this 2 base 2 this 1 / base 1 this 2 / base 2 this / base (mean over mean)
1 0 chat 56.7 56.5 56.5 56.6 0.996 0.998 0.997
2 0 chat 77.1 77.2 77.2 77.0 1.001 1.003 1.002
3 0 chat 93.1 93.2 93.1 93.2 1.001 0.999 1.000
4 0 chat 109.2 109.2 109.3 109.3 1.000 1.000 1.000
1 0 code 60.5 60.5 60.6 60.5 1.000 1.002 1.001
2 0 code 81.5 81.7 81.7 81.6 1.002 1.001 1.002
3 0 code 96.2 96.2 96.3 96.3 1.000 1.000 1.000
4 0 code 114.0 114.1 114.2 114.1 1.001 1.001 1.001
1 1 chat 62.5 62.6 62.6 62.5 1.002 1.002 1.002
2 1 chat 70.9 71.0 70.9 70.9 1.001 1.000 1.001
3 1 chat 75.0 74.7 75.0 74.9 0.996 1.001 0.999
4 1 chat 85.2 85.1 85.3 85.3 0.999 1.000 0.999
1 1 code 69.6 69.7 69.6 69.7 1.001 0.999 1.000
2 1 code 67.3 67.4 67.1 67.0 1.001 1.001 1.001
3 1 code 75.8 75.9 75.8 75.8 1.001 1.000 1.001
4 1 code 84.9 84.9 84.9 84.9 1.000 1.000 1.000

bench_openai (median of 3 requests per cell, one run per arm, base then this): token hashes equal to base in all four cells: yes.

prompt T base this this / base tokens per chunk, base / this
fibonacci-raw 0 71.3 71.2 0.999 3.16 / 3.16
fibonacci-raw 1 65.6 65.5 0.998 3.16 / 3.16
gpu-chat-no-think 0 56.9 56.8 0.999 2.349 / 2.349
gpu-chat-no-think 1 57.0 56.8 0.997 2.393 / 2.393

Method as run: each cell is a fresh server started just before it; a thermal-pressure check (powermetrics thermal sampler) ran at the start and the end of every cell, and a cell that was not Nominal at either end was discarded and re-run after a pause. In the default-path series two cells ended at Moderate thermal pressure and were discarded and re-run after a pause: this head run 1 (first attempt) and base run 2 (first attempt); their first attempts are in the gist as discarded-* files and differed from the re-runs by at most 0.4% (this 1) and 0.3% (base 2) in any cell. The fill and cold-32K cells below were not discarded. One-stream --no-drafts control cells on the base tree ran between series (2 of them are relevant here) and read 46.6 / 46.7 tok/s, against a gate of at least 42 tok/s and Nominal thermal pressure. These M5 Ultra series were run before the noise rule in #2984 and have no discarded first run for that purpose; every arm is a fresh server and the order is base, this, this, base wherever a pair is shown.

Fill (bench_fill): a 131,072-token cold prompt beside a 64-token reply, M5 Ultra, 512 GB

'Reply first' sends the reply, then the long prompt 0.5 s later; 'long first' sends the long prompt, then the reply 10 s later. exact is bench_fill's own check that every reply and long answer equals its run alone. The token_sha256 of the long answer in every request of every arm, read from the server logs, is 154a7500f4bb (21 requests, one value).

arm long TTFT alone (s) long TTFT, reply first (s) long TTFT, long first (s) short reply done, reply first (s) short reply done, long first (s) reply gap p95 / max, reply first (ms) reply gap p95 / max, long first (ms) bench_fill exact
base, run 1 136.45 138.5 137.44 140.44 129.63 182.9 / 138583.2 91.1 / 183.5 yes
this, unset 136.83 137.44 136.58 139.39 128.75 182.5 / 137530.6 91.1 / 183.7 yes
this, TF_DSV41_PREFILL_PIECES=1 134.17 139.41 138.8 6.26 13.01 50.7 / 5067.0 3769.2 / 5068.0 yes
this, --decode-share 0.25 136.55 140.64 139.66 3.29 6.69 687.9 / 815.1 705.1 / 820.4 yes
this, TENSORFOLD_PREFILL_POLICY=fcfs 135.64 140.12 141.78 8.61 14.12 2495.4 / 2985.4 2554.2 / 2909.3 yes
this, TENSORFOLD_PREFILL_POLICY=fcfs TENSORFOLD_PREFILL_TOKENS=1000 137.23 140.31 138.67 3.61 7.2 1106.3 / 1357.2 1112.5 / 1340.2 yes
base, run 2 137.16 139.61 137.71 141.55 129.91 182.4 / 139693.2 91.1 / 183.5 yes

Every fill cell started at Nominal thermal pressure and ended at Moderate thermal pressure after the 131,072-token prefill (the powermetrics thermal sampler's level, not memory pressure); the fill script sets end-of-cell pressure as informational for this long prefill, so none was discarded. Alone-run long TTFT of the seven fill cells ranges 134.17 to 137.23 s. In the cold 32,768-token series one kept cell had a macOS background process (mediaanalysisd) using 97% CPU at the end-of-cell check (this head, unset); thermal pressure was Nominal at both ends of all three cold cells.

Cold 32,768-token prompt (bench_cache --require-cold), M5 Ultra

arm cold TTFT (s) cached tokens token_sha decode tok/s
base 35.30 0 0e3f2ebade35 45.8
this, unset 35.35 0 0e3f2ebade35 45.7
this, TF_DSV41_PREFILL_PIECES=1 36.02 0 0e3f2ebade35 45.9

Cold TTFT over base (single runs): this unset 1.001, TF_DSV41_PREFILL_PIECES=1 1.021 (the pieces setting is opt-in; it is 2.1% slower than base here on one run). Token hashes equal in all three cells.

Not run: the pieces, decode-share and fcfs arms in the default-path series (they are opt-in settings, measured in the fill table only); TENSORFOLD_PREFILL_SHARE=1; the checkpoint's pinned revision is not recorded.

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

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby for the M5 Ultra receipt. It answers the policy cells we asked for. With TENSORFOLD_PREFILL_POLICY=fcfs, and with that plus TENSORFOLD_PREFILL_TOKENS=1000, the long answer's token_sha256 is 154a7500f4bb in every request, the same as the unset and base runs. With nothing set, this head is level with base: 0.997 to 1.002 mean over mean in all 16 bench_concurrent cells and 0.997 to 0.999 in bench_openai, with every token hash equal. With the reply sent first, the short reply beside a 131,072-token prompt now finishes in 3 to 9 s under the step settings, against about 140 s on base.

These M5 Ultra series came before the discarded first run in #2984's noise rule. Base ran first in each, so any first-run lift favours base, not this branch. We'll take them as they are, with no need to rerun them.

What's still open:

  1. The blank-share fix from our last note. The head is still fdcc22211, and applyShare (serve_lanes.zig:71-74) still skips the 0.25 default when TENSORFOLD_PREFILL_SHARE is set but blank, so the prompt keeps all the time again. Blank should read as unset, as it does everywhere else (rule 3 on DeepSeek-V4.1 on Metal: decode and prompt speed on M3 Ultra and M5 Ultra, token-exact (gate and order for the drafts) #2984). Please add a blank case to the test at serve_steps_test.zig:416. No measured arm set that variable, so this fix needs no new runs.
  2. The rebase onto dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's c97d661c4, or onto main once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 lands. The M5 Ultra numbers carry over if git range-diff shows this branch's commits as =.
  3. The M3 Ultra series under the noise rule. Please also rerun the lone cold 32K prompt with TF_DSV41_PREFILL_PIECES=1 on the M5 Ultra, with base on both sides. On a single run it read 36.02 s against base's 35.30 s (1.021), while on the M3 Ultra the same cell was level. A lone prompt in steps should cost what a whole one does. If the gap holds, please say so in the CHANGELOG line.
  4. Per rule 4 on DeepSeek-V4.1 on Metal: decode and prompt speed on M3 Ultra and M5 Ultra, token-exact (gate and order for the drafts) #2984, the CHANGELOG line should quote the served result from both chips, for example the 64-token reply's finish time beside the 131,072-token prompt, base against the default share.

Then it lands second in the stack, right after #2951.

  • Synthena (Ash's Agent)

@gilby

gilby commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

M3 Ultra, 512 GB: the receipt promised above, run on 2026-11-12 and 13 under the noise rule in #2984 (one discarded first run per series, base on both sides, both-under-both test).

  • Head: fdcc22211
  • Base: a43ad7b98
  • Checkpoint: deepseek-ai/DeepSeek-V4.1-Flash-1017 release weights as published, read from local disk (pinned revision: not recorded in these runs)
  • Hardware: M3 Ultra, 512 GB; the macOS version, Metal compiler, Xcode and Zig versions are not recorded in these logs
  • Tools, from the base tree: tools/bench_concurrent.py --tokens 128 --reps 3 --alone --serial at 1, 2, 3 and 4 streams, temperature 0 and 1; tools/bench_openai.py --tokens 256 --reps 3; tools/bench_fill.py --corpus <text file> --long 131072 --reply 64 --lead 10 (run through a small wrapper that adds the answers' token hashes to its JSON); tools/bench_cache.py --prompt-file <32,768-token file> --expected-prompt-tokens 32768 --require-cold --tokens 64 --repeats 1
  • Server flags: --context 1048576 --parallel 4 --prompt-cache-gib 0 --vision --no-update-check, TF_EXPERIMENTAL=deepseek; one fresh server per cell; every TENSORFOLD_PREFILL_* and TF_DSV41_* setting is unset unless the row names it
  • Arm order: default path, per series: one discarded run (base), base, this, this, base; fill: base, this with nothing set, TF_DSV41_PREFILL_PIECES=1, --decode-share 0.25, TENSORFOLD_PREFILL_POLICY=fcfs, base; cold 32K: base, this with nothing set, TF_DSV41_PREFILL_PIECES=1, base
  • Server binaries (sha256 prefix): base 0a5638a8e95e, this 09769dab0ae6

Summary: with nothing set this head is level with base on this chip in the default path and changes no token: aggregate decode this over base, mean over mean, is 0.998 to 1.002 in all 16 bench_concurrent cells (single-run ratios 0.995 to 1.005), bench_openai 0.998 to 1.001, token hashes equal in every cell, 0 failed or unequal comparisons; the cold 32,768-token first request is 43.13 s against 43.19 s and 43.18 s for base. Opt-in, a 64-token reply sent beside a 131,072-token cold prompt finishes in 6.5 to 14.9 s with TF_DSV41_PREFILL_PIECES=1, --decode-share 0.25 or TENSORFOLD_PREFILL_POLICY=fcfs, against 209 to 256 s for base and for this head with nothing set, every answer equal to its run alone; one caveat on the nothing-set fill arm is named under the fill table.

Cells under 0.98 of base: none in the default-path series. In the fill table the nothing-set arm's alone-run long first token is 0.963 and 0.972 of the two base runs as a speed ratio (single runs, outside the noise rule; see the note under the fill table).

Default path, nothing set: bench_concurrent aggregate tok/s

Order as run: base 1, this 1, this 2, base 2. Ratios: this 1 over base 1, this 2 over base 2, and mean over mean. Verdict by the #2984 rule: slower only if both this runs are under both base runs and the mean gap is over 1%, faster the mirror image, otherwise within noise. Token hashes equal in every cell across the four arms: yes; failed, alone or serial mismatches: 0. All 16 cells are reported: 0 slower, 0 faster.

streams T load base 1 this 1 this 2 base 2 this 1 / base 1 this 2 / base 2 this / base (mean over mean) verdict
1 0 chat 44.0 43.8 43.7 43.7 0.995 1.000 0.998 within noise
2 0 chat 55.9 55.9 55.9 55.8 1.000 1.002 1.001 within noise
3 0 chat 68.9 68.8 68.7 68.8 0.999 0.999 0.999 within noise
4 0 chat 78.3 78.3 78.3 78.3 1.000 1.000 1.000 within noise
1 0 code 47.1 47.1 47.1 47.1 1.000 1.000 1.000 within noise
2 0 code 59.7 59.7 59.7 59.7 1.000 1.000 1.000 within noise
3 0 code 72.4 72.4 72.3 72.3 1.000 1.000 1.000 within noise
4 0 code 84.2 84.0 84.1 84.0 0.998 1.001 0.999 within noise
1 1 chat 47.8 47.8 47.8 47.8 1.000 1.000 1.000 within noise
2 1 chat 50.7 50.8 50.7 50.9 1.002 0.996 0.999 within noise
3 1 chat 55.7 55.7 55.7 55.8 1.000 0.998 0.999 within noise
4 1 chat 61.7 61.7 61.6 61.6 1.000 1.000 1.000 within noise
1 1 code 52.1 52.2 52.1 52.1 1.002 1.000 1.001 within noise
2 1 code 49.0 49.1 49.0 49.1 1.002 0.998 1.000 within noise
3 1 code 56.5 56.5 56.7 56.7 1.000 1.000 1.000 within noise
4 1 code 62.4 62.4 62.6 62.3 1.000 1.005 1.002 within noise

bench_openai (decode tok/s, median of 3 requests per cell; two runs of each arm in the same order). Token hashes equal in all four runs of every cell: yes. 0 slower, 0 faster.

prompt T base 1 this 1 this 2 base 2 this 1 / base 1 this 2 / base 2 this / base (mean over mean) verdict tokens per chunk, all four runs
fibonacci-raw 0 54.5 54.5 54.5 54.5 1.000 1.001 1.000 within noise 3.16
gpu-chat-no-think 0 43.8 43.8 43.8 43.8 1.001 1.000 1.001 within noise 2.349
fibonacci-raw 1 49.9 49.9 49.8 49.8 0.999 1.000 1.000 within noise 3.16
gpu-chat-no-think 1 44.0 43.8 43.6 43.6 0.995 1.000 0.998 within noise 2.393

Thermal pressure level at the end of each default-path cell (powermetrics thermal sampler; every cell started at Nominal, and the discarded first run of every series ended at Nominal):

series base 1 this 1 this 2 base 2
bench_concurrent, 1 stream, T 0 and 1 Nominal Nominal Nominal Nominal
2 streams, T 0 Nominal Nominal Nominal Nominal
2 streams, T 1 Nominal Nominal Nominal Nominal
3 streams, T 0 Nominal Nominal Nominal Nominal
3 streams, T 1 Nominal Nominal Nominal Nominal
4 streams, T 0 Nominal Nominal Nominal Nominal
4 streams, T 1 Moderate Moderate Moderate Moderate
bench_openai, T 0 Nominal Nominal Nominal Nominal
bench_openai, T 1 Moderate Nominal Moderate Nominal

The kept cells that ended above Nominal in this part are the four 4-stream T 1 cells and two of the four bench_openai T 1 runs (base 1, this 2), all at Moderate. In each of those series base and this agree within 0.5% (4 streams T 1: 61.7 / 61.7 / 61.6 / 61.6 and 62.4 / 62.4 / 62.6 / 62.3 tok/s), and none ended at Heavy, so none was discarded.

Fill (bench_fill): a 131,072-token cold prompt beside a 64-token reply, M3 Ultra, 512 GB

'Reply first' sends the reply, then the long prompt 0.5 s later; 'long first' sends the long prompt, then the reply 10 s later. 'Reply done' is the time from sending the reply to its last token. exact is bench_fill's own check that every reply and long answer equals its run alone. The long answer's token_sha256 prefix is 154a7500f4bb in the alone, reply-first and long-first request of every arm (18 requests, one value, read from the JSON and the server logs), and the 64-token reply's is 7cfca992ef67 in all 18. The reply alone takes 1.81 to 1.82 s in every arm.

arm long first token alone (s) long first token, reply first (s) long first token, long first (s) reply done, reply first (s) reply done, long first (s) reply gap p95 / max, reply first (ms) reply gap p95 / max, long first (ms) bench_fill exact thermal pressure at end
base, run 1 167.55 210.49 262.04 214.13 255.88 305.7 / 210622.9 167.3 / 348.0 yes Moderate
this, nothing set 174.07 215.32 241.37 218.80 234.90 294.6 / 215430.3 154.6 / 306.3 yes Moderate
this, TF_DSV41_PREFILL_PIECES=1 169.04 203.71 241.02 7.59 13.84 65.0 / 5850.5 78.0 / 7600.7 yes Heavy
this, --decode-share 0.25 169.24 204.39 236.68 6.54 11.75 1237.2 / 1315.1 1546.4 / 1653.5 yes Heavy
this, TENSORFOLD_PREFILL_POLICY=fcfs 171.71 212.40 242.18 8.11 14.86 3147.2 / 3276.4 4104.9 / 4236.8 yes Moderate
base, run 2 169.13 205.74 245.62 209.18 239.23 288.1 / 205869.1 158.7 / 319.9 yes Heavy

Every fill cell started at Nominal and ended above it after the 131,072-token prefill, and none was discarded or re-run (a fill cannot be shortened): base run 1, this with nothing set and fcfs ended at Moderate; the pieces arm, the --decode-share arm and base run 2 ended at Heavy. These are kept cells that ended above Nominal, and the fill rows are single runs, so the noise rule does not apply to them. Read plainly: the alone-run long first token of the nothing-set arm (174.07 s) is 3.9% and 2.9% above the two base runs (167.55 s and 169.13 s, which differ from each other by 0.9%), and its reply-first long first token (215.32 s) is above both base runs (210.49 s, 205.74 s) while its long-first long first token (241.37 s) is below both (262.04 s, 245.62 s); the opt-in arms' alone runs (169.04 to 171.71 s) sit at the base level. The nothing-set arm is the same code path as base with the new setting off and gives the same tokens; I have not explained the 174.07 s, and a repeat would settle whether it is thermal or real.

Cold 32,768-token prompt (bench_cache --require-cold), M3 Ultra, 512 GB

arm cold first token (s) cached tokens token_sha decode tok/s thermal pressure at end
base, run 1 43.19 0 0e3f2ebade35 34.9 Heavy (second attempt)
this, nothing set 43.13 0 0e3f2ebade35 34.9 Heavy (second attempt)
this, TF_DSV41_PREFILL_PIECES=1 43.11 0 0e3f2ebade35 34.8 Heavy (second attempt)
base, run 2 43.18 0 0e3f2ebade35 34.9 Heavy (second attempt)

Cold first token over the mean of the two base runs (43.185 s), single runs: this with nothing set 0.999, TF_DSV41_PREFILL_PIECES=1 0.998; token hashes equal in all four cells. bench_cache also sends a second request on the same server; with the prompt cache off it is cold too (41.10 to 41.11 s in all four cells, cached_tokens 0), and is in the JSON.

All four cold cells are kept cells that ended above Nominal: each ended at Heavy on its first attempt, was discarded, was re-run once after cooling to Nominal, and ended at Heavy again, so the second attempt is kept and flagged. On this machine a cold-prefill cell of this size ends at Heavy whichever arm runs. The first attempts' raw files were not archived (their discard lines are in the window log).

Method as run: each cell is a fresh server started after the previous one was stopped and a 60 s settle, and a cell starts only when the powermetrics thermal sampler reports Nominal (the driver polls every 30 s; the longest wait was 92 s); the level is recorded again at the end of the cell. One run per series is discarded and not in any table, base is on both sides, and the series are per stream count and temperature (bench_concurrent) and per temperature (bench_openai). Because a 2-minute 4-stream cell that starts at Nominal ends at Moderate on this machine and the longer cells at Heavy, a cell is not discarded for its end level unless it ended at Heavy on a short cell: then it is discarded, cooled to Nominal and re-run once, and if it ends at Heavy again it is kept and flagged. No default-path cell ended at Heavy; the fill cells are not re-run, and the four cold cells are the flagged ones above. Tools and server flags are the ones in the header; the tools come from the base tree and the head does not touch tools/. Every arm and the scripts that ran them are in the gist.

Not run: the pieces, --decode-share and fcfs arms in the default-path series (they are opt-in settings, measured in the fill table and the cold 32K table only); TENSORFOLD_PREFILL_SHARE; a repeat of any fill cell; the checkpoint's pinned revision, the macOS version and the toolchain versions are not recorded. Kernel contract: this PR adds no kernel (git diff a43ad7b98..fdcc22211 --stat lists no .metal file and no contract record), so there is no contract run to post.

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

…hare in steps, as unset does

applyShare skipped the steps default whenever TENSORFOLD_PREFILL_SHARE was present, blank included,
while the lane host reads a blank value as unset and so as its own default share of 1: with steps on
and the variable blank, a prompt kept all the time and replies were back to one round a step. A
blank value now counts as unset here too. The share test gains the blank case (it read 1 before).
@gilby

gilby commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

New head: 49a92da01 (two commits on top of fdcc22211, not rebased).

  • b8c0cb299: your point 1 from 12 Nov 14:43, repeated at 23:50. applyShare now reads a blank TENSORFOLD_PREFILL_SHARE as unset, as the lane host does, so steps keep the 0.25 reply share instead of giving the prompt all the time. The share test at serve_steps_test.zig:416 has a blank case; it read a share of 1 before the change and 0.75 after. I checked the other settings this PR reads for the same rule: TF_DSV41_PREFILL_PIECES goes through platform/environment.zig (blank is its default, as its existing test checks), and TENSORFOLD_PREFILL_ORDER, _POLICY, _BUDGET and _TOKENS go through the lane host's own readers, which already take blank as unset; --decode-share is a flag. No measured arm set the share variable, so no receipt changes.
  • 49a92da01: point 4 from 12 Nov 23:50 (rule 4 on DeepSeek-V4.1 on Metal: decode and prompt speed on M3 Ultra and M5 Ultra, token-exact (gate and order for the drafts) #2984). The CHANGELOG line now quotes the served result on both chips: a 64-token reply sent beside a 131,072-token cold prompt finishes in 6.3 to 13.0 s with TF_DSV41_PREFILL_PIECES=1 against 130 to 142 s on base on the M5 Ultra, and in 7.6 to 13.8 s against 209 to 256 s on the M3 Ultra, every answer equal to its run alone. It leaves the lone cold 32K cell out until the rerun below settles it.

Receipts: the M5 Ultra and M3 Ultra receipts above were taken at fdcc22211. b8c0cb299 changes applyShare only for a blank TENSORFOLD_PREFILL_SHARE, which no measured arm set, and 49a92da01 is the CHANGELOG, so every posted cell (default path, fill table, cold 32K) still describes what this head serves in the arms measured.

Your other points:

  1. (12 Nov 14:43 and 23:50) The rebase: I am not pushing a rebase of a branch under review to follow main; this head stays on a43ad7b98 and is ready to be rebased at landing onto dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951, in the DeepSeek-V4.1 on Metal: decode and prompt speed on M3 Ultra and M5 Ultra, token-exact (gate and order for the drafts) #2984 order. A local rebase onto dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951 rebased on today's main conflicts where both PRs add a field to the same struct literal or option list (boot.zig, entry.zig, serve.zig, target.zig, the serve fixture and tests, docs/environment.md, CHANGELOG.md); each resolves by keeping both sides; the rebased branch builds and passes the DeepSeek host tests, and the device suite passes at the top of the rebased stack.
  2. (12 Nov 23:50) The M3 Ultra series under the noise rule: posted on 13 Nov. Still owed: the lone cold 32K prompt with TF_DSV41_PREFILL_PIECES=1 on the M5 Ultra with base on both sides (it read 1.021 on one run; on the M3 Ultra the same cell was level, 0.998). If the gap holds I'll say so in the CHANGELOG line. I'll also repeat the M3 Ultra fill arm with nothing set, whose alone-run first token (174.07 s) I could not explain, with base on both sides.

At 49a92da01 on an M5 Max: test-dsv41-metal-all 9/9 steps and 47/47 tests; zig build test passes apart from dashboard_test, which fails the same way on this base and is fixed on main by #2946; lean_check.py 345 on base and head; pr_check.py --pr 2950 0 errors and 0 warnings.

@gilby

gilby commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

M3 Ultra, 512 GB: the repeat of the fill cell with nothing set that the earlier comment said was still owed, at head 49a92da01.

  • Checkpoint: deepseek-ai/DeepSeek-V4.1-Flash-1017 revision 1bae0e1088993b9b82de08f59de5be5a34e42170, release weights as published, read from local disk
  • Hardware: M3 Ultra, 512 GB
  • Server flags: --context 1048576 --parallel 4 --prompt-cache-gib 0 --vision --no-update-check, TF_EXPERIMENTAL=deepseek, one fresh server per cell
  • Thermal: a cell starts at Nominal pressure after a 60 s settle and the end level is recorded
  • Head: 49a92da01; base a43ad7b98 (the binary of the earlier receipts); server binaries (sha256 prefix): this head 5abdef90e419, base 0a5638a8e95e
  • Tool: tools/bench_fill.py --corpus <text file> --long 131072 --reply 64 --lead 10, through the same wrapper that adds the answers' token hashes to its JSON; every setting such as TF_DSV41_PREFILL_PIECES, --decode-share and TENSORFOLD_PREFILL_POLICY unset in all four runs
  • Arm order: one discarded base run, then base, this, this, base (the discarded run is in the gist and in no table)

'Reply first' sends the 64-token reply, then the long prompt 0.5 s later; 'long first' sends the long prompt, then the reply 10 s later; 'reply done' is the time from sending the reply to its last token. Each arm is a fresh server and a single fill (a 131,072-token cold prompt is not repeatable inside one server).

arm long first token alone (s) long first token, reply first (s) long first token, long first (s) reply done, reply first (s) reply done, long first (s) reply gap p95 / max, reply first (ms) reply gap p95 / max, long first (ms) exact end level
base, run 1 170.26 196.11 230.19 199.34 223.56 267.7 / 196227.3 145.2 / 290.8 yes Heavy
this, run 1 170.04 194.71 227.65 197.92 221.01 265.5 / 194826.8 145.2 / 289.2 yes Heavy
this, run 2 170.34 195.66 228.68 198.89 222.07 267.7 / 195777.7 146.6 / 293.6 yes Heavy
base, run 2 170.04 194.63 227.58 197.84 220.92 266.0 / 194739.7 144.0 / 286.8 yes Heavy

The long answer's token_sha prefix is 154a7500f4bb in the alone, reply-first and long-first request of all four arms (12 requests) and the 64-token reply's is 7cfca992ef67 in all four; exact (bench_fill's own check that every reply and long answer equals its run alone) is true in all four, and the cold check passed. The reply alone takes 1.83 s in every arm.

All five servers (the discarded run included) ended at Heavy thermal pressure; this is what a cell this long does on this machine, and the arms ran the same load in the same order, so it does not separate them.

With nothing set this head equals base on every figure: this over base, mean over mean, is 1.000 for the alone-run first token, 0.999 and 0.997 for the long prompt's first token with the reply first and long first, and 0.999 and 0.997 for the reply's completion time. The alone-run first token is 170.0 to 170.3 s in all four runs (170.26, 170.04, 170.34, 170.04 s), so the single 174.07 s reading of the earlier receipt did not repeat; the two base runs are 0.22 s apart and the two this runs 0.30 s apart.

Still owed: the lone cold 32,768-token prompt with TF_DSV41_PREFILL_PIECES=1 on the M5 Ultra, with base on both sides.

Raw JSON, server logs and drivers: https://gist.github.com/gilby/c9070e544e81abb399bccd8a101cf1df

This branch has not been deployed

No deployments
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