Ollama MLX対応で Qwen3.5 を試す|35Bが27Bより速い?MoEの実力をベンチマーク

Ollama MLX対応で Qwen3.5 を試す|35Bが27Bより速い?MoEの実力をベンチマーク — Qwen3.5, Ollama, MLX 技術部

こんにちは、パレイド技術部です。

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は前述の通りなぜかタイムアウトでスキップしています。

モデルThinkingReply TTFT (秒)TPS (tok/s)出力トークン数総応答時間 (秒)
qwen3.5:9bOFF0.29719.8110.86
qwen3.5:9bON41.39116.269743.57
qwen3.5:27bOFF1.4233.7114.38
qwen3.5:27bON187.733.2621195.84
qwen3.5:35bOFF0.41116.4251.94
qwen3.5:35bON56.23315.186157.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 速度のトレードオフ

9b27b35b
モデルサイズ6.6GB17GB23GB
TPS (think OFF)19.83.716.4
TTFT0.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 に戻す対応も取れそうで、今後も楽しみですね。

タイトルとURLをコピーしました