编辑:我意识到,遗憾的是,我在第一个示例代码中的while
语句末尾忽略了一个分号,并自行误解了它。因此,对于具有threadIdx.x != s
的线程,事实上存在一个空循环,该循环之后是一个收敛点,并且此时所有其他线程的线程都在等待,而不会增加s
变量。对于对此感兴趣的人,我将在下面留下原始(未经修正)的问题。请注意,第一个示例中第二行末尾缺少分号,因此s++
与循环体没有任何共同之处。
-
我们在CUDA课程中学习了序列化,我们的老师告诉我们这样的代码:
__shared__ int s = 0;
while (s != threadIdx.x)
s++; // serialized code
最终会出现HW死锁,因为nvcc编译器会在while (s != threadIdx.x)
和s++
语句之间放置重新收敛点。如果我理解正确,这意味着一旦线程达到重新收敛点,该线程就会停止执行并等待其他线程,直到它们到达该点。但是,在这个例子中,这种情况永远不会发生,因为线程#0进入while循环的主体,到达重新收敛点而不增加s
变量,其他线程陷入无限循环。
工作解决方案应如下:
__shared__ int s = 0;
while (s < blockDim.x)
if (threadIdx.x == s)
s++; // serialized code
这里,块中的所有线程都进入循环体,所有线程都评估条件,只有线程#0在第一次迭代中递增s
变量(并且循环继续)。
我的问题是,为什么第二个例子在第一个例子挂起时有用?更具体地说,if
语句只是另一个分歧点,就汇编语言而言,应该编译成与循环中的条件相同的条件跳转指令。那么为什么在第二个例子中s++
之前没有任何重新收敛点,并且事实上它在声明之后立即消失了?
在其他来源中,我只发现每个分支独立计算一个不同的代码 - 例如在if/else
语句中,首先计算if
分支,并在同一个warp中屏蔽所有else
- 分支线程,然后其他线程计算else
分支,而第一个等待。在 if / else语句之后有一个重新收敛点。那么为什么第一个示例会冻结,而不是将循环拆分为两个分支(一个线程的true
分支和warp中所有其他分支的等待false
分支?“
谢谢。
答案 0 :(得分:1)
将重新收敛点放在while (s != threadIdx.x)
和s++;
的调用之间是没有意义的。它会破坏程序流,因为编译时所有线程都应该可以访问一段代码的重新收敛点。下图显示了第一段代码的流程图以及可能和不可能的重新收敛点。
关于通过SSY
指令记录收敛点的this answer,我在下面创建了类似于你的第一段代码的简单内核
__global__ void kernel_1() {
__shared__ int s;
if(threadIdx.x==0)
s = 0;
__syncthreads();
while (s == threadIdx.x)
s++; // serialized code
}
并使用-O3将其编译为CC = 3.5。下面是使用cuobjdum
二进制工具输出来观察CUDA程序集的结果。结果是:
我不是阅读CUDA程序集的专家,但我可以在行while
和0038
中看到00a0
循环条件检查。在行00a8
,如果它满足0x80
循环条件并再次执行代码块,则分支到while
。重新收敛点的简介位于第0058
行,引入行0xb8
作为收敛点,该收敛点位于出口附近的循环条件检查之后。
总的来说,目前还不清楚你要用这段代码实现什么。同样在第二段代码中,重新收敛点应该在while
循环代码块之后再次出现(我不是指while
和if
之间)。
答案 1 :(得分:1)
它“挂起”的原因既不是HW死锁也不是分支,至少不是直接的。您为一个或多个线程生成无限循环(已经怀疑)。
在您的示例中,没有真正的收敛点。由于您不使用任何同步,因此没有任何实际等待的线程。 while循环在这里发生的事情几乎是忙碌的等待。 如果所有线程都返回,则内核仅完成。由于你有一个(或多个)无限循环(偶然可能甚至没有 - 但这不太可能),内核永远不会完成。
您声明了一个共享变量s。该变量对于块内的所有线程都是已知的。 使用while语句,你基本上说(对每个线程):递增s直到它达到你的(本地)线程id的值。由于所有线程并行递增,因此引入了竞争条件。 例如:
你的解决方案也很混乱,因为每个线程连续执行序列化代码(毕竟这可能是意图 - 尽管实际上很奇怪):
大多数示例显示了一个程序,其中每个线程在某些代码上工作,然后所有线程都被同步,只有单个线程执行更多代码(可能需要所有线程的结果)。 所以,你的第二个例子“有效”,因为没有线程被卡在无限循环中,但我无法想到为什么有人会使用这样的代码, 因为它令人困惑,而且根本不平行。