결과: 병합 (2026-11-09 17:44, kai-scheduler/KAI-Scheduler#2532), 이슈 #2531 도 함께 닫힘. 사실은 GitHub 의 PR·이슈·리뷰·타임라인·검사 기록, KAI-Scheduler 소스(PR 의 바탕 커밋
0243c4e9)와 API 모듈(태그 v0.2.0), 기여 원장의 실측 원자료와 초안, TT Lab 저장소의 코스 커밋에서 가져왔습니다. 시각은 한국 시간입니다.
목차
- 한 줄 요약
- 1. 무엇이 문제였나: 예시 셋이 모두 아무것도 못 받는다
- 2. 원인: 타입의 0 과 -1 만 건너뛰는 검사
- 3. 무엇을 바꿨나: diff 요지
- 4. 반응과 대응: 질문 하나와 지워진 승인
- 5. 배운 것: 오픈소스 기여 요령
- 링크
한 줄 요약
| PR | kai-scheduler/KAI-Scheduler#2532 docs(queues): explain that an omitted limit is 0 and fix the example queues |
| 바꾼 것 | docs/queues/README.md 한 파일. 특수값 표 한 줄, 설명 한 문단, 예시 큐 셋에 빠진 limit: -1 과 memory 블록. 16줄 추가, 2줄 삭제 |
| 반응 | 다른 기여자가 코드 경로를 확인하고 질문 하나, 협업자 권한의 리뷰어 승인(뒤에 자동 해제), 메인테이너 승인과 skip-changelog 라벨 |
| 결과 | PR 공개 10-27 14:34 → 병합 11-09 17:44, 약 13일 3시간 10분. 병합 큐를 거친 squash 병합 |
1. 무엇이 문제였나: 예시 셋이 모두 아무것도 못 받는다
KAI-Scheduler 는 GPU 작업을 위한 쿠버네티스 스케줄러입니다. 파드에 schedulerName: kai-scheduler 를 적으면 기본 스케줄러 대신 이 스케줄러가
배치를 맡고, 파드가 속한 큐(Queue)의 몫을 따져 자원을 줍니다. 큐에는 자원(cpu·memory·gpu)마다 보장량 quota 와 상한 limit 이 있습니다.
TT Lab 학습 사이트의 「쿠버네티스 GPU 스케줄링과 AI 배치」 코스를 만들던 10-27 새벽에 이 함정을 처음 밟았습니다. 실험 VM 에 k3s 한 대를 띄우고
가짜 GPU 4장(fake-gpu-operator 0.2.1)과 KAI-Scheduler v0.18.6 을 올린 다음, gpu quota: 1 에 limit 을 적지 않은 큐를 만들어 GPU 1장짜리
파드를 넣었습니다. quota 가 1이니 1장은 받을 줄 알았는데, 파드는 Pending 에 머물고 PodGroup 에는 이런 조건이 붙었습니다.
OverLimit: nolimit quota has reached the allowable limit of GPUs. Limit is 0 GPUs,
currently 0 GPUs allocated and workload requested 1 GPUs.
limit 을 -1 로 바꾸자 기다리던 같은 파드가 배치되었습니다(실습 검증 기록 vfy-kai.log). 몇 초 만에 떴는지는 재지 않았습니다. 이 일은 코스의 실습 둘(「limit 을 비운 큐의 함정」,
「limit 을 -1 로 적어 고친다」)이 되었고, 같은 날 아침 코스 커밋(4cdcc0b3)의 읽기 자료에도 「limit 은 꼭 -1 이나 숫자로 적으세요」라고 적었습니다.
문제는 문서였습니다. 큐 문서의 특수값 표는 비워 둔 limit 을 「추가 자원 없음(기본값)」이라고 설명하고, 마지막 예시의 제목은
「Unlimited Queue - default value is -1」입니다. 읽는 사람은 비워 두면 quota 까지는 쓰거나, 아예 무제한이라고 기대하게 됩니다.
그래서 문서의 예시 큐 셋을 글자 그대로 적용하고, 같은 파드(requests: {cpu: 100m, memory: 32Mi}, limits: {nvidia.com/gpu: 1})를
하나씩 넣어 봤습니다. 원자료는 기여 원장의 kai-docs-test.log·kai-docs-after.log 이고, 파드마다 15초를 기다린 뒤 상태를 읽었습니다.
| 예시 큐 | 고치기 전(문서 그대로) | 고친 뒤(PR 의 첫 판) |
|---|---|---|
Basic Queue (memory 없음) |
Pending, Limit is 0 GB (메모리) |
Running |
Hierarchical Queue (cpu·gpu 에 limit 없음, memory 없음) |
Pending, Limit is 0 cores (CPU) |
Running |
Unlimited Queue (memory 없음) |
Pending, Limit is 0 GB (메모리) |
Running |
Basic Queue 에 memory: {quota: -1, limit: -1} 만 더해도 기다리던 파드가 Running 이 되었습니다. GPU 상한을 적은 예시도, 「Unlimited」라는
이름의 예시도 메모리 블록이 없어서 메모리를 조금이라도 청하는 파드를 받지 못한 것입니다. 쿠버네티스에서 메모리를 청하지 않는 파드는
드무니, 이 문서를 보고 처음 큐를 만드는 사람은 거의 반드시 여기서 막힙니다.
환경은 실험 VM 한 대(4 vCPU·4GiB), k3s v1.36.5, KAI-Scheduler v0.18.6 이고 GPU 는 가짜입니다. 진짜 GPU 노드나 여러 노드에서는 재지 않았습니다(미실측). 다만 이 판정은 스케줄러가 큐의 숫자만 보고 하는 일이라 GPU 가 진짜인지와는 상관없는 것으로 보입니다.
2. 원인: 타입의 0 과 -1 만 건너뛰는 검사
줄 번호는 PR 의 바탕 커밋 0243c4e9 와
그 커밋이 쓰는 API 모듈 kai-scheduler/api v0.2.0 기준입니다.
- 문서.
docs/queues/README.md61–67행 의 특수값 표가limit의0 or unset을 「No additional resources allowed (default)」라고 적습니다. 112행 의 예시 제목은 「Unlimited Queue - default value is -1」입니다. 76–126행의 예시 셋에는memory블록이 하나도 없습니다. - 타입.
scheduling/v2/resources.go17–24행 의QueueResource.Limit은 기본값 표시가 없는float64입니다. 적지 않으면 Go 의 영값인 0 이 됩니다. 차트에 든 Queue CRD 도limit을type: number로만 적고default가 없어서, API 서버가 -1 을 채워 주지 않습니다. - 값 옮기기.
proportion.go414–427행 이 큐의Limit을 그대로 상한으로 넘깁니다. 메모리만math.Max(-1, limit*mebibytes)로 단위를 바꾸는데, 0 에 무엇을 곱해도 0 입니다. - 검사.
max_allowed_check.go51–66행 의isOverLimit은 자원마다 따로 돌며, 상한이 정확히 -1 일 때만 건너뜁니다(54행). 그 밖에는 「이미 쓴 양 + 요청」이 상한 이하인지 봅니다. 상한이 0 이면 그 자원을 조금이라도 청하는 순간 넘칩니다. 요청이 0 인 자원은 건너뛰므로(58–60행), 메모리를 청하지 않는 파드라면 메모리 블록이 없어도 통과합니다.
정리하면 코드는 그 자체로 앞뒤가 맞습니다. 「비우면 0, 0 이면 막힘, -1 만 무제한」입니다. 어긋난 것은 문서의 설명과 예시였습니다. 그래서 이슈에는 두 길을 함께 적었습니다. 문서를 지금 동작에 맞추거나, 의도한 기본값이 -1 이라면 CRD 기본값이나 입장 단계에서 채우는 것입니다. 동작을 바꿀지는 메인테이너가 정할 일이라, 바로 머지할 수 있는 문서 쪽만 PR 로 만들고 「기본값을 바꾸는 쪽을 고르면 이 PR 은 닫아도 된다」고 적었습니다.
3. 무엇을 바꿨나: diff 요지
-| `limit` | `0` or unset | No additional resources allowed (default) |
+| `limit` | `0` or unset | No resources of this type can be allocated to the queue, including its quota (default) |
+
+Each resource (`cpu`, `memory`, `gpu`) is checked on its own. If a queue omits a resource or its `limit`,
+the limit for that resource is `0`, and workloads that request it stay pending with an `OverLimit` condition
+such as `Limit is 0 GB`. Set `limit: -1` for every resource you do not want to cap.
# Basic Queue, Hierarchical Queue
+ memory:
+ quota: 0
+ limit: -1
# Hierarchical Queue 의 cpu·gpu 에도
+ limit: -1
-### Unlimited Queue - default value is -1
+### Unlimited Queue
+`-1` is not filled in for you: set it on every resource.
# Unlimited Queue
+ memory:
+ quota: -1 # Unlimited quota
+ limit: -1 # No limit
(설명 문단은 원래 한 줄인 것을 읽기 좋게 세 줄로 나눠 적었습니다.)
PR 본문에는 1절의 고치기 전·뒤 표, 실측 환경, 「문서만 바꿨고 코드·차트는 그대로」라는 말, skip-changelog 라벨 부탁, 같은 파일의 다른
절을 고치는 열린 PR #2275(MIG 계산 설명)가 먼저 병합되면 리베이스하겠다는 말,
그리고 AI 도움을 받아 불일치를 찾고 실측하고 초안을 썼다는 고지를 넣었습니다.
이 저장소의 CONTRIBUTING 은 DCO(Signed-off-by), Conventional Commits 꼴의 PR 제목, 동작이 바뀌는 변경의 변경 기록 조각(문서만이면
skip-changelog 라벨)을 요구하고, 바깥 기여자의 PR 에는 「신뢰된 리뷰어 2명의 승인」을 요구합니다. AI 사용에 관한 저장소 규칙은 따로
없어서 상위 재단의 생성형 AI 지침을 읽고 사실대로 고지했습니다. 바깥 기여자는 라벨을 붙일 권한이 없어 본문에서 부탁했습니다.
4. 반응과 대응: 질문 하나와 지워진 승인
| 시각 | 일 |
|---|---|
| 10-27 새벽 | 실험 VM 에서 함정 발견, 문서 예시 셋 고치기 전·뒤 실측(원자료 파일 시각 01:46·02:30·02:38) |
| 10-27 14:33–14:34 | 이슈 #2531, 42초 뒤 PR 공개(첫 판 c871f61b, SSH 서명) |
| 10-27 19:28 | 다른 기여자의 리뷰(의견): 코드 경로가 새 설명과 맞다는 확인과 질문 하나 |
| 10-28 21:39 | 협업자 권한의 리뷰어가 첫 판을 승인 |
| 10-29 00:47 | 질문을 반영한 커밋 47bd30b5 push, 그 순간 승인이 자동 해제. 답글 |
| 10-29 00:54 | 두 커밋을 SSH 서명 커밋 하나(2c23833a)로 합쳐 강제 갱신 |
| 10-29 06:17 | 질문한 기여자가 「2c23833a reads right」 |
| 11-01 01:21 | 해제된 승인을 다시 봐 달라는 댓글 |
| 11-09 16:04–16:05 | 메인테이너 승인, skip-changelog 라벨 |
| 11-09 17:43–17:44 | 병합 큐에 들어가 검사 넷 통과, 17:44:50 병합(e5f0f298), 이슈 #2531 도 닫힘 |
질문. 다른 기여자는 proportion.go·CRD·isOverLimit 을 직접 따라가 「코드가 새 문장과 맞다」고 확인한 뒤, 요청이 아니라 질문이라며
하나를 물었습니다. 새 memory 블록이 quota: -1 이라 메모리 보장량까지 무제한이 되는데, GPU 예시는 「보장 없음」을 quota: 0 으로
적으니 상한만 풀려는 뜻이라면 quota: 0·limit: -1 이 더 가깝지 않겠느냐는 것입니다. 맞는 지적이었습니다. 우리가 바라던 것은
「상한을 0 으로 두지 말라」였지 보장량을 바꾸는 것이 아니었습니다. 그래서 상한을 적은 예시 둘(Basic·Hierarchical)의 메모리를
quota: 0·limit: -1 로 바꾸고, 모든 자원이 무제한인 Unlimited 예시만 quota: -1 로 두었습니다. 위 diff 가 그 최종본입니다.
이 최종본을 실험 VM 에서 다시 돌린 기록은 없습니다(미실측). 1절의 「고친 뒤」는 메모리 quota: -1 이던 첫 판의 결과입니다. 상한 검사는
limit 만 보므로 결과가 같을 것으로 보고, 같은 실측에서 Unlimited 예시의 gpu 가 quota: 0·limit: -1 로 GPU 1장을 받은 것이 그 근거이지만,
메모리 quota: 0 자체를 재지는 않았습니다.
지워진 승인. 협업자 권한의 리뷰어가 첫 판을 승인한 지 세 시간 뒤에 질문 반영 커밋을 올렸고, GitHub 의 타임라인에는 바로 그 push 로
승인이 해제(dismissed)된 기록이 남았습니다. 해제를 일으킨 커밋은 47bd30b5 입니다. 이 저장소는 새 커밋이 올라오면 이전 승인을 무효로 하는
설정을 쓰는 것으로 보입니다(저장소 설정은 볼 권한이 없어 미확인). 7분 뒤 우리는 두 커밋을 서명된 커밋 하나로 합쳐 다시 올렸는데,
11-01 에 남긴 재승인 부탁 댓글에는 「squash push 가 승인을 지웠다」고 적었습니다. 이벤트 기록을 다시 보면 squash 가 아니라 그 앞의
push 였습니다. 어느 쪽이든 승인 뒤의 push 는 승인을 지운다는 점은 같지만, 원인을 말할 때는 기억이 아니라 타임라인을 먼저 봐야 했습니다.
기다림과 병합. 10-29 새벽 질문이 정리된 뒤 11일 동안 새 리뷰가 없었습니다. 11-09 16:04 에 메인테이너가 승인하고 1분 뒤 라벨을 붙였고,
17:43 에 병합 큐에 넣었습니다. 병합 큐에서는 PR 워크플로·변경 기록 검사·PR 제목 검사·DCO 가 14초 안에 모두 통과했고, 문서만 바뀐 PR 이라
빌드·E2E 는 「필요 없음」으로 건너뛰었습니다. 병합 커밋의 저자는 우리 계정이고, 메시지는 PR 제목 끝에 (#2532) 와 DCO 서명이 붙은 것입니다.
PR 화면에는 빨간 검사 둘이 그대로 남아 있습니다. 변경 기록 검사와 승인 수를 세는 검사(Approval Gatekeeper)가 10-29 강제 갱신 때 돌아 실패했고, 11-09 16:03 에 다시 돌았지만 승인·라벨이 붙기 1~2분 전이라 또 실패했습니다. 누가 다시 돌렸는지는 공개 기록에서 확인하지 못했습니다. Approval Gatekeeper 는 바깥 기여자 PR 에 조직 멤버나 협업자의 승인 둘을 요구하는데, 병합 시점에 살아 있던 신뢰된 승인은 메인테이너의 하나였습니다(협업자의 승인은 해제된 채였고, 질문한 기여자는 의견만 남겼습니다). 이 검사는 병합 큐에서는 돌지 않았으니 병합을 막는 필수 검사가 아닌 것으로 보입니다. 규칙의 적용은 메인테이너의 판단이고, 우리가 따로 대응할 일은 없었습니다.
11-10 기준으로 이 커밋을 담은 릴리스 태그는 없습니다. 병합 뒤에 나온 v0.16.16·v0.14.12 는 옛 판 줄기의 패치 릴리스이고, 이 문서 변경은 main 에만 있습니다. 문서는 GitHub 의 main 에서 바로 읽힙니다.
5. 배운 것: 오픈소스 기여 요령
- 「비워 두면 무엇인가」는 문서가 아니라 타입과 CRD 에서 확인합니다. Go 구조체의 영값, CRD 의
default, 웹훅의 기본값 채우기 중 어디에도 -1 이 없으면 비운 값은 0 입니다. 0 이 「없음」인지 「막음」인지는 그 값을 읽는 검사 함수를 끝까지 따라가야 알 수 있습니다. - 문서 예시는 글자 그대로 적용해 봅니다. 예시 셋이 모두 실패한다는 사실은 코드를 읽어서가 아니라 복사해 붙여서 알았습니다. 「고치기 전·뒤」 표가 있으면 메인테이너도 검토자도 같은 실험을 머릿속으로 다시 할 필요가 없습니다.
- 동작을 바꿀지는 이슈로 묻고, 문서 PR 은 한쪽 답으로 냅니다. 기본값을 -1 로 바꾸는 것은 기존 사용자의 큐 동작을 바꾸는 일이라 외부 기여자가 정할 일이 아닙니다. 이슈에 두 길을 적고 문서 PR 에 「다른 쪽을 고르면 닫아도 된다」고 적어 두면, 메인테이너는 고르기만 하면 됩니다. 이번에는 이슈에 답글이 달리지 않았고, 문서 PR 이 병합되며 이슈도 닫혔습니다.
- 질문도 반영할 가치가 있습니다. 「요청이 아니라 질문」이라는 리뷰라도 예시의 뜻을 더 정확하게 만든다면 받아들입니다. quota 와 limit 은 둘 다 「얼마까지」처럼 들리지만, 하나는 보장이고 하나는 상한입니다. 문서 예시는 그 차이를 그대로 보여 줘야 합니다.
- 승인 뒤의 push 는 승인을 지웁니다. 고칠 것이 생기면 고치는 것이 맞지만, 승인 둘이 필요한 저장소에서 하나를 잃으면 기다림이 길어집니다.
리뷰 질문은 승인 전에 정리하고, 커밋을 합치는 일도 한 번에 끝냅니다. 무엇이 승인을 지웠는지는 타임라인 API(
issues/<번호>/timeline)의review_dismissed이벤트와dismissal_commit_id로 확인합니다.
티티 한마디. 학교 매점 카드에 「하루 한도」 칸이 있었는데, 비워 두면 무제한인 줄 알았대요. 그런데 매점 아주머니는 빈칸을 0원으로 읽으셨어요. 그래서 우유 하나도 못 샀죠! 안내문에는 「빈칸은 기본값」이라고만 쓰여 있었고요. 안내문에 「빈칸은 0원이에요, 무제한이면 -1 이라고 꼭 적으세요」를 더하자고 했더니, 13일 뒤에 새 안내문이 붙었어요. 중간에 낙서 하나 고쳤다고 받아 둔 도장이 지워진 건 좀 아까웠지만요.
링크
모든 자료는 2026-11-10 에 열람했습니다.
- PR: kai-scheduler/KAI-Scheduler#2532 (병합 커밋
e5f0f298) - 이슈: kai-scheduler/KAI-Scheduler#2531
- 고친 문서(바탕 커밋):
docs/queues/README.md - 동작의 근거(바탕 커밋):
proportion.go,max_allowed_check.go, Queue CRD,kai-scheduler/apiv0.2.0resources.go - 기여 규칙: CONTRIBUTING, Approval Gatekeeper 워크플로
- 같은 파일의 다른 PR: kai-scheduler/KAI-Scheduler#2275 MIG GPU 계산 설명(11-10 기준 열림)
- GitHub 타임라인 API: https://docs.github.com/en/rest/issues/timeline