Skip to content

[ET-VK] Do not return early before the barrier in the reduce shader - #22326

Open
msluszniak wants to merge 2 commits into
pytorch:mainfrom
msluszniak:ms/vulkan-reduce-barrier
Open

[ET-VK] Do not return early before the barrier in the reduce shader#22326
msluszniak wants to merge 2 commits into
pytorch:mainfrom
msluszniak:ms/vulkan-reduce-barrier

Conversation

@msluszniak

@msluszniak msluszniak commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Fixes #22325.

main() in reduce.glsl bounds-checks the global position and returns early, but both reduce_nonpacked_dim() and reduce_packed_dim() call barrier(). Vulkan requires barrier() to be reached by every invocation in the work group under uniform control flow, so an invocation that returns leaves the remaining ones waiting on a barrier that can never complete. On a Mali-G76 that hangs the GPU and the submit fails with VK_ERROR_DEVICE_LOST.

The work group is always sized with ngroups = 4 along group_dim, but group_dim is the larger of the two non-reduce dims of the output and is often smaller than 4. Reducing a 2D tensor along dim 1 gives an output whose two candidate group dims both have extent 1, so 12 of 16 invocations return early.

This carries the bounds check as a flag instead. Out of bounds invocations skip the loads and the accumulation, still write their unused shared memory slot, reach the barrier, and skip the output write. Their shared memory contents are never read by an in bounds group: within a work group the bounds check varies only along group_dim, which is exactly tid.y, and a group aggregates only its own slots.

Verification

Samsung Galaxy S10+ (Mali-G76), torch.sum(x, dim=1, keepdim=True) over (b, 384):

b before after
1 device lost passes, max abs diff 0
2 device lost passes, 1.9e-06
3 passes passes, 3.8e-06
4 passes passes, 2.9e-06
5 device lost passes, 3.8e-06
8 passes passes, 2.9e-06

sentence-transformers/all-MiniLM-L6-v2 ends in F.normalize(x, p=2, dim=1) over a (1, 384) tensor and previously lost the device on its first execution. It now runs with cosine 0.99999720 against the CPU reference, bit-identical across 10 executions.

No regression on the other models I have on device: the Whisper-tiny encoder is unchanged at cosine 0.99999702, and selfie segmentation stays bit-exact at 1.19e-07.


Update: extended to every shader with the same defect

I audited all 26 shaders that call barrier() and checked, for each return in main(), whether the guard is uniform across the work group. Three more had the same defect and are now fixed the same way:

shader dispatch why it diverges
reduce2d.glsl reduce_gwg, the same function reduce.glsl uses 4 groups along a group_dim whose extent can be 1
var_texture3d.glsl var_texture_gwg same construction, lwg_extents[group_dim] = 4u
softmax.glsl pick_softmax_gwg, texture path same construction

The remaining barrier-using shaders are correct and are left alone. Their guards are uniform across the work group, either because they test gl_WorkGroupID (coopmat_mm), because they test a global id component whose local size is 1 (reduce_per_row_buffer, native_layer_norm_buffer, rms_norm_buffer, fused_ce, linear_q4gsw_coop), because the tested dimension is zeroed first (softmax_buffer), or because the varying dimension is not in the guard (the three sdpa shaders). q4gsw_linear_gemv_coop__w_4x8 returns only when the whole work group is out of bounds.

One shader is deliberately not touched: quantize_and_pack_4h4w_with_group_sums.glsl guards on gl_GlobalInvocationID.x while dispatching (4, 1, 16) or (2, 1, 32) local, so its guard looks divergent ahead of two barriers. It is an int4 quantized path I have no model to exercise, and I would rather not ship an untested change to it. Flagged in the issue.

Verification of the three added here

On a Galaxy S26 Ultra (Adreno 840):

main() bounds-checks the global position and returns early:

    if (any(greaterThanEqual(scan_pos, tin_limits))) {
      return;
    }

Both reduce_nonpacked_dim() and reduce_packed_dim() then call barrier().
Vulkan requires barrier() to be reached by every invocation in the work
group under uniform control flow, so an invocation that returns leaves
the remaining ones waiting on a barrier that can never complete. On a
Mali-G76 this hangs the GPU and the submit fails with
VK_ERROR_DEVICE_LOST.

The work group is always sized with ngroups = 4 along group_dim, but
group_dim is picked as the larger of the two non-reduce dims of the
output, which can be smaller than 4. Reducing a 2D tensor along dim 1
produces an output whose two candidate group dims both have extent 1,
so 12 of the 16 invocations return early.

Carry the bounds check as a flag instead of returning, so the barrier
stays in uniform control flow. Out of bounds invocations skip the loads
and the accumulation, still write their (unused) shared memory slot,
reach the barrier, and skip the output write. Their shared memory
contents are never read by an in bounds group, because within a work
group the bounds check varies only along group_dim, which is exactly
tid.y, and a group aggregates only its own slots.

Verified on a Mali-G76: torch.sum(x, dim=1, keepdim=True) over (b, 384)
lost the device for b = 1, 2 and 5 and now passes for every b, matching
the CPU reference. all-MiniLM-L6-v2, whose final F.normalize reduces a
(1, 384) tensor, previously lost the device on load and now runs with
cosine 0.99999720 against the CPU reference, bit-identical across 10
executions.
@msluszniak
msluszniak requested a review from SS-JIA as a code owner August 30, 2026 09:30
@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/22326

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

⚠️ 16 Awaiting Approval

As of commit dbabf91 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
main() bounds-checks the global position and returns early, but both
reduce_nonpacked_dim() and reduce_packed_dim() call barrier(). Vulkan
requires barrier() to be reached by every invocation in the work group
under uniform control flow, so an invocation that returns leaves the
rest waiting on a barrier that can never complete. On a Mali-G76 this
hangs the GPU and the submit fails with VK_ERROR_DEVICE_LOST.

The work group is always sized with ngroups = 4 along group_dim, but
group_dim is the larger of the two non-reduce dims of the output and is
often smaller than 4. Reducing a 2D tensor along dim 1 gives an output
whose two candidate group dims both have extent 1, so 12 of the 16
invocations return early.

Carry the bounds check as a flag instead. all-MiniLM-L6-v2 ends in
F.normalize over a (1, 384) tensor and previously lost the device on its
first execution.

Backport of upstream pytorch/executorch#22326.
Auditing every shader that calls barrier() turned up three more with the
same defect as reduce.glsl: main() bounds-checks the global position and
returns early, while the routine it calls contains a barrier(). All
three are dispatched through the same style of work group sizing, which
sets 4 groups along a group_dim whose extent can be 1, so invocations
diverge and the survivors wait on a barrier that never completes.

  softmax.glsl        pick_softmax_gwg, texture path
  var_texture3d.glsl  var_texture_gwg
  reduce2d.glsl       reduce_gwg, the same function reduce.glsl uses

Each now carries the bounds check as a flag instead of returning, as
reduce.glsl already does.

The remaining barrier-using shaders were checked and are correct: their
guards are uniform across the work group, either because they test
gl_WorkGroupID (coopmat_mm) or because they test a global id component
whose local size is 1 (reduce_per_row_buffer, softmax_buffer,
rms_norm_buffer, native_layer_norm_buffer, fused_ce, the three sdpa
shaders, linear_q4gsw_coop). q4gsw_linear_gemv_coop__w_4x8 returns only
when the whole work group is out of bounds and says so in a comment.
msluszniak added a commit to software-mansion-labs/executorch that referenced this pull request Aug 30, 2026
reduce2d, var_texture3d and softmax build their work group the same way
reduce.glsl does, with 4 groups along a group_dim whose extent can be 1, so
they return early from a subset of invocations before a barrier(). Carry the
bounds check as a flag instead, as already done for reduce.glsl.

Upstream: pytorch/executorch#22325, pytorch/executorch#22326
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.

[Vulkan] reduce shader returns early before barrier(), hanging the GPU (VK_ERROR_DEVICE_LOST on Mali-G76)

2 participants