CPU推論で測ったこと

Kimi-K2.5 を CPU で動かし、非同期バッチに使うための速度を測りました。用途は Dagster、データセット増幅、蒸留用 teacher 生成、ローカルでの検証です。即時応答より、待ち時間を許容する処理を想定しています。

AMD EPYC 9175F、768GB DDR5-6400、llama.cpp server で Kimi-K2.5 Q4_K_S を試しました。CPU-only で動きますが、12チャネル帯域と prompt cache に合わせた運用が必要でした。

背景

Moonshot AI の1.03兆パラメータ MoE です。384 expert のうち8つを使い、活性パラメータは約32Bです。DeepSeek-V2 系の MLA(Multi-head Latent Attention)で KV cache を圧縮します。

RSS は Q4_K_S が約523GiB、Q4_K_M が約579GiBです。768GBに収まるため、GPUなしで起動できます。速度を次の用途で確認しました。

想定用途です。

  • Dagster pipeline
  • 非同期バッチ生成
  • データセット増幅 / 蒸留
  • GPU agent の補助 / 検証用 LLM
  • フォールバック用ローカルLLM

非同期処理で許容できる待ち時間と、context の使い方を検討します。

比較項目

  1. Q4_K_S量子化でのCPU推論速度をベンチマーク(基本性能)
  2. スレッド数とスループットの関係を測定し、最適スレッド数を特定
  3. Q4_K_Mでの32kコンテキスト運用時のPrefill/Decode速度を計測
  4. Prompt Cache(LCP similarity)の実効性を検証
  5. Dagsterパイプライン用途として実用に足るか判定

実験環境

項目仕様
CPUAMD EPYC 9175F(Zen 5, 16C, L3 512MB)
メモリDDR5-6400 768GB(12ch)
GPUNVIDIA RTX PRO 6000 Blackwell Max-Q 96GB
OSUbuntu 24.04 LTS
Runtimellama.cpp server(Podman rootless)

モデル仕様

項目Q4_K_SQ4_K_M
アーキテクチャdeepseek2(MoE + MLA)同左
総パラメータ1.03T同左
レイヤー数61同左
エキスパート数384(活性8)同左
量子化Q4_K_SQ4_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 context
  • -ctk f16 -ctv f16: KV cache は f16
  • --threads 13 --threads-batch 26: スレッド数
  • -mla 3 -amb 512: MLA のパラメータ

スレッド数別の起動コマンド

スレッド数を変えて計測しました。次は CPU-only の代表的なコマンドです。

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(他の処理との共存)

  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 buffer348 MiB
Total RSS約523 GiB / 755 GiB
Swap使用799 MiB(si/so発生なし)

メモリ配置(Q4_K_M / ctx=32k)

領域サイズ
KV cache4,148 MiB(K: 2,196 / V: 1,952)
CPU compute buffer348 MiB
CPU repack buffer459,665 MiB
モデルバッファ578.57 GiB(13分割GGUF)

16k context を含めても768GBに収まります。使用量の大半は KV cache ではなくモデル本体です。

CPU推論の実演動画

EPYC 9175F での CPU 推論を動画に記録しました。

動画リンク: https://www.youtube.com/watch?v=n8htU2pmzNI

動画では次の処理を確認できます。

  • llama.cpp server の起動と量子化モデルの読み込み
  • prefill 時の速度と入力内容
  • token ごとの生成
  • 実際の出力テキスト

結果

Q4_K_S基本ベンチマーク(th=14, ctx=16k)

リクエストPrompt(tok)PP速度(tok/s)Gen(tok)TG速度(tok/s)合計(s)
1st(キャッシュなし)82322.2443810.2779.7
2nd(キャッシュ保存)1,33519.981,0128.76115.6
3rd(LCPヒット)----cache lookup 62ms

初回は重く、対話では待ち時間が気になります。約80秒の生成なら、夜間や非同期バッチの候補になります。

3回目は LCP similarity が一致し、cache lookup が62msになりました。既存の prompt 状態を再利用すると、反復 job の待ち時間を減らせます。

スレッド最適化(ctx=8k)

スレッド数PP速度(tok/s)TG速度(tok/s)評価
1624.4312.94最大出力(基準)
1421.3212.50帯域飽和の開始点
1321.5811.67スイートスポット
1214.5811.86リソース効率重視

decode は th=13–14付近で伸びが止まりました。計算量よりメモリ帯域が制約になっている可能性があります。

th=13は推論速度の約9割を保ち、残り3コアを Dagster / Trino などに使うための候補です。

Q4_K_M Long Context実測(th=13, ctx=32k)

リクエストPrompt(tok)PP速度(tok/s)Gen(tok)TG速度(tok/s)備考
1st16,1486.153332.4416k一括Prefill、約44分
2nd(LCP 0.978)3563.402,0482.26キャッシュヒット、差分のみPrefill
3rd(LCP 0.999)123.111,0242.15ほぼ全量キャッシュ復元
4th(LCP 0.939)1,0503.211,0242.07部分キャッシュ + 差分Prefill

16k の一括 prefill は約44分でした。20 tok/sでも256kなら約3.5時間のため、毎回の全量処理は負担が大きくなります。

2回目以降は356、12、1050 tokens の差分だけを prefill しました。固定 digest が cache の再利用に寄与しました。

prefill は前半10 tok/s台、中盤7 tok/s前後、終盤4 tok/s台に低下しました。decode は2.44 tok/sで、1k出力に約6–7分、2kに約13–14分の見積もりです。対話には遅く、非同期バッチを想定します。

Prompt Cache効果(Q4_K_S)

状態サイズ効果
1,260トークン保存時159.5 MiBLCP similarity > 0.5でヒット
キャッシュ復元-数十ms(62ms実測)
TTFT短縮-反復実行でprompt eval時間が激減

長文のログにも LCP similarity の再利用があります。

  • selected slot by LCP similarity, sim_best = 0.978 (> 0.100 thold), f_keep = 0.980
  • selected slot by LCP similarity, sim_best = 0.999 (> 0.100 thold), f_keep = 0.870
  • selected slot by LCP similarity, sim_best = 0.939 (> 0.100 thold), f_keep = 0.940

固定 prefix を再評価せず、差分を処理しています。

追記:ik_llama.cpp による改善(Expert CPU + Attention GPU Hybrid)

追記の計測では ik_llama.cpp を最適化してビルドし、-ngl 999 -ot exps=CPU で Expert を CPU、Attention を GPU に置きました。

実行コマンド

  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
  

ベンチマーク結果(初期)

TaskPP(tok)TG(tok)N_KV(tok)T_PP(s)S_PP(t/s)T_TG(s)S_TG(t/s)
05,2647446,00759.59688.3337.81519.67
7477652596,28713.27757.6213.16419.68
1,0072791,0247,3316.24344.6952.45219.52
2,0321,0371,0248,36816.77261.8351.79319.77
3,0571,0413108,69516.63762.5716.12419.23
平均----63.0-19.6

後続実測

Prompt Cache とテンプレートを調整した後の値です。

RunPP(tok)TG(tok)N_KV(tok)T_PP(s)S_PP(t/s)T_TG(s)S_TG(t/s)備考
15,3304015,73041.298129.0620.45819.60新規リクエスト
24162,2417,9868.36349.75114.55219.56キャッシュ部分不一致
32,2559198,91920.631109.3048.05619.12キャッシュ部分不一致

評価

観測した値:

  • Run 1, 3のPrefillは100-130 t/sでした
  • S_TGは全Runで19 t/s前後でした。このBlackwell + Q4_K_Sの条件では、生成速度に差がありません
  • Run 2, 3の部分cache hitでは、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の根拠

th=16は12.94 tok/s、th=13は11.67 tok/sでした。3スレッド減らしても低下は10%未満です。12チャネル DDR5-6400 の理論帯域は約614GB/sですが、MoE のランダムアクセスでは全帯域を使えないと考えています。

長い入力の運用案

長い入力では、毎回全量を prefill しない運用を検討します。

運用案です。

  • ctx=16k–32kに抑える
  • System Digest(8k–16k程度)を起動時に一度 prefill する
  • LCP similarity で以降は差分を処理する
  • 出力は1kを基本とし、必要時のみ2kにする

量子化とcontextが異なる2構成の比較

Q4_K_S / ctx=16kは10 tok/s、Q4_K_M / ctx=32kは2.4 tok/sでした。32kの KV cache(4.1GB)の Attention 計算が影響した可能性があります。量子化と context の両方が違うため、単一要因の比較ではありません。

L3 cacheとCPU構成の仮説

CPU の速度には、再利用が多い作業セットの L3 cache hit が寄与した可能性があります。全体が cache に入るわけではなく、次の領域が候補です。

再利用が多いと考えられる領域です。

  • Router / Gatingロジック
  • Projection周辺
  • 直前レイヤのweight / 中間テンソル
  • KV再利用部分

EPYC 9175F の次の特性も、速度に関係すると考えています。

  1. L3(512MB)と16コア: コア数に対するcache容量が大きく、コア間競合が少ない可能性があります
  2. 12チャネルと16コア: メモリコントローラの待ち行列が短い可能性があります
  3. Zen 5のBF16 / AVX-512: 512-bit datapathとBF16ネイティブ処理がllama.cppの最適化に合うと考えています

Prompt Cacheを維持する入力設計

  • System Digestは完全固定(改行・空白・日付差分でキャッシュが割れる)
  • RAGコンテキストはsystemに混ぜず、user側に差し込む(キャッシュ維持が最優先)
  • 出力スタイルを短めに固定する(生成が遅い以上、出力長を抑えることもそのままUX改善になる)

aichat の .file /path/to/file で必要な文書を user 側に渡し、system 側の digest を固定する方法も使えます。

結論

1T級を CPU-only で動かせました。decode 10 tok/sは、今回想定した非同期の生成、データセット増幅、teacher 生成の候補になります。

LLM導入の運用案は、GPU(RTX PRO 6000、vLLMなど)を対話、CPU llama.cpp / th=13をバッチに分けるものです。768GB中523GiBを使い、200GB以上を DataFrame や Trino に残す見積もりですが、並列時の負荷は別に確認が必要です。

確認できたのは th=13付近の速度と、LCP similarity の再利用です。これをもとに、推論とデータ処理の資源配分を検討します。

推奨パラメータ(バッチ運用向け)

パラメータ推奨値理由
ctx16,384-32,768256kは非現実的
system digest8k(長くても12k-16k)起動時に一度prefillし以後はcache
threads13メモリ帯域飽和点、残り3コアをパイプラインに
ubatch256512より安定(失速しにくい可能性あり)
cache-ram32,768 MiBLCPヒット率の安定化
output1,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. 実行

「実施内容」のコマンドを使います。flash-attn と prompt cache 対応の llama.cpp が必要です。

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です。品質を期待する場合の候補ですが、今回の ctx=16k バッチでは Q4_K_S を候補にしています。

比較用ベンチマーク:Llama-4-Maverick-17B-128E

別の MoE でも量子化ごとの値を確認しました。

QuantPrefill (tok/s)Decode (tok/s)TTFT (1k context)
Q4_K_M65-6821-2412-17s
Q8_050-5215-1616-20s

量子化とモデル構造が CPU 推論にどう影響するかを比べる参考値です。