kind 로 구성한 kubernetes 에서 vLLM을 이용하여 LLM 서빙 실습을 진행하였습니다.
하지만 결국 구성에 실패하였고 실패한 이유는 마지막에 기술하겠습니다.
1. 실습 환경
- OS : ubuntu desktop 24.04
- 인텔 CPU, 16G RAM
- Nvidia Tesla P40 GPU, vRAM 24G
- docker 설치 완료
- NVIDIA Container Toolkit 설치 완료
| yu3papa@p40:~$ hostnamectl Static hostname: p40 Icon name: computer-desktop Chassis: desktop 🖥️ Machine ID: bafe33c24e6e4363b01917d32c70d7a3 Boot ID: ac303dc2485f4595b918976a3d491634 Operating System: Ubuntu 24.04.4 LTS Kernel: Linux 7.0.0-28-generic Architecture: x86-64 Hardware Vendor: Gigabyte Technology Co., Ltd. Hardware Model: H110M-DS2V Firmware Version: F22 Firmware Date: Tue 2017-11-07 Firmware Age: 8y 9month 1w yu3papa@p40:~$ nvidia-smi Sat Aug 15 15:38:38 2026 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 580.173.02 Driver Version: 580.173.02 CUDA Version: 13.0 | +-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 Tesla P40 On | 00000000:01:00.0 Off | Off | | N/A 46C P0 53W / 250W | 473MiB / 24576MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | No running processes found | +-----------------------------------------------------------------------------------------+ # docker의 디폴트 런타임을 nvidia 로 설정 yu3papa@p40:~$ sudo nvidia-ctk runtime configure --runtime=docker --set-as-defaultt INFO[0000] Loading config from /etc/docker/daemon.json INFO[0000] Wrote updated config to /etc/docker/daemon.json INFO[0000] It is recommended that docker daemon be restarted. yu3papa@p40:~$ sudo nvidia-ctk config \ --set accept-nvidia-visible-devices-as-volume-mounts=true \ --in-place yu3papa@p40:~$ cat /etc/docker/daemon.json { "default-runtime": "nvidia", "runtimes": { "nvidia": { "args": [], "path": "nvidia-container-runtime" } } } yu3papa@p40:~$ sudo systemctl restart docker |
2. kind 를 이용하여 kubernetes 클러스터를 구성하고 GPU 를 사용가능하게 설정
사전 조건 확인
| yu3papa@p40:~$ docker info --format '{{.DefaultRuntime}}' nvidia yu3papa@p40:~$ grep accept-nvidia /etc/nvidia-container-runtime/config.toml accept-nvidia-visible-devices-as-volume-mounts = true #accept-nvidia-visible-devices-envvar-when-unprivileged = true |
kind-gpu.yaml 파일 생성
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
extraMounts:
# GPU 전체를 노드에 주입
- hostPath: /dev/null
containerPath: /var/run/nvidia-container-devices/all
# NVIDIA Container Toolkit 을 노드 안으로
- {hostPath: /usr/bin/nvidia-container-runtime, containerPath: /usr/bin/nvidia-container-runtime}
- {hostPath: /usr/bin/nvidia-container-runtime-hook, containerPath: /usr/bin/nvidia-container-runtime-hook}
- {hostPath: /usr/bin/nvidia-container-cli, containerPath: /usr/bin/nvidia-container-cli}
- {hostPath: /usr/bin/nvidia-container-toolkit, containerPath: /usr/bin/nvidia-container-toolkit}
- {hostPath: /usr/bin/nvidia-ctk, containerPath: /usr/bin/nvidia-ctk}
- {hostPath: /usr/lib/x86_64-linux-gnu/libnvidia-container.so.1.20.0, containerPath: /usr/lib/x86_64-linux-gnu/libnvidia-container.so.1.20.0}
- {hostPath: /usr/lib/x86_64-linux-gnu/libnvidia-container-go.so.1.20.0, containerPath: /usr/lib/x86_64-linux-gnu/libnvidia-container-go.so.1.20.0}
- {hostPath: /etc/nvidia-container-runtime/config.toml, containerPath: /etc/nvidia-container-runtime/config.toml}
extraPortMappings:
- {containerPort: 30005, hostPort: 30005, listenAddress: "0.0.0.0"} # Ray Serve
- {containerPort: 30006, hostPort: 30006, listenAddress: "0.0.0.0"} # Ray Dashboard
kind 를 이용하여 kubernetes 클러스터 구성
| yu3papa@p40:~$ kind create cluster --name llmso --config kind-gpu.yaml Creating cluster "llmso" ... ✓ Ensuring node image (kindest/node:v1.36.1) 🖼 ✓ Preparing nodes 📦 ✓ Writing configuration 📜 ✓ Starting control-plane 🕹️ ✓ Installing CNI 🔌 ✓ Installing StorageClass 💾 Set kubectl context to "kind-llmso" You can now use your cluster with: kubectl cluster-info --context kind-llmso Not sure what to do next? 😅 Check out https://kind.sigs.k8s.io/docs/user/quick-start/ yu3papa@p40:~$ kubectl get po -A NAMESPACE NAME READY STATUS RESTARTS AGE kube-system coredns-589f44dc88-xchqk 1/1 Running 0 18s kube-system coredns-589f44dc88-xmprs 1/1 Running 0 18s kube-system etcd-llmso-control-plane 1/1 Running 0 26s kube-system kindnet-m8nwk 1/1 Running 0 18s kube-system kube-apiserver-llmso-control-plane 1/1 Running 0 26s kube-system kube-controller-manager-llmso-control-plane 1/1 Running 0 23s kube-system kube-proxy-kj5z6 1/1 Running 0 18s kube-system kube-scheduler-llmso-control-plane 1/1 Running 0 23s local-path-storage local-path-provisioner-855c7b7774-q95mj 1/1 Running 0 18s |
쿠버네티스 노드안의 containerd 데몬에 nvidia 런타임 등록
| yu3papa@p40:~$ docker container exec llmso-control-plane \ nvidia-ctk runtime configure --runtime=containerd \ --config=/etc/containerd/config.toml --set-as-default time="2026-08-15T07:07:35Z" level=info msg="Using config version 2" time="2026-08-15T07:07:35Z" level=info msg="Using CRI runtime plugin name \"io.containerd.grpc.v1.cri\"" time="2026-08-15T07:07:35Z" level=info msg="Wrote updated config to /etc/containerd/conf.d/99-nvidia.toml" time="2026-08-15T07:07:35Z" level=info msg="It is recommended that containerd daemon be restarted." yu3papa@p40:~$ docker container exec llmso-control-plane systemctl restart containerd |
3. "NVIDIA device plugin for Kubernetes" 설치
https://github.com/NVIDIA/k8s-device-plugin
GitHub - NVIDIA/k8s-device-plugin: NVIDIA device plugin for Kubernetes
NVIDIA device plugin for Kubernetes. Contribute to NVIDIA/k8s-device-plugin development by creating an account on GitHub.
github.com
쿠버네티스용 NVIDIA 디바이스 플러그인은 데몬셋 형태로 구성되어 있으며, 다음과 같은 작업들을 자동으로 수행할 수 있도록 해줍니다:
- 클러스터의 각 노드에 장착된 GPU의 수를 표시
- GPU의 상태를 주기적으로 확인
- Kubernetes 클러스터에서 GPU가 지원되는 컨테이너를 실행
| # RuntimeClass 리소스 생성 yu3papa@p40:~$ kubectl apply -f - <<EOF apiVersion: node.k8s.io/v1 kind: RuntimeClass metadata: name: nvidia handler: nvidia EOF runtimeclass.node.k8s.io/nvidia created # device plugin의 DaemonSet은 nodeAffinity로 nvidia.com/gpu.present 라벨이 있어야 함 yu3papa@p40:~$ kubectl label node llmso-control-plane nvidia.com/gpu.present=true --overwrite node/llmso-control-plane labeled # helm 을 이용하여 k8s-device-plugin 배포 yu3papa@p40:~$ helm repo add nvdp https://nvidia.github.io/k8s-device-plugin "nvdp" has been added to your repositories yu3papa@p40:~$ helm install nvdp nvdp/nvidia-device-plugin \ -n nvidia-device-plugin --create-namespace NAME: nvdp LAST DEPLOYED: Sat Aug 15 16:18:29 2026 NAMESPACE: nvidia-device-plugin STATUS: deployed REVISION: 1 TEST SUITE: None yu3papa@p40:~$ helm -n nvidia-device-plugin list NAME NAMESPACE REVISION UPDATED STATUS CHART APP VERSION nvdp nvidia-device-plugin 1 2026-08-15 16:18:29.152729285 +0900 KST deployed nvidia-device-plugin-0.19.3 0.19.3 # node 에 할당 가능한 GPU 가 존재하는지 확인 yu3papa@p40:~$ kubectl describe no llmso-control-plane | grep -A 7 Capacity: Capacity: cpu: 4 ephemeral-storage: 239251068Ki hugepages-1Gi: 0 hugepages-2Mi: 0 memory: 16257136Ki nvidia.com/gpu: 1 pods: 110 # POD 에서 gpu 사용가능한지 테스트 yu3papa@p40:~$ cat <<EOF | kubectl apply -f - apiVersion: v1 kind: Pod metadata: name: gpu-test spec: restartPolicy: Never runtimeClassName: nvidia containers: - name: cuda image: nvidia/cuda:12.4.1-base-ubuntu22.04 command: ["nvidia-smi"] resources: limits: nvidia.com/gpu: 1 EOF pod/gpu-test created yu3papa@p40:~$ kubectl get po gpu-test -o wide NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES gpu-test 0/1 Completed 0 33s 10.244.0.6 llmso-control-plane <none> <none> yu3papa@p40:~$ kubectl logs gpu-test Sat Aug 15 07:23:54 2026 +-----------------------------------------------------------------------------------------+ | NVIDIA-SMI 580.173.02 Driver Version: 580.173.02 CUDA Version: 13.0 | +-----------------------------------------+------------------------+----------------------+ | GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC | | Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. | | | | MIG M. | |=========================================+========================+======================| | 0 Tesla P40 On | 00000000:01:00.0 Off | Off | | N/A 46C P0 53W / 250W | 473MiB / 24576MiB | 0% Default | | | | N/A | +-----------------------------------------+------------------------+----------------------+ +-----------------------------------------------------------------------------------------+ | Processes: | | GPU GI CI PID Type Process name GPU Memory | | ID ID Usage | |=========================================================================================| | No running processes found | +-----------------------------------------------------------------------------------------+ |
4. KubeRay로 LLM 서빙
https://docs.ray.io/en/latest/index.html
Welcome to Ray!
Scale generic Python code with simple, foundational primitives that enable a high degree of control for building distributed applications or custom platforms. Learn more
docs.ray.io
KubeRay Operator 설치하면 오픈소스 스택으로 LLM 서빙을 CRD 만으로도 쉽게 운영이 가능합니다.
| # Helm 을 이용한 KubeRay Operator 설치 yu3papa@p40:~$ helm repo add kuberay https://ray-project.github.io/kuberay-helm/ "kuberay" has been added to your repositories yu3papa@p40:~$ kubectl create namespace kuberay-system namespace/kuberay-system created yu3papa@p40:~$ helm -n kuberay-system install kuberay-operator kuberay/kuberay-operator --version 1.6.0 NAME: kuberay-operator LAST DEPLOYED: Sat Aug 15 16:34:35 2026 NAMESPACE: kuberay-system STATUS: deployed REVISION: 1 TEST SUITE: None yu3papa@p40:~$ kubectl -n kuberay-system get all NAME READY STATUS RESTARTS AGE pod/kuberay-operator-f498688d8-vgmhm 1/1 Running 0 94s NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE service/kuberay-operator ClusterIP 10.96.118.46 <none> 8080/TCP 94s NAME READY UP-TO-DATE AVAILABLE AGE deployment.apps/kuberay-operator 1/1 1 1 94s NAME DESIRED CURRENT READY AGE replicaset.apps/kuberay-operator-f498688d8 1 1 1 94s # 등록된 CRD 확인 yu3papa@p40:~$ kubectl get crd | grep ray.io rayclusters.ray.io 2026-08-15T07:34:32Z raycronjobs.ray.io 2026-08-15T07:34:32Z rayjobs.ray.io 2026-08-15T07:34:32Z rayservices.ray.io 2026-08-15T07:34:33Z yu3papa@p40:~$ |
5. RayService CR을 배포하고 LLM 서빙
RayService CRD를 이용하면 RayCluster 를 구성하고 LLM 서빙하는 어플리케이션을 배포하고 관리합니다.
https://docs.ray.io/en/latest/cluster/kubernetes/getting-started/rayservice-quick-start.html
RayService Quickstart
Prerequisites: This guide mainly focuses on the behavior of KubeRay v1.6.0 and Ray 2.46.0. What’s a RayService?: A RayService manages these components: RayCluster: Manages resources in a Kubernetes...
docs.ray.io
ray-service.llm-serve.yaml 파일을 다운로드 하고 로컬 환경에 맞게 수정
- https://raw.githubusercontent.com/ray-project/kuberay/master/ray-operator/config/samples/ray-service.llm-serve.yaml
- 로컬 환경에 맞게 수정한 ray-service.llm-serve.yaml 파일 내용
# 공식 문서 대비 조정한 부분
- 모델: Qwen/Qwen2.5-7B-Instruct-AWQ, quantization: awq 명시 (AWQ 커널 강제 사용), dtype: auto(체크포인트 권장값 따름)
- gpu_memory_utilization: 0.85 — AWQ라 가중치 4~5GB뿐이라 KV캐시용으로 넉넉하게 확보
- max_model_len: 4096 — VRAM 여유가 생겨 문서 예제(1024)보다 실용적인 길이로 상향(필요시 더 늘릴 수 있음)
- nvidia.com/gpu: 4→1, num-gpus: "4"→"1", max_replicas: 4→1 — 물리 GPU 1장에 맞게 축소
- worker CPU/메모리도 32/32Gi → 8/24Gi로 축소 (노드가 16 vCPU/64GB라 이 정도면 모니터링 스택과 공존 가능)
apiVersion: ray.io/v1
kind: RayService
metadata:
name: vllm-service
namespace: kuberay
spec:
serveConfigV2: |
applications:
- name: llms
import_path: ray.serve.llm:build_openai_app
route_prefix: "/"
args:
llm_configs:
- model_loading_config:
model_id: qwen2.5-7b-instruct-awq
model_source: Qwen/Qwen2.5-7B-Instruct-AWQ
engine_kwargs:
dtype: auto
quantization: awq
max_model_len: 4096
gpu_memory_utilization: 0.85
deployment_config:
autoscaling_config:
min_replicas: 1
max_replicas: 1 # 물리 GPU 1장 고정
target_ongoing_requests: 16
max_ongoing_requests: 32
rayClusterConfig:
headGroupSpec:
rayStartParams:
num-gpus: "0"
template:
spec:
containers:
- name: ray-head
image: rayproject/ray-llm:2.52.0-py311-cu128 # vLLM+CUDA 포함
resources:
limits: {cpu: "2", memory: "5Gi"}
requests: {cpu: "2", memory: "4Gi"}
ports:
- containerPort: 8000 # Serve
- containerPort: 8080 # Metrics
- containerPort: 6379 # GCS
- containerPort: 8265 # Dashboard
- containerPort: 10001 # Client
workerGroupSpecs:
- groupName: gpu-group
replicas: 1
minReplicas: 1
maxReplicas: 1 # 물리 GPU 1장뿐이라 고정
rayStartParams:
num-gpus: "1"
template:
spec:
containers:
- name: ray-worker
image: rayproject/ray-llm:2.52.0-py311-cu128
resources:
limits: {cpu: "8", memory: "24Gi", nvidia.com/gpu: 1}
requests: {cpu: "8", memory: "24Gi", nvidia.com/gpu: 1}
env:
- name: HUGGING_FACE_HUB_TOKEN
valueFrom:
secretKeyRef:
name: hf-token
key: hf_token
---
apiVersion: v1
kind: Secret
metadata:
name: hf-token
namespace: kuberay
type: Opaque
stringData:
hf_token: hf_XUVCAF****************
rayservice CR 배포 및 확인
| # rayservice CR 배포 yu3papa@p40:~$ kubectl create ns kuberay namespace/kuberay created yu3papa@p40:~$ kubectl apply -f ray-service.llm-serve.yaml rayservice.ray.io/vllm-service created secret/hf-token created yu3papa@p40:~$ kubectl -n kuberay get all NAME DESIRED WORKERS AVAILABLE WORKERS CPUS MEMORY GPUS STATUS AGE raycluster.ray.io/vllm-service-fxvls 44s NAME SERVICE STATUS NUM SERVE ENDPOINTS rayservice.ray.io/vllm-service |
하지만 여기에서 기다려도 정상 동작이 되지 않았습니다.
6. KubeRay 를 이용한 vLLM 서빙 실패 이유
Tesla P40 GPU에서는 최신 vLLM을 순정(Out-of-the-box) 상태로 정상 구동할 수 없으며, 강제로 실행하려 해도 심각한 에러가 발생하거나 연산 속도가 극도로 처참해집니다.
아래 내용은 Gemini 답변 내용을 정리하였습니다.
1. 근본 원인 진단: 왜 Tesla P40에서 vLLM이 안 되는가?
① FlashAttention-2의 하드웨어 커널 미지원 (가장 결정적 원인)
vLLM의 압도적인 추론 속도와 PagedAttention 최적화는 내부적으로 FlashAttention-2 C++ CUDA 커널에 깊게 의존합니다.
- FlashAttention-2 요구사항: NVIDIA Ampere 아키텍처(Compute Capability sm_80 이상) (예: RTX 3090, A100, L40S 등)부터만 하드웨어적으로 지원됩니다.
- Tesla P40 아키텍처: 2016년에 출시된 Pascal 아키텍처(Compute Capability sm_61)입니다.
- 결과: vLLM이 C++ 커널을 로딩할 때 cudaErrorNoKernelImageForDevice 에러를 배출하며 실행이 거부됩니다.
② PyTorch 및 CUDA 빌드 커널의 Pascal(sm_61) 지원 중단
최신 PyTorch(2.2+) 바이너리와 vLLM이 사전 빌드(Pre-compiled)한 CUDA 휠(Wheel) 패키지들은 바이너리 용량을 줄이기 위해 sm_75(Turing) 이상만 포함하여 배포됩니다. sm_61 커널 코드가 아예 패키지 내부에 포함되어 있지 않습니다.
③ Pascal 세대의 FP16(Half-Precision) 연산 성능 절벽
만약 vLLM의 FlashAttention 옵션을 끄고 PyTorch Native Attention으로 강제 폴백(Fallback)시켜 실행한다 하더라도 하드웨어적 병목이 발생합니다.
- Tesla P40은 Tensor Core가 전혀 없습니다.
- FP16(Half Precision) 연산 파이프라인이 전용 유닛이 아닌 에뮬레이션 수준으로 동작하여 FP16 연산 속도가 FP32의 $\frac{1}{64}$ 수준으로 참혹하게 추락합니다.
- vLLM은 기본적으로 FP16/BF16 텐서 연산을 기반으로 동작하므로, P40에서는 토큰 생성 속도가 초당 1~2토큰 수준으로 처참해집니다.
2. P40 수량별 VRAM 및 서빙 프레임워크 성능 비교
Tesla P40(24GB)을 활용할 때 서빙 엔진 선택에 따른 실제 스펙 트레이드오프입니다. (Llama-3-8B 기준)
| 구분 | vLLM (FP16/AWQ) | Ollama / llama.cpp (GGUF Q4_K_M) | TGI (Text Generation Inference) |
| P40 구동 여부 | ❌ 불가 (C++ Kernel Error) | ✅ 100% 완벽 구동 (cuBLAS 가속) | 🔺 제약적 (Custom Docker 빌드 필요) |
| 추론 속도 (TPS) | N/A (구동 불가) | 약 30 ~ 45 Tokens/sec (매우 빠름) | 약 5 ~ 10 Tokens/sec (느림) |
| 연산 커널 백엔드 | FlashAttention-2 (sm_80+) | cuBLAS FP32 / INT8 / Dequantize | FlashAttention / XFormers |
| VRAM 점유량 | N/A | 약 5.5 GB (KV Cache 포함 18GB 여유) | 약 16 GB |
'LLM' 카테고리의 다른 글
| (LLMSO 3주차) [KodeKloud] vLLM을 사용하여 LLM 추론서버 운영 (0) | 2026.08.22 |
|---|---|
| (LLMSO 1주차) 트랜스포너 아키텍처 실습 (0) | 2026.08.04 |