Skip to content

Commit 80b6c34

Browse files
authored
[ExecuTorch][WebGPU] Register-tile the q4gsw quantized-linear kernel (#20500)
1 parent 45a14b9 commit 80b6c34

12 files changed

Lines changed: 547 additions & 64 deletions

File tree

backends/webgpu/runtime/WebGPUUtils.h

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@
1818

1919
namespace executorch::backends::webgpu::utils {
2020

21+
// Ceiling division for non-negative integers (mirrors Vulkan's utils::div_up).
22+
template <typename T>
23+
inline T div_up(T a, T b) {
24+
return (a + b - 1) / b;
25+
}
26+
2127
// Clamp workgroup size to device limit (SwiftShader caps at 128).
2228
inline uint32_t clamp_workgroup_size(WGPUDevice device, uint32_t desired) {
2329
WGPULimits limits = {};
@@ -34,7 +40,7 @@ inline uint32_t compute_1d_workgroup_count(
3440
uint32_t num_threads,
3541
uint32_t workgroup_size,
3642
const char* op_name) {
37-
uint32_t count = (num_threads + workgroup_size - 1) / workgroup_size;
43+
uint32_t count = div_up(num_threads, workgroup_size);
3844
WGPULimits limits = {};
3945
uint32_t max_count =
4046
wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
@@ -70,4 +76,16 @@ make_uniform(WGPUDevice device, const void* data, size_t size) {
7076
return buf;
7177
}
7278

79+
// Clamp a 1D workgroup count to the device limit, for grid-stride kernels that
80+
// loop over any excess work (vs compute_1d_workgroup_count, which throws).
81+
inline uint32_t clamp_workgroup_count(WGPUDevice device, uint32_t desired) {
82+
WGPULimits limits = {};
83+
uint32_t max_count =
84+
wgpuDeviceGetLimits(device, &limits) == WGPUStatus_Success &&
85+
limits.maxComputeWorkgroupsPerDimension > 0
86+
? limits.maxComputeWorkgroupsPerDimension
87+
: 65535u; // WebGPU spec-default floor
88+
return std::min(desired, max_count);
89+
}
90+
7391
} // namespace executorch::backends::webgpu::utils

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

Lines changed: 38 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <executorch/backends/webgpu/runtime/WebGPUGraph.h>
1010
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
1111
#include <executorch/backends/webgpu/runtime/ops/OperatorRegistry.h>
12+
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_coop4_wgsl.h>
1213
#include <executorch/backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear_wgsl.h>
1314

1415
#include <webgpu/webgpu.h>
@@ -34,6 +35,10 @@ struct Q4gswParams {
3435
};
3536
static_assert(sizeof(Q4gswParams) == 32, "Q4gswParams must be 32 bytes");
3637

38+
// Register-tile dims; MUST match TM/TN in q4gsw_linear.wgsl.
39+
constexpr int64_t kQ4gswTileM = 4;
40+
constexpr int64_t kQ4gswTileN = 4;
41+
3742
// et_vk.linear_q4gsw args: [in, weight, scales, group_size, bias, out].
3843
void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
3944
const int in_id = args.at(0);
@@ -85,10 +90,6 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
8590
"WebGPU linear_q4gsw: N*K_packed must be a multiple of 4 (u32-packed)");
8691
}
8792

88-
// One workgroup per output row (M); validate dispatch before any alloc.
89-
const uint32_t workgroup_count =
90-
utils::compute_1d_workgroup_count(device, M, 1, "linear_q4gsw");
91-
9293
// fp32-only byte-size guards (no runtime dtype); fp16 scales -> bail.
9394
const uint64_t scales_numel =
9495
static_cast<uint64_t>(num_groups) * static_cast<uint64_t>(padded_N);
@@ -116,6 +117,35 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
116117
"WebGPU linear_q4gsw: scales dims too small for K/N");
117118
}
118119

120+
// M==1 decode -> coop4 GEMV (needs K%8==0 && gs%8==0); else tiled GEMM.
121+
const uint32_t wg_size =
122+
utils::clamp_workgroup_size(device, kQ4gswLinearWorkgroupSizeX);
123+
const bool use_gemv = (M == 1u && K % 8u == 0u && gs % 8u == 0u);
124+
const char* shader_src = use_gemv ? kQ4gswLinearCoop4WGSL : kQ4gswLinearWGSL;
125+
uint32_t workgroup_count;
126+
if (use_gemv) {
127+
// coop4: fixed 64 lanes, 1 workgroup per output, grid-strided over M*N.
128+
const uint64_t outputs =
129+
static_cast<uint64_t>(M) * static_cast<uint64_t>(N);
130+
if (outputs == 0u || outputs > UINT32_MAX) {
131+
throw std::runtime_error("WebGPU linear_q4gsw: M*N out of range");
132+
}
133+
workgroup_count =
134+
utils::clamp_workgroup_count(device, static_cast<uint32_t>(outputs));
135+
if (workgroup_count == 0u) {
136+
throw std::runtime_error("WebGPU linear_q4gsw: zero GEMV dispatch");
137+
}
138+
} else {
139+
const int64_t total_tiles = utils::div_up<int64_t>(M, kQ4gswTileM) *
140+
utils::div_up<int64_t>(N, kQ4gswTileN);
141+
if (total_tiles > static_cast<int64_t>(UINT32_MAX)) {
142+
throw std::runtime_error(
143+
"WebGPU linear_q4gsw: tile count exceeds the 1D dispatch limit");
144+
}
145+
workgroup_count = utils::compute_1d_workgroup_count(
146+
device, static_cast<uint32_t>(total_tiles), wg_size, "linear_q4gsw");
147+
}
148+
119149
// Optional bias: real buffer if present, else a dummy for the fixed layout.
120150
uint32_t has_bias = 0;
121151
WGPUBuffer bias_buffer = nullptr;
@@ -156,7 +186,7 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
156186

157187
WGPUShaderSourceWGSL wgsl_desc = {};
158188
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
159-
wgsl_desc.code = {kQ4gswLinearWGSL, WGPU_STRLEN};
189+
wgsl_desc.code = {shader_src, WGPU_STRLEN};
160190
WGPUShaderModuleDescriptor shader_desc = {};
161191
shader_desc.nextInChain = &wgsl_desc.chain;
162192
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
@@ -186,8 +216,6 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
186216
WGPUPipelineLayout pipeline_layout =
187217
wgpuDeviceCreatePipelineLayout(device, &pl_desc);
188218

189-
const uint32_t wg_size =
190-
utils::clamp_workgroup_size(device, kQ4gswLinearWorkgroupSizeX);
191219
WGPUConstantEntry wg_size_constant = {};
192220
wg_size_constant.key = {"wg_size", WGPU_STRLEN};
193221
wg_size_constant.value = static_cast<double>(wg_size);
@@ -196,8 +224,9 @@ void q4gsw_linear_impl(WebGPUGraph& graph, const std::vector<int>& args) {
196224
pipeline_desc.layout = pipeline_layout;
197225
pipeline_desc.compute.module = shader;
198226
pipeline_desc.compute.entryPoint = {"main", WGPU_STRLEN};
199-
pipeline_desc.compute.constantCount = 1;
200-
pipeline_desc.compute.constants = &wg_size_constant;
227+
// coop4 GEMV uses fixed @workgroup_size(64); only the GEMM has an override.
228+
pipeline_desc.compute.constantCount = use_gemv ? 0u : 1u;
229+
pipeline_desc.compute.constants = use_gemv ? nullptr : &wg_size_constant;
201230
WGPUComputePipeline pipeline =
202231
wgpuDeviceCreateComputePipeline(device, &pipeline_desc);
203232

backends/webgpu/runtime/ops/quantized_linear/q4gsw_linear.wgsl

Lines changed: 52 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -18,47 +18,74 @@ struct Params {
1818

1919
override wg_size: u32 = 64u;
2020

21-
// One workgroup per row m, threads stride N; loop logical K only (in-bounds).
21+
// Register-tiled GEMM: dequant weight once per (n,k), reused across TM rows.
22+
const TM: u32 = 4u;
23+
const TN: u32 = 4u;
24+
const TILE_ELEMS: u32 = TM * TN; // accumulator size; keeps acc in sync with TM/TN
25+
2226
@compute @workgroup_size(wg_size, 1, 1)
23-
fn main(
24-
@builtin(workgroup_id) wid: vec3<u32>,
25-
@builtin(local_invocation_id) lid: vec3<u32>) {
26-
let m = wid.x;
27-
if (m >= params.M) {
27+
fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
28+
let nrt = (params.M + TM - 1u) / TM;
29+
let nct = (params.N + TN - 1u) / TN;
30+
let tiles = nrt * nct;
31+
// M==0 or N==0 -> tiles==0 -> every thread returns here, so the M-1u/N-1u
32+
// clamps below never underflow (the host also rejects M==0/N==0).
33+
if (gid.x >= tiles) {
2834
return;
2935
}
30-
let in_base = m * params.K;
36+
let row_tile = gid.x / nct;
37+
let col_tile = gid.x % nct;
38+
let m0 = row_tile * TM;
39+
let n0 = col_tile * TN;
40+
41+
var acc: array<f32, TILE_ELEMS>;
42+
for (var i: u32 = 0u; i < TILE_ELEMS; i = i + 1u) {
43+
acc[i] = 0.0;
44+
}
3145

32-
var n: u32 = lid.x;
46+
var k: u32 = 0u;
3347
loop {
34-
if (n >= params.N) {
48+
if (k >= params.K) {
3549
break;
3650
}
37-
var acc: f32 = 0.0;
38-
var k: u32 = 0u;
39-
loop {
40-
if (k >= params.K) {
41-
break;
42-
}
43-
// Packed weight byte for (n, k): row stride K_packed bytes, byte k/2.
44-
let byte_idx = n * params.K_packed + (k >> 1u);
51+
// Load the TM input values for column k once; reused across all TN columns.
52+
var in_reg: array<f32, TM>;
53+
for (var ml: u32 = 0u; ml < TM; ml = ml + 1u) {
54+
let m_eff = min(m0 + ml, params.M - 1u);
55+
in_reg[ml] = t_input[m_eff * params.K + k];
56+
}
57+
for (var nl: u32 = 0u; nl < TN; nl = nl + 1u) {
58+
// Clamp to last valid column; overhang result is never stored.
59+
let n_eff = min(n0 + nl, params.N - 1u);
60+
let byte_idx = n_eff * params.K_packed + (k >> 1u);
4561
let word = t_weight[byte_idx >> 2u];
4662
let b = (word >> ((byte_idx & 3u) * 8u)) & 0xFFu;
4763
var nib: u32;
4864
if ((k & 1u) == 0u) {
49-
nib = b & 0x0Fu; // even k -> low nibble
65+
nib = b & 0x0Fu; // even k -> low nibble
5066
} else {
5167
nib = (b >> 4u) & 0x0Fu; // odd k -> high nibble
5268
}
5369
let q = f32(i32(nib) - 8); // +8-shifted on pack; recover signed [-8,7]
54-
let scale = t_scales[(k / params.group_size) * params.padded_N + n];
55-
acc = acc + t_input[in_base + k] * q * scale;
56-
k = k + 1u;
70+
let dq = q * t_scales[(k / params.group_size) * params.padded_N + n_eff];
71+
for (var ml: u32 = 0u; ml < TM; ml = ml + 1u) {
72+
acc[ml * TN + nl] = acc[ml * TN + nl] + in_reg[ml] * dq;
73+
}
5774
}
58-
if (params.has_bias != 0u) {
59-
acc = acc + t_bias[n];
75+
k = k + 1u;
76+
}
77+
78+
for (var ml: u32 = 0u; ml < TM; ml = ml + 1u) {
79+
let m = m0 + ml;
80+
for (var nl: u32 = 0u; nl < TN; nl = nl + 1u) {
81+
let n = n0 + nl;
82+
if (m < params.M && n < params.N) {
83+
var v = acc[ml * TN + nl];
84+
if (params.has_bias != 0u) {
85+
v = v + t_bias[n];
86+
}
87+
t_out[m * params.N + n] = v;
88+
}
6089
}
61-
t_out[m * params.N + n] = acc;
62-
n = n + wg_size;
6390
}
6491
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
@group(0) @binding(0) var<storage, read_write> t_out: array<f32>;
2+
@group(0) @binding(1) var<storage, read> t_input: array<f32>;
3+
@group(0) @binding(2) var<storage, read> t_weight: array<u32>;
4+
@group(0) @binding(3) var<storage, read> t_scales: array<f32>;
5+
@group(0) @binding(4) var<storage, read> t_bias: array<f32>;
6+
7+
struct Params {
8+
M: u32,
9+
N: u32,
10+
K: u32,
11+
K_packed: u32,
12+
group_size: u32,
13+
padded_N: u32,
14+
has_bias: u32,
15+
_pad: u32,
16+
}
17+
@group(0) @binding(5) var<uniform> params: Params;
18+
19+
// Cooperative-over-K GEMV with u32-batched coalesced weight loads (64 lanes).
20+
const WG: u32 = 64u;
21+
var<workgroup> partial: array<f32, WG>;
22+
23+
@compute @workgroup_size(WG, 1, 1)
24+
fn main(
25+
@builtin(workgroup_id) wid: vec3<u32>,
26+
@builtin(num_workgroups) ngrp: vec3<u32>,
27+
@builtin(local_invocation_id) lid: vec3<u32>) {
28+
let total = params.M * params.N;
29+
let stride = ngrp.x;
30+
let num_words = params.K >> 3u; // K / 8 words per row
31+
let row_words = params.K_packed >> 2u; // u32s per weight row (= K/8)
32+
var idx = wid.x;
33+
loop {
34+
if (idx >= total) {
35+
break;
36+
}
37+
let m = idx / params.N;
38+
let n = idx % params.N;
39+
let in_base = m * params.K;
40+
let wbase = n * row_words;
41+
42+
var acc: f32 = 0.0;
43+
var w: u32 = lid.x;
44+
loop {
45+
if (w >= num_words) {
46+
break;
47+
}
48+
let word = t_weight[wbase + w];
49+
let k0 = w << 3u; // first K of this word
50+
let scale = t_scales[(k0 / params.group_size) * params.padded_N + n];
51+
let ib = in_base + k0;
52+
// 4 bytes, low+high nibble each -> 8 consecutive K.
53+
for (var bi: u32 = 0u; bi < 4u; bi = bi + 1u) {
54+
let byte = (word >> (bi * 8u)) & 0xFFu;
55+
let lo = f32(i32(byte & 0x0Fu) - 8);
56+
let hi = f32(i32((byte >> 4u) & 0x0Fu) - 8);
57+
let kk = bi << 1u;
58+
acc = acc + t_input[ib + kk] * lo * scale;
59+
acc = acc + t_input[ib + kk + 1u] * hi * scale;
60+
}
61+
w = w + WG;
62+
}
63+
64+
partial[lid.x] = acc;
65+
workgroupBarrier();
66+
var s: u32 = WG >> 1u;
67+
loop {
68+
if (s == 0u) {
69+
break;
70+
}
71+
if (lid.x < s) {
72+
partial[lid.x] = partial[lid.x] + partial[lid.x + s];
73+
}
74+
workgroupBarrier();
75+
s = s >> 1u;
76+
}
77+
if (lid.x == 0u) {
78+
var o = partial[0];
79+
if (params.has_bias != 0u) {
80+
o = o + t_bias[n];
81+
}
82+
t_out[idx] = o;
83+
}
84+
workgroupBarrier();
85+
idx = idx + stride;
86+
}
87+
}

0 commit comments

Comments
 (0)