JobSet 으로 분산 학습 작업을 묶는다
이 실습은 진짜 쿠버네티스에서 돕니다
VM 안에 k3s 한 대가 떠 있고, 가짜 GPU 4장, JobSet v0.13.0, Kueue 가 올라 있습니다. ml 네임스페이스의 LocalQueue lq 는 GPU 3장 쿼터의 ClusterQueue ml 로 이어져 있습니다. 처음 뜨는 데 3~4분 걸립니다. 세션이 끝나면 VM 과 파일은 사라집니다.
목표
여러 역할의 파드를 JobSet 하나로 묶고, 이름으로 서로 찾기·전체 재시작·성공 조건·큐 입장을 차례로 확인합니다.
왜 중요한가
분산 학습이 실패하는 가장 흔한 모습은 「일부만 살아서 GPU 를 붙잡고 기다리는 것」 입니다. 랭크 하나가 죽었는데 나머지가 모르고, 쿼터가 모자란데 반만 떠 있습니다. JobSet 의 실패 정책과 Kueue 의 한 덩어리 입장은 바로 그 상태를 만들지 않으려는 장치입니다. Kubeflow Trainer 같은 상위 도구도 이 위에 서 있으니, 그 도구가 이상하게 굴 때 아래층을 직접 읽을 수 있어야 합니다.
단계
- 리더 1·워커 2 의 JobSet
hello를/root/gsb-jobset/hello.yaml로 만들어 파드 셋을 Running 으로 둡니다. - 워커 안에서
hello-leader-0-0.hello.ml.svc.cluster.local을 조회해/root/gsb-jobset/dns.txt에 담습니다. - 완료 인덱스 1 인 워커의 JobSet 라벨 넷을
/root/gsb-jobset/labels.txt에키=값으로 적습니다. - JobSet
fragile(maxRestarts 2, 워커 backoffLimit 0)을 만들어 워커를 실패시키고, 재시작 횟수를/root/gsb-jobset/restart.txt에restarts=로 적습니다. - 리더만 끝나면 Completed 가 되는 JobSet
eval을 만듭니다. - GPU 3장을 쓰는 JobSet
train을 큐 라벨과 함께/root/gsb-jobset/train.yaml로 넣어 한 덩어리로 입장시킵니다. - GPU 5장을 쓰는 JobSet
big을 넣어 한 파드도 뜨지 않는 것을 보고 사유를/root/gsb-jobset/big.txt에 담습니다. /root/gsb-jobset/report.json에admitted,suspended,fragile_restarts를 적습니다.
참고
kubectl -n ml get jobsets의 RESTARTS·COMPLETED·SUSPENDED 열과kubectl -n ml get workloads를 함께 봅니다.- 파드 목록은
-l jobset.sigs.k8s.io/jobset-name=<이름>으로 거릅니다. - 흔한 실수 1: 4단계에서 backoffLimit 을 비워 두는 것. Job 이 혼자 파드를 다시 띄워 JobSet 의 재시작이 일어나지 않습니다.
- 흔한 실수 2: 6단계에서 큐 라벨을 Job 템플릿에 붙이는 것. 라벨은 JobSet 의 metadata 에 붙입니다.
리더 하나, 워커 둘
ml 네임스페이스에 JobSet hello 를 「/root/gsb-jobset/hello.yaml」 로 만들어 적용하세요. 역할 leader(Job 1벌, 파드 1개)와 worker(Job 1벌, 파드 2개)이고, GPU 와 큐 라벨은 없습니다. 파드 셋이 모두 Running 이어야 합니다.
역할마다 replicatedJobs 항목 하나입니다. Job 의 파드 수는 그 안의 parallelism 과 completions 로 정합니다.
워커는 리더를 이름으로 찾는다
hello 의 워커 파드 안에서 리더 파드의 이름(hello-leader-0-0.hello.ml.svc.cluster.local)을 조회해 결과를 「/root/gsb-jobset/dns.txt」 에 담으세요. 리더 파드의 IP 가 나와야 합니다.
busybox 의 nslookup 은 검색 도메인을 붙이지 않으니 끝까지 다 적은 이름으로 묻습니다. 이 이름은 JobSet 이 만든 헤드리스 서비스와 파드의 hostname·subdomain 이 함께 만듭니다.
파드에 붙은 JobSet 의 좌표
hello 의 워커 중 완료 인덱스가 1 인 파드의 라벨에서 jobset.sigs.k8s.io/jobset-name, jobset.sigs.k8s.io/replicatedjob-name, jobset.sigs.k8s.io/job-index, batch.kubernetes.io/job-completion-index 네 개를 「/root/gsb-jobset/labels.txt」 에 키=값 으로 적으세요.
라벨 셀렉터로 replicatedjob-name=worker 와 job-completion-index=1 을 함께 걸면 파드 하나가 나옵니다. 이 라벨들이 학습 코드의 RANK 계산 재료가 됩니다.
하나가 죽으면 전부 다시 시작한다
JobSet fragile(리더 1, 워커 1, failurePolicy.maxRestarts: 2, 워커 Job 의 backoffLimit: 0)을 만들고, 워커 컨테이너는 /tmp/fail 파일이 생기면 종료 코드 1로 끝나게 하세요. 워커 안에 그 파일을 만들어 실패시킨 뒤, JobSet 의 재시작 횟수를 「/root/gsb-jobset/restart.txt」 에 restarts= 로 적으세요. 지금 떠 있는 파드는 모두 새 시도(restart-attempt)의 파드여야 합니다.
backoffLimit 이 0 이 아니면 Job 이 파드를 혼자 다시 띄우고 끝나 JobSet 은 실패를 모릅니다. 재시작이 일어나면 status.restarts 가 오르고 파드 라벨 jobset.sigs.k8s.io/restart-attempt 도 함께 오릅니다.
리더가 끝나면 전체가 끝난다
JobSet eval 을 만드세요. 리더는 5초 뒤 종료 코드 0 으로 끝나고, 워커 둘은 계속 잡니다. successPolicy 를 operator: Any, targetReplicatedJobs: [leader] 로 두어 리더가 끝나면 JobSet 이 Completed 가 되어야 합니다.
successPolicy 가 없으면 기본은 모든 Job 이 끝나야 성공(All)이라 워커가 자는 동안 JobSet 은 끝나지 않습니다.
큐에 넣으면 한 덩어리로 입장한다
GPU 를 쓰는 JobSet train(리더 1 + 워커 2, 파드마다 GPU 1장, 라벨 kueue.x-k8s.io/queue-name: lq)을 「/root/gsb-jobset/train.yaml」 로 만들어 적용하세요. Kueue 가 Workload 하나로 입장시키고 파드 셋이 Running 이어야 합니다.
ml 네임스페이스의 lq 는 GPU 3장 쿼터의 ClusterQueue ml 로 이어져 있습니다. Workload 의 podSets 에 역할마다 하나씩 들어가는지 보세요.
쿼터를 넘으면 한 파드도 뜨지 않는다
GPU 를 5장 쓰는 JobSet big(리더 1 + 워커 4, 파드마다 1장, 같은 큐 라벨)을 넣으세요. 쿼터 3장을 넘으므로 JobSet 은 멈춘 채 파드를 하나도 만들지 않습니다. 그 Workload 의 사유를 「/root/gsb-jobset/big.txt」 에 담으세요.
Kueue 는 역할들의 요청을 더해 한꺼번에 판단합니다. 일부만 입장시키지 않습니다.
어느 JobSet 이 어떤 상태인가
「/root/gsb-jobset/report.json」 에 admitted(큐 라벨이 있고 입장한 JobSet 이름 목록), suspended(spec.suspend 가 true 인 JobSet 이름 목록), fragile_restarts(fragile 의 재시작 횟수) 세 키를 적으세요.
입장 여부는 JobSet 이 주인인 Workload 의 Admitted 조건으로 봅니다. 큐 라벨이 없는 JobSet 은 Workload 가 없습니다.