Skip to content

Commit ccd20bd

Browse files
committed
[ExecuTorch][WebGPU] Convert remaining native tests to GTest
Pull Request resolved: #20706 Convert the remaining hand-rolled `int main()` + printf/`bool ok` native tests to GTest so the whole `backends/webgpu/test/` suite is uniform, filterable via `--gtest_filter`, and self-reporting (extends the GTest conversion already applied to `test_dynamic_shape`). The five converted files are a harness-only change — every test case, tensor shape, tolerance, artifact filename, and skip condition is preserved 1:1, only the pass/fail reporting mechanism changes — and this diff additionally wires the already-GTest `webgpu_dynamic_shape_test` into the CI runner so the dynamic-shape suite actually executes. Key changes: - `test/test_webgpu_native.cpp`, `test/native/test_dispatch_order.cpp`, `test/native/test_index.cpp`, `test/native/test_scratch_buffer.cpp`, `test/native/test_update_cache.cpp` — `main`+`printf`/`bool ok` accumulator → `TEST()` cases using `EXPECT_*`/`ASSERT_*`; each keeps a custom `main()` that brings up the WebGPU device once then `RUN_ALL_TESTS()` (device-absent still SKIPs by returning 0). `test_index`/`test_webgpu_native` use inclusive `EXPECT_LE(err, tol)` to match the original `err > tol` fail gate exactly. - `CMakeLists.txt` — move every native-test target into the `if(TARGET GTest::gtest)` block, linking `GTest::gtest`. - `scripts/test_webgpu_native_ci.sh` — add `-DEXECUTORCH_BUILD_TESTS=ON` to the native-test configure so the now-gtest-gated targets are defined, and wire `webgpu_dynamic_shape_test` into the runner: export its `.pte`s + goldens via `export_dynamic_shape_cases`, add it to the built/run target list behind the same `--target help` probe, and run it guarded (mirroring the `index` test). - `test/test_build_webgpu.sh` — add `-DEXECUTORCH_BUILD_TESTS=ON` so the local build script (which builds the now-gtest-gated targets unconditionally) still finds them. ghstack-source-id: 399812941 @exported-using-ghexport Differential Revision: [D110536636](https://our.internmc.facebook.com/intern/diff/D110536636/)
1 parent 1fcfc97 commit ccd20bd

8 files changed

Lines changed: 825 additions & 1099 deletions

File tree

backends/webgpu/CMakeLists.txt

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -151,19 +151,9 @@ function(add_webgpu_native_test test_name test_src)
151151
endfunction()
152152

153153
if(EXECUTORCH_BUILD_WEBGPU_TEST)
154-
add_webgpu_native_test(webgpu_native_test test/test_webgpu_native.cpp)
155-
add_webgpu_native_test(
156-
webgpu_dispatch_order_test test/native/test_dispatch_order.cpp
157-
)
158-
add_webgpu_native_test(
159-
webgpu_scratch_buffer_test test/native/test_scratch_buffer.cpp
160-
)
161-
add_webgpu_native_test(
162-
webgpu_update_cache_test test/native/test_update_cache.cpp
163-
)
164-
165-
# Manifest-driven op-test framework: a generic gtest driver (webgpu_op_test) +
166-
# its device-free util unit test. GTest needs -DEXECUTORCH_BUILD_TESTS=ON.
154+
# All WebGPU native tests use GTest (device-dependent ones bring up the device
155+
# in their own main(); the fold unit test is device-free via gtest_main).
156+
# GTest needs -DEXECUTORCH_BUILD_TESTS=ON.
167157
if(NOT TARGET GTest::gtest)
168158
find_package(GTest QUIET)
169159
endif()
@@ -195,12 +185,28 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
195185
target_compile_options(webgpu_op_test_util_test PRIVATE -fexceptions)
196186
set_property(TARGET webgpu_op_test_util_test PROPERTY CXX_STANDARD 17)
197187

198-
# Dynamic-shape integration test: a gtest binary with its own main() that
199-
# brings up the device once (like webgpu_op_test).
188+
# Device-dependent native tests: each has its own main() that brings up the
189+
# device once, then RUN_ALL_TESTS(); link GTest::gtest (not gtest_main).
190+
add_webgpu_native_test(webgpu_native_test test/test_webgpu_native.cpp)
191+
target_link_libraries(webgpu_native_test PRIVATE GTest::gtest)
192+
add_webgpu_native_test(
193+
webgpu_dispatch_order_test test/native/test_dispatch_order.cpp
194+
)
195+
target_link_libraries(webgpu_dispatch_order_test PRIVATE GTest::gtest)
196+
add_webgpu_native_test(
197+
webgpu_scratch_buffer_test test/native/test_scratch_buffer.cpp
198+
)
199+
target_link_libraries(webgpu_scratch_buffer_test PRIVATE GTest::gtest)
200+
add_webgpu_native_test(
201+
webgpu_update_cache_test test/native/test_update_cache.cpp
202+
)
203+
target_link_libraries(webgpu_update_cache_test PRIVATE GTest::gtest)
200204
add_webgpu_native_test(
201205
webgpu_dynamic_shape_test test/native/test_dynamic_shape.cpp
202206
)
203207
target_link_libraries(webgpu_dynamic_shape_test PRIVATE GTest::gtest)
208+
add_webgpu_native_test(webgpu_index_test test/native/test_index.cpp)
209+
target_link_libraries(webgpu_index_test PRIVATE GTest::gtest)
204210

205211
# Device-free fold unit test (gtest_main provides main; no device needed).
206212
add_webgpu_native_test(
@@ -210,5 +216,4 @@ if(EXECUTORCH_BUILD_WEBGPU_TEST)
210216
webgpu_dispatch_2d_test PRIVATE GTest::gtest GTest::gtest_main
211217
)
212218
endif()
213-
add_webgpu_native_test(webgpu_index_test test/native/test_index.cpp)
214219
endif()

backends/webgpu/scripts/test_webgpu_native_ci.sh

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ UPDATE_CACHE_DIR="/tmp/update_cache"
4747
UPDATE_CACHE_OK=1
4848
INDEX_DIR="/tmp/index"
4949
INDEX_OK=1
50+
DYNAMIC_SHAPE_DIR="/tmp/dynamic_shape"
51+
DYNAMIC_SHAPE_OK=1
5052
EMBEDDING_MODEL="/tmp/webgpu_embedding_q4gsw.pte"
5153
EMBEDDING_INDICES="/tmp/webgpu_embedding_q4gsw_indices.bin"
5254
EMBEDDING_GOLDEN="/tmp/webgpu_embedding_q4gsw_golden.bin"
@@ -111,6 +113,11 @@ from executorch.backends.webgpu.test.ops.index.test_index import export_all_inde
111113
export_all_index_models('${INDEX_DIR}')
112114
" || { echo "WARN: index export failed; skipping index native test"; INDEX_OK=0; }
113115

116+
$PYTHON_EXECUTABLE -c "
117+
from executorch.backends.webgpu.test.ops.dynamic_shape.test_dynamic_shape_export import export_dynamic_shape_cases
118+
export_dynamic_shape_cases('${DYNAMIC_SHAPE_DIR}')
119+
" || { echo "WARN: dynamic_shape export failed; skipping dynamic_shape native test"; DYNAMIC_SHAPE_OK=0; }
120+
114121
# Non-fatal: a failed sdpa export makes the required 4k/8k configs hard-fail in
115122
# webgpu_native_test below (precise per-config error), so don't exit/mask here.
116123
$PYTHON_EXECUTABLE -c "
@@ -132,6 +139,7 @@ rm -rf "${BUILD_DIR}"
132139
cmake \
133140
-DEXECUTORCH_BUILD_WEBGPU=ON \
134141
-DEXECUTORCH_BUILD_WEBGPU_TEST=ON \
142+
-DEXECUTORCH_BUILD_TESTS=ON \
135143
-DDawn_DIR="${Dawn_DIR}" \
136144
-DEXECUTORCH_BUILD_EXTENSION_MODULE=ON \
137145
-DEXECUTORCH_BUILD_EXTENSION_DATA_LOADER=ON \
@@ -143,7 +151,7 @@ cmake \
143151
"${EXECUTORCH_ROOT}"
144152

145153
# ── 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 webgpu_dispatch_2d_test)
154+
TARGETS=(webgpu_native_test webgpu_dispatch_order_test webgpu_scratch_buffer_test webgpu_update_cache_test webgpu_index_test webgpu_dynamic_shape_test webgpu_dispatch_2d_test)
147155
BIN_DIR="${BUILD_DIR}/backends/webgpu"
148156

149157
# Which targets are defined depends on which diffs are landed (native_test +
@@ -211,6 +219,9 @@ fi
211219
if [[ "${INDEX_OK}" == "1" && -x "${BIN_DIR}/webgpu_index_test" ]]; then
212220
"${BIN_DIR}/webgpu_index_test" "${INDEX_DIR}"
213221
fi
222+
if [[ "${DYNAMIC_SHAPE_OK}" == "1" && -x "${BIN_DIR}/webgpu_dynamic_shape_test" ]]; then
223+
"${BIN_DIR}/webgpu_dynamic_shape_test" "${DYNAMIC_SHAPE_DIR}"
224+
fi
214225
[[ -x "${BIN_DIR}/webgpu_scratch_buffer_test" ]] && "${BIN_DIR}/webgpu_scratch_buffer_test"
215226
# Device-free: pure 2D workgroup-count fold unit test (no .pte, no GPU).
216227
[[ -x "${BIN_DIR}/webgpu_dispatch_2d_test" ]] && "${BIN_DIR}/webgpu_dispatch_2d_test"

backends/webgpu/test/native/test_dispatch_order.cpp

Lines changed: 74 additions & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,13 @@
1010
#include <executorch/extension/module/module.h>
1111
#include <executorch/extension/tensor/tensor.h>
1212

13+
#include <gtest/gtest.h>
14+
1315
#include <algorithm>
1416
#include <cmath>
1517
#include <cstdio>
1618
#include <cstdlib>
19+
#include <exception>
1720
#include <fstream>
1821
#include <string>
1922
#include <vector>
@@ -24,23 +27,8 @@ using namespace executorch::runtime;
2427

2528
namespace {
2629

27-
struct Case {
28-
const char* name;
29-
std::vector<int32_t> sizes;
30-
};
31-
32-
// Mirrors _CASES in test_dispatch_order.py (add-chain or rms_norm+add chain).
33-
const std::vector<Case> kCases = {
34-
{"single", {16, 16}},
35-
{"chain3", {64, 64}},
36-
{"chain5_tiny", {1, 1}},
37-
{"chain5_wide", {7, 896}},
38-
{"chain8", {256, 256}},
39-
{"deep32", {128, 128}},
40-
{"large_chain", {1024, 1024}},
41-
{"het_small", {1, 1, 7, 896}},
42-
{"het_deep", {1, 1, 5, 256}},
43-
};
30+
// Artifacts directory; set from env/argv in main() before RUN_ALL_TESTS().
31+
std::string g_dir; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
4432

4533
std::vector<float> read_f32_bin(const std::string& path) {
4634
std::ifstream f(path, std::ios::binary | std::ios::ate);
@@ -59,53 +47,35 @@ std::vector<float> read_f32_bin(const std::string& path) {
5947
return data;
6048
}
6149

62-
bool run_case(const std::string& dir, const Case& tc) {
63-
printf("\n--- dispatch_order[%s] ---\n", tc.name);
64-
const std::string base = dir + "/" + tc.name;
50+
// Mirrors _CASES in test_dispatch_order.py (add-chain or rms_norm+add chain).
51+
void run_case(const char* name, const std::vector<int32_t>& sizes) {
52+
const std::string base = g_dir + "/" + name;
6553
std::vector<float> input = read_f32_bin(base + ".input.bin");
6654
std::vector<float> golden = read_f32_bin(base + ".golden.bin");
67-
if (input.empty() || golden.empty()) {
68-
printf("FAIL: could not read input/golden for %s\n", tc.name);
69-
return false;
70-
}
55+
ASSERT_FALSE(input.empty() || golden.empty())
56+
<< "could not read input/golden for " << name;
7157

7258
Module module(base + ".pte");
73-
if (module.load_forward() != Error::Ok) {
74-
printf("FAIL: could not load %s.pte\n", tc.name);
75-
return false;
76-
}
59+
ASSERT_EQ(module.load_forward(), Error::Ok)
60+
<< "could not load " << name << ".pte";
7761

7862
size_t expected = 1;
79-
for (int32_t d : tc.sizes) {
63+
for (int32_t d : sizes) {
8064
expected *= static_cast<size_t>(d);
8165
}
82-
if (input.size() != expected) {
83-
printf(
84-
"FAIL: input numel %zu != expected %zu for %s\n",
85-
input.size(),
86-
expected,
87-
tc.name);
88-
return false;
89-
}
90-
auto x = make_tensor_ptr(tc.sizes, std::vector<float>(input));
66+
ASSERT_EQ(input.size(), expected)
67+
<< "input numel " << input.size() << " != expected " << expected
68+
<< " for " << name;
69+
auto x = make_tensor_ptr(sizes, std::vector<float>(input));
9170
auto result = module.forward({EValue(x)});
92-
if (!result.ok()) {
93-
printf("FAIL: forward failed (error %d)\n", (int)result.error());
94-
return false;
95-
}
71+
ASSERT_TRUE(result.ok()) << "forward failed (error " << (int)result.error()
72+
<< ")";
9673
const auto& outputs = result.get();
97-
if (outputs.empty() || !outputs[0].isTensor()) {
98-
printf("FAIL: no tensor output\n");
99-
return false;
100-
}
74+
ASSERT_TRUE(!outputs.empty() && outputs[0].isTensor()) << "no tensor output";
10175
const auto& out_tensor = outputs[0].toTensor();
102-
if (static_cast<size_t>(out_tensor.numel()) != golden.size()) {
103-
printf(
104-
"FAIL: output numel %zu != golden %zu\n",
105-
(size_t)out_tensor.numel(),
106-
golden.size());
107-
return false;
108-
}
76+
ASSERT_EQ(static_cast<size_t>(out_tensor.numel()), golden.size())
77+
<< "output numel " << (size_t)out_tensor.numel() << " != golden "
78+
<< golden.size();
10979
const float* out_data = out_tensor.const_data_ptr<float>();
11080

11181
float max_abs_err = 0.0f;
@@ -116,52 +86,76 @@ bool run_case(const std::string& dir, const Case& tc) {
11686
const float denom = std::max(std::abs(golden[i]), 1e-6f);
11787
max_rel_err = std::max(max_rel_err, abs_err / denom);
11888
}
119-
printf(
120-
"Max abs error: %e Max rel error: %e (%zu elements)\n",
121-
max_abs_err,
122-
max_rel_err,
123-
golden.size());
12489
// Lenient gate: pass iff abs<=tol OR rel<=tol (near-zero goldens).
125-
if (max_abs_err > 1e-3f && max_rel_err > 1e-3f) {
126-
printf("FAIL: dispatch_order[%s] exceeds tolerance 1e-3\n", tc.name);
127-
return false;
128-
}
129-
printf("PASS: dispatch_order[%s]\n", tc.name);
130-
return true;
90+
EXPECT_FALSE(max_abs_err > 1e-3f && max_rel_err > 1e-3f)
91+
<< "dispatch_order[" << name
92+
<< "] exceeds tolerance 1e-3 (max_abs_err=" << max_abs_err
93+
<< " max_rel_err=" << max_rel_err << ", " << golden.size()
94+
<< " elements)";
13195
}
13296

13397
} // namespace
13498

99+
TEST(DispatchOrder, single) {
100+
run_case("single", {16, 16});
101+
}
102+
103+
TEST(DispatchOrder, chain3) {
104+
run_case("chain3", {64, 64});
105+
}
106+
107+
TEST(DispatchOrder, chain5_tiny) {
108+
run_case("chain5_tiny", {1, 1});
109+
}
110+
111+
TEST(DispatchOrder, chain5_wide) {
112+
run_case("chain5_wide", {7, 896});
113+
}
114+
115+
TEST(DispatchOrder, chain8) {
116+
run_case("chain8", {256, 256});
117+
}
118+
119+
TEST(DispatchOrder, deep32) {
120+
run_case("deep32", {128, 128});
121+
}
122+
123+
TEST(DispatchOrder, large_chain) {
124+
run_case("large_chain", {1024, 1024});
125+
}
126+
127+
TEST(DispatchOrder, het_small) {
128+
run_case("het_small", {1, 1, 7, 896});
129+
}
130+
131+
TEST(DispatchOrder, het_deep) {
132+
run_case("het_deep", {1, 1, 5, 256});
133+
}
134+
135135
int main(int argc, char** argv) {
136-
std::string dir = "/tmp/dispatch_order";
136+
::testing::InitGoogleTest(&argc, argv);
137+
138+
// Artifacts dir: env wins, else first positional arg, else default (gtest
139+
// flags were already stripped by InitGoogleTest above).
140+
g_dir = "/tmp/dispatch_order";
137141
if (argc > 1) {
138-
dir = argv[1];
142+
g_dir = argv[1];
139143
}
140144
if (const char* env = std::getenv("WEBGPU_DISPATCH_ORDER_DIR")) {
141-
dir = env;
145+
g_dir = env;
142146
}
143147

144148
WebGPUContext ctx;
145149
try {
146150
ctx = create_webgpu_context();
147151
} catch (const std::exception& e) {
148-
printf("SKIP: %s\n", e.what());
152+
std::printf("SKIP: %s\n", e.what());
149153
return 0;
150154
}
151155
set_default_webgpu_context(&ctx);
152-
printf("WebGPU device acquired (native); case dir: %s\n", dir.c_str());
153-
154-
bool ok = true;
155-
for (const auto& tc : kCases) {
156-
ok = run_case(dir, tc) && ok;
157-
}
158156

157+
const int rc = RUN_ALL_TESTS();
159158
set_default_webgpu_context(nullptr);
160159
destroy_webgpu_context(ctx);
161-
162-
if (!ok) {
163-
return 1;
164-
}
165-
printf("\nAll dispatch_order tests passed\n");
166-
return 0;
160+
return rc;
167161
}

0 commit comments

Comments
 (0)