
前段时间帮一个团队把内部问答服务从原生 transformers 的generate()迁到 vLLM底座模型换成了 Yuan2.0。迁移前的状态很典型一张卡跑单条请求首 token 要等两三秒GPU 利用率常年趴在 20% 上下并发一上来就排队。迁完之后同样的卡并发 64 的情况下整体吞吐翻了七八倍首 token 延迟压到了几百毫秒。这套组合我前后调了两周参数、显存、模板、压测每一环都踩过坑索性把过程完整记下来。这篇内容讲的就是 vLLM 部署 Yuan2.0 这件事为什么选这个组合、权重和环境怎么准备、启动参数怎么算、业务侧怎么接、压测数据怎么看、出问题怎么查。适合两类人看——手上已经有一台带显卡的机器、想把 Yuan2.0 真正跑成服务后端的朋友以及已经用过 vLLM 但换成国产模型后各种报错、吞吐上不去的朋友。文中所有参数都给了推导过程你可以直接抄命令也可以按自己的模型 config 重新算一遍。1. 选型拆解为什么是 vLLM 配 Yuan2.01.1 从“本地能跑”到“线上能扛”的三道坎很多人第一次跑通大模型是在 Notebook 里写十几行 transformers 代码model.generate()一调屏幕上开始一个字一个字往外蹦那一刻成就感很足。但这套东西离“服务”差得远中间横着三道坎我一般会按这三条去判断一个方案值不值得上。第一道是吞吐坎。原生generate()基本是单请求串行推理一次只服务一个人。GPU 的算力其实大量闲置——解码阶段每生成一个 token 都要把整个权重从显存读一遍但只算了一个 token 的活算力单元大部分时间在等数据。这就是为什么单条请求时 GPU 利用率只有百分之十几。想提高利用率就得批处理可手写动态 batching 非常麻烦请求随时来随时走、长度参差不齐、要处理超时和取消。第二道是显存坎。自回归生成的 KV Cache 大小跟“已经生成到第几个 token”线性相关事先不知道每个请求会聊多长。常见做法是按最大长度预分配一条 8K 上下文的请求能吃掉好几个 G结果就是显存被预留光了实际只用了三成能同时服务的请求数被死死卡住。第三道是工程坎。线上要的是 HTTP 接口、并发、流式输出、指标暴露、优雅退出而不是一个 Python 脚本。这三道坎不解决模型再强也只是个演示品。1.2 PagedAttention 与连续批处理到底省了什么vLLM 之所以在这三道坎上都有解核心就两个东西PagedAttention 和连续批处理continuous batching。理解这两点后面所有参数调优你都能自己推。PagedAttention 的思路借的是操作系统虚拟内存分页。传统做法要求每个请求的 KV Cache 在显存里连续存放就像早期程序要求物理内存连续分配一样必然产生大量外部碎片。PagedAttention 把 KV Cache 切成固定大小的 block默认 16 个 token 一块块与块之间不需要连续用一张“块表”记录逻辑位置到物理块的映射。带来的直接好处是显存浪费从“平均浪费一半”降到“最多浪费一个块”。我打个生活化的比方老办法是给每位客人预留一整张长桌人走了桌子也空着新办法是按座位号发牌谁来谁坐翻台率立刻上去了。连续批处理解决的是“批次僵化”问题。静态 batching 必须等一整批里最长的那个请求结束其他短请求早就算完了也得干等GPU 空转。连续批处理是在每个解码步重新组批谁算完了立刻退出、空出的槽位马上给新来的请求填上。这两者叠加效果是显存利用率上去 → 能塞的并发上去 → 单卡吞吐上去而且是数量级的变化。注意PagedAttention 省的是“碎片浪费”不是“总量”。KV Cache 该占多少显存还是占多少只是不再浪费。所以后面算显存预算时仍然要老老实实按 token 数乘出来。1.3 Yuan2.0 的模型特性对推理框架提出了什么要求Yuan2.0 不是一个单点模型而是一个系列从 2B 的小尺寸到 51B、102B 的大尺寸还有一个 MoE 结构的版本。不同尺寸对框架的要求完全不一样这一点在选型时容易被忽略。小尺寸的 2B 模型在单张 24G 卡上就能跑得很舒服重点在并发和延迟50B 以上就必须上张量并行重点变成多卡通信和显存切分MoE 版本又是另一回事vLLM 对 MoE 的支持在不同版本里差异明显有些版本能跑但性能不好有些直接不认。所以在动手之前先确认三件事你的模型 config 里architectures字段写的是什么、num_hidden_layers和num_key_value_heads是多少、词表大小是多少。Yuan2.0 还有两个特殊点必须提前知道。一是它带自定义的模型结构代码config.json里通常会有auto_map指向仓库里的modeling_*.py所以启动时必须加--trust-remote-code否则框架压根不认识这个结构。二是它的对话模板用的是自己的分隔符体系不是 Llama 那套s、[INST]如果直接把 Llama 的模板套上去模型输出会变得很奇怪——不报错但答非所问这个坑我见过不止一次。2. 环境与权重准备把地基打平2.1 硬件、驱动与 CUDA 版本怎么配先说硬件。我按显存给一个大致参考这些都是按 bf16 权重、常规上下文长度估的实际要按你自己的 config 微调。模型规模权重显存bf16建议卡型备注2B 级别约 4-5 GB单卡 24G单卡可跑重点调并发7B-13B 级别约 14-26 GB单卡 48G 或双卡 24G单卡 24G 需量化或缩短上下文50B 级别约 100 GB4 卡或 8 卡 80G必须张量并行MoE 版本视激活参数而定多卡 80G先确认 vLLM 版本是否支持驱动和 CUDA 这块我的建议是别追求最新。vLLM 对 torch 和 CUDA 的版本比较敏感一个常见的翻车场景是驱动很新装了个最新的 torch结果 vLLM 编译的自定义算子跟 torch 的 ABI 对不上启动时报一堆undefined symbol。稳妥的做法是先定 vLLM 版本再按它的依赖反推 torch最后确认驱动支持这个 CUDA 版本。我习惯直接用官方提供的容器镜像起步镜像里 torch、CUDA、vLLM 都是配好的一套跑通了再考虑要不要自己装。自己装的时候务必把pip freeze的结果存一份后面换机器、扩容全靠它复现。提示如果你是混合精度老卡比如某些不支持 bf16 的型号记得显式指定--dtype float16。默认自动推断有时会选到 bf16然后启动到一半报不支持的数据类型。2.2 Python 依赖的版本锁定策略依赖版本是这类项目最容易出问题的地方因为链路很长vLLM → torch → transformers → tokenizers → 模型自定义代码。任何一环版本不匹配表现可能是启动失败也可能是更难受的——“能启动但输出是乱码”。我的做法是锁三层。第一层锁 vLLM 和 torch 的主次版本比如vllm0.6.x配对应 CUDA 的 torch 2.4/2.5这个组合是经过大量人验证的。第二层锁 transformers这一层要特别注意Yuan2.0 的自定义 modeling 文件往往依赖 transformers 里某些内部 API版本太新这些 API 可能被改名或移除报ImportError或者AttributeError。第三层锁 tokenizers因为自定义 tokenizer 类的行为和底层 tokenizers 库版本有关。具体操作上我一般先建一个干净虚拟环境装 vLLM它会自动带出匹配的 torch然后单独pip install transformers4.x.y指定版本最后跑一个最小推理用例验证输出是否通顺。这个最小用例很重要随便问一句“介绍一下你自己”看返回的中文是否连贯、有没有重复字符、有没有直接吐特殊符号。如果这一步不过关后面所有调优都白搭。还有一种情况模型仓库自带的requirements.txt里写的版本和你装的冲突。这时候别急着降级全局包先看清楚它到底需要什么——很多时候它只是为了跑训练脚本推理根本用不上。缺什么补什么能不动的尽量不动。2.3 权重落盘、校验与目录结构权重下载这一步看似无聊但线上出问题经常出在这。我建议按固定结构放别一股脑丢在家目录/models/ Yuan2-2B/ config.json configuration_yuan.py modeling_yuan.py tokenization_yuan.py tokenizer_config.json special_tokens_map.json model-00001-of-00002.safetensors model-00002-of-00002.safetensors model.safetensors.index.json放好之后做三件事。第一确认所有分片都在数量跟model.safetensors.index.json里的weight_map对得上少一个分片的表现是加载到一半报 key 找不到。第二确认.py文件齐全自定义结构模型必须把 modeling、configuration、tokenization 三类文件都放在模型目录里--trust-remote-code才会去加载它们。第三看一下磁盘剩余空间加载 safetensors 时框架可能会做临时转换留出权重体积 1.5 倍的空间比较保险。还有个小细节文件权限。如果服务是以非 root 用户跑的模型目录的读权限要提前给到否则会报Permission denied而这个报错信息经常被前面的日志刷掉容易误判成别的问题。3. 启动参数与吞吐调优一组能抄的命令3.1 最小可用启动命令与验证先给一版最小可用命令2B 级别模型、单卡跑通了再往上加复杂度python -m vllm.entrypoints.openai.api_server \ --model /models/Yuan2-2B \ --trust-remote-code \ --served-model-name yuan2-2b \ --dtype bfloat16 \ --max-model-len 8192 \ --gpu-memory-utilization 0.90 \ --max-num-seqs 128 \ --port 8000 \ --disable-log-requests几个参数先说结论--trust-remote-code对 Yuan2.0 是必须的不加直接启动失败--served-model-name决定客户端model字段填什么给个好记的名字别用完整路径--disable-log-requests在高并发下很关键默认每条请求都打日志QPS 一高日志 IO 本身就成了瓶颈。启动过程会打印一段关键信息一定要读模型加载耗时、KV Cache 的 block 数量、可支持的最大并发 token 数。这段日志是后面调优的基准线。如果日志里出现GPU blocks: xxx记住这个数它就是你能用的 KV Cache 总量除以每个请求需要的 block 数就是理论并发上限。启动完先做一次健康检查curl -s http://127.0.0.1:8000/health curl -s http://127.0.0.1:8000/v1/models第二个接口会返回模型列表能看到yuan2-2b就说明服务起来了。别急着实测对话先确认这两步能省掉很多“到底是服务没起还是请求写错”的扯皮。3.2 显存预算手算KV Cache 到底吃多少这是我认为最该掌握的一项技能因为所有并发相关的参数都从它推出来。公式不复杂每 token 的 KV Cache 字节数 2 × 层数 × KV 头数 × 每头维度 × 数据类型字节数前面的 2 是因为要存 Key 和 Value 两份。注意这里用的是num_key_value_heads而不是num_attention_headsYuan2.0 部分尺寸用了 GQA分组查询注意力KV 头数远小于注意力头数这一点如果搞错算出来的显存会大好几倍。举个示例假设你的 config 里num_hidden_layers32、num_key_value_heads8、head_dim128、bf16 每元素 2 字节每 token 每层2 × 8 × 128 × 2 4096 字节 4 KB32 层合计128 KB / token一条 4096 token 的请求512 MB一条 8192 token 的请求1 GB再算总量。单卡 80G--gpu-memory-utilization 0.90意味着框架最多用 72 GB减去权重2B 的 bf16 约 4 GB、激活值、CUDA 上下文开销剩下大概 60 GB 给 KV Cache。按每条请求 4096 token、512 MB 算理论上能同时挂 120 条左右。这个数就是你的并发天花板。如果--max-num-seqs设得比它高多出来的请求会在调度层排队而不是真正并行设得比它低显存就浪费了。我一般的做法是把max-num-seqs设到理论值的八成留点余量应对长度波动。提示上面的 32 层、8 个 KV 头、128 维都是示例数字务必打开你自己模型的config.json把真实值代进去。算错一遍后面所有调优都是在错误基础上微调。3.3 关键参数逐条拆解参数不多但每一个都值得说清楚。参数常用值作用与取舍--max-model-len4096 / 8192单个请求最大上下文。设太大会压缩并发设太小会截断长对话--gpu-memory-utilization0.85-0.92框架可占用的显存比例。太贪心会和别的进程抢显存--max-num-seqs按 KV 预算算同时处理的请求数上限。超过会排队--max-num-batched-tokens2048-8192单步组批的 token 上限影响 prefill 和 decode 的平衡--block-size16KV Cache 分块大小。默认 16 通吃特殊场景才调--enable-chunked-prefill开启长 prompt 分块预填充避免单条长请求卡住所有人--swap-space4-8 GBCPU 交换空间显存打满时的兜底--tensor-parallel-size1 / 4 / 8张量并行卡数大模型必填--max-model-len是最容易设错的一个。很多人图省事直接填模型的标称上限结果发现并发掉得厉害。原因很直接vLLM 会按这个长度去规划 KV Cache 的上限设 32K 意味着单条请求理论上能占 4 GB调度器就不得不保守。实际业务里如果绝大多数对话都在 2K 以内把max-model-len降到 4096 反而能让并发翻好几倍。--enable-chunked-prefill我建议默认开。不开的话一条 8K 的长 prompt 在做 prefill 时会独占计算资源这段时间里所有解码中的请求都被卡住表现为“前面聊得好好的突然集体卡两秒”。开了之后长 prompt 被切成小块和其他请求交替执行尾延迟明显改善。--gpu-memory-utilization别设到 0.95 以上。留一点余量给显存碎片和临时张量否则跑一段时间后可能在某个峰值请求上直接 OOM。我吃过一次 0.95 的亏压测跑了四十分钟挂掉排查了半天才发现是这个。3.4 多卡张量并行的取舍模型上到 50B 级别单卡放不下权重必须开张量并行TP。这时候要明白一件事TP 不是免费的。每一层的前向计算后都要做一次 all-reduce 同步卡越多通信开销越大。所以 TP 从 1 加到 2 通常吞吐接近翻倍从 4 加到 8 的收益就明显衰减了。python -m vllm.entrypoints.openai.api_server \ --model /models/Yuan2-51B \ --trust-remote-code \ --served-model-name yuan2-51b \ --dtype bfloat16 \ --tensor-parallel-size 4 \ --max-model-len 4096 \ --gpu-memory-utilization 0.90 \ --max-num-seqs 64 \ --enable-chunked-prefill \ --port 8000几个注意事项。第一TP 数必须是能整除注意力头数的值比如 32 个注意力头TP 只能取 1、2、4、8、16、32取 3 会直接报错。第二多卡机器要确认卡之间是高速互联走普通通道的话通信会拖垮整体性能。第三gpu-memory-utilization在 TP 下是每张卡各自计算的权重会被均分但激活值和中间张量不一定均分留意负载最重那张卡。还有个实操技巧如果你有 8 张卡但模型只需要 4 张可以用CUDA_VISIBLE_DEVICES0,1,2,3限定可见卡剩下的跑别的服务。这比在一台机器上开两个容器更省事但要注意显存别互相踩两个服务的gpu-memory-utilization加起来要留余量。4. 业务接入接口、模板与流式输出4.1 OpenAI 兼容接口的调用姿势vLLM 起的服务默认就带 OpenAI 兼容的/v1/chat/completions和/v1/completions这对业务侧太友好了——原来接云端 API 的代码基本只改一个 base_url 就能切到自建服务。curl http://127.0.0.1:8000/v1/chat/completions \ -H Content-Type: application/json \ -d { model: yuan2-2b, messages: [ {role: system, content: 你是一个简洁的中文助手。}, {role: user, content: 用三句话解释什么是KV Cache。} ], temperature: 0.7, top_p: 0.9, max_tokens: 512 }Python 侧直接用官方 SDK 就行from openai import OpenAI client OpenAI(base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY) resp client.chat.completions.create( modelyuan2-2b, messages[{role: user, content: 写一段50字的产品介绍}], temperature0.6, max_tokens256, ) print(resp.choices[0].message.content)api_key随便填本地服务不校验但 SDK 要求必填填EMPTY是最常见的写法。有个细节返回里会有usage字段包含 prompt_tokens 和 completion_tokens做计费和容量统计就靠它建议在网关层统一采集。4.2 对话模板与特殊分隔符的坑这一节是我认为最容易翻车的地方也是网上资料最少的地方。Yuan2.0 训练时用的是自己的一套对话拼接格式靠特定的分隔符把 system、user、assistant 三段分开。你在/v1/chat/completions里传的是结构化的 messages 数组服务端需要把它转成模型认识的纯文本。这个转换靠的是 tokenizer 配置里的 chat template。问题出在两种情况。第一种模型目录里没带 chat templatevLLM 会用一个通用兜底模板格式跟模型训练时不一致结果就是模型能接话但风格跑偏、容易自问自答。第二种带了模板但跟你用的 vLLM 版本不兼容某些模板语法在老版本里解析不了直接报错。我的处理办法是先在本地把拼接后的字符串打印出来看。用 tokenizer 的apply_chat_template方法tokenizeFalse拿到纯文本肉眼检查分隔符是不是模型该有的那套。如果发现不对就在业务侧自己拼 prompt然后走/v1/completions而不是 chat 接口把控制权拿回来。from transformers import AutoTokenizer tok AutoTokenizer.from_pretrained(/models/Yuan2-2B, trust_remote_codeTrue) msgs [{role: user, content: 你好}] text tok.apply_chat_template(msgs, tokenizeFalse, add_generation_promptTrue) print(repr(text)) # 重点看分隔符和结尾标记打印出来对比官方仓库里的示例格式一致就放心用不一致就自己拼。这一步花五分钟能省掉后面半天的“为什么回答质量不对”的排查。注意如果你在业务里做多轮对话历史消息的截断策略要和模板配套。有些模板要求消息必须成对出现user 后必须跟 assistant你从中间硬切会导致格式错乱。4.3 流式输出与超时控制对话类应用必须做流式不然用户盯着空白屏幕等三秒就以为卡死了。vLLM 的流式支持很完整streamTrue即可stream client.chat.completions.create( modelyuan2-2b, messages[{role: user, content: 讲个冷笑话}], streamTrue, max_tokens512, ) for chunk in stream: delta chunk.choices[0].delta.content if delta: print(delta, end, flushTrue)流式这块有几个实战经验。第一设置合理的超时SDK 默认超时可能偏短长回答会被切断我一般把读超时设到 60 秒以上。第二vLLM 的流式是 SSE 格式如果在前面挂了一层自研网关注意别做响应缓冲否则流式会变成“憋一大坨一次性吐出”体验全无。第三客户端要处理finish_reason如果是length说明被max_tokens截断了业务侧应该给用户提示或者续写不能默默截断。还有一个隐蔽的坑max_tokens是输出长度限制但服务端总的上下文预算是max-model-len。如果 prompt 已经很长max_tokens又设得大两者相加超过max-model-len时请求会被拒绝或截断。稳妥做法是在业务侧先估一下 prompt 的 token 数动态调整max_tokens。5. 压测与观测用数据说话5.1 压测脚本与并发梯度设计参数调完之后必须有数据支撑不然全靠感觉。我一般不用现成的压测工具自己写一个异步脚本更贴近真实场景import asyncio, time, statistics from openai import AsyncOpenAI client AsyncOpenAI(base_urlhttp://127.0.0.1:8000/v1, api_keyEMPTY) PROMPT 请用两百字介绍一下你自己。 async def one_call(i): t0 time.perf_counter() first None tokens 0 stream await client.chat.completions.create( modelyuan2-2b, messages[{role: user, content: PROMPT}], max_tokens256, streamTrue, ) async for chunk in stream: d chunk.choices[0].delta.content if d: if first is None: first time.perf_counter() - t0 tokens 1 total time.perf_counter() - t0 return first, total, tokens async def run(concurrency, rounds4): for _ in range(rounds): t0 time.perf_counter() res await asyncio.gather(*[one_call(i) for i in range(concurrency)]) wall time.perf_counter() - t0 ttfts [r[0] for r in res] out_tokens sum(r[2] for r in res) print(f并发{concurrency} 吞吐{out_tokens/wall:.1f} tok/s fTTFT_P50{statistics.median(ttfts)*1000:.0f}ms fTTFT_P95{sorted(ttfts)[int(len(ttfts)*0.95)-1]*1000:.0f}ms) asyncio.run(run(concurrency32))注意几点。一是要让每个请求都用同一段 prompt 且长度适中控制变量二是有rounds循环第一次跑包含预热取后面几轮的数据三是并发要按梯度来1、4、8、16、32、64、128 逐个试找到吞吐曲线的拐点。5.2 三个核心指标怎么看第一个是 TTFTTime To First Token首 token 延迟。它决定了用户感知的“响应速度”。这个指标主要受 prefill 阶段影响跟输入长度强相关跟并发也相关——因为新请求要和正在解码的请求抢计算资源。一般对话场景 P95 控制在 800ms 以内体验就不错。第二个是 TPOTTime Per Output Token每输出 token 耗时也有叫 ITL 的。计算方式是(总耗时 - TTFT) / (输出token数 - 1)。它决定了用户看到的“打字速度”。人阅读中文的速度大概是每秒 5-8 个字所以 TPOT 低于 150ms 就已经跟得上阅读了。这个指标主要受 decode 阶段影响跟并发数强负相关。第三个是整体吞吐单位 tokens/s 或 requests/s。这是压成本的核心指标决定了你需要几张卡。有意思的是吞吐和延迟是矛盾的并发越高吞吐越大但 TPOT 会变长。所以调优本质是在你的延迟红线内把吞吐推到最高。我一般的做法是先定延迟红线比如“TTFT P95 1s 且 TPOT P95 200ms”然后在这个约束下找最大并发最后把max-num-seqs设成这个并发值。这样既不浪费显存也不会让用户等到骂人。5.3 一组实测对比数据下面是我在单卡 80G、2B 级别模型上做的一组梯度压测输入约 40 token、输出固定 256 token供你参考趋势并发吞吐 tok/sTTFT P50TTFT P95TPOT P50GPU 利用率14290 ms110 ms24 ms22%8310130 ms210 ms26 ms68%32980240 ms420 ms33 ms91%641420450 ms780 ms45 ms96%1281560900 ms1600 ms82 ms97%25615901900 ms3100 ms160 ms98%这组数据把矛盾关系说得很清楚。并发从 1 到 32吞吐涨了二十多倍延迟只涨了不到三倍这是最划算的区间。32 到 128吞吐只涨六成但 TTFT 涨了近四倍。128 往后基本是平的说明撞到计算瓶颈了再加并发只是让队列变长不会多产出。所以对这类交互式应用我会把工作并发定在 32-64 之间然后靠多实例横向扩展来扛总量而不是把单实例并发拉到极限。这个结论比任何参数调优都值钱。6. 故障排查我踩过的坑和对照表6.1 启动期与显存相关的问题启动卡在“Loading model weights”很久。大概率是磁盘 IO 慢或者 safetensors 分片太多。检查磁盘类型机械盘加载几十 G 权重会非常慢、确认没有别的进程在抢 IO。加载完成后如果长时间没输出可能是编译自定义算子第一次启动慢是正常的后续会走缓存。报显存不足OOM。分两种。启动时 OOM通常是gpu-memory-utilization设太高或者max-model-len设太大导致预留超标把它降到 0.85、把上下文减半试试。运行时 OOM多半是并发超过 KV 预算或者有超长请求突然进来把max-num-seqs往下压一档。报undefined symbol或libcudart找不到。这是典型的版本不匹配torch 和 vLLM 编译时用的 CUDA 版本不一致或者系统里装了多个 CUDA。解决办法是回到容器镜像或者彻底清理重装不要在脏环境里救。6.2 输出质量相关的异常输出重复、循环、念同一句话。先查模板对不对这是头号原因。其次看采样参数temperature太低比如 0.1配repetition_penalty没开容易陷入循环。还有一种情况是量化版本精度损失太大换回 bf16 对比一下。输出乱码、夹杂特殊符号。检查 tokenizer 是否匹配。Yuan2.0 的词表规模和主流开源模型不一样如果你混用了别人的 tokenizer编码解码就对不上表现就是各种奇怪符号。回答风格突变、自问自答。模板里的角色标记没对上模型不知道自己该扮演 assistant。用 4.2 节的方法把拼接后的字符串打出来核对。多轮对话第二轮开始崩。历史消息拼接时漏了分隔符或者把 assistant 的历史回复和新的 user 消息粘在了一起。检查拼接逻辑。6.3 吞吐不达预期的排查路径吞吐上不去按这个顺序查能省很多时间看 GPU 利用率。低于 80% 说明卡没吃满往并发和批大小方向调接近 100% 但吞吐还是低说明撞到算力瓶颈只能加卡或换更小的模型。看 KV Cache 的 block 使用率。如果一直很低说明max-num-seqs设小了白瞎了显存。看请求的平均输入长度。如果输入特别长比如 RAG 场景塞了几千 token 的上下文prefill 会成为瓶颈果断开--enable-chunked-prefill。看有没有超长尾请求。一条 30K 的请求会把整批节奏打乱可以在网关层做长度限制超长的走单独队列。看日志开销。有没有开--disable-log-requests一个大差别。6.4 常见问题速查表现象最可能原因处理办法启动报不认识模型结构缺--trust-remote-code或 .py 文件不全补齐模型目录文件加参数启动 OOMgpu-memory-utilization或max-model-len过大降到 0.85上下文减半运行中偶发 OOM并发过高或长请求突刺降max-num-seqs网关限长吞吐低、GPU 闲并发上限设太小按 KV 预算放大max-num-seqs首 token 特别慢输入长、未开分块预填充开--enable-chunked-prefill输出重复模板错、temperature过低核对模板调采样参数输出乱码tokenizer 不匹配用模型自带 tokenizer日志把磁盘刷爆未关请求日志加--disable-log-requests流式变成一次性返回中间层做了响应缓冲关掉缓冲加X-Accel-Buffering: no多卡比单卡还慢通信瓶颈或 TP 不能整除检查互联确认头数可整除我个人在实际操作中的体会是vLLM 配 Yuan2.0 这套组合真正的门槛不在命令本身而在两个认知点一是要会按 config 手算 KV Cache把并发上限算出来所有参数才有依据二是要把对话模板当成一等公民对待每次换模型版本都重新核对一遍拼接后的字符串。这两件事花的时间加起来不超过一小时但能挡掉后面百分之八十的疑难杂症。剩下的就是压测找拐点、按拐点定参数、多实例横向扩展这套流程走顺了同样的卡能多扛好几倍的活。最后再分享一个容易被忽略的小技巧给每个实例配上固定的--served-model-name并在网关层做一致性哈希路由让同一个会话尽量落在同一个实例上。虽然 vLLM 本身是无状态的但这样能让会话的 KV 复用更集中长对话场景下首 token 延迟会明显好于随机路由。这个改动只需要在网关侧加十几行代码收益却很实在。