关于DirectEX:在HLSL DirectCompute着色器中实现SpinLock

Implementing a SpinLock in a HLSL DirectCompute shader

我尝试在计算着色器中实现自旋锁。但是我的实现似乎并没有锁定任何东西。

这是我实现自旋锁的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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(编译器47)。

这里是单线程版本,产生正确的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
[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;
            }
        }
    }
}

及以下是多线程版本。此版本在每次执行中产生类似但不同的结果,这是由于锁失效导致的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
[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();
}

使用的数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);

调度代码:

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

函数GetPixel16BitGrayFromRawImage访问RawImage字节地址缓冲区,以从灰度图像中获取16位像素值。它产生预期的结果。

任何帮助表示赞赏。


您是XY问题的受害者。

让我们从Y问题开始。
您的自旋锁不会锁定。
要了解为什么自旋锁不起作用,您需要检查GPU如何处理您正在创建的情况。您发出一个扭曲,该扭曲由一个或多个线程组组成,每个线程组由许多线程组成。只要执行是并行的,warp的执行速度就很快,这意味着构成warp的所有线程(如果愿意,可以是波前)必须同时执行同一条指令。每次您插入条件时(例如算法中的while循环),您的某些线程必须采用一条路由,而另一些则必须采用路由。这称为线程发散。问题是您不能并行执行不同的指令。

在这种情况下,GPU可以采用以下两种路线之一:

  • 动态分支意味着波前(经线)采用2条路径之一,并停用应采用另一条路径的线程。然后,它回滚以拾取剩下的睡眠线程。
  • 平面分支意味着所有线程都执行两个分支,然后每个线程都丢弃不需要的结果并保留正确的结果。
  • 现在有趣的部分:

    没有强制转换规则说明GPU应该如何处理分支。

    您无法预测GPU将使用一种方法还是另一种方法,并且在进行动态分支的情况下,无法提前知道GPU是否会hibernate直线路径(另一种方法是分支)更少的线程或更多的线程。无法预先知道,并且不同的GPU可能以不同的方式(并且将执行)执行代码。相同的GPU甚至可能会使用不同的驱动程序版本来更改其执行。

    在发生自旋锁的情况下,GPU(及其驱动程序以及当前使用的编译器版本)最有可能采用平面分支策略。这意味着两个分支均由扭曲的所有线程执行,因此基本上根本没有锁定。

    如果更改代码(或在循环之前添加[branch]属性),则可以强制执行动态分支流程。但这不能解决您的问题。在自旋锁的特定情况下,您要GPU执行的操作是关闭除一个线程外的所有线程。这并不是GPU想要做的。 GPU将尝试执行相反的操作,并关闭唯一以不同方式评估条件的线程。实际上,这将导致较少的分歧并提高性能a,但是在您的情况下,它将关闭唯一不在无限循环中的线程。因此,您可能会在无穷循环中获得完整的线程锁定波前,因为唯一可能解锁该循环的线程正在睡眠。您的自旋锁实际上已成为僵局。

    现在,在您的特定计算机上,该程序甚至可以正常运行。但是,您可以完全零保证程序可以在其他计算机上运行,??甚至可以在不同的驱动程序版本上运行。更新驱动程序和臂杆后,您的程序突然遇到GPU超时并崩溃。

    关于GPU中的自旋锁的最佳建议是不要使用它们。曾经

    现在让我们回到您的问题上。

    您真正需要的是一种在大型2维数组中计算数据总和的方法。
    因此,您真正要寻找的是一种好的归约算法。互联网上有一些,或者您可以根据自己的需要编写代码。

    如果需要,我只会添加一些链接以帮助您入门。

    发散题

    NVIDIA-2010年GPU技术大会幻灯片

    Goddeke-入门教程

    Donovan-GPU并行扫描

    Barlas-多核和GPU编程


    如kefren所述,由于翘曲发散,您的自旋锁不起作用。但是,有一种方法可以设计不会导致死锁的gpu自旋锁。我将此自旋锁用于像素着色器,但它也应在计算着色器中工作。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    RWTexture2D<uint> mutex; // all values are 0 in the beginning

    void doCriticalPart(int2 coord) {
       bool keepWaiting = true;
       while(keepWaiting) {
          uint originalValue;
          // try to set the mutex to 1
          InterlockedCompareExchange(mutex[coord], 0, 1, originalValue);
          if(originalValue == 0) { // nothing was locked (previous entry was 0)
             // do your stuff
             // unlock mutex again
             InterlockedExchange(mutex[coord], 0, originalValue);
             // exit loop
             keepWaiting = false;
          }
       }
    }

    为什么在我的第30页的学士论文中对此作了详细解释。还有一个GLSL的示例。

    注意:如果要在像素着色器中使用此自旋锁,则必须在调用此函数之前检查SV_SampleIndex == 0。像素着色器可能会产生一些辅助调用,以确定导致原子操作发生未定义行为的纹理提取mipmap级别。这可能导致无限循环执行这些辅助程序调用,从而导致死锁