在 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-fieldfare、rknn-toolkit2、librga、rkllama、rknn-llm、rknn_model_zoo
目录
- 执行摘要
- [turbo-fieldfare 项目技术剖析](#turbo-fieldfare 项目技术剖析)
- [Rockchip NPU 生态调研](#Rockchip NPU 生态调研)
- 可行性与差异分析
- 整体架构设计
- 关键模块实现方案
- 软件栈分层设计
- 数据流与执行时序
- 性能优化策略
- 部署与运行
- 风险与挑战
- 参考资料
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-llmv1.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 的做法是:
- 常驻部分:共享专家、注意力权重、Router 权重、Embedding、KV Cache------这些每个 token 都会用到,常驻内存约 1.35 GB。
- 流式部分 :256 个路由专家按需从 SSD 读取。每层维护一个 16 槽位的 LFU(Least Frequently Used)缓存,存放最近最常使用的专家权重。
- 流水线 :当 Metal 在计算当前层的注意力时,CPU 并行地用
pread系统调用从 SSD 读取下一层需要的专家权重到 Metal 可见的缓冲区。这种"计算-IO 重叠"是性能关键。
2.4 单层执行流程
turbo-fieldfare 对每一层 Transformer 的处理可以划分为三个阶段(项目文档称为 cb1 / io / cb2):
flowchart LR subgraph CB1"阶段 cb1:计算绑定(Metal)" A1Attention 计算 --> A2Router 计算 A2 --> A3Top-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 端(模型转换)" T2rknn-toolkit2\
通用模型转换\
ONNX/PyTorch → RKNN LLM_Trkllm-toolkit\
LLM 专属转换\
HF → RKLLM end subgraph Device"设备端(推理运行时)" RTrknn-runtime\
C/C++ API\
通用模型 LLM_RTrkllm-runtime\
C/C++ API\
LLM 推理 RGAlibrga\
2D 硬件加速\
图像预处理/内存搬运 DriverRKNPU 内核驱动\
已开源 end subgraph App"上层应用" Zoorknn_model_zoo\
部署示例\
YOLO/ResNet/... RLrkllama\
Ollama 风格 Server\
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/...);
- 提供
im2dAPI(C/C++); - 关键优势:零拷贝内存搬运,可在不同内存区域(CPU/NPU/VPU/GPU)间高效传输数据。
对本方案的用途:
- 若扩展到多模态(图像输入),用 librga 做图像预处理(resize/格式转换),避免 CPU 开销;
- 在专家权重从 SSD 加载到 NPU 可见内存时,可探索用 RGA 做内存搬运(虽然主要场景是 2D 图像,但 bitBlt 能力可用于张量拷贝);
- 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 用于调度和采样 |
关键约束:
- NPU 不支持动态形状(dynamic shape),所有输入维度必须在转换时确定;
- NPU 不支持动态路由(MoE 的 top-k 选择),需在 CPU 上完成路由后再调用 NPU;
- 内存与 CPU 共享,需谨慎控制峰值占用,避免触发 OOM 或 swap。
4. 可行性与差异分析
4.1 Apple Silicon vs Rockchip NPU 平台对比
graph LR subgraph Apple"Apple Silicon(turbo-fieldfare 原生平台)" A_GPUMetal GPU\
统一内存\
带宽 100+ GB/s A_SSDNVMe SSD\
随机读 \~50 μs A_RAM8--128 GB 统一内存\
GPU/CPU 共享 A_GPU --- A_RAM A_SSD --- A_RAM end subgraph RK"Rockchip RK3588(目标平台)" R_NPUNPU\
6 TOPS INT8\
独立地址空间 R_CPUA76/A55 CPU\
调度+采样 R_RAM4--16 GB LPDDR4x\
带宽 \~25 GB/s R_SSDNVMe/eMMC\
随机读 \~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 平台的结合点在于:
-
稠密部分复用 RKNN-LLM :Gemma 4 的 Attention、Router、共享专家、LayerNorm、RoPE、采样等"每个 token 都用"的算子,直接用
rkllm-toolkit转换为.rkllm模型,在 NPU 上运行。这部分无需重写。 -
MoE 路由专家自研流式加载:256 个路由专家无法一次性装入内存,需要:
- 离线把每个专家的 FFN 权重单独打包为
.rknn模型(固定形状,NPU 可执行); - 运行时在 CPU 上做 Router 推理(已包含在稠密部分),得到 top-8 专家 ID;
- 查询 LFU 缓存,缺失的专家从 SSD 流式加载到 NPU 可见内存;
- 调用 RKNN C API 执行专家 FFN 推理。
- 离线把每个专家的 FFN 权重单独打包为
-
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"客户端层" CLICLI 工具 APP桌面/Web App API第三方 OpenAI 客户端 end subgraph Service"HTTP 服务层(参考 rkllama)" ServerOpenAI 兼容 Server\
/v1/chat/completions\
/v1/completions Mgr模型生命周期管理\
加载/卸载/切换 CachePrompt Cache\
KV Cache 持久化 end subgraph Runtime"MoE 流式运行时(自研核心)" Orch执行编排器\
cb1/io/cb2 三阶段 ECache专家 LFU 缓存\
每层 16--32 槽 StreamSSD 流式加载器\
并行 pread Sampler采样器\
Top-K/Top-P/Temperature KVKV Cache 管理器\
滑动窗口 + 全注意力 end subgraph NPU"NPU 加速层(复用官方 SDK)" Denserkllm-runtime\
Attention/Router/共享专家 Expertrknn-runtime\
路由专家 FFN RGAlibrga\
内存搬运/预处理 end subgraph Storage"存储层" GTurbo".rkmojo 模型目录\
稠密部分 + 专家分片" SSDNVMe SSD\
专家权重存储 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 模型目录" Manifestmanifest.json\
元数据/校验 Densedense.rkllm\
Attention+Router+共享专家+Norm Embembedding.bin\
词嵌入 ExpertDirexperts/ KVTemplatekv_template.bin\
KV Cache 初始化模板 end subgraph Experts"experts/ 目录" E0expert_L01_E00.rknn E1expert_L01_E01.rknn ENexpert_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 HFHuggingFace Gemma4-26B-A4B\
原始权重 --> Split权重拆分脚本\
Python Split --> Dense稠密部分\
Attention/Router/共享专家 Split --> Experts路由专家\
逐个导出 Split --> EmbEmbedding Dense --> RKLLM_Trkllm-toolkit\
W8A8 量化 Experts --> RKNN_Trknn-toolkit2\
W8A8 量化 RKLLM_T --> DenseOutdense.rkllm RKNN_T --> ExpertOutexperts/\*.rknn Emb --> EmbOutembedding.bin DenseOut --> Pack打包器 ExpertOut --> Pack EmbOut --> Pack Pack --> RKMojo.rkmojo 目录 RKMojo --> Verify校验器\
manifest + 哈希
6.1.2 关键实现细节
稠密部分转换 (使用 rkllm-toolkit):
python
# 伪代码:转换稠密部分
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):
python
# 伪代码:转换单个路由专家
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 结构:
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推理入口\
generate/prefill OrchLayerOrchestrator\
逐层调度 CB1CB1 阶段\
NPU 稠密计算 IOIO 阶段\
专家流式加载 CB2CB2 阶段\
NPU 专家计算 SamplerSampler\
采样 KVKVCacheManager\
KV 管理 end subgraph NPU"NPU 调用" RKLLM_RTrkllm-runtime\
稠密部分 RKNN_RTrknn-runtime\
专家 FFN end subgraph Cache"专家缓存" LFULFUCache\
每层独立 PoolBufferPool\
NPU 可见内存池 end subgraph IO_Layer"IO 层" ReaderAsyncReader\
并行 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 核心数据结构
cpp
// 专家缓存条目
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)的简化定义,并添加了详细的注释说明内存管理和错误处理的关键点。
cpp
#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;
}
代码说明:
-
内存管理:
Tensor类使用 RAII 管理 NPU/CPU 内存,支持 NPU 零拷贝内存(通过rknn_create_mem分配)。ExpertCache预分配固定大小的 NPU 内存池,避免运行时动态分配导致的内存碎片化。KVCache使用滑动窗口(环形缓冲区)管理 KV Cache,支持 split-K/V 归一化路径。
-
错误处理:
- 使用
std::expected返回错误码,避免异常或裸指针。 - 专家加载失败时提供降级策略(使用零权重或跳过该专家)。
- 所有 API 调用都检查返回值,确保错误被及时捕获。
- 使用
-
异步 IO:
- 使用
std::async实现专家权重的异步 SSD 加载。 - 实际部署时应替换为
io_uring实现更高效的异步 IO。
- 使用
-
关键设计决策:
- 所有专家 FFN 共享同一个
rknn_context,通过rknn_set_io_mem动态替换权重内存,避免为每个专家创建独立 context 的开销。 - 缓存使用 LFU 淘汰策略,适合 MoE 中热门专家被反复使用的场景。
- 使用
std::osyncstream确保多线程日志输出不交错。
- 所有专家 FFN 共享同一个
6.3 KV Cache 管理模块
借鉴 turbo-fieldfare 的"25 层滑动窗口 + 5 层全注意力"设计:
graph LR subgraph KV"KV Cache 布局" subgraph SW"滑动窗口层(25 层)" S1L1: circular 1024 S2L2: circular 1024 SNL25: circular 1024 end subgraph FA"全注意力层(5 层)" F1L26: linear 4096 F2L27: linear 4096 FNL30: linear 4096 end end PrefillPrompt Prefill\
分块 128 token --> KV DecodeToken Decode\
逐 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 上实现:
cpp
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 应用层" CLICLI 工具\
rkmojo-cli ServerHTTP Server\
rkmojo-server end subgraph L3"L3 服务层" APIOpenAI API 兼容 SessionSession 管理 PromptCachePrompt Cache ModelMgrModel 生命周期 end subgraph L2"L2 运行时层(自研)" OrchLayerOrchestrator ECacheExpertCache LFU AsyncIOAsyncIO io_uring SamplerSampler KVMgrKVCacheManager TokenizerTokenizer end subgraph L1"L1 NPU 抽象层" RKLLM_Wrkllm-runtime 封装\
RKLLMWrapper RKNN_Wrknn-runtime 封装\
RKNNWrapper RGA_Wlibrga 封装\
RGAWrapper end subgraph L0"L0 SDK 层(官方)" RKLLM_RTlibrkllmrt.so RKNN_RTlibrknnrt.so RGA_RTlibrga.so Driverrknpu 内核驱动 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++ 接口,并处理错误恢复、资源管理:
cpp
// 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"构建依赖" CMakeCMake 3.16+ GCCarm-none-eabi-gcc 12+\
或 aarch64-linux-gnu-gcc CrossTool交叉编译工具链 end subgraph Libs"运行时库" RKLLM_LIBlibrkllmrt.so\
v1.3.0 RKNN_LIBlibrknnrt.so\
v2.3.2 RGA_LIBlibrga.so\
v1.10.6 Boostboost::asio\
HTTP 服务 Jsonnlohmann/json\
JSON 解析 SPSentencePiece\
分词 end subgraph Kernel"内核依赖" NPU_Driverrknpu 驱动\
已开源 IO_uringio_uring\
Linux 5.1+ DMA_BUFDMA-BUF\
零拷贝 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 StartRouter 输出 top-8 专家 ID --> Loop{遍历 8 个 ID} Loop --> Lookup查 LFU 缓存 Lookup --> Hit{命中?} Hit -- 是 --> Ready加入 ready 列表\
更新 use_count Hit -- 否 --> Miss加入 missing 列表 Miss --> Alloc从 LFU 淘汰一个槽位 Alloc --> Read提交 pread 请求 Read --> Waitwait_all Ready --> Wait Wait --> Done所有专家就位 Done --> CB2进入 cb2 阶段
8.4 Prefill 分块策略
flowchart LR Prompt完整 Prompt\
如 1024 token --> Chunk分块\
每块 128 token Chunk --> C1块 1: token 0-127 Chunk --> C2块 2: token 128-255 Chunk --> CN块 N: token 896-1023 C1 --> P1Prefill 块 1\
拉取专家服务 128 行 P1 --> KV1更新 KV Cache KV1 --> P2Prefill 块 2\
复用已缓存专家 P2 --> KV2更新 KV Cache KV2 --> PNPrefill 块 N PN --> KVN更新 KV Cache KVN --> Decode进入 Decode 阶段
分块的好处:
- 一次拉取的专家能服务 128 个 token 的 FFN 计算,IO 摊销更优;
- 内存峰值可控(每块中间结果独立释放);
- 与 turbo-fieldfare 的 128 token 分块保持一致,便于对照。
9. 性能优化策略
9.1 优化矩阵
graph TB subgraph Opt"性能优化维度" IOIO 优化 NPUNPU 利用率优化 Mem内存占用优化 Cache缓存命中率优化 end subgraph IO_Opt"IO 优化策略" IO1io_uring 异步 IO IO2专家文件预排序\
按层聚簇 IO3readahead 预取 IO4直接 IO 绕过页缓存 end subgraph NPU_Opt"NPU 优化策略" N1多核并行\
3 core 分配专家 N2算子融合\
Attention+Norm N3INT8 量化 N4零拷贝内存 end subgraph Mem_Opt"内存优化策略" M1专家缓存槽位动态调整 M2KV Cache 滑动窗口 M3权重内存池复用 M4分块 Prefill end subgraph Cache_Opt"缓存命中率优化" C1LFU 淘汰策略 C2专家热度预测 C3跨层专家共享 C4Prefill 预热 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 默认内核满足。
cpp
// 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源码仓库\
rkmojo-runtime --> Cross交叉编译\
aarch64-linux-gnu-gcc Cross --> Bin二进制产物 Bin --> Bin1rkmojo-cli Bin --> Bin2rkmojo-server Bin --> Bin3librkmojo.so ModelPC 端模型转换 --> RKMojo.rkmojo 目录 RKMojo --> Deploy部署包 Bin1 --> Deploy Bin2 --> Deploy Bin3 --> Deploy Deploy --> DeviceRK3588 设备 Device --> Run运行
10.2 部署步骤
1. 设备准备:
bash
# RK3588 设备上安装依赖
sudo apt install libio-uring-dev libboost-all-dev
# 拷贝官方运行时库
cp librkllmrt.so librknnrt.so librga.so /usr/lib/
2. 模型部署:
bash
# 拷贝 .rkmojo 目录到设备
scp -r gemma4-26b-a4b.rkmojo rk3588:/opt/models/
3. 启动服务:
bash
# 启动 HTTP Server
./rkmojo-server --model /opt/models/gemma4-26b-a4b.rkmojo \
--port 8080 \
--max-context 4096 \
--expert-cache-slots 16
4. 客户端调用:
bash
# 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 的性能监控脚本:
bash
# 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 化部署(可选)
dockerfile
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 V1v1.0\
Gemma4-26B\
RK3588 --> V2v1.5\
多模型支持\
Qwen3-MoE V2 --> V3v2.0\
多模态\
图像输入 V3 --> V4v3.0\
跨平台\
RK3576/RK3588S V4 --> V5v4.0\
分布式\
多 RK3588 集群
长期方向:
- 多模型支持:扩展到 Qwen3-MoE、DeepSeek-MoE 等其他 MoE 模型;
- 多模态:集成视觉编码器(用 rknn-toolkit2 转换 CLIP/ViT),支持图像输入;
- 跨平台:适配 RK3576(单核 NPU,但内存带宽更高)、RK3588S;
- 分布式:多 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 的许可协议。实际部署前请确认相关许可。