
1. 推理为什么慢先从Transformer的生成过程说起本地部署过大模型的朋友应该都有体会模型加载好后跑一次完整对话前几个字出得挺快越往后越慢并发一多GPU显存直接报警。这个问题背后藏着一个很容易被忽略又极其关键的机制——KV Cache。我最早遇到它是在用原始Transformers库写推理脚本时显存占用曲线诡异地上涨查了半天资料才搞明白原来模型每生成一个新token都要重新计算并保存一组Key和Value向量这就是KV Cache。1.1 生成阶段的计算过程拆解Transformer模型生成文本的方式是逐token自回归每一步都依赖前面所有token的信息。以自回归生成的第t步为例模型需要根据已有的token序列 (x_1, x_2, ..., x_{t-1}) 预测下一个token (x_t)。在没有KV Cache的情况下每一步都要把整个历史序列重新过一遍Self-Attention也就是对1到t-1所有位置都重新计算Query、Key、Value向量。这里的关键在于Attention的计算逻辑是“当前Query与所有历史Key做点积再与所有历史Value加权求和”。在没有缓存时历史位置的Key和Value也被当作新输入重新算了一遍但它们在之前t-1步时已经被算过且并没有变化。这就好比你每次翻到一本书的某一页都要从头把前面所有页重读一遍才能定位到目标位置时间全浪费在重复劳动上。1.2 KV Cache省掉了什么重复劳动KV Cache的思路非常朴素既然历史token的Key和Value在之前的计算中已经算出来了那就把它们存到显存里后面每次生成新token时直接复用只对当前新token计算Query并把新的Key、Value追加到缓存中就可以了。具体来说第t步只需要做一件事拿新token的Query去和缓存里的全部Key做Attention而不是重新计算所有历史token的Key和Value。这样每一步的计算量从“处理整个序列”降为“处理单token”生成阶段的整体时间复杂度从 (O(n^2)) 降为 (O(n))生成速度提升非常明显。以前在CPU上用纯Python推理一个几百token的句子要等几十秒引入KV Cache之后基本能做到实时出字。1.3 没有KV Cache大模型推理根本无法商用如果去掉KV Cache假设模型本身参数量是7B生成1024个token时后续每一步都要对整个序列做完整的Attention重算。实测数据表明不做缓存的推理时间会随序列长度成二次方增长到后面每出一个token的成本高得离谱单用户对话都撑不住更别说多用户并发。所以现在主流推理框架包括HuggingFace Transformers、vLLM、TGI、TensorRT-LLM全都内置了KV Cache机制。KV Cache不是可选项而是大模型在线推理的默认底座。但KV Cache也不是白给的它把“计算时间”换成了“显存空间”。模型参数不动KV Cache随着序列长度和并发数线性增长显存占用很快就成为瓶颈。这就是为什么“显存不够”“并发上不去”成为大模型服务化落地时最常遇到的坎。2. KV Cache的显存账本算一算你的GPU能扛多大并发KV Cache在推理时占据的显存往往超出很多人直觉。很多人以为显存主要被模型权重占着实际跑起来才发现权重可能只占一半另一半被KV Cache吃掉了。我自己第一次用A100 80G部署Llama-3-70B时量化后权重大概40G按默认配置跑起来居然直接爆显存最后查日志发现是KV Cache把剩余空间全占了还超出了不少。2.1 显存占用估算公式与实例KV Cache的显存占用可以用一个非常简单的公式估算[ \text{KV Cache显存} 2 \times \text{层数} \times \text{KV头数} \times \text{头维度} \times \text{序列长度} \times \text{批大小} \times \text{字节数} ]公式中的2对应每个token需要同时保存一组Key和一组Value。以Llama-3-8B模型为例架构参数是32层、GQA的KV头数为8、head_dim为128用FP162字节存储单请求序列长度设为4096[ 2 \times 32 \times 8 \times 128 \times 4096 \times 1 \times 2 536,870,912 \text{字节} \approx 512 \text{MB} ]单请求4096长度就要占用512MB批大小变成8时直接到4GB批大小16时要8GB。这还没算模型权重、激活值、优化器状态的占用。而如果换成没有GQA的传统MHA架构KV头数等于注意力头数比如LLaMA-2-70B的64个KV头KV Cache占用直接翻几倍显存压力更大。2.2 精度、GQA与长上下文对KV Cache的影响KV Cache的精度选择对显存影响巨大。FP16每个元素2字节INT8每个元素1字节FP8也是1字节。同样是8B模型、批量8、序列4096用INT8存储KV Cache只需要2GB相比FP16省了一半。代价是精度损失不过对KV Cache这种中间表示来说INT8量化通常不会对最终生成质量造成明显影响现在很多推理引擎默认开INT8 KV Cache。架构层面的GQAGrouped Query Attention也是为KV Cache量身定做的优化。它让多个Query头共享同一组Key、Value头KV头数从注意力头数降为原来的1/4甚至1/8KV Cache显存同比缩小。这也是为什么Llama-3、Mistral这些新模型都在用GQA性能上几乎无损显存却省了一大截。长上下文是另一个吞显存的大户。上下文长度从4K提升到32KKV Cache占用线性增长8倍。这导致一个很尴尬的情况模型声称支持128K上下文但实际部署时为了给足够长的KV Cache预留空间并发数只能压到个位数甚至1。2.3 KV Cache优化的常见方向KV Cache优化的思路大致有这么几条一是精度压缩用INT8、FP8甚至更激进的量化存KV Cache二是架构优化改用GQA、MLAMulti-head Latent Attention这类减少KV头的方案DeepSeek的MLA就把KV压缩成低秩潜向量压缩比非常夸张三是淘汰策略像StreamingLLM、H2O这类方法只保留重要token的KV丢掉不重要的部分用质量换显存四是管理优化也就是下面要讲的vLLM PagedAttention思路解决的是“预留了但没用上”的浪费问题。这些优化方向里工程上见效最快、最容易被忽视的其实是最后一种。很多框架为了省事会一次性给请求分配最大长度对应的KV Cache空间用户实际只生成了几十个token空间却按2048预留浪费率经常超过70%。vLLM解决的核心问题就是这种浪费。3. vLLM凭什么这么快PagedAttention与Continuous BatchingvLLM是目前大模型推理服务化最常用的框架之一。它的推理速度不一定是最快的但它在吞吐量和显存利用率上的优势非常突出尤其是面对大量并发请求时优势更加明显。它的底层核心是PagedAttention配合Continuous Batching把GPU的利用率拉到了一个新高度。3.1 显存碎片化比显存不够更隐蔽的敌人传统推理框架处理KV Cache时一般会为每个请求预先分配一块连续显存大小按该请求可能达到的最大序列长度计算。这种分配方式有两个问题一是内部碎片请求实际只生成了100个token却占用了2048 token的显存空间剩下90%以上的空间被白白闲置二是外部碎片不同请求先后结束、释放显存会在显存中留下很多不连续的小空洞后续请求要么分配不到足够大的连续空间要么被迫更早触发显存溢出。这两个问题叠加的结果是明明显存看起来“够用”实际能处理的并发却很低。我在用Transformers代码自己写批量推理时把batch调到8就报CUDA OOM但实际模型权重加激活值只占了一半多显存剩下全是被“预留”和“碎片化”吃掉了。3.2 PagedAttention像操作系统的内存分页一样管理KV CachePagedAttention的灵感来自操作系统的虚拟内存分页机制。它把KV Cache切分成固定大小的块block每个块能存固定数量token的KV向量比如16个。每个请求不再需要一整块连续显存而是可以分散存放在多个不连续的block中框架用一个索引表记录每个逻辑块对应的物理块位置。这样的好处是显而易见的。首先按需分配请求实际生成多少token就分配多少block不会预留多余空间其次物理块可以灵活复用多个请求甚至可以在同一个物理块上共享相同的KV内容典型场景是公共前缀比如系统提示词和并行采样。显存利用率能从50%左右提升到90%以上这是无数生产环境实测过的数字。3.3 Continuous Batching不让GPU空转传统推理把请求按batch整体处理一批请求中只要有任何一个还没完成整批其他已经完成的请求也要继续“陪跑”占用算力GPU存在大量空转。Continuous Batching则把调度粒度从“batch级别”细化到“step级别”每个解码步骤动态决定哪些请求可以继续运行、哪些新请求可以插入。机制上一个请求在前几个“prefill”阶段需要大量计算处理用户输入后面进入“decode”阶段每一步只需要计算一个token。Continuous Batching会把处于不同阶段的请求混在一起执行先做完的请求立刻退出并腾出位置新请求马上补进来。GPU上每一刻都在处理“有效计算”吞吐量自然就上去了。3.4 vLLM的其它工程特性vLLM能成为事实标准不只是靠PagedAttention和Continuous Batching两个核心机制它还集成了很多生产环境必需的工程能力。OpenAI兼容的API服务可以让现有应用一行代码切换Prefix Caching对多轮对话和相似请求能做到批量复用KV前缀量化支持涵盖了AWQ、GPTQ、FP8等主流方案张量并行可以把模型拆到多卡上部署。我最看重的其实是它对多模型的统一管理能力。一个vLLM服务可以同时加载多个模型通过--served-model-name区分不同模型的路由。这在做模型对比评测或者多模型负载均衡时非常省事不用为每个模型单独起一个进程。4. vLLM从安装到上线的完整实操原理讲再多不上手跑一遍都不算真的会用。下面记录我实际部署vLLM的完整过程包括环境、命令和关键参数。4.1 环境准备与安装vLLM安装前先确认环境Linux系统是首选Windows支持有限推荐用WSL2或者直接上服务器。CUDA版本11.8起步12.1及以上体验更好。硬件方面建议至少10G以上显存小模型在低显存上也能跑但并发能力会受很大限制。安装直接用pippip install vllm如果你要给vLLM配置额外依赖需要注意它默认会装对应torch版本最容易出问题的是环境中已有torch时版本冲突。我建议给vLLM单独建一个conda环境conda create -n vllm python3.11 conda activate vllm pip install vllm国内网络下载模型是个大坑HuggingFace经常连不上或速度慢解决办法是设置镜像源export HF_ENDPOINThttps://hf-mirror.com4.2 启动一个生产可用的API服务把模型下载到本地后一段最基本的vLLM启动命令长这样vllm serve /data/models/Qwen2.5-7B-Instruct \ --served-model-name qwen25-7b \ --max-model-len 8192 \ --gpu-memory-utilization 0.85 \ --tensor-parallel-size 1 \ --dtype auto \ --trust-remote-code--max-model-len控制模型允许的最大序列长度它决定KV Cache预留上限。很多新手喜欢把这个设得特别大以为能处理长文本更“厉害”实际上过大的max-model-len会直接压掉并发数服务在高并发下很容易OOM。我的经验是先估算你业务场景的典型输入加输出长度在这个基础上加一点余量即可。--gpu-memory-utilization是另一个关键参数默认0.9表示允许vLLM占用90%的显存。这个值不是越高越好调太高会导致KV Cache的可用块碎片化实际有效吞吐反而下降。我生产环境一般设在0.8到0.85留出足够余量给CUDA上下文和其他进程。--tensor-parallel-size用于单机多卡填2表示用两张GPU张量并行切分模型。千万不要把这张卡的数量填得超过物理GPU数量会直接启动失败。--dtype auto让vLLM自动选择精度如果你的显卡不支持bfloat16它自动落到float16很方便。第一次启动会慢一些因为要加载权重、构建CUDA Graph、编译算子日志里出现大量编译信息不用紧张。等看到INFO: Application startup complete.就说明服务已经就绪了。4.3 用OpenAI接口调用vLLM服务vLLM启动后默认在http://localhost:8000提供了一个OpenAI格式的API。可以直接用curl测试聊天接口curl http://localhost:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: qwen25-7b, messages: [{role: user, content: KV Cache是什么}], temperature: 0.7, max_tokens: 512 }model字段填的不是启动命令里的模型路径而是--served-model-name指定的名字。Python侧用openai官方SDK也能直接接只是把base_url改成vLLM的地址from openai import OpenAI client OpenAI( base_urlhttp://localhost:8000/v1, api_keyEMPTY, ) response client.chat.completions.create( modelqwen25-7b, messages[{role: user, content: 讲一个冷笑话}], max_tokens256, temperature0.8, ) print(response.choices[0].message.content)这种兼容设计让业务方做模型切换基本零成本从OpenAI官方服务切到自部署vLLM代码几乎不用改。4.4 单机多卡与多模型部署单机多卡部署时只要把--tensor-parallel-size设成GPU数量vLLM内部会自动做张量并行把模型权重和KV Cache拆到多张卡上。比如4卡A100部署70B模型就把--tensor-parallel-size设为4。启动前先用nvidia-smi确认能从当前环境访问到所有GPU容器部署时还要加--gpus all参数。多模型部署在vLLM 0.5.0之后的版本中支持得比较完善。一个服务同时加载多个模型的做法是vllm serve \ --model /data/models/Qwen2.5-7B-Instruct \ --served-model-name qwen \ --model /data/models/Llama-3.1-8B-Instruct \ --served-model-name llama \ --max-model-len 8192调用时用model字段区分要打哪个模型。这样多个模型共享一个服务进程和显存池相比各起一个服务内存和运维成本都低得多。代价是如果一个模型吃满了KV Cache另一个模型的可用资源会变少所以多模型部署时要评估好负载模型之间的显存隔离策略。5. 特殊部署场景CPU模式、LM Studio与vLLM的取舍不是所有人都有A100也不是所有场景都要求高并发。不少同学环境受限只能在CPU上跑模型或者习惯用本地GUI工具做推理。这块出现过一个很热的词叫“纯CPU模式”结合我自己踩过的坑聊聊不同场景的选型。5.1 vLLM纯CPU模式怎么跑vLLM从0.6.0版本开始提供了CPU后端你可以用--device cpu强行让它在CPU上运行。实际操作如下vllm serve /data/models/Qwen2.5-7B-Instruct \ --device cpu \ --served-model-name qwen \ --max-model-len 4096 \ --gpu-memory-utilization 0--gpu-memory-utilization在CPU模式下没有意义显存不会被使用。CPU后端依赖OpenVINO等算子在x86平台上做加速实测下来吞吐量比纯PyTorch CPU推理高一截但相比GPU还是慢一到两个数量级。这模式适合没有GPU的开发者做学习验证、接口联调不适合对响应速度有要求的线上服务。如果你完全不需要API服务只是在本地研究模型效果可以考虑更轻量的方案。llama.cpp系列工具在CPU上的优化比vLLM CPU后端更成熟对小模型和量化模型的支持也更顺手而且支持纯内存推理、无需安装庞大的CUDA依赖。vLLM CPU模式的技术栈更适合后续平滑迁移到GPU环境二选一的核心标准是“你后期会不会换到GPU”。5.2 LM Studio与vLLM到底怎么选LM Studio是最近很火的本地大模型桌面工具主打图形界面和开箱即用。它其实不是一个推理框架而是一个“壳”底层支持llama.cpp、MLX、ONNX Runtime等多种后端你可以下载模型后在GUI里直接聊天也可以开一个本地API端口兼容OpenAI接口。bionic是它最近的一个版本代号指的是那版发布固件。LM Studio与vLLM的区别可以用一句话概括LM Studio是给个人用户准备的模型管理器聊天客户端vLLM是给服务端场景准备的推理引擎。前者的优势是界面友好、模型下载管理方便、内存占用低、开箱即用后者的优势是高并发吞吐、PagedAttention显存优化、OpenAI兼容API、多卡部署、量化支持完善。我把两个的适用场景整理成了一张对比表对比维度LM StudiovLLM定位本地桌面推理与管理工具服务端高性能推理引擎底层后端llama.cpp / MLX / ONNX RuntimePagedAttention CUDAGPU要求可纯CPU运行低显存也能用推荐NVIDIA GPUCPU后端不成熟并发能力弱适合单用户强面向多用户服务多模型管理图形化管理切换方便单服务多模型API路由部署复杂度安装即用需要配置参数、处理依赖适合场景个人学习、开发调试生产环境、API服务5.3 不同负载下的部署选型参考纯本地体验、偶尔跑个模型对话用LM Studio或者Ollama就行省心省事。在服务器上开发调试、模型评测、低并发内部工具单张消费级显卡跑vLLM完全够用。生产环境对外提供服务、有并发压力、需要持续稳定运行vLLM基本是首选必要时用多卡张量并行或配K8s做弹性伸缩。特别提醒一种情况如果你的服务对延迟极其敏感比如要求首token延迟低于100毫秒vLLM的Continuous Batching调度机制可能引入额外排队延迟这时候TensorRT-LLM这类极致优化的框架更适合。vLLM更适合“高吞吐但能容忍一定延迟波动的场景”。选型没有银弹核心指标就两个一个是吞吐量TPS一个是首Token延迟TTFT你更看重哪个框架就该偏向哪个。6. 实战排坑部署vLLM过程中的典型问题部署vLLM的过程中遇到问题才是常态。这里把我和身边同事踩过的坑整理成速查表每个都是真实案例。6.1 高频问题速查表现象可能原因解决办法启动报CUDA out of memory显存被其他进程占用或权重KV Cache超显存nvidia-smi查占用调小--gpu-memory-utilization改用量化模型服务启动慢得离谱首次构建CUDA Graph、编译算子正常现象观察日志等待启动完成即可并发高时请求排队严重max-model-len设置过大KV Cache预留过多按实际业务调小max-model-len或调大gpu-memory-utilization输出到一半连接断开max_tokens超过模型最大输入输出限制调大--max-model-len或调小请求参数模型加载报trust_remote_code错误模型仓库有自定义Python代码需授权执行启动命令加--trust-remote-code多卡启动失败tensor-parallel-size大于GPU数量或有进程占用GPU检查nvidia-smi释放资源API返回404请求路径不对或served-model-name不匹配先访问/v1/models查看已加载模型名中文输出乱码模型tokenizer编码问题或请求编码问题加--tokenizer-mode auto检查客户端请求头编码6.2 几个容易被忽略的细节点--max-model-len不是越大越好这是所有新手最容易踩的坑。假如你设了32768即使每个请求平均只生成512个tokenvLLM也会为每个请求预留32768长度的KV索引空间。并发一高KV Cache块数量指数膨胀直接OOM。正确做法是根据业务统计的P99请求长度设置宁可多开一个服务兜底长文本也不要在主服务上预留过大空间。--gpu-memory-utilization调太高会引发碎片化。这个参数决定了vLLM进程占用的显存上限设成0.95时留给缓存分配器的空间虽然大了但KV Cache块之间容易产生碎片有效分配率反而下降。我一般在0.8到0.85之间既保证能利用大部分显存又不至于没有余量给峰值请求。新版本的vLLM默认开启CUDA Graph首次启动需要几百MB到几个GB的额外显存如果显存非常紧张可以通过--enforce-eager关闭CUDA Graph来降低启动显存占用代价是每次推理延迟会略高。这是显存和延迟的又一次取舍遇到“差一点启动不起来”的尴尬局面时先关掉CUDA Graph是最快的救急手段。6.3 排查问题的方法论遇到vLLM问题最快的方式是看启动日志和服务运行日志。vLLM的日志非常详细OOM前会有显存分配的详细打印排队延迟过高时日志里会有调度器的等待记录。另外/v1/models和/metrics这两个接口很有用前者确认模型注册情况后者提供Prometheus格式的指标能看到吞吐、延迟、队列深度的实时数据。生产环境建议把vLLM的指标接入监控系统因为很多问题不是立刻爆出来的而是缓慢劣化比如某个时间段并发峰值导致延迟直线上升、KV Cache的复用率持续下降等只有曲线能看出来。最后再分享一个小经验vLLM版本迭代非常快大版本升级前先看release notes尤其是配置项变化。我遇到过从0.5升级到0.6时--model参数用法变了、日志格式变了、CPU后端需要额外装依赖配置文件和监控脚本全都得跟着调整。保持vLLM版本不要太激进但也不能长期停在旧版生产环境建议锁定一个经过测试的稳定版本每次升级都先在测试环境跑一轮压测再上生产。这个习惯帮我避免了很多次线上事故。