文章

多租户 LLM 推理公平调度生产实战:用 Token Cost Accounting、VTC 与 Priority Lanes 治理 Noisy Neighbor

面向共享 GPU 的多租户大模型推理服务,系统说明为什么按请求数限流不足,并给出基于 Token 成本计量、VTC 公平调度、优先级通道与租户配额的落地方案,兼顾吞吐、隔离与尾延迟。

多租户 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

其中 ab 是平台自己的计量系数。它不是精确的 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 当前支持 fcfspriority 调度策略,请求也可以通过 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 / minute
  • queued_tokens
  • running_tokens
  • p95 queue time
  • p95 TTFT
  • throttled_requests
  • rejected_requests
  • fairness debt
  • budget 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,再做公平调度。没有硬边界时,公平调度只能决定竞争顺序,无法阻止错误配置或恶意租户无限制造排队请求;只有配额没有公平调度,则会在共享容量争用时出现体验不稳定。

参考资料

  1. Ying Sheng et al., Fairness in Serving Large Language Models, OSDI 2024 — https://www.usenix.org/conference/osdi24/presentation/sheng
  2. 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
  3. vLLM, OpenAI-Compatible Server — request priority / X-Vllm-Priority — https://docs.vllm.ai/en/latest/serving/online_serving/openai_compatible_server/
  4. vLLM, Serve CLI--scheduling-policy=fcfs|priorityhttps://docs.vllm.ai/en/latest/cli/serve/
  5. Kubernetes SIG Network, Gateway API Inference Extension — Introductionhttps://gateway-api-inference-extension.sigs.k8s.io/
  6. Kubernetes SIG Network, Gateway API Inference Extension — InferencePoolhttps://gateway-api-inference-extension.sigs.k8s.io/api-types/inferencepool/

常见问题

为什么按 QPS 或请求数做租户限流还不够?
因为 LLM 请求的输入和输出 token 长度差异很大,一次长上下文、长生成请求可能消耗远高于多次短请求的 GPU 服务时间。只按请求数限流无法准确表达资源占用。
VTC 和普通优先级队列是什么关系?
VTC 解决同一服务等级内的公平份额问题;优先级队列解决不同业务等级的先后顺序。生产上更适合分层组合,而不是把二者互相替代。
可以直接把 vLLM 的 priority 当成多租户公平调度吗?
不建议。vLLM priority 能实现请求优先处理,但严格优先级本身不提供租户份额公平;仍需在网关或调度层做租户计量、配额与公平队列。