MoE 模型的总权重可能远大于单 token 实际激活的专家权重,因此可以尝试按需读取专家,而不是让所有专家权重同时驻留内存。但“只读取被选中的专家”并不自动带来更快推理:缓存命中率、存储读取、权重布局和计算设备都会影响结果。
两组 BigMoeOnEdge 端侧实验呈现了同一个结论:专家流式加载可以降低权重驻留压力,但在 Decode 阶段,频繁读盘可能比计算更慢;HTP 加速 Prefill,也不保证 Decode 或整段请求更快。本文按项目架构图和 Android 实验记录整理,未在本次写作中重新运行实验;各项数字保留原记录的测试条件,不能直接当作跨设备结论。
专家流式推理如何工作
以下机制按本次更新的项目实验记录整理。重点是从路由结果到权重可用、再到算子执行的顺序,以及缓存和缓冲区何时可以回收。
每个 token、每层都要重新路由
常见 MoE Transformer 将普通前馈网络(FFN)替换为多个专家 FFN,Attention 仍负责 token 间的信息交互。每个 token 在每一层 MoE 根据当前特征计算路由分数,选择 Top-K 专家及其系数,再将专家输出加权合并:
Attention → Router 打分 → Top-K 专家 FFN → 加权合并
y = Σ w_j × Expert_j(x),j 属于当前选中的专家集合
记录中的 Qwen3.6-35B-A3B 每层有 256 个路由专家,每个 token 选择 8 个,这个计数不包括共享专家路径。模型文件保存全部专家,但每个 token 在一层中只使用选中的部分;专家随 token 和层变化,不能只加载一次固定的“小模型”。
Prefill 为输入批次中的每个 token 分别路由,所以整个批次涉及的专家集合可能很大。Decode 通常每步处理一个新 token,历史 token 不必重跑完整模型,但新 token 的路由与专家计算仍需执行。这里也要区分两种缓存:KV Cache 保存 Attention 的历史 K/V,专家缓存保存模型参数,二者解决的问题不同。
从实际路由找到 GGUF 权重切片
BigMoeOnEdge 复用 llama.cpp/ggml 的计算图与算子,把扩展点放在权重供给和阶段管理上。初始化时解析 GGUF,并通过捕获运行识别专家张量及其文件偏移。路由节点完成后,RouterHook 获取实际选中的专家编号,通知 ExpertStreamSource 准备权重;后续专家矩阵乘法才消费对应切片。
切片文件位置 = 张量文件起点 + expert_id × nb[2]
nb[2] 是该专家切片布局中的张量步长。一个专家通常包含 gate、up、down 等投影,也可能采用融合布局,因此要按模型的具体布局读取对应切片。
图中的 Session 跟踪模型加载、请求阶段和指标;RouterHook 接入路由与权重读取路径;ExpertStreamSource 管理 CPU 专家缓存和就绪状态;文件读取层负责定位读取和 I/O lane;PrefillPath 与 DeviceArena 管理 HTP 路径及双缓冲。计算图、tokenizer、采样和算子仍由 llama.cpp/ggml 提供。

缓存管理的是专家工作集
LRU(最近最少使用)缓存以“层编号+专家编号”标识条目,相关投影共同管理。每层投影预留虚拟地址范围,实际使用的切片才提交内存页面,使原有张量寻址方式可以继续使用。
| 状态 | 处理方式 |
|---|---|
| 命中 | 复用已有权重,将条目移到 LRU 热端 |
| 未命中 | 准备切片页面并提交读取,读取完成前禁止计算消费 |
| 超过预算 | 淘汰冷端条目,释放对应内存页面 |
| 仍在读取或计算中 | 保护当前工作集,防止权重提前被回收 |
实验里的 2000 MiB 是专家权重缓存预算,非专家权重、KV Cache 和计算缓冲区另计。为保护当前工作集,专家驻留量也可能暂时超过预算;它既不是进程总内存上限,也不是每个时刻都严格满足的驻留上限。缓存过小会导致反复淘汰和重读。
读取完成后,算子才可以使用切片
串行路径先读完本层选中的切片,再进行计算。读算重叠路径则将任务交给多路 I/O 线程,为每个专家投影维护带轮次标识的就绪状态:缓存命中立即就绪,未命中则在读取完成后发布就绪。
flowchart TD
A[路由节点完成] --> B[RouterHook 获取实际专家编号]
B --> C{专家缓存命中?}
C -->|是| D[复用权重并标记本轮就绪]
C -->|否| E[准备页面并异步读取 GGUF 切片]
E --> F[读取完成后发布本轮就绪]
D --> G[expert-ready hook 检查对应切片]
F --> G
G --> H[CPU 算子消费已就绪切片]
E -. 其他切片仍可并行读取 .-> H
图中虚线表示其他切片的读取可以与已就绪切片的计算重叠;每个被消费的切片仍必须先通过就绪检查。定制 llama.cpp 分支中的 expert-ready hook 在 CPU 算子使用切片前检查状态,尚未就绪时先短暂轮询,再等待通知。轮次标识防止误用上一轮的就绪状态,读取结束与层同步则保护缓冲区复用。
因此,“读算重叠”只可能隐藏部分 I/O 等待,不会消除读取,也不保证整个层完全并行。实际路由、算子依赖和存储带宽都构成约束。如果预测预取没有覆盖真实路由,仍必须补读缺失的专家;缓存命中率低时,Decode 仍可能频繁等待。
HTP Prefill 与 CPU Decode 使用不同权重路径
异构执行按批次长度切换:宽批次 Prefill 放到 HTP(Hexagon Tensor Processor)执行,短尾块和逐 token Decode 恢复 CPU 权重绑定。不支持的算子可以回退 CPU。
HTP 权重通过两个可复用的层槽位交替准备,同步屏障确认旧层完成后才允许覆盖槽位。CPU 专家缓存与 HTP 权重槽位分别管理;共享 KV 状态不意味着全部权重共用一份,也不要求常驻两份完整模型。这个安排有利于 Prefill 的权重准备与执行调度,但 Decode 仍受 CPU 计算和专家读取约束。
它和 mmap、repack 有什么不同
三种方式都可能让模型在设备内存紧张时运行,但管理粒度和代价不同:
| 方式 | 权重供给方式 | 主要代价 |
|---|---|---|
系统 mmap | 将文件映射到进程地址空间,由操作系统按需调页 | 缺页、内存回收和 Swap 行为由系统管理 |
| 专家流式加载 | 应用按路由结果读取专家,并用 LRU 管理专家缓存 | 缓存未命中时产生显式读取等待;需维护读取与计算流水线 |
repack | 转换权重布局以适配更快的计算内核 | 可能需要额外转换时间和内存峰值 |
mmap 不等于“完整模型必须先复制进物理内存”。llama.cpp 的加载选项也把 mmap 列为默认自动模式之一,并说明它按内存映射模型文件工作。操作系统仍可能回收页面或把数据换出,因此能启动不代表没有 I/O 等待。专家流式方案则把选择粒度从文件页面推进到路由所需的专家,换来更多应用侧控制,也承担了缓存策略和读算调度的复杂度。
repack 改变的是权重布局,使其更适合 CPU 计算内核;它与按需供给解决的问题不同。本次 BigMoeOnEdge 流式实验关闭了 repack,比较结果需要连同这一配置理解。
两组端侧实验
测试设备为 Android / Snapdragon 8 Gen 2 / Hexagon v73,记录约 15 GiB RAM 和 4 GiB Swap。运行使用 4 个 CPU 线程、ctx 2048、ubatch 512、greedy 解码和默认专家路由,没有使用有损路由策略。CPU 基线使用 llama.cpp commit 9c2e0e4。测试记录没有提供功耗、温度和输出质量对照,因此本文不据此判断能效或质量差异。
GPT-OSS-20B
模型文件约 10.71 GiB,输入 117 tokens、输出 64 tokens;预热 1 轮,正式测试 5 轮,表中为均值 ± 样本标准差。专家流式配置的专家缓存为 2000 MiB。
| 配置 | Prefill(token/s) | Decode(token/s) |
|---|---|---|
| llama.cpp CPU,repack | 14.92 ± 4.65 | 11.18 ± 0.13 |
| llama.cpp CPU,mmap / no-repack | 11.35 ± 0.83 | 7.25 ± 2.46 |
| 专家流式,CPU | 11.87 ± 0.25 | 3.10 ± 0.03 |
| HTP Prefill + CPU Decode | 39.51 ± 0.03 | 2.89 ± 0.03 |
这组数据里,原生 CPU repack 的 Decode 最快;HTP 路径的 Prefill 最快,但 Decode 略低于 CPU 流式配置。CPU 流式配置的缓存命中率为 46.6%,Decode 每 token 的逻辑专家读取量为 587.94 MiB,暴露 I/O 等待约 147.3 ms/token。也就是说,计算路径变快并没有抵消专家读取的等待。
Qwen3.6-35B-A3B-Q4_0
模型文件约 19.41 GiB,输入 97 tokens、目标输出 256 tokens。以下每种配置只测 1 次,适合观察本次运行现象,不适合据此做稳定的性能排名。
| 配置 | 专家缓存 | Prefill(token/s) | Decode(token/s) |
|---|---|---|---|
| 专家流式,CPU | 2000 MiB | 11.66 | 4.66 |
| 专家流式,CPU | 6000 MiB | 9.98 | 5.67 |
| HTP Prefill + CPU Decode | 6000 MiB | 25.81 | 3.78 |
| llama.cpp CPU,mmap / no-repack | OS 管理 | 7.76 | 2.03 |
| llama.cpp CPU,repack | — | 加载时 OOM | 未生成 token |
本次记录中,6000 MiB 缓存的 CPU 流式配置 Decode 最高,缓存命中率为 85.8%,逻辑专家读取量为 48.70 MiB/token。HTP 提高了 Prefill,但 Decode 低于相同缓存容量的 CPU 流式配置。repack 配置原计划输出 32 tokens,加载时就触发 OOM;关闭脚本内存保护后,系统 Swap 曾降到 0,随后进程被内核终止。相较之下,原生 mmap 完成了生成。
两组测试不能混成一个“流式方案快多少”的结论:模型、读取量、重复次数和缓存容量都不同。尤其第二组只有单次记录,缓存增加后 Prefill 反而下降,不能据此推断更大缓存会让 Prefill 变慢;更可靠的做法是固定设备状态,多轮测量并报告波动。
从结果能得到什么
第一,内存可运行性和速度是两个问题。 第二组模型文件大于设备记录的 RAM,但 mmap 仍完成了生成;repack 则在加载阶段 OOM。专家流式加载提供了另一种控制工作集的方式,但其 Decode 速度仍受缓存命中和读盘影响。
第二,缓存大小要和专家访问局部性一起测。 第二组从 2 GiB 增至 6 GiB 后,记录中的 Decode 速度由 4.66 提升到 5.67 token/s,命中率也达到 85.8%;这只能说明该组配置下的观察结果,不能推导出所有模型的缓存收益。
第三,Prefill 和 Decode 要分开看。 HTP 对 Prefill 的收益明显,但当 Decode 留在 CPU、并继续等待专家权重读取时,端到端收益会被另一阶段限制。只报 Prefill 峰值会高估用户实际感受到的速度。
第四,别把缓存容量当作总内存。 非专家权重、KV Cache、计算缓冲区和运行时本身仍占内存。部署前需要同时记录进程峰值、系统可用内存、Swap、缺页或 I/O 等待,以及目标上下文长度。
什么时候值得尝试
专家流式加载更适合作为“模型权重放不进内存时的工程选项”,而不是普遍的加速开关。尝试前建议先用原生 mmap 建立基线,再分别测 CPU 与异构 Prefill 路径;至少固定模型量化、输入长度、输出长度、缓存容量和设备温度,多轮记录 Prefill、Decode、命中率、读取量、总内存与 OOM 情况。
如果 mmap 已能稳定运行且 Decode 更快,应用侧专家缓存未必值得增加复杂度;如果模型无法完整驻留,再评估流式读取是否能在可接受的 I/O 等待下提供可用吞吐。无论选择哪条路径,都要把完整请求延迟和系统稳定性放在单个阶段的峰值速度之前。