🥨

llama.cppをSwiftLlamaで動かす on iPhone

に公開

最近ローカルLLM、オンデバイスLLMって流行ってますよね?

特にスマートフォンで動かすことがアツいと思っており、
iPhoneにてllama.cppSwiftLlamaを使って
ローカルLLMを動かしてみました🔥

llama.cppとはC/C++で書かれたLLM推論を行えるライブラリです👌
Apple Siliconにも対応しています。
また、XCFrameworkが使えるのでコンパイルすることなく
Swiftプロジェクトで扱うことが可能です👇
https://github.com/ggml-org/llama.cpp

さらに、今回はllama.cppをSwiftで簡単に扱うため、SwiftLlamaを使います😋
SwiftLlamaは、Swiftでより自然で使いやすいAPIを提供するパッケージで、
llama.cppの機能をSwiftから簡単に呼び出せるようにしたものです👇
https://github.com/ShenghaiWang/SwiftLlama

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 も対応する値に更新

- 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_clearllama_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:)AsyncThrowingStreamcontinuation.onTermination による Task キャンセルを連動。
  • 終了処理の整理: 早期終了と通常終了で finaliseOutput() の呼び分け、バッファクリアの扱いを明確化。

修正版Swiftllama

本家からForkして上記の修正点を加えたのはこちらです👇
https://github.com/shusuke07/SwiftLlama_32

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モデルを動かしてみるとこんな感じ。
思ったより早く推論できてますね!
https://x.com/shusukehashimot/status/1973038055373287496?s=61

まとめ

iPhone上でLLMを動かしてみました。
動かせるのが〜3Bパラメータくらいのモデルなので性能はそこそこという感じで、
かつ、チャットを続けるとバッテリーがみるみる減っていくのが分かります😂

次はMLX形式やCoreML形式のモデルを動かして、性能を比較していきたいと思います!🔥

Discussion