ノート:

Oracle Kubernetes Engine (OKE)へのOpenAI vLLM本番スタックのデプロイ

はじめに

本番ワークロードに大規模言語モデル(LLM)を採用する組織は、サードパーティの推論APIに依存するか、自己ホスト型の推論スタックをデプロイするという、インフラストラクチャの重要な決定に直面しています。自己ホスト型のデプロイメントには、完全なデータ・プライバシとコンプライアンス制御、ネットワーク・ラウンドトリップの排除による100ミリ秒未満の推論レイテンシ、大規模な予測可能なコスト、ベンダー・ロックインなしで任意のオープンソース・モデルを微調整して提供する自由といった大きな利点があります。

ただし、本番レベルのLLM推論スタックを最初から構築することは複雑です。GPU対応コンテナ・オーケストレーション、複数のモデル・レプリカにわたるインテリジェントなリクエスト・ルーティング、大規模なモデル重みに対応した永続ストレージ、継続的な監視がすべて統合され、確実に実行されている必要があります。

Oracle Cloud Infrastructureは、AI推論のための複数のパスを提供します。OCI Generative AI Serviceは、テナンシに分離された専用AIクラスタでフルマネージド・エクスペリエンスを提供し、サポートされているモデルを迅速に開始したいチームに最適です。このチュートリアルでは、独自の推論スタックをOKEにデプロイするという代替のアプローチをとります。このパスは、GPUドライバ、CUDAバージョン、モデル構成、サービングパラメーターを正確に制御する必要があるチーム、またはカスタムモデルのトレーニングと微調整を行い、直接サービスを提供したいチーム向けに設計されています。OCIは、超低レイテンシのRDMAクラスタ・ネットワーキングによって接続されたNVIDIA A10、A100、およびH100 GPUを搭載したベアメタルGPUインスタンスを提供し、クラウドの弾力性からメリットを得ながら、オンプレミスと同じレベルのハードウェア制御を実現します。

vLLM Production Stackは、vLLM上に構築されたオープンソースのKubernetesネイティブ・プラットフォーム、Meta、Mistral AI、IBMなどの組織による本番で使用される高スループットの推論エンジンを提供することで、自己ホスト推論の複雑さを解決します。効率的なGPUメモリー管理およびKVキャッシュ最適化により、標準サービス・フレームワークと比較して最大24xのスループットが向上します。OKEおよびOCI GPUシェイプと組み合せると、エンタープライズ・グレードのネットワーキング、ストレージおよびセキュリティを備えた本番環境に対応した推論プラットフォームが提供されます。このチュートリアルで使用するOCIデプロイメント・スクリプトは、公式のvLLM本番スタック・リポジトリにコントリビュートされ、維持されます。

このチュートリアルでは、インフラストラクチャ・プロビジョニングから最初の推論リクエストの実行まで、OKEへのvLLM本番スタックのデプロイについて説明します。

ノート:このチュートリアルでは、GPU推論デプロイメントに必要なOCIクラウド・リソースのフロー全体を理解するために、OCI CLIを使用してリソースを段階的にプロビジョニングします。本番環境では、バージョン管理された繰返し可能なデプロイメントにTerraformまたはOCI Resource Manager (Shepherd)を使用して、このインフラストラクチャをコード化することをお薦めします。

サブネット、OKEクラスタ、GPUノード・プール、vLLMポッドおよびロード・バランサを含むVCNを示すアーキテクチャ図

このチュートリアルでは、次のOCIサービスを使用します。

サービス 目 的
Oracle Kubernetesエンジン(OKE) コンテナ・オーケストレーションおよびGPUワークロード・スケジューリングのためのマネージドKubernetesクラスタ
OCIコンピュート(GPUシェイプ) モデル推論用のNVIDIA A10 (24GB)およびA100 (80GB) GPUインスタンス
OCIブロック・ボリューム 構成可能なパフォーマンス層によるモデルの重み付けのための永続ストレージ
OCI仮想クラウド・ネットワーク(VCN) サブネット、ゲートウェイおよびセキュリティ・リストを含むネットワーク・インフラストラクチャ
OCIロード・バランサ 推論エンドポイントへの外部アクセス
OCI Bastion プライベート・クラスタ・アクセスの管理対象SSHトンネル
OCIオブジェクト・ストレージ 事前認証済リクエスト(PAR) URLを使用した代替モデル・ソース

目的

このチュートリアルでは、次のことを実行します。

前提条件

ノート:このチュートリアルの出力例およびスクリーンショットでは、us-chicago-1を使用します。OCI_REGIONを設定することで、サポートされている任意のリージョンにデプロイできます。GPU容量はリージョンおよびアベイラビリティ・ドメインによって異なりますので、デプロイする前にターゲットGPUシェイプが使用可能であることを確認してください。リージョン別のGPUシェイプの可用性を確認し、容量エラーが発生した場合は別の可用性ドメイン(GPU_AD_INDEX)を試す準備をします。

ノート:このチュートリアルでは、有料GPUリソース(たとえば、VM.GPU.A10.1)をプロビジョニングします。OCI Always Freeワークロードではありません。継続的な料金を回避するために、完了時に必ずクリーンアップ・ステップを実行します。

ノート:このチュートリアルでは、Apache 2.0ライセンス・モデルであるopenai/gpt-oss-20bをOpenAIからデプロイします。Hugging Faceトークンは必要ありません。Meta Llama 3.1などのゲート付きモデルをデプロイする場合は、APIトークンを含むHugging Faceアカウントが必要です。

タスク1: 環境変数の構成

インフラストラクチャをデプロイする前に、必要なOCI構成を設定します。

  1. OCIコンソールでコンパートメントOCIDを検索します。「アイデンティティとセキュリティ」「コンパートメント」にナビゲートし、ターゲット・コンパートメントをクリックしてOCIDをコピーします。

    oci iam compartment list --query 'data[].{name:name,id:id}' --output table

    コンパートメントOCIDを示すOCI CLI出力

  2. 必要な環境変数をエクスポートします。

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. オプションで、次のいずれかの環境変数を設定して、デフォルトの構成をオーバーライドします。

    可変 デフォルト 説明
    OCI_REGION us-ashburn-1 デプロイメント用のOCIリージョン
    OCI_PROFILE DEFAULT OCI CLI構成プロファイル
    CLUSTER_NAME production-stack OKEクラスタの名前
    GPU_SHAPE VM.GPU.A10.1 ノード・プールのGPUコンピュート・シェイプ
    GPU_NODE_COUNT 1 プール内のGPUノードの数
    GPU_BOOT_VOLUME_GB 200 GPUノード用のブート・ボリューム・サイズ(GB)
    CPU_BOOT_VOLUME_GB 100 CPUノード用のブート・ボリューム・サイズ(GB)
    GPU_AD_INDEX 1 GPU配置用の可用性ドメイン索引(0ベース)
    PRIVATE_CLUSTER true パブリックKubernetes APIエンドポイントの場合はfalseに設定します
    KUBERNETES_VERSION v1.31.10 OKEクラスタのKubernetesバージョン

    たとえば、2つのA100 GPUノードを使用してデプロイするには:

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
    export GPU_SHAPE="BM.GPU.A100-v2.8"
    export GPU_NODE_COUNT="2"
  4. 使用可能なGPUシェイプを確認し、モデル・サイズ要件に基づいて1つ選択します。

    形状 GPU GPUタイプ GPUメモリー 推奨対象
    VM.GPU.A10.1 1 NVIDIA A10 24GB 7B–13Bパラメータ・モデル
    VM.GPU.A10.2 2 NVIDIA A10 48GB 小型モデルと平行なテンソル
    BM.GPU4.8 8 NVIDIA A100 40 GB 320GB 70Bモデル、コスト効率が高い
    BM.GPU.A100-v2.8 8 NVIDIA A100 80 GB 640GB 70B+パラメータ・モデル
    BM.GPU.H100.8 8 NVIDIA H100 640GB 最大モデル、RDMAサポート

    ノート:ベア・メタル・シェイプ(BM.*)は、仮想化オーバーヘッドのない専用ハードウェアを提供し、マルチGPUテンソル並列処理をサポートします。仮想マシン・シェイプ(VM.*)は、小規模なモデルではコスト効率が高くなります。

    ノート:このチュートリアルでは、VM.GPU.A10.1 (24 GB GPUメモリーを備えた単一のNVIDIA A10)を使用してopenai/gpt-oss-20bをデプロイします。これは、通常、単一のA10 GPUに適合する3.6Bアクティブ・パラメータを持つMixture of Experts (MoE)モデルです。高度なセクションでは、Llama 3.1 70Bなどの大規模なモデルに対してBM.GPU.H100.8を使用したマルチGPU構成を示します。

タスク2: 自動スクリプトを使用したデプロイ(クイック・スタート)

vLLM本番スタックには、すべてのOCIリソースをプロビジョニングし、単一のコマンドで推論スタックをデプロイする自動デプロイメント・スクリプトが含まれています。このアプローチは、迅速なデプロイメントに使用します。タスク3から10は、プロセスをカスタマイズするユーザーの各ステップを個別にカバーします。

  1. vLLM Production Stackリポジトリをクローニングします。

    git clone https://github.com/vllm-project/production-stack.git
    cd production-stack/deployment_on_cloud/oci
  2. コンパートメントOCIDをエクスポートします。

    export OCI_COMPARTMENT_ID="ocid1.compartment.oc1..xxxxx"
  3. デプロイメント・スクリプトを実行します。

    ./entry_point.sh setup

    VCN、クラスタ、要塞およびノード・プールの作成を示すentry_point.sh設定のターミナル出力

    パブリック・クラスタ(PRIVATE_CLUSTER=false)の場合、設定によってすべてのインフラストラクチャが作成され、1つのコマンドでvLLMスタックがデプロイされます。Helm値ファイルを2番目の引数として渡します:

    PRIVATE_CLUSTER=false ./entry_point.sh setup ./production_stack_specification.yaml

    プライベート・クラスタ(デフォルト)の場合、設定によってインフラストラクチャが作成されますが、Kubernetes APIに直接到達できません。別のターミナルを開き、トンネルを起動して、デプロイします:

    # In a separate terminal, start the SSH tunnel (auto-reconnects on drops):
    ./entry_point.sh tunnel
    
    # Back in the first terminal, deploy vLLM:
    ./entry_point.sh deploy-vllm ./production_stack_specification.yaml
  4. デプロイメントが実行中であることを確認します。

    kubectl get pods

    予期される出力:

    NAME                                            READY   STATUS    RESTARTS   AGE
    vllm-deployment-router-xxxxxxxxxx-xxxxx          1/1     Running   0          5m
    vllm-gpt-oss-deployment-vllm-xxxxxxxxxx-xxxxx    1/1     Running   0          5m

ノート:両方のポッドのステータスがRunningの場合、デプロイメントの準備ができています。「タスク10: 推論エンドポイントのテスト」に進みます。

ノート: GPUインスタンスはOCI容量制約の対象となります。スクリプトが15分を超えて「GPUノードの待機中」ループにとどまっている場合、選択した可用性ドメインでGPUシェイプが使用できない可能性があります。oci ce node-pool getを使用してノード・プールのステータスを確認し、「Out of host capacity」エラーを確認します。これを解決するには、./entry_point.sh cleanupを使用してクリーン・アップし、別の可用性ドメイン(GPU_AD_INDEX=0GPU_AD_INDEX=2など)または別のGPUシェイプ(GPU_SHAPE=VM.GPU.A10.2など)で再デプロイします。

ノート:デプロイメント・スクリプトでは、大幅なコストが発生するGPUインスタンスが使用されます(単一のA10 GPUの場合は1日当たり50ドル)。継続料金を回避するために、./entry_point.sh cleanupを必ず実行してください。

タスク3: VCNとネットワーキングの作成

OKEクラスタに必要なOCIネットワーク・インフラストラクチャを作成します。これには、Virtual Cloud Network (VCN)、ゲートウェイ、ルート表、セキュリティ・リストおよびサブネットが含まれます。各ネットワーキング・リソースは数秒で作成され、コマンドのフル・セットは2分以内に完了します。

  1. 10.0.0.0/16 CIDRブロックを含むVCNを作成します。

    VCN_ID=$(oci network vcn create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --display-name "${CLUSTER_NAME}-vcn" \
        --cidr-blocks '["10.0.0.0/16"]' \
        --dns-label "prodstack" \
        --query "data.id" \
        --raw-output)
  2. パブリック・サブネット・ルーティング用のインターネット・ゲートウェイを作成します。

    IGW_ID=$(oci network internet-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-igw" \
        --is-enabled true \
        --query "data.id" \
        --raw-output)
  3. プライベート・サブネットからのアウトバウンド・トラフィック用のNAT Gatewayを作成します。

    NAT_ID=$(oci network nat-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-nat" \
        --query "data.id" \
        --raw-output)
  4. Oracle Services Networkにアクセスできるサービス・Gatewayを作成します。OKEクラウド・コントローラは、Oracle Servicesを使用してワーカー・ノードを初期化します(可用性ドメイン・ラベルの設定、初期化テイントの削除)。サービス・ゲートウェイがない場合、GPUノードは初期化されていない状態のままになり、ブロック・ボリュームのプロビジョニングは失敗します。

    SGW_SERVICE_ID=$(oci network service list \
        --query "data[?contains(name, 'All') && contains(name, 'Services')].id | [0]" \
        --raw-output)
    
    SGW_SERVICE_NAME=$(oci network service list \
        --query "data[?contains(name, 'All') && contains(name, 'Services')].\"cidr-block\" | [0]" \
        --raw-output)
    
    SGW_ID=$(oci network service-gateway create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-sgw" \
        --services "[{\"serviceId\": \"${SGW_SERVICE_ID}\"}]" \
        --query "data.id" \
        --raw-output)
  5. プライベート・サブネットおよびパブリック・サブネットのルート表を作成します。

    PRIVATE_RT_ID=$(oci network route-table create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-private-rt" \
        --route-rules "[
            {\"cidrBlock\": \"0.0.0.0/0\", \"networkEntityId\": \"${NAT_ID}\"},
            {\"destination\": \"${SGW_SERVICE_NAME}\", \"destinationType\": \"SERVICE_CIDR_BLOCK\", \"networkEntityId\": \"${SGW_ID}\"}
        ]" \
        --query "data.id" \
        --raw-output)
    
    PUBLIC_RT_ID=$(oci network route-table create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-public-rt" \
        --route-rules "[{\"cidrBlock\": \"0.0.0.0/0\", \"networkEntityId\": \"${IGW_ID}\"}]" \
        --query "data.id" \
        --raw-output)

    ノート:プライベート・ルート表には、一般的なインターネット・アクセス用のNAT Gatewayルート(コンテナ・イメージのプル、モデルのダウンロード)と、Oracle Services Networkへの直接アクセス用のサービス・ゲートウェイ・ルートの2つのルールがあります。サービス・ゲートウェイ・ルートがクリティカルです。これがないと、OKEクラウド・コントローラはワーカー・ノードを初期化できず、ブロック・ボリュームのプロビジョニングを防ぎます。パブリック・ルート表は、ロード・バランサ・アクセスにインターネット・ゲートウェイを使用します。

  6. OKEに必要なイングレスおよびエグレス・ルールを含むセキュリティ・リストを作成します。

    SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 22, "max": 22}}, "description": "SSH access"},
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "10.244.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes pods CIDR"},
            {"source": "10.96.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes services CIDR"},
            {"source": "0.0.0.0/0", "protocol": "1", "isStateless": false, "icmpOptions": {"type": 3, "code": 4}, "description": "Path MTU discovery"}
        ]' \
        --query "data.id" \
        --raw-output)

    セキュリティ・ノート:このセキュリティ・リストの例は、わかりやすくするために意図的に幅広くなっています。本番環境では、SSHを要塞サブネットおよびIP範囲に制限し、ロード・バランサ・サブネットで0.0.0.0/0からのSSHが許可されないように、サブネットごとに個別のセキュリティ・リストまたはNSGを優先します。

    セキュアなデフォルト:まず、SSHをパブリックIPに制限し、SSHルールを要塞サブネットにのみアタッチします。Kubernetesポッド/サービスCIDRをワーカー・サブネットに保持し、ロード・バランサ・サブネットからSSHを完全に省略できます。

    オプション(推奨)の分割:要塞サブネット用にSSHのみの小さなセキュリティ・リストを作成し、ワーカー/LBサブネット用に別のリストを作成します。

    BASTION_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-bastion-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "YOUR_PUBLIC_IP/32", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 22, "max": 22}}, "description": "SSH from your IP"}
        ]' \
        --query "data.id" \
        --raw-output)
    
    WORKER_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-worker-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "10.244.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes pods CIDR"},
            {"source": "10.96.0.0/16", "protocol": "all", "isStateless": false, "description": "Kubernetes services CIDR"},
            {"source": "0.0.0.0/0", "protocol": "1", "isStateless": false, "icmpOptions": {"type": 3, "code": 4}, "description": "Path MTU discovery"}
        ]' \
        --query "data.id" \
        --raw-output)
    
    LB_SL_ID=$(oci network security-list create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-lb-sl" \
        --egress-security-rules '[{"destination": "0.0.0.0/0", "protocol": "all", "isStateless": false}]' \
        --ingress-security-rules '[
            {"source": "10.0.0.0/16", "protocol": "all", "isStateless": false, "description": "VCN internal traffic"},
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 80, "max": 80}}, "description": "HTTP (public LB)"},
            {"source": "0.0.0.0/0", "protocol": "6", "isStateless": false, "tcpOptions": {"destinationPortRange": {"min": 443, "max": 443}}, "description": "HTTPS (public LB)"}
        ]' \
        --query "data.id" \
        --raw-output)

    ノート:内部ロード・バランサのみを使用する場合は、前述の0.0.0.0/0ソースを10.0.0.0/16 (またはVCN CIDR)に置き換えてください。使用法: BASTION_SL_IDを要塞サブネットに、WORKER_SL_IDをAPI/workerサブネットに、LB_SL_IDをロードバランササブネットにアタッチします。

    ノート: GPUワーカー・ノードをクラスタに登録するには、KubernetesポッドCIDR (10.244.0.0/16)およびサービスCIDR (10.96.0.0/16)ルールが必要です。ICMPタイプ3コード4ルールはパスMTU検出を有効にし、パケットの断片化の問題を防止します。

  7. サブネットを作成します。クラスタには4つのサブネットが必要です。1つはKubernetes APIエンドポイント用、もう1つはワーカー・ノード用、もう1つはロード・バランサ用、もう1つはプライベート・クラスタへのアクセスに使用される要塞ホスト用です。

    API_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-api-subnet" \
        --cidr-block "10.0.0.0/28" \
        --route-table-id "${PRIVATE_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "kubeapi" \
        --prohibit-public-ip-on-vnic true \
        --query "data.id" \
        --raw-output)
    
    WORKER_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-worker-subnet" \
        --cidr-block "10.0.10.0/24" \
        --route-table-id "${PRIVATE_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "workers" \
        --prohibit-public-ip-on-vnic true \
        --query "data.id" \
        --raw-output)
    
    LB_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-lb-subnet" \
        --cidr-block "10.0.20.0/24" \
        --route-table-id "${PUBLIC_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "loadbalancers" \
        --query "data.id" \
        --raw-output)
    
    BASTION_SUBNET_ID=$(oci network subnet create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --vcn-id "${VCN_ID}" \
        --display-name "${CLUSTER_NAME}-bastion-subnet" \
        --cidr-block "10.0.30.0/24" \
        --route-table-id "${PUBLIC_RT_ID}" \
        --security-list-ids "[\"${SL_ID}\"]" \
        --dns-label "bastion" \
        --query "data.id" \
        --raw-output)
    サブネット CIDR 表示 目 的
    APIエンドポイント 10.0.0.0/28 プライベート Kubernetes APIサーバー
    ワーカー・ノード 10.0.10.0/24 プライベート GPUコンピュート・ノード
    ロード・バランサ 10.0.20.0/24 パブリック 外部サービス・アクセス
    要塞 10.0.30.0/24 パブリック プライベート・クラスタ・アクセス用のSSHトンネル

タスク4: OKEクラスタの作成

タスク3で作成したネットワーキング・リソースを使用して、管理対象KubernetesクラスタをOKEにデプロイします。クラスタのプロビジョニングには約10分かかります。このチュートリアルでは、Kubernetes APIエンドポイントの予約済パブリックIPを使用しないプライベート・クラスタ(スクリプトのデフォルト)を作成します。APIサーバーがパブリック・インターネットに公開されていないため、プライベート・クラスタは本番ワークロードの推奨アプローチです。

  1. プライベート・エンドポイントを含むOKEクラスタを作成します。

    oci ce cluster create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --name "${CLUSTER_NAME}" \
        --vcn-id "${VCN_ID}" \
        --kubernetes-version "${KUBERNETES_VERSION}" \
        --endpoint-subnet-id "${API_SUBNET_ID}" \
        --service-lb-subnet-ids "[\"${LB_SUBNET_ID}\"]" \
        --endpoint-public-ip-enabled false

    このコマンドは、作業リクエストIDを返します。クラスタ・リストからクラスタIDを取得します。

    CLUSTER_ID=$(oci ce cluster list \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --lifecycle-state CREATING \
        --query 'data[0].id' \
        --raw-output)
    echo "CLUSTER_ID=${CLUSTER_ID}"

    ノート:プライベート・クラスタには、予約済パブリックIPは必要ありません。ワーカー・ノードは、引き続きNAT Gatewayを介してインターネットにアクセスし、コンテナ・イメージをプルしてモデルをダウンロードします。kubectlアクセスのみに要塞を通るSSHトンネルが必要です(次のステップで構成)。

  2. クラスタがACTIVEになるのを待ちます。このステップには約10分かかります。

    oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.\"lifecycle-state\"" \
        --raw-output

    出力がACTIVEを返すまでコマンドをポーリングします。

    オプション: 簡潔なステータス・サマリー(プライベートAPIエンドポイントを含む)を表示します。

    oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query 'data.{name:name,state:"lifecycle-state","k8s-version":"kubernetes-version",endpoint:endpoints."private-endpoint"}' \
        --output table

    ACTIVE状態のOKEクラスタを示すOCI CLI出力

    kubectl get nodes -o wide

    2つのReadyノードを示すkubectlのgetノードの出力

  3. プライベート・クラスタにアクセスするためのOCI要塞を作成します。要塞は、プライベートKubernetes APIエンドポイントへの管理対象SSHトンネルを提供します。

    BASTION_ID=$(oci bastion bastion create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --bastion-type STANDARD \
        --target-subnet-id "${BASTION_SUBNET_ID}" \
        --name "${CLUSTER_NAME}-bastion" \
        --client-cidr-list '["YOUR_PUBLIC_IP/32"]' \
        --query "data.id" \
        --raw-output)

    ノート: YOUR_PUBLIC_IP/32を現在のパブリックIPに置き換えます。共有ネットワークの場合は、かわりに企業のCIDRブロックを使用してください。

    要塞がアクティブになるまで待機します(約1分)。

    oci bastion bastion get \
        --bastion-id "${BASTION_ID}" \
        --query "data.\"lifecycle-state\"" \
        --raw-output

    セキュリティ・ノート:本番環境では、0.0.0.0/0を使用しないでください。--client-cidr-listをパブリックIPまたは企業CIDR ("YOUR_PUBLIC_IP/32"など)に制限します。そうしないと、インターネット上のすべてのユーザーが要塞セッションを試行できます。

  4. プライベート・エンドポイントを使用してkubeconfigをダウンロードします。

    oci ce cluster create-kubeconfig \
        --cluster-id "${CLUSTER_ID}" \
        --file "${HOME}/.kube/config" \
        --region "${OCI_REGION}" \
        --token-version 2.0.0 \
        --kube-endpoint PRIVATE_ENDPOINT
  5. SSHトンネルのプライベート・エンドポイントIPアドレスを取得します。

    PRIVATE_ENDPOINT=$(oci ce cluster get \
        --cluster-id "${CLUSTER_ID}" \
        --query "data.endpoints.\"private-endpoint\"" \
        --raw-output)
    PRIVATE_IP="${PRIVATE_ENDPOINT%:*}"
    echo "Private endpoint IP: ${PRIVATE_IP}"
  6. 要塞ポート転送セッションを作成します。SSH公開鍵ファイルが必要です。

    SESSION_ID=$(oci bastion session create-port-forwarding \
        --bastion-id "${BASTION_ID}" \
        --target-private-ip "${PRIVATE_IP}" \
        --target-port 6443 \
        --session-ttl 10800 \
        --display-name "kubectl-tunnel" \
        --ssh-public-key-file ~/.ssh/id_rsa.pub \
        --query "data.id" \
        --raw-output)

    セッションがACTIVEになるまで待ってから、SSHコマンドを取得します。

    oci bastion session get \
        --session-id "${SESSION_ID}" \
        --query "data.{state:\"lifecycle-state\", ssh:\"ssh-metadata\".command}" 2>&1
  7. 別の端末を開き、前のステップのコマンドを使用してSSHトンネルを開始します。トンネルは、ローカル・ポート6443をプライベートKubernetes APIに転送します。

    ssh -i ~/.ssh/id_rsa -o IdentitiesOnly=yes -N -L 6443:<PRIVATE_IP>:6443 \
        -p 22 -o ServerAliveInterval=30 \
        <SESSION_OCID>@host.bastion.<REGION>.oci.oraclecloud.com

    ノート: <PRIVATE_IP><SESSION_OCID>および<REGION>を、前のステップの値に置き換えます。セッション中は、このターミナルを開いたままにします。-o IdentitiesOnly=yesフラグは、SSHエージェントに複数のキーがロードされている場合に「認証失敗が多すぎる」エラーを防止します。

  8. kubeconfigを更新して、ローカル・トンネルを介して接続します。

    CLUSTER_NAME_KUBE=$(kubectl config view --minify -o jsonpath='{.clusters[0].name}')
    kubectl config set-cluster "${CLUSTER_NAME_KUBE}" \
        --server=https://127.0.0.1:6443 \
        --insecure-skip-tls-verify=true

    ノート:クラスタ証明書が127.0.0.1ではなくプライベート・エンドポイントIPに対して発行されたため、--insecure-skip-tls-verifyフラグが必要です。トラフィックはSSHトンネルを介して暗号化されるため、これは安全です。

  9. デフォルト以外のOCI CLIプロファイル(API_KEY_AUTHなど)を使用している場合は、それを使用するようにkubeconfigを更新します。生成されたkubeconfigは、トークン生成用のDEFAULTプロファイルにデフォルト設定されます。

    kubectl config set-credentials \
        $(kubectl config view --minify -o jsonpath='{.users[0].name}') \
        --exec-env=OCI_CLI_PROFILE=${OCI_PROFILE}

    ヒント:ステップ6から9は、./entry_point.sh tunnelによって自動化されます。これは、ディスク拡張などの長時間実行操作中にSSHトンネルがドロップした場合にも自動再接続されます。別の端末で実行し、セッション中は実行したままにします。

  10. クラスタ・アクセスを確認します。

kubectl get nodes

GPUノード・プールがまだ追加されていないため、この時点で出力にはノードが表示されません。

No resources found

タスク5: GPUノード・プールの追加

GPUコンピュート・インスタンスを含むノード・プールをOKEクラスタに追加します。

  1. 最新のGPU互換OKEノード・イメージを検索します。OKEには、kubeletおよびノード登録コンポーネントが事前にインストールされている特定のイメージが必要です。node-pool-options APIを使用して、Kubernetesバージョンの正しいイメージを検索します。

    GPU_IMAGE_ID=$(oci ce node-pool-options get \
        --node-pool-option-id all \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --query "data.sources[?contains(\"source-name\", 'GPU') && contains(\"source-name\", 'OKE-${KUBERNETES_VERSION#v}') && contains(\"source-name\", '8.10')].\"image-id\" | [0]" \
        --raw-output)
    echo "GPU Image: ${GPU_IMAGE_ID}"

    ノート: Kubernetesバージョン(OKE-1.31.10など)と一致するOracle Linux 8.10 GPUイメージの問合せフィルタ。ARMベースのイメージが必要な場合は、8.10を適切なフィルタに置き換えます。

  2. GPUシェイプを持つ可用性ドメインを確認します。すべての可用性ドメインにGPU容量があるわけではありません。

    AD=$(oci iam availability-domain list \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --query "data[${GPU_AD_INDEX}].name" \
        --raw-output)
    echo "Availability Domain: ${AD}"

    ノート: GPU容量は、リージョンおよびアベイラビリティ・ドメインによって異なります。ノード・プールの作成が「ホスト容量不足」エラーで失敗した場合は、別の可用性ドメイン(GPU_AD_INDEX)またはGPUシェイプを試すか、通常のOCIプロセスを介して容量をリクエストします。

  3. 200 GBのブート・ボリュームを使用してGPUノード・プールを作成します。

    oci ce node-pool create \
        --compartment-id "${OCI_COMPARTMENT_ID}" \
        --cluster-id "${CLUSTER_ID}" \
        --name "${GPU_NODE_POOL_NAME:-gpu-pool}" \
        --kubernetes-version "${KUBERNETES_VERSION}" \
        --node-shape "${GPU_SHAPE}" \
        --node-image-id "${GPU_IMAGE_ID}" \
        --node-boot-volume-size-in-gbs "${GPU_BOOT_VOLUME_GB:-200}" \
        --size "${GPU_NODE_COUNT}" \
        --placement-configs "[{\"availabilityDomain\": \"${AD}\", \"subnetId\": \"${WORKER_SUBNET_ID}\"}]" \
        --initial-node-labels '[{"key": "app", "value": "gpu"}, {"key": "nvidia.com/gpu", "value": "true"}]'

    ノート:ノード・ラベルapp=gpuおよびnvidia.com/gpu=trueは、GPUノードで推論ポッドをスケジュールするために、後でvLLM Helmチャートによって使用されます。200 GBブート・ボリュームは、vLLMコンテナ・イメージ(最大10 GB)およびモデルの重みのための領域を提供しますが、ファイル・システムは使用する前に拡張する必要があります(タスク8を参照)。

  4. GPUノードが準備完了になるのを待ちます。これは通常、ノードがGPUドライバをプロビジョニング、ブート、インストールし、クラスタに登録する間に5~10分かかります。

    ノート: GPUインスタンスは容量制約の対象となります。ノード・プールがCREATING状態のままの場合は、OCIコンソールまたはoci ce node-pool getでノード・ステータスを確認します。「Out of host capacity(ホスト容量不足)」エラーは、その可用性ドメインで使用可能なGPUインスタンスがないことを意味します。これを解決するには、別の可用性ドメイン(GPU_AD_INDEX=0またはGPU_AD_INDEX=2)を試すか、別のGPUシェイプを試すか、OCIコンソールまたはサポート・チケットを使用して容量予約をリクエストします。

    kubectl get nodes -w

    ノードの準備が整ったら、期待される出力:

    NAME          STATUS   ROLES   AGE   VERSION
    10.0.10.x     Ready    node    5m    v1.31.10
  5. GPUがノードで検出されていることを確認します。

    kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPUs:.status.capacity.'nvidia\.com/gpu'

    予期される出力:

    NAME          GPUs
    10.0.10.x     1
  6. CoreDNSにパッチを適用して、GPUノードでスケジュールします。OKE GPUノードにはnvidia.com/gpu=present:NoSchedule taintがあります。GPUノードのみを持つクラスタでは、CoreDNSのようなシステム・ポッドは、このtaintに対する許容範囲なしでスケジュールできません。DNSがないと、ポッドは外部ホスト名を解決してモデルをダウンロードできません。

    kubectl patch deployment coredns -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}]'
    
    kubectl patch deployment kube-dns-autoscaler -n kube-system --type='json' \
      -p='[{"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}]'

    CoreDNSが実行されていることを確認します。

    kubectl get pods -n kube-system | grep coredns

    ノート:クラスタにシステム・ワークロード専用のCPUノード・プールがある場合、このステップは不要です。このパッチは、GPUノードがクラスタ内の唯一のノードである場合にのみ必要です。

    oci ce node-pool list --compartment-id "${OCI_COMPARTMENT_ID}" --cluster-id "${CLUSTER_ID}" \
        --query 'data[].{name:name,state:"lifecycle-state",shape:"node-shape",size:"node-config-details".size}' --output table

    ACTIVE状態のcpu-poolとgpu-poolを示すOCI CLI出力

タスク6: NVIDIAデバイス・プラグインのインストール

KubernetesがGPUでワークロードを検出してスケジュールできるように、NVIDIAデバイス・プラグインをインストールします。

  1. NVIDIAデバイス・プラグインDaemonSetを適用します。

    kubectl apply -f https://raw.githubusercontent.com/NVIDIA/k8s-device-plugin/v0.14.1/nvidia-device-plugin.yml
  2. プラグイン・ポッドの準備が完了するまで待ちます。

    kubectl wait --for=condition=Ready pods -l name=nvidia-device-plugin-ds -n kube-system --timeout=300s

    ノート:一部のOKE GPUノード・イメージには、事前インストールされたNVIDIAデバイス・プラグイン(nvidia-gpu-device-plugin)が含まれています。イメージにすでにそれが含まれている場合、アップストリームDaemonSetを適用すると、競合が発生しない2番目のインスタンスが作成されます。自動スクリプト(entry_point.sh deploy-vllm)は、ノード・イメージのバージョンに関係なくGPU検出が機能するように常にインストールします。

  3. GPUがKubernetesによって割り当て可能であることを確認します。

    kubectl get nodes -o=custom-columns=NAME:.metadata.name,GPUs:.status.allocatable.'nvidia\.com/gpu'

    予期される出力:

    NAME          GPUs
    10.0.10.x     1
  4. GPUノードのtaintを許容するようにCoreDNSにパッチを適用します。GPUノードが唯一のワーカー・ノードであるクラスタでは、OKE GPUノードにnvidia.com/gpu=present:NoSchedule taintがあるため、CoreDNSポッドをスケジュールできません。DNSがないと、ポッドはイメージ・レジストリを解決したり、ダウンロードURLをモデル化できません。

    kubectl patch deployment coredns -n kube-system --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations/-", "value": {"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}}
    ]'
    kubectl rollout status deployment/coredns -n kube-system --timeout=60s

    ノート:このステップは、GPUノードがクラスタ内の唯一のワーカー・ノードである場合にのみ必要です。システム・ワークロード専用のCPUノード・プールがある場合、CoreDNSはデフォルトでそこをスケジュールし、このパッチは不要です。

タスク7: ストレージの構成

OCI Block Volume StorageClassを適用して、モデルの重み付けに永続ストレージを提供します。

  1. StorageClass定義を適用します。

    kubectl apply -f oci-block-storage-sc.yaml

    このファイルは、次の2つのパフォーマンス層を定義します。

    apiVersion: storage.k8s.io/v1
    kind: StorageClass
    metadata:
      name: oci-block-storage-enc
    provisioner: blockvolume.csi.oraclecloud.com
    parameters:
      vpusPerGB: "10"
    reclaimPolicy: Delete
    volumeBindingMode: WaitForFirstConsumer
    allowVolumeExpansion: true
    StorageClass パフォーマンス ユース・ケース
    oci-block-storage-enc バランス(vpusPerGB: 10) デフォルトで、ほとんどのモデルでコスト効率が高い
    oci-block-storage-hp 高パフォーマンス(vpusPerGB: 20) より大規模なモデルへの迅速なモデル・ロード
  2. StorageClassesが使用可能であることを確認します。

    kubectl get storageclass

ノート:複数のポッドにまたがる共有ストレージを必要とするマルチノード・デプロイメントの場合は、ブロック・ボリュームではなく、ReadWriteManyアクセス・モードのOCI File Storage Service (NFS)を使用します。

タスク8: GPUノード・ファイルシステムの拡張

OCIブート・ボリュームには、指定したブート・ボリューム・サイズに関係なく、固定最大47 GBのパーティションがあります。vLLMコンテナ・イメージのみでは約10 GBであり、モデルの重みには追加の領域が必要です。DiskPressureの削除を回避するには、vLLMをデプロイする前にファイルシステムを拡張する必要があります。

ノート:これはOCI固有の要件です。ブート・ボリュームは200 GBにプロビジョニングされますが、オペレーティング・システムではデフォルトで最大47 GBのパーティションのみがプロビジョニングされます。残りの領域は手動で要求する必要があります。

  1. GPUノードの現在のファイル・システムのサイズを確認します。

    GPU_NODE=$(kubectl get nodes -l app=gpu -o jsonpath='{.items[0].metadata.name}')
    kubectl run check-disk --rm -i --restart=Never \
      --image=busybox:latest \
      --overrides="{\"spec\":{\"nodeName\":\"${GPU_NODE}\",\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"check\",\"image\":\"busybox:latest\",\"command\":[\"sh\",\"-c\",\"chroot /host df -h / | tail -1\"],\"securityContext\":{\"privileged\":true},\"volumeMounts\":[{\"name\":\"host\",\"mountPath\":\"/host\"}]}],\"volumes\":[{\"name\":\"host\",\"hostPath\":{\"path\":\"/\"}}]}}"

    出力には合計で約47 GBと表示され、拡張が必要であることが確認されます。

  2. 拡張コマンドを実行する権限付きポッドをGPUノードに作成します。

    kubectl run expand-disk --restart=Never \
      --image=busybox:latest \
      --overrides="{\"spec\":{\"nodeName\":\"${GPU_NODE}\",\"tolerations\":[{\"operator\":\"Exists\"}],\"containers\":[{\"name\":\"expand\",\"image\":\"busybox:latest\",\"command\":[\"sleep\",\"600\"],\"securityContext\":{\"privileged\":true},\"volumeMounts\":[{\"name\":\"host\",\"mountPath\":\"/host\"}]}],\"volumes\":[{\"name\":\"host\",\"hostPath\":{\"path\":\"/\"}}]}}"

    ポッドが起動するのを待ちます。

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. 単一のkubectl execコマンドで4つの拡張ステップをすべて実行します。これらを同時に実行すると、kubectl execがステップ間で終了コード137 (SIGKILL)を返すリスクが回避されます。これは、ホスト上の重いディスクI/O中に発生する可能性があります。

    kubectl exec expand-disk -- chroot /host bash -c '
      set -x
      growpart /dev/sda 3 || echo "growpart: partition may already be expanded"
      sleep 3
      pvresize /dev/sda3
      lvextend -l +100%FREE /dev/ocivolume/root || echo "lvextend: may already be extended"
      xfs_growfs /
      echo "EXPANSION_COMPLETE"
      df -h /
    '
    ステップ コマンド 目 的
    1 growpart /dev/sda 3 パーティション3を展開してフル・ディスクを使用します。
    2 pvresize /dev/sda3 LVM物理ボリュームのサイズ変更
    3 lvextend -l +100%FREE /dev/ocivolume/root 論理ボリュームの拡張
    4 xfs_growfs / XFSファイルシステムを拡張してボリュームをいっぱいにする

    ノート: 4つの操作はすべて冪等です。execが終了コード137を返した場合は、ブロック全体を再実行できます。出力でEXPANSION_COMPLETEを探し、成功を確認します。

  4. kubeletを再起動して、ノードが更新された割当て可能ストレージを報告し、検証してクリーン・アップします。

    kubectl exec expand-disk -- nsenter -t 1 -m -p -- systemctl restart kubelet 2>/dev/null \
        || echo "Warning: kubelet restart returned non-zero (non-critical)"
    kubectl exec expand-disk -- chroot /host df -h /
    kubectl delete pod expand-disk --force

    ノート: nsenterコマンドは、systemdにアクセスするためにホストのPIDネームスペースを入力します。プレーンchroot /host systemctl restart kubeletは、chroot内からsystemdバスに接続できないため、失敗します。

    予想される出力には、合計約189 GBが表示されます。

タスク9: vLLM本番スタックのデプロイ

Helmを使用してvLLM推論スタックをインストールします。

  1. vLLM Helmリポジトリを追加します。

    helm repo add vllm https://vllm-project.github.io/production-stack
    helm repo update
  2. Helm値ファイルをレビューします。production_stack_specification.yamlは、OCIのモデル、リソースおよびストレージを構成します。

    servingEngineSpec:
      runtimeClassName: ""
      modelSpec:
      - name: "gpt-oss"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "openai/gpt-oss-20b"
    
        replicaCount: 1
        requestCPU: 4
        requestMemory: "24Gi"
        requestGPU: 1
    
        # No HF token needed - Apache 2.0 licensed OpenAI open source model
    
        pvcStorage: "100Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          app: gpu
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.90"

    ノート: openai/gpt-oss-20bモデルは、エキスパートの混合(MoE)モデルで、フォワード・パスごとに20B合計パラメータおよび3.6Bアクティブ・パラメータがあります。Apache 2.0ライセンスでリリースされているため、Hugging Faceトークンは必要ありません。vllm/vllm-openaiコンテナ・イメージは、OpenAI互換APIサーバーを提供し、クライアントが自己ホスト・エンドポイントに対して標準のOpenAI SDKコールを使用できるようにします。

  3. スタックをデプロイします。ルーター・ポッドは次のステップでパッチが適用されるまでCrashLoopになるため、ここでは--waitを使用しないでください

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml

    vLLMエンジンポッドが開始されるまで待ちます(次にルータにパッチが適用されます)。

    kubectl wait --for=condition=Ready pods -l model=gpt-oss --timeout=600s

    ノート:エンジン・ポッドは、初回起動時にモデルの重みがダウンロードされるため、準備完了になるまでに数分かかります。ポッドがContainerCreatingにとどまる場合、コンテナ・イメージ(~10 GB)はまだプルされています。進行状況を確認するには、kubectl describe pod <pod-name>を使用します。

  4. ルータの配置にパッチを適用します。ルーターには、GPU許容値(GPUノードが容量を持つ唯一のノードである場合にスケジュールできるように)とメモリ制限(デフォルトの500 Miが OOMKillを引き起こす可能性がある)が必要です。

    kubectl patch deployment vllm-deployment-router --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations", "value": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}]},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "512Mi"},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "1Gi"}
    ]'

    ノート: OKE GPUノードには、非GPUワークロードのスケジューリングを妨げるnvidia.com/gpu=present:NoScheduleテイントがあるため、GPU許容範囲が必要です。ルータはGPUを使用せず、どこかで実行する必要があるため、この許容値によりGPUノードでスケジュールできます。専用CPUノード・プールがあるクラスタでは、この許容値は必要ありません。

  5. Helmリリースがデプロイされていることを確認します。

    helm list

    vllm-stackがデプロイされたことを示すヘルム・リストの端末出力

  6. ポッドが実行されていることを確認します。

    kubectl get pods

    予期される出力:

    NAME                                            READY   STATUS    RESTARTS   AGE
    vllm-deployment-router-xxxxxxxxxx-xxxxx          1/1     Running   0          5m
    vllm-gpt-oss-deployment-vllm-xxxxxxxxxx-xxxxx    1/1     Running   0          5m
    kubectl get pods

    kubectlはルーターとエンジンポッドの両方を表示するポッドを取得します1/1を実行中

  7. ポッド・ログのモデル・ロードの進行状況を確認します。

    kubectl logs -f deployment/vllm-gpt-oss-deployment-vllm

    モデルがロードされ、サーバーがリクエストを受け入れる準備ができていることを示すメッセージが表示されるまで待ちます。

タスク10: 推論エンドポイントのテスト

デプロイメントが推論リクエストを処理していることを検証します。vLLMプロダクション・スタックは、ルーター・サービスを介してOpenAI互換APIを公開するため、OpenAI SDKクライアントまたはcurlコマンドと対話できます。

次の図は、推論リクエストのライフサイクルを示しています。クライアント・リクエストからルーターのエンジン選択ロジック、vLLMエンジンのプリフィル・フェーズとデコード・フェーズ、およびストリームされたレスポンスとして戻ります。

クライアントからルーター、vLLMエンジン、GPUまでの推論リクエストのライフサイクルを示すシーケンス図

  1. 使用可能なモデルをリストして、デプロイメントが正常であることを確認します。

    kubectl get svc vllm-router-service

    ルーター・サービスは、デプロイされたすべてのモデルにAPIゲートウェイを提供します。クラスタはプライベート・エンドポイントを使用するため、kubectl port-forwardを介してサービスにアクセスします。

  2. ローカルマシンからルータサービスへのポート転送を開始します。新しいターミナルを開き(もう一方のターミナルで実行されているSSHトンネルを保持)、次を実行します:

    kubectl port-forward svc/vllm-router-service 8080:80

    これにより、マシンのlocalhost:8080が、クラスタ内のルーター・サービスのポート80にマップされます。

    セキュリティ・ノート: kubectl port-forwardはローカルでバインドされ、サービスを公開しません。これは、要塞トンネルを介してプライベート・クラスタを使用する際にテストする最も安全な方法です。

    ノート: port-forwardコマンドはフォアグラウンドで実行されます。テスト中は、この端末を開いたままにしておきます。完了したら、Ctrl+Cを押して停止します。

  3. 別の端末で、モデル・エンドポイントを問い合せて、モデルが使用可能であることを確認します。

    curl -s http://localhost:8080/v1/models | python3 -m json.tool

    期待される出力:

    {
        "object": "list",
        "data": [
            {
                "id": "openai/gpt-oss-20b",
                "object": "model",
                "created": 1234567890,
                "owned_by": "vllm"
            }
        ]
    }
  4. テキスト入力リクエストを送信します。

    curl -s http://localhost:8080/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "prompt": "Oracle Cloud Infrastructure is",
        "max_tokens": 50
      }' | python3 -m json.tool

    期待される出力(省略形):

    {
        "id": "cmpl-xxxxxxxxxxxx",
        "object": "text_completion",
        "model": "openai/gpt-oss-20b",
        "choices": [
            {
                "index": 0,
                "text": " a cloud computing platform that provides ...",
                "finish_reason": "length"
            }
        ],
        "usage": {
            "prompt_tokens": 5,
            "completion_tokens": 50,
            "total_tokens": 55
        }
    }
  5. チャット完了リクエストを送信します。これは、OpenAI Python SDKで使用されるものと同じ形式であり、プログラムでLLMと対話する最も一般的な方法です。

    curl -s http://localhost:8080/v1/chat/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "messages": [{"role": "user", "content": "What is Oracle Cloud Infrastructure in one sentence?"}],
        "max_tokens": 100
      }' | python3 -m json.tool

    期待される出力(省略形):

    {
        "id": "chatcmpl-xxxxxxxxxxxx",
        "object": "chat.completion",
        "model": "openai/gpt-oss-20b",
        "choices": [
            {
                "index": 0,
                "message": {
                    "role": "assistant",
                    "content": "Oracle Cloud Infrastructure (OCI) is Oracle's enterprise-grade cloud platform that provides a full range of services for building, deploying, and managing applications and workloads..."
                },
                "finish_reason": "length"
            }
        ],
        "usage": {
            "prompt_tokens": 71,
            "completion_tokens": 100,
            "total_tokens": 171
        }
    }

    どちらのレスポンスにも、choices配列内のモデルの生成されたテキスト、トークン使用状況統計、およびstop (モデルが自然に終了)またはlength (出力はmax_tokensで切り捨てられました)のいずれかのfinish_reasonが含まれます。

    ノート: APIリクエストのモデル名は、Helm値のmodelURLフィールドと一致するフル・モデル・パス(openai/gpt-oss-20b)です。OpenAI互換のクライアントは、base_urlhttp://localhost:8080/v1に設定することで、このエンドポイントを使用できます。

    openai/gpt-oss-20bからのカール・チャット完了リクエストおよびJSONレスポンスを示す端末出力

  6. 短いリクエストのエンドツーエンドのレイテンシを測定します。

    time curl -s http://localhost:8080/v1/chat/completions \
        -H "Content-Type: application/json" \
        -d '{"model":"openai/gpt-oss-20b","messages":[{"role":"user","content":"What is Kubernetes?"}],"max_tokens":50}' > /dev/null

    単一のA10 GPUでは、短い完了に対して1-3秒のエンドツーエンドが必要です。最初のトークンまでの時間(TTFT)は通常、プロンプトの長さに応じて50から200ミリ秒です。スループットを向上させるには、Helm値のreplicaCountを増やして、ルーターの背後にあるエンジン・レプリカを追加します。

タスク11: OCIロード・バランサを介した公開(オプション)

OCIロード・バランサを介して推論エンドポイントに外部からアクセスできるようにします。

セキュリティ・ノート:これにより、デフォルトで推論APIがパブリック・インターネットに公開されます。TLS、認証(APIキー/JWT/mTLS)、IP許可リストまたはWAFコントロールを使用しない本番では、これを有効にしないでください。公開する必要がある場合は、認証およびレート制限を強制するイングレス・コントローラまたはAPIゲートウェイで前面に配置するか、内部ロード・バランサを使用します。

  1. LoadBalancerタイプを使用するようにルーター・サービスにパッチを適用します。

    kubectl patch svc vllm-router-service \
      -p '{"spec": {"type": "LoadBalancer"}}'

    ノート:内部ロード・バランサが必要な場合は、OCI注釈をサービスに追加します(次の例)。これにより、エンドポイントはVCN内でプライベートに保たれます。

    kubectl annotate svc vllm-router-service \
      "service.beta.kubernetes.io/oci-load-balancer-internal"="true"
  2. 外部IPが割り当てられるのを待ちます。

    kubectl get svc vllm-router-service -w

    ロード・バランサがプロビジョニングされると、予期される出力:

    NAME                   TYPE           CLUSTER-IP     EXTERNAL-IP      PORT(S)
    vllm-router-service    LoadBalancer   10.96.x.x      129.xxx.xxx.xx   80:xxxxx/TCP
    kubectl get svc

    kubectlはルーターとエンジンのサービスを示すsvc出力を取得します

  3. 外部エンドポイントをテストします。

    curl http://<EXTERNAL-IP>/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
        "model": "openai/gpt-oss-20b",
        "prompt": "Hello from OCI!",
        "max_tokens": 50
      }'

タスク12: マルチGPUテンソル並列度の構成(拡張)

ベア・メタル・シェイプ上の複数のGPUにわたり、より大きなモデルをデプロイします。

Tensor Parallelismは、1つのノード上の複数のGPU間でモデルを分割します。これは、モデルのメモリー要件が単一のGPUを超える場合に必要です。たとえば、Meta Llama 3.1 70Bには約140 GBのGPUメモリーが必要ですが、これは単一のGPUの容量を超えていますが、8x A100 80 GBまたは8x H100 GPUにまたがります。

  1. Hugging Faceトークンを使用してKubernetesシークレットを作成します。Llama 3.1 70Bなどのゲート付きモデルでは、認証が必要です。

    kubectl create secret generic hf-token-secret \
      --from-literal=token=YOUR_HUGGINGFACE_TOKEN
  2. production_stack_specification.yamlをマルチGPU構成で更新します。

    servingEngineSpec:
      modelSpec:
      - name: "llama70b"
        repository: "vllm/vllm-openai"
        tag: "latest"
        modelURL: "meta-llama/Llama-3.1-70B-Instruct"
    
        replicaCount: 1
        tensorParallelSize: 8
    
        requestCPU: 32
        requestMemory: "256Gi"
        requestGPU: 8
    
        hf_token:
          secretName: "hf-token-secret"
          secretKey: "token"
    
        pvcStorage: "500Gi"
        pvcAccessMode:
          - ReadWriteOnce
        storageClass: "oci-block-storage-enc"
    
        nodeSelector:
          node.kubernetes.io/instance-type: "BM.GPU.H100.8"
        tolerations:
          - key: "nvidia.com/gpu"
            operator: "Exists"
            effect: "NoSchedule"
    
        extraArgs:
          - "--max-model-len=8192"
          - "--gpu-memory-utilization=0.95"
          - "--tensor-parallel-size=8"
  3. 更新された値でデプロイします。タスク9と同様に、--waitを使用しないでください。ルーターはパッチが適用されるまでCrashLoopになります。

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml
    kubectl wait --for=condition=Ready pods -l model=llama70b --timeout=900s

    次に、ルーターにパッチを適用して(タスク9、ステップ4と同じ)、次のことを確認します。

    kubectl patch deployment vllm-deployment-router --type='json' -p='[
      {"op": "add", "path": "/spec/template/spec/tolerations", "value": [{"key": "nvidia.com/gpu", "operator": "Exists", "effect": "NoSchedule"}]},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/requests/memory", "value": "512Mi"},
      {"op": "replace", "path": "/spec/template/spec/containers/0/resources/limits/memory", "value": "1Gi"}
    ]'
  4. ポッドが実行中で、すべてのGPUが使用中であることを確認します。

    kubectl get pods
    kubectl logs -f deployment/vllm-llama70b-deployment-vllm

ノート:マルチGPU構成をデプロイする前に、OKEクラスタに適切なベア・メタルGPUシェイプ(BM.GPU.H100.8など)を持つノード・プールがあることを確認してください。

タスク13: モデルに対するOCIオブジェクト・ストレージの使用(拡張)

Hugging Faceからダウンロードするのではなく、OCI Object Storageからモデルの重みをロードします。これは、プライベート・モデル、OCI内の高速ダウンロード、または外部インターネット・アクセスのない環境に役立ちます。

  1. モデルの重みをOCIオブジェクト・ストレージ・バケットにアップロードします。OCIコンソールで「ストレージ」「オブジェクト・ストレージ」にナビゲートし、まだバケットがない場合は作成します。

  2. バケットの事前認証済リクエスト(PAR) URLを作成します。OCIコンソールで、バケットを選択し、「事前認証済リクエスト」をクリックして、読取りアクセス権を持つ新規リクエストを作成します。

  3. PAR URLを使用するようにproduction_stack_specification.yamlを更新します。

    servingEngineSpec:
      modelSpec:
      - name: "custom-model"
        repository: "iad.ocir.io/YOUR_TENANCY/vllm-custom:latest"
        modelURL: "/models/custom-model"
    
        env:
          - name: BUCKET_PAR_URL
            value: "https://objectstorage.us-ashburn-1.oraclecloud.com/p/xxx/n/namespace/b/bucket/o"
          - name: MODEL_NAME
            value: "custom-model"
  4. 更新された値(--waitなし)でデプロイします。理由については、タスク9を参照してください)。

    helm upgrade -i \
        vllm vllm/vllm-stack \
        -f production_stack_specification.yaml
    kubectl wait --for=condition=Ready pods -l model=custom-model --timeout=600s
  5. ポッド・ログをチェックして、オブジェクト・ストレージからのモデルのロードを確認します。

    kubectl logs -f deployment/vllm-custom-model-deployment-vllm

タスク14: リソースのクリーン・アップ

継続料金を回避するために、デプロイされたすべてのリソースを削除します。

  1. クリーンアップ・スクリプトを使用してKubernetesリソースを削除します。

    cd production-stack/deployment_on_cloud/oci
    ./clean_up.sh

    これにより、Helmリリースがアンインストールされ、すべてのPersistentVolumeClaims、PersistentVolumesおよびカスタムvLLMリソースが削除されます。

  2. OKEクラスタおよびすべてのOCIネットワーキング・リソースを削除します。

    ./entry_point.sh cleanup

    これにより、次のリソースが順番に削除されます。

    • GPUノード・プール
    • OKEクラスタ
    • 要塞ホスト(作成されている場合)
    • サブネット(API、ワーカー、ロード・バランサ、要塞)
    • セキュリティ・リスト
    • サービス・ゲートウェイ、NAT Gatewayおよびインターネット・ゲートウェイ
    • ルート表
    • VCN
  3. 「開発者サービス」「Kubernetesクラスタ」および「ネットワーキング」「Virtual Cloud Networks」で、すべてのリソースがOCIコンソールで削除されていることを確認します。

    ./entry_point.sh cleanup

    完全なリソース分解を示すentry_point.shクリーンアップのターミナル出力

ノート:継続的な料金を回避するために、すべてのリソースが削除されていることを確認します。GPUインスタンスおよびブロック・ボリュームでは、アイドル状態でもコストがかかります。

次の手順

このチュートリアルでは、関数推論スタックをデプロイしました。本番ワークロードの場合は、次の機能改善を検討してください。

確認

その他の学習リソース

docs.oracle.com/learnの他のラボを調べるか、Oracle Learning YouTubeチャネルで無料のラーニング・コンテンツにアクセスしてください。また、Oracle Learning Explorerになるには、education.oracle.com/learning-explorerにアクセスしてください。

製品ドキュメントについては、Oracle Help Centerを参照してください。