From bd03d38808ff7f25647c731ed175943144bf39a3 Mon Sep 17 00:00:00 2001 From: Julian Ng-Thow-Hing Date: Tue, 14 Jul 2026 14:48:04 -0700 Subject: [PATCH 1/2] Update [ghstack-poisoned] --- backends/webgpu/test/ops/test_bmm.py | 106 +++++++++++++++++++++ backends/webgpu/test/ops/test_linear.py | 118 ++++++++++++++++++++++++ backends/webgpu/test/ops/test_matmul.py | 67 ++++++++++++++ 3 files changed, 291 insertions(+) create mode 100644 backends/webgpu/test/ops/test_bmm.py create mode 100644 backends/webgpu/test/ops/test_linear.py create mode 100644 backends/webgpu/test/ops/test_matmul.py diff --git a/backends/webgpu/test/ops/test_bmm.py b/backends/webgpu/test/ops/test_bmm.py new file mode 100644 index 00000000000..6fc7708a495 --- /dev/null +++ b/backends/webgpu/test/ops/test_bmm.py @@ -0,0 +1,106 @@ +# 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.bmm.default` (fp32 batched GEMM) export + golden for the WebGPU backend. + +Exports single-op batched-matmul graphs through VulkanPartitioner and writes a +torch-computed fp64 golden (the native binary has no ATen) + the raw fp32 inputs +the native test loads and compares. Configs span the vec4 fast path (K%4==0 and +N%4==0, wider 16B loads) and the scalar tiled path (K or N not a multiple of 4, +which also exercises the bounds guards), across batch sizes 1, 2, and 4. +""" + +from __future__ import annotations + +import unittest + +import torch + +from executorch.backends.vulkan.partitioner.vulkan_partitioner import ( + VulkanPartitioner, +) +from executorch.exir import to_edge_transform_and_lower + +# name -> (shape_a [B, M, K], shape_b [B, K, N]). Output is [B, M, N]. +CONFIGS = { + "square": ((2, 64, 64), (2, 64, 64)), # vec4 path, multi-tile, B=2 + "tall": ((4, 128, 16), (4, 16, 24)), # vec4 path, tall M, B=4 + "scalar": ((2, 7, 5), (2, 5, 3)), # scalar tiled path (K,N %4!=0) + "batch1": ((1, 33, 17), (1, 17, 5)), # B=1, scalar path, bounds guards +} + + +class BmmModule(torch.nn.Module): + def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + return torch.bmm(a, b) + + +def _det_inputs(shape_a, shape_b): + """Deterministic fp32 inputs (fixed seed) for a config.""" + g = torch.Generator().manual_seed(0) + a = torch.randn(*shape_a, generator=g, dtype=torch.float32) + b = torch.randn(*shape_b, generator=g, dtype=torch.float32) + return a, b + + +def _fp64_golden(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + """fp64 truth: bmm(a, b) accumulated in double, cast back to fp32.""" + return torch.bmm(a.double(), b.double()).to(torch.float32) + + +def _export(a: torch.Tensor, b: torch.Tensor): + ep = torch.export.export(BmmModule().eval(), (a, b)) + 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 BmmTest(unittest.TestCase): + def test_export_delegates(self) -> None: + for name, (sa, sb) in CONFIGS.items(): + with self.subTest(name=name): + a, b = _det_inputs(sa, sb) + et = _export(a, b) + self.assertTrue( + _delegated(et), f"Expected a VulkanBackend delegate (bmm {name})" + ) + + def test_golden_matches_fp64(self) -> None: + for name, (sa, sb) in CONFIGS.items(): + with self.subTest(name=name): + a, b = _det_inputs(sa, sb) + torch.testing.assert_close( + BmmModule()(a, b), _fp64_golden(a, b), atol=1e-4, rtol=1e-3 + ) + + +def export_bmm_model(pte_path: str, golden_path: str, input_path: str) -> None: + """Write a bmm .pte + torch fp64 golden (raw LE fp32) + raw LE fp32 inputs (a then b).""" + a, b = _det_inputs(*CONFIGS["square"]) + et = _export(a, b) + golden = _fp64_golden(a, b).numpy().astype(" (M, K, N). weight is [N, K]; output is [M, N]. +CONFIGS = { + "square": (64, 64, 64), # K % 4 == 0 -> vec4-over-K kernel + "tall_vec4": (32, 128, 16), # K % 4 == 0 -> vec4 path, skinny N + "scalar_k": (8, 7, 5), # K % 4 != 0 -> scalar tiled path + bounds guard + "k1": (4, 1, 8), # K == 1 rank-1, scalar path +} + + +class LinearModule(torch.nn.Module): + def __init__(self, weight: torch.Tensor) -> None: + super().__init__() + n, k = weight.shape + self.linear = torch.nn.Linear(k, n, bias=False) + with torch.no_grad(): + self.linear.weight.copy_(weight) + + def forward(self, x: torch.Tensor) -> torch.Tensor: + return self.linear(x) + + +def _det_inputs(m: int, k: int, n: int): + """Deterministic fp32 input [M,K] + weight [N,K] (fixed seed).""" + g = torch.Generator().manual_seed(0) + x = torch.randn(m, k, generator=g, dtype=torch.float32) + w = torch.randn(n, k, generator=g, dtype=torch.float32) + return x, w + + +def _export(m: torch.nn.Module, x: torch.Tensor): + ep = torch.export.export(m, (x,)) + return to_edge_transform_and_lower( + ep, partitioner=[VulkanPartitioner()] + ).to_executorch() + + +def _delegates(et) -> bool: + return any( + d.id == "VulkanBackend" + for plan in et.executorch_program.execution_plan + for d in plan.delegates + ) + + +class TestLinear(unittest.TestCase): + def test_export_delegates(self) -> None: + for name, (m, k, n) in CONFIGS.items(): + with self.subTest(name=name): + x, w = _det_inputs(m, k, n) + et = _export(LinearModule(w).eval(), x) + self.assertTrue( + _delegates(et), f"Expected a VulkanBackend delegate (linear {name})" + ) + + def test_golden_matches_fp64(self) -> None: + # Golden must match the fp64 F.linear truth (weight [N,K], no bias). + for name, (m, k, n) in CONFIGS.items(): + with self.subTest(name=name): + x, w = _det_inputs(m, k, n) + got = LinearModule(w).eval()(x) + golden = torch.nn.functional.linear(x.double(), w.double()) + torch.testing.assert_close( + got, golden.to(torch.float32), atol=1e-3, rtol=1e-3 + ) + + +def export_linear_model( + pte_path: str, golden_path: str, input_path: str, config: str = "square" +) -> None: + """Write a linear .pte + torch fp64 golden (raw LE fp32) + raw LE fp32 input.""" + m_dim, k, n = CONFIGS[config] + x, w = _det_inputs(m_dim, k, n) + model = LinearModule(w).eval() + golden = ( + torch.nn.functional.linear(x.double(), w.double()) + .to(torch.float32) + .numpy() + .astype(" (shape_a [M, K], shape_b [K, N]). Output is [M, N]. +CONFIGS = { + "square": ((64, 64), (64, 64)), + "tall": ((128, 32), (32, 16)), + "k1": ((8, 1), (1, 8)), + "nonmultiple": ((7, 5), (5, 3)), +} + + +class MatmulModule(torch.nn.Module): + def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor: + return torch.mm(a, b) + + +def _det_inputs(shape_a, shape_b): + """Deterministic fp32 inputs (fixed seed) for a config.""" + g = torch.Generator().manual_seed(0) + a = torch.randn(*shape_a, generator=g, dtype=torch.float32) + b = torch.randn(*shape_b, generator=g, dtype=torch.float32) + return a, b + + +def _export(a: torch.Tensor, b: torch.Tensor): + ep = torch.export.export(MatmulModule().eval(), (a, b)) + 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 MatmulTest(unittest.TestCase): + def test_export_delegates(self) -> None: + for name, (sa, sb) in CONFIGS.items(): + with self.subTest(name=name): + a, b = _det_inputs(sa, sb) + et = _export(a, b) + self.assertTrue( + _delegated(et), f"Expected a VulkanBackend delegate (mm {name})" + ) From d6efb2b90474a9a3366865f75b75854e4289d30a Mon Sep 17 00:00:00 2001 From: Julian Ng-Thow-Hing Date: Tue, 14 Jul 2026 15:29:57 -0700 Subject: [PATCH 2/2] Update [ghstack-poisoned] --- backends/webgpu/test/ops/test_bmm.py | 4 +--- backends/webgpu/test/ops/test_linear.py | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/backends/webgpu/test/ops/test_bmm.py b/backends/webgpu/test/ops/test_bmm.py index 6fc7708a495..3cc6ba17bf2 100644 --- a/backends/webgpu/test/ops/test_bmm.py +++ b/backends/webgpu/test/ops/test_bmm.py @@ -19,9 +19,7 @@ import torch -from executorch.backends.vulkan.partitioner.vulkan_partitioner import ( - VulkanPartitioner, -) +from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner from executorch.exir import to_edge_transform_and_lower # name -> (shape_a [B, M, K], shape_b [B, K, N]). Output is [B, M, N]. diff --git a/backends/webgpu/test/ops/test_linear.py b/backends/webgpu/test/ops/test_linear.py index 651a544188f..b24663c3db7 100644 --- a/backends/webgpu/test/ops/test_linear.py +++ b/backends/webgpu/test/ops/test_linear.py @@ -19,9 +19,7 @@ import torch -from executorch.backends.vulkan.partitioner.vulkan_partitioner import ( - VulkanPartitioner, -) +from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner from executorch.exir import to_edge_transform_and_lower # name -> (M, K, N). weight is [N, K]; output is [M, N].