K3s + Nvidia Container Toolkit + vLLM
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가 바뀌기 때문입니다.
이를 해결하기 위해 Service와 Ingress를 사용합니다.
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