Skip to content

Commit fda8deb

Browse files
committed
Update
[ghstack-poisoned]
2 parents d29e08a + b1af20c commit fda8deb

4 files changed

Lines changed: 268 additions & 410 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -161,12 +161,6 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
161161
add_webgpu_native_test(
162162
webgpu_update_cache_test test/native/test_update_cache.cpp
163163
)
164-
add_webgpu_native_test(
165-
webgpu_dynamic_shape_test test/native/test_dynamic_shape.cpp
166-
)
167-
add_webgpu_native_test(
168-
webgpu_dispatch_2d_test test/native/test_dispatch_2d.cpp
169-
)
170164

171165
# Manifest-driven op-test framework: a generic gtest driver (webgpu_op_test) +
172166
# its device-free util unit test. GTest needs -DEXECUTORCH_BUILD_TESTS=ON.
@@ -200,6 +194,21 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
200194
)
201195
target_compile_options(webgpu_op_test_util_test PRIVATE -fexceptions)
202196
set_property(TARGET webgpu_op_test_util_test PROPERTY CXX_STANDARD 17)
197+
198+
# Dynamic-shape integration test: a gtest binary with its own main() that
199+
# brings up the device once (like webgpu_op_test).
200+
add_webgpu_native_test(
201+
webgpu_dynamic_shape_test test/native/test_dynamic_shape.cpp
202+
)
203+
target_link_libraries(webgpu_dynamic_shape_test PRIVATE GTest::gtest)
204+
205+
# Device-free fold unit test (gtest_main provides main; no device needed).
206+
add_webgpu_native_test(
207+
webgpu_dispatch_2d_test test/native/test_dispatch_2d.cpp
208+
)
209+
target_link_libraries(
210+
webgpu_dispatch_2d_test PRIVATE GTest::gtest GTest::gtest_main
211+
)
203212
endif()
204213
add_webgpu_native_test(webgpu_index_test test/native/test_index.cpp)
205214
endif()

backends/webgpu/runtime/WebGPUUtils.h

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
#include <webgpu/webgpu.h>
1212

1313
#include <algorithm>
14+
#include <cmath>
1415
#include <cstdint>
1516
#include <cstring>
1617
#include <stdexcept>
@@ -63,23 +64,31 @@ inline uint32_t queried_max_workgroups(WGPUDevice device) {
6364
}
6465

6566
// Pure 2D fold of a 1D workgroup count (device-free, unit-testable): {count,1}
66-
// when count <= max, else {max, div_up(count, max)} so a >max workload fits the
67-
// per-dimension cap; throws if a 3rd dimension would be needed (out of scope).
68-
// The shader reconstructs the linear index from @builtin(num_workgroups).
67+
// when count <= max, else a near-square {x, y} with x ~ ceil(sqrt(count)) so
68+
// the launched grid stays close to count. A flat {max, div_up(count, max)}
69+
// split would leave up to ~half the workgroups inactive when count just exceeds
70+
// max, and inactive workgroups still cost launch/scheduling; the near-square
71+
// split keeps the waste to O(sqrt(count)). Throws if even a max*max grid is too
72+
// small (a 3rd dispatch dimension, out of scope). The shader reconstructs the
73+
// linear index from @builtin(num_workgroups), so any x/y factoring works.
6974
inline WgCount fold_workgroup_count_2d(
7075
uint32_t count,
7176
uint32_t max_count,
7277
const char* op_name) {
7378
if (count <= max_count) {
7479
return {count, 1u};
7580
}
76-
uint32_t y = (count + max_count - 1) / max_count;
81+
uint32_t x =
82+
static_cast<uint32_t>(std::ceil(std::sqrt(static_cast<double>(count))));
83+
x = std::min(x, max_count);
84+
// ceil-div written overflow-safe (count >= 1 here) as count nears UINT32_MAX.
85+
uint32_t y = 1u + (count - 1u) / x;
7786
if (y > max_count) {
7887
throw std::runtime_error(
7988
std::string("WebGPU ") + op_name +
8089
": workgroup count needs a 3rd dispatch dimension (unsupported)");
8190
}
82-
return {max_count, y};
91+
return {x, y};
8392
}
8493

8594
// 1D dispatch count (mirrors Vulkan div_up); throws if > device limit.
@@ -104,7 +113,7 @@ inline WgCount compute_2d_workgroup_count(
104113
uint32_t num_threads,
105114
uint32_t workgroup_size,
106115
const char* op_name) {
107-
uint32_t count = (num_threads + workgroup_size - 1) / workgroup_size;
116+
uint32_t count = div_up(num_threads, workgroup_size);
108117
return fold_workgroup_count_2d(
109118
count, queried_max_workgroups(device), op_name);
110119
}

backends/webgpu/test/native/test_dispatch_2d.cpp

Lines changed: 35 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -6,83 +6,55 @@
66
* LICENSE file in the root directory of this source tree.
77
*/
88

9+
// Device-free unit test for the pure 2D workgroup-count fold that lifts the
10+
// 65535 per-dim dispatch cap. Exercises the fold arithmetic only — no GPU.
11+
912
#include <executorch/backends/webgpu/runtime/WebGPUUtils.h>
1013

14+
#include <gtest/gtest.h>
15+
16+
#include <cmath>
1117
#include <cstdint>
12-
#include <cstdio>
13-
#include <stdexcept>
1418

1519
using executorch::backends::webgpu::utils::fold_workgroup_count_2d;
1620
using executorch::backends::webgpu::utils::WgCount;
1721

1822
namespace {
1923

20-
// Device-free unit test for the pure 2D workgroup-count fold that lifts the
21-
// 65535 per-dim dispatch cap. Exercises the fold arithmetic only — no GPU.
22-
int g_failures = 0;
24+
constexpr uint32_t kMax = 65535u;
2325

24-
void expect_fold(
25-
uint32_t count,
26-
uint32_t max_count,
27-
uint32_t want_x,
28-
uint32_t want_y) {
29-
WgCount got = fold_workgroup_count_2d(count, max_count, "test");
30-
bool ok = got.x == want_x && got.y == want_y &&
31-
static_cast<uint64_t>(got.x) * got.y >= count;
32-
printf(
33-
"%s fold(%u, max=%u) = {%u, %u} (want {%u, %u})\n",
34-
ok ? "PASS:" : "FAIL:",
35-
count,
36-
max_count,
37-
got.x,
38-
got.y,
39-
want_x,
40-
want_y);
41-
if (!ok) {
42-
g_failures++;
26+
// count <= max -> {count, 1}: the 1D fast path, byte-identical to the old path.
27+
TEST(DispatchFold, FastPath1D) {
28+
for (uint32_t count : {1u, kMax - 1u, kMax}) {
29+
const WgCount got = fold_workgroup_count_2d(count, kMax, "test");
30+
EXPECT_EQ(got.x, count);
31+
EXPECT_EQ(got.y, 1u);
4332
}
4433
}
4534

46-
void expect_throw(uint32_t count, uint32_t max_count) {
47-
bool threw = false;
48-
try {
49-
fold_workgroup_count_2d(count, max_count, "test");
50-
} catch (const std::exception&) {
51-
threw = true;
52-
}
53-
printf(
54-
"%s fold(%u, max=%u) throws (needs a 3rd dispatch dimension)\n",
55-
threw ? "PASS:" : "FAIL:",
56-
count,
57-
max_count);
58-
if (!threw) {
59-
g_failures++;
35+
// count > max -> near-square {x, y}: fits the per-dim cap, covers every
36+
// workgroup, and stays near-square so few invocations are inactive (launched -
37+
// count is O(sqrt(count)); a flat {max, div_up} split would idle up to ~half).
38+
TEST(DispatchFold, NearSquareFold) {
39+
// Includes prefill-scale QK counts (Hq*ceil(S/4)*ceil(ctx/4)/wg) that fold:
40+
// 131072 = S=2048 (32*512*512/64); 2097152 = large-S stress.
41+
for (uint32_t count :
42+
{kMax + 1u, 2u * kMax, 2u * kMax + 1u, 131072u, 2097152u}) {
43+
const WgCount got = fold_workgroup_count_2d(count, kMax, "test");
44+
const uint64_t launched = static_cast<uint64_t>(got.x) * got.y;
45+
const uint32_t root =
46+
static_cast<uint32_t>(std::ceil(std::sqrt(static_cast<double>(count))));
47+
EXPECT_LE(got.x, kMax) << "count=" << count;
48+
EXPECT_LE(got.y, kMax) << "count=" << count;
49+
EXPECT_GE(launched, count) << "count=" << count;
50+
EXPECT_LT(launched - count, 2ull * root)
51+
<< "count=" << count << " launched=" << launched;
6052
}
6153
}
6254

63-
} // namespace
64-
65-
int main() {
66-
const uint32_t kMax = 65535u;
67-
// 1D fast path: count <= max -> {count, 1}, byte-identical to the old path.
68-
expect_fold(1u, kMax, 1u, 1u);
69-
expect_fold(kMax - 1u, kMax, kMax - 1u, 1u);
70-
expect_fold(kMax, kMax, kMax, 1u);
71-
// Fold to 2D: count > max -> {max, div_up(count, max)}.
72-
expect_fold(kMax + 1u, kMax, kMax, 2u);
73-
expect_fold(2u * kMax, kMax, kMax, 2u);
74-
expect_fold(2u * kMax + 1u, kMax, kMax, 3u);
75-
// Prefill-scale QK counts (tiled grid = Hq*ceil(S/4)*ceil(ctx/4)/wg) that
76-
// exceed kMax and must fold.
77-
expect_fold(131072u, kMax, kMax, 3u); // S=2048: 32*512*512/64
78-
expect_fold(2097152u, kMax, kMax, 33u); // deep fold (large-S stress)
79-
// count > max^2 needs a 3rd dispatch dimension -> throws (out of scope).
80-
expect_throw(kMax * kMax + 1u, kMax);
81-
82-
if (g_failures != 0) {
83-
printf("\nFAIL: %d dispatch_2d fold case(s) failed\n", g_failures);
84-
return 1;
85-
}
86-
printf("\nAll dispatch_2d fold tests passed\n");
87-
return 0;
55+
// count > max^2 needs a 3rd dispatch dimension -> throws (out of scope).
56+
TEST(DispatchFold, ThrowsWhenNeeds3rdDimension) {
57+
EXPECT_ANY_THROW(fold_workgroup_count_2d(kMax * kMax + 1u, kMax, "test"));
8858
}
59+
60+
} // namespace

0 commit comments

Comments
 (0)