Jak wybrać wymiary siatki i bloku dla jąder CUDA?

Jest to pytanie o to, jak określić rozmiar siatki CUDA, bloku i gwintu. Jest to dodatkowe pytanie do tego zamieszczonego tutaj:

Https://stackoverflow.com/a/5643838/1292251

Po tym linku, odpowiedź od talonmies zawiera fragment kodu(patrz poniżej). Nie rozumiem komentarza "wartość zazwyczaj wybierana przez tuning i ograniczenia sprzętowe".

Nie znalazłem dobrego wyjaśnienia lub wyjaśnienia, które wyjaśniałoby to w CUDA dokumentacja. Podsumowując, moje pytanie brzmi: jak określić optymalny Rozmiar bloku (=liczba wątków) biorąc pod uwagę następujący kod:

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);

BTW, zacząłem moje pytanie od linku powyżej, ponieważ częściowo odpowiada na moje pierwsze pytanie. Jeśli nie jest to właściwy sposób zadawania pytań na temat przepełnienia stosu, proszę wybaczyć lub doradzić mi.

Author: Community, 2012-04-03

3 answers

Są dwie części tej odpowiedzi (ja ją napisałem). Jedna część jest łatwa do oszacowania, druga jest bardziej empiryczna.

Ograniczenia Sprzętowe:

Jest to łatwa do oszacowania część. Dodatek F aktualnego przewodnika po programowaniu CUDA zawiera listę twardych limitów, które ograniczają liczbę wątków na blok, jakie może mieć uruchomienie jądra. Jeśli przekroczysz którąkolwiek z tych wartości, twoje jądro nigdy nie będzie działać. Można je z grubsza podsumować jako:

  1. Każdy blok nie może mieć więcej niż 512/1024 wątków w całkowita ( zdolność obliczeniowa 1.x lub 2.x i później odpowiednio)
  2. maksymalne wymiary każdego bloku są ograniczone do [512.512.64]/[1024.1024.64] (Oblicz 1.x / 2.x lub później)
  3. Każdy blok nie może zużywać więcej niż 8K/16K/32k/64k/32K/64K/32K/64K / 32K / 64K (Oblicz 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. Każdy blok nie może zużywać więcej niż 16KB/48kb / 96kb pamięci współdzielonej (Oblicz 1.x / 2.x-6.2 / 7.0)

If you stay within te ograniczenia, każde jądro, które można pomyślnie skompilować, uruchomi się bez błędu.

Tuning Wydajności:

To jest część empiryczna. Liczba wątków na blok, którą wybierzesz w ramach ograniczeń sprzętowych opisanych powyżej, może i ma wpływ na wydajność kodu działającego na sprzęcie. Sposób zachowania każdego kodu będzie inny i jedynym prawdziwym sposobem na jego oszacowanie jest staranne benchmarking i profilowanie. Ale znowu bardzo zgrubnie podsumowane:

  1. liczba wątki na blok powinny być okrągłą wielokrotnością rozmiaru osnowy, która wynosi 32 na całym obecnym sprzęcie.
  2. każda jednostka wieloprocesorowa strumieniowa na GPU musi mieć wystarczająco dużo aktywnych WARP, aby wystarczająco ukryć wszystkie opóźnienia w pamięci i potoku instrukcji w architekturze i osiągnąć maksymalną przepustowość. Ortodoksyjnym podejściem jest próba osiągnięcia optymalnego zajętości sprzętu (do czego odnosi się odpowiedź Rogera Dahla ).

Drugi punkt to ogromny temat, w który wątpię, aby ktokolwiek spróbował go opisać w jednej odpowiedzi StackOverflow. Istnieją ludzie piszący prace doktorskie wokół ilościowej analizy aspektów problemu (patrz ta prezentacja Wasilij Wołkow z UC Berkley i ten artykuł Henry Wong z University of Toronto dla przykładów jak skomplikowane pytanie jest naprawdę).

Na poziomie podstawowym powinieneś przede wszystkim mieć świadomość, że wybrany rozmiar bloku (w zakresie legalnego bloku rozmiary zdefiniowane przez ograniczenia powyżej) może i ma wpływ na szybkość działania kodu, ale zależy to od posiadanego sprzętu i kodu, który jest uruchamiany. Dzięki benchmarkingowi prawdopodobnie zauważysz, że większość nietrywialnych kodów ma "sweet spot" w 128-512 wątkach na zakres bloków, ale będzie to wymagało analizy z twojej strony, aby znaleźć, gdzie to jest. Dobrą wiadomością jest to, że ponieważ pracujesz w wielokrotnościach wielkości warp, przestrzeń wyszukiwania jest bardzo skończona i najlepsza konfiguracja dla danego fragmentu kodu stosunkowo łatwa do znalezienia.

 122
Author: talonmies,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-01-10 14:42:28

Powyższe odpowiedzi wskazują, w jaki sposób rozmiar bloku może wpłynąć na wydajność i sugerują powszechną heurystykę wyboru w oparciu o maksymalizację zajętości. Nie chcąc podawać kryterium do wyboru rozmiaru bloku, warto wspomnieć, że CUDA 6.5 (teraz w wersji Release Candidate) zawiera kilka nowych funkcji wykonawczych, które pomagają w obliczeniach zajętości i konfiguracji uruchamiania, Zobacz

CUDA Pro Tip: Occupancy API upraszcza uruchomienie Konfiguracja

Jedną z przydatnych funkcji jest cudaOccupancyMaxPotentialBlockSize, która heurystycznie oblicza rozmiar bloku, który osiąga maksymalne obłożenie. Wartości podane przez tę funkcję mogą być następnie wykorzystane jako punkt początkowy ręcznej optymalizacji parametrów startu. Poniżej mały przykład.

#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");

}

EDIT

cudaOccupancyMaxPotentialBlockSize jest zdefiniowany w pliku cuda_runtime.h i jest zdefiniowany w następujący sposób:

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);
}

Znaczenie parametrów to po

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.

Zauważ, że od CUDA 6.5, trzeba obliczyć własne wymiary bloków 2D/3D z rozmiaru bloku 1D sugerowanego przez API.

Zauważ również, że CUDA driver API zawiera funkcjonalnie równoważne API do obliczania obłożenia, więc możliwe jest użycie cuOccupancyMaxPotentialBlockSize w kodzie driver API w taki sam sposób jak w powyższym przykładzie dla runtime API.

 30
Author: JackOLantern,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-09-13 07:28:45

Rozmiar bloku jest zwykle wybierany, aby zmaksymalizować "obłożenie". Szukaj na cuda obłożenie więcej informacji. W szczególności zobacz arkusz kalkulacyjny cuda Occupancy Calculator.

 10
Author: Roger Dahl,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2012-04-03 01:40:34