从一次 Batch Size 争论,思考 SGLang Omni 的性能验证与调度取舍
事情起因是团队里一次例行性能评审,两个同学为一个数字吵得不可开交。A说 Batch Size 开到 128 吞吐最高,B说开 64 延迟更稳,各自都贴了压测数据,看起来谁都没错。争论到最后变成互相质疑“你的压测环境不干净”“你的指标口径不对”。我在旁边听着,发现问题的焦点根本不是 Batch Size 本身,而是大家在使用 SGLang Omni 这类推理框架时,普遍缺少一套统一的性能验证方法和调度权衡意识。这个场景太典型了,值得单独拿出来聊聊。
先说结论:Batch Size 之争,本质上不是“哪个数值更好”的问题,而是“你在什么约束条件下、用什么指标、验证什么业务目标”的问题。SGLang Omni 作为目前多模态大模型推理里绕不开的框架,它的性能表现高度依赖调度策略、显存管理和前缀复用机制,Batch Size 只是中间变量,不是根因。这篇文章我会从那次争论出发,系统拆解 Batch Size 的底层原理、SGLang Omni 的特性机制、性能验证的完整方法论,以及实际调度过程中的取舍依据。内容偏实战,适合正在做 LLM/多模态推理服务性能调优的工程师,也适合刚上手 SGLang 生态、想搞清楚“为什么我压测的数据和别人不一样”的读者。
1. 内容整体设计与思路拆解
1.1 从一次争论看性能验证的本质问题
那次争论发生在一次模型服务升级评审会上。团队正在把一个视觉-语言模型(VLM)服务从自研推理脚本迁到 SGLang Omni 上,目标很简单:支撑更高的并发请求,同时把 P99 延迟控制在可接受范围。A 同学负责压测,他在固定的 A100 单卡环境下,用不同 Batch Size 跑了离线压测脚本,结论是 Batch Size=128 时吞吐最高,接近 1800 tokens/s。B 同学负责线上容量预估,他坚持认为 Batch Size=64 更符合线上真实场景,理由是真实请求的输入长度波动很大,128 会导致频繁的显存溢出和排队抖动。
我后来把两人的压测脚本和数据调出来做了对比,发现几个关键差异:A 用的是固定 512 token 的合成输入,B 用了一组采样的真实线上请求分布;A 统计的是纯 decode 阶段的吞吐,B 统计的是端到端(含 prefill)的 P99 延迟;A 没有开 RadixAttention 前缀缓存,B 默认开了。这几项差异叠加在一起,数据当然对不上。
注意:性能验证最怕的就是“控制变量没做干净”。Batch Size、输入长度分布、前缀复用率、并发数、是否开启 chunked prefill,这些因素相互耦合,单独讨论其中一个都是片面的。
所以我说,那场争论表面上是 Batch Size 的数字之争,实际暴露的是性能验证方法论的缺失。在进入具体调度策略之前,先把验证框架立起来比什么都重要。
1.2 SGLang Omni 的设计目标与适用场景
SGLang Omni 是 SGLang 生态面向多模态模型的推理引擎实现。它的核心目标不是简单地“把模型跑起来”,而是在高并发、多模态输入(文本+图像+音频等)混合的场景下,通过高效的调度和内存管理,把 GPU 利用率推到接近极限。相比 vLLM、TGI 等框架,SGLang 系最大的差异来自 RadixAttention 技术——它可以自动复用请求之间的公共前缀(包括系统提示词、少样本示例、多轮对话历史等),从而显著减少重复的 prefill 计算。
这对多模态场景尤其重要。图像 token 动辄几百上千,如果每个请求都重新计算图像部分的 KV Cache,显存和时间开销都是灾难。SGLang Omni 利用树状结构管理前缀,让不同请求共享已计算的 KV Cache,在这类场景下收益非常可观。我自己实测过,在图文混合请求占比高的服务中,RadixAttention 能让整体吞吐提升 2-3 倍,这比单纯调 Batch Size 带来的收益大得多。
Batch Size 在这个框架里的角色也需要重新定义。传统推理脚本中 Batch Size 是静态配置,但在 SGLang Omni 中,它更像是一个“目标水位”——框架会在显存允许的前提下,尽量把并发请求聚合到指定 Batch Size 附近,并结合 continuous batching(连续批处理)动态插拔请求。所以你在 SGLang 中设置 Batch Size,本质上是在声明一个调度目标,而不是一个硬性上限。
1.3 调度取舍的分析框架
提到调度就离不开取舍。SGLang Omni 的调度器在每一个 step 都要回答三个问题:这批请求谁先谁后?每个请求分多少计算资源?什么时候把请求踢出/加入 batch?
围绕这三个问题,存在三组核心矛盾:吞吐与延迟的矛盾(大 Batch 有利于吞吐,但可能恶化单请求的排队延迟);prefill 与 decode 的矛盾(两者计算特性不同,混跑时会互相拖累);显存利用率与稳定性的矛盾(开太大容易 OOM,开太小又浪费算力)。Batch Size 之争其实就是第一组矛盾的具体体现。
后面几节我会逐一展开这些矛盾,并结合 SGLang Omni 的源码行为和实测数据,给出可操作的取舍建议。理解了调度器的决策逻辑,你在定 Batch Size 时就不会再靠拍脑袋或盲目抄别人的配置。
2. 核心细节解析与实操要点
2.1 Batch Size 对吞吐和延迟的影响机制
先说基础:为什么 Batch Size 变大,吞吐会提升?GPU 的算力是并行的,单个请求往往吃不满。想象一条四车道高速公路,一辆车走只能占一个车道,效率必然低。把多个请求的路程打包在同一时间段内一起走,才能把四个车道都填满。batch 越大,车道上跑的车越多,单位时间运送的“token”总量自然越大。
但这条规律不是无限的。当 Batch Size 超过某个阈值后,吞吐增长会放缓甚至下跌,原因有几个:显存带宽成为瓶颈(所有请求都在抢 HBM 带宽);SM(流式多处理器)的占用率已经饱和,增加 batch 只增加排队不增加并行度;prefill 阶段的计算量随 batch 线性增长,而 prefill 和 decode 混跑时会相互打断。
延迟端的影响更微妙。Batch Size 增加会让单个请求的端到端延迟(尤其是 TTFT,即首 token 延迟)变长,因为请求可能在调度队列里等待“凑齐”一个更大的 batch。而 decode 阶段的 ITL(token 间延迟)也可能会变长——GPU 要轮流服务更多请求,每个请求分到的算力变少。这就是那场争论里 B 同学坚持 Batch Size=64 的核心顾虑。
我在多组实验里观察到:Batch Size 从 1 到 64,吞吐几乎是线性上升的;从 64 到 128,吞吐提升幅度下降到 15%-20% 左右;从 128 到 256,吞吐基本走平甚至略有下降,而 P99 延迟显著恶化。这说明在 A100 或 H100 这类显卡上,Batch Size=64 附近已经是一个相当合理的甜点区,盲目求大没有意义。
2.2 显存约束下的 Batch Size 上限计算
很多人不知道 Batch Size 的上限其实由显存决定,而且是可以提前算出来的。核心约束来自 KV Cache 的显存占用。对一个 transformer decoder 模型,单条序列的 KV Cache 大小可以粗略估算:
KV Cache 字节数 = 2(K 和 V 两组) × 层数 × 注意力头数 × 每头维度 × 序列长度 × 精度字节数
例如一个 7B 参数的模型,层数 32、头数 32、每头维度 128,序列长度 4096,使用 FP16(2 字节),单条完整序列的 KV Cache 大约是 2 × 32 × 32 × 128 × 4096 × 2 = 2GB。8B 模型在长度 8192 场景下单条序列能吃到 4GB 以上。一张 80GB 的 A100,光 KV Cache 一项就能限制 Batch Size 在 20-40 左右,远小于很多人以为的 128。
SGLang Omni 内部有显存池化机制,它允许请求按实际前缀长度复用缓存,所以 KV Cache 的“有效占用量”往往低于理论峰值。但这也意味着,如果你不开启前缀缓存,或你的请求没有公共前缀,那么 Batch Size 再大也会被显存硬约束压回来。
实操建议:在压测前先跑一个空转脚本,观察不同 Batch Size 和序列长度下的显存占用曲线,找到你硬件条件下的实际上限。不要只看模型参数量,KV Cache 往往才是吃显存的大头。
2.3 影响性能的关键运行参数与配置开关
SGLang Omni 的配置项里,有几个参数在 Batch Size 验证中起着决定性作用:
--max-running-requests:实际控制最大并发请求数的参数,Batch Size 的调度目标围绕它展开。建议先设一个保守值,观察显存占用和延迟表现后再逐步上调。--chunked-prefill-size:prefill 阶段每次处理的最大 token 数。开启后长 prompt 会被截断成多段处理,避免单个大请求阻塞整个 batch。这个参数对 TTFT 抖动的影响非常大。--radixattention:前端缓存开关。绝大多数场景都该开,尤其是多轮对话和带固定系统提示词的服务。--mem-fraction-static:静态显存分配比例。默认值通常偏保守,在专用推理机上可以适当调大,但要留出足够余量给动态 KV Cache。--schedule-conservativeness:调度保守程度。数值越大,调度器越倾向于等待更长的时间来凑 batch,这会提高吞吐但增加延迟。
我的经验是:先开 RadixAttention,再把 chunked prefill 设置为 512 或 1024,最后调整 max-running-requests——而不是反过来。因为前两者决定了显存和计算效率的基础水位,后者是随后调整的旋钮。
3. 实操过程与核心环节实现
3.1 搭建可复现的性能验证环境
那次争论之后,我搭了一套统一的压测环境,核心思路是让场景可复现、指标口径一致。这里直接把步骤写出来,你照着搭一遍就能避免绝大多数的“数据打架”问题。
硬件环境:单张 A100 80GB(或 H100),宿主机 CPU 核心数建议大于 32,内存大于 256GB。软件环境:CUDA 12.x,PyTorch 2.1+,SGLang 最新稳定版(按官方文档安装),Python 3.10+。统一用 Docker 容器跑,避免宿主机环境差异影响结果。
模型选择:我们当时选用的是一个开源 8B 视觉语言模型。关键点是确认模型能被 SGLang Omni 正确识别为多模态模型,并且支持图像输入。
压测脚本我用的是自研 Python 脚本加上并发请求模拟,核心逻辑是利用 asyncio 发一定量的并发请求,记录 token 数、耗时、显存峰值等数据。脚本要点是:每个请求使用相同的系统提示词(这样 RadixAttention 才有前缀可复用)、输入长度按真实分布采样、统计端到端延迟和每秒输出 token 数。
3.2 分场景压测:静态和动态 Batch Size 的对比
我拆了两个场景来测:静态 Batch 场景(关闭 continuous batching 的敏感度测试)和动态 Batch 场景(SGLang 默认的调度模式)。静态场景里,SGLang 的调度行为接近传统批处理,适合用来理解 Batch Size 的天花板;动态场景则贴近真实线上行为。
实测结果非常有意思。静态模式下,Batch Size=32 和 64 的吞吐差距不大,都在 1600-1700 tokens/s 之间,而 128 反而降到 1500 左右——这是因为显存和算力已经到顶,batch 增大带来的调度开销超过了并行收益。动态模式下,SGLang 的 continuous batching 允许 batch 在请求完成时立即补充新请求,整体吞吐可以稳定在 1800-2200 tokens/s,而且单个请求的排队时间明显更短。
这说明一个关键结论:在 SGLang Omni 中,动态调度策略比静态 Batch Size 对性能的影响更大。很多人的 Batch Size 争论其实是用静态思维思考动态系统,结论自然不成立。
对于 Batch Size 的选择,我建议把它理解为“目标水位”而不是“硬限制”。在 SGLang Omni 中通过 max-running-requests 和调度保守度参数来控制。我实测下来的推荐起点是:8B 级别模型、单卡 80G、输入长度 1024-4096,max-running-requests=48-64,开启 RadixAttention,chunked prefill=1024。这个配置在吞吐和延迟之间相对均衡。
3.3 训练到部署实战:Batch 参数调整全流程
我从项目实战里提取了一个标准的调参全流程,你可以把它当作自己的操作手册:
第一步,先测模型正确性和显存基线。启动服务后,用单请求验证输出正确,观察显存 idle 占用。第二步,用小并发(比如 8 并发)压一轮,记录 TTFT、ITL、吞吐和显存峰值,这轮数据是后面的对比基线。第三步,逐步调大并发数(16、32、48、64……),每次稳定运行 200 个请求后再记数据,观察延迟和吞吐的变化曲线。第四步,当并发数增大到显存接近上限或延迟超过业务 SLA 时,记录这个临界点,这就是你当前场景下的调度上限。第五步,在这个上限附近微调 chunked prefill 和调度保守度,找到吞吐最高且延迟可接受的配置组合。
我看到很多人跳过了第一和第二步,直接拿大并发压,结果 OOM 了也不知道是 Batch Size 还是显存碎片的问题。前两步花不了十分钟,但能帮你隔离变量的干扰。
3.4 调度策略录像分析与决策点挖掘
SGLang 的调度器是事件驱动的,每一步都有取舍。我实际抓取过 SGLang 运行时的调度日志,分析典型决策点。
Scenario 1:并发 64 个请求,其中 10 个是带图像的长输入(prefill 计算量大),其他是短文本。SGLang 的决策是把长输入的 prefill 拆成 chunks,穿插在短请求的 decode 之间,避免 10 个长输入同时进入 prefill 阶段导致 GPU 算力骤降。这种情况下 Batch Size 的意义其实已经被“计算量均衡”取代了。
Scenario 2:某条长输入的前缀已经在缓存中(之前有请求请求过相同系统提示词),SGLang 会直接复用前缀的 KV Cache,只计算增量部分。如果一个 batch 里 80% 的请求都有可复用前缀,那么实际 prefill 的计算量可以减少 60%-70%,Batch Size 的影响被大幅弱化。
Scenario 3:显存到达上限时,SGLang 会拒绝新的请求而不是让它们排队阻塞。这个行为对应的参数是 max-running-requests 和显存预留比例。如果你发现线上经常出现“请求被拒绝”的日志,不是 Batch Size 不够大,而是显存预留得太少。
从这些决策点可以看到,SGLang Omni 的调度不是简单“按 Batch Size 执行”,而是一个动态优化问题。你在配置里设的 Batch Size(或 max-running-requests)只是给调度器画了一条水位线,真正的性能表现由调度策略、缓存机制和显存状态共同决定。
4. 常见问题与排查技巧实录
4.1 Batch Size 与显存上限冲突
最常见的现象是:Batch Size 配得很大,启动后服务正常,但并发一上来就 OOM。排查思路是先看是 prefill 阶段的临时显存暴涨还是 KV Cache 累积导致的。用 nvidia-smi 盯住显存曲线,如果是锯齿状尖峰,多为 prefill 阶段的问题,调小 chunked prefill-size 即可缓解;如果是平滑上升直到 OOM,则属于 KV Cache 累积,需要调低 max-running-requests 或提高 mem-fraction-static 的预留比例。
一个小技巧:在压测脚本里加一个显存监控线程,每 100ms 采样一次,把峰值和均值都记下来。这样 OOM 之后你能具体知道是哪个环节出的问题,而不是瞎猜。
提醒:SGLang 的显存池化机制会让显存呈现“缓慢爬升、到达水位后保持”的特征。如果显存没有上升到预期水位就 OOM,优先检查 CUDA 版本和 PyTorch 的显存碎片问题,而不是盲目调 Batch Size。
4.2 延迟抖动与热点问题排查
延迟抖动是另一个高频问题。表现是:整体吞吐看着不错,但 P99 延迟偶尔飙到几秒。这通常不是因为 Batch Size 大了,而是因为少量大输入请求进入 prefill 阶段,占用了大量算力,导致其他请求的 decode 被阻塞。
排查方法:开启 SGLang 的请求级日志,按输入长度分组统计 TTFT,观察是否长输入请求出现时,其他请求的延迟就变差。如果确认是这种问题,解决方案是开 chunked prefill,并把 chunk size 调小到 512 左右;同时可以设置单独的 prefill 优先级,让长输入在调度队列里不独占资源。
4.3 前缀缓存不生效的情况
RadixAttention 不是万能的,我在实践中遇到它不生效的几种情况:请求间完全没有公共前缀(比如每次都重新生成随机系统提示词);输入经过了 tokenizer 的 padding 处理,导致 token 序列不一致;多模态输入的图像编码部分不在前缀缓存覆盖范围内。第三种尤其容易忽略——图像经过视觉编码器处理后生成的位置编码如果每次都不同,即使文本前缀相同,缓存也复用不了。
遇到这种情况,先检查同一系统提示词下两次请求的缓存命中日志。SGLang 有 API 可以查询缓存命中率,如果命中率异常低,优先检查 prompt 的构造方式,确保系统提示词和其他固定文本完全一致。实际项目里,很多“Batch Size 开大反而更慢”的现象,其实都是缓存没有生效,导致每次请求都在重复计算 prefill。
4.4 压测数据的可信度检查
最后给压测数据做个体检。我判断一份压测数据可不可信,会检查几个方面:是否说明了请求长度的分布区间(固定长度和真实分布的数据完全不可比);是否区分了 prefill 和 decode 阶段(端到端吞吐和 decode 吞吐是不同的指标);是否记录了显存曲线(只有 token 数的数据没有显存作为佐证,无法判断是否触顶);是否说明 RadixAttention 的前缀命中率(这不影响正确性,但极大影响性能结论)。
如果你拿到的压测报告没有这几项信息,那它只能作为方向性参考,不能作为决策依据。这是我们团队那次 Batch Size 争论最终达成一致的检查清单,现在也成了组里的默认规范。
5. 后续可以做的扩展与优化
5.1 高级调度策略和贪婪 Batch 的取舍
SGLang 社区在持续演进调度策略。除了基础的连续批处理,新的请求优先级队列、基于延迟预测的调度等方向也在探索中。从 Batch Size 扩展到调度策略层面时,核心思想是:在保证每个请求“可接受延迟”的前提下,尽量提高 GPU 的有效利用率。贪婪 Batch ——即只要有空位就立即补充请求——是提高吞吐的有效手段,但遇到长输入突发容易造成延迟尖刺。实践建议是开启调度保守度的自适应调节,或利用动态优先级队列隔离重量级请求。
5.2 结合 Prefix Cache 和抢占机制进一步优化调度
调度优化到深水区后,Prefix Cache 和抢占机制的组合是绕不开的话题。当显存吃紧时,调度器可以“踢出”一个前缀可复用的请求的 decode 状态,优先让新来的长请求利用缓存做 prefill,从而降低整体计算量。SGLang 对长文场景下的这种控制能力比大多数框架要强。在超长上下文的场景里,抢占机制配合 Prefix Cache,在实践中能把有效吞吐再翻一倍。
如果你要在这个方向上深入,建议关注 SGLang 的日志指标里缓存命中率和抢占次数两个字段,它们比单纯看吞吐更早揭示调度质量的变化。
5.3 多卡和多机场景下的负载均衡注意点
前面所有讨论都基于单卡场景。多卡和多机部署时,Batch Size 的调度问题又会升级——涉及张量并行、数据并行和跨节点的负载均衡。SGLang Omni 支持多卡推理,但跨节点通信会成为新瓶颈。此时 Batch Size 的意义会进一步让渡给“全局 batch 分配策略”:每个节点维持多少并发请求、请求如何路由到具体卡。
经验是:多卡场景下优先考虑数据并行而非张量并行,除非你的单张卡放不下模型。数据并行可以让多卡各自独立调度 Batch,天然避免跨节点通信的开销。张量并行虽然能提升单请求的吞吐,但会让所有卡共同服务于同一个请求,Batch Size 的提升空间反而受限。
我在实际部署中,多卡系统的性能瓶颈经常不在算力而在 PCIe 带宽和 NCCL 通信延迟。所以增卡不等于线性加速,先测一下不同 batch 下的 GPU 间通信占比,再决定要不要继续扩 batch。
5.4 长期运营中的监控与调优闭环
性能调优不是一次性工作,而是需要持续监控和迭代的闭环。我建议在推理服务中接入指标监控(Prometheus + Grafana),重点采集这几个指标:请求级 TTFT 和 ITL 分布、缓存命中率、显存使用率、GPU 利用率、调度排队长度。每周回溯一次这些指标,你会清晰地看到流量变化对调度策略的影响。
一个实际例子:我们上线一段时间后,发现某个时段的缓存命中率从 60% 掉到 20%,一查发现是产品侧改了系统提示词模板。这种变化如果不通过指标监控看出来,线上性能会悄然恶化,而大家还在争论“是不是 Batch Size 该调了”。
所以最终的体会是:Batch Size 只是调度方程里的一个变量,而 SGLang Omni 的调度器、缓存机制和显存池化共同构成了真正决定推理性能的系统。回到最初那场争论,现在我们的团队已经很少有人单纯说“把 Batch Size 调到多少了”,而是统一表达为“在什么前缀命中率、什么并发水位、什么延迟约束下,我们选择怎样的调度配置”。这套语言的变化,比任何单一调参技巧都更有价值。如果你正在搭建或优化推理服务,我建议把建立可信的性能验证体系放在第一位,把精力投入到理解框架的调度机制上——这些能力会让你在遇到类似争论时,不用站队,而是直接给出可复现的实验设计和清晰的技术结论。