Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions exir/backend/partitioner.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -112,3 +113,27 @@ def ops_to_not_decompose(
in the list returned by ops_to_not_decompose.
"""
return ([], None)

def transform_for_pre_decomposition(
self, exported_program: ExportedProgram
) -> ExportedProgram:
"""Transform an ATen program before default decompositions run.

EXIR invokes this hook automatically from
``to_edge_transform_and_lower``. Backend partitioners may override it
to apply transformations that must run before decomposition. Callers
should use ``to_edge_transform_and_lower`` instead of invoking this
method directly.

When multiple partitioners are used for a method, their transforms run
sequentially in the order supplied to ``to_edge_transform_and_lower``.

Args:
exported_program (ExportedProgram): The ATen-dialect program to
transform.

Returns:
ExportedProgram: The transformed ATen-dialect program.

"""
return exported_program
14 changes: 12 additions & 2 deletions exir/program/_program.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Copyright (c) Meta Platforms, Inc. and affiliates.
# All rights reserved.
# Copyright 2025 Arm Limited and/or its affiliates.
# Copyright 2025-2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.
Expand Down Expand Up @@ -1114,6 +1114,14 @@ def _can_skip_using_EDGE_DO_NOT_DECOMP(
return check_op_support is None


def _apply_pre_decomposition_transforms(
program: ExportedProgram, partitioners: List[Partitioner]
) -> ExportedProgram:
for partitioner in partitioners:
program = partitioner.transform_for_pre_decomposition(program)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC do_not_decomp will complain if a partitioner asked for something to be not decomposed and then it didn't partition, is this true for this as well? I can imagine that as a good thing if we are planning to do some transforms here in this pass.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comment. No, we don't record the same info to check whether the generated ops remain undelegated after the following decomposition and partitioning. Tracking them may require a new metedata field.
transform_for_annotation_pipeline() also applies transforms without tracking generated nodes. Do you think we should introduce such tracking for this hook?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yeah quantizer being backend specific yet no direct enforcement for its actions is something I am not very happy about.

The reason I am a bit nervous this is, now we are modifying the graph in the paritioner and without any consequences if this hook misbehaves.

That said, with your hook, since its a pass, writing a '_sanity_check_graph_for_non_decomp_ops` like fn can get tricky if multiple passes or even multiple partitioners have worked on the graph.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ok let's hope this doesn't get out of control, I will stamp this as is.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thank you, we need to be more cautious when adding new passes to this hook.

return program


def _gen_edge_manager_for_partitioners(
partitioner: Dict[str, List[Partitioner]],
aten_programs: Dict[str, ExportedProgram],
Expand All @@ -1137,11 +1145,13 @@ def _gen_edge_manager_for_partitioners(
ops_set_to_not_decompose_by_program = defaultdict(list)
edge_programs: Dict[str, ExportedProgram] = {}
for name, program in aten_programs.items():
partitioners_for_program = partitioner.get(name, [])
program = _apply_pre_decomposition_transforms(program, partitioners_for_program)

# Functionalize program before asking partitioners to preserve ops
program = program.run_decompositions({})

if partitioner is not None:
partitioners_for_program = partitioner.get(name, [])
final_ops_to_preserve = set()

# Decompose by default if there are no partitioners for the method
Expand Down
54 changes: 54 additions & 0 deletions exir/tests/test_partitioner_pre_decomposition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Copyright 2026 Arm Limited and/or its affiliates.
#
# This source code is licensed under the BSD-style license found in the
# LICENSE file in the root directory of this source tree.

import torch
from executorch.exir import to_edge_transform_and_lower
from executorch.exir.backend.partitioner import Partitioner, PartitionResult
from torch.export import export, ExportedProgram


class _SDPA(torch.nn.Module):
def forward(
self, query: torch.Tensor, key: torch.Tensor, value: torch.Tensor
) -> torch.Tensor:
return torch.nn.functional.scaled_dot_product_attention(query, key, value)


class _RecordingPartitioner(Partitioner):
def __init__(self, name: str, calls: list[str]) -> None:
super().__init__()
self.name = name
self.calls = calls
self.saw_sdpa = False

def transform_for_pre_decomposition(
self, exported_program: ExportedProgram
) -> ExportedProgram:
self.calls.append(self.name)
self.saw_sdpa = any(
node.target == torch.ops.aten.scaled_dot_product_attention.default
for node in exported_program.graph.nodes
)
return exported_program

def partition(self, exported_program: ExportedProgram) -> PartitionResult:
return PartitionResult(exported_program, {})


def test_partitioner_transforms_run_before_decomposition_in_order() -> None:
inputs = tuple(torch.randn(1, 3, 4, 5) for _ in range(3))
exported_program = export(_SDPA(), inputs, strict=True)
calls: list[str] = []
first = _RecordingPartitioner("first", calls)
second = _RecordingPartitioner("second", calls)

to_edge_transform_and_lower(
exported_program,
partitioner=[first, second],
)

assert calls == ["first", "second"]
assert first.saw_sdpa
assert second.saw_sdpa
Loading