Skip to content

[ET-VK][conv2d] Skip the store rounding of texels computed in f16 - #24940

Merged
SS-JIA merged 1 commit into
gh/SS-JIA/707/origfrom
gh/SS-JIA/708/orig
Nov 12, 2026
Merged

SS-JIA merged 1 commit into
gh/SS-JIA/707/origfrom
gh/SS-JIA/708/orig

Conversation

@pytorchbot

Copy link
Copy Markdown
Collaborator

This PR was created by the merge bot to help merge the original PR into the main branch.
ghstack PR number: #24857 by @SS-JIA
^ Please use this as the source of truth for the PR details, comments, and reviews
ghstack PR base: https://github.com/pytorch/executorch/tree/gh/SS-JIA/708/base
ghstack PR head: https://github.com/pytorch/executorch/tree/gh/SS-JIA/708/head
Merge bot PR base: https://github.com/pytorch/executorch/tree/gh/SS-JIA/707/orig
Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/SS-JIA/708/orig
Differential Revision: D130986955
@diff-train-skip-merge

## Ulterior Motive

Optimizing the FP16 1x1 convolutions of the DSR model on mobile GPUs.

## Rationale

**What**: `f16vec4` texels are stored to rgba16f images without the shader generator's FP32 rounding, and the conv2d epilogue (residual, scale, clamp, ReLU, leaky ReLU) computes in the shader's texel type. Half pointwise (`conv2d_pw_tiled`), direct (`conv2d_tiled`) and buffer-input im2col (`conv2d_gemm`) convolutions now pass their f16 accumulators to the store as they are.

**Why**: A texel computed in f16 is exact in f16, so the rounding never changes it, but it costs about 40 instructions per stored texel on Adreno 740. The epilogue took and returned `vec4`, so every Half store paid for the rounding, with or without an epilogue. With this commit three DSR 1x1 layers take 0.84 to 0.93 of the time on a Pixel 9 Pro XL (Mali-G715) and 0.87 to 0.91 on a Galaxy S23 (Adreno 740), and a DSR 3x3 direct convolution 0.92 and 0.96. A Galaxy S26 Ultra (Adreno 840) is within noise.

## Details

- Store rounding
  - **Before**: Every store to an rgba16f image passes its texel through `round_to_f16(vec4)`, which rounds to the nearest f16 with FP32 bit manipulation because some devices round toward zero. An `f16vec4` texel converts to `vec4` and is rounded too, which never changes it.
  - **After**: The generator also declares `round_to_f16(f16vec4)`, which only widens. `f16vec4` is a type only where `GL_EXT_shader_explicit_arithmetic_types_float16` is enabled, so the overload is declared under `#ifdef HAS_FLOAT16_ACCUMULATION`, which the conv2d shaders that accumulate in f16 define before their output image declaration. Other shaders keep rounding their texels.
- Epilogue
  - **Before**: `apply_epilogue()` computes in `vec4`; callers cast f16 accumulators to `vec4`.
  - **After**: It computes in `VEC4_T`, including the residual alpha multiply and the leaky ReLU slope, and the pointwise, direct and im2col shaders pass their accumulators without a cast. Where `VEC4_T` is `vec4` (Float variants, depthwise and transposed convolutions, texture-input im2col), the code compiles to the same Adreno 740 ISA.
- Epilogue parameters
  - **Before**: The alpha, slope and clamp bounds reach the shaders as given.
  - **After**: For the shaders that compute the epilogue in f16, the host rounds them to the nearest f16, ties to even, so their conversion to `f16vec4` is exact even where the GPU rounds conversions toward zero. `half_to_float` and `float_to_half` move out of `StagingBuffer.cpp`'s anonymous namespace for this.

Outputs without an epilogue are bit-identical. With an epilogue, Half variants that compute in f16 round to f16 after the residual add, the scale and the leaky ReLU, where they rounded once at the store before. Clamps are bit-identical: rounding is monotonic, so clamping an f16 value to the rounded bounds gives the rounded clamp of it. `FLT_MAX` bounds round to infinity.

16 SPIR-V modules change:
- `conv2d_pw_tiled_half`, `conv2d_pw_tiled_bufw_half`, `conv2d_pw_tiled_4x8_half`, `conv2d_pw_tiled_4x8_bufw_half`
- the 11 Half `conv2d_tiled` modules (`conv2d_1x1x4_half`, `conv2d_2x2x8_border_half`, ...)
- `conv2d_gemm_buffer_half`

32 more modules (the Float conv2d modules, and the Half depthwise, transposed and texture-input im2col modules) differ in the form of the epilogue's conversions and compile to identical Adreno 740 ISA.

Differential Revision: [D130986955](https://our.internmc.facebook.com/intern/diff/D130986955/)


ghstack-source-id: 469962071
Pull-Request: #24857
@pytorchbot
pytorchbot requested a review from SS-JIA as a code owner November 12, 2026 00:08
@pytorch-bot

pytorch-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🔗 Helpful Links

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

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

❗ 1 Active SEVs

There are 1 currently active SEVs. If your PR is affected, please view them below:

⏳ 17 Pending, 1 Unrelated Failure

As of commit 888e31c with merge base 115655c (image):

BROKEN TRUNK - The following job failed but were present on the merge base:

👉 Rebase onto the `viable/strict` branch to avoid these failures

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 Nov 12, 2026
@SS-JIA
SS-JIA added this pull request to stack #24958 November 12, 2026 00:37
@SS-JIA
SS-JIA merged commit 5f388c9 into main Nov 12, 2026
218 of 228 checks passed
@SS-JIA
SS-JIA deleted the gh/SS-JIA/708/orig branch November 12, 2026 00:38

This branch was previously deployed

1 inactive deployment
cadence — 888e31c5 Deployed Nov 12, 2026 by pytorchbot via hifi-op-test / hifi4 #37944
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.

2 participants