团队内部刚开始评估大模型私有化的时候,很多人第一反应是“直接用微信小程序里那种云端API不就行了吗”。等真把业务数据放进去试一圈,才发现外部接口慢、贵、还有数据出域的合规顾虑,这才开始认真研究自己部署推理服务。折腾了大半年,我把vLLM从单卡跑通到多卡集群,中间踩过不少坑,也有过几次凌晨两点的救火经历。这篇东西就是我自己从零到上线的实战记录,不打算写成一个通用教程,而是把我认为最关键、最容易翻车的地方逐一拆开讲清楚,适合准备做企业内网模型服务、正在对比推理方案、或者已经被显存和并发问题折磨过几轮的开发者。
1. 为什么企业私有化部署绕不开vLLM
1.1 先搞清楚:你是被哪个问题逼到这一步的
我接触到的团队,选择私有化部署大模型服务,理由基本逃不出三类。
第一类是数据出域问题。有些业务数据明文里带客户信息、内部财务信息,合同或者内部合规要求里写得明明白白,数据不能离开自有环境。这时候无论外部API效果多好,都用不了,只能自己把模型圈在内网里跑。
第二类是成本账单失控。外部API按token计费,平时测试没感觉,一旦接入真实业务流量,调用量上来之后月度账单涨得很快。尤其是文档处理、客服问答这类需要长文本和多次调用的场景,成本曲线是陡峭的。自建方案把成本从“按次计费”变成“一次性硬件投入+电费”,长期看账面上更划算。
第三类是模型私有化定制。业务方往往会基于开源底座模型做微调,把企业知识库、专属话术灌进去。微调后的模型权重不可能传到外部平台,只能在内部环境里自己加载和服务。
这三种情况都会导向同一个技术选型问题:推理服务用什么跑。早期方案有直接用深度学习框架做动态图推理的,有基于其他推理框架改的,但实践下来大家基本都会绕回vLLM。
1.2 vLLM到底动了哪块蛋糕
vLLM不是大模型本身,它是一个专门做大模型推理加速和服务的开源引擎,核心是“PagedAttention”这套显存管理思路。
传统推理框架处理请求时,会给每个请求的KV缓存(模型生成过程中保存的中间状态)分配一块连续显存。问题在于,每次请求生成的序列长度是动态的,预先按最大长度分配会浪费大量空间,而如果分配得太保守又放不下。这就类似老式内存分配里“碎片化”问题:显存明明剩很多,但都是零散的,新的请求塞不进去。
PagedAttention做的事,就是把KV缓存切分成固定大小的块,像操作系统管理虚拟内存一样,按需分配、可以分散存放。这样做的好处很直接:显存利用率大幅提升,同样的显存能塞下更多并发请求,吞吐量随之成倍上涨。vLLM还实现了continuous batching(连续批处理),请求结束一个就立刻补一个,不用等整个批次跑完,避免了GPU空转。
另一个被低估的优势是它提供了OpenAI兼容的HTTP接口。这意味着你原来怎么调用外部API,现在把base_url换成内网地址,请求格式、返回格式几乎不用改。这一点在迁移阶段省了大量研发成本,内部系统切过来基本是无痛的。
2. 部署前的三件套:算力、模型权重与运行环境
2.1 算力评估:先把账算清楚再下手
算力规划是私有化部署里最容易拍脑袋踩坑的环节。很多团队上来就问“7B模型要什么显卡”,但这个问题本身就不完整,因为显存占用不只是模型权重,还要叠加KV缓存和运行时开销。
先说权重大账。模型权重显存大约等于参数量乘以每个参数占用的字节数。以主流FP16精度为例,每个参数占2字节:
- 7B参数量模型,权重约14GB;
- 13B模型约26GB;
- 70B模型约140GB。
注意这只是权重,还没算KV缓存。KV缓存大小跟模型结构、最大序列长度、并发数强相关。序列越长、并发越高,KV缓存占用越大。此外还有CUDA运行上下文、激活值等开销,实际占用通常高出权重一大截。
基于我的经验,一个7B对话模型,最大上下文压在8192 token以内、并发10到20路的话,单张24GB显存的卡能跑,但要留足KV缓存预算,显存利用率别拉满。如果是70B级模型,单卡基本没戏,至少需要两张80GB级别的卡做张量并行,或者四张48GB级别的卡。做预算的时候我习惯按“模型权重+每路请求约0.5到1GB KV缓存×目标并发数+10%冗余”这个口径估算,宁可多算一点,也好过上线前发现塞不下。
2.2 模型权重从哪来、怎么选格式
模型获取这块,主流渠道是开源模型仓库平台。下载时注意选safetensors格式的权重,这种格式把模型参数分片存储,安全性和加载速度都比老式格式好,vLLM解析起来也更顺畅。
下载完的目录结构一般是模型配置JSON、分词器文件和多个分片权重文件,直接把整个目录路径传给vLLM即可,不需要人工改文件。
如果显存实在紧张,可以考虑量化模型。vLLM原生支持AWQ、GPTQ以及FP8等量化格式。量化的本质是把权重从16位压到4位或8位,用更少的显存换一些精度损失。我的建议是:能上FP16或BF16就别量化,生产环境优先保质量;确实需要量化来塞进现有显卡,优先试AWQ或FP8,从评测集上肉眼抽查输出质量,确认可接受再上线。
2.3 环境依赖:版本锁定是保命手段
vLLM对运行环境要求不低,最省心的方式是直接用官方预构建的Docker镜像。自己从源码编译不是不行,但需要手动对齐GPU驱动的计算平台版本、Python版本、深度学习框架版本,组合错了编译一次能折腾大半天。
用官方镜像的好处是依赖已经完全打过架了,拉下来启动就能跑。版本选择上我倾向用固定的版本号标签,而不是latest,因为推理引擎迭代快,前两天还能跑的版本,升级后启动参数或默认行为可能就变了。生产环境里“能用就别乱动”是我的基本原则。
3. 动手部署:把服务真正跑起来
3.1 启动命令逐参数拆一遍
假设权重已经放在内网服务器 /data/models/llm-7b-instruct 目录下,启动服务只需要一条命令:
vllm serve /data/models/llm-7b-instruct \ --served-model-name internal-llm \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --tensor-parallel-size 1 \ --max-num-seqs 32这里每个参数都很关键,我逐个解释。
--served-model-name是暴露给调用方的模型名。它不一定要跟真实权重名一致,我习惯起一个跟业务对得上的名字,这样前端配置不暴露底层模型信息,后续换底座模型也不需要改业务代码。
--host 0.0.0.0表示绑定所有网卡,这样内网其他机器才能访问。如果只在本机调试,用 127.0.0.1 就好,生产环境务必通过内网网关控制访问范围。
--gpu-memory-utilization控制显存使用上限,0.9表示最多吃90%显存。不要设成1.0,显卡驱动、显示输出、推理引擎自身都要占一点显存,设满容易启动即崩溃。
--max-model-len限制最大序列长度。这里有个容易被忽略的关系:这个值同时决定了KV缓存的预留上限,设得太高会吃掉大量显存,设得太低又会截断长文请求。先想清楚业务最长输入是多长,再定这个值。
--tensor-parallel-size是张量并行卡数。单卡就是1,多卡跑大模型时设为显卡数量,引擎会自动把模型切分到多张卡上。
--max-num-seqs限制最大并发序列数。它跟显存、延迟直接相关,我后面单独讲调优。
3.2 用OpenAI兼容接口验证服务
服务起来之后,先别急着接入业务,用命令行快速验证一下。vLLM默认提供OpenAI兼容的 /v1/chat/completions 接口,直接拿curl测:
curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{ "model": "internal-llm", "messages": [{"role": "user", "content": "用一句话介绍什么是大模型"}], "max_tokens": 256, "temperature": 0.7 }'返回的JSON结构和外部API几乎一模一样,包含角色、内容、token用量等字段。业务侧迁移时,只需要把请求的base_url从外部地址改成 http://内网地址:8000/v1 即可,请求体基本不用动。
我还习惯验证一下流式输出:
from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") resp = client.chat.completions.create( model="internal-llm", messages=[{"role": "user", "content": "写一段300字的欢迎语"}], max_tokens=1024, stream=True ) for chunk in resp: if chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="")流式返回对用户体验影响很大,客服问答、内容生成这类场景基本都要流式,长文本场景不流式的话用户得干等十几秒,体验很糟糕。
3.3 压测:用数据说话,别靠感觉
服务跑通之后,我建议立刻做一轮简单压测,哪怕只是用脚本模拟并发,也能暴露很多问题。我常用的方式是用Python的并发请求脚本,同时发50个请求,统计响应时间和吞吐量:
import time from concurrent.futures import ThreadPoolExecutor from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") def one_request(i): start = time.time() resp = client.chat.completions.create( model="internal-llm", messages=[{"role": "user", "content": f"请简要回答第{i}个问题"}], max_tokens=512 ) return time.time() - start with ThreadPoolExecutor(max_workers=50) as pool: t0 = time.time() results = list(pool.map(one_request, range(50))) total = time.time() - t0 print(f"总耗时: {total:.2f}s") print(f"平均单请求耗时: {sum(results)/len(results):.2f}s")压测时重点看两类指标:端到端平均耗时和吞吐量(每秒生成多少token)。如果并发稍微一高,平均耗时就从2秒跳到10秒以上,说明参数还有调整空间,别急着上线。
4. 调到生产可用:吞吐、延迟与稳定性
4.1 影响吞吐和延迟的几个旋钮
vLLM最核心的调优轨迹,其实就是平衡显存、并发和延迟三者关系。
首先是--max-num-seqs。它决定了同时有多少个序列挤在GPU上。这个值调大,吞吐量会上升,但每个请求能分到的算力变少,单个请求的响应会变慢。反过来调小,响应快了,但GPU可能吃不饱,整体吞吐下降。我的经验是先按显存预算给一个初值,然后压测观察,一般7B模型在24GB卡上从16到32之间来回试,找一个吞吐和延迟都能接受的平衡点。
其次是--max-model-len。前面提过它控制序列长度上限,实际上也充当了KV缓存的总量闸门。序列长度上限设得很大,比如32768,每个请求预留的缓存就多,能同时跑的请求数反而变少。有些团队为了“能力上限”把语境设得很长,结果并发上不去,典型的捡了芝麻丢西瓜。先看业务真实需要多长的上下文,再定这个值。
最后是--gpu-memory-utilization。它决定了显存分配策略的激进程度。有些显存是留给KV缓存动态增长的,设低了并发上不去,设高了可能触发显存溢出。我个人习惯设0.85到0.9之间,然后配合压测观察显存水位微调。
4.2 多卡部署的细节与通信开销
大模型单卡装不下时,就得用张量并行,把模型权重和计算切到多张卡上。启动时把 --tensor-parallel-size 设为卡数即可,但有几个细节值得注意。
卡间通信是张量并行的命脉。如果服务器支持高速卡间互联,通信开销很小;如果只靠PCIe总线通信,每步推理都要跨卡同步数据,性能会明显缩水。买机器或租卡时,优先选带高速互联配置的机型,多卡效率才起得来。
多卡模式下,KV缓存也在多卡间均匀分布,每张卡负责一部分。这时候 max-num-seqs 的计算要以单卡显存为准,不是总显存。比如两张80GB卡跑70B模型,权重分两半,各占70GB,每张卡剩下的空间才是可用缓存,加起来的并发上限并不等于单卡80GB容量的两倍。
4.3 用守护进程和指标监控守住服务
服务跑起来之后,第一件要做的事是把它交给守护进程管理,不能依赖人工盯着终端窗口。我习惯写一个systemd服务单元,设置开机自启和异常重启:
[Unit] Description=vLLM Inference Server After=network.target [Service] ExecStart=/usr/bin/docker run --name vllm-server --gpus all \ -v /data/models:/data/models \ -p 8000:8000 \ vllm-image:latest \ --model /data/models/llm-7b-instruct \ --served-model-name internal-llm \ --gpu-memory-utilization 0.9 Restart=always RestartSec=10 [Install] WantedBy=multi-user.target监控方面,vLLM内置了Prometheus指标接口,可以采集吞吐量、显存使用、延迟分布等核心指标。我后续还搭了一个简单的看板,专门盯三个数字:GPU显存利用率、平均首token延迟、每分钟请求数。显存利用率长期贴着上限,就要警惕KV缓存挤占;首token延迟突然变高,很可能是某个长上下文请求把算力吃光了。这些信号越早看到,越能避免业务方先发现问题。
5. 这些坑我基本都踩过
5.1 启动后立刻崩溃,报显存溢出
最经典的坑。症状是服务启动几秒后直接退出,打日志一看是CUDA out of memory。原因通常是 --gpu-memory-utilization 设得过高,或者 --max-model-len 设得过大,导致模型加载完加上KV缓存预分配直接爆显存。
排查顺序我建议这样:先把显存利用率降到0.8,把 max-model-len 降到4096,能启动再逐步往上加参数。如果这样还是溢出,说明你的显存真的不够装这个模型,要么换更大的卡,要么考虑量化版本。
5.2 请求都能响应,但首token迟迟不出来
服务不报错,接口也通,但用户发一句话要等好几秒才开始吐字。这个现象多半是某个长文本请求把计算队列堵住了。长序列生成的每一步计算量都远大于短序列,如果混跑场景里有大量长上下文请求,短请求就得排队等GPU空出来。
解决办法分两层:一是调低 max-num-seqs,限制同时运行的请求数,避免长序列把算力占满;二是在业务层面做分级,长文任务走单独的实例,别跟实时代聊的请求混在一起。我自己后来就是把“实时问答”和“离线文档处理”拆成了两套服务,问题才彻底解决。
5.3 量化模型上线后效果明显变差
为了省显存上了4位量化,结果线上用户反馈“回答变笨了”,一查评测指标确实掉了一截。量化本质是有损压缩,4位量化对模型质量的影响比多数人预想的大,尤其对逻辑推理、数学计算这类任务。
如果必须量化,我的做法是先跑一轮评测对比量化前后的输出,挑影响小的方案;同时准备一条后路——保留FP16权重版本,只对轻量场景走量化实例,重场景走精度更高的实例。生产环境里“效果为王”,省下的显存如果换来用户流失,得不偿失。
5.4 OpenAI兼容也不是百分之百无缝
兼容接口大大降低了迁移成本,但细节上还是有差异的。比如某些外部API特有的字段,vLLM不一定支持;流式返回的分块格式不完全一致;token计数字段在某些版本里会有细微差别。迁移前一定要把业务代码里用到的所有接口字段过一遍,跑通之后再上线。
5.5 服务升级后“变慢”了
我遇到过换了新版本镜像后,同样配置下吞吐反而下降的情况。查了半天发现是默认参数变了,旧的启动命令在新版本里某些参数没显式指定,采用了新的默认值。从那以后我每改一次镜像版本,都会把所有启动参数重新核对一遍,不依赖“应该没变”的感觉。升级之前先看变更记录,升级之后先压测,这两步省不掉。
最后说一点个人体会。vLLM这套服务跑顺之后,真正的运维压力其实不在引擎本身,而在流量模型和组织协作。什么场景该走长上下文、什么场景必须限制并发、谁有权限调参数,这些规则如果不在上线前定清楚,后面就是无休止的救火。我踩过几次坑之后,最大的收获不是学会了更多的启动参数,而是把“上线前先做容量评估、上线后紧贴指标调优”这个流程固定成了团队习惯。如果能重来一遍,我会在第一套服务上线前就多花两天时间把监控、守护进程和压测基线全部配齐,而不是等服务出问题再回来补课。