离线模式与服务模式的批构成差异与结论边界

在 **** 的 ****(8 卡,单卡 64GB HBM,卡间互联 392 GB/s)上用 SGLang 起服务。离线跑一次 bench_offline_throughput,把全部请求一次性交给引擎,得到的吞吐高、TTFT 低;换成 bench_serving 走同一个模型、同一组 --max-running-requests,吞吐低于离线值,ITL 的 P95 抬升。反向的情形同样存在:线上服务的稳态吞吐达标,改回离线一次性提交,却因峰值显存不足而启动失败。两次测量的硬件相同,差异来自批怎么构成、请求怎么到达。离线模式把请求当成一个静态批次处理,服务模式按到达逐个拼入动态批次,两者的调度与显存管理不同,同一模型命中的算子路径也可能不同,一处的数字不能搬到另一处。

两种模式的批构成与调度

离线模式的请求一次性提交,批次构成在一次前向内固定,没有到达过程与排队。SGLang 的 benchmark 文档把三种工具区分得清楚:bench_offline_throughput 直接在进程内起 Engine,不走 HTTP 服务端,测的是最大可达吞吐;bench_serving 是异步 HTTP 客户端,按受控速率与可配置并发向运行中的服务发请求,测的是在线指标;bench_one_batch 直接调 ModelRunner、跳过 scheduler,用固定静态批,因为没有动态 batching,真实服务能处理的 batch size 在这里可能直接 OOM。

服务模式走连续批处理(continuous batching),每一步动态拼入与退出请求。传统静态 batching 以批内最大序列长度对齐、浪费算力,连续批处理把粒度做细,代价是调度复杂度与批内序列长度的方差。

维度 离线模式 服务模式
请求到达 一次性提交,无到达过程 按速率到达,有排队
批构成 静态批,批内固定 continuous batching,每步动态拼入/退出
调度 直接跑 Engine,批构成不受调度约束 scheduler 决定批构成,--max-running-requests 限并发
显存管理 整批预占,峰值高 KV 池按 block 动态分配,可抢占与重算
主要指标 吞吐、输入吞吐 TTFT、TPOT / ITL、goodput
长序列处理 常整段 prefill chunked prefill 切块混批

参数与取值

参数 模式 作用
--max-running-requests 服务 并发序列上限,直接决定批规模与 KV 占用
--max-prefill-tokens 两者 单批 token 上限,压平 prefill 峰值
--chunked-prefill-size 服务 长 prefill 切块、与 decode 混批(-1 关闭)
--max-concurrency 压测客户端 受控并发,服务模式取数用
--num-prompts 压测 请求量,服务模式应显著大于并发数(无固定倍数要求)
gpu_memory_utilization 两者 显存预算比例,两种模式都要记

离线模式还有一个隐含参数:提交总量本身就是批规模,没有 --max-running-requests 的节流,峰值由这一批请求的总量决定。

显存与算子路径的连带差异

服务模式的 KV cache 按 block 分配,前缀缓存、抢占(preemption)与重算都在这一层发生;离线整批提交时,KV 占用按批内最长序列与全部请求量预占,峰值出现在 prefill。这决定同一批请求在两种模式下的显存曲线不同:离线看峰值,服务看池子的周转。chunked prefill 相关开关(--chunked-prefill-size、--max-prefill-tokens)在服务模式里用于把长 prefill 切块、与 decode 混批,离线模式常不启用或形同虚设,因此这一开关的收益只在服务模式里有对口负载。

同一模型在两种模式下命中的算子路径可能不同。动态 shape 与动态 batch 会掉出图捕获或后端 guard 的覆盖窗口,退回通用实现;这类回退只在服务模式的动态批下出现,离线静态批命中的是另一条路径。两种情况都要记 resolved_backend(本笔记自设的记录字段,非框架日志字段;真实凭证是启动日志里那一行后端选择):服务模式按「每步实际运行的序列数」记,离线模式按「批固定时的形状」记,两边的命中结果不能互换。

对照要固定哪些量

两种模式的指标不可比,除非对照的工况固定。要固定的量有三组:

三组不齐时,两种模式的差值里混进了负载差异,无法归因到模式本身。

触发条件与现象

定位动作

  1. 固定负载:同一请求集、同一 --random-input-len / --random-output-len 分布、同一 --max-running-requests,两种模式各跑一遍。
  2. 两种模式各取一套分段指标:prefill 段取 TTFT,decode 段取 TPOT / ITL,不看端到端单值。
  3. 对照批构成:记录服务模式下实际运行的序列数与队列长度,确认 --max-running-requests 是否被 KV 池压低。
  4. 对照显存:离线记峰值,服务记池子可用块数与周转。
  5. 核对命中路径:两种模式各记 resolved_backend,确认是否走了同一条实现。
  6. 不外推:离线结果只作离线结论,线上结论用线上工况复测。
# 服务模式:受控速率 + 可配置并发(字段名示意)
python -m sglang.bench_serving \
  --backend sglang \
  --max-concurrency 16 \
  --num-prompts 80 \
  --random-input-len 256 --random-output-len 32

# 离线模式:一次性提交,不走 HTTP 服务端(字段名示意)
python -m sglang.benchmark.offline_throughput \
  --model-path <model-path> \
  --num-prompts 80

服务模式的 --num-prompts 应显著大于并发数(无固定倍数要求),以便测到稳态;离线模式没有到达过程,提交量不构成这个约束,它测的是一次批的全部负载。

记录字段

mode: serving            # serving / offline
batching_mode: continuous # continuous / static
arrival_pattern:         # 固定速率 / 一次性提交
concurrency: --max-running-requests=<值>, --max-concurrency=<值>
batch_tokens: --max-prefill-tokens=<值>
chunked_prefill: <是否启用>, --chunked-prefill-size=<值>
workload:
  input_len: <分布>
  output_len: <分布>
  num_prompts: <服务模式应显著大于并发数>
perf:
  ttft:                  # prefill 段
  tpot:                  # decode 段
  itl_p95:
  throughput:
memory:
  offline_peak:          # 离线峰值
  serving_kv_pool:       # 服务模式池子可用块数
path:
  resolved_backend:      # 两种模式各记一条

小结

离线模式与服务模式在批构成、调度与显存管理上不同,同一模型命中的算子路径也可能不同,一种模式下的指标不能直接搬到另一种模式。判定时两种模式各取一套分段指标再对照:离线记峰值与总吞吐,服务记 TTFT / TPOT / ITL 与队列、池子周转。对照前把请求集、并发与预算三组量固定住,差值才归因得到模式本身。chunked prefill、prefix caching 这类开关只在有对口负载的模式里成立,写结论时必须带模式与批构成。