直接进入正题。这篇教程写的本地 AI 部署,指的是把大语言模型跑在你自己的电脑或服务器上,而不是调用云端 API。我用 Qwen2-7B(通义千问 2 代的 7B 参数版本)配合 vLLM 推理引擎来做全套流程,从环境搭建到启动服务,实测下来半小时左右确实能搞定,前提是你有一块显存够用的 NVIDIA 显卡,或者愿意用 CPU 慢慢跑(后面会讲两者的差别)。
在这篇里我会把每一步的关键选择和背后逻辑都讲清楚,包括为什么选 vLLM 而不是其他框架、量化版本怎么选、显存不够怎么办、上线之后哪些参数和性能有关,这些东西文档里写得不详细,但恰恰是实际部署时最影响使用感受的部分。
1. 部署前必须想清楚的三件事
很多人一上来就急着装环境,结果折腾一整天还在跟依赖库打架。我的建议是,动手之前先花十分钟把下面三个问题定下来,后面会顺利很多。
1.1 显存容量决定你的“模型上限”
跑 7B 模型,参数本身就要占 14GB 左右(以 FP16 精度计算,约 7B 参数 × 2 字节),这还只是权重文件的大小。实际运行时,vLLM 要为 KV Cache 预留内存,加上激活值、CUDA 上下文、Python 进程本身的占用,真实显存需求会明显超出模型权重的体积。
所以我的判断标准是:
- 16GB 显存:跑 Qwen2-7B 的 FP16 原版,比较勉强,但配合 vLLM 优化后可用,KV Cache 会偏小,长对话容易爆显存。
- 24GB 显存:跑 FP16 原版比较舒服,并发和上下文长度都有余量。
- 12GB 显存:需要上 AWQ 或 GPTQ 量化版,4bit 量化后权重约 5GB,可以跑起来但速度会略降。
- 8GB 显存:建议换 Qwen1.5-4B 或者 3B 的量化版,7B 的量化虽然能挤一挤,但基本没有吞吐余量,体验会很差。
如果你预算有限但想玩 7B,最推荐的路子是:一张 12GB 显卡(比如某款 3060 级别)+ vLLM + AWQ 4bit 量化版 Qwen2-7B,这是性价比最高的组合。
1.2 vLLM 不是唯一选择,但它是目前最省心的一个
国内聊部署的时候,装的是 vLLM 还是 llama.cpp 往往是分水岭。我用过几种框架,简单梳理一下:
| 框架 | 核心优势 | 适合场景 | 主要代价 |
|---|---|---|---|
| vLLM | PagedAttention 显存优化,高并发吞吐,OpenAI 兼容 API | 多用户服务、批量推理、API 服务 | 显存管理重,显存小的机器不友好 |
| llama.cpp | CPU 也能跑,量化支持成熟,轻量 | 无 GPU 的机器、嵌入式设备、单机单用户 | 并发差,纯 CPU 速度慢 |
| Transformers 原生态 | 上手快,Graphic 简单,微调方便 | 调试模型、实验性开发 | 推理无优化,吞吐量低,显存浪费严重 |
| Ollama | 一键部署,使用门槛极低 | 个人电脑、快速体验、非技术用户 | 灵活度低,内部机制黑盒,调参受限 |
vLLM 的优势不在于单请求延迟(单请求延迟其实不如 eager 模式下的 Transformers 快多少),而在于吞吐量和显存效率。PagedAttention 机制把 KV Cache 切块管理,显存碎片化和冗余少得多,所以多个请求同时进来时它能压得住,这也是把它用于本地私有 API 服务最核心的价值。
1.3 模型下载渠道和版本选择
Qwen2-7B 的权重托管在魔搭社区和 Hugging Face 上,国内机器建议直接从魔搭拉,速度快很多。注意别下错版本,Qwen2 系列里文件较多的是model-00001-of-00004.safetensors这种分片格式,不是单文件。
你需要明确选择的是基础版(Base)还是对话版(Instruct/Chat)。做本地聊天、API 服务、私有助手,选 Instruct 版;想拿来做文本补全、微调底座,才选 Base。这篇教程全部以Qwen/Qwen2-7B-Instruct为准。
另外,vLLM 的模型注册机制决定了它对官方模型文件格式要求较高。如果不是特别了解改造流程,不要自己去合并 safetensors 分片或转 GGUF 再塞给 vLLM,直接用官方原版格式就行。vLLM 官方也支持 AWQ 量化格式的模型,但同样建议优先下载别人已经量化好后统一发布的版本,而不是自己现场量化(量化需要的校准数据集和耗时,往往比部署本身还要麻烦)。
2. 环境准备与依赖安装
前面确定好显卡和模型之后,下面这部分是实际动手的第一步,也往往是坑最多的环节。我装过 CPU、GPU、conda、裸 Python 好几种环境组合,这里直接给出目前最稳妥、重复成功率最高的一套流程。
2.1 操作系统与 Python 环境建议
vLLM 在 Linux 上最顺滑,Windows 原生支持一直是“能用但不算最佳”的状态(虽然有 Windows 分支,但很多算子和库的预编译包不如 Linux 完善)。我的建议:
- 有 Linux 服务器(Ubuntu 20.04/22.04 实测最稳)就直接 Linux。
- Windows 用户优先开 WSL2(Ubuntu 22.04),在子系统里建虚拟环境跑 vLLM。
- 没有 Linux 也没有 WSL,那就用 Windows 原生 Python 环境,但大概率会遇到需要自行编译或手动装 C++ 运行库的情况,时间会拉长。
Python 版本我统一用 3.10 或 3.11。vLLM 对 Python 版本有要求,太新的 Python(比如 3.12)在 pip 解析依赖的时候可能找不到对应版本的预编译 wheel,反而会现场编译,时间长不说还容易出幺蛾子。
2.2 CUDA 与驱动检查
这一步别凭感觉来,直接在终端里检查:
nvidia-smi重点关注两行:上面是驱动版本(比如 535.xxx),下面是 CUDA 版本(比如 12.2)。vLLM 在 pip 安装时通常会自带一套与 CUDA runtime 匹配的预编译算子库,所以对系统全局的 CUDA Toolkit 要求没那么严格,但驱动版本必须够新。我实测的稳定组合是:驱动版本 ≥ 535,CUDA 11.8 或 12.1 均可。
还有一个常见问题是“我明明有 GPU 为什么 vLLM 检测不到”,大概率是以下原因:
- 跑在了 WSL 里但没有安装 Windows 侧的 GPU 驱动(WSL2 需要 Windows 侧驱动支持)。
- 用的是 CPU 版 PyTorch,后面装 vLLM 时和现有 torch 冲突,vLLM 报了无法加载 CUDA 算子。
- 多个 Python 环境混用,装错了环境。
2.3 创建虚拟环境并安装依赖
下面是我验证过的一套完整流程,直接复制到终端执行:
# 创建专用虚拟环境 conda create -n vllm-env python=3.11 -y conda activate vllm-env # 安装 vLLM(这里会自动拉取匹配的 PyTorch 版本) pip install vllm # 安装一些后面常用的辅助库 pip install openai # 用于调用 OpenAI 兼容接口 pip install flask # 做简单的前端界面或网关(可选)不同时期 vLLM 的版本变化很快,但我给的这种“不动脑子指定版本、直接 pip 最新版”的方法反而是最稳的。很多教程会指定vllm==0.4.x之类的旧版本,其实没有必要,新版本对 Qwen2 的支持更好,安装时直接用默认的最新版本即可。
装完之后跑一下验证:
python -c "import vllm; print(vllm.__version__)"能正常打印版本号,就说明基础环境没有问题了。如果报 CUDA 相关错误,先检查上面说的驱动和 PyTorch 版本,再回头重新装。
3. 模型下载与存储路径规划
环境就绪之后,第二步是准备模型权重。这一步看起来简单,其实也藏着不少决定后续体验的细节。
3.1 从魔搭社区拉取模型
我默认你使用的是中国大陆网络环境,所以在魔搭社区下载模型是最省时的。
# 安装 modelscope 库 pip install modelscope # 用 Python 下载模型 python -c " from modelscope import snapshot_download snapshot_download('Qwen/Qwen2-7B-Instruct', local_dir='./models/Qwen2-7B-Instruct', ignore_file_pattern=['*.pt', '*.bin']) "这里解释几个我踩过的关键点:
local_dir参数指定下载到当前目录下的指定文件夹,我习惯把模型统一放在独立磁盘区域,方便多个项目共用,不用每次重复下载。ignore_file_pattern屏蔽掉不需要的.pt和.bin文件。Qwen2 官方仓库里其实还有一些别的东西,比如可以忽略的检查点文件,而真正推理只需要safetensors格式的权重和两个json配置文件就够。- 下载完后务必检查目录内是否包含
config.json、tokenizer.json、tokenizer_config.json,还有完整的model-*.safetensors分片文件。如果迁移到别的机器,这几个文件一个都不能少。
3.2 磁盘空间和权限规划
Qwen2-7B 的权重实际占用空间大约 15GB(safetensors 分片总大小),加上 tokenizer 文件基本可忽略。下载前先df -h看一眼磁盘余量,低于 20GB 就别勉强了,后续还有缓存和日志占用。
另一个容易踩的坑是文件权限。如果跑 vLLM 的用户和下载模型的用户不是同一个(最常见的就是 root 下载、普通用户运行,或者反过来),会出现模型目录可读但内部分片文件不可读的情况。我习惯是用什么用户运行服务,就用什么用户下载模型,省去后续chmod的折腾。
3.3 扩展:下载量化版模型(低显存方案)
如果你的显存不足 16GB,需要下载量化版模型。同样在魔搭上搜索 Qwen2-7B-Instruct 相关的 AWQ 或 GPTQ 版本(文件里带awq或gptq字样),这是我测试过的可行方案。
python -c " from modelscope import snapshot_download snapshot_download('Qwen/Qwen2-7B-Instruct-AWQ', local_dir='./models/Qwen2-7B-Instruct-AWQ') "AWQ 量化版权重大约是 5~6GB,显存占用明显降低,同时精度损失在可接受范围内。不过需要特别提醒:如果做严肃的内容生成或代码补全,建议还是用 FP16 原版。量化模型在小概率下会出现重复、逻辑翻转等怪问题,排查起来反而不容易。
4. 启动 vLLM 服务并做功能验证
模型文件到位、环境就绪,下面进入“启动服务”的核心环节。这也是最紧张的时刻,因为一连串问题往往都是在这个阶段爆发。
4.1 基础启动命令与参数拆解
直接给出我用来启动 Qwen2-7B-Instruct 的标准命令:
python -m vllm.entrypoints.openai.api_server \ --model ./models/Qwen2-7B-Instruct \ --served-model-name qwen2-7b \ --host 0.0.0.0 \ --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 \ --dtype float16每个参数我都拆开讲讲:
--model:指向模型目录(本地路径,不是魔搭的 model id),我上面的命令是相对路径,实际建议写绝对路径,避免在不同工作目录启动时找不到文件。--served-model-name:这个参数很关键。它指定对外暴露的模型名,因为后面用 OpenAI SDK 调用时,model字段要填这个名字。不设置的话默认跟目录名相同,我建议显式指定,免得后面混乱。--host 0.0.0.0:允许所有 IP 访问。如果你只是本机调试,改成127.0.0.1更安全。--port 8000:服务端口。--gpu-memory-utilization 0.9:告诉 vLLM 可以用到底层 GPU 显存的比例。我设 0.9 在 24GB 显存下比较合适;如果只剩 16GB 左右,建议降到 0.8 或 0.7,留出显存给 CUDA context 和零星活动。太高会导致启动阶段直接报 CUDA out of memory,太低则意味着 KV Cache 变小,并发能力下降。--max-model-len 8192:允许的最大上下文长度(输入和输出加起来)。Qwen2-7B 的原生训练长度通常是 32k,设置 8192 意味着 KV Cache 按这个上限预留,显存占用更保守。若显存充裕且需要长文档处理,可以调成 16384 或 32768,但显存不足时超长上下文会直接 OOM。--dtype float16:强制用半精度加载。对于大部分非 A100/H100 级别的消费卡,FP16 是稳定性和性能的平衡点。
启动之后的日志,几行关键提示要注意看:
INFO: Started server process [xxxxx] INFO: Waiting for model to be loaded... INFO: Loading model weights took xxxx seconds INFO: Initializing LLM engine with GPU memory utilization=0.9 INFO: Starting vLLM server on http://0.0.0.0:8000看到Uvicorn running on http://0.0.0.0:8000这行,服务就算启动成功了。加载权重的过程在 HDD 上可能耗时几分钟,SDD 上一般 30 秒内完成,不要误以为卡死了。
4.2 用 OpenAI SDK 写一个测试客户端
启动完成后,用 OpenAI 的 SDK 来测最实用,因为后续无论是接聊天机器人框架还是写内部工具,走的都是同一套协议。
from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" # vLLM 不校验 key,但协议要求填 ) response = client.chat.completions.create( model="qwen2-7b", messages=[ {"role": "system", "content": "你是一个可靠、乐于助人的助手。"}, {"role": "user", "content": "请用 30 个字介绍一下深度学习推理的常见优化手段。"} ], temperature=0.7, max_tokens=512, stream=False ) print(response.choices[0].message.content)跑通这段脚本,一个标准的 OpenAI 兼容的本地大模型服务就正式可用了。后续无论是写 Python 脚本、接入微信群机器人还是做网页聊天,都是基于这套 API 进行二次开发。
关于参数的一点补充说明:
temperature控制随机性,偏低(0.2~0.4)适合代码生成和结构化文本,偏高(0.8~1.0)适合创意文案。max_tokens限制单次回复长度。它会影响整体吞吐和响应时间,不是越大越好。stream=True适合需要流式打字效果的场景。vLLM 对流式的支持很好,基本不会出现断句滞后。
4.3 服务管理的实用技巧
实际部署时服务是要常驻运行的,直接放在终端前台跑不是个办法。我常用的是nohup加日志重定向:
nohup python -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen2-7B-Instruct \ --served-model-name qwen2-7b \ --host 0.0.0.0 --port 8000 \ --gpu-memory-utilization 0.9 \ --max-model-len 8192 > /data/logs/vllm_server.log 2>&1 &每次启动前记得kill掉之前的旧进程,避免端口被占用,或者用lsof -i :8000查看端口占用情况再启动。日志文件会长得很快,建议配合logrotate或定时清空,不然磁盘爆掉是迟早的事。
5. 参数调优与性能观察
服务跑起来之后,接下来的重点工作是性能观察和参数微调。这个阶段最能感受到 vLLM 和别的框架的差异。
5.1 观测指标怎么看
vLLM 启动后日志里会周期性地打印关键统计信息,类似:
Avg prompt throughput: 25.0 tokens/s Avg generation throughput: 180.0 tokens/s Running: 4 reqs, Used KV cache: 35.6%这些指标含义如下:
- Avg prompt throughput:处理输入提示的速度,主要受模型并行度和 prefill 阶段效率影响。
- Avg generation throughput:生成输出 token 的速度,上限越高质量越高。
- Used KV cache:KV 缓存的占用比例。这个值持续接近 100% 时,后续请求可能会排队或触发显存溢出,需要调整并发或上下文长度。
我用 vLLM 的一个直观感受是:并发请求数量增加时,整体吞吐量并不暴跌,而是保持在一个稳定的高位。单个人用可能感知不到,但你尝试同时发 10 个、20 个请求时,就能看出它和 Transformers 默认推理的巨大差别。
5.2 与显存相关的调优思路
如果你在启动时报了显存不足,或吞吐达不到预期,下面这些参数是优先调整项:
| 参数 | 作用 | 调大/调小的影响 |
|---|---|---|
--gpu-memory-utilization | 显存利用上限 | 调大:KV Cache 更多,并发好;但容易 OOM。调小:稳定,但吞吐明显下降 |
--max-model-len | 最大上下文长度 | 调大:支持长文本,但 KV Cache 变大;调小:省显存,但超出长度会被拒绝 |
--max-num-seqs | 并发序列数上限 | 调大:并发高但显存压力大;调小:稳定但吞吐受限 |
--enforce-eager | 关闭 CUDA graph 优化 | 开启:显存省但慢;关闭(默认):快但显存占用高 |
我碰到过一个很典型的例子:在某张 12GB 显存的显卡上跑 Qwen2-7B-Instruct,gpu-memory-utilization设到 0.95 时启动就 OOM,降到 0.85 后正常启动,但并发稍微涨起来又会报“Unable to allocate KV cache”。最后把max-model-len从 8192 降到 4096,才稳定跑起来。所以,显存紧张时优先砍上下文长度,而不是砍并发,这是实操里很管用的经验。
5.3 为什么不用“聊天模板”踩坑
刚接触 vLLM 的朋友最容易忽略一个细节:OpenAI 兼容接口走的是/v1/chat/completions,它内部会自动套用模型的聊天模板;但如果你使用的是/v1/completions(纯补全接口),则不会套用任何聊天模板,需要用原始 token 序列方式组织输入。
部署了 Instruct 模型后,请优先使用chat.completions接口。不是为了好看,而是因为 Qwen2 的格式要求里包含了特殊的结束符和角色标记,如果绕过 chat 模板,只传一段裸文本,模型经常会输出一些莫名其妙的内容(例如把指令也当正文生成出来)。在接第三方框架时,也要确认框架默认调用的是不是 chat 接口。
6. 常见问题排查与避坑实录
VLLM 部署的难点很少集中在“启动”本身,更多是在运行过程中暴露出来的环境问题和资源问题。这里记录几个真实场景,也当作一份速查表。
6.1 启动报错与解决对照
| 错误现象 | 原因 | 解决方案 |
|---|---|---|
CUDA out of memory出现在启动阶段 | 显存利用率设置过高,或 CUDA context 占据额外显存 | 降低--gpu-memory-utilization,或者换量化模型 |
ModuleNotFoundError: No module named 'vllm._C' | 安装过程中没装对预编译包,或旧版本缓存干扰 | 升级 pip、升级 vllm、重建干净环境重装 |
RuntimeError: CUDA error: invalid device ordinal | PyTorch 没识别到显卡,驱动异常 | 检查nvidia-smi,确认驱动安装,确认运行环境不是 CPU 版 |
请求返回404 model not found | --served-model-name和客户端model字段不一致 | 统一两边模型名 |
| 生成速度突然变慢 | 上下文变长导致 KV Cache 占用高,或磁盘交换 | 减小max-model-len,或增加显存利用上限 |
| 服务正常但流式输出中断 | 网络代理或防火墙截断长连接 | 检查代理设置,确保本机访问不经过代理 |
6.2 两个特别提醒
提醒一:不要在生产环境直接跑python -m vllm.entrypoints.openai.api_server裸进程。
虽然它能正常工作,但缺少监控和守护能力。崩溃了不会自动拉起,磁盘满了也不会告警。我做本地服务时,至少会配合systemd或supervisor来做进程守护,再加一个简单的探活脚本,每分钟 curl 一下/v1/models接口,如果连续几次不通就重启服务。
提醒二:多版本 Python 和虚拟环境要严格隔离。
我见过不少人系统里既有 Python 3.8 又有 3.10,还有 Anaconda 和系统 Python 混用,最后 vLLM 报错后怎么查都是依赖冲突。建议一条路走到底:用 conda 建环境、固定在 3.10/3.11、安装好环境后把conda env export导出一份环境清单留底,方便回滚和复现。
6.3 关于“半小时”的一个诚实交代
标题里说的半小时,是在“下载模型已经完成 + GPU 环境没问题”的前提下。如果从零开始包括下载模型,那时间主要花在下载环节:15GB 的模型在 100MB 带宽下大约需要 20~30 分钟,在 10MB 的小水管下可能需要一两小时。
如果想压缩这部分时间,方法也很直接:把下载好的模型目录整个打包备分到移动硬盘或网盘,下次直接用;或者选择量化版模型(文件更小,下载时间能折半)。
7. 后续可以怎么扩展
服务本身跑通之后,剩下的就是按需扩展,这里列几个我自己试过且效果不错的方向。
7.1 搭建一个极简的 Web 聊天界面
后端用 Flask 封装一个接口,前端用几行 HTML + JavaScript 就可以实现一个可对话的页面。核心思路是把 vLLM 的/v1/chat/completions接口包一层:
from flask import Flask, request, jsonify from openai import OpenAI app = Flask(__name__) client = OpenAI( base_url="http://localhost:8000/v1", api_key="EMPTY" ) @app.route("/chat", methods=["POST"]) def chat(): data = request.get_json() messages = data.get("messages", []) resp = client.chat.completions.create( model="qwen2-7b", messages=messages, max_tokens=1024, temperature=0.7 ) return jsonify({"reply": resp.choices[0].message.content}) if __name__ == "__main__": app.run(host="0.0.0.0", port=9000)注意,由于本项目目标是用在本地内网,接口不需要做太复杂的鉴权,但如果要暴露到公网,务必在前面加一层反向代理和 API Key 校验,否则会变成别人都能调用的公共接口,那样不仅消耗显存,还会引发安全问题。
7.2 对接知识库检索(RAG)
Qwen2-7B 在知识类问答上的表现已经不错,但回忆和时效性仍然有限。本地方案可以搭配一个向量数据库(比如某开源的 embedding 工具 + 向量库),把文档切块后存储,用户提问时先做相似度检索,再把检索结果拼进 system prompt,让模型基于检索内容回答。
这个方向的调试重点不在模型部署(因为 vLLM 侧基本不用改),而在于切片长度、检索条数、上下文拼接方式。我实测下来,单轮问答用 4~6 个检索片段、每个片段 200~400 字,效果最为稳定。太长的拼接会稀释关键信息,太短的片段又缺乏上下文。
7.3 上多模型管理
vLLM 虽然没有直接在单进程里同时跑多个模型的官方老版本设计,但新版本已经有相关的服务管理能力;如果你的需求不复杂,更务实的做法是起两个 vLLM 进程,一个跑 7B 用于日常问答,一个跑更小的模型用于分类、摘要等专用任务,再用 Nginx 做按路径或按 header 转发的流量分发。
从这个基础往上走,你能搞得定的事情很多,但前提都是把这个“本地推理服务”真正当成一个基础设施来维护,而不是一个一次性跑通的玩具。
在我实际操作的过程里,最值得注意的一个体会是:大模型部署这件事,真正花时间的部分往往不在“把服务跑起来”的那几分钟,而在“把它跑稳”之后的一连串适配和调优。所以,第一次启动成功只是起点,后续你大概率会反复改参数、换模型、调 prompt,这些经验积累起来才是最有价值的产出。