Skip to content

[ET-VK] Lower eligible conv1d as conv2d over a singleton height dim - #22330

Open
msluszniak wants to merge 1 commit into
pytorch:mainfrom
msluszniak:ms/vulkan-conv1d-as-conv2d
Open

[ET-VK] Lower eligible conv1d as conv2d over a singleton height dim#22330
msluszniak wants to merge 1 commit into
pytorch:mainfrom
msluszniak:ms/vulkan-conv1d-as-conv2d

Conversation

@msluszniak

@msluszniak msluszniak commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #22329.

conv1d.glsl computes one output element per invocation with no tiling, no shared memory and no register blocking, and every tap re-fetches both a weight texel and an input texel, so nothing is reused between invocations. On top of that conv1d_gwg sets z = div_up_4(batch), so the texel's four lanes span the batch dimension and three of every four are dead at batch 1.

Measured inside a single Whisper-tiny encoder execution on an Adreno 840, using the shader query pool:

dispatch useful throughput
conv1d_float, 80 -> 384, k=3, L=3000 31.3 GFLOP/s
conv1d_float, 384 -> 384, k=3, s=2, L=1500 27.6 GFLOP/s
linear_vec_bias, M=1500, N=1536, K=384 921 GFLOP/s
linear_vec_bias, M=1500, N=384, K=1536 900 GFLOP/s

Same GPU, same run. Those two conv1d dispatches were 43% of the encoder's runtime.

conv2d_im2col.glsl + conv2d_gemm.glsl already solve this for 2-D convolutions. This pass rewrites 1-D convolutions into a 2-D convolution over a singleton height dim so the existing conv2d machinery (im2col selection, memory layout tagging, weight prepacking) handles them with no runtime changes.

The weight is reshaped in place through set_param_tensor rather than with a view_copy node, because add_conv2d_node reads it as a constant TensorRef and a runtime view would hand it a TENSOR instead.

Eligibility

Only convolutions that are certain to reach the im2col path are rewritten, so nothing silently moves onto a shader that has not been compared against conv1d:

  • groups == 1 and unit dilation, matching should_use_conv2d_im2col
  • out_channels >= kIm2colMinCOut (128), so the im2col path is selected on every vendor rather than only on Mali
  • kernel_size > 1, leaving pointwise convs on conv1d_pw
  • batch 1, since conv2d throws on batched input and a rewrite would turn a working conv1d into a hard error
  • a constant weight, so it can be reshaped in place

Results

Whisper-tiny encoder, Samsung Galaxy S26 Ultra (Adreno 840), fp32, interleaved A/B with the order reversed each round, warm-up iteration discarded, 60 timed iterations per arm. Both Vulkan arms include #22328, without which the encoder is not deterministic on this GPU at all.

arm median vs XNNPACK
Vulkan before 153.2 ms 0.84x
XNNPACK 128.0 ms 1.00x
Vulkan after 88.8 ms 1.44x

Cosine against the CPU reference is 0.99999684 after the change.

Isolated, just the encoder's two convs plus gelu, GPU time from the query pool:

before after
conv dispatches conv1d 17.7 + 47.7 ms im2col 0.05 + gemm 0.64 + im2col 0.19 + gemm 1.47 ms
total 66.5 ms 3.28 ms

20x on the frontend, and the output is unchanged at cosine 1.00000000, max abs diff 4.9e-04 against the CPU reference in both.

Routing and correctness matrix

Eight conv1d configurations exported and run on device, with the dispatched shader read back from the query pool:

case shader after the pass rewritten cosine max abs diff
80 -> 384, k=3 conv2d_im2col + conv2d_gemm yes 1.00000000 2.0e-06
384 -> 384, k=3, s=2 conv2d_im2col + conv2d_gemm yes 1.00000000 4.1e-06
128 -> 256, k=5, p=0 conv2d_im2col + conv2d_gemm yes 1.00000012 3.1e-06
64 -> 64, k=3 conv1d_float no 1.00000000 8.3e-07
256 -> 256, k=1 conv1d_pw_bias_texture no 1.00000000 9.5e-07
256 -> 256, k=3, depthwise conv1d_dw_bias_texture no 1.00000012 4.8e-07
128 -> 256, k=3, dilation 2 conv1d_float no 1.00000000 1.2e-06
256 -> 256, k=3, groups 4 conv1d_float no 1.00000000 9.5e-07

Every ineligible case keeps its original shader and every case matches the CPU reference.

test_vulkan_backend_conv1d_as_conv2d covers the rewritten path in CI; the two existing conv1d tests are grouped and depthwise, so they only exercise the untouched path.


Depends on #22328

Moving the conv frontend onto the im2col + GEMM path exposes a pre-existing Adreno defect in conv2d_gemm, which dispatches with the same near-square local work group as linear. Distinct outputs over 60 executions on the Adreno 840, with Linear.cpp already fixed:

distinct / 60
Whisper encoder, conv1d frontend 1
Whisper encoder, this PR 9
Whisper conv frontend alone, this PR 13

This is not caused by the rewrite. conv2d_gemm is already reached today by any conv2d with out_channels >= 128, and it is nondeterministic there for the same reason. #22328 now fixes the shared pick_xy_square_lwg helper rather than one caller, and with it every model above is 1 / 60.

The 88.8 ms figure above was measured with only Linear.cpp fixed. With #22328 applied in full the encoder is 91.4 ms, still 1.34x XNNPACK, and correct on every run.

conv1d.glsl computes one output element per invocation with no tiling and no
reuse between invocations, and its work grid packs texels along the batch dim,
wasting three of every four lanes at batch 1. On an Adreno 840 it reaches
about 30 GFLOP/s where the tiled linear reaches 900 GFLOP/s in the same graph.

The conv2d im2col + GEMM path already solves this, so rewrite 1-D convolutions
that are certain to reach it into a 2-D convolution over a singleton height
dim. Only convs with groups 1, unit dilation, kernel > 1, batch 1 and
out_channels >= kIm2colMinCOut are rewritten, so nothing moves onto a shader
that has not been compared against conv1d.

The Whisper-tiny encoder goes from 153.2 ms to 88.8 ms, from 0.84x XNNPACK to
1.44x.

Fixes pytorch#22329
@msluszniak
msluszniak requested a review from SS-JIA as a code owner August 30, 2026 12:55
@pytorch-bot

pytorch-bot Bot commented Aug 30, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22330

Note: Links to docs will display an error until the docs builds have been completed.

⚠️ 15 Awaiting Approval

As of commit 2d0e6fc with merge base c27baa8 (image):

AWAITING APPROVAL - The following workflows need approval before CI can run:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Aug 30, 2026
@github-actions

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

msluszniak added a commit to software-mansion-labs/executorch that referenced this pull request Aug 30, 2026
conv1d.glsl has no tiling and no reuse between invocations and packs its texels
along the batch dim, so it runs at roughly 3 percent of the throughput the same
GPU reaches on a matmul of equal MAC count. Route eligible 1-D convolutions
through the existing conv2d im2col + GEMM path instead.

Also backports set_param_tensor from upstream, which the pass uses to reshape
the weight in place.

Upstream: pytorch/executorch#22329, pytorch/executorch#22330
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ET-VK] conv1d runs at ~3% of the GPU's matmul throughput; routing it through the conv2d im2col path is 25x faster

2 participants