前阵子协助一位开发者把内部的开源对话模型部署成可供团队使用的推理服务遇到的情况很有代表性他手上只有一张24GB显存的显卡起初用原生的PyTorch包装了一个推理接口并发一上来就崩——要么显存直接溢出要么请求排队排到超时。后来换成vLLM高吞吐推理引擎同一块卡、同一个模型峰值吞吐量翻了四五倍几十个并发请求稳稳扛住长上下文场景也不掉链子。这篇文章就是围绕这套部署过程展开的完整复盘从显存原理到服务参数从压测数据到生产化踩坑希望能给正在做本地高并发推理服务的你一些可以直接落地的参考。1. 先搞清楚vLLM为什么能扛住高并发KV Cache的显存账本上手调优之前我建议你先把vLLM最核心的显存管理机制弄明白。很多人都知道vLLM快但不知道它快在哪结果参数瞎调一气效果反而不理想。1.1 传统推理服务的瓶颈显存被预分配写死大模型做自回归生成时每生成一个新token都要重新计算历史token的注意力权重。如果不做缓存每生成一个token就要重新走一遍全量计算耗时和上下文长度呈线性增长根本扛不住并发。于是就有了KV Cache把已经算好的每个token对应的Key向量和Value向量存下来后续生成时直接复用。问题来了这个缓存是动态增长的——请求生成到第10个token缓存就是10个token的量生成到第2000个就变成2000个token的量。原生推理框架的做法很粗糙为了避免运行期动态分配出问题很多实现会在请求开始时按最大长度比如2048一次性把KV Cache显存预留出来。实际业务里大部分请求可能只用了几十个token就结束了剩下的显存预留着却派不上用场一个请求就吃掉一大块显存。显存一旦被写死GPU上同时能跑的请求数就少得可怜并发自然上不去。这就像餐厅按最高客流提前把所有桌位都摆好结果平时根本没那么多客人剩下的桌子全空着排队的人却进不来。1.2 PagedAttention把KV Cache做成操作系统的内存页vLLM解决这个问题的思路懂操作系统的人一眼就能看懂内存分页。传统方案是给每个请求分配一整块连续显存vLLM则把KV Cache切成固定大小的块block默认每个块能存放16个token的KV数据。请求运行时按需申请块用完了就释放释放出来的块可以给下一个请求用。这个过程不需要一整块连续显存碎片化利用率大幅提升和操作系统的虚拟内存分页机制是一个道理。我拿一张24GB显卡、跑一个7B参数的开源对话模型bf16权重约16GB来算笔账你会更直观CUDA context和运行时占掉约1GB显存模型权重约16GB剩余大约7GB可以分配给KV Cache7B模型每个token产生的KV Cache在bf16精度下大约是0.1-0.2MB。按0.15MB/token算这7GB能支持约4.6万token的KV总量。如果用传统预分配方案每个请求固定留出2048 token的缓存约300MB那GPU上最多同时容纳20来个请求而且这20来个请求如果在第50个token就结束那整块300MB显存实际只用了几十MB白白浪费。用分页方案20个并发请求可能只消耗了总缓存的很小一部分剩余显存还能不断接纳新请求。这就是高吞吐的物理基础——同样的显存资源实际能承载的动态并发量完全不是一个量级。1.3 Continuous Batching迭代级调度比请求级调度聪明多少优化了显存只是第一步真正的并发能力提升还依赖调度策略。传统动态batching是请求级的把几个请求凑成一整批一起前向计算必须等这一批全部生成完毕才能腾出位置给下一批。如果一批里有一个特别长的请求其他短请求只能干等这就是典型的队头阻塞。vLLM的Continuous Batching在迭代级做调度每个decode步生成一个token结束后都已经完成的请求立刻退出批次、释放自己的KV Cache块新请求可以马上加入下一个迭代。整个批次是动态流动的没有必须等全队走完的概念。结果就是GPU的算力被压得更满。你的卡上总有几个请求在排队等待生成但几乎没有时刻是GPU闲着没事干的吞吐自然就上去了。理解了这个你就能明白后面为什么max-num-seqs这个参数对吞吐影响这么大。2. 本地部署vLLM从显卡选型到安装落地的完整清单原理归原理落到部署上还是有不少琐碎的坑。这节我把硬件选型、依赖安装和首次启动完整过一遍。2.1 硬件基线不同显存规模能跑什么模型先说你手头的卡能跑什么这里列个参考表精度默认是bf16量化后可以往上一档显存大小适合的模型参数量典型配置并发经验16GB7B以下7B量化后少量并发长上下文受限24GB7B-13B7B bf16 / 13B 量化中等并发几十路48GB13B-34B13B bf16 / 34B 量化较高并发80GB大几十B至百B级别量化或稀疏化高并发 长上下文多卡场景要额外注意卡间通信带宽。如果你准备搞tensor parallel张量并行两张卡之间的PCIe带宽不够的话每步同步开销会吃掉不少性能增量实测中NVLink连接的卡比普通PCIe组合效果要好很多。预算有限时优先考虑单张大显存卡不要为了拼显存把两张普通卡凑一起通信瓶颈会让你怀疑人生。2.2 软件依赖与安装CUDA版本、PyTorch版本和安装命令vLLM对CUDA工具链有一定要求显卡驱动太老会直接启动报错。装之前先确认驱动支持哪一版CUDA然后让PyTorch和vLLM保持在同一个CUDA体系下。我这边推荐的做法是用venv隔离环境避免污染系统Pythonpython -m venv vllm-venv source vllm-venv/bin/activate pip install -U pip pip install vllm装完验证一下版本和可用性python -c import vllm; print(vllm.__version__)如果网速不太理想可以换用国内镜像源来安装这个大家应该都知道怎么配不展开。这里提醒一句不要为了追新盲目升级。vLLM小版本升级偶尔会改变部分API行为生产环境里锁定版本号等测试通过了再统一升级比天天追新稳得多。我自己就遇到过升级后某个量化格式的兼容性发生变化原本跑得好好的模型启动直接报错。2.3 首次启动一个最简单的离线推理示例环境就绪后先用离线推理确认模型能正常加载运行再谈服务化。下面是最小示例from vllm import LLM, SamplingParams llm LLM(model/path/to/your-model) params SamplingParams( temperature0.7, max_tokens256, ) outputs llm.generate([介绍一下本地部署大模型推理服务的关键点], params) for output in outputs: print(output.outputs[0].text)第一次启动会有一段模型加载和权重处理时间日志里会输出GPU显存分配情况包括KV Cache预留了多少。跑完这条推理后nvidia-smi看一眼你会发现显存中有两个明显区域一块是模型权重另一块就是KV Cache池。确认KV Cache分配正常核心机制就工作了后面再上服务化配置就顺理成章。3. 把vLLM跑成在线API核心参数与生产化配置离线推理没问题之后真正的挑战是把vLLM跑成一个稳定接收高并发请求的在线服务。3.1 兼容主流API的接口一行命令拉起所有业务vLLM内置了一个高性能API服务器兼容业界主流的Chat Completions接口格式。一个命令就能启动vllm serve /path/to/your-model \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 64 \ --swap-space 4跑起来后用curl验证一下curl http://127.0.0.1:8000/v1/models能看到模型列表就说明服务起来了。再发一个对话请求curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d {model:your-model,messages:[{role:user,content:你好}],stream:true}接口兼容的好处是业务侧大量已有SDK和工具可以直接复用后端从别的推理服务切换到vLLM时业务代码几乎不用改。这对本地快速上线价值很大——不用为每个推理框架写一套对接层。3.2 影响吞吐和并发的前四个参数怎么定、为什么参数其实不少但真正起决定作用的是下面这几个我按重要程度排个序参数作用怎么定--max-model-len限制最长上下文输入输出按业务实际需求定别贪大--gpu-memory-utilization控制KV Cache最多占多少显存通常0.85-0.92别顶到1.0--max-num-seqs最大同时处理的序列数从16开始压测后往上抬--swap-space允许KV Cache溢写到CPU内存的量默认4GB够用别开太大max-model-len是最容易坑人的参数。很多人直接设成模型支持的最大值比如65536结果KV Cache预留空间被撑爆启动就OOM。实际业务里绝大多数请求上下文在几千到一万token之间32K已经是很充裕的设置。显存不够时优先压缩这个值。gpu-memory-utilization不是越高越好。CUDA运行时和上下文本身要占显存你设置0.98时留给系统的余量太少容易出现各种莫名其妙的启动失败。0.9附近是大多数卡的安全区。max-num-seqs控制单次迭代里最多有多少请求挤在一起。这个值太小GPU并行度不够太大一步decode要算太多序列单步延迟飙升TTFT反而变差。它和显存容量、模型规模强相关需要用压测数据来找甜点位。swap-space字面意思是显存不够时把KV Cache溢写到CPU内存。听着很美好但一旦触发swap速度断崖式下跌。生产环境尽量保证KV Cache完全落在显存里swap只当兜底别依赖它扛并发。3.3 多卡与量化怎么扩大并发上限单卡显存不够时两条常见路线第一条是张量并行启动参数加--tensor-parallel-size 2模型权重和KV Cache按层划分到两张卡上显存上限约等于两张卡之和。前提是卡间通信带宽要够否则每步同步的成本可能吃掉收益。第二条是模型量化AWQ或GPTQ这类4-bit量化后模型权重直接缩小到原来的四分之一左右。一张24GB卡甚至能跑13B-14B参数量级的量化模型省下来的显存全部可以转成KV Cache长上下文并发能力显著提升。量化模型的加载需要指定对应的quantization参数启动命令类似vllm serve /path/to/awq-model \ --host 0.0.0.0 \ --port 8000 \ --quantization awq \ --max-model-len 16384 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 64量化后的模型输出质量衰减在多数任务上不明显但代价是那一层精度损失。敏感任务建议先在业务数据集上做对比评估再决定别为了省显存无脑上4-bit。4. 压测数据与调参路径验证高吞吐不能靠感觉参数配好不算完你要能证明这套配置真的能扛住预期的并发。这一节讲我自己的压测方法和实测数据。4.1 设计一套本地压测口径并发数、QPS、TTFT、TPOT先明确四个核心指标并发数同时向服务发起请求的客户端数量QPS每秒完成的请求数衡量整体吞吐TTFT从发起请求到收到第一个token的耗时衡量响应速度TPOT每个输出token的平均生成耗时衡量生成速度压测脚本思路很简单开N个线程每个线程循环发请求统计总耗时、成功数、首token延迟import time import requests from concurrent.futures import ThreadPoolExecutor def send_one(): start time.time() r requests.post( http://127.0.0.1:8000/v1/chat/completions, json{ model: your-model, messages: [{role: user, content: 讲一个300字左右的科技故事}], stream: False, max_tokens: 300, }, timeout120, ) return time.time() - start, ... concurrency 20 with ThreadPoolExecutor(max_workersconcurrency) as ex: results list(ex.map(lambda _: send_one(), range(100)))实际压测时把并发从1逐步提到8、16、32、64记录每个并发档位下的QPS和TTFT曲线。4.2 同样的硬件不同配置的实测对比下面这组数据来自一张24GB显卡、一个开源7B对话模型、bf16精度输入prompt约500 token输出设定为200 token。不同环境数值会有差异但趋势有代表性配置并发16时QPS平均TTFT备注原生PyTorch动态batching约41200ms并发升到24时频繁超时vLLM默认参数约15450ms模型默认KV配置vLLM调优后约28-30200-250ms显存利用率0.9max-num-seqs64同样的卡调优前后吞吐差距接近两倍跟原生方案比差距更大。这说明vLLM的高吞吐是机制带来的但它给你的性能上限需要通过参数调优来兑现不是装上就自动拉满。4.3 调优顺序先调显存再调批大小最后面向场景做取舍我总结的调优路径分三步走照这个顺序不容易迷路第一步确认KV Cache空间。看启动日志里的缓存分配大小再看业务请求的平均token长度计算缓存是否够用。不够就先压max-model-len或者上量化把权重占的显存抠出来。第二步调max-num-seqs。从16开始按2倍往上翻观察QPS和TTFT。如果QPS还在涨且TTFT没有明显恶化继续加如果单步延迟明显变大说明batch太大回退一档。我这里的最佳点通常在64左右更大后TPOT开始上升体验变差。第三步按场景取舍。如果所有请求共享很长的系统提示词开启prefix caching能显著降低重复前缀的计算开销如果请求长度差异极大可以考虑按长度分开部署两个实例短文本实例把max-model-len设小腾出更多KV Cache空间给并发。这里多说一句vLLM的prefix caching会自动生效一部分但遇到动态batch被反复打断的场景效果会波动需要实测确认。5. 生产部署与踩坑实录那些文档里没写的事压测达标后剩下的就是把它变成一台7x24小时稳定运行的服务。这节内容基本是我踩过坑之后的总结价值不比上面的原理低。5.1 用Docker和systemd把它变成可靠服务本地机器上直接跑vllm serve当然没问题但生产环境建议用Docker隔离环境再用systemd守护保证崩溃后自动拉起。Docker启动示例docker run -d \ --name vllm-service \ --gpus all \ --shm-size 8g \ -p 8000:8000 \ -v /path/to/model:/models \ vllm-image \ vllm serve /models/your-model \ --host 0.0.0.0 \ --port 8000 \ --max-model-len 32768 \ --gpu-memory-utilization 0.9 \ --max-num-seqs 64--shm-size 8g很多人会漏掉容器默认的共享内存很小而大模型推理涉及的张量操作、DataLoader可能用到共享内存给够容量能少踩一个隐藏坑。再用systemd守护docker容器[Unit] DescriptionvLLM Inference Service Requiresdocker.service Afterdocker.service [Service] Restartalways ExecStart/usr/bin/docker start -a vllm-service ExecStop/usr/bin/docker stop vllm-service [Install] WantedBymulti-user.target这样即使服务因为某个请求直接崩溃systemd也会自动拉起容器不会等到第二天业务报障才发现服务挂了。5.2 我踩过的四个坑现象、根因、解决第一个坑是max-model-len设置过大启动后频繁OOM。当时把模型支持的最大上下文设满权重加KV Cache直接把显存顶到爆。根因就是KV Cache预留量跟max-model-len是线性相关的长度翻倍缓存也翻倍。解决方式是按业务实际需求设成32K甚至16K把省下来的显存留给并发。第二个坑是压测时偶发请求提前终止生成到一半连接断开。排查日志发现是某些请求触发到模型实现里的动态分支跟vLLM的CUDA graph缓存机制产生冲突某个batch形状不在已捕获范围内导致重启了一步重捕或者直接报错退出。解决方式是固定max-num-seqs避免batch大小来回变化稳定后就没再出现。第三个坑是量化模型启动失败。下载了AWQ量化版模型没传--quantization awqvLLM按默认格式去解析直接报格式不匹配。这类问题解决很简单参数名称对齐就行但文档里容易忽略报错信息又不太直白很容易多绕半小时。第四个坑是swap-space设置过高导致长尾延迟。当时为了保险把swap设置成32GB结果并发上去后KV Cache开始溢写CPU内存TTFT从200ms直接飙到几秒长尾惨不忍睹。后来把KV Cache控在显存内确保swap不触发延迟立刻恢复正常。swap是兜底机制不是并发扩展手段。5.3 监控与容量规划怎么判断它还能抗更多服务上线后我习惯盯三个信号第一启动日志里的GPU Cache状态。vLLM会输出当前KV Cache的总量和已用量这个数字比显存占用更直接地反映服务余量。第二TTFT的趋势。如果TTFT随着时间缓慢爬升说明排队在加剧离容量上限不远了如果TTFT平稳说明还有余量。第三请求队列深度。vLLM日志里有排队请求数量持续积压说明并发跑满要么扩容要么把业务侧限流。容量规划方面我的经验公式很简单先算单个长请求的KV Cache占用再乘以预期峰值并发加上模型权重和CUDA上下文的固定开销总和低于显卡总显存就基本安全记得留10%-15%余量给运行时波动。最后说点实际体会这套服务从第一天跑到现在给我的最大感受是vLLM不是银弹但它把本地高并发推理这件事的难度降低了一大截。短请求、小模型、低并发场景用更轻的方案或许更省事但只要你面对的是长上下文、高并发、需要稳定吞吐的本地服务vLLM的高吞吐机制确实能带来质变。最后分享一个小技巧如果你有多个不同模型需要服务不需要给每张卡单独找机器。在同一台GPU上跑多个vllm serve实例把gpu-memory-utilization按比例调低端口分开显存按模型大小切分就能一台机器同时服务多个模型。我目前就是一张卡上跑着两个模型实例其中一个负责长上下文对话另一个负责短文本向量化处理互不干扰显存利用率反而更高了。