大模型推理部署 · 学习文档

适用对象:准备大模型应用开发岗、需要把「下载权重 → 起服务 → 调参 → 排错」这条链路真正弄懂的人。

配套手册:同目录 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          │
└──────────────────────────────────────────────────────────────┘

三句话记住这张图:

  1. 模型平台管下载,Docker 管隔离,vLLM 管推理------三方互不替代。
  2. 换框架只影响环节三:环境、权重、客户端代码全都不用动。
  3. 换部署方式只影响环节二: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 靠两样东西:

  1. nvidia-container-toolkit (宿主机安装):注册一个叫 nvidia 的容器 runtime
  2. --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

四条永久性习惯:

  1. pip install 一律加 --no-cache-dir,或先 pip config set global.no-cache-dir true
  2. 大数据/大模型进数据盘,系统盘只留系统
  3. conda create -p <数据盘路径> 而非 -n(-n 默认建在系统盘)
  4. 每次大批量操作前先 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

这张表解释了两件事:

  1. 为什么 --max-model-len 从 8192 调到 32768 会"莫名 OOM"------KV 池按上限预分配,直接涨 4 倍
  2. 为什么长上下文服务的并发能力必然低------单条请求就把显存吃光了

注意 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 变差 延迟好、吞吐差

三个必须内化的因果链:

  1. max-model-len ↑ → KV 池预分配 ↑ → 可用并发 ↓ → 吞吐 ↓
  2. max-num-seqs ↑ → 同时处理的请求 ↑ → 共享显存 ↓ → 单条长度受限 / OOM 风险 ↑
  3. 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 建议 必须有(多进程共享内存)

三个必踩的坑:

  1. TP 下只写 CUDA_VISIBLE_DEVICES=0 会直接报"卡数不够" ------vLLM 需要自己看到所有卡。想限定用哪两张就写 CUDA_VISIBLE_DEVICES=0,1 + --tensor-parallel-size 2。
  2. TP size 必须整除 KV head 数。 Qwen3-8B 是 8 个 KV head(config.json 里的 num_key_value_heads),所以只能取 2 / 4 / 8,写 3 会报错。换模型前先查这个数。
  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 起,对比日志里的并发上限

岗位面试最可能问的四题(本文档都有答案):

  1. 8B 模型部署到 24G 卡上,最大并发是多少?怎么算的?(考显存账)
  2. vLLM 为什么快?PagedAttention 和连续批处理分别解决什么问题?(考原理)
  3. 服务启动报 OOM / 卡住 / 连不上,你怎么定位?(考排错分层)
  4. 多人使用、请求排队很长,你怎么排查和优化?(考排队治理:先 /metrics 定位分支 → 改参数 → 量化 → 多副本横向扩 → 客户端限流削峰)
相关推荐
小李不想当小白2 小时前
BKP备份寄存器与RTC实时时钟(STM32标准库学习笔记)
笔记·stm32·单片机·嵌入式硬件·学习·嵌入式
陈飞EXCEL3 小时前
北京杰飞 IT 旗下妙码生花少儿Python编程上线两大学习系统
开发语言·python·学习
SLAM必须dunk3 小时前
Agent入门学习一
学习
承渊政道5 小时前
Linux网络学习【从NAT与代理服务理解网络转发、探索内网穿透原理、再到认识DNS与ICMP、最后动手部署并测试内网穿透】
linux·网络·学习·内网穿透·dns·nat
栉甜6 小时前
Vue2学习内容(上)
前端·javascript·vue.js·学习
shangjian0076 小时前
K8S学习-基础组件
学习·容器·kubernetes
lifallen6 小时前
ub-mode skill:软件工程原则
人工智能·学习·ai·软件工程·ai编程
凉、介8 小时前
深入理解 ARMv7-A | Generic Timer
笔记·学习·嵌入式·arm
椒颜皮皮虾྅8 小时前
让文档不止于文字:DeploySharp + PP-Structure,打通版面分析与表格重建
开发语言·学习·c#·.net·cuda