技術仕様
このページは、現行 Android 実装をもとに、LLM tester with llama.cpp のフロントアプリ、設定保存、JNI ブリッジ、CMake/NDK ビルド、llama.cpp/ggml、API サーバー、WebUI 配信までの連携を技術仕様として整理したものです。
関連文書: 操作マニュアル | llama.cpp / JNI / CMake 深掘り | プライバシーポリシー
目次
1. 全体像
このアプリは「UI から直接ネイティブ推論を叩く経路」と「端末内 HTTP サーバー経由で推論を叩く経路」の 2 本を持っています。どちらの経路でも、モデルロード・ビジー制御・パラメータ適用・再初期化は ModelManager に集約され、最終的な推論実体は llama_jni 共有ライブラリ内の llama.cpp / ggml / mtmd に到達します。
MainActivity / SettingsActivity
↓
ConfigurationManager / ModelFileHelper
↓
ModelManager
↓
LlamaNative (Java native wrapper)
↓ JNI
llama_jni (jni_llama.cpp)
↓
llama.cpp + ggml + mtmd + libcurl + mbedTLS
別経路:
OllamaForegroundService
↓
OllamaApiServer (HTTP, same-device / LAN)
↓
ModelManager → LlamaNative → JNI → llama.cpp
| レイヤ | 主担当 | 役割 |
|---|---|---|
| UI | MainActivity / SettingsActivity / DocumentsActivity | 入力、状態表示、ログ表示、設定編集、API/WebUI 起動停止 |
| 設定 | ConfigurationManager | プロファイル JSON の保存・読込、既定値提供 |
| モデル資産 | ModelFileHelper | GGUF / mmproj のファイル名解決、保存場所、モダリティ推定 |
| 実行制御 | ModelManager | ロード、再初期化、排他制御、ネイティブ呼び出し集約 |
| Java ↔ Native | LlamaNative | native メソッド公開、トークン進捗コールバック登録 |
| Native 実装 | jni_llama.cpp | グローバル状態、初期化、サンプリング、停止シーケンス、ログ、クラッシュ記録 |
| HTTP | OllamaApiServer / OllamaForegroundService | Ollama 互換 API、OpenAI 互換 API、WebUI 静的配信、待機キュー |
2. フロントアプリ層
2-1. MainActivity の役割
- メイン画面は、プロンプト入力、モデル出力、処理ログ、ログファイル表示、API/WebUI 起動ボタンをまとめた運用 UI です。
sendButton押下時は、選択中プロファイルに対応するモデルが未ロードまたは不一致なら先にModelManager.loadConfiguration()を実行し、その後processGeneration()で推論します。- 直接入力の推論では
PromptTemplateManager.buildPromptForDirectInputWithSelection()を使い、GGUF メタデータ、カスタムテンプレート、Settings の system prompt、Think 設定を統合した最終プロンプトを生成します。 - ストリーミング有効時は
LlamaNative.TokenListenerを登録し、受信トークンをStreamOutputFilterで整形して UI に逐次反映します。非ストリーミング時はgenerate()の戻り値を一括表示します。 - 「モデル再初期化」は現在処理を中断し、必要なら API クライアント接続も切った上で
forceReinitializeConfiguration()を呼びます。
2-2. SettingsActivity の役割
- プロファイル名、モデル URL / ローカル GGUF、
n_ctx、n_threads、top_p、DRY、Mirostat、Think、GPU Offload Layers、API ポート、表示言語、ログレベルを編集します。 - ローカル GGUF 取り込みでは SAF のファイルピッカーを開き、選択ファイルをアプリのモデル保存先へコピーします。URL 指定時は後段でネイティブ download が使われます。
- ダウンロード進捗は
LlamaNative.DownloadProgressListenerで受け取り、ProgressBar とファイル情報表示に反映します。 - GPU Offload Layers はシークバーで 0〜40 を扱い、40 超相当は
-1に変換して「全層オフロード」扱いにしています。 - Documents ボタンからはアプリ内ドキュメント画面へ遷移し、マニュアルとプライバシーポリシーを表示します。
2-3. 起動時 / バックグラウンド連携
- アプリケーションクラス
LlamaApplicationは起動時にネイティブログ出力先を設定し、Java の未処理例外をlast_crash.txtに保存します。 - API/WebUI を常駐させるときは
OllamaForegroundServiceが Foreground Service として動作し、通知から Stop / Exit を行えます。 - MainActivity は Service からのブロードキャストを受け、サーバー状態と処理ログをリアルタイム更新します。
3. 設定・モデルファイル管理
3-1. ConfigurationManager
各プロファイルは外部ファイル領域の configs/*.json として保存されます。既定プロファイルは初回起動時に自動生成され、モデル URL、生成パラメータ、Think、GPU Offload、カスタムテンプレート、system prompt、mmproj 参照などを保持します。
| カテゴリ | 主な項目 | 用途 |
|---|---|---|
| モデル | modelUrl, multimodalProjectorUrl | GGUF と任意の mmproj 参照 |
| ロード系 | nCtx, nThreads, nBatch, gpuOffloadLayers | コンテキスト、CPU、GPU オフロード設定 |
| サンプリング | temp, topP, topK, Penalty, Mirostat, DRY, XTC | サンプラー構築にそのまま反映 |
| テンプレート | systemPrompt, customChatTemplate, enableThinking | プロンプト生成の最終形を制御 |
| UI / 挙動 | streaming | 画面・API のストリーミング出力既定値 |
- これとは別に、共有 MCP 設定と共有 Function Definitions は
SharedPreferences側へ保存され、モデル別プロファイルとは分離されています。 - 保存キーは
shared_mcp_servers_json、shared_function_definitions_jsonと各有効化スイッチで、WebUI 外で使うかどうかを個別に切り替えます。 /propsのwebui_settingsにはsharedMcpServersとsharedFunctionDefinitionsが入り、WebUI はこれをローカル設定と合わせて利用します。
3-2. ModelFileHelper
- モデル保存先は
getExternalFilesDir(null)優先、取れない場合は内部 files です。 - URL / ローカル参照からファイル名を抽出して保存先ファイルパスへ変換します。
.gguf以外はモデル候補として扱いません。- mmproj / projector / gemma4v / gemma4a などの名前規則からプロジェクタ候補を自動検出し、ファイル名トークン一致度で最良候補を選びます。
- 同じ仕組みで vision / audio モダリティも推定し、未ロード状態でも
/v1/modelsや/propsに反映します。
3-3. 分割 GGUF と HTTPS ダウンロード
- 分割 GGUF は
name-00001-of-00005.gguf形式を検出し、欠けている shard がないかを Java 側・Native 側の両方で確認します。 - HTTPS ダウンロード前には Android CA Store を PEM 束へ書き出し、ネイティブの libcurl に CA bundle パスを渡します。
- このため Android 側の証明書ストアに沿った通常の TLS 検証を Native download でも維持できます。
4. ModelManager の責務
ModelManager は UI と HTTP サーバーの共通実行基盤です。排他制御、モデルロード、再ロード、パラメータ適用、ネイティブログ設定、ダウンロード前の trust store 準備まで集約しています。
4-1. 排他と再初期化
busy,resetPending,reinitializingを持ち、UI 直入力と API 呼び出しが同じモデル状態を同時に壊さないようにしています。tryAcquire()に成功した経路だけがロード / 生成を実行し、完了時にrelease()します。- 強制再初期化は、必要なら
cancelGeneration()を投げ、現在処理の解放を待ってからreinitializeConfiguration()に入ります。
4-2. ロード手順
- プロファイルを読み込む。
- モデルファイル名を決定し、保存先パスを作る。
- 必要なら mmproj を自動検出またはダウンロードする。
- モデルファイルが無ければ Native download を実行する。
- 同一モデル再利用でなければ既存 model/context/mmproj を解放する。
- PRELOAD:
n_ctx=64で一度initWithMmproj()を呼び、ロード可否を早めに判定する。 - 本初期化: 本来の
n_ctxに戻して再度initWithMmproj()を呼ぶ。 applyConfiguration()でサンプリング系パラメータを Native 側へ反映する。
この 2 段ロードにより、巨大コンテキストでいきなり失敗する前に、最小限コンテキストでモデルファイル自体のロードを先に確認する構成になっています。
4-3. 推論時の扱い
- ModelManager 自体はプロンプト文字列の構築を行わず、PromptTemplateManager が作った完成済み prompt を
llama.generate()またはllama.generateWithMedia()に流します。 - Vision / Audio は Native 初期化済みのモダリティフラグ
supportsVision()/supportsAudio()を通して公開します。 - ログレベルとログパスは singleton 初期化時に一度適用され、ネイティブログと Java ログの両方で追跡しやすくしています。
5. JNI / C++ 層
5-1. Java 側の公開 API
LlamaNative は System.loadLibrary("llama_jni") で共有ライブラリをロードし、以下を公開します。
| メソッド | 用途 |
|---|---|
download(url, path) | libcurl によるモデル / mmproj ダウンロード |
initWithMmproj(modelPath, mmprojPath) | モデルと任意の multimodal projector 初期化 |
setLoadParameters(...) | ロード前に必要な n_ctx / n_threads / n_batch / GPU 層数を設定 |
setParameters(...) | Penalty / DRY / Mirostat / XTC などサンプラー関連を設定 |
generate(prompt), generateWithMedia(prompt, media) | テキスト / マルチモーダル生成 |
setTokenListener(listener) | ストリーミングトークン、完了、エラーのコールバック登録 |
cancelGeneration() | ネイティブ生成ループへ停止要求 |
getChatTemplate() | GGUF メタデータから chat template を取り出す |
supportsVision(), supportsAudio() | 現在ロード済みモデルのモダリティを返す |
5-2. Native 側のグローバル状態
g_model,g_ctx,g_mtmdによりモデル・推論コンテキスト・マルチモーダルコンテキストを保持します。g_current_model_path/g_current_mmproj_pathで同一モデル再初期化を回避します。g_supports_vision/g_supports_audioでロード済み能力を保持します。g_token_listenerとJavaVMを使い、Native スレッドから Java callback を安全に呼び戻します。g_cancel_generationは UI / API どちらからでも共有される停止フラグです。
5-3. initWithMmproj の流れ
- fatal signal handler を一度だけインストールし、ネイティブクラッシュ時に
native_crash.txtへ痕跡を残せるようにします。 - llama.cpp と mtmd のログコールバックを登録します。
- モデルファイル、mmproj ファイル、split GGUF の欠落を検査します。
- 既存 model / context / mtmd を必要に応じて解放します。
llama_backend_init()を呼び、登録済み ggml backend 数を確認します。llama_model_default_params()にn_gpu_layersを反映してllama_model_load_from_file()を実行します。llama_context_default_params()にn_ctx,n_threads,n_batch,n_threads_batchを入れてllama_init_from_model()を実行します。- 必要なら
initialize_optional_multimodal_support_locked()で mtmd を初期化し、vision/audio サポートを確定します。
5-4. generate の流れ
- 現在の memory をクリアし、prompt prefill に備えます。
- テキストのみなら
prefill_text_prompt_locked()、画像 / 音声付きならprefill_multimodal_prompt_locked()を使います。 - prefill 成功後に sampler chain を構築します。
- 最大
1024トークンまでループし、毎ステップllama_sampler_sample()→llama_sampler_accept()→llama_decode()を実行します。 - stop sequence 検出、EOG、context safety limit、cancel flag のいずれかで終了します。
- 差分トークンは
notify_token_delta()で Java へ渡し、完了時はnotify_token_complete()を送ります。
5-5. サンプラー構成
Native 側は Java の設定値を直接 sampler chain に変換します。順序は概ね以下です。
penalties → DRY → top_n_sigma → top_k → typical → top_p → min_p → XTC → temperature / dynamic temperature → mirostat v1 or v2 / fallback dist
DRY の sequence breakers は Java の既定値 \n,:,",* と同期されており、JNI 側でエスケープ展開して llama_sampler_init_dry() に渡されます。
5-6. 停止条件と出力整形
- 共通 stop sequences として
<|end|>,</s>,<|im_end|>,<end_of_turn>など複数テンプレート系の区切りを監視します。 - UTF-8 の妥当性確認を入れ、不完全な末尾バイト列は切り詰めます。
- Java 側ではさらに
ResponseMarkerSanitizerが出力上の余計な response marker を除去します。
5-7. ダウンロード・ログ・クラッシュ
- download は libcurl を使い、CA bundle が設定されていればそれを優先して TLS 検証します。
- split GGUF を検出すると primary shard 以外の shard URL も派生させて取得します。
- ログファイルは
ollama.log、Java のクラッシュはlast_crash.txt、Native の fatal signal はnative_crash.txtへ出力されます。
6. CMake / NDK ビルド
一般的な llama.cpp 導入の観点から JNI 境界や CMake 構成をさらに詳しく見る場合は、llama.cpp / JNI / CMake 深掘り を参照してください。
6-1. Gradle / NDK 側の特徴
compileSdk 35,targetSdk 35,minSdk 24です。- ABI は
arm64-v8aのみに絞られています。 - Native build は
src/main/cpp/CMakeLists.txtを使い、-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ONを渡しています。 - NDK は
27.2.12479018を固定し、Android 15 系で重要な 16KB page size 対応に合わせています。
6-2. CMake 側の構成
- プロジェクト名は
llama_android、C++17 / C11 を使用します。 - llama.cpp 本体は Git submodule 的な別ビルドではなく、
app/src/main/cpp/llama配下の source を直接 GLOB してllama_jniに静的に取り込んでいます。 - ggml は CPU backend を中心に必要 source を直接列挙し、ARM 向け実装も含めています。
- mtmd 系 source もリンクしているため、画像 / 音声 projector を伴うモデルを同じ
llama_jniで扱えます。 libcurl.aと mbedTLS 3 本の static library を IMPORTED 扱いでリンクしています。
6-3. Android 向けリンク上の注意
- リンクオプションに
-Wl,-z,max-page-size=16384と-Wl,-z,common-page-size=16384を付け、16KB ページ環境に合わせています。 -align-segmentsは Android NDK のld.lldで非対応のため明示的に避けています。GGML_USE_CPUとGGML_USE_K_QUANTSを compile definition で有効化しています。
6-4. この構成の意味
つまりこのアプリは「Java から JNI で共有ライブラリを呼ぶ」だけでなく、llama.cpp / ggml / mtmd / curl / TLS を Android 向けに一体ビルドした専用ランタイムを APK 内に持つ設計です。フロントアプリのボタン操作は、そのまま自前ビルドのローカル推論エンジンを制御する操作になっています。
7. API サーバー仕様
OllamaApiServer は軽量な独自 HTTP サーバーです。Foreground Service の中で待受し、同じ port 上で API と WebUI を同居させます。既定ポートは 11434 です。
7-1. 提供エンドポイント
| 経路 | 主用途 | 備考 |
|---|---|---|
POST /api/generate | 単一 prompt 生成 | Ollama 風 NDJSON streaming / 非 streaming 両対応 |
POST /api/chat | messages 配列による会話生成 | モデル family に応じて multi-turn prompt 化 |
GET/POST /api/tags | モデル一覧 | プロファイル一覧をモデル名として返す |
POST /v1/chat/completions | OpenAI 互換 chat completions | SSE streaming 対応 |
GET /v1/models, /models | モデル一覧 + 状態 | loaded/unloaded, modalities, path などを返す |
GET /props | llama.cpp WebUI 向け model props | default_generation_settings, chat_template, webui_settings |
GET /slots | slot 情報 | 総 slot 数は 1 固定 |
GET /health, /v1/health | ヘルスチェック | role と webui=true を返す |
/, /index.html ほか | Bundled WebUI 配信 | asset cache 付き |
7-2. 同時実行と待機キュー
- 推論 slot は 1 つだけです。実質 single-model / single-generation サーバーです。
- すでに使用中なら最大
10件まで待機キューに入り、最大60秒待ちます。 - キュー超過または待機タイムアウトでは
503を返します。 - モデル再初期化要求が入ると、新規要求は拒否され、待機中要求も reset in progress として打ち切られます。
7-3. /api/generate と /api/chat の内部処理
- リクエスト JSON を読む。
acquireGenerationSlot()で排他を取る。- 要求モデル名に対応する configuration をロードする。
- GGUF chat_template、custom template、system prompt、Think 設定を使って最終 prompt を組み立てる。
- request の
toolsか共有ツール設定がある場合はSharedToolManager.generateWithTools()に切り替わり、最大 10 ターンまで自動ツール実行を継続できます。 - streaming 時は token queue + writer thread を立て、ネイティブ生成スレッドをネットワーク I/O で止めない。
- client disconnect や end marker 検出時は
cancelGeneration()を発行する。
7-4. マルチモーダル入力の扱い
/api/chatと/v1/chat/completionsはcontent[]配列の中でtext,input_text,image_url,input_audioを扱えます。image_urlは HTTP/HTTPS の画像 URL または base64 data URL を受けられます。リモート取得は 10MB 上限です。input_audioは base64 本体を持つwav/mp3のみ受け付けます。- 画像・音声部分は内部で
<__media__>marker に置き換えられ、実データはbyte[][]として JNI へ渡されます。 - 現在ロード済みモデルが vision/audio 非対応なら
400で弾きます。
7-5. OpenAI 互換層の特徴
/v1/chat/completionsは streaming 時に SSE を返します。- 内部的には Ollama 互換経路と同じ prompt builder・ModelManager・JNI を使い、出力形式だけ OpenAI 互換 JSON / SSE へ変換しています。
n_predict=0が付いた場合は pre-encode only として空応答で早期復帰する分岐があります。- 一部 sampling 値は request override として configuration に上書き適用され、そのまま Native sampler へ入ります。
tool_callsが直接返らない場合でも、reasoning_contentや本文中の tool call marker から抽出を試みます。tool_choiceとparallel_tool_callsを受け取り、共有 MCP / Function Definitions 設定と組み合わせて内部ツール実行ループへ渡します。
8. WebUI と配信方式
8-1. WebUI の正体
- アプリは assets 配下に
webui/index.html,bundle.js,bundle.css,loading.htmlを同梱しています。 GET /または静的資産パスに対してhandleWebUi()がそれらを返します。- asset は一度読み込むと
webUiAssetCacheに保持され、以後はメモリキャッシュから返します。
8-2. WebUI と API の結合
- WebUI は別サーバーではなく、同じ
OllamaApiServerのルーティング内にあります。 /propsと/slotsが llama.cpp 風の WebUI 初期化に必要な設定を返し、webui_settingsには system message、Think 表示設定、共有 MCP / Function Definitions 設定が入ります。- ルート以外の未知パスは WebUI 資産名へ正規化され、既知 asset でなければ
index.htmlにフォールバックします。
8-3. 運用上の意味
つまり Android アプリは「ネイティブ推論エンジン + Ollama/OpenAI 互換 API + その API を使う WebUI」を 1 台の端末上に同梱した構成です。ローカルブラウザや LAN 内クライアントから同じ端末へアクセスするだけで、外部サーバーを置かずに一式を試せます。
9. 公開 Web ページとの関係
- この Mick Lab サイト自体は Firebase Hosting 上の静的サイトで、
public/配下を配信しています。 - Hosting 側では
/api/**をapi-fallback.jsonに rewrite し、それ以外は/index.htmlに fallback します。 - したがって、この公開ページ群はアプリ内のローカル API サーバーそのものではなく、端末内で動く API/WebUI 実装を説明するための静的ドキュメントです。
- 本ページの API / WebUI 記述は Android アプリ内部の
OllamaApiServerとOllamaForegroundServiceの仕様であり、Firebase Hosting 上の Web API 仕様ではありません。