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
- 1. What Was the Problem: The Example Sets Couldn't Receive Anything
- 2. Cause: A Validation Check That Only Skips 0 and -1 for Types
- 3. What Was Changed: Diff Summary
- 4. Reaction and Response: A Question and a Cleared Approval
- 5. What Was Learned: Tips for Open Source Contributions
- Links
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 |
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.
- Documentation.
docs/queues/README.mdlines 61–67 's special value table writeslimit's0 or unsetas "No additional resources allowed (default)". Line 112 's example title is "Unlimited Queue - default value is -1". None of the three examples in lines 76–126 have amemoryblock. - Type.
scheduling/v2/resources.goLines 17–24 'sQueueResource.Limitis afloat64with no default value indicated. If left unspecified, it becomes Go's zero value of 0. The Queue CRD included in the chart also specifieslimitonly astype: numberwithoutdefault, so the API server does not fill in -1. - Moving values.
proportion.goLines 414–427 pass this queue'sLimitdirectly as the limit. Only the memory unit is changed tomath.Max(-1, limit*mebibytes), but anything multiplied by 0 is 0. - Validation.
max_allowed_check.goLines 51–66 'sisOverLimitruns separately for each resource, and skips only when the limit is exactly -1 (line 54). Otherwise, it checks whether "used amount + request" is less than or equal to the limit. If the limit is 0, it overflows the moment you request even a tiny bit of that resource. Since resources with a request of 0 are skipped (lines 58–60), a pod that does not request memory will pass even without a memory block.
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
- "What happens if left blank" should be verified in the code types and CRDs, not just the documentation. Go 구조체의 영값, CRD 의
default, nor anywhere in the webhook's default-filling logic, then a blank value evaluates to 0. Whether 0 means "none" or "block" can only be known by following the inspection functions that read that value all the way through. - Try applying documentation examples literally. The fact that all three examples failed was discovered by copying and pasting, not by reading the code. If there is a "before and after" fix table, neither the maintainer nor the reviewer has to mentally re-run the same experiment.
- Ask whether to change the behavior via an issue, and submit the documentation PR with one proposed answer. Changing the default value to -1 alters existing users' queue behavior, so it is not something for an external contributor to decide. If you outline both paths in the issue and note in the documentation PR that "you can close this if you choose the other path," the maintainer only needs to make a choice. This time, no reply was posted to the issue, and the issue was closed automatically when the documentation PR was merged.
- Questions are also worth incorporating. Even if a review says "this is a question, not a request," if it makes the meaning of the example more accurate, it should be accepted. Quota and limit both sound like "up to how much," but one is a guarantee and the other is an upper bound. Documentation examples must clearly show that difference.
- A push after approval clears the approvals. It is correct to fix things when something needs fixing, but losing one of the required two approvals in a repository that requires dual approval leads to a long wait. Review questions should be sorted out before getting approval, and squashing commits should be done all at once. What cleared the approval can be checked via the Timeline API(
issues/<번호>/timeline)'sreview_dismissedevents anddismissal_commit_id.
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.
- PR: kai-scheduler/KAI-Scheduler#2532 (Merge commit
e5f0f298) - Issue: kai-scheduler/KAI-Scheduler#2531
- Modified document (base commit):
docs/queues/README.md - Basis of behavior (base commit):
proportion.go,max_allowed_check.go, Queue CRD,kai-scheduler/apiv0.2.0resources.go - Contribution guidelines: CONTRIBUTING, Approval Gatekeeper workflow
- Other PRs for the same file: kai-scheduler/KAI-Scheduler#2275 MIG GPU calculation explanation (open as of 11-10)
- GitHub Timeline API: https://docs.github.com/en/rest/issues/timeline