① 这篇讲什么——怎么量化地衡量和优化推理服务:吞吐、延迟、显存三者怎么权衡,看哪些指标、调哪些参数。
② 它的核心观点对不对——**"不可能三角"的思路是对的,而且正好解释了你之前那个疑问**:为什么 P 节点 token 预算大、并发小,D 节点预算小、并发大——那就是这套权衡在 PD 分离里的具体落地。
③ ⚠️ 但它的 API 和指标名基本是编的——python -m vllm.entrypoints.benchmark、VLLM_PROFILING_ENABLED=1、还有那些 vllm_xxx 指标名(少了 vllm: 前缀、好几个根本不存在)都别当真。而且它是 NVIDIA 视角,你是昇腾 NPU,工具名要换。
下面从零讲。
一、先建立三个指标的直觉(餐厅版)
| 指标 | 是什么 | 餐厅比喻 | 谁最关心 |
|---|---|---|---|
| 吞吐量 Throughput | 单位时间总共吐多少 token(Tokens/s) | 一小时总共出多少道菜 | 成本/离线批处理 |
| 延迟 Latency | 单个请求多久完成 | 一桌客人等多久上菜 | 交互/聊天体验 |
| 显存利用率 | 显存用了多少(决定能装多少 KV) | 仓库堆得多满 | 能不能同时接更多客 |
延迟拆成三截(这个必须分清)
端到端延迟 = TTFT + 生成token数 × TPOT
TTFT(Time To First Token) = 首字延迟:请求进来到吐第一个字
← 主要由 prefill 决定(读题要多久)
TPOT(Time Per Output Token)= 字间延迟:两个字之间的间隔
← 主要由 decode 决定(每吐一个字要多久)
E2E(端到端) = 整个请求从发到收完
这直接接上你刚学的:prefill 慢 → TTFT 高;decode 慢(搬运权重)→ TPOT 高。
二、⭐ 为什么三者冲突("不可能三角")
这是本章的骨架,我用最直白的方式讲。
冲突 1:吞吐 vs 延迟 —— 核心是"一次算多少"
大 batch(一次算很多):
✅ 算力吃饱 → 吞吐高
❌ 但每轮算得多 → 单步变慢;请求还可能要等凑批 → 延迟高
小 batch(一次算一点):
✅ 每步快、不用等 → 延迟低
❌ 但算力闲置 → 吞吐低、单 token 成本高
打比方:
大巴车(大 batch):一趟拉 50 人,人均成本低(吞吐高)
但你要等凑满 50 人才发车(延迟高)
出租车(小 batch):来一个走一个,快(延迟低)
但一趟只拉 1 人,人均成本高(吞吐低)
vLLM 的连续批缓和了这对矛盾——因为它允许"有人下车、有人上车",新请求不用等整批跑完。但权衡的本质没变。
冲突 2:显存利用率 vs 其他两个
显存用得越满 → KV Cache 越大 → 能装更多并发 → 吞吐↑、排队↓
但太满 → 没有缓冲 → 突发长请求一来就 OOM → 服务直接崩(吞吐归零)
**所以显存不是"越高越好",而是"在留够安全余量的前提下尽量高"**——你之前学到的 gpu_memory_utilization 就是这条水位线。
这张表是全文最实用的(业务对号入座)
| 业务类型 | 首要目标 | 调优方向 | 牺牲什么 |
|---|---|---|---|
| 成本敏感(离线批处理) | 单 token 成本最低 | 拉满吞吐(大 batch) | 延迟(尤其 TTFT) |
| 交互聊天 | 用户感觉快 | 压 TTFT 和 TPOT(小 batch) | 吞吐、硬件利用率 |
| 长文本/RAG | 撑住长上下文、高并发 | 优化显存 + 开前缀缓存 | 极致的吞吐或延迟 |
三、怎么测量(工具要换成昇腾的)
文档说的(NVIDIA 视角,你要替换)
| 文档写的 | 昇腾上你该用 |
|---|---|
nvidia-smi | **npu-smi** |
dcgm(看带宽) | msprof / Ascend PyTorch Profiler |
| Nsight(看时间线) | MindStudio Insight |
| "GPU 算力利用率" | AI Core 利用率 |
| "GPU 显存" | NPU 显存(HBM) |
/metrics 端点:真实存在,但指标名文档写错了
curl http://localhost:8000/metrics 这个是真的(vLLM 暴露 Prometheus 指标)。但文档给的指标名几乎全错——真实的是带 vllm: 前缀的:
| 文档写的(❌) | 真实的(✅) |
|---|---|
vllm_num_requests_waiting | **vllm:num_requests_waiting** |
vllm_num_requests_running | **vllm:num_requests_running** |
vllm_request_latency_seconds | **vllm:e2e_request_latency_seconds** |
vllm_ttft_seconds | **vllm:time_to_first_token_seconds** |
vllm_gpu_cache_usage_ratio | **vllm:gpu_cache_usage_perc** |
vllm_num_generated_tokens_total | **vllm:num_generation_tokens_total** |
vllm_batch_size | ❌ 不存在 |
vllm_num_blocks_in_gpu | ❌ 不存在 |
另外几个你以后会用到的真实指标:
前缀缓存命中率:
vllm:prefix_cache_queries_total
vllm:prefix_cache_hits_total ← 命中数 / 查询数 = 命中率
推测解码接受率:
vllm:spec_decode_num_draft_tokens_total
vllm:spec_decode_num_accepted_tokens_total ← 接受/草稿 = 接受率
这两个正好能回答你之前悬着的两个问题:前缀缓存到底命中了多少、MTP 猜的到底接受了多少。
压测命令:文档那个是编的
❌ python -m vllm.entrypoints.benchmark ...(不存在这个模块)
✅ vllm bench serve ... (在线服务压测,测 TTFT/TPOT/吞吐)
✅ vllm bench throughput ... (离线吞吐)
昇腾上一般配合 AISBench 做压测。
四、调优旋钮(这些全学过,现在串成体系)
文档第 3 步列的这些参数方向是对的,而且每个你都已经认识:
| 参数 | 往哪调 | 影响 |
|---|---|---|
**max_num_batched_tokens** | ⭐最重要的一个 | 每轮 token 预算。调大→吞吐↑但单步变慢(延迟↑);调小→反之 |
max_num_seqs | 并发天花板 | 调大→并发↑(吞吐↑),但调度开销和延迟↑ |
gpu_memory_utilization | 显存水位 | 决定给 KV Cache 留多少→决定并发上限 |
block_size | 货架大小 | 小块→对齐容易、前缀命中率高;大块→管理开销小 |
| 分块预填充 | 治长 prompt | 主要降 TTFT |
| 前缀缓存 | 相同前缀 | 降 TTFT、省显存,对 TPOT 无帮助 |
| 推测解码(MTP) | 一次吐多字 | 降 TPOT、提吞吐,但算力打满时收益下降 |
⭐ 这正好解释了我的 PD 分离配置
回看那套架构(抽象讲,不引具体值):
Prefill 节点:
token 预算【大】+ 并发【小】
→ 因为 prefill 是重活,一次算一大块才能把算力吃满
→ 追求:单请求的 prefill 效率(吞吐)
Decode 节点:
token 预算【小】+ 并发【大】
→ 因为 decode 每步只算 1~2 个 token(很轻),必须堆大并发喂饱算力
→ 预算小是为了让每步都很快(TPOT 低)
→ 追求:低延迟 + 高并发吞吐
这就是"吞吐 vs 延迟"权衡在两个阶段上的不同落点——两阶段脾气不同,所以最优解也不同。这正是 PD 分离能拿到收益的根本原因。
五、"先找瓶颈,再调参"(文档步骤一,这条最实用)
文档说调优第一步是判断瓶颈在哪——这跟你之前学的 profiling 完全对上。三类瓶颈:
| 瓶颈 | 怎么看出来 | 该往哪调 |
|---|---|---|
| 算力瓶颈(Compute-bound) | AI Core 利用率接近 100%,带宽不高 | 增大 batch、上量化/稀疏减少计算量 |
| 带宽瓶颈(Memory-bound) | 带宽打满、算力空闲 | 量化(少搬权重)、算子融合、推测解码 |
| 显存容量瓶颈 | 显存接近满、请求排队多 | 调gpu_memory_utilization、缩短max_model_len、开前缀缓存 |
之前学的"搬运 vs 计算"那个判断,就是这里的第一步——先定位是哪种瓶颈,再决定调什么。乱调参数(比如明明是带宽瓶颈却去加 batch)往往没用。
⚠️ :下发瓶颈(Host-bound)——NPU 大片空等在等 CPU 下命令。判据是时间线上 NPU 有空白。解法是开图模式( D 节点用 FULL_DECODE_ONLY 就是在治它)。
六、挂知识树
vLLM 知识体系
├─ 【前传】模型怎么装进来
├─ 【主线】怎么跑得快
│ ├─ FlashAttention / PagedAttention
│ ├─ Scheduler / 连续批
│ ├─ 分块预填充 / 前缀缓存
│ ├─ 图模式 / 推测解码
│ └─ 【本篇】性能剖析方法论 ← 横向统领上面所有技术
│ ├─ 三个指标:吞吐 / 延迟(TTFT·TPOT) / 显存
│ ├─ 不可能三角:大batch↔低延迟、显存↔OOM风险
│ └─ 方法:先定位瓶颈(算力/带宽/容量/下发)→ 再调对应旋钮
├─ 【观测】profiling(已学:torch profiler / msprof / MindStudio Insight)
│ └─ 本篇是"怎么用这些工具做决策"的方法论
├─ 【外传一】改结构(MoE / 多模态)
└─ 【外传二】改精度(W8A8 / FP8)
它是横向的一层——不新增技术,而是教在众多技术里怎么选、怎么调。前面学的每一个优化技术,都是这套方法论里的一个"旋钮"。
一句话总结
这篇讲的是**"怎么科学地调 vLLM":三个指标(吞吐 / 延迟 / 显存)构成不可能三角——大 batch 换吞吐但伤延迟,显存用得越满并发越高但 OOM 风险越大**。
方法就三步:① 先用 profiling +
/metrics定位瓶颈(算力 / 带宽 / 容量 / 下发);② 按业务目标(成本型 / 交互型 / 长文型)定方向;③ 调对应的旋钮——**max_num_batched_tokens是最核心的那个**,其余是max_num_seqs、显存水位、分块预填充、前缀缓存、推测解码。PD 分离配置就是这套权衡的落地:prefill 侧预算大并发小(榨吞吐),decode 侧预算小并发大(压延迟)。
立刻能做的实操建议:下次压测时,除了看总吞吐,专门抓三个数字——vllm:time_to_first_token_seconds(TTFT)、vllm:time_per_output_token_seconds(TPOT)、vllm:gpu_cache_usage_perc(KV 水位)。这三个数一出来,立刻判断当前卡在哪个阶段、该往哪个方向调,不用再凭感觉试参数。