Skip to content

Commit e6d8c46

Browse files
authored
[ExecuTorch][WebGPU] Tests for the matmul WebGPU ops
Differential Revision: D111755120 Pull Request resolved: #20918
1 parent e23657c commit e6d8c46

3 files changed

Lines changed: 287 additions & 0 deletions

File tree

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
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.bmm.default` (fp32 batched GEMM) export + golden for the WebGPU backend.
8+
9+
Exports single-op batched-matmul graphs through VulkanPartitioner and writes a
10+
torch-computed fp64 golden (the native binary has no ATen) + the raw fp32 inputs
11+
the native test loads and compares. Configs span the vec4 fast path (K%4==0 and
12+
N%4==0, wider 16B loads) and the scalar tiled path (K or N not a multiple of 4,
13+
which also exercises the bounds guards), across batch sizes 1, 2, and 4.
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+
# name -> (shape_a [B, M, K], shape_b [B, K, N]). Output is [B, M, N].
26+
CONFIGS = {
27+
"square": ((2, 64, 64), (2, 64, 64)), # vec4 path, multi-tile, B=2
28+
"tall": ((4, 128, 16), (4, 16, 24)), # vec4 path, tall M, B=4
29+
"scalar": ((2, 7, 5), (2, 5, 3)), # scalar tiled path (K,N %4!=0)
30+
"batch1": ((1, 33, 17), (1, 17, 5)), # B=1, scalar path, bounds guards
31+
}
32+
33+
34+
class BmmModule(torch.nn.Module):
35+
def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
36+
return torch.bmm(a, b)
37+
38+
39+
def _det_inputs(shape_a, shape_b):
40+
"""Deterministic fp32 inputs (fixed seed) for a config."""
41+
g = torch.Generator().manual_seed(0)
42+
a = torch.randn(*shape_a, generator=g, dtype=torch.float32)
43+
b = torch.randn(*shape_b, generator=g, dtype=torch.float32)
44+
return a, b
45+
46+
47+
def _fp64_golden(a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
48+
"""fp64 truth: bmm(a, b) accumulated in double, cast back to fp32."""
49+
return torch.bmm(a.double(), b.double()).to(torch.float32)
50+
51+
52+
def _export(a: torch.Tensor, b: torch.Tensor):
53+
ep = torch.export.export(BmmModule().eval(), (a, b))
54+
return to_edge_transform_and_lower(
55+
ep, partitioner=[VulkanPartitioner()]
56+
).to_executorch()
57+
58+
59+
def _delegated(et) -> bool:
60+
return any(
61+
d.id == "VulkanBackend"
62+
for plan in et.executorch_program.execution_plan
63+
for d in plan.delegates
64+
)
65+
66+
67+
class BmmTest(unittest.TestCase):
68+
def test_export_delegates(self) -> None:
69+
for name, (sa, sb) in CONFIGS.items():
70+
with self.subTest(name=name):
71+
a, b = _det_inputs(sa, sb)
72+
et = _export(a, b)
73+
self.assertTrue(
74+
_delegated(et), f"Expected a VulkanBackend delegate (bmm {name})"
75+
)
76+
77+
def test_golden_matches_fp64(self) -> None:
78+
for name, (sa, sb) in CONFIGS.items():
79+
with self.subTest(name=name):
80+
a, b = _det_inputs(sa, sb)
81+
torch.testing.assert_close(
82+
BmmModule()(a, b), _fp64_golden(a, b), atol=1e-4, rtol=1e-3
83+
)
84+
85+
86+
def export_bmm_model(pte_path: str, golden_path: str, input_path: str) -> None:
87+
"""Write a bmm .pte + torch fp64 golden (raw LE fp32) + raw LE fp32 inputs (a then b)."""
88+
a, b = _det_inputs(*CONFIGS["square"])
89+
et = _export(a, b)
90+
golden = _fp64_golden(a, b).numpy().astype("<f4")
91+
with open(pte_path, "wb") as f:
92+
f.write(et.buffer)
93+
golden.tofile(golden_path)
94+
with open(input_path, "wb") as f:
95+
f.write(a.numpy().astype("<f4").tobytes())
96+
f.write(b.numpy().astype("<f4").tobytes())
97+
print(
98+
f"Exported {pte_path}; golden {golden_path} ({golden.size} floats); "
99+
f"inputs {input_path} ({a.numel() + b.numel()} floats)"
100+
)
101+
102+
103+
if __name__ == "__main__":
104+
unittest.main()
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
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.linear.default` (fp32, no bias) export + golden for the WebGPU backend.
8+
9+
Exports single-op linear graphs through VulkanPartitioner and locks the
10+
delegation contract + an fp64 torch golden. The handler computes
11+
out[m,n] = sum_k x[m,k] * w[n,k] (weight is [N,K]); it picks a vec4-over-K
12+
kernel when K % 4 == 0 and a scalar tiled kernel otherwise, so the configs span
13+
both branches (plus a K==1 / non-multiple shape that exercises the bounds guard).
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+
# name -> (M, K, N). weight is [N, K]; output is [M, N].
26+
CONFIGS = {
27+
"square": (64, 64, 64), # K % 4 == 0 -> vec4-over-K kernel
28+
"tall_vec4": (32, 128, 16), # K % 4 == 0 -> vec4 path, skinny N
29+
"scalar_k": (8, 7, 5), # K % 4 != 0 -> scalar tiled path + bounds guard
30+
"k1": (4, 1, 8), # K == 1 rank-1, scalar path
31+
}
32+
33+
34+
class LinearModule(torch.nn.Module):
35+
def __init__(self, weight: torch.Tensor) -> None:
36+
super().__init__()
37+
n, k = weight.shape
38+
self.linear = torch.nn.Linear(k, n, bias=False)
39+
with torch.no_grad():
40+
self.linear.weight.copy_(weight)
41+
42+
def forward(self, x: torch.Tensor) -> torch.Tensor:
43+
return self.linear(x)
44+
45+
46+
def _det_inputs(m: int, k: int, n: int):
47+
"""Deterministic fp32 input [M,K] + weight [N,K] (fixed seed)."""
48+
g = torch.Generator().manual_seed(0)
49+
x = torch.randn(m, k, generator=g, dtype=torch.float32)
50+
w = torch.randn(n, k, generator=g, dtype=torch.float32)
51+
return x, w
52+
53+
54+
def _export(m: torch.nn.Module, x: torch.Tensor):
55+
ep = torch.export.export(m, (x,))
56+
return to_edge_transform_and_lower(
57+
ep, partitioner=[VulkanPartitioner()]
58+
).to_executorch()
59+
60+
61+
def _delegates(et) -> bool:
62+
return any(
63+
d.id == "VulkanBackend"
64+
for plan in et.executorch_program.execution_plan
65+
for d in plan.delegates
66+
)
67+
68+
69+
class TestLinear(unittest.TestCase):
70+
def test_export_delegates(self) -> None:
71+
for name, (m, k, n) in CONFIGS.items():
72+
with self.subTest(name=name):
73+
x, w = _det_inputs(m, k, n)
74+
et = _export(LinearModule(w).eval(), x)
75+
self.assertTrue(
76+
_delegates(et), f"Expected a VulkanBackend delegate (linear {name})"
77+
)
78+
79+
def test_golden_matches_fp64(self) -> None:
80+
# Golden must match the fp64 F.linear truth (weight [N,K], no bias).
81+
for name, (m, k, n) in CONFIGS.items():
82+
with self.subTest(name=name):
83+
x, w = _det_inputs(m, k, n)
84+
got = LinearModule(w).eval()(x)
85+
golden = torch.nn.functional.linear(x.double(), w.double())
86+
torch.testing.assert_close(
87+
got, golden.to(torch.float32), atol=1e-3, rtol=1e-3
88+
)
89+
90+
91+
def export_linear_model(
92+
pte_path: str, golden_path: str, input_path: str, config: str = "square"
93+
) -> None:
94+
"""Write a linear .pte + torch fp64 golden (raw LE fp32) + raw LE fp32 input."""
95+
m_dim, k, n = CONFIGS[config]
96+
x, w = _det_inputs(m_dim, k, n)
97+
model = LinearModule(w).eval()
98+
golden = (
99+
torch.nn.functional.linear(x.double(), w.double())
100+
.to(torch.float32)
101+
.numpy()
102+
.astype("<f4")
103+
)
104+
et = _export(model, x)
105+
with open(pte_path, "wb") as f:
106+
f.write(et.buffer)
107+
golden.tofile(golden_path)
108+
x.numpy().astype("<f4").tofile(input_path)
109+
print(
110+
f"Exported {pte_path}; golden {golden_path} ({golden.size} floats); "
111+
f"input {input_path} ({x.numel()} floats)"
112+
)
113+
114+
115+
if __name__ == "__main__":
116+
unittest.main()
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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.mm.default` (fp32 GEMM) module + configs for the WebGPU op-test framework.
8+
9+
`MatmulModule` + `CONFIGS` are imported by `cases.py` to drive the declarative op-test
10+
suite (export via VulkanPartitioner + fp64 torch golden, run on Dawn). `MatmulTest` is
11+
the export-delegation smoke test. Configs span a square matmul, a tall/skinny shape,
12+
K==1 (rank-1 update), and non-power-of-two dims that exercise the bounds guard.
13+
"""
14+
15+
import unittest
16+
17+
import torch
18+
19+
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
20+
from executorch.exir import to_edge_transform_and_lower
21+
22+
# name -> (shape_a [M, K], shape_b [K, N]). Output is [M, N].
23+
CONFIGS = {
24+
"square": ((64, 64), (64, 64)),
25+
"tall": ((128, 32), (32, 16)),
26+
"k1": ((8, 1), (1, 8)),
27+
"nonmultiple": ((7, 5), (5, 3)),
28+
}
29+
30+
31+
class MatmulModule(torch.nn.Module):
32+
def forward(self, a: torch.Tensor, b: torch.Tensor) -> torch.Tensor:
33+
return torch.mm(a, b)
34+
35+
36+
def _det_inputs(shape_a, shape_b):
37+
"""Deterministic fp32 inputs (fixed seed) for a config."""
38+
g = torch.Generator().manual_seed(0)
39+
a = torch.randn(*shape_a, generator=g, dtype=torch.float32)
40+
b = torch.randn(*shape_b, generator=g, dtype=torch.float32)
41+
return a, b
42+
43+
44+
def _export(a: torch.Tensor, b: torch.Tensor):
45+
ep = torch.export.export(MatmulModule().eval(), (a, b))
46+
return to_edge_transform_and_lower(
47+
ep, partitioner=[VulkanPartitioner()]
48+
).to_executorch()
49+
50+
51+
def _delegated(et) -> bool:
52+
return any(
53+
d.id == "VulkanBackend"
54+
for plan in et.executorch_program.execution_plan
55+
for d in plan.delegates
56+
)
57+
58+
59+
class MatmulTest(unittest.TestCase):
60+
def test_export_delegates(self) -> None:
61+
for name, (sa, sb) in CONFIGS.items():
62+
with self.subTest(name=name):
63+
a, b = _det_inputs(sa, sb)
64+
et = _export(a, b)
65+
self.assertTrue(
66+
_delegated(et), f"Expected a VulkanBackend delegate (mm {name})"
67+
)

0 commit comments

Comments
 (0)