27届大模型面试准备(七十):大模型推理服务的负载均衡与智能请求路由

引言

本篇是「工程实战深化」系列的第 70 篇(大模型线)。上一篇(六十九)讲了 Prefill-Decode 分离:把一个大模型推理集群拆成 prefill 池和 decode 池,各自扩缩。拆完之后立刻冒出一个问题——请求该去哪个实例? 这看似是「负载均衡」的老话题,但大模型推理的负载均衡比普通 Web 服务难得多:请求长度天差地别、KV Cache 有明显的位置局部性、GPU 利用率和延迟不是线性关系。

本篇把推理服务的请求路由讲透:为什么朴素轮询会翻车、最短队列路由怎么做、前缀亲和路由怎么提升缓存命中、代价感知路由怎么防长请求饿死,最后给一套可背的面试速答。建议和(六十九)PD 分离、(六十六)推理调度、(六十三)前缀缓存串起来看。


1. 为什么推理服务的负载均衡特别难

普通微服务的负载均衡假设「每个请求代价差不多」,轮询/一致性哈希基本够用。大模型推理不满足这个假设:

  普通 Web 服务                    大模型推理服务
  ┌─────────────┐                 ┌───────────────────────────┐
  │ 请求代价≈均等 │                 │ 请求代价方差极大            │
  │ 无状态       │                 │ 有状态(KV Cache 局部性)    │
  │ 延迟线性可加 │                 │ 延迟非线性(GPU 利用率拐点) │
  └─────────────┘                 └───────────────────────────┘

  推理请求长度分布示例:
  prompt 长度:  50 ──────────────────────────── 8000 token
  输出长度:     10 ──────────────────────────── 4000 token (长思维链)
  → 单请求算力/显存占用差 100 倍以上

三个难点:

  1. 代价方差大:一个 50 token 短问答和一个 8000 token RAG + 4000 token 长思维链,算力差百倍,轮询会把长请求堆积到个别实例。
  2. KV Cache 局部性:相同 system prompt 的请求落在同一实例能命中 prefix cache(六十三),路由破坏了局部性就白算。
  3. 延迟非线性:GPU 在某并发下利用率陡增、TBT 指数恶化(呼应六十一排队论),不能只看「平均负载」。

2. 常见路由策略对比

  策略            路由依据              优点              缺陷
  ──────────────────────────────────────────────────────────────
  Round Robin     依次轮流            简单、均匀         忽略队列长度与代价
  一致性哈希       key 哈希            稳定、可粘性       负载倾斜、无视实时
  Least-Queue     各实例 inflight 数  防堆积            忽略请求代价差异
  Prefix-Affinity 前缀 hash           保 prefix cache   可能打破负载均衡
  Cost-Aware      预估 token/显存      防长请求饿死      需准确代价估计
  Hybrid          组合上述             综合最优          实现复杂

下表是工程上最常用的两种「智能路由」:

策略适用场景核心指标风险
Least-Queue(最短队列)decode 池、输出长度相近inflight 序列数 + 预估剩余 token长输出请求仍可能扎堆
Prefix-Affinity(前缀亲和)prefill 池、固定人设/RAGprompt 前缀 hash热点前缀实例过载
Cost-Aware(代价感知)混合长度、要严格 SLA预估总 token × 单价 + 显存预估不准则失效

3. Least-Queue 最短队列路由

思路:每个实例上报「当前 inflight 序列数」和「预估剩余待生成 token 数」,路由选「预计最快空闲」的实例。

  实例 A: inflight=8, 剩余预估=800 token
  实例 B: inflight=3, 剩余预估=1200 token
  实例 C: inflight=5, 剩余预估=300 token
  新请求预估输出=200 token
  → 选 C (剩余最少, 最快腾出容量)

注意不能只看 inflight 数量,要看「剩余工作量」:一个 inflight=3 但每个都要生成 2000 token 的实例,可能比 inflight=8 但每个只剩 50 token 的实例更忙。

# 伪代码:Least-Queue 路由(带剩余工作量估计)
def least_queue_route(req, instances):
    best, best_score = None, float("inf")
    est_out = estimate_output_len(req)        # 预估本请求输出长度
    for inst in instances:
        load = inst.inflight_seqs * avg_decode_cost \
             + inst.remaining_tokens           # 已排队剩余 token
        score = load + est_out * decode_cost(inst)
        if score < best_score:
            best, best_score = inst, score
    return best

4. Prefix-Affinity 前缀亲和路由

核心直觉:把共享同一段前缀(固定人设、few-shot、RAG 公共文档)的请求,路由到持有该前缀 KV Cache 的同一实例,命中 prefix cache(呼应六十三/六十九),省算力、稳 TTFT。

  prompt = [system][few-shot][user_query]
            └── 公共前缀 ──┘  └─ 变化 ─┘
  hash(公共前缀) → 固定实例 P3
  → 所有用同一 system 的请求都去 P3,前缀段只算一次

实现要点:

  • 取 prompt 的「稳定前缀」做 hash(注意 user_query 会变,不能整段 hash)。
  • 维护「前缀 → 实例」映射表;实例下线时把映射失效,请求回退到普通路由并重算前缀。
  • 和 Least-Queue 冲突时,优先保前缀亲和(除非该实例过载触发熔断)。

5. Cost-Aware 代价感知路由

当 SLA 严格、请求长度差异大时,用「预估代价」做路由:代价 = (prompt_len × prefill_cost) + (output_len × decode_cost) + KV 显存占用。路由选「加入后负载仍最低」且「不超显存上限」的实例。

def cost_aware_route(req, instances):
    p, o = len(req.prompt), estimate_output_len(req)
    kv_mem = p * KV_PER_TOKEN                       # 本请求 KV 显存
    best, best_util = None, float("inf")
    for inst in instances:
        if inst.free_kv_mem < kv_mem:              # 显存硬约束
            continue
        util_after = (inst.busy_compute + p*prefill + o*decode) \
                     / inst.capacity
        if util_after < best_util:
            best_util, best = util_after, inst
    return best or fallback_least_queue(req, instances)

代价:需要较准的长度预估。工程上常用「历史同类请求长度分布」或「prompt 长度 + 简单回归」估算输出长度,误差大时退化为 Least-Queue。


6. 混合路由:工程落地最常用

真实网关几乎都是 Hybrid:先按前缀亲和保缓存,再在「命中前缀的实例集合」内做 Least-Queue/Cost-Aware 选最空的一个;若集合全过载则放宽到全局并触发前缀重算。

  请求 → hash(前缀) → 候选实例集合 {P3,P7}
                     └─ 集合内 Least-Queue → P7 (更空)
      若 {P3,P7} 全过载 → 全局 Cost-Aware → P2 (牺牲前缀缓存, 保 SLA)

7. 网关、限流与优先级

路由之上还要一层网关做全局治理(呼应六十六):

  • 全局并发上限:实例总容量 = Σ 各实例 max_seqs;超过则排队或拒绝。
  • 令牌桶限流:按用户/应用维度限 QPS,防单租户打爆(呼应六十六多租户)。
  • 优先级队列:VIP 请求插队,但要对低优先做公平性保护,避免饿死。
  • 熔断:某实例错误率/延迟超阈值,路由暂时剔除它(呼应七十下篇智能体侧同理)。
# 伪代码:带优先级与熔断的路由网关
class RouteGateway:
    def route(self, req):
        if self.rate_limiter.deny(req.tenant):     # 租户限流
            return RESP_TOO_MANY
        insts = self.candidates(req)                # 前缀亲和集合
        insts = [i for i in insts if not self.cb.open(i)]  # 剔除熔断实例
        if not insts:
            insts = [i for i in self.all if not self.cb.open(i)]
        target = self.hybrid_select(req, insts)     # Least-Queue/Cost-Aware
        self.cb.record_attempt(target)
        return target

8. 生产落地 checklist

  • 监控每实例 inflight、剩余 token、KV 显存占用、prefix cache 命中率、P99 TBT。
  • 路由决策周期要短(秒级),否则负载快照失真。
  • 前缀映射表要支持失效与迁移,避免实例扩缩时缓存全灭。
  • 压测用真实长度分布,别用固定长度(会掩盖路由倾斜)。
  • 熔断阈值设「错误率 + 延迟」双指标,单看一个会误剔。

面试速答

  1. 为什么轮询不行? 推理请求代价方差百倍,轮询会让长请求堆积、短请求饿死,且破坏 KV Cache 局部性。
  2. Least-Queue 看什么? 不只看 inflight 数量,看「剩余待生成 token 工作量」,选最快腾出容量的实例。
  3. Prefix-Affinity 解决什么? 同前缀请求落同实例命中 prefix cache,省算力稳 TTFT。
  4. Cost-Aware 风险? 依赖长度预估准确度,预估失准就退化成 Least-Queue。
  5. 混合路由怎么做? 先前缀亲和缩候选集,集内 Least-Queue/Cost-Aware 选最空,全过载则放宽全局并牺牲前缀缓存保 SLA。
  6. 网关还要做什么? 全局并发上限、租户限流、优先级队列、实例熔断。

高频追问清单

  • 前缀亲和和一致性哈希有什么区别?(前者按语义前缀保缓存局部性,后者按 key 均匀分布,目的不同)
  • 实例扩缩容时 prefix cache 映射怎么迁移,会冷启动吗?(映射表失效 + 请求回退重算前缀,冷启动短暂升高 TTFT)
  • 多租户下怎么既限流又保 SLA?(租户级令牌桶 + 全局容量 + 优先级队列 + 隔离)
  • 路由决策延迟本身会不会成为瓶颈?(决策在网关内存完成,微秒级,远低于 GPU 计算,可控)
  • 没有 RDMA 的分离集群,路由要额外考虑什么?(KV 传输走 TCP 慢,路由要尽量避免跨节点 KV 搬运,倾向 prefill/decode 同可用区)
  • prefix cache 命中率怎么量化并用作路由指标?(命中率 = 跳过计算 token / 总 token,低则意味路由散了,应强化前缀亲和)
Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐