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的所有线程(如果愿意,可以是波前)必须同时执行同一条指令。每次您插入条件时(例如算法中的
在这种情况下,GPU可以采用以下两种路线之一:
现在有趣的部分:
没有强制转换规则说明GPU应该如何处理分支。
您无法预测GPU将使用一种方法还是另一种方法,并且在进行动态分支的情况下,无法提前知道GPU是否会hibernate直线路径(另一种方法是分支)更少的线程或更多的线程。无法预先知道,并且不同的GPU可能以不同的方式(并且将执行)执行代码。相同的GPU甚至可能会使用不同的驱动程序版本来更改其执行。
在发生自旋锁的情况下,GPU(及其驱动程序以及当前使用的编译器版本)最有可能采用平面分支策略。这意味着两个分支均由扭曲的所有线程执行,因此基本上根本没有锁定。
如果更改代码(或在循环之前添加
现在,在您的特定计算机上,该程序甚至可以正常运行。但是,您可以完全零保证程序可以在其他计算机上运行,??甚至可以在不同的驱动程序版本上运行。更新驱动程序和臂杆后,您的程序突然遇到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的示例。
注意:如果要在像素着色器中使用此自旋锁,则必须在调用此函数之前检查