何を見つけたか
parallel-measure.py capacity(v10.15.1)が、メモリ圧の無いホストで並列数を 1 に固定している。 実行計画 13(issues/execution-plan-13.md、2026-09-19T02:20Z)の測定は「空き 6455 MiB・swap_low・起動してよい本数 1」で、同じ VM で動く他のワークフローも同じ結果だった。しかし同じ時刻の VM は、メモリの stall を測る PSI が 0.00、スワップの出入りが 10 秒で 6 ページで、圧は無い。
2026-09-19 02:30Z 前後に、このホスト(Docker Desktop の VM、MemTotal 21.5 GiB・9 CPU)で測った値:
| 見た値 |
結果 |
MemAvailable |
5793 MiB |
SwapTotal / SwapFree |
2047 / 0 MiB |
/proc/pressure/memory some / full の avg10・avg60・avg300 |
すべて 0.00 |
pswpin + pswpout の 10 秒間の増分 |
6 ページ |
このコンテナの cgroup memory.current / memory.peak / memory.max |
2642 MiB / 9422 MiB / max |
VM 全体の AnonPages のうち、このコンテナの memory.stat anon |
14.6 GiB のうち 2.1 GiB |
capacity --running 0 |
by_memory=1 allowed=1 limited_by=memory,swap_low,floor |
capacity --running 1 |
by_memory=2 allowed=1 limited_by=memory,swap_low(追加できない) |
capacity --running 2 |
by_memory=3 allowed=2 limited_by=memory,max,swap_low |
1 になる経路は 3 つの控えめな判定の重なりである。 どれか 1 つなら妥当だが、3 つが同時に効くと、1 本の実測(約 1.2 GiB、#621)に対して 最初の 1 本を足すのに 6 GiB 超の空きを要求する。
| # |
判定 |
いま起きていること |
| 1 |
予備 2048 MiB を空きから先に引く |
予備の根拠は「進行側の claude 本体(約 470 MiB)と他プロセスの揺れ」だが、本体も動いている担当も既に MemAvailable から引かれている。既にあるものを予備でもう一度引いている |
| 2 |
1 本の見込み 2048 MiB で割る |
実測 1.2 GiB に「テストとコンテナの山」を足した値。#1 の予備と足し合わさるため、1 本目に 4 GiB を要求する形になる |
| 3 |
SwapFree が 25% を下回れば 1 減らす |
SwapFree は今の圧ではなく過去の履歴である。 一度スワップへ追い出された冷たいページは、触られるか持ち主が終わるまで戻らないため、この VM では SwapFree が 0 のまま張り付く。PSI 0.00・スワップ I/O ほぼ 0 でも毎回 swap_low が付き、実質「常に 1 本分の追加の余裕を要求する」になっている |
swap_low の閾値 25% は 2026-09-17 の 1 点(空き 9.5 GiB・スワップの空き 15% → 2 本)で進行側の選択と一致するように決めた(確定仕様の表)。2 日後の空き 5.8 GiB・スワップの空き 0% では同じ判定が 1 本を返し、進行側の期待(2〜3 本)と食い違う。1 点で合わせた閾値が別の点で合わないので、閾値の値ではなく見ている量が違う。
どこで見つけたか
plugins/ndf/scripts/parallel-measure.py の run_capacity / lanes_by_memory(DEFAULT_RESERVE_MIB / DEFAULT_PER_LANE_MIB / DEFAULT_SWAP_FREE_MIN_PCT)
docs/specifications/ndf-execution-plan-and-parallel-capacity.md の「決定と理由」の初期値の表
issues/execution-plan-13.md の「測った値」(1 行目)
plugins/ndf/scripts/tests/test_parallel_measure.py の test_capacity_reports_the_measured_values(swap_low を期待している)
直さないと何が起きるか
提案する決め方
方針: 定数の当て推量を実測に置き換え、固定の上限を無くす。 決める量は 3 つで、それぞれ「何を測って決めるか」を持つ。測れない環境だけ引数の初期値へ落ちる。
| 決める量 |
いまの決め方 |
提案 |
| 足せる本数の元になる空き |
min(MemAvailable, cgroup の残り) − 予備 2048 |
同じ式。予備は「他プロセスの揺れ」だけにし、既に動いている本体と担当を含めない(MemAvailable が既に引いている)。初期値を 1024 MiB へ下げ、--reserve-mib はそのまま残す |
| 1 本の重さ |
定数 2048 MiB(実測 1.2 GiB に山を足した値) |
動いている担当から測る。 (いまの cgroup anon − 0 本のときの anon) ÷ running を 1 本の平均とし、山への余裕 --peak-factor(初期値 1.5)を掛ける。0 本のときだけ --per-lane-mib(初期値 1536)を使う。下限 --per-lane-min-mib(初期値 512)を割らない |
| 圧の判定 |
SwapFree が 25% を下回れば 1 減らす |
PSI で「いま stall しているか」を見る。 /proc/pressure/memory の some avg10 が --psi-some-max(初期値 10)を超えたら hold(今の本数を超えて足さない)。PSI が無いカーネルでは /proc/vmstat の pswpin + pswpout を --sample-seconds(初期値 2)挟んで 2 回読み、--swap-io-max-pages-per-sec(初期値 512)を超えたら hold。残量の判定(swap_low)は廃止する |
| 総本数の上限 |
定数 3 |
廃止する。 1 本の重さを測れるなら、上限は 空き ÷ 重さ が持つ。--max は引数として残すが既定を無くし、渡したときだけ limited_by に max が付く |
| 1 回の見直しで足す数 |
制限なし(上限 3 が実質の制限) |
--max-add(初期値 2)。 総数の上限ではなく、測る前に足す数の制限である。担当の重さは動き出すまで測れないため、足したら次の見直しで測ってからまた足す。0 本からの初回だけ見込みで動くことを許す形にする |
| 落ちた後の縮め方 |
oom_kill 増で running − 1 |
変えない |
判定を 3 段にする
| 段 |
条件 |
allowed |
limited_by |
grow |
圧なし・oom_kill 増なし |
running + min(--max-add, ⌊(空き − 予備) ÷ 1 本の重さ⌋)(--max があれば小さい方) |
memory / max_add / max |
hold |
PSI またはスワップ I/O が閾値を超えた |
max(running, 1) |
hold_psi / hold_swap_io |
shrink |
oom_kill が起点より増えた |
max(0, running − 1)(既存) |
oom_kill_increased |
いまの swap_low は圧の大きさに関係なく 1 本分(2 GiB 相当)を要求し、floor と重なると意味が読めない。段に分けると、表の 1 行から「なぜその本数か」が読める。
1 本の重さを測る仕組み
測る量は cgroup の memory.stat の anon である。 memory.current はページキャッシュ(file)を含み、テストや git の操作で膨らんで戻るため、1 本の重さとしては当てにならない。anon はこのホストで 5 秒間の揺れが 3 MiB だった。
| 何を |
どこに |
誰が |
0 本のときの anon(cgroup_anon_mib) |
実行計画の先頭「anon の起点」(oom_kill の起点 と並べる) |
conductor が開始時の capacity の出力から写す |
いまの anon・測った 1 本の重さ・使った 1 本の重さ |
「測った値」の表の列 anon / 1 本の実測 / 1 本の見込み |
capacity が cgroup_anon_mib / per_lane_observed_mib / per_lane_used_mib として出し、conductor が写す |
| 起点 |
--anon-baseline-mib <anon の起点> |
conductor が --oom-baseline と同じように渡す |
running ≥ 1 かつ起点があるときだけ実測を使う。同じコンテナで動く無関係のセッションは実測を重く見せる方向に働く(anon が増えるが running に数えない)ので、外れるのは安全側である。
まとまりを閉じるとき(concurrency)に「測った値」の表の 1 本の実測 の最大を「閉じたときの測定」へ写せば、次のまとまりの --per-lane-mib の根拠になる。初期値 1536 はこの列が溜まるまでの暫定値である。
試算
2026-09-19 のこのホスト(空き 5793 MiB・PSI 0.00・anon 2193 MiB)で、起点を 2193 とし、担当 1 本が 1.2 GiB を使ったと仮定した見直しの列:
| 見直し |
running |
anon |
1 本の実測 × 1.5 |
空き |
足せる数 |
allowed |
決めた条件 |
| 開始 |
0 |
2193 |
—(見込み 1536) |
5793 |
⌊(5793−1024)÷1536⌋ = 3 → 2 |
2 |
memory, max_add |
| 1 回目 |
2 |
4650 |
1229 × 1.5 = 1843 |
3336 |
⌊(3336−1024)÷1843⌋ = 1 |
3 |
memory |
| 2 回目 |
3 |
5880 |
1843 |
2106 |
0 |
3 |
memory |
いまの既定は同じ入力で 1 本のまま動かない。上限 3 を外しても、空き 5.8 GiB のホストでは重さの実測が 3 本で止める。空き 12 GiB のホストなら同じ手順で 5〜6 本まで伸び、5〜6 本で落ちた #621 の状況は「1 本の重さを測らずに一度に起動した」ことが原因なので、--max-add と見直しごとの実測が代わりに受ける。
変えないこと・範囲外
- 測定は拒否しない(設計の決定)は変えない。
hold も案内であって、起動を止めるのは手順である
- 「コンテナを起動する検査を持つ担当は同時に 1 本」(
parallel-work.md 下限 6)は変えない。コンテナは担当の cgroup の外で動き、anon に現れない
- 同じ VM で複数の進行側が同時に測ると、同じ空きを見て同時に起動する競合がある。今回は扱わず、起きたら別に起票する
/proc/meminfo・cgroup v2・PSI のどれも無い環境(macOS など)は、いままでどおり終了コード 3 で「測れない」とし、1 本で進める
受け入れ条件の案
- PSI 0.00・スワップ I/O ほぼ 0・
SwapFree 0 の入力で減点が付かず、grow の本数が出る(swap_low が出力に現れない)
- PSI の
some avg10 が閾値を超える入力、または PSI が無くスワップ I/O が閾値を超える入力で hold が付き、allowed が max(running, 1) になる
--anon-baseline-mib と --running ≥ 1 を渡すと、per_lane_observed_mib が (anon − 起点) ÷ running になり、per_lane_used_mib がそれに --peak-factor を掛けて --per-lane-min-mib で下から抑えた値になる。起点が無いか --running 0 なら --per-lane-mib を使う
--max を渡さないと総本数の上限が掛からず、--max-add が 1 回の見直しで足す数を抑える。limited_by に max_add が付く
- 出力に
cgroup_anon_mib / per_lane_observed_mib / per_lane_used_mib が増え、execution-plan.md の先頭に「anon の起点」、「測った値」の表に 3 列が増える
- 確定仕様の初期値の表が書き換わり、「予備に既に動いているものを含めない」「上限は測定が持つ」が明文化される
test_parallel_measure.py の 2026-09-17 の例が新しい判定の期待値に更新され、3 段(grow / hold / shrink)それぞれの検査がある
由来
v10.15.1 の配布後、実行計画 13(マイルストーン 13)の起動時の測定で並列数 1 が続いたことの調査。実行計画 13 の前提にも「スワップが 0 MiB で swap_low が掛かり、初回は 1 本」と記録されている。
関連
何を見つけたか
parallel-measure.py capacity(v10.15.1)が、メモリ圧の無いホストで並列数を 1 に固定している。 実行計画 13(issues/execution-plan-13.md、2026-09-19T02:20Z)の測定は「空き 6455 MiB・swap_low・起動してよい本数 1」で、同じ VM で動く他のワークフローも同じ結果だった。しかし同じ時刻の VM は、メモリの stall を測る PSI が 0.00、スワップの出入りが 10 秒で 6 ページで、圧は無い。2026-09-19 02:30Z 前後に、このホスト(Docker Desktop の VM、MemTotal 21.5 GiB・9 CPU)で測った値:
MemAvailableSwapTotal/SwapFree/proc/pressure/memorysome / full の avg10・avg60・avg300pswpin + pswpoutの 10 秒間の増分memory.current/memory.peak/memory.maxAnonPagesのうち、このコンテナのmemory.stat anoncapacity --running 0by_memory=1 allowed=1 limited_by=memory,swap_low,floorcapacity --running 1by_memory=2 allowed=1 limited_by=memory,swap_low(追加できない)capacity --running 2by_memory=3 allowed=2 limited_by=memory,max,swap_low1 になる経路は 3 つの控えめな判定の重なりである。 どれか 1 つなら妥当だが、3 つが同時に効くと、1 本の実測(約 1.2 GiB、#621)に対して 最初の 1 本を足すのに 6 GiB 超の空きを要求する。
MemAvailableから引かれている。既にあるものを予備でもう一度引いているSwapFreeが 25% を下回れば 1 減らすSwapFreeは今の圧ではなく過去の履歴である。 一度スワップへ追い出された冷たいページは、触られるか持ち主が終わるまで戻らないため、この VM ではSwapFreeが 0 のまま張り付く。PSI 0.00・スワップ I/O ほぼ 0 でも毎回swap_lowが付き、実質「常に 1 本分の追加の余裕を要求する」になっているswap_lowの閾値 25% は 2026-09-17 の 1 点(空き 9.5 GiB・スワップの空き 15% → 2 本)で進行側の選択と一致するように決めた(確定仕様の表)。2 日後の空き 5.8 GiB・スワップの空き 0% では同じ判定が 1 本を返し、進行側の期待(2〜3 本)と食い違う。1 点で合わせた閾値が別の点で合わないので、閾値の値ではなく見ている量が違う。どこで見つけたか
plugins/ndf/scripts/parallel-measure.pyのrun_capacity/lanes_by_memory(DEFAULT_RESERVE_MIB/DEFAULT_PER_LANE_MIB/DEFAULT_SWAP_FREE_MIN_PCT)docs/specifications/ndf-execution-plan-and-parallel-capacity.mdの「決定と理由」の初期値の表issues/execution-plan-13.mdの「測った値」(1 行目)plugins/ndf/scripts/tests/test_parallel_measure.pyのtest_capacity_reports_the_measured_values(swap_lowを期待している)直さないと何が起きるか
--running 1でallowed=1になるため、1 本目が終わるまで 2 本目を起動できない。並行度を上げるための実行計画が、測定によって 1 本に押し戻されるswap_lowが一度付くと外れないので、利用者が--swap-free-min-pct 0を毎回付けるか、既定を疑わずに 1 本で進めるかの二択になるmemory.current)が残らないため、確定仕様が言う「1 本の見込みは測った値の表から直す」ができない。空きだけでは 1 本あたりの実測を導けない提案する決め方
方針: 定数の当て推量を実測に置き換え、固定の上限を無くす。 決める量は 3 つで、それぞれ「何を測って決めるか」を持つ。測れない環境だけ引数の初期値へ落ちる。
min(MemAvailable, cgroup の残り) − 予備 2048MemAvailableが既に引いている)。初期値を 1024 MiB へ下げ、--reserve-mibはそのまま残す(いまの cgroup anon − 0 本のときの anon) ÷ runningを 1 本の平均とし、山への余裕--peak-factor(初期値 1.5)を掛ける。0 本のときだけ--per-lane-mib(初期値 1536)を使う。下限--per-lane-min-mib(初期値 512)を割らないSwapFreeが 25% を下回れば 1 減らす/proc/pressure/memoryのsome avg10が--psi-some-max(初期値 10)を超えたらhold(今の本数を超えて足さない)。PSI が無いカーネルでは/proc/vmstatのpswpin + pswpoutを--sample-seconds(初期値 2)挟んで 2 回読み、--swap-io-max-pages-per-sec(初期値 512)を超えたらhold。残量の判定(swap_low)は廃止する空き ÷ 重さが持つ。--maxは引数として残すが既定を無くし、渡したときだけlimited_byにmaxが付く--max-add(初期値 2)。 総数の上限ではなく、測る前に足す数の制限である。担当の重さは動き出すまで測れないため、足したら次の見直しで測ってからまた足す。0 本からの初回だけ見込みで動くことを許す形にするoom_kill増でrunning − 1判定を 3 段にする
allowedlimited_bygrowoom_kill増なしrunning + min(--max-add, ⌊(空き − 予備) ÷ 1 本の重さ⌋)(--maxがあれば小さい方)memory/max_add/maxholdmax(running, 1)hold_psi/hold_swap_ioshrinkoom_killが起点より増えたmax(0, running − 1)(既存)oom_kill_increasedいまの
swap_lowは圧の大きさに関係なく 1 本分(2 GiB 相当)を要求し、floorと重なると意味が読めない。段に分けると、表の 1 行から「なぜその本数か」が読める。1 本の重さを測る仕組み
測る量は cgroup の
memory.statのanonである。memory.currentはページキャッシュ(file)を含み、テストや git の操作で膨らんで戻るため、1 本の重さとしては当てにならない。anonはこのホストで 5 秒間の揺れが 3 MiB だった。anon(cgroup_anon_mib)oom_kill の起点と並べる)capacityの出力から写すanon・測った 1 本の重さ・使った 1 本の重さanon/1 本の実測/1 本の見込みcapacityがcgroup_anon_mib/per_lane_observed_mib/per_lane_used_mibとして出し、conductor が写す--anon-baseline-mib <anon の起点>--oom-baselineと同じように渡すrunning ≥ 1かつ起点があるときだけ実測を使う。同じコンテナで動く無関係のセッションは実測を重く見せる方向に働く(anonが増えるがrunningに数えない)ので、外れるのは安全側である。まとまりを閉じるとき(
concurrency)に「測った値」の表の1 本の実測の最大を「閉じたときの測定」へ写せば、次のまとまりの--per-lane-mibの根拠になる。初期値 1536 はこの列が溜まるまでの暫定値である。試算
2026-09-19 のこのホスト(空き 5793 MiB・PSI 0.00・
anon2193 MiB)で、起点を 2193 とし、担当 1 本が 1.2 GiB を使ったと仮定した見直しの列:いまの既定は同じ入力で 1 本のまま動かない。上限 3 を外しても、空き 5.8 GiB のホストでは重さの実測が 3 本で止める。空き 12 GiB のホストなら同じ手順で 5〜6 本まで伸び、5〜6 本で落ちた #621 の状況は「1 本の重さを測らずに一度に起動した」ことが原因なので、
--max-addと見直しごとの実測が代わりに受ける。変えないこと・範囲外
holdも案内であって、起動を止めるのは手順であるparallel-work.md下限 6)は変えない。コンテナは担当の cgroup の外で動き、anonに現れない/proc/meminfo・cgroup v2・PSI のどれも無い環境(macOS など)は、いままでどおり終了コード 3 で「測れない」とし、1 本で進める受け入れ条件の案
SwapFree0 の入力で減点が付かず、growの本数が出る(swap_lowが出力に現れない)some avg10が閾値を超える入力、または PSI が無くスワップ I/O が閾値を超える入力でholdが付き、allowedがmax(running, 1)になる--anon-baseline-mibと--running ≥ 1を渡すと、per_lane_observed_mibが(anon − 起点) ÷ runningになり、per_lane_used_mibがそれに--peak-factorを掛けて--per-lane-min-mibで下から抑えた値になる。起点が無いか--running 0なら--per-lane-mibを使う--maxを渡さないと総本数の上限が掛からず、--max-addが 1 回の見直しで足す数を抑える。limited_byにmax_addが付くcgroup_anon_mib/per_lane_observed_mib/per_lane_used_mibが増え、execution-plan.mdの先頭に「anon の起点」、「測った値」の表に 3 列が増えるtest_parallel_measure.pyの 2026-09-17 の例が新しい判定の期待値に更新され、3 段(grow/hold/shrink)それぞれの検査がある由来
v10.15.1 の配布後、実行計画 13(マイルストーン 13)の起動時の測定で並列数 1 が続いたことの調査。実行計画 13 の前提にも「スワップが 0 MiB で
swap_lowが掛かり、初回は 1 本」と記録されている。関連