研究先行:路由 trace 与缓存策略

1. 不写引擎,先证假设

“学习式缓存是否更好”这个问题,不需要能跑的引擎——只需要两样东西:

  1. 一条路由轨迹:每 token 每层选了哪些专家;
  2. 一个能重放轨迹、数命中/缺失的缓存模拟器

于是 M2(这一章)在 M3(引擎)之前完成。这是全项目最反直觉、也最有价值的顺序决定:用最小成本先拿研究信号。 一开始的轨迹是合成的,等引擎写好后(第 6 章)再换成真实推理导出的轨迹——结论复现,可信度上一个台阶。

2. 路由 trace 格式

src/trace/routing_trace 定义了一条紧凑轨迹:

struct RoutingRecord {
  uint32_t token;                 // 第几个 token
  uint32_t layer;                 // MoE 层号
  std::vector<uint32_t> experts;  // 选中的专家 id(top-k)
};
struct RoutingTrace { uint32_t n_layers, n_expert; std::vector<RoutingRecord> records; };

它有二进制读写(本机字节序,仅作本地研究工装),后来(第 9 章)又加了文本读——好让别的架构(经 llama.cpp 导出)也能喂进来。

3. 一个接口,三种策略

cache_policy.h 把”某 (layer, expert) 被访问时它在不在缓存”抽象成一个接口:

class CachePolicy {
  virtual bool Access(uint32_t layer, uint32_t expert) = 0;  // true=命中
};
ReplayStats Replay(const RoutingTrace& trace, CachePolicy* policy);

三种实现,同一接口:

  • LruPolicy:每层独立 LRU,每层 slots 个槽;
  • OsPageCachePolicy:一个全局 LRU(容量 = 每层预算 × 层数),模拟操作系统页缓存基线;
  • LearnedPinPolicy:把历史最热的专家钉住常驻(pin,恒命中),其余走 per-layer LRU。

LearnedPinPolicy 需要一个 UsageHistogram(哪个专家最热),由 BuildUsage(trace) 统计。这就是”学习”的部分——从使用历史里学出该 pin 谁。

4. 同一条轨迹,隔离变量

Replay(trace, policy)同一条轨迹重放每种策略,数命中/缺失:

LearnedPinPolicy learned(n_layers, pin, lru, BuildUsage(t));
LruPolicy lru(n_layers, budget);
OsPageCachePolicy os(budget * n_layers);
auto sl = Replay(t, &learned), sr = Replay(t, &lru), so = Replay(t, &os);

同轨迹 + 同总预算 → 只有策略在变。 这是整个研究”隔离变量、只比策略”的核心手法,直接对应科学对照实验。miss 数就是”需要从磁盘读专家”的次数——命中率越高,磁盘 I/O 越少。

5. 第一个结果

在一条偏斜的合成轨迹(少数热专家反复 + 冷专家轮转)上,单测 FirstResultLearnedBeatsBaselines 直接断言:

EXPECT_GT(sl.hits, sr.hits);   // learned > per-layer LRU
EXPECT_GT(sl.hits, so.hits);   // learned > OS 全局 LRU

——learned 赢。这是护城河的第一个信号,在写第一行前向代码之前拿到。它当然是合成的、乐观的;真正的检验是”真实推理轨迹上还成立吗”。要回答这个,得先把引擎写出来——这正是接下来三章的事。

下一章:OLMoE 前向:注意力、MoE、ggml 建图


This site uses Just the Docs, a documentation theme for Jekyll.