2012-12-07 14 views
6

Vorrei leggere (BS_X + 1) * (BS_Y + 1) posizioni di memoria globale di BS_x * BS_Y discussioni spostando il contenuto nella memoria condivisa e ho sviluppato il seguente codice.Analisi dell'accesso alla memoria coalescenza del mio kernel CUDA

int i  = threadIdx.x; 
int j  = threadIdx.y; 
int idx  = blockIdx.x*BLOCK_SIZE_X + threadIdx.x; 
int idy  = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y; 

int index1 = j*BLOCK_SIZE_Y+i; 

int i1  = (index1)%(BLOCK_SIZE_X+1); 
int j1  = (index1)/(BLOCK_SIZE_Y+1); 

int i2  = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1); 
int j2  = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1); 

__shared__ double Ezx_h_shared_ext[BLOCK_SIZE_X+1][BLOCK_SIZE_Y+1];  

Ezx_h_shared_ext[i1][j1]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)]; 

if ((i2<(BLOCK_SIZE_X+1))&&(j2<(BLOCK_SIZE_Y+1))) 
Ezx_h_shared_ext[i2][j2]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j2)*xdim+(blockIdx.x*BLOCK_SIZE_X+i2)]; 

A mio parere, la coalescenza è l'equivalente parallelo di letture di memoria consecutive di elaborazione sequenziale. Come posso rilevare ora se gli accessi alla memoria globale sono coalizzati? Osservo che c'è un salto indice da (i1, j1) a (i2, j2). Grazie in anticipo.

risposta

5

Ho valutato gli accessi di memoria del codice con un analizzatore a coalescenza scritto a mano. La valutazione mostra che il codice in meno sfrutta la coalescenza. Qui è l'analizzatore coalescenza che si possono trovare utili:

#include <stdio.h> 
#include <malloc.h> 

typedef struct dim3_t{ 
    int x; 
    int y; 
} dim3; 


// KERNEL LAUNCH PARAMETERS 
#define GRIDDIMX 4 
#define GRIDDIMY 4 
#define BLOCKDIMX 16 
#define BLOCKDIMY 16 


// ARCHITECTURE DEPENDENT 
// number of threads aggregated for coalescing 
#define COALESCINGWIDTH 32 
// number of bytes in one coalesced transaction 
#define CACHEBLOCKSIZE 128 
#define CACHE_BLOCK_ADDR(addr,size) (addr*size)&(~(CACHEBLOCKSIZE-1)) 


int main(){ 
    // fixed dim3 variables 
    // grid and block size 
    dim3 blockDim,gridDim; 
    blockDim.x=BLOCKDIMX; 
    blockDim.y=BLOCKDIMY; 
    gridDim.x=GRIDDIMX; 
    gridDim.y=GRIDDIMY; 

    // counters 
    int unq_accesses=0; 
    int *unq_addr=(int*)malloc(sizeof(int)*COALESCINGWIDTH); 
    int total_unq_accesses=0; 

    // iter over total number of threads 
    // and count the number of memory requests (the coalesced requests) 
    int I, II, III; 
    for(I=0; I<GRIDDIMX*GRIDDIMY; I++){ 
     dim3 blockIdx; 
     blockIdx.x = I%GRIDDIMX; 
     blockIdx.y = I/GRIDDIMX; 
     for(II=0; II<BLOCKDIMX*BLOCKDIMY; II++){ 
      if(II%COALESCINGWIDTH==0){ 
       // new coalescing bunch 
       total_unq_accesses+=unq_accesses; 
       unq_accesses=0; 
      } 
      dim3 threadIdx; 
      threadIdx.x=II%BLOCKDIMX; 
      threadIdx.y=II/BLOCKDIMX; 

      //////////////////////////////////////////////////////// 
      // Change this section to evaluate different accesses // 
      //////////////////////////////////////////////////////// 
      // do your indexing here 
      #define BLOCK_SIZE_X BLOCKDIMX 
      #define BLOCK_SIZE_Y BLOCKDIMY 
      #define xdim 32 
      int i  = threadIdx.x; 
      int j  = threadIdx.y; 
      int idx  = blockIdx.x*BLOCK_SIZE_X + threadIdx.x; 
      int idy  = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y; 

      int index1 = j*BLOCK_SIZE_Y+i; 

      int i1  = (index1)%(BLOCK_SIZE_X+1); 
      int j1  = (index1)/(BLOCK_SIZE_Y+1); 

      int i2  = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1); 
      int j2  = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1); 
      // calculate the accessed location and offset here 
      // change the line "Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)];" to 
      int addr = (blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1); 
      int size = sizeof(double); 
      ////////////////////////// 
      // End of modifications // 
      ////////////////////////// 

      printf("tid (%d,%d) from blockid (%d,%d) accessing to block %d\n",threadIdx.x,threadIdx.y,blockIdx.x,blockIdx.y,CACHE_BLOCK_ADDR(addr,size)); 
      // check whether it can be merged with existing requests or not 
      short merged=0; 
      for(III=0; III<unq_accesses; III++){ 
       if(CACHE_BLOCK_ADDR(addr,size)==CACHE_BLOCK_ADDR(unq_addr[III],size)){ 
        merged=1; 
        break; 
       } 
      } 
      if(!merged){ 
       // new cache block accessed over this coalescing width 
       unq_addr[unq_accesses]=CACHE_BLOCK_ADDR(addr,size); 
       unq_accesses++; 
      } 
     } 
    } 
    printf("%d threads make %d memory transactions\n",GRIDDIMX*GRIDDIMY*BLOCKDIMX*BLOCKDIMY, total_unq_accesses); 
} 

Il codice verrà eseguito per ogni filo della rete e calcola il numero di richieste incorporate, metriche di accesso alla memoria coalescenza.

Per utilizzare l'analizzatore, incollare la parte di calcolo dell'indice del codice nell'area specificata e scomporre gli accessi di memoria (matrice) in "indirizzo" e "dimensione". Ho già fatto questo per il vostro codice in cui le indexings sono:

int i  = threadIdx.x; 
int j  = threadIdx.y; 
int idx  = blockIdx.x*BLOCK_SIZE_X + threadIdx.x; 
int idy  = blockIdx.y*BLOCK_SIZE_Y + threadIdx.y; 

int index1 = j*BLOCK_SIZE_Y+i; 

int i1  = (index1)%(BLOCK_SIZE_X+1); 
int j1  = (index1)/(BLOCK_SIZE_Y+1); 

int i2  = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)%(BLOCK_SIZE_X+1); 
int j2  = (BLOCK_SIZE_X*BLOCK_SIZE_Y+index1)/(BLOCK_SIZE_Y+1); 

e l'accesso alla memoria è:

Ezx_h_shared_ext[i1][j1]=Ezx_h[(blockIdx.y*BLOCK_SIZE_Y+j1)*xdim+(blockIdx.x*BLOCK_SIZE_X+i1)]; 

L'analizzatore riporta 4096 thread accedono al 4064 blocchi di cache. Esegui il codice per la tua attuale griglia e dimensione del blocco e analizza il comportamento coalescente.

+0

Piuttosto interessante! NVIDIA ha anche un SDK che fornisce l'accesso diretto ai contatori delle prestazioni nel chip. https://developer.nvidia.com/nvidia-perfkit –

+0

@RogerDahl Bello! La memoria a coalescenza ha qualche contro nel chip? – ahmad

+0

Penso che la coalescenza sia una delle cose derivate da altri contatori. Il profiler Nsight ha questo problema sugli esperimenti di memoria: "Seleziona questo gruppo di esperimenti per identificare i colli di bottiglia relativi alla memoria di un kernel.Per ogni spazio di memoria della gerarchia di memoria CUDA vengono raccolte le metriche chiave, tra cui coalescenza, conflitti bancari, L1/L2 i tassi di successo della cache e le larghezze di banda ottenute. " I documenti del kit di perforazione hanno delle belle carte che descrivono dettagliatamente i contatori. Quelli possono probabilmente essere usati per trovare come contare la coalescenza. –

1

Il visual profiler è un ottimo strumento per controllare il tuo lavoro. Dopo aver corretto funzionalmente un pezzo di codice, eseguilo dall'interno del visual profiler. Su Linux, ad esempio, supponendo di avere una sessione X, basta eseguire nvvp da una finestra di terminale. Verrà quindi fornita una procedura guidata che richiederà all'utente di applicare il profilo insieme ai parametri della riga di comando.

Il profiler eseguirà quindi una corsa di base della tua app per raccogliere statistiche. È anche possibile selezionare una raccolta statistica più avanzata (che richiede sessioni aggiuntive) e una di queste sarà la statistica di utilizzo della memoria. Riporterà l'utilizzo della memoria come percentuale del picco e contrassegnerà anche gli avvisi per quello che considera un basso utilizzo che merita la tua attenzione.

Se si dispone di un numero di utilizzo superiore al 50%, l'app probabilmente sta funzionando nel modo previsto. Se hai un numero basso, probabilmente hai perso alcuni dettagli a coalescenza. Riporterà le statistiche separatamente per le letture della memoria e le scritture di memoria. Per ottenere il 100% o vicino ad esso, dovrai anche assicurarti che le letture e le scritture coalesced del warp siano allineate su 128 byte.

Un errore comune in queste situazioni consiste nell'utilizzare la variabile basata su threadIdx.y come l'indice che cambia più rapidamente. Non mi sembra che tu abbia commesso quell'errore. per esempio. è un errore comune fare shared[threadIdx.x][threadIdx.y] poiché questo è spesso il modo in cui lo pensiamo in C. Ma i thread sono raggruppati prima nell'asse x, quindi vogliamo usare shared[threadIdx.y][threadIdx.x] o qualcosa di simile. Se fai questo errore, il tuo codice può essere ancora funzionalmente corretto, ma nel profiler otterrai numeri di utilizzo a bassa percentuale, come circa il 12% o anche il 3%.

E come già detto, per ottenere un valore superiore al 50% e vicino al 100%, è necessario assicurarsi che non solo tutte le richieste di thread siano adiacenti, ma che siano allineate su un limite di 128B. A causa delle cache L1/L2, queste non sono regole rigide e veloci, ma linee guida. Le cache possono attenuare alcuni errori, in una certa misura.

+0

Cosa intendi per "utilizzo"? Che tutta la memoria cache memorizzata dalla memoria globale sia completamente utilizzata? Grazie. – JackOLantern

+0

corretto. Ad esempio, quando una transazione di memoria viene attivata da una richiesta di lettura, un intero 128 byte viene normalmente recuperato dalla memoria. Se il mio ordito ha bisogno solo di una singola quantità a 32 bit, allora userò solo 4 di quei 128 byte. Se tutta la mia attività di lettura fosse così, vedrei una percentuale di utilizzo di 4/128 = 3.125% Ma se invece tutti e 32 i thread in ogni warp richiedano un valore adiacente a 32 bit dallo stesso blocco di 128 byte allo stesso tempo (un accesso * coalescente *, quindi il mio utilizzo sarebbe del 100%, che è l'ideale. –

2

Poiché le GPU si sono evolute, i requisiti per ottenere accessi a coalescenza sono diventati meno restrittivi. La descrizione degli accessi a coalescenza è più accurata per le architetture GPU precedenti rispetto a quelle più recenti. In particolare, Fermi (capacità di calcolo 2.0) ha allentato significativamente i requisiti. Su Fermi e successivi, non è importante accedere alle posizioni di memoria consecutivamente. Invece, l'attenzione si è spostata sull'accesso alla memoria con il minor numero possibile di transazioni di memoria. Su Fermi, le transazioni di memoria globale sono 128 byte di larghezza. Quindi, quando i 32 thread in un warp colpiscono un'istruzione che esegue un carico o un archivio, le transazioni a 128 byte saranno pianificate per servire tutti i thread nel warp. Le prestazioni dipendono quindi da quante transazioni sono necessarie. Se tutti i thread accedono ai valori all'interno di un'area di 128 byte allineata a 128 byte, è necessaria una singola transazione. Se tutti i thread accedono a valori in aree di 128 byte diverse, saranno necessarie 32 transazioni. Questo sarebbe lo scenario peggiore per soddisfare le richieste di una singola istruzione in un ordito.

È possibile utilizzare uno dei profiler CUDA per determinare la media per il numero di transazioni necessarie per soddisfare le richieste. Il numero dovrebbe essere il più vicino possibile a 1.Numeri più alti significano che dovresti vedere se ci sono opportunità per ottimizzare gli accessi alla memoria nel tuo kernel.

+0

Grazie. Secondo il codice di Ahmad, 4096 thread fanno 4064 transazioni. Quindi concluderei che il mio codice è piuttosto inefficiente. Ho ragione? – JackOLantern

+0

@ user1886641 Poiché i dati di ogni 16 thread si adattano a 128 byte, ogni warp deve inviare idealmente 2 richieste. Il caso ideale per il tuo codice è di inviare (4096/32) * 2 = 256 richieste. – ahmad

Problemi correlati