Chapter 50
PyTorch 2 Quick Intro
PyTorch 2 Quick Intro
- Reference notebook - https://github.com/mrdbourke/pytorch-deep-learning/blob/main/extras/pytorch_2_intro.ipynb
- Reference book chapter - https://www.learnpytorch.io/pytorch_2_intro/
- PyTorch 2.0 release notes - https://pytorch.org/blog/pytorch-2.0-release/
import torch
print(torch.__version__)Output
2.0.0+cu118
!nvidia-smiOutput
Thu Apr 27 04:12:43 2023
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.85.12 Driver Version: 525.85.12 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA A100-SXM... Off | 00000000:00:04.0 Off | 0 |
| N/A 32C P0 47W / 400W | 0MiB / 40960MiB | 0% Default |
| | | Disabled |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
Quick code examples
Before PyTorch 2.0
import torch
import torchvision
model = torchvision.models.resnet50()After PyTorch 2.0
Note: some PyTorch 2.0 features may hinder the deployment of models - https://pytorch.org/get-started/pytorch-2.0/#inference-and-export
import torch
import torchvision
model = torchvision.models.resnet50() # note: this could any model
compiled_model = torch.compile(model)
### Training code
### Testing code0. Getting setup
import torch
# Check PyTorch version
pt_version = torch.__version__
print(f"[INFO] Current PyTorch version: {pt_version} (should be 2.x+)")
# Install PyTorch 2.0 if necessary
if pt_version.split(".")[0] == "1": # Check if PyTorch version begins with 1
!pip3 install -U torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
print("[INFO] PyTorch 2.x installed, if you're on Google Colab, you may need to restart your runtime.\
Though as of April 2023, Google Colab comes with PyTorch 2.0 pre-installed.")
import torch
pt_version = torch.__version__
print(f"[INFO] Current PyTorch version: {pt_version} (should be 2.x+)")
else:
print("[INFO] PyTorch 2.x installed, you'll be able to use the new features.")Output
[INFO] Current PyTorch version: 2.0.0+cu118 (should be 2.x+) [INFO] PyTorch 2.x installed, you'll be able to use the new features.
1. Get GPU info
Why get GPU info?
Because PyTorch 2.0 features (torch.compile()) work best on newer NVIDIA GPUs.
Well, what's a newer NVIDIA GPU?
To find out if your GPU is compatiable, see NVIDIA GPU compability scores - https://developer.nvidia.com/cuda-gpus
If your GPU has a score of 8.0+, it can leverage most if not all of the new PyTorch 2.0 features.
GPUs under 8.0 can still leverage PyTorch 2.0, however, the improvements may not be as noticable as those with 8.0+.
Note: If you're wondering what GPU you should use for deep learning, check out Tim Dettmers blog post "Which GPU for deep learning?" - https://timdettmers.com/2023/01/30/which-gpu-for-deep-learning/
# Make sure we're using a NVIDIA GPU
if torch.cuda.is_available():
gpu_info = !nvidia-smi
gpu_info = '\n'.join(gpu_info)
if gpu_info.find("failed") >= 0:
print("Not connected to a GPU, to leverage the best of PyTorch 2.0, you should connect to a GPU.")
# Get GPU name
gpu_name = !nvidia-smi --query-gpu=gpu_name --format=csv
gpu_name = gpu_name[1]
GPU_NAME = gpu_name.replace(" ", "_") # remove underscores for easier saving
print(f'GPU name: {GPU_NAME}')
# Get GPU capability score
GPU_SCORE = torch.cuda.get_device_capability()
print(f"GPU capability score: {GPU_SCORE}")
if GPU_SCORE >= (8, 0):
print(f"GPU score higher than or equal to (8, 0), PyTorch 2.x speedup features available.")
else:
print(f"GPU score lower than (8, 0), PyTorch 2.x speedup features will be limited (PyTorch 2.x speedups happen most on newer GPUs).")
# Print GPU info
print(f"GPU information:\n{gpu_info}")
else:
print("PyTorch couldn't find a GPU, to leverage the best of PyTorch 2.0, you should connect to a GPU.")Output
GPU name: NVIDIA_A100-SXM4-40GB
GPU capability score: (8, 0)
GPU score higher than or equal to (8, 0), PyTorch 2.x speedup features available.
GPU information:
Thu Apr 27 04:13:01 2023
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 525.85.12 Driver Version: 525.85.12 CUDA Version: 12.0 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 NVIDIA A100-SXM... Off | 00000000:00:04.0 Off | 0 |
| N/A 32C P0 47W / 400W | 3MiB / 40960MiB | 0% Default |
| | | Disabled |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
1.1 Globally set devices
Previously, we've set the device of our tensors/models using .to(device).
tensor.to(device)model.to(device)
But in PyTorch 2.0, it's possible to set the device with a context manager as well as a global device - https://pytorch.org/blog/pytorch-2.0-release/#beta-torchset_default_device-and-torchdevice-as-context-manager
See the docs - https://pytorch.org/tutorials/recipes/recipes/changing_default_device.html
import torch
# Set the device
device = "cuda" if torch.cuda.is_available() else "cpu"
# Set the device with context manager (requires PyTorch 2.x+)
with torch.device(device):
# All tensors or PyTorch objects created in the context manager will be on the target device without using .to()
layer = torch.nn.Linear(20, 30)
print(f"Layer weights are on device: {layer.weight.device}")
print(f"Layer creating data on device: {layer(torch.randn(128, 20)).device}")Output
Layer weights are on device: cuda:0 Layer creating data on device: cuda:0
import torch
# Set the device
device = "cuda" if torch.cuda.is_available() else "cpu"
# Set the device globally (requires pytorch 2.x+)
torch.set_default_device(device)
# All tensors or PyTorch objects created from here on out will be on the target device without using .to()
layer = torch.nn.Linear(20, 30)
print(f"Layer weights are on device: {layer.weight.device}")
print(f"Layer creating data on device: {layer(torch.randn(128, 20)).device}")Output
Layer weights are on device: cuda:0 Layer creating data on device: cuda:0
import torch
# Set the device globally (requires pytorch 2.x+)
torch.set_default_device("cpu")
# All tensors or PyTorch objects created from here on out will be on the target device without using .to()
layer = torch.nn.Linear(20, 30)
print(f"Layer weights are on device: {layer.weight.device}")
print(f"Layer creating data on device: {layer(torch.randn(128, 20)).device}")Output
Layer weights are on device: cpu Layer creating data on device: cpu
2. Setting up the experiments
Time to test speed!
To keep things simple, we'll run 4 experiments:
- Model: ResNet50 from torchvision
- Data: CIFAR10 from torchvision
- Epochs: 5 (single run) and 3x5 (multi run)
- Batch size: 128 (note: you may want to change this depending on the amount of memory your GPU has, this tutorial focuses on an A100)
- Image size: 224 (note: you may want to adjust this given the amount of GPU memory you have)
import torch
import torchvision
print(f"PyTorch version: {torch.__version__}")
print(f"TorchVision version: {torchvision.__version__}")
# Set the target device
device = "cuda" if torch.cuda.is_available() else "cpu"
print(f"Using device: {device}")Output
PyTorch version: 2.0.0+cu118 TorchVision version: 0.15.1+cu118 Using device: cuda
2.1 Create model and transforms
# Create model weights and transforms
model_weights = torchvision.models.ResNet50_Weights.IMAGENET1K_V2 # .DEFAULT also works here
transforms = model_weights.transforms()
transformsOutput
ImageClassification(
crop_size=[224]
resize_size=[232]
mean=[0.485, 0.456, 0.406]
std=[0.229, 0.224, 0.225]
interpolation=InterpolationMode.BILINEAR
)# Create model
model = torchvision.models.resnet50(weights=model_weights)
modelOutput
Downloading: "https://download.pytorch.org/models/resnet50-11ad3fa6.pth" to /root/.cache/torch/hub/checkpoints/resnet50-11ad3fa6.pth 100%|██████████| 97.8M/97.8M [00:01<00:00, 93.0MB/s]
ResNet(
(conv1): Conv2d(3, 64, kernel_size=(7, 7), stride=(2, 2), padding=(3, 3), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(maxpool): MaxPool2d(kernel_size=3, stride=2, padding=1, dilation=1, ceil_mode=False)
(layer1): Sequential(
(0): Bottleneck(
(conv1): Conv2d(64, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(256, 64, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(64, 64, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(64, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(64, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer2): Sequential(
(0): Bottleneck(
(conv1): Conv2d(256, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(256, 512, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(512, 128, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(128, 128, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(128, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(128, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer3): Sequential(
(0): Bottleneck(
(conv1): Conv2d(512, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(512, 1024, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(3): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(4): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(5): Bottleneck(
(conv1): Conv2d(1024, 256, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(256, 256, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(256, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(256, 1024, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(1024, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(layer4): Sequential(
(0): Bottleneck(
(conv1): Conv2d(1024, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(2, 2), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
(downsample): Sequential(
(0): Conv2d(1024, 2048, kernel_size=(1, 1), stride=(2, 2), bias=False)
(1): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
)
)
(1): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
(2): Bottleneck(
(conv1): Conv2d(2048, 512, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn1): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv2): Conv2d(512, 512, kernel_size=(3, 3), stride=(1, 1), padding=(1, 1), bias=False)
(bn2): BatchNorm2d(512, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(conv3): Conv2d(512, 2048, kernel_size=(1, 1), stride=(1, 1), bias=False)
(bn3): BatchNorm2d(2048, eps=1e-05, momentum=0.1, affine=True, track_running_stats=True)
(relu): ReLU(inplace=True)
)
)
(avgpool): AdaptiveAvgPool2d(output_size=(1, 1))
(fc): Linear(in_features=2048, out_features=1000, bias=True)
)# Count the number of parameters in the model
total_params = sum(
param.numel() for param in model.parameters() # count all parameters
# param.numel() for param in model.parameters() if param.requires_grad = True # only count parameters that are trainable
)
total_paramsOutput
25557032
Note: PyTorch 2.0 relative speedups will be most noticeable when as much of the GPU as possible is being used. This means a larger model (more trainable parameters) may take longer to train on the whole but will relatively faster. E.g. a model with 1M parameters may take ~10 minutes to train but a model with 25M parameters may take ~20 minutes to train.
def create_model(num_classes=10):
"""
Creates a resnet50 model with transforms and returns them both.
"""
model_weights = torchvision.models.ResNet50_Weights.DEFAULT
transforms = model_weights.transforms()
model = torchvision.models.resnet50(weights=model_weights)
# Adjust the head layer to suit our number of classes
model.fc = torch.nn.Linear(in_features=2048,
out_features=num_classes)
return model, transforms
model, transforms = create_model()
transformsOutput
ImageClassification(
crop_size=[224]
resize_size=[232]
mean=[0.485, 0.456, 0.406]
std=[0.229, 0.224, 0.225]
interpolation=InterpolationMode.BILINEAR
)2.2 Speedups are most noticeable when a large portion of the GPU(s) is being used
Since modern GPUs are so fast at performing operations, you will often notice the majority of relative speedups when as much data as possible is on the GPU.
In practice, you generally want to use as much of your GPU memory as possible.
- Increasing the batch size - we've been using batch size 32 but for GPUs with a larger memory capacity you'll generally want to use as large as possible, e.g. 128, 256, 512, etc
- Increasing data size - for example instead of using images that are 32x32, use 224x224 or 336x336, also you could use an increased embedding size for your data
- Increase the model size - for example instead of using a model with 1M parameters, use a model with 10M parameters
- Decreasing data transfer - since bandwidth costs (transferring data) will slow down a GPU (because it wants to compute on data)
As a result of doing the above, your relative speedups should be better.
E.g. overall training time may take longer but not linearly.
Resource for learning how to improve PyTorch model speed - https://sebastianraschka.com/blog/2023/pytorch-faster.html
Note: This concept of using as much data on the GPU as possible isn't restricted specificially to PyTorch 2.0, it applies to all versions on PyTorch and basically all models that train on GPUs.
2.3 Checking the memory limits of our GPU
Can do so using torch.cuda - https://pytorch.org/docs/stable/generated/torch.cuda.mem_get_info.html#torch.cuda.mem_get_info
# Check available GPU memory and total GPU memory
total_free_gpu_memory, total_gpu_memory = torch.cuda.mem_get_info()
print(f"Total free GPU memory: {round(total_free_gpu_memory * 1e-9, 3)} GB")
print(f"Total GPU memory: {round(total_gpu_memory * 1e-9, 3)} GB") Output
Total free GPU memory: 41.048 GB Total GPU memory: 42.482 GB
- If the GPU has 16GB+ of free memory, set batch size to 128
- If the GPU has less than 16GB of free, set batch size to 32
# Set batch size depending on amount of GPU memory
total_free_gpu_memory_gb = round(total_free_gpu_memory * 1e-9, 3)
if total_free_gpu_memory_gb >= 16:
BATCH_SIZE = 128 # Note: you could experiment with higher values here if you like.
IMAGE_SIZE = 224
print(f"GPU memory available is {total_free_gpu_memory_gb} GB, using batch size of {BATCH_SIZE} and image size {IMAGE_SIZE}")
else:
BATCH_SIZE = 32
IMAGE_SIZE = 128
print(f"GPU memory available is {total_free_gpu_memory_gb} GB, using batch size of {BATCH_SIZE} and image size {IMAGE_SIZE}")Output
GPU memory available is 41.048 GB, using batch size of 128 and image size 224
transforms.crop_size = 224
transforms.resize_size = 224
print(f"Updated data transforms:\n{transforms}")Output
Updated data transforms:
ImageClassification(
crop_size=224
resize_size=224
mean=[0.485, 0.456, 0.406]
std=[0.229, 0.224, 0.225]
interpolation=InterpolationMode.BILINEAR
)
2.4 More potential speedups with TF32
TF32 = TensorFloat32
TesnorFloat32 = a datatype that bridges Float32 and Float16
Float32 = a number is represented by 32 bytes (e.g. 010101010110011 = 7)
Float16 = a number is represented by 16 bytes (e.g. 01010101 = 4)
See more on precision in computing: https://en.wikipedia.org/wiki/Precision_(computer_science)
What we want is:
- Fast model training
- Accurate model training
TensorFloat32 = a datatype from NVIDIA which combines float32 and float16
TF32 is available on Ampere GPUs+
GPU_SCOREOutput
(8, 0)
if GPU_SCORE >= (8, 0): # check if GPU is compatiable with TF32
print(f"[INFO] Using GPU with score: {GPU_SCORE}, enabling TensorFloat32")
torch.backends.cuda.matmul.allow_tf32 = True
else:
print(f"[INFO] Using GPU with score: {GPU_SCORE}, TensorFloat32 not available")
torch.backends.cuda.matmul.allow_tf32 = FalseOutput
[INFO] Using GPU with score: (8, 0), enabling TensorFloat32
2.5 Preparing datasets
As before, we discussed we're going to use CIFAR10.
Homepage - https://www.cs.toronto.edu/~kriz/cifar.html
We can download the dataset from torchvision - https://pytorch.org/vision/main/generated/torchvision.datasets.CIFAR10.html
transformsOutput
ImageClassification(
crop_size=224
resize_size=224
mean=[0.485, 0.456, 0.406]
std=[0.229, 0.224, 0.225]
interpolation=InterpolationMode.BILINEAR
)# Create train and test datasets
import torchvision
train_dataset = torchvision.datasets.CIFAR10(root=".", # where to store data
train=True, # do we want training dataset?
download=True,
transform=transforms
)
test_dataset = torchvision.datasets.CIFAR10(root=".",
train=False,
download=True,
transform=transforms)
# Get the length of the datasets
train_len = len(train_dataset)
test_len = len(test_dataset)
print(f"[INFO] Train dataset length: {train_len}")
print(f"[INFO] Test dataset length: {test_len}")Output
Downloading https://www.cs.toronto.edu/~kriz/cifar-10-python.tar.gz to ./cifar-10-python.tar.gz
100%|██████████| 170498071/170498071 [00:13<00:00, 12868066.06it/s]
Extracting ./cifar-10-python.tar.gz to . Files already downloaded and verified [INFO] Train dataset length: 50000 [INFO] Test dataset length: 10000
train_dataset[0][1]Output
6
2.6 Create DataLoaders
Next:
- Turn datasets into DataLoaders
BATCH_SIZE * 2Output
256
from torch.utils.data import DataLoader
import os
NUM_WORKERS = os.cpu_count() # we want highest number of CPU cores to load data to GPU
train_dataloader = DataLoader(dataset=train_dataset,
batch_size=BATCH_SIZE,
shuffle=True,
num_workers=NUM_WORKERS)
test_dataloader = DataLoader(dataset=test_dataset,
batch_size=BATCH_SIZE,
shuffle=False,
num_workers=NUM_WORKERS)
# Print details:
print(f"Train dataloader num batches: {len(train_dataloader)} of batch size: {BATCH_SIZE}")
print(f"Test dataloader num batches: {len(test_dataloader)} of batch size: {BATCH_SIZE}")
print(f"Using num workers to load data (more is generally better): {NUM_WORKERS}")Output
Train dataloader num batches: 391 of batch size: 128 Test dataloader num batches: 79 of batch size: 128 Using num workers to load data (more is generally better): 12
2.7 Creating training and test loops
Want to create:
- Training and test loops + a timing step for each, so we know how long our models take to train/test
We covered this functionality in previous sections, one is here: https://www.learnpytorch.io/05_pytorch_going_modular/#4-creating-train_step-and-test_step-functions-and-train-to-combine-them
import time
from tqdm.auto import tqdm
from typing import Dict, List, Tuple
def train_step(epoch: int,
model: torch.nn.Module,
dataloader: torch.utils.data.DataLoader,
loss_fn: torch.nn.Module,
optimizer: torch.optim.Optimizer,
device: torch.device,
disable_progress_bar: bool = False) -> Tuple[float, float]:
"""Trains a PyTorch model for a single epoch.
Turns a target PyTorch model to training mode and then
runs through all of the required training steps (forward
pass, loss calculation, optimizer step).
Args:
model: A PyTorch model to be trained.
dataloader: A DataLoader instance for the model to be trained on.
loss_fn: A PyTorch loss function to minimize.
optimizer: A PyTorch optimizer to help minimize the loss function.
device: A target device to compute on (e.g. "cuda" or "cpu").
Returns:
A tuple of training loss and training accuracy metrics.
In the form (train_loss, train_accuracy). For example:
(0.1112, 0.8743)
"""
# Put model in train mode
model.train()
# Setup train loss and train accuracy values
train_loss, train_acc = 0, 0
# Loop through data loader data batches
progress_bar = tqdm(
enumerate(dataloader),
desc=f"Training Epoch {epoch}",
total=len(dataloader),
disable=disable_progress_bar
)
for batch, (X, y) in progress_bar:
# Send data to target device
X, y = X.to(device), y.to(device)
# 1. Forward pass
y_pred = model(X)
# 2. Calculate and accumulate loss
loss = loss_fn(y_pred, y)
train_loss += loss.item()
# 3. Optimizer zero grad
optimizer.zero_grad()
# 4. Loss backward
loss.backward()
# 5. Optimizer step
optimizer.step()
# Calculate and accumulate accuracy metric across all batches
y_pred_class = torch.argmax(torch.softmax(y_pred, dim=1), dim=1)
train_acc += (y_pred_class == y).sum().item()/len(y_pred)
# Update progress bar
progress_bar.set_postfix(
{
"train_loss": train_loss / (batch + 1),
"train_acc": train_acc / (batch + 1),
}
)
# Adjust metrics to get average loss and accuracy per batch
train_loss = train_loss / len(dataloader)
train_acc = train_acc / len(dataloader)
return train_loss, train_acc
def test_step(epoch: int,
model: torch.nn.Module,
dataloader: torch.utils.data.DataLoader,
loss_fn: torch.nn.Module,
device: torch.device,
disable_progress_bar: bool = False) -> Tuple[float, float]:
"""Tests a PyTorch model for a single epoch.
Turns a target PyTorch model to "eval" mode and then performs
a forward pass on a testing dataset.
Args:
model: A PyTorch model to be tested.
dataloader: A DataLoader instance for the model to be tested on.
loss_fn: A PyTorch loss function to calculate loss on the test data.
device: A target device to compute on (e.g. "cuda" or "cpu").
Returns:
A tuple of testing loss and testing accuracy metrics.
In the form (test_loss, test_accuracy). For example:
(0.0223, 0.8985)
"""
# Put model in eval mode
model.eval()
# Setup test loss and test accuracy values
test_loss, test_acc = 0, 0
# Loop through data loader data batches
progress_bar = tqdm(
enumerate(dataloader),
desc=f"Testing Epoch {epoch}",
total=len(dataloader),
disable=disable_progress_bar
)
# Turn on inference context manager
with torch.no_grad(): # no_grad() required for PyTorch 2.0, I found some errors with `torch.inference_mode()`, please let me know if this is not the case
# Loop through DataLoader batches
for batch, (X, y) in progress_bar:
# Send data to target device
X, y = X.to(device), y.to(device)
# 1. Forward pass
test_pred_logits = model(X)
# 2. Calculate and accumulate loss
loss = loss_fn(test_pred_logits, y)
test_loss += loss.item()
# Calculate and accumulate accuracy
test_pred_labels = test_pred_logits.argmax(dim=1)
test_acc += ((test_pred_labels == y).sum().item()/len(test_pred_labels))
# Update progress bar
progress_bar.set_postfix(
{
"test_loss": test_loss / (batch + 1),
"test_acc": test_acc / (batch + 1),
}
)
# Adjust metrics to get average loss and accuracy per batch
test_loss = test_loss / len(dataloader)
test_acc = test_acc / len(dataloader)
return test_loss, test_acc
def train(model: torch.nn.Module,
train_dataloader: torch.utils.data.DataLoader,
test_dataloader: torch.utils.data.DataLoader,
optimizer: torch.optim.Optimizer,
loss_fn: torch.nn.Module,
epochs: int,
device: torch.device,
disable_progress_bar: bool = False) -> Dict[str, List]:
"""Trains and tests a PyTorch model.
Passes a target PyTorch models through train_step() and test_step()
functions for a number of epochs, training and testing the model
in the same epoch loop.
Calculates, prints and stores evaluation metrics throughout.
Args:
model: A PyTorch model to be trained and tested.
train_dataloader: A DataLoader instance for the model to be trained on.
test_dataloader: A DataLoader instance for the model to be tested on.
optimizer: A PyTorch optimizer to help minimize the loss function.
loss_fn: A PyTorch loss function to calculate loss on both datasets.
epochs: An integer indicating how many epochs to train for.
device: A target device to compute on (e.g. "cuda" or "cpu").
Returns:
A dictionary of training and testing loss as well as training and
testing accuracy metrics. Each metric has a value in a list for
each epoch.
In the form: {train_loss: [...],
train_acc: [...],
test_loss: [...],
test_acc: [...]}
For example if training for epochs=2:
{train_loss: [2.0616, 1.0537],
train_acc: [0.3945, 0.3945],
test_loss: [1.2641, 1.5706],
test_acc: [0.3400, 0.2973]}
"""
# Create empty results dictionary
results = {"train_loss": [],
"train_acc": [],
"test_loss": [],
"test_acc": [],
"train_epoch_time": [],
"test_epoch_time": []
}
# Loop through training and testing steps for a number of epochs
for epoch in tqdm(range(epochs), disable=disable_progress_bar):
# Perform training step and time it
train_epoch_start_time = time.time()
train_loss, train_acc = train_step(epoch=epoch,
model=model,
dataloader=train_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
device=device,
disable_progress_bar=disable_progress_bar)
train_epoch_end_time = time.time()
train_epoch_time = train_epoch_end_time - train_epoch_start_time
# Perform testing step and time it
test_epoch_start_time = time.time()
test_loss, test_acc = test_step(epoch=epoch,
model=model,
dataloader=test_dataloader,
loss_fn=loss_fn,
device=device,
disable_progress_bar=disable_progress_bar)
test_epoch_end_time = time.time()
test_epoch_time = test_epoch_end_time - test_epoch_start_time
# Print out what's happening
print(
f"Epoch: {epoch+1} | "
f"train_loss: {train_loss:.4f} | "
f"train_acc: {train_acc:.4f} | "
f"test_loss: {test_loss:.4f} | "
f"test_acc: {test_acc:.4f} | "
f"train_epoch_time: {train_epoch_time:.4f} | "
f"test_epoch_time: {test_epoch_time:.4f}"
)
# Update results dictionary
results["train_loss"].append(train_loss)
results["train_acc"].append(train_acc)
results["test_loss"].append(test_loss)
results["test_acc"].append(test_acc)
results["train_epoch_time"].append(train_epoch_time)
results["test_epoch_time"].append(test_epoch_time)
# Return the filled results at the end of the epochs
return results3. Time models across a single run
Experiment 1: single run without torch.compile() for 5 epochs
3.1 Experiment 1 - Single run, no compile
# Set the number of epochs
NUM_EPOCHS = 5
# Set the learning rate as a constant
LEARNING_RATE = 0.003Note: Depending on your GPU/machine, the following code may take a while to run. E.g. in my experience on an A100, it takes about ~7 minutes.
# Create model
model, _ = create_model()
model.to(device)
# Create loss function and optimizer
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr=LEARNING_RATE)
# Train model and track the results
single_run_no_compile_results = train(model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
epochs=NUM_EPOCHS,
device=device)Output
0%| | 0/5 [00:00<?, ?it/s]
Training Epoch 0: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 0: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 0.7612 | train_acc: 0.7361 | test_loss: 0.6752 | test_acc: 0.7738 | train_epoch_time: 75.7051 | test_epoch_time: 7.6668
Training Epoch 1: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 1: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 2 | train_loss: 0.4214 | train_acc: 0.8545 | test_loss: 0.4838 | test_acc: 0.8338 | train_epoch_time: 71.1667 | test_epoch_time: 7.5469
Training Epoch 2: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 2: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 3 | train_loss: 0.2986 | train_acc: 0.8959 | test_loss: 0.4052 | test_acc: 0.8618 | train_epoch_time: 71.2197 | test_epoch_time: 7.4989
Training Epoch 3: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 3: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 4 | train_loss: 0.2270 | train_acc: 0.9201 | test_loss: 0.4700 | test_acc: 0.8494 | train_epoch_time: 71.3631 | test_epoch_time: 7.9202
Training Epoch 4: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 4: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 5 | train_loss: 0.1704 | train_acc: 0.9397 | test_loss: 0.3665 | test_acc: 0.8804 | train_epoch_time: 71.3793 | test_epoch_time: 7.7546
3.2 Experiment 2, single, using torch.compile()
Same setup as experiment 1 except with the new line torch.compile().
# Create model and transforms
model, _ = create_model()
model.to(device)
# Create loss function and optimizer
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr=LEARNING_RATE)
# Compile the model (requires PyTorch 2.0+)
import time
compile_start_time = time.time()
### New in PyTorch 2.x ###
compile_model = torch.compile(model) # NOTE: this should ideally be "compiled_model" not "compile_model"
compile_model.to(device) # make sure compiled on device
compile_end_time = time.time()
compile_time = compile_end_time - compile_start_time
print(f"Time to compile: {compile_time} |\nNote: the first time you compile a model/train a compiled model, the first epoch may take longer due to optimizations happening behind the scenes")
# Train the compiled model
single_run_compile_results = train(model=compile_model, # NOTE: this is an error! (leaving it in for future reference)
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
epochs=NUM_EPOCHS,
device=device)Output
Time to compile: 0.0066258907318115234 | Note: the first time you compile a model/train a compiled model, the first epoch may take longer due to optimizations happening behind the scenes
0%| | 0/5 [00:00<?, ?it/s]
Training Epoch 0: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 0: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 0.7831 | train_acc: 0.7294 | test_loss: 0.7470 | test_acc: 0.7410 | train_epoch_time: 118.4845 | test_epoch_time: 25.6489
Training Epoch 1: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 1: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 2 | train_loss: 0.4346 | train_acc: 0.8508 | test_loss: 0.5180 | test_acc: 0.8230 | train_epoch_time: 64.4995 | test_epoch_time: 7.1518
Training Epoch 2: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 2: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 3 | train_loss: 0.3171 | train_acc: 0.8909 | test_loss: 0.5831 | test_acc: 0.8085 | train_epoch_time: 64.7863 | test_epoch_time: 7.1712
Training Epoch 3: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 3: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 4 | train_loss: 0.2345 | train_acc: 0.9196 | test_loss: 0.4294 | test_acc: 0.8495 | train_epoch_time: 64.5203 | test_epoch_time: 7.1254
Training Epoch 4: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 4: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 5 | train_loss: 0.1723 | train_acc: 0.9399 | test_loss: 0.3651 | test_acc: 0.8841 | train_epoch_time: 64.4010 | test_epoch_time: 7.2215
# Create model and transforms
model, transforms = create_model()
model.to(device)
# Create loss function and optimizer
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr=LEARNING_RATE)
# Compile the model and time how long it takes
compile_start_time = time.time()
### New in PyTorch 2.x ###
compiled_model = torch.compile(model)
##########################
compile_end_time = time.time()
compile_time = compile_end_time - compile_start_time
print(f"Time to compile: {compile_time} | Note: The first time you compile your model, the first few epochs will be slower than subsequent runs.")
# Train the compiled model
single_run_compile_results = train(model=compiled_model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
epochs=NUM_EPOCHS,
device=device)3.3 Compare the results of experiment 1 and 2
# Turn experiment results into dataframes
import pandas as pd
single_run_no_compile_results_df = pd.DataFrame(single_run_no_compile_results)
single_run_compile_results_df = pd.DataFrame(single_run_compile_results) single_run_no_compile_results_dfOutput
train_loss train_acc test_loss test_acc train_epoch_time \ 0 0.761156 0.736069 0.675165 0.773833 75.705132 1 0.421439 0.854536 0.483774 0.833762 71.166738 2 0.298607 0.895896 0.405220 0.861847 71.219677 3 0.227025 0.920137 0.470029 0.849387 71.363075 4 0.170397 0.939738 0.366482 0.880439 71.379308 test_epoch_time 0 7.666803 1 7.546933 2 7.498850 3 7.920234 4 7.754647
| train_loss | train_acc | test_loss | test_acc | train_epoch_time | test_epoch_time | |
|---|---|---|---|---|---|---|
| 0 | 0.761156 | 0.736069 | 0.675165 | 0.773833 | 75.705132 | 7.666803 |
| 1 | 0.421439 | 0.854536 | 0.483774 | 0.833762 | 71.166738 | 7.546933 |
| 2 | 0.298607 | 0.895896 | 0.405220 | 0.861847 | 71.219677 | 7.498850 |
| 3 | 0.227025 | 0.920137 | 0.470029 | 0.849387 | 71.363075 | 7.920234 |
| 4 | 0.170397 | 0.939738 | 0.366482 | 0.880439 | 71.379308 | 7.754647 |
single_run_compile_results_dfOutput
train_loss train_acc test_loss test_acc train_epoch_time \ 0 0.783119 0.729360 0.746991 0.741001 118.484514 1 0.434553 0.850823 0.518003 0.822983 64.499461 2 0.317094 0.890861 0.583142 0.808544 64.786344 3 0.234511 0.919625 0.429354 0.849486 64.520294 4 0.172298 0.939878 0.365066 0.884098 64.401047 test_epoch_time 0 25.648897 1 7.151754 2 7.171236 3 7.125437 4 7.221537
| train_loss | train_acc | test_loss | test_acc | train_epoch_time | test_epoch_time | |
|---|---|---|---|---|---|---|
| 0 | 0.783119 | 0.729360 | 0.746991 | 0.741001 | 118.484514 | 25.648897 |
| 1 | 0.434553 | 0.850823 | 0.518003 | 0.822983 | 64.499461 | 7.151754 |
| 2 | 0.317094 | 0.890861 | 0.583142 | 0.808544 | 64.786344 | 7.171236 |
| 3 | 0.234511 | 0.919625 | 0.429354 | 0.849486 | 64.520294 | 7.125437 |
| 4 | 0.172298 | 0.939878 | 0.365066 | 0.884098 | 64.401047 | 7.221537 |
# Create filename to save the results
DATASET_NAME = "CIFAR10"
MODEL_NAME = "ResNet50"import matplotlib.pyplot as plt
import numpy as np
def plot_mean_epoch_times(non_compiled_results: pd.DataFrame,
compiled_results: pd.DataFrame,
multi_runs: bool=False,
num_runs: int=0,
save: bool=False,
save_path: str="",
dataset_name: str=DATASET_NAME,
model_name: str=MODEL_NAME,
num_epochs: int=NUM_EPOCHS,
image_size: int=IMAGE_SIZE,
batch_size: int=BATCH_SIZE) -> plt.figure:
# Get the mean epoch times from the non-compiled models
mean_train_epoch_time = non_compiled_results.train_epoch_time.mean()
mean_test_epoch_time = non_compiled_results.test_epoch_time.mean()
mean_results = [mean_train_epoch_time, mean_test_epoch_time]
# Get the mean epoch times from the compiled models
mean_compile_train_epoch_time = compiled_results.train_epoch_time.mean()
mean_compile_test_epoch_time = compiled_results.test_epoch_time.mean()
mean_compile_results = [mean_compile_train_epoch_time, mean_compile_test_epoch_time]
# Calculate the percentage difference between the mean compile and non-compile train epoch times
train_epoch_time_diff = mean_compile_train_epoch_time - mean_train_epoch_time
train_epoch_time_diff_percent = (train_epoch_time_diff / mean_train_epoch_time) * 100
# Calculate the percentage difference between the mean compile and non-compile test epoch times
test_epoch_time_diff = mean_compile_test_epoch_time - mean_test_epoch_time
test_epoch_time_diff_percent = (test_epoch_time_diff / mean_test_epoch_time) * 100
# Print the mean difference percentages
print(f"Mean train epoch time difference: {round(train_epoch_time_diff_percent, 3)}% (negative means faster)")
print(f"Mean test epoch time difference: {round(test_epoch_time_diff_percent, 3)}% (negative means faster)")
# Create a bar plot of the mean train and test epoch time for both compiled and non-compiled models
plt.figure(figsize=(10, 7))
width = 0.3
x_indicies = np.arange(len(mean_results))
plt.bar(x=x_indicies, height=mean_results, width=width, label="non_compiled_results")
plt.bar(x=x_indicies + width, height=mean_compile_results, width=width, label="compiled_results")
plt.xticks(x_indicies + width / 2, ("Train Epoch", "Test Epoch"))
plt.ylabel("Mean epoch time (seconds, lower is better)")
# Create the title based on the parameters passed to the function
if multi_runs:
plt.suptitle("Multiple run results")
plt.title(f"GPU: {gpu_name} | Epochs: {num_epochs} ({num_runs} runs) | Data: {dataset_name} | Model: {model_name} | Image size: {image_size} | Batch size: {batch_size}")
else:
plt.suptitle("Single run results")
plt.title(f"GPU: {gpu_name} | Epochs: {num_epochs} | Data: {dataset_name} | Model: {model_name} | Image size: {image_size} | Batch size: {batch_size}")
plt.legend();
# Save the figure
if save:
assert save_path != "", "Please specify a save path to save the model figure to via the save_path parameter."
plt.savefig(save_path)
print(f"[INFO] Plot saved to {save_path}")# Create directory for saving figures
import os
dir_to_save_figures_in = "pytorch_2_results/figures/"
os.makedirs(dir_to_save_figures_in, exist_ok=True)
# Create a save path for the single run results
save_path_single_run = f"{dir_to_save_figures_in}single_run_{GPU_NAME}_{MODEL_NAME}_{DATASET_NAME}_{IMAGE_SIZE}_train_epoch_time.png"
print(f"[INFO] Save path for single run results: {save_path_single_run}")
# Plot the results and save the figure(s)
plot_mean_epoch_times(non_compiled_results=single_run_no_compile_results_df,
compiled_results=single_run_compile_results_df,
multi_runs=False,
save_path=save_path_single_run,
save=True)Output
[INFO] Save path for single run results: pytorch_2_results/figures/single_run_NVIDIA_A100-SXM4-40GB_ResNet50_CIFAR10_224_train_epoch_time.png Mean train epoch time difference: 4.395% (negative means faster) Mean test epoch time difference: 41.502% (negative means faster) [INFO] Plot saved to pytorch_2_results/figures/single_run_NVIDIA_A100-SXM4-40GB_ResNet50_CIFAR10_224_train_epoch_time.png
<Figure size 1000x700 with 1 Axes>
3.4 Save single run results to file with GPU details
# Make a directory for single_run results
import os
pytorch_2_results_dir = "pytorch_2_results"
pytorch_2_single_run_results_dir = f"{pytorch_2_results_dir}/single_run_results"
os.makedirs(pytorch_2_single_run_results_dir, exist_ok=True)
# Create filenames for each of the dataframes
save_name_for_non_compiled_results = f"single_run_non_compiled_results_{DATASET_NAME}_{MODEL_NAME}_{GPU_NAME}.csv"
save_name_for_compiled_results = f"single_run_compiled_results_{DATASET_NAME}_{MODEL_NAME}_{GPU_NAME}.csv"
# Create filepaths to save the results to
single_run_no_compile_save_path = f"{pytorch_2_single_run_results_dir}/{save_name_for_non_compiled_results}"
single_run_compile_save_path = f"{pytorch_2_single_run_results_dir}/{save_name_for_compiled_results}"
print(f"[INFO] Saving non-compiled experiment 1 results to: {single_run_no_compile_save_path}")
print(f"[INFO] Saving compiled experiment 2 results to: {single_run_compile_save_path}")
# Save the results
single_run_no_compile_results_df.to_csv(single_run_no_compile_save_path)
single_run_compile_results_df.to_csv(single_run_compile_save_path)Output
[INFO] Saving non-compiled experiment 1 results to: pytorch_2_results/single_run_results/single_run_non_compiled_results_CIFAR10_ResNet50_NVIDIA_A100-SXM4-40GB.csv [INFO] Saving compiled experiment 2 results to: pytorch_2_results/single_run_results/single_run_compiled_results_CIFAR10_ResNet50_NVIDIA_A100-SXM4-40GB.csv
4. Time models across multiple runs
Time for multi-run experiments!
- Experiment 3 - 3x5 epochs without
torch.compile() - Experiment 4 - 3x5 epochs with
torch.compile()
Before running experiment 3 and 4, let's create 3 functions:
- Experiment 3:
create_and_train_non_compiled_model()- creates and trains a model for a single run (can put this function in a loop for mulitple runs). - Experiment 4:
create_compiled_model()- creates and compiles a model, returns the compiled model. - Experiment 4:
train_compiled_model()- trains a compiled model for a single run (can put this in a loop to train for multiple runs).
def create_and_train_non_compiled_model(epochs=NUM_EPOCHS,
learning_rate=LEARNING_RATE,
disable_progress_bar=False):
"""
Create and train a non-compiled PyTorch model.
"""
model, _ = create_model()
model.to(device)
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(model.parameters(),
lr=learning_rate)
results = train(model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
epochs=epochs,
device=device,
disable_progress_bar=disable_progress_bar)
return results
def create_compiled_model():
"""
Create a compiled PyTorch model and return it.
"""
model, _ = create_model()
model.to(device)
compile_start_time = time.time()
### New in PyTorch 2.0!!! ###
compiled_model = torch.compile(model)
compile_end_time = time.time()
compile_time = compile_end_time - compile_start_time
print(f"[INFO] Model compile time: {compile_time}")
return compiled_model
def train_compiled_model(model=compiled_model,
epochs=NUM_EPOCHS,
learning_rate=LEARNING_RATE,
disable_progress_bar=False):
"""
Train a compiled model and return the results.
"""
loss_fn = torch.nn.CrossEntropyLoss()
optimizer = torch.optim.Adam(compiled_model.parameters(),
lr=learning_rate)
compile_results = train(model=model,
train_dataloader=train_dataloader,
test_dataloader=test_dataloader,
loss_fn=loss_fn,
optimizer=optimizer,
epochs=epochs,
device=device,
disable_progress_bar=disable_progress_bar)
return compile_results4.1 Experiment 3 - Multiple runs, no compile
Note: Because we are running multiple runs, the code below may take a while to run. If one single run take ~7 minutes on an A100, the following code could take 20 minutes on an A100.
One of the most painful things of machine learning is that models take a while to train and one of the most beautiful things of machine learning is that models take a while to train. So plenty of time to go for a walk.
# Run non-compiled model for multiple runs
NUM_RUNS = 3
NUM_EPOCHS = 5
# Create an empty list to store multiple run results
non_compile_results_multiple_runs = []
# Run non-compiled model for multiple runs
for i in tqdm(range(NUM_RUNS)):
print(f"[INFO] Run {i+1} of {NUM_RUNS} for non-compiled model")
results = create_and_train_non_compiled_model(epochs=NUM_EPOCHS, disable_progress_bar=True)
non_compile_results_multiple_runs.append(results)Output
0%| | 0/3 [00:00<?, ?it/s]
[INFO] Run 1 of 3 for non-compiled model Epoch: 1 | train_loss: 0.7901 | train_acc: 0.7267 | test_loss: 0.6552 | test_acc: 0.7737 | train_epoch_time: 70.4700 | test_epoch_time: 7.6880 Epoch: 2 | train_loss: 0.4430 | train_acc: 0.8489 | test_loss: 0.5212 | test_acc: 0.8210 | train_epoch_time: 70.4249 | test_epoch_time: 7.9523 Epoch: 3 | train_loss: 0.3196 | train_acc: 0.8905 | test_loss: 0.4269 | test_acc: 0.8580 | train_epoch_time: 70.5179 | test_epoch_time: 7.5705 Epoch: 4 | train_loss: 0.2378 | train_acc: 0.9174 | test_loss: 0.3968 | test_acc: 0.8716 | train_epoch_time: 70.3693 | test_epoch_time: 7.6105 Epoch: 5 | train_loss: 0.1776 | train_acc: 0.9381 | test_loss: 0.4447 | test_acc: 0.8690 | train_epoch_time: 70.4685 | test_epoch_time: 7.5161 [INFO] Run 2 of 3 for non-compiled model Epoch: 1 | train_loss: 0.8053 | train_acc: 0.7198 | test_loss: 0.6471 | test_acc: 0.7709 | train_epoch_time: 70.9584 | test_epoch_time: 7.5721 Epoch: 2 | train_loss: 0.4451 | train_acc: 0.8475 | test_loss: 0.5613 | test_acc: 0.8169 | train_epoch_time: 70.4403 | test_epoch_time: 7.4987 Epoch: 3 | train_loss: 0.3173 | train_acc: 0.8916 | test_loss: 0.4261 | test_acc: 0.8548 | train_epoch_time: 70.4179 | test_epoch_time: 7.6314 Epoch: 4 | train_loss: 0.2412 | train_acc: 0.9165 | test_loss: 0.4067 | test_acc: 0.8668 | train_epoch_time: 70.5341 | test_epoch_time: 7.7344 Epoch: 5 | train_loss: 0.1729 | train_acc: 0.9392 | test_loss: 0.3699 | test_acc: 0.8767 | train_epoch_time: 70.7180 | test_epoch_time: 7.7019 [INFO] Run 3 of 3 for non-compiled model Epoch: 1 | train_loss: 0.7796 | train_acc: 0.7280 | test_loss: 1.0861 | test_acc: 0.7201 | train_epoch_time: 70.2628 | test_epoch_time: 7.6740 Epoch: 2 | train_loss: 0.4326 | train_acc: 0.8505 | test_loss: 0.6013 | test_acc: 0.7965 | train_epoch_time: 70.4564 | test_epoch_time: 7.5600 Epoch: 3 | train_loss: 0.3124 | train_acc: 0.8926 | test_loss: 0.4560 | test_acc: 0.8483 | train_epoch_time: 70.3929 | test_epoch_time: 7.5944 Epoch: 4 | train_loss: 0.2347 | train_acc: 0.9183 | test_loss: 0.3845 | test_acc: 0.8734 | train_epoch_time: 70.5535 | test_epoch_time: 7.4801 Epoch: 5 | train_loss: 0.1728 | train_acc: 0.9396 | test_loss: 0.4755 | test_acc: 0.8475 | train_epoch_time: 70.4916 | test_epoch_time: 7.7536
# Go through the non_compile_results_multiple_runs and create a dataframe for each and then combine each dataframe
non_compile_results_dfs = []
for result in non_compile_results_multiple_runs:
result_df = pd.DataFrame(result)
non_compile_results_dfs.append(result_df)
non_compile_results_multiple_runs_df = pd.concat(non_compile_results_dfs)
# Get the average results across the board
non_compile_results_multiple_runs_df = non_compile_results_multiple_runs_df.groupby(non_compile_results_multiple_runs_df.index).mean()
non_compile_results_multiple_runs_dfOutput
train_loss train_acc test_loss test_acc train_epoch_time \ 0 0.791669 0.724828 0.796088 0.754912 70.563755 1 0.440249 0.849002 0.561259 0.811445 70.440525 2 0.316451 0.891565 0.436321 0.853705 70.442901 3 0.237918 0.917394 0.395977 0.870616 70.485643 4 0.174429 0.938939 0.430049 0.864386 70.559365 test_epoch_time 0 7.644714 1 7.670343 2 7.598755 3 7.608333 4 7.657195
| train_loss | train_acc | test_loss | test_acc | train_epoch_time | test_epoch_time | |
|---|---|---|---|---|---|---|
| 0 | 0.791669 | 0.724828 | 0.796088 | 0.754912 | 70.563755 | 7.644714 |
| 1 | 0.440249 | 0.849002 | 0.561259 | 0.811445 | 70.440525 | 7.670343 |
| 2 | 0.316451 | 0.891565 | 0.436321 | 0.853705 | 70.442901 | 7.598755 |
| 3 | 0.237918 | 0.917394 | 0.395977 | 0.870616 | 70.485643 | 7.608333 |
| 4 | 0.174429 | 0.938939 | 0.430049 | 0.864386 | 70.559365 | 7.657195 |
4.2 Experiment 4, multiple runs, with compile
# Create compiled model
compiled_model = create_compiled_model()
# Create an empty list to store compiled model result runs
compiled_results_multiple_runs = []
# Run compiled model for multiple training runs
for i in tqdm(range(NUM_RUNS)):
print(f"[INFO] Run {i+1} of {NUM_RUNS} for compiled model")
# Train compiled model and append results (note: model will only be compiled once at the start of the first epoch of the first run)
results = train_compiled_model(model=compiled_model,
epochs=NUM_EPOCHS,
disable_progress_bar=False)
compiled_results_multiple_runs.append(results)Output
[INFO] Model compile time: 0.0026314258575439453
0%| | 0/3 [00:00<?, ?it/s]
[INFO] Run 1 of 3 for compiled model
0%| | 0/5 [00:00<?, ?it/s]
Training Epoch 0: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 0: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 0.7959 | train_acc: 0.7234 | test_loss: 0.7443 | test_acc: 0.7413 | train_epoch_time: 103.3092 | test_epoch_time: 22.3508
Training Epoch 1: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 1: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 2 | train_loss: 0.4377 | train_acc: 0.8504 | test_loss: 0.5481 | test_acc: 0.8118 | train_epoch_time: 63.8893 | test_epoch_time: 7.2228
Training Epoch 2: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 2: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 3 | train_loss: 0.3152 | train_acc: 0.8920 | test_loss: 0.5006 | test_acc: 0.8373 | train_epoch_time: 64.1252 | test_epoch_time: 6.9573
Training Epoch 3: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 3: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 4 | train_loss: 0.2342 | train_acc: 0.9190 | test_loss: 0.4468 | test_acc: 0.8478 | train_epoch_time: 64.0888 | test_epoch_time: 6.9980
Training Epoch 4: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 4: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 5 | train_loss: 0.1745 | train_acc: 0.9384 | test_loss: 0.4810 | test_acc: 0.8561 | train_epoch_time: 64.0858 | test_epoch_time: 7.1799 [INFO] Run 2 of 3 for compiled model
0%| | 0/5 [00:00<?, ?it/s]
Training Epoch 0: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 0: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 0.1925 | train_acc: 0.9319 | test_loss: 0.3669 | test_acc: 0.8817 | train_epoch_time: 64.1221 | test_epoch_time: 6.9803
Training Epoch 1: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 1: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 2 | train_loss: 0.1244 | train_acc: 0.9567 | test_loss: 0.3533 | test_acc: 0.8928 | train_epoch_time: 64.0442 | test_epoch_time: 6.8866
Training Epoch 2: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 2: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 3 | train_loss: 0.0890 | train_acc: 0.9687 | test_loss: 0.4817 | test_acc: 0.8640 | train_epoch_time: 64.0605 | test_epoch_time: 7.0766
Training Epoch 3: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 3: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 4 | train_loss: 0.0733 | train_acc: 0.9738 | test_loss: 0.4168 | test_acc: 0.8912 | train_epoch_time: 64.0965 | test_epoch_time: 6.9628
Training Epoch 4: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 4: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 5 | train_loss: 0.0577 | train_acc: 0.9798 | test_loss: 0.4056 | test_acc: 0.8967 | train_epoch_time: 64.3501 | test_epoch_time: 6.9296 [INFO] Run 3 of 3 for compiled model
0%| | 0/5 [00:00<?, ?it/s]
Training Epoch 0: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 0: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 1 | train_loss: 0.0682 | train_acc: 0.9764 | test_loss: 0.4597 | test_acc: 0.8816 | train_epoch_time: 64.3131 | test_epoch_time: 7.0507
Training Epoch 1: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 1: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 2 | train_loss: 0.0537 | train_acc: 0.9814 | test_loss: 0.4043 | test_acc: 0.8969 | train_epoch_time: 64.6288 | test_epoch_time: 6.9666
Training Epoch 2: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 2: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 3 | train_loss: 0.0454 | train_acc: 0.9841 | test_loss: 0.4339 | test_acc: 0.8890 | train_epoch_time: 64.1373 | test_epoch_time: 7.0102
Training Epoch 3: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 3: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 4 | train_loss: 0.0369 | train_acc: 0.9875 | test_loss: 0.4787 | test_acc: 0.8872 | train_epoch_time: 64.1874 | test_epoch_time: 6.9630
Training Epoch 4: 0%| | 0/391 [00:00<?, ?it/s]
Testing Epoch 4: 0%| | 0/79 [00:00<?, ?it/s]
Epoch: 5 | train_loss: 0.0376 | train_acc: 0.9866 | test_loss: 0.5105 | test_acc: 0.8842 | train_epoch_time: 64.0908 | test_epoch_time: 7.1625
# Want to combine all the results into a single average dataframe
compile_results_df = []
for result in compiled_results_multiple_runs:
results_df = pd.DataFrame(result)
compile_results_df.append(results_df)
compile_results_multiple_runs_df = pd.concat(compile_results_df)
# Get the average across all the runs for the compiled model
compile_results_multiple_runs_df = compile_results_multiple_runs_df.groupby(compile_results_multiple_runs_df.index).mean()
compile_results_multiple_runs_dfOutput
train_loss train_acc test_loss test_acc train_epoch_time \ 0 0.352225 0.877225 0.523651 0.834883 77.248146 1 0.205245 0.929492 0.435225 0.867155 64.187451 2 0.149879 0.948252 0.472081 0.863463 64.107671 3 0.114807 0.960094 0.447427 0.875396 64.124233 4 0.089908 0.968229 0.465696 0.878989 64.175556 test_epoch_time 0 12.127237 1 7.025314 2 7.014695 3 6.974602 4 7.090651
| train_loss | train_acc | test_loss | test_acc | train_epoch_time | test_epoch_time | |
|---|---|---|---|---|---|---|
| 0 | 0.352225 | 0.877225 | 0.523651 | 0.834883 | 77.248146 | 12.127237 |
| 1 | 0.205245 | 0.929492 | 0.435225 | 0.867155 | 64.187451 | 7.025314 |
| 2 | 0.149879 | 0.948252 | 0.472081 | 0.863463 | 64.107671 | 7.014695 |
| 3 | 0.114807 | 0.960094 | 0.447427 | 0.875396 | 64.124233 | 6.974602 |
| 4 | 0.089908 | 0.968229 | 0.465696 | 0.878989 | 64.175556 | 7.090651 |
4.3 Compare results of experiment 3 and 4
# Create a directory to save the multi-run figure to
os.makedirs("pytorch_2_results/figures", exist_ok=True)
# Create a path to save the figure for multiple runs
save_path_multi_run = f"pytorch_2_results/figures/multi_run_{GPU_NAME}_{MODEL_NAME}_{DATASET_NAME}_{IMAGE_SIZE}_train_epoch_time.png"
# Plot the mean epoch times for experiment 3 and 4
plot_mean_epoch_times(non_compiled_results=non_compile_results_multiple_runs_df,
compiled_results=compile_results_multiple_runs_df,
multi_runs=True,
num_runs=NUM_RUNS,
save_path=save_path_multi_run,
save=True)Output
Mean train epoch time difference: -5.291% (negative means faster) Mean test epoch time difference: 5.378% (negative means faster) [INFO] Plot saved to pytorch_2_results/figures/multi_run_NVIDIA_A100-SXM4-40GB_ResNet50_CIFAR10_224_train_epoch_time.png
<Figure size 1000x700 with 1 Axes>
4.4 Save multi runs results to file with GPU details
# Make a directory for multi_run results
import os
pytorch_2_results_dir = "pytorch_2_results"
pytorch_2_multi_run_results_dir = f"{pytorch_2_results_dir}/multi_run_results"
os.makedirs(pytorch_2_multi_run_results_dir, exist_ok=True)
# Create filenames for each of the dataframes
save_name_for_multi_run_non_compiled_results = f"multi_run_non_compiled_results_{NUM_RUNS}_runs_{DATASET_NAME}_{MODEL_NAME}_{GPU_NAME}.csv"
save_name_for_multi_run_compiled_results = f"multi_run_compiled_results_{NUM_RUNS}_runs_{DATASET_NAME}_{MODEL_NAME}_{GPU_NAME}.csv"
# Create filepaths to save the results to
multi_run_no_compile_save_path = f"{pytorch_2_multi_run_results_dir}/{save_name_for_non_compiled_results}"
multi_run_compile_save_path = f"{pytorch_2_multi_run_results_dir}/{save_name_for_compiled_results}"
print(f"[INFO] Saving experiment 3 non-compiled results to: {multi_run_no_compile_save_path}")
print(f"[INFO] Saving experiment 4 compiled results to: {multi_run_compile_save_path}")
# Save the results
non_compile_results_multiple_runs_df.to_csv(multi_run_no_compile_save_path)
compile_results_multiple_runs_df.to_csv(multi_run_compile_save_path)Output
[INFO] Saving experiment 3 non-compiled results to: pytorch_2_results/multi_run_results/single_run_non_compiled_results_CIFAR10_ResNet50_NVIDIA_A100-SXM4-40GB.csv [INFO] Saving experiment 4 compiled results to: pytorch_2_results/multi_run_results/single_run_compiled_results_CIFAR10_ResNet50_NVIDIA_A100-SXM4-40GB.csv
5. Possible improvements and extensions
- More powerful CPUs - to load data to the GPU as quickly as possible, relative speedups come from using as much of the GPU as possible.
- Use mixed precision training - uses a combination of float32 and float16 to train a model (generally improves model training time).
- Transformer based models may see more relative speedups than convolutional -
- Train for longer - the longer we train, the more we can benefit from the optimization steps that
torch.compile()does up front.
See more here: https://www.learnpytorch.io/pytorch_2_intro/#5-possible-improvements-and-extensions
6. Resources to learn more
See here: https://www.learnpytorch.io/pytorch_2_intro/#6-resources-to-learn-more
