1. Installation

1.1 Install K3s

curl -sfL https://get.k3s.io | sh -

# 이후 설정
mkdir -p ~/.kube
sudo cp /etc/rancher/k3s/k3s.yaml ~/.kube/config
sudo chown $(id -u):$(id -g) ~/.kube/config
chmod 600 ~/.kube/config
export KUBECONFIG=$HOME/.kube/config

.bashrc 에 저장

# K3s
export KUBECONFIG=$HOME/.kube/config

1.2 Install Helm

helm은 package manager로서 apt, yum, brew 같은 것

Ubuntu 설치시

# Snap 으로 설치시
sudo snap install helm --classic

# Apt 로 설치시 (위에 걸로 하면됨)
sudo apt-get install curl gpg apt-transport-https --yes
curl -fsSL https://packages.buildkite.com/helm-linux/helm-debian/gpgkey | gpg --dearmor | sudo tee /usr/share/keyrings/helm.gpg > /dev/null
echo "deb [signed-by=/usr/share/keyrings/helm.gpg] https://packages.buildkite.com/helm-linux/helm-debian/any/ any main" | sudo tee /etc/apt/sources.list.d/helm-stable-debian.list
sudo apt-get update
sudo apt-get install helm

설치 확인

helm version
helm list

1.3 Nvidia Container Tookit

K3s는 기본적으로 containerd를 사용합니다.
따라서 “Docker 컨테이너에서 GPU 사용”과 “K3s Pod에서 GPU 사용”은 설정 위치가 다릅니다.

# Install prerequisites
$ sudo apt-get update && sudo apt-get install -y --no-install-recommends curl gnupg2

# Configure the production repository
$ curl -fsSL https://nvidia.github.io/libnvidia-container/gpgkey | sudo gpg --dearmor -o /usr/share/keyrings/nvidia-container-toolkit-keyring.gpg \
  && curl -s -L https://nvidia.github.io/libnvidia-container/stable/deb/nvidia-container-toolkit.list | \
    sed 's#deb https://#deb [signed-by=/usr/share/keyrings/nvidia-container-toolkit-keyring.gpg] https://#g' | \
    sudo tee /etc/apt/sources.list.d/nvidia-container-toolkit.list

# Install
$ sudo apt-get update

# 설치
$ export NVIDIA_CONTAINER_TOOLKIT_VERSION=1.18.1-1
  sudo apt-get install -y \
      nvidia-container-toolkit=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      nvidia-container-toolkit-base=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container-tools=${NVIDIA_CONTAINER_TOOLKIT_VERSION} \
      libnvidia-container1=${NVIDIA_CONTAINER_TOOLKIT_VERSION}

아래처럼 설정하면 Docker Container 가 Nvidia GPU 를 사용 하도록 설정해 줍니다.
nvidia-ctk runtime configure --runtime=docker 실행시 /etc/docker/daemon.json 에 세팅값이 설정됩니다

# Docker 에서 GPU 사용 가능하게 해줌
# Docker runtime config
sudo nvidia-ctk runtime configure --runtime=docker
sudo systemctl restart docker

# Verify (Docker)
docker run --rm --gpus all nvidia/cuda:12.4.1-base-ubuntu22.04 nvidia-smi

Containerd 에서 GPU 사용은 다음과 같이 합니다.
K3s는 Containerd 를 사용 (반드시 설치 해야 함)

# K3s 에서 GPU 사용 가능하게 해줌 (K3s 는 Containerd 사용)
sudo nvidia-ctk runtime configure \
  --runtime=containerd \
  --config=/var/lib/rancher/k3s/agent/etc/containerd/config.toml

sudo systemctl restart containerd
sudo systemctl restart k3s
kubectl rollout restart daemonset nvdp-nvidia-device-plugin -n nvidia-device-plugin

1.4 Nvidia Device Plugin + GPU Feature Discovery (GFD)

Kubernetes 에서 GPU를 자원(resource)로 인식하게 만드는 컴포넌트
cpu, memory 지정은 기본적으로 지원되는데, gpu: 2 이런건 Nvidia device plugin을 설치해야지 됨

RuntimeClass 생성

K3s의 팟이 nvidia-container-runtime을 사용하도록 RuntimeClass를 먼저 생성합니다.

cat <<EOF | kubectl apply -f -
apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: nvidia
handler: nvidia
EOF

실제 설치

(여기서는 helm 으로 설치 방법을 알려줌)

  • gfd.enabled=true
    • GPU Feature Discovery 활성화
    • GPU가 존재하는 노드에 자동으로 라벨 부착
    • nodeAffinity 조건을 만족시켜 Device Plugin DaemonSet이 정상적으로 실행됨
helm repo add nvdp https://nvidia.github.io/k8s-device-plugin
helm repo update

# 어떤 버젼 있는지 확인
helm search repo nvdp --devel

# 위에서 확인한 버젼을 사용해서 설치/업그레이드
helm upgrade -i nvdp nvdp/nvidia-device-plugin \
  --namespace nvidia-device-plugin \
  --create-namespace \
  --set gfd.enabled=true \
  --set runtimeClassName=nvidia \
  --version 0.18.0

설치 확인

# DaemonSet 확인
$ kubectl get pods -n nvidia-device-plugin -o wide
NAME                                                    READY   STATUS    RESTARTS        AGE    IP           NODE                   NOMINATED NODE   READINESS GATES
nvdp-node-feature-discovery-gc-6476cc6bf4-t655p         1/1     Running   0               55m    10.42.0.23   anderson-ubuntu-3090   <none>           <none>
nvdp-node-feature-discovery-master-58788687cc-tzl9v     1/1     Running   0               55m    10.42.0.22   anderson-ubuntu-3090   <none>           <none>
nvdp-node-feature-discovery-worker-849zk                1/1     Running   1 (2m25s ago)   55m    10.42.0.26   anderson-ubuntu-3090   <none>           <none>
nvdp-nvidia-device-plugin-gpu-feature-discovery-m2fz8   1/1     Running   0               2m4s   10.42.0.34   anderson-ubuntu-3090   <none>           <none>
nvdp-nvidia-device-plugin-prt9m                         1/1     Running   0               2m4s   10.42.0.33   anderson-ubuntu-3090   <none>           <none>

$ kubectl get ds -n nvidia-device-plugin
NAME                                              DESIRED   CURRENT   READY   UP-TO-DATE   AVAILABLE   NODE SELECTOR                 AGE
nvdp-node-feature-discovery-worker                1         1         1       1            1           <none>                        72m
nvdp-nvidia-device-plugin                         1         1         1       1            1           <none>                        72m
nvdp-nvidia-device-plugin-gpu-feature-discovery   1         1         1       1            1           <none>                        72m
nvdp-nvidia-device-plugin-mps-control-daemon      0         0         0       0            0           nvidia.com/mps.capable=true   72m

1.5 Kubernetes Operation Tools

k9s

  • 터미널용 툴
wget https://github.com/derailed/k9s/releases/latest/download/k9s_Linux_amd64.tar.gz
tar -xzf k9s_Linux_amd64.tar.gz
sudo mv k9s /usr/local/bin/

2. vLLM

2.1 vllm namespace

kubectl create ns vllm || true

2.1 Persistent Volume 생성

PVC 생성

  • k3s 디렉토리 만들고 그 안에다가 다음의 파일들을 생성합니다.
cat <<'EOF' | kubectl apply -f -
# pvc-gpt-oss-20b.yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
  name: gpt-oss-20b
  namespace: vllm
spec:
  accessModes:
    - ReadWriteOnce
  resources:
    requests:
      storage: 50Gi  # Adjust based on model size
EOF

확인은 다음과 같이 합니다.

kubectl get pvc -n vllm

2.2 Secret 생성 (Hugging Face Token)

Gated model이거나 인증이 필요한 경우 Secret 설정이 필수입니다. CreateContainerConfigError가 발생한다면 이 설정이 빠졌는지 확인하세요.

# 터미널에서 직접 생성하는 것이 편합니다.
kubectl create secret generic hf-token-secret \
  -n vllm \
  --from-literal=token="YOUR_HUGGINGFACE_TOKEN"

2.3 Deploy vLLM

실제 vLLM을 배포합니다.
vLLM의 주요 옵션들을 정리하면 다음과 같습니다.

Option Description Usage
--model 사용할 모델의 이름 또는 경로 vllm serve facebook/opt-125m
--tensor-parallel-size, -tp Tensor Parallelism을 위한 GPU 개수 -tp 2
--pipeline-parallel-size, -pp Pipeline Parallelism을 위한 GPU 개수 -pp 2
--gpu-memory-utilization GPU 메모리 사용률 (기본값 0.9) --gpu-memory-utilization 0.95
--max-model-len 모델의 최대 컨텍스트 길이 제한 --max-model-len 4096
--trust-remote-code 모델의 커스텀 코드 실행 허용 (필요시) --trust-remote-code
--dtype 데이터 타입 설정 (auto, float16, bfloat16 등) --dtype bfloat16
--quantization, -q 양자화 설정 (awq, gptq, bitsandbytes 등) -q awq
--enforce-eager CUDA Graph 대신 Eager 모드 사용 (디버깅용) --enforce-eager
--served-model-name API에서 노출될 모델 이름 변경 --served-model-name my-gpt
--api-key API 접속을 위한 API Key 설정 --api-key mysecret
--enable-lora LoRA 어댑터 사용 활성화 --enable-lora
# vllm-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: gpt-oss-20b
  namespace: vllm
  labels:
    app: gpt-oss-20b
spec:
  replicas: 1
  selector:
    matchLabels:
      app: gpt-oss-20b
  template:
    metadata:
      labels:
        app: gpt-oss-20b
    spec:
      runtimeClassName: nvidia  # RuntimeClass 추가 (K3s에서 필수)
      volumes:
      - name: cache-volume
        persistentVolumeClaim:
          claimName: gpt-oss-20b
      # vLLM needs to access the host's shared memory for tensor parallel inference.
      - name: shm
        emptyDir:
          medium: Memory
          sizeLimit: "8Gi"
      containers:
      - name: gpt-oss-20b
        image: vllm/vllm-openai:latest
        command: ["/bin/sh", "-c"]
        args: [
          "vllm serve openai/gpt-oss-20b \
            --trust-remote-code \
            --enable-chunked-prefill \
            --max-model-len 4096 \
            --gpu-memory-utilization 0.90 \
            --dtype auto"
        ]
        env:
        - name: HUGGING_FACE_HUB_TOKEN
          valueFrom:
            secretKeyRef:
              name: hf-token-secret
              key: token
        ports:
        - containerPort: 8000
        resources:
          limits:
            cpu: "8"
            memory: 32Gi
            nvidia.com/gpu: "1"
          requests:
            cpu: "2"
            memory: 16Gi
            nvidia.com/gpu: "1"
        volumeMounts:
        - mountPath: /root/.cache/huggingface
          name: cache-volume
        - name: shm
          mountPath: /dev/shm
        livenessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 300
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8000
          initialDelaySeconds: 300
          periodSeconds: 5

설치가 완료되었으면 실제로 API가 동작하는지 테스트해 봅니다.
K3s 환경에서는 Cluster IP에 직접 접근이 가능하므로, Pod IP를 확인하여 바로 테스트할 수 있습니다.

먼저 Pod의 IP를 확인합니다.

$ kubectl get pod -n vllm -o wide

NAME                           READY   STATUS    RESTARTS   AGE   IP            NODE                   NOMINATED NODE   READINESS GATES
gpt-oss-20b-6b44fc66b6-kkr8t   1/1     Running   0          28m   10.42.0.114   anderson-ubuntu-3090   <none>           <none>

출력된 IP를 확인한 후 curl 명령어로 health 엔드포인트를 호출합니다.

# 예: IP가 10.42.0.114 인 경우
curl http://10.42.0.114:8000/health -v

아무런 내용 없이 200 OK 응답이 오면 정상적으로 실행 중인 상태입니다.

2.4 Production-Level Access (Ingress & Service)

앞서 Test 단계에서는 Pod IP로 직접 테스트했지만,
실제 운영 환경(Production)에서는 절대 Pod IP를 직접 사용하지 않습니다.
Pod는 언제든 죽었다 살아나며 IP가 바뀌기 때문입니다.

이를 해결하기 위해 ServiceIngress를 사용합니다.

1) Service (Load Balancer)

Service는 쉽게 말해 “내부 로드 밸런서” 입니다.

  • 여러 개의 Pod(replica)가 떠 있을 때, 이들을 하나의 고정된 IP(ClusterIP) 로 묶어줍니다.
  • 트래픽이 들어오면 살아있는 Pod들 중 하나로 부하 분산(Load Balancing) 을 해줍니다.
  • 즉, Pod가 죽고 새로 태어나도 Service의 주소는 변하지 않습니다.
# vllm-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: gpt-oss-20b-service
  namespace: vllm
spec:
  selector:
    app: gpt-oss-20b  # Deployment의 label과 일치해야 함
  ports:
    - protocol: TCP
      port: 80        # Service가 노출할 포트
      targetPort: 8000 # 실제 Pod가 떠 있는 포트
  type: ClusterIP     # 외부 노출 없이 내부 전용
$ kubectl get service -n vllm

NAME                  TYPE        CLUSTER-IP     EXTERNAL-IP   PORT(S)   AGE
gpt-oss-20b-service   ClusterIP   10.43.61.210   <none>        80/TCP    81s

$ curl httP://10.43.61.210/health  -v

2) Ingress (Gateway)

Ingress는 클러스터 “외부에서 들어오는 대문” 역할을 합니다.

  • llm.incredible.ai 처럼 도메인 주소를 보고 적절한 Service로 연결해 줍니다.
  • SSL/TLS 인증서 처리나 라우팅 규칙을 담당합니다.
# vllm-ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: gpt-oss-20b-ingress
  namespace: vllm
spec:
  rules:
  - host: llm.incredible.ai  # 1. 사용자가 이 도메인으로 접속하면
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: gpt-oss-20b-service # 2. 이 서비스(내부 로드밸런서)로 보냄
            port:
              number: 80

3) Local Test (가짜 도메인 사용)

로컬 개발 환경에서는 llm.incredible.ai 같은 도메인이 실제 인터넷에 없으므로, 내 컴퓨터가 이를 인식하도록 속여야 합니다.

Linux/Mac의 경우 /etc/hosts 파일을 수정합니다.

# /etc/hosts 파일 수정
sudo vim /etc/hosts

# 맨 아래에 다음 줄 추가 (K3s가 설치된 로컬 IP)
127.0.0.1 llm.incredible.ai

이제 주소창이나 터미널에서 진짜 도메인처럼 호출할 수 있습니다.

# 이제 IP 대신 도메인으로 호출 가능!
curl http://llm.incredible.ai/health

Uninstall

helm uninstall nvdp -n nvidia-device-plugin
kubectl delete namespace nvidia-device-plugin

확인

bash kubectl get pods -A | grep -i nvidia || true kubectl get ds -A | grep -i nvidia || true