OpenHarmony平台大语言模型本地推理:llama深度适配与部署技术详解
1. 概述
1.1 背景与目标
大语言模型(LLM)正从云端向边缘设备渗透,以满足低延迟、高隐私和离线使用的需求。OpenHarmony 作为一个功能强大的分布式操作系统,为在各类终端设备上运行 AI 模型提供了理想的平台。llama.cpp
是一个广受欢迎的 C/C++ 实现,它使得在消费级硬件上运行 LLaMA 系列模型成为可能。
ohosllama.cpp
项目正是将 llama.cpp
的核心能力引入 OpenHarmony 生态的桥梁。本指南将系统性地讲解该项目的实现原理,并带领开发者完成从源码整合、构建配置、NAPI 接口封装到应用层调用的全过程,旨在让开发者能够将 LLM 推理能力无缝集成到自己的 OpenHarmony 应用中。
1.2 核心概念
- llama.cpp: 一个用 C/C++ 编写的 LLaMA 模型推理库。其核心优势在于纯 C/C++ 实现、无外部依赖、支持多种量化格式,使其非常适合资源受限的边缘设备。
- OpenHarmony: 一个开源的、面向全场景的分布式操作系统。本文档聚焦于其基于 Node.js 的原生模块能力。
- NAPI (Native API): OpenHarmony 提供的一套 C API,用于在 C/C++ 和 JavaScript/ArkTS 之间建立桥梁。通过 NAPI,我们可以将原生 C++ 库的功能暴露给上层应用调用。
- HAP (Harmony Ability Package): OpenHarmony 应用的发布包格式,包含了应用的所有代码、资源、库文件和配置文件。
1.3 项目结构解析
ohosllama.cpp
仓库的结构清晰地展示了其集成思路:
ohosllama.cpp/
├── llama.cpp/ # 作为 git submodule 引入的 llama.cpp 原始代码
├── napi/ # NAPI 接口封装层
│ └── napi_init.cpp # 定义并导出给 ArkTS 调用的 C++ 函数
├── entry/ # 示例 OpenHarmony 应用
│ ├── ets/
│ │ └── pages/ # ArkTS UI 和业务逻辑
│ └── BUILD.gn # 示例应用的构建脚本
├── BUILD.gn # 核心原生库 的构建脚本
└── README.md
这个结构的核心思想是:将 llama.cpp
作为一个静态库编译,然后通过 NAPI 层将其关键功能(如加载模型、文本生成)封装成动态库 libohosllama.so
,最后在 OpenHarmony 应用中加载并调用这个动态库。
2. 环境准备
在开始之前,请确保您的开发环境已准备就绪:
- 操作系统: 推荐 Windows 10/11, macOS, 或 Linux。
- DevEco Studio: 安装最新版本的 DevEco Studio,并确保已配置好 OpenHarmony SDK。
- Git: 用于克隆项目仓库。
3. 核心移植步骤
3.1 获取源码
首先,克隆 ohosllama.cpp
项目。由于该项目包含了 llama.cpp
作为子模块,必须使用 --recursive
参数来确保所有依赖代码都被下载。
git clone --recursive https://gitcode.com/openharmony-sig/ohosllama.cpp
cd ohosllama.cpp
3.2 构建系统配置 (BUILD.gn
)
OpenHarmony 使用 gn
和 ninja
进行构建。项目根目录下的 BUILD.gn
文件是整个原生库的构建蓝图。我们来解析其关键部分:
# ohosllama.cpp/BUILD.gn
import("ohos.gni")
# 定义一个名为 "ohosllama" 的动态共享库
ohos_shared_library("ohosllama") {# 指定库的输出名称output_name = "ohosllama"# 指定参与编译的源文件# 这里包含了 llama.cpp 的核心实现文件和我们的 NAPI 封装文件sources = ["napi/napi_init.cpp","llama.cpp/ggml.c","llama.cpp/llama.cpp",# ... 其他 llama.cpp 源文件]# 指定头文件搜索路径include_dirs = ["llama.cpp","llama.cpp/common","napi",]# 指定 C 编译器参数cflags = ["-Wall","-Wextra","-Wno-unused-function",]# 指定 C++ 编译器参数cflags_cc = ["-std=c++11","-fno-rtti","-fno-exceptions",]# 定义外部依赖,这里依赖了 OpenHarmony 的 NAPI 模块deps = ["//foundation/arkui/napi:ace_napi",]# 指定库的安装目录part_name = "entry"subsystem_name = "applications"
}
这个 BUILD.gn
文件的作用就是告诉 OpenHarmony 构建系统:请将 llama.cpp
的 C/C++ 源码和我们的 napi_init.cpp
一起,编译成一个名为 libohosllama.so
的动态库,并链接 NAPI 框架。
3.3 NAPI 接口封装
NAPI 是连接 ArkTS 和 C++ 的关键。在 napi/napi_init.cpp
中,我们定义了可以被 ArkTS 直接调用的函数。
以模型加载和文本生成为例,其 C++ 实现框架如下:
// napi/napi_init.cpp
#include "napi/native_api.h"
#include "llama.h"
// 全局变量,用于存储已加载的模型和上下文
static llama_model *model = nullptr;
static llama_context *ctx = nullptr;
// 加载模型的 NAPI 函数
static napi_value LoadModel(napi_env env, napi_callback_info info) {size_t argc = 1;napi_value args[1];napi_get_cb_info(env, info, &argc, args, nullptr, nullptr);// 1. 从 ArkTS 传入的参数中获取模型路径字符串size_t str_size;napi_get_value_string_utf8(env, args[0], nullptr, 0, &str_size);char* model_path = new char[str_size + 1];napi_get_value_string_utf8(env, args[0], model_path, str_size + 1, &str_size);// 2. 调用 llama.cpp 的核心函数加载模型llama_model_params model_params = llama_model_default_params();model = llama_load_model_from_file(model_path, model_params);delete[] model_path;// 3. 返回一个布尔值表示加载是否成功napi_value result;napi_get_boolean(env, (model != nullptr), &result);return result;
}
// 文本生成的 NAPI 函数
static napi_value Completion(napi_env env, napi_callback_info info) {// ... 参数解析逻辑,获取 prompt, max_length 等 ...// ... 调用 llama.cpp 的核心推理逻辑 ...// ... 将生成的文本结果打包成 napi_value 返回 ...return result_string;
}
// 定义导出模块的入口
static napi_value Init(napi_env env, napi_value exports) {napi_property_descriptor desc[] = {{"loadModel", nullptr, LoadModel, nullptr, nullptr, nullptr, napi_default, nullptr},{"completion", nullptr, Completion, nullptr, nullptr, nullptr, napi_default, nullptr},{"unloadModel", nullptr, UnloadModel, nullptr, nullptr, nullptr, napi_default, nullptr},};napi_define_properties(env, exports, sizeof(desc) / sizeof(desc[0]), desc);return exports;
}
// 注册模块
NAPI_MODULE(NAPI_GETHASH, Init)
这段代码清晰地展示了 NAPI 的工作模式:接收 ArkTS 的参数 -> 转换为 C++ 数据类型 -> 调用底层 C++ 库函数 -> 将 C++ 结果转换为 NAPI 值 -> 返回给 ArkTS。
4. OpenHarmony 应用集成
4.1 依赖配置
在示例应用 entry
目录下的 BUILD.gn
文件中,我们需要声明对原生库 ohosllama
的依赖。
# entry/BUILD.gn
import("ohos.gni")
ohos_hap("entry") {# ... 其他配置 ...# 声明外部依赖,指向我们之前定义的 "ohosllama" 共享库external_deps = ["hilog:libhilog","ohosllama:ohosllama", # 关键:链接原生库]
}
这行配置确保了在打包 HAP 应用时,libohosllama.so
会被一同打包,并且应用在运行时能够找到并加载它。
4.2 ArkTS 界面与逻辑开发
在 ArkTS 层,我们可以像调用普通模块一样调用原生功能。
// entry/src/main/ets/pages/Index.ets
import ohosllama from 'libohosllama.so'; // 1. 导入原生模块
@Entry
@Component
struct Index {@State message: string = '点击下方按钮开始体验';@State prompt: string = '你好,OpenHarmony!';@State result: string = '';private modelPath: string = '/data/storage/el2/base/haps/entry/files/model.q4_0.gguf'; // 模型文件路径build() {Row() {Column() {Text(this.message).fontSize(20).fontWeight(FontWeight.Bold)TextInput({ placeholder: '输入提示词' }).width('90%').margin({ top: 20 }).onChange((value: string) => {this.prompt = value;})Button('加载模型').width('80%').margin({ top: 20 }).onClick(() => {// 2. 调用原生方法 loadModelconst success = ohosllama.loadModel(this.modelPath);if (success) {this.message = '模型加载成功!';} else {this.message = '模型加载失败,请检查路径!';}})Button('生成文本').width('80%').margin({ top: 10 }).onClick(() => {if (this.message === '模型加载成功!') {// 3. 调用原生方法 completionthis.result = ohosllama.completion(this.prompt, 128); // prompt, max_length} else {this.result = '请先加载模型!';}})Text(this.result).fontSize(16).margin({ top: 20 }).fontColor(Color.Blue).width('100%').textAlign(TextAlign.Start)}.width('100%')}.height('100%')}
}
5. 模型推理流程详解
这是整个系统的核心。当用户点击“生成文本”按钮后,一个完整的推理流程被触发。下面我们详细分解这个过程:
流程图概览
[ArkTS UI] --(1. 用户点击)--> [ArkTS onClick] --(2. 调用 NAPI)--> [C++ Completion Function]|| (3. 准备推理)V[llama_context] & [Tokenize Prompt]|| (4. 循环生成)V+---------------------------------------------+| a. 获取 Logits b. 采样 Token c. 解码文本 |+---------------------------------------------+|| (5. 返回结果)V
[ArkTS UI] <------------------------------------(6. NAPI 返回字符串)---- [C++ Completion Function]
分步详解
- 触发调用: 用户在 UI 中输入提示词,点击“生成文本”。ArkTS 的
onClick
事件被触发,执行ohosllama.completion(this.prompt, 128)
。 - 跨语言调用: ArkTS 引擎识别出
ohosllama
是一个原生模块,并将调用连同参数(prompt
字符串和128
数字)传递给 NAPI 框架。NAPI 框架找到在napi_init.cpp
中注册的Completion
函数并执行它。 - 准备推理 (C++ 层):
Completion
函数首先检查全局的llama_model
指针是否有效(即模型是否已加载)。- 它使用
llama_new_context_with_model
创建一个推理上下文llama_context
。这个上下文包含了 KV 缓存等推理过程中的临时状态。 - 最关键的一步:调用
llama_tokenize
函数,将用户输入的prompt
字符串(如 “你好,OpenHarmony!”)转换成一个整数数组(std::vector<llama_token>
)。每个整数代表模型词汇表中的一个 token。
- 循环生成 (C++ 层):
- 代码进入一个
for
循环,循环次数由max_length
参数控制。 - a. 获取 Logits: 调用
llama_decode
,将当前的 token 序列输入模型。模型会计算出下一个位置所有可能 token 的概率分布(这个概率分布在llama.cpp
中被称为 logits)。 - b. 采样 Token: 调用
llama_sample_*
系列函数(如llama_sample_top_p_top_k
),根据 logits 的概率分布,结合预设的采样策略(如 top-p, top-k, temperature),从中选择一个 token 作为本次生成的结果。 - c. 解码文本: 调用
llama_token_to_piece
,将刚刚采样得到的整数 token 转换回其对应的文本片段(如一个汉字或一个单词)。 - d. 更新状态: 将新生成的 token 追加到历史 token 序列的末尾,为下一次循环生成做准备。同时,将解码出的文本片段追加到一个总的
result
字符串中。 - 循环会一直持续,直到生成了指定数量的 token,或者生成了特殊的“结束序列”(EOS)token。
- 代码进入一个
- 返回结果 (C++ 层):
- 循环结束后,
Completion
函数得到了一个完整的、包含所有生成文本的std::string
。 - 它使用
napi_create_string_utf8
将这个 C++ 字符串转换成一个 NAPI 的字符串值。 - 函数返回这个 NAPI 值。
- 循环结束后,
- 更新 UI (ArkTS 层):
- NAPI 框架将返回的字符串值传递回 ArkTS 环境。
ohosllama.completion(...)
调用完成,返回值为生成的文本。this.result = ...
这行代码被执行,状态变量result
被更新。- ArkTS 的 UI 框架检测到状态变化,自动重新渲染
Text(this.result)
组件,将生成的文本显示在屏幕上。
6. 编译与运行
- 在 DevEco Studio 中打开
ohosllama.cpp
项目。 - 将一个量化好的 GGUF 格式模型文件(例如
model.q4_0.gguf
)推送到设备的/data/storage/el2/base/haps/entry/files/
目录下。可以通过hdc file send
命令或 DevEco Studio 的 Device File Browser 实现。 - 确保代码中的
modelPath
变量与设备上的模型文件路径一致。 - 点击
Build > Build Hap(s)/APP(s) > Build Debug Hap(s)
。 - 构建成功后,点击运行按钮,将应用部署到 OpenHarmony 设备或模拟器上。
- 在设备上操作应用,依次点击“加载模型”和“生成文本”,即可体验完整的端侧 LLM 推理流程。