SAMSUNG_042024 Advertisement SAMSUNG_042024 Advertisement SAMSUNG_042024 Advertisement

ML v Pythone 16 - trénovanie neurónových sietí na viacerých grafických kartách

0

Pri konfigurácii s jednou grafickou kartou máte model v jednom GPU, na ktorom pobeží tréningový proces. Model dostane vstupnú dávku údajov prekonvertovaných na tenzory z takzvaného dávkovača údajov. Najskôr vykoná dopredný prechod na výpočet stratovej funkcie. Pri spätnom prechode sa počítajú gradienty a optimalizátor aktualizuje parametre. Tento proces v závislosti od objemu údajov, na ktorých sa neurónová sieť trénuje trvá pomerne dlho. Ak máte k dispozícii viac GPU, môžete časovo najnáročnejšiu úlohu, čiže tréning neurónovej siete medzi ne rozdeliť.

Video s príkladmi

Proces učenia neurónových sietí v závislosti od objemu údajov, na ktorých sa neurónová sieť trénuje trvá pomerne dlho. Ak máte k dispozícii viac grafických kariet,, môžete časovo najnáročnejšiu úlohu, čiže tréning neurónovej siete medzi ne rozdeliť. Vo videu je kompletný príklad. Knižnice pre tento príklad sú k dispozícii len pre Linux.

Na ilustráciu trénovanie neurónovej siete určenej na generovanie textov na knihe, ktorá má 90 374 slov na CPU Intel Core i7-12700 trvalo 16 hodín a 25 minút. Relatívne lacná grafická karta RTX 4060 to zvládla 10 x rýchlejšie za 97 minút. Výkonnejšej karte RTX 4090 trvalo trénovanie len 27 minút, čiže bola 36 x rýchlejšia než CPU. Dve grafické karty by trénovanie neurónovej siete mohli teoreticky zvládnuť za polovičný čas. Vyskúšame, uvidíme.

Najskôr predstavíme technológie. DDP (Distribute Data Paralel) dokáže tréning modelu neurónovej siete rozdeliť medzi viac GPU, pričom každé GPU má vlastnú lokálnu kópiu modelu. Všetky repliky modelu a optimizérov sú identické. Majú rovnaké počiatočné parametre modelu a aj optimizéry používajú rovnaký náhodný základ. DDP interne udržiava túto synchronizáciu počas celého tréningového procesu. Každý proces však v dávkach prijíma rôzne vzorky údajov, ktoré rozdeľuje modul DistributedSampler. V praxi to znamená, že v systéme s dvomi GPU efektívne spracovávame dvojnásobok a v systéme so štyrmi GPU štvornásobok údajov v porovnaní s tréningom jedného GPU.

Na ilustráciu, japonský superpočítač MN-1 ktorý má 1024 GPU kariet NVIDIA Tesla P100 a prepojením Mellanox InfiniBand dokázal model ResNet-50 natrénovať na súbore údajov ImageNet za 15 minút.

Pri každom procese model dostane lokálne iný vstup, spustí dopredný a spätný prechod, a pretože vstupy boli odlišné, aj gradienty budú odlišné. Spustenie kroku optimalizácie v tomto bode by viedlo k rôznym parametrom naprieč našimi zariadeniami a namiesto jediného distribuovaného modelu by sme skončili so štyrmi odlišnými modelmi. Preto DDP v tejto fáze iniciuje krok synchronizácie. Prechody zo všetkých replík sú agregované pomocou modulu  bucketed ring. Synchronizácia nečaká na výpočet všetkých gradientov, ale robí komunikáciu pozdĺž pomysleného kruhu viacerých GPU, zatiaľ čo spätný prechod stále prebieha. To zaisťuje, že všetky GPU stále pracujú a nemusia čakať na synchronizáciu. Keď má každá replika modelu rovnaké gradienty, spustí sa krok optimizéra, a všetky parametre replík sa aktualizujú na rovnaké hodnoty. Pri spustení trénovania boli repliky vo všetkých procesoch na jednotlivých GPU identické a zostávajú synchronizované a identické aj na konci každého kroku a takto sú pripravené na ďalšiu iteráciu.

Začneme kódom na trénovanie neurónovej siete na jednom GPU a následne migrujeme ho na viac GPU cez DDP. Kód využíva knižnicu PyTorch.

Tento príklad pre jednu GPU by sme mohli robiť na lokálnom počítači s GPU NVIDIA vo Windows aj v Linuxe, prípadne v online prostredí Google Coleboratory s runtime prostredím nastaveným na GPU. Pre trénovanie neurónových sietí na viacerých GPU môžete využiť len lokálny počítač s operačným systémom Linux, pretože knižnica NVIDIA Collective Communications Library (NCCL) pre Windows nie je k dispozícii, prípadne virtuálny počítač s Linuxom a viacerými GPU.

Pre náš príklad sme použili PC s Ubuntu Linuxom s procesorom Intel Core i3 13100 (na procesore nezáleží) a grafickými kartami

  • ASUS TUF Gaming GeForce RTX 4060 Ti 8GB (architektúra Ada Lovelace)
  • GIGABYTE AORUS GeForce RTX 4060 ELITE 8G (architektúra Ada Lovelace)
  • ASUS Dual GeForce RTX 3050 OC Edition 8GB (architektúra Ampere)

Je to teda mix GPU rôznych modelov od rôznych výrobcov a rôznych architektúr – Ada Lovelace a Ampere.

Tri bežné grafické karty sa do PC skrinky normálnej veľkosti nezmestia, preto sme použili zostavu "na stole"

Postup inštalácie všetkých nástrojov a knižníc pre Linux je v druhej polovici tohoto videa

Nakoľko karty RTX 4060 Ti aj RTX 4040 zaberajú väčšiu šírku než dve pozície, aby sme mohli všetky tri karty zasunúť do základnej dosky, nemohli sme to urobiť v klasickej skrinke, ale na stole. Aby sme mohli tieto hrafické karty mať v skrinke, potrebovali by sme skrinku, ktorá nemá v spodnej časti šachtu na zdroj, ale má zdroj nainštalovaný inde, napríklad hore vpredu.  

Príklad pre jednu GPU by sme mohli robiť aj v prostredí Jupyter Notebook, avšak pre viac GPU bude potrebné spustiť Python program v súbore s príponou .py. Aby sme nemuseli robiť veľa zmien, tak aj príklad pre jednu GPU budeme robiť nie v Jupyter Notebooku ale kód sa bude spúšťať v súbore.

Pre zaujímavosť je vo výpisoch po jednotlivých epochách budeme vypisovať aj teplotu GPU, preto je potrebné nainštalovať knižnicu NVIDIA Management Library príkazom

pip install pynvml

Programový kód môžete napísať v editore, alebo vývojovom prostredí na ktoré stezvyknutí. My sme použili Visual Studio Code, ktoré je k dispozícii aj pre Linux.

Pre jednoduchosť model v tomto príklade využíva len lineárnu vrstvu a súbor údajov na trénovanie tvorí pomerne malá množina náhodných čísel. Aby bol kód čo najkratší a najzrozumiteľnejší budeme predpokladať, že máte aspoň jednu grafickú kartu NVIDIA, takže nebudeme kontrolovať prítomnosť grafickej karty. Tréningové dáta budú náhodné čísla, 65 536 záznamov, každý má 20 vstupov a jeden výstup.

Nakoľko trénovacie údaje sú náhodné čísla, a model je tvorený len jednou lineárnou vrstvou je jasné, že model sa tréningom nebude „zlepšovať“. Trénujeme na náhodných číslach takže na modeli a optimizéri nezáleží. Model má 20 vstupov a jeden výstup

model = torch.nn.Linear(20, 1) 

Taktiež GPU budú pracovať na minimálnu záťaž, pretože nebudú musieť násobiť veľkorozmerné matice, v čom spočíva ich sila. To je aj dôvod, prečo rozdiel v rýchlosti trénovania medzi grafickými kartami NVIDIA RTX 4060 Ti, RTX 4060 a RTX 3050 bude minimálny. Akonáhle by sme trénovali reálny model, vtedy by sa naplno prejavili výhody GPU. V tomto príklade sa nám jedná hlavne o princíp rozdeľovanie údaje pre niekoľko GPU.

Kód v súbore SingleGPU.py

import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import time
 
data_size = 65536
epochs = 500
batch_size = 32
 
def priprava_udajov(d_size: int):
    data = [(torch.rand(20), torch.rand(1)) for _ in range(d_size)]
    return data
 
def priprava_davky(dataset: Dataset, batch_size: int):
    return DataLoader(
        dataset,
        batch_size=batch_size,
        pin_memory=True,
        shuffle=True
    )
 
def trenovanie(model: torch.nn.Module, train_data: DataLoader, optimizer: torch.optim.Optimizer,gpu_id: int,):
        model = model.to(gpu_id)      # model do GPU
        for epoch in range(epochs):
            batch_sz = len(next(iter(train_data))[0])
            for source, targets in train_data:
                source = source.to(gpu_id)     # data do GPU
                targets = targets.to(gpu_id)
                # spustenie dávky
                optimizer.zero_grad()
                output = model(source)
                loss = F.cross_entropy(output, targets)              
                loss.backward()
                optimizer.step()
            teplota = torch.cuda.temperature(device=gpu_id)
            print(f"[GPU{gpu_id}] Epoch {epoch} | davka: {batch_sz} | Krokov: {len(train_data)} | Teplota GPU: {teplota} °C ")
 
 
def main(device):
    data = priprava_udajov(data_size)
    train_data = priprava_davky(data, batch_size)
    model = torch.nn.Linear(20, 1) 
    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)  
    # trenovanie
    teplota_start = torch.cuda.temperature(device)
    cas_start = time.time()
    trenovanie(model, train_data, optimizer, device)
    teplota_end = torch.cuda.temperature(device)
    cas_trvania = time.time() - cas_start
 
    print("")
    print("--------------------------------------------------")
    print("Trénovanie modelu trvalo", "%.2f" % cas_trvania ,"sekúnd")
    print(f"Teplota GPU{device}  Začiatok {teplota_start} °C  Koniec {teplota_end} °C ")
    print("--------------------------------------------------")
    print("")
 
 
if __name__ == "__main__":
    device = 0  # cuda:0 alebo 1 = cuda:1
    main(device)

Kód v súbore SingleGPU.py spustíme z terminálovej aplikácie príkazom

python SingleGPU.py

Trénovanie na GPU0 NVIDIA RTX 3060 Ti. GPU počas jednej epochy trénovania vykoná 2048 krokov

[GPU0] Epoch 0 | davka: 32 | Krokov: 2048 | Teplota GPU: 41 °C
[GPU0] Epoch 1 | davka: 32 | Krokov: 2048 | Teplota GPU: 41 °C
[GPU0] Epoch 2 | davka: 32 | Krokov: 2048 | Teplota GPU: 41 °C
[GPU0] Epoch 3 | davka: 32 | Krokov: 2048 | Teplota GPU: 41 °C
...
GPU0] Epoch 498 | davka: 32 | Krokov: 2048 | Teplota GPU: 49 °C
[GPU0] Epoch 499 | davka: 32 | Krokov: 2048 | Teplota GPU: 49 °C
 
--------------------------------------------------
Trénovanie modelu trvalo 292.93 sekúnd
Teplota GPU0  Začiatok 39 °C  Koniec 49 °C
--------------------------------------------------

Trénovanie na GPU0 NVIDIA RTX 3050. Pri teplote 55 stupňov sa spustil ventilátor, takže teplota na konci trénovania je skoro rovnaká ako na začiatku

[GPU1] Epoch 0 | davka: 32 | Krokov: 2048 | Teplota GPU: 46 °C
[GPU1] Epoch 1 | davka: 32 | Krokov: 2048 | Teplota GPU: 47 °C
[GPU1] Epoch 2 | davka: 32 | Krokov: 2048 | Teplota GPU: 47 °C
...
[GPU1] Epoch 498 | davka: 32 | Krokov: 2048 | Teplota GPU: 46 °C
[GPU1] Epoch 499 | davka: 32 | Krokov: 2048 | Teplota GPU: 46 °C
--------------------------------------------------
Trénovanie modelu trvalo 313.03 sekúnd
Teplota GPU1  Začiatok 45 °C  Koniec 46 °C
--------------------------------------------------

Ak chceme spustiť trénovanie na viacerých GPU,  je potrebná inicializácia skupiny distribuovaných procesov, ktoré bežia na GPU. Na každom GPU spravidla beží jeden proces. Inicializácia je potrebná aby všetky procesy mohli navzájom komunikovať.  V tejto funkcii pamätáme aj na to, že modifikovaný príklad môžeme spúšťať na viacerých prepojených počítači, pričom v každom z nich môže byť niekoľko GPU.

# inicializácia distribuovaného spracovania
# DDP (Distributed Data Paralel)
def ddp_setup(rank, n_GPUs):
    # rank - unikátny identifikátor procesu
    # n_GPUs - počet GPU
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "12355"
    init_process_group(backend="nccl", rank=rank, world_size=n_GPUs)
    torch.cuda.set_device(rank)

Parameter rank je identifikátor procesu, ktorý automaticky prideľuje funkcia mp.spawn() . Počet GPU zistíme pomocou funkcie

n_GPUs = torch.cuda.device_count()

Počet odovzdáme metóde, ktorá má tento parameter nazvaný world_size.

init_process_group(backend="nccl", rank=rank, world_size=n_GPUs)

Budeme to spúšťať na jednom PC, ktorý bude koordinovať komunikáciu naprieč všetkými našimi procesmi, preto parameter MASTER_ADDR nastavíme na „localhost“ a vyberieme voľný port. Keby sme používali viac počítačov, nastavili by sme ako master ten počítač, na ktorom beží proces 0.S nastavenými parametrami voláme init_process_group(). Nccl (NVIDIA Collective Communications Library)  https://developer.nvidia.com/nccl  je komunikačná knižnica spoločnosti Nvidia a je to back-end, ktorý sa používa na distribuovanú komunikáciu medzi viacerými GPU s podporou technológie NVIDIA CUDA.

Skôr než začneme náš model trénovať, musíme ho zabaliť pomocou DDP.

  ...
  self.model = model.to(gpu_id)               
  self.model = DDP(model, device_ids=[gpu_id])
  ...

Modely sú zabalené do DDP.  Ak chceme získať prístup k základným parametrom modelov, musíme zavolať modul modelu. DDP funguje tak, že spustí proces na každom GPU. Každý proces bude inicializovať objekt triedy na trénovanie modelu

Potrebujeme aby vstupná dávka údajov bola rozdelená na všetky GPU bez toho aby sa vzorky prekrývali. Preto je potrebné nastaviť hodnotu parametra shuffle na False.

def priprava_davky(dataset: Dataset, batch_size: int):
    return DataLoader(
        dataset,
        batch_size=batch_size,
        pin_memory=True,
        shuffle=False,
        sampler=DistributedSampler(dataset),
    )

V hlavnej funkcii je potrebné inicializovať skupinu distribuovaných procesov. Na to sme vytvorili funkciu dpp_setup(),  takže ju len zavoláme. Akonáhle je tréningová úloha spustená, môžeme zrušiť procesnú skupinu.

def main(rank: int, n_GPUs: int, n_Epochs: int):
    ddp_setup(rank, n_GPUs)
    data = priprava_udajov(data_size)
    ...
    destroy_process_group()

Zistíme počet GPU na ktorých bude úloha trénovania neurónovej siete spustená. Funkcia  mp.spawn, prevezme funkciu main() a vytvorí ju vo všetkých procesoch v distribuovanej skupine. Funkcia mp.spawn automaticky priradí každému procesu rank. Veľkosť dávky máme nastavenú na 32.

Kód v súbore MultiGPU.py Zvýraznené sú zmeny oproti programu pre jednu GPU

import torch
import torch.nn.functional as F
from torch.utils.data import Dataset, DataLoader
import time
 
#importy
import torch.multiprocessing as mp
from torch.utils.data.distributed import DistributedSampler
from torch.nn.parallel import DistributedDataParallel as DDP
from torch.distributed import init_process_group, destroy_process_group
import os
 
 
data_size = 65535
batch_size = 32
 
def priprava_udajov(d_size: int):
    data = [(torch.rand(20), torch.rand(1)) for _ in range(d_size)]
    return data
 
def priprava_davky(dataset: Dataset, batch_size: int):
    return DataLoader(
        dataset,
        batch_size=batch_size,
        pin_memory=True,
        shuffle=False,
        sampler=DistributedSampler(dataset),
    )
 
# inicializácia distribuovaného spracovania
# DDP (Distributed Data Paralel)
def ddp_setup(rank, n_GPUs):
    # rank - unikátny identifikátor procesu
    # n_GPUs - počet GPU
    os.environ["MASTER_ADDR"] = "localhost"
    os.environ["MASTER_PORT"] = "12355"
    init_process_group(backend="nccl", rank=rank, world_size=n_GPUs)
    torch.cuda.set_device(rank)
 
 
def trenovanie(model: torch.nn.Module, train_data: DataLoader, optimizer: torch.optim.Optimizer,gpu_id: int, n_Epochs: int):
        model = model.to(gpu_id) 
        model = DDP(model, device_ids=[gpu_id])             
        for epoch in range(n_Epochs):
            batch_sz = len(next(iter(train_data))[0])
            train_data.sampler.set_epoch(epoch)
            for source, targets in train_data:
                source = source.to(gpu_id)     # data do GPU
                targets = targets.to(gpu_id)
                # spustenie dávky
                optimizer.zero_grad()
                output = model(source)
                loss = F.cross_entropy(output, targets)              
                loss.backward()
                optimizer.step()
            print(f"[GPU{gpu_id}] Epoch {epoch} | davka: {batch_sz} | Krokov: {len(train_data)} ")
 
 
def main(rank: int, n_GPUs: int, n_Epochs: int):
    ddp_setup(rank, n_GPUs)
    data = priprava_udajov(data_size)
    train_data = priprava_davky(data, batch_size)
    model = torch.nn.Linear(20, 1) 
    optimizer = torch.optim.SGD(model.parameters(), lr=1e-3)  
 
    # trenovanie
    cas_start = time.time()
    trenovanie(model, train_data, optimizer, rank, n_Epochs)
    cas_trvania = time.time() - cas_start
   
    print("")
    print("--------------------------------------------------")
    print("Trénovanie modelu trvalo", "%.2f" % cas_trvania ,"sekúnd")
    print("--------------------------------------------------")
    print("")
    destroy_process_group()
 
 
if __name__ == "__main__": 
    n_GPUs = torch.cuda.device_count()
    mp.spawn(main, args=(n_GPUs, 500), nprocs=n_GPUs)

Trénovanie pre dve GPU. Z výpisu je zrejmé ako sú dáta rozdeľované medzi GPU0 a GPU1. Každé z GPU počas jednej epochy trénovania vykoná 1024 krokov

[GPU0] Epoch 0 | davka: 32 | Krokov: 1024
[GPU1] Epoch 0 | davka: 32 | Krokov: 1024
[GPU0] Epoch 1 | davka: 32 | Krokov: 1024
[GPU1] Epoch 1 | davka: 32 | Krokov: 1024
[GPU1] Epoch 2 | davka: 32 | Krokov: 1024
[GPU0] Epoch 2 | davka: 32 | Krokov: 1024
[GPU1] Epoch 3 | davka: 32 | Krokov: 1024
[GPU0] Epoch 3 | davka: 32 | Krokov: 1024
[GPU0] Epoch 4 | davka: 32 | Krokov: 1024
[GPU1] Epoch 4 | davka: 32 | Krokov: 1024
[GPU1] Epoch 5 | davka: 32 | Krokov: 1024
[GPU0] Epoch 5 | davka: 32 | Krokov: 1024
[GPU0] Epoch 6 | davka: 32 | Krokov: 1024
...
[GPU1] Epoch 498 | davka: 32 | Krokov: 1024
[GPU0] Epoch 498 | davka: 32 | Krokov: 1024
[GPU1] Epoch 499 | davka: 32 | Krokov: 1024
 
--------------------------------------------------
Trénovanie modelu trvalo 223.08 sekúnd
--------------------------------------------------

Trénovanie pre tri GPU. Z výpisu je zrejmé ako sú dáta rozdeľované medzi GPU0, GPU1 a GPU2. . Každé z GPU počas jednej epochy trénovania vykoná 683 krokov

[GPU0] Epoch 0 | davka: 32 | Krokov: 683
[GPU1] Epoch 0 | davka: 32 | Krokov: 683
[GPU2] Epoch 0 | davka: 32 | Krokov: 683
[GPU2] Epoch 1 | davka: 32 | Krokov: 683
[GPU0] Epoch 1 | davka: 32 | Krokov: 683
[GPU1] Epoch 1 | davka: 32 | Krokov: 683
[GPU1] Epoch 2 | davka: 32 | Krokov: 683
[GPU0] Epoch 2 | davka: 32 | Krokov: 683
[GPU2] Epoch 2 | davka: 32 | Krokov: 683
[GPU1] Epoch 3 | davka: 32 | Krokov: 683
[GPU0] Epoch 3 | davka: 32 | Krokov: 683
[GPU2] Epoch 3 | davka: 32 | Krokov: 683
[GPU2] Epoch 4 | davka: 32 | Krokov: 683
...
[GPU0] Epoch 496 | davka: 32 | Krokov: 683
[GPU1] Epoch 497 | davka: 32 | Krokov: 683
[GPU0] Epoch 497 | davka: 32 | Krokov: 683
[GPU2] Epoch 497 | davka: 32 | Krokov: 683
[GPU1] Epoch 498 | davka: 32 | Krokov: 683
[GPU0] Epoch 498 | davka: 32 | Krokov: 683
[GPU2] Epoch 498 | davka: 32 | Krokov: 683
[GPU1] Epoch 499 | davka: 32 | Krokov: 683
 
--------------------------------------------------
Trénovanie modelu trvalo 220.11 sekúnd
--------------------------------------------------

Ak by sme trénovali na jednom GPU potrebovali by sme v každej epoche 2048 krokov. Pri trénovaní na dvoch GPU stačí 1024 krokov a pri trénovaní na troch GPU 683 krokov. Ak by sme mali štyri GPU potrebovali by sme len 512 krokov pretože „tréningová záťaž“ je rozdelená medzi dve, tri, prípadne štyri GPU. Ak máte viac GPU, môžu zdieľať záťaž pri trénovaní, takže môžete svoj model trénovať rýchlejšie.

Rekapitulácia doterajších dielov

ML v Pythone 15 – neurónové siete na klasifikačné úlohy

ML v Pythone 14 – neurónová sieť na generovanie textu II

ML v Pythone 13 – vytvorenie a natrénovanie neurónovej siete ktorá rozozná či je huba jedlá

ML v Pythone 12 – neurónová sieť predpovedá výskyt cukrovky u indiánskeho kmeňa

ML v Pythone 11 – vytvorenie a učenie neurónovej siete na generovanie poviedok

ML v Pythone 10 – trénovanie neurónovej siete, príprava

ML v Pythone 9 – trénovanie neurónu

ML v Pythone 8 – príklad rozpoznávanie obrazu

ML v Pythone 7 – Využitie grafickej karty NVIDIA na výpočtovo náročné úlohy

ML v Pythone 6 – animované grafy a ich export ako video

ML v Pythone 5 – vizualizácia údajov pomocou grafov

Strojové učenie v Pythone 4 – práca s údajmi

ML v Pythone 3 – export a import údajov vo formáte CSV a Excel

Strojové učenie v Pythone 2 – knižnica Pandas na prácu s údajmi

Strojové učenie v Pythone 1 – prostredie Google Colab

Zobrazit Galériu

Luboslav Lacko

Všetky autorove články
Python strojove ucenie machine learning Multi GPU NVIDIA

Pridať komentár

Mohlo by vás zaujímať

Mohlo by vás zaujímať