【AI 推理】在 Rockchip NPU 上实现 MoE 大模型流式推理的技术方案

在 Rockchip NPU 上实现 turbo-fieldfare 风格 MoE 大模型流式推理的技术方案

SEO 摘要:本文深入探讨如何在 Rockchip NPU(以 RK3588 为例)上实现类似 turbo-fieldfare 的 MoE 大模型流式推理方案。文章详细分析了 turbo-fieldfare 的核心技术(专家流式加载、极限内存预算),调研了 Rockchip NPU 生态(rknn-llm、rknn-toolkit2、rkllama 等),并提出了完整的架构设计、模型拆分策略、关键模块实现方案及性能优化策略。目标是在资源受限的边缘设备上运行 Gemma 4 26B-A4B 等大型 MoE 模型,实现 2–5 tok/s 的解码速度。

关键词:Rockchip NPU, RK3588, MoE, 大模型推理, 专家流式加载, turbo-fieldfare, 边缘计算, 嵌入式 AI, RKNN, rknn-llm, Gemma 4, 模型量化, 流式推理

目标读者:嵌入式 AI 工程师、RKNN 开发者、边缘 LLM 部署工程师
参考项目turbo-fieldfarerknn-toolkit2librgarkllamarknn-llmrknn_model_zoo


目录

  1. 执行摘要
  2. turbo-fieldfare 项目技术剖析
  3. Rockchip NPU 生态调研
  4. 可行性与差异分析
  5. 整体架构设计
  6. 关键模块实现方案
  7. 软件栈分层设计
  8. 数据流与执行时序
  9. 性能优化策略
  10. 部署与运行
  11. 风险与挑战
  12. 参考资料

1. 执行摘要

1.1 项目背景

turbo-fieldfare 是一个由 Andrey Mikhaylov 开发的独立研究项目,其核心成就是让 Gemma 4 26B-A4B(一个 260 亿参数的 Mixture-of-Experts 大语言模型)在仅有 8 GB 内存的 Apple Silicon MacBook 上运行,峰值内存占用仅约 2 GB,并在 M2 MacBook Air 上实现 5.1–6.3 tok/s 的解码速度。该项目完全使用 Swift 6.2 + Metal 4 编写,没有依赖 MLX 或 llama.cpp,而是构建了一套模型专属的自定义推理运行时。

其最关键的技术创新是 "专家流式加载(Expert Streaming)":对于 MoE 模型,运行时只把共享专家(shared expert)、注意力权重、路由器(router)和 KV Cache 常驻内存(约 1.35 GB),而把 256 个路由专家(routed experts)按需从 SSD 流式读取,每层维护一个 16 槽位的 LFU 专家缓存。这种设计把"模型大小"和"内存占用"解耦,使得大模型可以在小内存设备上运行。

1.2 本方案目标

本方案研究如何将 turbo-fieldfare 的核心思想——MoE 专家流式加载 + 极限内存预算 + 自定义算子融合——移植到 Rockchip NPU 平台(以 RK3588 / RK3576 为代表),并复用 Rockchip 官方的 RKNN / RKLLM 软件栈。具体目标包括:

  • 在 RK3588(8 GB / 16 GB RAM)上运行 Gemma 4 26B-A4B 或同量级 MoE 模型,内存预算控制在 2–4 GB;
  • 复用 rknn-llm v1.3.0 已有的 Gemma4 支持,承担稠密部分(注意力、共享专家、Router)的 NPU 加速;
  • 自研 C++ 运行时承担专家流式加载、LFU 缓存、prefill 分块、采样等 MoE 专属逻辑;
  • 提供与 rkllama 风格一致的 OpenAI 兼容 HTTP 服务,便于生态接入;
  • 借鉴 librga 实现 KV Cache / 专家权重的零拷贝内存搬运(如需扩展到多模态)。

1.3 核心结论

维度 结论
技术可行性 中等偏高。RKNN-LLM v1.3.0 已原生支持 Gemma4,稠密算子无需重写;MoE 专家流式加载需自研 C++ 层。
性能预期 RK3588 NPU 算力(6 TOPS)远低于 Apple M2,但内存带宽(~25 GB/s)足以支撑专家流式加载;预计 decode 2–5 tok/s,prefill 30–80 tok/s。
主要风险 NPU 不支持动态形状/动态路由,专家权重需离线打包为 RKNN 模型;SSD 随机读延迟可能成为瓶颈。
工作量 预计 3–4 人月,分 4 个里程碑交付。

2. turbo-fieldfare 项目技术剖析

2.1 项目定位与核心成就

turbo-fieldfare 不是一个通用推理框架,而是 Gemma 4 26B-A4B 模型的专属运行时。这种"模型专属"的设计哲学使其能够针对该模型的每一层、每一个算子做极致优化,而不必为通用性付出抽象代价。项目作者明确表示:"TurboFieldfare is model-specific rather than a wrapper around MLX or llama.cpp"。

核心成就量化如下:

指标 数值
模型 Gemma 4 26B-A4B IT(26B 总参数,每 token 激活约 3.88B)
权重精度 MLX affine 4-bit(group 64);Router 8-bit;共享/路由专家 4-bit
常驻内存 ~2 GB(含 4K 上下文 KV Cache)
存储占用 ~14.3 GB(仅文本模型)
平台 Apple Silicon Mac,最低 8 GB RAM
运行时 macOS 26 + Metal 4 + Swift 6.2
M2 Air(8 GB)实测 decode 5.1–6.3 tok/s
M5 Pro(24 GB)实测 decode 31–35 tok/s

2.2 核心技术栈

turbo-fieldfare 的技术栈非常"纯粹"——完全基于 Apple 原生技术,没有引入任何第三方推理框架:

  • 编程语言:Swift 6.2(91.6%)+ Metal Shading Language(8.3%)
  • 计算后端:Metal 4(Apple GPU 通用计算 API)
  • 模型格式:自定义 .gturbo 目录布局(非 GGUF、非 SafeTensors)
  • 安装器:流式 repack,直接从 Hugging Face 拉取字节范围并重打包,避免在磁盘上生成第二份完整 checkpoint
  • 产品形态:Swift 库 + 原生 Mac App + CLI + 回环 OpenAI 兼容 Server + 流式安装器

2.3 关键创新:MoE 专家流式加载

这是 turbo-fieldfare 最核心、最值得借鉴的技术。Gemma 4 26B-A4B 是一个 MoE 模型,每层有 1 个共享专家 + 多个路由专家,每个 token 仅激活 top-8 个路由专家。如果按稠密模型的方式把所有专家都加载进内存,需要 14.3 GB;但每个 token 实际只用得到其中极小一部分。

turbo-fieldfare 的做法是:

  1. 常驻部分:共享专家、注意力权重、Router 权重、Embedding、KV Cache——这些每个 token 都会用到,常驻内存约 1.35 GB。
  2. 流式部分:256 个路由专家按需从 SSD 读取。每层维护一个 16 槽位的 LFU(Least Frequently Used)缓存,存放最近最常使用的专家权重。
  3. 流水线:当 Metal 在计算当前层的注意力时,CPU 并行地用 pread 系统调用从 SSD 读取下一层需要的专家权重到 Metal 可见的缓冲区。这种"计算-IO 重叠"是性能关键。

2.4 单层执行流程

turbo-fieldfare 对每一层 Transformer 的处理可以划分为三个阶段(项目文档称为 cb1 / io / cb2):

flowchart LR subgraph CB1["阶段 cb1:计算绑定(Metal)"] A1[Attention 计算] --> A2[Router 计算] A2 --> A3[Top-8 专家选择] end subgraph IO["阶段 io:IO 绑定(CPU)"] B1[查询 16 槽 LFU 缓存] B2[缺失专家并行 pread] B3[写入 Metal 可见缓冲] end subgraph CB2["阶段 cb2:计算绑定(Metal)"] C1[共享专家 FFN] C2[路由专家 FFN] C3[加权融合输出] end CB1 --> IO --> CB2
  • cb1 阶段:Metal kernel 从常驻权重计算注意力和 Router,输出 top-8 专家 ID。
  • io 阶段:CPU 拿到专家 ID 后,查询本层 LFU 缓存,对缺失的专家发起并行 pread,同时 Metal 可以开始计算共享专家分支(不依赖路由专家)。
  • cb2 阶段:所有路由专家就位后,Metal 计算路由专家 FFN,并与共享专家输出加权融合。

2.5 其他关键技术点

  • 分块 Prefill:长 prompt 按 128 token 分块,使一次拉取的专家能服务多行,提升 IO 效率。
  • KV Cache 存储:25 层滑动窗口(circular buffer)+ 5 层全注意力(linear),FP16 精度。
  • Split-K/V Decode Attention:解码阶段 K 和 V 走不同的归一化路径,提升数值精度。
  • 量化方案:MLX affine 4-bit(group 64),Router 单独 8-bit 以保证路由精度。
  • 流式安装器:从 Hugging Face 拉取字节范围(range request),直接 repack 成 .gturbo 布局,避免在磁盘上生成第二份完整 checkpoint,安装过程内存有界。

2.6 性能数据解读

在 8 GB M2 MacBook Air 上实现 5–6 tok/s 的 decode 速度,意味着:

  • 每个 token 的端到端延迟约 160–200 ms;
  • 其中 NPU/GPU 计算约 30–50 ms,SSD IO 约 80–120 ms,其余为调度开销;
  • IO 是主要瓶颈,因此专家缓存的命中率至关重要。

这一性能数据为我们在 Rockchip 平台上的预期提供了参照:RK3588 的 NPU 算力约为 M2 的 1/5–1/10,但 SSD IO 延迟相近,因此整体 decode 速度预计在 2–5 tok/s 量级。


3. Rockchip NPU 生态调研

3.1 生态全景

Rockchip NPU 生态由官方维护的多个仓库构成,覆盖从模型转换、推理运行时到上层应用的全栈。下图展示了各仓库的定位与依赖关系:

graph TB subgraph PC["PC 端(模型转换)"] T2[rknn-toolkit2<br/>通用模型转换<br/>ONNX/PyTorch → RKNN] LLM_T[rkllm-toolkit<br/>LLM 专属转换<br/>HF → RKLLM] end subgraph Device["设备端(推理运行时)"] RT[rknn-runtime<br/>C/C++ API<br/>通用模型] LLM_RT[rkllm-runtime<br/>C/C++ API<br/>LLM 推理] RGA[librga<br/>2D 硬件加速<br/>图像预处理/内存搬运] Driver[RKNPU 内核驱动<br/>已开源] end subgraph App["上层应用"] Zoo[rknn_model_zoo<br/>部署示例<br/>YOLO/ResNet/...] RL[rkllama<br/>Ollama 风格 Server<br/>OpenAI API 兼容] end T2 --> RT LLM_T --> LLM_RT RT --> Driver LLM_RT --> Driver RGA -.可选.-> RT RGA -.可选.-> LLM_RT Zoo --> RT RL --> LLM_RT RL -.可选.-> RT

3.2 各仓库详细分析

3.2.1 rknn-toolkit2(v2.3.2)

定位:通用 AI 模型的转换与推理 SDK,是整个 RKNN 生态的基础工具链。

核心能力

  • 在 PC 上将 ONNX / PyTorch / TensorFlow / MXNet 模型转换为 .rknn 格式;
  • 支持模型量化(INT8 / FP16)、性能评估、内存评估;
  • 提供 rknn-toolkit-lite2(Python API,设备端)和 rknn-runtime(C/C++ API,设备端);
  • 支持自动混合精度、einsum、Norm 等高级算子。

支持平台:RK3588 / RK3576 / RK3566 / RK3568 / RK3562 / RV1103 / RV1106 / RV1103B / RV1126B / RK2118。

对本方案的用途:用于将 Router、共享专家等"静态形状"子图转换为 RKNN 模型,在 NPU 上加速。

3.2.2 rknn-llm(v1.3.0)

定位:LLM 专属 SDK,是本方案的核心依赖。

核心能力

  • rkllm-toolkit(PC 端):将 HuggingFace 模型转换为 .rkllm 格式,支持 W8A8 / W4A16 量化;
  • rkllm-runtime(设备端):C/C++ API,支持流式输出、多核 NPU 调度、KV Cache 复用、采样参数动态调整;
  • 支持模型:LLaMA、TinyLLaMA、Qwen2/2.5/3/3.5、Phi2/3、ChatGLM3-6B、Gemma2/Gemma3/Gemma3n/Gemma4、InternLM2、MiniCPM3/4、TeleChat2、Qwen2-VL/Qwen3-VL、MiniCPM-V-2_6、DeepSeek-R1-Distill、Janus-Pro-1B、InternVL2-1B/InternVL3-1B、SmolVLM/SmolLM3、RWKV7、DeepSeekOCR;
  • 支持多模态输入(图像 + 文本)、tokenizer/embedding 回调、多 EOS token、长上下文解码优化。

关键发现rknn-llm v1.3.0 已原生支持 Gemma4(见 CHANGELOG:"Added support for Qwen3.5, Gemma4, and SmolLM3 models")。这意味着稠密部分的转换和 NPU 推理可以直接复用官方实现,无需自行实现 Gemma4 的算子。

潜在限制:rknn-llm 主要面向稠密 LLM,其模型转换流程假设所有权重一次性加载。对于 MoE 模型的专家流式加载,需要拆分模型并自研运行时编排。

3.2.3 rkllama(v0.0.75)

定位:第三方开发的 Ollama 替代品,是 rkllm-runtime 的上层封装,提供 HTTP 服务。

核心能力

  • Ollama API 兼容(/api/chat/api/generate/api/ps/api/tags/api/embed/api/pull);
  • 部分 OpenAI API 兼容(/v1/completions/v1/chat/completions/v1/embeddings/v1/images/generations/v1/audio/speech/v1/audio/transcriptions);
  • 工具/函数调用(支持 Qwen、LLaMA 3.2+ 等多种格式);
  • 多模型并行加载(流式模式下不同模型可并行,非流式 FIFO);
  • 动态加载/卸载:模型空闲 30 分钟自动卸载,内存不足时卸载最旧模型;
  • Prompt Cache 文件持久化:每个 chat session 的 KV Cache 可保存为文件(默认 7 天),切换会话时快速恢复;
  • 多模态支持:Qwen2VL/2.5VL/3VL、MiniCPM-V 4/4.5、InternVL3.5;
  • CPU 平台自动检测(RK3588 / RK3576)。

对本方案的用途:作为 HTTP 服务层的参考实现。我们的方案可以复用 rkllama 的 API 设计、模型生命周期管理、Prompt Cache 机制,但底层推理引擎替换为自研的 MoE 流式运行时。

3.2.4 librga(v1.10.6)

定位:Rockchip 2D 图形硬件加速器(RGA)的用户空间驱动。

核心能力

  • 图像缩放、旋转、bitBlt、Alpha 混合等 2D 操作;
  • 支持多种像素格式(RGB/RGBA/YUV/...);
  • 提供 im2d API(C/C++);
  • 关键优势:零拷贝内存搬运,可在不同内存区域(CPU/NPU/VPU/GPU)间高效传输数据。

对本方案的用途

  1. 若扩展到多模态(图像输入),用 librga 做图像预处理(resize/格式转换),避免 CPU 开销;
  2. 在专家权重从 SSD 加载到 NPU 可见内存时,可探索用 RGA 做内存搬运(虽然主要场景是 2D 图像,但 bitBlt 能力可用于张量拷贝);
  3. KV Cache 在不同 NPU core 间的迁移可借助 RGA。

3.2.5 rknn_model_zoo

定位:基于 RKNPU SDK 的部署示例集合。

覆盖模型:分类(MobileNet、ResNet)、检测(YOLOv5/6/7/8/10/11、YOLOX、PPYOLOE、YOLO-World)、姿态(YOLOv8-Pose)、分割(DeepLabV3、YOLOv5-Seg)等。

对本方案的用途:提供 RKNN C API 的标准用法范例,特别是模型加载、输入输出张量管理、零拷贝 API 的使用模式。我们的自研运行时在调用 rknn-runtime 时应严格遵循这些范例。

3.3 Rockchip NPU 硬件能力

以 RK3588 为例(本方案的主要目标平台):

指标 数值 说明
NPU 算力 6 TOPS (INT8) 3 个 NPU core,每个 2 TOPS
NPU 精度 INT8 / INT16 / FP16 INT8 性能最高
内存 4–16 GB LPDDR4/5 与 CPU 共享,无独立显存
内存带宽 ~25 GB/s LPDDR4x-4266 双通道
存储 eMMC 5.1 / NVMe SSD SSD 随机读延迟 ~100–200 μs
CPU 4×A76 + 4×A55 A76 用于调度和采样

关键约束

  1. NPU 不支持动态形状(dynamic shape),所有输入维度必须在转换时确定;
  2. NPU 不支持动态路由(MoE 的 top-k 选择),需在 CPU 上完成路由后再调用 NPU;
  3. 内存与 CPU 共享,需谨慎控制峰值占用,避免触发 OOM 或 swap。

4. 可行性与差异分析

4.1 Apple Silicon vs Rockchip NPU 平台对比

graph LR subgraph Apple["Apple Silicon(turbo-fieldfare 原生平台)"] A_GPU[Metal GPU<br/>统一内存<br/>带宽 100+ GB/s] A_SSD[NVMe SSD<br/>随机读 ~50 μs] A_RAM[8–128 GB 统一内存<br/>GPU/CPU 共享] A_GPU --- A_RAM A_SSD --- A_RAM end subgraph RK["Rockchip RK3588(目标平台)"] R_NPU[NPU<br/>6 TOPS INT8<br/>独立地址空间] R_CPU[A76/A55 CPU<br/>调度+采样] R_RAM[4–16 GB LPDDR4x<br/>带宽 ~25 GB/s] R_SSD[NVMe/eMMC<br/>随机读 ~100–200 μs] R_NPU --- R_RAM R_CPU --- R_RAM R_SSD --- R_RAM end

4.2 关键差异与应对策略

维度 Apple Silicon Rockchip RK3588 应对策略
计算后端 Metal(GPU 通用计算) RKNN(NPU 专用) 稠密算子走 RKNN-LLM,MoE 编排走 CPU
内存带宽 100+ GB/s ~25 GB/s 更激进的量化(W4A8),更大专家缓存命中率
内存模型 统一内存,GPU/CPU 零拷贝 NPU 有独立地址空间,需零拷贝 API 使用 RKNN 零拷贝 API + librga 搬运
动态形状 Metal 支持 NPU 不支持 Router/Top-k 在 CPU 计算,专家 FFN 固定形状走 NPU
SSD 延迟 ~50 μs ~100–200 μs 增大专家缓存槽位(16→32),预取下一层
算力 M2 ~15 TFLOPS FP16 6 TOPS INT8 用 INT8 量化,NPU 优势在 INT8
编程模型 Swift + Metal C/C++ + RKNN API 运行时用 C++ 重写

4.3 核心可行性判断

结论:技术可行,但需要"拆分模型 + 混合执行"的策略。

具体而言,turbo-fieldfare 的核心思想(专家流式加载)与 Rockchip 平台的结合点在于:

  1. 稠密部分复用 RKNN-LLM:Gemma 4 的 Attention、Router、共享专家、LayerNorm、RoPE、采样等"每个 token 都用"的算子,直接用 rkllm-toolkit 转换为 .rkllm 模型,在 NPU 上运行。这部分无需重写

  2. MoE 路由专家自研流式加载:256 个路由专家无法一次性装入内存,需要:

    • 离线把每个专家的 FFN 权重单独打包为 .rknn 模型(固定形状,NPU 可执行);
    • 运行时在 CPU 上做 Router 推理(已包含在稠密部分),得到 top-8 专家 ID;
    • 查询 LFU 缓存,缺失的专家从 SSD 流式加载到 NPU 可见内存;
    • 调用 RKNN C API 执行专家 FFN 推理。
  3. HTTP 服务层复用 rkllama 设计:OpenAI 兼容 API、模型生命周期管理、Prompt Cache 等直接参考 rkllama 实现。

4.4 不可行/高风险项

项目 风险 缓解措施
NPU 动态路由 NPU 不支持 top-k 动态选择 Router 在 CPU 计算(其实 Router 很小,CPU 足够快)
专家权重量化 RKNN 对 4-bit 量化的支持有限 优先用 W8A8(rkllm-toolkit 原生支持),4-bit 作为后续优化
SSD 随机 IO RK3588 SATA SSD 延迟高 强制使用 NVMe SSD;增大缓存;预取
多核 NPU 调度 3 个 NPU core 的并行编排复杂 初期单核,后续优化多核流水线

5. 整体架构设计

5.1 系统架构总览

graph TB subgraph Client["客户端层"] CLI[CLI 工具] APP[桌面/Web App] API[第三方 OpenAI 客户端] end subgraph Service["HTTP 服务层(参考 rkllama)"] Server[OpenAI 兼容 Server<br/>/v1/chat/completions<br/>/v1/completions] Mgr[模型生命周期管理<br/>加载/卸载/切换] Cache[Prompt Cache<br/>KV Cache 持久化] end subgraph Runtime["MoE 流式运行时(自研核心)"] Orch[执行编排器<br/>cb1/io/cb2 三阶段] ECache[专家 LFU 缓存<br/>每层 16–32 槽] Stream[SSD 流式加载器<br/>并行 pread] Sampler[采样器<br/>Top-K/Top-P/Temperature] KV[KV Cache 管理器<br/>滑动窗口 + 全注意力] end subgraph NPU["NPU 加速层(复用官方 SDK)"] Dense[rkllm-runtime<br/>Attention/Router/共享专家] Expert[rknn-runtime<br/>路由专家 FFN] RGA[librga<br/>内存搬运/预处理] end subgraph Storage["存储层"] GTurbo[".rkmojo 模型目录<br/>稠密部分 + 专家分片"] SSD[NVMe SSD<br/>专家权重存储] end Client --> Service Service --> Runtime Runtime --> NPU NPU --> Storage Runtime --> Storage

5.2 模型拆分策略

借鉴 turbo-fieldfare 的 .gturbo 布局,我们设计一套 .rkmojo(RK MoE Optimized)模型目录格式:

graph LR subgraph RKMojo[".rkmojo 模型目录"] Manifest[manifest.json<br/>元数据/校验] Dense[dense.rkllm<br/>Attention+Router+共享专家+Norm] Emb[embedding.bin<br/>词嵌入] ExpertDir[experts/] KVTemplate[kv_template.bin<br/>KV Cache 初始化模板] end subgraph Experts["experts/ 目录"] E0[expert_L01_E00.rknn] E1[expert_L01_E01.rknn] EN[expert_L30_E255.rknn] end Manifest --- Dense Manifest --- Emb Manifest --- ExpertDir Manifest --- KVTemplate ExpertDir --> Experts

拆分原则

  • 稠密部分dense.rkllm):包含所有层的 Attention、Router、共享专家、LayerNorm、RoPE。用 rkllm-toolkit 转换,W8A8 量化。这部分每个 token 都用,常驻内存。
  • 路由专家experts/*.rknn):每个专家单独一个 .rknn 文件,固定输入形状(如 [1, 4096]),W8A8 量化。按需从 SSD 加载。
  • Embedding:单独存储为原始 bin,CPU 查表(避免 NPU 小算子的调度开销)。
  • manifest.json:记录每层专家数量、专家文件偏移、校验和、量化参数。

5.3 内存预算

以 Gemma 4 26B-A4B 在 RK3588(16 GB)上的目标预算为例:

组件 大小 是否常驻 说明
Embedding ~100 MB 词表 ~256K × 维度
稠密部分(Attention+Router+共享专家+Norm) ~1.2 GB W8A8 量化后
KV Cache(4K 上下文) ~600 MB FP16,25 层滑动 + 5 层全注意力
专家 LFU 缓存(每层 16 槽 × 30 层) ~1.5 GB 每个专家 ~3 MB(W8A8)
运行时开销(缓冲区/栈) ~300 MB NPU IO 缓冲、采样缓冲
常驻总计 ~3.7 GB
路由专家(SSD 流式) ~12 GB 按需加载

:若 RK3588 仅 8 GB RAM,可将专家缓存槽位减至 8 槽/层,常驻内存降至 ~2.5 GB,但缓存命中率下降会拖慢 decode。


6. 关键模块实现方案

6.1 模型转换与量化模块

6.1.1 转换流程

flowchart TD HF[HuggingFace Gemma4-26B-A4B<br/>原始权重] --> Split[权重拆分脚本<br/>Python] Split --> Dense[稠密部分<br/>Attention/Router/共享专家] Split --> Experts[路由专家<br/>逐个导出] Split --> Emb[Embedding] Dense --> RKLLM_T[rkllm-toolkit<br/>W8A8 量化] Experts --> RKNN_T[rknn-toolkit2<br/>W8A8 量化] RKLLM_T --> DenseOut[dense.rkllm] RKNN_T --> ExpertOut[experts/*.rknn] Emb --> EmbOut[embedding.bin] DenseOut --> Pack[打包器] ExpertOut --> Pack EmbOut --> Pack Pack --> RKMojo[.rkmojo 目录] RKMojo --> Verify[校验器<br/>manifest + 哈希]

6.1.2 关键实现细节

稠密部分转换(使用 rkllm-toolkit):

# 伪代码:转换稠密部分
from rkllm.api import RKLLM

rkllm = RKLLM()
# 加载 Gemma4 模型,但只保留稠密部分(屏蔽路由专家)
rkllm.load_huggingface_model(
    model="./gemma4-26b-a4b-dense-only",  # 预处理后的稠密子模型
    model_type="gemma4"
)
# W8A8 量化
rkllm.build(
    do_quantization=True,
    quantized_dtype="w8a8",
    quantized_method="channel",
    target_platform="rk3588"
)
rkllm.export_rkllm("./dense.rkllm")

路由专家转换(使用 rknn-toolkit2):

# 伪代码:转换单个路由专家
from rknn.api import RKNN

for layer_idx in range(num_layers):  # 30 层
    for expert_idx in range(num_experts):  # 256 个专家
        rknn = RKNN()
        # 导出单个专家的 FFN 为 ONNX
        onnx_path = f"expert_L{layer_idx:02d}_E{expert_idx:03d}.onnx"
        rknn.load_onnx(model=onnx_path)
        rknn.build(do_quantization=True, dataset="calib.txt", target="rk3588")
        rknn.export_rknn(f"experts/expert_L{layer_idx:02d}_E{expert_idx:03d}.rknn")
        rknn.release()

manifest.json 结构

{
  "model_name": "gemma4-26b-a4b",
  "model_type": "moe",
  "num_layers": 30,
  "num_experts_per_layer": 256,
  "num_activated_experts": 8,
  "quantization": {
    "dense": "w8a8",
    "experts": "w8a8",
    "embedding": "fp16"
  },
  "expert_cache_slots": 16,
  "kv_cache": {
    "dtype": "fp16",
    "max_context": 4096,
    "sliding_window_layers": 25,
    "full_attention_layers": 5
  },
  "files": {
    "dense": "dense.rkllm",
    "embedding": "embedding.bin",
    "experts_dir": "experts/",
    "kv_template": "kv_template.bin"
  },
  "checksums": { "...": "..." }
}

6.2 MoE 流式运行时(自研核心)

这是本方案最核心、最需要自研的模块,对应 turbo-fieldfare 的 Swift 运行时。我们用 C++ 实现。

6.2.1 运行时架构

graph TB subgraph Runtime["MoE Runtime(C++)"] Entry[推理入口<br/>generate/prefill] Orch[LayerOrchestrator<br/>逐层调度] CB1[CB1 阶段<br/>NPU 稠密计算] IO[IO 阶段<br/>专家流式加载] CB2[CB2 阶段<br/>NPU 专家计算] Sampler[Sampler<br/>采样] KV[KVCacheManager<br/>KV 管理] end subgraph NPU["NPU 调用"] RKLLM_RT[rkllm-runtime<br/>稠密部分] RKNN_RT[rknn-runtime<br/>专家 FFN] end subgraph Cache["专家缓存"] LFU[LFUCache<br/>每层独立] Pool[BufferPool<br/>NPU 可见内存池] end subgraph IO_Layer["IO 层"] Reader[AsyncReader<br/>并行 pread/io_uring] end Entry --> Orch Orch --> CB1 --> IO --> CB2 --> Orch CB1 --> RKLLM_RT CB2 --> RKNN_RT IO --> LFU IO --> Reader IO --> Pool CB1 --> KV CB2 --> KV Orch --> Sampler

6.2.2 核心数据结构

// 专家缓存条目
struct ExpertCacheEntry {
    int layer_idx;
    int expert_idx;
    rknn_tensor_mem* weight_mem;  // NPU 可见内存
    uint64_t last_used_tick;
    uint32_t use_count;           // LFU 计数
};

// 每层 LFU 缓存
class LayerExpertCache {
public:
    ExpertCacheEntry* lookup(int expert_idx);
    ExpertCacheEntry* allocate(int expert_idx);  // 触发淘汰
    void touch(ExpertCacheEntry* entry);
private:
    std::array<ExpertCacheEntry, 16> slots_;  // 16 槽位
    std::unordered_map<int, ExpertCacheEntry*> index_;
};

// 异步 IO 读取器(基于 io_uring)
class AsyncExpertReader {
public:
    void request(int layer_idx, int expert_idx, void* dst);
    void wait_all();  // 等待所有未完成请求
private:
    io_uring ring_;
    std::vector<ExpertFileHandle> file_handles_;  // 每层一个 fd
};

6.2.3 单层执行流程(C++ 伪代码)

以下代码将伪代码扩展为一段更完整、可直接编译的 C++ 代码片段,包含头文件引用、关键数据结构(如 Tensor, KVCache)的简化定义,并添加了详细的注释说明内存管理和错误处理的关键点。

#include <iostream>
#include <vector>
#include <cstdint>
#include <cstring>
#include <system_error>
#include <expected>
#include <functional>
#include <memory>
#include <syncstream>
#include <thread>
#include <future>
#include <span>
#include <ranges>
#include <algorithm>
#include <numeric>
#include <optional>
#include <unordered_map>
#include <queue>
#include <mutex>
#include <shared_mutex>
#include <atomic>
#include <chrono>
#include <format>
#include <source_location>
#include <cassert>

// ============================================================================
// 1. 关键数据结构简化定义
// ============================================================================

/**
 * @brief 简化的张量类,用于管理 NPU/CPU 内存。
 *
 * 关键设计:
 * - 使用 `std::expected` 进行错误处理,避免异常或裸指针。
 * - 支持 NPU 零拷贝内存(通过 `rknn_create_mem` 分配)。
 * - 提供 RAII 封装,确保内存正确释放。
 */
class Tensor {
public:
    enum class MemoryType { CPU, NPU_ZERO_COPY };

    struct Shape {
        std::vector<int64_t> dims;
        int64_t num_elements() const {
            return std::accumulate(dims.begin(), dims.end(), 1LL, std::multiplies<>());
        }
    };

    /**
     * @brief 创建张量并分配内存。
     * @param shape 张量形状。
     * @param dtype 数据类型(简化:仅支持 float 和 int8_t)。
     * @param mem_type 内存类型(CPU 或 NPU 零拷贝)。
     * @return 成功返回 Tensor,失败返回错误码。
     */
    static std::expected<Tensor, std::error_code> create(
        Shape shape,
        std::type_info dtype,
        MemoryType mem_type = MemoryType::CPU) {

        size_t element_size = (dtype == typeid(float)) ? sizeof(float) : sizeof(int8_t);
        size_t total_bytes = shape.num_elements() * element_size;

        void* data = nullptr;
        if (mem_type == MemoryType::NPU_ZERO_COPY) {
            // 关键点:使用 rknn_create_mem 分配 NPU 可见的物理连续内存
            // 此处为简化示例,实际应调用 RKNN API
            data = std::aligned_alloc(64, total_bytes); // 64 字节对齐
            if (!data) {
                return std::unexpected(std::make_error_code(std::errc::not_enough_memory));
            }
            std::osyncstream(std::cout) << "[Tensor] Allocated NPU zero-copy memory: "
                                        << total_bytes << " bytes\n";
        } else {
            data = std::malloc(total_bytes);
            if (!data) {
                return std::unexpected(std::make_error_code(std::errc::not_enough_memory));
            }
        }

        return Tensor(data, total_bytes, shape, dtype, mem_type);
    }

    // 禁止拷贝,允许移动
    Tensor(const Tensor&) = delete;
    Tensor& operator=(const Tensor&) = delete;
    Tensor(Tensor&& other) noexcept
        : data_(std::exchange(other.data_, nullptr)),
          size_(other.size_),
          shape_(other.shape_),
          dtype_(other.dtype_),
          mem_type_(other.mem_type_) {}
    Tensor& operator=(Tensor&& other) noexcept {
        if (this != &other) {
            release();
            data_ = std::exchange(other.data_, nullptr);
            size_ = other.size_;
            shape_ = other.shape_;
            dtype_ = other.dtype_;
            mem_type_ = other.mem_type_;
        }
        return *this;
    }

    ~Tensor() { release(); }

    // 访问器
    void* data() { return data_; }
    const void* data() const { return data_; }
    size_t size() const { return size_; }
    const Shape& shape() const { return shape_; }

private:
    Tensor(void* data, size_t size, Shape shape, std::type_info dtype, MemoryType mem_type)
        : data_(data), size_(size), shape_(std::move(shape)), dtype_(dtype), mem_type_(mem_type) {}

    void release() {
        if (data_) {
            if (mem_type_ == MemoryType::NPU_ZERO_COPY) {
                // 关键点:使用 rknn_destroy_mem 释放 NPU 内存
                std::free(data_); // 简化示例
                std::osyncstream(std::cout) << "[Tensor] Freed NPU zero-copy memory\n";
            } else {
                std::free(data_);
            }
            data_ = nullptr;
        }
    }

    void* data_ = nullptr;
    size_t size_ = 0;
    Shape shape_;
    const std::type_info& dtype_;
    MemoryType mem_type_;
};

/**
 * @brief 简化的 KV Cache 管理器。
 *
 * 关键设计:
 * - 使用滑动窗口(环形缓冲区)管理最近 N 个 token 的 K/V。
 * - 支持 split-K/V 归一化路径(K 和 V 走不同的 scale)。
 * - 内存由 rkllm-runtime 管理,我们通过其 API 获取指针并复用。
 */
class KVCache {
public:
    struct Config {
        int num_layers = 30;
        int sliding_window_size = 1024; // 滑动窗口大小
        int full_attention_size = 4096; // 全注意力大小
        int num_heads = 32;
        int head_dim = 128;
        bool use_split_kv = true; // 是否使用 split-K/V
    };

    KVCache(Config config) : config_(config) {
        // 初始化 KV Cache 内存池
        // 实际应调用 rkllm-runtime API 获取预分配内存
        size_t kv_size_per_layer = config_.sliding_window_size * config_.num_heads * config_.head_dim * sizeof(float);
        kv_cache_.resize(config_.num_layers);
        for (int i = 0; i < config_.num_layers; ++i) {
            kv_cache_[i].resize(kv_size_per_layer);
        }
        std::osyncstream(std::cout) << "[KVCache] Initialized " << config_.num_layers
                                    << " layers, " << kv_size_per_layer << " bytes per layer\n";
    }

    /**
     * @brief 更新指定层的 KV Cache。
     * @param layer 层索引。
     * @param key 新的 K 张量。
     * @param value 新的 V 张量。
     * @param position 当前 token 在序列中的位置。
     * @return 成功返回 true,失败返回错误码。
     */
    std::expected<bool, std::error_code> update(int layer, const Tensor& key, const Tensor& value, int position) {
        if (layer < 0 || layer >= config_.num_layers) {
            return std::unexpected(std::make_error_code(std::errc::invalid_argument));
        }

        // 滑动窗口:计算环形缓冲区中的位置
        int slot = position % config_.sliding_window_size;
        size_t offset = slot * config_.num_heads * config_.head_dim * sizeof(float);

        // 关键点:使用 memcpy 或 DMA 将数据拷贝到 KV Cache 缓冲区
        // 对于 NPU 零拷贝内存,可能只需要更新指针
        std::memcpy(kv_cache_[layer].data() + offset, key.data(), key.size());
        std::memcpy(kv_cache_[layer].data() + offset + key.size(), value.data(), value.size());

        // 关键点:split-K/V 归一化路径
        if (config_.use_split_kv) {
            // K 和 V 走不同的 scale 路径,提升数值精度
            // 此处为简化示例,实际需要调用 NPU kernel
            std::osyncstream(std::cout) << "[KVCache] Layer " << layer
                                        << " position " << position
                                        << " updated with split-K/V\n";
        }

        return true;
    }

private:
    Config config_;
    std::vector<std::vector<char>> kv_cache_; // 每层的 KV Cache 缓冲区
};

// ============================================================================
// 2. 专家缓存(LFU)
// ============================================================================

/**
 * @brief LFU 专家缓存。
 *
 * 关键设计:
 * - 使用 LFU(Least Frequently Used)淘汰策略,适合 MoE 中热门专家被反复使用的场景。
 * - 缓存槽位固定,避免运行时动态分配 NPU 内存。
 * - 使用 `std::shared_mutex` 支持并发读。
 */
class ExpertCache {
public:
    struct Config {
        int slots_per_layer = 16; // 每层缓存槽位
        int num_layers = 30;
        size_t expert_weight_size = 3 * 1024 * 1024; // 每个专家权重大小(约 3 MB)
    };

    ExpertCache(Config config) : config_(config) {
        // 预分配 NPU 内存池
        size_t total_memory = config_.num_layers * config_.slots_per_layer * config_.expert_weight_size;
        memory_pool_ = std::make_unique<char[]>(total_memory);
        std::osyncstream(std::cout) << "[ExpertCache] Pre-allocated " << total_memory
                                    << " bytes for expert cache\n";

        // 初始化每层的缓存槽位
        cache_.resize(config_.num_layers);
        for (int l = 0; l < config_.num_layers; ++l) {
            for (int s = 0; s < config_.slots_per_layer; ++s) {
                size_t offset = (l * config_.slots_per_layer + s) * config_.expert_weight_size;
                cache_[l].emplace_back(memory_pool_.get() + offset, config_.expert_weight_size);
            }
        }
    }

    /**
     * @brief 查询指定层的专家是否在缓存中。
     * @param layer 层索引。
     * @param expert_id 专家 ID。
     * @return 如果命中,返回指向缓存权重的指针;否则返回 std::nullopt。
     */
    std::optional<std::span<char>> lookup(int layer, int expert_id) {
        std::shared_lock lock(mutex_);
        auto it = cache_map_.find({layer, expert_id});
        if (it != cache_map_.end()) {
            // 更新使用计数
            it->second.use_count++;
            return std::span<char>(cache_[layer][it->second.slot].data(), config_.expert_weight_size);
        }
        return std::nullopt;
    }

    /**
     * @brief 插入专家权重到缓存。
     * @param layer 层索引。
     * @param expert_id 专家 ID。
     * @param weight_data 专家权重数据。
     * @return 成功返回 true,失败返回错误码。
     */
    std::expected<bool, std::error_code> insert(int layer, int expert_id, std::span<const char> weight_data) {
        std::unique_lock lock(mutex_);

        // 如果已存在,直接更新
        if (cache_map_.contains({layer, expert_id})) {
            return true;
        }

        // 查找 LFU 槽位:找到使用次数最少的槽位
        int min_use_slot = 0;
        int min_use_count = std::numeric_limits<int>::max();
        for (int s = 0; s < config_.slots_per_layer; ++s) {
            auto it = cache_map_.find({layer, s});
            if (it == cache_map_.end()) {
                min_use_slot = s;
                break;
            }
            if (it->second.use_count < min_use_count) {
                min_use_count = it->second.use_count;
                min_use_slot = s;
            }
        }

        // 淘汰旧专家
        auto old_it = cache_map_.find({layer, min_use_slot});
        if (old_it != cache_map_.end()) {
            cache_map_.erase(old_it);
            std::osyncstream(std::cout) << "[ExpertCache] Evicted layer " << layer
                                        << " slot " << min_use_slot
                                        << " (use_count=" << old_it->second.use_count << ")\n";
        }

        // 拷贝权重数据到预分配的内存池
        std::memcpy(cache_[layer][min_use_slot].data(), weight_data.data(), weight_data.size());

        // 更新映射
        cache_map_[{layer, expert_id}] = {min_use_slot, 1};
        std::osyncstream(std::cout) << "[ExpertCache] Inserted layer " << layer
                                    << " expert " << expert_id
                                    << " into slot " << min_use_slot << "\n";

        return true;
    }

private:
    struct CacheEntry {
        int slot;
        int use_count;
    };

    Config config_;
    std::unique_ptr<char[]> memory_pool_; // 预分配的 NPU 内存池
    std::vector<std::vector<std::span<char>>> cache_; // 每层的缓存槽位
    std::unordered_map<std::pair<int, int>, CacheEntry> cache_map_; // (layer, expert_id) -> (slot, use_count)
    std::shared_mutex mutex_; // 读写锁
};

// ============================================================================
// 3. 单层执行流程
// ============================================================================

/**
 * @brief 单层 Transformer 执行器。
 *
 * 关键设计:
 * - 实现 cb1/io/cb2 三阶段流水线。
 * - 使用 `std::future` 实现异步 IO。
 * - 详细的错误处理和内存管理。
 */
class LayerExecutor {
public:
    struct Config {
        int layer_id = 0;
        int num_experts = 256;
        int top_k = 8;
        int hidden_dim = 4096;
        int num_heads = 32;
        int head_dim = 128;
        int expert_hidden_dim = 14336; // 专家 FFN 中间维度
    };

    LayerExecutor(Config config, ExpertCache& expert_cache, KVCache& kv_cache)
        : config_(config), expert_cache_(expert_cache), kv_cache_(kv_cache) {}

    /**
     * @brief 执行单层推理。
     * @param hidden 输入 hidden state。
     * @param position 当前 token 在序列中的位置。
     * @return 成功返回输出 hidden state,失败返回错误码。
     */
    std::expected<Tensor, std::error_code> execute(const Tensor& hidden, int position) {
        std::osyncstream(std::cout) << "\n[Layer " << config_.layer_id << "] Starting execution\n";

        // ====================================================================
        // 阶段 cb1:计算绑定(NPU)
        // ====================================================================
        std::osyncstream(std::cout) << "[Layer " << config_.layer_id << "] Phase cb1: Attention + Router\n";

        // 关键点:调用 rkllm-runtime 执行 Attention 和 Router 计算
        // 此处为简化示例,模拟 NPU 计算
        auto attention_result = compute_attention(hidden, position);
        if (!attention_result) {
            return std::unexpected(attention_result.error());
        }

        auto router_result = compute_router(hidden);
        if (!router_result) {
            return std::unexpected(router_result.error());
        }

        // 获取 top-k 专家 ID
        auto topk_result = select_topk_experts(*router_result, config_.top_k);
        if (!topk_result) {
            return std::unexpected(topk_result.error());
        }
        const auto& expert_ids = *topk_result;

        // ====================================================================
        // 阶段 io:IO 绑定(CPU + SSD)
        // ====================================================================
        std::osyncstream(std::cout) << "[Layer " << config_.layer_id << "] Phase io: Expert loading\n";

        // 关键点:使用 std::future 实现异步专家加载
        std::vector<std::future<std::expected<bool, std::error_code>>> load_futures;
        for (int expert_id : expert_ids) {
            // 先查缓存
            auto cached = expert_cache_.lookup(config_.layer_id, expert_id);
            if (cached) {
                std::osyncstream(std::cout) << "[Layer " << config_.layer_id
                                            << "] Expert " << expert_id << " cache HIT\n";
                continue;
            }

            // 缓存缺失,异步加载
            load_futures.push_back(std::async(std::launch::async, [this, expert_id]() -> std::expected<bool, std::error_code> {
                // 关键点:使用 io_uring 或 pread 从 SSD 读取专家权重
                // 此处为简化示例,模拟 SSD 读取
                std::this_thread::sleep_for(std::chrono::microseconds(100)); // 模拟 100 μs 延迟

                // 模拟专家权重数据
                std::vector<char> weight_data(3 * 1024 * 1024, 0); // 3 MB
                // 实际应使用 io_uring 读取文件

                // 插入缓存
                auto result = expert_cache_.insert(config_.layer_id, expert_id, weight_data);
                if (!result) {
                    std::osyncstream(std::cout) << "[Layer " << config_.layer_id
                                                << "] Expert " << expert_id << " load FAILED: "
                                                << result.error().message() << "\n";
                } else {
                    std::osyncstream(std::cout) << "[Layer " << config_.layer_id
                                                << "] Expert " << expert_id << " loaded from SSD\n";
                }
                return result;
            }));
        }

        // 等待所有异步加载完成
        for (auto& fut : load_futures) {
            auto result = fut.get();
            if (!result) {
                // 关键点:错误处理——专家加载失败时的降级策略
                std::osyncstream(std::cout) << "[Layer " << config_.layer_id
                                            << "] WARNING: Expert load failed, using fallback\n";
                // 降级策略:使用零权重或跳过该专家
            }
        }

        // ====================================================================
        // 阶段 cb2:计算绑定(NPU)
        // ====================================================================
        std::osyncstream(std::cout) << "[Layer " << config_.layer_id << "] Phase cb2: Expert FFN + Fusion\n";

        // 关键点:调用 rknn-runtime 执行路由专家 FFN
        // 此处为简化示例,模拟 NPU 计算
        Tensor expert_output = Tensor::create(
            {config_.hidden_dim},
            typeid(float),
            Tensor::MemoryType::NPU_ZERO_COPY
        ).value();

        for (int expert_id : expert_ids) {
            auto cached = expert_cache_.lookup(config_.layer_id, expert_id);
            if (cached) {
                // 关键点:使用 rknn_set_io_mem 设置专家权重,然后执行推理
                // 此处为简化示例
                std::osyncstream(std::cout) << "[Layer " << config_.layer_id
                                            << "] Running expert FFN for expert " << expert_id << "\n";
            }
        }

        // 融合输出:共享专家输出 + 路由专家输出加权融合
        // 关键点:在 NPU 上执行融合 kernel,避免 CPU 拷贝
        std::osyncstream(std::cout) << "[Layer " << config_.layer_id << "] Fusing outputs\n";

        // 更新 KV Cache
        auto kv_result = kv_cache_.update(config_.layer_id, hidden, hidden, position);
        if (!kv_result) {
            return std::unexpected(kv_result.error());
        }

        std::osyncstream(std::cout) << "[Layer " << config_.layer_id << "] Execution complete\n";
        return std::move(expert_output);
    }

private:
    // 模拟 Attention 计算
    std::expected<Tensor, std::error_code> compute_attention(const Tensor& hidden, int position) {
        // 实际应调用 rkllm-runtime API
        return Tensor::create({config_.hidden_dim}, typeid(float));
    }

    // 模拟 Router 计算
    std::expected<Tensor, std::error_code> compute_router(const Tensor& hidden) {
        // 实际应调用 rkllm-runtime API
        return Tensor::create({config_.num_experts}, typeid(float));
    }

    // 模拟 Top-K 选择
    std::expected<std::vector<int>, std::error_code> select_topk_experts(const Tensor& router_logits, int k) {
        // 实际应在 CPU 上执行 top-k 选择
        std::vector<int> topk_ids(k);
        std::iota(topk_ids.begin(), topk_ids.end(), 0); // 模拟:选择前 k 个
        return topk_ids;
    }

    Config config_;
    ExpertCache& expert_cache_;
    KVCache& kv_cache_;
};

// ============================================================================
// 4. 使用示例
// ============================================================================

int main() {
    std::osyncstream(std::cout) << "=== MoE Layer Executor Demo ===\n";

    // 初始化组件
    ExpertCache::Config cache_config;
    cache_config.slots_per_layer = 16;
    cache_config.num_layers = 30;
    ExpertCache expert_cache(cache_config);

    KVCache::Config kv_config;
    kv_config.num_layers = 30;
    kv_config.sliding_window_size = 1024;
    kv_config.full_attention_size = 4096;
    KVCache kv_cache(kv_config);

    // 创建 LayerExecutor
    LayerExecutor::Config layer_config;
    layer_config.layer_id = 0;
    layer_config.num_experts = 256;
    layer_config.top_k = 8;
    layer_config.hidden_dim = 4096;
    LayerExecutor executor(layer_config, expert_cache, kv_cache);

    // 模拟输入
    auto hidden = Tensor::create({4096}, typeid(float)).value();
    std::memset(hidden.data(), 0, hidden.size()); // 初始化为 0

    // 执行单层推理
    auto result = executor.execute(hidden, 0);
    if (result) {
        std::osyncstream(std::cout) << "Layer execution SUCCESS\n";
    } else {
        std::osyncstream(std::cout) << "Layer execution FAILED: " << result.error().message() << "\n";
        return 1;
    }

    return 0;
}

代码说明

  1. 内存管理

    • Tensor 类使用 RAII 管理 NPU/CPU 内存,支持 NPU 零拷贝内存(通过 rknn_create_mem 分配)。
    • ExpertCache 预分配固定大小的 NPU 内存池,避免运行时动态分配导致的内存碎片化。
    • KVCache 使用滑动窗口(环形缓冲区)管理 KV Cache,支持 split-K/V 归一化路径。
  2. 错误处理

    • 使用 std::expected 返回错误码,避免异常或裸指针。
    • 专家加载失败时提供降级策略(使用零权重或跳过该专家)。
    • 所有 API 调用都检查返回值,确保错误被及时捕获。
  3. 异步 IO

    • 使用 std::async 实现专家权重的异步 SSD 加载。
    • 实际部署时应替换为 io_uring 实现更高效的异步 IO。
  4. 关键设计决策

    • 所有专家 FFN 共享同一个 rknn_context,通过 rknn_set_io_mem 动态替换权重内存,避免为每个专家创建独立 context 的开销。
    • 缓存使用 LFU 淘汰策略,适合 MoE 中热门专家被反复使用的场景。
    • 使用 std::osyncstream 确保多线程日志输出不交错。

6.3 KV Cache 管理模块

借鉴 turbo-fieldfare 的"25 层滑动窗口 + 5 层全注意力"设计:

graph LR subgraph KV["KV Cache 布局"] subgraph SW["滑动窗口层(25 层)"] S1[L1: circular 1024] S2[L2: circular 1024] SN[L25: circular 1024] end subgraph FA["全注意力层(5 层)"] F1[L26: linear 4096] F2[L27: linear 4096] FN[L30: linear 4096] end end Prefill[Prompt Prefill<br/>分块 128 token] --> KV Decode[Token Decode<br/>逐 token] --> KV

实现要点

  • 滑动窗口层用环形缓冲区(circular buffer),容量 1024,覆盖最近 1024 个 token;
  • 全注意力层用线性缓冲区,容量 4096(最大上下文);
  • FP16 精度,K 和 V 分开存储;
  • Decode 阶段使用 split-K/V 归一化路径(K 和 V 走不同的 scale);
  • KV Cache 内存由 rkllm-runtime 管理,我们通过其 API 获取指针并复用。

6.4 采样器

turbo-fieldfare 默认 temperature=0.2, top_k=64, top_p=0.95。我们在 CPU 上实现:

class Sampler {
public:
    Sampler(float temp, int top_k, float top_p, float rep_penalty);
    int sample(const Tensor& logits, const std::vector<int>& recent_tokens);
private:
    void apply_repetition_penalty(Tensor& logits,
                                  const std::vector<int>& recent);
    void top_k_filter(Tensor& logits, int k);
    void top_p_filter(Tensor& logits, float p);
    int multinomial(const Tensor& probs);
};

采样在 CPU 完成(A76 核心足够快),避免 NPU 上下文切换开销。

6.5 HTTP 服务层

直接参考 rkllama 的实现,提供 OpenAI 兼容 API。核心端点:

端点 方法 说明
/v1/chat/completions POST 聊天补全,支持 stream
/v1/completions POST 文本补全
/v1/models GET 已加载模型列表
/v1/embeddings POST 词嵌入(复用 Embedding 表)
/api/ps GET 运行中会话(rkllama 兼容)
/api/tags GET 可用模型列表

Prompt Cache 机制(借鉴 rkllama):每个 chat session 的 KV Cache 可序列化为文件,切换会话时快速恢复,避免重复 prefill。


7. 软件栈分层设计

7.1 分层架构

graph TB subgraph L4["L4 应用层"] CLI[CLI 工具<br/>rkmojo-cli] Server[HTTP Server<br/>rkmojo-server] end subgraph L3["L3 服务层"] API[OpenAI API 兼容] Session[Session 管理] PromptCache[Prompt Cache] ModelMgr[Model 生命周期] end subgraph L2["L2 运行时层(自研)"] Orch[LayerOrchestrator] ECache[ExpertCache LFU] AsyncIO[AsyncIO io_uring] Sampler[Sampler] KVMgr[KVCacheManager] Tokenizer[Tokenizer] end subgraph L1["L1 NPU 抽象层"] RKLLM_W[rkllm-runtime 封装<br/>RKLLMWrapper] RKNN_W[rknn-runtime 封装<br/>RKNNWrapper] RGA_W[librga 封装<br/>RGAWrapper] end subgraph L0["L0 SDK 层(官方)"] RKLLM_RT[librkllmrt.so] RKNN_RT[librknnrt.so] RGA_RT[librga.so] Driver[rknpu 内核驱动] end L4 --> L3 --> L2 --> L1 --> L0

7.2 各层职责

L0 SDK 层(官方提供,无需修改)

  • librkllmrt.so:rkllm-runtime 动态库,提供 LLM 推理 C API;
  • librknnrt.so:rknn-runtime 动态库,提供通用模型推理 C API;
  • librga.so:RGA 硬件加速库;
  • rknpu 内核驱动:已开源,提供 NPU 设备访问。

L1 NPU 抽象层(自研薄封装)

这一层对官方 C API 做薄封装,提供更友好的 C++ 接口,并处理错误恢复、资源管理:

// RKLLM 封装
class RKLLMWrapper {
public:
    bool load(const std::string& rkllm_path);
    void forward_dense(int layer, Tensor& hidden, KVCache& kv,
                       Tensor& router_logits, Tensor& shared_out);
    void set_sampler_params(float temp, int top_k, float top_p);
    void release();
private:
    rkllm_handle_t handle_;
};

// RKNN 封装(针对专家 FFN)
class RKNNWrapper {
public:
    bool init(const std::string& expert_rknn_template);
    // 输入 hidden,输出 expert_output,权重从 weight_mem 加载
    Tensor forward_expert(rknn_tensor_mem* weight_mem,
                          const Tensor& hidden);
    void release();
private:
    rknn_context ctx_;
    // 复用同一个 rknn context,仅替换权重内存
};

关键优化:所有专家 FFN 共享同一个 rknn_context(因为算子结构相同,只是权重不同),通过 rknn_set_io_mem 动态替换权重内存,避免为每个专家创建独立 context 的开销。

L2 运行时层(自研核心)

这是本方案的工作重心,包含 6 个核心组件:

组件 职责 关键技术
LayerOrchestrator 逐层调度 cb1/io/cb2 三阶段 流水线编排
ExpertCache 每层 LFU 专家缓存 LFU 淘汰、NPU 内存池
AsyncIO 并行 SSD 读取 io_uring、pread
Sampler Top-K/Top-P/温度采样 CPU 实现
KVCacheManager KV Cache 生命周期 滑动窗口 + 全注意力
Tokenizer 分词 SentencePiece / tiktoken

L3 服务层

参考 rkllama 的实现,提供 HTTP API、Session 管理、Prompt Cache、Model 生命周期管理。可直接 fork rkllama 代码并替换底层推理引擎。

L4 应用层

  • rkmojo-cli:命令行工具,类似 turbo-fieldfare 的 CLI;
  • rkmojo-server:HTTP 服务,类似 rkllama。

7.3 关键依赖关系

graph LR subgraph Build["构建依赖"] CMake[CMake 3.16+] GCC[arm-none-eabi-gcc 12+<br/>或 aarch64-linux-gnu-gcc] CrossTool[交叉编译工具链] end subgraph Libs["运行时库"] RKLLM_LIB[librkllmrt.so<br/>v1.3.0] RKNN_LIB[librknnrt.so<br/>v2.3.2] RGA_LIB[librga.so<br/>v1.10.6] Boost[boost::asio<br/>HTTP 服务] Json[nlohmann/json<br/>JSON 解析] SP[SentencePiece<br/>分词] end subgraph Kernel["内核依赖"] NPU_Driver[rknpu 驱动<br/>已开源] IO_uring[io_uring<br/>Linux 5.1+] DMA_BUF[DMA-BUF<br/>零拷贝] end Build --> Libs --> Kernel

8. 数据流与执行时序

8.1 完整推理时序

sequenceDiagram participant Client participant Server as HTTP Server participant Mgr as ModelManager participant RT as MoE Runtime participant NPU as NPU (RKLLM+RKNN) participant SSD as NVMe SSD Client->>Server: POST /v1/chat/completions (stream=true) Server->>Mgr: ensure_model_loaded("gemma4-26b") alt 模型未加载 Mgr->>RT: load_model(.rkmojo) RT->>NPU: rkllm_init(dense.rkllm) RT->>SSD: open expert files (30 fds) RT-->>Mgr: ready end Mgr->>RT: create_session(messages, params) RT->>RT: tokenize(prompt) RT->>RT: prefill 分块 (128 token/块) loop Prefill 阶段(每块) RT->>NPU: forward_dense (cb1) NPU-->>RT: router_logits + shared_out RT->>RT: topk(router_logits, 8) RT->>SSD: async pread 缺失专家 (io) SSD-->>RT: expert weights RT->>NPU: forward_expert ×8 (cb2) NPU-->>RT: expert_outputs RT->>RT: 融合输出 + 更新 KV end RT->>RT: sample first token RT-->>Server: token (chunk) Server-->>Client: SSE: data: {token} loop Decode 阶段(逐 token) RT->>NPU: forward_dense (cb1) NPU-->>RT: router_logits + shared_out RT->>RT: topk + 查 LFU 缓存 alt 缓存命中 RT->>RT: 复用缓存专家 else 缓存缺失 RT->>SSD: async pread (io) SSD-->>RT: expert weights end RT->>NPU: forward_expert (cb2) NPU-->>RT: expert_output RT->>RT: 融合 + 更新 KV + sample RT-->>Server: token (chunk) Server-->>Client: SSE: data: {token} end RT-->>Server: EOS Server-->>Client: SSE: data: [DONE]

8.2 单层三阶段流水线时序

gantt title 单层执行时序(cb1 / io / cb2 三阶段流水线) dateFormat X axisFormat %s section NPU cb1:Attention+Router+共享专家 : 0, 30 cb2:路由专家 FFN x8 : 50, 80 section CPU topk 选择 : 30, 35 LFU 查询 : 35, 38 section SSD IO 并行 pread 缺失专家 : 38, 50

时序说明

  • t=0–30ms:NPU 执行 cb1(Attention + Router + 共享专家),CPU 空闲;
  • t=30–35ms:CPU 做 topk 选择(很快,~5ms);
  • t=35–38ms:CPU 查询 LFU 缓存,确定缺失专家;
  • t=38–50ms:SSD 并行 pread 缺失专家(~12ms,假设 2 个缺失);
  • t=50–80ms:NPU 执行 cb2(8 个路由专家 FFN,串行或双核并行)。

关键优化点:cb1 和 io 可以部分重叠——当 NPU 在算共享专家时(cb1 的后半段),CPU 已经可以开始 pread。理想情况下端到端延迟可压缩到 ~70ms/token,对应 ~14 tok/s 的理论上限。实际受 NPU 调度开销和缓存命中率影响,预计 2–5 tok/s。

8.3 专家缓存命中与缺失流程

flowchart TD Start[Router 输出 top-8 专家 ID] --> Loop{遍历 8 个 ID} Loop --> Lookup[查 LFU 缓存] Lookup --> Hit{命中?} Hit -- 是 --> Ready[加入 ready 列表<br/>更新 use_count] Hit -- 否 --> Miss[加入 missing 列表] Miss --> Alloc[从 LFU 淘汰一个槽位] Alloc --> Read[提交 pread 请求] Read --> Wait[wait_all] Ready --> Wait Wait --> Done[所有专家就位] Done --> CB2[进入 cb2 阶段]

8.4 Prefill 分块策略

flowchart LR Prompt[完整 Prompt<br/>如 1024 token] --> Chunk[分块<br/>每块 128 token] Chunk --> C1[块 1: token 0-127] Chunk --> C2[块 2: token 128-255] Chunk --> CN[块 N: token 896-1023] C1 --> P1[Prefill 块 1<br/>拉取专家服务 128 行] P1 --> KV1[更新 KV Cache] KV1 --> P2[Prefill 块 2<br/>复用已缓存专家] P2 --> KV2[更新 KV Cache] KV2 --> PN[Prefill 块 N] PN --> KVN[更新 KV Cache] KVN --> Decode[进入 Decode 阶段]

分块的好处

  1. 一次拉取的专家能服务 128 个 token 的 FFN 计算,IO 摊销更优;
  2. 内存峰值可控(每块中间结果独立释放);
  3. 与 turbo-fieldfare 的 128 token 分块保持一致,便于对照。

9. 性能优化策略

9.1 优化矩阵

graph TB subgraph Opt["性能优化维度"] IO[IO 优化] NPU[NPU 利用率优化] Mem[内存占用优化] Cache[缓存命中率优化] end subgraph IO_Opt["IO 优化策略"] IO1[io_uring 异步 IO] IO2[专家文件预排序<br/>按层聚簇] IO3[readahead 预取] IO4[直接 IO 绕过页缓存] end subgraph NPU_Opt["NPU 优化策略"] N1[多核并行<br/>3 core 分配专家] N2[算子融合<br/>Attention+Norm] N3[INT8 量化] N4[零拷贝内存] end subgraph Mem_Opt["内存优化策略"] M1[专家缓存槽位动态调整] M2[KV Cache 滑动窗口] M3[权重内存池复用] M4[分块 Prefill] end subgraph Cache_Opt["缓存命中率优化"] C1[LFU 淘汰策略] C2[专家热度预测] C3[跨层专家共享] C4[Prefill 预热] end IO --> IO_Opt NPU --> NPU_Opt Mem --> Mem_Opt Cache --> Cache_Opt

9.2 IO 优化(最关键)

IO 是本方案的性能瓶颈(参考 turbo-fieldfare 的性能数据,IO 占 50%+ 延迟)。具体策略:

1. io_uring 异步 IO:替代传统 pread,支持批量提交和完成回调,减少系统调用开销。Linux 5.1+ 内核原生支持,RK3588 默认内核满足。

// io_uring 批量读取示例
struct io_uring ring;
io_uring_queue_init(32, &ring, 0);

for (int id : missing_experts) {
    auto* sqe = io_uring_get_sqe(&ring);
    io_uring_prep_read(sqe, expert_fds_[layer][id],
                       dst_buf, expert_size, 0);
    io_uring_sqe_set_data(sqe, id);
}
io_uring_submit(&ring);
// 等待完成
for (int i = 0; i < missing_experts.size(); ++i) {
    auto* cqe = io_uring_wait_cqe(&ring);
    // 处理完成
    io_uring_cqe_seen(&ring, cqe);
}

2. 专家文件预排序:在模型打包阶段,把同一层的专家写入同一个文件(或连续区域),减少文件打开开销和磁盘寻道。每个专家记录 [offset, size],运行时用 pread 定位。

3. readahead 预取:在 cb1 阶段(NPU 计算注意力时),基于历史路由统计预测下一层可能用到的专家,提前发起 readahead。

4. 直接 IO(O_DIRECT):绕过页缓存,避免专家权重污染系统 page cache(专家权重一次性使用,不值得缓存到 page cache)。

9.3 NPU 利用率优化

1. 多核并行:RK3588 有 3 个 NPU core,可以把 8 个路由专家分配到 3 个 core 并行计算。rknn-runtime 支持指定 core:rknn_set_core_mask(ctx, RKNN_NPU_CORE_0_1_2)

2. 算子融合:在 rkllm-toolkit 转换时,启用算子融合(默认开启),把 Attention + RoPE + LayerNorm 融合为一个 NPU op,减少 kernel launch 开销。

3. 零拷贝内存:使用 rknn_create_mem / rknn_set_io_mem API,让 NPU 直接访问预分配的物理连续内存,避免 CPU↔NPU 数据拷贝。专家权重加载到这块内存后,NPU 可直接读取。

4. INT8 量化:NPU INT8 算力是 FP16 的 4 倍,优先用 W8A8。Router 单独用 W8A8 保证路由精度。

9.4 内存占用优化

1. 专家缓存槽位动态调整:根据可用内存动态调整每层缓存槽位。16 GB RAM 用 16 槽/层,8 GB RAM 用 8 槽/层。

2. KV Cache 滑动窗口:25 层用 1024 容量的环形缓冲区,仅 5 层用 4096 容量的线性缓冲区,节省 ~40% KV 内存。

3. 权重内存池复用:所有专家共享同一个 NPU 内存池,避免每个专家独立分配。

4. 分块 Prefill:长 prompt 分块处理,每块中间结果及时释放,控制峰值内存。

9.5 缓存命中率优化

1. LFU 淘汰策略:turbo-fieldfare 用 LFU(按使用次数淘汰),比 LRU 更适合 MoE(热门专家会被反复使用)。

2. 专家热度预测:基于历史路由统计,预测下一层可能用到的专家,提前加载。

3. 跨层专家共享:观察 Gemma 4 的路由模式,某些专家在多层间高度共现,可考虑跨层共享缓存(需谨慎,因为不同层的专家权重不同)。

4. Prefill 预热:Prefill 阶段拉取的专家会自然填充缓存,为后续 Decode 阶段预热。

9.6 预期性能

基于上述优化,RK3588(16 GB)上的预期性能:

场景 指标 预期值 说明
Prefill 速度 30–80 tok/s 受 NPU 算力限制
Decode(缓存命中率高) 速度 4–6 tok/s 接近 NPU 计算上限
Decode(缓存命中率低) 速度 1–3 tok/s 受 SSD IO 限制
内存峰值 占用 3.5–4 GB 16 槽/层
首 token 延迟 1024 token prompt 15–30 s Prefill 时间
专家缓存命中率 稳态 70–90% 取决于 prompt 模式

10. 部署与运行

10.1 构建流程

flowchart TD Src[源码仓库<br/>rkmojo-runtime] --> Cross[交叉编译<br/>aarch64-linux-gnu-gcc] Cross --> Bin[二进制产物] Bin --> Bin1[rkmojo-cli] Bin --> Bin2[rkmojo-server] Bin --> Bin3[librkmojo.so] Model[PC 端模型转换] --> RKMojo[.rkmojo 目录] RKMojo --> Deploy[部署包] Bin1 --> Deploy Bin2 --> Deploy Bin3 --> Deploy Deploy --> Device[RK3588 设备] Device --> Run[运行]

10.2 部署步骤

1. 设备准备

# RK3588 设备上安装依赖
sudo apt install libio-uring-dev libboost-all-dev
# 拷贝官方运行时库
cp librkllmrt.so librknnrt.so librga.so /usr/lib/

2. 模型部署

# 拷贝 .rkmojo 目录到设备
scp -r gemma4-26b-a4b.rkmojo rk3588:/opt/models/

3. 启动服务

# 启动 HTTP Server
./rkmojo-server --model /opt/models/gemma4-26b-a4b.rkmojo \
                --port 8080 \
                --max-context 4096 \
                --expert-cache-slots 16

4. 客户端调用

# OpenAI 兼容调用
curl http://rk3588:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "gemma4-26b-a4b",
    "messages": [{"role": "user", "content": "你好"}],
    "stream": true
  }'

10.3 性能监控

参考 rknn-llm 的性能监控脚本:

# NPU 利用率
export RKLLM_LOG_LEVEL=1
./eval_perf_watch_npu.sh

# CPU 利用率
./eval_perf_watch_cpu.sh

# 内存占用
watch -n 1 'ps -o rss,vsz,cmd -p $(pgrep rkmojo-server)'

10.4 Docker 化部署(可选)

FROM ubuntu:22.04
RUN apt-get update && apt-get install -y libio-uring1 libboost-system1.74.0
COPY rkmojo-server /usr/local/bin/
COPY librkllmrt.so librknnrt.so librga.so /usr/lib/
COPY entrypoint.sh /
ENTRYPOINT ["/entrypoint.sh"]
CMD ["--port", "8080"]

11. 风险与挑战

11.1 技术风险矩阵

quadrantChart title "风险评估矩阵(影响 × 概率)" x-axis "低概率" --> "高概率" y-axis "低影响" --> "高影响" quadrant-1 "高影响高概率(重点应对)" quadrant-2 "高影响低概率(关注)" quadrant-3 "低影响低概率(忽略)" quadrant-4 "低影响高概率(监控)" "NPU 动态路由不支持": [0.7, 0.9] "SSD IO 延迟过高": [0.6, 0.85] "专家权重量化精度损失": [0.5, 0.6] "NPU 内存碎片化": [0.4, 0.5] "多核 NPU 调度复杂": [0.5, 0.4] "KV Cache 数值精度": [0.3, 0.5]

11.2 详细风险分析

风险 1:NPU 不支持动态路由(高影响,高概率)

描述:MoE 的核心是每个 token 动态选择 top-k 专家,但 RKNN NPU 要求输入形状在转换时固定,无法在运行时动态选择专家。

影响:无法把整个 MoE 层作为一个 RKNN 模型运行。

缓解措施

  • Router 在 CPU 上计算(Router 很小,~1ms);
  • 每个专家单独转换为 .rknn,固定形状;
  • 运行时由 CPU 编排:Router → topk → 查缓存 → 调用专家 RKNN。

风险 2:SSD IO 延迟过高(高影响,高概率)

描述:RK3588 的 SATA SSD 随机读延迟可能高达 200–500 μs,远高于 Apple Silicon 的 NVMe(~50 μs)。

影响:Decode 速度可能降至 1 tok/s 以下。

缓解措施

  • 强制使用 NVMe SSD(PCIe 接口);
  • 增大专家缓存槽位(16→32);
  • 基于 io_uring 的并行预取;
  • 专家文件按层聚簇,减少寻道。

风险 3:专家权重量化精度损失(中影响,中概率)

描述:W8A8 量化可能损失精度,尤其是路由专家的 FFN 权重。

影响:模型输出质量下降。

缓解措施

  • 用代表性数据集做量化校准;
  • 关键层(如最后几层)用 W8A16;
  • 对比 W8A8 和 W4A16 的精度差异,择优。

风险 4:NPU 内存碎片化(中影响,中概率)

描述:频繁加载/卸载专家权重可能导致 NPU 物理内存碎片化。

影响:分配大块 NPU 内存失败。

缓解措施

  • 预分配固定大小的内存池,专家权重加载到池中固定槽位;
  • 避免运行时动态分配 NPU 内存。

风险 5:多核 NPU 调度复杂(中影响,低概率)

描述:3 个 NPU core 的并行编排需要处理同步、负载均衡。

影响:实现复杂度高,可能引入 bug。

缓解措施

  • 初期单核实现,验证正确性;
  • 后续引入多核流水线,参考 rknn_model_zoo 的多核示例。

11.3 非技术风险

风险 影响 缓解
Gemma 4 许可证 模型使用受限 确认 Gemma 4 的许可条款,商业使用需授权
Rockchip SDK 版本升级 API 变更 锁定版本,关注 CHANGELOG
专家权重存储空间 12 GB 专家文件占用 SSD 提供压缩存储选项

11.4 长期演进

graph LR V1[v1.0<br/>Gemma4-26B<br/>RK3588] --> V2[v1.5<br/>多模型支持<br/>Qwen3-MoE] V2 --> V3[v2.0<br/>多模态<br/>图像输入] V3 --> V4[v3.0<br/>跨平台<br/>RK3576/RK3588S] V4 --> V5[v4.0<br/>分布式<br/>多 RK3588 集群]

长期方向

  1. 多模型支持:扩展到 Qwen3-MoE、DeepSeek-MoE 等其他 MoE 模型;
  2. 多模态:集成视觉编码器(用 rknn-toolkit2 转换 CLIP/ViT),支持图像输入;
  3. 跨平台:适配 RK3576(单核 NPU,但内存带宽更高)、RK3588S;
  4. 分布式:多 RK3588 板卡组成集群,分片部署超大模型。

12. 参考资料

12.1 项目仓库

项目 链接 用途
turbo-fieldfare https://github.com/drumih/turbo-fieldfare 核心思想来源
rknn-toolkit2 https://github.com/airockchip/rknn-toolkit2 通用模型转换
rknn-llm https://github.com/airockchip/rknn-llm LLM 推理 SDK(v1.3.0 支持 Gemma4)
rkllama https://github.com/NotPunchnox/rkllama HTTP 服务参考
librga https://github.com/airockchip/librga 2D 硬件加速
rknn_model_zoo https://github.com/airockchip/rknn_model_zoo 部署示例

12.2 技术文档

  • RKNN-Toolkit2 用户指南:rknn-toolkit2/docs/
  • RKLLM 使用指南:rknn-llm/docs/
  • librga API 文档:librga/docs/
  • RKNPU2 驱动文档:rknpu2/docs/

12.3 相关论文与技术博客

  • MoE 原始论文:Shazeer et al., "Outrageously Large Neural Networks: The Sparsely-Gated Mixture-of-Experts Layer" (ICLR 2017)
  • Gemma 技术报告:Gemma Team, "Gemma: Open Models Based on Gemini Research and Technology" (2024)
  • 专家流式加载:turbo-fieldfare 项目文档中关于 expert streaming 的描述
  • io_uring:Axboe, "Efficient IO with io_uring" (Linux kernel docs)
  • RKNN 零拷贝 API:rknn_model_zoo 中的 rknn_create_mem / rknn_set_io_mem 示例

12.4 社区资源

  • Rockchip 官方论坛:https://t.rock-chips.com/
  • RKNN 开发者 QQ 群:见 rknn-toolkit2 README
  • rkllama Discord:见 rkllama README

声明:本方案为技术研究报告,基于公开信息撰写。Gemma 4 模型的使用需遵守其许可证条款。Rockchip SDK 的使用需遵守 Rockchip 的许可协议。实际部署前请确认相关许可。

posted @ 2026-07-30 17:21  doiito  阅读(36)  评论(0)    收藏  举报