PyTorch Multi-Process Inference Weight Sharing Via Inter-Process Communication
Introduction
PyTorch is probably the most popular deep learning framework for model training. Ideally, without any additional effort, PyTorch model could be used for inference seamlessly in Python. However, in practice, there are some concerns when using PyTorch for inference in Python, primarily because of the Global Interpreter Lock (GIL) in Python. The GIL prevents multiple threads from executing Python bytecodes at once, which can lead to performance bottlenecks in high-throughput applications. To overcome this limitation, PyTorch models can be run in multiple processes, where each process runs its own Python interpreter and has its own GIL. However, because model weights cannot be natively shared across processes in Python, multi-process inference on single-GPU can lead to duplicated model weights in GPU VRAM, which can quickly exhaust the available GPU memory. To mitigate this issue, model weights can be shared across processes using CUDA Inter-Process Communication (IPC).
It turns out that PyTorch multiprocessing module has abstracted away the details of CUDA IPC, and provides a simple API to share CUDA tensors across processes. In this blog post, I will demonstrate how to use PyTorch multiprocessing module to share model weights across multiple processes for inference, and avoid weight duplication in GPU VRAM.
PyTorch Multi-Process Inference Weight Sharing
In the following example, I will demonstrate how to share model weights via CUDA IPC across multiple processes for PyTorch and AOTInductor inference using PyTorch multiprocessing module.
1 | import argparse |
To run the example, we could use the following command to start an NVIDIA PyTorch container with GPU support.
1 | $ docker run -it --rm --gpus all --ipc=host --ulimit memlock=-1 --ulimit stack=67108864 -v $(pwd):/mnt -w /mnt nvcr.io/nvidia/pytorch:26.07-py3 |
To run the example, we could use the following commands to run the PyTorch or AOTInductor multi-process inference with weight sharing via CUDA IPC.
1 | $ python pytorch_inference_ipc.py --mode aoti --batches 128 --workers 24 |
By default, each model instance will take 2 GB of GPU VRAM. My GPU is an NVIDIA RTX 5080 with 16 GB of GPU VRAM. If there is no weight sharing, I would not be able to run the example with 24 workers successfully.
There are a few key operations or caveats in the example:
- If using PyTorch multi-process inference, the model weights are shared across processes using
model.share_memory(). This allows the model weights to be shared across processes without duplicating them in GPU VRAM. Although it seems that without usingmodel.share_memory(), themultiprocessingmodule will still share the model weights across processes, but it is still a good practice to explicitly callmodel.share_memory()to ensure that the model weights are shared across processes. - To avoid materializing the model weights for AOTInductor packages in each worker process, to build the AOTInductor packages, we set the
aot_inductor.package_constants_in_sotoFalsein theinductor_configs. This will prevent the model weights from being packaged into the AOTInductor packages, and when the model is loaded in each worker process usingtorch._inductor.aoti_load_package, the no model weights will be loaded from the package. Consequently, the GPU VRAM will not be inflated by model loading from many processes. Then, we explicitly load the model weights from the parent process state dict usingrunner.load_constants. This will bind the model weights in the AOTInductor package to the shared model weights in the parent process, and avoid weight duplication in GPU VRAM. - The
runner.load_constantsfunction has auser_managedargument, which is set toFalsein the example. This means that the AOTInductor runner will not manage the model weights, and the user is responsible for managing the model weights. The AOTInductor runner will just have a pointer to the model weights in the parent process, and will not manage the life cycle of model weights. Ifuser_managedis set toTrue, the AOTInductor runner will manage the model weights. But instead of making a copy of the model weights in GPU VRAM, the AOTInductor runner will still have a reference to the model weights in the parent process. The reference counter is effective across processes, so the model weights will not be freed until all references to the model weights are released. - In this example, the AOTInductor package was replicated for each worker process to break the OS mmap page sharing, in order to demonstrate the weight sharing via the parent process state dict. If we only have one AOTInductor package file, and each process loads the same package file, the OS will still share the same mmap pages for all processes, no model weights duplication will occur on GPU VRAM, because those loaded weights are read-only and the OS knows that it can be safely shared across processes.
TorchScript Weight Sharing
If the user is still using the deprecated TorchScript, it is still possible to share model weights across processes using a similar approach. However, I encountered a lot of problems. For example, I would have to instantiate the TorchScript model on CPU in all the processes first and then replace the model weights with the shared weights from the parent process. This will inflate the CPU memory usages significantly, unless I load the TorchScript model and bind the shared weights in the parent process sequentially for each process. Consequently, I did not include the TorchScript example in the previous example implementation.
Program Dependency Duplication
Even though we saved duplicate model weights in multi-process inference, the program dependencies (e.g., shared libraries) are still duplicated in each process. This is because each process has its own address space and loads its own copy of the program dependencies. However, the OS can still share the same physical memory pages for the program dependencies across processes, if those dependencies are loaded from the same shared library files. This is a common behavior in modern operating systems, and it helps to reduce the overall memory footprint of multi-process applications. Nevertheless, it is still expected that some memory overhead will be incurred for both host memory and GPU memory for each process, and the amount of overhead really depends.
Conclusions
In this blog post, we demonstrated how to share model weights across multiple processes in PyTorch multi-process inference using CUDA IPC. We also discussed the caveats and best practices for both PyTorch and AOTInductor, as well as the implications for TorchScript and program dependency duplication.
PyTorch Multi-Process Inference Weight Sharing Via Inter-Process Communication
https://leimao.github.io/blog/PyTorch-IPC-Multi-Process-Inference-Weight-Sharing/