In che modo le scelte di dataloading e checkpointing determinano l'utilizzo delle GPU, i costi di ripristino e la spesa di addestramento su larga scala, e le API di AI Runtime che consentono di gestirli al meglio.
Su larga scala, l'efficienza dell'addestramento è determinata da un'unica metrica: il "goodput", ovvero la percentuale di tempo che le GPU dedicano al calcolo produttivo anziché all'attesa o al ripristino dopo un guasto. Poiché i guasti delle GPU sono un evento previsto su larga scala, la capacità di ripristinare il sistema in modo rapido e automatico dopo un guasto è l'unico modo per mantenere un buon livello di goodput e gestire la spesa totale per le GPU.
Due sottosistemi determinano il successo o il fallimento di questo ripristino, eppure entrambi vengono regolarmente considerati come secondari: la pipeline di dati che alimenta gli acceleratori e il meccanismo di checkpointing che salva lo stato in modo che un job possa riprendere. Se si sbaglia anche solo uno di questi aspetti, ogni guasto costerà molto più tempo di inattività delle GPU del dovuto. Anche al di fuori degli scenari di guasto, una pipeline di dati che non riesce a tenere il passo con gli acceleratori lascerà le GPU inattive senza dare segnali evidenti, riducendo il goodput proprio come farebbe un crash. Esamineremo i meccanismi e i compromessi di entrambi, e come ciascuno di essi influisca sul goodput e sulla spesa totale per le GPU. Consulta la Guida alle prestazioni e alla resilienza dell'addestramento per indicazioni sul codice ed esempi.
Per l'aspetto legato all'infrastruttura dello stesso problema, ovvero come una flotta rileva e isola le GPU non integre prima che interrompano un job, consulta il post correlato, Come garantiamo l'affidabilità delle GPU in Databricks AI.
All'aumentare del numero di GPU in un job, la probabilità che questo completi l'intera durata senza interruzioni diminuisce rapidamente. Un utile modello approssimativo descritto nel post correlato di Databricks ipotizza che ogni GPU abbia un tasso di guasto annualizzato di circa l'1%. In base a questa ipotesi, il post rileva che "un job con 256 GPU in esecuzione per 30 giorni ha circa il 19% di probabilità di subire un guasto. Con 1.024 GPU, questa percentuale sale al 57%." e questi sono solo problemi a livello di infrastruttura.
Per calare questa stima nella realtà, il supercomputer delta con 608 GPU H100 ha registrato guasti ogni 1,9 ore; ciò significa che per un job con 32 GPU, il tempo medio prima di un guasto sarebbe di 36 ore. La conclusione principale è che il job di addestramento probabilmente subirà un guasto a un certo punto, e prendere le decisioni corrette può rendere il modello resiliente e ridurre il tempo totale perso quando ciò accade.
La resilienza si decide tutta nel checkpointing, e il meccanismo scelto ha un effetto primario sulla frequenza con cui è possibile salvare. Questa è la leva principale per il tuo goodput: se esegui un checkpoint una volta al giorno, un guasto richiederà di ripetere in media 12 ore di lavoro duplicato per ripristinare lo stato in cui si trovava il sistema al momento del guasto.
Il primo checkpoint che la maggior parte dei team scrive è un semplice torch.save sul rank 0. A seconda di come viene addestrato il modello, possono presentarsi due problemi:

Questo comportamento di blocco lascia le GPU inattive, riducendo il goodput. Esiste tuttavia un modo per ridurre il tempo dedicato al checkpointing dalle GPU: l'API di checkpoint distribuito di Torch.
Il checkpoint distribuito di PyTorch inverte questo approccio. Ogni rank scrive il proprio shard distinto in parallelo, insieme a un piccolo file .metadata che descrive come gli shard si compongono nei tensori completi.

Il tempo di salvataggio diminuisce all'incirca come 1/N con il numero di rank e, poiché il file .metadata registra il layout globale, lo stesso checkpoint può essere ricaricato su un numero diverso di GPU. DCP pianifica nuovamente i byte necessari per ogni nuovo rank, quindi il ripristino su un cluster a capacità ridotta dopo la perdita di nodi funziona senza problemi.
Spesso si pensa che DCP sia utile solo per i modelli con partizionamento (sharding), e che un job con parallelismo dei dati (DDP), in cui ogni rank contiene una replica identica dei pesi, non tragga alcun vantaggio. Non è così: DCP suddivide in shard lo stato del modello e lo scrive in parallelo su ciascun worker, anche per i task di addestramento DDP.
Inoltre, è la stessa API di cui avrai bisogno quando passerai a FSDP o al parallelismo dei tensor; adottarla fin da subito significa non dover mai riscrivere il codice di resilienza nel momento meno opportuno.
Anche con le scritture parallele, un salvataggio sincrono blocca l'addestramento finché i byte non sono memorizzati in modo permanente nello storage; per un checkpoint di grandi dimensioni su un volume remoto, questo comporta decine di secondi di inattività dell'acceleratore. async_save suddivide l'operazione: una copia rapida in un buffer di staging, seguita da un caricamento in background che si sovrappone alla continuazione dell'addestramento.

Il ciclo di addestramento paga solo per la copia di staging, non per il caricamento. Un checkpoint che prima costava decine di secondi di inattività ora non costa quasi nulla, ed è proprio questo che rende sostenibile il checkpointing frequente descritto nella sezione successiva.
Su AI Runtime, UCVolumeWriter e UCVolumeReader implementano DCP sui volumi UC, eseguendo lo staging dell'I/O tramite NVMe locale e contrassegnando un checkpoint come completato solo quando i relativi dati sono stati interamente scritti. Consulta la guida alle prestazioni e alla resilienza per tutti i dettagli e gli esempi di codice.
| Job di addestramento | Risparmio di async_save rispetto a torch.save |
|---|---|
| LLM DDP con 2,8 miliardi di parametri su 32xH100 | 1,8x (36 s contro 66 s) |
| LLM FSPD con 20 miliardi di parametri su 32xH100 | 58x (522 s contro 9 s) |
I dati sopra riportati escludono il tempo di storage di rete per torch.save.
È qui che i vari elementi si sommano. Quando un job fallisce, perde tutto ciò che è stato fatto dall'ultimo checkpoint valido e deve ricalcolarlo. Pertanto, il lavoro sprecato previsto per guasto è pari a circa la metà dell'intervallo di checkpoint, e i salvataggi asincroni economici consentono di ridurre tale intervallo.
Ridurre l'intervallo di un fattore pari a 10 riduce il tempo di ripristino previsto dello stesso fattore. Ricorda il dato di Llama 3 di circa 8,6 interruzioni al giorno: con questo tasso di guasto, eseguire un checkpoint ogni 2 ore significa prevedere di sprecare 8,6 ore al giorno in riaddestramento, con un goodput del 64%. Eseguendo il checkpoint ogni 30 minuti, si perdono solo 2,15 ore, con un goodput del 91%.
Inoltre, il ripristino deve essere automatico. Al riavvio, il job deve trovare il checkpoint più recente completato con successo, saltando quelli rimasti scritti a metà a causa del crash, e riprendere da lì senza alcun intervento umano. DCP rende questo processo affidabile: il file .metadata viene scritto solo dopo che tutti gli shard sono stati salvati, quindi la sua presenza è un indicatore affidabile del tipo "questo salvataggio è completo" su cui basare la selezione.

Un job di addestramento procede alla velocità del suo input più lento. Quando gli acceleratori attendono il batch successivo, il goodput si riduce poiché le GPU rimangono semplicemente inattive. L'unico modo per risolvere questo problema è garantire che la pipeline di input sovrapponga la preparazione dei dati per lo step successivo al calcolo di quello corrente, come mostrato nella figura seguente:

Spesso notiamo che i clienti che passano alla sovrapposizione del caricamento dei dati con il calcolo registrano una riduzione del 20-50% del tempo effettivo.
Su una piattaforma gestita, i dati di addestramento risiedono in uno storage di oggetti remoto. Su AI Runtime, i volumi di Unity Catalog (UC) vengono presentati come montaggi di rete.
Leggere i file direttamente da quel montaggio a ogni accesso vincola il tempo di step alla latenza di rete e scarica nuovamente gli stessi file a ogni epoca. La soluzione è un dataloader che copia ciascun file in uno storage locale rapido al primo accesso, gestisce le letture successive da quella cache locale e recupera i file successivi in parallelo mentre la GPU esegue i calcoli.

Con AI Runtime, UCVolumeDataset e DataLoader fanno esattamente questo (vedi la guida per esempi di codice). UCVolumeDataset distribuisce in streaming i file da un volume UC, memorizzando ciascuno di essi nella cache NVMe locale al primo accesso, e partiziona i file tra rank e worker in modo che ogni acceleratore riceva una porzione disgiunta e non sovrapposta. Il nostro DataLoader è una sottoclasse drop-in del DataLoader di PyTorch i cui valori predefiniti sono ottimizzati per questo percorso, in modo che i file vengano recuperati e memorizzati nella cache contemporaneamente mentre la GPU elabora, anziché uno alla volta sul thread di addestramento.
Consideriamo un semplice carico di lavoro di classificazione delle immagini: decodificare i file JPEG da un volume UC, applicare l'augmentation e addestrare un modello di computer vision. Vediamo due modi per farlo sulla stessa GPU, con lo stesso modello e la stessa dimensione del batch: il Dataset standard di PyTorch che legge da un volume UC rispetto a UCVolumeDataset combinato con i valori predefiniti del DataLoader di Databricks.
| Metrica (per GPU, a regime) | DataLoader standard di PyTorch, lettura diretta da UC | UCVolumeDataset + DataLoader di Databricks |
|---|---|---|
| Throughput Epoca 1 (immagini/sec) | 57.2 | 417 |
| Throughput Epoca 2 (immagini/sec) | 371.6 | 6590 |
| Utilizzo della GPU (%) | 12.6% | 53.3% |
Durante lo sviluppo di DataLoader, ci siamo assicurati che registri le sue metriche su MLflow, rendendo facile capire a colpo d'occhio se la pipeline di dati sta bloccando l'addestramento.

La metrica fetch_seconds misura esplicitamente quanto tempo impiega il dataloader a produrre un batch; durante questo tempo la GPU rimane inattiva.
Esiste un ultimo bug di resilienza che non produce alcun messaggio di errore, nessun crash e nessun job non riuscito, ma solo un modello leggermente peggiore di quanto dovrebbe essere. Si verifica quando salvi un checkpoint del modello, dell'ottimizzatore e dello step, ma non della posizione della pipeline di dati all'interno del dataset.
Consideriamo un job interrotto a metà di un'epoca. Ripristina correttamente il modello e riprende il loop di addestramento, ma il dataloader ricomincia dall'inizio del dataset.
Il job ripreso esegue nuovamente l'addestramento su esempi già visti in questa epoca e potenzialmente salta quelli che non aveva ancora raggiunto. Tra i molti riavvii che diventano routine su larga scala, questo introduce silenziosamente un bias nella distribuzione dei dati. Il modello continua ad addestrarsi, ma lo fa sul campionamento errato dei dati: proprio il tipo di errore silenzioso più costoso, perché il job viene completato e nessuno nota il problema finché le metriche non si rivelano deludenti.
La soluzione consiste nel trattare la posizione dei dati come parte del checkpoint. A seconda della pipeline, ciò significa tracciare l'offset di un campione o di uno shard e saltare in avanti al riavvio, fare in modo che un dataset personalizzato serializzi la propria posizione o salvare i checkpoint ai limiti dell'epoca. Tutto questo si basa su un unico prerequisito: il determinismo. Lo shuffling e l'augmentation attingono da generatori di numeri casuali, quindi anche i seed e gli stati RNG devono far parte del checkpoint, altrimenti l'ordine dei dati dopo un riavvio non corrisponderà a quello precedente e una posizione salvata punterà ai campioni errati.
Seed, ordine riproducibile e pipeline di dati ripristinabile sono tre espressioni di un'unica idea. La guida illustra ciascuna strategia con esempi di codice.
Un addestramento rapido e tollerante ai guasti deriva da una serie di decisioni che si sommano:
torch.save, anche per DDP, in modo che i salvataggi siano paralleli ed economici anziché un collo di bottiglia seriale.Il principio unificatore: checkpoint frequenti, economici e completi trasformano un guasto hardware da un evento che interrompe il job a un semplice errore di arrotondamento, e una pipeline di input sovrapposta mantiene gli acceleratori occupati nel frattempo. I salvataggi economici (asincroni) rendono sostenibile la frequenza; i salvataggi completi (modello, dati e RNG) garantiscono la correttezza del ripristino. Con entrambi questi elementi attivi e una flotta che rileva e isola l'hardware guasto, il tempo di addestramento effettivo si avvicina al limite massimo consentito dall'hardware, indipendentemente da quanto sia instabile il cluster sottostante.
Vuoi provarlo? Consulta la Guida alle prestazioni e alla resilienza dell'addestramento nella documentazione di Databricks AI Runtime per il codice completo, e leggi How we keep GPUs reliable across Databricks AI per scoprire l'aspetto legato all'infrastruttura.
(Questo post sul blog è stato tradotto utilizzando strumenti basati sull'intelligenza artificiale) Post originale
Iscriviti al nostro blog e ricevi gli ultimi articoli direttamente nella tua casella di posta.