Skip to content

Commit efb58ca

Browse files
committed
[ExecuTorch][WebGPU] Dynamic resize hook for linear_q4gsw
Pull Request resolved: #20576 **Make the 4-bit quantized linear serve any live M (rows) from one graph, so a dynamic prefill+decode graph computes correct-size outputs.** **Problem:** `linear_q4gsw` baked its dispatch count, `params.M`, and output shape at `build()` for the max M. On a dynamic-shape graph at a smaller live M (e.g. decode M=1 vs prefill M=S) it would over-dispatch and leave the output sized at the max. **Solution:** - Before: one fixed dispatch sized for the build-time M. - After: a tensor resize hook on the input recomputes the live M from `cur_dims`, rewrites `params.M`, updates the dispatch `workgroup_count_x` for the SAME kernel chosen at build (bicol GEMV / shmem GEMM / register-tiled), and sets the output `cur_dims` (= input dims with the last dim replaced by N). Inert until the input is resized. **Implementation:** - The build-time kernel select (bicol GEMV for M==1, else shmem GEMM for large K/N, else register-tiled) is fixed at build; the hook re-runs `compute_q4gsw_workgroup_count` for whichever of the three the build chose and rewrites the param UBO + output dims for the live M — it does not switch kernels (runtime M-switching is a separate optimization). - `own_uniform_buffer` keeps the param UBO alive so the hook can rewrite it. - Mirrors Vulkan `resize_q4gsw_linear_node` (recompute M-derived dispatch each execute). **Constraints:** Behavior-neutral on static graphs (hook fires only when the input's live M differs from the max). No kernel/WGSL/numerics change. Runtime M-based kernel switching is deliberately out of scope (a later opt diff). Co-authored-with: Claude Code. ghstack-source-id: 399812825 @exported-using-ghexport Differential Revision: [D109906094](https://our.internmc.facebook.com/intern/diff/D109906094/)
1 parent b45717f commit efb58ca

1 file changed

Lines changed: 128 additions & 40 deletions

File tree

backends/webgpu/runtime/ops/quantized_linear/QuantizedLinear.cpp

Lines changed: 128 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@
1818
#include <cstdint>
1919
#include <cstring>
2020
#include <stdexcept>
21+
#include <string>
22+
#include <vector>
2123

2224
namespace executorch::backends::webgpu {
2325

@@ -50,6 +52,62 @@ constexpr int64_t kQ4gswShmemTileN = 32;
5052
constexpr uint32_t kQ4gswShmemMinDim = 4096u;
5153
constexpr uint32_t kQ4gswShmemNMinDim = 2048u;
5254

55+
// Workgroup count for a linear_q4gsw dispatch (bicol GEMV / shmem GEMM / tiled
56+
// GEMM), with the range/limit guards shared by the build-time path and the
57+
// resize hook. use_gemv/use_shmem_gemm are the build-time routing decision (the
58+
// shader/pipeline is fixed at build); the resize hook re-runs this with live m.
59+
uint32_t compute_q4gsw_workgroup_count(
60+
WGPUDevice device,
61+
bool use_gemv,
62+
bool use_shmem_gemm,
63+
uint32_t m,
64+
uint32_t n,
65+
uint32_t wg_size,
66+
const char* op_name) {
67+
if (use_gemv) {
68+
// bicol: fixed 64 lanes, 2 output columns/workgroup, grid-strided over
69+
// ceil(N/2) column-pairs (M == 1 on this decode path).
70+
const uint64_t pairs = (static_cast<uint64_t>(n) + 1u) / 2u;
71+
if (pairs == 0u || pairs > UINT32_MAX) {
72+
throw std::runtime_error(
73+
std::string("WebGPU ") + op_name + ": N/2 out of range");
74+
}
75+
const uint32_t wgc =
76+
utils::clamp_workgroup_count(device, static_cast<uint32_t>(pairs));
77+
if (wgc == 0u) {
78+
throw std::runtime_error(
79+
std::string("WebGPU ") + op_name + ": zero GEMV dispatch");
80+
}
81+
return wgc;
82+
}
83+
if (use_shmem_gemm) {
84+
// shmem GEMM: one workgroup per tile, no grid-stride -> throw over limit.
85+
const int64_t total_wgs = utils::div_up<int64_t>(m, kQ4gswShmemTileM) *
86+
utils::div_up<int64_t>(n, kQ4gswShmemTileN);
87+
WGPULimits limits = {};
88+
const uint32_t max_wgs =
89+
wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
90+
limits.maxComputeWorkgroupsPerDimension > 0
91+
? limits.maxComputeWorkgroupsPerDimension
92+
: 65535u;
93+
if (total_wgs > static_cast<int64_t>(max_wgs)) {
94+
throw std::runtime_error(
95+
std::string("WebGPU ") + op_name +
96+
": shmem GEMM tile count exceeds the 1D dispatch limit");
97+
}
98+
return static_cast<uint32_t>(total_wgs);
99+
}
100+
const int64_t total_tiles = utils::div_up<int64_t>(m, kQ4gswTileM) *
101+
utils::div_up<int64_t>(n, kQ4gswTileN);
102+
if (total_tiles > static_cast<int64_t>(UINT32_MAX)) {
103+
throw std::runtime_error(
104+
std::string("WebGPU ") + op_name +
105+
": tile count exceeds the 1D dispatch limit");
106+
}
107+
return utils::compute_1d_workgroup_count(
108+
device, static_cast<uint32_t>(total_tiles), wg_size, op_name);
109+
}
110+
53111
// et_vk.linear_q4gsw args: [in, weight, scales, group_size, bias, out].
54112
void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
55113
const int in_id = args.at(0);
@@ -137,44 +195,8 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
137195
const char* shader_src = use_gemv ? kQ4gswLinearCoop4BicolWGSL
138196
: use_shmem_gemm ? kQ4gswLinearGemmShmemWGSL
139197
: kQ4gswLinearWGSL;
140-
uint32_t workgroup_count;
141-
if (use_gemv) {
142-
// bicol: fixed 64 lanes, 2 output columns/workgroup, grid-strided over
143-
// ceil(N/2) column-pairs (M == 1 on this decode path).
144-
const uint64_t pairs = (static_cast<uint64_t>(N) + 1u) / 2u;
145-
if (pairs == 0u || pairs > UINT32_MAX) {
146-
throw std::runtime_error("WebGPU linear_q4gsw: N/2 out of range");
147-
}
148-
workgroup_count =
149-
utils::clamp_workgroup_count(device, static_cast<uint32_t>(pairs));
150-
if (workgroup_count == 0u) {
151-
throw std::runtime_error("WebGPU linear_q4gsw: zero GEMV dispatch");
152-
}
153-
} else if (use_shmem_gemm) {
154-
// shmem GEMM: one workgroup per tile, no grid-stride -> throw over limit.
155-
const int64_t total_wgs = utils::div_up<int64_t>(M, kQ4gswShmemTileM) *
156-
utils::div_up<int64_t>(N, kQ4gswShmemTileN);
157-
WGPULimits limits = {};
158-
const uint32_t max_wgs =
159-
wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
160-
limits.maxComputeWorkgroupsPerDimension > 0
161-
? limits.maxComputeWorkgroupsPerDimension
162-
: 65535u;
163-
if (total_wgs > static_cast<int64_t>(max_wgs)) {
164-
throw std::runtime_error(
165-
"WebGPU linear_q4gsw: shmem GEMM tile count exceeds the 1D dispatch limit");
166-
}
167-
workgroup_count = static_cast<uint32_t>(total_wgs);
168-
} else {
169-
const int64_t total_tiles = utils::div_up<int64_t>(M, kQ4gswTileM) *
170-
utils::div_up<int64_t>(N, kQ4gswTileN);
171-
if (total_tiles > static_cast<int64_t>(UINT32_MAX)) {
172-
throw std::runtime_error(
173-
"WebGPU linear_q4gsw: tile count exceeds the 1D dispatch limit");
174-
}
175-
workgroup_count = utils::compute_1d_workgroup_count(
176-
device, static_cast<uint32_t>(total_tiles), wg_size, "linear_q4gsw");
177-
}
198+
const uint32_t workgroup_count = compute_q4gsw_workgroup_count(
199+
device, use_gemv, use_shmem_gemm, M, N, wg_size, "linear_q4gsw");
178200

179201
// Optional bias: real buffer if present, else a dummy for the fixed layout.
180202
uint32_t has_bias = 0;
@@ -287,12 +309,78 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
287309
bg_desc.entries = bg_entries;
288310
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
289311

290-
graph.add_dispatch({pipeline, bind_group, workgroup_count, "linear_q4gsw"});
312+
const size_t dispatch_idx = graph.add_dispatch(
313+
{pipeline, bind_group, workgroup_count, "linear_q4gsw"});
314+
315+
// Dynamic shapes: recompute dispatch + params.M for the live M. use_gemv and
316+
// use_shmem_gemm are captured (routing is fixed at build); the helper re-runs
317+
// the same path's workgroup-count formula with the live m.
318+
graph.add_tensor_resize_hook(
319+
in_id,
320+
[in_id,
321+
out_id,
322+
M,
323+
K,
324+
N,
325+
K_packed,
326+
gs,
327+
padded_N,
328+
has_bias,
329+
wg_size,
330+
use_gemv,
331+
use_shmem_gemm,
332+
dispatch_idx,
333+
uniform_buffer](WebGPUGraph& g) {
334+
const auto& d = g.cur_dims(in_id);
335+
if (d.empty()) {
336+
throw std::runtime_error(
337+
"WebGPU linear_q4gsw(resize): empty input dims");
338+
}
339+
const uint64_t numel = utils::numel_of(d);
340+
if (numel % static_cast<uint64_t>(K) != 0u) {
341+
throw std::runtime_error(
342+
"WebGPU linear_q4gsw(resize): live input numel not a multiple "
343+
"of K");
344+
}
345+
const uint32_t m =
346+
static_cast<uint32_t>(numel / static_cast<uint64_t>(K));
347+
if (m == 0u) {
348+
throw std::runtime_error("WebGPU linear_q4gsw(resize): live M == 0");
349+
}
350+
// Buffers/bind-groups were sized for the build-time max M; a larger
351+
// live M would write out of bounds.
352+
if (m > M) {
353+
throw std::runtime_error(
354+
"WebGPU linear_q4gsw(resize): live M exceeds the build-time max");
355+
}
356+
const uint32_t wgc = compute_q4gsw_workgroup_count(
357+
g.device(),
358+
use_gemv,
359+
use_shmem_gemm,
360+
m,
361+
N,
362+
wg_size,
363+
"linear_q4gsw(resize)");
364+
Q4gswParams p = {};
365+
p.M = m;
366+
p.N = N;
367+
p.K = K;
368+
p.K_packed = K_packed;
369+
p.group_size = gs;
370+
p.padded_N = padded_N;
371+
p.has_bias = has_bias;
372+
wgpuQueueWriteBuffer(g.queue(), uniform_buffer, 0, &p, sizeof(p));
373+
g.dispatch_at(dispatch_idx).workgroup_count_x = wgc;
374+
std::vector<int64_t> od(d.begin(), d.end());
375+
od.back() = static_cast<int64_t>(N);
376+
g.set_cur_dims(out_id, od);
377+
});
291378

292379
wgpuShaderModuleRelease(shader);
293380
wgpuBindGroupLayoutRelease(bgl);
294381
wgpuPipelineLayoutRelease(pipeline_layout);
295-
wgpuBufferRelease(uniform_buffer);
382+
// Graph owns it so the resize hook can rewrite it; freed in the dtor.
383+
graph.own_uniform_buffer(uniform_buffer);
296384
}
297385

298386
} // namespace

0 commit comments

Comments
 (0)