Skip to content

Commit 2512896

Browse files
authored
[ET-VK] Do not return early before the barrier in softmax and var (#22326)
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): - `all-MiniLM-L6-v2`, whose attention exercises `softmax`: unchanged at cosine 0.99999243 against the CPU reference, 1 distinct output over 20 executions. - Whisper-tiny encoder: unchanged. It stays nondeterministic on Adreno, but that is #22327 in the fp32 `linear` path, unrelated to this; its best replay still reaches cosine 0.999989. --- ## Update: rebased `reduce.glsl` and `reduce2d.glsl` landed with #22348, so this is now only `softmax.glsl` and `var_texture3d.glsl`. Rebased on main. cc @SS-JIA @manuelcandales @digantdesai @cbilgin
1 parent 4891009 commit 2512896

2 files changed

Lines changed: 75 additions & 58 deletions

File tree

‎backends/vulkan/runtime/graph/ops/glsl/softmax.glsl‎

Lines changed: 30 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@ int tid_to_smi(const ivec2 tid) {
5151
* This case is simpler because each element of a texel belongs to a separate
5252
* reduction dim, meaning we don't have to perform reduction along a texel.
5353
*/
54-
void softmax_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
54+
void softmax_nonpacked_dim(const ivec2 tid, ivec3 scan_pos, const bool in_bounds) {
5555
const int smi = tid_to_smi(tid);
5656
int group_i;
5757

58-
scan_pos[reduce_dim] = tid.x;
59-
vec4 max_elements = texelFetch(tin, scan_pos, 0);
60-
for (int i = tid.x; i < safe_idx(in_meta.sizes, reduce_dim);
61-
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
62-
max_elements = max(max_elements, texelFetch(tin, scan_pos, 0));
58+
vec4 max_elements = vec4(-1.0 / 0.0);
59+
if (in_bounds) {
60+
scan_pos[reduce_dim] = tid.x;
61+
max_elements = texelFetch(tin, scan_pos, 0);
62+
for (int i = tid.x; i < safe_idx(in_meta.sizes, reduce_dim);
63+
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
64+
max_elements = max(max_elements, texelFetch(tin, scan_pos, 0));
65+
}
6366
}
6467
shared_max[smi] = max_elements;
6568
barrier();
@@ -69,11 +72,13 @@ void softmax_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
6972
max_elements = max(max_elements, shared_max[group_i]);
7073
}
7174

72-
scan_pos[reduce_dim] = tid.x;
7375
vec4 denominators = vec4(0);
74-
for (int i = tid.x; i < safe_idx(in_meta.sizes, reduce_dim);
75-
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
76-
denominators += exp(texelFetch(tin, scan_pos, 0) - max_elements);
76+
if (in_bounds) {
77+
scan_pos[reduce_dim] = tid.x;
78+
for (int i = tid.x; i < safe_idx(in_meta.sizes, reduce_dim);
79+
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
80+
denominators += exp(texelFetch(tin, scan_pos, 0) - max_elements);
81+
}
7782
}
7883
shared_sum[smi] = denominators;
7984
barrier();
@@ -88,7 +93,8 @@ void softmax_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
8893
scan_pos[packed_dim] == (safe_idx(out_meta.limits, packed_dim) - 1);
8994

9095
scan_pos[reduce_dim] = tid.x;
91-
for (int i = tid.x; i < safe_idx(in_meta.sizes, reduce_dim);
96+
for (int i = in_bounds ? tid.x : safe_idx(in_meta.sizes, reduce_dim);
97+
i < safe_idx(in_meta.sizes, reduce_dim);
9298
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
9399
const vec4 numerators = op1(texelFetch(tin, scan_pos, 0) - max_elements);
94100
const vec4 safe_denom = max(denominators, vec4(1e-37));
@@ -120,7 +126,7 @@ void softmax_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
120126
* elements in texels (which occur when the size of the packed dim is not a
121127
* multiple of 4) so that they do not influence the output of reduction.
122128
*/
123-
void softmax_packed_dim(const ivec2 tid, ivec3 scan_pos) {
129+
void softmax_packed_dim(const ivec2 tid, ivec3 scan_pos, const bool in_bounds) {
124130
const int smi = tid_to_smi(tid);
125131
int group_i;
126132

@@ -129,11 +135,11 @@ void softmax_packed_dim(const ivec2 tid, ivec3 scan_pos) {
129135

130136
scan_pos[reduce_dim] = tid.x;
131137
vec4 max_elements = vec4(-3.402823e+38);
132-
for (int i = tid.x * 4; i < reduce_len;
138+
for (int i = in_bounds ? tid.x * 4 : reduce_len; i < reduce_len;
133139
i += NWORKERS * 4, scan_pos[reduce_dim] += NWORKERS) {
134140
max_elements = max(max_elements, texelFetch(tin, scan_pos, 0));
135141
}
136-
if (scan_pos[reduce_dim] == safe_idx(out_meta.limits, reduce_dim) - 1 && nspill > 0) {
142+
if (in_bounds && scan_pos[reduce_dim] == safe_idx(out_meta.limits, reduce_dim) - 1 && nspill > 0) {
137143
const vec4 intex = texelFetch(tin, scan_pos, 0);
138144
for (int i = 0; i < nspill; ++i) {
139145
max_elements.x = max(intex[i], max_elements.x);
@@ -153,11 +159,11 @@ void softmax_packed_dim(const ivec2 tid, ivec3 scan_pos) {
153159

154160
scan_pos[reduce_dim] = tid.x;
155161
vec4 denominators = vec4(0);
156-
for (int i = tid.x * 4; i < reduce_len;
162+
for (int i = in_bounds ? tid.x * 4 : reduce_len; i < reduce_len;
157163
i += NWORKERS * 4, scan_pos[reduce_dim] += NWORKERS) {
158164
denominators += exp(texelFetch(tin, scan_pos, 0) - max_element);
159165
}
160-
if (nspill > 0 && scan_pos[reduce_dim] == safe_idx(out_meta.limits, reduce_dim) - 1) {
166+
if (in_bounds && nspill > 0 && scan_pos[reduce_dim] == safe_idx(out_meta.limits, reduce_dim) - 1) {
161167
const vec4 intex = texelFetch(tin, scan_pos, 0);
162168
for (int i = 0; i < nspill; ++i) {
163169
denominators.x += exp(intex[i] - max_element);
@@ -177,12 +183,12 @@ void softmax_packed_dim(const ivec2 tid, ivec3 scan_pos) {
177183
const float safe_denominator = max(denominator, 1e-37);
178184

179185
scan_pos[reduce_dim] = tid.x;
180-
for (int i = tid.x * 4; i < reduce_len;
186+
for (int i = in_bounds ? tid.x * 4 : reduce_len; i < reduce_len;
181187
i += NWORKERS * 4, scan_pos[reduce_dim] += NWORKERS) {
182188
const vec4 numerators = op1(texelFetch(tin, scan_pos, 0) - max_element);
183189
imageStore(tout, scan_pos, op2(numerators, safe_denominator));
184190
}
185-
if (nspill > 0 && scan_pos[reduce_dim] == safe_idx(out_meta.limits, reduce_dim) - 1) {
191+
if (in_bounds && nspill > 0 && scan_pos[reduce_dim] == safe_idx(out_meta.limits, reduce_dim) - 1) {
186192
const vec4 numerator = op1(texelFetch(tin, scan_pos, 0) - max_element);
187193
vec4 outtex = op2(numerator, safe_denominator);
188194
[[unroll]] for (int i = nspill; i < 4; ++i) {
@@ -200,13 +206,14 @@ void main() {
200206
gl_LocalInvocationID[reduce_dim],
201207
gl_LocalInvocationID[group_dim]);
202208

203-
if (any(greaterThanEqual(scan_pos, out_meta.limits))) {
204-
return;
205-
}
209+
// Do not return early here. The routines below contain barrier() calls, and
210+
// returning would leave them in non-uniform control flow, which is undefined
211+
// and hangs the GPU on some drivers. Carry the bounds check instead.
212+
const bool in_bounds = !any(greaterThanEqual(scan_pos, out_meta.limits));
206213

207214
if (reduce_dim != packed_dim) {
208-
softmax_nonpacked_dim(tid, scan_pos);
215+
softmax_nonpacked_dim(tid, scan_pos, in_bounds);
209216
} else {
210-
softmax_packed_dim(tid, scan_pos);
217+
softmax_packed_dim(tid, scan_pos, in_bounds);
211218
}
212219
}

‎backends/vulkan/runtime/graph/ops/glsl/var_texture3d.glsl‎

Lines changed: 45 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -65,21 +65,23 @@ VEC4_T calculate_variance(VEC4_T sum, VEC4_T sum_sq, int count) {
6565
return variance;
6666
}
6767

68-
void reduce_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
68+
void reduce_nonpacked_dim(const ivec2 tid, ivec3 scan_pos, const bool in_bounds) {
6969
// shared memory index of this thread
7070
const int smi = tid_to_smi(tid);
7171

7272
VEC4_T sum = VEC4_T(0);
7373
VEC4_T sum_sq = VEC4_T(0);
7474
int count = 0;
7575

76-
scan_pos[reduce_dim] = tid.x;
77-
for (int i = tid.x; i < safe_idx(tin_sizes, reduce_dim);
78-
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
79-
VEC4_T val = load_texel(tin, scan_pos);
80-
sum += val;
81-
sum_sq += val * val;
82-
count += 1;
76+
if (in_bounds) {
77+
scan_pos[reduce_dim] = tid.x;
78+
for (int i = tid.x; i < safe_idx(tin_sizes, reduce_dim);
79+
i += NWORKERS, scan_pos[reduce_dim] += NWORKERS) {
80+
VEC4_T val = load_texel(tin, scan_pos);
81+
sum += val;
82+
sum_sq += val * val;
83+
count += 1;
84+
}
8385
}
8486
// Write partial output to shared memory and synchronize work group
8587
shared_sum[smi] = sum;
@@ -89,7 +91,7 @@ void reduce_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
8991

9092
// Since the reduction row is reduced to only one element, only the "main"
9193
// thread in the group needs aggregate the partial outputs
92-
if (tid.x == 0) {
94+
if (in_bounds && tid.x == 0) {
9395
int group_i = tid.y * NWORKERS;
9496
sum = shared_sum[group_i];
9597
sum_sq = shared_sum_sq[group_i];
@@ -132,7 +134,7 @@ void reduce_nonpacked_dim(const ivec2 tid, ivec3 scan_pos) {
132134
* elements in texels (which occur when the size of the packed dim is not a
133135
* multiple of 4) so that they do not influence the output of reduction.
134136
*/
135-
void reduce_packed_dim(const ivec2 tid, ivec3 scan_pos) {
137+
void reduce_packed_dim(const ivec2 tid, ivec3 scan_pos, const bool in_bounds) {
136138
// shared memory index of this thread
137139
const int smi = tid_to_smi(tid);
138140

@@ -142,29 +144,36 @@ void reduce_packed_dim(const ivec2 tid, ivec3 scan_pos) {
142144
// handled specially if it has padding elements.
143145
const int reduce_len = safe_idx(tin_sizes, packed_dim) - nspill;
144146

147+
// Out of bounds threads must not load or accumulate, but they must still
148+
// reach the barrier below, so the work is guarded rather than returned from.
149+
// They contribute a zero partial, which the aggregating thread of an in
150+
// bounds row never reads: `in_bounds` does not depend on tid.x, so a row is
151+
// either wholly in bounds or wholly out of it.
145152
VEC4_T sum = VEC4_T(0);
146153
VEC4_T sum_sq = VEC4_T(0);
147154
int count = 0;
148155

149-
// Partially accumulate over elements i, i + NWORKERS, i + 2*NWORKERS, ... of
150-
// the reduction row
151-
scan_pos[reduce_dim] = tid.x;
152-
for (int i = tid.x * 4; i < reduce_len;
153-
i += NWORKERS * 4, scan_pos[reduce_dim] += NWORKERS) {
154-
VEC4_T val = load_texel(tin, scan_pos);
155-
sum += val;
156-
sum_sq += val * val;
157-
count += 4;
158-
}
159-
// For the last texel in the dim, if there are padding elements then each
160-
// element of the texel needs to be processed individually such that the
161-
// padding elements are ignored
162-
if (scan_pos[reduce_dim] == safe_idx(tin_limits, reduce_dim) - 1 && nspill > 0) {
163-
const VEC4_T val = load_texel(tin, scan_pos);
164-
for (int i = 0; i < nspill; i++) {
165-
sum.x += val[i];
166-
sum_sq.x += val[i] * val[i];
167-
count += 1;
156+
if (in_bounds) {
157+
// Partially accumulate over elements i, i + NWORKERS, i + 2*NWORKERS, ... of
158+
// the reduction row
159+
scan_pos[reduce_dim] = tid.x;
160+
for (int i = tid.x * 4; i < reduce_len;
161+
i += NWORKERS * 4, scan_pos[reduce_dim] += NWORKERS) {
162+
VEC4_T val = load_texel(tin, scan_pos);
163+
sum += val;
164+
sum_sq += val * val;
165+
count += 4;
166+
}
167+
// For the last texel in the dim, if there are padding elements then each
168+
// element of the texel needs to be processed individually such that the
169+
// padding elements are ignored
170+
if (scan_pos[reduce_dim] == safe_idx(tin_limits, reduce_dim) - 1 && nspill > 0) {
171+
const VEC4_T val = load_texel(tin, scan_pos);
172+
for (int i = 0; i < nspill; i++) {
173+
sum.x += val[i];
174+
sum_sq.x += val[i] * val[i];
175+
count += 1;
176+
}
168177
}
169178
}
170179
// Write partial output to shared memory and synchronize work group
@@ -175,7 +184,7 @@ void reduce_packed_dim(const ivec2 tid, ivec3 scan_pos) {
175184

176185
// Since the reduction row is reduced to only one element, only the "main"
177186
// thread in the group needs aggregate the partial outputs
178-
if (tid.x == 0) {
187+
if (in_bounds && tid.x == 0) {
179188
sum = shared_sum[tid.y * NWORKERS];
180189
sum_sq = shared_sum_sq[tid.y * NWORKERS];
181190
count = shared_count[tid.y * NWORKERS];
@@ -211,13 +220,14 @@ void main() {
211220
gl_LocalInvocationID[reduce_dim],
212221
gl_LocalInvocationID[group_dim]);
213222

214-
if (any(greaterThanEqual(scan_pos, tin_limits))) {
215-
return;
216-
}
223+
// Do not return early here. The routines below contain barrier() calls, and
224+
// returning would leave them in non-uniform control flow, which is undefined
225+
// and hangs the GPU on some drivers. Carry the bounds check instead.
226+
const bool in_bounds = !any(greaterThanEqual(scan_pos, tin_limits));
217227

218228
if (reduce_dim != packed_dim) {
219-
reduce_nonpacked_dim(tid, scan_pos);
229+
reduce_nonpacked_dim(tid, scan_pos, in_bounds);
220230
} else {
221-
reduce_packed_dim(tid, scan_pos);
231+
reduce_packed_dim(tid, scan_pos, in_bounds);
222232
}
223233
}

0 commit comments

Comments
 (0)