Skip to content

Commit fc7b239

Browse files
committed
[ExecuTorch][WebGPU] Tests for the reduce WebGPU ops
Pull Request resolved: #20924 Export-delegation + fp64 golden tests for the reduce (`sum`/`mean`) ops. Key changes: - `test/ops/test_reduce.py` — fp64 goldens (dim variants) + delegation checks — fp64 library-reference goldens + delegation checks (≥2 cases/op). Co-authored-with: Claude Code. ghstack-source-id: 405026059 @exported-using-ghexport Differential Revision: [D111755115](https://our.internmc.facebook.com/intern/diff/D111755115/)
1 parent 9f47d80 commit fc7b239

1 file changed

Lines changed: 127 additions & 0 deletions

File tree

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
# Copyright (c) Meta Platforms, Inc. and affiliates.
2+
# All rights reserved.
3+
#
4+
# This source code is licensed under the BSD-style license found in the
5+
# LICENSE file in the root directory of this source tree.
6+
7+
"""`aten.sum.dim_IntList` / `aten.mean.dim` single-dim reduction export + fp64 golden.
8+
9+
Exports single-op sum/mean graphs through VulkanPartitioner and checks the kernel
10+
math against an fp64 torch reference. The handler reduces one dim at a time via an
11+
outer/r/inner decomposition: `dim=-1` gives inner=1 (unit-stride reduction), a
12+
middle dim gives inner>1 (the non-unit-stride path); `keepdim` toggles whether the
13+
reduced dim survives in the output shape.
14+
"""
15+
16+
from __future__ import annotations
17+
18+
import unittest
19+
20+
import torch
21+
22+
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
23+
from executorch.exir import to_edge_transform_and_lower
24+
25+
26+
class ReduceModule(torch.nn.Module):
27+
def __init__(self, op: str, dim: int, keepdim: bool) -> None:
28+
super().__init__()
29+
self.op = op
30+
self.dim = dim
31+
self.keepdim = keepdim
32+
33+
def forward(self, x: torch.Tensor) -> torch.Tensor:
34+
if self.op == "sum":
35+
return torch.sum(x, dim=self.dim, keepdim=self.keepdim)
36+
return torch.mean(x, dim=self.dim, keepdim=self.keepdim)
37+
38+
39+
# (name, shape, dim, keepdim): dim=-1 -> inner=1; middle dim -> inner>1.
40+
CONFIGS = [
41+
("last_dim_keep", (4, 8), -1, True),
42+
("last_dim_drop", (4, 8), -1, False),
43+
("middle_dim_drop", (2, 3, 4), 1, False), # inner=4: non-unit-stride reduction
44+
("middle_dim_keep", (2, 3, 4), 1, True),
45+
]
46+
47+
48+
def _det_input(shape) -> torch.Tensor:
49+
"""Deterministic fp32 [shape]; the C++ side reconstructs it bit-for-bit.
50+
51+
v[flat] = ((flat % 17) - 8) / 16 -- exact in fp32 (small modulus, po2 denominator).
52+
"""
53+
n = 1
54+
for s in shape:
55+
n *= s
56+
flat = torch.arange(n, dtype=torch.float32)
57+
return ((flat % 17) - 8).div(16.0).reshape(shape)
58+
59+
60+
def _export(m: torch.nn.Module, x: torch.Tensor):
61+
ep = torch.export.export(m, (x,))
62+
return to_edge_transform_and_lower(
63+
ep, partitioner=[VulkanPartitioner()]
64+
).to_executorch()
65+
66+
67+
def _delegates(et) -> bool:
68+
return any(
69+
d.id == "VulkanBackend"
70+
for plan in et.executorch_program.execution_plan
71+
for d in plan.delegates
72+
)
73+
74+
75+
def _fp64_golden(x: torch.Tensor, op: str, dim: int, keepdim: bool) -> torch.Tensor:
76+
xd = x.double()
77+
if op == "sum":
78+
ref = torch.sum(xd, dim=dim, keepdim=keepdim)
79+
else:
80+
ref = torch.mean(xd, dim=dim, keepdim=keepdim)
81+
return ref.to(torch.float32)
82+
83+
84+
class TestReduce(unittest.TestCase):
85+
def test_export_delegates(self) -> None:
86+
for op in ("sum", "mean"):
87+
for name, shape, dim, keepdim in CONFIGS:
88+
with self.subTest(op=op, config=name):
89+
x = _det_input(shape)
90+
et = _export(ReduceModule(op, dim, keepdim).eval(), x)
91+
self.assertTrue(
92+
_delegates(et),
93+
f"Expected a VulkanBackend delegate ({op} {name})",
94+
)
95+
96+
def test_matches_fp64_golden(self) -> None:
97+
for op in ("sum", "mean"):
98+
for name, shape, dim, keepdim in CONFIGS:
99+
with self.subTest(op=op, config=name):
100+
x = _det_input(shape)
101+
got = ReduceModule(op, dim, keepdim)(x)
102+
golden = _fp64_golden(x, op, dim, keepdim)
103+
torch.testing.assert_close(got, golden, atol=5e-4, rtol=1e-3)
104+
105+
106+
def export_reduce_model(
107+
op: str,
108+
shape,
109+
dim: int,
110+
keepdim: bool,
111+
pte_path: str,
112+
golden_path: str,
113+
input_path: str,
114+
) -> None:
115+
"""Write a reduce .pte + torch fp64 golden (raw LE fp32) + raw LE fp32 input."""
116+
m = ReduceModule(op, dim, keepdim).eval()
117+
x = _det_input(shape)
118+
et = _export(m, x)
119+
with open(pte_path, "wb") as f:
120+
f.write(et.buffer)
121+
_fp64_golden(x, op, dim, keepdim).numpy().astype("<f4").tofile(golden_path)
122+
x.numpy().astype("<f4").tofile(input_path)
123+
print(f"Exported {pte_path}; golden {golden_path}; input {input_path}")
124+
125+
126+
if __name__ == "__main__":
127+
unittest.main()

0 commit comments

Comments
 (0)