Repository navigation
[XNNPACK] Fold zero constant_pad_nd into convolutions, lower fp32<->fp16 casts as convert #24112
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
borys3kk
wants to merge
2
commits into
pytorch:main
Choose a base branch
from
borys3kk:xnnpack-fuse-pad-into-conv-fp16-convert-main
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+517
−11
Open
Changes from 1 commit
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,101 @@ | ||
| # 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. | ||
|
|
||
| from typing import cast, List, Tuple | ||
|
|
||
| import torch | ||
| from executorch.backends.xnnpack._passes.xnnpack_pass import XNNPACKPass | ||
| from executorch.backends.xnnpack.utils.quant_utils import is_dequant, is_quant | ||
| from executorch.exir.dialects._ops import ops as exir_ops | ||
| from executorch.exir.pass_base import PassResult | ||
|
|
||
|
|
||
| class FusePadIntoConvPass(XNNPACKPass): | ||
| """ | ||
| Folds a zero-valued constant_pad_nd on the two spatial dims into the | ||
| convolution that consumes it. XNNPACK convolutions take independent | ||
| top / right / bottom / left padding, so asymmetric pads such as | ||
| F.pad(x, (2, 1, 2, 1)) before a 4x4 filter fold as well, and the padded | ||
| copy of the activation (a full extra pass over the tensor, noticeably slow | ||
| in NHWC) disappears. | ||
|
|
||
| The graph is left intact so shape metadata stays valid: the pad node is | ||
| tagged FOLDED_PAD_TAG and its visitor aliases the pad output to the pad | ||
| input instead of emitting a node, and the convolution is tagged with the | ||
| combined (top, right, bottom, left) padding that its visitor serializes. | ||
|
|
||
| Must run after ChannelsLastTaggedReshapePass: if that pass had to insert | ||
| a layout conversion between the pad and the convolution, the pad's only | ||
| user is no longer the convolution and nothing is fused. | ||
| """ | ||
|
|
||
| FOLDED_PAD_TAG = "XNN_FOLDED_PAD" | ||
| FUSED_PADDING_TAG = "XNN_CONV_PADDING_TRBL" | ||
|
|
||
| @staticmethod | ||
| def get_fused_padding(node: torch.fx.Node): | ||
| return node.meta.get(FusePadIntoConvPass.FUSED_PADDING_TAG, None) | ||
|
|
||
| @staticmethod | ||
| def is_folded_pad(node: torch.fx.Node) -> bool: | ||
| return node.meta.get(FusePadIntoConvPass.FOLDED_PAD_TAG, False) | ||
|
|
||
| def _foldable(self, pad: torch.fx.Node) -> Tuple[bool, List[int]]: | ||
| """Returns (ok, [left, right, top, bottom]) for a 4D spatial zero pad.""" | ||
| if len(pad.users) != 1: | ||
| return False, [0, 0, 0, 0] | ||
| if len(pad.args) > 2 and cast(float, pad.args[2]) != 0.0: | ||
| return False, [0, 0, 0, 0] | ||
| src = pad.args[0] | ||
| if not isinstance(src, torch.fx.Node) or "val" not in src.meta: | ||
| return False, [0, 0, 0, 0] | ||
| if src.meta["val"].dim() != 4: | ||
| return False, [0, 0, 0, 0] | ||
| amounts = list(cast(List[int], pad.args[1])) | ||
| if len(amounts) % 2 or len(amounts) > 4 or any(a < 0 for a in amounts): | ||
| return False, [0, 0, 0, 0] | ||
| # Quantized graphs route the pad through q/dq pairs (InsertPadQDQPass); | ||
| # leave those alone. | ||
| if is_dequant(src) or any(is_quant(u) for u in pad.users): | ||
| return False, [0, 0, 0, 0] | ||
| amounts = amounts + [0] * (4 - len(amounts)) # [left, right, top, bottom] | ||
| return True, amounts | ||
|
|
||
| def call(self, graph_module: torch.fx.GraphModule): | ||
| modified = False | ||
| for pad in graph_module.graph.nodes: | ||
| if ( | ||
| pad.op != "call_function" | ||
| or pad.target != exir_ops.edge.aten.constant_pad_nd.default | ||
| ): | ||
| continue | ||
| ok, (left, right, top, bottom) = self._foldable(pad) | ||
| if not ok: | ||
| continue | ||
| conv = next(iter(pad.users)) | ||
| if ( | ||
| conv.op != "call_function" | ||
| or conv.target != exir_ops.edge.aten.convolution.default | ||
| or conv.args[0] is not pad | ||
| or conv.args[6] # transposed convolution pads differently | ||
| or self.get_fused_padding(conv) is not None | ||
| ): | ||
| continue | ||
| conv_padding = list(cast(List[int], conv.args[4])) | ||
| if len(conv_padding) == 1: | ||
| conv_padding = conv_padding + conv_padding | ||
| if len(conv_padding) != 2: | ||
| continue | ||
| conv.meta[self.FUSED_PADDING_TAG] = ( | ||
| conv_padding[0] + top, | ||
| conv_padding[1] + right, | ||
| conv_padding[0] + bottom, | ||
| conv_padding[1] + left, | ||
| ) | ||
| pad.meta[self.FOLDED_PAD_TAG] = True | ||
| modified = True | ||
| # Tags only, no graph change: the metadata stays as traced. | ||
| return PassResult(graph_module, modified) | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| # Copyright (c) Meta Platforms, Inc. and affiliates. | ||
|
borys3kk marked this conversation as resolved.
Outdated
|
||
| # 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. | ||
|
|
||
| import unittest | ||
|
|
||
| import torch | ||
| from executorch.backends.xnnpack.test.tester import Tester | ||
|
|
||
|
|
||
| class TestToCopyDtype(unittest.TestCase): | ||
| """fp32 <-> fp16 casts are lowered to XNNPACK convert nodes.""" | ||
|
|
||
| def setUp(self): | ||
| torch._dynamo.reset() | ||
|
|
||
| class Fp16Inner(torch.nn.Module): | ||
| """fp32 I/O contract around an fp16 model: the common deployment shape.""" | ||
|
|
||
| def __init__(self): | ||
| super().__init__() | ||
| self.conv = torch.nn.Conv2d(3, 8, 3, padding=1).half() | ||
|
|
||
| def forward(self, x): | ||
| return self.conv(x.to(torch.float16)).to(torch.float32) | ||
|
|
||
| class CastOnly(torch.nn.Module): | ||
| def forward(self, x): | ||
| return (x.to(torch.float16) * 2.0).to(torch.float32) | ||
|
|
||
| cast_ops = [ | ||
| "executorch_exir_dialects_edge__ops_dim_order_ops__to_dim_order_copy_default", | ||
| "executorch_exir_dialects_edge__ops_aten__to_copy_default", | ||
| ] | ||
|
|
||
| def _run(self, module, inputs): | ||
| ( | ||
| Tester(module.eval(), inputs) | ||
| .export() | ||
| .to_edge_transform_and_lower() | ||
| .check_not(self.cast_ops) | ||
| .check_count({"torch.ops.higher_order.executorch_call_delegate": 1}) | ||
| .to_executorch() | ||
| .serialize() | ||
| .run_method_and_compare_outputs(atol=2e-2, rtol=2e-2) | ||
| ) | ||
|
|
||
| def test_fp32_io_around_fp16_conv(self): | ||
| self._run(self.Fp16Inner(), (torch.randn(1, 3, 8, 8),)) | ||
|
|
||
| def test_cast_only(self): | ||
| self._run(self.CastOnly(), (torch.randn(2, 5),)) | ||
|
|
||
| def test_int_cast_not_partitioned(self): | ||
| class ToInt(torch.nn.Module): | ||
| def forward(self, x): | ||
| return x.to(torch.int32) | ||
|
|
||
| ( | ||
| Tester(ToInt().eval(), (torch.randn(2, 5),)) | ||
| .export() | ||
| .to_edge_transform_and_lower() | ||
| .check_not(["torch.ops.higher_order.executorch_call_delegate"]) | ||
| ) | ||
|
|
||
| def test_dtype_cast_with_dim_order_change_not_partitioned(self): | ||
| class ToHalfChannelsLast(torch.nn.Module): | ||
| def forward(self, x): | ||
| return x.to(torch.float16, memory_format=torch.channels_last) | ||
|
|
||
| ( | ||
| Tester(ToHalfChannelsLast().eval(), (torch.randn(1, 3, 4, 4),)) | ||
| .export() | ||
| .to_edge_transform_and_lower() | ||
| .check_not(["torch.ops.higher_order.executorch_call_delegate"]) | ||
| .check_count({self.cast_ops[0]: 1}) | ||
| ) | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.