Published on

Seldon Core+KServe로 LLM 롤링·카나리 배포

Authors

서빙 중인 LLM을 바꾸는 일은 단순한 Deployment 롤링업데이트보다 훨씬 위험합니다. 모델 파일이 크고(콜드스타트 길어짐), GPU 리소스가 비싸며(동시 이중가동 비용), 프롬프트/토크나이저/런타임 차이로 응답 품질이 급격히 흔들릴 수 있기 때문입니다. 그래서 LLM 운영에서는 점진적 트래픽 전환(카나리), 빠른 롤백, 세밀한 관측(지표·로그·트레이싱) 이 사실상 필수입니다.

이 글에서는 Kubernetes에서 널리 쓰이는 두 축인 Seldon CoreKServe를 함께 놓고, LLM을 롤링배포·카나리로 안전하게 교체하는 방법을 실전 관점에서 정리합니다. 특히 KServe의 InferenceService 트래픽 스플릿을 중심으로 설명하되, Seldon Core의 강점(그래프/실험/확장성)과 함께 “어떤 상황에 무엇을 선택할지” 기준도 제공합니다.

Seldon Core vs KServe: 무엇이 카나리에 더 적합한가

둘 다 “K8s에서 모델 서빙”을 위한 프레임워크지만, 접근 방식이 다릅니다.

KServe의 강점

  • 서빙 표준화: InferenceService 하나로 배포·스케일·라우팅을 묶어 관리
  • 트래픽 분할: canaryTrafficPercent 로 버전 간 트래픽 스플릿이 단순
  • Knative 기반 옵션: 요청 기반 오토스케일(제로 스케일 포함)과 라우팅 기능 활용 가능
  • LLM 런타임 연동: VLLM, Triton, Hugging Face 등 런타임 패턴을 비교적 깔끔하게 수용

Seldon Core의 강점

  • 서빙 파이프라인/그래프: 전처리·후처리·앙상블·A/B 실험을 “그래프”로 구성
  • 실험/정책: 라우팅, 미러링, 실험 설계를 더 유연하게 가져가기 쉬움
  • 확장성: 모델 외 컴포넌트(피처, 룰, 안전장치)를 함께 묶어 운영하기 좋음

결론: “카나리만”이면 KServe가 더 직관적

LLM 교체 시 가장 흔한 요구는 새 모델로 트래픽을 1%→5%→20%→50%→100% 식으로 올리며 품질/지표를 검증하는 것입니다. 이 관점에서는 KServe의 InferenceService 트래픽 스플릿이 가장 단순합니다.

반면, 프롬프트 가드레일, 후처리 필터, 툴콜/에이전트 정책 같은 컴포넌트를 “서빙 그래프”로 강하게 묶고 싶다면 Seldon Core가 강점이 있습니다. (에이전트 기반 LLM에서 툴콜 폭주를 막는 패턴은 LangChain 에이전트 무한루프·툴콜 폭주 차단법 도 함께 참고하면 좋습니다.)

아키텍처: LLM 롤링·카나리에서 실제로 필요한 것

LLM 카나리는 “트래픽 분할”만으로 끝나지 않습니다. 아래 요소들이 함께 있어야 장애·품질 저하를 안전하게 흡수할 수 있습니다.

  • 버전 단위 엔드포인트: v1, v2를 동시에 띄워 비교 가능해야 함
  • 트래픽 스플릿: 요청의 일부만 v2로 보내기
  • 관측 포인트: 지연시간(p50/p95/p99), 에러율, 토큰 처리량, GPU 메모리/Util, OOM, 큐 길이
  • 품질 검증: 오프라인 골든셋 + 온라인 샘플링 평가(가능하면)
  • 롤백의 단순성: “퍼센트 0으로” 또는 “이전 버전만”으로 즉시 복귀
  • 콜드스타트 대응: 모델 로딩 시간과 워밍업을 배포 전략에 포함

특히 LLM은 새 버전이 “정상 응답”을 내더라도 지연시간이 2배가 되면 ALB/Ingress에서 502/504가 쏟아질 수 있습니다. 인프라 레벨 점검은 AWS ALB 502·504 난사 - 원인별 해결 체크리스트 를 같이 보세요.

KServe로 LLM 카나리 배포하기: InferenceService 트래픽 스플릿

여기서는 “동일한 모델 서버 런타임”에서 모델 아티팩트만 교체하는 가장 흔한 패턴을 예로 듭니다.

1) 기본 InferenceService (v1)

아래 예시는 개념 전달을 위한 스켈레톤입니다. 실제 운영에서는 런타임(vLLM/Triton/HF Server), 스토리지(S3/GCS/PVC), GPU 리소스, 프로브를 환경에 맞게 조정해야 합니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-chat
  namespace: llm
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 4
    containers:
      - name: model
        image: ghcr.io/your-org/llm-server:1.0.0
        args:
          - "--model"
          - "/mnt/models/v1"
        resources:
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "24Gi"
        volumeMounts:
          - name: model-store
            mountPath: /mnt/models
    volumes:
      - name: model-store
        persistentVolumeClaim:
          claimName: llm-model-pvc
  • 모델을 PVC에 두는 이유는 “노드 로컬 캐시”나 “이미지에 bake”하는 것보다 교체 시 유연하기 때문입니다.
  • PVC가 Pending 에 걸려 배포가 멈추는 경우가 흔하니, EBS CSI 등 스토리지클래스 점검은 K8s PVC Pending 해결 - EBS CSI StorageClass 를 참고하세요.

2) v2를 카나리로 추가하고 트래픽 5%부터 시작

KServe는 “현재 안정 버전”과 “카나리 버전”을 동시에 띄우고, canaryTrafficPercent 로 트래픽을 나눌 수 있습니다.

apiVersion: serving.kserve.io/v1beta1
kind: InferenceService
metadata:
  name: llm-chat
  namespace: llm
spec:
  predictor:
    minReplicas: 1
    maxReplicas: 4
    containers:
      - name: model
        image: ghcr.io/your-org/llm-server:1.0.0
        args:
          - "--model"
          - "/mnt/models/v1"
        resources:
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "24Gi"
        volumeMounts:
          - name: model-store
            mountPath: /mnt/models
    volumes:
      - name: model-store
        persistentVolumeClaim:
          claimName: llm-model-pvc
  canaryTrafficPercent: 5
  canaryPredictor:
    minReplicas: 1
    maxReplicas: 2
    containers:
      - name: model
        image: ghcr.io/your-org/llm-server:1.1.0
        args:
          - "--model"
          - "/mnt/models/v2"
        resources:
          limits:
            nvidia.com/gpu: "1"
            cpu: "4"
            memory: "24Gi"
        volumeMounts:
          - name: model-store
            mountPath: /mnt/models
    volumes:
      - name: model-store
        persistentVolumeClaim:
          claimName: llm-model-pvc

핵심은 다음입니다.

  • 동시에 떠 있음: predictor 는 v1, canaryPredictor 는 v2
  • 트래픽 분할: canaryTrafficPercent: 5 이면 v2로 5% 라우팅
  • 롤백: 문제가 생기면 canaryTrafficPercent0 으로 내리거나 canaryPredictor 를 제거

3) 단계적 승격(runbook)

운영에서 가장 중요한 건 “어떤 지표를 보고 언제 퍼센트를 올릴지”입니다. 아래는 LLM에 맞춘 현실적인 예시입니다.

  • 5% (10~30분)
    • 5xx 증가 여부
    • p95/p99 지연시간이 기존 대비 +20% 이내인지
    • GPU OOM/재시작이 없는지
  • 20% (30~60분)
    • 토큰 처리량(token/s) 또는 동시성 처리량이 충분한지
    • 큐잉 지연이 증가하지 않는지
  • 50% (1~2시간)
    • 장시간 안정성(메모리 누수, 캐시 폭증, 커널 리셋 등)
  • 100%
    • canaryTrafficPercent100 으로 올린 뒤, v1을 제거하거나 다음 릴리스까지 유지

이때 “응답 품질”은 단순히 에러율로 잡히지 않습니다. 가능하면 샘플링 기반 온라인 평가(예: 특정 태그 요청만 미러링해 비교)나, 최소한 골든 프롬프트 셋을 배포 파이프라인에 넣어야 합니다.

Seldon Core로 카나리/롤링을 설계할 때의 포인트

Seldon Core를 쓰는 팀은 보통 다음 니즈가 있습니다.

  • LLM 호출 전후에 정책 엔진/필터/PII 마스킹을 붙이고 싶다
  • “모델”만이 아니라 전체 추론 그래프를 버전업하고 싶다
  • A/B 테스트, 미러링, 조건 기반 라우팅을 더 정교하게 하고 싶다

Seldon Core의 전형적인 접근은 “단일 모델 서버”가 아니라, 여러 컴포넌트를 묶은 배포 단위로 관리하는 것입니다.

예시: 전처리 + LLM + 후처리 그래프(개념)

아래는 개념 예시이며, 실제 CRD 스펙은 사용 중인 Seldon Core 버전에 맞춰 확인해야 합니다.

apiVersion: machinelearning.seldon.io/v1
kind: SeldonDeployment
metadata:
  name: llm-pipeline
  namespace: llm
spec:
  name: llm-pipeline
  predictors:
    - name: v1
      replicas: 2
      graph:
        name: preprocessor
        type: MODEL
        children:
          - name: llm
            type: MODEL
            children:
              - name: postprocessor
                type: MODEL
      componentSpecs:
        - spec:
            containers:
              - name: preprocessor
                image: ghcr.io/your-org/prompt-guard:1.0.0
              - name: llm
                image: ghcr.io/your-org/llm-server:1.0.0
              - name: postprocessor
                image: ghcr.io/your-org/response-filter:1.0.0

이 구조의 장점은 “카나리 대상”을 LLM 서버 하나로 한정하지 않고, 가드레일/필터 포함 전체 체인으로 잡을 수 있다는 점입니다. LLM을 교체하면 종종 출력 포맷이 미묘하게 바뀌어 후처리가 깨지는데, 이런 장애를 그래프 단위로 함께 검증할 수 있습니다.

LLM 롤링배포에서 자주 터지는 문제와 대응

1) 콜드스타트가 길어 카나리에서만 타임아웃

  • 증상: v2로 라우팅된 요청만 p99이 튀고, Ingress/ALB에서 504 증가
  • 대응:
    • minReplicas 를 0으로 두지 말고 최소 1 이상 유지
    • 워밍업 요청을 배포 직후 자동 실행(헬스체크와 별도)
    • 모델 로딩을 init 단계에서 끝내고 readiness를 늦게 열기

2) GPU 메모리 여유가 없어 “이중 가동”이 불가능

  • 증상: v1과 v2를 동시에 띄우는 순간 OOM
  • 대응:
    • 카나리 기간에만 임시로 GPU 노드 증설
    • v2를 더 작은 배치/컨텍스트로 시작해 메모리 상한을 낮추기
    • 양자화(예: 4bit)나 FlashAttention 같은 최적화로 footprint 감소
    • 로컬 LLM 최적화 아이디어는 Transformers 로컬 LLM 느림·OOM, 4bit+FlashAttn2 참고

3) “성공 응답”인데 비용이 폭증

  • 증상: 에러율은 낮지만 token/s가 떨어져 GPU 시간이 급증
  • 대응:
    • 지표를 RPS 가 아니라 “토큰 처리량”과 “토큰당 지연시간”으로 보기
    • 샘플링으로 평균 출력 토큰 길이 변화 감시
    • 프롬프트 템플릿 변경이 원인일 수 있으니 앱 릴리스와 모델 릴리스를 분리

4) 롤백은 했는데 이미 나간 응답이 문제를 만든다

  • 증상: 특정 버전 응답이 캐시/DB에 저장되어 이후 장애 유발
  • 대응:
    • 응답 저장 시 model_version 을 함께 저장
    • 비동기 파이프라인이면 멱등키/아웃박스 패턴 고려(LLM 결과 저장도 결국 이벤트 처리)

운영 체크리스트: “카나리 승격”을 자동화하려면

수동으로 퍼센트를 올리는 것도 가능하지만, LLM은 변수가 많아 승격 조건을 코드로 박아두는 편이 안전합니다.

  • SLO 기반 조건
    • p95, p99 지연시간이 기준 이하
    • 5xx 비율이 기준 이하
    • OOM/재시작 횟수 0
  • 품질 기반 조건
    • 골든셋 자동 평가 점수 하락이 임계치 이내
    • 금칙어/PII/정책 위반 탐지율이 증가하지 않음
  • 비용 기반 조건
    • 토큰당 GPU 시간 또는 토큰당 비용이 기준 이하

이 조건들을 만족할 때 canaryTrafficPercent52050100 으로 올리고, 실패하면 즉시 0 으로 내리는 형태가 가장 단순한 자동화입니다.

마무리: 추천 조합

  • “모델만 교체” + “간단한 카나리” 가 목적이면: KServe InferenceService + canaryPredictor + canaryTrafficPercent
  • “가드레일/전후처리/실험”까지 그래프로 묶고 싶다 면: Seldon Core 그래프 기반 배포 + (필요 시) 별도 라우팅 정책

LLM 롤링·카나리의 본질은 “배포 기능”보다 관측과 승격 기준입니다. 트래픽을 나눌 수 있게 된 순간부터는, 어떤 지표로 v2를 신뢰할지(그리고 언제 즉시 철회할지)를 팀의 런북으로 고정하는 것이 운영 성숙도를 결정합니다.