0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Kubernetes上でLLM推論APIを水平スケールさせる構成と注意点

0
Last updated at Posted at 2026-10-03

LLM推論APIを本番運用すると、アクセス数の増加だけでなく、入力・出力トークン数の偏りによって負荷が大きく変わります。CPU中心のWeb APIと同じ感覚でPod数だけ増やしても、GPUメモリ不足やキュー詰まり、スケール判定の遅延に悩まされがちです。

この記事では、Kubernetes上にvLLMベースの推論APIを配置し、複数のGPU Podへリクエストを分散する構成を実際に検証した内容をまとめます。結論としては、推論Podをステートレスに保ち、Serviceで分散し、GPU使用率だけでなく「待ちリクエスト数」や「処理時間」を見てスケールさせるのが重要です。

基本構成

検証では、次のような構成にしました。

Client
  |
  v
Kubernetes Service
  |
  +-- LLM inference Pod (GPU)
  +-- LLM inference Pod (GPU)
  +-- LLM inference Pod (GPU)

各Podは同じモデルをロードします。Pod間でKVキャッシュを共有する構成ではないため、リクエストは基本的にどのPodに送られても処理できるようにします。

チャット履歴などの状態をPod内に保持すると、Podの再起動やスケールインでセッションが失われます。履歴はリクエストに含めるか、外部のデータストアに保存し、推論Podはステートレスにしました。

まず、vLLMのDeploymentとServiceを用意します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: llm-api
spec:
  replicas: 2
  selector:
    matchLabels:
      app: llm-api
  template:
    metadata:
      labels:
        app: llm-api
    spec:
      terminationGracePeriodSeconds: 120
      containers:
        - name: vllm
          image: vllm/vllm-openai:latest
          args:
            - --model
            - /models/my-model
            - --served-model-name
            - my-model
            - --max-model-len
            - "4096"
          ports:
            - containerPort: 8000
          resources:
            requests:
              cpu: "2"
              memory: "16Gi"
              nvidia.com/gpu: "1"
            limits:
              cpu: "4"
              memory: "32Gi"
              nvidia.com/gpu: "1"
          readinessProbe:
            httpGet:
              path: /health
              port: 8000
            initialDelaySeconds: 30
            periodSeconds: 10
          startupProbe:
            httpGet:
              path: /health
              port: 8000
            failureThreshold: 60
            periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
  name: llm-api
spec:
  selector:
    app: llm-api
  ports:
    - port: 8000
      targetPort: 8000

モデルの配置方法は、コンテナイメージに含める方法、PersistentVolumeから読む方法、起動時にダウンロードする方法があります。今回の検証では、モデルをあらかじめノード上のボリュームに配置しました。

起動時にモデルをダウンロードすると、Pod追加のたびに起動時間とネットワーク負荷が発生します。特にHPAで急にPodを増やす場合、モデルロードがボトルネックになりやすいため注意が必要です。

ServiceとProbeの設計

Serviceは、Readiness Probeが成功したPodにだけトラフィックを送ります。LLMではコンテナプロセスが起動していても、モデルロード中は推論を受け付けられません。そのため、単純なTCPチェックでは不十分です。

検証中にReadiness Probeを設定しなかったところ、Pod起動直後のリクエストが失敗しました。プロセスは起動済みでも、モデルのロードが終わっていなかったためです。

また、モデルロード中のPodを「準備完了」と判定しないよう、Startup Probeを併用しました。モデルサイズやストレージ速度によって起動時間は大きく異なるので、failureThreshold と periodSeconds は実環境で調整します。

Podの削除時にも注意が必要です。推論中のリクエストを即座に切断すると、クライアント側ではタイムアウトや途中応答になります。terminationGracePeriodSeconds を確保し、アプリケーション側でもSIGTERMを受けたら新規受付を停止する設計にします。

本番では、次のようなPodDisruptionBudgetも設定しました。

apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
  name: llm-api-pdb
spec:
  minAvailable: 1
  selector:
    matchLabels:
      app: llm-api

これにより、ノードメンテナンスなどで全Podが同時に停止することを避けられます。

HPAで水平スケールする

リクエスト処理とスケール判定の流れを図にすると次の通りです。

まずはCPU使用率を使ったHPAを設定できます。

apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: llm-api
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: llm-api
  minReplicas: 2
  maxReplicas: 6
  behavior:
    scaleUp:
      stabilizationWindowSeconds: 30
    scaleDown:
      stabilizationWindowSeconds: 300
  metrics:
    - type: Resource
      resource:
        name: cpu
        target:
          type: Utilization
          averageUtilization: 70

適用後は次のコマンドで状態を確認します。

kubectl apply -f llm-api.yaml
kubectl apply -f llm-api-hpa.yaml

kubectl get pods -l app=llm-api
kubectl get svc llm-api
kubectl get hpa llm-api

ただし、LLM推論の負荷はCPU使用率だけでは判断しにくいです。GPUがほぼ100%でも、リクエストが効率よく処理されている場合があります。一方で、入力トークンが長いリクエストが集中すると、GPU使用率が一時的に低くても待ち時間が増えることがあります。

そのため、実運用では次のような指標をカスタムメトリクスとして利用するのが現実的です。

  • 待機中リクエスト数
  • リクエストの平均待ち時間
  • p95、p99レイテンシ
  • 1秒あたりの生成トークン数
  • GPUメモリ使用量
  • KVキャッシュ使用率

Prometheus Adapterなどを利用してHPAから参照できるようにします。例えば、待機リクエスト数が一定値を超えたらPodを増やす設計です。

ただし、GPUノードが空いていなければPod数だけ増えてもスケールしません。KubernetesのCluster Autoscalerなど、GPUノードを追加できる仕組みも合わせて検討します。

負荷試験で確認したこと

APIはOpenAI互換エンドポイントとして呼び出しました。

kubectl port-forward svc/llm-api 8000:8000

別ターミナルからリクエストを送ります。

curl http://localhost:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "my-model",
    "messages": [
      {"role": "user", "content": "KubernetesのReadiness Probeを説明してください"}
    ],
    "max_tokens": 256,
    "temperature": 0.2
  }'

簡単な負荷試験では、同時実行数を少しずつ増やし、レスポンスタイムとエラー率を確認しました。

hey -n 100 -c 5 \
  -m POST \
  -H "Content-Type: application/json" \
  -D request.json \
  http://localhost:8000/v1/chat/completions

request.json の例です。

{
  "model": "my-model",
  "messages": [
    {
      "role": "user",
      "content": "水平スケールの注意点を3つ説明してください"
    }
  ],
  "max_tokens": 128
}

検証では、Pod数を増やすことで同時リクエスト数は処理しやすくなりましたが、各Podがモデルをロードするため、GPUメモリはPod数に比例して必要になりました。

また、リクエストを増やしすぎると、Podを追加する前に既存Podのキューが膨らみます。スケールアウトにはPod起動とモデルロードの時間があるため、急激なスパイク対策には、キューイング、レート制限、クライアント側リトライの設計も必要です。

運用時の注意点

最も重要だったのは、リクエストの上限を明確にすることです。max_tokens や入力トークン数に上限を設定しないと、一部の長大なリクエストが他のリクエストを圧迫します。

API Gatewayやアプリケーション層で、次のような制御を入れました。

  • 入力トークン数の上限
  • max_tokens の上限
  • リクエストタイムアウト
  • 同時実行数の制限
  • 429レスポンスによるレート制限
  • 指数バックオフ付きのリトライ

リトライは便利ですが、タイムアウトした推論がサーバー側でまだ実行中だと、同じリクエストが二重に処理される可能性があります。リクエストIDを付与し、必要に応じて冪等性を管理します。

GPUを使う場合は、Deploymentのローリングアップデートも慎重に設定します。更新中に新Podを作れないと、古いPodを先に削除して一時的に容量不足になることがあります。GPUノードの空き状況を確認し、maxUnavailable: 0 などの設定を環境に合わせて検討します。

まとめ

Kubernetes上でLLM推論APIを水平スケールする場合、Deploymentのreplicasを増やすだけでは不十分です。

重要なポイントは次の通りです。

  • 推論Podをステートレスにする
  • Readiness Probeでモデルロード完了を判定する
  • Serviceで複数Podへ分散する
  • CPU使用率だけでなく待ち時間やキュー長を見る
  • GPUノードの追加能力を確保する
  • トークン数、同時実行数、タイムアウトを制限する
  • スケールイン時に実行中リクエストを保護する

特にLLMでは、リクエストごとのトークン数が性能を大きく左右します。まず固定条件で負荷試験を行い、その後に入力長や同時実行数を変えながら、レイテンシ・エラー率・GPUメモリ使用量を測るのがおすすめです。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?