模型加载完后,即使一条请求也没有,显存占用也可能很高。反过来,显存还没满,排队的长提示已经能把首字延迟拖垮。用一个显存百分比控制扩容,很容易在这两种场景里做反决定。
10 月 19 日 vLLM 的 Elastic EP 介绍了把背景准备与拓扑提交分开的做法。它提醒我们:扩容命令发出,与新容量真正可以接单,是两个时间点。新增实例正在加载权重时,不能把它当作已经可用的吞吐。Elastic EP 原文
先定义还缺多少可用容量
至少需要同时看三类量:尚未处理的工作、缓存余量、已就绪的执行资源。队列请求数不够精确,十条短请求和十条十万 Token 提示不是同一份工作。排队 Token 仍只是近似,还要区分 prefill、decode、模型与服务等级。
图里的服务器数量与柱形只是概念示意。下面给出一个离线策略函数:阈值全是教学参数,用来说明“已申请”不能重复计为“缺口”。
from dataclasses import dataclass from math import ceil @dataclass(frozen=True) class Signals: queued_tokens: int free_kv_blocks: int ready: int warming: int def desired(s, tokens_per_worker=1000, kv_reserve=10, maximum=8): values = (s.queued_tokens, s.free_kv_blocks, s.ready, s.warming) if any(x < 0 for x in values) or tokens_per_worker <= 0: raise ValueError("invalid signal") if maximum < s.ready + s.warming: raise ValueError("capacity exceeds configured maximum") extra = ceil(s.queued_tokens / tokens_per_worker) if s.free_kv_blocks < kv_reserve: extra = max(extra, 1) target = min(maximum, max(1, s.ready + extra)) launch = max(0, target - s.ready - s.warming) return target, launch assert desired(Signals(2500, 20, 2, 1)) == (5, 2) assert desired(Signals(0, 50, 2, 0)) == (2, 0) assert desired(Signals(0, 5, 2, 1)) == (3, 0) print("target and new launches:", desired(Signals(2500, 20, 2, 1)))这个函数只计算扩容建议,没有自动缩容。当前实例数与瞬时队列共同决定目标值,队列抖动会让建议抖动;生产控制器还需要观测窗口、冷却时间、最大变化率,以及对长时间预热失败的处理。tokens_per_worker不是显卡的固定额定值,应来自该模型和负载下的可持续处理能力。
指标没有值,不能解释成没有负载
如果用 Prometheus 驱动扩缩容,查询必须先聚合成控制器可消费的单值,并明确缺失序列如何处理。KEDA 2.18 的 Prometheus scaler 文档解释了单元素结果、阈值与空结果的相关选项;这是已归档版本的机制参考,部署时应核对实际安装版本。
监控断流时直接得到零,会把故障误认为空闲。新实例数增加、就绪数不增,则可能是模型加载、配额或健康检查出了问题,继续指数式申请实例未必有用。缩容更需要单独路径:停止接单、等待在途请求、迁移或放弃可重建缓存,最后再释放资源。
最后要把“目标实例数”与用户结果对账。若队列下降只是因为大量请求被拒绝,不能把它记成扩容成功;若预热占满网络使旧实例延迟变差,也需要降低准备并发。本文没有连接 Kubernetes、KEDA 或推理集群,代码只验证单次建议的算术和输入约束,不能据此承诺扩容速度或 SLO。