模型部署上去之后,GPU 利用率一直在 20%~30% 徘徊,响应却慢得让人怀疑是不是用 CPU 在跑。换一台机器想多塞几个并发请求,结果 OOM 直接崩掉。这不是单个模型的问题,而是整个推理链路上 GPU 资源调度没有理顺。
“AI 模型推理 GPU 资源调度”听起来像运维同学的活,但实际干过的人都知道,做算法、做应用、做后端的人每天都会被它绊一跤:显存怎么分、并发开多大、为什么多卡利用率上不去、为什么换了 GPU 反而更慢。这篇文章把我这几年在推理部署、模型微调、多用户共享 GPU 这些场景里攒下的调度经验和踩坑记录完整写出来,从单块显卡到集群调度、从 Ollama 本地推理到 PyTorch 微调,每个环节都会说清楚“为什么这么做”,也会给出可以直接照着操作的验证方法。
1. GPU 推理资源调度:先把问题定义清楚
提到 GPU 资源调度,很多人第一反应是“排队跑任务”,其实推理场景和训练场景完全是两码事。训练任务可以等,推理任务等不起,这就决定了调度策略的根本差异。
1.1 一句话说清“调度”到底在调度什么
很多人以为 GPU 调度只跟显存有关,这是最常见也最危险的误解。GPU 上能被调度的资源至少分四类:显存容量、计算单元(SM)、显存带宽、以及并发执行上下文。
显存决定能同时装下多少个模型副本和多少条并发请求,计算单元决定算力峰值能跑到多高,显存带宽决定数据搬运速度,而并发上下文(比如 CUDA stream、MPS 的并发通道)决定多个任务能不能真正并行而不是互相阻塞。
我见过一个生产事故:某服务部署了 4 个模型副本,每份显存占用 4GB,一块 16GB 的卡刚好塞下。表面上显存利用率接近 100%,但四个副本在同一个 GPU 上争抢 SM,实际吞吐比单副本只高了 40%,延迟反而涨了 3 倍。原因就是只算了显存够不够,没算 SM 份额够不够。调度的时候,这四个维度必须一起算,否则就会得到“看起来满载,实际很慢”的诡异结果。
1.2 推理调度的核心约束:延迟敏感
训练调度的目标函数是“吞吐最大化”,任务排队多久无所谓,GPU 利用率越高越好。推理的约束完全不同,它的目标函数是P99 延迟达标的前提下,尽量提升吞吐。
打个比方:训练就像货运火车,装得越多越好,晚点一两小时可以接受;推理就像外卖骑手,每一单都有时限 30 分钟,哪怕只有一单超时,用户就能感知到。
这个差异会直接反应到调度策略上。训练时可以大胆把 batch size 拉到 64、128,让 GPU 吃得越满越好;推理时如果 batch size 拉得太大,单个请求处理时间暴涨,P99 延迟就守不住。所以推理场景通常会在服务端做动态 batching——把一小段时间窗口内的请求攒起来凑成一个 batch,既保证延迟不超标,又尽量抬高 GPU 利用率。这个窗口设多长、batch 最多攒到多大,就是推理调度里最常见的调优项。
2. 单机推理:显存分配、CUDA Context 与并发上限
单机推理是一切调度的基础。先把一块卡上的资源算明白,再谈多卡和集群。
2.1 模型推理的显存占用到底怎么估算
很多人用“模型参数量 × 精度字节数”来估显存,这个算法只对了一半。比如 7B 模型半精度是 7×10^9 × 2 字节 ≈ 14GB,确实是对的,但这只是权重占用的显存。实际部署时还要算上三笔额外开销。
第一笔是CUDA Context,每个进程初始化 CUDA 环境时都会固定占掉约 300MB~500MB 显存,多进程部署多个模型时这笔开销会被放大。第二笔是KV Cache,它随并发请求数和序列长度线性增长。以 7B 模型为例,每个并发请求的 KV Cache 大约是几百 MB 到 1GB 不等,取决于上下文长度。第三笔是临时激活值,推理时前向计算产生的中间张量,虽然比训练时小得多,但 batch 变大时也不能忽略。
我常用的快速估算公式是:
显存需求 = 模型权重 × 精度系数 + 并发数 × 单请求 KV Cache 大小 + 常量开销(驱动 + CUDA Context + 框架缓存)
假设 7B FP16 模型,并发 32,单请求 KV Cache 0.4GB,那至少需要 14 + 12.8 + 1 = 27.8GB 显存。一块 24GB 的 4090 就不够,需要 48GB 的 A6000 或者 L40S。很多人在 24GB 卡上跑 7B,一压并发就 OOM,就是这个账没算明白。
2.2 为什么“GPU 利用率低”不一定是坏事
接着开头那个 30% 利用率的问题说。我用nvidia-smi看到的利用率,指的不是显存占用率,而是 SM 上有活跃 warp 的时间占比。推理服务如果延迟敏感,通常不会把 batch 拉到很大,SM 一直喂不满,利用率低其实是正常的。
判断推理型服务的 GPU 是否“够用”,不能只看利用率。更靠谱的指标链是:P99 延迟是否达标 → 吞吐是否满足业务峰值 → 显存是否有余量。链路全绿,哪怕利用率只有 20% 也没问题;链路中有任何一项超标,才需要去排查是不是 GPU 资源被卡住了。
真想分析 GPU 到底在忙什么,单靠nvidia-smi不够,我一般会用nsys抓一下时间线,看 kernel 之间有没有大量空隙。如果 kernel 之间空隙很大,说明数据搬运或者 CPU 侧预处理成了瓶颈,这时就算把 GPU 换成顶配也白搭。
2.3 PCIe 与显存带宽:容易被忽略的隐形瓶颈
热词里反复出现“PCIe”“GPU 对称内存”这类词,说明很多人已经遇到带宽瓶颈了。推理时权重要从显存搬进 SM 里的计算单元,每算一次都要搬一遍,所以显存带宽对推理延迟的影响经常超过算力。6550 这类数据中心卡显存带宽高达 3.35TB/s,而普通消费级卡往往只有几百 GB/s,差距非常可观。
另一个被忽略的是PCIe 带宽。如果数据要在 CPU 和 GPU 之间来回搬运,PCIe 4.0 x16 的理论带宽约 32GB/s,实际有效带宽只有一半左右。比如视觉模型要做 CPU 侧预处理再拷到 GPU,图像分辨率一大,这份拷贝时间就能吃掉整个推理延迟的一半。我的经验是,能用 GPU 算子完成的预处理就尽量在 GPU 上做,减少跨 PCIe 的搬运次数。
3. Ollama 本地推理:最常见的 GPU 调度实战场景
Ollama 是本地跑大模型时使用率最高的工具之一,但它对 GPU 的调度逻辑藏在环境变量后面,很多人装完发现模型在跑,却没意识到它根本没用上 GPU。
3.1 如何确认 Ollama 到底用没用到 GPU
判断方法很简单,跑一个模型后执行ollama ps,输出结果里能看到进程列表。关键是看PROCESSOR列,显示 100% GPU 说明权重全在显卡上,显示 GPU/CPU 混合说明有一部分层被卸载到了内存,显示 100% CPU 说明压根没启用 GPU。
我自己在第一次装 Ollama 时就踩过坑:ollama run llama3之后响应速度尚可,但ollama ps显示 100% CPU,原来是因为 CUDA 驱动没配对,Ollama 静默回退到了 CPU 推理。它不会报错,只会慢,如果不主动查进程列表根本发现不了。
3.2 显存不足时的调度策略:层卸载与并发控制
当模型权重大于显存容量时,Ollama 会把一部分 transformer 层放在 CPU 上,GPU 只跑剩下的层,这就是默认的“层卸载”策略。OLLAMA_NUM_GPU环境变量可以控制放到 GPU 上的层数。比如 32 层的模型,默认全放 GPU;如果显存不够,就设OLLAMA_NUM_GPU=20,只放 20 层到 GPU,剩下 12 层在 CPU 上算。
但这里有个容易忽略的点:推理是串行的,每一层都要按顺序计算。只要有一层在 CPU 上,整条链路的速度就被这一层拖住。所以层卸载只是“能跑”的方案,不是“快”的方案。如果模型是 14B、32B 级别,显存不够,我的建议是优先考虑量化版本(比如 Q4_K_M),把权重压到 5GB 以内,整模放 GPU,远好过硬塞 FP16 然后卸载一半层到 CPU。
并发层面的调度,Ollama 默认单请求独占,多请求排队。OLLAMA_NUM_PARALLEL环境变量可以开并行,比如设成 4 就能同时处理 4 个请求。但注意,并行数开太高,每个请求分到的显存和 SM 份额都会下降,OOM 和延迟飙升都可能在开大的瞬间出现。我实测 7B Q4 模型在 24GB 卡上,并行 4 是甜点,并行 8 延迟翻倍且收益很小。
3.3 非 NVIDIA GPU 的处理差异:Intel、AMD 与国产加速卡
Ollama 不只有 NVIDIA 生态可以用。Intel 平台走 SYCL 后端,运行前设置OLLAMA_INTEL_GPU=1并安装 Intel 官方 GPU 驱动;AMD 显卡走 ROCm 后端,需要装 ROCm 运行时,并且 Ollama 对 ROCm 版本有比较严格的配对要求。国产加速卡(比如昇腾 CANN 生态)也在陆续适配 Ollama,但需要把后端编译成 CANN 版本,不能直接拿官方包跑。
这些非 NVIDIA 生态最麻烦的地方在于驱动、运行时、框架三者的版本匹配。NVIDIA 的 CUDA 已经是事实标准,装错了大不了重装;ROCm 和 SYCL 的文档相对零散,版本兼容矩阵要自己试。实在要踩,我的建议是先看官方 Docker 镜像里锁定的版本,照着镜像里的依赖版本在宿主机上复现,能省一半时间。
4. WSL 环境 GPU 被系统阻止:完整排查链路
近几年在 Windows 上做 AI 开发的人越来越多,WSL 成了标配,但“failed to initialize nvml: gpu access blocked by the operating system”这个报错频繁出现在各种论坛里。这类问题本质上不是 Ollama 或 PyTorch 的问题,而是 Windows 侧 GPU 虚拟化通道没打通。
4.1 这个报错的根因是什么
WSL 2 里访问 GPU,依赖 Windows 宿主机的 GPU 驱动通过 dxgkrnl 虚拟化协议把 GPU 能力暴露给 Linux 内核。所以链路是:
Windows GPU 驱动 → WSL 内核的 GPU 虚拟化模块 → Linux 用户态的 CUDA 库 → 应用程序
这段链路里任何一环断了,或者版本不匹配,就会出现“gpu access blocked”或者“NVML 初始化失败”。最常见的是三种根因:Windows 侧驱动太老;WSL 内核没有更新;用户态 CUDA 库和驱动版本不匹配。
4.2 从现象到定位:三步排查法
第一步,在Windows 侧打开终端执行nvidia-smi,确认驱动能识别 GPU、显示的是正常状态而不是“不可用”。如果 Windows 侧都识别不了,优先更新 GE Force Experience 或企业版驱动。
第二步,进入WSL 终端执行nvidia-smi。注意看输出的驱动版本和 CUDA 版本,正常情况下应该和 Windows 侧一致。如果 WSL 里提示找不到命令,说明没装 CUDA 工具链,sudo apt install nvidia-cuda-toolkit可以解决,但我不推荐直接装系统包管理器里的版本,版本太老。
第三步,检查 WSL 内核是否支持 GPU 直通。在 WSL 里执行uname -r,如果内核版本太老,跑wsl --update把 WSL 本体更新到最新版。这一步是很多人忽略的:WSL 内核和 Windows 系统是分开更新的,光更新驱动不更新 WSL 内核,GPU 虚拟化模块就不会加载。
4.3 为什么“更新驱动”不一定能解决问题
我踩过一次很深的坑:驱动装的是最新版,WSL 也更新了,但报错依旧。最后发现问题是WSL 里装的 PyTorch 是 CPU 版,它内部调用 CUDA 的路径根本没有,所以去初始化 NVML 的库和驱动不是同一个 CUDA 版本。这种报错不是“被系统阻止”,而是用户态的 CUDA 库和 Windows 驱动里的 CUDA 运行时版本错配。
解决方法是统一基线:Windows 驱动是 572.61,对应的驱动内 CUDA 版本是 13.0,WSL 里的 CUDA 工具链也要用 13.x 或向下兼容的 12.x,不能在 WSL 里单独装一个 CUDA 11 的 PyTorch 包。建议直接用 PyTorch 官方命令装对应 CUDA 版本,比如pip install torch --index-url https://download.pytorch.org/whl/cu121,装完用torch.cuda.is_available()验证。
还有一个容易中招的场景是VMware Workstation Pro里跑 GPU 推理。虚拟化软件的 GPU 直通方案(vGPU 或 PCIe Passthrough)对虚拟硬件版本有要求,如果虚拟机是默认的旧硬件版本,GPU 直通功能根本不会生效。解决办法是把虚拟机硬件版本升级到支持 GPU 直通的版本,并且在虚拟机设置里显式指定 GPU 资源。
5. 多用户与集群场景:GPU Operator、资源池化与租用
单机搞定之后,面对多卡、多用户、甚至多机集群时,调度复杂度会上一个数量级。这里最核心的问题是:怎么把几块 GPU 按需切给不同的人、不同的任务,互不干扰。
5.1 从裸机到资源池化:容器化是基础
裸机上的多用户 GPU 调度非常痛苦,A 用户的任务显存泄漏,能把整块卡打挂,B 用户毫无办法。容器化是最基础的隔离手段。在 Kubernetes 里跑 GPU 任务,如果只是resources: nvidia.com/gpu: 1这么简单写,你会发现调度器根本不知道该去哪个节点找 GPU——这就是 GPU Operator 存在的理由。
GPU Operator 做的事情可以理解为一个“插线板管理工具”:它在 Kubernetes 集群里装上 NVIDIA 驱动、CUDA 运行时、容器运行时插件和监控组件,让 Kubernetes 能够感知每台节点上的 GPU 资源,并且把 GPU 以标准资源的方式暴露给调度器。部署它之前,节点上不能预先装 NVIDIA 驱动,否则会有冲突。
5.2 GPU Operator 的调度链路与验证
GPU Operator 装上后,调度的链路是:
用户提交 Pod(声明 nvidia.com/gpu: 1)→ Kubernetes 调度器发现节点有余量 → 节点上的 Device Plugin 把 GPU 设备挂载进容器 → 容器里 nvidia-smi 正常识别
验证调度是否生效,最快的方法就是部署一个临时 Pod 跑nvidia-smi,确认容器里看到的显卡编号和宿主机对得上。我遇到过的常见坑是 Pod 启动报Failed to initialize NVML,多数是 Device Plugin 和驱动版本不匹配,升级 GPU Operator 版本即可解决。
5.3 共享 GPU 的调度策略:时间片、MIG 与显存隔离
很多场景下,一人独占一张卡太浪费,但几组人同时用,又要防止互相干扰。这就得靠共享 GPU 调度。目前主流方案有三类:
| 方案 | 隔离粒度 | 优点 | 缺点 |
|---|---|---|---|
| 时间片(Time Slicing) | 按时间轮转 | 配置简单,兼容所有卡 | SM 争抢严重,延迟不稳定 |
| MIG(Multi-Instance GPU) | 硬件级切分显存和SM | 隔离最彻底,延迟稳定 | 仅 A100/H100 等新卡支持 |
| 显存+SM 限制(算子级) | 软件限制 | 灵活,支持消费级卡 | 实现复杂,需配合专用调度器 |
中小团队最常用的是时间片方案,配置很简单,但要注意它对延迟的破坏。我在一个语音识别服务上试过时间片切分,两个任务的 GPU 利用率都拉满时,P99 延迟从 80ms 涨到 450ms,完全不可用。后来改用 MIG 把一张 A100 切成 3 个实例,每个实例独享 SM 和显存分片,延迟彻底稳定下来。如果你的 GPU 支持 MIG,优先用 MIG 而不是时间片。
5.4 云上 GPU 租用的调度经验
“GPU 租用”本身就是一种资源调度:把算力需求放到云服务商的 GPU 实例上跑。这里最容易踩的坑是实例规格选择和弹性策略。推理服务最好选按量付费实例搭配弹性伸缩,不要选包年包月,因为推理负载的波峰波谷非常明显。
另一个经验是不同云厂商的 GPU 实例,CPU 和内存配比差异很大。有的 24GB 显存卡只配 8 核 CPU,跑大模型的 tokenizer 和预处理时 CPU 会成瓶颈。选型时一定看 CPU 核数和内存大小,而不仅仅看 GPU 型号。
6. 微调场景的显存调度与优化:从 PyTorch 到生产
标题虽然重点在“推理”,但“GPU 微调大模型”在热词里反复出现,而且微调的显存调度和推理是强关联的——很多推理坑(KV Cache、序列长度的影响)在微调阶段就已经埋下了。微调时显存不足,本质上也是调度问题:怎么把几十 GB 的计算量,塞进一块 24GB 的卡里。
6.1 PyTorch GPU 环境:先通过版本匹配这道关
PyTorch 安装教程类的问题常年居高不下,核心原因是版本匹配。pip install torch默认装的是 CPU 版,跑torch.cuda.is_available()永远返回 False。正确做法是到 PyTorch 官网看 CUDA 版本矩阵,然后装对应索引源的包。
我目前用的稳定组合是:驱动 572.xx(CUDA 13.0)+ PyTorch cu121 编译版,向后兼容没问题。选 cuda 版本的时候,不用盯着最新,装得能跑比装得最新重要得多。驱动版本向下兼容 CUDA 运行时,但反过来不行——老驱动撑不住新版 CUDA 编译出来的 PyTorch。
6.2 微调显存峰值估算:为什么一张 24GB 卡经常爆
微调阶段显存占用比推理多得多。除了权重本身,还要存梯度、优化器状态(Adam 要存一阶动量和二阶动量,FP16 下是权重的 4 倍)、以及前向传播留下的激活值。以 7B 模型为例:
| 项目 | 显存开销 |
|---|---|
| 权重(FP16) | 14GB |
| 梯度(FP16) | 14GB |
| Adam 优化器状态(FP32) | 42GB |
| 激活值(随 batch 和序列长度变化) | 数 GB 到数十 GB |
所以 7B 全参数微调在 24GB 卡上根本跑不动。全参微调建议 80GB 以上显存。24GB 卡要跑 7B 微调,必须做显存调度优化。
6.3 显存调度优化三板斧:混合精度、梯度检查点、CPU offload
第一板斧是混合精度(AMP):权重和激活用 FP16/BF16,优化器状态保持 FP32。这一步就能把优化器状态的 42GB 砍到约 20GB 左右。
第二板斧是梯度检查点(gradient checkpointing):不保存所有前向激活值,而是在反向传播时重算。激活占用的显存可以从几十 GB 降到几 GB,代价是训练速度慢约 30%。对 24GB 卡的微调用户来说,这个代价值得付。
第三板斧是 CPU offload:把优化器状态和梯度卸载到内存,GPU 只保留权重和激活。这是 ZeRO-Offload 的核心思路。24GB 卡 + 64GB 内存的组合,用 DeepSpeed ZeRO-Offload 能跑 7B 的 LoRA 微调,甚至有人拿它跑全参微调,只是慢得感人。
这三板斧的合理组合,我实测过:7B 模型在 4090(24GB)上做 LoRA 微调,开混合精度 + 梯度检查点 + batch size 1,显存稳定在 18GB 左右,训练能跑通。再多开一个 CPU offload,显存能压到 12GB,但每步训练时间涨了接近 50%,所以能不开就不开,优先想办法减小 batch 和序列长度。
6.4 微调完转推理时,显存调度要重算一遍
微调结束后的模型做推理部署,显存估算要重新按推理逻辑算,不能用训练时的数据。我遇到过同事把微调时的显存峰值当成推理占用,结果部署时预留了 3 倍冗余,白白浪费了一张卡的成本。推理部署参照第 2.1 节的公式重新算就行,这里要提的重点是:微调时用的 LoRA 权重,推理时可以先合并进主模型,省掉运行时加载 LoRA 适配器的开销,能省约 0.5~1GB 显存,也能降低加载耗时。
7. 从一块卡到一片卡:调度意识比工具更重要
最后分享一个我个人的实操体会:GPU 资源调度这件事,工具和参数固然重要,但真正拉开差距的是“先算账,再动手”的习惯。就算拥簇一堆 GPU Operator、MIG、Ollama 环境变量,如果不先算清楚显存账、带宽账、延迟账,配置出来的调度策略大概率是看着合理、跑起来崩盘。
一个小技巧收尾:日常排查 GPU 调度问题时,我习惯常驻三个终端命令——watch -n 0.5 nvidia-smi看实时占用,nsys profile --stats=true抓 kernel 时间线,ollama ps验证本地推理的真实使用情况。三张图拼起来,问题基本就水落石出了。
GPU 调度的本质是资源的生意:每一块显存、每一个 SM、每一份带宽,都要花在能带来实际收益的地方。先把账算清楚,再选择合适的调度手段,这块卡才算真正被用透了。