Perché esprimere calcoli come moltiplicazioni di matrici li rende più veloci?


18

Nel tutorial MNist di Google che utilizza TensorFlow , viene mostrato un calcolo in cui un passaggio equivale alla moltiplicazione di una matrice per un vettore. Google mostra innanzitutto un'immagine in cui ogni moltiplicazione numerica e aggiunta che andrebbero ad eseguire il calcolo sono scritte per intero. Successivamente, mostrano un'immagine in cui viene invece espressa come una moltiplicazione matriciale, sostenendo che questa versione del calcolo è, o almeno potrebbe essere, più veloce:

Se lo scriviamo come equazioni, otteniamo:

equazione scalare

Possiamo "vettorializzare" questa procedura, trasformandola in una moltiplicazione di matrice e un'aggiunta vettoriale. Questo è utile per l'efficienza computazionale. (È anche un modo utile di pensare.)

equazione vettoriale

So che equazioni di questo tipo sono di solito scritte nel formato di moltiplicazione di matrici dai professionisti dell'apprendimento automatico, e ovviamente possono vedere vantaggi nel farlo dal punto di vista della terseness del codice o della comprensione della matematica. Ciò che non capisco è l'affermazione di Google secondo cui la conversione dal modulo longhand al modulo matrice "è utile per l'efficienza computazionale"

Quando, perché e come sarebbe possibile ottenere miglioramenti delle prestazioni nel software esprimendo i calcoli come moltiplicazioni di matrice? Se dovessi calcolare personalmente la moltiplicazione di matrice nella seconda immagine (basata su matrice), come un essere umano, lo farei facendo ciascuno dei calcoli distinti mostrati nella prima immagine (scalare). Per me non sono altro che due notazioni per la stessa sequenza di calcoli. Perché è diverso per il mio computer? Perché un computer dovrebbe essere in grado di eseguire il calcolo della matrice più velocemente di quello scalare?

Risposte:


19

Ciò può sembrare ovvio, ma i computer non eseguono le formule , eseguono il codice e il tempo impiegato per l'esecuzione dipende direttamente dal codice che eseguono e solo indirettamente su qualsiasi concetto implementato dal codice. Due parti di codice logicamente identiche possono avere caratteristiche prestazionali molto diverse. Alcuni motivi che potrebbero sorgere nella moltiplicazione della matrice in particolare:

  • Utilizzando più thread. Non esiste quasi nessuna CPU moderna che non abbia più core, molti ne hanno fino a 8 e macchine specializzate per il calcolo ad alte prestazioni possono facilmente avere 64 su più socket. Scrivere il codice in modo ovvio, in un normale linguaggio di programmazione, ne usa solo uno . In altre parole, potrebbe utilizzare meno del 2% delle risorse di elaborazione disponibili della macchina su cui è in esecuzione.
  • Usando le istruzioni SIMD (confusamente, questo è anche chiamato "vettorializzazione" ma in un senso diverso rispetto alle citazioni di testo nella domanda). In sostanza, invece di 4 o 8 o così istruzioni aritmetiche scalari, alla CPU quella istruzione che esegue aritmetiche su 4 o 8 o così registri in parallelo. Questo può letteralmente fare alcuni calcoli (quando sono perfettamente indipendenti e adatti al set di istruzioni) 4 o 8 volte più velocemente.
  • Fare un uso più intelligente della cache . Gli accessi alla memoria sono più veloci se sono coerenti dal punto di vista temporale e spaziale , ovvero gli accessi consecutivi agli indirizzi vicini e quando si accede a un indirizzo due volte si accede ad esso due volte in rapida successione anziché con una lunga pausa.
  • Utilizzo di acceleratori come GPU. Questi dispositivi sono animali molto diversi dalle CPU e programmarli in modo efficiente è una forma d'arte a sé stante. Ad esempio, hanno centinaia di core, che sono raggruppati in gruppi di poche decine di core e questi gruppi condividono risorse: condividono alcuni KiB di memoria che è molto più veloce della memoria normale e quando un core del gruppo esegue un ifdichiarazione che tutti gli altri membri di quel gruppo devono aspettare.
  • Distribuisci il lavoro su più macchine (molto importante nei supercomputer!) Che introduce una serie enorme di nuovi mal di testa ma che, ovviamente, può dare accesso a risorse di calcolo notevolmente maggiori.
  • Algoritmi più intelligenti. Per la moltiplicazione delle matrici, il semplice algoritmo O (n ^ 3), correttamente ottimizzato con i trucchi sopra, è spesso più veloce di quelli subcubici per dimensioni di matrice ragionevoli, ma a volte vincono. Per casi speciali come matrici sparse, è possibile scrivere algoritmi specializzati.

Molte persone intelligenti hanno scritto un codice molto efficiente per le comuni operazioni di algebra lineare , usando i trucchi di cui sopra e molti altri e di solito anche con stupidi trucchi specifici della piattaforma. Pertanto, trasformare la formula in una moltiplicazione di matrice e quindi implementare quel calcolo chiamando in una libreria di algebra lineare matura beneficia di tale sforzo di ottimizzazione. Al contrario, se scrivi semplicemente la formula in modo ovvio in un linguaggio di alto livello, il codice macchina che alla fine viene generato non utilizzerà tutti quei trucchi e non sarà così veloce. Questo vale anche se prendi la formulazione della matrice e la implementi chiamando una ingenua routine di moltiplicazione della matrice che hai scritto tu stesso (di nuovo, in modo ovvio).

Rendere veloce il codice richiede lavoro e spesso molto lavoro se si desidera quell'ultima grammo di prestazioni. Poiché molti calcoli importanti possono essere espressi come combinazione di un paio di operazioni di algebra lineare, è economico creare un codice altamente ottimizzato per queste operazioni. Il tuo caso d'uso specifico una tantum, però? A nessuno importa di questo tranne te, quindi ottimizzarlo non è economico.


4

(sparsa) La moltiplicazione matrice-vettore è altamente parallelizzabile. Il che è molto utile se i tuoi dati sono grandi e hai una server farm a tua disposizione.

Ciò significa che puoi dividere la matrice e il vettore in blocchi e lasciare che macchine separate facciano parte del lavoro. Quindi condividere alcuni dei loro risultati tra loro e quindi ottenere il risultato finale.

Nel tuo esempio le operazioni sarebbero le seguenti

  1. imposta una griglia di processori ciascuno contenente un Wx, y secondo le loro coordinate nella griglia

  2. trasmettere il vettore sorgente lungo ciascuna colonna (costo O(log height) )

  3. avere ciascun processore alla moltiplicazione localmente (costo O(width of submatrix * heightof submatrix) )

  4. comprimi il risultato lungo ogni riga usando una somma (costo O(log width) )

Quest'ultima operazione è valida perché la somma è associativa.

Ciò consente anche di creare ridondanza e di evitare di dover inserire tutte le informazioni in un unico computer.

Per le piccole matrici 4x4 come si vede nella grafica è perché la CPU ha istruzioni speciali e registri per gestire tali operazioni.


Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.