在 HLSL DirectCompute 着色器中实现 SpinLock

fpi*_*tte 3 directx hlsl spinlock directcompute

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

以下是我实现自旋锁的方法:

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);
}
Run Code Online (Sandbox Code Playgroud)

背景:我需要一个自旋锁,因为我必须计算大型二维数组中的数据总和。总和是两倍。使用单线程和双循环计算总和会产生正确的结果。即使在计算总和时引入自旋锁以避免冲突,使用多线程计算总和也会产生错误的结果。

我无法使用 InterLockedAdd,因为总和不适合 32 位整数,而且我使用的是着色器模型 5(编译器 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;
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

下面是多线程版本。这个版本在每次执行时都会产生相似但不同的结果,在我看来,这是由不起作用的锁引起的。

[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();
}
Run Code Online (Sandbox Code Playgroud)

使用的数据:

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);
Run Code Online (Sandbox Code Playgroud)

发送代码:

FImmediateContext->CSSetShader(FComputeShaderGrayAutoComputeSumSqr, NULL, 0);
FImmediateContext->Dispatch(FImageParams.RawImageSize.X, FImageParams.RawImageSize.Y, 1);
Run Code Online (Sandbox Code Playgroud)

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

任何帮助表示赞赏。

kef*_*ren 7

您是XY 问题的受害者的受害者。

\n\n

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

\n\n

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

\n\n
    \n
  1. 动态分支意味着波前(扭曲)采用两条路线之一,并停用应采用另一条路线的线程。然后,它会回滚以拾取残留的休眠线。
  2. \n
  3. 扁平分枝意味着所有线程都执行两个分支,然后每个线程丢弃不需要的结果并保留正确的结果。
  4. \n
\n\n

现在有趣的部分是:

\n\n

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

\n\n

您无法预测 GPU 是否会使用一种方法或另一种方法,并且在动态分支的情况下,无法提前知道 GPU 是否会进入睡眠状态,即直接路由、其他线程或线程较少的分支或拥有更多的人。无法提前知道,并且不同的 GPU 可能会以不同的方式执行代码。相同的 GPU 甚至可能会因不同的驱动程序版本而改变其执行情况。

\n\n

对于自旋锁,您的 GPU(及其驱动程序以及您当前使用的编译器版本)最有可能采用平面分支策略。这意味着两个分支都由 warp 的所有线程执行,因此基本上根本没有锁。

\n\n

如果更改代码(或[branch]在循环之前添加属性),您可以强制动态分支流。但这并不能解决你的问题。在自旋锁的特殊情况下,您要求 GPU 执行的操作是关闭除一个线程之外的所有线程。这并不完全是 GPU 想要做的事情。GPU 将尝试执行相反的操作,并关闭唯一以不同方式评估条件的线程。这确实会减少分歧并提高性能\xe2\x80\xa6,但在你的情况下,它将关闭唯一不在无限循环中的线程。因此,您可能会得到锁定在无限循环中的完整线程波前,因为唯一可能解锁循环的线程......正在睡眠。你的自旋锁实际上已经变成了死锁

\n\n

现在,在您的特定机器上,该程序甚至可能运行良好。但是,您对该程序将在其他计算机上运行的保证为零,甚至可以在不同的驱动程序版本上运行。当您更新驱动程序并繁荣时,您的程序突然遇到 GPU 超时并崩溃。

\n\n

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

\n\n

现在让我们回到你的问题

\n\n

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

\n\n

如果您需要的话,我将添加一些链接来帮助您入门。

\n\n

题外话

\n\n

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

\n\n

Goddeke - 入门教程

\n\n

Donovan - GPU 并行扫描

\n\n

Barlas - 多核和 GPU 编程

\n