Skip to content

Commit fd8ec4a

Browse files
Merge pull request #2737 from jasonrandrews/review
Tech review of PyTorch threads
2 parents 5b02c6d + be524b2 commit fd8ec4a

4 files changed

Lines changed: 129 additions & 88 deletions

File tree

content/learning-paths/servers-and-cloud-computing/tune-pytorch-cpu-perf-with-threads/_index.md

Lines changed: 19 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
---
2-
title: Fine tune LLM performance on CPU with multithreading
2+
title: Fine tune LLM CPU inference performance with multithreading
33

44
draft: true
55
cascade:
66
draft: true
77

8-
minutes_to_complete: 20
8+
minutes_to_complete: 30
99

10-
who_is_this_for: ML Engineers looking to fine tune the inference performance of LLMs running on CPU
10+
who_is_this_for: This is an introductory topic ML engineers optimizing LLM inference performance on Arm CPUs.
1111

1212
learning_objectives:
13-
- Understand how PyTorch uses multiple threads for CPU inference and the various tradeoffs involved
14-
- Tune the thread count to improve performance for specific models and systems
13+
- Understand how PyTorch uses multiple threads for CPU inference
14+
- Measure performance impact of thread count on LLM inference
15+
- Tune thread count to optimize inference for specific models and systems
1516

1617
prerequisites:
17-
- Intermediate understanding of Python and PyTorch
18-
- Access to an Arm-based system
18+
- An [Arm-based cloud instance](/learning-paths/servers-and-cloud-computing/csp/) or an Arm server with at least 16 cores
19+
- Basic understanding of Python and PyTorch
20+
- Ability to install Docker on your Arm system
1921

2022
author: Kieran Hejmadi
2123

@@ -27,16 +29,23 @@ armips:
2729
tools_software_languages:
2830
- Python
2931
- PyTorch
30-
- Bash
3132
operatingsystems:
3233
- Linux
3334

3435

3536
further_reading:
3637
- resource:
37-
title: Arm Tool Solutions
38-
link: https://github.com/ARM-software/Tool-Solutions/tree/main
38+
title: PyTorch CPU Threading Documentation
39+
link: https://docs.pytorch.org/docs/stable/notes/cpu_threading_torchscript_inference.html
40+
type: documentation
41+
- resource:
42+
title: Arm Tool Solutions Repository
43+
link: https://github.com/ARM-software/Tool-Solutions/tree/main/ML-Frameworks/pytorch-aarch64
3944
type: website
45+
- resource:
46+
title: Docker install guide
47+
link: /install-guides/docker/
48+
type: install-guide
4049

4150

4251
### FIXED, DO NOT MODIFY

content/learning-paths/servers-and-cloud-computing/tune-pytorch-cpu-perf-with-threads/background.md

Lines changed: 25 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,30 @@
11
---
2-
title: Background Information
2+
title: Understand PyTorch threading for CPU inference
33
weight: 3
44

55
### FIXED, DO NOT MODIFY
66
layout: learningpathall
77
---
88

9-
## Background Information
9+
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.
1010

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.
11+
If a computation is split across many threads, the costs of creating the threads and synchronizing their results through shared memory can easily outweigh any performance gains from parallel execution. The same principle applies to generative AI workloads running on CPU.
1212

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
13+
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 latency (the time to process a single request) and throughput (the number of requests processed per unit time).
1614

17-
can easily outweigh any performance gains from parallel execution. The same principle applies to generative AI workloads running on CPU.
15+
PyTorch attempts to automatically choose an appropriate number of threads. However, as you will learn, in some cases you can manually tune this configuration to improve performance.
1816

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:
17+
## Multithreading with PyTorch on CPU
2018

21-
- **Latency** – the time to process a single request, and
22-
- **Throughput** – the number of requests processed per unit time.
19+
When running inference, PyTorch uses an Application Thread Pool. PyTorch supports two types of parallelism: inter-op parallelism spawns threads to run separate operations in a graph in parallel (for example, one thread for a matmul and another thread for a softmax), while intra-op parallelism spawns multiple threads to work on the same operation.
2320

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.
21+
The diagram below is taken from the [PyTorch documentation](https://docs.pytorch.org/docs/stable/index.html).
2522

26-
## Multi-threading with PyTorch on CPU
23+
![Diagram showing PyTorch's threading model with application thread pool, inter-op thread pool, and intra-op thread pool for CPU inference#center](./pytorch-threading.jpg "PyTorch threading model")
2724

25+
The `torch.set_num_threads()` [API](https://docs.pytorch.org/docs/stable/generated/torch.set_num_threads.html) sets the maximum number of threads to spawn in the Application Thread Pool.
2826

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-
![threading-in-pytorch](./pytorch-threading.jpg)
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).
27+
As of PyTorch 2.8.0, the default number of threads equals 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 determines the ideal number of threads based on the workload size, as shown in this code snippet from [ParallelOpenMP.h](https://github.com/pytorch/pytorch/blob/main/aten/src/ATen/ParallelOpenMP.h):
3628

3729
```cpp
3830
int64_t num_threads = omp_get_num_threads();
@@ -46,17 +38,13 @@ inline int64_t divup(int64_t x, int64_t y) {
4638
}
4739
```
4840

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.
41+
In PyTorch builds that use OpenMP, the maximum size of the application's thread pool can be configured at runtime using the `OMP_NUM_THREADS` environment variable. The actual number of threads used scales up to this limit depending on the workload and the `grain_size`.
5242

53-
## Basic PyTorch Example
43+
The example below demonstrates that the default settings on many-core systems might not provide optimal performance for all workloads.
5444

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:
45+
## Basic PyTorch example
5746

58-
- Observe the default performance of PyTorch’s parallelism
59-
- Print the parallel configuration using `torch.__config__.parallel_info()`.
47+
Create a new file named `pytorch_omp_example.py` with the following Python script. The script performs a matrix multiplication in eager mode on two 256×256 random matrices, showing the default performance of PyTorch's parallelism and printing the parallel configuration:
6048

6149
```python
6250
import os
@@ -95,13 +83,13 @@ if __name__ == "__main__":
9583
main()
9684
```
9785

98-
Run the python script
86+
Run the Python script:
9987

10088
```bash
10189
python pytorch_omp_example.py
10290
```
10391

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.
92+
The output is similar to:
10593

10694

10795
```output
@@ -124,12 +112,16 @@ Environment variables:
124112
ATen parallel backend: OpenMP
125113
```
126114

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.
115+
The number of threads is set to the core count of 96, and the execution time is 2.24 ms.
116+
117+
Reduce the number of OpenMP threads using the `OMP_NUM_THREADS` environment variable:
128118

129119
```bash
130120
OMP_NUM_THREADS=16 python pytorch_omp_example.py
131121
```
132122

123+
The output shows a different `Matrix multiply time` of of 0.64 ms.
124+
133125
```output
134126
PyTorch version: 2.10.0.dev20251124
135127
OMP_NUM_THREADS in environment: 16
@@ -150,4 +142,6 @@ Environment variables:
150142
ATen parallel backend: OpenMP
151143
```
152144

153-
We will now move on from this trivial example to a much larger workload of a large language model (LLM).
145+
The time varies with the number of threads and type of processor in your system.
146+
147+
In the next section, you'll apply these concepts to a much larger workload using a large language model (LLM).
Lines changed: 49 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,69 +1,94 @@
11
---
2-
title: Setup Environment
2+
title: Set up the environment
33
weight: 2
44

55
### FIXED, DO NOT MODIFY
66
layout: learningpathall
77
---
88

9-
## Build
9+
{{% notice Note %}}
10+
This Learning Path uses Arm's downstream canary release of PyTorch, which includes ready-to-use examples and scripts. While this release offers access to the latest downstream features, it's intended for experimentation rather than production use.
11+
{{% /notice %}}
1012

11-
In this learning path, we will use Arm’s downstream canary release of PyTorch, which includes ready-to-use examples and scripts. While this release offers access to the latest downstream features, it is intended for experimentation rather than production use.
13+
## Create a Hugging Face account
1214

13-
### 1. Create HuggingFace Account
15+
Create a [Hugging Face account](https://huggingface.co/) if you don't already have one. After creating your account, request access to the [1B](https://huggingface.co/google/gemma-3-1b-it) and [270M](https://huggingface.co/google/gemma-3-270m-it) variants of Google's Gemma-3 model. Access is typically granted within 15 minutes.
1416

15-
Create up a [huggingface account](https://huggingface.co/) if you do not already have one. Once created, request access to the [1B](https://huggingface.co/google/gemma-3-1b-it) and [270M](https://huggingface.co/google/gemma-3-270m-it) variants of Google's Gemma-3 model. It will take around 15 minutes to be granted access.
17+
## Connect to an Arm system and install Docker
1618

17-
### 2. Connect to an Arm system and install Docker
19+
If this is your first time using Arm-based cloud instances, see the [getting started guide](/learning-paths/servers-and-cloud-computing/csp/).
1820

19-
lease see our [getting started guide](https://learn.arm.com/learning-paths/servers-and-cloud-computing/csp/) if it is your first time using Arm-based cloud instances.
21+
The example code uses an AWS Graviton 4 (`m8g.24xlarge`) instance running Ubuntu 24.04 LTS, based on the Neoverse V2 architecture. You can use any Arm server with at least 16 cores. Keep note of your CPU count so you can adjust the example code as needed.
2022

21-
In this example I will be using an AWS Graviton 4 (`m8g.24xlarge`)instance running Ubuntu 24.04 LTS. This is based on the Neoverse V2 architecture.
23+
Install Docker using the [Docker install guide](/install-guides/docker/) or the [official documentation](https://docs.docker.com/engine/install/ubuntu/). Follow the post-installation steps to configure Docker for non-root usage.
2224

25+
## Run the PyTorch-aarch64 Docker container image
2326

24-
Install docker through the [official documentation](https://docs.docker.com/engine/install/ubuntu/) or our [Arm install guide](https://learn.arm.com/install-guides/docker/docker-desktop-arm-linux/). Make sure to follow the post-installation steps.
27+
You have two options for the Docker container. You can use a container image from Docker Hub or you can build the container image from source. Using a ready-made container makes it easier to get started, and building from source provides the latest software. The container image on Docker Hub is updated about once a month.
2528

29+
Open a terminal or use SSH to connect to your Arm Linux system.
2630

27-
### 3. Build the PyTorch-AArch64 Docker Container
31+
### Use container image from Docker Hub
2832

29-
Connect to the Arm instance and run the following to clone the repository.
33+
Download the ready-made container image from Docker Hub:
34+
35+
```bash
36+
docker pull armlimited/pytorch-arm-neoverse:latest
37+
```
38+
39+
Create a new container:
40+
41+
```bash
42+
docker run --rm -it armlimited/pytorch-arm-neoverse:latest
43+
```
44+
45+
The shell prompt will appear, and you are ready to start.
46+
47+
```output
48+
aarch64_pytorch ~>
49+
```
50+
51+
### Build from source
52+
53+
To build from source, clone the repository.
3054

3155
```bash
3256
git clone https://github.com/ARM-software/Tool-Solutions.git
3357
cd Tool-Solutions/ML-Frameworks/pytorch-aarch64/
3458
```
35-
Run the following bash script to build the container
59+
60+
Build the container:
3661

3762
```bash
3863
./build.sh -n $(nproc - 1)
3964
```
4065

41-
> **Note**: On a 96-core instance `m8g.24` AWS this will take approximately 20 minutes to build.
66+
On a 96-core instance such as AWS `m8g.24xlarge`, this build takes approximately 20 minutes.
4267

43-
Once the build has finished, run the following command replacing `<version>` with the version of torch and torchao just built.
68+
After the build completes, create a Docker container. Replace `<version>` with the version of torch and torchao that was built:
4469

4570
```bash
46-
./dockerize.sh ./results/torch-<version>linux_aarch64.whl ./results/torchao-<version>-py3-none-any.whl
71+
./dockerize.sh ./results/torch-<version>-linux_aarch64.whl ./results/torchao-<version>-py3-none-any.whl
4772
```
4873

49-
You should see the following output in your terminal, confirming that you are in the correct directory inside the Docker container.
74+
The shell prompt will appear, and you are ready to start.
5075

51-
```outpu
76+
```output
5277
aarch64_pytorch ~>
5378
```
5479

55-
### 5. Login to HuggingFace
56-
80+
## Log in to Hugging Face
5781

82+
Create a new Read token on Hugging Face by navigating to [Create new Access Token](https://huggingface.co/settings/tokens/new?tokenType=read).
5883

59-
Create a new `read` token on HuggingFace by clicking [this link](https://huggingface.co/settings/tokens/new?tokenType=read).
84+
![Screenshot of Hugging Face token creation page showing the 'Create new token' dialog with token type set to 'Read'#center](./hf-access-token.jpg "Hugging Face token creation")
6085

61-
![hf-token](./hf-access-token.jpg)
62-
63-
Provide a suitable token name, press create token and copy the generated token value. From within docker container, enter the following command and paste the token to login.
86+
Provide a token name, create the token, and copy the generated value. From within the Docker container, run the following command and paste the token when prompted:
6487

6588
```bash
6689
huggingface-cli login
6790
```
6891

69-
> **Note**: The login will not persist once the docker session has ended
92+
Messages indicating the token is valid and login is successful are printed.
93+
94+
Be aware that the login doesn't persist after the Docker container exits. You'll need to log in again if you restart the container.

0 commit comments

Comments
 (0)