2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

M5 MacBook (Apple Silicon) でvllm-mlxを動かす検証

2
Posted at

vllm-mlx M5 MacBook 検証ノート

Apple Silicon上でvLLMライクな推論を行うための検証記録です。
今回の検証では、M5でもvllm-mlxは問題なく動作し、Metal GPUを利用した推論を確認できました。
環境:MacBook Pro M5 / 24GB ユニファイドメモリ

本記事の内容はM5での動作を保証するものではありません。
(現時点でドキュメントにはM5のサポートは明記されていないため)

1. この記事でわかること

  • M5でのGPU推論(Metal / MLX)確認
  • OpenAI互換API動作
  • Continuous batchingで約2倍スループット改善(簡単な処理時間計測込み)

2. 背景

Apple Silicon 環境で LLM 推論を並列処理する方法を調べていました。
並列処理の代表的なライブラリとしてvLLMが挙げられますが、現時点で本家 vLLM は Apple Metal GPU に正式対応しておらず、Mac 環境では CPU 推論が中心になります。

そんな中、以下の記事でApple Silicon向けのvllm-mlxの存在を知りました。

vllm-mlxのドキュメントでサポートが明記されているのはM1〜M4まででした。
しかし、手軽に検証できる環境が先日購入したM5 MacBook Proしかなく、とりあえず試してみようと思い検証を実施しました。

3. vllm-mlxとは

vllm-mlx は、Apple Silicon向けにvLLMライクな推論APIを提供するOSSプロジェクト。

なお、同様のプロジェクトとしてvllm-metal が存在しますが、今回はvllm-mlxで検証を行いました。また、今回の検証はテキスト生成のみで、マルチモーダルは利用していません。

内部構成

vllm-mlx は vLLM の API を模倣したレイヤーを持ちつつ、推論処理は Apple の ML フレームワーク MLX を利用して実行されます。

参考: architecture.md

主な機能

  • LLMテキスト推論(mlx-lm)
  • 画像・動画理解(mlx-vlm)
  • 音声 TTS / STT(mlx-audio)
  • テキスト埋め込み(mlx-embeddings)
  • OpenAI互換 REST APIサーバー
  • Anthropic Messages API互換エンドポイント
  • Continuous Batching(-continuous-batching フラグで有効化)
  • Paged KV Cache

参考: README.md

4. 動作確認環境

項目 内容
マシン MacBook Pro M5
チップ Apple M5(10コア GPU)
ユニファイドメモリ 24GB
OS macOS Tahoe v26.3.1
使用モデル (README.md記載) mlx-community/Llama-3.2-3B-Instruct-4bit

補足: 現時点でvllm-mlxがサポートを明記しているのはM1〜M4までです。
M5の動作は未記載ですが、今回の検証では問題なく動作しました。

5. インストール

前提

  • macOS on Apple Silicon (M1/M2/M3/M4)
  • Python 3.10+
  • uv (推奨)

補足: 上記の通り、M5については明記されていないものの、今回の検証では動作しました。
また、Pythonは3.13.12を利用しました。

インストール方法の選択

用途によって2つの方法があります。

# CLIツールとしてシステム全体にインストール
# サーバーとして起動するだけであればこちらで十分
uv tool install git+https://github.com/waybarrios/vllm-mlx.git

# venv内にインストール(ドキュメント推奨)
# Python APIをimportして使う場合はこちらが必要
git clone https://github.com/waybarrios/vllm-mlx.git
cd vllm-mlx
uv pip install -e .

e(editableモード)はリポジトリのソースコードを直接参照するインストール方式。

インストール確認

# CLI確認
vllm-mlx --help

# Python APIの確認(venv内インストールの場合)
python -c "from vllm_mlx.models import MLXLanguageModel; print('OK')"

参考: installation.md

6. サーバー起動 (基本例)

vllm-mlx serve mlx-community/Llama-3.2-3B-Instruct-4bit --port 8000
  • 初回実行時はHugging Faceからモデルをダウンロード
  • 2回目以降はキャッシュから読み込まれる

起動モード

フラグ モード エンジン 特徴
なし(デフォルト) Simple SimpleEngine 単一ユーザー・最大スループット
--continuous-batching Continuous Batching BatchedEngine 複数ユーザー・並列処理

補足: その他のオプションは以下を参照。
CLI Reference
Configuration Reference

参考:
OpenAI-Compatible Server

参考: サーバー正常起動時のログ (抜粋)

Simpleモード(順次)
Loading model: mlx-community/Llama-3.2-3B-Instruct-4bit
Mode: Simple (maximum throughput)
INFO: SimpleEngine loaded (MLLM=False)
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)
Continuous Batchingモード(並列)
Mode: Continuous batching (for multiple concurrent users)
INFO: BatchedEngine loaded: mlx-community/Llama-3.2-3B-Instruct-4bit (mllm=False)
INFO: Metal memory limits set: allocation_limit=17.2GB (90% of 19.1GB), cache_limit=32GB
INFO: MemoryAwarePrefixCache initialized: max_memory=2127.2MB, max_entries=1000
INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

7. OpenAI SDK経由の基本コード

逐次処理・並列処理それぞれで処理時間を計測しました。

前提:openai SDKのインストール

pip install openai

逐次リクエスト (Simpleモード)

サーバー起動 (Simpleモード)
vllm-mlx serve mlx-community/Llama-3.2-3B-Instruct-4bit --port 8000
import time
from openai import OpenAI

client = OpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
)

prompts = [
    "日本の首都はどこですか?一言で答えてください。",
    "LLMとは何ですか?2文で説明してください。",
    "PythonでFizzBuzzを実装してください。",
    "機械学習とディープラーニングの違いを説明してください。",
    "富士山の高さは何メートルですか?",
]

total_start = time.time()
print("------ Simple Mode -------\n")
for i, prompt in enumerate(prompts):
    start = time.time()
    response = client.chat.completions.create(
        model="default",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=512,
        temperature=0.7,
    )
    elapsed = time.time() - start
    print(f"--- Prompt {i+1} ({elapsed:.2f}s) ---")
    print(f"Q: {prompt}")
    print(f"A: {response.choices[0].message.content}")
    print()

total_elapsed = time.time() - total_start
print(f"総処理時間: {total_elapsed:.2f}s")
print(f"プロンプト数: {len(prompts)}")

主なパラメータ(任意)

パラメータ 説明 今回の設定
max_tokens 最大生成トークン数 512
temperature
(0=決定的、1=多様)
生成のランダム 0.7

並列リクエスト (Continuous Batchingモード)

サーバー起動(Continuous Batchingモード)
vllm-mlx serve mlx-community/Llama-3.2-3B-Instruct-4bit --port 8000 --continuous-batching
import asyncio
import time
from openai import AsyncOpenAI

client = AsyncOpenAI(
    base_url="http://localhost:8000/v1",
    api_key="not-needed",
)

prompts = [
    "日本の首都はどこですか?一言で答えてください。",
    "LLMとは何ですか?2文で説明してください。",
    "PythonでFizzBuzzを実装してください。",
    "機械学習とディープラーニングの違いを説明してください。",
    "富士山の高さは何メートルですか?",
]

async def request(prompt: str, index: int) -> dict:
    start = time.time()
    response = await client.chat.completions.create(
        model="default",
        messages=[{"role": "user", "content": prompt}],
        max_tokens=512,
        temperature=0.7,
    )
    elapsed = time.time() - start
    return {
        "index": index,
        "prompt": prompt,
        "output": response.choices[0].message.content,
        "elapsed": elapsed,
    }

async def main():
    total_start = time.time()
    print("------ Continuous Batching Mode -------\n")

    tasks = [request(p, i) for i, p in enumerate(prompts)]
    results = await asyncio.gather(*tasks)

    total_elapsed = time.time() - total_start

    for r in results:
        print(f"--- Prompt {r['index'] + 1} ({r['elapsed']:.2f}s) ---")
        print(f"Q: {r['prompt']}")
        print(f"A: {r['output']}")
        print()

    print(f"総処理時間: {total_elapsed:.2f}s")
    print(f"プロンプト数: {len(prompts)}")

# .pyファイルとして実行する場合
asyncio.run(main())

# Jupyter / ノートブック環境の場合(イベントループが既に動いているため)
# await main()

逐次処理と並列処理の違い

項目 逐次処理 並列処理
クライアント OpenAI AsyncOpenAI
処理方式 for ループで順番に投入 asyncio.gather で同時投入
サーバーモード Simple Continuous Batching
総処理時間の目安 各リクエストの合計 最も遅いリクエストに近い

計測結果 (5プロンプト、Llama-3.2-3B-Instruct-4bit、M5 / 24GB)

逐次処理 並列処理
総処理時間 18.14s 8.96s
短縮率 - 約50%(約2倍のスループット)

並列処理では重いリクエスト(FizzBuzz・機械学習の説明)が同時に走るため、軽いリクエスト(一言回答)が先に返ってくる。逐次処理では重いリクエストが終わるまで後続が待たされていたのが解消される。

参考: 検証時の出力

逐次処理
------ Simple Mode -------

--- Prompt 1 (0.23s) ---
Q: 日本の首都はどこですか?一言で答えてください。
A: 東京です。

--- Prompt 2 (2.18s) ---
Q: LLMとは何ですか?2文で説明してください。
A: LLMは、Machine Learning (マシン・リーニング) を使用したための言語モデルです。これは、コンピュータが自然言語を理解して生成する能力を高めるために開発されたことでしょう。

この言語モデルの主な機能は、文脈に基づいて文章を生成し、改善する能力です。これには、事前に大量のデータを取り入れて、コンピュータがこれらのデータから一般的なパターンや規則を学ぶプロセスが含まれます。

--- Prompt 3 (6.50s) ---
Q: PythonでFizzBuzzを実装してください。
A: # FizzBuzz実装
## Python版

FizzBuzzは、数値1から100までの数字に対して、数字が3の倍数の場合に「Fizz」、数字が5の倍数の場合に「Buzz」、両方の場合に「FizzBuzz」という文字列を出力するゲームです。

### Code

def fizz_buzz(n):
    """
    FizzBuzzゲームを実行します。

    Args:
        n (int): ゲームの範囲 (1-100)

    Returns:
        None
    """
    for i in range(1, n+1):
        if i % 3 == 0 and i % 5 == 0:
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)

# ゲームの範囲を指定
n = 100

# FizzBuzzゲームを実行
fizz_buzz(n)

### Usage

1. Pythonを実行します。
2. `fizz_buzz()`関数を呼びます。最初の引数は、ゲームの範囲です (1-100)。
3. ゲームが実行され、数字1から100までの数字に対して、適切な文字列が出力されます。

### Example Output

1
2
Fizz
4
Buzz
Fizz
7
8
Fizz
Buzz
11
Fizz
13
14
FizzBuzz
...


### Note

このコードは、数字1から100までの数字に対してFizzBuzzゲームを実行します。ゲームの範囲を指定するために、引数`n`が使用されます。

--- Prompt 4 (8.72s) ---
Q: 機械学習とディープラーニングの違いを説明してください。
A: 機械学習とディープラーニングは、コンピュータが人類の知的能力を模倣するために使用される 2 つの重要なテクノロジです。両方とも、コンピュータがデータから新しい知識やパターンを学ぶために使用されますが、ディープラーニングは、機械学習のより高度な形態であり、特に認知学習、生成学習、自然言語処理など、より複雑なタスクに最適です。

**機械学習**

機械学習は、コンピュータがデータから新しい知識やパターンを学ぶために使用される手法です。コンピュータは、データを分析してパターンを特定し、基礎となる知識を学ぶために使用されます。機械学習は、以下の方法で使用されます。

- 検索
- 分類
- 回帰
- 回帰

機械学習は、コンピュータがデータから新しい知識やパターンを学ぶために使用される手法です。

**ディープラーニング**

ディープラーニングは、コンピュータが人類の知的能力を模倣するために使用される技術です。ディープラーニングは、機械学習のより高度な形態であり、特に認知学習、生成学習、自然言語処理など、より複雑なタスクに最適です。ディープラーニングは、以下の方法で使用されます。

- 認知学習
- 生成学習
- 自然言語処理

ディープラーニングは、コンピュータが人類の知的能力を模倣するために使用される技術です。

**ディープラーニングと機械学習の違い**

ディープラーニングと機械学習は、コンピュータが人類の知的能力を模倣するために使用される技術です。以下はディープラーニングと機械学習の違いです。

- **コンピュータの知能レベル**:ディープラーニングは、コンピュータが人類の知的能力を模倣するために使用される技術

--- Prompt 5 (0.52s) ---
Q: 富士山の高さは何メートルですか?
A: 富士山は日本の名物です。富士山の高さは3,776メートルです。

総処理時間: 18.14s
プロンプト数: 5
並列処理

------ Continuous Batching Mode -------

--- Prompt 1 (0.33s) ---
Q: 日本の首都はどこですか?一言で答えてください。
A: 東京です。

--- Prompt 2 (2.49s) ---
Q: LLMとは何ですか?2文で説明してください。
A: vLLMとは、vLLM(Versatile Large Language Model)は、Googleが開発した transformerベースの言語モデルです。このモデルは、自然言語処理や機械翻訳などのタスクに対して高い精度を示すことができ、これがvLLMの特徴です。vLLMは、各テキストを分割して、 Words、Shortcuts、またはBERTなどの特定の構造を使用して、タスクに適切な解釈を提供します。

--- Prompt 3 (8.96s) ---
Q: PythonでFizzBuzzを実装してください。
A: **FizzBuzzのPython実装**
=========================

FizzBuzzは、数字1から100までの数値を出力します。で割り切れた場合に「Fizz」、で割り切れた場合に「Buzz」、それぞれの数が表示されます。

def fizz_buzz(n):
    """FizzBuzzの実装"""
    for i in range(1, n+1):
        if i % 3 == 0 and i % 5 == 0:
            print("FizzBuzz")
        elif i % 3 == 0:
            print("Fizz")
        elif i % 5 == 0:
            print("Buzz")
        else:
            print(i)

# 1から100までのFizzBuzzを実行
fizz_buzz(100)

## **バージョン2: 条件式式**

FizzBuzzをより簡単に実装できます。

def fizz_buzz(n):
    """FizzBuzzの実装"""
    for i in range(1, n+1):
        print("Fizz" if i % 3 == 0 and i % 5 == 0 else
              "Buzz" if i % 3 == 0 else
              "Fizz" if i % 5 == 0 else
              i)

# 1から100までのFizzBuzzを実行
fizz_buzz(100)

## **バージョン3: List Comprehension**

FizzBuzzをさらに簡略化できます。

def fizz_buzz(n):
    """FizzBuzzの実示"""
    result = [str(i) for i in range(1, n+1) if i % 3 == 0 and i % 5 == 0 or
              i % 3 == 0 or i % 5 == 0]
    return "\n".join(result)

# 1から100までのFizzBuzzを出力
print(fizz_buzz(100))

## **バージョン4: Generator**

FizzBuzzをもっと簡単に実装できます。

def fizz_buzz(n):
    """FizzBuzzの実示"""
    for i in range(1, n+1):
        yield "Fizz" if i % 3 == 0 and i % 5 == 0 else
               "Buzz" if i % 3 == 0

--- Prompt 4 (8.96s) ---
Q: 機械学習とディープラーニングの違いを説明してください。
A: 機械学習とディープラーニングは、Artificial Intelligence(AI)とMachine Learning(ML)に属する 2つの重要な概念です。両者は、データから知識を学ぶ能力を持つAIシステムを開発するために使用されます。ただし、機械学習とディープラーニングにはさまざまな違いがあります。

**機械学習(Machine Learning、ML)

機械学習は、データから知識を学ぶ能力を持つAIシステムを開発するために使用される技術です。機械学習では、システムは一定のレベルで、既存のデータからパターンや関係を学ぶことができます。この学習プロセスは、データを分析して、システムが特定のタスクや問題に適したようにパラメータを最適化します。

特徴:

* データに基づいてパラメータを最適化します
* パラメータはデータに基づいて fine-tuning されます
* 1回の学習プロセスで基本的な知識が得られる

**ディープラーニング(Deep Learning、DL)

ディープラーニングは、 machines であるDeep Learning を使用して、複雑なデータから知識を学ぶ能力を持つAIシステムを開発するために使用される技術です。ディープラーニングでは、複数層のニューラルネットワークを使用して、複雑なパターンや関係を学ぶことができます。この学習プロセスは、データを分析して、システムが特定のタスクや問題に適したようにパラメータを最適化します。

特徴:

* データに基づいてパラメータを最適化します
* パラメータはデータに基づいて fine-tuning されます
* 1回の学習プロセスで複雑な知識が得られる

**主な違い**

機械学習とディープラーニングの主な違いは、層数とパラメータの数です。

* 機械学習: 1回の学習プロセスで基本的な知識が得られる
* ディープラーニング: 1回の学習プロセスで複雑な知識が得られる

また、ディープラーニ

--- Prompt 5 (0.67s) ---
Q: 富士山の高さは何メートルですか?
A: 富士山の高さは3,776メートルです。

総処理時間: 8.96s
プロンプト数: 5

8. Python API

uv pip install -e . (上記)でインストールした場合に利用可能です。
サーバーを立てずにPythonから直接推論が実行できます。

基本的な使い方

from vllm_mlx.models import MLXLanguageModel

model = MLXLanguageModel("mlx-community/Llama-3.2-3B-Instruct-4bit")
model.load()

# chat()を使う(チャットテンプレートを自動適用するため推奨)
messages = [{"role": "user", "content": "LLMとは何ですか?2文で説明してください。"}]
response = model.chat(messages)
print(response.text)

参考: Python API

generate() と chat() の違い

generate() chat()
入力 生のプロンプト文字列 messagesリスト
チャットテンプレート 適用されない(手動で適用する必要) 自動で適用される
出力品質 チャットテンプレートなしでは不安定 安定

検証メモ: generate() にチャットテンプレートなしでプロンプトを渡すと、Instructモデルが指示に従わずループや意図しない出力が発生しやすい。
chat() は内部で tokenizer.apply_chat_template() を呼び出すため、transformers での手動適用と同等の処理が自動で行われる。
参考: vllm_mlx/models/llm.py

参考: 検証コード&ログ

コード
from vllm_mlx.models import MLXLanguageModel

# Load model
model = MLXLanguageModel("mlx-community/Llama-3.2-3B-Instruct-4bit")
model.load()

# 1. Generate text
output = model.generate("LLMについて2文で教えてください。")
print(output.text)

# 2. Chat Interface
messages = [
    {"role": "user", "content": "LLMについて2文で教えてください。"}
]
response = model.chat(messages)
print(response.text)
ログ
1. Generate text 出力結果(ループ発生)
Large Language Model (LLM)は、AIによって開発された人工知能(人工に)が、人間の知能を模倣するためのパラダイムに適しているという不明確な概念です。LLMは、人工知能が人間のトピックとテキストを分析し、生成する能力を高めるために設計されています。LLMは、人工知能が人間の知能を模倣するためのパラダイムに適しているという不明確な概念です。LLMは、人工知能が人間のトピックとテキストを分析し、生成する能力を高めるために設計されています。
LLMを使用することで、人工知能は、人間の知能を模倣するためのパラダイムに適しているという不明確な概念を超えて、人間の知能を模倣するためのパラダイムに適しているという不明確な概念を超えて、人間の知能を模倣するためのパラダイムに適しているという不明確な概念を超えて、人間の知能を模倣するためのパラ
2. Chat Interface 出力結果
LLM(Large Language Model)は、 Reinforcement Learning from Human Feedback(人工的なフィードバックから学習する強化学習)やSelf-Supervised Learning(自律的な学習)を使用して、深度神経ネットワークに基づいています。
LLMは、文書や文章を分析して生成する能力が強く、さまざまなアプリケーションに使われるようになりました。

※ 出力形式は安定しているが、内容は誤りである点に注意(RLHMを中心としている点など)

OpenAI SDK経由との使い分け (例)

用途 アプローチ
本番バッチ処理・並列処理 OpenAI Server + AsyncOpenAI
パラメータを細かく制御したい OpenAI Server経由
手軽な動作確認・プロトタイプ MLXLanguageModel.chat()
サーバーを立てたくない場面 MLXLanguageModel.chat()

9. GPU使用の確認

Metal GPU が実際に使われているかを確認するため、
powermetrics で GPU 使用率を計測しました。

推論実行中に別ターミナルで以下を実行すると確認できます:

sudo powermetrics --samplers gpu_power -i 1000 -n 5

平常時 vs 推論中の比較

指標 平常時 推論中
HW active residency 1〜4% 100%
動作周波数 338 MHz(最低域) 1200〜1260 MHz(最高域)
GPU Power 3〜10 mW 5,400〜5,500 mW
idle residency 96〜99% 0%

推論中はGPUが最高周波数帯にフル稼働しており、Metal経由でGPUが使われていることを確認しました。

参考: GPU使用確認ログ

ログ
平常時のログ
*** Sampled system activity (Wed Mar 11 15:37:05 2026 +0900) (1006.29ms elapsed) ***

**** GPU usage ****

GPU HW active frequency: 338 MHz
GPU HW active residency:   1.50% (338 MHz: 1.5% 486 MHz:   0% 636 MHz:   0% 796 MHz:   0% 888 MHz:   0% 988 MHz:   0% 1084 MHz:   0% 1182 MHz:   0% 1278 MHz:   0% 1374 MHz:   0% 1470 MHz:   0% 1578 MHz:   0% 1620 MHz:   0%)
GPU SW requested state: (P1 : 100% P2 :   0% P3 :   0% P4 :   0% P5 :   0% P6 :   0% P7 :   0% P8 :   0% P9 :   0% P10 :   0% P11 :   0% P12 :   0% P13 :   0%)
GPU idle residency:  98.50%
GPU Power: 3 mW
推論中のログ
*** Sampled system activity (Wed Mar 11 15:37:32 2026 +0900) (1002.69ms elapsed) ***

**** GPU usage ****

GPU HW active frequency: 1233 MHz
GPU HW active residency: 100.00% (338 MHz:   0% 486 MHz:   0% 636 MHz:   0% 796 MHz:   0% 888 MHz:   0% 988 MHz:   0% 1084 MHz:  31% 1182 MHz:  24% 1278 MHz:  21% 1374 MHz:  14% 1470 MHz: 3.7% 1578 MHz: 2.9% 1620 MHz: 2.5%)
GPU SW requested state: (P1 :   0% P2 :   0% P3 :   0% P4 :   0% P5 :   0% P6 :   0% P7 :   0% P8 :   0% P9 :   0% P10 :   0% P11 :   0% P12 :   0% P13 : 100%)
GPU idle residency:   0.00%
GPU Power: 5498 mW

10. まとめ

今回の検証では、Apple Silicon M5 環境でも vllm-mlx が問題なく動作しました。
ただし、現時点で公式がサポートを明記していない点には注意が必要です。

確認できたポイント:

  • MLXバックエンドでGPU推論が動作
  • OpenAI互換APIとして利用可能
  • Python APIも利用可能
  • Continuous batchingにより約2倍のスループット改善

今後検証したいこと:

  • より大きいサイズのモデルでの品質・速度確認
  • Mac Studioへの展開

Apple Silicon環境でローカルLLMを並列処理したい場合、
vllm-mlxは有力な選択肢の一つになりそうです。

参考文献

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?