Skip to content

Commit 7013c8d

Browse files
Fix channels-last portable fallback by inserting explicit dim-order legalization (#20878)
Fixes #16507 #11086 I kept the pass and regressions in separate files to keep the change cleaner. A more optimized followup is probably worth doing if more issues like these appear. Added separate test file `exir/tests/test_legalize_portable_dim_order_pass.py` cc @JacobSzwejbka @angelayi @larryliu0820 @lucylq
1 parent ceecc9e commit 7013c8d

4 files changed

Lines changed: 319 additions & 3 deletions

File tree

exir/passes/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,9 @@
4040
from executorch.exir.passes.insert_write_back_for_buffers_pass import (
4141
insert_write_back_for_buffers_pass,
4242
)
43+
from executorch.exir.passes.legalize_portable_dim_order_pass import (
44+
LegalizePortableDimOrderPass,
45+
)
4346
from executorch.exir.passes.memory_format_ops_pass import MemoryFormatOpsPass
4447
from executorch.exir.passes.memory_planning_pass import MemoryPlanningPass
4548
from executorch.exir.passes.normalize_transpose_pass import NormalizeTransposePass
@@ -76,6 +79,7 @@
7679
"OpReplacePass",
7780
"ToDevicePass",
7881
"EdgeToBackendOpsPass",
82+
"LegalizePortableDimOrderPass",
7983
"MemoryFormatOpsPass",
8084
"MemoryPlanningPass",
8185
"HintBasedSymShapeEvalPass",
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
from typing import Optional
8+
9+
import executorch.exir.passes.dim_order_ops_registry # noqa: F401
10+
11+
import torch
12+
from executorch.exir.dialects._ops import ops as exir_ops
13+
from executorch.exir.dim_order_utils import get_dim_order
14+
from executorch.exir.pass_base import ExportPass, NodeMetadata, ProxyValue
15+
16+
17+
class LegalizePortableDimOrderPass(ExportPass):
18+
"""Insert contiguous dim-order copies before portable default-layout ops.
19+
20+
Runs during ``edge -> executorch`` lowering on leftover edge ops whose
21+
portable kernels require default dim order on their primary input.
22+
"""
23+
24+
_copy_op = exir_ops.edge.dim_order_ops._to_dim_order_copy.default
25+
_target_ops = {
26+
exir_ops.edge.aten._adaptive_avg_pool2d.default,
27+
exir_ops.edge.aten.avg_pool2d.default,
28+
exir_ops.edge.aten.expand_copy.default,
29+
exir_ops.edge.aten.mean.dim,
30+
exir_ops.edge.aten.sum.dim_IntList,
31+
}
32+
33+
def call_operator(self, op, args, kwargs, meta):
34+
if op not in self._target_ops:
35+
return super().call_operator(op, args, kwargs, meta)
36+
37+
input_arg = args[0]
38+
if isinstance(input_arg, ProxyValue) and input_arg.is_tensor():
39+
input_tensor: Optional[torch.Tensor] = input_arg.to_tensor()
40+
input_meta = NodeMetadata(input_arg.node.meta)
41+
elif isinstance(input_arg, torch.Tensor):
42+
input_tensor = input_arg
43+
input_meta = meta.copy()
44+
else:
45+
input_tensor = None
46+
input_meta = meta.copy()
47+
48+
if input_tensor is None or tuple(
49+
int(d) for d in input_tensor.dim_order()
50+
) == tuple(range(input_tensor.dim())):
51+
return super().call_operator(op, args, kwargs, meta)
52+
53+
contiguous_dim_order = get_dim_order(
54+
torch.contiguous_format, input_tensor.dim()
55+
)
56+
assert contiguous_dim_order is not None
57+
58+
legalized_input = super().call_operator(
59+
self._copy_op,
60+
(input_arg,),
61+
{"dim_order": contiguous_dim_order},
62+
input_meta,
63+
)
64+
return super().call_operator(
65+
op,
66+
(legalized_input, *args[1:]),
67+
kwargs,
68+
meta,
69+
)

exir/program/_program.py

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@
4545
convert_constant_dim_order_pass,
4646
dead_code_elimination_pass,
4747
EdgeToBackendOpsPass,
48+
LegalizePortableDimOrderPass,
4849
MemoryFormatOpsPass,
4950
OpReplacePass,
5051
remove_unused_parameters_pass,
@@ -405,12 +406,14 @@ def __init__(
405406
self,
406407
exported_program: ExportedProgram,
407408
after_to_edge_passes: bool,
409+
compile_config: Optional[EdgeCompileConfig] = None,
408410
):
409411
self.exported_program = exported_program
410412

411413
# Add a flag to denote whehter to_edge is called on this program
412414
# to detect misusage of directly calling to_executorch without to_edge
413415
self.after_to_edge_passes = after_to_edge_passes
416+
self.compile_config = compile_config or EdgeCompileConfig()
414417

415418
def transform(self, *passes: PassType) -> "ExirExportedProgram":
416419
self.exported_program = _transform(self.exported_program, *passes)
@@ -441,7 +444,9 @@ def to_executorch(
441444
raise RuntimeError("Must run to_edge before to_executorch.")
442445
config = config or ExecutorchBackendConfig()
443446
new_gm = self.exported_program.graph_module
444-
for p in edge_to_executorch_passes(config):
447+
for p in edge_to_executorch_passes(
448+
config, edge_compile_config=self.compile_config
449+
):
445450
new_gm_res = p(new_gm)
446451
assert new_gm_res is not None
447452
new_gm = new_gm_res.graph_module
@@ -478,6 +483,7 @@ def __deepcopy__(
478483
new_eep = ExirExportedProgram(
479484
copy.deepcopy(self.exported_program, memo),
480485
self.after_to_edge_passes,
486+
copy.deepcopy(self.compile_config, memo),
481487
)
482488
return new_eep
483489

@@ -673,6 +679,7 @@ def _to_edge(ep, config: EdgeCompileConfig) -> "ExirExportedProgram":
673679
],
674680
),
675681
False,
682+
config,
676683
)
677684
pre_op_replace_passes, post_op_replace_passes = _get_aten_to_edge_passes(config)
678685

@@ -753,7 +760,9 @@ def pre_memory_planning_passes(
753760

754761

755762
def edge_to_executorch_passes(
756-
config: ExecutorchBackendConfig, name: Optional[str] = None
763+
config: ExecutorchBackendConfig,
764+
name: Optional[str] = None,
765+
edge_compile_config: Optional[EdgeCompileConfig] = None,
757766
) -> List[PassType]:
758767
"""
759768
Returns a list of passes to lower from edge to executorch.
@@ -766,6 +775,7 @@ def edge_to_executorch_passes(
766775
else:
767776
device_cfg = propagate_device_config
768777

778+
edge_compile_config = edge_compile_config or EdgeCompileConfig()
769779
passes: List[PassType] = [
770780
# ExecuTorch backend ops are unable to handle unbacked symints. So after
771781
# this pass, passes cannot be Interpreter-based, because it will fail if
@@ -781,6 +791,9 @@ def edge_to_executorch_passes(
781791
RemoveGraphAssertsPass(),
782792
] + pre_memory_planning_passes(config, name)
783793

794+
if not edge_compile_config._skip_dim_order:
795+
passes.insert(len(config.passes), LegalizePortableDimOrderPass())
796+
784797
return passes
785798

786799

@@ -1702,7 +1715,9 @@ def to_executorch( # noqa (FLAKE8) C901
17021715
program = unsafe_remove_auto_functionalized_pass(program)
17031716
gm, new_signature = insert_write_back_for_buffers_pass(program)
17041717
new_gm = program.graph_module
1705-
for p in edge_to_executorch_passes(config, name):
1718+
for p in edge_to_executorch_passes(
1719+
config, name, edge_compile_config=self.compile_config
1720+
):
17061721
new_gm_res = p(new_gm)
17071722
assert new_gm_res is not None
17081723
new_gm = new_gm_res.graph_module

0 commit comments

Comments
 (0)