PyTorch jest paczką oprogramowania ogólnego przeznaczenia, do użycia w skryptach napisanych w języku Python. Jej głównym zastosowaniem jest tworzenie modeli uczenia maszynowego oraz ich uruchamianie na zasobach sprzętowych. Żeby wykonać poniższe przykłady, należy uruchomić zadanie dodając polecenia do kolejki w SLURM wykonując polecenie sbatch job.sh z zawartością widoczną w poniższych przykładach.
Uczenie maszynowe domyślnie odbywa się na CPU. Podłączając maszyny wirtualne posiadające dostęp do GPU, można zmodyfikować to zachowanie i wskazać, z którego GPU ma korzystać PyTorch:
zawartość pliku job-gpu.sh
#!/bin/bash #SBATCH --partition=gpu-a100-80gb #SBATCH --gres=gpu:a100_80gb:1 #SBATCH --time=00:10:00 #SBATCH --nodes=1 module load trytonp/apptainer/1.3.0 singularity exec --nv docker://nvcr.io/nvidia/pytorch:21.11-py3 python3 gpu.py
zawartość pliku gpu.py
import torch
device = 'cuda:0'
print(f"=> Set CUDA device = {device}")
number_of_inputs = 2
input_size = 576
output_size = 10
class NeuralNetwork(torch.nn.Module):
def __init__(self, input_size, output_size):
super(NeuralNetwork, self).__init__()
self.linear_relu_stack = torch.nn.Sequential(
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, output_size),
torch.nn.Softmax(dim=1)
)
def forward(self, x):
y = self.linear_relu_stack(x)
print("=> Model input size", x.size(), "Model output size", y.size())
return y.argmax(1)
model = NeuralNetwork(input_size, output_size)
model.to(device)
print(model)
X = torch.rand(number_of_inputs, input_size, device=device)
y = model(X)
print(f"=> Predicted class: {y}")Uruchomienie:
sbatch job-gpu.sh
Standardowe wyjście w pliku slurm-.out
=> Set CUDA device = cuda:0
NeuralNetwork(
(linear_relu_stack): Sequential(
(0): Linear(in_features=576, out_features=576, bias=True)
(1): ReLU()
(2): Linear(in_features=576, out_features=576, bias=True)
(3): ReLU()
(4): Linear(in_features=576, out_features=10, bias=True)
(5): Softmax(dim=1)
)
)
=> Model input size torch.Size([2, 576]) Model output size torch.Size([2, 10])
=> Predicted class: tensor([6, 6], device='cuda:0')
Jeżeli zbiory danych mają jednakową strukturę, można podzielić obliczenia na kilka urządzeń i uruchomić równolegle wykonanie modelu na wyizolowanym zbiorze danych. Następnie wyniki są zestawiane na wskazanym urządzeniu domyślnym. Rozproszenie obliczeń na kilka kart GPU za pomocą klasy DataParallel:
zawartość pliku job-multigpu-dp.sh
#!/bin/bash #SBATCH --partition=gpu-a100-80gb #SBATCH --gres=gpu:a100_80gb:2 #SBATCH --time=00:10:00 #SBATCH --nodes=1 module load trytonp/apptainer/1.3.0 singularity exec --nv docker://nvcr.io/nvidia/pytorch:21.11-py3 python3 multigpu-dp.py
zawartość pliku multigpu-dp.py
import torch
device = 'cuda:0'
print(f"=> Set master CUDA device = {device}")
number_of_inputs = 2
input_size = 576
output_size = 10
class NeuralNetwork(torch.nn.Module):
def __init__(self, input_size, output_size):
super(NeuralNetwork, self).__init__()
self.linear_relu_stack = torch.nn.Sequential(
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, output_size),
torch.nn.Softmax(dim=1)
)
def forward(self, x):
y = self.linear_relu_stack(x)
print("=> Model input size", x.size(), "Model output size", y.size())
return y.argmax(1)
model = NeuralNetwork(input_size, output_size)
model = torch.nn.DataParallel(model)
model.to(device)
print(model)
X = torch.rand(number_of_inputs, input_size, device=device)
y = model(X)
print(f"=> Predicted class: {y}")Uruchomienie
sbatch job-multigpu-dp.sh
Standardowe wyjście w pliku slurm-.out
=> Set master CUDA device = cuda:0
DataParallel(
(module): NeuralNetwork(
(linear_relu_stack): Sequential(
(0): Linear(in_features=576, out_features=576, bias=True)
(1): ReLU()
(2): Linear(in_features=576, out_features=576, bias=True)
(3): ReLU()
(4): Linear(in_features=576, out_features=10, bias=True)
(5): Softmax(dim=1)
)
)
)
=> Model input size torch.Size([1, 576]) Model output size torch.Size([1, 10])
=> Model input size torch.Size([1, 576]) Model output size torch.Size([1, 10])
=> Predicted class: tensor([9, 9], device='cuda:0')
Rozpraszanie za pomocą klasy DataParallel odbywa się teoretycznie za pomocą zmiany jednej linii w kodzie, jednak nie jest to zalecany sposób do rozpraszania obliczeń:
- Klasa DataParallel działa na jednym procesie wielowątkowo i maksymalnie tylko na jednej maszynie, podczas gdy uczenie za pomocą klasy DistributedDataParallel jest wieloprocesowe i działa zarówno przy uczeniu na jednej jak i wielu maszynach.
- Rozpraszanie obliczeń za pomocą DataParallel jest zwykle wolniejsze niż przy użyciu DistributedDataParallel nawet na pojedynczej maszynie. Jest to spowodowane przez GIL, model replikacji per-iteracyjnej i dodatkowe obciążenie wprowadzone przez rozproszenie danych wejściowych i zbieranie danych wyjściowych.
- W przypadku modeli uczenia maszynowego nie mieszczących się na jednym GPU, należy go zrównoleglić na wiele GPU. DistributedDataParallel (w przeciwieństwie do DataParallel) wspiera rozpraszanie modeli. Gdy DistributedDataParallel jest połączone z modelem równoległym, każdy proces DistributedDataParallel będzie korzystał z modelu równolegle, a wszystkie procesy łącznie będą wykorzystywać dane równolegle.
Rozproszenie obliczeń na kilka kart GPU za pomocą modułu DistributedDataParallel:
zawartość pliku job-multigpu-ddp-one-node.sh
#!/bin/bash #SBATCH --partition=gpu-a100 #SBATCH --gres=gpu:a100:2 #SBATCH --time=00:10:00 #SBATCH --nodes=1 GPU_NUMBER=2 module load trytonp/apptainer/1.3.0 singularity exec --nv docker://nvcr.io/nvidia/pytorch:21.11-py3 torchrun --standalone --nproc_per_node=$GPU_NUMBER multigpu-ddp.py
zawartość pliku multigpu-ddp.py
import os
import torch
local_rank = int(os.environ["LOCAL_RANK"])
device = local_rank
print(f"=> Set CUDA device = cuda:{local_rank}")
torch.distributed.init_process_group("nccl", init_method="env://")
number_of_inputs = 1
input_size = 576
output_size = 10
class NeuralNetwork(torch.nn.Module):
def __init__(self, input_size, output_size):
super(NeuralNetwork, self).__init__()
self.linear_relu_stack = torch.nn.Sequential(
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, output_size),
)
def forward(self, x):
logits = self.linear_relu_stack(x)
print("=> In Model: input size", x.size(), "output size", logits.size())
return logits
model = NeuralNetwork(input_size, output_size)
model.to(local_rank)
model = torch.torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])
print(model)
X = torch.rand(number_of_inputs, input_size, device=local_rank)
logits = model(X)
pred_probab = torch.nn.Softmax(dim=1)(logits).to(local_rank)
y_pred = pred_probab.argmax(1)
print(f"=> Predicted class: {y_pred}")Uruchomienie:
sbatch job-multigpu-ddp-one-node.sh
Standardowe wyjście w pliku slurm-.out
=> Set CUDA device = cuda:1
=> Set CUDA device = cuda:0
DistributedDataParallel(
(module): NeuralNetwork(
(linear_relu_stack): Sequential(
(0): Linear(in_features=576, out_features=576, bias=True)
(1): ReLU()
(2): Linear(in_features=576, out_features=576, bias=True)
(3): ReLU()
(4): Linear(in_features=576, out_features=10, bias=True)
)
)
)
DistributedDataParallel(
(module): NeuralNetwork(
(linear_relu_stack): Sequential(
(0): Linear(in_features=576, out_features=576, bias=True)
(1): ReLU()
(2): Linear(in_features=576, out_features=576, bias=True)
(3): ReLU()
(4): Linear(in_features=576, out_features=10, bias=True)
)
)
)
=> In Model: input size=> In Model: input size torch.Size([1, 576])torch.Size([1, 576]) output sizeoutput size torch.Size([1, 10])torch.Size([1, 10])
=> Predicted class: tensor([4], device='cuda:0')=> Predicted class: tensor([4], device='cuda:1')
Jeżeli nie ma potrzeby użycia backendu obliczeniowego MPI należy korzystać z NCCL ponieważ jako jedyne wspiera InfiniBand i GPUDirect. Dobrą praktyką jest korzystanie ze zmiennych środowiskowych env:// przy inicjalizacji grupy procesów. Rozproszenie obliczeń na kilka kart GPU wielo-węzłowo za pomocą modułu DistributedDataParallel:
zawartość pliku job-multigpu-ddp-two-nodes.sh
#!/bin/bash
#SBATCH --partition=gpu-a100-80gb
#SBATCH --gres=gpu:a100_80gb:1
#SBATCH --time=00:06:00
#SBATCH --nodes=2
WORK_DIR=`pwd`
GPU_NUMBER=1
MASTER_PORT=1235
MASTER_ADDR=$(hostname -I | awk '{print $1}')
for node in $(seq ${SLURM_JOB_NUM_NODES})
do
ssh `scontrol show hostnames ${SLURM_JOB_NODELIST} | awk "NR==${node}"` /bin/bash << EOF &
source /etc/profile.d/modules_tryplus.sh
module load trytonp/apptainer/1.3.0
cd ${WORK_DIR}
singularity exec --nv docker://nvcr.io/nvidia/pytorch:21.11-py3 torchrun --nnodes=${SLURM_JOB_NUM_NODES} --node_rank=$(expr ${node} - 1) --master_addr=${MASTER_ADDR} --master_port=${MASTER_PORT} --nproc_per_node=${GPU_NUMBER} multigpu-ddp.py
EOF
done
zawartość pliku multigpu-ddp.py
import os
import torch
local_rank = int(os.environ["LOCAL_RANK"])
device = local_rank
print(f"=> Set CUDA device = cuda:{local_rank}")
torch.distributed.init_process_group("nccl", init_method="env://")
number_of_inputs = 1
input_size = 576
output_size = 10
class NeuralNetwork(torch.nn.Module):
def __init__(self, input_size, output_size):
super(NeuralNetwork, self).__init__()
self.linear_relu_stack = torch.nn.Sequential(
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, input_size),
torch.nn.ReLU(),
torch.nn.Linear(input_size, output_size),
)
def forward(self, x):
logits = self.linear_relu_stack(x)
print("=> In Model: input size", x.size(), "output size", logits.size())
return logits
model = NeuralNetwork(input_size, output_size)
model.to(local_rank)
model = torch.torch.nn.parallel.DistributedDataParallel(model, device_ids=[local_rank])
print(model)
X = torch.rand(number_of_inputs, input_size, device=local_rank)
logits = model(X)
pred_probab = torch.nn.Softmax(dim=1)(logits).to(local_rank)
y_pred = pred_probab.argmax(1)
print(f"=> Predicted class: {y_pred}")Uruchomienie:
sbatch job-multigpu-ddp-two-nodes.sh
Standardowe wyjście w pliku slurm-.out
=> Set CUDA device = cuda:0
=> Set CUDA device = cuda:0
DistributedDataParallel(
(module): NeuralNetwork(
(linear_relu_stack): Sequential(
(0): Linear(in_features=576, out_features=576, bias=True)
(1): ReLU()
(2): Linear(in_features=576, out_features=576, bias=True)
(3): ReLU()
(4): Linear(in_features=576, out_features=10, bias=True)
)
)
)
DistributedDataParallel(
(module): NeuralNetwork(
(linear_relu_stack): Sequential(
(0): Linear(in_features=576, out_features=576, bias=True)
(1): ReLU()
(2): Linear(in_features=576, out_features=576, bias=True)
(3): ReLU()
(4): Linear(in_features=576, out_features=10, bias=True)
)
)
)
=> In Model: input size torch.Size([1, 576]) output size torch.Size([1, 10])
=> Predicted class: tensor([5], device='cuda:0')
=> In Model: input size torch.Size([1, 576]) output size torch.Size([1, 10])
=> Predicted class: tensor([6], device='cuda:0')
Dodatkowa informacja: komenda nvidia-smi
Interfejs zarządzania systemem NVIDIA (nvidia-smi) to narzędzie wiersza poleceń, przeznaczone do pomocy w zarządzaniu i monitorowaniu urządzeń NVIDIA GPU.
nvidia-smi
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 470.57.02 Driver Version: 470.57.02 CUDA Version: 11.4 |
|-------------------------------+----------------------+----------------------+
| 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... On | 00000000:01:08.0 Off | 0 |
| N/A 29C P0 43W / 400W | 0MiB / 40536MiB | 0% Default |
| | | Disabled |
+-------------------------------+----------------------+----------------------+
| 1 NVIDIA A100-SXM... On | 00000000:02:08.0 Off | 0 |
| N/A 30C P0 43W / 400W | 0MiB / 40536MiB | 0% Default |
| | | Disabled |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| No running processes found |
+-----------------------------------------------------------------------------+
Pomocne komendy polecenia nvidia-smi:
- Szczegółowe ustawienia kart w aktualnych zasobach:
nvidia-smi -i 0 -q
- Tryb trwałości i dołączania:
nvidia-smi -pm 1
- Wyświetlenie listę wszystkich dostępnych urządzeń NVIDIA:
nvidia-smi -L
- Monitorowanie ogólnego wykorzystania GPU z 1-sekundowymi interwałami aktualizacji:
nvidia-smi dmon
- Monitorowanie wykorzystania procesora GPU przez proces z 1-sekundowymi interwałami aktualizacji:
nvidia-smi pmon
- Sprawdzanie prędkości zegara:
nvidia-smi -q -d CLOCK
- Sprawdzenie aktualnego stanu każdego GPU i przyczyn spowolnienia zegara:
nvidia-smi -q -d PERFORMANCE
- Wyświetlenie topologii podłączeń GPU:
nvidia-smi topo --matrix
- Badanie przepustowości łączy NVLink:
nvidia-smi nvlink --status
Dodatkowa informacja: kolejki i procesy
Każde polecenie SLURM trafia do kolejki z unikalnym identyfikatorem.
- Sprawdzenie aktualnej kolejki użytkownika:
squeue -u $USER
- W celu przerwania wykonywania zadania należy wykonać:
scancel
gdzie jest zadaniem z kolejki squeue - Jeżeli istnieje potrzeba zabicia procesu na maszynie nie dealokując przy tym zasobów, można sprawdzić jego PID za pomocą polecenia:
netstat -nltp
- na danym węźle, a następnie zabić proces wykonując komendę:
kill -9