Qwen3.8 27Bは16GB Macで動く、2bitなら

「27Bのモデルが8GBのGPUで動くらしい」と聞いて、自分のMacで試したくなったなら。

動きます。ただし量子化を1段間違えると、生成速度は20分の1になります。しかもその境界は、ファイルサイズにしてわずか1.1GBの差です。

TL;DR: M5 MacBook Pro 16GB での実測。UD-IQ3_XXS(10.93GB)は decode 0.22 tok/s で実用不能。UD-Q2_K_XL(9.83GB)は 2.47〜4.52 tok/s で実用域。差は1.1GBです。

※本記事の数値は2026年8月22日時点の実測値です。


目次

崖はここにある — 1.1GBの差で decode が20倍変わる

結論から出します。同じモデルを2種類の量子化で動かした結果です。

量子化ファイルサイズCPU/GPU 分割decode
UD-IQ3_XXS(3bit)10.93GB19% / 81%0.22 tok/s
UD-Q2_K_XL(2bit)9.83GB11% / 89%2.47〜4.52 tok/s
1.1GB削っただけで20倍 — 16GB Macの量子化の崖。UD-Q2_K_XL(9.83GB)はdecode 4.52 tok/s、UD-IQ3_XXS(10.93GB)は0.22 tok/s

ファイルサイズの差は1.1GB。それで生成速度が最大20倍変わります。

これは「大きいモデルは少し遅い」という線形の話ではありません。ある一点を超えると崩落する、非線形の崖です。

計測条件

数値を出すからには条件を明示します。

項目
機体MacBook Pro M5 / 16GB ユニファイドメモリ / 10コア(4 Super + 6 Efficiency)
OSDarwin 25.5.0
推論エンジンOllama 0.32.14
モデルQwen3.8-27B(2026年8月公開 / Apache 2.0 / 27B・非MoE)
GGUFUnsloth Dynamic v3.0
KVキャッシュq8_0
num_ctx8192(全測定で統一)
iogpu.wired_limit_mb0(macOS既定のまま)
測定時の他アプリChrome・エディタ稼働中(通常の作業状態)

3bit側の0.22 tok/sは、Ollama自身のログに記録された値です。

slot print_timing: id  0 | task 146 | n_gen = 167, tg = 0.22 t/s, tg_3s = 0.14 t/s

167トークンを生成するのに12分かかりました。文章の生成が目で追えるどころか、1文字ずつ待つ体感です。

2bit側の実測4本

崖の手前、UD-Q2_K_XL で4パターン測りました。

タスク入力prefill出力decode実時間
短文質問40 tok300 tok3.03 t/s121秒
コード生成71 tok500 tok2.47 t/s228秒
長文要約3,302 tok57.4 t/s200 tok4.52 t/s104秒
上限なし1問80 tok1,537 tok2.47 t/s643秒(10.7分)

decode は 2.47〜4.52 tok/s の範囲でばらつきます。swapの状態に左右されるため、単一の値で語るのは不正確です。

実用感としては、500トークンのコードに3分48秒。1問に10.7分。読書スピードには追いつきますが、対話のテンポではありません。


「8GB VRAMで動く」は、GPUに収まっていない

今回の検証のきっかけは、Xで見かけた投稿でした。要旨はこうです。

Qwen3.8 27B が RTX 4060(8GB VRAM)で動作中。Unsloth の IQ4_XS 量子化で、ディスク上14.6GB、コンテキスト64,000トークン。prefill 150 tok/s、decode 5 tok/s。25 GPUレイヤーのみオフロードして8GB VRAM内に収まる(溢れなし)

この最後の一文が、実は成立していません。

「25レイヤーのみオフロード」の意味

14.6GBのモデルを8GBのVRAMに載せることは、物理的にできません。では何が起きているか。

モデルの一部(25レイヤー分)だけをGPUに置き、残りをCPU側のメモリに置いているのです。生成のたびに、GPUとCPUを行き来します。

「溢れなし」ではなく、溢れた分をCPUで処理している状態です。だからこそ decode が 5 tok/s という数字になります。GPUに完全に載っていれば、$300のGPUでももっと出ます。

Macでも同じことが起きる

そしてこれは、Macでも同じでした。しかももっと悪い条件でです。

ollama ps の出力を見てください。

NAME                                      SIZE   PROCESSOR          CONTEXT
...Qwen3.8-27B-GGUF:UD-IQ3_XXS            11 GB  19%/81% CPU/GPU    8192
...Qwen3.8-27B-GGUF:UD-Q2_K_XL            10 GB  11%/89% CPU/GPU    8192

PROCESSOR 列が「載ったかどうか」の答えです。100% GPU と出れば完全収容。19%/81% CPU/GPU は、19%がCPU側にこぼれているという意味です。

注目すべきは2bit側です。9.83GBまで削っても、まだ11%が溢れています。

なぜ16GBあるのに10GBが載らないのか

ここは私も最初、誤解していました。「macOSがGPUに割ける上限が低いのだろう」と考えたのですが、実測すると違いました。

まずMetalが申告する上限を確認します。

recommendedMaxWorkingSetSize = 11.84 GB(物理16GBの 74.0%)

11.84GBあります。 9.83GBのモデルなら、KVキャッシュを足しても入りそうに見えます。

ところがOllamaのロード時ログはこうでした。

msg="system memory" total="16.0 GiB" free="4.3 GiB"
msg="gpu memory"    available="11.3 GiB" free="11.8 GiB"

common_params_fit_impl: id=0, n_layer=65, mem=9319 MiB   ← 全層載せる案(不採用)
common_params_fit_impl: set ngl_per_device[0].n_layer=61 ← 実際に採用された層数
load_tensors: offloaded 61/66 layers to GPU

GPU側には11.3 GiB空いているのに、61層分(約8.8GB)しか載せていません。

理由は1行上にあります。system memory free="4.3 GiB"。ユニファイドメモリでは、GPUが使う領域もシステムメモリと同じ物理メモリです。ブラウザやエディタが握っている分は、GPUも使えません。

実際の内訳はこうなりました。

GPU側(MTL0)
├─ モデル本体          8,065.56 MiB
├─ KVキャッシュ          480.00 MiB
├─ 再帰状態              137.16 MiB
└─ 計算バッファ          248.10 MiB
                       ─────────────
                       約 8.7 GiB

CPU側(こぼれた分)
├─ モデル本体            924.30 MiB
├─ repack                 38.55 MiB
└─ KVキャッシュ           32.00 MiB
崖の正体。GPU側に8,930 MiB(61/66層)、CPU側に994 MiB(5/66層)が配置され、モデル本体8,066 MiBが大半を占める

ここでKVキャッシュが 480 MiB しかないことに気づきます。27Bクラスとしては異様に小さい数字です。

理由はモデルの構造にあります。config.json を見ると、64層の内訳がこうなっていました。

"layer_types": 64層 = linear_attention 48層 / full_attention 16層

全64層のうち48層が線形注意(linear attention) です。線形注意層はコンテキスト長に対してKVキャッシュが線形に増えません。だから262,144トークンという長大なコンテキストを持ちながら、メモリは抑えられています。

つまりこのモデルで効いているボトルネックは、KVキャッシュではなく重みそのものです。コンテキストを短くしても状況は改善しません。削るべきは量子化ビット数の方です。

つまり崖の位置は固定ではありません。 「9.83GBまでなら大丈夫」という絶対的な線があるのではなく、モデルサイズと、そのときの空きメモリが交差する場所に崖ができます。

これは実用上、重要な意味を持ちます。

  • 同じモデルでも、Chromeのタブを閉じれば速くなる可能性がある
  • ベンチマーク記事の数値は、測定者のマシンの空き状況に依存する
  • 本記事の decode が 2.47〜4.52 tok/s とばらついたのも、これが理由です

私の測定は「ブラウザとエディタを開いた通常の作業状態」で行いました。クリーンな状態ならもう少し伸びるはずですが、そちらは測っていません。 日常的に使う条件での数字の方が実用的だと判断したためです。

なお sudo sysctl iogpu.wired_limit_mb=13500 でMetal上限自体を引き上げることもできます。ただし空きメモリが足りなければ意味がなく、本記事では測定していません。


2bitに落としても品質は落ちない、落ちるのは速度だけ

「2bit量子化」と聞くと、壊れた日本語を想像するかもしれません。実際に出力させました。

課題はこれです。

Pythonで、指定ディレクトリ配下のMarkdownファイルを再帰的に走査し、YAMLフロントマターの slug フィールドが未設定または日本語を含むファイルの一覧を出力するスクリプトを書け。標準ライブラリのみを使い、エラー処理も含めること。

UD-Q2_K_XL の出力から、核心部分を抜き出します。

# ─── 日本語文字の判定 ───────────────────────────────
# ひらがな: U+3040–U+309F
# カタカナ: U+30A0–U+30FF
# 漢字 (CJK Unified Ideographs): U+4E00–U+9FFF
# 漢字拡張A: U+3400–U+4DBF
# 漢字互換区: U+F900–U+FAFF
_JP_CHAR_RE = re.compile(
    r'[\u3040-\u309f'   # ひらがな
    r'\u30a0-\u30ff'   # カタカナ
    r'\u4e00-\u9fff'   # CJK Unified Ideographs
    r'\u3400-\u4dbf'   # CJK Extension A
    r'\uf900-\ufaff'   # CJK Compatibility
    r']'
)

Unicodeの範囲が5つとも正確です。CJK拡張Aと互換漢字まで含めているのは、日本語判定として実務的に正しい実装です。型ヒント・docstring・argparse も揃っており、「標準ライブラリのみ」という制約も守られていました。

日本語の散文も破綻しません。製造業のAI導入について聞いたところ、「熟練工の暗黙知の否定感」「責任の所在の曖昧化」といった論点が返ってきました。

ここから引き出せる一般則

これは今回の検証で最も再利用価値のある発見です。

メモリが足りないとき、モデルを小さいものに替えるより、同じモデルを深く量子化する方が良い場合があります。

27Bを2bitで動かす方が、7Bを4bitで動かすより良い結果になる可能性があるということです。パラメータ数が持つ知識量は、量子化ビット数の低下より頑健だからです。

ただし今回のように、速度の崖を踏み抜かない範囲でという条件がつきます。


ネイティブ低ビット学習と事後量子化は、別の生き物

ここで混同されやすい話を整理します。

「27BがMacで動く」という話題には、まったく性質の違う2つのモデルが混ざっています。

Qwen3.8-27BBonsai-27B
低ビット化の方法事後量子化(学習後に圧縮)ネイティブ低ビット学習(最初から三値/1bitで訓練)
最小フットプリント9.83GB(2bit)でも溢れる3.9GB(1bit)/ 7.2GB(三値)
16GB Macでの立ち位置崖の縁余裕

同じ「27B」でも、メモリ特性がまるで違います。

事後量子化は、16bitで訓練したモデルを後から2bitに潰す操作です。情報を捨てているので、下げるほど品質リスクが増えます。だから2bitあたりが実用の下限になります。

ネイティブ低ビット学習は、最初から低ビットで表現できるように訓練します。だから1bitでも壊れません。Bonsai-27B が iPhone で動くのはこのためです。

Bonsai-27B の詳細は別記事で扱っています。

「27BがMacで動く」という情報を見たら、まずどちらの種類か確認してください。 必要メモリが2倍以上違います。


prefillを短文で測ると1/17に見える

ベンチマークの読み方の話です。今回、自分で測っていて危うく誤った数字を出しかけました。

同じモデル・同じ設定で、prefill速度がこう変わります。

入力トークン数prefill
40 tok3.4 tok/s
71 tok3.1 tok/s
3,302 tok57.4 tok/s

17倍の差があります。モデルは同じです。

理由は単純で、短い入力では固定オーバーヘッドが支配的になるからです。モデルの初期化やバッファ確保にかかる時間が、実際の処理時間より大きくなります。40トークンを3.4 tok/sで割った「12秒」の大半は、処理ではなく準備です。

つまり prefillのスループットは、数千トークン規模の入力で測らないと意味がありません。

冒頭で引用したX投稿の「prefill 150 tok/s」も、どの長さの入力で測ったかが書かれていません。同条件なら本記事の57.4 tok/sと比較できますが、そこは確認できていません。

ベンチマーク記事を読むときは、入力トークン数が明記されているかを見てください。書かれていなければ、prefillの数字は参考になりません。


踏んだ罠5つ(元投稿には書かれていない)

再現しようとして詰まった箇所です。どれもX投稿には書かれていませんでした。

罠1. UD- プレフィックスが必須。しかも exit code 0 で失敗する

Unsloth Dynamic v3 の量子化には UD- が付きます。

# ✗ 失敗する
ollama pull hf.co/unsloth/Qwen3.8-27B-GGUF:IQ3_XXS

# ✓ 正しい
ollama pull hf.co/unsloth/Qwen3.8-27B-GGUF:UD-IQ3_XXS

厄介なのは失敗の仕方です。11GBのダウンロードが100%まで進んだあと、こう出ます。

pulling cbb841a9ee06: 100% ▕████████████▏ 927 MB/927 MB
Error: file does not exist

そして終了コードは0です。スクリプトで回していると成功扱いになります。ollama list に出ているか必ず確認してください。

罠2. keep-alive の既定5分が短すぎる

Ollamaは既定で、5分使わなければモデルをメモリから降ろします。

この機体では再ロードに数分かかります。5分考えごとをしてから次の質問をすると、また数分待たされます。

APIを叩くなら keep_alive を明示してください。

{"model": "...", "prompt": "...", "keep_alive": "40m"}

罠3. swapがmacOSに自動拡張される

実行中のswap使用量です。

タイミングswap 使用量swap 総量
起動時(通常作業状態)3.42GB5.12GB
モデルロード中11.4GB12.29GB に自動拡張
実行中ピーク12.76GB12.29GB
アンロード後7.18GB10.24GB

macOSがswap領域を勝手に広げます。空き物理メモリは実行中ずっと0.05〜0.13GBに張り付きました。

SSDへの書き込みが継続的に発生するため、他のアプリを開いたままの常用は現実的ではありません

罠4. MTPは別ファイルで、Ollama単体では使えていない

元投稿の「native MTP経由でdecode 5 tok/s」という記述について。

MTP(Multi-Token Prediction)は投機的デコードを可能にする仕組みです。ただしUnslothのリポジトリでは、独立したファイルとして提供されています。

MTP/mtp-Qwen3.8-27B-Q4_0.gguf   1.37GB   ← ドラフトモデル

これをドラフトモデルとして併用するには、llama.cpp の --spec-type draft-mtp に相当する指定が必要です。本記事のOllamaでの測定では使っていません。 つまり元投稿の5 tok/sとは、そもそも構成が違います。

罠5. 測定用のスクリプトごと落とされる

swapが12GBを超えた状態では、測定用に走らせたシェルスクリプト自体がシステムに停止させられました。

長時間の生成を測るなら、nohup でプロセスを切り離し、結果を都度ディスクへ書き出してください。メモリ逼迫下では、監視している側が先に落ちます。


16GB Macで実際に選ぶべきモデル

ここまでの結論として、16GB Macで27B denseモデルを常用するのは薦めません。

動きはします。しかし500トークンのコードに3分48秒、1問に10.7分。Claude Codeのようなエージェント用途では成立しません。

16GBの現実解

同じ16GB機で実用になっている構成です。

モデルサイズ特徴
qwen3-coder:30b18GBMoE。パラメータ総数は大きいが、実際に動く部分(active parameter)が小さい
gemma4:12b7.6GBdense 12B。余裕を持って収まる
LFM2.5-8B-A1B5.2GB軽量。ビジネス文書用途で実績あり

dense 27Bは24GB以上の機体の領域です。パラメータ数だけでなく、denseかMoEかを見てください。同じ「30B」でもMoEなら16GBで動きます。

8GBの壁、16GBの崖

この機体で以前、14Bまでを測ったことがあります。Qwen3-14B-4bit が生成14.69 tok/s、ピークメモリ8.42GB。16GBの53%で収まり、実用域でした。

そのとき「メモリの壁が8GB→16GBで1段階動いた」と書きました。今回はその続きです。

  • 8GBの壁: 14B級が動かない
  • 16GBの実用域: 14Bまで(8.42GB / 14.7 tok/s)
  • 16GBの崖: 9.83GBと10.93GBの間

壁は「動く/動かない」の境界でした。崖は「動くが実用にならない」の境界です。16GBで気をつけるべきは、壁ではなく崖の方です。

関連記事:


再現手順

同じ検証をする場合の手順です。

# 1. 取得(UD- を忘れない。exit 0 で失敗するので list で確認)
ollama pull hf.co/unsloth/Qwen3.8-27B-GGUF:UD-Q2_K_XL
ollama list | grep qwen3.8

# 2. KVキャッシュ量子化を有効にして起動
OLLAMA_KV_CACHE_TYPE=q8_0 ollama serve

# 3. keep_alive を明示して実行
curl -s -X POST http://localhost:11434/api/generate -d '{
  "model": "hf.co/unsloth/Qwen3.8-27B-GGUF:UD-Q2_K_XL",
  "prompt": "(プロンプト)",
  "stream": false,
  "keep_alive": "40m",
  "options": {"num_ctx": 8192, "temperature": 0.6}
}'

# 4. GPUに載ったかを確認(PROCESSOR列が答え)
ollama ps

# 5. 実測値はOllama自身のログが確実
grep print_timing ~/.ollama/logs/server.log | tail -3

num_ctx を変えるとモデルの再ロードが走ります。比較測定をするなら、全ての測定で同じ値に固定してください。


本記事で測っていないこと

正直に書いておきます。以下は未測定です。

  • iogpu.wired_limit_mb を13500に引き上げた場合の UD-IQ3_XXS の速度
  • UD-IQ2_S(8.37GB)が100% GPUに載るかどうか
  • MTPドラフトモデルを併用した投機的デコードの効果
  • MLX版(mlx-community)との比較

特に1つ目は、3bitの品質を保ったまま速度を得られる唯一の道筋です。上限を引き上げれば崖の向こう側に橋が架かる可能性があります。測定でき次第、追記します。


まとめ

Qwen3.8-27B を M5 MacBook Pro 16GB で動かした実測結果をまとめます。

要点の再掲

  • UD-IQ3_XXS(10.93GB)は decode 0.22 tok/s で実用不能
  • UD-Q2_K_XL(9.83GB)は 2.47〜4.52 tok/s で実用域
  • 1.1GBの差で20倍変わる非線形の崖がある
  • 崖の位置は固定ではなく、モデルサイズと空きメモリが交差する場所にできる
  • 2bitでも品質は保たれる。落ちるのは速度だけ
  • prefillは数千トークンの入力で測らないと17倍ずれる

そして最も持ち帰る価値があるのはこれです。

メモリが足りないとき、モデルを小さいものに替えるより、同じモデルを深く量子化する方が良い場合があります。 ただし崖の位置を知らないと踏み抜きます。

16GB Macをお使いなら、まず ollama psPROCESSOR 列を見る習慣をつけてみてください。100% GPU になっていないモデルは、あなたが思っているより遅くなっています。


よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次