統合メモリ構成とグラボ構成を比べた回で、ひとつ宿題が残っていました。-ngl 99 を付けると起動に失敗し、省略すると llama.cpp が勝手に載る分だけ載せてくれて 44.2 tok/s 出る——ここまでは分かったのですが、その先の「自分で細かく指定する」領域は手つかずだったのです。

ローカルLLMには 「モデルのどの部分をGPUに置き、どの部分をCPU側のメモリに逃がすか」を手で指定する仕組みがあります。今回はそこに踏み込みます。

そして、確かめたい問いがもうひとつ。前回まで見てきたとおり、手元には性質の違う3つの構成があります。統合メモリのApple M4、同じく統合メモリのRadeon 780M、そして専用VRAMを持つRTX 3060。「メモリの持ち方」が違うと、この指定の効き方も変わるのでしょうか。

🧠 この記事でわかること
llama.cppの3つの指定手段-ngl / -ot / --n-cpu-moe)が実際に何をしているのか
・★統合メモリなら逃がしても損は小さい、という予想が外れた
・★M4では専門家をCPUへ回した瞬間、全部CPUに置くより10倍遅くなった
その原因を突き止めるまで(外れた仮説も含めて)
・★真犯人はmmap。--no-mmap で6.6倍まで回復した——警告は最初から出ていました
32GBのMacは30B級のMoEをQ6でも丸ごと抱えられる

「自分で決める」領域が残っていた

スポンサーリンク

大きなモデルを動かすとき、GPUのメモリに全部は載りません。そこで llama.cpp は載る分だけGPUに置き、残りをCPU側に回すという折衷をします。既定ではこれを自動でやってくれます。

ただ、この自動配分は「レイヤー(層)単位」で行われます。バイト単位ではありません。そして1トークン生成するたびに全部の層を通るので、CPU側に置かれた層があるとそこで足を引っ張られます。

「もっと賢い置き方があるのでは?」——そう思うのが自然です。実際、llama.cpp にはもっと細かく指定する手段が用意されています。今回はそれを実測で確かめました😊

用語集

用語意味
オフロード本来GPUで動かしたいものをCPU側へ「逃がす」こと。GPUのメモリに収まらないときの逃げ道です
統合メモリCPUとGPUが同じメモリを分け合う作り。Apple Siliconや内蔵GPUがこの形で、専用VRAMを持ちません
専用VRAMグラフィックボードが自前で持っているメモリ。メインメモリとは物理的に別物で、間をPCIeという通り道でつなぎます
tg(生成)返事を1文字ずつ書く速度(tok/s)。体感速度に直結。メモリ帯域が主役の処理
pp(読解)読ませた文章をまとめて処理する速度。演算力が主役
MoEMixture of Experts。たくさんの「専門家」を用意しておいて、1回の処理では一部だけを使う作り。容量は大きいのに動く量は少なくて済みます
専門家(expert)MoEの中の部品。今回のモデルは全体で30B規模ですが、1トークンあたり動くのは3B相当だけです
テンソルモデルを構成する数値のかたまり。ひとつひとつに名前が付いていて、名前で指定して行き先を変えられます
量子化(Q4 / Q6)モデルの数値を粗くして容量を減らすこと。数字が小さいほど軽く、粗くなります
mmapファイルを「読み込む」のではなく「メモリ上にあることにする」仕組み。実際に触った部分だけが読まれるので、起動が速くなります
スワップメモリが足りなくなったとき、中身をディスクへ一時退避すること。退避したものを使うたびに読み直すので、極端に遅くなります

指定の手段は3つある

llama.cpp で配置を指定する方法は、大きく3つです(バージョン b10090 で確認)。

オプション何をするか
-ngl NGPUに置く層の数を指定。既定は auto で、空きを見て自動で決めます
-ot
--override-tensor
テンソルの名前を正規表現で指定して、行き先を個別に変える。いちばん細かく指定できます
--n-cpu-moe N
-ncmoe
先頭N層ぶんの「専門家」だけCPUへ。MoE向けの近道です

ここで大事なのが、-ngl と後ろの2つでは「切り方」が違うことです。

切り方が2種類ある -ngl N = 層で横に切る 第48層 → GPU 第47層 → GPU 第2層 → CPU 第1層 → CPU 層はまるごと片方に置かれる 1トークンで全層を通るので、CPU側の層が足を引っ張る -ot / --n-cpu-moe = 中身で縦に切る 注意機構 → GPU 専門家の重み → CPU 同じ層の中でも、テンソル単位で行き先を分ける MoEはこの切り方と相性がいい……はず

MoEなら効くはず、という理屈

なぜ「中身で縦に切る」のがMoEと相性がいいのか。ここがこの記事の出発点なので、丁寧に書きます。

MoEというのは、たくさんの「専門家」を用意しておいて、1回の処理ではそのうち一部だけを使う作りです。今回使ったモデル(Qwen3-30B-A3B)は全体で30B規模ですが、1トークンあたり実際に動くのは3B相当だけです。

ここに面白いズレがあります。

MoEは「容量の大部分」と「毎回動く部分」がズレている 容量(ファイルサイズ)の内訳 注意機構など 専門家(expert)の重み = 容量のほとんど 1トークン生成するときに実際に読む量 毎回ぜんぶ 一部だけ このトークンでは使われない専門家 だから理屈のうえでは: 小さくて毎回使う部分 → 速いメモリ(GPU)へ / 大きくてたまにしか使わない部分 → CPU側へ ★ただしこれは「VRAMとRAMが別物」である前提の話。同じ器なら意味を持たないはず——それを今回確かめる

容量の大部分を占めるのは専門家なのに、1トークンで動くのはそのごく一部。逆に「毎回必ず使う部分(注意機構など)」は小さい。

ならば 「小さくて毎回使う部分をGPUに、大きくてたまにしか使わない部分をCPUに」 が最適な配置になるはずです。これが --n-cpu-moe の狙いです。

「逃がす先」が構成によって違う

ただし、いま書いた理屈には隠れた前提があります。「VRAMとRAMが別物である」という前提です。

「GPUに載らないぶん」は、どこへ逃げるのか 統合メモリ(M4 / 780M) 1つのメモリを分け合う CPU の取り分 GPU の取り分 境目は動かせるだけ 物理的には同じDRAM・同じ帯域 逃がしても「引っ越し」は起きない……はず 専用VRAM(RTX 3060) メインRAM 32GB DDR5 VRAM 12GB GDDR6 PCIe(細い橋) 別々のメモリ・別々の帯域 逃がした部分は橋を渡って通う =ここが遅さの原因になる 同じ「CPUへ逃がす」でも、意味がまったく違う(はず) 左は取り分を変えるだけ。右は物理的に別の場所へ引っ越して、毎回そこへ通うことになる
📌 検証前に立てた予想(=この記事の軸)
M4と780M(統合メモリ)はほぼ同じ挙動になる。逃がす先が同じ器だから
RTX 3060だけ違う結果になる。VRAMとRAMが物理的に別だから
これはあくまで予想です。前々回も予想が外れて、それが記事のいちばん面白い部分になりました。今回も外れたらそのまま書きます。

検証の設計と、始める前に崩れた前提

測定条件はp960p962と揃えました。プロンプト512トークン/生成128トークン/3回平均です。モデルは Qwen3-30B-A3B-Instruct-2507(48層)。

モデルのサイズ選びには理屈があります。3つの構成すべてで「分割が起きる」サイズを選ばないと、「設定が効かなかった」のか「そもそも分割が起きていない」のか区別できません。そこでQ6_K(23.36GiB)を選びました——M4の上限が約24GBだと想定していたためです。

⚠ ところが、始める前にこの前提が崩れました
llama.cpp が報告するM4のGPUメモリ上限は 25,559 MiB(約24.96 GiB)。想定の24GBより多く、Q6_K は丸ごと載ってしまいます
実際 -v で確認すると offloaded 49/49 layers to GPU32GBのMacは30B級のMoEをQ6でも抱えられるわけです(12GBのRTX 3060・16.28GBの780Mでは載りません)。
そこでMac側は「載らないから分割される」ではなく、わざと分割させて、その代償を測る方針に切り替えました。

Mac(M4)の結果

まず数字を出します。tg(生成速度)が体感に効く数字です。

設定pp512tg128意味
自動フィット375.4438.62おまかせ
-ncmoe 0392.1237.77全部GPU
-ncmoe 8205.1333.31 
-ncmoe 16136.2931.96 
-ncmoe 2497.3324.89半分
-ncmoe 3240.2912.34 
-ncmoe 4017.516.19 
-ncmoe 4813.862.53専門家を全部CPU
-ngl 038.0825.76全部CPU(比較の基準)

Qwen3-30B-A3B-Instruct-2507 Q4_K_M(17.28 GiB)/Apple M4・llama.cpp b10090(Metal)

★ここが今回いちばん驚いたところ
混ぜると、どちらの極端よりも遅くなります。
全部GPU=37.77、全部CPU=25.76。ところが「専門家だけCPU」は2.53——全部CPUに置くより10倍遅いのです。
「一部をGPUに残しているのに、全部CPUより遅い」。これは直感に反します😳

グラフにすると、落ち方がよく分かります。

専門家をCPUへ逃がすほど、生成速度はどうなるか 横軸=CPUへ逃がした層の数(48でぜんぶ)/縦軸=tg 生成速度 tok/s 0 10 20 30 40 0 8 16 24 32 40 48 2.5 Apple M4 / MoE Q4_K_M 0.4 Apple M4 / MoE Q6_K Q4_K_M を全部CPU=25.8 Q6_K を全部CPU=21.7 計測: prompt 512 / 生成 128 / 3回平均(llama.cpp b10090・Metal)

もうひとつ、モデルが大きいほど落ち方が急になります。同じ設定でも、Q6_K(23.36GiB)では比べものにならない落ち方をしました。

設定Q4_K_M(17.28GiB)Q6_K(23.36GiB)
全部GPU37.7731.41
-ncmoe 833.3120.47
-ncmoe 2424.891.67
-ncmoe 482.530.35
全部CPU25.7621.69

単位はいずれも tg(生成)tok/s。0.35 tok/s は1トークン出すのに約3秒で、この1条件の測定に約18分かかりました

★この表のいちばん下の行が、話を決めました
23.36GiBのQ6_Kを全部CPUで動かすと 21.69 tok/s。ちゃんと使える速度です。
つまり「32GBのMacにこのモデルは大きすぎる」わけではありません混ぜたときだけ壊れる——問題はサイズではなく置き方だ、ということです。

ついでに分かったこと

  • --n-cpu-moe 48-ot の正規表現指定は同じ結果(2.53 と 2.54)。つまり --n-cpu-moe は下の書き方を省略できるようにしたもの、という関係が実測で確かめられました
    -ot '\.ffn_(up|down|gate)_exps\.=CPU'
  • Denseなモデルでは --n-cpu-moe は黙って無視される。Qwen2.5-Coder の 7B に -ncmoe 8(17.39 → 18.48)、14B に -ncmoe 24(10.65 → 10.97)を付けても、いずれも誤差の範囲です。エラーも警告も出ませんので、効いているつもりで効いていない事故が起こり得ます
  • 発熱のせいではありません。1時間ぶん回した後に最初の条件を測り直したところ 37.77 → 39.78 と、むしろ僅かに速くなりました

外れた仮説:コピーのせいではなかった

さて、なぜこんなことになるのか。最初に立てた仮説はこうでした。

llama.cpp には --op-offload という既定ONの設定があって、「重みはCPU側にあるが、計算はGPUに投げる」という動きをする。だとすると毎トークン、専門家の重みをGPUへコピーしているのではないか。専門家は約16GBあるので、毎回運んでいれば桁は合う。

筋は通っています。というわけで -nopo 1 で切ってみました。

設定op-offloadtg128
-ncmoe 48ON(既定)2.46
-ncmoe 48OFF2.15
-ncmoe 24ON(既定)24.89
-ncmoe 24OFF20.73
-ngl 0ON(既定)25.76
-ngl 0OFF29.82

外れました。 切っても改善しないどころか、混在配置ではむしろ僅かに遅くなっています。--op-offload は犯人ではなく、むしろ助けていたわけです。

(余談ですが、全部CPU(-ngl 0)のときだけは切ったほうが速いのが面白いところです。25.76 → 29.82。GPUに投げる判断が空振りしていた分が消えた、と読めます。CPUだけで動かすつもりなら --no-op-offload を付けると1割ちょっと得をする、という実用的な話でもあります)

原因:片方に寄せたつもりで、両方に居座っていた

仮説が外れたので、ロード時のログを見ることにしました。-v を付けると、どのバッファに何バイト確保したかが出ます。

全部GPUに載せた場合:

load_tensors: offloaded 49/49 layers to GPU
load_tensors:   CPU_Mapped model buffer size =   166.92 MiB
load_tensors:  MTL0_Mapped model buffer size = 17691.34 MiB
                                        合計 ≒ 17.4 GiB

専門家をCPUへ逃がした場合(-ncmoe 48):

load_tensors: offloaded 49/49 layers to GPU
load_tensors:   CPU_Mapped  model buffer size =   166.92 MiB
load_tensors:   CPU_REPACK  model buffer size = 16740.00 MiB  ← CPU向けに並べ替えた実体
load_tensors:  MTL0_Mapped  model buffer size = 17583.34 MiB  ← Metal側は依然ほぼ全体を保持
                                        合計 ≒ 34.3 GiB       ← 物理32GBを超えている

これが答えでした。

★CPUへ逃がすと、モデルを二重に持つことになる
① CPU側はCPU向けに並べ替えた実体を新たに確保する(16.7GB)
② ところがMetal側は元のマップをほぼ全量そのまま保持している(17.6GB)
③ 合計34.3GBで物理32GBを突き破る。しかもMetal側は固定されていて追い出せないので、追い出せる側=CPU側の実体がディスクへ退避される
④ 退避したものを毎トークン読み直すので、極端に遅くなる

実際、測定中のシステムの状態を記録すると、はっきり出ていました。

状況固定されたメモリスワップ(ディスクへの退避)
何もしていないとき2.20 GB4GB確保 / 2.45GB使用
Q4_K_M -ncmoe 2421.07 GB4GB確保 / 2.63GB使用
Q6_K -ncmoe 2425.85 GB12GBまで拡張 / 11.27GB使用
Q6_K -ncmoe 4825.49 GB23.5GBまで拡張 / 22.04GB使用

22GBもディスクへ退避していたのです。これでは速いはずがありません。「モデルが大きいほど落ち方が急」だったのも、器(32GB)を突き破るのが早いからと考えれば筋が通ります。

なぜ「全部CPU」なら平気なのか

ここで疑問が湧きます。全部CPUに置いたときも、CPU向けの実体は作られるはずです。なのになぜそちらは平気なのでしょう。

プロセスが実際に使ったメモリを測ると、答えが出ました。「固定されていて追い出せないメモリ」「退避できるメモリ」を分けて見るのがコツです。

条件退避できるメモリ固定されたメモリ合計スワップ
全部GPU0.16 GB14.80 GB約15.0 GB2.7 GB
全部CPU17.18 GB2.42 GB約19.6 GB2.7 GB
-ncmoe 248.33 GB19.27 GB約27.6 GB2.7 GB
-ncmoe 4816.49 GB19.17 GB約35.7 GB10.3 GB

MoE Q4_K_M(17.28GiB)/生成8トークンのみの短時間実行で計測

全部CPUのときは、固定されたメモリが2.42GBしかありません。 Metalを使っていないので、固定するものが無いのです。だからOSは残りを自由にやりくりでき、19.6GBに収まります。

ところが混在させると、Metal側が19GBを固定したままになります。「専門家はCPUに置いた」はずなのに、です。固定された19GBは動かせないので、しわ寄せは退避できる側=CPUの実体に全部いきます。合計35.7GBで32GBを超え、10GBがディスクへ落ちる——これが正体でした。

📌 統合メモリなら「同じ器の中で取り分を変えるだけ」のはずでした
図Aで描いたとおり、統合メモリはCPUとGPUが同じメモリを分け合います。だから逃がしても引っ越しは起きないはずだった。
ところが実装のうえでは、片方に寄せたつもりで両方に居座っていた「同じメモリを使っている」ことと「llama.cppがそう扱ってくれる」ことは別だったわけです。

Windows(780M / RTX 3060)の結果

ここまではMacの話でした。同じことを、同じマシンの中で内蔵GPUと外付けグラボを切り替えながら測ってもらいました。Windows側は llama.cpp の Vulkan 版を使います。

まず結果を出します。

設定RTX 3060
(専用VRAM 12GB)
Radeon 780M
(統合メモリ 16.28GB)
自動フィット34.4317.16
-ncmoe 16起動失敗14.81
-ncmoe 2430.1013.13
-ncmoe 3225.7212.48
-ncmoe 4021.80
-ncmoe 4818.9311.97
48→24 の変化+59%+10%

MoE Q4_K_M(17.28GiB)/単位は tg(生成)tok/s

予想どおりでした。 専用VRAMのRTX 3060では設定を変えると大きく動き(+59%)、統合メモリの780Mではほとんど動きません(+10%)。Q6_Kに至っては780Mは+6%でほぼ横ばいでした。

理屈も素直です。780Mでは「GPUに置く」も「CPUに逃がす」も行き先が同じDDR5メモリなので、そもそも移動していないに等しい。だから数字が動かない。一方RTX 3060では、GPU側はGDDR6、CPU側はPCIe越しのDDR5と物理的に別の器なので、どちらに置くかが直接速度に出ます。

ところが、2台の統合メモリ機は正反対だった

ここで、3つを並べてみます。

構成メモリの持ち方自動フィット-ncmoe 48変化
Apple M4統合メモリ38.622.53−93%
Radeon 780M統合メモリ17.1611.97−30%
RTX 3060専用VRAM34.4318.93−45%
★予想は半分当たって、半分外れました
「780MとRTX 3060は違う挙動になる」——これは当たり
「M4と780Mは統合メモリどうしだから似た挙動になる」——これは大外れいちばん違ったのがM4でした。
同じ「統合メモリ」なのに、片方は設定がほとんど効かず、もう片方は設定で壊れる。これでは説明がつきません😳

説明のカギは、GPUメモリの使用量にありました。-ncmoe 48(専門家を全部CPUへ)を指定したとき、それぞれのGPUが実際に確保した量です。

構成GPUの窓口GPUが確保した量
Radeon 780MVulkan1,166 MB
RTX 3060Vulkan1,137 MB
Apple M4Metal17,583 MiB

Windowsの2台は、逃がした分だけきちんと減っています。 専門家をCPUに置いたのだから、GPU側に残るのは1.1GBほど——当たり前の挙動です。統合メモリの780Mも、専用VRAMのRTX 3060も、そこは同じでした。

Macだけが、17.6GBを抱えたままでした。

つまり、Macで起きていたことは「統合メモリだから」ではありませんでした。同じ統合メモリの780Mは普通に振る舞っています。MacのGPUを動かす窓口(Metal)の側の事情だったわけです。

真犯人はmmapだった(そして警告は最初から出ていた)

Windows側の報告に、見逃せない一文がありました。--n-cpu-moe を使うと llama.cpp がこんな警告を出す、というのです。

llama_model_loader: tensor overrides to CPU are used with mmap enabled
                    - consider using --no-mmap for better performance

「テンソルの行き先をCPUに変える指定をmmap有効のまま使っています。性能のために --no-mmap を検討してください」。

Macのログを見返しました。出ていました。 最初から、毎回。読み飛ばしていたのはこちらでした😅

そこで --no-mmap を付けて測り直したところ、バッファの内訳がこうなりました。

バッファ既定(mmapあり)--no-mmap
CPU(並べ替え済みの実体)16,740 MiB16,740 MiB
Metal側17,583 MiB784 MiB
合計約34.3 GiB約17.3 GiB

Metal側が17.6GBから784MBに落ちました。 Windows(1,137MB / 1,166MB)とほぼ同じ水準です。二重確保が消え、実行中の固定メモリも25GB前後から3.29GBに、スワップも通常の水準に戻りました。

★何が起きていたのか
mmapは「ファイルをメモリ上にあることにする」仕組みです。llama.cppはこれを使ってモデルを素早く読み込みます。
ところがMetalバックエンドは、そのマップ全体をひとかたまりのGPUバッファとして掴みます中の一部をCPUに移したところで、掴んだ範囲は変わりません
だから「専門家はCPUに置いた」つもりでも、Metal側はモデル全体を抱えたまま。そこにCPU側の実体が加わって、二重になっていた——というわけです。

速度も、当然のように戻りました。

設定既定(mmapあり)--no-mmap改善
自動フィット38.6240.29+4%
-ncmoe 2424.8929.50+19%
-ncmoe 482.5316.696.6倍

ただし、直しても2つの事実が残りました

ここで終われば「めでたし」なのですが、直した後の数字をよく見ると、まだ腑に落ちない点が2つあります。

ひとつめ。--no-mmap で直したあとのM4は、780MではなくRTX 3060寄りの振る舞いをします。

構成メモリの持ち方-ncmoe 48→24 の変化
Apple M4--no-mmap統合メモリ+77%
RTX 3060専用VRAM+59%
Radeon 780M統合メモリ+10%
設定を動かしたとき、どれだけ速度が変わるか 右へ行くほど専門家をGPUに戻す/縦軸=tg 生成速度 tok/s(MoE Q4_K_M) 0 10 20 30 40 -ncmoe 48 -ncmoe 40 -ncmoe 32 -ncmoe 24 -ncmoe 16 Apple M4(統合メモリ・既定) Apple M4(--no-mmap) RTX 3060(専用VRAM) Radeon 780M(統合メモリ) 2.5 ← ここだけ崩壊 Mac=llama.cpp b10090(Metal) / Windows=LM Studio 2.25.2(Vulkan)。版もバックエンドも異なる点に注意

この図が、今回の検証のすべてです。紫(780M)はほぼ真横——設定を動かしても変わりません。青(RTX 3060)は素直に右肩上がり。そして緑の破線(--no-mmap にしたM4)は、青にぴったり重なります。赤(既定のM4)だけが左端で崩れ落ちている、という構図です。

mmapの問題を取り除いてもなお、統合メモリどうしのM4と780Mは似ませんでした。 似ているのは、むしろM4とRTX 3060のほうです。

そう考えると、効き方を決めているのは「統合メモリか専用VRAMか」ではなく、GPU側とCPU側の実力差がどれだけ開いているかだと読めます。780Mは内蔵GPUが非力でCPUとの差が小さいので、どちらに置いてもさほど変わらない。M4はGPUが強いぶん、CPUへ逃がすと素直に損をする。RTX 3060はVRAMとPCIe越しのRAMという差がある——理由は違っても、「差が大きければ設定が効く」という一点で説明がつきます

ふたつめ。直した後でも、混ぜると「全部CPU」より遅いままです。

Mac(M4)の置き方tg
全部GPU(--no-mmap40.29
全部CPU(--no-op-offload付き)29.82
全部CPU25.76
混在(-ncmoe 48 --no-mmap16.69

二重確保が消えても、混在はどちらの極端より遅いままでした。--no-op-offload を足しても17.23で、ほとんど変わりません。

残っているのはおそらく1層ごとにGPU側とCPU側を行き来する、その切り替えそのもののコストです。48層を毎トークン通るので、回数がそのまま効いてきます。ただしこれは数字から逆算した解釈で、切り替えの回数や時間を直接測ったわけではありません——そこは正直に書いておきます。

実務の使い分け

3構成を測って分かったことを、使い方の形にまとめます。

やりたいこと選ぶべき設定
とにかく速く-ngl を省略して自動フィットに任せる。3構成すべてで自動が最速でした
VRAMを他の用途に空けたい--n-cpu-moe を上げる(RTX 3060なら48で1.1GBまで削減。速度は約半分)
起動を速くしたい--n-cpu-moe を上げる(780Mで78.8秒 → 6.4秒。GPUへ送る量が減るため)
限界まで詰めたい-ot で個別指定。ただしKVキャッシュのぶんを残すこと
Macで逃がすなら必ず --no-mmap を付ける
統合メモリ機(780M)でチューニングの効果は小さい。自動フィットで十分

「載る」と「使える」は別物

もうひとつ、Windows側で出た大事な教訓です。

  • 780Mでは -ncmoe 48 で、前回載らなかったQ4_K_Mが載りました。「設定次第で載せられる」は事実です。ただし載っても速くはなりません(11.97 < 自動フィットの17.16)。載せること自体が目的になると、むしろ遅くなります
  • RTX 3060では逆に、詰めすぎると落ちました。 -ncmoe 16 は重みこそVRAMに収まるものの、KVキャッシュを置く余地が無くなって起動に失敗します。チューニングの上限は「重みが入る限界」ではなく「KVキャッシュぶんを残した位置」です

条件が完全には揃っていないこと

正直に書いておきます。Mac側とWindows側では、llama.cppのバージョンもバックエンドも違います(Mac=Homebrew b10090 / Metal、Windows=LM Studio同梱 2.25.2 / Vulkan)。ですので「Metalだからこうなる」と言い切るには、本当は同じ版で揃えたいところです。

ただ、Mac側で --no-mmap を付けるだけで挙動がWindows側と揃ったことから、mmapとテンソル個別指定の組み合わせが引き金である点は確かだと考えています。

まとめ

  • 割り当ては自分で決められます。 -ngl(層で切る)/-ot(テンソル名で切る)/--n-cpu-moe(MoEの専門家を切る)の3つ
  • --n-cpu-moe N-ot の便利版。実測でも、専用VRAM使用量がMB単位まで一致しました。Denseなモデルでは警告もなく無視されます
  • Windows側では予想どおりでした。 専用VRAMのRTX 3060は設定で+59%動く一方、統合メモリの780Mは+10%。「行き先が同じ器」なら、そもそも移動していないのと同じです
  • ところが統合メモリどうしのM4と780Mは、正反対でした。 780Mは効かないだけ、M4は全部CPUに置くより10倍遅くなる。予想は半分当たって半分外れです
  • M4が壊れた原因はメモリ構造ではなく、mmapとの組み合わせ。 Metalはmmapした範囲をまるごと掴むので、中身をCPUに移してもGPU側が抱えたまま二重になり、スワップが22GBまで膨らんでいました。--no-mmap で6.6倍まで回復します
  • そして直した後も、M4は780Mに似ませんでした。 似ていたのはRTX 3060のほう。効き方を決めるのは「統合メモリか専用VRAMか」ではなく「GPU側とCPU側の実力差がどれだけ開いているか」だと考えると、3台とも説明がつきます
  • そして、その警告はllama.cppが最初から出していました。 読み飛ばしていたのはこちらです

「統合メモリか専用VRAMか」という構造の違いから素直に予想を立てて始めた検証でしたが、実際に効いていたのは実装の都合と、単純な実力差でした。構造の名前が同じでも中身は同じではない、というのは前回の「同じ統合メモリでも速度は同じにならない」とも重なる話で、2回続けて同じ落とし穴に足を取られたことになります😅

それと、今回いちばん反省したのは警告を読み飛ばしていたことです。原因を探して丸一日ぶん回した末に、答えはログの最初のほうに書いてありました。

それでは、今回はここまで。最後までありがとうございました😊

【実測環境】Mac: Apple M4 / macOS 26.5.2 / RAM 32GB(LPDDR5X-7500)/llama.cpp b10090(7347430f4・Metal)。GPUメモリ上限は recommendedMaxWorkingSetSize = 26800.60 MB(llama.cppの報告値 25,559 MiB)、iogpu.wired_limit_mb は既定の0。Windows: Windows 11 Pro 25H2 (26200.8875) / Ryzen 7 7840HS 内蔵 Radeon 780M + RTX 3060(OCuLink接続のeGPU)/ RAM 31.3GB(DDR5)/llama.cpp は LM Studio 同梱の署名済み Vulkan ランタイム 2.25.2(cb295bf)。モデルは Qwen3-30B-A3B-Instruct-2507(48層・MoE)の Q4_K_M(17.28 GiB)と Q6_K(23.36 GiB)、対照群に Qwen2.5-Coder 7B/14B Q4_K_M。計測は prompt 512トークン・生成128トークン・3回平均(ウォームアップは各ツールの内蔵機能を使用)。Mac側とWindows側でllama.cppの版もバックエンドも異なるため、構成間の絶対値比較ではなく「設定を変えたときの動き方」の比較として読んでください。数値は2026年7月時点の実測で、版により変わる可能性があります。