AOTInductor Input Mutation
Introduction
AOTInductor is a PyTorch compiler backend that compiles PyTorch models into optimized inference engines. In my previous impression, I thought that AOTInductor relies on functionalization and does not support memory mutations. Any memory mutations will be a consequence of AOTInductor’s internal optimizations, and the user cannot completely control them. When it comes to inputs, I could not imagine that AOTInductor would allow users to mutate inputs in-place, because it would break the functionalization assumption. However, it turns out that I was wrong. Actually, AOTInductor strictly relies on functionalization, meaning it expects a clean mathematical graph without internal memory mutations or global side effects before executing its code-generation phase. It does not mean that optimized engine produced by AOTInductor code-generation cannot have side effects, such as in-place input mutations.
The motivation of input mutation is the scenario that sometimes we would just like to get an output tensor that only changes a very small fraction of a large input tensor. Out-of-place operations would require allocating a new tensor and copying the unchanged data from the input tensor to the output tensor, which is inefficient. In-place operations can avoid this overhead by directly modifying the input tensor. In this blog post, I will demonstrate how to enable in-place input mutation optimizations in AOTInductor.
AOTInductor Input Mutation
The key of using in-place input mutation operations is to explicitly use in-place mutations, such as x.mul_(2) instead of x.mul(2), for the input tensors in the PyTorch model. For custom Triton kernels, in addition to an implementation of Triton kernel that mutates the input tensor in-place, wrapping it with triton_op and wrap_triton, the mutates_args argument should be used to explicitly indicate which input arguments are mutated.
After exporting the model with torch.export.export(..., strict=True), the ExportedProgram will still have the in-place operation torch.ops.aten.mul_ in the graph, but the top-level graph signature will not have any user_inputs_to_mutate. After decomposing the ExportedProgram with run_decompositions, the decomposed ExportedProgram will become functionalized, and the in-place operation will be replaced with an out-of-place operation torch.ops.aten.mul. The input mutation side effect will then be tracked in the decomposed graph signature, and the input tensor will be listed in user_inputs_to_mutate. The decomposed ExportedProgram, actually as well as the original ExportedProgram, can then be compiled with AOTInductor, and AOTInductor will respect the input mutation side effect and generate an optimized engine that performs in-place input mutation.
1 | import os |
The input mutations can be confirmed by checking the user_inputs_to_mutate mapping in the graph signature of the decomposed ExportedProgram or just the decomposed ExportedProgram itself.
1 | $ python aoti_input_mutation_example.py |
Caveats
In many cases, the inputs being mutated are caches. A natural implementation would just create a PyTorch model that has internal buffers registered via self.register_buffer(...) and mutate those buffers in-place. The AOTInductor engine generated from such a model will have thread-safety issues, which is not immediately obvious, if multiple threads are running the same engine concurrently, because the buffers are shared across threads. The input mutation approach, however, is thread-safe, because each thread has its own input tensor to mutate.
1 | import os |
In the decomposed ExportedProgram, we could clearly see that the buffer counter is tracked in buffers_to_mutate, and the output spec for the mutated buffer is of kind BUFFER_MUTATION. Consequently, we should avoid mutating registered buffers in the PyTorch model.
1 | $ python buffer_mutation.py |
Conclusions
TensorRT allows input mutations, so does AOTInductor.
References
AOTInductor Input Mutation
https://leimao.github.io/blog/2026/2026-09-01-AOTInductor-Input-Mutation/