在我使用__syncthreads()
故意删除线程的块中使用return
是否安全?
文档说明__syncthreads()
必须由块中的每个线程调用,否则会导致死锁,但实际上我从未遇到过这种行为。
示例代码:
__global__ void kernel(float* data, size_t size) {
// Drop excess threads if user put too many in kernel call.
// After the return, there are `size` active threads.
if (threadIdx.x >= size) {
return;
}
// ... do some work ...
__syncthreads(); // Is this safe?
// For the rest of the kernel, we need to drop one excess thread
// After the return, there are `size - 1` active threads
if (threadIdx.x + 1 == size) {
return;
}
// ... do more work ...
__syncthreads(); // Is this safe?
}
答案 0 :(得分:30)
简短问题的答案是“否”。围绕__syncthreads()
指令的Warp级分支差异将导致死锁并导致内核挂起。您的代码示例不保证安全或正确。实现代码的正确方法如下:
__global__ void kernel(...)
if (tidx < N) {
// Code stanza #1
}
__syncthreads();
if (tidx < N) {
// Code stanza #2
}
// etc
}
以便无条件执行__syncthreads()
指令。
编辑:只是添加一些确认此断言的附加信息,__syncthreads()
调用将编译到所有体系结构的PTX bar.sync
指令中。 PTX2.0指南(p133)文档bar.sync
并包含以下警告:
障碍是在每个warp的基础上执行的,就像a中的所有线程一样 扭曲是活跃的。因此,如果warp中的任何线程执行一个bar 指令,就好像warp中的所有线程都执行了 酒吧指导。经线中的所有线程都会停止,直到屏障 完成,屏障的到达计数增加 warp大小(不是warp中活动线程的数量)。在 有条件执行的代码,只有在使用条形指令时才应使用 众所周知,所有线程都以相同的方式评估条件( 扭曲并不分歧)。由于障碍是在每个经线上执行的 基础上,可选的线程数必须是warp大小的倍数。
因此,尽管存在任何相反的断言,除非您可以100%确定任何给定的 warp 中的每个线程都遵循以下条件,否则在__syncthreads()
调用周围进行条件分支是不安全的。相同的代码路径,不会出现扭曲分歧。
答案 1 :(得分:14)
Compute Capability 7.x(Volta)更新:
随着warp中线程之间引入独立线程调度,CUDA最终在实践中更加严格,现在匹配记录的行为。来自Programming Guide:
尽管__syncthreads()一直被记录为同步线程块中的所有线程,但Pascal和先前的体系结构只能在warp级别强制执行同步。在某些情况下,只要每个warp中至少有一些线程到达屏障,这就允许屏障成功而不会被每个线程执行。从Volta开始,每个线程强制执行CUDA内置__syncthreads()和PTX指令bar.sync(及其派生词),因此在块中所有未退出的线程到达之前不会成功。利用先前行为的代码可能会死锁,必须进行修改以确保所有未退出的线程都能到达屏障。
以下是之前的答案,其中涉及伏尔塔之前的行为。
更新:这个答案可能不会在talonmies&#39;之上添加任何内容。 (根据你对这个主题的理解,我想),但是冒着过于冗长的风险,我会提供帮助我更好地理解这些信息的信息。另外,如果你对事物的运作方式不感兴趣,那么#34;引擎盖下#34;或者除了官方文档之外可能有什么可能,这里没有什么可看的。总而言之,我仍然不建议做出超出官方记录的假设,特别是在希望支持多种或未来架构的环境中。我主要想指出的是,虽然CUDA Programming Guide明确地将其称为不良做法,但__syncthreads()
的实际行为可能与描述它的方式有些不同,对我来说有点不同。我想要的最后一件事就是传播错误的信息,所以我愿意讨论并修改我的答案!
这个答案没有TL; DR因为有太多误解的可能性,但这里有一些相关的事实:
__syncthreads()
的行为类似于块中warp的障碍,而不是块中的所有线程,尽管按照建议使用时它也是相同的。bar
指令(例如来自_syncthreads
),那就好像 all 经线中的线程一样。bar.sync
时(由内部__syncthreads()
生成),该块和屏障的到达计数将增加经线大小。这就是以前的观点。__syncthreads()
同步。该指令不会导致warp停止并等待发散路径上的线程。分支执行是序列化的,因此只有当分支重新加入或代码终止时,warp中的线程才会重新同步。在此之前,分支机构按顺序独立运行。同样,块的每个warp中只有一个线程需要命中__syncthreads()
才能继续执行。官方文档和其他来源支持这些声明。
由于__syncthreads()
充当了块中warp的障碍而不是块中的所有线程,正如编程指南中所描述的那样,似乎只需要一个简单的早期退出如果每个经线中至少有一个线程碰到障碍。 (但这并不是说你不能用内在的东西造成死锁!)这也假设__syncthreads()
将始终生成一个简单的bar.sync a;
PTX指令及其语义也不会改变,所以不要在生产中这样做。
One interesting study实际上调查了当你违反CUDA编程指南的建议时会发生什么,他们发现尽管在条件中滥用__syncthreads()
确实可能导致死锁块,并非所有使用条件代码中的内在函数都会这样做。从论文的D.1节开始:
编程指南建议仅当条件在整个线程块中的计算方式相同时,才能在条件代码中使用syncthreads()。本节的其余部分将研究syncthreads()在违反此建议时的行为。我们证明了syncthreads()作为warp的屏障,而不是线程。我们表明,当warp的线程由于分支发散而被序列化时,一条路径上的任何syncthreads()都不会等待来自另一条路径的线程,而只等待在同一线程块内运行的其他warp。
此声明与talonmies引用的the bit of the PTX documentation一致。具体做法是:
在每个warp的基础上执行障碍,就好像warp中的所有线程都是活动的一样。因此,如果warp中的任何线程执行bar指令,则好像warp中的所有线程都执行了bar指令。 warp中的所有线程都会停止,直到屏障完成,并且屏障的到达计数会增加warp大小(而不是warp中活动线程的数量)。
很明显,为什么b
指令中的可选线程计数bar.sync a{, b};
必须是warp大小的倍数 - 每当warp中的单个线程执行bar
指令时到达次数由经线大小增加,而不是经线中实际达到障碍的线程数。尽早终止的线程(遵循不同的路径)无论如何都被有效地计为到达。现在,引用段落中的下一句话确实表示不在条件代码中使用__syncthreads()
,除非&#34;众所周知所有线程都相同地评估条件(warp不发散)。&#34;这似乎是一个过于严格的建议(对于当前的架构),旨在确保到达计数实际上反映了触及障碍的实际线程数。如果击中屏障的至少一个线程增加了整个扭曲的到达次数,那么您可能会有更多的灵活性。
PTX文档中没有歧义,bar.sync a;
生成的__syncthreads()
指令等待当前协作线程数组(块)中的所有线程到达屏障a
。然而,关键是如何&#34;所有线程&#34;目前,每当屏障被击中时,通过以经线大小的倍数递增到达计数来确定(默认情况下,当未指定b
时)。这部分不是未定义的行为,至少不是并行线程执行ISA版本4.2。
请记住,即使没有条件,warp中也可能存在非活动线程 - &#34;块的最后一个线程,其线程数不是warp大小的倍数。&#34; (SIMT architecture notes)。但是__syncthreads()
并没有被禁止出现。
提前退出版本1:
__global__ void kernel(...)
if (tidx >= N)
return; // OK for <32 threads to hit this, but if ALL
// threads in a warp hit this, THEN you are deadlocked
// (assuming there are other warps that sync)
__syncthreads(); // If at least one thread on this path reaches this, the
// arrival count for this barrier is incremented by
// the number of threads in a warp, NOT the number of
// threads that reach this in the current warp.
}
如果每个warp中至少有一个线程命中同步,则不会死锁,但可能的问题是序列化执行不同代码路径的顺序。您可以更改上面的内核以有效地交换分支。
提前退出第2版:
__global__ void kernel(...)
if (tidx < N) {
// do stuff
__syncthreads();
}
// else return;
}
如果你在warp中至少有一个线程击中了障碍,仍然没有死锁,但在这种情况下分支执行的顺序是否重要?我不这么认为,但要求特定的执行订单可能是一个坏主意。
本文在一个更为复杂的例子中证明了这一点,与一个微不足道的早期退出相比,这也提醒我们在扭曲分歧时要谨慎。这里warp的前半部分([0,15]上的线程标识tid
)写入一些共享内存并执行__syncthreads()
,而另一半(线程标识tid
在[16]上,31])也执行__syncthreads()
但现在从经线的前半部分写入的共享存储器位置读取。首先忽略共享内存测试,您可能会遇到任何障碍的死锁。
// incorrect code to demonstrate behavior of __syncthreads
if (tid < 16 ) {
shared_array[tid] = tid;
__syncthreads();
}
else {
__syncthreads();
output[tid] =
shared_array[tid%16];
}
没有死锁,表明__syncthreads()
不会在warp中同步分叉线程。 不同的代码路径在warp中被序列化,并且代码路径中只需要一个线程就可以调用__syncthreads()
以每个warp级别工作。
但是,共享内存位显示某些不可预测的行为可以进入此位置。 warp的后半部分没有从上半部分获得更新值,因为分支散布序列化了warp的执行,而 else块首先执行。所以函数不能正常工作,但它也表明__syncthreads()
不会在变形中同步不同的线程。
__syncthreads()
不会等待warp中的所有线程,并且单个线程到达warp会有效地将整个warp计入到达屏障。 (目前的架构)。
在条件代码中使用__syncthreads()
会很危险,因为序列化的线程执行有多么不同。
只有在了解条件代码的工作原理以及如何处理分支差异(在中发生扭曲)时才使用条件代码中的内在函数。
请注意,我没有说要继续使用__syncthreads()
,其方式与记录方式不一致。