Skip to content

dsv41 metal: decode experts decode an expert's weights once for all of its rows (stacked on #1720, #2973) - #2983

Draft
gilby wants to merge 19 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/08-expert-rows
Draft

gilby wants to merge 19 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/08-expert-rows

Conversation

@gilby

@gilby gilby commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

On Metal, DeepSeek-V4.1's routed-expert kernels for more than one row ran every expert's rows in passes padded to four rows: a two-row launch did four rows of arithmetic, and a five-row slot ran two padded passes and decoded the expert's weights twice. With this change the multi-row gate/up and down kernels (tf_dsv41_xp_upc_rows, tf_dsv41_xp_down_rows) decode an expert's weights once per pass and size the pass by the rows left (2, 4, 6 or 8 for gate/up; 2 or 4 for down). Each row's chain is the one-row kernel's, product by product, so a row has the same bits whatever other rows are in the launch and however they route. One-row slots keep the one-row kernels, untouched.

Why it is exact: each row's 32 fp4 by E4M3 (or bf16) products run as a sequential fma chain from zero in k order, then the same scaled fma, the same eight-way tree and the same epilogue as the one-row kernel. No value crosses between rows, and padding rows are computed but never written.

Checks on this head

  • Head commit: 10676dba8
  • 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: the new commit touches the CHANGELOG only; at b471c9479 zig build test-dsv41-metal-all passed on the M5 Max (40/40) and on the M3 Ultra (40/40).
  • zig build time-dsv41-metal-synth (six synthetic layers at release widths, a 40-layer round, ms, base to this), rows on the same experts: 1 row 30.2 to 30.3, 2 rows 38.4 to 33.4, 3 rows 41.8 to 38.7, 4 rows 45.0 to 41.5, 5 rows 61.2 to 49.9, 8 rows 68.6 to 59.4, 16 rows 111.1 to 93.4, four slots of one row 45.6 to 41.9. Rows routed apart (about 18 distinct experts at 4 rows): 2 rows 39.5 to 36.6, 4 rows 50.3 to 46.6, 8 rows 75.5 to 65.2, 16 rows 116.0 to 96.3, four slots of one row 50.8 to 47.4.
  • 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: time-dsv41-metal-synth on the M3 Ultra, to confirm the pass sizes; run list in the comment.

Receipt

Kernel contract
  • Kernels registered with the contract, and the kernel each one replaces: tf_dsv41_xp_upc_rows (replaces tf_dsv41_xp_upc_multi) and tf_dsv41_xp_down_rows (replaces tf_dsv41_xp_down_multi), each registered with every row on the same six experts against the float64 reference (the down is byte-equal to it). An equality probe checks a launch's activations, outputs and combine output bit-equal to each row launched alone, for same, disjoint and random routing at rows 1, 2, 3, 4, 5, 7, 8, 9 and 16, finite and with NaN and Inf planted: 54 cases, 0 differ.
  • A planted control for each new kernel, and each one fails as planted: TF_CTL 6 on each (rows read their pass's first row), both red at the shapes gate; the probe's own control is red too. At TF_CTL 0 the source is what ships.
  • Each backend's SUMMARY line and exit status, M5 Max: base SUMMARY 95 kernels: 95 pass gates 1-4, 0 fail, 0 known findings; 190 controls: 190 red as planted, 0 missed; 0 limit problems; this head SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems. M5 Ultra at b471c9479, exit 0: --only dsv41 SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems; full run (with the one-line fix from Kernel contract on tensor-unit Macs: the full run aborts with ControlEditMissing in the llama gemm_untransposed control since #2251 #3067) 758 pass, the same 57 known findings as base. M3 Ultra at b471c9479, exit 0: --only dsv41 97 of 97 pass, 192 of 192 controls red; full run 547 pass, the same three known nonfinite findings as base. The new commit is the CHANGELOG.
  • Chips I could not run: M1, M2, M4.

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 18 commits November 12, 2026 01:30
…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).
…ers per row and dispatch class

Six synthetic layers of the release's widths (one of each attention kind, few experts) are timed at
1-16 rows of one slot and at 4 slots x 1 row, each dispatch alone and the layer on the serving
encoder, and split into FP8 rows, experts, attention and glue, with a 40-layer round weighted by
kind. timing_real's report takes a launch (context and spans) so both harnesses share it.
…x tile, bit-equal to the row kernel

Launches of 6 to 16 rows run the dense FP8 projections on fp8_tile.metal: 8 or 16 columns a
threadgroup, K split over up to 16 simdgroups in aligned power-of-two runs of groups, each 32-wide
group summed from zero in four 8-wide MMA steps in k order (the prompt tile's group root), the
groups' roots and the slices' roots in the same zero-padded pair tree the row kernel uses. Every
row's bits are the row kernel's one row a launch; 1-5 rows keep the row kernel (chosen by row
count only), since below 6 rows it reads the weights faster.

Contract: six registrations (f32, bf16, a forced 16-column form, one simdgroup, the row kernel one
row a launch as replaced baseline, the release wkv fixture), four planted controls, gate-5 records
with M1/M2 limits, and an equality probe over every model FP8 shape at 1-16 rows, every form, the
16/17 seam and a batch with NaN and Inf planted. Device tests: the layer's kernel by row count, and
four slots of two rows each in one launch against each alone.
…f its rows, bit-equal to one row a launch

The decode gate/up and down for an expert's slot of 2-16 rows ran Kimi's four-row passes: two rows
paid for four rows of arithmetic, five rows for two passes of four, each rereading the weights. The
new tf_dsv41_xp_upc_rows and tf_dsv41_xp_down_rows decode each 32-group of an expert's MXFP4
weights once per pass and apply it to every row in it (passes of 2, 4, 6 or 8 rows for gate/up, 2
or 4 for down, rows past the slot repeating its last row unwritten), reading the activations
straight from device memory. Each row's chain is the one-row kernel's: 32 products in k order from
zero, then its group's scale, then the same tree and epilogue, so a row's bits are the same alone
or with any other rows and routing. Slots of one row keep tf_dsv41_xp_upc_one/down_one unchanged.

Contract: registrations with every row on the same six experts (gate/up coded and down, gates 1-4
against the oracle), one planted control a kernel (rows reading their pass's first row, red at
shapes), gate-5 records for both launches, and a probe holding a launch's act, out and combine
output bit-equal to each row launched alone at 1-16 rows on the same, disjoint and random experts,
with NaN and Inf planted, plus its own control. Device test: the same equality at 2-16 rows.
@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

First real-weights receipt, with the contract on the M5 Ultra. One M5 Ultra 512 GB, deepseek-ai/DeepSeek-V4.1-Flash-1017 release weights, ReleaseFast, --parallel 4 --context 1048576 --prompt-cache-gib 0, fresh server per arm, tools/bench_concurrent.py --levels 1,2,3,4 --tokens 128 --reps 3 --temperatures 0,1 --alone --serial then tools/bench_openai.py --tokens 256 --reps 3. Every request's token_sha256 equals the stack tip's in every cell, and concurrent equals alone equals serial. Thermal pressure was logged nominal at both ends of every cell, and control cells before each block read 46.6 to 46.9 tok/s. Arms in the order stack tip (c97d661c4), #2973, this, this, #2973, stack tip; the two stack-tip runs differ by 0.1 tok/s or less. This head contains #2973, so its own effect is the last column.

Contract on the M5 Ultra at b471c9479 (tf-kernel-contract --only dsv41, with a fresh rotary receipt): SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems. test-dsv41-metal-all exits 0 there. The run without --only stops with ControlEditMissing before any SUMMARY, on this head and on the stack tip alike, so I have no full-suite line to give.

Greedy, aggregate tok/s:

streams code: stack tip #2973 this this / tip chat: stack tip #2973 this this / tip
1 61.2 61.6 65.4 1.069 56.5 56.6 59.9 1.060
2 81.4 85.9 95.2 1.170 76.9 80.2 88.1 1.146
3 96.2 96.1 98.7 1.026 93.1 93.0 95.2 1.023
4 114.0 113.8 116.8 1.025 109.2 109.0 111.6 1.022

Sampled cells have the same shape (two streams of sampled code 1.111). bench_openai: code 76.1 against 71.2 tok/s, chat 59.8 against 56.7. No cell is slower.

So on this chip the expert rows are worth 6 to 7% at one drafted stream, 15 to 17% at two (10 to 11% of it over #2973), and 2 to 3% at three and four, where rounds are plain and each expert has few rows. The M3 Ultra contract and receipt are running; I'll post them here. The issue line in the body now points at #2984.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. This is a clean change. ds_rows_chains (experts.metal:204-243) runs each row's 32 products as its own fma chain from zero in k order, followed by the same scaled fma and the same 8-lane tree as the one-row kernels. Padding rows repeat the last real row and are never written (r < rn in ds_up_out and at :328). Pass sizes follow the rows left (ds_tier, :245), so a five-row slot's gate/up now decodes its weights once instead of twice (its down still takes a pass of 4 and one of 2). The probe holds each row equal to that row launched alone under same, disjoint and random routing. Over the stack tip, with #2973 under it, the M5 Ultra receipt shows 6-7% at one drafted stream, 15-17% at two (10-11% of it over #2973 alone) and 2-3% at three and four, with no cell slower and every token_sha256 equal.

The code is ready. Owed before it can land:

  1. On the M3 Ultra 512 GB at this head:
    • the contract (--only dsv41) SUMMARY and the rows probe, since bit-equality relies on the compiler keeping the explicit fma chains on that chip;
    • the receipt: stack tip, this, this, stack tip; bench_concurrent --alone --serial at 1 to 4 streams, T 0 and T 1, plus bench_openai;
    • time-dsv41-metal-synth. The pass sizes (2, 4, 6 or 8 rows for gate/up, 2 or 4 for down) were picked from M5 Max timings, so please confirm them on the M3 or choose sizes that are no slower on either chip.
  2. With those numbers, please replace the synthetic M5 Max percentages in the CHANGELOG.md line with the served results on both chips.
  3. Your commit applies cleanly on dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's c97d661c4 by itself. It lands right after dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973, in the order on dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973. Once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 has landed, please rebase onto main so CI runs.
  • 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.

Summary: token hashes equal to base in every cell of every table. Decode, mean over mean against base, 1 to 4 streams, T 1 and 0, chat and code: 1 stream 1.060 to 1.074, 2 streams 1.103 to 1.170, 3 streams 1.023 to 1.055, 4 streams 1.023 to 1.049. bench_openai 1.057 to 1.095. Cold-prompt TTFT 0.825 to 1.003 of base at 2,048 to 65,536 tokens (single runs). The M5 Ultra --only dsv41 kernel contract is clean: 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems.

Cells under 0.98 of base: none.

Kernel contract

M5 Ultra, 512 GB (applegpu_g17, tensor units true), --only dsv41 with the rotary receipt (tf-kernel-contract --only dsv41 --rotary-accuracy <sweep json>, the sweep generated by tf-rotary-sweep in the same tree), exit status 0, SUMMARY line verbatim:

SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems; gates 118589 ms, limits 17 ms

M5 Ultra full run (tf-kernel-contract --rotary-accuracy <sweep json>, no --only): attempted, exit status 1, and no SUMMARY line was printed: it stopped with error: ControlEditMissing right after the core deltanet registrations (SHARED_CONV_LIMITS lines), before reaching any DeepSeek registration. The same abort happens on the earlier base a43ad7b98; the full run was not run on c97d661c4 on this chip. There is no M5 Ultra full-run SUMMARY yet; the logs are in the gist.

M3 Ultra, 512 GB (applegpu_g15, tensor units false; macOS 26.6.2 (25G83), Metal compiler 373.7), final contract run, both runs verbatim. --only dsv41:

SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems; gates 141015 ms, limits 19 ms

M3 Ultra full run (tf-kernel-contract --rotary-accuracy <sweep json>):

SUMMARY 547 kernels: 547 pass gates 1-4, 0 fail, 3 known findings; 581 controls: 581 red as planted, 0 missed; 0 limit problems; gates 2634780 ms, limits 183 ms

For comparison, the base tree on the M3 Ultra (the tree at c97d661c4, the base of this PR), full run:

SUMMARY 539 kernels: 539 pass gates 1-4, 0 fail, 3 known findings; 575 controls: 575 red as planted, 0 missed; 0 limit problems; gates 2634630 ms, limits 15026 ms

and its --only dsv41 run:

SUMMARY 89 kernels: 89 pass gates 1-4, 0 fail, 0 known findings; 186 controls: 186 red as planted, 0 missed; 0 limit problems; gates 119168 ms, limits 277 ms

The full run needs a rotary sweep receipt passed with --rotary-accuracy, generated by tf-rotary-sweep in the same tree; without it the full run stops at the start with UnrecordedRotaryCompiler (the first, receipt-less attempt of each M3 Ultra tree is in the gist as contract-full-without-rotary.log). The 3 known findings are the same three on base and on this head, all three of kind nonfinite: nonfinite metal affine_mm q4 g64, nonfinite metal affine_mm q4 g64 fp32 out, and nonfinite metal nemotron experts up oracle tf_xup_w3. The --only dsv41 runs report 0 known findings on both chips.

Device tests (zig build test-dsv41-metal-all)

M5 Ultra: build exit status 0, 1138 PASS lines, no line starting with FAIL; the log ends with failed command: ... test ... --listen=- (twice per log, once per test step) although the build exit status is 0. M3 Ultra, earlier run of the same step on this head: Build Summary: 9/9 steps succeeded; 40/40 tests passed, 1138 PASS lines, with the same failed command lines in the log. Selected M5 Ultra line:

PASS dsv41 metal experts rows: same/disjoint/mixed routing at 2-16 rows, 42 cases: act, out and y equal one row a launch
PASS dsv41 metal FP8 kernel by rows: row kernel at 1-5 rows, the MMA tile at 6-16

The base tree was not put through the device tests on either chip.

Decode, bench_concurrent aggregate tok/s, M5 Ultra, 512 GB

Order as run: base 1, this 1, [the other head twice], 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.5 59.9 59.9 56.5 1.060 1.060 1.060
2 0 chat 76.9 88.1 88.3 76.9 1.146 1.148 1.147
3 0 chat 93.1 95.2 95.2 93.0 1.023 1.024 1.023
4 0 chat 109.2 111.6 111.7 109.1 1.022 1.024 1.023
1 0 code 61.2 65.4 65.3 61.2 1.069 1.067 1.068
2 0 code 81.4 95.2 95.3 81.4 1.170 1.171 1.170
3 0 code 96.2 98.7 98.5 96.2 1.026 1.024 1.025
4 0 code 114.0 116.8 116.7 114.0 1.025 1.024 1.024
1 1 chat 62.6 66.4 66.3 62.6 1.061 1.059 1.060
2 1 chat 70.7 78.1 78.0 70.8 1.105 1.102 1.103
3 1 chat 75.0 79.1 79.1 75.0 1.055 1.055 1.055
4 1 chat 85.3 89.5 89.4 85.3 1.049 1.048 1.049
1 1 code 69.7 74.9 74.9 69.8 1.075 1.073 1.074
2 1 code 67.2 74.5 74.5 66.9 1.109 1.114 1.111
3 1 code 76.3 80.2 80.2 76.3 1.051 1.051 1.051
4 1 code 86.8 90.6 90.4 86.7 1.044 1.043 1.043

bench_openai decode tok/s (median of 3), M5 Ultra

Token hashes equal to base in all four cells across the four arms: yes. The run summary flags the two T 1 cells as unstable between the repeats inside one server, base included; their token hashes are still equal across all four arms.

prompt T base 1 this 1 this 2 base 2 this 1 / base 1 this 2 / base 2 this / base (mean over mean) tokens per chunk, base / this
fibonacci-raw 0 71.2 76.1 76.2 71.2 1.068 1.070 1.069 3.16 / 3.16
fibonacci-raw 1 67.1 73.6 73.4 67.1 1.097 1.092 1.095 3.2 / 3.2
gpu-chat-no-think 0 56.7 59.8 60.1 56.7 1.055 1.060 1.057 2.349 / 2.349
gpu-chat-no-think 1 57.7 61.2 60.9 57.7 1.061 1.056 1.058 2.393 / 2.393

Cold prompts (bench_cache --require-cold, one run per cell, reply 64 tokens, T 0), M5 Ultra

TTFT ratio below 1.000 is faster than base. Each cell is a single run and base ran before the heads (base at all four sizes, then the head of #2973, then the head of #2983), so these cells do not separate the change from drift.

prompt tokens base TTFT (s) this TTFT (s) this / base cached tokens (base / this) token_sha base token_sha this equal
2048 6.03 4.97 0.825 0 / 0 330cd07dd3cf 330cd07dd3cf yes
8192 11.90 11.93 1.003 0 / 0 b2da1b2b327d b2da1b2b327d yes
32768 37.28 37.26 0.999 0 / 0 7cfb5e60de47 7cfb5e60de47 yes
65536 70.14 69.25 0.987 0 / 0 cce981123927 cce981123927 yes

Synthetic row timing, M5 Ultra (time-dsv41-metal-synth, release-shape layers, fastest of 7, microseconds, layer 0 only). Base has no such step, so there is no base column: the table shows the head of #2973 next to this head because both were timed in the same run; the experts column is what #2983 changes.

Identical rows, 16 experts:

shape (streams x rows) head of #2973 round FP8 rows experts head of #2983 round FP8 rows experts experts, #2983 / #2973
1x1 625.6 287.3 193.6 552.3 279.1 182.5 0.943
1x2 661.5 267.1 257.0 648.2 295.7 211.0 0.821
1x3 690.0 282.4 264.8 646.8 283.8 228.1 0.861
1x4 729.8 310.9 275.4 681.4 309.6 233.5 0.848
1x5 922.6 338.1 430.5 780.9 339.0 295.3 0.686
1x6 981.7 378.0 440.5 820.9 371.1 299.3 0.679
1x8 1014.6 382.8 466.2 905.3 372.7 353.3 0.758
1x16 1446.6 486.4 850.9 1205.1 487.6 618.3 0.727
4x1 713.9 304.7 346.0 734.5 312.1 302.2 0.873

Different rows, 32 experts:

shape (streams x rows) head of #2973 round FP8 rows experts head of #2983 round FP8 rows experts experts, #2983 / #2973
1x1 590.4 260.6 193.0 586.1 269.3 194.2 1.006
1x2 695.3 260.2 320.1 642.5 260.6 280.3 0.876
1x3 821.0 284.6 404.5 754.4 285.6 351.2 0.868
1x4 866.4 308.5 436.8 815.9 311.3 401.4 0.919
1x5 1012.0 337.7 567.8 892.0 342.7 508.5 0.896
1x6 1096.9 377.9 644.9 1030.4 387.9 551.2 0.855
1x8 1210.4 379.7 674.4 1081.9 373.8 609.7 0.904
1x16 1653.4 484.1 1083.9 1442.4 488.5 848.3 0.783
4x1 780.5 309.0 417.5 782.7 309.2 368.7 0.883

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. Three cold cells were discarded for thermal state and re-run after a pause (base at 32,768 tokens: ended Heavy; base at 65,536: ended Moderate; the head of #2983 at 65,536: started Moderate), and one further attempt of base at 65,536 tokens was started and interrupted when the script was restarted (no result). The cold-size script was then re-started with end-of-cell pressure informational for the long prefills, so two kept cells ended above Nominal: base at 65,536 tokens ended Moderate and the head of #2983 at 32,768 tokens ended Heavy. The raw files of the discarded attempts were not kept, only the thermal and discard logs. The decode, bench_openai and synthetic-timing cells were not discarded. One-stream --no-drafts control cells on the base tree ran between series (4 of them are relevant here) and read 46.6 / 46.6 / 46.6 / 46.8 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. Kept cells that were not Nominal at an end, or where a macOS background process was busy: cold 65,536 tokens, base ended at Moderate thermal pressure; cold 32,768 tokens, head of #2983 ended at Heavy thermal pressure.

Not run: the full kernel contract on the M5 Ultra (it aborts, see above); the --only dsv41 contract on the base tree on the M5 Ultra; device tests on the base tree on either chip; synthetic timing on the base tree; the checkpoint's pinned revision is not recorded.

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

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. The M3 Ultra contract at b471c9479 is clean: --only dsv41 passes 97 of 97 kernels with all 192 controls red, and the full run passes 547 of 547 with the same three nonfinite known findings as the base tree. With test-dsv41-metal-all at 40/40 on that chip, the contract and rows-probe part of item 1 is done.

The M5 Ultra full-run abort doesn't come from this PR. It's main's Llama gemm_untransposed control, which you traced in #3067, and it stops the full run on main too. For this PR, the --only dsv41 line from that chip is enough.

The M5 Ultra receipt reads well. Every token_sha256 matches, every decode cell is 1.02 to 1.17 of the stack tip, and the two runs of each arm agree within 0.3 tok/s. The cold-prompt rows are single runs with base going first, and this PR doesn't touch the prompt path, so we'll treat them as unchanged rather than as a gain. Please leave TTFT out of the changelog line.

Still owed, as before:

  1. The M3 Ultra receipt under the rule 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: one discarded first run, then stack tip, this, this, stack tip.
  2. time-dsv41-metal-synth on the M3 Ultra, to confirm the pass sizes (2, 4, 6 or 8 rows for gate/up, 2 or 4 for down) or to pick sizes that are no slower on either chip.
  3. The CHANGELOG.md line, quoting the served results from both chips instead of the M5 Max synthetic percentages.
  4. Once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 has landed, a rebase onto main so CI runs, and taking the PR out of draft. It lands after dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973.
  • Synthena (Ash's Agent)

@gilby

gilby commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

M5 Ultra, 512 GB: the full kernel-contract SUMMARY that was missing above. The full run stops at registration on chips with tensor units since #2251 (#3067); with the one-line fix proposed there applied to this tree's copy, it completes on this head (b471c9479), exit status 0, macOS 27.2.1, Metal compiler 383.1:

SUMMARY 758 kernels: 758 pass gates 1-4, 0 fail, 57 known findings; 642 controls: 642 red as planted, 0 missed; 0 limit problems; gates 3719263 ms, limits 167 ms

The base tree c97d661c4, same machine, same fix:

SUMMARY 750 kernels: 750 pass gates 1-4, 0 fail, 57 known findings; 636 controls: 636 red as planted, 0 missed; 0 limit problems; gates 3760102 ms, limits 21777 ms

And --only dsv41 on this head, from the same build:

SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 192 controls: 192 red as planted, 0 missed; 0 limit problems; gates 118879 ms, limits 17 ms

The 57 known findings are the same 57 lines on the base tree and on this head, and none is a DeepSeek-V4.1 record: 41 are the #1009 masked-value entries of other families' attention kernels that this chip registers (Gemma 4, Spark2.5, MiniMax M3, Llama, GLM, the step kernels), and the rest are those families' prompt-attention entries, the affine_mm q4 g64 entries and the nemotron experts up oracle entry; the --only dsv41 run has none. The Llama control that #3067 is about goes red as planted with the fix (CONTROL control: metal llama gemm_00, the weights are read untransposed, as [k, n]: red at shapes, as planted). For comparison, the M3 Ultra full run posted earlier has 3 known findings because that chip does not register the tensor-unit kernels.

Full logs for the four trees and the applied one-line diff: https://gist.github.com/gilby/fb5f4ae45e3089e7f742c17753623740

@gilby

gilby commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

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

  • Head: b471c9479
  • Base: c97d661c4 (the head carries dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973's change, so these numbers include it)
  • 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 in every arm: 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 at temperature 0 and 1; tools/bench_cache.py --require-cold with 2,048-, 8,192- and 32,768-token prompt files, 64 reply tokens, one repeat
  • 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, per series: one discarded run (base), base, this, this, base; a cell starts only at Nominal thermal pressure after a 60 s settle and its end level is recorded; a cell that ends at Heavy is discarded and retried once, and if it ends at Heavy again it is kept and flagged
  • Server binaries (sha256 prefix): base 69a6bd19c1e9, this 927e31dc50b5

Summary: every one of the 20 cells is faster by the #2984 rule and none is slower: this over base, mean over mean, is 1.024 to 1.166 (+2.4% to +16.6%). By group: 1 and 2 streams 1.053 to 1.166, 3 and 4 streams 1.024 to 1.048, bench_openai 1.053 to 1.086. Because the head includes #2973's change, the 2-stream gains in particular are partly that PR's (see its receipt: 2-stream cells +3.6% to +5.4% there against +9.0% to +16.6% here). Token hashes equal in every cell, 0 failed or unequal comparisons. The cold first request (2,048, 8,192 and 32,768 tokens) is unchanged in TTFT within 0.6% (0.9938, 0.9977, 0.9991) with the same token hash in all four runs.

Cells under 0.98 of base: none.

bench_concurrent aggregate tok/s and bench_openai decode tok/s (median of 3 requests; the openai rows are 1 stream, load names the prompt)

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. Verdicts: 20 faster, 0 slower, 0 within noise, out of 20 cells.

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 end level (b1 / t1 / t2 / b2)
1 0 chat 43.5 45.9 45.9 43.5 1.055 1.055 1.055 faster Heavy*/Mod/Mod/Mod
2 0 chat 55.8 63.9 63.9 55.9 1.145 1.143 1.144 faster Heavy*/Mod/Mod/Mod
1 0 code 46.4 49.0 49.0 46.4 1.056 1.056 1.056 faster Heavy*/Mod/Mod/Mod
2 0 code 59.2 69.1 69.1 59.3 1.167 1.165 1.166 faster Heavy*/Mod/Mod/Mod
1 1 chat 47.7 50.3 50.2 47.7 1.055 1.052 1.053 faster Heavy*/Mod/Mod/Mod
2 1 chat 50.8 55.3 55.4 50.8 1.089 1.091 1.090 faster Heavy*/Mod/Mod/Mod
1 1 code 51.8 54.8 54.9 51.8 1.058 1.060 1.059 faster Heavy*/Mod/Mod/Mod
2 1 code 48.9 53.7 53.7 48.9 1.098 1.098 1.098 faster Heavy*/Mod/Mod/Mod
3 0 chat 68.8 70.4 70.6 68.9 1.023 1.025 1.024 faster Mod/Mod/Mod/Mod
4 0 chat 78.7 81.1 81.2 78.4 1.030 1.036 1.033 faster Mod/Mod/Mod/Mod
3 0 code 72.3 74.1 74.3 72.3 1.025 1.028 1.026 faster Mod/Mod/Mod/Mod
4 0 code 83.8 86.8 86.7 84.0 1.036 1.032 1.034 faster Mod/Mod/Mod/Mod
3 1 chat 55.8 58.6 58.4 55.8 1.050 1.047 1.048 faster Mod/Mod/Mod/Mod
4 1 chat 61.7 64.2 64.0 61.7 1.041 1.037 1.039 faster Mod/Mod/Mod/Mod
3 1 code 57.4 60.0 60.0 57.5 1.045 1.043 1.044 faster Mod/Mod/Mod/Mod
4 1 code 63.8 66.0 66.0 64.0 1.034 1.031 1.033 faster Mod/Mod/Mod/Mod
1 0 fibonacci-raw 54.5 57.5 57.6 54.4 1.056 1.059 1.058 faster Nom/Nom/Mod/Mod
1 0 gpu-chat-no-think 43.8 46.1 46.1 43.7 1.052 1.055 1.053 faster Nom/Nom/Mod/Mod
1 1 fibonacci-raw 50.3 54.6 54.6 50.2 1.085 1.087 1.086 faster Nom/Nom/Mod/Mod
1 1 gpu-chat-no-think 43.7 46.2 46.2 43.6 1.056 1.060 1.058 faster Nom/Nom/Mod/Mod

End level key: Nom = Nominal, Mod = Moderate, Heavy = Heavy, * = ended at Heavy twice, kept and flagged.

Exactness: token hashes are equal across all four runs in every one of the 20 cells (all reps of each cell compared), with 0 failed requests and 0 --alone / --serial mismatches. Checked against the raw JSON: hash equality over the four runs of each cell, and the failed, alone and serial counts over every run including the discarded ones.

Cold first request (bench_cache --require-cold)

First request only; bench_cache also sends a repeat and a concurrent pair on the same server, and with the prompt cache off those are cold too (cached_tokens 0 in all of them); they are in the JSON. Order as run: base, this, this, base. The 2,048-token series started with a discarded first run (base) that is not shown.

prompt tokens arm first token (s) cached tokens token_sha decode tok/s end level
2048 base, run 1 7.06 0 330cd07dd3cf 47.9 Nominal
2048 this, run 1 7.01 0 330cd07dd3cf 50.7 Nominal
2048 this, run 2 7.04 0 330cd07dd3cf 50.6 Nominal
2048 base, run 2 7.08 0 330cd07dd3cf 47.8 Nominal
8192 base, run 1 14.84 0 b2da1b2b327d 45.5 Nominal
8192 this, run 1 14.80 0 b2da1b2b327d 48.2 Moderate
8192 this, run 2 14.79 0 b2da1b2b327d 48.3 Moderate
8192 base, run 2 14.82 0 b2da1b2b327d 44.9 Moderate
32768 base, run 1 43.18 0 0e3f2ebade35 34.8 Heavy twice (flagged)
32768 this, run 1 43.16 0 0e3f2ebade35 37.5 Heavy twice (flagged)
32768 this, run 2 43.16 0 0e3f2ebade35 37.5 Heavy twice (flagged)
32768 base, run 2 43.22 0 0e3f2ebade35 34.9 Heavy twice (flagged)

TTFT over the mean of the two base runs, mean of the two this runs: 2,048 tokens 0.9938, 8,192 tokens 0.9977, 32,768 tokens 0.9991; token hashes equal in all four runs of each prompt length. The cold decode rate in the same requests is higher on this head (for example 50.7 and 50.6 against 47.9 and 47.8 at 2,048 tokens), single runs. All four 32,768-token cells ended at Heavy twice and are flagged: each ended at Heavy on its first attempt, was discarded and re-run once after cooling, and ended at Heavy again; the second attempt is kept. On this machine a cold prefill of that size ends at Heavy whichever arm runs.

Discarded, retried and flagged cells

  • The first run of every series is the discarded run and is in no table (series d-experts-c12, d-experts-c34, d-experts-oai, and the 2,048-token cold series).
  • Flagged, in the tables: base 1 of the 1-and-2-stream series (d-experts-c12-base1) ended at Heavy twice (first try discarded, second kept and flagged). It is the Heavy-affected base in the 8 cells of that series; base 1 and base 2 agree within 0.2% in every one of them (43.5/43.5, 55.8/55.9, 46.4/46.4, 59.2/59.3 and so on), and both this runs are above both base runs in every cell, so the verdicts do not depend on it.
  • Retried and kept: base 1 of the 3-and-4-stream series (d-experts-c34-base1) ended at Heavy on its first try, was discarded and re-run once; the second try ended at Moderate and is the one in the table.
  • Flagged, not in any table: the discarded first runs (disc) of the 1-and-2-stream and the 3-and-4-stream series each ended at Heavy twice (their first tries and re-runs are in the gist; they are not used).
  • Flagged, in the cold table: all four 32,768-token cells (see above).
  • Every other kept cell ended at Nominal or Moderate. The first try's raw files for each discarded cell are in the gist as .try1 files.

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 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: 1 and 2 streams in one series, 3 and 4 in another where noted) and per temperature (bench_openai). A 2-minute multi-stream cell that starts at Nominal usually ends at Moderate on this machine, so Moderate does not discard a cell; only an end at Heavy does: the cell is then discarded, cooled to Nominal and re-run once, and if it ends at Heavy again it is kept and flagged. Tools and server flags are the ones in the header. Every arm, the raw JSON, the server logs and the driver scripts are in the gist.

Not run: the checkpoint's pinned revision, the macOS version and the toolchain versions are not recorded; a base-versus-head comparison without #2973's change underneath; kernel-contract runs are not part of this series' data.

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

@gilby

gilby commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

New head: 10676dba8 (one commit on top of b471c9479, not rebased).

Receipts: the new commit is the CHANGELOG, so the M5 Ultra and M3 Ultra receipts and contract lines above (taken at b471c9479) describe this head's code. The checkpoint in both receipts is deepseek-ai/DeepSeek-V4.1-Flash-1017 at revision 1bae0e1088993b9b82de08f59de5be5a34e42170.

Your other items from 12 Nov 23:50:

  1. The M3 Ultra receipt under 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: posted on 13 Nov (all 20 cells faster, 1.024 to 1.166, none slower, every token hash equal).
  2. time-dsv41-metal-synth on the M3 Ultra, to confirm the pass sizes (2, 4, 6 or 8 rows for gate/up, 2 or 4 for down) or choose sizes no slower on either chip: still owed. If the M3 Ultra's experts column prefers other sizes, the change goes in its own commit with both chips' rows probe and contract.
  3. The rebase onto main and out of draft: I am not pushing a rebase of a branch under review to follow main; this head stays on c97d661c4 with dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973 under it and is ready to be rebased at landing after dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973. In a local rebase of the stack onto today's main the commit applies with a conflict only in CHANGELOG.md. I'll take it out of draft once the synthetic run above is in.

At 10676dba8: CHANGELOG only since b471c9479, where test-dsv41-metal-all passed on the M5 Max and the M3 Ultra (40/40); lean_check.py 345 on base and head; pr_check.py --pr 2983 0 errors and 0 warnings.

@gilby

gilby commented Nov 14, 2026

Copy link
Copy Markdown
Contributor Author

M3 Ultra, 512 GB: the time-dsv41-metal-synth round tables that the earlier comment said were still owed, at head 10676dba8.

Each figure is the fastest of 7 timed runs of one synthetic round (release-shape layers, a 40-layer round weighted by layer kind, no head, Engram or host), one run of the tool per table: these are single samples, not a bracketed A/B, and the tool's per-kernel columns are measured by differencing, so small columns can read slightly negative per layer.

The 40-layer round at this head, 16 experts, identical rows:

launch (slots x rows) round, concurrent (us) round, alone (us) FP8 rows (us) experts (us) attention (us) glue (us)
1 x 1 25,583.2 30,440.3 11,976.5 9,171.0 3,187.0 6,105.8
1 x 2 27,225.5 32,113.7 12,785.2 10,103.3 3,172.8 6,052.4
1 x 3 30,770.7 35,925.0 13,524.2 12,648.6 3,442.0 6,310.2
1 x 4 32,388.3 37,918.4 15,151.6 12,749.2 3,801.9 6,215.7
1 x 5 38,981.0 44,623.6 17,094.8 16,973.7 3,937.0 6,618.0
1 x 6 43,294.7 48,501.2 19,829.3 17,037.9 4,742.2 6,891.8
1 x 8 48,609.4 53,830.6 19,892.2 20,910.9 5,950.2 7,077.3
1 x 16 75,779.3 81,973.8 25,721.0 37,375.9 10,364.5 8,512.5
4 x 1 33,569.5 47,404.1 15,120.9 12,896.1 12,074.8 7,312.3

The same with 32 experts and different rows (TF_DSV41_TIMING_DIFFERENT=1 TF_DSV41_SYNTH_EXPERTS=32):

launch (slots x rows) round, concurrent (us) round, alone (us) FP8 rows (us) experts (us) attention (us) glue (us)
1 x 1 25,235.5 30,136.7 11,931.9 9,144.1 3,203.5 5,857.1
1 x 2 29,953.6 35,599.9 12,712.9 13,930.2 3,152.8 5,804.0
1 x 3 33,859.4 39,715.4 13,522.2 16,786.0 3,421.1 5,986.1
1 x 4 37,526.5 44,097.4 15,085.6 19,155.7 3,769.1 6,087.1
1 x 5 43,628.5 49,695.0 17,026.5 22,281.3 3,917.7 6,469.5
1 x 6 50,591.6 56,443.5 20,369.1 24,563.2 4,730.6 6,780.6
1 x 8 55,014.0 60,506.3 20,124.7 27,529.3 5,908.6 6,943.6
1 x 16 88,364.5 91,423.3 25,661.5 47,226.1 10,298.5 8,237.2
4 x 1 38,838.3 53,042.1 15,076.7 19,053.6 11,939.2 6,972.6

This head against the #2973 head, round totals and the experts column (recomputed from the two output files of each head). Identical rows, 16 experts:

launch (slots x rows) round: #2973 head (us) round: this head (us) change experts: #2973 head (us) experts: this head (us) change
1 x 1 25,013.0 25,583.2 +2.3% 9,095.0 9,171.0 +0.8%
1 x 2 29,962.1 27,225.5 -9.1% 13,701.4 10,103.3 -26.3%
1 x 3 31,690.5 30,770.7 -2.9% 14,085.0 12,648.6 -10.2%
1 x 4 34,013.7 32,388.3 -4.8% 14,864.8 12,749.2 -14.2%
1 x 5 45,058.9 38,981.0 -13.5% 23,098.1 16,973.7 -26.5%
1 x 6 49,467.4 43,294.7 -12.5% 23,745.4 17,037.9 -28.2%
1 x 8 52,155.8 48,609.4 -6.8% 24,899.6 20,910.9 -16.0%
1 x 16 84,007.2 75,779.3 -9.8% 45,873.4 37,375.9 -18.5%
4 x 1 35,135.3 33,569.5 -4.5% 14,744.2 12,896.1 -12.5%

Different rows, 32 experts:

launch (slots x rows) round: #2973 head (us) round: this head (us) change experts: #2973 head (us) experts: this head (us) change
1 x 1 25,378.7 25,235.5 -0.6% 9,205.8 9,144.1 -0.7%
1 x 2 33,747.4 29,953.6 -11.2% 17,193.4 13,930.2 -19.0%
1 x 3 35,624.5 33,859.4 -5.0% 19,522.7 16,786.0 -14.0%
1 x 4 39,156.1 37,526.5 -4.2% 21,404.4 19,155.7 -10.5%
1 x 5 47,818.4 43,628.5 -8.8% 27,671.3 22,281.3 -19.5%
1 x 6 55,600.9 50,591.6 -9.0% 30,693.2 24,563.2 -20.0%
1 x 8 61,380.2 55,014.0 -10.4% 34,379.9 27,529.3 -19.9%
1 x 16 97,075.9 88,364.5 -9.0% 58,689.6 47,226.1 -19.5%
4 x 1 41,113.5 38,838.3 -5.5% 21,568.5 19,053.6 -11.7%

At 16 rows the round goes 84,007 to 75,779 us with identical rows (-9.8%) and 97,076 to 88,364 us with different rows (-9.0%); the experts column is 45,873 to 37,376 us and 58,690 to 47,226 us. The FP8 rows column is the same at both heads within 161 us (identical rows) and 107 us (different rows), as it should be since only the experts differ.

This run times the pass sizes as shipped and not other sizes, so it shows the gain but does not say whether another pass size (2, 4, 6 or 8 rows for gate/up, 2 or 4 for down) would be faster on this chip; I have not timed those, so I am not proposing a change of sizes from it.

Raw outputs and drivers: https://gist.github.com/gilby/dec04808f0294eb3a588c9c1b80af0ad

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