У меня возникла идея о параллельном сокращении, основанном на warp, поскольку все потоки warp синхронизируются по определению.
Итак, идея заключалась в том, что входные данные могут быть уменьшены в 64 раз (каждый поток уменьшает два элемента) без какой-либо необходимости синхронизации.
То же, что и оригинальная реализация Марк Харрис, сокращение применяется на уровне блока, а данные - в общей памяти. http://gpgpu.org/static/sc2007/SC07_CUDA_5_Optimization_Harris.pdf
Я создал ядро для проверки его версии и моей версии на основе основы.
Ядро полностью идентично хранит элементы BLOCK_SIZE в общей памяти и выводит его результат на свой уникальный индекс блока в выходном массиве.
Сам алгоритм отлично работает. Протестировано с полным массивом, чтобы проверить "подсчет".
Тело функции реализаций:
/**
* Performs a parallel reduction with operator add
* on the given array and writes the result with the thread 0
* to the given target value
*
* @param inValues T* Input float array, length must be a multiple of 2 and equal to blockDim.x
* @param targetValue float
*/
__device__ void reductionAddBlockThread_f(float* inValues,
float &outTargetVar)
{
// code of the below functions
}
1. Реализация его версии:
if (blockDim.x >= 1024 && threadIdx.x < 512)
inValues[threadIdx.x] += inValues[threadIdx.x + 512];
__syncthreads();
if (blockDim.x >= 512 && threadIdx.x < 256)
inValues[threadIdx.x] += inValues[threadIdx.x + 256];
__syncthreads();
if (blockDim.x >= 256 && threadIdx.x < 128)
inValues[threadIdx.x] += inValues[threadIdx.x + 128];
__syncthreads();
if (blockDim.x >= 128 && threadIdx.x < 64)
inValues[threadIdx.x] += inValues[threadIdx.x + 64];
__syncthreads();
//unroll last warp no sync needed
if (threadIdx.x < 32)
{
if (blockDim.x >= 64) inValues[threadIdx.x] += inValues[threadIdx.x + 32];
if (blockDim.x >= 32) inValues[threadIdx.x] += inValues[threadIdx.x + 16];
if (blockDim.x >= 16) inValues[threadIdx.x] += inValues[threadIdx.x + 8];
if (blockDim.x >= 8) inValues[threadIdx.x] += inValues[threadIdx.x + 4];
if (blockDim.x >= 4) inValues[threadIdx.x] += inValues[threadIdx.x + 2];
if (blockDim.x >= 2) inValues[threadIdx.x] += inValues[threadIdx.x + 1];
//set final value
if (threadIdx.x == 0)
outTargetVar = inValues[0];
}
Ressources:
4 используемых syncthreads
12, если утверждения используются
11 читать + добавлять + писать операции
1 окончательная операция записи
5 использование регистра
Производительность:
пять тестовых пробегов: ~ 19,54 мс
2. Основанный на Warp подход: (Те же функциональные тела, что и выше)
/*
* Perform first warp based reduction by factor of 64
*
* 32 Threads per Warp -> LOG2(32) = 5
*
* 1024 Threads / 32 Threads per Warp = 32 warps
* 2 elements compared per thread -> 32 * 2 = 64 elements per warp
*
* 1024 Threads/elements divided by 64 = 16
*
* Only half the warps/threads are active
*/
if (threadIdx.x < blockDim.x >> 1)
{
const unsigned int warpId = threadIdx.x >> 5;
// alternative threadIdx.x & 31
const unsigned int threadWarpId = threadIdx.x - (warpId << 5);
const unsigned int threadWarpOffset = (warpId << 6) + threadWarpId;
inValues[threadWarpOffset] += inValues[threadWarpOffset + 32];
inValues[threadWarpOffset] += inValues[threadWarpOffset + 16];
inValues[threadWarpOffset] += inValues[threadWarpOffset + 8];
inValues[threadWarpOffset] += inValues[threadWarpOffset + 4];
inValues[threadWarpOffset] += inValues[threadWarpOffset + 2];
inValues[threadWarpOffset] += inValues[threadWarpOffset + 1];
}
// synchronize all warps - the local warp result is stored
// at the index of the warp equals the first thread of the warp
__syncthreads();
// use first warp to reduce the 16 warp results to the final one
if (threadIdx.x < 8)
{
// get first element of a warp
const unsigned int warpIdx = threadIdx.x << 6;
if (blockDim.x >= 1024) inValues[warpIdx] += inValues[warpIdx + 512];
if (blockDim.x >= 512) inValues[warpIdx] += inValues[warpIdx + 256];
if (blockDim.x >= 256) inValues[warpIdx] += inValues[warpIdx + 128];
if (blockDim.x >= 128) inValues[warpIdx] += inValues[warpIdx + 64];
//set final value
if (threadIdx.x == 0)
outTargetVar = inValues[0];
}
Ressources:
1 используемый syncthread
7 если утверждения
10 читать добавить операции записи
1 окончательная операция записи
5 использование регистра
5-битные сдвиги
1 добавить
1 sub
Производительность:
пять тестовых пробегов: ~ 20,82 мс
Тестирование обоих ядер несколько раз на Geforce 8800 GT 512 МБ с 256 mb значений float. И работающее ядро с 256 потоками на блок (100% заполняемость).
версия на основе warp ~ 1,28 миллисекунды медленнее.
Если будущая карта допускает большие размеры блоков, основанный на основах подход все равно не нуждается в дополнительной инструкции синхронизации, так как max 4096, который уменьшается до 64, которые уменьшаются на окончательный warp до 1
Почему это не быстрее?, или где ошибка в идее, ядро?
Из использования ressources подход warp должен быть впереди?
Edit1: Исправлено ядро, в котором активна только половина потоков, что не приводит к ошибкам чтения, добавляет новые данные о производительности