LLMの本番運用で多くの企業が直面するのが、想像を超えた推論コストです。推論コストはキャッシュ、バッチ処理、量子化、蒸留、モデルルーティングという5つの技術を組み合わせることで50〜80%削減できる余地があり、しかもそのほとんどは既存の実装を壊さずに導入可能です。本記事では、それぞれのテクニックの仕組みと実装例、効果の目安を体系的に整理してお届けします。
LLM推論コストの構造を理解する
推論コストの主な構成要素は、GPU時間、メモリ帯域、ネットワーク転送、そしてアイドル時の固定費です。特にGPU時間はトークン生成ごとに累積し、モデルサイズ、コンテキスト長、同時リクエスト数の3要素で線形〜非線形に増大します。最適化を始める前に「どこにコストが集中しているか」を可視化することが、効果的な施策選定の第一歩です。
【推論コストの構成要素と最適化ポイント】
推論コスト
├── GPU時間 --> バッチ処理・量子化で削減
│ ├── モデルサイズ依存
│ └── トークン生成長依存
├── メモリ帯域 --> 量子化で圧縮
│ └── コンテキスト長依存
├── ネットワーク --> キャッシュで削減
│ └── リクエスト頻度依存
└── アイドル費用 --> オートスケール・モデルルーティングで削減
└── 稼働率依存
※削減効果が最も大きいのは「そもそもLLMを呼ばない」
キャッシュ戦略と、軽量モデルへのルーティング。
プロンプトキャッシュとセマンティックキャッシュ
キャッシュには2種類があります。プロンプトキャッシュは、システムプロンプトやRAGコンテキストのような「変わらない先頭部分」を再利用する仕組みで、多くの商用APIが標準対応しています。セマンティックキャッシュは、完全一致ではなく「意味的に近いクエリ」の応答を再利用する仕組みで、エンベディング類似度で判定します。FAQ系アプリでは後者が劇的な効果を発揮します。
import redis, json, numpy as np
from openai import OpenAI
client = OpenAI()
r = redis.Redis(decode_responses=True)
def embed(text: str) -> list:
return client.embeddings.create(
input=text, model="text-embedding-3-small"
).data[0].embedding
def semantic_cache_lookup(query: str, threshold: float = 0.93):
q_vec = np.array(embed(query))
for key in r.keys("cache:*"):
entry = json.loads(r.get(key))
sim = np.dot(q_vec, entry["vec"]) / (
np.linalg.norm(q_vec) * np.linalg.norm(entry["vec"]))
if sim >= threshold:
return entry["answer"]
return None
def semantic_cache_store(query: str, answer: str):
r.set(f"cache:{hash(query)}",
json.dumps({"vec": embed(query), "answer": answer}))
バッチ処理とスループット最適化
バッチ推論は、複数のリクエストをまとめて処理することでGPUの並列性を最大限活用し、スループットを数倍に引き上げる手法です。リアルタイム性を要さないバックエンド処理(文書分類、要約パイプライン、夜間バッチ等)と相性が良く、商用APIのバッチAPIも約50%の割引が適用されます。
自社運用では、vLLMの「continuous batching」と呼ばれる動的バッチングが事実上の標準になっています。リクエストの到着タイミングに応じて動的にバッチを組み替えるため、レイテンシとスループットのバランスが取れるのが特徴です。
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Meta-Llama-3-8B-Instruct",
max_model_len=4096,
gpu_memory_utilization=0.9,
enable_prefix_caching=True,
)
params = SamplingParams(temperature=0.2, max_tokens=512)
prompts = [f"要約してください: {doc}" for doc in documents]
outputs = llm.generate(prompts, params) # continuous batching自動
量子化(Quantization)による省メモリ化
量子化は、モデルの重みを低ビット(FP16→INT8→INT4)に圧縮することで、GPUメモリ使用量と推論速度を改善する手法です。FP16からINT8への量子化では精度低下はほぼ無視できる一方、メモリ使用量は半分になります。INT4まで下げると若干の精度低下が見られますが、多くの実用タスクでは許容範囲内です。
主要な量子化方式はGPTQ(事後学習量子化で高精度)、AWQ(活性化対応量子化で速度重視)、GGUF(llama.cpp互換でCPU実行もOK)の3つです。用途に応じて選択します。
| 方式 | ビット幅 | 精度低下 | 速度改善 | メモリ削減率 | 対応ツール | 適した用途 |
|---|---|---|---|---|---|---|
| FP16 | 16bit | なし | 基準 | 基準 | 全般 | デフォルト |
| INT8(GPTQ) | 8bit | ほぼなし | 約1.5倍 | 約50% | AutoGPTQ | 汎用推論 |
| INT4(GPTQ) | 4bit | 数ポイント | 約2倍 | 約75% | AutoGPTQ | 推論最速化 |
| AWQ | 4bit | 軽微 | 約2〜2.5倍 | 約75% | AutoAWQ | バッチ推論 |
| GGUF | 2〜8bit | 設定次第 | 可変 | 50〜85% | llama.cpp | CPU/エッジ実行 |
from transformers import AutoModelForCausalLM, AutoTokenizer
from auto_gptq import AutoGPTQForCausalLM, BaseQuantizeConfig
model_name = "meta-llama/Meta-Llama-3-8B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
quantize_config = BaseQuantizeConfig(
bits=4, group_size=128, desc_act=False
)
model = AutoGPTQForCausalLM.from_pretrained(model_name, quantize_config)
examples = [tokenizer("サンプル文章" * 50, return_tensors="pt")]
model.quantize(examples)
model.save_quantized("./llama3-8b-int4")
知識蒸留(Knowledge Distillation)とモデル圧縮
知識蒸留は、大規模モデル(教師モデル)の出力を使って小規模モデル(生徒モデル)を訓練することで、教師の知識を小さく圧縮する手法です。GPT-4クラスのモデルの出力を使って7B級のオープンソースモデルをファインチューニングし、特定タスクで大規模モデルに迫る性能を引き出すことができます。初期投資は大きいものの、本番運用コストを桁で削減できる可能性があります。
| テクニック | コスト削減率 | 精度への影響 | 実装難易度 | 初期投資 | 適した規模 |
|---|---|---|---|---|---|
| プロンプトキャッシュ | 30〜70% | なし | 低 | なし | 全般 |
| セマンティックキャッシュ | 20〜60% | なし | 中 | 低 | FAQ系 |
| バッチ処理 | 30〜50% | なし | 中 | 低 | 非同期処理 |
| 量子化(INT8) | 約40% | 軽微 | 中 | 低 | 自社運用 |
| 量子化(INT4) | 約60% | 小〜中 | 中 | 低 | エッジ・大規模 |
| 知識蒸留 | 80〜95% | 中(要検証) | 高 | 中〜高 | 特化タスク |
| モデルルーティング | 50〜80% | 小 | 中 | 低 | API利用 |
モデルルーティング――タスクに応じた最適モデル切り替え
モデルルーティングは、リクエストの難易度を事前に判定し、簡単なものには軽量モデル、難しいものには高性能モデルに振り分ける仕組みです。実装はシンプルで、事前判定用の軽量モデルで「これは複雑か単純か」を判断し、結果に応じてルーティングします。
from openai import OpenAI
client = OpenAI()
def classify_complexity(query: str) -> str:
result = client.chat.completions.create(
model="gpt-4o-mini",
messages=[{"role": "user",
"content": f"""次の質問の難易度を判定してください。
質問: {query}
出力: 'simple' か 'complex' の1単語のみ"""}],
max_tokens=4,
).choices[0].message.content.strip().lower()
return result
def route_and_answer(query: str) -> str:
complexity = classify_complexity(query)
target_model = "gpt-4o" if complexity == "complex" else "gpt-4o-mini"
return client.chat.completions.create(
model=target_model,
messages=[{"role": "user", "content": query}],
).choices[0].message.content
まとめ――コスト削減は「組み合わせ」で効果を最大化する
- キャッシュは「そもそもLLMを呼ばない」最強のコスト削減策
- バッチ処理と量子化は自社運用の2本柱
- 知識蒸留は初期投資が大きい代わりに長期のランニングコストを劇的に下げる
- モデルルーティングはAPI利用時に最も即効性がある
- 単一手法より組み合わせで70%超の削減も可能
DE-STKでは、LLM推論コストの可視化、ボトルネック分析、最適化施策の設計と実装まで一貫して支援しています。月額コストが想定を超えて膨らんでいる方は、まずは現状分析からお気軽にご相談ください。
よくある質問
Q. LLMの推論コストを最も効果的に削減する方法は?
モデルルーティング(タスク難易度に応じたモデル切り替え)とプロンプトキャッシュの組み合わせが最も効果的です。簡単なタスクに軽量モデルを使い、反復的なクエリにキャッシュを適用することで、全体コストを50〜80%削減できるケースがあります。
Q. LLMの量子化で精度は下がりますか?
量子化のレベルによります。FP16からINT8の量子化では精度低下はほぼ無視できるレベルです。INT4まで下げると若干の精度低下が見られますが、多くの実用タスクでは許容範囲内です。タスクに応じた検証が推奨されます。
Q. セマンティックキャッシュとは何ですか?
過去の類似クエリに対する応答を再利用する仕組みです。完全一致ではなく、エンベディングの類似度に基づいてキャッシュヒットを判定するため、言い回しが異なる同様の質問にも対応できます。FAQ系のアプリケーションで特に効果を発揮します。