菜鸟AI - 让提示词生成更简单! 全站导航 全站导航
AI工具安装 新手教程 进阶教程 辅助资源 AI提示词 热点资讯 技术资讯 产业资讯 内容生成 模型技术 AI信息库

已有账号?

首页 > AI教程 > 安卓端侧大模型高效部署实战:模型加载与性能优化全攻略
进阶教程 大模型 模型加载与性能优化全

安卓端侧大模型高效部署实战:模型加载与性能优化全攻略

2026-06-01
阅读 0
热度 0
作者 菜鸟AI编辑部
摘要

摘要

通过JNI桥接,在Android端加载llama cpp量化模型,Kotlin侧声明外部函数接口,C++层实现模型加

上一篇文章完成了 llama.cpp 在 Android 平台的编译与 so 动态库加载,打下了基础设施。本文聚焦核心环节——将量化模型部署至手机端运行,涵盖 JNI 桥接、模型生命周期管理以及 UI 交互实现,逐步解析完整流程。

1. Kotlin 桥接层:Llama.kt

先从 Kotlin 端的桥接类入手,源码位于 app/src/main/java/com/example/llamatest/Llama.kt。通过 System.loadLibrary 加载预编译的 ggmlllama 原生库,并声明三个外部函数:loadModel(加载模型)、unloadModel(卸载模型)和 chat(对话生成)。采用 object 单例模式确保全局仅存在一个模型实例。

object Llama {
    init {
        System.loadLibrary("ggml")
        System.loadLibrary("llama")
    }
    external fun loadModel(path: String): Boolean
    external fun unloadModel()
    external fun chat(prompt: String): String
}

2. JNI C++ 完整实现:llama_wrapper.cpp

核心的 JNI 实现位于 app/src/main/cpp/llama_wrapper.cpp,封装了三个关键接口:模型加载(loadModel)、文本生成(generate)和内存释放(releaseModel)。全局变量管理是设计要点——模型指针、上下文指针和词表指针均以 static 变量持有,需严格控制生命周期。

模型加载:首先释放旧资源,再从文件加载模型并配置上下文参数(上下文窗口设为 1024,采用单线程推理),最终返回布尔值指示加载结果。

#include 
#include 
#include 
#include 

#ifdef __cplusplus
extern "C" {
#endif
#include "llama.h"
#ifdef __cplusplus
}
#endif

#define LOGD(...) __android_log_print(ANDROID_LOG_INFO, "LLAMA_FIX", __VA_ARGS__)

// 全局变量
static llama_model* g_model = nullptr;
static llama_context* g_ctx = nullptr;
static const llama_vocab* g_vocab = nullptr;

// 加载模型
extern "C" JNIEXPORT jboolean JNICALL
Ja va_com_example_llamatest_MainActivity_loadModel(JNIEnv* env, jobject, jstring modelPath) {
    // 清理旧资源
    if (g_ctx) { llama_free(g_ctx); g_ctx = nullptr; }
    if (g_model) { llama_model_free(g_model); g_model = nullptr; }
    g_vocab = nullptr;

    const char* path = env->GetStringUTFChars(modelPath, nullptr);
    llama_model_params mparams = llama_model_default_params();
    mparams.n_gpu_layers = 0;
    g_model = llama_model_load_from_file(path, mparams);
    env->ReleaseStringUTFChars(modelPath, path);
    if (!g_model) return JNI_FALSE;

    g_vocab = llama_model_get_vocab(g_model);
    llama_context_params cparams = llama_context_default_params();
    cparams.n_ctx = 1024;
    cparams.n_threads = 1;
    g_ctx = llama_init_from_model(g_model, cparams);
    return g_ctx ? JNI_TRUE : JNI_FALSE;
}

采样与生成:采样逻辑采用贪心策略,直接选取 logits 最大值对应的 token。生成循环逐 token 解码并追加至结果字符串,直至遇到 EOS 或达到最大生成长度(32 tokens)。注意 batch 生命周期管理——代码使用了 llama_batch_get_one 创建的临时 batch,因此无需调用 llama_batch_free

// 采样 token
static llama_token sample_token() {
    float* logits = llama_get_logits_ith(g_ctx, -1);
    int n_vocab = llama_vocab_n_tokens(g_vocab);
    int best = 0;
    float max_logit = -1e9;
    for (int i = 0; i < n_vocab; i++) {
        if (logits[i] > max_logit) {
            max_logit = logits[i];
            best = i;
        }
    }
    return (llama_token)best;
}

// 生成
extern "C" JNIEXPORT jstring JNICALL
Ja va_com_example_llamatest_MainActivity_generate(JNIEnv* env, jobject, jstring prompt) {
    if (!g_ctx || !g_model || !g_vocab) {
        return env->NewStringUTF("模型未加载");
    }
    // 拼装提示词格式
    const char* prompt_c = env->GetStringUTFChars(prompt, nullptr);
    std::string input = "user\n";
    input += prompt_c;
    input += "\nmodel\n";
    env->ReleaseStringUTFChars(prompt, prompt_c);

    // 分词
    std::vector tokens(512);
    int n_tokens = llama_tokenize(g_vocab, input.c_str(), (int)input.size(),
                                   tokens.data(), 512, true, false);
    if (n_tokens <= 0) {
        return env->NewStringUTF("分词失败");
    }

    // 推理提示词
    llama_batch batch = llama_batch_get_one(tokens.data(), n_tokens);
    llama_decode(g_ctx, batch);

    std::string result;
    const int MAX_GEN = 32;
    const llama_token eos = llama_vocab_eos(g_vocab);
    for (int i = 0; i < MAX_GEN; i++) {
        llama_token token = sample_token();
        if (token == eos || token == 0) break;
        char buf[256] = {0};
        llama_token_to_piece(g_vocab, token, buf, sizeof(buf)-1, 0, false);
        result += buf;
        // 推理下一个 token
        llama_batch b = llama_batch_get_one(&token, 1);
        llama_decode(g_ctx, b);
    }
    return env->NewStringUTF(result.c_str());
}

3. 布局文件:activity_main.xml

UI 层使用简洁的 LinearLayout 垂直布局,包含三个核心组件:显示结果的 TextView(ID:tv_result)、输入问题的 EditText(ID:et_input),以及“选择模型”和“发送”两个按钮。控件 ID 均采用下划线命名风格,与 Kotlin 代码中的 findViewById 调用保持一致。

"1.0" encoding="utf-8"?>
"http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gra vity="center"
    android:orientation="vertical"
    android:padding="16dp">

    "@+id/tv_result"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:textSize="24sp"
        android:textColor="#FF0000"
        android:textStyle="bold"
        android:gra vity="center"
        android:minHeight="300dp" />

    "@+id/et_input"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:layout_marginTop="20dp" />

    

4. Activity 完整逻辑:MainActivity.kt

这是整个 App 的控制器,负责文件选择、模型加载、线程管理及 UI 更新等关键环节。以下设计要点值得关注:

  • 模型加载状态标志:通过 isModelLoaded 布尔变量控制发送按钮的启用状态,防止模型未加载时发起请求。
  • 文件选择与复制:使用 Intent.ACTION_OPEN_DOCUMENT 触发系统文件选择器,让用户选取 gguf 模型文件,随后异步复制至应用私有目录。此举可规避非 root 设备的文件访问权限限制。
  • UI 线程安全:所有 UI 更新经由 uiHandler.post 在主线程执行,而模型加载与推理等耗时操作则置于子线程处理。
  • 资源释放:在 onDestroy 生命周期中调用 releaseModel 释放模型资源,杜绝内存泄漏风险。
package com.example.llamatest

import android.app.Activity
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.util.Log
import android.widget.EditText
import android.widget.TextView
import ja va.io.File
import ja va.io.FileOutputStream

class MainActivity : Activity() {
    private lateinit var tvResult: TextView
    private lateinit var etInput: EditText
    private lateinit var btnSelectModel: android.widget.Button
    private lateinit var btnSend: android.widget.Button
    private val REQUEST_FILE = 100
    private val uiHandler = Handler(Looper.getMainLooper())
    private var isModelLoaded = false

    external fun loadModel(modelPath: String): Boolean
    external fun generate(prompt: String): String
    external fun releaseModel()

    companion object {
        private const val TAG = "LLAMA_DEBUG_FINAL"
        init {
            try {
                Log.d(TAG, "【初始化】加载库:llama_jni")
                System.loadLibrary("llama_jni")
                Log.d(TAG, "【初始化】✅ 库加载成功")
            } catch (e: Exception) {
                Log.e(TAG, "【初始化】❌ 库加载失败", e)
            }
        }
    }

    override fun onCreate(sa vedInstanceState: Bundle?) {
        super.onCreate(sa vedInstanceState)
        setContentView(R.layout.activity_main)
        tvResult = findViewById(R.id.tv_result)
        etInput = findViewById(R.id.et_input)
        btnSelectModel = findViewById(R.id.btn_select_model)
        btnSend = findViewById(R.id.btn_send)

        btnSelectModel.setOnClickListener {
            val intent = Intent(Intent.ACTION_OPEN_DOCUMENT).apply {
                addCategory(Intent.CATEGORY_OPENABLE)
                type = "*/*"
            }
            startActivityForResult(intent, REQUEST_FILE)
        }

        btnSend.setOnClickListener {
            if (!isModelLoaded) {
                updateUIText("❌ 请先选择并加载模型!")
                return@setOnClickListener
            }
            val prompt = etInput.text.toString().trim()
            if (prompt.isEmpty()) {
                updateUIText("请输入问题")
                return@setOnClickListener
            }
            Thread {
                try {
                    val reply = generate(prompt)
                    uiHandler.post {
                        updateUIText("你:$prompt\n\nAI:$reply")
                        etInput.setText("")
                    }
                } catch (e: Exception) {
                    uiHandler.post { updateUIText("错误:${e.message}") }
                }
            }.start()
        }
    }

    override fun onDestroy() {
        super.onDestroy()
        try {
            releaseModel()
            isModelLoaded = false
        } catch (e: Exception) { /* 忽略 */ }
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)
        if (requestCode == REQUEST_FILE && resultCode == RESULT_OK) {
            val uri = data?.data ?: return
            Thread {
                val file = File(filesDir, "gemma.gguf")
                try {
                    contentResolver.openInputStream(uri)?.use { input ->
                        FileOutputStream(file).use { output -> input.copyTo(output) }
                    }
                    // 等待3秒后加载模型
                    Thread.sleep(3000)
                    val success = loadModel(file.absolutePath)
                    uiHandler.post {
                        if (success) {
                            isModelLoaded = true
                            updateUIText("✅ 模型加载成功!可以聊天了!")
                        } else {
                            updateUIText("❌ 模型加载失败")
                        }
                    }
                } catch (e: Exception) {
                    uiHandler.post { updateUIText("错误:${e.message}") }
                }
            }.start()
        }
    }

    private fun updateUIText(s: String) {
        runOnUiThread {
            tvResult.text = s
            tvResult.postInvalidate()
        }
    }
}

5. CMakeLists.txt 构建脚本配置

构建脚本位于 app/src/main/cpp/CMakeLists.txt。将 libllama.solibggml.so 作为预编译库导入,编译 llama_wrapper.cpp 生成 libllama_jni.so。关键点在于头文件路径必须指向 llama.h 所在目录,且链接顺序需正确——llama_jni 依赖 llamaggml,最后额外链接 androidlog 用于日志输出。

cmake_minimum_required(VERSION 3.22.1)
project(llamatest)

add_library(llama SHARED IMPORTED)
set_target_properties(llama PROPERTIES
    IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libllama.so)

add_library(ggml SHARED IMPORTED)
set_target_properties(ggml PROPERTIES
    IMPORTED_LOCATION ${CMAKE_SOURCE_DIR}/../jniLibs/${ANDROID_ABI}/libggml.so)

target_include_directories(llama INTERFACE ${CMAKE_SOURCE_DIR}/llama)
target_include_directories(ggml INTERFACE ${CMAKE_SOURCE_DIR}/llama)

add_library(llama_jni SHARED
    llama_wrapper.cpp)

target_include_directories(llama_jni PRIVATE
    ${CMAKE_SOURCE_DIR}/llama)

target_link_libraries(llama_jni
    llama
    ggml
    androidlog)

6. build.gradle.kts 构建配置要点

App 模块的构建文件需配置 CMake 路径、ABI 过滤(仅保留 arm64-v8a)及 C++ 标准。注意 compileSdktargetSdk 均设为 36,minSdk 为 35,以适配较新的 Android 版本。

plugins {
    alias(libs.plugins.android.application)
    alias(libs.plugins.kotlin.compose)
}

android {
    namespace = "com.example.llamatest"
    compileSdk = 36

    defaultConfig {
        applicationId = "com.example.llamatest"
        minSdk = 35
        targetSdk = 36
        versionCode = 1
        versionName = "1.0"

        externalNativeBuild {
            cmake {
                cppFlags += "-std=c++17"
            }
        }
        ndk {
            abiFilters.add("arm64-v8a")
        }
    }

    externalNativeBuild {
        cmake {
            path = file("src/main/cpp/CMakeLists.txt")
            version = "3.22.1"
        }
    }
    
    compileOptions {
        sourceCompatibility = Ja vaVersion.VERSION_11
        targetCompatibility = Ja vaVersion.VERSION_11
    }
    buildFeatures {
        compose = true
    }
}

dependencies {
    implementation("androidx.appcompat:appcompat:1.6.1")
    implementation("com.google.android.material:material:1.11.0")
    implementation("androidx.constraintlayout:constraintlayout:2.1.4")
    implementation(libs.androidx.core.ktx)
    implementation(libs.androidx.lifecycle.runtime.ktx)
    // 其他 compose 依赖...
}

7. 预置动态库部署

将上一篇文章编译生成的 libllama.solibggml.so 放置于 app/src/main/jniLibs/arm64-v8a/ 目录下。确保 ABI 架构与 build.gradle 中的配置一致。

8. 运行流程说明

本示例使用的模型为前文《端侧 AI 模型部署实战三(模型转换)》中量化得到的 gemma-3-4b-it-q4_K_M.gguf。在非 root 手机上直接加载外部存储文件会触发权限问题,因此采用文件选择器授权方案——用户通过系统文件选择器选取模型文件,再复制至应用私有目录后加载。以下为手机离线环境下的运行结果截图。

9. 运行效果展示

10. 实践中的关键难点

项目使用了今年 3 月的 llama.cpp 分支(b8648),版本较新。AI 生成的参考代码在编译与运行时频繁崩溃,耗费大量时间排查。最终直接将 PC 端 llama-cli.exe 的源码提供给 AI,要求其基于最新主干输出 JNI 移植版本,才使运行趋于稳定。

一个值得分享的经验:模型加载与推理生成的 JNI 接口移植是端侧部署的核心,基础代码框架可由 AI 辅助生成,但核心流程与关键逻辑仍需手动阅读源码、理解原理,不可全权依赖 AI。

11. 待解决的遗留问题

目前仅实现了纯文本 LLM 的推理,尚未支持多模态。此外,文本生成速度较慢——单次推理耗时超过 10 秒,存在较大优化空间。实时性仍无法满足对话场景需求,后续计划探索 RAG 方案在手机端的落地实现。

来源:互联网

免责声明

本网站新闻资讯均来自公开渠道,力求准确但不保证绝对无误,内容观点仅代表作者本人,与本站无关。若涉及侵权,请联系我们处理。本站保留对声明的修改权,最终解释权归本站所有。

同类文章推荐

相关文章推荐

更多