1T級MoE Kimi-K2.5のCPU推論実測:スレッド最適化からLong Context運用設計まで
Kimi-K2.5(1.03T MoE, Q4_K_S/Q4_K_M)をEPYC 9175FでCPU推論した全記録。スレッド最適化でth=13が最適解になる理由、Q4_K_Mでの16k Long Context実測、LCPキャッシュの効果、そしてDagsterバッチ運用に至る設計判断をまとめた。
はじめに
Kimi-K2.5のような1T級モデルをローカルで回す話は、どうしても「動くか動かないか」に寄りがちだ。ただ、今回自分が見たかったのはそこではなく、Dagsterパイプラインや非同期バッチ生成の基盤として実用になるかどうかだった。対話UIで即応性を求める用途ではなく、データセット増幅、蒸留用teacher生成、ローカル完結の検証基盤として成立するなら意味がある。
その前提で、AMD EPYC 9175F、768GB DDR5-6400、llama.cpp server、Kimi-K2.5 Q4_K_S というかなり尖った構成を試した。結論から言うと、CPUオンリーでも十分に成立する。ただし、成立の仕方は「全部盛りで速い」ではなく、12チャネル帯域とキャッシュ再利用を理解したうえで、運用の形を合わせる必要があった。
背景
Kimi-K2.5はMoonshot AIが開発した1.03兆パラメータのMoEモデル。384個のエキスパートから8個を選択し、推論時の活性パラメータは約32Bに抑えられる。DeepSeek-V2系アーキテクチャ(MLA: Multi-head Latent Attention)を採用しており、KVキャッシュが圧縮されるため、メモリ効率が高い。
Q4_K_S量子化でRSS約523GiB、Q4_K_Mで約579GiB。768GBのDDR5メモリに収まるため、GPUなしのCPU推論が物理的に成立する。問題は「成立する」と「使える」の間にある速度差で、それを埋めるのがこの検証の目的だった。
今回の検証で見たかったのは次の用途だ。
- Dagster pipeline
- 非同期バッチ生成
- データセット増幅 / 蒸留
- GPU agent の補助 / 検証用 LLM
- フォールバック用ローカルLLM
つまり、対話UXではなく、多少の待ち時間が許容される非同期ワークロードである。
目的
- Q4_K_S量子化でのCPU推論速度をベンチマーク(基本性能)
- スレッド数とスループットの関係を測定し、最適スレッド数を特定
- Q4_K_Mでの32kコンテキスト運用時のPrefill/Decode速度を計測
- Prompt Cache(LCP similarity)の実効性を検証
- Dagsterパイプライン用途として実用に足るか判定
実験環境
| 項目 | 仕様 |
|---|---|
| CPU | AMD EPYC 9175F(Zen 5, 16C, L3 512MB) |
| メモリ | DDR5-6400 768GB(12ch) |
| GPU | NVIDIA RTX PRO 6000 Blackwell Max-Q 96GB |
| OS | Ubuntu 24.04 LTS |
| Runtime | llama.cpp server(Podman rootless) |
モデル仕様
| 項目 | Q4_K_S | Q4_K_M |
|---|---|---|
| アーキテクチャ | deepseek2(MoE + MLA) | 同左 |
| 総パラメータ | 1.03T | 同左 |
| レイヤー数 | 61 | 同左 |
| エキスパート数 | 384(活性8) | 同左 |
| 量子化 | Q4_K_S | Q4_K_M(4.84 bpw) |
| モデルサイズ | 約520GiB(RSS) | 578.57 GiB |
| 学習コンテキスト長 | 262,144 | 同左 |
実施内容
ベンチマークコマンド(llama-sweep-bench)
MODEL=/models/snapshots/386fed8b054275941d6a495a9a7010fbf31b560d/Q4_K_S/Kimi-K2.5-Q4_K_S-00001-of-00013.gguf
IMG=compute.home.arpa/ik_llama-cuda:latest
podman run --rm -it \
--device nvidia.com/gpu=all \
--shm-size 16g \
--cap-add=SYS_NICE \
-v /mnt/data/hf/hub/models--unsloth--Kimi-K2.5-GGUF:/models:ro,Z \
$IMG \
/app/llama-sweep-bench \
--model "$MODEL" \
--no-mmap --merge-qkv \
-mla 3 -amb 512 \
-b 4096 -ub 4096 \
-ctk f16 -ctv f16 \
-c 131072 \
-ngl 999 -ot exps=CPU \
--threads 13 \
--threads-batch 26 \
--warmup-batch \
-n 128
このコマンドは以下を実行する:
llama-sweep-bench: ベンチマーク専用ツール-ngl 999 -ot exps=CPU: 全GPU層オフロード、Expert重みはCPU配置-c 131072: 131k コンテキスト対応-ctk f16 -ctv f16: KVキャッシュを f16 で保持--threads 13 --threads-batch 26: スレッド設定-mla 3 -amb 512: MLA(Multi-head Latent Attention)パラメータ
スレッド数別の起動コマンド
検証ではスレッド数を変えながら繰り返し計測した。以下はCPUオンリーでの代表的な起動コマンドである。
th=16 (Maximum Performance)
podman run --rm -p 8081:8080 --shm-size 16g --cap-add=SYS_NICE \
-v /mnt/data/hf/hub/models--unsloth--Kimi-K2.5-GGUF:/models:Z \
compute.home.arpa/llamacpp-zen5:latest \
-m /models/snapshots/386fed8b054275941d6a495a9a7010fbf31b560d/Q4_K_S/Kimi-K2.5-Q4_K_S-00001-of-00013.gguf \
--cache-type-k q8_0 --cache-type-v q8_0 --flash-attn on \
--ctx-size 8192 --parallel 1 --threads 16 --threads-batch 16 \
--batch-size 2048 --ubatch-size 512 --jinja --host 0.0.0.0 --port 8080
th=13 (Operational Sweetspot)
podman run --rm -p 8081:8080 --shm-size 16g --cap-add=SYS_NICE \
-v /mnt/data/hf/hub/models--unsloth--Kimi-K2.5-GGUF:/models:Z \
compute.home.arpa/llamacpp-zen5:latest \
-m /models/snapshots/386fed8b054275941d6a495a9a7010fbf31b560d/Q4_K_S/Kimi-K2.5-Q4_K_S-00001-of-00013.gguf \
--cache-type-k q8_0 --cache-type-v q8_0 --flash-attn on \
--ctx-size 8192 --parallel 1 --threads 13 --threads-batch 13 \
--batch-size 2048 --ubatch-size 512 --jinja --host 0.0.0.0 --port 8080
メモリ配置(Q4_K_S実測)
| 領域 | サイズ |
|---|---|
| KV cache (K) | 1,098 MiB |
| KV cache (V) | 976 MiB |
| CPU compute buffer | 348 MiB |
| Total RSS | 約523 GiB / 755 GiB |
| Swap使用 | 799 MiB(si/so発生なし) |
メモリ配置(Q4_K_M / ctx=32k)
| 領域 | サイズ |
|---|---|
| KV cache | 4,148 MiB(K: 2,196 / V: 1,952) |
| CPU compute buffer | 348 MiB |
| CPU repack buffer | 459,665 MiB |
| モデルバッファ | 578.57 GiB(13分割GGUF) |
768GB RAMを積んだマシンであれば16k contextはかなり現実的だと分かる。少なくとも今回のレンジでは、KV cacheが主役ではなく、圧倒的にモデル本体の重さが支配的だった。
CPU推論の実演動画
実際のCPU推論の出力内容を検証するため、EPYC 9175F上でのKimi-K2.5実行風景を撮影した。
動画リンク: https://www.youtube.com/watch?v=n8htU2pmzNI
この動画では以下を確認できる:
- llama.cpp serverの起動とモデル読み込み(量子化ロード)
- Prefill(プロンプト評価)時のトークン生成速度と内容
- Token-by-tokenのGenerate(生成)フェーズの出力
- 実際の生成テキストの品質確認
結果
Q4_K_S基本ベンチマーク(th=14, ctx=16k)
| リクエスト | Prompt(tok) | PP速度(tok/s) | Gen(tok) | TG速度(tok/s) | 合計(s) |
|---|---|---|---|---|---|
| 1st(キャッシュなし) | 823 | 22.24 | 438 | 10.27 | 79.7 |
| 2nd(キャッシュ保存) | 1,335 | 19.98 | 1,012 | 8.76 | 115.6 |
| 3rd(LCPヒット) | - | - | - | - | cache lookup 62ms |
初回はやはり重い。対話用途の快適さを期待する数字ではない。ただ、バッチ処理の1ジョブとして見れば成立する。80秒で1本のまとまった生成が返るなら、夜間バッチや非同期ワーカーとしては十分使い道がある。
3回目でLCP similarityが当たり、キャッシュルックアップが62msまで短縮されたのは今回の中でもかなり大きい発見だった。文脈をすべて毎回prefillするのではなく、既存状態との差分だけを見る形に持ち込めると、1T級でも反復ジョブが現実的になる。
スレッド最適化(ctx=8k)
| スレッド数 | PP速度(tok/s) | TG速度(tok/s) | 評価 |
|---|---|---|---|
| 16 | 24.43 | 12.94 | 最大出力(基準) |
| 14 | 21.32 | 12.50 | 帯域飽和の開始点 |
| 13 | 21.58 | 11.67 | スイートスポット |
| 12 | 14.58 | 11.86 | リソース効率重視 |
この表を見ると、prefillはコア増加にある程度追従しても、decodeは13-14付近で頭打ちになる。CPUコアを増やせばそのまま伸びる世界ではなく、先にメモリ帯域の壁へ当たっていると読むのが自然だ。
th=13で運用する意味は、残り3コアをDagster/Trino等のデータパイプラインに解放できること。推論速度の9割を維持しつつ、他プロセスとの共存が成立する。
Q4_K_M Long Context実測(th=13, ctx=32k)
| リクエスト | Prompt(tok) | PP速度(tok/s) | Gen(tok) | TG速度(tok/s) | 備考 |
|---|---|---|---|---|---|
| 1st | 16,148 | 6.15 | 333 | 2.44 | 16k一括Prefill、約44分 |
| 2nd(LCP 0.978) | 356 | 3.40 | 2,048 | 2.26 | キャッシュヒット、差分のみPrefill |
| 3rd(LCP 0.999) | 12 | 3.11 | 1,024 | 2.15 | ほぼ全量キャッシュ復元 |
| 4th(LCP 0.939) | 1,050 | 3.21 | 1,024 | 2.07 | 部分キャッシュ + 差分Prefill |
16kの一括prefillに約44分かかる事実は、256kコンテキストの「毎回ゼロからPrefill」が非現実的であることを示唆している。20 tok/sで256kを計算すると約3.5時間。
ただしprompt cache(LCP similarity)が効くケースでは話が変わる。2回目以降は356 token、12 token、1050 tokenの差分だけをprefillしている。固定digestを崩さない設計がそのまま効いている。
prefillの速度低下も顕著だった。前半は10 tok/s台、中盤は7 tok/s前後、終盤は4 tok/s台まで落ちた。decode側も2.44 tok/sと厳しく、1k出力に約6-7分、2k出力に約13-14分かかる。対話型には厳しいが、Dagsterなどから非同期に投げるバッチ用途なら成立する。
Prompt Cache効果(Q4_K_S)
| 状態 | サイズ | 効果 |
|---|---|---|
| 1,260トークン保存時 | 159.5 MiB | LCP similarity > 0.5でヒット |
| キャッシュ復元 | - | 数十ms(62ms実測) |
| TTFT短縮 | - | 反復実行でprompt eval時間が激減 |
Long Context検証のログでも以下のようにLCP similarityの再利用が出ている:
selected slot by LCP similarity, sim_best = 0.978 (> 0.100 thold), f_keep = 0.980selected slot by LCP similarity, sim_best = 0.999 (> 0.100 thold), f_keep = 0.870selected slot by LCP similarity, sim_best = 0.939 (> 0.100 thold), f_keep = 0.940
つまり2回目以降は、固定prefixをまるごと舐め直していない。固定digestを崩さない設計がそのまま効いている。
追記:ik_llama.cpp による改善(Expert CPU + Attention GPU Hybrid)
ik_llama.cppの最適化ビルドを使用し、Expert重みをCPU、Attention層をGPUに配置するHybrid構成(-ngl 999 -ot exps=CPU)で実測を取得した。
実行コマンド
podman run --rm -it --device nvidia.com/gpu=all \
-p 8081:8080 \
--shm-size 32g \
--cap-add=SYS_NICE \
-v /mnt/data/hf/hub/models--unsloth--Kimi-K2.5-GGUF:/models:ro,Z \
$IMG \
--host 0.0.0.0 --port 8080 \
-m "$MODEL" --no-mmap --jinja \
-c 131072 \
-n 128 \
--threads 13 --threads-batch 26 \
-b 2048 -ub 512 \
-ngl 999 -ot exps=CPU \
-ctk f16 -ctv f16 \
--merge-qkv -mla 3 -amb 512
ベンチマーク結果(初期)
| Task | PP(tok) | TG(tok) | N_KV(tok) | T_PP(s) | S_PP(t/s) | T_TG(s) | S_TG(t/s) |
|---|---|---|---|---|---|---|---|
| 0 | 5,264 | 744 | 6,007 | 59.596 | 88.33 | 37.815 | 19.67 |
| 747 | 765 | 259 | 6,287 | 13.277 | 57.62 | 13.164 | 19.68 |
| 1,007 | 279 | 1,024 | 7,331 | 6.243 | 44.69 | 52.452 | 19.52 |
| 2,032 | 1,037 | 1,024 | 8,368 | 16.772 | 61.83 | 51.793 | 19.77 |
| 3,057 | 1,041 | 310 | 8,695 | 16.637 | 62.57 | 16.124 | 19.23 |
| 平均 | - | - | - | - | 63.0 | - | 19.6 |
後続実測
Prompt Cache(LCP)やテンプレート最適化を進めた後の実測:
| Run | PP(tok) | TG(tok) | N_KV(tok) | T_PP(s) | S_PP(t/s) | T_TG(s) | S_TG(t/s) | 備考 |
|---|---|---|---|---|---|---|---|---|
| 1 | 5,330 | 401 | 5,730 | 41.298 | 129.06 | 20.458 | 19.60 | 新規リクエスト |
| 2 | 416 | 2,241 | 7,986 | 8.363 | 49.75 | 114.552 | 19.56 | キャッシュ部分不一致 |
| 3 | 2,255 | 919 | 8,919 | 20.631 | 109.30 | 48.056 | 19.12 | キャッシュ部分不一致 |
評価
良くなった点:
- Prefill耐性の向上: Run 1, 3で100-130 t/s級のPrefill速度を実現。長いプロンプトでも高速処理
- 生成速度の安定化: S_TGは全Runで19 t/s前後で安定。Blackwell + Q4_K_Sの組み合わせで頭打ち
- キャッシュ効果: Run 2, 3での部分キャッシュヒット時も、S_PPは50-110 t/sを維持
現在の制限:
- 生成速度のボトルネック: S_TGが約19 t/s固定のため、体感の「遅い/速い」はPrefill時間と出力トークン数に左右される
- キャッシュ一貫性: Run 2, 3で “Common part does not match fully” が出現。System Promptやテンプレートの細微な変更(改行・スペース・タイムスタンプ)でキャッシュが割れる
考察
メモリ帯域とth=13の根拠
Decode速度はth=13-14で飽和する。12チャネルDDR5-6400の理論帯域は約614GB/sだが、MoEのランダムアクセスパターンでは帯域をフルに使い切れない。th=16で12.94 tok/s、th=13で11.67 tok/sと、3スレッド減でも速度低下は10%未満。
th=13で運用する意味は、残り3コアをDagster/Trino等のデータパイプラインに解放できること。推論速度の9割を維持しつつ、他プロセスとの共存が成立する。
Long Contextの現実解
16k一括Prefillに44分かかる事実は、256kコンテキストの「毎回ゼロからPrefill」が非現実的であることを示唆している。
実運用の解は:
- ctx=16k-32kに抑える
- System Digest(8k-16k程度)を起動時に一度Prefill
- Prompt Cache(LCP similarity)で2回目以降は差分のみ処理
- 出力長は1k基本、必要時のみ2k
Decode 2.4 tok/s vs 10 tok/s
Q4_K_Sのctx=16kでは10 tok/s、Q4_K_Mのctx=32kでは2.4 tok/s。この差はコンテキスト長に起因する。32kのKVキャッシュ(4.1GB)のAttention計算がボトルネック。対話UXには厳しいが、バッチ処理なら待ち時間が問題にならない。
なぜこの構成がここまで回るのか
MoE推論がこのCPU構成で予想以上に動く理由は、「全部がキャッシュに入るから」ではない。再利用頻度の高い作業セットがL3に留まりやすいからだと考える方が自然だった。
L3が効いているのは次のような高再利用ホット領域である:
- Router / Gatingロジック
- Projection周辺
- 直前レイヤのweight / 中間テンソル
- KV再利用部分
加えて、EPYC 9175Fの物理特性そのものが効いている:
- 巨大L3(512MB)x 低コア数: 16コアに対して512MB L3。コア間競合がほぼ発生しない
- 超低メモリレイテンシ構成: 12チャネルに対して16コアで、メモリコントローラの待ち行列がほぼ発生しない
- Zen 5のBF16 / AVX-512: 物理的な512-bit datapathとBF16ネイティブ処理が
llama.cppの最適化と噛み合う
Prompt Cacheの設計原則
- System Digestは完全固定(改行・空白・日付差分でキャッシュが割れる)
- RAGコンテキストはsystemに混ぜず、user側に差し込む(キャッシュ維持が最優先)
- 出力スタイルを短めに固定する(生成が遅い以上、出力長を抑えることもそのままUX改善になる)
aichatの.file /path/to/fileによる入力も有効で、巨大文書を毎回system promptに埋め込むのではなく、固定のdigestをsystemに置き、必要な文書だけuser側に差し込む方がキャッシュ維持に有利。
結論
1T級モデルをCPUで動かすこと自体は技術的に確立できた。Decode 10 tok/sは対話用途には不十分だが、Dagsterパイプラインのバッチ生成、データセット増幅、蒸留用teacher生成には十分実用範囲。
運用設計の結論: GPU(RTX PRO 6000)側で対話・高速推論(vLLM等)を回し、CPU llama.cppはth=13で常駐させてバッチ知能として使う。768GBメモリのうち523GiBをモデルに使っても、残り200GB以上でDataFrame操作やTrinoクエリが並列実行できる。
今回の収穫は、単に1T級モデルが動いたことではない。th=13付近で12チャネル帯域が飽和すること、LCP similarityベースのprompt cache再利用が継続運用に効くこと、そしてEPYC 9175Fの構成がMoE推論にかなり噛み合っていることを、実測値ベースで整理できた点にある。
推奨パラメータ(バッチ運用向け)
| パラメータ | 推奨値 | 理由 |
|---|---|---|
| ctx | 16,384-32,768 | 256kは非現実的 |
| system digest | 8k(長くても12k-16k) | 起動時に一度prefillし以後はcache |
| threads | 13 | メモリ帯域飽和点、残り3コアをパイプラインに |
| ubatch | 256 | 512より安定(失速しにくい可能性あり) |
| cache-ram | 32,768 MiB | LCPヒット率の安定化 |
| output | 1,024 | 生成速度がボトルネックのため短く |
運用推奨コマンド
podman run --rm -p 8081:8080 --shm-size 16g --cap-add=SYS_NICE \
-v /mnt/data/hf/hub/models--unsloth--Kimi-K2.5-GGUF:/models:Z \
compute.home.arpa/llamacpp-zen5:latest \
-m /models/snapshots/386fed8b054275941d6a495a9a7010fbf31b560d/Q4_K_S/Kimi-K2.5-Q4_K_S-00001-of-00013.gguf \
--cache-type-k q8_0 --cache-type-v q8_0 --flash-attn on \
--ctx-size 131072 --parallel 1 --threads 13 --threads-batch 13 \
--batch-size 2048 --ubatch-size 512 --jinja --host 0.0.0.0 --port 8080
再現方法
1. モデル取得
# Q4_K_S
huggingface-cli download unsloth/Kimi-K2.5-GGUF \
--include "Q4_K_S/*" \
--local-dir /mnt/data/hf/hub/models--Kimi-K2.5-GGUF
# Q4_K_M
huggingface-cli download unsloth/Kimi-K2.5-GGUF \
--include "Q4_K_M/*" \
--local-dir /mnt/data/hf/hub/models--Kimi-K2.5-GGUF
2. 実行
上記「実施内容」セクションのコマンドを参照。llama.cppのflash-attnとprompt cache機能が必要。
3. 計測
curl -s http://localhost:8081/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"kimi","messages":[{"role":"user","content":"Explain MoE architecture"}],"max_tokens":512}'
サーバーログのprompt eval timeとeval timeから速度を抽出。
補足ノウハウ
Q4_K_S vs Q4_K_M
Q4_K_Mはモデルサイズが約60GB大きい(520→579GiB)。メモリに余裕があればQ4_K_Mの方が品質は高いが、速度差は大きくない。ctx=16k運用ならQ4_K_Sで十分。
比較用ベンチマーク:Llama-4-Maverick-17B-128E
比較用に、別のMoEモデルで量子化差も確認した。
| Quant | Prefill (tok/s) | Decode (tok/s) | TTFT (1k context) |
|---|---|---|---|
| Q4_K_M | 65-68 | 21-24 | 12-17s |
| Q8_0 | 50-52 | 15-16 | 16-20s |
CPU推論で量子化やモデル特性がどの程度体感差になるかを見る補助線としては有効だった。
