|
1 | | -# Machine Learning on HPC |
| 1 | +# Single-GPU Training with PyTorch |
| 2 | + |
| 3 | +It is important to optimize your script for the single-GPU case before moving to multi-GPU training. This is because as you request more resources, your queue time increases. We also want to avoid wasting resources by running code that is not optimized. |
| 4 | + |
| 5 | +Here we train a CNN on the MNIST dataset using a single GPU as an example. We profile the code and make performance improvements. |
| 6 | + |
| 7 | +This tutorial uses PyTorch but the steps are similar for TensorFlow. See our [TensorFlow](./06_tensorflow.md) page for details. |
| 8 | + |
| 9 | +## Step 1: Activate the Environment |
| 10 | + |
| 11 | +For simplicity we will use a pre-installed Conda environmnet. Run these commands to activate the environment: |
| 12 | + |
| 13 | +```bash |
| 14 | +$ ssh <YourNetID>@adroit.princeton.edu |
| 15 | +$ module load anaconda3/2023.9 |
| 16 | +$ conda activate /home/jdh4/.conda/envs/torch-env |
| 17 | +``` |
| 18 | + |
| 19 | +Watch a [video](https://www.youtube.com/watch?v=wqTgM-Wq4YY&t=296s) that covers everything on this page for single-GPU training with [profiling Python](https://researchcomputing.princeton.edu/python-profiling) using `line_profiler`. |
| 20 | + |
| 21 | +## Step 2: Run and Profile the Script |
| 22 | + |
| 23 | +``` |
| 24 | +fix link |
| 25 | +First, inspect the script ([see script](mnist_classify.py)) by running these commands: |
| 26 | +``` |
| 27 | + |
| 28 | +```bash |
| 29 | +(torch-env) $ cd multi_gpu_training/01_single_gpu |
| 30 | +(torch-env) $ cat mnist_classify.py |
| 31 | +``` |
| 32 | + |
| 33 | +We will profile the `train` function using `line_profiler` (see line 39) by adding the following decorator: |
| 34 | + |
| 35 | +```python |
| 36 | +@profile |
| 37 | +def train(args, model, device, train_loader, optimizer, epoch): |
| 38 | +``` |
| 39 | + |
| 40 | +Next, download the data while on the login node since the compute nodes do not have internet access: |
| 41 | + |
| 42 | +``` |
| 43 | +(torch-env) $ python download_mnist.py |
| 44 | +``` |
| 45 | + |
| 46 | +Below is the Slurm script: |
| 47 | + |
| 48 | +```bash |
| 49 | +#!/bin/bash |
| 50 | +#SBATCH --job-name=mnist # create a short name for your job |
| 51 | +#SBATCH --nodes=1 # node count |
| 52 | +#SBATCH --ntasks=1 # total number of tasks across all nodes |
| 53 | +#SBATCH --cpus-per-task=1 # cpu-cores per task (>1 if multi-threaded tasks) |
| 54 | +#SBATCH --mem=8G # total memory per node (4 GB per cpu-core is default) |
| 55 | +#SBATCH --gres=gpu:1 # number of gpus per node |
| 56 | +#SBATCH --time=00:05:00 # total run time limit (HH:MM:SS) |
| 57 | +#SBATCH --mail-type=begin # send email when job begins |
| 58 | +#SBATCH --mail-type=end # send email when job ends |
| 59 | + |
| 60 | +# which gpu node was used |
| 61 | +echo "Running on host" $(hostname) |
| 62 | + |
| 63 | +# print the slurm environment variables sorted by name |
| 64 | +printenv | grep -i slurm | sort |
| 65 | + |
| 66 | +module purge |
| 67 | +module load anaconda3/2023.9 |
| 68 | +conda activate /home/jdh4/.conda/envs/torch-env |
| 69 | + |
| 70 | +kernprof -o ${SLURM_JOBID}.lprof -l mnist_classify.py --epochs=3 |
| 71 | +``` |
| 72 | + |
| 73 | +`kernprof` is a profiler that wraps Python. Adroit has two different A100 nodes. Learn how to choose [specific nodes](https://researchcomputing.princeton.edu/systems/adroit#gpus). |
| 74 | + |
| 75 | +Finally, submit the job while specifying the reservation: |
| 76 | + |
| 77 | +```bash |
| 78 | +(torch-env) $ sbatch --reservation=multigpu job.slurm |
| 79 | +``` |
| 80 | + |
| 81 | +You should find that the code runs in about 20-40 seconds with 1 CPU-core depending on which A100 GPU node was used: |
| 82 | + |
| 83 | +``` |
| 84 | +$ seff 1937315 |
| 85 | +Job ID: 1937315 |
| 86 | +Cluster: adroit |
| 87 | +User/Group: aturing/cses |
| 88 | +State: COMPLETED (exit code 0) |
| 89 | +Cores: 1 |
| 90 | +CPU Utilized: 00:00:36 |
| 91 | +CPU Efficiency: 94.74% of 00:00:38 core-walltime |
| 92 | +Job Wall-clock time: 00:00:38 |
| 93 | +Memory Utilized: 593.32 MB |
| 94 | +Memory Efficiency: 7.24% of 8.00 GB |
| 95 | +``` |
| 96 | + |
| 97 | +For jobs that run for longer than 1 minute, one should use the `jobstats` command instead of `seff`. Use `shistory -n` to see which node was used or look in the `slurm-#######.out` file. |
| 98 | + |
| 99 | +Some variation in the run time is expected when multiple users are running on the same node. Also, the two A100 GPU nodes are not equal: |
| 100 | + |
| 101 | +| hostname | CPU | GPU | |
| 102 | +| ----------- | ----------- | ----------- | |
| 103 | +| adroit-h11g1 | Intel Xeon Gold 6442Y @ 2.6GHz | NVIDIA A100 80GB PCIe | |
| 104 | +| adroit-h11g2 | Intel Xeon Gold 6342 @ 2.8GHz | NVIDIA A100-PCIE-40GB | |
| 105 | + |
| 106 | +## Step 3: Analyze the Profiling Data |
| 107 | + |
| 108 | +We installed [line_profiler](https://researchcomputing.princeton.edu/python-profiling) into the Conda environment and profiled the code. To analyze the profiling data: |
| 109 | + |
| 110 | +``` |
| 111 | +(torch-env) $ python -m line_profiler -rmt *.lprof |
| 112 | +Timer unit: 1e-06 s |
| 113 | +
|
| 114 | +Total time: 30.8937 s |
| 115 | +File: mnist_classify.py |
| 116 | +Function: train at line 39 |
| 117 | +
|
| 118 | +Line # Hits Time Per Hit % Time Line Contents |
| 119 | +============================================================== |
| 120 | + 39 @profile |
| 121 | + 40 def train(args, model, device, train_loader, optimizer, epoch): |
| 122 | + 41 3 213.1 71.0 0.0 model.train() |
| 123 | + 42 2817 26106124.7 9267.3 84.5 for batch_idx, (data, target) in enumerate(train_loader): |
| 124 | + 43 2814 286242.0 101.7 0.9 data, target = data.to(device), target.to(device) |
| 125 | + 44 2814 296440.2 105.3 1.0 optimizer.zero_grad() |
| 126 | + 45 2814 1189206.1 422.6 3.8 output = model(data) |
| 127 | + 46 2814 81578.6 29.0 0.3 loss = F.nll_loss(output, target) |
| 128 | + 47 2814 1979990.2 703.6 6.4 loss.backward() |
| 129 | + 48 2814 841861.9 299.2 2.7 optimizer.step() |
| 130 | + 49 2814 2095.3 0.7 0.0 if batch_idx % args.log_interval == 0: |
| 131 | + 50 564 1852.9 3.3 0.0 print('Train Epoch: {} [{}/{} ({:.0f}%)]\tLoss: {:.6f}'.format( |
| 132 | + 51 282 2218.6 7.9 0.0 epoch, batch_idx * len(data), len(train_loader.dataset), |
| 133 | + 52 282 105753.3 375.0 0.3 100. * batch_idx / len(train_loader), loss.item())) |
| 134 | + 53 282 119.2 0.4 0.0 if args.dry_run: |
| 135 | + 54 break |
| 136 | +
|
| 137 | + 30.89 seconds - mnist_classify.py:39 - train |
| 138 | +``` |
| 139 | + |
| 140 | +The slowest line is number 42 which consumes 84.5% of the time in the training function. That line involves `train_loader` which is the data loader for the training set. Are you surprised that the data loader is the slowest step and not the forward pass or calculation of the gradients? Can we improve on this? |
| 141 | + |
| 142 | +### Examine Your GPU Utilization |
| 143 | + |
| 144 | +Use tools like [jobstats](https://researchcomputing.princeton.edu/support/knowledge-base/job-stats#jobstats), [gpudash](https://researchcomputing.princeton.edu/support/knowledge-base/gpu-computing#gpudash) and [stats.rc](https://researchcomputing.princeton.edu/support/knowledge-base/job-stats#stats.rc) to measure your GPU utilization. You can also do this on a [compute node in real time](https://researchcomputing.princeton.edu/support/knowledge-base/gpu-computing#gpu-utilization). |
| 145 | + |
| 146 | +Note that GPU utilization as measured using nvidia-smi is only a measure of the fraction of the time that a GPU kernel is running on the GPU. It says nothing about how many CUDA cores are being used or how efficiently the GPU kernels have been written. However, for codes used by large communities, one can generally associate GPU utilization with overall GPU efficiency. For a more accurate measure of GPU utilization, use [Nsight Systems or Nsight Compute](https://researchcomputing.princeton.edu/support/knowledge-base/gpu-computing#profiling) to measure the occupancy. |
| 147 | + |
| 148 | +## Step 4: Work through the Performance Tuning Guide |
| 149 | + |
| 150 | +Make sure you optimize the single GPU case before going to multiple GPUs by working through the [Performance Tuning Guide](https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html). |
| 151 | + |
| 152 | +## Step 5: Optimize Your Script |
| 153 | + |
| 154 | +One technique that was discussed in the [Performance Tuning Guide](https://pytorch.org/tutorials/recipes/recipes/tuning_guide.html) was using multiple CPU-cores to speed-up [ETL](https://en.wikipedia.org/wiki/Extract,_transform,_load). Let's put this into practice. |
| 155 | + |
| 156 | + |
| 157 | + |
| 158 | +*Credit for image above is [here](https://www.telesens.co/2019/04/04/distributed-data-parallel-training-using-pytorch-on-aws/).* |
| 159 | + |
| 160 | +In `mnist_classify.py`, change `num_workers` from 1 to 8. And then in `job.slurm` change `--cpus-per-task` from 1 to 8. Then run the script again and note the speed-up: |
| 161 | + |
| 162 | +``` |
| 163 | +(torch-env) $ sbatch --reservation=multigpu job.slurm |
| 164 | +``` |
| 165 | + |
| 166 | +How did the profiling data change? Watch the [video](https://www.youtube.com/watch?v=wqTgM-Wq4YY&t=296s) for the solution. For consistency between the Slurm script and PyTorch script, one can use: |
| 167 | + |
| 168 | +```python |
| 169 | +import os |
| 170 | +... |
| 171 | + cuda_kwargs = {'num_workers': int(os.environ["SLURM_CPUS_PER_TASK"]), |
| 172 | +... |
| 173 | +``` |
| 174 | + |
| 175 | +Several environment variables are set in the Slurm script. These can be referenced by the PyTorch script as demonstrated above. To see all of the available environment variables that are set in the Slurm script, add this line to `job.slurm`: |
| 176 | + |
| 177 | +``` |
| 178 | +printenv | sort |
| 179 | +``` |
| 180 | + |
| 181 | +Consider these external data loading libraries: [ffcv](https://github.com/libffcv/ffcv) and [NVIDIA DALI](https://developer.nvidia.com/dali). |
| 182 | + |
| 183 | +## Summary |
| 184 | + |
| 185 | +It is essential to optimize your code before going to multi-GPU training since the inefficiencies will only be magnified otherwise. The more GPUs you request in a Slurm job, the longer you will wait for the job to run. If you can get your work done using an optimized script running on a single GPU then proceed that way. Do not use multiple GPUs if your GPU efficiency is low. The average GPU efficiency on Della is around 50%. |
| 186 | + |
| 187 | +Next, we focus on scaling the code to multiple GPUs (go to [next section](./03_pytorch_dpp.md)). |
| 188 | + |
| 189 | +## How was the Conda environment made? |
| 190 | + |
| 191 | +Your `/home` directory on Adroit probably has a capacity of 9.3 GB. To store Conda environments in another location see [this page](https://researchcomputing.princeton.edu/support/knowledge-base/checkquota). See the Research Computing knowledge base on [PyTorch](https://researchcomputing.princeton.edu/support/knowledge-base/pytorch) for installation directions. |
0 commit comments