GPUは新しいデータベースだ

Dev.to / 2026/5/20

💬 オピニオンDeveloper Stack & InfrastructureSignals & Early TrendsIdeas & Deep Analysis

要点

  • この記事は、2十年前にデータベースがスケールの主なボトルネックになったのと同様に、現在はGPUインフラがAIの大半に依存する高コストでステートフル、かつ十分に理解されていないボトルネックになっていると主張しています。
  • また、多くのチームがGPUに対して、データベース時代に起きた典型的な失敗(層の置き方の誤り、性能計画の不備、問題が顕在化するまでブラックボックスとして扱う等)を繰り返していると述べています。
  • 重要な論点として、GPUは「速いCPU」ではなく、基本的に異なる計算モデルに基づいており、そのミスマッチを理解できていないことがムダの多くを生むとしています。
  • 著者は、適切なGPU運用パターンを最初に確立できたチームが、埋めるのが非常に難しいインフラ上の優位性を得られる一方、できないチームは同じ誤りをより速く、より高コストで繰り返すことになると示唆しています。
  • この記事は、GPU運用の「型(ベストプラクティス)」がまだ成熟していない段階(2026年)として、この状況を位置づけています。

_

20年前、チームは、スケールさせたデータベースをどう運用するかまったく分かっていませんでした。パターンが固まる前に、可能な限りの失敗をすべてやらかしたのです。私たちは今、GPUインフラに関して同じ状況にいます。同じ過ちを、より速く行っているだけです。
続きを読むこちら
_

2004年に、もし意味のある規模でウェブアプリケーションを運用していたなら、
最大のインフラ課題はデータベースでした。アプリケーション
サーバではありません。サーバはステートレスで、もっと追加できます。データベースは
、すべてが依存している唯一のステートフルな存在で、水平スケールできず、
運用コストが高く、そしてほとんど誰もそれをうまく運用する方法を知りませんでした。

チームはありとあらゆる失敗をしました。アプリケーションにロジックを入れすぎて
、データベースには入れない。データベースに入れすぎて
、アプリケーションには入れない。インデックスを正しく設定しない。キャッシュを
正しくしない。垂直スケールで限界まで引っ張ってから、慌てて
シャーディングに切り替える。自分たちのクエリプランがどうなっているか
まったく理解していなかった。データベースをブラックボックスとして扱い、
動かなくなってから、そうではないことを大変な手間と苦労で学んだのです。

その後の10年間で、パターンが固まりました。コネクションプーリング。
リードレプリカ。クエリ解析。適切なインデックス戦略。キャッシュ層。
知識は一般化しました。ツールは改善されました。マネージドなデータベースサービスは
複雑さの大部分を抽象化しました。今日、有能なチームなら、
特別な専門知識がなくても、重要な規模でデータベースを運用できます。

私たちは今、2026年になり、GPUインフラについて同じ状況にいます。
GPUは新しいデータベースです。高価で、ステートフルで、十分に理解されていない
、そしてすべてのAIが依存しているボトルネック。期待されるような形では
スケールせず、GPUを運用する大多数のチームによって
うまく運用されておらず、さらにそのパターンはまだ固まっていません。

これを最初に理解するチームは、閉じるのが非常に難しい
インフラ上の優位性を得るでしょう。理解しないチームは、
今後5年間を、2004年に誰もがデータベースで犯したのと同じ失敗を
より速く、そしてより高くつく形で繰り返すことに費やすことになります。

Why the GPU is not just a fast CPU

ほとんどのチームがGPUインフラで最初に犯す間違いは、
GPUを「とても速いCPU」として扱うことです。違います。GPUは根本的に
別の計算モデルであり、そのモデルと、多くの人がGPUを使う方法との
不一致こそが、ほとんどの無駄の原因になっています。

CPUはレイテンシを最適化しており、単一の複雑なタスクを
できるだけ速く完了させます。強力なコアが少数あり、
大きなキャッシュ、洗練された分岐予測、そしてアウト・オブ・オーダー実行を備えています。
連続した論理、条件分岐、そして
各ステップが前の結果に依存するタスクに向いています。

GPUはスループットを最適化しており、非常に多くの
単純なタスクを同時に完了させます。小さくて単純なコアが何千もあります。
大量のデータに対して、同じ操作を並列に適用することが得意です。
順次処理は苦手で、複雑な分岐を伴うものや、計算の途中で
CPUへデータを戻す必要があるものも苦手です。

実務上の結論はこうです。バッチ処理をしていないGPUは、
ほとんどがアイドル状態になります。本番環境でAI推論を
導入するチームが最もよく使うパターンは、1リクエストが来ると、モデルを実行して、
結果を返し、次のリクエストを待つ、というものです。これでは
GPUの実際の能力のごく一部しか使っていません。GPUの
稼働率(utilisation)という数値は妥当に見えます。
しかし、GPUの実際の計算スループットはひどいものです。

これは、データベースが、クエリごとに新しい接続を開いて
クエリを実行し、接続を閉じるようなものです。技術的には
機能しています。ですが、システムがどう使われるべきかという点を
まったく見落としているのです。

# 多くのチームがやること:1リクエストにつき1回推論
# GPUの稼働率は20-40%に見えるが、スループットは低い

async def handle_inference_request(prompt: str) -> str:
    result = model.generate(prompt)  # 待ち時間の間、GPUはほとんどアイドル
    return result

# あるべき姿:動的バッチ処理
# 複数のリクエストをまとめて処理する

class InferenceBatcher:
    def __init__(self, model, max_batch_size: int = 32, max_wait_ms: int = 50):
        self.model = model
        self.max_batch_size = max_batch_size
        self.max_wait_ms = max_wait_ms
        self.queue: asyncio.Queue = asyncio.Queue()

    async def infer(self, prompt: str) -> str:
        future = asyncio.Future()
        await self.queue.put((prompt, future))
        return await future
async def _batch_worker(self):
        while True:
            batch = []
            deadline = asyncio.get_event_loop().time() + (self.max_wait_ms / 1000)

            # バッチがいっぱいになるかデッドラインを過ぎるまでリクエストを収集する
            while len(batch) < self.max_batch_size:
                timeout = deadline - asyncio.get_event_loop().time()
                if timeout <= 0:
                    break
                try:
                    item = await asyncio.wait_for(
                        self.queue.get(),
                        timeout=timeout
                    )
                    batch.append(item)
                except asyncio.TimeoutError:
                    break

            if not batch:
                continue

            prompts = [item[0] for item in batch]
            futures = [item[1] for item in batch]

            # 単一のGPU呼び出しで全リクエストを同時に処理する
            results = self.model.generate_batch(prompts)

            for future, result in zip(futures, results):
                future.set_result(result)

ダイナミックバッチングは、GPU推論のためのコネクションプーリングです。
コストやスループットを気にするなら、これはオプションではありません。
また、多くの手作りの推論デプロイではデフォルトで実装されてもいません。
早期のWebアプリがコネクションプーリングを実装しなかったのと同じ理由で、
チームがそれを必要だと気づくまでには、
壁にぶつかる必要があったからです。


メモリ階層——誰も教えてくれないもの

GPUメモリはCPUメモリとは違います。
その違いを理解することが、
動くシステムと動かないシステムを分け、
管理可能な推論コストと、そうでない推論コストを分けます。

GPUには、デバイス上に専用のメモリ、VRAMがあります。VRAMは高速で有限、
そして高価です。VRAMが80GBあるGPUは、非常に高価なGPUです。
実行しているモデルはVRAMに収まらなければなりません。収まらない場合は、
量子化のような手法で小さくすることもできますし、
複数のGPUに分散することもできますが、壊滅的なパフォーマンス低下を
伴わずに、単純にシステムRAMへ溢れさせることはできません。
CPU RAMとGPU VRAMの間の帯域幅は、VRAMの帯域幅に比べて
桁違いに遅いのです。
「4-bitに量子化した」という話を聞くとき、これが理由です。4-bit量子化は、
メモリ使用量を概ね半分にするためです。
1台のGPUに収まるか、収まらないかの違いがここにあります。

GPUそのものの内部にも、計算がどれだけ速く動くかを決める
メモリ階層があります。会話ですでに処理されたトークンのための
キャッシュされた注意計算であるKVキャッシュはVRAM上に存在し、
シーケンス長とともに増えていきます。KVキャッシュの管理は
LLMサービングにおける最も重要なパフォーマンス判断の一つで、
ほとんどのチームは、長いコンテキストで
メモリ不足(OOM)のエラーに遭遇し始めるまで、
まったく考えていません。

# KVキャッシュ管理:それがないとどうなるか
# 新しい各トークンは、会話全体のコンテキストに対する注意を再生成する
# コストはシーケンス長に対してO(n²)

# vLLMや類似のシステムが違う点:
# PagedAttentionはKVキャッシュを固定サイズのブロックで管理する
# OSにおける仮想メモリのページングのように

# これにより:
# 1. 同じプレフィックスを持つリクエスト間でKVキャッシュを共有できる
# 2. メモリ利用率の向上(内部フラグメンテーションがない)
# 3. 最悪ケースのメモリを事前確保せずに可変長シーケンスを扱える

from vllm import LLM, SamplingParams# vLLM は KV キャッシュ管理を自動で処理します
# これは些細な最適化ではありません — 単純な実装に比べて
# 一般的なワークロードで 2〜4 倍のスループット向上です
llm = LLM(
    model="meta-llama/Llama-3-8b-instruct",
    gpu_memory_utilization=0.90,    # 10% の余裕を残す
    max_model_len=8192,             # 最大シーケンス長
    enable_prefix_caching=True,     # よくある接頭辞(システムプロンプト)をキャッシュする
    tensor_parallel_size=1,         # このモデルで使用する GPU の数
)

sampling_params = SamplingParams(
    temperature=0.7,
    max_tokens=512,
)

# 接頭辞キャッシュとは、あなたのシステムプロンプトが 1 回だけ計算され
# 以降のすべてのリクエストでキャッシュされることを意味します —
# すべての推論で使われる長いシステムプロンプトでは特に大きいです
outputs = llm.generate(prompts, sampling_params)

本番環境で LLM を提供しているほとんどのチームは PagedAttention を使っていません。
代わりに、GPU メモリのフラグメンテーションと冗長な
計算で、GPU メモリの 50〜70% を無駄にしてしまうような素朴な推論実装を使っています。
コスト差は「わずか」とは言えません。

The scaling question everyone asks wrong

チームの AI インフラが負荷で苦しくなり始めると、
最初にほぼ必ず出てくる質問は「GPU をもっと追加すべきか?」です。

これは、データベースが 2008 年に苦しんでいたときに
「データベースサーバを追加すべきか?」が最初の誤った質問だったのと
同じ理由で、間違ったタイミングで投げられる、間違った問いです。
正しい問いは「なぜ、現在の GPU をそんなに非効率に使っているのか?」です。

GPU の利用率が 60% 未満であることは、ほぼ常に
バッチングの問題です。GPU に当たる前に
リクエストが効率よくグループ化されていません。GPU を増やして
利用率の数値を半分にできたとしても、それは
60% で動いていた 1 セットの代わりに、30% のキャパシティで動く 倍のインフラを意味します。コストは 2 倍になり、何も解決しません。

GPU の利用率は高いのにレイテンシがまだ悪い場合は、ほぼ常に
モデルサイズの問題です。提供しているリクエスト量に対して
モデルが大きすぎます。より小さく量子化したモデル、または別の
アーキテクチャなら、計算コストのほんの一部で
レイテンシ要件を満たせる可能性があります。

# スケールを決める前に、本当に重要なものを測定する
import time
import psutil
from prometheus_client import Histogram, Gauge, Counter

# これらのメトリクスは、実際にどこが問題なのかを教えてくれます
GPU_UTILISATION = Gauge(
    'gpu_utilisation_percent',
    ' GPU compute utilisation',
    [' device_id']
)

GPU_MEMORY_USED = Gauge(
    ' gpu_memory_used_bytes',
    ' GPU VRAM in use',
    [' device_id']
)

BATCH_SIZE = Histogram(
    ' inference_batch_size',
    ' Number of requests processed per batch',
    buckets=[1, 2, 4, 8, 16, 32, 64]
)

TOKENS_PER_SECOND = Histogram(
    ' inference_tokens_per_second',
    ' Throughput of inference in tokens per second',
    buckets=[10, 25, 50, 100, 200, 400, 800]
)

TIME_TO_FIRST_TOKEN = Histogram(
    ' inference_ttft_seconds',
    ' Time from request to first token generated',
    buckets=[.05, .1, .25, .5, 1, 2, 5]
)REQUEST_QUEUE_DEPTH = Gauge(
    'inference_queue_depth',
    'GPUを待っているリクエスト数'
)

class InstrumentedInferenceServer:
    async def infer(self, prompts: list[str]) -> list[str]:
        BATCH_SIZE.observe(len(prompts))
        REQUEST_QUEUE_DEPTH.set(self.queue.qsize())

        start = time.perf_counter()
        results = await self._run_inference(prompts)
        duration = time.perf_counter() - start

        total_tokens = sum(len(r.split()) for r in results)
        TOKENS_PER_SECOND.observe(total_tokens / duration)

        return results

バッチサイズ、キューの深さ、トークン/秒、
そして GPU 使用率と VRAM 使用量と並行して「最初のトークンまでの時間」を見られるなら、
「GPUをもっと必要としているのか」という問いはほぼ自明です。多くの場合、答えは「いいえ、バッチングをもっと上手くする必要があります」または「いいえ、より小さなモデルを使う必要があります」で、
スケールアウトは不要であることが分かります。

開始のコールドスタート問題(誰も想定していなかった)

データベースは起動に数秒かかります。GPU推論サーバーは数分かかります。

予期せず再起動したデータベースは、
ほとんどの場合 30 秒以内に復帰します。再起動した LLM 推論サーバーは、
リクエストを受け付ける前に、ストレージからモデル重みを VRAM に読み込む必要があります。
4-bit 量子化で保存された 70B パラメータモデルは、だいたい 35GB です。
典型的なクラウドストレージの帯域幅では、ネットワークストレージから VRAM へ 35GB を読み込むのに、良好な条件下でさえ数分かかります。

これはインシデントの性質をまったく変えてしまいます。データベースの一瞬の途切れは
一時的な中断です。一方、GPU サーバーの一瞬の途切れは、影響を受けた
各インスタンスに対して数分間の停止を意味します。ステートレスな
アプリケーションサーバーではうまく機能し、データベースでも十分に機能するオートスケーリングは、
GPU 推論ではうまくいきません。新しいインスタンスが
準備完了になるまでに非常に時間がかかるためです。

この点を理解してきたチームは、ウォームプールを用意しています。つまり、GPU インスタンスに
モデルをすでに読み込んだ状態で待機させ、
まだ到来していないトラフィックをやり過ごさずに受けられるようにします。これは無駄に感じます。
ただし、数分単位のレイテンシ悪化を伴わずに
トラフィックスパイクを扱うには、それ以外に方法がありません。

# ウォームプール戦略による Kubernetes のデプロイ
# 最低レプリカ数を維持することで、低トラフィック時でもインスタンスをウォーム状態に保つ
# これはお金がかかります。代替はコールドスタートのレイテンシです。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-inference
spec:
  replicas: 3  # これ以下にはスケールしない。ここがウォームプール。
  template:
    spec:
      containers:
      - name: inference-server
        image: myteam/inference:latest
        resources:
          limits:
            nvidia.com/gpu: 1
          requests:
            nvidia.com/gpu: 1
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          # モデルの読み込みには時間がかかります。このプローブは
          # モデルが VRAM に完全に読み込まれるまで成功してはいけません。
          initialDelaySeconds: 180   # 最低3分
          periodSeconds: 10
          failureThreshold: 30       # さらに5分間リトライします
        lifecycle:
          preStop:
            exec:
              # 終了前に処理中のリクエストを捌く
              command: ["/bin/sh", "-c", "sleep 30"]

返却形式: {"translated": "翻訳されたHTML"}---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-inference-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-inference
  minReplicas: 3    # ウォームプールの下限
  maxReplicas: 10
  metrics:
  - type: External
    external:
      metric:
        name: inference_queue_depth
      target:
        type: AverageValue
        averageValue: "5"  # レプリカあたりのキューが5リクエストを超えたらスケール
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 60
      policies:
      - type: Pods
        value: 2
        periodSeconds: 120   # 最大で2分ごとにポッドを2つ追加
                             # 応答には十分速く、
                             # スパイク時に過剰にプロビジョニングしない程度に
    scaleDown:
      stabilizationWindowSeconds: 600  # スケールダウンする前に10分待つ
                                       # コールドスタートのコストが、
                                       # ゆらぎ(yo-yo)スケーリングを招く
                                       # そしてそれは非常に高くつく

スケールダウンの安定化ウィンドウは、意図的に長くしています。コールドスタート
のコストが非常に高いため、短い
トラフィックの落ち込みに反応してスケールダウンしてから再びスケールアップするよりも、
インスタンスを動かし続けておく方が安くつくのです。
これは、ステートレスなWebサービスから来た人には直感に反します。
しかしGPUインフラの運用上の現実はそうなっています。

コストモデルは逆さまになっている

データベースのコストはデータ量とクエリの複雑さに応じて増えます。データが
増え、クエリが複雑になるほど、より多く支払う必要があります。

一方でGPUのコストは時間に比例して増えます。GPUが存在する
すべての秒数ぶんを支払う必要があり、リクエストに応えているかどうかは関係ありません。
アイドル状態のGPUは、
忙しいGPUと同じコストです。

これにより、通常のインフラ経済が反転します。ステートレスな
アプリケーションサーバでは、アイドル容量は安価で、トラフィックが減ればゼロまで
スケールでき、支払いも発生しません。ですがGPU推論では、ゼロまで
スケールすると、トラフィックが戻ったときのコールドスタートが発生します。
本番の推論サービスにおける、最小限に成立する容量はゼロではありません。
それは、
許容できるコールドスタートのレイテンシと、あなたの
トラフィックスパイクのパターンによって決まる、ウォームプールに必要な容量そのものです。

この状況に折り合いをつけたチームは、GPUコストを、使用量に応じて変動する
変動費として考えるのをやめ、容量を買う固定費として考えるようになりました。
問いは「トラフィックが低いとき、どうすればGPUの支払いを減らせるか?」ではありません。
問いは「常時稼働させるべき容量の適切な量はどれくらいで、そしてそれを
効率よく使えていることをどう保証するか?」です。

効率的に使うとは、高いバッチ充填率、高いGPU時間あたりのトークンスループット、
低いアイドル時間を意味します。上記の指標は、この
計算の入力です。これらがなければ、
インフラが正しく見積もられているかどうかを、推測するしかありません。

いま現れてきているパターン

2026年にGPUインフラをうまく運用しているチームは、
運用面での規律において、2012年にデータベースをうまく運用していた
チームとよく似ています——十分な人数が痛い目にあって、
パターンが固まり始めたあとです。

彼らは、GPUの利用率を遅行指標として扱い、
トークンスループットを先行指標として扱います。彼らはすべてを計測します:
バッチサイズ、キューの深さ、最初のトークンまでの時間、
VRAMの使用量、KVキャッシュのヒット率。
彼らは直感ではなく、実測されたトラフィックパターンに基づいてウォームプールの規模を決めます。
そして、品質の基準を満たす最小のモデルを使います。払える最大のモデルではありません。
なぜなら、より小さなモデルは、ほぼあらゆる実用的な指標において、
うまくバッチ化できない大きなモデルよりも効率的にバッチ処理できるからです。

また、彼らは、受け入れるまでに時間のかかることも受け入れています。
それは、GPUインフラにとって正しい抽象化が「高速計算」ではなく
「スループット容量」だということです。問いは「この機械は1リクエストを
どれくらい速く処理できるのか?」ではありません——GPUはそれに関しては、
どのみち高速です。
問いは「許容できるレイテンシで、このインフラは
1ドルあたり何件のリクエストを処理できるのか?」です。
この問いには、別の
指標、別のアーキテクチャ、そしてCPUインフラの経験から持ち込むのとは異なる
メンタルモデルが必要になります。

データベースにたとえる比喩は、見た目以上に深いところまで及んでいます。2004年、
データベースをブラックボックスとして扱い——データを入れて、データを
取り出し、遅ければRAMを追加する——ようにしていたチームは、
最終的に、そもそも自分たちの
アーキテクチャでは突破できない壁にぶつかりました。
データベースの内部で何が起きているのかを理解していたチームは、
クエリプラン、インデックスの利用、ロック競合、バッファプールの挙動——そういったことを踏まえて
スケールする仕組みを作りました。

GPUはブラックボックスではありません。メモリ階層、バッチングのモデル、
コスト構造、そしてデータベースと同じように
理解を報いる一方で無知を罰する性能特性を持っています。

パターンは形になりつつあります。いまそれを学んでいるチームは、
5年後に、2015年にデータベースを理解していたエンジニアが持っていたのと同じ
優位性を得るでしょう。

そして失敗は、まさにいま、規模の大きいところで、しかも高いコストを伴って起きています。
ほとんどの失敗は同じ失敗です。ほとんどは回避できます。