在 HLSL DirectCompute 着色器中实现 SpinLock



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

以下是我实现旋转锁的方式:

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);
}

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

我无法使用 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;
}
}
}
}

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

[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(及其驱动程序以及您当前使用的编译器版本)很可能会采用平面分支策略。这意味着两个分支都由 warp 的所有线程执行,因此基本上根本没有锁。

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

现在,在您的特定机器中,程序甚至可以正常运行。但是您完全不能保证该程序可以在其他机器上运行,甚至可以使用不同的驱动程序版本。您更新驱动程序和繁荣,您的程序突然遇到 GPU 超时并崩溃。

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

现在让我们回到你的Y问题

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

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

关于背离的题外话

NVIDIA - GPU 技术大会 2010 幻灯片

戈德克 - 入门教程

多诺万 - GPU 并行扫描

Barlas - 多核和 GPU 编程

正如 kefren 提到的,由于经线发散,您的自旋锁不起作用。但是,有一种方法可以设计不会导致死锁的 gpu 旋转锁。我将这个旋转锁用于像素着色器,但它也应该在计算着色器中工作。

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 级别,这会导致原子操作的未定义行为。这可能会导致这些帮助程序调用的循环无限执行,从而导致死锁

相关内容

  • 没有找到相关文章

最新更新