結果: マージ完了 (2026-11-09 17:44, kai-scheduler/KAI-Scheduler#2532), issue #2531 も一緒にクローズ。 実際には GitHub の PR・issue・レビュー・タイムライン・チェックの記録、KAI-Scheduler のソース (PR のベースコミット
0243c4e9) と API モジュール (タグ v0.2.0)、 コントリビューション原簿の実測原データと下書き、TT Lab リポジトリのコースのコミットから取得しました。時刻は韓国標準時 (KST) です。
目次
- 一言まとめ
- 1. 何が問題だったのか: 例のセットがすべて何も受け付けない
- 2. 原因: 型の 0 と -1 のみをスキップするチェック
- 3. 何を変更したのか: diff の要点
- 4. 反応と対応: 1 つの質問と消えた承認
- 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 1 ファイル。特殊値の表の 1 行、説明の 1 段落、例のキューセットに不足している limit: -1 と memory のブロック。16 行追加、2 行削除 |
| 反応 | 他のコントリビューターによるコードパスの確認と 1 つの質問、コラボレーター権限を持つレビュアーによる承認 (後に自動解除)、メンテナーの承認と skip-changelog ラベル |
| 結果 | PR 公開 10-27 14:34 → マージ 11-09 17:44、約 13 日 3 時間 10 分。マージキューを経た squash マージ |
1. 何が問題だったのか: 例のセットがすべて何も受け付けない
KAI-Scheduler は GPU ワークロードのための Kubernetes スケジューラーです。Pod に schedulerName: kai-scheduler を記述すると、デフォルトのスケジューラーの代わりにこのスケジューラーが
配置を引き受け、Pod が属するキュー (Queue) の割当量を考慮してリソースを割り当てます。キューには、リソース (cpu・memory・gpu) ごとに保証量 quota と上限 limit があります。
TT Lab の学習サイトの「Kubernetes GPU スケジューリングと AI バッチ」コースを作成していた 10-27 の未明に、この罠を最初に踏みました。実験用 VM に k3s を 1 台立ち上げ、
ダミーの GPU 4 基 (fake-gpu-operator 0.2.1) と KAI-Scheduler v0.18.6 をデプロイした後、gpu quota: 1 に limit を記述していないキューを作成して、GPU 1 基分の Pod を投入しました。クォータが 1 だから 1 基分は受け付けられるだろうと思ったのですが、Pod は 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 に変更すると、待ち受けていた同じ Pod が配置されました (ハンズオン検証の記録 vfy-kai.log)。何秒で起動したかは計測していません。この出来事はコースのハンズオン 2 つ (「limit を空けたキューの罠」
、「limit に -1 と書いて直す」) となり、同日朝のコースのコミット (4cdcc0b3) のリーディング資料にも「limit は必ず -1 か数値を書いてください」と記載しました。
問題はドキュメントでした。キューのドキュメントの特殊値の表では、空けておいた limit を「追加リソースなし (デフォルト)」と説明しており、最後の例のタイトルは
「Unlimited Queue - default value is -1」です。読む側としては、空けておけばクォータまでは使用できるか、完全に無制限であると期待してしまいます。
そのため、ドキュメントの例のキューセットを 文字通りに 適用し、同じ Pod (requests: {cpu: 100m, memory: 32Mi}, limits: {nvidia.com/gpu: 1}) を
1 つずつ投入してみました。原データはコントリビューション原簿の kai-docs-test.log·kai-docs-after.log であり、Pod ごとに 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} を追加するだけで、待ち受けていた Pod が Running になりました。GPU の上限を記述した例も、「Unlimited」という
名前の例もメモリブロックがなかったため、メモリを少しでも要求する Pod を受け付けられなかったのです。Kubernetes でメモリを要求しない Pod は
稀であるため、このドキュメントを見て最初にキューを作成する人はほぼ確実にここでつまずきます。
環境は実験用 VM 1 台 (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 行目)、メモリを要求しない Pod であれば メモリブロックがなくても通過します。
要約すると、コードはそのままで辻褄が合っています。「空ければ 0、0 ならブロック、-1 のみ無制限」です。狂っていたのはドキュメントの説明と例でした。 そのため、issue には 2 つの方向性を併記しました。ドキュメントを現在の動作に合わせるか、意図されたデフォルト値が -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
(説明の段落は元々 1 行だったものを読みやすく 3 行に分割して記載しました。)
PR の本文には、第 1 節の修正前後の表、実測環境、「ドキュメントのみを変更し、コード・チャートはそのまま」という旨、 skip-changelog ラベルの付与のお願い、同一ファイルの別の
セクションを修正するオープンな PR #2275(MIG 計算の説明) が先にマージされた場合はリベースするという旨、
および AI の助けを借りて不一致を発見し、実測して下書きを作成した旨の告知を記載しました。
このリポジトリの CONTRIBUTING では DCO (Signed-off-by)、Conventional Commits 形式の PR タイトル、動作が変更される変更に対する変更履歴の断片 (ドキュメントのみの場合は
skip-changelog ラベル) が求められ、外部コントリビューターの PR には「信頼されたレビュアー 2 名の承認」が求められます。AI の使用に関するリポジトリのルールは特に
なかったため、上位財団の生成 AI ガイドラインを読み、事実通りに告知しました。外部コントリビューターにはラベルを付与する権限がないため、本文で依頼しました。
4. 反応と対応: 1 つの質問と消えた承認
| 時刻 | 日 |
|---|---|
| 10-27 未明 | 実験用 VM で罠を発見、ドキュメントの例のセットの修正前後を実測 (原データファイルの時刻 01:46・02:30・02:38) |
| 10-27 14:33–14:34 | issue #2531、42 秒後に PR 公開 (初版 c871f61b、SSH 署名) |
| 10-27 19:28 | 他のコントリビューターによるレビュー (コメント): コードパスが新しい説明と一致していることの確認と 1 つの質問 |
| 10-28 21:39 | コラボレーター権限を持つレビュアーが初版を承認 |
| 10-29 00:47 | 質問を反映したコミット 47bd30b5 を push、その瞬間に承認が自動解除。返信 |
| 10-29 00:54 | 2 つのコミットを 1 つの 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 | マージキューに入り 4 つのチェックを通過、17:44:50 にマージ (e5f0f298)、issue #2531 もクローズ |
質問。 他のコントリビューターは proportion.go・CRD・isOverLimit を直接辿って「コードが新しい文章と一致している」ことを確認した上で、要求ではなく質問として
1 点尋ねました。新しい memory ブロックが quota: -1 であるためメモリの 保証量まで無制限になるが、GPU の例は「保証なし」を quota: 0 と
記述しているため、上限のみを解除する意図であれば quota: 0·limit: -1 の方が近いのではないかという指摘です。もっともな指摘でした。私たちが望んでいたのは
「上限を 0 にしないこと」であり、保証量を変更することではありませんでした。そのため、上限を記述した 2 つの例 (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 自体を再計測したわけではありません。
消えた承認。 コラボレーター権限を持つレビュアーが初版を承認してから 3 時間後に質問反映コミットを上げ、GitHub のタイムラインにはまさにその push によって
承認が解除 (dismissed) された記録が残りました。解除を引き起こしたコミットは 47bd30b5 です。このリポジトリは新しいコミットがアップロードされると以前の承認を無効にする設定を使用しているようです(リポジトリの設定を見る権限がないため未確認)。7分後、私たちは2つのコミットを署名付きコミット1つにまとめて再アップロードし、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の画面には赤い検査が2つそのまま残っています。変更履歴の検査と承認の数を数える検査(Approval Gatekeeper)が10-29の強制更新時に実行されて失敗し、11-09 16:03に再び実行されましたが、承認とラベルが付く1〜2分前だったため再び失敗しました。誰が再実行したのかは公開記録から確認できませんでした。Approval Gatekeeperは外部コントリビューターのPRに対して組織のメンバーや協力者による2つの承認を求めますが、マージ時点で有効だった信頼された承認はメンテナーの1つでした(協力者の承認は解除されたままで、質問したコントリビューターは意見のみを残しました)。この検査はマージキューでは実行されなかったため、マージを妨げる必須の検査ではないようです。ルールの適用はメンテナーの判断であり、私たちが個別に対応すべき事柄ではありませんでした。
11-10の時点でこのコミットを含むリリースタグはありません。マージのあとに登場したv0.16.16やv0.14.12は古いバージョンの系統のパッチリリースであり、このドキュメントの変更はmainにのみ存在します。ドキュメントはGitHubのmainから直接読み込まれます。
5. 学んだこと:オープンソースコントリビューションのコツ
- 「空欄にするとどうなるか」はドキュメントではなく型とCRDで確認します。 Go 구조체의 영값, CRD 의
default、ウェブホックのデフォルト値の補完のどこにも-1がない場合、空欄の値は0です。0が「なし」なのか「ブロック」なのかは、その値を読み込む検証関数を最後まで追いかけなければわかりません。 - ドキュメントの例は文字通りに適用してみます。 3つの例がすべて失敗するという事実は、コードを読んで知ったのではなく、コピーして貼り付けて知りました。「修正前・修正後」の表があれば、メンテナーもレビュアーも同じ実験を頭の中でやり直す必要がありません。
- 動作を変更すべきかはissueで問いかけ、ドキュメントのどちらか一方の答えとしてPRを提出します。 デフォルト値を-1に変更することは既存ユーザーのキューの動作を変更する作業であり、外部コントリビューターが決めることではありません。issueに2つの道筋を書き、ドキュメントのPRに「別の道を選んだ場合は閉じてもよい」と書いておけば、メンテナーは選ぶだけで済みます。今回はissueに返信が寄せられず、ドキュメントのPRがマージされるとともにissueも閉じられました。
- 質問も反映する価値があります。 「リクエストではなく質問である」というレビューであっても、例の意味をより正確にするものであれば受け入れられます。quotaとlimitはどちらも「どこまでか」のように聞こえますが、一方は保証であり、もう一方は上限です。ドキュメントの例はその違いをそのまま示さなければなりません。
- 承認のあとのpushは承認を消します。 修正すべき箇所が生じたら修正するのが当然ですが、2つの承認が必要なリポジトリで1つを失うと待ち時間が長くなります。レビューの質問は承認の前に整理し、コミットをまとめる作業も一度に終わらせます。何が承認を消したのかはタイムラインAPI(
issues/<번호>/timeline)のreview_dismissedイベントとdismissal_commit_idで確認します。
ティティの一言。 学校の売店のカードに「1日の限度額」の欄があったのですが、空欄にしておけば無制限だと思ったそうです。でも、売店のおばちゃんは空欄を0円として読み取りました。だから牛乳一本も買えませんでした!案内文には「空欄はデフォルト値」とだけ書かれていました。案内文に「空欄は0円です、無制限なら-1と必ず書いてください」を追加しようと言ったら、13日後に新しい案内文が貼り出されました。途中の落書きを一つ直したせいで、もらっていたスタンプが消されてしまったのはちょっと惜しかったですが。
リンク
すべての資料は2026-11-10に閲覧しました。
- PR: kai-scheduler/KAI-Scheduler#2532 (マージコミット
e5f0f298) - issue: 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