diff --git a/backends/webgpu/test/op_tests/cases.py b/backends/webgpu/test/op_tests/cases.py index ff32ca8d870..1f7e1c4d5e2 100644 --- a/backends/webgpu/test/op_tests/cases.py +++ b/backends/webgpu/test/op_tests/cases.py @@ -63,6 +63,9 @@ from executorch.backends.webgpu.test.ops.test_q8ta_conv2d_dw import ( make_q8ta_conv2d_dw_module, ) +from executorch.backends.webgpu.test.ops.test_q8ta_conv2d import ( + make_q8ta_conv2d_module, +) from executorch.backends.webgpu.test.ops.test_pixel_shuffle import PixelShuffleModule from executorch.backends.webgpu.test.ops.test_group_norm import GroupNormModule from executorch.backends.webgpu.test.ops.test_index_select import IndexSelectModule @@ -1571,6 +1574,36 @@ def case(name, c, k, h, w, n=1, **kw): ) +@register_op_test("q8ta_conv2d") +def _q8ta_conv2d_suite() -> WebGPUTestSuite: + # Golden = XNNPACK-static-PT2E converted eager (fp32); general (groups==1) + # conv, full IC reduction. W_out%4==0 for output packing. + def case(name, ic, oc, k, h, w, n=1, **kw): + return Case( + name=name, + construct={"ic": ic, "oc": oc, "k": k, "h": h, "w": w, "n": n, **kw}, + inputs=((n, ic, h, w),), + ) + + return WebGPUTestSuite( + module_factory=lambda **kw: make_q8ta_conv2d_module(**kw), + cases=[ + case("k3", 8, 8, 3, 8, 8), + case("oc_gt", 4, 8, 3, 8, 8), + case("no_bias", 8, 8, 3, 8, 8, bias=False), + case("stride2", 8, 8, 3, 8, 8, stride=2), + case("dil2", 8, 8, 3, 8, 8, dilation=2, padding=2), + case("oc6", 8, 6, 3, 8, 8), + case("ic3", 3, 8, 3, 8, 8), + case("asym", 8, 8, (3, 5), 8, 8, padding=(1, 2)), + case("batch2", 8, 8, 3, 8, 8, n=2), + ], + golden_dtype="float32", + atol=1e-3, + rtol=1e-3, + ) + + @register_op_test("dequantize_per_tensor") def _dequant_const_suite() -> WebGPUTestSuite: # Independent GPU dequantize check: baked int8 const vs torch dequant (breaks diff --git a/backends/webgpu/test/ops/test_q8ta_conv2d.py b/backends/webgpu/test/ops/test_q8ta_conv2d.py new file mode 100644 index 00000000000..5490f27d82a --- /dev/null +++ b/backends/webgpu/test/ops/test_q8ta_conv2d.py @@ -0,0 +1,62 @@ +# 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. + +"""`et_vk.q8ta_conv2d` module + configs: int8 general (ungrouped) conv. + +A standard `nn.Conv2d` (groups == 1) through XNNPACK static PT2E lowers to a +delegated `quantize_per_tensor -> q8ta_conv2d -> dequantize_per_tensor` subgraph +(quantize/dequantize = the landed C0 ops). The `module_factory` returns the +CONVERTED module, so the op-test framework goldens the WebGPU output against the +converted eager (fp32 fake-quant), e2e. +""" + +import unittest + +import executorch.backends.vulkan.custom_ops_lib # noqa: F401 registers et_vk ops + +import torch +import torch.nn as nn + +from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner +from executorch.backends.xnnpack.quantizer.xnnpack_quantizer import ( + get_symmetric_quantization_config, + XNNPACKQuantizer, +) +from executorch.exir import to_edge_transform_and_lower +from torchao.quantization.pt2e.quantize_pt2e import convert_pt2e, prepare_pt2e + + +def make_q8ta_conv2d_module( + ic, oc, k, h, w, stride=1, padding=1, dilation=1, bias=True, n=1, seed=0 +): + torch.manual_seed(seed) + conv = nn.Conv2d( + ic, oc, k, stride=stride, padding=padding, dilation=dilation, bias=bias + ).eval() + ex = (torch.randn(n, ic, h, w),) + q = XNNPACKQuantizer().set_global( + get_symmetric_quantization_config(is_per_channel=True, is_dynamic=False) + ) + prepared = prepare_pt2e(torch.export.export(conv, ex).module(), q) + prepared(*ex) # calibrate + return convert_pt2e(prepared) + + +class Q8taConv2dTest(unittest.TestCase): + def test_delegates(self) -> None: + m = make_q8ta_conv2d_module(8, 8, 3, 8, 8) + et = to_edge_transform_and_lower( + torch.export.export(m, (torch.randn(1, 8, 8, 8),)), + partitioner=[VulkanPartitioner()], + ).to_executorch() + self.assertTrue( + any( + d.id == "VulkanBackend" + for plan in et.executorch_program.execution_plan + for d in plan.delegates + ) + ) + self.assertIn(b"q8ta_conv2d", et.buffer)