1. 从一次争论说起Batch Size 不是越大越好那次争论发生在一次大模型服务的性能评审会上。有位同事信誓旦旦地说Batch Size 开到 256 肯定比 32 快得多理由是 GPU 的利用率更高吞吐量一定翻好几倍。另一位同事当场反对说上一个项目就是把 Batch Size 从 8 调到 64结果吞吐确实上去了但首 Token 延迟从 300 毫秒暴涨到 1.2 秒线上客户直接投诉。两边谁也说服不了谁最后问题落到我头上你说到底该信哪个我当时的回答很简单别信经验信验证。但“验证”这两个字说起来轻巧做起来全是坑。那次争论之后我专门用SGLang Omni把整套推理服务的性能验证流程重新梳理了一遍顺便把 Batch Size 和调度器之间的耦合关系彻底想明白了。这篇博文就是那段调研和实战的完整记录。先说结论省得你看到最后才恍然大悟Batch Size 从来不是一个独立参数它是推理引擎、显存、调度策略和业务延迟目标之间的一个权衡点。脱离场景谈 Batch Size 就是耍流氓。SGLang Omni 这类带连续批处理和 RadixAttention 的推理引擎更是把 Batch Size 的语义从“一次喂多少条数据”变成了“调度器在每一轮迭代里实际放行多少条请求”这中间的区别才是性能验证真正要抓的核心。这篇文章适合三类人看正在做大模型推理服务压测的工程师、纠结 SGLang 配置参数选型的算法工程师以及被业务方追着问“为什么吞吐上不去”的架构师。我会把这套验证方法论、调度取舍思路和踩坑记录全部摊开讲保证能直接抄作业。2. 重新理解 SGLang Omni它到底优化了什么2.1 不是又一个推理框架而是调度逻辑的重写SGLang 这个名字早期会被拿来和 vLLM、TGI 对比但如果只把它当成“又一个高性能推理框架”就完全错过了它最有价值的部分。SGLang 最核心的思想是RadixAttention——把请求的公共前缀比如系统提示词、上下文前缀在 KV Cache 层面做树状复用而不是每条请求都从头计算一遍。这个设计对性能验证的影响是颠覆性的。传统推理框架里你调大 Batch Size意味着同时有更多请求在 GPU 上排队计算每条请求都要完整走一遍 Prefill 和 Decode 阶段。但在 SGLang 里如果一批请求共享大量前缀同一份 KV Cache 可以直接被多条请求命中实际计算量根本不是按请求条数线性增长的。我在实际压测里见过一个很夸张的例子一组 1000 条 QA 测试请求共享一个 2 万字的长系统提示词。同样 Batch Size 设为 32SGLang 的 Prefill 耗时只有普通框架的 40% 左右。这就是为什么你在 SGLang 上调 Batch Size不能简单套用其他框架的经验值。再说Omni这个版本。SGLang Omni 是面向多模态和混合负载场景的集成方案把文本、图像、视频等内容类型的推理调度统一到了一套流程里。也就是说你不能再假设模型只需要处理纯文本。图像 token 的数量远超文本 token一条带图的请求可能吃掉十几倍的计算量Batch Size 如果还按请求条数算调度的颗粒度就太粗了。2.2 SGLang Omni 的调度参数Batch Size 只是其中一环SGLang 的引擎层有几个参数是性能验证时必须一起盯着的光调 Batch Size 不看其他参数等于只换了一个轮胎就上高速参数作用和 Batch Size 的关系max-running-requests决定同时有多少请求处于活跃执行状态比 Batch Size 更接近“真正的并发数”schedule-policy调度策略如 LPM、随机或偏好优先决定请求按什么顺序进入 Batchmax-num-batched-tokens单轮迭代最多处理的 token 数限制 Batch 的总计算量防 OOMradix-trees使能并管理前缀缓存影响可复用 KV Cache 的请求比例chunked-prefill将长 Prefill 拆成小块调度避免大请求阻塞小请求的 Decode这里边的关键点在于Batch Size 控制的是“放行多少个请求”但max-num-batched-tokens控制的是“一次实际计算多少 token”。两条线必须同时约束只调任何一条都会出问题。举个例子你把 Batch Size 从 32 调到 64但max-num-batched-tokens没动假设值还是 8192。结果就是前 10 条长请求就把 token 额度打满了剩下 54 条请求虽然被放行也进不了本轮计算只能在队列里等着被下一轮调度。这时候你测出来的“Batch Size 64”其实实际有效并发只有十几条吞吐数据会非常难看。反过来如果把max-num-batched-tokens调得很高比如 65536Batch Size 只有 8遇到一批长文本请求仍然可能把显存打爆。原因很简单8 条请求每条 8000 token总 token 数 64000照样超过显存承载能力。所以在 SGLang Omni 上做性能验证第一课就是Batch Size、max-num-batched-tokens、显存容量、模型参数量四者必须放到一个公式里看。3. 性能验证方法论怎么科学地选择 Batch Size3.1 先定义场景你要优化的是吞吐还是延迟回归到开头那场争论。同事 A 说 Batch Size 大吞吐高同事 B 说 Batch Size 大延迟爆炸。两个人其实都对只是守着不同的优化目标。Batch Size 本身没有好坏它只是在吞吐和延迟之间做一个交换。选 Batch Size 之前必须先把业务场景的压力目标量化。我通常会把场景拆成三类离线批处理场景比如离线对一批历史工单做分类晚上跑跑一小时还是两小时没本质区别。这类场景直接追求吞吐最大化Batch Size 往大调眼神都不用眨。唯一限制是显存和稳定性只要不 OOM越大越好。在线交互场景比如聊天助手、客服机器人用户可感知的指标是首 Token 延迟和整体回复流畅度。这类场景必须限制 Batch Size不能让它无限膨胀。因为连续批处理下大的 Prefill 请求会抢占 GPU 资源阻塞其他请求的 Decode 阶段。混合负载场景一部分请求是长文档分析Prefill 重一部分请求是短对话Decode 重。这是 SGLang Omni 最擅长处理也最考验调度参数的场景。Batch Size 的选择必须和调度策略配合否则两类请求会互相拖垮。先想清楚你是哪一类再去调参。否则你拿着第二类场景的延迟要求去做第一类场景的最大吞吐验证得到的结论毫无意义。3.2 怎么测才靠谱固定变量法的实际落地性能验证最怕的就是变量不固定。SGLang Omni 的参数互相耦合如果同时改了 Batch Size、调度策略、prefix cache 配置测出来的结果根本说不清是谁的贡献。我自己的习惯是每一次性能对比实验只动一个变量。以 Batch Size 为核心变量的压测我一般这么设计固定请求数据准备 1000 条相同分布的真实请求包括请求长度、输入输出 token 比例、并发模拟方式。严禁随机生成请求因为 token 长度分布不均会直接污染实验结果。固定调度策略把schedule-policy固定为默认策略不要动。因为不同策略在不同 Batch Size 下的表现差异很大混在一起测无法归因。固定 token 预算max-num-batched-tokens保持默认或一个中间值不随 Batch Size 联动调整。这个值单独在第二轮实验里测。梯度调 Batch Size从 1、2、4、8、16、32、64、128 这样指数级往上加。每档至少跑 300 条请求统计平均吞吐tokens/s、平均首 Token 延迟TTFT、平均单 Token 生成延迟TPOT和显存峰值。记录归一化指标不要只看吞吐裸值。用 “吞吐/显存占用” 算单位显存吞吐用 “P95 延迟” 看长尾影响。这两个归一化指标才是跨配置可比对的。我自己实测过一组 Llama 3 8B 模型的数据放在这里给你参考Batch Size吞吐 (tokens/s)P95 TTFT (ms)P95 TPOT (ms)显存峰值 (GB)17802104517.8832003405220.53256006807324.2647100135011827.61287200260019028.9看到数据里隐藏的信息了吗Batch Size 从 64 到 128吞吐几乎没有增长但延迟翻了近一倍。这说明 64 附近已经逼近了 GPU 计算资源的饱和点。再往上加请求只是在队列里排队GPU 的算力没有被更高效地利用反而是排队时间被叠加到了延迟里。这类“拐点”效应单看吞吐指标是发现不了的必须要同时看延迟分布和资源利用率。3.3 为什么“连续批处理”让 Batch Size 的意义变了传统推理框架里很多框架的做法是固定 Batch Size服务端会等一批请求集齐了再统一推理类似于大巴车满员才发车。SGLang 的做法完全不同它用的是类似操作系统的抢占式调度只要有空闲算力就立刻把新的请求调度进来不会傻等 Batch 填满。这意味着什么在 SGLang Omni 里你设置的 Batch Size 其实是一个天花板不是地板。系统不会因为 Batch 没满就闲着但会被 Batch Size 限制最大并发量。换句话说max-running-requests设得越小不管上层来多少流量实际被处理的请求数不会超过这个上限。理解这个区别之后调参的思路就变了。传统框架里调 Batch Size是调整请求“攒一批”的粒度SGLang Omni 里调 Batch Size是调整系统允许的最大并发预算。前者是批处理思维后者是资源池思维。所以在 SGLang Omni 上进行性能验证我不建议只盯着 Batch Size 这一个参数来回试。它只是决定并发上限的其中一个旋钮。真正影响调度表现的是max-running-requests活跃请求上限和max-num-batched-tokens单轮 token 预算的配合比例。提示SGLang 也提供了--cpu-offload-gb这类参数可以把部分 KV Cache 挪到 CPU 上换更大并发空间。但这招要慎用CPU 和 PCIe 带宽会成为新瓶颈我测过之后发现吞吐掉得挺明显只适合长文本但低并发的场景。4. 调度取舍从操作系统到推理引擎的同一套逻辑4.1 调度问题从来不是“哪个调度器好用”先把标题里的另一个关键词“调度”摊开说。这类热词近期热度很高——操作系统 CPU 调度、负载调度器、大数据调度工具、AGV 调度系统、农机调度、无人机运输协同调度等等。听起来跨度很大但本质都指向同一个问题有限的资源怎么分配给进入系统的任务才能让整体目标最优。操作系统的进程调度非常典型核数有限进程一堆怎么分时间片CPU 繁忙时让谁等待、谁执行延时敏感的任务优先跑批量计算任务靠后放这就是多级反馈队列。大模型推理的调度困境一模一样GPU 显存和算力是有限的请求的 token 长度分布极度不均匀有些请求要快速响应在线对话有些请求希望吃到最大吞吐离线批处理所以调度这个层面就不该问“SGLang 好还是 vLLM 好”而要问“我的业务目标适合哪种调度策略SGLang Omni 的调度参数能不能帮我实现”。4.2 大模型推理里的三种调度策略对比SGLang Omni 提供了多种调度策略针对不同负载特征效果差异非常大。我粗浅地跑过对比实验把三类典型的调度策略和适用场景梳理了一下第一类FIFO先来先服务这最接近直觉谁先来就先调度谁。优点是公平实现简单缺点是长 Prefill 请求会挡住后面所有短请求。想象一下一条带 5 张图的请求进了队列后面有 50 条聊天请求要立刻回FIFO 调度下这些聊天请求只能干等。这批长请求做完 Prefill 之前短请求连首字都出不来。所以 FIFO 只适合请求长度分布非常均匀的离线场景。第二类LPM最长前缀匹配 / 高命中优先SGLang 里前缀命中和调度策略是绑定的。高命中优先会让那些和缓存中的前缀完全匹配的请求优先执行这种策略会提升 RadixAttention 的命中率因为越集中处理相似前缀请求KV Cache 复用效率越高。问题是如果业务里前缀分布很散这个策略基本退化成随机调度收益不大。第三类长尾感知 / 饥饿避免SGLang 里有 starvation-avoidance 机制这类策略专门防止某个短请求一直排在长请求后面饿死。SGLang 社区版甚至专门处理了“长 Prefill 排在最前面导致后续请求全部饥饿”的场景会把长 Prefill 拆块或者限制它的连续执行次数。我在多模态混合负载上实测这类策略能让 P95 延迟稳定下降 30% 以上代价是总吞吐可能小幅回落 5%-8%。这些策略本质上就是在抄操作系统的调度设计——时间片轮转、优先级调度、预防饥饿这些操作系统里的基础思路在推理引擎里全都能找到对应物。所谓“大模型调度平台的任务及队列管理”说到底也没逃出这个框架。4.3 调度层和上层平台的关系你不能只调引擎很多人忽略的是SGLang Omni 只是推理引擎这一层的调度它上面还有一层任务调度平台。平台负责决定“哪些任务该下发到推理引擎”引擎负责决定“任务进入后怎么排队执行”。这两层必须协同调引擎参数调得再好平台层任务下发策略不行性能照样上不去。用调度平台的术语来说队列管理任务进哪个队列队列优先级怎么定。相当于把请求按业务重要程度先分流一次。任务依赖有的任务需要先做完离线数据预处理才能推理平台层不处理好依赖引擎层接到任务才刚刚开始等数据。告警机制任务执行失败要第一时间告警到企业微信这类 IM 工具。不配告警的调度平台等于给引擎层埋雷故障发现总慢半拍。所以我在做 SGLang Omni 性能验证时从来不会只压引擎而是会把上层任务调度策略一起纳入测试范围。比如在离线批处理场景里到底应该让平台一次性把 1000 条任务全推给推理服务还是按 200 条一批的粒度分批发这两种方式对引擎的队列压力完全不同Batch Size 的参数选择自然也不同。我在一个文档总结场景里测试过一次性全量推入时引擎请求队列积压严重TTFT 平均多了近 400ms改成 300 条一批分批推入后队列压力明显缓解吞吐反而提升了约 15%因为每批请求长度更均匀调度层的缓存命中率保持在较高水平。上层任务调度粒度对推理性能的影响往往比引擎参数本身更值得先排查。5. 实操过程一次完整的 SGLang Omni 性能验证实录5.1 从压测脚本到指标采集一步步给你看这次验证我跑的是 SGLang Omni 的 Docker 版本模型是 Llama 3.1 8B纯文本模式还没上多模态先把底子打好。压测工具用的是自写的 Python 并发脚本不走官方 benchmark 脚本因为要完全控制请求分布和并发模型。环境是这样的GPU单卡 A100 80G后来换到 4090 验证了一次双卡场景驱动和 CUDACuda 12.4 / PyTorch 2.4SGLang0.4.x 版本分支镜像版本比较新官方 release 更新很快请求数据集1000 条混合长度请求平均输入 800 token输出 300 token长度标准差比较大启动命令大概长这样docker run --gpus all --shm-size 32g \ -v /data/models:/models \ -p 30000:30000 \ lmsysorg/sglang:latest \ python3 -m sglang.launch_server \ --model-path /models/Llama-3.1-8B-Instruct \ --host 0.0.0.0 --port 30000 \ --max-running-requests 128 \ --max-num-batched-tokens 8192 \ --schedule-policy lpm \ --radix-trees 256这里--radix-trees 256是启用 RadixAttention 缓存。压测脚本里我用 ThreadPoolExecutor 模拟并发客户端同时测 TTFT 和 TPOT。TTFT 就是从请求发出到收到第一个 token 的耗时TPOT 就是第一个 token 到最后一个 token 之间的平均间隔。这两个指标一个管“用户等待感”一个管“生成流畅感”。在压测之前一定要跑一发 smoke test确认服务稳定响应再开始正式记录。否则启动阶段 GPU 还在做权重加载、显存尚未稳定直接压测会把启动噪声混进数据里。5.2 参数排查过程用二分法圈定性能拐点我第一步是把 Batch Size也就是max-running-requests当成变量快速定出拐点区间。先跑一组 16、32、64发现 32 到 64 吞吐还在涨但 P95 TTFT 开始恶化。再跑一组 40、48、56把拐点范围压缩到 48 附近。这就是性能调优里非常经典的“粗扫 细扫”二段式方法。没有必要一开始就用 1、2、4、8、16、32、64、128 一档一档完整跑太耗时间。先把量级找对再放大拐点附近的细节能省至少一半实验时间。找到拐点后我还做了第二组实验这次变量是max-num-batched-tokens固定max-running-requests48。从 8192 跑过来发现 16384 时吞吐提升明显但 32768 时显存压力增大A100 80G 吃得消而 TTFT 又有轻微反弹。综合考虑最终选定了 48 并发请求、16384 token 预算这一组参数。选完这两个核心参数之后还得验证调度策略的影响。同样的 48/16384 配置下我分别用 LPM 和 FIFO 跑了一轮。LPM 策略下前缀缓存命中率高一点但整体差距不到 8%说明这组请求的前缀相似度本身不够高。这也提醒我如果未来测试多模态混合请求策略的影响权重会明显放大因为图像的 token 序列存在大量可复用的公共 segment。5.3 显存这样算就不会在压测中途被 OOM 干翻SGLang 里最烦的问题就是压测跑到一半显存溢出。预判显存用量这事我总结了两个心法。第一是套公式估算第二是实时监控实测。估算公式一般这么套权重显存 ≈ 模型参数量 × 2BF16或 × 4FP32。一个 8B 模型BF16 下大约 16GB。KV Cache 显存 ≈ 模型层数 × KV 头数 × 精度因子 × 最大并发 token 数。这个值波动很大要看模型的 hidden size 和层数配置不好一概而论。运行时开销包括激活值、临时计算 buffer一般留 4-6GB 余量比较保险。我在 80G A100 上评估8B 模型加 48 并发、16384 token 预算、2048 上下文长度整套下来显存大约在 22GB 到 26GB 之间浮动。这个余量还比较从容。但如果换成 70B 模型权重就得 140GB单卡完全跑不动必须走张量并行或多卡公式里的显存估算逻辑也得彻底换一遍。另一个实用技巧是压测期间每 10 秒自动截取一次nvidia-smi的显存快照压测结束后取峰值。别靠肉眼盯终端一轮压测跑 15 分钟眼睛早花了。写个简单循环就能自动记录后面做参数对比时直接取数非常省事。注意SGLang 的显存管理支持--mem-fraction-static参数通常是安全默认值不要为了多跑些 KV Cache 把它强行调太高。超过适度值后CUDA 内存碎片会显著增加极端情况下引擎报CUDA OOM的同时还会连带把已占用的显存释放不掉造成假性 OOM。6. 踩坑实录Batch Size 验证里的四个常见问题6.1 并发不是越大越好客户端连接数的坑第一次压测我把客户端并发调到了 256以为这样能压出服务上限结果服务端根本没被打满客户端自己先出问题了。线程创建过多导致上下文切换开销暴涨请求发出的节奏也忽快忽慢整个压测数据都在剧烈抖动曲线跟心电图一样。后来我把压测客户端改成异步方式用信号量控制并发上限并发数和服务端max-running-requests对等或者略微高一点数据才稳定下来。记住一个原则压测瓶颈应该打在服务端而不是客户端自己先成了瓶颈。6.2 预热阶段容易被忽略首轮请求数据不可信SGLang Omni 启动之后RadixAttention 缓存是空的模型的预热也没完成。如果压测一开始就采集数据前面几十条请求的 TTFT 会偏高因为缓存命中率很低每条请求都得从头 Prefill。我一般的做法是先发 50 条请求做预热等缓存和模型状态稳定再清零指标计数器开始正式记录。有条件的话把预热请求尽量选得和数据集中常见前缀相似这样能提前“喂热”缓存后边正式数据里的缓存命中率更接近真实生产情况。6.3 只看吞吐均值容易漏掉性能回退的隐蔽信号有一次我调完参数吞吐均值从 5300 涨到了 6100看起来是大胜利。但一翻 TPOT 分布发现 P99 从 90ms 涨到了 210ms。也就是说大部分请求体验变好了但有一小撮请求却慢了两倍多。这种长尾恶化在总吞吐指标里根本看不出来必须看分位点分布。现在我压测后的第一件事就是画延迟分布直方图先看尾巴尾巴没问题再看均值。如果你用的压测工具不输出分位点那它就不适合做在线场景的性能验证。6.4 调度策略切换后Batch Size 拐点会跟着移动最后这个坑最隐蔽。同样的 Batch Size 参数在 LPM 策略下是合理拐点切到 FIFO 策略之后性能曲线就整体变了。因为不同策略下同一批次请求的执行顺序不同KV Cache 命中率和显存占用峰值都会变。所以调参的顺序必须是先把调度策略定死之后再去调 Batch Size如果策略换了之前定的 Batch Size 基本要重新验证一遍。这也是为什么我不建议“跟着别人博客里抄参数配置”的原因。你抄到的 Batch Size 和 token 预算是在他的请求分布、他的模型、他的调度策略下测出来的最优解换个场景可能直接水土不服。参数本身没有魔法魔法在你有没有一套能快速验证的方法。7. 调度取舍的另外几条思路从调度平台到资源调度7.1 大模型推理服务的调度别只盯着引擎层经常收到类似的问题“我们用的是 SGLang为什么上了生产之后性能还不如压测”我先反问的一句总是你们压测的时候压的只有推理引擎吧生产环境里你前面还挂着负载均衡器、鉴权服务、限流网关后面还有日志采集、向量检索、缓存服务。任何一个环节成为瓶颈推理引擎性能再好也白搭。这类问题可以类比但不仅限于大数据调度平台的思路——有些生产环境里调度任务执行失败后会自动告警并把失败任务转到重试队列这就是“失败重试和降级保护”逻辑。推理服务同样要有相应的限流熔断机制上游请求过载时网关该直接丢弃还是返回排队提示这决定了引擎层会不会被流量突发打死。调度平台里那些“队列管理”“优先级调度”的思路放到推理场景里依旧成立。我处理过一起故障就特别典型。客户侧在高峰期突发 8000 QPS推理服务瞬间被打爆但监控面板上引擎的 GPU 使用率才 70%怎么看怎么不对劲。后来排查链路发现前端网关的线程池先耗尽了大量请求在网关上排队等待转发根本没进到推理引擎里。那时候我才真正体会到全链路压测的必要性。7.2 多机多卡的调度牵扯到另一个领域比单机推理更麻烦的是多机多卡场景。SGLang 支持张量并行TP和数据并行DP多卡时请求会被拆到不同 worker 上这时的调度复杂度直接上升一个量级。请求路由要考虑每张卡的当前负载、缓存命中情况、显存余量。这种场景下经常要用到类似“cp-sat 调度”这样的约束求解思路把“在哪张卡上跑哪些请求”建模成一个资源约束问题。我之前用 OR-Tools 的 CP-SAT 求解器去跑过一个小规模的显卡分配优化帮助排布不同模型副本的放置策略收益确实存在但那个工程量就远不止推理引擎本身了。也就是说调度取舍这件事在不同的资源边界下有不同的解法。单卡单模型是调 SGLang 参数多卡多模型就要上升到调度系统设计再往上到集群层面任务挂起、排队、抢占、多租户隔离已经是完整的分布式调度系统问题了。本文的核心参数验证法适用于第一层更上两层至少需要一套可观测性平台支撑否则参数调优和故障定位都会寸步难行。最后分享一点个人体会。研究“Batch Size 和调度取舍”这件事对我最大的启发是性能验证的本质不是为了找一个“最佳参数”而是为了建立一个“能解释系统如何取舍”的模型。你最终调出来的 Batch Size只是这套模型的一个具体输出值。下次业务方再提出“能不能快一点”的时候你能直接告诉他们瓶颈不在引擎层在请求分布或者瓶颈确实在引擎层但需要加卡而不是调参数。这种判断力才是做性能工作最有价值的部分。