Skip to content

Commit a753492

Browse files
committed
[ExecuTorch][WebGPU] GPU timestamp query profiling for SDPA
Pull Request resolved: #20167 Add a faithful re-port of Vulkan's `vkapi::QueryPool` (`backends/vulkan/runtime/vk_api/QueryPool.{h,cpp}`) so a bench can read true on-GPU per-kernel time, isolated from submit/readback latency — the basis for comparing the WGSL SDPA kernels against the Vulkan reference. Opt-in via the `WEBGPU_TIMESTAMP_QUERY` env var; off by default, so the production `execute()` path is byte-identical. `WebGPUQueryPool` mirrors the Vulkan `ShaderDuration` data model and the ticks->ns conversion exactly. Three deviations are forced by the WebGPU API (not unforced divergences): per-dispatch bracketing uses a compute-pass `timestampWrites` descriptor (begin/end-of-pass) since WebGPU has no mid-encoder `writeTimestamp`; results are read via `resolveQuerySet` + buffer map (no host-side `vkGetQueryPoolResults`); and the `TimestampQuery` capability is requested as an explicit device feature (fail-open if the adapter lacks it). `WebGPUGraph::execute()` brackets each compute pass when the pool is active; chained `update_cache`/QK/softmax/AV dispatches carry a `kernel_name` label for attribution. Co-authored-with Claude. ghstack-source-id: 392065610 @exported-using-ghexport Differential Revision: [D107678235](https://our.internmc.facebook.com/intern/diff/D107678235/)
1 parent 231509c commit a753492

2 files changed

Lines changed: 137 additions & 7 deletions

File tree

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

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,8 @@ void build_dispatch(
156156
uint64_t uniform_size,
157157
uint32_t workgroup_count_x,
158158
uint32_t wg_size,
159-
bool retain_uniform = false) {
159+
bool retain_uniform = false,
160+
const char* kernel_name = "") {
160161
WGPUDevice device = graph.device();
161162

162163
WGPUShaderSourceWGSL wgsl_desc = {};
@@ -227,7 +228,7 @@ void build_dispatch(
227228
bg_desc.entries = bg_entries;
228229
WGPUBindGroup bind_group = wgpuDeviceCreateBindGroup(device, &bg_desc);
229230

230-
graph.add_dispatch({pipeline, bind_group, workgroup_count_x});
231+
graph.add_dispatch({pipeline, bind_group, workgroup_count_x, kernel_name});
231232

232233
wgpuShaderModuleRelease(shader);
233234
wgpuBindGroupLayoutRelease(bgl);
@@ -269,7 +270,8 @@ static WGPUBuffer record_update_cache_dispatch(
269270
sizeof(uc),
270271
wgc,
271272
uc_wg,
272-
dynamic_pos);
273+
dynamic_pos,
274+
"update_cache");
273275
return ubuf;
274276
}
275277

@@ -473,7 +475,8 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
473475
sizeof(p),
474476
wgc,
475477
qk_wg,
476-
dynamic_pos);
478+
dynamic_pos,
479+
"sdpa_compute_attn_weights");
477480
qk_buf = ubuf;
478481
qk_idx = graph.num_dispatches() - 1;
479482
}
@@ -496,7 +499,8 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
496499
sizeof(p),
497500
wgc,
498501
0,
499-
dynamic_pos);
502+
dynamic_pos,
503+
"sdpa_softmax");
500504
softmax_buf = ubuf;
501505
}
502506

@@ -521,7 +525,8 @@ void sdpa_with_kv_cache_impl(WebGPUGraph& graph, const std::vector<int>& args) {
521525
sizeof(p),
522526
wgc,
523527
av_wg,
524-
dynamic_pos);
528+
dynamic_pos,
529+
"sdpa_compute_out");
525530
av_buf = ubuf;
526531
}
527532

backends/webgpu/test/test_webgpu_native.cpp

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1122,6 +1122,129 @@ static bool test_resize_hook(const std::string& blob_path) {
11221122
return true;
11231123
}
11241124

1125+
// Capacity-overrun must throw; runs without a device or TimestampQuery.
1126+
static bool test_query_pool_overrun_throws() {
1127+
printf("\n--- Test: WebGPUQueryPool capacity-overrun guard ---\n");
1128+
WebGPUQueryPool qp;
1129+
try {
1130+
qp.reset(1);
1131+
} catch (const std::exception&) {
1132+
printf("PASS: reset beyond capacity throws\n");
1133+
return true;
1134+
}
1135+
printf("FAIL: reset beyond capacity did not throw\n");
1136+
return false;
1137+
}
1138+
1139+
// WebGPUQueryPool roundtrip: time a probe pass; assert non-zero GPU duration.
1140+
static bool test_query_pool_roundtrip(const WebGPUContext& ctx) {
1141+
printf("\n--- Test: WebGPUQueryPool roundtrip ---\n");
1142+
if (!ctx.timestamp_supported) {
1143+
printf("SKIP: adapter lacks TimestampQuery feature\n");
1144+
return true;
1145+
}
1146+
WGPUDevice device = ctx.device;
1147+
1148+
// Probe loop iterates enough to burn a measurable, non-zero GPU duration.
1149+
const char* kProbeWGSL =
1150+
"@group(0) @binding(0) var<storage, read_write> out: array<f32>;\n"
1151+
"@compute @workgroup_size(64)\n"
1152+
"fn main(@builtin(global_invocation_id) gid: vec3<u32>) {\n"
1153+
" var acc = 0.0;\n"
1154+
" for (var i = 0u; i < 8192u; i = i + 1u) {\n"
1155+
" acc = acc + f32(i) * 1.000001;\n"
1156+
" }\n"
1157+
" out[gid.x] = acc;\n"
1158+
"}\n";
1159+
1160+
WGPUShaderSourceWGSL wgsl_desc = {};
1161+
wgsl_desc.chain.sType = WGPUSType_ShaderSourceWGSL;
1162+
wgsl_desc.code = {kProbeWGSL, WGPU_STRLEN};
1163+
WGPUShaderModuleDescriptor shader_desc = {};
1164+
shader_desc.nextInChain = &wgsl_desc.chain;
1165+
WGPUShaderModule shader = wgpuDeviceCreateShaderModule(device, &shader_desc);
1166+
1167+
WGPUBindGroupLayoutEntry bgl_entry = {};
1168+
bgl_entry.binding = 0;
1169+
bgl_entry.visibility = WGPUShaderStage_Compute;
1170+
bgl_entry.buffer.type = WGPUBufferBindingType_Storage;
1171+
WGPUBindGroupLayoutDescriptor bgl_desc = {};
1172+
bgl_desc.entryCount = 1;
1173+
bgl_desc.entries = &bgl_entry;
1174+
WGPUBindGroupLayout bgl = wgpuDeviceCreateBindGroupLayout(device, &bgl_desc);
1175+
1176+
WGPUPipelineLayoutDescriptor pl_desc = {};
1177+
pl_desc.bindGroupLayoutCount = 1;
1178+
pl_desc.bindGroupLayouts = &bgl;
1179+
WGPUPipelineLayout pl = wgpuDeviceCreatePipelineLayout(device, &pl_desc);
1180+
1181+
WGPUComputePipelineDescriptor pipe_desc = {};
1182+
pipe_desc.layout = pl;
1183+
pipe_desc.compute.module = shader;
1184+
pipe_desc.compute.entryPoint = {"main", WGPU_STRLEN};
1185+
WGPUComputePipeline pipe =
1186+
wgpuDeviceCreateComputePipeline(device, &pipe_desc);
1187+
1188+
WGPUBufferDescriptor obd = {};
1189+
obd.size = 64 * sizeof(float);
1190+
obd.usage = WGPUBufferUsage_Storage;
1191+
WGPUBuffer out_buf = wgpuDeviceCreateBuffer(device, &obd);
1192+
1193+
WGPUBindGroupEntry bg_entry = {};
1194+
bg_entry.binding = 0;
1195+
bg_entry.buffer = out_buf;
1196+
bg_entry.size = obd.size;
1197+
WGPUBindGroupDescriptor bg_desc = {};
1198+
bg_desc.layout = bgl;
1199+
bg_desc.entryCount = 1;
1200+
bg_desc.entries = &bg_entry;
1201+
WGPUBindGroup bg = wgpuDeviceCreateBindGroup(device, &bg_desc);
1202+
1203+
WebGPUQueryPool qp;
1204+
qp.initialize(device, 1);
1205+
qp.reset(1);
1206+
1207+
WGPUCommandEncoder enc = wgpuDeviceCreateCommandEncoder(device, nullptr);
1208+
WGPUPassTimestampWrites tw = qp.writes_for(0);
1209+
WGPUComputePassDescriptor pass_desc = {};
1210+
pass_desc.timestampWrites = &tw;
1211+
WGPUComputePassEncoder pass =
1212+
wgpuCommandEncoderBeginComputePass(enc, &pass_desc);
1213+
wgpuComputePassEncoderSetPipeline(pass, pipe);
1214+
wgpuComputePassEncoderSetBindGroup(pass, 0, bg, 0, nullptr);
1215+
wgpuComputePassEncoderDispatchWorkgroups(pass, 1, 1, 1);
1216+
wgpuComputePassEncoderEnd(pass);
1217+
wgpuComputePassEncoderRelease(pass);
1218+
qp.record(0, "probe", {1, 1, 1}, {64, 1, 1});
1219+
qp.resolve(enc);
1220+
WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(enc, nullptr);
1221+
wgpuQueueSubmit(ctx.queue, 1, &cmd);
1222+
wgpuCommandBufferRelease(cmd);
1223+
wgpuCommandEncoderRelease(enc);
1224+
1225+
qp.extract_results(ctx.instance);
1226+
1227+
wgpuBufferRelease(out_buf);
1228+
wgpuComputePipelineRelease(pipe);
1229+
wgpuPipelineLayoutRelease(pl);
1230+
wgpuBindGroupLayoutRelease(bgl);
1231+
wgpuBindGroupRelease(bg);
1232+
wgpuShaderModuleRelease(shader);
1233+
1234+
if (qp.results().size() != 1) {
1235+
printf("FAIL: expected 1 duration, got %zu\n", qp.results().size());
1236+
return false;
1237+
}
1238+
const uint64_t dur = qp.results()[0].execution_duration_ns;
1239+
printf(" probe duration: %llu ns\n", (unsigned long long)dur);
1240+
if (dur == 0) {
1241+
printf("FAIL: probe duration is zero (expected monotonic non-zero)\n");
1242+
return false;
1243+
}
1244+
printf("PASS: WebGPUQueryPool roundtrip -- non-zero GPU kernel duration\n");
1245+
return true;
1246+
}
1247+
11251248
int main(int argc, char** argv) {
11261249
std::string model_path = "webgpu_add_test.pte";
11271250
if (argc > 1) {
@@ -1163,7 +1286,9 @@ int main(int argc, char** argv) {
11631286
set_default_webgpu_context(&ctx);
11641287
printf("WebGPU device acquired (native)\n");
11651288

1166-
bool ok = test_single_add(model_path);
1289+
bool ok = test_query_pool_overrun_throws();
1290+
ok = test_query_pool_roundtrip(ctx) && ok;
1291+
ok = test_single_add(model_path) && ok;
11671292

11681293
if (!chained_model_path.empty()) {
11691294
ok = test_chained_add(chained_model_path) && ok;

0 commit comments

Comments
 (0)