NAS 放模型,GPU 跑 vLLM:一次启动卡住的排查记录

上周我们把内部 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 服务来说,这比一次性追求"架构漂亮"更实用。

相关推荐
TYKJ0231 小时前
带宽100M但传输只有30M?你的服务器可能该换TCP算法了
后端·算法
小黑蛋9121 小时前
HTTP、TLS 与证书深度解析 —— 从裸奔到全副武装的安全通信之旅
后端
随风,奔跑1 小时前
RabbitMQ
后端·rabbitmq
前端白袍2 小时前
代码规范:RESTful API 全面介绍
后端·restful·代码规范
神奇小汤圆2 小时前
一次 JVM OOM,资深工程师应该如何完整复盘?
后端
孟陬2 小时前
一个小小 alias,提升开发幸福感
前端·后端·命令行
JunLa2 小时前
OpenClaw Agent
后端
AskHarries3 小时前
为什么大多数人创业第一步就错了
人工智能·后端
tyung3 小时前
Go 手写二叉堆优先队列:避开 container/heap 的性能陷阱
数据结构·后端·go