Skip to content

Insert write-back copy_ nodes at the earliest safe point - #20744

Open
JPL11 wants to merge 13 commits into
pytorch:mainfrom
JPL11:early-write-back-copies
Open

JPL11 wants to merge 13 commits into
pytorch:mainfrom
JPL11:early-write-back-copies

Conversation

@JPL11

@JPL11 JPL11 commented Jul 6, 2026

Copy link
Copy Markdown

Fixes #7345

Summary

insert_write_back_for_buffers_pass placed every write-back copy_ at the end of the graph, arbitrarily extending the lifetime of the value being written back and wasting space in the memory plan.

Each copy_(buffer, value) is now inserted at the earliest point that preserves the end-of-graph semantics. The copy must come after:

  • the value itself,
  • every reader of the buffer or any alias of it (they must observe the old contents) — this is the alias hazard that blocked the earlier attempt on the issue,
  • every mutation of the value or any alias of it (so we snapshot the final value),
  • all placeholders.

Aliases are found with a forward walk using schema alias_info; getitem, submodule calls, and schema-less targets are treated conservatively as aliasing, and mutation detection uses alias_info.is_write (conservative when unknown). This matters because reinplace_pass runs before this pass, so in-place ops can be present; views are still view_copy at this point since ReplaceViewCopyWithViewPass runs later.

If the value written back by one copy may alias the buffer mutated by another, the copies' relative order matters, so in that case all copies fall back to the old end-of-graph placement in their original order.

Test plan

Two new tests in exir/tests/test_passes.py:

  • test_mutable_buffers_write_back_is_inserted_early: the copy_ lands immediately after the value it writes back, before the rest of the graph.
  • test_mutable_buffers_write_back_after_old_value_reads: regression test for the alias/old-read hazard — a read of the buffer's old value traced after the new value is computed keeps the write-back late.

Existing test_mutable_buffers passes (its "After" graph comment updated for the new placement). Full exir/tests/test_passes.py, test_memory_planning.py, and emit/test/test_emit.py pass locally (one pre-existing failure on clean main, test_to_out_variant_none_output, unrelated).

cc @JacobSzwejbka @angelayi @metascroy

The insert_write_back_for_buffers pass placed every write-back copy_ at
the end of the graph, arbitrarily extending the lifetime of the value
being written back and wasting space in the memory plan.

Now each copy_(buffer, value) is inserted at the earliest point that
preserves the end-of-graph semantics: after the value is computed, after
every reader of the buffer or any alias of it (they must observe the old
contents), and after any mutation of the value or any alias of it (so we
snapshot the final value). Aliases are found with a forward walk using
schema alias_info, treating getitem, submodule calls, and schema-less
targets conservatively. If the value written back by one copy may alias
the buffer mutated by another, all copies fall back to the old
end-of-graph placement in their original order.

Fixes pytorch#7345
Copilot AI lite review requested due to automatic review settings July 6, 2026 17:14
@pytorch-bot

pytorch-bot Bot commented Jul 6, 2026

Copy link
Copy Markdown

🔗 Helpful Links

🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/20744

Note: Links to docs will display an error until the docs builds have been completed.

⚠️ 15 Awaiting Approval

As of commit 136a063 with merge base d32fa30 (image):

AWAITING APPROVAL - The following workflows need approval before CI can run:

This comment was automatically generated by Dr. CI and updates every 15 minutes.

@meta-cla

meta-cla Bot commented Jul 6, 2026

Copy link
Copy Markdown

Hi @JPL11!

Thank you for your pull request and welcome to our community.

Action Required

In order to merge any pull request (code, docs, etc.), we require contributors to sign our Contributor License Agreement, and we don't seem to have one on file for you.

Process

In order for us to review and merge your suggested changes, please sign at https://code.facebook.com/cla. If you are contributing on behalf of someone else (eg your employer), the individual CLA may not be sufficient and your employer may need to sign the corporate CLA.

Once the CLA is signed, our tooling will perform checks and validations. Afterwards, the pull request will be tagged with CLA signed. The tagging process may take up to 1 hour after signing. Please give it that time before contacting us about it.

If you have received this in error or have any questions, please contact us at cla@meta.com. Thanks!

@linux-foundation-easycla

linux-foundation-easycla Bot commented Jul 6, 2026

Copy link
Copy Markdown

CLA Signed
The committers listed above are authorized under a signed CLA.

  • ✅ login: JPL11 / name: Jacky Li (1af26c7)

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

This PR needs a release notes: label

If your change should be included in the release notes (i.e. would users of this library care about this change?), please use a label starting with release notes:. This helps us keep track and include your important work in the next release notes.

To add a label, you can comment to pytorchbot, for example
@pytorchbot label "release notes: none"

For more information, see
https://github.com/pytorch/pytorch/wiki/PyTorch-AutoLabel-Bot#why-categorize-for-release-notes-and-how-does-it-work.

Copilot AI left a comment

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.

Pull request overview

This PR updates insert_write_back_for_buffers_pass so write-back aten.copy_ nodes for mutated buffers/inputs are inserted at the earliest safe point in the FX graph (instead of always at the end), reducing live ranges and improving downstream memory planning.

Changes:

  • Add schema-driven alias/mutation analysis utilities to determine the earliest safe insertion point for each write-back copy_.
  • Insert write-backs earlier when independent, while falling back to end-of-graph insertion when write-backs may interfere via aliasing.
  • Extend exir/tests/test_passes.py with new regression tests covering early insertion and “old value read” alias hazards, and update the existing test_mutable_buffers expected graph comment.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
exir/passes/insert_write_back_for_buffers_pass.py Implements earliest-safe write-back insertion with conservative alias/mutation analysis and an independence fallback.
exir/tests/test_passes.py Adds/updates tests to validate earlier insertion and alias-hazard ordering constraints.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread exir/passes/insert_write_back_for_buffers_pass.py Outdated
Comment thread exir/passes/insert_write_back_for_buffers_pass.py
@nil-is-all nil-is-all added the module: exir Issues related to Export IR and the code under exir/ label Jul 10, 2026
Copilot AI review requested due to automatic review settings July 10, 2026 18:51

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@JPL11

JPL11 commented Jul 10, 2026

Copy link
Copy Markdown
Author

Pushed the ufmt formatting fix for the lintrunner job. The lintrunner-mypy failures are in backends/cortex_m/quantizer/pattern_matcher.py and backends/arm/_passes/arm_pass_utils.py, which this PR does not touch, so those look like they need a fix on main.

@JPL11

JPL11 commented Jul 10, 2026

Copy link
Copy Markdown
Author

Triaged the six failures in the pull workflow run; none are from this diff:

  • unittest / linux, unittest-editable / linux, test-arm-backend-no-driver (test_pytest_ops_tosa): all die in environment setup building the tosa-tools serialization wheel (ERROR: Use build.verbose instead of cmake.verbose for scikit-build-core >= 0.10). Looks like a new scikit-build-core release broke that third party package today; the exir test suites never ran.
  • android / build-android and unittest-nxp-neutron (2h timeout): both also failed on main's last completed pull run (29037897153), so pre existing.
  • test-binary-size-linux-gcc: Fail 52168 > 48500. That measures the compiled C++ size_test binary; this PR only touches exir/*.py, so it cannot move that number. The threshold was last calibrated 2026-03-06 and something on main appears to have outgrown it.

For what it is worth, the write back tests and the full exir/tests/test_passes.py pass locally on this branch.

@nil-is-all

Copy link
Copy Markdown
Contributor

Thanks @JPL11, some CI failures are irrelevant. I'll trigger it again just-in-case. Could you sign the MetaCLA agreement if you haven't yet?

Copilot AI review requested due to automatic review settings July 11, 2026 23:58

Copilot AI left a comment

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.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@JPL11

JPL11 commented Jul 11, 2026

Copy link
Copy Markdown
Author

Thanks @nil-is-all! CLA is signed already.

The re-run did surface one real lint hit in my file that I had missed (flake8 B007, an unused loop variable from an enumerate I no longer needed), fixed and pushed. lintrunner --skip MYPY is clean locally on both changed files now. The remaining red jobs should be the external ones from the triage above (tosa-tools wheel setup, android and nxp from main, the binary size threshold).

@JPL11 JPL11 closed this Jul 12, 2026
@JPL11 JPL11 reopened this Jul 12, 2026
@meta-cla

meta-cla Bot commented Jul 12, 2026

Copy link
Copy Markdown

Thank you for signing our Contributor License Agreement. We can now accept your code for this (and any) Meta Open Source project. Thanks!

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jul 12, 2026

@ErenAta16 ErenAta16 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Read this because #7345 has been picked up and dropped three times and this is the
first branch that actually implements it with tests. The approach is right: doing
a conservative alias walk before choosing the insertion point is the part that
makes the optimisation safe, and _may_alias_input and _mutates_input both
default to True when the schema cannot tell you, which is the correct direction to
fail in. node_order being built from enumerate(gm.graph.nodes) means the graph
is already topologically sorted, so the single forward pass in _collect_aliases
is sound rather than lucky.

The two tests are the right two. Asserting node_order[copy] == node_order[copy.args[1]] + 1
pins the optimisation, and test_mutable_buffers_write_back_after_old_value_reads
pins the safety condition that makes it non-trivial. That is more care than this
issue has had from anyone before.

One regression, and it is a crash rather than a behaviour change.
_insert_copy computes

last_placeholder = [node for node in gm.graph.nodes if node.op == "placeholder"][-1]

unconditionally at line 151, before anything checks whether there is a write-back
to insert. A graph with no placeholders makes that an IndexError. Modules with
no inputs are unusual but legal and torch.export handles them, so this is
reachable:

class NoInput(torch.nn.Module):
    def forward(self):
        return torch.ones(3) * 2

ep = export(NoInput().eval(), args=())
insert_write_back_for_buffers_pass(ep)

Run against the installed 1.4.1 pass and then against this branch's version of the
same file, same input:

main          placeholder count 0   pass completes cleanly
this branch   placeholder count 0   IndexError: list index out of range

insert_write_back_for_buffers_pass calls _insert_copy unconditionally at line
307, so there is no earlier return to save it. The graph in question has nothing
to write back, so today the pass is a no-op on it and afterwards it would throw.

Neither of the new tests catches this because both modules take an x. The
cheapest fix is to make the lookup tolerant rather than to guard the call site,
something like

placeholders = [node for node in gm.graph.nodes if node.op == "placeholder"]
last_placeholder = placeholders[-1] if placeholders else None

and then treating a None last_placeholder as "no placeholder floor" in
_insertion_point, where latest would start from return_node instead. A third
test with an input-free module would pin it.

One thing I could not check, so treat it as untested rather than as approval:
I did not verify the memory-planning win the issue asks for. I could not construct
a BUFFER_MUTATION output spec quickly outside your test harness, since raw
export alone did not produce one for me and your tests go through to_edge. So
I have verified the safety of the insertion logic by reading and the regression by
running, and I have taken the optimisation itself on the strength of your first
test rather than measuring a memory plan.

Environment for the runs above: executorch 1.4.1, torch 2.14.0+cpu, Python
3.12.10, Windows 11.

Copilot AI review requested due to automatic review settings September 16, 2026 22:28

Copilot AI left a comment

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.

🟢 Approval recommended

No unresolved review comments remain, and all reviewed changes are approval-ready.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 17, 2026 22:00

Copilot AI left a comment

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.

🟡 Changes recommended

A critical alias-analysis issue can place a write-back before a later mutation.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

exir/passes/insert_write_back_for_buffers_pass.py:47

  • _may_alias_input discards which input carries the schema's alias set. For example, aten.expand_as returns an alias of self, not its other input; if the mutable buffer is other, this marks the expanded self result and all of its users as buffer aliases. That can move an otherwise safe write-back past unrelated work or trigger the multi-copy fallback, defeating the earliest-safe placement. Match the return alias set to the specific input, using the broad result only when that relationship is unavailable.
    schema = getattr(node.target, "_schema", None)
    if schema is None:
        return True
    return any(ret.alias_info is not None for ret in schema.returns)
  • Files reviewed: 3/3 changed files
  • Comments generated: 2
  • Review effort level: Lite

Comment thread exir/passes/insert_write_back_for_buffers_pass.py Outdated
Comment thread exir/passes/insert_write_back_for_buffers_pass.py Outdated
Copilot AI review requested due to automatic review settings September 17, 2026 22:16

Copilot AI left a comment

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.

🔵 Needs a closer look

Two moderate findings remain regarding alias-set precision and alias-analysis performance.

Review details

Suppressed comments (2)

exir/passes/insert_write_back_for_buffers_pass.py:110

  • Because _may_alias_input is only a boolean, this branch adds every FX input of any schema-known aliasing op to the alias set. For an op such as view_as or expand_as, the second tensor supplies shape and does not alias the first tensor; if it is used later, _insertion_point waits for those unrelated users, and in multi-buffer cases this can create a false intersection that forces the whole write-back group to the end. Match return/input alias sets from the schema and reserve the all-input fallback for targets whose aliasing is actually unknown.
                if _may_alias_input(node):
                    for arg in node.all_input_nodes:
                        if arg not in aliases:
                            aliases.add(arg)
                            changed = True

exir/passes/insert_write_back_for_buffers_pass.py:104

  • This fixed-point walk can become quadratic on a long alias chain: because node_order is traversed forward, a seed near the end discovers only one predecessor per scan, so the loop rescans the whole graph for each backward step. _collect_aliases is also recomputed multiple times per mutable output (during independence checking and insertion), which can make export time grow substantially for large graphs with many views or mutable outputs. Consider a worklist over producer/user edges and caching each closure per seed.
    changed = True
    while changed:
        changed = False
        for node in node_order:
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Copilot AI review requested due to automatic review settings September 17, 2026 22:28
@JPL11

JPL11 commented Sep 17, 2026

Copy link
Copy Markdown
Author

Addressed the two findings from the latest Copilot review in 1303796. Precision: the alias walk is now schema-driven — _aliasing_inputs matches argument alias sets against return alias sets, so a shape-supplying argument (e.g. view_as/expand_as second arg) no longer joins the closure; the all-inputs fallback is kept only for targets whose aliasing is genuinely unknown (no schema, getitem, submodule calls), and view_copy aliases exactly its base. Performance: the undirected alias adjacency is built once per graph and each closure is a cached BFS (_AliasIndex), replacing the per-seed fixed-point rescan; closures are shared across independence checking and insertion-point selection. All existing write-back tests pass unchanged, including the aliased-fallback case, which still degrades to end-of-graph insertion.

Copilot AI left a comment

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.

🔵 Needs a closer look

Alias- and mutation-sensitive graph reordering warrants final human review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

…_inputs (C901); rename shadowing loop var (F402)
Copilot AI review requested due to automatic review settings September 17, 2026 23:18

Copilot AI left a comment

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.

🟡 Changes recommended

Address the CI typing issue and avoid unnecessary alias analysis before approval.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

exir/passes/insert_write_back_for_buffers_pass.py:253

  • When a graph has no write-back candidates, this still builds node_order and walks every node to construct the alias index. Because this pass runs for every exported program, models with no buffer/user-input copies now pay the full schema/alias-analysis cost without changing the graph; return early when mutated_outputs is all None before constructing these analyses.
    alias_index = _AliasIndex(list(gm.graph.nodes))
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread exir/passes/insert_write_back_for_buffers_pass.py Outdated
Copilot AI review requested due to automatic review settings September 17, 2026 23:28

Copilot AI left a comment

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.

🟡 Changes recommended

Alias analysis must conservatively handle incomplete non-ATen schemas.

Get a fresh assessment by requesting another Copilot review.

Review details

Suppressed comments (1)

exir/passes/insert_write_back_for_buffers_pass.py:133

  • Because this accepts any schema-backed target, a custom op with a schema but no Tensor(a!) annotation falls through as non-mutating. That can let the write-back run before a custom op mutates an alias of return_node, leaving the buffer with a stale value; the existing CSE policy treats non-aten:: schemas as untrusted for this reason. Treat non-ATen/unknown schemas conservatively (or otherwise reject missing mutation metadata).
    schema = getattr(node.target, "_schema", None)
    if schema is None:
        return True
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread exir/passes/insert_write_back_for_buffers_pass.py
Copilot AI review requested due to automatic review settings September 17, 2026 23:40

Copilot AI left a comment

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.

🟡 Changes recommended

Aliasing write-back destinations need a conservative fallback to preserve their ordering.

Get a fresh assessment by requesting another Copilot review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment thread exir/passes/insert_write_back_for_buffers_pass.py Outdated
Copilot AI review requested due to automatic review settings September 17, 2026 23:50

Copilot AI left a comment

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.

🔵 Needs a closer look

The alias-aware ordering and fallback behavior require final human review.

Review details
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. module: exir Issues related to Export IR and the code under exir/

Projects

None yet

Development

Successfully merging this pull request may close these issues.

insert_write_back_for_buffers_pass should inject copy_ nodes at the earliest possible spot.

5 participants