Skip to content

Commit 8d68f96

Browse files
committed
[ExecuTorch][WebGPU] 2D compute dispatch tests — prefill golden + fold unit test
Pull Request resolved: #20584 **Test coverage for the 2D dispatch fold, stacked above the cap-lift op.** **Problem**: The 2D fold is load-bearing index math — a wrong `{x, y}` means out-of-bounds writes or dropped threads — and the prefill shapes that exercise it previously threw at the 1D cap, so they were untested. **Solution**: A device-free unit test for the fold arithmetic, plus two single-shot prefill SDPA golden configs that fold each kernel family. - **Before**: no coverage for >65535-workgroup dispatch; `llama1b_prefill_512`/`_2048` shapes threw at the cap - **After**: `fold_workgroup_count_2d` unit-tested at the cap boundaries, and the two prefill shapes run as goldens **Implementation**: - `test/native/test_dispatch_2d.cpp` — device-free unit test for `utils::fold_workgroup_count_2d`: the 1D fast path, the 2D fold, the real Llama-1B QK counts at S=512 (`{65535, 3}`) and S=2048 (`{65535, 33}`), and the needs-3rd-dimension throw; asserts each `{x, y}` covers `[0, count)` - `llama1b_prefill_512` + `llama1b_prefill_2048` configs appended to the byte-mirrored `CONFIGS` (`test_sdpa.py`) and `kSdpaConfigs` (`test_webgpu_native.cpp`) - Registers `webgpu_dispatch_2d_test` in CMake + the native CI script **Constraints**: - The Python/C++ config entries byte-mirror each other (kept in sync) - `add` shares the element-form path with QK, so it is covered structurally; a dedicated >16M-element `add` fold case is omitted as disproportionate Co-authored-with: Claude Code. ghstack-source-id: 398258612 @exported-using-ghexport Differential Revision: [D109517683](https://our.internmc.facebook.com/intern/diff/D109517683/)
1 parent 332c50f commit 8d68f96

5 files changed

Lines changed: 109 additions & 1 deletion

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
164164
add_webgpu_native_test(
165165
webgpu_dynamic_shape_test test/native/test_dynamic_shape.cpp
166166
)
167+
add_webgpu_native_test(
168+
webgpu_dispatch_2d_test test/native/test_dispatch_2d.cpp
169+
)
167170

168171
# Manifest-driven op-test framework: a generic gtest driver (webgpu_op_test) +
169172
# its device-free util unit test. GTest needs -DEXECUTORCH_BUILD_TESTS=ON.

backends/webgpu/scripts/test_webgpu_native_ci.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ cmake \
143143
"${EXECUTORCH_ROOT}"
144144

145145
# ── Build + run every native test target that exists in this tree ────────────
146-
TARGETS=(webgpu_native_test webgpu_dispatch_order_test webgpu_scratch_buffer_test webgpu_update_cache_test webgpu_index_test)
146+
TARGETS=(webgpu_native_test webgpu_dispatch_order_test webgpu_scratch_buffer_test webgpu_update_cache_test webgpu_index_test webgpu_dispatch_2d_test)
147147
BIN_DIR="${BUILD_DIR}/backends/webgpu"
148148

149149
# Which targets are defined depends on which diffs are landed (native_test +
@@ -212,6 +212,8 @@ if [[ "${INDEX_OK}" == "1" && -x "${BIN_DIR}/webgpu_index_test" ]]; then
212212
"${BIN_DIR}/webgpu_index_test" "${INDEX_DIR}"
213213
fi
214214
[[ -x "${BIN_DIR}/webgpu_scratch_buffer_test" ]] && "${BIN_DIR}/webgpu_scratch_buffer_test"
215+
# Device-free: pure 2D workgroup-count fold unit test (no .pte, no GPU).
216+
[[ -x "${BIN_DIR}/webgpu_dispatch_2d_test" ]] && "${BIN_DIR}/webgpu_dispatch_2d_test"
215217

216218
echo "=== WebGPU native tests on Dawn: all run targets passed ==="
217219

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
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/WebGPUUtils.h>
10+
11+
#include <cstdint>
12+
#include <cstdio>
13+
#include <stdexcept>
14+
15+
using executorch::backends::webgpu::utils::fold_workgroup_count_2d;
16+
using executorch::backends::webgpu::utils::WgCount;
17+
18+
namespace {
19+
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;
23+
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++;
43+
}
44+
}
45+
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++;
60+
}
61+
}
62+
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;
88+
}

backends/webgpu/test/ops/test_sdpa.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,9 @@ class SdpaConfig:
6161
SdpaConfig("llama1b_decode", 32, 8, 64, 1, 512, 127),
6262
# D=6 is not a multiple of 4: the WebGPU head_dim%4 guard must reject it at load.
6363
SdpaConfig("reject_d6", 4, 4, 6, 4, 16, 0),
64+
# 2D-dispatch cap (>65535 wg): S=512 folds QK; S=2048 folds QK+softmax+AV (cap+1).
65+
SdpaConfig("llama1b_prefill_512", 32, 8, 64, 512, 512, 0),
66+
SdpaConfig("llama1b_prefill_2048", 32, 8, 64, 2048, 2048, 0),
6467
]
6568

6669

backends/webgpu/test/test_webgpu_native.cpp

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -750,6 +750,18 @@ static const SdpaConfig kSdpaConfigs[] = {
750750
16.0f,
751751
/*required=*/false,
752752
/*expect_reject=*/true},
753+
// 2D-dispatch cap (>65535 wg): S=512 folds QK; S=2048 folds QK+softmax+AV
754+
// (cap+1).
755+
{"llama1b_prefill_512", 32, 8, 64, 512, 512, 0, 16.0f, /*required=*/true},
756+
{"llama1b_prefill_2048",
757+
32,
758+
8,
759+
64,
760+
2048,
761+
2048,
762+
0,
763+
16.0f,
764+
/*required=*/true},
753765
};
754766

755767
// Ramp denominator; mirror of test_sdpa.py::_RAMP_DENOM (keep in sync).

0 commit comments

Comments
 (0)