|
| 1 | +--- |
| 2 | +title: Background Information |
| 3 | +weight: 3 |
| 4 | + |
| 5 | +### FIXED, DO NOT MODIFY |
| 6 | +layout: learningpathall |
| 7 | +--- |
| 8 | + |
| 9 | +## Background Information |
| 10 | + |
| 11 | +A well-known challenge in parallel programming is choosing the right number of threads for a given amount of work. When multiple threads are created to perform a task, the actual computation must be large enough to justify the overhead of coordinating those threads. |
| 12 | + |
| 13 | +For example, if a computation is split across many threads, the costs of: |
| 14 | +- creating the threads, and |
| 15 | +- synchronizing their results through shared memory |
| 16 | + |
| 17 | +can easily outweigh any performance gains from parallel execution. The same principle applies to generative AI workloads running on CPU. |
| 18 | + |
| 19 | +When work is distributed across multiple threads, communication and synchronization overhead increases the total amount of work the system must perform. This creates a trade-off between: |
| 20 | + |
| 21 | +- **Latency** – the time to process a single request, and |
| 22 | +- **Throughput** – the number of requests processed per unit time. |
| 23 | + |
| 24 | +PyTorch attempts to automatically choose an appropriate number of threads. However, as we will show, in some cases you may want to manually fine-tune this configuration to improve performance. |
| 25 | + |
| 26 | +## Multi-threading with PyTorch on CPU |
| 27 | + |
| 28 | + |
| 29 | +The diagram below is taken from the [PyTorch documentation](https://docs.pytorch.org/docs/2.8/notes/cpu_threading_torchscript_inference.html). When running inference, PyTorch uses an **Application Thread Pool**. In PyTorch, there are inter-op parallelism, which is spawning threads to run separate operations in a graph in parallel (e.g., 1 thread for a matmul and another thread for a softmax). Additionally there's intra-op parallelism can be used to spawn multiple threads to work on the same operation. |
| 30 | + |
| 31 | + |
| 32 | + |
| 33 | +In PyTorch, the `torch.set_num_threads()` [API](https://docs.pytorch.org/docs/stable/generated/torch.set_num_threads.html) is used to set the maximum number of threads to spawn in the Application Thread Pool. |
| 34 | + |
| 35 | +As of PyTorch 2.8.0, the default number of threads is equal to the number of CPU cores (see [PyTorch CPU Threading Documentation](https://docs.pytorch.org/docs/2.8/notes/cpu_threading_torchscript_inference.html) for more detail). PyTorch looks to find the ideal number of threads as described with the following code snippet taken from the PyTorch source code, [ParallemOpenMP.h](https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/ParallelOpenMP.h). |
| 36 | + |
| 37 | +```cpp |
| 38 | +int64_t num_threads = omp_get_num_threads(); |
| 39 | + if (grain_size > 0) { |
| 40 | + num_threads = std::min(num_threads, divup((end - begin), grain_size)); |
| 41 | + } |
| 42 | + |
| 43 | +... |
| 44 | +inline int64_t divup(int64_t x, int64_t y) { |
| 45 | + return (x + y - 1) / y; |
| 46 | +} |
| 47 | +``` |
| 48 | + |
| 49 | +In PyTorch builds that use OpenMP, the maximum size of the application’s thread pool can be configured once at runtime using the `OMP_NUM_THREADS` environment variable. The actual number of threads used will scale up to this limit depending on the workload and the `grain_size`. |
| 50 | + |
| 51 | +The short example below illustrates that the default settings on many-core systems may not provide optimal performance for all workloads. |
| 52 | + |
| 53 | +## Basic PyTorch Example |
| 54 | + |
| 55 | +Create a new file named `pytorch_omp_example.py` and paste in the Python script below. The script performs a matrix multiplication in eager mode on two 256×256 random matrices. |
| 56 | +For this relatively small computation, we will: |
| 57 | + |
| 58 | +- Observe the default performance of PyTorch’s parallelism |
| 59 | +- Print the parallel configuration using `torch.__config__.parallel_info()`. |
| 60 | + |
| 61 | +```python |
| 62 | +import os |
| 63 | +import time |
| 64 | +import torch |
| 65 | + |
| 66 | + |
| 67 | +def main(): |
| 68 | + print(f"PyTorch version: {torch.__version__}") |
| 69 | + |
| 70 | + # Read OMP_NUM_THREADS from the environment |
| 71 | + omp_threads = os.environ.get("OMP_NUM_THREADS") |
| 72 | + print(f"OMP_NUM_THREADS in environment: {omp_threads}") |
| 73 | + |
| 74 | + # If it's set and looks like a number, use it to set PyTorch's intra-op threads |
| 75 | + if omp_threads and omp_threads.isdigit(): |
| 76 | + torch.set_num_threads(int(omp_threads)) |
| 77 | + |
| 78 | + # Show how many threads PyTorch will actually use for intra-op parallelism |
| 79 | + print(f"torch.get_num_threads(): {torch.get_num_threads()}\n") |
| 80 | + |
| 81 | + # A simple operation to illustrate parallelism: |
| 82 | + size = 256 |
| 83 | + a = torch.randn(size, size) |
| 84 | + b = torch.randn(size, size) |
| 85 | + |
| 86 | + start = time.time() |
| 87 | + c = a @ b # matrix multiplication (runs in a parallel region on CPU) |
| 88 | + end = time.time() |
| 89 | + |
| 90 | + print(f"Result shape: {c.shape}") |
| 91 | + print(f"Matrix multiply time: {end - start:.5f} seconds") |
| 92 | + print(f"\nThreading Information = {torch.__config__.parallel_info()}") |
| 93 | + |
| 94 | +if __name__ == "__main__": |
| 95 | + main() |
| 96 | +``` |
| 97 | + |
| 98 | +Run the python script |
| 99 | + |
| 100 | +```bash |
| 101 | +python pytorch_omp_example.py |
| 102 | +``` |
| 103 | + |
| 104 | +You will observe the an output similar to the follow. As you can see the number of threads is set to core count of 96 and the time to execute is 2.24 ms. |
| 105 | + |
| 106 | + |
| 107 | +```output |
| 108 | +PyTorch version: 2.10.0.dev20251124 |
| 109 | +OMP_NUM_THREADS in environment: None |
| 110 | +torch.get_num_threads(): 96 |
| 111 | +
|
| 112 | +Result shape: torch.Size([256, 256]) |
| 113 | +Matrix multiply time: 0.00224 seconds |
| 114 | +
|
| 115 | +Threading Information = ATen/Parallel: |
| 116 | + at::get_num_threads() : 96 |
| 117 | + at::get_num_interop_threads() : 96 |
| 118 | +OpenMP 201511 (a.k.a. OpenMP 4.5) |
| 119 | + omp_get_max_threads() : 96 |
| 120 | +Intel(R) MKL-DNN v3.11.0 (Git Hash 0b8a866c009b03f322e6526d7c33cfec84a4a97a) |
| 121 | +std::thread::hardware_concurrency() : 96 |
| 122 | +Environment variables: |
| 123 | + OMP_NUM_THREADS : [not set] |
| 124 | +ATen parallel backend: OpenMP |
| 125 | +``` |
| 126 | + |
| 127 | +Now reduce the number of OpenMP threads using the `OMP_NUM_THREADS` value and observe a reduction is matrix multiply time to 0.64 ms. |
| 128 | + |
| 129 | +```bash |
| 130 | +OMP_NUM_THREADS=16 python pytorch_omp_example.py |
| 131 | +``` |
| 132 | + |
| 133 | +```output |
| 134 | +PyTorch version: 2.10.0.dev20251124 |
| 135 | +OMP_NUM_THREADS in environment: 16 |
| 136 | +torch.get_num_threads(): 16 |
| 137 | +
|
| 138 | +Result shape: torch.Size([256, 256]) |
| 139 | +Matrix multiply time: 0.00064 seconds |
| 140 | +
|
| 141 | +Threading Information = ATen/Parallel: |
| 142 | + at::get_num_threads() : 16 |
| 143 | + at::get_num_interop_threads() : 96 |
| 144 | +OpenMP 201511 (a.k.a. OpenMP 4.5) |
| 145 | + omp_get_max_threads() : 16 |
| 146 | +Intel(R) MKL-DNN v3.11.0 (Git Hash 0b8a866c009b03f322e6526d7c33cfec84a4a97a) |
| 147 | +std::thread::hardware_concurrency() : 96 |
| 148 | +Environment variables: |
| 149 | + OMP_NUM_THREADS : 16 |
| 150 | +ATen parallel backend: OpenMP |
| 151 | +``` |
| 152 | + |
| 153 | +We will now move on from this trivial example to a much larger workload of a large language model (LLM). |
0 commit comments