1. GPU调度全景图
1.1 先聊聊“调度”这件事
调度,说白了就是“谁先干、谁后干、干多久”。
你想想看,一个系统里资源就那么多——计算单元、内存带宽、寄存器文件。但任务可能成百上千。谁来决定它们怎么排队?谁来决定哪个任务先拿到计算资源?这就是调度干的事。
我个人习惯把调度比作“交通指挥”。CPU调度像是城市里的红绿灯,车不多,规则简单。GPU调度呢?更像是北京西二旗地铁站早高峰——人山人海,但还得保证每个人都能挤上车,还不能踩踏。
嗯,这个比喻虽然糙了点,但道理是通的。
1.2 为什么需要调度?
你可能觉得:任务来了就执行呗,有什么好调的?
我在项目中遇到过这样的场景:一个深度学习训练任务,把GPU跑满了,但显存利用率只有30%。为什么?因为没有调度好——计算单元在等数据,数据在等搬运,搬运在等总线。说白了,资源在“空转”。
调度要解决的核心问题有三个:
- 资源利用率:让计算单元、内存、总线别闲着
- 公平性:多个任务别让一个饿死
- 延迟控制:实时任务不能等太久
我曾经见过一个团队,写了个CUDA程序,性能死活上不去。后来发现是线程块调度没做好,SM(流多处理器)之间负载严重不均。有的SM忙死,有的SM闲死。这就是调度没搞好的典型后果。
核心观点:没有调度,GPU就是一堆高性能的“废铁”。调度决定了这些算力能不能真正变成性能。
1.3 GPU与CPU在调度上的核心差异
这个问题,我面试过很多人,十个有八个答不全。咱们来拆开看。
1.3.1 调度粒度不同
| 维度 | CPU | GPU |
|---|---|---|
| 调度单位 | 线程(Thread) | 线程束(Warp)/ 线程块(Thread Block) |
| 典型数量 | 几十个线程 | 数万个线程 |
| 切换开销 | 高(上下文切换) | 极低(硬件零开销切换) |
CPU调度一个线程,要保存寄存器、程序计数器、栈指针……一套下来几百个周期。GPU呢?硬件天然支持多线程上下文,切换就是换一组寄存器指针的事。说白了,GPU的线程切换几乎是“免费的”。
1.3.2 调度策略不同
CPU用的是“抢占式调度”。操作系统说了算,时间片到了就给你切走。你正在算一个东西,系统说停,你就得停。这叫“被动让出”。
GPU用的是“协作式调度”。线程块自己决定什么时候让出SM。你算完了,或者卡在访存了,硬件自动切到下一个就绪的线程束。这叫“主动让出”。
为什么会这样?因为GPU的线程太多了。几万个线程,操作系统根本管不过来。只能靠硬件自己搞定。
小技巧:写GPU程序时,尽量让每个线程的工作量差不多。否则会出现“木桶效应”——最慢的那个线程决定了整个线程块的完成时间。我踩过这个坑,后来学乖了。
1.3.3 调度目标不同
CPU调度追求的是“低延迟”。你点一下鼠标,系统得赶紧响应。所以CPU调度器会尽量让交互线程优先执行。
GPU调度追求的是“高吞吐”。你跑一个矩阵乘法,不在乎单个元素算得快不快,在乎的是整个矩阵算完要多快。所以GPU调度器会尽量让所有SM都满载,哪怕单个线程慢一点也没关系。
说白了,CPU是“单兵作战”,GPU是“人海战术”。
1.4 GPU调度全景图
下面这张图,是我自己画的一个GPU调度全景图。你看完应该能对整体有个感觉。
这张图里,我刻意把调度分成了三个层次:
- 主机端调度:CPU把任务打包成命令,扔给GPU
- 全局调度:GigaThread Engine把线程块分配到各个SM
- SM内部调度:Warp调度器在每个时钟周期选一个就绪的Warp执行
这三个层次,每一层都有自己的调度策略。你写程序时,其实就是在跟这三层调度打交道。
注意:很多初学者只关注第三层(Warp调度),忽略了前两层。我曾经犯过这个错——在CPU端频繁提交小kernel,结果调度开销占了总时间的一半。后来改成合并提交,性能直接翻倍。
1.5 小结
GPU调度,说白了就是“让几万个线程高效地共享有限的计算资源”。它跟CPU调度最大的区别在于:
- CPU调度线程,GPU调度Warp
- CPU是抢占式,GPU是协作式
- CPU追求低延迟,GPU追求高吞吐
嗯,这些概念先记着。后面我们会一步步深入,把每一层调度都拆开来看。
一句话记住:GPU调度不是“谁先来谁先上”,而是“谁准备好了谁上”。这个思维转变,是理解GPU编程的第一步。