中古で調達したMacBook Air M1(2020 / 16GB)を、家庭内分散AI推論基盤「Coordinator」のWorkerとして参加させました。Linuxサーバー群で構成されたクラスタにmacOSを混ぜるのは初めてで、macOS固有の落とし穴を3つ続けて踏んだので、同じ構成を考えている方のために全手順と回避策をまとめます。
この記事で解決すること
- M1 MacをOllama推論ワーカーとして常時稼働させるための設定一式(pmset・自動ログイン・launchd)
- Homebrew版Ollamaで
llama-server binary not foundが出る問題の正体と回避策 - Pythonワーカーだけ「No route to host」になる、macOSローカルネットワーク許可の罠
- M1(16GB)でgemma3:12bがどの程度の速度で動くかの実測値
なぜM1 Airか
このクラスタの方針は「フロンティアモデルと張り合わず、今ある機材で実用価値を最大化する」です。M1 Airは中古市場で値頃感が出ていて、16GBのユニファイドメモリならgemma3:12b(Q4・約8GB)がMetal GPUで動きます。ファンレスで無音、アイドル消費電力も数W。常時起動ワーカーとしての適性は高い、はず…というのが仮説でした。
役割は2段階で考えています。今回はまず汎用CPU推論ワーカー(cpu_inference)として参加させ、Web検索ワーカーの移設とgemma3:12bのルーティング設計は次回に回します。
Step 1: 土台 ― 常時起動のための電源・ログイン設定
SSH(リモートログイン)を有効にしたら、まずスリープを止めます。
sudo pmset -a sleep 0 disksleep 0
sudo pmset -a displaysleep 10
pmset -g # sleep 0 になっていることを確認
蓋を閉じて運用する場合は sudo pmset -a disablesleep 1 も追加します。さらに重要なのが自動ログイン(システム設定 → ユーザとグループ)。後述のlaunchd LaunchAgentはログインセッション内で動くため、再起動後に誰もログインしないとワーカーが起動しません。FileVaultが有効だと自動ログインは選べないので注意してください。
Step 2: Ollama ― 落とし穴1: Homebrew版は使えない
素直に brew install ollama でインストールしたところ、モデル実行で即死しました。
$ ollama run gemma3:12b "こんにちは"
Error: 500 Internal Server Error: error starting llama-server:
llama-server binary not found (checked: /opt/homebrew/Cellar/ollama/0.30.7/...)
調べると、Homebrewの0.30系ARM64ボトルに llama-server バイナリが同梱されていない既知のバグ(ollama/ollama issue #16535)でした。APIサーバーは起動するのに推論バックエンドが存在しないという質の悪い壊れ方です。修正PRは出ているもののマージ前。同じマシンでも公式のデスクトップアプリなら正常動作するとのことなので、公式版に切り替えます。
# Homebrew版を撤去
brew services stop ollama
brew uninstall ollama
# 公式版を取得・配置
curl -L -o ~/Downloads/Ollama.dmg https://ollama.com/download/Ollama.dmg
hdiutil attach ~/Downloads/Ollama.dmg
cp -R /Volumes/Ollama/Ollama.app /Applications/
hdiutil detach /Volumes/Ollama
open /Applications/Ollama.app # 初回は本体画面での操作が必要
公式アプリはメニューバーに常駐し、ログイン時自動起動がデフォルト。CLIはアプリ同梱のものにシンボリックリンクを張ります。
sudo ln -sf /Applications/Ollama.app/Contents/Resources/ollama /usr/local/bin/ollama
救いだったのは、pull済みのモデルは ~/.ollama に保存されていてインストール方式に依存しないこと。8GBの再ダウンロードは不要でした。
Step 3: 実測 ― M1でgemma3:12bはどれくらい動くか
$ ollama run gemma3:12b "日本語で自己紹介してください" --verbose
(省略)
load duration: 15.100016166s
prompt eval rate: 15.79 tokens/s
eval rate: 7.68 tokens/s
生成速度 7.68 tok/s。メモリ帯域68GB/sのM1で12Bモデルを動かす値としては妥当なところです。RTX 3070 Tiには遠く及びませんが、CPU専用機での12B推論よりは大幅に速く、「GPUワーカーが塞がっている時の第二候補」として実用域。コールドロードに15秒かかる点は、利用頻度が低いとルーティング時の体感に効くので OLLAMA_KEEP_ALIVE での調整候補としてメモしておきます。
Step 4: Python環境 ― 落とし穴2: macOS同梱のpython3は古い
ワーカースクリプト用のvenvを作って起動したところ、即座に例外で落ちました。
File "worker_base.py", line 183, in <module>
load_info: dict | None = None,
TypeError: unsupported operand type(s) for |: 'type' and 'NoneType'
macOS同梱の python3 は3.9で、dict | None 型ヒント記法(3.10+)が使えません。さらにredis-pyの7.x系もPython 3.10以上を要求するため二重にアウト。Homebrewで新しいPythonを入れてvenvを作り直します。
brew install python@3.13
rm -rf ~/coordinator/venv
/opt/homebrew/bin/python3.13 -m venv ~/coordinator/venv
~/coordinator/venv/bin/pip install "redis==7.4.0" psycopg2-binary requests psutil
redis==7.4.0 のピン留めは前回記事の障害対応の成果です。素の pip install redis だと8.0系が入り、BRPOPが沈黙する地雷を踏みます。
Step 5: launchd化 ― macOSのsystemd相当
Linux勢はsystemdでワーカーを管理していますが、macOSにはsystemdがありません。対応物はlaunchdです。ユーザーのLaunchAgentとして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>jp.mapleharp.coordinator-worker</string>
<key>ProgramArguments</key>
<array>
<string>/Users/hogehoge/coordinator/venv/bin/python</string>
<string>/Users/hogehoge/coordinator/worker.py</string>
</array>
<key>WorkingDirectory</key><string>/Users/hogehoge/coordinator</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key><true/>
<key>StandardOutPath</key><string>/Users/hogehoge/coordinator/worker.log</string>
<key>StandardErrorPath</key><string>/Users/hogehoge/coordinator/worker.log</string>
</dict>
</plist>
systemdとの対応関係はこうです。
| systemd | launchd |
|---|---|
| ExecStart | ProgramArguments |
| Restart=always | KeepAlive=true |
| enable(自動起動) | RunAtLoad=true + 自動ログイン |
| systemctl start/stop | launchctl bootstrap / bootout |
| journalctl -u | StandardOutPathのログをtail |
# 起動
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/jp.mapleharp.coordinator-worker.plist
# 停止
launchctl bootout gui/$(id -u)/jp.mapleharp.coordinator-worker
# 状態確認
launchctl print gui/$(id -u)/jp.mapleharp.coordinator-worker
注意点として、KeepAliveはクラッシュしても延々と再起動し続けるので、環境が壊れている状態で放置するとログが荒れます。トラブル時はまずbootoutで止めてから直すのが作法です。
落とし穴3(最大の難所): Pythonだけ「No route to host」になる
満を持してワーカーを起動すると、Redisへの接続で謎のエラーが出ました。
redis.exceptions.ConnectionError: Error 65 connecting to 192.168.0.40:6379. No route to host.
ところが同じSSHセッションから nc -vz 192.168.0.40 6379 を打つと succeeded。pingも全ポートのncも通るのに、Pythonからの接続だけが「経路がない」と言われる。ネットワークの知識だけで考えると完全に矛盾した状況です。
正体はmacOSの「ローカルネットワーク」プライバシー制限でした。ポイントは3つあります。
- 制限はバイナリ単位。
ncやpingはApple純正なので制限対象外、Homebrewで入れたpython3.13は対象。だから切り分けが誤導される - 未許可バイナリからのLAN接続は、許可を求めるでもエラーらしいエラーでもなく、黙って Errno 65(No route to host)になる
- 許可ダイアログはGUIセッションにしか出ない。SSH越しの操作では永遠に許可できない
対処は「本体でデスクトップにログインした状態で、launchdジョブを起動する」こと。pythonがLAN接続を試みた瞬間に本体画面へ「”python3.13″がローカルネットワーク上のデバイスの検索・接続を求めています」ダイアログが出るので「許可」を押します。出ない場合は システム設定 → プライバシーとセキュリティ → ローカルネットワーク で手動でオンにします。
もう1つ運用上の注意: 許可はバイナリの実体に紐づくため、venvを作り直すと再許可が必要になる場合があります。「venvを再構築したらワーカーが沈黙した」時は、まずここを疑ってください。
一気通貫テスト
許可を通すと、ログが一気に流れました。
INFO Worker starting. ID=macbookair Queues=['tasks:cpu:macbookair', 'tasks:cpu:shared'] ...
INFO Ollama OK
INFO [sanity] llama3.2:3b OK
INFO Redis OK
INFO [register] worker_id=macbookair models=['llama3.2:3b'] supports=['cpu_inference']
INFO [heartbeat] started (interval=240s)
INFO Waiting on queues ['tasks:cpu:macbookair', 'tasks:cpu:shared'] ...
Coordinator経由でMacBook指名のタスクを流します。
curl -s -X POST http://localhost:8000/route \
-H "Content-Type: application/json" \
-d '{"prompt": "1+1は?数字だけ答えてください。", "model": "llama3.2:3b", "worker": "macbookair"}'
結果テーブルを確認すると——
status | worker | response
--------+------------+----------
done | macbookair | 2
Redisからのタスク取得 → M1での推論(1357ms)→ PostgreSQLへの書き込みまで一気通貫成功。MacBook Air M1、クラスタ正式参加です。なお初回のテストではPostgreSQL側の許可漏れ(pg_hba.conf)で別Workerに自己修復されるという一幕がありました。その顛末は前回記事をどうぞ。
まとめと次回
macOSをLinuxクラスタに混ぜる際の落とし穴は「Homebrew版Ollamaのllama-server欠落」「同梱Python 3.9」「ローカルネットワーク許可」の3つでした。特に最後の1つは、ncやpingが通ってしまうためにネットワーク障害と誤診しやすいのが厄介です。Errno 65 + macOS + 非純正バイナリ、の組み合わせを見たらまずプライバシー設定を疑ってください。
次のステップは2つ。gemma3:12bのルーティング設計(現状GPU専用モデル扱いのためM1には流れない。Worker×モデル統計への拡張が必要になりそう)と、Web検索ワーカーのMacBook移設です。M1の静音・低消費電力という特性を、常時稼働の前線にどう活かすか——続きは次回。