AI模型逐字接力怎么调度?TokenRouter提出请求为中心方案
你有没有想过,一个AI大模型回答问题的时候,其实可能不是"一个"模型在干活?2025年,一篇叫R2R的研究发现了一件挺颠覆认知的事:让一个32B的大模型和一个1.5B的小模型配合着写,只需要把5%的"难词"交给大模型处理,剩下95%全靠小模型搞定,最终答案的质量居然能跟全程用大模型一样好。这比"每次问题整体交给大模型还
一个AI大模型给出回答时,背后干活的真的只是“一个”模型吗?
2025年有篇名为R2R的研究给出了一个挺颠覆认知的结果:让32B的大模型和1.5B的小模型搭班写,只把5%的“难词”交给大模型,剩下95%都由小模型包了,最后答案质量竟能和全程用大模型持平。这比按整个问题粗放分派聪明得多,原来那种分法,可能得把40%的问题都扔给大模型,才能拿到同样效果。
这个结果让人兴奋。要是真能做到“每个字都挑一挑该由哪个模型来写”,省钱也好、提质也罢,想象空间都相当大。
不过TokenRouter这篇论文的作者们很快撞上一个尴尬现实:想法很漂亮,放到真实服务器上跑,慢得让人绝望。
核心问题:理论上的美好,工程上的地狱
先把话说清楚,所谓“按token路由”究竟指什么。
AI生成文字,是一个字一个字(准确说是一个token一个token)往外蹦的过程,这叫作解码。
解码*:大模型依据已经生成的内容,预测并输出下一个字(token),如此一步步循环,直到整段回答写完。
传统路由走的是“session级”或者“query级”,也就是整段对话、整个问题自始至终交给同一个模型。ChatGPT、Cursor这些商业产品目前就是这么做的:先判断你的问题难不难,再分派给合适的模型,分派定了,这个模型就得把活干到底。
token级路由激进得多:每生成一个字,都重新判断一次这个字该由哪个模型来写。简单的字交给小模型,省钱也快;碰上需要深度思考的难点,马上切换到大模型。这一段写完,可能又切回小模型。
听着挺美,问题也随之而来。业界常用的SGLang和vLLM这类现有服务系统,打设计之初就是给“单个模型服务”用的。它们藏着一个假设:同一批正在处理的请求步调应当一致,大家一起往前走一步,再一起走下一步。
这个假设在token级路由面前彻底塌了,论文点出了三个具体的坑。
第一个坑是步调不一致。32B大模型算一步的耗时,可能是1.5B小模型的好几倍。系统若硬要这批请求按同一节奏走,就好比一群人跑步比赛,非等最慢的人跑完这一圈,所有人才能开始下一圈。快的人只能大把时间干等着,显卡资源白白浪费。
这就像和朋友们一起吃自助餐,有人吃得快有人吃得慢。餐厅若规定所有人必须同时去同一个窗口取同一道菜,吃得快的人就只能眼巴巴看着慢的人,谁也没法按自己的节奏吃完整顿饭。不改变这个“统一节奏”的规则,快的人永远要给慢的人陪跑,饭吃到一半肚子早就饿得咕咕叫了。
第二个坑叫批次准入延迟。token级路由让模型切换特别频繁,一个刚被路由过来的请求,常常撞上目标模型正忙着处理上一批任务,只好排队等下一轮。这种排队会制造出大量“空隙”,原本能攒成一大批一起处理的请求,被拆得支离破碎。
第三个坑是实现复杂度太高。现有服务系统压根没给“每一步都要做路由决策”留接口,开发者想实现token级路由,得对一个庞大的代码库大动干戈,还得跟系统里原本耦合很紧的连续批处理、前缀缓存等功能小心协调,稍有不慎就牵一发动全身。
TokenRouter的解法:让开发者只管写“一个请求的旅程”
面对这三个坑,TokenRouter团队拿出的设计原则叫“以请求为中心编程,以模型为中心执行”。
这句话听着有点绕,拆开讲其实很直白。开发者写路由逻辑时,完全不用操心底层批处理、调度、异步执行那些脏活累活,只需站在“一个请求”的视角,描述它该怎么在不同模型之间流转。而真正跑起来时,系统会在背后把这套逻辑拆成多个独立运转、以模型为单位的服务进程,异步调度的复杂性由系统自己消化。
这种思路有点像去银行办业务。你不用知道银行后台怎么调度柜员、怎么分配窗口,只需知道“先取号,再等叫号,然后到窗口办事”,剩下的调度都是银行内部系统的事。要是银行非要求你自己学会排班算法才能取号,估计没人愿意去了。TokenRouter想让开发者像普通储户一样简单,而不是把每个人都逼成排班专家。
具体来说,TokenRouter把路由算法抽象成三个函数:route(路由判断)、send(发送请求)、receive(接收请求)。
route函数在每解码完一步之后被调用,它根据刚生成的内容(比如logits、隐藏状态、采样出的token),判断这个请求接下来是留在本地模型继续写,还是转交给另一个模型。
send函数在决定转交时被调用,负责打包一个“请求包”,里面装着请求ID、对方模型还没见过的那部分token、请求的状态(是还在进行还是已经结束),以及算法需要的任何额外信息。
receive函数在收到别的模型转交来的请求包时被调用,负责把这个包转换成本地能识别的格式,让接收方模型接着往下写。
论文团队拿五种不同的token级路由算法做了验证,分别是CITER、R2R、R-Stitch、Co-LLM和ME,结果每一种都能被这三个函数干净地表达出来,没有哪个算法需要绕开这套框架另搞一套。
系统架构上,TokenRouter给每个模型配了一个“独立小秘书”。
接口设计回答的是“开发者怎么写代码”,真正难啃的骨头是“系统怎么跑得快”。
TokenRouter的做法是:每个参与路由的大模型都配一个专属的子服务器*(subserver)。这是一个独立运行的服务进程,里头有自己的调度器、模型运行器和私有的KV缓存池,只为一个LLM的推理工作服务。
这些子服务器各自独立运转,谁也不等谁,彼此之间靠“对等请求”消息通信协调。对外,整个系统只暴露一个统一接口,客户端根本感知不到背后有几个模型在互相配合,用起来和直接访问一个普通的单模型服务器没差别。
步调不一致的问题,靠解耦三循环执行来解决。
解耦三循环执行*:标准单模型服务原本就有两个异步循环,一个是客户端服务器循环,负责接收请求和返回结果,另一个是解码循环,负责调度和执行模型推理。TokenRouter在这两个之外又加了第三个“跨模型循环”,专门在不同模型的子服务器之间收发请求。
有了这第三条循环,被路由出去的请求可以先离开本地处理队列,转到另一个模型那边处理,处理完再自动回来接上。它离开的这段时间,本地剩下的请求照常跑,谁也不用等谁。
请求“回家”时的效率问题,论文专门设计了handoff-resume(交接-恢复)机制。如果每次从别的模型转回来都当成全新请求处理,那就要重新做前缀匹配、重新分配KV缓存,这些都是实打实的计算开销。TokenRouter为此引入了一个“挂起”状态,夹在“运行中”和“已完成”之间。请求被转出去时标记为挂起,调度器直接跳过它,但它的服务状态完整保留。等它从别的模型那边处理完回来,把新生成的token直接拼接上去,重新塞回本地队列继续跑,就像它从来没离开过一样。
这很像你去另一个部门办事盖章,办完回到自己工位,不用重新打卡、重新登记工位信息,拿着盖好章的文件接着干活就行。要是每次回来都得重走一遍入职流程,光手续就能把人耗死,真正干活的时间反而被挤占了。
实验数据印证了这个机制的价值。论文团队测了一个没有这套交接机制的“标准服务基线”(Std. Serving),看它每一步解码的时间都花在哪儿:前缀匹配占20.94%,缓存节点加锁占17.47%,释放锁占13.81%,更新基数缓存(RadixCache)占32.62%,真正用于模型推理计算的时间只占4.2%。换句话说,九成以上的时间都耗在“重新认领身份”这类行政手续上,干活的时间少得可怜。
延迟批处理:用一点点等待换来更大的效率。
步调不一致的问题解决了,批次准入延迟这个坑还没填。
论文给了一个很直观的对比实验,画了四种调度方式的时间线图。用“谁先来谁先处理”这种急性子策略(论文里叫Eager-Async,即时异步),请求会零零散散到达,每次都得单独开一批处理,平均延迟是10.83个时间单位。TokenRouter采用的延迟批处理策略,平均延迟降到8.67,比急性子策略快了将近21%。
延迟批处理的思路是,子服务器不要一有请求到达就立刻处理,先攒一攒,等同模型请求攒够一个阈值数量,再一起打包处理。
这听上去有点反直觉:明明要降延迟,为什么反而故意多等一会儿?
关键在于“攒批”能摊薄成本。打个比方,煎饼摊来一个客就摊一张,看似最麻利,但每摊一张都得重新热锅、重新调面糊,效率并不高。反之,愿意等一等,把三四个顾客的单子攒齐了再一口气连着摊,头一位虽多等几十秒,整体出餐速度反倒上去了,锅和面糊的重复准备被分摊掉了。若死守一客一摊,摊主再拼也供不上多少人,队伍只会越排越长。
当然,这里有个关键权衡:攒批的阈值该定多大?
太小,批次准入延迟的问题压根没解决。太大,又会出现另一个问题:请求困在队列里迟迟凑不够数,小模型那边没活干,时间白白浪费。论文把这个阈值叫B,还专门搭了一套数学模型,用来算出让吞吐量最大化的最优阈值B*。
这套数学模型建立在离散时间马尔可夫链*(DTMC)之上:它专门刻画系统状态随时间随机变化的过程,下一个状态是什么,只看当前状态,更早的历史并不参与决定。
简单说,论文把整个系统每一次“有模型处理完一批请求”的时刻定义成一个状态,状态里记录每个模型排队等待的请求数、正在执行的批次大小、以及这批还要多久执行完。给定路由算法的转移概率和每个模型单步解码所需的时间,这套模型就能推导出吞吐量作为阈值B的函数,再搜索出能让吞吐量最大化的最优阈值。
论文在R2R算法上专门做了验证:固定小模型的阈值为1,调整大模型的阈值B1,对比理论预测吞吐量和硬件上实际测出来的吞吐量。结果显示,并发数为2、3、4的各种情况下,数学模型预测的最优阈值和实际测出的最优阈值高度吻合。这说明这套数学建模不是纸上谈兵,真能指导实际参数调优。
实际效果:吞吐量涨了多少。
设计思路说了这么多,最终还是要看数字说话。
论文选取了8张A100-80G GPU组成服务器,围绕五种token级路由算法(CITER、R2R、R-Stitch、Co-LLM、ME)和三类工作负载(低难度推理、高难度推理、代理型多轮任务)展开测试,把TokenRouter、官方原始实现(Official Code)与作者自建的标准SGLang基线(Std. Serving)放在一起对比。
15种“算法加工作负载”组合跑下来,TokenRouter的解码吞吐量比表现最好的基线高出2.01到64.15倍;端到端请求完成延迟相比标准服务基线缩短了2.03到63.64倍。
输出变长时的表现尤其稳。论文对比了“低难度推理”(最大输出2048个token)和“高难度推理”(最大输出8192个token)两种场景:标准服务基线的吞吐量在输出变长后掉了58.1%到85.2%,TokenRouter则基本平稳,几乎没受影响。
原因并不复杂:输出越长,模型之间切换的次数就越多,每次切换若都要付出“重新认领身份”的代价,累积起来相当可观。TokenRouter用挂起恢复机制把这笔开销压到最低,自然不怕输出变长。
吞吐量与单用户响应速度的权衡图上,有个数字很扎眼:并发数从1涨到16时,TokenRouter吞吐量涨了8.61倍,单用户速度还能保住51.7%;官方R2R实现吞吐量只涨到5.14倍,单用户速度只剩31.3%。更夸张的是,TokenRouter在并发16时的吞吐量是R2R并发1时的18.58倍,单用户速度反而高出1.13倍。即便对响应速度要求更苛刻,它也能在这个更严格的约束下跑出远超对手的吞吐量。
工程优化拆开看同样清楚。并发数为8时,单是扩展CUDA图这一项,就把基础实现的132.8 token/s提到230.79 token/s,相当于官方R2R实现(134.9 token/s)的1.71倍;再叠加异步执行和延迟批处理,最终跑到372.48 token/s,整体提升2.76倍。
换模型组合,这套优化思路照样稳。论文测了从0.6B配8B到4B配8B共四种小模型大模型搭配,相比R2R,TokenRouter吞吐量提升在1.99到3.21倍之间,说明系统优化并非针对某种模型大小的取巧,而是具备普适性。
吞吐量与准确率的权衡图上,论文把TokenRouter、R2R和几种query级路由方法(来自RouteLLM的几个变体)摆在一起比较。结果显示,TokenRouter把token级路由推到了一条全新的帕累托前沿:同样准确率水平下吞吐量更高,同样吞吐量水平下准确率也更高,整体上比粗粒度的query级路由更有竞争力。
写在后面
读这篇论文,最大的感触是:不少在算法层面已被证明“更好”的想法,卡在工程落地这一步,原因往往不是算法本身不行,而是整个服务系统的底层假设和新想法天然冲突。R2R那篇论文证明了token级路由在理论上确实比query级路由聪明,可如果没有TokenRouter把“怎么把这事跑快”解决掉,那个漂亮的5%路由比例大概率只能停在实验室的离线评测里,没法真正服务用户。
这让人想起很多“学术上可行、工程上没人做”的案例,差的往往不是一个新想法,而是一整套配套的基础设施。
论文里标准服务基线的延迟拆解表格值得多看两眼:九成以上的时间耗在前缀匹配和缓存维护这些“行政手续”上,真正的模型推理只占4.2%。这个数字说明一个挺反直觉的事实——系统慢,很多时候不是因为“算得慢”,而是因为“管理账本太麻烦”。这跟企业里“开会讨论怎么干活的时间比真正干活的时间还长”是一个道理。
论文在结尾也点出了一处局限:这套数学模型把两次路由之间出现的 token 数当作服从几何分布来处理。对多数 token 级路由算法来说,这个前提站得住;可一旦遇到不符合该假设的边界情况,优化的结果就未必准确。论文把问题留给了后续研究——路由模式若变得更为复杂、偏离简单概率分布更远,延迟批处理的最优阈值又该如何重新推导?
Q&A
Q1:TokenRouter是什么?
A:这是一个专为token级LLM路由设计的高效服务系统,采用“请求为中心编程,模型为中心执行”的架构,解决多模型协作推理时的步调不一致和批次延迟问题,吞吐量比现有实现最高提升64.15倍。
Q2:token级路由和传统的query级路由有什么区别?
A:query级路由把整个问题交给一个模型从头写到尾;token级路由每生成一个字都重新判断该用哪个模型,粒度更细。像R2R算法只需把5%的token交给大模型,就能达到跟全程用大模型相当的质量。
Q3:TokenRouter为什么要用延迟批处理而不是即时处理请求?
A:换成立即处理的思路,请求到达时目标模型若正忙,就只能排队等,批次因此被切得很碎。延迟批处理的做法是先把同一模型的请求攒到一定数量,再统一处理,用稍微多等一会儿换取更大的批次,整体延迟反而降下来了。