Skip to main content

모델을 띄우는 일과 서비스를 운영하는 일은 다르다

앞선 장들에서 우리는 모델을 여러 GPU와 여러 노드에 나누어 올리는 방법을 살펴보았다. 이제부터는 조금 다른 질문을 해야 한다. "모델이 한 번 뜬다"는 것과 "사용자가 매일 안정적으로 쓴다"는 것은 전혀 다른 문제다.

개발 환경에서는 vllm serve 한 줄로도 감탄이 나온다. 거대한 모델이 응답을 시작하고, GPU 사용률이 올라가고, 초당 토큰 수가 찍힌다. 하지만 실서비스에서는 그 순간부터 진짜 일이 시작된다. 새 버전을 어떻게 배포할 것인가? 모델 로딩에 20분이 걸리는데 장애가 나면 어떻게 복구할 것인가? GPU 한 장에 수천만 원이 들어가는데 한밤중 유휴 시간에도 계속 켜둘 것인가? 사용자가 "느리다"고 말할 때 병목이 프리필인지, 디코드인지, 네트워크인지, KV 캐시 부족인지 어떻게 알 것인가?

Kubernetes는 이런 질문에 답하기 위한 공통 운영판이다. 다만 Kubernetes가 LLM을 자동으로 이해해 주는 것은 아니다. Kubernetes는 파드를 띄우고, 리소스를 예약하고, 죽은 컨테이너를 다시 살리는 데 능숙하다. 반면 LLM 서빙은 GPU 토폴로지, 모델 가중치, KV 캐시, 토큰 단위 지연 시간, 긴 컨텍스트 요청 같은 독특한 특성을 가진다. 그래서 Kubernetes 위에는 KServe, Triton Inference Server, Ray Serve, NVIDIA Dynamo, BentoML 같은 서빙 플랫폼이 올라온다.

이 장의 핵심은 특정 도구 하나를 찬양하는 것이 아니다. 운영 구조를 보는 눈을 만드는 것이다. 소규모 팀은 BentoML이나 단순 vLLM 배포에서 시작할 수 있고, 여러 모델을 한 서버에서 다루는 팀은 Triton이 어울릴 수 있다. Kubernetes 네이티브 선언형 운영을 원하면 KServe가 강력하다. Ray 기반 분산 애플리케이션과 LLM 파이프라인을 함께 다룬다면 Ray Serve가 자연스럽다. 그리고 reasoning 모델, 긴 입력, 대규모 GPU 풀에서 프리필과 디코드를 분리해야 하는 단계에 이르면 Dynamo 같은 분산 추론 계층이 의미를 갖는다.

Kubernetes와 서빙 플랫폼으로 운영하기

Kubernetes 위의 LLM 서빙 계층

LLM 서빙 플랫폼은 대체로 세 층으로 나눠 생각할 수 있다. 가장 아래에는 GPU 노드와 Kubernetes가 있다. 여기서 NVIDIA Device Plugin, GPU Operator, 노드 라벨, PVC, 네트워크, 스토리지가 준비된다. 그 위에는 vLLM, TensorRT-LLM, SGLang, llama.cpp 같은 추론 엔진이 있다. 실제로 토큰을 생성하고 KV 캐시를 관리하는 계층이다. 가장 위에는 KServe, Triton, Ray Serve, Dynamo, BentoML 같은 운영 플랫폼이 있다. 이 계층은 배포, 라우팅, 오토스케일링, 모델 버전, 헬스 체크, 관측, 장애 복구를 담당한다.

여기서 중요한 감각은 "추론 엔진"과 "서빙 플랫폼"을 구분하는 것이다. vLLM은 훌륭한 엔진이지만, 여러 팀이 사용하는 운영 환경에서는 엔진 하나만으로는 부족할 수 있다. 반대로 KServe나 Ray Serve는 자체적으로 모든 토큰 생성을 새로 구현하는 것이 아니라, vLLM 같은 엔진을 감싸서 운영 가능한 서비스로 만든다. Triton은 여러 백엔드를 한 서버에서 다룰 수 있게 해주며, Dynamo는 여러 엔진을 대규모 분산 추론 시스템으로 조율하는 계층에 가깝다.

실전에서는 다음과 같은 선택 기준이 유용하다.

상황먼저 검토할 선택지
단일 노드, 단일 LLM, 빠른 시작vLLM 단독 배포 또는 BentoML
Kubernetes 네이티브 선언형 배포KServe
LLM, 임베딩, 리랭커, 비전 모델을 함께 운영Triton Inference Server
Ray 기반 분산 작업, 복잡한 파이프라인, 멀티 노드 조율Ray Serve + KubeRay
프리필/디코드 분리, KV 캐시 인식 라우팅, 대규모 GPU 풀NVIDIA Dynamo 또는 llm-d 계열

도구를 고를 때 가장 흔한 실수는 "가장 복잡한 플랫폼이 가장 좋은 플랫폼"이라고 생각하는 것이다. 한 장짜리 GPU에서 7B 모델 하나를 사내 챗봇으로 쓰는 상황에 거대한 분산 서빙 스택을 올리면, 모델보다 운영 시스템이 더 어려워진다. 반대로 수십 장의 H100에서 장문 reasoning 모델을 서비스하면서 단순 Deployment와 Service만으로 버티려 하면, 어느 순간 TTFT가 폭발하고 GPU 비용이 새어 나가는 것을 막기 어렵다.

KServe: 선언형으로 LLM을 운영하는 방식

KServe는 Kubernetes 위에서 모델 서빙을 표준화하려는 플랫폼이다. 사용자는 복잡한 Deployment, Service, Gateway, 라우팅 구성을 직접 조립하기보다 InferenceService 또는 LLM용 리소스를 선언한다. 그러면 KServe 컨트롤러가 필요한 파드와 네트워크 구성을 만들어 준다. KServe는 기존 예측 모델뿐 아니라 vLLM, llm-d 같은 생성형 AI 백엔드도 지원하는 방향으로 발전해 왔고, OpenAI 호환 API, 모델 캐싱, KV 캐시 오프로딩, 요청 기반 스케일링 같은 LLM 운영 기능을 포함한다1.

KServe가 특히 매력적인 지점은 "운영 의도를 YAML로 남긴다"는 점이다. 어떤 모델을, 어떤 런타임으로, 몇 개의 GPU를 써서, 어느 스토리지에서 읽을지를 선언할 수 있다. GitOps를 사용하는 팀이라면 모델 배포 이력이 코드 저장소에 남고, 변경 리뷰와 롤백이 쉬워진다.

다만 멀티 노드 vLLM을 KServe로 운영할 때는 몇 가지 중요한 제약을 반드시 이해해야 한다. KServe의 Hugging Face vLLM 멀티 노드 ServingRuntime은 workerSpec.tensorParallelSize와 workerSpec.pipelineParallelSize를 통해 병렬화 크기를 설정한다. 이 값들은 환경 변수로 주입하는 것이 아니라 ServingRuntime과 InferenceService의 workerSpec에 선언해야 한다2.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llama-large
  annotations:
    serving.kserve.io/deploymentMode: Standard
    serving.kserve.io/autoscalerClass: none
spec:
  predictor:
    deploymentStrategy:
      type: Recreate
    model:
      modelFormat:
        name: huggingface
      runtime: kserve-huggingfaceserver-multinode
      storageUri: pvc://llama-model-pvc/models/llama
    workerSpec:
      pipelineParallelSize: 2
      tensorParallelSize: 4
      containers:
      - name: worker-container
        resources:
          requests:
            nvidia.com/gpu: "4"
          limits:
            nvidia.com/gpu: "4"

여기서 tensorParallelSize는 모델의 행렬 연산과 가중치를 여러 GPU에 나누는 텐서 병렬 크기다. pipelineParallelSize는 모델 레이어를 여러 단계로 나누는 파이프라인 병렬 크기다. vLLM 문맥에서는 흔히 "TP는 한 노드 안 GPU 수, PP는 노드 수"처럼 설명하지만, Kubernetes와 KServe에서는 그렇게 단순히 1:1로 대응한다고 생각하면 위험하다. KServe는 workerSpec의 GPU 요청량과 TP·PP 값을 조합해 Ray head와 worker 파드를 구성한다. 따라서 총 GPU 수, 파드당 GPU 수, 실제 노드의 GPU 배치가 서로 맞아야 한다.

예를 들어 TP=4, PP=2라면 논리적으로 총 8개의 GPU가 필요하다. 파드당 nvidia.com/gpu: 4를 요청하면 대체로 4 GPU 파드 두 개로 구성되는 식이다. 하지만 클러스터에 4장짜리 GPU 노드가 없거나, 이미 다른 파드가 GPU를 일부 점유하고 있다면 스케줄링이 막힌다. 멀티 GPU LLM은 "GPU가 총 8장 있다"보다 "동시에, 같은 조건으로, 올바른 노드에 배치될 수 있는가"가 더 중요하다.

KServe 멀티 노드 vLLM에서 스토리지도 핵심이다. 멀티 노드 구성은 PVC를 요구하며, 여러 노드가 동시에 모델 파일에 접근해야 하므로 ReadWriteMany, 즉 RWX 접근 모드를 지원하는 스토리지가 필요하다2. NFS, CephFS, EFS 같은 공유 파일 시스템이 자주 쓰인다. 70B 모델 가중치는 정밀도에 따라 100GB를 훌쩍 넘고, TensorRT 엔진이나 캐시 파일까지 포함하면 더 커진다. 모델을 매번 Hugging Face에서 다시 받게 만들면 장애 복구보다 다운로드가 더 오래 걸리는 기묘한 상황이 생긴다.

또 하나의 중요한 제약은 오토스케일링이다. KServe의 멀티 노드 vLLM 구성은 Standard 모드에서 동작하며, 멀티 노드 설정에서는 autoscaler를 none으로 두어야 하는 제한이 있다2. 이는 처음 보면 아쉽지만 이유가 있다. TP와 PP로 묶인 파드들은 하나의 모델 인스턴스처럼 함께 움직여야 한다. 일반 웹 서버처럼 파드 하나를 더 붙인다고 용량이 선형으로 늘지 않는다. 특히 pipelineParallelSize는 "트래픽이 늘었으니 노드 하나 더 추가" 같은 일반 오토스케일링 개념이 아니라, 모델을 몇 단계로 나눠 실행할지 결정하는 구조적 설정이다.

따라서 운영 중 TP나 PP를 바꾸는 일은 신중해야 한다. KServe 문서에서도 프로덕션에서 이 값을 동적으로 바꾸는 것을 권장하지 않는다. 개발 중에는 Recreate 전략이 더 안전한 경우가 많다. 기본 RollingUpdate는 기존 서비스가 Ready 상태를 유지한 채 새 서비스를 띄우려 하기 때문에, 대형 모델에서는 순간적으로 두 배 이상의 GPU와 VRAM이 필요할 수 있다. 여분 GPU가 충분하지 않으면 새 파드는 Pending에 머물고, 배포는 끝나지 않는다.

프로브 설정도 일반 웹 애플리케이션과 다르게 잡아야 한다. 7B 모델은 몇 분 안에 뜰 수 있지만, 70B 이상 모델은 다운로드, 샤딩, CUDA 그래프 캡처, 워커 동기화 때문에 훨씬 오래 걸릴 수 있다. startupProbe, readinessProbe, livenessProbe의 initialDelaySeconds, failureThreshold, timeoutSeconds를 넉넉히 두지 않으면 Kubernetes가 "아직 로딩 중인 정상 파드"를 "죽은 파드"로 오해하고 계속 재시작한다. 대형 모델에서 CrashLoopBackOff가 반복된다면, 먼저 로그와 함께 프로브 시간이 너무 짧지 않은지 확인해야 한다.

Triton Inference Server: LLM만 있는 세상은 아니다

vLLM은 LLM 하나를 빠르게 서빙할 때 매우 강력하다. 그러나 실제 AI 서비스는 LLM 하나로 끝나지 않는 경우가 많다. 사용자의 이미지를 임베딩하는 비전 인코더, 검색 결과를 재정렬하는 리랭커, 금칙어를 탐지하는 분류기, 음성 인식 모델, 후처리 Python 코드가 함께 움직인다. 이런 환경에서는 NVIDIA Triton Inference Server가 빛난다.

Triton은 TensorRT, PyTorch, ONNX Runtime, OpenVINO, Python backend, TensorRT-LLM 등 여러 백엔드를 지원하고, HTTP/gRPC API, 헬스 체크, Prometheus 메트릭을 제공하는 범용 추론 서버다3. 쉽게 말해 "여러 종류의 모델을 한 지붕 아래에서 운영하는 서버"다. LLM만 놓고 보면 vLLM 단독 구성이 단순하고 빠를 수 있지만, LLM 주변의 모델 생태계까지 함께 보면 Triton의 장점이 커진다.

Triton에서 가장 먼저 이해할 기능은 동적 배칭(dynamic batching)이다. 작은 요청이 여러 개 들어올 때, 이를 즉시 하나씩 GPU에 던지면 GPU가 바쁘게 보이지만 실제 처리량은 낮을 수 있다. 동적 배칭은 짧은 시간 동안 요청을 모아 하나의 배치로 실행한다. max_queue_delay_microseconds를 너무 길게 잡으면 지연 시간이 늘고, 너무 짧게 잡으면 배치가 잘 안 모인다. 즉 동적 배칭은 "기다림으로 처리량을 사는" 기술이다.

두 번째는 instance group이다. 같은 모델을 GPU 몇 개에 몇 인스턴스로 띄울지 정할 수 있다. 작은 ONNX 모델은 GPU 하나에 여러 인스턴스를 올릴 수 있고, 무거운 LLM은 한 인스턴스가 여러 GPU를 독점할 수 있다. 이 설정은 멀티 모델 환경에서 특히 중요하다. 임베딩 모델이 LLM과 같은 GPU를 두고 싸우면, LLM의 KV 캐시가 밀려나거나 지연 시간이 튈 수 있다.

세 번째는 concurrent model execution, 즉 여러 모델의 동시 실행이다. Triton은 서로 다른 모델 요청을 동시에 처리할 수 있다. 예를 들어 RAG 서비스에서 임베딩 모델, 리랭커, LLM이 모두 필요하다면, Triton의 모델 저장소와 앙상블 구성을 통해 파이프라인을 구성할 수 있다. 고정된 순서의 전처리-추론-후처리라면 ensemble이 유용하고, 조건 분기나 복잡한 로직이 필요하면 Python backend나 Business Logic Scripting 방식이 어울린다.

Triton은 TensorRT-LLM backend와 함께 LLM 최적화에도 쓰인다. TensorRT-LLM은 NVIDIA GPU에서 LLM 추론을 가속하기 위한 라이브러리로, Triton을 통해 TensorRT-LLM 엔진을 배포할 수 있다4. 다만 이 방식은 모델 변환과 엔진 빌드 과정이 필요하다. 성능은 뛰어날 수 있지만, vLLM처럼 Hugging Face 모델 이름 하나로 바로 띄우는 경험과는 다르다. 운영팀이 엔진 빌드 파이프라인, 모델 버전, GPU 아키텍처별 엔진 호환성을 관리할 수 있어야 한다.

Triton을 선택할 때의 감각은 이렇다. "우리 서비스가 LLM 하나만 잘 돌리면 된다"면 Triton이 과할 수 있다. "LLM은 전체 AI 파이프라인의 일부이고, 여러 프레임워크 모델을 한 운영 체계에서 관리해야 한다"면 Triton은 강력한 선택지다.

Ray Serve와 KubeRay: 분산 애플리케이션으로서의 LLM

Ray는 Python 기반 분산 컴퓨팅 프레임워크이고, Ray Serve는 그 위에서 모델 서빙 애플리케이션을 구성하는 계층이다. Kubernetes에서는 KubeRay가 Ray 클러스터를 CRD로 선언하고 운영한다. vLLM 공식 문서에서도 KubeRay를 사용하면 Ray 클러스터를 YAML로 선언하고, 오퍼레이터가 파드 스케줄링, 네트워크 설정, 재시작, blue-green 배포를 처리한다고 설명한다5. Ray 문서 역시 KubeRay, Ray Serve, vLLM을 조합해 Kubernetes에서 OpenAI 호환 LLM 서비스를 배포하는 예제를 제공한다6.

Ray Serve의 장점은 LLM을 단순 서버가 아니라 분산 애플리케이션의 일부로 다룰 수 있다는 점이다. 예를 들어 요청이 들어오면 전처리 actor가 입력을 정리하고, 검색 actor가 벡터 DB에서 문서를 가져오고, 리랭커 actor가 후보를 줄이고, vLLM actor가 답변을 생성하고, 후처리 actor가 정책 검사를 수행하는 구조를 만들 수 있다. 각 actor는 CPU나 GPU 리소스를 다르게 요청할 수 있다.

Kubernetes만으로도 여러 Deployment를 만들어 비슷한 파이프라인을 구성할 수 있다. 그러나 Ray는 Python 코드 안에서 분산 실행 그래프를 표현하기가 쉽고, Ray Serve는 모델 replica와 라우팅을 Ray 생태계 안에서 관리한다. 특히 이미 학습, 배치 처리, 데이터 처리에 Ray를 쓰고 있는 조직이라면 서빙까지 같은 프레임워크로 연결하는 장점이 있다.

다만 Ray Serve는 공짜 점심이 아니다. 단일 LLM을 한 노드에서 띄우는 정도라면 Ray의 head node, worker group, object store, dashboard, autoscaling 구성이 오히려 부담이 된다. 반대로 여러 노드와 여러 모델, 복잡한 DAG, 동적 리소스 할당이 필요하다면 Ray Serve가 단순 Kubernetes manifest보다 훨씬 표현력이 좋다.

운영 관점에서는 Ray head 파드가 중요한 단일 지점이 되므로 리소스를 넉넉히 주고, Ray dashboard와 Serve metrics를 Prometheus에 연결해야 한다. GPU actor가 죽었을 때 재시작은 되는지, 요청이 어느 replica로 라우팅되는지, 모델 로딩 실패가 Ray 레벨에서 어떻게 드러나는지까지 관찰해야 한다.

NVIDIA Dynamo: 프리필과 디코드를 분리하는 이유

LLM 추론은 크게 두 단계로 나뉜다. 첫 번째는 프리필(prefill)이다. 사용자가 보낸 긴 입력 프롬프트를 한 번에 처리해 첫 토큰을 만들 준비를 하고, KV 캐시를 쌓는 단계다. 두 번째는 디코드(decode)다. 이미 만들어진 KV 캐시를 참조하면서 다음 토큰을 하나씩 생성하는 단계다.

이 둘은 GPU를 쓰는 방식이 다르다. 프리필은 긴 입력을 병렬로 처리하므로 계산량이 크다. 디코드는 토큰을 하나씩 생성하므로 메모리 접근과 KV 캐시 관리가 중요하다. 두 단계를 같은 GPU 풀에서 섞어 처리하면, 긴 문서 요약 요청 몇 개가 들어왔을 때 프리필이 GPU를 오래 붙잡고, 짧은 채팅 요청의 디코드까지 밀리는 일이 생긴다. 사용자는 "첫 토큰이 왜 이렇게 늦지?"라고 느낀다.

NVIDIA Dynamo는 이런 문제를 대규모 분산 추론 관점에서 다룬다. Dynamo는 프리필과 디코드 단계를 서로 다른 GPU 또는 노드에 분리하고, 수요에 따라 GPU 워커를 동적으로 배치하며, LLM-aware request routing과 비동기 GPU 간 데이터 전송을 통해 응답 시간을 줄이는 것을 목표로 한다7. NVIDIA는 Dynamo가 TensorRT-LLM, vLLM, SGLang 같은 주요 LLM 엔진을 조율하는 분산 추론 프레임워크라고 설명한다8.

여기서 핵심은 KV 캐시다. 프리필 단계가 끝나면 디코드 단계는 그 요청의 KV 캐시를 이어받아야 한다. 만약 프리필 GPU와 디코드 GPU가 다르다면, KV 캐시를 빠르게 옮겨야 한다. 느리게 옮기면 분리한 이점이 사라진다. Dynamo는 NIXL, 즉 NVIDIA Inference Transfer Library를 통해 GPU 간 비동기 데이터 전송을 가속하고, Distributed KV Cache Manager로 KV 캐시 위치와 재사용을 관리하는 구조를 제시한다7.

Smart Router도 중요하다. 많은 서비스는 동일한 시스템 프롬프트, 반복되는 RAG 템플릿, 멀티턴 대화의 이전 문맥을 자주 재사용한다. 이미 어떤 워커에 유사한 KV 캐시가 있다면, 그 워커로 요청을 보내는 것이 새로 프리필을 계산하는 것보다 빠를 수 있다. Dynamo의 KV-aware routing은 이런 중복 계산을 줄이려는 접근이다.

Dynamo가 특히 의미 있는 워크로드는 다음과 같다.

  • 입력이 길고 출력도 긴 reasoning 모델

  • 시스템 프롬프트와 도구 호출이 반복되는 agentic workflow

  • 여러 GPU 노드에서 프리필 병목과 디코드 병목이 번갈아 나타나는 대규모 서비스

  • KV 캐시 재사용률이 높고, TTFT와 ITL SLO를 모두 관리해야 하는 서비스

작은 워크스테이션에서 14B 모델 하나를 돌리는 사용자가 Dynamo부터 시작할 필요는 없다. 하지만 수십 장 이상의 GPU에서 "왜 GPU 사용률은 높은데 사용자는 느리다고 하지?"라는 질문이 나오기 시작하면, 프리필-디코드 분리와 KV 캐시 인식 라우팅은 더 이상 논문 속 이야기가 아니다.

BentoML: 모델 서비스를 제품처럼 포장하기

BentoML은 개발자 경험이 좋은 모델 서빙 프레임워크다. 모델, 코드, 의존성, API 정의를 하나의 서비스 단위로 묶고, OCI 호환 컨테이너 이미지로 만들어 Kubernetes에 배포할 수 있다. vLLM 문서에서도 BentoML을 사용하면 vLLM을 백엔드로 하는 LLM 서버를 OpenAI 호환 엔드포인트로 제공하고, 로컬 실행 또는 컨테이너화 후 Kubernetes 배포가 가능하다고 설명한다9. BentoVLLM 예제 저장소 역시 Llama, Qwen, DeepSeek 계열 모델을 vLLM과 BentoML로 self-hosting하는 패턴을 제공한다10.

BentoML의 장점은 "복잡한 플랫폼을 먼저 세우기보다, 서비스를 잘 포장하는 것"에 있다. Python 코드로 API를 정의하고, 필요한 환경 변수를 secret으로 주입하고, 이미지를 빌드해 배포한다. 팀 규모가 작고, 모델별 커스텀 로직이 많고, 빠르게 실험해야 한다면 BentoML은 부담이 적다.

예를 들어 같은 vLLM을 쓰더라도 BentoML은 서비스 코드 안에 인증 전처리, 프롬프트 템플릿, 응답 포맷 변환, 간단한 정책 검사를 함께 넣기 쉽다. 반면 KServe는 플랫폼 표준화에 강하고, Triton은 멀티 백엔드 모델 저장소에 강하며, Ray Serve는 분산 DAG에 강하다. BentoML은 "ML 서비스를 애플리케이션처럼 패키징하고 배포하는 경험"에 초점이 있다.

다만 아주 큰 멀티 노드 병렬 LLM을 정교하게 운영하거나, 프리필-디코드 분리와 KV 캐시 인식 라우팅까지 다루려면 BentoML 단독보다는 vLLM, Kubernetes, KServe, Ray, Dynamo 같은 계층과의 조합을 고민해야 한다.

Kubernetes 리소스 설정: GPU는 CPU처럼 나눠 쓰기 어렵다

Kubernetes에서 GPU를 쓰려면 보통 NVIDIA Device Plugin 또는 GPU Operator가 필요하다. 이 구성은 노드의 GPU를 nvidia.com/gpu 같은 리소스로 노출한다. 파드는 다음처럼 GPU를 요청한다.

resources:
  requests:
    cpu: "8"
    memory: 64Gi
    nvidia.com/gpu: "4"
  limits:
    cpu: "16"
    memory: 96Gi
    nvidia.com/gpu: "4"

GPU 리소스는 CPU처럼 0.5개를 요청하는 식으로 자연스럽게 쪼개 쓰기 어렵다. 한 파드가 nvidia.com/gpu: 1을 요청하면 GPU 한 장이 통째로 할당되는 것이 일반적이다. 개발 환경에서 time-slicing을 써서 여러 파드가 GPU를 공유할 수는 있지만, LLM 프로덕션에서는 VRAM 격리와 예측 가능한 지연 시간이 중요하므로 신중해야 한다.

MIG는 Multi-Instance GPU의 약자로, A100이나 H100 같은 일부 GPU를 하드웨어적으로 여러 조각으로 나누는 기능이다. 예를 들어 작은 임베딩 모델이나 reranker를 별도 MIG slice에 올리면 대형 LLM이 쓰는 GPU와 격리할 수 있다. Kubernetes에서는 nvidia.com/mig-1g.10gb 같은 리소스로 요청한다.

resources:
  limits:
    nvidia.com/mig-1g.10gb: 1

하지만 MIG는 만능이 아니다. 70B LLM처럼 여러 GPU의 전체 VRAM과 NVLink 대역폭이 필요한 모델에는 오히려 방해가 될 수 있다. MIG는 작은 모델의 멀티 테넌시에는 좋지만, 텐서 병렬 LLM에는 전체 GPU를 쓰는 구성이 더 자연스러운 경우가 많다.

노드 셀렉터와 affinity도 필수다. 클러스터 안에 L40S, A100, H100이 섞여 있다면 모델을 아무 GPU에나 배치해서는 안 된다. 70B 모델은 H100 80GB 노드에만, 7B 모델은 L40S 노드에도 배치할 수 있다. GPU Feature Discovery가 붙인 라벨을 활용하면 다음처럼 특정 GPU 제품군을 지정할 수 있다.

nodeSelector:
  nvidia.com/gpu.product: NVIDIA-H100-SXM5-80GB

GPU 노드에는 taint를 걸어 일반 CPU 워크로드가 올라오지 못하게 하고, LLM 파드에는 toleration을 부여하는 방식도 흔하다.

tolerations:
- key: nvidia.com/gpu
  operator: Exists
  effect: NoSchedule

이런 설정은 사소해 보이지만 비용과 직결된다. GPU 노드에서 로그 수집기나 일반 웹 서버가 CPU와 메모리를 잡아먹어 LLM 파드가 스케줄링되지 않는 일이 실제로 발생한다. 비싼 GPU가 비어 있는데 파드가 Pending인 상황은, 대부분 라벨·taint·리소스 요청·스토리지 조건 중 하나가 맞지 않아서 생긴다.

배포 전략: RollingUpdate가 항상 좋은 것은 아니다

웹 서버에서는 RollingUpdate가 기본 미덕처럼 여겨진다. 새 버전 파드를 조금씩 띄우고, 준비가 끝나면 옛 파드를 내린다. 하지만 대형 LLM에서는 이 전략이 GPU를 두 배로 요구할 수 있다.

예를 들어 8 GPU를 사용하는 70B 모델이 있다. RollingUpdate로 새 버전을 띄우려면 기존 8 GPU가 계속 서비스하는 동안 새 8 GPU가 추가로 필요할 수 있다. 클러스터에 여유 GPU가 없다면 새 파드는 영원히 Pending 상태가 된다. 이 경우에는 Recreate 전략이 더 현실적이다. 잠깐의 중단을 감수하고 기존 파드를 내린 뒤 새 파드를 올리는 방식이다.

프로덕션에서 무중단이 꼭 필요하다면 방법은 있다. 같은 모델을 별도 이름의 새 서비스로 띄우고, 준비가 끝난 뒤 Gateway나 로드밸런서에서 트래픽을 전환한다. 즉 Kubernetes Deployment의 RollingUpdate에만 의존하지 않고, 상위 라우팅 계층에서 blue-green 또는 canary 배포를 구현하는 것이다. KServe, RayService, Gateway API, Envoy AI Gateway 같은 도구가 이 지점에서 의미를 갖는다.

프로브는 배포 안정성의 또 다른 핵심이다. LLM 파드에는 보통 세 종류의 프로브가 필요하다. startupProbe는 모델 로딩이 끝날 때까지 충분히 기다리게 해준다. readinessProbe는 트래픽을 받아도 되는지 판단한다. livenessProbe는 장시간 응답 불능 상태인 프로세스를 재시작한다.

문제는 이 값들이 너무 공격적으로 설정될 때다. 100GB가 넘는 모델을 PVC에서 읽고, 여러 GPU에 샤딩하고, CUDA 커널을 준비하는 동안 /health가 실패할 수 있다. 이때 livenessProbe가 먼저 작동하면 Kubernetes는 로딩 중인 컨테이너를 죽이고, 다시 처음부터 로딩한다. 대형 모델 운영에서 프로브는 "빨리 죽이기"보다 "충분히 기다리되, 정말 죽었을 때만 복구하기"에 가깝게 설계해야 한다.

관측 체계: GPU 사용률만 보면 속는다

LLM 서비스가 느릴 때 nvidia-smi만 보는 것은 체온계 하나로 종합검진을 하려는 것과 비슷하다. GPU 사용률은 중요하지만, 그것만으로는 사용자가 느끼는 지연 시간을 설명하기 어렵다. LLM 관측은 토큰 단위 지표와 GPU 지표를 함께 봐야 한다.

먼저 사용자 경험에 가까운 지표가 있다. TTFT(Time To First Token)는 요청 후 첫 토큰이 나오기까지 걸린 시간이다. 사용자가 "응답이 시작된다"고 느끼는 순간을 결정한다. ITL(Inter-Token Latency)은 토큰과 토큰 사이의 간격이다. 스트리밍 응답이 얼마나 부드럽게 흘러가는지를 보여준다. TPOT(Time Per Output Token)은 출력 토큰 하나당 평균 시간이고, E2EL(End-to-End Latency)은 요청 전체 완료 시간이다. tokens/sec는 처리량을 나타내지만, 단독으로 보면 위험하다. 처리량이 높아도 TTFT가 10초라면 채팅 서비스로는 나쁘다.

vLLM은 Prometheus 형식의 metrics endpoint를 제공하므로, Prometheus가 이를 scrape하고 Grafana에서 대시보드를 만들 수 있다. 여기에 OpenTelemetry를 붙이면 요청 trace와 모델 지표를 연결할 수 있다. 예를 들어 API Gateway에서 들어온 request id가 vLLM 요청, GPU 노드, downstream 검색 서비스까지 이어지면 "이 사용자의 요청이 왜 느렸는가"를 추적할 수 있다. KServe도 KEDA와 OpenTelemetry 기반 metric을 활용해 vLLM의 num_requests_running 같은 LLM 특화 지표를 스케일링에 사용할 수 있는 방향을 제공한다11.

운영 대시보드는 최소한 다음 축을 가져야 한다.

  • 사용자 지연 시간: TTFT, ITL, TPOT, E2EL, p50/p95/p99 latency

  • 처리량과 큐: input tokens/sec, output tokens/sec, running requests, waiting requests, queue time

  • GPU 상태: GPU utilization, memory used, memory bandwidth, temperature, ECC error, XID error

  • KV 캐시 상태: KV cache usage, eviction, prefix cache hit ratio, cache pressure

  • 배포 상태: pod restart count, readiness failure, model load time, pending pods, PVC mount latency

여기서 "KV cache pressure"는 특히 중요하다. VRAM이 모델 가중치로 꽉 차 있는 상태에서 컨텍스트 길이를 늘리거나 동시 요청이 증가하면 KV 캐시가 급격히 커진다. 엔진은 새 요청을 거절하거나, 캐시를 밀어내거나, 배치를 줄이거나, 결국 OOM을 낼 수 있다. 겉으로는 GPU 메모리 사용률이 90%로 평온해 보이지만, 내부적으로는 KV 캐시 블록이 부족해 요청이 줄을 서고 있을 수 있다.

Triton을 쓰는 경우에는 8002 포트의 Prometheus metrics를 확인한다. Triton은 GPU 사용률, 서버 처리량, 지연 시간, 모델별 요청 통계 같은 지표를 제공한다3. LLM과 비LLM 모델을 함께 운영한다면 모델별 큐 대기 시간과 실행 시간을 나눠 봐야 한다. 리랭커 모델이 느려져서 전체 RAG 응답이 늦는 것인데 LLM만 튜닝하는 실수를 피할 수 있다.

오토스케일링: CPU 사용률이 아니라 토큰과 큐를 보라

일반 웹 서비스의 HPA는 CPU 사용률 70% 같은 기준으로도 꽤 잘 동작한다. LLM에서는 그렇지 않다. CPU가 한가해도 GPU 큐는 터질 수 있고, GPU utilization이 높아도 사용자는 만족할 수 있다. 반대로 GPU utilization이 낮아도 TTFT가 길 수 있다. 긴 입력 요청이 프리필 큐에서 막히면 디코드 GPU가 놀고 있을 수 있기 때문이다.

LLM 오토스케일링은 다음 지표를 중심으로 보는 편이 낫다. 대기 요청 수, running request 수, TTFT p95, KV cache usage, tokens/sec, GPU memory 여유, queue time이다. KEDA를 쓰면 Prometheus나 OpenTelemetry에서 가져온 커스텀 metric을 기반으로 replica를 조정할 수 있다. KServe도 LLM autoscaler 방향에서 vLLM metric 기반 스케일링을 지원하는 흐름을 보여준다11.

다만 멀티 GPU·멀티 노드 모델은 replica 하나가 GPU 여러 장을 요구한다. 스케일 아웃은 "파드 하나 추가"가 아니라 "GPU 8장을 한꺼번에 확보"하는 일이 될 수 있다. 그래서 오토스케일링은 항상 클러스터 오토스케일러, 노드 풀 준비 시간, 모델 로딩 시간, PVC 성능과 함께 설계해야 한다. 사용자가 몰린 뒤 H100 노드를 새로 만들고 150GB 모델을 다운로드한다면, 이미 피크 트래픽은 지나갔을 가능성이 높다.

현실적인 전략은 예측 가능한 최소 replica를 유지하고, 피크 시간 전에 warm pool을 준비하며, 작은 모델과 큰 모델의 라우팅 정책을 나누는 것이다. 모든 요청을 70B 모델로 보내지 말고, 간단한 요청은 8B나 14B 모델로 처리하게 하면 비용과 지연 시간을 동시에 줄일 수 있다.

운영 구조 예시: 작은 팀에서 대형 서비스까지

작은 팀이 사내 도구로 LLM을 운영한다면 BentoML 또는 단순 vLLM Deployment로 시작해도 충분하다. PVC에 모델을 캐시하고, GPU nodeSelector를 걸고, Prometheus와 Grafana로 기본 지표를 본다. 이 단계에서는 복잡한 오토스케일링보다 "재시작해도 모델을 다시 다운로드하지 않는 것", "OOM이 나면 이유를 알 수 있는 것", "배포할 때 기존 서비스를 실수로 죽이지 않는 것"이 더 중요하다.

서비스가 커져 모델이 여러 개가 되면 Triton이나 KServe를 검토한다. 임베딩, 리랭커, LLM, 분류기를 함께 관리해야 한다면 Triton의 멀티 모델 서빙과 ensemble이 편하다. Kubernetes 네이티브 선언형 운영과 표준화가 중요하면 KServe가 좋다. 여러 팀이 각자 모델을 배포하는 조직에서는 KServe의 CRD 기반 운영이 플랫폼팀과 모델팀 사이의 계약이 된다.

분산 파이프라인과 Ray 생태계가 중요해지면 KubeRay와 Ray Serve가 자연스럽다. 특히 학습, 배치 추론, 온라인 서빙이 Ray 기반으로 연결된 조직에서는 운영 모델이 단순해진다.

마지막으로 GPU 수가 수십 장에서 수백 장으로 늘고, reasoning 모델의 긴 입력과 긴 출력이 비용을 밀어 올리기 시작하면 Dynamo 같은 분산 추론 계층을 검토한다. 이 단계에서는 단순 replica 수보다 프리필 GPU 풀과 디코드 GPU 풀의 균형, KV 캐시 재사용률, NIXL 같은 고속 전송 계층, SLO 기반 라우팅이 더 중요해진다.

운영자가 마지막에 확인해야 할 체크리스트

LLM Kubernetes 운영은 멋진 아키텍처 그림보다 작은 설정 하나에 더 자주 무너진다. 배포 전에 다음 질문에 답할 수 있어야 한다.

  • 모델 가중치는 PVC에 캐시되는가, 파드 재시작 때 다시 다운로드하지 않는가?

  • 멀티 노드라면 스토리지가 RWX를 지원하고, 모든 파드에서 같은 경로로 접근 가능한가?

  • GPU nodeSelector, affinity, taint/toleration이 실제 노드 라벨과 일치하는가?

  • TP·PP 설정과 파드당 GPU 요청량이 실제 클러스터 토폴로지와 맞는가?

  • RollingUpdate가 필요한 추가 GPU를 클러스터가 감당할 수 있는가, 아니면 Recreate나 blue-green이 나은가?

  • startup/readiness/liveness probe가 모델 로딩 시간을 충분히 기다리는가?

  • TTFT, ITL, tokens/sec, GPU utilization, KV cache pressure를 볼 수 있는가?

  • 장애 시 새 파드가 뜨는 시간과 모델 로딩 시간이 SLO 안에 들어오는가?

  • 오토스케일링 기준이 CPU가 아니라 LLM 특화 metric에 연결되어 있는가?

  • 비용이 큰 모델로 모든 요청을 보내지 않고, 모델 라우팅 정책이 있는가?

이 체크리스트에 답하다 보면 자연스럽게 깨닫게 된다. 대형 언어모델 운영의 본질은 "VRAM에 모델을 억지로 올리는 기술"에서 "비싼 GPU를 안정적으로, 낭비 없이, 사용자가 느끼기에 빠르게 쓰는 기술"로 이동한다.

멀티 GPU와 멀티 노드는 모델을 크게 만들기 위한 수단이다. Kubernetes와 서빙 플랫폼은 그 큰 모델을 서비스로 살아 있게 만드는 장치다. 이제 다음 장에서는 이렇게 구축한 시스템이 실제로 얼마나 빠른지, 어디서 막히는지, 어떤 숫자를 믿어야 하는지 벤치마킹과 병목 분석으로 들어가 보자.

참고

1KServe GitHub Repository

2KServe Multi-node/Multi-GPU Inference with Hugging Face vLLM Serving Runtime

3NVIDIA Triton Inference Server Documentation

4TensorRT-LLM User Guide — NVIDIA Triton Inference Server

5KubeRay — vLLM Documentation

6Serve a Large Language Model using Ray Serve LLM on Kubernetes

7Introducing NVIDIA Dynamo, a Low-Latency Distributed Inference Framework

8ai-dynamo/dynamo GitHub Repository

9BentoML — vLLM Documentation

10BentoVLLM GitHub Repository

11Announcing KServe v0.15: Advancing Generative AI Model Serving

책 목록

VRAM의 벽을 넘는 멀티 GPU LLM 운영 가이드

전체 목차 보기

이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.

틸노트는 사용자가 AI를 활용해 노트를 쉽게 작성하거나 편집할 수 있는 서비스입니다.

AI 스퀘어 단톡방에 함께해요

AI 활용과 틸노트 사용 팁을 나누는 오픈채팅방입니다. 입장 비밀번호는 tilnote입니다.

참여하기