perf(vulkan): lm_head column blocking -1.07ms/tok; the 9.3ms floor I briefed does not exist - #186
Merged
Merged
Conversation
The one GEMM per decode token that stays on the portable scalar kernel is the
lm_head. VT_VULKAN_DISPATCH_STATS names it exactly: "gemv DECLINED: not MatmulBT
(b is [K,N]; that layout is already coalesced) (bt=0 m=1 k=5120 n=248320)". The
decline is CORRECT and is untouched here -- in [K,N] adjacent lanes already read
adjacent addresses, so vt_matmul_vec's shape would make a coalesced access
strided. The kernel moves k*n bf16 = 2.543 GB per token.
THE 9.3 ms FLOOR THIS ROW WAS BRIEFED WITH IS WRONG, and the error is the roof,
not the arithmetic. 2.543 GB at GB10's THEORETICAL 273 GB/s is 9.3 ms, making the
measured 12.43 ms look like 75% of roof with ~3.1 ms of headroom. Nothing on this
box reaches 273. vt_matmul_vec -- a genuinely coalesced streaming kernel -- was
run on the IDENTICAL byte count (benchmarks/vulkan_gemm_ab.cpp, M=1 K=5120
N=248320) and takes 11.04 ms = 230.3 GB/s. 230 GB/s is the practical ceiling
here, so the honest headroom was 1.4 ms.
MEASURED NEGATIVE, and it is in the record so it is not re-tried: the four-way K
unroll is ZERO. The memory-level-parallelism hypothesis -- one outstanding load
per lane, the exact defect a four-way unroll fixed for vt_matmul_vec -- was built
with the accumulation order preserved and A/B'd on ONE binary: 12.430 ms/call
rolled vs 12.532 unrolled, three replicates each, if anything half a percent the
wrong way. The driver already pipelines the rolled loop. That arm is not in this
commit.
WHAT WORKED IS CONTIGUITY PER WORKGROUP, the one structural difference left
between the two kernels at identical byte counts. vt_matmul_vec gives one
workgroup one output element and strides its 128 lanes along K, so the workgroup
sweeps a contiguous 10 KB run of b. The flat scalar body gives one INVOCATION one
output element, so a 128-lane workgroup reads 256 contiguous bytes at row q and
then jumps n*2 = 486 KB to row q+1; contiguity then survives only across
workgroups still at the same q, and they drift apart.
VT_VULKAN_MATMUL_NCOLS gives a workgroup 128*NCOLS CONSECUTIVE output columns of
one row. Lane t owns columns t + c*128, NOT t*NCOLS + c -- the strided assignment
is what keeps each of the NCOLS loads a fully coalesced 128-lane contiguous read.
MEASURED on GB10, 27B decode, vt_matmul ms/call at output-len 36, arms
interleaved within each replicate:
NCOLS 1 (flat) 12.485 203.7 GB/s
NCOLS 2 12.459 204.1
NCOLS 4 11.541 220.4 <- default
NCOLS 8 12.812 198.5
NCOLS 4 beat NCOLS 1 in 6 of 6 interleaved pairs; two-length diff (len 36 minus
len 4 over 32 tokens) -1.066 ms/token; 95.7% of the measured 230 GB/s ceiling.
Blocking is a TRADE, not a monotone win: at 8 the dispatch is only 243
workgroups (~31k threads) and the device runs out of latency-hiding work faster
than the longer run buys back, so it is SLOWER than not blocking at all. The
optimum is interior, which is why the gate pins the constant 4 rather than
"not 1". E2E: 8 alternated AB/BA pairs, A wins 7 of 8 and 3 of 4 clean pairs,
clean-leg median TPOT 241.90 vs 242.67 ms (4.121 -> 4.134 tok/s) -- same sign and
magnitude as the GPU-timestamp result, but 0.3% of wall, so the paging-immune
measurement is the binding one.
PORTED FROM. No new op and no new module: one specialization constant on
vt_matmul. The per-element math is unchanged from src/vt/cpu/cpu_ops.cpp:187-249
MatmulChunked, which this kernel already answered to. The column-blocked dispatch
shape is llama.cpp's mul_mat_vec.comp idiom (a workgroup owning a block of
outputs, each lane holding its own accumulator across the reduction) at
ggml/src/ggml-vulkan/vulkan-shaders/mul_mat_vec.comp @ pin 237ad9b96, transposed
onto the [K,N] orientation llama.cpp does not carry.
NUMERICALLY FREE, AND GATED AS SUCH. Each of a lane's accumulators owns ONE
output element and sums the whole K sequentially, which is exactly the CPU
kernel's order, so vt_matmul keeps the byte-exact tier that the coopmat and GEMV
tactics both gave up. The gate is a memcmp of the blocked and flat arms in ONE
process, plus the specialization VALUES from a new PipelineKeys() -- both arms
are the same module and produce identical bytes, so PipelineExistsFor and any
numeric tolerance would pass identically if the optimization silently stopped
being selected. Two scratch mutations confirm it bites: defaulting NCOLS to 1
fails the mechanism assertion, splitting the K reduction into two partials fails
the memcmp. An earlier fixture used the small exact operand ladder the
neighbouring cases use and the reassociation mutation SURVIVED it -- every
partial sum was exact in f32 -- so the operands now span decades.
The kernel also reports its arm ("scalar matmul ARM: bt=.. ncols=..") under
VT_VULKAN_DISPATCH_STATS: both arms of a performance A/B produce bit-identical
output and report the same shader name in the histogram, so without a marker an
arm that failed to take its flag is indistinguishable from a bad leg. One leg of
the first measurement block read 215 ms/call and three replicates of the same arm
then read 12.39-12.43; the marker is what settled that.
GATES (GB10, all under flock $HOME/gpu.lock): test_vulkan_backend 30/30 (2369
assertions), test_backend_cross_device 11/11 (132), VLLM_CPP_DEVICE=vulkan
test_opt_paged_engine 6/6 prompts token-exact (96/96 tokens) with 0 declines.
llvmpipe: 30/30 (1826), 11/11 (132), 6/6 (96/96). gen-vulkan-spirv.py --check
clean at pinned glslang 16.5.0.
PRE-EXISTING AND NOT CAUSED HERE, verified identical on the 93852c2 base:
check-public-doc-tables fails on a non-canonical BENCHMARKS H2 section, one
em-dash, and a STATUS.md ratchet overrun; check-env-doc lists 12 undocumented
VT_GEMMA4_*/VT_ROCM_*/VT_SERVER_* vars. This change adds none of them, documents
its own env lever, and leaves STATUS.md strictly smaller than at base.
FOLLOW-UP IN THE SAME CHANGE: THE 20x BIMODALITY
Two parallel agents reported the same bimodal decode legs with opposite
explanations: "a lever bigger than every other Vulkan optimization" and
"contamination from concurrent 27B benchmarks". NEITHER survives measurement.
This commit adds the cheap reproducer that made the question answerable and
records what it settled.
LOCK DISCIPLINE, STATED. Every 27B run in this row went through
flock $HOME/gpu.lock, either as `flock <lockfile> <cmd>` or as `exec 9>...;
flock 9` held across a whole block; the P4 block sat QUEUED on it for 20+
minutes behind another agent's bench, which is the evidence that it was real.
The only thing NOT taken under the lock was the dgx BUILD (nice -n 19, -j6),
which is CPU and RAM but not a second 27B.
MEASURED 1 -- IT HAPPENS WITH THE BOX EXCLUSIVELY OURS. 16 alternated decode
legs under the lock, with /usr/bin/time -v and /proc/vmstat deltas per leg.
MemAvailable immediately before EVERY leg was 119,024-119,276 MB of 119 GB, so
no other process of any size was resident. One leg collapsed to 259.62 ms/call
(vs 12.42-12.46 for its neighbours) while vt_matmul_vec in the same leg moved
+0.3%. That leg is indistinguishable from the healthy ones on every memory
metric: peak RSS 105,994,272 KB vs 105,995,280 and 105,995,248; major faults
1098 vs 1090 and 1102; TTFT 6346 ms vs 6273 and 6228; and it started with MORE
free memory than either. Co-residency, page reclaim/swap, and model-load or
prefill effects are all REFUTED.
Heavy paging was measured separately and is NOT this: four legs in the same
block ran with 46k-304k major faults, 40-270x normal from a cold page cache, and
read 12.46, 13.24, 13.29 and 13.92 ms/call. Real paging costs about 10%, not 20x.
MEASURED 2 -- IT DOES NOT REPRODUCE STANDALONE AT LOW OCCUPANCY. The kernel was
being chased through a 3-minute, 106 GB 27B leg, which is a terrible experiment.
benchmarks/vulkan_gemm_ab.cpp -- already this row's same-binary A/B vehicle for
exactly this kernel -- gains an `nn` orientation (b as [K,N], the lm_head's, and
the one the GEMV tactic correctly declines) and a `cycles` argument that FREES
and REALLOCATES the weight between measurements, which is what separates "the
slow state belongs to this ALLOCATION" from "to this process" or "to this
moment". It also prints GB/s beside GFLOP/s, because at M=1 the arithmetic is
trivial and the whole cost is streaming b, and it dumps the pipeline keys so the
arm is self-evident.
`vulkan-gemm-ab 1 5120 248320 12 nn 12` on an idle box (3 GB used, 116 GB
available) reaches the same kernel on the same 2.543 GB in seconds:
12 reallocations x 12 reps = 144 measurements, ALL between 12.39 and 13.96 ms.
ZERO collapses, no bimodality at all.
INFERRED, AND NOT ESTABLISHED. The one variable separating "collapses at 1 leg in
16" from "never in 144 measurements" is that the 27B process holds 106 GB RSS of
a 119 GB unified box while the reproducer holds 2.5 GB, so near-ceiling
OCCUPANCY, rather than contention with another process, is the surviving
hypothesis. Why this kernel and not its neighbour is consistent with it: at [K,N]
a workgroup reads 256 contiguous bytes and then jumps n*2 = 486 KB, touching
~620k distinct 4 KB pages per call, where vt_matmul_vec walks one weight row
contiguously -- so vt_matmul is by far the most address-translation-hostile
kernel in the model. The ballast experiment that would test occupancy directly
was NOT run: an unguarded large allocation has OOM-rebooted this box before.
SUGGESTIVE, EXPLICITLY NOT A CLAIM. Across the blocks where both arms existed,
every collapse landed on the FLAT arm: NCOLS 1 three of 23 legs, NCOLS 4/8 zero
of 31 (five of 37 vs zero of 31 including the two blocks that predate column
blocking). Fisher exact one-sided p is about 0.06-0.07, which is not significant,
and "the optimization also fixes the pathology" is exactly the just-so story this
project has been burned by. Settling it needs roughly 60 more legs and it is the
named next experiment.
BOTTOM LINE. The bimodality is REAL, specific to vt_matmul, survives an exclusive
box and is not page reclaim; the mechanism is OPEN. It is not "bigger than every
other Vulkan lever" on this evidence: at 1 leg in 16 it costs roughly 6% of mean
decode time, against the 8.5% column blocking takes off this kernel
deterministically and reproducibly.
No library code changes here, so the gates are unchanged and were re-run anyway:
test_vulkan_backend 30/30 (1826), test_backend_cross_device 11/11 (132) on
llvmpipe, gen-vulkan-spirv.py --check clean, and the new benchmark mode
verifies against its host oracle (worst rel err 0.000e+00 in both orientations).
FOLLOWING_AGENTS_PROTOCOL
Following-Agents-Protocol: true
AI-Assisted: true
Assisted-by: Claude-Code:claude-opus-5 [Claude Code]
rohitpaul
pushed a commit
to rohitpaul/vllm.cpp
that referenced
this pull request
Aug 9, 2026
Each of mudler#184, mudler#185 and mudler#186 was measured against `93852c28` in isolation, so none of their numbers described a tree carrying all three. STATUS.md was deliberately left at the CONSERVATIVE 4.24 -- the largest single lever -- rather than a sum, because adding independently-measured deltas would have been inventing a number. This is the measurement that replaces it. METHOD. `git archive` of merged main `81ea01f0` to dgx, `vt_matmul.comp` md5 verified identical on both sides. Fresh Release configure. 8 wall-clock legs with the page cache dropped before each and `flock $HOME/gpu.lock` held, then a two-length GPU-timestamp diff (output-len 36 minus 4, over 32 decode tokens) so prefill and one-time costs cancel. Qwen3.6-27B bf16, 1 prompt, 32-in, c1. MEASURED, 8 legs, ALL CLEAN: TPOT 232.18 to 234.07 ms, decode 4.27 to 4.31 tok/s, MEDIAN 4.285, spread 0.8%. Zero bimodal collapses. MEASURED, per decode token: shader before merged delta vt_matmul_vec 214.1 210.1 -4.0 vt_matmul (lm_head) 12.43 11.57 -0.86 vt_rms_norm -> vt_rms_norm_wide 7.97 1.57 -6.40 TOTAL GPU 240.3 227.7 -12.6 Wall 233.0 ms, so host is 5.3 ms/token. Every lever reproduced its own claim on the merged tree: vt_rms_norm_wide at 0.0123 ms/call is exactly what its row reported, and the other two deltas match theirs inside the leg spread. A FIRST ATTEMPT WAS INVALID AND IS RECORDED, because the failure is reusable. It read 0.75-1.27 tok/s. VLLM_CPP_VULKAN defaults to AUTO, which resolves to OFF -- Vulkan is opt-in so it cannot register into gate builds -- and the fresh configure omitted -DVLLM_CPP_VULKAN=ON. The options had been copied from the reference build's CMakeCache.txt through `grep | head -15`, and the alphabetical list ended at TRITON_TARGET, exactly one line before VULKAN. My own truncation hid the flag. Three tells were already in the output and are the cheap check: no `[vt vulkan]` lines despite VT_VULKAN_DISPATCH_STATS=1, no `[vt reference-tier]` lines, and "Asynchronous scheduling is ENABLED" where every valid Vulkan run reports it disabled. Same family as this campaign's stale-binary false greens, in the opposite direction -- a false catastrophe rather than a false pass. A CORRECTION TO THIS CAMPAIGN'S ROOF ARITHMETIC. mudler#186 established that GB10 does not reach its theoretical 273 GB/s, by running a known-good streaming kernel on the identical byte count: 230.3 GB/s. That correctly retires the 9.3 ms lm_head floor. It does NOT invalidate the layer-GEMV percentages, which MEASURE 243-248 GB/s -- above 230.3 -- so 230.3 is a ceiling for THAT SHAPE (k=5120, n=248320, one 2.54 GB buffer), not a device ceiling. Why one shape's ceiling sits ~7% below the same kernel's on layer weights is UNEXPLAINED, and it is the same lone buffer the 20x bimodal collapse attaches to. WHERE THE REMAINING 3.1 ms IS. llama.cpp Vulkan is 4.35 tok/s = 229.9 ms/token on the same 50.89 GiB weights on this box. vt_matmul_vec is now 92% of our GPU time at 248.0 GB/s, and its lever is CLOSED with the binding constraint identified as DRAM bandwidth on a ~50 GB working set streamed once per token. The named residuals are lm_head (219.8 GB/s, 95.4% of its own shape's measured ceiling, worth ~1.3 ms if it could reach 248) and host at 5.3 ms. Also refreshes .agents/NOW.md in the same change and appends the dated checkpoint below the enforced marker in .agents/state.md, per the handoff contract. KNOWN RED, unchanged and pre-existing: docs/STATUS.md remains over its shrink-only ratchet (this change shrinks it by 3 more chars). FOLLOWING_AGENTS_PROTOCOL Following-Agents-Protocol: true AI-Assisted: true Assisted-by: Claude-Code:claude-opus-5 [Claude Code]
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The brief's premise was wrong, and that is the biggest finding
I briefed this row with a 9.3 ms floor for the lm_head GEMM: 2.543 GB at
GB10's 273 GB/s. That floor does not exist, because nothing on this box
reaches 273 GB/s. Running
vt_matmul_vec— a genuinely coalesced streamingkernel — on the identical byte count measures 11.04 ms = 230.3 GB/s.
That is the practical ceiling. Real headroom was 1.4 ms, not the 3.1 I
claimed.
Every "% of roof" figure this campaign has quoted against 273 GB/s is optimistic
by the same ~16%.
Measured
VT_VULKAN_MATMUL_NCOLSgives a workgroup 128×NCOLS consecutive outputcolumns, with lane
towning columnst + c*128(nott*NCOLS + c) so everyload stays coalesced. Blocking is a trade, not monotone: NCOLS 8 is slower
than not blocking at all (243 workgroups starves latency hiding) and NCOLS 2
ties. The optimum is interior, at 4.
Byte-exactness of the general matmul path is preserved, and known by memcmp
rather than by argument: each accumulator owns one output element and sums the
whole K sequentially. The gate compares both arms bitwise in one process. Two
scratch mutations bite — and notably an earlier version of the fixture
survived the reassociation mutation because its operands were exact in f32,
so the fixture now spans decades.
Prefill: zero impact.
vt_matmulruns exactly once per output token; prefilltakes coopmat. Blocking is host-gated to
bt == 0regardless.e2e: 8 AB/BA pairs, wins 7/8 and 3/4 clean pairs, 242.67 → 241.90 ms TPOT.
0.3% of wall, at the noise edge, so the GPU-timestamp number is the binding one.
The bimodal collapse is REAL — this refutes what I told two agents
I twice relayed that the ~1.8x bimodal legs were self-inflicted contamination
from concurrent 27B benchmarking. That is now refuted by direct measurement.
Reproduced with the box exclusively this row's,
flock ~/gpu.lockheld on every27B run: 16 legs,
MemAvailable119,024–119,276 MB of 119 GB before every leg.One leg hit 259.62 ms/call and is indistinguishable from its healthy neighbours
on every memory metric — RSS 105,994,272 KB vs 105,995,280, major faults 1098 vs
1090, TTFT 6346 ms vs 6273 — and it started with more free memory.
Refuted: co-residency, page reclaim, load and prefill effects. Separately,
four legs with 46k–304k major faults (40–270× normal) read only 12.46–13.92 ms, so
heavy paging costs ~10%, not 20×.
A cheap reproducer (
nnorientation + realloc cycles invulkan_gemm_ab.cpp)reaches the same kernel on the same 2.543 GB in seconds instead of a 3-minute /
106 GB leg: 144 measurements, all 12.39–13.96 ms, zero collapses on an idle
box.
Inferred, not established: the one variable left is that the 27B holds 106 GB
RSS of 119 GB while the reproducer holds 2.5 GB — near-ceiling occupancy, not
contention. The ballast test that would settle it was not run because that has
OOM-rebooted this box.
Suggestive, explicitly not a claim: every collapse landed on the flat arm
(NCOLS 1: 3/23 legs; NCOLS 4/8: 0/31), Fisher one-sided p ≈ 0.07. Needs ~60 more
legs.
And it is not "bigger than every other lever", which is also my
overstatement: at 1 leg in 16 it costs ~6% of mean decode time, against the 8.5%
column blocking removes deterministically.
Gates, verified by the operator on the MERGED tree
The three-lever combination is something no row tested. On a clean llvmpipe build
of the merge:
test_vulkan_backend32/32, 1905 assertions (all three rows'mechanism tests present),
test_backend_cross_device11/11,VLLM_CPP_DEVICE=vulkan test_opt_paged_engine6/6 token-exact (96/96), 0declines, and
gen-vulkan-spirv.py --checkreproduces the committed SPIR-Vbyte-for-byte under pinned glslang 16.5.0.
Row-reported on GB10, not re-run here:
test_vulkan_backend30/30 (2369),test_backend_cross_device11/11,test_opt_paged_engine6/6 token-exact.Conflicts, resolved by kind
Eight, none by automatic three-way combine. Union for the append-only record, the
spec narrative,
ENVIRONMENT.mdrows and the test cases; merge-by-key forSTATUS.mdand theBENCHMARKS.mdrow (trimmed to the 220-char cell bound). Invulkan_context.{h,cpp}both rows had added a near-identical pipeline-keyaccessor under different names — kept the general
PipelineKeys()as the onetaking the mutex and reimplemented
PipelineKeysFor()as a filter over it, ratherthan shipping two walks of the same cache. A resolution script asserted anchor
uniqueness, which caught that
=======appears nine times as a comment rule inthe test file.
Known red, named rather than papered over
check-pr-sizecounts the regeneratedvulkan_spirv.cppas reviewable productcode;
docs/STATUS.mdremains over its shrink-only ratchet, pre-existing on main.FOLLOWING_AGENTS_PROTOCOL