以下文章來(lái)源于Android高效開(kāi)發(fā),作者2BAB
作者 / Android 谷歌開(kāi)發(fā)者專(zhuān)家 El Zhang (2BAB)
在今年的廈門(mén)和廣州 Google I/O Extended 上,我分享了《On-Device Model 集成 (KMP) 與用例》。本文是當(dāng)時(shí) Demo 的深入細(xì)節(jié)分析,同時(shí)也是后面幾篇同類(lèi)型文章的開(kāi)頭。通過(guò)本文你將了解到:
移植 Mediapipe 的 LLM Inference Android 官方 Demo 到 KMP,支持在 iOS 上運(yùn)行。
KMP 兩種常見(jiàn)的調(diào)用 iOS SDK 的方式:
Kotlin 直接調(diào)用 Cocoapods 引入的第三方庫(kù)。
Kotlin 通過(guò) iOS 工程調(diào)用第三方庫(kù)。
KMP 與多平臺(tái)依賴(lài)注入時(shí)的小技巧 (基于 Koin)。
On-Device Model 與 LLM 模型 Gemma 1.1 2B 的簡(jiǎn)單背景。
On-Device Model 本地模型
大語(yǔ)言模型 (LLM) 持續(xù)火熱了很長(zhǎng)一段時(shí)間,而今年開(kāi)始這股風(fēng)正式吹到了移動(dòng)端,包括 Google 在內(nèi)的最新手機(jī)與系統(tǒng)均深度集成了此類(lèi) On-Device Model 的相關(guān)功能。對(duì)于 Google 目前的公開(kāi)戰(zhàn)略中,On-Device Model 這塊的大語(yǔ)言模型主要分為兩個(gè):
Gemini Nano: 非開(kāi)源,支持機(jī)型較少 (某些機(jī)型支持特定芯片加速如 Tensor G4),具有強(qiáng)勁的表現(xiàn)。目前可以在桌面平臺(tái) (Chrome) 和部分 Android 手機(jī)上使用 (Pixel 8/9 Samsung 和小米部分機(jī)型)。據(jù)報(bào)道晚些時(shí)候會(huì)公開(kāi)給更多的開(kāi)發(fā)者進(jìn)行使用和測(cè)試。
Gemma: 開(kāi)源,支持所有滿(mǎn)足最低要求的機(jī)型,同樣有不俗的性能表現(xiàn),與 Nano 使用類(lèi)似的技術(shù)路線(xiàn)進(jìn)行訓(xùn)練。目前可以在多平臺(tái)上體驗(yàn) (Android/iOS/Desktop)。
目前多數(shù)移動(dòng)端開(kāi)發(fā)者尚無(wú)法直接基于 Gemini Nano 開(kāi)發(fā),所以今天的主角便是 Gemma 1 的 2B 版本。想在移動(dòng)平臺(tái)上直接使用 Gemma,Google 已給我們提供一個(gè)開(kāi)箱即用的工具: Mediapipe。MediaPipe 是一個(gè)跨平臺(tái)的框架,它封裝了一系列預(yù)構(gòu)建的 On-Device 機(jī)器學(xué)習(xí)模型和工具,支持實(shí)時(shí)的手勢(shì)識(shí)別、面部檢測(cè)、姿態(tài)估計(jì)等任務(wù),還可應(yīng)用于生成圖片、聊天機(jī)器人等各種應(yīng)用場(chǎng)景。感興趣的朋友可以試玩它的 Web 版 Demo,以及相關(guān)文檔。

而其中的 LLM Inference API (上表第一行),用于運(yùn)行大語(yǔ)言模型推理的組件,支持 Gemma 2B/7B,Phi-2,F(xiàn)alcon-RW-1B,StableLM-3B 等模型。針對(duì) Gemma 的預(yù)轉(zhuǎn)換模型 (基于 TensorFlow Lite) 可在 Kaggle 下載,并在稍后直接放入 Mediapipe 中加載。

LLM Inference
Android Sample
Mediapipe 官方的 LLM Inference Demo 包含了 Android/iOS/Web 前端等平臺(tái)。

打開(kāi) Android 倉(cāng)庫(kù)會(huì)發(fā)現(xiàn)幾個(gè)特點(diǎn):
純 Kotlin 實(shí)現(xiàn)。
UI 是純 Jetpack Compose 實(shí)現(xiàn)。
依賴(lài)的 LLM Task SDK 已經(jīng)高度封裝,暴露出來(lái)的方法僅 3 個(gè)。
再查看 iOS 的版本:
UI 是 SwiftUI 實(shí)現(xiàn),做的事情和 Compose 一模一樣,稍微再簡(jiǎn)化掉一些元素 (例如 Topbar 和發(fā)送按鈕)。
依賴(lài)的 LLM Task SDK 已經(jīng)高度封裝,暴露出來(lái)的方法一樣為 3 個(gè)。
所以,一個(gè)好玩的想法出現(xiàn)了:Android 版本的這個(gè) Demo 具備移植到 iOS 上的基礎(chǔ);移植可使兩邊的代碼高度高度一致,大幅縮減維護(hù)成本,而核心要實(shí)現(xiàn)的僅僅是橋接下 iOS 上的 LLM Inference SDK。
Kotlin Multiplatform
移植工程所使用的技術(shù)叫做 Kotlin Multiplatform (縮寫(xiě)為 KMP),它是 Kotlin 團(tuán)隊(duì)開(kāi)發(fā)的一種支持跨平臺(tái)開(kāi)發(fā)的技術(shù),允許開(kāi)發(fā)者使用相同的代碼庫(kù)來(lái)構(gòu)建 Android、iOS、Web 等多個(gè)平臺(tái)的應(yīng)用程序。通過(guò)共享業(yè)務(wù)邏輯代碼,KMP 能顯著減少開(kāi)發(fā)時(shí)間和維護(hù)成本,同時(shí)盡量保留每個(gè)平臺(tái)的原生性能和體驗(yàn)。Google 在今年的 I/O 大會(huì)上也宣布對(duì) KMP 提供一等的支持,把一些 Android 平臺(tái)上的庫(kù)和工具遷移到了多平臺(tái),KMP 的開(kāi)發(fā)者可以方便的使用它到 iOS 等其他平臺(tái)。
盡管 Mediapipe 也支持多個(gè)平臺(tái),但我們這次主要聚焦在 Android 和 iOS。一方面更貼近現(xiàn)實(shí),各行各業(yè)使用 KMP 的公司的用例更多在移動(dòng)端上;另外一方面也更方便對(duì)標(biāo)其他移動(dòng)端開(kāi)發(fā)技術(shù)棧。
移植流程
初始化
使用 IDEA 或 Android Studio 創(chuàng)建一個(gè) KMP 的基礎(chǔ)工程,你可以借助 KMP Wizard 或者第三方 KMP App 的模版。如果你沒(méi)有 KMP 的相關(guān)經(jīng)驗(yàn),可以看到它其實(shí)就是一個(gè)非常類(lèi)似 Android 工程的結(jié)構(gòu),只不過(guò)這一次我們把 iOS 的殼工程也放到根目錄,并且在 app 模塊的 build.gradle.kts 內(nèi)同時(shí)配置了 iOS 的相關(guān)依賴(lài)。

封裝和調(diào)用 LLM Inference
我們?cè)?commonMain 中,根據(jù) Mediapipe LLM Task SDK 的特征抽象一個(gè)簡(jiǎn)單的接口,使用 Kotlin 編寫(xiě),用以滿(mǎn)足 Android 和 iOS 兩端的需要。該接口取代了原有倉(cāng)庫(kù)里的 InferenceModel.kt 類(lèi)。
// app/src/commonMain/.../llm/LLMOperator
interface LLMOperator {
/**
* To load the model into current context.
* @return 1. null if it went well 2. an error message in string
*/
suspend fun initModel(): String?
fun sizeInTokens(text: String): Int
suspend fun generateResponse(inputText: String): String
suspend fun generateResponseAsync(inputText: String): Flow>
}
在 Android 上面,因?yàn)?LLM Task SDK 原先就是 Kotlin 實(shí)現(xiàn)的,所以除了初始化加載模型文件,其余的部分基本就是代理原有的 SDK 功能。
class LLMInferenceAndroidImpl(private val ctx: Context): LLMOperator {
private lateinit var llmInference: LlmInference
private val initialized = AtomicBoolean(false)
private val partialResultsFlow = MutableSharedFlow>(...)
override suspend fun initModel(): String? {
if (initialized.get()) {
return null
}
return try {
val modelPath = ...
if (File(modelPath).exists().not()) {
return "Model not found at path: $modelPath"
}
loadModel(modelPath)
initialized.set(true)
null
} catch (e: Exception) {
e.message
}
}
private fun loadModel(modelPath: String) {
val options = LlmInference.LlmInferenceOptions.builder()
.setModelPath(modelPath)
.setMaxTokens(1024)
.setResultListener { partialResult, done ->
// Transforming the listener to flow,
// making it easy on UI integration.
partialResultsFlow.tryEmit(partialResult to done)
}
.build()
llmInference = LlmInference.createFromOptions(ctx, options)
}
override fun sizeInTokens(text: String): Int = llmInference.sizeInTokens(text)
override suspend fun generateResponse(inputText: String): String {
...
return llmInference.generateResponse(inputText)
}
override suspend fun generateResponseAsync(inputText: String): Flow> {
...
llmInference.generateResponseAsync(inputText)
return partialResultsFlow.asSharedFlow()
}
}
而針對(duì) iOS,我們先嘗試第一種調(diào)用方式:直接調(diào)用 Cocoapods 引入的庫(kù)。在 app 模塊引入 cocoapods 的插件,同時(shí)添加 Mediapipe 的 LLM Task 庫(kù):
// app/build.gradle.kts
plugins {
...
alias(libs.plugins.cocoapods)
}
cocoapods {
...
ios.deploymentTarget = "15"
pod("MediaPipeTasksGenAIC") {
version = "0.10.14"
extraOpts += listOf("-compiler-option", "-fmodules")
}
pod("MediaPipeTasksGenAI") {
version = "0.10.14"
extraOpts += listOf("-compiler-option", "-fmodules")
}
}
注意上面的引入配置中要添加一個(gè)編譯參數(shù)為 -fmodules 才可正常生成 Kotlin 的引用 (參考鏈接)。
一些 Objective-C 庫(kù),尤其是那些作為 Swift 庫(kù)包裝器的庫(kù),在它們的頭文件中使用了 @import 指令。默認(rèn)情況下,cinterop 不支持這些指令。要啟用對(duì) @import 指令的支持,可以在 pod() 函數(shù)的配置塊中指定 -fmodules 選項(xiàng)。
之后,我們?cè)?iosMain 中便可直接 import 相關(guān)的庫(kù)代碼,如法炮制 Android 端的代理思路:
// 注意這些 import 是 cocoapods 開(kāi)頭的
import cocoapods.MediaPipeTasksGenAI.MPPLLMInference
import cocoapods.MediaPipeTasksGenAI.MPPLLMInferenceOptions
import platform.Foundation.NSBundle
...
class LLMOperatorIOSImpl: LLMOperator {
private val inference: MPPLLMInference
init {
val modelPath = NSBundle.mainBundle.pathForResource(..., "bin")
val options = MPPLLMInferenceOptions(modelPath!!)
options.setModelPath(modelPath!!)
options.setMaxTokens(2048)
options.setTopk(40)
options.setTemperature(0.8f)
options.setRandomSeed(102)
// NPE was thrown here right after it printed the success initialization message internally.
inference = MPPLLMInference(options, null)
}
override fun generateResponse(inputText: String): String {...}
override fun generateResponseAsync(inputText: String, ...) :... {
...
}
...
}
但這回我們沒(méi)那么幸運(yùn),MPPLLMInference 初始化結(jié)束的一瞬間有 NPE 拋出。最可能的問(wèn)題是因?yàn)?Kotlin 現(xiàn)在 interop 的目標(biāo)是 Objective-C,MPPLLMInference 的構(gòu)造器比 Swift 版本多一個(gè) error 參數(shù),而我們傳入的是 null。
constructor( options: cocoapods.MediaPipeTasksGenAI.MPPLLMInferenceOptions, error:CPointer>?)
但幾番測(cè)試各種指針傳入,也并未解決這個(gè)問(wèn)題:
// 其中一種嘗試 memScoped { val pp: CPointerVar> = allocPointerTo() val inference = MPPLLMInference(options, pp.value) Napier.i(pp.value.toString()) }
于是只能另辟蹊徑采用第二種方案: 通過(guò) iOS 工程調(diào)用第三方庫(kù)。
// 1. 聲明一個(gè)類(lèi)似 LLMOperator 的接口但更簡(jiǎn)單,方便適配 iOS 的 SDK。
// app/src/iosMain/.../llm/LLMOperator.kt
interface LLMOperatorSwift {
suspend fun loadModel(modelName: String)
fun sizeInTokens(text: String): Int
suspend fun generateResponse(inputText: String): String
suspend fun generateResponseAsync(
inputText: String,
progress: (partialResponse: String) -> Unit,
completion: (completeResponse: String) -> Unit
)
}
// 2. 在 iOS 工程里實(shí)現(xiàn)這個(gè)接口
// iosApp/iosApp/LLMInferenceDelegate.swift
class LLMOperatorSwiftImpl: LLMOperatorSwift {
...
var llmInference: LlmInference?
func loadModel(modelName: String) async throws {
let path = Bundle.main.path(forResource: modelName, ofType: "bin")!
let llmOptions = LlmInference.Options(modelPath: path)
llmOptions.maxTokens = 4096
llmOptions.temperature = 0.9
llmInference = try LlmInference(options: llmOptions)
}
func generateResponse(inputText: String) async throws -> String {
return try llmInference!.generateResponse(inputText: inputText)
}
func generateResponseAsync(inputText: String, progress: @escaping (String) -> Void, completion: @escaping (String) -> Void) async throws {
try llmInference!.generateResponseAsync(inputText: inputText) { partialResponse, error in
// progress
if let e = error {
print("(self.errorTag) (e)")
completion(e.localizedDescription)
return
}
if let partial = partialResponse {
progress(partial)
}
} completion: {
completion("")
}
}
...
}
// 3. iOS 再把代理好的(重點(diǎn)是初始化)類(lèi)傳回給 Kotlin
// iosApp/iosApp/iosApp.swift
class AppDelegate: UIResponder, UIApplicationDelegate {
...
func application(){
...
let delegate = try LLMOperatorSwiftImpl()
MainKt.onStartup(llmInferenceDelegate: delegate)
}
}
// 4. 最初 iOS 在 KMP 上的實(shí)現(xiàn)細(xì)節(jié)直接代理給該對(duì)象(通過(guò)構(gòu)造器注入)
class LLMOperatorIOSImpl(
private val delegate: LLMOperatorSwift) : LLMOperator {
...
}
細(xì)心的朋友可能已經(jīng)發(fā)現(xiàn),兩端的 Impl 實(shí)例需要不同的構(gòu)造器參數(shù),這個(gè)需求一般使用 KMP 的 expect 與 actual 關(guān)鍵字解決。下面的代碼中:
利用了 expect class 不需要構(gòu)造器參數(shù)聲明的特點(diǎn)加了層封裝 (類(lèi)似接口)。
利用了 Koin 實(shí)現(xiàn)各自平臺(tái)所需參數(shù)的注入,再統(tǒng)一把創(chuàng)建的接口實(shí)例注入到 Common 層所需的地方。
// Common
expect class LLMOperatorFactory {
fun create(): LLMOperator
}
val sharedModule = module {
// 從不同的 LLMOperatorFactory 創(chuàng)建出 Common 層所需的 LLMOperator
single { get().create() }
}
// Android
actual class LLMOperatorFactory(private val context: Context){
actual fun create(): LLMOperator = LLMInferenceAndroidImpl(context)
}
val androidModule = module {
// Android 注入 App 的 Context
single { LLMOperatorFactory(androidContext()) }
}
// iOS
actual class LLMOperatorFactory(private val llmInferenceDelegate: LLMOperatorSwift) {
actual fun create(): LLMOperator = LLMOperatorIOSImpl(llmInferenceDelegate)
}
module {
// iOS 注入 onStartup 函數(shù)傳入的 delegate
single { LLMOperatorFactory(llmInferenceDelegate) }
}
小結(jié): 我們通過(guò)一個(gè)小小的案例,領(lǐng)略到了 Kotlin 和 Swift 的深度交互。還借助 expect/actual 關(guān)鍵字與 Koin 的依賴(lài)注入,讓整體方案更流暢和自動(dòng)化,達(dá)到了在 KMP 的 Common 模塊調(diào)用 Android 和 iOS Native SDK 的目標(biāo)。
移植 UI 和 ViewModel
原項(xiàng)目里的 InferenceMode 已經(jīng)被上一節(jié)的 LLMOperator 所取代,因此我們拷貝除 Activity 的剩下 5 個(gè)類(lèi):

下面我們修改幾處代碼使 Jetpack Compose 的代碼可以方便的遷移到 Compose Multiplatform。
首先是外圍的 ViewModel,KMP 版本我在這里使用了 Voyage,因此替換為 ScreenModel。不過(guò)官方 ViewModel 的方案也在實(shí)驗(yàn)中了,請(qǐng)參考這個(gè)文檔。
// Android 版本
class ChatViewModel(
private val inferenceModel: InferenceModel
) : ViewModel() {...}
// KMP 版本,轉(zhuǎn)換 ViewModel 為 ScreenModel,并修改傳入對(duì)象
class ChatViewModel(
private val llmOperator: LLMOperator
):ScreenModel{...}
Voyage https://github.com/adrielcafe/voyager
文檔 https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-viewmodel.html
相應(yīng)的 ViewModel 初始化方式也更改成 ScreenModel 的方法:
// Android 版本
@Composable
internal fun ChatRoute(
chatViewModel: ChatViewModel = viewModel(
factory = ChatViewModel.getFactory(LocalContext.current.applicationContext)
)
) {
...
ChatScreen(...) {...}
}
// KMP 版本,改成外部初始化后傳入
@Composable
internal fun ChatRoute(
chatViewModel: ChatViewModel
) {
// 此處采用了默認(rèn)參數(shù)注入的方案,便于解耦。
// koinInject() 是 Koin 官方提供的針對(duì) Compose
// 的 @Composable 函數(shù)注入的一個(gè)方法。
@Composable
fun AiScreen(llmOperator:LLMOperator = koinInject()) {
// 使用 ScreenModel 的 remember 方法
val chatViewModel = rememberScreenModel { ChatViewModel(llmOperator) }
...
Column {
...
Box(...) {
if (showLoading) {
...
} else {
ChatRoute(chatViewModel)
}
}
}
}
對(duì)應(yīng)的 ViewModel 內(nèi)部的 LLM 功能調(diào)用接口也要進(jìn)行替換:
// Android 版本
inferenceModel.generateResponseAsync(fullPrompt)
inferenceModel.partialResults
.collectIndexed { index, (partialResult, done) ->
...
}
// KMP 版本,把 Flow 的返回前置了,兼容了兩個(gè)平臺(tái)的 SDK 設(shè)計(jì)
llmOperator.generateResponseAsync(fullPrompt)
.collectIndexed { index, (partialResult, done) ->
...
}
然后是 Compose Multiplatform 特定的資源加載方式,把 R 文件替換為 Res:
// Android 版本 Text(stringResource(R.string.chat_label)) // KMP 版本,該引用是使用插件從 xml 映射而來(lái) // (commonMain/composeResources/values/strings.xml) import mediapiper.app.generated.resources.chat_label ... Text(stringResource(Res.string.chat_label))
至此我們已經(jīng)完成了 ChatScreen ChatViewModel 的主頁(yè)面功能遷移。
最后是其他的幾個(gè)輕微改動(dòng):
LoadingScreen 我們?nèi)绶ㄅ谥苽魅?LLMOperator 進(jìn)行初始化 (替換原有 InferenceModel)。
ChatMessage 只需修改了 UUID 調(diào)用的一行 API 到原生實(shí)現(xiàn) (Kotlin 2.0.20 后就不需要了)。
ChatUiState 則完全不用動(dòng)。
剩下的就只有整體修改下 Log 庫(kù)的引用等小細(xì)節(jié)。
小結(jié): 倘若略去 Log、R 文件的引用替換以及 import 替換等,核心的修改其實(shí)僅十幾行,便能把整個(gè) UI 部分也跑起來(lái)了。
簡(jiǎn)單測(cè)試
那 Gemma 2B 的性能如何,我們看幾個(gè)簡(jiǎn)單的例子。此處主要使用三個(gè)版本的模型進(jìn)行測(cè)試,模型的定義在 me.xx2bab.mediapiper.llm.LLMOperator (模型在兩端部署請(qǐng)參考項(xiàng)目 README)。
gemma-2b-it-gpu-int4
gemma-2b-it-cpu-int4
gemma-2b-it-cpu-int8
其中:
it 指代一種變體,即 Instruction Tuned 模型,更適合聊天用途,因?yàn)樗鼈兘?jīng)過(guò)微調(diào)能更好地理解指令,并生成更準(zhǔn)確的回答。
int4/8 指代模型量化,即將模型中的浮點(diǎn)數(shù)轉(zhuǎn)換為低精度整數(shù),從而減小模型的大小和計(jì)算量以適配小型的本地設(shè)備例如手機(jī)。當(dāng)然,模型的精度和回答準(zhǔn)確度也會(huì)有一些下降。
CPU 和 GPU指針對(duì)的硬件平臺(tái),這方便了設(shè)備 GPU 較弱甚至沒(méi)有時(shí)可選擇 CPU 執(zhí)行。從下面的測(cè)試結(jié)果你會(huì)發(fā)現(xiàn)當(dāng)前移動(dòng)設(shè)備上 CPU 版本也常常會(huì)占優(yōu),因?yàn)槟P鸵?guī)模小、簡(jiǎn)單對(duì)話(huà)計(jì)算操作也不大,并且 Int 量化也有利于 CPU 的指令執(zhí)行。
首先我們測(cè)試一個(gè)簡(jiǎn)單的邏輯: "蘆筍是不是一種動(dòng)物"?可以看到下圖的 CPU 版本答案比兩個(gè) GPU (iOS 和 Android) 更合理。而下一個(gè)測(cè)試是翻譯答案為中文,則是三個(gè)嘗試都不太行。

接著我們提高了測(cè)試問(wèn)題的難度,讓它執(zhí)行區(qū)分動(dòng)植物的單詞分類(lèi): 不管是 GPU 或者 CPU 的版本都不錯(cuò)。

再次升級(jí)上個(gè)問(wèn)題,讓它用 JSON 的方式輸出答案,就出現(xiàn)明顯的問(wèn)題:
圖 1 沒(méi)有輸出完整的代碼片段,缺少了結(jié)尾的三個(gè)點(diǎn) ```。
圖二分類(lèi)錯(cuò)誤,把山竹放到動(dòng)物,植物出現(xiàn)了兩次向日葵。
圖三同二的錯(cuò)誤,但這三次都沒(méi)有純輸出一個(gè) JSON,實(shí)際上還是不夠嚴(yán)格執(zhí)行作為 JSON Responder 的角色。

最后,這其實(shí)不是極限,如果我們使用 cpu-int8 的版本,則可以高準(zhǔn)確率地解答上面問(wèn)題。以及,如果把本 Demo 的 iOS 入口代碼發(fā)送給它分析,也能答的不錯(cuò)。

Gemma 1 的 2B 版本測(cè)試至此,我們發(fā)覺(jué)其推理效果還有不少進(jìn)步空間,勝在回復(fù)速度不錯(cuò)。而事實(shí)上 Gemma 2 的 2B 版本前不久已推出,并且據(jù)官方測(cè)試其綜合水平已超過(guò) GPT 3.5。這意味著在一臺(tái)小小的手機(jī)里,本地的推理已經(jīng)可以達(dá)到一年半前的主流模型效果??偨Y(jié)實(shí)現(xiàn)這個(gè)本地聊天 Demo 的遷移和測(cè)試,給了我們些一手的經(jīng)驗(yàn):
LLM 的 On-Device Model 發(fā)展非常迅速,而借助 Google 的一系列基礎(chǔ)設(shè)施可以讓第三方 Mobile App 開(kāi)發(fā)者也迅速地集成相關(guān)的功能,并跨越 Android 與 iOS 雙平臺(tái)。
觀望目前情況綜合判斷,LLM 的 On-Device Model 有望在今年達(dá)到初步可用狀態(tài),推理速度已經(jīng)不錯(cuò),準(zhǔn)確度還有待進(jìn)一步測(cè)試 (例如 Gemma 2 的 2B 版本 + Mediapipe)。
遵循 Android 團(tuán)隊(duì)目前的策略 "Kotlin First"并大膽使用 Compose,是頗具前景的——在基礎(chǔ)設(shè)施完備的情況下,一個(gè)聊天的小模塊僅寥寥數(shù)行修改即可遷移到 iOS。
-
Google
+關(guān)注
關(guān)注
5文章
1812瀏覽量
60617 -
移植
+關(guān)注
關(guān)注
1文章
417瀏覽量
29518 -
開(kāi)源
+關(guān)注
關(guān)注
3文章
4306瀏覽量
46398 -
iOS
+關(guān)注
關(guān)注
8文章
3401瀏覽量
155642 -
LLM
+關(guān)注
關(guān)注
1文章
350瀏覽量
1385
原文標(biāo)題:【GDE 分享】移植 Mediapipe LLM Demo 到 Kotlin Multiplatform
文章出處:【微信號(hào):Google_Developers,微信公眾號(hào):谷歌開(kāi)發(fā)者】歡迎添加關(guān)注!文章轉(zhuǎn)載請(qǐng)注明出處。
發(fā)布評(píng)論請(qǐng)先 登錄
無(wú)法在OVMS上運(yùn)行來(lái)自Meta的大型語(yǔ)言模型 (LLM),為什么?
怎樣去使用MediaPipe的helloworld example呢
求助,鴻蒙移植kotlin代碼,需要將其轉(zhuǎn)換成java實(shí)現(xiàn)嗎?
求助,官方出的MESH DEMO怎么改成了Kotlin和JAVA混和了?
分析Kotlin和Java EE的關(guān)系
Kotlin的概述
使用Kotlin替代Java重構(gòu)AOSP應(yīng)用
bilisoleil-kotlin Kotlin版仿B站項(xiàng)目
將其Android應(yīng)用的Java代碼遷移到Kotlin
使用Mediapipe控制Gripper
Kotlin發(fā)布2023年路線(xiàn)圖:K2編譯器、完善教程文檔等
Kotlin的語(yǔ)法糖解析
Kotlin聲明式UI框架Compose Multiplatform支持iOS
由Java改為 Kotlin過(guò)程中遇到的坑
詳解Object Detection Demo的移植
移植Mediapipe LLM Demo到Kotlin Multiplatform
評(píng)論