研究先行:路由 trace 与缓存策略
1. 不写引擎,先证假设
“学习式缓存是否更好”这个问题,不需要能跑的引擎——只需要两样东西:
- 一条路由轨迹:每 token 每层选了哪些专家;
- 一个能重放轨迹、数命中/缺失的缓存模拟器。
于是 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 赢。这是护城河的第一个信号,在写第一行前向代码之前拿到。它当然是合成的、乐观的;真正的检验是”真实推理轨迹上还成立吗”。要回答这个,得先把引擎写出来——这正是接下来三章的事。