AndroidのローカルAIで読書ブログ投稿アプリを作ることにした 02

2026年9月10日木曜日

Androidアプリ プログラミング

t f B! P L

Pixel 10 Pro FoldのTPUでGemma 4を動かす ― Tensor G5向けモデルを自作Androidアプリから使う

前回は、本の感想をBloggerへ投稿する専用Androidアプリを作ろうとしたところから始まり、ChatGPTへの共有、Gemini Nano、Google AI Edge Galleryと調べていくうちに、最終的にはGemma 4をLiteRT-LMから直接利用する方針になった。

そして最後に決めたのが、Java版とは別にKotlinの新しいプロジェクトを作り、まずはGemmaを自作アプリから直接動かしてみることだった。

今回はその続きである。

Gemma 4を動かすだけでなく、Pixel 10 Pro Foldに搭載されているGoogle Tensor G5のTPUを使って、Tensor G5専用モデルから実際に日本語を生成するところまで確認できた。

前回の記事より少し技術寄りになるが、今回は「動いた」という結果だけではなく、実際にAndroid Studioでどのような構成を作り、どのファイルを変更し、どのコードを書いたのかまでまとめておく。

前回の最後に決めたKotlin版を実際に作った

前回の記事では、GemmaをLiteRT-LMから直接利用するため、新しいKotlinプロジェクトを作るところで終わっていた。

そこでまずは、本来作りたい読書ブログ画面には手を付けず、モデルを選択して文章を生成するだけの小さな動作確認アプリを作った。

モデルファイルを選択

アプリ内部へコピー

LiteRT-LMでモデルを初期化

「ローカルAIで生成」

日本語の生成結果を表示
前回、「次はKotlin版」ってところで終わったよね。
まずは本番画面を作らず、LiteRT-LMだけ動かすテスト版を作る話だったな。
作った。
モデル選んで、「ローカルAIで生成」を押すだけ。
それでいい。
まずAI部分だけ動くことを確認した方が原因を切り分けやすい。
GPU版Gemmaは動いた。
でも本命はTensor G5のTPU。
いよいよそっちだな。

今回実際にやった作業を先にまとめる

細かい説明へ入る前に、実際に行った作業を順番に並べると次のようになる。

  1. Kotlin + XML ViewsのAndroidプロジェクトを作る
  2. LiteRT-LMを呼び出すテスト画面を作る
  3. モデルファイルを選択してアプリ内部へ保存できるようにする
  4. Google Tensor G5用のDynamic Featureモジュールを作る
  5. Tensor用のlibLiteRtDispatch_GoogleTensor.soをDynamic Featureへ配置する
  6. AndroidManifest.xmlへNPUとTensor用native libraryの宣言を追加する
  7. LiteRT-LMのKotlin APIとnative runtimeの世代を揃える
  8. Tensor G5モデルではBackend.NPUを選択する
  9. AABを作り、bundletoolでDynamic Feature込みのAPKセットを作る
  10. Pixel 10 Pro Foldへインストールする
  11. 旧Tensor G5モデルを選択して初期化する
  12. プロンプトを送り、日本語文章の生成を確認する
こうして並べると、「NPUに変更して終わり」では全然ないね。
Kotlinのコード変更自体は少ない。
むしろAndroid側のnative runtime構成を作る作業の方が大きい。

まずは普通のKotlinアプリとしてモデル選択画面を作る

最初のテスト画面は非常に単純なものにした。

  • モデル選択ボタン
  • 現在読み込んでいるモデル名
  • 実行バックエンド表示
  • プロンプト入力欄
  • 生成ボタン
  • 生成結果表示欄

モデルファイルはAndroidのファイル選択画面から選べるようにした。

例えばMainActivity側では、次のような形でファイルピッカーを呼び出せる。

private val modelPicker =
    registerForActivityResult(
        ActivityResultContracts.OpenDocument()
    ) { uri ->

        if (uri != null) {
            importModel(uri)
        }
    }

private fun selectModel() {
    modelPicker.launch(arrayOf("*/*"))
}

実際のアプリでは.litertlmモデルを選択し、それをそのまま外部ストレージから利用するのではなく、アプリ内部のmodelsディレクトリへコピーした。

簡略化したサンプルなら次のようになる。

private fun copyModelToAppStorage(
    uri: Uri,
    fileName: String
): File {

    val modelDir = File(filesDir, "models")
    modelDir.mkdirs()

    val destination = File(modelDir, fileName)

    contentResolver.openInputStream(uri).use { input ->
        requireNotNull(input)

        destination.outputStream().use { output ->
            input.copyTo(output)
        }
    }

    return destination
}
モデルって数GBあるけど、毎回ファイル選択するの?
最初だけ。
一度選んだモデルをアプリ専用領域へコピーして、そのパスを保存しておけばいい。
次回からそのモデルを使えるわけね。

GPU用モデルとTensor G5用モデルは実行方法が違う

GPUモデルであれば、LiteRT-LMのバックエンドとしてGPUを指定する。

val backend = Backend.GPU()

一方、Google Tensor G5向けモデルの場合はNPUバックエンドを指定する。

val backend = Backend.NPU(
    nativeLibraryDir = applicationInfo.nativeLibraryDir
)

Google Tensorでは一般的にTPUという呼び方をするが、LiteRT-LMのAPI上ではBackend.NPUという名前になっている。

そのためテストアプリでは、

実行バックエンド: TPU / NPU (Google Tensor G5)

と表示するようにした。

モデル名からTensor G5向けかを判定する簡単な例なら、次のように書ける。

private fun selectBackend(modelFile: File): Backend {

    val name = modelFile.name.lowercase()

    return if (name.contains("google_tensor_g5")) {

        Backend.NPU(
            nativeLibraryDir = applicationInfo.nativeLibraryDir
        )

    } else {

        Backend.GPU()
    }
}
TPUを使うのにコードではNPUなんだ。
LiteRT-LM側のバックエンド名がNPUなんだ。
その先でGoogle Tensor用runtimeへ接続する。

ただしBackend.NPUを指定するだけでは動かない

ここがGPU版と大きく違うところだった。

Tensor G5では、

Backend.NPU(...)

と書くだけではなく、Google Tensor向けのnative runtimeをアプリ側に用意する必要がある。

Androidアプリ

LiteRT-LM

Google Tensor Dispatch Runtime

Google Tensor G5 TPU / NPU

中心になるのが次のnative libraryである。

libLiteRtDispatch_GoogleTensor.so

実際の作業1 ― Tensor Runtime用のモジュールを追加する

今回の構成では、Tensor用runtimeを通常のappモジュールへ直接入れず、Dynamic Featureとして分離した。

サンプルとして、次のようなプロジェクト構成を考える。

TensorLocalAiSample
│
├─ app
│
└─ tensor_runtime
    ├─ runtime_strings
    └─ google_tensor_runtime

settings.gradle.ktsへモジュールを追加する。

include(":app")

include(":tensor_runtime:runtime_strings")
include(":tensor_runtime:google_tensor_runtime")

次にapp側のbuild.gradle.ktsへDynamic Featureを登録する。

android {

    namespace = "com.example.tensorlocalai"

    defaultConfig {
        applicationId = "com.example.tensorlocalai"
        minSdk = 32
    }

    dynamicFeatures += setOf(
        ":tensor_runtime:google_tensor_runtime"
    )
}

ここで使っているcom.example.tensorlocalaiは記事用のサンプルパッケージ名である。

実際の作業2 ― Dynamic Feature側を作る

google_tensor_runtime側は、コードを持つ画面ではなくTensor用native libraryを運ぶためのモジュールとして作った。

google_tensor_runtime/build.gradle.ktsの基本形は次のようになる。

plugins {
    id("com.android.dynamic-feature")
}

android {
    namespace = "com.example.tensorlocalai.tensorruntime"

    compileSdk = 36

    defaultConfig {
        minSdk = 32
    }

    sourceSets {
        getByName("main") {
            jniLibs.srcDirs("src/main/jni")
        }
    }
}

dependencies {
    implementation(project(":app"))
}

そしてnative libraryを次の場所へ配置する。

tensor_runtime/
└─ google_tensor_runtime/
   └─ src/
      └─ main/
         └─ jni/
            └─ arm64-v8a/
               └─ libLiteRtDispatch_GoogleTensor.so
普通のapp/src/main/jniLibsじゃないの?
今回はTensor runtimeをDynamic Featureに分離した。
だから専用モジュール側へ置いている。
Pixel用の部品だけ別APKになる感じか。
そう考えると分かりやすい。

実際の作業3 ― Dynamic FeatureのManifestを書く

ローカル動作確認では、Tensor runtimeをinstall-time Featureとしてインストールする構成にした。

サンプルのAndroidManifest.xmlは次のようになる。

<manifest
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:dist="http://schemas.android.com/apk/distribution">

    <dist:module
        dist:instant="false"
        dist:title="@string/tensor_runtime_title">

        <dist:delivery>

            <dist:install-time>

                <dist:removable
                    dist:value="true" />

            </dist:install-time>

        </dist:delivery>

        <dist:fusing
            dist:include="true" />

    </dist:module>

</manifest>

今回removable="true"としているのは、bundletoolでTensor runtimeを独立したFeature splitとして確認するための検証用構成である。

最終的にGoogle Playで配布する場合の構成とは別に考える必要がある。

実際の作業4 ― appのAndroidManifest.xmlを修正する

Tensor用Dispatch Runtimeを用意しても、それだけではPixel側のTensor用vendor libraryへアクセスできない。

そこでapp側のAndroidManifest.xmlへNPUの宣言を追加する。

<uses-feature
    android:name="android.hardware.npu"
    android:required="false" />

さらに<application>内へTensor関連のnative libraryを追加する。

<application
    android:extractNativeLibs="true"
    ... >

    <uses-native-library
        android:name="libOpenCL.so"
        android:required="false" />

    <uses-native-library
        android:name="libcdsprpc.so"
        android:required="false" />

    <uses-native-library
        android:name="libedgetpu_litert.so"
        android:required="false" />

    <uses-native-library
        android:name="libedgetpu_util.so"
        android:required="false" />

    <uses-native-library
        android:name="libedgetpu_client.google.so"
        android:required="false" />

    <uses-native-library
        android:name="libedgetpu_tachyon.google.so"
        android:required="false" />

</application>

これらのライブラリを自分のAPKへコピーするという意味ではない。

Pixel側に存在するTensor関連ライブラリを、自分のアプリから利用できるようにするための宣言である。

このuses-native-libraryって、ここに書いた.soを自分で全部用意するわけじゃないの?
違う。
Android端末側にあるライブラリを、アプリから参照できるようにする宣言だ。
Tensor Dispatchは自分のアプリ側。
その先のTensor用ライブラリはPixel側、ということね。
そういう構成。

実際の作業5 ― LiteRT-LMのKotlin側とnative側を揃える

LiteRT-LMはKotlinだけで動いているわけではない。

Kotlin / Java API

liblitertlm_jni.so

libLiteRt.so

Google Tensor Dispatch

Tensor G5

Kotlin側のAPIとliblitertlm_jni.so側のJNIインターフェースは、同じ世代に揃える必要がある。

今回、実際にTensor G5で文章生成まで成功した構成では、Google AI Edge Galleryで動いているnative runtime側に合わせて、LiteRT-LM v0.15.0相当のKotlin APIを使用した。

作業としては、LiteRT-LM公式ソースの、

kotlin/java/com/google/ai/edge/litertlm/

以下にあるKotlin APIソースを、サンプルプロジェクトなら次の位置へ配置する。

app/
└─ src/
   └─ main/
      └─ java/
         └─ com/
            └─ google/
               └─ ai/
                  └─ edge/
                     └─ litertlm/
                        ├─ Engine.kt
                        ├─ Config.kt
                        ├─ Conversation.kt
                        ├─ Message.kt
                        ├─ LiteRtLmJni.kt
                        └─ ...

関連する依存関係も追加する。

dependencies {

    implementation(
        "com.google.code.gson:gson:2.12.1"
    )

    implementation(
        kotlin("reflect")
    )

    implementation(
        "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.11.0"
    )
}
LiteRT-LMってGradleに一行書けば終わるイメージだったんだけど。
GPUならかなり近い。
でも今回のTensor G5検証では、Gallery側nativeとJNIの世代を揃える必要があった。
.soだけ新しくしたり、Kotlinだけ新しくしたりはダメ?
JNIの関数定義まで一致する必要がある。
native側なので、合わない場合は普通のKotlin例外ではなくアプリごと落ちることもある。

Google AI Edge Galleryのnative runtimeを動作確認用として利用した

今回の目的は、まず「Pixel 10 Pro FoldのTensor G5で本当にGemma 4を生成まで動かせるのか」を確認することだった。

そこで、すでに同じ端末でNPU動作しているGoogle AI Edge Galleryのnative runtimeを比較対象として利用した。

Android Debug Bridgeから、インストールされているGalleryのAPK位置を確認できる。

adb shell pm path com.google.ai.edge.gallery

すると、base APKだけではなく複数のsplit APKが表示される。

package:/data/app/.../base.apk
package:/data/app/.../split_config.arm64_v8a.apk
package:/data/app/.../split_config.ja.apk
package:/data/app/.../split_google_tensor_runtime.apk
package:/data/app/.../split_google_tensor_runtime.config.arm64_v8a.apk

必要なsplitをPCへ取得する場合は、

adb pull <APK_PATH> gallery-arm64.apk

のように取得し、ZIPとして展開してnative libraryを確認できる。

今回確認した主なファイルは、

libLiteRt.so
liblitertlm_jni.so
libLiteRtDispatch_GoogleTensor.so

である。

動作確認したバイナリはSHA-256も記録した。

libLiteRt.so

31672bad4be4dcaa7c7efbecbb0346e36ad30209807529a8366ce291acb9726e


liblitertlm_jni.so

83f96e035bd6b49a691113743a8b7f26912f0a84a0df07351dafd5c5dbe33b5d


libLiteRtDispatch_GoogleTensor.so

e3776e8bd21cc721cf873e8e802a74365adc60f789fb925c3be46de7729bc593
このGallery由来native runtimeは、Tensor G5での技術的な動作確認をするために利用した検証構成である。

Google AI Edge Galleryから取得したバイナリを、そのまま一般配布用アプリへ同梱してよいという意味ではない。

一般公開するアプリでは、公式に再配布可能なruntime構成へ整理する必要がある。

実際の作業6 ― EngineConfigを作ってNPUで初期化する

必要なruntimeが揃ったら、Kotlin側からTensor G5用のEngineを作る。

サンプルコードは次のようになる。

import com.google.ai.edge.litertlm.Backend
import com.google.ai.edge.litertlm.Engine
import com.google.ai.edge.litertlm.EngineConfig

private fun createTensorEngine(
    modelFile: File
): Engine {

    val config = EngineConfig(
        modelPath = modelFile.absolutePath,

        backend = Backend.NPU(
            nativeLibraryDir =
                applicationInfo.nativeLibraryDir
        ),

        cacheDir = cacheDir.absolutePath
    )

    return Engine(config)
}

Engineを作っただけではまだ推論できないので、バックグラウンドスレッド側でinitialize()する。

val engine = createTensorEngine(modelFile)

withContext(Dispatchers.Default) {

    engine.initialize()
}

数GBのモデルを扱うため、UIスレッド上で初期化すると画面が固まる。

そのためCoroutinesを使ってバックグラウンド処理にした。

コードだけ見ると、NPU指定って意外と普通ね。
Kotlin側だけ見るとそう。
難しいのは、そのBackend.NPUの先に必要なnative runtimeを正しく用意するところだ。

実際の作業7 ― Conversationを作って文章を生成する

Engineが初期化できたらConversationを作る。

今回使ったGemma 4 E2BではSamplerを次のように設定した。

val conversationConfig =
    ConversationConfig(

        samplerConfig = SamplerConfig(
            topK = 1,
            topP = 1.0,
            temperature = 1.0
        )
    )

そして文章を生成する。

val result =
    engine.createConversation(
        conversationConfig
    ).use { conversation ->

        conversation
            .sendMessage(
                "日本語で、読書ブログの冒頭に使える文章を作ってください。"
            )
            .toString()
    }

実際のアプリでは、この処理もUIスレッドから分離する。

val generatedText =
    withContext(Dispatchers.Default) {

        engine.createConversation(
            conversationConfig
        ).use { conversation ->

            conversation
                .sendMessage(prompt)
                .toString()
        }
    }

resultTextView.text = generatedText

なぜtopKを1にしているのか

今回使用したGemma 4 E2Bモデルでは、SamplerのtopKに制約があった。

そのため、

topK = 1

としている。

この設定でGPU版でもTensor G5版でも文章生成まで確認できた。

Tensor G5用モデルは「旧モデル」を使った

今回の記事で特に注意しておきたいのがモデルのバージョンである。

今回Tensor G5 NPUで生成まで成功したのは、現在公開されている新しいモデルではなく、一つ前の旧Tensor G5モデルだった。

動作確認できた旧モデルのサイズは、

3,953,110,901 bytes

約3.95GBである。

Android画面上ではGiB換算のため、およそ3.68GBと表示された。

SHA-256は次の通り。

62faebcfd101acb841c33249530430397e031eb17d4dd3d2a71193d135705f27

一方、現在の約3.11GB版Tensor G5モデルでは、

Input tensor not found

となり、この環境では正常に利用できなかった。

ここで重要なのは、自作アプリだけで発生した現象ではないことである。

新しい約3.11GB版をGoogle AI Edge Galleryへ読み込ませても正常に動作しなかった。

一方、旧約3.95GB版をAI Edge Galleryへ読み込ませた場合は、

Model on NPU

となり、文章生成まで成功した。

そのため今回の検証では、アプリ側の問題とモデル側の問題を混同しないよう、GalleryでもNPU動作が確認できている旧モデルを基準にした。

新しい方じゃなくて、わざわざ古いモデルを使ったのはそういう理由か。
新モデルが動かない状態でアプリを作ると、モデルが悪いのかコードが悪いのか分からなくなる。
Galleryでも動く旧モデルを基準にすれば、少なくともモデルは正常だと判断できる。
そういうこと。

実際の作業8 ― AABを作る

今回Tensor runtimeをDynamic Featureとしているため、動作確認ではAABを作り、bundletoolを使ってFeature split込みでインストールした。

サンプルプロジェクトの場所を、

C:\Dev\TensorLocalAi

とした場合、まずプロジェクトディレクトリへ移動する。

cd C:\Dev\TensorLocalAi

Android Studio付属のJavaを使う場合は、例えば次のように設定する。

set "JAVA_HOME=C:\Program Files\Android\Android Studio\jbr"
set "PATH=%JAVA_HOME%\bin;%PATH%"

そしてAABを作成する。

gradlew.bat :app:bundleDebug

成功すれば、例えば、

app\build\outputs\bundle\debug\app-debug.aab

が作成される。

実際の作業9 ― bundletoolでAPKセットを作る

次にbundletoolでAABからAPKSを作成する。

java -jar tools\bundletool-all-1.18.3.jar build-apks ^
--bundle=app\build\outputs\bundle\debug\app-debug.aab ^
--output=tensor-local-ai-debug.apks ^
--overwrite

ここで作成される.apksは一つの普通のAPKではなく、端末に必要な複数のsplit APKをまとめたファイルである。

実際の作業10 ― Pixelへインストールする

ADBでPixelが見えていることを確認する。

adb devices

例えば、

List of devices attached
YOUR_DEVICE_ID    device

と表示されたとする。

bundletoolからインストールする。

java -jar tools\bundletool-all-1.18.3.jar install-apks ^
--apks=tensor-local-ai-debug.apks ^
--device-id=YOUR_DEVICE_ID ^
--adb="%LOCALAPPDATA%\Android\Sdk\platform-tools\adb.exe"

ここでYOUR_DEVICE_IDは自分の端末IDへ置き換える。

インストール後は、Tensor runtime用splitも端末に入っている状態になる。

base.apk
split_config.arm64_v8a.apk
split_config.ja.apk
split_config.xxhdpi.apk
split_google_tensor_runtime.apk
split_google_tensor_runtime.config.arm64_v8a.apk
Android StudioでRunするのと何が違うの?
今回はDynamic Featureが確実にどう分割されて端末へ入ったのか確認したかった。
だからAABとbundletoolで明示的にインストールした。

実際の作業11 ― 旧Tensor G5モデルを読み込む

アプリを起動したら、「モデル選択」から旧Tensor G5モデルを選択する。

アプリ内部へコピーしたあと、モデル名を判定してNPUバックエンドを選択する。

val isTensorModel =
    modelFile.name
        .lowercase()
        .contains("google_tensor_g5")

val backend =
    if (isTensorModel) {

        Backend.NPU(
            nativeLibraryDir =
                applicationInfo.nativeLibraryDir
        )

    } else {

        Backend.GPU()
    }

そしてEngineを初期化する。

val engineConfig =
    EngineConfig(
        modelPath = modelFile.absolutePath,
        backend = backend,
        cacheDir = cacheDir.absolutePath
    )

val engine = Engine(engineConfig)

engine.initialize()

そしてTensor G5 TPU / NPUで生成に成功した

ここまでの構成で旧Tensor G5モデルを読み込むと、モデル初期化が完了した。

アプリ画面では、

モデル使用可能

実行バックエンド: TPU / NPU (Google Tensor G5)

モデル: Tensor G5用Gemma 4 E2B (約3.68 GB)

と表示された。

さらに日本語プロンプトを入力して生成ボタンを押すと、実際に日本語文章が返ってきた。

Pixel 10 Pro Fold

Tensor G5用Gemma 4 E2B旧モデル

LiteRT-LM

Google Tensor Dispatch Runtime

Tensor G5 TPU / NPU

日本語文章生成
で、最終結果は?
生成できた。
初期化成功だけじゃなく?
ちゃんと日本語の文章が返ってきた。
画面も「生成完了」。
バックエンドは?
TPU / NPU (Google Tensor G5)
じゃあ目的達成だな。

GPUへのフォールバックで動いたわけではない

ここは念のため明確にしておきたい。

今回の結果は、NPUの初期化に失敗したあとGPUへ切り替わって文章が生成できた、というものではない。

Tensor G5向けモデルに対して明示的に、

Backend.NPU(
    nativeLibraryDir = applicationInfo.nativeLibraryDir
)

を指定している。

その状態でEngineを初期化し、Conversationを作成し、日本語生成まで完了した。

「NPUで失敗したからGPUが頑張りました」じゃないのね?
違う。
Tensor G5専用モデルをNPUバックエンドで動かして、そのまま生成まで進んでいる。
じゃあちゃんとPixelのAI用ハードウェアを使えたわけだ。

作業内容をファイル単位で整理すると

今回の作業を「どのファイルを触ったか」で整理すると、だいたい次のようになる。

settings.gradle.kts Dynamic Feature用モジュールを追加。
app/build.gradle.kts Dynamic Featureの登録、Kotlin APIに必要な依存関係を追加。
app/src/main/AndroidManifest.xml NPU featureとTensor用vendor native libraryを宣言。
tensor_runtime/google_tensor_runtime/build.gradle.kts Dynamic Featureとして設定し、jniディレクトリを登録。
tensor_runtime/google_tensor_runtime/src/main/AndroidManifest.xml Tensor runtimeをinstall-time Featureとして設定。
tensor_runtime/google_tensor_runtime/src/main/jni/arm64-v8a/ Google Tensor用Dispatch Runtimeを配置。
app/src/main/jniLibs/arm64-v8a/ 動作確認用としてLiteRT / LiteRT-LM native runtimeを配置。
app/src/main/java/com/google/ai/edge/litertlm/ 今回使用したnative側と同世代のLiteRT-LM Kotlin APIを配置。
MainActivity.kt モデル選択、生成ボタン、結果表示を実装。
Local AI管理クラス モデル保存、GPU/NPU判定、Engine初期化、Conversation生成をまとめる。

最終的なサンプル構成

TensorLocalAiSample
│
├─ settings.gradle.kts
│
├─ app
│  ├─ build.gradle.kts
│  │
│  └─ src
│     └─ main
│        ├─ AndroidManifest.xml
│        │
│        ├─ java
│        │  ├─ com/example/tensorlocalai/
│        │  │  ├─ MainActivity.kt
│        │  │  └─ LocalAiManager.kt
│        │  │
│        │  └─ com/google/ai/edge/litertlm/
│        │     ├─ Engine.kt
│        │     ├─ Config.kt
│        │     ├─ Conversation.kt
│        │     └─ ...
│        │
│        └─ jniLibs
│           └─ arm64-v8a
│              ├─ libLiteRt.so
│              └─ liblitertlm_jni.so
│
└─ tensor_runtime
   ├─ runtime_strings
   │
   └─ google_tensor_runtime
      ├─ build.gradle.kts
      │
      └─ src
         └─ main
            ├─ AndroidManifest.xml
            │
            └─ jni
               └─ arm64-v8a
                  └─ libLiteRtDispatch_GoogleTensor.so

今回動いた構成をまとめる

  • Google Pixel 10 Pro Foldを使用
  • Google Tensor G5のTPU / NPUを使用
  • AndroidアプリはKotlin + XML Views
  • LiteRT-LMからBackend.NPUを指定
  • Google Tensor Dispatch RuntimeをDynamic Featureとして配置
  • AndroidManifest.xmlでTensor用vendor native libraryを公開
  • Kotlin APIとJNI/native runtimeの世代を揃える
  • Gemma 4 E2Bの旧Tensor G5モデル約3.95GB版を使用
  • SamplerConfigtopK = 1
  • AABを作り、bundletoolでFeature split込みでインストール
  • NPUバックエンドのまま日本語生成まで成功
Pixel 10 Pro Fold

Google Tensor G5

TPU / NPU

Google Tensor Dispatch Runtime

LiteRT-LM

Gemma 4 E2B

自作Androidアプリで日本語生成

前回は「TPUを使おう」で終わった。そして今回は本当に動いた

前回の記事を書いた時点では、Google AI Edge GalleryでGemma 4を動かし、Pixel 10 Pro FoldにはTensor G5とTPUがあるのだから、それを自作アプリでも使ってみたいというところまでだった。

その時点では、実際に自作アプリからTensor G5向けモデルを動かせるかどうかは分かっていなかった。

今回、Kotlin版のテストアプリを作り、モデルの保存、Dynamic Feature、Tensor用Dispatch Runtime、AndroidManifest.xml、LiteRT-LM、NPU Backendという部品を一つずつ組み合わせることで、ようやく全体の形が分かった。

そして最後に、Tensor G5のTPU / NPU上でGemma 4 E2Bを動かし、日本語文章を返すところまで確認できた。

前回、「Tensor G5のTPU、使えるものは使おう」って言って終わったよね。
言ってたな。
本当に使えた。
これで読書ブログアプリのAI部分はかなり方向が固まった。
あとは本の写真とメモを渡して、ちゃんとブログ記事を作らせる。
それとAmazonアソシエイト、Blogger投稿だな。
前回、「今度こそ設計変わらないでしょうね?」って聞いたんだけど。
TPUまで動いたんだから、たぶん大丈夫だろ。
「たぶん」?
前回も言ったけど、それは知らない。

次はテスト画面から本来の読書ブログアプリへ

ここまでは、あくまでLiteRT-LMとTensor G5を動かすためのテスト画面だった。

しかし、一番大きな技術的確認だった「自作Androidアプリから端末内AIを呼び出し、日本語文章を生成する」という部分は動いた。

しかも今回は、Android側に管理されたGemini Nanoを利用する方式ではなく、自分で選んだGemma 4モデルをLiteRT-LMから直接読み込み、Pixel 10 Pro FoldのTensor G5を使って動かしている。

次はいよいよ、本来作りたかった読書ブログ専用アプリへこのAI機能を組み込んでいく。

本を撮影

書名と感想メモを入力

「AIで記事を作成」

Tensor G5上のGemma 4が文章生成

Amazonアソシエイトリンクを追加

Bloggerへ投稿

最初に考えていた「ChatGPTアプリへ共有して、生成された文章をコピーして戻る」という構成から見ると、かなり違うものになった。

ただし利用する側から見れば、最終的な操作はむしろ単純になる。

本の写真と感想を入れてボタンを押せば、端末内AIが記事を作る。

それが最初から作りたかったアプリの形である。

この記事について

この記事の構成、文章、会話部分の生成にはOpenAIのChatGPTを使用しています。
Androidアプリの設計やLiteRT-LM、Tensor G5の動作確認についてもChatGPTと相談しながら進めています。

記事内のパッケージ名、Windowsのプロジェクトパス、Android端末ID、クラス名の一部などは、実際の開発環境固有の値ではなく、説明用のサンプル値へ置き換えています。

一方、今回開発しているAndroidアプリのAI生成機能そのものについては、クラウド上のChatGPT APIを利用しているわけではありません。
Gemma 4をAndroid端末内で実行し、Google Pixel 10 Pro FoldのTensor G5に搭載されたTPU / NPUを利用してローカルで文章を生成しています。

なお、今回紹介したGoogle AI Edge Gallery由来のnative runtime構成は、Tensor G5での動作確認を目的とした検証構成です。一般配布するアプリとして利用する場合は、公式に配布・再配布可能なruntime構成へ整理する必要があります。

検索

年月ごとアーカイブ

フレンドブログ

Translate

自己紹介

自分の写真
映画が好き! 音楽好き! 演奏も好き! ミキシングが一番好き!

QooQ