0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

RTX 4070 12GBに収まらない35B級MoEをik_llama.cppとRAMで動かす

はじめに

LLMをローカルのGPUで動かすときに、VRAMの制約で上限が決まってきます。量子化をしてもRTX4070 の12GBVRAMではどうあがいても35Bモデルは乗せることができません。。。

しかし、ik_llama.cppを使うと、VRAMに収まらない35B級MoEのweightをGPUとCPUへ振り分け、RTX 4070 12GBでもロードして生成できます。

今回使うQwen3.6-35B-A3B Q4_K_Mは、GGUFだけで約20.76GiBあります。

先に算数をします。

モデル: 22,285,080,192 bytes(約20.76GiB)
VRAM:   12GB

入りません。

CUDAを信じても、算数までは曲げてくれません。

ここで使うのが、ik_llama.cppの自動配置、CPU MoE、tensor overrideです。モデル全体をVRAMへ詰め込むのではなく、一部のtensor、特にMoE expertをCPU RAMへ置きます。

つまり今回は、

12GB VRAMへ無理に全部載せる
        ↓
GPUへ置くweightとRAMへ置くweightを分ける
        ↓
35B級MoEをローカルPCで生成可能にする

という実験です。

そして「ロードできました」で終わらせず、VRAM、RAM、swap、4K・8K contextの成立境界まで確認します。

この記事で確認すること

今回の目的は、RTX 4070 12GBで35B級MoEを「無理やり全部VRAMへ載せる」ことではありません。

見たいのは次の3点です。

  1. ik_llama.cppのfit、CPU MoE、tensor overrideで、VRAMに収まらないQwen3.6-35B-A3B Q4_K_Mをロード・生成できるか。
  2. 4K contextでVRAM、RAM、swapがどこまで増えるか。
  3. 8K contextまで広げたとき、どの配置が安全基準を満たすか。

つまり、この記事は速度勝負ではなく、12GB VRAMの上限をRAM併用で越えるときの成立条件を見ます。

「12GB VRAMに35Bを載せた」というより、正確にはik_llama.cppで載らない分をRAMへ逃がし、GPUに収まらないモデルを動かせるかを検証する記事です。

ik_llama.cppとは何か

ik_llama.cppは、llama.cppを基に、追加の量子化方式や推論最適化を実験しているforkです。

今回重要なのは、名前よりも次の配置機能です。

機能 できること 今回の使い方
--fit 使用可能なVRAMに合わせて自動配置する 35B級モデルを手軽に起動
--cpu-moe MoE expertをCPU側へ置く VRAMを約3.2GiBまで削減
-ot 正規表現でtensorの配置先を指定する expert層16~39をCPUへ固定

llama.cppにもCPU/GPUハイブリッド配置機能はあります。そのため、「ik_llama.cppだけが大きなモデルを動かせる」という話ではありません。

ik_llama.cppの面白さは、MoEのexpertをどこへ置くか試しながら、VRAM容量を超えるモデルの成立条件を探れることです。

12GB GPUだから20GiB超のGGUFを諦めるのではなく、VRAMを高速な作業場所、RAMを大容量のweight置き場として使います。

今回の主役は、モデルをVRAMへ「載せる」ことではなく、計算に参加させながら、どこへ置くかを制御することです。

使用したモデル

本命はQwen3.6-35B-A3Bです。

公式モデルカードでは、総parameter数は35B、1 tokenあたりにactivateされるparameterは3Bとされています。MoEは全expertを毎回計算せず、入力に応じて一部だけを使うためです。

項目 値
Model Qwen3.6-35B-A3B
Model type MoE、35B total / 3B activated
Experts 256
Activated experts 8 routed + 1 shared
Quant Q4_K_M
GGUF Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf
GGUF size 22,285,080,192 bytes(約20.76GiB)
License Apache-2.0
今回の入力 text only

GGUFはbartowski/Qwen_Qwen3.6-35B-A3B-GGUFのQ4_K_Mを使用しました。

ここで注意したいのは、3B activatedだから3Bモデルと同じメモリ量になるわけではないことです。

計算時に呼ばれるexpertは一部でも、使われる可能性のある全weightには置き場所が必要です。

Active 3Bと聞くと軽そうですが、待機中のexpertも家賃は払います。

ik_llama.cppでweightをGPUとRAMへ分ける

今回の配置を単純化すると、次のようになります。

Qwen3.6 GGUF
  ├─ attention、shared tensor、一部expert → GPU VRAM
  └─ 一部または全部のMoE expert         → CPU RAM

GPUへ置けるtensorには12GBという上限があります。CPU RAMを併用すればweightの保持先を広げられますが、RAM使用量とswapにも注意が必要です。

今回は次の3方式を試しました。

配置 内容 狙い
fit backendによる自動配置 手軽にVRAM内へ収める
manual expert層16~39をCPUへ固定 配置を再現可能にする
CPU MoE MoE expertをCPUへ置く VRAM使用量を抑える

検証環境

項目 値
GPU NVIDIA GeForce RTX 4070 12GB
Compute capability 8.9
CPU Intel Core i7-14700F、20 cores / 28 threads
RAM / Swap 32GiB nominal(OS認識31GiB)/ 8GiB
OS Ubuntu 24.04.4 LTS
Kernel 6.17.0-29-generic
NVIDIA driver 580.126.20
CUDA toolkit 12.8 / V12.8.93
llama.cpp c34b92235b2d6a07963f896085f9ca077ff400b4
ik_llama.cpp 5f917a64b391b7d31839845153a473a65f630458

GPUはデスクトップ表示にも使っています。環境確認時点で777MiBのVRAMを使用していたため、12,282MiBすべてを推論へ使える条件ではありません。

また、更新の速いプロジェクトなので、llama.cppとik_llama.cppはcommitを固定しました。「同じコマンド名なら半年後も同じ動作」とは限らない世界です。

ビルド

llama.cppとik_llama.cppを取得し、検証時のcommitへ固定します。

サンプルコード(ビルド)
mkdir -p vendor build

git clone https://github.com/ggml-org/llama.cpp.git vendor/llama.cpp
git -C vendor/llama.cpp checkout --detach \
  c34b92235b2d6a07963f896085f9ca077ff400b4

git clone https://github.com/ikawrakow/ik_llama.cpp.git vendor/ik_llama.cpp
git -C vendor/ik_llama.cpp checkout --detach \
  5f917a64b391b7d31839845153a473a65f630458

RTX 4070のcompute capability 8.9に合わせ、CUDA architectureを89へ固定してRelease buildします。

cmake \
  -S vendor/llama.cpp \
  -B build/llama.cpp-cuda-sm89 \
  -DCMAKE_BUILD_TYPE=Release \
  -DGGML_CUDA=ON \
  -DGGML_NATIVE=ON \
  -DCMAKE_CUDA_ARCHITECTURES=89 \
  -DLLAMA_BUILD_UI=OFF \
  -DLLAMA_USE_PREBUILT_UI=OFF

cmake --build build/llama.cpp-cuda-sm89 \
  --target llama-cli llama-server llama-bench \
  --parallel 8

cmake \
  -S vendor/ik_llama.cpp \
  -B build/ik_llama.cpp-cuda-sm89 \
  -DCMAKE_BUILD_TYPE=Release \
  -DGGML_CUDA=ON \
  -DGGML_NATIVE=ON \
  -DCMAKE_CUDA_ARCHITECTURES=89

cmake --build build/ik_llama.cpp-cuda-sm89 \
  --target llama-cli llama-server llama-bench llama-sweep-bench \
  --parallel 8

llama.cppはWeb UIを使わないため、次の2項目を無効化しています。

-DLLAMA_BUILD_UI=OFF
-DLLAMA_USE_PREBUILT_UI=OFF

ビルド後は、version、CUDA Runtimeへのリンク、CMake設定を確認します。

サンプルコード(ビルド確認)
build/llama.cpp-cuda-sm89/bin/llama-cli --version
build/ik_llama.cpp-cuda-sm89/bin/llama-cli --version

ldd build/llama.cpp-cuda-sm89/bin/llama-cli | grep libcudart
ldd build/ik_llama.cpp-cuda-sm89/bin/llama-cli | grep libcudart

grep -E \
  'GGML_CUDA:BOOL=ON|GGML_NATIVE:BOOL=ON|CMAKE_CUDA_ARCHITECTURES.*=89' \
  build/llama.cpp-cuda-sm89/CMakeCache.txt

grep -E \
  'GGML_CUDA:BOOL=ON|GGML_NATIVE:BOOL=ON|CMAKE_CUDA_ARCHITECTURES.*=89' \
  build/ik_llama.cpp-cuda-sm89/CMakeCache.txt

確認できたversionは次の通りです。

llama.cpp:    version 1 (c34b922)
ik_llama.cpp: version 1 (5f917a6)

この環境では、初回クリーンビルドに約13分かかりました。

モデル取得

Q4_K_M GGUFを取得し、ファイルサイズとSHA-256を検証します。

サンプルコード(モデル取得と検証)
MODEL_FILE="Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf"

curl -L --fail --retry 5 --retry-delay 5 --continue-at - \
  --output "${MODEL_FILE}.part" \
  https://huggingface.co/bartowski/Qwen_Qwen3.6-35B-A3B-GGUF/resolve/5c2410d71524f4f72b023ce8daf7a80528226d5f/Qwen_Qwen3.6-35B-A3B-Q4_K_M.gguf

mv "${MODEL_FILE}.part" "${MODEL_FILE}"

stat -c '%s bytes' "${MODEL_FILE}"
sha256sum "${MODEL_FILE}"

期待値:

22285080192 bytes
b46fedd33e0bfb0cae308aa3c158d0a4b2c4a1d2185a1ed6f093cdaf39064772

量子化ファイルだけで約20.76GiBあるため、ダウンロード前にストレージ容量も確認したほうがよいです。VRAMの話をしていたら、先にSSDが困ることもあります。

測定条件

モデルのロードだけでなく、短い日本語応答の生成、VRAM、RAM、swapを確認しました。

text only
context: 4096
生成上限: 128 tokens
temperature: 0
single request
MTPなし
mmprojなし
--mlockなし

確認項目は次の4点です。

  1. モデルをロードできる
  2. 128 tokens以内の生成を完了できる
  3. VRAM、RAM、swapを記録できる
  4. 安全弁を作動させず終了できる

測定中は500ms間隔でVRAMとシステムメモリを記録しました。

また、PC全体を巻き込むベンチマークにしないため、次の条件で停止します。

安全弁 閾値
試行開始後のswap増加 2,048MiB超
MemAvailable 1GiB未満
stdout 64MiB超
timeout 15分

PCがベンチの下へ隠れたくなる状況を避けます。

VRAM・RAM監視と安全停止の実装

記事だけで測定方法を再現できるよう、実験で使った監視処理を1本のshell scriptにまとめると次のようになります。

サンプルコード(VRAM・RAM監視と安全停止)
#!/usr/bin/env bash
set -euo pipefail

if [[ $# -lt 2 ]]; then
  echo "usage: $0 <run-name> <command...>" >&2
  exit 2
fi

run_name="$1"
shift
gpu_log="$(mktemp)"
memory_log="$(mktemp)"
stdout_log="$(mktemp)"
stderr_log="$(mktemp)"

initial_swap_used_kib="$(
  awk '
    /^SwapTotal:/ { total = $2 }
    /^SwapFree:/  { free = $2 }
    END { print total - free }
  ' /proc/meminfo
)"

nvidia-smi \
  --query-gpu=timestamp,name,memory.used,memory.free,utilization.gpu,temperature.gpu,power.draw \
  --format=csv \
  -lms 500 > "${gpu_log}" &
gpu_monitor_pid=$!

(
  printf 'timestamp_epoch,mem_total_kib,mem_available_kib,swap_total_kib,swap_free_kib\n'
  while true; do
    awk -v now="$(date +%s.%N)" '
      /^MemTotal:/     { total = $2 }
      /^MemAvailable:/ { available = $2 }
      /^SwapTotal:/    { swap_total = $2 }
      /^SwapFree:/     { swap_free = $2 }
      END {
        printf "%s,%s,%s,%s,%s\n",
          now, total, available, swap_total, swap_free
      }
    ' /proc/meminfo
    sleep 0.5
  done
) > "${memory_log}" &
memory_monitor_pid=$!

cleanup() {
  kill "${gpu_monitor_pid}" "${memory_monitor_pid}" 2>/dev/null || true
  wait "${gpu_monitor_pid}" "${memory_monitor_pid}" 2>/dev/null || true
}
trap cleanup EXIT INT TERM

# setsidで独立したprocess groupにし、安全弁作動時に子processもまとめて止める。
setsid timeout --signal=TERM --kill-after=30s 15m \
  "$@" \
  > "${stdout_log}" \
  2> "${stderr_log}" &
command_pid=$!

abort_reason=""

while kill -0 "${command_pid}" 2>/dev/null; do
  read -r mem_available_kib swap_used_kib < <(
    awk '
      /^MemAvailable:/ { available = $2 }
      /^SwapTotal:/    { total = $2 }
      /^SwapFree:/     { free = $2 }
      END { print available, total - free }
    ' /proc/meminfo
  )

  swap_growth_kib=$((swap_used_kib - initial_swap_used_kib))
  stdout_bytes="$(stat -c '%s' "${stdout_log}")"

  if (( mem_available_kib < 1024 * 1024 )); then
    abort_reason="MemAvailable below 1024 MiB"
  elif (( swap_growth_kib > 2048 * 1024 )); then
    abort_reason="swap growth exceeded 2048 MiB"
  elif (( stdout_bytes > 64 * 1024 * 1024 )); then
    abort_reason="stdout exceeded 64 MiB"
  fi

  if [[ -n "${abort_reason}" ]]; then
    printf '%s\n' "${abort_reason}" >&2
    kill -TERM -- "-${command_pid}" 2>/dev/null || true
    break
  fi

  sleep 0.5
done

set +e
wait "${command_pid}"
status=$?
set -e

exit "${status}"

この監視処理では、推論プロセスと同時にGPUメモリ、system memory、swapを取り、安全弁を超えた場合はプロセスグループごと停止します。

4K contextを各3回測定

fit、CPU MoE、manualの実行順による偏りを抑えるため、試行ごとに順番を入れ替えて各3回測定しました。

最初に固定promptを作ります。

サンプルコード(固定prompt)
PROMPT_FILE="smoke-ja.txt"

printf '%s\n' \
  '次の質問に日本語で簡潔に答えてください。' \
  'CPUとGPUを併用して大きな言語モデルを動かす利点を、一文で説明してください。' \
  > "${PROMPT_FILE}"

共通変数:

サンプルコード(共通変数)
IK_CLI="<ik_llama.cppのllama-cli>"
MODEL="<Qwen3.6-35B-A3B Q4_K_M GGUF>"
PROMPT="<固定prompt>"

fitはbackendへ自動配置を任せます。

サンプルコード(4K fit)
./measure.sh 4k-fit-1 \
  "${IK_CLI}" \
  -m "${MODEL}" \
  -f "${PROMPT}" \
  -c 4096 \
  -n 128 \
  --temp 0 \
  --no-display-prompt \
  --simple-io \
  --fit

CPU MoEはMoE expertをCPU側へ置きます。

サンプルコード(4K CPU MoE)
./measure.sh 4k-cpu-moe-1 \
  "${IK_CLI}" \
  -m "${MODEL}" \
  -f "${PROMPT}" \
  -c 4096 \
  -n 128 \
  --temp 0 \
  --no-display-prompt \
  --simple-io \
  --cpu-moe \
  -ngl 999

manualはexpert層16~39をCPU側へ固定します。

サンプルコード(4K manual)
./measure.sh 4k-manual-1 \
  "${IK_CLI}" \
  -m "${MODEL}" \
  -f "${PROMPT}" \
  -c 4096 \
  -n 128 \
  --temp 0 \
  --no-display-prompt \
  --simple-io \
  -ngl 999 \
  -ot 'blk[.](1[6-9]|[23][0-9])[.]ffn_.*_exps.*=CPU'

同じ3コマンドを各3回実行しました。実際には順序をfit → CPU MoE → manual、CPU MoE → manual → fit、manual → fit → CPU MoEと巡回させています。

4Kでは3方式ともロード・生成に成功

以下は各3試行の中央値です。

配置 完走 Peak VRAM Peak system RAM Swap増加
fit 3/3 10,673MiB 19,554MiB 0MiB
manual 3/3 10,662MiB 19,817MiB 0MiB
CPU MoE 3/3 3,251MiB 27,410MiB 0MiB

qwen36-4k-memory.jpg

3構成すべて、4K contextでロードと生成に成功しました。

fitとmanualは約10.7GiBのVRAMを使用し、RAM使用量は約19~20GiBに収まりました。

一方、CPU MoEではPeak VRAMが3,251MiBまで下がりました。約7.4GiBのVRAMを空けられるため、画面表示や別のGPU処理と共存しやすくなります。

ただし、Peak system RAMは27,410MiBまで増えました。

VRAMから消えたweightは蒸発していません。RAMが食べています。

RAM 32GiB環境では成立しましたが、ブラウザ、IDE、Dockerなどを同時に動かすなら余裕は大きくありません。

8K contextまで広げる

4Kが成立したため、同じ128 token生成でcontextを8,192へ拡大しました。

サンプルコード(8K fit)
./measure.sh 8k-fit \
  "${IK_CLI}" \
  -m "${MODEL}" \
  -f "${PROMPT}" \
  -c 8192 \
  -n 128 \
  --temp 0 \
  --no-display-prompt \
  --simple-io \
  --fit

manualもcontext以外は同じ条件です。

サンプルコード(8K manual)
./measure.sh 8k-manual \
  "${IK_CLI}" \
  -m "${MODEL}" \
  -f "${PROMPT}" \
  -c 8192 \
  -n 128 \
  --temp 0 \
  --no-display-prompt \
  --simple-io \
  -ngl 999 \
  -ot 'blk[.](1[6-9]|[23][0-9])[.]ffn_.*_exps.*=CPU'

結果:

配置 Peak VRAM Peak system RAM Swap増加 判定
fit 10,822MiB 20,381MiB 0MiB 成立
manual 10,581MiB 15,344MiB 4,176MiB 安全基準外
CPU MoE - - - 未実施

fitは8Kでも成立

fitはPeak VRAM 10,822MiB、Peak system RAM 20,381MiB、swap増加0MiBで生成を完了しました。

今回の32GiB RAM環境では、8Kで安全基準を満たしたのはfitでした。

manualは生成できたが、不成立と判定

manualもモデルロードと生成までは完了しました。

ただし、試行開始時からswapが4,176MiB増加し、設定した2,048MiBの安全上限を超えました。ログでは約11.16GiBのpinned host memory確保も確認しています。

表のPeak system RAMが15,344MiBと低く見えるのは、必要メモリが減ったからではありません。大量のページがswapへ退避した後の観測値なので、むしろ危険側です。

返事は返ってきました。PCの応答性も一緒に持っていかれました。

そのため、今回は次のように区別します。

計算可能: yes
32GiB RAMで安全に常用可能: no

CPU MoEの8K試験は、manual試験後に約3.7GiBのswap使用が残っていたため実施しませんでした。結果を増やすために、次の測定条件を壊しては意味がありません。

チャット形式でも正常に返るか

固定promptによる成立確認とは別に、OpenAI互換のchat endpointからリクエストしました。

サーバー起動:

サンプルコード(chat server起動)
"<ik_llama.cppのllama-server>" \
  -m "<Qwen3.6-35B-A3B Q4_K_M GGUF>" \
  -c 4096 \
  --fit \
  --fit-margin 1024 \
  --jinja \
  --reasoning off \
  --reasoning-budget 0 \
  --host 127.0.0.1 \
  --port 18080

別terminalからリクエスト:

サンプルコード(chat request)
curl -N http://127.0.0.1:18080/v1/chat/completions \
  -H 'Content-Type: application/json' \
  -d '{
    "model": "local",
    "messages": [
      {
        "role": "user",
        "content": "次の質問に日本語で簡潔に答えてください。\nCPUとGPUを併用して大きな言語モデルを動かす利点を、一文で説明してください。"
      }
    ],
    "temperature": 0,
    "max_tokens": 128,
    "stream": true,
    "chat_template_kwargs": {
      "enable_thinking": false
    }
  }'

質問:

CPUとGPUを併用して大きな言語モデルを動かす利点を、
一文で説明してください。

応答:

CPUがモデルの制御や前処理を担いGPUが並列演算に特化することで、
リソースを最適に分散させ、推論速度の向上やメモリ効率の改善を実現します。

文字化け、反復、途中終了、user-facing出力へのthinking channel混入はありませんでした。

ここでは回答品質の優劣までは評価しません。「chat templateを通した通常の日本語応答が成立する」ことだけを確認しています。

成立条件と安全上の境界

ik_llama.cppを使えば35B級MoEを12GB GPUで動かせますが、RAM容量まで無視できるわけではありません。

特に8K manualは回答生成まで完了しています。しかし、swapが4GiB以上増えたため、常用可能とは判定していません。

ローカルLLMでは、次の2つは別です。

出力が返った
安定して使える

また、CLIの会話モードや長いsynthetic promptでは、入力待ちの反復や配置条件の変化も起きました。そのため、用途ごとに測定方法を分けています。

確認内容 使用方法
4K / 8Kの成立性 監視付き短文生成
通常のチャット出力 llama-serverのchat endpoint
VRAM、RAM、swap nvidia-smiと/proc/meminfo

つまり、ik_llama.cppが広げるのは「VRAMだけで決まっていたモデル選択の上限」です。その代わり、RAM容量、swap、配置方法が新しい制約になります。

まとめ

今回の実測で伝えたいことは、次の3点です。

  1. ik_llama.cppを使うと、12GB VRAMに収まらない35B級MoEをGPUとRAMへ分割して動かせる。
  2. 4Kではfit、manual、CPU MoEが各3/3でロードと生成を完了した。fitとmanualはPeak VRAM約10.7GiB、CPU MoEはPeak VRAM約3.2GiB、Peak RAM約27.4GiBだった。
  3. 8Kではfitがswap増加なしで成立した。manualは生成できたがswapが4,176MiB増加し、32GiB RAM環境の安全基準では不成立と判定した。

つまり、RTX 4070 12GBでも35B級MoEは動きます。

ただし勝負はVRAM容量だけではありません。

- 何をVRAMへ置くか
- 何をRAMへ逃がすか
- RAMとswapにどこまで余裕があるか
- contextをどこまで伸ばすか

で成立条件が変わります。

ik_llama.cppはVRAMを増やす魔法ではありません。

しかし、VRAMだけでは選べなかったモデルを、RAMとの分担で動作候補にするための道具にはなりました。

参考

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?