Skip to content

dsv41 metal: a multi-stream round's per-slot stages run as one dispatch each (stacked on #1720) - #2954

Draft
gilby wants to merge 22 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/05-slot-dispatch
Draft

gilby wants to merge 22 commits into
ashhart:mainfrom
gilby:dsv41-metal-opt/05-slot-dispatch

Conversation

@gilby

@gilby gilby commented Nov 11, 2026 •

Copy link
Copy Markdown
Contributor

What this changes

On Metal, a DeepSeek-V4.1 decode round with several streams ran one dispatch a stream for the window key, the KV source writes, decode attention, and the indexer's scorer and candidates, with the indexer taking a barrier a stream. With this change a round with two or more streams runs each of those stages as one dispatch over a per-layer slot table, and the indexer takes the same barriers as a one-stream round. Each kernel's per-row body is moved unchanged into an inline function that both the per-slot kernel and the new *_segs kernel call, so a row's arithmetic and order are the same and a row has the same bits alone or with others. One-stream rounds run exactly the old code.

What to expect: the dispatches inside these loops go from 114 a stream to 114 a round, and the indexer's barriers from 9 a stream plus 8 to 17. The attention-side loops had no barriers between streams, so only the indexer's really held the GPU up; my estimate for the release model is a few percent at four streams, and the receipt will say.

Checks on this head

Receipt

  • Main against branch: one stream level and three and four streams faster on both chips, every token unchanged. M5 Ultra: 1 stream 0.999 to 1.002, 3 and 4 streams 1.015 to 1.031 with drafts and with --no-drafts. M3 Ultra (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): 3 and 4 streams 1.008 to 1.028, 6 of 8 cells faster, the rest within noise, none slower; 1 and 2 streams 0.999 to 1.009. Receipts: https://gist.github.com/gilby/c93f79e41189fbfcbc3674117fa12855 (M5 Ultra), https://gist.github.com/gilby/1da4b87d59800ff945461fb7b81efc52 (M3 Ultra), measured at c6d2413c7; cd32ace02 touches a test and the package's export list only, c469d807b the CHANGELOG only.
  • Boxes it reaches: Apple Silicon Macs serving DeepSeek-V4.1 natively, in rounds with two or more streams.
  • New switch? Default, accepted values, profile row: none.
  • Environment: measured at c6d2413c7 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 and the contract on an M5 Max. Not run: M3, the release weights.
  • Exactness: token_sha256 equal to the stack tip in every cell of every block on both chips, 0 failed, 0 --alone / --serial mismatches; the contract holds every per-slot kernel byte-equal to the base's body.
  • 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 (1 stream) 0.999 to 1.001 on both chips.
  • Prompt speed before and after: n/a, the prompt path is not touched.
  • Licence of any new weights or drafter: n/a.
Kernel contract
  • Kernels registered with the contract, and the kernel each one replaces: tf_dsv41_f_kv_segs (tf_dsv41_f_kv), tf_dsv41_c_pool2_segs (pool2 and its two scatters), tf_dsv41_c_fp4rt_segs, tf_dsv41_c_fp4pack_segs, tf_dsv41_attn_dec_segs_bf16 and its prefetch form, tf_dsv41_ix_scores_dec_segs, tf_cand_segs. Each is checked bit-equal to its per-slot kernel launched a slot at a time: the attention side at 1, 2, 3 and 4 slots and rows 1, 2, 4, 16 and 17, heads 4 and 64; the indexer at both ratios, with and without the candidate mask; gate 1 the same with NaN and Inf planted. The per-slot kernels keep their float64 registrations and now run the shared body.
  • A planted control for each new kernel, and each one fails as planted: TF_CTL 6 (every row takes slot 0's state) on each new kernel except the prefetch attention; seven controls, all red at the shapes gate. At TF_CTL 0 the source is what ships.
  • Each backend's SUMMARY line and exit status, M5 Max: base SUMMARY 89 kernels: 89 pass gates 1-4, 0 fail, 0 known findings; 186 controls: 186 red as planted, 0 missed; 0 limit problems; this head SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 193 controls: 193 red as planted, 0 missed; 0 limit problems. M5 Ultra at c6d2413c7, exit 0: --only dsv41 SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 193 controls: 193 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 kernels pass, 57 known findings, the same 57 as base. M3 Ultra at c6d2413c7, exit 0: --only dsv41 97 of 97 pass, 193 of 193 controls red; full run 547 pass, the same three known nonfinite findings as base. cd32ace02 and c469d807b change no kernel or record.
  • 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 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).
…, with its dispatches and barriers a layer

time-dsv41-metal-slots records each layer's encode (dispatches, barriers) and the median wall time of a
round over synthetic weights, at 1, 2 and 4 slots of one row and one slot of four rows, so the cost of
the per-slot loops shows without the release checkpoint.
…ssor writes and decode attention

A decode launch over several slots ran the window key (tf_dsv41_f_kv), the ratio-2 pool and the two
scatters of its kept inputs, the FP4 round trip into the compressed rows, the index-key packing and the
decode attention once a slot, because each slot's state lives in buffers of its own. Each of these now
has a slot form (*_segs) that indexes a per-layer table of the slots' buffers (segs.zig, written as the
layer is encoded, every buffer it reaches declared with useResource) by the row's slot (Meta.rowseg).

The per-row body of every kernel moved into an inline function the per-slot kernel and its slot form
both call, so a row runs the same instructions alone or beside other slots; launches of one slot keep
the per-slot kernels. The pool's slot form copies the kv and score words it keeps in place of the two
scatters. The attention picks its prefetch form when no slot has more than two rows.

Contract: each slot kernel is registered as a replacement of its per-slot kernel, bit-equal to it
launched a slot at a time at 1, 2, 3 and 4 slots of 1 to 17 rows (heads 4 and 64), NaN and infinities
included, with a TF_CTL 6 control (every row reading or writing the first slot's state) that goes red
at the shapes gate for all but the prefetch attention.
…idate blocks and top-512

An index-source layer of a launch over several slots ran the decode scorer, a barrier, the candidate
blocks (at the candidate source), a barrier and the top-512 once a slot. With the layer's slot table
the scorer (tf_dsv41_ix_scores_dec_segs) runs every row over its slot's keys, key count and first
position, the candidates (tf_cand_segs) over its slot's key count, leaving a slot with no keys as the
per-slot path does, and the unchanged top-512 over all rows; one barrier between stages instead of
one a slot. The merged path keeps every row's scores at a fixed stride (the scratch's key width), so
the top-512 reads each row's first `visible` scores exactly as before; launches of one slot keep the
per-slot code.

As for the attention side, each kernel's row body is an inline function both forms call. Contract:
both registered as replacements, bit-equal to the per-slot launchers a slot at a time at 1 to 4 slots
of 1 to 17 rows, ratios 1 and 2, with and without a candidate mask, NaN and infinities included, each
with a TF_CTL 6 control (the first slot's keys or key count for every row) red at the shapes gate.
@ashhart

ashhart commented Nov 11, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. This is careful work. Each per-slot body moved unchanged into a function both kernels call, every slot buffer is reached through its own table entry, ragged slots stop at their own key counts while the top-512 only reads each row's visible keys, and the contract plants a cross-slot control on the new kernels. No need to split it: the two halves share the slot table. Asks:

  1. Please post the tf-kernel-contract --only dsv41 SUMMARY from the M3 Ultra and the M5 Ultra. The one in the body is from an M5 Max, and the release only runs on the two Ultras.
  2. The one-stream path's kernels changed too (tf_dsv41_f_kv, the compressor kernels, tf_dsv41_attn_dec*, tf_dsv41_ix_scores_dec, tf_cand now call the moved bodies). The served tests compare with a serial reference that runs the same new kernels, so the receipt's one-stream token_sha256 against the stack tip is what shows the bits didn't move. Please include it at T 0 and T 1 on both chips.
  3. In the receipt, the 3- and 4-stream cells matter most. Those rounds are plain under dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's switch, and that is where dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 is still 3 to 4.5% behind --no-drafts. Please give --no-drafts and the default side by side there, so we can see whether this closes it.

Two pointers, not blockers. segs.table (metal/segs.zig:68) declares 5-7 resources a slot a layer, about 900 calls a round at four slots, on the encode time this PR shortens. One useResources call per encoder would avoid that. Also, TfDsv41Seg is defined in five kernel files against one Zig struct. A shared definition would stop them drifting apart.

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. Arms base, (another branch), this, this, base; tools/bench_concurrent.py --levels 1,2,3,4 --tokens 128 --reps 3 --temperatures 0,1 --alone --serial. Every token_sha256 equals base; concurrent equals alone equals serial. This branch over the mean of three base runs:

N T0 chat T0 code T1 chat T1 code
1 1.001 0.979 0.980 1.000
2 0.999 0.992 0.993 0.987
3 0.999 1.016 0.998 0.998
4 1.006 1.028 1.007 1.001

Four streams of greedy code go from 83.8 to 83.9 tok/s to 86.1 and 86.3, and three from 72.2 to 73.3 and 73.5; the rest is level. The two 0.98 cells at one stream are against a base mean lifted by its first run (47.1 against 45.1 and 45.5 in the later two); one-stream rounds run the old kernels here, and this branch read 44.8 and 45.1 on code and 52.2 on sampled code, the same as the later base runs. tools/bench_openai.py: code 50.6 against 50.8 to 51.3, chat 40.5 against 40.7 to 41.0.

So the gain is small, as the description expected: up to about 3% at four greedy streams, where routing is shared and these stages are a larger share of the round. An M5 Ultra run follows. I'd understand if this is not worth eight kernels on its own; it is the first half of making rows cheap, and the FP8 rows on the matrix tile are the larger half.

…sources call

segs.table declared every buffer a layer's slot kernels reach with its own useResource call, five to
seven a slot a layer. segs.declare now hands every region buffer of the launch's slots (each layer's
window, and the pools, index keys, carries and kept inputs of the KV sources) to the encoder in one
useResources:count:usage: call, made by forward.encode and by forward.run for each command buffer
before its first layer; a launch of one slot declares nothing, as before. ComputeEncoder gains
useResources beside useResource.

time-dsv41-metal-slots also reports the median host encode of a whole round (a command buffer
encoded, never committed).
… and the per-slot bodies held to the base

The slot table entry was declared in each of the five kernel sources against one Zig struct. It now
lives in deepseek_v41/segs.h, which sources.zig puts before fused, compress, csa2_attn, index and cand
(as kv_pages.h is shared); the contract compiles the header with a kernel that writes sizeof and every
field offset and checks them against segs.zig's Seg, which the contract probes now use too.

After the gates the contract also launches each per-slot kernel that now calls a moved body
(tf_dsv41_f_kv, the pool, round trip and key packing, tf_dsv41_attn_dec in both prefetch forms,
tf_dsv41_ix_scores_dec and tf_cand) and the base's body of it on the same inputs, random and with
non-finite plants, at every probe shape, and requires the same bytes: 164 cases. The base's kernels
are kept only in the test tree (tests/contract/dsv41_base), appended to today's source under tf_base_
names, so they share its helpers and nothing ships twice.
@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Head is now c6d2413c7 (two commits on top; not rebased). The receipts you asked for are queued on both Ultras; the code side of your points:

  1. The --only dsv41 SUMMARY from the M3 Ultra and the M5 Ultra: queued on both, and I'll post the two lines here. On an M5 Max at this head it is unchanged: SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 193 controls: 193 red as planted, 0 missed; 0 limit problems.
  2. The one-stream kernels: you are right that nothing compared them with the base's source, and most of their registrations are float64-bounded, so a few-ulp drift would have passed. The contract now does: the base's bodies of tf_dsv41_f_kv, pool2, fp4rt, fp4pack, the four tf_dsv41_attn_dec instances, tf_dsv41_ix_scores_dec and tf_cand are kept in the test tree only (tests/contract/dsv41_base/), compiled under tf_base_ names beside today's source, and every probe shape is run through both with random and non-finite inputs: 120 cases and 44 indexer cases, 0 differ. Planting a factor in a kept body turns it red. On the served side, the M3 Ultra receipt above has every token_sha256 equal to the stack tip's at one stream, T 0 and T 1; the M5 Ultra's follows.
  3. --no-drafts and the default side by side at 3 and 4 streams on both chips: queued. One correction to the premise, since it changed after your note: with the copy-window fix in dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 (c97d661c4), the default is already within 0.4% of --no-drafts at 3 and 4 streams on both chips, so there is no gap left for this to close; what it can show is both arms moving together.

Your two pointers are done. The slots' buffers are declared once per encoder in one useResources call (segs.declare, called from forward.encode and once per command buffer in forward.run; a pure addition to the Metal wrapper), and segs.table no longer declares anything. On the seven-layer synthetic model the encode time moves by 2 to 3 µs at four slots, inside the noise there; the release model's roughly 900 calls a round are what the Ultra receipts will show. TfDsv41Seg now lives in one header (segs.h) prepended to the five kernel sources the way kv_pages.h is, and the contract compiles it with a kernel that writes back its size and every field offset and compares them with the Zig struct: size 72 and 11 offsets, 0 differ.

One thing I could not produce: the contract with no --only stops during registration with ControlEditMissing before any SUMMARY, on this head and identically on the base, so the dsv41 run is the one I can give you.

At this head on an M5 Max: test-dsv41-metal-all 9/9 steps and 38/38, test-kernel-contract 32/32 steps, lean_check.py 345 on base and head, pr_check.py --pr 2954 0 errors and 0 warnings. The body's issue line now points at #2984.

@gilby

gilby commented Nov 12, 2026

Copy link
Copy Markdown
Contributor Author

Thanks. Head is now c6d2413c7 (two commits on top of the one you read; not rebased). Your two pointers are in, and the first Ultra numbers.

Pointers. The slots' buffers are now declared once per encoder in one useResources call (segs.declare, called from forward.encode and once per command buffer in forward.run; segs.table declares nothing), and TfDsv41Seg lives in one header, kernels/metal/deepseek_v41/segs.h, prepended to the five sources as kv_pages.h is; the contract compiles it and compares its size and 11 field offsets with the Zig struct.

Your item 2, the one-stream path: you are right that the served tests could not show it, and most per-slot registrations are float64-bounded. So the contract now keeps the stack tip's bodies of tf_dsv41_f_kv, the compressor kernels, the four tf_dsv41_attn_dec instances, tf_dsv41_ix_scores_dec and tf_cand in the test tree only and requires today's per-slot kernels to match them byte for byte on every probe shape, random and non-finite: 120 cases and 44 indexer cases, 0 differ (a planted change in a kept body turned it red). On the served model the one-stream token_sha256 at T 0 and T 1 equals the stack tip's on the M3 Ultra (the table above) and on the M5 Ultra (below).

Item 1, the contract on the Ultras. M5 Ultra at 1db9200c5 (tf-kernel-contract --only dsv41): SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 193 controls: 193 red as planted, 0 missed; 0 limit problems. The M3 Ultra run, and both chips at c6d2413c7, are queued; M5 Max at c6d2413c7 gives the same line.

M5 Ultra receipt at 1db9200c5. 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 stack tip (a43ad7b98), this, this, stack tip. Greedy, aggregate tok/s, this over the stack tip:

streams code chat
1 61.3 / 61.3 56.6 / 56.5
2 81.9 / 81.6 (1.004) 77.4 / 77.0 (1.005)
3 98.2 / 96.2 (1.020) 94.9 / 93.0 (1.020)
4 117.0 / 114.0 (1.026) 111.9 / 109.2 (1.026)

Item 3: --no-drafts and the default side by side at 3 and 4 streams are running on both chips at this head and I'll post them here. The issue line in the body now points at #2984.

At c6d2413c7 on an M5 Max: test-dsv41-metal-all 9/9 steps and 38/38, test-kernel-contract 32/32 steps, lean_check.py 345 on base and head, pr_check.py --pr 2954 0 errors and 0 warnings.

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby, and for the M5 Ultra receipt. Both pointers are in at c6d2413c7. segs.declare (metal/segs.zig:43-62) hands every slot buffer to the encoder in useResources calls. TfDsv41Seg lives in one header, segs.h, and the contract checks its layout against the Zig struct. Keeping the stack tip's bodies in zig/tests/contract/dsv41_base/ and requiring today's per-slot kernels to match them byte for byte answers our item 2 better than the served tests could.

On item 3, agreed. With #1720's copy-window fix at c97d661c4, the default is already level with --no-drafts at 3 and 4 streams, so the side-by-side isn't needed for this PR. The comparison with the stack tip you posted is the one that counts.

Asks:

  1. segs.table no longer declares anything, so every encoder that runs a layer over several slots now has to call segs.declare first. Only forward.encode and forward.run do. zig/tests/dsv41_metal/real_test.zig:116-133 (timeLaunch, called with four slots at :161) runs pkg.layer.encode over four slots without it, so its slot kernels read buffers that were never declared to the encoder. The four-slot path in dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951's timing_real.zig (which this rebases onto) and the four-slot shape in dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973's timing_synth.zig (which rebases onto this) have the same gap. Please make it hard to miss. One option is to declare in the layer path: one useResources call for the layer's slot buffers, and its KV source's, when attn.encode builds its table (attn.zig:39). That is one call a layer, still far fewer than the ~900 single calls before. The other is to keep the per-encoder declare and add it to those callers.
  2. The contract SUMMARY from the M3 Ultra at the final head, with the base-body comparison's case counts. The M5 Ultra line you posted is from 1db9200c5, before the two new commits, so please post one at the final head as well.
  3. Once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 has landed, rebase onto the PR before this one in the landing order (dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951, dsv41 metal: prompts in pieces between the other streams' rounds (stacked on #1720) #2950, then this; dsv41 metal: Engram rows are warmed ahead and joined at their layer's command buffer (stacked on #1720) #2953 is closed). Your commits apply cleanly on dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720's c97d661c4. Against dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951 the only overlap is the import line in forward.zig. The segs.declare call then goes inside runTimed's loop.
  • 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: c6d2413c7
  • 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_concurrent.py --tokens 128 --reps 3 --temperatures 0,1 --alone --serial at --levels 1,3,4 (block A) and --levels 1,2,3,4 (block B); tools/bench_openai.py --tokens 256 --reps 3 --temperatures 0,1 (block B); zig build test-dsv41-metal-all; zig build tf-kernel-contract tf-rotary-sweep, tf-rotary-sweep, tf-kernel-contract --only dsv41 --rotary-accuracy <sweep json> and the full tf-kernel-contract --rotary-accuracy <sweep json>
  • 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): block A (default drafts and --no-drafts, built in a second tree): base --no-drafts, this --no-drafts, base default, this default, this --no-drafts, base --no-drafts, this default, base default; block B (default drafts, first build of the head): base, this, this, two runs of a different change that is not part of this PR, base
  • Builds: base binary sha256 d4689bd4b9ea2581…; this head built twice: 9581835d3d3fe489… (block B) and 06c8659fb33d3bbd… (block A and the second contract run); arm labels in the gist file names are explained in its README

Summary: token hashes equal to base in every cell of every block. At one stream this head is level with base (mean over mean 0.999 to 1.000 with default drafts, 1.001 to 1.002 with --no-drafts, block A); at 3 and 4 streams it is faster by 1.7 to 3.1% (1.017 to 1.031 default, 1.018 to 1.031 --no-drafts). Block B (1 to 4 streams, default drafts, the first build) agrees: 1 stream 0.999 to 1.002, 2 streams 1.004 to 1.007, 3 and 4 streams 1.015 to 1.026. The M5 Ultra --only dsv41 kernel contract is clean (0 failures, 0 known findings, 193 of 193 controls red as planted).

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; 193 controls: 193 red as planted, 0 missed; 0 limit problems; gates 100152 ms, limits 191 ms

A second build of the same head in a second tree gives (verbatim):

SUMMARY 97 kernels: 97 pass gates 1-4, 0 fail, 0 known findings; 193 controls: 193 red as planted, 0 missed; 0 limit problems; gates 101355 ms, limits 189 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 base tree a43ad7b98. 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; 193 controls: 193 red as planted, 0 missed; 0 limit problems; gates 119933 ms, limits 18 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; 582 controls: 582 red as planted, 0 missed; 0 limit problems; gates 2612269 ms, limits 187 ms

For comparison, the base tree on the M3 Ultra (the tree at c97d661c4; that is the base of #2973 and #2983, while the base of this PR is a43ad7b98), 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 for both builds of the head; 1135 PASS lines (second build: 1135); no line starting with FAIL. The log has no N/N tests passed summary line; it ends with failed command: ... test ... --listen=- (it appears twice in each log, once for each of the two test steps) although the build exit status is 0. Selected lines:

PASS dsv41 metal multi-slot forward: 2 slots, 8 rows in one launch vs one launch a slot: 0 logits, 0 tap values, 0 state bytes differ
PASS dsv41 metal multi-slot forward: 3 slots, 7 rows in one launch vs one launch a slot: 0 logits, 0 tap values, 0 state bytes differ
PASS dsv41 metal multi-slot forward: 4 slots, 16 rows in one launch vs one launch a slot: 0 logits, 0 tap values, 0 state bytes differ
PASS dsv41 metal multi-slot forward: 4 slots, 4 rows in one launch vs one launch a slot: 0 logits, 0 tap values, 0 state bytes differ
PASS dsv41 metal multi-slot forward: 4 slots, 16 rows in one launch vs one launch a slot: 0 logits, 0 tap values, 0 state bytes differ
PASS dsv41 metal multi-slot verify rounds: 64 choices, 4 launches in one vs 16 apart, 0 state bytes differ

M3 Ultra device tests for this head: not run.

Decode, bench_concurrent aggregate tok/s: block A (second build of the head), default drafts

Order as run: base 1, this 1, this 2, base 2 (the full block order is in the header). Token hashes equal in every cell across the four arms: yes; failed / alone / serial mismatches: 0. All 12 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.6 56.6 56.5 56.6 1.000 0.998 0.999
3 0 chat 93.1 95.2 95.1 93.0 1.023 1.023 1.023
4 0 chat 109.2 112.3 112.3 109.1 1.028 1.029 1.029
1 0 code 61.3 61.2 61.2 61.2 0.998 1.000 0.999
3 0 code 96.3 98.6 98.5 96.2 1.024 1.024 1.024
4 0 code 114.0 117.5 117.5 114.0 1.031 1.031 1.031
1 1 chat 62.8 62.8 62.7 62.8 1.000 0.998 0.999
3 1 chat 74.8 76.3 76.4 74.9 1.020 1.020 1.020
4 1 chat 85.3 87.1 87.1 85.3 1.021 1.021 1.021
1 1 code 69.5 69.6 69.3 69.4 1.001 0.999 1.000
3 1 code 75.6 76.8 76.8 75.5 1.016 1.017 1.017
4 1 code 85.0 86.7 86.5 84.9 1.020 1.019 1.019

Block A, --no-drafts

Token hashes equal in every cell across the four arms: yes; failed / alone / serial mismatches: 0. All 12 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 46.6 46.7 46.6 46.6 1.002 1.000 1.001
3 0 chat 93.2 95.3 95.4 93.1 1.023 1.025 1.024
4 0 chat 109.4 112.6 112.6 109.3 1.029 1.030 1.030
1 0 code 46.7 46.8 46.8 46.7 1.002 1.002 1.002
3 0 code 96.3 98.7 98.8 96.3 1.025 1.026 1.025
4 0 code 114.2 117.7 117.7 114.2 1.031 1.031 1.031
1 1 chat 46.2 46.3 46.3 46.2 1.002 1.002 1.002
3 1 chat 75.1 76.5 76.5 75.1 1.019 1.019 1.019
4 1 chat 85.4 87.3 87.3 85.5 1.022 1.021 1.022
1 1 code 46.3 46.4 46.4 46.3 1.002 1.002 1.002
3 1 code 76.4 77.8 77.8 76.4 1.018 1.018 1.018
4 1 code 86.9 88.8 88.8 86.9 1.022 1.022 1.022

Block B (first build of the head), default drafts, 1 to 4 streams

Order as run: base 1, this 1, this 2, then two runs of a different change (not reported here), then base 2. Token hashes equal in every cell: 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 56.6 56.6 56.5 1.002 1.002 1.002
2 0 chat 77.0 77.4 77.4 77.0 1.005 1.005 1.005
3 0 chat 93.0 94.9 94.9 93.0 1.020 1.020 1.020
4 0 chat 109.2 111.9 111.9 109.0 1.025 1.027 1.026
1 0 code 61.3 61.3 61.3 61.4 1.000 0.998 0.999
2 0 code 81.6 81.9 81.9 81.5 1.004 1.005 1.004
3 0 code 96.2 98.2 98.3 96.3 1.021 1.021 1.021
4 0 code 114.0 117.0 116.9 114.0 1.026 1.025 1.026
1 1 chat 62.6 62.5 62.5 62.5 0.998 1.000 0.999
2 1 chat 70.9 71.1 71.3 70.9 1.003 1.006 1.004
3 1 chat 75.0 76.2 76.2 75.0 1.016 1.016 1.016
4 1 chat 85.4 86.9 86.9 85.2 1.018 1.020 1.019
1 1 code 69.8 69.7 69.7 69.5 0.999 1.003 1.001
2 1 code 67.1 67.4 67.3 66.7 1.004 1.009 1.007
3 1 code 75.8 76.8 76.9 75.7 1.013 1.016 1.015
4 1 code 84.9 86.3 86.4 84.7 1.016 1.020 1.018

bench_openai decode tok/s (median of 3), block B

Token hashes equal to base in all four cells across the four arms: yes.

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.4 71.3 71.3 71.2 0.999 1.001 1.000 3.16 / 3.16
fibonacci-raw 1 67.2 67.1 67.2 67.1 0.998 1.000 0.999 3.2 / 3.2
gpu-chat-no-think 0 56.7 56.8 56.8 56.7 1.001 1.001 1.001 2.349 / 2.349
gpu-chat-no-think 1 57.9 57.9 57.9 57.8 1.000 1.002 1.001 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. One cell was discarded and re-run: base run 2 of block B (first attempt ended at Moderate thermal pressure; its raw file was not kept, only the thermal line and the discard log). Block A and the other block B cells were Nominal at both ends. 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. Kept cells that were not Nominal at an end, or where a macOS background process was busy: block A, base --no-drafts run 1 a macOS background process (mediaanalysisd) was using 17% CPU at the end.

Not run: M3 Ultra device tests; cold-prompt cells for this head; bench_openai for block A.

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

@ashhart

ashhart commented Nov 12, 2026

Copy link
Copy Markdown
Owner

Thanks @gilby. The M5 Ultra receipt at c6d2413c7 is clear. One stream is level (0.999 to 1.002). Three and four streams are 1.5 to 3.1% faster, both with drafts and with --no-drafts, and every token hash is equal. Both chips' contract lines at the final head are in as well. The M3 Ultra passes 97 of 97 kernels with 193 controls red on --only dsv41, and its full run has the same three known nonfinite findings as base. That covers our ask 2.

The full-run abort on the M5 Ultra (ControlEditMissing) happens on base too. It's tracked in #3067 and doesn't hold this PR.

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. We'll take them as they are.

Still open:

  1. Ask 1 from our last note. The head hasn't moved, so timeLaunch (zig/tests/dsv41_metal/real_test.zig:116-133) still runs pkg.layer.encode over four slots without segs.declare. Its slot kernels read buffers that were never declared to the encoder. After the rebase, the four-slot paths in dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951's timing_real.zig and dsv41 metal: launches of 6 to 16 rows run their FP8 projections on a matrix tile (stacked on #1720) #2973's timing_synth.zig will have the same gap. Declaring in the layer path, when attn.encode builds its table, would cover every caller. Adding declare to those callers works too.
  2. Please merge the two CHANGELOG lines into one and quote the served result from both chips, 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. On the M5 Ultra that's one stream level and three and four streams 1.5 to 3.1% faster; please add the M3 Ultra figure.
  3. The M3 Ultra decode series under the noise rule.
  4. Once dsv41 metal: the lane target, the prompt cache and the native server over Metal #1720 lands, a rebase onto dsv41 metal: prompts in pieces between the other streams' rounds (stacked on #1720) #2950. The order is dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951, dsv41 metal: prompts in pieces between the other streams' rounds (stacked on #1720) #2950, then this PR.
  • 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 (c6d2413c7), exit status 0, macOS 27.2.1, Metal compiler 383.1:

SUMMARY 758 kernels: 758 pass gates 1-4, 0 fail, 57 known findings; 643 controls: 643 red as planted, 0 missed; 0 limit problems; gates 3730588 ms, limits 3728 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; 193 controls: 193 red as planted, 0 missed; 0 limit problems; gates 100927 ms, limits 214 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: c6d2413c7
  • 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 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
  • 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 0a5638a8e95e, this a807c0a3c68d

Summary: this head gains at 3 and 4 streams and is level elsewhere. At 3 and 4 streams (8 bench_concurrent cells) this over base, mean over mean, is 1.008 to 1.028 (+0.8% to +2.8%): 6 of the 8 are faster by the #2984 rule and 2 (3 streams, temperature 1, chat 1.010 and code 1.008) are within noise. At 1 and 2 streams and in bench_openai (12 cells) it is 0.999 to 1.009, all within noise (the 2-stream chat cells at 1.009 and 1.009 are the largest). Token hashes equal in every cell, 0 failed or unequal comparisons. No cell is slower.

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: 6 faster, 0 slower, 14 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.6 43.5 43.5 43.5 0.998 1.000 0.999 within noise Mod/Mod/Mod/Mod
2 0 chat 55.8 56.3 56.3 55.8 1.009 1.009 1.009 within noise Mod/Mod/Mod/Mod
1 0 code 46.4 46.4 46.4 46.3 1.000 1.002 1.001 within noise Mod/Mod/Mod/Mod
2 0 code 59.8 59.7 59.7 59.3 0.998 1.007 1.003 within noise Mod/Mod/Mod/Mod
1 1 chat 47.8 47.7 47.8 47.7 0.998 1.002 1.000 within noise Mod/Mod/Mod/Mod
2 1 chat 50.8 51.3 51.2 50.8 1.010 1.008 1.009 within noise Mod/Mod/Mod/Mod
1 1 code 51.8 51.8 51.7 51.8 1.000 0.998 0.999 within noise Mod/Mod/Mod/Mod
2 1 code 49.0 49.2 49.2 48.9 1.004 1.006 1.005 within noise Mod/Mod/Mod/Mod
3 0 chat 68.6 69.8 69.7 68.8 1.017 1.013 1.015 faster Mod/Mod/Mod/Mod
4 0 chat 78.7 80.7 80.7 78.3 1.025 1.031 1.028 faster Mod/Mod/Mod/Mod
3 0 code 72.2 73.5 73.4 72.3 1.018 1.015 1.017 faster Mod/Mod/Mod/Mod
4 0 code 83.8 86.1 86.2 84.0 1.027 1.026 1.027 faster Mod/Mod/Mod/Mod
3 1 chat 55.8 56.3 56.3 55.7 1.009 1.011 1.010 within noise Mod/Mod/Mod/Mod
4 1 chat 61.7 62.9 62.9 61.7 1.019 1.019 1.019 faster Mod/Mod/Mod/Mod
3 1 code 56.5 57.0 57.0 56.6 1.009 1.007 1.008 within noise Mod/Mod/Mod/Mod
4 1 code 62.4 63.3 63.6 62.5 1.014 1.018 1.016 faster Mod/Mod/Mod/Mod
1 0 fibonacci-raw 54.5 54.4 54.4 54.4 1.000 0.999 0.999 within noise Nom/Mod/Mod/Mod
1 0 gpu-chat-no-think 43.8 43.7 43.8 43.7 0.999 1.000 1.000 within noise Nom/Mod/Mod/Mod
1 1 fibonacci-raw 50.3 50.2 50.2 50.2 0.998 0.999 0.999 within noise Nom/Mod/Mod/Mod
1 1 gpu-chat-no-think 43.8 43.6 43.7 43.7 0.996 1.001 0.999 within noise Nom/Mod/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.

Discarded, retried and flagged cells

  • The first run of every series is the discarded run and is in no table (series d-slots-c12, d-slots-c34, d-slots-oai).
  • One kept arm was retried: this 2 of the 3-and-4-stream series (d-slots-c34-this2) ended at Heavy on its first try, was discarded and re-run once after cooling; the second try ended at Moderate and is the one in the table. The first try's raw files are in the gist as .try1 files.
  • Nothing else ended at Heavy and no cell is flagged. The 1-stream bench_openai base 1 ended at Nominal and its three other arms at Moderate; base 1 and this agree within 0.2% there (mean over mean).

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; kernel-contract runs are not part of this series' data.

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

…e a multi-slot layer launch

segs.table declares nothing since the useResources commit, so an encoder that runs a layer over
several slots has to call segs.declare first. forward.encode and forward.run do; real_test's
timeLaunch, which runs one layer over four slots, did not, so its slot kernels read buffers never
declared to the encoder. It now declares them on its pass's encoder. The package exports segs so
the tests can.
@gilby

gilby commented Nov 13, 2026

Copy link
Copy Markdown
Contributor Author

New head: c469d807b (two commits on top of c6d2413c7, not rebased).

Receipts: the M5 Ultra and M3 Ultra receipts and both chips' contract lines above were taken at c6d2413c7. cd32ace02 touches only a release-weights test (timeLaunch, which the server never runs) and adds segs to the package's export list, which adds no code to the server; c469d807b is the CHANGELOG. So every posted cell still describes the served code. The contract is unchanged too: no kernel or record moved.

Your other points:

  1. (12 Nov 23:50) The M3 Ultra decode series under the noise rule: posted on 13 Nov (6 of the 8 three- and four-stream cells faster, the rest within noise, none slower, every token hash equal).
  2. (12 Nov 13:18 and 23:50) The rebase onto dsv41 metal: prompts in pieces between the other streams' rounds (stacked on #1720) #2950: 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. In a local rebase of the stack onto today's main, this PR's commits apply over dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951 and dsv41 metal: prompts in pieces between the other streams' rounds (stacked on #1720) #2950 with conflicts only in CHANGELOG.md and in context: the segs import sits beside dsv41 metal: TF_DSV41_ROUND_PROF times each decode round and the drafter's passes (stacked on #1720) #2951's round_prof import, and segs.declare lands in runTimed's loop, as you predicted, with every added line unchanged.

At c469d807b on an M5 Max: test-dsv41-metal-all 9/9 steps and 38/38 tests; zig build test-dsv41-metal-real builds (its tests skip without the release weights); 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 2954 0 errors and 0 warnings.

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