banner
约 7,200 字
24 分钟

vLLM 高并发推理:从 KV Cache 瓶颈到性能监控调优

摘要

本文围绕高并发 AI 推理服务的三个层次展开:KV Cache 显存瓶颈与 PagedAttention 解决思路、Continuous Batching 调度机制、推测解码(Speculative Decoding)与 MEDUSA、EAGLE 加速方案,最后以 Qwen3-0.6B 在 RTX 4090D 上的部署实验验证 TTFT、TPOT、并发吞吐与 Prometheus 监控指标,并给出场景化调优建议。

vLLM 高并发推理:从 KV Cache 瓶颈到性能监控调优

写在前面

把大语言模型(Large Language Model,LLM)部署成线上服务时,真正决定服务能力的往往不是模型本身,而是推理引擎的显存管理、调度策略和解码方式。本文系统整理高并发 AI 服务的设计主线:先说明 KV Cache(Key-Value Cache,键值缓存)为什么是显存瓶颈,再解释 vLLM 如何用 PagedAttention 和 Continuous Batching 解决内存与调度问题,接着介绍推测解码(Speculative Decoding,投机采样)及其工程化变体 MEDUSA 与 EAGLE,最后在 RTX 4090D 上用 Qwen3-0.6B 完成部署、延迟测量、并发压测和 Prometheus(性能监控工具)指标采集,把原理落到可复现的工程流程。

模块

解决的核心问题

关键结论

KV Cache 与 PagedAttention

高并发下显存被 KV Cache 占满、碎片化

块化存储与按需分配可节省 90% 以上显存

Continuous Batching

静态批处理导致 GPU(Graphics Processing Unit,图形处理器)空闲

请求完成即补位,吞吐提升 2 倍以上

推测解码

小批量时 Decode 阶段算力浪费

草稿模型猜、目标模型验证,单请求延迟显著下降

性能监控与调优

瓶颈难以定位

用 TTFT、TPOT、KV Cache 利用率等指标做场景化调优

一、背景:高并发 AI 服务的内存瓶颈

1.1 KV Cache 是什么

Transformer 在自回归生成时,每一步都要让当前 token 与历史所有 token 做注意力计算。如果不做缓存,每生成一个新 token 都要重新计算前面所有位置的 Key(键)和 Value(值)张量,计算量随序列长度平方级增长。KV Cache 的思路是:把每一层已经算好的 K、V 张量保存下来,后续步骤只计算新位置的部分,避免重复计算。

以生成“我爱中国”为例,输入序列长度为 4:

步骤

输入

无 KV Cache

有 KV Cache

Step 1

[我]

计算 Q1、K1、V1,生成“爱”

计算 Q1、K1、V1,缓存 K1、V1

Step 2

[我, 爱]

重新计算 K1、V1,再算 K2、V2

复用缓存的 K1、V1,只算 K2、V2

Step 3

[我, 爱, 中]

重新计算 K1、K2、V1、V2

复用 K1..2、V1..2,只算 K3、V3

KV Cache 用显存换算力,但显存会随着上下文长度和并发请求数快速增长。

1.2 显存占用计算:KV Cache 才是显存杀手

以 LLaMA-2-7B 为例估算单请求的 KV Cache 占用:

每个 token 的 KV Cache = 2(K 和 V)x 32(层数)x 4096(隐藏维度)x 2(FP16,即 16 位浮点格式,每元素 2 字节)= 524,288 字节,约 0.5 MB。

上下文 4096 个 token 时,单个请求需要 4096 x 0.5 MB = 2 GB。并发 100 个请求就是 100 x 2 GB = 200 GB,远超单卡 A100(80 GB 显存)的容量。即使模型本身只占约 14 GB,KV Cache 在高并发场景下依然是真正的显存瓶颈。

1.3 KV Cache 的三大瓶颈

第一个瓶颈是内存碎片。内部碎片指为每个请求预分配最大长度(如 2048 个 token),实际只用 500,剩余空间被浪费;外部碎片指请求 A 占 0-500、请求 B 占 1000-1500,中间 500-999 虽然空闲,却因为不连续无法分配给需要连续内存的请求 C。

第二个瓶颈是动态长度导致的显存浪费。同一个系统里,有的请求只生成 100 个 token,有的要生成 2000 个 token,传统方案按最大长度预分配,平均利用率往往低于 30%。

第三个瓶颈是 Copy-on-Decoding 开销。使用 Beam Search(束搜索)时,需要把 KV Cache 完整复制到每个分支。每次复制量约为束宽 4 x 序列长度 1000 x 0.5 MB = 2 GB,而且每生成一个 token 都可能触发一次,严重拖慢生成速度。

二、PagedAttention:把显存当成虚拟内存管理

2.1 核心思想:块化存储与 Block Table

PagedAttention 借鉴操作系统虚拟内存的思想:虚拟内存把不连续的物理页映射给连续的虚拟地址,PagedAttention 则把不连续的显存块映射给逻辑上连续的序列。KV Cache 不再要求在物理显存中连续存放,而是分割成固定大小的 Block(块,通常 16 个 token 一块),通过 Block Table(块映射表)维护逻辑到物理的映射关系。

可以把这套机制类比成图书馆的目录卡片:一套丛书不必摆在连续的格子里,只要在目录上记录每一册的位置,读者依然能按顺序阅读。PagedAttention 就是 GPU 显存的目录卡片。

PagedAttention 存储映射示意
PagedAttention 存储映射示意

图 1 展示了 PagedAttention 的存储映射关系:上方是逻辑视图中的连续序列,中间是物理显存中的非连续存储,右侧是 Block Table。这张图对应一个明确结论:序列的逻辑连续性由映射表保证,物理上允许任意碎片化存放。

2.2 机制一:按需分配

传统方式在启动时按最大序列长度预分配显存,例如 2048 x 0.5 MB = 1024 MB,无论实际用多少都占满。PagedAttention 则生成 1 个 token 才分配 1 个 block,用满一个 block 才申请新 block。

生成 100 个 token 的序列时,需要 ceil(100 / 16) = 7 个 block,实际占用 7 x 16 x 0.5 MB = 56 MB;传统方式需要 1024 MB。按需分配节省约 94.5% 的显存。

2.3 机制二:写时复制

Beam Search 和 Parallel Sampling(并行采样)都面临同一个问题:多条路径或多个回复共享同一个 Prompt 前缀,KV Cache 完全相同,传统做法要完整复制多份。

PagedAttention 用写时复制(Copy-on-Write)解决:Prompt 阶段多个采样共享同一份 KV Cache,用引用计数记录共享数量;只有生成阶段真正产生分歧(输出不同 token)时才分配私有 Block。以 3 个并行采样为例,Prompt 阶段共享 5 个 block,生成阶段只新增 3 个私有 block,共 8 个 block;传统方式需要 3 x 6 = 18 个 block,节省约 55% 显存。

2.4 机制三:动态重排

当新请求到达时,PagedAttention 不需要寻找连续的大块显存,只需要收集分散的空闲 block 即可。这与停车场的逻辑一致:不需要一整排连续车位,有空位就能停。这一特性是 Continuous Batching 能够高效运转的前提。

三、vLLM 架构:Batch 与 Continuous Batching

3.1 Batch 的概念

Batch(批处理)指把多个用户请求打包在一起,送入 GPU 做一次前向传播。GPU 擅长并行计算,处理 1 个请求和同时处理 8 个请求的耗时差别不大,吞吐量却可以提升数倍,就像一辆公交车载 1 个人和载 30 个人跑一趟的油耗差不多。

假设同时有 3 个用户请求:A 需要生成 100 个 token,B 需要 200 个,C 需要 150 个。它们被放进同一个 batch,GPU 每一步同时为 3 个请求各生成 1 个 token。问题在于请求完成时间不同:短的先完成,长的还没结束,GPU 怎么处理空出来的位置?

3.2 静态 Batch 与 Continuous Batching

静态 Batch(同步等待)要求 batch 内所有请求都完成才释放资源,等待时间等于最长的那个请求,即 max(100, 200, 150) = 200 步。请求 A 完成后,对应位置要空闲 100 步,白白浪费算力。

Continuous Batching(连续批处理)在请求完成后立即从队列取新请求填入,GPU 始终满负荷运行,请求完成即刻腾出位置。vLLM 的调度粒度细到单次迭代,每生成一个 token 就检查一次,因此称为 Iteration-level Scheduling(迭代级调度)。

静态 Batch 与 Continuous Batching 对比
静态 Batch 与 Continuous Batching 对比

图 2 是两种调度的对比示意:静态 Batch 中先完成的请求仍然占位,直到整批结束;Continuous Batching 中请求完成立即释放,新请求马上补入。这一对比对应正文的核心结论,即动态补位消除了空闲等待。

以下数据来自对两种调度的性能对比:

指标

静态 Batch

Continuous Batching

提升

GPU 利用率

40%-60%

85%-95%

1.5-2 倍

吞吐量(tokens/s)

800

1800

2.25 倍

平均延迟(P50,第 50 百分位延迟)

1200 ms

800 ms

1.5 倍

3.3 Iteration-level Scheduling 伪代码

每个 iteration(生成一个 token)调度一次,核心流程如下:

Python
while True:
    # 1. 处理新到达的请求(Prefill 阶段)
    new_requests = get_new_requests()
    for req in new_requests:
        blocks_needed = ceil(len(req.prompt_tokens) / block_size)
        allocate_blocks(req, blocks_needed)
        batch.add(req)

    # 2. 执行一次前向传播(每个请求生成一个 token)
    outputs = model_forward(batch)

    # 3. 处理完成的请求
    for req in batch:
        if outputs[req].token == EOS:  # EOS:End of Sequence,序列结束符
            batch.remove(req)
            free_blocks(req)   # PagedAttention 立即释放 blocks
    # 关键:本 iteration 即可加入新请求

传统方案是 Request-level(请求级)调度,整个 batch 全部完成才处理下一批;vLLM 是 Iteration-level,每次前向传播后都检查并调度,相当于餐厅不等所有客人吃完才翻台,而是哪桌吃完就立刻安排新客人。

3.4 vLLM 系统架构与 PagedAttention 实现

vLLM 系统架构
vLLM 系统架构

图 3 是 vLLM 的系统架构总览,展示请求进入后由调度器管理 batch,显存由 Block Manager 统一分配,底层通过自定义 CUDA(Compute Unified Device Architecture,NVIDIA 并行计算平台)内核执行注意力计算。这张图说明 vLLM 不是一个简单的模型加载器,而是包含调度、显存管理与内核优化的完整推理引擎。

在实现层面,Block Table 是一个关键数据结构:

Python
class BlockTable:
    def __init__(self, block_size=16):
        self.block_size = block_size
        self.blocks = []          # 物理块索引列表
        self.num_tokens = 0       # 逻辑 token 数量

    def append_token(self):
        if self.num_tokens % self.block_size == 0:
            # 当前 block 满了,需要新块
            new_block = gpu_memory.allocate_block()
            self.blocks.append(new_block)
        self.num_tokens += 1

    def get_physical_indices(self, position: int):
        block_idx = position // self.block_size
        offset = position % self.block_size
        physical_block = self.blocks[block_idx]
        return physical_block * self.block_size + offset

注意力计算时,KV Cache 是分散的,需要自定义 CUDA Kernel 按 Block Table 加载物理块再计算得分。性能优化的关键在合并内存访问:GPU 多个线程同时访问不同 block 的数据,把分散的小请求合并成少量大请求,充分利用显存带宽。

四、推测解码:让 Decode 阶段不再等待

4.1 为什么需要推测解码

自回归解码一次前向传播只能生成一个 token,当 batch 较小时,Decode 阶段属于 Memory-Bound(显存带宽受限),GPU 算力没有被吃满。推测解码的思路是:用一个更小的草稿模型(Draft Model)快速猜出接下来 K 个 token,再让目标模型(Target Model)一次前向验证整段草稿,把多次串行前向变成一次并行验证。

推测解码示例
推测解码示例

图 4 是推测解码的一次草稿与验证过程。绿色表示草稿模型提出且被目标模型接受的 token,红色表示被拒绝的 token,蓝色表示目标模型对被拒绝位置的订正。图中目标模型只推理了 9 次,就解码出 38 个 token,这就是推测解码的收益来源。

4.2 算法流程

推测解码来自论文《Fast Inference from Transformers via Speculative Decoding》(2023),完整流程分五步:

  1. 生成草稿:草稿模型自回归生成长度为 K 的候选 token 序列,并记录每个候选 token 的概率 p。

  2. 评分:目标模型对这 K 个候选 token 做一次前向传播,得到真实概率 q;这一步与评分单个 token 的耗时相当。

  3. 判断接受:对每个候选 token,以 min(1, q/p) 作为接受概率,生成均匀随机数判断接受还是拒绝。

  4. 处理结果:如果全部接受,直接从目标模型采样第 K+1 个 token;如果第 t 个被拒绝,则从修正概率分布 q(x) - p(x) >= 0 中采样一个新 token。

  5. 返回结果:把接受的草稿 token 与新采样 token 拼接作为最终输出。

关键性质是:被拒绝的 token 会从修正分布重新采样,数学上保证最终输出分布与原始模型一致,即推测解码不改变生成质量。只要草稿的平均命中 token 数大于 0,就有推理加速。

4.3 实验结果要点

论文用 T5-XXL(11B)作为目标模型,用 T5-large(800M)、T5-base(250M)、T5-small(77M)作为草稿模型,在英语到德语翻译和文本摘要任务上测量加速比。结论有三点:

  • 比目标模型小几个数量级的草稿模型,其候选 token 被接受的概率 α(Alpha)通常在 0.5-0.9 之间。

  • 对于所有模型,草稿分布调整得越尖锐,α 越高;即使简单的一元、二元近似模型也能产生非零的 α。

  • 英语到德语翻译中,二元模型的 α 为 0.2,在草稿成本系数 c=0(草稿计算成本可忽略)时仍实现了 1.25 倍加速。

4.4 MEDUSA:去掉独立草稿模型

传统推测解码需要维护两个模型,工程上不够优雅。MEDUSA(论文《Medusa: Simple LLM Inference Acceleration Framework with Multiple Decoding Heads》,2024)提供 One Model 方案:在原始模型解码器后面增加多个解码头(Medusa Heads),不做 Next-Token 预测,而是并行预测后 4 个 token 的候选。

Medusa 的训练成本很低:原始模型参数冻结,只微调解码头,单个 A100-80G GPU 一天内即可完成。但实验显示,每个 Head 只选 Top-1 时,预测 next-next token 的准确率只有约 60%,因此 MEDUSA 改为每个 Head 选 Top-k,构建树状候选结构,并用 Tree Attention(树状注意力)在单次前向中并行验证所有候选路径。Attention Mask 保证每个 token 只能看到自己和祖先节点,不能看到兄弟节点。

MEDUSA 还引入 Typical Acceptance(典型接受)策略:接受标准不是概率最高,而是信息量与分布熵接近的“典型”token,既不太可预测、也不太意外。温度参数控制接受严格度:温度高则接受更多候选、解码更快但质量可能下降。MEDUSA-1 实现约 2.2 倍加速,MEDUSA-2 联合微调主干与解码头后达到 2.83 倍,在数学、代码、抽取类任务上超过 3 倍。

4.5 EAGLE:在特征层做自回归

EAGLE(Extrapolation Algorithm for Greater Language-model Efficiency)的核心区别在于预测对象。传统推测解码在 token 层预测,但离散 token 的不确定性高,例如输入 “The capital of France is”,下一个词可能是 Paris、located 或 a;而在特征层(倒数第二层隐藏状态),Paris 对应的特征向量与 France、capital 在语义空间中距离很近,序列更连续、更规则、更容易预测。

不同推测解码方法的草稿阶段对比
不同推测解码方法的草稿阶段对比

图 5 对比了四类方法的草稿阶段:Speculative Sampling 与 Lookahead 基于 token 预测 token;MEDUSA 各解码头独立使用特征预测 token;EAGLE 使用特征序列和提前一个时间步的 token 序列预测下一个特征,再从特征采样 token。这张图对应一个结论:EAGLE 通过特征级自回归降低了草稿预测难度。

EAGLE 的草稿模型由三部分组成:嵌入层和 LM Head 直接复用目标模型参数(不需要额外训练),自回归头部由一个全连接层和一个解码器层组成,先降维融合特征与 token 嵌入,再预测下一个特征。草稿模型参数量很小,7B 模型仅 0.24B 可训练参数;LLaMA2-Chat 70B 在对话、代码生成、数学推理和指令遵循任务上实现 2.7-3.5 倍延迟加速。

三代 EAGLE 的演进如下:

版本

核心突破

解决的问题

加速比

EAGLE-1

特征级预测 + Shifted-Token

Token 预测不确定性高

约 3.0 倍

EAGLE-2

动态草稿树(上下文感知)

静态树结构浪费计算

约 4.2 倍

EAGLE-3

Training-time Test + 多层特征融合

特征约束限制 Scaling、训练与推理不一致

约 6.5 倍

EAGLE-2 的改进是动态草稿树:草稿模型的置信度与接受率强正相关,扩展阶段优先扩展全局接受概率高的节点,重排阶段重新排序所有候选并保证仍构成连通树,同时调整注意力掩码使不同分支不可见。相比静态树,计算资源集中到高概率分支,比 EAGLE-1 快 20%-40%。

EAGLE-3 解决的是 EAGLE-1/2 的隐藏瓶颈。传统训练输入是目标模型的真实特征,推理时输入却包含草稿模型自己生成的特征,误差随生成步数累积,第 5 步接受率从约 0.8 降到约 0.6,而且训练数据扩大 8 倍加速比几乎不增长。EAGLE-3 在训练时模拟推理的多步生成,让草稿模型适应自己的预测结果,各步接受率都保持在约 0.8。

大 batch 场景下差异尤其明显:SGLang 测试中 batch=64 时,EAGLE-1/2 吞吐量降到 0.99 倍(几乎无加速),EAGLE-3 仍保持 1.38 倍。原因是大 batch 下 GPU 已经接近 Compute-Bound(算力受限),草稿计算边际收益小,草稿质量就成了决定因素。这给工程选型一个明确结论:高并发生产环境中,EAGLE-3 是目前能同时兼顾大 batch 高吞吐与推测解码加速的方案。

4.6 推测解码与 vLLM 的集成

推测解码与 Continuous Batching 结合时有三个冲突点,对应的解决方案:

冲突点

解决方案

草稿阶段需要 batch=1,Continuous Batching 需要动态 batch

草稿阶段作为独立 micro-batch,验证阶段合并入主 batch

MEDUSA 等 Tree Attention 需要特殊 Mask

vLLM 支持自定义 Attention Backend

草稿模型与 KV Cache 的显存碎片

PagedAttention 统一管理草稿和目标模型的 KV Cache

以 EAGLE + vLLM 为例,单次 Iteration 的流程是:先选择 batch 中序列最长或历史接受率高的请求;再用 EAGLE 草稿模型(0.24B 参数)树状采样 3-5 步生成候选树;目标模型通过 Tree Attention 并行验证所有候选;最后更新 Block Table,保留被接受 token 的 block,释放被拒绝的 block。

五、vLLM 性能监控与调优

5.1 关键性能指标

监控 vLLM 服务首先需要一组明确指标:

指标

含义

观测方式

TTFT(Time To First Token,首 Token 延迟)

从请求发出到收到第一个输出 token 的时间

流式输出时间戳

TPOT(Time Per Output Token,每输出 Token 耗时)

相邻输出 token 的间隔

流式输出时间戳

吞吐量

每秒生成的 token 数(tokens/s)

压测统计

接受率 α

草稿 token 被目标模型接受的比例

vllm:spec_decode_draft_acceptance_rate

接受率可以理解为猜题命中率:草稿模型猜了 5 个 token,目标模型验证后接受 3 个,则 α = 0.6。接受率越高推测解码加速越明显;接受率太低时,草稿计算就是纯开销。

延迟分位数 P50、P95、P99 分别表示第 50、95、99 百分位的延迟,用于衡量不同负载水平下的典型延迟和尾延迟。P50 反映多数请求的体验,P99 反映最差情况下的体验,是并发压测中观察饱和拐点的关键指标。

5.2 核心优化参数

vLLM 的启动参数按目标可分为四类,实际部署时按“高吞吐、低延迟、省显存”三个目标组合:

类别

参数

说明

并行与分布式

--tensor-parallel-size

张量并行(Tensor Parallelism,TP),按 GPU 数切分

并行与分布式

--pipeline-parallel-size

流水线并行(Pipeline Parallelism,PP),大模型跨节点使用

并行与分布式

--data-parallel-size

数据并行(Data Parallelism,DP),高并发场景

内存与 KV Cache

--max-model-len

最大序列长度,按实际需求设置避免浪费显存

内存与 KV Cache

--max-num-batched-tokens

最大批处理 token 数,越大吞吐越高

内存与 KV Cache

--max-num-seqs

最大并发序列数,按显存与序列长度调整

内存与 KV Cache

--kv-cache-dtype

KV Cache 数据类型,fp8 可省显存且损失小于 1%

内存与 KV Cache

--enable-chunked-prefill

分块预填充,强烈建议开启

内存与 KV Cache

--enable-prefix-caching

前缀缓存,多轮对话场景必开

内存与 KV Cache

--gpu-memory-utilization

GPU 显存利用率上限,推荐 0.90-0.95

量化与加载

--quantization

量化方式:awq、gptq、fp8、int8 等

量化与加载

--dtype

计算精度:float16、bfloat16、float32

推测解码

--speculative-model

草稿模型路径

推测解码

--num-speculative-tokens

推测 token 数,建议 3-5

推测解码

--speculative-draft-tensor-parallel-size

草稿模型 TP,通常为 1

调度与抢占

--scheduling-policy

fcfs(先来先服务)或 priority

调度与抢占

--preemption-mode

swap(换出到 CPU)或 recompute(重算)

5.3 监控工具栈

vLLM 内置 Prometheus 格式的 Metrics,启动时加 --enable-metrics 参数即可暴露:

纯文本
curl http://localhost:8000/metrics

vllm:prompt_tokens_total              # 输入 token 累计
vllm:generation_tokens_total          # 生成 token 累计
vllm:time_to_first_token_seconds      # TTFT 分布(P50/P99)
vllm:time_per_output_token_seconds    # TPOT 分布
vllm:gpu_kv_cache_usage_percent       # KV Cache 利用率
vllm:num_requests_running             # 当前运行中的请求数
vllm:num_requests_waiting             # 队列等待中的请求数
vllm:spec_decode_draft_acceptance_rate # 推测解码接受率(v0.8.5 以上)

深度分析可以用 NVIDIA Nsight Systems 做 Kernel 级分析,例如 nsys profile -o vllm_profile python -m vllm.entrypoints.openai.api_server,用于判断 Attention 内核是否成为瓶颈;生产环境则用 Grafana Dashboard 汇总关键指标做秒级监控。

5.4 场景化调优策略

场景一:TTFT 过高(大于 2 秒)。先诊断:如果 prompt 长度超过 4096 且并发请求超过 10,瓶颈是 Prefill 计算受限;如果 KV Cache 利用率超过 0.9,瓶颈是显存受限触发换出。对应解法:Chunked Prefill 把长 prompt 拆成 512 token 的小块与 decode 交错执行,避免长 prompt 独占 GPU;Prefix Caching 复用系统提示词的 KV Cache;必要时做 PD 分离,即把 Prefill(预填充阶段)和 Decode(解码阶段)分开部署。Chunked Prefill 有效的本质是分时复用,就像长途车每跑一段就让出车道给短途车。

场景二:TPOT 抖动大。先诊断 batch size 波动(1-32)和推测解码接受率骤降。对应解法:用 --max-num-seqs 64 控制 batch 波动上限;高温度(大于 0.8)时把草稿长度从 4 降到 2 或使用 Typical Acceptance;PD 分离后 Decode 独立运行,TPOT 更稳定。

场景三:GPU 利用率低于 50%。先诊断是 Memory-Bound(序列过长、显存占满但计算未满)还是通信开销(张量并行时 NVLink,即 NVIDIA 高速互联,成为瓶颈)。对应解法:增大 --max-num-seqs 直到 GPU 计算利用率达到 85% 以上;用 --quantization fp8 --kv-cache-dtype fp8 省显存提吞吐;batch 小于 8 时启用推测解码获得 2-3 倍加速。注意 batch 增大后推测解码效果会减弱,因为 GPU 已经从 Memory-Bound 转为 Compute-Bound,草稿模型的额外计算反而成为负担。

六、实战:Qwen3-0.6B 部署与性能测量

6.1 实验环境与启动配置

实验环境为 RTX 4090D(24 GB 显存)、Qwen3-0.6B(FP16)、vLLM 0.18.0。基础启动命令如下:

bash
python -m vllm.entrypoints.openai.api_server \
    --model Qwen/Qwen3-0.6B \
    --port 8000 \
    --dtype float16 \
    --enforce-eager \
    --trust-remote-code

启动脚本内置了四个场景配置,对应不同优化目标:

场景

关键参数

优化目标

basic

默认参数

快速验证环境可用

high_throughput

--max-num-seqs 64--max-num-batched-tokens 4096--gpu-memory-utilization 0.92--enable-prefix-caching

最大化 tokens/s

low_latency

--max-num-seqs 16--max-num-batched-tokens 512--enable-chunked-prefill--gpu-memory-utilization 0.90

最小化 TTFT/TPOT

save_memory

--max-model-len 2048--max-num-seqs 16--gpu-memory-utilization 0.85--preemption-mode swap

避免显存不足

服务启动后先用基础对话验证:单轮对话直接调用 OpenAI 兼容接口,多轮对话把历史消息追加到 messages 中,验证 Continuous Batching 下多轮交互正常,并检查 usage 中的输入输出 token 数。

6.2 CASE 1:流式输出与延迟测量

通过 stream=True 记录每个输出 chunk 的到达时间,可以精确计算 TTFT 和 TPOT:

Python
def measure_streaming_latency(prompt, max_tokens=200):
    token_timestamps = []
    request_start = time.perf_counter()
    stream = client.chat.completions.create(
        model="Qwen3-0.6B",
        messages=[{"role": "user", "content": prompt}],
        stream=True,
        max_tokens=max_tokens
    )
    for chunk in stream:
        if chunk.choices[0].delta.content:
            token_timestamps.append(time.perf_counter())
    # TTFT:从发送请求到收到第一个 token
    ttft = (token_timestamps[0] - request_start) * 1000
    # TPOT:相邻 token 间隔
    intervals = [(token_timestamps[i] - token_timestamps[i-1]) * 1000
                 for i in range(1, len(token_timestamps))]
    return ttft, intervals

分别用短、中、长三种 prompt 测试,结果如下:

测试场景

TTFT (ms)

TPOT 平均 (ms)

TPOT P50 (ms)

TPOT P95 (ms)

吞吐 (tok/s)

短 Prompt(“你好”)

355.5

13.6

14.0

14.8

58.8

中 Prompt(自注意力机制)

40.0

8.8

8.6

10.1

111.8

长 Prompt(PagedAttention 背景 + 问题)

63.2

9.3

9.1

10.5

106.3

一个反直觉的现象是短 Prompt 的 TTFT 反而最高(355.5 ms)。这是冷启动效应:第一个请求时 GPU 的 CUDA Kernel 尚未预热,包含即时编译(JIT,Just-In-Time)和内存分配,导致首次 TTFT 显著偏高;后续请求完成预热后回落到 40-63 ms。生产环境通常先发送预热请求,避免冷启动影响首批用户。

6.3 CASE 2:并发压测

压测脚本用线程池以指定并发度发送 20 个请求,统计 QPS(Queries Per Second,每秒查询数)、吞吐量与延迟分位数:

Python
with concurrent.futures.ThreadPoolExecutor(max_workers=concurrency) as executor:
    futures = [executor.submit(send_chat_request, p, max_tokens) for p in prompts]
    for f in concurrent.futures.as_completed(futures):
        all_results.append(f.result())
total_time = time.perf_counter() - bench_start
# 计算 QPS、吞吐量(tokens/s)、P50/P95/P99 延迟

分别以 1、2、4、8、16 并发度测试,结果如下:

并发度

QPS (req/s)

吞吐量 (tok/s)

P50 延迟 (ms)

P99 延迟 (ms)

1

1.02

101.0

899.8

1453.9

2

2.02

201.6

985.1

1048.2

4

3.85

385.4

1018.2

1128.3

8

6.35

635.0

1037.3

1101.5

16

9.87

987.2

1520.6

2085.3

两组关键结论:

  • 并发从 1 升到 8,吞吐量从 101 增长到 635 tok/s(约 6.3 倍),P50 延迟只从约 900 ms 增至 1037 ms(约 15%),这正是 Continuous Batching 的效果:GPU 并行处理多个请求,吞吐随并发近似线性增长。

  • 并发到 16 时出现拐点:P99 延迟跳升到 2085 ms,说明 GPU 算力接近饱和,服务从 Memory-Bound 转向 Compute-Bound。压测验证了理论预期:处理 1 个请求和 8 个请求的单请求耗时差别不大,但总吞吐量随并发近似线性增长。

6.4 CASE 3:Prometheus 指标采集

/metrics 端点采集并解析关键指标,用正则提取 Counter/Gauge 类型数值,并用 bucket 数据估算 TTFT、TPOT 分位数:

Python
patterns = {
    "prompt_tokens_total": r'vllm:prompt_tokens_total[^\n]*?\s+([\d.]+)',
    "generation_tokens_total": r'vllm:generation_tokens_total[^\n]*?\s+([\d.]+)',
    "num_requests_running": r'vllm:num_requests_running\s+([\d.]+)',
    "gpu_cache_usage_perc": r'vllm:gpu_cache_usage_perc\s+([\d.]+)',
}
for key, pattern in patterns.items():
    match = re.search(pattern, raw_text)
    if match:
        metrics[key] = float(match.group(1))

负载前后对比:

指标

负载前

负载后(20 个并发请求)

增量

输入 token 累计(prompt_tokens_total

6,514

6,774

+260

生成 token 累计(generation_tokens_total

21,634

24,634

+3,000

持续监控模式每 2 秒采集一次,负载期间输出如下:

纯文本
[  0.1s] 运行: 0  等待: 0  KV Cache: 0.0%  生成tokens: 21634
[  2.1s] 运行: 0  等待: 0  KV Cache: 0.0%  生成tokens: 22552   # +918
[  4.1s] 运行: 0  等待: 0  KV Cache: 0.0%  生成tokens: 23450   # +898
[  6.2s] 运行: 0  等待: 0  KV Cache: 0.0%  生成tokens: 24443   # +993
[  8.2s] 运行: 0  等待: 0  KV Cache: 0.0%  生成tokens: 24634   # 完成

指标解读:

  • 生成速度约为每 2 秒新增 900 tokens,即约 450 tok/s 的实际吞吐(8 并发 x 约 56 tok/s/请求),与并发压测的 635 tok/s 处于同一量级。

  • KV Cache 利用率为 0% 是因为 Qwen3-0.6B 模型很小(约 1.12 GB),RTX 4090D 有 24 GB 显存,KV Cache 占比极低;换成 7B/13B 模型或更长上下文,该指标会显著上升,此时 vllm:gpu_kv_cache_usage_percent 才是核心监控对象。

  • 运行中请求数为 0 是因为 vLLM 0.18.0 的部分 Gauge 指标在两次采样之间请求已完成;生产环境应搭配 Grafana 做秒级连续采集,避免采样窗口漏掉瞬时状态。

七、总结

高并发 AI 服务的优化路径可以归纳为三层:

第一层是显存管理。KV Cache 是并发扩展的物理约束,PagedAttention 用块化存储、按需分配、写时复制解决碎片与浪费,这是所有上层调度的基础。

第二层是调度策略。Continuous Batching 把调度粒度从请求级细化到迭代级,请求完成立即补位,让 GPU 保持满载,实测并发从 1 升到 8 时吞吐增长约 6.3 倍而 P50 延迟仅增 15%。

第三层是解码加速。小 batch 时推测解码把 Decode 阶段从 Memory-Bound 变成 Compute-Bound,MEDUSA 与 EAGLE 分别从多头预测和特征级预测两条路线去掉或弱化独立草稿模型;EAGLE-3 通过训练时模拟推理误差,在大 batch 高并发下仍保持 1.38 倍吞吐提升。

工程上的选择条件可以这样判断:batch 小、GPU 利用率低时优先启用推测解码;batch 已经很大时靠 Continuous Batching 和增大 --max-num-seqs 吃满算力,推测解码可能反而拖后腿;长 prompt 场景优先 Chunked Prefill 与 Prefix Caching;显存紧张时用 fp8 量化与 --kv-cache-dtype fp8。无论哪种场景,都应该先建立 TTFT、TPOT、吞吐量、KV Cache 利用率等指标的监控基线,再按指标做参数调整,避免凭感觉调参。

END