Add OpBackend, for backends that lower by rewriting operators - #22366
Add OpBackend, for backends that lower by rewriting operators#22366rascani wants to merge 1 commit into
Conversation
A `Partitioner` may only tag nodes -- `to_backend` asserts the graph it returns is identical -- so a backend whose kernels replace operators in place has no interface of its own. Arm's `ReplaceQuantNodesPass` and the Cortex-M pass manager each reach for a different hook; #21516 asks for a third. `OpBackend.lower(ep, method_name) -> ep` is that interface, and `EdgeProgramManager.to_op_backend` applies one to every method. `to_executorch` is only reachable from a manager, so a manager method is the useful shape; it carries the constant methods and the etrecord across the rebuild. Unlike `to_backend` it also carries the rest of the compile config rather than replacing it, so `_skip_dim_order` -- which the explicit-layout work needs -- survives. That holds for a recipe with no partitioner, which is the op-library case this exists for; behind a delegate partitioner `to_backend` has already replaced the config before an operator backend runs, and giving composition its own answer is left to that work. Each backend gets a private copy of the program: a deep copy of the graph, and its own state dict, constants, graph signature and range constraints. The passes backends are built from rewrite in place, and the helper the contract points at for adding an input inserts into `graph_signature.input_specs`, so without the copy the caller's program -- still held as an earlier stage's artifact -- is rewritten too, and left half-rewritten if the backend raises. The weights the graph refers to are shared, not duplicated. Adding an input is the easy way to return a program its own graph signature no longer describes, so the returned program is checked before it is handed on: unchecked, that surfaces as a message-less assertion in a pre-emit pass or a bare `KeyError` from the emitter, naming neither the backend nor the input. `ExportedProgram.validate()` would be the obvious check and is the wrong one -- it re-enters the edge verifier, which rejects the operators an operator backend exists to install. Marked experimental: one implementer, one commit of history. The decorator on an ABC warns once per implementing module, at the `class` statement. Nothing calls this yet. The recipe support that does is the next commit. Authored with Claude Code.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22366
Note: Links to docs will display an error until the docs builds have been completed. ❌ 2 New Failures, 4 Unrelated FailuresAs of commit d6d7380 with merge base 6067c7a ( NEW FAILURES - The following jobs have failed:
FLAKY - The following job failed but was likely due to flakiness present on trunk:
BROKEN TRUNK - The following jobs failed but was present on the merge base:👉 Rebase onto the `viable/strict` branch to avoid these failures
This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
Summary
A
Partitionermay only tag nodes --to_backendasserts the graph it returnsis identical -- so a backend whose kernels replace operators in place has no
interface of its own. Arm's
ReplaceQuantNodesPassand the Cortex-M passmanager each reach for a different hook; #21516 asks for a third.
OpBackend.lower(ep, method_name) -> epis that interface, andEdgeProgramManager.to_op_backendapplies one to every method.to_executorchis only reachable from a manager, so a manager method is the useful shape; it
carries the constant methods and the etrecord across the rebuild.
Unlike
to_backendit also carries the rest of the compile config rather thanreplacing it, so
_skip_dim_order-- which the explicit-layout work needs --survives. That holds for a recipe with no partitioner, which is the
op-library case this exists for; behind a delegate partitioner
to_backendhas already replaced the config before an operator backend runs, and giving
composition its own answer is left to that work.
Each backend gets a private copy of the program: a deep copy of the graph, and
its own state dict, constants, graph signature and range constraints. The
passes backends are built from rewrite in place, and the helper the contract
points at for adding an input inserts into
graph_signature.input_specs, sowithout the copy the caller's program -- still held as an earlier stage's
artifact -- is rewritten too, and left half-rewritten if the backend raises.
The weights the graph refers to are shared, not duplicated.
Adding an input is the easy way to return a program its own graph signature no
longer describes, so the returned program is checked before it is handed on:
unchecked, that surfaces as a message-less assertion in a pre-emit pass or a
bare
KeyErrorfrom the emitter, naming neither the backend nor the input.ExportedProgram.validate()would be the obvious check and is the wrong one --it re-enters the edge verifier, which rejects the operators an operator backend
exists to install.
Marked experimental: one implementer, one commit of history. The decorator on
an ABC warns once per implementing module, at the
classstatement.Nothing calls this yet. The recipe support that does is the next commit.
Authored with Claude Code.