なぜDeepSeek-V3.2はKimi-K2.5より遅く見えるのか:プロンプトキャッシュ不一致とTGボトルネックの解析
DeepSeek-V3.2をllama.cppで実行した際のベンチマークログから、デコード速度が14-15 tok/sに張り付く原因をプロンプトキャッシュ不一致とメモリ帯域の観点で分析した記録。
はじめに
DeepSeek V3.2 Speciale をローカルで回したときに、Kimi系のサービスよりかなり遅く感じる場面があった。今回のメモでは、その体感差を印象論で終わらせず、ログから PP、TG、KV cache、prompt cache、MoE実装差のどこに効いていそうかを切り分けた。
手元の観測だけでも、遅さの中心がどこにあるかはかなり明確だった。PPはそれなりに出ているのに、TGがずっと低い。しかも会話継続で効いてほしいprompt cacheが部分不一致で捨てられている。この2点が、今回の体感差の説明としてかなり強い。
背景
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と比較して「遅い」という印象があったが、原因がモデル性能なのか運用設定なのかを切り分ける必要があった。
目的
- DeepSeek-V3.2のデコード速度が14-15 tok/sに停滞する原因を特定する
- llama.cppのログから、キャッシュ制御と最適化フラグの影響を定量的に把握する
- 改善策の優先順位を整理する
実験環境
- 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 ID | PP (tok) | TG (tok) | 累積トークン | PP速度 (tok/s) | TG速度 (tok/s) | PP時間 (s) | TG時間 (s) |
|---|---|---|---|---|---|---|---|
| 0 | 2,731 | 1,024 | 3,755 | 99.75 | 14.57 | 27.4 | 70.3 |
| 1026 | 857 | 1,024 | 5,636 | 74.47 | 15.21 | 11.5 | 67.3 |
| 2051 | 311 | 982 | 6,929 | 52.92 | 14.51 | 5.9 | 67.7 |
| 3034 | 4,865 | 1,024 | 12,818 | 100.22 | 14.22 | 48.5 | 72.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は未使用。
どこが遅さを作っているのか
1. Speculative decoding(未使用)
ログにはno implementations specified for speculative decodingと出ていた。体感生成速度を引き上げる手段として speculative decodingは直球だ。特にTGが低いケースでは、この手の改善がそのままレスポンス感に効く。「まだ効かせられる余地が残っている」と言ってよい。
2. KV cacheの精度設定
llama.cpp側のログではKV self size: f16になっている。短いコンテキストではそこまで目立たなくても、長文や長生成になるほどKV cacheに伴うメモリ帯域とattentionコストが効いてくる。Kimi-K2.5ではKVキャッシュをq8_0に量子化して帯域負荷を軽減していた。DeepSeek-V3.2でもこの設定を適用すれば、TG速度の改善が期待できる。
3. MoE最適化の差
ログ上はfused_moe=1が有効だが、vLLMやサービスプロバイダのような専用カーネル最適化に比べると、llama.cppのMoE実装はまだ汎用的。エキスパートルーティング周りの実装差が速度差として現れている可能性がある。
4. キャッシュが効いてない(ログで確定)
ここは推測ではなく、ログからほぼ言い切れる部分だった。各タスクでCommon part does not match fullyが出て、その直後にkv cache rm [p0, end)が出ている。
prompt cacheが部分不一致のために破棄されていることを意味する。今回のメモでは、<think>の有無やsystem、prefixの混在が毎回ズレを作っている可能性に触れていた。先頭トークン列が固定されないなら、会話継続なのにキャッシュのうまみを捨ててしまう。
つまり「DeepSeekが遅い」のではなく、「キャッシュが効いていない状態で比較していた」公算が大きい。
考察
TG 14-15 tok/s の壁の正体
デコード速度がタスク間でほぼ一定(14-15 tok/s)なのは、メモリ帯域の物理的な限界に当たっていることを示唆する。現状のKVキャッシュはf16精度で確保されており、文脈が長くなるほどAttention計算のメモリ帯域負荷が支配的になる。
プロンプトキャッシュ不一致の影響
Kimi-K2.5での運用では、固定プレフィックス(System Prompt + ナレッジダイジェスト)を完全に一致させることで、LCPキャッシュのヒット率を高く維持していた。DeepSeek-V3.2の検証では以下の要因でキャッシュが効いていなかった:
<think>タグの有無の揺れ: Thinking Promptの有無がリクエストごとに変わり、先頭トークン列が不一致- System Promptの微妙な差異: テンプレートが固定されていなかった
- 会話履歴の管理差: Kimi-K2.5は前の文脈を再利用する設計だったが、DeepSeek側は毎回文脈を再構築していた
改善策の優先順位
| 優先度 | 施策 | 期待効果 | 実装コスト |
|---|---|---|---|
| A | プロンプトプレフィックスの固定化 | PP大幅短縮 | 低(設定変更のみ) |
| B | Speculative Decoding導入 | TG体感速度改善 | 中(ドラフトモデル選定が必要) |
| C | KVキャッシュq8_0化 | TG帯域負荷軽減 | 低(フラグ変更のみ) |
| D | 生成条件の統制 | 公平な比較の実現 | 低(テスト設計の問題) |
A. プロンプトキャッシュの完全一致を作る
まず最初にやるべきなのはここだ。ログの不一致例を見ると、<think>の有無やsystem、prefixテンプレートが揺れていて、毎回同一トークン列になっていない。サーバ側テンプレート、thinkingの有無、system文面を固定して、先頭から完全一致するよう揃える。
B. Speculative decodingを使える構成にする
次に効きやすいのがspeculative decodingだ。今回のボトルネックはTGなので、この手の改善は体感変化に直結しやすい。
C. KV cacheの型を落とす
cache-type-k/vのような指定でKV cacheの型をq8やiqに落とせることがある。長文かつ長生成のケースで帯域面の重さがじわじわ効いてくる。
D. 生成条件を揃えて比較する
出力トークン数、温度、top_p、stop、streamの有無、system文、thinkingの有無が違えば、見かけの速度差は簡単に生まれる。特にthinkingを入れると、見える出力トークン数が同じでも内部生成が増えて遅く見えることがある。
結論
同じハードウェア、同じランタイムでも、プロンプトキャッシュの運用一つでスループットが大きく変わる。最初は「DeepSeekはKimiより遅い」と思い込んでいたが、ログを丁寧に読むと設定の違いが主な要因であることがわかる。
まずはキャッシュ完全一致を作り、そのうえでspeculative decodingとKV cacheの調整を試す。この順で当てていくのが、いちばん効率のいい改善ルートだと考えている。
再現方法
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_PPとS_TGを抽出し、改善前後の差分を比較する。
補足ノウハウ
プロンプトキャッシュを効かせるための原則
- 先頭トークン列を固定する: System Prompt → 固定コンテキスト → 可変部分、の順序を厳守
- Thinkingモードは一貫させる: 有効にするなら全リクエストで有効に。リクエストごとに切り替えるとキャッシュが毎回破棄される
- 温度・top_pを揃える: 生成パラメータの差異もキャッシュヒット率に影響する場合がある
Kimi-K2.5との公平な比較をするには
- 出力トークン数、温度、top_p、stopシーケンス、stream有無を完全に一致させる
- Thinking Tokenの扱いを統一する
- 同一のハードウェア・スレッド数・KVキャッシュ設定で計測する
