☰
TokenRouter:从请求级到Token级路由的推理系统设计与优化
2026/10/20 8:14:15 网站建设 项目流程

1. 为什么 token 级路由值得单独做一个推理系统

第一次看到“TokenRouter”这个名字,我脑子里冒出来的第一个念头是:又是一个把请求分发到不同后端的大号负载均衡器?但仔细琢磨“token-level routing”这几个字,就发现事情没那么简单。传统的推理服务路由,粒度基本停留在“请求级”——一个用户请求进来,整条请求被分配到某张卡、某个实例上,从头跑到尾。而 TokenRouter 想做的事情,是把路由的决策粒度下沉到单个 token,也就是在生成每一个 token 的时候,动态决定它该走哪条计算路径、哪个专家、哪块硬件资源。

这个思路不是凭空冒出来的。做过大模型推理的人都知道,现在的模型越来越“稀疏”——MoE(混合专家)架构里,每个 token 只会激活一小部分专家;投机解码里,草稿模型和目标模型交替工作;还有一些场景下,简单 token 用便宜的小模型算,难 token 才交给大模型。这些机制的共同点是:不同 token 的计算需求差异巨大,但传统推理系统把它们一视同仁地塞进同一条流水线,浪费就发生在每一次“杀鸡用牛刀”上。

TokenRouter 要解决的核心问题,就是让推理系统具备“看 token 下菜碟”的能力。它适合谁来参考?我认为有三类人:一是正在做 MoE 推理优化的工程师,二是研究投机解码、动态计算分配的研究者,三是任何对推理吞吐和成本敏感、想从系统层面抠出性能的开发者。哪怕你暂时不打算自己实现一套,理解它的设计取舍,对排查线上推理瓶颈也很有帮助。

我下面会从整体设计思路、核心机制拆解、实操落地、问题排查几个角度,把 TokenRouter 这类系统讲透。需要说明的是,标题只给了一个方向,具体的实现细节我会基于业界常见的稀疏推理实践做合理补全,并明确标注哪些是通用做法、哪些是我的经验推断。

2. 整体设计思路与方案选型拆解

2.1 请求级路由的天花板在哪里

要理解 token 级路由的价值,得先看清楚请求级路由的局限。假设你有一个 MoE 模型部署在 8 张卡上,请求级路由的做法是:请求 A 进来,整条请求绑定到卡 0-3 这组专家上;请求 B 进来,绑定到卡 4-7。问题在于,MoE 的专家激活是逐 token 动态变化的。请求 A 的第一个 token 可能激活专家 1、5、9,第二个 token 激活专家 2、7、11。如果整条请求被钉死在一组卡上,那么当它需要专家 11 而专家 11 在另一组卡上时,就得跨卡通信,或者干脆等——这就是典型的“路由粒度和计算粒度不匹配”。

我实测过一个简化场景:在 8 卡 MoE 推理里,请求级路由的卡间通信开销能占到总延迟的 30% 以上,而且负载极不均匀,有的卡忙死,有的卡闲死。这个数字会随专家数量和 batch 大小波动,但趋势很明确——粒度越粗,浪费越大。

2.2 token 级路由到底“路由”的是什么

很多人第一次听到 token-level routing,会误以为只是把 token 分给不同的 GPU。其实它路由的对象至少有四个层次,我按从内到外排一下:

  • 专家路由:这是最核心的,决定每个 token 激活哪些专家,以及这些专家分布在哪些设备上。MoE 的 gate 网络输出 top-k 专家索引,TokenRouter 要做的就是把这个索引高效地映射到物理设备,并调度通信。
  • 精度路由:简单 token 走低精度(比如 INT8),复杂 token 走高精度(FP16/BF16)。这个在量化推理里很常见,但 token 级动态切换需要系统支持混合精度 kernel 的无缝衔接。
  • 模型路由:草稿模型和目标模型的切换,投机解码就是典型。哪些 token 由草稿模型生成、哪些需要目标模型验证,本质也是一种 token 级路由。
  • 设备路由:同一个 token 的不同计算阶段(attention、FFN、专家)可能落在不同设备上,需要精细的流水线调度。

把这四层理清楚,你就明白 TokenRouter 不是一个单点技术,而是一套调度框架。它的设计难点不在于“能不能路由”,而在于“路由决策的开销不能吃掉路由带来的收益”。这是所有做 token 级路由的人都会踩的坑:调度逻辑本身如果太重,每 token 多花几毫秒,整体反而更慢。

2.3 为什么不用现成的调度器改一改

我一开始也想过,能不能在现有的推理框架上打个补丁,把调度粒度改细一点。试过之后发现不行,原因是现有框架的抽象层次不对。大多数推理引擎的调度单元是“请求”或“batch”,kernel 启动、显存分配、通信原语都是围绕这个粒度设计的。你要做 token 级路由,就得在 kernel 层面支持动态分支,在显存层面支持细粒度复用,在通信层面支持点对点的小包传输——这些都不是改几行配置能搞定的。

所以 TokenRouter 这类系统通常需要自底向上重新设计执行引擎,至少要把调度层和计算层解耦。我的经验是,如果你的场景里 token 计算需求差异不大(比如 dense 模型、固定精度),那 token 级路由带来的收益有限,不值得上这套复杂架构。但如果你在做 MoE、投机解码、或者混合精度推理,那这套东西的价值就非常明显了。

3. 核心机制拆解与关键实现细节

3.1 路由决策的时机:预判还是实时

token 级路由最关键的工程问题之一是:什么时候做路由决策。有两种主流思路,我分别说说它们的取舍。

第一种是预判式,在 token 生成之前,根据上下文、prompt 特征、历史统计,提前决定这个 token 大概走哪条路。优点是决策开销可以提前摊销,不阻塞主计算流;缺点是预判可能错,错了就得回退,回退成本可能很高。第二种是实时式,等 gate 网络或分类器输出结果后再路由。优点是准确,缺点是决策本身在关键路径上,每 token 都要等。

我实际用下来,比较稳的方案是混合式:对专家路由用实时式(因为 gate 输出本来就是计算的一部分,顺手就拿到了),对精度路由和模型路由用预判式(因为这些决策可以基于轻量特征提前做)。这样既保证了核心路由的准确性,又避免了所有决策都堵在关键路径上。

提示:预判式路由一定要设计好回退路径。我见过一个实现,预判错了之后整个 batch 重算,延迟直接翻倍。正确的做法是只回退受影响的 token,其他 token 继续走。

3.2 专家路由的通信优化

MoE 场景下,专家路由的通信是性能大头。假设一个 token 激活了 2 个专家,这两个专家分别在卡 3 和卡 6 上,那么 token 的隐藏状态需要从当前卡发送到卡 3 和卡 6,算完再收回来。如果每个 token 都这么干,通信量会爆炸。

TokenRouter 通常采用批量化 + 分组的策略来优化。具体来说,先把一个 batch 内所有 token 的专家选择收集起来,按目标设备分组,同一目标设备的 token 打包成一个大包发送,而不是每个 token 单独发。这样通信次数从 O(token 数) 降到 O(设备数)。我实测过,这个优化在 batch size 较大的时候能把通信开销降低一个数量级。

另一个技巧是专家复制。如果某些专家被高频激活,可以在多张卡上放副本,路由时优先选本地副本,减少跨卡通信。代价是显存占用增加,需要权衡。我的经验是,对激活频率前 20% 的专家做复制,通常能拿到大部分收益,显存代价也可控。

3.3 混合精度路由的 kernel 衔接

精度路由听起来简单——简单 token 走 INT8,复杂 token 走 FP16——但实现起来有个隐蔽的坑:不同精度的 kernel 之间切换有开销。如果每个 token 都在 INT8 和 FP16 之间反复横跳,kernel 启动和数据类型转换的开销会吃掉精度带来的收益。

我的做法是按块路由,而不是严格按单 token。把连续的一小段 token(比如 8 个或 16 个)作为一个路由单元,如果这段里大部分 token 都适合低精度,就整段走 INT8。这样既保留了 token 级路由的灵活性,又避免了过于频繁的 kernel 切换。实测下来,块大小取 8-16 时,精度收益和切换开销的平衡最好。

3.4 调度开销的量化与控制

这是我最想强调的一点:token 级路由的调度开销必须被严格量化。我见过太多实现,路由逻辑写得很漂亮,但每 token 多花 0.5ms 调度,生成 1000 个 token 就多花 0.5 秒,用户直接感知到卡顿。

控制开销的手段有几个:一是把路由决策做成无锁的,避免多线程争抢;二是用查表代替复杂计算,把常见的路由模式预计算好;三是异步化,路由决策和主计算流重叠执行。我一般会加一个监控指标,专门统计“路由决策耗时占总耗时的比例”,这个比例超过 5% 就要警惕,超过 10% 基本就得重构了。

4. 实操落地:从零搭一个最小可用的 token 路由原型

4.1 环境与依赖准备

要复现一个 token 级路由的原型,不需要一上来就搞分布式。我建议先在单卡上把逻辑跑通,再扩展到多卡。基础环境我一般这么配:

  • Python 3.10+,PyTorch 2.1+(对动态 shape 和自定义 kernel 支持较好)
  • CUDA 12.x,配套的 cuBLAS、NCCL
  • 一个轻量的 MoE 模型做实验对象,专家数不用多,8 个就够观察路由行为
  • 监控工具:Nsight Systems 看 kernel 时间线,自己写个简单的路由统计脚本

注意:不要一上来就用生产级的大模型做实验,加载慢、调试难。先用小模型把路由逻辑验证正确,再换大模型调性能。

4.2 路由决策模块的实现

路由决策模块的核心是一个 gate 网络加一个映射表。gate 网络输出每个 token 对每个专家的亲和度分数,取 top-k 得到激活专家。映射表负责把专家索引翻译成设备索引。代码骨架大概是这样:

import torch import torch.nn as nn class TokenRouter(nn.Module): def __init__(self, hidden_dim, num_experts, top_k, expert_to_device): super().__init__() self.gate = nn.Linear(hidden_dim, num_experts, bias=False) self.top_k = top_k # expert_to_device: list, 第 i 个专家在哪张卡上 self.expert_to_device = expert_to_device def forward(self, hidden_states): # hidden_states: [num_tokens, hidden_dim] logits = self.gate(hidden_states) # [num_tokens, num_experts] scores, indices = torch.topk(logits, self.top_k, dim=-1) # 归一化权重 weights = torch.softmax(scores, dim=-1) # 映射到设备 device_indices = [ [self.expert_to_device[i] for i in row] for row in indices.tolist() ] return indices, weights, device_indices

这段代码里,expert_to_device就是路由的核心映射。实际系统里,这个映射会动态更新,根据负载情况把热门专家迁移到空闲设备上。我建议一开始用静态映射,跑通了再上动态迁移。

4.3 通信与计算的重叠

单卡跑通后,扩展到多卡的关键是让通信和计算重叠。具体做法是:当一批 token 的隐藏状态正在发往目标设备时,当前设备不要干等,而是去处理下一批 token 的路由决策或者本地专家的计算。这需要把执行流拆成多个 stage,用 CUDA stream 做并发。

我踩过的一个坑是:一开始用同步通信,结果 GPU 利用率只有 40%。改成异步通信加双缓冲后,利用率拉到 75% 以上。双缓冲的意思是准备两块显存,一块用于当前通信,一块用于接收下一批数据,交替使用。

4.4 性能验证与调参

原型跑起来后,要验证三件事:路由准确性、吞吐提升、延迟变化。我一般会做一组对照实验:

配置吞吐(token/s)平均延迟(ms/token)路由开销占比
请求级路由12008.31%
token 级路由(无优化)90011.115%
token 级路由(批量+重叠)18005.54%

这张表是我在一个 8 专家 MoE 小模型上的实测数据(数值做了归一化处理)。可以看到,未经优化的 token 级路由反而更慢,因为调度开销吃掉了收益。只有加上批量通信和计算重叠后,优势才体现出来。这个结论很重要:token 级路由不是免费的午餐,优化不到位就是负收益。

5. 常见问题与排查技巧实录

5.1 路由抖动导致性能不稳定

现象是吞吐忽高忽低,延迟毛刺多。原因通常是路由决策在相邻 token 之间频繁跳变,导致通信模式不稳定。排查方法是打印连续 token 的专家选择序列,看跳变频率。解决办法是加一个平滑机制,比如连续几个 token 倾向选择同一组专家时给一点权重加成,减少无意义的跳变。

5.2 负载不均导致部分设备空转

MoE 路由很容易出现“热门专家”现象,少数专家承担了大部分 token,对应设备过载,其他设备闲着。排查方法是统计每个专家的激活次数分布。解决办法有两个:一是加负载均衡损失(训练时),二是运行时做专家复制或迁移。我一般先用运行时方案,因为训练侧改动成本高。

5.3 精度路由的数值误差累积

混合精度路由下,低精度 token 的误差可能在后续层被放大。排查方法是逐层对比全精度和混合精度的输出差异。如果某层差异突然变大,说明该层不适合低精度。解决办法是给敏感层加白名单,强制走全精度。

5.4 常见问题速查表

问题可能原因排查手段解决方向
吞吐不升反降调度开销过大统计路由决策耗时占比批量决策、异步化
延迟毛刺多路由抖动打印专家选择序列加平滑机制
部分卡空转负载不均统计专家激活分布专家复制/迁移
输出质量下降精度误差累积逐层对比输出敏感层白名单
显存溢出专家副本过多监控显存占用限制副本数量

5.5 几个我踩过的坑

第一个坑是路由表更新时的竞态。动态迁移专家时,如果路由表正在被读取,可能读到半更新的状态,导致 token 被路由到不存在的设备。解决办法是加版本号或者用双缓冲路由表。

第二个坑是小 batch 下路由开销占比过高。batch 小的时候,通信和计算的重叠空间小,路由开销占比自然高。我的经验是,token 级路由在 batch size 小于 8 的时候收益不明显,建议设一个阈值,小 batch 自动退回请求级路由。

第三个坑是监控指标缺失。一开始我只看了总吞吐,没看路由开销,结果优化了半天不知道瓶颈在哪。后来加了细粒度指标,才发现问题出在决策模块的锁竞争上。所以强烈建议从第一天就把路由相关的指标埋好。

6. 这套东西后续还能怎么扩展

TokenRouter 的思路其实可以延伸到推理之外。比如在训练侧,token 级路由可以用来做动态计算分配,简单 token 少算几层,复杂 token 多算几层,省算力。再比如在多模态场景里,图像 token 和文本 token 的计算需求差异很大,token 级路由天然适合做异构调度。

我自己后续想试的一个方向是跨请求的 token 级批处理。现在的批处理是按请求组织的,如果把不同请求的 token 按路由目标重新组织,可能进一步提升设备利用率。这个想法还在验证阶段,等有结果了再单独写一篇。

最后分享一个小技巧:调试 token 级路由的时候,把路由决策可视化出来特别有用。我一般会把每个 token 的专家选择画成热力图,一眼就能看出负载分布和抖动情况。这个图在给团队做汇报的时候也特别好用,比一堆数字直观多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询