ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SGLang max_batch_size调优:吞吐、TTFT与调度取舍的实证分析

SGLang max_batch_size调优:吞吐、TTFT与调度取舍的实证分析 上周四下午组里就一行配置参数吵了起来。“max_batch_size 给我提到 512不然 A100 的算力全浪费了。”“你疯了线上 TTFT 绝对崩。”“你测过吗没测就别下结论。”……最后我把两个人都按在椅子上花了一个下午用同一套 SGLang 服务跑了一组对照实验得到的结论跟双方想的都不太一样。这篇文章就把这次 Batch Size 争论、性能验证过程和背后的调度取舍完整记录下来后面也会讲我们内部那个叫 Omni 的全局调度方案是怎么把 batch 决策从单机维度拉到集群维度的。适合正在用 SGLang 做推理服务、被 max_batch_size、并发数、TTFT/TPOT 折磨的朋友参考。1. 争论的起点Batch Size 在 SGLang 里到底管什么1.1 我们常说的 Batch Size和 SGLang 里的不是一回事很多争论其实是概念错位造成的。训练场景里的 batch size 是静态的一批样本喂进去前向传播、反向传播、更新梯度然后下一批。但 SGLang 在推理阶段采用的是连续批处理continuous batching这是它调度性能的核心。连续批处理下没有“一批请求固定多少条”的概念调度器在每一个迭代步都会重新决定当前这轮同时跑哪些请求、哪些请求可以插入、哪些请求要暂时靠边。所以 SGLang 配置里那个 max_batch_size严格理解不是“一批有多大”而是“系统里最多允许多少个请求同时处于处理中”。它的作用更像一个闸门而不是一个分组的框。你把闸门放得很宽不代表每个迭代步都会跑满更不代表跑满了吞吐就最高。这一点必须反复强调因为很多人包括我们组里那位主张 512 的同事一开始都是用“训练 batch 越大越高效”的惯性在想问题。放到 SGLang 里这个直觉只在硬件利用率没到饱和时成立一旦进入调度器的复杂博弈batch 大小和吞吐的关系就变成了曲线而不是直线。1.2 请求在 SGLang 调度器里要经历的两次“分组”一次请求从进入到结束在 SGLang 调度器里要经历两个完全不同的阶段。第一个是 prefill也就是预填充阶段。新请求进来之后系统要把输入的 prompt 做注意力计算生成对应的 KV Cache这属于计算密集型操作有点像厨房里“备菜”要把所有材料洗好切好。第二个是 decode也就是逐 token 生成阶段。调度器会把多个请求的当前 decode token 拼成一个大矩阵一起走一次前向计算生成下一个 token这更像“出菜”一桌一桌地端。SGLang 的调度器在每个迭代步都要决定哪些请求走 prefill、哪些请求继续 decode。这里的“分组”是动态拼凑的不是固定的。更复杂的是SGLang 还有 RadixAttention也就是基于前缀树的 KV Cache 复用机制。如果新请求的 prompt 前缀跟之前某个请求完全一样那这部分 prefill 结果可以直接复用不用重新算。这就意味着同一个 max_batch_size在不同前缀命中率下系统实际跑出来的压力天差地别。1.3 这场争论背后真正的复杂度回头看那天的争执我们发现大家纠结的其实不是数字而是三个字取舍。max_batch_size 设小一点单个请求的时延更可控TTFT 和 TPOT 都更稳但 GPU 矩阵计算的规模可能喂不满设大一点理论上能把卡塞得更满可是 prefill 风暴一来decode 阶段被拖慢长尾时延直接爆炸。在这个场景里吞吐、TTFT、TPOT、显存水位、排队延迟五个指标互相拉扯。你不可能同时让它们全部最优。SGLang 的调度器已经帮你做了很多动态决策但真正决定“系统工作在曲线哪个位置”的还是你设置的上限和流入的负载。这也是后来我们为什么单独做了个 Omni 层专门去处理单机调度解决不了的问题。2. 性能验证不凭感觉跑数据说话2.1 实验设计固定并发只动 max_batch_size争吵没有意义我开始设计实验。第一原则是一次只动一个变量。我们把模型固定在 Llama-3.1-8B-Instruct单卡 A100-80GTensor Parallel 设成 1。请求长度固定为输入 1024 token、输出 256 token这个配置模拟我们线上一个比较重的 RAG 场景。压测工具用 SGLang 自带的 bench_serving类似这样python3 -m sglang.bench_serving \ --backend sglang \ --model meta-llama/Llama-3.1-8B-Instruct \ --num-prompts 1000 \ --request-rate 32 \ --output-len 256这里最关键的是并发控制。我们通过压测客户端维持固定并发数 32也就是同一时刻最多 32 个请求在系统里“活着”。然后只修改服务端的 max_batch_size从 32、64、128、256、512 五档去扫。每次跑之前先预热 200 个请求让模型结构和 RadixAttention 的前缀缓存进入稳定状态每个配置重复 3 轮取中位数。有人会问为什么不在每个 batch size 下让并发也变化因为那样测出来的是“系统承受能力”而不是“batch 参数影响”。我们要回答的问题很明确假设同时只有 32 个用户在用服务端允许更多请求一起跑到底能不能跑得更快允许到多少开始坏事2.2 结果先上升然后断崖长尾爆炸这张表是那次实验几轮数据里比较典型的一组虽然具体数字只对当时那套环境有效但曲线形态在后来多次复测里都很稳定max_batch_size吞吐token/sTTFT P50msTTFT P95msTPOT P95ms平均排队延迟ms峰值显存321180320850584548%64146042013006616061%128152068029009252074%256129011007800178240086%512980230019000320850093%吞吐从 1180 一路上升到 1520看起来 batch 从 32 提到 128 是划算的。但过了 128 之后直接掉头向下512 档甚至还不如 64 档。与此同时TTFT 的 P95 以近乎垂直的方式往上冲从 2900ms 直接涨到 19000ms。P50 看起来只是翻倍但 P95 翻了六倍多这说明系统里有大量请求在某个环节被卡住了。主张 512 的同事看到这张表沉默了。他要的高吞吐确实在 128 之前出现了但并不是一条单调上升的曲线。而且如果我们按线上时延 SLO 来卡比如 TTFT P95 必须小于 3 秒那最优配置直接锁定在 128 以下根本轮不到 512 说话。2.3 为什么 max_batch_size 增大反而变慢这个反直觉的现象拆开来看有三个原因。第一个是排队效应。max_batch_size 是上限不是目标。当系统允许 512 个请求同时在跑而并发只到 32 时大量请求并不会立刻开始计算而是会被调度器“挂起来”。它们占用了队列位置却并不产出 token直到前面有请求结束或调度器决定插入。这样实际的有效计算比例在下降排队延迟自然暴涨。第二个是 prefill 打断 decode。连续批处理最头疼的场面就是 prefill 和 decode 抢计算资源。大 batch 意味着有更多空间让新请求插入到已经运行的 decode 批次里。每插入一个 prefill那一整轮迭代都会因为 prefill 的密集计算而变慢正在 decode 的请求被迫等待。batch 越大这种“插队”频率越高decode 阶段的吞吐反而被拖垮。第三个是 KV Cache 和抢占相关开销。max_batch_size 调大后调度器要为潜在的请求预留更多 KV Cache 空间。RatidAttention 的前缀树命中率在这种高并发插入模式下也会下降因为 Cache 被频繁替换、驱逐。更糟的是当显存压力接近上限SGLang 可能会触发抢占让部分请求的 KV Cache 释放掉后面再重新计算这就是纯赔本的买卖。2.4 性能验证的可靠姿势P95 才是老板这次实验给我最深的感受是只看吞吐均值会严重误导决策。吞吐高了P95 TTFT 如果崩成两位数秒级在线上就是一堆“转菊花”投诉。我们的实际做法是先把业务 SLO 写死比如“TTFT P95 3s、TPOT P95 100ms”然后在这个前提下去选 batch 配置而不是先选 batch 再祈祷时延能撑住。另外扫描配置的时候别一上来就全组合跑那样既慢又难定位问题。我现在的习惯是固定并发从小到大扫 max_batch_size记下吞吐和时延拐点找到拐点后再固定一个比较稳的 batch 值去扫并发数看系统能扛到什么程度。先粗扫后细扫每个配置必须预热每次只看一个变量。这样才能从性能验证里得到可信的结论。3. Omni 调度取舍从单机 batch 到全局调度3.1 SGLang Omni 是什么先解释一下标题里的 SGLang Omni。SGLang 官方仓库里并没有一个独立组件叫 OmniOmni 是我们内部的叫法我们基于 SGLang 的连续批处理和 RadixAttention在集群层面加了一个全局调度层让多台 SGLang worker 像一个整体一样工作。所以“SGLang Omni”本质上是 SGLang 调度能力在我们实际生产环境里的延伸。为什么需要这一层因为单机的最优 batch 配置换到多实例、多模型、混合流量的场景里就不够用了。比如我们集群里同时跑在线问答和离线批量打分两者对时延、吞吐的要求完全不同。如果只是给每个 SGLang 实例配同一个 max_batch_size要么在线请求被离线任务堵住要么 GPU 利用率低得让人心疼。3.2 全局调度器的输入信号和输出决策Omni 调度器做的事情本质上是一个带反馈的控制环。它周期性采集每个 worker 的 metrics包括当前正在跑的请求数、排队长度、KV Cache 使用率、decode 批次大小、prefill 与 decode 的比例、平均 decode 单个 token 的耗时等等。然后综合这些信号对每个 worker 做几件事是否接受新请求、新请求进入哪个优先队列、需不需要把 batch 窗口调小或调大、要不要把某些请求导流到更空闲的实例。打个比方一个餐馆有多个灶台每口锅的火候不一有的锅正炒着一个大菜有的锅刚空出来。全局调度员不是把菜全倒进最大那口锅而是根据每口锅的温度、出菜速度、排队客人的耐心程度动态决定哪道菜上哪口锅哪道菜先在菜单上多等一会。3.3 四种调度取向怎么取舍在实际调 Omni 的过程中我们把负载抽象成四种典型取向每种对应一套 batch 策略取向适用场景max_batch_size 策略调度重点主要代价高吞吐优先离线批量、离线评测尽量调大让 GPU 尽量满载优先凑大 decode 批次TTFT、长尾时延不可控低时延优先在线对话、RAG 问答调小控制在稳定区间阻止新 prefill 频繁插队硬件利用率偏低稳定性优先生产主链路、强 SLO根据 P95 反向压测动态限流、有界队列可能牺牲少量吞吐成本优先离线混合业务、资源紧张不断试探最大化吞吐接受排队和抢占时延波动剧烈没有哪一种取向是永远对的。最怕的是嘴上说低时延优先手上却把 max_batch_size 拉到 512这就是自相矛盾。先明确取舍方向再谈参数否则性能验证跑出来也是一堆无法解释的数字。3.4 我们在生产环境里的选择拆小而不是撑大这个可能就是整篇文章最核心的取舍。我们最后没有选择“最大吞吐那档配置”而是把在线和离线拆成了两条调度路径。在线请求路由到 max_batch_size 只有 64 的 worker 组P95 TTFT 稳定在 1.5 秒以内离线请求路由到 max_batch_size 可以到 256 甚至更高的 worker 组专门用来填吞吐。以往我会觉得“让每个实例都跑在大 batch 才是高效”但 Omni 上线后的数据告诉我小 batch 多实例往往比大 batch 单实例更稳。原因是长尾时延和抢占重试的成本在大 batch 下会被无限放大而小 batch 让调度器有更细的颗粒度去调整一个实例抖动不会拖垮所有请求。很多团队把资源浪费在“把单个 batch 调满”上我现在的思路是“让系统里同时存在多个不同 batch 大小的调度小区”每个小区服务一类 SLO。4. 经验复盘与避坑指南4.1 这次压测里踩过的坑第一件事就是没预热导致的假数据。第一次跑 512 档吞吐低得离谱排查了半天发现是模型刚加载完RadixAttention 缓存是空的系统还在做各种初始化。后来统一预热 200 个请求数据才稳定下来。第二件事是并发数最开始没锁死压测工具默认不限并发导致测出来的曲线其实是并发在变化batch 参数的影响被完全淹没。第三件事是“只看吞吐”的老毛病。第一轮结果出来128 档吞吐最高我们差点就拍板了。还好顺手看了下 TTFT P952900ms 已经踩到线上 SLO 边缘。如果当时不听长尾指标上线后就是一批用户抱怨“转圈转半天”。第四件事是显存预留的坑。max_batch_size 调大后SGLang 会按上限预留 KV Cache pool看起来显存剩 20%实际上已经被“预定”了再加载一个 LoRA 直接 OOM。别被空闲显存的数字骗了要看调度器的预留策略。4.2 常见问题速查表现象可能原因排查和解决方案增大 batch 后吞吐反而下降prefill 插队、排队延迟增加、KV Cache 命中率下降扫描 batch 找到吞吐拐点固定并发再测开启 RadixAttention 前缀复用TTFT 正常但 TPOT 波动大decode 阶段一直在被新请求 prefill 打断降低 max_batch_size把在线流量路由到专门的 worker 组显存没有占满却开始 OOMKV Cache 按 max_batch_size 预留物理显存被锁定减小 max_batch_size检查调度器的 cache 预留策略P50 很低P95 高得离谱出现“饥饿请求”部分请求长期排队或被抢占加有界队列拒绝过载请求调整调度优先级多实例下某个 worker 长期空闲全局调度没有感知该实例的 decode 状态接入 Omni 调度器按 KV Cache 利用率做路由所有 worker 都忙但整体吞吐上不去可能存在公共前缀命中率低或输入长度差异过大抓一个 trace 看 prefill/decode 比例必要时拆分长请求4.3 给后来者的调参经验别把并发和 batch 混在一起。并发数代表业务同时有多少请求打到系统里它由上游 QPS、超时时间和客户端连接池决定max_batch_size 是服务端允许同时处理的闸门。合理顺序是先标定业务预期并发和时延 SLO再决定 max_batch_size而不是反过来。再一个很实用的经验线上配置一定要参数化不要写死。我们后来把 max_batch_size、并发上限、路由策略全部做成可以动态调整的配置配合监控指标做灰度发布。这样即使新版本或者流量模型变了也能快速拉回到稳定配置不用每次重新压测。最后如果要复用 RadixAttention 的能力业务层尽量在设计 prompt 时保持公共前缀稳定。实测中如果所有请求都带同一个超长 system prompt 且公共前缀能复用prefill 的计算压力会大幅下降在这种情况下把 batch 调大一些通常是安全的。反之如果请求 prompt 千差万别前缀命中率接近零那 batch 一大就是灾难。聊到最后我发现团队里为 batch size 吵架的次数虽然多但每次吵完都会推动我们进一步理解调度器的工作方式这其实是好事。我个人现在最深的体会就是一句话先把 SLO 定死再把并发摸清最后让 batch 去适配整体调度而不是让业务去迁就一个拍脑袋的数字。以后谁再跟我说 max_batch_size512我就先把这份压测表拍在桌上然后问他一句你要的高吞吐到底是 P50、P95还是 P99
返回列表