推理引擎 Batch 调度:吞吐提升不能牺牲尾延迟

📅 2026/7/5 7:43:41
推理引擎 Batch 调度:吞吐提升不能牺牲尾延迟
推理引擎 Batch 调度吞吐提升不能牺牲尾延迟一、Batch 不是越大越好大模型推理服务里Batch 调度是提升吞吐的常见手段。多个请求合并执行可以提高 GPU 利用率减少 kernel launch 开销。但 Batch 不是越大越好。等待凑批会增加排队时间大请求会拖慢小请求尾延迟很容易被吞吐指标掩盖。推理调度器要同时看吞吐、首 token 延迟、总延迟和公平性而不是只追求显卡跑满。二、先拆推理排队路径flowchart TD A[请求进入] -- B[队列分级] B -- C[等待凑批] C -- D[Prefill] D -- E[Decode 循环] E -- F[流式返回]Prefill 阶段受输入长度影响大Decode 阶段受输出长度和并发序列数影响大。把所有请求无脑放进一个队列会让短请求被长上下文请求压住。batch_scheduler: max_batch_tokens: 8192 max_wait_ms: 15 queues: short_context: 2048 long_context: 8192max_wait_ms是关键参数。等待太久首 token 延迟会变差等待太短Batch 利用率上不去。三、调度要按 token 预算struct RequestMeta { prompt_tokens: usize, max_new_tokens: usize, arrival_ms: u64, } fn cost(req: RequestMeta) - usize { req.prompt_tokens req.max_new_tokens }按请求数凑批很粗糙。一个 200 token 的请求和一个 8000 token 的请求对显存和计算的压力完全不同。更合理的做法是按 token 预算、KV cache 占用和预计 decode 步数做组合。还要限制单个租户或单类请求占满队列。否则某个批处理任务会把在线对话的尾延迟打穿。Token 预算调度的一个进阶实现是动态预算分配不是给每个 batch 固定上限而是根据当前队列中请求的 prompt 长度分布动态调整窗口大小。当队列里短请求居多时缩小预算上限、加快轮转频率让短请求快速出队当长请求堆积时分配专用的长请求 batch 窗口同时用更高频率的短请求轮转穿插其间避免长请求完全吃满显存导致小请求饥饿。在 Rust 实现中可以用BinaryHeap维护请求的 token 预算优先级每次 schedule 先尝试填满当前 budget超出部分流到下一轮。对于流式输出的 decode 阶段token 预算需预留弹性空间——decode 逐步产生 token每步是否重排 batch 取决于 concurrent sequences 的 KV 占用是否超出上限而不是简单地按静态 token 数判断。四、尾延迟要进反馈回路调度器不能只配置一次就结束。线上流量会变化prompt 长度分布、输出长度、模型版本和硬件负载都会变。调度策略要根据指标反馈动态调小或调大凑批窗口。latency_guard: p95_first_token_ms: 800 p99_total_ms: 12000 shrink_batch_when_violate: true当 P95 首 token 延迟超阈值时调度器可以降低max_wait_ms或拆分长上下文队列。吞吐下降一点换来用户可感知延迟稳定通常是值得的。还要观察饥饿问题。长请求如果一直被短请求抢占也会超时。可以给每个请求记录等待时间超过阈值后提升优先级。公平性不是平均分资源而是避免任何一类请求长期得不到服务。最后压测要构造混合流量。只用固定长度 prompt 测 QPS无法反映真实调度压力。短问答、长文档总结、代码生成、批量任务混在一起才能看出 Batch 策略是否可靠。调度器还要把决策过程写进观测指标。一次请求为什么进入长上下文队列等待了多久最终和哪些请求组成 batch是否因为 token 预算被拆分这些信息要能回放。否则线上尾延迟升高时只能猜是模型慢、队列慢还是调度策略慢。scheduler_trace: record_queue_name: true record_batch_wait_ms: true record_batch_token_sum: true sample_rate: 0.05五、总结推理引擎 Batch 调度要围绕 token 预算、队列分级、凑批等待、尾延迟和公平性设计。吞吐提升很重要但不能以尾延迟失控为代价。调度器真正的价值是让硬件效率和用户体验同时站住。