1. AI 系统性能工程的核心命题从单点优化到全链路治理做 AI 系统性能工程这件事最怕的就是把它当成单纯的“调参”或者“加机器”。我见过太多团队在模型上线之后发现延迟飙高、吞吐上不去、GPU 利用率长期趴在 30% 以下第一反应就是“模型太大了换个小模型吧”或者“再加两张卡”。这些做法有时候能缓解症状但绝大多数情况下问题根本不在模型本身而在于整个系统的性能链路没有被系统性地拆解过。AI 系统性能工程的核心命题是把一个 AI 应用从数据进入、预处理、推理计算、后处理到结果返回的完整链路当成一个端到端的系统来对待。它跟传统后端性能工程最大的区别在于传统服务的瓶颈通常在 IO 或数据库而 AI 系统的瓶颈可能在任何一个环节——可能是数据预处理阶段的 CPU 解码可能是 GPU 显存带宽可能是推理框架的调度策略也可能是服务化层的排队逻辑。你如果不把整条链路拆开看永远不知道真正的瓶颈在哪里。这篇文章适合三类人看第一类是做 AI 应用开发但总觉得“跑得不够快”的工程师第二类是需要对 AI 服务做容量规划和成本控制的架构师第三类是对性能工程感兴趣、想了解 AI 系统特殊性的后端开发者。不管你现在用的是哪家的大模型、哪个推理框架下面这些思路和方法论都是通用的。2. 性能指标体系怎么建别只看延迟和吞吐2.1 四个核心指标的定义与关系做性能工程的第一步永远是建立指标体系。没有指标优化就是盲人摸象。AI 系统的性能指标比传统服务要复杂一些因为多了 GPU 这个维度。我一般会从四个核心指标入手端到端延迟E2E Latency从请求发出到收到完整响应的总时间。这个指标用户感知最直接但它太粗了出问题的时候没法定位。所以必须拆成首 Token 延迟TTFTTime To First Token和Token 间延迟TPOTTime Per Output Token。对于流式输出的场景TTFT 决定了用户觉得“这个系统反不反应得过来”TPOT 决定了用户觉得“输出流不流畅”。吞吐量Throughput单位时间内系统能处理的请求数或生成的 Token 数。这里有个关键区分——是请求级吞吐还是Token 级吞吐。对于生成式任务Token 级吞吐更能反映系统的真实处理能力因为不同请求的输出长度可能差很多。GPU 利用率包括计算利用率SM Occupancy和显存带宽利用率。很多人只看计算利用率但实际上很多 AI 推理任务是显存带宽瓶颈而不是计算瓶颈。你看到 GPU 计算利用率只有 40%不一定是调度问题可能是数据搬运把带宽吃满了。资源效率比每瓦功耗或每美元成本能产出多少有效 Token。这个指标在规模化部署的时候特别重要因为它直接决定了你的单位经济模型能不能跑通。2.2 指标之间的权衡关系这四个指标不是独立的它们之间存在明确的权衡关系。你压低延迟通常要牺牲吞吐你提高吞吐延迟就会上升。这不是工程做得不好而是排队论的基本规律。我一般会用一个简单的框架来帮团队理清权衡优化目标主要手段代价降低 TTFT减少批处理大小、优先级调度吞吐下降、GPU 利用率降低提高吞吐增大批处理、连续批处理延迟上升、显存压力增大降低 TPOT量化、投机解码、KV Cache 优化精度可能下降、实现复杂度上升提高资源效率动态批处理、弹性伸缩系统复杂度上升、冷启动问题这张表的关键在于没有免费的优化。每次你想动一个指标先想清楚愿意在哪个维度上付出代价。我见过太多人试图同时优化所有指标最后什么也没优化成。2.3 怎么设定合理的性能目标设定性能目标不能拍脑袋。我的做法是分三步第一步确定业务可接受的下限。比如一个对话系统TTFT 超过 2 秒用户就会觉得卡TPOT 超过 100ms 用户就会觉得输出一顿一顿的。这些是从用户体验反推出来的硬约束。第二步测算当前系统的理论上限。根据模型大小、硬件规格、推理框架的效率算出一个理论上的最优值。比如一个 7B 模型在 A100 上做 FP16 推理理论 TTFT 大概在多少这个是可以估算的。第三步在理论值和业务下限之间找一个工程上可达的目标。通常我会把目标定在理论值的 60% 到 70% 左右留出余量给突发流量和系统抖动。注意性能目标一定要跟业务方对齐。我踩过的坑是技术团队自己定了一个很激进的延迟目标结果为了达标把批处理压得很小GPU 利用率惨不忍睹成本翻了三倍业务方根本不买单。性能工程永远是在延迟、吞吐、成本之间找平衡不是单维度刷榜。3. 推理链路的性能拆解从请求进入到结果返回3.1 请求预处理阶段的隐藏开销很多人做性能分析的时候眼睛只盯着 GPU觉得只要 GPU 跑满了就没问题。但实际上请求预处理阶段的开销经常被严重低估。一个典型的 AI 推理请求在进入 GPU 之前要经过这些步骤网络接收、协议解析、鉴权校验、请求排队、Tokenization、输入拼接比如 System Prompt 拼接、Batch 组装。这些步骤看起来都很轻但叠加起来在高压场景下可能占到端到端延迟的 20% 到 40%。我实测过一个案例一个对话服务GPU 推理本身只花了 80ms但端到端延迟是 200ms。用火焰图一查发现 Tokenization 花了 30msSystem Prompt 拼接花了 20ms剩下的时间花在排队和网络传输上。Tokenization 慢是因为用的分词器没有做缓存每次都在重新加载词表。System Prompt 拼接慢是因为每次都在做字符串拼接而没有预编译。这类问题的排查方法很简单在链路的每个关键节点打时间戳算出各阶段的耗时占比。不要靠猜要靠数据。我一般会在网关层、预处理层、推理层、后处理层各打一个时间戳然后聚合分析。3.2 批处理策略的选择与调优批处理是 AI 推理性能优化最核心的手段没有之一。但批处理不是越大越好也不是所有场景都适合。静态批处理是最简单的方案攒够 N 个请求一起送进 GPU。优点是实现简单GPU 利用率高。缺点是延迟不可控——第一个请求可能要等最后一个请求到了才能开始处理。对于延迟敏感的场景静态批处理基本不可用。动态批处理稍微好一点设定一个最大等待时间窗口窗口内到的请求攒成一批。这样延迟有上限但吞吐会受流量波动影响。流量低谷的时候批处理大小上不去GPU 利用率就下来了。连续批处理Continuous Batching是目前生成式模型推理的主流方案。它的核心思想是不等一整批请求全部完成而是每生成一个 Token 就检查有没有请求完成、有没有新请求可以加入。这样 GPU 几乎不会空转吞吐和延迟的平衡也更好。vLLM 和 TensorRT-LLM 都支持这种模式。我选批处理策略的经验是离线批量任务静态批处理批大小拉满追求最大吞吐在线对话服务连续批处理配合优先级队列混合场景分队列处理离线任务用低优先级队列在线任务用高优先级队列批大小的调优有个经验公式从显存容量的 70% 反推最大批大小然后在这个范围内做压测找到延迟和吞吐的拐点。拐点通常在 GPU 计算利用率达到 85% 到 90% 的位置。超过这个点之后延迟会急剧上升而吞吐增长非常有限。3.3 KV Cache 的管理与优化KV Cache 是生成式模型推理的显存大户。以 LLaMA 2 7B 为例FP16 精度下每个 Token 的 KV Cache 大约占 0.5MB。如果并发 32 个请求每个请求平均输出 512 个 Token光 KV Cache 就要占 8GB 显存。这还没算模型权重和中间激活值。KV Cache 的优化手段主要有几个方向PagedAttention是目前最主流的方案vLLM 就是靠这个打出了名气。它的核心思想借鉴了操作系统的虚拟内存分页机制把 KV Cache 切成固定大小的 Block按需分配不要求连续显存。这样显存碎片率大幅降低同样显存能支持的并发数能提升 2 到 4 倍。KV Cache 量化是另一个方向。把 KV Cache 从 FP16 量化到 INT8 甚至 INT4显存占用直接减半或减到四分之一。代价是精度会有轻微下降但在大多数对话场景下用户感知不到。我实测过 INT8 量化困惑度上升不到 0.1但并发能力翻倍。Prefix Caching对 System Prompt 很长的场景特别有用。如果多个请求共享同一段 System Prompt那这段 Prompt 的 KV Cache 可以复用不用每个请求都重新算一遍。这个优化在多轮对话和 Agent 场景下效果非常明显。实操心得KV Cache 的显存分配一定要留余量。我踩过的坑是把显存算得刚刚好结果流量高峰时新请求进不来已经在处理的请求又因为显存不足被中断整个服务雪崩。后来我固定留 15% 到 20% 的显存作为缓冲稳定性好了很多。4. 模型层面的性能优化量化、蒸馏与编译4.1 量化方案的选型与实测对比量化是模型推理加速最直接的手段。但量化的水很深不同方案的效果差异很大。训练后量化PTQ是最常用的方案不需要重新训练直接对训练好的模型做量化。GPTQ、AWQ、SmoothQuant 都是这个路子的。优点是实现简单、成本低缺点是在低比特比如 4bit 以下时精度损失比较明显。量化感知训练QAT是在训练过程中模拟量化误差让模型自己适应量化。精度保持得更好但需要重新训练成本高。一般只在 PTQ 效果不达标的时候才考虑。我实测过几个主流方案在 7B 模型上的表现量化方案比特数显存节省推理加速精度损失困惑度FP16 基线160%1.0x0GPTQ475%2.3x0.3AWQ475%2.5x0.2SmoothQuant850%1.8x0.05INT8 PTQ850%1.9x0.08从表里能看出来4bit 量化的加速比最高但精度损失也最大。8bit 量化是个比较稳妥的选择精度损失小加速比也不错。我的建议是对话类应用优先考虑 8bit对精度不敏感的场景可以上 4bit。4.2 投机解码的适用场景投机解码Speculative Decoding这两年被讨论得很多但很多人对它的适用场景有误解。它的核心思想是用一个小模型Draft Model快速生成多个候选 Token然后用大模型Target Model一次性验证这些 Token 对不对。如果对了就相当于大模型一次生成了多个 Token速度就上去了。但这里有个关键前提Draft Model 的生成速度必须远快于 Target Model而且 Draft 的准确率要足够高。如果 Draft Model 太慢或者猜中的概率太低那投机解码反而会拖慢整体速度。我实测下来的经验是投机解码在输入输出比较确定的场景下效果最好比如代码生成、翻译、摘要。在开放式对话场景下因为下一个 Token 的不确定性太高Draft Model 猜中的概率低加速效果就不明显。另外投机解码会增加显存占用因为要同时加载两个模型。如果显存本来就紧张上投机解码可能得不偿失。4.3 模型编译与图优化模型编译是把推理图做算子融合、内存规划、内核选择的过程。PyTorch 2.0 的torch.compile、TensorRT、ONNX Runtime 都提供了这类能力。图优化能带来的收益主要来自几个方面算子融合减少了内核启动开销和显存读写内存复用降低了峰值显存占用内核自动调优针对具体硬件选择最优实现。我一般会在模型确定之后、上线之前做一轮编译优化。实测下来TensorRT 在 NVIDIA GPU 上的加速比通常在 1.3x 到 2x 之间具体取决于模型结构和输入形状的稳定性。如果输入形状变化很大TensorRT 的优势会打折扣因为它的优化是跟形状绑定的。注意模型编译不是一劳永逸的。每次模型更新、推理框架升级、CUDA 版本变化都需要重新编译和验证。我建议把编译过程纳入 CI/CD 流程每次模型变更自动触发编译和性能回归测试。5. 服务化与调度层的性能工程5.1 推理服务的部署架构选型推理服务的部署架构直接决定了系统的弹性能力和资源效率。常见的架构有三种单模型单服务每个模型独立部署一个服务。优点是隔离性好、故障不扩散缺点是资源利用率低每个服务都要预留峰值资源。多模型共享服务多个模型跑在同一个服务里共享 GPU 资源。优点是资源利用率高缺点是隔离性差一个模型出问题可能影响其他模型。模型路由 弹性伸缩前面加一个路由层根据请求特征路由到不同的模型实例实例数根据负载动态调整。这是目前大规模部署的主流方案兼顾了资源效率和隔离性。我选架构的原则是先看流量特征再看资源预算。流量稳定且模型单一单模型单服务就够了流量波动大或者模型多就上路由加弹性伸缩。5.2 请求队列与优先级管理请求队列的设计对延迟指标影响很大。FIFO 队列最简单但没法区分请求的紧急程度。实际生产环境里我一般会用多级优先级队列实时队列对话类请求延迟敏感优先级最高普通队列批量推理请求延迟不敏感优先级中等后台队列离线任务优先级最低只在系统空闲时处理队列之间要有抢占机制高优先级队列有请求时可以抢占低优先级队列正在使用的资源。但抢占不能太粗暴否则低优先级任务永远做不完。我一般会设置一个最小资源保障确保低优先级队列至少能拿到 10% 到 20% 的资源。队列深度也需要控制。队列太浅突发流量来了直接拒绝请求队列太深请求排队时间过长端到端延迟失控。我的经验值是队列深度设置为系统平均处理能力的 2 到 3 倍超过这个深度的请求直接返回限流响应让客户端重试。5.3 自动扩缩容的策略与陷阱自动扩缩容是控制成本的关键手段但 AI 服务的扩缩容比传统 Web 服务要复杂得多。传统 Web 服务扩容就是加实例启动时间通常几秒到几十秒。AI 推理服务扩容要加载模型权重、初始化 CUDA 上下文、编译推理图启动时间可能几分钟甚至十几分钟。这意味着扩容决策必须提前不能等负载上来了才扩。我的做法是基于预测式扩容 反应式扩容的组合策略预测式扩容根据历史流量模式提前 10 到 15 分钟扩容。比如每天上午 9 点流量开始上升那 8 点 45 分就开始扩容。反应式扩容监控队列深度和 GPU 利用率超过阈值立即触发扩容作为预测式扩容的补充。缩容比扩容更危险。缩容太快流量反弹时来不及扩容缩容太慢资源浪费。我一般会设置一个缩容冷却期比如 15 分钟内负载持续低于阈值才触发缩容而且每次缩容不超过当前实例数的 25%。踩坑记录有一次我设置了一个比较激进的缩容策略结果下午流量突然反弹扩容来不及服务直接过载。后来我把缩容冷却期从 5 分钟调到 20 分钟并且加了缩容前的流量预测检查再也没出过类似问题。6. 性能测试与压测方法论6.1 压测场景的设计原则AI 系统的压测跟传统服务压测有个本质区别请求之间的相互影响更大。因为批处理的存在一个请求的延迟会受到同批次其他请求的影响。所以压测场景的设计要特别小心。我设计压测场景的时候会覆盖这几类稳态压测固定并发数持续跑 10 到 30 分钟观察系统在稳定状态下的延迟和吞吐。这是基线测试用来确定系统的稳态性能。阶梯压测并发数从低到高逐步增加每个阶梯跑 5 分钟观察系统在不同负载下的表现找到性能拐点。突发压测瞬间把并发数拉到峰值的 2 到 3 倍持续 1 到 2 分钟测试系统的抗突发能力。长尾压测模拟少量超长请求比如输出 4096 个 Token和大量短请求混合的场景测试系统在混合负载下的公平性。压测数据要记录完整的延迟分布不能只看平均值。P50、P90、P95、P99 都要看。AI 系统的延迟分布通常是长尾的P99 可能是 P50 的 5 到 10 倍。如果只看平均值会严重低估用户的真实体验。6.2 压测工具的选择与配置压测工具的选择取决于你的协议和场景。如果是 HTTP 接口Locust、wrk、k6 都能用。如果是 gRPC 接口ghz 或者自己写客户端。我比较推荐用Locust因为它是 Python 写的跟 AI 生态比较近自定义压测逻辑很方便。比如你要模拟不同输入长度的请求或者模拟流式输出的消费行为用 Locust 写起来很顺手。压测客户端本身不能成为瓶颈。我见过有人用单机跑压测结果压测机的 CPU 先跑满了测出来的数据根本不准。压测机的配置至少要跟被测服务在一个量级或者用多台压测机分布式压测。另外压测流量要跟生产流量特征对齐。输入长度分布、输出长度分布、请求间隔分布这些都要尽量模拟真实情况。如果压测用的都是短请求测出来的性能会明显好于真实场景。6.3 性能回归与持续监控性能工程不是一次性的工作而是持续的过程。每次模型更新、代码变更、配置调整都可能引入性能回归。所以必须建立性能回归测试和持续监控机制。我的做法是建立性能基线在系统稳定运行的时候记录一组标准压测场景的性能数据作为基线。CI 中集成性能测试每次代码合并前跑一轮轻量级性能测试跟基线对比。如果关键指标退化超过 10%阻止合并。生产环境持续监控用 Prometheus Grafana 监控 TTFT、TPOT、吞吐、GPU 利用率等核心指标设置告警阈值。定期全量压测每周或每两周做一次全量压测更新性能基线发现潜在的性能退化趋势。监控指标要区分症状指标和根因指标。TTFT 和 TPOT 是症状指标用户能感知到GPU 利用率、队列深度、显存占用是根因指标用来定位问题。告警应该主要基于症状指标但排查问题时要看根因指标。7. 常见性能问题与排查实录7.1 延迟突然飙升的排查思路延迟飙升是最常见的性能问题。我的排查顺序是第一步确认影响范围。是所有请求都慢还是特定类型的请求慢是所有实例都慢还是个别实例慢这个信息决定了排查方向。第二步看症状指标的时间线。TTFT 和 TPOT 是同时飙升还是只有其中一个TTFT 飙升通常是排队或预处理问题TPOT 飙升通常是 GPU 计算或显存带宽问题。第三步看根因指标。GPU 利用率、显存占用、队列深度、CPU 利用率哪个指标异常如果 GPU 利用率突然下降可能是遇到了计算瓶颈之外的问题比如显存不足导致频繁换页。第四步看请求特征。是不是突然来了很多超长请求是不是输入长度分布发生了变化批处理系统对请求特征的变化很敏感。我整理了一个速查表症状可能原因排查方法TTFT 飙升TPOT 正常队列积压、预处理慢看队列深度、预处理耗时TTFT 正常TPOT 飙升GPU 计算瓶颈、显存带宽瓶颈看 GPU 利用率、显存带宽两者同时飙升系统过载、资源不足看整体负载、GPU 利用率个别实例慢硬件故障、显存碎片对比正常实例的指标特定请求慢输入过长、特殊字符分析慢请求的特征7.2 显存不足与 OOM 的预防显存不足是 AI 推理服务最常见的故障之一。OOM 一旦发生整个服务可能崩溃影响面很大。预防 OOM 的核心是显存预算管理。我会把显存分成几块模型权重、KV Cache、中间激活值、CUDA 上下文、缓冲余量。每块都设定上限加起来不超过总显存的 85%。KV Cache 的显存管理是最容易出问题的。因为 KV Cache 是动态增长的请求越多、输出越长占用越大。我的做法是设置KV Cache 上限达到上限后新请求排队等待而不是继续分配导致 OOM。另外要监控显存碎片率。长时间运行之后显存碎片可能越来越严重明明总空闲显存够但就是分配不出连续的大块。PagedAttention 能缓解这个问题但也不能完全避免。定期重启服务实例是简单有效的办法我一般会设置每天凌晨低峰期滚动重启。7.3 吞吐上不去的几个隐蔽原因吞吐上不去GPU 利用率也不高这种情况最让人头疼。我遇到过几个比较隐蔽的原因CPU 预处理成为瓶颈。GPU 在等 CPU 做 Tokenization 和 Batch 组装。解决办法是把预处理放到单独的进程池或者用 GPU 加速的分词器。Python GIL 限制。推理服务的调度逻辑如果是 Python 写的GIL 可能成为瓶颈。解决办法是把调度逻辑用 C 重写或者用多进程架构。网络带宽不足。输入输出数据量大的时候网络可能成为瓶颈。特别是多机部署的时候节点间的通信带宽要足够。锁竞争。多个线程竞争同一把锁导致大量时间花在等锁上。用 py-spy 抓一下火焰图就能看出来。日志写入阻塞。同步写日志在高并发下会成为瓶颈。改成异步日志或者降低日志级别。这些问题单看指标都不明显需要结合火焰图和链路追踪才能定位。我建议在性能测试阶段就打开 profiling不要等到生产环境出问题了才去查。8. 成本与性能的平衡单位经济模型8.1 推理成本的构成与计算做性能工程不能只看技术指标还要看成本。一个延迟很低但成本极高的方案在商业上是不可持续的。AI 推理的成本主要由几块构成GPU 租用成本或折旧成本、CPU 和内存成本、网络带宽成本、存储成本。其中 GPU 成本通常占大头70% 到 90% 不等。计算单位成本的时候我一般会算每百万 Token 的成本。公式是每百万 Token 成本 每小时总成本 / 每小时产出 Token 数 × 1,000,000这个指标把性能和成本统一到了一个维度上。优化性能的最终目的就是降低这个数字。举个例子一张 A100 每小时成本按 10 元算如果系统每小时能产出 500 万 Token那每百万 Token 成本就是 2 元。如果把吞吐优化到每小时 1000 万 Token成本就降到 1 元。这就是性能优化的商业价值。8.2 性能优化投入的优先级排序性能优化也是要讲投入产出比的。不是所有优化都值得做。我一般按这个优先级排序第一优先级零成本或低成本优化。比如调整批处理参数、优化队列策略、开启 Prefix Caching。这些优化不需要额外硬件投入改改配置就能见效。第二优先级中等成本优化。比如模型量化、推理框架切换、图编译优化。需要一定的开发投入但不需要额外买卡。第三优先级高成本优化。比如换更好的 GPU、增加机器、自研推理内核。这些需要真金白银的投入只有在前面两轮优化都做完之后才考虑。我见过一些团队一上来就想着换卡结果发现批处理参数都没调对换了卡性能提升也不明显。先把免费的优化做透再考虑花钱的事。8.3 弹性伸缩与成本控制弹性伸缩是控制成本的关键手段但要用好并不容易。扩容要快缩容要慢。扩容慢了会丢请求缩容快了会在流量反弹时措手不及。我一般设置扩容触发阈值低一些比如 GPU 利用率 70% 就扩缩容触发阈值高一些比如 GPU 利用率持续 30 分钟低于 30% 才缩。预留实例和按需实例搭配。流量基线部分用预留实例成本更低峰值部分用按需实例灵活。比例大概是 7:3 到 8:2。利用闲时做离线任务。流量低谷期 GPU 利用率低可以把离线推理任务调度到这些时段提高整体资源效率。这需要有一个统一的任务调度层能区分在线和离线任务。经验分享我在一个项目里通过闲时调度离线任务把整体 GPU 利用率从 35% 提升到了 60%单位 Token 成本降了将近 40%。这个优化的投入只是写了一个调度器没有增加任何硬件。9. 从性能工程到系统可靠性性能工程做到最后一定会跟可靠性工程交汇。因为性能问题往往是可靠性问题的前兆——延迟升高、队列积压、显存不足这些如果不管下一步就是服务不可用。我现在的做法是把性能指标和可靠性指标放在一起看。SLO 里既包含可用性目标比如 99.9% 请求成功也包含性能目标比如 P99 TTFT 小于 2 秒。两者任何一个不达标都触发告警和排查。另外性能工程的经验要沉淀成容量规划模型。根据业务增长预测提前算出需要多少 GPU、多少实例、什么规格的机器。不要等到资源不够了才去申请那时候往往已经影响业务了。容量规划模型的核心参数是峰值 QPS、平均输入长度、平均输出长度、目标延迟、单实例处理能力。这几个参数确定之后所需实例数就能算出来。我一般会留 30% 到 50% 的余量应对突发流量和硬件故障。最后说一个我自己的体会AI 系统性能工程最难的从来不是某个具体的技术点而是建立一套持续观测、持续优化、持续验证的机制。技术方案会过时框架会迭代但方法论和工程习惯是可以长期复用的。把指标建好、把链路拆清楚、把压测做扎实、把成本算明白剩下的就是不断迭代的事了。
阅读完成 · 觉得有帮助?