EchoCache / deploy.ja.md
NagaYu's picture
EchoCache v1.0.0: docs, benchmark results and the full application source
46985e0 verified
|
Raw History Blame Contribute Delete
18.6 kB

EchoCache デプロイ手順(Hugging Face Spaces 無料枠)

所要時間 約10分。コピペで完了します。 前提: ブラウザ、git、python3。有料プラン・GPU・モデルのダウンロードは一切不要です。

English version: deploy.md


1. Space を新規作成する

  1. https://huggingface.co/new-space を開く

  2. 次のとおり入力する

    項目 値
    Owner 自分のアカウント
    Space name EchoCache(任意。以降 <user>/EchoCache と表記)
    License apache-2.0
    SDK Gradio
    Space hardware CPU basic · 2 vCPU · 16 GB · FREE
    Visibility Public または Private(どちらでも動作します)
  3. Create Space を押す。空のリポジトリが作られます。

Gradio のテンプレート選択画面が出た場合は「Blank」で構いません。どうせ全ファイルを差し替えます。


2. ファイルを配置して push する

まず書き込みトークンで一度だけログインします(write 権限のトークンを https://huggingface.co/settings/tokens で発行)。

hf auth login          # 古い環境では: huggingface-cli login

以下をそのまま実行してください(<user> だけ自分のアカウント名に置換)。

USER=<user>; SPACE=EchoCache; SRC="$(pwd)"
git clone https://huggingface.co/spaces/$USER/$SPACE ~/$SPACE-space && \
cp "$SRC/app.py" "$SRC/requirements.txt" "$SRC/deploy.md" "$SRC/deploy.ja.md" ~/$SPACE-space/ && \
cp "$SRC/README.gradio.md" ~/$SPACE-space/README.md && \
cd ~/$SPACE-space && git add app.py requirements.txt README.md deploy.md deploy.ja.md && \
git commit -m "Deploy EchoCache: semantic cache + cost observability" && git push

git clone で認証を求められたら、ユーザー名に自分のアカウント名、パスワードにアクセストークンを入力します。

push すると Space が自動でビルドされます(Building → Running、初回1〜2分)。 ビルドログは Space ページの右上 Logs から確認できます。

README.gradio.md を README.md として配置する点に注意してください(公開リポジトリの README.md は sdk: static です)。

sdk_version でビルドが失敗する場合は、README.md の sdk_version: を Space の Settings に表示されている選択肢のバージョンへ書き換えて push し直してください。それ以外は変更不要です。


3. Secrets に HF_TOKEN を設定する(任意・設定しなくても全機能が動く)

埋め込みAPIによる再ランキングを試したいときだけ設定します。

  1. Space ページ → Settings → Variables and secrets
  2. New secret → Name: HF_TOKEN / Value: 自分のトークン → Save (HUGGINGFACE_HUB_TOKEN / HUGGINGFACEHUB_API_TOKEN も同じ意味で読み取られます。 ローカル実行時にこれらをシェルに export していると、意図せず local+embedding で起動して 外部通信が発生します。UIヘッダーと Health タブの mode で必ず確認してください)
  3. Restart this Space
HF_TOKEN なし HF_TOKEN あり
類似判定 ローカル完結(既定・推奨) ローカル + 埋め込みの加重平均
主機能 すべて動作 すべて動作
失敗時 — 402/429/タイムアウトでも自動でローカルのみに戻る

無料アカウントの推論クレジットは月 $0.10 程度しかありません。設定しない運用が既定だと考えてください。 UI のヘッダーに local-only / local+embedding のどちらで動いているかが常時表示されます。

同じ画面で任意の環境変数も設定できます(よく使うもの):

PRICE_IN_PER_1K   = 0.003     # 自社の入力単価。設定しないと節約額は $0 のまま
PRICE_OUT_PER_1K  = 0.015     # 自社の出力単価
MAX_ENTRIES       = 5000      # テナント毎の上限
MAX_TOTAL_ENTRIES = 20000     # 全テナント合計の上限(メモリの実質的な歯止め)
MAX_TENANTS       = 256       # 区画数の上限。超えると最も古いテナントの区画が丸ごと消える
DEFAULT_THRESHOLD = 0.92      # 既定は保守的。Sweep を見てから動かす

4. 動作確認(UI で3分)

  1. Space を開く(https://<user>-echocache.hf.space)。ヘッダーに Mode: local-only と出ていれば正常です。

  2. Store で1件登録

    • Store タブ → tenant_id に acme、route に support
    • prompt: How do I reset my password?
    • response: Open Settings > Security > Reset password.
    • Store を押す → "stored": true と entry_id が返ります。
    • (Seed 3 demo entries ボタンで3件まとめて投入することもできます)
  3. Lookup でヒットを確認

    • Lookup タブ → tenant_id に acme
    • prompt: Hi team, how do I reset my password? Thanks in advance!
    • Lookup → "hit": true, "stage": "exact"。挨拶と結びが正規化で除去され、完全一致キーに落ちています。
    • 次に How can I reset my password? を threshold 0.80 で試すと "stage": "cosine" のヒットになります(実測の類似度は約 0.85。既定の 0.92 のままだと miss になり、best_similarity に 0.85 が返ります。この数字を見てから閾値を決めるのが正しい手順です)。
    • 誤ヒット防止の確認: Why do I need to reset my password? を threshold 0.70 で実行 → "reason": "guard_rejected"、guard_reasons に question_type_mismatch:q={why},c={how}。類似度が高くてもヒットしないことが確認できます。
    • テナント分離の確認: tenant_id を globex に変えて同じ質問 → "reason": "empty_index"(他テナントの登録内容は一切見えません)。
  4. Dashboard で記録を確認

    • Dashboard タブ → Refresh
    • requests / hits / misses / hit rate / saved tokens / estimated saving の1行サマリ、ヒット率の時系列、テナント別の累積節約、p90レイテンシ、再利用上位、索引の健全性が出ます。
    • Export events CSV でイベントログを持ち出せます(既定ではプロンプト本文は記録されません)。
  5. Sweep で閾値を決める

    • Sweep タブ → Download a sample log でサンプルCSVを取得 → そのまま request log CSV にアップロード
    • 単価(price in/out)を入れて Run sweep
    • 「この閾値なら何%再利用でき、いくら浮くか」が文章と表とグラフで出ます。MismatchGuard on/off の2本が並ぶので、保守的な設定でいくら浮くかがそのまま稟議資料になります。
  6. 実際のAPIパスを取得

    • API Docs タブ: 稼働中のサーバーから組み立てた実パス(curl と gradio_client)が表示されます。
    • 画面最下部の Use via API リンク(Gradio 自身が生成)が常に正典です。

CLI で確認したい場合

SPACE="https://<user>-echocache.hf.space"

EVENT_ID=$(curl -s -X POST "$SPACE/gradio_api/call/store" -H "Content-Type: application/json" \
  -d '{"data": ["acme","How do I reset my password?","Open Settings > Security > Reset password.","support",86400]}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['event_id'])")
curl -s -N "$SPACE/gradio_api/call/store/$EVENT_ID"

EVENT_ID=$(curl -s -X POST "$SPACE/gradio_api/call/lookup" -H "Content-Type: application/json" \
  -d '{"data": ["acme","Hi team, how do I reset my password? Thanks!",0.92,"support"]}' \
  | python3 -c "import sys,json; print(json.load(sys.stdin)['event_id'])")
curl -s -N "$SPACE/gradio_api/call/lookup/$EVENT_ID"
pip install gradio_client
python3 - <<'PY'
from gradio_client import Client
c = Client("<user>/EchoCache")
print(c.predict("acme", "how do i reset my password", 0.92, "support", api_name="/lookup"))
print(c.predict(api_name="/health"))
PY

5. 運用の型

5-1. スリープ前にエクスポート、復帰後にインポート

無料枠はディスク非永続・48時間無操作でスリープ(スリープ時間は変更不可)。索引はメモリ上にしかないので、明示的に持ち出すのが運用の型です。

  1. Backup タブ → include response bodies にチェック → Export index
  2. index.json をダウンロードして手元(または S3 / Drive / リポジトリ外の安全な場所)に保管
  3. スリープや再起動のあと、Space を開く → Backup タブ → index.json をアップロード → Import index
  4. 結果の imported / skipped_no_response / expired / rejected_by_safety を確認
  • インポート時はベクトルを再計算するので、VECTOR_DIM を変えていても取り込めます。
  • インポート時に安全検査が再実行されます(skip_safety は原則オフのまま)。
  • 応答本文を含めずにエクスポートしたファイルは、分析用です。取り込んでもキャッシュとしては使えません(skipped_no_response に計上されます)。

日次でバックアップしたいなら、手元から叩くだけです:

python3 - <<'PY'
from gradio_client import Client
import datetime
c = Client("<user>/EchoCache")
path, receipt = c.predict(True, "", api_name="/export_index")
dst = "echocache-%s.json" % datetime.date.today()
open(dst, "w").write(open(path).read())
print(dst, receipt)
PY

5-2. 閾値は Sweep の結果を見てから決める

  • 既定の 0.92 は保守的です。この値では主に「完全一致+表記ゆれ(挨拶・敬語・句読点・大文字小文字)」が再利用されます。
  • 言い回しが変わる本当の言い換えは、文字 n-gram では 0.80〜0.90 に落ちるのが普通です。
  • 本番のログを1日分 CSV(tenant_id, prompt, response, route)にして Sweep にかけ、再利用率・節約額・境界ヒット件数・Guard 棄却件数を見てから決めてください。勘で下げないこと。
  • 下げる前に、まず Audit タブで Guard の棄却理由を見ます。「棄却が多い=閾値が高すぎる」とは限らず、「そもそも意味が違う質問が多い」だけのこともあります。
  • 公開ベンチマーク(echocache-guard-benchmark)では 閾値0.80 + Guard有効が正答率99.2%・再利用率100%・誤再利用1/90でした。ただしこれは合成データ1セットの結果です。自分のログで測ってください。

5-3. 定期的に見る場所

頻度 見るもの 判断
毎日 Dashboard の hit rate / saved cost 期待どおり伸びているか
毎日 Audit の Guard rejections 棄却理由の傾向。誤ヒットの予兆
週次 Dashboard の索引健全性(evicted / expired) 追い出しが多いなら TTL か上限を見直す
週次 Sweep を最新ログで再実行 閾値の再確認
変更時 Health の mode local-only から意図せず変わっていないか

6. よくある失敗と対処

メモリを食う / Space が落ちる

索引メモリはほぼ エントリ数 × VECTOR_DIM × 4 バイトで、**MAX_ENTRIES はテナント毎**です。既定では 1テナント満杯で約 80 MB、全体は MAX_TOTAL_ENTRIES=20000 で約 330 MB に抑えられています。

対処(Settings → Variables and secrets、上から順に効きます):

MAX_TOTAL_ENTRIES = 8000      # 全体の歯止め。まずここを下げる
MAX_ENTRIES       = 2000      # テナント毎の上限
MAX_TEXT_CHARS    = 4000      # 1エントリの保存文字数
VECTOR_DIM        = 2048      # メモリ半減。精度低下はわずか
EVENT_BUFFER      = 2000      # イベントのリングバッファ

Health タブの approx_memory_mb と vector_matrix_mb_per_full_tenant で実測値を確認できます。 VECTOR_DIM を変えたら、既存のエクスポートは再インポートで作り直されます(ベクトルは再計算されるので互換性の心配は不要です)。

保存したはずのテナントが空になっている

テナント数が MAX_TENANTS(既定256)に達すると、新しいテナントの初回書き込み時に、 最も長く使われていないテナントの区画が丸ごと破棄されます(削除のみで、テナント間でデータが 混ざることはありません)。実際のテナント数より十分大きい値を設定してください。 全体のエントリ数が MAX_TOTAL_ENTRIES に達した場合は、最大の区画から1件ずつ古い順に追い出されます。

ヒットしない

下げる前に順番に確認してください。

  1. Lookup の結果の reason を見る
    • empty_index … そのテナントに1件も入っていない(tenant_id の打ち間違いが最多)
    • no_candidates … SimHash の帯に候補なし。文面が大きく違う
    • below_threshold … best_similarity が出ます。この数字を見てから閾値を判断する
    • guard_rejected … 意味が違うと判定された。次を見る
  2. Audit タブで Guard の棄却理由を見る(numeric_mismatch / negation_mismatch / proper_noun_mismatch / temporal_mismatch / question_type_mismatch)。理由が妥当なら、それは正しい miss です。閾値を下げても直りません。
  3. 正規化の効きを確認する: Store したプロンプトと Lookup のプロンプトが、挨拶・敬語・記号を除いて本当に同じか。cached_prompt_preview が返ってくるので見比べられます。
  4. それでも取りこぼしているなら、Sweep で 0.86 → 0.82 のように段階的に下げ、borderline_hits と guard_blocks の増え方を見る。
  5. 特定の検査が業務に合わないと確信できた場合のみ、GUARD_DISABLE=proper_noun のように個別に外す(全部外すのは非推奨)。

誤ヒットが出た(最優先で対処)

  1. 閾値を上げる(例: 0.92 → 0.95)。即効性があります。
  2. Audit タブ、または Dashboard → most reused entries で該当の entry_id を特定する。
  3. Audit タブ下部の Invalidate entry に tenant_id と entry_id(先頭16文字で十分)を入れて失効させる。
    • ルート単位なら key_prefix にルート名(例: support)
    • テナント全体なら *
  4. 同じ型の誤ヒットが再発するなら、その事例を CSV に追加して Sweep を回し直し、その型を弾ける閾値を採用する。
  5. 意味の差が Guard の5種(否定・数量・固有名詞・時制・疑問種別)で説明できない型なら、その質問カテゴリはキャッシュ対象から外す(route を分けて、その route を定期的に invalidate する運用が簡単です)。

埋め込みAPIが 402 / 429 になる

  • 402 Payment Required … 無料枠の推論クレジット(月 $0.10 程度)が尽きています。
  • 429 Too Many Requests … レート制限。

HF_TOKEN を消したのに local+embedding のままなら、HUGGINGFACE_HUB_TOKEN / HUGGINGFACEHUB_API_TOKEN が残っていないか確認してください(同じ意味で読まれます)。

どちらでも主機能は止まりません。 例外は握りつぶされ、ローカルベクトルのみで判定が続きます。連続失敗が閾値を超えるとサーキットブレーカーが開き、EMBED_COOLDOWN_SEC(既定600秒)は呼び出しを止めて無駄な課金を防ぎます。 Health タブの embedding.last_error と cooldown_remaining_sec で状態を確認できます。恒久的に止めるなら Secrets から HF_TOKEN を削除して Restart してください(local-only に戻ります)。

Space が 48時間で寝てしまう / 起きたら索引が空

無料枠の仕様です(スリープ時間は変更不可、ディスクは非永続)。5-1 の運用(エクスポート→インポート)で引き継いでください。 常時起動が必要なら有料ハードウェアか、自前ホストへ同じ app.py を置くことになります(python app.py で動きます)。

/store が stored: false で返る

安全検査で弾かれています。reason を見てください。

reason 意味
credit_card_luhn Luhn 検証を通った数字列(カード番号の可能性)。注文番号などは通過します
credential_prefix sk- / ghp_ / AKIA / xoxb- / AIza / hf_ などの資格情報
jwt_structure JWT の三部構造
high_entropy_secret 長い高エントロピー英数字列(識別子らしい構造のものは除外されます)
contact_pii_excess メールアドレスや電話番号が規定数を超過
private_key_block PEM 秘密鍵ブロック

これは正常動作です。 秘密情報をキャッシュに入れないための最後の防波堤なので、閾値を緩める前に、そもそもその本文をキャッシュに入れてよいのかを検討してください。

API のパスが見つからない / 404

Gradio のバージョンでパスが変わります(Gradio 5・6 は /gradio_api/call/<name>、4 は /call/<name>)。 ハードコードせず、API Docs タブか画面最下部の Use via API リンクから取得してください。gradio_client を使えばパスの違いは吸収されます。


付録: ローカルで動かす

pip install -r requirements.txt
python3 app.py          # http://localhost:7860

Space と同じ挙動です。環境変数はそのままシェルで渡せます:

PRICE_IN_PER_1K=0.003 PRICE_OUT_PER_1K=0.015 MAX_ENTRIES=1000 python3 app.py