第42回 推論コストとスケール設計 — Pythonで実装するコスト最適化・キャッシュ・バッチ処理・SLA運用

はじめに — 現場のつまずきに寄り添う

プロダクトが成長すると、突如「推論コスト」と「スケールの不確実性」が重くのしかかります。どこを計測し、どの施策を優先し、いつロールアウトするか。机上の理論だけではなく、チームで合意して実装できる手順が必要です。本記事では、Pythonでそのまま使える雛形とチェックリストを提示します。まずは落ち着いて「何を測るか」から始めましょう。

何を計測するか(現状把握)

まず計測対象を定め、ログを集めて単位当たりコストに換算します。重要指標を下の表にまとめます。

指標 計測方法(例) 単位 用途
リクエスト数 APIゲートウェイログ/アプリログ 件/月、件/秒 全体負荷と課金粒度判断
トークン数(入力・出力) レスポンス解析でトークン数を算出 トークン/件 モデル利用コスト推定
レイテンシ アプリ計測(p50/p95/p99) ms SLA評価・パス分岐設計
エラー率 HTTP 5xx / 4xx 集計 % 信頼性評価・エラーバジェット
クラウド請求メトリクス 請求API/請求CSV 通貨単位/月 予算管理・アラート

まずは下のシンプルなPythonスクリプトでログ(JSON行)を集計し、単価を掛けて月次試算を出します。

import json
from collections import defaultdict

UNIT_PRICE_PER_TOKEN = 0.00002  # 仮の単価

def aggregate_log(path):
    agg = defaultdict(lambda: 0)
    with open(path) as f:
        for line in f:
            r = json.loads(line)
            agg['requests'] += 1
            agg['tokens'] += r.get('tokens', 0)
            agg['errors'] += 1 if r.get('status', 200) >= 500 else 0
    return agg

if __name__ == '__main__':
    agg = aggregate_log('access.log')
    cost = agg['tokens'] * UNIT_PRICE_PER_TOKEN
    print(f"requests: {agg['requests']}, tokens: {agg['tokens']}, est_cost: {cost:.2f}")

短期で効くコスト削減施策:キャッシュ戦略

キャッシュは最も即効性が高い手段です。レスポンス全体キャッシュ、部分(属性)キャッシュ、TTL設計がポイントです。

戦略 説明 導入目安
レスポンス全体キャッシュ 同一リクエストであればモデル呼び出しを省略 定型回答が多いAPIに有効
部分キャッシュ 事前計算できる部分(テンプレートやメタデータ)をキャッシュ 動的部分が小さいとき
TTLと破棄ポリシー ビジネス要件に合わせて短め/長めを使い分ける 誤キャッシュによる古い応答の混乱を回避

Redisを使った簡単なキャッシュ例(redis-py)。キー設計とTTLに注意してください。

import redis
import json

r = redis.Redis()

def cache_key(user_id, prompt_hash):
    return f"resp:{user_id}:{prompt_hash}"

def get_cached(key):
    v = r.get(key)
    return json.loads(v) if v else None

def set_cached(key, value, ttl=3600):
    r.set(key, json.dumps(value), ex=ttl)

# 使い方の流れ
k = cache_key('user123', 'hash_of_prompt')
resp = get_cached(k)
if resp is None:
    resp = call_model_api()  # モデル呼び出し
    set_cached(k, resp, ttl=600)
ユースケース キー例 TTL 破棄ポリシー
ユーザーの同一問い合わせ resp:{user_id}:{prompt_hash} 10分〜1時間 ユーザーが編集したら削除
一般FAQ faq:{question_hash} 24時間〜7日 コンテンツ更新時に全削除

スループット最適化:バッチ処理と並列化

個々のリクエストをまとめるバッチはAPIコスト単価の低減に直結します。バッチサイズはモデルとレイテンシ要件で調整します。

パターン 利点 注意点
同期→バッチ変換 実装が単純、リクエストをまとめてコール 遅延が増える(バッチウィンドウの設定)
asyncioによる非同期バッチ 高スループット/柔軟なタイムアウト制御 実装コストが上がる
ワーカー+キュー(例:Celery) 耐障害性・スケーラビリティに優れる 運用・監視が必要

簡単な同期バッチの例(疑似コード):

def process_requests_sync(queue, batch_size=8, window_s=1.0):
    batch = []
    start = time.time()
    while True:
        req = queue.get()
        batch.append(req)
        if len(batch) >= batch_size or (time.time() - start) >= window_s:
            responses = call_model_batch(batch)
            for r, req in zip(responses, batch):
                req.reply(r)
            batch = []
            start = time.time()

asyncioを使った例(簡易):

import asyncio

async def batcher(in_q, out_q, batch_size=8, window_s=0.5):
    while True:
        batch = []
        try:
            req = await asyncio.wait_for(in_q.get(), timeout=window_s)
            batch.append(req)
        except asyncio.TimeoutError:
            pass
        while len(batch) < batch_size:
            try:
                req = in_q.get_nowait()
                batch.append(req)
            except asyncio.QueueEmpty:
                break
        if batch:
            res = await call_model_batch_async(batch)
            for r, req in zip(res, batch):
                await out_q.put((req, r))

ベンチマークの目安:

バッチサイズ 期待効果 測定値
1 最低レイテンシだがコスト高 p99レイテンシ短、コスト/tx高
4〜16 コスト効率が向上する領域(モデル依存) バッチごとの総コスト/tx低下
>32 レイテンシ悪化。スループット重視のバッチ処理に限定 キュー滞留時間増

レイテンシ vs コストの設計(低遅延経路と低コスト経路の二重化)

重要なパターンは、低遅延が必要なリクエストを優先する経路と、低コストで処理するバッチ経路を分ける設計です。サンプリングで高コストモデルを限定的に使う方法も有効です。

目的 設計例 注意点
低レイテンシ 優先度キュー→即時モデル呼び出し コストが高くなりやすい
コスト削減 低優先度はバッチ経路へルーティング 応答遅延の許容範囲を明確化

簡易な回路遮断(circuit breaker)の実装例:

import time

class CircuitBreaker:
    def __init__(self, fail_threshold=5, reset_timeout=60):
        self.fail_threshold = fail_threshold
        self.reset_timeout = reset_timeout
        self.fail_count = 0
        self.opened_at = None

    def call(self, func, *args, **kwargs):
        if self.opened_at and (time.time() - self.opened_at) < self.reset_timeout:
            raise RuntimeError('circuit open')
        try:
            res = func(*args, **kwargs)
            self.fail_count = 0
            return res
        except Exception:
            self.fail_count += 1
            if self.fail_count >= self.fail_threshold:
                self.opened_at = time.time()
            raise

SLA / SLI の実務設計とコストアラート

ビジネス側と合意すべき指標と、アラートの作り方をまとめます。

指標 推奨閾値例 用途
99p レイテンシ < 1.5秒(対外API)/<300ms(社内UI) 顧客体験の定量化
Error budget 月間許容エラー率 0.1% など 可用性合意と運用判断
Cost-per-transaction 目標値を設定(例:$0.02/tx) コスト運用のKPI

Prometheus / Grafana での可視化や、クラウド請求APIの定期取得でコストアラートを作ります。以下は請求を集計してSlack通知するジョブのテンプレートです(疑似コード)。

import requests

def compute_cost_per_tx(total_cost, total_requests):
    return total_cost / total_requests if total_requests else float('inf')

def notify_slack(webhook, text):
    requests.post(webhook, json={'text': text})

# スケジュールジョブ例
if __name__ == '__main__':
    total_cost = get_cloud_billing_monthly()  # 実装はクラウドAPIに合わせる
    total_requests = get_metric('requests')
    cpt = compute_cost_per_tx(total_cost, total_requests)
    if cpt > TARGET_CPT:
        notify_slack(SLACK_WEBHOOK, f'Cost per tx exceeded: {cpt:.4f}')

テストと検証の手順

  • ローカルで負荷プロファイルを作成(スモーク→ピーク)
  • ステージングでA/B検証(コストと品質の比較)
  • 段階的ローリング(まず一部トラフィック→段階的に拡大)
  • ロールバック基準を運用手順として定義(SLO超過やエラー率増)
ステップ 目的 検証項目
ローカル負荷試験 基本性能確認 p95/p99, コスト/tx, メモリ・CPU
ステージングA/B 実トラフィック近似で比較 品質劣化・ユーザ影響の有無
本番段階導入 小ロットで実運用確認 アラート監視、ロールバック可否

現場でよくある落とし穴とチェックリスト

  • ハードコーディングされた単価やエンドポイント(設定化していない)
  • キャッシュキー不整合による意図せぬキャッシュミス
  • バッチ遅延が業務フローに与える影響を見落とす
  • 請求データの遅延(クラウド請求は通知遅延あり)を考慮していない
チェック項目 アクション
設定の分離 単価、閾値、TTLはコンフィグに切り出す
キャッシュ整合性 編集時のインバリデーションを実装
コストアラート 月次だけでなく週次/日次の監視と通知
ロールアウト手順 A/B → カナリア → 全面展開の手順書化

まとめ

推論コストとスケール設計は、測ることから始めて小さな施策を積み重ねることが重要です。まずはログ収集と単価換算の自動化、次にキャッシュとバッチで即効性を出し、最後にレイテンシとコストのトレードオフを明文化してSLAへ落とし込みます。この記事のポイントを簡潔なチェックリストにまとめます。

  • まず計測:リクエスト数 / トークン数 / レイテンシ / エラー率 / 請求
  • 短期改善:レスポンス全体/部分キャッシュ(Redis)を導入
  • 中期改善:バッチ化と並列化でコスト/txを下げる(ベンチマーク必須)
  • 運用設計:レイテンシ優先経路と低コスト経路を分離、回路遮断を導入
  • SLA設計:99p、error budget、cost-per-transactionを合意し監視する
  • テスト:ローカル→ステージング→本番の段階的検証とロールバック基準を用意

本記事で示したコードは雛形です。実際の環境(モデル種別、クラウドプロバイダ、データ特性)に合わせて閾値やパラメータを調整してください。次回は「モデル選択とコスト精緻化(複数モデルの運用)」について取り上げます。