DeepSeek Harnessのキャッシュヒット率はOllamaで出ない

沈黙したままの計器と振り切れた計器が並ぶ図

「DeepSeek Harnessでキャッシュヒット98.2%らしい。自分のはどうなんだろう」

そう思って画面を探しても、その数字は見つかりません。設定に問題があるわけではなく、ollama launch dsh の構成ではそもそも表示自体が存在しないからです。

TL;DR

  • Ollamaは cached_tokens を返さないため、DeepSeek Harnessの cache N% 表示は「0%」ではなく丸ごと消えます
  • 実測ではキャッシュが効いて 31.983秒 → 0.042秒(761倍)。それでも prompt_eval_count は 13,220 のまま1つも動きません
  • 測りたいなら vLLM の --enable-prompt-tokens-details か SGLang の --enable-cache-report が要ります。どちらも既定はオフです

先日、OllamaでDeepSeek Harnessを起動する方法という記事を書きました。今回はその続きで、起動したあとに待っている落とし穴についてです。

検証環境: Apple M5 / RAM 16GB / macOS 26.6.2 (25G83) / Ollama 0.33.2 / qwen3:4b(※2026年8月時点)

最初に白状しておくと、私はこの現象を自分のミスだと思っていました。起動した dsh の画面を上から下まで確認しても、それらしい表示が見当たらない。設定を変えれば出てくるはずだと思い込み、しばらく settings.yaml をいじり続けました。表示されないのが正常だと気づいたのは、ソースコードを読んでからです。


目次

prompt_eval_count を見ている限り、キャッシュは一生見えない

プロンプトキャッシュが効いているかを確かめるとき、最初に見たくなるのはトークン数です。キャッシュが効いたら入力トークンが減るはずだ、と考えるのが自然でしょう。

その直感は、Ollamaでは通用しません。

同じシステムプロンプト(21,600文字・13,220トークン)を投げて2回計測しました。

ケースprompt_eval_countprompt_eval_duration
A: 初回(cold)13,22031.983秒
B: 2回目(同一prefix)13,2200.042秒

Bは761倍速く終わっています。キャッシュは明確に効いています。それなのに prompt_eval_count13,220 のまま1つも変わりません

Ollamaが返しているのは「実際に計算したトークン数」ではなく「論理的なプロンプト長」だからです。キャッシュから読んだ分も律儀に数えて返してくるため、トークンカウンタを眺めている限り、ヒットもミスも同じ顔をしています


唯一の信号は時間だった。31.983秒が0.042秒になる

トークン数が動かないなら、何を見ればいいのでしょうか。

答えは prompt_eval_duration です。前処理にかかった時間だけが、キャッシュの有無で桁違いに動きます。

計測に使ったスクリプトは以下です。Ollamaのネイティブ /api/chat を叩いて、応答に含まれる統計値を並べているだけです。

import json, urllib.request, time

URL = "http://localhost:11434/api/chat"

def call(tag, sysmsg, user):
    body = json.dumps({
        "model": "qwen3:4b",
        "messages": [
            {"role": "system", "content": sysmsg},
            {"role": "user", "content": user},
        ],
        "stream": False,
        "options": {"num_predict": 3},
    }).encode()
    req = urllib.request.Request(URL, data=body,
                                 headers={"Content-Type": "application/json"})
    d = json.load(urllib.request.urlopen(req, timeout=600))
    pec = d.get("prompt_eval_count")
    ped = d.get("prompt_eval_duration", 0) / 1e9
    print(f"[{tag}] count={pec} duration={ped:.3f}s")

BIG = "あなたは製造業向けの技術文書レビュアーです。" * 400

call("warmup", "短い前置き", "1")          # モデルロードを分離する
call("A: 初回", BIG, "OKとだけ答えて")
call("B: 2回目", BIG, "OKとだけ答えて")

最初の warmup は必ず入れてください。筆者は最初これを省いて計測し、初回だけ妙に遅い数字が出て首をかしげました。モデルのロード時間が prompt_eval_duration に混ざっていただけです。

出力は次のようになりました。

[warmup] count=21 duration=0.272s
[A: 初回] count=13220 duration=31.983s
[B: 2回目] count=13220 duration=0.042s

Ollama環境でキャッシュ効率を語りたいなら、prompt_eval_duration の比を見るしかありません。これが唯一の代替指標です。


システムプロンプトの先頭に1文字足すと、13,220トークンが全部無駄になる

キャッシュが効く条件も測っておきました。Ollamaが使っているのは prefix cache(接頭辞キャッシュ)で、これには厳しい性質があります。

同じスクリプトでケースを増やした結果です。

ケースprompt_eval_countprompt_eval_duration
A: 初回(cold)13,22031.983秒
B: 2回目(同一prefix)13,2200.042秒
C: prefix同一・末尾のみ差替13,2210.217秒
D: prefix先頭に1文字追加13,22131.407秒
E: Dのprefixを再利用13,2210.042秒

注目すべきはDです。システムプロンプトの先頭に “X” を1文字足しただけで31.4秒に戻りました。残りの13,220トークンは一言一句そのままなのに、1トークンも再利用されません。接頭辞が一致しなくなった時点で、その先は全部やり直しになるからです。

一方でCのように末尾だけ変える分には0.217秒で済みます。会話の続きは安く、前置きの書き換えは高い、ということです。

ここから「98.2%」の正体が見えます。 あれだけ高い数字が出るのは、セッション中にプロンプトの先頭を一切動かさない設計になっているからです。逆に、システムプロンプトの冒頭に現在時刻やリクエストIDを差し込む実装は、キャッシュヒット率がゼロに落ちます。


98.2%が出るのは、DeepSeekがcache_controlを捨てたから

では、報告された98.2%はどこから出た数字なのでしょうか。DeepSeek Harnessのソースコード(deepseek-ai/deepseek-harness、★203,885、最終push 2026-08-27)に、設計思想まで含めて答えがありました。

DeepSeekのAPIは、応答の usage に次のフィールドを載せます。

prompt_tokens = prompt_cache_hit_tokens + prompt_cache_miss_tokens

このうち prompt_cache_hit_tokens を拾って表示しているのが98.2%です。

面白いのはここからで、設計ノートにAnthropic方式を意図的に捨てた経緯が残っていました。

DeepSeek prompt caching is automatic, so the adapters map prompt_cache_hit_tokens OUT of responses without ever sending a hint IN. This was Anthropic-style cache_control surface with no provider that could honor it.
(出典: .agents/notes/archived/simplification/2026-07-04-prune-producerless-vocabulary-variants.md

Claude APIでは cache_control を付けて「ここをキャッシュしろ」と指示します。DeepSeek Harnessも当初は同じ口を持っていましたが、実装できるプロバイダが1つも無かったため削除されました。

DeepSeekのキャッシュは全自動です。指示を送る必要がない代わりに、結果を受け取ることしかできません。この非対称性があとで効いてきます。


だから ollama launch dsh の画面に cache N% は出ない

ここまでの2つ、「Ollamaは cached_tokens を返さない」と「DeepSeek Harnessは受け取ることしかできない」を突き合わせると、結論が出ます。

まず ollama launch dsh が何を設定しているかを確認しました。

ollama launch dsh --model qwen3-coder:30b

--model を省くとヘッドレス環境では落ちます。実際に出たエラーの原文です。

Error: model selection requires an interactive terminal; use --model to run in headless mode

起動すると dsh web: http://127.0.0.1:3080 が立ち上がります。このときOllamaが生成する ~/.ollama/launch/dsh/settings.yaml が決定的でした。

agent-default-model:
    provider: ollama
    model: qwen3-coder:30b
llm-pi-ai:
    providers:
        ollama:
            api: openai-completions
            baseURL: http://127.0.0.1:11434/v1

api: openai-completions です。つまりDeepSeek専用アダプタは使われません。汎用のOpenAI互換アダプタがOllamaの /v1 を叩きます。そのエンドポイントが返す usage を実測しました。

{"prompt_tokens": 6609, "completion_tokens": 5, "total_tokens": 6614}

3フィールドだけで、prompt_tokens_details.cached_tokens はありません。受け取る側のコードはこうです。

// packages/llm/llm-pi-ai/src/stream.ts
...usage.cacheRead > 0 ? { cacheReadTokens: usage.cacheRead } : {}

cacheRead が立たなければ、cacheReadTokens というキー自体が出力されません。値が0になるのではなく、存在しなくなります。

そして表示側の設計ノートが、最後の一押しでした。

cacheHitRate(totals) is round(cacheRead / (input + cacheRead + cacheWrite) * 100), and undefined before any input is billed. FooterComponent omits the whole cache N% segment while the rate is undefined, so an empty session shows no meaningless zero.
(出典: .agents/notes/archived/feature/2026-07-21-tui-footer-cache-hit-rate.md

cache N% のセグメントが丸ごと省略されます。 「0%」という無意味な表示を避けるための丁寧な設計ですが、Ollama利用者から見ると探しても見つからない項目になります。

もう1つ副作用があります。使用量の計算式はこうです。

inputTokens: usage.prompt_tokens - (cacheRead ?? 0)

cacheReadundefined なら引かれる分は0です。実際には31.983秒が0.042秒になるほど再利用が起きているのに、入力トークンは満額で計上されます。Ollamaで使う限り、DeepSeek Harnessの消費量表示は実態より膨らんでいると考えたほうが安全です。


Ollama cloud では、時間の信号すら消える

ローカルモデルなら prompt_eval_duration という逃げ道がありました。クラウドモデルではどうでしょうか。

deepseek-v4-pro:cloud で同じ計測をしたところ、返ってくるフィールドが減っていました。ネイティブ /api/chat の応答キーを比較します。

ローカル llama3.2:1bクラウド deepseek-v4-pro:cloud
応答キー数107
欠落しているキーprompt_eval_duration / eval_duration / load_duration

時間の統計値が3つとも返ってきません。

トークン数でも見えず、時間も返ってこない。クラウドモデルを指定した構成では、キャッシュ効率を測る手段が1つも残りません。キャッシュの話を諦めるか、次の節の方法に移るかの二択です。


測りたいなら vLLM か SGLang を、フラグ付きで起動する

キャッシュヒット率を数字で見たい場合、選択肢は「cached_tokens を返してくれる推論サーバーを使う」に絞られます。主要な2つを調べたところ、どちらも対応していました。

ただし両方とも既定ではオフです。フラグを明示的に付ける必要があります。

ランタイムフラグ既定値定義箇所
vLLM--enable-prompt-tokens-detailsfalserust/src/cmd/src/cli.rs
SGLang--enable-cache-reportFalsepython/sglang/srt/server_args.py

SGLangの説明文が用途をそのまま書いています。

Return number of cached tokens in usage.prompt_tokens_details for each openai request.

vLLM側も「キャッシュされたトークンがあるとき prompt_tokens_detailsusage に含める」という同趣旨の記述です。

このフラグを付け忘れると、サーバー内部でキャッシュが効いていても usage には出ません。DeepSeek Harnessは受け取れず、cache N% は消えたままになります。

乗り換えるべきかは、扱うトークン量次第です。 Ollamaの価値は導入の手軽さで、ollama launch dsh の1コマンドはその象徴です。vLLMとSGLangは起動オプションを自分で組む前提のツールで、キャッシュ効率という1指標のために払うコストとしては小さくありません。

日常的にエージェントを回してコストの内訳を数字で管理したいなら移る価値があります。手元で試すだけなら prompt_eval_duration の比で十分です。


まとめ

DeepSeek Harnessのキャッシュヒット率98.2%を自分でも測ろうとしたところ、そもそも測る手段が無いことがわかりました。

要点の再掲

  • cache N% は0%ではなく表示自体が消えます(Ollamaが cached_tokens を返さないため)
  • 動くのは prompt_eval_duration だけ。実測は 31.983秒 → 0.042秒
  • prefix cacheなので、システムプロンプトの先頭を1文字変えるだけで13,220トークン分がやり直しになります
  • クラウドモデルでは prompt_eval_duration すら返らず、測定手段がなくなります
  • 数字で見たいなら vLLM --enable-prompt-tokens-details か SGLang --enable-cache-report を付けて起動してください

他者の数値を追いかけて、結局追いかけられなかった、という結果です。ただ、測れない理由のほうが、98.2%という数字そのものより実用的な知見だったと感じています。

自分の環境でキャッシュが正しく機能しているか確認したい方は、この記事のスクリプトをそのまま実行してみてください。2回目の実行が0.1秒を切っていれば、あなたの環境でもキャッシュは有効です。

セキュリティ上の注意: dsh プロセスの環境変数には、各種APIキーが平文で入ります。ps eww で他プロセスから読める状態になるため、ターミナルのスクリーンショットを共有する際は必ずマスクしてください。

関連記事
OllamaでDeepSeek Harnessを起動する方法 — ollama launch dsh 完全ガイド

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA


目次