llama.cpp ET 后端深度指南:在开源多核 RISC-V 加速器上构建、运行与调优 LLM 推理
【免费下载链接】llama.cppLLM inference in C/C++项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp
本文基于仓库中的 docs/backend/ET.md 及其引用的 算子支持报告、ggml-et 后端实现 与 内核源码,完整覆盖 ET 后端的背景定位、功能限制与受支持模型清单、从工具链到 CMake 的构建流程、容器化运行方式,并深入到内核开发约束、ET_PERF性能日志格式、运行时 Profiling 与 Uberkernel 内核融合机制,帮助读者在 ET-SOC 平台上真正把 llama.cpp 跑起来并具备二次开发能力。
一、ET 后端的背景与定位
ET是 llama.cpp 针对 ET-SOC 中可以找到对应的两个构建开关:
option(GGML_ET "ggml: use ET backend" OFF) option(GGML_ET_SYSEMU "ggml: use ET backend via sysemu" OFF)GGML_ET默认关闭,需要显式开启;GGML_ET_SYSEMU则用于在没有实体硬件时通过系统级模拟器(sysemu)运行后端,这对 CI 验证和早期开发非常有用。后端的主实现位于 ggml-et.cpp,算子分发在 ggml-et-ops.cpp,设备端裸机内核则位于 et-kernels/src 目录。
二、当前限制与受支持模型
2.1 功能限制
原文档明确列出了 ET 后端的四项限制,使用与选型时必须心中有数:
| 限制项 | 说明 |
|---|---|
| 算子覆盖 | 仅支持有限的算子集合,具体以 docs/ops.md 与 docs/ops/ET.csv 为准 |
| 量化格式 | 仅支持q8_0、q4_0(fp16、q4_K为部分支持) |
| 并发 | 同一时间只允许一个llama.cpp 实例使用该设备(当前固件限制) |
| MoE | MoE 模型支持有限(但可用) |
受上述限制影响,只有部分模型能够完全跑在 ET-SOC 上。需要说明的是:任何 llama.cpp 支持的模型都可以加载运行,只是部分甚至大部分算子会回落到 CPU 后端,性能会显著下降。
2.2 完全受支持的模型清单
原文档列出的可全量跑在 ET-SOC 上的模型包括:
- Qwen3 系列(非 MoE),例如
ggml-org/Qwen3-0.6B-GGUF:q8_0、ggml-org/Qwen3-14B-GGUF:q8_0 - Llama 3.2(1B/3B),例如
lmstudio-community/Llama-3.2-1B-Instruct-GGUF:q8_0 - SmolLM2,例如
unsloth/SmolLM2-135M-Instruct-GGUF:q8_0 - Llama 3.1 系列
- RWKV v7 系列
- TinyLLaMA
从源码结构看,这一清单与设备端内核的覆盖面是吻合的:et-kernels/src 下已有mul_mat_Q4_0.c、mul_mat_Q8_0.c、rope_f32.c、flash_attn_ext_f32.c、rms_norm_f32.c等面向稠密 Transformer 的核心内核,以及rwkv_wkv6_f32.c、rwkv_wkv7_f32.c等专门服务 RWKV v6/v7 的非注意力内核,还有mul_mat_id_Q4_0.c、mul_mat_id_Q8_0.c用于 MoE 的路由矩阵乘。
2.3 算子支持报告
docs/ops/ET.csv 是机器可读的算子支持矩阵,全文超过一万六千行,逐行记录每个算子在不同参数组合下的支持状态,表头为:
"backend_name","op_name","op_params","test_mode","supported","error_message","backend_reg_name"例如其中一条记录为:
"ET","ABS","type=f16,ne_a=[128,2,2,2],v=0","support","0","no","ET"supported字段为0表示该算子在对应参数形态下未在 ET 后端实现(运行时将回落到 CPU 或其他后端)。评估一个新模型是否适合 ET 平台时,对照此文件检查其计算图涉及的算子是最可靠的方法。
三、环境准备与构建
3.1 前置条件
构建 ET 后端需要两套外部组件,两者默认都应安装到/opt/et(也可以分别通过环境变量指定):
- 自定义 RISC-V 工具链:按
aifoundry-org/riscv-gnu-toolchain仓库et/aifoundry分支的说明安装; - ET 平台组件:按
aifoundry-org/et-platform仓库的说明安装。
安装后设置路径:
# 设置工具链与 ET 平台路径(/opt/et 为默认值) export ET_TOOLCHAIN=/opt/et export ET_PLATFORM=/opt/et这个路径解析逻辑在后端源码中同样体现:ggml-et.cpp 中的ggml_et_get_default_et_path()按ET_TOOLCHAIN→TOOLCHAIN_ROOT的顺序查找环境变量,都未设置时回落到默认的/opt/et。启用 sysemu 模式时,后端会基于该路径定位sys_emu可执行文件以及BootromTrampolineToBL2.elf、ServiceProcessorBL2_fast-boot.elf、MachineMinion.elf、MasterMinion.elf、WorkerMinion.elf等固件文件(见 ggml-et.cpp 的ggml_et_get_default_sysemu_options()),并在启动前校验这些文件存在且非空——若文件缺失,模拟器会静默挂起。
3.2 构建 llama.cpp
先获取带 ET 后端的 llama.cpp 源码(按原文档说明应检出et分支):
git clone https://github.com/aifoundry-org/llama.cpp cd llama.cpp然后执行构建:
cmake -B build -DGGML_ET=ON cmake --build build --config Release # 可选: # cmake --install build若希望在系统模拟器(sysemu)上构建而非面向实体硬件:
cmake -B build -DGGML_ET=ON -DGGML_ET_SYSEMU=ON cmake --build build --config Release3.3 运行
运行方式与常规 llama.cpp 二进制完全一致(前提是 ET-SOC 设备已安装、内核驱动已加载):
llama-cli -m mymodel.gguf # 或 llama-server -hf ggml-org/Qwen3-8B-GGUF:q8_0在 Docker 容器内运行时,需要把设备节点透传进去。ET 设备使用两个字符设备:/dev/et0_mgmt(管理通道)与/dev/et0_ops(算子/数据通道):
docker run \ --device=/dev/et0_mgmt:/dev/et0_mgmt \ --device=/dev/et0_ops:/dev/et0_ops \ ...四、内核开发:裸机 RISC-V 内核的编写约束
4.1 开发位置与构建方式
计算内核开发位于ggml/src/ggml-et/et-kernels目录。内核使用自定义 RISC-V GNU 工具链编译、由 CMake 管理,当前以裸机 ELF 文件形式产出,不依赖标准库或任何其他运行时。核心逻辑大量使用内联汇编直接操作 ET 的向量/SIMD 指令。典型的内核文件包括:
- 矩阵乘:mul_mat_Q4_0.c、mul_mat_Q8_0.c、mul_mat_f32.c,以及带 matrix engine 后缀的硬件矩阵引擎变体(如
mul_mat_f16_matrix_engine.c、mul_mat_Q4_0_matrix_engine.c); - 注意力:flash_attn_ext_f32.c、
flash_attn_ext_f16_me.c; - 归一化/逐元素:
rms_norm_f32.c、rms_norm_mul_f32.c(融合 RMS_NORM+MUL)、softmax_f32.c、glu_f32.c、el_map_f32.c等。
原文档特别提示:多数内核目前实现得非常朴素,仍有大量可优化的空间(low hanging fruits),对贡献者是很好的切入点。
4.2 硬件未实现指令的编译期检查
这是 ET 内核开发中最容易踩的坑:编译器可能发出硬件尚未实现、固件软件模拟也尚未就绪的指令。在固件未来能够在异常处理中透明地捕获并模拟这些指令之前,内核构建流程包含一个检查步骤:若编译产物中发现了未实现指令,构建直接失败。问题指令清单如下:
- 浮点除法/取余:
FDIV.PI、FDIVU.PI、FREMU.PI、FREM.PI、FDIV.S、FDIV.PS - 平方根/倒数平方根:
FSQRT.S、FSQRT.PS、FRSQ.PS - 三角函数:
FSIN.PS - 长整型与浮点互转(long cast):
FCVT.S.L、FCVT.S.LU、FCVT.L.S、FCVT.LU.S
由此得出的实践约束是:目前应避免任何涉及浮点数的除法、三角函数,以及 long 与 float 之间的直接转换。
仓库中已提供的官方绕路方案在 math_fp.h:
et_fdiv(a, b):利用 ET 硬件的FRCP.PS(倒数)指令实现a / b = a × (1/b),源码见 math_fp.h;et_powf(base, exp):基于FLOG.PS/FEXP.PS实现pow(base, exp) = exp(exp × ln(base)),并手工处理了 0、负底数(NaN)、exp=0/1等边界情况,见 math_fp.h;- long 转 float:假定 long 值足够小(能塞进 32 位),可经
int中转,即a = (float)(int)(b)。
此外,et_platform 的et-common-libs/include/etsoc/isa/目录中有一批更高层的助手函数(封装了张量扩展指令、同步原语等复杂指令)。它们最初是为固件需求开发的,不参与计算内核的构建流程,但内核开发者完全可以参考或直接尝试链接使用。
4.3 提交前必做事项
修改任何算子和/或内核后,必须按 docs/ops.md 中的说明更新受支持算子报告(即docs/ops/ET.csv一类的文件),否则支持矩阵会与实际能力脱节。
五、性能观测:ET_PERF 日志与运行时 Profiling
5.1 ET_PERF 内核级性能日志
启用日志(例如通过--log-fileCLI 参数)后,每一次计算内核执行都会输出一个以ET_PERF开头、以管道符分隔键值对的性能行。原文档给出的示例:
ET_PERF|op=MUL_MAT|kernel=mul_mat_f32_Q8_0xf32|duration_us=3112|tensor=Qcur-0|shape=[4096,2,1,1]|start_us=48437862009|end_us=48437865121|flops=67100672 ET_PERF|op=ROPE|kernel=rope_f32|duration_us=9266|tensor=Qcur-0|shape=[128,32,2,1]|start_us=48437865128|end_us=48437874394|mode=0x0|n_dims=128|freq_base=500000.00|freq_scale=1.00各字段含义:
| 键 | 含义 |
|---|---|
op | ggml 算子名(如MUL_MAT、ROPE) |
kernel | 实际执行的内核名,含类型变体(如mul_mat_f32_Q8_0xf32表示 Q8_0 权重 × f32 激活) |
duration_us | 该次内核执行耗时(微秒) |
tensor/shape | 目标张量名称与四维形状[ne0,ne1,ne2,ne3] |
start_us/end_us | 起止时间戳(微秒) |
flops | 有效浮点运算数(注意:不是每秒浮点运算数 FLOPS) |
| 其余键 | 随算子而异,如 ROPE 的mode、n_dims、freq_base、freq_scale,MUL_MAT_ID 的n_expert/n_expert_used |
从源码看,这些日志由 ggml-et-ops.h 中的ET_PERF_START()/ET_PERF_END()/ET_PERF_END_EXT()宏生成:宏在ET_PERF_RECORD编译定义下展开为基于ggml_time_us()计时并通过GGML_LOG_DEBUG输出的代码,未定义时退化为空操作。这解释了文档中“启用日志时才输出”的行为——ET_PERF行走的是 DEBUG 日志通道,因此需要--log-file等参数打开 DEBUG 级日志才能看到。每个算子函数(如ggml_et_op_mul_mat、ggml_et_op_rope、ggml_et_op_flash_attn_ext)在 ggml-et-ops.cpp 中都遵循“计时起点 → 启动内核 → 计时终点并附算子专属字段”的同一模式。
5.2 GGML_ET_PROFILE:运行时级 Profiling
设置环境变量GGML_ET_PROFILE为某个路径,即可开启 ET-SOC 运行时级别的 profiling。退出时 profiling/tracing 结果会写入该路径下。文档描述为et_runtime_trace.json与kernel_map;从 ggml-et.cpp 的源码看,实际打开的第二个文件为kernel_id.json(内核名到运行时 KernelId 的映射),配合 trace 文件使用,可用于把 trace 中的内核 ID 还原为可读内核名。
export GGML_ET_PROFILE=/path/to/profile_dir llama-cli -m mymodel.gguf # 退出后在 profile_dir 中查看 trace 结果六、Uberkernel:内核融合与设备侧分发
ET 后端实现了一个名为Uberkernel的机制(名字源自 Esperanto AI 的编译器),用于缓解 ET SDK 中相当可观的算子间间隙(op-to-op gap):它把多个已有的内核实现在设备侧用同步原语串联调度,一次启动、连续执行,减少算子切换开销。
开发 Uberkernel 难度远高于普通内核:由于处理器设计的原因,子内核调用之间不存在天然的内存可见性边界(no natural memory visibility horizon),缓存一致性必须显式处理。uberkernel.c 中的调度器按kernel_id分发到各内核入口,代码中可以看到大量针对激活张量的 L2 缓存逐出(evict_region_past_l2)调用,以及注释掉的“weights 为只读、永不陈旧”说明——这正是文档所称“开发与调试困难”的直接体现。
关键事实(以原文档与源码为准):
- Uberkernel 默认关闭,由环境变量
GGML_ET_UBERKERNEL控制;源码中(ggml-et.cpp)对非空且非0的值判为启用,即export GGML_ET_UBERKERNEL=1开启; - 启用后可带来显著性能提升,但目前仅在 LLaMA 3.2 系列与 Qwen 3.5 上验证过,其他模型不建议开启;
- 从 uberkernel.c 的分发
switch可见其当前覆盖的内核族:逐元素映射(el_map)、GLU、ROPE、RMS_NORM/RMS_NORM_MUL、SOFTMAX、SET_ROWS/GET_ROWS、SUM_ROWS、CUMSUM、NORM/L2_NORM/GROUP_NORM、REPEAT、DIAG、TRI/SOLVE_TRI、PAD、CONT、FILL、SET、CONCAT、IM2COL,三类源的MUL_MAT_ID、FLASH_ATTN_EXT(f32 与 f16 矩阵引擎变体)、GATED_DELTA_NET、SSM_SCAN、RWKV 的WKV6/WKV7,以及多种MUL_MAT(f16/f32/Q8_0/Q4_0,含 matrix engine 变体)。
七、路线图
原文档在 Roadmap 一节给出的后续规划包括:
- 为所有模型启用 Uberkernel(当前仅限 LLaMA 3.2 与 Qwen 3.5 验证);
- 支持更多算子(扩大 docs/ops/ET.csv 中 supported 的覆盖面);
- 改进TTS 模型支持;
- 支持更多量化格式(当前以
q8_0/q4_0为主,fp16、q4_K仅部分支持)。
小结
ET 后端让 llama.cpp 跑上了完全开源的 RISC-V 多核加速器:构建只需两个 CMake 开关(-DGGML_ET=ON,模拟器再加-DGGML_ET_SYSEMU=ON),运行与常规二进制无异(容器场景挂载/dev/et0_mgmt与/dev/et0_ops);选型时以q8_0/q4_0量化与 受支持模型清单 为准;内核开发需牢记裸机 ELF 形态与未实现指令清单,用math_fp.h中的et_fdiv/et_powf替代除法与幂运算;调优则依赖ET_PERF日志、GGML_ET_PROFILE运行时 trace,以及(在验证过的模型上)GGML_ET_UBERKERNEL融合调度。相关入口文件:后端实现 ggml-et.cpp、算子层 ggml-et-ops.cpp、内核目录 et-kernels/src、算子支持矩阵 docs/ops/ET.csv、算子文档说明 docs/ops.md。
【免费下载链接】llama.cppLLM inference in C/C++项目地址: https://gitcode.com/GitHub_Trending/ll/llama.cpp
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考