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 сохраняет индекс потока.

  1. на for loop используется как число нити меньше, чем размер массива.
  2. цикл while используется для итерации Cd[n].
  3. я не использовал общую память, потому что я взял много блоков и потоков. Таким образом, объем общей памяти, необходимой для каждого блока, невелик.

PS: если устройство поддерживает больше потоков и блоков, замените k<Length/500 С k<Length/(supported number of threads)