ARTICLE DETAIL

资讯详情

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

不换硬件,推理成本降60%:量化、蒸馏、批处理与提示词缓存实战

不换硬件,推理成本降60%:量化、蒸馏、批处理与提示词缓存实战 推理成本这件事很多团队都是在收到第一张云账单之后才开始认真对待的。模型上线跑得好好的延迟也还能接受结果月底一看账单GPU 实例的费用比预期翻了两三倍。这时候第一反应往往是要不要换更便宜的卡或者要不要上更小的模型但真动手之后发现换硬件意味着重新适配、重新压测、重新验证精度周期长、风险大而且很多时候根本解决不了问题——因为成本高的根源不在硬件而在推理链路的组织方式。Chip Huyen 关于降低推理成本的那套思路核心观点其实很朴素在动硬件之前先把软件层面能榨的空间榨干。量化、蒸馏、批处理、提示词缓存这几件事单独拎出来都不新鲜但真正把它们组合起来、按正确的顺序落地能带来的成本下降幅度往往超出预期。我自己的经验是一个没有做过任何优化的推理服务经过系统性的软件层优化之后单位 token 成本降到原来的三分之一甚至更低是完全可能的而且精度损失控制在可接受范围内。这篇内容适合几类人看一是正在为推理账单发愁、但又不想大动干戈换硬件的工程团队二是刚接触模型部署、想搞清楚量化到底损不损精度批处理为什么有时候反而更慢这类问题的开发者三是需要在有限预算下把模型服务跑起来的小团队。我会把每个手段的原理、适用边界、实操中容易踩的坑都讲清楚尽量让你看完就能判断自己的场景该先动哪一刀。1. 先搞清楚钱到底花在哪推理成本的构成拆解在动手优化之前如果不先弄清楚成本的结构很容易把力气用错地方。我见过不少团队一上来就冲着量化去结果发现瓶颈其实在别的地方白折腾一场。1.1 推理成本不只是 GPU 小时单价很多人算推理成本的时候脑子里只有一个数字GPU 实例每小时的租用价格。但实际上真实的单位推理成本应该这样拆单位 token 成本 (硬件小时成本 × 占用时长) / 有效产出 token 数这个公式里有两个变量可以动一个是分子里的硬件成本和占用时长另一个是分母里的有效产出。换硬件动的是分子里的单价但如果你能把分母做大——也就是让同样的硬件在同样时间里产出更多有效 token——效果是一样的而且往往风险更小。分母做大有两条路一是提高吞吐单位时间处理更多请求二是减少浪费别让 GPU 空转、别重复计算。批处理主要作用于前者提示词缓存主要作用于后者。这两件事都不需要换硬件。还有一个容易被忽略的隐性成本显存占用。显存不够的时候你要么降 batch size 导致吞吐上不去要么换更大显存的卡导致单价上升。量化在这里的价值就体现出来了——它同时降低了显存占用和计算量等于一箭双雕。1.2 你的瓶颈是算力、显存还是调度优化之前必须先定位瓶颈否则就是瞎猜。我一般用下面这个判断流程现象大概率瓶颈优先手段GPU 利用率长期低于 50%调度/批处理批处理、请求排队优化GPU 利用率高但吞吐上不去算力量化、蒸馏显存吃紧、batch size 上不去显存量化、KV cache 优化大量重复前缀的请求重复计算提示词缓存单请求延迟要求极高无法批处理蒸馏小模型、缓存这张表是我自己在排查时反复用的基本上能覆盖大部分场景。关键是要先测别凭感觉。用nvidia-smi看 GPU 利用率用 profiling 工具看每个环节的耗时占比把数据拿到手再决定动哪里。提示GPU 利用率低不一定是坏事如果延迟要求高、请求稀疏低利用率是正常的。这时候硬上批处理反而会增加延迟得不偿失。要先明确你的服务是吞吐优先还是延迟优先。1.3 一个真实的成本对比场景举个我实际遇到的例子。一个中等规模的对话服务原来用单请求串行处理A100 上跑一个 7B 模型GPU 利用率只有 30% 左右每天处理 50 万次请求每次平均输入 500 token、输出 200 token。优化前GPU 利用率 30%单位时间产出低成本居高不下。做了三件事之后第一引入动态批处理把并发请求攒起来一起算GPU 利用率提到 75%第二对模型做 INT8 量化显存占用降了一半batch size 能开更大第三对系统提示词做缓存因为大量请求共享同一段前缀这部分计算直接省掉。结果同样的硬件日处理能力从 50 万次提到 130 万次左右单位请求成本降到原来的 40% 上下。整个过程没有换任何硬件。这个例子的意义不是说每个人都能降这么多而是说明软件层的空间往往比想象中大。下面几节我会把每个手段拆开讲。2. 量化把权重从 FP16 压到 INT8 到底损失了什么量化是这几个手段里最立竿见影的一个也是最容易被误解的一个。很多人一听量化就担心精度崩掉其实关键不在于要不要量化而在于怎么量化、量化到什么程度。2.1 量化的本质是降低数值精度模型权重默认一般是 FP16 或 BF16每个参数占 2 字节。量化到 INT8每个参数占 1 字节显存直接减半量化到 INT4再减半。同时整数运算在支持它的硬件上比浮点运算快计算量也下来了。但精度降低必然带来信息损失。问题的核心是哪些参数对精度敏感哪些不敏感。如果一刀切地把所有参数都压到同一个低精度敏感的那部分就会出问题。所以好的量化方案都是分层处理的。常见的量化粒度有三种per-tensor整个张量用一个缩放因子最省事但最粗糙per-channel每个通道一个缩放因子精度好很多per-group每小组参数一个缩放因子精度最好但开销略大实操中per-channel 和 per-group 是主流选择per-tensor 基本只在极端追求速度的场景用。2.2 训练后量化与量化感知训练的区别这两条路经常被混淆我分开说。训练后量化PTQ模型已经训练好了直接拿来做量化校准。做法是准备一批有代表性的校准数据几百到几千条就够让模型跑一遍统计每层激活值的分布据此确定缩放因子。优点是快、不需要重新训练缺点是精度损失相对大一些尤其是低比特INT4 以下时。量化感知训练QAT在训练过程中就模拟量化的误差让模型学会适应低精度。精度损失最小但需要重新训练成本高。一般只在 PTQ 精度不达标、又必须用极低比特时才考虑。对绝大多数推理降本场景PTQ 就够了。我自己的经验是7B 以上的模型做 INT8 PTQ精度损失通常在 1% 以内很多任务上甚至测不出来差异。INT4 就要谨慎一些需要针对具体任务验证。2.3 实操用常见工具做 INT8 量化以 ONNX Runtime 的量化为例大致流程是这样from onnxruntime.quantization import quantize_dynamic, QuantType quantize_dynamic( model_inputmodel_fp16.onnx, model_outputmodel_int8.onnx, weight_typeQuantType.QInt8, per_channelTrue, # 开启 per-channel精度更好 reduce_rangeFalse, # 视硬件而定某些老卡需要 True )几个关键点per_channelTrue强烈建议开启精度提升明显开销可接受reduce_range在支持 INT8 完整范围的硬件上设 False老硬件上设 True 避免溢出动态量化dynamic只量化权重激活值运行时量化适合 Transformer 类模型静态量化static需要校准数据精度更好但流程更复杂量化完之后一定要做精度验证别只看模型能不能跑起来。准备一个覆盖你实际任务的评测集对比量化前后的输出差异。我一般会看两个指标任务准确率的变化以及输出文本的困惑度perplexity变化。前者看业务影响后者看模型整体健康度。2.4 量化踩过的坑说几个我自己踩过的坑一校准数据不具代表性。有次偷懒用了几十条通用语料做校准结果量化后模型在特定领域的表现明显下降。后来换成从真实请求里采样的数据问题就没了。校准数据一定要贴近实际输入分布。坑二忽略某些层的敏感性。第一层和最后一层往往对精度更敏感有些量化工具支持把这些层排除在外保持 FP16精度会好不少。如果量化后精度不达标可以先试试这个。坑三以为量化一定更快。在有些硬件上INT8 的算子支持不完善反而要走 fallback 路径速度不升反降。量化前先确认目标硬件的 INT8 支持情况跑个 benchmark 再决定。注意量化不是免费的午餐。它省的是显存和算力代价是精度和一定的工程复杂度。如果你的服务本来就没到成本瓶颈没必要为了量化而量化。3. 蒸馏用大模型教出一个小模型如果说量化是把同一个模型压小那蒸馏就是重新造一个更小的模型。两者解决的问题不同量化主要省显存和算力蒸馏则是从模型规模上直接降维。3.1 蒸馏的核心是软标签知识蒸馏的基本框架是 teacher-student一个大模型teacher把自己的知识传给学生模型student。关键在于传什么。传统训练用的是硬标签hard label比如分类任务里就是这个样本属于类别 A。但 teacher 模型输出的其实是一个概率分布比如70% 是 A20% 是 B10% 是 C。这个分布包含了类别之间的相对关系信息叫做软标签soft label。学生模型学软标签能学到比硬标签更多的信息。用温度参数 T 来控制软标签的软硬程度import torch import torch.nn.functional as F def distillation_loss(student_logits, teacher_logits, labels, T4.0, alpha0.7): # 软标签损失学生模仿教师的概率分布 soft_loss F.kl_div( F.log_softmax(student_logits / T, dim-1), F.softmax(teacher_logits / T, dim-1), reductionbatchmean ) * (T * T) # 硬标签损失学生也要学真实标签 hard_loss F.cross_entropy(student_logits, labels) # 加权组合 return alpha * soft_loss (1 - alpha) * hard_lossT 越大分布越平滑类别间的相对信息越明显T 太小就退化成硬标签了。alpha 控制软硬损失的权重一般 0.5 到 0.9 之间。3.2 蒸馏不只是分类任务的事上面是分类任务的经典做法但现在大模型场景下蒸馏更多是生成任务的蒸馏。这时候软标签不好直接定义常见做法有几种输出蒸馏让学生模型直接模仿 teacher 生成的文本用标准的语言建模损失训练logits 蒸馏如果 student 和 teacher 词表一致可以直接对齐两者的输出分布特征蒸馏对齐中间层的表示让学生学到 teacher 的内部表征思维链蒸馏让 teacher 生成带推理过程的答案学生学这个过程而不只是学最终答案最后一种在推理能力迁移上效果特别好。teacher 把怎么想的过程展示出来学生学到的就不只是答案而是解题方法。3.3 蒸馏的适用边界蒸馏不是万能的有几个前提第一你得有个足够强的 teacher。如果 teacher 本身就不行蒸馏出来的 student 更不行。teacher 的质量决定了 student 的上限。第二任务要相对聚焦。蒸馏适合把大模型在某个特定任务上的能力压缩到小模型。如果你想让学生模型具备 teacher 的全部通用能力那基本做不到——小模型的容量摆在那里。第三要有足够的训练数据。蒸馏需要大量样本让 teacher 打标签数据不够的话效果会打折。我自己的判断标准是如果你的场景是单一或少数几个明确任务且对延迟/成本敏感蒸馏非常值得做如果是开放式通用对话蒸馏的性价比就要打问号了。3.4 蒸馏和量化的组合拳这两者不冲突可以叠加。常见做法是先用蒸馏把模型规模降下来比如从 70B 蒸到 7B再对 7B 做量化INT8 或 INT4。这样规模降一次、精度降一次最终模型可能只有原来的几十分之一大但精度还能保持可用。不过要注意叠加的精度损失是累积的。蒸馏损失一点量化再损失一点最后可能就超出可接受范围了。所以每做一步都要验证别一口气全上完再看结果。4. 批处理让 GPU 别再空转批处理是提升吞吐最直接的手段但也是最容易做错的手段。做对了吞吐翻倍做错了延迟爆炸。4.1 静态批处理和动态批处理的取舍静态批处理固定 batch size攒够一批再一起算。实现简单但问题是如果请求到达不均匀要么等很久才攒够延迟高要么攒不够就发利用率低。动态批处理不固定 batch size设定一个时间窗口比如 10ms窗口内到达的请求一起算窗口结束就发。这样既能保证一定的批大小又能控制延迟上限。实际生产里基本都是动态批处理。核心参数是最大等待时间和最大 batch size两者共同决定延迟和吞吐的平衡点。4.2 连续批处理才是大模型场景的正解传统批处理有个问题一批请求必须全部算完才能释放如果其中有个请求输出特别长整批都得等它。在大模型生成场景下这个问题特别严重因为输出长度差异很大。连续批处理continuous batching解决了这个问题不等整批结束某个请求一生成完就立刻移出腾出的位置马上填入新请求。这样 GPU 几乎不会因为等待长请求而空转。vLLM、TensorRT-LLM 这些推理框架都内置了连续批处理。如果你还在用朴素的批处理切换到这些框架带来的吞吐提升往往是最明显的。4.3 批处理参数怎么调给一组我常用的起点参数你可以据此微调参数建议起点调整方向最大 batch size显存允许的 70%显存够就往上加最大等待时间10-50ms延迟敏感就调小KV cache 显存占比总显存的 60-80%吞吐优先就调大最大序列长度按实际 P99 设别盲目设很大调参的核心逻辑是先保证延迟达标再在延迟约束下最大化吞吐。别一上来就追求最大吞吐把延迟搞崩了用户体验就没了。4.4 批处理的坑坑一以为 batch 越大越好。batch 太大时单次前向的计算时间线性增长但吞吐的边际收益递减而且延迟会明显上升。存在一个最优点要实测找。坑二忽略 padding 浪费。同一批里序列长度差异大时短的会被 padding 到最长浪费算力。用长度分桶length bucketing或者连续批处理能缓解。坑三请求排队策略没设计好。先进先出不一定最优长请求会拖累短请求。可以考虑按预估输出长度调度或者给短请求更高优先级。提示批处理的效果高度依赖请求模式。如果你的请求本来就稀疏且延迟敏感批处理帮不上忙这时候应该把精力放在缓存和蒸馏上。5. 提示词缓存省下那些重复计算提示词缓存是我认为性价比最高、最容易被忽略的一个手段。很多服务的请求里有大量重复的前缀——系统提示词、few-shot 示例、固定的上下文——这些每次都重新算一遍纯属浪费。5.1 缓存的是什么KV CacheTransformer 推理时每个 token 都要和前面所有 token 做注意力计算。为了不重复算会把前面 token 的 Key 和 Value 缓存起来这就是 KV cache。提示词缓存本质上就是把公共前缀的 KV cache 存下来复用。如果两个请求共享同一段前缀那么这段前缀的 KV cache 只需要算一次后续请求直接读取即可。对于系统提示词很长比如几千 token的场景这个节省非常可观。5.2 前缀缓存和语义缓存的区别这两种缓存经常被混为一谈其实完全不同前缀缓存prefix caching精确匹配前缀。两个请求的前缀 token 序列完全一致才能命中。实现简单、命中率高对于固定系统提示词的场景是主流方案。语义缓存semantic caching对请求做语义匹配意思相近就返回缓存结果。命中率更高但实现复杂而且有返回错误结果的风险需要谨慎使用。生产环境里前缀缓存是标配语义缓存要视场景而定。对于问答类、客服类这种问题重复度高的场景语义缓存价值大对于创意生成类语义缓存基本没用。5.3 实操前缀缓存的落地要点以 vLLM 为例开启前缀缓存很简单from vllm import LLM, SamplingParams llm LLM( modelyour-model, enable_prefix_cachingTrue, # 开启前缀缓存 gpu_memory_utilization0.9, )但开启只是第一步真正要发挥效果请求的组织方式很关键把固定不变的内容系统提示词、示例放在最前面变化的内容放后面多个请求尽量共享相同的前缀结构避免在前缀里插入时间戳、随机 ID 这类每次都变的内容否则永远命中不了我见过一个团队系统提示词里带了个当前时间结果前缀缓存命中率几乎为零。把时间挪到用户消息里之后命中率直接上去了。5.4 缓存的失效和一致性缓存不是一劳永逸的要注意几个问题缓存容量有限。KV cache 占显存缓存太多会挤占推理空间。需要设置合理的缓存大小和淘汰策略一般是 LRU。模型更新后缓存要清空。换了模型权重旧的 KV cache 就失效了必须清掉否则结果会错乱。多轮对话的缓存管理。多轮对话里每一轮的前缀都在变长缓存要能复用上一轮的结果。这需要框架支持增量缓存不是所有框架都做得好。6. 组合策略按什么顺序动刀单独讲完每个手段最后说说怎么组合。顺序错了效果会大打折扣。6.1 先测量再优化这是最重要的一条。别凭感觉决定优化什么先用 profiling 工具把瓶颈找出来。我一般会看这几个数据GPU 利用率nvidia-smi或框架自带监控每个请求的输入/输出 token 分布请求到达的时间分布是否均匀前缀重复率有多少请求共享前缀各环节耗时占比预处理、推理、后处理这些数据拿到手该优化什么一目了然。6.2 推荐的优化顺序根据我的经验性价比从高到低的顺序大致是提示词缓存改动最小收益立竿见影尤其是系统提示词长的场景连续批处理换个推理框架的事吞吐提升明显量化显存和算力双降但要做精度验证蒸馏需要训练成本最高但降本幅度也最大前两个基本是配置级的改动一两天就能上线量化需要一两周做验证蒸馏是项目级的周期以月计。6.3 一个完整的优化案例回到开头那个例子完整走一遍第一步测量。发现 GPU 利用率 30%前缀重复率高系统提示词 800 token占输入的一大半请求到达相对均匀。第二步上提示词缓存。把系统提示词固定下来开启前缀缓存。这一步就把有效计算量降了约 40%。第三步上连续批处理。换用支持连续批处理的推理框架GPU 利用率从 30% 提到 60%。第四步量化。INT8 量化后显存减半batch size 能开更大利用率进一步提到 75%。第五步验证。跑评测集确认精度损失在 1% 以内上线。最终单位成本降到原来的 40% 左右全程没换硬件。6.4 什么时候该考虑换硬件软件优化不是无限的。当你把上面这些都做到位GPU 利用率稳定在 80% 以上延迟也压到了极限成本还是降不下来那说明确实到了硬件的天花板。这时候再考虑换更高效的卡或者用专门的推理加速硬件。但我的经验是大部分团队根本到不了这一步。软件层的空间远比想象中大先把这些榨干再说。7. 几个容易被忽略的细节最后补充几个实操中容易忽略、但影响不小的点。7.1 输入输出的长度控制输出 token 是成本的大头因为它是逐个生成的。控制输出长度能直接降本。方法包括在提示词里明确要求简洁回答、设置 max_tokens 上限、对超长输出做截断或摘要。输入侧也一样别把无关的上下文一股脑塞进去。RAG 场景里检索回来的文档要精挑细选塞太多反而增加成本还降低质量。7.2 监控和持续优化成本优化不是一次性的。请求模式会变、模型会更新、流量会增长需要持续监控。建议把单位 token 成本、GPU 利用率、缓存命中率这些指标做成看板定期 review。我一般会设几个告警GPU 利用率突然下降可能有异常请求、缓存命中率下降可能前缀结构变了、单位成本上升可能哪里退化了。7.3 精度和成本的平衡点最后说个心态问题。降本不是越低越好要在精度和成本之间找平衡。我的做法是先确定精度底线比如任务准确率不能低于某个值然后在这个约束下尽量降本。别为了省成本把精度搞崩了那样省下的钱还不够赔用户体验的。具体操作上每做一步优化都跑一遍评测记录精度和成本的变化画出一条曲线找到那个再降本精度就明显掉的拐点停在拐点之前一点的位置。这套方法我在几个项目里反复用过效果稳定。核心就一句话先测量、再优化、每步验证、持续监控。硬件是最后的手段不是第一手段。
返回列表