Skip to content

Commit 772fde7

Browse files
committed
Update
[ghstack-poisoned]
1 parent 45cb409 commit 772fde7

7 files changed

Lines changed: 380 additions & 1 deletion

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ set(WEBGPU_SRCS
3030
runtime/WebGPUGraph.cpp
3131
runtime/WebGPUDelegateHeader.cpp
3232
runtime/WebGPUDevice.cpp
33+
runtime/WebGPUQueryPool.cpp
3334
runtime/ops/OperatorRegistry.cpp
3435
runtime/ops/add/BinaryOp.cpp
3536
runtime/ops/rms_norm/RmsNorm.cpp

backends/webgpu/runtime/WebGPUDevice.cpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
#include <cstdlib>
1414
#include <memory>
1515
#include <stdexcept>
16+
#include <vector>
1617

1718
namespace executorch {
1819
namespace backends {
@@ -137,6 +138,16 @@ WebGPUContext create_webgpu_context() {
137138
WGPUStatus_Success) {
138139
device_desc.requiredLimits = &supported_limits;
139140
}
141+
142+
// Bench: enable TimestampQuery if available; fail-open (skip timing if not).
143+
std::vector<WGPUFeatureName> required_features;
144+
if (wgpuAdapterHasFeature(ctx.adapter, WGPUFeatureName_TimestampQuery)) {
145+
required_features.push_back(WGPUFeatureName_TimestampQuery);
146+
device_desc.requiredFeatureCount = required_features.size();
147+
device_desc.requiredFeatures = required_features.data();
148+
ctx.timestamp_supported = true;
149+
}
150+
140151
device_desc.uncapturedErrorCallbackInfo.callback = on_device_error;
141152

142153
WGPUWaitStatus device_wait = webgpu_wait(

backends/webgpu/runtime/WebGPUDevice.h

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010

1111
#include <webgpu/webgpu.h>
1212

13+
#include <executorch/backends/webgpu/runtime/WebGPUQueryPool.h>
14+
15+
#include <memory>
16+
1317
namespace executorch {
1418
namespace backends {
1519
namespace webgpu {
@@ -19,6 +23,10 @@ struct WebGPUContext {
1923
WGPUAdapter adapter = nullptr;
2024
WGPUDevice device = nullptr;
2125
WGPUQueue queue = nullptr;
26+
// True if the device was created with the TimestampQuery feature (bench).
27+
bool timestamp_supported = false;
28+
// Bench-only: timestamp-query pool, lazily created in execute() (env-gated).
29+
std::unique_ptr<WebGPUQueryPool> querypool;
2230
};
2331

2432
WebGPUContext create_webgpu_context();

backends/webgpu/runtime/WebGPUGraph.cpp

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#include <executorch/backends/webgpu/runtime/WebGPUCompat.h>
1616
#include <executorch/backends/webgpu/runtime/WebGPUDevice.h>
1717

18+
#include <cstdlib>
1819
#include <cstring>
1920
#include <stdexcept>
2021

@@ -496,18 +497,48 @@ void WebGPUGraph::copy_inputs(
496497
}
497498
}
498499

500+
namespace {
501+
// Bench gate: WEBGPU_TIMESTAMP_QUERY enables per-pass GPU timestamp queries.
502+
bool should_timestamp_query() {
503+
static const bool enabled = std::getenv("WEBGPU_TIMESTAMP_QUERY") != nullptr;
504+
return enabled;
505+
}
506+
} // namespace
507+
499508
void WebGPUGraph::execute() {
500509
const size_t n = dispatches_.size();
501510
const size_t chunk = execute_config_.chunk_size;
502511

503512
if (chunk == 0 || n <= chunk) {
513+
// Bench: timestamp-query pool, null unless env-gated + feature present.
514+
WebGPUQueryPool* qp = nullptr;
515+
if (should_timestamp_query() && n > 0) {
516+
if (auto* ctx = get_default_webgpu_context()) {
517+
if (ctx->timestamp_supported) {
518+
if (!ctx->querypool || ctx->querypool->capacity() < n) {
519+
ctx->querypool = std::make_unique<WebGPUQueryPool>();
520+
ctx->querypool->initialize(device_, static_cast<uint32_t>(n));
521+
}
522+
qp = ctx->querypool.get();
523+
qp->reset(static_cast<uint32_t>(n));
524+
}
525+
}
526+
}
527+
504528
WGPUCommandEncoderDescriptor enc_desc = {};
505529
WGPUCommandEncoder encoder =
506530
wgpuDeviceCreateCommandEncoder(device_, &enc_desc);
507531

508532
// One pass per dispatch: enforces storage RAW ordering across deps.
509-
for (const auto& dispatch : dispatches_) {
533+
for (size_t i = 0; i < n; i++) {
534+
const auto& dispatch = dispatches_[i];
535+
// tw must outlive BeginComputePass (the descriptor points at it).
536+
WGPUPassTimestampWrites tw = {};
510537
WGPUComputePassDescriptor pass_desc = {};
538+
if (qp) {
539+
tw = qp->writes_for(static_cast<uint32_t>(i));
540+
pass_desc.timestampWrites = &tw;
541+
}
511542
WGPUComputePassEncoder pass =
512543
wgpuCommandEncoderBeginComputePass(encoder, &pass_desc);
513544
wgpuComputePassEncoderSetPipeline(pass, dispatch.pipeline);
@@ -517,22 +548,45 @@ void WebGPUGraph::execute() {
517548
pass, dispatch.workgroup_count_x, 1, 1);
518549
wgpuComputePassEncoderEnd(pass);
519550
wgpuComputePassEncoderRelease(pass);
551+
if (qp) {
552+
qp->record(
553+
static_cast<uint32_t>(i),
554+
dispatch.kernel_name,
555+
{dispatch.workgroup_count_x, 1, 1},
556+
{1, 1, 1});
557+
}
520558
}
521559

522560
for (const auto& copy : output_copies_) {
523561
wgpuCommandEncoderCopyBufferToBuffer(
524562
encoder, copy.src_buffer, 0, copy.staging_buffer, 0, copy.nbytes);
525563
}
526564

565+
if (qp) {
566+
qp->resolve(encoder);
567+
}
568+
527569
WGPUCommandBufferDescriptor cmd_desc = {};
528570
WGPUCommandBuffer cmd = wgpuCommandEncoderFinish(encoder, &cmd_desc);
529571
wgpuQueueSubmit(queue_, 1, &cmd);
530572

531573
wgpuCommandBufferRelease(cmd);
532574
wgpuCommandEncoderRelease(encoder);
575+
576+
if (qp) {
577+
qp->extract_results(instance_);
578+
qp->print_results();
579+
}
533580
return;
534581
}
535582

583+
// GPU timestamp queries assume one submit; chunked execute is multi-submit.
584+
if (should_timestamp_query()) {
585+
throw std::runtime_error(
586+
"WebGPU: WEBGPU_TIMESTAMP_QUERY is incompatible with chunked execute "
587+
"(multi-submit); disable chunking to use GPU timestamp queries");
588+
}
589+
536590
const size_t first_chunk = execute_config_.initial_chunk_size > 0
537591
? execute_config_.initial_chunk_size
538592
: chunk;

backends/webgpu/runtime/WebGPUGraph.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ struct WebGPUDispatch {
3131
WGPUComputePipeline pipeline = nullptr;
3232
WGPUBindGroup bind_group = nullptr;
3333
uint32_t workgroup_count_x = 1;
34+
std::string kernel_name; // bench label
3435
};
3536

3637
struct OutputCopy {
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
* Copyright (c) Meta Platforms, Inc. and affiliates.
3+
* All rights reserved.
4+
*
5+
* This source code is licensed under the BSD-style license found in the
6+
* LICENSE file in the root directory of this source tree.
7+
*/
8+
9+
#include <executorch/backends/webgpu/runtime/WebGPUCompat.h>
10+
#include <executorch/backends/webgpu/runtime/WebGPUQueryPool.h>
11+
12+
#include <cstdio>
13+
#include <map>
14+
#include <stdexcept>
15+
#include <string>
16+
17+
namespace executorch::backends::webgpu {
18+
19+
namespace {
20+
21+
struct MapCallbackData {
22+
WGPUMapAsyncStatus status = WGPUMapAsyncStatus_Error;
23+
};
24+
25+
void map_callback(
26+
WGPUMapAsyncStatus status,
27+
WGPUStringView /*message*/,
28+
void* userdata1,
29+
void* /*userdata2*/) {
30+
auto* data = static_cast<MapCallbackData*>(userdata1);
31+
data->status = status;
32+
}
33+
34+
constexpr uint64_t kTimestampBytes = sizeof(uint64_t);
35+
36+
} // namespace
37+
38+
WebGPUQueryPool::~WebGPUQueryPool() {
39+
if (readback_buf_) {
40+
wgpuBufferRelease(readback_buf_);
41+
}
42+
if (resolve_buf_) {
43+
wgpuBufferRelease(resolve_buf_);
44+
}
45+
if (qset_) {
46+
wgpuQuerySetRelease(qset_);
47+
}
48+
}
49+
50+
void WebGPUQueryPool::initialize(WGPUDevice device, uint32_t max_pairs) {
51+
if (max_pairs == 0) {
52+
return;
53+
}
54+
// Re-init guard; mirrors Vulkan QueryPool (avoids leaking a prior QuerySet).
55+
if (qset_ != nullptr) {
56+
return;
57+
}
58+
capacity_pairs_ = max_pairs;
59+
const uint32_t count = 2 * max_pairs;
60+
const uint64_t bytes = static_cast<uint64_t>(count) * kTimestampBytes;
61+
62+
WGPUQuerySetDescriptor qsd = {};
63+
qsd.type = WGPUQueryType_Timestamp;
64+
qsd.count = count;
65+
qset_ = wgpuDeviceCreateQuerySet(device, &qsd);
66+
67+
WGPUBufferDescriptor rbd = {};
68+
rbd.size = bytes;
69+
rbd.usage = WGPUBufferUsage_QueryResolve | WGPUBufferUsage_CopySrc;
70+
resolve_buf_ = wgpuDeviceCreateBuffer(device, &rbd);
71+
72+
WGPUBufferDescriptor mbd = {};
73+
mbd.size = bytes;
74+
mbd.usage = WGPUBufferUsage_MapRead | WGPUBufferUsage_CopyDst;
75+
readback_buf_ = wgpuDeviceCreateBuffer(device, &mbd);
76+
// WebGPU timestamps are already nanoseconds, so ns_per_tick_ stays 1.0.
77+
}
78+
79+
void WebGPUQueryPool::reset(uint32_t num_dispatches) {
80+
// Fail loud on overrun; mirrors Vulkan QueryPool VK_CHECK_COND guard.
81+
if (num_dispatches > capacity_pairs_) {
82+
throw std::runtime_error(
83+
"WebGPUQueryPool: num_dispatches " + std::to_string(num_dispatches) +
84+
" exceeds capacity " + std::to_string(capacity_pairs_));
85+
}
86+
num_pairs_ = num_dispatches;
87+
durations_.clear();
88+
}
89+
90+
WGPUPassTimestampWrites WebGPUQueryPool::writes_for(uint32_t i) {
91+
WGPUPassTimestampWrites tw = {};
92+
tw.querySet = qset_;
93+
tw.beginningOfPassWriteIndex = 2 * i;
94+
tw.endOfPassWriteIndex = 2 * i + 1;
95+
return tw;
96+
}
97+
98+
void WebGPUQueryPool::record(
99+
uint32_t i,
100+
const std::string& name,
101+
std::array<uint32_t, 3> gwg,
102+
std::array<uint32_t, 3> lwg) {
103+
ShaderDuration d;
104+
d.idx = i;
105+
d.kernel_name = name;
106+
d.global_wg = gwg;
107+
d.local_wg = lwg;
108+
durations_.push_back(d);
109+
}
110+
111+
void WebGPUQueryPool::resolve(WGPUCommandEncoder encoder) {
112+
if (num_pairs_ == 0) {
113+
return;
114+
}
115+
const uint32_t count = 2 * num_pairs_;
116+
wgpuCommandEncoderResolveQuerySet(encoder, qset_, 0, count, resolve_buf_, 0);
117+
wgpuCommandEncoderCopyBufferToBuffer(
118+
encoder,
119+
resolve_buf_,
120+
0,
121+
readback_buf_,
122+
0,
123+
static_cast<uint64_t>(count) * kTimestampBytes);
124+
}
125+
126+
void WebGPUQueryPool::extract_results(WGPUInstance instance) {
127+
if (num_pairs_ == 0) {
128+
return;
129+
}
130+
const uint32_t count = 2 * num_pairs_;
131+
const uint64_t bytes = static_cast<uint64_t>(count) * kTimestampBytes;
132+
133+
MapCallbackData cb;
134+
WGPUBufferMapCallbackInfo cb_info = {};
135+
cb_info.mode = WGPUCallbackMode_WaitAnyOnly;
136+
cb_info.callback = map_callback;
137+
cb_info.userdata1 = &cb;
138+
webgpu_wait(
139+
instance,
140+
wgpuBufferMapAsync(readback_buf_, WGPUMapMode_Read, 0, bytes, cb_info));
141+
142+
if (cb.status != WGPUMapAsyncStatus_Success) {
143+
printf(
144+
"WebGPUQueryPool: readback map failed (status %d)\n", (int)cb.status);
145+
return;
146+
}
147+
const uint64_t* ticks = static_cast<const uint64_t*>(
148+
wgpuBufferGetConstMappedRange(readback_buf_, 0, bytes));
149+
if (ticks != nullptr) {
150+
for (auto& d : durations_) {
151+
const uint64_t t0 = ticks[2 * d.idx];
152+
const uint64_t t1 = ticks[2 * d.idx + 1];
153+
d.start_time_ns = static_cast<uint64_t>(t0 * ns_per_tick_);
154+
d.end_time_ns = static_cast<uint64_t>(t1 * ns_per_tick_);
155+
d.execution_duration_ns =
156+
(t1 >= t0) ? static_cast<uint64_t>((t1 - t0) * ns_per_tick_) : 0;
157+
}
158+
}
159+
wgpuBufferUnmap(readback_buf_);
160+
}
161+
162+
void WebGPUQueryPool::print_results(bool tsv) const {
163+
const char* sep = tsv ? "\t" : " ";
164+
if (tsv) {
165+
printf("idx%skernel%sgwg%sduration_us\n", sep, sep, sep);
166+
} else {
167+
printf("=== WebGPUQueryPool: per-dispatch GPU time ===\n");
168+
}
169+
for (const auto& d : durations_) {
170+
const double us = d.execution_duration_ns / 1000.0;
171+
printf(
172+
"%u%s%s%s(%u,%u,%u)%s%.3f\n",
173+
d.idx,
174+
sep,
175+
d.kernel_name.empty() ? "dispatch" : d.kernel_name.c_str(),
176+
sep,
177+
d.global_wg[0],
178+
d.global_wg[1],
179+
d.global_wg[2],
180+
sep,
181+
us);
182+
}
183+
if (tsv) {
184+
return;
185+
}
186+
std::map<std::string, std::pair<uint64_t, uint32_t>> totals;
187+
for (const auto& d : durations_) {
188+
auto& t = totals[d.kernel_name.empty() ? "dispatch" : d.kernel_name];
189+
t.first += d.execution_duration_ns;
190+
t.second += 1;
191+
}
192+
printf("--- per-kernel mean / total (us) ---\n");
193+
for (const auto& kv : totals) {
194+
const double mean_us = kv.second.first / kv.second.second / 1000.0;
195+
const double total_us = kv.second.first / 1000.0;
196+
printf(
197+
"%s%smean %.3f%stotal %.3f (n=%u)\n",
198+
kv.first.c_str(),
199+
sep,
200+
mean_us,
201+
sep,
202+
total_us,
203+
kv.second.second);
204+
}
205+
}
206+
207+
uint64_t WebGPUQueryPool::get_mean_shader_ns(
208+
const std::string& kernel_name) const {
209+
uint64_t sum = 0;
210+
uint32_t n = 0;
211+
for (const auto& d : durations_) {
212+
if (d.kernel_name == kernel_name) {
213+
sum += d.execution_duration_ns;
214+
n += 1;
215+
}
216+
}
217+
return n == 0 ? 0 : sum / n;
218+
}
219+
220+
} // namespace executorch::backends::webgpu

0 commit comments

Comments
 (0)