ik_llama.cpp 修复 Termux/Android 构建:IQK_API 符号可见性、ARM DOTPROD 特性与移动端 CPU 推理实践
2026/9/19 21:18:42 网站建设 项目流程

ik_llama.cpp 修复 Termux/Android 构建:IQK_API 符号可见性、ARM DOTPROD 特性与移动端 CPU 推理实践

【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp

本指南基于 ik_llama.cpp 仓库中 Pull Request #336《Fix termux/android build》的完整修复记录,深入讲解该 PR 修复的 Android/Termux 构建失败根因(Android 默认隐藏符号可见性导致链接器找不到 iqk 函数)、IQK_API导出宏的设计、iqk_flash_attn_noalibi声明修复,以及围绕__ARM_FEATURE_DOTPROD展开的移动端 CPU 推理可行性讨论。读完本文,你将理解 iqk 内核在非 x86 平台上的编译开关机制,掌握在 Android 设备上正确编译、运行 ik_llama.cpp 并开展基准测试的方法,以及移动端量化模型推理的性能特点。

一、PR #336 背景:Termux/Android 构建失败的根源

ik_llama.cpp 的核心竞争力之一是其高度优化的 iqk 矩阵乘法与量化内核。这些代码集中位于 ggml/src/iqk 目录。然而,当贡献者saood06尝试在 Android 设备的 Termux 环境中构建该仓库时,遇到了链接失败——在设备上能够复现编译错误。

ikawrakow(项目作者)在 PR 讨论中直接点出了根因:

问题出在 Android 上,iqk 函数没有指定可见性(visibility),Android 默认使用隐藏可见性(hidden visibility),所以链接器找不到 iqk 函数。

这是典型的 Android 平台符号导出问题:Android NDK 编译共享库时默认采用-fvisibility=hidden,所有未显式标注导出属性的函数符号都会被隐藏,导致链接阶段无法解析对 iqk 内核函数的引用。该 PR 同时修复了 #159(Termux 编译问题),并在 github-data/issues/387(bitnet 1.58 在 Termux 上段错误)等后续 issue 中继续演进。

二、核心修复一:IQK_API符号可见性宏

2.1 修复方案讨论

针对隐藏可见性导致的链接失败,项目作者给出了两个候选方案:

  1. 新增一个类似GGML_APIIQK_API宏;
  2. 直接复用GGML_API——因为 iqk 代码是作为ggml库的一部分编译的。

贡献者saood06尝试了方案 2(即 PR 中的 "Attempt fix 3"),但未能成功,最终选择了方案 1,用独立的IQK_API宏进行了清理。

2.2IQK_API宏的最终实现

在代码评审阶段,ikawrakow 给出了兼顾静态库与共享库场景的完整实现建议,该建议已成为仓库中 ggml/src/iqk/iqk_config.h 的实际代码:

#ifdef GGML_SHARED # if defined(_WIN32) && !defined(__MINGW32__) # ifdef GGML_BUILD # define IQK_API __declspec(dllexport) # else # define IQK_API __declspec(dllimport) # endif # else # define IQK_API __attribute__ ((visibility ("default"))) # endif #else # define IQK_API #endif

该宏的语义分解如下:

编译场景IQK_API展开作用
共享库 + Windows(非 MinGW)+ 正在构建 ggml__declspec(dllexport)导出符号
共享库 + Windows(非 MinGW)+ 外部使用__declspec(dllimport)导入符号
共享库 + 其他平台(含 Android/Linux/macOS)__attribute__((visibility("default")))强制符号对外可见,覆盖 Android 的 hidden 默认值
静态库(未定义GGML_SHARED无需可见性标注

评审时作者特别强调"要让它也能用于静态构建"(To have this also work for a static built),因为静态库场景下不需要也不应引入 dll 导入导出的语义,这正是#else分支置空的原因。

2.3IQK_API在源码中的实际应用

该宏被广泛标注在 iqk 内核的对外接口上,既包括声明(头文件)也包括定义(实现文件),例如:

  • ggml/src/iqk/iqk_mul_mat.h 中声明了iqk_mul_matiqk_mul_mat_4diqk_mul_mat_moeiqk_moe_fused_up_gateiqk_dequant_typeiqk_fa_work_buffer_sizeiqk_flash_attn_noalibiiqk_topk_moeiqk_fused_delta_netiqk_indexer_topk等函数;
  • ggml/src/iqk/iqk_mul_mat.cpp 中extern "C" IQK_API int iqk_dequant_type(int type, int Ny)以及iqk_mul_mat系列的实现均带extern "C" IQK_API前缀;
  • ggml/src/iqk/iqk_kda.cpp 与 ggml/src/iqk/iqk_cpu_ops.h 中iqk_kda的声明与定义同样使用IQK_API

extern "C"保证符号按 C 链接约定导出(避免 C++ 名字修饰),IQK_API则负责可见性/导入导出。两者组合使用,确保 iqk 内核在 Android 等隐藏可见性平台上也能被链接器正确解析。

三、核心修复二:iqk_flash_attn_noalibi声明修复

PR 还顺带修复了第二个编译问题:iqk_flash_attn_noalibi定义(definition)在IQK_IMPLEMENT未定义的情况下发生了变化。

在 iqk 的架构中,ggml/src/iqk/iqk_config.h 通过以下逻辑决定是否启用 iqk 实现:

#if defined IQK_IMPLEMENT #undef IQK_IMPLEMENT #endif #if defined __AVX2__ || defined __ARM_FEATURE_DOTPROD #define IQK_IMPLEMENT #endif

也就是说,只有 x86 的 AVX2 或 ARM 的 DOTPROD(向量点积)扩展可用时,IQK_IMPLEMENT才会被定义。贡献者saood06的测试设备不支持__ARM_FEATURE_DOTPROD,因此IQK_IMPLEMENT未被定义,暴露了iqk_flash_attn_noalibi定义与声明不一致的问题。

修复后的定义位于 ggml/src/iqk/iqk_flash_attn.cpp:

extern "C" IQK_API bool iqk_flash_attn_noalibi(int type_q, int type_mask, float max_bias, int neq3, int neq2, long nbq3, long nbq2, int nek3, int nek2, long nbk3, long nbk2, int nev3, int nev2, long nbv3, long nbv2, int ne2, int ne1, long nb1, int int_type_k_in, // type of k int int_type_v, // type of v int Dk, // K head size int Dv, // V head size int neq1, // number of columns in q int nek1, // number of rows in k int stride_q, // distance between q columns in bytes int stride_k, // distance between k rows in bytes int stride_v, // distance between v rows in bytes int stride_m, // distance between mask rows (in bytes const void * q, // q matrix. const void * k, // k matrix. Assumed to be fp16, nq x nk elements const void * v, // v matrix. Assumed to be fp16, nq x nk elements const void * mask, // mask. If not null, assumed to be fp16. nq x nk elements const void * sinks, // mask. If not null, assumed to be fp16. nq x nk elements float scale, // scale applied before softmax float softcap, // if > 0, a "soft-cap" operation is applied before softmax float * qkv, // v*softmax(scale*(k*q)) [[maybe_unused]] void * work_buffer_in, [[maybe_unused]] barrier_t barrier, [[maybe_unused]] void * barrier_data, ...);

该函数的入口实现与文件整体一样,被#if defined IQK_IMPLEMENT && defined GGML_IQK_FLASH_ATTENTION守卫(iqk_flash_attn.cpp),只有同时满足"CPU 支持 iqk 所需特性"且"编译时启用了 flash attention"才会参与编译。PR 修复的正是它在IQK_IMPLEMENT未定义时的签名一致性问题,并移除了重复的extern "C" IQK_API前缀(评审中作者提出"这里真的需要重复写extern "C" IQK_API吗?",贡献者随即修改)。

四、ARM 特性依赖:__ARM_FEATURE_DOTPRODggml_vdotq_s32回退

4.1 DOTPROD 是 iqk 在 ARM 上的硬门槛

从 iqk_config.h 可见,iqk 在 ARM 上的启用完全依赖__ARM_FEATURE_DOTPROD(ARMv8.2-A 起引入的点积指令扩展,常见于支持dotprod的 armv8.2-a+ 或 armv9 内核)。这是编译期宏,由编译器根据-march/-mcpu参数自动定义,因此在 Android 上能否启用 iqk 优化,取决于你为构建指定的架构标志

4.2ggml_vdotq_s32:无 DOTPROD 时的软件回退

ikawrakow 在讨论中指出,iqk 代码中一致地使用了ggml_vdotq_s32,而 ggml 在__ARM_FEATURE_DOTPROD不可用时提供了实现。这在 ggml/src/ggml-impl.h 中得到验证:

#if !defined(__ARM_FEATURE_DOTPROD) inline static int32x4_t ggml_vdotq_s32(int32x4_t acc, int8x16_t a, int8x16_t b) { const int16x8_t p0 = vmull_s8(vget_low_s8 (a), vget_low_s8 (b)); const int16x8_t p1 = vmull_s8(vget_high_s8(a), vget_high_s8(b)); return vaddq_s32(acc, vaddq_s32(vpaddlq_s16(p0), vpaddlq_s16(p1))); } #else #define ggml_vdotq_s32(a, b, c) vdotq_s32(a, b, c) #endif // !defined(__ARM_FEATURE_DOTPROD)

有 DOTPROD 时直接映射到硬件指令vdotq_s32;没有时用vmull_s8+vpaddlq_s16模拟点积累加。在 ggml/src/ggml-quants.c 等多处量化内核中,ggml_vdotq_s32被大量用于 Q2/Q3/Q4/Q5/Q6 各类量化格式的矩阵乘累加。

作者指出唯一缺少的 DOTPROD 替代实现是vdotq_laneq_s32(按 lane 加载的点积变体)。如果补齐它,理论上就能在任意通用__aarch64__上启用 iqk,而不再要求__ARM_FEATURE_DOTPROD。这一点在 PR 中并未落地,但对理解 iqk 的 ARM 支持边界很有价值。

4.3 IQK 的构建开关:GGML_IQK_MUL_MAT

除了架构特性宏,iqk 代码是否编入构建还由 CMake 选项GGML_IQK_MUL_MAT控制。在 ggml/src/CMakeLists.txt 中:

  • iqk/iqk_quantize.cppiqk/iqk_cpu_ops.cpp始终在源文件列表中;
  • 开启GGML_IQK_MUL_MAT后,追加iqk_mul_mat.cppiqk_kda.cppiqk_flash_attn.cppfa/iqk_fa_*.cpp及全部iqk_gemm_*.cpp,并定义GGML_USE_IQK_MULMAT(即-DGGML_USE_IQK_MULMAT);
  • flash attention 部分额外受GGML_IQK_FLASH_ATTENTIONGGML_IQK_FA_ALL_QUANTS控制(后者决定 KV cache 是否支持 q4_0/q4_1/iq4_nl 等更多量化类型,见 iqk_flash_attn.cpp)。

五、Android/Termux 实战:构建、运行与基准测试

5.1 两种 Android 构建路径

仓库 docs/android.md 提供了两条 Android 构建路径,均与 PR #336 的修复直接相关:

路径一:Termux 内构建(无需 root)

apt update && apt upgrade -y apt install git make cmake

建议将模型文件放在~/目录以获得最佳性能(Termux 的存储重定向会带来额外 IO 开销):

cd storage/downloads mv model.gguf ~/

路径二:Android NDK 交叉编译(在 PC 上执行)

$ mkdir build-android $ cd build-android $ export NDK=<your_ndk_directory> $ cmake -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-23 -DCMAKE_C_FLAGS=-march=armv8.4a+dotprod .. $ make

注意这里显式传入-march=armv8.4a+dotprod——正如 PR 所揭示的,dotprod是激活 iqk 内核的关键架构标志。arm64-v8a是所有现代 64 位 Android 设备通用的 ABI,而android-23对应 Android 6.0 的最低 API 级别。

编译产物需要处理 Android 的权限模型:sdcard 上无法修改文件权限,因此要将可执行文件拷贝到 Termux 的 home 目录并添加执行权限:

$ cp -r /sdcard/llama.cpp/bin /data/data/com.termux/files/home/ $ cd /data/data/com.termux/files/home/bin $ chmod +x ./*

随后即可运行:

$ cd /data/data/com.termux/files/home/bin $ ./llama-cli -m ../model/model.gguf -n 128 -cml

5.2 架构标志选择的实战经验(来自 PR 实测)

PR 作者在真实设备上积累了两个关键经验:

  1. armv9-a编译出的二进制可能触发非法指令:即使设备 CPU 声称支持 armv9 指令集,armv9-a构建仍可能出现 illegal instruction。改为armv8.2-a+dotprod+fp16后可以正常运行;
  2. 必须手动指定架构标志:不指定时构建出的二进制输出为乱码(gibberish),指定正确标志后,编译时间显著变长——这恰恰说明iqk_mul_mat.cpp等 iqk 源文件真正进入了编译流程。

5.3 移动端基准测试:llama-sweep-bench 实测数据

PR 作者在手机上(1×3.00 GHz Cortex-X2 + 3×2.40 GHz Cortex-A710 的 SoC)使用 4 线程、关闭 flash attention 跑出了当时的最佳结果,命令为:

bin/llama-sweep-bench -m ~/ggml-model-iq2_bn_r4.gguf -t 4

模型为IQ2_B量化(1-bit 级)的 BitNet 模型,测试结果如下:

PPTGN_KVT_PP sS_PP t/sT_TG sS_TG t/s
512128010.26149.905.13024.95
51212851211.84043.246.44519.86
512128102416.33631.346.92518.48
512128153613.91436.807.68516.66
512128204814.82534.548.16815.67
512128256017.94028.548.69414.72
512128307219.04026.898.91114.36
512128358420.54924.929.31913.74

数据解读要点:

  • prompt 处理(PP):短上下文约 50 t/s,随 KV cache 增长(N_KV 增加)下降明显,说明大上下文下的访存压力对移动端 CPU 影响显著;
  • token 生成(TG):从约 25 t/s 降至约 14 t/s,同样呈现随上下文增长而衰减的规律;
  • 作者强调这些数字在多次运行间波动极大,即使使用taskset将进程固定在性能核上也无法稳定复现,原因是系统调度器、热降频(thermal throttling)与核心分配等多种因素叠加。

5.4 性能不稳定的根因讨论与 ARM 优化方向的启示

PR 讨论后半段深入剖析了移动端性能波动与 iqk ARM 优化的适配问题,值得 iqk 使用者与移植者关注:

  • 热降频与调度:作者回顾了自己约 10 年前的移动端数值计算实验——手机满载一两分钟后性能就会彻底崩溃。现代 SoC(尤其是该测试设备的 SoC)以热降频著称,即使设备已经发热,性能仍可能大幅波动,这让跨后端对比(如 CPU vs Vulkan)难以得出可靠结论;
  • 寄存器溢出(register spillage):ikawrakow 指出其 ARM 优化完全基于 Apple M2 芯片调优,常常使用超过可用数量的向量寄存器。在 M2 Max 上,寄存器溢出(spill)带来的代价反而低于少用向量寄存器;但低端 ARM 处理器可能无法很好地处理这种取舍。这正是 iqk 内核在旗舰手机以外的 ARM 设备上可能不是最优的原因之一;
  • 编译器差异:作者使用的是 clang(M2 开发环境),并推测编译器未必能为低端芯片生成最优代码——PR 作者在设备上也只尝试过 clang;
  • 结论:iqk 在移动端(尤其非 Apple 芯片)仍属于"能跑但未充分调优"的状态,作者因此考虑购入树莓派或 Rock 5B 开发板来系统性地支持移动/低端 ARM 设备。可以推断,后续的 ARM 优化工作将围绕寄存器分配策略、编译器选择以及 DOTPROD 依赖的进一步放宽展开。

5.5 移动端推理的价值判断与生态现状

即便存在上述限制,PR 讨论仍指出了 iqk 上 Android 的价值点:

  • CPU 推理可作为 Vulkan 的替代:许多手机 GPU 算力有限,而 Vulkan 后端的性能一般(虽在持续改进)。iqk 修复构建后,在 CPU 上运行 ik_llama.cpp 成为移动端可行的备选方案
  • BitNet 模型是移动端的理想展示场景:1-bit 级量化模型对内存带宽和算力要求极低,PR 作者明确表示 BitNet(需移植)是移动端的最佳契合点。相关生态问题可参考 github-data/discussions/401(Termux 上安装 bitnet 等 CPU 模型的讨论)与 github-data/issues/387(bitnet 1.58 在 Termux 上段错误的排查);
  • 跨后端对比的候选组合:作者建议用 LLaMA-3B 的IQ4_XS/IQ4_KS或 BitNet 模型,在 CPU、Vulkan 与 OpenCL 三个后端上对比。

六、总结与可验证结论

PR #336 是 ik_llama.cpp 移动端支持的关键一步,其核心结论可归纳为:

  1. Android 构建失败的根因是共享库默认隐藏符号可见性,而非计算代码本身的问题;
  2. IQK_API(ggml/src/iqk/iqk_config.h)通过__attribute__((visibility("default")))与 Windows 的dllexport/dllimport分支,同时覆盖共享库与静态库场景,与extern "C"组合标注在 iqk_mul_mat.h、iqk_mul_mat.cpp、iqk_kda.cpp 等所有对外接口上;
  3. iqk_flash_attn_noalibi的声明修复保证了IQK_IMPLEMENT未定义时也能编译通过;
  4. ARM 上启用 iqk 的硬性前提是编译时具备__ARM_FEATURE_DOTPROD,无该特性时ggml_vdotq_s32提供软件回退(ggml/src/ggml-impl.h),vdotq_laneq_s32是尚未补齐的最后一块拼图;
  5. 移动端实测表明 ik_llama.cpp 已能在 Android 上运行并产出合理结果(IQ2_B 模型约 25 t/s 生成速度),但性能受热降频与调度影响波动较大,且 iqk 的 ARM 优化目前以 Apple M2 为基准,对低端 ARM 芯片的适配仍在演进中。

对于希望在移动设备上使用 ik_llama.cpp 的开发者,正确的构建姿势是:使用 NDK 或 Termux 工具链,为arm64-v8a目标显式指定-march=armv8.2-a+dotprod+fp16(而非激进的 armv9-a),开启GGML_IQK_MUL_MAT让 iqk 内核进入编译,再用llama-sweep-bench结合taskset固定核心进行基准测试,并对多次运行间的性能波动保持预期。

【免费下载链接】ik_llama.cppllama.cpp fork with additional SOTA quants and improved performance项目地址: https://gitcode.com/GitHub_Trending/ik/ik_llama.cpp

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询