TT Lab
Blog

한국어 English 日本語 中文

Get started
Blog

Blog

An Omitted Limit Meant 0, Not Unlimited: How the KAI-Scheduler Queue Documentation PR Was Merged in 13 Days

Result: Merged (2026-11-09 17:44, kai-scheduler/KAI-Scheduler#2532), and issue #2531 was also closed. Actually, this was gathered from GitHub PRs, issues, reviews, timelines, inspection logs, the KAI-Scheduler source (the PR's base commit 0243c4e9), and the API module (tag v0.2.0), as well as raw measurement data and drafts from contribution records, along with commit logs from the TT Lab repository. All times are in Korea Standard Time.

Table of Contents

One-Line Summary

PR kai-scheduler/KAI-Scheduler#2532 docs(queues): explain that an omitted limit is 0 and fix the example queues
Changes docs/queues/README.md One file. One line in the special values table, one paragraph of explanation, and the missing limit: -1 and memory blocks in the example queue set. 16 lines added, 2 lines deleted
Reactions Another contributor checked the code path and asked a question, a reviewer with collaborator permissions approved it (which was automatically dismissed later), followed by a maintainer approval and the skip-changelog label
Result PR opened 10-27 14:34 → Merged 11-09 17:44, approximately 13 days, 3 hours, and 10 minutes. Squash merged via the merge queue
적지 않은 limit 은 -1 이 아니라 0 으로 읽힌다 KAI-Scheduler v0.18.6 실측(10-27), 바탕 커밋 0243c4e9 의 소스 줄 ① 큐에서 limit 을 빼면 일어나는 일 limit 생략 CRD 기본값 없음 float64 의 0 resources.go L23 상한 0 -1 만 건너뜀 isOverLimit: 쓴 양 + 요청 ≤ 상한 을 자원마다 따로 본다 상한이 0 이면 그 자원을 조금이라도 청하는 파드는 OverLimit ② 문서 예시를 그대로 적용한 큐에 같은 파드 하나 파드: cpu 100m · memory 32Mi · GPU 1장 고치기 전 고친 뒤 Basic Pending · 메모리 0 GB Running Hierarchical Pending · CPU 0 cores Running Unlimited Pending · 메모리 0 GB Running PR: 표 한 줄과 설명 한 문단, 예시 셋에 limit -1 (+16 −2) 「Unlimited Queue - default value is -1」 제목도 고침 최종본(memory quota 0)으로 다시 돌린 기록은 없음: 미실측
Figure: An unconfigured limit was read as 0, preventing the documentation's example queue sets from receiving any pods

1. What Was the Problem: The Example Sets Couldn't Receive Anything

KAI-Scheduler is a Kubernetes scheduler for GPU workloads. By writing schedulerName: kai-scheduler in a pod, this scheduler takes over placement instead of the default scheduler and allocates resources by evaluating the share of the queue to which the pod belongs. Queues have a guaranteed amount quota and a limit limit for each resource (cpu, memory, and gpu).

I first fell into this trap in the early hours of October 27 while creating the "Kubernetes GPU Scheduling and AI Batching" course on the TT Lab learning site. After spinning up a single k3s instance on an experimental VM with 4 fake GPUs (fake-gpu-operator 0.2.1) and KAI-Scheduler v0.18.6, I created a queue without specifying a limit for gpu quota: 1 and submitted a single-GPU limit pod. Since the quota was 1, I expected it to receive 1 GPU, but the pod remained in Pending status and the PodGroup was given the following condition:

OverLimit: nolimit quota has reached the allowable limit of GPUs. Limit is 0 GPUs,
currently 0 GPUs allocated and workload requested 1 GPUs.

limit to -1 , and the long-awaited identical pod was scheduled (Lab verification record vfy-kai.log). I didn't measure how many seconds it took to start up. This incident became two lab exercises in the course ("The Trap of the Queue with an Empty Limit," "Fixing It by Writing -1 for the Limit"), and on the same morning, I also wrote in the reading materials of the course commit (4cdcc0b3) that "make sure to write the limit as -1 or a number."

The problem was the documentation. The special value table in the queue documentation explains that an empty limit means "No additional resources (default)," and the title of the final example is "Unlimited Queue - default value is -1." Readers are led to expect that leaving it empty will allow usage up to the quota, or mean completely unlimited. So I applied the example queue set from the document verbatim , and submitted the same pods (requests: {cpu: 100m, memory: 32Mi}, limits: {nvidia.com/gpu: 1}) one by one. The source data is from the contribution ledger kai-docs-test.log·kai-docs-after.log , and I read the status after waiting 15 seconds for each pod.

Example Queue Before Fix (As in Docs) After Fix (First PR Version)
Basic Queue (memory None) Pending, Limit is 0 GB (Memory) Running
Hierarchical Queue (No limit , No memory for cpu and gpu) Pending, Limit is 0 cores (CPU) Running
Unlimited Queue (memory None) Pending, Limit is 0 GB (Memory) Running

Just adding memory: {quota: -1, limit: -1} to the Basic Queue made the awaited pod transition to Running. Both the example specifying a GPU upper bound and the example named "Unlimited" lacked memory blocks, failing to accept pods requesting even a tiny amount of memory. Since pods that do not request memory are rare in Kubernetes, anyone creating a queue for the first time based on this documentation will almost certainly get stuck here.

The environment was a single experimental VM (4 vCPU, 4GiB), k3s v1.36.5, KAI-Scheduler v0.18.6, and a fake GPU. I did not measure this on real GPU nodes or multiple nodes (unmeasured). However, since this determination is made by the scheduler simply looking at the queue numbers, it appears unrelated to whether the GPU is real or not.

2. Cause: A validation check that only skips 0 and -1 of the type

Line numbers are based on the base commit of the PR 0243c4e9 and the API module kai-scheduler/api v0.2.0 used by that commit.

In short, the code is consistent in itself: "blank means 0, 0 means blocked, only -1 is unlimited." What was out of alignment was the documentation's explanation and examples. Therefore, the issue presented two paths: either update the documentation to match the current behavior, or, if the intended default is -1, populate it via CRD defaults or at the admission stage. Since changing the behavior is up to the maintainers, I created a PR only for the documentation part that can be merged immediately, noting that "if the path of changing the default is chosen, this PR can be closed."

3. What was changed: Diff summary

-| `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

(The explanatory paragraph, originally a single line, was split into three lines for readability.)

The PR body included the before-and-after tables from Section 1, the measurement environment, the statement that "only the documentation was changed, code and charts remain as-is," skip-changelog label request, a note that if another open PR modifying a different section of the same file #2275(MIG calculation explanation) is merged first, I would rebase, and a notice that AI assistance was used to find the discrepancy, run measurements, and write the draft.

This repository's CONTRIBUTING requires DCO(Signed-off-by), PR titles in the Conventional Commits format, a changelog fragment for behavior-changing modifications (or skip-changelog label if docs-only), and "approval from 2 trusted reviewers" for external contributors' PRs. There were no specific repository rules regarding AI usage, so I read the parent foundation's generative AI guidelines and disclosed it truthfully. Since external contributors do not have permission to apply labels, I requested it in the body.

4. Reaction and response: A question and a dismissed approval

Time Day
10-27 early morning Found a trap in the experimental VM, measured documentation examples before and after (raw data file timestamps 01:46, 02:30, 02:38)
10-27 14:33–14:34 Issue #2531, published PR 42 seconds later (first version c871f61b, SSH signed)
10-27 19:28 Review (comment) from another contributor: confirmation that the code path matches the new description and a single question
10-28 21:39 Reviewer with collaborator permissions approves the first version
10-29 00:47 Commit reflecting the question 47bd30b5 pushed, approval automatically dismissed at that moment. Reply
10-29 00:54 Squashed both commits into a single SSH-signed commit (2c23833a) and force-updated
10-29 06:17 Commenting contributor says "2c23833a reads right"
11-01 01:21 Comment asking to re-review the dismissed approval
11-09 16:04–16:05 Maintainer approval, skip-changelog label
11-09 17:43–17:44 Entered merge queue, passed four checks, merged at 17:44:50 (e5f0f298), issue #2531 also closed

Question. Another contributor traced proportion.go·CRD·isOverLimit directly and confirmed that "the code matches the new phrasing," then asked a question, noting it was a question rather than a request for changes. Since the new memory block used quota: -1 , making even the memory guaranteed amountunlimited, whereas the GPU example writes "no guarantee" as quota: 0 , wouldn't quota: 0·limit: -1 be closer if the intent was only to lift the upper limit? It was a valid point. What we wanted was "do not set the upper limit to 0," not to change the guaranteed amount. Therefore, we changed the memory in the two examples that specify limits (Basic and Hierarchical) to quota: 0·limit: -1 , and left only the Unlimited example—where all resources are unlimited—as quota: -1 . The diff above is that final version.

There is no record of running this final version again on the experimental VM (unmeasured). The "after fixing" in Section 1 is the result from the first version where memory was quota: -1 . Since limit checks only look at limit , we assumed the results would be the same, supported by the fact that in the same measurement, the Unlimited example's gpu received 1 GPU as quota: 0·limit: -1 , but memory quota: 0 itself was not re-measured.

Dismissed approval. Three hours after a reviewer with collaborator permissions approved the first version, the commit reflecting the question was pushed, and GitHub's timeline recorded that the approval was immediately dismissed by that push. The commit that triggered the dismissal was 47bd30b5 . It appears that this repository uses a setting that invalidates previous approvals when new commits are uploaded (which could not be confirmed as I do not have permission to view repository settings). Seven minutes later, we combined the two commits into a single signed commit and pushed it again, noting in the re-approval request comment left on 11-01 that "squash push cleared the approvals." Looking at the event history again, it was not a squash, but the push right before it. Either way, a push after approval clears the approvals, but when stating the cause, I should have looked at the timeline first rather than relying on memory.

Waiting and merging. After the questions from the early morning of 10-29 were resolved, there were no new reviews for 11 days. On 11-09 at 16:04, a maintainer approved it, added a label one minute later, and placed it in the merge queue at 17:43. In the merge queue, the PR workflow, changelog check, PR title check, and DCO all passed within 14 seconds, and since it was a documentation-only PR, build and E2E checks were skipped as "Not Required." The author of the merge commit is our account, and the message includes the PR title at the end (#2532) along with the DCO signature attached.

On the PR page, two red checks remain as they were. The changelog check and the approval-counting check (Approval Gatekeeper) ran and failed during the 10-29 force-update, and ran again on 11-09 at 16:03, but failed again because it was 1 to 2 minutes before the approval and labels were attached. Who re-ran them could not be verified from the public records. The Approval Gatekeeper requires two approvals from organization members or collaborators for external contributor PRs, but at the time of merging, the only trusted approval that remained active was the maintainer's (the collaborator's approval had been dismissed, and the questioning contributor only left comments). Since this check did not run in the merge queue, it appears not to be a mandatory check blocking the merge. The application of the rules is up to the maintainer's judgment, and there was nothing further for us to handle.

As of 11-10, there is no release tag containing this commit. The v0.16.16 and v0.14.12 releases that came out after the merge are patch releases of older version branches, and this documentation change exists only on main. The documentation is read directly from main on GitHub.

5. What We Learned: Open Source Contribution Tips

A Word from TT. The school cafeteria card had a "daily limit" field, and she thought leaving it blank meant unlimited. But the cafeteria lady read the blank space as 0 won. So she couldn't even buy a single carton of milk! The notice only said, "Blank spaces default to the default value." When she suggested adding to the notice, "Blank spaces are 0 won; make sure to explicitly write -1 if you want it to be unlimited," a new notice was posted 13 days later. It was a bit disappointing that the stamp she had earned was wiped out just because she fixed a small typo in between, though.

Links

All materials were accessed on 2026-11-10.

Sign in to like

Comments

No comments yet.

Sign in to leave a comment