跳转至

Load Balancing

负载均衡将请求分配到多个推理副本,避免部分 GPU 拥塞而其他副本空闲。推理请求与普通请求的关键差异在于:不同请求的计算量与显存占用差异巨大,仅凭「连接数」或「请求数」均衡会把一批长请求压到同一个副本上,造成局部拥塞。

快速开始

选择路由信号是第一步。相比连接数,更有效的信号是各副本的队列长度、正在处理的活动 token 数、或按输入长度估算的预计完成时间。这些信号能反映副本的真实剩余容量,让路由器把新请求发给「最空」的副本。

「按 token 预算加权」的路由可以形式化为对每个副本计算一个剩余容量得分,选得分最高者:

\[ \text{score}_i = C_{\max,i} - B_{\text{active},i} \]

其中 \(C_{\max,i}\) 是副本 \(i\) 的 token 预算上限,\(B_{\text{active},i}\) 是它当前占用的活跃 token 数。若再考虑请求自身成本,可以进一步用「剩余预算减去预估占用」排序。这一思路与「最少连接」的差别在于,权重从连接数换成了 token 预算。

在调优路由之前,先确认所有副本的模型版本与配置完全一致。若副本之间存在模型权重、量化方式、batch 上限或 KV Cache 配置的差异,任何路由策略都无法做到公平,反而会把差异放大成尾延迟。

为验证路由效果,应在压测中混入不同长度的请求,观察各副本的负载是否接近、p95 延迟是否改善。若某副本长期满载而其他空闲,说明信号没选对或路由没有真正生效。可以用各副本 B_{\text{active},i} 的方差作为均衡程度的量化指标:方差下降说明路由更均匀。

难点

仅按连接数均衡会忽略请求长度:一个长上下文请求可能消耗数十倍于短请求的资源,连接数却完全看不出差别。必须用 token 预算或估算计算量来加权,才能避免「看起来均衡、实际上过载」。

会话粘性、前缀缓存与多模型路由让分配更复杂。会话粘性要求同一用户的连续请求落到同一副本;前缀缓存希望相同前缀的请求集中到同一副本以提高命中率;多模型路由则要先按请求指定的模型分组,再在各组内做负载均衡。这些目标之间可能互相冲突,需要明确优先级。

前缀缓存与负载均衡的冲突尤其典型:把相同前缀的请求都导向同一副本能提高缓存命中,却会让该副本过载。工程上常用「一致性哈希」在两者间折中——把请求按前缀哈希映射到环形空间,让相近请求倾向同一副本,同时保留副本变化时仅迁移小部分请求的特性。具体实现可参考 SGLang 的 RadixAttention 前缀缓存 的设计思路,其中路由器与缓存感知调度协同工作。

尾延迟排查也需要路由信息:当 p95 延迟恶化时,必须知道每个请求被分到了哪个副本、依据什么信号,否则无法判断是路由失误还是副本本身变慢。

案例

某路由器改为按「空闲 token 预算」分配:把长上下文请求优先送给当前剩余 token 预算最多的副本,短请求则走快速通道以保持低延迟。这样既避免了长请求扎堆,又让短请求不受长请求拖累。

为支持排查,路由器记录每条请求的路由原因(目标副本、当前队列、触发信号)。当出现尾延迟时,团队可以回溯是某个副本变慢、还是路由信号本身失真。一条结构化路由日志可以这样记录:

log = {
    "request_id": req.id,
    "target_replica": chosen.replica_id,
    "signal": "token_budget",
    "scores": {r.id: score(r) for r in replicas},
    "timestamp": time.monotonic(),
}

验证时对比改进前后的各副本负载方差与 p95 TTFT:负载方差下降、p95 延迟改善即说明路由有效。结论是推理负载均衡必须基于「容量信号」而非「请求计数」,并保留路由日志用于事后定位。

相关主题