LLM WikiAccess-protected knowledge portal
← 스터디 홈
16편 · 약 17분

KServe 0.20 RC0: LLMInferenceService가 모델별 라우팅을 안전하게 켜고 전역 설정을 캐시하는 법

왜 이 릴리스를 지금 봐야 하나

2026년 7월 16일 공개된 KServe v0.20.0-rc0는 전체 제품 소개보다 LLMInferenceService 제어면(control plane)을 다듬은 릴리스로 읽는 편이 정확하다. 릴리스 노트에서 눈에 띄는 변화는 세 가지다.

  • model-based routing를 안전하게 켜기 위한 게이트(annotation)와 모드 분리
  • status.addresses서비스가 실제로 서빙하는 모델 이름을 노출하는 상태 계약 확장
  • inferenceservice-config캐시로 읽고 변경 시 전체 LLMInferenceService를 재조정하도록 바꾼 설정 경로 최적화

이 셋은 각각 다른 문제를 푼다.

  1. 경로 안전성: 구버전 프리셋이 남아 있는 클러스터에서 헤더 기반 모델 라우팅이 조용히 켜지는 문제를 막는다.
  2. 관측성: 게이트웨이 주소가 어떤 모델 집합을 대표하는지 상태에서 바로 확인할 수 있다.
  3. 확장성: reconcile loop가 돌 때마다 API server에서 ConfigMap을 다시 읽지 않아도 된다.

즉, 이 RC는 "새 기능 하나 추가"보다 LLM 서빙을 운영 가능한 API로 굳히는 전환점에 가깝다.


무엇이 바뀌었나: 운영자 관점 요약

항목0.20 RC0에서 바뀐 점운영 의미
모델 기반 라우팅 게이트serving.kserve.io/model-based-routing-enabled: "true"가 있어야 기본 모드에서 활성화구 프리셋/혼합 버전 환경에서 silent enablement 방지
라우팅 규칙 처리 위치config merge 단계가 아니라 HTTPRoute 생성 단계에서 규칙 확장/제거템플릿은 유지하고, 실제 배포 시점에만 기능 토글 가능
상태 노출status.addresses[*].models[*].name 추가어떤 주소가 어떤 base model / LoRA adapter를 서빙하는지 확인 가능
전역 설정 읽기inferenceservice-config를 캐시된 client 경로로 읽고 변경 시 전체 서비스 enqueuereconcile당 API server 직접 조회 감소, 설정 반영 일관성 증가
조건 문서화GatewaysReady, HTTPRoutesReady, InferencePoolReady 등 condition 의미 정리대시보드/CLI에서 실패 지점 판단 쉬워짐

이 글에서는 특히 첫 세 항목을 하나의 흐름으로 묶어 보겠다. 라우팅 기능을 켜는 방식이 바뀌면 상태 표현과 검증 포인트도 같이 바뀌기 때문이다.

KServe 0.20 RC0 — LLMInferenceService 라우팅/설정 제어면 Controller reconcile + Gateway routing 1) loadConfig() cached client로 inferenceservice-config 조회 변경 시 모든 LLMInferenceService enqueue API server 직접 조회는 fallback 경로 2) model-based routing 판단 header name 존재? service annotation = true? gateway annotation이 false 아님? mode = enabled / forced / disabled 3) HTTPRoute 생성 게이트 ON → LoRA 매치 확장 게이트 OFF → header match 제거 같은 템플릿을 기능별로 재사용 규칙 확장/제거를 spec merge 밖으로 이동 4) status 갱신 addresses[*].models 조건 집계 GatewaysReady HTTPRoutesReady / InferencePoolReady inferenceservice-config modelBasedRoutingHeaderName modelBasedRoutingMode scheduler/ingress/global settings 게이트 판정 enabled: 서비스=true + 게이트웨이!=false forced: 무조건 사용 disabled: 항상 제거 구 프리셋의 silent enablement 차단 Gateway / HTTPRoute header 기반 모델 선택 LoRA adapter 이름을 base model과 같은 풀로 매핑 비활성 시 관련 match 자체를 spec에서 제거 조건 실패 시 HTTPRoutesReady=False 상태 소비자 kubectl 대시보드 자동 검증 파이프라인 주소 ↔ 모델 매핑 확인 운영 리스크 포인트 RC0이므로 GA 전 필드/조건 의미가 바뀔 수 있다. 특히 mixed-version preset, gateway annotation override, 전역 ConfigMap fan-out 규모를 canary에서 먼저 검증해야 한다. 장점은 분명하지만, 잘못된 header 계약이나 구 프리셋 재사용은 라우팅 불일치보다 더 조용한 misroute를 만들 수 있다.
KServe 0.20 RC0 LLMInferenceService 제어면 흐름

1. 모델 기반 라우팅은 이제 "있으면 켜지는 기능"이 아니다

이번 변경의 핵심은 isModelBasedRoutingEnabled() 판단 로직이다. 이제 컨트롤러는 다음 세 조건을 먼저 본다.

  1. 전역 설정에 modelBasedRoutingHeaderName이 있는가
  2. 서비스 스펙의 annotation serving.kserve.io/model-based-routing-enabled"true"인가
  3. 게이트웨이 쪽에서 같은 annotation을 "false"로 강제하지 않았는가 (enabled 모드 기준)

이 순서가 중요하다. 예전처럼 단지 HTTPRoute 템플릿에 헤더 매치가 있다고 해서 라우팅 기능이 살아나지 않는다. 워크로드가 대체 서빙 모델 이름(alternative served model name)을 포함한 새 프리셋으로 배포됐다는 신호가 있어야 한다.

즉, 업그레이드 중인 클러스터에서 가장 위험한 상황이던 "구 템플릿이지만 신기능이 조용히 활성화되는 상태"를 막는다. 릴리스 노트와 PR 설명이 모두 이 문제를 직접 언급한다.

모드 해석: enabled / forced / disabled

Config에는 두 필드가 들어간다.

  • ModelBasedRoutingHeaderName
  • ModelBasedRoutingMode

모드는 세 가지다.

모드의미언제 쓰나
enabled기본값. 서비스 annotation과 게이트웨이 상태를 함께 본다일반 운영 기본값
forced게이트웨이 override를 무시하고 기능을 강제한다실험/테스트 환경, 일시적 강제 전환
disabled헤더 기반 모델 라우팅을 완전히 제거한다호환성 문제, 롤백, 구 게이트웨이 유지

운영적으로는 enabled가 안전한 기본값이고, forced는 롤백 플랜 없이 오래 유지하면 안 된다. 강제로 켠 상태는 잘못된 헤더 계약을 더 빨리 드러내지만, 동시에 misroute를 더 빠르게 확산시킬 수도 있다.


2. 규칙 확장/제거를 HTTPRoute 생성 단계로 옮긴 이유

이번 PR은 라우팅 규칙 확장과 제거를 config merge 단계가 아니라 expectedHTTPRoute() 생성 단계에서 수행한다.

이 차이가 왜 중요할까?

이전 문제

설정 병합 단계에서 모델 라우팅 규칙을 확장해 버리면, 최종 merged spec이 이미 특정 기능 전제를 내장하게 된다. 그러면 다음 두 상황을 다루기 어렵다.

  • 같은 템플릿을 쓰되 클러스터/게이트웨이별로 기능 ON/OFF를 다르게 하고 싶을 때
  • 구 프리셋과 신 프리셋이 섞여 있는 업그레이드 구간에서 안전하게 기능을 접고 싶을 때

RC0 이후 동작

expectedHTTPRoute()는 실제 라우트 객체를 만들 때 이렇게 판단한다.

  • 기능이 켜져 있으면 expandLoRAAdapterMatches()LoRA adapter 이름까지 같은 백엔드 풀로 매치한다.
  • 기능이 꺼져 있으면 stripModelBasedRoutingRules()해당 header match를 아예 제거한다.

즉, 템플릿은 더 일반적인 상태로 남고, 실제 배포 결과만 환경에 맞게 달라진다. 이건 GitOps 관점에서도 장점이 있다. 템플릿 저장소를 복제하지 않고, 컨트롤러가 마지막 mile에서 기능 플래그를 적용하기 때문이다.

LoRA에서 특히 유용한 이유

LoRA adapter는 base model 위에 이름이 여러 개 붙는 구조라, 게이트웨이 입장에서는 "이 요청이 결국 어느 base pool로 가야 하는가"가 중요하다. RC0의 확장 로직은 base model OR adapter-1 OR adapter-2 형태의 매치를 같은 InferencePool에 붙여 준다. 이 덕분에 운영자는 adapter 수가 늘어나도 풀 구성 자체를 늘리지 않고 헤더 계약만 확장할 수 있다.


3. status.addresses에 모델 이름이 붙으면 무엇이 달라지나

LLM 서빙에서 주소 하나가 모델 하나를 뜻하던 시기는 짧았다. shared gateway, scheduler, LoRA adapter, multi-node 배포가 들어오면 주소와 모델의 관계는 다대다가 된다.

RC0은 이 현실을 status.addresses[*].models[*].name으로 드러낸다. 상태 타입을 보면 SourcedAddressAddressableOrigin에 더해 Models []ModelSourcedAddressStatus를 가진다.

운영자 입장에서 이 필드가 유용한 이유는 세 가지다.

  1. 게이트웨이 주소가 어떤 모델 집합을 대표하는지 확인할 수 있다.
  2. LoRA adapter 이름이 실제 라우팅 표면에 노출됐는지 검증할 수 있다.
  3. 추후 대시보드나 admission check에서 "주소는 살아 있는데 모델 노출이 비어 있는 상태"를 비정상으로 간주할 수 있다.

중요한 점은 이 상태가 아직 모델 이름만 노출한다는 점이다. 가중치, 버전, tenant, rollout percentage 같은 정보는 없다. 그래서 이 필드만으로 트래픽 분산 정책 전체를 복원할 수는 없다. 하지만 적어도 kubectl get -o yaml 수준에서 "라우팅이 어느 이름으로 열렸는지"를 바로 확인할 수 있게 된 건 큰 진전이다.


4. 전역 ConfigMap을 매번 API server에서 읽지 않는다는 것의 의미

PR #5573은 겉으로는 작은 최적화처럼 보이지만, 실제로는 컨트롤러 확장성에 직접 닿아 있다. 설명 그대로 이전 구현은 다음 지점들에서 reconcile마다 API server에 직접 접근했다.

  • reconcile loop 본문
  • gateway / HTTPRoute / InferencePool change handler
  • routing status update
  • scheduler deployment 예상 계산

RC0에서는 loadConfig()가 controller-runtime client의 캐시된 read 경로를 쓰고, cmd/llmisvc/main.go에서 inferenceservice-config 하나만 잡는 field-selector 기반 cache scope를 추가한다. 그리고 해당 ConfigMap 변경을 별도 watch 경로로 받아 모든 LLMInferenceService를 enqueue한다.

왜 이 설계가 낫나

  • 평상시에는 캐시 hit로 끝나므로 API server 압력이 낮아진다.
  • 전역 설정이 바뀌면 변경 사실을 놓치지 않고 fan-out reconcile이 일어난다.
  • nonInferenceServiceConfigMapPredicate와 별도 watch를 분리해 중복 이벤트 경로를 피한다.

트레이드오프도 있다

전역 ConfigMap 하나를 바꾸면 모든 LLMInferenceService가 다시 줄을 선다. 서비스 수가 아주 많은 클러스터라면 이건 여전히 큰 reconcile burst다. 다만 이전처럼 각 서비스가 자기 차례마다 API server를 때리는 패턴보다는 제어하기 쉽다. 운영자는 이 변경을 "부하 제거"로만 이해하면 안 되고, burst를 예측 가능한 한 번의 fan-out으로 바꾼 것으로 이해하는 편이 맞다.


5. 이제 어디가 고장 났는지 status condition만으로 더 빨리 좁힐 수 있다

RC0 직전과 직후를 비교할 때 자주 놓치는 부분이 ConditionType 문서화다. 이번 릴리스에서 condition 주석이 강화되면서 RouterReady 아래에 무엇이 집계되는지가 명확해졌다.

  • GatewaysReady
  • HTTPRoutesReady
  • InferencePoolReady
  • SchedulerWorkloadReady

이 구조는 중요하다. 예를 들어 요청이 404/503으로 보인다고 해서 곧바로 모델 워크로드 문제로 가면 안 된다.

  1. GatewaysReady=False면 네트워크 부모 리소스부터 봐야 한다.
  2. HTTPRoutesReady=False면 route 생성/acceptance/ResolvedRefs 계층 문제다.
  3. InferencePoolReady=False면 스케줄러 또는 backendRef 마이그레이션 구간을 봐야 한다.
  4. SchedulerWorkloadReady=False면 실제 scheduler deployment/pod 측면이다.

즉, RC0의 라우팅 게이트는 단지 기능 토글이 아니라 문제 분해 방식 자체를 더 구조화한다.


운영자가 봐야 할 실제 검증 포인트

1) 기능이 켜졌는지

metadata:
  annotations:
    serving.kserve.io/model-based-routing-enabled: "true"

이 annotation이 없으면 기본 모드에서는 헤더 기반 모델 라우팅이 켜지지 않는다. 업그레이드 후 "왜 헤더를 보내도 base route만 타지?"라는 질문이 나오면 제일 먼저 여기부터 봐야 한다.

2) 헤더 계약이 무엇인지

전역 설정의 modelBasedRoutingHeaderName이 비어 있으면 기능이 무조건 꺼진다. 템플릿과 게이트웨이 문서에서 실제 헤더 이름이 일치하는지 확인해야 한다.

3) 상태에 모델이 보이는지

status.addresses를 봤을 때 기대한 base model / LoRA adapter 이름이 들어가야 한다. 이름이 비어 있거나 일부만 보이면 규칙 확장 또는 상태 반영 경로를 의심해야 한다.

4) ConfigMap 변경 반영이 재조정으로 이어지는지

inferenceservice-config를 수정한 뒤 전체 LLMInferenceService가 다시 reconcile되는지 확인해야 한다. 반영이 안 되면 캐시 watch, predicate, 또는 field-selector scope가 잘못된 것이다.


도입 체크리스트

  • [ ] KServe v0.20.0-rc0를 production-wide로 올리기 전에 canary cluster에서 먼저 검증한다.
  • [ ] 사용하는 LLMInferenceService preset/template에 serving.kserve.io/model-based-routing-enabled: "true"가 실제로 들어가는지 확인한다.
  • [ ] modelBasedRoutingHeaderName와 게이트웨이/클라이언트가 보내는 헤더 이름이 정확히 일치하는지 확인한다.
  • [ ] mixed-version 환경이면 ModelBasedRoutingMode=enabled로 시작하고 forced는 짧은 실험에만 사용한다.
  • [ ] kubectl get llminferenceservice -o yaml에서 status.addresses[*].models[*].name에 base model과 LoRA adapter가 기대대로 노출되는지 확인한다.
  • [ ] RouterReady, GatewaysReady, HTTPRoutesReady, InferencePoolReady를 대시보드에 분리해 표시한다.
  • [ ] inferenceservice-config 변경 후 reconcile burst가 어느 정도인지 controller QPS / workqueue metrics로 측정한다.
  • [ ] RC 단계이므로 GA 전 field name, condition semantics, preset default가 바뀔 가능성을 감안해 롤백 플랜을 준비한다.

어디까지를 기대해야 하고, 어디부터는 아직 이르나

이번 RC0는 분명 좋은 방향이다. 하지만 과대평가하면 안 된다.

  • 이것만으로 multi-model traffic splitting, weighted canary, tenant isolation이 완성되는 것은 아니다.
  • status.addresses의 모델 정보는 디버깅에는 유용하지만, 아직 배포 정책 전체의 진실 원천(source of truth) 은 아니다.
  • ConfigMap caching은 API server 부하를 줄여 주지만, 대규모 클러스터에서는 전역 설정 변경 시의 fan-out burst를 별도로 다뤄야 한다.

그래도 운영자 입장에서는 충분히 의미 있다. RC0는 LLMInferenceService를 단순 CRD가 아니라 게이트, 상태 계약, 캐시 전략을 갖춘 컨트롤 플레인 API로 밀어 올리고 있기 때문이다.

Open question

KServe 웹사이트 문서가 이 RC0의 status.addresses[*].models와 model-based routing gate 동작을 언제 정식 운영 문서로 흡수할지는 아직 불명확하다. 현재로서는 릴리스 노트, PR, 태그된 소스 코드가 더 정확한 1차 자료다.

References

  • https://github.com/kserve/kserve/releases/tag/v0.20.0-rc0
  • https://github.com/kserve/kserve/pull/5579
  • https://github.com/kserve/kserve/pull/5573
  • https://github.com/kserve/kserve/pull/5586
  • https://github.com/kserve/kserve/blob/v0.20.0-rc0/config/llmisvcconfig/config-llm-template.yaml
  • https://github.com/kserve/kserve/blob/v0.20.0-rc0/pkg/controller/v1alpha2/llmisvc/config_merge.go
  • https://github.com/kserve/kserve/blob/v0.20.0-rc0/pkg/controller/v1alpha2/llmisvc/config_loader.go
  • https://github.com/kserve/kserve/blob/v0.20.0-rc0/pkg/controller/v1alpha2/llmisvc/controller.go
  • https://github.com/kserve/kserve/blob/v0.20.0-rc0/pkg/apis/serving/v1alpha2/llm_inference_service_types.go
  • https://github.com/kserve/kserve/blob/v0.20.0-rc0/pkg/apis/serving/v1alpha2/llm_inference_service_lifecycle.go