☰
TokenRouter:从请求级到Token级的推理路由优化实践
2026/10/14 22:53:18 网站建设 项目流程

1. 从一次推理延迟抖动说起:为什么要做 token 级路由

去年帮一个做智能客服的朋友排查线上问题,现象很怪:同一台推理服务器,白天响应稳定在 200ms 左右,一到晚上高峰期就飙到 1.5s,GPU 利用率却只有 40% 上下。日志翻了个底朝天,最后定位到根因——请求长度分布极度不均。短请求(十几个 token 的意图识别)和长请求(上千 token 的多轮对话摘要)混在同一个 batch 里,长请求把整个 batch 的完成时间拖死,短请求明明算完了也得干等,显存里塞着一堆已经算完的序列占着位置不放。

这个问题在推理系统里非常典型。传统做法是按请求(request-level)做调度和路由:一个请求进来,要么全走这条路径,要么全走那条路径,batch 也是整请求进整请求出。可真实业务里,一个请求内部的 token 之间差异巨大——开头几个 token 可能是系统提示词,中间是用户问题,结尾是格式约束。不同位置的 token 对算力的需求、对精度的敏感度、对延迟的容忍度完全不同。TokenRouter这个项目要解决的核心问题就在这:把路由和调度的粒度从"请求"下沉到"token",让每个 token 或者每组 token 能独立决定走哪条计算路径、进哪个 batch、什么时候可以提前释放。

说白了,它想干的事是:让推理系统像交通调度一样,不再按"整车"放行,而是按"每节车厢"甚至"每个座位"来动态分配车道。这个思路对做推理优化的同学来说,价值在于它把优化空间从请求级一下子打开到了 token 级,理论上能同时改善吞吐、延迟和显存利用率。适合谁看?如果你在做 LLM 推理服务、写过 continuous batching、折腾过 PagedAttention 或者 speculative decoding,这篇会比较对胃口;如果你只是调用 API 的应用层同学,也能从中理解为什么有些推理服务快有些慢。

2. TokenRouter 的整体设计与思路拆解

2.1 核心思路:把"路由决策"从请求级下沉到 token 级

先把这个概念讲清楚。所谓 token-level routing,指的是在推理过程中,针对序列中的每一个 token(或每一组 token),动态决定它的计算路径。这个"路径"可以有好几层含义,我按从浅到深列一下:

  • 计算精度路径:这个 token 走 FP16 还是 INT8,走全量注意力还是稀疏注意力。
  • 模型分支路径:走主干大模型还是走一个小型草稿模型(类似 speculative decoding 的思路,但粒度更细)。
  • 调度路径:这个 token 进哪个 batch、和哪些 token 拼在一起、什么时候可以出队。
  • 显存路径:KV cache 放在哪一级存储,是否值得保留,是否可以提前驱逐。

传统 request-level routing 的问题是决策太粗。一个请求里如果只有 10% 的 token 需要高精度,剩下 90% 其实可以走低精度快速通道,但 request-level 只能整请求统一处理,要么全高要么全低,浪费就出来了。TokenRouter 的设计动机就是消除这种"粒度错配"。

2.2 为什么不是 request-level 就够了:三个真实痛点

我拿实际场景说明为什么必须下沉到 token 级。

痛点一:混合长度 batch 的尾部浪费。假设一个 batch 里有 8 个请求,长度分别是 20、30、50、80、120、200、500、1000。continuous batching 虽然能在请求完成时立刻换入新请求,但一个 batch 内所有序列必须同步推进到最长序列结束。那 7 个短请求算完后,它们的 slot 被占着,新请求进不来,GPU 在给一个长序列"陪跑"。如果按 token 级调度,短序列的 token 算完就能立刻释放 slot,长序列的 token 继续留着,利用率能明显改善。

痛点二:prefill 和 decode 的资源冲突。prefill 阶段是计算密集型,decode 阶段是访存密集型。混在一起跑,GPU 的算力和带宽都没吃满。TokenRouter 允许把 prefill token 和 decode token 分开路由到不同的计算队列,各自用最适合的 kernel。

痛点三:不同 token 的精度需求差异。有研究观察到,序列里"关键 token"(比如实体名、数字、代码符号)对量化误差敏感,而大量"过渡 token"(比如虚词、标点、重复模式)对精度不敏感。token 级路由可以让敏感 token 走高精度路径,其余走低精度,整体精度损失可控而速度提升明显。

2.3 架构选型:为什么用"路由器 + 执行器"分离的设计

TokenRouter 的架构我理解下来是两层:一个轻量的Router(路由器)和一个可插拔的Executor(执行器)池。Router 负责对每个 token 做决策,Executor 负责实际计算。这个分离设计有几个好处:

  • Router 本身要足够轻,不能成为瓶颈。所以它通常是一个小 MLP 或者基于规则的打分器,跑在 CPU 或者一个小 CUDA kernel 上,单 token 决策开销控制在微秒级。
  • Executor 可以异构。你可以挂一个 FP16 的大模型 executor,再挂一个 INT8 的小模型 executor,Router 根据 token 特征分发。
  • 决策和执行解耦后,Router 的策略可以热更新,不用重启整个推理服务。

注意:Router 的决策延迟必须远小于它节省下来的计算时间,否则就是负优化。经验值是 Router 单 token 开销要控制在目标 token 计算时间的 5% 以内。

2.4 与已有方案的对比:它站在谁的肩膀上

TokenRouter 不是凭空冒出来的,它和几个已有方向有交集但又有区别。我做个表对比一下,方便你判断它适合什么场景。

方案路由粒度主要优化目标典型局限
Continuous Batching请求级吞吐batch 内长尾拖累
PagedAttention请求级(显存块)显存利用率不解决计算路径差异
Speculative Decodingtoken 级(草稿验证)延迟只优化 decode,不涉及精度/调度
MoE 路由token 级(专家选择)参数量与算力平衡绑定在模型结构内,不涉及系统调度
TokenRoutertoken 级(多维)吞吐+延迟+显存综合实现复杂度高,Router 本身有开销

可以看到,TokenRouter 的野心更大,它想统一处理精度、分支、调度、显存四个维度的路由。代价就是系统复杂度上去了,这也是为什么它更适合有一定推理优化基础的团队。

3. 核心细节解析与实操要点

3.1 Router 的决策特征怎么选

Router 要决策,就得有输入特征。我梳理了几类在实践中比较有效的特征,你可以根据自己场景取舍:

  • 位置特征:token 在序列中的绝对位置和相对位置。开头(系统提示)和结尾(格式约束)通常需要更高精度。
  • 注意力权重统计:这个 token 被后续 token 关注的强度。高关注度的 token 往往是语义关键点,值得走高精度。
  • token 类型:通过一个轻量分类器判断是数字、代码、实体还是普通文本。数字和代码对量化敏感。
  • 历史路由反馈:这个 token 之前走过的路径效果如何,形成一个闭环。

实操中我建议先用规则打底(位置 + token 类型),跑通了再上学习型 Router。一上来就训一个 Router 网络,很容易因为特征工程没做好而效果不如规则。

3.2 决策开销的控制:别让路由器变成新瓶颈

这是最容易翻车的地方。我见过有团队 Router 用了一个 6 层 Transformer,单 token 决策要 200 微秒,结果整体吞吐反而降了 30%。控制开销有几个手段:

  1. 批量决策:不要一个 token 一个 token 地调 Router,攒一批(比如 64 个)一起决策,摊薄 kernel launch 开销。
  2. 查表 + 缓存:对于重复出现的 token 模式(比如固定的系统提示词),决策结果直接缓存,命中率能到 60% 以上。
  3. 决策降频:不是每个 token 都要重新决策,可以每 N 个 token 决策一次,中间沿用上次结果。N 取 4 到 8 比较常见。

提示:Router 的开销要用 profiler 实测,不要拍脑袋估算。我一般会在 Router 前后各打一个 CUDA event,看真实耗时占比。

3.3 执行器池的异构管理

Executor 池里可能同时有不同精度、不同规模的执行器。管理要点:

  • 显存隔离:不同 executor 的 KV cache 要分开管理,避免互相挤占。可以用独立的显存池。
  • 负载均衡:Router 分发时要考虑各 executor 的当前队列深度,别把高精度 executor 压垮而低精度 executor 闲着。
  • 降级策略:高精度 executor 过载时,能把一部分非关键 token 降级到低精度路径,保证整体不雪崩。

3.4 KV cache 的 token 级生命周期管理

这是 TokenRouter 比较有特色的部分。传统 KV cache 是按序列管理的,序列结束才释放。token 级管理意味着:

  • 每个 token 的 KV 有独立的"存活标记"。
  • 当 Router 判断某个 token 后续不会再被关注(比如注意力权重已经衰减到阈值以下),可以提前驱逐它的 KV。
  • 驱逐策略要保守,宁可晚释放也别误删,否则会出正确性问题。

我实测下来,在长对话场景里,token 级 KV 驱逐能省 15% 到 25% 的显存,代价是 Router 要多维护一份存活状态表。

4. 实操过程与核心环节实现

4.1 环境准备与依赖

假设你在一台单卡或者多卡机器上从零搭一个最小可用的 TokenRouter 原型。基础环境:

# 基础依赖(以常见推理框架为例) pip install torch>=2.1 pip install transformers pip install triton # 用于写自定义 kernel

硬件上,建议至少一张显存 24G 以上的卡,方便同时挂两个 executor 做对比实验。如果只有一张小卡,可以先用同一个模型模拟两种精度路径。

4.2 第一步:实现一个规则版 Router

先不上学习型 Router,用规则跑通链路。下面是一个简化示例,展示 Router 的核心决策逻辑:

class RuleBasedRouter: def __init__(self, high_precision_positions=(0, 5), sensitive_types=("number", "code")): self.high_precision_positions = high_precision_positions self.sensitive_types = sensitive_types def route(self, token_id, position, token_type): # 规则1:序列开头几个 token 走高精度 if position in self.high_precision_positions: return "high_precision" # 规则2:敏感类型走高精度 if token_type in self.sensitive_types: return "high_precision" # 默认走低精度快速路径 return "low_precision"

这个 Router 单 token 决策就是几次判断,开销可以忽略。先用它把 prefill、decode、KV 管理都串起来,确认正确性没问题。

4.3 第二步:接入双执行器

准备两个 executor,一个 FP16,一个 INT8。核心是把 Router 的输出映射到对应的计算队列:

class ExecutorPool: def __init__(self, high_exec, low_exec): self.executors = { "high_precision": high_exec, "low_precision": low_exec, } self.queues = {k: [] for k in self.executors} def dispatch(self, token, route_decision): self.queues[route_decision].append(token) def step(self): # 两个队列各自成 batch 推进 for name, queue in self.queues.items(): if queue: batch = queue[:] queue.clear() self.executors[name].forward(batch)

这里的关键是两个队列独立成 batch,各自按自己的节奏推进。高精度队列可能 batch 小但算得慢,低精度队列 batch 大算得快,互不阻塞。

4.4 第三步:token 级 KV 生命周期

给每个 token 的 KV 加一个引用计数或者存活标记:

class TokenKVCache: def __init__(self): self.kv = {} # token_id -> kv tensor self.alive = {} # token_id -> bool def mark_alive(self, token_id): self.alive[token_id] = True def try_evict(self, token_id, attention_score, threshold=0.01): # 注意力权重低于阈值才允许驱逐 if attention_score < threshold: self.alive[token_id] = False del self.kv[token_id]

驱逐阈值要调,我一般从 0.01 开始试,观察精度和显存两条曲线。

4.5 第四步:参数选择与实测记录

我做过一组对比实验,配置和结果大致如下(数据是模拟量级,用于说明趋势):

配置吞吐 (tokens/s)P99 延迟 (ms)显存占用 (GB)
纯 request-level1200150018.5
TokenRouter(规则版)158098015.2
TokenRouter(学习版)172086014.8

可以看到,规则版就能拿到 30% 左右的吞吐提升,学习版再多一些。显存下降主要来自 token 级 KV 驱逐。延迟改善主要来自长短序列不再互相拖累。

注意:这些数字强依赖你的请求长度分布。如果你的请求长度都很均匀,TokenRouter 的收益会小很多,甚至因为 Router 开销变成负优化。上线前一定要用真实流量分布压测。

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

5.1 正确性出问题:token 走错路径导致输出乱码

这是最吓人的问题。表现是输出偶尔出现重复、截断或者语义跳变。排查思路:

  • 先关掉 token 级 KV 驱逐,只保留精度路由,看问题是否消失。如果消失,说明是 KV 驱逐误删。
  • 再关掉精度路由,只保留调度路由,看是否复现。逐步二分定位。
  • 检查 Router 的决策是否在 prefill 和 decode 阶段一致。我踩过一个坑:prefill 时按位置路由,decode 时位置信息没传对,导致同一个 token 两个阶段走了不同精度,KV 对不上。

5.2 吞吐不升反降:Router 开销吃掉收益

前面提过,Router 太重是主因。排查清单:

  • 用 profiler 看 Router 耗时占比,超过 10% 就要警惕。
  • 检查是否每个 token 都调了 Router,改成批量决策。
  • 检查决策缓存是否生效,命中率低于 40% 说明缓存键设计有问题。

5.3 显存没降:KV 驱逐没真正触发

可能原因:

  • 驱逐阈值设得太低,几乎没有 token 满足条件。把阈值调高试试。
  • 存活标记没被正确更新,token 一直被标记为 alive。
  • 驱逐后显存没归还给池子,只是逻辑删除。要确认底层显存分配器真的回收了。

5.4 常见问题速查表

现象可能原因排查动作
输出乱码/重复KV 误驱逐或路径不一致二分关闭各路由维度
吞吐下降Router 开销过大profiler 看 Router 占比
显存不降驱逐未触发或未归还检查阈值和分配器
延迟抖动队列负载不均看各 executor 队列深度
精度下降低精度路径覆盖了敏感 token扩大高精度规则范围

5.5 几条踩坑心得

第一,先保证正确性再谈性能。TokenRouter 引入的路径分支越多,正确性验证越难。我建议每加一个路由维度,都跑一遍完整的回归测试集,别攒着一起测。

第二,Router 策略要可回滚。线上出问题时,能一键切回 request-level 模式。这个开关一定要有。

第三,别迷信学习型 Router。在很多场景下,精心设计的规则能达到学习型 Router 80% 的效果,但复杂度和调试成本低一个数量级。先用规则,等规则到瓶颈了再上学习。

第四,监控要细到 token 级。传统监控看请求级指标,TokenRouter 下你要看每个路由分支的 token 数、耗时、命中率,否则出了问题根本不知道是哪条路径的锅。

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

TokenRouter 这个思路往下走,我觉得有几个方向值得试。一是把路由决策和投机采样结合,让 Router 同时决定"这个 token 要不要走草稿模型验证",把两个 token 级优化合并成一个决策。二是把 Router 做成可学习的在线策略,用强化学习根据实时反馈调整路由,不过这需要一套稳定的奖励信号,工程量大。三是把 token 级路由扩展到多机场景,让不同机器承担不同精度的 executor,Router 变成跨节点的调度器,这时候网络延迟就成了新的约束。

我自己在实际操作中的体会是,token 级路由的价值不在于某一个单点优化,而在于它提供了一个统一的框架,把精度、调度、显存这些原本割裂的优化维度放到同一个决策层里。你不需要一次性把所有维度都做上,挑一个收益最大的先做,跑通了再逐步加,这样风险可控,收益也能持续叠加。最后分享一个小技巧:调试阶段把 Router 的每个决策都打日志,用真实请求回放,你能很直观地看到哪些 token 被分到了哪条路径,很多隐藏问题一眼就能看出来。

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

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

立即咨询