Ricerca…


introduzione

Questo argomento introduce alcuni dei meccanismi di base del calcolo parallelo necessari per comprendere e utilizzare pienamente OpenCL.

Thread ed esecuzione

La chiave del parallelismo consiste nell'usare più thread per risolvere un problema (duh.) Ma ci sono alcune differenze rispetto alla classica programmazione multithread nel modo in cui i thread sono organizzati.

Per prima cosa parliamo della tua tipica GPU, per amor di semplicità su cui mi concentrerò

Una GPU ha molti core di elaborazione, che lo rendono ideale per eseguire molti thread in parallelo. Questi core sono organizzati in Streaming Processors (SM, termine NVidia), di cui una GPU ha un determinato numero.

Tutti i thread in esecuzione all'interno di un SM sono chiamati "thread block". Ci possono essere più thread su un SM che su core. Il numero di core definisce la cosiddetta "dimensione Warp" (termine NVidia). I fili all'interno di un blocco di filo sono sheduled in cosiddetti "orditi".

Un esempio rapido da seguire: un tipico NVidia SM ha 32 core di elaborazione, quindi la sua dimensione di curvatura è 32. Se il mio blocco di thread ora ha 128 thread da eseguire, verranno sottoposti a shpedaggio in 4 warps (4 warps * 32 warp size = 128 thread).

La dimensione del curvatura è piuttosto importante quando si sceglie il numero di fili in seguito.

Tutti i thread all'interno di un singolo warp condividono un singolo contatore di istruzioni. Ciò significa che i 32 thread sono veramente sincronizzati in quanto ogni thread esegue ogni comando nello stesso momento. Qui sta una trappola per le prestazioni: questo vale anche per le dichiarazioni di ramificazione nel tuo kernel!

Esempio: ho un kernel che ha un'istruzione if e due rami. 16 dei miei thread all'interno di un warp eseguiranno il ramo uno, l'altro 16 il ramo due. Fino all'istruzione if, tutti i thread all'interno del warp sono sincronizzati. Adesso metà di loro sceglie un ramo diverso. Quello che succede è che l'altra metà rimarrà dormiente fino a quando l'istruzione sbagliata ha finito di eseguire i primi 16 thread. Quindi quei fili saranno dormienti fino a quando gli altri 16 fili finiranno il loro ramo.

Come puoi vedere, le cattive abitudini di branching possono rallentare gravemente il tuo codice parallelo, perché entrambe le istruzioni vengono eseguite nel peggiore dei casi. Se tutti i thread all'interno di un warp decidono di aver bisogno solo di una delle istruzioni, l'altra viene completamente ignorata e non si verifica alcun ritardo.

Anche la sincronizzazione dei thread non è una cosa semplice. È possibile sincronizzare solo le discussioni withing un singolo SM. Tutto al di fuori dell'SM non è sincronizzabile all'interno del kernel. Dovrai scrivere kernel separati e lanciarli uno dopo l'altro.

Memoria GPU

La GPU offre sei diverse regioni di memoria. Differiscono nella loro latenza, dimensione e accessibilità da diversi thread.

  • Memoria globale: la più grande memoria disponibile e una delle poche per lo scambio di dati con l'host. Questa memoria ha la latenza più alta ed è disponibile per tutti i thread.
  • Memoria costante: una parte di sola lettura della memoria globale, che può essere letta solo da altri thread. Il suo vantaggio è la minore latenza rispetto alla memoria globale
  • Texture Memory: anche una parte della memoria costante, specificamente progettata per le trame
  • Memoria condivisa: questa area di memoria è posizionata vicino all'SM e può essere raggiunta solo da un blocco di thread singolo. Offre una latenza inferiore alla memoria globale e un po 'meno latenza rispetto alla memoria costante.
  • Registri: accessibili solo da un singolo thread e la memoria più veloce di tutti loro. Ma se il compilatore rileva che non ci sono abbastanza Registri per le esigenze del kernel, esternalizza le variabili alla memoria locale.
  • Memoria locale: parte della memoria accessibile solo da thread nella regione di memoria globale. Usato come backup per i registri, da evitare se possibile.

Accesso alla memoria

Lo scenario tipico per l'utilizzo della memoria è quello di memorizzare i dati di origine e i dati elaborati nella memoria globale. Quando un threadblock si avvia, copia prima tutte le parti rilevanti nella memoria condivisa prima di ottenere le loro parti nei registri.

La latenza di accesso alla memoria dipende anche dalla tua strategia di memoria. Se accedete ciecamente ai dati, otterrete le peggiori prestazioni possibili.

I diversi ricordi sono organizzati nelle cosiddette "banche". Ogni richiesta di memoria per un banco può essere gestita in un singolo ciclo di clock. Il numero di banchi nella memoria condivisa è uguale alla dimensione del curvatura. La velocità di memoria può essere aumentata evitando l'accesso alla banca in conflitto all'interno di un singolo ordito.

Per copiare la memoria condivisa da o verso la memoria globale il modo più veloce è quello di "allineare" le chiamate di memoria. Ciò significa che il primo thread in un warp deve accedere al primo elemento nella banca della memoria condivisa e globale. Il secondo thread il secondo elemento e così via. Questa chiamata verrà ottimizzata in un'unica istruzione di trasferimento della memoria che copia l'intero banco nella memoria di destinazione in un colpo solo.



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow