PyTorch


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
#SBATCH --gres=gpu:a100:1
#SBATCH --time=00:10:00
#SBATCH --nodes=1

module load tryton/singularity/singularity-3.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-<nazwa-zadania>.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
#SBATCH --gres=gpu:a100:2
#SBATCH --time=00:10:00
#SBATCH --nodes=1

module load tryton/singularity/singularity-3.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-<nazwa-zadania>.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 tryton/singularity/singularity-3.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-<nazwa-zadania>.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
#SBATCH --gres=gpu:a100:1
#SBATCH --time=00:10:00
#SBATCH --nodes=2


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_tryton_kdm.sh
   module load tryton/singularity/singularity-3.3.0
   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-<nazwa-zadania>.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 <JOB-ID> 
    gdzie <JOB-ID> 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 <PID>