☰
GPU Die Scaling破坏细粒度调度:为什么大卡跑不满kernel?
2026/9/30 1:27:35 网站建设 项目流程

做GPU计算的人大概都有这种体验:新卡到手,SM数量比上一代翻了一倍不止,满心期待kernel能跑得飞快,结果一测,性能只涨了三成,甚至有些场景不升反降。打开Nsight一查,一堆SM在空转,全局的线程块分配像是堵了车。我以前习惯性把这归咎于“大卡优化没做”,直到最近认真读了一篇专门讨论GPU Die Scaling如何破坏细粒度调度的论文,才反应过来:硬件越做越大之后,芯片内部的细粒度调度机制本身正在失效,这已经不是一句“优化代码”就能解决的问题了。

这篇论文的核心内容我前前后后读了两遍,又对照自己平时调kernel的经验验证了一遍,觉得非常值得拿出来聊聊。它涉及的背景知识比较多,我会把CTA、warp、全局分发器、局部性这些概念尽量用项目实战的角度讲清楚,也会把论文里几个关键发现拆开揉碎。无论你是做CUDA算子开发、GPU驱动优化,还是在PyTorch里训练大模型时被“显卡利用率低”反复折磨,这篇解读应该都能给你一些新思路。

1. 整体设计与思路拆解:为什么细粒度调度会“崩”

1.1 论文在讲什么问题:GPU变大,调度器却在原地踏步

每一代旗舰GPU的规格表几乎都在重复同一个故事:半导体工艺在往下走,晶体管密度往上翻,SM数量跟着水涨船高。消费级旗舰已经从几十个SM的规模慢慢往上百个走,数据中心卡更是一路堆到上百甚至更多。如果你只盯着算力数字,会觉得GPU的进步非常线性。但真正跑过分布式训练、大模型推理或者科学计算kernel的人都知道,从一块小卡换到一块大卡,性能并不会按SM数量等比缩放,很多场景甚至会出现“加了核心反而跑不满”的怪现象。

这篇论文切的角度就是这件事:Die Scaling,也就是芯片在工艺缩比下不断把更多计算单元塞进同一块die的过程,正在系统性破坏GPU内部原本高效的细粒度调度。细粒度调度指的是硬件把海量的小任务——具体来说就是线程块(Thread Block / CTA)——快速、均匀地分发到各个SM上执行的那套机制。论文的核心论断是:当SM数量增长时,这套本该隐藏在硬件背后的机制开始成为新的瓶颈,而且它不是单个kernel代码层面的问题,是整个芯片规模扩展方式的问题。

说实话,我最初看到这个题目时以为又是那种“论文式废话”:用一堆理论模型证明一个大家都知道的结论。但读进去之后发现,论文把调度问题拆得非常细,从全局工作分配器的原子操作竞争,到局部性丢失,再到依赖关系造成的尾延迟,每一步都有数据和模型支撑。对做GPU计算的人来说,这是一篇能改变你排查性能问题思路的文章。

1.2 细粒度调度为什么重要:CPU调度和GPU调度的本质差异

聊调度之前,先分清楚CPU调度和GPU调度是完全不同的玩法。CPU的调度是操作系统层面的东西,线程数量通常只有几十个,调度器要考虑的是公平性、缓存亲和性、优先级。而GPU的调度是硬件层面的东西,一张卡上同时运行着成千上万个轻量线程,硬件必须以极低的开销在纳秒级别切换执行单元,任何软件介入都会把性能拖垮。

GPU整个执行模型可以理解成一个“大批量订单分发”的过程。你启动一个kernel,实际上就是向硬件提交了几十万甚至上百万个线程块。硬件端的GigaThread引擎或者叫全局工作分配器,会按顺序把这些线程块分配给各个SM;每个SM拿到一个线程块之后,再在自己的warp调度器里把其中的线程分组执行。这个机制效率极高,因为它完全不需要操作系统参与,纯硬件完成,线程块之间的切换开销几乎为零。

但是,细粒度调度成立的前提是“分发这件事够快”。线程块数量少的时候,全局分发器一秒钟处理几千个块完全没压力;可当芯片规模扩大,SM数量翻倍,线程块数量也随之翻倍,全局分发器成了一个单点。论文里指出的问题就在这里:硬件分发器虽然快,但它毕竟是集中式的,当一个die上SM数量过多,所有线程块的出生、分配、回收都挤在同一个逻辑节点上,原子操作和队列操作开始互相竞争。你可以把它想象成一座城市只有一条上高速的匝道,车多了,匝道就成瓶颈了,无论高速车道本身修得多宽都没用。

1.3 为什么单独把“Die Scaling”拎出来说

Die Scaling本身是个老话题,芯片制造一直在往更小的工艺节点走。但这篇论文强调的不是晶体管本身缩小,而是“在同一块die上集成更多SM”这件事改变了调度问题的性质。以前GPU规模小,调度器裕量很大,性能瓶颈总在计算、访存或者内核代码本身;现在SM数量上了百,调度器本身的吞吐量成了与计算吞吐量同等级别的约束。这个转变非常关键,因为过去几十年积累的性能优化方法论,默认都是“调度不是瓶颈”,而这篇论文告诉你,这个前提在大芯片上已经不成立了。

2. 核心概念解析:从CTA、Warp到硬件分发器

2.1 Die Scaling到底是什么

Die Scaling字面意思是芯片的缩放。半导体工艺从28nm到14nm到7nm到5nm,每个晶体管占的面积在缩小,所以同样的die面积能放下更多逻辑单元。对GPU来说,这个“更多逻辑单元”主要体现在两个方向:一是单个SM内部的计算单元更多,二是整个芯片上的SM数量更多。

论文关注的是后者。SM数量增多的直接后果是,一次kernel启动需要填满的工作量也更大了。假设一个SM一次能容纳16个线程块,32个SM的卡需要512个线程块才能跑满,128个SM的卡则需要2048个线程块。线程块数量大了,调度系统的压力线性增长,但硬件调度器的处理能力并没有跟着线性增长。其中最关键的资源就是全局工作分配器相关的原子计数器、FIFO队列和分发总线。这些资源不像SM那样能随着工艺缩比自然扩展,因为它们本质上是串行逻辑,加再多硬件也改变不了“所有线程块都要经过同一条路径”的现实。读这篇论文时我脑子里反复出现一句话:扩展计算单元是容易的,扩展集权式调度是难的。

2.2 CTA与Warp:两个容易混淆的调度粒度

网上经常有人问cooperative thread array在GPU计算里是什么概念,跟warp什么关系。这里正好借论文把这两个概念彻底讲清楚。

Warp是执行层面的概念。CUDA里32个线程组成一个warp,硬件调度器每次调度一个warp到SM的计算单元上执行。warp是“一次取指、一次执行”的最小单位,它的粒度是固定的,由硬件架构决定。

CTA(Cooperative Thread Array)是编程层面的概念。你在CUDA里写的线程块(Thread Block)在硬件上就是一个CTA,一组可以协同工作、共享数据和同步的线程集合。CTA的大小由程序员决定,可以是64、128、256甚至更多线程。一个CTA会被整个分派给某一个SM,并在该SM上驻留到执行完毕,不能拆分到多个SM。

两者的关键区别在于:warp是“执行流水线上的任务切片”,CTA是“任务本身的最小分发单位”。全局调度器把CTA分给SM,SM再把CTA内的线程切成warp来喂给执行单元。所以细粒度调度实际上包含两个层级:全局层级的CTA调度,以及每个SM内部的warp调度。论文讨论的破坏点,主要发生在全局层级的CTA调度上,也就是CTA分发到SM的这一环。

有人问kernel算子是什么,在GPU计算中,一个kernel就是一次完整的CTA分发过程。你启动一个grid,硬件生成所有CTA,然后经过全局分发器一个送进各个SM。所以kernel执行的全流程可以简单概括为:启动kernel、生成grid与CTA、全局调度器分发CTA、SM接收CTA、SM内warp调度器调度warp、执行计算与访存、CTA完成并回收。这个流程里每一步都可能成为瓶颈,而论文论证了在die scaling背景下,分发CTA这一步正在成为新的天花板。

2.3 硬件分发器:全局调度与本地调度的分工

NVIDIA的硬件里,全局工作分配工作由一组专用逻辑负责,常被称为GigaThread引擎,它负责生成线程块ID,并以流式方式把新块分配给有空闲槽位的SM。每个SM侧还有一个本地调度器,负责从常驻的CTA中选择可以发射的warp。

全局分发器和本地调度器的分工很有意思:全局分发器只在CTA粒度上工作,频率不需要高,但必须快速响应;本地调度器则在每个时钟周期都要做选择,是整个系统的“高频核心”。传统上,这种分工非常高效,因为全局分发器的事务量相对很小。但当芯片变大,一次kernel启动的CTA数量暴增,全局分发器的事务量也跟着暴增,之前“不算事”的开销占比开始上升。

论文中对这个问题的量化思路是:把全局分发器建模为一个串行服务队列,CTA到达速率由grid大小决定,服务速率由硬件分发逻辑决定。当grid的CTA数量超过分发器吞吐能力时,就会形成排队效应,SM开始出现饥饿,利用率曲线从线性变平缓。这是一种典型的排队效应,只不过发生在硬件层面。对这个模型的推导,让原本模糊的“调度慢”变成了一个可预测的量化指标,这是论文里我觉得最有价值的部分。

3. 论文核心发现:Die Scaling如何破坏细粒度调度

3.1 全局工作分配器的原子操作瓶颈

第一个核心发现是全局分发器内部的原子操作成为热点。线程块ID的分配在硬件上依赖原子计数器,多个SM并行地到全局分发器那里“领任务”,本质上是对同一个计数器做原子自增。小芯片上SM数量少,这种竞争可以忽略;但SM数量上到几十上百之后,所有SM同时伸手到同一个分发器拿块号,原子操作的等待时间就会显著拉长。

我在实际开发中也踩过类似的坑:用全局内存的atomicAdd做动态任务分配,线程数量一多,性能立刻掉一半。硬件分发器里的原子操作虽然比软件原子操作快得多,但竞争的基本逻辑是相同的——同一个共享资源被大量并发访问,最终吞吐受限于这个共享资源的服务速率,而不是计算单元的数量。论文指出,唯一的出路是让分发逻辑分布式化,比如每个SM预取一段线程块ID,自己维护本地计数器,但这又会带来负载不均衡和跨SM协调的问题,属于典型的分布式系统难题。

3.2 尾部效应与负载不均衡

第二个发现和线程块调度的“尾部”有关。GPU的任务并行有一个经典现象:如果grid的CTA数量刚好是SM容量的整数倍,任务可以完美铺满;但现实中往往不是整数倍,最后一批CTA会让一部分SM先空闲下来,整个kernel的执行时间被“拖尾”拉长。

这个问题在小芯片上不明显,因为最后一批CTA的等待时间很短。但随着die scaling,SM数量变大,同一个grid内CTA数量也变大,而grid大小往往不是程序员刻意对齐到SM容量的。尾部效应的影响被放大:当调度器分发器的排队延迟变大时,最后一批CTA到达SM的时间也会更晚,尾部延迟叠加排队延迟,造成整体的执行时间比理论计算多出一大截。

论文里特别分析了依赖型kernel的情况,比如需要全局同步、或者CTA之间存在数据依赖的任务。这类任务里,后面的CTA必须等前面的CTA完成,本身就有天然的尾延迟。当分发器排队延迟加入之后,尾延迟进一步恶化。这让我想起实际训练大模型时经常遇到的“墙钟时间虚高”问题:理论上FLOPs算得明明白白,但跑起来就是比预估慢,很多时候就是调度层的尾部效应没有算进去。

3.3 数据局部性丧失

第三个发现是调度对大芯片访存局部性的破坏。传统GPU的线程块调度有一个隐性优点:块之间的分发顺序是近似顺序的,相邻ID的CTA往往会被送到相邻的SM,而这些CTA访问的数据往往在地址空间上也相邻,所以L2缓存的命中率会比较高。这个优点在SM数量多了以后开始失效。

原因很简单:当全局分发器排队时,SM空闲的顺序和被分发CTA的顺序脱节了。一个SM空下来,分发器给它派发的可能已经是远处排队的某块,这个块要访问的数据不一定和周边SM正在处理的数据共享缓存行。多个SM之间对L2的重用关系被打乱,局部性红利丢失,访存路径上的压力上升。

我自己在调kernel时就有体验:同一个算法,按block ID顺序处理数据可能比随机映射快不少。论文的结论等于给这个观察找到了理论依据——当SM数量小的时候,顺序分发自然保持了局部性;SM数量大时,分发顺序和缓存局部性的强绑定关系被分布式排队打散,性能退化就出现了。

3.4 依赖关系下的调度停滞风险

另一个有趣的点是,细粒度调度被破坏之后,可能触发依赖型任务的“类死锁”状态。GPU的CTA调度本来不保证执行顺序,同一个grid内的CTA理论上可以以任意顺序执行,因此硬件分发器不感知CTA之间的依赖。如果程序使用全局同步机制,比如cooperative groups里的grid.sync(),那么所有CTA必须都到达同步点,任何一个CTA迟迟未分发都会卡住整批。

die scaling让这个问题变得更危险:CTA数量越大,全局分发器的排队越严重,极端情况下,某些CTA迟迟没有被分发,而其他CTA已经完成同步并等待,相当于所有SM被集体卡住。这种现象虽然不完全是传统意义上的死锁,但效果类似——整体吞吐归零,直到滞后的CTA被分发器处理完毕。论文里认为,这种“调度引起的停滞”会在更大规模的GPU上越来越频繁,程序员如果继续假设调度是绝对可靠的,就会遇到莫名其妙的长尾任务。

4. 实操视角:论文结论如何影响你的CUDA程序

4.1 怎么判断你的kernel已经撞上调度瓶颈

看完论文先别急着改代码,第一步是判断自己的场景是不是真撞上了调度瓶颈。我推荐三个办法,都不复杂。

第一,看Nsight Compute的Schedule分析,重点看SM Active Warps的占比。如果平均活跃warp数低于理论峰值,但SM内指令发射并不拥塞,说明SM在等任务而不是在等计算,调度分发嫌疑很大。

第二,做个简单的对照实验:把同一个kernel的grid切成两半跑,一种用大grid小CTA,一种用小grid大CTA(保持总线程数一致)。如果大grid版本的性能明显差于小grid版本,并且SM利用率更低,说明全局分发压力是真实存在的。

第三,看kernel启动耗时和运行耗时占比。在现代GPU上,如果一个小kernel的启动和分发开销接近甚至大于实际计算时间,调度开销已经压不住了。这也是为什么很多框架会做kernel fusion,把一堆小kernel合并成一个大kernel,表面上是减少启动次数,深层逻辑就是减少全局调度器的压力。

这里面最坑的是,调度瓶颈的“症状”和访存瓶颈非常像:都是SM利用率低、执行时间比理论值长。区别在于,访存瓶颈时SM大部分时间在等内存返回,而调度瓶颈时SM在等新块分配。Nsight里的Stall Reasons如果显示等待资源占比高,而DRAM吞吐不高,就该怀疑调度了。

4.2 缓解方案:调代码可以有限度地对抗调度瓶颈

论文主要是在诊断问题,但也给了程序员一些可以操作的思路,我结合自己的实践整理成几种有效方案。

一是改用persistent kernel。这种模式不依赖全局分发器反复生成新CTA,而是启动固定数量的常驻CTA,每个CTA自己循环处理数据块。相当于把调度器的工作卸载了一部分给软件,让每个SM去主动“取任务”。这在小芯片上收益不明显,在大芯片上收益可能非常可观,因为它避开了全局分发器的排队。我第一次用persistent kernel是在处理稀疏矩阵的时候,原本大grid带来的调度开销让kernel跑得很憋屈,改成常驻块后性能直接提升了一截。

二是尽量控制grid的CTA总量。CUDA编程里经常有人无脑把block数量设成“线程数/256”甚至更大,然后让硬件自行调度。但在大芯片上,CTA数量过多并不是好事。建议把block数量控制在比SM容量稍多一点的范围,比如SM总数乘以每个SM最大驻留块数再乘一个1.5到2的系数。这样既能保证负载均衡,又不至于让分发器超载。

三是尽可能让CTA之间的数据访问保持局部性。既然die scaling会打乱调度顺序,那就要在编程层面主动把数据布局和block ID绑定,让相邻block处理相邻数据,这样即使在调度顺序被打乱的情况下,L2的局部性损失也能最小化。

四是善用cooperative launch和grid sync,但一定要清楚它们的代价。cooperative launch要求所有CTA同时驻留,本质上是放弃了调度器“边分发边执行”的流水线能力,在大芯片上对资源要求极高。如果确实需要全局同步,尽量把grid控制在可同时驻留的规模,否则会被迫等待所有CTA调度完成,反而放大调度瓶颈。

4.3 硬件设计与未来架构的启示

论文的最终落脚点是硬件层面。如果die scaling继续下去,集中式全局分发器迟早会成为不可忽视的瓶颈,未来架构大概率会走向层级化调度:多个SM组成一个cluster,cluster内部有自己的分发器,全局调度器只做cluster级别的分配,cluster内部再细粒度调度。这种设计类似分布式系统中的两级调度,牺牲一定的均衡度来换取可扩展性。

从软件角度看,编程模型也会跟着变化。CUDA已经在往thread block cluster的方向演进,cluster内可以做硬件级同步和分布式共享内存。cluster本质上是把“CTA调度”拓展出了一层中间粒度,让程序员可以显式控制任务在die上的放置位置。论文的观点刚好印证了这个趋势:细粒度调度不能只靠硬件黑盒,必须让软件有更多控制权,才能在超大芯片上维持高利用率。

5. 常见问题与排查技巧实录

5.1 调度相关常见问题速查

我把实际工作中遇到的和调度问题容易混淆的几种情况整理成了表格,方便对照排查。

现象可能原因排查方向
SM利用率低但DRAM吞吐不高调度分发排队用persistent kernel对比测试
小kernel启动耗时占比过大全局分发器开销做kernel fusion合并
加大grid的CTA数反而变慢原子竞争、队列排队降低CTA数量,用grid-stride
同规模任务在大卡上性能缩水尾部效应+调度排队叠加检查调度时间曲线
cooperative launch卡死或极慢全局调度能力不足改用小grid或软件同步

这张表不能当万能药,但可以帮你快速判断该往哪个方向深挖。我在排查性能问题时的习惯是:先把调度嫌疑排除掉,再去看访存和计算。因为调度问题最隐蔽,常规性能分析工具给的提示最模糊,越早排除越好。

5.2 几条实操心得

最后分享几条我自己折腾过来的经验。

第一,不要迷信SM数量。规格表上的SM翻倍只是理论算力的翻倍,不代表你的kernel能拿满。真正决定性能的往往是“调度+访问”这条暗线。做性能预估时,把硬件调度的开销预留10%到20%的裕量,大芯片上不会吃亏。

第二,做GPU驱动开发和底层优化的人,可以多关注硬件分发器的行为。很多驱动层面的性能怪问题,比如kernel启动延迟不稳定、并发流互相影响,根源可能就在全局分发器的竞争上。驱动层如果能给不同流分配独立的分发通道,是比用户态能做的更彻底的优化。

第三,如果你在用PyTorch或其他框架训练大模型,遇到扩展性差的问题,除了检查数据加载和通信,也不妨看看底层kernel的调度特性。框架层面很多融合算子其实已经把调度优化做进去了,但自定义算子往往没有这层考虑。写自定义CUDA算子时,尽量避免“小而多”的kernel,能融合就融合,对调度器友好一点,整体收益会超出你的预期。

第四,对大芯片上的grid-stride loop,我个人的经验是块数设为SM数的4到8倍通常是个甜点区。太少负载不均衡,太多调度开销增大。具体数值还是要看kernel的访存特征和CTA大小,但至少能有一个不错的起点。

这篇论文给我最大的收获是:GPU性能优化的下一个战场,已经从“把单个SM喂饱”变成了“让几百个SM协作时不互相拖累”。理解了die scaling对细粒度调度的破坏,很多以前解释不了的神秘性能下降都有了解释。如果你最近也在为某张新卡的利用率头疼,这篇文章值得花两小时仔细读一遍,然后对照你手里的kernel重新看一遍Nsight的数据,多半能看到不一样的东西。

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

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

立即咨询