推理服务为什么用户都断开了 GPU 还在忙:从 cancel propagation 到幽灵解码清理的工程实战
用户都关页面了为什么显卡还在持续发热流式推理一旦进入生产最让运维困惑的场景之一就是前端早已超时、客户端连接也已经断开GPU 利用率却没有立刻回落。很多团队会先怀疑continuous batching太激进或者怀疑模型输出太长但真正被忽略的往往是取消信号根本没有穿透到调度和解码层。⚠️ 请求在网关上看似结束在 Worker 内部却还保留着可运行状态结果就是队列继续排、KV 槽位继续占、decode loop 继续吐 token。图 1取消信号没有贯穿全链路时推理任务会在后台继续跑更麻烦的是这类浪费不会像 OOM 那样立刻炸出来而是慢慢污染整池吞吐。 少量幽灵请求混进批处理后调度器仍会给它们分配 step导致真实在线请求被迫共享时间片。表面上看QPS没掉很多TTFT和P99却开始持续抬升如果系统还开启了重试客户端断连造成的无效解码会和新请求同时争抢资源尾延迟会被进一步放大。 真正的问题不是有没有 cancel而是 cancel 停在哪一层线上取消链路通常至少经过网关、调度器、Worker 和模型执行器四层。很多实现只在 HTTP 或 SSE 断开时打日志却没有把取消状态写进共享任务表结果调度器仍然认为这个请求“可继续推进”。 一旦任务已经拿到 KV page 或进入 decode micro-batch后面的执行器如果缺少中断检查点就会把整轮 token 生成跑完才释放资源。此时最浪费的不是一个请求而是整批被拖住的 slot。⚙️图 2任意一层没有确认取消都会把无效计算继续传下去下面这组线上观测值很能说明问题。 当取消率超过 8% 但释放时延仍维持在秒级时吞吐损失往往不是偶发噪声而是服务治理缺口。策略取消到释放时延无效 decode 占比P99 变化主要症状只在网关断连2400 ms11.8%27%Worker 仍继续跑调度层感知取消910 ms5.1%12%已入批次任务仍滞留Decode step 检查中断260 ms1.7%4%残余浪费可控心跳 令牌双门禁140 ms0.8%1%最稳适合生产️ 更稳的做法是把取消做成可观测的资源回收协议可靠的实现通常会同时做三件事。✅ 一是给每个请求下发独立cancel_token让网关断连、上游超时和业务主动撤销都映射到同一个状态源二是在调度器和 decode loop 都加轻量检查点避免任务进了执行层就只能“跑完再说”三是把 KV page、输出缓冲和 slot 占用挂到同一个回收流程里而不是只停生成、不做资源归还。️ 这样做的重点不是优雅取消而是尽快把真实容量还给下一批请求。图 3观察取消积压和无效 decode 占比比只看 QPS 更容易定位问题defon_decode_step(req,runtime):ifruntime.cancelled(req.request_id):runtime.release_kv(req.request_id)runtime.release_slot(req.request_id)returnstoppedtokenruntime.decode_one_token(req)runtime.flush_if_needed(req,token)returnrunning 接下来 3 到 6 个月取消治理会成为推理平台的基础能力很多团队过去把取消当成“体验优化”实际上它更接近容量治理。 只要模型服务继续朝流式输出、长上下文和多租户方向发展幽灵请求带来的资源损耗就会越来越像隐性税负。笔者认为下一阶段更值得投入的不是把取消写成更多异常分支而是把cancel_backlog、zombie_decode_ratio、release_latency_ms做成上线门禁让回收效率和吞吐、质量一样被持续审计。图 4更成熟的推理平台会把取消、回收和容量门禁联成一条链推理服务里最浪费的算力常常不是慢请求本身而是那些用户已经放弃、系统却还在继续计算的请求。 谁能先把取消信号做成端到端协议谁就更可能在同样的 GPU 预算下拿到更稳定的吞吐和更低的尾延迟。你们线上更常见的问题是取消不生效还是资源释放总是慢半拍欢迎在评论区交流。