Skip to content

Commit c86418c

Browse files
[ExecuTorch][WebGPU] squeeze_copy + unsqueeze_copy test suites (cases.py op-test framework)
Pull Request resolved: #20393 Registers `aten.squeeze_copy.dims` and `aten.unsqueeze_copy.default` in the `cases.py` op-test framework: a `_squeeze_suite` of 3 configs (squeeze leading/middle/multiple size-1 dims) and a `_unsqueeze_suite` of 3 configs (insert dim at front/middle/last) that `generate_op_tests` exports via `VulkanPartitioner` and compares to a torch golden on Dawn. Also adds `test/ops/squeeze/test_squeeze.py` (`SqueezeModule` + `CONFIGS` + `_op_delegated` smoke test), `test/ops/unsqueeze/test_unsqueeze.py` (`UnsqueezeModule` + `CONFIGS` + `_op_delegated` smoke test), and the two partitioner-allowlist entries in `tester.py`. ghstack-source-id: 397026525 @exported-using-ghexport Differential Revision: [D108793152](https://our.internmc.facebook.com/intern/diff/D108793152/)
1 parent 621084c commit c86418c

4 files changed

Lines changed: 188 additions & 0 deletions

File tree

backends/webgpu/test/op_tests/cases.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,16 @@
4949
N as _SIGMOID_N,
5050
SigmoidModule,
5151
)
52+
53+
from executorch.backends.webgpu.test.ops.test_squeeze import (
54+
CONFIGS as _SQUEEZE_CONFIGS,
55+
SqueezeModule,
56+
)
57+
58+
from executorch.backends.webgpu.test.ops.test_unsqueeze import (
59+
CONFIGS as _UNSQUEEZE_CONFIGS,
60+
UnsqueezeModule,
61+
)
5262
from executorch.backends.webgpu.test.ops.test_view_copy import (
5363
CONFIGS as _VIEW_CONFIGS,
5464
ViewModule,
@@ -184,3 +194,29 @@ def _sigmoid_suite() -> WebGPUTestSuite:
184194
atol=1e-4,
185195
rtol=1e-4,
186196
)
197+
198+
199+
@register_op_test("squeeze")
200+
def _squeeze_suite() -> WebGPUTestSuite:
201+
# CONFIGS: name -> (shape, dim) where dim is an int or a tuple.
202+
return WebGPUTestSuite(
203+
module_factory=lambda dim: SqueezeModule(dim),
204+
cases=[
205+
Case(name=n, construct={"dim": dim}, inputs=(shape,))
206+
for n, (shape, dim) in _SQUEEZE_CONFIGS.items()
207+
],
208+
golden_dtype="float32", # reshape copies values; fp64 bit-identical
209+
)
210+
211+
212+
@register_op_test("unsqueeze")
213+
def _unsqueeze_suite() -> WebGPUTestSuite:
214+
# CONFIGS: name -> (shape, dim).
215+
return WebGPUTestSuite(
216+
module_factory=lambda dim: UnsqueezeModule(dim),
217+
cases=[
218+
Case(name=n, construct={"dim": dim}, inputs=(shape,))
219+
for n, (shape, dim) in _UNSQUEEZE_CONFIGS.items()
220+
],
221+
golden_dtype="float32", # reshape copies values; fp64 bit-identical
222+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.squeeze_copy.dims` module + configs for the WebGPU op-test framework.
8+
9+
`SqueezeModule` + `CONFIGS` are imported by `cases.py` to drive the declarative
10+
op-test suite. `SqueezeTest` is the export-delegation smoke
11+
test.
12+
"""
13+
14+
import unittest
15+
16+
import torch
17+
18+
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
19+
from executorch.exir import to_edge_transform_and_lower
20+
21+
# name -> (input_shape, squeeze_dim)
22+
CONFIGS = {
23+
"dim0": ((1, 3, 4), 0),
24+
"mid": ((2, 1, 4), 1),
25+
"multi": ((1, 3, 1, 4), (0, 2)),
26+
}
27+
28+
29+
class SqueezeModule(torch.nn.Module):
30+
def __init__(self, dim):
31+
super().__init__()
32+
self.dim = dim
33+
34+
def forward(self, x: torch.Tensor) -> torch.Tensor:
35+
return torch.squeeze(x, self.dim)
36+
37+
38+
def _det_input(shape):
39+
g = torch.Generator().manual_seed(0)
40+
return torch.randn(*shape, generator=g, dtype=torch.float32)
41+
42+
43+
def _lower(dim, x: torch.Tensor):
44+
ep = torch.export.export(SqueezeModule(dim).eval(), (x,))
45+
return to_edge_transform_and_lower(ep, partitioner=[VulkanPartitioner()])
46+
47+
48+
def _delegated(et) -> bool:
49+
return any(
50+
d.id == "VulkanBackend"
51+
for plan in et.executorch_program.execution_plan
52+
for d in plan.delegates
53+
)
54+
55+
56+
def _op_delegated(edge, op_substr: str) -> bool:
57+
# op must be absorbed into the delegate, not left as a CPU-fallback node.
58+
gm = edge.exported_program().graph_module
59+
return all(op_substr not in str(getattr(n, "target", "")) for n in gm.graph.nodes)
60+
61+
62+
class SqueezeTest(unittest.TestCase):
63+
def test_export_delegates(self) -> None:
64+
for name, (shape, dim) in CONFIGS.items():
65+
with self.subTest(name=name):
66+
edge = _lower(dim, _det_input(shape))
67+
et = edge.to_executorch()
68+
self.assertTrue(
69+
_delegated(et),
70+
f"Expected a VulkanBackend delegate (squeeze {name})",
71+
)
72+
self.assertTrue(
73+
_op_delegated(edge, "squeeze_copy"),
74+
f"squeeze_copy not delegated (fell back to CPU) for {name}",
75+
)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
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.unsqueeze_copy.default` module + configs for the WebGPU op-test framework.
8+
9+
`UnsqueezeModule` + `CONFIGS` are imported by `cases.py` to drive the declarative
10+
op-test suite. `UnsqueezeTest` is the export-delegation smoke
11+
test.
12+
"""
13+
14+
import unittest
15+
16+
import torch
17+
18+
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
19+
from executorch.exir import to_edge_transform_and_lower
20+
21+
# name -> (input_shape, unsqueeze_dim)
22+
CONFIGS = {
23+
"front": ((3, 4), 0),
24+
"mid": ((2, 4), 1),
25+
"last": ((3, 4), 2),
26+
}
27+
28+
29+
class UnsqueezeModule(torch.nn.Module):
30+
def __init__(self, dim):
31+
super().__init__()
32+
self.dim = dim
33+
34+
def forward(self, x: torch.Tensor) -> torch.Tensor:
35+
return torch.unsqueeze(x, self.dim)
36+
37+
38+
def _det_input(shape):
39+
g = torch.Generator().manual_seed(0)
40+
return torch.randn(*shape, generator=g, dtype=torch.float32)
41+
42+
43+
def _lower(dim, x: torch.Tensor):
44+
ep = torch.export.export(UnsqueezeModule(dim).eval(), (x,))
45+
return to_edge_transform_and_lower(ep, partitioner=[VulkanPartitioner()])
46+
47+
48+
def _delegated(et) -> bool:
49+
return any(
50+
d.id == "VulkanBackend"
51+
for plan in et.executorch_program.execution_plan
52+
for d in plan.delegates
53+
)
54+
55+
56+
def _op_delegated(edge, op_substr: str) -> bool:
57+
# op must be absorbed into the delegate, not left as a top-level CPU-fallback node.
58+
gm = edge.exported_program().graph_module
59+
return all(op_substr not in str(getattr(n, "target", "")) for n in gm.graph.nodes)
60+
61+
62+
class UnsqueezeTest(unittest.TestCase):
63+
def test_export_delegates(self) -> None:
64+
for name, (shape, dim) in CONFIGS.items():
65+
with self.subTest(name=name):
66+
edge = _lower(dim, _det_input(shape))
67+
et = edge.to_executorch()
68+
self.assertTrue(
69+
_delegated(et),
70+
f"Expected a VulkanBackend delegate (unsqueeze {name})",
71+
)
72+
self.assertTrue(
73+
_op_delegated(edge, "unsqueeze_copy"),
74+
f"unsqueeze_copy not delegated (fell back to CPU) for {name}",
75+
)

backends/webgpu/test/tester.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
exir_ops.edge.aten.view_copy.default,
2626
exir_ops.edge.aten.select_copy.int,
2727
exir_ops.edge.aten.sigmoid.default,
28+
exir_ops.edge.aten.squeeze_copy.dims,
29+
exir_ops.edge.aten.unsqueeze_copy.default,
2830
]
2931

3032

0 commit comments

Comments
 (0)