Come ho addestrato 10 TB per Stable Diffusion su SageMaker

Dec 03 2022
Con dettagli sul mio prossimo libro sulla formazione distribuita con AWS! Trattare con molti dati è difficile, non c'è dubbio. Tuttavia, con alcune scelte intelligenti e design semplici, anche tu puoi ridimensionare i tuoi progetti per replicarli e produrre risultati all'avanguardia.

Con dettagli sul mio prossimo libro sulla formazione distribuita con AWS!

Un'immagine generata, "Las Vegas sott'acqua come un cartone animato"

Trattare con molti dati è difficile, non c'è dubbio. Tuttavia, con alcune scelte intelligenti e design semplici, anche tu puoi ridimensionare i tuoi progetti per replicarli e produrre risultati all'avanguardia.

In questo post ti illustrerò un caso di studio in cui ho utilizzato AWS, in particolare SageMaker, S3 e FSx per Lustre, per scaricare, elaborare e addestrare un modello di diffusione stabile. Ho utilizzato SageMaker Distributed Data Parallel per ottimizzare la discesa del gradiente su larga scala. Ho anche utilizzato il parallelismo dei lavori sul backend modulare di SageMaker per scaricare ed elaborare tutti i file.

Il mio codice di addestramento si basa su un campione di Corey Barrett di AWS MLSL, disponibile qui. Proverò ad aggiornare quel repository con tutto il codice sorgente per questo progetto entro la fine dell'anno.

Se sei affamato di più contenuti sull'esecuzione di lavori su larga scala su SageMaker e sull'intero ambito di pre-addestramento di modelli di visione e linguaggio su AWS, dai un'occhiata al mio libro in uscita nell'aprile 2023!

Nel mio libro, trascorro 15 capitoli guidandoti attraverso l'intero ciclo di vita, dalla ricerca del caso d'uso giusto, alla preparazione del tuo ambiente, alla formazione, alla risoluzione dei problemi, alla valutazione, all'operatività e al ridimensionamento.

Il mio libro di formazione distribuito è l'ideale per i principianti ; è veramente per gli sviluppatori Python principianti e intermedi che vogliono acquisire le competenze per addestrare i propri modelli di visione e linguaggio di grandi dimensioni.

Immergiamoci!

1/ Scarica i file del parquet Laion-5B con i lavori SageMaker

Il set di dati di base utilizzato per addestrare la diffusione stabile è Laion-5B. Questo è un set di dati open source che fornisce miliardi di coppie di immagini/testo da Internet. Romain Beaumont, un contributore al set di dati Laion-5B, ha reso open source il suo strumento per scaricare ed elaborare questo set di dati e l'ho usato come base. Il suo codice sorgente è disponibile qui. Grazie Romaino!

Il set di dati di base è composto da due parti: più di 100 file parquet che puntano alle immagini originali e alle loro didascalie e alle immagini scaricate stesse.

Innanzitutto, ho scaricato i file parquet con una semplice funzione Python. Fortunatamente il toolkit di Romain ti consente di scaricare direttamente nel tuo bucket S3, il che ti consente di aggirare qualsiasi vincolo di archiviazione locale. L'ho provato alcune volte sulla mia istanza di SageMaker Studio, passando a una macchina più grande quando avevo bisogno di lavorare con un file più grande.

Successivamente, l'ho racchiuso in un singolo script che potevo eseguire sul mio lavoro SageMaker.

import argparse
import os

def parse_args():
    
    parser = argparse.ArgumentParser()    
    
    # points to your session bucket                 
    parser.add_argument("--bucket", type=str, default=os.environ["SM_HP_BUCKET"])
    
    args = parser.parse_args()
    
    return args

def download_parquet(bucket):
    cmd = '''for i in {00000..00127}; 
            do wget https://huggingface.co/datasets/laion/laion2B-en/resolve/main/part-$i-5114fd87-297e-42b0-9d11-50f1df323dfa-c000.snappy.parquet -O - | aws s3 cp - s3://{}/metadata/laion2B-en-joined/part-$i-4cfd6e30-f032-46ee-9105-8696034a8373-c000.snappy.parquet;  
        done'''.format(bucket)

    # runs the command
    os.system(cmd)

if __name__ == "__main__":
    
    args = parse_args()
        
    download_parquet(args.bucket)

Una vista del mio secchio S3 contenente le 128 lime per parquet

2/ Scarica le coppie di immagini e testo Laion-5B

Successivamente, è arrivato il momento di scaricare le immagini stesse. Ho fatto un elenco di tutti i file di parquet, l'ho esaminato e ho inviato ogni file di parquet al proprio lavoro SageMaker.

Il mio script per scaricare l'immagine e le coppie di testo era simile a questo:

from img2dataset import download
import shutil
import os
import multiprocessing
import threading
import argparse

def parse_args():
    
    parser = argparse.ArgumentParser()
        
    parser.add_argument("--cores", type=int, default=multiprocessing.cpu_count())

    parser.add_argument("--threads", type=int, default=threading.active_count())
    
    parser.add_argument("--parquet", type=str, default=os.environ["SM_CHANNEL_PARQUET"])
    
    parser.add_argument("--file_name", type=str, default=os.environ["SM_HP_FILE_NAME"])
            
    parser.add_argument("--bucket", type=str, default=os.environ["SM_MODULE_DIR"].split('/')[2])
        
    args = parser.parse_args()
    
    return args

def prep_system():
    
    args = parse_args()
    
    # send joint path and file name
    url_list = "{}/{}".format(args.parquet, args.file_name)
    
    part_number = args.file_name.split('-')[1]

    # point to output path in S3
    s3_output = "s3://{}/data/part-{}/".format(args.bucket, part_number)
    
    return args, url_list, s3_output

    
if __name__ == "__main__":
    
    args, url_list, s3_output = prep_system()
    
    download(
        processes_count=args.cores,
        thread_count=args.threads,
        # takes a single parquet file
        url_list=url_list,
        image_size=256,
        # copies to S3 directly, bypassing local disk
        output_folder=s3_output,
        # each image / caption pair is a tarball
        output_format="webdataset",
        input_format="parquet",
        url_col="URL",
        caption_col="TEXT",
        enable_wandb=False,
        number_sample_per_shard=1000,
        distributor="multiprocessing",
    )

Quindi, ho esaminato il mio elenco di file di parquet e ho inviato ogni file di parquet al proprio lavoro . Chiamiamo questo lavoro parallelismo , è un modo semplice ed efficace per eseguire lavori di calcolo paralleli.

def get_estimator(part_number, p_file, output_dir):
    
    hyperparameters = {"file_name": p_file}

    estimator = PyTorch(entry_point="download.py",
                          base_job_name="laion-part-{}".format(part_number),
                          role=role,
                          source_dir="scripts",
                          # configures the SageMaker training resource, you can increase as you need
                          instance_count=1,
                          instance_type="ml.c5.18xlarge",
                          py_version="py36",
                          framework_version = '1.8',
                          sagemaker_session=sagemaker_session,
                          volume_size = 250,
                          debugger_hook_config=False,
                          hyperparameters=hyperparameters,
                          output_path = output_dir)
    return estimator

for p_file in parquet_list[:18]:
    
    part_number = p_file.split('-')[1]

    output_dir = "s3://{}/data/part-{}/".format(bucket, part_number)

    if is_open(output_dir):

        est = get_estimator(part_number, p_file, output_dir)

        est.fit({"parquet":"s3://{}/meta/{}".format(bucket, p_file)}, wait=False)

L'ho eseguito su 18 lavori diversi, tutti in esecuzione contemporaneamente, per circa 24 ore.

Gli oggetti risultanti avevano questo aspetto:

Una vista del mio bucket S3 che contiene gli oggetti del set di dati Web; ogni file tar è composto da 999 coppie immagine/testo

3/ Creare FSx per il volume Lustre dal percorso S3

Successivamente, ho impiegato ben venti minuti per creare un nuovo volume FSx for Lustre dai miei percorsi S3. Questo è stato incredibilmente facile da fare; andata e ritorno compresi alcuni dei miei singhiozzi e il tempo di creazione del volume è stato di circa venti minuti. Il tutto senza lasciare la console AWS.

S3 ha una funzionalità davvero interessante per calcolare la dimensione totale di un percorso. L'ho usato per stimare la quantità totale di dati che stavo usando: un modesto 9,5 TB.

Una vista del mio bucket S3 dopo aver calcolato la dimensione totale: 9,5 TB

La mia configurazione di Lustre era così:

Il mio volume FSx for Lustre con provisioning con 12 TB di spazio

Perché approvvigionare Lustre? Perché riduce il tempo di download dei dati sul tuo lavoro di formazione SageMaker da, su questa scala probabilmente 90 minuti solidi o più, a meno di 60 secondi.

Ho passato un po' di tempo a configurare il mio VPC per abilitare la formazione su SageMaker e scrivere i risultati su S3. Ho anche esteso un AWS Deep Learning Container con tutti i miei pacchetti per risparmiare tempo durante la formazione; invece di pip installare tutto per ogni lavoro, lo faccio una volta durante la creazione dell'immagine, quindi semplicemente scarico quell'immagine sul mio lavoro in fase di esecuzione.

4/ Costruisci un indice di linee json

Dopo questo, ho perso alcune ore cercando di gestire la funzione load_dataset() predefinita con il mio formato webdataset. Sospetto ancora che sia possibile, ma ho deciso di scrivere solo il mio caricatore di dati.

Per risparmiare tempo e denaro sul mio enorme cluster GPU, ho creato un json lines index . Questo era semplicemente un file multi-GB con 50 milioni di oggetti json, ognuno dei quali puntava a una coppia immagine/testo valida su FSx per Lustre.

Ho eseguito un grande lavoro SageMaker basato su CPU per elencare tutti i file delle mie 18 parti su Lustre. Successivamente, ho caricato, testato e creato una funzione di caricamento dati molto piccola sul mio notebook Studio.

Ho utilizzato 96 CPU per circa 10 minuti su SageMaker Studio per creare il mio caricatore di dati

5/ Esegui su 192 GPU con la formazione distribuita di SageMaker

Dopodiché, era giunto il momento di superare un'unica epoca! Ho aumentato i miei limiti per le istanze ml.p4d.24xlarge sulla formazione SageMaker in us-east-1 utilizzando AWS Service Quota a 24. Poiché ogni istanza ha 8 GPU, si tratta di un totale di 192 GPU.

Ho eseguito alcune corse con solo l'1% dei miei dati complessivi per assicurarmi che il ciclo di allenamento funzionasse come previsto; la nuova funzionalità delle piscine calde è stata estremamente utile!

La mia configurazione di allenamento finale era così.

import sagemaker
from sagemaker.huggingface import HuggingFace

sess = sagemaker.Session()

role = sagemaker.get_execution_role()

version = 'v1'

image_uri = '<aws account id>.dkr.ecr.us-east-1.amazonaws.com/stable-diffusion:{}'.format(version )

# required in this version of the train script
data_channels['sd_base_model'] = 's3://dist-train/stable-diffusion/conceptual_captions/sd-base-model/'

hyperparameters={'pretrained_model_name_or_path':'/opt/ml/input/data/sd_base_model',
                'train_data_dir':'/opt/ml/input/data/training/laion-fsx',
                'index_name':'data_index.jsonl',
                'caption_column':'caption',
                'image_column':'image',
                'resolution':256,
                'mixed_precision':'fp16',
                # this is per device
                'train_batch_size':22,
                'learning_rate': '1e-10',
                # 'max_train_steps':1000000,
                'num_train_epochs':1,
                'output_dir':'/opt/ml/model/sd-output-final',  
                'n_rows':50000000}

est = HuggingFace(entry_point='finetune.py',
                  source_dir='src',
                  image_uri=image_uri,
                  sagemaker_session=sess,
                  role=role,
                  output_path="s3://laion-5b/output/model/", 
                  instance_type='ml.p4dn.24xlarge',
                  keep_alive_period_in_seconds = 60*60,
                  py_version='py38',
                  base_job_name='fsx-stable-diffusion', 
                  instance_count=24,
                  enable_network_isolation=True,
                  encrypt_inter_container_traffic = True,
                  # all opt/ml paths point to SageMaker training 
                  hyperparameters = hyperparameters,
                  distribution={"smdistributed": { "dataparallel": { "enabled": True } }},
                  max_retry_attempts = 30,
                  max_run = 4 * 60 * 60,
                  debugger_hook_config=False,
                  disable_profiler = True,
                  **kwargs)

est.fit(inputs=data_channels, wait=False)

Quello che mi ha sorpreso di più è stato che la differenza tra i miei lavori più grandi e quelli più piccoli, da 7 milioni a 50 milioni di coppie immagine/testo, era di soli 20 minuti circa. Entrambi i lavori hanno utilizzato 24 istanze ed entrambi hanno trascorso la maggior parte del tempo semplicemente inizializzando le GPU per il lavoro. Il download dell'immagine, la configurazione di MPI, il caricamento dei dati e la preparazione di NCCL hanno richiesto la maggior parte del tempo di esecuzione.

Il mio tempo per completare 1 epoca su 50 milioni di coppie immagine/testo con 200 GPU? 15 minuti!!

Esatto, 15 minuti. Dalle 18:30 alle 18:45, l'unico momento in cui le mie GPU sono state effettivamente utilizzate. Il lavoro complessivo ha richiesto circa un'ora per essere completato, anche in questo caso la maggior parte del tempo è stata dedicata all'inizializzazione, al provisioning, al caricamento e al caricamento del modello finito.

Un ciclo di formazione di 15 minuti su 200 GPU SageMaker

Per me, questo indica un'enorme opportunità nella progettazione e nell'ottimizzazione dei sistemi GPU. Dobbiamo accettare quella tassa di 40 minuti solo per addestrare i nostri modelli? Non c'è modo! Diventiamo creativi sulle soluzioni per risolvere questo problema.

Per essere chiari, non ho addestrato questo modello alla convergenza. Ho semplicemente testato le prestazioni su una singola epoca, che ho registrato a 15 minuti. Per allenarmi per 1000 epoche, metterei in preventivo 15.000 minuti, che dovrebbero richiedere poco più di 10 giorni.

7/ Distribuisci su hosting Amazon SageMaker

Successivamente, ho distribuito il modello base sull'hosting SageMaker. Come ci si potrebbe aspettare, un modello addestrato su una singola epoca non funzionerà molto bene, quindi ho semplicemente utilizzato la prima versione di Stable Diffusion nel mio account AWS.

Configurazione per ospitare Stable Diffusion su SageMaker

Ed ecco la mia immagine risultante!

Un'immagine generata dall'input: "un albero di Natale a Las Vegas"

E questo è un impacco! In questo post ho spiegato come ho lavorato con 10 TB di dati per addestrare una singola epoca di Stable Diffusion sulla formazione distribuita di SageMaker. Il mio ciclo di allenamento è stato completato in 15 minuti!

Spero di aver iniziato a stuzzicare il tuo appetito per ulteriori contenuti sulla preformazione di modelli di visione e linguaggio di grandi dimensioni. Sto scrivendo un libro su questo argomento , che sarà disponibile nell'aprile 2023. A gennaio potrai unirti alla mia cerchia degli autori, partecipare alla festa di lancio, incontrare altri esperti di dominio in quest'area e altro ancora!