こんにちは、パレイド技術部です。
Ollama が v0.19 で Apple MLX フレームワークに対応しました。M5 チップの GPU Neural Accelerator を活かして推論速度が大幅に向上するとのこと。特に推しているのが Qwen3.5-35B-A3B(MoE モデル)です。
今回は実際に Mac 上で Qwen3.5 の 9b / 27b / 35b を動かし、Thinking ON/OFF でのベンチマークを取ってみました。結果はかなり意外なものでした。
検証環境
- MacBook Air (M5, 32GB Unified Memory)
- macOS 15.4
- Ollama v0.19(MLX 対応版)
- モデル: qwen3.5:4b / qwen3.5:9b / qwen3.5:27b / qwen3.5:35b
35B を試してみたが…
Ollama ブログの推奨する qwen3.5:35b を試してみました。ollama list で確認するとモデルサイズは 23GB。動作自体は問題なく、MLX 対応の恩恵かロードも速い。
ただし、実行中のメモリ消費は約 30GB に達します。32GB の Mac ではブラウザを閉じてようやく動く程度で、常用は厳しい印象です。Ollama ブログでも「32GB 以上推奨」と書いていますが、35B を快適に使うには 64GB 以上が現実的でしょう。
一方で、27b は 17GB 程度に収まるため、32GB Mac でも他のアプリと共存できます。メモリ制約のある環境では 27b の方が「使える」モデルです。
…と思っていたのですが、ベンチマークを取ったら話が変わりました。
4B はなぜかタイムアウト
まず最小の qwen3.5:4b を試したのですが、ベンチマークスクリプトが応答を返さずタイムアウトしました。Ollama 自体は起動しており、モデルのロードも正常に完了します。ストリーミングで確認すると thinking チャンクは流れてくるのですが、そこから先に進まず、数分待っても応答(response)が返りません。
ollama run qwen3.5:4b で対話的に使うと普通に動くので、API 経由での特定条件(temperature 0.0 + stream)で再現する問題かもしれません。原因は特定できておらず、今回のベンチマークからは除外しています。
ベンチマーク: シンプルに「こんにちは」への応答速度
Ollama の Chat API にストリーミングで「こんにちは」を投げ、以下を計測しました。
- Reply TTFT: 最初の応答トークンが返るまでの時間(秒)
- TPS: 応答トークンの生成速度(tokens/sec)
- Thinking ON/OFF: qwen3.5 は thinking(推論過程の内部生成)がデフォルト ON
各モデル 3 回実行の中央値です。4bは前述の通りなぜかタイムアウトでスキップしています。
| モデル | Thinking | Reply TTFT (秒) | TPS (tok/s) | 出力トークン数 | 総応答時間 (秒) |
|---|---|---|---|---|---|
| qwen3.5:9b | OFF | 0.297 | 19.8 | 11 | 0.86 |
| qwen3.5:9b | ON | 41.391 | 16.2 | 697 | 43.57 |
| qwen3.5:27b | OFF | 1.423 | 3.7 | 11 | 4.38 |
| qwen3.5:27b | ON | 187.73 | 3.2 | 621 | 195.84 |
| qwen3.5:35b | OFF | 0.411 | 16.4 | 25 | 1.94 |
| qwen3.5:35b | ON | 56.233 | 15.1 | 861 | 57.7 |
35B が 27B より 4倍速い?
最も目を引くのが 35b (16.4 tok/s) vs 27b (3.7 tok/s) の差です。パラメータ数が多い 35B の方が圧倒的に速い。
これは Qwen3.5-35B が MoE(Mixture of Experts) アーキテクチャだからと考えられます。
- 35B-A3B: 総パラメータ 35B だが、推論時に活性化されるのは 3B だけ
- 27B: 全パラメータが密結合(Dense)で、推論時に全 27B を使う
計算量だけ見ると、35B は実質 3B モデル並みです。知識は 35B 分あるのに、速度は 3B 級。MoE の設計思想がそのまま数字に出た形です。
メモリ vs 速度のトレードオフ
| 9b | 27b | 35b | |
|---|---|---|---|
| モデルサイズ | 6.6GB | 17GB | 23GB |
| TPS (think OFF) | 19.8 | 3.7 | 16.4 |
| TTFT | 0.3秒 | 1.4秒 | 0.4秒 |
| 実メモリ消費 | 〜10GB | 〜20GB | 〜30GB |
32GB の Mac で使う場合、27b は「メモリに収まるが遅い」、35b は「速いがメモリがギリギリ」。実用上、9b が最もバランスが良く、メモリに余裕があるなら 35b を試す価値がある、という構図です。
Thinking モードの罠
Qwen3.5 は thinking(Chain-of-Thought 的な内部推論)がデフォルト ON です。「こんにちは」のような単純な挨拶に対しても、9b で 41 秒、27b で 188 秒も thinking に費やします。
Thinking ON の場合、最初の応答が返るまで何も表示されないので、ユーザーからは「フリーズした」ように見えます。実際、ベンチマークスクリプトのデバッグ中にこれにハマりました。
チャットボット用途など即応性が必要な場面では think: false を明示的に指定するのが実用的です。Ollama の Chat API では以下のように制御できます。
{
"model": "qwen3.5:9b",
"messages": [{"role": "user", "content": "こんにちは"}],
"think": false,
"stream": true
}
計測スクリプト
今回の計測に使ったスクリプトを載せておきます。依存は requests のみで、Ollama が動いていれば単独で実行できます。
#!/usr/bin/env python3
"""Qwen3.5 ベンチマーク: Thinking ON/OFF で TTFT + TPS を計測"""
import json, sys, time, requests
BASE_URL = "http://localhost:11434"
MODELS = ["qwen3.5:9b", "qwen3.5:27b", "qwen3.5:35b"]
PROMPT = "こんにちは"
RUNS = 3
def bench(model, think):
payload = {
"model": model,
"messages": [{"role": "user", "content": PROMPT}],
"think": think,
"options": {"temperature": 0.0},
"stream": True,
}
start = time.time()
reply_ttft = None
final = {}
with requests.post(f"{BASE_URL}/api/chat", json=payload,
timeout=(10, 600), stream=True) as r:
r.raise_for_status()
for line in r.iter_lines():
if not line: continue
chunk = json.loads(line)
msg = chunk.get("message", {})
if msg.get("content") and reply_ttft is None:
reply_ttft = time.time() - start
if chunk.get("done"):
final = chunk
break
ec = final.get("eval_count", 0)
ed = final.get("eval_duration", 0)
tps = round(ec / (ed / 1e9), 1) if ec and ed else None
td = final.get("total_duration", 0)
return reply_ttft, tps, ec, round(td / 1e9, 2) if td else None
for model in MODELS:
for think in [False, True]:
results = [bench(model, think) for _ in range(RUNS)]
# 中央値(TPS で取る)
results.sort(key=lambda x: x[1] or 0)
mid = results[len(results)//2]
mode = "ON" if think else "OFF"
print(f"{model} think={mode}: "
f"TTFT={mid[0]:.3f}s TPS={mid[1]} "
f"tokens={mid[2]} total={mid[3]}s")
まとめ
- 9b が最も実用的。TTFT 0.3 秒、20 tok/s で十分快適。メモリも 10GB 程度
- 35b は MoE の恩恵で 27b より 4 倍速いが、メモリ 30GB 必要。32GB Mac ではギリギリ
- 27b は中途半端?。メモリは 35b より少ないが、速度が 3.7 tok/s では厳しい
- 4b は API 経由だとなぜかタイムアウト。原因不明、要追調査
- Thinking はデフォルト ON。即応性が必要なら
think: falseを指定すること
今回の Ollama の MLX 対応で Apple Silicon の推論環境は確実に良くなりました。特に Qwen3.5 は魅力的ですが、Ollama API での利用は制約が多く、自作ツールでは直接 MLX を使った実装に切り替えていました。今回の対応でまた Ollama に戻す対応も取れそうで、今後も楽しみですね。


