ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

AI系统性能工程实战:从度量定位到推理训练优化

AI系统性能工程实战:从度量定位到推理训练优化 1. 为什么“AI 系统性能工程”值得单独拎出来讲这两年大家都在聊模型怎么训、参数怎么调、效果怎么刷榜但真正把 AI 系统推到生产环境里跑起来的人都知道训练只是上半场性能工程才是决定这套系统能不能活下去的下半场。我见过太多团队模型指标漂亮得不行一上生产就露馅推理延迟从实验室的 80ms 飙到线上的 800msGPU 利用率常年趴在 30% 以下显存动不动就 OOM扩容吧成本扛不住不扩容吧用户骂街。这些问题不是模型本身的问题而是系统性能工程没做到位。所谓 AI 系统性能工程说白了就是围绕 AI 工作负载训练、推理、数据处理去系统性地分析、度量、优化整条链路的性能表现。它横跨的层面特别多从最底层的 GPU/加速器硬件特性到 CUDA kernel 级别的算子优化再到框架层的计算图调度往上还有服务层的批处理策略、请求队列管理、多实例编排最后到集群层的资源调度和弹性伸缩。任何一个环节掉链子整体性能都会被拖垮。这篇文章适合谁看如果你是把模型从 notebook 往生产推的算法工程师你需要知道推理服务为什么慢如果你是负责部署和运维的后端或平台工程师你需要理解 AI 负载和传统 Web 服务的本质差异如果你是技术负责人你需要一套判断“这套系统到底还有多少优化空间”的方法论。我会尽量把原理讲透同时给出可以直接抄作业的实操手段不玩虚的。需要先明确一个认知AI 系统的性能问题绝大多数不是单点问题而是链路问题。你优化了算子结果卡在数据预处理你优化了批处理结果卡在显存带宽你上了多卡并行结果卡在通信。所以性能工程的核心方法论是“先度量、再定位、后优化”而不是凭感觉瞎调。接下来我会按这个思路一层一层往下拆。2. 性能工程的第一性原理先搞清楚瓶颈在哪2.1 度量先行没有数据就没有优化我踩过最大的坑就是一上来就凭直觉改代码。看到推理慢第一反应是“模型太大”于是去量化、去剪枝折腾一周发现瓶颈其实在 CPU 侧的图像预处理上GPU 一直在等数据。这种教训太多了所以我现在坚持一个原则任何优化动作之前必须先有可信的度量数据。度量要分层次做。最粗的一层是端到端指标QPS、P50/P95/P99 延迟、错误率、资源利用率。这一层告诉你“系统现在什么状态”。但光有这层不够因为它不告诉你“为什么”。所以第二层要做分段计时把一次请求拆成网络接收、预处理、模型前向、后处理、结果返回几个阶段分别打点。第三层是深入到 kernel 级别用 profiler 看每个算子的耗时和 GPU 占用。这里有个经验P99 延迟比平均延迟重要得多。AI 服务的延迟分布往往是长尾的平均值看着还行但 P99 可能是平均值的五到十倍。用户感知到的卡顿几乎都来自长尾。所以做性能目标时一定要盯 P99 甚至 P999而不是平均值。2.2 瓶颈的三种类型算力、带宽、调度定位瓶颈时我习惯把它归到三类里。第一类是算力瓶颈GPU 的 SM 利用率打满算力就是不够这种情况只能靠更强的卡、更高效的算子或者模型压缩来解决。第二类是带宽瓶颈包括显存带宽和 PCIe/NVLink 通信带宽典型表现是 GPU 利用率不高但就是快不起来数据搬运成了瓶颈。第三类是调度瓶颈CPU 和 GPU 之间、多个 GPU 之间、多个服务实例之间的协调没做好导致资源空转。区分这三类有个简单办法看 GPU 利用率和显存带宽利用率的关系。如果 GPU 算力利用率高、带宽也高那是真算力瓶颈如果算力利用率低但带宽高说明在等数据搬运如果两个都低那基本是调度或 CPU 侧的问题。这个判断能帮你快速缩小排查范围避免在错误的方向上浪费时间。2.3 一个真实的定位案例之前有个推荐模型的推理服务线上 P99 延迟 600ms目标是压到 200ms 以内。团队一开始怀疑是模型太大准备上量化。我让他们先做分段计时结果发现模型前向只占 120ms剩下 480ms 全耗在特征拼接和序列化上。特征是从多个存储拉取的串行执行每个存储平均 80ms六个存储就是 480ms。问题定位清楚后优化就简单了把特征拉取改成并行加一层本地缓存P99 直接降到 180ms模型一行没改。这个案例说明性能工程的价值往往不在于把某个环节做到极致而在于找到那个真正拖后腿的环节。如果当时直接上量化可能模型精度掉了延迟还没降下来纯属白忙。3. 推理服务的性能优化从单请求到高并发3.1 批处理AI 推理性能的最大杠杆如果说推理优化只能做一件事那一定是批处理Batching。GPU 这种硬件天生适合并行计算单个请求喂进去算力利用率可能只有百分之几纯属浪费。把多个请求攒成一批一起算吞吐量能提升几倍到几十倍这是 AI 推理和传统 Web 服务最大的区别。但批处理不是简单地把请求堆一起就完事。核心矛盾在于批越大吞吐越高但单个请求的等待时间也越长。你攒批攒了 50ms那这 50ms 就是额外延迟。所以工业界普遍用动态批处理Dynamic Batching设置一个最大等待窗口比如 10ms和一个最大批大小比如 32窗口内攒到多少算多少到点就发。这样在延迟和吞吐之间取平衡。我实测过一组数据同一个 BERT 类模型单请求推理延迟 15msQPS 大概 60开启动态批处理批大小 16、等待窗口 10msQPS 能到 700 以上P99 延迟只增加了 12ms 左右。这个投入产出比比换卡划算多了。注意批处理对延迟敏感的场景要慎用。比如实时对话、在线游戏这类用户对延迟极其敏感攒批带来的额外等待可能得不偿失。这时候要考虑用更小的批或者干脆不批靠模型压缩和算子优化来提速。3.2 显存管理与 KV Cache 优化大模型推理绕不开 KV Cache。自回归生成时每生成一个 token 都要用到之前所有 token 的 Key 和 Value如果每次都重算计算量是平方级的根本没法用。所以要把历史 KV 缓存下来。但 KV Cache 非常吃显存一个 13B 模型、batch size 32、序列长度 2048 的情况下KV Cache 可能占掉几十 GB 显存。优化 KV Cache 有几个方向。一是分页管理借鉴操作系统的虚拟内存思路把 KV Cache 切成固定大小的块按需分配避免预分配造成的浪费这个思路在 vLLM 的 PagedAttention 里体现得很典型。二是量化把 KV Cache 从 FP16 压到 INT8 甚至 INT4显存占用直接砍半甚至更多代价是轻微的精度损失。三是共享前缀多个请求如果有相同的 system prompt那部分 KV 可以共享不用每个请求都存一份。显存管理的另一个重点是内存池化。频繁的显存分配和释放会产生碎片时间长了就会出现“明明总显存够但就是分配不出来”的情况。解决办法是启动时预分配一大块显存自己管理分配避免频繁调用底层分配器。这个在长稳服务里特别重要我见过跑了三天才 OOM 的服务就是碎片累积导致的。3.3 算子融合与计算图优化模型推理时很多小算子串行执行每个算子都要读写一次显存算子之间的调度也有开销。算子融合就是把能合并的算子合成一个减少显存读写和 kernel 启动次数。比如常见的 ConvBNReLU 融合LayerNorm 和后续的矩阵乘融合都能带来明显的加速。计算图优化则是从更高层面做文章。比如常量折叠把编译期能算的提前算掉、死代码消除去掉推理用不到的节点、算子替换用更高效的等价算子。这些工作现在很多框架都能自动做但前提是你要把模型导出成正确的图格式并且确保没有动态控制流打断优化。我个人的经验是算子融合的收益在小模型和低 batch 场景下更明显因为这时候 kernel 启动开销占比高。大 batch 场景下计算本身占主导融合的收益相对小一些。所以优化要有针对性别盲目上。3.4 多实例与并发编排单张卡跑一个模型实例往往吃不满资源尤其是小模型。这时候可以在一张卡上跑多个实例每个实例处理一部分请求提高资源利用率。但多实例会带来显存竞争和算力竞争需要仔细调参。更常见的是多卡多实例的编排。这里的关键是负载均衡和请求路由。如果每个实例的负载不均有的排队有的空闲整体吞吐就上不去。好的做法是在网关层做感知后端负载的路由把请求发给当前最空闲的实例。另外要考虑实例的亲和性比如同一个用户的连续请求尽量路由到同一个实例这样能复用 KV Cache减少重复计算。4. 训练与数据链路的性能工程4.1 数据加载最容易被忽视的瓶颈训练性能优化大家第一反应都是分布式并行、混合精度这些但数据加载往往是真正的瓶颈只是它藏得比较深。GPU 在那边等着数据CPU 还在慢慢解码图片、做增强GPU 利用率自然上不去。判断数据加载是不是瓶颈看一个指标就够了GPU 利用率是否周期性波动。如果利用率在 90% 和 30% 之间来回跳基本就是数据供给跟不上。解决办法有几个增加数据加载的 worker 数量、把数据预处理放到 GPU 上做、提前把数据预处理成二进制格式减少解码开销、用更快的存储。我做过一个图像训练任务原始数据是 JPEG每个 epoch 光解码就花掉大量 CPU 时间。后来把数据预处理成 WebDataset 格式把图片和标签打包成 tar 分片配合多 worker 预取GPU 利用率从 60% 拉到 95% 以上训练时间直接砍掉三分之一。这个优化不需要改模型纯工程手段性价比极高。4.2 分布式训练的通信优化多卡训练时梯度同步是绕不开的。数据并行下每个卡算完梯度要 AllReduce 求平均这个通信量跟模型参数量成正比。模型一大通信就成了瓶颈卡越多反而越慢这就是所谓的通信墙。优化通信有几个思路。一是梯度压缩把 FP32 梯度压成 FP16 甚至更低精度再传通信量减半。二是通信与计算重叠在算反向传播的时候就开始传已经算好的梯度别等全部算完再传。三是拓扑感知的通信策略让通信尽量走 NVLink 这种高带宽链路少走 PCIe。四是混合并行把数据并行、张量并行、流水线并行组合起来减少单次通信的量。这里有个反直觉的点不是卡越多越快。加卡能提升算力但通信开销也线性增长存在一个收益递减的拐点。我见过 8 卡比 4 卡还慢的案例就是因为通信没优化好。所以扩容之前一定要先测一下扩展效率别盲目堆卡。4.3 Checkpoint 与容错的开销大模型训练动辄几天几周中间要存 checkpoint。如果 checkpoint 存得慢训练就得停下来等这个开销很可观。一个百 GB 级的 checkpoint写到慢速存储可能要几分钟一天存几次累积起来就是几小时。优化 checkpoint 的思路是异步保存先把参数拷到 CPU 内存或者另一块显存训练继续跑后台慢慢往存储写。这样训练几乎不中断。另外可以用增量保存只存变化的部分减少写入量。还有分片保存多个 rank 并行写不同的分片提高写入带宽。容错方面要考虑节点故障时的恢复策略。如果每次故障都从头开始那成本太高。好的做法是定期存 checkpoint故障后从最近的 checkpoint 恢复并且要保证恢复过程本身足够快。这里有个细节恢复时的数据加载位置要和 checkpoint 对齐否则数据会重复或遗漏这个坑我踩过。5. 性能监控与持续优化体系5.1 建立分层监控指标体系性能工程不是一次性工作而是持续的过程。要持续就得有监控。我建议建立三层指标体系。业务层看 QPS、延迟分位数、错误率、成本系统层看 GPU 利用率、显存占用、CPU 负载、网络带宽、存储 IO应用层看批大小分布、队列长度、缓存命中率、各阶段耗时。这三层要能关联起来。比如业务层发现 P99 延迟涨了能下钻到系统层看是不是某张卡利用率异常再下钻到应用层看是不是批处理策略出了问题。没有这种关联能力监控就只是一堆孤立的数字出问题时还是抓瞎。5.2 压测与容量规划上线前一定要做压测而且要用真实流量分布压不能只用均匀流量。真实场景下请求大小、序列长度、并发模式都是变化的用平均值压测会严重高估系统能力。我习惯用生产流量的采样回放来做压测这样测出来的容量才靠谱。容量规划要留 buffer。AI 系统的性能对输入分布很敏感序列长度翻倍显存和计算量可能涨好几倍。所以规划时不能按当前流量卡着算要预留足够的余量。我的经验是按峰值流量的 1.5 到 2 倍来规划资源同时做好弹性伸缩流量涨了能快速扩容。5.3 常见性能问题速查表现象可能原因排查方向解决手段GPU 利用率低且波动数据供给不足看数据加载耗时、worker 数增加 worker、预处理加速、预取GPU 利用率低且平稳调度或 CPU 瓶颈看 CPU 负载、kernel 启动开销算子融合、减少同步、多实例显存 OOM 但总量够显存碎片看分配器行为、长稳趋势内存池化、预分配、分页管理P99 延迟远高于均值长尾请求、排队看延迟分布、队列长度动态批处理调参、负载均衡多卡扩展效率低通信瓶颈看通信耗时占比、拓扑梯度压缩、通信重叠、混合并行吞吐上不去但资源没满串行环节分段计时找串行点并行化、异步化、缓存这张表是我这些年排查问题的经验沉淀遇到性能问题先对号入座能省不少时间。当然实际情况往往更复杂多个问题交织在一起这时候就要靠分层度量来逐个击破。6. 我踩过的坑和几条实在的经验先说一个最容易被忽视的点性能优化要有基线也要有回归测试。我见过团队优化完一个环节指标确实好了但过两周又退回去了因为没人守着。所以每次优化后要把配置和指标固化下来纳入 CI一旦回归就报警。性能这东西不进则退。第二个经验是别过早优化但也别太晚。过早优化是指在还没搞清楚瓶颈时就瞎调浪费时间还可能引入 bug。太晚优化是指等系统上线、用户投诉了才动手那时候改造成本高得多。我的建议是在架构设计阶段就考虑性能预留优化空间但具体调优放到有真实负载之后做。第三个是关注成本不只是速度。性能工程的终极目标不是最快而是在满足延迟要求的前提下成本最低。有时候多花点算力换更低的延迟是值得的有时候反过来。要算清楚每 QPS 的成本别为了刷指标把成本刷上天。最后一个也是我觉得最重要的性能工程是团队协作不是个人英雄主义。算法、后端、运维、硬件每个角色看到的视角都不一样只有把大家的信息拼起来才能看到完整的图景。我见过太多因为信息不对称导致的无效优化明明瓶颈在 AB 团队却在拼命优化 C。建立跨团队的度量共享和沟通机制比任何单点技术都重要。这套东西展开讲能讲很多AI 系统性能工程本身就是个持续演进的领域硬件在变、框架在变、模型结构也在变没有一劳永逸的方案。但只要掌握了“度量、定位、优化、回归”这个循环面对新问题就不会慌。后面有机会再聊聊具体某个框架或者某类模型的性能调优细节那又是另一番天地了。
返回列表