1. 项目缘起与整体架构思路1.1 为什么要在生产环境自建推理服务最早接触这个需求是因为团队手上同时跑着好几个模型一个负责长链推理的 DeepSeek-R1 蒸馏版本一个负责日常对话和结构化输出的 Qwen3 系列还有一个做向量化的 embedding 小模型。起初图省事每个模型单独起一个进程、单独占一张卡结果就是显存利用率惨不忍睹——大模型那张卡常年跑到 70% 就上不去了小模型那张卡更是长期闲置。业务侧一到大促或者活动期并发一上来排队延迟直接飙到十几秒用户端体验崩得很难看。后来我们把目光转向了 vLLM。选它的核心理由其实就三条第一是PagedAttention这套显存管理机制能把 KV Cache 按块管理碎片率大幅下降同样的卡能塞下更多并发序列第二是Continuous Batching也就是连续批处理请求不用等齐一批再走来一个塞一个吞吐和延迟的平衡比静态批处理好太多第三是它对 OpenAI 兼容接口的支持比较完整业务侧几乎不用改代码就能切过来。但真到了生产环境部署才发现“能跑起来”和“跑得稳”之间隔着一条鸿沟。多模型并发、显存争抢、调度策略、超时重试、监控告警每一个环节都能让你半夜被叫起来。这篇就把我们踩过的坑、验证过的参数、以及最后沉淀下来的部署方案完整梳理一遍适合正在做推理服务自建、或者准备把模型从单机 demo 迁到生产环境的同学参考。1.2 多模型并发的三种部署形态对比在动手之前先想清楚多模型到底怎么放。我们实际验证过三种形态各有适用场景不能一概而论。部署形态资源占用隔离性运维复杂度适用场景单进程多模型最低差低模型都很小、流量低多进程单卡分时中等中中模型中等、错峰明显多进程多卡独立最高好高生产主力、SLA 要求高第一种单进程多模型vLLM 本身支持在一个引擎里加载多个 LoRA 适配器但如果是完全不同的基座模型硬塞进一个进程会互相干扰一个模型 OOM 可能把整个进程拖垮生产环境基本不考虑。第二种多进程单卡分时靠的是显存够大、模型够小或者业务错峰明显。我们试过把 embedding 模型和 Qwen3 小尺寸版本放一张卡白天对话流量大时 embedding 排队晚上批处理任务上来时对话又卡调度非常难受最后放弃。第三种多进程多卡独立是我们最终采用的方案。每个模型独占一张或一组卡通过网关层做路由和限流。隔离性最好一个模型出问题不影响其他模型代价就是卡的数量上去了。但对于有 SLA 要求的线上服务这个代价是值得的。提示不要为了省卡把关键模型和边缘模型混部线上事故往往就出在这种“省一点”的决策上。1.3 整体架构分层设计我们最终的架构分四层。最底层是 GPU 资源池按模型划分卡组往上是 vLLM 推理引擎层每个模型一个独立实例暴露 OpenAI 兼容接口再往上是网关层负责鉴权、限流、路由、重试最上面是业务调用层。网关层这里有个关键设计按模型维度做独立限流。因为不同模型的单次推理耗时差异巨大DeepSeek-R1 这种带长链推理的单请求可能跑几十秒Qwen3 对话可能一两秒就返回。如果用一个全局并发阈值长推理模型会把短请求的配额吃光。我们给每个模型单独配置了最大并发数和排队超时具体参数后面细说。2. 核心参数解析与显存规划2.1 显存到底怎么算才不翻车显存规划是部署的第一道坎算错了要么 OOM 起不来要么浪费一半卡。vLLM 的显存占用主要分三块模型权重、KV Cache、以及运行时开销。模型权重这块比较好算看模型参数量和精度。以常见的 7B 模型为例FP16 精度下大约 14GBINT8 量化后约 7GBINT4 量化后约 3.5GB。但注意这只是权重还没算 KV Cache。KV Cache 才是真正吃显存的大头而且它和并发数、序列长度直接挂钩。计算公式大致是KV Cache 显存 2 × 层数 × 注意力头数 × 头维度 × 序列长度 × 并发数 × 精度字节数这个公式看着复杂实际用的时候不用手算vLLM 启动时会打印它能分配多少 KV Cache 块。关键是理解gpu_memory_utilization这个参数——它控制 vLLM 愿意占用多少比例的显存。默认 0.9意思是留 10% 给其他开销。我们踩过的坑是一开始把gpu_memory_utilization设成 0.95想着多榨一点显存出来结果跑了一段时间后偶发 OOM。原因是显存碎片和 CUDA 上下文本身也要占地方留的余量不够。后来统一改成 0.85 到 0.88 之间稳定性明显提升。注意gpu_memory_utilization不是越高越好生产环境建议留足 12% 到 15% 的余量尤其是多进程共享一张卡的时候。2.2 max_model_len 与 max_num_seqs 的取舍这两个参数直接决定并发能力和单请求长度上限是最容易配错的。max_model_len是单个请求的最大序列长度包括输入和输出。设太大KV Cache 块被单个长请求占满其他请求排不进来设太小长文本请求直接被拒。我们的做法是先统计业务侧的真实分布取 P99 长度再往上留 20% 余量。max_num_seqs是同时处理的最大序列数也就是并发上限。这个值和max_model_len是此消彼长的关系——同样的显存序列越长能并发的数量就越少。我们实测过一组数据在一张 80GB 的卡上跑 7B 模型FP16 精度max_model_lenmax_num_seqs实测稳定并发单请求平均延迟40962561801.2s8192128952.1s1638464483.8s可以看到序列长度翻倍并发能力基本腰斩。所以这两个参数必须结合业务实际来定不能拍脑袋。我们最后给对话模型定的是 8192 长度、128 并发给长推理模型定的是 16384 长度、32 并发因为长推理请求本来就少但单个耗时长。2.3 张量并行与流水线并行的选择当单个模型大到一张卡放不下时就得上并行。vLLM 支持张量并行TP和流水线并行PP我们主要用 TP。TP 是把模型的每一层切分到多张卡上每张卡算一部分然后做 all-reduce 通信。优点是延迟低因为所有卡同时算缺点是通信量大卡间带宽要求高。PP 是把模型按层切成几段每段放一张卡数据像流水线一样流过通信量小但延迟高因为存在流水线气泡。我们的经验是卡间有高速互联比如 NVLink就用 TP没有就用 PP 或者干脆量化后单卡跑。曾经在一个只有 PCIe 互联的环境里硬上 TP4结果通信成了瓶颈吞吐还不如单卡量化版本。这个坑踩得很值后来选型时第一件事就是确认卡间互联带宽。3. 实操部署全流程与关键配置3.1 环境准备与依赖锁定环境这块最大的教训是一定要锁版本。vLLM 迭代非常快不同版本之间 API 和行为都可能有变化。我们吃过一次亏测试环境用的某个版本生产环境装的时候自动拉了最新版结果gpu_memory_utilization的默认行为变了直接导致 OOM。现在我们的做法是用 requirements 文件把 vLLM、PyTorch、CUDA 驱动版本全部锁死并且在 CI 里做一次冒烟测试。基础依赖大致是这些# 锁定版本的依赖示例 vllm0.6.x torch2.4.x transformers4.44.xCUDA 版本要和驱动匹配这个不用多说。另外建议单独建虚拟环境不要和训练环境混用因为训练和推理对 PyTorch 的编译选项要求可能不一样。提示生产镜像构建时把模型权重也打进去或者用共享存储挂载避免每次启动都去下载启动时间能从几分钟降到几十秒。3.2 单模型启动命令详解先看一个我们线上在用的启动命令然后逐段拆解python -m vllm.entrypoints.openai.api_server \ --model /models/qwen3-7b \ --served-model-name qwen3-7b \ --tensor-parallel-size 1 \ --gpu-memory-utilization 0.86 \ --max-model-len 8192 \ --max-num-seqs 128 \ --dtype float16 \ --port 8000 \ --host 0.0.0.0 \ --disable-log-requests--served-model-name是暴露给业务侧的模型名建议和实际模型解耦方便后续换模型不改业务代码。--disable-log-requests在生产环境建议打开否则每个请求都打日志磁盘 IO 会成为瓶颈我们有一次就是日志把磁盘写满了导致服务异常。--dtype显式指定不要依赖自动推断。有些模型权重是 bfloat16有些是 float16混用可能出问题。3.3 多模型并发的端口与资源编排多模型部署时每个实例占一个端口用 systemd 或者容器编排来管理。我们用的是容器方案每个模型一个容器通过--gpus指定卡。关键点是卡和端口的映射关系要固定不能动态分配。因为网关层的路由配置是静态的如果容器重启后卡变了性能特征也会变监控数据就对不上了。我们的做法是在编排文件里写死CUDA_VISIBLE_DEVICES每个模型固定用哪几张卡。启动顺序也有讲究。如果多个模型共享同一张卡虽然不推荐但资源紧张时难免要错开启动时间避免同时初始化抢显存。我们一般间隔 30 秒启动一个等前一个的显存分配稳定了再起下一个。3.4 网关层的限流与重试配置网关层我们用的是自己写的一层轻量代理核心逻辑是按模型维度维护一个令牌桶请求进来先取令牌取不到就排队或者直接返回 429。限流阈值怎么定我们的方法是压测。用 locust 或者 wrk 对单个模型实例打流找到吞吐开始下降、延迟开始飙升的拐点把那个并发数乘以 0.8 作为限流上限。留 20% 余量是为了应对突发流量和长尾请求。重试策略要特别小心。推理请求默认不要自动重试因为推理是有副作用的消耗算力而且长推理请求重试代价极高。我们的做法是只对连接错误和 5xx 做有限重试且重试次数不超过 1 次超时错误直接返回给业务侧由业务决定是否重试。4. 常见问题与排查技巧实录4.1 启动就 OOM 的几种典型原因OOM 是最高频的问题但原因各不相同得对症下药。第一种是gpu_memory_utilization设太高。前面说过留足余量。第二种是max_model_len设太大KV Cache 预分配就占满了。vLLM 启动时会按最大长度预分配 KV Cache 块如果你设了 32768 但实际业务根本用不到那就是纯浪费。第三种是卡上还有别的进程残留nvidia-smi看着没进程但显存没释放这种情况重启容器或者用fuser查一下。排查顺序建议是先看nvidia-smi确认卡是干净的再降gpu_memory_utilization到 0.8 试还不行就降max_model_len。4.2 并发上不去、吞吐低的排查思路有时候服务能跑但并发一压就上不去延迟还高。这种情况八成是调度或者配置问题。先看 vLLM 的日志它会打印实际的 batch size 和 KV Cache 使用率。如果 KV Cache 使用率一直很低但并发上不去可能是max_num_seqs设小了。如果 KV Cache 使用率很高但吞吐还是低那可能是卡间通信或者 CPU 预处理成了瓶颈。我们遇到过一次CPU 预处理tokenization成了瓶颈因为请求文本特别长tokenize 耗时比推理还久。后来把 tokenization 放到网关层用多进程池做推理引擎只收 token id吞吐直接翻倍。这个优化思路值得记一下推理引擎只做推理预处理和后处理尽量外移。4.3 长请求拖垮短请求的隔离方案这是多模型并发里最隐蔽的坑。一个长推理请求占着 KV Cache 不放短请求排不进来用户体验就是“有时候秒回有时候等半天”。我们的解决方案是按请求长度分级。网关层根据输入长度预估一个等级长请求走单独的队列和限流池短请求走快速通道。vLLM 侧可以通过设置不同的max_num_seqs和调度优先级来配合虽然 vLLM 原生调度是 FIFO但通过网关层的队列隔离效果已经很明显。另一个技巧是给长请求设置更短的排队超时超时就返回让业务侧异步处理不要占着在线通道。4.4 常见问题速查表现象可能原因排查动作解决方向启动 OOM显存参数过高看启动日志的显存分配降 utilization 或 max_model_len并发上不去max_num_seqs 小看 KV Cache 使用率调大并发或缩短序列延迟忽高忽低长短请求混排看请求长度分布队列隔离、分级限流服务偶发崩溃显存碎片看崩溃时间点留足余量、定期重启吞吐突然下降CPU 预处理瓶颈看 CPU 使用率预处理外移、多进程提示生产环境建议加一个定时重启策略比如每天凌晨低峰期重启一次能解决大部分显存碎片导致的偶发问题。5. 监控告警与稳定性加固5.1 必须监控的核心指标服务上线只是开始能不能稳住靠的是监控。我们重点盯这几个指标首 token 延迟、单请求总延迟、每秒生成 token 数、KV Cache 使用率、排队请求数、GPU 利用率和显存占用。首 token 延迟最能反映用户体验因为它决定了用户看到第一个字的时间。总延迟反映整体性能。KV Cache 使用率是预警指标持续高于 90% 就说明该扩容或者调参了。排队请求数是限流阈值的直接反馈一旦持续大于 0说明限流设得太松或者容量不够。这些指标 vLLM 原生通过 Prometheus 暴露接一个 Prometheus 加 Grafana 就能看。告警阈值我们设的是首 token 延迟 P99 超过 3 秒告警KV Cache 使用率持续 5 分钟超过 92% 告警。5.2 灰度发布与回滚机制模型更新或者参数调整一定要灰度。我们的做法是新起一个实例把 5% 的流量切过去观察半小时各项指标没问题再逐步放大比例。回滚要快。网关层的路由配置支持热更新发现异常直接把流量切回旧实例整个过程不超过 1 分钟。这个能力在关键时刻能救命我们有一次新模型上线后长请求延迟异常靠灰度加回滚在用户大规模投诉前就切回来了。5.3 压测方法与容量规划上线前必须压测而且要压到极限。我们用 locust 模拟真实请求分布包括长短请求的比例。压测的目标不是找到最大吞吐而是找到延迟开始劣化的拐点那个拐点就是你的安全容量上限。容量规划时按峰值流量的 1.5 倍准备资源。因为推理服务的流量往往有突发性而且模型性能会随输入内容波动留足余量才能保证 SLA。6. 一些踩坑后的个人体会部署这套东西前后折腾了小两个月最大的体会是推理服务的稳定性八成靠配置两成靠硬件。同样的卡参数配得好能多扛一倍并发配得差三天两头出问题。另一个体会是不要迷信默认值。vLLM 的默认参数是通用场景下的折中但你的业务场景一定有自己的特点必须根据实际压测数据来调。我们最开始就是全用默认值结果长推理模型把短请求全堵死了后来做了队列隔离才解决。还有就是监控要趁早。我们一开始没上监控出了问题全靠用户反馈排查起来两眼一抹黑。后来把指标接上很多问题在用户感知之前就能发现和处理。最后分享一个小技巧把每次参数调整和对应的压测结果记在一个表格里时间长了你就有了自己业务场景下的“参数地图”下次扩容或者换模型时直接查表效率高很多。这个习惯看起来笨但真的省事。