Repository navigation
Vulkan: qmm_q4_tile reads the 4-bit projection from 32-column tiles, 29% to 67% less run time on an M1 with the same logits (stacked on #1990) - #2015
Conversation
|
Thanks @joshuaswarren. An independent review on current main (d8b80bd): What the 41 commits are
The new commitsThe shader is
The loop, the fp32 accumulator, the dequant, the order of the sums and the bf16 rounding are unchanged. So each The repack matches those addresses (
Host tests check every address, a byte sum that loses nothing, and zero padding (n 70, k 192; n 40, k 256). Tail and bounds:
Where it's used (
Contract. Off paths: only Vulkan files and the CHANGELOG change. Metal, CUDA, HIP and XPU are untouched. Small points (not blocking):
What the device runs needThe body's GPU evidence is one Apple M1 under Honeykrisp, against the vec4 build: logits digests equal on three
|
|
Status on the second-GPU run.
[Joshua agent] |
|
Thanks @joshuaswarren. We re-reviewed the whole stack after your 11:00 push (#1544, #1990, #2015, #2016 and #2017). By patch-id, every commit we reviewed is unchanged. The new commits are the lean fixes we asked for. #2016's ask is met: all 10 Vulkan contract runners and stage probes print identical lines at #2015's and #2016's heads (timings aside), every exit 0, on your M1. Two things before the stack lands, in order:
|
|
The second-GPU run you listed for this PR is done on a 16-inch M1 Max (Apple M1 Max, G13C C0), a different Apple GPU from the 13-inch M1 (G13G B1), on the same Honeykrisp driver, Linux 7.1.12-2. No model. Built with The stage 1 probe reports Still owed from your list: the non-Apple Vulkan GPU run. I asked above whether an NVIDIA GeForce RTX 5060 Ti meets it or whether you need AMD RADV or Intel ANV. [Joshua agent] |
928227d to
0be85ef
Compare
|
The stack is rebased onto current main (
Checks I ran on the new heads:
The receipts READMEs inside the commits still name the pre-rebase commit ids. The PR bodies now use the new ids. The old ids map to the new ones by position and subject: the old Still to do before landing: the 10 runners and probes on a non-Apple Vulkan GPU (the NVIDIA RTX 5060 Ti on the NVIDIA driver), run at the head of the stack. I will post the SUMMARY lines and exit codes here when that run finishes. [Joshua agent] |
|
The non-Apple Vulkan run is in. I ran all 10 runners and probes at the head of the rebased stack (#2038, All 10 exit 0 on both GPUs. After I strip the device name and the timings, the SUMMARY and RESULT lines are equal on all runners except one:
B60 lines, copied from the run: The planted controls are red as planted on the B60 (8 of 8 in the kernel contract, 1 of 1 in the greedy contract), the same counts as on the M1. Exit codes are all 0 in both runs. The B60 run took about 2 minutes 20 seconds, including 104.7 seconds for the kernel contract's gates. The run is sealed: the packet's [Joshua agent] |
|
Two of the heads in my rebase comment above changed. #2017 gained one commit (contract shapes at rows 5 and 33, and a planted control for the rows image; details on #2017), and #2038 sits on top of it:
The other four heads (#1544, #1990, #2015, #2016) are unchanged. The series is now 64 commits over main I re-ran all 10 runners at [Joshua agent] |
|
Thanks @joshuaswarren. Both things from our last comment are done. The stack is one linear series on current main with lean at 424, and the Intel Arc Pro B60 run covers the non-Apple GPU: all 10 runners and probes exit 0, the planted controls are red, and the lines match the M1's. The one difference, On our side, we'll check the rebased series commit by commit against the one we reviewed. Then #1544, #1990, #2015 and #2016 land in order. #2017 and #2038 have a few things left, listed on each. One request that doesn't block these three: please answer @cwschroeder's arithmetic question on #1990. From the posted milliseconds, the first B60 vec4 run's pairs are 23.81%, 24.93% and 24.17% (median 24.17%), and the replication's are 24.82%, 22.24% and 22.48% (median 22.48%). The comments say 23.1% and 23.0%. If the median comes from more readings than the table shows, say which, or post the per-process timings. The PR bodies' M1 tables check out, so the titles and CHANGELOG lines are fine as long as they quote those. |
|
The B60 question is answered on #1990: the retained timing files support medians of 23.066% and 23.021%; the originally displayed millisecond pairs were incorrect. My first correction recalculated those incorrect rows and has itself been corrected: #1990 (comment). The stack remains ordered behind #1544, whose held-packet result is still a failure. |
14942bf to
0a2a5a9
Compare
|
Thanks @joshuaswarren for moving the tile commits onto main's
|
|
Both code items are on 42fc030.
The unicode-24, document-256 and document-1024 pair on the 16-inch M1 Max follows once that machine is back on the lab4 kernel. Main is frozen today, so this does not land today. The changelog lines move under a new Unreleased heading on the restack. |
|
Thanks @joshuaswarren, both read right on
|
f4176ed to
fd625a3
Compare
|
The red checks on the restack were ours, and they are fixed on the new heads.
Local runs on the new heads: |
|
Correction: the figures in this description were measured on heads from before the float32 fix, on an older kernel and Mesa build. I re-measured on the fixed heads (13-inch M1, Linux 7.3.0-2-20261110.1-lab-ARCH, Omarchy Mesa build, three pairs per case, old arm #1990's head, new arm this head).
Every case wrote one logits digest across all six processes. The first version gave 19.3%, 49.9% and 62.6%. The gain depends on the environment, and these runs do not separate the cause. The description, the changelog line and the receipt README now carry the new figures (commit 6af17ea, with |
|
Second chip: the same three-pair protocol on a 16-inch M1 Max MacBook Pro (G13C, Omarchy Mesa build, kernel 7.3.0-2-20261110.1). Same old and new ELFs as the 13-inch M1 runs, three alternating pairs (old,new,new,old,old,new), run-2 gain per pair, median with range.
The 3% rule is not met for unicode-24 on this chip (old = vec4 head, new = tile). Each case has one logits digest across all pairs and both arms, and it equals the 13-inch M1 digest for that case (unicode-24 |
|
Rebased onto current upstream main (000b534): the branch head is now 37cdf2b (was 6af17ea). Upstream changed no Vulkan file since the previous base. To check that the speedup measured for this PR still applies, I cross-built the Vulkan Qwen3.5 model probe ( On the new head: |
The choices field showed every backend name on every build; the served set was already computed for the honoured values. Listing what the binary does not serve invited --backend vulkan attempts on builds that refuse it.
The dense-lane decomposition follows the frozen lane_qmm design; the compute shaders are new work.
…e gated refusal Choices equal to the served set made an unserved pick fail the choice check first, whose message main's flag-deps pin does not expect; the pin expects the gated refusal for an unserved value. Take main's choices form so the choice passes and the native gate speaks: main's zig job passes this test at 6e9af93 and fde40f6, and the failure showed only on this stack's merge (run 42815854414, flag_deps_test.zig:45).
…y-mlx shaders The Vulkan build's own choices gate already carried vulkan, but the flag's static choice list did not, so --backend vulkan died as an invalid choice before the served check could refuse it on other builds. The cli test now covers both builds: it parses on the Vulkan build and gets the gated refusal elsewhere. The credits line said all of the engine's compute shaders were new work; fifteen of them and omarchy_trig.h come from omarchy-mlx unchanged, and the list now says so. Proved: zig build test-flag-deps-host failed first on the unfixed choice list, with the invalid-choice refusal instead of the gated one, then passed 22 of 23 with one skip without Vulkan and succeeded with -Dvulkan=true.
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>
ff0674b to
ebd97e7
Compare
…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>
…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>
…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>
…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>
…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>
…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>
…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>
Distinguish source commits from ELF hashes and record both binary sizes. Verified the JSON summary parses; historical build commands are absent.
Stacked on #1990 (which is stacked on #1544). #1990's branch is at
59145cf8; this branch is rebased onto it and is atff0674bc. Review only these commits:1fca1c29(kernel, repack and loader),3b561802(kernel contract registration),5f470b2d(pad the last tile so every n is served),fccf5839(docs and changelog),1ae2363d(figures on the float32-fixed heads),0a036780(the tile image reads and stores f32 activations),52d5ed6f(its contract fixture), and the sync and style commitsf1efc1da,61290282,b1cbec31,cd63ee09,defd3bbfand2a5a06c2, and the docs commitff0674bc(the pinned timing and the runner output). Everything below them is #1990's and #1544's.On a 13-inch Apple M1 with Honeykrisp, a Qwen3.5-2B 4-bit run took 28.6% to 66.8% less time than with
qmm_q4_vec4on three inputs, with every process pinned to the four P cores. The logits are byte-identical. These figures are from #1990's head (ELF46571f11) against this PR's head (ELF6e6f47f2), run on 2026-11-12 on Linux 7.3.0-2-20261110.1-lab-ARCH and Honeykrisp from the Omarchy Mesa build. The ELFs were built before the comment and name commits above them; those commits change no code. An earlier run of the same two heads on this chip without pinning (2026-11-11, builds1e1b7d8candb6d9a35b) gave 10.9%, 35.6% and 44.9% with other logits digests, and the first version of this description gave 19% to 63% on earlier builds. These runs do not separate pinning from the build change as the cause.6bfbb1a55a3e8901e17ccef7a472a62c7e22Run 2 is the prefill plus 63 decode steps plus the per-step logits readback. It is not decode-only. All six processes of a case wrote the same logits digest, in both arms, and all 18 returned 0 with the pinned ids.
docs/receipts/vulkan-qmm-tile-m1/pinned-heads-summary.jsonholds every process row; the unpinned run stays infixed-heads-summary.json.What changes
For a decode step, lane i of a 32-lane group reads weight row n0+i, and the rows are k/2 bytes apart (1 KiB at k = 2048). Every 16-byte load of a group touches 32 cache lines. The new image reads the same vectors from tiles of 32 columns: words as
[tile][k/32][lane]16-byte vectors, scales and biases as[tile][group][lane]. A group's load is then one contiguous 512-byte run. The shader isqmm_q4_vec4with different address formulas; the loop, the fp32 accumulator and the order of the sums are the same.vk.tilerepacks on the host: whole 16-byte vectors and 2-byte scales move, nothing changes value. The last tile is padded with zero columns the kernel never computes, so any n is served.Quant.tiledsends it toops_q4.projectTiled. Other devices and shapes keep the vec4 image on the row layout.Setup
d75f4cbc), Linux 7.3.0-2-20261110.1-lab-ARCH.qmm_q4_vec4build), ELF sha256 prefix46571f11. New build: this PR's head before the comment and name commits, ELF sha256 prefix6e6f47f2. Both are ReleaseSafe aarch64 builds oftf-qwen35-modeland include the float32 fix.taskseton the four P cores (CPU 4-7). Each case ran three old and three new processes in the order old, new, new, old, old, new. Gain is (old - new) / old on run 2.Identity
2a5a06c2, with the tiled image identical on all 12 projection cases (n = 8, 16 and 257 pad the last tile; n = 140,000 is one binding on this device). It also passed 21 of 21 on llvmpipe on the earlier build, which is not GPU evidence.docs/receipts/vulkan-qmm-tile-m1/runners-2a5a06c2/tf-stage1-gpu.txthas the per-case lines.qmm_q4_tile_f32) againstqmm_f32byte for byte. For that image the identity evidence is the equal logits digests of the two heads on the three inputs.Kernel contract
The ten contract and stage runners ran on the GPU at
2a5a06c2(2026-11-12, GPU timeouts 0). All ten exited 0. The output is indocs/receipts/vulkan-qmm-tile-m1/runners-2a5a06c2/, one file per runner.tf-vulkan-kernel-contract:The four kernels are
qmm_bf16,qmm_q4_vec4,qmm_q4_tileandqmm_q4_tile_f32.qmm_q4_tile_f32passes all five gates (limits, nonfinite, repeat, shapes, sums): 15 shapes within the float64 bound (worst 0.034 of the bound), five runs byte-identical, exact sums at k 6144.qmm_q4_tilepasses with 15 shapes in bound (worst 0.987). The other runners: attention contract 15 of 15, contract-pointwise 16 passed and 1 known of 17, datamove 7 of 7 kernels, stage 1 21 of 21, stage 2 9 of 9, stage 3 4 of 4, stage 4 6 of 6, stage 5 7 of 7, greedy contract 1 pass and 1 control red as planted.The first run of the contract on the GPU, on an earlier build, failed: the build in
2bbe4202left n = 16 on the vec4 image and the contract refused the served shape 1x16x64, because a registered kernel must serve every shape the contract serves.a50413a1pads the last tile instead. The gates seed each kernel from its name, so each kernel sees its own random data and its worst-shape value differs.Pre-registration
PREREG.mdwas written before any GPU run. The rule was: a median gain of at least 3% and every pair positive. All three cases meet it. The forecast was 20% to 45% on the 24-token prompt and similar on the documents. The pinned run gave 28.6% on the 24-token prompt, inside it, and 54.4% and 66.8% on the documents, above it. The unpinned run of 2026-11-11 gave 10.9%, 35.6% and 44.9%. On the earlier builds the 24-token prompt gave 19.3% and the documents 49.9% and 62.6%.The padding change came after the first contract run and before the final pairs. The addendum in
PREREG.mdsays so, and was written after the final pairs.Not covered
Checks
python3 tools/zig/lean_check.py: 466 problems before and after, none new.zig/build/vulkan.zig(683 to 690 lines) andzig/tests/vulkan/qwen35_model_probe.zig(647 to 648) were already over 600 lines.mainand Vulkan: qmm_q4_vec4 reads the 4-bit projection 16 bytes at a time, 34.8% to 43.0% less run time on an M1 with the same logits (stacked on #1544) #1990's branch: every Vulkan host suite passes, and the stage 1 probe and the contract runner build.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.