第1章:GPU抢占机制概述
各位同学,今天我们来聊聊GPU抢占这个话题。说实话,我在做实时系统之前,一直觉得GPU就是个“发出去就不管”的加速器。直到有一次,一个自动驾驶的项目让我彻底改变了这个看法——嗯,那台车差点撞上护栏。
GPU抢占,说白了就是让正在执行的任务停下来,把计算资源让给更紧急的任务。你想想看,在实时系统里,如果一辆车的刹车指令被一个渲染任务堵住了,后果不堪设想。
1.1 GPU上下文切换
先说说上下文切换。CPU的上下文切换大家都很熟悉——保存寄存器、恢复寄存器,几微秒的事。但GPU呢?我个人的经验是,GPU的上下文切换比CPU重得多。
为什么?因为GPU要保存的东西太多了:
- 显存状态:当前任务占用的显存数据,包括纹理、缓冲区、着色器代码
- 计算管线状态:所有SM(流式多处理器)上的寄存器文件、共享内存
- 调度器状态:正在等待的线程块、已完成的任务队列
我在项目中遇到过这样一个场景:一个深度学习推理任务正在跑,突然来了一个高优先级的控制指令。结果呢?光保存显存数据就花了将近2毫秒。对于实时系统来说,这简直是天文数字。
关键点:GPU上下文切换的开销主要来自显存数据的搬移。CPU切换只需要保存几十个寄存器,GPU可能要保存几百兆甚至上GB的显存数据。
1.2 抢占粒度
抢占粒度,就是你能让任务停在哪一步。我习惯把抢占粒度分成三个层次,每个层次都有它的适用场景。
指令级抢占
这是最细的粒度。GPU可以在执行完当前指令后立即暂停。听起来很完美对吧?但现实很骨感。
我曾经试过在NVIDIA的GPU上实现指令级抢占,结果发现:
- 每条指令的上下文都很小,切换快
- 但GPU的指令流水线很深,要等所有正在执行的指令都完成
- 实际上,指令级抢占的延迟并不比线程块级低多少
我的建议:除非你的实时性要求极其苛刻(微秒级),否则别碰指令级抢占。实现复杂,收益有限。
线程块级抢占
这是目前最实用的方案。GPU以线程块(Thread Block)为单位进行抢占。当一个线程块执行完毕后,GPU可以决定是否启动下一个线程块。
我记得有一次做工业控制的项目,用的就是线程块级抢占。效果还不错:
- 抢占延迟:几十微秒到几百微秒
- 实现难度:中等
- 资源开销:需要额外的调度逻辑
说白了,线程块级抢占就是在“细粒度”和“实现复杂度”之间找了个平衡点。
应用级抢占
这是最粗的粒度。整个应用(进程)被暂停,等更高优先级的任务完成后,再恢复执行。
应用级抢占的优点是实现简单——操作系统就能搞定。但缺点也很明显:
- 抢占延迟可能达到毫秒级甚至更高
- 需要保存和恢复整个应用的显存状态
- 不适合对实时性要求高的场景
注意:应用级抢占在桌面GPU上很常见,但在实时系统中,我建议你尽量避免。我曾经在一个项目中用了应用级抢占,结果高优先级任务等了几毫秒才拿到GPU——这在实时系统里是不可接受的。
1.3 不同厂商的抢占方案
不同GPU厂商的抢占方案差异很大。我主要用过NVIDIA和AMD的GPU,说说我的感受。
NVIDIA的抢占方案
NVIDIA在Pascal架构之后引入了比较完善的抢占机制。我个人觉得,NVIDIA的方案有几个特点:
| 特性 | 说明 |
|---|---|
| 抢占粒度 | 支持线程块级和指令级抢占 |
| 上下文切换 | 硬件支持快速上下文切换,但显存数据需要软件管理 |
| 优先级 | 支持32个优先级级别 |
| 抢占延迟 | 线程块级:10-100微秒;指令级:1-10微秒 |
NVIDIA的CUDA提供了cudaSetPriority函数,可以设置任务的优先级。但要注意,这个优先级只是建议性的,GPU调度器不一定会严格遵守。
// 设置CUDA任务的优先级
cudaStream_t stream;
cudaStreamCreateWithPriority(&stream, cudaStreamNonBlocking, 10);
嗯,这里要注意:优先级值越大,优先级越高。我刚开始用的时候搞反了,结果高优先级任务反而被低优先级任务堵住了。
AMD的抢占方案
AMD的GPU在抢占方面和NVIDIA有些不同。AMD的ROCm平台提供了类似的抢占机制,但实现方式不太一样。
| 特性 | 说明 |
|---|---|
| 抢占粒度 | 主要支持应用级和线程块级 |
| 上下文切换 | 硬件上下文切换开销较大,建议尽量减少切换次数 |
| 优先级 | 支持8个优先级级别 |
| 抢占延迟 | 应用级:1-10毫秒;线程块级:50-500微秒 |
AMD的抢占方案有个特点:它更依赖软件层面的调度。我曾经在AMD的GPU上做过测试,发现线程块级抢占的延迟波动比较大。有时候50微秒就能完成,有时候要500微秒。
避坑指南:如果你要用AMD的GPU做实时系统,我建议你多做压力测试。我曾经在一个项目中,AMD GPU的抢占延迟在特定负载下会突然飙升到毫秒级——差点把项目搞黄了。
小结
好了,这一章我们聊了GPU抢占的基本概念。总结一下:
- GPU上下文切换比CPU重得多,主要开销在显存数据搬移
- 抢占粒度从细到粗:指令级、线程块级、应用级
- NVIDIA和AMD的抢占方案各有优劣,选择时要看具体需求
下一章,我们会深入讨论抢占延迟的测量方法。说实话,测量抢占延迟比我想象的要难得多——我当年第一次测的时候,数据完全不对,后来才发现是测量工具本身引入了延迟。嗯,这些坑我们下章再聊。