Как выбрать размеры сетки и блока для ядер 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);


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

1026   3  

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/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. каждый потоковый многопроцессорный блок на GPU должен иметь достаточно активных искажений, чтобы достаточно скрыть все различные задержки конвейера памяти и команд архитектуры и достичь максимальной пропускной способности. Ортодоксальный подход здесь заключается в том, чтобы попытаться достичь оптимальной загрузки оборудования (что ответ Роджера Даля имеет в виду).

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

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

ответы выше указывают на то, как размер блока может повлиять на производительность и предложить общую эвристику для его выбора на основе максимизации занятости. Не желая предоставлять the критерий для выбора размера блока, стоит отметить, что 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

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

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

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

Comments

    Ничего не найдено.