llama.cppをSwiftLlamaで動かす on iPhone
最近ローカルLLM、オンデバイスLLMって流行ってますよね?
特にスマートフォンで動かすことがアツいと思っており、
iPhoneにてllama.cppとSwiftLlamaを使って
ローカルLLMを動かしてみました🔥
llama.cppとはC/C++で書かれたLLM推論を行えるライブラリです👌
Apple Siliconにも対応しています。
また、XCFrameworkが使えるのでコンパイルすることなく
Swiftプロジェクトで扱うことが可能です👇
さらに、今回はllama.cppをSwiftで簡単に扱うため、SwiftLlamaを使います😋
SwiftLlamaは、Swiftでより自然で使いやすいAPIを提供するパッケージで、
llama.cppの機能をSwiftから簡単に呼び出せるようにしたものです👇
SwiftLlamaの修正🔨
既存のSwiftLlamaをそのまま使うと上手くいかなかったので(上手く扱えなかったので)
何点か修正を行いました。
主な修正点まとめ👀
① llama.cppのリビジョン更新
記述されていたllama.cppのリビジョンではエラーが多発し動かなかったため、
リビジョンを変更しました。
② 生成トークンの NUL 文字除去
テキストを生成してみたら大量のNUL文字が発生しました。
NUL文字除去をしておかないとこのようにヌルヌルになるので除去処理を入れました。
(※NUL文字を可視化してます)

③ 使いたいLLMのストップトークンの追加
qwen系のLLMを扱いたかったのでqwen系で使われるストップトークンを追加しました。
使いたいLLMに合わせて設定しましょう。
※アプリ側からセットすることも可能です。
④ 独自の履歴管理
SwiftLlamaにはセッション内履歴管理機能がありますが、
1インスタンス=1セッション固定となっており、
複数チャットを切り替えると履歴が混ざってしまいます。
チャットごとに履歴を持たせたいため、
対策としてアプリ側にてセッションIDごとに[Chat]型へ整形して管理しています。
各ファイルの修正点(要約)💁
- Package.resolved
- 依存の
llama.cppリビジョンを更新:- 旧:
b6d6c5289f1c9c677657c380591201ddb210b649 - 新:
323951f1bdcdfbd5b5ff3a9a7c3770e63b1a560e
- 旧:
- Package.swift
-
LlamaFrameworkバイナリの参照を更新:- URL:
.../b5046/llama-b5046-xcframework.zip→.../b6264/llama-b6264-xcframework.zip -
checksumも対応する値に更新
- URL:
- Sources/SwiftLlama/LlamaModel.swift
-
import osを追加(subsystem: "SwiftLlama", category: "LlamaModel")。 - モデル/コンテキスト初期化の安全性向上:
- ロード失敗やコンテキスト作成失敗時に
llama_backend_free()/llama_free_model()/llama_free()を確実に解放。 - 学習時コンテキスト長と要求コンテキスト長の事前チェック追加。
- ロード失敗やコンテキスト作成失敗時に
-
batch初期化でConfiguration.historyLimitを考慮(旧Configuration.historySize依存を解消)。 - EOG 判定を API 変更に追随:
-
llama_token_is_eog(model, ...)→llama_vocab_is_eog(llama_model_get_vocab(model), ...)
-
- 生成トークンの NUL 文字を即時除去して蓄積(文字化け回避)。
-
llama_token_to_piece/llama_tokenizeを語彙 (llama_model_get_vocab(model)) ベース呼び出しへ更新。 -
clear()でllama_kv_cache_clear→llama_memory_clear(llama_get_memory(context), true)に変更。 - 協調停止用
stop()を追加。
- Sources/SwiftLlama/Models/Chat.swift
-
qwenPromptを追加(<|im_start|>...<|im_end|>形式)。
- Sources/SwiftLlama/Models/Configuration.swift
- フィールド追加:
debugLogTokens: Bool,historyLimit: Int - 旧
static let historySize = 5を廃止。historyLimitは初期化時にmax(0, ...)で正規化。
- Sources/SwiftLlama/Models/Prompt.swift
-
Typeに.qwenを追加。encodeQwenPrompt()実装を追加。 - 既存各プロンプトの履歴結合で
history.suffix(Configuration.historySize)を廃止し、呼び出し側でのトリミングに委譲(history.map { ... })。
- Sources/SwiftLlama/Models/StopToken.swift
-
qwen用のストップトークンを追加:"<|im_end|>"
- Sources/SwiftLlama/Swiftllama.swift
- プロンプト準備で
historyLimitに基づき履歴をトリミング(セッション有無を問わず)。 - ストリーム中の NUL 文字を逐次除去。
- ストップトークン処理を再設計:
- 空配列時はそのまま出力。
- 一定長ウィンドウで逐次判定し、マッチ時にトークン直前までを出力して停止。
- キャンセル協調:
- 生成ループで
Task.isCancelledを確認して早期終了。 -
start(for:)のAsyncThrowingStreamでcontinuation.onTerminationによるTaskキャンセルを連動。
- 生成ループで
- 終了処理の整理: 早期終了と通常終了で
finaliseOutput()の呼び分け、バッファクリアの扱いを明確化。
修正版Swiftllama
本家からForkして上記の修正点を加えたのはこちらです👇
LLMの呼び出し
HuggingFaceでお好きなLLM(.ggufファイル)をダウンロードしましょう。
ストリーミング処理でのLLMの呼び出しコード(Swift)はこちら👇
import SwiftLlama
// モデルの初期化
guard let swiftLlama = try? SwiftLlama(
modelPath: "path/to/your/model.gguf",
modelConfiguration: .init(
seed: 1234, // ランダムシード
topK: 40, // Top-K サンプリング
topP: 0.9, // Top-P サンプリング
nCTX: 2048, // コンテキストサイズ
temperature: 0.7, // 温度パラメータ(創造性)
batchSize: 2048, // バッチサイズ
maxTokenCount: 1024, // 最大生成トークン数
stopTokens: StopToken.llama, // 停止トークン(モデルによって異なる)
debugLogTokens: false, // デバッグログ
historyLimit: 5 // 履歴保持数
)
) else {
print("モデルの読み込みに失敗しました")
return
}
// プロンプトの作成
let prompt = Prompt(
type: .llama, // モデルタイプに応じて変更
systemPrompt: "あなたは役立つAIアシスタントです。",
userMessage: "SwiftUIでボタンを作る方法を教えてください"
)
// ストリーミング推論の実行
for try await token in await swiftLlama.start(
for: prompt,
sessionSupport: false
) {
print(token, terminator: "") // リアルタイムで出力
}
print("") // 改行
チャット動作
1.5Bモデルを動かしてみるとこんな感じ。
思ったより早く推論できてますね!
まとめ
iPhone上でLLMを動かしてみました。
動かせるのが〜3Bパラメータくらいのモデルなので性能はそこそこという感じで、
かつ、チャットを続けるとバッテリーがみるみる減っていくのが分かります😂
次はMLX形式やCoreML形式のモデルを動かして、性能を比較していきたいと思います!🔥
Discussion