들어가며
사례 1. H100 한 장으로 여러 모델 올리기 — AKS MIG 설정
클러스터 MIG 구성 구조
- 노드에 nvidia.com/mig.config={프리셋 키} 레이블을 붙입니다.
- MIG Manager가 레이블 변화를 감지합니다.
- ConfigMap에 정의된 해당 프리셋의 파티션 구성을 GPU에 적용합니다.
- 적용이 완료되면 노드에 nvidia.com/mig-1g.12gb: 7 같은 형태로 슬라이스 리소스가 노출됩니다.
# 틀린 예 — NVIDIA 공식 문서 기준 키, 이 클러스터에서 인식 안 됨 kubectl label node {노드명} nvidia.com/mig.config=all-1g.12gb --overwrite # 맞는 예 — 커스텀 ConfigMap 기준 키 kubectl label node {노드명} nvidia.com/mig.config=h100-all-1g --overwrite12345
틀린 키를 넣으면 MIG Manager가 레이블 변화를 감지는 하되 ConfigMap에서 해당 키를 찾지 못해 아무 동작도 하지 않습니다. 에러 로그도 거의 안 남아서 "왜 적용이 안 되지?"를 한참 찾게 되는, 가장 조용하게 실패하는 지점이었습니다.
적용 검증
kubectl get node {노드명} -o json \ | jq '.status.capacity | with_entries(select(.key | startswith("nvidia")))'12
정상 적용 시 아래처럼 슬라이스 수가 capacity에 잡힙니다.
{ "nvidia.com/mig-1g.12gb": "7" }1
이후 Pod spec에서 해당 리소스명으로 요청하면 슬라이스 하나를 할당받습니다. 소형 모델 요청이 들어올 때마다 노드를 추가하기 전에, MIG 분할부터 검토해볼 만합니다.
사례 2. ND96 두 대로 대형 모델 서빙하기 — livenessProbe와 reconciler 사이에서
환경 개요
| 항목 | 헤더 2 |
|---|---|
| 모델 | Motif-3 (rc3), BF16 기준 약 542GB |
| 아키텍처 | GDN hybrid (Transformer + SSM linear attention 혼합) |
| 노드 | ND96 × 2 (H100 SXM × 8장, 640GB per node) |
| 병렬화 구성 | TP=2, DP=8, local_ranks_per_node=4 |
PP를 배제한 이유: SSM recurrent state 문제
멀티노드 서빙에서 가장 자연스러운 선택은 PP(Pipeline Parallel)입니다. 그런데 Motif-3는 PP를 쓸 수 없었습니다. Transformer 레이어와 SSM 계열 linear attention 레이어가 교대로 배치된 GDN hybrid 구조인데, SSM 레이어는 recurrent state(이전 스텝의 hidden state)를 다음 레이어로 넘겨야 합니다. vLLM의 PP 구현은 이 state가 stage 경계를 넘는 전달 경로를 지원하지 않아, PP로 분할하면 state가 유실되어 출력이 깨집니다.
그래서 TP=2, DP=8 조합으로 구성했습니다 — 노드 내 GPU 8장을 4개 TP 그룹으로 나누고, 두 노드에 걸쳐 DP=8로 동일 모델을 복제해 요청을 분산하는 구조입니다.
핸드셰이크 타임아웃 — 인터프리터 레벨 개입으로 해결
기동 직후 HANDSHAKE timeout exceeded: 120s 에러가 났습니다. 542GB 모델을 Blob Storage에서 읽고 CUDA 커널을 컴파일하는 시간이 vLLM 내부 기본 핸드셰이크 타임아웃(120초)을 초과한 겁니다. 문제는 이 값이 CLI 옵션이나 환경변수로 노출되지 않는다는 점이었습니다.
해결 방법은 sitecustomize.py를 이용한 Python 인터프리터 레벨 monkey-patch였습니다. 이 파일은 Python이 기동할 때 site 모듈 초기화 직후, 어떤 사용자 코드보다 먼저 실행됩니다.
# sitecustomize.py import vllm.distributed.parallel_state as ps def _patched_timeout(): return 7200.0 # 120s → 2시간 ps._get_default_timeout = _patched_timeout1234567
이 파일을 이미지의 Python site-packages 경로에 넣어두면 vLLM 코드가 import되기 전에 패치가 적용됩니다.
진짜 문제: Helm 차트 하드코딩과 reconciler의 충돌
타임아웃을 해결하고 나니 pod가 계속 재시작됐습니다. failureThreshold: 3(30초 무응답 시 kill)이 차트 템플릿에 직접 박혀 있었고, 모델 로딩에 수십 분이 걸리는 상황에서는 기동 자체가 불가능한 설정이었습니다.
values.yaml로 오버라이드가 안 돼서 kubectl patch로 직접 값을 바꿔봤습니다.
kubectl patch deployment motif-3-leader -n model-serving \ --type=json \ -p='[{"op":"replace","path":"/spec/template/spec/containers/0/livenessProbe/failureThreshold","value":1080}]'123
patch는 성공하지만, 10~20초 후 값이 다시 3으로 돌아옵니다. 원인은 Kubernetes operator의 reconcile loop입니다. 커스텀 리소스를 관리하는 컨트롤러가 주기적으로 실제 상태를 desired state(Helm 차트가 정의한 상태)와 비교하고 차이가 있으면 되돌리기 때문에, kubectl patch로 바꾼 값은 desired state에 반영되지 않아 곧바로 revert됩니다. 근본적으로 바꾸려면 차트 템플릿 자체를 수정해야 했는데, 이건 서빙팀 권한 밖이었습니다.
현실적인 결론
멀티노드 시도는 인프라 레이어 제약으로 중단하고, ND96 단일 노드에서 TP=8로 전환했습니다. 542GB 모델을 640GB 노드에 올리면 KV 캐시에 약 70GB를 쓸 수 있어 안정적으로 운영 가능한 수준이었고, 현재 이 구성으로 서빙 중입니다.
마치며
두 사례 모두 "모델이 문제"가 아니라 "우리 클러스터가 어떻게 구성되어 있는지"가 관건이었습니다. MIG는 커스텀 ConfigMap 키 이름을, 멀티노드는 차트에 하드코딩된 값과 컨트롤러의 reconcile 동작 방식을 몰랐다면 계속 헤맸을 문제들입니다.
셀프서빙으로 전환되어도 이런 인프라 레벨의 제약은 그대로 남아있을 가능성이 높습니다. GPU 자원을 나누거나 여러 노드에 걸쳐 배포하시기 전에, 클러스터 고유의 설정(커스텀 키 이름, 차트에 하드코딩된 값 등)부터 먼저 확인해보시길 권합니다.
읽어주셔서 감사합니다!