空いた壁のくぼみを、真鍮の先端がついた物差しで測る家具職人を描いた油彩画。そこに合う時計をつくる前の採寸で、床には現代のレーザー距離計がある。

ノート

MacでローカルLLMを構築する — mlx-serveの導入とAPI検証

Apple Silicon搭載Macでmlx-serveとQwen3-4B-4bitを使い、ローカルLLMを構築する手順。2026年11月5日と6日のM1 Max(64 GB)での導入、起動、API検証と未検証事項を記録します。

mlx-serveとQwen3-4B-4bitをMacに導入し、手元でモデルを起動してAPI応答を確かめました。以下の所要時間と速度は、2026年11月5日と6日にApple M1 Max・64 GB・macOS 26.6で得た値です。別のMacで同じ値になることは保証しません。

使用条件とライセンス

使用したのはGitHub Releases v26.11.1の mlx-serve-bin-macos-arm64.tar.gz です。mlx-serve本体はMIT、同梱する第三者部分にはApache-2.0/BSDの表示があり、配布物のNOTICEで確認できます。モデルは mlx-community/Qwen3-4B-4bit で、元の Qwen/Qwen3-4B に基づくApache-2.0の配布です。利用前に、手元の配布物のLICENSE・NOTICEとモデルのライセンス表示を確認してください。公式にはDMGとHomebrewの案内もありますが、ここでは試していません。

アーカイブを確認し、実行ファイルを探す

公式GitHub Releasesのv26.11.1からアーカイブを取得しました。ARCHIVE と UNPACK_DIR は実際の保存先に合わせてください。この手順は、両パスを合わせたうえでzshとbashの両方で実行しました。SHA-256が一致しなければ解凍せず、実行ファイルやバージョンを確認できなければ次へ進みません。

ARCHIVE="$HOME/Downloads/mlx-serve-bin-macos-arm64.tar.gz"
UNPACK_DIR="$HOME/Downloads/mlx-serve-v26.11.1"
if ! printf '%s  %s\n' '9acaa7a21b294536c3db9f2683d85079727b4e8db725d34260020499523dc267' "$ARCHIVE" | shasum -a 256 -c -; then
  printf '%s\n' 'SHA-256が一致しません。ここで中止してください。' >&2
elif ! mkdir -p "$UNPACK_DIR" || ! tar -xzf "$ARCHIVE" -C "$UNPACK_DIR"; then
  printf '%s\n' '解凍できませんでした。ここで中止してください。' >&2
else
  BIN="$(find "$UNPACK_DIR" -type f -name mlx-serve -print -quit)"
  if [ -z "$BIN" ] || [ ! -x "$BIN" ]; then
    unset BIN
    printf '%s\n' '実行可能な mlx-serve が見つかりません。' >&2
  elif ! "$BIN" --version; then
    unset BIN
    printf '%s\n' 'バージョンを確認できませんでした。' >&2
  fi
fi

SHA-256は一致し、--version の先頭行は mlx-serve 26.11.1 (fast) でした。配布物の署名チームIDはS77TW432YG(David DalcuのDeveloper ID)でした。上のコマンドには署名の検証は含まれません。BIN には解凍先で見つけた実行ファイルのパスが入ります。次の手順も同じターミナルで実行します。

モデルを取得して起動する

モデルの取得には通信が必要です。11月5日の初回取得では9ファイル、計2,173 MBのダウンロードに約54秒かかりました。モデルは ~/.mlx-serve/models/ に保存されます。11月6日にはモデルが既にあり、pull は再ダウンロードせずに進みました。

if [ -n "${BIN:-}" ] && [ -x "$BIN" ]; then
  "$BIN" pull mlx-community/Qwen3-4B-4bit &&
  "$BIN" --model "$HOME/.mlx-serve/models/mlx-community/Qwen3-4B-4bit" --serve --host 127.0.0.1 --port 11234
else
  printf '%s\n' '先にアーカイブとバージョンを確認してください。' >&2
fi

起動後、数秒以内に Server listening on http://127.0.0.1:11234 と表示されました。コンテキスト長はメモリの空き具合で自動的に決まり、同じMacでも12,288 tokenのときと870 tokenのときがありました。

--host 127.0.0.1 は省略しません。公式の cli.md は既定値を 0.0.0.0、SECURITY.md は 127.0.0.1 と記しており、記述が一致しないためです。ここではLANに公開しません。

APIの応答を確かめる

サーバーを動かしたまま、別のターミナルで以下の順に実行しました。HTTPステータスと保存した応答を確認します。x-api-key: local-test はローカルのリクエストで使った値です。

OpenAI互換・通常応答

OUT_DIR="$(mktemp -d)"
curl -sS -o "$OUT_DIR/chat.json" -w 'HTTP %{http_code}\n' \
  -H 'Content-Type: application/json' \
  --data-binary @- http://127.0.0.1:11234/v1/chat/completions <<'JSON'
{"model":"mlx-community/Qwen3-4B-4bit","max_tokens":128,"messages":[{"role":"user","content":"ローカルLLMとは何か、3文で説明してください。"}]}
JSON
python3 -m json.tool --no-ensure-ascii "$OUT_DIR/chat.json"

11月6日の応答はHTTP 200でした。65 tokenを生成し、速度は81.0 tokens/秒。回答の冒頭は「ローカルLLMとは、自社や組織のサーバー内に置かれる人工知能モデルのことです。」でした。

OpenAI互換・ストリーム

curl -sSN -o "$OUT_DIR/stream.sse" -w 'HTTP %{http_code}\n' \
  -H 'Content-Type: application/json' \
  --data-binary @- http://127.0.0.1:11234/v1/chat/completions <<'JSON'
{"model":"mlx-community/Qwen3-4B-4bit","max_tokens":64,"stream":true,"messages":[{"role":"user","content":"日本の首都はどこですか。短く答えてください。"}]}
JSON
grep '^data:' "$OUT_DIR/stream.sse"

HTTP 200でした。data: 行が順に返り、最後は data: [DONE] でした。回答は「東京」でした。

Anthropic互換・メッセージ

curl -sS -o "$OUT_DIR/messages.json" -w 'HTTP %{http_code}\n' \
  -H 'Content-Type: application/json' \
  -H 'anthropic-version: 2023-06-01' \
  -H 'x-api-key: local-test' \
  --data-binary @- http://127.0.0.1:11234/v1/messages <<'JSON'
{"model":"mlx-community/Qwen3-4B-4bit","max_tokens":64,"messages":[{"role":"user","content":"推論を英語で一語で説明してください。"}]}
JSON
python3 -m json.tool --no-ensure-ascii "$OUT_DIR/messages.json"

HTTP 200でした。回答は「Inference.」、stop_reason は end_turn でした。このMacで観測した生成速度は34〜90 tokens/秒でした。同時に動いている処理によって変わります。別の機種での速度を示す値ではありません。

エラーと未検証事項

SHA-256が一致しない場合は、その配布物を実行しないでください。11月6日には、別のプログラムがGPUメモリを使っている状態で、同じ3つのリクエストがすべてHTTP 400になりました。エラーは「Prompt (29 tokens) requires ~661MB GPU memory but only ~53MB is available …」でした。このエラーが出たら、まずメモリを使っているプログラムを閉じて再試行してください。ほかのHTTPエラーでは保存した応答とサーバーログを確認してください。

ネットワークを切った状態での実行、App、Homebrew、GGUFは検証していません。解析・テレメトリがないという説明は公式FAQの記載であり、こちらで通信を測定した結果ではありません。評価方法を深める場合はLoRA 評価の解説を参照してください。

どの依頼も、まずお話を伺うところから。

どんな業務か、どんなデータをお持ちか、どこで動かす必要があるかをお聞かせください。2 営業日以内にご返信します。

プロジェクトの相談biz@random-walk.co.jp