
部署 vLLM 的人大概都经历过同一个场景镜像拉好了、权重下载完了、命令敲下去屏幕刷出几十行日志然后在一个ValueError或者torch.cuda.OutOfMemoryError上停住。你去搜索搜到的答案要么是半年前的版本、要么只说了调小 max_model_len却不告诉你为什么要调、调到多少合适。这篇内容就是把我在 vLLM 部署大模型过程中反复踩过的坑整理成一份 FAQ覆盖单机多卡、纯 CPU 模式、一个实例挂多个模型、模型架构没注册导致的ValueError以及 vLLM 和 SGLang、Ollama 之间到底该怎么选。不管你是刚拉起第一个vllm serve的新手还是已经在压测吞吐准备上线的老手里面应该都有几条能直接用上的。1. 报错日志的三段式读法先分清是参数、权重还是显存的问题很多人读 vLLM 日志的方式是从下往上找红色的字这在实际排查里效率极低因为最底下的那个异常往往只是最上层的表现真正的根因藏在中间某一行。我自己的习惯是先把日志按启动阶段切成三段判断报错发生在哪一段再决定往哪个方向查。这个方法不保证一次命中但能把排查范围从整个 vLLM缩小到某一个环节。1.1 启动流程决定了报错出现的位置vLLM 启动一个推理引擎大致会走这么几步解析命令行参数和config.json、下载或加载 tokenizer、按torch_dtype确定权重精度、加载模型权重到显存、探测显存余量并预分配 KV Cache、捕获 CUDA Graph、最后才是拉起 HTTP 服务并开始监听端口。这六步基本是串行的所以报错出现的位置天然就告诉了你问题在哪儿。如果在第一、二步就挂了通常和模型路径、config.json缺字段、tokenizer 文件缺失、chat_template没定义有关。这类错误的典型长相是OSError: ... does not appear to have a file named config.json或者ValueError: Tokenizer class ... does not exist。它的特点是GPU 显存占用几乎为零nvidia-smi上看不到进程吃显存。如果卡在第三步和第四步也就是权重加载阶段那基本就是精度不匹配或者单卡装不下。精度问题长这样TypeError: expected scalar type BFloat16 but found Float16或者反过来。装不下的表现是显存曲线一路爬升爬到一半突然掉下去然后抛torch.cuda.OutOfMemoryError。第五步之后报错的才是大家最熟悉的 KV Cache 分配失败典型信息是The models max seq len (32768) is larger than the maximum number of tokens that can be stored in KV cache (xxxx)。这一步报错其实是好消息因为模型本身已经加载成功了你只需要在参数上做减法。1.2 高频报错对照表与第一反应动作我把手边整理的一份对照表放出来左侧是日志里实际会看到的片段右侧是我遇到时第一件会做的事。这张表不是穷举但覆盖了日常八成以上的启动失败。日志关键片段大概率根因第一反应动作does not appear to have a file named config.json路径写错或权重没下完用ls确认目录里有config.json和*.safetensorsexpected scalar type BFloat16 but found Float16权重精度和dtype不一致显式指定--dtype bfloat16或--dtype float16max seq len ... larger than ... KV cacheKV Cache 装不下目标上下文降--max-model-len或升 TP 卡数torch.cuda.OutOfMemoryError出现在加载权重阶段单卡显存小于权重体积上--tensor-parallel-size或换量化权重CUDA error: no kernel image is available编译架构和显卡算力不匹配换预编译 wheel或按算力重新编译Address already in use端口被别的进程占了ss -lntp找占用进程换--portmodel class xxx not found模型架构没在当前版本注册见第 6 节完整排查链这张表比起记住所有报错更重要的是养成先归类再动手的习惯。我见过太多人一看到 OOM 就无脑加卡结果实际问题是--gpu-memory-utilization被设成了 0.98 导致连激活值的内存都不够。1.3 一个容易被忽略的习惯把日志落盘vllm serve默认是把日志打到 stdout 的容器里跑的话一旦重启报错就没了。我的做法是在启动脚本里加一层重定向同时把进程号、启动命令、时间戳一起记下来。这样事后追查昨天下午那次 OOM 到底是什么参数的时候不会抓瞎。#!/usr/bin/env bash set -euo pipefail TS$(date %Y%m%d-%H%M%S) LOG_DIR/var/log/vllm mkdir -p $LOG_DIR CMD(vllm serve /models/Qwen2.5-7B-Instruct \ --served-model-name qwen2.5-7b \ --tensor-parallel-size 1 \ --max-model-len 8192 \ --gpu-memory-utilization 0.90 \ --port 8000) echo CMD: ${CMD[*]} $LOG_DIR/launch-$TS.log exec ${CMD[]} $LOG_DIR/runtime-$TS.log 21注意--gpu-memory-utilization里的 0.90 不是显卡用到 90% 就停而是 vLLM 规划 KV Cache 时使用的总显存上限比例。这两件事差别很大第 2 节会专门算这笔账。另外一个细节是VLLM_LOGGING_LEVEL环境变量。默认级别在排查启动问题时信息量不够临时调到DEBUG能看到它到底选了哪个 attention 后端、哪套 CUDA Graph 尺寸很多玄学崩溃的线索就在这里。2. 显存账本gpu_memory_utilization 到底算的是什么显存问题占了 vLLM 使用问题的绝大部分而绝大多数显存问题的根源是没搞清楚 vLLM 把显存花在了哪几处。我见过有人把--gpu-memory-utilization从 0.9 调到 0.95 就以为能多塞 5% 的并发实际日志里 KV Cache 的 token 数只涨了不到 3%因为另外几份开销是不可压缩的。2.1 权重、激活、KV Cache、CUDA Graph 四份开销一张卡上的显存大致被分成四块。第一块是模型权重这部分是死的7B 的 bf16 权重大约 14GB 到 15GB70B 就得 140GB 上下。第二块是前向计算过程中的临时激活值和各种 buffer跟 batch 大小、序列长度正相关量不大但必须有。第三块是 KV Cache也就是 PagedAttention 管理的那一池 block它是唯一真正能被gpu_memory_utilization影响的大头。第四块是 CUDA Graph 捕获占用的显存在开启图捕获时会有几个 GB 的额外开销。所以gpu_memory_utilization 0.9的意思是vLLM 认为自己最多可以占用显卡总显存的 90%然后在这 90% 里先扣掉权重和峰值激活剩下的才切成 KV Cache 的 block。如果你还开了 LoRA、开了投机解码这些也都要从这 90% 里出。理解了这个顺序就明白为什么这个参数从 0.9 提到 0.95 收益有限——有限的增量里还要跟其它开销抢。一个很实用的日志行是它启动完成时打印的GPU KV cache size和Maximum concurrency for N tokens per request。后者直接告诉你在当前配置下N 长度的请求最多能同时跑多少个。这个数字比任何理论推算都可信我调参基本就盯着它。2.2 从 max_model_len 和并发数倒推 KV Cache 需求KV Cache 的一个显存估算思路是拿公式粗算再把结果和日志里的实际值比对。单 token 的 KV Cache 量大致等于2 × 层数 × KV 头数 × 头维度 × 每元素字节数。以常见的 7B 级模型为例28 层、KV 头数 4GQA、头维度 128、bf16 每元素 2 字节单 token 就是 2 × 28 × 4 × 128 × 2 ≈ 57KB。这个数乘上max_model_len再乘上你想要的并发数就是 KV Cache 的最低需求。举个具体的账如果max_model_len 8192想同时跑 30 个请求那么需要 57KB × 8192 × 30 ≈ 14GB 的 KV Cache 空间。一张 24GB 的卡上放着 15GB 权重扣掉激活和 CUDA Graph能拿出来的 KV Cache 往往只有 4GB 到 6GB。这就是为什么很多人在 24GB 卡上跑 7B、上下文开 32K 会直接失败——不是权重放不下是 KV Cache 放不下。降低这个需求有三个旋钮降max_model_len、开--kv-cache-dtype fp8把每元素字节数压到 1、上多卡把权重摊开从而腾出更多显存给 KV。前两个都不改模型精度是性价比最高的选择。fp8 的 KV Cache 会带来一点点精度损失实测在多数对话任务上肉眼看不出差别长文本严格对齐任务的场景就需要自己验证一下。2.3 几个参数的真实取舍与实测对照下面是我在一台双卡 24GB 机器上跑同一个 7B 模型的几组配置实测数字来自启动日志只作为量级参考不同模型差别会很大。max_model_lenkv-cache-dtypegpu-memory-utilization日志报告的最大并发该长度32768auto0.90启动失败KV Cache 不足32768fp80.90约 1.9x8192auto0.90约 12x8192auto0.85约 10x4096auto0.90约 24x从这张表能看出两个反直觉的点。第一从 32768 降到 8192并发数是按比例放大的说明在权重固定的前提下 KV Cache 池子不变序列长度和并发数就是纯粹的除法关系。第二gpu-memory-utilization从 0.9 降到 0.85并发只掉了两成左右说明那 5% 里确实还塞着别的东西。所以我一般不会为了省一点点显存把它调到 0.8 以下除非遇到了别的进程要共卡。提示显存非常紧张的时候--enforce-eager能省掉 CUDA Graph 那部分开销代价是 decode 阶段吞吐下降。这个取舍在显存刚好卡在临界点时很值得——能跑起来比跑得快重要。2.4 值得单独盯一眼的 max_num_batched_tokens很多人不知道--max-num-batched-tokens也会影响显存峰值。它控制的是单次前向里最多调度多少 token值大则批处理效率高、吞吐好但激活值的峰值显存也会涨。默认值通常是max_model_len或者某个按场景推导出来的数我遇到过的启动不报错、一加压就 OOM里有一半是这个参数偏大导致的。处理方式很简单把它设成小于等于max_model_len的一个值比如长上下文服务里设成 8192 甚至 4096。代价是长 prompt 会被切分调度首 token 延迟略微上升换来的是显存曲线平缓很多不会因为某一个超长请求进来就把整个池子顶爆。3. 单机多卡张量并行不是加卡就变快单机多卡是 vLLM 最常被问到的话题之一也是最容易出现卡加了但没变快甚至更慢的地方。核心原因在于 vLLM 的多卡是张量并行TP它把每一层的矩阵乘按维度切开分给不同卡每层算完都要做一次 all-reduce 通信。通信开销和计算开销在同一个数量级这就决定了网内带宽是硬约束。3.1 张量并行的切分条件与踩雷点TP 能不能开第一个前提是模型的注意力头数能被 TP size 整除。上面那个 GQA 例子里 KV 头数是 4那--tensor-parallel-size只能取 1、2、4。有人拿 8 卡去跑一个 KV 头数为 4 的模型结果报错说头数不能整除就是这个原因。这个限制在 GQA、MQA 模型上比传统 MHA 模型更容易撞到因为 KV 头数本身就被设计得很小。第二个前提是隐藏维度也要能整除否则 vLLM 会在加载时直接报 shape 不匹配。这类错误的信息很直白一般会明确告诉你哪个 tensor 的维度对不上按提示调整 TP size 即可。第三个最容易踩的是权重文件的切分方式。有些模型的 checkpoint 是按 TP8 预先切好的你拿 TP2 去加载vLLM 能找到文件但顺序对不上结果就是输出完全乱码却没有任何报错。这种问题特别隐蔽我的排查办法是拿一个固定问题跑两次不同的 TP 配置对比输出是否一致不一致就说明加载环节有问题。3.2 流水线并行与分布式后端的组合约束当模型大到单机装不下就需要考虑--pipeline-parallel-size。它把模型按层切成若干段每段放一组卡上数据在组间传递通信量比 TP 小得多但会引入流水线气泡吞吐和延迟都会受影响。TP 和 PP 可以组合使用总卡数是tp × pp并且要求节点拓扑合理。这里有个坑PP 对分布式后端有要求很多情况下需要走 Ray。直接用默认的多进程后端会在启动时报后端不匹配。我的建议是单机场景优先用 TP跨机场景才上 TPPP并且提前把ray start --head跑起来、确认节点都注册上了再启 vLLM。Ray 集群本身的状态问题会伪装成 vLLM 的问题比如卡在等待 worker 就绪十分钟然后超时实际是某个节点的 Ray 没连上。注意多机部署时 NCCL 相关的超时几乎都和网络有关而不是模型配置。看到NCCL timeout先查网卡、MTU 和防火墙规则别急着改 vLLM 参数。3.3 通信带宽决定了你该不该上多卡这张表是我自己判断要不要上多卡的粗略标准按卡间互联方式分类。卡间互联大致带宽量级跑 TP 的实际感受NVLink / NVSwitch数百 GB/sTP2 到 8 都有明显收益PCIe 4.0 x16 直连数十 GB/sTP2 勉强能接受TP4 开始不明显PCIe 走 PCIe Switch带宽进一步共享TP 基本不划算不如换单张大显存卡结论很朴素如果两张卡之间没有高速互联那么 TP2 的收益常常跑不过直接把模型量化后塞进一张卡。我做过一组对比同样的 7B 模型PCIe 环境 TP2 的吞吐一度比单卡 bf16 还低因为通信把省下来的时间吃掉了。这不是 vLLM 的问题是硬件物理规律。4. 一个服务挂多个模型LoRA 复用与多实例并行两种路线一个实例服务多个模型这个需求背后通常有几种不同动机有的是多个微调版本要同时对外提供、底座相同有的是几个完全不同的模型要共存还有的是想做 A/B 实验。这三种动机对应的方案完全不一样选错了会白白浪费卡。4.1 LoRA 适配器共享底座的最省卡方案如果多个模型其实是同一个底座的 LoRA 微调版本那 vLLM 的 LoRA 支持就是最省显存的做法。只加载一份底座权重多个 LoRA 适配器按需动态换入换出显存增量基本只有适配器本身的大小。配置方式是在启动时把所有适配器登记上。vllm serve /models/Qwen2.5-7B-Instruct \ --served-model-name base-model \ --enable-lora \ --max-lora-rank 64 \ --max-loras 4 \ --max-cpu-loras 8 \ --lora-modules \ adapter-a/models/lora-a \ adapter-b/models/lora-b请求侧只要把model字段写成adapter-a就会走对应适配器写base-model就走底座。几个参数里--max-lora-rank要覆盖你所有适配器里最大的 rank填小了会加载失败--max-loras是同时驻留显存的适配器数量超出的会被换到 CPU 上--max-cpu-loras控制 CPU 侧能缓存多少。有个实际经验如果 LoRA 切换频繁把--max-loras设成等于适配器总数、让它全部常驻显存比来回换入换出快得多代价是显存多占一点。反过来如果适配器数量远大于并发请求的多样性就让它按需切换省显存。提示LoRA 只能在支持的模型结构上启用一些特殊架构尤其在多头注意力实现上有定制的模型不支持。启动时报LoRA is not supported for this model时不要反复试参数直接换多实例方案。4.2 多实例部署端口、显存与路由的分配如果几个模型底座都不同或者其中一个不支持 LoRA那正确做法是每个模型起一个独立进程用CUDA_VISIBLE_DEVICES做物理隔离。这样虽然每个实例都有自己的权重副本但互不干扰一个崩了不影响另一个。CUDA_VISIBLE_DEVICES0 vllm serve /models/model-a \ --served-model-name model-a --port 8000 \ --gpu-memory-utilization 0.85 CUDA_VISIBLE_DEVICES1 vllm serve /models/model-b \ --served-model-name model-b --port 8001 \ --gpu-memory-utilization 0.85 两个细节值得说。第一--gpu-memory-utilization别给到 0.9因为同一张卡上如果还有别的进程比如监控、其它小实例留出余量能避免互相挤爆。第二多实例前面一般要挂一层网关做路由Nginx 或者一个轻量 Python 反代都行把/v1/chat/completions按model字段分发到不同端口对客户端表现得像一个服务。4.3 选型判断的几条经验线我的判断逻辑很简单按下面的顺序问自己这些模型的底座是不是同一个是的话优先 LoRA。底座不同但总显存够放多个副本吗够就多实例。显存不够放多个副本有没有一个模型能覆盖 90% 的请求有的话就只部署它剩下的走降级。都不满足才考虑量化后共存并接受精度和性能的损失。第三条经常被忽略。实际业务里我必须同时提供四个模型的情形远比想象中少更多是我以为我需要对四个模型都提供在线服务。先把访问量统计拿出来看一眼往往能省下好几张卡。5. 纯 CPU 模式跑 vLLM先想清楚你到底图什么纯 CPU 模式是另一类高频问题。常见的动机是手边只有 CPU 服务器、想在没有显卡的环境里验证流程、或者合规上不允许用 GPU。我的态度比较明确——vLLM 的 CPU 后端是能跑的但它不是为 CPU 优化的如果你追求的是能在 CPU 上跑起来做个小工具有比它更合适的选择。5.1 CPU 后端的硬件门槛与编译要求vLLM 的 CPU 后端对指令集有要求基本需要 AVX512 级别部分版本对 AMX 有额外支持。在只有 AVX2 的老机器上要么根本装不上要么能装上但速度慢到不可用。所以第一件事是先确认 CPU 指令集lscpu里看 flags 有没有avx512f。第二个门槛是安装方式。CPU 后端在很多版本里不在默认的 wheel 里需要从源码编译或者装专门标注 CPU 的包。编译过程对编译器和 PyTorch 版本有要求报错通常发生在cmake阶段。我的做法是先创建干净虚拟环境、装好和 vLLM 版本匹配的 CPU 版 PyTorch再编译别在已有的一堆依赖里硬混。5.2 环境变量与参数怎么配CPU 模式和 GPU 模式在参数上有几处显著差别。export VLLM_CPU_KVCACHE_SPACE40 export VLLM_CPU_OMP_THREADS_BIND0-31 vllm serve /models/Qwen2.5-1.5B-Instruct \ --device cpu \ --dtype bfloat16 \ --block-size 128 \ --max-model-len 2048 \ --max-num-seqs 8逐个说。VLLM_CPU_KVCACHE_SPACE是给 KV Cache 预留的内存单位 GiB它在 CPU 上的地位相当于 GPU 上的gpu_memory_utilization必须显式设置。VLLM_CPU_OMP_THREADS_BIND控制 OpenMP 线程绑定到哪些核绑对了性能差别很明显绑错了会互相抢核。--block-size在 CPU 上一般要调大。CPU 后端做内存管理时block 太小会导致管理开销占比过高128 是我用下来比较平衡的值。--dtype bfloat16通常比 float16 快因为它的动态范围和 CPU 上的指令支持通常更好。--max-num-seqs建议保守CPU 上并发多了只会让每个请求都变慢总吞吐未必提升。5.3 什么场景该放弃 vLLM 换别的方案CPU 模式下我实测的生成速度是个位数的 token/s量级上和 GPU 差两三个数量级。这意味着它只能用于两类场景离线批处理不在乎单条延迟和功能验证跑通就行。如果你的真实需求是在普通电脑上跑一个能对话的本地模型那 llama.cpp 系或是基于它的 Ollama 这类工具会更合适它们在 CPU 上的量化推理优化投入大得多内存占用也友好。反过来如果你要在 CPU 上做离线批量标注、处理几十万条数据、跑一晚上也无所谓vLLM 的连续批处理优势还是能体现出来的因为它能把并发调度做满。注意不要把 GPU 环境的内存参数直接搬到 CPU 环境。CPU 上VLLM_CPU_KVCACHE_SPACE给太大会直接把机器内存吃光触发系统 OOM Killer表现是进程被无声杀掉、日志里什么异常都没有。这个现象我第一次遇到时以为是段错误查了很久。6. ValueError: model class not found模型架构没注册的完整排查链ValueError: Model class xxx not found这一类报错出现在比较新的模型上特别频繁比如带着modularpipeline这种命名后缀的架构。它的本质很简单模型config.json里的architectures字段写了一个名字而当前安装的 vLLM 版本里没有注册这个名字对应的实现类。下面是我完整的排查链路。6.1 架构名到模型类的映射是怎么走的vLLM 启动时读config.json取出architectures数组里的第一个名字然后去自己的模型注册表里查。这个注册表在包内的model_executor/models/registry.py里维护是一个从架构名到 Python 类的映射。查到了就实例化查不到就抛你看到的那句ValueError。这里有个命名转换约定值得知道架构名和类名之间是有规律对应的。比如Qwen2ForCausalLM这类名字在注册表里既能按原名匹配也能按某种归一化后的形式匹配把驼峰转成小写下划线之类。像MiniMaxH3ModularPipeline这种带Pipeline或Modular后缀的名字往往来自新版模型仓库引入的模块化建模方式老版本 vLLM 的注册表里根本没有这个键于是报错。6.2 从报错到定位的四步排查第一步确认报错里的名字和config.json里的是不是完全一致。有时候是自定义模型仓库里architectures写了个手误的名字这种情况下升级 vLLM 永远解决不了问题。直接打开config.json比对字符串注意大小写。第二步确认当前 vLLM 版本。vllm --version拿到版本号然后去对照该模型官方文档要求的 vLLM 最低版本。新模型发布时通常会在模型卡里明确写出需要 vLLM x.y.z。如果版本低了升级就是唯一解。第三步确认是不是需要--trust-remote-code。有一部分模型把建模代码放在自己的仓库里就是那些.py文件需要在启动时显式允许加载远程代码。加上这个参数后再试一次很多时候就过了。但要清楚这意味着你在执行别人仓库里的代码涉及生产环境要做代码审查。第四步确认模型是不是压根不在支持列表里。有些架构在某个版本之后才被纳入也有些架构只在特定分支上支持。这时候升级到最新版、或者切换到 nightly 版本的 wheel 往往能解决。四步走完还是报同样的错那基本可以确定当前版本确实不支持这个架构需要等上游合并或者走自定义实现的路子。6.3 升级、trust_remote_code 与自写插件的边界升级是最省事的但生产环境升级有成本尤其是跨小版本时参数默认值可能变、某些参数可能被废弃。我的做法是先在测试机上用同一个模型把升级后的行为跑一遍重点看三件事日志里报告的最大并发有没有变化、同样的输入输出是否一致、以及之前用到的命令行参数是否还都合法。--trust-remote-code属于能用但要知道自己在做什么尤其是从第三方来源拿到的模型仓库。至少要扫一眼仓库里的.py文件有没有奇怪的网络行为或文件写入。最后是自写插件。vLLM 支持通过插件机制注册外部模型实现原理就是往那个注册表里加键。这条路适合团队内部有长期维护能力的场景比如你司自研了一个模型架构、需要长期在 vLLM 上跑。做法是在一个独立的包里实现模型类并暴露register()入口用vllm.general_plugins的 entry point 注册进去这样不用改 vLLM 源码升级 vLLM 时也不会冲突。提示自写实现最容易出的问题不是模型结构本身而是权重名映射。模型官方的state_dict键名和 vLLM 期望的键名不一致时加载会成功但输出是噪声。写完实现后一定用少量输入对比原始实现的输出逐层对齐不要只看能不能加载。7. vLLM、SGLang 与 Ollama 的能力边界怎么划这三个经常被放在一起比较但它们的定位差异其实很大。vLLM 和 SGLang 都是面向服务端高吞吐的推理引擎Ollama 更偏向本地一键跑起来的易用性工具。搞混定位就会得出Ollama 比 vLLM 慢所以不好这种没什么意义的结论。7.1 三者在吞吐、延迟与易用性上的差异维度vLLMSGLangOllama主要定位服务端批量推理服务端批量推理强在结构化生成本地单机便捷使用连续批处理成熟PagedAttention成熟RadixAttention有限前缀缓存支持需开启原生强项自动复用前缀有限复杂结构化输出一般约束解码是强项一般部署复杂度中等中等偏高很低量化支持AWQ / GPTQ / FP8 等覆盖较广GGUF 为主适合谁需要自建推理服务的团队多轮对话、Agent 场景个人本机体验差异最本质的一条是缓存策略。SGLang 的 RadixAttention 把前缀复用做进了调度核心多轮对话里历史部分不重复计算这个设计在 Agent 类负载同一段 system prompt 反复出现、多轮追加上收益非常明显。vLLM 也有前缀缓存用--enable-prefix-caching打开效果在多请求共享长 system prompt 时也能看到只是默认不开很多人不知道要手动打开。Ollama 的优势在于门槛。一条命令拉起、模型自动管理、对 CPU 和 Mac 的统一内存支持友好做原型验证或者个人使用体验最好。它的弱项是并发设计目标就不是给你几十路并发打满的。7.2 典型场景的选型建议我的建议按场景分三类在线服务、并发几十路以上、需要自建 API。选 vLLM 或 SGLang。两者都能做SGLang 在多轮对话和需要强制 JSON Schema 输出的场景更省心vLLM 在模型覆盖广度、社区生态和部署资料丰富度上更稳。选哪个很多时候取决于你的模型在哪个引擎上支持得更好。离线批处理、几万条数据、追求单位时间处理量。两个都可以压一把再决定。这时候差异主要来自你的数据形态如果请求之间的 prompt 有大量公共前缀SGLang 的优势会被放大如果请求彼此独立、长度均匀两者差距会缩小到百分之十几以内。个人本机、想快速试模型、单机单用户。直接用 Ollama 这类工具别折腾 vLLM。为了一个人的体验去配一套服务端引擎投入产出比不划算。还有一类场景是从 Ollama 迁移到 vLLM 的。迁移时最容易忽略的是 chat template。Ollama 在模型包里帮你打理好了模板vLLM 需要从 tokenizer 配置里读如果模型仓库没带chat_template就要用--chat-template手动指定一个 jinja 文件。模板不对的表现是模型回答格式混乱、把角色标记当正文吐出来看起来像模型变笨了其实是模板问题。8. 上生产前必须压一遍的几件事把服务跑起来只是第一步上生产前还有几件事必须做。这部分是我从几次线上抖动里总结出来的都属于文档里不太会写、但踩一次就记一辈子的类型。8.1 压测要看的不是 QPS 而是这几条曲线很多人压测只看 QPS 一个数这不够。vLLM 暴露了 Prometheus 指标端点/metrics里这几条才是真正反映健康度的vllm:num_requests_waiting排队中的请求数。持续大于零说明调度已经饱和再压只会让延迟线性上涨。vllm:gpu_cache_usage_percKV Cache 使用率。长时间贴着 90% 以上说明离抢占不远了。vllm:time_to_first_token_seconds首 token 延迟。这是用户体感最敏感的一条。vllm:request_queue_time_seconds排队等待时间和第一条互相印证。日志里的Preemption字样一旦出现抢占说明显存不够服务开始丢弃并重算请求的历史 KV延迟会出现尖刺。我的做法是压到吞吐曲线拐点就开始关注num_requests_waiting它开始持续增长的那个并发数就是这套配置的实际服务上限。往上压到 OOM 之前你已经没有可用的余量了。8.2 常见线上抖动的原因归类线上跑一段时间后出现的性能抖动通常归到这几类KV Cache 碎片导致的抢占。长短请求混跑时长请求占着 block 不放短请求被挤到抢占。表现是 P99 延迟周期性飙高。处理方式是把长请求分流到独立实例或者用--max-num-seqs限制单批规模。显存被别的东西偷走。同一个节点上有别的进程在跑或者监控 agent 占了一点。表现是运行几天后突然 OOM。处理方式是给 vLLM 容器设显存可见范围把--gpu-memory-utilization留出 5% 到 10% 的余量。请求长度分布超出预期。你按平均 500 token 压测通过了线上来了一个 30000 token 的请求直接把批次顶爆。处理方式是设--max-model-len加校验服务端在入口处就把超长请求拒掉而不是让它进引擎再失败。健康检查把它自己搞挂。有些探针配置成高频请求完整推理接口结果探针流量占了实际算力。正确做法是探/health那个端点很轻。8.3 一些配置模板与个人收尾经验最后把一份我觉得比较稳的启动模板放这儿适合单机 8 卡跑一个 70B 级的模型参数我按前面几节的逻辑注释过。vllm serve /models/your-model \ --served-model-name your-model \ --tensor-parallel-size 8 \ --dtype bfloat16 \ --max-model-len 16384 \ --max-num-batched-tokens 8192 \ --gpu-memory-utilization 0.88 \ --kv-cache-dtype fp8 \ --enable-prefix-caching \ --swap-space 8 \ --disable-log-requests \ --api-key $VLLM_API_KEY \ --port 8000几点说明。--swap-space是 CPU 侧的交换空间KV Cache 不够时把部分 block 换出去能缓解抢占但会增加延迟我一般给 8GiB 图个保险。--disable-log-requests在生产环境必开否则每个请求都打一行日志高并发下日志本身就成了瓶颈。--api-key一定要设默认无鉴权的话只要端口暴露出去谁都能调。关于--kv-cache-dtype fp8需要提醒一句在部分版本上它需要配合--calculate-kv-scales使用否则精度可能出问题升级版本后这两个参数的搭配关系可能变化上线前用一小批有标准答案的样本验证一下输出质量比任何理论分析都靠谱。还有一条我很想强调的经验每次改配置只改一个参数。我早期调优的坏习惯是一次改三四个参数结果跑得快了不知道是谁的功劳出问题了也不知道是谁的锅。后来强制自己一次只动一个虽然慢但每一次调整都能沉淀成确定的经验不会再出现上周明明能跑这周怎么不行了的情况。这个习惯比任何一个具体的参数值都值钱。如果你是从 GPU 环境切到 CPU 环境、或者从单模型切到多模型上面的结论要重新验证一遍因为瓶颈会换位置——GPU 上的瓶颈是显存带宽和 KV CacheCPU 上的瓶颈是内存带宽和核数。同一套参数换环境直接用出问题的概率远大于出效果的概率。