Add a capacity-aware memory planner over a target memory map - #21821
Draft
rascani wants to merge 1 commit into
Draft
Add a capacity-aware memory planner over a target memory map#21821rascani wants to merge 1 commit into
rascani wants to merge 1 commit into
Conversation
ExecuTorch plans tensors into numbered arenas and the runtime supports several of them, but nothing tells the planner how large an arena is. greedy honors whatever mem_id a custom pool pass assigned, puts everything else in arena 1, and over-subscription only surfaces as a link error or a runtime abort. That makes it awkward to target a part whose fast memory is a small tightly-coupled region alongside a larger SRAM. This adds TargetMemoryMap, an ordered list of banks each with a byte capacity, and banked_greedy, which plans against it: buffers fill the fastest bank that can hold them and spill to the next, and a buffer that fits nowhere fails the export rather than the device. With a single bank the plan is bit-identical to greedy, asserted per-spec at alignments 16, 32 and 256. The map is deliberately a subset of the target's pools. Declaring a bank hands that arena to the planner; an undeclared mem_id behaves exactly as it does under greedy, so a region that must hold only what a pass deliberately pinned there -- a DMA-visible pool, accelerator-private scratch -- is protected by leaving it out. Custom pool passes therefore compose: the mem_id they assign is honored as a pin, pins are placed before any unpinned buffer so nothing can take the space they need, and unpinned buffers still share storage with them. Reviewers may find these the least obvious parts. Placement is one pass in size-descending order, which is what gives each bank the ordering pick_shared_obj requires without any repacking; the sort keys on the incoming size, as greedy's does, because realigning first manufactures ties whose order then differs. The unpinned phase cannot call pick_shared_obj directly, because pins and unpinned buffers are each sorted but their concatenation is not, so _reusable_object applies the same two reuse rules read-only and filters undersized objects where that function asserts. A bank's alignment must be a multiple of the graph's, which is what keeps per-bank realignment order-preserving. Banking costs total bytes, and that is inherent rather than incidental: a shared object never spans banks, so once a buffer spills the sum of the arenas can exceed what greedy needed for one. On a 30-buffer graph with a 6 KiB fast bank that is 12288 against greedy's 10752. Pinning many buffers into a declared bank costs a little more again, since pins take first claim on its capacity. The trade buys residency in fast memory, not a smaller total. Nothing consumes this yet. The runtime still backs every arena from one pool, so per-bank regions in a runner are a follow-up, and an example belongs with that change rather than ahead of it. share_mutable_buffers is also deferred: it reserves arena 2 and needs a change to core memory planning. Authored with assistance from Claude Code.
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/21821
Note: Links to docs will display an error until the docs builds have been completed. ✅ No FailuresAs of commit 26aed9f with merge base a56af1c ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
This PR needs a
|
rascani
force-pushed
the
exir-banked-memory-planning
branch
from
August 13, 2026 18:36
26c58fb to
26aed9f
Compare
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Summary
ExecuTorch plans tensors into numbered arenas and the runtime supports several of them, but nothing tells the planner how large an arena is. greedy honors whatever mem_id a custom pool pass assigned, puts everything else in arena 1, and over-subscription only surfaces as a link error or a runtime abort. That makes it awkward to target a part whose fast memory is a small tightly-coupled region alongside a larger SRAM.
This adds TargetMemoryMap, an ordered list of banks each with a byte capacity, and banked_greedy, which plans against it: buffers fill the fastest bank that can hold them and spill to the next, and a buffer that fits nowhere fails the export rather than the device. With a single bank the plan is bit-identical to greedy, which is asserted per-spec at three alignments.
The map is deliberately a subset of the target's pools. Declaring a bank hands that arena to the planner; an undeclared mem_id behaves exactly as it does under greedy, so a region that must hold only what a pass deliberately pinned there -- a DMA-visible pool, accelerator-private scratch -- is protected by leaving it out. Custom pool passes therefore compose: the mem_id they assign is honored as a pin, pins are placed before any unpinned buffer so nothing can take the space they need, and unpinned buffers still share storage with them.
Reviewers may find these the least obvious parts. Placement is one pass in size-descending order, which is what gives each bank the ordering pick_shared_obj requires without any repacking. The unpinned phase cannot call pick_shared_obj directly, because pins and unpinned buffers are each sorted but their concatenation is not, so _reusable_object applies the same two reuse rules read-only and filters undersized objects where that function asserts. A bank's alignment must be a multiple of the graph's, which is what keeps per-bank realignment order-preserving.
Not covered here: the runtime still backs every arena from one pool, so per-bank regions in a runner are a follow-up, as is share_mutable_buffers, which reserves arena 2.
Authored with assistance from Claude Code.