上季度我们组接了一个活儿:在公司内网部署一套可供多个业务方统一调用的大模型推理服务。需求听上去并不复杂——模型和数据都不能出域,接口必须稳定,能扛住不同团队的调用节奏,同时还要在不新增太多预算的情况下尽量把吞吐打上去。
试了两三条技术路线之后,最终把方案定在了 vLLM 上。它不是唯一能跑大模型的框架,但确实是把显存利用率和并发调度这件事做得最省心的一个。这篇文章把我从需求分析、硬件规划、环境安装、服务启动,到压测调参、权限管理、监控排障的完整过程写下来,里面有大量我实际动手后的参数结论和踩坑记录。适合正准备在企业内部署私有化大模型推理服务的同学参考,也欢迎已经上车的朋友一起交流。
1. 为什么企业私有化部署优先考虑 vLLM 而不是从头造轮子
1.1 私有化场景的三个硬约束
很多团队拿到"部署大模型"这个需求时,第一反应是找一段推理代码,配上 Flask 或者 FastAPI 包装一下就跑。这种做法在 Demo 阶段绝对够用,但一旦进入企业私有化场景,三个硬约束马上会逼你重新选型。
第一,数据不能出域。模型文件、用户输入、推理日志全部留在内网,意味着不能用任何公网推理服务,模型要么买商业授权后内部部署,要么用开源权重自己做微调部署。第二,接口面要兼容。企业内部不是只有你一个系统在用 AI,OA、客服、知识库、数据分析平台都会来调,它们不可能为每一套推理框架写一套适配层。第三,资源是有限的,GPU 采购有周期,年底一复盘还要讲单位显存产出了多少可用 token。这种情况下,框架的吞吐效率和显存利用效率就成了硬指标。
vLLM 之所以适合做这件事,就是因为它把这几个问题都对准了:标准的 OpenAI 兼容 HTTP 接口,业务方几乎零改造接入;自带分页显存管理和连续批处理,同等显存下能服务的并发请求比朴素方案高不少;开源可审计,代码和依赖可控,正好满足私有化部署对透明度的要求。
1.2 PagedAttention 和连续批处理到底解决了什么
谈 vLLM 绕不开 PagedAttention,但我发现很多文章把它讲得太神了。用大白话解释其实就是一句话:传统方案的 KV Cache 是按"整个请求"连续预分配的,而 PagedAttention 把它拆成了固定大小的页,可以离散地放在显存里。
打个比方,传统方式就像每个来停车的人都必须从入口到出口预留一整条连续车位,哪怕他实际只停了两个位子,中间空出来的也没法给别人用。PagedAttention 则像把停车场切成标准车位,来的车需要几个就占几个,走了一辆车立刻可以补下一辆。显存碎片少了,同一张卡能同时容纳的序列就多了,这是吞吐提升最根本的来源。
连续批处理(Continuous Batching)解决的是另一个问题。没有连续批处理的框架,通常按批次处理:一批请求必须全部生成完,才开始下一批,中间的 GPU 空档期非常明显。vLLM 的做法是每轮迭代动态决定谁继续生成、谁可以退出、谁可以进入,相当于一个流水线永远在几乎满载地跑。实测中最直观的感受是:长请求和短请求混在一起时,短请求不会被长请求堵到天荒地老。
1.3 什么时候不该用 vLLM
说句公道话,不是所有场景都该无脑上 vLLM。如果你的业务非常 niche,比如需要自定义 attention 算子、完全定制的采样策略,或者你的调用量极低、一天只有几百次,那直接用 vLLM 反而有点过度设计,自己包一层简单推理更灵活。vLLM 的优化方向是"高并发、高吞吐",这和企业私有化场景的大多数诉求一致,但具体到个别低流量系统,框架的复杂性会超过收益。做技术选型,先确认自己的流量模型再动手,别本末倒置。
2. 动手部署前,显存规划和模型选型这两件事必须做扎实
2.1 GPU 显存都花在哪三个地方
部署之前不把显存账算清楚,后面调参就是盲人摸象。一个大模型跑在 GPU 上,显存主要被三块占用:模型权重、KV Cache、激活值和运行时开销。
模型权重最好算:参数量 × 每个参数占用的字节数,FP16 精度下就是 2 字节,一个 14B 模型权重大约 28GB。KV Cache 是推理过程中缓存历史 token 的 K 和 V 矩阵,这部分显存随并发请求数和上下文长度动态变化,是调优空间最大的一块。激活值则是前向计算中的中间张量,通常波动比较剧烈,尤其是在长序列输入时。
vLLM 启动时会打印实际使用的 GPU 内存情况,配合gpu-memory-utilization参数控制上限。我项目里常用的估算方式是这样:单卡 80GB,跑一个 14B 量级模型,权重约 28GB,把显存利用率上限设为 0.9,意味着最多可用 72GB,减去权重后大约 44GB 留给 KV Cache 和激活值。按经验,这类模型每 token 的 KV 占用大约在 1~2MB,心里大概就有数了:能容纳多少并发、多长的上下文,是可以粗略推算的。
2.2 一张对照表帮你决定模型规模和并行度
模型规模和显存并行度的搭配,是部署前最容易纠结的问题。我整理了一张基于 FP16 精度的经验对照表,供参考:
| 模型规模 | 权重显存估算 | 推荐显存组合 | 建议张量并行度 |
|---|---|---|---|
| 7B~8B | 约 14~16GB | 单卡 24GB | 1 |
| 14B~20B | 约 28~40GB | 单卡 80GB 或双卡 24GB | 2 |
| 30B 级别 | 约 60GB 左右 | 双卡 80GB | 2 |
| 70B 级 | 约 140GB | 4×80GB | 4 |
这个表是按 FP16 算的,如果采用量化精度,权重显存能明显下降,并行度也可以相应收敛。顺带提醒一句,别被"显存刚好够放权重"迷惑,你还要给 KV Cache 和激活值留足余量,否则并发一上来必然崩。
2.3 量化要不要上:精度、显存和兼容性的三角博弈
私有化部署经常要在同样预算下塞更大的模型,量化就成了绕不开的话题。常见的做法是 FP8、AWQ 这类权重量化,可以把权重显存砍掉一半左右。我的倾向是:70B 级别、又对延迟和并发有明确要求的场景,量化很香;14B 以下的中小模型,除非显存实在紧张,否则不建议为了省那点显存牺牲精度。
需要特别注意的是量化格式和 vLLM 加载逻辑的兼容性。量化模型在仓库里一般会有对应的配置文件和量化格式标记,你必须保证load_format和实际格式一致,否则启动时会报意料之外的错误,或者模型行为跟原来明显不一致。第一次跑量化模型时,建议先在测试环境对同一组问题做一次对照验证,确认输出质量稳定再上线。
3. 从装环境到首个请求的落地记录
3.1 环境安装里的版本雷区
做企业部署,我第一个建议是优先使用官方容器镜像,而不是在一台裸机上直接 pip 安装。原因是 vLLM 对 Python 版本、CUDA 运行时、PyTorch 版本都有联动要求,手搓环境很容易踩到版本不匹配的坑。如果你因为某些安全要求不得不在裸机上装,也有几条经验可以避开大部分问题。
Python 版本优先选 3.10 或 3.11,太新或太旧都会遇到某些二进制依赖装不住。安装 vLLM 之前先把 pip 升级到最新,然后直接pip install vllm,它会把你需要的 PyTorch 等依赖一起拉齐。这里的关键教训是:不要在已经装好其他深度学习框架的环境里直接装 vLLM,那样会把 PyTorch 版本冲突的雷全部引爆。实际部署时,我给 vLLM 单独建了一个虚拟环境,其他项目完全隔离。
3.2 一条启动命令把服务拉起来
vLLM 新版本的启动方式已经统一成了vllm serve子命令。下面这条命令是我在自己的部署中反复用到的基准形态:
vllm serve /data/models/企业版-14B-chat \ --served-model-name enterprise-llm \ --tensor-parallel-size 2 \ --gpu-memory-utilization 0.85 \ --max-model-len 16384 \ --max-num-seqs 64 \ --trust-remote-code \ --host 0.0.0.0 \ --port 8000逐个解释一下这些参数的意义。
--served-model-name是对外暴露的模型名,业务方只需要认这个统一名字,内部换模型路径时不用通知所有下游改配置,这是企业化运营里一个很重要的解耦点。--tensor-parallel-size表示把模型张量切分到几张卡上联合推理,单机多卡场景就用它;跨机器部署时还需要配合管道并行或分布式推理配置,复杂度会高一截。--gpu-memory-utilization是显存使用上限,设得太低会浪费显存,设得太高又会增加 OOM 风险,我一般从 0.85 开始调。--max-model-len是允许的最大上下文长度,直接决定 KV Cache 的上限预算。--max-num-seqs是最大并发序列数,等于给服务设置了一个并发阀门,防止极端流量把显存打穿。
如果你的 vLLM 版本比较老,也可能遇到python -m vllm.entrypoints.openai.api_server这种写法,功能等价,按版本选择即可。
3.3 用 curl 验证 OpenAI 兼容接口
服务起来之后,第一件事是检查健康状态:
curl http://127.0.0.1:8000/health返回正常的健康信息后,再发一个最简单的对话请求:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "enterprise-llm", "messages": [{"role": "user", "content": "你好,介绍一下你自己"}], "temperature": 0.7 }'能拿到结构完整的返回 JSON,说明链路已经通了。这一步值得注意的细节是:请求里的model字段要填--served-model-name对外暴露的名字,而不是本地权重目录名,很多同事第一次就在这里栽了跟头。日志里如果看到 "Initializing an LLM engine" 之后有明显的报错,说明权重路径或配置有问题,先根据日志尾部定位,不要反复重启同一个命令。
4. 上线前压测调参:别等业务方喊慢了才动手
4.1 先分清该看哪些指标
私有化服务上线前不做压测,几乎等于裸奔。但压测也不是随便拿一个并发工具打一打就完事,至少要分清几个指标,它们代表的业务体验完全不同。
首 token 延迟(TTFT)决定用户"等多久才开始看到内容";单个 token 间隔(TPOT)决定文本生成的流畅度;端到端延迟和整体吞吐则反映系统在并发压力下的综合表现。vLLM 在"高并发高吞吐"场景下最擅长,它的调参主要是在延迟和吞吐之间找平衡。压测时我习惯同时记录两个并发档位:一个是业务常态峰值,一个是系统极限压力,两档数据都拿到,才能决定最终参数。
4.2 一次实际压测中的调参过程
我第一次给这套服务压测时,用的全部是默认参数。结果很真实:低并发下延迟挺漂亮,一到高并发就出现了排队等待,P99 延迟直接垮掉。看了一眼监控,发现running个数的上限就是默认的max-num-seqs,大量的请求都在等待队列里。
第一次调整是把gpu-memory-utilization从 0.8 提到 0.9,给 KV Cache 多留了空间;接着把max-num-seqs从默认值提到 64。压测结果显示吞吐明显涨了,但 TTFT 也跟着上涨,因为同一时刻真正并发执行的序列变多了,显存和计算资源被摊薄。最后我把max-num-seqs稳定在 48 左右,P50 端到端延迟和吞吐取得了一个比较理想的平衡点。
4.3 参数之间互相牵制的关系
调 vLLM 的参数本质上是在做资源置换,几个核心参数彼此牵制,我习惯画一张对应关系来辅助决策:
| 参数 | 调大后的收益 | 调大后的代价 |
|---|---|---|
max-num-seqs | 支持更多并发,减少排队 | 单请求延迟上升,显存压力增大 |
gpu-memory-utilization | 更多 KV Cache 空间,减少抢占 | OOM 风险上升 |
max-model-len | 支持更长上下文 | 每请求 KV 占用增多,整体并发下降 |
记住一个原则:max-model-len不是越大越好,它只是业务能接受的上下文上限,建议按真实业务的 95% 分位数来设定。剩下的显存余量,尽量分配给 KV Cache 和并发序列数,这是提升吞吐最有效率的方向。
5. 企业化收尾:鉴权、监控、日志与模型更新
5.1 API 鉴权的三个层次
vLLM 本身提供了简单的 API Key 鉴权,--api-key参数可以启用。但这在企业内部往往不够,我建议至少做三道防护。
第一,网络隔离,服务只绑定内网地址,不暴露公网;第二,服务级鉴权,在 vLLM 前面加一层轻量网关或反向代理,统一负责 token 签发、黑白名单和调用频控,这样即使某个业务方的鉴权信息泄露,也只在网关这一层就能掐断;第三,调用审计,记录来源应用、调用次数、请求耗时,出了问题能快速追溯。网关这层的重要性在于,它可以成为所有内部模型服务的统一入口,后续接更多模型时,业务方不需要关心模型到底部署在哪台机器上。
5.2 用自带的 metrics 接口对接监控
vLLM 自带/metrics接口,以标准监控格式暴露运行指标,这一步很多人没用起来。部署完成后先确认一下:
curl http://127.0.0.1:8000/metrics | grep vllm常见的指标里,vllm:num_requests_running和vllm:num_requests_waiting最关键——前者能看出当前有多少请求正在执行,后者是所有排队请求的数量。只要waiting长期大于 0,说明并发设置或副本数已经到瓶颈了。另外还有 KV Cache 使用率、token 吞吐速率等指标,把这些接到监控面板上,比靠人肉盯日志靠谱太多。我的经验是设置一条告警:当waiting平均值连续五分钟高于某个阈值时,自动通知运维介入,而不是等业务方来投诉。
5.3 systemd 守护与日志轮转
容器环境一般交给编排平台管,但裸机或虚拟机部署时,我强烈建议给 vLLM 配一个 systemd 服务。原因很简单:大模型加载一次权重可能要好几分钟,进程意外退出后如果没人管,服务就是长时间不可用。这是我的一个最小服务配置示例:
[Unit] Description=vLLM Inference Service After=network.target [Service] User=llm WorkingDirectory=/data/vllm ExecStart=/data/vllm-env/bin/vllm serve /data/models/企业版-14B-chat --served-model-name enterprise-llm --tensor-parallel-size 2 --gpu-memory-utilization 0.85 --max-model-len 16384 --max-num-seqs 48 Restart=on-failure RestartSec=10 LimitNOFILE=65535 [Install] WantedBy=multi-user.target日志处理上,vLLM 的标准输出直接交给系统日志即可,再用 logrotate 做轮转,避免日志文件把磁盘写满。这块容易被忽略,但推理服务的日志增长速度远比想象中快,尤其是高并发时,每一条请求都有完整的输入输出记录。
5.4 模型灰度更新与快速回滚
企业私有化部署早晚要面对换模型这件事,比如更新了微调版本、换了更好的基座。直接停掉服务再启动新模型,会影响所有在用业务,这是不可接受的。
我的做法是在网关层维护两个上游服务:一个是稳定版本,一个是灰度版本。新模型先在灰度端口跑起来,用内部测试团队定向访问验证;确认没问题后,把网关的流量权重逐步切到新模型,保留旧模型进程作为快速回滚的备份。如果一切正常,再停掉旧进程。整个过程业务方无感,回滚时只需要把网关权重切回去,非常干净。
6. 复盘几个实际踩过的坑:从报错信息到根因
6.1 并发一高就超时的真实排查链路
有次同事反馈,压测到了某个并发量之后,请求普遍超时。我第一反应是看监控,发现running数量已经打满,waiting队列一直在累积,每个新请求都要排队等很久,TTFT 自然爆表。很多人这时候会直接加机器、加副本,但正确的第一步是确认排队原因。
如果waiting高、running也满了,说明max-num-seqs限制了并发上限。在显存有余量的前提下,可以先调高这个值;显存没有余量,再加副本。那次我们实际是显存还有空间,只是默认并发值偏保守,调整之后问题直接消失。这个案例说明:遇到超时别急着堆资源,先看清楚这是在排队还是在处理。
6.2 显存看着够却 OOM 的隐性问题
另一次是并发并不算高,却出现了 CUDA OOM。排查后发现,问题不在 KV Cache 的常驻占用,而在激活值的瞬时峰值——当批处理中同时出现几个长序列时,前向计算产生的中间张量会在一瞬间冲得非常高。这也是我把gpu-memory-utilization普遍压到 0.85 而不是 0.95 的原因,留出 10% 以上的波动缓冲空间是值得的。开发环境里还可以用--enforce-eager来降低启动时的 CUDA 图内存开销,代价是部分性能下降,但用于排查阶段非常有效。
6.3 换模型后输出语义不对,先查进程和缓存
还有一次很隐蔽:配置文件的模型路径已经换成了新权重,但对外输出总觉得还带着旧模型的风格。排查到最后,发现是底层 worker 进程没有真正重启,服务进程缓存了旧的权重句柄。权重路径变了,进程还在占着旧显存,自然吐出来还是旧内容。从那以后我养成了一个习惯:换模型版本时,明确先停服务、再改配置、再启动,并且用一组固定的"回归问题集"做冒烟验证,输出稳定后才算真正切换完成。
6.4 多卡环境下偶发连接异常
多卡推理时,偶尔会遇到卡间通信连接被重置的报错。这类问题通常是多卡通信库配置与硬件环境不匹配,常见诱因包括网卡选型、交换机流控策略、机内互联方式不一致。排查思路是先把并行度降到 1 验证模型本身的正确性,排除权重或代码问题后,再逐项检查通信相关配置。单机多卡部署时,保持所有 GPU 的驱动版本一致,也能减少很多莫名其妙的偶发错误。
最后再说一个我自己一直保留的小习惯:每次发版或调整参数后,都会把启动日志里首次打印的显存占用记下来,连同压测指标一起存档。vLLM 是那种配置项看起来不多、但组合起来千变万化的系统,保持每次变更的基线数据,再遇到异常时,你才能快速判断是参数问题、数据问题还是硬件问题,而不是推倒重来。