Skip to content

vulkan: implement MatMul (fixes #6858)#6865

Open
giuliocorradi wants to merge 1 commit into
Tencent:masterfrom
giuliocorradi:vulkan-matmul
Open

vulkan: implement MatMul (fixes #6858)#6865
giuliocorradi wants to merge 1 commit into
Tencent:masterfrom
giuliocorradi:vulkan-matmul

Conversation

@giuliocorradi

Copy link
Copy Markdown

Implements the Vulkan MatMul requested in #6858.

MatMul had no Vulkan implementation — there is no src/layer/vulkan/matmul_vulkan.cpp — so every MatMul in a Vulkan graph ran on the CPU, forcing a download of both operands and a re-upload of the result mid-graph. On a 22-model YOLO zoo, yolo12n (16 MatMul layers) was the only model where Vulkan was slower than the CPU.

Results

Raw forward pass, Radeon 8060S (Strix Halo, RADV GFX1151), 640×640, 4 threads, median of 3 alternating sessions, same build on both sides:

model MatMuls Vulkan before Vulkan after
yolo12n 16 62.64 ms 8.93 ms 7.0x
yolo11n 2 9.77 ms 6.02 ms 1.6x
yolov8n 0 5.99 ms 5.93 ms 1.01x

yolov8n contains no MatMul and is included as a control — it does not move, which is what attributes the other two gains to this layer rather than to build or machine differences.

Two notes on honesty of measurement:

Correctness

tests/test_matmul passes. It runs the GPU path through test_layer, covering 1-D through 4-D operands, transB, and broadcasting.

Full ctest: 177/178 both with and without this commit, identical failure sets. The single failure is test_binaryop_3test_binaryop_atan2_special op_type=10, a pre-existing atan2 special-value mismatch on RADV, byte-identical on pristine a4d2ea1.

Worth flagging: my first draft failed test_matmul, and the failures were instructive enough to be worth recording here.

  1. Degenerate axes. A 1-D operand contributes no dimension of its own. MatMul::forward computes into a (N,1) / (1,M) blob and reshapes the extra axis away. Creating (N, M, …) unconditionally gave dims=2 where the CPU gives dims=1. The final shape is now created directly and described to the shader with explicit strides, because a reshaped blob does not carry the strides the general case implies.

  2. Rank promotion. With mixed ranks, MatMul::forward lifts a 3-D operand with reshape(w, h, c, 1) — its channel axis becomes the depth axis, and depth then strides by the original cstep, not w*h. Reading A.c as channels rejected valid broadcasts: test_matmul_13 (A (14,23,10) × B (5,14,1,16)) hit the broadcast guard, returned −1, and the never-created blob then segfaulted in record_download.

  3. Dispatch grid. Dispatch is over the logical (N, M, outd, outc) grid rather than over the output blob, since a reshaped blob no longer carries M and outd separately while the shader decodes depth out of y as gy / M. The Mat dispatcher overload supplies the group counts without allocating.

Implementation notes

  • support_vulkan_packing stays at its default false: the shader indexes elements one at a time, which keeps batch slicing a simple stride instead of re-deriving pack4/pack8 addressing for every rank.
  • Shape specialisation constants are left unspecialised, so shapes are resolved per call via push constants and one pipeline serves every shape.
  • ncnn_add_layer(MatMul) already picks the Vulkan source up; no CMake change needed.

This is a straightforward scalar implementation — correct and a large improvement over the CPU round trip, but not a tuned GEMM. Tiling and cooperative matrix would go considerably further on the batched attention shapes from #6858. Happy to iterate if you would rather land something closer to that first.

🤖 Generated with Claude Code

MatMul had no Vulkan implementation, so every MatMul in a Vulkan graph ran
on the CPU. That forces a download of both operands and a re-upload of the
result in the middle of the graph, which is the dominant cost for
attention-based models -- on a 22-model YOLO zoo, yolo12n (16 MatMul
layers) was the only model where Vulkan was *slower* than the CPU.

The shader indexes elements one at a time and keeps the unpacked layout
(support_vulkan_packing stays false), which makes batch slicing a simple
stride instead of re-deriving pack4/pack8 addressing for every rank.

Output shape and broadcasting follow MatMul::forward exactly:

  * A 1-D operand contributes no dimension of its own. The CPU path
    computes into a degenerate (N,1) or (1,M) blob and reshapes the extra
    axis away, so the final shape is created directly here and described to
    the shader with explicit strides -- a reshaped blob does not carry the
    strides the general case would imply.

  * With mixed ranks, MatMul::forward lifts a 3-D operand with
    reshape(w, h, c, 1), which turns its CHANNEL axis into the DEPTH axis;
    depth then strides by the original cstep rather than by w*h.

  * Dispatch is over the logical (N, M, outd, outc) grid rather than over
    the output blob, because a reshaped blob no longer carries M and outd
    separately and the shader decodes depth out of y as gy / M.

Correctness: tests/test_matmul passes (it exercises the GPU path through
test_layer, covering 1-D through 4-D, transB and broadcasting). Full ctest
is 177/178 both with and without this commit -- identical failure sets; the
one failure is a pre-existing atan2 special-value mismatch on RADV.

Raw forward pass on a Radeon 8060S (Strix Halo, RADV GFX1151), 640x640,
4 threads, median of 3 alternating sessions, same build both sides:

    model     MatMuls   vulkan before   vulkan after
    yolo12n        16        62.64 ms        8.93 ms    7.0x
    yolo11n         2         9.77 ms        6.02 ms    1.6x
    yolov8n         0         5.99 ms        5.93 ms    1.01x

yolov8n has no MatMul and is included as a control: it does not move.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@tencent-adm

Copy link
Copy Markdown
Member

CLA assistant check
Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our Contributor License Agreement before we can accept your contribution.
You have signed the CLA already but the status is still pending? Let us recheck it.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe0817fbae

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +190 to +194
else if (outc == 1 && outd == 1)
{
top_blob.create(N, M, elemsize, opt.blob_vkallocator);
}
else if (outd == 1)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Preserve the input-derived rank for singleton batches

When neither operand is 1-D, these extent-based branches collapse singleton batch axes: a rank-3 multiplication with batch size 1 returns a 2-D blob, while a rank-4 multiplication with outd == 1 returns a 2-D or 3-D blob. The CPU implementation instead unconditionally creates rank 3 for max_ABdims == 3 and rank 4 for max_ABdims == 4, even when those extents equal one, so enabling Vulkan changes the output shape and can make downstream layers interpret its axes differently. Select the output rank from max_ABdims, not solely from outc/outd values.

Useful? React with 👍 / 👎.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants