上周我们把内部 AI 服务从"单机测试"挪到"小团队自托管"模式:NAS 统一放模型,GPU 服务器只跑 vLLM,前面接一个 OpenAI-compatible 网关。架构看起来很干净,实际启动时卡住了。
现象也很迷惑:镜像拉下来了,容器是 running,GPU 服务器的 nvidia-smi 也正常,但 /health 一直不通。网关侧只看到 upstream timeout,业务同学看到的则是"模型服务还没起来"。
这篇不是 vLLM 参数大全,而是一次排查记录。重点是我们怎么把"启动慢"拆成镜像、NAS、权限、GPU runtime 和健康检查几段,最后把链路固定下来。
背景:为什么要把模型放 NAS
团队里有几台 GPU 服务器,但模型版本越来越多:
- 基础模型有多个尺寸。
- RAG 服务和代码助手使用的 tokenizer、配置、量化版本不同。
- 有些模型只是临时评测,不适合复制到每台机器。
- GPU 服务器本机盘更想留给缓存、日志和短期实验数据。
所以我们选了一个折中方案:
| 部分 | 放在哪里 | 原因 |
|---|---|---|
| 模型源目录 | NAS | 统一管理版本,避免多台机器重复保存 |
| vLLM 容器 | GPU 服务器 | 贴近 GPU,减少推理链路复杂度 |
| HF / Torch 缓存 | GPU 本机 SSD | 避免推理服务往 NAS 写临时文件 |
| API 网关 | 内网服务节点 | 对上游隐藏模型加载和实例切换细节 |
这个方案能跑,但启动链路比单机复杂。排查时不能只盯着 vLLM 日志。
故障现场
当时服务启动命令大概是这样:
bash
docker run -d \
--name vllm-qwen \
--gpus all \
--ipc=host \
-p 8000:8000 \
-v /mnt/models:/models:ro \
-v /data/vllm-cache:/cache \
-e HF_HOME=/cache/hf \
vllm/vllm-openai:latest \
--model /models/Qwen3-32B \
--served-model-name qwen3 \
--host 0.0.0.0 \
--port 8000
外部看到的问题:
bash
curl -fsS http://127.0.0.1:8000/health
# curl: (7) Failed to connect to 127.0.0.1 port 8000
日志里没有一个很明确的"根因错误",更像是长时间停在模型加载阶段。
我们把排查过程整理成了这张表:
| 顺序 | 排查层 | 当时结论 |
|---|---|---|
| 1 | 镜像拉取 | 不是根因,但需要固定版本,避免不同节点环境不一致 |
| 2 | NAS 挂载 | 宿主机能看到模型,容器内路径需要单独验证 |
| 3 | 权限与目录结构 | 有一台机器容器用户读不到部分文件 |
| 4 | NAS 读速 | 首次加载慢,目录遍历和大文件读取都要测 |
| 5 | GPU runtime | 宿主机正常不等于容器正常 |
| 6 | health 设计 | 冷启动窗口太短会造成误判和反复重启 |
1. 先把镜像层固定下来
我们先没有改 vLLM 参数,而是把镜像层单独验证。
bash
docker pull vllm/vllm-openai:latest
docker image inspect vllm/vllm-openai:latest --format '{{.Id}} {{.Size}}'
docker image inspect vllm/vllm-openai:latest --format '{{json .RepoDigests}}'
再确认镜像内部能导入 vLLM 和 PyTorch:
bash
docker run --rm --entrypoint python3 vllm/vllm-openai:latest -c \
'import torch, vllm; print(torch.__version__); print(torch.version.cuda); print(vllm.__version__)'
有一台新 GPU 服务器在 Docker Hub 拉取上不稳定。我们在这个阶段用毫秒镜像(1ms.run)的 Docker Hub 同名入口做了对照验证:
bash
docker pull docker.1ms.run/vllm/vllm-openai:latest
这里的目的很窄:只排除"镜像入口不稳定、不同节点镜像不一致"这一层。它不会解决 NAS 权限、GPU runtime 或 vLLM ready 问题。排查记录里把这一点写清楚,后面讨论就不容易跑偏。
最终我们决定:生产启动不再裸用 latest,至少记录镜像 digest;关键环境固定版本标签。
2. NAS 路径要从容器视角看
宿主机上看模型目录是正常的:
bash
findmnt -T /mnt/models
df -hT /mnt/models
ls -lah /mnt/models/Qwen3-32B | head
但这个结果只能说明宿主机看到 NAS。vLLM 真正运行在容器里,所以必须进容器看一次。
bash
docker run --rm \
-v /mnt/models:/models:ro \
--entrypoint bash \
vllm/vllm-openai:latest \
-lc 'id; ls -lah /models; ls -lah /models/Qwen3-32B | head'
这一步发现一个小问题:有台机器的 NAS 挂载点是 /data/models,但 compose 里仍然写 /mnt/models。宿主机上人工 ls 查的是正确目录,容器挂进去的却是空目录。
这个问题很普通,但在大模型启动里会被放大。因为日志不一定直接写"你挂错目录了",有时会表现成 tokenizer、config、权重文件加载失败。
我们后来给上线检查加了一条容器内验证:
bash
docker run --rm \
-v /mnt/models:/models:ro \
--entrypoint bash \
vllm/vllm-openai:latest \
-lc 'test -r /models/Qwen3-32B/config.json && echo model-path-ok'
只有输出 model-path-ok,才进入下一步。
3. 权限问题不要靠猜
NAS 目录来自同步任务,文件属主在不同机器上不完全一致。宿主机用户能读,不代表容器用户能读。
我们先看数字 UID:
bash
stat /mnt/models/Qwen3-32B
ls -ln /mnt/models/Qwen3-32B | head
再在容器里测:
bash
docker run --rm \
-v /mnt/models:/models:ro \
--entrypoint bash \
vllm/vllm-openai:latest \
-lc 'id; test -r /models/Qwen3-32B/config.json && echo readable'
问题出在一台机器的 NAS ACL 上:目录能列出来,但部分权重分片对容器用户不可读。vLLM 加载到对应分片时才卡住或失败。
我们没有把整个模型目录改成全局可写,而是给推理服务使用的用户组读权限:
bash
sudo chgrp -R inference /mnt/models/Qwen3-32B
sudo chmod -R g+rX /mnt/models/Qwen3-32B
同时保持只读挂载:
bash
-v /mnt/models:/models:ro
缓存单独放本机:
bash
mkdir -p /data/vllm-cache/hf /data/vllm-cache/torch
这个取舍很关键:模型目录是共享资产,推理服务不应该顺手把临时文件、下载缓存或日志写进去。
4. NAS 慢不一定会报错
权限修完后,服务能起,但冷启动还是明显慢。这个阶段我们才开始测 NAS 读速。
先测大文件顺序读:
bash
time dd if=/mnt/models/Qwen3-32B/model-00001-of-000xx.safetensors \
of=/dev/null bs=64M count=16 status=progress
再测目录遍历和小文件读取:
bash
time find /mnt/models/Qwen3-32B -type f | wc -l
time bash -lc 'for f in /mnt/models/Qwen3-32B/*.json; do head -c 1024 "$f" >/dev/null; done'
这一步的结论是:NAS 足够做模型统一管理,但不适合所有服务都从 NAS 做频繁冷启动。尤其是早上多人测试、服务重建、多个实例同时拉起时,读延迟会被放大。
我们最后做了分层:
| 场景 | 模型读取方式 |
|---|---|
| 临时评测 | 直接从 NAS 读,接受启动慢 |
| 部门试用 | NAS 做源目录,启动前同步到本机 SSD |
| 稳定在线服务 | 本机 NVMe 放当前模型,NAS 只做版本源 |
同步可以用很普通的方式:
bash
rsync -a --info=progress2 /mnt/models/Qwen3-32B/ /data/models/Qwen3-32B/
然后把 vLLM 的模型路径改成本机目录:
bash
--model /data/models/Qwen3-32B
这个改动比继续堆 vLLM 参数更有效。
5. GPU runtime 要独立验证
还有一个误区:宿主机 nvidia-smi 正常,就以为容器 GPU 一定正常。
我们用最小 CUDA 容器测了一次:
bash
nvidia-smi
docker run --rm --gpus all nvidia/cuda:12.4.1-runtime-ubuntu22.04 nvidia-smi
其中一台机器宿主机正常,容器内失败。继续查 runtime:
bash
docker info | grep -i runtime
nvidia-container-cli info
修复后重新配置 Docker runtime:
bash
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker
再用 vLLM 镜像测 PyTorch:
bash
docker run --rm --gpus all \
--entrypoint python3 \
vllm/vllm-openai:latest \
-c 'import torch; print(torch.cuda.is_available()); print(torch.cuda.device_count())'
只有这里返回 True 和正确卡数,才继续看模型规模、显存利用率和并行参数。
6. health 不能只看容器状态
最后一个问题出在健康检查。我们的编排脚本把容器 running 当成服务可用,网关又很快开始转请求。实际 vLLM 还在加载模型。
后来改成三段判断:
bash
docker ps --filter name=vllm-qwen
curl -fsS http://127.0.0.1:8000/health
curl -s http://127.0.0.1:8000/v1/models
再加一个最小 chat completion:
bash
curl -s http://127.0.0.1:8000/v1/chat/completions \
-H 'Content-Type: application/json' \
-d '{
"model": "qwen3",
"messages": [{"role": "user", "content": "ping"}],
"max_tokens": 8
}'
Compose 里的 healthcheck 也给了冷启动窗口:
yaml
healthcheck:
test: ["CMD-SHELL", "curl -fsS http://127.0.0.1:8000/health || exit 1"]
interval: 30s
timeout: 5s
retries: 10
start_period: 10m
这个 start_period 看起来保守,但对 NAS 冷启动和大模型加载很有必要。否则服务还没 ready,就被重启策略打断,最后变成"越重启越起不来"。
最后的启动模板
最后我们把启动命令收敛成这样:
bash
docker run -d \
--name vllm-qwen \
--gpus all \
--ipc=host \
-p 8000:8000 \
-v /data/models:/models:ro \
-v /data/vllm-cache:/cache \
-e HF_HOME=/cache/hf \
-e TORCH_HOME=/cache/torch \
vllm/vllm-openai:latest \
--model /models/Qwen3-32B \
--served-model-name qwen3 \
--host 0.0.0.0 \
--port 8000 \
--gpu-memory-utilization 0.90
配套检查脚本只保留几个关键动作:
bash
set -e
test -r /data/models/Qwen3-32B/config.json
docker run --rm --gpus all nvidia/cuda:12.4.1-runtime-ubuntu22.04 nvidia-smi >/dev/null
docker logs --tail=100 vllm-qwen
curl -fsS http://127.0.0.1:8000/health
curl -s http://127.0.0.1:8000/v1/models
复盘:这次真正改了什么
这次故障不是一个单独的 vLLM bug,最后改动分散在几层:
| 层级 | 改动 |
|---|---|
| 镜像 | 拉取单独验证,记录 digest,避免节点环境漂移 |
| NAS | 容器内验证模型路径,不只看宿主机 |
| 权限 | 按用户组给只读权限,不让服务写共享模型目录 |
| 存储 | NAS 做模型源,高频服务启动前同步到本机 SSD |
| GPU | 容器内单独验证 --gpus all 和 PyTorch CUDA |
| health | 用 /health、/v1/models 和最小请求判断 ready |
我比较认可这次排查里的一个原则:大模型服务启动慢时,不要急着调最后一层参数。vLLM 日志只是最后的表现,前面还有镜像、共享存储、权限、GPU runtime 和编排健康检查。
把这些边界拆清楚之后,后面换模型、扩 GPU 节点、接新网关,排查会轻很多。对于小团队自托管 AI 服务来说,这比一次性追求"架构漂亮"更实用。