Опитвайки се да разберем фрагмента за крайна редукция на cuda sdk по отношение на сложността и условията на изпълнение

Опитвам се да разбера паралелното намаляване в Cuda (много е интересно). В последния ми въпрос относно паралелната редукция Робърт Кровела даде много интуитивно и подробно обяснение, което ми помогна много. Много интуитивен. Сега, гледайки пробите за намаляване на Cuda SDK, има няколко тъмни петна.

  1. Защо (в коментара по-долу) сложността на работата е запазена в O(n)? В какъв случай би се случило обратното? Имам същия въпрос относно сложността на стъпките.

    Тази версия добавя множество елементи на нишка последователно. Това намалява общата цена на алгоритъма, като същевременно запазва сложността на работата O(n) и сложността на стъпките
    O(log n).

  2. Може ли някой да даде интуитивен пример за коментара относно количеството споделена памет („Забележете, че това ядро ​​се нуждае... разпределете blockSize*sizeof(T) байта“) и как е свързано с кода?

  3. Защо nIsPow2 е толкова важно? Някой може ли да обясни или да даде пример?

  4. Защо използваме mySum например в следващите задачи? sdata[tid] = mySum = mySum + sdata[tid + 256];, а не само sdata[tid]+=data[tid+256] като в презентацията на Маркс Харис?

    /*This version adds multiple elements per thread sequentially.  This reduces the   
    overall cost of the algorithm while keeping the work complexity O(n) and the       
    step complexity O(log n).
    (Brent's Theorem optimization)
    
    Note, this kernel needs a minimum of 64*sizeof(T) bytes of shared memory.
    In other words if blockSize <= 32, allocate 64*sizeof(T) bytes.
    If blockSize > 32, allocate blockSize*sizeof(T) bytes.*/
    
    template <class T, unsigned int blockSize, bool nIsPow2> 
    __global__ void
    
    reduce6(T *g_idata, T *g_odata, unsigned int n)
    
    {
    T *sdata = SharedMemory<T>();
    
    // perform first level of reduction,
    // reading from global memory, writing to shared memory
    unsigned int tid = threadIdx.x;
    unsigned int i = blockIdx.x*blockSize*2 + threadIdx.x;
    unsigned int gridSize = blockSize*2*gridDim.x;
    
    T mySum = 0;
    
    // we reduce multiple elements per thread.  The number is determined by the
    // number of active thread blocks (via gridDim).  More blocks will result
    // in a larger gridSize and therefore fewer elements per thread
    while (i < n)
    {
        mySum += g_idata[i];
    
        // ensure we don't read out of bounds -- this is optimized away for powerOf2 sized arrays
        if (nIsPow2 || i + blockSize < n)
            mySum += g_idata[i+blockSize];
    
        i += gridSize;
    }
    
    // each thread puts its local sum into shared memory
    sdata[tid] = mySum;
    __syncthreads();
    
    
    // do reduction in shared mem
    if (blockSize >= 512)
    {
        if (tid < 256)
        {
            sdata[tid] = mySum = mySum + sdata[tid + 256];
        }
    
        __syncthreads();
    }
    
    if (blockSize >= 256)
    {
        if (tid < 128)
        {
            sdata[tid] = mySum = mySum + sdata[tid + 128];
        }
    
        __syncthreads();
    }
    
    if (blockSize >= 128)
    {
        if (tid <  64)
        {
            sdata[tid] = mySum = mySum + sdata[tid +  64];
        }
    
        __syncthreads();
    }
    
    if (tid < 32)
    {
        // now that we are using warp-synchronous programming (below)
        // we need to declare our shared memory volatile so that the compiler
        // doesn't reorder stores to it and induce incorrect behavior.
        volatile T *smem = sdata;
    
        if (blockSize >=  64)
        {
            smem[tid] = mySum = mySum + smem[tid + 32];
        }
    
        if (blockSize >=  32)
        {
            smem[tid] = mySum = mySum + smem[tid + 16];
        }
    
        if (blockSize >=  16)
        {
            smem[tid] = mySum = mySum + smem[tid +  8];
        }
    
        if (blockSize >=   8)
        {
            smem[tid] = mySum = mySum + smem[tid +  4];
        }
    
        if (blockSize >=   4)
        {
            smem[tid] = mySum = mySum + smem[tid +  2];
        }
    
        if (blockSize >=   2)
        {
            smem[tid] = mySum = mySum + smem[tid +  1];
        }
    }
    
    // write result for this block to global mem
    if (tid == 0)
        g_odata[blockIdx.x] = sdata[0];
    

    }


person Darkmoor    schedule 11.12.2013    source източник
comment
Не задавайте въпроси в коментарите на кода! Това е невероятно трудно за четене и разбиране.   -  person talonmies    schedule 11.12.2013


Отговори (1)


Подвъпрос 1:

Уж reduce6 добавя повече работа - всяка нишка обобщава няколко елемента. Общият брой нишки обаче намалява със същия коефициент, така че общото количество работа остава O(n). Що се отнася до „сложността на стъпките“ - това трябва да е броят на извикванията на ядрото. Това наистина е O(log(n)), тъй като броят на извикванията на ядрото е намалял (всяко извикване намалява вектора на данните с по-голям фактор). Мисля, че все пак е Theta(log(n)), ако си спомням правилно как са зададени размерите на мрежата.

Подвъпрос 2

Е, ядрото приема, че всяка деформация е "пълна" в смисъл, че има достатъчно входни данни за всичките му нишки. Деформацията е 32 нишки и всяка нишка чете поне 2 входни елемента, така че ядрото приема, че има поне 64 входни елемента (чийто размер е 64*sizeof(T)). Това важи и за споделената памет, тъй като частта с едно изкривяване на ядрото взема своите данни от споделената памет, като чете 32*2 елемента от sizeof(T) байта от нея.

Подвъпрос 3

Когато nIsPow2 е зададено на true - и имайте предвид, че това не е параметър, който ядрото получава, а по-скоро различна версия на кода, компилиран отделно, отколкото за nIsPow2, което е false - условието nIsPow2 || i + blockSize < n винаги се изпълнява и компилаторът (ако приемем, че е достатъчно интелигентен ) ще избегне напълно обвързаната проверка. Така всяка нишка (безопасно) ще изпълни mySum += g_idata[i+blockSize].

Подвъпрос 4

mysum е сумата на деформация. След като приключим с изчисляването на това, някаква нишка от всяка деформация трябва да я сподели с другите деформации - и това се прави чрез споделена памет (до която всички деформации в блока имат достъп). Сега имайте предвид, че „двойните присвоявания“ - както към mysum, така и към местоположението на споделената памет - се дължат на по-нататъшно използване от различни нишки: Всяка нишка изчислява своя собствена mysum стойност - многократно добавяйки към нея. След окончателното присвояване на mysum към споделена памет, нишката съществува - и нишка с по-нисък индекс използва това присвояване. Но всички нишки изпълняват един и същ код, така че изпълняват „ненужни“ присвоявания от mysum към споделена памет.

person einpoklum    schedule 13.12.2013
comment
@darkmoor: Ако инсталирате функцията с nIsPow2, зададено на true, вие по същество се „задължавате“ да я изпълнявате само с n стойности, които са степен на 2. Компилаторът ще отбележи, че nIsPow2 е постоянна истинска стойност и няма да генерира код за тестване на състоянието. Това изяснява ли нещата? - person einpoklum; 14.12.2013
comment
За да обобщя, моля, поправете ме, ако греша. Ако nIsPow2=true условието i + blockSize < n ще бъде проверено. В противен случай, ако nIsPow2=false ще приемем, че if (nIsPow2 || i + blockSize < n) не съществува и присвояването mySum += g_idata[i+blockSize]; ще бъде изпълнено без проверка на i + blockSize < n. Прав ли съм? Също така, моля за коментари относно Q4? - person Darkmoor; 14.12.2013
comment
Не, напротив. Ако nIsPow2==true тогава условието никога не се проверява. Ако е невярно, трябва да имаме обвързаната проверка... - person einpoklum; 14.12.2013
comment
Разбирам какво казахте, но основното объркване е какво ще се случи, когато заданието mySum += g_idata[i+blockSize]; не бъде изпълнено? С присвояването i = blockIdx.x*blockSize*2 + threadIdx.x; мисля, че четем стойност в позиция i от блока n, след това от съответните позиции от блоковете n+2, n+4 и т.н. и след това сумираме резултата. Какво ще кажете за стойностите в междинните блокове, т.е. n+1, n+3 и т.н.? - person Darkmoor; 14.12.2013
comment
За да обобщя, поправете ме, ако греша. Всяка нишка(tid), която изчислява sdata[tid] = mySum; __syncthreads();, има своята локална памет mySum или sdata[tid] стойност. След __syncthreads() същите нишки за блок с техните локални стойности, т.е. mySum или sdata[tid] се използват в изразите if. Така че в първия израз if следните са еквивалентни: sdata[tid] = mySum = mySum + sdata[tid + 256]; или sdata[tid] = mySum = sdata[tid] + sdata[tid + 256]; или sdata[tid] = mySum + sdata[tid + 256]; или sdata[tid] = sdata[tid] + sdata[tid + 256];Благодаря отново einpoklum:) - person Darkmoor; 15.12.2013
comment
нека да продължим тази дискусия в чата - person einpoklum; 15.12.2013