지난 분기에 ML 팀이랑 한바탕했다. 정확히 말하면 싸운 건 아니고, 슬랙 채널에서 매주 같은 문장이 반복됐다. "노드 비어있어요?", "어제 학습 잡 중간에 죽었는데 누가 뺏어갔나요?", "공정하게 좀 합시다."
우리 팀에서 운영하는 GPU 노드 풀은 A100 8장씩 박힌 노드 6대. ML 팀이 4개 프로젝트로 나뉘어 있고, 각 프로젝트마다 학습 잡을 그냥 kubectl apply -f job.yaml 식으로 던지고 있었다. 누가 먼저 던지면 임자. 누군가가 8장짜리 잡을 큐잉 없이 던지면 한 노드 통째로 점유, 그 다음 사람은 Pending. 이게 P0 잡이든 실험성 잡이든 구분이 없었다.
결국 Kueue를 도입했다. 3개월 지난 지금, 슬랙에서 그 문장들이 사라졌다. 도입 과정에서 삽질도 많았고, 처음엔 오히려 더 욕먹기도 했다. 정리해본다.
도입 전 우리가 시도한 (실패한) 방법들
처음엔 ResourceQuota로 어떻게든 해보려 했다. 네임스페이스마다 nvidia.com/gpu: 12 같은 쿼터를 박았다. 결과는 별로였다. ResourceQuota는 "당신은 이만큼만 쓸 수 있어"라고 막을 뿐, 큐잉을 해주지는 않는다. 쿼터를 초과하면 그냥 잡 생성이 실패한다. ML 엔지니어 입장에선 "내가 던진 잡이 사라졌다"는 느낌. 다시 던져야 한다. 게다가 노드가 비어있어도 다른 팀 쿼터를 못 빌려쓴다.
다음엔 PriorityClass + Preemption으로 가봤다. 우선순위 높은 잡이 낮은 잡을 죽이는 구조. 이건 더 큰 문제가 됐다. 12시간 돌던 학습이 중간에 evict되면 체크포인트 안 박아둔 경우 그 시간이 통째로 날아간다. 새벽 3시에 누가 P0 잡 던지면 멘탈이 나가는 거다. ML 팀에서 "이럴 거면 그냥 노드 풀 나눠달라"는 요청이 왔다. 노드를 나누면 활용률이 떨어진다. 안 그래도 A100 비싸 죽겠는데.
Kueue로 갈아탄 이유
Kueue 1.x를 본격적으로 보기 시작한 건 작년 말부터였다. 사실 처음 봤을 때는 "또 하나의 스케줄러 추상화인가" 싶었다. K8s에 스케줄러 위에 얹는 컴포넌트는 정말 많다. Volcano, Yunikorn, KAI Scheduler... 다 각자 강점이 있다. 근데 Kueue는 기본 스케줄러를 갈아치우는 게 아니라 그 위에 큐잉 레이어만 얇게 얹는 방식이다. 이게 우리 같은 일반 운영 팀한테는 진입 장벽이 낮았다.
올해 4월에 Red Hat이 Kueue 1.3 빌드를 발표했는데, JobSet/LeaderWorkerSet 지원 + v1beta2 API 안정화가 들어갔다. 분산 학습 잡을 "all-or-nothing"으로 묶어서 처리해주는 게 우리 케이스에 정확히 들어맞았다. 8장 짜리 분산 학습 잡이 4장만 admission되고 나머지 4장 기다리다 타임아웃 나는 상황이 잦았는데, Kueue는 그걸 미연에 방지한다.
결정적으로, Cohort 개념이 있다. Cohort 안에서는 쿼터를 빌려쓸 수 있다. 우리 팀 ML 프로젝트들을 같은 cohort에 묶어두면, 한 팀이 노드를 안 쓰고 있을 때 다른 팀이 빌려서 쓸 수 있다. ResourceQuota로는 못 하는 일이다.
처음 2주, 욕을 먹다
도입 첫 주에 일이 터졌다. ClusterQueue를 너무 타이트하게 잡았다. 각 ML 프로젝트별 LocalQueue를 만들고, ClusterQueue 하나에 nominalQuota를 정확히 GPU 카드 수만큼 박았다. 결과적으로 burst 잡이 들어왔을 때 큐잉만 되고 admission이 안 됐다. ML 팀 입장에서는 "Kueue 도입했더니 잡이 더 늦게 시작한다"는 인상이었다.
문제는 두 가지였다.
첫째, borrowingLimit을 안 줬다. cohort 안에서 빌릴 수 있는 한도를 명시 안 했더니 Kueue가 보수적으로 admission을 미뤘다. 둘째, lendingLimit도 마찬가지로 안 줬다. 결과적으로 cohort 안에서 자원 공유가 제대로 안 됐다.
apiVersion: kueue.x-k8s.io/v1beta1
kind: ClusterQueue
metadata:
name: ml-team-alpha
spec:
cohort: ml-shared
resourceGroups:
- coveredResources: ["nvidia.com/gpu", "cpu", "memory"]
flavors:
- name: a100-flavor
resources:
- name: "nvidia.com/gpu"
nominalQuota: 12
borrowingLimit: 24 # 다른 팀에서 24장까지 빌릴 수 있게
lendingLimit: 8 # 우리가 빌려줄 수 있는 한도
- name: "cpu"
nominalQuota: 96
- name: "memory"
nominalQuota: 768Gi
이걸 4개 팀 ClusterQueue에 다 적용하고 나서야 노드 활용률이 다시 올라왔다. 도입 전 평균 GPU 사용률이 58% 정도였는데, 한 달 지나서 73%까지 올라갔다. 큐잉으로 못 돌릴 잡이 줄어든 효과다.
분산 학습 잡 들어왔을 때
ML 팀에서 PyTorch DDP로 8노드 분산 학습을 돌리겠다고 했다. 이 때가 두 번째 시련이었다. 그냥 Kueue Workload로 묶었더니, partial admission이 일어났다. 4노드만 먼저 admit되고 나머지 4노드는 다음 사이클까지 대기. PyTorch DDP는 모든 워커가 다 떠야 학습이 시작되니까 4노드는 그냥 놀고 있는 셈.
이걸 해결한 게 JobSet과 Kueue 1.3의 통합이었다. JobSet으로 8노드를 하나의 단위로 묶고, Kueue가 그걸 gang scheduling으로 처리하게 했다.
apiVersion: jobset.x-k8s.io/v1alpha2
kind: JobSet
metadata:
name: distributed-training
labels:
kueue.x-k8s.io/queue-name: ml-team-alpha-lq
spec:
network:
enableDNSHostnames: true
replicatedJobs:
- name: workers
replicas: 1
template:
spec:
parallelism: 8
completions: 8
template:
spec:
containers:
- name: trainer
image: ml/pytorch-trainer:latest
resources:
limits:
nvidia.com/gpu: 1
이제 Kueue는 "8 GPU가 동시에 확보 가능할 때까지 전체 잡을 대기시킨다." Partial admission 없음. 그리고 cohort 안에서 다른 팀 한가한 자원도 빌려쓸 수 있어서, 대기 시간이 생각보다 짧았다.
의외로 어려웠던 것: 관측성
스케줄링이 잘 돌아가는지 어떻게 알지? 이게 의외로 까다로웠다. Kueue는 메트릭을 Prometheus 포맷으로 export하긴 한다. kueue_pending_workloads, kueue_admitted_active_workloads 같은 것들. 근데 이걸 그래프로 그려놓고도 한참 쳐다봤다.
진짜 보고 싶은 건 두 가지였다.
- 어떤 잡이 얼마나 오래 큐에 있었는지
- 어떤 ClusterQueue가 다른 ClusterQueue 자원을 얼마나 빌려쓰고 있는지
전자는 kueue_admission_wait_time_seconds 히스토그램으로 어느 정도 됐다. 후자는 결국 kubectl get workload를 파싱하는 작은 exporter를 별도로 만들어야 했다. 이 부분은 아직 Kueue 본체가 약하다. GitHub issue 보니까 community에서도 같은 얘기 나오고 있어서 다음 마이너에서 개선될 것 같다.
지금 우리는
도입 3개월이 지난 시점에서 솔직히 말하면, Kueue 도입 자체가 모든 걸 해결한 건 아니다. 슬랙에서 "노드 비어있어요?"가 사라진 건 맞지만, 대신 "내 잡이 왜 30분째 큐에 있어요?"라는 새로운 질문이 생겼다. 답은 보통 "cohort 안에 자원이 모자라거나, 우선순위 높은 잡이 먼저 들어가서요" 정도. 그래도 이건 데이터로 설명 가능한 질문이라 훨씬 낫다.
MultiKueue도 보고 있다. 다른 리전 클러스터의 GPU 노드 풀까지 한 큐로 묶으면 정말 재밌어질 것 같은데, 이건 아직 우리한테는 오버엔지니어링 같다. 한 클러스터에서 cohort만 잘 짜도 충분히 효과 본다.
조만간 cooperative preemption도 검증해볼 계획이다. 체크포인트 박는 잡을 만들도록 ML 팀과 합의가 되면 그 때부터 진짜 우선순위 기반 스케줄링이 가능해진다.
혹시 비슷한 상황 겪고 계신 분 있으면 cohort 설계 어떻게 하시는지 댓글 남겨주세요. 우리도 아직 답을 못 찾은 부분이 많다.
'IT > Kubernets' 카테고리의 다른 글
| distroless 파드 디버깅, kubectl debug로 5초 (0) | 2026.05.28 |
|---|---|
| Kubernetes Job, backoffLimit만 쓰면 OOM 한 번에 재시도 6번이 따라온다 (0) | 2026.05.26 |
| matchLabelKeys 안 썼다가 롤링 업데이트 중 한 노드에 트래픽 70% 쏠린 사건 (0) | 2026.05.23 |
| Pod resize, kubelet은 사실 어떻게 하는가 — 1.35 GA 내부 동작 (0) | 2026.05.23 |
| Karpenter consolidation, 새벽에 한꺼번에 다 날아간 이야기 (0) | 2026.05.22 |