Analisi delle prestazioni e ottimizzazione del modello PyTorch – Parte 2.

Performance analysis and optimization of PyTorch model - Part 2.

Come Identificare e Ridurre i Calcoli della CPU Nel Tuo Training Step con PyTorch Profiler e TensorBoard

Foto di Denise Chan su Unsplash

Questo è il secondo articolo di una serie di post sul tema dell’analisi e dell’ottimizzazione di un modello PyTorch in esecuzione su una GPU. Nel nostro primo post abbiamo dimostrato il processo – e il significativo potenziale – di analizzare e ottimizzare iterativamente un modello PyTorch utilizzando PyTorch Profiler e TensorBoard. In questo post ci concentreremo su un tipo specifico di problema di performance che è particolarmente diffuso in PyTorch a causa del suo utilizzo dell’esecuzione immediata: la dipendenza dalla CPU per porzioni dell’esecuzione del modello. Identificare la presenza e la fonte di questi tipi di problemi di performance può essere piuttosto difficile e spesso richiede l’uso di un analizzatore di performance dedicato. In questo post condivideremo alcuni consigli per identificare tali problemi di performance quando si utilizza PyTorch Profiler e il plugin PyTorch Profiler TensorBoard.

I Pro e i Contro dell’Esecuzione Immediata

Uno dei principali vantaggi di PyTorch è la sua modalità di esecuzione immediata. In modalità immediata, ogni operazione PyTorch che forma il modello viene eseguita indipendentemente non appena viene raggiunta. Questo è in contrasto con la modalità grafica in cui l’intero modello viene precompilato in un singolo grafico in modo ottimale per l’esecuzione sulla GPU ed eseguito come un tutto. Di solito, questa precompilazione porta a una migliore performance (ad esempio, vedere qui). In modalità immediata, il contesto di programmazione ritorna all’applicazione dopo ogni operazione, consentendoci di accedere ed valutare tensori arbitrari. Ciò rende più facile costruire, analizzare e debuggare modelli di ML. D’altra parte, rende anche il nostro modello più suscettibile all’inserimento (a volte accidentale) di blocchi di codice subottimali. Come dimostreremo, sapere come identificare e correggere tali blocchi di codice può avere un impatto significativo sulla velocità del tuo modello.

Esempio Giocattolo

Nelle seguenti sezioni introduciamo l’esempio giocattolo che utilizzeremo per la nostra dimostrazione. Il codice è molto liberamente basato sull’esempio del nostro precedente post e sulla funzione di perdita definita in questo tutorial PyTorch.

Iniziamo definendo un semplice modello di classificazione. La sua architettura non è significativa per questo post.

import torchimport torch.nn as nnimport torch.nn.functional as Fimport torch.optimimport torch.profilerimport torch.utils.dataimport torchvision.modelsimport torchvision.transforms as Tfrom torchvision.datasets.vision import VisionDatasetimport numpy as npfrom PIL import Image# modello di esempioclass Net(nn.Module):    def __init__(self):        super().__init__()        self.conv1 = nn.Conv2d(3, 8, 3, padding=1)        self.conv2 = nn.Conv2d(8, 12, 3, padding=1)        self.conv3 = nn.Conv2d(12, 16, 3, padding=1)        self.conv4 = nn.Conv2d(16, 20, 3, padding=1)        self.conv5 = nn.Conv2d(20, 24, 3, padding=1)        self.conv6 = nn.Conv2d(24, 28, 3, padding=1)        self.conv7 = nn.Conv2d(28, 32, 3, padding=1)        self.conv8 = nn.Conv2d(32, 10, 3, padding=1)        self.pool = nn.MaxPool2d(2, 2)    def forward(self, x):        x = self.pool(F.relu(self.conv1(x)))        x = self.pool(F.relu(self.conv2(x)))        x = self.pool(F.relu(self.conv3(x)))        x = self.pool(F.relu(self.conv4(x)))        x = self.pool(F.relu(self.conv5(x)))        x = self.pool(F.relu(self.conv6(x)))        x = self.pool(F.relu(self.conv7(x)))        x = self.pool(F.relu(self.conv8(x)))        x = torch.flatten(x, 1) # flatten all dimensions except batch        return x

Successivamente, definiamo una funzione di perdita di entropia incrociata abbastanza standard. Questa funzione di perdita sarà il principale oggetto della nostra discussione.

def log_softmax(x):    return x - x.exp().sum(-1).log().unsqueeze(-1)def weighted_nll(pred, target, weight):    assert target.max() < 10    nll = -pred[range(target.shape[0]), target]    nll = nll * weight[target]    nll = nll / weight[target].sum()    sum_nll = nll.sum()    return sum_nll# definizione della perdita personalizzataclass CrossEntropyLoss(nn.Module):    def forward(self, input, target):        pred = log_softmax(input)        loss = weighted_nll(pred, target, torch.Tensor([0.1]*10).cuda())        return loss

Infine, definiamo il dataset e il ciclo di addestramento:

# dataset con immagini casuali che imitano le proprietà di CIFAR10class FakeCIFAR(VisionDataset):    def __init__(self, transform):        super().__init__(root=None, transform=transform)        self.data = np.random.randint(low=0,high=256,size=(10000,32,32,3),dtype=np.uint8)        self.targets = np.random.randint(low=0,high=10,size=(10000),dtype=np.uint8).tolist()    def __getitem__(self, index):        img, target = self.data[index], self.targets[index]        img = Image.fromarray(img)        if self.transform is not None:            img = self.transform(img)        return img, target    def __len__(self) -> int:        return len(self.data)transform = T.Compose(    [T.Resize(256),     T.PILToTensor()])train_set = FakeCIFAR(transform=transform)train_loader = torch.utils.data.DataLoader(train_set, batch_size=1024,                               shuffle=True, num_workers=8, pin_memory=True)device = torch.device("cuda:0")model = Net().cuda(device)criterion = CrossEntropyLoss().cuda(device)optimizer = torch.optim.SGD(model.parameters(), lr=0.001, momentum=0.9)model.train()# ciclo di addestramento avvolto con oggetto profilerwith torch.profiler.profile(        schedule=torch.profiler.schedule(wait=1, warmup=4, active=3, repeat=1),        on_trace_ready=torch.profiler.tensorboard_trace_handler(’./log/example’),        record_shapes=True,        profile_memory=True,        with_stack=True) as prof:    for step, data in enumerate(train_loader):        inputs = data[0].to(device=device, non_blocking=True)        labels = data[1].to(device=device, non_blocking=True)        inputs = (inputs.to(torch.float32) / 255. - 0.5) / 0.5        if step >= (1 + 4 + 3) * 1:            break        outputs = model(inputs)        loss = criterion(outputs, labels)        optimizer.zero_grad(set_to_none=True)        loss.backward()        optimizer.step()        prof.step()

Un esperto sviluppatore PyTorch potrebbe aver già notato che il nostro esempio contiene alcune righe di codice inefficienti nella funzione di perdita. Allo stesso tempo, non c’è nulla di ovviamente sbagliato e questi tipi di inefficienze non sono rari. Se vuoi testare la tua competenza in PyTorch, cerca di trovare tre problemi con la nostra implementazione della perdita di entropia incrociata prima di continuare a leggere. Nelle prossime sezioni supporremo di non essere riusciti a trovare questi problemi da soli e mostreremo come possiamo utilizzare PyTorch Profiler e il suo plugin TensorBoard associato per identificarli.

Come nel nostro post precedente, eseguiremo iterativamente un esperimento, identificheremo i problemi di prestazioni e cercheremo di risolverli. Eseguiremo i nostri esperimenti su un’istanza Amazon EC2 g5.2xlarge (contenente una GPU NVIDIA A10G e 8 vCPU) e utilizzando l’immagine Docker ufficiale di AWS PyTorch 2.0. La nostra scelta di ambiente di addestramento era relativamente arbitraria e non dovrebbe essere vista come un’approvazione di nessuno dei suoi componenti.

Risultati di prestazioni iniziali

Nell’immagine sottostante mostriamo la scheda Panoramica del rapporto sulle prestazioni dello script sopra.

Performance Overview of Baseline Model (Captured by Author)

Come possiamo vedere, la nostra utilizzazione della GPU è relativamente alta al 92,04% e il tempo di passo è di 216 millisecondi. (Come nel nostro post precedente, la panoramica in torch-tb-profiler versione 0.4.1 somma il tempo di passo di tutti e tre i passaggi di addestramento.) Da questo rapporto da solo potresti non pensare che ci fosse qualcosa di sbagliato nel nostro modello. Tuttavia, la Vista traccia del rapporto sulle prestazioni racconta una storia completamente diversa:

Trace View of Baseline Model (Captured by Author)

Come evidenziato sopra, il passaggio in avanti della nostra perdita di entropia incrociata da solo richiede 211 dei 216 millisecondi del passo di addestramento! Questo è un chiaro segnale che qualcosa non va. La nostra funzione di perdita contiene un numero ridotto di calcoli rispetto al modello e certamente non dovrebbe rappresentare il 98% del tempo del passo. Guardando più da vicino lo stack di chiamate, possiamo vedere alcune chiamate di funzione che rafforzano i nostri sospetti, tra cui “to”, “copy_” e “cudaStreamSynchronize”. Questa combinazione indica di solito che i dati vengono copiati dalla CPU alla GPU, non qualcosa che vogliamo che accada nel mezzo del nostro calcolo di perdita. In questo caso, il nostro problema di prestazioni coincide anche con un breve calo dell’utilizzo della GPU, come evidenziato nell’immagine. Tuttavia, questo non è sempre il caso. Spesso, i cali nell’utilizzo della GPU non saranno allineati con il problema di prestazioni o potrebbero non essere visti affatto.

Ora sappiamo di avere un problema di prestazioni nella nostra funzione di perdita e che è probabile che sia legato alla copia dei tensori dall’host alla GPU. Tuttavia, questo potrebbe non essere sufficiente per identificare la linea di codice precisa che sta causando il problema. Per facilitare la nostra ricerca, avvolgeremo ogni linea di codice con un gestore di contesto torch.profiler.record_function etichettato e ripeteremo l’analisi di profilazione.

# definizione della perdita personalizzataclass CrossEntropyLoss(nn.Module):    def forward(self, input, target):        with torch.profiler.record_function('log_softmax'):            pred = log_softmax(input)        with torch.profiler.record_function('define_weights'):            weights = torch.Tensor([0.1]*10).cuda()        with torch.profiler.record_function('weighted_nll'):            loss = weighted_nll(pred, target, torch.Tensor([0.1]*10).cuda())        return loss

L’aggiunta delle etichette ci aiuta a identificare la definizione del peso, o più precisamente, la copia dei pesi nella GPU, come la linea di codice problematica.

Problema di prestazioni della definizione dei pesi come visto nella visualizzazione traccia (captured by author)

Ottimizzazione n. 1: Rimuovere le copie ridondanti da host a GPU dal passaggio di addestramento

Una volta identificato il nostro primo problema, risolverlo è piuttosto banale. Nel blocco di codice sottostante, copiamo il nostro vettore di peso sulla GPU una sola volta nella funzione di inizializzazione della perdita:

class CrossEntropyLoss(nn.Module):    def __init__(self):        super().__init__()        self.weight = torch.Tensor([0.1]*10).cuda()    def forward(self, input, target):        with torch.profiler.record_function('log_softmax'):            pred = log_softmax(input)        with torch.profiler.record_function('weighted_nll'):            loss = weighted_nll(pred, target, self.weight)        return loss

L’immagine sottostante mostra i risultati dell’analisi delle prestazioni dopo questa correzione:

Panoramica delle prestazioni dopo l'ottimizzazione n. 1 (captured by author)

Purtroppo, la nostra prima ottimizzazione ha avuto un impatto molto marginale sul tempo del passo. Se guardiamo il rapporto Vista traccia, possiamo vedere che abbiamo un nuovo grave problema di prestazioni che dobbiamo affrontare.

Vista traccia dopo l'ottimizzazione n. 1 (captured by author)

Il nostro nuovo rapporto indica un problema proveniente dalla nostra funzione weighted_nll. Come prima, abbiamo usato torch.profiler.record_function per identificare la linea di codice problematica. In questo caso si tratta della chiamata assert.

def weighted_nll(pred, target, weight):    with torch.profiler.record_function('assert'):        assert target.max() < 10    with torch.profiler.record_function('range'):        r = range(target.shape[0])    with torch.profiler.record_function('index'):        nll = -pred[r, target]    with torch.profiler.record_function('nll_calc'):        nll = nll * weight[target]        nll = nll/ weight[target].sum()        sum_nll = nll.sum()    return sum_nll

Si noti che questo problema era presente anche nell’esperimento di base, ma era nascosto dal nostro precedente problema di prestazioni. Non è raro durante l’ottimizzazione delle prestazioni che problemi gravi, precedentemente nascosti da altri problemi, si manifestino improvvisamente in questo modo.

Un’analisi più approfondita della call stack mostra chiamate a “item”, “_local_scalar_dense” e “cudaMemcpyAsync”. Questo è spesso un indicatore che i dati vengono copiati dalla GPU all’host. Infatti, la nostra chiamata di assert, che viene eseguita sulla CPU, richiede l’accesso al tensore di destinazione residente sulla GPU, provocando così una copia di dati altamente inefficiente.

Ottimizzazione #2: Rimuovere le copie ridondanti dalla GPU all’host dal passaggio di training

Pur verificando la legalità delle etichette di input potrebbe essere giustificato, dovrebbe essere fatto in modo che non influisca così negativamente sulle nostre prestazioni di training. Nel nostro caso, la correzione del problema è una questione semplice di spostare l’assert nella pipeline di input dei dati, prima che le etichette vengano copiate nella GPU. Dopo la rimozione dell’assert, le nostre prestazioni rimangono per lo più invariate:

Performance Overview Following Optimization #2 (Captured by Author)

Nota importante: Sebbene il nostro obiettivo sia di solito quello di cercare di ridurre le copie tra host e GPU nel passaggio in avanti, ci sono momenti in cui questo non è possibile (ad esempio, se richiediamo un kernel non supportato dalla GPU) o indesiderabile (ad esempio, se l’esecuzione di un particolare kernel sulla CPU aumenterà le prestazioni).

L’analisi della Trace View ci introduce al nostro prossimo problema di prestazioni:

Trace View Following Optimization #2 (Captured by Author)

Di nuovo, vediamo che la nostra precedente ottimizzazione ha scoperto un nuovo grave problema di prestazioni, questa volta durante l’indicizzazione del nostro tensore pred. Gli indici sono definiti dai tensori r e target. Mentre il tensore target risiede già sulla GPU, il tensore r, definito sulla riga precedente, non lo fa. Questo, ancora una volta, attiva una copia inefficiente di dati dall’host alla GPU.

Ottimizzazione #3: Sostituire range con torch.arange

La funzione range di Python restituisce una lista sulla CPU. La presenza di qualsiasi lista nel passaggio di training dovrebbe essere un campanello d’allarme. Nel blocco di codice sottostante, sostituiamo l’uso di range con torch.arange e lo configuriamo per creare il tensore di output direttamente sulla GPU:

def weighted_nll(pred, target, weight):    with torch.profiler.record_function('range'):        r = torch.arange(target.shape[0], device="cuda:0")    with torch.profiler.record_function('index'):        nll = -pred[r, target]    with torch.profiler.record_function('nll_calc'):        nll = nll * weight[target]        nll = nll/ weight[target].sum()        sum_nll = nll.sum()    return sum_nll

I risultati di questa ottimizzazione sono mostrati di seguito:

Performance Overview Following Optimization #3 (Captured by Author)

Ora stiamo parlando!! Il tempo del nostro passo è sceso a 5,8 millisecondi, un aumento delle prestazioni del 3700%.

La Trace View aggiornata mostra che la funzione di perdita è scesa a un valore molto ragionevole di 0,5 millisecondi.

Trace View Following Optimization #3 (Captured by Author)

Ma c’è ancora spazio per migliorare. Analizziamo da vicino la Trace View della funzione weighted_nll che occupa la maggior parte del calcolo della perdita.

Trace View della funzione weighted_nll (catturato dall'autore)

Dalla traccia possiamo vedere che la funzione è formata da numerosi piccoli blocchi, ognuno dei quali è alla fine mappato su un kernel CUDA individuale che viene caricato sulla GPU tramite la chiamata CudaLaunchKernel. Idealmente, vorremmo ridurre il numero totale di kernel GPU per ridurre l’interazione tra CPU e GPU. Un modo per farlo è preferire, quando possibile, operatori PyTorch di livello superiore, come torch.nn.NLLLoss. Tali funzioni si presume che “fondono” insieme le operazioni sottostanti, riducendo così il numero totale di kernel.

Ottimizzazione n. 4: sostituire la NLL personalizzata con torch.nn.NLLLoss

Il blocco di codice qui sotto contiene la nostra definizione di perdita aggiornata, che ora utilizza torch.nn.NLLLoss.

class CrossEntropyLoss(nn.Module):    def __init__(self):        super().__init__()        self.weight = torch.Tensor([0.1]*10).cuda()    def forward(self, input, target):        pred = log_softmax(input)        nll = torch.nn.NLLLoss(self.weight)        loss = nll(pred, target)        return loss

Qui abbiamo preso la libertà di introdurre un altro errore comune che procederemo a dimostrare.

Utilizzando la funzione di livello superiore riduciamo ulteriormente il tempo di esecuzione a 5,3 millisecondi (rispetto a 5,8).

Performance Overview seguito dall'ottimizzazione n. 4 (catturato dall'autore)

Tuttavia, se osserviamo da vicino la Trace View, possiamo vedere che una parte significativa della funzione di perdita viene ora spesa per inizializzare l’oggetto torch.nn.NLLLoss!

Trace View seguente l'ottimizzazione n. 4 (catturato dall'autore)

Tornando alla nostra funzione di perdita, possiamo vedere che stiamo inizializzando un nuovo oggetto NLLLoss in ogni iterazione del passaggio di addestramento. Naturalmente, l’inizializzazione dell’oggetto avviene sulla CPU e, anche se (nel nostro caso) è relativamente veloce, è qualcosa che vorremmo evitare di fare durante il nostro passaggio di addestramento.

Ottimizzazione n. 5: Evitare di inizializzare oggetti nel passaggio di addestramento

Nel blocco di codice qui sotto abbiamo modificato la nostra implementazione di perdita in modo che venga creato un’unica istanza di torch.nn.NLLLoss nella funzione init.

class CrossEntropyLoss(nn.Module):    def __init__(self):        super().__init__()        self.weight = torch.Tensor([0.1]*10).cuda()        self.nll = torch.nn.NLLLoss(self.weight)     def forward(self, input, target):        pred = log_softmax(input)        loss = self.nll(pred, target)        return loss

I risultati mostrano un ulteriore miglioramento nel tempo di esecuzione che ora è di 5,2 millisecondi.

Ottimizzazione n. 6: Utilizzare torch.nn.CrossEntropyLoss invece di perdita personalizzata

PyTorch include una built-in torch.nn.CrossEntropyLoss che ora valutiamo e confrontiamo con la nostra implementazione di perdita personalizzata.

criterion = torch.nn.CrossEntropyLoss().cuda(device)

Il tempo di esecuzione risultante è un nuovo record di 5 millisecondi per un miglioramento complessivo delle prestazioni del 4200% (rispetto ai 216 millisecondi con cui abbiamo iniziato).

Il miglioramento delle prestazioni del passaggio in avanti del calcolo della perdita è ancora più sorprendente: da un punto di partenza di 211 millisecondi, siamo scesi a soli 79 microsecondi(!), come si può vedere qui sotto:

Ottimizzazione #7: Compilare la funzione di perdita

Per il nostro ultimo tentativo di ottimizzazione, configureremo la funzione di perdita per l’esecuzione in modalità grafica utilizzando l’API torch.compile. Come abbiamo discusso a lungo in questo post e dimostrato nel prequel di questo post, torch.compile utilizzerà tecniche come la fusione dei kernel e l’esecuzione fuori ordine per mappare la funzione di perdita in kernel di calcolo a basso livello in modo ottimale per l’acceleratore di formazione sottostante.

criterion = torch.compile(torch.nn.CrossEntropyLoss().cuda(device))

Nell’immagine qui sotto si può vedere il risultato della Trace View di questo esperimento.

La prima cosa che si nota è l’apparizione di termini contenenti “OptimizedModule” e “dynamo” che sono indicativi dell’uso di torch.compile. Possiamo anche vedere che, in pratica, la compilazione del modello non ha ridotto il numero di kernel caricati dalla funzione di perdita, il che significa che non ha identificato opportunità per ulteriore fusione del kernel. Infatti, nel nostro caso, la compilazione della perdita ha effettivamente causato un aumento del tempo di esecuzione del passaggio in avanti della funzione di perdita da 79 a 154 microsecondi. Sembra che la CrossEntropyLoss non sia abbastanza sostanziosa per beneficiare di questa ottimizzazione.

Potreste chiedervi perché non possiamo semplicemente applicare la compilazione di torch alla nostra funzione di perdita iniziale e fare affidamento su di essa per compilare il nostro codice in modo ottimale. Ciò potrebbe risparmiare tutta la fatica dell’ottimizzazione passo dopo passo che abbiamo descritto sopra. Il problema con questo approccio è che sebbene la compilazione PyTorch 2.0 (al momento della stesura di questo testo) ottimizzi effettivamente alcuni tipi di crossover GPU-CPU, alcuni tipi faranno crashare la compilazione del grafico e altri creeranno più grafici piccoli anziché uno grande. L’ultima categoria causa interruzioni del grafico che limitano sostanzialmente la capacità della funzione di compilazione di torch di migliorare le prestazioni. (Un modo per affrontare questo problema è di chiamare torch.compile con il flag fullgraph impostato su True.) Vedere il nostro post precedente per maggiori dettagli sull’uso di questa opzione.

Risultati

Nella tabella qui sotto riassumiamo i risultati degli esperimenti che abbiamo eseguito:

Optimization Experiments Results (By Author)

Le nostre successive ottimizzazioni hanno portato a un incredibile aumento delle prestazioni del 4143%! Ricordiamo che siamo partiti da una funzione di perdita piuttosto innocua. Senza un’analisi approfondita del comportamento della nostra applicazione, potremmo non aver mai saputo che c’era qualcosa che non andava e avremmo continuato a pagare 41 volte più di quanto necessario nella nostra vita.

Potreste aver notato che l’utilizzo della GPU è diminuito significativamente nei nostri ultimi test. Questo indica un potenziale enorme per ulteriori ottimizzazioni delle prestazioni. Sebbene la nostra dimostrazione sia giunta alla fine, il nostro lavoro non è finito. Vedere il nostro post precedente per alcune idee su come procedere da qui.

Conclusioni

Sommarizziamo alcune delle cose che abbiamo imparato. Dividiamo il riassunto in due parti. Nella prima, descriviamo alcune abitudini di codifica che possono influire sulle prestazioni della formazione. Nella seconda, raccomandiamo alcuni suggerimenti per la profilazione delle prestazioni. Si noti che queste conclusioni si basano sull’esempio che abbiamo condiviso in questo post e potrebbero non applicarsi al vostro caso d’uso. I modelli di apprendimento automatico variano notevolmente in proprietà e comportamento. Pertanto, si consiglia vivamente di valutare queste conclusioni in base ai dettagli del proprio progetto.

Consigli di codifica

Il modo in cui si implementa il passaggio in avanti del proprio modello può avere un impatto significativo sulle sue prestazioni. Qui elenchiamo solo alcune raccomandazioni basate sull’esempio che abbiamo coperto in questo post.

  1. Evitare di inizializzare tensori costanti nel passaggio in avanti. Farlo invece nel costruttore.
  2. Evitare di utilizzare assert su tensori residenti sulla GPU nel passaggio in avanti. Spostarli invece nella pipeline di input dei dati e/o verificare se PyTorch ha eventuali metodi integrati per eseguire la verifica dei dati di cui si ha bisogno.
  3. Evitare l’uso di liste. Verificare se l’utilizzo di torch.arange per creare un tensore direttamente sul dispositivo può essere una migliore alternativa.
  4. Usare gli operatori PyTorch come torch.nn.NLLLoss e torch.nn.CrossEntropyLoss invece di creare le proprie implementazioni di perdita.
  5. Evitare di inizializzare oggetti nel passaggio in avanti. Farlo invece nel costruttore.
  6. Considerare l’utilizzo di torch.compile quando rilevante.

Consigli di analisi delle prestazioni

Come abbiamo dimostrato, la Trace View del plugin Tensorboard PyTorch Profiler è stata fondamentale nell’individuare i problemi di prestazioni nel nostro modello. Di seguito riassumiamo alcuni dei principali punti salienti del nostro esempio:

  1. L’utilizzo elevato della GPU NON è necessariamente un segno che il proprio codice sta funzionando in modo ottimale.
  2. Stare attenti alle porzioni del codice che impiegano più tempo del previsto.
  3. Utilizzare torch.profiler.record_function per individuare i problemi di prestazioni.
  4. Le cadute di utilizzo della GPU non sono necessariamente allineate con la fonte del problema di prestazioni.
  5. Stare attenti alle copie di dati non intenzionali dall’host alla GPU. Questi sono identificati tipicamente dalle chiamate a “to”, “copy_” e “cudaStreamSynchronize”, che è possibile cercare nella Trace View.
  6. Stare attenti alle copie di dati non intenzionali dalla GPU all’host. Questi sono tipicamente identificati dalle chiamate a “item” e “cudaStreamSynchronize”, che è possibile cercare nella Trace View.

Sommario

In questo post ci siamo concentrati sui problemi di prestazioni nelle applicazioni di addestramento derivanti dall’interazione ridondante tra CPU e GPU durante il passaggio in avanti della fase di addestramento. Abbiamo dimostrato come gli analizzatori delle prestazioni come PyTorch Profiler e il relativo plugin TensorBoard possono essere utilizzati per individuare tali problemi e facilitare un significativo miglioramento delle prestazioni.

Come nel nostro post precedente, sottolineiamo che il percorso verso un’ottimizzazione di successo varierà notevolmente in base ai dettagli del progetto di addestramento, compresa l’architettura del modello e l’ambiente di addestramento. In pratica, raggiungere i propri obiettivi potrebbe essere più difficile rispetto all’esempio che abbiamo presentato qui. Alcune delle tecniche descritte potrebbero avere scarso impatto sulle prestazioni o addirittura peggiorarle. Notiamo inoltre che le precise ottimizzazioni che abbiamo scelto e l’ordine in cui abbiamo deciso di applicarle erano in qualche modo arbitrari. Si incoraggia vivamente a sviluppare propri strumenti e tecniche per raggiungere i propri obiettivi di ottimizzazione in base ai dettagli specifici del progetto.