
1. 为什么推理引擎值得单独拿出来聊大模型部署这件事很多人第一反应是“把权重下载下来找个框架加载起个HTTP服务就完事了”。真到线上跑起来才发现事情远没有这么简单单张卡吞吐上不去、并发一高延迟就爆炸、显存明明够却频繁OOM、同样的硬件别人能扛三倍流量你只能扛一倍。这些问题的根源往往不在模型本身而在推理引擎这一层。vLLM就是在这个背景下被大量团队选中的推理引擎。它最早出圈靠的是PagedAttention这个显存管理机制后来围绕它长出了一整套面向生产的推理架构Continuous Batching、调度器、KV Cache分页管理、Prefix Caching、张量并行、OpenAI兼容接口等等。热词里出现的“vllm部署deepseek”“docker vllm/vllm-openai加载qwen3-embedding”“vllm scheduler逻辑”“glm使用哪个版本的镜像”本质上都是同一件事的不同侧面——大家想把这个引擎用起来、用对、用到生产级别。这篇文章面向的是准备把vLLM真正落到生产环境的人你可能已经跑通过一次demo但不确定调度器怎么工作、KV Cache到底省在哪、镜像里带不带模型、并发上不去该调什么。我会从PagedAttention的底层逻辑讲起一路拆到生产级架构设计和踩坑经验。基础概念会讲清楚但重点放在“为什么这么设计”和“实际怎么调”上因为这两块才是文档里最缺、线上最容易翻车的地方。2. PagedAttention到底解决了什么问题2.1 从KV Cache的显存浪费说起要理解PagedAttention得先理解它要对付的敌人KV Cache的碎片化。Transformer在自回归生成时每生成一个token都要用到之前所有token的Key和Value。为了避免重复计算推理引擎会把历史token的K、V缓存下来这就是KV Cache。问题在于KV Cache的大小是动态的你事先不知道一个请求最终会生成多长可能50个token就结束也可能2000个token才停。传统做法是给每个请求预分配一块连续显存按最大可能长度来留。比如模型最大支持4096长度那就按4096给每个请求留位置。这带来两个直接后果一是内部碎片请求实际只用了200个token剩下3900个位置白白占着二是外部碎片不同请求的块大小不一显存里到处是零散空洞最后明明总空闲显存够却拼不出一块连续空间给新请求。实测中这种浪费能到什么程度在请求长度分布比较分散的场景下传统连续分配的有效显存利用率经常只有20%到40%。也就是说你买了80G的卡真正用来存有效KV的不到30G剩下的全在碎片和预留里躺着。2.2 分页思想把操作系统那套搬过来PagedAttention的核心思路非常朴素既然连续分配会碎片化那就不连续分配。它借鉴了操作系统虚拟内存分页的做法把KV Cache切成固定大小的块block每个块存固定数量token的K和V比如16个token一块。这些块在物理显存里可以散落各处通过一张块表block table把逻辑上的连续序列映射到物理上不连续的块。这个设计带来的变化是根本性的。首先显存按需分配请求生成到第17个token时才需要第二个块不会提前占着。其次块大小固定不存在外部碎片任何空闲块都能被任何请求使用。第三块表是逻辑到物理的映射意味着多个序列可以共享同一批物理块——这一点直接催生了后面的Prefix Caching和并行采样优化。用一个生活化的类比传统做法像给每个客人订一整层酒店哪怕他只住一晚PagedAttention像把酒店拆成标准间谁住几晚就开几间房间还能动态续订空出来的房间随时给下一位客人。2.3 块大小怎么选一个被低估的参数块大小block size是PagedAttention里少数需要你手动权衡的参数默认通常是16。这个值不是随便定的。块太小比如4块表会变得很长每次注意力计算要遍历更多块元数据开销和寻址开销上升调度器管理成本也高。块太大比如64最后一个块内部的浪费就明显了——一个请求如果只多用3个token却占了一个64的块浪费接近95%。而且块越大Prefix Caching的共享粒度越粗命中率会下降。经验上16是个比较稳的默认值兼顾了寻址效率和内部浪费。如果你的业务请求普遍很长比如都是几千token的长文档问答可以试32如果请求普遍很短比如分类、抽取类任务16甚至更小反而更划算。这个参数没有银弹得结合你真实的长度分布来定最靠谱的办法是拿线上流量采样跑一遍对比。注意块大小一旦确定通常不建议在同一个服务里混用。不同块大小的KV Cache无法互相共享Prefix Caching会失效。3. Continuous Batching吞吐量的真正来源3.1 静态Batching为什么浪费很多人第一次做推理优化想到的是batching把多个请求凑成一批一起算利用GPU的并行能力。这叫静态batching问题是它要求一批里的请求同时开始、同时结束。现实是请求长度差异巨大。一批里如果有三个请求分别生成20、200、2000个token静态batching会让整批等最长的那个。前两个早就生成完了却要占着显存和计算资源陪跑直到最长的结束才能释放。GPU利用率在这种场景下惨不忍睹短请求的延迟也被硬生生拉长。3.2 迭代级调度每个token都是一次调度机会Continuous Batching也叫iteration-level scheduling把调度粒度从“一批请求”细化到“一次前向迭代”。每一轮前向计算结束后调度器都会重新审视哪些请求已经生成完了可以退出哪些新请求可以加进来哪些还在生成需要继续。这样一来短请求生成完立刻退出释放资源新请求立刻补位GPU几乎不会空转。长请求和短请求可以混在一起跑短请求的延迟不再被长请求拖累。这是vLLM吞吐量能比朴素实现高出一个数量级的关键原因之一。从调度器视角看每一轮它要做的事大致是先处理已完成的序列并释放其KV块再从等待队列里挑选能塞进当前显存预算的新请求然后组成本轮batch做前向。这个“挑选”过程就是vLLM scheduler逻辑的核心也是热词里大家反复问的部分。3.3 调度器的取舍吞吐优先还是延迟优先vLLM的调度器默认偏向吞吐它会尽量把batch填满把显存用足。这在离线批处理或者吞吐敏感的场景下是对的但在线服务里可能带来延迟抖动一个请求可能因为前面排了太多长请求而等待较久。实际调优时你要关注几个方向。一是限制单批的最大序列数max_num_seqs防止batch过大导致单轮计算时间过长、首token延迟升高。二是控制最大等待序列数避免队列无限堆积。三是结合业务SLA决定是否开启chunked prefill——把长prompt的prefill拆成多轮和decode请求交错执行避免一个超长prompt把整轮decode卡住。这里有个常见误区以为batch越大吞吐越高。实际上batch增大到一定程度后单轮计算时间线性增长而吞吐增长趋缓延迟却持续恶化。找到那个拐点比盲目调大更重要。4. KV Cache管理与显存预算实战4.1 显存都花在哪了部署vLLM时显存大致分三块模型权重、KV Cache、以及框架和CUDA的运行时开销。模型权重是固定的比如一个7B模型FP16大约14G70B大约140G需要多卡。运行时开销通常几个G。剩下的才是KV Cache能用的空间。vLLM有个关键参数gpu_memory_utilization默认0.9意思是允许vLLM使用单卡90%的显存。它会先加载模型权重然后把剩余显存的大部分划给KV Cache。这个值设太高容易和别的进程抢显存导致OOM设太低则KV Cache空间不足并发上不去。计算KV Cache能存多少token有个粗略公式可用显存除以每token每层的KV字节数乘以层数。每token的KV大小取决于模型隐藏维度、层数、精度。以FP16为例每token每层KV约占 2 × hidden_size × 2字节。这个数乘以层数就是每token的总开销。知道这个你就能估算出给定显存下能同时缓存多少token进而推算并发能力。4.2 Prefix Caching省在重复前缀上很多线上场景的请求共享大量前缀同一个系统提示词、同一份长文档被反复提问、多轮对话里历史部分重复。Prefix Caching让这些重复前缀的KV块被多个请求共享物理上只存一份。它的原理还是靠块表和哈希。系统对每个块的内容算哈希如果新请求的前缀块哈希和已有块一致就直接复用不重新计算也不重新占显存。这在RAG场景里收益特别大——同一份知识库文档被不同问题引用时文档部分的KV只算一次。开启Prefix Caching后要注意它和块大小的关系块越大哈希粒度越粗命中率越低块越小命中率越高但管理开销越大。另外前缀必须完全一致才能命中哪怕差一个token从差异点之后的块都无法复用。4.3 显存不够时的排查顺序线上遇到OOM别急着降并发按这个顺序查先看gpu_memory_utilization是不是设太高和别的进程冲突再看max_model_len是不是设得过大导致单个请求预留空间过多然后看是否有超长请求把KV Cache吃满考虑加长度限制或拒绝策略最后才考虑降max_num_seqs或换更小的模型。有个容易被忽略的点vLLM启动时会做显存profiling如果启动时显存就被别的进程占着它算出来的可用KV空间会偏小导致并发能力被低估。所以部署时尽量保证启动瞬间显存干净。5. 生产级部署镜像、模型与接口5.1 镜像里到底带不带模型这是热词里问得最多的问题之一“vllm docker镜像中带模型吗”。答案是不带。官方镜像vllm/vllm-openai只包含vLLM本身和依赖模型权重需要你另外挂载或下载。常见做法有两种。一是启动时通过参数指定模型路径让容器去HuggingFace或本地目录加载二是把模型权重提前下载到宿主机用volume挂进容器。生产环境强烈建议第二种因为启动时下载模型既慢又不可控网络抖动会直接导致服务起不来。用docker跑的时候典型命令是挂载模型目录、暴露端口、指定模型路径。比如加载一个本地模型把宿主机模型目录挂到容器内然后用--model指向容器内路径。GPU要用--gpus all或者nvidia runtime暴露给容器。5.2 版本选择别盲目追新热词里有人问“glm使用vllm哪个版本的镜像”这类问题的通用答案是优先选和你的模型、CUDA驱动匹配的稳定版本而不是最新版。vLLM迭代很快新版本可能引入新特性也可能引入回归。选版本的实操建议先确认你的CUDA驱动版本再对照vLLM各版本要求的CUDA和PyTorch版本然后看你的模型是否在官方支持列表里有些新模型需要较新版本才有对应的实现最后在测试环境跑通再上生产。镜像tag尽量用具体的版本号别用latest否则某天重启拉到一个不兼容的新版就麻烦了。5.3 OpenAI兼容接口的实用细节vLLM的OpenAI兼容server让迁移成本极低原来调OpenAI的代码改个base_url就能用。但有几个细节要注意。一是模型名。请求里的model字段要和启动时指定的模型名对应否则会报错。二是并发和超时。本地部署没有云端那么强的弹性客户端要设合理的超时和重试。三是流式输出。开启stream后首token延迟TTFT是体验关键这又回到调度和prefill优化上。四是embedding模型。像qwen3-embedding这类模型vLLM也支持以embedding模式提供服务但要注意它和生成模型的调度特性不同通常单独起一个实例更稳。对接Chatbox这类客户端时填好base_url和模型名即可注意有些客户端会做模型列表探测确保你的server正确暴露了模型信息。6. 常见问题与排查速查6.1 启动就OOM先确认是不是gpu_memory_utilization太高或者有残留进程占着显存。用nvidia-smi看清楚谁在占。如果是模型太大单卡放不下考虑张量并行或量化。启动时的profiling阶段OOM往往是可用显存本身就不够不是参数问题。6.2 吞吐上不去检查batch是否真的填满了。如果max_num_seqs设得太小或者请求队列一直很浅GPU利用率自然低。看调度器日志里每轮batch的实际大小。另外确认是否开了Continuous Batching默认开以及是否有长请求在拖累整批。6.3 首token延迟高多半是prefill阶段被长prompt或大队列拖住。可以试chunked prefill把长prefill拆开让decode请求有机会插进来。也可以限制单请求最大长度拒绝超长请求。6.4 Prefix Caching没效果先确认前缀是否真的完全一致差一个字符都不行。再看块大小是否过大导致命中粒度太粗。还要确认这个特性是否真的开启了有些版本默认关闭。现象优先排查常见原因启动OOM显存占用、utilization参数过高、残留进程吞吐低batch实际大小队列浅、seq上限小首token慢prefill、队列深度长prompt阻塞、chunked未开缓存不命中前缀一致性、块大小前缀有差异、块过大并发上不去KV空间、长度限制显存预算不足、超长请求7. 我在实际部署中的几点体会调vLLM这件事参数只是表象真正决定效果的是你对业务流量特征的理解。请求长度分布、前缀重复率、并发模式这些决定了你该往哪个方向调。我见过太多人照搬网上的参数结果和自己的流量完全不匹配。一个很实用的习惯是上线前先拿真实流量采样统计长度分布和前缀重复情况再据此定块大小、长度上限和并发参数。另一个习惯是把调度器日志打开观察每轮batch的实际构成很多性能问题看一眼日志就清楚了。最后分享一个小技巧如果显存总是差一点与其降并发不如先看看有没有超长请求在吃预算加一个长度拒绝策略往往比降并发更划算。这个内容后续还可以往多机多卡、量化部署、以及和上层网关的配合方向继续展开每一块都有不少细节可挖。