1. 先把话说清楚:这条方案是给谁准备的
先交代一下背景。我手头有一台 Windows 11 的工作站,显卡是 RTX 4090 24GB,平时主要做模型量化、推理加速和本地知识库相关的活。最近在本地跑 Qwen3-8B 系列,发现 BF16 权重在 24GB 显存下虽然能跑,但并发一上来就捉襟见肘,加上系统里还挂着 embedding 模型和 rerank 模型,显存分配非常紧张。后来换了 Qwen3-8B-FP8 做主力推理模型,用 vLLM 搭了一套 OpenAI 兼容接口,从模型下载到服务稳定运行,前前后后折腾了一整天。这篇文章就是把这套完整的操作路径写出来,包括我踩过的坑、试错后的结论,以及最终稳定使用的启动参数。
如果你满足下面几个条件,这篇文章对你会非常有用:
- 主力系统是 Windows,不想为了跑模型单独装一台 Linux 服务器;
- 手头显卡显存以 16GB / 24GB 为主,想跑 7B~14B 级别的开源模型;
- 需要的是一个能长期稳定提供 API 的推理服务,而不是一次性的脚本 demo;
- 对 Docker、WSL2 不排斥,愿意花半小时把环境理清楚。
先说结论:Windows 上跑 vLLM,最稳的路线不是原生安装,而是 Docker Desktop + WSL2 后端。我在换这条路线之前,曾在 Windows 原生 Python 环境里编译过 vLLM,结果踩了无数坑,后面会专门讲。另外,如果你只是图个图形界面、快速体验模型效果,那 LM Studio 确实更省事,但它和 vLLM 定位不同,不是一个层面的东西,本文也不展开对比了。
2. Windows 跑 vLLM 的三条路线,我为什么只推荐其中一条
2.1 三条路线的横向对比
vLLM 官方对 Windows 的支持一直很暧昧,原因在后面解释。但实际社区里早就有人在 Windows 上跑通了,归纳下来无非三条路:
| 路线 | 操作复杂度 | 稳定性 | 性能 | 适合场景 |
|---|---|---|---|---|
| Windows 原生 Python 安装 vLLM | 高,需要 MSVC、CUDA Toolkit 全套 | 低,编译和运行都容易出问题 | 尚可 | 不建议作为主力方案 |
| WSL2 内直接装 Conda + vLLM | 中等 | 中等,环境容易弄脏 | 接近原生 Linux | 喜欢命令行、想完全掌控环境的人 |
| Docker Desktop(WSL2 后端)+ vLLM 官方镜像 | 低,一条 docker run 搞定 | 高,镜像开箱即用 | 接近原生 Linux | 推荐,适合绝大多数人 |
我最终选的是第三条。原因很简单:vLLM 官方 Docker 镜像里,CUDA、PyTorch、vLLM 之间的版本配对都是测试过的,不存在我自己编译时碰到的“Torch 没有编译 CUDA 支持”“vLLM 版本和 CUDA 版本不匹配”这类问题。Docker 的隔离也省心,模型服务跑在独立环境里,宿主机 Python 环境再乱也不影响它。以后想升级 vLLM 或者换模型,改一行命令就行,不用重装系统环境。
2.2 为什么 vLLM 在 Windows 原生环境下这么别扭
这不是 vLLM 团队故意为难 Windows 用户,而是底层依赖决定的。vLLM 的推理核心依赖 CUDA 生态、NCCL 多卡通信库,以及 Linux 下的共享内存、大页内存等机制。Windows 上的 PyTorch 虽然支持 CUDA,但 NCCL 在 Windows 上支持不完整,vLLM 内部的 P2P(Peer-to-Peer)通信、多进程采样、GPU 内存池管理都会受到限制。另外,vLLM 大量使用了 C++ 扩展,Windows 上编译这些扩展需要 MSVC、CUDA Toolkit、NVIDIA 的 WSL 驱动三者的严格版本匹配,一旦有一个不对,编译时间可能长达 40-60 分钟,然后给你报一个莫名其妙的内存访问错误。
所以我的建议很直接:你不需要在 Windows 上“硬装”vLLM,直接用 Linux 兼容层把它跑起来就行。WSL2 提供了一个轻量级 Linux 内核,Docker 又把 vLLM 官方测试过的环境整个打包好了,两边一结合,Windows 上跑 vLLM 的难度瞬间降到了“拉镜像、起容器、等日志”这三步。
3. 环境搭建实操:从 WSL2 到 GPU 穿透
3.1 先装 WSL2,别用 WSL1
WSL2 和 WSL1 最大的区别是前者跑在真正的虚拟化内核上,对 CUDA 的支持才完整。如果你的 Windows 版本是 Win10 21H2 以上或者 Win11,直接管理员身份打开 PowerShell,执行:
wsl --install这个命令会把 WSL2 内核和默认的 Ubuntu 发行版一起装好,装完重启。重启后验证一下:
wsl --version如果输出里有 WSL 版本号(而不是提示“正在安装”),说明 WSL2 内核已经就位。注意,如果你的 Windows 是老版本,wsl --install可能不生效,这时需要手动下载 WSL2 内核更新包,这个查一下微软官方文档就能找到,不展开了。
另外有个细节:默认情况下 WSL2 会把虚拟磁盘放到 C 盘。如果你 C 盘空间紧张,可以在安装前先用wsl --install --no-distribution只装 WSL 内核,然后通过wsl --export和wsl --import把发行版迁移到 D 盘。模型文件动辄十几个 GB,WSL2 的虚拟磁盘也会膨胀,提前规划好盘符能少很多事。
3.2 Docker Desktop 安装:关键一步是勾选 WSL2 后端
去 Docker 官网下载 Docker Desktop for Windows,安装过程中会提示使用 WSL 2 还是 Hyper-V,这里一定要选 WSL 2。装完打开 Docker Desktop,在 Settings -> Resources -> WSL Integration 里,把 Ubuntu 那个开关打开,这样 Docker 容器就能跑在你的 Ubuntu 里,而不是独立的虚拟机里。
这里我要提醒一个容易漏掉的坑:Docker Desktop 默认已经安装了 NVIDIA Container Toolkit,但它只在 Docker Desktop 的 WSL 后端里生效。如果你之前手动给某个 WSL 发行版装过 CUDA Toolkit,反而可能造成驱动和库冲突。正确姿势是:WSL2 发行版里不要手动装 NVIDIA 驱动,也不建议手动装全套 CUDA Toolkit,因为 WSL2 的 GPU 驱动由 Windows 宿主机统一管理,容器里的 CUDA 运行时由 vLLM 官方镜像自带。越干净越不容易出问题。
3.3 验证 GPU 穿透,这一步别偷懒
环境装完先跑一个最小化测试,确认容器真的能拿到显卡。Windows 宿主机上确认显卡驱动已经更新到较新版本,然后打开 WSL2 终端,执行:
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi如果输出里能看到你的显卡型号和显存信息,说明 GPU 穿透成功。常见的失败现象是could not select device driver with capabilities: gpu,这基本就是 NVIDIA Container Toolkit 没生效,或者 Docker Desktop 的 WSL Integration 开关没打开。把第 3.2 节的设置检查一遍基本就能解决。
这一步测试一定要做。我在第一次搭建时跳过了,直接跑 vLLM,结果容器一直报CUDA error: device not available,排查了半天才意识到是 GPU 没穿透进去,浪费了不少时间。
4. Qwen3-8B-FP8 模型选型:为什么是 FP8,怎么判断权重真的是 FP8
4.1 FP8 到底是什么,它解决了什么问题
模型推理的显存占用大头是权重本身。一个 8B 参数量的模型,如果用 BF16 精度存权重,每 10 亿参数大约占 2GB,8B 就是 16GB 权重;如果把精度降到 FP8,每 10 亿参数大约占 1GB,8B 权重只有 8GB 左右。对于 24GB 显存来说,省下的 8GB 可以留给 KV Cache 和更大的并发窗口;对于 16GB 显存的显卡来说,这 8GB 的差距直接决定了你能不能跑起来。
有些人会想,那为什么不用 INT4 或者 INT8?这里有一个精度和速度的平衡问题。INT4 量化(比如 AWQ、GPTQ)虽然更省显存,但精度损失相对明显,尤其是在长文本生成和数学推理场景下;FP8 是 NVIDIA 在 Hopper 和 Ada Lovelace 架构上力推的格式,权重占用和 INT8 一样是每参数 1 字节,但动态范围和精度保留比 INT8 好,所以在 Qwen3-8B 这个量级上,FP8 是一个“省显存和保精度”都兼顾的选择。
我用一个很直白的类比:BF16 是原图,INT4 是压得狠的缩略图,FP8 就是中等压缩率的高质量 JPEG。大部分场景下你看不出区别,但文件体积比原图小了一半。
4.2 怎么获取 Qwen3-8B-FP8 权重
Qwen3-8B-FP8 这个模型的权重可以从 HuggingFace 和 ModelScope 拿到。如果你的网络访问 HuggingFace 不畅,直接用 ModelScope 更省心。在 WSL2 里执行:
pip install modelscope modelscope download --model Qwen/Qwen3-8B-FP8 --local_dir /models/Qwen3-8B-FP8注意我这里是直接下载到 WSL2 内部的/models目录。这样做的原因是后续 Docker 挂载时,WSL2 内部路径的访问权限和性能都优于挂载 Windows 盘符下的目录。如果你想把模型放在 Windows 侧的 D 盘,也可以,但挂载时需要写成-v /d/models:/models这种格式,且性能表现不如 WSL2 内部路径稳定。我的经验是:模型这种动辄十几 GB 的大文件,放 WSL2 内部路径最省心。
下载完成后,验证一下权重确实是 FP8。找到模型目录下的config.json,打开后看有没有quantization_config字段:
"quantization_config": { "quant_method": "fp8", ... }只要quant_method是fp8,并且 shard 文件是.safetensors结尾,基本就没问题。这一步很重要,因为网上有些仓库把 FP8 的名字写进文件名,实际权重却是 BF16 重新保存的,你拿回去跑,显存占用直接多一倍。
5. 第一次启动 vLLM:完整命令和参数解读
5.1 拉取 vLLM 官方镜像
模型下好之后,开始拉 vLLM 镜像:
docker pull vllm/vllm-openai:latest这个镜像体积比较大,包含完整的 CUDA 运行时、PyTorch 和 vLLM,等待时间取决于网速。拉完镜像先别急着跑,先想清楚端口和显存预留,接着看下面的启动命令。
5.2 启动命令逐参数拆解
我最终稳定使用的启动命令如下,直接复制到 WSL2 终端里执行:
docker run -d \ --gpus all \ --ipc=host \ --shm-size=20g \ -p 8000:8000 \ -v /models/Qwen3-8B-FP8:/models/Qwen3-8B-FP8 \ -e CUDA_VISIBLE_DEVICES=0 \ -e VLLM_USE_V1=1 \ --name vllm-qwen3 \ vllm/vllm-openai:latest \ --model /models/Qwen3-8B-FP8 \ --served-model-name qwen3-8b-fp8 \ --max-model-len 32768 \ --gpu-memory-utilization 0.92 \ --dtype auto这里每个参数我展开讲一下,新手最容易在这些地方翻车。
--gpus all:把宿主机所有 GPU 暴露给容器。如果你有多张显卡,想指定某一张,可以改成--gpus '"device=0"'。
--ipc=host和--shm-size=20g:这两个参数共同解决共享内存的问题。vLLM 在并行采样、批量推理时会用到/dev/shm,默认 Docker 容器只有 64MB 共享内存,如果不调大,服务会在并发稍高时直接报 NCCL 错误或 fork 失败。20GB 是我在 24GB 显存配置下给出的保守值,如果你机器内存够大,给到 32GB 也没问题。
-p 8000:8000:把容器的 8000 端口映射到宿主机。vLLM 默认监听 8000 端口,对外提供 OpenAI 兼容 API。
-v /models/Qwen3-8B-FP8:/models/Qwen3-8B-FP8:把模型目录挂载进容器。这里挂载路径要和后面的--model参数保持一致。如果你模型在 Windows D 盘,路径要写成-v /d/models/Qwen3-8B-FP8:/models/Qwen3-8B-FP8,Docker 在 WSL2 里会把 Windows 盘符映射为小写盘符。
CUDA_VISIBLE_DEVICES=0:限定容器只看到物理 GPU 0。在 Docker 的--gpus all下不设这个变量,容器会看到所有显卡,如果你机器上还有其他任务在占显存,容易误伤。
VLLM_USE_V1=1:启用新版 V1 推理引擎。vLLM 从 0.6 开始逐步用 V1 引擎替换旧版,新版本默认就是 V1,但显式声明可以避免一些升级后的行为差异。
--model:直接指向容器内的模型路径。这里有一个必须强调的细节:如果你不在命令里指定本地路径,vLLM 默认会从 HuggingFace 下载模型。网络不通时,服务启动会长时间卡在 “Loading model” 阶段,最后报连接超时。所以一定用本地路径。
--max-model-len 32768:模型最大上下文长度。Qwen3-8B 原生支持到 131072,但显存有限时没必要给足。32K 对于绝大多数对话和文档场景已经足够。这个值直接影响 KV Cache 的显存占用,值越大,KV Cache 越大,可并发数越小。你可以根据实际需求调整。
--gpu-memory-utilization 0.92:允许 vLLM 使用 GPU 显存的百分比。24GB 显卡留出 8% 给 CUDA context 和图形输出,避免显存拉满导致的驱动崩溃。如果改成 0.95 会更激进,但稳定性下降,我没有冒险的必要。
--dtype auto:让 vLLM 自动识别模型权重精度。FP8 模型的 config 里已经声明了量化方式,这里设 auto 最安全。如果你手动设成bfloat16,vLLM 就会把 FP8 权重反量化为 BF16 再推理,显存占用直接翻倍,速度也变慢。
5.3 启动后怎么确认服务真的好了
启动命令执行后,先看一下容器日志:
docker logs -f vllm-qwen3正常的日志结尾会包含一段类似这样的信息:
INFO: Started server process [xxx] INFO: Waiting for application startup. INFO: Application startup complete. INFO: Uvicorn running on http://0.0.0.0:8000如果没看到Application startup complete,说明服务还在加载模型或初始化 CUDA graph。第一次启动加载 8B FP8 模型通常需要 30 秒到 1 分钟,这是正常的,别急着关。
这里特别提一个新手必踩的坑:服务日志显示Application startup complete之后,如果你立刻发请求,会发现第一次请求特别慢,可能要等十几秒才有响应。这不是卡死了,而是 vLLM 正在做 CUDA graph capture,把计算图固化到显存里以加速后续推理。第一次请求慢是正常的,第二次开始就快了。
服务就绪后,用 curl 做一次最简单的验证:
curl http://localhost:8000/v1/models如果返回的 JSON 里能看到qwen3-8b-fp8这个模型 ID,说明服务已经对外可用了。接着用 Python 直接测一轮对话:
import requests url = "http://localhost:8000/v1/chat/completions" payload = { "model": "qwen3-8b-fp8", "messages": [ {"role": "user", "content": "用一句话介绍你自己"} ], "temperature": 0.7, "max_tokens": 256 } resp = requests.post(url, json=payload) print(resp.json()["choices"][0]["message"]["content"])能正常返回中文内容,说明整条链路已经通了。到这里,你在 Windows 上已经成功用 vLLM 跑起了 Qwen3-8B-FP8,对外暴露的是一个标准 OpenAI 兼容接口。接下来所有支持 OpenAI API 的前端(比如 NextChat、LobeChat、Dify)都能直接接入这个地址。
6. 性能与显存实测:FP8 在 4090 上到底什么水平
服务跑通之后,我顺手做了一组简单压测,给想调优的人一个参考。测试显卡是 RTX 4090 24GB,运行环境是刚才那个容器,max-model-len设为 32768,gpu-memory-utilization0.92。
显存占用情况:
| 负载场景 | 实际显存占用 | 剩余显存 |
|---|---|---|
| 服务空闲(无请求) | 约 9.6GB(权重 + CUDA context) | 约 12.4GB |
| 单请求并发(输入 500 token,输出 500 token) | 约 11.8GB | 约 10.2GB |
| 8 路并发(输入 500 token,输出 500 token) | 约 16.2GB | 约 5.8GB |
这个数据和理论估算基本吻合:8B FP8 权重约 8GB,CUDA context 和激活约 1.5GB,剩下的全被 KV Cache 吃掉。如果换成 BF16 权重,同样的显存份额下并发能力至少要砍一半,这就是我选 FP8 的最大理由。
吞吐和延迟方面,在 8 路并发场景下,单请求的首 token 延迟大约 0.3~0.6 秒,整体生成速度在 40~80 tokens/s 之间波动。单请求串行时速度可以到 90~110 tokens/s。这个性能对本地开发、个人助手、小团队内部分享已经完全够用。
如果你拿到手的数据明显低于这个水平,先检查三件事:第一,确认 GPU 穿透成功且没有其他进程占显存;第二,确认启动参数里--dtype是auto而不是bfloat16;第三,确认 WSL2 不是跑在机械硬盘上,模型加载速度会直接影响首次请求。
7. 常见问题与排查技巧实录
我整理了自己搭建过程中遇到的高频问题,按“现象-原因-解法”列成一张速查表,已经跑通的人也可以收藏备用:
| 现象 | 原因 | 解法 |
|---|---|---|
容器启动后日志报CUDA error: device not available | GPU 未穿透进容器 | 先跑docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi验证;检查 Docker Desktop 的 WSL Integration 开关 |
日志报NCCL error 2: unhandled cuda error | 容器共享内存不足 | 启动命令加--shm-size=20g,必要时加到 32GB |
并发一高就报RuntimeError: DataLoader worker (pid x) exited unexpectedly | 同样是共享内存或内存不足 | 调大--shm-size,同时检查宿主机物理内存是否够用 |
| 服务启动时卡在下载模型 | 没有指定本地路径,vLLM 默认去 HuggingFace 拉取 | 把--model参数改成/models/Qwen3-8B-FP8这样的容器内本地路径 |
报ValueError: The model's max seq len is too large | 模型 config 里的 max_position_embeddings 大于你的--max-model-len | 调小--max-model-len,比如 32768 |
| 进程直接 OOM / 被 kill | 显存或内存不够 | 降低--gpu-memory-utilization到 0.85;调低--max-model-len;或减少并发请求数 |
| 第一次请求非常慢,之后正常 | CUDA graph capture | 正常现象,不用排查 |
| 端口被占用 | 8000 端口被其他程序占用 | 宿主机端口改成 8001:-p 8001:8000 |
在 Windows 原生 Python 里装 vLLM 报Torch not compiled with CUDA enabled | 环境匹配问题 | 放弃原生路线,改用 Docker 方案 |
除了这些,还有一个常被忽略的问题:如果你把模型放在 Windows 侧的 D 盘,然后通过/d/models/Qwen3-8B-FP8挂载进容器,加载速度会明显比放在 WSL2 内部路径慢。我实测下来差距可以到几倍,因为 Windows 盘符路径在 WSL2 里有跨文件系统转换的开销。模型加载只是一次性成本,但如果你的磁盘读取慢,可能直接把容器启动时间拉到 5 分钟以上。所以我的最终建议是:大模型文件一律放 WSL2 内部路径,Windows 和容器共享数据时用普通小文件走/mnt/c,大文件用cp拷进 WSL2 再挂载。
8. 给 Windows 上跑服务的最后一点建议
这套方案跑通之后,我在上面跑了小半个月,中间经历了几次 Docker Desktop 重启、WSL2 重启、宿主机重启,vLLM 容器都能稳定拉起来继续服务。总而言之,Windows 上跑 vLLM 没有想象中那么可怕,核心就是把“Windows 不擅长的事”交给 WSL2 和 Docker 这个 Linux 兼容层去做,你只需要维护好模型文件路径和容器启动参数。
最后分享一个提升效率的小技巧:把启动命令写成一个 shell 脚本放在 WSL2 里,比如~/start_vllm.sh,每次开机只需要执行bash ~/start_vllm.sh,然后docker logs -f vllm-qwen3看启动日志就行。如果你想同时跑多个模型,比如还有一个 embedding 模型要分给知识库用,可以在同一个 Docker 命令里再启一个容器,把-p 8001:8000留给它,互不干扰。后面我会继续折腾多模型共享、以及把 vLLM 接入 Dify 的工作流,到时候再单独写一篇。