Skip to content

parallel-measure capacity: 予備の二重計上と SwapFree の残量判定が重なり、メモリ圧の無いホストでも並列数が 1 に固定される #780

Description

@takemi-ohama

何を見つけたか

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.pyrun_capacity / lanes_by_memoryDEFAULT_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.pytest_capacity_reports_the_measured_valuesswap_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/memorysome avg10--psi-some-max(初期値 10)を超えたら hold(今の本数を超えて足さない)。PSI が無いカーネルでは /proc/vmstatpswpin + pswpout--sample-seconds(初期値 2)挟んで 2 回読み、--swap-io-max-pages-per-sec(初期値 512)を超えたら hold残量の判定(swap_low)は廃止する
総本数の上限 定数 3 廃止する。 1 本の重さを測れるなら、上限は 空き ÷ 重さ が持つ。--max は引数として残すが既定を無くし、渡したときだけ limited_bymax が付く
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.statanon である。 memory.current はページキャッシュ(file)を含み、テストや git の操作で膨らんで戻るため、1 本の重さとしては当てにならない。anon はこのホストで 5 秒間の揺れが 3 MiB だった。

何を どこに 誰が
0 本のときの anoncgroup_anon_mib 実行計画の先頭「anon の起点」(oom_kill の起点 と並べる) conductor が開始時の capacity の出力から写す
いまの anon・測った 1 本の重さ・使った 1 本の重さ 「測った値」の表の列 anon / 1 本の実測 / 1 本の見込み capacitycgroup_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 が付き、allowedmax(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_bymax_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 本」と記録されている。

関連

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area: ndf-skillNDF の Skill 本体bugSomething isn't workingpriority: high実害・安全機構の欠落など、優先して対応する

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions