尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

SGLang Omni 压测实战:Batch Size 与吞吐延迟的调度取舍

SGLang Omni 压测实战:Batch Size 与吞吐延迟的调度取舍 上周五下午我和同事在 SGLang Omni 多模态服务的压测上吵了一架。他坚持把 batch size 拉到 256理由是吞吐比 16 时翻了快三倍GPU 利用率也好看我坚持只敢开到 32因为线上 P99 延迟已经飘到不可接受。吵到最后我们各自甩出一份压测报告谁也说服不了谁——后来仔细复盘才发现我俩说的根本不是同一件事。这篇文章想借这次争论把 Batch Size、SGLang Omni、性能验证、调度取舍这几件事串起来讲透为什么并发一高反而变慢为什么同一个 batch 参数在不同人手里测出天壤之别以及我后来沉淀下来的一套可复用的压测与调参顺序。适合正在做 LLM 服务部署、多模态推理优化或者被吞吐 vs 延迟撕扯过的平台同学参考。1. 被吵翻天的那个下午Batch Size 之争到底在争什么1.1 两种立场两套实验两个结论同事的实验方法很粗暴用压测脚本固定发送 9000 条请求把并发数从 16 调到 256统计整段时间里模型吐出的总 token 数除以总耗时。结论是吞吐从 400 tokens/s 涨到 1100 tokens/s所以他觉得越大越快不上 256 就是浪费 GPU。我这边盯的是用户真实体验。把 batch 从 32 拉到 128 以后首 token 延迟TTFT的 P99 从 0.9 秒飙到 4.3 秒每 token 生成间隔TPOT也跟着恶化。站在线上服务角度看这是不可接受的体验降级我当然反对。问题在于他只看平均吞吐我只盯尾延迟他的请求里纯文本占大多数我的流量里有大量图文混排他只跑 3 分钟我跑了 30 分钟。两者测的根本不是同一个系统状态。1.2 争论为什么容易无解因为 batch size 在 SGLang Omni 这类连续批处理continuous batching架构里不是一个孤立旋钮。它同时影响 prefill 阶段的批大小、decode 阶段的并行度、KV cache 的占用水位、以及调度器每次挑选请求时的决策空间。你调高一个参数背后至少四五个环节跟着变只用一个吞吐指标打包判断必然产生分歧。另外多模态场景比纯文本更复杂。一条图文请求里图像 encoder 要先产出 embedding再拼进文本序列走 prefill一条带视频的请求更是可能产生几千个视觉 token。这些环节的最优 batch和纯文本 decode 的最优 batch完全可能是两个数。所以任何一方只要拿着单一指标下结论都会掉进同一个坑。我现在遇到类似的争论第一反应不是站队而是先问你们两边的输入分布、观测指标、时间窗口到底对齐了没有2. Batch Size 在 SGLang Omni 里到底作用于哪几个环节2.1 先建立 SGLang Omni 的心智模型SGLang Omni 的本质是 SGLang 框架在多模态方向上的服务能力输入不再只是文本还可以是图像、音频、视频。它的核心组件包括多模态输入编码器、RadixAttention 前缀缓存、连续批处理调度器、分块预填充chunked prefill和抢占机制。理解调度取舍必须理解一个前提连续批处理没有传统静态批处理里那种固定的 batch size。静态批处理是攒满 N 条请求才一起跑跑完再一起退出连续批处理则是新请求随时可以加入当前运行批次已完成的请求立刻退出GPU 流水线尽量不空转。所以你在 SGLang Omni 里真正调的不是batch_size64这样的参数而是几个间接控制并发规模的启动参数。我在实战里最常改的是这三个参数作用调大后会发生什么--max-running-requests限制同时处于 decode 状态的请求数越接近显存/算力上限吞吐增加但延迟风险上升--mem-fraction-static控制 KV cache 池占用显存比例池子越大能驻留更多前缀但留给计算的空间变小--chunked-prefill-size切分长 prompt 预填充的块大小块越大 prefill 越集中块越小越能穿插 decode2.2 Batch Size 实际作用在四个不同环节prefill 阶段。新请求进入后需要把输入 prompt 转成 KV cache多模态场景还要先用 vision/audio encoder 把非文本输入编码成 embedding。这个阶段是内存带宽密集型图像 token 越多成本越高。chunked prefill 会把长 prompt 切成小块避免一次塞爆 GPU。decode 阶段。这是自回归生成的主战场每生成一个 tokenGPU 要遍历当前批次所有序列的 KV cache。这里 batch 太大单个 decode step 的时间会显著变长TPOT 跟着恶化。KV cache 池。SGLang 用 RadixAttention 管理 KV cache是一个 radix 树结构相同前缀可以跨请求共享。池子大小由--mem-fraction-static决定。请求来得越多前缀冲突概率越高缓存被逐出的速度也越快。多模态 encoder 阶段。这是 SGLang Omni 特有的环节。vision encoder 处理一批图像时不会和文本 decode 抢同一个计算流但会抢 GPU 资源。如果 encoder 批处理量过大会对正常的文本生成产生周期性阻塞。这四个环节各自的最优并发数完全不一样。prefill 希望吞吐大decode 希望单步延迟低KV cache 希望复用率高encoder 希望别堵住主链路。单一 batch size 能让四个环节同时最优基本不可能。3. 一次可信的性能验证要盯哪些指标我的实验清单3.1 指标选型别再用单个吞吐数下结论我后来给自己的压测定了一套必看指标缺一个都不允许写结论TTFTTime To First TokenP50 和 P99 都看。P99 直接决定用户体验是否可接受。TPOT / ITL每 token 生成间隔。这个指标比总吞吐更能反映 decode 阶段的健康度。有效吞吐Goodput在约定 SLA 内完成的请求占比。比如 TTFT 小于 1 秒、TPOT 小于 100ms 的请求占多少百分比。这个指标可以同时约束吞吐和延迟。前缀缓存命中率SGLang Omni 的 RadixAttention 是否在真正工作。命中率低意味着大量请求在重复 prefill 相同前缀GPU 在做无用功。抢占次数preemption count调度器把已有请求的 KV cache 换出或重算的次数。抢占风暴是性能劣化的典型信号。显存峰值与 KV cache 利用率看是否真的缺显存还是只是 KV 池规划不合理。3.2 怎么构造一份不会骗人的测试负载我发现很大一部分 batch size 争论根源是负载构造不一样。现在我压测时会固定以下几件事输入分布明确纯文本、图文混排、音视频请求的比例。多模态请求占比一旦超过某个阈值batch 上限就得往下压。请求长度分布prompt 长度和输出长度分开配置不能全用短 prompt 跑。长 prompt 会放大 prefill 成本和缓存压力。到达模式恒定速率模拟平稳流量泊松到达模拟有突发性的真实流量。真实线上一阵一阵的突刺比恒定负载更能暴露问题。预热至少预热 20 到 30 个批次跳过前 100 条请求。CUDA kernel 和缓存状态稳定了再记录数据。运行时长每个配置跑 15 到 30 分钟。3 分钟能看到的问题太有限缓存碎片化和抢占风暴通常都是后面才出现。3.3 我的一次完整实测过程以 8 卡 A100、SGLang Omni 服务部署完毕为例。我会先起一个干净的压测环境确认没有其他实验在占用 GPU。然后针对某个 batch 上限跑这样一轮# 先看服务指标端点确认当前请求队列和缓存状态 curl -s http://127.0.0.1:30000/metrics | grep -E sglang:(num_running_reqs|num_waiting_reqs|gpu_cache_usage_percent|prefix_cache_hit_rate) # 固定输入分布文件使用压测工具发请求 python3 benchmark.py \ --requests-file ./dataset.jsonl \ --concurrency 64 \ --duration 1800 \ --output ./result_batch64.json跑完以后我会把结果和指标端点的记录放在同一张表里。只看吞吐不看缓存命中率和抢占次数的压测我说句难听的基本是自嗨。3.4 几个特别容易误导人的坑第一平均值会骗人。一次压测平均延迟 90ms看起来很好但 P99 可能已经冲到 800ms。线上只要出现 1% 的超时用户体验就崩了。第二缓存命中率的差异会让两次实验的真实工作量完全不可比。A 实验缓存命中率 80%B 实验命中率 30%即使两者表面吞吐一样B 实际做的 prefill 计算量要大得多。不看缓存命中率你会误以为两个 batch 参数表现相同。第三压测时间过短会漏掉隐性故障。SGLang 的抢占机制在压力持续一段时间后才会频繁触发5 分钟以内基本看不到30 分钟后才现出原形。我在实测中最常用的一招把服务指标端点的sglang:num_waiting_reqs拉出来看。如果等待队列长时间不为 0说明调度器已经处理不过来了这时候加大 batch 只会让队列更长而不是让吞吐更高。4. 为什么加大 Batch Size 之后反而变慢了调度取舍的真相4.1 我机器上的实验结果在我自己的压测环境里固定输入分布和到达模式只调--max-running-requests得到过这么一组典型数据运行请求上限平均吞吐 (tokens/s)TTFT P99 (s)TPOT P99 (ms)缓存命中率抢占次数163980.74591%8327620.95887%236410101.98474%9712810954.113255%341注意第 128 档吞吐只比 64 档多了不到 10%但 TTFT P99 翻了一倍多抢占次数从 97 跳到 341。这就是典型的大 batch 负收益区间——表面吞吐还在涨实际上系统已经开始通过抢占和重算来硬撑了。4.2 大 batch 如何一步步拖垮延迟首先是 decode step 变慢。batch 越大单步计算中要遍历的 KV cache 越多GPU 每个 step 耗时线性增长所有在跑请求的 TPOT 一起遭殃。这一点是最直观的。然后是 prefill 挤压。新请求不断进入prefill 阶段需要大量算力。如果 batch 拉得很大调度器会把大量不同前缀的请求同时拉进 running 集合radix 树里的公共前缀被各种长尾请求冲散缓存命中率断崖式下跌。结果就是所有请求都要从头 prefillGPU 开始反复处理实际上可以复用的前缀。最后是抢占风暴。显存不会无限增长KV cache 池大小是固定的。当同时运行的请求太多池子放不下时SGLang 只能选择抢占preemption。抢占有两种一种是 swap把某些序列的 KV cache 换到 CPU 内存有空位了再换回来另一种是 recompute直接丢掉 KV cache等资源足够时重新计算。无论哪一种都是在让 GPU 做无用功。一个比较贴切的生活类比批处理调度就像厨房出餐。batch size 不是菜单上印了多少道菜而是同一时间放进厨房的顾客数。厨师GPU的灶台有限你让 20 个人同时挤进厨房每个人反而都要等更久。你看到厨房的灯全亮着GPU 利用率高但出餐速度并没有变快顾客等位时间却长了。4.3 多模态场景让问题更复杂SGLang Omni 比纯文本场景多了一个变量vision encoder 和 audio encoder 的批处理。图文请求进来图像要先经过 encoder 生成 embedding这个阶段占显存、占算力但和文本 decode 的步调完全不同步。我有一次压测视频理解场景发现吞吐波动特别大把指标端点的数据拉出来才看懂encoder 每处理完一批视频帧GPU 要集中做一大波视觉 token 的 prefill这时候文本 decode 全部卡住等视觉部分处理完文本生成才继续。batch 越大这种抽风式的步进就越明显。4.4 调度策略在这里的角色SGLang 的调度器并不是简单地来多少处理多少。它有多种调度策略常见的有 FCFS先来先服务和 LPM最长前缀匹配longest-prefix-match优先选择缓存命中率高的请求。LPM 策略的设计意图很明确让共享前缀多的请求尽可能一起跑最大化 RadixAttention 收益。但当 batch 容量上限设得过大调度器为了填满容量会不断把新请求加入 running 集合原本稳定的前缀复用结构被打散LPM 的贪心优势就发挥不出来了。这就是我在实验里看到缓存命中率从 87% 掉到 55% 的深层原因。不是 RadixAttention 不好是 batch 容量这个约束条件把调度器的决策空间逼到了墙角。5. 从争论到共识我沉淀的调参与验证顺序5.1 先定 SLA再谈 Batch Size那次争吵过后我和同事约定了一套统一的调参流程现在团队做推理服务优化都走这个路径先定 SLA明确业务可接受的 TTFT 和 TPOT 上限。比如TTFT P95 小于 1 秒TPOT P95 小于 100ms。没有 SLA任何指标争论都是鸡同鸭讲。固定输入分布确认请求里多模态占比、prompt 长度分布、输出长度分布写进压测脚本作为不可变前置条件。低并发测基线先把--max-running-requests调到很低比如 8 或 16测出单请求延迟的底线确认模型算力本身没有瓶颈。逐步抬高并发按 16、32、64、128 的梯度递增每一档稳定跑 15 到 30 分钟记录吞吐、TTFT、TPOT、缓存命中率、抢占次数。找拐点把数据拉成表格找到吞吐不再明显增长但延迟和抢占开始恶化的那个档位这就是当前硬件和负载下的合理上限。真实流量回归用泊松到达模式模拟线上突刺跑至少 1 小时确认不会在持续压力下触发抢占风暴。5.2 我在启动服务时的具体做法服务启动阶段我会先把调度策略和资源占用参数按保守值写好python3 -m sglang.launch_server \ --model-path /models/Qwen-VL-Chat \ --host 0.0.0.0 \ --port 30000 \ --schedule-policy lpm \ --max-running-requests 32 \ --mem-fraction-static 0.8 \ --chunked-prefill-size 8192--schedule-policy lpm让调度器优先复用前缀缓存--max-running-requests先压到 32后面根据压测数据再放开。这里我的经验是起步宁小勿大先把延迟基线保住再逐步找到吞吐边界。注意具体参数名和默认值会随 SGLang 版本变化部署前最好先看一眼当前版本的启动帮助和指标端点输出我上面只是通用示例。5.3 团队关于性能报告的口径约束为了让团队不再发生我说吞吐高、你说延迟差的争论我们定义了一个性能报告必须包含的字段模型路径与 SGLang 版本GPU 型号与并行配置输入分布纯文本/图文/音频视频比例prompt 长度范围输出长度范围到达模式恒定速率还是泊松到达并发数运行窗口预热量正式测试时长关键指标平均吞吐、TTFT P50/P99、TPOT P50/P99、Goodput、缓存命中率、抢占次数、显存峰值谁的报告缺字段谁就没有资格下结论。这个约定执行之后团队内部的性能争论明显少了很多。5.4 那次争论留给我的最大收获现在回过头看那次 Batch Size 争论真正缺的不是更多实验而是一套共同的验证口径和判断框架。batch 参数本身没有绝对的好坏它是在目标 SLA 内最大化有效吞吐的约束条件。同一个模型、同一批硬件如果业务方对延迟不敏感128 可能是合适的如果面向交互式应用32 可能都已经偏大。我现在的习惯是任何性能讨论先问三个问题——你的 SLA 是多少你的缓存命中率是多少你的抢占次数是多少这三个数字对齐了再讨论 Batch Size 才有意义。
返回列表