27届大模型面试准备(六十六):大模型推理调度与弹性扩缩容工程------排队、优先级、抢占与弹性
引言
前面(五十九)讲了推理引擎内核、(五十四)讲了成本与吞吐、(六十一)讲了可观测性。本文站在它们之上讲"调度层":当一堆请求涌进来,引擎内部怎么排队、怎么决定谁先跑、GPU 不够了怎么弹性扩缩、高优请求怎么不被长尾拖死。这是推理服务从"能跑"到"生产级稳"的关键一跳。
调度不直接产生 token,但决定了尾延迟、GPU 利用率和 SLA 达标率。下面给调度模型、优先级策略、弹性方案与代码,结尾给速答。
一、为什么调度是独立的一层
无调度(朴素 FIFO)的问题
请求到达 -> 排队 -> 依次处理
问题: 短请求被前面长请求阻塞; 高优请求和闲聊同权;
GPU 利用率低(部分批次不满); 峰值打满后直接拒绝
生产需要:① 高优请求低延迟;② GPU 高利用率(批处理拼车);③ 峰值能弹性扩容;④ 失败能降级不雪崩。
二、请求调度模型
单实例调度层次
[入口网关] -> [全局队列(按优先级/SLA分层)] -> [实例调度器]
|
连续批处理(continuous batching): 迭代间动态拼批
+ 优先级抢占(preempt): 高优可插队
+ token budget: 限制单请求最大 token 防饿死
2.1 连续批处理(回顾 + 调度视角)
(五十九)讲过 continuous batching:不等整批结束,每 decode 步动态把新请求加入、结束的移出。调度层要配合:新请求来了立刻进当前步的批,而不是等下一批。
2.2 优先级与抢占
短请求(分类)不应被长请求(长文生成)饿死。两种策略:
-
优先级队列:高优请求排前面,先调度。
-
抢占(preemption):长请求占用 GPU 时,若高优到来,把低优的 KV 换出(swap to CPU)腾位给高优,高优完成再换回续跑。
抢占示意
GPU 在跑 [长请求A(剩余多)]
高优[短请求B] 到达 -> 抢占A(KV换出CPU) -> 跑B(快完) -> 换回A续跑
结果: B 尾延迟从秒级降到百毫秒级, A 仅多一次 swap 开销
代码(优先级队列骨架):
python
import heapq
class Scheduler:
def __init__(self):
self.pq = [] # (-priority, seq, req)
self.seq = 0
def submit(self, req, priority=0):
self.seq += 1
heapq.heappush(self.pq, (-priority, self.seq, req))
def pop(self):
if not self.pq:
return None
_, _, req = heapq.heappop(self.pq)
return req
# 抢占: 当高优到达且 GPU 满, 选最低优先级运行中请求 swap_out
2.3 Token budget 防饿死
给每个请求设最大 decode 步数 / 最大等待时间,超时则降级或换出,防止单个超长请求长期占卡。
三、SLA 分层与队列隔离
SLA 分层(多租户/多业务)
tier0 高优(付费/实时): 独立队列 + 预留算力, P99 严
tier1 普通: 共享队列, 尽力而为
tier2 离线/批量: 闲时算力, 可抢占
隔离: 每 tier 独立队列 + 算力配额, 避免互相拖累
类比(五十七)成本治理:不同 tier 配不同预算与降级策略。高优预留少量常驻实例,普通/离线用弹性实例。
四、弹性扩缩容
GPU 贵,不能常驻峰值。按负载弹性伸缩:
弹性伸缩信号
- 队列长度(积压请求数)
- GPU 利用率(低则缩, 高则扩)
- P99 延迟(超 SLA 则扩)
- 吞吐(tokens/s)趋势
缩容风险: 正在跑的请求怎么办?
策略: 缩容只摘"空闲实例"; 繁忙实例等其请求排空再摘(drain)
扩容: 新实例加载权重需时间(冷启动秒级~十秒), 用预热 + 冗余扛抖动
代码(基于队列长度的扩缩决策):
python
def decide_replicas(qlen, cur, min_r, max_r):
if qlen > 50 and cur < max_r:
return cur + 1 # 积压多, 扩容
if qlen < 5 and cur > min_r:
return cur - 1 # 空闲, 缩容(需 drain 保护)
return cur
冷启动优化:① 权重放本地 NVMe 而非远程存储;② 多实例共享同一份模型权重(pinned memory + copy-on-write);③ 预热实例(warm pool)应对突发。
五、批处理拼车与利用率
利用率优化
- 动态 batch size: 按显存剩量塞更多请求
- chunked prefill: 长 prefill 切块, 与 decode 交错, 降 TTFT 毛刺
- 请求对齐: 相似长度请求拼一批, 减少 padding 浪费
chunked prefill(vLLM/SGLang 支持):把长输入的 prefill 切成多块,每块之间穿插 decode 步,避免"一个长 prefill 卡住整批 decode",显著降低 TTFT 长尾。
六、限流、排队与优雅降级
过载保护
- 入口限流: 超容量返回 429(带 Retry-After), 而非雪崩
- 队列上限: 超出丢弃或转异步(呼应 B54 实验)
- 降级: 高负载时切小模型(呼应 B64 路由)/关非必要功能
- 背压: 下游满则上游减速, 全链路不堆内存
关键:限流要在网关层(B64)做,调度层做队列与抢占,两层配合。限流不是"拒绝",是"保护 SLA 下的有序服务"。
七、可观测:调度也要看得见
调度层必看指标(接六十一面板)
- 队列长度/排队等待时间(P50/P99)
- 各 tier 的 TTFT/TBT/TPS
- 抢占次数/swap 开销
- 实例数/GPU 利用率/扩缩事件
- 限流拒绝率/降级触发率
调度出问题(如某 tier 饿死、抢占风暴)能立刻从面板定位。
面试速答
问:连续批处理和优先级抢占什么关系?
答:连续批处理让新请求每步动态入批;优先级抢占让高优可把低优 KV 换出插队,两者配合实现"高利用率 + 低尾延迟"。
问:缩容时正在跑的请求怎么办?
答:只摘空闲实例;繁忙实例进入 drain(等请求排空再摘),避免中断在途请求。
问:chunked prefill 解决什么?
答:长 prefill 会卡住整批 decode 抬高 TTFT 毛刺;切块后与 decode 交错,降长尾。
高频追问清单
- 连续批处理为什么比静态批处理利用率高?
- 抢占的 KV 换出换入开销多大?什么场景值得?
- token budget 怎么设才不会误杀长请求?
- SLA 分层各 tier 算力怎么预留与隔离?
- 弹性扩容冷启动怎么从秒级降到百毫秒?
- 限流 429 和队列等待怎么配合?
- chunked prefill 和 continuous batching 如何协同?
- 多模型(B64)下调度怎么跨模型池分配?
- 调度层指标怎么和(六十一)可观测面板接?
- 突发流量下预热实例(warm pool)怎么定规模?