Lecture 11 · 负载均衡与路由:Preble 与 LLMSelector · Load Balancing & Routing

AI Inference Infrastructure · Lecture 11。个人学习讲义与配套实验,按主题整理并持续修订。

课程源码 ZIP ↓

个人整理的学习笔记,非官方讲义;课程来源与整理说明见课程介绍。配套示例未逐一运行验证。

本篇目录

对应 Maps to:Stanford CS349D AI Inference Infrastructure(Spring 2026),#14 Wed May 13 Load Balancing & Routing。必读:V. Srivatsa, Z. He, R. Abhyankar, D. Li, Y. Zhang, Preble: Efficient Distributed Prompt Scheduling for LLM Serving(ICLR 2025);L. Chen, J. Q. Davis, B. Hanin, P. Bailis, M. Zaharia, J. Zou, I. Stoica, Optimizing Model Selection for Compound AI Systems(LLMSelector,2025)。 依据说明:原课没有公开 slides。本讲依据两篇论文的全文(含附录),LLMSelector 算法里”平局怎么处理”参考了作者开源的实现(llmselector/compoundai/optimizer.py)。§1 的经典负载均衡(球放进盒子、二选一、过时信息)、§2 的”集群的总缓存”分析、§5 的综合和 §6”之后的发展”是我补充的;§6 里 SGLang 路由器的算法依据它开源代码里的说明。讲义里的纸上数字全部来自配套代码的实际运行。 配套代码 Code:code/lecture11/(numpy;4 个脚本 + 2 个模块:cluster_router.py 是带前缀缓存的多副本集群模拟器,llm_selector.py 是复合 AI 系统的玩具世界;代码 03 约 55 秒,其余不到 20 秒) 预计阅读时间:3–4 小时(含运行代码)


0. 本讲导航 · How to read this lecture

0.1 学习目标 Learning goals

学完这一讲,你应该能够:

  1. 说清多副本集群里经典的派发策略(随机、轮转、最短队列、二选一、最少剩余工作)各自需要什么信息、好在哪里、在什么情况下会出事(请求大小重尾、负载信息过时)。
  2. 解释为什么前缀缓存让 LLM 的路由变难:只看负载会让每个副本都缓存同一批东西(集群的总缓存 = 一个副本的缓存),只看缓存会把请求吸到同一个副本上(扎堆)。
  3. 说清 Preble 的设计:全局基数树 + 每卡本地调度的两层结构;E2 的”利用 vs 探索”规则(缓存的 token 多于没缓存的就利用);负载代价 L + M + P;事后再平衡与自动扩展;用前缀共享来搭配 prefill 和 decode;本地的优先级组。
  4. 解释 LLMSelector 要解决的问题(复合 AI 系统里每个模块用哪个模型)、它的两个经验观察(端到端表现随模块表现单调、模块表现可以由 LLM 诊断),以及”一次改一个模块”的算法为什么能跳出端到端贪心的局部最优。
  5. 区分三个层次的”路由”:请求 → 副本(负载与缓存)、请求 → 模型(质量与成本)、模块 → 模型(复合系统),并说出各层需要的信息。

0.2 先修与本讲范围 Prerequisites & scope

默认你已经会 Assumed本讲只给直觉,后面再展开 Previewed only
第 2 讲:连续批处理、迭代级调度多模型共享 GPU、冷启动(第 12 讲)
第 3 讲:前缀缓存、RadixAttention 的基数树与 LRU智能体程序的调度(第 13 讲)
第 7 讲:分块 prefill、prefill / decode 的不同瓶颈SGLang 路由器的工程细节(第 16 讲)
第 9 讲:Mooncake 的缓存感知调度、链式哈希排队论的推导(前置讲 P4)

0.3 本讲地图 Map

§1  集群里的经典负载均衡:随机、轮转、最短队列、二选一、过时的信息        ← 代码 01
§2  前缀亲和 vs 负载均衡:为什么 LLM 的路由变难                          ← 代码 02
§3  Preble:E2 调度、负载代价、再平衡、本地优先级                        ← 代码 03
§4  LLMSelector:复合 AI 系统里每个模块用哪个模型                        ← 代码 04
§5  三个层次的路由
§6  之后的发展
§7–11  小结、常见误区、练习、延伸阅读、下一讲预告

0.4 记号 Notation

符号含义
NN副本数(数据并行的模型实例数;每个副本可以是一块 GPU,也可以是张量并行的几块)
ρ\rho利用率 = 到达率 ÷ 总服务能力
cached / missed一个请求的提示里,已经在某个副本上缓存的 token 数 / 没缓存的 token 数
HHPreble 的历史窗口(默认 3 分钟)
Li,Mi,PiL_i, M_i, P_iPreble 的负载代价三项:副本 ii 窗口内的计算量、为放下新请求要淘汰的缓存的重算代价、新请求在 ii 上的 prefill 时间
ThbalTh_{bal}Preble 再平衡的阈值:最忙副本的负载超过最轻副本的多少倍时开始分流
KK、LL(§4)候选模型数、复合系统的模块数;分配 ff 把每个模块映射到一个模型,共 KLK^L 种
qq(本讲代码里)诊断器判断正确的概率

0.5 关于数字 · About the numbers

本讲的数字有三类:(1) 派发模拟:N 个先来先服务队列(代码 01),与 LLM 无关,单位是”平均服务时间”;(2) 集群模拟:每个副本是一块 A6000 上的 Mistral-7B(Preble 的实验设定),每一步的时间来自第 1 讲的纸上 GPU(roofline),副本内部做连续批处理 + 分块 prefill + 段级前缀缓存,路由器按不同策略派发;负载是仿照 Preble 表 1 的合成负载(平均长度和共享比例相近,热度分布、文档个数等是假设值);(3) 玩具世界:LLMSelector 的模型和题目都是生成出来的,模型名 A–E 不代表任何真实模型。论文里的数字写成”论文报告”。


1. 集群里的经典负载均衡 · Classic load balancing

1.1 为什么要有路由器

一个模型上线以后,几乎从来不只部署一份。原因有三:一块(或一组)GPU 的吞吐有限,流量大了要加副本;副本之间互为备份,坏一个不至于停服;不同地区的用户要就近服务。这些副本是数据并行的(第 6 讲):每个副本都有完整的模型,彼此独立地处理请求。于是集群前面需要一个路由器(也叫负载均衡器、网关),每来一个请求就决定”交给哪个副本”。

这个问题在 LLM 之前就研究了几十年(Web 服务器、数据库、搜索后端)。先把经典答案过一遍,因为 LLM 路由器的大部分想法都从这里来,它们的失败方式也会在 LLM 里重演。

1.2 五种派发策略

策略需要什么信息直觉
随机(random)什么都不需要最简单;但运气不好时会连续派给同一台
轮转(round robin)只需要一个计数器依次派给 0, 1, …, N−1;每台收到的请求数完全一样多
最短队列(JSQ,join the shortest queue;也叫 least connections)每台当前有几个请求谁闲派给谁
二选一(power of two choices)随机抽 2 台的队列长度只比较两台,就能拿到最短队列的大部分好处
最少剩余工作(LWL,least work left)每台还剩多少活(要知道每个请求的大小)比”排了几个”更准

**二选一为什么这么有效?**看一个更简单的模型:“球放进盒子”。把 nn 个球依次放进 nn 个盒子:纯随机放时,最满的盒子大约有 ln⁡n/ln⁡ln⁡n\ln n / \ln\ln n 个球;每个球随机看两个盒子、放进较空的那个,最满的盒子骤降到约 ln⁡ln⁡n/ln⁡2\ln\ln n / \ln 2(Azar 等 1994)。从”不看”到”看两个”是指数级的改善,再多看几个只改善常数。代码 01 演示 1:

盒子数 nn随机二选一三选一
1,0005.43.02.4
100,0008.23.43.0

排队。真实系统里请求有大小、会离开,派发器面对的是 NN 个队列。代码 01 演示 2 模拟了 16 台先来先服务的服务器(单位是平均服务时间,格式”平均 / P99”):

利用率 0.9随机轮转最短队列二选一最少剩余工作
请求大小指数分布(cv = 1)11.56 / 56.75.59 / 26.51.63 / 6.72.91 / 9.71.42 / 5.4
对数正态,cv = 3(更像 LLM)43.31 / 471.649.69 / 603.42.89 / 24.46.89 / 42.82.34 / 13.8

三个要点:

  1. 看负载的策略远好于盲目的策略。随机派发时每台服务器就是一个独立的 M/M/1 队列,平均响应时间 1/(1−ρ)1/(1-\rho),利用率 0.9 时约 10 倍服务时间(前置讲 P4);最短队列把 16 台服务器”拼”成了一个共享的队列,几乎不排队。
  2. 轮转的好处很脆弱。指数分布下它比随机好一倍(每台的到达更均匀);可一旦请求大小重尾,排队主要来自”少数超大请求挡在前面”,到达是否均匀不再重要:代码里换 8 个种子平均,两者相差在噪声以内。LLM 请求的长度就是重尾的:几十 token 的闲聊和几万 token 的文档问答混在一起。
  3. “排了几个”不等于”还有多少活”。请求大小相差悬殊时,最短队列的 P99 明显变差,知道请求大小的最少剩余工作好得多。所以 LLM 路由器常按 token 数(而不是请求数)估计负载。

1.3 过时的信息与扎堆

最短队列有一个隐含前提:路由器知道此刻每台服务器的队列长度。真实系统里这个信息往往是定期拉取的监控数据,或者是路由器自己的估计。Mitzenmacher(2000)研究了”信息过时”会怎样,代码 01 演示 3 复现了他的结论(利用率 0.9):

刷新间隔随机最短队列二选一
实时11.561.632.91
4 个服务时间11.5613.534.07
16 个服务时间11.5645.956.86

信息一过时,最短队列就扎堆(herding):快照里最短的那台,在下一次刷新之前收下所有请求,刷新后又换一台被淹没。刷新间隔是 4 个服务时间时它已经比随机还差。二选一几乎不受影响:每个请求只在随机抽到的两台里比较,不同请求抽到的不同,天然把流量分散开。

记住这个模式:“所有请求都根据同一份信息做同一个选择”,就会扎堆。§2 会看到,前缀亲和制造了同样的扎堆,只不过信息是”谁有我的前缀”。

1.4 LLM 推理和经典模型有什么不同

把经典结论搬到 LLM 上,要注意四点不同:

  1. 请求大小相差上百倍,而且事先只知道一半:提示长度到达时就知道,输出长度要等生成完才知道(第 1 讲)。
  2. 副本内部不是先来先服务:连续批处理每一步都重新组批(第 2 讲),多个请求同时在一个副本上推进,“队列”的概念变成了”这个副本的批有多满、显存还剩多少”。
  3. 显存是第二种资源:一个副本可能算力空着、显存却被 KV 占满,新请求只能等(第 3 讲)。
  4. 副本不再一模一样:每个副本都有自己的前缀缓存(第 3 讲),同一个请求在缓存了它前缀的副本上只需算一小段,在别的副本上要从头 prefill。服务时间取决于”派给谁”——这是经典模型里没有的,也是本讲的核心难题。

2. 前缀亲和 vs 负载均衡 · The cache–balance dilemma

2.1 真实负载:提示又长又共享

Preble 的出发点是一项负载研究(论文 §2 和附录 A)。它分析了五类 LLM 应用的公开数据集和 Azure 的 LLM 请求 trace(论文报告):

负载平均提示 : 输出(token)提示里共享的比例共享来源
工具调用(ToolBench)1835 : 4385%同一个工具的说明和示例
具身 agent(ALFWorld)2285 : 1697%任务示例 + 之前每一步的交互
程序生成(APPS)3871 : 19097%通用代码示例 + 同一题的并行采样
视频问答(NExT-QA)9865 : 488%同一段视频的 token
长文档问答(LooGLE)23474 : 1691%同一份文档

四条结论:(1) 提示比输出长得多(Azure 的聊天 trace 平均 4 倍,视频问答 2,494 倍),所以 prefill 是大头;(2) 共享很普遍、而且比例很高;(3) 大多数请求有一段”关键部分”(key portion)——前缀树里比它所有祖先加起来还长的那个节点,例如文档本身,它决定了这个请求该和谁放在一起;(4) 真实流量的强度变化很大(Azure trace 里相邻请求的间隔从 2 微秒到 217 秒)。

所以前缀缓存(第 3 讲)在这些负载上价值巨大。问题是:缓存在每个副本上各有一份,路由器的决定直接决定了能不能命中。

2.2 两种极端都不行

只看负载(轮转、最短队列……):请求被均匀地撒到所有副本上。每个副本都会看到所有文档、所有工具,于是每个副本都试图缓存同一批热门内容,冷门内容在哪里都留不住。代码 02 演示 1(4 个副本,长文档问答 1 请求/秒):

路由平均延迟命中率复制倍数各副本完成的请求 %
轮转9.89 s0.321.3425/25/25/25
最少剩余 token6.93 s0.331.3427/24/25/23
纯前缀亲和658.62 s0.331.000/0/0/100
命中过半才亲和3.88 s0.581.0024/22/38/16

(复制倍数 = 各副本缓存的 token 之和 ÷ 去重后的 token;1.00 表示每份内容只存在一个副本上。)

只看缓存(纯前缀亲和:总派给前缀匹配最长的副本):结果是灾难性的——所有请求都去了同一个副本。我原以为是热门文档压垮了一个副本,实际原因更简单:长文档负载里所有请求共享一段只有 13 个 token 的系统提示。第一个请求派到哪个副本,那个副本就有了”最长的匹配”(哪怕只有 13 个 token),于是之后的每个请求都被吸过去;其他副本一个请求都收不到,永远没有机会缓存任何东西。这就是 §1.3 的扎堆,只不过信息是”谁有我的前缀”。视频问答、工具调用上也一样(代码 02 演示 1)。

2.3 中间路线:命中够多才亲和

合理的做法是只在”值得”的时候亲和:前缀匹配占提示的比例够高(例如过半)才派给匹配最长的副本,否则按负载派。上表最后一行就是这个策略:命中率从 0.33 升到 0.58,平均延迟比最好的只看负载的路由低 1.8 倍。三种负载上低 1.4–2.3 倍。

阈值取多少?代码 02 演示 3 扫描了 0.1 到 0.9:在 0.25–0.75 之间结果几乎一样,因为真正值得共享的部分(文档、工具说明)都占提示的大头;太低(0.1)时”只共享系统提示”也会触发亲和,扎堆又回来了;太高(0.9)时长文档的问题部分(约 9%)就让很多请求够不上。Preble 的 E2 用的正是 0.5,并给了一个漂亮的理由(§3.2)。

2.4 真正的收益:集群的总缓存

前缀感知路由最根本的好处,不是”命中同一个副本”这么简单,而是让缓存容量随集群规模增长。代码 02 演示 2 把副本从 2 个加到 8 个(请求率同比例增加):

副本数轮转的命中率命中过半才亲和的命中率
20.300.46
40.320.58
80.330.66

轮转时每个副本面对的都是”全部文档”,能缓存的永远只是一个副本的显存那么多(Mistral-7B 在 A6000 上约 21.9 万 token,约 10 份平均长度的文档);亲和路由把文档分到不同副本上,NN 个副本的缓存加起来用。第 9 讲的 Mooncake 用”分布式 KV 池”解决同一个问题——那是把缓存从副本里拿出来共享;这里是在不共享存储的前提下,靠路由把内容分区。

2.5 热点:一个副本扛不住的前缀

分区也有代价:如果某个前缀特别热,它的全部流量都会落到同一个副本上。代码 02 演示 4:工具调用负载里最热的工具约占 40% 的流量,8 个副本(每个只能承担 1/8):

路由平均延迟各副本完成 %
最少请求1.78 s12/12/13/12/12/13/12/13
命中过半才亲和35.42 s7/14/8/7/40/7/9/8
缓存感知 + 负载保护1.52 s13/12/12/13/12/12/13/13

“缓存感知 + 负载保护”是 SGLang 路由器思路的简化版:各副本的未完成请求数相差太大(差值超过 8、且最多的超过最少的 1.5 倍)时,暂时只看负载。被分流出去的请求在新副本上把热门前缀算一遍,热门前缀于是自动被复制到多个副本上——命中率和亲和一样高(0.84),延迟比只看负载还低。

教训:前缀亲和必须配一个”太不均衡就放弃亲和”的保护。接下来看 Preble 怎样把这些想法系统化。


3. Preble:前缀感知的分布式调度 · Preble

论文卡片 Paper card · Preble

  • 问题:前缀缓存(SGLang、vLLM)都是单 GPU 的优化;生产中的推理是分布式的,按负载均衡派发会把共享前缀的请求撒到不同 GPU 上,各算一遍;总派给同一个 GPU 又会把它压垮。
  • 关键想法:E2(Exploitation + Exploration):缓存的 token 多于没缓存的就”利用”已有缓存,否则”探索”负载最轻的 GPU;负载按”GPU 计算时间”统一度量(窗口内的计算 + 淘汰代价 + 新请求的代价),共享的部分记为零;再加上事后再平衡、自动扩展、按前缀命中搭配 prefill / decode、本地的优先级组。
  • 结构:全局调度器(请求级,维护全局基数树)+ 每个模型实例一个本地调度器(迭代级)。
  • 结果(论文报告):Mistral-7B(2 / 4 块 A6000)和 Llama-3-70B(8 块 H100,TP4 × 2),五种负载,平均延迟比 SGLang 低 1.5–14.5 倍,P99 低 2–10 倍。
  • 局限:负载代价依赖离线回归出来的 prefill / decode 时间函数;全局调度器是单点(论文估计一个能撑 70–391 块 A100);对缓存的了解来自异步的淘汰通知。

3.1 两层结构

Preble 的全局调度器做请求级的决定(派给哪个副本),本地调度器做迭代级的决定(每一步组什么批,第 2 讲)。理由:全局的决定需要全局的信息(各副本的负载、谁缓存了什么),但只需要每个请求做一次,一个调度器就能撑几百块 GPU;迭代级的决定要每几十毫秒做一次、要看到 GPU 的实时状态,只能放在本地。

全局调度器的核心数据结构是全局基数树(radix tree,第 3 讲):每个节点记录它有几个 token、哪些 GPU 缓存了它、窗口 HH 内每个 GPU 上有几个请求共享了它。新请求到来时先在树上做前缀匹配,然后把自己插进去——插入发生在派发时,不等 GPU 算完。这一点很关键:并行生成的 8 份代码几乎同时到达,第一份还没算完,后面 7 份就已经知道”这个题目将在 GPU 3 上”,于是跟过去。代码里的 Replica.view_match 就是这个”路由器视角”:缓存里已有的段 + 已经派来、即将算出的段。本地调度器淘汰缓存时异步通知全局调度器,全局树据此更新(实现上大多是只读操作,可以无锁)。

3.2 E2:利用还是探索

对一个新请求,E2(论文算法 1)先在全局树上匹配,得到 cached(最长的匹配)和 missed = 提示长度 − cached:

  • cached > missed:利用(exploit)。只在缓存了最长匹配的那些 GPU 里选,挑负载代价最低的。
  • 否则:探索(explore)。先看有没有哪个 GPU 被 decode 占满(见 §3.4),有就派给它;否则在所有 GPU 里挑负载代价最低的。

为什么是”cached > missed”?论文的理由很直观:利用能省下的是 cached 个 token 的重算;而这个请求真正”可以拿去平衡负载”的只是它的 missed 部分。省下的比能平衡的多,就利用;反之,这个请求本质上是一个新的大计算,应该交给最需要活干的 GPU。这正是 §2.3 里的阈值 0.5。

3.3 负载代价:L + M + P

“负载最轻”怎么定义?Preble 把三种代价都换算成 GPU 计算时间,并且只在全局调度器里记 token 数,不去测真实时间(论文算法 2):

  1. LiL_i:GPU ii 在最近窗口 HH(默认 3 分钟)内的计算量 = 窗口内派给它的每个请求的”prefill 时间(只算没命中的 token)+ decode 时间(用窗口内的平均输出长度估计)“。用历史窗口而不是此刻的队列,理由有二:从派发到真正执行,负载还会变;而且一个前缀放在哪里,影响的是将来很多请求,是长期的事。
  2. MiM_i:为了放下这个请求,GPU ii 要淘汰哪些缓存节点(按它的淘汰顺序找出来),每个节点的代价 = 重算它的时间 × 它的命中率(窗口内共享它的请求占比)。越长、越热的节点,淘汰越贵。
  3. PiP_i:这个请求在 GPU ii 上的 prefill 时间(只算 missed 部分)。decode 时间不算,因为在哪里都一样。

prefill 和 decode 的时间都来自离线回归:论文的测量显示,prefill 时间大致与提示长度成正比(线性层占大头),单个请求的 decode 时间大致与上下文长度成正比(论文附录 B)。

**我们的复现发现:三项里 L 一枝独大。**代码 03 演示 2 记录了基本 E2 每次计算代价时三项的平均值:

负载LMP
工具调用 40/s1616.4 s0.000 s0.078 s
长文档问答 1.5/s114.9 s0.032 s3.009 s
视频问答 6/s152.8 s0.001 s1.030 s

原因在 decode 项:Preble 用”单个请求 decode 的时间”估计每个请求的 decode 代价(A6000 上 Mistral-7B 每个 token 约 24 毫秒,43 个 token 就是约 1 秒),而真实的 decode 是成批做的、一个请求的边际代价小得多。窗口里几百上千个请求的 decode 估计加起来,把 P 和 M 淹没了。于是探索时 E2 近似于”选最近 3 分钟派得最少的副本”;淘汰代价 M 几乎总是 0(要淘汰的通常是冷门节点)。这不是说 E2 不好,而是说它的效果主要来自规则(利用 / 探索 + 负载均衡),而不是这个精巧的代价模型。

3.4 事后再平衡、自动扩展、prefill / decode 搭配

事后再平衡。E2 是贪心的:一个前缀派到某个 GPU 后就一直在那里,直到被淘汰。如果这个前缀越来越热,所有”利用”它的请求都去同一个 GPU——这正是 §2.5 的热点。Preble 的办法:如果最忙 GPU 的负载超过最轻 GPU 的 ThbalTh_{bal} 倍,就把本该”利用”最忙 GPU 的请求改派给最轻的 GPU。被改派的请求在新 GPU 上重算这个前缀,前缀于是被复制。如果改派之后还是扛不住(窗口内平均排队时间翻倍),就自动扩展:把这个前缀复制到多个 GPU 上,按负载切分它的子树。

代码 03 演示 3 复现了这个需求(最热工具约占 40% 流量,8 个副本,70 请求/秒):

配置平均延迟P99
E2(无再平衡)35.44 s147.8 s
E2 + 再平衡 ThbalTh_{bal} = 2.01.58 s3.6 s
E2 + 再平衡 ThbalTh_{bal} = 1.21.55 s3.5 s

没有再平衡的 E2 和”命中过半才亲和”一样崩溃:热门工具只在一个副本上有最长匹配,负载代价只在这些副本之间比较。打开再平衡后崩溃消失,阈值不敏感。(我们没有实现自动扩展。)

用前缀共享搭配 prefill 和 decode。第 7 讲讲过,prefill 是计算密集的,decode 是带宽密集的,两者搭配才能用满 GPU。Preble 的观察是:如果一个请求的提示几乎全部命中缓存,它几乎只剩 decode,可以看作”decode 单元”;一个提示很长、没命中、输出很短的请求则是”prefill 单元”。所以在探索时,如果某个 GPU 上 decode 单元太多(算力闲着),就把这个要重算的请求(prefill 单元)优先派给它。这条规则排在负载代价比较之前。此外本地调度器还照常做分块 prefill(第 7 讲)。

3.5 本地调度器:优先级组

每个副本的等待队列按什么顺序接纳?先来先服务(vLLM)不管缓存;最长前缀优先(SGLang,第 3 讲)可能让命中少的请求饿死。Preble 的折中:按”已缓存的比例”把请求分成 PP 个优先级组(例如 100 个 token 里 63 个已缓存、PP = 10,就进第 6 组);组批时按比例从各组取,高优先级的组取得多,例如要取 55 个请求,就从第 10 组取 10 个、第 9 组取 9 个……第 1 组取 1 个。命中多的请求优先,但每个组都有份额。显存不够时按 LRU 淘汰树上的节点(可以只淘汰一个节点的一部分)。

3.6 评测与我们的复现

论文报告:五种负载、两种模型、两种集群上,Preble 比”轮转负载均衡 + 每个实例自己做前缀缓存的 SGLang”平均延迟低 1.5–14.5 倍、P99 低 2–10 倍;编程负载提升最小(平均 1.56–1.8 倍),因为它的输出最长、decode 占比最大;在 Azure trace 的到达模式下混合工具调用和视频问答,平均延迟、P99、TTFT、TPOT 都有数倍改善。消融(论文图 5,工具调用、Zipf 1.1 热度):基本 E2 已经有明显改善,再平衡主要改善 P99,prefill / decode 搭配两者都改善,优先级队列改善 P99 而不改善平均延迟。全局调度器每秒能处理 245(ToolBench,前缀树最复杂)到 2,931(视频问答)个请求。

代码 03 演示 1 是图 3 的纸上版(格式”平均 / P99”,秒;节选):

负载请求/秒轮转(基线)缓存感知 + 保护E2 + 再平衡Preble 完整版
工具调用404.23 / 13.61.92 / 4.32.12 / 6.11.95 / 4.6
多步 agent500.51 / 2.10.45 / 1.80.45 / 1.80.45 / 1.8
并行生成代码89.58 / 78.47.49 / 61.47.72 / 62.07.91 / 66.2
视频问答46.69 / 23.41.74 / 8.31.90 / 7.02.02 / 7.3
视频问答6122.06 / 232.910.79 / 30.46.09 / 15.63.91 / 13.7
长文档问答1.552.51 / 106.86.30 / 24.27.31 / 26.99.90 / 37.3
长文档问答2158.80 / 288.026.69 / 73.815.88 / 44.314.49 / 45.3

和论文一致的地方:长文档和视频问答收益最大(共享的上下文长、缓存放不下几份);编程负载收益最小;工具调用和 agent 的共享部分每个副本都放得下,轮转也能命中大部分,收益小。基线和 Preble 的平均延迟之比在 1.0–31.2 倍之间,但比值最大的格子里基线已经过载、队列越排越长,比值取决于模拟时长,不能当作”加速比”。

和论文不一样、值得记住的地方(详见代码 README 的”关于实验结果”):

  1. 简单的”缓存感知 + 负载保护”在中低负载下不输 Preble,长文档 1/s 时甚至更好(3.88 vs 5.35 秒);Preble 只在最高的几档负载下明显更好,而对比”E2 + 再平衡”和”完整版”两列,那里很大一部分优势来自副本内部的优先级排队(视频 8/s:90.0 → 20.6 秒),而不是路由。
  2. prefill / decode 搭配可能有害(代码 03 演示 2):工具调用上 P99 从 6.1 降到 4.6 秒,长文档和视频问答上反而变差——它们的”探索”请求是几千到几万 token 的 prefill,塞给 decode 多的副本,会挡住那里正在 decode 的请求。
  3. 本地优先级组主要改善平均延迟(代码 03 演示 5,视频 6/s:先来先服务 6.09 秒 → 优先级组 3.96、最长前缀优先 3.91),命中少的请求的 P99 和先来先服务时差不多,与论文”改善 P99、不改善平均”正好相反;而且优先级组和最长前缀优先几乎一样。
  4. 窗口 HH 越长越好一些(长文档 10.09 → 6.76 秒),但没有数量级差别;论文说不敏感。

4. LLMSelector:每个模块用哪个模型 · LLMSelector

论文卡片 Paper card · LLMSelector

  • 问题:复合 AI 系统(compound AI system)由多次 LLM 调用组成。现有优化大多调提示词、调模块之间的连接,所有模块用同一个模型。每个模块用哪个模型?KK 个模型、LL 个模块有 KLK^L 种分配。
  • 两个经验观察:(1) 其他模块固定时,端到端表现往往随某个模块的表现单调变化;(2) 模块级的表现可以由一个 LLM 读执行记录准确判断。
  • 算法:轮流提名一个模块,用”LLM 诊断器”估计每个候选模型在这个模块上的表现,换上最好的;直到连续几轮不再变化。每轮的 API 调用数与模型数成正比,总轮数与模块数成正比。
  • 结果(论文报告):10 个真实模型(GPT-4o、Claude 3.5 Sonnet、Gemini 1.5 Pro、Llama 3.1 405B 等),三种复合系统、六个数据集,比”所有模块用同一个最好的模型”高 5%–70%,也比 DSPy 的提示词优化(MIPROv2)好。
  • 局限:只考虑静态系统(模块数、调用顺序、分配都固定);单调性假设并不总成立;没有考虑成本。

4.1 为什么一门推理系统课要讲”选模型”

前面三节的”路由”是把请求派给同一个模型的不同副本。另一类路由是把请求派给不同的模型:简单的问题给便宜的小模型,难的给大模型(模型路由、级联);一个应用的不同步骤用不同的模型。这直接决定了推理集群的形态——要部署哪些模型、每个模型多少副本、流量怎么分——也决定了成本。LLMSelector 研究的是这类路由里结构最清楚的一种:复合 AI 系统里每个模块的模型分配。

复合 AI 系统的例子(论文图 2、图 5):

  • 定位-求解(locate-solve):一次调用从长表格里找到指定 ID 的题目,另一次调用求解;
  • 自我改进(self-refine):生成 → 批评 → 改写;
  • 多智能体辩论(multi-agent debate):三个生成者各给一个初始答案,三个辩论者讨论哪个是对的。

4.2 问题与假设

把系统写成一个有向无环图,每个节点是一个 LLM 模块。一个分配 ff 给每个模块指定一个模型;目标是最大化端到端表现 P(f)=Ez[p(f,z)]P(f) = \mathbb{E}_z[p(f, z)](zz 是一道题)。为了能分析,论文做了三个假设:表现是二元的(对或错);端到端表现是各模块表现的单调递增函数;模块表现满足两个单调性:

  • 模块内单调:对一个模块来说,模型 kk 比 k′k' 好,不管其他模块用什么都成立;
  • 模块间单调:把一个模块换成更好的模型,不会让其他模块变差。

在这些假设下,“每个模块各自选最好的模型”就是全局最优,而且按模块逐个优化,LL 轮就能收敛(论文定理 4.1,要求每道题的最优分配唯一)。

4.3 算法:一次改一个模块,用诊断器打分

LLMSelector(论文算法 1)的循环:

  1. 从一个随机分配开始;
  2. 轮流提名模块 jj;对每个候选模型 kk,把模块 jj 换成 kk,在训练集上跑一遍;
  3. 对每道题,用 LLM 诊断器估计模块 jj 的表现:把系统描述、问题、每个模块的输出、最终答案和正确答案交给一个 LLM,问”是不是模块 jj 导致了错误”(答对的题一律判”没错”)。模块得分 = 诊断器的判断 + γ\gamma × 端到端是否答对;
  4. 每道题记下所有得分最高的模型,选出”在最多道题上并列最高”的模型(这是作者开源实现里的聚合方式;论文写作”取众数”);
  5. 连续 L+1L + 1 次分配都不变就停止。每轮评估 KK 个分配,预算按 KK 累加。

**为什么不直接看端到端准确率?**因为端到端准确率会把”一个模块的改进”藏起来。论文的 TableArithmetic 案例(图 4):Claude 3.5 Sonnet 定位从不出错、却总以为 10.9 < 10.11(求解出错),Gemini 1.5 Pro 正相反;GPT-4o mini 两样都中等。从”两个模块都用 GPT-4o mini”出发,只换定位或只换求解都不能提高端到端准确率,所以”逐模块看端到端”的贪心停住了;诊断器却能看出”换成 Claude 以后定位不再出错”,于是 LLMSelector 接受这个改动,下一轮再把求解换成 Gemini,准确率达到 100%。论文报告 LLMSelector 只需评估 10 个分配(穷举要 25 个)。

4.4 我们的玩具复现

代码 04 演示 1 构造了同样的结构:模型 A 定位强、求解弱;B 正相反;C 两样都中等;每道题只有一个难度,同一个模型在两项技能上”一起顺手、一起不顺手”。训练集上 25 种分配的准确率(行 = 定位,列 = 求解,节选):

定位 \ 求解ABC
A0.020.960.44
B0.000.010.00
C0.010.440.45

C+C(0.45)是局部最优:换掉一半(A+C 0.44、C+B 0.44)都不更好——C 做对的恰好是”对 C 来说容易”的题,换掉一个模块,另一个模块仍然只在这些题上做对。从 C+C 出发,贪心停在原地(测试集 0.51),LLMSelector 走到 A+B(测试集 0.99),诊断器准确率降到 0.8 也能找到。从全部 25 个初始分配出发,贪心失败 8 次,LLMSelector 25 次全部成功。

**在随机模型池上呢?**代码 04 演示 2 生成了 10 个随机模型池(每池 5 个模型,能力 = 通用水平 + 各技能的专长),三种系统各跑一遍(测试集准确率,池平均):

系统单一最好贪心随机搜索LLMSelector穷举评估的分配数(贪心 / LS / 穷举)
定位-求解0.5990.6570.6240.6540.66720 / 19 / 25
生成-批评-改写0.8320.9080.9040.9070.91628 / 31 / 125
三人辩论0.8240.8320.8200.8400.85052 / 192 / 15,625

三点:(1) 混用模型确实比最好的单一模型好(LLMSelector 高 1.6–7.5 个百分点,穷举高 2.6–8.4);(2) 在随机池上贪心和 LLMSelector 差不多,都接近穷举——演示 1 那种卡住贪心的结构在随机池里不常见;论文主实验没有和贪心比较,这是我们的补充;(3) 同样的预算下随机搜索更差:搜索空间大时,“一次改一个模块”的结构本身就很有用。

诊断器不准时(代码 04 演示 3):三人辩论上,诊断器判断正确的概率从 1.0 降到 0.6,LLMSelector 评估的分配数从 68 涨到 633——模块得分变嘈杂,“连续几轮不变”的停止条件更难满足;准确率下降得比较慢(并且不单调)。诊断器是 LLMSelector 跳出局部最优的唯一凭据,也是它额外的成本来源。

4.5 假设不成立时

论文承认两个单调性假设”并不总成立”,它的定性例子(附录图 6、图 7)正好是两种反例,代码 04 演示 4 在玩具世界里重现了它们:

  • 模型看不出自己的错(违反模块间单调):论文的例子里 Claude 3.5 Sonnet 当批评者时倾向于同意自己生成的答案,换成 GPT-4o 当批评者才发现错误。我们让”批评者与生成者是同一个模型时发现错误的概率乘以 0.3”:10 个池的最优分配全都让二者用不同的模型,比”同一个模型”最好的高 7.7 个百分点;LLMSelector 10 次都找到了这个结构。
  • 辩论需要多样的初始答案(违反模块内单调):论文的例子里三个 GPT-4o 生成者都答 8(错),辩论者无从挑选;换成三个不同的模型,答出 8、3、−18,辩论者认出了 3。玩具世界里同一个模型错的时候总错成同一个答案:6 / 10 个池的最优分配用了 2–3 种生成模型,比”三个生成者用同一个模型”高 2.6 个百分点。(第一版代码里辩论者可以在三个答案都错时凭自己的知识答对,结果多样性几乎不重要;改成和论文一致的”只在初始答案里挑”后才出现这个效应,见代码 README。)

4.6 系统视角

LLMSelector 本身是一个离线的”编译”步骤:给定系统结构、训练集和模型池,输出一个分配。但它的结果改变了推理集群要服务的流量:一个分配可能让某个小模型承担大部分调用、让某个大模型只在一个模块里出现。如果再把成本加进来(每个模型的价格不同),目标就变成”在预算内最大化准确率”或”在准确率不降的前提下最省钱”,这与请求级的模型路由(把简单问题交给便宜模型)是同一类问题(练习 7)。


5. 三个层次的路由 · Three levels of routing

层次决定什么需要的信息本课相关
请求 → 副本同一个模型的哪个副本各副本的负载(最好按 token)、缓存了什么、显存余量本讲 §1–§3;第 9 讲 Mooncake;第 7 讲 Dynamo 的 Smart Router
请求 → 模型用哪个模型回答问题难度的估计、各模型的质量与价格§6 的模型路由与级联
模块 → 模型复合系统里每一步用哪个模型模块级的表现(诊断器)、端到端的评测本讲 §4;第 13 讲的智能体程序

三个层次有共同的教训:

  1. 信息决定上限,信息的新鲜度决定会不会出事。最短队列需要实时的队列长度,信息过时就扎堆;前缀亲和需要知道谁缓存了什么,Preble 在派发时就更新全局树、SGLang 路由器用请求历史维护近似的树;LLMSelector 需要模块级的表现,就请一个 LLM 来诊断。
  2. “所有决定都追逐同一个最优”会制造热点。最短队列的扎堆、前缀亲和的崩溃、“三个生成者都用最强的模型”的多样性丧失,是同一种现象。随机性(二选一)、显式的负载保护(再平衡)、显式的多样性,都是解药。
  3. 简单规则拿走大部分收益。二选一只看两台;“命中过半才亲和 + 负载保护”不输 Preble;贪心在多数模型池上不输 LLMSelector。精巧的设计在边界情况(热点、局部最优、过载)下才显出价值。

6. 之后的发展 · Since then

  • 生产中的缓存感知路由器。SGLang 的路由器(2024 年起,Rust 实现)为每个工作进程维护一棵近似基数树(根据派过去的请求记录,存原始字符而不是 token,避免分词开销;后台定期按 LRU 淘汰叶子),不去查询工作进程真实的缓存:最长匹配的比例超过阈值就派给它,否则派给”树最小”(缓存空余最多)的工作进程;各进程未完成请求数的差值超过绝对阈值、且最大值超过最小值的若干倍时,退化为最短队列。代码 02、03 的”缓存感知 + 负载保护”是它的简化版(不过阈值时我们派给负载最轻的,而不是树最小的)。第 16 讲会再讲。NVIDIA Dynamo 的 KV 感知路由(第 7 讲)用各工作进程上报的 KV 事件维护全局索引,同时考虑命中和负载;Kubernetes 生态里也出现了带前缀感知打分的推理网关。
  • 共享的 KV 池。第 9 讲的 Mooncake 把 KV 放进跨机器的分布式池,路由器可以把请求派给空闲的机器、再从池里拉前缀,部分绕开”亲和 vs 均衡”的两难;代价是传输带宽。
  • 模型路由与级联。FrugalGPT(Chen 等 2023,与 LLMSelector 同一第一作者)按顺序尝试由便宜到贵的模型,答案足够可信就停;RouteLLM(Ong 等 2024)训练一个路由器在强、弱两个模型之间选择。它们是”请求 → 模型”这一层的代表。
  • 提示与模型的联合优化。DSPy、TextGrad 优化提示词,LLMSelector 优化模型分配;把两者结合、再加入成本约束,是复合系统优化的自然方向。第 13 讲会从”程序”的角度看复合系统的调度。

7. 小结 · Summary

  1. 多副本集群的路由是一个经典问题:看负载的策略远好于盲目的策略;二选一只看两台就拿到大部分好处;请求大小重尾时要按工作量(token)而不是请求数估计负载;负载信息过时会让最短队列扎堆,二选一则很稳(代码 01)。
  2. 前缀缓存让副本不再一模一样:只看负载,每个副本缓存同一批东西,集群的总缓存只有一个副本那么大;只看缓存(纯亲和),一段共享的系统提示就能把所有请求吸到一个副本上。“命中过半才亲和”兼顾两者,并让缓存容量随副本数增长;但一个超热的前缀仍会压垮单个副本,必须有负载保护(代码 02)。
  3. Preble:全局基数树(派发时就记下”前缀将在哪里”)+ 本地迭代级调度;E2 在 cached > missed 时利用、否则探索;负载代价 L + M + P;事后再平衡复制热点;用前缀命中搭配 prefill / decode;本地优先级组。我们的复现里长文档 / 视频问答收益最大,负载代价几乎只剩 L 一项,简单的缓存感知启发式在中低负载下不输 Preble(代码 03)。
  4. LLMSelector:复合系统里每个模块用哪个模型;一次改一个模块、用 LLM 诊断器打模块分,能跳出端到端贪心的局部最优(案例 0.51 → 0.99);随机模型池上贪心与它相当;诊断器不准时预算上涨;单调性假设不成立时(看不出自己的错、需要多样性),最优分配会刻意混用模型(代码 04)。
  5. 路由有三个层次(请求 → 副本、请求 → 模型、模块 → 模型),共同的教训是:信息与其新鲜度决定上限;所有决定追逐同一个最优会制造热点;简单规则拿走大部分收益。

8. 常见误区 · Common pitfalls

  1. “轮转把请求分得最均匀,所以最好。” 它只让请求数均匀,不看请求大小、不看谁闲;请求大小重尾时它和随机差不多(代码 01 演示 2)。
  2. “最短队列总是最好的。” 它依赖实时信息;信息过时几个服务时间,它就比随机还差(代码 01 演示 3)。
  3. “前缀亲和就是把请求派给前缀匹配最长的副本。” 不加阈值和负载保护的纯亲和会崩溃,一段很短的共享系统提示就足以把所有请求吸到一个副本上(代码 02 演示 1)。
  4. “前缀感知路由的收益只是命中率高一点。” 更根本的是集群的总缓存随副本数增长;只看负载的路由下,加副本不会提高命中率(代码 02 演示 2)。
  5. “Preble 的收益来自精巧的负载代价模型。” 在我们的复现里,L 项比 M、P 大 40–2 万倍,探索近似于”选最近派得最少的副本”;收益主要来自利用 / 探索的规则和再平衡(代码 03 演示 2)。
  6. “prefill / decode 搭配总是有益的。” 当”探索”的请求是很长的 prefill 时,把它塞给 decode 多的副本会挡住那里的 decode(代码 03 演示 2)。
  7. “每个模块都用最强的模型,整个系统就最强。” 模型可能看不出自己的错、多个相同模型会犯同样的错;最优分配常常刻意混用模型(代码 04 演示 4)。
  8. “LLMSelector 总比贪心强。” 它的优势在贪心会卡住的结构里;在随机模型池上两者相当,诊断器不准时它还更费预算(代码 04 演示 2–3)。

9. 练习 · Exercises

练习 1、3、4 的参考答案里的数字是用 cluster_router.py 的函数实际算出来的;练习 6 的数字来自代码 04 的输出。

练习 1(球放进盒子)。nn = 100 万。(a) 按渐近公式,纯随机和二选一时最满的盒子大约有几个球?(b) 代码 01 演示 1 在 nn = 10 万时量到随机 8.2、二选一 3.4,而公式给出 4.71 和 3.53。为什么随机的实测值比公式大得多?

参考答案

(a) ln⁡n/ln⁡ln⁡n=13.82/2.63≈5.26\ln n / \ln\ln n = 13.82 / 2.63 \approx 5.26;ln⁡ln⁡n/ln⁡2=2.63/0.693≈3.79\ln\ln n / \ln 2 = 2.63 / 0.693 \approx 3.79。(b) 渐近公式只给出增长的阶,省略了常数和低阶项;纯随机时精确的结果大约是 ln⁡nln⁡ln⁡n(1+o(1))\frac{\ln n}{\ln\ln n}(1 + o(1)),这个 o(1)o(1) 在 n=105n = 10^5 时还很大(收敛极慢)。二选一的公式是 ln⁡ln⁡n/ln⁡2+O(1)\ln\ln n/\ln 2 + O(1),常数项小,所以吻合得更好。要比较的是”增长速度”:从 1,000 到 10 万,随机从 5.4 涨到 8.2,二选一只从 3.0 涨到 3.4。

练习 2(随机派发 = M/M/1)。泊松到达、指数服务时间,随机把请求派给 16 台服务器中的一台。(a) 每台服务器看到的是什么队列?利用率 0.9 时平均响应时间的理论值是多少?(b) 代码 01 的模拟给出 11.56,为什么和理论值有差距?

参考答案

(a) 泊松流随机分流后仍是泊松流,每台都是独立的 M/M/1 队列,平均响应时间 1/(μ−λ)=1/(1−ρ)1/(\mu - \lambda) = 1/(1-\rho) 个服务时间,ρ\rho = 0.9 时是 10。(b) 有限长度的模拟有统计误差:利用率 0.9 的 M/M/1 队列”记忆”很长(一次长队要很久才消散),5 万个请求分到 16 台,每台只有约 3,000 个,均值的波动可达一两成。换种子或加长模拟会更接近 10。高利用率下的模拟要么跑很长、要么多个种子取平均——代码 01 演示 2 对重尾分布就这么做了。

练习 3(E2 的利用 / 探索)。4 个副本(A6000 + Mistral-7B)。一个长文档请求的提示有 23,000 个 token:13 token 的系统提示、21,000 token 的文档、1,987 token 的问题。副本 2 缓存了系统提示 + 这份文档,其他副本只缓存了系统提示。(a) E2 会利用还是探索?候选副本有哪些?(b) 如果派给副本 2,要 prefill 多少 token、约多少时间?派给副本 0 呢?(用 StepModel(MISTRAL_7B, A6000).prefill(n),忽略已缓存部分对注意力的额外开销。)

参考答案

(a) cached = 21,013,missed = 1,987,cached > missed → 利用;候选只有副本 2(只有它有最长匹配)。如果副本 2 已经很忙、超过最轻副本的 ThbalTh_{bal} 倍,再平衡会把它改派到最轻的副本。(b) 副本 2:约 2,000 个 token,prefill(2000) 约 0.27 秒(2,100 token 时 0.2817 秒);副本 0:约 23,000 个 token,prefill(21000) 就要 3.77 秒,再加问题部分超过 4 秒——相差约 15 倍。这就是为什么”cached > missed”时要利用:省下的计算远大于”能拿去平衡的”那 2,000 个 token。

练习 4(集群的总缓存)。Mistral-7B 在 A6000 上(显存的 90% 可用)能放多少 token 的 KV?长文档负载有 300 份文档、平均约 21,000 token。(a) 一个副本最多能缓存几份平均长度的文档?(b) 4 个副本、轮转 vs 理想的分区,集群一共能缓存几份不同的文档?(c) 按 Zipf 1.1 的热度,最热的 3 份文档占多少流量?

参考答案

(a) kv_capacity_tokens(MISTRAL_7B, A6000) = 219,089 token(48 GB × 0.9 − 14.5 GB 权重,每 token 128 KiB),约 10 份(还要给运行中的请求留空间,实际更少)。(b) 轮转:每个副本缓存的都是”最近最常见”的那 10 份左右,彼此大量重复,集群里不同的文档仍然只有十来份(代码 02 测得复制倍数 1.34);理想分区:约 40 份,互不重复。(c) 最热的 3 份约占 20.3% + 9.5% + 6.1% ≈ 36%。所以即使只缓存最热的几份,命中率也有上限;能缓存 40 份比 10 份多覆盖的是中等热度的长尾——代码 02 演示 2 里命中率从 0.32 升到 0.58 主要来自这部分。

练习 5(优先级组)。Preble 的本地调度器有 PP = 10 个优先级组。(a) 一个请求 100 个 token 里有 63 个已缓存,进第几组?(b) 为什么”要取 55 个请求时从第 10 组取 10 个、第 9 组取 9 个……”?这个方案在什么情况下仍然会让命中少的请求等很久?

参考答案

(a) 第 6 组(论文的例子;按”已缓存比例 × 10”向下取整)。(b) 10 + 9 + … + 1 = 55,每组的份额与优先级成正比:命中多的请求优先(它们只占很少的 prefill 预算、能很快完成),但命中少的组每轮也有份额,不会饿死。仍会等很久的情况:低优先级组里排了大量请求而份额只有 1 个 / 轮;或者显存不够、命中少的请求需要的空间大,轮到它时放不下,只能继续等。代码 03 演示 5 里没有出现饿死:命中少的请求的 P99 和先来先服务时差不多。

练习 6(LLMSelector 的预算)。(a) 10 个模型、6 个模块(三人辩论),穷举要评估多少个分配?LLMSelector 至少要评估多少个(假设从一开始就是最优分配)?(b) 代码 04 演示 1 里 LLMSelector 从 C+C 出发评估了 20 个分配,论文报告它在 TableArithmetic 上只需 10 个。为什么不同?

参考答案

(a) 穷举 10610^6 个。LLMSelector 每轮评估 10 个分配;停止条件是”连续 L+1L + 1 次分配相同”,历史从初始分配算起,即使一开始就最优,也要 6 轮(LL 轮)才能凑齐 7 个相同的记录,即 60 个分配。(b) 记账方式不同。我们的停止条件来自开源实现:C+C → A+C(第 1 轮)→ A+B(第 2 轮)→ A+B(第 3 轮)→ A+B(第 4 轮),4 轮 × 5 = 20;论文图 4(c1) 统计的可能是找到最优分配之前评估的分配数(2 轮 × 5 = 10),不含确认收敛的轮次。读论文的预算数字时,要弄清楚”收敛的确认”算不算在内。

练习 7(加入成本)。假设代码 04 演示 1 的五个模型每次调用的价格分别是 A 5、B 3、C 1、D 0.5、E 0.2(单位任意)。(a) A+B 每道题的成本是多少?比 C+C 贵几倍?(b) 如果目标是”准确率每提高一个百分点最多多花 0.1”,该选哪个分配?(c) 设计一个把成本纳入 LLMSelector 的办法。

参考答案(要点)

(a) A+B:5 + 3 = 8;C+C:2;贵 4 倍。(b) A+B 比 C+C 的测试准确率高 0.99 − 0.51 = 48 个百分点,多花 6,每个百分点 0.125,略高于 0.1 的门槛——严格按这个标准应该留在 C+C,或者去找更便宜的组合(例如 A 定位 + 更便宜的求解模型,如果有的话)。这说明成本约束会改变”最优分配”。(c) 一种办法:把模块得分改成”诊断器得分 − λ × 该模型的价格 / 最高价格”,用不同的 λ 跑几遍得到一条”成本—准确率”曲线,再按预算选点;另一种是在 LLMSelector 收敛后,逐个模块尝试换成更便宜的模型,只要端到端准确率下降不超过阈值就接受。

练习 8(思考:近似树会错在哪里)。SGLang 路由器不查询工作进程真实的缓存,而是根据”派过去的请求”维护近似的基数树,并定期按 LRU 淘汰叶子。(a) 近似树和真实缓存会在什么时候不一致?(b) 不一致会导致什么后果?(c) Preble 的全局树和 Dynamo 的 KV 事件各怎样减少这种不一致?

参考答案(要点)

(a) 工作进程因为显存压力淘汰了某个前缀,近似树却还记着它(树的淘汰节奏和真实的不同);或者请求在工作进程上被拒绝、超时,前缀根本没算出来;工作进程重启后缓存清空。(b) 路由器把请求派给”以为有缓存”的进程,结果要从头 prefill,而且是在一个本来就承接了很多同类请求、可能很忙的进程上;更糟的是这些误派会反复发生,直到近似树把这个前缀淘汰。(c) Preble:本地调度器淘汰时异步通知全局调度器,全局树据此删除对应 GPU 的记录(仍有通知延迟);Dynamo:工作进程上报 KV 块的存储 / 删除事件,路由器的索引直接反映真实状态(代价是事件流的开销)。这是”信息新鲜度”的又一个例子。

练习 9(思考:Preble 遇上 prefill / decode 解耦)。第 7 讲的解耦部署把 prefill 和 decode 放在不同的实例上。如果要在 xPyD 部署上做前缀感知的路由,哪些决定要变?Preble 的”用前缀命中搭配 prefill / decode”还有意义吗?

参考答案(要点)

前缀缓存主要服务 prefill,所以亲和的对象变成 prefill 实例:请求先按前缀命中和负载选 prefill 实例,再按 decode 实例的显存和负载选 decode 实例(两步路由,或者联合优化 KV 传输的距离)。多轮对话的输出也要回写到 prefill 侧的缓存(或像 Mooncake 那样放进共享的 KV 池),否则下一轮命中不了。Preble 的”搭配”规则是为了在同一个实例里混合 prefill 和 decode,解耦部署里两者已经分开,这条规则就没有意义了;但”命中多的请求几乎只剩 decode”这个观察仍然有用——它意味着这类请求的 prefill 实例开销很小,甚至可以直接在 decode 实例上补算那一小段(一些系统就是这样做的)。


10. 延伸阅读 · Further reading

本讲的必读材料

  • V. Srivatsa, Z. He, R. Abhyankar, D. Li, Y. Zhang. Preble: Efficient Distributed Prompt Scheduling for LLM Serving. ICLR 2025(arXiv:2407.00023)。
  • L. Chen, J. Q. Davis, B. Hanin, P. Bailis, M. Zaharia, J. Zou, I. Stoica. Optimizing Model Selection for Compound AI Systems. arXiv:2502.14815,2025。

经典负载均衡

  • Y. Azar, A. Broder, A. Karlin, E. Upfal. Balanced Allocations. STOC 1994(球放进盒子的”二选一”)。
  • M. Mitzenmacher. The Power of Two Choices in Randomized Load Balancing. IEEE TPDS 2001。
  • M. Mitzenmacher. How Useful Is Old Information? IEEE TPDS 2000(过时信息与扎堆)。

相关工作

  • M. Zaharia et al. The Shift from Models to Compound AI Systems. BAIR Blog,2024。
  • O. Khattab et al. DSPy: Compiling Declarative Language Model Calls into Self-Improving Pipelines. ICLR 2024(第 13 讲的选读)。
  • L. Chen, M. Zaharia, J. Zou. FrugalGPT: How to Use Large Language Models While Reducing Cost and Improving Performance. TMLR 2024。
  • I. Ong et al. RouteLLM: Learning to Route LLMs with Preference Data. 2024。
  • SGLang 路由器(sgl-project/sglang 仓库中的 cache-aware 策略实现与文档)。

背景

  • 第 3 讲:RadixAttention(Preble 的单机前缀缓存);第 7 讲:分块 prefill、Dynamo 的 Smart Router;第 9 讲:Mooncake 的 KVCache-centric 调度;前置讲 P4:排队论。

11. 下一讲预告 · Next lecture

这一讲的集群里只有一个模型,每个副本独占自己的 GPU。可现实中的推理平台要服务成百上千个模型:大多数模型流量很小、而且随时间起伏,给每个模型都留几块 GPU 太浪费;把多个模型塞进同一块 GPU,又会互相干扰,冷门模型一来请求还要先花几十秒把权重加载进来。第 12 讲:多租户与 GPU 共享读两篇论文:LithOS 想给 GPU 做一个”操作系统”——按 TPC(GPU 上比 SM 更粗一级的单元)细粒度地分配算力、把大内核切成小块以便抢占、按需调整频率省电;Prism 让多个模型共享 GPU 显存,像”气球”一样按需伸缩各模型的 KV 空间,统一空间共享和时间共享;选读 Fork in the Road 分析生产级 serverless 平台冷启动的延迟从哪里来。我们会模拟多个模型在流量漂移下的静态划分 vs 弹性共享、时间片 vs 空间共享的延迟,以及冷启动的代价。

← 返回课程目录