Skip to content

Commit f447b40

Browse files
committed
[ExecuTorch][WebGPU] Dynamic resize hooks for rms_norm, embedding, rope
Pull Request resolved: #20575 These ops baked their dispatch count, param UBO, and output dims at `build()` for the max seq-len. On a dynamic-shape graph at a smaller live S they would over-dispatch and leave the output sized at the max, so the resize engine could not actually shrink them. This adds tensor resize hooks to rms_norm, embedding_q4gsw, and apply_rotary_emb. When an input is resized, each hook recomputes the live row/token count, rewrites the param UBO, updates the dispatch `workgroup_count_x`, and sets the output's `cur_dims`. The hook is inert until a resize happens, so static graphs are byte-identical. Implementation: - `rms_norm`: recompute `num_rows` from live `cur_dims`; out dims follow the input. - `embedding_q4gsw`: recompute `num_indices`/`total_blocks`; out dims = indices dims + `[embed_dim]`. - `apply_rotary_emb`: `add_rope_dispatch` now returns its uniform handle; one hook rewrites both the xq and xk dispatches/UBOs for the live S and sets both outputs. - Each keeps its uniform buffer alive via `own_uniform_buffer` (the hook rewrites it) instead of releasing it at build. Mirrors Vulkan per-op `resize_*_node` (recompute sizes + dispatch each execute). No kernel/WGSL/numerics change. Behavior-neutral on static graphs (hook only fires when live dims differ from max). `quantized_linear` and SDPA resize hooks land in following diffs; `prepack` needs none (constants are fixed-size). ghstack-source-id: 399812824 @exported-using-ghexport Differential Revision: [D109906096](https://our.internmc.facebook.com/intern/diff/D109906096/)
1 parent 4fa5bc1 commit f447b40

3 files changed

Lines changed: 241 additions & 30 deletions

File tree

backends/webgpu/runtime/ops/embedding_q4gsw/EmbeddingQ4gsw.cpp

Lines changed: 78 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <cstdint>
1717
#include <cstring>
1818
#include <stdexcept>
19+
#include <vector>
1920

2021
namespace executorch::backends::webgpu {
2122

@@ -36,12 +37,48 @@ static_assert(
3637
sizeof(EmbeddingParams) == 32,
3738
"EmbeddingParams must be 32 bytes");
3839

39-
uint64_t numel_of(const std::vector<int64_t>& dims) {
40-
uint64_t n = 1;
41-
for (int64_t d : dims) {
42-
n *= static_cast<uint64_t>(d);
40+
// Resize hook body: recompute counts/dispatch; out = indices dims +
41+
// [embed_dim].
42+
void resize_embedding_q4gsw(
43+
WebGPUGraph& g,
44+
int indices_id,
45+
int out_id,
46+
uint32_t embed_dim,
47+
uint32_t blocks_per_row,
48+
uint32_t gs_u,
49+
uint32_t groups_per_row,
50+
uint32_t bytes_per_row,
51+
uint32_t wg_size,
52+
size_t dispatch_idx,
53+
WGPUBuffer params_buf) {
54+
const auto& id = g.cur_dims(indices_id);
55+
const uint64_t ni = utils::numel_of(id);
56+
if (ni == 0) {
57+
throw std::runtime_error("WebGPU embedding_q4gsw: zero indices");
4358
}
44-
return n;
59+
const uint64_t total_blocks = ni * blocks_per_row;
60+
if (total_blocks > UINT32_MAX) {
61+
throw std::runtime_error(
62+
"WebGPU embedding_q4gsw: total_blocks exceeds uint32");
63+
}
64+
std::vector<int64_t> od = id;
65+
od.push_back(static_cast<int64_t>(embed_dim));
66+
g.set_cur_dims(out_id, od);
67+
EmbeddingParams p = {};
68+
p.embed_dim = embed_dim;
69+
p.blocks_per_row = blocks_per_row;
70+
p.num_indices = static_cast<uint32_t>(ni);
71+
p.group_size = gs_u;
72+
p.groups_per_row = groups_per_row;
73+
p.bytes_per_row = bytes_per_row;
74+
p.total_blocks = static_cast<uint32_t>(total_blocks);
75+
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
76+
g.dispatch_at(dispatch_idx).workgroup_count_x =
77+
utils::compute_1d_workgroup_count(
78+
g.device(),
79+
static_cast<uint32_t>(total_blocks),
80+
wg_size,
81+
"embedding_q4gsw(resize)");
4582
}
4683

4784
// arg order mirrors Vulkan EmbeddingQ4gsw.cpp.
@@ -99,7 +136,7 @@ void embedding_q4gsw_impl(WebGPUGraph& graph, const std::vector<int>& args) {
99136
}
100137

101138
// Leading index dims flatten row-major (mirrors Vulkan num_indices).
102-
const uint64_t out_numel = numel_of(out.dims);
139+
const uint64_t out_numel = utils::numel_of(out.dims);
103140
const uint32_t num_indices = static_cast<uint32_t>(out_numel / embed_dim);
104141
const uint32_t groups_per_row = static_cast<uint32_t>(scales.dims[1]);
105142
const uint32_t blocks_per_row = embed_dim / 32u;
@@ -116,9 +153,9 @@ void embedding_q4gsw_impl(WebGPUGraph& graph, const std::vector<int>& args) {
116153
}
117154

118155
// Per-type byte guards (no runtime dtype): indices i32, weight u8, fp32 rest.
119-
const uint64_t indices_numel = numel_of(indices.dims);
120-
const uint64_t weight_numel = numel_of(weight.dims);
121-
const uint64_t scales_numel = numel_of(scales.dims);
156+
const uint64_t indices_numel = utils::numel_of(indices.dims);
157+
const uint64_t weight_numel = utils::numel_of(weight.dims);
158+
const uint64_t scales_numel = utils::numel_of(scales.dims);
122159
if (indices_numel != num_indices ||
123160
indices.nbytes != indices_numel * sizeof(int32_t) ||
124161
weight.nbytes != weight_numel ||
@@ -228,13 +265,43 @@ void embedding_q4gsw_impl(WebGPUGraph& graph, const std::vector<int>& args) {
228265
bg_desc.entries = bg_entries;
229266
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
230267

231-
graph.add_dispatch(
268+
const size_t dispatch_idx = graph.add_dispatch(
232269
{pipeline, bind_group, workgroup_count, "embedding_q4gsw"});
233270

271+
// Dynamic shapes: recompute counts/dispatch; out = indices + [embed_dim].
272+
const uint32_t gs_u = static_cast<uint32_t>(group_size);
273+
WGPUBuffer params_buf = uniform_buffer;
274+
graph.add_tensor_resize_hook(
275+
indices_id,
276+
[indices_id,
277+
out_id,
278+
embed_dim,
279+
blocks_per_row,
280+
gs_u,
281+
groups_per_row,
282+
bytes_per_row,
283+
wg_size,
284+
dispatch_idx,
285+
params_buf](WebGPUGraph& g) {
286+
resize_embedding_q4gsw(
287+
g,
288+
indices_id,
289+
out_id,
290+
embed_dim,
291+
blocks_per_row,
292+
gs_u,
293+
groups_per_row,
294+
bytes_per_row,
295+
wg_size,
296+
dispatch_idx,
297+
params_buf);
298+
});
299+
234300
wgpuShaderModuleRelease(shader);
235301
wgpuBindGroupLayoutRelease(bgl);
236302
wgpuPipelineLayoutRelease(pipeline_layout);
237-
wgpuBufferRelease(uniform_buffer);
303+
// Graph owns it so the resize hook can rewrite it; freed in the dtor.
304+
graph.own_uniform_buffer(uniform_buffer);
238305
}
239306

240307
} // namespace

backends/webgpu/runtime/ops/rms_norm/RmsNorm.cpp

Lines changed: 48 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
*/
88

99
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
10+
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
1011
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
1112
#include <executorch/backends/webgpu/runtime/ops/rms_norm/rms_norm_vec4_wgsl.h>
1213
#include <executorch/backends/webgpu/runtime/ops/rms_norm/rms_norm_wgsl.h>
@@ -31,6 +32,39 @@ struct RmsNormParams {
3132
};
3233
static_assert(sizeof(RmsNormParams) == 16, "RmsNormParams must be 16 bytes");
3334

35+
// Resize hook body: recompute num_rows + rewrite the UBO for the live input.
36+
void resize_rms_norm(
37+
WebGPUGraph& g,
38+
int in_id,
39+
int out_id,
40+
uint32_t row_width,
41+
float epsilon,
42+
size_t dispatch_idx,
43+
WGPUBuffer params_buf) {
44+
const auto& d = g.cur_dims(in_id);
45+
const uint64_t numel = utils::numel_of(d);
46+
if (numel % static_cast<uint64_t>(row_width) != 0) {
47+
throw std::runtime_error(
48+
"WebGPU rms_norm: numel not a multiple of row_width");
49+
}
50+
const uint32_t rows =
51+
static_cast<uint32_t>(numel / static_cast<uint64_t>(row_width));
52+
if (rows == 0) {
53+
throw std::runtime_error("WebGPU rms_norm: zero rows");
54+
}
55+
if (rows > 65535u) {
56+
throw std::runtime_error(
57+
"WebGPU rms_norm: num_rows exceeds the 1D dispatch limit (65535)");
58+
}
59+
RmsNormParams p = {};
60+
p.num_rows = rows;
61+
p.row_width = row_width;
62+
p.epsilon = epsilon;
63+
wgpuQueueWriteBuffer(g.queue(), params_buf, 0, &p, sizeof(p));
64+
g.dispatch_at(dispatch_idx).workgroup_count_x = rows;
65+
g.set_cur_dims(out_id, d);
66+
}
67+
3468
void rms_norm_impl(WebGPUGraph& graph, const std::vector<int>& args) {
3569
// et_vk.rms_norm.default args: [in, weight, eps, out]
3670
const int in_id = args.at(0);
@@ -187,14 +221,25 @@ void rms_norm_impl(WebGPUGraph& graph, const std::vector<int>& args) {
187221
static_assert(
188222
kRmsNormVec4WorkgroupSizeX == 64,
189223
"must match @workgroup_size and WG_SIZE in rms_norm_vec4.wgsl");
190-
graph.add_dispatch({pipeline, bind_group, num_rows});
224+
const size_t dispatch_idx =
225+
graph.add_dispatch({pipeline, bind_group, num_rows});
226+
227+
// Dynamic shapes: recompute num_rows + rewrite the UBO for the live input.
228+
WGPUBuffer params_buf = uniform_buffer;
229+
graph.add_tensor_resize_hook(
230+
in_id,
231+
[in_id, out_id, row_width, epsilon, dispatch_idx, params_buf](
232+
WebGPUGraph& g) {
233+
resize_rms_norm(
234+
g, in_id, out_id, row_width, epsilon, dispatch_idx, params_buf);
235+
});
191236

192237
// Release intermediate objects (pipeline and bind_group are kept by dispatch)
193238
wgpuShaderModuleRelease(shader);
194239
wgpuBindGroupLayoutRelease(bgl);
195240
wgpuPipelineLayoutRelease(pipeline_layout);
196-
// Drop our ref; the bind group keeps the uniform buffer alive until release.
197-
wgpuBufferRelease(uniform_buffer);
241+
// Graph owns it so the resize hook can rewrite it; freed in the dtor.
242+
graph.own_uniform_buffer(uniform_buffer);
198243
}
199244

200245
} // namespace

backends/webgpu/runtime/ops/rope/RotaryEmbedding.cpp

Lines changed: 115 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,15 @@ struct RotaryParams {
3434
};
3535
static_assert(sizeof(RotaryParams) == 32, "RotaryParams must be 32 bytes");
3636

37-
uint64_t numel_of(const std::vector<int64_t>& dims) {
38-
uint64_t n = 1;
39-
for (int64_t d : dims) {
40-
n *= static_cast<uint64_t>(d);
41-
}
42-
return n;
43-
}
37+
// A rope dispatch: its param-uniform (rewritten on resize) and its index in the
38+
// graph's dispatch list (so a resize hook can update the workgroup count).
39+
struct RopeDispatch {
40+
WGPUBuffer uniform;
41+
size_t dispatch_index;
42+
};
4443

4544
// Rotate one (x->out) with the shared shader; freqs shared between xq and xk.
46-
void add_rope_dispatch(
45+
RopeDispatch add_rope_dispatch(
4746
WebGPUGraph& graph,
4847
WGPUDevice device,
4948
WGPUComputePipeline pipeline,
@@ -58,7 +57,8 @@ void add_rope_dispatch(
5857
uint32_t workgroup_count) {
5958
const uint32_t half_dim = head_dim / 2u;
6059
// out.dims == in.dims (asserted in impl), so this matches the caller's wgc.
61-
const uint32_t num_pairs = static_cast<uint32_t>(numel_of(out.dims) / 2u);
60+
const uint32_t num_pairs =
61+
static_cast<uint32_t>(utils::numel_of(out.dims) / 2u);
6262

6363
RotaryParams params = {};
6464
params.n_heads = n_heads;
@@ -101,10 +101,67 @@ void add_rope_dispatch(
101101
bg_desc.entries = bg_entries;
102102
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
103103

104-
graph.add_dispatch(
104+
const size_t dispatch_index = graph.add_dispatch(
105105
{pipeline, bind_group, workgroup_count, "apply_rotary_emb"});
106106

107-
wgpuBufferRelease(uniform_buffer);
107+
// Graph owns it so a resize hook can rewrite it; freed in the dtor.
108+
graph.own_uniform_buffer(uniform_buffer);
109+
return {uniform_buffer, dispatch_index};
110+
}
111+
112+
// Resize hook body: recompute S/num_pairs + both dispatches; out follows xq/xk.
113+
void resize_rope(
114+
WebGPUGraph& g,
115+
int xq_id,
116+
int xk_id,
117+
int xq_out_id,
118+
int xk_out_id,
119+
uint32_t n_heads_q,
120+
uint32_t n_heads_k,
121+
uint32_t head_dim,
122+
uint32_t half_dim,
123+
uint32_t wg_size,
124+
size_t q_idx,
125+
size_t k_idx,
126+
WGPUBuffer q_ubuf,
127+
WGPUBuffer k_ubuf) {
128+
const auto& qd = g.cur_dims(xq_id);
129+
const auto& kd = g.cur_dims(xk_id);
130+
if (qd.size() < 3 || kd.size() < 3) {
131+
throw std::runtime_error("apply_rotary_emb(resize): q/k rank must be >= 3");
132+
}
133+
const uint32_t s = static_cast<uint32_t>(qd[qd.size() - 3]);
134+
const uint64_t qn = utils::numel_of(qd);
135+
const uint64_t kn = utils::numel_of(kd);
136+
// pk = pq (seq=s); require k's seq == s, not silently q's.
137+
if (static_cast<uint32_t>(kd[kd.size() - 3]) != s) {
138+
throw std::runtime_error(
139+
"apply_rotary_emb(resize): q and k seq lengths differ");
140+
}
141+
// freqs stay max-allocated; shader indexes by position (S = prefix).
142+
RotaryParams pq = {};
143+
pq.n_heads = n_heads_q;
144+
pq.seq = s;
145+
pq.head_dim = head_dim;
146+
pq.half_dim = half_dim;
147+
pq.num_pairs = static_cast<uint32_t>(qn / 2u);
148+
RotaryParams pk = pq;
149+
pk.n_heads = n_heads_k;
150+
pk.num_pairs = static_cast<uint32_t>(kn / 2u);
151+
wgpuQueueWriteBuffer(g.queue(), q_ubuf, 0, &pq, sizeof(pq));
152+
wgpuQueueWriteBuffer(g.queue(), k_ubuf, 0, &pk, sizeof(pk));
153+
g.dispatch_at(q_idx).workgroup_count_x = utils::compute_1d_workgroup_count(
154+
g.device(),
155+
static_cast<uint32_t>(qn / 2u),
156+
wg_size,
157+
"apply_rotary_emb(resize)");
158+
g.dispatch_at(k_idx).workgroup_count_x = utils::compute_1d_workgroup_count(
159+
g.device(),
160+
static_cast<uint32_t>(kn / 2u),
161+
wg_size,
162+
"apply_rotary_emb(resize)");
163+
g.set_cur_dims(xq_out_id, qd);
164+
g.set_cur_dims(xk_out_id, kd);
108165
}
109166

110167
// args: [xq, xk, freqs_cos, freqs_sin, out_list(ValueList[xq_out, xk_out])].
@@ -164,9 +221,9 @@ void apply_rotary_emb_impl(WebGPUGraph& graph, const std::vector<int>& args) {
164221
}
165222

166223
// All tensors are fp32; output shapes equal their inputs.
167-
const uint64_t xq_numel = numel_of(xq.dims);
168-
const uint64_t xk_numel = numel_of(xk.dims);
169-
const uint64_t freqs_numel = numel_of(freqs_cos.dims);
224+
const uint64_t xq_numel = utils::numel_of(xq.dims);
225+
const uint64_t xk_numel = utils::numel_of(xk.dims);
226+
const uint64_t freqs_numel = utils::numel_of(freqs_cos.dims);
170227
if (freqs_numel != static_cast<uint64_t>(seq) * half_dim ||
171228
xq.nbytes != xq_numel * sizeof(float) ||
172229
xk.nbytes != xk_numel * sizeof(float) ||
@@ -246,7 +303,7 @@ void apply_rotary_emb_impl(WebGPUGraph& graph, const std::vector<int>& args) {
246303
WGPUComputePipeline pipeline_k =
247304
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
248305

249-
add_rope_dispatch(
306+
RopeDispatch q_disp = add_rope_dispatch(
250307
graph,
251308
device,
252309
pipeline_q,
@@ -259,7 +316,7 @@ void apply_rotary_emb_impl(WebGPUGraph& graph, const std::vector<int>& args) {
259316
seq,
260317
head_dim,
261318
xq_wgc);
262-
add_rope_dispatch(
319+
RopeDispatch k_disp = add_rope_dispatch(
263320
graph,
264321
device,
265322
pipeline_k,
@@ -272,6 +329,48 @@ void apply_rotary_emb_impl(WebGPUGraph& graph, const std::vector<int>& args) {
272329
seq,
273330
head_dim,
274331
xk_wgc);
332+
WGPUBuffer q_ubuf = q_disp.uniform;
333+
WGPUBuffer k_ubuf = k_disp.uniform;
334+
const size_t q_idx = q_disp.dispatch_index;
335+
const size_t k_idx = k_disp.dispatch_index;
336+
337+
// Dynamic shapes: recompute S/num_pairs + both dispatches; out follows xq/xk.
338+
const int xq_out_id = out_list[0];
339+
const int xk_out_id = out_list[1];
340+
// Register on both xq and xk so the recompute fires whichever is marked dirty
341+
// (q and k co-resize on S; resize_rope is idempotent, so a double-fire when
342+
// both are dirty is harmless).
343+
auto rope_hook = [xq_id,
344+
xk_id,
345+
xq_out_id,
346+
xk_out_id,
347+
n_heads_q,
348+
n_heads_k,
349+
head_dim,
350+
half_dim,
351+
wg_size,
352+
q_idx,
353+
k_idx,
354+
q_ubuf,
355+
k_ubuf](WebGPUGraph& g) {
356+
resize_rope(
357+
g,
358+
xq_id,
359+
xk_id,
360+
xq_out_id,
361+
xk_out_id,
362+
n_heads_q,
363+
n_heads_k,
364+
head_dim,
365+
half_dim,
366+
wg_size,
367+
q_idx,
368+
k_idx,
369+
q_ubuf,
370+
k_ubuf);
371+
};
372+
graph.add_tensor_resize_hook(xq_id, rope_hook);
373+
graph.add_tensor_resize_hook(xk_id, rope_hook);
275374

276375
wgpuShaderModuleRelease(shader);
277376
wgpuBindGroupLayoutRelease(bgl);

0 commit comments

Comments
 (0)