1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Capacitor×llama.cpp オンデバイスLLMで「空応答が出る」を半日溶かして根本解決した話

1
Last updated at Posted at 2026-07-22

iPhone 上の WebView アプリ(Capacitor)で、GGUF モデルを llama.cpp / Metal で動かす構成のデバッグ記録です。「たまに空応答(0トークン)が返る」という現象の根本原因が、思い込んでいた場所とことごとく違ったという、切り分け方法込みの実話です。

同じ構成(Capacitor + llama.cpp)の日本語記事はほぼ見当たらず(ネイティブSwiftやReact Nativeの記事は多数)、ハマりどころが未共有だったので残します。

構成

  • Capacitor(WebView)+ Vue 3
  • オンデバイス LLM: llama-cpp-capacitor プラグイン経由で llama.cpp(Metal 埋め込みシェーダ)
  • モデル: Qwen2.5-3B-Instruct Q4_K_M(GGUF, Apache 2.0)
  • 実機: iPhone 12 Pro Max(A14 / RAM 6GB)

現象

実機で生成を回すと、生成レスの歩留まりが run ごとに 1〜10/10 と激しくばらつく。悪い run では tokens_predicted: 0・本文空の「空応答」が多発する。Mac の llama-server では同じモデル・同じプロンプトで全く問題なし。

誤診の記録(正直に)

最初、私は根本原因を 3回連続で外した

  1. 「3Bが6GB機に重すぎる(メモリ不足)」→ 1.5Bに落としても改善せず
  2. 「数時間叩いてサーマルスロットリング」→ 25分アイドルでも改善せず
  3. 「端末再起動で直る」→ devicectl device reboot 直後の冷えた端末でも改善せず

さらに JS 側の緩和(temperature 段階上げ / プロンプト・プライミング / リトライ回数増)も試したが、いずれも歩留まりを安定させられなかった。推測ベースの対処で半日溶かした。

切り分け1: Mac と実機を「同一入力」で比較する

推測をやめ、device が送るのと寸分違わぬプロンプト・パラメータを Mac の llama-server に投げて比較した。

① 手動ChatML → /completion(device方式):        即EOS 0/20
② messages → /v1/chat/completions(jinja方式):  即EOS 0/20

Mac では 0%。プロンプト・テンプレート・サンプリングパラメータ・モデル・端末HW はすべて無罪と確定。差分は「実行時の llama.cpp ビルドそのもの」だけに絞られた。

教訓: prompt 層か framework 層かは、同一入力を別ランタイムに投げれば1回で切り分けられる

切り分け2: ネイティブログを直接読む

Capacitor アプリは JS の console.log しか見えないと思いがちだが、devicectl でネイティブの stdout/stderr が取れる。

xcrun devicectl device process launch --console --terminate-existing \
  --device <UDID> <bundle-id>

これで llama.cpp の ggml_metal_* や自前の fprintf が全部見える。ここから真因が2つ出てきた。

真因A: 再ビルドした framework の Metal シェーダが実行時コンパイル失敗

そもそも「ネイティブを直したい→framework を再ビルド」で最初に踏んだ地雷。手元の cpp/ ソースからビルドすると、実行時に:

lm_ggml_metal_load_library: error: MTLLibraryErrorDomain Code=3
  unknown type name 'block_q1_0'
  use of undeclared identifier 'FC_UNARY'
  unknown type name 'ggml_metal_kargs_unary'

= 埋め込み .metal シェーダと ggml ヘッダのバージョンが不整合(.metal が新しい量子化型・カーネル引数構造体を参照するが、コンパイル対象のコードに定義が無い)。Metal ライブラリのロードに失敗し CPU フォールバック → 0.2 tok/s の激遅になっていた。これを当初「ハング」と誤認していた。

正しく動く framework は、別の内部整合したソーススナップショット(モジュラ構成の新しい ggml-metal、ggml_metal_library_init API)からビルドされていた。そちらから再ビルドすると Metal 正常・14 tok/s に復帰。

(動作版のログ)
lm_ggml_metal_library_init: using embedded metal library
lm_ggml_metal_library_init: loaded in 0.020 sec

教訓: 「全モデルが遅い」時は、サーマルやメモリを疑う前に --consolemetal_library_init: loaded(正常)か metal_load_library: error(CPUフォールバック)かを見る。ログ1本で確定できる。

真因B: 空応答の正体は prefill の llama_decode 失敗

Metal を直しても歩留まりの分散は残った。ここで自前の fprintf を仕込んで追うと、決定打が出た:

  • 空応答(tp=0)は stopped_eos: false … EOS で止まったのではない
  • ログを数えると prefill decode failed の回数が空応答の数と完全一致

つまり空応答の正体は「即EOS」ではなく、プロンプトの prefill 中に llama_decode が失敗して 0 トークンで返っていた。さらに戻り値を出力させ、決定的パラメータを特定した:

n_batch を 512 → 128 に下げると、decode 失敗が 0 になった。

6GB 機では、n_batch=512 の prefill 用 compute buffer がメモリを圧迫し、大きいプロンプト(=会話が進んだ後半)ほど llama_decode が失敗していた。これが「run ごとの激しい分散」の正体だった。

修正

内部整合したソースから再ビルドする前提で、3点:

  1. n_batch を 128 に(主因。prefill の compute buffer を縮小)
  2. prefill 前に KV キャッシュを全クリアしてフル prefill(コンテキスト使い回しの不整合予防)
  3. 生成初手で EOG(終了トークン)を弾く際のフォールバックを、llama_get_logits_ith 依存からサンプラーの候補配列ベースに変更(保険)

結果、実機で 追試 5/5 run が 10/10・空応答 0(以前は 5/10 で空多発)に安定。

まとめ / 教訓

  • オンデバイス LLM の生成不良は、JS(prompt / param / リトライ)を疑う前に native の loadPrompt / decode を計器で追う。今回、JS 緩和で3周空転した後、fprintf 1発で真因に到達した。
  • n_batch は、メモリ制約端末で llama_decode の成否を分ける見落としやすい重要パラメータ
  • 切り分けは推測ではなく「同一入力を別ランタイムへ」「--console でネイティブログ」の2手で機械的に。
  • Metal 埋め込みシェーダは、ソースツリーのバージョン整合が崩れていると実行時コンパイルで静かに失敗する。ビルドは内部整合したスナップショットから。

Mac で動いたから実機でも動く、は嘘。実行時のメモリ制約で挙動が変わるという、オンデバイス特有の教訓でした。

参考

この「Capacitor + llama.cpp」構成の直接の解法記事は見つけられませんでしたが、背景理解・切り分けに役立ったものを挙げます。

ビルド / Metal(真因Aで直接役立った)

  • llama.cpp build-xcframework.sh — iOS 向けの正しい cmake フラグ(-DGGML_METAL=ON -DGGML_METAL_EMBED_LIBRARY=ON -DGGML_METAL_USE_BF16=ON)の一次資料。再ビルドが CPU フォールバックしていた原因の特定に直結した
  • llama.cpp docs/build.md

日本語の先行記事(構成把握・RAM律速の裏取り)

Capacitor 系プラグインの現状(黎明期)

「空応答」の別要因(今回とは原因が違ったが切り分けの参考)

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?