搞AI推理的人,多少都经历过这种场景:模型在单卡上跑得挺欢,一上生产就卡成幻灯片;显存看着够用,并发一上来直接OOM;四张卡摆在那儿,一查利用率不到30%。我花了不少时间专门折腾推理侧的GPU调度,从底层CUDA特性到上层服务框架都摸过一遍,这篇文章把核心思路和能直接落地的方案整理一下,给正在做推理服务优化、模型部署的同行做个参考。
先说结论:推理场景下的GPU调度优化,和训练完全是两码事。训练追求的是把GPU算力吃满,吞吐越大越好;推理拼的是延迟和成本的平衡,你既要让单次请求足够快,又要在有限的显存里塞下尽可能多的并发,还得处理不同模型、不同大小请求带来的碎片化问题。这里面的核心矛盾,在于GPU资源怎么分配、怎么复用、怎么在请求级别做切分。
1. 推理场景下GPU到底在忙什么:先搞清瓶颈在哪
很多人一上来就调框架参数,结果改了半天没效果,原因在于根本没看清推理请求在GPU上的真实执行路径。推理和训练不一样,训练是成批的数据灌进去,权重更新是固定的迭代模式;推理则是单个或多个请求进来,走一遍前向传播,输出结果返回。这导致GPU在推理时的利用率天然就上不去,尤其是小batch请求,kernel启动开销、显存带宽、算子调度延迟占比非常高。
1.1 为什么推理的GPU利用率普遍偏低
打个不严谨的比方:训练像是装满货物的重卡在高速上跑,载荷大、路线固定、引擎效率高;推理更像是出租车跑市区,走走停停,每单就拉一两个人,时间都花在等红灯和起步上了。
放到GPU上具体拆解,主要有三个原因。第一个是kernel启动开销,PyTorch默认的CPU侧调度会让每个GPU kernel都有微秒级的启动延迟,请求量小的时候,这部分延迟能占到端到端的30%以上。第二个是显存带宽的瓶颈,推理主要是访存密集型操作,尤其是Decoder阶段,每次只生成长度有限的token,但模型权重和KVCache都得从显存里过一遍,带宽不够,算力再多也是浪费。第三个是请求间的资源争抢,多个并发请求如果没有统一调度,很容易互相挤占显存带宽和SM资源,谁都快不了。
我早期优化的一个项目,刚开始看nvidia-smi,GPU利用率有80%多,以为优化空间不大了,后来用NVIDIA Nsight一测才发现,SM的活跃计算占比其实不到40%,大量时间花在数据搬运和kernel等待上。优化显存访问路径之后,单卡吞吐直接翻了一倍还多。
1.2 推理优化的核心指标:吞吐、延迟、显存的三方博弈
推理GPU调度优化,始终是在三件事之间找平衡:吞吐量(Throughput)、延迟(Latency)和显存占用(Memory Footprint)。这三者通常是互斥的,你不可能同时做到极致。
| 指标 | 追求方向 | 典型代价 |
|---|---|---|
| 吞吐量 | 每秒处理的请求数最大化 | 单请求延迟上升,显存占用增加 |
| 延迟 | 单请求响应时间最小化 | batch size受限,吞吐下降 |
| 显存占用 | 单卡承载更多模型和并发 | 需要量化/切分,精度和速度受影响 |
实际业务里,这三者的优先级由场景决定。像实时对话助手,延迟是生命线,你不可能因为要提吞吐就把batch塞到128,用户等不了;像离线批量分析、内容审核这类,延迟要求低,可以放心开大batch冲吞吐。
所以做GPU调度优化,第一步不是调参,是明确业务指标:P99延迟红线是多少?单卡QPS需要到多少?模型数量有多少、模型切换频繁吗?这些答案决定了后续所有的技术选型。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. GPU环境搭建与验证:地基打不稳,后面全白搭
调度优化跑在GPU环境之上,但环境本身就有很多坑。我在多台服务器上配过CUDA、PyTorch、容器环境,踩过的坑能列一长串,这里说几个和调度优化直接相关的。
2.1 CUDA、驱动、PyTorch的版本匹配陷阱
GPU环境最让人头疼的就是版本匹配问题。很多人装完PyTorch发现跑不了,第一反应是代码写错了,其实八成是CUDA版本和显卡驱动版本对不上。
先记住一个结论:显卡驱动决定CUDA运行时的上限,PyTorch内置的CUDA runtime只要不高于驱动的最大支持版本就行。也就是说,更新驱动能向下兼容旧版CUDA,但反过来不行。
具体到安装流程,我的建议是这样的:
- 先确认显卡型号和驱动版本,用
nvidia-smi查看右上角的CUDA Version,注意这里显示的是驱动支持的最高版本,不代表你环境里实际用的版本。 - PyTorch的安装。到PyTorch官网选对应CUDA版本,比如当前常用的是cu118、cu121、cu124。选择标准是:你的驱动版本必须大于等于这个CUDA版本号。
- 装完验证。这时不要急着跑模型,先跑个小例子确认GPU真的能用:
python复制import torch
print(torch.__version__)
print(torch.cuda.is_available())
print(torch.cuda.device_count())
print(torch.cuda.get_device_name(0))
如果cuda.is_available()返回False,最常见的原因是驱动版本太老,或者PyTorch的CUDA版本和驱动不兼容,而不是显卡坏了。
有个很容易踩的坑:torch.cuda的显存分配用的是caching allocator,也就是PyTorch默认会缓存显存块,不立刻释放给其他进程。当你用多进程跑推理时,明明某个进程任务结束了,nvidia-smi里显存还是被占着,这不是泄漏,而是PyTorch故意为之,为了下次分配更快。处理方式是在进程退出前调torch.cuda.empty_cache(),或者设置环境变量PYTORCH_CUDA_ALLOC_CONF=backend:cudaMallocAsync换用异步分配策略。
2.2 多GPU环境下的显存可见性与调度初始验证
服务器上插了多张卡,第一件事不是直接跑模型,而是搞清楚系统能看到几张卡、每张卡的状态如何。推荐几个排查命令:
bash复制# 查看GPU整体状态,包括利用率、显存、温度、功耗
nvidia-smi
# 实时刷新查看
watch -n 1 nvidia-smi
# 查看GPU拓扑,确认NVLink/NVSwitch连接方式
nvidia-smi topo -m
nvidia-smi topo -m这个命令容易被忽略,但对多卡调度特别重要。它会显示GPU之间的连接方式,是NVLink直连、PCIe交换机还是走CPU。多卡做张量并行(Tensor Parallel)时,卡和卡之间的通信带宽直接影响推理性能,如果两张卡之间走的是PCIe而非NVLink,那么TP的通信开销可能大到你怀疑人生。
验证完多卡可见性,接着做一个小测试:分别在不同GPU上跑同一个模型,对比延迟和吞吐差异。实操时我用过一段简单的脚本,循环指定cuda:0、cuda:1、cuda:2,观察各自的执行时间。有时候同一批卡,因为散热位置、供电设计不一样,性能会有几个百分点的差异,调度策略如果能感知到这种差异,可以把高负载请求优先调度到性能更好的卡上。
另外,如果机器上装了容器环境,比如Docker或者Kubernetes,GPU的透传方式也影响调度。默认的--gpus all是直接把整卡透传给容器,简单粗暴,但一个
