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点です。
-
ik_llama.cppのfit、CPU MoE、tensor overrideで、VRAMに収まらないQwen3.6-35B-A3B Q4_K_Mをロード・生成できるか。 - 4K contextでVRAM、RAM、swapがどこまで増えるか。
- 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点です。
- モデルをロードできる
- 128 tokens以内の生成を完了できる
- VRAM、RAM、swapを記録できる
- 安全弁を作動させず終了できる
測定中は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 |
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点です。
- ik_llama.cppを使うと、12GB VRAMに収まらない35B級MoEをGPUとRAMへ分割して動かせる。
-
4Kでは
fit、manual、CPU MoEが各3/3でロードと生成を完了した。fitとmanualはPeak VRAM約10.7GiB、CPU MoEはPeak VRAM約3.2GiB、Peak RAM約27.4GiBだった。 -
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との分担で動作候補にするための道具にはなりました。
