
Benvenuti sulle montagne russe dell’ottimizzazione del machine learning! Questo post ti guiderà attraverso il mio processo per ottimizzare qualsiasi sistema ML per un addestramento e un’inferenza rapidissimi in 4 semplici passaggi.
Immagina questo: finalmente vieni inserito in un nuovo fantastico progetto ML in cui stai addestrando il tuo agente a contare quanti hot dog ci sono in una foto, il cui successo potrebbe far fruttare alla tua azienda decine di dollari!
Ottieni l’ultimo modello di rilevamento di oggetti hotshot implementato nel tuo framework preferito che ha molte stelle GitHub, esegui alcuni esempi di giocattoli e dopo circa un’ora individua gli hotdog come uno studente al verde al terzo anno di college ripetuto, la vita è bella.
I prossimi passi sono ovvi, vogliamo adattarlo ad alcuni problemi più difficili, questo significa più dati, un modello più grande e, naturalmente, tempi di addestramento più lunghi. Ora stai guardando i giorni di allenamento invece delle ore. Va bene però, stai ignorando il resto del tuo team da 3 settimane ormai e probabilmente dovresti dedicare una giornata a superare l’arretrato di revisioni del codice e di e-mail passivo-aggressive che si sono accumulate.
Torni il giorno dopo, dopo esserti sentito bene con i pignoli penetranti e assolutamente necessari che hai lasciato sui MR dei tuoi colleghi, solo per scoprire che le tue prestazioni sono crollate e crollate dopo un periodo di allenamento di 15 ore (il karma funziona velocemente).
I giorni che seguono si trasformano in un vortice di prove, prove ed esperimenti, e ogni potenziale idea impiega più di un giorno per essere realizzata. Questi iniziano rapidamente ad accumulare centinaia di dollari in costi di elaborazione, il che porta alla grande domanda: come possiamo renderlo più veloce ed economico?
Benvenuti sulle montagne russe emotive dell’ottimizzazione del machine learning! Ecco un semplice processo in 4 passaggi per volgere la situazione a tuo favore:
- Segno di riferimento
- Semplificare
- Ottimizzare
- Ripetere
Questo è un processo iterativo e ci saranno molte volte in cui ripeterai alcuni passaggi prima di passare a quello successivo, quindi è meno un sistema a 4 passaggi e più una cassetta degli attrezzi, ma 4 passaggi suonano meglio.
“Misura due volte, taglia una volta” — Qualcuno saggio.
La prima (e probabilmente la seconda) cosa che dovresti sempre fare è profilare il tuo sistema. Può trattarsi di qualcosa di semplice come cronometrare il tempo necessario per eseguire uno specifico blocco di codice o di qualcosa di complesso come eseguire una traccia completa del profilo. Ciò che conta è che tu disponga di informazioni sufficienti per identificare i colli di bottiglia nel tuo sistema. Eseguo più benchmark a seconda della fase in cui ci troviamo nel processo e in genere li divido in 2 tipi: benchmarking di alto livello e di basso livello.
Alto livello
Questo è il genere di cose che mostrerai al tuo capo al settimanale “Quanto siamo fottuti?” riunione e vorrei che queste metriche fossero parte di ogni corsa. Questi ti daranno un’idea di alto livello delle prestazioni del tuo sistema.
Lotti al secondo — quanto velocemente stiamo esaurendo ciascuno dei nostri lotti? questo dovrebbe essere il più alto possibile
Passi al secondo — (Specifico per RL) la velocità con cui attraversiamo il nostro ambiente per generare i nostri dati dovrebbe essere la più alta possibile. Ci sono alcune interazioni complicate tra il tempo del passo e i lotti di treno di cui non parlerò qui.
Utilizzo GPU — quanta parte della tua GPU viene utilizzata durante l’allenamento? Questo dovrebbe essere costantemente vicino al 100%, altrimenti hai un tempo di inattività che può essere ottimizzato.
Utilizzo CPU — quante CPU vengono utilizzate durante la formazione? Ancora una volta, questo dovrebbe essere il più vicino possibile al 100%.
FLOP – operazioni in virgola mobile al secondo, questo ti dà un’idea dell’efficacia con cui stai utilizzando l’hardware totale.
Basso livello
Utilizzando le metriche di cui sopra puoi quindi iniziare a guardare più in profondità dove potrebbe trovarsi il tuo collo di bottiglia. Una volta che hai questi, vuoi iniziare a guardare metriche e profilazioni più dettagliate.
Profilazione temporale — Questo è l’esperimento più semplice e spesso più utile da eseguire. Strumenti di profilazione come cprofiler può essere utilizzato per avere una visione d’insieme dei tempi di ciascuno dei componenti nel loro complesso o per osservare i tempi di componenti specifici.
Profilazione della memoria — Un altro punto fermo degli strumenti di ottimizzazione. I grandi sistemi richiedono molta memoria, quindi dobbiamo assicurarci di non sprecarne neanche una parte! strumenti come profilatore di memoria ti aiuterà a restringere il campo in cui il tuo sistema sta consumando la RAM.
Profilazione del modello — Strumenti come Tensorboard sono dotati di eccellenti strumenti di profilazione per osservare cosa sta divorando le tue prestazioni all’interno del tuo modello.
Profilazione di rete — Il carico di rete è un colpevole comune del collo di bottiglia del sistema. Ci sono strumenti come wireshark per aiutarti a profilarlo, ma a dire il vero non lo uso mai. Preferisco invece eseguire la profilazione temporale dei miei componenti e misurare il tempo totale impiegato all’interno del mio componente e quindi isolare quanto tempo proviene dall’I/O della rete stessa.
Assicurati di dare un’occhiata a questo fantastico articolo sulla profilazione in Python da RealPython per maggiori informazioni!
Una volta identificata un’area della tua profilazione che necessita di essere ottimizzata, semplificala. Taglia tutto il resto tranne quella parte. Continua a ridurre il sistema in parti più piccole finché non raggiungi il collo di bottiglia. Non aver paura di profilare mentre semplifichi, questo ti garantirà di andare nella giusta direzione mentre iteri. Continua a ripeterlo finché non trovi il collo di bottiglia.
Suggerimenti
- Sostituisci altri componenti con stub e funzioni fittizie che forniscono solo i dati previsti.
- Simula funzioni pesanti con
sleep
funzioni o calcoli fittizi. - Utilizzare dati fittizi per rimuovere il sovraccarico della generazione e dell’elaborazione dei dati.
- Inizia con le versioni locali a processo singolo del tuo sistema prima di passare a quelle distribuite.
- Simula più nodi e attori su una singola macchina per rimuovere il sovraccarico della rete.
- Trova la prestazione massima teorica per ciascuna parte del sistema. Se tutti gli altri colli di bottiglia nel sistema scomparissero, ad eccezione di questo componente, quali sarebbero le nostre prestazioni attese?
- Ancora profilo! Ogni volta che semplifichi il sistema, riesegui la tua profilazione.
Domande
Una volta che abbiamo individuato il collo di bottiglia, ci sono alcune domande chiave a cui vogliamo rispondere
Qual è la prestazione massima teorica di questo componente?
Se avessimo sufficientemente isolato la componente con collo di bottiglia, dovremmo essere in grado di rispondere a questa domanda.
Quanto siamo lontani dal massimo?
Questo divario di ottimalità ci informerà su quanto è ottimizzato il nostro sistema. Ora, potrebbe darsi che ci siano altri vincoli rigidi una volta che reintroduciamo il componente nel sistema e va bene, ma è fondamentale essere almeno consapevoli di quale sia il divario.
Esiste un collo di bottiglia più profondo?
Chiediti sempre questo, forse il problema è più profondo di quanto pensavi inizialmente, nel qual caso ripetiamo il processo di benchmarking e semplificazione.
Ok, quindi diciamo che abbiamo identificato il collo di bottiglia più grande, ora arriviamo alla parte divertente, come possiamo migliorare le cose? Di solito ci sono 3 aree che dovremmo considerare per possibili miglioramenti
- Calcolare
- Comunicazione
- Memoria
Calcolare
Per ridurre i colli di bottiglia computazionali dobbiamo cercare di essere quanto più efficienti possibile con i dati e gli algoritmi con cui lavoriamo. Questo è ovviamente specifico del progetto e c’è un’enorme quantità di cose che si possono fare, ma diamo un’occhiata ad alcune buone regole pratiche.
Parallelizzazione — assicurati di svolgere quanto più lavoro possibile in parallelo. Questa è la prima grande vittoria nella progettazione del tuo sistema che può avere un impatto significativo sulle prestazioni. Considera metodi come la vettorizzazione, il batching, il multithreading e il multiprocessing.
Memorizzazione nella cache — precalcola e riutilizza i calcoli dove puoi. Molti algoritmi possono trarre vantaggio dal riutilizzo dei valori precalcolati e dal salvataggio del calcolo critico per ciascuna fase di training.
Scarico – sappiamo tutti che Python non è noto per la sua velocità. Fortunatamente possiamo scaricare i calcoli critici su linguaggi di livello inferiore come C/C++.
Ridimensionamento dell’hardware — Questa è una specie di scappatoia, ma quando tutto il resto fallisce, possiamo sempre utilizzare più computer per risolvere il problema!
Comunicazione
Qualsiasi ingegnere esperto ti dirà che la comunicazione è la chiave per realizzare un progetto di successo e con questo intendiamo ovviamente la comunicazione all’interno del nostro sistema (Dio non voglia che dobbiamo mai parlare con i nostri colleghi). Alcune buone regole pratiche sono:
Nessun tempo morto — Tutto l’hardware disponibile deve essere utilizzato in ogni momento, altrimenti si lasciano sul tavolo i guadagni in termini di prestazioni. Ciò è solitamente dovuto a complicazioni e al sovraccarico della comunicazione nel sistema.
Rimani locale — Mantieni tutto su un’unica macchina il più a lungo possibile prima di passare a un sistema distribuito. Ciò mantiene il sistema semplice ed evita il sovraccarico di comunicazione di un sistema distribuito.
Asincrono > Sincronizza — Identificare tutto ciò che può essere fatto in modo asincrono, ciò contribuirà a ridurre i costi della comunicazione mantenendo il lavoro in movimento mentre i dati vengono spostati.
Evitare di spostare i dati — spostare i dati dalla CPU alla GPU o da un processo all’altro è costoso! Fatelo il meno possibile o riducetene l’impatto eseguendo l’operazione in modo asincrono.
Memoria
Ultima ma non meno importante è la memoria. Molte delle aree sopra menzionate possono essere utili per alleviare il collo di bottiglia, ma potrebbe non essere possibile se non hai memoria disponibile! Diamo un’occhiata ad alcune cose da considerare.
Tipi di dati — mantenerli il più piccoli possibile contribuendo a ridurre il costo della comunicazione e della memoria e, con i moderni acceleratori, ridurrà anche il calcolo.
Memorizzazione nella cache – simile alla riduzione dei calcoli, la memorizzazione nella cache intelligente può aiutarti a risparmiare memoria. Tuttavia, assicurati che i dati memorizzati nella cache vengano utilizzati con una frequenza sufficiente da giustificare la memorizzazione nella cache.
Pre-assegnare — non è qualcosa a cui siamo abituati in Python, ma essere severi con la pre-allocazione della memoria può significare che sai esattamente quanta memoria ti serve, riduce il rischio di frammentazione e se sei in grado di scrivere sulla memoria condivisa, ridurrai la comunicazione tra i tuoi processi!
Raccolta dei rifiuti — fortunatamente Python gestisce la maggior parte di questo per noi, ma è importante assicurarsi di non mantenere valori di grandi dimensioni nell’ambito senza averne bisogno o, peggio, con una dipendenza circolare che può causare una perdita di memoria.
Essere pigro — Valutare le espressioni solo quando necessario. In Python, puoi usare espressioni generator invece di list comprehension per operazioni che possono essere valutate pigramente.
Allora, quando avremo finito? Bene, questo dipende davvero dal tuo progetto, quali sono i requisiti e quanto tempo ci vuole prima che la tua sanità mentale in declino venga finalmente meno!
Man mano che rimuovi i colli di bottiglia, otterrai rendimenti decrescenti sul tempo e sugli sforzi che stai investendo per ottimizzare il tuo sistema. Durante il processo devi decidere quando buono è abbastanza buono. Ricorda, la velocità è un mezzo per raggiungere un fine, non farti prendere dalla trappola dell’ottimizzazione fine a se stessa. Se non avrà alcun impatto sugli utenti, probabilmente è ora di voltare pagina.
Costruire sistemi ML su larga scala è DIFFICILE. È come giocare a un gioco contorto di “Dov’è Waldo” incrociato con Dark Souls. Se riesci a trovare il problema devi fare più tentativi per risolverlo e finisci per passare la maggior parte del tuo tempo a prenderti a calci in culo, chiedendoti “Perché passo il venerdì sera a fare questo?”. Avere un approccio semplice e basato su principi può aiutarti a superare la battaglia finale con il boss e ad assaporare quei dolci, dolci FLOP massimi teorici.
Fonte: towardsdatascience.com