배포 자습서: Kubernetes에서 vLLM 실행하기

배포 자습서: Kubernetes에서 vLLM 실행하기 (Using Kubernetes)

Kubernetes에서 vLLM을 배포하면 머신러닝 모델을 확장성 있고 효율적으로 서빙할 수 있어요. 이 가이드는 네이티브 Kubernetes로 vLLM을 배포하는 과정을 처음부터 차근차근 안내해요.

배포 방식은 이것만 있는 게 아니에요. 다음 도구들로도 Kubernetes에 vLLM을 배포할 수 있어요:

CPU 배포 (Deployment with CPUs)

!!! note 여기서 CPU를 쓰는 건 데모·테스트 목적일 뿐이에요. 성능은 GPU와 비교할 수 없을 정도로 차이가 나요.

먼저 Hugging Face 모델을 내려받고 저장하기 위한 Kubernetes PVC와 Secret을 만들어요:

??? console "Config"

```bash
cat <<EOF |kubectl apply -f -
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: vllm-models
spec:
  accessModes:
    - ReadWriteOnce
  volumeMode: Filesystem
  resources:
    requests:
      storage: 50Gi
---
apiVersion: v1
kind: Secret
metadata:
  name: hf-token-secret
type: Opaque
stringData:
  token: "REPLACE_WITH_TOKEN"
EOF
```

여기서 token 필드는 Hugging Face 액세스 토큰을 담아요. 토큰 생성 방법은 Hugging Face 문서에서 확인할 수 있어요.

다음으로 vLLM 서버를 Kubernetes Deployment와 Service로 띄워요. 프로세서 아키텍처에 맞는 vLLM 이미지를 골라야 해요:

??? console "Config"

```bash
VLLM_IMAGE=public.ecr.aws/q9t5s3a7/vllm-cpu-release-repo:latest       # use this for x86_64
VLLM_IMAGE=public.ecr.aws/q9t5s3a7/vllm-arm64-cpu-release-repo:latest # use this for arm64
cat <<EOF |kubectl apply -f -
apiVersion: apps/v1
kind: Deployment
metadata:
  name: vllm-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app.kubernetes.io/name: vllm
  template:
    metadata:
      labels:
        app.kubernetes.io/name: vllm
    spec:
      containers:
      - name: vllm
        image: $VLLM_IMAGE
        command: ["/bin/sh", "-c"]
        args: [
          "vllm serve meta-llama/Llama-3.2-1B-Instruct"
        ]
        env:
        - name: HF_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-token-secret
              key: token
        ports:
          - containerPort: 8000
        volumeMounts:
          - name: llama-storage
            mountPath: /root/.cache/huggingface
      volumes:
      - name: llama-storage
        persistentVolumeClaim:
          claimName: vllm-models
---
apiVersion: v1
kind: Service
metadata:
  name: vllm-server
spec:
  selector:
    app.kubernetes.io/name: vllm
  ports:
  - protocol: TCP
    port: 8000
    targetPort: 8000
  type: ClusterIP
EOF
```

로그를 통해 vLLM 서버가 성공적으로 시작했는지 확인할 수 있어요(모델을 내려받는 데 몇 분 걸릴 수 있어요):

kubectl logs -l app.kubernetes.io/name=vllm
...
INFO:     Started server process [1]
INFO:     Waiting for application startup.
INFO:     Application startup complete.
INFO:     Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit)

GPU 배포 (Deployment with GPUs)

전제 조건: GPU가 있는 Kubernetes 클러스터가 실행 중이어야 해요.

  1. vLLM용 PVC, Secret, Deployment 생성하기

    PVC는 모델 캐시를 저장하는 데 쓰이며 선택 사항이에요. hostPath나 다른 스토리지 옵션을 써도 돼요:

    Yaml
    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
      name: mistral-7b
      namespace: default
    spec:
      accessModes:
      - ReadWriteOnce
      resources:
        requests:
          storage: 50Gi
      storageClassName: default
      volumeMode: Filesystem
    

    Secret도 선택 사항이고, gated 모델에 접근할 때만 필요해요. gated 모델을 쓰지 않는다면 이 단계는 건너뛰어도 돼요.

    apiVersion: v1
    kind: Secret
    metadata:
      name: hf-token-secret
      namespace: default
    type: Opaque
    stringData:
      token: "REPLACE_WITH_TOKEN"
    

    다음으로 vLLM 모델 서버를 실행할 Deployment 파일을 만들어요. 아래 예시는 Mistral-7B-Instruct-v0.3 모델을 배포해요.

    NVIDIA GPU와 AMD GPU 두 가지 예시가 있어요.

    NVIDIA GPU:

    Yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: mistral-7b
      namespace: default
      labels:
        app: mistral-7b
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: mistral-7b
      template:
        metadata:
          labels:
            app: mistral-7b
        spec:
          volumes:
          - name: cache-volume
            persistentVolumeClaim:
              claimName: mistral-7b
          # vLLM needs to access the host's shared memory for tensor parallel inference.
          - name: shm
            emptyDir:
              medium: Memory
              sizeLimit: "2Gi"
          containers:
          - name: mistral-7b
            image: vllm/vllm-openai:latest
            command: ["/bin/sh", "-c"]
            args: [
              "vllm serve mistralai/Mistral-7B-Instruct-v0.3 --trust-remote-code --enable-chunked-prefill --max-num-batched-tokens 1024"
            ]
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-token-secret
                  key: token
            ports:
            - containerPort: 8000
            resources:
              limits:
                cpu: "10"
                memory: 20G
                nvidia.com/gpu: "1"
              requests:
                cpu: "2"
                memory: 6G
                nvidia.com/gpu: "1"
            volumeMounts:
            - mountPath: /root/.cache/huggingface
              name: cache-volume
            - name: shm
              mountPath: /dev/shm
            livenessProbe:
              httpGet:
                path: /health
                port: 8000
              initialDelaySeconds: 60
              periodSeconds: 10
            readinessProbe:
              httpGet:
                path: /health
                port: 8000
              initialDelaySeconds: 60
              periodSeconds: 5
    

    AMD GPU:

    MI300X 같은 AMD ROCm GPU를 쓴다면 아래 deployment.yaml을 참고하세요.

    Yaml
    apiVersion: apps/v1
    kind: Deployment
    metadata:
      name: mistral-7b
      namespace: default
      labels:
        app: mistral-7b
    spec:
      replicas: 1
      selector:
        matchLabels:
          app: mistral-7b
      template:
        metadata:
          labels:
            app: mistral-7b
        spec:
          volumes:
          # PVC
          - name: cache-volume
            persistentVolumeClaim:
              claimName: mistral-7b
          # vLLM needs to access the host's shared memory for tensor parallel inference.
          - name: shm
            emptyDir:
              medium: Memory
              sizeLimit: "8Gi"
          hostNetwork: true
          hostIPC: true
          containers:
          - name: mistral-7b
            image: rocm/vllm:rocm6.2_mi300_ubuntu20.04_py3.9_vllm_0.6.4
            securityContext:
              seccompProfile:
                type: Unconfined
              runAsGroup: 44
              capabilities:
                add:
                - SYS_PTRACE
            command: ["/bin/sh", "-c"]
            args: [
              "vllm serve mistralai/Mistral-7B-v0.3 --port 8000 --trust-remote-code --enable-chunked-prefill --max-num-batched-tokens 1024"
            ]
            env:
            - name: HF_TOKEN
              valueFrom:
                secretKeyRef:
                  name: hf-token-secret
                  key: token
            ports:
            - containerPort: 8000
            resources:
              limits:
                cpu: "10"
                memory: 20G
                amd.com/gpu: "1"
              requests:
                cpu: "6"
                memory: 6G
                amd.com/gpu: "1"
            volumeMounts:
            - name: cache-volume
              mountPath: /root/.cache/huggingface
            - name: shm
              mountPath: /dev/shm
    

    전체 예시와 샘플 yaml 파일은 https://github.com/ROCm/k8s-device-plugin/tree/master/example/vllm-serve에서 볼 수 있어요.

  2. vLLM용 Kubernetes Service 만들기

    mistral-7b Deployment를 노출할 Kubernetes Service 파일을 만들어요:

    Yaml
    apiVersion: v1
    kind: Service
    metadata:
      name: mistral-7b
      namespace: default
    spec:
      ports:
      - name: http-mistral-7b
        port: 80
        protocol: TCP
        targetPort: 8000
      # The label selector should match the deployment labels & it is useful for prefix caching feature
      selector:
        app: mistral-7b
      sessionAffinity: None
      type: ClusterIP
    
  3. 배포하고 테스트하기

    kubectl apply -f <filename>로 deployment와 service 구성을 적용하세요:

    kubectl apply -f deployment.yaml
    kubectl apply -f service.yaml
    

    배포를 테스트하려면 다음 curl 명령을 실행해요:

    curl http://mistral-7b.default.svc.cluster.local/v1/completions \
      -H "Content-Type: application/json" \
      -d '{
            "model": "mistralai/Mistral-7B-Instruct-v0.3",
            "prompt": "San Francisco is a",
            "max_tokens": 7,
            "temperature": 0
          }'
    

    서비스가 올바르게 배포되었다면 vLLM 모델로부터 응답을 받을 수 있어요.

gRPC 서빙 (Serving with gRPC)

vLLM은 --grpc 플래그를 넘기면 HTTP 대신 gRPC로 모델을 서빙할 수 있어요. 이를 위해선 선택적 gRPC 의존성이 필요해요:

pip install vllm[grpc]

--grpc를 쓰면 서버는 표준 gRPC Health Checking Protocol(grpc.health.v1.Health)을 노출해요. 이는 Kubernetes 1.24부터 지원되는 네이티브 gRPC 프로브와 통합돼요.

gRPC로 배포하려면 vllm serve 명령에 --grpc를 추가하고, httpGet 프로브를 grpc 프로브로 바꾸세요:

containers:
- name: mistral-7b
  image: vllm/vllm-openai:latest
  command: ["/bin/sh", "-c"]
  args: [
    "pip install vllm[grpc] && vllm serve mistralai/Mistral-7B-Instruct-v0.3 --grpc --port 50051 --trust-remote-code"
  ]
  ports:
  - containerPort: 50051
  livenessProbe:
    grpc:
      port: 50051
    initialDelaySeconds: 120
    periodSeconds: 10
  readinessProbe:
    grpc:
      port: 50051
    initialDelaySeconds: 120
    periodSeconds: 5

!!! note gRPC 헬스 서비스는 프로브가 실행될 때마다 엔진 상태를 확인해요. 엔진이 unhealthy하거나 서버가 종료되는 중이면 프로브는 NOT_SERVING을 반환해요.

grpcurl로 헬스 서비스를 직접 확인할 수도 있어요:

grpcurl -plaintext localhost:50051 grpc.health.v1.Health/Check

트러블슈팅 (Troubleshooting)

Startup/Readiness 프로브 실패, 컨테이너 로그에 "KeyboardInterrupt: terminated"

서버 시작에 필요한 시간보다 startup·readiness 프로브의 failureThreshold가 너무 낮으면 Kubernetes 스케줄러가 컨테이너를 죽일 수 있어요. 이런 일이 일어났음을 알려주는 신호는 두 가지예요:

  1. 컨테이너 로그에 "KeyboardInterrupt: terminated"가 있다
  2. kubectl get eventsContainer $NAME failed startup probe, will be restarted 메시지가 보인다

이를 완화하려면 failureThreshold를 높여 모델 서버가 서빙을 시작할 시간을 더 줘요. 이상적인 failureThreshold를 찾으려면 매니페스트에서 프로브를 제거한 뒤, 모델 서버가 서빙 준비가 될 때까지 걸리는 시간을 측정하면 돼요.

마무리 (Conclusion)

Kubernetes로 vLLM을 배포하면 GPU 자원을 활용해 ML 모델을 효율적으로 확장·관리할 수 있어요. 위 단계를 따라 하면 자신의 Kubernetes 클러스터 안에서 vLLM 배포를 설정하고 테스트할 수 있을 거예요. 문제가 생기거나 제안이 있다면 문서에 기여해 주셔도 좋아요.

출처: 공식문서

더 알아보기 (Learn more)