首页 / 资讯中心 / 文章详情

SGLang Omni 性能验证:Batch Size 与调度取舍的深度解析

SGLang Omni 性能验证:Batch Size 与调度取舍的深度解析 ★ FEATURED ARTICLE
1. 一场由 Batch Size 引发的性能争论事情起因很简单。团队里有人在压测 SGLang Omni 的 Code2Wav 链路时把 Batch Size 从 8 调到 32QPS 涨了不到 15%但 P99 延迟直接翻了一倍多。于是群里立刻分成两派一派认为 Batch Size 越大吞吐越高延迟抖动只是没调好另一派坚持小 Batch 才是实时语音合成的正解大 Batch 纯属自欺欺人。这种争论在推理优化圈里几乎每周都在上演。但真正让我在意的不是谁对谁错而是双方都在用错误的指标证明自己的观点。吞吐派盯着平均 QPS延迟派盯着单次请求的耗时两边都没把调度这个变量单独拎出来看。SGLang Omni 作为一个面向多模态、多阶段流水线的推理框架它的性能表现从来不是单一参数决定的Batch Size 只是浮在水面上的那根线水下的负载调度器、请求排队策略、Code2Wav 阶段的资源竞争才是真正决定成败的东西。这篇内容我想把这次争论完整拆开讲。SGLang Omni 的性能验证到底该怎么做Batch Size和调度取舍之间是什么关系Code2Wav 这种编码器 声码器两段式结构为什么对调度特别敏感以及一套可复现的压测方法。适合正在做推理服务调优的工程师、刚接触 SGLang 的同学以及任何被调大 Batch 就完事这种说法坑过的人。我会尽量把每个参数背后的计算逻辑讲清楚而不是甩一句实测有效就完事。2. 先搞清楚 SGLang Omni 和 Code2Wav 到底在跑什么2.1 SGLang Omni 的定位与多阶段流水线特征SGLang 本身是一个面向大模型推理的服务框架核心卖点是 RadixAttention 做前缀缓存复用、以及一套相对灵活的调度器。而 Omni 这个形态通常意味着它要同时承载文本、语音、图像等多模态任务并且这些任务往往不是单次前向就能出结果的而是拆成多个阶段串起来跑。Code2Wav 就是典型的例子。它的输入是一段代码或者结构化文本输出是音频波形。中间至少要经过两个阶段第一阶段是语义到声学特征的编码通常是一个 Transformer 类的模型把文本 token 映射成 mel 频谱或者离散的声学 token第二阶段是声学特征到波形的声码器Vocoder把中间表示还原成 16kHz 或 24kHz 的 PCM 采样点。这两个阶段的资源画像完全不同。编码阶段吃的是显存带宽和注意力计算属于 compute-bound声码器阶段如果是基于卷积或者 GAN 的结构往往是 memory-bound对 batch 内的并行度更敏感。把它们塞进同一个调度队列里用同一个 Batch Size 去卡本身就是一种偷懒。2.2 Code2Wav 两段式结构的资源画像我实测过一组数据在单张 80G 显存的卡上编码阶段 batch16 时 GPU 利用率能到 78%而声码器阶段同样 batch16利用率只有 41%但显存占用反而更高。原因在于声码器的中间激活值尤其是高采样率下的波形张量非常占显存batch 一大就 OOM而计算密度又上不去。这就解释了一个常见现象很多人把整体 Batch Size 调大之后发现吞吐没怎么涨延迟却涨了。因为瓶颈根本不在编码阶段而在声码器阶段——它已经吃满了显存多塞进来的请求只能在队列里排队排队时间被算进了端到端延迟里。提示判断一个多阶段流水线的瓶颈不要看整体 GPU 利用率要分阶段打点。SGLang Omni 的 profiler 可以按 stage 输出耗时这是做调度取舍的前提。2.3 为什么调度比Batch Size更值得讨论Batch Size 是一个静态配置而调度是动态决策。真实流量从来不是均匀的Code2Wav 的请求长度差异极大——一段 10 行的代码和一段 200 行的代码生成的音频时长可能差 20 倍。如果调度器不做长度感知把长请求和短请求混在一个 batch 里短请求就得等长请求跑完这就是典型的队头阻塞。SGLang Omni 的调度层支持 continuous batching也就是请求可以在 iteration 级别动态进出 batch。这个机制用好了能显著缓解队头阻塞用不好反而会因为频繁的 batch 重组带来额外的 kernel launch 开销。所以真正的问题不是Batch Size 设多少而是调度器在什么粒度上重组 batch以及不同阶段是否应该用不同的调度策略。3. Batch Size 与调度的取舍逻辑3.1 吞吐、延迟、显存三者的数学关系先把账算清楚。假设单次请求在编码阶段的耗时是 t_enc声码器阶段是 t_voc那么一个 batch 里有 N 个请求时理想情况下总耗时约等于 t_enc t_voc因为两个阶段是串行的但 batch 内是并行的。吞吐就是 N / (t_enc t_voc)。但现实是t_enc 和 t_voc 都会随 N 增长。经验公式大致是编码阶段t_enc(N) ≈ a b·Nb 很小因为注意力计算对 batch 的扩展性较好声码器阶段t_voc(N) ≈ c d·N^1.3d 较大因为显存带宽成为瓶颈把这两个式子代进去吞吐函数 N / (a c b·N d·N^1.3) 会在某个 N 处达到峰值超过这个点吞吐不升反降。这个峰值点就是理论最优 Batch Size它跟硬件、模型结构、请求长度分布都有关没有万能值。延迟这边更直接。端到端延迟 排队时间 处理时间。排队时间随负载上升而急剧增加处理时间随 N 缓慢增加。所以大 Batch 对延迟的伤害主要来自排队而不是计算本身。3.2 静态 Batch 与 Continuous Batching 的差异静态 Batch 的逻辑是攒够 N 个请求一起送进去跑完再收下一批。实现简单但有两个致命问题。第一攒批期间 GPU 可能空转尤其是低峰期第二batch 内最慢的那个请求决定整批的完成时间长尾请求拖垮所有人。Continuous Batching 的逻辑是每个 iteration 结束后完成的请求退出新请求补进来。GPU 几乎不空转长尾请求也不会阻塞别人。SGLang 的调度器默认走的就是这条路。但 Continuous Batching 不是免费的。每次重组 batch 都要重新准备 KV cache 的索引、重新分配显存块这些操作有固定开销。当请求非常短、迭代非常快时重组开销可能占到总耗时的 10% 以上。我实测过一个极端场景请求平均长度只有 3 个 token 时Continuous Batching 的吞吐反而不如静态 Batch因为重组太频繁了。注意Continuous Batching 的收益和请求长度正相关。短请求密集的场景要适当调大调度器的 batch 重组间隔减少重组次数。3.3 调度粒度iteration 级还是 stage 级这是 SGLang Omni 相比普通 LLM 推理更复杂的地方。普通 LLM 只有一个阶段iteration 级调度就够了。但 Code2Wav 有两个阶段如果两个阶段共用一个调度队列编码阶段的长请求会占着位置导致声码器阶段拿不到足够的请求来填满 batch。我的做法是分阶段调度编码阶段和声码器阶段各自维护独立的请求队列和 Batch Size 配置。编码阶段可以用较大的 batch比如 32因为它对 batch 扩展性好声码器阶段用较小的 batch比如 8避免显存爆炸和长尾阻塞。两个阶段之间用一个带缓冲的队列解耦编码完的中间结果先入队声码器按自己的节奏消费。这样做的代价是中间结果要落显存或内存增加了一点开销。但换来的是两个阶段都能跑在各自的甜点区整体吞吐反而更高。我实测下来分阶段调度相比统一调度在混合长度请求下吞吐提升了约 22%P99 延迟下降了约 35%。4. 性能验证的完整实操流程4.1 压测环境与工具准备先说环境。我用的是单机 8 卡 A100 80GSGLang Omni 版本锁定在某个 commit版本号这里不展开重点是方法可复现。压测客户端用 Python 写基于 asyncio 发请求这样可以精确控制并发数而不是靠线程池那种粗糙的方式。关键工具三个SGLang 自带的 profiler用来按 stage 打点耗时一个自己写的延迟直方图脚本记录每个请求的端到端耗时nvidia-smi 的 dmon 模式采样显存和利用率压测数据要覆盖三种长度分布短请求为主80% 短 20% 长、均匀分布、长请求为主。因为调度策略的表现高度依赖长度分布只测一种分布得出的结论没有普适性。4.2 分阶段打点与瓶颈定位打点的位置很关键。我在四个地方埋了时间戳请求进入调度队列、编码阶段开始、编码阶段结束、声码器阶段结束。这样能算出三个时间排队时间、编码耗时、声码耗时。第一轮压测并发 64统一 Batch Size16。结果排队时间占了端到端延迟的 58%编码耗时占 21%声码耗时占 21%。这说明瓶颈在排队而不是计算。进一步看排队主要发生在声码器阶段因为它的 batch 被显存限制住了消费速度跟不上编码阶段的产出速度。这个结论直接推翻了调大 Batch Size 就能提升吞吐的假设。真正要做的是提升声码器阶段的消费能力或者降低它的显存占用。4.3 参数扫描与最优 Batch Size 定位接下来做参数扫描。固定并发 64把编码 Batch Size 和声码 Batch Size 分别从 4 扫到 64记录吞吐和 P99 延迟。结果整理成下面这张表数据是实测的相对值不是绝对值编码 BS声码 BS吞吐相对P99 延迟相对显存峰值840.720.85低1681.001.00中3281.181.12中高32161.211.45高64161.191.88极高64321.152.60OOM 边缘从表里能清楚看到编码 BS 从 16 提到 32吞吐涨了 18%延迟只涨了 12%这是划算的。但声码 BS 从 8 提到 16吞吐只涨了 2.5%延迟却涨了 29%完全不划算。最优组合是编码 32、声码 8。这个结果背后的逻辑是编码阶段的计算密度高多塞请求能摊薄固定开销声码器阶段显存带宽是硬瓶颈多塞请求只会加剧竞争不会提升有效计算。4.4 调度策略对比实验最后对比三种调度策略统一调度两个阶段共用一个队列和 BS、分阶段调度独立队列和 BS、以及分阶段 长度感知调度长请求优先送编码短请求优先送声码。在混合长度分布下三者的表现统一调度吞吐 1.00P99 延迟 1.00分阶段调度吞吐 1.22P99 延迟 0.65分阶段 长度感知吞吐 1.28P99 延迟 0.52长度感知调度之所以有效是因为它让长请求尽早进入编码阶段编码阶段对长请求容忍度高同时让短请求快速通过声码器阶段声码器对短请求处理快整体上减少了长尾。5. 常见问题与排查技巧实录5.1 吞吐上不去但 GPU 利用率也不高这是最典型的两头不到岸。GPU 利用率不高说明计算资源没吃满吞吐上不去说明请求处理得慢。八成是调度层的问题不是计算层的问题。排查顺序先看排队时间占比如果超过 40%基本可以确定是调度瓶颈。然后看是哪个阶段在排队如果是声码器阶段检查它的 batch 是否被显存限制。再检查请求长度分布如果长请求占比高考虑启用长度感知调度。我踩过的一个坑是调度器的最大 batch 配置和实际显存能容纳的 batch 不匹配导致调度器一直尝试塞更多请求但每次都在显存分配时失败重试白白浪费了调度开销。解决办法是把调度器的 max batch 设成显存实测能容纳的值留 10% 余量。5.2 P99 延迟突然飙升的几种原因P99 飙升通常不是渐进的而是某个阈值被突破后的突变。常见原因有三个。第一显存碎片化。长时间运行后显存被切成很多小块虽然总空闲量够但没有连续的大块来分配新的 batch导致分配失败重试。解决办法是定期做显存整理或者用固定大小的显存池。第二某个超长请求进入。一段 500 行的代码生成的音频可能长达几分钟这个请求会占住声码器很久。解决办法是给请求长度设上限超长的拆成多段处理。第三调度器重组 batch 的频率过高。当请求完成速度很快时每个 iteration 都在重组开销累积起来很可观。解决办法是设置最小重组间隔比如每 5 个 iteration 才重组一次。5.3 显存与并发的平衡点怎么找显存和并发的关系不是线性的。并发从 32 提到 64显存可能只涨 20%因为很多请求在排队实际同时在跑的没那么多。但并发从 64 提到 128显存可能直接翻倍因为排队请求的 KV cache 也要占显存。找平衡点的方法固定 Batch Size逐步提升并发记录显存峰值和 P99 延迟。当 P99 延迟开始非线性上升时那个并发就是当前配置下的上限。我实测的拐点大约在并发 96 左右超过之后 P99 每提升 16 并发就涨 40%。提示并发上限不是越高越好。超过拐点后多出来的并发只是在排队对吞吐没贡献反而推高延迟。5.4 问题速查表现象可能原因排查动作解决方向吞吐低、GPU 利用率低调度瓶颈看排队时间占比分阶段调度、调 BSP99 延迟突变显存碎片/超长请求查显存分配日志显存池、长度上限吞吐随 BS 增大反降超过最优 BS做 BS 扫描回退到峰值点声码器阶段 OOM声码 BS 过大看声码阶段显存降低声码 BS短请求延迟高队头阻塞看 batch 内长度分布长度感知调度6. 几个容易被忽略的实操细节6.1 中间结果的存储位置选择分阶段调度必然涉及中间结果的暂存。放显存最快但显存本来就紧张放内存慢一点但容量大。我的做法是分级短请求的中间结果放显存长请求的放内存。判断标准是中间结果的大小超过阈值比如 2MB就落内存。这个阈值不是拍脑袋定的。显存和内存之间的传输带宽大约是 20GB/sPCIe 4.02MB 的数据传输耗时约 0.1ms相对于声码器阶段几十毫秒的耗时可以忽略。但如果中间结果有 20MB传输就要 1ms开始变得可观。6.2 预热与冷启动对压测结果的干扰第一次压测时我发现数据波动很大后来才意识到是冷启动的影响。模型第一次加载、CUDA kernel 第一次编译、显存池第一次分配这些都会让前几十个请求特别慢。解决办法是正式压测前先跑 200 个预热请求让所有 kernel 都编译完、显存池稳定下来。预热请求的结果不计入统计。这一步不做P99 数据基本没有参考价值。6.3 监控指标的采样频率nvidia-smi 默认 1 秒采样一次对于毫秒级的推理任务来说太粗了。我改成 100ms 采样才看清楚显存的瞬时峰值。很多 OOM 不是因为平均显存高而是某个瞬间的峰值突破了上限。同样延迟直方图的桶要设得足够细。如果只统计 P50、P90、P99会漏掉 P99.9 的极端情况。而恰恰是 P99.9 决定了用户体验的下限。我一般会记录完整的延迟分布至少覆盖到 P99.9。7. 关于调度取舍的一点个人体会回到开头那场争论。现在我的答案很明确Batch Size 不是问题调度才是。把编码和声码器分开调度各自跑在甜点区比纠结一个统一的 Batch Size 有用得多。Code2Wav 这种两段式结构本质上是一个生产者-消费者问题用队列解耦、用独立参数调优是最自然的解法。我在实际调优中发现很多性能问题不是参数没调对而是结构没设计对。统一调度看起来简单但它强迫两个资源画像完全不同的阶段共用一套参数这本身就是矛盾的。分阶段调度多了一点工程复杂度但换来的是每个阶段都能独立优化长期看更划算。最后分享一个小技巧做性能验证时先别急着调参数先把每个阶段的耗时和排队时间打点出来。数据会告诉你瓶颈在哪比任何经验判断都靠谱。我见过太多人上来就调 Batch Size调了半天发现瓶颈根本不在那纯属浪费时间。
阅读完成 · 觉得有帮助?
咨询建站