118BのLLMモデル Laguna-S-2.1をおうちで動かしてみよう!
この記事は人力で実験した上で、AIを使って書かれています
※ローカルLLM運用 llama.cppの超マニアックな記事です。
VRAM 56GB に、57.6GB のモデルを載せて128k context, 39.9 t/s
7/21に出たばかりのLaguna S 2.1(118B-A8B の MoE)を、非対称な2枚構成(V100 32GB + RTX 3090 24GB)で動かした話。モデルは4bit量子化でも57.6GBあって、VRAM合計を素で超える。普通なら「載らないから量子化をもっと落とす」か「RAMにはみ出して遅いまま諦める」の二択に見えるが、MoE では両方とも不正解だ。載り切らないまま実用速度で回す方法がある。
なぜそれが成立するのか、どう配線すれば最速になるのか。
先に断っておく。この手の質問をLLMに投げると、たいてい間違った答えが返ってくる。「57.6GBのモデルを56GBのVRAMで動かしたい」と聞けば、まず量子化をもっと落とせ、コンテキストを削れと返ってくる。次に-nglを下げてRAMに載せろと勧められ、遅くなっても「原理上そういうもの」で片付けられる。マルチGPUの話をすれば「llama.cppのrow splitは遅い、NVLINKとvLLMじゃなきゃ性能は出ない」と、確かめもしない古い定説を差し出してくる。どれもこの構成では不正解だ。初心者ほどこの誘導に素直に従い、動かないか、遅いまま諦める。
この記事はそのおかしな考察を一つずつ潰す。公式READMEに載っていない、実測から出た事実と、陥りやすい誤解の訂正を中心にまとめる。最初に結論、あとから根拠の順で書く。
先に結論
MoEは載り切らなくてよい。 routed expert の一部をホストRAMに逃がす「部分オフロード」が妥協ではなく正しい設計。VRAMに全載せするために量子化を無駄に落とすのは筋が悪い。
--n-cpu-moe は複数GPU構成だとトラップ。 先頭N層を逃がすショートカットなので、layer split している2枚では片方しか軽くならない。-ot(override-tensor)で両カードの担当層を名指しする。
この構成では tensor split より layer split が速い。 VRAMに載りきらない場合、tensor split は逆効果。
DFlash は現状切る。 acceptance rate が低く、オーバーヘッドでむしろ遅くなる。日本語生成も不向きで二重にダメ。
thinking を習慣でONにしない。 公式も現状は初期リリースでThinkingがものすごく長くなる傾向と明言している。Github copilotなど、ハーネス側で長考を抑えるのが良い。
以下、環境と根拠。
実験環境(この知見の前提)
数値・構成はすべて以下のハードでの実測。
GPU 0:Tesla V100-SXM2 32GB、PCIe gen3 x4、power cap 200W
GPU 1:RTX 3090 24GB、PCIe gen4 x16、power cap 200W
合計VRAM:56GB(32 + 24、非対称)
システムRAM:96GB DDR4(expertオフロード先)
多GPU方式:layer split(既定)
推論:llama.cpp(main)、GGUF は unsloth UD-IQ4_XS
肝は 2枚が非対称(容量 32≠24、かつ V100 側の PCIe が gen3 x4 と細い)という点。同一カード2枚 or 単一GPUなら、以下の結論の一部(特に tensor split の項)は変わる。
普通のマザーボードでGPUを2個付ける方法(脱線)
サーバー用など、特殊なマザーボードを除き、普通のマザーボードにはCPU直結PCIE x16のスロットが一つしかない。二つ目以降のPCIEスロットが付いているかもしれないが、大抵それらは形状は大きくても電気的にはPCIE x1 だったりする(接点をよーく見てみよう)。llama.cppでは推論自体には問題ないことが多いが、モデルのロードがどうしても遅くなってしまう。
今回使用したB550 GAMING X V2 (最安のマザーボード)も当然この制約がある。こういうときは、CPUに近い側のm.2 コネクタをPCIEに変換してGPUを付けてしまおう。起動用のSSDはもう一つのm.2 コネクタに挿せばよい。変換はAmazon等で一式売られている。こうすれば、CPU直結のx16とx4という、普通のマザーボードで引っ張りだせる最大限のPCIEレーンをGPUに割り当てられる。
対象モデルの構造
総 / active パラメータ:118B / 約8B(per token) MoE
レイヤー:48(global 12 + SWA 36、window 512、比 1:3)
expert:256 routed + 1 shared、top-10 routing
attention:GQA、8 KV heads、head dim 128
量子化(検証対象):UD-IQ4_XS、 約57.6GB
まず前提として、57.6GB は VRAM 56GB に載らない。KVキャッシュも載せなくちゃいけないから、もう絶対にVRAMに載らない。これがこの記事全体の出発点で、だから部分オフロードが要る。
構造上の重要点は SWA(sliding window attention)が48層中36層に効くこと。128k context の KV キャッシュが q8 で 約3.2GB に収まる。フルKV(48層全部が global)なら約13GBのところ、36層が window 512 に抑えられて 1/4 になる。KVが小さく済むおかげで長コンテキスト運用は現実的だが、そもそも重みだけでVRAMを超えているので、載せ方の設計がすべてになる。
1. MoEオフロードに関する誤解の訂正
誤解A:「MoEモデルはVRAMに全部載せないと動かない」→ 誤り
MoEモデルの重みの大半は routed expert の FFN にある。per-token で実際に読むのは active な一部(top-10 expert)だけ。だから expert FFN をホストRAMに置き、必要な分だけ読む「部分オフロード」が正しい設計であって、妥協ではない。
そもそも 57.6GB の重みは 56GB の VRAM に全載せできない。
「VRAMに収まる量子化まで落とす」のは、MoEでは間違い。
正しくは 非expert(attention / shared / embed)を優先的にGPU常駐させ、routed expert の一部を RAM にオフロードする
この前提を取り違えると「載らない=無理」と誤診して、無駄に量子化を落としたり構成を諦めたりする。MoEは載り切らなくてよい。
(もちろん)VRAMがたくさんあって全部載るなら、それが最速なのはその通り。性能を見ながら量子化を決めよう。
補足:部分オフロードとは dense モデルは全パラメータを毎トークン計算するので、RAMに逃がした層はPCIe越しの読み出しがそのまま速度に直撃する。それに対しMoE は per-token で読むのが active expert だけなので、めったに使われない expert をRAMに置いても平均的なペナルティが小さい。「大きいMoEが小さいVRAMで回る」のはこの非対称性による。
誤解B:--n-cpu-moe を使えばVRAM載りきらないMoEが動く → 複数GPUだとトラップ
--n-cpu-moe N は 先頭N層 の expert を CPU に逃がすショートカット。これが非対称構成でトラップになる。
layer split は、モデルの層を前半・後半で2枚に割り振る。この構成だと大まかに、先頭〜中盤の層が V100(32GB)に、後半の層が RTX 3090(24GB)に載る。ここで問題になるのが、--n-cpu-moe が逃がすのは常に先頭からだという点。
仮に 3090 側(後半層を担当)がVRAM逼迫で 0.5GB 足りないとする。ここで --n-cpu-moe 4 に設定しても、CPUに逃げるのは先頭4層=全部 V100 側の expert。意味ない。
V100:31.3 → 27.3GB(4GB空いた。もともと逼迫してないのに)
3090:23.7 → 23.7GB(全然減ってない)
V100にいくら余白を作っても、詰まっている3090は最後まで救われない。数字を増やして --n-cpu-moe 25 とかにすれば、いつか後半層まで到達して3090も軽くなるが、それはつまりV100のVRAMを全く使っていないってことで、速度がガタ落ちする。「逼迫している側だけを狙って剝がす」ができないのが --n-cpu-moe の限界。
対処は -ot(--override-tensor)の正規表現で、両カードの担当層を名指しすること。同じようなことをしているがはるかに自由が効く。
-ot "blk\.(0?[0-2]|4[6-7])\.ffn_.*_exps\.=CPU"
これは先頭3層(0〜2、V100側)+ 末尾2層(46〜47、3090側)の expert を CPU へ逃がす例。片方に偏らせず、逼迫している側の層を名指しで剝がせるのがショートカットとの決定的な差。--n-cpu-moe が素直に効くのは単一GPUの場合だけで、複数構成では -ot の層番号指定が必須になる。
補足:-ot の読み方 blk\.(...)\.ffn_.*_exps\.=CPU は「該当ブロック番号の FFN expert テンソルだけCPUに置く」という指定。層番号を正規表現で列挙するので、layer split で「どの層がどちらのカードに載っているか」を把握した上で、逼迫している側の層を狙って剥がす。ここが --n-cpu-moe(先頭から機械的に)との決定的な差。
2. tensor split と layer split の使い分け
tensor parallel(-sm tensor)は一般には有効な多GPU手法で、実際この同じ2枚・同じPCIe構成でも、VRAMに収まるモデル(Qwen3.6-27B や Gemma4-12B)なら layer split 比で2倍近い速度が素直に出る。帯域のほぼ揃った2枚なので、並列がきれいに効く。
問題は VRAMにモデルが載りきらない時だ。実測では、layer split(デフォルト)が offload 5層で 39.9 t/s、対して tensor split(-sm tensor)は offload 13層まで増えて 21.9 t/s と、ほぼ半減した。
なぜ tensor split 側だけ offload が倍以上に膨らむのか。tensor parallel は層を2枚に縦割りして同時に走らせる方式なので、両カードのVRAMが揃っている前提で組まれている。片方が小さいと、大きい側もそれに合わせて使える枠が頭打ちになる——遅い方・小さい方が全体を律速する。この構成だと 3090 が 24GB なので、V100 は 32GB あっても 24GB 分しか使えず、8GB を遊ばせておくことに。使えるVRAM総量が実質 56GB → 48GB に縮む。
当然、その8GBに載っていたはずの expert は行き場を失ってCPUに行くしかない。これが offload 5層 → 13層の正体だ。tensor parallelの並列利得を取りにいったら、VRAMを揃えるために容量を捨てさせられ、捨てた分のoffloadペナルティで並列利得を上回る損が出る。 本末転倒になる。
つまり分岐はこう:
モデルが小さくVRAMに収まる(27B/12Bなど)→ tensor parallel が速い(2倍近く)。素直に使え
モデルが載りきらずoffloadが要る(今回の57.6GB)→ layer split。tensor parallelはVRAMを揃える都合で容量を捨て、その分offloadが増えてかえって遅くなる
tensor parallel の優位は「全部載る」が前提の数字だ。非対称な2枚で容量を目一杯使い切りたいケースにそれを持ち込むと、揃えるために切り捨てた容量がoffloadに化けて、この逆転を踏む。
3. offload層数の詰め方(layer split)
VRAMに載せれば載せるほど、つまりoffloadを最小化するほど速い。ただし攻めすぎると 計算のためのバッファ不足でエラー落ちする。二分探索で最小点を出す。
offload 8層 → 30.8 t/s
offload 7層 → 33.4 t/s
offload 5層 → 39.9 t/s(最速・限界点)
offload 4層 → compute buffer OOM(メモリ不足)
expert 1層あたり約1GB(IQ4_XS)。逼迫している側のカードの空きが下限を決める。非対称に振る(空きのある側は戻す、無い側だけ剥がす)ことで総offload層数を減らせる。
最終VRAM占有 — 片カード 31.3/32、もう片カード 23.7/24。合計 55.0/56 GiB、余白約1GB。ここが物理的な最速点。
4. DFlash(speculative decoding)は現状使えない
DFlash は block diffusion ドラフター + ターゲットの隠れ状態injection という特殊方式で、汎用の draft-model spec decode とは別物
acceptance rate が約8% と低く、ドラフトのオーバーヘッドで むしろ遅くなる。(対策版が出たが試してない)
laguna 固有の fork でしか動かない(llama.cppのmainでは --spec-type draft-dflash がエラー)
現状は DFlash を切って素で回すのが正解。公式のプレスは投機ありの数字(DGX Spark で decode 22-24 t/s 等)を出しているが、あれは NVFP4 + poolside 純正スタックでの話。上流llama.cpp + 汎用構成では前提が違う。
5. サンプリングと挙動
公式推奨サンプリング(S 2.1)
--temp 0.7 --top-p 0.95 --top-k 20
top-k 20 が eval-certified の truncation。llama.cpp は generation_config.json を読まないので明示指定が必要(デフォルト top-k 40 では公式条件と異なる)
min_p は入れない(spec decoding と併用時にエラーの報告あり、公式も非推奨)
XS 2.1(別モデル、33B)の temp 1.0 条件を S 2.1 に流用しないこと
reasoning の扱い
--reasoning-preserve はロード時に付ける(agentic でターン間の reasoning を保持する)。ただし thinking の効き方はハーネスで全然違う。ここは一般論じゃなく今日コードを書いてわかったこと。
同じ IQ4_XS を、Pi coding agent で使うと thinking が止まらない。進捗がないまま延々と長考してしまう。公式も 長考を初期版の制約として挙げているが、Piではそれが露骨に出て使い物にならなかった。ところが同じモデルを GitHub Copilot に繋ぐと、thinking の長さが適正で、普通に働く。Copilot 側はブラウザも叩くし自動化も回る、その環境でだけ素直に動いた。
モデルは同じ、量子化も同じ。違うのはハーネスだけだ。thinking の受け渡し(前ターンの reasoning_content を次に残すか削るか)の実装がハーネスごとに違うので、足回り次第で暴走もするし適正にもなる。
6. 確定した起動構成(layer split・最速)
-ngl 99 -c 131072 -ctk q8_0 -ctv q8_0 -fa on -fit off
-b 1024 -ub 512 --parallel 1 --jinja --reasoning-preserve
--temp 0.7 --top-p 0.95 --top-k 20
-ot "blk\.(0?[0-2]|4[6-7])\.ffn_.*_exps\.=CPU"
補足:
-fit off は必須。 -fit on は溢れを層まるごとRAMに逃がす(expert限定ではない)ため遅い。MoE の expert だけを狙って逃がすには手動 -ot を使う
llama-swap 等で -ot を渡す場合、値にクォートを付けない。 空白が無いので裸で1トークン。ツールによってはクォート文字が引数に混入して壊れる
まとめ
MoEは載り切らなくてよい。非expertをGPUに、expertの一部をRAMに逃がす部分オフロードが正しい設計だ。そのオフロードは --n-cpu-moe の「先頭から」任せにせず、-ot で逼迫している側の層を名指しして最小化する。
そして、LLMが自信満々に返してくる定説をそのまま踏まない。量子化を落とせ、コンテキストを削れ、tensor parallelが速い——どれも「全部VRAMに載る」前提の話で、載りきらないMoEには当てはまらない。tensor parallelは非対称な2枚だと大きい側の容量を揃えるために切り捨て、その分offloadが増えてかえって遅くなる。この構成では layer split が正解だった。
数字(56GBに57.6GB、128k context、39.9 t/s)は「モデル + その量子化 + その配線 + そのハーネス」のセット値だ。ハーネスひとつ変えるだけで、Lagunaは暴走もするし適正にもなる。別環境にそのまま持ち込める数字なんて無い。確かめもせず定説を差し出す相手(人でもLLMでも)を疑い、自分のハードで二分探索して最小点を出す。それが唯一の答え合わせになる。
