Erase the cat/slice/select nop ops after memory planning (#22651) - #22651
Erase the cat/slice/select nop ops after memory planning (#22651)#22651mcremon-meta wants to merge 1 commit into
Conversation
🔗 Helpful Links🧪 See artifacts and rendered test results at hud.pytorch.org/pr/pytorch/executorch/22651
Note: Links to docs will display an error until the docs builds have been completed. ✅ No FailuresAs of commit 48b0e78 with merge base a33ac10 ( This comment was automatically generated by Dr. CI and updates every 15 minutes. |
|
@mcremon-meta has exported this pull request. If you are a Meta employee, you can view the originating Diff in D118922409. |
This PR needs a
|
Summary:
`_cat_nop`, `_slice_copy_nop` and `_select_copy_nop` do nothing at runtime. A
cat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
**Why it has to live inside the memory planning pass.** A standalone pass is
not possible:
- Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
- After planning nothing may run at all:
# WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
# THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
# THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
`CadenceMemoryPlanning.run` already mutates the graph after `mem_planning.run`
- `SimplifyIdmaOpsPass` retargets nodes and runs dead code elimination there -
so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
**The assumptions that warning refers to are real.** Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips `find_peak_memory_usage` and, in executorch/util, the activation memory
profiler. `update_all_tensors_lifetime` alone does not fix it, because
`update_tensor_lifetime` only ever widens a lifetime:
end = node_idx if end is None or end < node_idx else end
so a stale larger bound survives. `_refresh_lifetimes` uses the first call to
identify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside `call` rather than at class scope, because
their schemas come from `ops_registrations` and `memory_planning` does not
import it - resolving at import time breaks any module that imports
`CadenceMemoryPlanning` without having registered the ops first.
**The kernels stay, deliberately.** They are dead for anything compiled after
this, but 14 checked-in `.pte` artifacts under `arvr/libraries/audio/xr2/` and
`arvr/projects/audio/research/tacf/xr2/` were built before it and still contain
`aten::_cat_nop` / `aten::_slice_copy_nop` instructions. Removing the kernels
makes those models abort at load with "Missing operator". Deleting them is a
follow-up, once every prebuilt artifact has been regenerated. Nothing here
changes what an existing `.pte` can run.
Measured on wakeword stage 2 split_n9, RT600 (HiFi4), one frame, per-op
profiling **off** (profiled totals are not usable here: instrumentation costs
roughly a fixed amount per instruction, so deleting instructions deletes
instrumentation that would not exist in production, overstating this by ~5x):
| configuration | total cycles | .pte |
| before | 689,335 | 510,800 |
| after | 680,790 | 509,392 |
8,545 cycles, 1.23%, and 1,408 bytes. Ten instructions go away.
Differential Revision: D118922409
6e2a72e to
2a14267
Compare
Summary:
`_cat_nop`, `_slice_copy_nop` and `_select_copy_nop` do nothing at runtime. A
cat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
**Why it has to live inside the memory planning pass.** A standalone pass is
not possible:
- Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
- After planning nothing may run at all:
# WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
# THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
# THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
`CadenceMemoryPlanning.run` already mutates the graph after `mem_planning.run`
- `SimplifyIdmaOpsPass` retargets nodes and runs dead code elimination there -
so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
**The assumptions that warning refers to are real.** Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips `find_peak_memory_usage` and, in executorch/util, the activation memory
profiler. `update_all_tensors_lifetime` alone does not fix it, because
`update_tensor_lifetime` only ever widens a lifetime:
end = node_idx if end is None or end < node_idx else end
so a stale larger bound survives. `_refresh_lifetimes` uses the first call to
identify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside `call` rather than at class scope, because
their schemas come from `ops_registrations` and `memory_planning` does not
import it - resolving at import time breaks any module that imports
`CadenceMemoryPlanning` without having registered the ops first.
**The kernels stay, deliberately.** They are dead for anything compiled after
this, but 14 checked-in `.pte` artifacts under `arvr/libraries/audio/xr2/` and
`arvr/projects/audio/research/tacf/xr2/` were built before it and still contain
`aten::_cat_nop` / `aten::_slice_copy_nop` instructions. Removing the kernels
makes those models abort at load with "Missing operator". Deleting them is a
follow-up, once every prebuilt artifact has been regenerated. Nothing here
changes what an existing `.pte` can run.
Measured on wakeword stage 2 split_n9, RT600 (HiFi4), one frame, per-op
profiling **off** (profiled totals are not usable here: instrumentation costs
roughly a fixed amount per instruction, so deleting instructions deletes
instrumentation that would not exist in production, overstating this by ~5x):
| configuration | total cycles | .pte |
| before | 689,335 | 510,800 |
| after | 680,790 | 509,392 |
8,545 cycles, 1.23%, and 1,408 bytes. Ten instructions go away.
Differential Revision: D118922409
2a14267 to
104aff1
Compare
Summary:
`_cat_nop`, `_slice_copy_nop` and `_select_copy_nop` do nothing at runtime. A
cat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
**Why it has to live inside the memory planning pass.** A standalone pass is
not possible:
- Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
- After planning nothing may run at all:
# WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
# THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
# THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
`CadenceMemoryPlanning.run` already mutates the graph after `mem_planning.run`
- `SimplifyIdmaOpsPass` retargets nodes and runs dead code elimination there -
so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
**The assumptions that warning refers to are real.** Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips `find_peak_memory_usage` and, in executorch/util, the activation memory
profiler. `update_all_tensors_lifetime` alone does not fix it, because
`update_tensor_lifetime` only ever widens a lifetime:
end = node_idx if end is None or end < node_idx else end
so a stale larger bound survives. `_refresh_lifetimes` uses the first call to
identify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside `call` rather than at class scope, because
their schemas come from `ops_registrations` and `memory_planning` does not
import it - resolving at import time breaks any module that imports
`CadenceMemoryPlanning` without having registered the ops first.
**The kernels stay, deliberately.**
Differential Revision: D118922409
104aff1 to
48b9750
Compare
Summary:
`_cat_nop`, `_slice_copy_nop` and `_select_copy_nop` do nothing at runtime. A
cat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
**Why it has to live inside the memory planning pass.** A standalone pass is
not possible:
- Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
- After planning nothing may run at all:
# WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
# THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
# THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
`CadenceMemoryPlanning.run` already mutates the graph after `mem_planning.run`
- `SimplifyIdmaOpsPass` retargets nodes and runs dead code elimination there -
so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
**The assumptions that warning refers to are real.** Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips `find_peak_memory_usage` and, in executorch/util, the activation memory
profiler. `update_all_tensors_lifetime` alone does not fix it, because
`update_tensor_lifetime` only ever widens a lifetime:
end = node_idx if end is None or end < node_idx else end
so a stale larger bound survives. `_refresh_lifetimes` uses the first call to
identify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside `call` rather than at class scope, because
their schemas come from `ops_registrations` and `memory_planning` does not
import it - resolving at import time breaks any module that imports
`CadenceMemoryPlanning` without having registered the ops first.
**The kernels stay, deliberately.**
Differential Revision: D118922409
48b9750 to
d768a9c
Compare
Summary:
`_cat_nop`, `_slice_copy_nop` and `_select_copy_nop` do nothing at runtime. A
cat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
**Why it has to live inside the memory planning pass.** A standalone pass is
not possible:
- Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
- After planning nothing may run at all:
# WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
# THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
# THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
`CadenceMemoryPlanning.run` already mutates the graph after `mem_planning.run`
- `SimplifyIdmaOpsPass` retargets nodes and runs dead code elimination there -
so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
**The assumptions that warning refers to are real.** Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips `find_peak_memory_usage` and, in executorch/util, the activation memory
profiler. `update_all_tensors_lifetime` alone does not fix it, because
`update_tensor_lifetime` only ever widens a lifetime:
end = node_idx if end is None or end < node_idx else end
so a stale larger bound survives. `_refresh_lifetimes` uses the first call to
identify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside `call` rather than at class scope, because
their schemas come from `ops_registrations` and `memory_planning` does not
import it - resolving at import time breaks any module that imports
`CadenceMemoryPlanning` without having registered the ops first.
**The kernels stay, deliberately.**
Reviewed By: DrJessop
Differential Revision: D118922409
d768a9c to
78b124b
Compare
Summary:
`_cat_nop`, `_slice_copy_nop` and `_select_copy_nop` do nothing at runtime. A
cat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
**Why it has to live inside the memory planning pass.** A standalone pass is
not possible:
- Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
- After planning nothing may run at all:
# WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
# THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
# THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
`CadenceMemoryPlanning.run` already mutates the graph after `mem_planning.run`
- `SimplifyIdmaOpsPass` retargets nodes and runs dead code elimination there -
so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
**The assumptions that warning refers to are real.** Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips `find_peak_memory_usage` and, in executorch/util, the activation memory
profiler. `update_all_tensors_lifetime` alone does not fix it, because
`update_tensor_lifetime` only ever widens a lifetime:
end = node_idx if end is None or end < node_idx else end
so a stale larger bound survives. `_refresh_lifetimes` uses the first call to
identify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside `call` rather than at class scope, because
their schemas come from `ops_registrations` and `memory_planning` does not
import it - resolving at import time breaks any module that imports
`CadenceMemoryPlanning` without having registered the ops first.
**The kernels stay, deliberately.**
Reviewed By: DrJessop
Differential Revision: D118922409
78b124b to
48b0e78
Compare
Summary:
_cat_nop,_slice_copy_nopand_select_copy_nopdo nothing at runtime. Acat is only rewritten to its nop form once memory planning can place every
input at a contiguous offset inside the output, and a slice or select likewise
once its output can be colocated inside its input, so by the time planning has
run they hold by construction. The instructions that remain are pure dispatch
overhead.
This erases them, pointing their consumers at the output buffer the producers
have already written in place. A nop that cannot be erased raises rather than
being skipped: these ops exist only to carry a placement constraint from
constraint generation to here, so one reaching the emitter is a compiler bug.
Why it has to live inside the memory planning pass. A standalone pass is
not possible:
Before planning the nodes cannot go, because the node is what carries the
placement constraint. Remove it early and the aliasing never happens.
After planning nothing may run at all:
WARNING: DO NOT ADD ANY MORE PASSES AFTER MEMORY PLANNING PASS.
THERE ARE A LOT OF ASSUMPTIONS IN THE STACK THAT MEMORY PLANNING IS
THE LAST PASS BEFORE THE EMITTER.
(exir/program/_program.py)
CadenceMemoryPlanning.runalready mutates the graph aftermem_planning.runSimplifyIdmaOpsPassretargets nodes and runs dead code elimination there -so that slot is the one point where placement is decided but the program is not
yet emitted. This joins it.
The assumptions that warning refers to are real. Spec lifetimes are node
indices, so erasing nodes leaves them pointing past the end of the graph. That
trips
find_peak_memory_usageand, in executorch/util, the activation memoryprofiler.
update_all_tensors_lifetimealone does not fix it, becauseupdate_tensor_lifetimeonly ever widens a lifetime:so a stale larger bound survives.
_refresh_lifetimesuses the first call toidentify exactly which specs the recompute reaches, clears those, and rebuilds
them. Clearing a wider set would leave specs the recompute never revisits stuck
at None, which reads as "no lifetime" and silently drops them from the memory
reports. With that, no change is needed outside the Cadence backend - the shared
executorch diagnostics work unmodified.
The nop targets are looked up inside
callrather than at class scope, becausetheir schemas come from
ops_registrationsandmemory_planningdoes notimport it - resolving at import time breaks any module that imports
CadenceMemoryPlanningwithout having registered the ops first.The kernels stay, deliberately.
Reviewed By: DrJessop
Differential Revision: D118922409