最近给一个小团队的内部 AI 服务做上线前检查。Demo 阶段很顺:单台 GPU、一个 vLLM 容器、外面接 OpenAI-compatible API。真正准备给全组试用时,问题变成了:GPU 要不要加?p95 能不能接受?冷启动会不会把网关拖超时?

我最后没有先谈扩卡,而是先做了三件便宜的事:镜像缓存验证、GPU runtime 验证、最小并发压测。

背景

服务结构很普通:

业务调用方 -> 网关 -> vLLM(OpenAI API) -> GPU
                      |
                      +-- 模型目录 / 缓存目录

单人请求没问题,但上线前至少要回答:

  • 新机器能不能稳定拉到同一套 vLLM / CUDA 镜像。
  • 容器里是否真的看得到 GPU。
  • 模型缓存有没有复用,冷启动是不是每次都慢。
  • 10、20、50 并发下 p95 和错误率是什么。

镜像先做可重复验证

我不喜欢一上来就 docker compose up -d,因为镜像拉取、模型加载、GPU runtime 会混在同一段日志里。

先单独拉镜像:

docker pull vllm/vllm-openai:latest
docker image inspect vllm/vllm-openai:latest --format '{{.Id}} {{.Size}}'
docker run --rm --entrypoint python3 vllm/vllm-openai:latest -V

如果新 GPU 机器卡在上游镜像入口,就在这一层换成多源入口验证。我这里用过毫秒镜像(1ms.run)的同名路径:

docker pull docker.1ms.run/vllm/vllm-openai:latest
docker pull docker.1ms.run/nvidia/cuda:12.4.1-runtime-ubuntu22.04

重点不是“换源后万事大吉”,而是把镜像层单独变成可验证项。镜像层过了,再看后面的 runtime 和压测。

GPU 要在容器里看

宿主机 nvidia-smi 正常不够。容器内要再跑一遍:

nvidia-smi
docker run --rm --gpus all nvidia/cuda:12.4.1-runtime-ubuntu22.04 nvidia-smi

这个检查能提前排掉一类低级但很耗时的问题:驱动正常,容器没拿到 GPU;或者 --gpus all 忘了传,日志却看起来像 vLLM 自己的问题。

缓存目录必须单独放

模型目录只读,缓存目录单独挂:

mkdir -p /data/vllm-cache/hf /data/vllm-cache/torch

docker run --rm --gpus all   -p 8000:8000   -v /mnt/models:/models:ro   -v /data/vllm-cache:/cache   -e HF_HOME=/cache/hf   -e TORCH_HOME=/cache/torch   vllm/vllm-openai:latest   vllm serve /models/Qwen3-32B --host 0.0.0.0 --port 8000 --served-model-name qwen3

这样冷启动慢时,可以区分是模型太大、缓存没命中,还是每次都在重复初始化。

压测只跑平均值没意义

我先用 k6 跑 10 个 VU,3 分钟,看失败率和 p95:

import http from 'k6/http';
import { check } from 'k6';

export const options = {
  vus: 10,
  duration: '3m',
  thresholds: {
    http_req_failed: ['rate<0.02'],
    http_req_duration: ['p(95)<8000'],
  },
};

export default function () {
  const body = JSON.stringify({
    model: 'qwen3',
    messages: [{ role: 'user', content: '给出一个上线检查清单' }],
    max_tokens: 128,
  });
  const res = http.post('http://127.0.0.1:8000/v1/chat/completions', body, {
    headers: { 'Content-Type': 'application/json' },
  });
  check(res, { ok: r => r.status === 200 });
}

然后只扩一个变量:10、20、50 并发分别跑。每次记录 p95、失败率、GPU 利用率和 ready 耗时。

复盘

最后发现,第一轮瓶颈不是 GPU 算力,而是冷启动和缓存没跑稳。服务重启后 ready 太慢,网关窗口又偏短,所以业务侧看到的像是“模型服务不稳定”。

这类问题如果直接买 GPU,很容易花钱但不解决主要矛盾。更合理的顺序是:

  1. 镜像层固定 tag/digest,并验证新节点可拉取。
  2. 容器内验证 GPU 可见性。
  3. 模型目录只读,缓存目录独立。
  4. 先跑小并发基线,再决定限流、预热或扩容。

GPU 很贵,排查命令很便宜。上线前先把这几层跑清楚,扩容决策才不会靠感觉。