見出し画像

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でも)を疑い、自分のハードで二分探索して最小点を出す。それが唯一の答え合わせになる。

いいなと思ったら応援しよう!