(来源:科技行者)

这项由dphn.ai的研究人员独立完成的研究发布于2026年7月,以预印本形式上传至arXiv,编号为arXiv:2607.23402v1。研究横跨英伟达Ampere、Hopper以及Blackwell(含服务器版与消费者版两种型号)四款显卡,并以Pascal作为历史对照基准,是目前已知首次对英伟达GPU"线程分叉"行为进行跨代际系统性测量的公开研究。
GPU,也就是显卡里的图形处理器,是当今人工智能、游戏渲染和科学计算的核心引擎。与普通电脑CPU"一次处理一件事"的方式不同,GPU的设计哲学是"一口气同时处理几千件事"——它把几千个小任务打包成一组一组,每组32个,齐头并进地执行同一条指令。这种方式就像一支步伐整齐的军乐队,每个人同时迈左脚,效率极高。
但问题来了:如果军乐队里有几个人想迈右脚怎么办?
在GPU的世界里,这个问题叫做"线程分叉"(warp divergence)。当同一组32个线程里,有些线程需要走A路,有些需要走B路,GPU就不得不暂停整齐划一的行进,先带A路的人走一遍,再带B路的人走一遍,让其余人在旁边干等着。这种等待是实实在在的性能损耗,GPU的效率会因此大打折扣。
自2017年英伟达在Volta架构中引入"独立线程调度"(Independent Thread Scheduling,简称ITS)机制以来,业界普遍认为GPU处理线程分叉的方式已经基本定型。Ampere上的测量结果?大概也适用于Hopper和Blackwell吧——这是一个流传甚广但从未被认真验证的假设。这项研究的意义正在于此:它用真实的实验数据,系统性地检验了这个假设是否成立。
一、GPU里的"军乐队困境":什么是线程分叉,为什么它很重要
要理解这项研究,先得弄清楚GPU为什么会遭遇这种困境。
GPU的核心执行单位叫做"warp"(线程束),每个warp里有32个线程同时执行同一条指令。这就像军乐队的一个方阵,指挥给出一个口令,所有人同时响应。这种方式的效率极高,因为控制指令只需要发一次,32个人同时干活。
然而,现实世界的计算任务并不总是这么整齐。假设你在GPU上跑一个程序,需要判断每个线程对应的数据是否大于某个阈值,大的走左路,小的走右路。如果碰巧32个线程里有16个需要走左路,另外16个需要走右路,那GPU就遇到麻烦了——它没办法同时让所有人走不同的路。它只能先"屏蔽"走右路的16个人,带左路的16个人把左边的活干完;然后再反过来,屏蔽左路的人,带右路的人把右边的活干完。两段路加起来,总时间是各自时间之和,而原本只有一半人在干活。
这个问题早在GPU诞生之初就存在,几十年来学术界和工业界提出了各种解决思路。英伟达在2017年推出的"独立线程调度"是迄今为止最重要的架构改变之一——它让每个线程拥有独立的"程序计数器"(可以理解为自己的行进记录),而不再共用一个。这个改变从根本上解决了一个长期困扰工程师的死锁问题:在旧架构里,如果一个线程在等待同组其他线程释放锁,就可能陷入永远无法前进的僵局;独立线程调度让每个线程都能独立推进,不再相互牵制。
但ITS究竟如何影响了线程分叉的实际性能代价?这个问题在七年间几乎没有人系统地测量过。
二、这支研究团队如何搭建实验室:测量方法的设计
要测量线程分叉的真实代价,需要非常精心的实验设计,因为其中有一个容易被忽视的陷阱。
GPU编译器非常聪明。有时候,当它发现程序里有一个简单的分支判断,它不会真的让线程分叉,而是采取一种叫"谓词化"(predication)的策略——让所有32个线程都把两条路的代码都执行一遍,只不过用一个标志位来控制哪条路的结果有效。这种方式下,GPU看起来像是发生了分叉,但实际上32个线程仍然步调一致,效率并没有降低。如果实验设计不够严谨,很可能把这种"假分叉"当成真分叉来测量,得出错误的结论。
研究团队针对这个陷阱做了两重防护。每个测试程序都被刻意设计成足够复杂,让编译器无法把分支谓词化,并且在反汇编代码里人工确认确实产生了真正的分叉指令(在后Volta架构上是BSSY/BSYNC指令,在Pascal上是SSY/SYNC指令)。与此同时,他们还用硬件性能计数器独立验证,确保测量到的是真实的SIMD效率下降。
实验在五款GPU上进行,分别是Pascal架构的GTX 1080(作为ITS之前的历史基准)、Ampere架构的RTX A6000和RTX 3090、Hopper架构的H100、Blackwell服务器版的Jetson AGX Thor以及Blackwell消费者版的RTX 5080。时间测量使用GPU内部的时钟计数器,这个计数器是频率无关的,所以不同GPU之间的结果可以直接比较,不受时钟频率差异干扰。每个数据点取201次测量的中位数,变异系数低于万分之一,结果极为稳定。
静态分析部分则覆盖了更广的范围:研究团队编译了一个包含38个测试程序的语料库,涵盖简单分支、嵌套分支、switch语句、带提前退出的循环、短路求值以及刻意构造的不可规约控制流,针对从sm_80到sm_120的所有架构版本生成并分析了汇编代码。
三、七年来性能代价的真相:线性序列化,一个出人意料的稳定规律
实验的第一个核心发现,可以用一句话来概括:线程分叉的性能代价,在过去七年里几乎没有变化,而且这个代价的规律极为简单。
具体来说,如果一个warp里的32个线程分裂成k条不同的路径,那么整体执行时间大约就是单条路径时间的k倍。研究团队在对数坐标系上画出了所有GPU的测量结果,发现所有曲线都与理想的"k倍线"完美平行。Ampere上每条路径的时间常数约为54,100个时钟周期,Hopper约为58,100个,两款Blackwell约为46,100个,Pascal约为70,100个。这些数字之间有差距,但这个差距来自各自架构的单线程执行效率差异,而非分叉处理机制的不同。
更关键的是:从k=1(不分叉)到k=32(32条路全部分裂)的全程,这种线性关系都成立,没有任何超线性的"重组惩罚"。以Blackwell为例,32路分叉的代价是31.7到31.9倍的单路代价——几乎就是完美的线性乘法,没有任何额外的开销堆叠。
这个发现有一个重要的历史延伸:Pascal架构,也就是2017年之前尚未引入ITS的那代显卡,同样遵守这个线性规律,而且精度高达0.6%以内。这意味着,线性序列化并不是ITS带来的特性,而是SIMT执行模型本身固有的行为,早在现代GPU诞生之初就已如此。
为了进一步确认这个结论,研究团队还用硬件性能计数器独立验证了"执行效率"这一指标。他们使用的是Nsight Compute工具提供的一个比率指标,衡量每条被执行的warp指令中平均有多少个线程是活跃的(满载时是32个)。测量结果与理论预测高度吻合:k=1时是31.9,k=2时是16.2,k=4时是8.3,k=8时是4.25,k=16时是2.23,k=32时是1.21——换言之,效率就是32除以k。四款后ITS显卡的曲线几乎完全重叠。
四、加塞更多任务能解决分叉问题吗?一个反直觉的答案
GPU工程师常见的一个应对延迟的策略是提高"占用率"——让更多的warp同时驻留在同一个SM(流式多处理器)上。GPU在等待内存操作完成时,会自动切换到另一个warp来执行,从而隐藏延迟。这个策略在应对内存延迟时非常有效。
那么,提高占用率能不能同样地"掩盖"线程分叉的代价呢?
这项研究给出了明确的否定答案。实验中,研究团队系统地改变了每个SM上同时运行的warp数量,从极低的占用率一路扫到严重超额订阅的高占用率,然后测量32路分叉的性能惩罚。结果是:无论占用率高低,惩罚始终稳定在约28到31倍之间。Pascal同样如此,惩罚维持在29.9到30.9倍,没有随占用率改变。
原因在于分叉和延迟的本质区别。内存延迟是一种"等待"——warp在等待数据从内存返回,这段时间什么也干不了,但其他warp可以趁机干活来填充这段空闲。而线程分叉是一种"指令膨胀"——一个分叉成k条路的warp会发出k倍于正常情况的指令,这些指令都需要真实的执行时间,没有任何"空闲"可以被利用。多加几个warp进来,只是增加了队列里等待执行的指令数量,并不能让分叉的指令更快执行完。
这个发现对实际工程有直接的指导意义:如果你的程序存在严重的线程分叉,靠调节线程块大小、增加并发来"补偿"是没用的。唯一真正有效的办法是在根源上消除分叉——或者用"谓词化"技术把分支改成全线程并行执行,或者重新组织数据使得同一个warp里的线程尽可能走同一条路。
五、"谓词化":一个简单但有限的解决方案
在介绍静态机制之前,研究团队还专门验证了一个广为人知的优化建议:把小的分支改写成无分支的谓词化代码。
具体操作是这样的:假设原本的代码是"如果条件成立就做A,否则做B",谓词化改写后变成"所有线程都把A和B都做一遍,但只保留各自条件对应的那个结果"。这样一来,32个线程永远保持步调一致,不存在任何分叉。代价是每个线程做了一些"多余的"计算,但由于消除了分叉的序列化,总体效率反而更高。
实验数据非常干净:把一个两路分叉的分支替换为谓词化版本,执行时间从2倍降回1倍,四款后ITS显卡的测量比值都在2.00的0.5%以内。Pascal上的比值是1.94倍降到1倍,略有不同但规律一致。从Pascal到Blackwell,这个优化手段的有效性七年来没有变化。
当然,谓词化并非万能。它适用于两条路都比较短、且两条路的工作量相近的场景。如果两条路各自的计算量都很大,那谓词化反而会让每个线程都做双份工作,得不偿失。这时候更好的办法是重新组织数据,让相同路径的线程聚集到同一个warp里。
六、前进保证的现实边界:ITS承诺了什么,又没承诺什么
独立线程调度的一个核心卖点是"前进保证"——每个线程都能独立向前推进,不会被同warp的其他线程卡住。研究团队通过实验考察了这个保证在实践中的表现。
他们设计了一个32个线程同时竞争同一把锁的测试:每个线程依次尝试获取锁、进入临界区、释放锁。这个测试在所有被测GPU上都顺利完成,32个线程最终都得到了服务,包括Pascal在内。不过研究团队也注意到,这个特定的锁在获取的同一个分支内就会被释放,并没有触发经典的"堆栈死锁"场景——那种场景需要一个线程在warp分叉后一直持有锁等待其他线程到达同一分支,这是旧式SIMT栈模型的致命弱点,研究团队因为不想在共享硬件上制造潜在的无限等待,没有测试这种极端情况。
更有意思的是另一个测试:一个"生产者-消费者"探针,其中一个线程必须先完成某项工作,其他线程才能继续。结果显示,即使"生产者"线程要做一千万次迭代的繁重工作,"消费者"线程也只需要最多两次自旋等待就能观察到生产者的结果。这说明,在没有竞争性同步的情况下,分叉的路径并不是细粒度地交替执行,而是按照结构顺序依次执行到完成。ITS提供了前进保证,但并不意味着路径之间会精细地互相穿插——它们仍然是依次串行执行的,只是每条路最终都能到达终点。
七、汇编代码里的时代印记:从指令栈到屏障寄存器
动态性能代价七年不变的背后,GPU汇编层面的控制流机制却发生了实质性的演变。这部分是研究中技术含量最高、也最值得关注的发现。
在Pascal时代,管理线程重聚的机制是一个基于指令栈的方案,用SSY和SYNC两条指令来操作。SSY的作用是把一个"重聚点"的地址压入一个专属于这个warp的栈里,SYNC则是从栈里弹出这个地址并让所有线程在此汇合。嵌套的分支就产生嵌套的SSY/SYNC对。研究团队检查了Pascal的整个测试语料库,确认没有出现哪怕一条BSSY、BSYNC或BREAK指令——这些都是后来才有的东西。
Volta之后(包括Ampere和Hopper),这套栈机制被完全替换成了"屏障寄存器"方案:BSSY在某个目标地址建立一个重聚屏障,BSYNC则等待这个屏障被所有相关线程触发。这种方式更灵活,也更便于编译器精确控制重聚时机,不再依赖隐式的硬件栈行为。
到了Blackwell,又多了一层新东西,这也是整个研究中最新颖的发现之一。
八、Blackwell的新发明:双层屏障与"可靠"标记
在Blackwell(sm_100及之后)的汇编代码里,BSSY和BSYNC指令多出了一个在Hopper及更早架构上不存在的修饰符。研究团队发现这个修饰符占用指令编码中的2个比特位,有两个非零取值,分别是`.RECONVERGENT`和`.RELIABLE`。
`.RECONVERGENT`标记的是真正的"终极重聚点"——这是编译器判定所有分叉路径必然汇合的那个指令位置,也就是控制流图中的"立即后支配节点"(IPDom)。这类屏障从不被提前跳出。
`.RELIABLE`标记的则是一种"早期局部重聚点",出现在循环展开迭代的边界处或者短路求值的子句边界处。它嵌套在`.RECONVERGENT`屏障的内部,而且部分线程可以通过BREAK指令提前离开这个屏障,而无需等待所有线程都到达。
这个双层设计使得编译器能够在一个大的分叉区域内部设置多个"中间站",让早点完成工作的线程能够在中间站短暂重聚,而不必一路等到最终的汇合点。这种精细控制是单一的裸屏障无法表达的,代表了控制流表示能力的实质性进步。
研究团队还用一个非常直接的方法检验了这个字段是否具有实际的运行时效果:他们直接在编译好的二进制文件中修改这2个比特位,然后运行修改后的程序,观察结果有没有变化。为了确认驱动程序确实执行了被修改的字节而非重新推导,他们首先用一个"正向控制实验"验证:禁用一条存储指令确实会改变输出结果,证明驱动程序是按字节执行的。在此基础上,他们把屏障字段的值改遍了所有可能的组合,包括制造出`.RECONVERGENT`类型的BREAK(这是汇编器本不会产生的组合)和错配的BSSY/BSYNC对,结果在所有尝试的程序上,输出结果、程序完成、warp级别的前进保证以及性能计数器都没有任何变化。由此可以判断,这个字段在测试范围内是一个静态的编译器分类标记,而非运行时控制信号。
九、Blackwell独有的另外两件新东西
除了双层屏障,Blackwell还带来了两个在Ampere和Hopper上完全不存在的控制流特性。
第一个是统一分支指令BRA.U。当编译器能够证明一个分支对warp里所有32个线程的结果都相同(也就是说,不存在分叉的可能),它就会发出BRA.U而不是普通的BRA。BRA.U前面通常还有一条统一数据路径上的ISETP比较指令。在研究的测试语料库中,Blackwell上出现了23次BRA.U,Ampere和Hopper上是零。这个机制让均匀分支和分叉分支在汇编层面有了明确的区分,是Blackwell在控制流表达上的一个语义增强。
第二个是显式的部分掩码warp同步指令WARPSYNC。在使用`__shfl`或`__syncwarp`等warp内同步原语时,如果使用的是一个与数据相关的掩码(也就是说,并非所有32个线程都参与同步),Blackwell会显式地发出WARPSYNC指令,而Ampere和Hopper只是发出裸的shuffle指令,没有任何屏障。这意味着Blackwell把部分掩码重聚这个概念明确地编进了ISA,而不是留给硬件隐式处理。
此外,研究团队还注意到,Blackwell把活跃的重聚屏障嵌套深度上限从之前架构的4层降低到了3层,当需要第4层嵌套时,编译器会用谓词化来代替屏障,而不是溢出到内存(BMOV指令在所有被测架构上都从未出现过)。
十、重聚点的"漂移":从Ampere到Blackwell,越来越精确
通过对38个测试程序在所有架构上的汇编代码进行系统分析,研究团队还量化了一个具体的演变趋势:编译器把重聚屏障放在哪里,相对于理论上最优的位置(立即后支配节点,IPDom)是偏早、偏晚还是恰好对齐?
在Ampere上,72.7%的分叉分支恰好在IPDom处重聚,而有29个分支的重聚点晚于IPDom——这是"延迟重聚",也是经典SIMT栈模型的遗留特征,在那个模型里,嵌套循环等复杂结构往往会导致重聚被推迟。到了Hopper,恰好在IPDom处重聚的比例升至90.8%,延迟重聚的数量降至7个。到了Blackwell,延迟重聚进一步萎缩到仅剩2个,但由于引入了`.RELIABLE`类型的早期局部屏障,提前于IPDom重聚的案例增加了,导致恰好在IPDom处重聚的比例回落至83.2%。这不是一种退步,而是一种新的精细化:那些"提前"重聚的点并不是真正的提前合并,而是双层屏障体系中的中间站,最终仍然会在`.RECONVERGENT`标记的IPDom处完成真正的汇合。
延迟重聚从Ampere的29个降至Blackwell的2个,这个数字本身就说明了很多问题。旧式SIMT栈的一个主要副作用是重聚时机难以精确控制,导致本可以更早合并的线程被延迟等待;现代屏障寄存器方案让编译器对重聚时机有了更精细的掌控,趋势是越来越向IPDom靠拢。
归根结底,这项研究的核心发现可以用一句话来概括:**GPU线程分叉的性能代价是一个稳定的规律,但管理这个代价的内部机制在不断演化。**
对于写GPU代码的工程师来说,这意味着从Ampere到Blackwell,性能预测模型可以直接复用:k条路径就是k倍的时间代价,效率就是32除以k,谓词化能有效消除小分支的代价,增加占用率无法掩盖分叉造成的指令膨胀。这些规律七年不变,甚至可以追溯到ITS出现之前的Pascal时代。
对于做GPU工具开发的工程师来说,情况则恰恰相反——Blackwell引入的双层屏障、统一分支指令和显式部分掩码同步,是实实在在的新机制,任何针对旧架构设计的二进制分析工具或SASS层面的模型,如果不更新对这些新指令的支持,都会对Blackwell的控制流产生误解。
这项研究最值得反复品味的一点,也许是它揭示的那种"表面稳定,内部演化"的张力。程序员看到的性能行为七年如一,而硬件和编译器之下的机制已经经历了从指令栈到裸屏障、再到带可靠性标记的双层屏障的三代演变。这种演变没有体现在任何官方文档里,只能通过这样的反汇编实验才能观察到。它提醒我们,"黑盒"背后的故事往往比我们以为的更丰富。感兴趣的读者可以通过arXiv编号2607.23402查阅完整论文,所有实验方法和数据都有详尽记录。
Q&A
Q1:GPU线程分叉的性能代价在Ampere和Blackwell上有什么区别?
A:根据这项研究的测量结果,Ampere和Blackwell上线程分叉的性能代价遵循完全相同的规律:k条分叉路径就消耗k倍的时间,执行效率恰好是32除以k。两者的唯一区别是每条路径的基础执行速度,Blackwell比Hopper快约15到20%,这反映的是整体单线程性能的提升,而非分叉处理方式的改变。
Q2:增加GPU占用率能不能减少线程分叉的性能损失?
A:不能。这项研究用实验数据证明,32路分叉的性能惩罚在从轻度到严重超额订阅的整个占用率范围内始终稳定在约30倍左右,包括Pascal在内的所有被测架构都是如此。原因是线程分叉会让GPU发出更多指令,而提高占用率只能掩盖内存延迟,无法减少需要执行的指令数量。
Q3:Blackwell的BSSY/BSYNC指令里的.RELIABLE和.RECONVERGENT标记有什么实际作用?
A:根据这项研究的测试,这两个标记在实际运行时没有可观测到的效果——研究团队直接修改了编译好的二进制文件里的这2个比特位,程序的输出、执行效率和性能计数器都没有发生变化。目前的结论是,这是编译器和汇编器用来区分"真正的汇合点"与"可提前离开的中间屏障"的静态分类标记,记录的是控制流结构信息,而非运行时控制指令。

0755-75968522

