ARTICLE DETAIL

资讯详情

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

Batch Size 之争背后:SGLang Omni 性能验证与调度取舍

Batch Size 之争背后:SGLang Omni 性能验证与调度取舍 从一次 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 Size128 时吞吐最高接近 1800 tokens/s。B 同学负责线上容量预估他坚持认为 Batch Size64 更符合线上真实场景理由是真实请求的输入长度波动很大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 阶段的 ITLtoken 间延迟也可能会变长——GPU 要轮流服务更多请求每个请求分到的算力变少。这就是那场争论里 B 同学坚持 Batch Size64 的核心顾虑。我在多组实验里观察到Batch Size 从 1 到 64吞吐几乎是线性上升的从 64 到 128吞吐提升幅度下降到 15%-20% 左右从 128 到 256吞吐基本走平甚至略有下降而 P99 延迟显著恶化。这说明在 A100 或 H100 这类显卡上Batch Size64 附近已经是一个相当合理的甜点区盲目求大没有意义。2.2 显存约束下的 Batch Size 上限计算很多人不知道 Batch Size 的上限其实由显存决定而且是可以提前算出来的。核心约束来自 KV Cache 的显存占用。对一个 transformer decoder 模型单条序列的 KV Cache 大小可以粗略估算KV Cache 字节数 2K 和 V 两组 × 层数 × 注意力头数 × 每头维度 × 序列长度 × 精度字节数例如一个 7B 参数的模型层数 32、头数 32、每头维度 128序列长度 4096使用 FP162 字节单条完整序列的 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-sizeprefill 阶段每次处理的最大 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.xPyTorch 2.1SGLang 最新稳定版按官方文档安装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 Size32 和 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-4096max-running-requests48-64开启 RadixAttentionchunked prefill1024。这个配置在吞吐和延迟之间相对均衡。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 调到多少了”而是统一表达为“在什么前缀命中率、什么并发水位、什么延迟约束下我们选择怎样的调度配置”。这套语言的变化比任何单一调参技巧都更有价值。如果你正在搭建或优化推理服务我建议把建立可信的性能验证体系放在第一位把精力投入到理解框架的调度机制上——这些能力会让你在遇到类似争论时不用站队而是直接给出可复现的实验设计和清晰的技术结论。
返回列表