Scaling up BERT-like model Inference on modern CPU – Parte 2

'BERT-like model Inference on modern CPU - Part 2'

Introduzione: Utilizzare il software Intel per ottimizzare l’efficienza dell’IA sulla CPU

Come abbiamo dettagliato nel nostro precedente post sul blog, i processori Intel Xeon forniscono un insieme di funzionalità appositamente progettate per carichi di lavoro di intelligenza artificiale come AVX512 o VNNI (Istruzioni Vettoriali per Reti Neurali) per un’efficienza di inferenza ottimale utilizzando reti neurali quantizzate intere per l’inferenza insieme a strumenti di sistema aggiuntivi per garantire che il lavoro venga svolto nel modo più efficiente possibile. In questo post del blog, ci concentreremo sulle ottimizzazioni del software e ti forniremo un’idea delle prestazioni della nuova generazione di processori Xeon di Intel, Ice Lake. Il nostro obiettivo è fornirti una visione completa di ciò che è disponibile sul lato del software per sfruttare al massimo l’hardware Intel. Come nel precedente post del blog, mostriamo le prestazioni con risultati di benchmark e grafici, insieme a nuovi strumenti per rendere facili da usare tutte queste opzioni e funzionalità.

Ad aprile, Intel ha lanciato la sua ultima generazione di processori Intel Xeon, chiamata Ice Lake, rivolta a carichi di lavoro di intelligenza artificiale più efficienti e performanti. Più precisamente, i processori Xeon Ice Lake possono raggiungere un’accelerazione fino al 75% su una varietà di compiti NLP rispetto alla precedente generazione di processori Xeon Cascade Lake. Ciò viene ottenuto da una combinazione di miglioramenti hardware e software, come nuove istruzioni e supporto PCIe 4.0 presenti sulla nuova architettura Sunny Cove per supportare carichi di lavoro di apprendimento automatico e di deep learning. Infine, Intel ha lavorato su ottimizzazioni dedicate per vari framework che ora vengono forniti con le versioni Intel, come l’Estensione Intel per Scikit Learn, TensorFlow di Intel e l’Estensione Intel per PyTorch.

Tutte queste funzionalità sono molto a basso livello nello stack di ciò che gli scienziati dei dati e gli ingegneri di machine learning utilizzano nel loro set di strumenti quotidiano. Nella stragrande maggioranza delle situazioni, è più comune fare affidamento su framework e librerie di livello superiore per la manipolazione di array multidimensionali, come PyTorch e TensorFlow, e utilizzare operatori matematici altamente ottimizzati come BLAS (Basic Linear Algebra Subroutines) per la parte computazionale.

In questo ambito, Intel svolge un ruolo essenziale fornendo componenti software nell’ambito di oneAPI, che semplifica l’utilizzo di routine di algebra lineare altamente efficienti tramite Intel oneMKL (Math Kernel Library), un framework di parallelizzazione di livello superiore con Intel OpenMP o Threading Building Blocks (oneTBB). Inoltre, oneAPI fornisce alcune librerie specifiche per il dominio, come Intel oneDNN per le primitive delle reti neurali profonde (ReLU, fully-connected, ecc.) o oneCCL per la comunicazione collettiva, particolarmente utile quando si utilizzano configurazioni distribuite per eseguire operazioni di all-reduce efficienti su più host.

Alcune di queste librerie, in particolare MKL o oneDNN, sono incluse nativamente nei framework come PyTorch e TensorFlow (a partire dalla versione 2.5.0) per offrire all’utente finale tutti i miglioramenti delle prestazioni già pronti all’uso. Quando si desidera sfruttare funzionalità hardware molto specifiche, Intel fornisce versioni personalizzate dei software più comuni, ottimizzate appositamente per la piattaforma Intel. Questo è ad esempio il caso di TensorFlow, per il quale Intel fornisce versioni personalizzate, altamente ottimizzate del framework, o dell’Estensione Intel per PyTorch (IPEX), che può essere considerata come un laboratorio di funzionalità prima dell’integrazione in PyTorch.

Approfondimento: Sfruttare le funzionalità avanzate di Intel per migliorare le prestazioni dell’IA

Regolazioni delle prestazioni

Come evidenziato in precedenza, affronteremo un nuovo insieme di elementi regolabili per migliorare le prestazioni della nostra applicazione di intelligenza artificiale. Da un punto di vista generale, ogni framework di apprendimento automatico e di deep learning è composto dagli stessi elementi:

  1. Un modo strutturato per rappresentare i dati in memoria (vettori, matrici, ecc.)
  2. Implementazione degli operatori matematici
  3. Parallelizzazione efficiente dei calcoli sull’hardware di destinazione

Oltre ai punti elencati sopra, i framework di deep learning forniscono modi per rappresentare il flusso dei dati e le dipendenze per calcolare i gradienti. Questo esula dallo scopo di questo post del blog e sfrutta gli stessi componenti elencati in precedenza!

Figura 1. Panoramica delle librerie Intel nell’ambito di oneAPI

1. Librerie di allocazione e gestione della memoria

Questo post del blog tralascerà deliberatamente il primo punto sulla rappresentazione dei dati in quanto è qualcosa di specifico del framework. Per riferimento, PyTorch utilizza la propria implementazione chiamata ATen, mentre TensorFlow si basa sulla libreria open source Eigen a questo scopo.

Anche se è molto complesso applicare ottimizzazioni generiche a diverse strutture e layout degli oggetti, c’è un’area in cui possiamo avere un impatto: l’allocazione della memoria. Come breve promemoria, l’allocazione della memoria qui si riferisce al processo di richiedere in modo programmato al sistema operativo un’area dinamica (sconosciuta in precedenza) sul sistema in cui saremo in grado di memorizzare elementi, come ad esempio la funzione malloc e le sue derivate in C o l’operatore new in C++. L’efficienza della memoria, sia in termini di velocità che di frammentazione, è un vasto argomento scientifico e ingegneristico con molteplici soluzioni a seconda del compito e dell’hardware sottostante. Negli ultimi anni abbiamo visto sempre più lavoro in questo settore, con in particolare:

  • jemalloc (Facebook – 2005)
  • mimalloc (Microsoft – 2019)
  • tcmalloc (Google – 2020)

Ognuno propone differenti approcci per migliorare gli aspetti dell’allocazione e gestione della memoria su diversi software.

2. Parallelizzazione efficiente dei calcoli

Ora che abbiamo un modo efficiente per rappresentare i nostri dati, abbiamo bisogno di un modo per sfruttare al massimo l’hardware computazionale a nostra disposizione. Curiosamente, per quanto riguarda l’inferenza, le CPU hanno un potenziale vantaggio rispetto alle GPU nel senso che sono ovunque e non richiedono componenti specifici dell’applicazione e personale amministrativo per essere utilizzate.

Le CPU moderne sono dotate di molti core e di meccanismi complessi per aumentare le prestazioni generali del software. Tuttavia, come abbiamo evidenziato nel primo post del blog, hanno anche caratteristiche che possono essere regolate a seconda del tipo di carico di lavoro (CPU o I/O bound) che si intende utilizzare, per migliorare ulteriormente le prestazioni per la propria applicazione.

Tuttavia, implementare algoritmi paralleli potrebbe non essere semplice come aggiungere più core per svolgere il lavoro. Molteplici fattori, come le strutture dati utilizzate, l’accesso concorrente ai dati, l’invalidazione della cache della CPU – tutti questi possono prevenire che l’algoritmo sia effettivamente più veloce. Come riferimento, consigliamo la presentazione di Scott Meyers: CPU Caches e perché sono importanti se siete interessati ad approfondire l’argomento.

Fortunatamente, ci sono librerie che rendono il processo di sviluppo di tali algoritmi paralleli più facile e meno soggetto a errori. Tra le librerie parallele più comuni possiamo menzionare OpenMP e TBB (Threading Building Blocks), che lavorano a vari livelli, dall’API di programmazione in C/C++ all’ottimizzazione delle variabili di ambiente e alla pianificazione dinamica. Sull’hardware Intel, si consiglia di utilizzare l’implementazione Intel della specifica OpenMP, spesso indicata come “IOMP” e disponibile come parte del toolkit Intel oneAPI.

Figura 2. Esempio di codice che mostra il calcolo parallelo attraverso OpenMP

3. Operatori matematici ottimizzati

Ora che abbiamo coperto i blocchi di costruzione necessari per progettare strutture dati efficienti e algoritmi paralleli, l’ultimo pezzo rimasto è quello che esegue il calcolo, quello che implementa la varietà di operatori matematici e strati di reti neurali per fare ciò che amiamo di più, progettare reti neurali! 😊

In ogni toolkit per programmatori, ci sono diversi livelli che possono fornire supporto alle operazioni matematiche, che possono quindi essere ottimizzate in modo diverso a seconda di vari fattori come il layout di archiviazione dei dati utilizzato (memoria contigua, frammentata, impacchettata, ecc.), il formato dei dati che rappresenta ciascun elemento scalare (Float32, Integer, Long, Bfloat16, ecc.) e ovviamente le varie istruzioni supportate dal processore.

Oggi quasi tutti i processori supportano operazioni matematiche di base su elementi scalari (un singolo elemento alla volta) o in modalità vettorializzata (cioè operano su più elementi all’interno delle stesse istruzioni CPU, dette SIMD “Single Instruction Multiple Data”). Insiemi famosi di istruzioni SIMD sono SSE2, AVX, AVX2 e AVX-512 presenti sulle ultime generazioni di CPU Intel in grado di operare su 16 byte di contenuto in un singolo ciclo di clock della CPU.

La maggior parte delle volte, non è necessario preoccuparsi troppo dell’assemblaggio effettivo generato per eseguire una semplice addizione elemento per elemento tra due vettori, ma se lo si fa, ci sono ancora alcune librerie che consentono di andare un livello più in alto rispetto alla scrittura di codice che chiama instrinseche specifiche della CPU per implementare kernel matematici efficienti. Questo è ad esempio ciò che fornisce Intel MKL “Math Kernel Library”, insieme all’interfaccia famosa BLAS “Basic Linear Algebra Subroutines” per implementare tutte le operazioni di base per l’algebra lineare.

Infine, oltre a tutto ciò, si possono trovare alcune librerie specifiche per determinati domini come l’Intel oneDNN che porta tutti i blocchi di costruzione più comuni ed essenziali necessari per implementare strati di reti neurali. Intel MKL e oneDNN sono integrati nativamente nel framework PyTorch, dove possono migliorare le prestazioni per alcune operazioni come Linear + ReLU o Convolution. Sul lato di TensorFlow, oneDNN può essere abilitato impostando la variabile di ambiente TF_ENABLE_ONEDNN_OPTS=1 ( TensorFlow >= 2.5.0 ) per ottenere una macchina simile sotto il cofano.

Elaborazione AI più efficiente sui più recenti processori Intel Ice Lake

Per riportare le prestazioni della gamma di prodotti Ice Lake seguiremo attentamente la metodologia che abbiamo utilizzato per il primo post del blog di questa serie. Come promemoria, adotteremo lo stesso schema per testare le varie configurazioni che evidenzieremo in questo secondo post del blog. Più precisamente, i risultati presentati nelle seguenti sezioni sono basati su:

  • PyTorch: 1.9.0
  • TensorFlow: 2.5.0
  • Dimensioni del Batch: 1, 4, 8, 16, 32, 128
  • Lunghezza della Sequenza: 8, 16, 32, 64, 128, 384, 512

Presenteremo i risultati tramite metriche accettate dal settore per stabilire le prestazioni delle ottimizzazioni proposte:

  • Latency: Tempo necessario per eseguire una singola richiesta di inferenza (ovvero una “chiamata in avanti”) attraverso il modello, espresso in millisecondi.
  • Throughput: Numero di richieste di inferenza (ovvero “chiamate in avanti”) che il sistema può sostenere entro un periodo definito, espresso in chiamate al secondo.

<p+Forniremo anche una linea di base iniziale mostrando i risultati out-of-the-box e una seconda linea di base applicando tutte le diverse ottimizzazioni evidenziate nel primo post del blog. Tutto è stato eseguito su un'istanza cloud fornita da Intel con il processore Intel Ice Lake Xeon Platinum 8380 che opera su Ubuntu 20.04.2 LTS.

Puoi trovare gli stessi processori presso i vari fornitori di servizi cloud:

  • Istanze AWS m6i / c6i
  • Serie Azure Ev5 / Dv5

Figura 3. Specifiche del processore Intel Ice Lake Xeon 8380

Stabilire la linea di base

Come accennato in precedenza, le linee di base saranno composte da due configurazioni diverse: – Out-of-the-box: Eseguiamo i carichi di lavoro così come sono, senza alcuna ottimizzazione – Ottimizzato: Applichiamo le diverse impostazioni presenti nel Blog n. 1

Inoltre, in base ai commenti che abbiamo ricevuto sul post del blog precedente, abbiamo deciso di cambiare il modo in cui presentiamo il framework all’interno dei benchmark risultanti. Pertanto, per il resto di questo secondo post del blog, divideremo i risultati dei benchmark del framework secondo quanto segue:

  • Framework che utilizzano la modalità “eager” per i calcoli (PyTorch, TensorFlow)
  • Framework che utilizzano la modalità “graph” per i calcoli (TorchScript, TensorFlow Graph, Intel Tensorflow)

Linea di base: Latenze dei framework in modalità “eager”

I framework che operano in modalità “eager” di solito scoprono il grafo effettivo durante l’esecuzione. Più precisamente, il grafo di calcolo effettivo non è noto in anticipo e si esegue gradualmente (in modo “eager”) un operatore che diventerà l’input del successivo, etc. fino a raggiungere i nodi foglia (output).

Questi framework di solito offrono maggiore flessibilità nell’algoritmo che si implementa a discapito di un aumento del tempo di esecuzione e di una leggera maggiore utilizzo di memoria per tenere traccia di tutti gli elementi necessari per il passaggio all’indietro.

Infine, di solito è più difficile abilitare ottimizzazioni del grafo come la fusione degli operatori in questi framework. Ad esempio, molte librerie di deep learning come oneDNN hanno kernel ottimizzati per la Convoluzione + ReLU, ma è necessario sapere prima di eseguire il grafo che questo pattern si verificherà all’interno della sequenza di operazioni, il che, per definizione, non è possibile nei framework “eager”.

Figura 4. Latenze di PyTorch in base al numero di core coinvolti

Figura 5. Latenze di TensorFlow di Google in base al numero di core coinvolti

Figura 6. Latenze di TensorFlow di Google con oneDNN abilitato in base al numero di core coinvolti

Figura 7. Latenze di Intel TensorFlow in base al numero di core coinvolti

La tendenza generale evidenzia l’impatto positivo del numero di core sulle latenze osservate. Nella maggior parte dei casi, l’aumento del numero di core riduce il tempo di calcolo per le diverse dimensioni dei carichi di lavoro. Tuttavia, l’aggiunta di più core al compito non comporta una riduzione monotona delle latenze, c’è sempre un compromesso tra la dimensione del carico di lavoro e il numero di risorse allocate per eseguire il lavoro.

Come si può vedere dai grafici sopra, un pattern molto comune tende a emergere dall’utilizzo di tutti i core disponibili su sistemi con più di una CPU (più di un socket). La comunicazione tra socket introduce un significativo overhead di latenza e comporta un miglioramento molto limitato della latenza complessiva.

Inoltre, questo overhead di comunicazione tra socket tende ad essere sempre meno percettibile man mano che il carico di lavoro diventa più grande, il che significa che l’utilizzo di tutte le risorse computazionali beneficia dell’utilizzo di tutti i core disponibili. In questo ambito, sembra che PyTorch (Figura 1) e Intel TensorFlow (Figura 4) abbiano un supporto leggermente migliore per il parallelismo, come mostrato per la lunghezza della sequenza 384 e 512, per la quale l’utilizzo di tutti i core riduce comunque la latenza osservata.

Baseline: Tempi di latenza dei framework grafici

Questa volta confrontiamo le prestazioni quando si utilizzano i framework in modalità “Grafico”, in cui il grafico è completamente noto in anticipo e tutte le allocazioni e ottimizzazioni come la potatura del grafico e la fusione degli operatori possono essere effettuate.

Figura 8. Tempi di latenza di TorchScript in relazione al numero di core coinvolti

Figura 9. Tempi di latenza di TensorFlow di Google in relazione al numero di core coinvolti

Figura 10. Tempi di latenza di TensorFlow di Google con oneDNN abilitato in relazione al numero di core coinvolti

Figura 11. Tempi di latenza di Intel TensorFlow in relazione al numero di core coinvolti

Questo viene spesso definito “traccia” del grafico e, come puoi vedere qui, i risultati non sono così diversi da TorchScript (modalità di esecuzione del grafico da PyTorch) rispetto a TensorFlow(s). Tutte le implementazioni di TensorFlow sembrano funzionare meglio di TorchScript quando la parallelizzazione è limitata (basso numero di core coinvolti nei calcoli intra-operazione), ma sembra che questo non scalino efficientemente all’aumentare delle risorse di calcolo, mentre TorchScript sembra essere in grado di sfruttare meglio la potenza delle CPU moderne.

Tuttavia, la differenza tra tutti questi framework nella maggior parte dei casi è molto limitata.

Taratura dell’allocatore di memoria: Può influire sulle latenze osservate?

Uno dei componenti cruciali su cui si basa ogni programma che alloca dinamicamente la memoria è l’allocatore di memoria. Se sei familiare con la programmazione in C/C++, questo componente fornisce i dettagli di basso livello per malloc/free o new/delete. La maggior parte delle volte non devi preoccuparti troppo di esso e quelli predefiniti (come glibc nella maggior parte delle distribuzioni Linux) forniranno ottime prestazioni di default. Tuttavia, in alcune situazioni potrebbe non fornire le prestazioni più efficienti, poiché questi allocatori predefiniti sono progettati per essere “buoni” nella maggior parte dei casi e non ottimizzati per carichi di lavoro o parallelismo specifici.

Quindi, quali sono le alternative e quando sono più adatte rispetto a quelle predefinite? Beh, ancora una volta, dipende dal tipo di contesto intorno al tuo software.

Situazioni possibili sono un elevato numero di allocazioni/deallocazioni che causano frammentazione nel tempo, hardware e/o architettura specifici su cui esegui il tuo software e infine il livello di parallelismo della tua applicazione.

Vedi dove sta andando? L’apprendimento profondo e, per estensione, tutte le applicazioni che effettuano calcoli intensivi sono fortemente multithreaded, questo è anche il caso delle librerie software come PyTorch, TensorFlow e qualsiasi altro framework mirato ai carichi di lavoro di apprendimento automatico.

Le strategie predefinite di allocazione della memoria spesso si basano su pool di memoria globali che richiedono l’uso di primitive di sincronizzazione per funzionare, aumentando la pressione complessiva sul sistema e riducendo le prestazioni della tua applicazione. Alcuni lavori recenti di aziende come Google, Facebook e Microsoft hanno fornito strategie alternative di allocazione della memoria implementate in librerie di allocazione della memoria personalizzate che possono essere facilmente integrate direttamente nei componenti software o utilizzate per sostituire dinamicamente la libreria utilizzata per l’allocazione/deallocazione.

Tra queste librerie, possiamo citarne alcune come tcmalloc, jemalloc e mimalloc.

Figura 12. Benchmark di vari allocatori di memoria su diversi compiti

In questo post del blog ci concentreremo solo sul benchmark di tcmalloc e jemalloc come possibili candidati per sostituire l’allocatore di memoria. Per essere pienamente trasparenti, per il contesto dei risultati riportati di seguito abbiamo utilizzato tcmalloc come parte del pacchetto gperftools disponibile nelle distribuzioni Ubuntu versione 2.9 e jemalloc 5.1.0-1.

Benchmark degli allocatori di memoria

Di nuovo, confrontiamo prima le prestazioni rispetto ai framework che eseguono in modo immediato. Questo è potenzialmente il caso in cui l’allocatore può svolgere il ruolo più importante: poiché il grafico è sconosciuto prima della sua esecuzione, ogni framework deve gestire la memoria richiesta per ciascuna operazione quando incontra l’esecuzione effettiva del nodo sopra, senza possibilità di pianificazione anticipata. In questo contesto, l’allocatore è un componente importante a causa di tutte le chiamate di sistema per allocare e recuperare memoria.

Figura 13. Allocazione di memoria di PyTorch e tempi di latenza in relazione al numero di core coinvolti

Figura 14. Allocazione di memoria di TensorFlow di Google e tempi di latenza in relazione al numero di core coinvolti

Figura 15. Allocazione di memoria di TensorFlow di Google con oneDNN abilitato e tempi di latenza in relazione al numero di core coinvolti

Figura 16. Allocazione di memoria di Intel TensorFlow e tempi di latenza in relazione al numero di core coinvolti

Come si può vedere dal grafico sopra, è possibile notare che l’allocatore della libreria standard (glibc) spesso è in ritardo in termini di prestazioni, ma fornisce prestazioni ragionevoli. L’allocatore jemalloc è talvolta il più veloce, ma solo in situazioni molto specifiche in cui la concorrenza non è così elevata. Ciò può essere spiegato dalla struttura sottostante che jemalloc utilizza internamente, che esula dall’ambito di questo blog, ma puoi leggere il blog di Facebook Engineering se vuoi saperne di più.

Infine, sembra che tcmalloc sia quello che offre le prestazioni migliori in generale per tutti i carichi di lavoro qui testati. Ancora una volta, tcmalloc ha un approccio diverso rispetto a Jemalloc nel modo in cui alloca le risorse, in particolare tcmalloc mantiene un pool di segmenti di memoria localmente per ogni thread, riducendo così la necessità di avere percorsi globali, esclusivi e critici.

Di nuovo, per ulteriori dettagli, ti invito a leggere l’intero blog del team di Google Abseil.

Ora, torniamo alla modalità grafica dove testiamo i framework che hanno una rappresentazione onnipotente del grafo di calcolo complessivo.

Figura 17. L’allocatore di memoria TorchScript e le latenze di scaling dei core

Figura 18. L’allocatore di memoria TensorFlow di Google e le latenze di scaling dei core

Figura 19. L’allocatore di memoria TensorFlow di Google con oneDNN abilitato e le latenze di scaling dei core

Figura 20. L’allocatore di memoria TensorFlow di Intel e le latenze di scaling dei core

Questa volta, conoscendo la struttura sottostante dei flussi degli operatori e delle forme delle matrici coinvolte, il framework può pianificare e riservare in anticipo le risorse necessarie. In questo contesto, come mostrato nel grafico sopra, la differenza tra i framework è molto piccola e non c’è un chiaro vincitore tra jemalloc e tcmalloc. Naturalmente, glibc è ancora leggermente indietro come allocatore di memoria generico, ma la differenza è meno significativa rispetto alla configurazione eager. Per riassumere, ottimizzare l’allocatore di memoria può offrire un interessante guadagno di qualche millisecondo alla fine del processo di ottimizzazione, soprattutto se si utilizzano già grafi di calcolo tracciati.

OpenMP

Nella sezione precedente abbiamo parlato della gestione della memoria all’interno del software di apprendimento automatico che coinvolge principalmente carichi di lavoro legati alla CPU. Tali software spesso si affidano a framework intermedi come PyTorch o TensorFlow per l’apprendimento profondo, che comunemente astraggono tutte le implementazioni sottostanti altamente parallelizzate degli operatori.

Scrivere algoritmi altamente paralleli e ottimizzati è una vera sfida ingegneristica e richiede una comprensione a basso livello di tutti gli elementi effettivi che entrano in gioco operati dalla CPU (sincronizzazione, cache di memoria, validità della cache, ecc.). In questo contesto, è molto importante essere in grado di sfruttare primitive per implementare tali algoritmi potenti, riducendo il tempo di consegna e il tempo di calcolo di molto rispetto all’implementazione di tutto da zero.

Esistono molte librerie disponibili che forniscono tali funzionalità di livello superiore per accelerare lo sviluppo di algoritmi. Tra le più comuni si possono citare OpenMP, Thread Building Blocks e direttamente dal C++ quando si punta a una versione recente dello standard. Nella parte successiva di questo post del blog, ci limiteremo a OpenMP e in particolare a confrontare l’implementazione basata sulla comunità e open source GNU con quella di Intel OpenMP. Quest’ultima si rivolge in particolare alle CPU Intel ed è ottimizzata per fornire prestazioni di prim’ordine quando viene utilizzata come sostituto del GNU OpenMP.

OpenMP espone molte variabili di ambiente per configurare automaticamente le risorse sottostanti che saranno coinvolte nei calcoli, come ad esempio il numero di thread da utilizzare per la distribuzione dei calcoli (thread intra-op), il modo in cui il pianificatore di sistema deve associare ciascuno di questi thread alle risorse della CPU (thread, core, socket) e altre variabili che offrono ulteriore controllo all’utente. Intel OpenMP espone più di queste variabili di ambiente per fornire all’utente ancora più flessibilità per regolare le prestazioni del proprio software.

Figura 21. Latenze di OpenMP vs Intel OpenMP eseguendo PyTorch

Figura 22. Latenze di OpenMP vs Intel OpenMP eseguendo PyTorch

Come già detto, ottimizzare OpenMP è qualcosa che puoi iniziare a modificare quando hai provato tutte le altre opzioni di ottimizzazione legate al sistema. Può portare un aumento finale alla velocità del tuo modello con una singola variabile di ambiente da impostare.

Inoltre, è importante notare che l’ottimizzazione della libreria OpenMP funzionerà solo all’interno del software che utilizza internamente l’API OpenMP. Più specificamente, ora solo PyTorch e TorchScript fanno davvero uso di OpenMP e quindi beneficiano dell’ottimizzazione del backend OpenMP.

Ciò spiega anche perché abbiamo riportato solo le latenze per questi due framework.

Ottimizzazione automatica delle prestazioni: ottimizzazione bayesiana con Intel SigOpt

Come già accennato, molte opzioni possono essere regolate per migliorare la latenza e la velocità di calcolo sulle CPU Intel, ma poiché ce ne sono molte, regolarle tutte per ottenere prestazioni ottimali può essere complicato. Ad esempio, nei nostri esperimenti, sono state regolate le seguenti opzioni:

  • Il numero di core: anche se utilizzare tutti i core disponibili è spesso una buona idea, non sempre offre le migliori prestazioni perché comporta anche una maggiore comunicazione tra i diversi thread. Inoltre, ottenere prestazioni migliori con meno core può essere molto utile in quanto consente di eseguire più istanze contemporaneamente, ottenendo sia una migliore latenza che una migliore velocità di calcolo.
  • L’allocatore di memoria: quale allocatore di memoria tra l’allocatore predefinito malloc, tcmalloc di Google e jemalloc di Facebook offre le migliori prestazioni?
  • La libreria di parallelismo: quale libreria di parallelismo tra GNU OpenMP e Intel OpenMP offre le migliori prestazioni?
  • Transparent Huge Pages: abilitare Transparent Huge Pages (THP) sul sistema offre migliori prestazioni?
  • Il parametro di tempo di blocco KMP: imposta il tempo, in millisecondi, che un thread dovrebbe attendere, dopo aver completato l’esecuzione di una regione parallela, prima di dormire.

Naturalmente, l’approccio brute force, consistente nel provare tutte le possibilità, fornirà i migliori valori di regolazione da utilizzare per ottenere prestazioni ottimali, ma, considerando che la dimensione dello spazio di ricerca è N x 3 x 2 x 2 x 2 = 24N, può richiedere molto tempo: su una macchina con 80 core fisici, ciò significa provare al massimo 24 x 80 = 1920 configurazioni diverse! 😱

Fortunatamente, SigOpt di Intel, attraverso l’ottimizzazione bayesiana, ci consente di rendere questi esperimenti di messa a punto più rapidi e convenienti da analizzare, pur fornendo prestazioni simili all’approccio brute force.

Quando analizziamo la differenza relativa tra la migliore latenza assoluta e ciò che SigOpt fornisce, osserviamo che sebbene spesso non sia altrettanto buono del brute force (tranne per lunghezza sequenza = 512 in quel caso specifico), fornisce prestazioni molto simili, con un divario massimo del 8,6% in questa figura.

Figura 23. Migliore latenza assoluta trovata dall’ottimizzazione automatica SigOpt rispetto al brute force Figura 24. Migliore latenza relativa trovata dall’ottimizzazione automatica SigOpt rispetto al brute force

SigOpt è anche molto utile per l’analisi: fornisce molte figure e informazioni preziose. Innanzitutto, fornisce il miglior valore che è stato in grado di trovare, le regolazioni corrispondenti e la cronologia degli esperimenti e come si è migliorato man mano che gli esperimenti procedevano, ad esempio, con lunghezza sequenza = 20:

Figura 25. Migliore valore riportato da SigOpt Figura 26. Migliore valore riportato da SigOpt

In questa configurazione specifica, 16 core insieme alle altre regolazioni sono stati in grado di fornire i migliori risultati, il che è molto importante da sapere, perché come accennato in precedenza, ciò significa che è possibile eseguire più istanze del modello in parallelo pur avendo la migliore latenza per ciascuna.

Mostra anche che si è convergito a circa 20 esperimenti, il che significa che forse 25 esperimenti invece di 40 sarebbero stati sufficienti. È disponibile una vasta gamma di altre informazioni preziose, come l’importanza dei parametri:

Come ci si aspettava, il numero di core è di gran lunga il parametro più importante, ma gli altri giocano anche un ruolo, ed è molto dipendente dall’esperimento. Ad esempio, per l’esperimento con lunghezza sequenza = 512, questa era l’importanza dei parametri:

Figura 27. Migliore valore SigOpt per Batch Size = 1, Lunghezza Sequenza = 20 Figura 28. Migliore valore SigOpt per Batch Size = 1, Lunghezza Sequenza = 512

In questo caso, l’impatto dell’utilizzo di OpenMP rispetto a Intel OpenMP era maggiore dell’impatto del selettore, l’importanza relativa di ciascuna regolazione è più equilibrata rispetto all’esperimento con lunghezza sequenza = 20. E moltissime altre figure, spesso interattive, sono disponibili su SigOpt, come:

  • Storia degli esperimenti in 2D, che consente di confrontare le regolazioni rispetto alle regolazioni o rispetto agli obiettivi
  • Storia degli esperimenti in 3D, che consente di fare la stessa cosa della storia degli esperimenti in 2D con un’altra regolazione/obiettivo.

Conclusioni – Accelerare i Transformers per la Produzione

In questo post, abbiamo mostrato come i nuovi processori Intel Ice Lake Xeon siano adatti per eseguire carichi di lavoro di intelligenza artificiale su larga scala insieme agli elementi software che è possibile sostituire e ottimizzare al fine di sfruttare appieno il potenziale dell’hardware. Tutti questi elementi devono essere presi in considerazione dopo aver impostato le varie regolazioni di livello inferiore descritte nel blog precedente per massimizzare l’utilizzo di tutti i core e le risorse.

Da Hugging Face, abbiamo una missione di democratizzare l’apprendimento automatico all’avanguardia e una parte fondamentale del nostro lavoro è rendere questi modelli all’avanguardia il più efficienti possibile, utilizzando meno energia e memoria su larga scala e rendendoli più accessibili alle aziende di tutte le dimensioni.

La nostra collaborazione con Intel attraverso il 🤗 Hardware Partner Program ci consente di rendere facilmente disponibili alla comunità avanzate tecniche di efficienza e ottimizzazione, attraverso la nostra nuova libreria open source 🤗 Optimum dedicata alle prestazioni di produzione.

Per le aziende che desiderano accelerare l’elaborazione dei loro modelli Transformer, il nostro nuovo prodotto 🤗 Infinity offre una soluzione pronta all’uso, raggiungendo una latenza fino a 1 ms su GPU e 2 ms su processori Intel Xeon Ice Lake.

Se hai trovato questo post interessante o utile per il tuo lavoro, ti preghiamo di considerare di dare una stella a Optimum. E se questo post è stato musica per le tue orecchie, prendi in considerazione di unirti al nostro team di ottimizzazione del Machine Learning!