Skip to main content

KServe LLMInferenceService로 모델 서빙

KServe의 LLMInferenceService로 GLM-5.3을 Prefill과 Decode가 분리된 구조로 배포합니다. Prefill은 단일 노드 TP8 Deployment로 실행되고, Decode는 두 노드로 구성된 LeaderWorkerSet으로 실행됩니다.

시작하기 전에

  • 사용 전 준비를 참고하여 네임스페이스, models PVC, Gateway, NetworkAttachmentDefinition을 확인합니다.
  • Queue와 Scheduler 사용을 참고하여 serving LocalQueue를 준비합니다.
  • models PVC의 <모델 경로>에 모델 가중치를 준비합니다.
  • 서비스 호스트명과 AIGatewayRoute 호스트명은 *.moduai.kakaocloud.com 아래에서 서로 다른 값으로 준비합니다.

구성

요청은 Gateway, HTTPRoute, InferencePool을 거쳐 Decode Pod에 전달됩니다. Decode Pod는 Prefill Pod에서 생성한 KV 캐시를 전달받아 토큰을 생성합니다.

리소스생성 주체기능
LLMInferenceService사용자Prefill Deployment, Decode LeaderWorkerSet, EPP, InferencePool, HTTPRoute 생성
AIGatewayRoute사용자같은 InferencePool을 별도 호스트명으로 노출하고 모델 이름으로 라우팅
BackendTrafficPolicy사용자두 HTTPRoute의 타임아웃, 재시도, Rate Limit 설정
<서비스명>-inference-poolKServePrefill Pod와 Decode Leader Pod를 묶고 EPP에 연결
<서비스명>-kserve-routeKServe서비스 호스트명으로 요청을 받는 HTTPRoute
AIGatewayRoute와 같은 이름의 HTTPRouteAI GatewayAIGatewayRoute 호스트명으로 요청을 받는 HTTPRoute

EPP는 Prefill 프로파일로 Prefill Pod를 선택하고 Decode 프로파일로 Decode Pod를 선택합니다. KV 캐시는 UCX와 InfiniBand를 통해 Prefill에서 Decode로 전달됩니다.

주요 설정

공통 설정

필드설명
metadata.labelsKueue Queue와 우선순위. kueue.x-k8s.io/ 접두사 라벨만 생성되는 Pod에 전파
metadata.annotationsInfiniBand NetworkAttachmentDefinition. NIC 요청 수만큼 네임스페이스 이름 반복
spec.model.uripvc://models/<모델 경로> 형식의 모델 위치
spec.model.namevLLM이 노출하는 모델 이름. 요청 본문의 model 값과 동일해야 함
spec.annotationsDecode Pod의 Istio 사이드카 주입 비활성화
spec.prefill.annotationsPrefill Pod의 Istio 사이드카 주입 비활성화
spec.router.scheduler.annotationsEPP Pod의 Istio 사이드카 주입 비활성화

Decode 설정

  • tensor: 8, data: 2, dataLocal: 1로 설정하면 Decode 그룹 하나가 Leader 1개와 Worker 1개, 총 2개 노드와 16개 GPU로 구성됩니다.
  • Leader의 라우팅 사이드카는 8000 포트에서 요청을 받고 8001 포트의 vLLM으로 전달합니다.
  • kv_rolekv_consumer로 지정합니다.
  • Leader와 Worker 모두 volcano-scheduler를 사용합니다.

Prefill 설정

  • Prefill은 단일 노드 TP8 Deployment로 구성합니다.
  • kv_rolekv_producer로 지정합니다.
  • --max-num-batched-tokens--enforce-eager를 Prefill 전용 옵션으로 사용합니다.
중요

spec.router.route.http.spechostnames만 지정하고 rules를 생략하면 InferencePool이 Gateway에 연결되지 않습니다. rules[].backendRefs<서비스명>-inference-pool을 반드시 지정하세요.

중요

spec.worker에는 initContainers를 추가하지 마세요. 라우팅 사이드카는 Leader에만 필요합니다. Worker에 같은 항목을 추가하면 Worker Pod 생성이 실패하고 Gang을 구성할 수 없어 Leader도 Pending 상태에 머뭅니다.

Step 1. LLMInferenceService 생성

다음 매니페스트를 llm-inference-service.yaml로 저장합니다.

llm-inference-service.yaml
apiVersion: serving.kserve.io/v1alpha2
kind: LLMInferenceService
metadata:
name: <서비스명>
namespace: <사용자 namespace>
labels:
kueue.x-k8s.io/queue-name: serving
kueue.x-k8s.io/priority-class: high
annotations:
k8s.v1.cni.cncf.io/networks: |-
<사용자 namespace>,
<사용자 namespace>,
<사용자 namespace>,
<사용자 namespace>,
<사용자 namespace>,
<사용자 namespace>,
<사용자 namespace>,
<사용자 namespace>
spec:
model:
uri: pvc://models/<모델 경로>
name: zai-org/GLM-5.3
annotations:
sidecar.istio.io/inject: "false"

router:
gateway:
refs:
- name: <사용자 namespace>
namespace: <사용자 namespace>
route:
http:
spec:
hostnames:
- <서비스 호스트명>.moduai.kakaocloud.com
rules:
- matches:
- path:
type: PathPrefix
value: /
backendRefs:
- group: inference.networking.k8s.io
kind: InferencePool
name: <서비스명>-inference-pool
port: 8000
scheduler:
labels:
kueue.x-k8s.io/queue-name: serving
kueue.x-k8s.io/priority-class: high
annotations:
sidecar.istio.io/inject: "false"
config:
inline:
apiVersion: llm-d.ai/v1alpha1
kind: EndpointPickerConfig
plugins:
- type: disagg-headers-handler
- type: always-disagg-pd-decider
- type: disagg-profile-handler
parameters:
deciders:
prefill: always-disagg-pd-decider
- type: prefill-filter
- type: decode-filter
- type: approx-prefix-cache-producer
parameters:
maxPrefixTokensToMatch: 131072
- type: inflight-load-producer
- type: prefix-cache-affinity-filter
parameters:
peakPrefillThroughput: 33821
- type: token-load-scorer
- type: active-request-scorer
- type: max-score-picker
schedulingProfiles:
- name: prefill
plugins:
- pluginRef: prefill-filter
- pluginRef: prefix-cache-affinity-filter
- pluginRef: token-load-scorer
- pluginRef: max-score-picker
- name: decode
plugins:
- pluginRef: decode-filter
- pluginRef: active-request-scorer
- pluginRef: max-score-picker

replicas: 1
parallelism:
tensor: 8
data: 2
dataLocal: 1
template:
schedulerName: volcano-scheduler
initContainers:
- name: llm-d-routing-sidecar
args:
- --port=8000
- --vllm-port=8001
- --kv-connector=nixlv2
- --secure-proxy=false
- --enable-ssrf-protection=false
containers:
- name: main
image: docker.io/vllm/vllm-openai:v0.28.0
args:
- --kv-cache-dtype=fp8_e4m3
- --max-num-seqs=32
- --no-enable-prefix-caching
- --speculative-config='{"method":"mtp","num_speculative_tokens":5}'
- --tool-call-parser=glm47
- --reasoning-parser=glm45
- --enable-auto-tool-choice
- --kv-transfer-config='{"kv_connector":"NixlConnector","kv_role":"kv_consumer","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"]}}'
env:
- name: UCX_TLS
value: rc_mlx5,rc,cuda_ipc,cuda_copy,sm,shm,self
- name: UCX_NET_DEVICES
value: all
- name: NCCL_SOCKET_IFNAME
value: eth0
- name: GLOO_SOCKET_IFNAME
value: eth0
- name: VLLM_NIXL_SIDE_CHANNEL_HOST
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: VLLM_NIXL_SIDE_CHANNEL_PORT
value: "5600"
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
securityContext:
runAsNonRoot: false
resources:
limits:
nvidia.com/gpu: "8"
nvidia.com/mlnxnics: "8"
cpu: "200"
memory: 1800Gi
requests:
nvidia.com/gpu: "8"
nvidia.com/mlnxnics: "8"
cpu: "200"
memory: 1800Gi
volumes:
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 64Gi

worker:
schedulerName: volcano-scheduler
containers:
- name: main
image: docker.io/vllm/vllm-openai:v0.28.0
args:
- --kv-cache-dtype=fp8_e4m3
- --max-num-seqs=32
- --no-enable-prefix-caching
- --speculative-config='{"method":"mtp","num_speculative_tokens":5}'
- --tool-call-parser=glm47
- --reasoning-parser=glm45
- --enable-auto-tool-choice
- --kv-transfer-config='{"kv_connector":"NixlConnector","kv_role":"kv_consumer","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"]}}'
env:
- name: UCX_TLS
value: rc_mlx5,rc,cuda_ipc,cuda_copy,sm,shm,self
- name: UCX_NET_DEVICES
value: all
- name: NCCL_SOCKET_IFNAME
value: eth0
- name: GLOO_SOCKET_IFNAME
value: eth0
- name: VLLM_NIXL_SIDE_CHANNEL_HOST
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: VLLM_NIXL_SIDE_CHANNEL_PORT
value: "5600"
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
securityContext:
runAsNonRoot: false
resources:
limits:
nvidia.com/gpu: "8"
nvidia.com/mlnxnics: "8"
cpu: "200"
memory: 1800Gi
requests:
nvidia.com/gpu: "8"
nvidia.com/mlnxnics: "8"
cpu: "200"
memory: 1800Gi
volumes:
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 64Gi

prefill:
replicas: 1
parallelism:
tensor: 8
annotations:
sidecar.istio.io/inject: "false"
template:
schedulerName: volcano-scheduler
containers:
- name: main
image: docker.io/vllm/vllm-openai:v0.28.0
args:
- --kv-cache-dtype=fp8_e4m3
- --max-num-batched-tokens=16384
- --no-enable-prefix-caching
- --enforce-eager
- --speculative-config='{"method":"mtp","num_speculative_tokens":5}'
- --tool-call-parser=glm47
- --reasoning-parser=glm45
- --enable-auto-tool-choice
- --kv-transfer-config='{"kv_connector":"NixlConnector","kv_role":"kv_producer","kv_buffer_device":"cuda","kv_connector_extra_config":{"backends":["UCX"]}}'
env:
- name: UCX_TLS
value: rc_mlx5,rc,cuda_ipc,cuda_copy,sm,shm,self
- name: UCX_NET_DEVICES
value: all
- name: NCCL_SOCKET_IFNAME
value: eth0
- name: GLOO_SOCKET_IFNAME
value: eth0
- name: VLLM_NIXL_SIDE_CHANNEL_HOST
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: VLLM_NIXL_SIDE_CHANNEL_PORT
value: "5600"
- name: VLLM_HTTP_TIMEOUT_KEEP_ALIVE
value: "120"
securityContext:
runAsNonRoot: false
resources:
limits:
nvidia.com/gpu: "8"
nvidia.com/mlnxnics: "8"
cpu: "200"
memory: 1800Gi
requests:
nvidia.com/gpu: "8"
nvidia.com/mlnxnics: "8"
cpu: "200"
memory: 1800Gi
volumes:
- name: dshm
emptyDir:
medium: Memory
sizeLimit: 64Gi

매니페스트를 적용합니다.

kubectl apply -f llm-inference-service.yaml
주의

배포 후 metadata.labels 또는 spec.prefill.labels를 변경하면 Prefill Deployment의 selector가 변경되어 field is immutable 오류가 발생할 수 있습니다. 이 경우 Prefill Deployment를 삭제하면 KServe 컨트롤러가 다시 생성합니다.

Step 2. Gateway에 연결

LLMInferenceService가 생성한 InferencePool을 별도의 호스트명으로 노출하려면 Gateway 설정 - AI Gateway 구성을 참고하세요.

Step 3. 배포 상태 확인

kubectl get llmisvc <서비스명> -n <사용자 namespace>
kubectl get lws,deploy,pods -l app.kubernetes.io/name=<서비스명> -n <사용자 namespace> -o wide
kubectl get podgroup,workload -n <사용자 namespace>
kubectl get inferencepool,httproute -n <사용자 namespace>

다음 조건을 확인합니다.

  • Prefill Deployment가 1/1 상태인지 확인합니다.
  • Decode Leader와 Worker Pod가 서로 다른 노드에서 Running 상태인지 확인합니다.
  • EPP Pod가 Running 상태인지 확인합니다.
  • Kueue Workload의 ADMITTED 조건이 True인지 확인합니다.
  • LLMInferenceService의 모든 상태 조건이 True인지 확인합니다.
kubectl get llmisvc <서비스명> -n <사용자 namespace> \
-o jsonpath='{range .status.conditions[*]}{.type}{"\t"}{.status}{"\t"}{.reason}{"\n"}{end}'

문제 해결

증상확인 항목
Pod가 SchedulingGated 상태Kueue가 Workload를 승인하지 않은 상태. Queue 라벨 위치와 요청 자원 확인
Workload가 승인됐지만 Decode Pod가 PendingGang 배치에 필요한 GPU 노드 2대를 확보할 수 있는지 확인하고 Pod 이벤트 점검
InferencePoolReadyWaitingForGatewayHTTPRoute의 rules[].backendRefs가 InferencePool을 참조하는지 확인
Worker Pod가 생성되지 않고 Leader도 Pendingspec.worker에 불필요한 initContainers가 추가되었는지 확인

자세한 확인 명령은 문제 해결을 참고하세요.

리소스 정리

kubectl delete aigatewayroute <AIGatewayRoute 이름> -n <사용자 namespace>
kubectl delete llminferenceservice <서비스명> -n <사용자 namespace>

자동 생성된 Prefill Deployment, Decode LeaderWorkerSet, EPP, InferencePool과 HTTPRoute가 함께 삭제되었는지 확인합니다.