Проблема планирования CUDA или ошибка запуска ядра?

В моей программе у меня есть рабочая очередь для каждого блока, выданного при запуске ядра. Каждый блок остается в цикле, выталкивая работу в свою очередь, обрабатывая ее и возвращая динамически сгенерированную работу обратно. Поддерживается массив флагов деков, указывающих, какие деки активны, т. е. имеют работу. Если очередь пуста, ядро ​​входит в другой цикл, пытаясь украсть работу из очереди другого блока. Условие остановки достигается, когда больше нет активных деков.

В тесте я установил все очереди, начиная с 1 рабочего элемента. Моя проблема в том, что некоторые блоки вообще не работают. Поскольку некоторые из них не работают, они остаются активными, и моя программа входит в бесконечный цикл.

Теперь к коду. Ядро и вспомогательные функции pop и push:

bool __inline__ __device__ pushBottom(int *aiDequeBottoms , const int &iBid , const unsigned int &uiSize ,
unsigned int &uiPushStartIdx)   
{
int iOldBot = aiDequeBottoms[iBid];
uiPushStartIdx = iOldBot;
iOldBot += uiSize;
if(iOldBot < DEQUE_SIZE)
{
    aiDequeBottoms[iBid] = iOldBot;
    return true;
}
else
{
    return false;
}
}

bool __inline__ __device__ popTop(int *aiDequesBottoms , unsigned int *auiDequesAges , const int &iBid ,
int2 &popStartIdxAndSize)
{
int index;
unsigned int oldAge = auiDequesAges[iBid];
int localBot = aiDequesBottoms[iBid];
index = oldAge >> WORK_STEALING_TAG_NBITS;
if(localBot < index + 2*WORK_STEALING_BATCH_SIZE)
{
    return false;
}

int localTag = oldAge & WORK_STEALING_TAG_MASK;
int size = min(WORK_STEALING_BATCH_SIZE , localBot - index);
unsigned int newAge = (index+size << WORK_STEALING_TAG_NBITS)| localTag;

if(oldAge == atomicCAS(&auiDequesAges[iBid] , oldAge , newAge))
{
    popStartIdxAndSize.x = index;
    popStartIdxAndSize.y = size;
    return true;
}
else
{
    return false;
}
}

bool __inline__ __device__ popBottom(int *aiDequesBottoms , unsigned int *auiDequesAges , const int &iBid ,
int2 &popStartIdxAndSize)
{
int localBot = aiDequesBottoms[iBid];
if(localBot == 0)
{
    return false;
}

int index = localBot;
localBot = localBot - WORK_STEALING_BATCH_SIZE;
aiDequesBottoms[iBid] = localBot;
unsigned int oldAge = auiDequesAges[iBid];
int oldAgeTop = int(oldAge >> WORK_STEALING_TAG_NBITS);
if(localBot > oldAgeTop)
{
    popStartIdxAndSize.y = WORK_STEALING_BATCH_SIZE;
    popStartIdxAndSize.x = index - WORK_STEALING_BATCH_SIZE;
    return true;
}

aiDequesBottoms[iBid] = 0;
unsigned int newAge = ((oldAge & WORK_STEALING_TAG_MASK) + 1) % (WORK_STEALING_TAG_MASK + 1);
if(index > oldAgeTop)
{
    if(oldAge == atomicCAS(&auiDequesAges[iBid] , oldAge , newAge))
    {
        popStartIdxAndSize.y = index - oldAgeTop;
        popStartIdxAndSize.x = index - popStartIdxAndSize.y;
        return true;
    }
}

auiDequesAges[iBid] = newAge;
return false;
}

//----------------------------------------------------------------------------------------------------------------------------
// Function to pop work from deques. Each block try to pop from its own deque. If work isn't available, it try to steal from
// other deques.
//----------------------------------------------------------------------------------------------------------------------------
template <typename Work>
bool __inline__ __device__ popWork(bool *abDequeFlags , int *aiDequesBottoms , unsigned int *auiDequesAges ,
const Work *aDeques , const int &iTid , const int &iBid , unsigned int &uiPopDequeIdx , int2 &popStartIdxAndSize ,
int &iLocalDequeCounter , bool &bPopFlag , unsigned int *uiActiveDeques , unsigned int &uiActiveDequesIdx , Work &work)
{
if(iTid == 0)
{   //Try to pop from block deque
    iLocalDequeCounter = 0;
    bPopFlag = popBottom(aiDequesBottoms , auiDequesAges , iBid , popStartIdxAndSize);
    if(bPopFlag)
    {
        uiPopDequeIdx = iBid;
    }
    else
    {
        abDequeFlags[iBid] = false;
    }
}
__syncthreads();

while(!bPopFlag)
{   //No more work, try to steal some!
    if(iTid == 0)
    {
        uiActiveDequesIdx = 0;
    }
    __syncthreads();

    if(iTid < NDEQUES)
    {
        if(abDequeFlags[iTid] == true) //assuming iTid >= NDEQUES
        {   //Set this deque for a work stealing atempt.
            unsigned int uiIdx = atomicAdd(&uiActiveDequesIdx,1);
            uiActiveDeques[uiIdx] = iTid;
        }
    }
    __syncthreads();

    if(iTid == 0)
    {   //Try to steal until succeeds or there are no more deques left to search
        bPopFlag = false;
        for(uiPopDequeIdx = 0 ; uiPopDequeIdx < uiActiveDequesIdx && bPopFlag == false ; ++uiPopDequeIdx)
        {
            bPopFlag = popTop(aiDequesBottoms , auiDequesAges , uiPopDequeIdx , popStartIdxAndSize);
        }
    }
    __syncthreads();

    if(uiActiveDequesIdx == 0)
    { //No more work to steal. End.
        return false;
    }
}

//Get poped data
if(iTid < popStartIdxAndSize.y) //assuming number of threads >= WORK_SIZE
{
    work = aDeques[uiPopDequeIdx*DEQUE_SIZE + popStartIdxAndSize.x + iTid];
}

return true;
}

//----------------------------------------------------------------------------------------------------------------------------
// Function to push work on deques. To achieve better coalescent global memory accesses the input data is assumed to be tight
// packed in shared mem.
//----------------------------------------------------------------------------------------------------------------------------
template <typename Work>
bool __inline__ __device__ pushWork(int *aiDequesBottoms , Work *aDeques , const int &iTid , const int &iBid ,
const unsigned int &uiDequeOutputCounter , Work *aOutputLocalWork)
{
//Transfer to global mem.
unsigned int uiWorkLeft = uiDequeOutputCounter;
unsigned int uiThreadOffset = iTid;

while(uiWorkLeft > 0)
{
    unsigned int uiWorkTransfered = min(WORK_STEALING_BATCH_SIZE , uiWorkLeft);
    unsigned int uiPushStartIdx;
    bool bPushFlag;
    if(iTid == 0)
    {
        bPushFlag = pushBottom(aiDequesBottoms , iBid , uiWorkTransfered , uiPushStartIdx);
    }
    __syncthreads();
    if(!bPushFlag)
    {
        return false;
    }

    if(iTid < uiWorkTransfered)
    {
        aDeques[DEQUE_SIZE*iBid + uiPushStartIdx +  uiThreadOffset] = aOutputLocalWork[uiThreadOffset];
    }

    uiThreadOffset += WORK_STEALING_BATCH_SIZE;
    uiWorkLeft -= uiWorkTransfered;
}

return true;
}

void __global__ workKernel(bool *abDequeFlags , int *aiDequesBottoms , unsigned int *auiDequesAges , int2 *aDeques , 
int2 *aOutput , unsigned int *puiOutputCounter)
{
int iTid = threadIdx.x;
int iBid = blockIdx.x;

__shared__ int2 aOutputLocalWork[DEQUE_SHARED_SIZE];
__shared__ unsigned int uiPopDequeIdx;
__shared__ int2 popStartIdxAndSize;
__shared__ int iLocalDequeCounter;
__shared__ bool bPopFlag;
__shared__ unsigned int uiActiveDeques[NDEQUES]; //Contains indices for deques with useful work that can be stolen
__shared__ unsigned int uiActiveDequesIdx;
__shared__ unsigned int uiLastOutputCounter;

int2 work;
int iRun = 0;

while(true) //Work loop will continue until cannot pop from bottom or cannot steal work from other deques
{
    if(!popWork<int2>(abDequeFlags , aiDequesBottoms , auiDequesAges , aDeques , iTid , iBid , uiPopDequeIdx ,
        popStartIdxAndSize , iLocalDequeCounter , bPopFlag , uiActiveDeques , uiActiveDequesIdx , work))
    {   //No more work
        return; 
    }

    //Useful work comes here. For now, just some dummy code for testing.
    if(iRun < 5)
    {   //Just 5 iterations that generate more work
        if(iTid < popStartIdxAndSize.y)
        {
            unsigned int uiNewWorkCounter = 1;
            int iDequeOutputCounter = atomicAdd(&iLocalDequeCounter , uiNewWorkCounter);
            work.x++; work.y++;
            aOutputLocalWork[iDequeOutputCounter] = work;
            __syncthreads();

            if(iTid == 0)
            {
                uiLastOutputCounter = atomicAdd(puiOutputCounter , iLocalDequeCounter);
            }
            __syncthreads();

            if(iTid < iLocalDequeCounter) //assuming iLocalDequeCounter <= blockDim.x
            {
                aOutput[uiLastOutputCounter + iTid] = aOutputLocalWork[iTid];
            }
        }
    }

    //Push back to global mem
    if(!pushWork<int2>(aiDequesBottoms , aDeques , iTid , iBid , iLocalDequeCounter , aOutputLocalWork))
    {   //overflow
        return;
    }
    ++iRun;
}
}

А это тест:

#define NDEQUES 256
#define DEQUE_SIZE 20000

void workStealingWrap(bool *abDequeFlags , int *auiDequesBottoms , unsigned int *auiDequesAges , int2 *aDeques , 
int2 *aOutput , unsigned int *puiOutputCounter)
{
workKernel<<<NDEQUES , WORK_STEALING_THREADS>>>(abDequeFlags , auiDequesBottoms , auiDequesAges , aDeques , aOutput , 
    puiOutputCounter);
CUT_CHECK_ERROR("workKernel");
}


//----------------------------------------------------------------------------------------------------------
// This entry point is for work stealing testing.
//----------------------------------------------------------------------------------------------------------
int main(int argc, char* argv[])
{
//Test 0: All deques start with 1 work item.
bool h_abDequeFlags[NDEQUES];
int h_aiDequesBottoms[NDEQUES];
unsigned int h_auiDequesAges[NDEQUES];
int2 *h_aDeques = (int2*) malloc(sizeof(int2)*NDEQUES*DEQUE_SIZE);
unsigned int h_uiOutputCounter;
int2 *h_aOutput = (int2*) malloc(sizeof(int2)*NDEQUES*DEQUE_SIZE);

for(int i = 0 ; i < NDEQUES ; ++i)
{
    h_abDequeFlags[i] = true;
    h_aiDequesBottoms[i] = 1;
    h_auiDequesAges[i] = 0;
    int2 work; work.x = i ; work.y = i;
    h_aDeques[DEQUE_SIZE*i] = work;
}

bool *d_abDequeFlags;
int *d_auiDequesBottoms;
unsigned int *d_auiDequesAges;
int2 *d_aDeques;
GPUMALLOC((void**)&d_abDequeFlags , sizeof(bool)*NDEQUES);
GPUMALLOC((void**)&d_auiDequesBottoms , sizeof(int)*NDEQUES);
GPUMALLOC((void**)&d_auiDequesAges , sizeof(unsigned int)*NDEQUES);
GPUMALLOC((void**)&d_aDeques , sizeof(int2)*NDEQUES*DEQUE_SIZE);

TOGPU(d_abDequeFlags , h_abDequeFlags , sizeof(bool)*NDEQUES);
TOGPU(d_auiDequesBottoms , h_aiDequesBottoms , sizeof(int)*NDEQUES);
TOGPU(d_auiDequesAges , h_auiDequesAges , sizeof(unsigned int)*NDEQUES);
TOGPU(d_aDeques , h_aDeques , sizeof(int2)*NDEQUES*DEQUE_SIZE);

int2 *d_aOutput;
unsigned int *d_puiOutputCounter;
GPUMALLOC((void**)&d_aOutput , sizeof(int2)*NDEQUES*DEQUE_SIZE);
GPUMALLOC((void**)&d_puiOutputCounter , sizeof(unsigned int));
GPUMEMSET(d_aOutput , -1 , sizeof(int2)*NDEQUES*DEQUE_SIZE);
GPUMEMSET(d_puiOutputCounter , 0 , sizeof(unsigned int));

workStealingWrap(d_abDequeFlags , d_auiDequesBottoms , d_auiDequesAges , d_aDeques , d_aOutput , d_puiOutputCounter);

FROMGPU(h_aOutput , d_aOutput , sizeof(int2)*NDEQUES*DEQUE_SIZE);
FROMGPU(&h_uiOutputCounter , d_puiOutputCounter , sizeof(unsigned int));
assert(h_uiOutputCounter == NDEQUES);
for(int i = 0 ; i < NDEQUES*DEQUE_SIZE ; ++i)
{
    int2 work = h_aOutput[i];
    if(i < NDEQUES)
    {
        assert(work.x >= 1 && work.x < NDEQUES*5 && work.y >= 1 && work.y < NDEQUES*5);
    }
    else
    {
        assert(work.x == -1 && work.y == -1);
    }
}

GPUFREE(d_abDequeFlags);
GPUFREE(d_auiDequesBottoms);
GPUFREE(d_auiDequesAges);
GPUFREE(d_aDeques);
GPUFREE(d_aOutput);
GPUFREE(d_puiOutputCounter);

safeFree(h_aDeques);
safeFree(h_aOutput);
}

При отладке этого кода с помощью NSight я убедился, что работают только первые 8 блоков. Мне интересно, является ли это проблемой планирования, и опрос popWork потребляет все ресурсы, или это просто ошибка в моей программе. Любая помощь будет очень признательна.


person dsilva.vinicius    schedule 20.07.2012    source источник
comment
Способен ли ваш графический процессор фактически запускать больше блоков за раз? Вы можете запланировать 1000 блоков на любом графическом процессоре, но это не значит, что они будут работать одновременно. GPU будет работать столько, сколько сможет, а последующие блоки будут выполняться только тогда, когда закончатся предыдущие.   -  person CygnusX1    schedule 20.07.2012
comment
Я знаю, что блоки не будут работать одновременно. Но я ожидал, что планировщик будет, по крайней мере, более интеллектуальным и не позволит ядрам голодать таким образом. Есть ли у вас ссылки, в которых говорится, что более поздние блоки запускаются только после завершения ранних? Если это правда, я уменьшу количество выпущенных блоков. Я не помню, чтобы читал, как планирование деформации выполняется в CUDA.   -  person dsilva.vinicius    schedule 20.07.2012
comment
Блоки никогда не планируются. Если ресурсов для запуска всего не хватает, новые блоки могут запуститься только тогда, когда закончится старый. Я полагаю, что это упоминается в руководстве по программированию, но я также провел свои собственные тесты (хотя и на графических процессорах до Fermi), и они подтвердили это (а также обнаружили некоторые другие проблемы).   -  person CygnusX1    schedule 20.07.2012
comment
Блоки потока должны выполняться независимо: их должно быть возможно выполнять в любом порядке, параллельно или последовательно. Это требование независимости позволяет планировать блоки потоков в любом порядке для любого количества ядер, как показано на рис. 1-4, что позволяет программистам писать код, который масштабируется в зависимости от количества ядер. (Руководство по программированию CUDA C, версия 4.0, стр. 10). В моем случае блоки не могут работать последовательно. Думаю, мне придется остановить голосование.   -  person dsilva.vinicius    schedule 20.07.2012
comment
Если вы будете осторожны, подсчитайте, сколько блоков может обрабатывать ваш графический процессор, вы можете заставить их работать по-настоящему параллельно. Так делают многие, несмотря на то, что это несколько противоречит методологии NVIDIA.   -  person CygnusX1    schedule 20.07.2012


Ответы (1)


Я нашел некоторые проблемы в коде. Я не запускал его, поэтому я не знаю, насколько это актуально.

Реализация popTop отсутствует. Я предполагаю, что это может быть успешно или неудачно, и результат повлияет на bPopFlag. Я предполагаю, что у вас может быть ненулевое uiActiveDequesIdx, в то время как bPopFlag все еще равно нулю.

  • В этом случае в popWork у вас есть цикл while(!bPopFlag). Представьте, что два варпа достигают последнего __syncthreads() в цикле. Теперь первая деформация проверит uiActiveDequesIdx (предположим, что она не равна нулю), вернется к началу цикла, а поток iTid установит uiActiveDequesIdx в 0. Теперь вторая деформация возобновит выполнение с последнего __syncthreads(), проверит uiActiveDequesIdx (который теперь 0) и выходит из цикла. С этого момента ваши варпы разойдутся на __syncthreads(), и будут происходить плохие вещи.

  • В pushWork bPushFlag является локальным регистром. Изменяется только iTid == 0 потоком, но потом читается всеми потоками. Вы можете прочитать неинициализированное значение и получить расходящиеся варпы дальше __syncthreads().

Могут быть еще проблемы, которых я еще не видел. В частности, меня беспокоит то, что вы можете пропустить __threadfence() при установке флагов в глобальной памяти (о том, что он делает, смотрите в Руководстве по программированию)

Кроме того, дважды проверьте другие циклы на наличие проблем с синхронизацией, подобных проблеме, о которой я сообщал выше, где некоторые перекосы все еще находятся в старой итерации цикла, а другие уже в новой итерации. Я думаю, что хороший общий подход заключается в том, что если у вас есть нерасходящийся цикл, зависящий от некоторых общих значений, всегда ставьте __syncthreads() в конце цикла, и только когда вы выполняете некоторую позднюю оптимизацию кода, попробуйте удалить Это.

person CygnusX1    schedule 20.07.2012
comment
Спасибо за советы. Я еще не тестировал код должным образом из-за проблемы с планированием блоков. Я просто исправлю указанные ошибки, чтобы избежать проблем позже. Я отмечу ваш пост как ответ, так как вы указали на основную проблему в комментариях к моему вопросу. - person dsilva.vinicius; 20.07.2012
comment
Я отредактировал вопрос с низкоуровневыми функциями push и pop, если вы хотите правильно его скомпилировать. - person dsilva.vinicius; 20.07.2012