1. 为什么我要从零搭建一套AI基础设施
1.1 一个让我彻底改变想法的真实场景
三年前我还在做传统的后端开发,每天跟数据库、消息队列、微服务打交道。直到有一次,团队接了一个模型推理服务的需求,我信心满满地按照以往经验搭了一套服务架构,结果上线第一天就被打脸了。GPU利用率只有15%,推理延迟忽高忽低,显存溢出报错像下雨一样频繁。那一刻我才意识到,AI基础设施和传统后端完全是两个物种。
传统后端关注的是CPU、内存、磁盘IO、网络吞吐,而AI场景下,GPU显存、算力调度、模型加载、批处理策略、通信带宽才是真正的命脉。你没法用管理MySQL的思路去管理一个推理集群,就像你没法用开出租车的经验去开飞机。这个认知转变,是我决定系统学习AI-Infra的起点。
这篇文章适合谁看?如果你是一个有后端基础但没怎么碰过AI系统的工程师,或者你正在从传统运维转向AI平台方向,又或者你是一个小团队里被推出来“搞一下模型部署”的那个人,那我的这些踩坑经验应该能帮你少走不少弯路。我不打算讲太多论文里的理论,而是把我在实际项目中怎么选型、怎么配置、怎么排查问题的过程完整地摊开来讲。
1.2 AI-Infra到底包含哪些东西
很多人一听到AI-Infra就觉得是“装个GPU服务器跑模型”,这个理解太窄了。我把它拆成四个层面来看,这样你在规划自己的项目时不容易漏掉关键环节。
最底层是硬件与驱动层,包括GPU选型、显存容量规划、NVLink互联、驱动版本匹配、CUDA工具链安装。这一层出问题,上面全白搭。我见过太多人因为驱动版本和框架版本不匹配,折腾一整天才跑起来一个Hello World。
往上是资源调度层,核心问题是怎么把有限的GPU资源分配给多个任务。是用Kubernetes加设备插件,还是用Slurm做HPC调度,还是简单粗暴地手动指定GPU编号?不同规模团队的选择完全不同。小团队手动管理可能更高效,但一旦任务数量超过个位数,没有调度系统就会乱成一锅粥。
再往上是模型服务层,涉及推理框架选型(比如TensorRT、ONNX Runtime、vLLM)、批处理策略、量化方案、模型版本管理。这一层直接决定了你的服务延迟和吞吐量,也是最容易出性能瓶颈的地方。
最顶层是可观测与运维层,包括GPU指标采集、日志聚合、告警规则、成本核算。没有这一层,你的系统就是一个黑盒,出了问题只能靠猜。
1.3 我的整体设计思路和选型逻辑
在动手之前,我先明确了自己的约束条件:团队规模5人以内,GPU资源是4张A100 40G,预算有限不可能上商业平台,但要求能支持至少3个模型服务同时在线,且单次推理延迟控制在200ms以内。
基于这些约束,我做了几个关键决策。调度层我选了Kubernetes加NVIDIA Device Plugin,原因是团队已经有一定K8s基础,学习成本最低,而且社区生态成熟,遇到问题容易找到答案。模型服务层我选了Triton Inference Server作为统一入口,因为它支持多框架、动态批处理、模型版本管理这些我需要的功能,省得自己造轮子。监控层用Prometheus加Grafana加DCGM Exporter,这套组合几乎是业界标配,资料多、坑少。
这里我要特别说一下为什么没有选Slurm。Slurm在HPC领域确实很强,但对于我们这种需要长期在线服务而不是跑批任务的场景,K8s的声明式管理和滚动更新能力更合适。选型没有绝对的对错,关键看你的业务形态。
2. 硬件与驱动层:那些文档不会告诉你的细节
2.1 GPU选型时我踩过的三个坑
第一个坑是只看显存不看带宽。我一开始觉得40G显存够大了,结果跑一个70亿参数的模型做推理时发现,瓶颈根本不在显存容量,而在显存带宽。A100的显存带宽是1555GB/s,而某些消费级卡虽然显存也有24G,但带宽只有几百GB/s,推理吞吐量差了三四倍。所以选卡时一定要看显存带宽这个参数,它比容量更影响推理速度。
第二个坑是忽略了PCIe通道数。我们一开始把GPU插在PCIe 3.0 x8的槽位上,结果多卡通信时带宽直接砍半。后来查了主板手册才发现,只有特定槽位才是x16的。这个细节在服务器采购时一定要跟供应商确认清楚,不然后期没法改。
第三个坑是电源功率余量不足。4张A100满载功耗接近1200W,加上CPU和主板,整机峰值可能到2000W以上。我们最初配的1600W电源在压力测试时直接触发过载保护关机了。建议电源额定功率至少留30%余量,别卡着算。
2.2 驱动和CUDA版本匹配的实操方法
驱动版本和CUDA版本的匹配关系是新手最容易翻车的地方。我的经验是:先确定你要用的深度学习框架版本,然后反推它需要的CUDA版本,再反推对应的驱动最低版本。
具体操作上,我习惯用nvidia-smi查看当前驱动版本,然后去NVIDIA官方文档查兼容表。比如驱动版本515.65.01支持CUDA 11.7及以下。如果你要装PyTorch 2.0,它默认编译的是CUDA 11.7或11.8,那就需要驱动版本至少是515以上。
安装时我推荐用官方runfile方式而不是apt,因为apt会自动升级驱动可能破坏现有环境。命令大概是先禁用nouveau驱动,然后进入文本模式,运行runfile并加上--no-opengl-files参数避免覆盖图形库。装完后用nvidia-smi和nvcc --version双重确认。
注意:每次升级驱动前一定要记录当前版本号,并且准备好回滚方案。我有一次升级后导致所有容器无法启动,最后是靠重装系统解决的,代价太大。
2.3 多卡互联的实际配置经验
如果你有多张GPU,NVLink的配置直接影响多卡推理的性能。以A100为例,它支持NVLink桥接,带宽可达600GB/s,远超PCIe 4.0的64GB/s。但NVLink不是插上就自动生效的,需要在BIOS里确认NVLink选项已启用,并且拓扑结构要合理。
我一般用nvidia-smi topo -m来查看GPU之间的连接关系。理想情况下,需要频繁通信的GPU应该通过NVLink直连,而不是绕道PCIe。如果你的模型做了张量并行,那NVLink几乎是必须的,否则通信开销会吃掉大部分算力。
另外提醒一点,NVLink桥接器的安装需要断电操作,而且不同型号的GPU桥接器不通用。买的时候一定要确认型号匹配,我见过有人买了A100的桥接器想插到A800上,结果物理接口就不一样。
3. 资源调度层:用K8s管理GPU集群的实战配置
3.1 为什么我最终选择了Kubernetes
在调度层选型时我对比了三种方案:手动管理、Slurm、Kubernetes。手动管理在只有一两张卡时最省事,但一旦任务多了,GPU分配冲突、环境隔离、服务发现这些问题会消耗大量精力。Slurm适合批处理任务,但它的服务编排能力弱,不适合长期在线的推理服务。
Kubernetes的优势在于声明式API和丰富的生态。我可以用一个YAML文件描述“我需要2张GPU、16G内存、跑这个镜像”,剩下的调度、重启、扩缩容都由集群自动处理。而且NVIDIA官方提供了Device Plugin和GPU Operator,安装配置都比较成熟。
当然K8s也有代价,它的学习曲线陡峭,网络和存储的配置复杂度高。如果你的团队完全没有K8s经验,我建议先从单机Docker Compose开始,等业务量上来了再迁移。
3.2 NVIDIA Device Plugin的安装与验证
安装Device Plugin是让K8s识别GPU的第一步。我推荐用Helm安装GPU Operator,它会自动处理驱动、容器运行时、Device Plugin、DCGM Exporter等组件的部署和版本匹配。
安装完成后,用kubectl describe node <节点名>查看Capacity字段,应该能看到nvidia.com/gpu: 4这样的资源。如果看不到,通常是以下几个原因:驱动没装好、容器运行时没配置nvidia作为默认runtime、或者Device Plugin的DaemonSet没正常运行。
验证GPU可用性最直接的方法是跑一个测试Pod,在容器里执行nvidia-smi。如果能看到GPU信息,说明整条链路是通的。我建议把这个测试Pod保存成模板,每次集群变更后都跑一遍。
3.3 GPU资源分配的几种策略对比
K8s默认的GPU调度是整卡分配,一个Pod要么占一张卡,要么不占。但实际场景中,有时候一个小模型只需要几G显存,整卡分配太浪费。这时候可以考虑几种方案。
第一种是MIG(Multi-Instance GPU),A100及以上支持把一张卡切成多个独立实例,每个实例有独立的显存和计算单元。配置需要在驱动层启用MIG模式,然后在Device Plugin里暴露MIG设备。优点是隔离性好,缺点是切分粒度固定,不够灵活。
第二种是时间片共享,通过配置让多个Pod分时复用同一张GPU。这种方式实现简单,但隔离性差,一个Pod的显存泄漏可能影响其他Pod。
第三种是显存超卖,一些第三方调度器支持按显存申请量分配。这种方式最灵活,但需要额外的组件支持,稳定性需要自己验证。
我的建议是:生产环境优先用MIG做隔离,测试环境可以用时间片共享提高利用率。
| 策略 | 隔离性 | 灵活性 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 整卡分配 | 高 | 低 | 低 | 大模型推理 |
| MIG | 高 | 中 | 中 | 多小模型混合部署 |
| 时间片共享 | 低 | 高 | 低 | 开发测试环境 |
| 显存超卖 | 中 | 高 | 高 | 资源紧张且任务轻量 |
4. 模型服务层:Triton Inference Server的落地细节
4.1 为什么选Triton而不是自己写Flask服务
我最初是用Flask加Gunicorn包装模型做推理服务的,简单直接。但很快遇到几个问题:并发请求下GPU利用率上不去、批处理需要自己实现、多模型管理混乱、版本更新需要重启服务。
Triton解决了这些问题。它内置了动态批处理,能把多个小请求合并成一个大batch送给GPU,吞吐量提升非常明显。它支持多模型同时加载,每个模型有独立的版本目录,更新模型只需要放新版本文件夹,Triton会自动热加载。它还提供了标准的HTTP和gRPC接口,客户端不用关心底层框架。
当然Triton也有学习成本,它的模型仓库目录结构、配置文件格式、后端选择都需要花时间熟悉。但一旦跑通,后续维护成本比自研服务低得多。
4.2 模型仓库的目录结构和配置要点
Triton的模型仓库有固定的目录规范。每个模型一个文件夹,里面按版本号建子文件夹,每个版本文件夹里放模型文件和config.pbtxt配置文件。
config.pbtxt是核心,它定义了模型的输入输出张量、使用的后端框架、批处理策略、实例数量等。我重点说几个容易配错的参数。
max_batch_size决定了动态批处理的最大批次。设太小浪费GPU并行能力,设太大可能导致显存溢出。我的经验是从8开始试,逐步往上加,观察显存占用和延迟变化。
instance_group控制每个GPU上启动多少个模型实例。对于计算密集的模型,通常一个实例就能打满GPU;对于IO密集或小模型,可以设多个实例提高并发。
dynamic_batching里的preferred_batch_size和max_queue_delay_microseconds需要配合调整。前者告诉Triton优先凑成多大的batch,后者控制等待时间。延迟敏感的场景把等待时间设小,吞吐优先的场景可以设大一些。
4.3 动态批处理的参数调优实录
我拿一个BERT-base的文本分类模型做了调优实验。初始配置是max_batch_size=8,max_queue_delay=1000微秒,实测QPS是120,P99延迟45ms。
第一轮调整把max_batch_size提到32,QPS涨到280,但P99延迟也涨到80ms。这说明批处理确实提升了吞吐,但增加了尾延迟。
第二轮把max_queue_delay降到200微秒,QPS回落到200,P99延迟降到35ms。这个配置适合对延迟敏感的场景。
第三轮我尝试了preferred_batch_size: [16, 32],让Triton优先凑16或32的batch,QPS稳定在260,P99延迟55ms。这个折中方案最终被采纳。
调优的核心逻辑是:批处理越大,GPU利用率越高,吞吐越大,但单个请求的等待时间也越长。你需要根据业务对延迟和吞吐的权重来找到平衡点。
实操心得:调参时一次只改一个变量,并且用固定的压测工具和数据集。我习惯用
perf_analyzer,它是Triton自带的压测工具,能直接输出延迟和吞吐的百分位数据。
4.4 模型版本管理与灰度发布
Triton的版本管理很简单:在模型文件夹下建不同数字的版本目录,Triton会自动加载最新版本。但生产环境不能这么粗暴,你需要控制流量切换。
我的做法是在config.pbtxt里设置version_policy,指定只加载特定版本或版本范围。灰度发布时,先部署新版本但只分配少量流量,观察指标正常后再全量切换。
具体操作上,我用了两个Triton实例加一个反向代理。旧版本实例和新版本实例同时运行,反向代理按权重转发请求。等新版本稳定后,再把旧版本实例下线。这种方式对客户端完全透明,回滚也只需要调整代理权重。
5. 可观测与运维层:让GPU集群不再是黑盒
5.1 GPU指标采集的完整方案
没有监控的GPU集群就像没有仪表盘的飞机。我用的方案是DCGM Exporter加Prometheus加Grafana。DCGM Exporter是NVIDIA官方提供的指标导出器,能采集GPU利用率、显存占用、温度、功耗、ECC错误等几十个指标。
部署上,DCGM Exporter通常以DaemonSet形式运行在每个GPU节点上,暴露一个HTTP端点给Prometheus抓取。Prometheus的抓取间隔我设的是15秒,太频繁会增加开销,太稀疏会漏掉瞬时峰值。
Grafana面板我推荐直接导入NVIDIA官方提供的Dashboard模板,它已经包含了最常用的图表。然后根据业务需求再自定义一些面板,比如按模型维度的推理延迟分布、按Pod维度的GPU使用时长统计。
5.2 我设置的几条关键告警规则
告警不在多而在准。我设置了四条核心告警,覆盖了最常见的故障场景。
第一条是GPU显存使用率超过90%持续5分钟。这通常意味着有内存泄漏或者batch size设得太大,需要及时干预,否则下一步就是OOM崩溃。
第二条是GPU温度超过85度。A100的降频温度是85度左右,超过这个值算力会下降。如果持续高温,需要检查散热风道和机房环境温度。
第三条是推理P99延迟超过500ms持续3分钟。这说明服务已经无法满足SLA,可能是流量突增或者模型出了问题。
第四条是GPU ECC错误计数增加。显存出现不可纠正错误是硬件故障的前兆,需要尽快安排维护。
告警通道我用的企业微信机器人,分级发送。P0级别直接电话通知,P1级别发群消息,P2级别只记录不打扰。
5.3 成本核算与资源回收的实践
GPU资源很贵,浪费就是烧钱。我做了两件事来控制成本。
第一件是给每个Pod打上成本标签,记录它属于哪个项目、哪个团队。然后写了一个脚本每天统计各项目的GPU使用时长,生成报表发给负责人。这个动作让大家的资源意识明显提高,很多长期闲置的测试任务被主动清理了。
第二件是设置了空闲资源回收机制。对于开发测试环境的Pod,如果连续2小时GPU利用率低于5%,就自动发送提醒;如果连续6小时低于5%,就自动缩容到零。当然这个策略需要提前和团队沟通好,避免误杀正在调试的任务。
6. 常见问题与排查技巧实录
6.1 GPU相关问题的排查思路
GPU问题排查我总结了一个从下到上的检查顺序。先看物理层:nvidia-smi能不能正常输出,GPU是否被识别,温度功耗是否正常。再看驱动层:dmesg | grep -i nvidia有没有报错,驱动版本和CUDA版本是否匹配。然后看容器层:容器里能不能看到GPU设备,nvidia-container-cli -k -j info输出是否正常。最后看应用层:框架能不能正常调用CUDA,显存分配是否成功。
这个顺序能帮你快速定位问题在哪一层,避免盲目尝试。
6.2 推理服务性能不达预期的排查清单
| 现象 | 可能原因 | 排查方法 | 解决方向 |
|---|---|---|---|
| GPU利用率低 | batch size太小 | 查看Triton的batch统计 | 增大max_batch_size |
| 延迟波动大 | 队列等待时间长 | 查看queue时间指标 | 调整max_queue_delay |
| 吞吐上不去 | 模型实例数不足 | 查看instance_group配置 | 增加实例数 |
| 显存溢出 | batch太大或泄漏 | 监控显存曲线 | 减小batch或排查泄漏 |
| 多卡效率低 | 通信瓶颈 | 查看NVLink拓扑 | 优化并行策略 |
6.3 几个让我印象深刻的故障案例
有一次服务突然全部超时,排查发现是某个模型的显存泄漏导致GPU OOM,进而影响了同一张卡上的其他模型。教训是:生产环境一定要做显存隔离,要么用MIG,要么至少把关键模型独立部署。
还有一次是驱动自动升级导致CUDA不可用。原因是系统开启了unattended-upgrades,半夜自动更新了驱动。教训是:生产环境的GPU节点一定要关闭自动更新,所有变更走人工审批。
最后一个案例是网络存储挂载失败导致模型加载卡住。Triton在启动时会从模型仓库加载所有模型,如果仓库在网络存储上且挂载异常,整个服务就起不来。教训是:模型仓库尽量用本地SSD,或者至少做好挂载失败的快速检测和告警。
6.4 日常运维的检查清单
我每天早上会花十分钟过一遍这些检查项:GPU节点是否全部Ready、DCGM Exporter是否正常上报、Prometheus是否有抓取失败、Triton实例是否全部健康、模型版本是否与预期一致、告警是否有未处理的积压。这个习惯帮我提前发现了不少潜在问题,比出了问题再救火要从容得多。
这套AI-Infra从零搭到现在稳定运行了半年多,中间经历了三次大版本迭代和无数次小调整。最大的体会是:不要追求一步到位,先让核心链路跑通,再逐步优化。我见过太多人一开始就想搭一个完美的平台,结果三个月过去了还在选型阶段。先跑起来,问题会在运行中暴露,你也会在解决过程中真正理解每个组件的作用。