Как выбрать размеры сетки и блока для ядер CUDA?

это вопрос о том, как определить размеры сетки, блока и потока CUDA. Это дополнительный вопрос к тому, который размещен здесь:

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

следуя по этой ссылке, ответ от 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);

кстати, я начал свой вопрос со ссылки выше, потому что она частично отвечает на мой первый вопрос. Если это не правильный способ задавать вопросы о переполнении стека, пожалуйста, извините или посоветуйте мне.

3 ответов


есть две части этого ответа (я написал его). Одна часть легко поддается количественной оценке, другая более эмпирическая.

Аппаратные Ограничения:

Это легко количественно определить часть. В приложении F к текущему руководству по программированию CUDA перечислен ряд жестких ограничений, которые ограничивают количество потоков на блок запуска ядра. Если вы превысите любой из них, ядро никогда не будет работать. Их можно приблизительно резюмировать следующим образом:

  1. каждый блок может быть чем 512/1024 потоков в общей сложности (Вычислительные Возможности 1.x или 2.x и позже соответственно)
  2. максимальные размеры каждого блока ограничено [512,512,64] / [1024,1024,64] (вычислить 1.x / 2.x или позже)
  3. каждый блок не может потреблять больше, чем 8K/16k/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. каждый блок не может потреблять больше, чем 16kb/48kb / 96kb общей памяти (Вычислять 1.x / 2.x-6.2 / 7.0)

Если вы останетесь в этих пределах, любое ядро, которое вы можете успешно скомпилировать, запустится без ошибок.

Настройки Производительность:

Это эмпирическая часть. Количество потоков на блок, выбранное в рамках аппаратных ограничений, описанных выше, может и влияет на производительность кода, выполняемого на оборудовании. Как каждый код ведет себя будет отличаться, и единственный реальный способ его количественной оценки-это тщательный бенчмаркинг и профилирование. Но опять же, очень грубо резюмировал:

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

второй момент-огромная тема, которую я сомневаюсь, что кто-то попытается охватить ее в одном ответе StackOverflow. Есть люди, пишущие кандидатские диссертации вокруг количественного анализа аспектов проблемы (см. презентации Василий Волков из UC Berkley и этой статье Генри Вонг из Университета Торонто для примеров того, как сложный вопрос на самом деле).

на начальном уровне, вы должны, в основном, следует учитывать, что размер блока выбрать (в пределах правового размеры блоков определяются ограничения выше) может и не повлиять на как быстро ваш код будет работать, но это зависит от оборудования и кода. По бенчмаркингу вы, вероятно, обнаружите, что у большинства нетривиальных кодов есть "сладкое пятно" в потоках 128-512 на диапазон блоков, но вам потребуется некоторый анализ с вашей стороны, чтобы найти, где это. Этот хорошей новостью является то, что, поскольку вы работаете в кратных размерах деформации, пространство поиска очень ограничено и лучшая конфигурация для данного фрагмента кода относительно проста в поиске.


ответы выше указывают, как размер блока может повлиять на производительность и предложить общую эвристику для его выбора на основе максимизации занятости. Не желая предоставлять на критерий чтобы выбрать размер блока, стоит отметить, что CUDA 6.5 (теперь в версии-кандидате на выпуск) включает в себя несколько новых функций времени выполнения, чтобы помочь в вычислениях занятости и конфигурации запуска, см.

Совет 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");

}

редактировать

на cudaOccupancyMaxPotentialBlockSize определена в cuda_runtime.h file и определяется следующим образом:

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 среды выполнения в примере выше.


blocksize обычно выбирается для максимизации "занятости". Поиск по занятости CUDA для получения дополнительной информации. В частности, см. таблицу калькулятора занятости CUDA.