Processore vettoriale - Vector processor

In informatica , un processore vettoriale o un processore array è un'unità di elaborazione centrale (CPU) che implementa un set di istruzioni in cui le sue istruzioni sono progettate per operare in modo efficiente ed efficace su grandi array unidimensionali di dati chiamati vettori . Ciò è in contrasto con i processori scalari , le cui istruzioni operano solo su singoli elementi di dati, e in contrasto con alcuni di quegli stessi processori scalari che hanno unità aritmetiche SIMD o SWAR aggiuntive . I processori vettoriali possono migliorare notevolmente le prestazioni su determinati carichi di lavoro, in particolare la simulazione numerica e attività simili. Le tecniche di elaborazione vettoriale operano anche nell'hardware delle console per videogiochi e negli acceleratori grafici .

Le macchine vettoriali sono apparse nei primi anni '70 e hanno dominato il design dei supercomputer dagli anni '70 agli anni '90, in particolare le varie piattaforme Cray . La rapida caduta del rapporto prezzo-prestazioni dei progetti di microprocessori convenzionali ha portato alla fine del supercomputer vettoriale alla fine degli anni '90.

Storia

Primi lavori

Lo sviluppo dell'elaborazione vettoriale è iniziato nei primi anni '60 presso Westinghouse nel loro progetto "Solomon". L'obiettivo di Solomon era aumentare drasticamente le prestazioni matematiche utilizzando un gran numero di semplici coprocessori matematici sotto il controllo di una singola CPU master . La CPU ha fornito un'unica istruzione comune a tutte le unità logiche aritmetiche (ALU), una per ciclo, ma con un punto dati diverso su cui lavorare ciascuna. Ciò ha permesso alla macchina Solomon di applicare un singolo algoritmo a un grande set di dati , alimentato sotto forma di array.

Nel 1962, Westinghouse annullò il progetto, ma lo sforzo fu riavviato presso l' Università dell'Illinois come ILLIAC IV . La loro versione del progetto originariamente prevedeva una macchina da 1 GFLOPS con 256 ALU, ma, quando fu finalmente consegnata nel 1972, aveva solo 64 ALU e poteva raggiungere solo da 100 a 150 MFLOPS. Tuttavia, ha dimostrato che il concetto di base era valido e, quando utilizzato su applicazioni ad alta intensità di dati, come la fluidodinamica computazionale , l'ILLIAC era la macchina più veloce del mondo. L'approccio ILLIAC dell'utilizzo di ALU separate per ciascun elemento di dati non è comune ai progetti successivi e viene spesso indicato in una categoria separata, calcolo massicciamente parallelo . In questo periodo Flynn ha classificato questo tipo di elaborazione come una prima forma di SIMT .

Un computer per operazioni con funzioni è stato presentato e sviluppato da Kartsev nel 1967.

Supercomputer

La prima implementazione di successo dell'elaborazione vettoriale avvenne nel 1966, quando furono introdotti sia il Control Data Corporation STAR-100 che il Texas Instruments Advanced Scientific Computer (ASC).

L'ASC di base (cioè "one pipe") ALU utilizzava un'architettura pipeline che supportava calcoli sia scalari che vettoriali, con prestazioni di picco che raggiungevano circa 20 MFLOPS, facilmente ottenibili durante l'elaborazione di vettori lunghi. Le configurazioni ALU espanse supportavano "due tubi" o "quattro tubi" con un corrispondente guadagno di prestazioni 2X o 4X. La larghezza di banda della memoria era sufficiente per supportare queste modalità espanse.

Lo STAR-100 era altrimenti più lento dei supercomputer di CDC come il CDC 7600 , ma nelle attività relative ai dati potevano tenere il passo pur essendo molto più piccoli e meno costosi. Tuttavia, la macchina ha anche impiegato molto tempo per decodificare le istruzioni vettoriali e prepararsi per eseguire il processo, quindi ha richiesto set di dati molto specifici su cui lavorare prima di accelerare effettivamente qualcosa.

La tecnica vettoriale è stata sfruttata appieno per la prima volta nel 1976 dal famoso Cray-1 . Invece di lasciare i dati in memoria come lo STAR-100 e l'ASC, il progetto Cray aveva otto registri vettoriali , che contenevano sessantaquattro parole a 64 bit ciascuno. Le istruzioni vettoriali sono state applicate tra i registri, il che è molto più veloce che parlare con la memoria principale. Mentre lo STAR-100 applicherebbe una singola operazione su un lungo vettore in memoria e poi passerebbe all'operazione successiva, il progetto Cray caricherebbe una sezione più piccola del vettore nei registri e quindi applicherebbe il maggior numero di operazioni possibile a quei dati , evitando così molte delle operazioni di accesso alla memoria molto più lente.

Il progetto Cray utilizzava il parallelismo della pipeline per implementare istruzioni vettoriali anziché più ALU. Inoltre, il progetto prevedeva pipeline completamente separate per istruzioni diverse, ad esempio l'addizione/sottrazione era implementata in hardware diverso rispetto alla moltiplicazione. Ciò ha permesso di convogliare una serie di istruzioni vettoriali in ciascuna delle subunità ALU, una tecnica chiamata concatenamento vettoriale . Il Cray-1 normalmente aveva una prestazione di circa 80 MFLOPS, ma con un massimo di tre catene in funzione poteva raggiungere un picco di 240 MFLOPS e una media di circa 150, molto più veloce di qualsiasi macchina dell'epoca.

Image
Modulo processore Cray J90 con quattro processori scalari/vettoriali

Seguirono altri esempi. Control Data Corporation ha cercato di rientrare di nuovo nel mercato di fascia alta con la sua macchina ETA-10 , ma ha venduto poco e hanno colto l'occasione come un'opportunità per lasciare completamente il campo del supercalcolo. Nei primi e metà degli anni 1980 le aziende giapponesi ( Fujitsu , Hitachi e Nippon Electric Corporation (NEC) vector machines basati sui registri introdotti simili al Cray-1, in genere essendo leggermente più veloce e molto più piccolo. Oregon a base di Floating Point Systems (FPS) costruito processori array aggiuntivi per minicomputer , in seguito costruendo i propri minisupercomputer .

Per tutto il tempo, Cray ha continuato a essere il leader delle prestazioni, battendo continuamente la concorrenza con una serie di macchine che hanno portato a Cray-2 , Cray X-MP e Cray Y-MP . Da allora, il mercato dei supercomputer si è concentrato molto di più sull'elaborazione massicciamente parallela piuttosto che su migliori implementazioni di processori vettoriali. Tuttavia, riconoscendo i vantaggi dell'elaborazione vettoriale, IBM ha sviluppato Virtual Vector Architecture da utilizzare nei supercomputer che accoppiano diversi processori scalari per agire come processore vettoriale.

Sebbene i supercomputer vettoriali simili al Cray-1 siano meno popolari in questi giorni, NEC ha continuato a realizzare questo tipo di computer fino ai giorni nostri con la serie di computer SX . Più di recente, l' SX-Aurora TSUBASA colloca il processore e 24 o 48 gigabyte di memoria su un modulo HBM 2 all'interno di una scheda che assomiglia fisicamente a un coprocessore grafico, ma invece di fungere da coprocessore, è il computer principale con il computer compatibile con il PC in cui è collegato serve funzioni di supporto.

Un esempio estremo e raro di un processore di array è stato l'Aspex Microelectronics ASP, che si è classificato come "Massive wide SIMD" ma aveva ALU a livello di bit e predicazione a livello di bit, e quindi potrebbe essere definitivamente considerato un processore di array (vettoriale). Il Linedancer, rilasciato nel 2010, conteneva 4096 ALU SIMD predicate a 2 bit, ciascuna con la propria Content-Addressable Memory , ed era in grado di eseguire 800 miliardi di istruzioni al secondo. È interessante notare che, secondo la tassonomia di Flynn, l'ASP era sia un processore associativo che un processore di array.

GPU

Le moderne unità di elaborazione grafica ( GPU ) includono una serie di pipeline di shader che possono essere guidate da kernel di elaborazione e possono essere considerate processori vettoriali (utilizzando una strategia simile per nascondere le latenze di memoria). Come mostrato nell'articolo di Flynn del 1972, il fattore distintivo chiave delle GPU basate su SIMT è che ha un'unica istruzione decodificatore-trasmettitore, ma che i core che ricevono ed eseguono la stessa istruzione sono altrimenti ragionevolmente normali: le proprie ALU, i propri file di registro, i loro proprie unità Load/Store e le proprie cache di dati L1 indipendenti. Quindi, sebbene tutti i core eseguano simultaneamente la stessa identica istruzione in blocco l'uno con l'altro, lo fanno con dati completamente diversi da posizioni di memoria completamente diverse. Questo è significativamente più complesso e complesso di "Packed SIMD" , che è strettamente limitato all'esecuzione di sole operazioni aritmetiche pipeline parallele. Sebbene gli esatti dettagli interni delle odierne GPU commerciali siano segreti proprietari, il team MIAOW è stato in grado di mettere insieme informazioni aneddotiche sufficienti per implementare un sottoinsieme dell'architettura AMDGPU.

Confronto con le architetture moderne

A partire dal 2016 la maggior parte delle CPU commerciali implementa architetture che dispongono di istruzioni SIMD a lunghezza fissa . A prima vista questi possono essere considerati una forma di elaborazione vettoriale perché operano su insiemi di dati multipli (vettorizzati, di lunghezza esplicita) e prendono in prestito funzionalità dai processori vettoriali. Tuttavia, per definizione, l'aggiunta di SIMD non può di per sé qualificare un processore come un vero processore vettoriale perché SIMD è a lunghezza fissa e i vettori sono variabili. La differenza è illustrata di seguito con esempi, che mostrano e confrontano le tre categorie: Pure SIMD, Predicated SIMD e Pure Vector Processing.

Altri disegni CPU includono alcune istruzioni multiple per l'elaborazione vettoriale su diversi (Vectorised) insiemi di dati, tipicamente noti come MIMD ( M ultiple I nstruction, M ultiple D ata) e realizzate con VLIW ( V ery L ong I nstruction W ord). Il processore Fujitsu FR-V VLIW/ vector combina entrambe le tecnologie.

Differenza tra SIMD e processore vettoriale.

I set di istruzioni SIMD mancano di funzionalità cruciali rispetto ai set di istruzioni del processore vettoriale. Il più importante di questi è che i processori vettoriali, intrinsecamente per definizione e design, sono sempre stati di lunghezza variabile sin dal loro inizio.

Laddove i SIMD puri (a larghezza fissa, senza predicazione) sono comunemente erroneamente definiti "Vettori" (perché SIMD viene utilizzato per elaborare dati che risultano essere Vettori), attraverso un'attenta analisi e confronto di ISA storici e moderni, i processori Vector effettivi possono essere osservato per avere le seguenti caratteristiche che nessun SIMD ISA ha:

  • un modo per impostare la lunghezza del vettore (come l' setvlistruzione in RISCV RVV) o fornire una funzione REP(ripetizione dell'istruzione) in qualche forma, senza limitare le ripetizioni a una potenza di due
  • Iterazione e riduzione su elementi all'interno di Vettori. I vettori RISC-V a partire dalla versione 0.10 hanno solo la riduzione, mentre i sistemi SX-Aurora e successivi Cray hanno l'iterazione e la riduzione.

SIMD predicato (parte della tassonomia di Flynn ) che è una maschera completa dei predicati a livello di elemento individuale su ogni istruzione Vector come è ora disponibile in ARM SVE2. e AVX-512 , si qualifica quasi come un processore Vector. Il SIMD predicato utilizza ALU SIMD a larghezza fissa ma consente l'attivazione (predicata) controllata localmente delle unità per fornire l'aspetto di vettori di lunghezza variabile. Gli esempi seguenti aiutano a spiegare queste distinzioni categoriali.

SIMD , essendo un'elaborazione batch a larghezza fissa, non è in grado di far fronte all'iterazione e alla riduzione. Ciò è ulteriormente illustrato con esempi, di seguito.

Simd vs vector.png

Inoltre, i processori vettoriali possono essere più efficienti in termini di risorse (utilizzare hardware più lento, risparmiando energia, ma ottenendo comunque un throughput) e avere una latenza inferiore rispetto a SIMD, attraverso il concatenamento vettoriale .

Considera sia un processore SIMD che un processore vettoriale che lavorano su 4 elementi a 64 bit, eseguendo una sequenza LOAD, ADD, MULTIPLY e STORE. Se la larghezza SIMD è 4, il processore SIMD deve CARICARE completamente 4 elementi prima di poter passare agli ADD, deve completare tutti gli ADD prima di poter passare ai MULTIPLY e allo stesso modo deve completare tutti i MULTIPLY prima di poter avviare gli STORE. Questo è per definizione e per progettazione .

Dover eseguire LOAD a 64 bit simultanei di 4 dimensioni e STORE a 64 bit è molto costoso in termini di hardware (percorsi dati a 256 bit verso la memoria). Avere 4x ALU a 64 bit, in particolare MULTIPLY, allo stesso modo. Per evitare questi costi elevati, un processore SIMD dovrebbe avere LOAD a 64 bit di larghezza 1, STORE a 64 bit di larghezza 1 e solo ALU a 64 bit di larghezza 2. Come mostrato nel diagramma, che presuppone un modello di esecuzione multi-problema , le conseguenze sono che le operazioni ora richiedono più tempo per essere completate. Se la multiemissione non è possibile, le operazioni richiedono ancora più tempo perché il LD potrebbe non essere emesso (avviato) contemporaneamente ai primi ADD, e così via. Se ci sono solo ALU SIMD a 64 bit di larghezza 4, il tempo di completamento è ancora peggiore: solo quando tutti e quattro i LOAD sono stati completati possono iniziare le operazioni SIMD e solo quando tutte le operazioni ALU sono state completate possono iniziare gli STORE.

Un processore vettoriale, al contrario, anche se è un singolo problema e non utilizza SIMD ALU, avendo solo LOAD a 64 bit di larghezza 1, STORE a 64 bit di larghezza 1 (e, come nel Cray-1 , la capacità di eseguire MULTIPLY simultaneamente con ADD), può completare le quattro operazioni più velocemente di un processore SIMD con LOAD da 1, STORE da 1 e SIMD da 2. Questo utilizzo più efficiente delle risorse, dovuto al concatenamento vettoriale , è un vantaggio chiave e una differenza rispetto a SIMD. SIMD per progettazione e definizione non può eseguire il concatenamento se non per l'intero gruppo di risultati.

Descrizione

In termini generali, le CPU sono in grado di manipolare uno o due dati alla volta. Ad esempio, la maggior parte delle CPU ha un'istruzione che essenzialmente dice "aggiungi A a B e metti il ​​risultato in C". I dati per A, B e C potrebbero essere, almeno in teoria, codificati direttamente nell'istruzione. Tuttavia, in un'implementazione efficiente le cose raramente sono così semplici. I dati vengono raramente inviati in forma grezza e vengono invece "puntati" passando un indirizzo a una posizione di memoria che contiene i dati. La decodifica di questo indirizzo e l'estrazione dei dati dalla memoria richiede del tempo, durante il quale la CPU normalmente rimane inattiva in attesa che vengano visualizzati i dati richiesti. Con l'aumento della velocità della CPU, questa latenza della memoria è storicamente diventata un grande ostacolo alle prestazioni; vedere Muro di memoria .

Al fine di ridurre la quantità di tempo impiegata da questi passaggi, la maggior parte delle moderne CPU utilizza una tecnica nota come pipeline di istruzioni in cui le istruzioni passano a turno attraverso diverse sottounità. La prima sottounità legge l'indirizzo e lo decodifica, la successiva "recupera" i valori a quegli indirizzi e la successiva esegue i calcoli. Con il pipelining il "trucco" è iniziare a decodificare l'istruzione successiva anche prima che la prima abbia lasciato la CPU, alla maniera di una catena di montaggio , quindi il decodificatore di indirizzi è costantemente in uso. Qualsiasi istruzione particolare richiede la stessa quantità di tempo per essere completata, un tempo noto come latenza , ma la CPU può elaborare un intero batch di operazioni, in modo sovrapposto, molto più velocemente e in modo più efficiente che se lo facesse una alla volta.

I processori vettoriali portano questo concetto un passo avanti. Invece di convogliare solo le istruzioni, convogliano anche i dati stessi. Il processore riceve istruzioni che dicono non solo di aggiungere A a B, ma di aggiungere tutti i numeri "da qui a qui" a tutti i numeri "da lì a lì". Invece di dover costantemente decodificare le istruzioni e quindi recuperare i dati necessari per completarle, il processore legge una singola istruzione dalla memoria, ed è semplicemente implicito nella definizione dell'istruzione stessa che l'istruzione opererà di nuovo su un altro dato, ad un indirizzo maggiore di un incremento rispetto al precedente. Ciò consente un notevole risparmio di tempo di decodifica.

Per illustrare la differenza che questo può fare, considera il semplice compito di sommare due gruppi di 10 numeri insieme. In un normale linguaggio di programmazione si scriverebbe un "loop" che raccogliesse a turno ciascuna delle coppie di numeri e poi le aggiungesse. Per la CPU, questo assomiglierebbe a questo:

; Hypothetical RISC machine
; add 10 numbers in a to 10 numbers in b, storing results in c
; assume a, b, and c are memory locations in their respective registers
  move  $10, count   ; count := 10
loop:
  load  r1, a
  load  r2, b
  add   r3, r1, r2   ; r3 := r1 + r2
  store r3, c
  add   a, a, $4     ; move on
  add   b, b, $4
  add   c, c, $4
  dec   count        ; decrement
  jnez  count, loop  ; loop back if count is not yet 0
  ret

Ma per un processore vettoriale, questo compito sembra notevolmente diverso:

; assume we have vector registers v1-v3 
; with size equal or larger than 10
  move   $10, count    ; count = 10
  vload  v1, a, count
  vload  v2, b, count
  vadd   v3, v1, v2
  vstore v3, c, count
  ret

Si noti la completa mancanza di loop nelle istruzioni, perché è l' hardware che ha eseguito 10 operazioni sequenziali: in effetti il ​​conteggio dei loop è su base esplicita per istruzione .

Gli ISA vettoriali in stile Cray fanno un ulteriore passo avanti e forniscono un registro di "conteggio" globale, chiamato Vector Length (VL):

; again assume we have vector registers v1-v3
; with size larger than or equal to 10
  setvli  $10        # Set vector length VL=10
  vload   v1, a      # 10 loads from a
  vload   v2, b      # 10 loads from b
  vadd   v3, v1, v2  # 10 adds
  vstore v3, c       # 10 stores into c
  ret

Ci sono diversi risparmi insiti in questo approccio.

  1. sono necessarie solo tre traduzioni di indirizzi. A seconda dell'architettura, questo può rappresentare di per sé un risparmio significativo.
  2. Un altro risparmio è il recupero e la decodifica dell'istruzione stessa, che deve essere eseguita solo una volta anziché dieci.
  3. Anche il codice stesso è più piccolo, il che può portare a un uso più efficiente della memoria, alla riduzione delle dimensioni della cache delle istruzioni L1, alla riduzione del consumo energetico.
  4. Con la dimensione del programma ridotta, la previsione del ramo ha un lavoro più semplice.
  5. Con la lunghezza (equivalente alla larghezza SIMD) non codificata nell'istruzione, non solo la codifica è più compatta, è anche "a prova di futuro" e consente anche ai progetti di processori incorporati di considerare l'utilizzo di Vettori esclusivamente per ottenere tutti gli altri vantaggi , piuttosto che puntare ad alte prestazioni.

Inoltre, nei più moderni ISA per processori vettoriali, è stato introdotto "Fail on First" o "Fault First" (vedi sotto) che porta ancora più vantaggi.

Ma oltre a questo, un processore vettoriale ad alte prestazioni può avere più unità funzionali che aggiungono quei numeri in parallelo. Il controllo delle dipendenze tra questi numeri non è richiesto poiché un'istruzione vettoriale specifica più operazioni indipendenti. Ciò semplifica la logica di controllo richiesta e può migliorare ulteriormente le prestazioni evitando stalli. Le operazioni matematiche si sono quindi completate complessivamente molto più velocemente, il fattore limitante è il tempo necessario per recuperare i dati dalla memoria.

Non tutti i problemi possono essere attaccati con questo tipo di soluzione. L'inclusione di questi tipi di istruzioni aggiunge necessariamente complessità alla CPU principale. Questa complessità in genere rende le altre istruzioni più lente, ad esempio ogni volta che non somma molti numeri di fila. Le istruzioni più complesse aumentano anche la complessità dei decodificatori, il che potrebbe rallentare la decodifica delle istruzioni più comuni come la normale aggiunta. ( Questo può essere in qualche modo mitigato mantenendo l'intero ISA ai principi RISC : RVV aggiunge solo circa 190 istruzioni Vector anche con le funzionalità avanzate. )

I processori vettoriali sono stati tradizionalmente progettati per funzionare al meglio solo quando ci sono grandi quantità di dati su cui lavorare. Per questo motivo, questi tipi di CPU sono stati trovati principalmente nei supercomputer , poiché i supercomputer stessi erano, in generale, trovati in luoghi come i centri di previsione meteorologica e i laboratori di fisica, dove vengono "sgranocchiate" enormi quantità di dati. Tuttavia, come mostrato sopra e dimostrato da RISC-V RVV, l' efficienza degli ISA vettoriali porta altri vantaggi che sono convincenti anche per i casi d'uso Embedded.

Istruzioni vettoriali

L'esempio di pseudocodice vettoriale di cui sopra parte dal presupposto che il computer vettoriale può elaborare più di dieci numeri in un batch. Per una maggiore quantità di numeri nel registro vettoriale, diventa irrealizzabile per il computer avere un registro così grande. Di conseguenza, il processore vettoriale acquisisce la capacità di eseguire i loop da solo o espone al programmatore una sorta di registro di controllo vettoriale (stato), generalmente noto come lunghezza del vettore.

Le istruzioni auto-ripetibili si trovano nei primi computer vettoriali come lo STAR-100, dove l'azione di cui sopra sarebbe stata descritta in una singola istruzione (un po' come vadd c, a, b, $10). Si trovano anche nell'architettura x86 come REPprefisso. Tuttavia, solo calcoli molto semplici possono essere eseguiti in modo efficace nell'hardware in questo modo senza un aumento dei costi molto elevato. Poiché tutti gli operandi devono essere in memoria per l'architettura STAR-100, anche la latenza causata dall'accesso è diventata enorme.

È interessante notare, tuttavia, che Broadcom ha incluso lo spazio in tutte le operazioni Vector del Videocore IV ISA per un REPcampo, ma a differenza dello STAR-100 che utilizza la memoria per le sue ripetizioni, le ripetizioni Videocore IV sono su tutte le operazioni comprese le operazioni aritmetiche vettoriali. La lunghezza della ripetizione può essere un piccolo intervallo di potenza di due o proveniente da uno dei registri scalari.

Il Cray-1 ha introdotto l'idea di utilizzare i registri del processore per contenere i dati vettoriali in batch. Le lunghezze dei batch (Vector Length, VL) possono essere impostate dinamicamente con un'istruzione speciale, il significato rispetto a Videocore IV (e, soprattutto, come verrà mostrato di seguito, anche SIMD) è che la lunghezza della ripetizione non deve far parte di la codifica delle istruzioni. In questo modo, è possibile fare molto più lavoro in ogni batch e la codifica delle istruzioni è molto più elegante e compatta, l'unico inconveniente è che per sfruttare appieno questa capacità di elaborazione batch aggiuntiva, il carico di memoria e la velocità di memorizzazione di conseguenza avevano anche per aumentare. A volte si dice che questo sia uno svantaggio dei processori Vector in stile Cray: la realtà è che si limita a raggiungere un throughput ad alte prestazioni, come si vede nelle GPU , che affrontano esattamente lo stesso problema. In parole povere: vuoi sgranocchiare numeri, hai bisogno di larghezza di banda.

I moderni computer SIMD affermano di migliorare i primi Cray utilizzando direttamente più ALU, per un grado di parallelismo più elevato rispetto al solo utilizzo della normale pipeline scalare. I moderni processori vettoriali (come SX-Aurora TSUBASA ) combinano entrambi, inviando più dati a più SIMD ALU pipeline interne, il numero emesso viene scelto dinamicamente dal programma Vector in fase di esecuzione. Le maschere possono essere utilizzate per caricare e memorizzare selettivamente i dati nelle posizioni di memoria e utilizzare quelle stesse maschere per disabilitare selettivamente l'elemento di elaborazione delle ALU SIMD. Alcuni processori con SIMD ( AVX-512 , ARM SVE2 ) sono capaci di questo tipo di elaborazione selettiva, per elemento ( "predicato" ), e sono questi che in qualche modo meritano la nomenclatura "Vector Processor" o almeno meritano la pretesa di essere in grado di "elaborazione vettoriale". I processori SIMD senza predicazione per elemento ( MMX , SSE , AltiVec ) categoricamente no.

Le GPU moderne, che hanno molte piccole unità di calcolo, ciascuna con le proprie ALU SIMD indipendenti, utilizzano qualcosa chiamato Single Instruction Multiple Threads (SIMT). Le unità SIMT vengono eseguite da un'unità di istruzione sincronizzata a trasmissione singola condivisa. I "registri vettoriali" sono molto ampi e le condotte tendono ad essere lunghe. La parte "threading" di SIMT riguarda il modo in cui i dati vengono gestiti in modo indipendente su ciascuna delle unità di calcolo.

Inoltre, le GPU come il Broadcom Videocore IV e altri processori vettoriali esterni come il NEC SX-Aurora TSUBASA possono utilizzare meno unità vettoriali rispetto a quanto implica la larghezza: invece di avere 64 unità per un registro di 64 numeri, l'hardware potrebbe invece eseguire un ciclo pipeline su 16 unità per un approccio ibrido. Il Broadcom Videocore IV è anche in grado di questo approccio ibrido: affermando nominalmente che il suo motore SIMD QPU supporta operazioni di array FP di 16 lunghe nelle sue istruzioni, in realtà le esegue 4 alla volta, come (un'altra) forma di "thread".

Esempio di istruzioni vettoriali

In questo esempio iniziamo con un algoritmo ("IAXPY"), prima lo mostriamo in istruzioni scalari, quindi SIMD, quindi SIMD predicata e infine istruzioni vettoriali. Questo aiuta in modo incrementale a illustrare la differenza tra un processore vettoriale tradizionale e un moderno SIMD. Iniziamo con una variante intera a 32 bit della funzione "DAXPY", in c :

void iaxpy(size_t n, int a, const int x[], int y[]) {
    for (size_t i = 0; i < n; i++)
        y[i] = a * x[i] + y[i];
}

In ogni iterazione, ogni elemento di y ha un elemento di x moltiplicato per a e aggiunto ad esso. Il programma è espresso in forma lineare scalare per la leggibilità.

Assemblatore scalare

La nostra versione scalare di questo caricherebbe uno di ciascuno di x e y, elaborerebbe un calcolo, memorizzerebbe un risultato e ripeterebbe:

loop:
  load32  r1, x      ; load one 32bit data
  load32  r2, y
  mul32   r1, a, r1  ; r1 := r1 * a
  add32   r3, r1, r2 ; r3 := r1 + r2
  store32 r3, y
  addl    x, x, $4   ; x := x + 4
  addl    y, y, $4
  subl    n, n, $1   ; n := n - 1
  jgz     n, loop    ; loop back if n > 0
out:
  ret

Il codice simile a STAR rimane conciso, ma poiché la vettorizzazione dello STAR-100 era volutamente basata sugli accessi alla memoria, ora è necessario uno slot di memoria aggiuntivo per elaborare le informazioni. È inoltre necessaria una latenza doppia a causa del requisito aggiuntivo di accesso alla memoria.

  ; Assume tmp is pre-allocated
  vmul tmp, a, x, n ; tmp[i] = a * x[i]
  vadd y, y, tmp, n ; y[i] = y[i] + tmp[i]
  ret

SIMD Pure puro (non predicato, confezionato)

Una moderna architettura Packed SIMD, conosciuta con molti nomi (elencata nella tassonomia di Flynn ), può eseguire la maggior parte delle operazioni in batch. Il codice è per lo più simile alla versione scalare. Supponiamo che sia x che y siano correttamente allineati qui (iniziano solo su un multiplo di 16) e che n sia un multiplo di 4, poiché altrimenti sarebbe necessario del codice di installazione per calcolare una maschera o per eseguire una versione scalare. Assumiamo inoltre, per semplicità, che le istruzioni SIMD abbiano un'opzione per ripetere automaticamente gli operandi scalari, proprio come può farlo ARM NEON. In caso contrario, è necessario utilizzare uno "splat" (trasmissione) per copiare l'argomento scalare in un registro SIMD:

  splatx4   v4, a        ; v4 = a,a,a,a

Il tempo impiegato sarebbe sostanzialmente lo stesso di un'implementazione vettoriale y = mx + cdescritta sopra.

vloop:
  load32x4  v1, x
  load32x4  v2, y
  mul32x4   v1, a, v1  ; v1 := v1 * a
  add32x4   v3, v1, v2 ; v3 := v1 + v2
  store32x4 v3, y
  addl      x, x, $16  ; x := x + 16
  addl      y, y, $16
  subl      n, n, $4   ; n := n - 4
  jgz       n, vloop   ; go back if n > 0
out:
  ret

Nota che entrambi i puntatori x e y vengono incrementati di 16, perché è la lunghezza (in byte) di quattro interi a 32 bit. È stata presa la decisione che l'algoritmo deve gestire solo SIMD di larghezza 4, quindi la costante è codificata nel programma.

Sfortunatamente per SIMD, l'indizio era nell'assunzione di cui sopra, "che n è un multiplo di 4" e "accesso allineato", che, chiaramente, è un caso d'uso specialistico limitato.

Realisticamente, per i cicli generici come nelle librerie portatili, dove n non può essere limitato in questo modo, l'overhead di installazione e pulizia per SIMD al fine di far fronte a non multipli della larghezza SIMD, può superare di gran lunga il conteggio delle istruzioni all'interno il ciclo stesso. Supponendo che nel peggiore dei casi l'hardware non possa eseguire accessi alla memoria SIMD disallineati, un algoritmo del mondo reale:

  • prima bisogna avere una sezione preparatoria che lavori sui dati iniziali non allineati, fino al primo punto in cui possono subentrare le operazioni SIMD memory-aligned. ciò comporterà operazioni solo scalari (più lente) o operazioni SIMD impaccate di dimensioni inferiori. ogni copia implementa l'intero ciclo interno dell'algoritmo
  • eseguire il ciclo SIMD allineato alla larghezza SIMD massima fino agli ultimi elementi (quelli rimanenti che non si adattano alla larghezza SIMD fissa)
  • avere una fase di pulizia che, come la sezione preparatoria, è altrettanto ampia e altrettanto complessa.

SIMD a otto larghezze richiede la ripetizione dell'algoritmo del ciclo interno prima con elementi SIMD a quattro larghezze, poi SIMD a due larghezze, quindi uno (scalare), con un test e un ramo tra ciascuno di essi, al fine di coprire il primo e l'ultimo SIMD rimanente elementi (0 <= n <= 7).

Questo più che triplica la dimensione del codice, infatti in casi estremi si traduce in un aumento di ordine di grandezza nel conteggio delle istruzioni! Questo può essere facilmente dimostrato compilando l'esempio iaxpy per AVX-512 , usando le opzioni "-O3 -march=knl"di gcc .

Nel tempo, man mano che ISA si evolve per continuare a migliorare le prestazioni, ISA Architects aggiunge SIMD a 2 larghezze, quindi SIMD a 4 larghezze, quindi a 8 larghezze e oltre. Iniziamo a vedere, quindi, perché AVX-512 esiste in x86.

Senza predizione, più ampia è la larghezza del SIMD, peggiori sono i problemi, portando a una massiccia proliferazione di codici operativi, prestazioni degradate, consumo energetico extra e complessità del software non necessaria.

I processori vettoriali, d'altra parte, sono progettati per emettere calcoli di lunghezza variabile per un conteggio arbitrario, n, e quindi richiedono pochissima configurazione e nessuna pulizia. Anche rispetto a quegli ISA SIMD che hanno maschere (ma nessuna setvlistruzione), i processori vettoriali producono codice molto più compatto perché non hanno bisogno di eseguire calcoli espliciti di maschere per coprire gli ultimi elementi (illustrati di seguito).

SIMD predicato

Supponendo un ipotetico predicato (maschera compatibile) SIMD ISA, e assumendo nuovamente che le istruzioni SIMD possano far fronte a dati disallineati, il ciclo di istruzioni sarebbe simile a questo:

vloop:
  # prepare mask. few ISAs have min though
  min       t0, n, $4     ; t0 = min(n, 4)
  shift     m, $1, t0     ; m = 1<<t0
  sub       m, m, $1      ; m = (1<<t0)-1
  # now do the operation, masked by m bits
  load32x4  v1, x, m
  load32x4  v2, y, m
  mul32x4   v1, a, v1, m  ; v1 := v1 * a
  add32x4   v3, v1, v2, m ; v3 := v1 + v2
  store32x4 v3, y, m
  # update x, y and n for next loop
  addl      x, t0*4      ; x := x + t0*4
  addl      y, t0*4
  subl      n, n, t0     ; n := n - t0
  # loop?
  jgz       n, vloop     ; go back if n > 0
out:
  ret

Qui possiamo vedere che il codice è molto più pulito ma un po' complesso: almeno comunque non c'è configurazione o pulizia: all'ultima iterazione del ciclo, la maschera del predicato sarà impostata su 0b0000, 0b0001, 0b0011, 0b0111 o 0b1111 , con conseguente esecuzione di operazioni tra 0 e 4 elementi SIMD, rispettivamente. Un'ulteriore potenziale complicazione: alcuni ISA RISC non hanno un'istruzione "min", che necessitano invece di utilizzare un branch o un confronto predicato scalare.

È chiaro come il SIMD predicato meriti almeno il termine "Capacità vettoriale", perché può far fronte a Vettori di lunghezza variabile utilizzando maschere di predicato. Il passo in evoluzione finale per un "vero" Vector ISA però è quello di non avere alcuna prova in ISA a tutti di larghezza SIMD, lasciando che tutto fino all'hardware.

Puro (vero) vettore ISA

Per ISA vettoriali in stile Cray come RVV, viene utilizzata un'istruzione chiamata "setvl" (set Vector Length). L'hardware definisce innanzitutto quanti valori di dati può elaborare in un "Vettore": questo potrebbe essere registri effettivi o potrebbe essere un loop interno (l'approccio ibrido, menzionato sopra). Questa quantità massima (il numero di "Lanes" hardware) è denominata "MVL" (Maximum Vector Length). Nota che, come abbiamo visto in SX-Aurora e Videocore IV, MVL potrebbe essere una quantità di corsia hardware reale o virtuale . (Nota: come menzionato nel Tutorial ARM SVE2, i programmatori non devono commettere l'errore di assumere una larghezza Vector fissa: di conseguenza MVL non è una quantità che il programmatore deve conoscere. Questo può essere un po' sconcertante dopo anni di mentalità SIMD).

Chiamando setvl con il numero di elementi di dati in sospeso da elaborare, "setvl" è autorizzato (più simile, richiesto) a limitarlo alla lunghezza massima del vettore (MVL) e quindi restituisce il numero effettivo che può essere elaborato dall'hardware in successive istruzioni Vector, e imposta il registro speciale interno, "VL", allo stesso importo. ARM si riferisce a questa tecnica come programmazione "Vector Length Agnostic" nei suoi tutorial su SVE2.

Di seguito è riportato l'assemblatore vettoriale in stile Cray per lo stesso ciclo in stile SIMD, sopra. Guarda attentamente come viene utilizzato t0 (che, contenente una comoda copia di VL, può variare) al posto delle costanti hardcoded:

vloop:
  setvl   t0, n      # VL=t0=min(MVL, n)
  vld32   v0, x      # load vector x
  vld32   v1, y      # load vector y
  vmadd32 v1, v0, a  # v1 += v0 * a
  vst32   v1, y      # store Y
  add     y, t0*4    # advance y by VL*4
  add     x, t0*4    # advance x by VL*4
  sub     n, t0      # n -= VL (t0)
  bnez    n, vloop   # repeat if n != 0

Questo in realtà non è molto diverso dalla versione SIMD (elabora 4 elementi di dati per loop) o dalla versione Scalar iniziale (elabora solo uno). Possiamo vedere che n contiene ancora il numero di elementi di dati rimanenti da lavorare, ma che t0 contiene la copia VL - il numero che va da elaborare in ogni iterazione. t0 viene sottratto da n dopo ogni iterazione e se n è zero, tutti gli elementi sono stati elaborati.

Una serie di cose affascinanti da notare, quando si confronta con la variante dell'assieme SIMD predicato:

  1. l' setvlistruzione ha incorporato al suo interno minun'istruzione
  2. Laddove la variante SIMD ha codificato sia la larghezza (4) nella creazione della maschera che la larghezza SIMD (load32x4 ecc.) gli equivalenti Vector ISA non hanno tale limite. Ciò rende i programmi Vector portabili, indipendenti dal fornitore e a prova di futuro.
  3. l'impostazione VL crea effettivamente una maschera del predicato nascosta che viene applicata automaticamente ai Vettori
  4. Laddove con SIMD predicato la lunghezza in bit della maschera è limitata a quella che può essere contenuta in un registro scalare (o maschera speciale), i registri maschera di Vector ISA non hanno tale limitazione. I vettori Cray-I potrebbero essere poco più di 1.000 elementi (nel 1977).

Così possiamo vedere, molto chiaramente, come i Vector ISA riducano il numero di istruzioni.

Si noti inoltre che, proprio come la variante SIMD predicata, i puntatori a x e y vengono avanzati di t0 volte quattro perché entrambi puntano a dati a 32 bit, ma che n viene decrementato di t0 dritto. Rispetto all'assemblatore SIMD a dimensione fissa c'è una differenza apparente molto piccola: x e y sono anticipati della costante 16 codificata, n è decrementata di 4 codificata, quindi inizialmente è difficile apprezzarne il significato. La differenza sta nel rendersi conto che l'hardware Vector potrebbe essere in grado di eseguire 4 operazioni simultanee, o 64, o 10.000, sarebbe esattamente lo stesso Vector Assembler per tutti e non ci sarebbe ancora alcun codice di pulizia SIMD . Anche rispetto al SIMD con capacità di predicato, è ancora più compatto, più chiaro, più elegante e utilizza meno risorse.

Non solo abbiamo un programma molto più compatto (risparmiando sulla dimensione della cache L1) ma, come accennato in precedenza, la versione Vector può inviare molta più elaborazione dei dati alle ALU, risparmiando ancora energia perché Instruction Decode e Issue possono rimanere inattivi.

Un altro aspetto affascinante qui: il numero di elementi che entrano nella funzione può iniziare da zero . Questo imposta Vector Length a zero, che disabilita effettivamente tutte le istruzioni Vector, trasformandole in no-ops , in fase di runtime. Pertanto, a differenza del SIMD non predicato, anche quando non ci sono elementi da elaborare non c'è ancora codice di pulizia sprecato.

Esempio di riduzione vettoriale

In questo esempio iniziamo con un algoritmo che prevede la riduzione. Proprio come nell'esempio precedente, lo mostriamo prima in istruzioni scalari, poi in SIMD e infine in istruzioni vettoriali. Iniziamo in c :

void (size_t n, int a, const int x[]) {
    int y = 0;
    for (size_t i = 0; i < n; i++)
        y += x[i];
    return y;
}

Qui, un accumulatore (y) viene utilizzato per sommare tutti i valori nell'array, x.

Assemblatore scalare

La nostra versione scalare di questo caricherebbe ciascuno di x, lo aggiungerebbe a y e lo ripeterebbe:

  set     y, 0     ; y initialised to zero
loop:
  load32  r1, x    ; load one 32bit data
  add32   y, y, r1 ; y := y + r1
  addl    x, x, $4 ; x := x + 4
  subl    n, n, $1 ; n := n - 1
  jgz     n, loop  ; loop back if n > 0
out:
  ret y            ; returns result, y

Questo è molto semplice. "y" inizia da zero, gli interi a 32 bit vengono caricati uno alla volta in r1, aggiunti a y, e l'indirizzo dell'array "x" viene spostato all'elemento successivo nell'array.

Riduzione SIMD

È qui che iniziano i problemi. SIMD per progettazione non è in grado di eseguire operazioni aritmetiche "inter-elemento". L'Elemento 0 di un registro SIMD può essere aggiunto all'Elemento 0 di un altro registro, ma l'Elemento 0 non può essere aggiunto a qualcosa di diverso da un altro Elemento 0. Ciò pone alcune gravi limitazioni alle potenziali implementazioni. Supponiamo per semplicità che n sia esattamente 8:

  addl      r3, x, $16 ; for 2nd 4 of x
  load32x4  v1, x      ; first 4 of x
  load32x4  v2, r3     ; 2nd 4 of x
  add32x4   v1, v2, v1 ; add 2 groups

A questo punto abbiamo eseguito quattro aggiunte:

  • x[0]+x[4] - Primo SIMD ADD: elemento 0 del primo gruppo aggiunto all'elemento 0 del secondo gruppo
  • x[1]+x[5] - Second SIMD ADD: elemento 1 del primo gruppo aggiunto all'elemento 1 del secondo gruppo
  • x[2]+x[6] - Terzo SIMD ADD: elemento 2 del primo gruppo aggiunto all'elemento 2 del secondo gruppo
  • x[3]+x[7] - Quarto SIMD ADD: elemento 3 del primo gruppo aggiunto all'elemento 2 del secondo gruppo

ma con larghezza 4 SIMD essere incapace in base alla progettazione di aggiungere x[0]+x[1]ad esempio le cose vanno in discesa rapidamente proprio come ha fatto con il caso generale di utilizzo di SIMD per general-purpose IAXPY loop. Per sommare i nostri quattro risultati parziali, è possibile utilizzare SIMD a due larghezze, seguito da una singola aggiunta scalare, per produrre infine la risposta, ma, spesso, i dati devono essere trasferiti da registri SIMD dedicati prima che possa essere eseguito l'ultimo calcolo scalare .

Anche con un ciclo generale (n non fisso), l'unico modo per utilizzare SIMD a 4 larghezze è assumere quattro "flussi" separati, ciascuno sfalsato di quattro elementi. Infine, i quattro risultati parziali devono essere sommati. Altre tecniche implicano lo shuffle: si possono trovare esempi online per AVX-512 su come eseguire la "Somma orizzontale"

A parte le dimensioni del programma e la complessità, sorge un ulteriore potenziale problema se si tratta di calcolo in virgola mobile: il fatto che i valori non vengano sommati in ordine rigoroso (quattro risultati parziali) potrebbe comportare errori di arrotondamento.

Riduzione ISA vettoriale

I set di istruzioni vettoriali hanno operazioni di riduzione aritmetica integrate nell'ISA. Se possiamo assumere che n sia minore o uguale alla Lunghezza massima del vettore, sono necessarie solo tre istruzioni:

  setvl      t0, n  # VL=t0=min(MVL, n)
  vld32      v0, x  # load vector x
  vredadd32  y, v0  # reduce-add into y

Il codice quando n è maggiore della Lunghezza massima del vettore non è molto più complesso ed è un modello simile al nostro primo esempio ("IAXPY").

  set     y, 0
vloop:
  setvl   t0, n      # VL=t0=min(MVL, n)
  vld32   v0, x      # load vector x
  vredadd32 y, y, v0 # add all x into y
  add     x, t0*4    # advance x by VL*4
  sub     n, t0      # n -= VL (t0)
  bnez    n, vloop   # repeat if n != 0
  ret y

La semplicità dell'algoritmo è netta rispetto a SIMD. Di nuovo, proprio come con l'esempio IAXPY, l'algoritmo è indipendente dalla lunghezza (anche su implementazioni Embedded in cui la Lunghezza massima del vettore potrebbe essere solo una).

Le implementazioni in hardware possono, se sono certe che verrà prodotta la risposta giusta, eseguire la riduzione in parallelo. Alcuni ISA Vector offrono una modalità di riduzione parallela come opzione esplicita, per quando il programmatore sa che qualsiasi potenziale errore di arrotondamento non ha importanza e la bassa latenza è fondamentale.

Questo esempio evidenzia ancora una volta una differenza fondamentale fondamentale fondamentale tra i "veri" processori vettoriali e quei processori SIMD, inclusa la maggior parte delle GPU commerciali, che sono "ispirati" dalle funzionalità dei processori vettoriali.

Approfondimenti da esempi

Rispetto a qualsiasi processore SIMD che afferma di essere un processore vettoriale, l'ordine di grandezza della riduzione delle dimensioni del programma è quasi scioccante. Tuttavia questo livello di eleganza a livello ISA ha un prezzo piuttosto alto a livello hardware:

  1. Dall'esempio IAXPY, vediamo che, a differenza dei processori SIMD, che possono semplificare il proprio hardware interno evitando di gestire l'accesso alla memoria disallineato, un processore vettoriale non può farla franca con tale semplificazione: vengono scritti algoritmi che si basano intrinsecamente sul successo di Vector Load e Store, indipendentemente dall'allineamento dell'inizio del vettore.
  2. Mentre dall'esempio di riduzione vediamo che, a parte le istruzioni di permuta , SIMD per definizione evita del tutto le operazioni tra le corsie (l'elemento 0 può essere aggiunto solo a un altro elemento 0), i Vector Processor affrontano direttamente questo aspetto. Ciò che i programmatori sono costretti a fare nel software (usando lo shuffle e altri trucchi, per scambiare i dati nella "corsia" giusta) devono fare i processori vettoriali nell'hardware, automaticamente.

Nel complesso quindi c'è una scelta da avere

  1. software complesso e hardware semplificato (SIMD)
  2. software semplificato e hardware complesso (Vector Processors)

Queste differenze nette sono ciò che distingue un processore vettoriale da uno che ha SIMD.

Caratteristiche del processore vettoriale

Laddove molti SIMD ISA "prendono in prestito" o "si ispirano" all'elenco seguente, le caratteristiche tipiche che avrà un buon processore vettoriale sono:

  • Vector Load and Store : questi salvano intrinsecamente sulle ricerche di memoria virtuale e sono progettati per inserire i dati nei registri con il minimo sforzo. I miglioramenti avanzati di Vector Load/store includono il supporto per l' imballaggio della struttura , Fail-First, Gather-scatter , Indexed, Unit e Element strides.
  • Operazioni mascherate : come ora si trovano comunemente nelle GPU , le maschere predicate consentono costrutti paralleli if/then/else senza rami (che sono intrinsecamente scalari per natura)
  • Comprimi ed espandi : solitamente utilizzando una maschera di bit, i dati vengono compressi linearmente o espansi (ridistribuiti) in base al fatto che i bit nella maschera siano impostati o cancellati, preservando sempre l'ordine sequenziale e non duplicando mai i valori (a differenza di Gather-Scatter aka permute) . Queste istruzioni sono presenti in AVX-512
  • Register Gather, Scatter (aka permute) – una variazione meno restrittiva più generica del tema Comprimi/Espandi che richiede invece un vettore per specificare gli indici da utilizzare per "riordinare" un altro vettore. Gather/Scatter è più complesso da implementare rispetto a Compress/Expand e, essendo intrinsecamente non sequenziale, può interferire con il concatenamento di vettori . Da non confondere con le modalità Gather-scatter Memory Load/Store, le operazioni Gather/Scatter Vector agiscono sui registri Vector e sono spesso chiamate invece istruzioni di permuta .
  • Splat ed Extract : utili per l'interazione tra Scalar e Vector, trasmettono un singolo valore attraverso un Vector o estraggono un elemento da un Vector, rispettivamente.
  • Iota - un'istruzione molto semplice e strategicamente utile che rilascia immediati in sequenza incrementale in elementi successivi. Di solito parte da zero.
  • Riduzione e Iterazione : operazioni che eseguono mapreduce su un vettore (ad esempio, trovare l'unico valore massimo di un intero vettore o sommare tutti gli elementi). L'iterazione è della forma x[i] = y[i] + x[i-1]dove la riduzione è della formax = y[0] + y[1]… + y[n-1]
  • Supporto Matrix Multiply : sia tramite il caricamento algoritmico dei dati dalla memoria, sia il riordino (rimappatura) dell'accesso normalmente lineare agli elementi Vector, o la fornitura di "Accumulatori", le matrici di dimensioni arbitrarie possono essere elaborate in modo efficiente. IBM POWER10 fornisce istruzioni MMA anche se per larghezze Matrix arbitrarie che non si adattano all'esatta dimensione SIMD sono necessarie tecniche di ripetizione dei dati, il che è uno spreco di risorse del file di registro. L'Aspex ASP Linedancer aveva un motore DMA per il riordino della memoria 2D/3D che richiedeva uno sforzo significativo per essere utilizzato in modo ottimale. NVidia fornisce un'API Matrix CUDA di alto livello sebbene i dettagli interni non siano disponibili. La tecnica più efficiente in termini di risorse è il riordino sul posto dell'accesso a dati vettoriali altrimenti lineari.
  • Formati matematici avanzati : spesso includono l' aritmetica del campo di Galois , ma possono includere decimale codificato binario o virgola fissa decimale e supporto per operazioni aritmetiche molto più grandi (precisione arbitraria) supportando il riporto e l'esecuzione paralleli
  • Manipolazione dei bit – incluse le versioni vettorializzate di operazioni di permutazione a livello di bit, inserimento ed estrazione di campi di bit, operazioni di centrifuga, conteggio della popolazione e molte altre .

Funzionalità di elaborazione vettoriale GPU

Con molte applicazioni 3D Shader che richiedono operazioni trigonometriche e vettori brevi per operazioni comuni (RGB, ARGB, XYZ, XYZW) il supporto per quanto segue è tipicamente presente nelle GPU moderne, oltre a quelle che si trovano nei processori vettoriali:

  • Sub-vettori - elementi possono tipicamente contenere due, tre o quattro sottoelementi (vec2, vec3, vec4) dove un dato bit di maschera predicato si applica a tutto vec2 / 3/4, non gli elementi in sub-vettore. I sub-vettori sono introdotti anche in RISC-V RVV (denominato "LMUL"). I sottovettori sono una parte integrante fondamentale della specifica Vulkan SPIR-V .
  • Swizzle sub-vettoriale – alias "Lane Shuffling" che consente calcoli tra elementi sub-vettoriali senza bisogno di istruzioni extra (costose, dispendiose) per spostare i sotto-elementi nelle "corsie" SIMD corrette. Salva anche i bit della maschera del predicato. In effetti questo è un mini-permuto in volo del sotto-vettore, molto presente nei binari 3D Shader ed è sufficientemente importante da far parte delle specifiche Vulkan SPIR-V . Il Broadcom Videocore IV usa la terminologia "Lane rotante " dove il resto del settore usa il termine "swizzle" .
  • Trascendentali : le operazioni trigonometriche come seno , coseno e logaritmo sono ovviamente presenti in modo molto più predominante in 3D rispetto a molti carichi di lavoro HPC impegnativi . È interessante, tuttavia, che la velocità è molto più importante dell'accuratezza in 3D per le GPU, dove il calcolo delle coordinate dei pixel semplicemente non richiede un'elevata precisione. La specifica Vulkan lo riconosce e stabilisce requisiti di precisione sorprendentemente bassi, in modo che l'hardware GPU possa ridurre il consumo energetico. Il concetto di riduzione della precisione laddove semplicemente non è necessario viene esplorato nell'estensione MIPS-3D .

Le funzionalità aggiuntive includono un'unità di mappatura delle texture che a volte è un'unità separata dal processore GPU principale. Spesso sono incluse anche le istruzioni per l' interpolazione della trama e molte altre istruzioni specialistiche come la normalizzazione del vettore e il prodotto scalare .

Non molto è disponibile pubblicamente sui moderni set di istruzioni GPU. Ulteriori informazioni e suggerimenti sulle caratteristiche di GPU Vector ISA possono essere trovate esaminando la specifica SPIR-V , la specifica Vulkan e la specifica OpenCL . Queste API sono state create dal Khronos Group da membri come Intel, Google, AMD e NVIDIA, e quindi forniscono informazioni sulle funzionalità fornite dalle GPU più veloci del mondo. Di aiuto è anche il set di istruzioni retroingegnerizzate della GPU MALI Midgard.

Prima l'errore (o l'errore)

Introdotto in ARM SVE2 e RISC-V RVV è il concetto di carichi vettoriali sequenziali speculativi. ARM SVE2 ha un registro speciale chiamato "First Fault Register", dove RVV modifica (tronca) la Vector Length (VL).

Il principio di base di ffirst è quello di tentare un grande Vector Load sequenziale, ma per consentire all'hardware di troncare arbitrariamente la quantità effettiva caricata alla quantità che avrebbe successo senza generare un errore di memoria o semplicemente a una quantità (maggiore di zero) che è più conveniente. L'importante è che le istruzioni successive vengano notificate o possano determinare esattamente quanti Load sono effettivamente andati a buon fine, utilizzando quella quantità per eseguire solo il lavoro sui dati che sono stati effettivamente caricati.

Contrasta questa situazione con SIMD, che è una larghezza di carico fissa (inflessibile) e una larghezza di elaborazione dei dati fissa, incapace di far fronte a carichi che attraversano i confini della pagina, e anche se lo fossero non sono in grado di adattarsi a ciò che è effettivamente riuscito, tuttavia, paradossalmente, se il programma SIMD dovesse anche solo tentare di scoprire in anticipo (in ogni ciclo interno, ogni volta) cosa potrebbe avere successo in modo ottimale, quelle istruzioni servono solo a ostacolare le prestazioni perché, per necessità, farebbero parte del ciclo interno critico.

Questo inizia a suggerire il motivo per cui ffirst è così innovativo ed è meglio illustrato da memcpy o strcpy quando implementato con SIMD non-ffirst non predicato standard a 128 bit. Per IBM POWER9 il numero di istruzioni ottimizzate a mano per implementare strncpy è superiore a 240. Al contrario, la stessa routine strncpy nell'assemblatore RVV ottimizzato a mano è di sole 22 istruzioni.

L'esempio SIMD sopra potrebbe potenzialmente presentare errori e guasti alla fine della memoria, a causa di tentativi di lettura di troppi valori: potrebbe anche causare un numero significativo di pagine o errori disallineati attraversando allo stesso modo i limiti. Al contrario, consentendo all'architettura vettoriale la libertà di decidere quanti elementi caricare, la prima parte di un strncpy, se inizia inizialmente su un limite di memoria non ottimale, può restituire carichi appena sufficienti in modo tale che nelle successive iterazioni del ciclo il i batch di letture della memoria vettorizzata sono allineati in modo ottimale con le cache sottostanti e le disposizioni della memoria virtuale. Inoltre, l'hardware può scegliere di utilizzare l'opportunità per terminare la lettura della memoria di una determinata iterazione del ciclo esattamente su un limite di pagina (evitando una costosa seconda ricerca TLB), con l'esecuzione speculativa che prepara la successiva pagina di memoria virtuale mentre i dati sono ancora in fase di elaborazione nell'attuale ciclo continuo. Tutto questo è determinato dall'hardware , non dal programma stesso.

Prestazioni e velocità

Sia r il rapporto di velocità del vettore e f il rapporto di vettorizzazione. Se il tempo impiegato dall'unità vettoriale per aggiungere un array di 64 numeri è 10 volte più veloce della sua controparte scalare equivalente, r = 10. Inoltre, se il numero totale di operazioni in un programma è 100, di cui solo 10 sono scalari (dopo la vettorizzazione), quindi f = 0.9, ovvero il 90% del lavoro è svolto dall'unità vettoriale. Ne consegue l'accelerazione ottenibile di:

Quindi, anche se le prestazioni dell'unità vettoriale sono molto alte ( ) otteniamo un'accelerazione inferiore a , il che suggerisce che il rapporto f è cruciale per le prestazioni. Questo rapporto dipende dall'efficienza della compilazione come adiacenza degli elementi in memoria.

Programmazione di architetture di calcolo eterogenee

Varie macchine sono state progettate per includere sia processori tradizionali che processori vettoriali, come Fujitsu AP1000 e AP3000. La programmazione di macchine così eterogenee può essere difficile poiché lo sviluppo di programmi che sfruttano al meglio le caratteristiche di processori diversi aumenta il carico del programmatore. Aumenta la complessità del codice e riduce la portabilità del codice richiedendo l'interlacciamento del codice specifico dell'hardware nel codice dell'applicazione. Il bilanciamento del carico di lavoro dell'applicazione tra i processori può essere problematico, soprattutto perché in genere hanno caratteristiche prestazionali diverse. Esistono diversi modelli concettuali per affrontare il problema, ad esempio utilizzando un linguaggio di coordinamento e blocchi di programmazione del programma (librerie di programmazione o funzioni di ordine superiore). Ogni blocco può avere un'implementazione nativa diversa per ogni tipo di processore. Gli utenti semplicemente programmano utilizzando queste astrazioni e un compilatore intelligente sceglie la migliore implementazione in base al contesto.

Guarda anche

link esterno

Riferimenti