我尝试在计算着色器中实现旋转锁。但是我的实现似乎没有锁定任何东西。
以下是我实现旋转锁的方式:
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 可以采用以下两种路由之一:
- 动态分支,这意味着波前(翘曲)采用 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 级别,这会导致原子操作的未定义行为。这可能会导致这些帮助程序调用的循环无限执行,从而导致死锁