LLM

(LLMSO 2주차) K8S 환경에서 로컬 LLM 서빙

yu3papa 2026. 8. 13. 10:52

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 파일을 다운로드 하고 로컬 환경에 맞게 수정

# 공식 문서 대비 조정한 부분
- 모델: 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