Accelerazione del raffinamento distribuito di PyTorch con tecnologie Intel

'Accelerazione del raffinamento distribuito di PyTorch con Intel'

Nonostante le loro straordinarie prestazioni, i modelli di deep learning all’avanguardia richiedono spesso molto tempo per essere addestrati. Per accelerare i lavori di addestramento, i team di ingegneria si affidano all’addestramento distribuito, una tecnica divide-et-impera in cui i server raggruppati mantengono ciascuno una copia del modello, lo addestrano su un sottoinsieme del set di addestramento e scambiano i risultati per convergere a un modello finale.

Le unità di elaborazione grafica (GPU) sono da tempo la scelta de facto per addestrare modelli di deep learning. Tuttavia, l’avvento del transfer learning sta cambiando il gioco. I modelli raramente vengono addestrati da zero su enormi set di dati. Invece, vengono spesso raffinati su set di dati specifici (e più piccoli) per creare modelli specializzati che sono più accurati del modello di base per determinati compiti. Poiché questi lavori di addestramento sono molto più brevi, l’utilizzo di un cluster basato su CPU può rivelarsi un’opzione interessante che mantiene sotto controllo sia il tempo di addestramento che i costi.

Di cosa tratta questo post

In questo post, imparerai come accelerare i lavori di addestramento di PyTorch distribuendoli su un cluster di server CPU Intel Xeon Scalable, alimentati dall’architettura Ice Lake e che eseguono librerie software ottimizzate per le prestazioni. Costruiremo il cluster da zero utilizzando macchine virtuali e potrai facilmente replicare la demo sulla tua infrastruttura, sia in cloud che in locale.

Eseguendo un lavoro di classificazione del testo, affineremo un modello BERT sul dataset MRPC (uno dei compiti inclusi nel benchmark GLUE). Il dataset MRPC contiene 5.800 coppie di frasi estratte da fonti di notizie, con un’etichetta che ci indica se le due frasi in ogni coppia sono semanticamente equivalenti. Abbiamo scelto questo dataset per il suo tempo di addestramento ragionevole e testare altre attività GLUE è solo a una variabile di distanza.

Una volta che il cluster è pronto e in funzione, eseguiremo un lavoro di base su un singolo server. Successivamente, lo estenderemo a 2 server e 4 server e misureremo l’aumento di velocità.

Durante il processo, affronteremo i seguenti argomenti:

  • Elencare l’infrastruttura richiesta e i blocchi di costruzione del software,
  • Configurare il nostro cluster,
  • Installare le dipendenze,
  • Eseguire un lavoro su un singolo nodo,
  • Eseguire un lavoro distribuito.

Mettiamoci al lavoro!

Utilizzo dei server Intel

Per ottenere le migliori prestazioni, utilizzeremo server Intel basati sull’architettura Ice Lake, che supporta funzionalità hardware come Intel AVX-512 e Intel Vector Neural Network Instructions (VNNI). Queste funzionalità accelerano le operazioni tipicamente presenti nell’addestramento e nell’inferenza di deep learning. Puoi saperne di più in questa presentazione (PDF).

Tutti e tre i principali fornitori di servizi cloud offrono macchine virtuali alimentate da CPU Intel Ice Lake:

  • Amazon Web Services: istanze Amazon EC2 M6i e C6i.
  • Azure: macchine virtuali delle serie Dv5/Dsv5, Ddv5/Ddsv5 e Edv5/Edsv5.
  • Google Cloud Platform: macchine virtuali N2 Compute Engine.

Ovviamente, è possibile utilizzare anche i propri server. Se sono basati sull’architettura Cascade Lake (precedente all’Ice Lake), sono pronti per l’uso poiché Cascade Lake include anche AVX-512 e VNNI.

Utilizzo delle librerie di prestazioni Intel

Per sfruttare AVX-512 e VNNI in PyTorch, Intel ha progettato l’estensione Intel per PyTorch. Questa libreria software fornisce un miglioramento delle prestazioni immediato per l’addestramento e l’inferenza, quindi dovremmo installarla sicuramente.

Quando si tratta di addestramento distribuito, il principale collo di bottiglia delle prestazioni è spesso la rete. Infatti, i diversi nodi nel cluster devono scambiarsi periodicamente informazioni sullo stato del modello per rimanere sincronizzati. Poiché i modelli di trasformatori sono modelli di grandi dimensioni con miliardi di parametri (a volte anche di più), il volume di informazioni è significativo e le cose peggiorano man mano che aumenta il numero di nodi. Pertanto, è importante utilizzare una libreria di comunicazione ottimizzata per il deep learning.

In realtà, PyTorch include il pacchetto torch.distributed, che supporta diversi back-end di comunicazione. Qui, useremo la Intel oneAPI Collective Communications Library (oneCCL), un’implementazione efficiente di modelli di comunicazione utilizzati nel deep learning (all-reduce, ecc.). Puoi saperne di più sulle prestazioni di oneCCL rispetto ad altri back-end in questo post sul blog di PyTorch.

Ora che abbiamo chiarito i blocchi di costruzione, parliamo della configurazione generale del nostro cluster di addestramento.

Configurazione del nostro cluster

In questa demo, sto usando istanze Amazon EC2 che eseguono Amazon Linux 2 (c6i.16xlarge, 64 vCPU, 128GB di RAM, networking da 25Gbit/s). La configurazione sarà diversa in altri ambienti, ma i passaggi dovrebbero essere molto simili.

Si prega di tenere presente che sarà necessario disporre di 4 istanze identiche, quindi potrebbe essere opportuno pianificare un qualche tipo di automazione per evitare di eseguire la stessa configurazione 4 volte. Qui, configurerò manualmente un’istanza, creerò un’immagine di macchina Amazon (AMI) da questa istanza e utilizzerò questa AMI per avviare altre tre istanze identiche.

Dal punto di vista della rete, avremo la seguente configurazione:

  • Aprire la porta 22 per l’accesso ssh su tutte le istanze per la configurazione e il debug.
  • Configurare l’accesso ssh senza password tra l’istanza master (quella da cui si avvierà l’addestramento) e tutte le altre istanze (inclusa la master).
  • Aprire tutte le porte TCP su tutte le istanze per la comunicazione oneCCL all’interno del cluster. Assicurarsi di NON aprire queste porte al mondo esterno. AWS fornisce un modo comodo per farlo consentendo solo le connessioni dalle istanze che eseguono un determinato gruppo di sicurezza. Ecco come appare la mia configurazione.

Ora, procediamo con la configurazione manuale della prima istanza. Prima creo l’istanza stessa, collego il gruppo di sicurezza sopra e aggiungo 128GB di spazio di archiviazione. Per ottimizzare i costi, l’ho avviata come istanza Spot.

Una volta che l’istanza è attiva, mi connetto ad essa con ssh per installare le dipendenze.

Installazione delle dipendenze

Ecco i passaggi che seguirò:

  • Installare i toolkit Intel,
  • Installare la distribuzione Anaconda,
  • Creare un nuovo ambiente conda,
  • Installare PyTorch e l’estensione Intel per PyTorch,
  • Compilare e installare oneCCL,
  • Installare la libreria transformers.

Sembra molto, ma non c’è niente di complicato. Ecco a voi!

Installazione dei toolkit Intel

Prima di tutto, scarichiamo e installiamo il toolkit Intel OneAPI base e il toolkit AI. Puoi saperne di più sul sito web Intel.

wget https://registrationcenter-download.intel.com/akdlm/irc_nas/18236/l_BaseKit_p_2021.4.0.3422_offline.sh
sudo bash l_BaseKit_p_2021.4.0.3422_offline.sh

wget https://registrationcenter-download.intel.com/akdlm/irc_nas/18235/l_AIKit_p_2021.4.0.1460_offline.sh
sudo bash l_AIKit_p_2021.4.0.1460_offline.sh

Installazione di Anaconda

Successivamente, scarichiamo e installiamo la distribuzione Anaconda.

wget https://repo.anaconda.com/archive/Anaconda3-2021.05-Linux-x86_64.sh
sh Anaconda3-2021.05-Linux-x86_64.sh

Creazione di un nuovo ambiente conda

Disconnettiamoci e riconnettiamoci nuovamente per aggiornare i percorsi. Quindi, creiamo un nuovo ambiente conda per mantenere le cose ordinate.

yes | conda create -n transformer python=3.7.9 -c anaconda
eval "$(conda shell.bash hook)"
conda activate transformer
yes | conda install pip cmake

Installazione di PyTorch e dell’estensione Intel per PyTorch

Successivamente, installiamo PyTorch 1.9 e il toolkit di estensione Intel. Le versioni devono corrispondere.

yes | conda install pytorch==1.9.0 cpuonly -c pytorch
pip install torch_ipex==1.9.0 -f https://software.intel.com/ipex-whl-stable

Compilazione e installazione di oneCCL

Quindi, installiamo alcune dipendenze native necessarie per compilare oneCCL.

sudo yum -y update
sudo yum install -y git cmake3 gcc gcc-c++

In seguito, cloniamo il repository oneCCL, costruiamo la libreria e la installiamo. Di nuovo, le versioni devono coincidere.

source /opt/intel/oneapi/mkl/latest/env/vars.sh
git clone https://github.com/intel/torch-ccl.git
cd torch-ccl
git checkout ccl_torch1.9
git submodule sync
git submodule update --init --recursive
python setup.py install
cd ..

Installazione della libreria transformers

In seguito, installiamo la libreria transformers e le dipendenze necessarie per eseguire le attività GLUE.

pip install transformers datasets
yes | conda install scipy scikit-learn

Infine, cloniamo un fork del repository transformers che contiene l’esempio che eseguiremo.

git clone https://github.com/kding1/transformers.git
cd transformers
git checkout dist-sigopt

Siamo finiti! Eseguiamo un job su un singolo nodo.

Avvio di un job su un singolo nodo

Per ottenere una base di confronto, avviamo un job su un singolo nodo eseguendo lo script run_glue.py in transformers/examples/pytorch/text-classification. Questo dovrebbe funzionare su qualsiasi istanza ed è un buon controllo per verificare il corretto funzionamento prima di passare all’addestramento distribuito.

python run_glue.py \
--model_name_or_path bert-base-cased --task_name mrpc \
--do_train --do_eval --max_seq_length 128 \
--per_device_train_batch_size 32 --learning_rate 2e-5 --num_train_epochs 3 \
--output_dir /tmp/mrpc/ --overwrite_output_dir True

Questo job richiede 7 minuti e 46 secondi. Ora, configuriamo i job distribuiti con oneCCL e velocizziamo tutto!

Configurazione di un job distribuito con oneCCL

Sono necessari tre passaggi per eseguire un job di addestramento distribuito:

  • Elenca i nodi del cluster di addestramento,
  • Definisci le variabili d’ambiente,
  • Modifica lo script di addestramento.

Elenca i nodi del cluster di addestramento

Nell’istanza master, in transformers/examples/pytorch/text-classification, creiamo un file di testo chiamato hostfile. Questo file memorizza i nomi dei nodi nel cluster (gli indirizzi IP funzionano anche).

Ecco il mio file:

ip-172-31-28-17.ec2.internal
ip-172-31-30-87.ec2.internal
ip-172-31-29-11.ec2.internal
ip-172-31-20-77.ec2.internal

Definisci le variabili d’ambiente

In seguito, dobbiamo impostare alcune variabili d’ambiente sul nodo master, in particolare il suo indirizzo IP. Puoi trovare ulteriori informazioni sulle variabili di oneCCL nella documentazione.

for nic in eth0 eib0 hib0 enp94s0f0; do
  master_addr=$(ifconfig $nic 2>/dev/null | grep netmask | awk '{print $2}'| cut -f2 -d:)
  if [ "$master_addr" ]; then
    break
  fi
done
export MASTER_ADDR=$master_addr

source /home/ec2-user/anaconda3/envs/transformer/lib/python3.7/site-packages/torch_ccl-1.3.0+43f48a1-py3.7-linux-x86_64.egg/torch_ccl/env/setvars.sh

export LD_LIBRARY_PATH=/home/ec2-user/anaconda3/envs/transformer/lib/python3.7/site-packages/torch_ccl-1.3.0+43f48a1-py3.7-linux-x86_64.egg/:$LD_LIBRARY_PATH
export LD_PRELOAD="${CONDA_PREFIX}/lib/libtcmalloc.so:${CONDA_PREFIX}/lib/libiomp5.so"

export CCL_WORKER_COUNT=4
export CCL_WORKER_AFFINITY="0,1,2,3,32,33,34,35"
export CCL_ATL_TRANSPORT=ofi
export ATL_PROGRESS_MODE=0

Modificando lo script di addestramento

Le seguenti modifiche sono già state applicate al nostro script di addestramento (run_glue.py) per abilitare l’addestramento distribuito. Dovrai applicare modifiche simili quando utilizzi il tuo codice di addestramento.

  • Importa il pacchetto torch_ccl.
  • Ricevi l’indirizzo del nodo master e il rango locale del nodo nel cluster.
+import torch_ccl
+
 import datasets
 import numpy as np
 from datasets import load_dataset, load_metric
@@ -47,7 +49,7 @@ from transformers.utils.versions import require_version


 # Genererà un errore se la versione minima di Transformers non è installata. Rimuovi a tuo rischio.
-check_min_version("4.13.0.dev0")
+# check_min_version("4.13.0.dev0")

 require_version("datasets>=1.8.0", "Per risolvere: pip install -r examples/pytorch/text-classification/requirements.txt")

@@ -191,6 +193,17 @@ def main():
     # oppure passando l'opzione --help a questo script.
     # Ora manteniamo set di argomenti distinti, per una separazione più pulita delle responsabilità.

+    # aggiungi rango locale per cpu-dist
+    sys.argv.append("--local_rank")
+    sys.argv.append(str(os.environ.get("PMI_RANK", -1)))
+
+    # variabili di ambiente specifiche di ccl
+    if "ccl" in sys.argv:
+        os.environ["MASTER_ADDR"] = os.environ.get("MASTER_ADDR", "127.0.0.1")
+        os.environ["MASTER_PORT"] = "29500"
+        os.environ["RANK"] = str(os.environ.get("PMI_RANK", -1))
+        os.environ["WORLD_SIZE"] = str(os.environ.get("PMI_SIZE", -1))
+
     parser = HfArgumentParser((ModelArguments, DataTrainingArguments, TrainingArguments))
     if len(sys.argv) == 2 and sys.argv[1].endswith(".json"):

La configurazione è ora completa. Scaliamo il nostro lavoro di addestramento a 2 nodi e 4 nodi.

Esecuzione di un lavoro distribuito con oneCCL

Sul nodo master, uso mpirun per avviare un lavoro a 2 nodi: -np (numero di processi) è impostato su 2 e -ppn (processi per nodo) è impostato su 1. Quindi, i primi due nodi in hostfile saranno selezionati.

mpirun -f hostfile -np 2 -ppn 1 -genv I_MPI_PIN_DOMAIN=[0xfffffff0] \
-genv OMP_NUM_THREADS=28 python run_glue.py \
--model_name_or_path distilbert-base-uncased --task_name mrpc \
--do_train --do_eval --max_seq_length 128 --per_device_train_batch_size 32 \
--learning_rate 2e-5 --num_train_epochs 3 --output_dir /tmp/mrpc/ \
--overwrite_output_dir True --xpu_backend ccl --no_cuda True

In pochi secondi, un lavoro parte sui primi due nodi. Il lavoro viene completato in 4 minuti e 39 secondi, un aumento di velocità del 1,7x.

Impostando -np su 4 e avviando un nuovo lavoro, vedo ora un processo in esecuzione su ciascun nodo del cluster.

L’addestramento viene completato in 2 minuti e 36 secondi, un aumento di velocità del 3x.

Un’ultima cosa. Cambiando --task_name in qqp, ho eseguito anche il task GLUE delle coppie di domande Quora, che si basa su un dataset molto più grande (oltre 400.000 campioni di addestramento). I tempi di fine-tuning sono stati:

  • Singolo nodo: 11 ore e 22 minuti,
  • 2 nodi: 6 ore e 38 minuti (1,71x),
  • 4 nodi: 3 ore e 51 minuti (2,95x).

Sembra che l’aumento di velocità sia abbastanza consistente. Sentiti libero di continuare a sperimentare con diverse velocità di apprendimento, dimensioni di batch e impostazioni di oneCCL. Sono sicuro che puoi andare ancora più veloce!

Conclusion

In questo articolo, hai imparato come creare un cluster di training distribuito basato su CPU Intel e librerie di performance e come utilizzare questo cluster per velocizzare i lavori di fine-tuning. Infatti, il transfer learning sta riportando il training su CPU nel gioco e dovresti sicuramente considerarlo quando progetti e costruisci i tuoi prossimi workflow di deep learning.

Grazie per aver letto questo lungo articolo. Spero tu lo abbia trovato informativo. Feedback e domande sono benvenuti all’indirizzo [email protected] . Fino alla prossima volta, continua a imparare!

Julien