VK-C: cooperative-matrix GEMM tactic, verified executing on GB10 - #97
Merged
Conversation
…tionally
VK-C foundation. No coopmat kernel yet -- this is the capability layer the tactic
selection will branch on, plus the measured premises that shape the kernel.
MEASURED 2026-08-07, not assumed:
GB10 11 configurations, ALL SUBGROUP scope, including
16x16x16 A=bf16 B=bf16 C=f32 R=f32 <- our model dtype, our f32
accumulate contract. Also f16->f16, f16->f32 and u8/s8->u32/s32.
llvmpipe VK_KHR_cooperative_matrix NOT PRESENT AT ALL.
Three consequences worth stating before any kernel exists. (1) The coopmat path
can only ever be executed on dgx -- CI reaches llvmpipe only, so the CI leg tests
the SCALAR FALLBACK, and tactic selection returning "no coopmat" there is the
property CI can actually gate. (2) f32 operands can NEVER use coopmat: every
reported configuration takes f16/bf16/int8 inputs, so falling back is a hardware
constraint rather than a policy. (3) The coopmat path accumulates in 16x16x16
tiles, so it cannot inherit the scalar kernel's per-output-element sequential f32
order; it will sit in the NMSE tier and claim no order-matching with the CPU.
ENABLEMENT IS CONDITIONAL, and that is load-bearing: naming an unsupported
extension in VkDeviceCreateInfo makes vkCreateDevice FAIL OUTRIGHT, so an
unconditional request would take the entire Vulkan backend down on llvmpipe. The
predicate is narrow on purpose -- extension present AND VK_KHR_shader_bfloat16 AND
an EXACT 16x16x16/bf16/bf16/f32/f32/SUBGROUP configuration AND a known subgroup
size. Vulkan matches configurations exactly; there is no "nearest", so a device
with only f16 or int8 configurations correctly gets the scalar path.
The loader gains an OPTIONAL entry-point tier for this. The existing X-macro
VT_CHECKs every name, which is right for core functions and fatal for extension
ones: vkGetPhysicalDeviceCooperativeMatrixPropertiesKHR is legitimately null on
llvmpipe and would have aborted the backend at startup.
One honesty fix in the gate itself: `MESSAGE("text" << flag)` expands to
`MessageBuilder << text << flag` and renders as "1", which reads as if the
capability were TRUE. For a line whose whole job is to report a capability, that
is the worst failure mode, so the string is assembled before the macro. It now
prints "no" on llvmpipe, which is the correct answer.
Vulkan gate 11/11 (485 assertions), cross-device 11/11 (123), clean -Werror
Vulkan-ON build 0 warnings.
FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-5 [Claude Code]
… fallback The tensor-core tactic for kMatmul/kMatmulBT. LANDS THE KERNEL; DOES NOT VERIFY IT ON HARDWARE -- see the honesty note at the end. SHAPE DICTATED BY HARDWARE, NOT CHOSEN. Probed on GB10 2026-08-07: 11 cooperative-matrix configurations, all SUBGROUP scope, and this shader is written to exactly one of them -- 16x16x16, A=bf16 B=bf16 C=f32 R=f32. Vulkan matches configurations EXACTLY, with no nearest-fit, so the host predicate asks for that tuple and nothing else. Structure ported from llama.cpp mul_mm_cm2.comp @ 237ad9b96 (one subgroup per 16x16 output tile, K walked in 16-wide steps, coopMatMulAdd accumulating in f32); the per-element semantics come from our cpu_ops.cpp MatmulChunked, which is the oracle for both tactics. SELECTION IS FOUR HARD REQUIREMENTS, not heuristics -- device reports that exact configuration, subgroup size 32, BOTH operands bf16, and K % 16 == 0. Each failure means the scalar kernel runs, which is always correct. f32 operands can NEVER use this path because every reported configuration takes bf16/f16/int8 inputs; that is hardware, not policy. K must be whole because a ragged tail cannot be masked inside a cooperative-matrix load and truncating it would silently drop dot-product terms. Ragged M and N are fine -- the shader bounds-checks its store. TWO TRAPS AVOIDED, BOTH ALREADY ON RECORD IN THIS BACKEND. The workgroup size is a LITERAL 32, not local_size_x_id: that spelling emits `ExecutionMode LocalSize 1 1 1` at the vulkan1.1 target and silently runs one thread per workgroup (measured earlier, recorded in vt_common.glsl), so the size is compiled in and the host refuses the tactic on any device reporting a different subgroup size. And the result is ALWAYS spilled through shared memory rather than stored directly: a direct coopMatStore would have to target the uint32 view this backend binds, i.e. store a coopmat<float> through a uint pointer, which glslang ACCEPTS WITHOUT COMPLAINT -- and no device reachable from CI can execute a coopmat shader at all, so that mistake would not have surfaced here. A direct-store fast path is a legitimate later refinement, but it is a SPEED change and belongs behind an A/B on real hardware, not in the landing that first has to be correct. NUMERICS: this tactic does NOT match the scalar one bit-for-bit and does not claim to. The scalar kernel walks K sequentially per output element precisely so its order matches the CPU's; a cooperative matrix accumulates a tile at a time in a hardware-defined order. Both accumulate in f32 and round once on store, so both belong in the NMSE tier -- but only the scalar one shares the CPU's order. HONEST STATE: NOT ONE COOPERATIVE-MATRIX INSTRUCTION HAS EXECUTED. llvmpipe, the only Vulkan device reachable from CI and from this box, does not expose VK_KHR_cooperative_matrix at all, so every gate here exercises the SCALAR tactic and the selection correctly returning false. The kernel compiles and the tactic logic is gated; the dgx/GB10 run is owed and is what would make this a claim. Vulkan gate 11/11 (490 assertions), cross-device 11/11 (123), generator 14/14, clean -Werror build 0 warnings. FOLLOWING_AGENTS_PROTOCOL Assisted-by: Claude Code:claude-opus-5 [Claude Code]
…ually ran
NVIDIA GB10: coopmat bf16xbf16->f32 16x16x16 SUBGROUP: YES, subgroup 32
bf16 GEMM tactic: COOPMAT
bf16 GEMM NMSE vs the f32 oracle: 0.000000
test_vulkan_backend 12/12, 491 assertions
llvmpipe: coopmat: no -> tactic: scalar, coopmat module NEVER BUILT
THE GATE THAT ALMOST WASN'T, and it is the point of this commit. The pre-existing
cross-device GEMM case uses f32 operands; the tactic requires bf16. It therefore
passed on GB10 while exercising the coopmat path NOT AT ALL -- green over an
unexercised path, which reads exactly like success. Numbers can never separate a
working tactic from a silent fallback here, because the scalar kernel is equally
correct. So VulkanContext::PipelineExistsFor(module) is added and the load-bearing
assertion is the TACTIC: coopmat where the device has the configuration, scalar
where it does not, and on llvmpipe the coopmat module asserted NEVER BUILT --
selecting it there would fail at pipeline creation. Same failure shape the
op-provider decline counters exist for.
NMSE 0.0 is expected, not suspicious: the inputs are exactly representable in
bf16, so the products are exact and f32 accumulation of 32 terms has nothing to
round. That isolates the TILE ORDERING as the thing under test rather than input
narrowing. M=20, N=12 are ragged on purpose so the bounds-checked store runs.
Selection is four HARD requirements, each falling back to the always-correct
scalar kernel: the exact reported configuration, subgroup size 32, BOTH operands
bf16, and K % 16 == 0. f32 operands can never take this path because every
configuration GB10 reports takes bf16/f16/int8 -- hardware, not policy. A ragged K
tail cannot be masked inside a cooperative-matrix load and truncating it would
silently drop dot-product terms.
NO SPEED NUMBER IS CLAIMED. The tactic is correct and it engages; whether it beats
the scalar kernel is unmeasured, and that A/B on GB10 is what VK-C still owes.
Numerics also do not match the scalar tactic bit-for-bit and do not claim to --
the scalar kernel walks K sequentially per output element to match the CPU's
order, a cooperative matrix accumulates a tile at a time in a hardware-defined
one. Both sit in the NMSE tier.
FOLLOWING_AGENTS_PROTOCOL
Assisted-by: Claude Code:claude-opus-5 [Claude Code]
…OPMAT # Conflicts: # .agents/state.md # docs/BENCHMARKS.md # docs/STATUS.md # scripts/check-public-doc-tables.py
mudler
force-pushed
the
row/BACKEND-VULKAN-COOPMAT
branch
2 times, most recently
from
August 7, 2026 07:56
42db1fd to
2d52074
Compare
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.
VK-C: the cooperative-matrix (tensor-core) GEMM tactic, verified executing on GB10.The gate that almost wasn't
The pre-existing cross-device GEMM case uses f32 operands. The tactic requires bf16. So it passed on GB10 while exercising the coopmat path not at all — green over an unexercised path, which reads exactly like success.
Numbers can never separate a working tactic from a silent fallback here, because the scalar kernel is equally correct. So
VulkanContext::PipelineExistsFor(module)is added, and the load-bearing assertion is the tactic, not the NMSE:Same failure shape the op-provider decline counters exist for.
NMSE 0.0 is expected, not suspicious. The inputs are exactly representable in bf16, so the products are exact and f32 accumulation of 32 terms has nothing to round — which isolates the tile ordering as the thing under test rather than input narrowing. M=20, N=12 are ragged on purpose so the bounds-checked store runs.
Shape dictated by hardware, not chosen
Probed on GB10 first: 11 configurations, all SUBGROUP scope — f16→f16 and f16→f32 at 16x16x16 / 16x8x16 / 16x8x8, u8/s8→u32/s32 at 16x16x32 / 16x8x32, and 16x16x16 bf16/bf16/f32/f32, which is our model dtype with our f32-accumulate contract. Vulkan matches configurations exactly — there is no nearest-fit — so the shader is written to that one tuple and the host predicate asks for it and nothing else.
Structure ported from llama.cpp
mul_mm_cm2.comp@237ad9b96; per-element semantics from our owncpu_ops.cppMatmulChunked, the oracle for both tactics.Selection: four hard requirements
Each failure falls back to the always-correct scalar kernel.
Ragged M and N are fine — the store is bounds-checked.
Two traps avoided because they were already on record
local_size_x_idis unusable atvulkan1.1— it emitsExecutionMode LocalSize 1 1 1and silently runs one thread per workgroup (measured earlier in this campaign). The workgroup size is a literal 32, so the SPIR-V is compiled for subgroup 32 and the host refuses the tactic on any device reporting otherwise.The result is always spilled through shared memory. A direct
coopMatStorewould have to target the uint32 view this backend binds — storing acoopmat<float>through auintpointer — which glslang accepts without complaint, and no CI-reachable device can execute a coopmat shader, so it would never have surfaced. A direct-store fast path is a legitimate later refinement, but it is a speed change and belongs behind a hardware A/B.Conditional extension enablement
Naming an unsupported extension in
VkDeviceCreateInfomakesvkCreateDevicefail outright, so an unconditional request would take the entire Vulkan backend down on llvmpipe. Enablement is gated on the probe, and the loader gains an optional entry-point tier — its existing X-macroVT_CHECKs every name, which is right for core functions and fatal for extension ones.Split for reviewability
check-pr-sizeflagged the original 969 lines against a 900 cap, so this PR was split at a natural seam: this one is the tactic, and the A/B harness plus its measured numbers follow in a separate PR. That keeps each reviewable on its own terms rather than trimming arbitrarily.The speed result exists and is good — 11.1x-32.9x on NVIDIA Thor — but it belongs with the harness that produced it.
What is NOT claimed
No speed number. The tactic is correct and it engages; whether it beats the scalar kernel is unmeasured, and that A/B on GB10 is what
VK-Cstill owes.Numerics also do not match the scalar tactic bit-for-bit and do not claim to — the scalar kernel walks K sequentially per output element to match the CPU's order; a cooperative matrix accumulates a tile at a time in a hardware-defined one. Both sit in the NMSE tier.
Gates
Clean
-WerrorVulkan-ON builds on both boxes, 0 warnings. Local (llvmpipe):test_vulkan_backend12/12,test_backend_cross_device11/11, generator 14/14. dgx (GB10):test_vulkan_backend12/12 (491),test_backend_cross_device11/11.Pre-existing red, not from this branch:
check-fusion-consistencyonminimax_h3_video_vae_device, failing onmainsince before this work.🤖 Generated with Claude Code