ローカルLLMの話をすると、モデルの精度やtokens/secが気になります。

自分も最初はそこを見ていました。

ただ、実際のMacで先に困ったのは回答品質ではなく、21GBまで増えたメモリ、WindowServerの監視タイムアウト、Ollamaの二重起動でした。

モデルサーバーだけが動いているわけではありません。同じMacでエージェント、テスト、ブラウザ、定期ジョブも動きます。それぞれは正常でも、合計がMacの容量を超えると画面操作ごと止まります。

それだと仕事を継続するためのローカルLLMが、仕事をしているMacを止めてしまいます・・・!

なので、モデルを速くする前に、Macを普通に使い続けられるようにしました。

Ollamaのログの37.8%がport競合だった

調査したときのOllamaログは572MB、2,175,526行ありました。

そのうちaddress already in useが821,819行。37.8%です。

原因は、手動で作ったlaunchdサービスとHomebrewのサービスが、同じportでollama serveを起動しようとしていたことでした。

しかも2つのサービスで設定が違いました。一方にはモデル数、並列数、keep-alive、KV cacheの制限があり、もう一方にはありません。

制限値を書いて安心していても、制限のない方が先にportを取れば設定は使われません。

ここで、モデル設定より先に次を確認する必要があると分かりました。

資源ガード、実行リース、Ollama、watchdog、人間へのエスカレーションでMacを守る制御フロー

図1: 重い処理を始める前の入場制限と、始めた後の減圧を分けています。

Ollamaを起動する人を一人にする

最初にやったのは、Ollamaを起動する仕組みを一つへ揃えることでした。

launchd、Homebrew services、手動起動、アプリ内のプロセスが混ざっていないかを見ます。

その上で、次の設定を一箇所へ集めました。

設定ファイルが合っているだけでは足りません。

実際に動いているプロセスの環境変数、listenしているport、ロード済みモデルも起動前に確認します。想定と違う場合は、新しい重いジョブを始めません。

ここは「設定した内容」と「いま動いているもの」を分けて見るようにしています。

非常用のOllamaを分けた

利用上限時のフェイルオーバーは、通常の環境が不安定なときにも使いたいです。

そこで非常用Ollamaは、専用port、専用model directory、Macの内蔵ストレージへ分けました。

以前は通常利用のモデルを外付けSSDへ置いていました。容量だけ考えると合理的です。

ただ、SSDが外れたときに非常用モデルまでなくなる構成は、非常用としては少し不安でした。小さいモデルだけは内蔵側へ置いています。

速くするための分離ではありません。

通常系の設定変更、port競合、外付けストレージの問題を、非常用まで一緒に受けないための分離です。

重い仕事を始める前にresource guardを通す

無人で動く重いジョブの入口へresource guardを置いています。

主に見ているのは、空きメモリ率とCPUコアあたりのloadです。

swap使用量や圧縮メモリも見ていますが、こちらは負荷が下がった後もしばらく戻りません。主判定へ使うと、Macが回復した後もずっと処理を止めることがあったので、遠いbackstopにしています。

free_memory_ratio < hard_limit  → 今回は見送る
load_per_core > hard_limit      → 今回は見送る
active_heavy_leases >= limit    → 今回は見送る
metrics unavailable             → 見送って理由を記録
otherwise                       → leaseを取って実行

計測できないときは、たぶん大丈夫だろうで始めません。

ただし黙って止まり続けるのも困るので、見送った理由はJSONLへ残します。連続して見送りになった場合は、resource guard自体の異常として通知します。

同時実行数は、ただのカウンターにしない

最初は同時実行数を数字だけで持てばよいと思っていました。

ただ、プロセスが途中で落ちると数字が戻らず、ずっと満員になります。

いまはジョブごとにPIDと開始時刻を持ったリースを作り、生きているリースだけ数えます。

{
  "job": "local-llm-continuation",
  "pid": 12345,
  "startedAt": "2026-08-03T10:00:00+09:00",
  "weight": 1,
  "owner": "resource-guard"
}

PIDは再利用されるので、開始時刻やコマンド情報も合わせて見ます。

正常終了ならその場でリースを返し、異常終了したものはreaperが掃除します。

上限を超えた仕事を大量のキューへ積むこともやめました。Macが回復した瞬間に保留ジョブが全部起きると、またすぐ重くなるためです。

その回は見送り、次の定期実行で再評価します。必ず実行する必要がある仕事だけ、期限と再開レートを持った別キューへ置きます。

実行後はwatchdogで少しずつ減圧する

resource guardを通ったあとも、モデルのロードやテストで負荷は変わります。

watchdogは別周期で動かし、いきなりプロセスをkillせず段階的に減圧します。

  1. 新しい重いジョブを短時間受け付けない
  2. 使っていないローカルモデルをアンロードする
  3. 並列数を下げる、または新規受付を止める
  4. 状態と上位プロセスを記録して人へ知らせる

自動で行うのは、影響範囲が小さく、元へ戻せる操作までです。

メモリを多く使っているという理由だけで、ユーザーのブラウザや開発プロセスを自動killしません。どの仕事を止めるかは、メモリ量だけでは決められないためです。

このあたりは、以前の21GBメモリ暴走の調査とWindowServer監視タイムアウトの解剖でかなり痛い目を見ました。

開始と解除で、同じ閾値を使わない

空きメモリが10%を行ったり来たりすると、同じ閾値では許可と拒否が何度も切り替わります。

たとえば10%未満が続いたらbrownoutへ入り、20%以上が5分続いたら解除する、というように条件を分けています。

NORMAL --(危険が3回続く)--> BROWNOUT
BROWNOUT --(回復が5分続く)--> NORMAL
BROWNOUT --(さらに悪化)----> HUMAN_ATTENTION

瞬間的な値だけでは切り替えず、数回の観測か持続時間を使います。

閾値の数値はMacのメモリ量、普段同時に開くアプリ、モデルサイズで変わります。別のMacへそのままコピーせず、実測で調整しています。

見ている数字

メモリを使わないことが目的ではありません。

Macを普通に操作できる範囲で、許可した仕事を最後まで終えられるかを見ています。

いまの確認項目

ローカルLLMを一度動かすだけなら、ollama runで十分です。

仕事の裏で何度も動かすようになると、普通の常駐サービスと同じように所有者、同時実行数、減圧、回復が必要になりました。モデルより先にMacを守る、くらいの順番でちょうどよかったです。

ローカルLLM設計・全8回

MacでローカルLLMを実際に動かして、うまくいかなかったところを8つに分けて書いています。

  1. PART 01仕事の配置
  2. PART 02コンテキストの削減
  3. PART 03handoff
  4. PART 04冪等性
  5. PART 05プロセス管理
  6. PART 06復帰監視
  7. PART 07安全ゲート
  8. PART 08完了率の計測