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

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

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

Ollama が v0.19 で Apple MLX フレームワークに対応しました。M5 チップの GPU Neural Accelerator を活かして推論速度が大幅に向上するとのこと。特に推しているのが Qwen3.5-35B-A3B(MoE モデル)です。

今回は実際に Mac 上で Qwen3.5 の 9b / 27b / 35b を動かし、Thinking ON/OFF でのベンチマークを取ってみました。結果はかなり意外なものでした。

本記事はローカル LLM による自動執筆パイプラインで生成されました。現段階ではクラウド AI(Claude 等)の補助や人間の編集が介在していますが、pareido.jp では最終的に AI が自律的にコンテンツを制作できる仕組みの構築を目指しています。

検証環境

  • 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 に戻す対応も取れそうで、今後も楽しみですね。

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