diff --git a/backends/webgpu/test/op_tests/cases.py b/backends/webgpu/test/op_tests/cases.py index 26afe1506ff..6b47b4c399e 100644 --- a/backends/webgpu/test/op_tests/cases.py +++ b/backends/webgpu/test/op_tests/cases.py @@ -40,6 +40,10 @@ _ramp, RmsNormModule, ) +from executorch.backends.webgpu.test.ops.view_copy.test_view_copy import ( + CONFIGS as _VIEW_CONFIGS, + ViewModule, +) # rms_norm coverage is exactly the 14 cases the native test covered. RMS_NORM_CASES = _CASES @@ -121,3 +125,22 @@ def _mul_suite() -> WebGPUTestSuite: Case(name=name, inputs=(sa, sb)) for name, (sa, sb) in _MUL_CONFIGS.items() ], ) + + +def _fn_config_suite(module_cls, configs) -> WebGPUTestSuite: + """Builder for ops whose per-case spec is a (shape, fn) pair (view/select/slice). + The fn is a `construct` kwarg baked into the .pte module, never a serialized input. + """ + return WebGPUTestSuite( + module_factory=lambda fn: module_cls(fn), + cases=[ + Case(name=n, construct={"fn": fn}, inputs=(shape,)) + for n, (shape, fn) in configs.items() + ], + golden_dtype="float32", # gather/copy: fp64 bit-identical, skip dual-oracle + ) + + +@register_op_test("view_copy") +def _view_copy_suite() -> WebGPUTestSuite: + return _fn_config_suite(ViewModule, _VIEW_CONFIGS) diff --git a/backends/webgpu/test/ops/view_copy/__init__.py b/backends/webgpu/test/ops/view_copy/__init__.py new file mode 100644 index 00000000000..2e41cd717f6 --- /dev/null +++ b/backends/webgpu/test/ops/view_copy/__init__.py @@ -0,0 +1,5 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. diff --git a/backends/webgpu/test/ops/view_copy/test_view_copy.py b/backends/webgpu/test/ops/view_copy/test_view_copy.py new file mode 100644 index 00000000000..9ffcc444e32 --- /dev/null +++ b/backends/webgpu/test/ops/view_copy/test_view_copy.py @@ -0,0 +1,65 @@ +# Copyright (c) Meta Platforms, Inc. and affiliates. +# All rights reserved. +# +# This source code is licensed under the BSD-style license found in the +# LICENSE file in the root directory of this source tree. + +"""`aten.view_copy.default` module + configs for the WebGPU op-test framework. + +`ViewModule` + `CONFIGS` are imported by `cases.py` to drive the declarative +op-test suite. `ViewCopyTest` is the export-delegation +smoke test. +""" + +import unittest + +import torch + +from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner +from executorch.exir import to_edge_transform_and_lower + +# name -> (input_shape, view_fn) +CONFIGS = { + "reshape": ((2, 3, 4), lambda x: x.reshape(6, 4)), + "flatten": ((2, 3, 4), lambda x: x.reshape(-1)), + "reshape4d": ((2, 3, 4), lambda x: x.reshape(1, 2, 3, 4)), +} + + +class ViewModule(torch.nn.Module): + def __init__(self, fn): + super().__init__() + self.fn = fn + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.fn(x) + + +def _det_input(shape): + g = torch.Generator().manual_seed(0) + return torch.randn(*shape, generator=g, dtype=torch.float32) + + +def _export(fn, x: torch.Tensor): + ep = torch.export.export(ViewModule(fn).eval(), (x,)) + return to_edge_transform_and_lower( + ep, partitioner=[VulkanPartitioner()] + ).to_executorch() + + +def _delegated(et) -> bool: + return any( + d.id == "VulkanBackend" + for plan in et.executorch_program.execution_plan + for d in plan.delegates + ) + + +class ViewCopyTest(unittest.TestCase): + def test_export_delegates(self) -> None: + for name, (shape, fn) in CONFIGS.items(): + et = _export(fn, _det_input(shape)) + self.assertTrue( + _delegated(et), + f"Expected a VulkanBackend delegate (view_copy {name})", + ) diff --git a/backends/webgpu/test/tester.py b/backends/webgpu/test/tester.py index eafb64c6961..bb09fffb43e 100644 --- a/backends/webgpu/test/tester.py +++ b/backends/webgpu/test/tester.py @@ -22,6 +22,7 @@ exir_ops.edge.aten.add.Tensor, exir_ops.edge.et_vk.rms_norm.default, exir_ops.edge.aten.mul.Tensor, + exir_ops.edge.aten.view_copy.default, ]