适用对象:准备大模型应用开发岗、需要把「下载权重 → 起服务 → 调参 → 排错」这条链路真正弄懂的人。
配套手册:同目录
vllm-docker-deployment-guide.md(查命令、查参数用,不讲原理)。本文定位:讲清"为什么",把命令背后的机制、显存账、报错根因串成一条线。
目录
- [0. 怎么用这份文档](#0. 怎么用这份文档)
- [1. 一张图看懂整条链路](#1. 一张图看懂整条链路)
- [2. 第 1 层认知 · 推理框架到底在干什么](#2. 第 1 层认知 · 推理框架到底在干什么)
- [3. 第 2 层认知 · GPU 软件栈的四层结构](#3. 第 2 层认知 · GPU 软件栈的四层结构)
- [4. 第 3 层认知 · 权重从哪来、放哪去](#4. 第 3 层认知 · 权重从哪来、放哪去)
- [5. 第 4 层认知 · 显存账(面试高频)](#5. 第 4 层认知 · 显存账(面试高频))
- [6. 第 5 层认知 · 参数不是玄学,每个都对应一条物理约束](#6. 第 5 层认知 · 参数不是玄学,每个都对应一条物理约束)
- [7. 实战:一条命令拆成两半(docker 参数 / vLLM 参数)](#7. 实战:一条命令拆成两半(docker 参数 / vLLM 参数))
- [8. 故障案例库(都是真实踩过的)](#8. 故障案例库(都是真实踩过的))
- [9. 自测题(附答案要点)](#9. 自测题(附答案要点))
- [10. 一页速查表](#10. 一页速查表)
- [11. 第 6 层认知 · 高并发与排队治理](#11. 第 6 层认知 · 高并发与排队治理)
- 附:学习路线建议
0. 怎么用这份文档
三遍读法:
| 轮次 | 目标 | 时长 | 做法 |
|---|---|---|---|
| 第一遍 | 建立地图 | 30 min | 只读第 1、3、5 章,知道"有哪几层、每层管什么" |
| 第二遍 | 打通原理 | 2 h | 逐章读,第 5 章显存账自己动手算一遍 |
| 第三遍 | 抗遗忘 | 每次部署前 | 看第 8 章故障库 + 第 10 章速查表 + 第 11.8 治理清单 |
一条铁律:能在 30 秒内解释"为什么用 Docker 而不是 pip"、"为什么 --max-model-len 会影响并发"、"为什么排队不等于 GPU 不够",才算真懂。背命令不值钱,命令网上都有。
1. 一张图看懂整条链路
初学者最大的困惑是"这几个东西到底谁管谁"。先定死职责边界:
┌──────────────────────────────────────────────────────────────┐
│ 环节一:拿到权重 │
│ 工具:modelscope CLI / huggingface-cli / git lfs │
│ 产物:一个目录,里面 config.json + *.safetensors + tokenizer │
│ 关键认知:下载完这里,"模型平台"就退场了,跟部署再无关系 │
└───────────────────────────┬──────────────────────────────────┘
↓ 一堆静态文件
┌──────────────────────────────────────────────────────────────┐
│ 环节二:准备运行环境 │
│ 二选一: │
│ a) 宿主机 pip install vllm(直装) │
│ b) Docker 镜像(vllm/vllm-openai) │
│ 关键认知:这一层解决的是"依赖 + 隔离",不解决性能 │
└───────────────────────────┬──────────────────────────────────┘
↓ 一个能 import vllm 的环境
┌──────────────────────────────────────────────────────────────┐
│ 环节三:起推理服务 │
│ 主角:vLLM(或 SGLang / LMDeploy / TGI) │
│ 产物:http://0.0.0.0:8000/v1/chat/completions │
│ 关键认知:这一层才真正决定吞吐、延迟、显存怎么花 │
└───────────────────────────┬──────────────────────────────────┘
↓ OpenAI 兼容 HTTP 接口
┌──────────────────────────────────────────────────────────────┐
│ 环节四:接入应用 │
│ 客户端只认 base_url + api_key + model 名 │
│ 关键认知:应用侧完全不关心你用的是 vLLM 还是 SGLang │
└──────────────────────────────────────────────────────────────┘
三句话记住这张图:
- 模型平台管下载,Docker 管隔离,vLLM 管推理------三方互不替代。
- 换框架只影响环节三:环境、权重、客户端代码全都不用动。
- 换部署方式只影响环节二:pip 直装 vs 容器,对外接口一模一样。
常见误区:"用了 ModelScope 就得用它的 SDK 起服务"。错。它只是个下载器,下下来的就是标准 HuggingFace 格式,谁都能读。
2. 第 1 层认知 · 推理框架到底在干什么
2.1 没有框架时,模型怎么跑
最朴素的写法(也是理解一切的起点):
python
from transformers import AutoModelForCausalLM, AutoTokenizer
import torch
model = AutoModelForCausalLM.from_pretrained(path, torch_dtype=torch.bfloat16).cuda()
tok = AutoTokenizer.from_pretrained(path)
out = model.generate(**tok("你好", return_tensors="pt").to("cuda"), max_new_tokens=100)
print(tok.decode(out[0]))
这段代码能跑,但上线就是灾难,原因有三:
| 问题 | 后果 |
|---|---|
| 一次只能处理一条请求(batch 固定) | 10 个用户并发 = 排 10 次队,GPU 大量空转 |
| 每次请求重新算全量 KV | 长 prompt 延迟极高 |
| 显存按「最长序列 × 最大并发」整块预分配 | 碎片严重,显存利用率常常不到 40% |
推理框架要解决的,就是这三件事。所以"为什么要框架"不是一个部署问题,是一个吞吐问题。
2.2 vLLM 快在哪:两个核心机制
① PagedAttention(分页 KV cache)
传统做法:给每条请求预留一块连续显存,长度按上限算 → 实际用了 500 token,却占着 8192 的坑,浪费 90%。
PagedAttention 借用操作系统的虚拟内存分页思想:把 KV cache 切成固定大小的 block(默认 16 token 一块),用页表把逻辑位置映射到物理 block。
- 显存按需分配,用多少给多少
- block 可以离散存放,不要求连续
- 相同前缀的请求可以共享同一批 block(这就是 prefix caching 的底层)
效果:显存浪费从 60~80% 降到几个百分点,同样一张卡能塞下 2~4 倍的并发。
② Continuous Batching(连续批处理 / 迭代级调度)
传统批处理:一批 8 条请求,必须等最慢那条生成完才换下一批。短请求生成了 10 token 就结束,还得空等别人 500 token。
连续批处理:每生成一个 token 就重新调度一次。哪条结束了立刻腾出位置,队列里的新请求马上插进来。
效果:GPU 几乎不空转,吞吐随并发线性上升到显存上限。
③ 其它常见优化(知道名字即可)
| 机制 | 一句话 |
|---|---|
| Prefix Caching | 系统提示词/长文档这类相同前缀,KV 只算一次 |
| Chunked Prefill | 超长 prompt 分块喂,避免一条长请求饿死其他请求 |
| CUDA Graph | 小 batch 场景把 kernel 调用打包,省 CPU 调度开销 |
| 量化(AWQ/GPTQ/FP8) | 权重精度降级,显存减半、吞吐提升,质量略降 |
面试常问:"vLLM 为什么比 HuggingFace generate 快 10 倍以上?"
答:不是模型变了,是调度和显存管理变了------PagedAttention 解决显存碎片(能塞更多并发),连续批处理解决 GPU 空转(不浪费算力)。两条加起来就是数量级差异。
2.3 框架选型
| 框架 | 强项 | 什么场景选 |
|---|---|---|
| vLLM | 生态最全、OpenAI 兼容最完善、文档多、坑最少 | 默认首选,90% 场景 |
| SGLang | RadixAttention 前缀复用极致、复杂结构化输出/多轮 | 超高并发、大量共享前缀(Agent、RAG 多轮) |
| LMDeploy | 国产卡(昇腾/寒武纪)适配好、自带 W4A16 | 非 NVIDIA 卡、量化优先 |
| TGI | HuggingFace 官方,与 HF 生态紧耦合 | 已在 HF 体系内 |
| Ollama | 一行命令本地跑,体验最好 | 本机试用、Demo,不要上生产 |
Ollama 有个专门的坑:它吃的是 GGUF 格式,不能直接读 safetensors,得先转换+量化。所以"下好 Qwen3-8B 想让 Ollama 跑"是走不通的,要么换 vLLM,要么走 llama.cpp 转换链路。
3. 第 2 层认知 · GPU 软件栈的四层结构
排错时最有用的一张图。报错永远来自某一层,先定位层,再查解法。
┌─────────────────────────────────────────┐
│ 4. 推理框架 vLLM / SGLang │ ← 参数、调度、显存策略
├─────────────────────────────────────────┤
│ 3. 算子库 PyTorch + CUDA runtime │ ← torch 版本必须匹配 cu 版本
├─────────────────────────────────────────┤
│ 2. 容器运行时 nvidia-container-toolkit│ ← 决定容器能不能看见 GPU
├─────────────────────────────────────────┤
│ 1. 内核驱动 NVIDIA driver + CUDA │ ← 宿主机层面,容器内改不了
└─────────────────────────────────────────┘
↑
物理 GPU(显存是硬约束)
3.1 谁依赖谁
- 驱动在最底层 ,由宿主机安装。
nvidia-smi显示的CUDA Version: 12.4是驱动支持的最高 CUDA 版本,不是你装的 CUDA 版本。 - torch 自带 CUDA runtime (wheel 里打包了
nvidia-cuda-runtime-cu12这类包)。所以torch.version.cuda才是"实际用的 CUDA 版本",只要 ≤ 驱动支持的上限就能跑。 - 判断规则:
torch.version.cuda>nvidia-smi里的 CUDA Version → 会报CUDA error: no kernel image is available或直接 import 失败。
这条规则的实际用途 :不用手动装 CUDA Toolkit。pip install vllm 会把匹配的 torch + cuda runtime 一起拉下来,只要驱动够新就行。老教程让你先装 CUDA Toolkit 再装 cuDNN,那是 2019 年的做法,现在纯属添乱。
3.2 容器为什么能看见 GPU
Docker 默认是"看不见硬件的沙箱"。让它看见 GPU 靠两样东西:
nvidia-container-toolkit(宿主机安装):注册一个叫nvidia的容器 runtime--gpus all(启动参数):让 docker 通过这个 runtime 把/dev/nvidia*设备节点 + 驱动库挂进容器
所以:
bash
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
这条命令能打印出显卡,就说明第 1、2 层全通了。它跑不通,后面 vLLM 一条命令都别试------顺序不能颠倒。
漏装的典型报错:
docker: Error response from daemon: could not select device driver "" with capabilities: [[gpu]]
3.3 算力云为什么不能跑 Docker
AutoDL / 恒源云 / 矩池云这类平台的本质:一台物理 GPU 服务器,用容器切分给几十个租户 。你的提示符 root@autodl-container-xxxx 就是证据------你已经在容器里了。
由此推出三个必然后果:
| 后果 | 原因 | 你该怎么做 |
|---|---|---|
| 装不了 Docker | DinD 需要 --privileged,会导致容器逃逸、威胁同机其他租户,平台一律不给 |
直接 pip install vllm,隔离这层平台已经替你做完了 |
| 改不了驱动 | 驱动在宿主机,容器内无权限装内核模块 | 按镜像给的 CUDA 版本选 torch,别想着升级 |
| 访问不了外网 | 默认网络策略 | source /etc/network_turbo 开学术加速(每个新终端都要重来) |
这里有个认知升级 :Docker 的价值是"环境隔离"。当平台已经给了你一个隔离容器,再套一层 Docker 的边际收益为零,成本却极高(权限、磁盘、网络三重阻碍)。所以"用 Docker 部署"这句话只在自有服务器上成立。
判断自己在哪种环境,一条命令:
bash
cat /proc/1/cgroup | head -3 # 出现 docker / kubepods 就是在容器里
ls /.dockerenv 2>/dev/null # 有这个文件也是
4. 第 3 层认知 · 权重从哪来、放哪去
4.1 三条下载路径
| 方式 | 命令 | 优点 | 缺点 |
|---|---|---|---|
| ModelScope CLI | modelscope download --model Qwen/Qwen3-8B --local_dir /path |
国内直连快、几乎不断 | 需要 pip install modelscope |
| HuggingFace CLI | hf download Qwen/Qwen3-8B --local-dir /path |
生态标准 | 国内需代理,huggingface-cli 已改名为 hf |
| Git LFS | git clone https://... |
能看 commit、能切分支 | 大文件慢、易断、不续传,不推荐 |
关键认知 :三者下下来的东西完全一样------都是标准 HuggingFace 目录结构。选哪个只看网络条件。
4.2 三种下载方式产生的目录层级
这是最容易出错的地方,因为同一份权重,层级可以差两层。
① 指定单文件 + --local_dir(平铺)
bash
modelscope download --model Qwen/Qwen3-8B README.md --local_dir ./dir
./dir/README.md ← 就一个文件,不嵌套
② 下整个仓库 + --local_dir(平铺,不建模型ID层)
bash
modelscope download --model Qwen/Qwen3-8B --local_dir /data/Qwen3-8B
/data/Qwen3-8B/
├── config.json
├── generation_config.json
├── tokenizer.json
├── tokenizer_config.json
├── vocab.json
├── merges.txt
├── model-00001-of-00005.safetensors
├── ...
├── model-00005-of-00005.safetensors
├── model.safetensors.index.json
├── LICENSE
└── README.md
③ 不加 --local_dir(走缓存,按模型ID建两层)
~/.cache/modelscope/hub/Qwen/Qwen3-8B/...
改缓存根的写法:export MODELSCOPE_CACHE=/data/modelscope_cache(写进 ~/.bashrc 永久生效)。
有个细节值得记:子路径文件会保留子目录 。下
onnx/model.onnx会得到./dir/onnx/model.onnx,因为onnx/是仓库里真实存在的层级,不是模型ID层。
4.3 路径相对 / 绝对的经典事故
真实踩过的一条:
bash
cd /home/ZHAOJG
modelscope download --model Qwen/Qwen3-8B --local_dir ./home/ZHAOJG/models
结果落到了 /home/ZHAOJG/home/ZHAOJG/models。
原因是 ./ 开头 = 相对路径,跟当前目录拼接:
/home/ZHAOJG + ./home/ZHAOJG/models = /home/ZHAOJG/home/ZHAOJG/models
记住这张表:
| 写法 | 类型 | cwd 为 /home/ZHAOJG 时的落点 |
|---|---|---|
./home/ZHAOJG/models |
相对(点开头) | /home/ZHAOJG/home/ZHAOJG/models ❌ |
home/ZHAOJG/models |
相对(无点) | /home/ZHAOJG/home/ZHAOJG/models ❌ |
/home/ZHAOJG/models |
绝对(斜杠开头) | /home/ZHAOJG/models ✅ |
models |
相对(纯名字) | /home/ZHAOJG/models ✅ |
规则一句话 :路径以 / 开头才是绝对路径;以 . 开头的永远是相对路径,哪怕后面跟着一串看似完整的路径。
事故后的挽救(不用重下):
bash
# 先确认结构
find /home/ZHAOJG -maxdepth 4 -type d -name models
du -sh /home/ZHAOJG/home/ZHAOJG/models
# 目标位置不存在 → 直接改名
mv /home/ZHAOJG/home/ZHAOJG/models /home/ZHAOJG/models
# 清空壳(rmdir 只能删空目录,删不干净会自动报错,安全)
rmdir -p /home/ZHAOJG/home/ZHAOJG 2>/dev/null
安全提示:
rm -rf配相对路径是事故高发区。rm -rf home在/下执行会删掉整个/home。删目录优先用rmdir------它只能删空目录,天然带护栏。
4.4 磁盘规划:系统盘 vs 数据盘
装 vLLM 撑爆磁盘(OSError: [Errno 28] No space left on device)是最常见的上线阻塞,根因永远是把大文件写进了小分区。
先建正确的空间账:
| 内容 | 体积量级 | 该放哪 |
|---|---|---|
| Python 环境 + vLLM + torch + nvidia-* 依赖 | 15~20 GB | 数据盘(conda 环境放 /root/autodl-tmp/envs/vllm) |
| 模型权重(8B bf16) | 16 GB | 数据盘 |
| pip 缓存 | 3~8 GB | 必须关掉 (--no-cache-dir)或重定向 |
| 临时解包目录 TMPDIR | 峰值 5~10 GB | 数据盘(export TMPDIR=...) |
| 日志 | 几百 MB | 数据盘 |
为什么 pip 会撑爆磁盘(Errno 28 的真实机理):
pip 安装时不是"删旧的再写新的",而是先解包到临时目录,再替换。所以在替换 torch 的那一刻,磁盘上同时存在两份 torch(旧的 2~3GB + 新的 2~3GB),峰值占用是平时的两倍。日志里出现
Moving to .../site-packages/torchgen/
from .../site-packages/~orchgen
说明正在做替换,此时空间不够就炸。
三步解决方案(按顺序):
bash
# 1) 立刻回收
pip cache purge
conda clean -afy
rm -rf /tmp/pip-* ~/.cache/pip ~/.cache/torch
# 2) 大件挪走(权重是最大头)
mkdir -p /root/autodl-tmp/models
mv /home/ZHAOJG/models /root/autodl-tmp/models/Qwen3-8B
# 3) 环境也放到数据盘,系统盘一分不占
conda create -p /root/autodl-tmp/envs/vllm python=3.12 -y
conda activate /root/autodl-tmp/envs/vllm
export TMPDIR=/root/autodl-tmp/tmp && mkdir -p $TMPDIR
pip install vllm --no-cache-dir -i https://pypi.tuna.tsinghua.edu.cn/simple
四条永久性习惯:
pip install一律加--no-cache-dir,或先pip config set global.no-cache-dir true- 大数据/大模型进数据盘,系统盘只留系统
conda create -p <数据盘路径>而非-n(-n默认建在系统盘)- 每次大批量操作前先
df -h,养成看数字的习惯
另一条容易忽略的:
mv跨分区时会退化为"复制 + 删除",16GB 要几分钟,中途 Ctrl-C 会留下半份文件。跨分区搬大模型用rsync -avP更稳,可断点续传。
5. 第 4 层认知 · 显存账(面试高频)
部署失败一半以上是显存算错。这里要能手算出数字,而不是靠试。
5.1 权重占用
权重显存 ≈ 参数量 × 每参数字节数
| 精度 | 每参数字节 | 8B 模型 |
|---|---|---|
| FP32 | 4 | 32 GB |
| BF16 / FP16 | 2 | 16 GB |
| INT8 | 1 | 8 GB |
| INT4(AWQ/GPTQ) | 0.5 | 4 GB |
注意:MoE 模型按激活参数算算力,按总参数算显存。Qwen3-30B-A3B 是 30B 总参数、3B 激活------显存要按 30B(60GB)算,跑不动一样跑不动。
5.2 KV Cache 占用(最容易算错的一项)
公式:
KV Cache = 2 × L × H_kv × D × S × B × bytes
2 : K 和 V 各一份
L : 层数
H_kv : KV head 数(GQA 下远小于 Q head 数)
D : head_dim
S : 序列长度(prompt + 输出)
B : 并发数(batch)
bytes : 精度字节数(bf16 = 2)
以 Qwen3-8B 为例(L=36, H_kv=8, D=128, bf16):
单个 token 的 KV:
2 × 36 × 8 × 128 × 2 bytes = 147,456 bytes ≈ 144 KB/token
于是:
| 序列长度 | 单条请求 KV | 32 条并发 |
|---|---|---|
| 4,096 | 0.56 GiB | 18 GiB |
| 8,192 | 1.12 GiB | 36 GiB |
| 32,768 | 4.50 GiB | 144 GiB |
这张表解释了两件事:
- 为什么
--max-model-len从 8192 调到 32768 会"莫名 OOM"------KV 池按上限预分配,直接涨 4 倍 - 为什么长上下文服务的并发能力必然低------单条请求就把显存吃光了
注意 MHA / GQA / MQA 的差别就在
H_kv:Llama-2-7B 是 MHA(H_kv=32),Qwen3-8B 是 GQA(H_kv=8),同样的模型规模,KV 显存差 4 倍。这是"为什么新模型更省显存"的实质原因。
5.3 激活与框架开销
这部分不能精确算,按经验留余量:
| 项 | 典型占用 |
|---|---|
| CUDA context + 框架初始化 | 1~2 GB |
| 激活值(随 batch × seq 增长) | 1~4 GB |
| CUDA Graph 捕获 | 0.5~1 GB |
保守做法:算完权重 + KV 后,再加 3~4GB 缓冲。
5.4 总账公式与配卡判断
需要的显存 = 权重 + KV Cache × 并发 + 激活开销(3~4GB)
可用显存 = 卡容量 × gpu-memory-utilization - 系统占用
实例:Qwen3-8B bf16,目标 8192 上下文、32 并发
权重 16 GB
KV 1.12 × 32 = 36 GB ← 直接超了
开销 3 GB
─────────────────────
合计 55 GB → 单张 24G / 32G 卡都跑不动
所以现实做法是先降并发目标:
| 卡 | 总显存 ×0.9 | 权重 16G + 开销 3G 后 | 8192 上下文的 KV 池 | 估算并发 |
|---|---|---|---|---|
| RTX 4090 24G | ~21.6 GB | ~2.6 GB | 2.6 GB | ≈2 条 |
| RTX 5090 32G | ~28.8 GB | ~9.8 GB | 9.8 GB | ≈8 条 |
| A100 40G | ~36 GB | ~17 GB | 17 GB | ≈15 条 |
| A100 80G | ~72 GB | ~53 GB | 53 GB | ≈47 条 |
这是粗估 (按每 token 1.12 GB / 8192 算)。真实值以 vLLM 启动日志里那行
Maximum concurrency for 8192 tokens per request: Nx为准------它会扣掉 CUDA context、graph capture 等实际开销,通常比粗估乐观一点。
5.5 显存不够的四条出路(按推荐度)
| 方案 | 做法 | 代价 |
|---|---|---|
| 1. 降 max-model-len | 8192 → 4096 | 上下文变短,最省事,先试这个 |
| 2. 降并发上限 | --max-num-seqs 8 |
排队变长,但服务不挂 |
| 3. 量化权重 | 换 AWQ/GPTQ/FP8 版本 | 权重 16GB→8GB/4GB,质量略降,需要重下 |
| 4. 加卡 TP | --tensor-parallel-size 2 |
确实能提并发 (每卡只装 1/N 权重,省下的显存全部释放给 KV),但算力只涨约 1.6 倍(通信损耗),单请求延迟会变差 |
关于第 4 条的常见误解(本节早期版本写错过,已修正):以为"每卡 KV 减半,所以总 KV 不变、并发不涨"。错。算一遍就清楚:
总 KV 池(按 token 计)= (每卡可用显存 − 权重/N − 开销) ÷ (每 token KV ÷ N)
单卡 24G,Qwen3-8B bf16(权重 16G,每 token KV 144KB):
每卡 KV 显存 = 24×0.9 − 16 − 2.4 = 3.2 GB
token 容量 = 3.2 GB ÷ 144 KB ≈ 2.3 万
TP=2:每卡只装 8G 权重,且每卡每 token 只存 72KB(KV head 被切半):
每卡 KV 显存 = 21.6 − 8 − 2.4 = 11.2 GB
token 容量 = 11.2 GB ÷ 72 KB ≈ 16.3 万 ← 约 7 倍
并发涨了约 7 倍,不是 2 倍------权重分片省下来的显存全部变成了 KV 池,这是 TP 常被低估的一面。
但后半句必须记住:KV 池大了 7 倍,算力却只涨约 1.6 倍(每生成一个 token 都要跨卡 all-reduce)。所以 TP 的真实效果是并发数暴涨、但每条请求都变慢------它把"排队"改造成了"大家一起慢",用户对延迟的体感未必变好。这是 11.3 节说"排队问题优先选多副本"的根本原因。
别信估算,看日志。vLLM 启动时会打印真实分配:
GPU 0: NVIDIA GeForce RTX 5090 (total 32.0 GiB, free 31.2 GiB)
model weights take 16.1 GiB
graph capture took 0.8 GiB
Available KV cache memory: 11.3 GiB
GPU KV cache size: 78,000 tokens
Maximum concurrency for 8,192 tokens per request: 9.5x
最后一行 Maximum concurrency for X tokens per request: Nx 就是权威答案,比任何公式都准。养成看它的习惯。
6. 第 5 层认知 · 参数不是玄学,每个都对应一条物理约束
每个参数背后都是一条物理约束,理解了就能自己推导,不用背。
| 参数 | 它控制的物理量 | 调大的后果 | 调小的后果 |
|---|---|---|---|
--model |
权重路径(容器内路径) | --- | --- |
--served-model-name |
对外暴露的模型别名 | --- | 默认用长路径名,客户端难写 |
--dtype |
权重精度 | fp32 显存翻倍 | 老卡(V100)必须 float16 |
--max-model-len |
KV 池的上限依据 | 显存预占暴涨,并发骤降 | 超长请求直接被拒 |
--gpu-memory-utilization |
vLLM 允许占用的显存比例 | 逼近 1.0 → OOM 崩溃 | 太低 → 浪费卡 |
--max-num-seqs |
同时在跑的请求数 | 并发高但单请求变慢 | 排队,吞吐上不去 |
--tensor-parallel-size |
张量并行卡数 | 越大越能装 | 单卡装不下就 OOM |
--api-key |
鉴权 token | --- | 不设 = 裸奔,任何人都能调 |
--enable-prefix-caching |
相同前缀复用 KV | 长系统提示场景吞吐暴涨 | 默认关,白白重算 |
--max-num-batched-tokens |
单次前向的 token 预算 | 吞吐高、TTFT 变差 | 延迟好、吞吐差 |
三个必须内化的因果链:
max-model-len ↑→ KV 池预分配 ↑ → 可用并发 ↓ → 吞吐 ↓max-num-seqs ↑→ 同时处理的请求 ↑ → 共享显存 ↓ → 单条长度受限 / OOM 风险 ↑gpu-memory-utilization ↑→ KV 池 ↑ → 并发 ↑ → 但接近 1.0 会与框架本身抢显存而崩
参数怎么定才不玄:
bash
# 1) 先统计你业务的真实长度分布(拿日志里的 token 数算 P99)
# 2) max-model-len = P99 × 1.2(向上取到 1024 的倍数)
# 3) 起服务,读日志里 "Maximum concurrency for ... : Nx"
# 4) N 太小 → 降 max-model-len 或上量化;N 富余 → 提 max-num-seqs
# 5) 压测验证,看 TTFT / TPOT / 吞吐三个数
7. 实战:一条命令拆成两半(docker 参数 / vLLM 参数)
bash
docker run -d --name qwen3 --gpus all --ipc=host --shm-size=16g \
--restart unless-stopped \
-v /data/models/Qwen3-8B:/models/Qwen3-8B:ro \
-p 8000:8000 \
vllm/vllm-openai:latest \
--model /models/Qwen3-8B \
--served-model-name Qwen3-8B \
--dtype bfloat16 \
--max-model-len 8192 \
--gpu-memory-utilization 0.9 \
--api-key sk-xxx
分界线是镜像名:
docker run -d --name ... -p 8000:8000 ← docker 的参数:容器怎么跑
vllm/vllm-openai:latest ← 镜像名 = 转折点
--model ... --api-key sk-xxx ← vLLM 的参数:程序怎么跑
镜像自带 entrypoint,所以镜像名后的参数等价于在容器内执行 vllm serve --model ...。
换框架只换后半截:
bash
# SGLang(注意镜像无默认 entrypoint,必须写 python3 -m ...)
docker run -d --gpus all --ipc=host --shm-size=16g \
-v /data/models/Qwen3-8B:/models/Qwen3-8B:ro -p 30000:30000 \
lmsysorg/sglang:latest \
python3 -m sglang.launch_server --model-path /models/Qwen3-8B \
--host 0.0.0.0 --port 30000
几个 docker 参数为什么必加:
| 参数 | 不加会怎样 |
|---|---|
--ipc=host |
多进程共享内存不足,大概率卡在启动初始化不动(不是报错,是挂住,最难查) |
--shm-size=16g |
同类问题(不能改 ipc 时的替代方案) |
-v ...:ro |
权重被容器误写;多容器共享时更该只读 |
--restart unless-stopped |
机器重启后服务不自启 |
-p 8000:8000 |
只有容器内可访问 |
宿主机路径 vs 容器内路径------最高频的错误来源:
-v 宿主机路径:容器内路径
/data/models/Qwen3-8B : /models/Qwen3-8B
↑ --model 只能写这个
写了宿主机路径给 --model,报错 ValueError: ... does not appear to have a file named config.json。
8. 故障案例库(都是真实踩过的)
真实踩过的坑,按"症状 → 根因 → 解法"组织。排错时先对照症状。
8.1 Errno 28: No space left on device
- 症状 :
pip install vllm中途报错,日志出现Moving to .../torchgen/ from .../~orchgen - 根因:pip 替换 torch 时新旧两份并存,峰值翻倍;系统盘只有 30~50GB
- 解法 :
pip cache purge+conda clean -afy+ 权重挪数据盘 +conda create -p <数据盘>+--no-cache-dir+export TMPDIR=<数据盘>
8.2 could not select device driver "" with capabilities: [[gpu]]
- 根因 :没装
nvidia-container-toolkit,或装完没重启 docker - 解法 :装 toolkit →
nvidia-ctk runtime configure --runtime=docker→systemctl restart docker→ 用nvidia/cuda:...-base镜像验证
8.3 docker: command not found(且 curl 超时)
- 症状 :
curl: (35) OpenSSL SSL_connect: Connection reset by peer - 根因:在算力云容器里,既没有外网也没有 Docker 权限
- 解法 :放弃 Docker,直接
pip install vllm
8.4 ValueError: ... does not have a file named config.json
- 根因 :
--model写了宿主机路径而非容器内路径;或下载时多套了一层目录 - 解法 :
docker exec qwen3 ls /models/Qwen3-8B确认容器内看得见;宿主机find确认真实层级
8.5 启动卡住不动,无报错
- 根因 :
--ipc=host漏了,共享内存不足 - 解法 :加
--ipc=host(或--shm-size=16g);同时docker logs -f看停在哪一行
8.6 CUDA out of memory 但 nvidia-smi 显示显存够
- 根因 :
gpu-memory-utilization太高(接近 1.0),或max-model-len按原始 32K 算 KV - 解法 :降到 0.85~0.9,
max-model-len按业务实际长度给,别用模型原生最大值
8.7 conda activate: CommandNotFoundError
- 根因:bash 没初始化 conda
- 解法 :
source /root/miniconda3/etc/profile.d/conda.sh(或用数据盘环境时直接source activate /path)
8.8 vllm 命令找不到但 import vllm 成功
- 根因 :装进了 A 环境,
vllm脚本在 A 环境的 bin 里,当前 shell 用的是 B 环境 - 解法 :
which python/which vllm对照,或直接用python -m vllm.entrypoints.openai.api_server
9. 自测题(附答案要点)
先自己答,再对照下面的要点。答不出来的章节回去重读。
Q1. 为什么"下好权重"和"起服务"是两件无关的事?
下载产物是标准 HF 目录结构(config.json + safetensors + tokenizer),任何框架都能读。ModelScope 只负责传输,不参与推理。所以 CLI 下完就能卸掉它。
Q2. --local_dir ./dir 和 --local_dir dir 有什么区别?
没有区别,都相对当前目录。 有区别的是 \`./home/xxx\` 这种写法 ------它会把整串路径拼到 cwd 后面。绝对路径必须 \`/\` 开头。
Q3. 8B 模型 bf16 权重占多大显存?为什么"8B 模型 16GB 就能跑"是错的?
16GB 只是权重。还要加 KV cache + 激活。8192 上下文单条请求就要 1.12GB KV,32 并发就是 36GB,加上权重 16GB 共 52GB+。所以 24G 卡上 8B 模型 8192 上下文只能撑 2 条左右并发(vLLM 日志会告诉你准确值)。
Q4. max-model-len 为什么会影响并发数?
vLLM 按这个上限预分配 KV 池。设 8192 就把显存切成 8192-token 的块;设 32768 同样的显存只能切出 1/4 的块,可并发的请求数直接降到 1/4。
Q5. 为什么 --max-model-len 不直接给模型的 32K 原生上限?
能跑但浪费。绝大多数业务 P99 长度远小于 32K,按原生上限预分配等于把 3/4 显存空占着,并发能力白白损失。参数应服从业务分布,不服从模型规格。
Q6. 连续批处理(continuous batching)解决了什么?
解决"短请求等长请求"的 GPU 空转。每生成一个 token 重新调度,完成的请求立即腾位,新请求立即插入。对比静态批处理,吞吐能提升数倍。
Q7. PagedAttention 为什么能提升显存利用率?
KV cache 按固定 block 分页存放,不再要求连续,也不按上限整块预留。消除内部碎片,且相同前缀的 block 可共享(prefix caching 的基础)。
Q8. 为什么算力云不能装 Docker?装了会怎样?
DinD 需要 --privileged,会导致容器逃逸威胁同机租户,平台不授权。就算装上 dockerd 也起不来,卡在 \`Cannot connect to the Docker daemon\`。正确做法是 pip 直装。
Q9. 为什么 pip install vllm 会撑爆系统盘,而"单独装 torch 再装 vllm"有时能缓解?
撑爆的原因是 pip 替换已装包时新旧两份并存(峰值翻倍)+ wheel 缓存 + TMPDIR 在系统盘。如果镜像里 torch 版本刚好匹配,vllm 就不再重装 torch,省掉最大的一次峰值。
Q10. TP=2 能提升并发吗?
能,但是间接的:每卡只装一半权重和一半 KV,剩余显存可放更多 KV block,所以总并发约翻倍。但通信开销会抬高单请求延迟。TP 解决的是"装不下"和"吞吐",不降低延迟。
Q11. 怎么用一条命令证明容器看见 GPU 了?
\`docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi\`。能看到显卡表格才算通,这步不通后面全白搭。
Q12. --model 该写哪个路径?
容器内路径(-v 后面的冒号右边),不是宿主机路径。写错报 \`does not have a file named config.json\`。
Q13. 排队很长,是不是说明 GPU 不够、该加卡了?
不一定。先看 \`/metrics\` 三个数:\`gpu_cache_usage_perc\` 满了才是真显存瓶颈;\`num_requests_running\` 贴着 \`max_num_seqs\` 是闸门被自己压低;两项都没满而 GPU 还闲着,那是 prefill 没喂饱或客户端在串行发请求------后两种加卡一点用都没有。
Q14. 有两张卡,想解决排队,用 TP=2 还是起两个副本?
起两个副本(每卡一个进程 + 负载均衡)。副本互不通信,吞吐近似线性翻倍、单请求延迟不变;TP 每步都要跨卡 all-reduce,算力只涨约 1.6 倍却让每条请求变慢。TP 的正确用途是"单卡装不下模型",不是"消排队"。
Q15. 为什么单卡起两个副本通常是亏的?
每个进程都要把权重完整加载进自己的显存。8B bf16 权重 16GB,两个副本就要 32GB,24G 卡直接放不下;就算卡够大,多出来的那份权重是纯浪费,KV 池反而比单进程更小。只有权重足够小(量化后 ≤ 4GB)时单卡多副本才有意义。
Q16. 为什么"越堵,加一张卡的收益越大"?
排队长度对负载率是非线性的(L = ρ/(1−ρ)),分母是剩余空闲能力。ρ=0.5 时加卡只快 2 倍,ρ=0.95 时能快 15 倍------因为空闲能力是最稀缺的资源,翻倍它的收益远超翻倍算力。另外两个副本能互相接盘,不会"一台在算、八个人干等"。
Q17. 请求一直积压,vLLM 会自己拒绝吗?什么时候必须上限流?
不会。vLLM 的 waiting queue 是无界的,只堆不拒。所以只要 ρ > 1(waiting 持续增长不回落),扩多少副本都只是推迟崩溃,必须同时做客户端限流(Semaphore)、超时快速失败,以及长任务异步化。
10. 一页速查表
bash
# ---------- 看环境 ----------
cat /etc/os-release | head -3 # 发行版
uname -m # x86_64 / aarch64
nvidia-smi # 卡型、显存、驱动支持的最高 CUDA
python -c "import torch;print(torch.__version__, torch.version.cuda)"
df -h # 磁盘,装之前必看
ls /.dockerenv; cat /proc/1/cgroup # 是否在容器里
# ---------- 下权重 ----------
pip install modelscope
modelscope download --model Qwen/Qwen3-8B --local_dir /data/models/Qwen3-8B
ls -lh /data/models/Qwen3-8B # 必须有 config.json + *.safetensors
# ---------- 装 Docker(仅自有服务器)----------
curl -fsSL https://get.docker.com | sudo sh -s -- --mirror Aliyun
sudo systemctl enable --now docker
sudo usermod -aG docker $USER && newgrp docker
sudo apt install -y nvidia-container-toolkit
sudo nvidia-ctk runtime configure --runtime=docker && sudo systemctl restart docker
docker run --rm --gpus all nvidia/cuda:12.4.0-base-ubuntu22.04 nvidia-smi
# ---------- 起服务 ----------
docker run -d --name qwen3 --gpus all --ipc=host --shm-size=16g \
--restart unless-stopped \
-v /data/models/Qwen3-8B:/models/Qwen3-8B:ro -p 8000:8000 \
vllm/vllm-openai:latest \
--model /models/Qwen3-8B --served-model-name Qwen3-8B \
--dtype bfloat16 --max-model-len 8192 \
--gpu-memory-utilization 0.9 --api-key sk-xxx
docker logs -f qwen3 # 等 "Application startup complete"
# ---------- 验证 ----------
curl http://localhost:8000/v1/models -H "Authorization: Bearer sk-xxx"
curl http://localhost:8000/v1/chat/completions \
-H "Authorization: Bearer sk-xxx" -H "Content-Type: application/json" \
-d '{"model":"Qwen3-8B","messages":[{"role":"user","content":"你好"}],"max_tokens":64}'
# ---------- 排队治理(第 11 章)----------
curl -s localhost:8000/metrics | grep -E "^vllm:(num_requests_waiting|num_requests_running|gpu_cache_usage_perc)"
grep "Maximum concurrency" <启动日志> # 并发槽位数,算 ρ 的分母
# ---------- 多卡 ----------
# 多副本:参数与单卡完全一致,只加 CUDA_VISIBLE_DEVICES 和 port
CUDA_VISIBLE_DEVICES=0 vllm serve /models/Qwen3-8B --port 8000 --max-model-len 4096 &
CUDA_VISIBLE_DEVICES=1 vllm serve /models/Qwen3-8B --port 8001 --max-model-len 4096 &
# TP:加 --tensor-parallel-size,CUDA_VISIBLE_DEVICES 给集合或不设
CUDA_VISIBLE_DEVICES=0,1 vllm serve /models/Qwen3-8B --port 8000 --tensor-parallel-size 2
export NCCL_DEBUG=WARN NCCL_P2P_DISABLE=1 # 4090/5090 无 NVLink 时的兜底
docker run -d --gpus '"device=0,1"' --ipc=host --shm-size=16g ...
# ---------- 排错 ----------
docker logs --tail 100 qwen3
nvidia-smi # OOM 时看真实占用
curl localhost:8000/metrics # Prometheus 指标
df -h # 又开始报错先看这个
六个数字必须会算/会看:
| 数字 | 怎么来 |
|---|---|
| 权重显存 | 参数量 × 2(bf16) |
| KV/token | 2 × 层数 × KV头数 × head_dim × 2 字节 |
| 可用显存 | 卡容量 × 0.9 - 系统占用 |
| 实际并发上限 | 日志里 Maximum concurrency for 8192 tokens per request: Nx |
| 吞吐 | 压测的 request throughput + output token throughput |
| 负载率 ρ | (每秒到达请求数 × 平均单请求耗时)÷ 并发槽位数 |
11. 第 6 层认知 · 高并发与排队治理
前 10 章解决"怎么跑起来",这一章解决"人一多为什么不崩"。
核心命题一句话:排队 = 到达率 > 服务率。所有手段无非两条:提服务率,或压到达率。
11.1 排队时间 ≠ 生成时间
用户感知的延迟拆三段,各段解法完全不同:
总延迟 = 排队等待 + prefill (TTFT) + decode (TPOT × 输出长度)
↑ 本章管这个 ↑ 参数/算力 ↑ 模型与输出长度
最常见的误判:把"慢"一股脑归因于"GPU 不够"。实际上如果卡在闲着而请求却在排队,加卡一点用都没有------先测出是哪一段。
11.2 三步定位瓶颈(看 /metrics,别猜)
bash
curl -s localhost:8000/metrics | grep -E "^vllm:(num_requests_waiting|num_requests_running|gpu_cache_usage_perc)"
请求排队积压(waiting 持续 > 0)
│
┌─────────────────────┼─────────────────────┐
↓ ↓ ↓
gpu_cache_usage running == 两项都没打满
≈ 100% max_num_seqs GPU 利用率却偏低
│ │ │
↓ ↓ ↓
真·显存瓶颈 人为上限太低 调度或客户端瓶颈
加卡/降长度/量化 调大 max-num-seqs 调 max-num-batched-tokens
客户端改异步并发
| 分支 | 读数含义 | 为什么 | 手段 |
|---|---|---|---|
| ① KV cache 打满 | gpu_cache_usage_perc 长期 ≥ 95% |
没有多余 block 分给新请求 | 降 max-model-len / 量化 / 加卡 |
| ② 并发撞上限 | num_requests_running 贴着 max_num_seqs |
显存还有富余,闸门被自己压低 | 调大 --max-num-seqs |
| ③ 两项都没满 | GPU 利用率却偏低 | 请求根本没喂进去 | 调 --max-num-batched-tokens、客户端异步化 |
第三类最容易被漏掉,也最冤:卡在闲着,人却在排队。这时候加卡纯属浪费钱。
11.3 多卡的两条路线(几乎所有人都会选错)
横向扩展 · 多副本(推荐) 纵向切分 · TP=2
┌────────────────────────┐ ┌──────────────────────┐
│ 负载均衡 least_conn │ │ 一个模型实例 │
└───────┬────────┬───────┘ └──────┬────────┬──────┘
↓ ↓ ↓ ↓
┌─────────┐ ┌─────────┐ ┌────────┐ ┌────────┐
│ 副本 A │ │ 副本 B │ │ GPU 0 │ │ GPU 1 │
│ GPU 0 │ │ GPU 1 │ │权重 KV │ │权重 KV │
│ 全量权重│ │ 全量权重│ │ 各一半 │ │ 各一半 │
└─────────┘ └─────────┘ └────────┘ └────────┘
吞吐近似线性翻倍 · 延迟不变 解决装不下 · 延迟略升
← 排队问题的首选 每步都要跨卡通信
| 多副本 | TP 张量并行 | |
|---|---|---|
| 多张卡在干什么 | 各算各的,互不通信 | 协同算同一个请求 |
| 目的 | 提高吞吐、消排队 | 让大模型装得下 |
| 总吞吐 | ≈ 线性 ×N | < 线性(通信损耗,2 卡实测约 1.6~1.9 倍) |
| 单请求延迟 | 不变 | 略升 |
| 并发槽位 | ×N | 涨得多(见 5.5),但每条都变慢 |
| 依赖 | 无(不需要 NCCL) | 需要 NCCL / 共享内存 |
| 什么时候用 | 排队、并发不够 | 单卡装不下模型 |
为什么单卡搞不了多副本------每个进程都要把权重完整读进自己的显存:
单卡 24G · 1 个副本 [ 权重 16G ][ KV 5.6G ][开销]
可行,但 KV 只剩 5.6G
单卡 24G · 2 个副本 [ 副本A 权重 16G ][ 副本B 权重 16G ]
↑ 24G 卡容量(超出 8G)
放不下 ------ 权重要占两份
双卡 · 各 1 个副本 GPU0 [ 权重 16G ][ KV 5.6G ][开销]
GPU1 [ 权重 16G ][ KV 5.6G ][开销]
各算各的,吞吐翻倍
所以多副本 ≈ 多进程 ≈ 通常需要多张 GPU。单卡多副本只在模型权重足够小时成立(量化后 ≤ 4GB,如 Qwen3-1.7B 或 7B-AWQ);8B bf16 就别想了------多出来的一份权重是纯浪费,KV 池反而更小。
11.4 多卡部署参数怎么改
改的东西很少,但两种模式改的地方相反 ,最容易翻车的是 CUDA_VISIBLE_DEVICES。
多副本:业务参数一个都不用改。每个进程的参数与单卡部署逐字相同,只额外加设备号和端口:
bash
CUDA_VISIBLE_DEVICES=0 vllm serve /models/Qwen3-8B \
--served-model-name Qwen3-8B --port 8000 \
--max-model-len 4096 --gpu-memory-utilization 0.9 --max-num-seqs 128
CUDA_VISIBLE_DEVICES=1 vllm serve /models/Qwen3-8B \
--served-model-name Qwen3-8B --port 8001 \
--max-model-len 4096 --gpu-memory-utilization 0.9 --max-num-seqs 128
TP:只加一个参数:
bash
vllm serve /models/Qwen3-8B \
--served-model-name Qwen3-8B --port 8000 \
--tensor-parallel-size 2 \
--max-model-len 4096 --gpu-memory-utilization 0.9 \
--max-num-seqs 256 \
--distributed-executor-backend mp
| 参数 | 多副本 | TP |
|---|---|---|
--model |
不变 | 不变 |
--port |
每个副本不同 | 不变(只有一个入口) |
CUDA_VISIBLE_DEVICES |
必须逐个指定 | 不能逐个指定 ,给集合(0,1)或干脆不设 |
--tensor-parallel-size |
不设(默认 1) | = 卡数 |
--gpu-memory-utilization |
不变 | 不变(含义是「每卡」的比例) |
--max-model-len |
不变 | 不变(逻辑长度上限,不随卡数分片) |
--max-num-seqs |
不变 | 可适度调大(总 KV 池变大了) |
--distributed-executor-backend |
不涉及 | 单机 mp,跨机才 ray |
Docker --ipc=host |
建议 | 必须有(多进程共享内存) |
三个必踩的坑:
- TP 下只写
CUDA_VISIBLE_DEVICES=0会直接报"卡数不够" ------vLLM 需要自己看到所有卡。想限定用哪两张就写CUDA_VISIBLE_DEVICES=0,1+--tensor-parallel-size 2。 - TP size 必须整除 KV head 数。 Qwen3-8B 是 8 个 KV head(
config.json里的num_key_value_heads),所以只能取 2 / 4 / 8,写 3 会报错。换模型前先查这个数。 - 消费级卡(4090/5090)没有 NVLink,多卡通信走 PCIe,P2P 还可能直接不可用:
bash
export NCCL_DEBUG=WARN
export NCCL_P2P_DISABLE=1 # 报 NCCL P2P 相关错误时加,能跑但会慢
Docker 下多卡还要显式给设备和共享内存(漏 --ipc=host 的症状是启动静默卡住,见 8.5,多卡时更常见):
bash
docker run -d --gpus '"device=0,1"' --ipc=host --shm-size=16g ...
多副本的入口(nginx,两个易漏点):
nginx
upstream vllm_backend {
least_conn; # 别用轮询:请求耗时差异大,轮询会堆到同一副本
server 127.0.0.1:8000 max_fails=3 fail_timeout=10s;
server 127.0.0.1:8001 max_fails=3 fail_timeout=10s;
}
server {
listen 8080; # 别和后端 8000/8001 撞端口
location / {
proxy_pass http://vllm_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
proxy_read_timeout 3600s; # 默认 60s 会截断长生成
}
}
11.5 排队论:为什么加一张卡收益超线性
排队长度对负载率是非线性的 ------L = ρ/(1−ρ),分母的 1−ρ 是"剩余空闲能力"。ρ 从 0.9 涨到 0.95,空闲能力从 10% 掉到 5%,直接腰斩,队列就翻倍。
加第二张卡做了两件事:空闲能力变多,而且两台能互相接盘(不会出现"一台在算、八个人干等")。所以效果是乘出来的,不是加出来的。
| 单卡负载率 ρ | 1 张卡 | 2 张卡(多副本) | 排队改善 |
|---|---|---|---|
| 50% | 1.0 | 0.5 | ~2 倍 |
| 70% | 2.3 | 0.8 | ~3 倍 |
| 80% | 4.0 | 1.0 | ~4 倍 |
| 90% | 9.0 | 1.1 | ~8 倍 |
| 95% | 19.0 | 1.2 | ~15 倍 |
(表内为系统内平均请求数,M/M/1 与 M/M/2 模型估算。结论:越堵,加一张卡越划算。)
怎么估你现在的 ρ:
ρ =(每秒到达请求数 × 平均单请求耗时)÷ 并发槽位数
并发槽位数取启动日志里 Maximum concurrency for N tokens per request: Nx 的 N。
| ρ | 现象 | 该做什么 |
|---|---|---|
| < 0.8 | waiting 偶尔 > 0 但能自己消化 |
加卡收益极大,直接加 |
| ≈ 1 | waiting 缓慢增长、时高时低 |
加卡能救,但要同时上限流 |
| > 1.5 | waiting 单调涨到几百 |
加卡只是推迟崩溃,先限流 + 异步化 |
两个诚实的保留 :
① 上表是 M/M/c 模型,假设服务率恒定。实际 vLLM 有连续批处理,请求越多单 token 效率越高,真实曲线比这里平缓 ;但一旦 KV cache 打满、槽位用尽就退化成纯排队,那时候反而比这里更陡 。数字看趋势,别当精确预测。
② 前提是"两个独立服务台",即多副本。TP 不符合这条曲线(见 5.5)。
11.6 只有一张卡怎么办(不加卡的四招)
不靠加卡,靠把权重变小、把显存让给 KV:
| 招 | 效果 | 成本 |
|---|---|---|
| 量化 AWQ / FP8 | 权重 16G→8G/4G,KV 池直接翻倍多 | 重下权重,质量略降 |
降 max-model-len |
8192→4096,KV 池能切的块数翻倍 | 免费 |
开 prefix caching |
共享前缀只算一次,省的是 prefill 算力 | 免费 |
限 max_tokens |
缩短每条请求占用槽位的时间 | 免费 |
量化是唯一能"等效加显存"的手段------权重省下的每一 GB 都变成 KV 池。8B 模型做 AWQ 4bit 后权重约 4GB,24G 卡能腾出 17GB 给 KV,并发能力比 bf16 版本高 6 倍以上。这个幅度往往比加一张卡还大(加卡也才翻倍)。
bash
vllm serve /models/Qwen3-8B-AWQ \
--served-model-name Qwen3-8B --port 8000 \
--quantization awq \
--max-model-len 4096 \
--enable-prefix-caching \
--max-num-seqs 256 \
--gpu-memory-utilization 0.9
11.7 客户端与业务侧削峰
vLLM 的 waiting queue 是无界的------它不会拒绝,只会一直堆。所以到达率持续大于服务率时,扩多少副本都只是推迟崩溃。
python
import asyncio, httpx
from openai import AsyncOpenAI
http_client = httpx.AsyncClient(
limits=httpx.Limits(max_connections=200, max_keepalive_connections=50)) # 默认连接池太小
client = AsyncOpenAI(base_url="http://127.0.0.1:8080/v1", api_key="sk-xxx",
http_client=http_client)
sem = asyncio.Semaphore(64) # 自己限流,别把服务端灌爆
async def ask(p):
async with sem:
return await client.chat.completions.create(
model="Qwen3-8B",
messages=[{"role": "user", "content": p}],
max_tokens=512, # 必须设上限,否则一条失控请求能占住槽位几分钟
timeout=120)
await asyncio.gather(*[ask(p) for p in prompts])
| 手段 | 做法 | 解决什么 |
|---|---|---|
| 客户端并发闸门 | Semaphore(N),N 略大于服务端 max_num_seqs 总和 |
防止灌爆 |
| 快速失败 | 队列超阈值直接返回"稍后重试" | 别让用户干等 3 分钟 |
| 异步化 | 长任务提交后返回任务 ID,结果回调推送 | 2~3 分钟级任务的唯一正解 |
| 结果缓存 | 相同入参直接命中(语义缓存更好) | 削掉重复请求 |
| 优先级调度 | 启动加 --scheduling-policy priority,请求带 priority |
重要请求插队 |
| 批量合并 | 多条短请求合成一次调用 | 减少调度开销 |
11.8 治理优先级清单
按顺序做,做完一条测一次,别一口气全上:
| 序 | 手段 | 成本 | 何时停手 |
|---|---|---|---|
| 1 | 看 /metrics 定位分支(11.2) |
0 | 必须先做,否则后面全是瞎调 |
| 2 | 改参数:max-model-len + prefix caching + max-num-seqs |
0 | 5 分钟见效 |
| 3 | 客户端限流 + 超时 + 异步化 + 缓存 | 低 | waiting 不再单调增长 |
| 4 | 量化(AWQ / FP8) | 中(重下权重) | 单卡并发翻几倍,往往就够了 |
| 5 | 横向多副本 + nginx least_conn |
高(加卡) | 真要扩容时的正解 |
| 6 | TP | 高 | 只在单卡装不下时用 |
一句话收尾:排队是"到达率 > 服务率"的数学问题。扩容是提高服务率,限流是降低到达率,两头都要做;只扩不限,队列只会从一个长队变成两个长队。
附:学习路线建议
第 1 周 第 1~4 章(链路、框架原理、GPU 栈、权重管理)
└ 动手:在一台服务器上完整跑通一次部署,不看教程
第 2 周 第 5~6 章(显存账、参数)
└ 动手:同一个服务,用 3 组不同 max-model-len 起 3 次,记录日志里的并发上限
第 3 周 第 7~8 章(命令拆解、故障库)
└ 动手:故意制造 5 个故障(漏 --ipc、写错路径、调高 gpu-memory-utilization 等),自己修
第 4 周 压测 + 多副本 + 负载均衡
└ 动手:benchmark_serving 出一组 TTFT/TPOT/吞吐数据,写一页调优结论
第 5 周 第 11 章(排队治理)
└ 动手:用 /metrics 给自己的服务定位瓶颈分支;同一个服务分别用 bf16 和 AWQ 起,对比日志里的并发上限
岗位面试最可能问的四题(本文档都有答案):
- 8B 模型部署到 24G 卡上,最大并发是多少?怎么算的?(考显存账)
- vLLM 为什么快?PagedAttention 和连续批处理分别解决什么问题?(考原理)
- 服务启动报 OOM / 卡住 / 连不上,你怎么定位?(考排错分层)
- 多人使用、请求排队很长,你怎么排查和优化?(考排队治理:先 /metrics 定位分支 → 改参数 → 量化 → 多副本横向扩 → 客户端限流削峰)