Намаляване на CUDA - атомарно срещу сумиране с една нишка

Наскоро тествах алгоритъма за намаляване с помощта на CUDA (този, който можете да намерите например на http://www.cuvilib.com/Reduction.pdf, страница 16). Но в края на това се натъкнах на проблеми, когато не използвах атомарност. Така че основно правя сбора на всеки блок и го съхранявам в споделен масив. След това го връщам в глобалния масив x (tdx е threadIndex.x, а i е глобален индекс).

if(i==0){
        *sum = 0.; // Initialize to 0
    }
__syncthreads();
if (tdx == 0){       
    x[blockIdx.x] = s_x[tdx]; //get the shared sums in global memory
}
__syncthreads();

След това искам да сумирам първите x елемента (колкото имам блокове). Когато правя с атомарност, работи добре (същия резултат като процесора), но когато използвам коментирания ред по-долу, не работи и често дава "nan":

if(i == 0){    
    for(int k = 0; k < gridDim.x; k++){
        atomicAdd(sum, x[k]); //Works good
       //sum[0] += x[k]; //or *sum += x[k]; //Does not work, often results in nan
    }
}

Сега всъщност използвам atomicadd директно, за да сумирам споделените суми, но бих искал да разбера защо това не работи. Атомното добавяне е пълна глупост, когато ограничавате операцията до една нишка. И простата сума трябва да работи добре!


person François Laenen    schedule 24.07.2013    source източник
comment
__syncthreads() синхронизира нишки само в един и същ блок, а не в различни блокове. Мисля, че неправилният резултат се дължи на проблем със синхронизацията. Чрез atomicAdd вие налагате синхронизацията между различните блокове, които ви липсват от __syncthreads().   -  person Vitality    schedule 24.07.2013
comment
Наистина, когато добавя __syncthreads() вътре в цикъла for, простата сума работи! Но не го разбирам. Правя сумата само с една единствена нишка в глобален масив, така че защо трябва да ме е грижа за синхронизирането във for цикъла?   -  person François Laenen    schedule 24.07.2013
comment
Добре, мисля, че разбрах! Глобалният масив няма непременно да бъде записан при влизане в цикъла, защото всички блокове няма да са синхронизирани. И така, каква е командата за глобална синхронизираща нишка?   -  person François Laenen    schedule 24.07.2013
comment
Операндите x[k] са резултатите от изчисленията от различни блокове: x[0] е резултатът от блок 0, x[1] е резултатът от блок 1 и т.н. Подозирам, че нишката 0 може да започне да ги добавя, преди някои блокове наистина да са приключили своите изчисления. Опитайте следното. Поставете втория кодов фрагмент в друго ядро, така че да се наложи синхронизация, и след това опитайте дали редът sum[0] += x[k]; работи.   -  person Vitality    schedule 24.07.2013
comment
Относно новия ви въпрос, CUDA няма безопасен механизъм за синхронизиране между блоковете.   -  person Vitality    schedule 24.07.2013
comment
Добре, тъжно... Използването на друго ядро ​​също работи добре, налагайки синхронизиране на устройството. Благодаря ти за пояснението!   -  person François Laenen    schedule 24.07.2013
comment
Добавих отговор на вашия въпрос. Ако е било полезно, моля, приемете го, така че други хора от общността да могат по-бързо да намерят решение, ако открият подобен проблем.   -  person Vitality    schedule 24.07.2013


Отговори (2)


__syncthreads() синхронизира нишки само в един и същи блок, а не в различни блокове и CUDA няма безопасен механизъм за синхронизиране между блокове.

Неправилният резултат се дължи на проблем със синхронизацията. Операндите x[k] са резултатите от изчисленията от различни блокове: x[0] е резултатът от блок 0, x[1] е резултатът от блок 1 и т.н. Нишката 0 може да започне да ги добавя, преди някои блокове наистина да са приключили своите изчисления.

Трябва да поставите втория кодов фрагмент в различно ядро, така че синхронизирането да се наложи и редът sum[0] += x[k]; вече може да работи.

person Vitality    schedule 24.07.2013
comment
Имайте предвид също, че atomic НЕ е безопасен начин за осигуряване на синхронизиране между блокове, особено когато броят на блоковете ви се увеличава. Когато опитам с 10^5 елемента, 512 нишки на блок, следователно 196 блока, получавам резултата nan. Това просто помага да се забавят процесите на сумиране, оставяйки време на другите нишки от другите блокове да напишат своите резултати, но това определено не е чист начин да се справите с това. Друго ядро ​​е по-добро - person François Laenen; 24.07.2013
comment
Определено си прав. Ревизирах отговора си, като премахнах изречението относно atomicAdd. - person Vitality; 24.07.2013
comment
Малко добавяне също: всъщност търсих решение, без да разрязвам ядрото си на две, защото редуцирането ще се използва сред другите операции във функция на устройство. Но няма синхронизиране в края на функцията на устройството. Намерих обаче хубав начин, който работи с 10^5 елемента: поставете __threadfence() в блока if, когато извличате споделена памет в глобалната памет, така че всяка нишка 0 от всеки блок да гарантира, че всички нишки могат да виждат неговото писане. И можете да добавите нещо по-здраво. Всъщност има пример в cuda prog. ръководство, раздел B5. - person François Laenen; 24.07.2013

Както беше посочено, вашият проблем се дължи на липсваща синхронизация след първото преминаване, тъй като не можете да синхронизирате между блоковете. Има добър упътване за намаляването на примерни кодове, предоставени с комплекта инструменти.

Като казах това, силно бих препоръчал на хората да не пишат редуциращи ядра (или други примитиви като сканиране), където такива примитиви съществуват в кода на библиотеката. Много по-добре е да инвестирате усилията си другаде и да използвате повторно съществуващия оптимизиран код, където е наличен. Това не важи, ако правите това, за да научите, разбира се!

Препоръчвам ви да разгледате Thrust и CUB.

person Tom    schedule 24.07.2013