Skip to content

Commit 9e216c7

Browse files
authored
Keep unsupported BF16 and dynamic reshapes out of XNNPACK (pytorch#23021)
Leave BF16 convolution and Conv1d/unsqueeze reshapes with multiple dynamic dimensions to ExecuTorch's native kernels. Backend regression tests export and execute Conv1d, Conv2d, and unsqueeze without a model dependency. They check BF16 fallback, dynamic batch and length, and continued delegation with one dynamic dimension against PyTorch outputs. Authored with OpenAI Codex.
1 parent eb457f1 commit 9e216c7

5 files changed

Lines changed: 142 additions & 0 deletions

File tree

‎backends/xnnpack/partition/config/gemm_configs.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from executorch.exir.backend.utils import WhyNoPartition
4141
from executorch.exir.dialects._ops import ops as exir_ops
4242
from torch.export import ExportedProgram
43+
from torch.fx.experimental.symbolic_shapes import free_symbols
4344
from torch.fx.passes.utils.source_matcher_utils import (
4445
get_source_partitions,
4546
SourcePartition,
@@ -363,6 +364,12 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
363364

364365
kernel_node = get_input_node(node, 1)
365366
kernel_shape = get_shape(kernel_node)
367+
if any(
368+
value.meta["val"].dtype == torch.bfloat16
369+
for value in (node, kernel_node, get_input_node(node, 0))
370+
):
371+
why(node, "XNNPACK does not support BF16 convolution")
372+
return False
366373
# The weight rank is the only reliable indicator of the conv dimensionality.
367374
# stride, padding and dilation may each be a single value that ATen
368375
# broadcasts over every spatial dim.
@@ -371,6 +378,17 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
371378
why(node, "Only support 1D + 2D Conv")
372379
return False # Only support 1D + 2D Conv
373380

381+
# Conv1d lowering inserts reshapes with at most one inferred dimension.
382+
if (
383+
conv_dim == 1
384+
and sum(
385+
bool(free_symbols(d)) for d in get_input_node(node, 0).meta["val"].shape
386+
)
387+
> 1
388+
):
389+
why(node, "Conv1d reshapes only support one dynamic dimension")
390+
return False
391+
374392
weight_quant_params = QuantParams.from_weights(kernel_node, ep)
375393
groups = cast(int, node.args[8])
376394
is_transpose = node.args[6]

‎backends/xnnpack/partition/config/generic_node_configs.py‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from executorch.exir.backend.utils import is_shape_dynamic, WhyNoPartition
3535
from torch._subclasses.fake_tensor import FakeTensor
3636
from torch.export import ExportedProgram
37+
from torch.fx.experimental.symbolic_shapes import free_symbols
3738

3839
logger = logging.getLogger(__name__)
3940
why = WhyNoPartition(logger=logger)
@@ -900,4 +901,8 @@ def check_constraints(self, node: torch.fx.Node, ep: ExportedProgram) -> bool:
900901
why(node, reason="unsqueeze_copy only supported on the trailing dimension")
901902
return False
902903

904+
if sum(bool(free_symbols(d)) for d in node.meta["val"].shape) > 1:
905+
why(node, reason="XNNPACK reshape only supports one dynamic dimension")
906+
return False
907+
903908
return True

‎backends/xnnpack/test/ops/test_conv1d.py‎

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,57 @@ class TestConv1d(unittest.TestCase):
2222
def setUp(self):
2323
torch._dynamo.reset()
2424

25+
def test_dynamic_conv1d(self):
26+
for dynamic_dims in ((0,), (2,), (0, 2)):
27+
with self.subTest(dynamic_dims=dynamic_dims):
28+
shapes = {
29+
0: torch.export.Dim("batch", min=1, max=4),
30+
2: torch.export.Dim("length", min=5, max=16),
31+
}
32+
tester = (
33+
Tester(
34+
torch.nn.Conv1d(2, 2, 3).eval(),
35+
(torch.randn(2, 2, 11),),
36+
dynamic_shapes=({dim: shapes[dim] for dim in dynamic_dims},),
37+
)
38+
.export()
39+
.to_edge_transform_and_lower()
40+
.check_count(
41+
{
42+
"torch.ops.higher_order.executorch_call_delegate": int(
43+
len(dynamic_dims) == 1
44+
)
45+
}
46+
)
47+
.to_executorch()
48+
.serialize()
49+
)
50+
for batch, length in ((1, 5), (4, 16), (2, 7)):
51+
inputs = torch.randn(
52+
batch if 0 in dynamic_dims else 2,
53+
2,
54+
length if 2 in dynamic_dims else 11,
55+
)
56+
tester.run_method_and_compare_outputs(inputs=(inputs,))
57+
58+
def test_bf16_conv1d_fallback(self):
59+
(
60+
Tester(
61+
torch.nn.Conv1d(2, 2, 3).eval().to(torch.bfloat16),
62+
(torch.randn(2, 2, 11, dtype=torch.bfloat16),),
63+
)
64+
.export()
65+
.to_edge_transform_and_lower(
66+
ToEdgeTransformAndLower(
67+
partitioners=[XnnpackPartitioner(enable_bf16=True)]
68+
)
69+
)
70+
.check_count({"torch.ops.higher_order.executorch_call_delegate": 0})
71+
.to_executorch()
72+
.serialize()
73+
.run_method_and_compare_outputs(atol=0.01, rtol=0.01)
74+
)
75+
2576
class Conv1d(torch.nn.Module):
2677
def __init__(self, dtype: torch.dtype = torch.float):
2778
groups = 1

‎backends/xnnpack/test/ops/test_conv2d.py‎

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,24 @@ class TestConv2d(unittest.TestCase):
220220
def setUp(self):
221221
torch._dynamo.reset()
222222

223+
def test_bf16_conv2d_fallback(self):
224+
(
225+
Tester(
226+
torch.nn.Conv2d(2, 2, 3).eval().to(torch.bfloat16),
227+
(torch.randn(2, 2, 7, 7, dtype=torch.bfloat16),),
228+
)
229+
.export()
230+
.to_edge_transform_and_lower(
231+
ToEdgeTransformAndLower(
232+
partitioners=[XnnpackPartitioner(enable_bf16=True)]
233+
)
234+
)
235+
.check_count({"torch.ops.higher_order.executorch_call_delegate": 0})
236+
.to_executorch()
237+
.serialize()
238+
.run_method_and_compare_outputs(atol=0.01, rtol=0.01)
239+
)
240+
223241
def _test(
224242
self,
225243
m: torch.nn.Module,
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
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+
import unittest
8+
9+
import torch
10+
from executorch.backends.xnnpack.test.tester import Tester
11+
12+
13+
class TestUnsqueeze(unittest.TestCase):
14+
def test_dynamic_trailing_unsqueeze(self):
15+
class Unsqueeze(torch.nn.Module):
16+
def __init__(self, dim):
17+
super().__init__()
18+
self.dim = dim
19+
20+
def forward(self, x):
21+
return x.unsqueeze(self.dim)
22+
23+
for dim in (-1, 2):
24+
for dynamic_dims in (1, 2):
25+
with self.subTest(dim=dim, dynamic_dims=dynamic_dims):
26+
shapes = {
27+
i: torch.export.Dim(f"dim_{i}", min=1, max=8)
28+
for i in range(dynamic_dims)
29+
}
30+
tester = (
31+
Tester(
32+
Unsqueeze(dim),
33+
(torch.randn(2, 3),),
34+
dynamic_shapes=(shapes,),
35+
)
36+
.export()
37+
.to_edge_transform_and_lower()
38+
.check_count(
39+
{
40+
"torch.ops.higher_order.executorch_call_delegate": int(
41+
dynamic_dims == 1
42+
)
43+
}
44+
)
45+
.to_executorch()
46+
.serialize()
47+
)
48+
for batch, width in ((1, 1), (8, 8), (4, 2)):
49+
inputs = torch.randn(batch, width if dynamic_dims == 2 else 3)
50+
tester.run_method_and_compare_outputs(inputs=(inputs,))

0 commit comments

Comments
 (0)