本实验借助 AI 完成工程实现与验证。下文区分已验证的观察、待验证的假设和结果的适用边界。
一、问题:性能对比首先要保证比较条件一致
参与 LLM 推理框架贡献时,性能类修改经常需要回答:这次修改到底带来了什么变化?
如果两次测试使用不同模型、请求长度、客户端或运行参数,即使数字变好了,也很难判断变化来自代码还是测试条件。
为后续性能 PR 建立可复用的验证基础,我搭建了一个小型 benchmark 工程,第一版只比较 vLLM 和 SGLang:
- 同一台机器、同一张 GPU。
- 同一个模型快照。
- 输入长度 1024,输出长度 256。
- 并发分别为 1、8、32、64。
- 收集 TTFT、TPOT、ITL、输出吞吐量、GPU 显存和 GPU 利用率。
- 保存原始结果、运行环境,并自动生成表格和图。
这次实验的目标是建立受控基线。结果只适用于本文的硬件、模型和配置。
二、复现:固定模型、环境和请求矩阵
硬件与软件环境
| 项目 | 配置 |
|---|---|
| GPU | NVIDIA RTX 4060 Laptop,8188 MiB |
| GPU 驱动 | 616.64 |
| PyTorch | 2.13.0+cu130 |
| PyTorch CUDA | 13.0 |
| 模型 | Qwen/Qwen2.5-0.5B-Instruct |
| 数据类型 | float16 |
| 最大上下文长度 | 2048 |
| 显存配置比例 | 0.45 |
| Tensor Parallel | 1 |
| Prefix Cache | 关闭 |
| CUDA Graph | 关闭 |
模型固定到以下 revision:
7ae557604adf67be50417f59c2c2f167def9a775
实际运行的引擎 commit:
makefile
vLLM:
ced6857afa0ea7b2e3f0846a62e1394e90f15607
SGLang:
af5c0659c7c133b77aa0e1557c6db83a449d093d
两套引擎使用同一份固定版本的 vLLM 官方 benchmark 客户端,通过 OpenAI-compatible completions 接口发起请求。
客户端运行时版本为 vLLM 0.30.0,同时保存安装包元数据,避免只记录仓库版本。
Workload
| 项目 | 设置 |
|---|---|
| 输入 | 随机合成 prompt,1024 tokens |
| 输出 | 256 tokens |
| 并发 | 1、8、32、64 |
| 每组重复次数 | 3 |
| 每次正式测试请求数 | 64 |
| 正式测试前预热 | 16 个请求 |
| seed | 20261002 |
| temperature | 0 |
| ignore EOS | 开启 |
两个引擎、四档并发、每档三次重复,总计:
ini
2 × 4 × 3 = 24 次正式运行
24 × 64 = 1536 个正式请求
还需要说明后端选择:本次 vLLM 关闭 FlashInfer sampling,SGLang 使用 Triton attention 和 PyTorch sampling。因此,这是一组明确配置下的比较,不能代表两个引擎各自默认配置或最佳调优结果。
工程入口为:
lua
python3 study.py config.local.json
配置文件需要填写实际环境、模型路径和启动参数。脚本负责启动服务、等待就绪、运行客户端、采样 GPU 指标并生成报告,只清理自己启动的进程组。
三、Root Cause:为什么只有最终数字还不够
本次没有修复某个引擎的单一性能 bug,也没有通过 profiling 确认具体瓶颈。工程首先处理的是测试证据容易失真的几个环节。
1. 请求长度不等于实际生成长度
设置输出长度为 256,不代表每个请求实际生成了 256 tokens。
提前 EOS、请求失败或统计口径变化,都可能改变工作量。吞吐量变高时,必须先确认服务确实完成了相同数量、相同长度的请求。
因此,报告生成前会检查实际输入和输出长度,以及成功请求数。
2. 工作树 commit 不一定对应实际安装版本
仓库 checkout 到某个 commit,并不能单独证明运行中的 Python 包就来自该 commit。
本次同时记录引擎源码版本、运行时版本和安装包元数据。来源不明确的数据不能作为有效比较结果。
3. GPU 利用率不能直接证明瓶颈
GPU 利用率是设备采样指标。它无法单独说明某个 kernel 已经饱和,也无法直接区分排队、prefill、decode 或 CPU 调度开销。
本次显存和利用率均为整张设备的指标,还可能包含 Windows、WSL 和桌面进程的影响。它们不是引擎进程独占的资源统计。
四、修改:把比较条件变成工程约束
工程主要实现了四部分。
首先,统一客户端和 workload,避免两个引擎使用不同请求生成方式或统计口径。
其次,保存服务启动日志、版本信息、原始客户端结果和 GPU 采样。GPU 采样间隔为 0.5 秒,通过正式测试的开始标记与运行时长近似对齐采样窗口。
然后,在生成报告前检查数据完整性。请求失败、长度不一致、指标非有限值、关键环境信息缺失或源码状态不满足要求时,不用默认值填补结果,而是拒绝生成有效比较。
最后,自动输出:
- Markdown 实验报告。
- CSV 和 JSON 结果。
- PNG 和 SVG 图。
- 原始文件的 SHA-256 校验信息。
报告中的延迟和吞吐量采用"三次运行各自均值的中位数"。
图中的误差线表示三次运行的最小值到最大值,不是置信区间。
五、测试:先验证数据可信,再解释性能
完整实验完成了 24 次正式运行、1536 个请求,实际输入和输出长度符合 1024/256 的要求。
工程工具和健康检查相关测试共 9 项通过。
搭建过程中也遇到了环境问题,包括 CUDA 编译工具与依赖版本匹配、首次启动编译耗时,以及服务健康检查超时。处理这些问题后,保留了对应日志和健康检查回归验证。
这些验证能够支持"本文记录的请求与指标符合实验约束",但没有覆盖完整模型精度验证、长期稳定性或生产 SLA。
六、Benchmark:并发增加,吞吐与延迟一起上升
TTFT 表示首 token 延迟;TPOT 表示首 token 后的平均每 token 时间;ITL 表示 token 间隔延迟。下表中两项数值一致,是本次结果的表现。
显存与 GPU 利用率是正式测试窗口内的设备级采样统计。每个单元格汇总三次重复运行。
| 引擎 | 并发 | TTFT ms | TPOT ms | ITL ms | 输出 tok/s | 显存 MiB | GPU % |
|---|---|---|---|---|---|---|---|
| vLLM | 1 | 108.797 | 25.656 | 25.656 | 38.265 | 7602 | 56.520 |
| vLLM | 8 | 336.269 | 26.687 | 26.687 | 288.611 | 7632 | 59.491 |
| vLLM | 32 | 1540.927 | 36.519 | 36.519 | 748.255 | 7639 | 76.061 |
| vLLM | 64 | 4483.341 | 43.594 | 43.594 | 1024.441 | 7632 | 83.584 |
| SGLang | 1 | 68.901 | 24.908 | 24.908 | 39.870 | 7762 | 52.032 |
| SGLang | 8 | 283.826 | 26.950 | 26.950 | 275.718 | 7753 | 52.489 |
| SGLang | 32 | 983.756 | 29.631 | 29.631 | 958.525 | 7738 | 63.048 |
| SGLang | 64 | 1722.324 | 32.651 | 32.651 | 1630.277 | 7738 | 75.027 |
观察一:提高并发带来了吞吐增长,也增加了延迟
两个引擎都呈现出类似趋势:并发增加时,输出吞吐提高,TTFT 和 TPOT 也上升。
可能的解释是更多请求形成批处理,摊薄了部分执行开销;同时,每个请求需要承担更多等待和共享资源开销。
但这仍是待验证的解释。本次没有做分阶段 profiling,不能据此认定变化具体来自 scheduler、attention kernel 或 prefill/decode 的某一部分。
观察二:高并发结果存在明显重复运行差异
并发 64 时,三次运行的输出吞吐范围为:
bash
vLLM:827.42--1271.89 tok/s
SGLang:1577.79--1640.69 tok/s
因此,只展示一次运行或单个汇总值,会遗漏重要信息。
采样中温度和时钟也有变化,但更快的运行并不总对应更高时钟。本次证据不足以把吞吐波动归因于温度或降频。
观察三:有限请求波次不能代表持续服务能力
并发 64 的每次测试只有 64 个正式请求,更接近一轮有限请求波次。
它不能直接回答持续到达流量下的最大承载能力,也不能推导 p99 延迟或 SLA。
此外,两个引擎顺序运行,没有交替配对测试,桌面负载也未完全隔离。这些因素都限制了结果的解释范围。
观察四:显存高不等于请求失败,也不代表还有足够余量
本次有效请求未发生 OOM,但设备级显存占用已较高。
这只能说明当前模型、配置和 workload 成功完成,不能推导更长上下文、更大模型或更高持续负载也能够运行。
七、Maintainer 反馈:目前仍是本地验证基线
这套 benchmark 工程尚未获得 maintainer review,也没有对应的已合并性能 PR。
本文数据没有验证 HiCache、P2P KV offload 等特定功能路径,不能作为这些功能的性能证据。
后续如果某个具体 PR 涉及性能变化,将使用这套工程执行 Before/After:
- 固定模型、机器、客户端和配置。
- 尽量只改变待验证的代码 commit。
- 交替运行 baseline 和修改版本。
- 保存原始数据和环境信息。
- 结合具体代码路径解释变化。
如果 PR 只修复正确性问题,没有合理的性能影响预期,就优先提供复现与回归测试,不额外制造性能结论。
八、踩坑总结:让结果可以被检查
这次工程最有价值的部分,是把测试条件和结果边界保存下来。
- 检查实际工作量。 参数写了 1024/256,还要验证请求确实完成了相应长度。
- 记录实际运行版本。 仓库 commit、安装包和运行时信息需要对应。
- 保留重复运行差异。 中位数之外,范围同样重要。
- 区分设备指标与进程指标。 整卡显存和利用率不能直接归因于引擎。
- 区分观察与原因。 吞吐变化已经测到,具体瓶颈仍需要进一步验证。
- 明确适用范围。 小模型、笔记本 GPU 和有限请求波次的结论,不能直接推广到生产集群。
后续会围绕真实性能 PR 使用这套基线,输出可检查的 Before/After 证据,并解释变化与代码之间的关系。
参考资料: