多租户 LLM 推理公平调度生产实战:用 Token Cost Accounting、VTC 与 Priority Lanes 治理 Noisy Neighbor
为什么共享 GPU 最先暴露的不是吞吐,而是公平性
企业把多个应用、部门或客户租户接入同一个大模型推理集群后,最常见的第一版治理方式是「每个租户每秒最多 N 个请求」。这对普通 HTTP 服务通常有效,但对 LLM 并不充分。
原因很直接:一个请求并不等于一份固定成本。一个 200-token 输入、50-token 输出的问答请求,与一个 20K 上下文、继续生成 2K token 的分析请求,都只会被 QPS 计数器记成「1 次」。如果一个租户持续提交长上下文或长生成任务,它完全可能在不违反请求数限制的情况下占据大部分 GPU 服务时间。
OSDI 2024 的《Fairness in Serving Large Language Models》正是从这个问题出发:LLM 请求长度不可预测,而且推理引擎会把多个请求动态组合执行,因此传统按请求数或固定时间片理解公平性并不合适。论文使用考虑输入、输出 token 的成本函数定义服务量,并提出 Virtual Token Counter(VTC) 做工作保持型公平调度。
Microsoft Research 的 FairServe 进一步从多租户实际工作负载出发指出,不同应用不仅 token 长度差异明显,还可能在一次业务请求中触发不同数量的 LLM 调用。因此,生产治理需要把 throttling、服务成本计量和调度 放在一起,而不是只在入口加一个 QPS 限流器。
核心原则:公平的对象应该是「已消费服务量」
生产系统需要区分三个经常混在一起的概念:
| 概念 | 含义 | 解决的问题 |
|---|---|---|
| Quota | 一个租户在某个时间窗口最多能使用多少资源 | 防止单租户无限制消耗 |
| Priority | 资源紧张时,哪个业务等级应该先执行 | 表达业务等级先后 |
| Fairness | 同一资源池里,多个活跃租户如何按约定份额分享服务能力 | 共享容量下的份额公平 |
这三者不能互相替代。
用 Token Cost Accounting 代替纯请求计数
最简单可落地的服务成本模型可以写成:
service_cost = a * prompt_tokens + b * completion_tokens
其中 a、b 是平台自己的计量系数。它不是精确的 GPU 成本模型,而是用于调度和配额的一致性近似。不同模型、不同硬件、不同上下文长度下真实计算成本并不完全线性,因此生产上应把「公平计量单位」和「财务计费单位」分开。
如果系统已经能拿到请求结束时的 usage,可以采用「入队预估 + 完成后校准」:
estimated_cost = prompt_tokens + expected_output_tokens
actual_cost = prompt_tokens + completion_tokens
debt_delta = actual_cost - estimated_cost
预估值用于 admission 和排队,实际值用于修正租户的 service counter。这样既不需要在生成结束前知道准确输出长度,也避免长期低估大生成请求。
VTC 的关键价值:让空闲容量不被白白浪费
固定配额常见的问题是:A 租户今天没流量,它的额度也不能被 B 临时使用;结果 GPU 明明空闲,B 仍被限流。
VTC 这类 work-conserving 调度方法的价值,是在存在空闲资源时允许其他活跃租户继续消费,同时在多个租户都持续有积压时,依据各自已经获得的服务量恢复公平。
生产实现不必把研究算法原封不动塞进模型引擎。更现实的做法是,在网关或独立 scheduler 中维护每个租户的虚拟服务计数器:
def choose_tenant(backlogged_tenants):
return min(backlogged_tenants, key=lambda t: t.virtual_service / t.weight)
def on_request_finished(tenant, prompt_tokens, completion_tokens):
cost = prompt_tokens + completion_tokens
tenant.virtual_service += cost
这段伪代码只表达核心思路:优先选择「归一化后已经拿到较少服务」的租户。真正上线时还要处理新租户加入、长时间空闲后的 counter 对齐、请求取消、流式生成、权重变化和跨副本状态同步。
Priority 不是 Fairness:两层调度要分开
vLLM 当前支持 fcfs 和 priority 调度策略,请求也可以通过 priority 字段或 X-Vllm-Priority 头传入优先级,数值越低越早处理。
这非常适合实现「在线聊天高于离线总结」「付费交互高于后台批处理」之类的 QoS lane,但不要把它直接当成租户公平调度。例如:
P0: interactive-critical
P1: interactive-standard
P2: async-batch
正确的组合方式是:
先按业务等级选择 Priority Lane
↓
在同一 Lane 内按租户做 Weighted Fair Scheduling / VTC
↓
选出请求
↓
映射为模型引擎可理解的 priority
如果只有 strict priority,而没有 lane 内公平,一个持续发请求的高优先级租户仍然可能压制同等级的其他租户;如果只有公平调度,又无法表达「在线关键业务必须优先于夜间批量摘要」这种业务优先级。
一套可落地的多租户推理调度链路
推荐把职责拆成五层。
1. Tenant Identity:先把租户身份做成强约束
请求进入推理网关后,必须先解析出稳定的:
tenant_id application_id service_tier model request_id
不要直接信任客户端传入的 priority。优先级应该由平台根据租户、产品套餐和业务场景映射,否则调用方可以自行把所有请求标成最高优先级。
2. Admission:先判断「能不能进队列」
Admission 层负责硬边界:
- 最大并发请求数;
- 最大排队深度;
- 每分钟 / 每小时 token budget;
- 单请求最大输入与最大输出;
- 租户 burst budget;
- 全局 overload 时的降级或拒绝策略。
这里解决的是「不能让一个租户无限制进入系统」,不是最终的公平排序。
3. Fair Queue:按租户维护服务债务
建议每个 model + priority_lane 建一个逻辑公平队列,队列里再按 tenant 选择下一批可调度请求。租户状态至少包含:
tenant_id: tenant-a
weight: 2
virtual_service: 183420
queued_requests: 12
estimated_queued_tokens: 9450
running_requests: 3
token_budget_remaining: 280000
weight 可以映射套餐或内部业务权重。权重为 2 并不意味着「永远比权重 1 快一倍」,而是表示在持续竞争时目标服务份额更高。
4. Engine:把平台决策映射给 vLLM 等推理引擎
如果引擎支持请求优先级,可以把平台的 lane 映射为 engine priority。vLLM 当前文档明确支持 --scheduling-policy priority,OpenAI-compatible API 也支持请求级 priority。
但平台仍应保留自己的租户级排队与计量。原因是引擎看到的是「请求」,平台看到的才是「租户、套餐、预算、应用和业务 SLA」。
Kubernetes Gateway API Inference Extension 也在推理网关层引入了 serving priority、InferencePool 和 Endpoint Picker 等抽象,说明越来越多的调度决策正在从普通 L7 负载均衡升级为 inference-aware routing。这类能力适合承担 endpoint selection,而租户公平政策仍应由平台策略层明确控制。
5. Metering:完成后回写实际服务量
每个请求完成、取消或超时后,至少记录:
{
"tenant_id": "tenant-a",
"model": "qwen3-32b",
"priority_lane": "interactive-standard",
"prompt_tokens": 1820,
"completion_tokens": 436,
"queue_ms": 84,
"ttft_ms": 312,
"e2e_ms": 2410,
"finish_reason": "stop"
}
调度系统最重要的不是单个请求日志,而是能按租户聚合出:
service_tokens / minutequeued_tokensrunning_tokensp95 queue timep95 TTFTthrottled_requestsrejected_requestsfairness debtbudget utilization
这样才能判断「慢」到底来自模型本身、队列竞争,还是某个租户持续吃掉共享容量。
生产上推荐采用「双预算 + 公平队列」
单一 token bucket 很难同时满足突发和长期公平。更实用的是两套预算:
短周期 Burst Budget:例如按 10 秒或 1 分钟控制短时突发,避免瞬时把队列打满。
长周期 Sustained Budget:例如按 1 小时或 1 天控制累计 token 使用量,防止某租户长期超额。
两者之外,再让 VTC / weighted fairness 决定已经被允许进入系统的请求如何分享当前 GPU 容量。因此三层含义很清楚:
- Rate Limit / Quota:你最多能用多少;
- Fair Scheduler:大家都要用时怎么分;
- Priority Lane:不同业务等级谁先。
哪些场景最适合这套方案
第一类是企业内部共享模型平台。 研发助手、客服、知识问答、批处理任务共用 GPU 时,QPS 差异并不能代表真实成本。
第二类是 SaaS 多租户模型服务。 不同客户套餐需要不同资源权重,但平台又希望空闲容量可以被其他客户充分利用。
第三类是 Agent 平台。 一次用户操作可能触发多次模型调用,单纯统计入口 HTTP 请求会严重低估实际推理服务量。FairServe 对「应用间 LLM 调用次数不同」这一问题的讨论尤其值得参考。
第四类是在线与离线任务混跑。 此时 priority lane 很重要,但 lane 内仍需要公平策略,避免高优先级池内部出现 noisy neighbor。
常见误区
误区一:每租户 10 QPS 就公平了。 不是。请求长度、生成长度和一次业务操作触发的模型调用数都可能不同。QPS 只能表达请求频率,不能表达实际服务量。
误区二:直接按 max_tokens 扣额度。 max_tokens 是上限,不是实际生成量。如果长期按上限结算,会系统性惩罚设置了大上限但通常很早停止的业务。更适合做 admission 预留,完成后再按实际 usage 结算。
误区三:最高付费等级直接永远最高 priority。 严格优先级如果没有 starvation protection,可能让低等级任务长期得不到服务。更稳妥的是「有限数量的 lane + lane 内加权公平 + 每个 lane 的容量保护」。
误区四:只看 GPU utilization。 GPU 利用率高只能说明机器忙,不代表租户公平。必须同时看各租户的服务份额、排队时间、拒绝率和 token debt。
误区五:公平调度一定会降低吞吐。 公平性与吞吐确实可能存在权衡,但不能简单等同于「公平 = 低吞吐」。VTC 的设计目标本身就是 work-conserving;工程上真正需要压测的是队列策略是否破坏 batching、prefix locality 和 GPU 饱和度,而不是先假设公平调度必然低效。
上线检查
上线前至少完成以下检查:
- 租户身份是否只能由可信网关写入;
- 是否同时限制请求数、并发数和 token budget;
- prompt token 是否能在入队前准确计算或可靠估算;
- completion token 是否在结束、取消、超时时正确结算;
- 新租户加入和长时间空闲后,virtual counter 是否会造成「信用囤积」;
- weighted fairness 的权重修改是否支持平滑生效;
- priority lane 是否设置 starvation protection;
- 流式生成断开后是否仍能回收 running slot;
- scheduler 状态丢失时是否有明确的 fail-open / fail-close 策略;
- 是否对单租户 p95 queue time、TTFT、throttle rate、token share 建立监控;
- 是否用短请求、长上下文、长生成、Agent 多调用四类流量做混合压测;
- 是否验证公平策略不会显著破坏 batching 效率。
FAQ
为什么不直接使用传统 Weighted Fair Queuing? 传统网络调度通常知道数据包大小,而 LLM 输出长度在开始生成时并不知道,同时 continuous batching 会让多个请求在 GPU 上交织执行。因此可以借鉴 WFQ 的「服务份额」思想,但计量、校准和调度时机必须适配 token 逐步生成的特点。
VTC 是否必须修改 vLLM 源码? 不一定。研究型实现可以深入 engine scheduler;企业平台更容易先在外部 gateway/scheduler 做租户队列和 admission,再把选中的请求送给 vLLM。只有当外部排队无法获得足够细粒度的生成进度或导致 batching 效率明显下降时,再考虑下沉到引擎层。
配额和公平调度哪个应该先做? 先做硬配额与 admission,再做公平调度。没有硬边界时,公平调度只能决定竞争顺序,无法阻止错误配置或恶意租户无限制造排队请求;只有配额没有公平调度,则会在共享容量争用时出现体验不稳定。
参考资料
- Ying Sheng et al., Fairness in Serving Large Language Models, OSDI 2024 — https://www.usenix.org/conference/osdi24/presentation/sheng
- Redwan Ibne Seraj Khan et al., Ensuring Fair LLM Serving Amid Diverse Applications (FairServe), Microsoft Research / arXiv, 2024 — https://www.microsoft.com/en-us/research/publication/ensuring-fair-llm-serving-amid-diverse-applications/ 与 https://arxiv.org/abs/2411.15997
- vLLM, OpenAI-Compatible Server — request priority / X-Vllm-Priority — https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/
- vLLM, Serve CLI —
--scheduling-policy=fcfs|priority— https://docs.vllm.ai/en/latest/cli/serve/ - Kubernetes SIG Network, Gateway API Inference Extension — Introduction — https://gateway-api-inference-extension.sigs.k8s.io/
- Kubernetes SIG Network, Gateway API Inference Extension — InferencePool — https://gateway-api-inference-extension.sigs.k8s.io/api-types/inferencepool/