「M3 Maxを買ったのに、思ったほどローカルLLMが速くない…」
「ファンが回るばかりで、トークン生成がカクつく」
ローカル環境でLLMを動かす際、このような課題に直面することは少なくありません。
フロントエンド開発の現場では、ReactやTypeScriptを用いたUI構築において、ミリ秒単位のレンダリングパフォーマンスが重視されます。ユーザー体験(UX)において「待たされること」ほどストレスなものはないからです。これはAIチャットボットでも同様であり、レスポンスの遅延は思考のフローを分断し、ツールの有用性を大きく損なってしまいます。
実は、Ollamaは非常に優秀なランタイムですが、Apple Silicon特有のハードウェア構成に対して、デフォルト設定が常に「最適」とは限りません。特にメモリ管理においては、安全側に倒された設定が、せっかくのGPU性能を眠らせていることがあります。
本記事では、ブラックボックスになりがちな「推論パフォーマンス」について解説します。感覚的な「なんとなく速くなった」ではなく、ハードウェアの特性を理解し、論理的にパラメータを決定するアプローチで、Macのポテンシャルを最大限に引き出す方法を探求していきましょう。
2. ブラックボックスを開ける:OllamaとApple Siliconの通信簿
まず、高性能なMacでもLLMの動作が遅くなる理由を紐解いていきます。その鍵は、Apple Siliconの最大の特徴である「Unified Memory Architecture (UMA)」と、Ollama(その背後にあるllama.cpp)の連携の仕組みにあります。
Unified Memory Architecture (UMA) の基礎理解
従来のPCアーキテクチャでは、CPUにはメインメモリ(RAM)、GPUにはビデオメモリ(VRAM)と、物理的に分かれたメモリが存在していました。データを処理するには、CPUからGPUへデータをコピーする必要があり、この「コピー時間」が大きなボトルネックとなります。
対してApple SiliconのUMAは、CPUとGPUが「同じメモリプール」を共有します。これにより、データのコピーなしに、CPUとGPUが同じデータにアクセスできることが、高速処理の理由です。
しかし、注意すべき点も存在します。
- システムとの共存: メモリが共有されているということは、OSやブラウザなどとVRAM領域を取り合うことになります。
- 配分ルール: macOSは、安定動作のためにGPUが使用できるメモリ量に制限をかけています(通常は全メモリの約75%程度)。
ボトルネックはどこにある?CPU vs GPU vs メモリ帯域
Ollamaで推論を行う際、処理は「プロンプト処理(Prompt Processing)」と「トークン生成(Token Generation)」の2段階に分かれます。
- プロンプト処理: 入力テキストを理解するフェーズ。並列処理が得意で、GPUの処理能力が直結します。
- トークン生成: 1文字ずつ続きを予測するフェーズ。ここは計算量よりも「メモリからどれだけ速くデータを読み出せるか」というメモリ帯域幅(Memory Bandwidth)が支配的になります。
もし、モデルサイズが大きすぎてGPUに割り当て可能なメモリ(VRAM相当)に収まりきらない場合、あふれた計算レイヤーはCPUに回されます。CPUとGPUを行き来する処理が発生した瞬間、UMAのメリットである「高速アクセス」が損なわれ、推論速度は著しく低下します。
つまり、最適化のゴールは「可能な限り全てのレイヤーをGPU(Metal)で処理させ、かつシステムスワップを発生させない最適なラインを見極めること」になります。
3. 現状を数値化する:パフォーマンス計測用スクリプトの実装
「設定を変えたら速くなった気がする」という感覚値ではなく、改善には正確な計測が不可欠です。OllamaのAPIには、推論にかかった時間を詳細に返す機能が備わっています。
ここでは、Pythonを用いて正確な「トークン生成速度(tokens per second)」を計測するスクリプトを実装します。
感覚値に頼らないベンチマーク環境の構築
以下のスクリプトを benchmark_ollama.py として保存してください。このスクリプトは、指定したモデルに対して同じプロンプトを送信し、ウォームアップ(初回ロードの影響除外)を行った上で、純粋な生成速度を計測します。
import requests
import json
import time
# 計測設定
MODEL_NAME = "Llamaモデル" # 計測したいモデル名
PROMPT = "Explain the concept of quantum computing in simple terms." # 負荷をかけるためのプロンプト
API_URL = "http://localhost:11434/api/generate"
ITERATIONS = 3 # 計測回数
def run_benchmark(model, prompt):
payload = {
"model": model,
"prompt": prompt,
"stream": False, # 計測のためストリームはオフ
"options": {
"num_ctx": 4096, # コンテキストサイズを固定
"temperature": 0 # 生成内容を固定してブレをなくす
}
}
try:
start_time = time.time()
response = requests.post(API_URL, json=payload)
response.raise_for_status()
end_time = time.time()
data = response.json()
# 重要なメトリクス抽出
eval_count = data.get('eval_count', 0) # 生成されたトークン数
eval_duration = data.get('eval_duration', 0) # 生成にかかった時間(ナノ秒)
if eval_duration == 0:
return 0
# ナノ秒を秒に変換して計算
tok_per_sec = eval_count / (eval_duration / 1e9)
return tok_per_sec
except requests.exceptions.RequestException as e:
print(f"Error: {e}")
return 0
print(f"🚀 Benchmarking model: {MODEL_NAME}...")
# 1. ウォームアップ(モデルロード時間を計測から除外)
print("Running warmup...")
run_benchmark(MODEL_NAME, "Hello")
# 2. 本計測
results = []
for i in range(ITERATIONS):
print(f"Iteration {i+1}/{ITERATIONS}...")
speed = run_benchmark(MODEL_NAME, PROMPT)
results.append(speed)
print(f" -> {speed:.2f} tok/s")
avg_speed = sum(results) / len(results)
print(f"\n📊 Average Speed: {avg_speed:.2f} tok/s")
実行と分析
ターミナルで実行します。
python3 benchmark_ollama.py
出力された数値(例:35.4 tok/s)がベースラインとなります。設定変更後にこの数値がどう変化するかを記録し、論理的に評価していきましょう。
3. コア設定の解剖:推論速度を左右する環境変数の最適解
Ollamaは、起動時に環境変数を読み込むことで挙動を最適化できます。macOSの場合、launchctl で永続的に設定するか、ターミナルから一時的に設定して起動することで適用可能です。Apple Siliconのポテンシャルを最大限に引き出すための設定を解説します。
OLLAMA_NUM_GPU:GPUオフロード層数の決定ロジック
パフォーマンスに最も直結するのがこの設定です。Ollamaは通常、利用可能なVRAM容量に基づいて自動的にオフロードするレイヤー数を決定しますが、明示的に指定することで「可能な限りGPUに載せる」あるいは「システム安定性のために一部をCPUに残す」といった細かい制御が可能になります。
- デフォルト: 自動(VRAM容量とモデルサイズから算出)
- 設定値: 数値(GPUにオフロードするレイヤー数)
Apple Silicon(M1/M2/M3/M4チップ)の場合、Unified Memoryを活用して全てのレイヤーをGPUで処理させるのが理想的です。しかし、他のアプリケーションを起動している場合など、VRAMが逼迫してクラッシュするケースでは、あえて数値を下げる調整が必要になります。
# 例:全レイヤーをGPUに強制オフロード
# 999のようなモデルの総レイヤー数を超える値を指定すると、可能な限り全層をGPUに割り当てます
export OLLAMA_NUM_GPU=999
ollama serve
OLLAMA_NUM_THREAD:Pコア/Eコアとスレッド数の関係
Apple Siliconは「高性能コア(Performance Cores)」と「高効率コア(Efficiency Cores)」のハイブリッド構成です。推論処理の一部がCPUで行われる場合、スレッド数を無闇に増やすと、Eコアへの割り当てやコンテキストスイッチのオーバーヘッドにより、かえってパフォーマンスが低下することがあります。
- 推奨設定: 実装されているPコア(高性能コア)の数に合わせる。
例えば、M1 Pro(8コアCPU:6P + 2E)やM3 Proの場合、Pコア数に合わせた値を設定するのが最適解の出発点となります。以下のコマンドでPコア数を確認できます。
# macOSでPコア(高性能コア)の数を確認するコマンド
sysctl -n hw.perflevel0.logicalcpu
# 確認した数値(例: 6)を設定
export OLLAMA_NUM_THREAD=6
num_ctx:コンテキスト長とVRAM消費のトレードオフ
これは環境変数ではなくAPI呼び出し時やModelfileで指定するパラメータですが、パフォーマンスへの影響は甚大です。特に、最新のLlamaモデルやMistralモデルなど、長いコンテキストに対応したモデルを扱う際は注意が必要です。
コンテキスト長(記憶できる会話の長さ)を増やすと、KV Cacheと呼ばれるメモリ領域が消費されます。デフォルトは 2048 ですが、これを 8192 や 128k に拡張すると、モデル本体以外に大量のメモリを消費し、結果としてモデル本体がGPU(Metal)からメインメモリ(CPU処理)へ追い出される原因になります。
計算式(概算):KV Cache Size ≈ 2 * Layers * Hidden_Size * Context_Len * 2bytes(fp16)
数式よりも重要なのは、「コンテキスト長を倍にすると、推論に必要なVRAMの空き容量も跳ね上がる」という事実です。推論速度が極端に遅い場合、まずは num_ctx をデフォルト値に戻して検証するのがトラブルシューティングの定石です。特にコード生成や長文要約で長いコンテキストを必要とする場合は、より小さなパラメータサイズのモデル(量子化モデルなど)への切り替えも検討すべきです。
4. メモリ容量別・最適化プリセット(8GB/16GB/32GB+)
ここからは、Macのメモリ容量に合わせた具体的な戦略を提示します。Modelfileを作成し、カスタムモデルとして保存することで設定を固定できます。
8GBモデル:スワップ回避のための限界設定
8GBのMac(MacBook Airなど)は、ローカルLLMには過酷な環境であることは否定できません。OSがシステム領域としてメモリを占有するため、LLMに割り当てられるのは実質的に数GB程度です。ここでスワップ(SSDをメモリ代わりに使うこと)が発生すると、推論速度は劇的に低下します。
戦略: 小規模モデル + 高圧縮量子化の活用
- 推奨モデル: Phiシリーズの最新軽量モデル, Llamaの8Bモデル (q4_k_m 量子化), Gemmaの軽量版
- Modelfile設定例:
# ベースモデルには軽量なものを指定
FROM Llamaモデル
# コンテキストを小さくしてメモリ節約(デフォルトの半分程度)
PARAMETER num_ctx 2048
# GPUレイヤーは自動調整に任せる
# 明示的に指定しないことでOllamaの自動最適化を利用
ポイント: q4_0 や q4_k_m といった4bit量子化モデルを選ぶのが鉄則です。fp16(16bit)のモデルは、8GBマシンではメモリ不足で動作しないか、激しいスワップを引き起こします。
16GBモデル:7B/14Bモデルの快適動作ライン
多くのフロントエンド開発環境で使用されているスペックはこのラインに該当するでしょう。7B〜8Bクラスのモデルなら非常に快適に動作し、量子化を活用すれば12B〜14Bクラスの中規模モデルも実用範囲内に入ります。
特に最近では、Mistral AIからエッジデバイス向けの強力なモデル(Ministralシリーズなど)や、コード生成に特化したモデルも登場しており、選択肢が広がっています。
戦略: パフォーマンスと精度のバランス重視
- 推奨モデル: Llamaの8Bモデル (q8_0 または fp16), Mistralの中規模モデル (12B-14Bクラス, q5_k_m)
- Modelfile設定例:
FROM mistral
# コンテキストを少し広げても余裕あり
PARAMETER num_ctx 4096
# システムメッセージで役割を明確化
SYSTEM "あなたはTypeScriptとReactに精通した優秀なコーディングアシスタントです。"
注意点: VS CodeなどのIDEやブラウザ(特にタブを多く開いている場合)を同時に使用しながら開発する場合、14B以上のモデルはメモリ圧迫の原因になり得ます。開発中の「ながら利用」には、8Bクラスの軽量モデルを使うのが、システム全体のレスポンス(UX)を損なわないための最適解です。
32GB以上:並列処理と大型モデルへの挑戦
32GB、64GB、あるいはそれ以上のMシリーズMax/Ultraチップ搭載機を使用している場合、高度な運用が可能です。70Bクラスの大型モデルや、複数のモデルを同時に立ち上げる実験的なアプローチも視野に入ります。
戦略: 高精度モデル & マルチモデル並列実行
- 推奨モデル: Llamaの70Bクラス (q4_k_m), Command Rなどの大型モデル
- 並列実行の設定:
OLLAMA_MAX_LOADED_MODELSを調整することで、コーディング用とチャット用の異なるAIを同時にメモリに保持し、切り替え待ち時間をゼロにできます。
# 2つのモデルを同時にロードしておく(例:チャット用とコード補完用)
export OLLAMA_MAX_LOADED_MODELS=2
ollama serve
このクラスのマシンパワーがあれば、num_ctx を 32768 以上に設定して、大規模なドキュメント解析やリファクタリング提案を行わせても、GPUのみで高速に完結させることが可能です。最新のコード特化モデル(Devstral等の系列)が利用可能であれば、そのパラメータ数に応じた量子化レベルを選択して試してみる価値は大いにあります。
5. 検証とトラブルシューティング
設定を施した後は、必ず検証を行います。予期せぬエラーや、逆に遅くなる現象への対処法です。論理的なアプローチでボトルネックを特定しましょう。
メモリ割り当てエラーへの対処法
ログに mmap load failed や out of memory が出る場合、Ollamaが確保しようとしたメモリ領域がOSの物理許容量を超えています。これは特に、VRAMとメインメモリを共有するApple Silicon特有の「Unified Memory Architecture」の限界に達したことを意味します。
対処法として、以下のステップを推奨します:
- モデルの量子化レベルを下げる: 最も確実な方法です。例えば、q8(8bit量子化)からq4_k_m(4bit量子化)に変更するだけで、メモリ消費量はほぼ半減します。精度とのトレードオフになりますが、多くのタスクではq4で十分な結果が得られます。
- コンテキスト長(num_ctx)を制限する: デフォルトのコンテキスト長(多くの場合2048や4096)がメモリを圧迫している可能性があります。Modelfileで
PARAMETER num_ctx 2048のように明示的に値を小さく設定し直してください。 - 並列リクエスト数の調整: サーバーモードで使用している場合、
OLLAMA_NUM_PARALLELなどの環境変数がデフォルト値のままだと、複数のリクエストを同時に処理しようとしてメモリ不足に陥ることがあります。これを1に設定して動作を確認してください。
※以前のバージョンで試験的に言及されることがあったメモリ制限用の環境変数は、最新のOllamaでは挙動が異なるか、公式に推奨されていない場合があります。基本的にはモデルサイズとコンテキスト長で制御するのがベストプラクティスです。
推論速度が低下する「メモリ帯域飽和」の検知
macOS標準のコマンド powermetrics を使うと、リアルタイムでGPUの使用率とメモリ帯域を確認できます。これはボトルネック特定に非常に有効です。
sudo powermetrics --samplers gpu_power,bandwidth -i 1000
推論中に「GPU Active Residency」が100%近くに張り付いているなら、GPUは全力を出しています。もしGPU使用率が低いのに生成速度が遅い場合は、以下の可能性が高いです:
- CPUフォールバック: モデルの一部がCPUで処理されている(オフロード不足)。
- メモリスワップ発生: アクティビティモニタの「メモリ」タブで「スワップ使用領域」を確認してください。ここが0バイトでなければ、SSDへの読み書きが発生しており、パフォーマンスは劇的に低下します。他のアプリを終了するか、より軽量なモデルへ切り替える必要があります。
最終確認:最適化前後でのベンチマーク比較
最初作成した benchmark_ollama.py をもう一度実行します。
- Before: 15.2 tok/s
- After: 48.5 tok/s
このように明確な数値向上が確認できれば、チューニングは成功です。このスピード感こそが、ローカルLLMを実務で使うためのスタートラインとなります。
まとめ:論理的な最適化で、快適なAI開発ライフを
Apple Siliconは強力ですが、魔法の杖ではありません。その仕組み(UMA)を理解し、適切なパラメータ(スレッド数、オフロード層数、コンテキスト長)を与えることで、初めてその真価を発揮します。
今回紹介した手法を使えば、Mac上のOllamaは、これまでとは別次元のレスポンスを返してくれるはずです。待ち時間が減れば、試行錯誤の回数が増え、結果として開発の質も向上します。
しかし、ローカル環境にはどうしても「ハードウェアの物理的な限界」が存在します。チーム全体でナレッジを共有したり、数百ページのドキュメントをRAGで検索させたりといった大規模な運用には、ローカルマシンのチューニングだけでは対応しきれない場面も出てくるでしょう。
そうした場合は、チーム全体でのAI活用を加速させるクラウドプラットフォームの導入も一つの選択肢です。最新のモデルを最適化されたクラウドインフラで即座に利用でき、セキュアな環境でナレッジ共有を実現するサービスを活用することで、インフラ管理の煩わしさから解放され、本来の「価値創造」に集中できる環境を構築できます。ローカルの限界を感じたり、チームでの運用を検討し始めた際には、クラウドベースのソリューションを評価してみることをおすすめします。
コメント