KServe LLMInferenceService로 모델 서빙
KServe의 LLMInferenceService로 GLM-5.3을 Prefill과 Decode가 분리된 구조로 배포합니다. Prefill은 단일 노드 TP8 Deployment로 실행되고, Decode는 두 노드로 구성된 LeaderWorkerSet으로 실행됩니다.
시작하기 전에
- 사용 전 준비를 참고하여 네임스페이스,
modelsPVC, Gateway, NetworkAttachmentDefinition을 확인합니다. - Queue와 Scheduler 사용을 참고하여
servingLocalQueue를 준비합니다. modelsPVC의<모델 경로>에 모델 가중치를 준비합니다.- 서비스 호스트명과 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-pool | KServe | Prefill Pod와 Decode Leader Pod를 묶고 EPP에 연결 |
<서비스명>-kserve-route | KServe | 서비스 호스트명으로 요청을 받는 HTTPRoute |
| AIGatewayRoute와 같은 이름의 HTTPRoute | AI Gateway | AIGatewayRoute 호스트명으로 요청을 받는 HTTPRoute |
EPP는 Prefill 프로파일로 Prefill Pod를 선택하고 Decode 프로파일로 Decode Pod를 선택합니다. KV 캐시는 UCX와 InfiniBand를 통해 Prefill에서 Decode로 전달됩니다.
주요 설정
공통 설정
| 필드 | 설명 |
|---|---|
metadata.labels | Kueue Queue와 우선순위. kueue.x-k8s.io/ 접두사 라벨만 생성되는 Pod에 전파 |
metadata.annotations | InfiniBand NetworkAttachmentDefinition. NIC 요청 수만큼 네임스페이스 이름 반복 |
spec.model.uri | pvc://models/<모델 경로> 형식의 모델 위치 |
spec.model.name | vLLM이 노출하는 모델 이름. 요청 본문의 model 값과 동일해야 함 |
spec.annotations | Decode Pod의 Istio 사이드카 주입 비활성화 |
spec.prefill.annotations | Prefill Pod의 Istio 사이드카 주입 비활성화 |
spec.router.scheduler.annotations | EPP Pod의 Istio 사이드카 주입 비활성화 |
Decode 설정
tensor: 8,data: 2,dataLocal: 1로 설정하면 Decode 그룹 하나가 Leader 1개와 Worker 1개, 총 2개 노드와 16개 GPU로 구성됩니다.- Leader의 라우팅 사이드카는 8000 포트에서 요청을 받고 8001 포트의 vLLM으로 전달합니다.
kv_role은kv_consumer로 지정합니다.- Leader와 Worker 모두
volcano-scheduler를 사용합니다.
Prefill 설정
- Prefill은 단일 노드 TP8 Deployment로 구성합니다.
kv_role은kv_producer로 지정합니다.--max-num-batched-tokens와--enforce-eager를 Prefill 전용 옵션으로 사용합니다.
spec.router.route.http.spec에 hostnames만 지정하고 rules를 생략하면 InferencePool이 Gateway에 연결되지 않습니다. rules[].backendRefs에 <서비스명>-inference-pool을 반드시 지정하세요.
spec.worker에는 initContainers를 추가하지 마세요. 라우팅 사이드카는 Leader에만 필요합니다. Worker에 같은 항목을 추가하면 Worker Pod 생성이 실패하고 Gang을 구성할 수 없어 Leader도 Pending 상태에 머뭅니다.
Step 1. LLMInferenceService 생성
다음 매니페스트를 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가 Pending | Gang 배치에 필요한 GPU 노드 2대를 확보할 수 있는지 확인하고 Pod 이벤트 점검 |
InferencePoolReady가 WaitingForGateway | HTTPRoute의 rules[].backendRefs가 InferencePool을 참조하는지 확인 |
Worker Pod가 생성되지 않고 Leader도 Pending | spec.worker에 불필요한 initContainers가 추가되었는지 확인 |
자세한 확인 명령은 문제 해결을 참고하세요.
리소스 정리
kubectl delete aigatewayroute <AIGatewayRoute 이름> -n <사용자 namespace>
kubectl delete llminferenceservice <서비스명> -n <사용자 namespace>
자동 생성된 Prefill Deployment, Decode LeaderWorkerSet, EPP, InferencePool과 HTTPRoute가 함께 삭제되었는지 확인합니다.