주:

Oracle Kubernetes Engine(OKE)에 OpenAI vLLM 프로덕션 스택 배포

소개

프로덕션 워크로드를 위해 대규모 언어 모델(LLM)을 채택하는 조직은 타사 추론 API에 의존하거나 자체 호스팅된 추론 스택을 배포하는 등 중요한 인프라 의사 결정에 직면합니다. 자체 호스팅 배포는 전체 데이터 프라이버시 및 규제준수 제어, 네트워크 왕복을 제거하여 100밀리초 미만의 추론 대기 시간, 대규모 예측 가능한 비용, 벤더 종속 없이 모든 오픈 소스 모델을 미세 조정하고 서비스할 수 있는 자유 등 상당한 이점을 제공합니다.

그러나 프로덕션급 LLM 추론 스택을 처음부터 새로 구축하는 것은 복잡합니다. 이를 위해서는 GPU 인식 컨테이너 통합관리, 여러 모델 복제본 전반의 지능형 요청 라우팅, 대규모 모델 가중치를 위한 영구 스토리지, 지속적인 모니터링이 모두 안정적으로 통합 및 실행되어야 합니다.

Oracle Cloud Infrastructure는 AI 추론을 위한 다양한 경로를 제공합니다. OCI 생성형 AI 서비스는 테넌시에 격리된 전용 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 프로덕션 스택을 배포하는 과정을 안내합니다.

참고: 이 자습서에서는 OCI CLI를 사용하여 GPU 추론 배포에 필요한 OCI 클라우드 리소스의 전체 흐름을 파악하는 데 도움이 되는 리소스를 단계별로 프로비저닝합니다. 프로덕션 환경의 경우 반복 가능한 버전 제어 배포를 위해 Terraform 또는 OCI Resource Manager(Shepherd)를 사용하여 이 인프라를 코딩하는 것이 좋습니다.

서브넷, OKE 클러스터, GPU 노드 풀, vLLM POD 및 로드 밸런서가 포함된 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 상시 무료 워크로드가 아닙니다. 완료 시 항상 정리 단계를 실행하여 진행 중인 요금을 피하십시오.

주: 이 자습서에서는 OpenAI의 Apache 2.0 라이센스 모델인 openai/gpt-oss-20b를 배포합니다. Hugging Face 토큰이 필요하지 않습니다. Meta Llama 3.1과 같은 게이팅된 모델을 배포하려면 API 토큰이 포함된 Hugging Face 계정이 필요합니다.

작업 1: 환경 변수 구성

인프라를 배치하기 전에 필요한 OCI 구성을 설정합니다.

  1. OCI 콘솔에서 컴파트먼트 OCID를 찾습니다. ID 및 보안 > 구획으로 이동한 다음 대상 구획을 누르고 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 버전

    예를 들어, 두 개의 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 구성을 검토하고 모델 크기 요구사항에 따라 하나를 선택합니다.

    모양 GPU GPU 유형입니다. GPU 메모리 권장 대상
    VM.GPU.A10.1 1 엔비디아 A10 24 GB 7B–13B 매개변수 모델
    VM.GPU.A10.2 2 엔비디아 A10 48 GB 작은 모델과 평행한 텐서
    BM.GPU4.8 8 엔비디아 A100 40GB 320 GB 70B 모델, 비용 효율성
    BM.GPU.A100-v2.8 8 엔비디아 A100 80 GB 640 GB 70B+ 매개변수 모델
    BM.GPU.H100.8 8 엔비디아 H100 640 GB 최대 모델, RDMA 지원

    주: 베어메탈 구성(BM.*)은 가상화 오버헤드 없이 전용 하드웨어를 제공하고 다중 GPU 텐서 병렬화를 지원합니다. 가상 머신 구성(VM.*)은 소규모 모델에서 더 비용 효율적입니다.

    Note: This tutorial uses VM.GPU.A10.1 (single NVIDIA A10 with 24 GB GPU memory) to deploy openai/gpt-oss-20b, a Mixture of Experts (MoE) model with 3.6B active parameters that typically fits on a single A10 GPU. 고급 섹션에서는 Llama 3.1 70B과 같은 더 큰 모델에 대해 BM.GPU.H100.8를 사용하는 다중 GPU 구성을 보여줍니다.

작업 2: 자동화된 스크립트를 사용하여 배치(빠른 시작)

vLLM 프로덕션 스택에는 모든 OCI 리소스를 프로비저닝하고 단일 명령으로 추론 스택을 배포하는 자동화된 배포 스크립트가 포함되어 있습니다. 빠른 배포를 위해 이 접근 방식을 사용합니다. 작업 3 - 10은 프로세스를 사용자 정의하려는 사용자에 대해 각 단계를 개별적으로 다룹니다.

  1. vLLM 운용 스택 저장소를 복제합니다.

    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)의 경우 설정은 모든 기반구조를 만들고 단일 명령으로 vLLM 스택을 배치합니다. Helm 값 파일을 두번째 인수로 전달합니다.

    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 용량 제약이 적용됩니다. 스크립트가 "GPU 노드 대기 중" 루프에 15분 이상 머무르는 경우 선택한 가용성 도메인에서 GPU 구성을 사용하지 못할 수 있습니다. oci ce node-pool get가 있는 노드 풀 상태를 확인하고 "호스트 용량 부족" 오류를 찾습니다. 이를 해결하려면 ./entry_point.sh cleanup로 정리하고 다른 가용성 도메인(예: GPU_AD_INDEX=0 또는 GPU_AD_INDEX=2) 또는 다른 GPU 구성(예: GPU_SHAPE=VM.GPU.A10.2)으로 재배치하십시오.

주: 배치 스크립트는 상당한 비용이 발생하는 GPU 인스턴스를 사용합니다(단일 A10 GPU의 경우 하루 최대 50달러). 진행 중인 요금을 피하기 위해 완료되면 항상 ./entry_point.sh cleanup를 실행하십시오.

작업 3: VCN 및 네트워킹 생성

OKE 클러스터에 필요한 OCI 네트워크 인프라를 생성합니다. 여기에는 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 게이트웨이를 생성합니다.

    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에 액세스하기 위한 서비스 게이트웨이를 생성합니다. 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 게이트웨이 경로(컨테이너 이미지 풀링, 모델 다운로드)와 Oracle Services Network에 직접 액세스하기 위한 서비스 게이트웨이 경로가 있습니다. 서비스 게이트웨이 경로가 중요합니다. 그렇지 않으면 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 범위로 제한하고 서브넷당 별도의 보안 목록 또는 NSG를 선호하므로, 로드 밸런서 서브넷이 0.0.0.0/0에서 SSH를 허용하지 않습니다.

    보안 기본값: SSH를 공용 IP로 제한하고 배스천 서브넷에만 SSH 규칙을 연결하여 시작합니다. 작업자 서브넷에 Kubernetes POD/서비스 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, API/작업자 서브넷에 WORKER_SL_ID, 로드 밸런서 서브넷에 LB_SL_ID를 연결합니다.

    참고: GPU 작업자 노드가 클러스터에 등록하려면 Kubernetes POD CIDR(10.244.0.0/16) 및 서비스 CIDR(10.96.0.0/16) 규칙이 필요합니다. ICMP 유형 3 코드 4 규칙은 경로 MTU 검색을 사용으로 설정하여 패킷 단편화 문제를 방지합니다.

  7. 서브넷을 생성합니다. 클러스터에는 4개의 서브넷이 필요합니다. 하나는 Kubernetes API 엔드포인트, 하나는 작업자 노드, 하나는 로드 밸런서, 다른 하나는 전용 클러스터에 액세스하는 데 사용되는 배스천 호스트입니다.

    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에서 생성된 네트워킹 리소스를 사용하여 OKE에 관리형 Kubernetes 클러스터를 배치합니다. 클러스터의 프로비저닝 시간은 약 10분입니다. 이 자습서에서는 Kubernetes API 끝점에 대해 예약된 퍼블릭 IP를 사용하지 않는 프라이빗 클러스터(스크립트 기본값)를 생성합니다. API 서버는 공용 인터넷에 노출되지 않으므로 전용(private) 클러스터는 운용 작업 로드에 권장되는 접근 방식입니다.

  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 게이트웨이를 통해 인터넷에 액세스하여 컨테이너 이미지를 가져오고 모델을 다운로드합니다. 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

    kubectl get nodes 출력에서 Ready 노드 2개 표시

  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 블록을 대신 사용합니다.

    배스천이 ACTIVE가 될 때까지 기다립니다(약 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

    주: --insecure-skip-tls-verify 플래그는 클러스터 인증서가 127.0.0.1가 아닌 프라이빗 끝점 IP에 대해 발행되었기 때문에 필요합니다. 이는 트래픽이 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 노드 풀 추가

OKE 클러스터에 GPU 컴퓨트 인스턴스가 있는 노드 풀을 추가합니다.

  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 버전과 일치하는 Oracle Linux 8.10 GPU 이미지에 대한 질의 필터(예: OKE-1.31.10)입니다. 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. 200GB 부트 볼륨으로 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=gpunvidia.com/gpu=true는 나중에 vLLM Helm 차트에서 GPU 노드의 추론 POD 일정을 잡는 데 사용됩니다. 200GB 부트 볼륨은 vLLM 컨테이너 이미지(~10GB) 및 모델 가중치를 위한 공간을 제공하지만, 사용하기 전에 파일 시스템을 확장해야 합니다(작업 8 참조).

  4. GPU 노드가 준비될 때까지 기다립니다. 일반적으로 노드가 프로비저닝, 부트, GPU 드라이버 설치 및 클러스터에 등록하는 동안 5~10분 정도 걸립니다.

    주: GPU 인스턴스는 용량 제약 조건을 따릅니다. 노드 풀이 CREATING 상태로 유지되면 OCI 콘솔 또는 oci ce node-pool get에서 노드 상태를 확인합니다. "호스트 용량 부족" 오류는 해당 가용성 도메인에서 사용 가능한 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. GPU 노드에서 일정을 잡으려면 CoreDNS를 패치합니다. OKE GPU 노드에는 nvidia.com/gpu=present:NoSchedule 테인트가 있습니다. GPU 노드만 있는 클러스터에서는 CoreDNS와 같은 시스템 포드가 이 테인트에 대한 허용 없이 일정을 잡을 수 없습니다. DNS가 없으면 POD는 모델을 다운로드하기 위해 외부 호스트 이름을 분석할 수 없습니다.

    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를 적용하면 충돌이 발생하지 않는 두번째 인스턴스가 생성됩니다. 자동화된 스크립트(entry_point.sh deploy-vllm)는 노드 이미지 버전에 관계없이 GPU 감지가 작동하도록 항상 설치합니다.

  3. Kubernetes에서 GPU를 할당할 수 있는지 확인합니다.

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

    예상된 출력:

    NAME          GPUs
    10.0.10.x     1
  4. GPU 노드 테인트 허용을 위해 CoreDNS를 패치합니다. GPU 노드가 유일한 작업자 노드인 클러스터에서는 OKE GPU 노드에 nvidia.com/gpu=present:NoSchedule 테인트가 있기 때문에 CoreDNS 포드의 일정을 잡을 수 없습니다. DNS가 없으면 POD는 이미지 레지스트리 또는 모델 다운로드 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 블록 볼륨 StorageClass을 적용하여 모델 가중치에 대한 영구 스토리지를 제공합니다.

  1. StorageClass 정의를 적용합니다.

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

    이 파일은 두 개의 성능 계층을 정의합니다.

    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

주: 여러 POD에서 공유 스토리지가 필요한 다중 노드 배치의 경우 블록 볼륨 대신 ReadWriteMany 액세스 모드와 함께 OCI 파일 스토리지 서비스(NFS)를 사용하십시오.

작업 8: GPU 노드 파일 시스템 확장

OCI 부팅 볼륨에는 지정한 부팅 볼륨 크기에 관계없이 최대 47GB의 고정된 파티션이 있습니다. vLLM 컨테이너 이미지만으로는 약 10GB이며 모델 가중치에는 추가 공간이 필요합니다. DiskPressure 제거가 발생하지 않도록 하려면 vLLM을 배치하기 전에 파일 시스템을 확장해야 합니다.

참고: OCI 관련 요구 사항입니다. 부트 볼륨은 200GB로 프로비저닝되지만 운영 체제는 기본적으로 최대 47GB의 파티션만 분할합니다. 남은 공간은 수동으로 요구해야 합니다.

  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\":\"/\"}}]}}"

    출력에는 총 47GB가 표시되며 확장이 필요함을 확인합니다.

  2. GPU 노드에서 확장 명령을 실행할 권한 있는 POD를 만듭니다.

    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\":\"/\"}}]}}"

    POD가 시작될 때까지 기다립니다.

    kubectl wait --for=condition=Ready pod/expand-disk --timeout=60s
  3. 단일 kubectl exec 명령에서 네 개의 확장 단계를 모두 실행합니다. 이를 함께 실행하면 kubectl exec에서 호스트의 디스크 I/O가 많을 때 발생할 수 있는 단계 간 종료 코드 137(SIGKILL)을 반환할 위험이 없습니다.

    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 파일 시스템 확장

    주: 네 개의 작업은 모두 멱등 작업입니다. 실행 코드가 종료 코드 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 버스에 연결할 수 없으므로 실패합니다.

    예상 출력에는 총 189GB가 표시되어야 합니다.

작업 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 모델은 20B 총 매개변수와 순방향 전달당 3.6B 활성 매개변수가 있는 전문가 혼합(MoE) 모델입니다. Apache 2.0 라이센스로 릴리스되었으므로 Hugging Face 토큰이 필요하지 않습니다. vllm/vllm-openai 컨테이너 이미지는 클라이언트가 자체 호스트된 끝점에 대해 표준 OpenAI SDK 호출을 사용할 수 있는 OpenAI 호환 API 서버를 제공합니다.

  3. 스택을 배치합니다. 라우터 포드는 다음 단계에서 패치될 때까지 CrashLoop가 되므로 여기서 --wait를 사용하지 마십시오.

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

    vLLM 엔진 Pod가 시작될 때까지 기다립니다(다음에는 라우터에 패치가 적용됨).

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

    참고: 엔진 포드는 처음 시작할 때 모델 가중치를 다운로드하므로 Ready가 되려면 몇 분 정도 걸립니다. 포드가 ContainerCreating로 유지되면 컨테이너 이미지(~10GB)가 여전히 풀링됩니다. 진행률을 확인하려면 kubectl describe pod <pod-name>를 사용합니다.

  4. 라우터 배치에 패치를 적용합니다. 라우터에는 GPU 허용(GPU 노드가 용량이 있는 유일한 노드일 때 예약할 수 있음) 및 증가된 메모리 제한(기본값 500Mi로 인해 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 스택을 보여주는 helm 목록의 터미널 출력

  6. POD가 실행 중인지 확인합니다.

    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 get pods showing router and engine pods both 달리기 1/1

  7. Pod 로그에서 모델 로드 진행률을 확인합니다.

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

    모델이 로드되었으며 서버가 요청을 수락할 준비가 되었음을 알리는 메시지가 표시될 때까지 기다립니다.

작업 10: 추론 끝점 테스트

배포가 추론 요청을 제공하고 있는지 검증합니다. vLLM 프로덕션 스택은 라우터 서비스를 통해 OpenAI 호환 API를 노출하므로 모든 OpenAI SDK 클라이언트 또는 curl 명령이 이 API와 상호 작용할 수 있습니다.

다음 다이어그램은 클라이언트 요청에서 라우터의 엔진 선택 논리, 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의 curl 채팅 완료 요청 및 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-200ms입니다. 처리량을 높이려면 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에 더 큰 모델을 배포할 수 있습니다.

텐서 병렬화는 단일 노드의 여러 GPU에 모델을 분할합니다. 이는 모델의 메모리 요구 사항이 단일 GPU를 초과하는 경우에 필요합니다. 예를 들어, Meta Llama 3.1 70B에는 약 140GB의 GPU 메모리가 필요하며, 이는 단일 GPU의 용량을 초과하지만 8x A100 80GB 또는 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. Pod가 실행 중이고 모든 GPU가 사용 중인지 확인합니다.

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

주: 다중 GPU 구성을 배치하기 전에 OKE 클러스터에 적절한 베어메탈 GPU 구성(예: BM.GPU.H100.8)이 있는 노드 풀이 있는지 확인하십시오.

작업 13: 모델에 OCI Object Storage 사용(고급)

Hugging Face에서 다운로드하는 대신 OCI Object Storage에서 모델 가중치를 로드합니다. 이는 프라이빗 모델, OCI 내에서의 빠른 다운로드 또는 외부 인터넷 액세스가 없는 환경에 유용합니다.

  1. 모델 가중치를 OCI Object Storage 버킷에 업로드합니다. 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 없이 업데이트된 값으로 배치합니다. 그 이유는 Task 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. Pod 로그를 확인하여 Object Storage에서 모델 로드를 확인합니다.

    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 게이트웨이 및 인터넷 게이트웨이
    • 경로 테이블
    • VCN
  3. 개발자 서비스 > Kubernetes 클러스터네트워킹 > 가상 클라우드 네트워크 아래의 OCI 콘솔에서 모든 리소스가 제거되었는지 확인합니다.

    ./entry_point.sh cleanup

    전체 리소스 분석을 보여주는 entry_point.sh 정리 터미널 출력

주: 진행 중인 요금을 피하려면 모든 리소스가 삭제되어야 합니다. GPU 인스턴스 및 블록 볼륨은 유휴 상태에서도 비용이 발생합니다.

다음 작업

이 자습서에서는 기능 추론 스택을 배포했습니다. 운용 작업 로드의 경우 다음과 같은 향상된 기능을 고려하십시오.

승인

추가 학습 자원

docs.oracle.com/learn에서 다른 랩을 탐색하거나 Oracle Learning YouTube 채널에서 더 많은 무료 학습 콘텐츠에 액세스하세요. 또한 education.oracle.com/learning-explorer를 방문하여 Oracle Learning Explorer가 되십시오.

제품 설명서는 Oracle Help Center를 참조하십시오.