ARTICLE DETAIL

资讯详情

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

大模型推理优化实战:从显存、调度到解码的分层调优指南

大模型推理优化实战:从显存、调度到解码的分层调优指南 1. 推理优化到底在优化什么大模型推理优化这个词这两年从论文标题一路卷到生产环境周报里。很多人第一次接触会觉得它是个纯算法问题但真到了线上环境你会发现它更像一个系统工程问题显存、带宽、算力、延迟、成本五个变量互相拉扯你动了其中一个另外四个一定会跟着变。先把问题定义清楚。推理优化要解决的核心矛盾只有一个在给定硬件资源下让单位时间内产出的有效 token 尽可能多同时把单个请求的响应时间控制在可接受范围内。注意这里有两个目标吞吐和延迟它们经常是打架的。你把 batch 开大吞吐上去了但单个请求要等更久你把 batch 压小延迟好看了但 GPU 利用率掉下来单位成本飙升。所以真正做推理优化的人脑子里始终有一张权衡表优化目标主要手段代价提升吞吐增大 batch、Continuous Batching单请求延迟上升降低延迟减小 batch、投机解码吞吐下降、额外算力开销降低显存KV Cache 量化、PagedAttention精度损失、实现复杂度上升降低单位成本提高 GPU 利用率、请求调度调度逻辑复杂、尾延迟抖动我见过太多团队一上来就盯着某个热门技术名词猛搞结果发现瓶颈根本不在那儿。推理优化的第一步永远不是选技术而是定位瓶颈。你的瓶颈是显存不够导致 batch 上不去还是算力打满但显存空着还是调度层把请求攒太久这三种情况对应的优化路径完全不同。举个我实际遇到的例子。有个团队反馈说他们的推理服务吞吐上不去QPS 卡在某个值死活不动。他们第一反应是上 Speculative Decoding觉得能加速。结果我一看监控GPU 利用率只有 40%显存倒是快满了。这说明什么说明瓶颈在显存容量不在算力。你上投机解码只会让显存更紧张吞吐反而可能下降。后来他们把 KV Cache 做了量化显存腾出来batch 从 8 提到 32吞吐直接翻了三倍多投机解码根本没用上。这个案例说明一个道理推理优化是分层的每一层的瓶颈和解法都不一样。我习惯把它分成四层来看模型层模型结构、量化、蒸馏、剪枝决定单次前向计算的理论下限显存层KV Cache 管理、PagedAttention、显存复用决定你能同时跑多少请求调度层Continuous Batching、请求优先级、抢占策略决定资源怎么分配给不同请求解码层投机解码、并行采样、beam search 优化决定每个 token 怎么更快地产出这四层是有依赖关系的。模型层没优化好显存层再怎么折腾也白搭显存层没解决调度层根本施展不开。所以正确的顺序是自底向上排查自顶向下优化。先看模型层有没有明显的浪费再看显存层是不是卡住了 batch size然后看调度层有没有把 GPU 喂饱最后才考虑解码层的花活。接下来我会按这个分层逻辑把每一层的核心方法、实操细节、踩坑经验都拆开讲。每个方法我都会说清楚它解决什么问题、在什么场景下用、参数怎么调、有什么坑。你可以对照自己的监控数据找到对应的层去下手。2. 显存层优化KV Cache 是主战场2.1 KV Cache 为什么是显存杀手要理解 KV Cache 优化得先算清楚它到底占多少显存。Transformer 解码时每生成一个 token都需要用到之前所有 token 的 Key 和 Value 向量。如果每次都重新算计算量是 O(n²)所以工程上会把历史 K、V 缓存下来这就是 KV Cache。它的显存占用公式是KV Cache 显存 2 × batch_size × seq_len × num_layers × num_heads × head_dim × dtype_bytes拿一个 7B 模型举例假设 32 层、32 个 head、head_dim 128、FP16 存储2 × 1 × 2048 × 32 × 32 × 128 × 2 bytes ≈ 1.07 GB这是单条 2048 长度请求的 KV Cache。如果你 batch 开到 32直接就是 34 GB。而 7B 模型本身的权重才 14 GBFP16。KV Cache 比模型权重还大这就是为什么很多服务显存不够用。更麻烦的是KV Cache 是动态增长的。请求进来时你不知道它要生成多长只能按最大长度预留。传统做法是按 max_seq_len 预分配结果短请求浪费大量显存。这就是 PagedAttention 要解决的问题。2.2 PagedAttention 的分页思路PagedAttention 的核心思想借鉴了操作系统的虚拟内存分页。它把 KV Cache 切成固定大小的 block每个 block 存固定数量 token 的 K、V。请求的 KV Cache 不再是一块连续内存而是一张 block 表按需分配。这样做的好处有三个消除内部碎片短请求只占它实际需要的 block不会浪费支持共享多个请求如果有相同前缀比如相同的 system prompt可以共享 block便于调度block 可以非连续存放显存分配更灵活实测下来PagedAttention 能把显存利用率从 20%-40% 提升到 80% 以上。这意味着同样的显存你能塞进去的 batch 大了两三倍吞吐自然就上去了。但 PagedAttention 不是没有代价。block 表的管理有额外开销block size 选小了管理开销大选大了碎片又多。我一般建议 block_size 设成 16 或 32这是实践下来比较平衡的值。另外PagedAttention 对 attention kernel 的实现有要求不是所有推理框架都支持得好选框架时要确认这一点。2.3 KV Cache 量化的精度与收益权衡如果 PagedAttention 还不够下一步就是量化 KV Cache。把 FP16 的 K、V 压成 INT8 甚至 INT4显存直接减半或减到四分之一。但这里有个关键问题KV Cache 量化对精度的影响比权重量化大。因为 K、V 是动态计算的分布随输入变化量化误差会累积到后续所有 token 的生成中。我实测过INT8 量化在大多数任务上精度损失可以接受困惑度上升 1%-3%但 INT4 就明显了长文本生成容易出现重复和逻辑断裂。具体操作上KV Cache 量化一般用 per-channel 或 per-token 的 scale而不是全局 scale。因为不同 head、不同 token 的 K、V 分布差异很大全局 scale 会把小值压没。实现时要注意量化 scale 要在线校准不能只用校准集定死K 和 V 可以分开量化V 对精度更敏感可以保留更高精度量化后 attention 的 softmax 要小心数值稳定性提示KV Cache 量化不是免费的午餐。如果你的任务对精度要求极高比如代码生成、数学推理建议先做 INT8 试试INT4 要谨慎评估。2.4 显存层的实操检查清单在动手优化显存之前先跑一遍这个检查清单确认瓶颈真的在显存用nvidia-smi或框架自带监控看显存占用曲线是稳定高位还是锯齿状看 batch size 是不是被显存卡住了尝试手动调大看会不会 OOM算一下理论 KV Cache 占用和实际占用对比看有没有浪费检查是否有请求长度分布极不均匀的情况短请求被长请求拖累如果确认是显存瓶颈优化顺序建议是先上 PagedAttention再考虑 KV Cache 量化最后才是模型层量化。因为 PagedAttention 是无损的量化是有损的能无损解决就别用有损的。3. 调度层优化Continuous Batching 怎么把 GPU 喂饱3.1 静态 Batching 的致命缺陷早期推理服务用的是静态 batching攒一批请求一起送进模型等这批全部生成完再收下一批。这个模式有个致命问题木桶效应。一批里只要有一个请求要生成 2000 个 token其他请求哪怕 10 个 token 就结束了也得等着。结果就是 GPU 在大部分时间里都在为少数长请求空转利用率极低。我见过最夸张的情况静态 batching 下 GPU 利用率只有 15%因为 batch 里总有一两个超长请求拖后腿。3.2 Continuous Batching 的迭代级调度Continuous Batching 的思路是不等整批结束而是每个解码步都重新组批。某个请求生成完了立刻把它踢出去腾出的位置马上塞新请求进来。这样 GPU 每个解码步都是满的利用率能拉到 70% 以上。具体实现上调度器维护一个 running 队列和一个 waiting 队列。每个解码步检查 running 队列里有没有请求结束有就移出从 waiting 队列按优先级取请求填充到 running 队列对 running 队列里的所有请求做一次前向计算更新每个请求的 KV Cache 和状态这个逻辑听起来简单但工程上有几个难点KV Cache 要能动态增删这就是为什么 Continuous Batching 通常和 PagedAttention 一起用分页管理才能支持动态增删attention mask 要动态构造每个请求的序列长度不同mask 要按实际长度生成采样要按请求独立进行不同请求的 temperature、top_p 可能不同不能混在一起算3.3 调度策略的参数调优Continuous Batching 有几个关键参数需要调参数作用建议值max_batch_size单批最大请求数根据显存和延迟要求定一般 32-256max_tokens_per_batch单批最大 token 数控制计算量防止长请求挤占scheduling_policy调度策略FIFO 或优先级看业务需求preemption_mode抢占模式recompute 或 swap看显存压力我重点说下抢占。当显存不够时调度器需要把某些请求的 KV Cache 换出去腾地方给新请求。有两种做法Recompute直接丢掉被抢占请求的 KV Cache等它恢复时重新算。省显存但费算力。Swap把 KV Cache 换到 CPU 内存恢复时再换回来。省算力但费带宽。怎么选看你的瓶颈。如果算力紧张、显存相对宽裕用 swap如果显存紧张、算力有富余用 recompute。实测下来大多数场景 recompute 更划算因为 PCIe 带宽往往是瓶颈。3.4 调度层的避坑经验说几个我在调度层踩过的坑坑一请求长度分布极端不均。如果 90% 的请求都很短10% 的请求超长Continuous Batching 的效果会打折扣。因为长请求会长期占用 batch 位置。解法是给长请求单独设一个队列或者用 max_tokens_per_batch 限制单批计算量。坑二优先级反转。高优先级请求进来时如果 running 队列满了需要抢占低优先级请求。但如果低优先级请求已经生成了很多 token抢占它的代价很大。解法是设置抢占阈值只抢占生成 token 数少的请求。坑三调度开销随 batch 增大而上升。每个解码步都要做一次调度决策batch 越大调度逻辑越复杂。如果调度开销超过了计算节省就得不偿失。实测 batch size 超过 256 后调度开销开始明显需要优化调度器实现。提示Continuous Batching 不是银弹。如果你的请求都是短请求且长度均匀静态 batching 可能更简单高效。先分析你的请求分布再决定。4. 解码层优化Speculative Decoding 的适用边界4.1 投机解码的基本原理Speculative Decoding 的核心思想是用一个小模型快速草拟多个 token再用大模型一次性验证。如果小模型草拟的 token 大模型都认可那就相当于一次前向计算生成了多个 token速度提升。具体流程小模型draft model自回归生成 K 个 token大模型target model对这 K 个 token 做一次并行前向得到每个位置的概率分布按接受率规则逐个验证接受前缀拒绝后缀被拒绝的位置用大模型的分布重新采样一个 token关键参数是 K即草拟长度。K 太小加速有限K 太大拒绝率高浪费算力。一般 K 取 4-8 比较合适。4.2 加速比的计算与预期投机解码的理论加速比取决于两个因素接受率 α和草拟长度 K。期望生成 token 数是E[tokens] (1 - α^(K1)) / (1 - α)如果 α 0.8K 5期望生成约 3.4 个 token。也就是说一次大模型前向能出 3.4 个 token理论加速 3.4 倍。但实际加速要打折扣因为小模型的前向也有开销。实测下来7B 模型配 1B draft model加速比大概在 1.8-2.5 倍。如果 draft model 太大开销吃掉收益太小接受率低。draft model 和 target model 的参数量比在 1:7 到 1:10 之间比较合适。4.3 什么场景不适合投机解码投机解码不是万能的以下场景要谨慎batch size 已经很大投机解码主要省的是解码步数当 batch 很大时GPU 已经打满省步数不省时间任务对精度极敏感虽然理论上投机解码不改变输出分布但实现细节可能导致微小偏差draft model 和 target model 分布差异大接受率低加速效果差显存紧张draft model 也要占显存可能挤占 batch size我个人的经验是投机解码适合低 batch、低延迟场景比如单用户对话、代码补全。高吞吐场景下Continuous Batching 的收益更直接。4.4 投机解码的替代方案如果投机解码不适合你的场景还有几个替代思路Medusa在 target model 上加多个解码头并行预测多个未来 token不需要单独的 draft modelLookahead Decoding用 Jacobi 迭代并行生成多个 token不需要 draft modelPrompt Lookup Decoding如果输入和输出有大量重复比如摘要任务直接从输入里找候选 token这些方法的共同思路都是打破自回归的串行依赖用并行计算换串行步数。选哪个看你的具体场景和框架支持。5. 吞吐优化的整体调优流程5.1 从监控数据定位瓶颈优化之前先建监控。需要看的指标指标含义瓶颈信号GPU 利用率算力使用率低于 60% 说明没喂饱显存占用显存使用量接近上限说明显存瓶颈batch size实际批大小远小于设定值说明调度有问题首 token 延迟TTFT高说明 prefill 慢或排队久每 token 延迟TPOT高说明解码慢请求队列长度等待请求数持续增长说明吞吐不足先看 GPU 利用率和显存占用这两个。如果 GPU 利用率低但显存满是显存瓶颈去优化 KV Cache。如果 GPU 利用率高但吞吐还是上不去是算力瓶颈考虑量化或投机解码。如果两个都不高是调度瓶颈检查 Continuous Batching 配置。5.2 分阶段优化路线图我一般建议按这个顺序推进第一阶段无损优化上 PagedAttention提升显存利用率上 Continuous Batching提升 GPU 利用率调优 batch size 和调度参数这一阶段不需要改模型风险低收益大。大多数团队做完这一步吞吐能提升 3-5 倍。第二阶段有损但可控的优化KV Cache INT8 量化权重量化GPTQ、AWQ评估精度损失是否可接受这一阶段需要做精度评估建议在业务指标上验证不只看困惑度。第三阶段架构级优化投机解码模型蒸馏或换更小的模型多模型路由简单请求走小模型复杂请求走大模型这一阶段改动大需要更多工程投入但收益也更大。5.3 成本与性能的平衡最后说下成本。推理优化的终极目标不是性能最高而是单位成本最低。有时候性能提升 20% 但成本增加 50%就不划算。算成本要看两个数每百万 token 的 GPU 成本和每百万 token 的电费。前者取决于 GPU 利用率和实例价格后者取决于功耗。优化时要把这两个都算进去。举个例子投机解码能降低延迟但需要额外部署 draft modelGPU 成本可能上升。如果你的业务对延迟不敏感就不值得。反过来如果延迟直接影响用户体验和留存那多花的成本就是值得的。提示优化前先定义清楚你的目标函数。是最大化吞吐、最小化延迟、还是最小化成本目标不同优化路径完全不同。6. 常见问题与排查技巧实录6.1 吞吐上不去的排查顺序遇到吞吐上不去按这个顺序查看 GPU 利用率低于 60% 继续查高于 80% 说明算力瓶颈看显存占用接近上限说明显存瓶颈batch 上不去看 batch size实际 batch 远小于设定值查调度器看请求长度分布长尾请求拖累考虑分离队列看框架配置确认 PagedAttention 和 Continuous Batching 都开了6.2 精度下降的归因方法量化或投机解码后精度下降怎么定位原因对比困惑度在标准数据集上对比看上升幅度分任务评估不同任务对量化敏感度不同分开看逐层分析如果是权重量化看哪些层敏感对这些层保留高精度长文本测试量化误差在长文本上累积更明显重点测6.3 显存 OOM 的应急处理线上 OOM 时按这个优先级处理降低 max_batch_size先保服务可用开启 KV Cache 量化腾显存开启抢占让调度器动态管理限制单请求最大长度防止超长请求6.4 常见问题速查表问题可能原因排查方法解决方向吞吐低GPU 没喂饱看利用率调 batch、开 Continuous Batching延迟高batch 太大或排队久看队列长度减小 batch、加实例OOMKV Cache 太大算理论占用量化、PagedAttention、限长精度降量化误差对比困惑度调量化粒度、混合精度加速不明显投机接受率低统计接受率换 draft model、调 K6.5 几个反直觉的经验最后分享几个我踩坑得来的反直觉经验经验一batch size 不是越大越好。超过某个点后吞吐不再增长延迟却线性上升。找到那个拐点停在拐点前一点。经验二量化不一定省时间。INT8 量化省显存但反量化有开销算力紧张时可能更慢。要实测。经验三投机解码在 batch 大时可能负优化。batch 大时 GPU 已经打满投机解码的额外计算反而拖慢。经验四调度器的开销容易被忽略。Python 实现的调度器在高 QPS 下会成为瓶颈考虑用 C 或 Rust 重写关键路径。经验五prefill 和 decode 要分开优化。prefill 是计算密集型decode 是访存密集型瓶颈不同优化手段也不同。有些框架支持 prefill 和 decode 分离部署值得考虑。这些经验没有一条是看文档能学到的都是实际跑出来、踩出来、调出来的。推理优化这个领域理论是一回事工程是另一回事。多跑 benchmark多看监控多试参数比看一百篇论文都管用。
返回列表