CUDA: __syncthreads() внутри операторов if

У меня вопрос о синхронизации CUDA. В частности, мне нужно уточнить синхронизацию в операторах if. Я имею в виду, если я поместил __syncthreads() в область действия оператора if, попавшего на долю потоков внутри блока, что происходит? Я думал, что некоторые потоки останутся "навсегда", ожидая других потоков, которые не попадут в точку синхронизации. Итак, я написал и выполнил некоторый пример кода для проверки:

__global__ void kernel(float* vett, int n)
{
    int index = blockIdx.x*blockDim.x + threadIdx.x;
    int gridSize = blockDim.x*gridDim.x;

    while( index < n )
    {   
        vett[index] = 2;
        if(threadIdx.x < 10)
        {
            vett[index] = 100;
            __syncthreads();
        }
        __syncthreads();

        index += gridSize;
    }
}

Удивительно, но я заметил, что результат был довольно "нормальным" (64 элемента, blockize 32):

100 100 100 100 100 100 100 100 100 100 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2
100 100 100 100 100 100 100 100 100 100 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2

Итак, я немного изменил свой код следующим образом:

__global__ void kernel(float* vett, int n)
{
    int index = blockIdx.x*blockDim.x + threadIdx.x;
    int gridSize = blockDim.x*gridDim.x;

    while( index < n )
    {   
        vett[index] = 2;
        if(threadIdx.x < 10)
        {
            vett[index] = 100;
            __syncthreads();
        }
        __syncthreads();
            vett[index] = 3;
        __syncthreads();

        index += gridSize;
    }
}

И результат был:

3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 
3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 3 

Опять же, я ошибся: я думал, что потоки внутри оператора if после изменения элемента вектора останутся в состоянии ожидания и никогда не выйдут из области if. Итак... не могли бы вы прояснить, что случилось? Возникает ли поток после точки синхронизации разблокировать потоки, ожидающие барьера? Если вам нужно воспроизвести мою ситуацию, я использовал CUDA Toolkit 5.0 RC с SDK 4.2. Большое спасибо заранее.

Ответ 1

Короче говоря, поведение undefined. Поэтому иногда он может делать то, что вы хотите, а может и нет, или (вполне вероятно) просто повесит или разрушит ваше ядро.

Если вам действительно интересно, как все работает внутри, вам нужно помнить, что потоки не выполняются независимо, а warp (группа из 32 потоков) за раз.

Это, конечно, создает проблему с условными ветвями, где условное выражение не равномерно оценивается по всей деформации. Проблема решается путем выполнения обоих путей один за другим, каждый из которых отключает те потоки, которые не должны выполнять этот путь. IIRC на существующем оборудовании сначала берется ветвь, тогда путь выполняется, когда ветвь не занята, но это поведение undefined и, следовательно, не гарантируется.

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

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

Если точки синхронизации вставлены, undefined и даже немного меняется между архитектурами, поэтому снова нет гарантий. Единственный (неофициальный) комментарий, который вы получите от Nvidia, заключается в том, что компилятор довольно хорош в поиске оптимальных точек синхронизации. Однако часто возникают тонкие проблемы, которые могут перемещать оптимальную точку дальше, чем вы могли бы ожидать, особенно если потоки выходят раньше.

Теперь, чтобы понять поведение директивы __syncthreads(), которая преобразуется в инструкцию bar.sync в PTX), важно понять, что эта инструкция не выполняется для потока, но для всего warp сразу (независимо независимо от того, отключены ли какие-либо потоки), потому что необходимо синхронизировать только перекосы блока. Потоки warp уже выполняются синхронно, и дальнейшая синхронизация либо не будет иметь эффекта (если все потоки включены), либо приведет к тупиковой ситуации при попытке синхронизации потоков из разных условных кодов кода.

Вы можете работать от этого описания до того, как ведет себя ваш конкретный код. Но имейте в виду, что все это undefined, никаких гарантий нет, и использование определенного поведения может нарушить ваш код в любое время.

Вы можете посмотреть руководство PTX для получения более подробной информации, особенно для bar.sync, который компилируется __syncthreads(). Генри Вонг "Демистификация GPU Microarchitecture через Microbenchmarking" ,, на которую ссылается ахмад, также стоит прочитать. Несмотря на то, что на данный момент устаревшая архитектура и версия CUDA, разделы об условном ветвлении и __syncthreads() по-прежнему остаются в целом действительными.

Ответ 2

Модель CUDA - MIMD, но текущие графические процессоры NVIDIA реализуют __syncthreads() при детализации warp вместо потока. Это означает, что warps inside a thread-block синхронизированы не обязательно threads inside a thread-block. __syncthreds() ожидает, что все "перекосы" блока потоков попадут в барьер или выйдут из программы. Подробнее см. Henry Wong Demistifying paper.

Ответ 3

Вы не должны использовать __syncthreads(), если только оператор не достигнут во всех потоках в одном блоке потока, всегда. Из руководство по программированию (B.6):

__syncthreads() разрешен в условном коде, но только в том случае, если условное вычисление идентично по всему блоку потока, в противном случае выполнение кода может зависать или создавать непреднамеренные побочные эффекты.

В принципе, ваш код не является хорошо сформированной программой CUDA.

Ответ 4

__ syncthreads() используется для синхронизации потоков внутри блока. Это означает, что все потоки в блоке будут ждать завершения всех, прежде чем продолжить.

Рассмотрим случай, когда в блоке есть некоторые потоки, которые входят в оператор if, а некоторые - нет. Ожидания этих потоков будут заблокированы; навсегда.

Как правило, это не хороший стиль для синхронизации в условном выражении. Лучше избегать этого и перепроектировать ваш код, если он у вас есть. Цель синхронизации состоит в том, чтобы убедиться, что все потоки объединены вместе, почему вы отфильтровываете их с помощью if-statement в первую очередь?

Чтобы добавить, если требуется синхронизация по блокам. Требуется перезапуск ядра.