1.编译llama.cpp命令行(电脑版本);
2.交叉编译安卓命令行版本。
一、Llama.cpp是什么?
llama.cpp是一个开源项目,专门为在本地CPU上部署量化模型而设计。它提供了一种简单而高效的方法,将训练好的量化模型转换为可在CPU上运行的低配推理版本。
1.1 KV Cache
KV Cache是大模型推理中常用到的一个技巧,可以减少重复计算,加快推理速度。
KV Cache之所以可行,就是因为在LLM decoding阶段的Attention计算需要causal mask,这就会导致前面已经生成的Token不需要与后面的Token重新计算Attention,从而每个token的生成只和KV相关,与之前的Q无关,进而可以让我们通过缓存KV的方式避免重复计算。
1.2 LLM推理流程
大模型推理流程:
用户提问(prompt)-》1.输入Tokenize化--〉2.获取token的embedding+位置编码--》3.大模型推理一般包括两个阶段prefilling和decoding。
1.2.1 输入Tokenize化
原始的输入都是文本,也有可能是多模态类型的音频、图像,我们需要将其转化成大模型能识别的token。Token可以翻译为词元,在自然语言处理中,它通常是指一个词、一个字符或者一个子词等语言单位。tokenize就相当于传统NLP任务中的分词。用户输入的提问,通过切分,形成一个token列表。
大模型有一个词汇表,其中包含了模型能够识别的所有token。在将文本转化为token序列时,会根据词汇表中的token进行匹配和划分。
这一步的主要目的是将原始的prompt转化为token的id序列。
1.2.2 获取token的embedding+位置编码
这一步的操作是将token的id序列转化成embedding序列,embedding就是每个token的向量化表示。此外,在真正开始推理之前embedding中还需要加入位置编码(positional encoding)信息,这是由于transformer结构不再使用基于循环的方式建模文本输入,序列中不再有任何信息能够提示token之间的相对位置关系。具体来说,每个token在整个序列中所处的位置都会有一个位置向量,这一向量会与token的embedding向量相加。步骤1和2相当于大模型推理的前处理,将文本或者多模态输入变成一个N x d的矩阵正式进入后续大模型的推理阶段。
1.2.3 大模型推理
大模型推理一般包括两个阶段prefilling和decoding,如下图所示。

1.2.3.1 Prefilling阶段
Prefilling阶段就是将步骤2中生成的N x d矩阵送到大模型中做一遍推理。N就是prompt分词之后的长度,也即上下文的长度。N通常情况下会比较大,当前的大模型普遍可以支持128K的上下文。因为要一次性完成整个prompt对应token的计算,所以prefilling阶段的计算量很大,但它是一次性的过程,所以在整个推理中只占不到 10% 的时间。
更核心的是要解决Attention计算过程中显存占用过大的问题,业界的主流的方案就是FlashAttention。
大模型的最后一层往往是softmax,用来输出每个token的概率,从中选择概率最大的作为大模型的第一个输出token。对应的是模型的首响输出。
事实上,如果不考虑速度因素,单纯利用prefilling也可以完成整个大模型的推理过程。每完成一次大模型推理我们就可以获得一个token,将其与prompt和之前输出的所有token组合重新开始新一轮的大模型推理,直到输出停止符为止。
不过我们很容易看出这种推理模式包含非常多的重复计算,每增加一个token,prompt部分的推理就会重复一次。但是这些重复计算是没有意义的,所以才会将大模型推理拆分成了prefilling+decoding两阶段,目的就是要在prefilling阶段计算KV Cache供decoding阶段使用,当然prefilling也会生成第一个输出token。
1.2.3.2 decoding阶段
decoding阶段就是自回归输出每个token的过程。这里首先要强调一点,prefilling和decoding用的都是相同的大模型,只是推理方式不同:prefilling阶段一次性灌入整个prompt,而decoding阶段则是一次灌入一个token。所以prefilling阶段主要的操作是gemm(矩阵乘),到了decoding阶段就变成了gemv(矩阵向量乘)。正是由于 decoding阶段是逐个token生成的,每一次回答都会生成很多token,所以decoding阶段的数量非常多,占到整个推理过程的90%以上。decoding阶段就是要利用prefilling阶段生成的KV Cache来避免重复计算,这里的核心点在于causal mask机制保证了transformer每一个block中的Attention计算可以实现逐行运算。
将prefilling阶段输出的第一个token加decoding阶段所有的输出token组合起来即可以转换为最终的输出。
1.2.3 采样参数
为什么我们实际在使用大模型的时候同样的输入每次输出也不同呢?
这里面涉及到了大模型采样参数的问题。在prefilling阶段我们提到,可以选择softmax层中概率最大的token作为输出,这可以看做是一种贪婪解码,我们可以完全不选择top1作为输出,而是引入更多采样策略让生成的结果更加多样化,比如从topk中随机选择一个,又或者引入beam-search之类的解码策略。
二、Llama.cpp编译
首先我们尝试编译llama.cpp.
2.1 下载llama.cpp
项目的github地址:
https://github.com/ggerganov/llama.cpp
2.1.1 采用git克隆项目
可以采用git下载:
$ git clone https://github.com/ggerganov/llama.cpp
然后同步submodules
$ cd llama.cpp$ git submodule update kompute
因为科学上网的问题,如果一直同步失败。这种情况下,可以考虑下载项目的方式。
2.1.2 手动下载项目
1)下载llama.cpp
llama.cpp项目页,code-->DownloadZip,然后下载。下载得到压缩包llama.cpp-master.zip,然后解压缩。
2)下载submodule。
[submodule "kompute"] path = ggml/src/kompute url = https://github.com/nomic-ai/kompute.git
项目--》ggml-->src-->kompute @ 4565194 点击进入,同样(code-->DownloadZip),下载完成后,解压缩,然后拷贝到目标目录。
ggml/src/kompute
这样,项目就下载成功了。
2.2 编译项目
llama.cpp提供了本地API调用版本(直接调用本地模型进行推理),以及服务端版本(C/S架构)。
我们采用本地API版本。
首先看项目下的README.md
$ make -j && ./llama-cli -m models/llama-13b-v2/ggml-model-q4_0.gguf -p "Building a website can be done in 10 simple steps:\nStep 1:" -n 400 -e
可以看到,直接采用make编译。项目已经配置了cmake.
2.2.1 如何编译
在项目的docs/build.md, 有编译说明文档。
Linux or MacOS:采用make编译:
$ make
采用cmake编译:
$ cmake -B build $ cmake --build build --config Release
我们直接使用 make编译。
编译完成以后,会在项目下生成一个build目录,生成物在此目录下。

项目根目录或者bin目录下是生成的可执行文件。此目录下的 llama-cli 和main 就是llama.cpp的命令行程序。
2.2.2 测试大模型推理
$ chmod +x llama-cli$ ./llama-cli -m [模型名] --prompt [提问]$ ./llama-cli -m [模型名] -p [提问]
提问可以使用 -p 或者 -- prompt
例如:可以选择一个模型。模型未下载的话需要先进行下载。
$ ./llama-cli -m ./models/MiniCPM-0-2-Q4_K.gguf --prompt "北京有什么好玩的地方"
得到推理结果:
build: 0 (unknown) with Android (11349228, +pgo, +bolt, +lto, -mlgo, based on r487747e) clang version 17.0.2 (https://android.googlesource.com/toolchain/llvm-project d9f89f4d16663d5012e5c09495f3b30ece3d2362) for x86_64-apple-darwin23.2.0main: llama backend initmain: load the model and apply lora adapter, if anyllama_model_loader: loaded meta data with 24 key-value pairs and 219 tensors from ./models/MiniCPM-0-2-Q4_K.gguf (version GGUF V3 (latest))llama_model_loader: Dumping metadata keys/values. Note: KV overrides do not apply in this output.llama_model_loader: - kv 0: general.architecture str = minicpmllama_model_loader: - kv 1: general.name str = MiniCPMllama_model_loader: - kv 2: minicpm.context_length u32 = 8192llama_model_loader: - kv 3: minicpm.embedding_length u32 = 1024llama_model_loader: - kv 4: minicpm.block_count u32 = 24llama_model_loader: - kv 5: minicpm.feed_forward_length u32 = 2560llama_model_loader: - kv 6: minicpm.rope.dimension_count u32 = 128llama_model_loader: - kv 7: minicpm.attention.head_count u32 = 8llama_model_loader: - kv 8: minicpm.attention.head_count_kv u32 = 2llama_model_loader: - kv 9: minicpm.attention.layer_norm_rms_epsilon f32 = 0.000010llama_model_loader: - kv 10: general.file_type u32 = 15llama_model_loader: - kv 11: minicpm.tie_lm_head bool = falsellama_model_loader: - kv 12: tokenizer.ggml.model str = llamallama_model_loader: - kv 13: tokenizer.ggml.pre str = defaultllama_model_loader: - kv 14: tokenizer.ggml.tokens arr[str,122753] = ["", "", "", "", "'llm_load_print_meta: EOS token = 2 ''llm_load_print_meta: UNK token = 0 ''llm_load_print_meta: LF token = 1099 '<0x0A>'llm_load_print_meta: max token length = 48llm_load_tensors: ggml ctx size = 0.20 MiBllm_load_tensors: offloading 24 repeating layers to GPUllm_load_tensors: offloading non-repeating layers to GPUllm_load_tensors: offloaded 25/25 layers to GPUllm_load_tensors: Metal buffer size = 242.04 MiBllm_load_tensors: CPU buffer size = 67.43 MiB.................................................llama_new_context_with_model: n_ctx = 8192llama_new_context_with_model: n_batch = 2048llama_new_context_with_model: n_ubatch = 512llama_new_context_with_model: flash_attn = 0llama_new_context_with_model: freq_base = 10000.0llama_new_context_with_model: freq_scale = 1ggml_metal_init: allocatingggml_metal_init: found device: Apple M1ggml_metal_init: picking default device: Apple M1ggml_metal_init: using embedded metal libraryggml_metal_init: GPU name: Apple M1ggml_metal_init: GPU family: MTLGPUFamilyApple7 (1007)ggml_metal_init: GPU family: MTLGPUFamilyCommon3 (3003)ggml_metal_init: GPU family: MTLGPUFamilyMetal3 (5001)ggml_metal_init: simdgroup reduction support = trueggml_metal_init: simdgroup matrix mul. support = trueggml_metal_init: hasUnifiedMemory = trueggml_metal_init: recommendedMaxWorkingSetSize = 11453.25 MBllama_kv_cache_init: Metal KV buffer size = 192.00 MiBllama_new_context_with_model: KV self size = 192.00 MiB, K (f16): 96.00 MiB, V (f16): 96.00 MiBllama_new_context_with_model: CPU output buffer size = 0.47 MiBllama_new_context_with_model: Metal compute buffer size = 241.75 MiBllama_new_context_with_model: CPU compute buffer size = 18.01 MiBllama_new_context_with_model: graph nodes = 824llama_new_context_with_model: graph splits = 2llama_init_from_gpt_params: warming up the model with an empty run - please wait ... (--no-warmup to disable)main: llama threadpool init, n_threads = 4system_info: n_threads = 4 (n_threads_batch = 4) / 8 | AVX = 0 | AVX_VNNI = 0 | AVX2 = 0 | AVX512 = 0 | AVX512_VBMI = 0 | AVX512_VNNI = 0 | AVX512_BF16 = 0 | FMA = 0 | NEON = 1 | SVE = 0 | ARM_FMA = 1 | F16C = 0 | FP16_VA = 1 | RISCV_VECT = 0 | WASM_SIMD = 0 | BLAS = 1 | SSE3 = 0 | SSSE3 = 0 | VSX = 0 | MATMUL_INT8 = 0 | LLAMAFILE = 1 | sampler seed: 1822835924sampler params: repeat_last_n = 64, repeat_penalty = 1.000, frequency_penalty = 0.000, presence_penalty = 0.000 top_k = 40, tfs_z = 1.000, top_p = 0.950, min_p = 0.050, typical_p = 1.000, temp = 0.800 mirostat = 0, mirostat_lr = 0.100, mirostat_ent = 5.000sampler chain: logits -> logit-bias -> penalties -> top-k -> tail-free -> typical -> top-p -> min-p -> temp-ext -> softmax -> dist generate: n_ctx = 8192, n_batch = 2048, n_predict = -1, n_keep = 1 北京有什么好玩的地方吗 北京旅游攻略北京有哪些好玩的地方?北京有什么好玩的地方吗 北京旅游攻略1、颐和园(北京颐和园):中国四大皇家园林之一,位于北京西郊,以水景和园林艺术闻名于世,被誉为“皇家园林博物馆”。颐和园以其碧水、碧草、碧花、碧石、碧泉、碧松、碧池、碧莲、碧云、碧林等自然景观而闻名于世,是中华民族文化的象征。颐和园位于北京西郊,以水景和园林艺术闻名于世,被誉为“皇家园林博物馆”。颐和园以其碧水、碧草、碧花、碧石、碧泉、碧莲、碧云、碧林等自然景观而闻名于世,是中华民族文化的象征。颐和园是中国八大名胜之一。颐和园位于北京西山南麓,东临昆明湖,西濒昆明湖,北依九龙山。它由内湖、外湖、山门、东配殿、西配殿、玉祥殿等部分构成。整个园呈南北走向,园内群山环绕,水景丰富。颐和园的门票为20元。颐和园有1个入口,游客可乘坐园中索道游览。门票:1.门票:门票包括门票和景区的游览车,游览车收费在10元--60元之间,景区内游览车收费标准在20-100元之间。2.景区的游览车:游览车费用在20元--100元之间。颐和园的门票包括门票和景区的游览车,游览车收费在10元-60元之间,景区内游览车收费标准在20-100元之间。颐和园有1个入口,游客可乘坐园中索道游览。门票:1.门票:门票包括门票和景区的游览车,游览车收费在10元--60元之间,景区内游览车收费标准在20-100元之间。颐和园的门票包括门票和景区的游览车,游览车费用在20元--100元之间。颐和园的门票包括门票和景区的游览车,游览车费用在20元-100元之间。颐和园有1个入口,游客可乘坐园中索道游览。门票:1.门票:门票包括门票和景区的游览车,游览车费用在20元-100元之间,景区内游览车收费标准在20-100元之间。2.景区的游览车:游览车费用在20元-100元之间。颐和园门票包括门票和景区的游览车,游览车费用在20元-100元之间。颐和园门票包括门票和景区的游览车,游览车费用在20元-100元之间。颐和园的门票包括门票和景区的游览车,游览车费用在20元-100元之间。颐和园门票包括门票和景区的游览车,游览车费用在20元-100元之间。颐和园有1个入口,游客可乘坐园中索道游览。门票:1.门票:门票包括门票和景区的游览车,游览车费用在20元-100元之间,景区内游览车收费标准在20-100元之间。2.景区的游览车:游览车费用在20元-100元之间。颐和园门票包括门票和景区的游览车,游览车费用在20元-100元之间。颐和园有1个入口,游客可乘坐园中索道游览。门票:1.门票:门票包括门票和景区的游览车,游览车费用在20元-100元之间,景区内游览车收费标准在20-100元之间。2.景区的游览车:游览车费用在20元-100元之间。 [end of text]llama_perf_sampler_print: sampling time = 174.82 ms / 888 runs ( 0.20 ms per token, 5079.63 tokens per second)llama_perf_context_print: load time = 213.10 msllama_perf_context_print: prompt eval time = 23.00 ms / 5 tokens ( 4.60 ms per token, 217.35 tokens per second)llama_perf_context_print: eval time = 8911.11 ms / 882 runs ( 10.10 ms per token, 98.98 tokens per second)llama_perf_context_print: total time = 9372.60 ms / 887 tokens
三、交叉编译
大模型版本需要在手机上运行时,需要进行交叉编译。编译安卓版本为例。
3.1 安卓命令行版本
项目docs/android.md 有编译说明文档。
3.1.1 编译安卓端命令行
主要的编译步骤:
$ mkdir build-android$ cd build-android$ export NDK=$ cmake -DCMAKE_INSTALL_PREFIX=./out -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-23 -DCMAKE_C_FLAGS="-fPIC" ..$ 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 -fPIC" ..$ make
export NDK=
设置环境变量NDK,指定NDK的目录。
cmake设置cmake相关参数。
以下对cmake的参数说明。
参数 | 取值 | 说明 |
CMAKE_TOOLCHAIN_FILE | NDK的toolchain的cmake文件 | NDK中cmake |
ANDROID_ABI | 指令集类型,arm64-v8a:arm64位版本 | 指定的指令集类型,2019年以后,推荐使用ARM64版本 |
ANDROID_PLATFORM | 安卓平台版本 | 安卓平台版本。android-23,目标平台为安卓23 |
CMAKE_C_FLAGS | 设置了 C 编译器的标志, | 其中 -march=armv8.4a+dotprod 指定了生成的代码将针对 ARMv8.4-A 架构以及 dot product 指令集进行优化。 |
CMake编译器标志设置的常用参数。
参数 | 作用 | 备注 |
-O3 -Wall -Wextra | -O3 表示启用优化级别 3
-Wall 和 -Wextra 表示启用所有警告。
| cmake -DCMAKE_C_FLAGS="-O3 -Wall -Wextra" |
-fPIC | -fPIC 是一个编译选项,通常在编译共享库(shared library)时使用。它表示生成位置无关代码(Position Independent Code),这对于共享库在内存中的加载和重定位非常重要 | 一般交叉编译使用 |
-march=armv8.4a+dotprod | 指定了生成的代码将针对 ARMv8.4-A 架构以及 dot product 指令集进行优化。 | 一些手机不支持这个优化选项 |
3.1.2 安装命令行
通过上面的编译,得到了编译后的生成物,但是比较分散。是否存在一个安装命令,将执行文件,依赖库、头文件等安装到一个指定目录,方便使用呢?
我们查看项目 build_android/cmake_install.cmake
看到脚本中CMAKE_INSTALL_PREFIX,可以通过指定安装目录进行安装。
# Set the install prefixif(NOT DEFINED CMAKE_INSTALL_PREFIX) set(CMAKE_INSTALL_PREFIX "/Users/zhouronghua/WORK/DEV/projects/llama.cpp-master/build-android/out")endif()string(REGEX REPLACE "/$" "" CMAKE_INSTALL_PREFIX "${CMAKE_INSTALL_PREFIX}")
因此,执行cmake的时候,可以设置安装目录
$ cmake -DCMAKE_INSTALL_PREFIX=./out -DCMAKE_TOOLCHAIN_FILE=$NDK/build/cmake/android.toolchain.cmake -DANDROID_ABI=arm64-v8a -DANDROID_PLATFORM=android-23 -DCMAKE_C_FLAGS="-fPIC" ..$ make
编译完成,执行安装命令即可。
$ make install
会安装到项目的指定路径:项目/build-android/out目录中。

安装后的目录中包含可执行文件 bin文件,头文件 include,库文件: lib
生成的命令行执行文件中,llama-cli 是安卓命令行测试工具。
3.1.3 安卓命令行安装
命令行安装测试的步骤:
1)安装模型;
2)安卓可执行文件;
3)修改可执行文件权限;
4)执行推理命令
我们可以根据实际情况,安装命令行。我们可以编写安装脚本build_llama_runner.sh。
set -x# Build#ndk-build -j 8 APP_BUILD_SCRIPT=jni/Android_llava.mk# Setup phone environmentPHONE_PATH=/data/local/tmp/llamacpp# Setup phone environmentBIN_PATH=/data/local/tmp/llamacpp/bin# Model pathMODEL_PATH=/data/local/tmp/llamacpp/modelsadb shell mkdir -p $PHONE_PATHadb shell mkdir -p $BIN_PATHadb shell mkdir -p $MODEL_PATH# Push built binariesadb push bin $PHONE_PATH# install modeladb push models/MiniCPM-0-2-Q4_K.gguf $MODEL_PATH# install libraryadb push arm64-v8a/* $PHONE_PATH# Set execute permissionadb shell "chmod +x $BIN_PATH/*"
注:我们将需要安装的文件进行整理,可执行文件放在bin目录下,库文件放在arm64-v8a目录下,
模型文件放下models目录下。安装的时候,比较方便。
bin目录下的文件:对应 项目/build-android/out/bin 下的可执行文件
arm64-v8a:对应 项目/build-android/out/lib 下的库文件。libomp.so这个库文件怎么来的,后面会进行说明。
models: 推理使用的模型文件。

3.1.4 命令行测试
安装完成以后,可以进行测试。
编写脚本 run_mincpm_1b.sh ,方便执行。
set -x# model fileMODEL_FILE=MiniCPM-0-2-Q4_K.gguf#MODEL_FILE=minCPM-2-Q4_1.gguf# Setup phone environmentPHONE_PATH=/data/local/tmp/llamacpp# Model path(模型文件安装目录)MODEL_PATH=/data/local/tmp/llamacpp/models# promptPROMPT="北京有什么好玩的地方"# Set execute permissionadb shell "chmod +x $PHONE_PATH/bin/llama-cli"# Run using the below commandsadb shell "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PHONE_PATH $PHONE_PATH/bin/llama-cli -m $MODEL_PATH/$MODEL_FILE --prompt \"$PROMPT\"" -t 4adb shell "LD_LIBRARY_PATH=$LD_LIBRARY_PATH:$PHONE_PATH $PHONE_PATH/bin/llama-cli -m $MODEL_PATH/$MODEL_FILE --prompt \"$PROMPT\"" -t 6
可以看到命令行输出结果则成功。
3.1.5 命令行执行常见错误
1. 命令行执行提示缺少 libomp.so 库
library "libomp.so" not found: needed by /data/local/tmp/llamacpp/libggml.so
这个错误提示表明在加载 libggml.so
库时,系统无法找到所需的 libomp.so
库。这通常是由于缺少 OpenMP 库导致的问题。OpenMP 是一种支持多线程编程的标准,而 libomp.so
是 OpenMP 库的动态链接库文件。
怎么找到libomp.so
库呢?
需要从NDK的库中查找,一般的NDK都自带了这个库。
在NDK根目录,搜索“libomp.so
”,就能找到,。

3.2 JNI编译
JNI编译提供有两种方式,一种是源码依赖,将需要的C++代码都编译到目标so库中。
第二种方式是库以来,直接使用编译生成的库进行链接。
3.2.1 源码依赖
项目中提供了源码依赖的demo工程。
项目/examples/llama.android 这个是demo工程。

项目包含两个module:
-llama: JNI封装库
-app: Demo项目
3.2.1.1 JNI编译
我们看 项目/llama/src/main/cpp/CMakeLists.txt 编译脚本,
# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html.# For more examples on how to use CMake, see https://github.com/android/ndk-samples.# Sets the minimum CMake version required for this project.cmake_minimum_required(VERSION 3.22.1)# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},# Since this is the top level CMakeLists.txt, the project name is also accessible# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level# build script scope).project("llama-android")#include(FetchContent)#FetchContent_Declare(# llama# GIT_REPOSITORY https://github.com/ggerganov/llama.cpp# GIT_TAG master#)# Also provides "common"#FetchContent_MakeAvailable(llama)# Creates and names a library, sets it as either STATIC# or SHARED, and provides the relative paths to its source code.# You can define multiple libraries, and CMake builds them for you.# Gradle automatically packages shared libraries with your APK.## In this top level CMakeLists.txt, ${CMAKE_PROJECT_NAME} is used to define# the target library name; in the sub-module's CMakeLists.txt, ${PROJECT_NAME}# is preferred for the same purpose.##load local llama.cppadd_subdirectory(../../../../../../ build-llama)# In order to load a library into your app from Java/Kotlin, you must call# System.loadLibrary() and pass the name of the library defined here;# for GameActivity/NativeActivity derived applications, the same library name must be# used in the AndroidManifest.xml file.add_library(${CMAKE_PROJECT_NAME} SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. llama-android.cpp)# Specifies libraries CMake should link to your target library. You# can link libraries from various origins, such as libraries defined in this# build script, prebuilt third-party libraries, or Android system libraries.target_link_libraries(${CMAKE_PROJECT_NAME} # List libraries link to the target library llama common android log)
add_subdirectory添加的是项目的源码目录,采用源码编译。
1)当前编译的so库是:libllama-android.so
2)需要依赖的编译目录:项目
3)链接需要的依赖库:libllama.so、libcommon.a、安卓项目依赖库:libandroid.so、liblog.so
3.2.1.2 libllama.so编译规则
项目/src/CMakeLists.txt 编译脚本
# TODO: should not use thisif (WIN32) if (BUILD_SHARED_LIBS) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) endif()endif()## libraries## llama(目标包含的源文件)add_library(llama ../include/llama.h llama.cpp llama-vocab.cpp llama-grammar.cpp llama-sampling.cpp unicode.h unicode.cpp unicode-data.cpp )# libllama.so的头文件目录target_include_directories(llama PUBLIC . ../include)target_compile_features (llama PUBLIC cxx_std_11) # don't bump# libllama.so的链接依赖库target_link_libraries(llama PUBLIC ggml)# 编译选项设置if (BUILD_SHARED_LIBS) set_target_properties(llama PROPERTIES POSITION_INDEPENDENT_CODE ON) target_compile_definitions(llama PRIVATE LLAMA_SHARED LLAMA_BUILD)endif()
3.2.2 库依赖
通过3.1.3 我们了解了引用 llama.cpp需要的库和头文件,因此可以通过采用 依赖库+头文件的方式,直接进行依赖呢?
这种方法也是可以的。
1)引用头文件;
2)引用依赖库;
3)编写jni调用接口;
4)配置cmake
新建模块llamatextlib
3.2.2.1 引入头文件
llamatextlib/src/main/cpp/include
将头文件导入到此目录下。

3.2.2.2 引入依赖库
llamatextlib/src/main/jniLibs/arm64-v8a 下,添加需要依赖的库文件。因为安卓主要支持arm64-v8a。我们统一采用 arm64-v8a版本。

3.2.2.3 编写JNI调用接口
JNI接口可以参考 exsamples的Demo程序。
JNI调用接口,主要有3个:
1)加载模型:参数为模型路径
2)发送prompt,轮询获取推理结果;
3)卸载模型。
/** * 加载模型 * * @param pathToModel 模型文件路径 * @author zhouronghua * @time 2024/9/24 19:30 */ override suspend fun load(pathToModel: String, extraInfo: Map?) { withContext(runLoop) { when (threadLocalState.get()) { is State.Idle -> { // 开始加载模型时间 val t1 = System.currentTimeMillis() // 以下是模型加载处理 val model = load_model(pathToModel) if (model == 0L) throw IllegalStateException("load_model() failed") val context = new_context(model) if (context == 0L) throw IllegalStateException("new_context() failed") val batch = new_batch(512, 0, 1) if (batch == 0L) throw IllegalStateException("new_batch() failed") val sampler = new_sampler() if (sampler == 0L) throw IllegalStateException("new_sampler() failed") Log.i(tag, "Loaded model $pathToModel") threadLocalState.set(State.Loaded(model, context, batch, sampler)) // 模型加载完成 val t2 = System.currentTimeMillis() llmMessagePublisher?.publish( LlmMetricMessage( LlmMetricType.ModelLoaded, (t2 - t1).toDouble() ) ) } else -> throw IllegalStateException("Model already loaded") } } }
发送prompt: completion_init
轮询推理结果:completion_loop
/** * 发送提问 * * @param message 提问内容 * @author zhouronghua * @time 2024/9/24 20:03 */ override fun send(message: String, endCallback: ((Boolean) -> Unit)?): Flow = flow { // 重置当次推测暂停状态 isPause.set(false) when (val state = threadLocalState.get()) { is State.Loaded -> { // 开始时间 val t1 = System.currentTimeMillis() // 当前token数 val ncur = IntVar(completion_init(state.context, state.batch, "$message", nlen)) // 计算初始的token数 val oldTokenCount = ncur.value // 是否是首响token var isFirst = true while (ncur.value <= nlen) { // 依次获取推测内容分段 if (isPause.get()) { // 如果设置暂停则结束当次推测 break } // 轮询遍历获取流式内容 val str = completion_loop(state.context, state.batch, state.sampler, nlen, ncur) if (str == null) { break } if (isFirst) { // 首响通知 val t2 = System.currentTimeMillis() llmMessagePublisher?.publish( LlmMetricMessage( LlmMetricType.FirstTokenGenerated, (t2 - t1).toDouble() ) ) isFirst = false } if (str == null || str == "\n\n\n\n") { Log.e(tag, "out str == null or eos , break") break } // 发射当前的增量内容 if (!isPause.get()) { emit(str) } } // 清理缓存空间 kv_cache_clear(state.context) // 统计消息发送结束 val t3 = System.currentTimeMillis() llmMessagePublisher?.publish( LlmMetricMessage( LlmMetricType.GenerationFinished, (t3 - t1).toDouble() ) ) // token生成速度 llmMessagePublisher?.publish( LlmMetricMessage( LlmMetricType.GenerationSpeed, (ncur.value - oldTokenCount).toDouble() / (t3 - t1) * 1000 ) ) // 通知用户智能体回复已经结束 endCallback?.invoke(true) } else -> { // 通知用户智能体回复已经结束 endCallback?.invoke(false) } } }.flowOn(runLoop)
卸载模型:
/** * Unloads the model and frees resources. * * This is a no-op if there's no model loaded. */ override suspend fun unload() { withContext(runLoop) { when (val state = threadLocalState.get()) { is State.Loaded -> { free_context(state.context) free_model(state.model) free_batch(state.batch) free_sampler(state.sampler) threadLocalState.set(State.Idle) } else -> {}} } }
其余的是一些JNI 接口函数
private external fun log_to_android() private external fun load_model(filename: String): Long private external fun free_model(model: Long) private external fun new_context(model: Long): Long private external fun free_context(context: Long) private external fun backend_init(numa: Boolean) private external fun backend_free() private external fun new_batch(nTokens: Int, embd: Int, nSeqMax: Int): Long private external fun free_batch(batch: Long) private external fun new_sampler(): Long private external fun free_sampler(sampler: Long) private external fun bench_model( context: Long, model: Long, batch: Long, pp: Int, tg: Int, pl: Int, nr: Int ): String private external fun system_info(): String private external fun completion_init( context: Long, batch: Long, text: String, nLen: Int ): Int private external fun completion_loop( context: Long, batch: Long, sampler: Long, nLen: Int, ncur: IntVar ): String? private external fun kv_cache_clear(context: Long)
3.2.2.4 编写CMakeList.txt
cmake文件与源码编译类似,不过依赖的头文件和库文件有些不同。
# For more information about using CMake with Android Studio, read the# documentation: https://d.android.com/studio/projects/add-native-code.html.# For more examples on how to use CMake, see https://github.com/android/ndk-samples.# Sets the minimum CMake version required for this project.cmake_minimum_required(VERSION 3.22.1)# Declares the project name. The project name can be accessed via ${ PROJECT_NAME},# Since this is the top level CMakeLists.txt, the project name is also accessible# with ${CMAKE_PROJECT_NAME} (both CMake variables are in-sync within the top level# build script scope).# llama文本推理库project("llamatext")# cmake 自带变量 设置编译类型为releaseset(CMAKE_BUILD_TYPE Release)# 设置 C++ 编译器标志set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -fPIC")# 编译目标库add_library(${CMAKE_PROJECT_NAME} SHARED # List C/C++ source files with relative paths to this CMakeLists.txt. llama-android.cpp)# 设置 目标库依赖的头文件target_include_directories(${CMAKE_PROJECT_NAME} PUBLIC ${USER_LOCAL_C_INCLUDES} ./include)# Specifies libraries CMake should link to your target library. You# can link libraries from various origins, such as libraries defined in this# build script, prebuilt third-party libraries, or Android system libraries.target_link_libraries(${CMAKE_PROJECT_NAME} # List libraries link to the target library llama common ggml android log)# 添加依赖库目标add_library(common STATIC IMPORTED)add_library(llama SHARED IMPORTED)add_library(ggml SHARED IMPORTED)# 依赖库标目属性set_target_properties( common PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libcommon.a)set_target_properties( llama PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libllama.so)set_target_properties( ggml PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libggml.so)
头文件:指向导入的include目录
库文件:依赖库的都是通过 IMPORT进行依赖。
common库,指向jniLibs下的静态依赖库。
# 依赖库标目属性set_target_properties( common PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libcommon.a
libllama.so指向jniLibs下的对应库
set_target_properties( llama PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libllama.so)
libggml.so指向jniLibs下的依赖库。
set_target_properties( ggml PROPERTIES IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/arm64-v8a/libggml.so)
这样,就通过库以来集成了llama.cpp。如果有版本更新,编译生成新的so库,替换更新就行。
参考文献:
1.《大模型推理--KV Cache》
大模型推理--KV Cache_prefilling decoding-CSDN博客