
1. 为什么并发实验的数字总在“打架”做推理服务性能测试的人大概都经历过这种场面同一台机器、同一份模型权重、同一批请求上午跑出来的吞吐是 2400 tokens/s下午再跑就变成 1800换个人来跑又变成 2100。然后团队里开始互相怀疑——是不是你压测脚本写错了是不是机器被别人占了是不是模型加载方式不一样问题往往不在人身上而在于并发实验的变量没有被真正锁死。vLLM 和 SGLang 这两个推理引擎表面上看都是“起个服务、发请求、看吞吐”但它们内部对显存管理、请求调度、批处理策略的处理方式差异很大任何一个环节没对齐数字就失去可比性。这篇内容面向的是需要给推理服务做容量规划、选型对比、性能回归的工程师。我会把“让性能数字可复现、可解释”这件事拆开讲哪些变量必须固定、哪些指标必须定义清楚、vLLM 和 SGLang 在并发场景下的行为差异在哪、以及怎么设计一套别人拿去就能跑出同样结果的实验流程。核心关键词就三个vLLM、SGLang、并发实验目标只有一个——让数字站得住脚。先说一个反直觉的结论并发实验里最难复现的不是峰值吞吐而是延迟分布。吞吐数字大家都能跑到差不多的量级但 P99 延迟经常差出两三倍原因通常藏在请求到达模式、调度队列积压和显存碎片里。所以后面我会把重点放在“怎么让延迟也可解释”上而不只是盯着 tokens/s。2. 先把实验变量钉死一份可复现的基线清单在跑任何对比之前得先承认一件事没有基线清单的实验等于没做实验。我见过太多人直接pip install vllm然后开跑跑完发现 torch 版本被顺带升级了CUDA 算子行为变了数字自然对不上。下面这份清单是我踩过几次坑之后固定下来的建议直接抄。2.1 环境层那些容易被忽略的隐式变量环境层要固定的东西比想象中多。除了 GPU 型号、驱动版本、CUDA 版本这些显性项还有几个隐式项经常被漏掉Python 与 PyTorch 版本vLLM 对 torch 版本有强依赖安装 vLLM 时很可能把你环境里已有的 torch 替换掉。如果你之前装过特定版本的 torch 做别的实验装完 vLLM 后那个实验可能就跑不通了。所以要么用独立虚拟环境要么在装之前pip freeze before.txt装完再pip freeze after.txt做 diff。编译缓存状态vLLM 和 SGLang 都会在首次运行时做算子编译尤其是用了 FlashAttention、Triton kernel 的情况下。首次运行慢、后续快这是正常的。对比实验要么都跑“冷启动”要么都跑“热启动”不能一个冷一个热。GPU 时钟与功耗墙数据中心卡和消费卡在持续负载下会降频。如果 A 引擎跑 10 分钟、B 引擎跑 3 分钟B 可能还没触发降频就结束了数字自然好看。建议固定测试时长或者用nvidia-smi -lgc锁频如果权限允许。我一般会写一个env_snapshot.sh把下面这些一次性 dump 出来存档nvidia-smi --query-gpuname,driver_version,memory.total,clocks.max.sm --formatcsv python -c import torch; print(torch.__version__, torch.version.cuda) pip list | grep -Ei vllm|sglang|torch|transformers|flash提示这份快照要跟实验结果一起存。三个月后有人质疑你的数字你能拿出当时的完整环境比任何解释都有用。2.2 模型层权重、精度、并行度三件套模型层最容易出问题的是精度。同一个模型FP16 和 BF16 的吞吐能差 10% 以上量化版本INT8、FP8、AWQ、GPTQ差异更大。对比实验里两个引擎必须用完全相同的权重文件和相同的 dtype。并行度也要对齐。vLLM 用--tensor-parallel-sizeSGLang 用--tp-size名字不同但含义一样。如果一个是 TP2、一个是 TP1那比出来的不是引擎差异是并行策略差异。单卡能放下就都用单卡放不下就都设成相同的 TP 值并且记录清楚。还有一个隐蔽的点KV Cache 的 dtype。vLLM 支持--kv-cache-dtype fp8SGLang 也有类似选项。默认情况下两者可能不一致KV Cache 精度直接影响显存占用和吞吐。对比时要么都开、要么都关。2.3 请求层到达模式比请求数量更重要这是最容易被低估的一环。很多人压测就是“发 1000 个请求看总耗时”但并发实验的核心是到达模式。常见的三种到达模式特点适用场景闭环closed-loop固定并发数一个请求返回再发下一个模拟稳定负载下的极限吞吐开环open-loop按固定速率发请求不管前一个是否返回模拟真实流量测延迟分布突发burst瞬间打入大量请求测调度器抗压和队列行为vLLM 和 SGLang 在开环模式下的表现差异往往比闭环模式大得多因为开环会真正考验调度队列。如果你的实验只跑闭环得出的“SGLang 比 vLLM 快 X%”结论可能完全不适用于线上。请求本身的参数也要固定输入长度prompt tokens、输出长度max_tokens、是否流式、temperature 是否设为 0贪心解码。输出长度不固定是延迟数据不可复现的头号元凶——模型提前遇到 EOS 就停了每个请求实际生成的 token 数不一样P99 自然飘。3. vLLM 与 SGLang 的调度差异数字背后的机制把变量钉死之后还得理解为什么两个引擎会跑出不同的数字。不理解机制数字就是黑盒出了问题也不知道往哪查。3.1 PagedAttention 与 RadixAttention 的分野vLLM 的核心是PagedAttention把 KV Cache 切成固定大小的 block 来管理类似操作系统的分页。好处是显存碎片少、并发请求多的时候不容易 OOM。它的调度器以“批”为单位连续批处理continuous batching让新请求能插进正在运行的批次里。SGLang 的核心是RadixAttention用基数树radix tree来组织 KV Cache重点优化前缀共享。如果多个请求有相同的前缀比如同一个 system prompt、few-shot 示例SGLang 能直接复用那部分 KV不用重复计算。这在多轮对话、批量同模板任务里优势非常明显。这个差异直接决定了实验设计如果你的请求之间前缀重复度高SGLang 会显著占优如果每个请求都是完全独立的长 prompt两者差距会缩小。所以做对比实验时必须明确请求集的前缀重复特征否则结论没有普适性。3.2 无 NVLink 环境下差异会被放大热词里有个问题很典型“sglang 无 nvlink 影响多大”。在多卡 TP 场景下卡间通信带宽直接决定扩展效率。NVLink 的带宽远高于 PCIe如果机器没有 NVLinkTP 的通信开销会吃掉很大一部分收益。对 vLLM 和 SGLang 来说无 NVLink 的影响程度不完全一样因为两者的通信模式有差异vLLM 的 TP 通信主要在 AllReduce每层都要同步SGLang 在 RadixAttention 下如果前缀共享做得好某些计算可以避免通信压力分布不同。但总体规律是无 NVLink 时TP 越大两个引擎的吞吐都会明显下降且下降幅度随 TP 增大而增大。所以做并发实验时如果测试机没有 NVLink一定要在报告里注明并且把 TP1 和 TP2 的结果分开列。拿无 NVLink 的 TP4 数字去跟别人有 NVLink 的 TP4 数字比是没有意义的。3.3 调度队列延迟毛刺的来源两个引擎都有请求队列当并发超过处理能力时请求会在队列里排队。队列行为决定了 P99 延迟vLLM 的调度器倾向于优先填满批次在队列积压时会尽量多塞请求吞吐优先。SGLang 在调度时会考虑前缀匹配可能为了复用 KV 而调整请求顺序这在某些负载下会带来额外的排队延迟。实测中我观察到在开环高并发下vLLM 的 P50 延迟通常更低但 P99 可能更高SGLang 的延迟分布有时更集中。这不是绝对的取决于请求集特征。但理解这个机制后你就知道为什么不能只看平均延迟——平均值会把毛刺抹平而线上用户感受到的恰恰是毛刺。4. 一套能跑出可解释数字的实验流程前面讲了变量和机制现在给一套完整流程。这套流程我在多个项目里用过别人拿去照着跑基本能复现出同量级的数字。4.1 压测工具的选择与配置工具选型上我推荐vLLM 自带的 benchmark 脚本和SGLang 的 bench 工具作为起点因为它们对各自引擎的接口适配最好。但如果要做严格对比最好用同一个客户端比如基于 OpenAI 兼容接口写一个统一的压测脚本这样客户端开销一致。关键配置项# 统一压测客户端的核心参数 config { num_requests: 500, # 总请求数 concurrency: 64, # 并发数闭环 request_rate: 20, # 每秒请求数开环 prompt_len: 512, # 固定输入长度 max_tokens: 256, # 固定输出长度 temperature: 0, # 贪心解码保证输出确定 stream: True, # 流式便于测 TTFT }注意max_tokens固定不代表实际输出长度固定。如果模型在 256 之前就输出 EOS实际长度会短。要保证输出长度一致可以在 prompt 里明确要求“输出恰好 N 个 token”或者用 ignore_eos 参数强制生成到 max_tokens。后者更可靠。4.2 指标定义TTFT、TPOT、吞吐到底怎么算指标定义不统一是数字不可解释的另一个大坑。我建议至少报告这四个TTFTTime To First Token从请求发出到收到第一个 token 的时间。反映 prefill 阶段性能受 prompt 长度和调度延迟影响。TPOTTime Per Output Token首 token 之后平均每个输出 token 的耗时。反映 decode 阶段性能。吞吐tokens/s总生成 token 数除以总耗时。注意是“生成 token”还是“输入输出 token”要写清楚。P99 延迟端到端延迟的 99 分位。这个数字最能反映用户体验。计算 TPOT 时有个细节如果流式返回TPOT (总延迟 - TTFT) / (输出 token 数 - 1)。分母减 1 是因为第一个 token 已经算在 TTFT 里了。这个细节很多人搞错导致 TPOT 数字对不上。4.3 预热与稳态判定首次运行包含编译和显存分配数字不可用。我的做法是先跑一轮完整的预热相同配置丢弃结果再跑正式轮。预热轮的作用是让算子编译完成、显存池稳定。稳态判定可以看吞吐曲线如果连续几个采样窗口的吞吐波动小于 5%就认为进入稳态。采样窗口建议 10 秒一个点。如果一直波动很大说明系统没稳定可能是显存碎片或调度抖动这时候的数字不能用来做结论。4.4 结果记录模板每次实验存一份结构化记录字段包括环境快照、模型信息、引擎版本、请求配置、四个核心指标、以及备注比如“本轮观察到 GPU 降频”。用 JSON 或 CSV 都行关键是字段固定方便后续横向对比。5. 那些让数字失真的坑我一个个踩过前面是方法论这一节讲具体的坑。这些都是我在实际对比 vLLM 和 SGLang 时真实遇到的写出来帮你省时间。5.1 安装 vLLM 把 torch 换掉了热词里“安装 vllm 会改变已经安装好的 torch”是真实存在的。vLLM 的依赖声明里对 torch 版本有要求pip 解析依赖时可能升级或降级你现有的 torch。后果是你之前基于旧 torch 写的自定义算子、编译好的扩展可能全部失效。规避方法永远在独立虚拟环境里装 vLLM 和 SGLang。如果必须在同一环境装之前记录 torch 版本装之后检查是否变化变了就评估影响。我现在的习惯是每个引擎一个 conda env互不干扰。5.2 并发数设成一样但实际并发不一样闭环模式下你设concurrency64意思是同时最多 64 个请求在飞。但两个引擎的客户端实现可能不同一个用线程池、一个用 asyncio实际并发数可能有偏差。更隐蔽的是如果某个引擎处理慢请求返回慢实际在飞的请求数会低于设定值。验证方法在压测过程中采样服务端的“运行中请求数”指标。vLLM 和 SGLang 都暴露了 metrics 接口可以拉num_requests_running。如果这个数字跟你的设定差很多说明并发没控住。5.3 输出长度不一致导致吞吐虚高前面提过但值得再强调。如果 A 引擎的请求平均输出 200 tokenB 引擎平均输出 180 token那 B 的“每秒请求数”会更高但“每秒 token 数”可能更低。只看请求吞吐会得出错误结论。解决强制固定输出长度ignore_eos或者报告时同时给出平均输出长度让读者自己判断。5.4 显存碎片在长时间运行后显现短时间压测几分钟看不出显存碎片问题但线上服务是长时间运行的。我做过一个 2 小时的持续压测发现 vLLM 在运行 40 分钟后吞吐有轻微下降怀疑是显存碎片累积。SGLang 的 RadixAttention 在长时间运行下如果前缀缓存不断增长也可能触发淘汰逻辑带来延迟抖动。所以如果条件允许做一轮长时间稳定性测试哪怕只跑 30 分钟也能发现短测发现不了的问题。5.5 把冷启动数字当稳态数字这个坑太常见了。第一次请求触发编译TTFT 可能是稳态的 10 倍。如果压测请求数少比如 100 个冷启动的影响会拉高整体平均延迟。一定要预热且预热请求数要足够覆盖编译过程。6. 让结论站得住的报告写法实验做完怎么写结论也有讲究。我见过太多报告写“SGLang 比 vLLM 快 30%”然后被问“什么条件下”就答不上来。6.1 结论必须带条件任何性能结论都要带上前提模型是什么、精度是什么、TP 多少、请求集特征是什么、到达模式是什么。正确的写法是“在 Qwen 系列模型、BF16、TP1、prompt 长度 512、输出长度 256、开环 20 req/s 的条件下SGLang 的吞吐比 vLLM 高约 X%P99 延迟低约 Y%。”6.2 区分“统计显著”和“工程显著”两个引擎差 2%在多次重复实验下可能是统计显著的但工程上没意义——部署复杂度、生态成熟度、运维成本可能更重要。报告里要区分这两类差异别让读者被小数字带偏。6.3 给出复现路径好的报告会附上环境快照、压测脚本、请求集生成方式、原始数据。别人能照着复现你的结论才有说服力。我现在习惯把实验目录打包存档包含env_snapshot.sh、bench.py、results.json、README.mdREADME 里写清楚每一步怎么跑。7. 关于选型我自己的判断逻辑最后说点个人经验。vLLM 和 SGLang 不是非此即彼的关系选哪个取决于你的负载特征。如果你的场景是多轮对话、批量同模板任务、前缀重复度高SGLang 的 RadixAttention 能带来实打实的收益值得优先考虑。如果你的场景是请求高度异构、前缀各不相同、追求生态和社区支持vLLM 的成熟度和文档完善度更让人放心。无 NVLink 的机器上两者都要谨慎用大 TP因为通信开销会吃掉大部分并行收益。单卡能跑就单卡跑不动再考虑 TP2TP4 以上在无 NVLink 环境下要非常谨慎。还有一点版本迭代很快。vLLM 和 SGLang 都在高频更新这个月的数据下个月可能就变了。所以性能结论要带版本号并且定期复测。我一般每个季度重跑一次基线看看有没有回归。这套流程跑下来你的并发实验数字就不再是“玄学”而是有环境、有机制、有复现路径的工程结论。别人质疑的时候你能一条条对回去这才是性能测试该有的样子。