Реализация SpinLock в шейдере HLSL DirectCompute - PullRequest
1 голос
/ 19 сентября 2019

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

Вот как я реализую спин-блокировку:

void LockAcquire()
{
    uint Value = 1;

    [allow_uav_condition]
    while (Value) {
        InterlockedCompareExchange(DataOutBuffer[0].Lock, 0, 1, Value);
    };
}

void LockRelease()
{
    uint Value;
    InterlockedExchange(DataOutBuffer[0].Lock, 0, Value);
}

Справочная информация: мне нужна спин-блокировка, потому что мне нужно вычислить суммуданных в большом массиве 2 измерений.Сумма двойная.Вычисление суммы с помощью одного потока и двойного цикла дает правильный результат.Вычисление суммы с многопоточностью дает неправильный результат, даже при введении спин-блокировки, чтобы избежать конфликта при вычислении суммы.

Я не могу использовать InterLockedAdd, потому что сумма не помещается в 32-разрядное целое число, и яЯ использую модель 5 шейдера (Compiler 47).

Вот версия с одним потоком, которая дает правильный результат:

[numthreads(1, 1, 1)]
void CSGrayAutoComputeSumSqr(
    uint3 Gid  : SV_GroupID,
    uint3 DTid : SV_DispatchThreadID, // Coordinates in RawImage window
    uint3 GTid : SV_GroupThreadID,
    uint  GI   : SV_GroupIndex)
{
    if ((DTid.x == 0) && (DTid.y == 0)) {
        uint2 XY;
        int   Mean = (int)round(DataOutBuffer[0].GrayAutoResultMean);
        for (XY.x = 0; XY.x < (uint)RawImageSize.x; XY.x++) {
            for (XY.y = 0; XY.y < (uint)RawImageSize.y; XY.y++) {
                int  Value  = GetPixel16BitGrayFromRawImage(RawImage, rawImageSize, XY);
                uint UValue = (Mean - Value) * (Mean - Value);
                DataOutBuffer[0].GrayAutoResultSumSqr += UValue;
            }
        }
    }
}

и ниже - многопоточная версия.Эта версия выдает схожий, но различный результат при каждом выполнении, ИМО вызвано неработающей блокировкой.

[numthreads(1, 1, 1)]
void CSGrayAutoComputeSumSqr(
    uint3 Gid  : SV_GroupID,
    uint3 DTid : SV_DispatchThreadID, // Coordinates in RawImage window
    uint3 GTid : SV_GroupThreadID,
    uint  GI   : SV_GroupIndex)
{
    int  Value  = GetPixel16BitGrayFromRawImage(RawImage, RawImageSize, DTid.xy);
    int  Mean   = (int)round(DataOutBuffer[0].GrayAutoResultMean);
    uint UValue = (Mean - Value) * (Mean - Value);
    LockAcquire();
    DataOutBuffer[0].GrayAutoResultSumSqr += UValue;
    LockRelease();
}

Используемые данные:

cbuffer TImageParams : register(b0)
{
    int2   RawImageSize;       // Actual image size in RawImage
}

struct TDataOutBuffer
{
    uint   Lock;                             // Use for SpinLock
    double GrayAutoResultMean;
    double GrayAutoResultSumSqr;
};

ByteAddressBuffer                  RawImage       : register(t0);
RWStructuredBuffer<TDataOutBuffer> DataOutBuffer  : register(u4);

Код отправки:

FImmediateContext->CSSetShader(FComputeShaderGrayAutoComputeSumSqr, NULL, 0);
FImmediateContext->Dispatch(FImageParams.RawImageSize.X, FImageParams.RawImageSize.Y, 1);

Функция GetPixel16BitGrayFromRawImage обращается к буферу адресов RawImage, чтобы получить 16-битное значение пикселя из изображения в оттенках серого.Это дает ожидаемый результат.

Любая помощь приветствуется.

1 Ответ

1 голос
/ 23 сентября 2019

Вы стали жертвой проблемы XY здесь.

Давайте начнем с проблемы Y. Ваша спин-блокировка не блокируется. Чтобы понять, почему ваша спин-блокировка не работает, вам нужно проверить, как GPU обрабатывает создаваемую вами ситуацию.Вы выпускаете деформацию, состоящую из одной или нескольких групп потоков, каждая из которых состоит из множества потоков.Выполнение деформации является быстрым, пока выполнение является параллельным, это означает, что все потоки, которые создают деформацию (волновой фронт, если вы предпочитаете), должны выполнить одну и ту же инструкцию в одновременно.Каждый раз, когда вы вставляете условие (например, цикл while в вашем алгоритме), некоторые из ваших потоков должны следовать по маршруту, а другие - по другому.Это называется расхождение потоков.Проблема в том, что вы не можете выполнять различные инструкции параллельно.

В этом случае графический процессор может использовать один из двух маршрутов:

  1. Динамическое ветвление , что означает, что волновой фронт (деформация) принимает один из 2 маршрутов.и деактивирует потоки, которые должны брать другие.Затем он откатывается, чтобы забрать спящие потоки, где они были оставлены.
  2. Плоское ветвление , что означает, что все потоки выполняют обе ветви, затем каждый поток отбрасывает нежелательный результат и сохраняет правильныйone.

Теперь забавная часть:

Не существует правила приведения, указывающего, как GPU должен обрабатывать ветвления.

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

В случае вашей спин-блокировки ваш графический процессор (и его драйвер, и версия компилятора, которую вы используете в настоящее время), скорее всего, используют стратегию плоского ветвления.Это означает, что обе ветви выполняются всеми потоками деформации, поэтому в принципе блокировки вообще нет.

Если вы измените код (или добавите атрибут [branch] перед циклом), вы можете принудительно вызватьдинамическое ветвление потока.Но это не решит ваши проблемы.В конкретном случае спин-блокировки, что вы просите, чтобы GPU отключил все потоки, кроме одного .И это не совсем то, что GPU захочет сделать.Графический процессор попытается сделать обратное, и закроет единственный поток, который оценивает условие по-другому.Это действительно приведет к уменьшению дивергенции и увеличению производительности ... но в вашем случае это отключит единственный поток, который не находится в бесконечном цикле .Таким образом, вы можете получить полный фронт потоков, заблокированных в бесконечном цикле, потому что единственный, который может разблокировать цикл ... это спящий режим.Ваша спин-блокировка фактически стала взаимоблокировкой .

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

Наилучшим предложением о спин-блокировках в графических процессорах является… .не использовать их.Когда-либо.

Теперь давайте вернемся к вам Y задача .

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

Я просто добавлю несколько ссылок для начала, если они вам понадобятся.

Отступление о расхождении

NVIDIA - Технологическая конференция GPU 2010 Слайды

Годдек - Вводное руководство

Донован - параллельное сканирование графических процессоров

Barlas - программирование многоядерных и графических процессоров

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...