Skip to content

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

Description

@msluszniak

馃悰 Describe the bug

conv1d.glsl runs at roughly 3 percent of the throughput the same GPU reaches on a matmul with the same MAC count. It is the single reason the Whisper-tiny encoder loses to XNNPACK on Adreno.

Re-expressing the two nn.Conv1d layers of the Whisper-tiny encoder as nn.Conv2d with a singleton height dim, which routes them to the existing conv2d_im2col + conv2d_gemm path, is 25x faster on the conv frontend and turns the whole encoder from 0.79x XNNPACK into 1.40x.

Measurements

Samsung Galaxy S26 Ultra, Snapdragon SM8850 (Adreno 840), ExecuTorch at c27baa8, fp32.

Whisper-tiny encoder end to end, interleaved A/B with the order reversed each round, warm-up iteration discarded, 60 timed iterations per arm:

arm median vs XNNPACK
Vulkan 163.3 ms 0.79x
XNNPACK 129.7 ms 1.00x
Vulkan, conv1d re-expressed as conv2d with H=1 92.4 ms 1.40x

Per-dispatch GPU timestamps from the shader query pool, one encoder execution (153.4 ms of GPU time against 154 ms of wall clock, so this is entirely GPU bound):

kernel dispatches ms share
conv1d_float 2 65.7 42.8%
matmul_vec_texture3d_float 8 22.0 14.3%
linear_vec_bias_texture3d_texture2d_float 20 21.6 14.1%
softmax_texture3d_float 4 16.8 11.0%
view_texture_float 37 10.4 6.8%
clone_float 32 9.0 5.9%

Two dispatches, 43 percent of the runtime. Converting those to useful arithmetic:

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

Same GPU, same run, seconds apart, and the two linear dispatches have an identical MAC count to each other. conv1d is 30x off what the machine demonstrably does.

Why

Two things in the conv1d path:

  1. conv1d.glsl computes one output element per invocation with no tiling, no shared memory and no register blocking. Every tap re-fetches both a weight texel and an input texel, so nothing is reused across invocations. The 384 -> 384 layer issues 1152 iterations x 2 texture fetches per invocation across 576k invocations, about 1.3 G texture fetches for 664 M MACs.

  2. conv1d_gwg sets z = div_up_4(batch), so the texel's four lanes span the batch dimension. At batch 1, which is every audio and sequence model I am aware of, three of every four lanes are dead. The nchw_to_image dispatch for a [1, 80, 3000] input is [3000, 80, 1], a 4x over-allocated texture, against [3000, 1, 20] for the equivalent [1, 80, 1, 3000].

The conv2d path already solves this

should_use_conv2d_im2col() routes SlidingWindow conv2d to conv2d_im2col.glsl + conv2d_gemm.glsl when groups == 1 && dilation == 1 && (device_is_mali() || c_out >= 128). conv1d has no equivalent and always dispatches the naive shader.

Isolated test, just the Whisper conv frontend (two convs plus gelu), same weights in both, both verified against the CPU reference at cosine 1.00000000 and max abs diff 4.9e-04:

form GPU time dispatches
Conv1d 66.5 ms conv1d 17.7 + conv1d 47.7
Conv2d, H=1 2.64 ms im2col 0.06 + gemm 0.64 + im2col 0.19 + gemm 1.47

25x on the frontend, 27.6x on the conv dispatches alone.

Full encoder with the conv layers wrapped so they lower as conv2d: one delegate blob, cosine 0.99999684 against the CPU reference, and the 92.4 ms in the first table.

Suggested fix

Rewrite aten.convolution with a 3-D input into unsqueeze -> conv2d -> squeeze before partitioning, so the existing conv2d machinery (im2col selection, layout tagging, weight prepacking) applies unchanged. I will send a PR.

Note that Whisper on Adreno additionally needs #22327 to be correct at all; this issue is only about speed.

Versions

ExecuTorch at c27baa8. Device: Samsung Galaxy S26 Ultra, Snapdragon SM8850, Adreno 840, Android 16.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions