Foundation Models:Tool calling の複数同時呼び出しができればいいのに

<追記:2025/8/26>
「できればいいのに」というタイトルだが、できることが分かったので、記事後半の参考記事や、記事下にあるピンバックを参照。


大規模なテキストベースである日記を Spotlight にインデクシングさせ、Foundation Models と組み合わせて何かできないか実験中。

前回で、ツール呼び出しで日記のテキスト(ダミー)を取得して、その内容をもとに回答できることは分かった。が、コンテクストウィンドウサイズの都合上、以下を考慮する必要があることも分かった。

  • 日記は全文でなく細分化してツールに渡すべき
  • 細分化したうえでも、ヒットした全件をツールに返すわけにはいかない

となると、1度の検索で回答に足る情報が得られるかは怪しい。自律的にキーワードを変えながら、少なくとも2、3度はモデルに検索してもらいたい。

そこで、インストラクションを以下のようにして、前回作成したダミーデータには存在しないエピソードについて尋ねることで、1どのやりとりで複数回同じツール呼び出しをするか実験してみた。

ユーザーは自分の日記について質問を投げかけます。あなたはその質問に答えるために、必ず以下のStepをすべて実行した上で答えてください。
Step1. 検索キーワードを考えます
Step2. Step1で考えたキーワードをもとに日記を検索します
Step3. Step2で得られた結果を確認し、質問に回答できる内容があればそれをもとに回答します。なければStep4に進みます
Step4. Step1とは別の検索キーワードを考えます
Step5. Step4で考えたキーワードをもとに日記を検索します
Step6. Step5で得られた結果を確認し、質問に回答できる内容があれば、それをもとに回答します。なければStep7に進みます
Step7. 該当する記録がないことをユーザーに答えて、終了です。

が、2度呼び出されることはなかった。

po session.transcript

Transcript(entries: [
(Instructions) ユーザーは自分の日記について質問を投げかけます。あなたはその質問に答えるために、必ず以下のStepをすべて実行した上で答えてください。
...省略, 
(Prompt) 壺を買ったのはいつ?
Response Format: Message, 
(ToolCalls) searchDiaryDatabase: {"queries": ["壺"]}, 
(ToolOutput searchDiaryDatabase) ["日付2025-04-08 21:54:00 +0000 本文: 鏡の前で前髪とにらめっこ。..省略.. 他愛ない近況を話しながら、帰り道の影まで軽くなった気分。"], 
(Response) {"text": "壺を買ったことは2025年04月08日の日記には記録されていません。"}
]

プロンプト上、強制的に2度検索するよう指示してもダメ。ちなみに、おなじ日記検索ツールを名前(searchDiaryDatabase/searchDiaryDatabase2/searchDiaryDatabase3)と説明文を変えた異なる定義を増やしたところ、それらを同時並行で呼び出すことは稀にあった。

つまり、ToolCalls→ToolOutput→ToolCalls→… という自律的な再呼び出しは、今のところできなさそう?


ChatGPT に聞いた。

iOS の Foundation Models で tool calling という機能がありますが、1回のレスポンスに対して同じツールを複数回呼び出すことはできるのでしょうか。たとえば、データベースを検索するtoolがあったとして、1度の検索で回答に必要な情報が揃わなかった場合、2度、3度とキーワードを変えながら検索し直す、など。

iOS の Foundation Models フレームワークにおける Tool Calling(ツール呼び出し)では、1 回のレスポンス(session.respond(…) の呼び出し)に対して、モデルが“同じツールを複数回呼び出す”ことが可能です。

実際の動作例として:

  • Medium によるデモでは、「Pikachu と Bulbasaur に“love”を送って」というプロンプトに対し、モデルが同じツールを 2回呼び出し、それぞれのキャラクターに処理を行っている様子が確認できます  。
  • また、「モデルは複数のツールを並行して呼び出すこともあり、特定の都市ごとに天気情報を取得するような場合には、同じツールを複数回呼ぶ可能性がある」と明記されています  。

このように、同一のツールでも、モデルの判断によって必要に応じて複数回呼び出され、各呼び出しの結果がセッション内のトランスクリプトに含まれて、最終レスポンスに組み込まれます。

動作例で参照しているふたつの事例は、同じ Medium 記事だった。

SwiftUI: Demystify Foundation Model in SUPER Detail! With a Chat App! | by Itsuki | Jun, 2025 | Level Up Coding

The model decides to whether to use a tool (or multiple tools) or not, which tool to use, how many times it should use it. (That is the model can call a tool multiple times in parallel to satisfy the request, like when retrieving weather details for several cities.)

As we can see here, the model decided that it needed to call my tool twice, first time with the name Pikachu, and second time with Bulbasaur.

まさにこういうことをしたいのだけど。つまり、できるっちゃできるのか?


ちなみに、この記事にある PromptBuilder というのは知らなかった。条件付きなど動的なプロンプトを生成できるらしい?

Foundation Models の RAG に Core Spotlight を活用できそうか超簡単な実験:その2

大規模なテキストベースである日記と、ローカルLLMとを組み合わせて何かできないか、Foundation Models で実験する試みの、前回の続き。

Spotlight に日記の文章をインデクシングできたとして、その情報をうまいこと Foundation Models の tool calling で引き出すことができるのか。仮に「散髪を最後にしたのはいつ?」という質問を想定して、モックデータをもとに検証してみた。

  1. Foundation Models の tool calling の挙動を確認
    • タイミング、回数、使うキーワード、日本語でも可能か など)
  2. ツールから得た日記本文を回答に活用できるか
    • 本文のように長い文章を読んで解釈してくれるか/一問一答でピンポイントな情報がツールから得られる必要があるか
    • 複数日分の本文が返っても、区別して情報処理できるか

ツールから得られる情報の処理の仕方が知りたいので、今回は一旦常に同じデータを返すようにする。

import FoundationModels
import Foundation

struct DiarySearchTool: Tool {
    let name = "searchDiaryDatabase"
    let description = "検索キーワードで日記のエントリーを検索する"
    
    @Generable
    struct Arguments {
        @Guide(description: "検索キーワード")
        var queries: [String]
    }
    
    
    struct Entry {
        var date: Date
        var content: String
    }
    
    func call(arguments: Arguments) async throws -> [String] {
        // Foundation Modelsがtool callingしたか/どんなクエリで検索しようとしたか出力
        print("### augumantes: \(arguments)") 
        
        let entries = dummyEntries // 常に同じモックデータを返す
        let formattedEntries = entries.map {
            "日付\($0.date) 本文: \($0.content)"
        }
        return formattedEntries
    }
}

モックデータとして3日分、それぞれ以下を取り入れるよう ChatGPT に考えさせた。これは、単純に「散髪」というキーワードに反応するだけでなく、日記の中身をモデルが理解した上でレスポンスするかを確かめるため。

1. 散髪に行こうか迷っている
2. 散髪に行った
3. 散髪に行ったことを友達に気がついてもらえた

let dummyEntries: [Entry] = [
    .init(date: "2025-07-18 20:13:00".toDate()!,
          content:
              """
              鏡の前で前髪とにらめっこ。伸ばすか切るか、天秤は揺れっぱなし。
              散髪に行こうか迷っているうちにコーヒーは冷め、予定は延び、前髪だけが視界を奪う。
              雨予報も背中を押さず、今日は見送るか、どうするか。
              """),
    .init(date: "2025-06-09 17:02:00".toDate()!,
          content:
              """
              雲の切れ間みたいに決心が差した午後、散髪に行った。
              軽くなった頭皮に風が通る。落ちた髪の山を見下ろし、スマホの自撮りで角度を研究。
              鏡越しの自分と握手。担当さんのハサミとシャンプーの柑橘の香り。肩の荷まで落ちた気がする。
              """),
    .init(date: "2025-04-08 21:54:00".toDate()!,
          content:
              """
              駅前で友達と合流。開口一番「なんか爽やか!」と笑われ、
              散髪に行ったことを友達に気がついてもらえた。前髪の軽さだけでなく、
              歩幅まで半歩分広がった気がする。他愛ない近況を話しながら、帰り道の影まで軽くなった気分。
              """),
]

なんかムズムズする日記、、笑
プロンプトを与えて、いよいよ Foundation Models に質問してみる。

let session = LanguageModelSession(
    tools: [DiarySearchTool()],
    instructions:
        "ユーザーは自分の日記について質問を投げかけます。あなたは関連するキーワードを使用して日記のデータを検索し、その質問に回答します。"
)

予想以上にうまく答えられている。内容を理解した上で答えていそうなことはわかった。

以下ログを見ると、モデルがツールに渡している検索キーワードが、質問文そのままだったりしてイマイチなので、ここはプロンプトなどで調整の余地がありそう。(あるいは Core Spotlight のセマンティックサーチが吸収してくれるかも?)

Send message: 最後に髪を切ったのはいつですか?
### augumantes: Arguments(queries: ["髪を切ったのはいつですか?"])
PartiallyGenerated(id: FoundationModels.GenerationID(value: ..., text: Optional("最後に髪を切ったのは2025年6月9日の午後です。"))

Send message: 散髪を友達に気づいてもらえたのはいつですか?
### augumantes: Arguments(queries: ["散髪", "友達に気づいてもらえた"])
PartiallyGenerated(id: FoundationModels.GenerationID(value: ..., text: Optional("散髪を友達に気づいてもらったのは日付2025-07-18です。"))

Send message: 散髪を迷っていたのはいつですか?
### augumantes: Arguments(queries: ["散髪を迷っていたのはいつですか?"])
PartiallyGenerated(id: FoundationModels.GenerationID(value: ..., text: Optional("散髪を迷っていたのは2025年4月8日のことです。"))

最後に、ここまででいくつか失敗があったのでまとめておく。

Tool calling 経由で大規模な文章が渡るとコンテクストウィンドウ超過になる

予想はしていたがやはりそうだった。初めは、筆者の日記の実データを用いて検証したのだが、1日分だけで2千〜5千文字近くあり、余裕で “Exeeded models context window size” エラーとなった。

これは、たとえば長文の日記本文を小分けにして Spotlight にインデクシングさせることで解決するかもしれない。

指示文が不十分だとうまくtool calling してくれない

当初、インストラクション、ツール説明、プロンプトは以下のように記述していた。

struct DiarySearchTool: Tool {
    let name = "searchDiaryDatabase"
    let description = "日記のエントリを条件に従って検索します"


    @Generable
    struct Arguments {
        @Guide(description: "検索キーワード")
        var queries: [String]
    }
...

let session = LanguageModelSession(
    tools: [DiarySearchTool()],
    instructions: "あなたは日記データをもとにユーザーからの質問に答える役をします。"
)

しかし以下の通り、珍紛漢紛な返答ばかりで、そもそもツールを呼び出していないことが判明。最終的に、`searchDiaryDatabase` というツールを認識しているか、という直接的な質問をして、はじめて「AI」という謎キーワードでツールを呼び出すようになった。

Send message: 最後に散髪に行ったのはいつ?
... // ←ツールを呼び出していない
PartiallyGenerated(id: FoundationModels.GenerationID(value: ..., text: Optional("最後に散髪に行ったのは2024年7月15日のことです。"))

Send message: 日記から最後に散髪に行った日のエントリーを検索して、その日付を教えてください。
... // ←ツールを呼び出していない
PartiallyGenerated(id: FoundationModels.GenerationID(value: ..., text: Optional("検索結果は以下の通りです。最後に散髪に行った日に関するエントリーは存在しません。"))

Send message: searchDiaryDatabase というツールを使って、最後に散髪へ行った日を検索して、その日付を教えてください。
... // ←ツールを呼び出していない
PartiallyGenerated(id: FoundationModels.GenerationID(value: "E042ADEE-3C46-424C-9B2F-DD572136FC2B"), text: Optional("散髪に行く日についての情報は取得できませんでした。"))

Send message: searchDiaryDatabase というツールを認識していますか?
### augumantes: Arguments(queries: ["AI"]) // ←ツールを呼び出した
...
PartiallyGenerated(id: FoundationModels.GenerationID(value: ..., text: Optional("以下に日記のエントリを3つ返します。\n1. 日付2025-07-18 20:13:00 +0000 本文: 鏡の前で前髪とにらめっこ。伸ばすか切るか、天秤は揺れっぱなし。散髪に行こうか迷っているうちにコーヒーは冷め、予定は延び、前髪だけが視界を奪う。雨予報も背中を押さず、今日は見送るか、どうするか。\n2. 日付2025-06-09 17:02:00 +0000 本文: 雲の切れ間みたいに決心が差した午後、散髪に行った。軽くなった頭皮に風が通る。落ちた髪の山を見下ろし、スマホの自撮りで角度を研究。鏡越しの自分と握手。担当さんのハサミとシャンプーの柑橘の香り。肩の荷まで落ちた気がする。\n3. 日付2025-06-08 21:54:00 +0000 本文: 駅前で友達と合流。開口一番「なんか爽やか!」と笑われ、散髪に行ったことを友達に気がついてもらえた。前髪の軽さだけでなく、歩幅まで半歩分広がった気がする。他愛ない近況を話しながら、帰り道の影まで軽くなった気分。"))

インストラクション、プロンプトを英語に直したことで期待通りにツールを呼び出すようになったので、日本語と英語の違いか、と思ったがその後この英語をそのまま日本語に訳して適用したところ、問題なくレスポンスしてくれるようになった。やはりプロンプトの設計が重要。

OS 26 beta 5 の Foundation Models framework アップデート内容

Foundation Models の開発者からこんな投稿があったのでチェック。


  • ガードレイルに permissiveContentTransformations オプションが追加
    • 要約やスタイル調整といったユースケースにおいて原文にセンシティブなトピックを含む可能性があるケースを想定(政治記事など)
    • 含んでいてもテキスト応答にへ関することを許容し、LanguageModelSession.GenerationError.guardrailViolation エラーを投げないガードレイル設定
    • permissiveContentTransformations | Apple Developer Documentation
  • 新しい「拒否」エラーの追加
  • #Praiground 使用時にフィードバックボタンを追加
    • Xcode 上で直接、モデル動作に関するフィードバックを送信可能に
  • GeneratedContent を透明化(“no longer opaque”
    • GeneratedContent.Kind が追加され、任意の生成コンテンツを表示するビューの構築が可能
    • プリミティブ型、配列、構造化オブジェクトを含む、JSON互換のデータ種類を表す
      • .array([GeneratedContent]), .bool(Bool), .number(Double), .string(String), .structure(properties: [String.: GeneratedContent], orderedKeys: [String]), .null
    • ユーザーが設定可能な生成スキーマの取り扱いに有用
  • 推論スタックの最適化が含まれており、トークンの処理速度が向上

Foundation Models の RAG に Core Spotlight を活用できそうか超簡単な実験

WWDC24:Support semantic search with Core Spotlight で記したように、日記データを処理するオンデバイス LLM の実験のために Foundation Models の活用を検討しており、エントリ検索のための RAG として Core Spotlight が使えないか考えている。

ユーザー質問を受けた Foundation Models 側で、適切なクエリを生成できることが分かれば何かは作りだせそう。しかし以前の反省のとおりいきなりモデルの実装をし始めるのは悪手。まずは Foundation Models に対し自然言語でやりとりをし、狙う用途を満たす知識や能力があるかを先に検証するべき。

ということで、次のプロンプトを投げてみた。

あなたはCore Spotlightにインデックスされた日記データをもとにユーザーからの質問に答える役をします。Core Spotlightから情報を引き出すためには、ユーザーの質問を解釈し、Core Spotlightに投げるクエリを考える必要があります。ユーザーから「最後に散髪したのはいつ?」と尋ねられたら、どんなクエリを投げますか?

返ってきた答えは以下。

「最後に散髪したのはいつ?」という質問に対して、Core Spotlightに投げるクエリを考えるには、以下のようなアプローチが考えられます。このクエリは、ユーザーの行動履歴や、関連するキーワードを考慮して作成されます。

```plaintext
Core Spotlight Query:

- Type: Query
- Language: Natural Language
- Query: 最後に散髪したのはいつ?

- Context:
  - Keywords: 散髪、カット、美容室、ヘアスタイル、ヘアサロン
  - Date Range: 最近までの履歴を考慮

- Filters:
  - Category: Personal Care
  - Action: Haircut
  - Location: Salon/Barber Shop
  - Date: Last occurrence

- Constraints:
  - Language: Japanese
  - Language Preference: Japanese

```

このクエリは、ユーザーの行動履歴を基に「散髪」に関連するキーワードを特定し、最近までの履歴を考慮して「最後に散髪したのはいつ?」という質問に応答します。また、日付範囲を最近のものに限定し、具体的な場所やアクションを強調することで、より正確な情報 retrieval を促進します。

Context / Filters / Constraints をクエリ設計に含めているあたり、同じ Apple の技術である Core Spotlight のコンセプトを理解できているのかは怪しいが、少なくとも質問内容から的確にキーワード抽出ができそうなことは分かった。(まあここはさすがにLLMの得意分野だろう、、)

WWDC24:Support semantic search with Core Spotlight

日記データとローカルLLMを組み合わせて何かできないか考えているが、Foundation Models の adapter training は日々更新される情報の追加学習には向いていない(し、そもそもユーザー生成データを学習させることもできない)。文書データと相性が良さそうな RAG を iOS でうまいこと活用できないか ChatGPT に壁打ちしていたら、Core Spotlight によるセマンティック検索が有効と教えてくれたので、去年の WWDC セッションビデオを見てみた。

完全一致でなくともある程度文脈を汲み取ったコンテンツ検索ができるようで、さすが Spotlight 技術なのでバッチ更新も可能だし、Core Spotlight でアプリ内から検索処理が行えるのなら、Foundation Models に tool calling 経由で自律的に検索させることができるだろうか?


0:00 – Introduction

  • CoreSpotlight フレームワークでアプリ内に強力な検索体験を構築する新しい API を紹介
  • セマンティック検索により、意味的に類似した検索語でも適切な結果を取得可能
  • 完全にローカルでプライベートな検索インデックスを提供
  • ジャーナルのエントリ検索アプリを例に、検索体験の構築方法を説明

1:37 – Searchable content

  • 検索可能なコンテンツを設計 → 検索インデックスを構築 → Spotlight に提供
  • CSSearchableItemuniqueIdentifier, domainIdentifier, attributeSet を設定
  • CSSearchableAttributeSetcontentType, title, textContent, contentURL を設定し、セマンティック検索に最適化
    • アイテムが画像、動画アセットの場合は、アセットへのパスを contentURL に指定
    • 添付ファイルやWebコンテンツへの参照は、独自のコンテンツタイプと属性をインデクスに提供
    • ソースアイテムとの関係に relateUniqueIdentifier を指定
  • バッチインデックス作成と client state で効率的な Spotlight への提供を実現
  • item.isUpdate を使い、すでにインデックス済みの属性上書きを防ぎ、必要なものに限り提供可能

4:10 – Migration and Recovery

  • 移行と復元:一貫性を維持するために重要
  • なんらかの問題で移行と復元を要する場合、Spotlight がアプリに際インデックス化を要求
  • CoreSpotlight Delegate extension で Spotlight からの再インデックス要求に対応
    • Spotlight はアプリとは別にこの Extension を呼び出すことができる(デバイスがアイドル状態の時にバックグラウンドで再インデックスを実行)
  • mdutil ツールでデバッグとテストが可能

6:56 – Results and suggestions

  • CSUserQueryContext でクエリを設定し、セマンティック検索を有効化
  • ランク付けされた結果とサジェスションを取得、最大結果数も指定可能
  • メタデータ構文でフィルタクエリを構築し、結果をカスタマイズ
    • 検索結果をUIにに合わせる最適な手法(e.g. 画像のみ表示時は画像のみとするようクエリ指定)
  • 検索バーからのクエリ文字列とクエリコンテクストから CSUserQuery を生成
  • 非同期バッチで結果を取得し、compareByRank でソート
    • 候補(CSSuggestion)は入力文字列の補完として返却
  • セマンティック検索はプロセスに必要な学習モデルのダウンロードが必要
    • ロード・アンロードでメモリ容量を節約(CSUserQuery.prepare, .prepareWithProtectionClasses を検索UI表示時に実行)

9:18 – Ranking

  • エンゲージメントと鮮度のシグナルでランキングを改善
  • lastUsedDate プロパティでコンテンツの使用頻度を記録
  • ユーザーの検索行動(閲覧、スクロール、詳細表示)を追跡
  • 適応的なランキング体験でユーザーが最も関心を持つコンテンツを優先表示

10:17 – Wrap-up

  • CoreSpotlight で完全にプライベートなオンデバイス検索インデックスを構築
  • セマンティック検索により、より強力で直感的な検索体験を提供
  • 複数プラットフォームとロケールに対応した多様なコンテンツ検索を実現
  • App Intents や CoreData との統合でさらなる検索機能を拡張

Foundation Models adapter training toolkit を試してみたが

結論から言えば、実行環境がツールキットの必要条件を満たしておらず失敗した。

Foundation Models adapter training – Apple Intelligence – Apple Developer

このページに説明がある通り、adapter training toolkit が必要とするマシンスペックはメモリ32GB。対して筆者私用の MacBook Air (M2) は16GBしか積んでいない、、

Requirements

  • Mac with Apple silicon and at least 32GB memory, or Linux GPU machines
  • Python 3.11 or later

ツールキットのセットアップ自体は、ページの手順に従って難なく完了したのだが、テストコマンド実行で失敗した。

adapter_training_toolkit % python -m examples.generate --prompt "Prompt here"
Traceback (most recent call last):

(省略)

RuntimeError: MPS backend out of memory (MPS allocated: 18.13 GB, other allocations: 384.00 KB, max allowed: 18.13 GB). Tried to allocate 16.00 MB on private pool. Use PYTORCH_MPS_HIGH_WATERMARK_RATIO=0.0 to disable upper limit for memory allocations (may cause system failure).

ChatGPT 先生によると、

メモリ 32 GB 推奨という前提に対して物理メモリ 16 GB の Mac で動かしている ことが、今回の MPS out-of-memory エラーの根本原因と考えてかまいません。

要素おおよそのメモリ消費
7 B パラメータ級モデル (FP16)7 B × 2 bytes ≒ 14 GB
ロード直後の重複バッファ(重み再配置や勾配用の一時領域)+2 〜 4 GB
トークン埋め込み/ KV キャッシュ(推論時)数百 MB 〜 数 GB (入力長/バッチで変動)
Python / PyTorch 自身・OS・他プロセス2 〜 4 GB

16 GB machines の場合、Unified Memory 全体が 16 GB = GPU が占有できる上限も 16 GB 未満 です。

PyTorch-MPS の既定 “high-watermark” が 90 % なので、確保できるのは最大でも ≈ 14.4 GB 程度。

上の表の時点でオーバーしており、転送途中でエラーになります。


Mac Studio 買うか、、

Foundation Models は追加学習できるらしい

Foundation Models について、セッションメモや実践についていくつかまとめてきた。

これらを通して筆者は、Foundation Models のコンセプトは Apple の用意したモデルを受動的に使うもので、追加学習する方法は提供されていないものだと理解していた。実際、Foundation Models の API には、外部情報と連携する tool calling の仕組みはあっても、モデルをファインチューニングする手段は提供されていない。

それが、先日参加した WWDC25 Recap イベントにて、近くの方から「できる」という情報をいただいて驚いた。その後、Apple の Technology Evangelist の方にも確認したところ、やはりできるとのこと。以下のページにその手順がまとまっていると教えてくださった。

Foundation Models adapter training – Apple Intelligence – Apple Developer

Adapters are an advanced technique that *adapt* a large language model (LLM) with new skills or domains. With the adapter training toolkit, you can train adapters to specialize the on-device system LLM’s abilities, and then use your adapter with the Foundation Models framework.

トレーニングには Adapter training toolkit なるものを用いるらしい。ローカルマシンと Python コードでトレーニングし、別途 Entitlement をリクエストする必要がある。また、マシンスペックもメモリ32GB以上とそれなり。事前学習させたモデルをアプリに組み込むので、例えばユーザー生成コンテンツをもとに Foundation Models のレスポンスを生成することはできないだろう。

Foundation Models は OS と共にバージョンアップされ続けるが、聞いた話ではその都度学習がリセットされ、その度に再学習させ直す必要があるとのこと。


ちなみに、このアダプターについてはセッション内に言及あったようで、それをいちからビデオを見直して探すのは大変なので、これまで見たセッションの transcript を溜めていたフォルダから Cursor に探させた。その答えが以下。

「Meet the Foundation Models framework」セッション

  • 該当箇所: 3:47〜3:57付近
  • 内容要約:
  • 「特定のユースケース(例:コンテンツタグ付け)向けに、モデルの能力を最大化するためのspecialized adapters(専用アダプター)を提供している」と明記。
  • さらに「今後もモデルを継続的に改善していく」と述べており、アダプターを使った拡張性やカスタマイズ性に触れています。
  • 原文抜粋:

> For certain common use cases, such as content tagging, we also provide specialized adapters that maximize the model’s capability in specific domains.

ついでに ChatGPT o3 に解説させた。

https://chatgpt.com/share/686d278b-cf78-8005-a273-061117f3d216

WWDC25:Explore large language models on Apple Silicon with MLX

以前 Foundation Models framework を実際に使ってみたという投稿をした。

Foundation Models framework の最大のメリットはオンデバイスのLLMを非常に手軽に扱えるという点だが、一方で高度な推論に不向きであったり、独自データによる追加学習といった高度なユースケースには対応していない → できるらしい。(追記: 2025/07/08)オンデバイスかつ手軽に追加学習を行いたい場合には、このMLXを使えばよさそう。


0:00 – イントロダクション

  • MLX は Apple Silicon 向けに設計されたオープンソースの機械学習ライブラリ
  • Apple Silicon 上で大規模言語モデル(LLM)の推論とファインチューニングを実行可能
  • Metal を使用して GPU で高速化し、統一メモリを活用して CPU と GPU が同時に同じデータで動作可能
  • Python、Swift、C++、C の API を提供
  • DeepSeek AI の6700億パラメータモデルを4.5ビット量子化で380GBのメモリを使用して実行可能(M3 Ultraの512GB統一メモリを活用)

3:07 – MLX LMの概要

  • MLX LM は MLX 上に構築された Python パッケージで、大規模言語モデルの実行と実験に特化
  • コマンドラインツールを提供し、コードを書かずにテキスト生成やファインチューニングが可能
  • Python API も提供し、生成やトレーニングプロセスなど詳細な制御を実装可能
  • Hugging Face と密接に統合されており、数千のモデルをダウンロード可能
  • インストールは pip install mlx-lm で簡単

3:51 – テキストの生成

  • コマンドライン:
    • mlx_lm.generate コマンドで Hugging Face のモデルまたはローカルパス、テキストプロンプトを指定して生成
    • サンプリング温度、top-p、最大トークン数などのフラグで動作を調整可能
    • mlx_lm.generate —help
  • Python API:
    • load 関数でモデルとトークナイザーを読み込み、設定
    • generate 関数でトークン生成ループを実行し、出力テキストを返す
    • モデルは完全に構造化された MLX ニューラルネットワークで、層の構造やパラメータを検査・修正可能
      • print(model)
      • print(model.parameters()) で学習した重みづけを確認
  • 会話の維持:
    • Key-Value Cache(KV Cache)を使用して複数ターンの会話を中間結果として維持
      • キャッシュを再利用し、時間と処理を節約、マルチターンの会話に有効
    • make_prompt_cache 関数でキャッシュを作成し、generate 関数に渡すことで履歴を保持

8:42 – 量子化

  • 目的: モデルの精度を落とすことでメモリ使用量削減と推論速度向上(多くは品質に影響しない)
  • MLX LMの利点: 量子化が組み込まれており、追加ツールや変換スクリプトが不要
  • 使用方法:
    • mlx_lm.convertコマンドで Hugging Face からモデルをダウンロード、変換、ローカル保存を1ステップ実行
    • 例:  16ビットMistralモデルを4ビットに量子化して大幅にサイズ削減
  • Python APIでの細かい制御:
    • quant_predicate を使用。例: 埋め込み層と最終投影層を6ビット、その他を4ビットに設定可能
      • 最後の埋め込み投射層は量子化の影響を受けやすいため高精度を維持したい
    • 品質と効率のバランスを最適化

11:39 – ファインチューニング

  • 目的: 大規模言語モデルを特定のドメインやタスクに適応させる
  • MLXの利点: クラウド不要でローカルMac上でファインチューニング可能
    • 外部で実行するコストがかからない
    • データがデバイスから出ない
  • 2つのアプローチ:
    • 完全ファインチューニング: 事前学習済みモデルの全パラメータを更新。最大の柔軟性だがリソース消費量増える
    • 低ランクアダプター(LoRA): 少数の新しいパラメータを追加し、元のネットワークを凍結したままそれらのみをトレーニング。高速、軽量、メモリ効率的
  • 実装:
    • mlx_lm.lora コマンドでひとつでファインチューニング開始
    • 量子化されたモデルのアダプタをトレーニング可能(メモリ使用量を劇的削減)
    • 設定ファイルで学習率、オプティマイザー設定、評価間隔などを細かく制御
  • アダプターの融合:
    • mlx_lm.fuse コマンドでアダプターをベースモデルに融合
    • 単一の自己完結型モデルを生成し、配布と使用が容易
      • --upload-repo に Hugging Face のリポジトリ名を指定するだけでアップロード可能

17:02 – MLXSwiftでのLLM

  • Swiftでの実装: 例: 28行のコードで量子化Mistralモデルを読み込み、テキスト生成を実行
  • 主要コンポーネント:
    • ModelContainer: モデルとトークナイザーへの並行アクセスを安全に管理するアクター
    • プロンプトのトークン化
    • 生成ループの実行
  • 会話履歴の維持:
    • Key-Value Cacheを明示的に作成。
      • GenerateParameters の生成
      • キャッシュの抽出
      • TokenIterator を使用して生成をステップバイステップで制御
  • 利点: Pythonと同じワークフローと機能を、Swiftで完全にネイティブに実現

Foundation Models でオセロを作ってみたが、、

これの続き。型安全に構造化したデータを出力できる特徴を活かして何かできないか?と選んだテーマがオセロだったが、結果的にはゲームが成立しなかった。そもそもFoundation Modelsで一番最初に作るモノとしては適切でなかったかもしれない笑

紆余曲折経て、最終的に以下のようなデータモデルになった。

@Generable(description: "Represents the current state of an Othello game.")
struct OthelloGame {
    @Generable(description: "Represents the coordinates of a disc. Expressed as 'x:y' in string format (e.g., a:5).")
    struct DiscCoordinate {
        @Generable(description: "The X coordinate of the disc (a...h).")
        enum XCoordinate: String {
            case a, b, c, d, e, f, g, h
        }
        @Generable(description: "The Y coordinate of the disc (1...8).")
        enum YCoordinate: String {
            case `1`, `2`, `3`, `4`, `5`, `6`, `7`, `8`
        }
        
        @Guide(description: "The X position of the disc.")
        var x: XCoordinate
        @Guide(description: "The Y position of the disc.")
        var y: YCoordinate
    }
    
    @Guide(description: "Coordinates occupied by the white player.")
    var whiteDiscs: [DiscCoordinate] = [.init(.d, .`4`), .init(.e, .`5`)]
    
    @Guide(description: "Coordinates occupied by the black player.")
    var blackDiscs: [DiscCoordinate] = [.init(.e, .`4`), .init(.d, .`5`)]
    
    @Guide(description: "A message shown to the opponent based on the current state of the game when you make a move.")
    var message: String
}

うまくいかなかったというのは具体的には、初期状態の盤からゲームを進めるたびに最新の盤を返すよう instruction に指示していたがそれが不完全だったり、あり得ない場所に石を置いたりするといった感じだ。

ちなみにChatGPTだと次のように、間違うことはあれど一定成立するので、広義にLLMがオセロをできない、というわけではない。

たかだか2時間程度の試行錯誤なので、もっと頑張れば精度上げられたかもしれないが、大前提として以下のような向き不向きがあると思いやめた。

現状の Foundation Models 自体がオセロゲームを正確に扱えない

構造化した出力だけでなく、ChatGPT と同様に自然言語的なやりとりでも試みてみたが、初手から出力精度は ChatGPT 4o にはるかに下がる傾向があり、オンデバイスという特徴であったり、Foundation Models 自体の学習内容からして、オセロ自体に向いていないのではと考えている。

構造化させすぎることでコンテクスト上限を容易に超えてしまう

ゲームの向き不向き以外にも、数ターンでコンテクストの上限を迎える問題があった。検証してはいないのであくまで仮説だが、上述のように @Generable を構造化した分、レスポンスに要するトークンは増大すると推測している(データモデルを表す文字列が長くなるため)。もちろんゲームの特徴上、本来は会話のように過去の履歴を残す必要はなく、最後の盤とターンだけを引き継いだセッションを再生成すれば良いだろうが。


こうしたことからなんとなく掴めたことがふたつある。まずは、Foundation Models の得意分野としては、過度な構造化が不要な自然言語を軸としたユースケースで(素直に)活用するのが良いと思っている。また、構造化するしないを置いておいても、省トークンの工夫として型の命名などに気を付ける必要があるかもしれない。

次に、いきなり @Generable といったデータモデルを設計実装する前に、まず自然言語で対話してみて、ユースケースを満たす性能を有しているか、事前検証してみるのが良いと思った。Playgournd でも良いし、チャットアプリ作ってでも良い。Foundation Models が LLM をアプリに組み込みやすくしてくれているおかげで、チャットレベルであれば30分もかからず実装することができる。

Foundation Models framework を触ってみたが、、

以前紹介した Foundation Models framework を触ってみたが、シミューレタ起動すると例外が発生した。

let session = LanguageModelSession()
let response = try await session.respond(to: "What's a good name for a trip to Japan? Reply only with a title")
print("response: \(response)")
Passing along InferenceError::inferenceFailed::Error Domain=com.apple.UnifiedAssetFramework Code=5000 "There are no underlying assets (neither atomic instance nor asset roots) for consistency token for asset set com.apple.modelcatalog" UserInfo={NSLocalizedFailureReason=There are no underlying assets (neither atomic instance nor asset roots) for consistency token for asset set com.apple.modelcatalog} in response to ExecuteRequest

調べてみると、

Your app runs on iOS / iPadOS / visionOS / macOS 26, with Apple Intelligence enabled.

If you use a simulator, be sure that your Mac is on macOS 26, with Apple Intelligence enabled.

Apple Intelligence is available for your system language and region. If not sure, set the system language of your device to English and the region to United States.

Always check the availability when using Apple Foundation Models, as demonstrated in the Apple sample.

https://developer.apple.com/forums/thread/787445

なるほど、macOS は Sequoia 15.5 のままだったので、M2 搭載 Mac かつ Apple Intelligence 有効でも、動作要件を満たせていないのだった。(どこかのセッションで触れてた気がする)

というわけで、iPadOS 26 にアップデートした M1 搭載 iPad でビルドしたら、無事結果を得ることができた。

response: Optional(FoundationModels.LanguageModelSession.Response<Swift.String>(userPrompt: "What\'s a good name for a trip to Japan? Reply only with a title", duration: 5.951339542, content: "\"Samurai Sojourn in the Rising Sun\"", transcriptEntries: ArraySlice([(Response) "Samurai Sojourn in the Rising Sun"])))

Foundation Models のネタとして、何ができるだろうと考えていたのだが、オセロゲームの実装を閃いた。もちろんオセロ自体、ChatGPTと対戦することもできるだろうが、構造化した結果を保証する Foundation Models の強みが活かせそうだと思ったのと、ストリーム方式の結果出力に同期して状態更新を行ってみる、良いサンプルにもなりそうだからだ。

しかしChatGPT相手のオセロってどんな感じなんだろうと、ひとつ試してみたが、出力結果がブレたり、ルールガン無視したり、打てる手を否定してきたりとなかなかカオスだった笑

https://chatgpt.com/share/68594389-f13c-8005-bb90-78b4f628e1ae

これに比べて、Foundation Models framework のモデルがどれくらい精度高い/低いのか気になる。

This website stores cookies on your computer. These cookies are used to provide a more personalized experience and to track your whereabouts around our website in compliance with the European General Data Protection Regulation. If you decide to to opt-out of any future tracking, a cookie will be setup in your browser to remember this choice for one year.

Accept or Deny