在HLSL DirectCompute着色器中实现SpinLock

fpiette

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

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

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)。

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

[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位像素值。它产生预期的结果。

任何帮助表示赞赏。

凯夫伦

您是此处XY问题的受害者

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

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

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

现在有趣的部分:

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

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

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

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

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

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

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

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

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

离题论

NVIDIA-GPU技术大会2010幻灯片

Goddeke-入门教程

Donovan-GPU并行扫描

Barlas-多核和GPU编程

本文收集自互联网,转载请注明来源。

如有侵权,请联系 [email protected] 删除。

编辑于
0

我来说两句

0 条评论
登录 后参与评论

相关文章