☰
CUDA Warp函数实战:shuffle与ballot优化归约与流式压缩
2026/10/21 9:46:05 网站建设 项目流程

搞GPU并行计算的人,迟早会跟Warp级函数打交道。CUDA把一个Warp定义为32个线程,它既是硬件的调度单位,也是软件优化的天然分界线。很多初学者写内核,习惯用__syncthreads()配合共享内存来协调线程,这套思路能解决大部分问题,但当数据交换只发生在相邻lane、归约只需要一个Warp内部完成时,用共享内存等于绕远路。这个时候,__shfl_sync、__ballot_sync这一族Warp函数就派上用场了。

这篇实战笔记聚焦在“其它Warp函数”——也就是大多数人用得少、但性能关键路径上非常值钱的这些内建原语。我会从执行模型讲清楚它们的适用场景,给出手写归约、流式压缩和带矩阵片段的完整示例,也会把我在实际调试中踩过的mask、发散、边界问题一并记录。适合已经写过一些CUDA内核、想进一步压榨内存带宽和计算吞吐的开发者。

1. Warp函数解决什么问题:从执行模型说起

1.1 锁步执行是“硬件约定”,不是你的编写假设

很多人刚接触CUDA时,都听过“一个Warp里的32个线程以锁步方式执行同一条指令”这种说法。这个描述在概念层面是对的:GPU的调度器确实以Warp为单位取指,一个Warp里的线程共享程序计数器,同一时刻应该执行同一条指令。但你别把“锁步”理解成“绝对同步”,更不要因此写出想当然的代码。真实运行时,Warp内的线程会因为分支发散、内存访问延迟、指令缓存未命中等原因,在某些时刻处于不同的推进状态,只是硬件会在每个指令发射点尽量保证它们在同一个指令上汇合。

理解这一点,就理解了Warp函数的设计动机。__shfl_sync、__ballot_sync这类函数,并不依赖“此刻所有线程都必须正好执行到这里”这种脆弱的假设,而是通过显式传入的mask参数,让你告诉硬件“当前有哪些lane正在参与这次协作”。哪怕整个Warp因为发散只剩下4个线程在跑,只要这4个线程的掩码信息正确,它们之间的数据交换依然是安全、可预期的。这就是Warp原语比共享内存更优雅的地方:它把“协作”从“大家一起到某处汇合”变成了“明确宣布谁参与、谁不参与”。

1.2 没有Warp原语的时候,线程间交换数据有多麻烦

设想一个最基础的场景:一个Warp里的32个线程,各自算出一个float,你想把它们的和归约到每个线程手里。这件事如果只用最基础的工具,大概长这样:先把每个线程的值写入共享内存数组,然后调用__syncthreads()保证所有线程都写完了,再让特定的线程去读别人的数值做累加,最后再__syncthreads(),把结果广播出去。两步同步、一轮共享内存读写,代码能写,但心里清楚这很笨重。

笨在哪?第一,共享内存本身有延迟,虽然比全局内存快得多,但寄存器访问的延迟更低;第二,__syncthreads()是block级别的屏障,如果这个block里只有8个Warp,你为了一个Warp内的归约,让整个block的所有线程都停下来等,代价明显不划算;第三,共享内存数组可能会bank conflict,访问模式一旦不对,性能直接打折。Warp原语把数据交换直接放在寄存器之间,一条shuffle指令就完成了“把某个lane的值搬给另一个lane”的动作,全程不碰共享内存、不需要block级同步。这才是正经的并行优化思路。

1.3 一张表看懂Warp函数家族

在展开具体代码之前,我先给这族函数做个总览。好多人一看到__shfl_sync这串字母就头大,其实它们分门别类之后非常清晰。

函数/类别核心用途典型场景
__all_sync/__any_sync判断Warp内全部/任意lane是否满足某个条件提前退出、误差控制
__ballot_sync把每个lane的布尔条件压成一个32位掩码流式压缩、直方图、分支统计
__shfl_sync/__shfl_up_sync/__shfl_down_sync/__shfl_xor_sync寄存器间按规则交换数据Warp归约、前缀和、广播、转置
__reduce_add_sync等一个Warp内直接做归约sm_80+设备上的极简归约
__match_any_sync/__match_all_sync找出哪些lane的值与当前lane相同图算法、数据聚合、查重
nvcuda::wmma以Warp为粒度操作矩阵fragmentTensor Core上的矩阵乘

我这么说吧:投票函数解决的是“怎么知道Warp里发生什么”,shuffle解决的是“怎么让Warp里的数据高效流动”,归约和匹配是在这两个基础上的高级封装,wmma则把整个Warp当成一个矩阵计算单元来用。理解了它们各自的定位,后面写代码时你才能选得准、用得对。

2. 投票函数实战:all、any和ballot怎么选

2.1__ballot_sync:把lane的条件压进32位掩码

这一族函数最核心的是__ballot_sync。它的签名是unsigned __ballot_sync(unsigned mask, bool predicate),含义很直观:Warp内的每一个lane,如果当前predicate为真,返回值的对应位就置1。比如lane 3和lane 7的条件为真,返回值就是(1<<3) | (1<<7)。这一下就把“哪些线程满足条件”这个信息从散布在32个线程里的布尔值,压缩成了一条寄存器里的整数。

我经常用来做的一件事是“Warp内快速统计”。比如你要判断一个Warp里满足条件的线程数,如果不用__ballot_sync,你得先把布尔值写入共享内存,再同步、再让一个lane去数,多绕。用了__ballot_sync,一个__popc(ballot)就把32位里1的个数数出来了,两条指令搞定。这里有个隐藏好处:__ballot_sync返回的掩码本身就是32位整数,可以直接参与位运算,这让它在很多算法里不仅能“统计”,还能“定位”——比如下面这个流式压缩的例子。

2.2 手写一个基于ballot的Warp级流式压缩

流式压缩(stream compaction)在并行计算里很常见:给一个数组和一个判定条件,你要把满足条件的元素挪到前面去,不满足的丢掉。朴素做法是原子操作一个个写,慢。用__ballot_sync可以在Warp粒度上快速算出“每个Warp里到底有几个有效元素,这些元素应该从哪个位置开始写”。

__global__ void compactKernel(const int* in, int* out, int n, int* totalCount) { int idx = blockIdx.x * blockDim.x + threadIdx.x; int lane = threadIdx.x % 32; // 必须保证所有处于活动状态的lane都走到这行 bool pred = (idx < n) && (in[idx] != 0); // 得到当前Warp内所有满足条件的lane掩码 unsigned ballotMask = __ballot_sync(0xffffffffu, pred); // 当前Warp内有效元素总数 int warpCount = __popc(ballotMask); // 当前lane之前(更低位)有多少个有效lane int lanePrefix = __popc(ballotMask & ((1u << lane) - 1)); // 让lane 0去申请一个Warp内元素的起始偏移 int warpBase = 0; if (lane == 0) { warpBase = atomicAdd(totalCount, warpCount); } // 把warpBase广播给所有lane warpBase = __shfl_sync(0xffffffffu, warpBase, 0); // 只有满足条件的线程才写内存 if (pred) { out[warpBase + lanePrefix] = in[idx]; } }

这段代码的核心思想是:lanePrefix算出了“在写数据时,我这个元素应该在当前Warp块内的第几个位置”,warpBase算出了“整个Warp块的起始写位置”,两者一加,每个元素的目标位置就唯一确定了,完全不需要反复原子操作。我第一次跑通这个版本的时候,最强烈的感受是:原来ballot的价值不只是“判断有没有”,而是能把“谁有、谁在第几位”这种空间信息一次性拿出来。

2.3 all、any与ballot的选型原则

__all_sync和__any_sync我平时用得不多,但一旦用了就很关键。前者返回所有参与lane的predicate都为真,后者返回至少一个为真。典型的场景是“收敛检查”:迭代算法里判断整个Warp是否都满足误差阈值了,满足就提前退出;或者判断某个分支条件是否至少有一个lane需要执行。

选型其实就一句话:如果你只需要知道“全真还是全假”,用all/any;如果你需要知道“具体哪些lane是真”,必须用ballot。很多新手在需要掩码信息时却用any,然后回头又去逐个线程判断,这是绕弯。另外提醒一句,这三个函数都有_sync后缀,意味着你自己要负责传入正确的mask。它们不会自动帮你筛选参与者,传错mask轻则结果错误,重则跟死锁一样卡住不动,这个我在第6章会专门展开。

3. Warp Shuffle详解:寄存器之间真实的数据交换

3.1 四条shuffle指令的语义

shuffle家族是Warp函数里最常用、也最容易被用错的一组。它们做的事本质上是“把某个lane寄存器里的值,搬到另一个lane的寄存器里”。注意,是寄存器到寄存器,不经过共享内存。四条指令的差异只在“目标lane怎么选”:

指令目标lane选择方式典型用途
__shfl_sync(mask, val, srcLane)直接指定源lane编号广播、任意模式交换
__shfl_up_sync(mask, val, delta)lane - delta,数据从低位往高位流前缀和、串行依赖
__shfl_down_sync(mask, val, delta)lane + delta,数据从高位往低位流归约
__shfl_xor_sync(mask, val, laneMask)lane ^ laneMask,按位异或选源蝶形交换、转置

up和down对照着看更好记:up是“上面的人把数据给你”,down是“下面的人把数据给你”。xor最特别,它适合做那种需要“对称交换”的场景,比如快速傅里叶变换的蝶形运算,就能用lane ^ 1、lane ^ 2这种方式一层层交换数据。

3.2 三种边界行为必须记牢

shuffle的边界行为是整个系列最容易踩坑的地方。我直接说结论,都是实测验证过的:

第一,__shfl_up_sync在lane - delta < 0时,返回调用者自己的值。也就是说,lane 0调用__shfl_up_sync(mask, val, 1),得到的是val自己,不是0,也不是未定义值。很多人做前缀和时想当然地认为“上面没人就是0”,结果就是重复累加,结果翻倍。

第二,__shfl_down_sync在lane + delta >= 32时,同样是返回调用者自己的值。做归约的时候,最后一次delta=16,lane 0到15都能拿到高16位的值,而lane 16到31拿到的是自己之前的值,所以归约完的结果虽然所有lane都有,但高16位其实是无意义中间值。我一般只信任低16位,或者最后让lane 0来取最终结果。

第三,width参数默认是整个Warp(32),如果你想在一个更小的子Warp内做shuffle,高位的lane行为是未定义的。这个width参数以前的老式__shfl函数经常用,改成_sync版本后我几乎不碰它,老老实实用完整Warp,逻辑清晰也好调试。

还有一个容易忽略的点:shuffle的数据类型虽然文档说支持大多数内置类型,但如果你传一个结构体进去,编译器会拆成多次shuffle,并不保证原子性,中间状态可能被其他lane读到。所以要么只shuffle基本类型,要么手动把结构体拆开处理。

3.3 用shuffle做Warp归约和广播

Warp内归约是shuffle最经典的应用。下面这段代码我相信每个CUDA开发者都手写过:

__inline__ __device__ float warpReduceSum(float val) { // 从距16开始,每次减半 for (int offset = 16; offset > 0; offset >>= 1) { val += __shfl_down_sync(0xffffffffu, val, offset); } return val; }

执行的逻辑是:第一步,lane 0~15分别加上lane 16~31的值,这样低16个lane各自持有两个元素的和;第二步,offset=8,lane 0~7加上lane 8~15的值;以此类推。5次shuffle之后,lane 0手里就是Warp全部32个元素的总和,其他lane手里的数值也会因为边界行为保留某些中间和,但只有lane 0的值是完整结果。

这里有个细节值得多说一句:shuffle归约比共享内存归约快,不只是因为省了共享内存延迟,还因为它把__syncthreads()这个block级屏障彻底干掉了。你的block里有7个Warp,各自做各自的归约,互不相干,为什么非要所有人停下来等?

广播的写法更简单,一条指令:

float value = __shfl_sync(0xffffffffu, fromLane0, 0);

几乎所有lane都能拿到lane 0的值,前提是fromLane0这个变量在lane 0上是真实有效的。我在第2章的流式压缩里就是这么干的:先让lane 0去做原子操作,再把结果用shuffle广播出来。

3.4 前缀和里的shfl_up陷阱

前缀和(prefix sum / scan)是另一个高频场景。hillis-steele风格的做法是每轮把偏移翻倍,然后用__shfl_up_sync从左边拿值。初版代码很容易写成这样:

// 错误示范 for (int offset = 1; offset < 32; offset <<= 1) { val += __shfl_up_sync(0xffffffffu, val, offset); }

问题在于lane - offset < 0时,__shfl_up_sync返回调用者自己的值,于是lane 0在第一轮offset=1时,会把自己的值又加了一次。第二轮offset=2时,lane 0和lane 1都会拿到自己当前的值,继续翻倍加。最终结果就是前面若干位置的值膨胀,完全不对。

正确的做法是:要么用__shfl_sync显式指定srcLane = lane - offset,并且用条件判断让源lane非法时补0;要么先得到源lane的原始值再过滤一遍。举个用__shfl_sync的正确版本:

float inclusiveScan(float val, unsigned mask) { int lane = threadIdx.x % 32; for (int offset = 1; offset < 32; offset <<= 1) { int srcLane = lane - offset; float other = (srcLane >= 0) ? __shfl_sync(mask, val, srcLane) : 0.0f; val += other; } return val; }

这里的关键是不去依赖那个“越界返回自己”的边界行为,而是自己在逻辑层面对越界情况做补0处理。我后来写所有涉及shuffle的代码,都会问自己一句:边界lane的数据流向是什么?如果我不希望它“回流到源自己”,那就要在代码层面做显式保护。这个习惯帮我避了不少坑。

4. 完整实战:一版能跑通的多Block归约内核

4.1 从Warp归约到Block归约

单Warp归约只能处理32个数,实际业务里动不动就是几百万个数,肯定要拆到多个Block和多个Warp。于是问题分成两半:首先每个Warp内部做一个局部归约,得到一个Warp总和;然后把同一个Block里不同Warp的结果再做一次归约,得到Block总和;最后把不同Block的总和加起来。

这个两级结构在不用Warp函数的时候会写得很啰嗦:Warp内用共享内存、Warp间还要共享内存+__syncthreads()。用了shuffle之后,Warp内归约已经完全不需要共享内存,只有Warp间归约还需要一小块共享内存数组,以及一次block级同步。我贴一个完整的内核,注释写清楚每一步在做什么。

4.2 完整内核代码

__global__ void blockReduceSum(const float* in, float* out, int n) { int tid = threadIdx.x; int gid = blockIdx.x * blockDim.x + tid; // 越界的线程补0,保证归约结果不受影响 float val = (gid < n) ? in[gid] : 0.0f; unsigned mask = 0xffffffffu; int lane = tid % 32; int warpId = tid / 32; // 第一步:每个Warp内部做shuffle归约 for (int offset = 16; offset > 0; offset >>= 1) { val += __shfl_down_sync(mask, val, offset); } // 每个Warp只有lane 0的结果是完整的 __shared__ float warpSums[32]; if (lane == 0) { warpSums[warpId] = val; } __syncthreads(); // 第二步:block内第一个Warp把各Warp的部分和再归约一次 if (warpId == 0) { float blockVal = (lane < (blockDim.x / 32)) ? warpSums[lane] : 0.0f; for (int offset = 16; offset > 0; offset >>= 1) { blockVal += __shfl_down_sync(mask, blockVal, offset); } // 最终结果由lane 0原子累加到全局 if (lane == 0) { atomicAdd(out, blockVal); } } }

几点说明。warpSums的大小写成32,前提是blockDim.x不超过1024(也就是最多32个Warp),这是CUDA默认的线程数上限,所以够用。如果你的block里只有8个Warp,第二个归约阶段里lane 8到31对应的warpSums[lane]没被写过,必须补0,不然读出来的是垃圾值。代码里用(lane < (blockDim.x / 32))做了保护。这个细节很多教程不会写,但少了它,只要blockDim不是完整32的整数倍或者Warp数少于32,结果就是脏的。

4.3 性能细节与改进方向

这套两级归约跑起来的性能,通常比纯共享内存版本快10%~20%。但这个数字取决于你的数据量和block配置。我实测下来,block大小设在256或512比较划算,也就是8或16个Warp,这样第二级归约的压力小,原子操作也少。如果block太大,第二级归约的shuffle轮次没增加(还是5轮),但共享内存数组占得大,同步开销也上去了;如果block太小,原子操作的次数就多,累加热点反而竞争严重。

还有个进阶方向:如果n对不上blockDim.x的整数倍,最好单独起一个kernel处理尾部数据,而不是像我上面那样用(gid < n) ? in[gid] : 0.0f来补零。原因很简单:补零后整个Warp还是参与了同样的计算量,尾部那个block浪费了几乎一整块执行时间。我记得有一次数据量n=1_000_003,block配256,最后一个block只有3个线程有数据,其余253个线程全在补0,那一块kernel的执行时间比前面所有块加起来都长。后来改成把尾部数据单独用单Warp处理,整体时间立刻降下来了。

另一个常见优化是把第一步的Warp归约换成__reduce_add_sync。现代CUDA设备上,一条指令就能完成Warp内加法归约:

#if __CUDA_ARCH__ >= 800 val = __reduce_add_sync(mask, val); #endif

代码会简洁很多,性能通常也更好。我一般在追求极致性能的迭代版本里用这个,但教学版本还是会写循环shuffle,因为能让人理解数据流,排查问题时也更容易看清每一步。

5. 更高阶的Warp特性:矩阵指令、匹配指令与规约指令

5.1 wmma:让Tensor Core直接服务Warp

如果说shuffle是“Warp函数里的田忌赛马”,那wmma(Warp Matrix Multiply-Accumulate)就是“直接给你一张必胜底牌”。CUDA的mma.h头文件里提供了一套以Warp为粒度操作矩阵的API。注意,这里的粒度是“一个Warp协同算一个矩阵Tile”,不是每个线程算一个数。基础形状是16x16x16,也就是一个Warp的32个lane,共同完成一个16x16的矩阵乘上16x16的矩阵,再累加回16x16的累加器。

写起来大概是这种感觉:

#include <mma.h> using namespace nvcuda; __global__ void wmmaKernel(const half* a, const half* b, float* c, int lda, int ldb, int ldc) { // 定义三个fragment:矩阵A、矩阵B、累加器C wmma::fragment<wmma::matrix_a, 16, 16, 16, half, wmma::row_major> a_frag; wmma::fragment<wmma::matrix_b, 16, 16, 16, half, wmma::col_major> b_frag; wmma::fragment<wmma::accumulator, 16, 16, 16, float> c_frag; // 累加器初始化为0 wmma::fill_fragment(c_frag, 0.0f); // 把全局内存里的矩阵数据加载到fragment中 wmma::load_matrix_sync(a_frag, a, lda); wmma::load_matrix_sync(b_frag, b, ldb); // 执行矩阵乘累加:c = a * b + c wmma::mma_sync(c_frag, a_frag, b_frag, c_frag); // 把结果写回全局内存 wmma::store_matrix_sync(c, c_frag, ldc, wmma::mem_row_major); }

这段代码看起来很短,但背后的数据布局极其讲究。fragment里的数据在32个lane之间如何分布,是由硬件决定的,你写代码时绝对不能假设“lane 0一定持有A矩阵的第0行”。加载和存储都必须通过load_matrix_sync和store_matrix_sync来桥接。我自己的经验是:wmma非常适合那种形状规整的GEMM,但在不规则形状的数据上,fragment的布局治理成本很高,用之前一定先确认业务数据结构能不能跟16x16的Tile对得上。

5.2__match_any_sync:找“同伙”的指令

__match_any_sync是一条很冷门但很强大的指令。它的作用是:在一个Warp里,找出所有与当前lane持有相同值的lane,返回一个掩码。签名是unsigned __match_any_sync(unsigned mask, unsigned value)。

这个指令有一个很巧妙的应用场景:动态数据聚合。比如你在做图算法时,每个顶点要发给邻居一些消息,但如果好几条消息的值恰好相同,你可以让这些顶点共享同一份数据,减少重复计算和重复访存。用__match_any_sync一下就能知道哪些lane的值是一样的,然后用__shfl_sync让其中最低位的lane把值广播出去,其他lane直接拿来用。

匹配指令还有个兄弟__match_all_sync,它返回一个布尔值表示“Warp内所有lane的值是否都相同”,如果相同,掩码是所有32位;如果不同,传出的掩码是未定义的。所以我一般只用__match_any_sync,因为它永远能给你一个可靠答案。

5.3 归约指令与syncwarp:小细节大作用

除了__reduce_add_sync,还有__reduce_min_sync、__reduce_max_sync、__reduce_and_sync、__reduce_or_sync、__reduce_xor_sync。它们都是sm_80+架构才有的,作用是把一个Warp里的某个值按指定运算归约到所有lane。这些指令本质上就是“shuffle循环的硬件版本”。我前面已经给过__reduce_add_sync的代码,使用条件和__CUDA_ARCH__ >= 800绑定就行。

还有一个容易被忽略的是__syncwarp(mask)。它的作用是在Warp内做轻量级同步,比__syncthreads()轻得多,因为它只同步一个Warp,不涉及其他Warp。什么场景用得到?当你在Warp内先让部分lane写共享内存,再让另外一些lane来读,中间需要保证写入可见时,__syncwarp()就派上用场了。它产生的是memory fence级别的效果,比__syncthreads()更细粒度。但我提醒一句,如果你不确定当前Warp到底哪些lane在活动,最好先拿到准确的mask,否则__syncwarp也可能出现不可预期的行为。

6. 常见问题与避坑实录

6.1 mask参数写错引发的“灵异现象”

Warp函数带_sync后缀的,都必须显式传入一个mask参数,表示“哪些lane会参与这个函数调用”。最容易犯的错是把mask写成0——不是“没有线程参与”,而是“所有线程都不参与”,行为直接是未定义的,程序可能干脆卡死或者返回错误结果,而且这种报错特别难查,因为它不是编译错误,而是运行时的逻辑错误。另一个常见错误是在条件分支里不带脑子的传0xffffffff。如果整个Warp都进了同一个分支,那没问题;但如果只有一部分lane进了分支,而你又传了全1掩码,函数会认为没进分支的lane也必须出现在调用点,结果就是死锁或者未定义行为。

我的建议是:能用全1掩码的场景,尽量保证所有lane都在同一控制流里;必须处理子集时,先算清楚当前活动lane的掩码,再传进去。还有一种被很多人接受的实践是mask = __activemask(),但这个函数返回的是“当前调用点的活动lane掩码”,在分支里配合使用也要小心,如果被调用的warp函数内部还有shuffle,掩码计算稍有偏差同样出问题。最稳的还是从业务逻辑层面控制好控制流。

6.2 发散分支里的shuffle:指令集陷阱

Warp发散和shuffle结合是另一个高频踩坑点。比如你写了个if (val > threshold) { float t = __shfl_down_sync(0xffffffffu, val, 1); },看起来没问题,但仔细想想:万一整个Warp里只有lane 0的val > threshold为真,那么执行shuffle时只有lane 0在调用点,其他lane都在else或者分支外面。此时传全1掩码,等于告诉硬件“全部32个lane都会来”,可实际只有1个lane来,最终这个shuffle的结果完全没有意义。

正确做法是:把参与协作的lane掩码算出来,或者让shuffle调用放在所有lane都能执行到的统一位置,用pred去选值而不是用if去隔离线程。我一般会写成类似float t = __shfl_down_sync(mask, val, 1); if (pred) { ... use t ... }这样,让shuffle本身在统一控制流里执行,选值逻辑放到shuffle之后。

6.3 常见问题速查表

现象原因解决办法
归约结果比预期大一倍__shfl_up_sync越界时返回自身值,被误当0用__shfl_sync显式指定源lane,或对srcLane < 0补0
结果在某些线程上正确、某些线程上错误只信任了shuffle之后的局部值,忘了边界lane拿到的是中间结果归约最终值以lane 0为准,或加mask过滤
程序偶发卡死,跑着跑着没反应mask传了全0,或分支内调用warp函数但掩码没算对统一控制流,或用活动lane掩码
编译报错:函数需要早期声明用了__reduce_add_sync但没检查架构版本用#if __CUDA_ARCH__ >= 800包起来
尾部数据浪费严重补0导致整个尾部block空转单独kernel处理尾部,或调整block大小
wmma结果错乱fragment布局假设错误用load/store_matrix_sync,不要手动猜布局

这张表是我自己踩过坑之后整理出来的,大部分都是“初看很迷,仔细想全是细节”。Warp函数本身不难,难的是你对边界条件和参与lane的理解是否足够精确。

这套Warp函数用熟了之后,回头再看那些需要高吞吐的并行内核,思路会很不一样。我个人现在的习惯是:凡是数据交换只发生在一个Warp内部,优先考虑shuffle;凡是需要跨Warp协作,才考虑共享内存和block级同步;凡是遇到“要不要为每个线程都保留完整结果”这种问题,先问自己一次“只让部分lane持有结果,然后用shuffle按需取,行不行”。这样做下来,很多代码的同步开销都能压下去一大截。如果你正在优化归约、扫描这类基础算子,不妨先把今天这些函数逐个在本地跑一遍,刻意在边界条件下测一测,比看一百遍文档都管用。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询