ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

AI性能工程实战:从指标构建到推理训练优化

AI性能工程实战:从指标构建到推理训练优化 很多人一提到 AI 性能工程第一反应是“调 GPU 参数”“改推理框架配置”。干过几年之后我越来越觉得这个领域真正难的其实不是单个点的调优而是你能不能建立起一整套从指标体系、压测方法到排障流程的工作方式。之前写过第一篇的基础内容今天这篇继续往深了讲把训练和推理两条链路里那些最常踩的坑、最有效的做法以及我自己的实操经验一次性梳理清楚。这篇文章适合正在做 AI 应用落地、大模型推理服务、训练平台优化的人看。不管你是算法工程师、后端开发还是 SRE只要你的工作跟“让 AI 系统跑得更快更省”有关这里面的方法论和具体手法都能直接用。内容会偏工程实践不聊太虚的概念重点放在“为什么这么做”和“怎么做才不翻车”。1. AI 系统的性能问题为什么和传统软件不一样1.1 传统性能优化的惯性思维在哪里失效做过后端性能优化的人通常有一套成熟打法先压测再看监控QPS 上不去就扩副本接口慢就用 profiling 找到热点函数缓存、连接池、异步化三板斧下去基本能解决大部分问题。这套思路放到 AI 系统里不能说完全没用但如果你照搬过来很容易撞墙。最核心的差异在于传统软件的性能瓶颈是相对分散的而 AI 系统的性能是高度耦合的。举个具体例子一个推荐系统里嵌了一个 CTR 模型你单独优化模型的推理延迟到 10ms但上游特征工程在高峰期耗时突然飙到 80ms整个推荐接口的 P99 依然惨不忍睹。这时候你再快 10 倍模型也没用瓶颈已经转移了。换句话说AI 系统本身是一整个流水线数据接入、特征处理、模型推理、后处理、业务逻辑调度哪个环节弱整条链路就烂在那里。还有一个容易被忽视的点传统性能优化追求的是“峰值能力”比如双十一压测支持多少万 QPSAI 系统要的是“稳定的低延迟 高吞吐”而且两者往往是冲突的。你为了追求吞吐把 batch size 加大单次推理的延迟会明显上升用户侧的体感变差。你为了追求低延迟用很小的 batchGPU 利用率又上不去成本翻倍。这就是 AI 性能工程里最经典的“吞吐 vs 延迟”矛盾所有方案设计本质上都在找这个平衡点。1.2 AI 链路里的新瓶颈数据搬移和资源焦虑传统软件性能工程师盯的最多的是 CPU、内存、磁盘 IO。AI 系统里多了一个“显眼包”——GPU但 GPU 并不是唯一需要注意的资源。我自己的体会是AI 系统里最大的隐性瓶颈其实有两个一个是数据搬移一个是显存墙。先说话数据搬移。很多训练任务看起来 GPU 利用率在 90% 以上以为已经拉满了但仔细拼开看计算和通信的 overlap 情况发现大量时间花在节点间同步梯度、CPU 到 GPU 的拷贝上。数据搬移不像计算那样容易量化而且问题往往藏得很深比如 GPU Direct 没开、网络拓扑不对、存储带宽不达标表现出来却是莫名其妙的“GPU 吃不满”或“训练速度忽快忽慢”。再说显存墙。大模型的权重、优化器状态、中间激活、KV Cache 每一项都是显存大户。显存一爆第一反应都是“调小 batch”但这就等于让算力打折。真正可做的方向比想象中多激活重算、梯度累积、混合精度、显存碎片整理、KV Cache 量化每一个都值得单独去试。这也回答了我经常被问的一个问题“显存不够怎么办”标准答案不是无脑调小 batch而是把显存消耗拆开看看哪一项最大再针对性地做优化。2. 别只盯 GPU 利用率AI 系统的指标体系到底怎么建2.1 训练阶段从“看多少卡”到“看 MFU”训练侧最容易犯的错误是拿 GPU 利用率作为唯一指标。这个数字高不代表你的计算效率高它只能说明 GPU 没有完全闲着。同样的模型训练有人能用 16 张卡跑出很高的 MFU有人用 32 张卡反而更慢原因就在于并行策略、通信同步和数据管线这些“看不见的部分”。我更推荐用这几个指标组合起来看训练健康度指标含义关注点GPU 利用率SM 繁忙程度是否被小算子拖垮MFU / HFU模型算力利用率真实计算效率samples/s 或 tokens/s训练吞吐端到端效果通信占比同步时间占总时长比例并行策略是否合理显存占用权重激活梯度等是否存在浪费和 OOM 风险训练吞吐反映了整个数据流水线到计算再到通信的最终效果它的升降比单看 GPU 利用率更有说服力。举个我自己踩过的例子有一次我们训练一个多模态模型GPU 利用率看起来很正常但 samples/s 一直不达标。后来用 PyTorch Profiler 详细排查才发现问题出在 DataLoader 的 prefetch 不够GPU 每个 step 要空等几百毫秒。GPU 利用率高是因为它一旦开算就全力以赴但“算”和“等”交替进行整体吞吐就被拉下来了。所以训练侧一定要结合“吞吐 利用率 通信时间”一起看单看哪一个都会被误导。2.2 推理阶段延迟拆开看才知道疼在哪推理侧的指标比训练侧复杂因为用户的真实体验是“端到端延迟”但模型服务内部的耗时结构完全是另一回事。一个大模型推理服务如果只看接口平均耗时基本等于什么都没看你需要把延迟拆成几个关键段TTFTTime To First Token从请求进来到把第一个 token 返回给用户的时间。聊天场景里 TTFT 直接决定“用户会觉得你卡吗”。TPOTTime Per Output Token生成阶段每个 token 的平均耗时。它决定了后续内容的流畅度。ITLInter-Token Latency相邻 token 之间的实际间隔用户打字速度的类比就是这句话。整体吞吐一个实例每秒能生成的 token 数或者并发输入和完成的请求数。互联网后端常见的“P99 延迟”在这里依然适用但需要特别小心生成式场景的分布。大模型的 decode 阶段受批大小和内存带宽影响很大尾延迟往往比均值高出几倍所以只看平均值容易产生“还挺快”的错觉P95/P99 才有真正的参考意义。同时还要注意“排队时间”作为独立指标因为高并发下大量请求在等 prefill 或等显存用户明明模型推理很快整体却卡顿这种情况和模型本身关系不大更可能是调度或 GC 策略没做好。2.3 成本效率指标每千 token 成本才是王道AI 性能工程做了几年我最大的一个感触是绕来绕去最终都会回到“单位成本”和“单位算力产出”这两个词上。模型效果好是一回事能不能用可控成本稳定对外提供服务是另一回事。这就是为什么现在很多团队在推“每百万 token 的推理成本”这个指标。你可以自己做一张简单的成本账把一台推理机器的租用成本除以它一个小时内产出的 token 总量得到每 token 成本。同一型号的模型A 团队配置出来的每 token 成本可能是 B 团队的三分之一但服务质量差别不大。差距从哪来就是 batch 策略、KV Cache 命中率、量化方案、在线离线部署规划这些细节叠出来的。性能工程做到最后本质是一种“用更少资源办更多事”的工程能力指标上的体现就是成本效率。3. 推理侧性能优化框架、参数、量化的实操取舍3.1 框架选型定上限别在错误的地基上搬砖推理框架的选型几乎是所有性能优化工作的前提。我经常跟团队说一句话“同样的模型换框架可能直接带来 2-5 倍吞吐提升这是任何后续参数调优都比不了的。”现在主流的自研或开源框架里性能表现比较好的基本集中在 vLLM、TensorRT-LLM另外还有不少团队基于 FasterTransformer 风格的组件自研调度层。选型时除了看 benchmark 数据更重要的是看它们和你的部署环境、模型结构、生态成熟度能不能匹配。vLLM 的核心优势是 PagedAttention 和连续批处理Continuous Batching它把显存利用率提升了非常多动态 batch 时也不容易因为显存碎片被迫降低并发。TensorRT-LLM 的优势是在 NVIDIA 硬件上做了极致的 kernel 融合和低精度优化吞吐比 vLLM 高一点但它对模型支持的灵活性差一些很多自定义算子需要自己写 plugin。小模型或者需要频繁迭代的模型用高灵活性的框架更稳妥定制化极高的生产环境用 TensorRT 系做深度优化更容易出效果。我的建议是不要把框架当作一个固定配置去用而是要理解它的调度语义。比如 vLLM 里一个模型实例的 max_num_seqs 和 gpu_memory_utilization 两个参数直接决定了单实例能承接的并发量和显存预留比例。调高了这俩吞吐可能上去但显存风险跟着来调低了延迟很稳但成本不划算。类似这种参数的调整背后需要有一套压测数据来支撑而不是拍脑袋。3.2 KV Cache 和调度策略被低估的两个大头推理侧优化里KV Cache 往往是压榨性能最大的突破口。大模型生成 token 时要把历史 token 的 key/value 缓存下来缓存越大显存占用越高但命中率越高速度越快。这里就有一个“缓存分配和预留给新请求空间”的平衡问题。连续批处理是另一个容易被低估的优化。传统 batch 是“一批请求来齐了才开始推理”如果请求到达时间很分散GPU 大量时间在空等。连续批处理则允许新到的请求插入当前批次只要显存和算力允许随时放进推理流里。这个机制对吞吐的提升非常可观但副作用是可能引入额外的调度延迟尤其是 prefill 阶段长序列和 decode 短序列混在一起时必须做优先级控制。生产环境里我还推荐用chunked prefill处理长短不一的请求。把很长的 prefill 切成小块和 decode 阶段插在一起执行避免一个长序列请求把 GPU 全占了导致其他请求全部卡死。这套方案对“P99 高但不清楚为什么高”的推理服务往往立竿见影。3.3 量化和投机解码的“性价比账”量化几乎是推理部署的必选项。从 FP16 到 INT8推理速度通常提高 40%-80%显存减半效果在多数场景下可接受到 INT4 则要看模型敏感度有些模型精度损失小有些则崩得厉害。AWQ、GPTQ 这些方法在 7B、13B 模型上的表现都有公开数据但实际用下来一定要拿你自己的业务数据做离线评测别只看 benchmark 上那个把模型烤糊才测出来的数字。投机解码Speculative Decoding的思路是让一个小的草稿模型先快速生成一批候选 token再由大模型一次性验证。这个方案在小模型和超大模型搭配的场景下效果非常好实测里生成长文本的吞吐能提升 2-3 倍。但它也挑场景如果业务本身的输出很短收益会打折如果草稿模型和大模型分布太接近正确率太高验证步骤反而成为拖累。所以做优化时一定要把“场景特征”纳入考量不要拿一个方案到处套。4. 训练侧性能工程从静态调参走向数据驱动4.1 训练画像先把 GPU 吃饱这件事拆开看训练侧优化的目标是让每张卡都“吃满”注意这里说的吃满不是单纯的利用率高而是计算、通信、数据供给三者的重叠度和效率同时达标。怎么判断当前瓶颈在哪我会用 profiling 工具给训练任务做个“画像”记录每个 step 里 GPU compute 时间、通信时间、数据加载等待时间、空闲时间的占比。如果GPU compute 时间正常但 step 总时间很长多半是同步通信或数据加载拖了后腿。如果GPU 利用率跳动很大说明数据供给不稳定DataLoader 或者预取逻辑有问题。如果通信时间占比持续偏高比如超过 20%-30%就要考虑模型并行策略是否需要调整了。如果显存占用中 activation 占比异常大说明 batch 太大或者没有开启激活重算。拿现实里的例子说我们有一次训练 70B 模型从 32 卡扩到 64 卡以后理论上训练速度应该接近翻倍结果只提升了 40%。用 Nsight Compute 和 PyTorch Profiler 一看通信占比从 15% 涨到 35%Allreduce 把时间吃掉了。后来把数据并行度降了改为张量并行结合流水线并行通信耗时降下来整体吞吐明显回升。这个案例说明训练优化要跟着数据走不是简单加卡就能解决问题。4.2 数据管线和数据供给最容易被忽视的隐藏瓶颈训练性能的问题里数据管线的坑最隐蔽因为它不会直接把任务打挂只会让效率一点点漏掉。GPU 计算速度越快数据供给的瓶颈就越突出。很多人以为 num_workers 调大就完事了其实还要看是否开启 persistent_workers、prefetch_factor 是否匹配、数据是否落在本地 NVMe甚至 CPU 和 GPU 之间拷贝的 pinned memory 设置。这些细节加起来一个 step 的等待时间可能从 10ms 变成 100ms整体训练时长拉长一倍都不稀奇。我的做法是把数据读取链路当成一个独立服务来设计。比如训练图像模型时先把图片做批量预处理并缓存成 TFRecord/LMDB 格式训练文本模型时提前把 tokenize 结果落盘避免每个 epoch 都在重复做 tokenize。数据增强如果太重优先放到 GPU 上做或者并行化。这样即使数据量大供给端也能稳定跟上 GPU 的消耗。4.3 并行策略不只是“模型太大放不下”的选择题很多团队选择模型并行是因为“单卡放不下”但实际上并行策略的选择对性能的影响远不止显存这一点。数据并行最简单但对大模型来说通信量太大张量并行能减少通信量但 GPU 数量需要成组约束物理拓扑很关键流水线并行则可以减少卡间通信但会引入“流水线气泡”需要靠 micro-batch 调度尽量填满。这几个策略通常不是二选一而是组合使用。一个 70B 模型在机间用数据并行机内用张量并行层间再用流水线并行这种组合在工程上非常常见。但组合多了通信模式就复杂了任何一块配置不合理都会导致算力浪费。我个人觉得最佳实践是“先跑通再 profiling再调整”不要一开始就信某篇博客的推荐配置不同硬件拓扑和网络环境下最优组合可能完全不一样。5. 实战排障一次推理服务的性能问题完整复盘5.1 排查前的准备性能基线是唯一可信的依据先讲一个我自己的习惯接到任何性能问题反馈不急着改配置先做三件事。第一确认监控数据是完整可信的至少要有时间戳对齐的 QPS、延迟分布、GPU 利用率、显存、网络 IO 和日志第二和发布历史对照看性能恶化是从哪个版本开始的这个信息往往直接指向嫌疑代码第三用压测工具在当前环境跑出一组性能基线作为后续改动前后的对比依据。没跑基线就动手是最常见的翻车方式。性能问题的表现往往是多个变量共同作用的结果你改了一个参数症状可能没消失只是因为另一个瓶颈被掩盖了。有基线才有资格谈控制变量。我建议每个 AI 服务在上线前就把压测镜像和压测流量准备好版本更新自动跑一轮把性能变化作为发布准入的一部分。5.2 常见性能异常速查表下面这张表是我在实际排查里经常对照的不一定能覆盖所有情况但能帮你快速定位大头问题现象优先怀疑方向快速验证手段GPU 利用率低CPU 忙DataLoader/预处理线程观察 CPU 占用和 step 等待时间GPU 利用率低网络忙多机通信瓶颈检查 NCCL 日志和网络吞吐TTFT 高prefill 慢KV Cache 分配/显存碎片看 prefill 耗时占比TPOT 高输出卡顿decode 未优化、量化不合适对比不同 batch 下的 TPOT并发高时延迟整体飙升调度器排队/显存不足看 queue 长度和显存水位显存 OOMbatch/激活/KV Cache 分配过大逐项估算显存消耗训练 loss 震荡严重学习率/数据顺序/梯度同步问题对比相同数据下的基线这张表的逻辑是“现象 → 方向 → 验证”而不是“现象 → 结论”。因为同一现象可能对应多种原因直接把结论拍死很容易误判。比如 GPU 利用率低可能因为是数据加载也可能是因为算子本身太小、并行度不够。所以先列方向再逐个验证是稳定不翻车的排障姿态。5.3 一次真实案例P95 延迟突增背后的真相有一回我们上线了一个大模型聊天服务压测下来的平均延迟和 P50 都很好唯独 P95 一直在 3-4 秒之间横跳。从监控看GPU 利用率并不高显存也够看起来一切都正常。我们一开始怀疑是偶发 GC 或网络抖动后来加了分阶段埋点才发现prefill 和 decode 的时间分布极不均匀。原因其实很典型长 prompt 的请求和短 prompt 的请求混在同一个连续 batch 里每次新请求的 prefill 都会抢占 GPU把原来正在进行的 decode 任务打断后续 token 生成全部往后推。表现上就是 P50 很漂亮P95 被这些“插队”的长请求拖得很惨。最后我们开启了 chunked prefill同时限制了单个 prefill 请求占用的算力配额让新请求插入时的冲击变得平滑。改完后 P95 从 3.5 秒降到 1.2 秒总吞吐还略有上涨。这个案例特别能说明一个道理性能问题往往不是“某段代码写慢了”而是“调度策略和请求特征不匹配”。没有端到端的延迟分解你根本不知道 P95 的锅到底该谁背。6. 性能工程落地把“个体调优”变成“团队能力”6.1 性能平台和可观测性别等事故再找原因AI 性能工程做了几年我最大的感触是“单点英雄”模式在长期维护阶段根本扛不住。今天 A 调好了明天 B 上线一个版本把你调好的参数覆盖了后天模型微调后延迟特征变了你又得重新从头摸一遍。要解决这个问题必须把性能优化沉淀成流程和平台。至少需要两层基础设施一层是监控和 trace能看清每个请求在模型服务内部的耗时分解第二层是自动化的压测和基准回归让每次模型更新或配置变更都有性能数据。有了这两个基础性能优化的效率会完全不一样。像 vLLM 提供/metrics接口输出 TTFT 和 TPOT 这些关键指标很多团队还嫌不够会自己在服务层和网关层再做 trace。要真正定位到“哪个环节拖慢了 P99”没有这种贯穿链路的数据是做不到的。6.2 把性能准入前置到发布流程里我再补充一个很管用的方法论把性能压测前置和 CI 流程绑定。模型或者推理框架一有变化就在一个标准的压测环境里自动跑一套基准把 TTFT、TPOT、吞吐、显存峰值和已有的性能阈值对比不经机械的汇报流程自然地在发版前发现性能退化。这听起来会增加工作量但实际维护了一套压测脚本后纯自动化跑一次也就十几分钟比起线上事故后半夜排查的代价小太多了。具体操作上性能用例要和真实流量特征匹配不能只压一个“固定 prompt 长度”。我会建议你从线上收集一部分真实请求的 prompt 长度分布和并发模式做成回放压测集。这样测出来的数据才不会失真。做性能准入的团队还会维护一个“历史基线库”每轮压测都自动对比偏移超过阈值就直接阻断发布性能出问题的人在发布前就知道而不是等用户来投诉。6.3 我的实操心得一次只改一个变量最后分享一个我自己坚持了很久的小习惯也算给新手的一个建议做性能优化一次只改一个变量。这话听起来特别简单但我见过太多人一次性改了 batch、量化、框架版本、并行策略结果性能有变化却根本分不清是哪个起了作用。我会在本子上记下每次实验的改动点、期望效果、实际数据和基线对比。这种笨办法反而最快因为每次实验的结论都是干净的不会滋生“我明明改了 A 和 B效果反而差了”这种说不清的事故。等实验积累多了你手里就有了一张“什么场景对应什么优化”的决策表后面再做类似任务就能直接抄自己的作业。性能工程是一个没有终点的过程模型在迭代硬件在换代流量在变化你永远不可能调出“完美”的配置。唯一能依靠的就是这套可量化、可复现、可持续迭代的方法。这套方法积累了你的团队、你的系统、你的场景专属数据这才是性能工程真正有价值的资产。
返回列表