콘텐츠로 이동

Pod 명세 요구 사항

목표: 이 스택의 Pod가 요구하는 권한, 포트, 볼륨, CPU, 메모리를 확인하고, 클러스터 정책과 노드의 할당 가능(allocatable) 리소스가 이 요구를 허용하는지 확인합니다.

권한 및 실행 모드

Pod에 필요한 권한

레퍼런스 구성은 기본 실행 모드인 non-root로 modelserver를 실행합니다. non-root 모드에서 차트는 modelserver Pod와 컨테이너에 다음 값을 설정합니다.

항목 기본값 설명
실행 사용자 uid 2000(runAsUser: 2000, runAsGroup: 0) 이미지가 USER 2000을 선언합니다
컨테이너 securityContext runAsNonRoot: true, allowPrivilegeEscalation: false, readOnlyRootFilesystem: true, capabilities.drop: [ALL], seccompProfile: RuntimeDefault 차트가 modelserver 컨테이너에 적용합니다
추가 capability unset non-root에서는 capabilities.add가 적용되지 않으므로 capability를 추가하면 효과 없이 정책 위반만 발생합니다
fsGroup 2000(fsGroupChangePolicy: OnRootMismatch) 컴파일 캐시 등의 볼륨에 쓸 수 있게 합니다

non-root 모드는 Pod Security Standards의 baseline을 충족합니다. non-root 모드는 restricted를 충족하지 못합니다.

non-root 및 root

non-root 모드에서는 각 노드의 컨테이너 런타임에서 modelserver 컨테이너의 memlock 제한을 unlimited로 설정하세요(containerd LimitMEMLOCK drop-in 참고). NIXL은 KV 전송 버퍼를 RDMA용으로 등록할 때(ibv_reg_mr) 그 버퍼의 메모리를 고정합니다. 고정한 메모리는 RLIMIT_MEMLOCK에 포함됩니다. memlock 제한을 unlimited로 올려야 하는 작업은 P/D 분리에서 RoCE로 KV를 전송하는 작업뿐입니다. NPU 사용 자체에는 memlock 제한을 올릴 필요가 없습니다.

RDMA를 활성화한 경우(devices.rdma.enabled: true) memlock 제한이 unlimited가 아니면 엔트리포인트가 시작 시 실패합니다.

관리형 Kubernetes처럼 노드의 컨테이너 런타임 설정을 바꿀 수 없으면 차트에 examples/root-privileged.yaml을 -f로 적용하세요. root 모드에서는 엔트리포인트가 ulimit -l unlimited로 memlock 제한을 직접 올립니다. 스택의 네임스페이스는 privileged Pod를 허용해야 합니다. root 모드는 Unverified입니다.

1
2
3
helm upgrade --install <release-name> ./coding-assistant-stack -n <ns> \
  -f ./coding-assistant-stack/examples/root-privileged.yaml \
  <values to fill in>

컨테이너 포트

NetworkPolicy나 방화벽으로 Pod 간 트래픽을 제한하는 클러스터에서는 다음 표의 포트와 NIXL 사이드 채널 포트로 가는 Pod 간 트래픽을 허용하세요.

다음 표는 레퍼런스 구성 기준입니다. 레퍼런스 구성에서 prefill은 pipeline parallel(PP) 병렬도 4로, decode는 data parallel(DP) 병렬도 4로 실행하고, decode 앞에 라우팅 사이드카를 둡니다. 이름 열은 차트가 선언하는 컨테이너 포트 이름입니다. 병렬도가 다르면 rank 포트 수도 달라집니다(주요 차트 설정 참고).

P/D role 포트 이름 수신하는 프로세스
prefill 8000 modelserver API 서버
prefill 8001, 8002, 8003 vllm-rank1..3 포트 shim(8000으로 전달)
prefill 5555 dp-rpc 선언되지만 수신하는 프로세스 없음
prefill 5600 nixl 선언되지만 수신하는 프로세스 없음
decode 8000 sidecar 라우팅 사이드카
decode 8200 modelserver rank 0
decode 8201, 8202, 8203 vllm-rank1..3 rank 1, 2, 3
decode 5555 dp-rpc DP rank 조정
decode 5600 nixl 선언되지만 수신하는 프로세스 없음

prefill의 8001, 8002, 8003은 포트 shim이 받습니다. Endpoint Picker(EPP)와 라우팅 사이드카는 prefill에도 decode와 같은 포트 목록(8000, 8001, 8002, 8003)으로 요청을 보냅니다. 그런데 PP로 실행한 prefill의 API 서버는 8000만 엽니다. 그래서 차트가 포트 shim을 실행해 8001, 8002, 8003으로 온 요청을 8000으로 전달합니다.

NIXL 사이드 채널 주소와 포트

prefill과 decode의 NIXL은 KV 전송을 시작하기 전에 사이드 채널로 연결 정보를 주고받습니다(handshake). 사이드 채널과 KV 전송은 서로 다른 네트워크를 사용합니다.

통신 네트워크
사이드 채널(handshake) Pod 네트워크
KV 전송 RoCE NIC. NIXL RBLN 백엔드가 NPU와 같은 NUMA 노드의 NIC을 선택합니다

사이드 채널 포트는 rank마다 다릅니다. prefill은 5559 + 2·rank, decode는 5659 + 2·rank를 사용합니다. 예를 들어 레퍼런스 구성의 decode(DP4)는 5659, 5661, 5663, 5665를 엽니다. 차트는 사이드 채널 포트를 컨테이너 포트로 선언하지 않습니다. NetworkPolicy를 사용하면 prefill Pod와 decode Pod 사이에 사이드 채널 포트를 허용하세요.

다음 values로 사이드 채널의 기본 포트와 바인딩 주소를 변경합니다.

키 설명 기본값
devices.rdma.sideChannel.basePort.{prefill,decode} P/D role별 기본 포트 5559 / 5659
devices.rdma.sideChannel.host podIP는 Pod 네트워크를 사용합니다. IPv4 주소를 지정하면 그 주소를 그대로 사용합니다 podIP

devices.rdma.sideChannel.host는 사이드 채널이 바인딩할 주소입니다. KV 전송에 쓰는 NIC은 이 값과 관계없이 NIXL RBLN 백엔드가 선택하므로, 이 값을 바꿔도 KV 전송 경로와 대역폭은 바뀌지 않습니다. 또 사이드 채널은 Pod 네트워크를 쓰므로 RoCE NIC이 down 상태여도 handshake는 성공합니다. 이때 decode는 남은 RoCE NIC으로 다시 연결합니다(장애 대응 › RoCE NIC 장애 참고).

볼륨

스택은 modelserver Pod마다 다음 볼륨을 마운트합니다.

볼륨 마운트 차트 기본 용량 공유 범위 accessModes
storage.model /mnt/shared_data unset 모든 modelserver Pod가 같은 볼륨을 읽음 Pod가 여러 노드에 걸치면 ReadWriteMany
storage.compileCache /workspace 200Gi prefill과 decode에 PVC를 하나씩 두고, 각 PVC를 같은 P/D role의 복제본이 공유 ReadWriteMany(차트 기본값)
storage.modelOffload /tmp 기본은 비활성(크기 제한 없는 emptyDir). 활성화하면 512Gi Pod별로 분리 kind: pvc로 활성화하면 ReadWriteOnce(고정)
storage.dshm /dev/shm 32Gi Pod별로 분리(메모리 emptyDir) N/A

레퍼런스 구성은 Pod를 여러 노드에 배치합니다. 따라서 storage.model과 storage.compileCache에는 ReadWriteMany(RWX)를 지원하는 StorageClass가 필요합니다. 두 볼륨은 다음과 같이 지정하세요.

  • storage.model은 반드시 지정하세요. 가중치 위치가 클러스터마다 다르므로 차트에 기본값이 없고, 지정하지 않으면 렌더링이 실패합니다. 레퍼런스 구성은 Secret 및 모델 저장소에서 만든 250Gi PVC를 kind: existingClaim으로 지정합니다. 용량은 모델 가중치 크기에 여유 공간을 더해 정하세요.
  • storage.compileCache는 차트가 PVC를 생성합니다. 차트 기본 용량은 200Gi입니다. storageClass를 비워 두면 클러스터 기본 StorageClass를 사용합니다. 용량과 accessModes는 최초 설치 전에 정하세요(PVC 구성, 용량 및 accessModes 참고).

볼륨 소스는 kind만으로 선택합니다. 허용하는 kind 값은 볼륨마다 다릅니다.

볼륨 pvc existingClaim emptyDir hostPath nfs
storage.model ○ ○ ○ ○ ○
storage.compileCache ○ ○ ✕ ○ ○
storage.modelOffload ○ ✕ ○ ✕ ✕

차트는 compileCache에 emptyDir를 허용하지 않습니다. modelserver 워크로드는 Deployment라서 복제본별 영속 볼륨이 없고, emptyDir를 쓰면 Pod가 재시작할 때마다 다시 컴파일하기 때문입니다.

storage.dshm에는 kind가 없고, kind를 지정하면 렌더링이 실패합니다. storage.dshm은 항상 메모리 기반 emptyDir입니다. /dev/shm에 쓴 데이터는 modelserver 메모리 제한에 포함됩니다. storage.dshm.size를 높이면 modelserver.resources의 메모리 requests와 limits도 같은 양만큼 높이세요.

storage.modelOffload 활성화 설정

storage.modelOffload는 엔진이 모델 로딩과 warm-up 중 모델 가중치를 내려놓는 /tmp 볼륨입니다. KV 캐시를 저장하는 Rebellions Direct Storage(RDS)와는 다른 볼륨입니다. 레퍼런스 구성은 오프로딩을 사용하지 않습니다(enabled: false).

오프로딩을 사용하려면 다음 values를 지정하세요. 이 구성은 Unverified입니다.

1
2
3
4
5
storage:
  modelOffload:
    enabled: true
    storageClass: <sc-name>   # kind: pvc에서 필수입니다. reclaimPolicy: Delete인 StorageClass를 지정하세요.
    size: 512Gi

차트는 enabled 값에 맞춰 VLLM_RBLN_DISABLE_OFFLOAD 또는 RBLN_OFFLOAD_DIR를 설정합니다. 두 변수를 serveEnv나 extraServeEnv로 직접 지정하면 렌더링이 실패합니다.

리소스 및 노드 예산

modelserver.resources의 기본값은 CPU 40코어, 메모리 630Gi이며 requests와 limits가 같습니다. prefill과 decode는 같은 modelserver.resources 값을 사용합니다. 메모리 기본값이 630Gi인 이유는 NIXL이 KV 버퍼를 등록할 때 decode가 호스트 메모리를 추가로 사용하기 때문입니다.

설치 전에 각 노드의 할당 가능 메모리와 CPU가 다음 계산식을 충족하는지 확인하세요.

allocatable(memory) ≥ modelserver memory × (pods on that node) + routing sidecar 256Mi × (decode pods on that node) + EPP 8Gi + gateway ~0.2Gi
allocatable(CPU)    ≥ modelserver CPU    × (pods on that node) + routing sidecar 0.2  × (decode pods on that node) + EPP 4 + gateway 0.1

메모리: 가장 놓치기 쉬운 항목은 차트가 함께 실행하는 EPP입니다. EPP를 계산에서 빼면 할당 가능 메모리를 몇 GB 초과해 Pod가 스케줄링되지 않을 수 있습니다. 설치 전 메모리 확인 방법은 노드 메모리 예산 확인을 참고하세요.

CPU: 서빙에는 기본값인 40코어면 충분합니다. 하지만 최초 컴파일은 서빙보다 많은 스레드를 실행합니다. CPU 제한(limit)이 너무 낮으면 cgroup이 컴파일 스레드의 실행을 지연(throttle)시킵니다. throttle은 로그에 아무것도 남기지 않고 시작만 느리게 만듭니다. 컴파일이 시작 예산(시작 및 준비 상태 프로브 참고)을 넘으면 시작 프로브가 실패하고 kubelet이 Pod를 다시 시작합니다.

cpuManagerPolicy: static인 노드에서는 modelserver가 전용 CPU를 받으므로 throttle이 발생하지 않습니다. 모델별 CPU 값은 그 모델 레시피의 values 파일을 사용하세요(차트 준비 및 기본 구성 참고).

노드별 하드웨어 수량은 하드웨어를 참고하세요.

노드 메모리 예산 확인

설치 전에 각 노드의 할당 가능 메모리를 조회해 리소스 및 노드 예산의 메모리 계산식과 비교하세요.

kubectl get nodes -o json | jq -r '.items[]
  | "\(.metadata.name)\t\(.status.allocatable.memory)"'

메모리 계산식은 modelserver Pod를 배치할 노드마다 따로 계산하세요. (pods on that node)와 (decode pods on that node)에는 그 노드에 배치할 Pod 수를 넣습니다. EPP와 게이트웨이는 클러스터에 하나씩 실행되므로, EPP와 게이트웨이의 항은 두 Pod가 배치되는 노드를 계산할 때만 포함하세요.