Skip to content

Commit cab2d4f

Browse files
committed
[ExecuTorch][WebGPU] 2D compute dispatch — lift the 65535 per-dim cap (prefill path)
Pull Request resolved: #20583 **Lift the 65535 workgroup-per-dim dispatch cap so single-shot SDPA prefill runs at any sequence length.** **Problem**: The WebGPU backend is 1D-dispatch-only and throws when a kernel's workgroup count exceeds the device per-dim limit (`maxComputeWorkgroupsPerDimension`, spec floor 65535). SDPA prefill QK exceeds it around S~362 (softmax/AV at S=2048), blocking single-shot / long-context prefill. **Solution**: Fold a >limit 1D workgroup count into 2D; the shader reconstructs the linear index from `@builtin(num_workgroups)`. - **Before**: `compute_1d_workgroup_count` throws if `count > limit`; dispatch `(count, 1, 1)`. - **After**: `compute_2d_workgroup_count` returns `{count, 1}` (fast path) or a near-square `{x, y}` (`x = ceil(sqrt(count))` clamped to `limit`, `y = div_up(count, x)`); dispatch `(x, y, 1)`. A flat `{limit, div_up(count, limit)}` split would idle up to ~half the launched workgroups when `count` just exceeds `limit`; the near-square split holds the waste to `O(sqrt(count))` (e.g. 65536 -> `{256, 256}`, 0 inactive). **Implementation**: - `WgCount` + pure `fold_workgroup_count_2d` + `compute_2d_workgroup_count` in `WebGPUUtils.h` (device-free, unit-testable; `queried_max_workgroups` factored out of the 1D path) - `WebGPUDispatch.workgroup_count_y` (default 1, declared last so existing aggregate inits are unchanged); both `dispatchWorkgroups` calls + the profiling record pass `(x, y, 1)` - Per-kernel in-shader reconstruction: thread-form `idx = gid.x + gid.y*(num_workgroups.x*wg_size)` (QK/AV/add); row-form `row_idx = wid.x + wid.y*num_workgroups.x` (softmax — keeps a `valid` predicate, not an early return, so `workgroupBarrier()`s stay uniform) - `Sdpa.cpp`: QK/softmax/AV counts via the 2D helper; the dynamic-`input_pos` resize hook recomputes both x and y for QK - Reference: ET-Vulkan dispatches over natural N-D extents (never folds a flat count nor guards the per-dim limit) and MLX `get_2d_grid_dims` packs whole tensor dims; for our flattened scalar count the near-square split is the correct no-shape-info analog (a pack-to-limit split would reproduce the idle-half waste) **Constraints**: - `y=1` fast path keeps every non-folded dispatch byte-identical to the prior 1D path - Scope = prefill path only; `rms_norm`/`embedding`/`lm_head`/`update_cache` are row/token-indexed and never hit the cap, so they keep the 1D path - Throws if a 3rd dispatch dimension would be needed — unreachable for real prefill (the `uint32` element guard fires first at S~11585) Co-authored-with: Claude Code. ghstack-source-id: 399812920 @exported-using-ghexport Differential Revision: [D109517684](https://our.internmc.facebook.com/intern/diff/D109517684/)
1 parent 789fd4c commit cab2d4f

13 files changed

Lines changed: 176 additions & 85 deletions

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -814,15 +814,15 @@ void WebGPUGraph::execute() {
814814
wgpuComputePassEncoderSetBindGroup(
815815
pass, 0, dispatch.bind_group, 0, nullptr);
816816
wgpuComputePassEncoderDispatchWorkgroups(
817-
pass, dispatch.workgroup_count_x, 1, 1);
817+
pass, dispatch.workgroup_count_x, dispatch.workgroup_count_y, 1);
818818
wgpuComputePassEncoderEnd(pass);
819819
wgpuComputePassEncoderRelease(pass);
820820
#ifdef WGPU_BACKEND_ENABLE_PROFILING
821821
if (qp) {
822822
qp->record(
823823
static_cast<uint32_t>(i),
824824
dispatch.kernel_name,
825-
{dispatch.workgroup_count_x, 1, 1},
825+
{dispatch.workgroup_count_x, dispatch.workgroup_count_y, 1},
826826
{1, 1, 1});
827827
}
828828
#endif // WGPU_BACKEND_ENABLE_PROFILING
@@ -894,7 +894,10 @@ void WebGPUGraph::execute() {
894894
wgpuComputePassEncoderSetBindGroup(
895895
pass, 0, dispatches_[i].bind_group, 0, nullptr);
896896
wgpuComputePassEncoderDispatchWorkgroups(
897-
pass, dispatches_[i].workgroup_count_x, 1, 1);
897+
pass,
898+
dispatches_[i].workgroup_count_x,
899+
dispatches_[i].workgroup_count_y,
900+
1);
898901
wgpuComputePassEncoderEnd(pass);
899902
wgpuComputePassEncoderRelease(pass);
900903
}

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ struct WebGPUDispatch {
4848
WGPUBindGroup bind_group = nullptr;
4949
uint32_t workgroup_count_x = 1;
5050
std::string kernel_name; // bench label
51+
uint32_t workgroup_count_y = 1; // 2D fold (>65535); 1 = unchanged 1D path
5152
// DMA copy command; default Compute keeps existing positional inits valid.
5253
enum class Kind { Compute, Copy };
5354
Kind kind = Kind::Compute;

backends/webgpu/runtime/WebGPUUtils.h

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <webgpu/webgpu.h>
1212

1313
#include <algorithm>
14+
#include <cmath>
1415
#include <cstdint>
1516
#include <cstring>
1617
#include <stdexcept>
@@ -47,27 +48,76 @@ inline uint32_t clamp_workgroup_size(WGPUDevice device, uint32_t desired) {
4748
return desired;
4849
}
4950

51+
struct WgCount {
52+
uint32_t x;
53+
uint32_t y;
54+
};
55+
56+
// Device's max workgroups per dispatch dimension; the WebGPU spec-default floor
57+
// (65535) if the query fails — never under-reports a real device's capacity.
58+
inline uint32_t queried_max_workgroups(WGPUDevice device) {
59+
WGPULimits limits = {};
60+
return wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
61+
limits.maxComputeWorkgroupsPerDimension > 0
62+
? limits.maxComputeWorkgroupsPerDimension
63+
: 65535u;
64+
}
65+
66+
// Pure 2D fold of a 1D workgroup count (device-free, unit-testable): {count,1}
67+
// when count <= max, else a near-square {x, y} with x ~ ceil(sqrt(count)) so
68+
// the launched grid stays close to count. A flat {max, div_up(count, max)}
69+
// split would leave up to ~half the workgroups inactive when count just exceeds
70+
// max, and inactive workgroups still cost launch/scheduling; the near-square
71+
// split keeps the waste to O(sqrt(count)). Throws if even a max*max grid is too
72+
// small (a 3rd dispatch dimension, out of scope). The shader reconstructs the
73+
// linear index from @builtin(num_workgroups), so any x/y factoring works.
74+
inline WgCount fold_workgroup_count_2d(
75+
uint32_t count,
76+
uint32_t max_count,
77+
const char* op_name) {
78+
if (count <= max_count) {
79+
return {count, 1u};
80+
}
81+
uint32_t x =
82+
static_cast<uint32_t>(std::ceil(std::sqrt(static_cast<double>(count))));
83+
x = std::min(x, max_count);
84+
// ceil-div written overflow-safe (count >= 1 here) as count nears UINT32_MAX.
85+
uint32_t y = 1u + (count - 1u) / x;
86+
if (y > max_count) {
87+
throw std::runtime_error(
88+
std::string("WebGPU ") + op_name +
89+
": workgroup count needs a 3rd dispatch dimension (unsupported)");
90+
}
91+
return {x, y};
92+
}
93+
5094
// 1D dispatch count (mirrors Vulkan div_up); throws if > device limit.
5195
inline uint32_t compute_1d_workgroup_count(
5296
WGPUDevice device,
5397
uint32_t num_threads,
5498
uint32_t workgroup_size,
5599
const char* op_name) {
56100
uint32_t count = div_up(num_threads, workgroup_size);
57-
WGPULimits limits = {};
58-
uint32_t max_count =
59-
wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
60-
limits.maxComputeWorkgroupsPerDimension > 0
61-
? limits.maxComputeWorkgroupsPerDimension
62-
: 65535u; // WebGPU spec-default floor
63-
if (count > max_count) {
101+
if (count > queried_max_workgroups(device)) {
64102
throw std::runtime_error(
65103
std::string("WebGPU ") + op_name +
66104
": workgroup count exceeds the 1D dispatch limit");
67105
}
68106
return count;
69107
}
70108

109+
// 2D dispatch count: fold the 1D count across x/y when it exceeds the per-dim
110+
// limit (lifts the cap, e.g. for SDPA prefill). Same fast path as compute_1d.
111+
inline WgCount compute_2d_workgroup_count(
112+
WGPUDevice device,
113+
uint32_t num_threads,
114+
uint32_t workgroup_size,
115+
const char* op_name) {
116+
uint32_t count = div_up(num_threads, workgroup_size);
117+
return fold_workgroup_count_2d(
118+
count, queried_max_workgroups(device), op_name);
119+
}
120+
71121
// Create a uniform buffer mapped-at-creation, copy `size` bytes in, and unmap.
72122
inline WGPUBuffer
73123
make_uniform(WGPUDevice device, const void* data, size_t size) {

backends/webgpu/runtime/ops/add/BinaryOp.cpp

Lines changed: 29 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,8 @@ void add_impl(WebGPUGraph& graph, const std::vector<int>& args) {
5353

5454
uint32_t wg_size =
5555
utils::clamp_workgroup_size(device, kBinaryAddWorkgroupSizeX);
56-
uint32_t workgroup_count =
57-
utils::compute_1d_workgroup_count(device, num_elements, wg_size, "add");
56+
utils::WgCount workgroup_count =
57+
utils::compute_2d_workgroup_count(device, num_elements, wg_size, "add");
5858

5959
WGPUConstantEntry wg_size_constant = {};
6060
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
@@ -158,40 +158,38 @@ void add_impl(WebGPUGraph& graph, const std::vector<int>& args) {
158158
bg_desc.entries = bg_entries;
159159
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
160160

161-
graph.add_dispatch({pipeline, bind_group, workgroup_count});
161+
graph.add_dispatch(
162+
{pipeline, bind_group, workgroup_count.x, "", workgroup_count.y});
162163
const size_t dispatch_idx = graph.num_dispatches() - 1;
163164

164165
// Dynamic shapes: recompute numel/dispatch; out follows the larger operand.
165166
WGPUBuffer params_buf = uniform_buffer;
166-
auto add_resize = [in1_id,
167-
in2_id,
168-
out_id,
169-
alpha,
170-
wg_size,
171-
dispatch_idx,
172-
params_buf](WebGPUGraph& g) {
173-
const auto& d1 = g.cur_dims(in1_id);
174-
const auto& d2 = g.cur_dims(in2_id);
175-
const uint64_t n1 = utils::numel_of(d1);
176-
const uint64_t n2 = utils::numel_of(d2);
177-
const uint64_t numel = n2 > n1 ? n2 : n1;
178-
const uint64_t n_min = n2 > n1 ? n1 : n2;
179-
// The flat add follows the larger operand and broadcasts the smaller; valid
180-
// only when the smaller tiles evenly into it (rejects e.g. [4,1] vs [1,3],
181-
// whose true [4,3] result this flat kernel cannot produce).
182-
if (n_min == 0u || numel % n_min != 0u) {
183-
throw std::runtime_error(
184-
"add(resize): operands are not broadcast-compatible by numel");
185-
}
186-
g.set_cur_dims(out_id, n2 > n1 ? d2 : d1);
187-
AddParams p = {};
188-
p.num_elements = static_cast<uint32_t>(numel);
189-
p.alpha = alpha;
190-
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
191-
g.dispatch_at(dispatch_idx).workgroup_count_x =
192-
utils::compute_1d_workgroup_count(
167+
auto add_resize =
168+
[in1_id, in2_id, out_id, alpha, wg_size, dispatch_idx, params_buf](
169+
WebGPUGraph& g) {
170+
const auto& d1 = g.cur_dims(in1_id);
171+
const auto& d2 = g.cur_dims(in2_id);
172+
const uint64_t n1 = utils::numel_of(d1);
173+
const uint64_t n2 = utils::numel_of(d2);
174+
const uint64_t numel = n2 > n1 ? n2 : n1;
175+
const uint64_t n_min = n2 > n1 ? n1 : n2;
176+
// The flat add follows the larger operand and broadcasts the smaller;
177+
// valid only when the smaller tiles evenly into it (rejects e.g. [4,1]
178+
// vs [1,3], whose true [4,3] result this flat kernel cannot produce).
179+
if (n_min == 0u || numel % n_min != 0u) {
180+
throw std::runtime_error(
181+
"add(resize): operands are not broadcast-compatible by numel");
182+
}
183+
g.set_cur_dims(out_id, n2 > n1 ? d2 : d1);
184+
AddParams p = {};
185+
p.num_elements = static_cast<uint32_t>(numel);
186+
p.alpha = alpha;
187+
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
188+
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
193189
g.device(), static_cast<uint32_t>(numel), wg_size, "add(resize)");
194-
};
190+
g.dispatch_at(dispatch_idx).workgroup_count_x = wgc.x;
191+
g.dispatch_at(dispatch_idx).workgroup_count_y = wgc.y;
192+
};
195193
graph.add_tensor_resize_hook(in1_id, add_resize);
196194
graph.add_tensor_resize_hook(in2_id, add_resize);
197195

backends/webgpu/runtime/ops/add/binary_add.wgsl

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ struct Params {
1111
override wg_size: u32 = 256;
1212

1313
@compute @workgroup_size(wg_size)
14-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
15-
let idx = gid.x;
14+
fn main(
15+
@builtin(global_invocation_id) gid: vec3<u32>,
16+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
17+
let idx = gid.x + gid.y * (num_workgroups.x * wg_size);
1618
if (idx >= params.num_elements) {
1719
return;
1820
}

backends/webgpu/runtime/ops/add/binary_add_wgsl.h

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
namespace executorch::backends::webgpu {
1414

1515
// @generated from binary_add.wgsl - DO NOT EDIT.
16-
// wgsl-sha256: c1ceec80c8d4d3d56986ad91ce0d7f9a57cd8467b8c3aa07a28da70e51d141d9
16+
// wgsl-sha256: e66bd67465c2a0296e09668df54f87605a4c91015a615f3734cdd0f140a74477
1717
inline constexpr const char* kBinaryAddWGSL = R"(
1818
@group(0) @binding(0) var<storage, read> input1: array<f32>;
1919
@group(0) @binding(1) var<storage, read> input2: array<f32>;
@@ -28,8 +28,10 @@ struct Params {
2828
override wg_size: u32 = 256;
2929
3030
@compute @workgroup_size(wg_size)
31-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
32-
let idx = gid.x;
31+
fn main(
32+
@builtin(global_invocation_id) gid: vec3<u32>,
33+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
34+
let idx = gid.x + gid.y * (num_workgroups.x * wg_size);
3335
if (idx >= params.num_elements) {
3436
return;
3537
}

backends/webgpu/runtime/ops/sdpa/Sdpa.cpp

Lines changed: 29 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,7 @@ void build_dispatch(
143143
WGPUBuffer uniform_buffer,
144144
uint64_t uniform_size,
145145
uint32_t workgroup_count_x,
146+
uint32_t workgroup_count_y,
146147
uint32_t wg_size,
147148
bool retain_uniform = false,
148149
const char* kernel_name = "") {
@@ -216,7 +217,12 @@ void build_dispatch(
216217
bg_desc.entries = bg_entries;
217218
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
218219

219-
graph.add_dispatch({pipeline, bind_group, workgroup_count_x, kernel_name});
220+
graph.add_dispatch(
221+
{pipeline,
222+
bind_group,
223+
workgroup_count_x,
224+
kernel_name,
225+
workgroup_count_y});
220226

221227
wgpuShaderModuleRelease(shader);
222228
wgpuBindGroupLayoutRelease(bgl);
@@ -257,6 +263,7 @@ static WGPUBuffer record_update_cache_dispatch(
257263
ubuf,
258264
sizeof(uc),
259265
wgc,
266+
1,
260267
uc_wg,
261268
retain_uniform,
262269
"update_cache");
@@ -478,7 +485,7 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
478485
}
479486
const int64_t qk_tiles = Hq * utils::div_up(S, kSdpaTileM) *
480487
utils::div_up(context_len, kSdpaTileN);
481-
const uint32_t wgc = utils::compute_1d_workgroup_count(
488+
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
482489
device, static_cast<uint32_t>(qk_tiles), qk_wg, "QK");
483490
AttnWeightsParams p = make_attn_weights_params(
484491
S, Hq, Hkv, D, context_len, input_pos, g, scale);
@@ -494,7 +501,8 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
494501
3,
495502
ubuf,
496503
sizeof(p),
497-
wgc,
504+
wgc.x,
505+
wgc.y,
498506
qk_wg,
499507
true,
500508
"sdpa_compute_attn_weights");
@@ -505,7 +513,7 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
505513
// Dispatch 4: softmax, one workgroup per (h,s) row of width context_len.
506514
{
507515
// One workgroup per (h,s) row; wg_size 1 keeps the device dispatch check.
508-
const uint32_t wgc = utils::compute_1d_workgroup_count(
516+
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
509517
device, static_cast<uint32_t>(Hq * S), 1, "softmax");
510518
SoftmaxParams p = make_softmax_params(Hq, S, context_len);
511519
WGPUBuffer ubuf = graph.make_uniform_buffer(&p, sizeof(p));
@@ -518,7 +526,8 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
518526
2,
519527
ubuf,
520528
sizeof(p),
521-
wgc,
529+
wgc.x,
530+
wgc.y,
522531
0,
523532
true,
524533
"sdpa_softmax");
@@ -530,7 +539,7 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
530539
{
531540
const int64_t av_tiles =
532541
Hq * utils::div_up(S, kSdpaTileM) * utils::div_up(D, kSdpaTileN);
533-
const uint32_t wgc = utils::compute_1d_workgroup_count(
542+
const utils::WgCount wgc = utils::compute_2d_workgroup_count(
534543
device, static_cast<uint32_t>(av_tiles), av_wg, "AV");
535544
ComputeOutParams p = make_compute_out_params(S, Hq, Hkv, D, context_len, g);
536545
WGPUBuffer ubuf = graph.make_uniform_buffer(&p, sizeof(p));
@@ -545,7 +554,8 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
545554
3,
546555
ubuf,
547556
sizeof(p),
548-
wgc,
557+
wgc.x,
558+
wgc.y,
549559
av_wg,
550560
true,
551561
"sdpa_compute_out");
@@ -632,25 +642,28 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
632642
wgpuQueueWriteBuffer(gr.queue(), qk_buf, 0, &qp, sizeof(qp));
633643
const int64_t qk_tiles =
634644
Hq * utils::div_up(s, kSdpaTileM) * utils::div_up(ctx, kSdpaTileN);
635-
gr.dispatch_at(qk_idx).workgroup_count_x =
636-
utils::compute_1d_workgroup_count(
637-
gr.device(), static_cast<uint32_t>(qk_tiles), qk_wg, "QK(resize)");
645+
const utils::WgCount qk_wgc = utils::compute_2d_workgroup_count(
646+
gr.device(), static_cast<uint32_t>(qk_tiles), qk_wg, "QK(resize)");
647+
gr.dispatch_at(qk_idx).workgroup_count_x = qk_wgc.x;
648+
gr.dispatch_at(qk_idx).workgroup_count_y = qk_wgc.y;
638649

639650
// softmax: one workgroup per (h,s) row.
640651
SoftmaxParams sp = make_softmax_params(Hq, s, ctx);
641652
wgpuQueueWriteBuffer(gr.queue(), softmax_buf, 0, &sp, sizeof(sp));
642-
gr.dispatch_at(softmax_idx).workgroup_count_x =
643-
utils::compute_1d_workgroup_count(
644-
gr.device(), static_cast<uint32_t>(Hq * s), 1, "softmax(resize)");
653+
const utils::WgCount sm_wgc = utils::compute_2d_workgroup_count(
654+
gr.device(), static_cast<uint32_t>(Hq * s), 1, "softmax(resize)");
655+
gr.dispatch_at(softmax_idx).workgroup_count_x = sm_wgc.x;
656+
gr.dispatch_at(softmax_idx).workgroup_count_y = sm_wgc.y;
645657

646658
// AV: one thread per TM x TN tile; grid = Hq*ceil(S/TM)*ceil(D/TN).
647659
ComputeOutParams op = make_compute_out_params(s, Hq, Hkv, D, ctx, g);
648660
wgpuQueueWriteBuffer(gr.queue(), av_buf, 0, &op, sizeof(op));
649661
const int64_t av_tiles =
650662
Hq * utils::div_up(s, kSdpaTileM) * utils::div_up(D, kSdpaTileN);
651-
gr.dispatch_at(av_idx).workgroup_count_x =
652-
utils::compute_1d_workgroup_count(
653-
gr.device(), static_cast<uint32_t>(av_tiles), av_wg, "AV(resize)");
663+
const utils::WgCount av_wgc = utils::compute_2d_workgroup_count(
664+
gr.device(), static_cast<uint32_t>(av_tiles), av_wg, "AV(resize)");
665+
gr.dispatch_at(av_idx).workgroup_count_x = av_wgc.x;
666+
gr.dispatch_at(av_idx).workgroup_count_y = av_wgc.y;
654667

655668
// Output attn has the same shape as q: [.., S, Hq, D].
656669
gr.set_cur_dims(out_id, gr.cur_dims(q_id));

backends/webgpu/runtime/ops/sdpa/sdpa_compute_attn_weights.wgsl

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -53,17 +53,21 @@ fn store_qk(s: u32, c: u32, h: u32, raw: f32) {
5353
}
5454

5555
@compute @workgroup_size(wg_size, 1, 1)
56-
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
56+
fn main(
57+
@builtin(global_invocation_id) gid: vec3<u32>,
58+
@builtin(num_workgroups) num_workgroups: vec3<u32>) {
5759
let nrt = (params.S + TM - 1u) / TM;
5860
let nct = (params.context_len + TN - 1u) / TN;
5961
let tiles = nrt * nct;
6062
let total = tiles * params.Hq;
61-
if (gid.x >= total) {
63+
// 2D dispatch fold: recover the linear tile index across x/y.
64+
let idx = gid.x + gid.y * (num_workgroups.x * wg_size);
65+
if (idx >= total) {
6266
return;
6367
}
6468

65-
let h = gid.x / tiles;
66-
let rem = gid.x % tiles;
69+
let h = idx / tiles;
70+
let rem = idx % tiles;
6771
let row_tile = rem / nct;
6872
let col_tile = rem % nct;
6973
let kvh = h / params.g;

0 commit comments

Comments
 (0)