1D Min-свертка в CUDA
У меня есть два массива, a и b, и я хотел бы вычислить "минимальную свертку" для получения результата c. Простой псевдо-код выглядит следующим образом:
for i = 0 to size(a)+size(b)
c[i] = inf
for j = 0 to size(a)
if (i - j >= 0) and (i - j < size(b))
c[i] = min(c[i], a[j] + b[i-j])
(edit: изменены циклы, чтобы начать с 0 вместо 1)
Если бы min был суммой, мы могли бы использовать быстрое преобразование Фурье (FFT), но в случае min такого аналога нет. Вместо этого я хотел бы сделать этот простой алгоритм как можно быстрее, используя GPU (CUDA). Я был бы счастлив найти существование код, который делает это (или код, который реализует случай суммы без FFTs, чтобы я мог адаптировать его для своих целей), но мой поиск до сих пор не дал никаких хороших результатов. Мой вариант использования будет включать a и b размером от 1000 до 100 000.
вопросы:
код для этого эффективно уже существует?
Если я собираюсь реализовать это сам, структурно, как должно выглядеть ядро CUDA, чтобы максимизировать эффективность? Я пробовал простое решение, где каждый c [i] вычисляется отдельным потоком, но это не кажется лучшим способом. Любые советы по настройке структуры блоков потоков и шаблонов доступа к памяти?
3 ответов
альтернативу, которая может быть полезна для больших a
и b
можно использовать блок на выходную запись в c
. Использование блока позволяет коалесцировать память, что будет важно в том, что является ограниченной пропускной способностью памяти, и довольно эффективное сокращение общей памяти может использоваться для объединения частичных результатов на поток в конечный результат на блок. Вероятно, лучшая стратегия-запустить столько блоков на MP, сколько будет работать одновременно и иметь каждый блок испускает несколько выходных точек. Это устраняет некоторые накладные расходы планирования, связанные с запуском и удалением многих блоков с относительно низким общим количеством команд.
пример как это можно сделать:
#include <math.h>
template<int bsz>
__global__ __launch_bounds__(512)
void minconv(const float *a, int sizea, const float *b, int sizeb, float *c)
{
__shared__ volatile float buff[bsz];
for(int i = blockIdx.x; i<(sizea + sizeb); i+=(gridDim.x*blockDim.x)) {
float cval = INFINITY;
for(int j=threadIdx.x; j<sizea; j+= blockDim.x) {
int t = i - j;
if ((t>=0) && (t<sizeb))
cval = min(cval, a[j] + b[t]);
}
buff[threadIdx.x] = cval; __syncthreads();
if (bsz > 256) {
if (threadIdx.x < 256)
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+256]);
__syncthreads();
}
if (bsz > 128) {
if (threadIdx.x < 128)
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+128]);
__syncthreads();
}
if (bsz > 64) {
if (threadIdx.x < 64)
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+64]);
__syncthreads();
}
if (threadIdx.x < 32) {
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+32]);
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+16]);
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+8]);
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+4]);
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+2]);
buff[threadIdx.x] = min(buff[threadIdx.x], buff[threadIdx.x+1]);
if (threadIdx.x == 0) c[i] = buff[0];
}
}
}
// Instances for all valid block sizes.
template __global__ void minconv<64>(const float *, int, const float *, int, float *);
template __global__ void minconv<128>(const float *, int, const float *, int, float *);
template __global__ void minconv<256>(const float *, int, const float *, int, float *);
template __global__ void minconv<512>(const float *, int, const float *, int, float *);
[отказ от ответственности: не проверял или протестированные, используйте на свой страх и риск]
это одна точность с плавающей запятой, но та же идея должна работать для двойной точности с плавающей запятой. Для integer вам нужно будет заменить C99 INFINITY
макрос с чем-то вроде INT_MAX
или LONG_MAX
, но принцип остается тем же самым в противном случае.
более быстрая версия:
__global__ void convAgB(double *a, double *b, double *c, int sa, int sb)
{
int i = (threadIdx.x + blockIdx.x * blockDim.x);
int idT = threadIdx.x;
int out,j;
__shared__ double c_local [512];
c_local[idT] = c[i];
out = (i > sa) ? sa : i + 1;
j = (i > sb) ? i - sb + 1 : 1;
for(; j < out; j++)
{
if(c_local[idT] > a[j] + b[i-j])
c_local[idT] = a[j] + b[i-j];
}
c[i] = c_local[idT];
}
**Benckmark:**
Size A Size B Size C Time (s)
1000 1000 2000 0.0008
10k 10k 20k 0.0051
100k 100k 200k 0.3436
1M 1M 1M 43,327
Старая Версия, Для размеров между 1000 и 100000 я тестировал эту наивную версию:
__global__ void convAgB(double *a, double *b, double *c, int sa, int sb)
{
int size = sa+sb;
int idT = (threadIdx.x + blockIdx.x * blockDim.x);
int out,j;
for(int i = idT; i < size; i += blockDim.x * gridDim.x)
{
if(i > sa) out = sa;
else out = i + 1;
if(i > sb) j = i - sb + 1;
else j = 1;
for(; j < out; j++)
{
if(c[i] > a[j] + b[i-j])
c[i] = a[j] + b[i-j];
}
}
}
я заполнил массив a
и b
С некоторыми случайными двойными числами и c
С 999999 (только для тестирования). Я подтвердил c
array (в CPU) с помощью вашей функции (без каких-либо изменений).
я также удалил conditionals изнутри внутреннего цикла, поэтому он будет проверь их только один раз.
я не уверен на 100%, но я думаю, что следующая модификация имеет смысл. С вами был i - j >= 0
, что то же самое, что i >= j
, это означает, что как только j > i
он никогда не войдет в этот блок 'X' (начиная с j++):
if(c[i] > a[j] + b[i-j])
c[i] = a[j] + b[i-j];
поэтому я рассчитал переменную out
условный цикл if i > sa
, что означает, что цикл завершится, когда j == sa
, если i < sa
это означает, что цикл завершится (раньше) на i + 1
из-за условие i >= j
.
другое условие i - j < size(b)
означает, что вы начнете выполнение блока "X", когда i > size(b) + 1
С j
начинается всегда = 1. Так что мы можем поставить j
значением, которое должно начинаться, таким образом
if(i > sb) j = i - sb + 1;
else j = 1;
посмотреть, если вы можете проверить эту версию с реальными массивами данных, и дать мне обратную связь. Кроме того, любые улучшения приветствуются.
редактировать : новая оптимизация может быть реализовано, но это одно не имеет большого значения.
if(c[i] > a[j] + b[i-j])
c[i] = a[j] + b[i-j];
мы можем устранить if, по:
double add;
...
for(; j < out; j++)
{
add = a[j] + b[i-j];
c[i] = (c[i] < add) * c[i] + (add <= c[i]) * add;
}
наличие:
if(a > b) c = b;
else c = a;
это то же самое, что иметь c = (a
если a > b, то c = 0 * a + 1 * b; => c = b; если a c = a;
**Benckmark:**
Size A Size B Size C Time (s)
1000 1000 2000 0.0013
10k 10k 20k 0.0051
100k 100k 200k 0.4436
1M 1M 1M 47,327
я измеряю время копирования с CPU на GPU, запуск ядра и копирование с GPU на CPU.
GPU Specifications
Device Tesla C2050
CUDA Capability Major/Minor 2.0
Global Memory 2687 MB
Cores 448 CUDA Cores
Warp size 32
я использовал ваш алгоритм. Думаю, это тебе поможет.
const int Length=1000;
__global__ void OneD(float *Ad,float *Bd,float *Cd){
int i=blockIdx.x;
int j=threadIdx.x;
Cd[i]=99999.99;
for(int k=0;k<Length/500;k++){
while(((i-j)>=0)&&(i-j<Length)&&Cd[i+k*Length]>Ad[j+k*Length]+Bd[i-j]){
Cd[i+k*Length]=Ad[j+k*Length]+Bd[i-j];
}}}
Я взял 500 потоков на блок. И, 500 блоков на сетку. Поскольку количество потоков на блок в моем устройстве ограничено 512, я использовал 500 потоков. Я взял размер всех массивов как Length
(=1000).
работает:
1. i
сохраняет индекс блока и j
сохраняет индекс потока.
- на
for
loop используется как число нити меньше, чем размер массива. - цикл while используется для итерации
Cd[n]
. - я не использовал общую память, потому что я взял много блоков и потоков. Таким образом, объем общей памяти, необходимой для каждого блока, невелик.
PS: если устройство поддерживает больше потоков и блоков, замените k<Length/500
С k<Length/(supported number of threads)