vLLM Serving延迟调优:H100上TTFT与ITL的配置优化实战
2026/9/10 22:30:57 网站建设 项目流程

实际做 LLM 在线推理服务时,“服务能启动”和“服务的延迟指标达标”是两件完全不同的事。最近在 8 卡 H100 上做 vLLM Serving 压测实验时,最大的体会是:同一个模型、同一份请求负载、同样的并发数,只调整 vLLM 的启动配置,p95 TTFT 和 p95 ITL 就能出现明显差异;配置调优之后的效果,可以稳定超过默认基线。这里的 TTFT 是 Time To First Token,即首 token 延迟;ITL 是 Inter-Token Latency,即 token 间延迟;p95 是 95 分位延迟,用来衡量尾部性能。

这批实验的价值不在于某一个具体参数的值,而在于一套可复现的调优流程:先搭建环境并记录基线,再理解关键配置项对 prefill 和 decode 两个阶段的真实影响,然后用对照实验验证每一项改动的收益,最后把结论沉淀成可复用的发布检查清单。本文会按这条主线展开,先讲清楚 TTFT、ITL 和 p95 的测量口径,再给出 H100 环境下的 vLLM 部署步骤,接着逐项拆解影响延迟的关键配置参数,然后给出一份可直接使用的实验脚本和排查清单。

这个主题适合已经能跑通 vLLM 基础服务、但在压测时发现首 token 偏慢、并发上来后 tail latency 明显抬高的开发者。无论你是要给生产环境选参数,还是给团队做容量评估,下面的方法都可以直接复用。

1. 先搞清楚优化对象:TTFT、ITL 和 p95 分别度量什么

1.1 一次推理请求的两个阶段

一次 LLM 在线推理请求在 vLLM 内部大体分成两个阶段:预填充(prefill)和解码(decode)。

预填充阶段对用户输入的全部 prompt token 做一次前向计算,产出第一个输出 token。从请求到达服务端算起,到客户端收到第一个输出 token 的时间,就是 TTFT。解码阶段则是逐 token 生成后续内容,每两个连续输出 token 之间的时间间隔就是 ITL。vLLM 在线服务的日志和 metrics 里常出现的 TPOT(Time Per Output Token)与 ITL 含义相近,都是衡量输出阶段单步耗时的指标。

实际项目中要对齐几个边界,否则指标会失真:

  • TTFT 是端到端概念,包含网络传输、排队、调度、prefill 计算、首 token 返回这几段。单独看模型耗时不够,要同时观察客户端侧耗时和服务端指标。
  • ITL 关注的是稳定程度。平均值能反映整体吞吐,但 p95 ITL 能暴露并发升高后的调度抖动、显存不足导致的抢占、以及 batch 中长输入拖慢短请求等问题。
  • p95 表示 95% 的请求延迟都低于这个值。它比平均值更敏感,也是生产环境 SLA 常用的统计口径。

1.2 平均值、p95、p99 怎么选

只优化平均值,很容易掩盖尾部问题。一个典型场景是:默认配置下,短 prompt 请求的平均 ITL 看起来不错,但短请求和长请求混跑时,长请求的 prefill 会阻塞解码阶段,导致短请求的 ITL 尾部明显上抬。此时平均值可能只涨了 10%,p95 却涨了 40%。

所以每轮实验至少记录四组数据:

指标平均值p95p99
TTFT每轮压测记录每轮压测记录每轮压测记录
ITL每轮压测记录每轮压测记录每轮压测记录
TPOT每轮压测记录每轮压测记录每轮压测记录
吞吐tokens/s,按轮统计

如果只记录平均值,调优方向很容易被误导。p95 和 p99 才是判断“配置是否真的超过基线”的关键口径。

1.3 为什么在 H100 上做这类实验

H100 SXM 80GB 配备 HBM3 显存,显存带宽高、容量大,NVLink 互联带宽也很可观。这些硬件特性决定了它能承受更大的 batch 和更长的上下文。但大显存不会自动带来低延迟,相反,显存越大,默认配置越容易把 batch 拉大;batch 一旦变大,prefill 阶段和 decode 阶段对 GPU 计算资源的争抢就更明显。

这也是标题里“config beats the baseline”这句话的实际含义:硬件相同、模型相同、负载相同,差异来自 vLLM 如何分配显存、如何调度请求、如何切分模型并行。H100 只是放大了这些配置项的效果,让问题更容易被观察到。

2. H100 环境准备:先复现基线,再谈优化

2.1 硬件、驱动、Python 版本确认

拿到服务器之后,别急着起服务。先把环境确认一遍,避免后面所有结果都被驱动或 CUDA 版本污染。

检查项推荐值说明
GPU 型号与数量H100 SXM 80GB,8 卡4 卡也可以,并行参数需要相应调整
驱动版本550 或更新新版 vLLM 对驱动有最低版本要求
CUDA 版本12.1 以上优先看 vLLM 官方要求的 cu 版本
Python3.10 或 3.11部分依赖在 3.12 下兼容性不稳定
模型格式HuggingFace 权重第一步先不量化,方便定位问题

验证命令:

nvidia-smi python -c "import torch; print(torch.cuda.is_available(), torch.cuda.get_device_name(0))"

这里要强调一个常见坑:torch.cuda.is_available()返回True,不代表 PyTorch、CUDA、vLLM 三者版本匹配。环境问题应该在实验开始前解决,而不是在压测结果异常时怀疑配置参数。

2.2 安装 vLLM 并在 Ubuntu 上启动

官方推荐用 pip 安装编译好的 wheel。以 Ubuntu 24.04 部署为例,先建虚拟环境再安装:

python -m venv .venv source .venv/bin/activate pip install --upgrade pip pip install vllm

如果原始环境没有锁定版本,落地前要先确认 vLLM 与 PyTorch、CUDA 的匹配关系。生产环境建议把requirements.txtpyproject.toml里的版本固定住,并记录在实验说明里。

注意:不要直接在 Windows 上尝试用 pip 安装 vLLM 做生产部署。vLLM 原生依赖 CUDA 生态和 Linux 内核特性,Windows 下的支持非常有限,实验里见到的问题大多是环境问题,而不是配置问题。

2.3 用默认参数跑出第一版基线

基线阶段要尽量“不改任何优化参数”,只设置必要参数:模型路径、端口、服务名、张量并行数。

CUDA_VISIBLE_DEVICES=0,1,2,3,4,5,6,7 \ python -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen2.5-72B-Instruct \ --served-model-name qwen2.5-72b \ --tensor-parallel-size 8 \ --host 0.0.0.0 \ --port 8000 \ --disable-log-requests

--disable-log-requests是为了避免压测时请求日志刷屏。压测阶段建议保留默认的 metrics 输出,稍后可以用/metrics接口观察服务端指标。

基线启动后,不要立刻压测。先用一个低并发请求确认服务和模型加载正常:

curl http://127.0.0.1:8000/v1/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"qwen2.5-72b","messages":[{"role":"user","content":"你好"}],"max_tokens":16}'

能正常返回之后,再记录第一版基线数据。基线数据是后面所有对照实验的参照物,一定要把启动参数、模型路径、请求配置完整保存下来。

3. 影响 TTFT 和 ITL 的关键配置项

3.1 显存分配:gpu-memory-utilization 与 KV cache

--gpu-memory-utilization控制 vLLM 最多占用多少比例的 GPU 显存。默认值通常是 0.9,也就是预留 10% 给模型权重之外的计算开销。

这个参数直接影响 KV cache 能分配多少空间:

  • 调大:KV cache 空间更多,能容纳更多并发请求,排队和抢占减少,TTFT 的尾部通常会下降。
  • 调小:显存更安全,但并发能力下降,请求排队变长,p95 TTFT 会抬升。
  • 调得过大:模型加载、CUDA context、临时 buffer 分配时可能直接 OOM。

配合它的是--swap-space,默认约 4GB,代表 CPU 内存里预留多少空间做 KV cache 换出。实验阶段不建议依赖 swap,因为它会显著抬升 ITL。

3.2 批处理上限:max-num-seqs 与 max-num-batched-tokens

vLLM 的核心优势是 continuous batching,也就是不需要等一个 batch 全部结束再接收新请求,而是在每个迭代步结束后动态调整 batch。--max-num-seqs控制一个迭代步里最多同时处理的请求数,默认常见值是 256;--max-num-batched-tokens控制一个迭代步里最多处理的 token 总数。

这两个参数需要一起理解:

  • max-num-seqs太小:并发请求被拒或排队,TTFT 尾部变高。
  • max-num-seqs太大:batch 变大,单次前向计算时间变长,ITL 和 TPOT 上升。
  • max-num-batched-tokens太小:GPU 利用率不足,吞吐下降。
  • max-num-batched-tokens太大:一个迭代步耗时超过预期,输出节奏变慢。

不同版本的默认值差异较大,有的版本默认 2048,有的版本跟随max-model-len。实验前先执行python -m vllm.entrypoints.openai.api_server --help确认当前版本的实际默认值,不要凭记忆配置。

3.3 上下文长度上限:max-model-len 与调度预分配

--max-model-len是模型支持的上下文长度上限。vLLM 的调度器会按这个上限为每个请求预留 KV cache 空间。如果设置得过大,可容纳的并发请求数会下降,请求更容易排队,p95 TTFT 会明显抬升;如果设置得过小,长请求会直接报错或被拒绝。

实验阶段建议先统计业务里 prompt 长度和期望输出长度的分布,再决定max-model-len。例如业务中 95% 的请求总长度在 4K token 以内,就不要为了偶尔几个长请求把上限设到 32K,可以考虑单独为一个长上下文服务实例配置更大的长度。这里的取舍本质是“资源预留粒度”的取舍,不是越大越好。

3.4 预填充与解码的协调:chunked prefill

长请求的 prefill 会占用一整段 GPU 计算时间,期间若 batch 里有短请求在 decode,短请求的输出就会被推迟。chunked prefill 会把一次 prefill 的输入 token 切分成多个 chunk,插入到 decode 迭代之间执行,避免长 prefill 长时间阻塞其他请求。

实际效果:

  • 对混合长短请求的在线服务,chunked prefill 能明显降低 p95 ITL。
  • 代价是单条长请求的 TTFT 可能略有上升,因为 prefill 被分片后不是一次性算完。
  • 在较新版本的 vLLM 中,chunked prefill 可能默认开启;旧版本需要用--enable-chunked-prefill显式开启。落地前先确认版本行为。

这个配置是“配置超过基线”的常见来源之一,尤其适合在线对话类负载。

3.5 并行策略与实践参数:tensor-parallel、enforce-eager

--tensor-parallel-size决定模型权重如何切分到多张卡上。对 72B 级别的模型在 8 卡 H100 上,TP=8 是常见选择,因为单卡放不下完整权重。对较小模型,TP 过大反而引入不必要的通信开销,ITL 可能变差。实验时应按模型实际显存需求选择最小可用 TP。

--enforce-eager是个经常被误用的参数。它关闭 CUDA graph 加速,降低显存占用,但会牺牲一部分执行速度和稳定性,通常只用于调试或显存极度紧张的场景。生产环境不要默认加这个参数。

量化参数也会间接影响延迟。FP8、AWQ、GPTQ 等量化方式能减少模型权重占用,从而释放显存给 KV cache,提高并发能力。但量化会改变计算特性,必须单独做一轮对照实验,不能假定一定更快。

3.6 前缀缓存与推测解码

--enable-prefix-caching允许多请求共享相同前缀的 KV cache,对多轮对话、system prompt 固定的场景收益明显,能降低重复 prefill 带来的 TTFT。

推测解码(speculative decoding)用一个小模型先草拟多个 token,再由大模型一次性验证。在 batch 较小的场景下,它能降低单请求的 ITL;在 batch 很大的场景下收益可能被稀释。接入方式因版本而异,实验前先查看当前版本的--speculative-model相关参数说明。

3.7 容易被忽略的辅助配置

配置项作用建议
--enable-metrics输出 Prometheus 格式指标到/metrics压测和生产都建议开启
--disable-log-requests关闭每个请求的访问日志压测时减少日志 I/O 干扰
--served-model-name对外暴露的模型名固定名称,方便客户端对齐
--max-parallel-loading-workers模型加载并行数大模型加载时可适当调大提速

4. 配置对照实验:如何证明“配置超过基线”

4.1 设计请求负载

对照实验最怕负载不固定。每次请求长度不同、输出长度不同、并发忽高忽低,结果就无法归因到配置项上。

推荐固定以下维度:

  • prompt 长度固定,例如统一 512 token。
  • 输出长度固定,例如统一max_tokens=128
  • 并发数固定,例如 32、64、128 三档分别压测。
  • 压测时长固定,例如预热 2 分钟,正式 5 分钟。

用 OpenAI SDK 写一个最小压测脚本,记录每个请求的 TTFT 和完整 token 序列:

import statistics import time from concurrent.futures import ThreadPoolExecutor from openai import OpenAI client = OpenAI(base_url="http://127.0.0.1:8000/v1", api_key="EMPTY") def send_one(prompt, max_tokens=128): start = time.perf_counter() resp = client.chat.completions.create( model="qwen2.5-72b", messages=[{"role": "user", "content": prompt}], max_tokens=max_tokens, stream=True, ) first_token_time = None token_times = [] last_time = time.perf_counter() for _ in resp: now = time.perf_counter() if first_token_time is None: first_token_time = now - start token_times.append(now - last_time) last_time = now return { "ttft": first_token_time, "itls": token_times, "total": time.perf_counter() - start, } def run_load(concurrency, rounds=50): results = [] with ThreadPoolExecutor(max_workers=concurrency) as pool: futures = [pool.submit(send_one, "你好" * 256) for _ in range(rounds)] for f in futures: results.append(f.result()) return results

这里注意两点:

  • 使用stream=True,否则拿不到首 token 时间。
  • 记录每个 token 的间隔,而不是只记录总耗时,否则 ITL 只能靠总耗时除以 token 数去近似,会掩盖抖动。

4.2 记录指标与保存实验现场

每轮实验结束后,把结果序列化保存,并同时采集服务端/metrics

curl -s http://127.0.0.1:8000/metrics | grep vllm | tee metrics_round_01.txt

客户端脚本负责输出:

round=01 concurrency=64 requests=50 p95_ttft=1.23s avg_ttft=0.98s p99_ttft=1.51s p95_itl=42ms avg_itl=31ms p99_itl=58ms throughput=... tokens/s

/metrics里重点看vllm:num_requests_runningvllm:num_requests_waiting,用来判断延迟抬升是来自执行时间还是排队。

4.3 实验矩阵与执行顺序

建议按下面顺序推进,每轮只改一个变量:

轮次实验目的改动点
0基线默认配置
1验证 chunked prefill开启--enable-chunked-prefill
2验证前缀缓存增加--enable-prefix-caching
3验证 batch 容量调整--max-num-seqs
4验证显存分配调整--gpu-memory-utilization
5最终组合把有效改动合并

每轮都必须重启服务,同一个进程内改参数不生效。重启后先低并发冒烟,再进入正式压测。把启动命令、负载参数、结果文件放在同一个目录,避免后期追查时不知道数据来源。

5. 实验示例:从基线到优化配置的完整对照

5.1 基线配置示例

python -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen2.5-72B-Instruct \ --served-model-name qwen2.5-72b \ --tensor-parallel-size 8 \ --host 0.0.0.0 --port 8000 \ --disable-log-requests

5.2 优化配置示例

python -m vllm.entrypoints.openai.api_server \ --model /data/models/Qwen2.5-72B-Instruct \ --served-model-name qwen2.5-72b \ --tensor-parallel-size 8 \ --gpu-memory-utilization 0.92 \ --max-num-seqs 128 \ --max-num-batched-tokens 4096 \ --max-model-len 8192 \ --enable-chunked-prefill \ --enable-prefix-caching \ --host 0.0.0.0 --port 8000 \ --disable-log-requests \ --enable-metrics

注意,这份参数是针对 72B 模型和固定业务负载的示例,不是通用最优解。--max-num-seqs从默认值降到 128,看起来是“降低并发”,但实际作用是控制单步 batch 过大导致 ITL 变差;配合更大的--max-num-batched-tokens和 chunked prefill,让 GPU 在吞吐和延迟之间更平衡。

5.3 结果与分析思路

下面这组数字只用于演示分析方法,不同模型、不同请求分布下结果会有差异:

场景p95 TTFTp95 ITL备注
基线1.85s68ms长请求 prefill 周期性阻塞 decode
开 chunked prefill1.76s41msITL 明显改善,TTFT 变化不大
再开 prefix caching1.52s38ms重复前缀请求受益
调整 batch 上限后1.31s35ms排队和单步 batch 同时改善
最终组合1.28s33ms整体超过基线

判断配置是否有效的标准不是单一指标变好,而是:

  • p95 TTFT 和 p95 ITL 都要看,不能只挑好看的说。
  • 对比/metrics里 running 和 waiting 请求数,区分延迟来自执行还是排队。
  • 如果 ITL 全面下降但吞吐下降超过 20%,要确认业务更在意延迟还是吞吐。

6. 常见问题与排查路径

6.1 现象到原因排查表

问题现象常见原因检查方式处理建议
服务启动后显存 OOMgpu-memory-utilization过大,或 CUDA context 占用超预期查看nvidia-smi中进程占用降低该配置,或开启--enforce-eager临时验证
并发上来后 TTFT 突然抬升请求排队,max-num-seqs或 KV cache 不足/metricsvllm:num_requests_waiting调大gpu-memory-utilization,或降低max-model-len
p95 ITL 偏高长 prefill 阻塞 decode,或 batch 单步过大关闭其他配置单独压测开启 chunked prefill,降低max-num-seqs
双卡/多卡无法启动tensor-parallel-sizeCUDA_VISIBLE_DEVICES不一致nvidia-smi核对卡序显式指定CUDA_VISIBLE_DEVICES,确认卡数量足够
/metrics没有输出未开启--enable-metrics,或网络策略屏蔽端口curl /metrics检查启动命令增加--enable-metrics
--enforce-eager加不加都纠结误以为它一定提升性能对比两轮压测生产默认关闭,调试显存问题时再开
MoE 模型 TTFT 偏高参数量大但激活参数少的模型,prefill 阶段仍要处理完整权重对比同规模 Dense 模型优先优化 prefill 阶段的 batch 调度,必要时量化

6.2 排查链路

遇到异常指标,按顺序排查,不要跳步:

  1. 输入是否正确:请求模型名、prompt、max_tokens是否符合预期。
  2. 文件路径和命名:模型路径、served-model-name是否写错。
  3. 依赖版本:vLLM、PyTorch、CUDA、驱动版本是否匹配。
  4. 配置是否生效:重启后检查启动日志中的实际参数。
  5. 资源状态:显存、CPU、网络、端口是否被占用。
  6. 日志关键字:vllm相关 ERROR、WARNING、OOM 信息。
  7. 版本限制:当前 vLLM 版本是否支持你使用的参数和模型架构。

6.3 enforce-eager 到底要不要用

实践中经常有人因为显存不足,给启动命令加上--enforce-eager,然后发现首 token 变慢。原因很简单:CUDA graph 能减少 kernel launch 开销,关闭后每次前向计算都变慢。它适合用来排除显存问题,但不要把它当成常态优化手段。

如果加了--enforce-eager仍然 OOM,说明模型权重、KV cache 和 CUDA context 的总需求已经超过显存物理容量。正确做法是降低max-model-lenmax-num-seqs,或使用量化模型,而不是继续压榨默认显存上限。

7. 最佳实践与扩展方向

7.1 vLLM 生产发布前检查清单

  • 固定 vLLM、PyTorch、CUDA 版本,保存启动命令到代码仓库。
  • 按业务峰值预留显存缓冲,gpu-memory-utilization不要在生产环境拉满。
  • max-model-len与业务请求长度分布对齐,不要贪长。
  • 开启--enable-metrics,对 TTFT、ITL、waiting 请求数配置告警。
  • 压测前先预热,预热时间不少于 2 分钟。
  • 客户端设置超时和重试,避免服务端排队拖垮调用方。
  • 每轮实验保存完整现场:启动命令、负载参数、原始结果、metrics 快照。
  • 上线前用真实业务流量回放做一次回归,确认配置收益在真实分布下仍然成立。

7.2 进一步优化方向

如果基础配置调优已经做完,下一步可以从这几个方向继续:

  • 推测解码:在小并发场景下降低单请求 ITL,需要评估草稿模型额外显存成本。
  • 量化选型:FP8、AWQ、GPTQ 在不同模型上的延迟特性不同,必须逐模型实验。
  • 调度与版本升级:新版本 vLLM 的调度策略和默认参数会变化,升级后要重新跑一轮对照。
  • 与其他推理引擎做横向对比:SGLang 等方案在部分前缀复用和调度场景下表现不同,选型要基于自己的负载,而不是别人的基准测试。
  • 模型结构本身的优化:MoE 类模型 prefill 阶段计算量大,TTFT 偏高是结构特性,服务层可以配合 batch 调度缓解,但根本改善依赖模型侧优化。

做 vLLM 调优最忌讳的是“一次改十个参数,看起来变好了就上线”。真正有效的方法是把每一项改动单独验证,确认收益来源,再组合出稳定方案。这套流程在 H100 上成立,换到其他型号的 GPU 也成立,差别只是具体的推荐参数不同。把基线、对照、复现这三件事做好,vLLM Serving 的延迟问题就变成了一个有清晰路径的工程问题,而不是靠运气调参。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询