Repository navigation
Vulkan: one submission per batch of dispatches, 30% to 56% less run time on an M1 with the same logits (stacked on #2015) - #2016
Conversation
|
Thanks @joshuaswarren. An independent review on current main (d8b80bd): What the 45 commits areCommits 1-41 are #2015's series, patch-identical (checked with
The commits apply as described under our earlier review. Lean is 465 at the head, with none added. The author is @joshuaswarren's The change
Change 1The posted contract
Small points (not blocking)
What the device runs need
|
|
Every Vulkan contract runner and stage probe now has a GPU run at #2015's head and at this head, and the lines are equal. Setup. 13-inch M1 (Apple M1, G13G B1) on the Honeykrisp Vulkan driver, Linux 7.1.12-2. No model. Ten aarch64 ReleaseSafe runners built per head with
Compared with the elapsed and gate milliseconds removed, 10 of 10 runners print the same lines at both heads. What this does not cover.
On the two small points: I will add the [Joshua agent] |
|
The batched build also has a run on a 16-inch M1 Max (Apple M1 Max, G13C C0), the second Apple GPU, on the Honeykrisp driver, Linux 7.1.12-2. No model. Built as in my comment above from the batched commit before the merges of main and the lean fixes; the runtime and runner sources are unchanged since. Both exit 0. Contract runner sha256 The stage 1 probe reports [Joshua agent] |
f73e550 to
9d13b72
Compare
|
Batched submission on the Intel Arc Pro B60, the first measurement of this lever on a second vendor's driver: 65.0% at unicode-24.
Median 65.02% (range 64.5 to 65.0). All six exit 0, My forecast was 15 to 30 percent and it missed by more than a factor of two, in the direction that matters: on the Apple GPU batching is worth about 40 percent (40.5% and 40.6% at 24 tokens), and on the Intel driver it is worth about 65 percent. The pairs are tight, so this is not noise - it is the driver's per-submit and fence cost, which is higher on the B60 and which batching removes. That also explains the end-to-end gap between the two devices: the B60's stack-over-vec4 figure is 68.75% against the M1's 78.5%, and roughly 25 points of the B60's remaining headroom sit in this one lever. Any per-device default should account for it. [Joshua agent] |
|
Batched submission on top of vec4 at document-256 on the 13-inch M1: 19.29%, inside my 15 to 35 percent forecast (45%).
All six exit 0, [Joshua agent] |
|
Thanks @joshuaswarren. The series checks out against the one we reviewed. All ten runners matched with batching on, and the B60 timing shows the lever is bigger on Intel's driver (65%) than on the M1 (about 40%), with the same logits. This lands right after #2015. |
|
Correction: my earlier reply recalculated the displayed table, but those displayed rows do not match the retained timing file. The retained B60 run-2 pairs are 13536→4735 ms (65.019%), 13533→4731 ms (65.041%), and 13351→4738 ms (64.512%). Their median is 65.019%. Each retained timing record says |
|
[Joshua's agent] Note 🤖 Claude Sonnet 5.5 responding on behalf of joshuawarren Following @ashhart's request on #2038, the fix for a control that records a dispatch and then fails a host allocation is now in this PR, as two commits on top of
Regression test, in Host tests ( #2017 and #2038 are rebased onto this head with their own commits unchanged. The only difference in #2038 is that |
Fix for the failed host allocation after a recorded dispatch. Head is e0cffaa, no new commit in this comment. What changed
Test: "a failed host allocation after a recorded dispatch is dropped before the fixture and control free their buffers" (
Scope
Test change
Review: a second-model review is GREEN at e0cffaa. The reviewer did not run the suite. The RED and GREEN logs above come from the implementer. [Joshua's agent] |
|
Thanks @joshuaswarren, the discard fix with its RED/GREEN test is where we hoped it would land. |
Pushed
[Joshua's agent] |
49ef638 to
2c19a13
Compare
|
Thanks @joshuaswarren. This PR's eight commits are the same patches we read on 5 Nov, and the batching code still checks out. Main is frozen for today's release, and this lands right after #2015, so it waits on #2015's float32 fix.
We'll write the CHANGELOG line.
|
2c19a13 to
432c089
Compare
|
Restacked onto the fixed #2015 head and pushed: 432c089. The description now names this PR's nine commits instead of the pre-rebase ids. The batched-submission changelog line moved under the new The float32-build pair on the 16-inch M1 Max at unicode-24, with digests, run-2 times and the ten contract runners at this head, follows once that machine's measurement queue drains. Main is frozen today, so nothing lands today. |
a8075e7 to
e1d39b0
Compare
|
The red checks on the restack were ours, and they are fixed on the new heads.
Local runs on the new heads: |
|
I re-measured the 24-token case on the float32-fixed heads, and the figure in this description is too high for them. Setup: a 13-inch M1, Qwen3.5-2B 4-bit, the 24-token prompt, context 512, 64 decode steps. Old is the #2015 head and new is this head, six fresh processes in the order old, new, new, old, old, new.
The median gain is 57.8%, so a run takes about 58% less time here, not 68%. Decode is 126.9 ms per token before and 53.5 after. All six processes produced one logits digest ( The 49% and 30% figures for the 256 and 1,024-token documents came from the earlier heads, before the float32 fix. They have not been re-measured on these heads yet. I will re-run both cases and then correct the changelog line and the receipt, so the description matches the measurements. |
|
All three cases are re-measured on the float32-fixed heads (13-inch M1, Linux 7.3.0-2-20261110.1-lab-ARCH, three pairs per case, six of six processes valid, one logits digest per case). The description, the changelog line and the receipt README now carry these figures (commit df6b49c).
The first figures (68.2%, 48.8%, 30.5%) came from earlier builds on Linux 7.1.12 and an older Mesa build. The gain depends on the environment: the earlier 256-token builds, run on today's kernel and Mesa, gave 20.5% (20.8 s against 16.6 s). Every run time is slower than in the first figures, and these runs do not separate the cause. My earlier forecast for the document cases (within 8 points of the first figures) missed: the 256-token gain is 15 points lower. A first 1,024-token attempt failed my own packet's prompt-token pin and is not counted. The summary is |
The first figures (40% to 52%, against ashhart#1544's head) came from builds before the float32 fix, on an older kernel and Mesa build. On the fixed head, on a 13-inch M1 with Linux 7.3.0-2-20261110.1-lab-ARCH, the three cases took 34.8%, 41.0% and 43.0% less time than the same head with the scalar kernel selected (three pairs for the first two, two pairs for the 1,024-token document, one logits digest per case). The gain depends on the environment; these runs do not separate the cause. The identity probe and the kernel contract output are from the earlier builds and were not re-run. Signed-off-by: joshuaswarren <816217+joshuaswarren@users.noreply.github.com>
…es for the oracle Each shader taken from omarchy-mlx now opens with its source path and commit, what changed, and the copyright line; THIRD_PARTY_NOTICES.md lists every such file. The 34 placeholder comment lines in 17 files become one-line comments of what the code does, or go. The push-constant block, the gate helper and the labels that named another project's layout now describe our own layout and say oracle. No code changes. Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com>
…repare Main's Fixture.prepare now calls adopt (main 6e9af93), so the tile registration rebases onto that shape instead of the old fill-in-place form.
…to 32-column tiles A decode step reads each weight row k/2 bytes apart, so every 16-byte load of a 32-lane group touches 32 cache lines. The new image reads the same vectors from a tile layout, [tile][k/32][lane] for the words and [tile][group][lane] for the scales and biases, so a group's load is one 512-byte run. The loop, the order of the sums and every value are qmm_q4_vec4's; only the address formulas change. vk.tile repacks on the host (pure moves of 16-byte vectors and 2-byte scales). The Qwen3.5 loader tiles each projection whose n is a multiple of 32 and whose words fit one binding, when the arena alignment keeps x 16-byte aligned, and Quant.tiled sends it to ops_q4.projectTiled. The tied lm head is a second, tiled upload of the embedding table: the embedding gathers rows and keeps the row layout. Projections that do not fit (n = 16, devices with a small storage range) keep the vec4 image. stage1_probe runs the tiled image on every tileable projection case and requires bytes equal to the default image; the fixture gains four served shapes (n 4096 and 6144 at k 2048, n 2048 at k 6144, m 1 and 32). Proved by: every Vulkan host suite passes; stage 1 probe 21 of 21 on a software Vulkan device with the tiled image identical on all 6 tileable cases there. Not measured on a GPU yet.
…tes as qmm_bf16 The contract runner gets a launch record and a fourth fixture for the tiled image. Its inputs are repacked into 32-column tiles after the float64 reference is computed from the row layout, so the same served shapes, bound, guards and input checks apply. Shapes that cannot be tiled are refused like the other edges the kernel does not serve. The fixture's kernels table is now 10 entries. Proved by: the runner builds and its limits gate passes for qmm_q4_tile on a software Vulkan device (which cannot hold the large shapes). The SUMMARY lines on the Apple GPUs are in the receipts commit.
…with zero columns The kernel contract refused qmm_q4_tile on 1x16x64: a registered kernel must serve every shape the contract serves, and n = 16 is not a multiple of 32. The repack now stores n rounded up to whole tiles and fills the extra columns with zeros. The kernel never computes a column past n, so no sum changes. The loader tiles every projection whose padded words fit one binding, including the n = 16 ones, and projectTiled checks the padded geometry. The contract fixture and the stage 1 probe size their buffers the same way. Proved by: every Vulkan host suite passes; stage 1 probe 21 of 21 on a software Vulkan device with the tiled image identical to the default image on all 11 cases that fit one binding there (n 8, 16 and 257 included).
…-inch M1 The receipts folder holds the identity probe (21 of 21, the tiled image byte-identical on all 12 projection cases), the contract runner's output (3 kernels pass gates 1 to 4, 8 of 8 controls caught), the pre-registration with its addendum, a summary generated from the raw run files, and the SHA-256 of each raw file. Against the vec4 build a run took 19%, 50% and 63% less time on three inputs, with the same logits digest in all six processes of each case.
… keeps its name Layers 0-3 store f32. The restack tiled every projection and handed those views to qmm_q4_tile, which reads packed bf16. projectTiled now refuses a non-bf16 view, and the loader keeps row weights unless the layer's activations are bf16. Fixture.kernel applies the name field again, so vec4 and tile no longer report as qmm_bf16. Proved: zig build stage1-host-test model-host-test -Dvulkan=true, 87/87 steps succeeded. The new tests failed first (expected error.Invalid, found void; fixture name stayed the default).
The first figures (19%, 50%, 63%) came from heads before the float32 fix, on an older kernel and Mesa build. On the fixed heads, on a 13-inch M1 with Linux 7.3.0-2-20261110.1-lab-ARCH, the three cases took 10.9%, 35.6% and 44.9% less time than with qmm_q4_vec4 (three pairs each, one logits digest per case). The gain depends on the environment; these runs do not separate the cause. The stage 1 probe and the kernel contract output are from the earlier builds and were not re-run. Signed-off-by: joshuaswarren <816217+joshuaswarren@users.noreply.github.com>
The f32 layers kept row weights because qmm_q4_tile only read bf16 x. ACT_F32 builds the same accumulation over tiled words, so every layer tiles. Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com>
A planted-input fixture runs qmm_q4_tile_f32 through gates 1-4 with f32 activations: x is the bf16 values widened exactly, and the output is f32 with the same guard words. Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com>
The zig lean check allows one-line comments within 120 columns only. Comment change, no code change. Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com>
…le_f32 Adds the P-core pinned ashhart#1990 against ashhart#2015 timing on the 13-inch M1 (28.6%, 54.4%, 66.8%, equal logits digests), the output of the ten contract and stage runners at 2a5a06c including qmm_q4_tile_f32, and replaces the earlier-build kernel contract and identity probe files. Signed-off-by: Joshua Warren <816217+joshuaswarren@users.noreply.github.com>
…ce per batch Every Pipeline.run was its own submission with a fence wait. On a 13-inch M1 the 31,104 dispatches of a 64-step Qwen3.5-2B run (486 a step) cost about 90 us each in submit and wait alone, about 40 percent of the run. Run now records into the open command buffer and Commands submits when 128 dispatches are waiting, or when the next upload, download or fill needs the queue, so a readback still sees every earlier dispatch and no kernel's arithmetic changes. A pipeline holds a ring of 256 descriptor sets: bind fills the next one and record binds the current one, so a set is never rewritten while a recorded dispatch still uses it (a flush at 128 keeps the ring from wrapping). Commands.begin returns the open buffer instead of resetting it; submit with nothing recorded is a no-op. Proved by: every Vulkan host suite passes; stage 1 to 5 probes on a software Vulkan device: 21 of 21, 9 of 9, 4 of 4, 6 of 6 and 7 of 7. Not measured on a GPU yet.
…on a 13-inch M1 The receipts folder holds the stage 1 probe on the GPU (21 of 21, the tiled image equal to the default image on all 12 projection cases), the contract runner's output (3 kernels pass gates 1 to 4, 8 of 8 controls caught), the pre-registration, a summary generated from the raw run files, and the SHA-256 of each raw file. Against the tile build a run took 68%, 49% and 30% less time on three inputs, with the same logits digest in all six processes of each case.
…nd that the fence timeout covers a batch
…ontracts gates.runAll converts every kernel error, Timeout and InFlight included, into a FAIL verdict. An unguarded GPU call let a fence timeout unwind into the rig's frees and vkDestroyDevice with work possibly in flight, instead of the tree's exit-124 and exit-1 doctrine (commands.zig:104,122). Guard the datamove rig's upload and download and the four fixture launches with vk.orExit. In the greedy contract the draw arm returned select()'s Timeout and InFlight past the fixture boundary and the control arm submitted raw; guard both. A NonFinite refusal still reaches the refused marker. Host tests lock the swallowing channel and the refusal.
…t call fails A host allocation can fail between a recorded dispatch and the submission that drains it. gates.runAll catches the error and the next prepare frees buffers the still-open command buffer references, so a later submit would dispatch on freed device memory. Commands.discard resets the open command buffer and clears the batch state. The qmm fixture and control, the datamove rig, the greedy rig and its device teardown call it before they free anything. Two failures left the stream usable after it should be terminal, and a gate wrapper that catches and continues could reuse it. A vkResetFences failure after a signaled wait left the fence signaled, and a failed vkEndCommandBuffer left an invalid open buffer for the next begin. Both now store a terminal error in Commands: begin and every later submit refuse without recording or queueing, and vk.orExit ends the process. Host tests drive discard through a stubbed vkResetCommandBuffer and force each failure. A further test plants a failed host allocation on the qmm fixture's and the control's allocator, then resets each with a dispatch still recorded. Without discard in those resets the dispatch stays open and the test fails.
Comment-only change: each two-line comment in the Vulkan sources becomes one line of at most 120 columns. Lean check drops the 13 multi-line comment problems this branch adds versus main; no code changed. Checked: lean_check adds zero problems versus main; vulkan-host-test 75/75 steps, 228/230 tests, 2 skipped.
The first figures (68%, 49%, 30%) came from heads before the float32 fix, on an older kernel and Mesa build. On the fixed heads, on a 13-inch M1 with Linux 7.3.0-2-20261110.1-lab-ARCH, the three cases took 57.8%, 34.1% and 16.6% less time (three pairs each, one logits digest per case). Earlier builds of the 256-token document in the same environment gave 20.5%, so the gain depends on the kernel and Mesa build; these runs do not separate the cause. The stage 1 probe and the kernel contract output are from the earlier builds and were not re-run. Signed-off-by: joshuaswarren <816217+joshuaswarren@users.noreply.github.com>
a49c36e to
b39a43e
Compare
Stacked on #2015 (tile, head
ff0674bc), which is stacked on #1990 (vec4) and #1544. This pull request owns ten commits:a190006d(the change: one command buffer, submit per batch),a99efc60(figures and changelog),4e9ca88eand7a4eec9b(comment style for the lean gate),136a8c98(Commands docs),2208eadfand6feedba4(fail-fast on fence-unknown errors and dropped records),ba49a1c1(comment style),b59f423f(changelog under the Unreleased heading) and4547f1e9(figures measured on the float32-fixed heads). The commits below those are #2015's, #1990's and #1544's.On a 13-inch M1 with Honeykrisp, a Qwen3.5-2B 4-bit run took 30% to 56% less time than the tile build on three inputs, most on the shortest prompt. The logits digests of the two builds are equal on every input. No kernel changes. Each cell is the median of three pairs (old, new, new, old, old, new), from the run of 12 November.
e946185d64ae3188aaae7d1182c353787e89Two cells have wide ranges: the 1,024-token cell spans 4.2 points and the 24-token cell 4.3 points, both under the 5% spread I registered before the run. My forecast was about 52% at the 256-token document and about 32% at the 1,024-token document; the 256-token cell came in 10 points under it. Run 2 is the prefill plus 63 decode steps plus the per-step logits readback. It is not decode-only.
These figures replace the ones first posted here (68.2%, 48.8% and 30.5%). Those came from heads before the float32 fix, on Linux 7.1.12-2-20261013.18-sep-ARCH with Mesa git-9323171c12. The gain depends on the environment: the earlier builds of the 256-token document, run on the kernel and Mesa build above, gave a 20.5% gain (20.8 s against 16.6 s) with the earlier digest
147a2d2e0232. Every run time here is slower than in the first figures, and these runs do not separate the cause. The scalar and vec4 builds were not re-run, so equality with their digests is not re-checked.What changes
Every
Pipeline.runwas its own submission with a fence wait. A 64-step run is 31,104 dispatches (486 a step). A profile of the tile build showed the small kernels (copy, gather, norm, add) at 89 to 119 us of host wall time each, which is almost all submit and wait, and the GPU idle between them.Pipeline.runrecords into the open command buffer.Commandssubmits when 128 dispatches are waiting, or when the next upload, download or fill needs the queue, so a readback still sees every earlier dispatch.Commands.beginreturns the open buffer instead of resetting it.submitwith nothing recorded does nothing.bindfills the next one andrecordbinds the current one, so a set is never rewritten while a recorded dispatch uses it. The flush at 128 keeps the ring from wrapping.What callers can rely on:
runno longer waits for its dispatch. Results are visible after any later download, upload or fill, or aftersubmit. The callers ofbeginandsubmitarefill,upload,download,Greedyand the greedy contract fixture; they append to the open buffer and submit it. A fence timeout now covers a whole batch (up to 128 dispatches) instead of one dispatch.Setup
d75f4cbc), Linux 7.3.0-2-20261110.1-lab-ARCH.b6d9a35b(6,116,824 bytes). New build: Vulkan: one submission per batch of dispatches, 30% to 56% less run time on an M1 with the same logits (stacked on #2015) #2016's head ELFd9b28417(6,109,056 bytes). Both are built from the float32-fixed heads.Identity
Kernel contract
tf-vulkan-kernel-contracton the GPU, exit 0, with this change:No kernel is added or changed here; the run shows the gates still pass when every dispatch goes through the batched path. The output is in
kernel-contract-m1-13-inch.txt.Pre-registration
PREREG.mdwas written before any GPU run. The rule was: a median gain of at least 3% and every pair positive. The forecast was 15% to 35% on the 24-token prompt and 5% to 15% on the documents. All three results are above it: 57.8%, 34.1% and 16.6%. I had taken the profile's per-dispatch host time (about 90 us, 31,104 dispatches) as the cost. The real cost was larger, because the GPU also sat idle between dispatches while the host waited on each fence.Not covered
196c1f1c(12 Nov, in the comment below): run 2 on the 256-token document went from 3,596 and 3,595 ms to 2,307 and 2,295 ms (1.56x), and on the 1,024-token document from 8,613 and 8,595 ms to 5,368 and 5,383 ms (1.60x), with equal logits digests on the three teacher-forced cases. Other chips, AMD and Intel are not measured here.Checks
python3 tools/zig/lean_check.py: 465 problems before and after, none new.raw-files-sha256.txtlists the hash of each one.Coordination
Part of #1420.
atc check 1420 --jsonon 2026-11-03: open, complete, no other pull requests, adviceyours. My public session on #1420 is 191551954422646. This pull request is stacked on #1544, #1990 and the PRs between them;atc boardshows no duplicate attempt against the stack.