前回の記事 で、M1 Pro / 16GB の Mac 環境において TurboFieldfare を用い、26B クラスの MoE モデル(Gemma 4 26B-A4B)を常駐 RAM 1.5GB・スワップ増加ゼロで動作させた検証記録を書きました。

しかし、その裏で一つ大きな謎が残っていました。同じ入力プロンプトを 16 回連続で実行した際、推論速度が 4.298〜11.884 tok/s (最速と最遅で 2.77倍)と大きくブレた現象です。前回の記事で、私は 「I/O側は未計測」 と書き残しました。

今回はその伏線を回収する「解答編」です。物理スワップ(書き込み)が増えていないなら、SSD からの読み出し(ディスクリートリード)側で何が起きているのか。macOS 標準のプロファイリングツールである iostat を用いて、推論中の物理ディスクの動きを追った実測データの解剖記録です。

先に結論:スワップゼロの裏で、ディスクは悲鳴を上げていた

結論から言うと、 メモリの約束が守られた裏で、SSD ディスクリートリードが物理的なボトルネックになっていました。

前作の検証どおり、 peak RSS は 1,521.5〜1,551.3 MB に収まり、 vm.swapusage の増加もゼロでした。しかし、推論(decode)処理中のデバイス層(disk0)を観測したところ、生成開始から終了までの全期間にわたり、 平均 891.8MB/s〜1,510.0MB/s(最大 2,153.1MB/s 超) という激しい連続読み込みが継続していたことが判明しました。

さらに衝撃的だったのは、生成中の CPU idle 率が 平均 77.4〜80.8%(8コア全体のシステム平均値)と高止まりしていた点です。

つまり、計算ユニット(CPUやGPU)が演算で飽和して遅くなっているのではなく、 「計算器は暇を持て余しており、SSD から Expert の重みが届くのをひたすら待っていた」 のが速度ブレとボトルネックの正体でした。

実機計測環境と COMMUNITY_BENCHMARKS 準拠プロトコル

計測は、前作と同じ Mac 実機環境および公開されているコミュニティのベンチマーク手順12に乗せて実施しました。

計測機材・仕様

  • 機材: MacBookPro18,3 (Apple M1 Pro 8コア CPU [6P+2E] / 14コア GPU / 16GB ユニファイドメモリ / 内蔵 NVMe SSD disk0 500.3GB)
  • OS / ツールチェーン: macOS 26.5.2 (25F84) / Swift 6.3.3
  • 検証コードベース: TurboFieldfare (検証用 Git コミット add22ff)1
  • 使用モデル: Gemma 4 26B-A4B IT 4-bit(scratch/gemma4.gturbo 配下 37 ファイル、計 14,291,915,755 バイト、manifest_sha256: 1cb53c...

計測手順とプロトコル

計測開始前に pgrep で他プロセスが存在しないことを確認し、AC 電源接続・Low Power Mode 無効の標準状態で実行しました。

バックグラウンドで 1 秒間隔のディスク I/O 統計を取得する iostat を起動し、その裏で 3 つのベンチマークケースを新規プロセスとして順次実行しました。

iostat -w 1 -c 99999 disk0 > iostat.log 2>&1 &
フェーズ開始時刻終了時刻所要時間設定・生成トークン数tok/s 実測値
warmup(破棄)22:27:1922:28:1657sprefill 61tok-
measured: short-explanation22:28:1622:29:1660sprefill 61tok / new 511tok / decode 50.24s10.171
measured: medium-review22:29:1622:30:3579sprefill 430tok / new 669tok / decode 59.09s11.322
measured: long-synthesis22:30:3522:33:26170sprefill 3015tok / new 604tok / decode 59.59s10.135

すべてのケースで stop=endOfTurn による完走を確認しています。

解剖:decode 中の I/O アクセスパターンと CPU idle 80% の衝撃

iostat.log から得られた全 366 個の 1 秒サンプルを、ケースごとに集計した結果が以下です。

ケースサンプル数 (s)平均 MB/s最小 MB/s最大 MB/s平均 tps平均 idle%
warmup57941.22.71587.15,14980.3%
short-explanation60891.819.91676.44,99677.4%
medium-review791,186.111.22153.16,35080.8%
long-synthesis1701,510.0183.32029.18,02179.8%

同機で実験終了直後に採取したモデル未稼働時(アイドル時)の 5 秒間のベースライン計測では、 0.00〜11.23 MB/s(tps 0〜264)でした。生成中の値はこのベースラインを桁違いに上回っています。

スワップ(書き込み)と Expert Streaming(読み出し)の相違

LLM のオフロード運用において、最も致命的なのは RAM 不足による物理スワップ(書き込み)です。 macOS では物理 RAM を超えたスワップ発生時に「swap death spiral」と呼ばれる破滅的なシステム遅延やカーネルパニックが報告されています3

一方で、 TurboFieldfare 等の Expert Streaming は 「読み出しのみ」 です。物理スワップによるフリーズは回避できますが、トークンごとに必要な Expert の重みを SSD から mmap 経由で引っ張ってくるため、ディスクアクセス帯域がそのままレスポンスに直結します。

CPU idle 80% が意味するもの

推論中、 1 秒あたりの転送要求回数(tps)は 5,000〜8,000 回 に達していました。それにもかかわらず CPU の idle 率は平均 80% 前後を維持しています(M1 Pro 全 8 コアのシステム平均値)。

これは TurboFieldfare の開発者が Show HN で語っていた構造4とも完全に合致しています。すなわち、 「I/O 読み込み」と「GPU/CPU 演算」がパイプラインで交互に動いており、GPU は I/O 完了を待ち、I/O は GPU 処理を待つ という細切れの交互待ちが発生しているためです。

Note

システム全体の CPU idle 率が 80% ということは、単一の I/O 制御コアや GPU スレッドが動いていたとしても、残りの多くのコアがデータ到着を待って待機している状態を意味します。

考察:モデルサイズ 14.3GB に対して prefill/decode 全体で 250GB 読まれていた現象をどう見るか

最も大きなケースであった long-synthesis(全体で 170 秒間)の iostat データを合算すると、 disk0 からの概算累積読み込み量は 約 250.68 GB に達していました。

モデル全体のファイルサイズは 14.3GB(Gemma 4 26B-A4B IT 4-bit)です。 250GB という数値は、 モデルサイズ全体の約 17.5 倍 に相当します。

単純な「1トークンあたりの割算」をしてはいけない理由

ここで「250GB ÷ 生成 604 トークン = 1 トークンあたり約 415MB 読んでいる」と単純計算したくなるかもしれませんが、 この数値帰属は間違いです。

なぜなら、 170 秒の実行時間のうち、トークン生成(decode)にあてられたのは 59.59 秒(604tok)に過ぎず、残りの約 110 秒間 はコンテキストの処理(prefill 3,015tok)にあてられているからです。 250GB には、 prefill フェーズで全レイヤーの重みを読み込んだ巨大な I/O 量が含まれています。

したがって、 decode 1 トークンあたりの読み込み量をこの合計値から単純に導くことはできません。また、 OS カーネルによるファイル先読み(readahead)機能や mmap のページアロケーション挙動によって、実際に計算で使われたバイト数よりも広い範囲が disk0 のアクセス統計として計上される性質もあります。

Warning

学術研究でも SSD オフロードがトークンあたりのエネルギー消費(J/token)を大幅に増加させることが指摘されています5。 SSD オフロードは RAM 容量の制約を回避する強力な手段ですが、帯域とエネルギー消費の観点からは無料の魔法ではありません。

デバイス層プロファイリング(iostat)の限界

今回の iostat による計測は物理ディスク disk0 全体の統計です。推論プロセス以外のバックグラウンド処理(Spotlight のインデックス作成等)の影響を完全排除するためには、 sudo fs_usage -f filesys を用いたプロセス・ファイル単位のシステムコール追跡が必要です。これが次のプロファイリング課題となります。

まとめと今後の検証展望

前作記事で抱いた「なぜ同じプロンプトで 2.8 倍も速度がブレるのか」という疑問に対し、今回の実測データは明確な回答を示してくれました。

  1. スワップ増加ゼロの陰で、SSD は限界近くまで読まれていた: peak RSS 1.5GB で動作する裏で、 decode 中は平均 900MB/s〜1.5GB/s の連続ランダムリードが継続していた。
  2. ボトルネックは計算能力ではなく I/O レシプロシティ: CPU idle 率 77〜81% が示すとおり、計算ユニットは SSD からの Expert 重み搬送を待っていた。
  3. ローカル LLM の評価基準: 1 回の単発 tok/s や RAM 使用量だけで判断せず、長時間・連続運用時の SSD 帯域とレイテンシの挙動を考慮することが重要である。

今後、さらに詳細なプロセスごとのアクセス内訳を追跡する場合は、 fs_usage コマンドによるシステムコール解析を進めていく予定です。

sudo fs_usage -w -f filesys TurboFieldfare

ローカルLLMの導入やAIシステム基盤のパフォーマンス設計・コスト最適化について相談したいことがあれば、コンタクトフォーム からお気軽にどうぞ。

Footnotes

  1. drumih/turbo-fieldfare 2

  2. COMMUNITY_BENCHMARKS.md

  3. Managed SSD offloading for MoE to prevent macOS kernel panics (llama.cpp #19825)

  4. Show HN スレッドの投稿(I/O と GPU の交互動作)

  5. SSD Offloading for LLM Mixture-of-Experts Weights Considered Harmful in Energy Efficiency (arXiv:2508.06978)