Back to blog
    llama.cppAndroidKotlinintegrationon-device AIVulkan

    llama.cpp Android 集成:Kotlin 与 Vulkan 指南

    将 llama.cpp 集成到 Android 应用,使用 Kotlin。涵盖 JNI 绑定、Vulkan GPU 加速、模型加载、内存管理与设备测试。

    Edward Xi Yang

    llama.cpp 在 Android 设备上使用 CPU 多线程和 Vulkan GPU 加速运行 GGUF 语言模型。llama.android 项目通过 JNI 提供预构建的 Kotlin 绑定,使 Kotlin 开发者的集成非常简单。

    本指南涵盖从项目设置到生产部署的完整集成路径。

    集成选项

    选项 1:llama.android 库(推荐)

    llama.cpp 仓库包含 llama.android,一个带有 Kotlin 绑定的预构建 Android 库。这是实现端侧 AI 最快的途径。

    将其添加到您的项目:

    1. 从 llama.cpp 仓库克隆或下载 llama.android 模块
    2. 将其作为模块包含在您的 Android 项目中
    3. 在应用的 build.gradle.kts 中添加依赖
    dependencies {
        implementation(project(":llama"))
    }

    选项 2:从源码构建

    如需更多控制权,使用 Android NDK 将 llama.cpp 构建为原生库:

    mkdir build-android && cd build-android
    cmake .. \
        -DCMAKE_TOOLCHAIN_FILE=$NDK_PATH/build/cmake/android.toolchain.cmake \
        -DANDROID_ABI=arm64-v8a \
        -DANDROID_PLATFORM=android-26 \
        -DLLAMA_VULKAN=ON \
        -DBUILD_SHARED_LIBS=ON
    cmake --build . --config Release

    这会生成 libllama.so,将其放入 jniLibs 目录即可。

    选项 3:预构建 AAR

    一些社区项目将 llama.cpp 发布为 AAR(Android Archive),您可以作为 Maven 依赖项包含。请检查最新且维护中的版本。

    项目设置

    最低要求

    • Android API 26+(Android 8.0)
    • ARM64(arm64-v8a)目标架构
    • NDK r25+ 用于构建原生代码
    • 目标设备 4GB+ RAM(用于 1B 模型)

    构建配置

    // app/build.gradle.kts
    android {
        defaultConfig {
            ndk {
                abiFilters += "arm64-v8a" // 仅 64 位 ARM
            }
        }
    }

    权限

    推理无需特殊权限。模型下载需要:

    <uses-permission android:name="android.permission.INTERNET" />

    加载模型

    class LlamaEngine(private val context: Context) {
        private var model: Long = 0 // 原生指针
        private var ctx: Long = 0   // 原生上下文指针
    
        suspend fun loadModel(modelPath: String) = withContext(Dispatchers.Default) {
            // 使用 GPU 加速加载模型
            model = LlamaNative.loadModel(
                modelPath = modelPath,
                nGpuLayers = 99,    // 将所有层卸载到 Vulkan
            )
            require(model != 0L) { "Failed to load model" }
    
            // 创建推理上下文
            ctx = LlamaNative.createContext(
                model = model,
                nCtx = 2048,        // 上下文窗口
                nThreads = 4,       // CPU 线程数
                nBatch = 512,       // 批次大小
            )
            require(ctx != 0L) { "Failed to create context" }
        }
    
        fun unload() {
            if (ctx != 0L) {
                LlamaNative.freeContext(ctx)
                ctx = 0
            }
            if (model != 0L) {
                LlamaNative.freeModel(model)
                model = 0
            }
        }
    }

    Vulkan GPU 加速

    Vulkan 是 Android 的 GPU 计算 API。llama.cpp 使用它来加速推理过程中的矩阵运算。通过将 nGpuLayers 设置为模型的层数来启用。

    Vulkan 支持取决于设备:

    • 骁龙 8 Gen 2+:完整 Vulkan 计算支持,性能最佳
    • Tensor G3/G4:良好的 Vulkan 支持
    • 骁龙 7 Gen 3+:支持 Vulkan,中等加速效果
    • 旧款/入门级设备:可能缺少 Vulkan 计算支持。自动回退到 CPU。

    运行时检查 Vulkan 可用性:

    fun isVulkanAvailable(): Boolean {
        return try {
            val vk = android.hardware.HardwareBuffer::class.java
            android.os.Build.VERSION.SDK_INT >= 26
            // 更可靠的方式:尝试创建 Vulkan 实例
        } catch (e: Exception) {
            false
        }
    }

    生成文本

    class LlamaEngine(private val context: Context) {
        // ... 上面的 loadModel 和 unload
    
        suspend fun generate(
            prompt: String,
            maxTokens: Int = 256,
            temperature: Float = 0.7f,
            onToken: (String) -> Unit = {}
        ): String = withContext(Dispatchers.Default) {
            val result = StringBuilder()
    
            LlamaNative.generate(
                context = ctx,
                prompt = prompt,
                maxTokens = maxTokens,
                temperature = temperature,
            ) { token ->
                result.append(token)
                // 分派到主线程更新 UI
                withContext(Dispatchers.Main) {
                    onToken(token)
                }
            }
    
            result.toString()
        }
    }

    ViewModel 集成

    class AiViewModel(application: Application) : AndroidViewModel(application) {
        private val engine = LlamaEngine(application)
        private val _response = MutableStateFlow("")
        val response: StateFlow<String> = _response
        private val _isGenerating = MutableStateFlow(false)
        val isGenerating: StateFlow<Boolean> = _isGenerating
    
        fun loadModel(path: String) {
            viewModelScope.launch {
                engine.loadModel(path)
            }
        }
    
        fun generate(prompt: String) {
            viewModelScope.launch {
                _isGenerating.value = true
                _response.value = ""
    
                engine.generate(prompt, maxTokens = 256) { token ->
                    _response.value += token
                }
    
                _isGenerating.value = false
            }
        }
    
        override fun onCleared() {
            engine.unload()
            super.onCleared()
        }
    }

    Jetpack Compose UI

    @Composable
    fun AiChatScreen(viewModel: AiViewModel = viewModel()) {
        val response by viewModel.response.collectAsState()
        val isGenerating by viewModel.isGenerating.collectAsState()
        var input by remember { mutableStateOf("") }
    
        Column(modifier = Modifier.fillMaxSize().padding(16.dp)) {
            // 回复区域
            Text(
                text = response,
                modifier = Modifier.weight(1f).verticalScroll(rememberScrollState())
            )
    
            // 输入区域
            Row(modifier = Modifier.fillMaxWidth()) {
                TextField(
                    value = input,
                    onValueChange = { input = it },
                    modifier = Modifier.weight(1f),
                    enabled = !isGenerating
                )
                Button(
                    onClick = {
                        viewModel.generate(input)
                        input = ""
                    },
                    enabled = !isGenerating
                ) {
                    Text("Send")
                }
            }
        }
    }

    内存管理

    Android 的内存管理非常激进。系统会杀死后台进程以释放 RAM,并在内存压力下终止您的应用。

    可用内存检查

    fun getAvailableMemoryMb(): Long {
        val memInfo = ActivityManager.MemoryInfo()
        val activityManager = getSystemService(ACTIVITY_SERVICE) as ActivityManager
        activityManager.getMemoryInfo(memInfo)
        return memInfo.availMem / (1024 * 1024)
    }
    
    fun canLoadModel(modelSizeMb: Long): Boolean {
        val available = getAvailableMemoryMb()
        // 为应用和系统预留 500MB
        return available > modelSizeMb + 500
    }

    生命周期管理

    class AiService : LifecycleObserver {
        private var engine: LlamaEngine? = null
    
        @OnLifecycleEvent(Lifecycle.Event.ON_RESUME)
        fun onResume() {
            // 如果 AI 界面处于活动状态,考虑加载模型
        }
    
        @OnLifecycleEvent(Lifecycle.Event.ON_PAUSE)
        fun onPause() {
            // 卸载模型以释放内存
            engine?.unload()
        }
    }

    onTrimMemory 处理

    override fun onTrimMemory(level: Int) {
        super.onTrimMemory(level)
        if (level >= ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW) {
            engine?.unload()
        }
    }

    模型分发

    Asset Delivery(Play Feature Delivery)

    对于超过 150MB 的模型,使用 Play Asset Delivery 以避免 APK 大小限制:

    // build.gradle.kts
    assetPacks += ":model_pack"
    
    // model_pack/build.gradle.kts
    plugins {
        id("com.android.asset-pack")
    }
    assetPack {
        packName.set("model_pack")
        dynamicDelivery {
            deliveryType.set("install-time") // 或 "fast-follow" 或 "on-demand"
        }
    }

    安装后下载

    安装后下载模型:

    suspend fun downloadModel(url: String, destination: File): Boolean {
        return withContext(Dispatchers.IO) {
            val client = OkHttpClient()
            val request = Request.Builder().url(url).build()
            val response = client.newCall(request).execute()
    
            response.body?.let { body ->
                val totalBytes = body.contentLength()
                destination.outputStream().use { output ->
                    body.byteStream().use { input ->
                        val buffer = ByteArray(8192)
                        var bytesRead: Long = 0
                        var read: Int
    
                        while (input.read(buffer).also { read = it } != -1) {
                            output.write(buffer, 0, read)
                            bytesRead += read
                            val progress = bytesRead.toFloat() / totalBytes
                            // 更新进度 UI
                        }
                    }
                }
                true
            } ?: false
        }
    }

    对于可以在应用重启后继续的后台下载,使用 WorkManager

    跨设备测试

    Android 设备种类繁多。需要在以下设备上测试:

    1. 旗舰机(SD 8 Gen 3, 12GB):验证最佳性能
    2. 中端机(SD 7 Gen 3, 6-8GB):验证 1B 模型流畅运行
    3. 入门机(SD 6 Gen 3, 4GB):验证模型无法加载时的优雅降级
    4. 旧款旗舰(SD 8 Gen 1, 8GB):验证两年前的旗舰机能正常工作

    使用 Firebase Test Lab 或 BrowserStack 进行设备覆盖测试,无需拥有每台设备。

    生产清单

    1. 模型在最低配置设备上正确加载并生成
    2. 检测到 Vulkan 时使用 Vulkan 加速
    3. Vulkan 不可用时 CPU 回退正常工作
    4. 内存检查防止在低 RAM 设备上加载
    5. 在 onPause/onTrimMemory 时卸载模型
    6. 安装后下载的进度正确显示
    7. 下载后验证模型完整性(SHA256)
    8. 用户可以取消生成
    9. 模型不可用时应用正常运行

    GGUF 模型决定了输出质量。在您的领域数据上微调的模型(通过 Ertas 等平台)会生成针对您应用目的的特定回复。无论使用哪个模型,llama.cpp 的集成方式都是一样的。

    就本文向 AI 提问

    Ship AI that runs on your users' devices.

    Free plan with 30 credits/mo, no card required. Paid plans from $10/mo USD.

    Keep reading