Як вибрати розміри сітки та блоку для ядер CUDA?


112

Це питання про те, як визначити розміри сітки, блоку та нитки CUDA. Це додаткове запитання до поставленого тут .

Перейшовши за цим посиланням, відповідь від talonmies містить фрагмент коду (див. Нижче). Я не розумію коментар "значення, яке зазвичай вибирається за допомогою настройки та апаратних обмежень".

Я не знайшов хорошого пояснення чи уточнення, яке пояснює це в документації CUDA. Підсумовуючи, моє запитання полягає в тому, як визначити оптимальний blocksize(кількість ниток) з урахуванням наступного коду:

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

Відповіді:


148

На цю відповідь є дві частини (я це написав). Одну частину легко оцінити, іншу - більш емпірично.

Обмеження обладнання:

Це просто визначити кількість. У Додатку F до поточного посібника з програмування CUDA перераховано ряд жорстких меж, які обмежують кількість потоків на блок запуску ядра. Якщо ви перевищите будь-яке з них, ваше ядро ​​ніколи не буде працювати. Їх можна приблизно скласти як:

  1. Кожен блок не може містити більше 512/1024 потоків ( Compute Capability 1.x або 2.x і пізніше відповідно)
  2. Максимальні розміри кожного блоку обмежені [512,512,64] / [1024,1024,64] (Обчислити 1.x / 2.x або пізніші)
  3. Кожен блок не може споживати більше 8k / 16k / 32k / 64k / 32k / 64k / 32k / 64k / 32k / 64k регістрів загалом (Обчислити 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. Кожен блок не може споживати більше 16 кбіт / 48 кб / 96 кб спільної пам'яті (обчислити 1.x / 2.x-6.2 / 7.0)

Якщо ви залишитесь у цих межах, будь-яке ядро, яке ви зможете успішно скласти, запуститься без помилок.

Налаштування продуктивності:

Це емпірична частина. Кількість потоків у блоці, які ви обираєте в межах апаратних обмежень, зазначених вище, може і впливати на ефективність роботи коду на апаратному забезпеченні. Поведінка кожного коду буде різною, і єдиний реальний спосіб його кількісного визначення - це ретельне тестування та профілювання. Але знову ж таки, приблизно приблизно:

  1. Кількість потоків на один блок має бути круглим кратним розміру основи, що становить 32 на всіх поточних пристроях.
  2. Кожен потоковий багатопроцесорний блок на графічному процесорі повинен мати достатньо активних деформацій, щоб достатньо приховати всі різні затримки архітектури пам’яті та інструкцій та досягти максимальної пропускної здатності. Ортодоксальний підхід полягає в тому, щоб спробувати досягти оптимальної апаратури (на що йдеться у відповіді Роджера Даля ).

Другий момент - це величезна тема, яку я сумніваюся, хтось спробує висвітлити в одній відповіді StackOverflow. Люди пишуть кандидатські дисертації навколо кількісного аналізу аспектів проблеми (дивіться цю презентацію Василя Волкова з UC Berkley та цю статтю Генрі Вонга з Університету Торонто для прикладів того, наскільки складним є питання насправді).

На рівні входу вам слід в основному знати, що обраний вами розмір блоку (в межах легальних розмірів блоків, визначених вище обмеженнями) може і впливає на те, як швидко буде працювати ваш код, але це залежить від обладнання у вас є і код, який ви працюєте. За допомогою бенчмаркінгу, ймовірно, ви побачите, що більшість нетривіальних кодів має "солодке місце" у 128-512 потоках на діапазоні блоків, але для цього знадобиться певний аналіз, щоб знайти, де це. Хороша новина полягає в тому, що оскільки ви працюєте в кратних розмірах основи, простір пошуку дуже обмежений, а найкращу конфігурацію для заданого фрагмента коду знайти досить просто.


2
"Кількість ниток в блоці має бути круглим кратним розміру основи", це не обов'язково, але ви витрачаєте ресурси, якщо це не так. Я помітив, що cudaErrorInvalidValue повертається cudaGetLastError після запуску ядра із занадто великою кількістю блоків (схоже, обчислювач 2.0 не може обробити 1 мільярд блоків, обчислити 5.0 може) - тому тут є і обмеження.
masterxilo

4
Ваш посилання Василь Волков мертвий. Я припускаю, що вам сподобався його стаття у вересні 2010 року: Краща ефективність роботи на нижчій посаді (зараз її можна знайти на nvidia.com/content/gtc-2010/pdfs/2238_gtc2010.pdf ), тут є бітбукет з кодом: bitbucket.org/rvuduc/volkov -gtc10
ofer.sheffer

37

Відповіді вище вказують на те, як розмір блоку може впливати на продуктивність, і пропонують загальну евристику для його вибору на основі максимізації кількості. Не бажаючи надати своє критерій вибору розміру блоку, то варто було б відзначити , що CUDA 6.5 (зараз в Release Candidate версії) включає в себе кілька нових функцій у час виконання , щоб допомогти в розрахунках зайнятості та конфігурації запуску, см

Порада CUDA Pro: API зайнятості спрощує конфігурацію запуску

Однією з корисних функцій є те, cudaOccupancyMaxPotentialBlockSizeщо евристично обчислює розмір блоку, який досягає максимальної зайнятості. Значення, надані цією функцією, можуть бути використані в якості вихідної точки ручної оптимізації параметрів запуску. Нижче наведено невеликий приклад.

#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Визначається в cuda_runtime.hфайлі і визначається наступним чином :

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

Значення параметрів таке

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.

Зауважте, що, як і для CUDA 6.5, потрібно обчислити власні розміри блоків 2D / 3D з розміру блоку 1D, запропонованого API.

Зауважимо також, що API драйверів CUDA містить функціонально еквівалентні API для обчислення кількості, тому їх можна використовувати cuOccupancyMaxPotentialBlockSize код API драйвера таким же чином, як і для API виконання програми у наведеному вище прикладі.


2
У мене два питання. По-перше, коли слід вибрати розмір сітки як minGridSize над розрахованим вручну gridSize. По-друге, ви згадали, що "Значення, що надаються цією функцією, можуть бути використані в якості вихідної точки ручної оптимізації параметрів запуску". - Ви маєте на увазі параметри запуску все ще потрібно вручну оптимізувати?
нурабха

Чи є вказівки щодо розрахунку розмірів блоку 2D / 3D? У моєму випадку я шукаю розміри блоків 2D. Чи просто випадок обчислення факторів x і y при множенні разом дають початковий розмір блоку?
Грем Дауес

1
@GrahamDawes це може зацікавити.
Роберт Кровелла

9

Зазвичай розмір блоків вибирається для досягнення максимальної «заповненості». Для отримання додаткової інформації знайдіть на сайті CUDA. Зокрема, див. Таблицю калькулятора зайнятості CUDA.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.