Accelerazione degli addebiti di formazione PyTorch con FP8

Impulso alla formazione di PyTorch con addebbiti accelerati FP8

Come sfruttare al massimo la tua GPU moderna

Foto di Deva Darshan su Unsplash

I progressi rivoluzionari nel campo dell’IA degli ultimi anni sono stati esemplificati dal crescente interesse e dalla proliferazione delle applicazioni basate su LLM, come ChatGPT. Questi progressi sono stati alimentati da sviluppi entusiasmanti nelle macchine utilizzate per allenare i modelli di intelligenza artificiale. Nuove e innovative architetture, sofisticati tensor processing core e acceleratori HW dedicati hanno permesso la convergenza di modelli di IA di dimensioni sempre maggiori, a velocità sempre più rapide. In questo post, ci concentreremo su un particolare sviluppo nell’HW specializzato per l’IA: l’inclusione di core di elaborazione tensoriale a 8 bit in virgola mobile (FP8) dedicati. Questi core appaiono nelle architetture di HW per l’IA più moderne (ad esempio, Nvidia Hopper, Nvidia Ada Lovelace e Habana Gaudi2) e consentono un significativo aumento delle operazioni in virgola mobile al secondo (FLOPS), oltre a opportunità di ottimizzazione della memoria e risparmio energetico sia per l’allenamento che per le attività di inferenza dell’IA.

Per sfruttare al meglio le capacità a livello di HW del FP8, è necessario un supporto adeguato nello stack del software e nel framework di sviluppo che utilizziamo per costruire le nostre applicazioni di allenamento e inferenza dell’IA. In questo post, descriveremo come modificare uno script di allenamento PyTorch per utilizzare il supporto integrato per il tipo di dato FP8 di una Nvidia H100 GPU. Inizieremo fornendo una motivazione per l’uso del tipo di dato FP8. Successivamente, esamineremo il supporto specifico per il tipo di dato FP8 esposto dall’API di PyTorch fornita dalla libreria Transformer Engine e mostreremo come integrarle in uno script di allenamento semplice. Sebbene non affronteremo la teoria dietro l’uso di FP8 per l’allenamento dell’IA, noteremo le sfide potenziali coinvolte nel suo utilizzo. Infine, dimostreremo le significative opportunità di ottimizzazione offerte dal tipo di dato FP8.

Avvertenze

Si prega di non interpretare la menzione di alcun componente di software, metodologia o servizio come un sostegno al loro utilizzo. La progettazione migliore per lo sviluppo di machine learning varia notevolmente in base ai dettagli specifici del proprio carico di lavoro di intelligenza artificiale. Si prega inoltre di tenere presente che le API e i comportamenti di alcuni dei pacchetti software e dei componenti che menzioneremo possono cambiare al momento della lettura di questo post. Si consiglia vivamente di valutare le decisioni di progettazione potenziali in base all’HW e al software più aggiornati disponibili.

Motivazione

Man mano che i modelli di IA diventano sempre più sofisticati, aumentano anche le risorse necessarie per allenarli. La Nvidia H100 GPU, descritta come il supporto a “prestazioni e scalabilità senza precedenti”, è (al momento della stesura di questo articolo) il nuovo e più potente acceleratore di intelligenza artificiale di Nvidia, progettato appositamente con l’obiettivo di consentire lo sviluppo dell’IA delle prossime generazioni. Con la corsa all’IA attuale in pieno svolgimento, la domanda di queste GPU è stata enorme (ad esempio, vedi qui). Di conseguenza, e non sorprendentemente, il costo di queste GPU è risultato estremamente elevato, forse persino proibitivo per molti dei nostri lettori. Fortunatamente, i fornitori di servizi cloud come AWS, GCP e Microsoft Azure offrono accesso “pay as you go” (addebito per ora/seconde) a macchine alimentate da H100, aprendo così l’opportunità del loro utilizzo a una comunità molto più ampia di sviluppatori di IA.

In AWS, le GPU H100 vengono offerte come componente della famiglia di istanze AWS EC2 p5 recentemente annunciata. Si sostiene che queste istanze possano “accelerare il tempo di soluzione fino a 4 volte rispetto alle istanze EC2 basate su GPU di generazioni precedenti e ridurre i costi per addestrare modelli di Machine Learning fino al 40%”.

In un post recente abbiamo discusso alcune delle considerazioni che dovrebbero essere prese in considerazione nella scelta di un’istanza per l’addestramento di Machine Learning. Abbiamo evidenziato il fatto che il tipo di istanza più ottimale dipenderà molto dal progetto in questione. In particolare, quando si tratta di istanze per l’addestramento di Machine Learning, più grande non è sempre meglio. Questo è particolarmente vero per la famiglia di istanze p5. È vero che il p5 probabilmente supererà qualsiasi altro tipo di istanza, in quanto l’H100 è un vero mostro delle prestazioni. Ma una volta che si tiene conto del costo del p5 ($98,32 all’ora per l’istanza p5.48xlarge con 8 GPU – al momento della stesura di questo articolo), si potrebbero trovare altri tipi di istanza più adatti.

Nella prossima sezione addestreremo un modello di computer vision relativamente grande su un’istanza p5.48xlarge e ne confrontiamo le prestazioni con una p4d.24xlarge che contiene 8 GPU Nvidia A100.

Modello giocattolo

Nel blocco di codice qui sotto definiamo un modello di classificazione supportato da un Vision Transformer (ViT) (utilizzando il popolare pacchetto Python timm versione 0.9.10) insieme a un dataset generato casualmente. I backbone dei ViT sono disponibili in diverse forme e dimensioni. Qui abbiamo scelto quella che viene spesso definita la configurazione ViT-Huge, con 632 milioni di parametri, per sfruttare al meglio la capacità dell’H100 per i modelli di grandi dimensioni.

import torch, timeimport torch.optimimport torch.utils.dataimport torch.distributed as distfrom torch.nn.parallel.distributed import DistributedDataParallel as DDPimport torch.multiprocessing as mp# modificare la dimensione del batch in base alla memoria della GPUbatch_size = 64from timm.models.vision_transformer import VisionTransformerfrom torch.utils.data import Dataset# usare dei dati casualiclass FakeDataset(Dataset):    def __len__(self):        return 1000000    def __getitem__(self, index):        rand_image = torch.randn([3, 224, 224], dtype=torch.float32)        label = torch.tensor(data=[index % 1000], dtype=torch.int64)        return rand_image, labeldef mp_fn(local_rank, *args):    # configurare il processo    dist.init_process_group("nccl",                            rank=local_rank,                            world_size=torch.cuda.device_count())    torch.cuda.set_device(local_rank)    device = torch.cuda.current_device()        # creare il dataset e il dataloader    train_set = FakeDataset()    train_loader = torch.utils.data.DataLoader(        train_set, batch_size=batch_size,        num_workers=12, pin_memory=True)    # definire il modello ViT-Huge    model = VisionTransformer(            embed_dim=1280,            depth=32,            num_heads=16,        ).cuda(device)    model = DDP(model, device_ids=[local_rank])    # definire la loss e l'ottimizzatore    criterion = torch.nn.CrossEntropyLoss()    optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)    model.train()    t0 = time.perf_counter()    summ = 0    count = 0    for step, data in enumerate(train_loader):        # copiare i dati sulla GPU        inputs = data[0].to(device=device, non_blocking=True)        label = data[1].squeeze(-1).to(device=device, non_blocking=True)          # utilizzare la precisione mista per sfruttare il supporto bfloat16        with torch.autocast(device_type='cuda', dtype=torch.bfloat16):            outputs = model(inputs)            loss = criterion(outputs, label)        optimizer.zero_grad(set_to_none=True)        loss.backward()        optimizer.step()                # catturare il tempo dello step        batch_time = time.perf_counter() - t0        if step > 10:  # saltare i primi step            summ += batch_time            count += 1        t0 = time.perf_counter()        if step > 50:            break    print(f'tempo medio dello step: {summ/count}')if __name__ == '__main__':    mp.spawn(mp_fn,             args=(),             nprocs=torch.cuda.device_count(),             join=True)

Abbiamo addestrato questo modello sia sui tipi di istanza p5.48xlarge che p4d.24xlarge utilizzando il container AWS deep learning dedicato PyTorch 2.1 AWS deep learning (763104351884.dkr.ecr.us-east-1.amazonaws.com/pytorch-training:2.1.0-gpu-py310-cu121-ubuntu20.04-ec2).

Senza sorprese, le prestazioni step-time del p5 superano quelle del p4d — 0.199 secondi per step rispetto a 0.41 — più del doppio più veloci!! Ciò significa dimezzare il tempo per addestrare i modelli di machine learning di grandi dimensioni. Tuttavia, quando si tiene conto della differenza di costo ($32.77 all’ora per il p4d rispetto a $98.32 all’ora per il p5 — al momento della stesura di questo articolo) si scopre una storia completamente diversa. Il rapporto prezzo-prestazioni del p5 è ~30% peggiore rispetto al p4d!! Questo è molto lontano dal miglioramento del 40% che è apparso nell’annuncio del p5.

A questo punto potresti trarre una delle due possibili conclusioni. La prima possibilità è che, nonostante tutto l’hype, il p5 semplicemente non sia la macchina giusta per te. La seconda è che il p5 potrebbe comunque essere valido, ma sarebbero necessarie delle modifiche al tuo modello per sfruttarne appieno il potenziale. Nelle prossime sezioni adotteremo il secondo approccio e mostreremo come l’utilizzo del datatype FP8 — unico per il tipo di istanza p5 — possa completamente alterare i risultati comparativi prezzo-prestazioni.

Integrazione di FP8 con Transformer Engine

La prima cosa che dovremmo sottolineare è che, al momento della stesura di questo articolo, PyTorch (versione 2.1) non include un datatype nativo a 8-bit float. Per programmare il nostro script in modo che utilizzi FP8 utilizzeremo Transformer Engine (TE), una libreria dedicata per l’accelerazione dei modelli Transformer su GPU NVIDIA. TE (versione 0.12) è preinstallato nel container AWS PyTorch 2.1 DL.

Sebbene la teoria dietro l’utilizzo di FP8 per l’addestramento sia al di là della portata di questo post (ad esempio, vedi qui), è importante essere consapevoli che la meccanica dell’utilizzo di FP8 è molto più complessa rispetto alle alternative a 16-bit (float16 e bfloat16). Fortunatamente, l’implementazione di TE nasconde tutti i dettagli disordinati all’utente. Consulta la documentazione ufficiale e questo esempio semplice per istruzioni su come utilizzare le API di TE. Per saperne di più su ciò che accade dietro le quinte, assicurati di guardare i seguenti due video tutorial.

Addestramento FP8 con Transformer Engine | NVIDIA On-Demand

La sessione comprenderà un’introduzione a FP8 e presisione mista, una panoramica delle funzionalità di Transformer Engine e un…

www.nvidia.com

FP8 per Deep Learning | NVIDIA On-Demand

FP8 è una naturale progressione per accelerare l’apprendimento profondo (DL) oltre i formati a 16-bit comuni nell’ambiente moderno…

www.nvidia.com

Per modificare il nostro modello per l’utilizzo di TE, avvolgiamo il Transformer Layer specializzato di TE con una classe di blocco di trasformatore personalizzata che soddisfi la firma del livello di blocco di timm.

import transformer_engine.pytorch as tefrom transformer_engine.common import recipeclass TE_Block(te.transformer.TransformerLayer):    def __init__(            self,            dim,            num_heads,            mlp_ratio=4.,            qkv_bias=False,            qk_norm=False,            proj_drop=0.,            attn_drop=0.,            init_values=None,            drop_path=0.,            act_layer=None,            norm_layer=None,            mlp_layer=None    ):        super().__init__(            hidden_size=dim,            ffn_hidden_size=int(dim * mlp_ratio),            num_attention_heads=num_heads,            hidden_dropout=proj_drop,            attention_dropout=attn_drop            )

Successivamente, modifichiamo l’inizializzazione di VisionTransformer per utilizzare il nostro blocco personalizzato:

modello = VisionTransformer(      embed_dim=1280,      depth=32,      num_heads=16,      block_fn=TE_Block      ).cuda(device)

Fino ad ora non abbiamo apportato alcuna modifica specifica per H100: lo stesso codice può essere eseguito su istanze p4d alimentate da A100.
L’ultima modifica consiste nell’incapsulare il passaggio in avanti del modello con un
manager di contesto te.fp8_autocast. Questo cambiamento richiede una GPU che supporti FP8:

with torch.autocast(device_type='cuda', dtype=torch.bfloat16):    with te.fp8_autocast(enabled=True):        outputs = model(inputs)    perdita = criterio(outputs, label)

Alcune osservazioni cautele sul
uso di FP8

L’uso di una rappresentazione in virgola mobile a 8 bit (rispetto a una rappresentazione a 16 o 32 bit) implica una precisione inferiore e un range dinamico inferiore. Ciò può avere un impatto significativo sulla raggiungibilità e/o sulla velocità di convergenza del modello. Anche se l’implementazione sottostante di TE FP8 è progettata per affrontare questa sfida, non c’è alcuna garanzia che funzioni con il tuo modello. Potrebbe essere necessario manipolare le meccaniche sottostanti di FP8 (ad esempio, utilizzando le API di ricetta TE), regolare alcuni degli iperparametri e/o limitare l’applicazione di FP8 a sezioni del modello. Potresti scoprire che, nonostante tutti i tuoi tentativi, il tuo modello non è semplicemente compatibile con FP8.

Risultati

Nella tabella sottostante riassumiamo i risultati dei nostri esperimenti su entrambi i tipi di istanza EC2 p4d.24xlarge e p5.48xlarge, con e senza la libreria TE. Per gli esperimenti su p5.48xlarge, abbiamo raddoppiato la dimensione del batch per aumentare l’utilizzo della memoria GPU da 80 GB. L’uso di FP8 riduce il consumo di memoria GPU consentendo un ulteriore aumento delle dimensioni del batch.

Risultati degli esperimenti (By Author)

Possiamo vedere che l’uso del blocco trasformatore TE ha aumentato la performance del rapporto prezzo-prestazioni sia sui tipi di istanze p4d (~19%) che su quelli p5 (~32%). Utilizzando FP8, le prestazioni per p5 aumentano di ulteriori ~20%. Seguendo le ottimizzazioni TE e FP8, la performance del rapporto prezzo delle istanze p5.48large basate su H100 supera quella delle istanze p4d.24large basate su A100, anche se non di molto (~2%). Tenendo conto dell’aumento di 3x nella velocità di addestramento, possiamo affermare in modo sicuro che p5 sarebbe il tipo di istanza migliore per l’addestramento del nostro modello ottimizzato.

Si noti che l’aumento relativamente piccolo del rapporto prezzo-prestazioni (molto inferiore al 40% menzionato nell’annuncio di p5) ci lascia desiderare ulteriori ottimizzazioni specifiche per H100… ma quelle dovranno aspettare un altro post :).

Riassunto

In questo post abbiamo dimostrato come programmare uno script di addestramento PyTorch per utilizzare tipi di dati in virgola mobile a 8 bit. Abbiamo inoltre dimostrato come l’uso di FP8 possa essere un fattore chiave per ottenere le migliori prestazioni dalle moderne GPU come Nvidia H100. È importante sottolineare che la fattibilità di FP8 e il suo impatto sulle prestazioni di addestramento possono variare notevolmente in base ai dettagli del modello.

Questo post continua una lunga serie di pubblicazioni sul tema dell’ottimizzazione dei carichi di lavoro di machine learning. Assicurati di dare un’occhiata ad alcuni dei nostri altri post su questo importante argomento.