Skip to content

Commit 8232449

Browse files
committed
[ExecuTorch][WebGPU] Generate typed to_copy shader variants
Pull Request resolved: #21449 **Generate typed `to_copy` variants from one byte-identical template** The two conversion directions duplicated the same WGSL structure and could drift independently. This moves fp32-to-int32 and int32-to-fp32 into one typed family while preserving both expanded payloads. Mirrors Vulkan `backends/vulkan/runtime/graph/ops/glsl/view_convert_buffer.{glsl,yaml}`. Key changes: - `to_copy_convert.wgsl` and YAML — declare the two typed variants. - Generated headers and codegen locks — preserve symbols, workgroups, registry entries, and payload hashes. - Structural/native tests — lock both directions and the round trip. Host routing and dispatch remain unchanged; tests expand fixture coverage. Co-authored-with: Claude Code. ghstack-source-id: 411961470 @exported-using-ghexport Differential Revision: [D113979712](https://our.internmc.facebook.com/intern/diff/D113979712/)
1 parent 4c414a6 commit 8232449

8 files changed

Lines changed: 191 additions & 28 deletions

File tree

backends/webgpu/runtime/ops/to_copy/to_copy_float_to_int.wgsl renamed to backends/webgpu/runtime/ops/to_copy/to_copy_convert.wgsl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
@group(0) @binding(0) var<storage, read> input: array<f32>;
2-
@group(0) @binding(1) var<storage, read_write> output: array<i32>;
1+
@group(0) @binding(0) var<storage, read> input: array<${IN_TYPE}>;
2+
@group(0) @binding(1) var<storage, read_write> output: array<${OUT_TYPE}>;
33

44
struct Params {
55
num_elements: u32,
@@ -14,5 +14,5 @@ fn main(@builtin(global_invocation_id) gid: vec3<u32>) {
1414
if (idx >= params.num_elements) {
1515
return;
1616
}
17-
output[idx] = i32(input[idx]);
17+
output[idx] = ${OUT_TYPE}(input[idx]);
1818
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
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+
to_copy_convert:
8+
parameter_names_with_default_values:
9+
IN_TYPE: f32
10+
OUT_TYPE: i32
11+
shader_variants:
12+
- NAME: to_copy_float_to_int
13+
- NAME: to_copy_int_to_float
14+
IN_TYPE: i32
15+
OUT_TYPE: f32

backends/webgpu/runtime/ops/to_copy/to_copy_float_to_int_wgsl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace executorch::backends::webgpu {
1414

15-
// @generated from to_copy_float_to_int.wgsl - DO NOT EDIT.
15+
// @generated from to_copy_convert.wgsl - DO NOT EDIT.
1616
// wgsl-sha256: c331e00e3171eecbe6317ac9df0a5f9cd6d25da26a9a587250f1cc6086dc3c8f
1717
inline constexpr const char* kToCopyFloatToIntWGSL = R"(
1818
@group(0) @binding(0) var<storage, read> input: array<f32>;

backends/webgpu/runtime/ops/to_copy/to_copy_int_to_float.wgsl

Lines changed: 0 additions & 18 deletions
This file was deleted.

backends/webgpu/runtime/ops/to_copy/to_copy_int_to_float_wgsl.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
namespace executorch::backends::webgpu {
1414

15-
// @generated from to_copy_int_to_float.wgsl - DO NOT EDIT.
15+
// @generated from to_copy_convert.wgsl - DO NOT EDIT.
1616
// wgsl-sha256: e18dd733a3838f83eded4977a2a2b21119099c8409b234f12474fae5acc9b195
1717
inline constexpr const char* kToCopyIntToFloatWGSL = R"(
1818
@group(0) @binding(0) var<storage, read> input: array<i32>;

backends/webgpu/test/op_tests/cases.py

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,13 @@
145145
SqueezeModule,
146146
)
147147

148+
from executorch.backends.webgpu.test.ops.test_to_copy import (
149+
to_copy_float_input,
150+
to_copy_int_input,
151+
ToCopyFloatToIntToFloatModule,
152+
ToCopyIntToFloatModule,
153+
)
154+
148155
from executorch.backends.webgpu.test.ops.test_unary_activations import (
149156
_lin as _unary_lin,
150157
CLAMP_CONFIGS,
@@ -845,6 +852,38 @@ def _view_copy_suite() -> WebGPUTestSuite:
845852
return _fn_config_suite(ViewModule, _VIEW_CONFIGS)
846853

847854

855+
def _to_copy_factory(variant: str) -> torch.nn.Module:
856+
return {
857+
"int_to_float": ToCopyIntToFloatModule,
858+
"float_roundtrip": ToCopyFloatToIntToFloatModule,
859+
}[variant]()
860+
861+
862+
@register_op_test("to_copy")
863+
def _to_copy_suite() -> WebGPUTestSuite:
864+
cases = []
865+
for n in (63, 64, 65, 257):
866+
cases.extend(
867+
[
868+
Case(
869+
name=f"int_to_float_{n}",
870+
construct={"variant": "int_to_float"},
871+
inputs=(InputSpec(shape=(n,), gen=to_copy_int_input),),
872+
),
873+
Case(
874+
name=f"float_roundtrip_{n}",
875+
construct={"variant": "float_roundtrip"},
876+
inputs=(InputSpec(shape=(n,), gen=to_copy_float_input),),
877+
),
878+
]
879+
)
880+
return WebGPUTestSuite(
881+
module_factory=_to_copy_factory,
882+
cases=cases,
883+
golden_dtype="float32",
884+
)
885+
886+
848887
@register_op_test("select")
849888
def _select_suite() -> WebGPUTestSuite:
850889
return _fn_config_suite(SelectModule, _SELECT_CONFIGS)

backends/webgpu/test/ops/test_to_copy.py

Lines changed: 83 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,14 @@
1313
lvp golden (yolo11n / Depth-Anything-V2).
1414
"""
1515

16+
import math
1617
import unittest
1718

1819
import torch
1920

2021
from executorch.backends.vulkan.partitioner.vulkan_partitioner import VulkanPartitioner
2122
from executorch.exir import to_edge_transform_and_lower
23+
from executorch.exir.lowered_backend_module import get_lowered_submodules
2224

2325

2426
class ToCopyIntToFloatModule(torch.nn.Module):
@@ -27,18 +29,47 @@ def forward(self, x: torch.Tensor) -> torch.Tensor:
2729
return x.to(torch.float32)
2830

2931

32+
class ToCopyFloatToIntModule(torch.nn.Module):
33+
def forward(self, x: torch.Tensor) -> torch.Tensor:
34+
return x.to(torch.int32)
35+
36+
37+
class ToCopyFloatToIntToFloatModule(torch.nn.Module):
38+
def forward(self, x: torch.Tensor) -> torch.Tensor:
39+
return x.to(torch.int32).to(torch.float32)
40+
41+
3042
class ToCopyFloatModule(torch.nn.Module):
3143
def forward(self, x: torch.Tensor) -> torch.Tensor:
3244
# Same-dtype copy (flat byte-copy path); copy=True keeps the op from
3345
# being elided as a no-op.
3446
return x.to(torch.float32, copy=True)
3547

3648

37-
def _export(model: torch.nn.Module, x: torch.Tensor):
49+
def to_copy_int_input(shape: tuple[int, ...]) -> torch.Tensor:
50+
n = math.prod(shape)
51+
return (torch.arange(n, dtype=torch.int32) - n // 2).reshape(shape)
52+
53+
54+
def to_copy_float_input(shape: tuple[int, ...]) -> torch.Tensor:
55+
n = math.prod(shape)
56+
pattern = torch.tensor(
57+
[-8.75, -3.0, -1.5, -0.25, 0.0, 0.25, 1.5, 3.0, 8.75],
58+
dtype=torch.float32,
59+
)
60+
repeats = (n + pattern.numel() - 1) // pattern.numel()
61+
return pattern.repeat(repeats)[:n].reshape(shape)
62+
63+
64+
def _lower(model: torch.nn.Module, x: torch.Tensor):
3865
ep = torch.export.export(model.eval(), (x,))
39-
return to_edge_transform_and_lower(
40-
ep, partitioner=[VulkanPartitioner()]
41-
).to_executorch()
66+
edge = to_edge_transform_and_lower(ep, partitioner=[VulkanPartitioner()])
67+
return ep, edge
68+
69+
70+
def _export(model: torch.nn.Module, x: torch.Tensor):
71+
_, edge = _lower(model, x)
72+
return edge.to_executorch()
4273

4374

4475
def _delegated(et) -> bool:
@@ -49,6 +80,29 @@ def _delegated(et) -> bool:
4980
)
5081

5182

83+
def _prepartition_cast_dtypes(ep) -> list[torch.dtype]:
84+
return [
85+
node.args[1]
86+
for node in ep.graph_module.graph.nodes
87+
if node.op == "call_function" and node.target == torch.ops.aten.to.dtype
88+
]
89+
90+
91+
def _delegated_cast_dtypes(edge) -> list[torch.dtype]:
92+
graph_module = edge.exported_program().graph_module
93+
if any(
94+
"_to_dim_order_copy" in str(getattr(node, "target", ""))
95+
for node in graph_module.graph.nodes
96+
):
97+
return []
98+
return [
99+
node.kwargs["dtype"]
100+
for _, lowered, _ in get_lowered_submodules(graph_module)
101+
for node in lowered.original_module.graph_module.graph.nodes
102+
if "_to_dim_order_copy" in str(getattr(node, "target", ""))
103+
]
104+
105+
52106
class ToCopyTest(unittest.TestCase):
53107
def test_int_to_float_delegates(self) -> None:
54108
x = torch.tensor([1, 2, 3], dtype=torch.int32)
@@ -57,6 +111,31 @@ def test_int_to_float_delegates(self) -> None:
57111
_delegated(et), "Expected a VulkanBackend delegate (to_copy int->float)"
58112
)
59113

114+
def test_float_to_int_delegates(self) -> None:
115+
x = torch.tensor([-3.75, -1.0, 0.0, 1.9, 63.0], dtype=torch.float32)
116+
et = _export(ToCopyFloatToIntModule(), x)
117+
self.assertTrue(
118+
_delegated(et), "Expected a VulkanBackend delegate (to_copy float->int)"
119+
)
120+
121+
def test_roundtrip_keeps_both_casts_in_delegate(self) -> None:
122+
x = torch.tensor([-3.75, -1.0, 0.0, 1.9, 63.0], dtype=torch.float32)
123+
ep, edge = _lower(ToCopyFloatToIntToFloatModule(), x)
124+
expected = [torch.int32, torch.float32]
125+
self.assertEqual(_prepartition_cast_dtypes(ep), expected)
126+
self.assertEqual(_delegated_cast_dtypes(edge), expected)
127+
self.assertTrue(_delegated(edge.to_executorch()))
128+
129+
for module, one_direction_input in (
130+
(ToCopyFloatToIntModule(), x),
131+
(
132+
ToCopyIntToFloatModule(),
133+
torch.tensor([-3, -1, 0, 1, 63], dtype=torch.int32),
134+
),
135+
):
136+
_, one_direction_edge = _lower(module, one_direction_input)
137+
self.assertNotEqual(_delegated_cast_dtypes(one_direction_edge), expected)
138+
60139
def test_float_passthrough_delegates(self) -> None:
61140
x = torch.tensor([1.0, 2.0, 3.0], dtype=torch.float32)
62141
et = _export(ToCopyFloatModule(), x)

backends/webgpu/test/test_wgsl_codegen.py

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ def test_generated_output_manifest_digest(self) -> None:
213213
self.assertEqual(len(outputs), 134)
214214
self.assertEqual(
215215
digest.hexdigest(),
216-
"19a0baf9345bec02fe2091a0e6320b81d966724bedc33feb0779c6a824925972",
216+
"ab15be30e7cfa2cb2f6fa7743d3b9f03535f5cc0b88d64d9b6bad8f777efda25",
217217
)
218218

219219
def test_rope_hf_reconstructs_full_2d_grid_stride(self) -> None:
@@ -907,6 +907,54 @@ def test_rms_norm_template_roundtrip_byte_identical(self) -> None:
907907
got, want, f"{header_name} not reproduced from rms_norm.wgsl template"
908908
)
909909

910+
def test_to_copy_convert_template_roundtrip_byte_identical(self) -> None:
911+
to_copy_dir = g.BACKEND_ROOT / "runtime/ops/to_copy"
912+
template_path = to_copy_dir / "to_copy_convert.wgsl"
913+
spec = g.parse_template_spec(template_path.with_suffix(".yaml"))
914+
variants = {params["NAME"]: params for params in spec[template_path.stem]}
915+
expected = {
916+
"to_copy_float_to_int": (
917+
"f32",
918+
"i32",
919+
"c331e00e3171eecbe6317ac9df0a5f9cd6d25da26a9a587250f1cc6086dc3c8f",
920+
),
921+
"to_copy_int_to_float": (
922+
"i32",
923+
"f32",
924+
"e18dd733a3838f83eded4977a2a2b21119099c8409b234f12474fae5acc9b195",
925+
),
926+
}
927+
self.assertEqual(set(variants), set(expected))
928+
template = template_path.read_text()
929+
930+
for name, (in_type, out_type, expected_hash) in expected.items():
931+
params = variants[name]
932+
self.assertEqual(
933+
(params["IN_TYPE"], params["OUT_TYPE"]), (in_type, out_type)
934+
)
935+
expanded = g.preprocess(template, {**g.WGSL_HELPERS, **params})
936+
self.assertEqual(g.wgsl_sha256(expanded), expected_hash)
937+
938+
header = (to_copy_dir / f"{name}_wgsl.h").read_text()
939+
body = header.split('R"(', 1)[1].split(')";', 1)[0][1:]
940+
self.assertEqual(body, expanded)
941+
self.assertEqual(g.embedded_sha256(header), expected_hash)
942+
self.assertEqual(g.parse_workgroup_size(body), (64, 1, 1))
943+
944+
entries = {entry.name: entry for entry in g.registry_entries()}
945+
self.assertEqual(
946+
entries["to_copy_float_to_int"].include,
947+
"runtime/ops/to_copy/to_copy_float_to_int_wgsl.h",
948+
)
949+
self.assertEqual(
950+
entries["to_copy_int_to_float"].include,
951+
"runtime/ops/to_copy/to_copy_int_to_float_wgsl.h",
952+
)
953+
self.assertEqual(
954+
hashlib.sha256(g.registry_path().read_bytes()).hexdigest(),
955+
"ce1777820bffe77e7cdda312f86a3fd41a090f8f282a6add66666446d06b1608",
956+
)
957+
910958
def test_rms_norm_half_variant_is_type_correct(self) -> None:
911959
# A DTYPE=half expansion must emit compilable WGSL: `enable f16;`, an f32
912960
# accumulator, loads widened to f32 for the reduction, and the store

0 commit comments

Comments
 (0)