GPU 调度与抢占:warp 选择、block 分配、上下文切换为什么贵
更新时间:2026-09-25。上一篇 GPU 架构深入:SIMT、warp 调度与内存层级 建立了一个核心印象:GPU 靠大量 warp 之间的快速切换隐藏访存延迟。但那里留下了一个没展开的问题——warp 之间的切换几乎免费,为什么两个客户进程之间的 GPU 上下文切换却贵到操作系统尽量避免?本篇把调度分两层讲透:SM 内部的 warp 级调度(微观、零成本),以及整个 GPU 在多个上下文之间让出与抢占(宏观、毫秒级)。分支发散如何影响 warp 就绪,可配合 SIMT 与 warp 分支发散 一起读。
本文要回答的问题
- 一个 SM 上同时有几十个就绪 warp,每个周期到底发射谁?GTO 策略为什么比轮询更常用?记分牌和双发射在其中起什么作用?
- thread block 是怎样被分配到具体 SM 的?为什么 block 一旦落地就不再迁移?
- 都说 warp 切换零开销,为什么 GPU 进程的上下文切换却是毫秒级?要保存的状态到底有多少?
- 传统 GPU 为什么只能等 kernel 执行完才让出?「长 kernel 把显示器卡死」的历史问题从哪来、后来怎么解决?
- 图形上下文的像素级抢占与计算上下文的指令级抢占差别在哪?MPS 时间片和 MIG 分区各用什么思路消除切换?
一、warp 级调度:两个就绪 warp 之间选谁
先把视角放在单个 SM 内部。一个现代 SM 上同时驻留几十个 warp(例如 64 个),它们的完整寄存器状态始终活在寄存器堆里——这正是 GPU 架构与 Warp 调度 讲过的「用空间换时间」。每个时钟周期,warp scheduler 面对的不是「要不要切换」,而是一个更具体的选择问题:ready warp 往往不止一个,这一拍的发射槽给谁。
判定就绪靠的是 scoreboard(记分牌)。每条指令发射前,调度器查它的源操作数是否就绪、目标功能单元是否空闲:操作数还在等访存结果或长延迟运算,这个 warp 就被标记为未就绪,不参与本轮仲裁。一条全局内存加载动辄几百周期,warp 在发射加载后立即挂起,调度器一拍之内就转向别的 warp——这里的「切换」没有任何保存恢复动作,因为下周期切回来时,寄存器和程序计数器一个都没动过。这是理解全篇数字的基准:warp 间切换成本为零周期,状态常驻是前提。
仲裁策略有两种典型选择。round-robin(轮询)在就绪 warp 间平均分配发射机会,公平但对 cache 不友好:连续几拍访问的地址属于不同 warp,数据局部性被打散。当代 NVIDIA GPU 默认采用的是 GTO(Greedy-Then-Oldest,贪心优先于最老):一个 warp 就绪后,尽量连续发射它的指令,直到它遇到长延迟停顿,再转而服务等待最久的 warp。GTO 的好处是让单个 warp 快速推进到下一次访存,把更多在途请求压向内存系统,同时改善 L1 的地址局部性。学术测量显示,在多数负载上 GTO 比轮询高出数个百分点的 IPC,代价是 warp 间公平性下降——但 warp 本来就不属于不同租户,SM 内部无所谓「公平」,总吞吐才是目标。
双发射(dual issue)是另一层选择。新一代 SM 每拍可从同一个 warp 发射两条独立指令(如一条整数加加一条访存),调度器额外检查两条指令的操作数互不依赖、且使用不同功能单元。它本质上是 warp 内的指令级并行,和 warp 间切换是正交的两种延迟隐藏手段。
需要把「延迟隐藏」量化一下。假设一条全局加载往返约 400 周期,而 SM 每拍都希望有指令可发,那么单靠一个 warp 必然饿死;维持满载所需的就绪 warp 数,粗略等于「平均延迟除以每 warp 平均每多少拍能贡献一条可发射指令」。访存密集的 kernel 里这个数可能要到三四十,这就是为什么 SM 要支持几十个并发 warp、为什么寄存器容量要按最坏情况配满。GTO 相比轮询的另一个收益在这里:连续发射让一个 warp 尽快把多条独立加载同时抛出去,在途内存请求数(MLP,memory-level parallelism)更高,内存级延迟被进一步并行摊薄。调度器实现上并不神秘:每个 warp 维护一个「年龄」计数和几位就绪状态,组合逻辑一拍内完成筛选与仲裁,面积相对执行单元小到可以忽略。
二、block 如何分配到 SM:一次性落地,不再迁移
把镜头拉到芯片级。kernel 启动时,驱动把 grid 切成大量 thread block(CTA),全部放进一个全局分发队列;多个 SM 从队列里领取 block。分配的硬约束是资源:每个 block 要声明线程数、每线程寄存器数、共享内存大小,SM 必须同时有足够的寄存器堆、共享内存和 warp 槽位才接纳它。于是 block 不是平均分的——小 block 能让一个 SM 塞下十几个,大 block 可能只占两个,分发器按「谁还有空资源」动态派活。
这里有一个关键的不灵活之处:block 一旦分配到某个 SM,就钉死在那里,直到执行完毕,中途不迁移。原因和上一节同源——block 内所有 warp 的寄存器状态、共享内存内容都物理地位于该 SM,搬走它等于做一次完整的 warp 状态转储,代价等同于后面要讲的上下文切换。调度器能做的只是决定还剩多少空间、再塞几个新 block 进去。这也解释了 SIMT 与 warp 分支发散 里强调的 occupancy 为什么重要:SM 上驻留的 warp 越多,记分牌手里能挑的就绪 warp 就越多,延迟隐藏越充分;而驻留数恰恰由 block 的资源形状在分配那一刻决定。
这个分发模型还有两个值得知道的副作用。其一是尾部效应:grid 里最后几个 block 往往只落在少数几个 SM 上,其余 SM 已经空闲却无法接手,kernel 的收尾时间被最慢的几个 block 拉长,这也是 block 数应远多于 SM 数的原因之一。其二是 block 间在硬件层面没有公平和顺序保证,先启动的 block 可能后完成,因此需要跨 block 协作的算法(如全局同步)必须走 kernel 边界或专门的协作组机制,不能假设执行次序。一种反过来利用「不迁移」特性的写法是 persistent kernel(常驻 kernel):启动恰好填满全机的少量 block,让它们反复从队列里认领工作,从而省掉反复启停 kernel 的开销——代价是这些 block 长期占住 SM,对抢占和共享更不友好。
三、上下文切换为什么贵:要搬走的是一整个 SM 的状态
现在回答全篇的核心问题。warp 切换零成本,是因为状态不出寄存器堆;而两个 GPU 上下文(两个进程、或图形与计算)之间切换,恰恰要求把当前上下文占用的寄存器状态全部清空、换成另一个上下文的。算一笔账:一个 SM 支持 64 个 warp、每 warp 32 线程、每线程 256 个 32 位寄存器,单 SM 的寄存器状态就是 64×32×256×4 字节,约 2 MB;一块高端 GPU 有上百个 SM,整体寄存器状态超过 200 MB,此外还有共享内存、各类分页表与硬件上下文。这些数据要经 L2、HBM 转出再转回,毫秒级开销由此而来。
传统架构(Fermi 到早期 Kepler)更极端:计算上下文根本不支持中途抢占,驱动只能等当前 kernel 跑完才让出 GPU。如果一个 kernel 本身跑几百毫秒甚至数秒,期间整个 GPU——包括负责显示的图形流水线相关资源——都可能被占住。当年在 Windows 上启动一个 CUDA 程序后拖窗口明显卡顿、画面冻结,甚至触发驱动超时重置(TDR,Timeout Detection and Recovery,默认约 2 秒)杀掉 kernel,根源都在这里:没有抢占点,就没有调度自由。
这里还要区分两类被占用的资源。一类是执行与状态资源(SM、寄存器、共享内存),抢占时必须处理;另一类是系统资源(显存分配、上下文内存页表、显示控制器)。早期架构上,哪怕计算 kernel 用不到显示器,整个 GPU 的全局锁也会把图形提交一并堵住——计算与图形共享同一命令队列和地址空间,谁先提交谁独占。这也是为什么当时严肃的 CUDA 开发常建议另备一块卡做显示,计算卡专门跑任务:不是性能问题,是抢占点缺失下的无奈隔离。
解决思路是在「等 kernel 结束」和「保存全部寄存器」之间找更细的可抢占粒度,下一节展开。先用一张表把各层切换的量级摆清楚。表中数字是量级而非承诺:同一指令级抢占,空 kernel 与占满 255 个寄存器的 kernel 保存量可差近十倍,驱动版本、显存带宽、当时 L2 冷热都会改变实测值;工程上应以实测为准,表只用于建立「哪一层该是什么数量级」的直觉。
| 切换粒度 | 保存/恢复的状态 | 典型开销 | 说明 |
|---|---|---|---|
| warp 间切换(SM 内) | 无,寄存器常驻 | 约 0 周期 | 记分牌选择就绪 warp,非抢占 |
| block 换入换出 | 整个 block 的 warp 寄存器+共享内存 | 数十微秒级 | 硬件一般不做,block 不迁移 |
| 指令级抢占(计算) | SM 内全部 warp 的架构寄存器 | 约 0.1–2 ms(大 kernel 更高) | 抢占点灵活,随寄存器使用量上升 |
| 像素级抢占(图形) | 图形管线状态,粒度到像素/图元批 | 约数十–数百微秒 | 为保证显示刷新率而设计 |
| 传统无抢占 kernel 让出 | 无保存,但等待 kernel 结束 | kernel 剩余时长,可达数百 ms–秒级 | 长 kernel 卡死显示的根源 |
| MPS 时间片切换 | 共享同一 GPU 的进程上下文 | 毫秒级(含缓存扰动) | 时间片通常设为数十 ms 摊薄成本 |
| MIG 分区之间 | 无切换 | 约 0(硬件静态隔离) | 各分区独占 SM/L2/显存切片 |
四、抢占粒度的演进:像素级、指令级,以及分区即免切换
图形侧最先需要抢占。GPU 一边渲染桌面、一边跑后台任务,显示器却以固定节奏(60 Hz 下每 16.7 ms 一帧)要求扫描输出,渲染被长任务拖住就会丢帧、花屏。为此图形流水线设计了 pixel-level preemption(像素级抢占):抢占边界设在图元或像素批次之间,需要保存的图形管线状态比成百上千个 warp 的寄存器小得多,切换可控制在数百微秒内,足以保证显示线程按时拿到下一帧。
计算侧的指令级抢占来得更晚。NVIDIA 自 Maxwell 起改进、到 Pascal GP100 引入完整的 instruction-level preemption(指令级抢占):计算 kernel 可以在指令边界被打断,硬件把 SM 上所有 warp 的寄存器、程序计数器等架构状态压缩保存到显存,恢复时再读回。开销随当时驻留状态浮动——轻载 kernel 上是几十到几百微秒,占满寄存器的大 kernel 上可达一到数毫秒。注意这和 CPU 上微秒级、只存几十个寄存器的进程切换仍不是一个量级:GPU 一个上下文的「寄存器」是按 MB 计的。即便如此,它把「最多等一个 kernel」变成了「最多等一条指令边界」,TDR 误杀和画面冻结从此基本成为历史问题。
还有一个容易被忽略的间接成本:切换会冷掉 L1/L2。被切走的上下文刚把工作集焐热,切回来时缓存已被别人冲刷,恢复后的头几十微秒实际吞吐明显偏低。这也是为什么即使硬件支持抢占,调度器仍倾向于让一个上下文连续跑尽量久——每次抢占付的账不只有状态搬运,还有缓存再预热。实时图形管线之所以能承受频繁抢占,是因为它每帧本来就在重写数据、对缓存连续性不敏感;计算 kernel 则相反。
五、MPS 与 MIG:用时间片和空间分区替代频繁抢占
数据中心面对的是另一个问题:一块 GPU 被多个进程或多个租户共享,怎样让大家都能按时拿到服务,同时避开昂贵的上下文切换。
MPS(Multi-Process Service,多进程服务)走时间片路线。多个进程的 kernel 在 MPS 服务器编排下提交到同一 GPU,同一时刻可以并发执行以填满 SM,但在需要排队或租户配额受限时按时间片轮转。每次轮转仍是一次真正的上下文切换,毫秒级成本照付;工程上的做法是把时间片设得足够长(常见数十毫秒),让切换成本占比降到百分之一以下。MPS 提升利用率,但不提供隔离:一个进程的 kernel 出错或把显存写坏,可能波及同卡其他进程。
MIG(Multi-Instance GPU,多实例 GPU)走空间分区路线,思路彻底不同:它在硬件级把 SM、L2 切片、显存块、缓存甚至 NoC 入口静态切分成最多七个独立实例,每个实例对客户呈现为一块完整的小 GPU。实例之间不共享执行资源,也就根本不需要切换——分区的「调度开销」为零,因为各自的寄存器状态始终留在自己的 SM 上。一个实例跑死,不影响其他实例;故障隔离、性能可预测性都远好于时间片,代价是弹性差:分区在运行前静态配置,不能按 kernel 瞬时自由合并算力。一个粗略的对应关系是:MPS 像单机多任务操作系统,用时间片换利用率;MIG 像硬件虚拟机,用空间换隔离。理解了上下文切换贵在状态搬运,就能看出 MIG 的本质是让每个上下文独占存放状态的硬件,从而绕开搬运。
补充一个实践细节:MIG 的分区是有 profile 层级的,不同大小的切片按 SM 与显存配额划分(如一个全高实例约占整块卡七成资源,可继续切成两个半高实例),配置变更需要销毁重建实例,不能热调。因此 MIG 适合租户稳定、服务长期驻留的云场景,不适合一天到晚提交零散短任务的批处理队列——后者更适合 MPS 或整卡排队。另外要强调,指令级抢占解决的是「高优先级任务能否插进来」,MPS 解决的是「多进程如何填卡」,MIG 解决的是「故障与性能如何隔离」,三者并非互相替代:一台 MIG 实例内部仍可以叠加 MPS 让多个进程共享,也照样受抢占机制保护。
这一篇在知识体系里的位置
本篇是 GPU 架构深入:SIMT、warp 调度与内存层级 的调度续篇:前者回答「SM 内部为什么要有那么多 warp」,本篇回答「warp 之间、上下文之间两个层次的切换各要付什么代价」。分支发散如何改变 warp 就绪节奏,见 SIMT 与 warp 分支发散;从软件侧理解 occupancy 与寄存器压力怎样决定 SM 上能驻留多少 warp,见 occupancy 与寄存器压力,它是本篇第三节开销数字的另一面。更宏观的专用与通用路线对比,可回看 AI 加速器:脉动阵列与数据流——脉动阵列几乎不存在可抢占的线程上下文,这也是它能效高但灵活性低的原因之一。
总结
GPU 调度分两层:SM 内 warp 切换因寄存器状态常驻而零成本,GTO 加记分牌只解决「这拍发射谁」;而跨上下文抢占要把按 MB 计的全部 warp 状态搬去显存,传统架构只能等 kernel 结束、造成毫秒到秒级卡顿,指令级与像素级抢占把打断点做细,MPS 用长时间片摊薄切换成本,MIG 则干脆用硬件空间分区让状态无须搬运、切换开销归零。