推理服务为什么一做 Decode 抢占就开始稳尾延迟却丢吞吐:从 Preemptive Scheduling 到 KV Resume 的工程实战
很多团队给大模型推理网关补上Decode抢占后最先看到的是聊天类短请求不再被长输出拖死。⚠️ 真到线上混合流量一压新的问题马上出现了P99稳了整机tokens/s却往下掉GPU利用率也开始跟着抖。Decode抢占真正难的地方不是把某个请求暂停而是暂停后能否带着完整KV回到正确批次。 如果系统只记住request id和当前位置却没保存页表映射、采样状态和恢复优先级所谓抢占只是把长请求切碎再用更差批次重算尾段。图 1最常见的错觉是把“短请求变快”直接等同于“整套推理服务更优”为什么 Decode 抢占最容易把吞吐和公平性一起打乱第一层根因是很多实现只盯等待时长不盯批次重组成本。 短请求一多调度器就持续把 decode 槽位让给高优先级会话长请求因此被切成大量小片段片段越碎连续批次越难合并kernel launch和KV page命中率都会变差。结果是尾延迟改善单卡吞吐却下降。第二层根因出在KV Resume的粒度过粗。 有些系统恢复时只校验 token 偏移不校验页引用、采样器状态和约束解码上下文结果就是请求虽然“恢复成功”却要重新拼页、搬运缓存甚至回退到局部 prefill。 这类开销不会立刻报错却会持续挤压 decode 时间片。图 2真正昂贵的不是暂停动作本身而是恢复时还能不能回到合适的 batch 形状一套更稳的 Preemptive Scheduling 与 KV Resume 治理链路更稳的做法通常是把抢占从“紧急插队”改成“可恢复的有界抢占”。 调度器先计算短请求等待收益再估算被打断请求的恢复代价只有当收益明显高于代价且目标请求具备可验证的resume lease时才允许执行抢占。✅ 这样优化的不只是单次响应而是整条队列的总完成效率。策略交互请求 P99单卡 tokens/s长请求重排次数只做 FIFO1820 ms1510激进抢占1080 ms12611有界抢占 KV Resume1160 ms1453defshould_preempt(short_req,running_req,queue_state):gainestimate_wait_gain(short_req,queue_state)costestimate_resume_cost(running_req.kv_pages,running_req.sampler_state)ifnotrunning_req.resume_lease.valid:returnFalseifrunning_req.decode_step16:returnFalsereturngaincost*1.35这次回放了2 x H20上的64条混合请求分别包含客服问答、函数调用和长摘要任务。 只做激进抢占时交互类请求的确最快但恢复队列会持续堆积长请求平均被重排11次补上resume lease、页引用复用和恢复批次重组后P99只回退80 ms吞吐拉回15%。️图 3抢占收益必须和恢复成本一起记账否则就是把延迟从前台挪到后台真正该管理的不是优先级而是可恢复性契约很多团队把这类问题归因于模型大、显存紧或流量脏实际更关键的是调度契约没有定义“什么请求值得被打断、打断后如何恢复、恢复时谁先回批”。⚙️ 抢占不是免费的QoS开关如果恢复路径不可证系统只是把成本藏进队列里。更成熟的做法是把交互请求和批量请求分成不同车道再让Decode抢占只发生在具备KV Resume能力的会话之间。⭐ 这样既能保住前台对话的尾延迟也不会让长任务无限次被切碎。对函数调用、结构化输出和多租户场景尤其如此因为这类请求一旦恢复错位代价往往不只是慢。图 4真正稳的抢占系统返回的不只是更低延迟还要返回可恢复、可审计的调度证据未来 3 到 6 个月推理调度会从“能抢占”转向“能证明恢复”未来3到6个月推理平台的分水岭不会是谁先把抢占做进调度器而是谁先把resume hit ratio、copied KV bytes和恢复后首轮批次命中率做成默认指标。 只有当系统能证明一次抢占不会把长请求拖入碎片化恢复Decode抢占才算真正可上线。