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