背景

Kimi-K2.5(1T級MoE)をEPYC 9175F上で運用し、バッチ処理で10-13 tok/sの安定したデコード速度を確認していた。次のステップとして、DeepSeek-V3.2をセカンドオピニオン用のモデルレーンとして評価することにした。

同じllama.cpp環境で動かしたところ、Prefill(PP)は50-100 tok/sと高速だったが、デコード(TG)が14-15 tok/sに張り付き、それ以上伸びなかった。Kimi-K2.5と比較して「遅い」という印象があったが、原因がモデル性能なのか運用設定なのかを切り分ける必要があった。

目的

  1. DeepSeek-V3.2のデコード速度が14-15 tok/sに停滞する原因を特定する
  2. llama.cppのログから、キャッシュ制御と最適化フラグの影響を定量的に把握する
  3. 改善策の優先順位を整理する

実験環境

  • CPU: AMD EPYC 9175F(Zen 5, 16C)
  • メモリ: DDR5-6400 768GB(12ch)
  • OS: Ubuntu 24.04 LTS
  • Runtime: llama.cpp(server mode, fused_moe=1)
  • Model: DeepSeek-V3.2 Speciale(MoE構成)
  • KV Cache: f16(デフォルト)

結果

推論スループット実測値

以下は連続する4つのタスクのログから抽出した値。

Task IDPP (tok)TG (tok)累積トークンPP速度 (tok/s)TG速度 (tok/s)PP時間 (s)TG時間 (s)
02,7311,0243,75599.7514.5727.470.3
10268571,0245,63674.4715.2111.567.3
20513119826,92952.9214.515.967.7
30344,8651,02412,818100.2214.2248.572.0

PPは入力量に応じて50-100 tok/sの幅で変動する。一方TGは14.22-15.21 tok/sの極めて狭い範囲に集中しており、入力量や累積トークン数に依存しない「壁」が存在する。

ログに観測されたキャッシュ不一致

  Common part does not match fully → kv cache rm [p0, end)
  

このログが全タスクで繰り返し出現。プロンプトの先頭トークン列がリクエスト間で不一致となり、KVキャッシュが毎回破棄されている。

Speculative Decodingの状態

  no implementations specified for speculative decoding
  

ドラフトモデル未指定のため、Speculative Decodingは未使用。

考察

TG 14-15 tok/s の壁の正体

デコード速度がタスク間でほぼ一定(14-15 tok/s)なのは、メモリ帯域の物理的な限界に当たっていることを示唆する。現状のKVキャッシュはf16精度で確保されており、文脈が長くなるほどAttention計算のメモリ帯域負荷が支配的になる。

Kimi-K2.5ではKVキャッシュをq8_0に量子化して帯域負荷を軽減していた。DeepSeek-V3.2でもこの設定を適用すれば、TG速度の改善が期待できる。

プロンプトキャッシュ不一致の影響

Kimi-K2.5での運用では、固定プレフィックス(System Prompt + ナレッジダイジェスト)を完全に一致させることで、LCPキャッシュのヒット率を高く維持していた。DeepSeek-V3.2の検証では以下の要因でキャッシュが効いていなかった:

  1. <think>タグの有無の揺れ: Thinking Promptの有無がリクエストごとに変わり、先頭トークン列が不一致
  2. System Promptの微妙な差異: テンプレートが固定されていなかった
  3. 会話履歴の管理差: Kimi-K2.5は前の文脈を再利用する設計だったが、DeepSeek側は毎回文脈を再構築していた

つまり「DeepSeekが遅い」のではなく、「キャッシュが効いていない状態で比較していた」公算が大きい。

MoE最適化の差

ログ上はfused_moe=1が有効だが、vLLMやサービスプロバイダのような専用カーネル最適化に比べると、llama.cppのMoE実装はまだ汎用的。エキスパートルーティング周りの実装差が速度差として現れている可能性がある。

感想

これは典型的な「ベンチマークの罠」だった。同じハードウェア、同じランタイムでも、プロンプトキャッシュの運用一つでスループットが大きく変わる。最初は「DeepSeekはKimiより遅い」と思い込んでいたが、ログを丁寧に読むと運用設定の差が主因だった。

TGが14-15 tok/sに張り付く現象自体は、KVキャッシュのf16設定とメモリ帯域で説明がつく。Kimi-K2.5と同じq8_0設定を適用していれば、異なる結果が出ていたはず。

再現方法

1. DeepSeek-V3.2の実行

  podman run --rm -p 8081:8080 --shm-size 16g --cap-add=SYS_NICE \
  -v /path/to/deepseek-v3.2:/models:Z \
  compute.home.arpa/llamacpp-zen5:latest \
  -m /models/DeepSeek-V3.2-Speciale.gguf \
  --cache-type-k f16 --cache-type-v f16 --flash-attn on \
  --ctx-size 16384 --parallel 1 --threads 13 --threads-batch 13 \
  --batch-size 2048 --ubatch-size 512 --jinja --host 0.0.0.0 --port 8080
  

2. 改善版(KVキャッシュ量子化 + プロンプト固定)

  # KVキャッシュをq8_0に変更
--cache-type-k q8_0 --cache-type-v q8_0

# プロンプトキャッシュを有効化
--prompt-cache /tmp/deepseek-cache.bin
  

加えて、System Promptと<think>タグの有無をリクエスト間で完全に固定する。

3. 計測

llama.cppのサーバーログからS_PPS_TGを抽出し、改善前後の差分を比較する。

補足ノウハウ

プロンプトキャッシュを効かせるための原則

  1. 先頭トークン列を固定する: System Prompt → 固定コンテキスト → 可変部分、の順序を厳守
  2. Thinkingモードは一貫させる: 有効にするなら全リクエストで有効に。リクエストごとに切り替えるとキャッシュが毎回破棄される
  3. 温度・top_pを揃える: 生成パラメータの差異もキャッシュヒット率に影響する場合がある

Kimi-K2.5との公平な比較をするには

  • 出力トークン数、温度、top_p、stopシーケンス、stream有無を完全に一致させる
  • Thinking Tokenの扱い(ログ上はExclude reasoning tokensでスロット選択からは除外されているが、生成自体は行われている)を統一する
  • 同一のハードウェア・スレッド数・KVキャッシュ設定で計測する

改善策の優先順位

優先度施策期待効果実装コスト
Aプロンプトプレフィックスの固定化PP大幅短縮低(設定変更のみ)
BSpeculative Decoding導入TG体感速度改善中(ドラフトモデル選定が必要)
CKVキャッシュq8_0化TG帯域負荷軽減低(フラグ変更のみ)
D生成条件の統制公平な比較の実現低(テスト設計の問題)