LLM推理服务从原理到实战:KV Cache、连续批处理与量化调优 1. 大模型推理到底在做什么从一次对话说起你每次在聊天框里敲下一句话按下回车几秒钟后屏幕上开始一个字一个字往外蹦——这个过程背后跑的就是LLM inference也就是大模型推理。很多人把它和训练混为一谈其实两者完全是两码事。训练是让模型从海量文本里学规律动辄几千张卡跑几个月推理是拿着已经训好的权重根据你给的输入算出下一个词该是什么然后一个词一个词往外吐。前者是上学后者是考试答题。我刚开始接触这块的时候以为推理就是把模型跑起来这么简单。真正上手才发现一个能聊天的demo和一个能扛住线上流量的推理服务中间隔着的工程量比想象中大得多。你要考虑显存够不够、首字延迟能不能压到一秒以内、并发上来之后吞吐会不会崩、batch怎么组、KV Cache怎么管、量化到什么精度还不掉点……这些问题没有一个能靠调个库糊弄过去。这篇内容适合三类人看一是刚入门想搞清楚推理链路上到底有哪些环节的工程师二是已经在做推理服务、但总觉得性能上不去想系统梳理一遍的开发者三是做技术选型、需要判断不同方案取舍的架构同学。我会尽量把每个环节为什么这么设计讲透而不是只丢一堆参数让你抄。涉及具体数字的地方我会说明这是基于常见实践的经验值不同硬件和模型会有出入你按自己的场景校准。核心关键词就两个LLM和inference。围绕这两个词我们把整条推理链路拆开揉碎讲一遍。2. 推理链路的整体设计为什么是这套架构2.1 一次推理请求的完整生命周期先建立全局视角。当你发一条消息给推理服务大致会经过这么几个阶段请求接入与排队请求到达服务端进入调度队列等待被分配计算资源。Tokenization把你的文本切成token序列这一步看着简单但分词器的选择会影响后续所有计算。Prefill预填充把整个输入序列一次性喂进模型计算出每个位置的KVKey-Value并缓存起来。这一步是计算密集型的输入越长越慢。Decode解码基于缓存一个token一个token地生成输出。每一步只算一个新token但要把之前所有token的KV都读一遍所以这一步是显存带宽密集型的。Detokenization与流式返回把生成的token转回文本边生成边推给前端。资源回收请求结束释放KV Cache占用的显存。这个生命周期里Prefill和Decode的特性完全不同这是理解所有推理优化的一把钥匙。Prefill像是一次性搬一大堆砖算力吃满Decode像是每次只搬一块砖但要把整个仓库的门都开一遍带宽吃满。很多优化手段本质上就是在分别对付这两个阶段的瓶颈。2.2 为什么推理和训练是两套工程有人会问既然都是前向传播为什么不能复用训练的框架原因有几个批处理方式不同训练用大batch追求吞吐推理要兼顾延迟batch往往很小甚至为1。显存压力点不同训练要存激活值、梯度、优化器状态推理只需要存权重和KV Cache。精度要求不同训练对数值稳定性极敏感推理可以大胆用低精度量化。调度目标不同训练追求单位时间算得多推理追求每个请求又快又稳。我踩过的一个坑是早期直接拿训练脚本改吧改吧当推理服务用结果单请求延迟还行一上并发就雪崩。后来才明白训练框架的调度逻辑根本没为多请求交错执行设计过。这就是为什么现在有vLLM、TensorRT-LLM、SGLang这些专门为推理打造的框架。2.3 方案选型的三个核心维度选推理方案我一般看三个维度维度关注点典型取舍延迟首字延迟TTFT、每token延迟TPOT低延迟往往牺牲吞吐吞吐每秒处理token数、并发请求数高吞吐需要大batch和高效调度成本显存占用、硬件利用率量化省显存但可能掉精度这三个维度是互相拉扯的。你要极致低延迟就得小batch、少排队硬件利用率上不去成本就高你要极致吞吐就得攒大batch单个请求的延迟就上来了。没有全能方案只有匹配场景的方案。线上对话场景通常优先保TTFT因为用户对第一句话多久出来最敏感离线批量生成场景则优先保吞吐。3. 核心细节解析那些决定性能的关键点3.1 KV Cache推理优化的命门KV Cache是整个推理优化里最重要的概念没有之一。简单说自注意力机制在生成每个新token时需要用到之前所有token的Key和Value。如果不缓存每生成一个token都要把前面所有token重新算一遍KV计算量随序列长度平方增长。缓存之后每步只需要算新token的KV然后和缓存拼接。用生活类比你在写一篇文章每写一个字都要回顾前面所有字。KV Cache相当于你把前面每个字的要点笔记都记下来了写新字时直接翻笔记不用重新读全文。但KV Cache是有代价的——它吃显存。计算公式大致是KV Cache大小 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × batch大小 × 精度字节数举个例子一个70亿参数、32层、32头的模型头维度128序列长度4096batch为1用FP162字节2 × 32 × 32 × 128 × 4096 × 1 × 2 ≈ 2.1 GB单请求就2GB如果并发32个请求光KV Cache就要67GB比模型权重本身还大。这就是为什么显存管理是推理服务的核心难题。注意很多人只盯着模型权重占多少显存忽略了KV Cache。实际线上跑起来KV Cache往往是显存杀手。3.2 PagedAttention把显存碎片治明白传统KV Cache是给每个请求预分配一块连续显存按最大可能长度分配。问题是大部分请求根本用不到最大长度预分配的空间白白浪费而且不同请求长度不一显存碎片严重。vLLM提出的PagedAttention借鉴了操作系统虚拟内存分页的思路把KV Cache切成固定大小的block比如16个token一块按需分配不要求连续。这样一来显存利用率大幅提升浪费从按最大长度预留变成按实际使用分配。不同请求可以共享block比如同一个prompt的多个采样结果可以共享前缀的KV。碎片问题基本消失。实测下来PagedAttention能把吞吐提升2到4倍具体倍数取决于请求长度分布。这个技术现在已经是主流推理框架的标配。3.3 连续批处理别让GPU闲着传统批处理是静态batch攒够一批请求一起跑完再跑下一批。问题是同一批里有的请求生成10个token就结束了有的要生成500个短的得等长的GPU在等待期间大量空转。连续批处理Continuous Batching的思路是不等整批结束某个请求一完成就立刻把新请求塞进来。这样GPU几乎一直在干活利用率大幅提升。这个机制配合PagedAttention是vLLM高吞吐的两大支柱。我实测过一个对比同样硬件、同样模型静态batch的吞吐大概是连续batch的三分之一到一半。请求长度差异越大差距越明显。3.4 量化用精度换显存和速度量化是把模型权重和激活值从高精度FP16/BF16压到低精度INT8/INT4的过程。好处很直接显存占用降低能塞下更大的模型或更多并发。显存带宽压力降低Decode阶段提速。部分硬件上低精度计算本身更快。常见的量化方案方案精度特点适用场景FP16/BF1616位无损基线显存充足INT88位掉点小提速明显通用推荐INT44位省显存多可能掉点显存紧张GPTQ/AWQ4位训练后量化质量较好本地部署GGUF多档面向CPU/边缘设备安卓本地运行提示量化不是免费的午餐。INT4在数学推理、代码生成这类任务上掉点可能比较明显对话类任务通常还好。上线前一定要在你的实际任务上评测别只看论文里的平均分。3.5 投机解码用小模型加速大模型投机解码Speculative Decoding是个很巧的思路用一个小的草稿模型快速生成几个候选token然后让大模型一次性验证这几个token对不对。因为大模型验证一批token的成本和生成一个token差不多如果草稿命中率高整体就能提速。关键指标是接受率——草稿模型猜的token有多少被大模型认可。接受率高提速明显接受率低反而拖慢。一般来说草稿模型和主模型越像接受率越高。实践中常见2到3倍的提速。这个技术特别适合延迟敏感的场景因为它不改变输出分布理论上无损只是加速。4. 实操过程从零搭一个能用的推理服务4.1 环境准备与框架选择假设你要在单机多卡上部署一个开源模型我推荐的技术栈推理框架vLLM生态成熟、上手快或TensorRT-LLM极致性能、配置复杂模型格式HuggingFace格式或已量化版本服务层框架自带的OpenAI兼容API或自己套一层FastAPI监控Prometheus Grafana盯TTFT、TPOT、吞吐、显存安装vLLM大致是这样pip install vllm启动一个服务python -m vllm.entrypoints.openai.api_server \ --model /path/to/your/model \ --tensor-parallel-size 2 \ --max-model-len 8192 \ --gpu-memory-utilization 0.9几个参数解释一下--tensor-parallel-size张量并行度等于用几张卡。2就是两张卡分摊模型。--max-model-len最大序列长度直接影响KV Cache预留。设太大浪费显存设太小长请求会被拒。--gpu-memory-utilization显存使用上限比例。0.9表示用90%留10%给系统和其他开销。注意max-model-len别拍脑袋设。先统计你线上请求的长度分布取P99再留点余量。设成模型支持的最大值比如32K会导致KV Cache预留过多并发上不去。4.2 关键参数的计算与调优并发数怎么估显存减去模型权重剩下的给KV Cache。用前面公式反推能放多少个请求。比如剩40GB给KV Cache单请求2GB理论上并发20。但实际要留余量按15算比较稳。batch大小怎么定不是越大越好。batch太大TTFT会明显上升因为新请求要等当前batch算完。我一般从较小值起步逐步加压观察TTFT和吞吐的拐点。精度怎么选显存够就BF16不够上INT8再不够INT4。每次降精度都要跑一遍评测集确认掉点在可接受范围。4.3 压测与性能观测搭好服务必须压测。我常用的工具是locust或wrk构造不同长度、不同并发的请求重点看四个指标指标含义健康参考TTFT首字延迟对话场景1sTPOT每token延迟50ms较流畅吞吐每秒token数越高越好显存占用GPU显存使用留10%余量压测时一定要模拟真实长度分布。只测短请求会给你虚假的乐观一上长请求就崩。我吃过这个亏demo阶段全是短问答性能漂亮得很上线后用户开始贴长文档延迟直接翻好几倍。4.4 一个真实的调优记录分享一次我调优的经历。初始配置单卡A100 80G7B模型BF16max-model-len设32K。压测结果并发8时TTFT就超过2秒吞吐上不去。排查发现两个问题一是max-model-len设太大KV Cache预留过多实际并发上不去二是没开连续批处理的高级调度。调整max-model-len降到8K覆盖P99开启chunked prefill把长prefill切成小块避免阻塞decode。调整后并发16时TTFT降到800ms吞吐提升约2.5倍。这个案例说明参数不是越大越好要匹配真实负载。5. 常见问题与排查技巧实录5.1 显存溢出OOM怎么破OOM是推理服务最常见的故障。排查顺序看是加载时OOM还是运行时OOM。加载时OOM说明模型太大得量化或加卡。运行时OOM多半是KV Cache爆了。降低max-model-len或限制最大并发。检查是否有请求长度异常。有些用户会贴超长文本需要在前置层做长度截断。实操心得给服务加一个最大输入长度的硬限制超了直接拒绝并返回友好提示。别指望模型自己扛长请求会把整个服务的显存拖垮。5.2 首字延迟高得离谱TTFT高通常有几个原因排队严重并发超过服务能力请求在队列里等。解决靠扩容或限流。Prefill太长输入序列很长Prefill计算量大。可以用chunked prefill缓解。调度策略问题新请求被长请求阻塞。检查是否开启了合理的调度优先级。5.3 吞吐上不去吞吐低往往是GPU利用率低。检查batch是不是太小GPU没吃饱。有没有开连续批处理。KV Cache是不是碎片化严重导致频繁换入换出。是不是被某个超长请求拖住了整条流水线。5.4 输出质量下降如果发现量化后输出变差先确认是不是量化导致的用同一批prompt对比BF16和量化版本的输出。检查量化方案是否匹配你的任务。代码任务对量化更敏感。考虑混合精度关键层保持高精度其他层量化。5.5 常见问题速查表现象可能原因排查方向OOMKV Cache过大降max-model-len、限并发TTFT高排队或Prefill长扩容、chunked prefill吞吐低GPU空转开连续批处理、调batch输出乱码量化掉点换量化方案、混合精度服务不稳定长请求拖累输入长度硬限制6. 进阶方向还能往哪优化6.1 前缀缓存复用很多场景下不同请求共享相同的前缀比如系统提示词、few-shot示例。前缀缓存Prefix Caching可以把这部分KV缓存下来复用避免重复计算。对于系统提示词很长的应用提速非常明显。vLLM和SGLang都支持这个特性开启后TTFT能降不少。6.2 分离式架构Prefill和Decode特性不同混在一起跑会互相干扰。分离式架构Disaggregated Serving把两个阶段拆到不同机器上Prefill机器算力强Decode机器带宽强。这样各自都能跑到最优整体效率更高。这个方向现在很热但工程复杂度也高适合大规模部署。6.3 边缘侧推理如果要在手机、嵌入式设备上跑LLM思路完全不同。这时候算力和显存都极其有限得靠极致量化比如GGUF的Q4_K_M档。小模型1B到3B参数级别。CPUNPU混合计算。安卓上跑GGUF格式的模型现在有不少成熟方案适合做离线、隐私敏感的场景。但别指望速度和云端比能跑起来、能用就是胜利。6.4 智能体场景下的推理挑战当LLM作为智能体Agent自主决策、调用工具、多轮循环时推理服务的压力模式和单轮对话完全不同请求数暴增一个任务可能触发几十次模型调用。上下文累积每轮都要带上历史序列越来越长。容错要求高智能体自主运行时推理失败会导致整个任务链断裂。这就要求推理服务不仅快还要稳、要能容错。实践中我会给智能体场景单独配一套推理资源和普通对话隔离避免互相影响。同时做好重试和降级主模型超时就切备用小模型保证任务能继续。7. 我踩过的坑和几条实在建议做推理工程这几年踩的坑比写的代码还多。挑几个最有代表性的说说。第一个坑迷信benchmark数字。论文和官方文档里的吞吐数字都是在理想条件下测的固定长度、固定并发。真实流量是长尾分布一堆短请求里混着几个超长请求性能表现和benchmark差很远。永远用自己的真实数据压测。第二个坑忽略冷启动。模型加载、CUDA kernel编译、缓存预热都需要时间。线上扩容时新实例冷启动可能几十秒这段时间请求会打到老实例上。要么提前预热要么做好流量调度。第三个坑量化一刀切。以为所有任务都能无脑INT4结果代码生成任务掉点严重。后来改成按任务分模型对话用INT4代码用INT8各取所需。第四个坑监控只看平均值。平均延迟好看但P99可能惨不忍睹。用户感知的是最差的那几次体验。盯P95和P99别只盯平均。最后分享一个实用技巧给推理服务加一个降级开关。当负载超过阈值时自动降低max-model-len或切换到更小的模型保证服务不崩。宁可回答得糙一点也别整个服务挂掉。这个开关在流量突增时救过我好几次。推理工程没有银弹每个场景都要具体调。但只要你理解了Prefill和Decode的本质差异、KV Cache的显存账、连续批处理和量化的取舍逻辑大部分问题都能找到方向。剩下的就是动手压测、看数据、调参数一轮一轮磨出来。