模型推理为什么一上 Continuous Batching 就开始吞吐更高却尾延迟更差:从 Queue Slice 到 Prefill Budget 的工程实战
吞吐抬上去之后用户体感不一定更好很多团队把在线推理从静态批切到Continuous Batching后先看到卡利用率和TPS一起上升。 于是它很容易被当成默认配置好像批次不断流延迟和成本都会一起变好。上线后常见另一面混合流量一上来短问答先变慢工具回调都在等首包。图 1吞吐变高只说明批次更满不代表每类请求都受益问题不是模型突然变慢而是调度器把“谁先进批次”和“谁先占算力”混成一件事。⚠️ 只盯平均吞吐时这点容易被遮住因为长请求塞满批次后账面tokens/s仍好看变差的是短请求等待和p95首包延迟。 真正被放大的不是并发本身而是 prefill 和 decode 的争抢Continuous Batching同时承接两类不同负载prefill一次吞下整段提示词decode则是很多轮很短的小步前进。✅ 若放在同一把尺子下排队长提示词就更容易抢到大块时间片因为它带来的是整段上下文装载、缓存分配和注意力计算。图 2长提示词进入批次时短请求常被迫等它先完成大段 prefill这个冲突在真实业务里更明显。 一旦系统提示变长、RAG拉回多段文档或工具返回大块JSON某个新请求的prefill就会吞掉多轮decode的机会。结果不是所有请求都慢一点而是短请求被几条长请求卡在入口回复忽快忽慢。⚙️ Queue Slice 管顺序Prefill Budget 管上限更稳的做法是承认prefill和decode不是一种负载再拆成两层调度。Queue Slice给请求标记阶段确保已进入decode的请求不被新来的长提示词轻易挤掉Prefill Budget限制单个周期最多吃掉多少prefill tokens避免一轮塞进太多长上下文。图 3把阶段拆开调度后系统才知道该先保谁的连续性关键不是把长请求挡在外面而是给它们设上限。 当本轮prefill budget吃满新来的长提示词就留到下一轮当前批次里的decode请求继续跑短请求不会一直被大块上下文拖住。吞吐会回落一点但尾延迟和体感立刻变好。defnext_batch(queue,cycle):batch[]used_prefill_tokens0forreqinqueue:ifreq.stagedecode:batch.append(req)continuenext_usedused_prefill_tokensreq.prompt_tokensifnext_usedcycle.prefill_budget:batch.append(req)used_prefill_tokensnext_usediflen(batch)cycle.max_requests:breakreturnbatch 发布门槛别只看吞吐还要看短请求有没有被拖住如果评测只看总吞吐和平均延迟调度问题暴露不出来。 更值得监控的是三笔账短请求多久进入首轮计算prefill吃掉多少 token 配额以及decode连续多少轮没被打断。只要这三项恶化就说明服务正在用短请求体验给长请求吞吐让路。图 4真正该看的不是平均值而是不同阶段是否在抢同一笔预算策略总吞吐TPSp95首包延迟短请求超时率常见现象固定批次2282.1 s3.9%资源利用率偏低但波动可控只开Continuous Batching2622.8 s5.2%吞吐更高短请求更容易被长提示词拖住Queue SlicePrefill Budget2551.7 s1.5%吞吐只回落一点尾延迟明显更稳这组对比里不是第三种方案吞吐略低而是它止住了最贵的损失。 在线推理里用户不会因为后台多跑几次长prefill就投诉却会因为短请求连续排队直接感知“系统卡了”。把预算先分给连续性再追满卡通常比只追峰值更适合生产。 接下来拉开差距的不是谁先开这个开关笔者认为未来 3 到 6 个月推理服务真正拉开差距的不是谁先把Continuous Batching接上而是谁先把调度做成“阶段有边界、预算有上限、指标能回证”的系统。 同样一张卡、同样一个模型若调度器还把所有请求当成同一种 token 流比拼的就不是推理能力而是谁更能忍受尾延迟抖动。如果当前链路还只有一条总队列和一个max_batch_tokens它大概率已在高峰时段悄悄偏向长提示词。✅ 真正值得先补的是Queue Slice、Prefill Budget和分阶段面板而不是继续把平均吞吐当成唯一发布门槛。你们现在的推理监控已经把prefill等待和decode等待分开记账了吗