Skip to content

Commit fb104cc

Browse files
pytorchbotJCNTH
andauthored
[ExecuTorch][WebGPU] Fix per-op GPU-timestamp inflation on tile-based mobile GPUs (#21043)
This PR was created by the merge bot to help merge the original PR into the main branch. ghstack PR number: #21033 by @JCNTH ^ Please use this as the source of truth for the PR details, comments, and reviews ghstack PR base: https://github.com/pytorch/executorch/tree/gh/JCNTH/102/base ghstack PR head: https://github.com/pytorch/executorch/tree/gh/JCNTH/102/head Merge bot PR base: https://github.com/pytorch/executorch/tree/main Merge bot PR head: https://github.com/pytorch/executorch/tree/gh/JCNTH/102/orig @diff-train-skip-merge Co-authored-by: Julian Ng-Thow-Hing <juliannth@meta.com>
1 parent e8c47d7 commit fb104cc

3 files changed

Lines changed: 112 additions & 8 deletions

File tree

backends/webgpu/runtime/WebGPUQueryPool.cpp

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#include <executorch/backends/webgpu/runtime/WebGPUCompat.h>
1010
#include <executorch/backends/webgpu/runtime/WebGPUQueryPool.h>
1111

12+
#include <algorithm>
1213
#include <cstdio>
1314
#include <map>
1415
#include <stdexcept>
@@ -125,6 +126,34 @@ void WebGPUQueryPool::resolve(WGPUCommandEncoder encoder) {
125126
static_cast<uint64_t>(count) * kTimestampBytes);
126127
}
127128

129+
// Mali can pin begin-of-pass timestamps. Preserve raw start/end timestamps but
130+
// derive durations from consecutive ends. State is local to this extraction;
131+
// the first dispatch can still include work before its recorded pass.
132+
void fill_shader_durations(
133+
std::vector<ShaderDuration>& durations,
134+
const uint64_t* ticks,
135+
double ns_per_tick) {
136+
std::sort(
137+
durations.begin(),
138+
durations.end(),
139+
[](const ShaderDuration& a, const ShaderDuration& b) {
140+
return a.idx < b.idx;
141+
});
142+
uint64_t prev_end = 0;
143+
bool have_prev = false;
144+
for (auto& d : durations) {
145+
const uint64_t t0 = ticks[2 * d.idx];
146+
const uint64_t t1 = ticks[2 * d.idx + 1];
147+
const uint64_t base = have_prev ? std::max(t0, prev_end) : t0;
148+
d.start_time_ns = static_cast<uint64_t>(t0 * ns_per_tick);
149+
d.end_time_ns = static_cast<uint64_t>(t1 * ns_per_tick);
150+
d.execution_duration_ns =
151+
(t1 > base) ? static_cast<uint64_t>((t1 - base) * ns_per_tick) : 0;
152+
prev_end = have_prev ? std::max(prev_end, t1) : t1;
153+
have_prev = true;
154+
}
155+
}
156+
128157
void WebGPUQueryPool::extract_results(WGPUInstance instance) {
129158
if (num_pairs_ == 0) {
130159
return;
@@ -149,14 +178,7 @@ void WebGPUQueryPool::extract_results(WGPUInstance instance) {
149178
const uint64_t* ticks = static_cast<const uint64_t*>(
150179
wgpuBufferGetConstMappedRange(readback_buf_, 0, bytes));
151180
if (ticks != nullptr) {
152-
for (auto& d : durations_) {
153-
const uint64_t t0 = ticks[2 * d.idx];
154-
const uint64_t t1 = ticks[2 * d.idx + 1];
155-
d.start_time_ns = static_cast<uint64_t>(t0 * ns_per_tick_);
156-
d.end_time_ns = static_cast<uint64_t>(t1 * ns_per_tick_);
157-
d.execution_duration_ns =
158-
(t1 >= t0) ? static_cast<uint64_t>((t1 - t0) * ns_per_tick_) : 0;
159-
}
181+
fill_shader_durations(durations_, ticks, ns_per_tick_);
160182
}
161183
wgpuBufferUnmap(readback_buf_);
162184
}

backends/webgpu/runtime/WebGPUQueryPool.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,12 @@ class WebGPUQueryPool {
8383
std::vector<ShaderDuration> durations_;
8484
};
8585

86+
// Per-op durations from begin/end tick pairs (consecutive-end delta).
87+
void fill_shader_durations(
88+
std::vector<ShaderDuration>& durations,
89+
const uint64_t* ticks,
90+
double ns_per_tick);
91+
8692
#endif // WGPU_BACKEND_ENABLE_PROFILING
8793

8894
} // namespace executorch::backends::webgpu

backends/webgpu/test/test_webgpu_native.cpp

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,78 @@ void test_query_pool_roundtrip(const WebGPUContext& ctx) {
153153
printf(" probe duration: %llu ns\n", (unsigned long long)dur);
154154
EXPECT_NE(dur, 0u) << "probe duration is zero (expected monotonic non-zero)";
155155
}
156+
157+
// Device-free: tick->duration delta math (the Mali begin-pinning fix).
158+
void test_query_pool_delta_math() {
159+
auto durs = [](std::vector<uint32_t> idxs) {
160+
std::vector<ShaderDuration> v;
161+
for (uint32_t i : idxs) {
162+
ShaderDuration d;
163+
d.idx = i;
164+
v.push_back(d);
165+
}
166+
return v;
167+
};
168+
// Well-behaved backend (begin >= prev end): per-op == end - begin, unchanged.
169+
{
170+
const uint64_t ticks[] = {10, 20, 20, 35, 40, 50};
171+
auto d = durs({0, 1, 2});
172+
fill_shader_durations(d, ticks, 1.0);
173+
EXPECT_EQ(d[0].execution_duration_ns, 10u);
174+
EXPECT_EQ(d[1].execution_duration_ns, 15u);
175+
EXPECT_EQ(d[2].execution_duration_ns, 10u);
176+
}
177+
// Tile GPU: begin pinned, ends cumulative -> recover per-op; sum == wall.
178+
{
179+
const uint64_t ticks[] = {0, 20, 0, 50, 0, 90};
180+
auto d = durs({0, 1, 2});
181+
fill_shader_durations(d, ticks, 1.0);
182+
EXPECT_EQ(d[0].execution_duration_ns, 20u);
183+
EXPECT_EQ(d[1].execution_duration_ns, 30u);
184+
EXPECT_EQ(d[2].execution_duration_ns, 40u);
185+
const uint64_t sum = d[0].execution_duration_ns +
186+
d[1].execution_duration_ns + d[2].execution_duration_ns;
187+
EXPECT_EQ(sum, 90u);
188+
}
189+
// Recorded out of order: the idx-sort keeps the delta correct.
190+
{
191+
const uint64_t ticks[] = {0, 20, 0, 50, 0, 90};
192+
auto d = durs({2, 0, 1});
193+
fill_shader_durations(d, ticks, 1.0);
194+
for (const auto& x : d) {
195+
const uint64_t exp = x.idx == 0 ? 20u : (x.idx == 1 ? 30u : 40u);
196+
EXPECT_EQ(x.execution_duration_ns, exp) << "idx " << x.idx;
197+
}
198+
}
199+
// Non-monotone end (op1 end < prev end): running-max base + zero-clamp.
200+
{
201+
const uint64_t ticks[] = {0, 100, 0, 40, 0, 120};
202+
auto d = durs({0, 1, 2});
203+
fill_shader_durations(d, ticks, 1.0);
204+
EXPECT_EQ(d[0].start_time_ns, 0u);
205+
EXPECT_EQ(d[0].end_time_ns, 100u);
206+
EXPECT_EQ(d[0].execution_duration_ns, 100u);
207+
EXPECT_EQ(d[1].start_time_ns, 0u);
208+
EXPECT_EQ(d[1].end_time_ns, 40u);
209+
EXPECT_EQ(d[1].execution_duration_ns, 0u);
210+
EXPECT_EQ(d[2].start_time_ns, 0u);
211+
EXPECT_EQ(d[2].end_time_ns, 120u);
212+
EXPECT_EQ(d[2].execution_duration_ns, 20u);
213+
}
214+
// Each extraction is independent; no previous-end state leaks across calls.
215+
{
216+
const uint64_t first_ticks[] = {0, 100, 0, 140};
217+
auto first = durs({0, 1});
218+
fill_shader_durations(first, first_ticks, 1.0);
219+
EXPECT_EQ(first[1].execution_duration_ns, 40u);
220+
221+
const uint64_t second_ticks[] = {10, 30, 30, 60};
222+
auto second = durs({0, 1});
223+
fill_shader_durations(second, second_ticks, 1.0);
224+
EXPECT_EQ(second[0].execution_duration_ns, 20u);
225+
EXPECT_EQ(second[1].execution_duration_ns, 30u);
226+
}
227+
}
156228
#endif // WGPU_BACKEND_ENABLE_PROFILING
157229

158230
void test_update_cache(const std::string& model_path) {
@@ -1594,6 +1666,10 @@ TEST(WebGPUNative, QueryPoolOverrunThrows) {
15941666
TEST(WebGPUNative, QueryPoolRoundtrip) {
15951667
test_query_pool_roundtrip(*get_default_webgpu_context());
15961668
}
1669+
1670+
TEST(WebGPUNative, QueryPoolDeltaMath) {
1671+
test_query_pool_delta_math();
1672+
}
15971673
#endif // WGPU_BACKEND_ENABLE_PROFILING
15981674

15991675
// The override wg_size must not change results: run the rms_norm scalar kernel

0 commit comments

Comments
 (0)