Come si scelgono le dimensioni della griglia e del blocco per i kernel CUDA?


112

Questa è una domanda su come determinare le dimensioni della griglia, del blocco e del filo CUDA. Questa è una domanda aggiuntiva a quella postata qui .

Seguendo questo collegamento, la risposta di talonmies contiene uno snippet di codice (vedi sotto). Non capisco il commento "valore solitamente scelto dalla messa a punto e dai vincoli hardware".

Non ho trovato una buona spiegazione o chiarimento che spieghi questo nella documentazione CUDA. In sintesi, la mia domanda è come determinare l'ottimale blocksize(numero di thread) dato il seguente codice:

const int n = 128 * 1024;
int blocksize = 512; // value usually chosen by tuning and hardware constraints
int nblocks = n / nthreads; // value determine by block size and total work
madd<<<nblocks,blocksize>>>mAdd(A,B,C,n);

Risposte:


148

Ci sono due parti in quella risposta (l'ho scritta io). Una parte è facile da quantificare, l'altra è più empirica.

Vincoli hardware:

Questa è la parte facile da quantificare. L'appendice F dell'attuale guida alla programmazione CUDA elenca una serie di limiti rigidi che limitano il numero di thread per blocco che un avvio del kernel può avere. Se superi uno di questi, il tuo kernel non verrà mai eseguito. Possono essere riassunti approssimativamente come:

  1. Ogni blocco non può avere più di 512/1024 thread in totale ( capacità di calcolo 1.xo 2.xe successive rispettivamente)
  2. Le dimensioni massime di ogni blocco sono limitate a [512,512,64] / [1024,1024,64] (Calcola 1.x / 2.x o successivo)
  3. Ogni blocco non può consumare più di 8k / 16k / 32k / 64k / 32k / 64k / 32k / 64k / 32k / 64k registri totali (Calcola 1.0,1.1 / 1.2,1.3 / 2.x- / 3.0 / 3.2 / 3.5-5.2 / 5.3 / 6-6,1 / 6,2 / 7,0)
  4. Ogni blocco non può consumare più di 16kb / 48kb / 96kb di memoria condivisa (Compute 1.x / 2.x-6.2 / 7.0)

Se rimani entro questi limiti, qualsiasi kernel che puoi compilare con successo verrà avviato senza errori.

Ottimizzazione delle prestazioni:

Questa è la parte empirica. Il numero di thread per blocco che scegli entro i vincoli hardware delineati sopra può influire sulle prestazioni del codice in esecuzione sull'hardware. Il modo in cui si comporta ogni codice sarà diverso e l'unico vero modo per quantificarlo è attraverso un'attenta analisi comparativa e profiling. Ma ancora una volta, riassunto in modo molto approssimativo:

  1. Il numero di thread per blocco dovrebbe essere un multiplo rotondo della dimensione di curvatura, che è 32 su tutto l'hardware corrente.
  2. Ogni unità multiprocessore di streaming sulla GPU deve avere un numero sufficiente di warp attivi per nascondere sufficientemente tutta la diversa latenza della pipeline di memoria e di istruzioni dell'architettura e ottenere il massimo throughput. L'approccio ortodosso qui è provare a ottenere un'occupazione hardware ottimale (cosa si riferisce la risposta di Roger Dahl ).

Il secondo punto è un argomento enorme che dubito che qualcuno proverà a coprirlo in una singola risposta StackOverflow. Ci sono persone che scrivono tesi di dottorato sull'analisi quantitativa degli aspetti del problema (vedi questa presentazione di Vasily Volkov della UC Berkley e questo articolo di Henry Wong dell'Università di Toronto per esempi di quanto sia complessa la domanda).

A livello di ingresso, dovresti principalmente essere consapevole del fatto che la dimensione del blocco che scegli (entro l'intervallo delle dimensioni dei blocchi legali definite dai vincoli sopra) può e ha un impatto sulla velocità di esecuzione del tuo codice, ma dipende dall'hardware hai e il codice che stai eseguendo. Con il benchmarking, probabilmente scoprirai che la maggior parte del codice non banale ha un "punto debole" nell'intervallo 128-512 thread per blocco, ma richiederà alcune analisi da parte tua per trovare dove si trova. La buona notizia è che, poiché stai lavorando su multipli della dimensione di curvatura, lo spazio di ricerca è molto limitato e la migliore configurazione per un dato pezzo di codice è relativamente facile da trovare.


2
"Il numero di thread per blocco deve essere un multiplo rotondo della dimensione di curvatura" questo non è un must, ma se non lo è si sprecano risorse. Ho notato che cudaErrorInvalidValue viene restituito da cudaGetLastError dopo un avvio del kernel con troppi blocchi (sembra che il calcolo 2.0 non possa gestire 1 miliardo di blocchi, il calcolo 5.0 può), quindi ci sono dei limiti anche qui.
masterxilo

4
Il tuo collegamento Vasili Volkov è morto. Presumo che ti sia piaciuto il suo articolo di settembre 2010: Better Performance at Lower Occupancy (attualmente disponibile su nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf ), C'è un bitbucket con il codice qui: bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

Le risposte sopra evidenziano come le dimensioni del blocco possono influire sulle prestazioni e suggeriscono un'euristica comune per la sua scelta basata sulla massimizzazione dell'occupazione. Senza voler fornire il criterio per scegliere la dimensione del blocco, varrebbe la pena ricordare che CUDA 6.5 (ora nella versione Release Candidate) include diverse nuove funzioni di runtime per aiutare nei calcoli di occupazione e nella configurazione del lancio, vedere

Suggerimento CUDA Pro: l'API Occupancy semplifica la configurazione del lancio

Una delle funzioni utili è cudaOccupancyMaxPotentialBlockSizeche calcola euristicamente una dimensione del blocco che raggiunge la massima occupazione. I valori forniti da tale funzione potrebbero quindi essere utilizzati come punto di partenza di un'ottimizzazione manuale dei parametri di lancio. Di seguito è riportato un piccolo esempio.

#include <stdio.h>

/************************/
/* TEST KERNEL FUNCTION */
/************************/
__global__ void MyKernel(int *a, int *b, int *c, int N) 
{ 
    int idx = threadIdx.x + blockIdx.x * blockDim.x; 

    if (idx < N) { c[idx] = a[idx] + b[idx]; } 
} 

/********/
/* MAIN */
/********/
void main() 
{ 
    const int N = 1000000;

    int blockSize;      // The launch configurator returned block size 
    int minGridSize;    // The minimum grid size needed to achieve the maximum occupancy for a full device launch 
    int gridSize;       // The actual grid size needed, based on input size 

    int* h_vec1 = (int*) malloc(N*sizeof(int));
    int* h_vec2 = (int*) malloc(N*sizeof(int));
    int* h_vec3 = (int*) malloc(N*sizeof(int));
    int* h_vec4 = (int*) malloc(N*sizeof(int));

    int* d_vec1; cudaMalloc((void**)&d_vec1, N*sizeof(int));
    int* d_vec2; cudaMalloc((void**)&d_vec2, N*sizeof(int));
    int* d_vec3; cudaMalloc((void**)&d_vec3, N*sizeof(int));

    for (int i=0; i<N; i++) {
        h_vec1[i] = 10;
        h_vec2[i] = 20;
        h_vec4[i] = h_vec1[i] + h_vec2[i];
    }

    cudaMemcpy(d_vec1, h_vec1, N*sizeof(int), cudaMemcpyHostToDevice);
    cudaMemcpy(d_vec2, h_vec2, N*sizeof(int), cudaMemcpyHostToDevice);

    float time;
    cudaEvent_t start, stop;
    cudaEventCreate(&start);
    cudaEventCreate(&stop);
    cudaEventRecord(start, 0);

    cudaOccupancyMaxPotentialBlockSize(&minGridSize, &blockSize, MyKernel, 0, N); 

    // Round up according to array size 
    gridSize = (N + blockSize - 1) / blockSize; 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Occupancy calculator elapsed time:  %3.3f ms \n", time);

    cudaEventRecord(start, 0);

    MyKernel<<<gridSize, blockSize>>>(d_vec1, d_vec2, d_vec3, N); 

    cudaEventRecord(stop, 0);
    cudaEventSynchronize(stop);
    cudaEventElapsedTime(&time, start, stop);
    printf("Kernel elapsed time:  %3.3f ms \n", time);

    printf("Blocksize %i\n", blockSize);

    cudaMemcpy(h_vec3, d_vec3, N*sizeof(int), cudaMemcpyDeviceToHost);

    for (int i=0; i<N; i++) {
        if (h_vec3[i] != h_vec4[i]) { printf("Error at i = %i! Host = %i; Device = %i\n", i, h_vec4[i], h_vec3[i]); return; };
    }

    printf("Test passed\n");

}

MODIFICARE

Il cudaOccupancyMaxPotentialBlockSizeè definito nel cuda_runtime.hfile ed è definito come segue:

template<class T>
__inline__ __host__ CUDART_DEVICE cudaError_t cudaOccupancyMaxPotentialBlockSize(
    int    *minGridSize,
    int    *blockSize,
    T       func,
    size_t  dynamicSMemSize = 0,
    int     blockSizeLimit = 0)
{
    return cudaOccupancyMaxPotentialBlockSizeVariableSMem(minGridSize, blockSize, func, __cudaOccupancyB2DHelper(dynamicSMemSize), blockSizeLimit);
}

Il significato dei parametri è il seguente

minGridSize     = Suggested min grid size to achieve a full machine launch.
blockSize       = Suggested block size to achieve maximum occupancy.
func            = Kernel function.
dynamicSMemSize = Size of dynamically allocated shared memory. Of course, it is known at runtime before any kernel launch. The size of the statically allocated shared memory is not needed as it is inferred by the properties of func.
blockSizeLimit  = Maximum size for each block. In the case of 1D kernels, it can coincide with the number of input elements.

Si noti che, a partire da CUDA 6.5, è necessario calcolare le proprie dimensioni del blocco 2D / 3D dalla dimensione del blocco 1D suggerita dall'API.

Si noti inoltre che l'API del driver CUDA contiene API funzionalmente equivalenti per il calcolo dell'occupazione, quindi è possibile utilizzarle cuOccupancyMaxPotentialBlockSizenel codice dell'API del driver nello stesso modo mostrato per l'API di runtime nell'esempio sopra.


2
Ho due domande. In primo luogo, quando si dovrebbe scegliere la dimensione della griglia come minGridSize rispetto a gridSize calcolata manualmente. In secondo luogo hai detto che "I valori forniti da quella funzione potrebbero essere usati come punto di partenza di un'ottimizzazione manuale dei parametri di lancio." - vuoi dire che i parametri di lancio devono ancora essere ottimizzati manualmente?
nurabha

C'è qualche guida su come calcolare le dimensioni del blocco 2D / 3D? Nel mio caso sto cercando le dimensioni del blocco 2D. È solo un caso di calcolo dei fattori xey quando moltiplicati insieme danno la dimensione del blocco originale?
Graham Dawes

1
@GrahamDawes questo potrebbe interessarti.
Robert Crovella

9

La dimensione del blocco viene solitamente selezionata per massimizzare l '"occupazione". Cerca su CUDA Occupancy per ulteriori informazioni. In particolare, vedere il foglio di calcolo del calcolatore di occupazione CUDA.

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.