Skip to content

VK-C: cooperative-matrix GEMM tactic, verified executing on GB10 - #97

Merged
localai-bot merged 4 commits into
mainfrom
row/BACKEND-VULKAN-COOPMAT
Aug 7, 2026
Merged

VK-C: cooperative-matrix GEMM tactic, verified executing on GB10#97
localai-bot merged 4 commits into
mainfrom
row/BACKEND-VULKAN-COOPMAT

Conversation

@localai-bot

@localai-bot localai-bot commented Aug 7, 2026

Copy link
Copy Markdown
Collaborator

VK-C: the cooperative-matrix (tensor-core) GEMM tactic, verified executing on GB10.

NVIDIA GB10:  coopmat bf16xbf16->f32 16x16x16 SUBGROUP: YES, subgroup size 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

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:

  • coopmat where the device reports 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 — 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 own cpu_ops.cpp MatmulChunked, the oracle for both tactics.

Selection: four hard requirements

Each failure falls back to the always-correct scalar kernel.

requirement why it is not a heuristic
exact configuration reported Vulkan has no nearest-fit match
subgroup size 32 the shader's workgroup is a literal 32
both operands bf16 every reported configuration takes bf16/f16/int8 — f32 can never use this path
K % 16 == 0 a ragged K tail cannot be masked inside a coopmat load; truncating silently drops dot-product terms

Ragged M and N are fine — the store is bounds-checked.

Two traps avoided because they were already on record

local_size_x_id is unusable at vulkan1.1 — it emits ExecutionMode LocalSize 1 1 1 and 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 coopMatStore would have to target the uint32 view this backend binds — storing a coopmat<float> through a uint pointer — 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 VkDeviceCreateInfo makes vkCreateDevice fail 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-macro VT_CHECKs every name, which is right for core functions and fatal for extension ones.

Split for reviewability

check-pr-size flagged 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-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.

Gates

Clean -Werror Vulkan-ON builds on both boxes, 0 warnings. Local (llvmpipe): test_vulkan_backend 12/12, test_backend_cross_device 11/11, generator 14/14. dgx (GB10): test_vulkan_backend 12/12 (491), test_backend_cross_device 11/11.

Pre-existing red, not from this branch: check-fusion-consistency on minimax_h3_video_vae_device, failing on main since before this work.

🤖 Generated with Claude Code

mudler added 4 commits August 7, 2026 06:38
…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
mudler force-pushed the row/BACKEND-VULKAN-COOPMAT branch 2 times, most recently from 42db1fd to 2d52074 Compare August 7, 2026 07:56
@localai-bot
localai-bot merged commit cf7f5f5 into main Aug 7, 2026
17 of 22 checks passed
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