Backend acceleratori

È piuttosto semplice descrivere un calcolo Tensor , ma quando e come viene eseguito tale calcolo dipenderà dal backend utilizzato per i Tensor e da quando i risultati sono necessari sulla CPU host.

Dietro le quinte, le operazioni sui Tensor vengono inviate ad acceleratori come GPU o TPU oppure vengono eseguite sulla CPU quando non è disponibile alcun acceleratore. Ciò avviene automaticamente e semplifica l'esecuzione di calcoli paralleli complessi utilizzando un'interfaccia di alto livello. Tuttavia, può essere utile capire come avviene questo invio ed essere in grado di personalizzarlo per ottenere prestazioni ottimali.

Swift per TensorFlow ha due backend per l'esecuzione di calcoli accelerati: la modalità desiderosa di TensorFlow e X10. Il backend predefinito è la modalità desiderosa di TensorFlow, ma può essere sovrascritta. È disponibile un tutorial interattivo che ti guida attraverso l'uso di questi diversi backend.

Modalità desiderosa di TensorFlow

Il backend in modalità entusiasta di TensorFlow sfrutta l'API TensorFlow C per inviare ogni operazione Tensor a una GPU o CPU non appena viene rilevata. Il risultato di tale operazione viene quindi recuperato e passato all'operazione successiva.

Questo invio operazione per operazione è semplice da comprendere e non richiede alcuna configurazione esplicita nel codice. Tuttavia, in molti casi non si ottengono prestazioni ottimali a causa del sovraccarico derivante dall'invio di molte piccole operazioni, combinato con la mancanza di fusione e ottimizzazione delle operazioni che può verificarsi quando sono presenti i grafici delle operazioni. Infine, la modalità desiderosa di TensorFlow non è compatibile con i TPU e può essere utilizzata solo con CPU e GPU.

X10 (tracciamento basato su XLA)

X10 è il nome del backend Swift per TensorFlow che utilizza il tracciamento pigro del tensore e il compilatore di ottimizzazione XLA per migliorare in molti casi in modo significativo le prestazioni rispetto all'invio operazione per operazione. Inoltre, aggiunge la compatibilità per i TPU , acceleratori specificatamente ottimizzati per i tipi di calcoli presenti nei modelli di machine learning.

L'uso di X10 per i calcoli Tensor non è l'impostazione predefinita, quindi è necessario attivare questo backend. Ciò avviene specificando che un Tensor è posizionato su un dispositivo XLA:

let tensor1 = Tensor<Float>([0.0, 1.0, 2.0], on: Device.defaultXLA)
let tensor2 = Tensor<Float>([1.5, 2.5, 3.5], on: Device.defaultXLA)

Dopodiché, la descrizione di un calcolo è esattamente la stessa della modalità desiderosa di TensorFlow:

let tensor3 = tensor1 + tensor2

Ulteriori dettagli possono essere forniti durante la creazione di un Tensor , ad esempio quale tipo di acceleratore utilizzare e anche quale, se ne sono disponibili diversi. Ad esempio, è possibile creare un Tensor sul secondo dispositivo TPU (supponendo che sia visibile all'host su cui è in esecuzione il programma) utilizzando quanto segue:

let tpuTensor = Tensor<Float>([0.0, 1.0, 2.0], on: Device(kind: .TPU, ordinal: 1, backend: .XLA))

Non viene eseguito alcun movimento implicito di Tensor tra i dispositivi, quindi se due Tensor su dispositivi diversi vengono utilizzati insieme in un'operazione, si verificherà un errore di runtime. Per copiare manualmente il contenuto di un Tensor su un nuovo dispositivo, è possibile utilizzare l'inizializzatore Tensor(copying:to:) . Alcune strutture su larga scala che contengono Tensor al loro interno, come modelli e ottimizzatori, hanno funzioni di supporto per spostare tutti i loro Tensor interni su un nuovo dispositivo in un unico passaggio.

A differenza della modalità desiderosa di TensorFlow, le operazioni che utilizzano il backend X10 non vengono inviate individualmente man mano che vengono rilevate. Invece, l'invio a un acceleratore viene attivato solo leggendo i valori calcolati all'host o posizionando una barriera esplicita. Il modo in cui funziona è che il runtime inizia dal valore letto all'host (o dall'ultimo calcolo prima di una barriera manuale) e traccia il grafico dei calcoli che danno come risultato quel valore.

Questo grafico tracciato viene quindi convertito nella rappresentazione intermedia XLA HLO e passato al compilatore XLA per essere ottimizzato e compilato per l'esecuzione sull'acceleratore. Da lì, l'intero calcolo viene inviato all'acceleratore e si ottiene il risultato finale.

Il calcolo è un processo che richiede tempo, quindi X10 è utilizzato al meglio con calcoli massivamente paralleli espressi tramite un grafico e eseguiti molte volte. I valori hash e la memorizzazione nella cache vengono utilizzati in modo che grafici identici vengano compilati solo una volta per ogni configurazione univoca.

Per i modelli di machine learning, il processo di addestramento spesso prevede un ciclo in cui il modello viene sottoposto più e più volte alla stessa serie di calcoli. Avrai bisogno che ciascuno di questi passaggi venga visto come una ripetizione della stessa traccia, piuttosto che come un lungo grafico con unità ripetute al suo interno. Ciò è abilitato dall'inserimento manuale di una chiamata alla funzione LazyTensorBarrier() nelle posizioni del codice in cui desideri che termini una traccia.

Supporto a precisione mista in X10

È supportato l'addestramento con precisione mista tramite X10 e per controllarlo vengono fornite API sia di basso che di alto livello. L' API di basso livello offre due proprietà calcolate: toReducedPrecision e toFullPrecision che convertono tra precisione completa e ridotta, insieme a isReducedPrecision per eseguire query sulla precisione. Oltre ai Tensor , i modelli e gli ottimizzatori possono essere convertiti dalla precisione completa a quella ridotta utilizzando questa API.

Tieni presente che la conversione a precisione ridotta non modifica il tipo logico di a Tensor . Se t è un Tensor<Float> , t.toReducedPrecision è anche un Tensor<Float> con una rappresentazione sottostante a precisione ridotta.

Come con i dispositivi, non sono consentite operazioni tra tensori di diversa precisione. Ciò evita la promozione silenziosa e indesiderata a float a 32 bit, che sarebbe difficile da rilevare da parte dell'utente.