Ollama

Ollama をlaunchdで常駐させたら Mac がフル稼働になった話——KeepAlive の罠と2日かかった原因特定

Ichinose Taito — MBTI × 心理学 × AI

心理学で、
人間関係を
ちょっとラクにする。

MBTIとアドラー心理学を軸に、
自分と他者を理解するヒントを発信しています。

記事を読む ›
ちのくん
ちのくん
— こんな活動をしています —
Ollama をlaunchdで常駐させたら Mac がフル稼働になった話——KeepAlive の罠と2日かかった原因特定

何が起きたか

Ollama を launchd で常駐させてから、作業中に Mac の風切り音が急に大きくなり始めた。Activity Monitor を開くと ollama_llama_server が CPU 数十〜百数十% を常時食っている。モデルを使っていないのに、だ。「KeepAlive=true にしとけば楽」と思って設定した plist が、逆に Mac を常時フル稼働状態にしていた。これに気づくまで2日かかった。

環境

  • macOS 15.4 Sequoia(MacBook Pro M5 32GB)
  • Ollama 0.6.x(Homebrew インストール済み)
  • qwen3.5:9b / gemma4:26b をメインモデルとして使用
  • launchd plist を ~/Library/LaunchAgents/ に配置
  • 自動化スクリプト: 01_Scripts/lib/ai_client.py 経由で各パイプラインが呼び出し

詰まったポイント

最初に疑ったのはスクリプト側だった。ai_client.py がループしてるとか、パイプラインが暴走してるとか。でも全部の Python プロセスを止めてもファンは回り続けた。

ps aux | grep ollama で確認すると、ollama_llama_server が生きていた。Ollama はモデルをリクエストされた瞬間にサーバープロセスを起動して、デフォルトで 5分間メモリに保持する仕様だ。問題はそこじゃなくて、保持が終わった後に launchd が即座にプロセスを再起動していたこと——これが原因だった。

KeepAlive=true は「プロセスが落ちたら即座に再起動する」という設定なので、Ollama がモデルをアンロードしてプロセスを終了しようとすると、launchd が黙って復活させる。その際にモデルの再ロードが走る。ループだ。

エラーログは特に出ない。/usr/local/var/log/ にも ~/Library/Logs/ にも何も残っていなかった。「正常に動いているけど重い」という状態で、デバッグの手がかりが掴みにくかった。

解決までの手順

ステップ1: まず現状の plist を確認する

cat ~/Library/LaunchAgents/com.taito.ollama.plist

この時点の中身は KeepAlive=trueRunAtLoad=true のシンプルな構成だった。モデルが自動アンロードされてプロセスが終了するたびに launchd が再起動する状態だった、という仮説がここで固まった。

ステップ2: OLLAMA_KEEP_ALIVE を短くセットして様子を見る

Ollama にはモデルの保持時間を制御する環境変数がある。デフォルトは 5m(5分)。まずこれを 0 にしてリクエスト直後に解放させてみた。

launchctl unload ~/Library/LaunchAgents/com.taito.ollama.plist
OLLAMA_KEEP_ALIVE=0 ollama serve &

CPU 使用率が下がった。モデルが即アンロードされてプロセスが静かになっている——という解釈でほぼ合ってると思う(推測を含む)。

ステップ3: KeepAliveOnDemand 相当に切り替える

launchd の KeepAlive=true を削除して、RunAtLoad=false にした。これで Ollama は最初から起動しない。必要な時にスクリプト側から呼び出す構成に変える。

launchctl unload ~/Library/LaunchAgents/com.taito.ollama.plist

ステップ4: plist を書き直す

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key>
  <string>com.taito.ollama</string>
  <key>ProgramArguments</key>
  <array>
    <string>/usr/local/bin/ollama</string>
    <string>serve</string>
  </array>
  <key>EnvironmentVariables</key>
  <dict>
    <key>OLLAMA_KEEP_ALIVE</key>
    <string>2m</string>
    <key>OLLAMA_MAX_LOADED_MODELS</key>
    <string>1</string>
    <key>OLLAMA_NUM_PARALLEL</key>
    <string>1</string>
  </dict>
  <key>RunAtLoad</key>
  <false/>
  <key>KeepAlive</key>
  <false/>
  <key>ThrottleInterval</key>
  <integer>30</integer>
</dict>
</plist>

KeepAlive=false にしてプロセスが落ちたら落ちっぱなしにする。ThrottleInterval=30 は「再起動するなら30秒待て」というセーフガード。今回は KeepAlive を切ったので直接は効かないが、保険で残している。

ステップ5: スクリプト側で起動・停止を管理する

常駐させない分、使う前に Ollama が生きているか確認して、なければ起動する処理を ai_client.py の呼び出し前に入れた。

import subprocess
import time
import httpx

def ensure_ollama_running() -> bool:
    try:
        r = httpx.get("http://localhost:11434/api/tags", timeout=2)
        return r.status_code == 200
    except Exception:
        pass
    subprocess.Popen(
        ["ollama", "serve"],
        stdout=subprocess.DEVNULL,
        stderr=subprocess.DEVNULL
    )
    for _ in range(10):
        time.sleep(1)
        try:
            r = httpx.get("http://localhost:11434/api/tags", timeout=1)
            if r.status_code == 200:
                return True
        except Exception:
            continue
    return False

ステップ6: 動作確認

再起動後、パイプラインを走らせながら Activity Monitor を確認。推論中は CPU が上がるが、完了後 2分で落ち着くようになった。ファンが止まった。

コード/設定の抜粋

上の plist で設定している環境変数を整理すると:

# 手動で確認する場合
OLLAMA_KEEP_ALIVE=2m \
OLLAMA_MAX_LOADED_MODELS=1 \
OLLAMA_NUM_PARALLEL=1 \
ollama serve

OLLAMA_MAX_LOADED_MODELS=1 は「同時にメモリに展開するモデルを1つに絞る」設定。qwen3.5:9b と gemma4:26b を混在させていたので、これがないと両方が展開されたままになる。32GB 共有メモリ環境だと地味に効く。

OLLAMA_NUM_PARALLEL=1 は同時処理リクエスト数の上限。パイプラインが並列で投げてくることがあるので、Mac のメモリプレッシャーが高まりやすいときの安全弁として入れている。

試してわかったこと

KeepAlive=true はサーバー用途のデーモン向けの設定で、Ollama みたいに「使うときだけ動く」モデルには相性が悪い。起動コストが高いプロセスを常駐させたい気持ちはわかるが、モデルのアンロード→再ロードのループを launchd が黙って引き起こす罠がある。

OLLAMA_KEEP_ALIVE の存在は公式ドキュメントに書いてあるけど、launchd との組み合わせで何が起きるかは書いていない。実際に詰まってみないと分からない類いの問題だった。

Apple Silicon の共有メモリアーキテクチャは GPU と CPU でメモリを共有しているので、モデルが展開されているだけで他のアプリへの影響が出やすい。「Ollama は軽い」という印象で使い始めると、この常駐コストに気づきにくい。

あと、エラーが出ない問題は本当に厄介だった。ログが空、プロセスは正常、でも Mac が重い——という状態だと、原因の切り分けに時間がかかる。ps auxActivity Monitor を両方使いながら「どのプロセスがいつ何をしているか」を時系列で見るのが結局一番早かった。

まとめ

Ollama を launchd で常駐させるなら、KeepAlive=false + OLLAMA_KEEP_ALIVE=2m + OLLAMA_MAX_LOADED_MODELS=1 のセットが Apple Silicon Mac では現実的な設定だった。
常駐の利便性より、使わない時間のメモリ・CPU 解放を優先するほうが作業環境として安定する。
エラーが出ない重さこそが一番デバッグしにくいので、ps aux で状況を地道に観察することをお勧めする。✨